diff --git a/docs/internal/FAITHFUL_TIMING_PLAN.md b/docs/internal/FAITHFUL_TIMING_PLAN.md index 0c8ccf1d..6c06956b 100644 --- a/docs/internal/FAITHFUL_TIMING_PLAN.md +++ b/docs/internal/FAITHFUL_TIMING_PLAN.md @@ -213,6 +213,56 @@ on a fixed region -> next. ## 5. Status / Log (update every session) +- **2026-07-21 (VLC load-charge batching — shipped; dual still ~22 ms):** + Runtime-only batch: under `psx_next_service_cycle`, `psx_cyc_charge` + accumulates into `g_psx_cyc_batch` (no per-insn `psx_cycle_count` store); + flush at IRQ check / MMIO sync / savestate / advance-past-deadline. + Absorb/fudge still per-insn; guest totals at barriers unchanged; no MotK + regen. Dual headless MotK (`PSX_NETPLAY_TIMING=1`, pinned halves): heavy + 25–50 band host med ≈39.7 fps / guest ≈21.9 ms/f / admit ≈3.1 (guest peer + ≈39.8 / 22.5 / 2.8) — same floor as pre-batch (~21 ms / ~40–42). Counter + publish was not the dual-peer tax; residual remains load-delay volume / + LLC under phase-locked VLC. Next: PGO retrain after hot-path edits, or + accept same-machine lockstep FMV floor. +- **2026-07-21 (MotK FMV host cost — MDEC/IRQ/charge; dual still ~21 ms):** + Aimed to cut MotK FMV host work so two lockstep peers fit ~16.7 ms/f. + Shipped bit-exact host opts: MDEC MB output reserve (no per-byte + ensure_capacity), sparse-column IDCT, ch0 DMA burst feed + (`mdec_dma_write_words`), sticky-IRQ undeliverable early-out in + `psx_check_interrupts`, `psx_cyc_charge` pre-deadline bump on compiled + loads/steps. Dual headless MotK (`PSX_NETPLAY_TIMING=1`, pinned halves): + band 25–50 fps still guest ≈21 ms/f / fps ≈40–42 (admit ≈2.5–3) — same + floor as before. HARD_CAP 16K→64K tried, no gain (real CD/timer events + already shorten the deadline); reverted. Residual is phase-locked dual + VLC load-delay volume / LLC contention, not MDEC FIFO or present/admit. + Next: emitter-level load-charge batching for VLC leaves, PGO retrain + after these hot-path edits, or accept same-machine lockstep FMV floor. +- **2026-07-21 (netplay FMV — lockstep guest inflation, not present/admit):** + User A/B: two windowed offline MotK intros fine; headless netplay FMV still + slow vs offline headless. Opt-in `PSX_NETPLAY_TIMING=1` splits the [FPS] + line into guest ms/f vs admit ms/f. Heavy FMV (~25–50 fps samples): guest + ≈21 ms/f, admit ≈3 ms/f (median) — frame time is dominated by the guest + quantum under phase-locked dual MDEC, not Swap and not INPUT_CONFIRM wait. + Offline headless same stretch is ~17 ms/f (≈57 fps). Pipelined CONFIRM + tried in recomp-net (tip publish, drain next tick) — no FPS gain on + localhost (~41→~41.5); reverted. Present-path / half-rate work is a dead + end for this regression. Next lever: reduce MotK FMV host cost so two + aligned peers fit a 16.7 ms budget (or accept same-machine lockstep floor). +- **2026-07-21 (netplay FMV — restore present-before-admit):** + User confirmed early same-machine netplay FMV was ~50–60 and gameplay + under lockstep stays ~60 — so the rematch-safe `finish→admit→pace→present` + order was the FMV regression (expensive depth24 CPU present after admit). + Restored `finish→present→admit/pace` via RAII `NetplayVblankTail` (admit + on every return path; offline still paces before present). Kept half-rate + depth24 present skip + UDP `poll()` barrier; no `SDL_Delay(0)`. Verify + MotK intro FPS + tick-0 arm after rebuild. +- **2026-07-21 (netplay FMV — re-land half-rate depth24 present):** + Windowed same-machine MotK netplay FMV was back at ~30–40 after the + rematch-safe `finish→admit→pace→present` order. Re-landed host-only + half-rate depth24 present: after admit, skip pace+Swap every other + depth24 vblank (present first, then alternate); admit/barrier unchanged + (no `SDL_Delay(0)` / present-before-admit). Offline path untouched. + Rebuild MotK `build-release` + verify intro FPS and tick-0 arm. - **2026-07-21 (lobby game_version + release pins):** WS lobby now carries `game_version` alongside `game_name` (create/list/join). Server rejects `version_mismatch` / `game_mismatch`; list can filter by either. diff --git a/runtime/include/psx_cyc.h b/runtime/include/psx_cyc.h index 2744fc2b..1e18923e 100644 --- a/runtime/include/psx_cyc.h +++ b/runtime/include/psx_cyc.h @@ -38,11 +38,40 @@ extern "C" { #endif +/* Load-charge batching (MotK VLC): under the published deadline, accumulate + * into g_psx_cyc_batch instead of storing psx_cycle_count every insn. Flush + * at IRQ edges / MMIO (psx_cyc_batch_flush). Absorb/fudge state still updates + * per insn — only the host counter publish is deferred. */ +static inline void psx_cyc_charge(uint32_t cycles) { + if (cycles == 0u) return; +#if defined(__GNUC__) || defined(__clang__) + if (__builtin_expect(g_ls_replay_active | g_event_step_conservative, 0)) { +#else + if (g_ls_replay_active || g_event_step_conservative) { +#endif + psx_advance_cycles(cycles); + return; + } + if (psx_in_device_service) { + psx_cycle_count += (uint64_t)cycles; + return; + } + uint64_t next = psx_cycle_count + (uint64_t)g_psx_cyc_batch + (uint64_t)cycles; + if (psx_next_service_cycle != 0u && next < psx_next_service_cycle) { + uint32_t sum = g_psx_cyc_batch + cycles; + if (sum >= g_psx_cyc_batch) { /* no uint32 wrap */ + g_psx_cyc_batch = sum; + return; + } + } + psx_advance_cycles(cycles); /* publishes any pending batch first */ +} + /* §1 base (Beetle cpu.cpp:795-798). */ static inline void psx_cyc_base(CPUState* cpu) { uint8_t w = cpu->read_absorb_which; if (cpu->read_absorb[w]) cpu->read_absorb[w]--; - else psx_advance_cycles(1u); + else psx_cyc_charge(1u); } /* GPR_DEPRES (Beetle cpu.cpp:702-705): zero ReadAbsorb[n] for every source/dest @@ -114,7 +143,7 @@ static inline uint32_t psx_cyc_load_word(CPUState* cpu, uint32_t addr, cpu->read_absorb_which = 0u; uint32_t fudge = (uint32_t)((cpu->read_fudge >> 4) & 2u); cpu->ld_absorb = 5u; /* main-RAM wait 3 + completion 2 */ - psx_advance_cycles(fudge + 5u); + psx_cyc_charge(fudge + 5u); cpu->ld_which_t = (uint8_t)rt; } uint32_t off = phys & 0x1FFFFFu; @@ -146,7 +175,7 @@ static inline uint16_t psx_cyc_load_half(CPUState* cpu, uint32_t addr, cpu->read_absorb_which = 0u; uint32_t fudge = (uint32_t)((cpu->read_fudge >> 4) & 2u); cpu->ld_absorb = 5u; - psx_advance_cycles(fudge + 5u); + psx_cyc_charge(fudge + 5u); cpu->ld_which_t = (uint8_t)rt; } uint32_t off = phys & 0x1FFFFFu; diff --git a/runtime/include/psx_cycles.h b/runtime/include/psx_cycles.h index 9e0f933a..0f009cf4 100644 --- a/runtime/include/psx_cycles.h +++ b/runtime/include/psx_cycles.h @@ -35,6 +35,12 @@ void psx_cycles_pc_sample_fire(void); /* Lockstep replay flag (defined in dirty_ram_interp.c). */ extern int g_ls_replay_active; +/* Deferred under-deadline charges (MotK VLC load-charge batching). + * psx_cyc_charge accumulates here; publish via psx_cyc_batch_flush / + * psx_advance_cycles before IRQ checks, MMIO, or any cycle read that must + * match the published counter. Guest totals at those barriers are unchanged. */ +extern uint32_t g_psx_cyc_batch; + /* Advance guest time. The common production path is inlined: bump the * counter and only service devices when the next event deadline is due. * Guest-visible timing is unchanged (service_to_now replays exact events). @@ -44,6 +50,22 @@ extern int g_ls_replay_active; * per-instruction charge. MotK VLC issues millions of advances/s; two add+ * branch pairs there were pure host tax. */ static inline void psx_advance_cycles(uint32_t cycles) { +#if !defined(PSX_COSIM) + if (g_psx_cyc_batch) { + uint32_t b = g_psx_cyc_batch; + g_psx_cyc_batch = 0; + if (cycles <= UINT32_MAX - b) cycles += b; + else { + /* Extreme: publish b first, then continue with cycles. */ + psx_cycle_count += (uint64_t)b; + if (!psx_in_device_service && + (psx_next_service_cycle == 0u || + psx_cycle_count >= psx_next_service_cycle)) { + psx_devices_service_to_now(); + } + } + } +#endif if (cycles == 0u) return; #if defined(PSX_COSIM) psx_advance_cycles_slow(cycles); @@ -69,7 +91,17 @@ static inline void psx_advance_cycles(uint32_t cycles) { #endif } -/* Read accessor for telemetry. */ +/* Publish deferred charges (IRQ edge / MMIO / savestate). */ +static inline void psx_cyc_batch_flush(void) { +#if !defined(PSX_COSIM) + uint32_t b = g_psx_cyc_batch; + if (!b) return; + g_psx_cyc_batch = 0; + psx_advance_cycles(b); +#endif +} + +/* Read accessor for telemetry (includes deferred batch). */ uint64_t psx_get_cycle_count(void); /* Idle-loop cycle skip (see psx_cycles.c "Idle-loop cycle skip"). */ diff --git a/runtime/src/boot_state.c b/runtime/src/boot_state.c index 048908d1..b4acb417 100644 --- a/runtime/src/boot_state.c +++ b/runtime/src/boot_state.c @@ -1,6 +1,7 @@ #include "boot_state.h" #include "overlay_api.h" /* PSX_OVERLAY_CODEGEN_HASH / _ABI_TAG / _CODEGEN_VER */ #include "gpu_render.h" /* gr_vram_transfer_in / gr_vram_transfer_out */ +#include "psx_cycles.h" #include "pst_wire.h" #include #include @@ -196,6 +197,8 @@ int boot_state_save(const CPUState* cpu, uint32_t bios_checksum, uint8_t cyc[8]; PstW w; pst_w_init(&w, cyc, sizeof cyc); + /* Publish deferred load-charge batch before snapshotting the clock. */ + psx_cyc_batch_flush(); ok = pst_w_u64(&w, psx_cycle_count) && write_section(f, BS_SEC_CLOCK, cyc, 8); } diff --git a/runtime/src/interrupts.c b/runtime/src/interrupts.c index fb2fb5ea..c44af965 100644 --- a/runtime/src/interrupts.c +++ b/runtime/src/interrupts.c @@ -731,6 +731,7 @@ int psx_interrupt_delivery_needed(const CPUState* cpu) { } void psx_check_interrupts(CPUState* cpu) { + psx_cyc_batch_flush(); extern int g_ls_suppress_record; #define PSX_CHECK_INTERRUPTS_RETURN() do { if (g_ls_suppress_record > 0) g_ls_suppress_record--; return; } while (0) #ifdef PSX_COSIM @@ -739,6 +740,24 @@ void psx_check_interrupts(CPUState* cpu) { #define COSIM_IRQ_NOTE(kind_) cosim_irq_note(cpu, (kind_), COSIM_IRQ_TAKE_PC(), g_dirty_safe_resume_pc, s_compiled_interrupt_resume_pc, cpu->cop0[COP0_SR]) #endif + /* MotK VLC / FMV hot edge: sticky unmasked I_STAT (CD/VBlank) while + * IEc or IM2 is clear — no architectural delivery possible. Skip the + * mid-path bookkeeping / irq_deliver_eval that used to run every BB. + * Guest-visible timing unchanged (same non-delivery). LTO can collapse + * this into the VLC call sites. */ + static uint32_t s_fast_maintenance; + if (!in_exception && !s_defer_switch_pending && (i_stat & i_mask) != 0) { + uint32_t sr = cpu->cop0[COP0_SR]; + if (!(sr & 0x01u) || !(sr & (1u << 10))) { + if ((++s_fast_maintenance & 0x3FFFu) == 0) { + extern void savestate_poll(CPUState* cpu, uint32_t resume_pc); + savestate_poll(cpu, s_compiled_interrupt_resume_pc); + debug_server_poll(); + } + return; + } + } + /* Genuine entry fast paths. Generated resident code calls this at every * basic-block edge, which can mean tens of millions of calls inside an FMV * polling loop. psx_advance_cycles() has already serviced every device @@ -758,7 +777,6 @@ void psx_check_interrupts(CPUState* cpu) { * Poll host maintenance periodically so save/load and the debug socket stay * responsive even when the guest spends a long time in this path. At the * observed 1M+ block edges/s this is sub-frame latency. */ - static uint32_t s_fast_maintenance; if (g_idle_skip_enabled < 0) (void)psx_idle_skip_is_enabled(); /* COP0 software interrupts (CAUSE.IP0/IP1 vs SR.IM0/IM1, bits 8-9): raised * purely by guest mtc0 to CAUSE, with no INTC involvement. Games use this @@ -1613,7 +1631,7 @@ irq_deliver_eval: void psx_check_interrupts_at(CPUState* cpu, uint32_t resume_pc) { uint32_t prev = s_compiled_interrupt_resume_pc; s_compiled_interrupt_resume_pc = resume_pc; - psx_check_interrupts(cpu); + psx_check_interrupts(cpu); /* flushes load-charge batch on entry */ s_compiled_interrupt_resume_pc = prev; } diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 28616805..63fb91c6 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -333,6 +333,8 @@ static std::string s_fps_base_title; static FramePacer s_frame_pacer = { 0 }; static int s_turbo_present_skip = 0; static int s_fmv_skip_present_skip = 0; +/* Netplay + depth24: present every other vblank (admit still every tick). */ +static int s_netplay_depth24_present_skip = 0; static uint32_t s_fmv_skip_last_mdec = 0; static int s_fmv_skip_hold = 0; @@ -354,6 +356,7 @@ static void present_session_reset(void) { s_frame_pacer = FramePacer{ 0 }; s_turbo_present_skip = 0; s_fmv_skip_present_skip = 0; + s_netplay_depth24_present_skip = 0; s_fmv_skip_last_mdec = 0; s_fmv_skip_hold = 0; smooth_60_reset(); @@ -2672,12 +2675,35 @@ static void capture_override_pad(int override_word, PsxNetPad* out) { out->connected = 1; } +/* Opt-in netplay barrier split: guest quantum vs admit wait (PSX_NETPLAY_TIMING=1). + * Printed on the existing [FPS] line — no separate log files. */ +static int s_np_timing_enabled = -1; +static uint64_t s_np_admit_ticks = 0; +static uint64_t s_np_guest_ticks = 0; +static uint64_t s_np_last_admit_end = 0; +static uint64_t s_np_timing_frames = 0; + +static int netplay_timing_on(void) { + if (s_np_timing_enabled < 0) { + const char *e = std::getenv("PSX_NETPLAY_TIMING"); + s_np_timing_enabled = (e && e[0] && e[0] != '0') ? 1 : 0; + } + return s_np_timing_enabled; +} + /* Stage local pad + poll admit until published. Parks the guest fiber when * called from the vblank callback (or before scheduler entry). Latches one * sample per sim tick; stalls on INPUT_CONFIRM desync. */ static void netplay_barrier_admit(int override) { if (!psx_netplay_active()) return; static int desync_logged = 0; + const uint64_t admit_t0 = + netplay_timing_on() ? SDL_GetPerformanceCounter() : 0; + /* Guest quantum = time since previous admit returned (fiber ran). */ + if (admit_t0 && s_np_last_admit_end && admit_t0 >= s_np_last_admit_end) { + s_np_guest_ticks += admit_t0 - s_np_last_admit_end; + s_np_timing_frames++; + } for (;;) { uint32_t dt = 0, lh = 0, rh = 0; /* Load apply/ready suppresses INPUT (and can sit silent for seconds on @@ -2719,6 +2745,11 @@ static void netplay_barrier_admit(int override) { } if (psx_netplay_poll_admit()) { desync_logged = 0; + if (admit_t0) { + const uint64_t t1 = SDL_GetPerformanceCounter(); + if (t1 >= admit_t0) s_np_admit_ticks += t1 - admit_t0; + s_np_last_admit_end = t1; + } return; } #ifndef PSX_NO_DEBUG_TOOLS @@ -3040,8 +3071,24 @@ static void sdl_vblank_present(void) { s_fps_base_title.c_str(), fps, speed); SDL_SetWindowTitle(sdl_window, title); } - std::fprintf(stderr, "[FPS] game: %.1f fps (%.2fx) | frames: %llu\n", - fps, speed, (unsigned long long)s_frame_count); + if (netplay_timing_on() && s_np_timing_frames > 0) { + const double invf = 1000.0 / (double)frequency; + const double admit_ms = + (double)s_np_admit_ticks * invf / (double)s_np_timing_frames; + const double guest_ms = + (double)s_np_guest_ticks * invf / (double)s_np_timing_frames; + std::fprintf(stderr, + "[FPS] game: %.1f fps (%.2fx) | frames: %llu | " + "guest=%.2f ms/f admit=%.2f ms/f (n=%llu)\n", + fps, speed, (unsigned long long)s_frame_count, + guest_ms, admit_ms, (unsigned long long)s_np_timing_frames); + s_np_admit_ticks = 0; + s_np_guest_ticks = 0; + s_np_timing_frames = 0; + } else { + std::fprintf(stderr, "[FPS] game: %.1f fps (%.2fx) | frames: %llu\n", + fps, speed, (unsigned long long)s_frame_count); + } std::fflush(stderr); s_fps_last_time = now; s_fps_last_frame = s_frame_count; @@ -3154,12 +3201,32 @@ static void sdl_vblank_present(void) { * (below) for the interactive present path; it still covers the turbo / * FMV-skip paths that early-return before pacing. * - * Delay-sync netplay (pre-rematch order): finish the completed tick, then - * admit the next tick's pads (publish → SIO) BEFORE pace/present so both - * peers share the same input before host GL work. */ + * Delay-sync netplay host FPS (MotK FMV): finish → present → admit/pace. + * Local Swap overlaps the peer's guest quantum; admit still runs every + * tick via the RAII tail (including early returns that skip present). + * Offline keeps pace-before-present. Barrier wait stays UDP poll — do + * not pair this order with SDL_Delay(0) busy-spin (tick-0 hang). */ + struct NetplayVblankTail { + int override_ = -1; + bool active_ = false; + bool skip_pace_ = false; + explicit NetplayVblankTail(int ov) + : override_(ov), active_(psx_netplay_active() != 0) {} + void skip_pace() { skip_pace_ = true; } + ~NetplayVblankTail() { + if (!active_) return; + if (psx_return_to_lobby_requested()) return; + netplay_barrier_admit(override_); + if (skip_pace_ || psx_return_to_lobby_requested()) return; + uint64_t perf_start = runtime_perf_section_begin(); + frame_pacer_wait(&s_frame_pacer, g_frame_period_ms); + runtime_perf_section_end(perf_start, &g_runtime_perf.pacer_ticks); + latency_ring_mark(LAT_PACED); + } + } netplay_tail(override); + if (psx_netplay_active()) { psx_netplay_finish_frame(); - netplay_barrier_admit(override); } else if (g_headless) { sample_headless_pad_into_sio(override); } else { @@ -3259,13 +3326,19 @@ static void sdl_vblank_present(void) { turbo_loads_active && g_turbo_audio_sink_enabled); #endif - if (g_headless) return; + if (g_headless) { + netplay_tail.skip_pace(); + return; + } /* TCP turbo is for automated validation and trace capture. It keeps the * simulation advancing and the debug server polling, but removes frontend * presentation and wall-clock pacing. */ #ifndef PSX_NO_DEBUG_TOOLS - if (debug_server_turbo_enabled()) return; + if (debug_server_turbo_enabled()) { + netplay_tail.skip_pace(); + return; + } #endif /* Turbo mode: while TAB is held, skip both VRAM->ARGB conversion and @@ -3279,7 +3352,10 @@ static void sdl_vblank_present(void) { const int TURBO_PRESENT_EVERY = 30; if (keys[SDL_SCANCODE_TAB]) { turbo_skip = (turbo_skip + 1) % TURBO_PRESENT_EVERY; - if (turbo_skip != 0) return; /* skip render this frame */ + if (turbo_skip != 0) { + netplay_tail.skip_pace(); + return; /* skip render this frame */ + } } else { turbo_skip = 0; } @@ -3297,7 +3373,10 @@ static void sdl_vblank_present(void) { g_turbo_loads_frames++; const int TL_PRESENT_EVERY = 30; s_turbo_present_skip = (s_turbo_present_skip + 1) % TL_PRESENT_EVERY; - if (s_turbo_present_skip != 0) return; + if (s_turbo_present_skip != 0) { + netplay_tail.skip_pace(); + return; + } } /* FMV auto-skip: run uncapped (no wall-clock pacing) and suppress nearly all @@ -3306,28 +3385,43 @@ static void sdl_vblank_present(void) { if (fmv_skip_active) { const int FMV_PRESENT_EVERY = 30; s_fmv_skip_present_skip = (s_fmv_skip_present_skip + 1) % FMV_PRESENT_EVERY; - if (s_fmv_skip_present_skip != 0) return; + if (s_fmv_skip_present_skip != 0) { + netplay_tail.skip_pace(); + return; + } } - /* Wall-clock pacing: always runs once fast_boot has ended, even when the - * display is still disabled (e.g. game crt0 setup). Skipped only by the - * turbo and fast_boot early-returns above. Netplay admits first (above), - * then paces here with everyone else, then presents — pre-rematch order. - * frame_pacer_wait is the race-free replacement for the old open-coded - * loop whose double counter read could underflow into a ~24.7-day - * SDL_Delay (Bug B hard freeze). */ - frame_pacer_wait(&s_frame_pacer, g_frame_period_ms); - latency_ring_mark(LAT_PACED); + /* Netplay FMV: skip present every other depth24 vblank (admit still runs + * in the RAII tail). Present-first frame, then alternate. */ + if (psx_netplay_active() && gpu_display_is_depth24()) { + if (s_netplay_depth24_present_skip) { + s_netplay_depth24_present_skip = 0; + netplay_tail.skip_pace(); + return; + } + s_netplay_depth24_present_skip = 1; + } else { + s_netplay_depth24_present_skip = 0; + } - /* Low-latency input: the early sample above is now ~one pacer-wait old. - * Refresh the device state and re-sample right before present so the next - * CPU frame reads near-fresh input (the dominant input->photon cost on a - * vsync-light box). Re-stamp the ring's input mark to measure from here. */ - if (g_low_latency_input && !psx_netplay_active()) { - SDL_GameControllerUpdate(); /* refresh pad state after the wait */ - SDL_PumpEvents(); /* refresh keyboard state */ - sample_pad_into_sio(override); - latency_ring_restamp_input(); + /* Offline wall-clock pacing before present. Netplay paces in NetplayVblankTail + * AFTER present so Swap overlaps the peer's guest quantum. */ + if (!psx_netplay_active()) { + uint64_t perf_start = runtime_perf_section_begin(); + frame_pacer_wait(&s_frame_pacer, g_frame_period_ms); + runtime_perf_section_end(perf_start, &g_runtime_perf.pacer_ticks); + latency_ring_mark(LAT_PACED); + + /* Low-latency input: the early sample above is now ~one pacer-wait old. + * Refresh the device state and re-sample right before present so the next + * CPU frame reads near-fresh input (the dominant input->photon cost on a + * vsync-light box). Re-stamp the ring's input mark to measure from here. */ + if (g_low_latency_input) { + SDL_GameControllerUpdate(); /* refresh pad state after the wait */ + SDL_PumpEvents(); /* refresh keyboard state */ + sample_pad_into_sio(override); + latency_ring_restamp_input(); + } } /* Mod hooks. Run after all normal input sampling. */ diff --git a/runtime/src/psx_cycles.c b/runtime/src/psx_cycles.c index 06c136b2..6c808b35 100644 --- a/runtime/src/psx_cycles.c +++ b/runtime/src/psx_cycles.c @@ -17,6 +17,7 @@ #endif uint64_t psx_cycle_count = 0; +uint32_t g_psx_cyc_batch = 0; static int s_cycle_replay_active = 0; static uint64_t s_cycle_replay_live = 0; @@ -83,10 +84,13 @@ static void advance_devices(uint32_t c) { #define PSX_DEADLINE_HARD_CAP 16384u static uint64_t s_devices_synced_cycle = 0; /* devices are advanced up to here */ +/* Exported for the inlined psx_advance_cycles / psx_cyc_charge hot path. */ uint64_t psx_next_service_cycle = 0; /* absolute; 0 = dirty, recompute */ -int psx_in_device_service = 0; /* re-entrancy guard (header hot path) */ -static uint64_t s_next_watchdog = 0; /* sparse throttle anchors in service_to_now */ -static uint64_t s_next_pc_sample = 0; +int psx_in_device_service = 0; /* re-entrancy guard */ +#define s_next_service_cycle psx_next_service_cycle +#define s_in_device_service psx_in_device_service +static uint64_t s_next_watchdog = 0; +static uint64_t s_next_pc_sample = 0; /* Absolute inclusive limit for dirty_ram_interp.c's exact one-cycle path. * Zero means that the next charge must visit psx_advance_cycles(). */ @@ -187,6 +191,7 @@ void psx_devices_service_to_now(void) { * advance through service_to_now even when already synced — MotK FMV pays * that on every GPU/CD/MDEC MMIO touch. Recompute here instead. */ void psx_devices_mmio_sync(void) { + psx_cyc_batch_flush(); if (s_devices_synced_cycle != psx_cycle_count) { psx_devices_service_to_now(); } else { @@ -270,7 +275,7 @@ void psx_advance_cycles_slow(uint32_t cycles) { } uint64_t psx_get_cycle_count(void) { - return psx_cycle_count; + return psx_cycle_count + (uint64_t)g_psx_cyc_batch; } /* ===== Idle-loop cycle skip (wait-loop elision, 2026-07-06) ================== @@ -486,6 +491,7 @@ void psx_idle_note_check(CPUState *cpu, uint32_t check_pc) { * would try to replay a bogus gap. Re-anchor devices at the restored cycle and * force a fresh deadline on the next charge. */ void psx_cycles_resync_after_restore(void) { + g_psx_cyc_batch = 0; s_devices_synced_cycle = psx_cycle_count; psx_next_service_cycle = 0; /* recompute on next charge */ psx_in_device_service = 0; @@ -500,6 +506,17 @@ void psx_cycles_reset_for_boot(void) { s_next_pc_sample = 0; } +void psx_cycles_reset_for_boot(void) { + g_psx_cyc_batch = 0; + psx_cycle_count = 0; + s_devices_synced_cycle = 0; + s_next_service_cycle = 0; + s_in_device_service = 0; + g_psx_cycle_fast_limit = 0; + s_next_watchdog = 0; + s_next_pc_sample = 0; +} + /* ---- Mult/div completion-stall timing (faithful R3000A; Beetle muldiv_ts_done) ---- * * MULT/MULTU/DIV/DIVU don't stall at the op; they set a completion DEADLINE. diff --git a/runtime/src/psx_lobby_client.c b/runtime/src/psx_lobby_client.c index 486ba626..6af3ba3d 100644 --- a/runtime/src/psx_lobby_client.c +++ b/runtime/src/psx_lobby_client.c @@ -128,6 +128,8 @@ static int list_filter_version_strict(void) return gv && gv[0] && strcmp(gv, "dev") != 0; } +static void queue_send(const char *json); + static void queue_list_request(void) { char msg[384]; @@ -159,7 +161,6 @@ static int json_extract_object(const char *json, const char *key, char *out, siz static void parse_match_caps_object(const char *obj, PsxLobbyMatchCaps *out); static void ingest_match_caps_from_json(const char *json); static int append_match_caps_json(char *dst, size_t dst_cap, const PsxLobbyMatchCaps *caps); -static void queue_send(const char *json); const char *psx_lobby_default_url(void) {