Files
psxrecomp/recompiler/src/bios_slice_walker.cpp
T
Matthew Stanley 7c40d33db3 Phase 1 complete: recompiler proven correct on SCPH1001.BIN
Phase 1a (boot slice): 30 instructions at 0xBFC00000, compiles clean.
Phase 1b (instruction coverage): 54 opcodes implemented in strict_translator,
  44,629 walker-reachable instructions covered, 0 missing.
Phase 1c (function discovery): 76 functions from 2 seeds, 2,988 instructions,
  199 call edges, 27 indirect jumps. All validated through StrictTranslator.
Phase 1d (indirect control flow): 27 sites classified — 21 A0/B0/C0 dispatch
  stubs, 4 computed calls, 2 computed tail calls. Ghidra cross-referenced
  with 0 disagreements.
Phase 1e (relocation): ROM-to-RAM copy at FUN_bfc00420 proven with both
  Ghidra (static) and DuckStation (runtime). 69 address aliases, 68/69
  function entries match byte-for-byte in RAM. Normalization rule documented.

Includes PRINCIPLES.md and DEBUG.md adapted from recomp-template.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 21:26:04 -07:00

194 lines
7.8 KiB
C++

// bios_slice_walker.cpp — see header.
#include "bios_slice_walker.h"
#include <cstdint>
#include <stdexcept>
#include <string>
#include "fmt/format.h"
#include "mips_decoder.h"
namespace PSXRecompV4 {
namespace {
uint32_t read_u32_le(const std::vector<uint8_t>& rom, uint32_t offset) {
if (offset + 3 >= rom.size()) {
throw std::runtime_error(fmt::format(
"BiosSliceWalker: read_u32_le out of range (offset=0x{:X}, rom_size=0x{:X})",
offset, rom.size()));
}
return static_cast<uint32_t>(rom[offset + 0])
| (static_cast<uint32_t>(rom[offset + 1]) << 8)
| (static_cast<uint32_t>(rom[offset + 2]) << 16)
| (static_cast<uint32_t>(rom[offset + 3]) << 24);
}
const char* format_name(PSXRecomp::InstrFormat f) {
switch (f) {
case PSXRecomp::InstrFormat::R: return "R";
case PSXRecomp::InstrFormat::I: return "I";
case PSXRecomp::InstrFormat::J: return "J";
case PSXRecomp::InstrFormat::SPECIAL: return "SPECIAL";
case PSXRecomp::InstrFormat::REGIMM: return "REGIMM";
case PSXRecomp::InstrFormat::COP0: return "COP0";
case PSXRecomp::InstrFormat::COP2: return "COP2";
case PSXRecomp::InstrFormat::UNKNOWN: return "UNKNOWN";
}
return "UNKNOWN";
}
void record_unsupported(WalkResult& result, const PSXRecomp::DecodedInstruction& d, const std::string& reason) {
UnsupportedEntry e;
e.address = d.address;
e.raw = d.raw;
e.opcode_top6 = static_cast<uint8_t>(d.opcode);
e.decoded_format = format_name(d.format);
e.reason = reason;
result.unsupported.push_back(std::move(e));
result.ok = false;
}
} // namespace
WalkResult BiosSliceWalker::walk(const std::vector<uint8_t>& rom,
uint32_t base_addr,
uint32_t start_addr,
uint32_t max_bytes) {
WalkResult result;
result.ok = true;
result.start_addr = start_addr;
if (start_addr < base_addr || start_addr >= base_addr + rom.size()) {
throw std::runtime_error(fmt::format(
"BiosSliceWalker: start_addr 0x{:08X} outside ROM [0x{:08X}, 0x{:08X})",
start_addr, base_addr, base_addr + static_cast<uint32_t>(rom.size())));
}
const uint32_t walk_end = start_addr + max_bytes; // exclusive
uint32_t pc = start_addr;
while (pc < walk_end) {
// Bounds check both the current instruction and a possible delay slot fetch.
if (pc + 4 > base_addr + rom.size()) {
throw std::runtime_error(fmt::format(
"BiosSliceWalker: walk fell off end of ROM at pc=0x{:08X}", pc));
}
const uint32_t offset = pc - base_addr;
const uint32_t raw = read_u32_le(rom, offset);
const PSXRecomp::DecodedInstruction d = PSXRecomp::MipsDecoder::decode(raw, pc);
const TranslateResult tr = StrictTranslator::translate(d);
if (!tr.supported) {
record_unsupported(result, d, tr.fail_reason);
// Continue walking so the unsupported list is COMPLETE in one pass.
// Do NOT push to emitted (it would be incomplete/wrong).
pc += 4;
// Do not honor terminator semantics on a failed translation; the
// unsupported entry already proves the slice is broken.
continue;
}
if (tr.is_terminator) {
// 1) Translate the delay-slot instruction.
const uint32_t delay_pc = pc + 4;
if (delay_pc + 4 > base_addr + rom.size()) {
throw std::runtime_error(fmt::format(
"BiosSliceWalker: terminator at 0x{:08X} has no delay slot in ROM", pc));
}
const uint32_t delay_offset = delay_pc - base_addr;
const uint32_t delay_raw = read_u32_le(rom, delay_offset);
const PSXRecomp::DecodedInstruction delay_d =
PSXRecomp::MipsDecoder::decode(delay_raw, delay_pc);
const TranslateResult delay_tr = StrictTranslator::translate(delay_d);
if (!delay_tr.supported) {
record_unsupported(result, delay_d,
fmt::format("delay slot of terminator at 0x{:08X}: {}",
pc, delay_tr.fail_reason));
pc += 8;
continue;
}
if (delay_tr.is_terminator) {
// Branch in delay slot of branch is illegal on R3000A. Fail loud.
record_unsupported(result, delay_d,
fmt::format("delay slot of terminator at 0x{:08X} is itself a terminator (illegal on R3000A)", pc));
pc += 8;
continue;
}
// 1.5) If the terminator declared a pre-delay snapshot, emit it
// BEFORE the delay slot. This is used by conditional
// branches to capture rs/rt values into function-scope C
// locals so the branch condition (in tr.c_code, emitted
// after the delay slot) reads pre-delay state. The walker
// stays structurally the same — just one more EmittedInstr
// pushed when this string is non-empty. No control flow.
if (!tr.pre_delay_code.empty()) {
EmittedInstr ep;
ep.address = pc; // logically belongs to the branch
ep.raw = raw;
ep.c_code = tr.pre_delay_code;
ep.comment = fmt::format("[pre-delay snapshot for 0x{:08X}] {}", pc, tr.comment);
result.emitted.push_back(std::move(ep));
}
// 2) Emit delay slot (program order in C output: delay slot
// executes BEFORE control transfer in MIPS semantics — well, in
// parallel, but the C representation puts the side-effects of
// the delay slot before the PC update + return).
EmittedInstr ed;
ed.address = delay_pc;
ed.raw = delay_raw;
ed.c_code = delay_tr.c_code;
ed.comment = fmt::format("[delay slot of 0x{:08X}] {}", pc, delay_tr.comment);
result.emitted.push_back(std::move(ed));
// 3) Emit terminator.
EmittedInstr et;
et.address = pc;
et.raw = raw;
et.c_code = tr.c_code;
et.comment = tr.comment;
result.emitted.push_back(std::move(et));
result.end_addr = delay_pc; // delay slot is the last addr in the slice
result.instruction_count = static_cast<uint32_t>(result.emitted.size());
result.termination_reason = tr.terminator_kind;
result.terminator_target = tr.terminator_target;
if (std::string(tr.terminator_kind) == "rfe") {
result.termination_note =
"RFE; FIRST_MILESTONE.md listed ERET, substituted per session approval 2026-04-06 "
"(R3000A has no ERET)";
}
return result;
}
// Non-terminator: emit and advance.
EmittedInstr ei;
ei.address = pc;
ei.raw = raw;
ei.c_code = tr.c_code;
ei.comment = tr.comment;
result.emitted.push_back(std::move(ei));
pc += 4;
}
// Walk hit max_bytes without seeing a terminator.
if (result.emitted.empty()) {
// Could happen only if every instruction was unsupported. ok is already false.
result.end_addr = pc - 4;
result.instruction_count = 0;
} else {
result.end_addr = result.emitted.back().address;
result.instruction_count = static_cast<uint32_t>(result.emitted.size());
}
result.termination_reason = "size_limit";
return result;
}
} // namespace PSXRecompV4