timing: GTE-read + MFC0 + muldiv-stall load-delay give-back (ruler#2 100%)

Closes the last steady-state cycle divergences; native now == Beetle on
the complete 12-loop ruler #2 suite.

- MFC2/CFC2 (GTE reg -> GPR read): new psx_gte_read stalls to gte_ts_done
  AND hands the stall amount to following instructions as a load-delay
  give-back (Beetle cpu.cpp:1332-1341: LDAbsorb=gte_ts_done-ts,
  LDWhich=rt). MTC2/CTC2 (writes) keep stall-only psx_gte_stall.
- MFC0: arms ld_absorb=0 / ld_which_t=rt (Beetle delayed load) — sets
  ReadFudge=rt so a load in the next slot gets no fudge.
- psx_muldiv_stall (MFLO/MFHI): now CONSUMES the pending load give-back
  while stalling to muldiv_ts_done, plus the muldiv_ts_done-1 off-by-one
  shortcut (Beetle cpu.cpp:1723-1736). No-load code is unaffected
  (read_absorb==0 -> plain advance).

Wired in both static emitters (code_generator, strict_translator) and
the dirty interp.

METHODOLOGY (Rule 15): Beetle cyc_watch must be sampled at STEADY STATE
— its warm-up window reports the no-give-back value (the source of the
prior "gte_rtps +15 EXACT" mis-record; true steady Beetle = +11).

Validated at steady state, native == Beetle on ALL 12 ruler #2 loops:
the 4 divergences closed (gte_rtps 18->14, gte_nclip 11->7,
gte_read_use 19->14, ld_div 45->49); the 8 CPU-load/alu/div/mult loops
held. Ruler #1 delta 0. Tomba 2 FMV plays (no regression).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-06-27 14:42:47 -07:00
parent fade5603fb
commit d5977976e5
7 changed files with 103 additions and 18 deletions
+17 -4
View File
@@ -1074,7 +1074,14 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr)
uint32_t rt = get_rt(instr);
uint32_t rd = get_rd(instr);
if (cop_op == 0x00) { // MFC0 - move from COP0
code = fmt::format("{} = cpu->cop0[{}]; /* mfc0 */", reg_name(rt), rd);
// MFC0 is a delayed load (Beetle: LDAbsorb=0, LDWhich=rt) — no
// give-back cycles, but it sets ReadFudge=rt so a load in the next
// slot gets no fudge. §1+DO_LDS ran in the block's psx_cyc_step.
code = fmt::format(
"{} = cpu->cop0[{}];"
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n cpu->ld_absorb = 0u; cpu->ld_which_t = {}u;\n#endif"
" /* mfc0 (delayed load) */",
reg_name(rt), rd, rt);
} else if (cop_op == 0x04) { // MTC0 - move to COP0
code = fmt::format("cpu->cop0[{}] = {}; /* mtc0 */", rd, reg_name(rt));
} else if (cop_op == 0x10 && (instr & 0x3F) == 0x10) { // RFE
@@ -1096,14 +1103,20 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr)
// before the access; cost-free unless an op is still in flight.
const char* gte_stall =
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_gte_stall(cpu);\n#endif\n ";
// MFC2/CFC2 (GPR-dest reads): stall to the GTE deadline AND hand the
// stall amount to the next instruction(s) as a load-delay give-back
// (Beetle MFC2/CFC2: LDAbsorb=gte_ts_done-ts, LDWhich=rt). §1+DO_LDS
// ran in the block's psx_cyc_step (COP2 is non-load).
const std::string gte_read = fmt::format(
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_gte_read(cpu, {});\n#endif\n ", rt);
if (cop_op == 0x00) { // MFC2 - move from COP2 data
if ((rd >= 8 && rd <= 11) || rd == 15 || rd == 28 || rd == 29 || rd == 31) {
code = gte_stall + fmt::format("{} = gte_read_data(cpu, {}); /* mfc2 */", reg_name(rt), rd);
code = gte_read + fmt::format("{} = gte_read_data(cpu, {}); /* mfc2 */", reg_name(rt), rd);
} else {
code = gte_stall + fmt::format("{} = cpu->gte_data[{}]; /* mfc2 */", reg_name(rt), rd);
code = gte_read + fmt::format("{} = cpu->gte_data[{}]; /* mfc2 */", reg_name(rt), rd);
}
} else if (cop_op == 0x02) { // CFC2 - move from COP2 control
code = gte_stall + fmt::format("{} = cpu->gte_ctrl[{}]; /* cfc2 */", reg_name(rt), rd);
code = gte_read + fmt::format("{} = cpu->gte_ctrl[{}]; /* cfc2 */", reg_name(rt), rd);
} else if (cop_op == 0x04) { // MTC2 - move to COP2 data
if (rd == 7 || (rd >= 8 && rd <= 11) || rd == 14 || rd == 15 || rd == 28 || rd == 30) {
code = gte_stall + fmt::format("gte_write_data(cpu, {}, {}); /* mtc2 */", rd, reg_name(rt));