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 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-08 15:02:20 -07:00
parent 54abcb70b4
commit 9515cb7dfe
6 changed files with 157 additions and 19 deletions
+14
View File
@@ -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:
+4
View File
@@ -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<bool>(runtime, "turbo_audio_sink");
}
if (runtime.contains("idle_skip_store_counters")) {
rt.idle_skip_store_counters =
toml::find<bool>(runtime, "idle_skip_store_counters");
}
if (runtime.contains("idle_skip")) {
rt.idle_skip = toml::find<bool>(runtime, "idle_skip");
}
+5
View File
@@ -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
+7
View File
@@ -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)
+9
View File
@@ -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. */
+118 -19
View File
@@ -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;