FMV optimizations

This commit is contained in:
Alex Vanderveen
2026-07-21 15:21:00 -04:00
committed by Matthew Stanley
parent 0b890cdc13
commit 7cf9958135
8 changed files with 284 additions and 40 deletions
+50
View File
@@ -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.
+32 -3
View File
@@ -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;
+33 -1
View File
@@ -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"). */
+3
View File
@@ -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 <stdio.h>
#include <stdlib.h>
@@ -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);
}
+20 -2
View File
@@ -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;
}
+123 -29
View File
@@ -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. */
+21 -4
View File
@@ -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.
+2 -1
View File
@@ -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)
{