diff --git a/runtime/include/psx_instr_cost.h b/runtime/include/psx_instr_cost.h index 37639e89..3006cb63 100644 --- a/runtime/include/psx_instr_cost.h +++ b/runtime/include/psx_instr_cost.h @@ -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