feat(timing): Stage-2 #1a — data-load cost in psx_instr_base_cycles (Δ-validated)

Transcribed from Beetle PS_CPU::ReadMemory: a CPU load adds +2 cycles (LWC2 +1);
stores posted (~0). psx_instr_base_cycles now returns 3 for LB/LH/LWL/LW/LBU/LHU/LWR,
2 for LWC2, 1 else. Flows to BOTH backends via the single-source seam.
Δ GATE (cyc_watch @0x80017FC4, native vs Beetle per-iteration): 46 -> 56 (toward
Beetle 91) — load cost closed ~10 cyc; remaining ~35 = later components (I-cache
fetch, mult/div, GTE, region/absorb refinements). FMV still streams (no regression).
Approximation noted in the header: no scratchpad-free / load-delay-absorb / region
yet (Δ-gated next).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-06-26 20:59:19 -07:00
parent 077ffa2104
commit 2ef47bd388
+25 -5
View File
@@ -31,12 +31,32 @@
extern "C" {
#endif
/* CPU instruction base cycle cost (execute cycles only; excludes data-access
* wait states, which the memory path charges). `insn` is the raw 32-bit MIPS
* word. Stage 1: identity. */
/* Guest CPU cycle cost for one retired instruction. `insn` is the raw 32-bit MIPS
* word. Transcribed from the in-tree Beetle oracle (mednafen psx/cpu.cpp) + psx-spx;
* each component verified against Beetle via the cyc_watch DELTA gate
* (FAITHFUL_TIMING_PLAN.md §3c). Both backends consume this one function.
*
* STAGE 2 component #1 — data-memory access (the dominant ~2x gap):
* Beetle PS_CPU::ReadMemory: a normal CPU load adds `lts += 2` (LWC loads +1);
* stores are POSTED (WriteMemory adds no CPU stall); scratchpad reads add 0.
* So: load opcode = 1 (execute) + 2 (data access) = 3; LWC2 = 2; store/other = 1.
* NOT YET MODELED (later Δ-gated components, will adjust here / in the mem path):
* - scratchpad-vs-RAM region (scratchpad load should be 1, not 3),
* - the read-after-load "fudge" (+0/2) and load-delay ABSORB (next-insn discount),
* - I-cache instruction-fetch timing, mult/div latency, GTE per-command costs.
* This RAM-load base is an upper bound on the load component (no absorb yet); the
* Δ gate shows whether we under/overshoot and what to add next. */
static inline uint32_t psx_instr_base_cycles(uint32_t insn) {
(void)insn;
return 1u;
uint32_t op = insn >> 26;
switch (op) {
case 0x20: case 0x21: case 0x22: case 0x23: /* LB LH LWL LW */
case 0x24: case 0x25: case 0x26: /* LBU LHU LWR */
return 3u; /* 1 execute + 2 data-access */
case 0x32: /* LWC2 (GTE load): +1 */
return 2u;
default:
return 1u;
}
}
#ifdef __cplusplus