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;