Validation: integrate PR590 for current-head regression review

This commit is contained in:
Matthew Stanley
2026-10-14 23:48:02 -07:00
10 changed files with 497 additions and 20 deletions
+87
View File
@@ -493,6 +493,93 @@ 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 (several edges per pass are
accepted only together with such a counter store; the device distance is
measured from the current cycle, not from the devices' last service) (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`.
#### Developer contract for `idle_skip_store_counters`
**Eligibility is an audited allowlist, not an automatic guarantee.** The
detector can check a loop's shape over the passes it observes. It cannot prove
that the loop has no later exit that depends on the counter (for example, an
exit at `counter == 500`): a skip would jump past it. So the extension only
skips at sites the title's developer has audited and declared:
```toml
[runtime]
idle_skip = true
idle_skip_store_counters = true
# Block-edge PCs of each audited loop (the detector anchors on either).
idle_skip_store_counter_sites = ["0x8008B4D0", "0x8008B518"]
```
Unlisted sites are never skipped. An empty or missing list turns the
extension off, and the runtime log says so. Before adding a site, read the
loop's code and record its exit structure next to the list: every exit that
depends on the counter must be the final timeout test (the counter reaching
the value the skip stops short of), and no other code may read the counter.
R4 (SLUS-00797) is the reference audit. PsyQ libetc `v_wait` sits at
`0x8008B4A8`:
1. It stores `timeout << 15` to `0x10(sp)` and returns at once if the VSync
count at `0x800A6548` has already reached the target.
2. **`0x8008B4D0`:** it loads the counter, subtracts 1, stores it and reloads
it. If it equals -1 it takes the timeout path (`0x8008B4F0`: prints
"VSync: timeout", calls `ChangeClearPAD(0)` and `ChangeClearRCnt(3, 0)`,
then jumps to the exit).
3. **`0x8008B518`:** it reloads the VSync count and branches back to
`0x8008B4D0` while the count is still below the target.
4. **`0x8008B530`:** the exit.
The only exit that depends on the counter is the `== -1` timeout test. The skip
never takes the counter to 1 or below, so that test always runs for real. The
counter is a private stack slot of this frame.
A skip leaves out stores to the counter word that the guest would have made.
That is invisible only if nothing observes the word between those stores. The
assumptions, and how each one is enforced:
| Assumption | Enforced by |
|---|---|
| The loop makes exactly one guest store per pass: a 4-byte-aligned word in RAM or the scratchpad, at the same address every pass, one lower each pass. | `idle_stores_ok` plus the address/value check at the anchor. Any other store count, any device or BIOS-area address, or a changed pattern resets the detector. |
| The loop touches no MMIO, so it cannot start DMA, acknowledge an IRQ or poll a device mid-span. | `g_mmio_access_count` must be unchanged across the pass. |
| Everything else in the pass is a pure function of the registers. Only the counter register and the counter word progress; every other GPR is invariant at the anchor. | The register snapshot is compared at the anchor; one progress register is allowed, and only if it moves in step with the word. |
| No other observer (IRQ handler, DMA, timer, SPU, CD, GPU, host-side mod or debugger code) runs between two device events. | The skip lands on the anchor strictly before the next scheduled device event, measured from the current cycle. Every IRQ, DMA completion and VBlank callback therefore sees the counter value that unskipped execution would have stored at that same edge. Host mod hooks run only at VBlank or at function entry, and neither happens inside the span. |
| The loop has no exit that depends on the counter, except the final timeout test. | **Not checkable by the detector.** It is the caller's audit: only the sites listed in `idle_skip_store_counter_sites` are eligible. |
| The loop's own exit test still runs. | The counter is never lowered to 1 or below, so the `--timeout == -1` path executes for real. |
| The game does not read the counter word through another pointer, inside or outside the loop. | Partly checked: a read that changes a register or the edge count resets the detector. Otherwise this is part of the site audit. |
What differs from unskipped execution: the guest store count (`frame_fingerprint`
`wc`/`ws`) and the host time. Guest cycles, device-register writes, scratchpad
and RAM contents at every device event are the same. That is why the judge
columns `cyc`, `mmio`, `mc`, `sp`, `sc` and `qc` must match. A title that
breaks one of the assumptions in a way the detector cannot see (for example, a
second CPU-side writer driven by a device it does not schedule) must leave
the key off.
Evidence (R4, OpenBIOS, M4 release build with debug tools, `frame_fingerprint`, key off vs on):
- boot into attract: 9000 frames
- race from a savestate: 3600 frames
- all judge columns identical, 0 dispatch misses
- `wc`/`ws` lower with the key on
The other psxrecomp titles have no disc in this test setup, so they keep the
key off until they are run the same way.
### `turbo_loads` / `offer_turbo_loads` — deprecated and ignored
**Do not use these keys.** Load acceleration is owned by the Mods catalog:
+1 -1
View File
@@ -1 +1 @@
77a1ea51cf0498290de3f78b4fb42ed51640dcee62b379ff44cde8b47cc5714e
ad06939d1795d8a832885c34bd5e3d3edfcc4782873a603b5eeff8592f210186
+10
View File
@@ -544,6 +544,16 @@ 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_store_counter_sites")) {
for (const auto& a : toml::find<std::vector<std::string>>(
runtime, "idle_skip_store_counter_sites"))
rt.idle_skip_store_counter_sites.push_back(
parse_hex(a, "runtime.idle_skip_store_counter_sites"));
}
if (runtime.contains("idle_skip")) {
rt.idle_skip = toml::find<bool>(runtime, "idle_skip");
}
+9
View File
@@ -354,6 +354,15 @@ 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;
// idle_skip_store_counter_sites: the audited loop sites (block-edge PCs
// the detector anchors on) where the store-counter extension may skip.
// Unlisted sites are never skipped; an empty list disables the extension.
std::vector<uint32_t> idle_skip_store_counter_sites;
// 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
@@ -1822,6 +1822,13 @@ if(BUILD_TESTING)
add_test(NAME psx_cycle_event_boundaries_test
COMMAND psx_cycle_event_boundaries_test)
# idle_skip store-counter loops: PsyQ v_wait skips, near misses do not.
add_executable(idle_skip_store_counter_test
tests/test_idle_skip_store_counter.c
src/psx_cycles.c)
target_include_directories(idle_skip_store_counter_test PRIVATE include)
add_test(NAME idle_skip_store_counter_test COMMAND idle_skip_store_counter_test)
add_executable(psx_vblank_clock_test
tests/test_psx_vblank_clock.c
src/psx_vblank_clock.c)
+11
View File
@@ -257,6 +257,17 @@ int psx_idle_skip_is_enabled(void);
/* Cycles until the nearest IRQ-observable device event (mask-aware); the bound an idle skip may not cross. */
uint32_t psx_idle_cycles_to_next_observable_event(void);
extern int g_idle_skip_enabled;
/* [runtime] idle_skip_store_counters (psx_cycles.c): 1 on, 0 off, -1 env. */
extern int g_idle_skip_ext;
/* [runtime] idle_skip_store_counter_sites: the audited anchor PCs where the
* store-counter extension may skip (at most 16). Unlisted sites never are. */
void psx_idle_skip_set_store_counter_sites(const uint32_t *pcs, int n);
/* The host write that lowers a skipped loop counter (psx_host_write_word);
* the store-counter extension stays off until one is installed. */
void psx_idle_skip_set_host_writer(void (*w)(uint32_t addr, uint32_t val));
/* The store path's last-word-store record (memory.c): the gate the detector
* sets while the extension is on, and the address/value it reads. */
void psx_idle_skip_set_store_tracker(int *gate, const uint32_t *addr, const uint32_t *val);
extern uint64_t g_idle_skip_count;
extern uint64_t g_idle_skip_cycles;
extern uint32_t g_idle_skip_last_pc;
+22
View File
@@ -46,6 +46,9 @@
#include "autocompile.h"
#include "code_provider.h"
extern "C" void psx_event_step_conservative_env_init(void);
/* memory.c's last-word-store record for the idle-loop detector. */
extern "C" int g_psx_track_last_store;
extern "C" uint32_t g_guest_last_store_addr, g_guest_last_store_val;
#include "overlay_backend.h"
#include "gpu.h"
#include "draw_distance.h"
@@ -16925,6 +16928,25 @@ 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)" : "");
{
psx_idle_skip_set_host_writer(psx_host_write_word);
psx_idle_skip_set_store_tracker(&g_psx_track_last_store,
&g_guest_last_store_addr,
&g_guest_last_store_val);
}
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);
psx_idle_skip_set_store_counter_sites(
gc.runtime.idle_skip_store_counter_sites.data(),
(int)gc.runtime.idle_skip_store_counter_sites.size());
if (g_idle_skip_enabled && g_idle_skip_ext)
std::fprintf(stdout, "psxrecomp: idle_skip store-counter loops on%s, "
"%zu audited site(s)%s\n",
ext_env ? " (environment override)" : "",
gc.runtime.idle_skip_store_counter_sites.size(),
gc.runtime.idle_skip_store_counter_sites.empty()
? " -- none listed, so nothing is skipped" : "");
psx_precise_slice_init_from_env();
}
for (uint32_t site : gc.vsync_event_horizon_sites)
+13
View File
@@ -1042,6 +1042,13 @@ 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 word store, for the idle-loop detector's store-counter
* loops (psx_cycles.c, [runtime] idle_skip_store_counters). Recorded only
* while that option is on (g_psx_track_last_store); main.cpp hands these to
* the detector with psx_idle_skip_set_store_tracker. */
int g_psx_track_last_store = 0;
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 +1983,10 @@ 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++;
if (g_psx_track_last_store) {
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 +2198,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++;
if (g_psx_track_last_store) 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 +2568,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++;
if (g_psx_track_last_store) g_guest_last_store_addr = 0xFFFFFFFFu;
if (cpu_store_isolated()) { isc_store(addr, val, 1); return; }
/* KSEG2 guard — see psx_read_word_raw. */
+168 -19
View File
@@ -623,6 +623,99 @@ 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 */
/* Last-store tracking (memory.c writes it only while this is set) and the
* host writer used to lower a skipped counter. Both live here so every
* target that links the cycle model links them too. */
static int s_track_dummy;
static const uint32_t s_no_store = 0xFFFFFFFFu;
static int *s_track = &s_track_dummy;
static const uint32_t *s_last_addr = &s_no_store, *s_last_val = &s_no_store;
static void (*s_idle_host_write)(uint32_t addr, uint32_t val) = NULL;
void psx_idle_skip_set_host_writer(void (*w)(uint32_t, uint32_t)) { s_idle_host_write = w; }
void psx_idle_skip_set_store_tracker(int *gate, const uint32_t *addr, const uint32_t *val) {
s_track = gate ? gate : &s_track_dummy;
s_last_addr = addr ? addr : &s_no_store;
s_last_val = val ? val : &s_no_store;
}
enum { IDLE_EDGES_MAX = 8, IDLE_SITES_MAX = 16 };
/* Audited sites. The detector proves the loop's shape over the passes it
* sees; it cannot prove that no later counter-dependent exit exists (an exit
* at counter == 500 would be skipped over). Only sites a developer audited
* and declared are eligible. */
static uint32_t s_idle_sites[IDLE_SITES_MAX];
static int s_idle_site_count = 0;
void psx_idle_skip_set_store_counter_sites(const uint32_t *pcs, int n) {
if (n < 0 || !pcs) n = 0;
if (n > IDLE_SITES_MAX) n = IDLE_SITES_MAX;
for (int i = 0; i < n; i++) s_idle_sites[i] = pcs[i];
s_idle_site_count = n;
}
static int idle_site_listed(uint32_t pc) {
for (int i = 0; i < s_idle_site_count; i++)
if (s_idle_sites[i] == pc) return 1;
return 0;
}
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;
}
/* Without a host writer the counter cannot be lowered: no extension. */
*s_track = g_idle_skip_ext > 0 && s_idle_host_write != NULL && s_track != &s_track_dummy;
return *s_track;
}
/* 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) {
*addr = 0xFFFFFFFFu;
*val = 0;
const uint64_t d = stores_now - s_idle_last_stores;
if (d == 0) return 1;
if (!ext || d != 1 || *s_last_addr == 0xFFFFFFFFu) return 0;
const uint32_t phys = *s_last_addr & 0x1FFFFFFFu;
if ((phys & 3u) != 0u) return 0;
if (!(phys < 0x00800000u || (phys >= 0x1F800000u && phys < 0x1F800400u))) return 0;
*addr = *s_last_addr;
*val = *s_last_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 +734,26 @@ 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;
}
/* The extension applies only at an audited site: the anchor while
* counting its inner edges, else the PC being checked. */
const uint32_t site = (s_idle_pc != 0 && check_pc != s_idle_pc) ? s_idle_pc : check_pc;
const int ext = idle_skip_ext_on() && idle_site_listed(site);
/* 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 +772,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,38 +815,63 @@ 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;
if (s_idle_streak < IDLE_STREAK_MIN) return;
/* Several edges per pass are accepted only for the store-counter shape
* (v_wait). Multi-edge loops with no counter store -- OpenBIOS's
* 0xBFC057C0 poll, kernel 0x16BC -- moved R4's boot timing when skipped
* (frame 192, cyc +3857), so they keep running for real. */
if (s_idle_iter_edges && !s_idle_mem_progress) return;
uint32_t q = s_idle_quantum;
uint32_t dist = devices_cycles_to_next_idle_event();
/* The devices report distances from where they were last serviced
* (s_devices_synced_cycle), which can trail psx_cycle_count by a few
* hundred cycles. For the store-counter shape, measure from now: an
* overstated distance lands the skip after the event and delivers the
* IRQ late (R4 boot, frame 961: +402 cycles). */
if (s_idle_iter_edges || s_idle_mem_progress) {
const uint64_t lag = psx_cycle_count > s_devices_synced_cycle
? psx_cycle_count - s_devices_synced_cycle : 0;
if ((uint64_t)dist <= lag) return;
dist -= (uint32_t)lag;
}
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)
s_idle_host_write(s_idle_store_addr, s_idle_store_val - k);
g_idle_skip_count++;
g_idle_skip_cycles += skip;
@@ -738,6 +885,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;
@@ -0,0 +1,169 @@
/*
* Pin the idle-loop detector's store-counter extension
* ([runtime] idle_skip_store_counters, psx_cycles.c).
*
* The positive case is PsyQ libetc's v_wait as R4 runs it (0x8008B4A8): two
* interrupt-check edges per pass (0x8008B4D0 and 0x8008B518), one word store
* per pass to a stack counter that drops by exactly 1, no MMIO, registers
* identical at the anchor edge. It must skip whole passes, lower the counter
* by the same number of passes through the host writer, never to 1 or below,
* and land on the anchor strictly before the next device event.
*
* Near misses that must NOT skip: an extra store per pass, an MMIO read per
* pass, a counter that drops by 2, a store to MMIO space, and the same loop
* with the option off.
*
* Build/run: ctest -R idle_skip_store_counter_test
*/
#include "cpu_state.h"
#include "psx_cycles.h"
#include <stdint.h>
#include <stdio.h>
#include <string.h>
int g_ls_replay_active = 0;
int g_ls_mode = 0;
int g_precise_mode = 0;
int g_psx_call_bail = 0;
int g_boot_state_cpu_timing_restored = 0; /* boot_state.c, read by psx_cycles.c */
int psx_mod_host_output_scope(void) { return 0; } /* mod_runtime.cpp */
uint32_t i_mask = 0;
uint64_t g_guest_store_count = 0;
uint64_t g_mmio_access_count = 0;
static uint32_t g_guest_last_store_addr = 0xFFFFFFFFu, g_guest_last_store_val;
static int g_psx_track_last_store;
static uint32_t s_event_at = 0; /* absolute cycle of the next device event */
void sio_advance(uint32_t c) { (void)c; }
void cdrom_advance(uint32_t c) { (void)c; }
void dma_advance(uint32_t c) { (void)c; }
void timers_advance(uint32_t c) { (void)c; }
void interrupts_advance_cycles(uint32_t c) { (void)c; }
void interrupts_service_scheduled_events(void) {}
uint32_t interrupts_cycles_to_vblank(void) {
uint64_t now = psx_get_cycle_count();
return now >= s_event_at ? 1u : (uint32_t)(s_event_at - now);
}
uint32_t timers_cycles_to_irq(uint32_t m) { (void)m; return UINT32_MAX; }
uint32_t cdrom_cycles_to_irq(uint32_t m) { (void)m; return UINT32_MAX; }
uint32_t dma_cycles_to_internal_event(void) { return UINT32_MAX; }
uint32_t dma_cycles_to_deliverable_irq(uint32_t m) { (void)m; return UINT32_MAX; }
uint32_t sio_cycles_to_irq(uint32_t m) { (void)m; return UINT32_MAX; }
uint32_t psx_spu_sample_event_cycles_to_next(void) { return UINT32_MAX; }
void psx_spu_sample_event_service(void) {}
int psx_get_in_exception(void) { return 0; }
void starvation_watchdog_check(void) {}
void starvation_ring_pc_sample(void) {}
int psx_netplay_active(void) { return 0; }
int psx_selfcheck_enabled(void) { return 0; }
void dirty_ram_ld_delay_discard(void) {}
void dirty_ram_irq_ambient_resync_after_restore(void) {}
/* Guest RAM word the loop counts down, and the host writer under test. */
static uint32_t s_counter;
static int s_host_writes;
static void host_write(uint32_t addr, uint32_t val) {
(void)addr; s_counter = val; s_host_writes++;
}
#define ANCHOR 0x8008B4D0u
#define EDGE2 0x8008B518u
#define COUNTER_ADDR 0x801FFF10u /* stack word in main RAM */
enum { EXTRA_STORE = 1, MMIO_READ = 2, STEP2 = 4, MMIO_STORE = 8 };
static void guest_store(uint32_t addr, uint32_t val) {
g_guest_store_count++;
if (g_psx_track_last_store) { g_guest_last_store_addr = addr; g_guest_last_store_val = val; }
}
static int fails;
static void check(int ok, const char *what) {
printf(" %s %s\n", ok ? "ok " : "FAIL", what);
if (!ok) fails++;
}
static uint64_t s_start, s_after_first_skip;
static uint32_t s_counter_after_first_skip;
static uint64_t run(int ext, int quirks, int passes, uint32_t start_counter) {
static CPUState cpu;
memset(&cpu, 0, sizeof cpu);
cpu.gpr[2] = 1u; /* slt result at the edges: still waiting */
cpu.gpr[29] = 0x801FFF00u;
psx_cycles_resync_after_restore(NULL);
s_start = psx_get_cycle_count();
s_event_at = (uint32_t)s_start + 200000u;
s_after_first_skip = 0;
s_counter = start_counter;
s_host_writes = 0;
g_idle_skip_enabled = 1;
g_idle_skip_ext = ext;
g_idle_skip_count = 0;
for (int i = 0; i < passes && psx_get_cycle_count() < s_event_at; i++) {
psx_idle_note_check(&cpu, ANCHOR);
if (g_idle_skip_count && !s_after_first_skip) {
s_after_first_skip = psx_get_cycle_count();
s_counter_after_first_skip = s_counter;
}
psx_advance_cycles(9);
if (s_counter <= 1) break;
s_counter -= (quirks & STEP2) ? 2u : 1u;
guest_store((quirks & MMIO_STORE) ? 0x1F801070u : COUNTER_ADDR, s_counter);
if (quirks & EXTRA_STORE) guest_store(COUNTER_ADDR + 4u, 0);
if (quirks & MMIO_READ) g_mmio_access_count++;
psx_advance_cycles(7);
psx_idle_note_check(&cpu, EDGE2);
}
return g_idle_skip_count;
}
int main(void) {
psx_idle_skip_set_host_writer(host_write);
psx_idle_skip_set_store_tracker(&g_psx_track_last_store, &g_guest_last_store_addr,
&g_guest_last_store_val);
/* No audited site: the extension never skips. */
psx_idle_skip_set_store_counter_sites(NULL, 0);
check(run(1, 0, 64, 100000u) == 0, "no audited site listed: no skip");
/* A different audited site: this loop is still not eligible. */
const uint32_t other[] = {0x80012340u};
psx_idle_skip_set_store_counter_sites(other, 1);
check(run(1, 0, 64, 100000u) == 0, "loop not at a listed site: no skip");
/* R4's audited v_wait site (both block edges of the loop). */
const uint32_t sites[] = {ANCHOR, EDGE2};
psx_idle_skip_set_store_counter_sites(sites, 2);
uint64_t skips = run(1, 0, 64, 100000u);
check(skips > 0, "v_wait: passes are skipped");
check(s_host_writes > 0, "v_wait: the counter is lowered through the host writer");
check(s_counter > 1u, "v_wait: the counter never reaches the timeout value");
check(s_after_first_skip < s_event_at,
"v_wait: the skip lands on the anchor before the device event "
"(the IRQ is taken by a real pass)");
check(s_event_at - s_after_first_skip < 200000u / 2u,
"v_wait: the skip covers most of the wait");
check((s_after_first_skip - s_start) % 16u == 0,
"v_wait: cycles advanced in whole 16-cycle passes");
check((uint64_t)(100000u - s_counter_after_first_skip) * 16u + 16u ==
s_after_first_skip - s_start + 16u,
"v_wait: the counter dropped by exactly one per pass, skipped passes included");
uint64_t short_skips = run(1, 0, 4000, 40u);
check(s_counter >= 1u && s_counter <= 2u && short_skips > 0,
"v_wait with a small timeout: skips stop before the timeout test");
check(run(0, 0, 64, 100000u) == 0, "option off: no skip");
check(run(1, EXTRA_STORE, 64, 100000u) == 0, "extra store per pass: no skip");
check(run(1, MMIO_READ, 64, 100000u) == 0, "MMIO read per pass: no skip");
check(run(1, STEP2, 64, 100000u) == 0, "counter drops by 2: no skip");
check(run(1, MMIO_STORE, 64, 100000u) == 0, "store to MMIO space: no skip");
psx_idle_skip_set_host_writer(NULL);
check(run(1, 0, 64, 100000u) == 0, "no host writer installed: no skip");
printf("%s (%d failure%s)\n", fails ? "FAILED" : "passed", fails, fails == 1 ? "" : "s");
return fails ? 1 : 0;
}