mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
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:
@@ -213,6 +213,22 @@ on a fixed region -> next.
|
||||
|
||||
## 5. Status / Log (update every session)
|
||||
|
||||
- **2026-06-27 (GTE-read + MFC0 + muldiv give-back — IMPLEMENTED + VALIDATED, ruler #2 100%):**
|
||||
Closed the last steady-state divergences. KEY METHODOLOGY FINDING (Rule 15): Beetle's
|
||||
cyc_watch must be sampled at STEADY STATE — its boot/warm-up window reports the
|
||||
no-give-back value, which is why earlier sessions mis-recorded gte_rtps as "+15 EXACT"
|
||||
(true steady = +11). Added ruler #2 probes (load_use, gte_read_use, ld_div) to Δ-gate it.
|
||||
Shipped: `psx_gte_read` (MFC2/CFC2: stall to gte_ts_done AND arm ld_absorb=stall/
|
||||
ld_which_t=rt give-back; MTC2/CTC2 keep stall-only `psx_gte_stall`); MFC0 arms
|
||||
ld_absorb=0/ld_which_t=rt (suppresses a following load's fudge); `psx_muldiv_stall` now
|
||||
CONSUMES read_absorb during the MFLO/MFHI stall + the muldiv_ts_done-1 off-by-one — all
|
||||
transcribed from Beetle cpu.cpp:1332-1341/1723-1736, in both emitters + the interp.
|
||||
**All 12 ruler #2 loops == Beetle at steady state** (gte_rtps 18→14, gte_nclip 11→7,
|
||||
gte_read_use 19→14, ld_div 45→49 fixed; the 8 CPU-load/alu/div/mult loops held); ruler #1
|
||||
delta 0; Tomba 2 FMV plays (no regression). The R3000A load-delay + GTE/muldiv interlock
|
||||
is now hardware-faithful across every micro-benchmark. NEXT axis: I-cache fetch (ruler #1
|
||||
84/77 cold-refill spikes). Commits on wt/tomba2-load-accuracy (unpushed).
|
||||
|
||||
- **2026-06-27 (load ReadFudge/LDAbsorb — IMPLEMENTED + VALIDATED, both rulers exact):**
|
||||
Shipped the shared per-instruction R3000A load-delay interlock. New `runtime/include/
|
||||
psx_cyc.h`: §1 base + GPR_DEPRES + DO_LDS (`psx_cyc_step`) as static-inline helpers over
|
||||
|
||||
@@ -14,9 +14,19 @@ 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 (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). **NOW Δ-GATED** by new ruler #2 probe loops, measured at
|
||||
FOLLOW-UP — **DONE + VALIDATED (2026-06-27, same session).** GTE-read (MFC2/CFC2) +
|
||||
MFC0 LDAbsorb give-back arming, and the muldiv-stall give-back consumption / off-by-one
|
||||
(Beetle cpu.cpp:1332-1341 / 1723-1736). Shipped: `psx_gte_read` (stall + arm give-back) for
|
||||
MFC2/CFC2 in both emitters + interp (MTC2/CTC2 stay `psx_gte_stall`); MFC0 arms
|
||||
ld_absorb=0/ld_which_t=rt; `psx_muldiv_stall` now consumes read_absorb during the stall +
|
||||
the muldiv_ts_done-1 off-by-one. **All 12 ruler #2 loops now == Beetle at steady state**:
|
||||
the 4 divergences closed (gte_rtps 18→14, gte_nclip 11→7, gte_read_use 19→14, ld_div 45→49)
|
||||
and the 8 prior matches held; ruler #1 delta 0; Tomba 2 FMV plays (jungle scene, no
|
||||
regression). The original FOLLOW-UP text + the Δ-gate table below is retained for the record.
|
||||
|
||||
Original FOLLOW-UP note (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). **Δ-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):
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -1225,8 +1225,11 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
|
||||
|
||||
if (rs == 0x00) { // MFC0 rt, rd -- gpr[rt] = cop0[rd]
|
||||
r.supported = true;
|
||||
r.c_code = emit_gpr_write(rt,
|
||||
fmt::format("cpu->cop0[{}]", static_cast<int>(rd)));
|
||||
// MFC0 is a delayed load (Beetle: LDAbsorb=0, LDWhich=rt): no give-back
|
||||
// cycles, but sets ReadFudge=rt so a load in the next slot gets no fudge.
|
||||
r.c_code = emit_gpr_write(rt, fmt::format("cpu->cop0[{}]", static_cast<int>(rd)))
|
||||
+ fmt::format("\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n cpu->ld_absorb = 0u; cpu->ld_which_t = {}u;\n#endif",
|
||||
static_cast<int>(rt));
|
||||
r.comment = fmt::format("mfc0 {}, cop0[{}]", gpr_name(rt), rd);
|
||||
return r;
|
||||
}
|
||||
@@ -1255,16 +1258,22 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
|
||||
// completion deadline (gte_execute arms it). Cost-free unless in flight.
|
||||
const std::string 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
|
||||
// function's per-instruction 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 ", static_cast<int>(rt));
|
||||
|
||||
if (cop_op == 0x00) { // MFC2 — move from COP2 data register
|
||||
r.supported = true;
|
||||
// Computed/sign-extended data registers must route through the
|
||||
// helper instead of reading the backing array directly.
|
||||
if ((rd >= 8 && rd <= 11) || rd == 15 || rd == 28 || rd == 29 || rd == 31) {
|
||||
r.c_code = gte_stall + emit_gpr_write(rt,
|
||||
r.c_code = gte_read + emit_gpr_write(rt,
|
||||
fmt::format("gte_read_data(cpu, {})", static_cast<int>(rd)));
|
||||
} else {
|
||||
r.c_code = gte_stall + emit_gpr_write(rt,
|
||||
r.c_code = gte_read + emit_gpr_write(rt,
|
||||
fmt::format("cpu->gte_data[{}]", static_cast<int>(rd)));
|
||||
}
|
||||
r.comment = fmt::format("mfc2 {}, gte_data[{}]", gpr_name(rt), rd);
|
||||
@@ -1272,7 +1281,7 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
|
||||
}
|
||||
if (cop_op == 0x02) { // CFC2 — move from COP2 control register
|
||||
r.supported = true;
|
||||
r.c_code = gte_stall + emit_gpr_write(rt,
|
||||
r.c_code = gte_read + emit_gpr_write(rt,
|
||||
fmt::format("cpu->gte_ctrl[{}]", static_cast<int>(rd)));
|
||||
r.comment = fmt::format("cfc2 {}, gte_ctrl[{}]", gpr_name(rt), rd);
|
||||
return r;
|
||||
|
||||
@@ -113,6 +113,9 @@ extern uint32_t psx_mult_latency_u(uint32_t rs); /* MULTU (unsigned) */
|
||||
* per-instruction cycle charging. */
|
||||
extern void psx_gte_set(CPUState* cpu, uint32_t latency);
|
||||
extern void psx_gte_stall(CPUState* cpu);
|
||||
/* MFC2/CFC2 GTE register read: stall to the deadline AND arm the load-delay
|
||||
* give-back (ld_absorb=gte_ts_done-now, ld_which_t=rt) — Beetle MFC2/CFC2. */
|
||||
extern void psx_gte_read(CPUState* cpu, uint32_t rt);
|
||||
extern uint32_t psx_gte_cmd_latency(uint32_t cmd); /* cost-1 for the 6-bit op */
|
||||
|
||||
extern void psx_unaligned_access(CPUState* cpu, uint32_t addr, uint32_t pc);
|
||||
|
||||
@@ -1321,7 +1321,11 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
|
||||
return 0;
|
||||
case 0x10: { /* COP0 */
|
||||
uint32_t cop_op = rs;
|
||||
if (cop_op == 0x00) { /* MFC0 */
|
||||
if (cop_op == 0x00) { /* MFC0 — delayed load (Beetle: LDAbsorb=0, LDWhich=rt) */
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
cpu->ld_absorb = 0u;
|
||||
cpu->ld_which_t = (uint8_t)rt;
|
||||
#endif
|
||||
cpu->gpr[rt] = cpu->cop0[rd];
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
@@ -1341,17 +1345,17 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
|
||||
uint32_t cop_op = rs;
|
||||
/* Faithful GTE: any COP2 register access stalls to the pending command
|
||||
* completion deadline (gte_execute armed it via psx_gte_set). */
|
||||
if (cop_op == 0x00) { /* MFC2 */
|
||||
if (cop_op == 0x00) { /* MFC2 — read: stall + give-back (Beetle) */
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
psx_gte_stall(cpu);
|
||||
psx_gte_read(cpu, rt);
|
||||
#endif
|
||||
cpu->gpr[rt] = gte_read_data(cpu, (uint8_t)rd);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
if (cop_op == 0x02) { /* CFC2 */
|
||||
if (cop_op == 0x02) { /* CFC2 — read: stall + give-back (Beetle) */
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
psx_gte_stall(cpu);
|
||||
psx_gte_read(cpu, rt);
|
||||
#endif
|
||||
cpu->gpr[rt] = gte_read_ctrl(cpu, (uint8_t)rd);
|
||||
cpu->gpr[0] = 0;
|
||||
|
||||
@@ -108,11 +108,41 @@ void psx_muldiv_set(CPUState* cpu, uint32_t latency) {
|
||||
}
|
||||
|
||||
void psx_muldiv_stall(CPUState* cpu) {
|
||||
/* MFLO/MFHI stall to the mult/div completion deadline (Beetle cpu.cpp:1723-1736).
|
||||
* While stalling it CONSUMES a pending load-delay give-back (read_absorb) — each
|
||||
* stalled cycle decrements read_absorb[read_absorb_which] — so cycles that would
|
||||
* have been "free" for following instructions are spent here instead. Plus the
|
||||
* off-by-one shortcut: a deadline exactly one cycle out just retracts (no stall).
|
||||
* (No-load code has read_absorb==0, so this reduces to a plain advance.) */
|
||||
if (cpu->muldiv_ts_done > psx_cycle_count) {
|
||||
psx_advance_cycles((uint32_t)(cpu->muldiv_ts_done - psx_cycle_count));
|
||||
if (cpu->muldiv_ts_done == psx_cycle_count + 1u) {
|
||||
cpu->muldiv_ts_done--; /* off-by-one: retract the deadline, no advance */
|
||||
return;
|
||||
}
|
||||
uint32_t stall = (uint32_t)(cpu->muldiv_ts_done - psx_cycle_count);
|
||||
uint8_t w = cpu->read_absorb_which; /* fixed during the stall */
|
||||
uint32_t give = cpu->read_absorb[w];
|
||||
cpu->read_absorb[w] = (uint8_t)(give > stall ? give - stall : 0u); /* consume */
|
||||
psx_advance_cycles(stall);
|
||||
}
|
||||
}
|
||||
|
||||
/* MFC2/CFC2 (GTE register read → GPR): stall to the GTE command completion deadline
|
||||
* AND hand the stall amount to the next instruction(s) as a load-delay give-back
|
||||
* (Beetle cpu.cpp:1332-1341: LDAbsorb = gte_ts_done - timestamp, LDWhich = rt). The
|
||||
* §1+DO_LDS that bracket this ran in the instruction's psx_cyc_step (COP2 is non-load).
|
||||
* MTC2/CTC2 (writes) use psx_gte_stall (stall only, no give-back). */
|
||||
void psx_gte_read(CPUState* cpu, uint32_t rt) {
|
||||
if (cpu->gte_ts_done > psx_cycle_count) {
|
||||
uint32_t stall = (uint32_t)(cpu->gte_ts_done - psx_cycle_count);
|
||||
cpu->ld_absorb = stall;
|
||||
psx_advance_cycles(stall);
|
||||
} else {
|
||||
cpu->ld_absorb = 0u;
|
||||
}
|
||||
cpu->ld_which_t = (uint8_t)rt;
|
||||
}
|
||||
|
||||
/* ---- GTE (COP2) per-command completion-stall timing ----
|
||||
*
|
||||
* Faithful R3000A/GTE model (Beetle cpu.cpp:1410-1412 + gte.cpp GTE_Instruction
|
||||
|
||||
Reference in New Issue
Block a user