mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-27 02:32:02 -04:00
feat(timing): faithful mult/div completion-stall + ruler #2 native half (validated EXACT)
Stage-2's biggest cost gap closed. MULT/MULTU/DIV/DIVU now set a completion deadline; MFLO/MFHI stall guest cycles until it — exactly like Beetle's muldiv_ts_done. Latencies transcribed from cpu.cpp: DIV/DIVU=37; MULT via MULT_Tab24 (14/10/7) on the first-operand magnitude. Native previously charged ZERO for this. - Per-instruction cycle charging is now the DEFAULT on this audit branch (PSX_CODEGEN_CYCLE_PER_INSN), required so the stall absorbs correctly (the running cycle count must be accurate mid-block). Validated cycle-neutral for the identity base model; Tomba 2 still reaches the FMV. - runtime: CPUState.muldiv_ts_done + psx_muldiv_set/stall + psx_mult_latency_s/u in psx_cycles.c. Emitted at the mult/div/mflo/mfhi sites in the GAME emitter (gated PSX_ENABLE_BLOCK_CYCLES). Stall fires even for mflo/mfhi to $zero. - ruler #2 native half: tools/cycle_testrom/CMakeLists.txt builds psx-cyctest (links the recompiled test EXE), boots disc/cyctest.cue (serial CYCT-00101 so disc-identity matches, no warning dialog). measure.py --port 4600. VALIDATION (native vs Beetle oracle, per-iteration component cost): div +38 vs +38 EXACT | div_spaced +38 vs +38 EXACT (absorb works) | mult +15 vs +15 EXACT | baseline/alu match. Remaining: load native +7 vs Beetle +5 (memory.c flat +6 wait-state needs calibration to ~4) — a smaller refinement. BIOS emitter still block-mode (ruler #1 unchanged at 30) — applying per-insn + muldiv there is the next step to close ruler #1's div-stall gap. No FMV regression (i_stat 0x8D, screenshot verified). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -15,8 +15,16 @@
|
||||
namespace PSXRecomp {
|
||||
|
||||
static bool codegen_cycle_per_insn() {
|
||||
// DEFAULT ON for the faithful-timing (cycle-audit) branch: each instruction
|
||||
// charges its cost at its own site, so the running cycle count is correct
|
||||
// mid-block. This is REQUIRED for stateful timing — mult/div completion-stall
|
||||
// (mflo/mfhi wait for muldiv_ts_done) and later GTE stalls — to absorb
|
||||
// correctly, exactly like Beetle's per-instruction timestamp. Block-up-front
|
||||
// charging loses the in-between timing and can only charge full latency.
|
||||
// Set PSX_CODEGEN_CYCLE_PER_INSN=0 to force the old block-up-front mode.
|
||||
const char* e = std::getenv("PSX_CODEGEN_CYCLE_PER_INSN");
|
||||
return e != nullptr && e[0] != '\0' && e[0] != '0';
|
||||
if (e != nullptr && e[0] != '\0') return e[0] != '0';
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -628,16 +636,18 @@ std::string CodeGenerator::translate_mult(uint32_t instr) {
|
||||
uint32_t rs = get_rs(instr);
|
||||
uint32_t rt = get_rt(instr);
|
||||
|
||||
return fmt::format("{{ int64_t result = (int64_t)(int32_t){} * (int64_t)(int32_t){}; cpu->lo = (uint32_t)result; cpu->hi = (uint32_t)(result >> 32); }}",
|
||||
reg_name(rs), reg_name(rt));
|
||||
return fmt::format("{{ int64_t result = (int64_t)(int32_t){} * (int64_t)(int32_t){}; cpu->lo = (uint32_t)result; cpu->hi = (uint32_t)(result >> 32); }}"
|
||||
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_muldiv_set(cpu, psx_mult_latency_s({}));\n#endif",
|
||||
reg_name(rs), reg_name(rt), reg_name(rs));
|
||||
}
|
||||
|
||||
std::string CodeGenerator::translate_multu(uint32_t instr) {
|
||||
uint32_t rs = get_rs(instr);
|
||||
uint32_t rt = get_rt(instr);
|
||||
|
||||
return fmt::format("{{ uint64_t result = (uint64_t){} * (uint64_t){}; cpu->lo = (uint32_t)result; cpu->hi = (uint32_t)(result >> 32); }}",
|
||||
reg_name(rs), reg_name(rt));
|
||||
return fmt::format("{{ uint64_t result = (uint64_t){} * (uint64_t){}; cpu->lo = (uint32_t)result; cpu->hi = (uint32_t)(result >> 32); }}"
|
||||
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_muldiv_set(cpu, psx_mult_latency_u({}));\n#endif",
|
||||
reg_name(rs), reg_name(rt), reg_name(rs));
|
||||
}
|
||||
|
||||
std::string CodeGenerator::translate_div(uint32_t instr) {
|
||||
@@ -646,7 +656,8 @@ std::string CodeGenerator::translate_div(uint32_t instr) {
|
||||
|
||||
// MIPS division: quotient in LO, remainder in HI
|
||||
// Handle division by zero (MIPS behavior: result is unpredictable but doesn't trap)
|
||||
return fmt::format("if ({} != 0) {{ cpu->lo = (uint32_t)((int32_t){} / (int32_t){}); cpu->hi = (uint32_t)((int32_t){} % (int32_t){}); }} else {{ cpu->lo = ((int32_t){} >= 0) ? 0xFFFFFFFF : 1; cpu->hi = (uint32_t){}; }}",
|
||||
return fmt::format("if ({} != 0) {{ cpu->lo = (uint32_t)((int32_t){} / (int32_t){}); cpu->hi = (uint32_t)((int32_t){} % (int32_t){}); }} else {{ cpu->lo = ((int32_t){} >= 0) ? 0xFFFFFFFF : 1; cpu->hi = (uint32_t){}; }}"
|
||||
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_muldiv_set(cpu, 37u);\n#endif",
|
||||
reg_name(rt), reg_name(rs), reg_name(rt), reg_name(rs), reg_name(rt), reg_name(rs), reg_name(rs));
|
||||
}
|
||||
|
||||
@@ -656,28 +667,33 @@ std::string CodeGenerator::translate_divu(uint32_t instr) {
|
||||
|
||||
// MIPS division: quotient in LO, remainder in HI
|
||||
// Handle division by zero (MIPS behavior: result is unpredictable but doesn't trap)
|
||||
return fmt::format("if ({} != 0) {{ cpu->lo = {} / {}; cpu->hi = {} % {}; }} else {{ cpu->lo = 0xFFFFFFFF; cpu->hi = {}; }}",
|
||||
return fmt::format("if ({} != 0) {{ cpu->lo = {} / {}; cpu->hi = {} % {}; }} else {{ cpu->lo = 0xFFFFFFFF; cpu->hi = {}; }}"
|
||||
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_muldiv_set(cpu, 37u);\n#endif",
|
||||
reg_name(rt), reg_name(rs), reg_name(rt), reg_name(rs), reg_name(rt), reg_name(rs));
|
||||
}
|
||||
|
||||
std::string CodeGenerator::translate_mfhi(uint32_t instr) {
|
||||
uint32_t rd = get_rd(instr);
|
||||
|
||||
// Stall until the mult/div completion deadline (faithful R3000A) — happens
|
||||
// even when rd==$zero (the read still stalls on HW).
|
||||
const char* stall = "\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_muldiv_stall(cpu);\n#endif";
|
||||
if (config_.optimize_zero_reg && rd == 0) {
|
||||
return "/* nop: write to $zero */";
|
||||
return fmt::format("/* nop: write to $zero */{}", stall);
|
||||
}
|
||||
|
||||
return fmt::format("{} = cpu->hi;", reg_name(rd));
|
||||
return fmt::format("{} = cpu->hi;{}", reg_name(rd), stall);
|
||||
}
|
||||
|
||||
std::string CodeGenerator::translate_mflo(uint32_t instr) {
|
||||
uint32_t rd = get_rd(instr);
|
||||
|
||||
const char* stall = "\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_muldiv_stall(cpu);\n#endif";
|
||||
if (config_.optimize_zero_reg && rd == 0) {
|
||||
return "/* nop: write to $zero */";
|
||||
return fmt::format("/* nop: write to $zero */{}", stall);
|
||||
}
|
||||
|
||||
return fmt::format("{} = cpu->lo;", reg_name(rd));
|
||||
return fmt::format("{} = cpu->lo;{}", reg_name(rd), stall);
|
||||
}
|
||||
|
||||
std::string CodeGenerator::translate_mthi(uint32_t instr) {
|
||||
|
||||
@@ -55,6 +55,12 @@ typedef struct CPUState {
|
||||
* precompiled gcc overlay DLLs (which bake field offsets) are unaffected;
|
||||
* only the sljit JIT reads it. Populated once at startup. */
|
||||
void *sljit_helpers[SLJIT_HLP_COUNT];
|
||||
|
||||
/* Mult/div completion deadline (absolute guest cycle). Set by MULT/MULTU/
|
||||
* DIV/DIVU to psx_cycle_count()+latency; a later MFLO/MFHI stalls (advances
|
||||
* cycles) until this deadline — faithful R3000A behavior (Beetle
|
||||
* muldiv_ts_done). Appended at END so prior field offsets are unchanged. */
|
||||
uint64_t muldiv_ts_done;
|
||||
} CPUState;
|
||||
|
||||
/* Trap trampolines — defined in runtime/src/traps.c */
|
||||
@@ -66,6 +72,16 @@ typedef struct CPUState {
|
||||
* value and rely on cpu->pc (0 = handled, resume at caller). */
|
||||
extern int psx_syscall(CPUState* cpu, uint32_t code);
|
||||
extern void psx_arith_overflow(CPUState* cpu);
|
||||
/* Mult/div completion-stall timing (psx_cycles.c). MULT/MULTU/DIV/DIVU call
|
||||
* psx_muldiv_set with their latency (DIV/DIVU = 37; MULT/MULTU via the latency
|
||||
* helpers, indexed on the first operand magnitude). MFLO/MFHI call
|
||||
* psx_muldiv_stall, which advances guest cycles to the deadline if not yet
|
||||
* reached. Faithful only with per-instruction cycle charging. */
|
||||
extern void psx_muldiv_set(CPUState* cpu, uint32_t latency);
|
||||
extern void psx_muldiv_stall(CPUState* cpu);
|
||||
extern uint32_t psx_mult_latency_s(uint32_t rs); /* MULT (signed) */
|
||||
extern uint32_t psx_mult_latency_u(uint32_t rs); /* MULTU (unsigned) */
|
||||
|
||||
extern void psx_unaligned_access(CPUState* cpu, uint32_t addr, uint32_t pc);
|
||||
extern void psx_break(CPUState* cpu, uint32_t code, uint32_t pc);
|
||||
/* Fail-closed native entry guard: a function's CPS entry-switch calls this when
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* psx_cycles.c — PSX guest CPU cycle clock. */
|
||||
|
||||
#include "psx_cycles.h"
|
||||
#include "cpu_state.h"
|
||||
#include "cdrom.h"
|
||||
#include "dma.h"
|
||||
#include "interrupts.h"
|
||||
@@ -66,3 +67,48 @@ void psx_advance_cycles(uint32_t cycles) {
|
||||
uint64_t psx_get_cycle_count(void) {
|
||||
return psx_cycle_count;
|
||||
}
|
||||
|
||||
/* ---- Mult/div completion-stall timing (faithful R3000A; Beetle muldiv_ts_done) ----
|
||||
*
|
||||
* MULT/MULTU/DIV/DIVU don't stall at the op; they set a completion DEADLINE.
|
||||
* A later MFLO/MFHI that reads HI/LO before the deadline STALLS (advances guest
|
||||
* cycles) until it. Instructions executed in between absorb the latency — so the
|
||||
* stall is (deadline - now), not a flat charge (this is why div+2filler+mflo costs
|
||||
* the same as div+mflo: the fillers ran during the latency window). REQUIRES
|
||||
* per-instruction cycle charging (PSX_CODEGEN_CYCLE_PER_INSN / the interp), so
|
||||
* `now` is the true cycle position at the op — block-up-front charging breaks it.
|
||||
*
|
||||
* Latencies transcribed from Beetle cpu.cpp: DIV/DIVU = 37 (fixed). MULT/MULTU =
|
||||
* MULT_Tab24 indexed by the leading-zero count of the (sign-folded, for signed)
|
||||
* first operand | 0x400 — i.e. 14 for small magnitudes (<12 significant bits),
|
||||
* 10 for medium, 7 for large. The | 0x400 caps the index at 21 (never l==0). */
|
||||
|
||||
static const uint8_t PSX_MULT_TAB24[24] = {
|
||||
/* i<12: 7+4+3=14 */ 14,14,14,14,14,14,14,14,14,14,14,14,
|
||||
/* 12<=i<21: 7+3=10 */ 10,10,10,10,10,10,10,10,10,
|
||||
/* i>=21: 7 */ 7,7,7
|
||||
};
|
||||
|
||||
static inline uint32_t psx_clz32(uint32_t v) {
|
||||
/* v is never 0 here (callers OR in 0x400). */
|
||||
return (uint32_t)__builtin_clz(v);
|
||||
}
|
||||
|
||||
uint32_t psx_mult_latency_s(uint32_t rs) { /* MULT (signed): sign-fold magnitude */
|
||||
return PSX_MULT_TAB24[psx_clz32((rs ^ (uint32_t)((int32_t)rs >> 31)) | 0x400u)];
|
||||
}
|
||||
uint32_t psx_mult_latency_u(uint32_t rs) { /* MULTU (unsigned) */
|
||||
return PSX_MULT_TAB24[psx_clz32(rs | 0x400u)];
|
||||
}
|
||||
|
||||
/* DIV/DIVU latency is the fixed constant 37 — emitted directly at the op site. */
|
||||
|
||||
void psx_muldiv_set(CPUState* cpu, uint32_t latency) {
|
||||
cpu->muldiv_ts_done = psx_cycle_count + (uint64_t)latency;
|
||||
}
|
||||
|
||||
void psx_muldiv_stall(CPUState* cpu) {
|
||||
if (cpu->muldiv_ts_done > psx_cycle_count) {
|
||||
psx_advance_cycles((uint32_t)(cpu->muldiv_ts_done - psx_cycle_count));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,3 +11,5 @@ generated/
|
||||
disc/license_data.dat
|
||||
disc/*.bin
|
||||
disc/*.cue
|
||||
build/
|
||||
psx_freeze_heartbeat.json
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Native test-runtime for the cycle micro-benchmark ROM (ruler #2).
|
||||
# Links the recompiled test EXE into the standard runtime so it boots the
|
||||
# synthetic disc (disc/cyctest.cue) and runs the isolation loops with the real
|
||||
# cost model. measure.py --port 4600 then reads per-iteration cycle costs to
|
||||
# compare against the Beetle oracle (4382).
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(CycleTestROM C CXX)
|
||||
|
||||
set(CMAKE_C_STANDARD 99)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
|
||||
# The psxrecomp worktree root is two levels up (tools/cycle_testrom -> worktree).
|
||||
set(PSXRECOMP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH
|
||||
"Path to psxrecomp")
|
||||
|
||||
include("${PSXRECOMP_ROOT}/runtime/runtime.cmake")
|
||||
|
||||
set(GAME_GENERATED_FULL
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/generated/cycle_testrom.exe_full.c")
|
||||
set(GAME_GENERATED_DISPATCH
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/generated/cycle_testrom.exe_dispatch.c")
|
||||
|
||||
psxrecomp_add_runtime_target(psx-cyctest
|
||||
GAME_GENERATED_FULL_C "${GAME_GENERATED_FULL}"
|
||||
GAME_GENERATED_DISPATCH_C "${GAME_GENERATED_DISPATCH}"
|
||||
DEBUG_PORT 4600
|
||||
WINDOW_TITLE "Cycle Test ROM"
|
||||
DEFAULT_BIOS_PATH "${PSXRECOMP_ROOT}/bios/SCPH1001.BIN"
|
||||
DEFAULT_GAME_CONFIG_PATH "game.toml"
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
BOOT = cdrom:\CYCTEST.EXE;1
|
||||
BOOT = cdrom:\CYCT_001.01;1
|
||||
TCB = 4
|
||||
EVENT = 10
|
||||
STACK = 801FFF00
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
<identifiers
|
||||
system = "PLAYSTATION"
|
||||
application = "PLAYSTATION"
|
||||
volume = "CYCTEST"
|
||||
volume_set = "CYCTEST"
|
||||
volume = "CYCT00101"
|
||||
volume_set = "CYCT00101"
|
||||
publisher = "PSXRECOMP"
|
||||
data_preparer= "MKPSXISO"
|
||||
copyright = "SYSTEM.CNF;1"
|
||||
/>
|
||||
<license file="license_data.dat"/>
|
||||
<directory_tree>
|
||||
<file name="system.cnf" type="data" source="SYSTEM.CNF"/>
|
||||
<file name="cyctest.exe" type="data" source="../cycle_testrom.exe"/>
|
||||
<file name="system.cnf" type="data" source="SYSTEM.CNF"/>
|
||||
<file name="cyct_001.01" type="data" source="../cycle_testrom.exe"/>
|
||||
</directory_tree>
|
||||
</track>
|
||||
</iso_project>
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
# native cost-model side; sideloaded raw into Beetle for the HW-oracle side.
|
||||
[game]
|
||||
name = "CycleTestROM"
|
||||
id = "CYCTEST-001"
|
||||
exe = "tools/cycle_testrom/cycle_testrom.exe"
|
||||
id = "CYCT-00101"
|
||||
exe = "cycle_testrom.exe"
|
||||
load_address = "0x80010000"
|
||||
entry_pc = "0x80010000"
|
||||
text_size = "0x00000800"
|
||||
|
||||
[recompiler]
|
||||
seeds = "tools/cycle_testrom/seeds.txt"
|
||||
out_dir = "tools/cycle_testrom/generated"
|
||||
seeds = "seeds.txt"
|
||||
out_dir = "generated"
|
||||
strict = true
|
||||
|
||||
[runtime]
|
||||
|
||||
Reference in New Issue
Block a user