With [timing] guest_cycle_scale active, every one-cycle psx_cyc_charge went
straight to psx_cpu_charge and psx_advance_cycles, bypassing the batch the
scale-1 path uses. [timing] guest_cycle_scale_batch (PSX_GCS_BATCH=0/1)
scales the charge in place, with the same fractional carry and order as
psx_cpu_charge, then batches it like the scale-1 path. It flushes at the same
observation points. Default off.
R4 exactness, frame_fingerprint off vs on, 9000 frames each: headless boot
through intro, title and attract; a 1P Grand Prix race; and a 2P VS race.
cyc, mmio, mc and qc are identical on every frame. sp, wr, pc and ws are
identical at the end. The only differences are single-frame VBlank straddles
of wc/sc (never two frames in a row). 0 dispatch misses.
Speed, release builds, races at 60 Hz:
- M4 (macOS sample): psx_cpu_charge 4.3 % -> 0 % of emulation-thread samples;
busy 46.0 -> 44.4 % (2 pairs).
- 9800X3D: no measurable change (30.0 vs 30.5 %; one core 44.6 vs 44.2 %).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The detector cannot prove a loop has no later counter-dependent exit, so
the extension now skips only at block-edge PCs the title lists after an
audit. Unlisted sites are never skipped; an empty list disables it. Docs
record R4's v_wait audit as the reference; tests cover unlisted sites.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Last-store tracking: memory.c owns the record and writes it only while
the extension is on (g_psx_track_last_store); main.cpp hands it and the
host writer (psx_host_write_word) to psx_cycles.c through
psx_idle_skip_set_store_tracker / _set_host_writer, so psx_cycles.c no
longer references memory.c (fixes the tests that link one without the
other).
- Exact from boot: the device distance is now measured from the current
cycle (devices report it from their last service, up to a few hundred
cycles behind), and several edges per pass are accepted only together
with a counter store. Before this, R4's boot diverged at frame 962 (an
OpenBIOS multi-edge poll and a late VBlank IRQ after a v_wait skip).
- tests/test_idle_skip_store_counter.c: a v_wait trace skips (whole
passes, counter lowered by the host writer, never to the timeout, lands
before the event); an extra store, an MMIO read, a decrement of 2, a
store to MMIO, the option off and no host writer do not.
- generated/OpenBIOS.emitter.sha re-stamped.
R4 frame_fingerprint off vs on, 9000 frames each: boot through intro and
attract, a 1P race and a 2P VS race: cyc, mmio, mc, sp, sc and qc
identical on every frame, 0 dispatch misses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #561. Guest time now has two entry points (psx_cycles.h): the
CPU charge psx_cpu_charge() (instruction base, load fudge/region/completion,
i-cache refills, BIOS HLE costs), which applies [timing] guest_cycle_scale,
and the unscaled device advance psx_advance_cycles() (DMA, idle skips,
mul/div and GTE deadline waits). The emitters' block-mode charges emit
psx_cpu_charge; overlay DLLs accumulate it raw and publish through the
cpu_charge callback (was advance_cycles, same slot) into the same host
function, so nothing is scaled at the DLL boundary. The batched
psx_cyc_charge path, the dirty-RAM interpreter, psx_cyc_readmem, i-cache
refills and bios_hle.c all charge through it. At scale 1 it is exactly
psx_advance_cycles.
Gate in psxrecomp: [timing] guest_cycle_scale_gate, RAM predicates judged
at every VBlank edge (deterministic, works online, no mod needed), plus
psx_mod_set_guest_cycle_scale_gate() in mod_plugins.h for custom logic or
to tie the scale to a mod (guest_cycle_scale_gated = true; every mod
session reset shuts it). The carried fraction and both gate states are a
boot_state section (BS_SEC_GCS, written only when the scale is not 1), so
savestates and the netplay rollback snapshot re-simulate the same cycles.
psx_guest_cycle_scale_gate_test pins tier identity, the gates and rollback
re-simulation against the real psx_cycles.c.
OpenBIOS regenerated (emitter stamp); retail SCPH1001 is not.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No CPU is emulated: recompiled code charges the guest clock a fixed cost
per instruction. guest_cycle_scale = N charges 1/N of it (instruction,
load, mul/div, GTE latencies, overlay DLL batches) while VBlank, timers,
CD, SPU and DMA keep hardware time, so the game never runs out of frame
time. A title constant in game.toml (1 = faithful, 1..64), never a player
setting; guest_cycle_scale_gated applies it only while the title's mod
opens the gate (psx_guest_cycle_scale_gate_open). PSX_GUEST_CYCLE_SCALE
overrides for testing. A scale other than 1 is folded into the netplay
content fingerprint as a safety net.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SPU sample event was part of the internal device slicer but not of the
idle-skip observable-event distance, so a poll loop waiting on SPU IRQ9 could
be skipped across several 768-cycle sample boundaries before it could
acknowledge and re-arm. Include the sample deadline in
devices_cycles_to_next_idle_event() when IRQ_SPU is unmasked (mask-gated like
the other sources), expose the distance through
psx_idle_cycles_to_next_observable_event() for tests, and extend
psx_cycle_event_boundaries_test: masked IRQ9 does not bound the skip; unmasked
IRQ9 stops it at the first sample boundary, never across later ones.
Detect a side-effect-free poll loop by its repeated same-PC interrupt
checks and, when the window touched no MMIO and issued no stores,
fast-forward guest time to the next internal device event
(vblank/timer/cdrom/dma/sio) in whole loop quanta — bit-exact versus
executing the spins one by one. Off switch: PSX_IDLE_SKIP=0 env or the
idle_skip TCP command; gated off in exception context, precise mode, and
lockstep.
memory.c adds the two always-on gates the skip requires: g_guest_store_count
(bumped at every raw store chokepoint) and g_mmio_access_count (bumped on
every device-page read/write). Any store or MMIO touch in the window
disqualifies it as a pure poll loop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The "v4" suffix was a holdover from iterative project restarts.
There is now one psxrecomp; all identifiers, include guards, cmake
variables, function names, log strings, schema tags, and tool paths
drop the v4 suffix.
Changes:
- PSXRECOMP_V4_ROOT → PSXRECOMP_ROOT (cmake)
- PSXRECOMP_V4_RUNTIME_SOURCES/_INCLUDE_DIRS/_BIOS_GENERATED → no V4
- psxrecomp_v4_add_runtime_target → psxrecomp_add_runtime_target
- PSXRECOMP_V4_*_H include guards → PSXRECOMP_*_H
- "psxrecomp-v4" log/window strings → "psxrecomp"
- Schema version strings in recompiler JSON output
- Tool script hardcoded absolute paths updated
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Scaffold for guest-cycle-driven peripheral timing per
docs/CYCLE_TIMING_ARCH.md. The counter is defined and exposed via
freeze_check, but psx_advance_cycles has no callers yet — counter
stays 0. Validation: zero behavior change.
Files:
- runtime/include/psx_cycles.h: extern uint64_t psx_cycle_count;
void psx_advance_cycles(uint32_t cycles); uint64_t psx_get_cycle_count(void);
- runtime/src/psx_cycles.c: scaffold body; peripheral hooks empty
(1.0e-b will add sio_advance, 1.0e-c will add gpu_advance, etc.)
- runtime/CMakeLists.txt: psx_cycles.c added to PSX_SHARED_SOURCES
- runtime/src/debug_server.c: freeze_check exposes psx_cycle_count
- docs/CYCLE_TIMING_ARCH.md: design doc — single cycle clock,
generic per-block emit (no card-specific paths), MMIO-split codegen
per explicit user direction, IRQ-pending-vs-CPU-entry separation,
five-step migration plan (1.0e-a..e)
Validation against 0d15e2b baseline:
- t=20s/30s tx_writes match (1160/5480)
- 4x CROSS succeeds; card txn distribution unchanged (10 reach READ_DATA)
- "MC" bytes still flow at txn 0 bytes 10-11
- psx_cycle_count = 0 throughout (no callers in this slice)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>