mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
5b3c7dd08d
Today's session work
- runtime/src/debug_server.c: freeze_check TCP cmd snapshots all liveness
counters (PC, exception entries, exception_reentry_blocks, dirty_ram
blocks/insns, fn_entry total + top-K histogram, SIO IRQ pending +
countdown, sio_stat/i_stat/i_mask, mc_max_state) for tight-loop
diagnosis when the runtime hangs
- runtime/src/debug_server.c: wtrace slot 12 covering [0x72F0..0x72F4]
(data-byte counter); measured peak=1 confirming setup_data_phase resets
the counter mid-cycle
- runtime/src/interrupts.c: exception_entries_total + exception_reentry_blocks
counters + psx_get_freeze_diag accessor
- runtime/src/sio.c: sio_get_freeze_diag accessor for IRQ pacing internals
- memory entries: phase4_card_gate_setup_writes_2026_04_28 (refutes the
prior \"missing [0x7568+slot] clearer\" framing — setup functions write
LSB=0 directly), phase4_72f0_one_byte_per_vblank_2026_04_28 (peak=1
measurement)
Overnight session work bundled in
- runtime/src/dirty_ram_interp.{c,h}: small MIPS interpreter for
install-at-runtime kernel RAM stubs (Rule 18; e.g., SIO data-byte
handler the BIOS writes to RAM 0xCF0)
- recompiler/src/full_function_emitter.{cpp,h}: 3057 jal/jalr continuation
return labels added to dispatch table (continuation fix); branch
fall-through fix for conditional-branch delay slots
- recompiler/src/strict_translator.cpp: matching translator updates
- generated/: regenerated SCPH1001 dispatch + full C output reflecting
the recompiler changes
- runtime/include/debug_server.h, src/memory.c: card_txn ring + SIO IRQ
ring + EvCB walk ring + per-PC dirty_ram counters + always-on wtrace
coverage for [0x755A], [0x7514], [0x7528], [0x7520], [0x74A4],
[0x75C0..C8], [0x74B8..C0]
- recompiler/tests/, runtime/tests/: SIO/card protocol unit tests
- tools/: 60+ Python diagnostic scripts (chain_trace analysis, SIO trace
diff, EvCB walker, wtrace dumpers, etc.)
- docs/: psx_bios_disasm reference + dynamic_handler_install rationale
- OVERNIGHT_2026_04_28.md, SUMMARY_20260412_2.md: session logs
Phase 4 status (still blocked, root cause better understood)
- BIOS card chain runs through R-handlers correctly (chain_trace shows
R1..R11 cycling 26x); shell IS retriggering _card_read
- Install handler at ROM 0xBFC15F1C increments [0x72F0] correctly per
SIO IRQ; gate at <0x80 / <0x7F is correct
- Bottleneck: SIO IRQ countdown is access-paced, fires ~1× per VBlank
vs ~128× needed; setup_data_phase (called from outer coord on VBlank)
zeroes [0x72F0] before counter reaches 128
- Both attempted fixes today caused freezes:
* VBlank deferral on [0x75C0]==1 — pending VBlank already in i_stat
delivered anyway; tighter dispatch gate also locked up
* Cycle-paced sio_tick at rates 50, 200 (and gated on [0x75C0]==1) —
runtime freezes once data phase activates, root cause unidentified
- delay=5 narrows the threshold (peak=2 vs peak=1, no freeze) but
doesn't get to 0x80
- Real fix likely requires deeper instrumentation of the freeze (call
stack / dispatch state at the moment of TCP starvation)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
211 lines
8.0 KiB
C++
211 lines
8.0 KiB
C++
/*
|
|
* L1 Decoder Conformance Test
|
|
*
|
|
* Compares MipsDecoder::decode() mnemonic output against Rabbitizer
|
|
* (trusted MIPS disassembler) for every instruction word in the BIOS
|
|
* ROM's known code regions.
|
|
*
|
|
* Usage:
|
|
* l1_decoder_test [--rom PATH] [-v|--verbose] [--log-dir DIR]
|
|
*
|
|
* Exit codes:
|
|
* 0 all instructions match
|
|
* 1 at least one mismatch
|
|
* 2 usage / I/O error
|
|
*/
|
|
|
|
#include <cstdio>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <map>
|
|
#include <algorithm>
|
|
#include <fstream>
|
|
|
|
#include "mips_decoder.h"
|
|
#include "rabbitizer.hpp"
|
|
#include "instructions/InstructionR3000GTE.hpp"
|
|
#include "common/RabbitizerConfig.h"
|
|
#include "fmt/format.h"
|
|
|
|
// ── BIOS code regions (from Ghidra analysis of SCPH1001.BIN) ────────────
|
|
|
|
struct CodeRegion {
|
|
uint32_t rom_start; // byte offset in ROM file
|
|
uint32_t rom_end; // exclusive
|
|
uint32_t vaddr_base; // virtual address of first byte
|
|
const char* name;
|
|
};
|
|
|
|
static const CodeRegion kRegions[] = {
|
|
{ 0x00000, 0x0DC60, 0xBFC00000, "Boot" },
|
|
{ 0x10000, 0x16760, 0xBFC10000, "Kernel" },
|
|
{ 0x18000, 0x42800, 0xBFC18000, "Shell" },
|
|
};
|
|
|
|
// ── Bucket for failure aggregation ──────────────────────────────────────
|
|
|
|
struct Bucket {
|
|
std::string key; // "oracle_mnem -> our_mnem"
|
|
unsigned count = 0;
|
|
uint32_t first_addr = 0;
|
|
uint32_t first_word = 0;
|
|
};
|
|
|
|
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
|
|
static std::string to_lower(const std::string& s) {
|
|
std::string out;
|
|
out.reserve(s.size());
|
|
for (char c : s)
|
|
out += (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c;
|
|
return out;
|
|
}
|
|
|
|
// Is this a COP2 GTE command? (opcode=0x12, CO bit=bit25 set)
|
|
static bool is_gte_command(uint32_t word) {
|
|
return ((word >> 26) & 0x3F) == 0x12 && ((word >> 25) & 1);
|
|
}
|
|
|
|
// Get oracle mnemonic via the right Rabbitizer class
|
|
static std::string oracle_mnemonic(uint32_t word, uint32_t addr) {
|
|
if (is_gte_command(word)) {
|
|
rabbitizer::InstructionR3000GTE insn(word, addr);
|
|
return to_lower(insn.getOpcodeName());
|
|
}
|
|
rabbitizer::InstructionCpu insn(word, addr);
|
|
return to_lower(insn.getOpcodeName());
|
|
}
|
|
|
|
// ── main ────────────────────────────────────────────────────────────────
|
|
|
|
int main(int argc, char** argv) {
|
|
const char* rom_path = "bios/SCPH1001.BIN";
|
|
bool verbose = false;
|
|
const char* log_dir = nullptr;
|
|
|
|
for (int i = 1; i < argc; i++) {
|
|
if ((std::strcmp(argv[i], "--rom") == 0) && i + 1 < argc)
|
|
rom_path = argv[++i];
|
|
else if (std::strcmp(argv[i], "-v") == 0 || std::strcmp(argv[i], "--verbose") == 0)
|
|
verbose = true;
|
|
else if ((std::strcmp(argv[i], "--log-dir") == 0) && i + 1 < argc)
|
|
log_dir = argv[++i];
|
|
else if (argv[i][0] != '-')
|
|
rom_path = argv[i];
|
|
}
|
|
|
|
// ── Configure Rabbitizer ────────────────────────────────────────────
|
|
// Disable pseudo-instructions so Rabbitizer reports the actual opcode
|
|
// (e.g. "or" not "move", "beq" not "beqz").
|
|
RabbitizerConfig_Cfg.pseudos.enablePseudos = false;
|
|
|
|
// ── Load ROM ────────────────────────────────────────────────────────
|
|
std::ifstream f(rom_path, std::ios::binary | std::ios::ate);
|
|
if (!f) {
|
|
fmt::print(stderr, "l1: cannot open {}\n", rom_path);
|
|
return 2;
|
|
}
|
|
auto rom_size = static_cast<size_t>(f.tellg());
|
|
f.seekg(0);
|
|
std::vector<uint8_t> rom(rom_size);
|
|
f.read(reinterpret_cast<char*>(rom.data()), static_cast<std::streamsize>(rom_size));
|
|
f.close();
|
|
|
|
if (rom_size != 524288) {
|
|
fmt::print(stderr, "l1: expected 524288 bytes, got {}\n", rom_size);
|
|
return 2;
|
|
}
|
|
|
|
// ── Walk code regions ───────────────────────────────────────────────
|
|
unsigned total = 0, pass = 0, fail = 0;
|
|
std::map<std::string, Bucket> buckets;
|
|
|
|
for (const auto& region : kRegions) {
|
|
unsigned region_total = 0, region_pass = 0;
|
|
|
|
for (uint32_t off = region.rom_start; off < region.rom_end; off += 4) {
|
|
uint32_t word = static_cast<uint32_t>(rom[off])
|
|
| (static_cast<uint32_t>(rom[off + 1]) << 8)
|
|
| (static_cast<uint32_t>(rom[off + 2]) << 16)
|
|
| (static_cast<uint32_t>(rom[off + 3]) << 24);
|
|
uint32_t addr = region.vaddr_base + (off - region.rom_start);
|
|
|
|
// Our decoder
|
|
auto ours = PSXRecomp::MipsDecoder::decode(word, addr);
|
|
std::string our_mnem = to_lower(ours.mnemonic ? ours.mnemonic : "???");
|
|
|
|
// Rabbitizer oracle
|
|
std::string orc_mnem = oracle_mnemonic(word, addr);
|
|
|
|
total++;
|
|
region_total++;
|
|
|
|
if (our_mnem == orc_mnem) {
|
|
pass++;
|
|
region_pass++;
|
|
} else {
|
|
fail++;
|
|
std::string key = orc_mnem + " -> " + our_mnem;
|
|
auto& b = buckets[key];
|
|
if (b.count == 0) {
|
|
b.key = key;
|
|
b.first_addr = addr;
|
|
b.first_word = word;
|
|
}
|
|
b.count++;
|
|
|
|
if (verbose) {
|
|
fmt::print(" FAIL 0x{:08X}: 0x{:08X} oracle={:<12s} ours={}\n",
|
|
addr, word, orc_mnem, our_mnem);
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt::print("{:<8s}: {}/{} ok\n", region.name, region_pass, region_total);
|
|
}
|
|
|
|
// ── Summary ─────────────────────────────────────────────────────────
|
|
fmt::print("\nL1 decoder: {}/{} ok", pass, total);
|
|
if (fail > 0)
|
|
fmt::print(" ({} failures)", fail);
|
|
fmt::print("\n");
|
|
|
|
// ── Bucket report ───────────────────────────────────────────────────
|
|
if (!buckets.empty()) {
|
|
std::vector<Bucket> sorted;
|
|
sorted.reserve(buckets.size());
|
|
for (auto& [k, b] : buckets)
|
|
sorted.push_back(b);
|
|
std::sort(sorted.begin(), sorted.end(),
|
|
[](const Bucket& a, const Bucket& b) { return a.count > b.count; });
|
|
|
|
fmt::print("\nFailure buckets (oracle -> ours):\n");
|
|
for (size_t i = 0; i < sorted.size() && i < 40; i++) {
|
|
fmt::print(" {:>6} {:<40s} first: 0x{:08X} [0x{:08X}]\n",
|
|
sorted[i].count, sorted[i].key,
|
|
sorted[i].first_addr, sorted[i].first_word);
|
|
}
|
|
|
|
// Optional log output
|
|
if (log_dir) {
|
|
std::string summary_path = std::string(log_dir) + "/l1_summary.txt";
|
|
std::string failures_path = std::string(log_dir) + "/l1_failures.log";
|
|
|
|
if (FILE* sf = std::fopen(summary_path.c_str(), "w")) {
|
|
std::fprintf(sf, "L1 decoder: %u/%u ok (%u failures)\n", pass, total, fail);
|
|
std::fclose(sf);
|
|
}
|
|
if (FILE* ff = std::fopen(failures_path.c_str(), "w")) {
|
|
for (const auto& b : sorted)
|
|
std::fprintf(ff, "%6u %-40s first: 0x%08X [0x%08X]\n",
|
|
b.count, b.key.c_str(), b.first_addr, b.first_word);
|
|
std::fclose(ff);
|
|
}
|
|
}
|
|
}
|
|
|
|
return fail > 0 ? 1 : 0;
|
|
}
|