mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
docs(timing): empirically-derived load ReadFudge/LDAbsorb model + impl spec
Measured Beetle's per-instruction cost through the ruler #2 load loops via adjacent-PC region cyc_watch (Beetle observes per-instruction; native compiled emitter observes only at block leaders). Nails the last load-path component: - fudge = +2 iff the previous instruction committed no pending load (ReadFudge=0x20; (reg>>4)&2 is 0 for all real regs), else 0 — NOT per-register - region+completion = 5 (main RAM); LDAbsorb give-back excludes fudge - the load-delay-slot instruction does not absorb (its §1 precedes its DO_LDS); instructions after it do, until a GPR_DEP/RES of the loaded reg ends it - Beetle per-insn: load = lw7/addiu1/bne0/nop0; load2 = lw7/lw6/addiu1/bne0/nop0 This is the de-risked spec for a pervasive per-instruction implementation (ReadAbsorb[]/ReadAbsorbWhich/LDWhich in both emitters + the dirty interp), validatable via the ruler-loop anchors (baseline 3 / load 8 / load2 14). Read-only derivation; no runtime/emitter code changed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -213,6 +213,19 @@ on a fixed region -> next.
|
||||
|
||||
## 5. Status / Log (update every session)
|
||||
|
||||
- **2026-06-27 (load ReadFudge/LDAbsorb — MODEL NAILED empirically, impl pending):**
|
||||
Derived the last load-path component (the residual on both rulers) by measuring
|
||||
Beetle's PER-INSTRUCTION cost via adjacent-PC region cyc_watch. Confirmed:
|
||||
fudge = +2 iff the previous instruction committed no pending load (ReadFudge=0x20;
|
||||
`(reg>>4)&2` is 0 for all real regs), else 0; region+completion=5 (LDAbsorb excludes
|
||||
fudge); the load-delay-slot instruction does NOT absorb (its §1 precedes its DO_LDS),
|
||||
the instructions after it do. Per-instruction Beetle data: load = lw7/addiu1/bne0/nop0;
|
||||
load2 = lw7/lw6/addiu1/bne0/nop0. Full model + implementation spec in
|
||||
`accuracy/load_readfudge_ldabsorb.md`. Implementation is pervasive (per-instruction
|
||||
ReadAbsorb + GPR_DEP/RES in both emitters + interp) → a focused next task on a clean
|
||||
tree; validate via the ruler-loop anchors (baseline 3 / load 8 / load2 14). No code
|
||||
changed this step (read-only empirical derivation).
|
||||
|
||||
- **2026-06-27 (interp-path cycle ruler — enabler DONE + validated):** The dirty
|
||||
interp now emits `debug_server_cyc_observe(pc)` per instruction (gated), so
|
||||
interp-executed PCs are cyc_watch-anchorable (were not). Validated: a live
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# Load ReadFudge / LDAbsorb — empirically-derived model + implementation spec
|
||||
|
||||
Status: **MODEL NAILED (empirically, 2026-06-27), implementation PENDING.** This is
|
||||
the last unmodeled piece of the load path — the residual on BOTH rulers
|
||||
(ruler #1 native 54 vs Beetle 56; ruler #2 `load2` native +10 vs Beetle +11). It
|
||||
is the R3000A load-delay pipeline interlock (Beetle `ReadFudge`/`ReadAbsorb[]`/
|
||||
`LDAbsorb`/`LDWhich`). Derived by measuring Beetle's PER-INSTRUCTION cost via
|
||||
adjacent-PC region cyc_watch (Beetle observes per-instruction; the native compiled
|
||||
emitter observes only at block leaders, so native per-insn is not observable — Beetle
|
||||
is the per-instruction oracle here).
|
||||
|
||||
## Empirical per-instruction costs (Beetle oracle, ruler #2 loops)
|
||||
|
||||
| loop | per-instruction Δ (cycles) | per-iter |
|
||||
|-----------|---------------------------------------------|----------|
|
||||
| baseline | addiu **1**, bne **1**, nop **1** | 3 |
|
||||
| load | lw **7**, addiu **1**, bne **0**, nop **0** | 8 (+5) |
|
||||
| load2 | lw1 **7**, lw2 **6**, addiu **1**, bne **0**, nop **0** | 14 (+11) |
|
||||
|
||||
Reading these:
|
||||
- **1st lw = 7**: fudge 2 + region 3 + completion 2. The +2 fudge appears because the
|
||||
instruction before it (nop / branch) committed NO load.
|
||||
- **2nd lw = 6**: fudge **0** (prev instruction was a load) + region 3 + completion 2
|
||||
+ **its own §1 base +1 (NOT absorbed)**. = 0+3+2+1.
|
||||
- **delay-slot instruction (addiu) stays 1**: its §1 runs BEFORE its own DO_LDS sets
|
||||
`ReadAbsorb[loaded]`, so it cannot absorb the load it follows.
|
||||
- **bne, nop = 0**: absorbed from the load's `LDAbsorb` give-back.
|
||||
|
||||
## The model (Beetle cpu.cpp, confirmed by the table above)
|
||||
|
||||
Per instruction, in order:
|
||||
1. **fetch** (§2, separate axis — I-cache; 0 for cached-RAM steady state).
|
||||
2. **§1 base** (cpu.cpp:795-798): `if (ReadAbsorb[ReadAbsorbWhich]) ReadAbsorb[ReadAbsorbWhich]--; else timestamp++;`
|
||||
— `ReadAbsorbWhich` = the GPR the most-recently-committed load wrote.
|
||||
3. **GPR_DEP/RES** (cpu.cpp:702-705): for each source/dest reg of this instruction,
|
||||
`ReadAbsorb[reg]=0` (save/restore `ReadAbsorb[0]`). This ends the give-back when the
|
||||
loaded value is actually used (load-use hazard).
|
||||
4. **execute**; a load (LW/LH/.../LWC2) runs ReadMemory (§3, cpu.cpp:364-451):
|
||||
- `timestamp += (ReadFudge>>4)&2` — the **fudge**. NOTE: `ReadFudge` holds the last
|
||||
load's dest reg (0-31) OR **0x20** when the previous instruction committed no load.
|
||||
`(x>>4)&2` is **0 for any reg 0-31** and **2 only for 0x20**. So fudge = **+2 iff the
|
||||
previous instruction committed no pending load**, else 0. (This is the non-obvious
|
||||
part — it is NOT per-register.)
|
||||
- region wait-state (main RAM = 3; scratchpad 0x1F800000-0x1F8003FF = 0 early-return;
|
||||
other regions differ), then `+2` (CPU load) or `+1` (LWC2/GTE load).
|
||||
- `LDAbsorb = lts - timestamp` = **region + completion (EXCLUDES fudge)** = 5 for main RAM.
|
||||
5. **DO_LDS** (cpu.cpp:800), at the END of the handler: commits the PREVIOUS instruction's
|
||||
pending load: `GPR[LDWhich]=LDValue; ReadAbsorb[LDWhich]=LDAbsorb; ReadFudge=LDWhich;
|
||||
ReadAbsorbWhich |= LDWhich&0x1F; LDWhich=0x20;`. When `LDWhich==0x20` (no pending),
|
||||
`ReadFudge` becomes 0x20 → next load gets +2 fudge.
|
||||
|
||||
Stores are posted (+0, already modeled). Scratchpad loads are +0 (early-return, LDAbsorb=0).
|
||||
|
||||
## Why our current model is off
|
||||
|
||||
Current: flat `+1` base per instruction (PSX_CODEGEN_CYCLE_PER_INSN) + `memory.c`
|
||||
charges a flat region wait-state (PSX_RAM_READ_WAIT_CYCLES=4) per RAM read. This
|
||||
captures region+completion≈5 for an isolated load (matches `load`=+5) but MISSES:
|
||||
(a) the **fudge** (+2 on a load whose predecessor wasn't a load → `load2` 2nd lw),
|
||||
(b) the **absorb give-back** (following instructions going free), and
|
||||
(c) scratchpad=+0. (a) and (b) happen to partially cancel on the `load` loop (why
|
||||
single-load already matches) but not on `load2` / dependent-use sequences.
|
||||
|
||||
## Implementation plan (pervasive — a focused next task)
|
||||
|
||||
1. CPUState (append at end): `ReadFudge`, `ReadAbsorb[33]`, `ReadAbsorbWhich`,
|
||||
`LDWhich`, `LDAbsorb`. (`LDValue`/value-commit already handled by the existing
|
||||
load-delay-slot correctness path — only the TIMING state is new.)
|
||||
2. `psx_cycles.c`: helpers for §1 (`psx_cyc_base`), the load cost+fudge+LDAbsorb arm,
|
||||
and DO_LDS timing-commit. region wait-state stays address-keyed in `memory.c` but
|
||||
must feed LDAbsorb (region+completion), and set fudge from ReadFudge.
|
||||
3. Both emitters (code_generator.cpp, strict_translator.cpp) + dirty interp: replace the
|
||||
flat per-instruction `+1` with the §1 call, and emit `GPR_DEP/RES` zeroing for each
|
||||
instruction's source/dest registers (the pervasive part — every opcode passes its
|
||||
regs). Loads arm LDWhich/LDAbsorb; the next instruction's timing-DO_LDS commits.
|
||||
4. Interaction with muldiv/GTE stalls: those use `*_ts_done` deadlines independent of
|
||||
ReadAbsorb — keep them; just ensure the §1 base replacement doesn't double-count.
|
||||
5. **Validate** via the existing ruler-loop anchors (observable at loop tops): `baseline`=3,
|
||||
`load`=8, `load2`=14 must match Beetle EXACTLY; ruler #1 [c5c→ca4] should reach 56;
|
||||
FMV no-regression. Add a load-then-dependent-use loop to ruler #2 to pin the
|
||||
GPR_DEP give-back termination.
|
||||
|
||||
Risk: pervasive per-instruction emit change to the cost model — do it on a clean tree
|
||||
with full budget, validate one ruler loop at a time.
|
||||
Reference in New Issue
Block a user