diff --git a/FAITHFUL_TIMING_PLAN.md b/FAITHFUL_TIMING_PLAN.md index 475c74a9..86d0541d 100644 --- a/FAITHFUL_TIMING_PLAN.md +++ b/FAITHFUL_TIMING_PLAN.md @@ -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: diff --git a/accuracy/load_readfudge_ldabsorb.md b/accuracy/load_readfudge_ldabsorb.md index 4d23b27d..8b80adc0 100644 --- a/accuracy/load_readfudge_ldabsorb.md +++ b/accuracy/load_readfudge_ldabsorb.md @@ -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[]`/ diff --git a/recompiler/src/code_generator.cpp b/recompiler/src/code_generator.cpp index 389182be..b76dcbf9 100644 --- a/recompiler/src/code_generator.cpp +++ b/recompiler/src/code_generator.cpp @@ -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"; diff --git a/recompiler/src/full_function_emitter.cpp b/recompiler/src/full_function_emitter.cpp index 0de527b7..d4e9d816 100644 --- a/recompiler/src/full_function_emitter.cpp +++ b/recompiler/src/full_function_emitter.cpp @@ -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; diff --git a/recompiler/src/strict_translator.cpp b/recompiler/src/strict_translator.cpp index 75268ed2..05732b1a 100644 --- a/recompiler/src/strict_translator.cpp +++ b/recompiler/src/strict_translator.cpp @@ -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(static_cast(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(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(rt), addr_expr); + "cpu->gpr[{}] = (uint32_t)(int32_t)(int8_t)psx_cyc_load_byte(cpu, {}, {}, 0x{:X}u);", + static_cast(rt), addr_expr, static_cast(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(static_cast(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(rt), static_cast(rt)); + static_cast(rt), mask, static_cast(rt), static_cast(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(static_cast(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(rt), static_cast(rt)); + static_cast(rt), mask, static_cast(rt), static_cast(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(static_cast(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(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(rt), static_cast(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(static_cast(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(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(rt), static_cast(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(static_cast(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(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(rt), addr_expr); + "cpu->gpr[{}] = (uint32_t)psx_cyc_load_byte(cpu, {}, {}, 0x{:X}u);", + static_cast(rt), addr_expr, static_cast(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(static_cast(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(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(rt), static_cast(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(rs)) + : fmt::format("(uint32_t)((int32_t)cpu->gpr[{}] + ({}))", static_cast(rs), static_cast(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(rs)) - : fmt::format("(uint32_t)((int32_t)cpu->gpr[{}] + ({}))", static_cast(rs), static_cast(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(rt), addr); - } else if (offset == 0) { - r.c_code = gte_stall + fmt::format( - "cpu->gte_data[{}] = cpu->read_word(cpu->gpr[{}]);", - static_cast(rt), static_cast(rs)); } else { r.c_code = gte_stall + fmt::format( - "cpu->gte_data[{}] = cpu->read_word((uint32_t)((int32_t)cpu->gpr[{}] + ({})));", - static_cast(rt), static_cast(rs), static_cast(offset)); + "cpu->gte_data[{}] = psx_cyc_lwc2_read(cpu, {});", + static_cast(rt), addr); } r.comment = fmt::format("lwc2 gte[{}], {}({})", rt, static_cast(offset), gpr_name(rs)); diff --git a/runtime/include/cpu_state.h b/runtime/include/cpu_state.h index 95ecf494..aa6fd01c 100644 --- a/runtime/include/cpu_state.h +++ b/runtime/include/cpu_state.h @@ -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 */ diff --git a/runtime/include/psx_cyc.h b/runtime/include/psx_cyc.h new file mode 100644 index 00000000..0166d551 --- /dev/null +++ b/runtime/include/psx_cyc.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 +#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 */ diff --git a/runtime/include/psx_instr_cost.h b/runtime/include/psx_instr_cost.h index 7e7a2713..690e7597 100644 --- a/runtime/include/psx_instr_cost.h +++ b/runtime/include/psx_instr_cost.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 diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 0f9e91b8..b073e6df 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -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); diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index a17c999c..2b27aece 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -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 diff --git a/runtime/src/memory.c b/runtime/src/memory.c index 8b97bdde..810281a4 100644 --- a/runtime/src/memory.c +++ b/runtime/src/memory.c @@ -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;