From 9515cb7dfeee43eb8a6816fd21269306e129b2da Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:02:20 -0700 Subject: [PATCH] idle_skip: elide store-counter wait loops (PsyQ v_wait) idle_skip only skipped loops with zero stores and a single interrupt-check edge, so it never fired on PsyQ libetc's v_wait -- the VSync(0)/VSync(n) spin most PsyQ titles run each frame -- which decrements a stack timeout word every pass and crosses two block edges. New opt-in on top of idle_skip, [runtime] idle_skip_store_counters (PSX_IDLE_SKIP_EXT=0/1 for one run): - an iteration may span up to 8 block edges; the first edge is the anchor and the edge count must be stable; - one word store per iteration to the same RAM/scratchpad address whose value drops by exactly 1 (a register holding it may drop with it). Skips lower the counter by k (never to <= 1) and, for multi-edge loops, land on the anchor strictly before the next device event so an IRQ is taken at the same edge as without the skip. memory.c records the last word store's address and value for the detector. R4 race, 3600 frames from one savestate, held input: cyc, mmio, mc, sp, sc and qc identical every frame; wc differs only by the elided counter stores. Release build on an Apple M4: emulation-thread busy 47.5% -> 34.9% at 60 Hz. Co-Authored-By: Claude Opus 5.5 --- docs/config_schema.md | 14 ++++ recompiler/src/config_loader.cpp | 4 + recompiler/src/config_loader.h | 5 ++ runtime/src/main.cpp | 7 ++ runtime/src/memory.c | 9 ++ runtime/src/psx_cycles.c | 137 ++++++++++++++++++++++++++----- 6 files changed, 157 insertions(+), 19 deletions(-) diff --git a/docs/config_schema.md b/docs/config_schema.md index 4f2c8975..e22162fc 100644 --- a/docs/config_schema.md +++ b/docs/config_schema.md @@ -493,6 +493,20 @@ overlay_region_floor = "0x10000" # optional: lowest RAM address treated as ove # eligible. Clamped >= 0x10000; PSX_OVERLAY_REGION_FLOOR overrides. ``` +`idle_skip` only elides loops with no stores and one interrupt-check edge. +`idle_skip_store_counters = true` (with `idle_skip`; `PSX_IDLE_SKIP_EXT=0/1` +overrides it for one run) also elides loops that count a stack timeout down +by one each pass and span up to eight block edges: PsyQ libetc's `v_wait`, +the `VSync(0)`/`VSync(n)` spin most PsyQ titles run every frame. The skip +lowers the counter by the number of passes it skips (never to 1 or below, so +the real timeout test still runs) and lands on the loop's first edge strictly +before the next device event, so an interrupt is taken at the same edge as +without it. Guest cycles, device-register writes and scratchpad writes are +unchanged; the only difference is that the skipped passes' stores to the +counter word are not individually made (`frame_fingerprint` `wc`/`ws` count +fewer writes, its judge columns match). In R4 races it removes about a quarter +of the emulation thread's time. Off under netplay, like `idle_skip`. + ### `turbo_loads` / `offer_turbo_loads` — deprecated and ignored **Do not use these keys.** Load acceleration is owned by the Mods catalog: diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index 78df8ade..e809208c 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -544,6 +544,10 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& if (runtime.contains("turbo_audio_sink")) { rt.turbo_audio_sink = toml::find(runtime, "turbo_audio_sink"); } + if (runtime.contains("idle_skip_store_counters")) { + rt.idle_skip_store_counters = + toml::find(runtime, "idle_skip_store_counters"); + } if (runtime.contains("idle_skip")) { rt.idle_skip = toml::find(runtime, "idle_skip"); } diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index ffbe6f88..90511e5f 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -354,6 +354,11 @@ struct RuntimeConfig { // device events still advance exactly. Opt-in per game; the idle_skip // debug command and PSX_IDLE_SKIP environment variable support live A/B. bool idle_skip = false; + // idle_skip_store_counters: with idle_skip, also skip loops that count a + // stack timeout down once per iteration and span several block edges -- + // PsyQ libetc's v_wait, the VSync(0) spin. Bit-exact (psx_cycles.c); + // PSX_IDLE_SKIP_EXT=0/1 overrides it for one run. + bool idle_skip_store_counters = false; // overlay_autocompile_cmd: variant-capture automation (step 2.8). A // shell command (run via cmd.exe /C, cwd = project root) that compiles diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index db52ae0b..dd792d73 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -16317,6 +16317,13 @@ int main(int argc, char** argv) { std::fprintf(stdout, "psxrecomp: idle_skip %s%s\n", g_idle_skip_enabled ? "enabled" : "disabled", idle_env ? " (environment override)" : ""); + extern int g_idle_skip_ext; + const char *ext_env = std::getenv("PSX_IDLE_SKIP_EXT"); + g_idle_skip_ext = ext_env ? (ext_env[0] == '1' ? 1 : 0) + : (gc.runtime.idle_skip_store_counters ? 1 : 0); + if (g_idle_skip_enabled && g_idle_skip_ext) + std::fprintf(stdout, "psxrecomp: idle_skip store-counter loops on%s\n", + ext_env ? " (environment override)" : ""); psx_precise_slice_init_from_env(); } for (uint32_t site : gc.vsync_event_horizon_sites) diff --git a/runtime/src/memory.c b/runtime/src/memory.c index b1e8235b..24e408b0 100644 --- a/runtime/src/memory.c +++ b/runtime/src/memory.c @@ -1042,6 +1042,11 @@ uint64_t g_mmio_access_count = 0; * ANY store is not a pure poll loop. CPU stores and in-flight DMA writes * both funnel through the psx_write_*_raw chokepoints. */ uint64_t g_guest_store_count = 0; +/* The last guest store, for the idle-loop detector's store-counter loops + * (psx_cycles.c): its address and value when it was a word store, else + * address 0xFFFFFFFF. Two plain stores on the store path. */ +uint32_t g_guest_last_store_addr = 0xFFFFFFFFu; +uint32_t g_guest_last_store_val = 0; /* ---- Card protocol trace: tracks I_MASK bit 7 transitions ---- */ #define IMASK_TRACE_CAP 4096 @@ -1976,6 +1981,8 @@ void psx_write_word(uint32_t addr, uint32_t val) { } static void psx_write_word_raw(uint32_t addr, uint32_t val) { g_guest_store_count++; + g_guest_last_store_addr = addr; + g_guest_last_store_val = val; /* (pgxp) CPU stores carry their shadow through the PGXP_STORE hook; * DMA and host stores drop it below (pgxp_untracked_store). */ /* IsC first, before any decode (cpu_store_isolated). */ @@ -2187,6 +2194,7 @@ void psx_write_half(uint32_t addr, uint16_t val) { } static void psx_write_half_raw(uint32_t addr, uint16_t val) { g_guest_store_count++; + g_guest_last_store_addr = 0xFFFFFFFFu; if (cpu_store_isolated()) { isc_store(addr, val, 2); return; } /* KSEG2 guard — see psx_read_word_raw. */ @@ -2556,6 +2564,7 @@ void psx_host_write_byte(uint32_t addr, uint8_t val) { static void psx_write_byte_raw(uint32_t addr, uint8_t val) { g_guest_store_count++; + g_guest_last_store_addr = 0xFFFFFFFFu; if (cpu_store_isolated()) { isc_store(addr, val, 1); return; } /* KSEG2 guard — see psx_read_word_raw. */ diff --git a/runtime/src/psx_cycles.c b/runtime/src/psx_cycles.c index a4e39015..043409ff 100644 --- a/runtime/src/psx_cycles.c +++ b/runtime/src/psx_cycles.c @@ -12,6 +12,7 @@ #include "dma.h" #include "interrupts.h" #include "sio.h" +#include "psx_memory.h" #include "starvation_ring.h" #include "timers.h" #if defined(PSX_HAS_RECOMP_NET) @@ -623,6 +624,67 @@ static int idle_skip_on(void) { int psx_idle_skip_is_enabled(void) { return idle_skip_on(); } +/* Store-counter loops (opt-in on top of idle_skip: [runtime] + * idle_skip_store_counters, PSX_IDLE_SKIP_EXT=1). PsyQ libetc's v_wait -- + * the VSync(0)/VSync(n) spin every PsyQ title runs each frame -- is + * + * do { if (--timeout == -1) { timeout path } } while (VSync_count < target); + * + * with `timeout` a stack word: one guest store per iteration, and two block + * edges (interrupt-check boundaries) per iteration. The base detector needs + * zero stores and one edge, so it never fires there. The extension accepts + * - several edges per iteration: the first edge seen is the anchor and up + * to IDLE_EDGES_MAX other edges between two anchor visits are one + * iteration (their count must be stable too); + * - exactly one word store per iteration, to the same RAM/scratchpad + * address, whose value drops by exactly 1 each iteration (a register + * holding the same value may drop with it). + * Skips stay bit-exact: k whole iterations, landing on the anchor strictly + * BEFORE the next device event when the loop has more than one edge (an IRQ + * may be taken at any of them, so the last iteration runs for real), the + * counter (and its register) lowered by k and never to 1 or below, so the + * real timeout test still runs. */ +int g_idle_skip_ext = -1; /* -1 = read PSX_IDLE_SKIP_EXT once */ +enum { IDLE_EDGES_MAX = 8 }; +static uint32_t s_idle_edges = 0; /* non-anchor edges since the anchor */ +static uint32_t s_idle_iter_edges = 0; /* edges in the last whole iteration */ +static uint32_t s_idle_store_addr = 0xFFFFFFFFu; +static uint32_t s_idle_store_val = 0; +static int s_idle_mem_progress = 0; + +static int idle_skip_ext_on(void) { + if (g_idle_skip_ext < 0) { + const char *e = getenv("PSX_IDLE_SKIP_EXT"); + g_idle_skip_ext = e ? (e[0] == '1') : 0; + } + return g_idle_skip_ext > 0; +} + +/* Stores since the last edge: 0, or (extension) one word store to RAM or + * the scratchpad. *addr/*val name it; addr 0xFFFFFFFF when none. */ +static int idle_stores_ok(uint64_t stores_now, int ext, uint32_t *addr, uint32_t *val) { + extern uint32_t g_guest_last_store_addr, g_guest_last_store_val; + *addr = 0xFFFFFFFFu; + *val = 0; + const uint64_t d = stores_now - s_idle_last_stores; + if (d == 0) return 1; + if (!ext || d != 1 || g_guest_last_store_addr == 0xFFFFFFFFu) return 0; + const uint32_t phys = g_guest_last_store_addr & 0x1FFFFFFFu; + if ((phys & 3u) != 0u) return 0; + if (!(phys < 0x00800000u || (phys >= 0x1F800000u && phys < 0x1F800400u))) return 0; + *addr = g_guest_last_store_addr; + *val = g_guest_last_store_val; + return 1; +} + +static void idle_reset_progress(void) { + s_idle_streak = 0; + s_idle_quantum = 0; + s_idle_progress_reg = -2; + s_idle_progress_delta = 0; + s_idle_mem_progress = 0; +} + void psx_idle_note_check(CPUState *cpu, uint32_t check_pc) { #ifdef PSX_COSIM (void)cpu; (void)check_pc; @@ -641,12 +703,23 @@ void psx_idle_note_check(CPUState *cpu, uint32_t check_pc) { s_idle_pc = 0; s_idle_streak = 0; s_idle_have_snap = 0; + s_idle_edges = 0; + return; + } + const int ext = idle_skip_ext_on(); + + /* Extension: an edge inside the anchor's iteration. Only MMIO is checked + * here; stores are counted over the whole iteration at the anchor. */ + if (ext && s_idle_pc != 0 && check_pc != s_idle_pc && s_idle_edges < IDLE_EDGES_MAX && + g_mmio_access_count == s_idle_last_mmio) { + s_idle_edges++; return; } uint64_t cyc = psx_cycle_count; - if (check_pc == s_idle_pc && s_idle_have_snap && - g_guest_store_count == s_idle_last_stores && + uint32_t st_addr, st_val; + const int stores_ok = idle_stores_ok(g_guest_store_count, ext, &st_addr, &st_val); + if (check_pc == s_idle_pc && s_idle_have_snap && stores_ok && g_mmio_access_count == s_idle_last_mmio) { int changed = -1, changed_count = 0; for (int i = 1; i < 32; i++) { @@ -665,27 +738,42 @@ void psx_idle_note_check(CPUState *cpu, uint32_t check_pc) { * executes. No stores/MMIO are allowed, so there is no hidden work. */ int progress_reg = changed_count == 0 ? -1 : (changed_count == 1 && progress_delta == -1 ? changed : -2); + /* Extension: the one store must be the same counter word, one lower + * than last iteration, positive; a progressing register must hold it. */ + int mem_progress = 0; + if (st_addr != 0xFFFFFFFFu) { + if (st_addr == s_idle_store_addr && st_val == s_idle_store_val - 1u && + (int32_t)st_val > 1 && + (progress_reg <= 0 || cpu->gpr[progress_reg] == st_val)) + mem_progress = 1; + else + progress_reg = -2; + } else if (s_idle_store_addr != 0xFFFFFFFFu) { + progress_reg = -2; /* the store pattern changed */ + } uint32_t quantum = (uint32_t)(cyc - s_idle_last_cycle); if (progress_reg != -2 && quantum > 0 && quantum <= IDLE_QUANTUM_MAX) { if (quantum == s_idle_quantum && progress_reg == s_idle_progress_reg && - progress_delta == s_idle_progress_delta) { + progress_delta == s_idle_progress_delta && + mem_progress == s_idle_mem_progress && + s_idle_edges == s_idle_iter_edges) { if (s_idle_streak < 1000000u) s_idle_streak++; } else { s_idle_quantum = quantum; s_idle_progress_reg = progress_reg; s_idle_progress_delta = progress_delta; + s_idle_mem_progress = mem_progress; s_idle_streak = 1; } } else { - s_idle_streak = 0; - s_idle_quantum = 0; - s_idle_progress_reg = -2; - s_idle_progress_delta = 0; + idle_reset_progress(); } + s_idle_store_addr = st_addr; + s_idle_store_val = st_val; + s_idle_iter_edges = s_idle_edges; idle_snapshot_regs(cpu); - } else if (check_pc == s_idle_pc && - g_guest_store_count == s_idle_last_stores && + } else if (check_pc == s_idle_pc && stores_ok && g_mmio_access_count == s_idle_last_mmio) { /* Second consecutive observation of this PC with no stores/MMIO — * take the baseline snapshot; the next hit can compare. Defers the @@ -693,18 +781,17 @@ void psx_idle_note_check(CPUState *cpu, uint32_t check_pc) { * PC every check (pure host cost, never skipped). */ idle_snapshot_regs(cpu); s_idle_have_snap = 1; - s_idle_streak = 0; - s_idle_quantum = 0; - s_idle_progress_reg = -2; - s_idle_progress_delta = 0; + s_idle_store_addr = st_addr; + s_idle_store_val = st_val; + s_idle_iter_edges = s_idle_edges; + idle_reset_progress(); } else { s_idle_pc = check_pc; - s_idle_streak = 0; - s_idle_quantum = 0; - s_idle_progress_reg = -2; - s_idle_progress_delta = 0; + idle_reset_progress(); s_idle_have_snap = 0; + s_idle_store_addr = 0xFFFFFFFFu; } + s_idle_edges = 0; s_idle_last_cycle = cyc; s_idle_last_stores = g_guest_store_count; s_idle_last_mmio = g_mmio_access_count; @@ -713,18 +800,28 @@ void psx_idle_note_check(CPUState *cpu, uint32_t check_pc) { uint32_t q = s_idle_quantum; uint32_t dist = devices_cycles_to_next_idle_event(); if (dist <= q) return; /* one real iteration reaches it */ - uint32_t k = (dist + q - 1u) / q; /* first check boundary >= event */ + uint32_t k; + if (s_idle_iter_edges == 0) + k = (dist + q - 1u) / q; /* first check boundary >= event */ + else + k = (dist - 1u) / q; /* land on the anchor before it */ if (s_idle_progress_reg > 0) { uint32_t value = cpu->gpr[s_idle_progress_reg]; uint32_t max_k = value > 1u ? value - 1u : 0u; if (k > max_k) k = max_k; - if (k == 0) return; } + if (s_idle_mem_progress) { + uint32_t max_k = (int32_t)s_idle_store_val > 1 ? s_idle_store_val - 1u : 0u; + if (k > max_k) k = max_k; + } + if (k == 0) return; uint64_t skip = (uint64_t)k * q; if (skip > IDLE_SKIP_MAX_CYCLES) return; if (s_idle_progress_reg > 0) cpu->gpr[s_idle_progress_reg] -= k; + if (s_idle_mem_progress) + psx_host_write_word(s_idle_store_addr, s_idle_store_val - k); g_idle_skip_count++; g_idle_skip_cycles += skip; @@ -738,6 +835,8 @@ void psx_idle_note_check(CPUState *cpu, uint32_t check_pc) { s_idle_streak = 0; idle_snapshot_regs(cpu); s_idle_have_snap = 1; + s_idle_store_addr = s_idle_mem_progress ? s_idle_store_addr : 0xFFFFFFFFu; + s_idle_store_val = s_idle_store_val - (s_idle_mem_progress ? k : 0u); s_idle_last_cycle = psx_cycle_count; s_idle_last_stores = g_guest_store_count; s_idle_last_mmio = g_mmio_access_count;