mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
timing: faithful R3000A load-delay interlock (ReadFudge/LDAbsorb)
Implements the last unmodeled load-path cycle component — the R3000A load-delay pipeline interlock (Beetle ReadAbsorb/ReadFudge/LDAbsorb/ LDWhich) — as ONE shared per-instruction model across the dirty interp and BOTH static emitters, replacing the flat per-instruction +1 base and the flat +4 main-RAM read wait-state. Model (transcribed from beetle-psx cpu.cpp + libretro.cpp, verified Δ): - §1 base: a pending load's per-register give-back (read_absorb) is consumed INSTEAD of charging +1 (pipeline write-back overlap). - GPR_DEPRES: each opcode's src/dst regs end the give-back on use. - DO_LDS: commits the prior load's give-back + ReadFudge. - load ReadMemory: +2 fudge iff the predecessor committed no load; main-RAM region +3 + completion +2 (LWC2 +1) become the LDAbsorb give-back; scratchpad +0. New runtime/include/psx_cyc.h (inline §1/deps/DO_LDS + step) over new CPUState timing fields; psx_cyc_load_*/psx_cyc_lwc2_read in memory.c (region-aware, own the whole load interlock, value via uncharged psx_read_*); psx_cyc_dep_res_mask in psx_instr_cost.h (per-opcode GPR_DEP/RES, gen-time literal). cpu->read_* rewired to uncharged in main.cpp; charge_main_ram_read removed. Wired before each instruction body in code_generator.cpp (game) and full_function_emitter.cpp + strict_translator.cpp (BIOS); loads route through psx_cyc_load_*. Validated against the Beetle oracle: - ruler #2 load2: native +10 -> +11 == Beetle (all other components still exact: alu+1/load+5/div+38/mult+15/gte_rtps+15/gte_nclip+8). - ruler #1 [0x80001C5C->0x80001CA4]: 54 -> 56 == Beetle steady-state (residual 84/77 spikes = I-cache cold refill, the separate P2 axis). - Tomba 2 boots to the intro FMV (pixels on screen, no regression). Follow-up (separate commit): GTE-read (MFC2/CFC2) + MFC0 LDAbsorb give-back, and muldiv-stall give-back consumption — needed for mixed game/FMV faithfulness but do not affect the rulers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -213,6 +213,25 @@ on a fixed region -> next.
|
||||
|
||||
## 5. Status / Log (update every session)
|
||||
|
||||
- **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
|
||||
new CPUState fields `read_absorb[33]/read_absorb_which/read_fudge/ld_which_t/ld_absorb`;
|
||||
`psx_cyc_load_word/half/byte` + `psx_cyc_lwc2_read` in memory.c do the Beetle ReadMemory
|
||||
timing (clear give-back, +2 fudge iff predecessor committed no load, region RAM +3 +
|
||||
completion +2/+1 as the LDAbsorb give-back, scratchpad +0). The pure dep/res classifier
|
||||
`psx_cyc_dep_res_mask` (transcribed from Beetle per-opcode GPR_DEP/RES) lives in
|
||||
psx_instr_cost.h. Wired into the dirty interp + BOTH static emitters (code_generator game,
|
||||
full_function_emitter+strict_translator BIOS); loads now route value reads through the
|
||||
UNCHARGED psx_read_* (cpu->read_* rewired in main.cpp; the flat +4 charge_main_ram_read is
|
||||
gone). **Δ-validated against Beetle:** ruler #2 `load2` +10 → **+11** == Beetle, every other
|
||||
component still exact (alu+1/load+5/div+38/mult+15/gte_rtps+15/gte_nclip+8); ruler #1
|
||||
[c5c→ca4] 54 → **56** == Beetle steady-state (84/77 spikes = I-cache cold refill, P2). Tomba2
|
||||
boots to the intro FMV (screenshot pixels, no regression). Builds clean (tools/BIOS/game/
|
||||
runtime/cyctest). FOLLOW-UP (separate commit): GTE-read/MFC0 give-back + muldiv-stall
|
||||
give-back consumption (don't affect rulers; needed for mixed-code faithfulness). Supersedes
|
||||
the "MODEL NAILED, impl pending" entry below.
|
||||
|
||||
- **2026-06-27 (load ReadFudge/LDAbsorb — MODEL NAILED empirically, impl pending):**
|
||||
Derived the last load-path component (the residual on both rulers) by measuring
|
||||
Beetle's PER-INSTRUCTION cost via adjacent-PC region cyc_watch. Confirmed:
|
||||
|
||||
@@ -1,6 +1,28 @@
|
||||
# Load ReadFudge / LDAbsorb — empirically-derived model + implementation spec
|
||||
|
||||
Status: **MODEL NAILED (empirically, 2026-06-27), implementation PENDING.** This is
|
||||
Status: **IMPLEMENTED + VALIDATED (2026-06-27).** Shipped as the shared per-instruction
|
||||
R3000A load-delay interlock in `runtime/include/psx_cyc.h` (§1 base + GPR_DEPRES + DO_LDS,
|
||||
+ `psx_cyc_load_*`/`psx_cyc_lwc2_read` in `memory.c`), driven by `psx_cyc_dep_res_mask`
|
||||
(`runtime/include/psx_instr_cost.h`, transcribed from Beetle's per-opcode GPR_DEP/RES).
|
||||
Wired into the dirty interp (`dirty_ram_interp.c`) and BOTH static emitters
|
||||
(`code_generator.cpp` game path; `full_function_emitter.cpp`+`strict_translator.cpp` BIOS
|
||||
path). Loads route value reads through the UNCHARGED `psx_read_*` (cpu->read_* rewired in
|
||||
main.cpp); the data-access cost is charged once, inside the interlock. VALIDATED against the
|
||||
Beetle oracle: ruler #2 `load2` native +10 → **+11** == Beetle (all other components
|
||||
unchanged + matching); ruler #1 [0x80001C5C→0x80001CA4] 54 → **56** == Beetle steady-state
|
||||
(residual 84/77 spikes = I-cache cold refill, the separate P2 axis); Tomba 2 boots to the
|
||||
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 (not in this commit; 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). These don't affect the rulers (no CPU-load give-back in the
|
||||
div/mult/gte loops) but are needed for full faithfulness in mixed game/FMV code.
|
||||
|
||||
---
|
||||
ORIGINAL SPEC (model derivation) follows.
|
||||
|
||||
This is
|
||||
the last unmodeled piece of the load path — the residual on BOTH rulers
|
||||
(ruler #1 native 54 vs Beetle 56; ruler #2 `load2` native +10 vs Beetle +11). It
|
||||
is the R3000A load-delay pipeline interlock (Beetle `ReadFudge`/`ReadAbsorb[]`/
|
||||
|
||||
@@ -160,17 +160,16 @@ std::string CodeGenerator::translate_lw(uint32_t instr) {
|
||||
uint32_t rs = get_rs(instr);
|
||||
uint32_t rt = get_rt(instr);
|
||||
int16_t offset = get_imm16(instr);
|
||||
std::string addr = (offset == 0) ? reg_name(rs)
|
||||
: fmt::format("{} + {}", reg_name(rs), offset);
|
||||
uint32_t mask = 1u << rs; /* GPR_DEP rs (load: dest rt armed via LDWhich) */
|
||||
|
||||
if (config_.optimize_zero_reg && rt == 0) {
|
||||
return "/* nop: load to $zero */";
|
||||
}
|
||||
|
||||
if (offset == 0) {
|
||||
return fmt::format("{} = cpu->read_word({});", reg_name(rt), reg_name(rs));
|
||||
} else {
|
||||
return fmt::format("{} = cpu->read_word({} + {});",
|
||||
reg_name(rt), reg_name(rs), offset);
|
||||
/* load to $zero: no GPR write, but the data access + R3000A interlock still run */
|
||||
return fmt::format("(void)psx_cyc_load_word(cpu, {}, 0, 0x{:X}u);", addr, mask);
|
||||
}
|
||||
return fmt::format("{} = psx_cyc_load_word(cpu, {}, {}, 0x{:X}u);",
|
||||
reg_name(rt), addr, rt, mask);
|
||||
}
|
||||
|
||||
std::string CodeGenerator::translate_sw(uint32_t instr) {
|
||||
@@ -323,68 +322,60 @@ std::string CodeGenerator::translate_lb(uint32_t instr) {
|
||||
uint32_t rs = get_rs(instr);
|
||||
uint32_t rt = get_rt(instr);
|
||||
int16_t offset = get_imm16(instr);
|
||||
std::string addr = (offset == 0) ? reg_name(rs)
|
||||
: fmt::format("{} + {}", reg_name(rs), offset);
|
||||
uint32_t mask = 1u << rs;
|
||||
|
||||
if (config_.optimize_zero_reg && rt == 0) {
|
||||
return "/* nop: load to $zero */";
|
||||
}
|
||||
|
||||
if (offset == 0) {
|
||||
return fmt::format("{} = (int32_t)(int8_t)cpu->read_byte({});", reg_name(rt), reg_name(rs));
|
||||
} else {
|
||||
return fmt::format("{} = (int32_t)(int8_t)cpu->read_byte({} + {});",
|
||||
reg_name(rt), reg_name(rs), offset);
|
||||
return fmt::format("(void)psx_cyc_load_byte(cpu, {}, 0, 0x{:X}u);", addr, mask);
|
||||
}
|
||||
return fmt::format("{} = (int32_t)(int8_t)psx_cyc_load_byte(cpu, {}, {}, 0x{:X}u);",
|
||||
reg_name(rt), addr, rt, mask);
|
||||
}
|
||||
|
||||
std::string CodeGenerator::translate_lbu(uint32_t instr) {
|
||||
uint32_t rs = get_rs(instr);
|
||||
uint32_t rt = get_rt(instr);
|
||||
int16_t offset = get_imm16(instr);
|
||||
std::string addr = (offset == 0) ? reg_name(rs)
|
||||
: fmt::format("{} + {}", reg_name(rs), offset);
|
||||
uint32_t mask = 1u << rs;
|
||||
|
||||
if (config_.optimize_zero_reg && rt == 0) {
|
||||
return "/* nop: load to $zero */";
|
||||
}
|
||||
|
||||
if (offset == 0) {
|
||||
return fmt::format("{} = cpu->read_byte({});", reg_name(rt), reg_name(rs));
|
||||
} else {
|
||||
return fmt::format("{} = cpu->read_byte({} + {});",
|
||||
reg_name(rt), reg_name(rs), offset);
|
||||
return fmt::format("(void)psx_cyc_load_byte(cpu, {}, 0, 0x{:X}u);", addr, mask);
|
||||
}
|
||||
return fmt::format("{} = psx_cyc_load_byte(cpu, {}, {}, 0x{:X}u);",
|
||||
reg_name(rt), addr, rt, mask);
|
||||
}
|
||||
|
||||
std::string CodeGenerator::translate_lh(uint32_t instr) {
|
||||
uint32_t rs = get_rs(instr);
|
||||
uint32_t rt = get_rt(instr);
|
||||
int16_t offset = get_imm16(instr);
|
||||
std::string addr = (offset == 0) ? reg_name(rs)
|
||||
: fmt::format("{} + {}", reg_name(rs), offset);
|
||||
uint32_t mask = 1u << rs;
|
||||
|
||||
if (config_.optimize_zero_reg && rt == 0) {
|
||||
return "/* nop: load to $zero */";
|
||||
}
|
||||
|
||||
if (offset == 0) {
|
||||
return fmt::format("{} = (int32_t)(int16_t)cpu->read_half({});", reg_name(rt), reg_name(rs));
|
||||
} else {
|
||||
return fmt::format("{} = (int32_t)(int16_t)cpu->read_half({} + {});",
|
||||
reg_name(rt), reg_name(rs), offset);
|
||||
return fmt::format("(void)psx_cyc_load_half(cpu, {}, 0, 0x{:X}u);", addr, mask);
|
||||
}
|
||||
return fmt::format("{} = (int32_t)(int16_t)psx_cyc_load_half(cpu, {}, {}, 0x{:X}u);",
|
||||
reg_name(rt), addr, rt, mask);
|
||||
}
|
||||
|
||||
std::string CodeGenerator::translate_lhu(uint32_t instr) {
|
||||
uint32_t rs = get_rs(instr);
|
||||
uint32_t rt = get_rt(instr);
|
||||
int16_t offset = get_imm16(instr);
|
||||
std::string addr = (offset == 0) ? reg_name(rs)
|
||||
: fmt::format("{} + {}", reg_name(rs), offset);
|
||||
uint32_t mask = 1u << rs;
|
||||
|
||||
if (config_.optimize_zero_reg && rt == 0) {
|
||||
return "/* nop: load to $zero */";
|
||||
}
|
||||
|
||||
if (offset == 0) {
|
||||
return fmt::format("{} = cpu->read_half({});", reg_name(rt), reg_name(rs));
|
||||
} else {
|
||||
return fmt::format("{} = cpu->read_half({} + {});",
|
||||
reg_name(rt), reg_name(rs), offset);
|
||||
return fmt::format("(void)psx_cyc_load_half(cpu, {}, 0, 0x{:X}u);", addr, mask);
|
||||
}
|
||||
return fmt::format("{} = psx_cyc_load_half(cpu, {}, {}, 0x{:X}u);",
|
||||
reg_name(rt), addr, rt, mask);
|
||||
}
|
||||
|
||||
std::string CodeGenerator::translate_lwl(uint32_t instr) {
|
||||
@@ -396,16 +387,17 @@ std::string CodeGenerator::translate_lwl(uint32_t instr) {
|
||||
return "/* nop: load to $zero */";
|
||||
}
|
||||
|
||||
// LWL: Load Word Left - loads bytes into the most-significant portion of rt
|
||||
// addr = rs + offset; byte_pos = addr & 3
|
||||
// Result merges high bytes from aligned word with low bytes from current rt value
|
||||
// Generate a helper call: cpu->rt = psx_lwl(cpu, addr, cpu->rt)
|
||||
if (offset == 0) {
|
||||
return fmt::format("{} = psx_lwl(cpu, {}, {});", reg_name(rt), reg_name(rs), reg_name(rt));
|
||||
} else {
|
||||
return fmt::format("{} = psx_lwl(cpu, {} + {}, {});",
|
||||
reg_name(rt), reg_name(rs), (int32_t)offset, reg_name(rt));
|
||||
// LWL: Load Word Left - merges high bytes from the aligned word into rt.
|
||||
// psx_lwl runs the full R3000A load interlock on the aligned address (GPR_DEP rs,
|
||||
// arm LDWhich=rt) and returns the merged value.
|
||||
std::string addr = (offset == 0) ? reg_name(rs)
|
||||
: fmt::format("{} + {}", reg_name(rs), (int32_t)offset);
|
||||
uint32_t mask = 1u << rs;
|
||||
if (config_.optimize_zero_reg && rt == 0) {
|
||||
return fmt::format("(void)psx_lwl(cpu, {}, {}, 0, 0x{:X}u);", addr, reg_name(rt), mask);
|
||||
}
|
||||
return fmt::format("{} = psx_lwl(cpu, {}, {}, {}, 0x{:X}u);",
|
||||
reg_name(rt), addr, reg_name(rt), rt, mask);
|
||||
}
|
||||
|
||||
std::string CodeGenerator::translate_lwr(uint32_t instr) {
|
||||
@@ -417,16 +409,15 @@ std::string CodeGenerator::translate_lwr(uint32_t instr) {
|
||||
return "/* nop: load to $zero */";
|
||||
}
|
||||
|
||||
// LWR: Load Word Right - loads bytes into the least-significant portion of rt
|
||||
// addr = rs + offset; byte_pos = addr & 3
|
||||
// Result merges low bytes from aligned word with high bytes from current rt value
|
||||
// Generate a helper call: cpu->rt = psx_lwr(cpu, addr, cpu->rt)
|
||||
if (offset == 0) {
|
||||
return fmt::format("{} = psx_lwr(cpu, {}, {});", reg_name(rt), reg_name(rs), reg_name(rt));
|
||||
} else {
|
||||
return fmt::format("{} = psx_lwr(cpu, {} + {}, {});",
|
||||
reg_name(rt), reg_name(rs), (int32_t)offset, reg_name(rt));
|
||||
// LWR: Load Word Right - merges low bytes from the aligned word into rt.
|
||||
std::string addr = (offset == 0) ? reg_name(rs)
|
||||
: fmt::format("{} + {}", reg_name(rs), (int32_t)offset);
|
||||
uint32_t mask = 1u << rs;
|
||||
if (config_.optimize_zero_reg && rt == 0) {
|
||||
return fmt::format("(void)psx_lwr(cpu, {}, {}, 0, 0x{:X}u);", addr, reg_name(rt), mask);
|
||||
}
|
||||
return fmt::format("{} = psx_lwr(cpu, {}, {}, {}, 0x{:X}u);",
|
||||
reg_name(rt), addr, reg_name(rt), rt, mask);
|
||||
}
|
||||
|
||||
std::string CodeGenerator::translate_swl(uint32_t instr) {
|
||||
@@ -1153,19 +1144,20 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr)
|
||||
// Faithful GTE: COP2 reg write stalls to the command deadline.
|
||||
const char* gte_stall =
|
||||
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_gte_stall(cpu);\n#endif\n ";
|
||||
// LWC2 load timing: §1+DO_LDS via the block's psx_cyc_step(cpu,0)
|
||||
// (op 0x32 is non-load), the GTE deadline stall via psx_gte_stall,
|
||||
// then psx_cyc_lwc2_read does the ReadMemory timing (completion +1,
|
||||
// no LDWhich arm — the dest is a GTE register).
|
||||
std::string addr = (offset == 0)
|
||||
? reg_name(rs)
|
||||
: fmt::format("{} + {}", reg_name(rs), offset);
|
||||
bool special = (rt == 7 || (rt >= 8 && rt <= 11) || rt == 14 || rt == 15 || rt == 28 || rt == 30);
|
||||
if (special) {
|
||||
std::string addr = (offset == 0)
|
||||
? reg_name(rs)
|
||||
: fmt::format("{} + {}", reg_name(rs), offset);
|
||||
code = gte_stall + fmt::format("gte_write_data(cpu, {}, cpu->read_word({})); /* lwc2 gte[{}] */",
|
||||
code = gte_stall + fmt::format("gte_write_data(cpu, {}, psx_cyc_lwc2_read(cpu, {})); /* lwc2 gte[{}] */",
|
||||
rt, addr, rt);
|
||||
} else if (offset == 0) {
|
||||
code = gte_stall + fmt::format("cpu->gte_data[{}] = cpu->read_word({}); /* lwc2 gte[{}], ({}) */",
|
||||
rt, reg_name(rs), rt, reg_name(rs));
|
||||
} else {
|
||||
code = gte_stall + fmt::format("cpu->gte_data[{}] = cpu->read_word({} + {}); /* lwc2 gte[{}], {}({}) */",
|
||||
rt, reg_name(rs), offset, rt, offset, reg_name(rs));
|
||||
code = gte_stall + fmt::format("cpu->gte_data[{}] = psx_cyc_lwc2_read(cpu, {}); /* lwc2 gte[{}], ({}) */",
|
||||
rt, addr, rt, addr);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -1289,9 +1281,18 @@ std::string CodeGenerator::translate_basic_block(
|
||||
}
|
||||
|
||||
const bool cycle_per_insn = codegen_cycle_per_insn();
|
||||
auto emit_one_cycle = [&](const std::string& indent) {
|
||||
// Per-instruction R3000A load-delay interlock (cycle_per_insn mode): §1 base +
|
||||
// GPR_DEPRES + DO_LDS, emitted BEFORE the instruction body so §1 precedes any
|
||||
// muldiv/GTE deadline stall the body applies (Beetle order). CPU loads (op
|
||||
// 0x20-0x26) are SKIPPED here — psx_cyc_load_* runs their full interlock inside
|
||||
// the body (and arms LDWhich=rt). The dep/res mask is a gen-time literal. This
|
||||
// replaces the old flat per-instruction psx_advance_cycles(1u).
|
||||
auto emit_pre_timing = [&](uint32_t in, const std::string& indent) {
|
||||
uint32_t op = in >> 26;
|
||||
if (op >= 0x20u && op <= 0x26u) return; // CPU load: interlock inside psx_cyc_load_*
|
||||
ss << "#ifdef PSX_ENABLE_BLOCK_CYCLES\n";
|
||||
ss << indent << "psx_advance_cycles(1u);\n";
|
||||
ss << indent << fmt::format("psx_cyc_step(cpu, 0x{:X}u);\n",
|
||||
psx_cyc_dep_res_mask(in));
|
||||
ss << "#endif\n";
|
||||
};
|
||||
if (!cycle_per_insn && block_exec_cycles > 0) {
|
||||
@@ -1352,13 +1353,18 @@ std::string CodeGenerator::translate_basic_block(
|
||||
}
|
||||
|
||||
if (!is_cf) {
|
||||
if (cycle_per_insn) emit_pre_timing(instr, config_.indent);
|
||||
ss << translate_instruction(addr, instr) << "\n";
|
||||
if (cycle_per_insn) {
|
||||
emit_one_cycle(config_.indent);
|
||||
}
|
||||
} else {
|
||||
// Control flow is handled at block exit
|
||||
if (addr == exit_branch_addr) {
|
||||
// Per-instruction interlock ORDER (Beetle): the branch's §1+deps+DO_LDS
|
||||
// runs at the branch PC, THEN the delay slot's at PC+4. Emit the branch
|
||||
// step FIRST (it is pure timing — does not touch GPR values, so it is
|
||||
// safe before the branch-condition capture below).
|
||||
if (cycle_per_insn)
|
||||
emit_pre_timing(block.exit_instr.instruction, config_.indent);
|
||||
|
||||
// MIPS delay slot handling: emit delay slot instruction BEFORE branch/jump
|
||||
std::string delay_saved_cond; // non-empty if condition was pre-captured
|
||||
if (block.exit_instr.has_delay_slot) {
|
||||
@@ -1387,18 +1393,13 @@ std::string CodeGenerator::translate_basic_block(
|
||||
delay_saved_cond, cond);
|
||||
}
|
||||
ss << config_.indent << "/* delay slot (always executes) */\n";
|
||||
// Delay slot's own §1+deps+DO_LDS, before its body (skipped if a load).
|
||||
if (cycle_per_insn) emit_pre_timing(delay_instr, config_.indent);
|
||||
ss << translate_instruction(delay_slot_addr, delay_instr) << "\n";
|
||||
if (cycle_per_insn) {
|
||||
emit_one_cycle(config_.indent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cycle_per_insn) {
|
||||
emit_one_cycle(config_.indent);
|
||||
}
|
||||
|
||||
// Now emit the branch/jump
|
||||
if (block.exit_instr.type == ControlFlowType::Branch) {
|
||||
// Check if this is a branch-and-link (bgezal/bltzal)
|
||||
@@ -2392,8 +2393,8 @@ std::string CodeGenerator::generate_file(
|
||||
ss << " * addr is the effective (possibly unaligned) address.\n";
|
||||
ss << " * rt_value is the current value of the destination register.\n";
|
||||
ss << " */\n";
|
||||
ss << "static uint32_t psx_lwl(CPUState* cpu, uint32_t addr, uint32_t rt_value) {\n";
|
||||
ss << " uint32_t word = cpu->read_word(addr & ~3u);\n";
|
||||
ss << "static uint32_t psx_lwl(CPUState* cpu, uint32_t addr, uint32_t rt_value, uint32_t rt, uint32_t reg_mask) {\n";
|
||||
ss << " uint32_t word = psx_cyc_load_word(cpu, addr & ~3u, rt, reg_mask); /* full load interlock */\n";
|
||||
ss << " switch (addr & 3u) {\n";
|
||||
ss << " case 0: return (rt_value & 0x00FFFFFFu) | (word << 24);\n";
|
||||
ss << " case 1: return (rt_value & 0x0000FFFFu) | (word << 16);\n";
|
||||
@@ -2406,8 +2407,8 @@ std::string CodeGenerator::generate_file(
|
||||
ss << " * addr is the effective (possibly unaligned) address.\n";
|
||||
ss << " * rt_value is the current value of the destination register.\n";
|
||||
ss << " */\n";
|
||||
ss << "static uint32_t psx_lwr(CPUState* cpu, uint32_t addr, uint32_t rt_value) {\n";
|
||||
ss << " uint32_t word = cpu->read_word(addr & ~3u);\n";
|
||||
ss << "static uint32_t psx_lwr(CPUState* cpu, uint32_t addr, uint32_t rt_value, uint32_t rt, uint32_t reg_mask) {\n";
|
||||
ss << " uint32_t word = psx_cyc_load_word(cpu, addr & ~3u, rt, reg_mask); /* full load interlock */\n";
|
||||
ss << " switch (addr & 3u) {\n";
|
||||
ss << " case 0: return word;\n";
|
||||
ss << " case 1: return (rt_value & 0xFF000000u) | (word >> 8);\n";
|
||||
|
||||
@@ -375,6 +375,19 @@ bool FullFunctionEmitter::emit_function(
|
||||
|
||||
const bool per_insn_cycles = bios_cycle_per_insn();
|
||||
|
||||
// Per-instruction R3000A load-delay interlock (cycle_per_insn mode): §1 base +
|
||||
// GPR_DEPRES + DO_LDS for one instruction, emitted BEFORE its body so §1 precedes
|
||||
// any muldiv/GTE deadline stall (Beetle order). CPU loads (op 0x20-0x26) are
|
||||
// SKIPPED — psx_cyc_load_* runs their full interlock inside the body. The dep/res
|
||||
// mask is a gen-time literal. Replaces the old flat per-instruction +1.
|
||||
auto emit_insn_interlock = [&](uint32_t w) {
|
||||
if (!per_insn_cycles) return;
|
||||
uint32_t op = w >> 26;
|
||||
if (op >= 0x20u && op <= 0x26u) return; // CPU load: interlock inside psx_cyc_load_*
|
||||
out += fmt::format("#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_cyc_step(cpu, 0x{:X}u);\n#endif\n",
|
||||
psx_cyc_dep_res_mask(w));
|
||||
};
|
||||
|
||||
for (auto it = addr_to_raw.begin(); it != addr_to_raw.end(); ++it) {
|
||||
uint32_t addr = it->first;
|
||||
uint32_t raw = it->second;
|
||||
@@ -422,9 +435,7 @@ bool FullFunctionEmitter::emit_function(
|
||||
// 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";
|
||||
}
|
||||
emit_insn_interlock(raw);
|
||||
|
||||
// Decode and translate.
|
||||
PSXRecomp::DecodedInstruction d = PSXRecomp::MipsDecoder::decode(raw, addr);
|
||||
@@ -547,13 +558,11 @@ bool FullFunctionEmitter::emit_function(
|
||||
if (ds_tr.supported && !ds_tr.is_terminator) {
|
||||
out += fmt::format(" /* DELAY (orphaned) 0x{:08X}: {:08X} {} */\n",
|
||||
ds_addr, ds_raw, ds_tr.comment);
|
||||
// Orphaned delay slot is inlined here (not an addr_to_raw
|
||||
// entry), so charge its interlock directly, BEFORE its body
|
||||
// (block mode already counts it in the owning block_cycles).
|
||||
emit_insn_interlock(ds_raw);
|
||||
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.
|
||||
@@ -584,13 +593,11 @@ bool FullFunctionEmitter::emit_function(
|
||||
if (ds_tr.supported && !ds_tr.is_terminator) {
|
||||
out += fmt::format(" /* DELAY (orphaned) 0x{:08X}: {:08X} {} */\n",
|
||||
ds_addr, ds_raw, ds_tr.comment);
|
||||
// Orphaned delay slot is inlined here (not an addr_to_raw
|
||||
// entry), so charge its interlock directly, BEFORE its body
|
||||
// (block mode already counts it in the owning block_cycles).
|
||||
emit_insn_interlock(ds_raw);
|
||||
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);
|
||||
@@ -610,13 +617,11 @@ bool FullFunctionEmitter::emit_function(
|
||||
if (ds_tr.supported && !ds_tr.is_terminator) {
|
||||
out += fmt::format(" /* DELAY (orphaned) 0x{:08X}: {:08X} {} */\n",
|
||||
ds_addr, ds_raw, ds_tr.comment);
|
||||
// Orphaned delay slot is inlined here (not an addr_to_raw
|
||||
// entry), so charge its interlock directly, BEFORE its body
|
||||
// (block mode already counts it in the owning block_cycles).
|
||||
emit_insn_interlock(ds_raw);
|
||||
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);
|
||||
@@ -653,13 +658,11 @@ bool FullFunctionEmitter::emit_function(
|
||||
if (ds_tr.supported && !ds_tr.is_terminator) {
|
||||
out += fmt::format(" /* DELAY (orphaned) 0x{:08X}: {:08X} {} */\n",
|
||||
ds_addr, ds_raw, ds_tr.comment);
|
||||
// Orphaned delay slot is inlined here (not an addr_to_raw
|
||||
// entry), so charge its interlock directly, BEFORE its body
|
||||
// (block mode already counts it in the owning block_cycles).
|
||||
emit_insn_interlock(ds_raw);
|
||||
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;
|
||||
|
||||
@@ -847,15 +847,16 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
|
||||
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
||||
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
||||
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
||||
const uint32_t mask = 1u << rs; /* GPR_DEP rs; dest rt armed via LDWhich */
|
||||
r.supported = true;
|
||||
const std::string addr_expr = fmt::format(
|
||||
"(uint32_t)((int32_t)cpu->gpr[{}] + ({}))", static_cast<int>(rs), simm);
|
||||
if (rt == 0) {
|
||||
r.c_code = fmt::format("(void)cpu->read_byte({});", addr_expr);
|
||||
r.c_code = fmt::format("(void)psx_cyc_load_byte(cpu, {}, 0, 0x{:X}u);", addr_expr, mask);
|
||||
} else {
|
||||
r.c_code = fmt::format(
|
||||
"cpu->gpr[{}] = (uint32_t)(int32_t)(int8_t)cpu->read_byte({});",
|
||||
static_cast<int>(rt), addr_expr);
|
||||
"cpu->gpr[{}] = (uint32_t)(int32_t)(int8_t)psx_cyc_load_byte(cpu, {}, {}, 0x{:X}u);",
|
||||
static_cast<int>(rt), addr_expr, static_cast<int>(rt), mask);
|
||||
}
|
||||
r.comment = fmt::format("lb {}, {}({})", gpr_name(rt), simm, gpr_name(rs));
|
||||
return r;
|
||||
@@ -906,17 +907,18 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
|
||||
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
||||
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
||||
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
||||
const uint32_t mask = 1u << rs; /* GPR_DEP rs (LWL does not GPR_DEP rt) */
|
||||
r.supported = true;
|
||||
const std::string body = (rt == 0)
|
||||
? "(void)cpu->read_word(psx_aligned);"
|
||||
? fmt::format("(void)psx_cyc_load_word(cpu, psx_aligned, 0, 0x{:X}u);", mask)
|
||||
: fmt::format(
|
||||
"uint32_t psx_byte_offset = psx_addr & 3u; "
|
||||
"uint32_t psx_shift_left = (3u - psx_byte_offset) * 8u; "
|
||||
"uint32_t psx_keep_mask = (1u << psx_shift_left) - 1u; "
|
||||
"uint32_t psx_word = cpu->read_word(psx_aligned); "
|
||||
"uint32_t psx_word = psx_cyc_load_word(cpu, psx_aligned, {}, 0x{:X}u); "
|
||||
"uint32_t psx_old_rt = cpu->gpr[{}]; "
|
||||
"cpu->gpr[{}] = (psx_old_rt & psx_keep_mask) | (psx_word << psx_shift_left);",
|
||||
static_cast<int>(rt), static_cast<int>(rt));
|
||||
static_cast<int>(rt), mask, static_cast<int>(rt), static_cast<int>(rt));
|
||||
r.c_code = fmt::format(
|
||||
"{{ uint32_t psx_addr = (uint32_t)((int32_t)cpu->gpr[{}] + ({})); "
|
||||
"uint32_t psx_aligned = psx_addr & ~3u; "
|
||||
@@ -931,17 +933,18 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
|
||||
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
||||
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
||||
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
||||
const uint32_t mask = 1u << rs; /* GPR_DEP rs (LWR does not GPR_DEP rt) */
|
||||
r.supported = true;
|
||||
const std::string body = (rt == 0)
|
||||
? "(void)cpu->read_word(psx_aligned);"
|
||||
? fmt::format("(void)psx_cyc_load_word(cpu, psx_aligned, 0, 0x{:X}u);", mask)
|
||||
: fmt::format(
|
||||
"uint32_t psx_byte_offset = psx_addr & 3u; "
|
||||
"uint32_t psx_shift_right = psx_byte_offset * 8u; "
|
||||
"uint32_t psx_keep_mask = ~(0xFFFFFFFFu >> psx_shift_right); "
|
||||
"uint32_t psx_word = cpu->read_word(psx_aligned); "
|
||||
"uint32_t psx_word = psx_cyc_load_word(cpu, psx_aligned, {}, 0x{:X}u); "
|
||||
"uint32_t psx_old_rt = cpu->gpr[{}]; "
|
||||
"cpu->gpr[{}] = (psx_old_rt & psx_keep_mask) | (psx_word >> psx_shift_right);",
|
||||
static_cast<int>(rt), static_cast<int>(rt));
|
||||
static_cast<int>(rt), mask, static_cast<int>(rt), static_cast<int>(rt));
|
||||
r.c_code = fmt::format(
|
||||
"{{ uint32_t psx_addr = (uint32_t)((int32_t)cpu->gpr[{}] + ({})); "
|
||||
"uint32_t psx_aligned = psx_addr & ~3u; "
|
||||
@@ -956,11 +959,12 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
|
||||
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
||||
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
||||
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
||||
const uint32_t mask = 1u << rs;
|
||||
r.supported = true;
|
||||
const std::string body = (rt == 0)
|
||||
? "(void)cpu->read_half(psx_addr);"
|
||||
: fmt::format("cpu->gpr[{}] = (uint32_t)(int32_t)(int16_t)cpu->read_half(psx_addr);",
|
||||
static_cast<int>(rt));
|
||||
? fmt::format("(void)psx_cyc_load_half(cpu, psx_addr, 0, 0x{:X}u);", mask)
|
||||
: fmt::format("cpu->gpr[{}] = (uint32_t)(int32_t)(int16_t)psx_cyc_load_half(cpu, psx_addr, {}, 0x{:X}u);",
|
||||
static_cast<int>(rt), static_cast<int>(rt), mask);
|
||||
r.c_code = fmt::format(
|
||||
"{{ uint32_t psx_addr = (uint32_t)((int32_t)cpu->gpr[{}] + ({})); "
|
||||
"if (psx_addr & 1u) {{ psx_unaligned_access(cpu, psx_addr, 0x{:08X}u); return; }} "
|
||||
@@ -975,10 +979,12 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
|
||||
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
||||
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
||||
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
||||
const uint32_t mask = 1u << rs;
|
||||
r.supported = true;
|
||||
const std::string body = (rt == 0)
|
||||
? "(void)cpu->read_word(psx_addr);"
|
||||
: fmt::format("cpu->gpr[{}] = cpu->read_word(psx_addr);", static_cast<int>(rt));
|
||||
? fmt::format("(void)psx_cyc_load_word(cpu, psx_addr, 0, 0x{:X}u);", mask)
|
||||
: fmt::format("cpu->gpr[{}] = psx_cyc_load_word(cpu, psx_addr, {}, 0x{:X}u);",
|
||||
static_cast<int>(rt), static_cast<int>(rt), mask);
|
||||
r.c_code = fmt::format(
|
||||
"{{ uint32_t psx_addr = (uint32_t)((int32_t)cpu->gpr[{}] + ({})); "
|
||||
"if (psx_addr & 3u) {{ psx_unaligned_access(cpu, psx_addr, 0x{:08X}u); return; }} "
|
||||
@@ -993,15 +999,16 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
|
||||
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
||||
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
||||
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
||||
const uint32_t mask = 1u << rs;
|
||||
r.supported = true;
|
||||
const std::string addr_expr = fmt::format(
|
||||
"(uint32_t)((int32_t)cpu->gpr[{}] + ({}))", static_cast<int>(rs), simm);
|
||||
if (rt == 0) {
|
||||
r.c_code = fmt::format("(void)cpu->read_byte({});", addr_expr);
|
||||
r.c_code = fmt::format("(void)psx_cyc_load_byte(cpu, {}, 0, 0x{:X}u);", addr_expr, mask);
|
||||
} else {
|
||||
r.c_code = fmt::format(
|
||||
"cpu->gpr[{}] = (uint32_t)cpu->read_byte({});",
|
||||
static_cast<int>(rt), addr_expr);
|
||||
"cpu->gpr[{}] = (uint32_t)psx_cyc_load_byte(cpu, {}, {}, 0x{:X}u);",
|
||||
static_cast<int>(rt), addr_expr, static_cast<int>(rt), mask);
|
||||
}
|
||||
r.comment = fmt::format("lbu {}, {}({})", gpr_name(rt), simm, gpr_name(rs));
|
||||
return r;
|
||||
@@ -1012,10 +1019,12 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
|
||||
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
||||
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
||||
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
||||
const uint32_t mask = 1u << rs;
|
||||
r.supported = true;
|
||||
const std::string body = (rt == 0)
|
||||
? "(void)cpu->read_half(psx_addr);"
|
||||
: fmt::format("cpu->gpr[{}] = (uint32_t)cpu->read_half(psx_addr);", static_cast<int>(rt));
|
||||
? fmt::format("(void)psx_cyc_load_half(cpu, psx_addr, 0, 0x{:X}u);", mask)
|
||||
: fmt::format("cpu->gpr[{}] = (uint32_t)psx_cyc_load_half(cpu, psx_addr, {}, 0x{:X}u);",
|
||||
static_cast<int>(rt), static_cast<int>(rt), mask);
|
||||
r.c_code = fmt::format(
|
||||
"{{ uint32_t psx_addr = (uint32_t)((int32_t)cpu->gpr[{}] + ({})); "
|
||||
"if (psx_addr & 1u) {{ psx_unaligned_access(cpu, psx_addr, 0x{:08X}u); return; }} "
|
||||
@@ -1315,22 +1324,21 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
|
||||
// Faithful GTE: COP2 reg write stalls to the command deadline.
|
||||
const std::string gte_stall =
|
||||
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_gte_stall(cpu);\n#endif\n ";
|
||||
// §1+DO_LDS charged by full_function_emitter's psx_cyc_step (op 0x32 is
|
||||
// non-load); the GTE deadline stall by psx_gte_stall; psx_cyc_lwc2_read does
|
||||
// the ReadMemory timing (completion +1, no LDWhich arm — dest is a GTE reg).
|
||||
std::string addr = offset == 0
|
||||
? fmt::format("cpu->gpr[{}]", static_cast<int>(rs))
|
||||
: fmt::format("(uint32_t)((int32_t)cpu->gpr[{}] + ({}))", static_cast<int>(rs), static_cast<int>(offset));
|
||||
bool special = (rt == 7 || (rt >= 8 && rt <= 11) || rt == 14 || rt == 15 || rt == 28 || rt == 30);
|
||||
if (special) {
|
||||
std::string addr = offset == 0
|
||||
? fmt::format("cpu->gpr[{}]", static_cast<int>(rs))
|
||||
: fmt::format("(uint32_t)((int32_t)cpu->gpr[{}] + ({}))", static_cast<int>(rs), static_cast<int>(offset));
|
||||
r.c_code = gte_stall + fmt::format(
|
||||
"gte_write_data(cpu, {}, cpu->read_word({}));",
|
||||
"gte_write_data(cpu, {}, psx_cyc_lwc2_read(cpu, {}));",
|
||||
static_cast<int>(rt), addr);
|
||||
} else if (offset == 0) {
|
||||
r.c_code = gte_stall + fmt::format(
|
||||
"cpu->gte_data[{}] = cpu->read_word(cpu->gpr[{}]);",
|
||||
static_cast<int>(rt), static_cast<int>(rs));
|
||||
} else {
|
||||
r.c_code = gte_stall + fmt::format(
|
||||
"cpu->gte_data[{}] = cpu->read_word((uint32_t)((int32_t)cpu->gpr[{}] + ({})));",
|
||||
static_cast<int>(rt), static_cast<int>(rs), static_cast<int>(offset));
|
||||
"cpu->gte_data[{}] = psx_cyc_lwc2_read(cpu, {});",
|
||||
static_cast<int>(rt), addr);
|
||||
}
|
||||
r.comment = fmt::format("lwc2 gte[{}], {}({})",
|
||||
rt, static_cast<int>(offset), gpr_name(rs));
|
||||
|
||||
@@ -70,6 +70,20 @@ typedef struct CPUState {
|
||||
* stalls until it — faithful R3000A behavior (Beetle gte_ts_done).
|
||||
* Appended at END so prior field offsets are unchanged. */
|
||||
uint64_t gte_ts_done;
|
||||
|
||||
/* ---- R3000A load-delay pipeline interlock (Beetle ReadAbsorb/ReadFudge/
|
||||
* LDAbsorb/LDWhich) — TIMING ONLY; the load VALUE delay is handled by the
|
||||
* existing load-delay correctness path. See psx_cyc.h + accuracy/
|
||||
* load_readfudge_ldabsorb.md. Models: a load's data-access cost becomes a
|
||||
* per-register "give-back" (read_absorb) that following instructions consume
|
||||
* instead of charging their own +1 base; plus the +2 "fudge" charged on a
|
||||
* load whose predecessor committed no load. Appended at END so prior field
|
||||
* offsets are unchanged (precompiled overlay DLLs bake offsets). */
|
||||
uint8_t read_absorb[33]; /* ReadAbsorb[0..31] + [32]=0x20 DO_LDS dummy slot */
|
||||
uint8_t read_absorb_which; /* ReadAbsorbWhich: GPR the last committed load wrote */
|
||||
uint8_t read_fudge; /* ReadFudge: last committed load's dest reg, or 0x20 = none */
|
||||
uint8_t ld_which_t; /* LDWhich (timing): pending load dest reg, 0x20 = none */
|
||||
uint32_t ld_absorb; /* LDAbsorb: pending load's give-back (region+completion) */
|
||||
} CPUState;
|
||||
|
||||
/* Trap trampolines — defined in runtime/src/traps.c */
|
||||
@@ -225,4 +239,10 @@ static inline int psx_call_contract(CPUState* cpu, uint32_t site_ra,
|
||||
}
|
||||
#endif
|
||||
|
||||
/* R3000A load-delay interlock helpers (psx_cyc.h). Included LAST so CPUState is
|
||||
* fully defined first; psx_cyc.h's own #include "cpu_state.h" is a guarded no-op.
|
||||
* This makes the per-instruction timing API visible to EVERY generated file and
|
||||
* runtime TU that includes cpu_state.h. */
|
||||
#include "psx_cyc.h"
|
||||
|
||||
#endif /* PSXRECOMP_CPU_STATE_H */
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/* psx_cyc.h — R3000A load-delay pipeline interlock (ReadAbsorb / ReadFudge /
|
||||
* LDAbsorb / LDWhich). TIMING ONLY — the load VALUE delay is handled by the
|
||||
* existing load-delay correctness path; this models only the CPU cycle cost.
|
||||
*
|
||||
* FAITHFUL_TIMING_PLAN.md / accuracy/load_readfudge_ldabsorb.md. Transcribed
|
||||
* (facts, not code) from the in-tree Beetle oracle
|
||||
* psxrecomp/beetle-psx/mednafen/psx/cpu.cpp (RunReal §1 base 795-798, GPR_DEPRES
|
||||
* 702-705, DO_LDS 800, ReadMemory 364-451) and libretro.cpp (main-RAM read wait
|
||||
* = +3, libretro.cpp:884). Every backend — the dirty-RAM interpreter and BOTH
|
||||
* static emitters (code_generator.cpp, full_function_emitter.cpp/strict_translator.cpp)
|
||||
* — accounts guest CPU cycles THROUGH THESE HELPERS so they can never disagree.
|
||||
*
|
||||
* Model (per retired instruction, in program order):
|
||||
* §1 base : if(ReadAbsorb[ReadAbsorbWhich]) ReadAbsorb[ReadAbsorbWhich]--;
|
||||
* else timestamp++; -- a pending load's give-back is
|
||||
* consumed INSTEAD of the +1 base (pipeline write-back overlap).
|
||||
* deps : for each GPR this opcode reads/writes, ReadAbsorb[reg]=0 (ends a
|
||||
* give-back when the loaded value is used). ReadAbsorb[0] preserved.
|
||||
* DO_LDS : commit the PREVIOUS instruction's pending load TIMING:
|
||||
* ReadAbsorb[LDWhich]=LDAbsorb; ReadFudge=LDWhich; ...; LDWhich=0x20.
|
||||
* load only : ReadMemory — clear the current give-back slot, charge the +2 fudge
|
||||
* iff the predecessor committed no load (ReadFudge==0x20), charge the
|
||||
* region wait (main RAM +3) + completion (+2 CPU / +1 LWC2); the
|
||||
* (region+completion) becomes this load's LDAbsorb give-back, armed on
|
||||
* LDWhich=rt for the NEXT instruction's DO_LDS to commit.
|
||||
*/
|
||||
#ifndef PSX_CYC_H
|
||||
#define PSX_CYC_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "cpu_state.h" /* CPUState (guard-safe: cpu_state.h includes us last) */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern void psx_advance_cycles(uint32_t cycles);
|
||||
|
||||
/* §1 base (Beetle cpu.cpp:795-798). */
|
||||
static inline void psx_cyc_base(CPUState* cpu) {
|
||||
uint8_t w = cpu->read_absorb_which;
|
||||
if (cpu->read_absorb[w]) cpu->read_absorb[w]--;
|
||||
else psx_advance_cycles(1u);
|
||||
}
|
||||
|
||||
/* GPR_DEPRES (Beetle cpu.cpp:702-705): zero ReadAbsorb[n] for every source/dest
|
||||
* GPR of this instruction, preserving ReadAbsorb[0] (skipping bit 0 == Beetle's
|
||||
* save/restore of ReadAbsorb[0]). */
|
||||
static inline void psx_cyc_deps(CPUState* cpu, uint32_t reg_mask) {
|
||||
reg_mask &= 0xFFFFFFFEu; /* never touch ReadAbsorb[0] */
|
||||
while (reg_mask) {
|
||||
unsigned n = (unsigned)__builtin_ctz(reg_mask);
|
||||
cpu->read_absorb[n] = 0u;
|
||||
reg_mask &= reg_mask - 1u;
|
||||
}
|
||||
}
|
||||
|
||||
/* DO_LDS timing-commit (Beetle cpu.cpp:800). LDWhich==0x20 (no pending) writes the
|
||||
* dummy slot read_absorb[32] and sets read_fudge=0x20 (=> next load gets +2 fudge). */
|
||||
static inline void psx_cyc_lds(CPUState* cpu) {
|
||||
uint8_t lw = cpu->ld_which_t;
|
||||
cpu->read_absorb[lw] = (uint8_t)cpu->ld_absorb;
|
||||
cpu->read_fudge = lw;
|
||||
cpu->read_absorb_which = (uint8_t)(cpu->read_absorb_which | (lw & 0x1Fu));
|
||||
cpu->ld_which_t = 0x20u;
|
||||
}
|
||||
|
||||
/* Full per-instruction interlock for a NON-CPU-load instruction (ALU, shift,
|
||||
* branch, jump, store, COP control, LWC2/SWC2 pre-step, mult/div, mfhi/mflo, ...).
|
||||
* reg_mask from psx_cyc_dep_res_mask(). MUST be emitted BEFORE the instruction
|
||||
* body so §1 precedes any muldiv/GTE deadline stall in the body (Beetle order). */
|
||||
static inline void psx_cyc_step(CPUState* cpu, uint32_t reg_mask) {
|
||||
psx_cyc_base(cpu);
|
||||
psx_cyc_deps(cpu, reg_mask);
|
||||
psx_cyc_lds(cpu);
|
||||
}
|
||||
|
||||
/* The GPR dep+res bitmask used by psx_cyc_step lives in psx_instr_cost.h
|
||||
* (psx_cyc_dep_res_mask) — a standalone pure function shared by the emitters
|
||||
* (gen-time literal) and the interpreter (runtime), with no CPUState dependency. */
|
||||
|
||||
/* CPU data load value+timing (memory.c). Does the full Beetle per-instruction
|
||||
* sequence: §1 base + GPR_DEPRES(reg_mask) + (LDWhich==rt cancel) + DO_LDS +
|
||||
* ReadMemory timing (clear give-back, fudge, region+completion, arm LDAbsorb +
|
||||
* LDWhich=rt), and returns the raw value (caller applies the same width/sign as the
|
||||
* prior cpu->read_* call). For LWL/LWR pass the WORD-ALIGNED address. */
|
||||
extern uint32_t psx_cyc_load_word(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask);
|
||||
extern uint16_t psx_cyc_load_half(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask);
|
||||
extern uint8_t psx_cyc_load_byte(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask);
|
||||
|
||||
/* LWC2 (GTE load) ReadMemory timing only (completion +1, NO LDWhich arm — the dest
|
||||
* is a GTE register). Call AFTER psx_cyc_step(cpu,0) (§1+DO_LDS) and psx_gte_stall.
|
||||
* Returns the raw 32-bit value. */
|
||||
extern uint32_t psx_cyc_lwc2_read(CPUState* cpu, uint32_t addr);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PSX_CYC_H */
|
||||
@@ -36,14 +36,16 @@ extern "C" {
|
||||
* each component verified against Beetle via the cyc_watch DELTA gate
|
||||
* (FAITHFUL_TIMING_PLAN.md §3c). Both backends consume this one function.
|
||||
*
|
||||
* DATA-MEMORY ACCESS IS *NOT* CHARGED HERE — it lives in the memory path.
|
||||
* The runtime already owns the load/store data-access cost address-keyed in
|
||||
* runtime/src/memory.c (charge_main_ram_read → psx_guest_read_*: +6 cyc per
|
||||
* main-RAM read wait-state, scratchpad/MMIO/ROM excluded), exactly like Beetle
|
||||
* charges it in PS_CPU::ReadMemory (region wait + completion), NOT in the
|
||||
* opcode dispatch. The opcode word alone cannot know the target region
|
||||
* (RAM vs scratchpad vs MMIO), so a per-opcode load surcharge here is wrong by
|
||||
* construction and DOUBLE-COUNTS what memory.c already charges.
|
||||
* DATA-MEMORY ACCESS IS *NOT* CHARGED HERE — it lives in the load-delay interlock.
|
||||
* Guest CPU data loads route through psx_cyc_load_* (runtime/src/memory.c, declared
|
||||
* in psx_cyc.h), which charges the Beetle ReadMemory cost address-keyed (main RAM
|
||||
* region +3, completion +2, scratchpad +0) as the LDAbsorb give-back, plus the +2
|
||||
* ReadFudge — exactly like PS_CPU::ReadMemory, NOT in the opcode dispatch. The
|
||||
* opcode word alone cannot know the target region (RAM vs scratchpad vs MMIO), so a
|
||||
* per-opcode load surcharge HERE is wrong by construction and would double-count.
|
||||
* NOTE: in per-instruction mode (the default) the §1 base is charged by psx_cyc_base
|
||||
* (psx_cyc.h), not by this function — psx_instr_base_cycles is only the block-mode
|
||||
* (PSX_CODEGEN_CYCLE_PER_INSN=0) fallback sum and the dep/res mask seed below.
|
||||
*
|
||||
* HISTORY: Stage-2 #1a (commit 2ef47bd) returned 3 for loads / 2 for LWC2
|
||||
* ("1 execute + 2 data-access"). The BIOS-kernel cycle ruler (region
|
||||
@@ -59,6 +61,55 @@ static inline uint32_t psx_instr_base_cycles(uint32_t insn) {
|
||||
return 1u; /* pure CPU execute base; data access charged in memory.c */
|
||||
}
|
||||
|
||||
/* GPR dep+res bitmask (bit n => GPR n) for one MIPS-I instruction, transcribed
|
||||
* from the Beetle oracle's per-opcode GPR_DEP/GPR_RES (cpu.cpp). Drives the
|
||||
* load-delay interlock's GPR_DEPRES step (psx_cyc_step / psx_cyc_load_*, psx_cyc.h)
|
||||
* — the give-back ends when a following instruction reads/writes the loaded reg.
|
||||
* For CPU loads this returns the source (rs) ONLY: the dest rt is the delayed-load
|
||||
* target (armed via LDWhich), not a GPR_RES. Pure (insn-only) so the static emitters
|
||||
* fold it to a literal at gen-time and the interpreter calls it at runtime. */
|
||||
static inline uint32_t psx_cyc_dep_res_mask(uint32_t insn) {
|
||||
uint32_t op = insn >> 26;
|
||||
uint32_t rs = (insn >> 21) & 0x1Fu;
|
||||
uint32_t rt = (insn >> 16) & 0x1Fu;
|
||||
uint32_t rd = (insn >> 11) & 0x1Fu;
|
||||
uint32_t M_rs = 1u << rs, M_rt = 1u << rt, M_rd = 1u << rd;
|
||||
switch (op) {
|
||||
case 0x00: { /* SPECIAL */
|
||||
uint32_t fn = insn & 0x3Fu;
|
||||
switch (fn) {
|
||||
case 0x00: case 0x02: case 0x03: return M_rt | M_rd; /* SLL/SRL/SRA */
|
||||
case 0x04: case 0x06: case 0x07: return M_rs | M_rt | M_rd; /* SLLV/SRLV/SRAV */
|
||||
case 0x08: case 0x09: return M_rs | M_rd; /* JR/JALR */
|
||||
case 0x10: case 0x12: return M_rd; /* MFHI/MFLO */
|
||||
case 0x11: case 0x13: return M_rs; /* MTHI/MTLO */
|
||||
case 0x18: case 0x19: case 0x1A: case 0x1B: return M_rs | M_rt; /* MULT/MULTU/DIV/DIVU */
|
||||
case 0x20: case 0x21: case 0x22: case 0x23:
|
||||
case 0x24: case 0x25: case 0x26: case 0x27:
|
||||
case 0x2A: case 0x2B: return M_rs | M_rt | M_rd; /* ADD..NOR/SLT/SLTU */
|
||||
default: return 0u; /* SYSCALL/BREAK/... */
|
||||
}
|
||||
}
|
||||
case 0x01: { /* REGIMM/BCOND: DEP rs, RES link(31 for *AL variants) */
|
||||
uint32_t link = ((rt & 0x1Eu) == 0x10u) ? (1u << 31) : 0u;
|
||||
return M_rs | link;
|
||||
}
|
||||
case 0x02: return 0u; /* J */
|
||||
case 0x03: return 1u << 31; /* JAL (RES 31) */
|
||||
case 0x04: case 0x05: return M_rs | M_rt; /* BEQ/BNE */
|
||||
case 0x06: case 0x07: return M_rs; /* BLEZ/BGTZ */
|
||||
case 0x08: case 0x09: case 0x0A: case 0x0B:
|
||||
case 0x0C: case 0x0D: case 0x0E: return M_rs | M_rt; /* ADDI..XORI */
|
||||
case 0x0F: return M_rt; /* LUI (RES rt) */
|
||||
case 0x20: case 0x21: case 0x22: case 0x23:
|
||||
case 0x24: case 0x25: case 0x26: return M_rs; /* LB..LWR (load: rs only) */
|
||||
case 0x28: case 0x29: case 0x2A: case 0x2B:
|
||||
case 0x2E: return M_rs | M_rt; /* SB/SH/SWL/SW/SWR */
|
||||
/* COP0/1/2/3 (0x10-0x13), LWC* (0x30-0x33), SWC* (0x38-0x3B): no GPR_DEP/RES */
|
||||
default: return 0u;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -369,8 +369,9 @@ void dirty_ram_log_marker(uint32_t addr, uint32_t tag, int kind) {
|
||||
e->transferred = (uint8_t)(200 + kind);
|
||||
}
|
||||
|
||||
static inline uint32_t interp_lwl(CPUState *cpu, uint32_t addr, uint32_t rt_value) {
|
||||
uint32_t word = cpu->read_word(addr & ~3u);
|
||||
/* LWL/LWR are merge-only here; the timed aligned-word read is done by the caller via
|
||||
* psx_cyc_load_word (Beetle reads the aligned word; GPR_DEP rs only, arms LDWhich=rt). */
|
||||
static inline uint32_t lwl_merge(uint32_t addr, uint32_t word, uint32_t rt_value) {
|
||||
switch (addr & 3u) {
|
||||
case 0: return (rt_value & 0x00FFFFFFu) | (word << 24);
|
||||
case 1: return (rt_value & 0x0000FFFFu) | (word << 16);
|
||||
@@ -379,8 +380,7 @@ static inline uint32_t interp_lwl(CPUState *cpu, uint32_t addr, uint32_t rt_valu
|
||||
}
|
||||
}
|
||||
|
||||
static inline uint32_t interp_lwr(CPUState *cpu, uint32_t addr, uint32_t rt_value) {
|
||||
uint32_t word = cpu->read_word(addr & ~3u);
|
||||
static inline uint32_t lwr_merge(uint32_t addr, uint32_t word, uint32_t rt_value) {
|
||||
switch (addr & 3u) {
|
||||
case 0: return word;
|
||||
case 1: return (rt_value & 0xFF000000u) | (word >> 8);
|
||||
@@ -899,17 +899,10 @@ static void exec_delay_slot(CPUState *cpu, uint32_t pc) {
|
||||
uint32_t dummy_next = 0;
|
||||
(void)exec_one(cpu, pc, &dummy_next);
|
||||
g_dirty_ram_insns_run++;
|
||||
/* CYCLE MODEL: the delay-slot instruction is a real retired R3000A
|
||||
* instruction and costs its own cycle. The main interp loop charges 1 for
|
||||
* the branch/jump itself; charge 1 here for the delay slot so a branch+slot
|
||||
* pair costs 2 — matching the recompiler's block.instruction_count (which
|
||||
* counts both) and hardware. Without this the dirty-interp under-charges 1
|
||||
* cycle per taken branch vs compiled-overlay code, drifting the shared
|
||||
* guest-cycle timeline and forking timer-sensitive code (Tomba2 logo).
|
||||
* Charged through the single-source cost fn (psx_instr_cost.h). */
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
psx_advance_cycles(psx_instr_base_cycles(insn));
|
||||
#endif
|
||||
/* CYCLE MODEL: the delay-slot instruction is a real retired R3000A instruction
|
||||
* and is charged its own per-instruction interlock INSIDE exec_one (top-of-fn
|
||||
* §1+deps+DO_LDS, or psx_cyc_load_* for a load delay slot) — so a branch+slot
|
||||
* pair costs both, matching hardware. No separate charge here. */
|
||||
}
|
||||
|
||||
static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
|
||||
@@ -927,6 +920,17 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
|
||||
|
||||
*next_pc_out = pc + 4;
|
||||
|
||||
/* Per-instruction R3000A load-delay interlock (single-source: psx_cyc.h, shared
|
||||
* with both static emitters). §1 base + GPR_DEPRES + DO_LDS run HERE, before the
|
||||
* instruction body, so §1 precedes any muldiv/GTE deadline stall the body applies
|
||||
* (Beetle order). CPU loads (op 0x20-0x26) are skipped here — psx_cyc_load_* runs
|
||||
* their full interlock inside the body (and arms LDWhich=rt). This replaces the
|
||||
* old flat per-instruction psx_advance_cycles(psx_instr_base_cycles). */
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
if (!(opc >= 0x20u && opc <= 0x26u))
|
||||
psx_cyc_step(cpu, psx_cyc_dep_res_mask(insn));
|
||||
#endif
|
||||
|
||||
/* Update last-store PC tracker so SIO PC tracer attribution stays
|
||||
* coherent through interpreted stubs. */
|
||||
g_debug_last_store_pc = pc;
|
||||
@@ -1373,45 +1377,52 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
|
||||
}
|
||||
return abort_unsupported(pc, insn, "COP2 op");
|
||||
}
|
||||
/* CPU loads: the full per-instruction R3000A interlock (§1 + GPR_DEPRES(rs) +
|
||||
* DO_LDS + ReadMemory timing + arm LDWhich=rt) lives in psx_cyc_load_*; exec_one's
|
||||
* top-of-function step is skipped for op 0x20-0x26 so it is not double-charged.
|
||||
* #ifndef PSX_ENABLE_BLOCK_CYCLES these still read the value via the uncharged
|
||||
* accessor (psx_cyc_load_* falls back to a plain read when cycles are off). */
|
||||
case 0x20: { /* LB rt, simm(rs) */
|
||||
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
||||
cpu->gpr[rt] = (uint32_t)(int32_t)(int8_t)cpu->read_byte(addr);
|
||||
cpu->gpr[rt] = (uint32_t)(int32_t)(int8_t)psx_cyc_load_byte(cpu, addr, rt, 1u << rs);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
case 0x21: { /* LH */
|
||||
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
||||
cpu->gpr[rt] = (uint32_t)(int32_t)(int16_t)cpu->read_half(addr);
|
||||
cpu->gpr[rt] = (uint32_t)(int32_t)(int16_t)psx_cyc_load_half(cpu, addr, rt, 1u << rs);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
case 0x22: { /* LWL */
|
||||
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
||||
cpu->gpr[rt] = interp_lwl(cpu, addr, cpu->gpr[rt]);
|
||||
uint32_t word = psx_cyc_load_word(cpu, addr & ~3u, rt, 1u << rs);
|
||||
cpu->gpr[rt] = lwl_merge(addr, word, cpu->gpr[rt]);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
case 0x23: { /* LW */
|
||||
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
||||
cpu->gpr[rt] = cpu->read_word(addr);
|
||||
cpu->gpr[rt] = psx_cyc_load_word(cpu, addr, rt, 1u << rs);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
case 0x24: { /* LBU */
|
||||
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
||||
cpu->gpr[rt] = (uint32_t)cpu->read_byte(addr);
|
||||
cpu->gpr[rt] = (uint32_t)psx_cyc_load_byte(cpu, addr, rt, 1u << rs);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
case 0x25: { /* LHU */
|
||||
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
||||
cpu->gpr[rt] = (uint32_t)cpu->read_half(addr);
|
||||
cpu->gpr[rt] = (uint32_t)psx_cyc_load_half(cpu, addr, rt, 1u << rs);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
case 0x26: { /* LWR */
|
||||
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
||||
cpu->gpr[rt] = interp_lwr(cpu, addr, cpu->gpr[rt]);
|
||||
uint32_t word = psx_cyc_load_word(cpu, addr & ~3u, rt, 1u << rs);
|
||||
cpu->gpr[rt] = lwr_merge(addr, word, cpu->gpr[rt]);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
@@ -1448,12 +1459,12 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
|
||||
interp_swr(cpu, addr, cpu->gpr[rt]);
|
||||
return 0;
|
||||
}
|
||||
case 0x32: { /* LWC2 */
|
||||
case 0x32: { /* LWC2 — §1+DO_LDS charged by exec_one's top step (mask 0) */
|
||||
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
psx_gte_stall(cpu); /* COP2 reg write stalls to GTE completion */
|
||||
#endif
|
||||
gte_write_data(cpu, (uint8_t)rt, cpu->read_word(addr));
|
||||
gte_write_data(cpu, (uint8_t)rt, psx_cyc_lwc2_read(cpu, addr));
|
||||
return 0;
|
||||
}
|
||||
case 0x3A: { /* SWC2 */
|
||||
@@ -1624,10 +1635,7 @@ static void psx_run_precise(CPUState *cpu, uint32_t bcyc, int deadline_entry) {
|
||||
for (int i = 0; i < MAX_PRECISE_INSNS; i++) {
|
||||
uint32_t next_pc = 0;
|
||||
g_unsupported_seen = 0;
|
||||
int transferred = exec_one(cpu, pc, &next_pc);
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
psx_advance_cycles(psx_instr_base_cycles(fetch_word(pc & 0x1FFFFFFFu)));
|
||||
#endif
|
||||
int transferred = exec_one(cpu, pc, &next_pc); /* charges its own interlock */
|
||||
g_dirty_ram_insns_run++;
|
||||
if (g_unsupported_seen) {
|
||||
/* Valid compiled code always decodes; if not, hand the committed PC to
|
||||
@@ -1909,9 +1917,8 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
g_ra_load_snap_srcaddr = 0;
|
||||
}
|
||||
}
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
psx_advance_cycles(psx_instr_base_cycles(insn)); /* single-source cost */
|
||||
#endif
|
||||
/* Per-instruction cycle cost (R3000A load-delay interlock) is charged
|
||||
* INSIDE exec_one (top-of-fn §1+deps+DO_LDS, or psx_cyc_load_* for loads). */
|
||||
dirty_ram_log_instruction(cpu, pc, insn, before_s0, next_pc,
|
||||
transferred ? cpu->pc : next_pc,
|
||||
transferred);
|
||||
|
||||
+10
-4
@@ -2642,13 +2642,19 @@ int main(int argc, char** argv) {
|
||||
/* Initialize CPU state. */
|
||||
CPUState cpu;
|
||||
std::memset(&cpu, 0, sizeof(cpu));
|
||||
/* R3000A load-delay interlock init (Beetle: BACKED_LDWhich=0x20 = no pending
|
||||
* load; ReadFudge=0 so the first load gets no fudge). Rest is correctly 0. */
|
||||
cpu.ld_which_t = 0x20;
|
||||
|
||||
/* Wire memory function pointers. */
|
||||
cpu.read_word = psx_guest_read_word; /* +6cyc main-RAM read wait states */
|
||||
/* Wire memory function pointers. Guest data loads now route their CPU cycle
|
||||
* cost through the faithful load-delay interlock (psx_cyc_load_* in memory.c),
|
||||
* so these VALUE accessors are the UNCHARGED psx_read_* — the data-access
|
||||
* wait-state is charged once, inside the interlock, never here. */
|
||||
cpu.read_word = psx_read_word;
|
||||
cpu.write_word = psx_write_word;
|
||||
cpu.read_half = psx_guest_read_half;
|
||||
cpu.read_half = psx_read_half;
|
||||
cpu.write_half = psx_write_half;
|
||||
cpu.read_byte = psx_guest_read_byte;
|
||||
cpu.read_byte = psx_read_byte;
|
||||
cpu.write_byte = psx_write_byte;
|
||||
/* Wire the sljit JIT host-helper table (cpu-relative => position-independent
|
||||
* shards; prerequisite for the persisted sljit shard cache). Harmless when
|
||||
|
||||
+82
-37
@@ -853,49 +853,94 @@ uint8_t psx_read_byte(uint32_t addr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- CPU guest-side data reads: model PS1 main-RAM read latency ----
|
||||
* The R3000A has no usable D-cache (its data cache is repurposed as the 1 KB
|
||||
* scratchpad), so every CPU data read from main DRAM costs the base opcode cycle
|
||||
* plus a data-access wait-state. The base opcode cycle is charged per instruction
|
||||
* by the emitter/interp (psx_instr_base_cycles); here we add ONLY the data-access
|
||||
* wait-state, and only for main-RAM targets — scratchpad, MMIO, BIOS ROM and
|
||||
* open-bus are excluded (they have their own access timing). Keyed on the runtime
|
||||
* effective physical address (all of KUSEG/KSEG0/KSEG1 alias the same DRAM), NOT
|
||||
* the code region, so relocated kernel code is timed correctly.
|
||||
/* ---- CPU guest-side data loads: faithful R3000A load-delay pipeline interlock ----
|
||||
* The R3000A has no usable D-cache (it is repurposed as the 1 KB scratchpad), so a
|
||||
* CPU data load from main DRAM stalls the pipeline. Beetle (cpu.cpp ReadMemory,
|
||||
* 364-451) models this as: a +2 "fudge" iff the predecessor committed no load, the
|
||||
* region wait (main RAM = +3, libretro.cpp:884), and a completion cost (+2 CPU /
|
||||
* +1 LWC2); the (region+completion) becomes a per-register LDAbsorb "give-back" that
|
||||
* following instructions consume instead of their own +1 base (pipeline write-back
|
||||
* overlap). The §1 base + GPR_DEPRES + DO_LDS that bracket this run in psx_cyc.h.
|
||||
*
|
||||
* VALUE = 4, the Beetle ORACLE-accurate cost (the cycle test ROM load-isolation
|
||||
* loop measures a main-RAM load at +5 over baseline = base 1 + 4; Beetle ReadMemory
|
||||
* = fudge(0/2) + region wait(0 RAM) + completion(+2), netting 4 in the common fudged
|
||||
* case). The old 6 came from DuckStation (RAM_READ_TICKS), which our oracle
|
||||
* contradicts. Ruler #2 load == Beetle EXACTLY at 4.
|
||||
* These functions own the WHOLE per-instruction interlock for a CPU load (they call
|
||||
* psx_cyc_base/deps/lds), so the emitters/interp invoke them in place of the prior
|
||||
* cpu->read_* call and emit NO separate psx_cyc_step for the load. They return the
|
||||
* raw value via the UNCHARGED psx_read_* (cpu->read_* is now uncharged too — the
|
||||
* data-access cost is charged exactly once, here). Keyed on the runtime effective
|
||||
* physical address (KUSEG/KSEG0/KSEG1 alias the same DRAM).
|
||||
*
|
||||
* ⚠ KNOWN ISSUE this value CAUSES (under active root-cause on branch
|
||||
* wt/tomba2-load-accuracy): lowering 6→4 DETERMINISTICALLY WEDGES Tomba 2's
|
||||
* BIOS-shell boot — pc=0/exception at shell pc 0xBFC2CE64 (epc 0x80000048),
|
||||
* reproducible at identical total_checks across restarts. The faster-but-accurate
|
||||
* load cost makes the CPU outrun a shell loop's timing assumption, exposing a
|
||||
* downstream bug (likely incomplete device/loop timing). We deliberately KEEP the
|
||||
* accurate value and fix the real cause rather than mask it with a wrong cost —
|
||||
* faithful core (CLAUDE.md Rule -1). Until the wedge is fixed, Tomba 2 will not
|
||||
* reach the FMV on this branch; ruler #2 (its own boot disc) is unaffected.
|
||||
*
|
||||
* RESIDUAL beyond the constant (stateful, future): Beetle's per-load ReadFudge
|
||||
* variation (back-to-back loads differ ~1; ruler load2 was 10 vs 11) and LDAbsorb
|
||||
* give-back (a load whose result is read by the next instruction is absorbed).
|
||||
*
|
||||
* These wrappers are wired to cpu->read_* (see main.cpp), so the penalty applies
|
||||
* to BOTH statically-recompiled and dirty-RAM-interpreted guest loads, but NOT to
|
||||
* debug-server / device reads that call psx_read_* directly. */
|
||||
#define PSX_RAM_READ_WAIT_CYCLES 4u /* oracle-accurate; ⚠ causes Tomba 2 boot wedge @0xBFC2CE64 — root-cause in progress */
|
||||
* Region residual: only main-RAM (+3) and scratchpad (+0) region waits are modeled;
|
||||
* MMIO/BIOS-ROM device read waits (e.g. SPU +36) remain a separate unmodeled axis —
|
||||
* those loads still get the +2 completion (faithful) but region 0. */
|
||||
extern void psx_advance_cycles(uint32_t cycles);
|
||||
|
||||
static inline void charge_main_ram_read(uint32_t addr) {
|
||||
if ((addr & 0x1FFFFFFFu) < RAM_SIZE) psx_advance_cycles(PSX_RAM_READ_WAIT_CYCLES);
|
||||
/* Beetle ReadMemory data-access timing (cpu.cpp:369-448), after §1/deps/DO_LDS.
|
||||
* compl_cost = 2 (CPU load) / 1 (LWC2); arm_rt = GPR to arm as pending load, or
|
||||
* 0x20 = none (LWC2, dest is a GTE reg). */
|
||||
static inline void psx_cyc_readmem(CPUState* cpu, uint32_t phys,
|
||||
uint32_t compl_cost, uint32_t arm_rt) {
|
||||
/* ReadMemory start (369-370): clear the current give-back slot. */
|
||||
cpu->read_absorb[cpu->read_absorb_which] = 0u;
|
||||
cpu->read_absorb_which = 0u;
|
||||
/* Scratchpad (the D-cache): no wait, no give-back (414-422). */
|
||||
if (phys >= 0x1F800000u && phys <= 0x1F8003FFu) {
|
||||
cpu->ld_absorb = 0u;
|
||||
cpu->ld_which_t = (uint8_t)arm_rt;
|
||||
return;
|
||||
}
|
||||
/* fudge (424): +2 iff the predecessor committed no load (read_fudge==0x20). */
|
||||
psx_advance_cycles((uint32_t)((cpu->read_fudge >> 4) & 2u));
|
||||
uint32_t region = (phys < RAM_SIZE) ? 3u : 0u; /* main RAM +3; others: separate axis */
|
||||
uint32_t cost = region + compl_cost; /* LDAbsorb = region + completion */
|
||||
cpu->ld_absorb = cost;
|
||||
psx_advance_cycles(cost);
|
||||
cpu->ld_which_t = (uint8_t)arm_rt;
|
||||
}
|
||||
|
||||
uint32_t psx_guest_read_word(uint32_t addr) { charge_main_ram_read(addr); return psx_read_word(addr); }
|
||||
uint16_t psx_guest_read_half(uint32_t addr) { charge_main_ram_read(addr); return psx_read_half(addr); }
|
||||
uint8_t psx_guest_read_byte(uint32_t addr) { charge_main_ram_read(addr); return psx_read_byte(addr); }
|
||||
/* The interlock half of a load (§1+deps+(cancel)+DO_LDS+ReadMemory). Gated on
|
||||
* PSX_ENABLE_BLOCK_CYCLES so the Beetle-oracle build (cycles off) does a plain read. */
|
||||
static inline void psx_cyc_load_timing(CPUState* cpu, uint32_t addr,
|
||||
uint32_t rt, uint32_t reg_mask) {
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
psx_cyc_base(cpu);
|
||||
psx_cyc_deps(cpu, reg_mask);
|
||||
if (cpu->ld_which_t == rt) cpu->ld_which_t = 0u; /* cancel pending load to same dest */
|
||||
psx_cyc_lds(cpu);
|
||||
psx_cyc_readmem(cpu, addr & 0x1FFFFFFFu, 2u, rt);
|
||||
#else
|
||||
(void)cpu; (void)addr; (void)rt; (void)reg_mask;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint32_t psx_cyc_load_word(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask) {
|
||||
psx_cyc_load_timing(cpu, addr, rt, reg_mask);
|
||||
return psx_read_word(addr);
|
||||
}
|
||||
uint16_t psx_cyc_load_half(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask) {
|
||||
psx_cyc_load_timing(cpu, addr, rt, reg_mask);
|
||||
return psx_read_half(addr);
|
||||
}
|
||||
uint8_t psx_cyc_load_byte(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask) {
|
||||
psx_cyc_load_timing(cpu, addr, rt, reg_mask);
|
||||
return psx_read_byte(addr);
|
||||
}
|
||||
|
||||
/* LWC2 (GTE load): §1/DO_LDS done by psx_cyc_step(cpu,0); the GTE deadline stall by
|
||||
* psx_gte_stall — both emitted before this call. Completion +1, no LDWhich arm. */
|
||||
uint32_t psx_cyc_lwc2_read(CPUState* cpu, uint32_t addr) {
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
psx_cyc_readmem(cpu, addr & 0x1FFFFFFFu, 1u, 0x20u);
|
||||
#else
|
||||
(void)cpu;
|
||||
#endif
|
||||
return psx_read_word(addr);
|
||||
}
|
||||
|
||||
/* Deprecated uncharged passthroughs (the +4 flat wait-state model is gone; load
|
||||
* timing now lives in psx_cyc_load_*). Kept so any stray reference stays valid. */
|
||||
uint32_t psx_guest_read_word(uint32_t addr) { return psx_read_word(addr); }
|
||||
uint16_t psx_guest_read_half(uint32_t addr) { return psx_read_half(addr); }
|
||||
uint8_t psx_guest_read_byte(uint32_t addr) { return psx_read_byte(addr); }
|
||||
|
||||
void psx_write_byte(uint32_t addr, uint8_t val) {
|
||||
if (sr_ptr && (*sr_ptr & 0x10000u)) return;
|
||||
|
||||
Reference in New Issue
Block a user