From 9ae534dc4294d4207ef90bd763f4d8e623c4e1b9 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 27 Jun 2026 18:17:07 -0700 Subject: [PATCH] timing: faithful device-region MMIO read waits (SPU +36, CDC +6*size, +1, ...) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the placeholder region cost (RAM +3 / everything-else +0) in psx_cyc_readmem with the full Beetle MemRW device-region read-wait table (libretro.cpp), size-aware: main RAM (<0x800000) +3 SPU 0x1F801C00-1FFF +36 (32-bit) / +16 (8/16-bit) CDC 0x1F801800-180F +6 * size GPU/MDEC/SysCtrl/FIO/SIO/IRQ/DMA/Timers (0x1F801000-113F) +1 BIOS ROM / Expansion-PIO / unmatched / scratchpad +0 Thread the access size (1/2/4) from psx_cyc_load_word/half/byte and psx_cyc_lwc2_read into psx_cyc_readmem (SPU/CDC waits are width-dependent). The device wait combines with the existing +2 completion (+1 LWC2) and fudge exactly as Beetle ReadMemory (LDAbsorb = region + completion). Runtime-only: psx_cyc_load_* signatures unchanged, no emitter regen. New ruler #2 loops mmio_timer (Timer0 read -> +3) and mmio_spu (32-bit SPU read -> +38) added to gen_testrom.py. Validated: cyctest compiled (4600) == Beetle (4382) EXACT on all 15 loops incl. mmio_timer +3 / mmio_spu +38; the 13 prior loops unchanged; Tomba 2 boots past the BIOS to its SCEA-Presents intro splash, no freeze. Documented residual: DMACycleSteal (dynamic per-read DMA bus-steal) remains unmodeled — needs the live steal count from the DMA controller; not statically rulerable. Co-Authored-By: Claude Opus 4.8 --- FAITHFUL_TIMING_PLAN.md | 20 ++++++++++ runtime/src/memory.c | 62 ++++++++++++++++++++++-------- tools/cycle_testrom/gen_testrom.py | 12 +++++- 3 files changed, 78 insertions(+), 16 deletions(-) diff --git a/FAITHFUL_TIMING_PLAN.md b/FAITHFUL_TIMING_PLAN.md index 3bde121e..cf0485e8 100644 --- a/FAITHFUL_TIMING_PLAN.md +++ b/FAITHFUL_TIMING_PLAN.md @@ -213,6 +213,26 @@ on a fixed region -> next. ## 5. Status / Log (update every session) +- **2026-06-27 (device-region MMIO read waits — DONE, branch wt/tomba2-mmio-waits off the + I-cache tip):** Replaced the placeholder `region = (phys= 0x1FC00000u && phys <= 0x1FC7FFFFu) return 0u; + if (phys >= 0x1F000000u && phys <= 0x1F7FFFFFu) return 0u; + /* Hardware MMIO window 0x1F801000..0x1F802FFF (921). */ + if (phys >= 0x1F801000u && phys <= 0x1F802FFFu) { + if (phys >= 0x1F801C00u && phys <= 0x1F801FFFu) /* SPU (929) */ + return (size == 4u) ? 36u : 16u; + if (phys >= 0x1F801800u && phys <= 0x1F80180Fu) /* CDC (979) */ + return 6u * size; + if (phys >= 0x1F801810u && phys <= 0x1F801817u) return 1u; /* GPU (994) */ + if (phys >= 0x1F801820u && phys <= 0x1F801827u) return 1u; /* MDEC (1007) */ + if (phys >= 0x1F801000u && phys <= 0x1F801023u) return 1u; /* SysControl (1020) */ + if (phys >= 0x1F801040u && phys <= 0x1F80104Fu) return 1u; /* FrontIO/pad (1043) */ + if (phys >= 0x1F801050u && phys <= 0x1F80105Fu) return 1u; /* SIO (1055) */ + if (phys >= 0x1F801070u && phys <= 0x1F801077u) return 1u; /* IRQ (1094) */ + if (phys >= 0x1F801080u && phys <= 0x1F8010FFu) return 1u; /* DMA (1106) */ + if (phys >= 0x1F801100u && phys <= 0x1F80113Fu) return 1u; /* Timers (1119) */ + return 0u; /* unmatched MMIO: Beetle adds no device wait */ + } + return 0u; /* unknown / open-bus region */ +} + /* Beetle ReadMemory data-access timing (cpu.cpp:369-448), after §1/deps/DO_LDS. * compl_cost = 2 (CPU load) / 1 (LWC2); arm_rt = GPR to arm as pending load, or - * 0x20 = none (LWC2, dest is a GTE reg). */ -static inline void psx_cyc_readmem(CPUState* cpu, uint32_t phys, + * 0x20 = none (LWC2, dest is a GTE reg). size = access width in bytes (1/2/4). */ +static inline void psx_cyc_readmem(CPUState* cpu, uint32_t phys, uint32_t size, uint32_t compl_cost, uint32_t arm_rt) { /* ReadMemory start (369-370): clear the current give-back slot. */ cpu->read_absorb[cpu->read_absorb_which] = 0u; @@ -904,8 +935,8 @@ static inline void psx_cyc_readmem(CPUState* cpu, uint32_t phys, } /* fudge (424): +2 iff the predecessor committed no load (read_fudge==0x20). */ psx_advance_cycles((uint32_t)((cpu->read_fudge >> 4) & 2u)); - uint32_t region = (phys < RAM_SIZE) ? 3u : 0u; /* main RAM +3; others: separate axis */ - uint32_t cost = region + compl_cost; /* LDAbsorb = region + completion */ + uint32_t region = psx_mmio_read_wait(phys, size); /* device-region wait */ + uint32_t cost = region + compl_cost; /* LDAbsorb = region + completion */ cpu->ld_absorb = cost; psx_advance_cycles(cost); cpu->ld_which_t = (uint8_t)arm_rt; @@ -913,37 +944,38 @@ static inline void psx_cyc_readmem(CPUState* cpu, uint32_t phys, /* The interlock half of a load (§1+deps+(cancel)+DO_LDS+ReadMemory). Gated on * PSX_ENABLE_BLOCK_CYCLES so the Beetle-oracle build (cycles off) does a plain read. */ -static inline void psx_cyc_load_timing(CPUState* cpu, uint32_t addr, +static inline void psx_cyc_load_timing(CPUState* cpu, uint32_t addr, uint32_t size, uint32_t rt, uint32_t reg_mask) { #ifdef PSX_ENABLE_BLOCK_CYCLES psx_cyc_base(cpu); psx_cyc_deps(cpu, reg_mask); if (cpu->ld_which_t == rt) cpu->ld_which_t = 0u; /* cancel pending load to same dest */ psx_cyc_lds(cpu); - psx_cyc_readmem(cpu, addr & 0x1FFFFFFFu, 2u, rt); + psx_cyc_readmem(cpu, addr & 0x1FFFFFFFu, size, 2u, rt); #else - (void)cpu; (void)addr; (void)rt; (void)reg_mask; + (void)cpu; (void)addr; (void)size; (void)rt; (void)reg_mask; #endif } uint32_t psx_cyc_load_word(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask) { - psx_cyc_load_timing(cpu, addr, rt, reg_mask); + psx_cyc_load_timing(cpu, addr, 4u, rt, reg_mask); return psx_read_word(addr); } uint16_t psx_cyc_load_half(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask) { - psx_cyc_load_timing(cpu, addr, rt, reg_mask); + psx_cyc_load_timing(cpu, addr, 2u, rt, reg_mask); return psx_read_half(addr); } uint8_t psx_cyc_load_byte(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask) { - psx_cyc_load_timing(cpu, addr, rt, reg_mask); + psx_cyc_load_timing(cpu, addr, 1u, rt, reg_mask); return psx_read_byte(addr); } /* LWC2 (GTE load): §1/DO_LDS done by psx_cyc_step(cpu,0); the GTE deadline stall by - * psx_gte_stall — both emitted before this call. Completion +1, no LDWhich arm. */ + * psx_gte_stall — both emitted before this call. 32-bit access, completion +1, no + * LDWhich arm. */ uint32_t psx_cyc_lwc2_read(CPUState* cpu, uint32_t addr) { #ifdef PSX_ENABLE_BLOCK_CYCLES - psx_cyc_readmem(cpu, addr & 0x1FFFFFFFu, 1u, 0x20u); + psx_cyc_readmem(cpu, addr & 0x1FFFFFFFu, 4u, 1u, 0x20u); #else (void)cpu; #endif diff --git a/tools/cycle_testrom/gen_testrom.py b/tools/cycle_testrom/gen_testrom.py index dcb727b8..fc101452 100644 --- a/tools/cycle_testrom/gen_testrom.py +++ b/tools/cycle_testrom/gen_testrom.py @@ -38,7 +38,7 @@ REG.update({ # ABI names we use "$a0": 4, "$a1": 5, "$a2": 6, "$a3": 7, "$t0": 8, "$t1": 9, "$t2": 10, "$t3": 11, "$t4": 12, "$t5": 13, "$t6": 14, "$t7": 15, - "$s0": 16, "$s1": 17, "$ra": 31, "$sp": 29, "$gp": 28, + "$s0": 16, "$s1": 17, "$t8": 24, "$t9": 25, "$ra": 31, "$sp": 29, "$gp": 28, }) def _r(x): @@ -119,6 +119,7 @@ def build(): a.label("entry") a.emit(li("$sp", STACK_TOP)) a.emit(li("$t5", SCRATCH_RAM)) # load/store base + a.emit(li("$t8", 0x1F800000)) # MMIO base (scratchpad base; +0x1100 timer, +0x1C00 SPU) a.emit(li("$t2", 0x12345678)) # div/mult operand (dividend/multiplicand) a.emit(li("$t3", 0x0000007F)) # div/mult operand (divisor/multiplier, !=0) # store something at the scratch addr so loads read a known value @@ -183,6 +184,15 @@ def build(): # muldiv_ts_done). Divergence here would mean the muldiv stall must consume the # pending load give-back; native==Beetle means no divergence to fix. inner_loop("ld_div", [lw("$t4", 0, "$t5"), divu("$t2", "$t3"), mflo("$t7")]) + # mmio_timer: one lw from Timer0 (0x1F801100). Isolates a device-region MMIO read + # wait: Beetle MemRW charges +1 for the timer/counter region, then ReadMemory adds + # the +2 completion -> component +3 (vs main-RAM load's region 3 -> +5). Timer reads + # are side-effect-free + value-independent, so the per-iter delta is deterministic. + inner_loop("mmio_timer", [lw("$t4", 0x1100, "$t8")]) + # mmio_spu: one 32-bit lw from the SPU region (0x1F801C00, voice 0). The dramatic + # case: Beetle MemRW charges +36 for a 32-bit SPU read, + the +2 completion -> + # component +38. Pins the size-dependent SPU wait (the largest device wait modeled). + inner_loop("mmio_spu", [lw("$t4", 0x1C00, "$t8")]) # icache_miss: force an I-cache miss every iteration via 4KB aliasing. The loop # top and a "victim" block 0x1000 bytes away map to the SAME direct-mapped line