// bios_address_model_test.cpp — proves the profile-driven BiosAddressModel is // function-level equivalent to the hardcoded SCPH1001 helpers it replaced. // // The byte-identity check on the regenerated SCPH1001_*.c is artifact-level // evidence; this test is the function-level version: every 4-byte address in // the RAM and ROM ranges is swept through the model built from // bios/SCPH1001.toml and compared against verbatim copies of the pre-refactor // implementations (full_function_emitter.cpp / function_discovery.cpp as of // the commit this refactor replaced). It also exercises the from_config // invariants. // // Usage: bios_address_model_test [path/to/SCPH1001.toml] // default: bios/SCPH1001.toml relative to the CWD (run from framework root) #include #include #include #include #include #include #include "../src/bios_address_model.h" #include "../src/config_loader.h" using PSXRecompV4::BiosAddressModel; using PSXRecompV4::BiosAddrCopy; using PSXRecompV4::BiosConfig; using PSXRecompV4::BiosInstallSlot; // --------------------------------------------------------------------------- // Reference implementations: verbatim pre-refactor SCPH1001 hardcodes. // --------------------------------------------------------------------------- static uint32_t ref_normalize(uint32_t addr) { uint32_t phys = addr & 0x1FFFFFFFu; if (phys >= 0x1FC10000u && phys <= 0x1FC17FFFu) { phys = phys - 0x1FC10000u + 0x00000500u; } if (phys >= 0x00030000u && phys <= 0x0005AFFFu) { phys = phys - 0x00030000u + 0x1FC18000u; } return phys; } static uint32_t ref_ram_alias_to_rom(uint32_t addr) { uint32_t phys = addr & 0x1FFFFFFFu; if (phys >= 0x00000500u && phys < 0x00008500u) { return 0xBFC10000u + (phys - 0x00000500u); } if (phys >= 0x00030000u && phys <= 0x0005AFFFu) { return 0xBFC18000u + (phys - 0x00030000u); } return addr; } static uint32_t ref_runtime_pc(uint32_t rom_pc) { uint32_t phys = rom_pc & 0x1FFFFFFFu; if (phys >= 0x1FC18000u && phys <= 0x1FC42FFFu) { return 0x80030000u + (phys - 0x1FC18000u); } if (phys >= 0x1FC10000u && phys <= 0x1FC17FFFu) { return phys - 0x1FC10000u + 0x00000500u; } return rom_pc; } static uint32_t ref_relocate_j_target(uint32_t rom_addr, uint32_t target) { uint32_t phys = rom_addr & 0x1FFFFFFFu; if (phys >= 0x1FC10000u && phys <= 0x1FC17FFFu) { uint32_t runtime_addr = phys - 0x1FC10000u + 0x00000500u; return (runtime_addr & 0xF0000000u) | (target & 0x0FFFFFFFu); } if (phys >= 0x1FC18000u && phys <= 0x1FC42FFFu) { uint32_t runtime_addr = 0x80030000u + (phys - 0x1FC18000u); return (runtime_addr & 0xF0000000u) | (target & 0x0FFFFFFFu); } return target; } static uint32_t ref_rom_to_ram_phys(uint32_t rom_addr) { uint32_t phys = rom_addr & 0x1FFFFFFFu; if (phys >= 0x1FC10000u && phys <= 0x1FC17FFFu) { return phys - 0x1FC10000u + 0x00000500u; } return phys; } // --------------------------------------------------------------------------- static int g_failures = 0; static void expect_eq(uint32_t got, uint32_t want, const char* what, uint32_t at) { if (got != want) { std::fprintf(stderr, "FAIL %s(0x%08X): got 0x%08X want 0x%08X\n", what, at, got, want); if (++g_failures > 20) { std::fprintf(stderr, "too many failures, aborting\n"); std::exit(1); } } } static void expect_throw(const char* what, void (*fn)(BiosConfig&), BiosConfig base) { try { fn(base); BiosAddressModel::from_config(base); std::fprintf(stderr, "FAIL invariant '%s': expected from_config to throw\n", what); g_failures++; } catch (const std::runtime_error&) { /* expected */ } } static BiosConfig minimal_cfg() { BiosConfig cfg{}; cfg.config_path = "test://synthetic"; cfg.load_address = 0xBFC00000u; cfg.text_size = 0x80000u; return cfg; } static BiosAddrCopy mk(const char* name, uint32_t rom_lo, uint32_t rom_hi, uint32_t ram_lo, uint32_t runtime_base, bool key_is_ram, bool bless) { BiosAddrCopy c; c.name = name; c.rom_lo = rom_lo; c.rom_hi = rom_hi; c.ram_lo = ram_lo; c.runtime_base = runtime_base; c.key_is_ram = key_is_ram; c.kernel_bless = bless; return c; } int main(int argc, char** argv) { const std::string profile = argc > 1 ? argv[1] : "bios/SCPH1001.toml"; BiosAddressModel model; try { const auto cfg = PSXRecompV4::load_bios_config(profile); model = BiosAddressModel::from_config(cfg); } catch (const std::exception& ex) { std::fprintf(stderr, "cannot load %s: %s\n(run from the framework root)\n", profile.c_str(), ex.what()); return 2; } // --- Sweep: RAM range + ROM range, every 4 bytes, all helpers --------- auto sweep = [&](uint32_t lo, uint32_t hi) { for (uint64_t a = lo; a < hi; a += 4) { const uint32_t addr = static_cast(a); expect_eq(model.normalize(addr), ref_normalize(addr), "normalize", addr); expect_eq(model.ram_alias_to_rom(addr), ref_ram_alias_to_rom(addr), "ram_alias_to_rom", addr); expect_eq(model.runtime_pc(addr), ref_runtime_pc(addr), "runtime_pc", addr); expect_eq(model.rom_to_ram_phys(addr), ref_rom_to_ram_phys(addr), "rom_to_ram_phys", addr); // J-target relocation: source sweep with two representative // 26-bit targets (one per runtime region shape). expect_eq(model.relocate_j_target(addr, 0x0FC12344u), ref_relocate_j_target(addr, 0x0FC12344u), "relocate_j(a)", addr); expect_eq(model.relocate_j_target(addr, 0x00030010u), ref_relocate_j_target(addr, 0x00030010u), "relocate_j(b)", addr); } }; sweep(0x00000000u, 0x00200000u); // RAM (2 MiB) sweep(0x1FC00000u, 0x1FC80000u); // BIOS ROM phys sweep(0xBFC00000u, 0xBFC80000u); // BIOS ROM KSEG1 (mask path) sweep(0x80000000u, 0x80200000u); // RAM KSEG0 (mask path) // Main RAM aliases cover the first 8 MiB of physical space. Dispatch // normalization must agree with memory access normalization at every // mirror, including the exact 0x80200000 boundary seen in indirect calls. expect_eq(model.normalize(0x00200000u), 0x00000000u, "normalize.ram_mirror", 0x00200000u); expect_eq(model.normalize(0x007FFFFCu), 0x001FFFFCu, "normalize.ram_mirror", 0x007FFFFCu); expect_eq(model.normalize(0x80200000u), 0x00000000u, "normalize.ram_mirror", 0x80200000u); expect_eq(model.normalize(0xA07FFFFCu), 0x001FFFFCu, "normalize.ram_mirror", 0xA07FFFFCu); // --- kbless / rom-keyed accessors match the SCPH1001 constants -------- if (!model.has_kbless()) { std::fprintf(stderr, "FAIL: no kbless window\n"); g_failures++; } expect_eq(model.kbless_ram_lo(), 0x500u, "kbless_ram_lo", 0); expect_eq(model.kbless_ram_hi(), 0x8500u, "kbless_ram_hi", 0); expect_eq(model.kbless_rom_off(), 0x10000u, "kbless_rom_off", 0); expect_eq(model.rom_keyed_ram_lo(), 0x30000u, "rom_keyed_ram_lo", 0); expect_eq(model.rom_keyed_ram_hi_incl(), 0x5AFFFu, "rom_keyed_ram_hi_incl", 0); // --- install slots are RANGES ---------------------------------------- // Only a range START is a slot: that is where the emitter plants the // compare-against-ROM hook. Interior words are covered by the range, not // by a second hook. if (!model.is_install_slot(0xCF0u)) { std::fprintf(stderr, "FAIL: 0xCF0 not an install slot\n"); g_failures++; } if (model.is_install_slot(0xCF4u)) { std::fprintf(stderr, "FAIL: 0xCF4 claims install slot\n"); g_failures++; } // bios/SCPH1001.toml declares the measured Psy-Q patch sites. The legacy // 0xCF0 form (ram_addr alone) must still mean a 4-word jalr stub. { const BiosInstallSlot* sio = model.install_slot_at(0xCF0u); if (!sio) { std::fprintf(stderr, "FAIL: no slot at 0xCF0\n"); g_failures++; } else { expect_eq(sio->len, 0x10u, "slot.0xCF0.len", 0xCF0u); expect_eq(sio->hi(), 0xD00u, "slot.0xCF0.hi", 0xCF0u); expect_eq(sio->resume_addr(), 0xD00u, "slot.0xCF0.resume", 0xCF0u); if (sio->resume != BiosInstallSlot::Resume::Jalr) { std::fprintf(stderr, "FAIL: 0xCF0 is not a jalr slot\n"); g_failures++; } } // The exception-handler prologue rewrite: a range, resumed by // fallthrough at its end (the patched words ARE the function). const BiosInstallSlot* exc = model.install_slot_at(0xC88u); if (!exc) { std::fprintf(stderr, "FAIL: no slot at 0xC88\n"); g_failures++; } else { expect_eq(exc->len, 0x30u, "slot.0xC88.len", 0xC88u); expect_eq(exc->resume_addr(), 0xCB8u, "slot.0xC88.resume", 0xC88u); } // The card-handler replacement is a `jr` out of the kernel: no resume. const BiosInstallSlot* card = model.install_slot_at(0x6444u); if (!card) { std::fprintf(stderr, "FAIL: no slot at 0x6444\n"); g_failures++; } else expect_eq(card->resume_addr(), 0u, "slot.0x6444.resume", 0x6444u); // Range CONTAINMENT, not just the start. The emitter drops any // dispatch key inside a range, so this predicate decides which keys // survive; getting it wrong wedges the boot (the 0x357C loop). if (!model.in_install_slot_range(0xCF0u) || !model.in_install_slot_range(0xCF4u) || !model.in_install_slot_range(0xCFCu)) { std::fprintf(stderr, "FAIL: 0xCF0 range does not contain its own words\n"); g_failures++; } if (model.in_install_slot_range(0xD00u)) { std::fprintf(stderr, "FAIL: range hi 0xD00 claimed as inside (must be exclusive)\n"); g_failures++; } if (model.in_install_slot_range(0xCECu)) { std::fprintf(stderr, "FAIL: 0xCEC below the range claimed as inside\n"); g_failures++; } // Emitted order is the runtime verifier's contract. uint32_t prev = 0; for (const BiosInstallSlot& sl : model.install_slots()) { if (sl.ram_addr < prev) { std::fprintf(stderr, "FAIL: install slots not sorted at 0x%08X\n", sl.ram_addr); g_failures++; } prev = sl.ram_addr; } } // --- from_config invariants refuse bad profiles ----------------------- expect_throw("overlapping ROM windows", [](BiosConfig& c) { c.address_copies = { mk("a", 0x1FC10000u, 0x1FC18000u, 0x500u, 0x500u, true, false), mk("b", 0x1FC14000u, 0x1FC43000u, 0x30000u, 0x80030000u, false, false), }; }, minimal_cfg()); expect_throw("two bless windows", [](BiosConfig& c) { c.address_copies = { mk("a", 0x1FC10000u, 0x1FC14000u, 0x500u, 0x500u, true, true), mk("b", 0x1FC14000u, 0x1FC18000u, 0x9000u, 0x9000u, true, true), }; }, minimal_cfg()); expect_throw("misaligned bounds", [](BiosConfig& c) { c.address_copies = { mk("a", 0x1FC10002u, 0x1FC18000u, 0x500u, 0x500u, true, false), }; }, minimal_cfg()); expect_throw("ROM window outside image", [](BiosConfig& c) { c.address_copies = { mk("a", 0x1FB00000u, 0x1FB10000u, 0x500u, 0x500u, true, false), }; }, minimal_cfg()); expect_throw("runtime_base phys mismatch", [](BiosConfig& c) { c.address_copies = { mk("a", 0x1FC10000u, 0x1FC18000u, 0x500u, 0x80000600u, true, false), }; }, minimal_cfg()); expect_throw("bless on rom-keyed copy", [](BiosConfig& c) { c.address_copies = { mk("a", 0x1FC18000u, 0x1FC43000u, 0x30000u, 0x80030000u, false, true), }; }, minimal_cfg()); expect_throw("overlapping RAM windows / double-fold", [](BiosConfig& c) { // a folds ROM->RAM into [0x500,0x8500); b (rom-keyed) folds RAM // [0x500,...) back out — sequential normalize() would double-fold // a's output. Refused (as overlapping RAM destinations). c.address_copies = { mk("a", 0x1FC10000u, 0x1FC18000u, 0x500u, 0x500u, true, false), mk("b", 0x1FC20000u, 0x1FC28000u, 0x500u, 0x500u, false, false), }; }, minimal_cfg()); // --- install-slot invariants refuse bad declarations ------------------ { auto with_slots = [](std::vector slots) { BiosConfig c = minimal_cfg(); c.address_copies = { mk("k", 0x1FC10000u, 0x1FC18000u, 0x500u, 0x500u, true, true), }; c.install_slots = std::move(slots); return c; }; auto slot = [](uint32_t at, uint32_t len) { BiosInstallSlot s; s.ram_addr = at; s.len = len; s.resume = BiosInstallSlot::Resume::Fallthrough; return s; }; expect_throw("misaligned slot start", [](BiosConfig&) {}, with_slots({slot(0xC8Au, 0x10u)})); expect_throw("zero-length slot", [](BiosConfig&) {}, with_slots({slot(0xC88u, 0u)})); expect_throw("misaligned slot length", [](BiosConfig&) {}, with_slots({slot(0xC88u, 0x12u)})); expect_throw("overlapping slots", [](BiosConfig&) {}, with_slots({slot(0xC88u, 0x30u), slot(0xCB4u, 0x10u)})); expect_throw("slot outside the bless window", [](BiosConfig&) {}, with_slots({slot(0x9000u, 0x10u)})); expect_throw("slot range crossing the bless window end", [](BiosConfig&) {}, with_slots({slot(0x84F0u, 0x30u)})); // Declared out of order: accepted, and sorted for the runtime. { BiosConfig c = with_slots({slot(0x4964u, 0x2Cu), slot(0xC88u, 0x30u)}); const auto m = BiosAddressModel::from_config(c); expect_eq(m.install_slots()[0].ram_addr, 0xC88u, "slots.sorted[0]", 0); expect_eq(m.install_slots()[1].ram_addr, 0x4964u, "slots.sorted[1]", 0); } } // --- empty model degenerates to the KSEG mask ------------------------- { BiosAddressModel empty; expect_eq(empty.normalize(0xBFC12345u), 0x1FC12345u, "empty.normalize", 0xBFC12345u); expect_eq(empty.runtime_pc(0xBFC12344u), 0xBFC12344u, "empty.runtime_pc", 0xBFC12344u); if (empty.has_kbless() || empty.has_rom_keyed_ram_window()) { std::fprintf(stderr, "FAIL: empty model claims windows\n"); g_failures++; } const std::string n = empty.emit_normalize_c(); if (n.find("0x1FC1") != std::string::npos) { std::fprintf(stderr, "FAIL: empty model emits copy folds\n"); g_failures++; } } if (g_failures == 0) { std::printf("bios_address_model_test: all checks passed\n"); return 0; } std::fprintf(stderr, "bios_address_model_test: %d failure(s)\n", g_failures); return 1; }