Files
Matthew Stanley 5b3c7dd08d phase4: freeze_check diagnostic + accumulated overnight infra
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>
2026-04-28 21:29:19 -07:00

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;
}