feat(timing): BIOS emitter per-instruction mode + muldiv stall — ruler #1 now EXACT

Apply the faithful mult/div completion-stall to the BIOS emitter so real kernel
code is cycle-accurate, not just the game-emitter test ROM.

- full_function_emitter.cpp: per-instruction cycle charging (default on, same
  PSX_CODEGEN_CYCLE_PER_INSN gate as the game emitter). One psx_advance_cycles(1)
  at the top of every in-function instruction (terminators, non-terminators, and
  in-block delay slots are all separate addr_to_raw entries → execution order),
  plus the 4 inlined orphaned-delay-slot sites. Block-up-front charge disabled in
  per-insn mode. The +1-at-top convention keeps the divu-set / mflo-stall offsets
  cancelling identically to the game emitter (verified exact).
- strict_translator.cpp: MULTU/DIV/DIVU emit psx_muldiv_set (DIV/DIVU=37, MULTU via
  psx_mult_latency_u); MFHI/MFLO emit psx_muldiv_stall. (Signed MULT has zero BIOS
  inventory, so it's unimplemented here as before.)

RESULT — ruler #1 [0x80001C5C->0x80001CA4] (BIOS EvCB-search prologue, divu+mflo):
  native 30 -> 56, Beetle 56 — STEADY DELTA 0, EXACT. (The remaining 84-on-cold-hit
  spikes are Beetle's I-cache line refill — the next component, fetch timing.)
Tomba 2 FMV no regression (i_stat 0x8D, full frame verified).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-06-27 00:22:01 -07:00
parent eeb24bdc8f
commit 180b821fab
2 changed files with 69 additions and 13 deletions
+58 -7
View File
@@ -5,6 +5,7 @@
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <fstream>
#include <map>
#include <set>
@@ -19,6 +20,16 @@
namespace PSXRecompV4 {
// Per-instruction cycle charging (mirrors code_generator.cpp). DEFAULT ON for
// the faithful-timing branch so the running cycle count is accurate mid-block —
// required for the mult/div completion-stall (mflo/mfhi wait for muldiv_ts_done)
// to absorb correctly. Set PSX_CODEGEN_CYCLE_PER_INSN=0 to force block-up-front.
static bool bios_cycle_per_insn() {
const char* e = std::getenv("PSX_CODEGEN_CYCLE_PER_INSN");
if (e != nullptr && e[0] != '\0') return e[0] != '0';
return true;
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
@@ -362,6 +373,8 @@ bool FullFunctionEmitter::emit_function(
}
};
const bool per_insn_cycles = bios_cycle_per_insn();
for (auto it = addr_to_raw.begin(); it != addr_to_raw.end(); ++it) {
uint32_t addr = it->first;
uint32_t raw = it->second;
@@ -386,19 +399,33 @@ bool FullFunctionEmitter::emit_function(
out += "#endif\n";
// Phase 1.0e-d: advance guest cycles for this block. Macro-
// gated; when off, generated code matches pre-1.0e-d output.
uint32_t bcyc = 0;
auto bcit = block_cycles.find(addr);
if (bcit != block_cycles.end()) bcyc = bcit->second;
if (bcyc > 0) {
out += "#ifdef PSX_ENABLE_BLOCK_CYCLES\n";
out += fmt::format(" psx_advance_cycles({}u);\n", bcyc);
out += "#endif\n";
// In per-instruction mode the charge is emitted per instruction
// below (so muldiv/GTE stalls absorb correctly), NOT block-up-front.
if (!per_insn_cycles) {
uint32_t bcyc = 0;
auto bcit = block_cycles.find(addr);
if (bcit != block_cycles.end()) bcyc = bcit->second;
if (bcyc > 0) {
out += "#ifdef PSX_ENABLE_BLOCK_CYCLES\n";
out += fmt::format(" psx_advance_cycles({}u);\n", bcyc);
out += "#endif\n";
}
}
if (should_probe_pc(addr)) {
out += fmt::format(" debug_server_log_probe(0x{:08X}u, cpu);\n", addr);
}
}
// Per-instruction cycle charge (faithful-timing mode). Emitted for EVERY
// in-function instruction (terminators, non-terminators, in-block delay
// slots are all separate addr_to_raw entries) in execution order, so the
// running cycle count is exact at MULT/DIV/MFLO/MFHI (and later GTE) for
// the completion-stall to absorb correctly. Orphaned (out-of-function)
// delay slots are inlined elsewhere and charged at those sites.
if (per_insn_cycles) {
out += "#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_advance_cycles(1u);\n#endif\n";
}
// Decode and translate.
PSXRecomp::DecodedInstruction d = PSXRecomp::MipsDecoder::decode(raw, addr);
TranslateResult tr = StrictTranslator::translate(d);
@@ -521,6 +548,12 @@ bool FullFunctionEmitter::emit_function(
out += fmt::format(" /* DELAY (orphaned) 0x{:08X}: {:08X} {} */\n",
ds_addr, ds_raw, ds_tr.comment);
out += fmt::format(" {}\n", ds_tr.c_code);
// Orphaned delay slot is inlined here (not an
// addr_to_raw entry), so charge its cycle directly
// in per-instruction mode (block mode already counts
// it in the owning block_cycles).
if (per_insn_cycles)
out += "#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_advance_cycles(1u);\n#endif\n";
}
}
// Resolve branch. Target was decoded as absolute virtual addr.
@@ -552,6 +585,12 @@ bool FullFunctionEmitter::emit_function(
out += fmt::format(" /* DELAY (orphaned) 0x{:08X}: {:08X} {} */\n",
ds_addr, ds_raw, ds_tr.comment);
out += fmt::format(" {}\n", ds_tr.c_code);
// Orphaned delay slot is inlined here (not an
// addr_to_raw entry), so charge its cycle directly
// in per-instruction mode (block mode already counts
// it in the owning block_cycles).
if (per_insn_cycles)
out += "#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_advance_cycles(1u);\n#endif\n";
}
}
uint32_t target = relocate_j_target(addr, tr.terminator_target);
@@ -572,6 +611,12 @@ bool FullFunctionEmitter::emit_function(
out += fmt::format(" /* DELAY (orphaned) 0x{:08X}: {:08X} {} */\n",
ds_addr, ds_raw, ds_tr.comment);
out += fmt::format(" {}\n", ds_tr.c_code);
// Orphaned delay slot is inlined here (not an
// addr_to_raw entry), so charge its cycle directly
// in per-instruction mode (block mode already counts
// it in the owning block_cycles).
if (per_insn_cycles)
out += "#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_advance_cycles(1u);\n#endif\n";
}
}
uint32_t target = relocate_j_target(addr, tr.terminator_target);
@@ -609,6 +654,12 @@ bool FullFunctionEmitter::emit_function(
out += fmt::format(" /* DELAY (orphaned) 0x{:08X}: {:08X} {} */\n",
ds_addr, ds_raw, ds_tr.comment);
out += fmt::format(" {}\n", ds_tr.c_code);
// Orphaned delay slot is inlined here (not an
// addr_to_raw entry), so charge its cycle directly
// in per-instruction mode (block mode already counts
// it in the owning block_cycles).
if (per_insn_cycles)
out += "#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_advance_cycles(1u);\n#endif\n";
}
}
uint8_t rs = (raw >> 21) & 0x1F;
+11 -6
View File
@@ -245,7 +245,8 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
case 0x10: { // MFHI rd
r.supported = true;
r.c_code = emit_gpr_write(rd, "cpu->hi");
r.c_code = emit_gpr_write(rd, "cpu->hi")
+ "\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_muldiv_stall(cpu);\n#endif";
r.comment = fmt::format("mfhi {}", gpr_name(rd));
return r;
}
@@ -260,7 +261,8 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
case 0x12: { // MFLO rd
r.supported = true;
r.c_code = emit_gpr_write(rd, "cpu->lo");
r.c_code = emit_gpr_write(rd, "cpu->lo")
+ "\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_muldiv_stall(cpu);\n#endif";
r.comment = fmt::format("mflo {}", gpr_name(rd));
return r;
}
@@ -278,8 +280,9 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
r.c_code = fmt::format(
"{{ uint64_t psx_p = (uint64_t)cpu->gpr[{}] * (uint64_t)cpu->gpr[{}]; "
"cpu->lo = (uint32_t)(psx_p & 0xFFFFFFFFu); "
"cpu->hi = (uint32_t)(psx_p >> 32); }}",
static_cast<int>(rs), static_cast<int>(rt));
"cpu->hi = (uint32_t)(psx_p >> 32); }}"
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_muldiv_set(cpu, psx_mult_latency_u(cpu->gpr[{}]));\n#endif",
static_cast<int>(rs), static_cast<int>(rt), static_cast<int>(rs));
r.comment = fmt::format("multu {}, {}", gpr_name(rs), gpr_name(rt));
return r;
}
@@ -312,7 +315,8 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
"int32_t psx_r = psx_n - (psx_q * psx_d); "
"cpu->lo = (uint32_t)psx_q; "
"cpu->hi = (uint32_t)psx_r; "
"}} }}",
"}} }}"
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_muldiv_set(cpu, 37u);\n#endif",
static_cast<int>(rs), static_cast<int>(rt));
r.comment = fmt::format("div {}, {}", gpr_name(rs), gpr_name(rt));
return r;
@@ -337,7 +341,8 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
"uint32_t psx_r = psx_n - (psx_q * psx_d); "
"cpu->lo = psx_q; "
"cpu->hi = psx_r; "
"}} }}",
"}} }}"
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_muldiv_set(cpu, 37u);\n#endif",
static_cast<int>(rs), static_cast<int>(rt));
r.comment = fmt::format("divu {}, {}", gpr_name(rs), gpr_name(rt));
return r;