ruler#2: add give-back probes (load_use, gte_read_use, ld_div) + Δ-gates

Adds three cycle-isolation loops that exercise the load-delay give-back
TERMINATION and the two not-yet-modeled give-back interactions, each
measured against the Beetle oracle AT STEADY STATE (Beetle's warm-up
window reports the no-give-back value — the source of the handoff's
mis-recorded "gte_rtps +15 EXACT"; true steady Beetle = +11):

- load_use (lw; addu deps the loaded reg): native +5 == Beetle +5 —
  Δ-validates the GPR_DEP give-back termination shipped in the prior
  commit (load/load2 load into dead regs and never exercise it).
- gte_read_use / gte_rtps / gte_nclip: native OVER-counts by 4-5 — the
  MFC2/CFC2 LDAbsorb give-back (gte_ts_done-now) is unmodeled.
- ld_div (lw; divu; mflo): native UNDER-counts by 4 — the muldiv stall
  must CONSUME the pending lw give-back (Beetle decrements ReadAbsorb
  while stalling MFLO to muldiv_ts_done).

The 8 CPU-load/alu/div/mult components all MATCH at steady state, so the
prior load-interlock commit is solid. The 4 confirmed divergences are
the Δ-gates for the GTE-read/MFC0/muldiv give-back follow-up; spec +
targets in accuracy/load_readfudge_ldabsorb.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-06-27 14:29:31 -07:00
parent d8c4a8ec9f
commit fade5603fb
2 changed files with 35 additions and 3 deletions
+20 -3
View File
@@ -14,10 +14,27 @@ unchanged + matching); ruler #1 [0x80001C5C→0x80001CA4] 54 → **56** == Beetl
intro FMV (no regression). The `(ReadFudge>>4)&2` fudge, the region(RAM +3)+completion(+2)
LDAbsorb give-back, and scratchpad +0 are all modeled per below.
FOLLOW-UP (not in this commit; ACCURACY axis-2): GTE-read (MFC2/CFC2) + MFC0 LDAbsorb
FOLLOW-UP (CONFIRMED divergences — ACCURACY axis-2): GTE-read (MFC2/CFC2) + MFC0 LDAbsorb
give-back arming, and the muldiv-stall give-back consumption / off-by-one (Beetle
cpu.cpp:1332-1338 / 1723-1736). These don't affect the rulers (no CPU-load give-back in the
div/mult/gte loops) but are needed for full faithfulness in mixed game/FMV code.
cpu.cpp:1332-1338 / 1723-1736). **NOW Δ-GATED** by new ruler #2 probe loops, measured at
STEADY STATE (Beetle must run long enough — its warm-up window reports the no-give-back
value, which is why the handoff mis-recorded gte_rtps as "+15 EXACT"; the true steady
Beetle value is +11). Steady-state native(no give-back) vs Beetle(give-back):
| loop | native | Beetle | gap | divergence source |
|---------------|--------|--------|-----|------------------------------------------|
| gte_rtps | 18 | **14** | +4 | MFC2 give-back (LDAbsorb=gte_ts_done-ts) |
| gte_nclip | 11 | **7** | +4 | MFC2 give-back |
| gte_read_use | 19 | **14** | +5 | MFC2 give-back (then dependent use) |
| ld_div | 45 | **49** | −4 | muldiv stall must CONSUME the lw give-back|
Implementation: MFC2/CFC2 — after §1+DO_LDS, stall to gte_ts_done AND set
ld_absorb = (gte_ts_done - now), ld_which_t = rt (replace psx_gte_stall on READS only;
MTC2/CTC2 stay stall-only). MFC0 — arm ld_which_t=rt, ld_absorb=0. muldiv stall (MFLO/MFHI)
— while stalling to muldiv_ts_done, decrement read_absorb[read_absorb_which] per cycle, and
the muldiv_ts_done-1 off-by-one decrement (Beetle cpu.cpp:1723-1736). Re-validate the FULL
12-loop suite at steady state (the 8 passing components must stay matched).
All-CPU-load + alu + div + mult components MATCH at steady state — COMMIT 1 is solid.
---
ORIGINAL SPEC (model derivation) follows.
+15
View File
@@ -153,6 +153,11 @@ def build():
inner_loop("load", [lw("$t4", 0, "$t5")])
# load2: two lw (linearity check: should be ~2x the load delta)
inner_loop("load2", [lw("$t4", 0, "$t5"), lw("$t7", 0, "$t5")])
# load_use: lw then an addu that DEPs the loaded reg ($t4) — the dependent
# use ends the LDAbsorb give-back (GPR_DEP), so the following instrs do NOT
# absorb it. Isolates the give-back-TERMINATION path (load/load2 load into
# dead regs and never exercise it). Δ vs Beetle pins psx_cyc_deps.
inner_loop("load_use", [lw("$t4", 0, "$t5"), addu("$t6", "$t4", "$t3")])
# div: divu + mflo (isolates div latency + stall-on-read, worst case)
inner_loop("div", [divu("$t2", "$t3"), mflo("$t4")])
# div_spaced: divu, 2 filler addu, then mflo (stall partly absorbed)
@@ -168,6 +173,16 @@ def build():
inner_loop("gte_rtps", [cop2cmd(0x01), mfc2("$t4", 14)])
# gte_nclip: NCLIP command (0x06, cost 8) + mfc2 MAC0 (reg 24) read.
inner_loop("gte_nclip", [cop2cmd(0x06), mfc2("$t4", 24)])
# gte_read_use: RTPS + mfc2 result read + an addu that DEPs the mfc2 dest
# ($t4). PROBE for the GTE-read LDAbsorb give-back (Beetle MFC2 sets
# LDAbsorb=gte_ts_done-ts). If our stall-only model diverges from Beetle this
# loop reveals it; if native==Beetle there is no give-back divergence to fix.
inner_loop("gte_read_use", [cop2cmd(0x01), mfc2("$t4", 14), addu("$t6", "$t4", "$t3")])
# ld_div: lw arms a load give-back, then divu+mflo. PROBE for the muldiv-stall
# give-back consumption (Beetle decrements ReadAbsorb while stalling MFLO to
# 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")])
# loop outer forever
o = a.labels["outer"]