mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 18:25:35 -04:00
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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user