Files
psxrecomp/recompiler/tests/reachable_discovery_test.cpp
T
Shokunin d67f0f9396 recompiler: compile a KUSEG-linked EXE for its link segment; exact dispatch
A PS-X EXE header may carry KUSEG addresses (Alien Resurrection SLUS-00633,
Kula World SCES-01000); the BIOS then runs the program at KUSEG PCs, so its
links, EPCs and I-cache tags are KUSEG. The parser folded such a header to
KSEG0, so the whole program compiled for a segment it never runs in, and
the dispatch lookup matched any segment by physical address
(docs/SEGMENT_AWARE_CODE.md §3.2, §5.3, §5.5).

- EXE parser: keeps the header's addresses. link_segment() is the load
  address's segment; the load address and entry must share it and lie in
  the 8 MiB RAM window of KUSEG, KSEG0 or KSEG1 (KSEG1 compiles as an
  uncached home segment). phys_load_address()/contains_phys() do byte
  addressing; the BSS overlap check and the text_size bound are physical.
- function_analysis: the three JAL scans take the target's top bits from
  the delay slot's PC instead of assuming KSEG0 (unchanged for KSEG0).
- Seeds are range-checked by physical address. A seed in the link segment
  is an entry (0x000100E4 was silently dropped before). A seed for the same
  bytes in another segment asks for a segment variant (§5.4): it is listed
  in a WARNING, never folded into the home body. Seeds outside the image are
  counted. The producer_range, cross_call_allow and hosted_interior
  directives have no variant form: a foreign spelling is refused with the
  link-segment one instead of a bare "invalid" error.
- Config code sites that are matched exactly (widescreen and cull sites,
  mod hooks, hot/data-shard funcs, the VSync-query function and its
  event-horizon return PCs, persisted options) and name the image's bytes
  in another segment are refused with the spelling that matches, since they
  would now match nothing. Kinds matched by physical address (byte patches,
  the full-word-guarded cull keep/angle/aspect-cone and signed-X-bound
  sites, dome call sites) reach the same bytes in any spelling and are not
  refused. Overlay compiles are exempt until PR E.
- tools/collect_game_misses.py wrote every dirty-RAM miss as a KSEG0 seed,
  which a KUSEG-linked image now reports instead of compiling. It writes
  them in the link segment (--segment, --game-toml, or a segmented text
  range; it refuses to guess from a physical one) and adds the
  segment_misses rows at their full PC.
- Dispatch source moves to game_dispatch_emitter.cpp. The lookup indexes
  the physical word, then requires row.addr == addr; a second row for one
  word is an error until variants share the table. psx_game_address_in_text
  stays physical, so a segment miss takes the clean-text-miss path (the
  previous commit records it).

For a KSEG0 title the game C is byte-identical; its dispatch file changes
only in psx_game_find_entry (the exact-PC compare) and its comment.

Tests:
- segment_aware_codegen closes link-segment, fetch-tag-segment,
  irq-resume-segment, resume-pc-segment, store-pc-segment,
  home-seed-accepted, alias-fetch-coherence and segment-miss; they stay as
  regression guards, and resume-pc-segment now also checks the dispatch
  rows' keys and resume PCs.
- kuseg_dispatch_lookup: KUSEG rows, names and baked PCs; aliases miss while
  text stays physical; variant seeds reported; a function reached only by
  jal and a SaveState-style continuation are discovered in a KUSEG image
  (kills the two discovery scans reverted to KSEG0; the third, the
  invalid-JAL data heuristic, reads the same bits either way); mixed-segment
  headers rejected; a KSEG1 image charged per instruction; foreign config
  sites refused; the RAM-mirror contract as before.
- kuseg_dispatch_lookup also compiles the lookup's binary-search form (a
  table spanning 2 MiB or more) and queries every segment's alias of each
  row; refuses a KSEG0 event-horizon site and KSEG0 seeds directives with
  their KUSEG spelling; accepts a KSEG0 signed_x_bound site and applies it.
- emitter_runtime_pc_test: the dispatch rows follow runtime_pc() and keep
  the compile identity in func_ names (kills B's two surviving mutants: 76
  and 12 failing checks); two rows on one physical word are an error.
- collect_game_misses (new, tools/tests): link-segment seeds from each
  source, segment misses at their full PC, no guessing, no duplicates,
  against a fake server using the runtime's own summary row format.
- reachable_discovery_test: the parser keeps KUSEG, accepts KSEG1, rejects
  mixed segments and non-RAM load addresses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:57:34 -07:00

884 lines
44 KiB
C++

#include "function_analysis.h"
#include "ps1_exe_parser.h"
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <set>
#include <string>
#include <vector>
namespace {
constexpr uint32_t kLoad = 0x80010000u;
int failures = 0;
#define CHECK(cond, message) do { \
if (!(cond)) { \
std::fprintf(stderr, "FAIL: %s\n", (message)); \
failures++; \
} \
} while (0)
void put32(std::vector<uint8_t>& data, size_t offset, uint32_t value) {
data[offset + 0] = static_cast<uint8_t>(value);
data[offset + 1] = static_cast<uint8_t>(value >> 8);
data[offset + 2] = static_cast<uint8_t>(value >> 16);
data[offset + 3] = static_cast<uint8_t>(value >> 24);
}
uint32_t jal(uint32_t target) {
return 0x0C000000u | ((target >> 2) & 0x03FFFFFFu);
}
std::vector<uint8_t> make_exe_buffer(uint32_t image_size,
uint32_t entry = kLoad) {
std::vector<uint8_t> data(2048u + image_size, 0);
std::memcpy(data.data(), "PS-X EXE", 8);
put32(data, 0x10, entry);
put32(data, 0x18, kLoad);
put32(data, 0x1C, image_size);
return data;
}
PSXRecomp::PS1Executable parse(std::vector<uint8_t> data) {
std::string error;
auto exe = PSXRecomp::PS1ExeParser::parse_buffer(data, error);
CHECK(exe.has_value(), error.c_str());
return exe.value();
}
std::set<uint32_t> starts(const PSXRecomp::FunctionAnalysisResult& result) {
std::set<uint32_t> out;
for (const auto& function : result.functions) out.insert(function.start_addr);
return out;
}
struct ApeSwitchFixture {
std::vector<uint8_t> image;
uint32_t entry = kLoad + 0x500u;
uint32_t jr_pc = kLoad + 0x528u;
uint32_t table = kLoad + 0xA00u;
std::vector<uint32_t> cases = {
kLoad + 0x540u, kLoad + 0x550u, kLoad + 0x560u};
};
ApeSwitchFixture make_ape_switch() {
ApeSwitchFixture f;
f.image = make_exe_buffer(0x1000u, f.entry);
const size_t text = 2048u;
put32(f.image, text + 0x500, 0x3C088001u); // lui t0,0x8001
put32(f.image, text + 0x504, 0x25100A00u); // addiu s0,t0,0x0a00
put32(f.image, text + 0x508, 0x00000000u);
put32(f.image, text + 0x50C, 0x2C620003u); // sltiu v0,v1,3
put32(f.image, text + 0x510, 0x1040001Bu); // beq v0,zero,+0x580
put32(f.image, text + 0x514, 0x00000000u);
put32(f.image, text + 0x518, 0x00031080u); // sll v0,v1,2
put32(f.image, text + 0x51C, 0x00501021u); // addu v0,v0,s0
put32(f.image, text + 0x520, 0x8C420000u); // lw v0,0(v0)
put32(f.image, text + 0x524, 0x00000000u);
put32(f.image, text + 0x528, 0x00400008u); // jr v0
put32(f.image, text + 0x52C, 0x00000000u);
for (size_t i = 0; i < f.cases.size(); i++) {
size_t off = static_cast<size_t>(f.cases[i] - kLoad);
put32(f.image, text + off, 0x24020001u + static_cast<uint32_t>(i));
put32(f.image, text + off + 4u, 0x03E00008u);
put32(f.image, text + off + 8u, 0x00000000u);
put32(f.image, text + 0xA00u + i * 4u, f.cases[i]);
}
put32(f.image, text + 0x580, 0x03E00008u);
put32(f.image, text + 0x584, 0x00000000u);
return f;
}
bool resolves_ape_switch(const ApeSwitchFixture& f,
uint32_t entry,
uint32_t producer_lo = kLoad,
uint32_t producer_hi = kLoad + 0x1000u) {
auto exe = parse(f.image);
PSXRecomp::ExactJumpTable table;
return PSXRecomp::resolve_exact_bounded_jump_table(
exe, entry, kLoad + 0x1000u, f.jr_pc, 2u, table,
nullptr, producer_lo, producer_hi);
}
ApeSwitchFixture make_scheduled_switch() {
auto f = make_ape_switch();
const size_t text = 2048;
put32(f.image, text + 0x500, 0);
put32(f.image, text + 0x504, 0);
put32(f.image, text + 0x508, 0x2C620003u); // sltiu v0,v1,3
put32(f.image, text + 0x50C, 0x1040001Cu); // beq v0,zero,+0x580
put32(f.image, text + 0x510, 0x3C028001u); // lui v0 (guard delay)
put32(f.image, text + 0x514, 0x24420A00u); // addiu v0,v0,0xa00
put32(f.image, text + 0x518, 0x00031880u); // sll v1,v1,2
put32(f.image, text + 0x51C, 0x00621821u); // addu v1,v1,v0
put32(f.image, text + 0x520, 0x8C620000u); // lw v0,0(v1)
return f;
}
void check_scheduled_switch() {
const size_t text = 2048;
auto f = make_scheduled_switch();
auto exe = parse(f.image);
PSXRecomp::ExactJumpTable table;
CHECK(PSXRecomp::resolve_exact_bounded_jump_table(
exe, f.entry, kLoad + 0x600u, f.jr_pc, 2u, table) &&
table.table_base == f.table && table.table_count == 3u,
"scheduled table constant resolves exactly");
std::set<uint32_t> targets;
for (auto target : table.targets) targets.insert(target.second);
CHECK(targets == std::set<uint32_t>(f.cases.begin(), f.cases.end()),
"scheduled table has exactly the expected case targets");
PSXRecomp::FunctionAnalyzer analyzer(exe);
auto result = analyzer.analyze_exact_entries({f.entry});
for (auto target : f.cases)
CHECK(result.exact_reachable_pcs.count(target),
"scheduled switch cases are reached from the function root");
auto renamed = f;
put32(renamed.image, text + 0x510, 0x3C088001u);
put32(renamed.image, text + 0x514, 0x25020A00u);
CHECK(resolves_ape_switch(renamed, renamed.entry),
"scheduled constant can rename registers");
auto no_load_delay = f;
no_load_delay.jr_pc = kLoad + 0x524u;
put32(no_load_delay.image, text + 0x524, 0x00400008u);
CHECK(!resolves_ape_switch(no_load_delay, f.entry),
"scheduled switch must respect the R3000 load delay");
auto signed_low = f;
signed_low.image.resize(text + 0x9000u);
put32(signed_low.image, 0x1C, 0x9000u);
put32(signed_low.image, text + 0x510, 0x3C028002u);
put32(signed_low.image, text + 0x514, 0x24428A00u);
for (size_t i = 0; i < f.cases.size(); ++i)
put32(signed_low.image, text + 0x8A00u + 4u * i, f.cases[i]);
CHECK(resolves_ape_switch(signed_low, f.entry, kLoad, kLoad + 0x9000u),
"scheduled constant sign-extends ADDIU low half");
CHECK(!resolves_ape_switch(f, f.entry, kLoad, kLoad + 0x900u),
"scheduled table must belong to the producer");
auto jump = [](uint32_t offset) {
return 0x08000000u | (((kLoad + offset) >> 2) & 0x03FFFFFFu);
};
const std::vector<std::vector<std::pair<size_t, uint32_t>>> mutations = {
{{0x508, 0x2C630003u}, {0x50C, 0x1060001Cu}}, // bound writes index
{{0x510, 0x3C038001u}, {0x514, 0x24620A00u}}, // LUI writes index
{{0x514, 0x24430A00u}}, // ADDIU writes index
{{0x514, 0x25020A00u}}, // wrong constant source
{{0x510, 0x3C228001u}}, // reserved LUI fields
{{0x50C, jump(0x580) | 0x04000000u}}, // JAL delay
{{0x50C, 0x5040001Cu}}, // BEQL
{{0x50C, 0x1040FFFFu}}, // self-loop with clobbered condition
{{0x50C, 0x10400000u}}, // reject into LUI
{{0x500, jump(0x510)}}, // earlier edge skips bound
{{0x590, jump(0x510)}}, // later edge skips bound
{{0x540, jump(0x510)}, {0x544, 0}}, // case skips bound
{{0x504, jump(0x580)}}, // bound in delay slot
{{0xA00, kLoad + 0x510u}}, // case target skips bound
{{0xA04, kLoad + 0x2000u}}, // out of host
};
for (const auto& changes : mutations) {
auto broken = f;
for (auto change : changes)
put32(broken.image, text + change.first, change.second);
CHECK(!resolves_ape_switch(broken, broken.entry),
"unsafe scheduled-switch mutation fails closed");
}
}
} // namespace
int main() {
check_scheduled_switch();
auto image = make_exe_buffer(0x2000);
const size_t text = 2048;
// Root: direct call to a framed target, direct call to a frameless leaf,
// unresolved jalr, direct call beyond the eventual bound, then return.
put32(image, text + 0x00, jal(kLoad + 0x100));
put32(image, text + 0x04, 0x00000000u);
put32(image, text + 0x08, jal(kLoad + 0x300));
put32(image, text + 0x0C, 0x00000000u);
put32(image, text + 0x10, 0x0320F809u); // jalr $ra,$t9 (unresolved)
put32(image, text + 0x14, 0x00000000u);
put32(image, text + 0x18, jal(kLoad + 0x1100));
put32(image, text + 0x1C, 0x00000000u);
put32(image, text + 0x20, 0x03E00008u);
put32(image, text + 0x24, 0x00000000u);
// Callable direct target.
put32(image, text + 0x100, 0x27BDFFF0u);
put32(image, text + 0x104, 0x03E00008u);
put32(image, text + 0x108, 0x27BD0010u);
// Valid-looking callback not referenced by a direct call.
put32(image, text + 0x200, 0x27BDFFF0u);
put32(image, text + 0x204, 0x03E00008u);
put32(image, text + 0x208, 0x27BD0010u);
// A direct JAL is sufficient callable-boundary evidence even when the
// target is a tiny frameless leaf.
put32(image, text + 0x300, 0x24020001u); // addiu $v0,$zero,1
put32(image, text + 0x304, 0x03E00008u);
put32(image, text + 0x308, 0x00000000u);
// Callable target outside the verified static bound.
put32(image, text + 0x1100, 0x27BDFFF0u);
put32(image, text + 0x1104, 0x03E00008u);
put32(image, text + 0x1108, 0x27BD0010u);
auto exe = parse(image);
std::string error;
CHECK(PSXRecomp::apply_static_analysis_bound(exe, 0x1000, error),
"valid page-aligned analysis bound is accepted");
CHECK(exe.code_size() == 0x1000 && exe.code_data.size() == 0x1000,
"accepted bound truncates header and analysis payload together");
PSXRecomp::FunctionAnalyzer analyzer(exe);
const auto reachable = starts(analyzer.analyze_exact_entries({kLoad}));
CHECK(reachable == std::set<uint32_t>({kLoad, kLoad + 0x100,
kLoad + 0x300}),
"reachable analysis follows framed and frameless direct JAL targets");
CHECK(!reachable.count(kLoad + 0x200),
"unseen indirect callback fails closed");
CHECK(!reachable.count(kLoad + 0x1100),
"direct target beyond verified bound fails closed");
// A JAL-shaped word can target an embedded table whose first pointer also
// happens to decode as a legal MIPS instruction. Three adjacent local
// pointers are stronger data evidence; keep that target interpreted.
auto pointer_image = make_exe_buffer(0x1000);
put32(pointer_image, text + 0x00, jal(kLoad + 0x400));
put32(pointer_image, text + 0x04, 0x00000000u);
put32(pointer_image, text + 0x08, 0x03E00008u);
put32(pointer_image, text + 0x0C, 0x00000000u);
put32(pointer_image, text + 0x400, kLoad + 0x500);
put32(pointer_image, text + 0x404, kLoad + 0x520);
put32(pointer_image, text + 0x408, kLoad + 0x540);
auto pointer_exe = parse(pointer_image);
PSXRecomp::FunctionAnalyzer pointer_analyzer(pointer_exe);
const auto pointer_starts = starts(
pointer_analyzer.analyze_exact_entries({kLoad}));
CHECK(!pointer_starts.count(kLoad + 0x400),
"reachable analysis rejects a dense local pointer table target");
// Discover call targets in stable rounds. An early root's JAL into the
// middle of a later explicit root must remain absorbed by that host rather
// than splitting/hard-capping it before the host is walked.
auto interior_call_image = make_exe_buffer(0x1000);
put32(interior_call_image, text + 0x00, jal(kLoad + 0x108));
put32(interior_call_image, text + 0x04, 0x00000000u);
put32(interior_call_image, text + 0x08, 0x03E00008u);
put32(interior_call_image, text + 0x0C, 0x00000000u);
put32(interior_call_image, text + 0x100, 0x24020001u);
put32(interior_call_image, text + 0x104, 0x24420001u);
put32(interior_call_image, text + 0x108, 0x24420001u);
put32(interior_call_image, text + 0x10C, 0x03E00008u);
put32(interior_call_image, text + 0x110, 0x00000000u);
auto interior_call_exe = parse(interior_call_image);
PSXRecomp::FunctionAnalyzer interior_call_analyzer(interior_call_exe);
const auto interior_call_starts = starts(
interior_call_analyzer.analyze_exact_entries(
{kLoad, kLoad + 0x100}));
CHECK(interior_call_starts ==
std::set<uint32_t>({kLoad, kLoad + 0x100}),
"direct call into an owned function stays an interior alias");
// Ownership must also settle among roots discovered in the same round.
// The initial root calls both A and an address B inside A. Neither address
// is owned by the initial partition, but walking A without B as a cap
// reaches B, so B is an alias rather than a sibling function.
auto same_round_image = make_exe_buffer(0x1000);
put32(same_round_image, text + 0x00, jal(kLoad + 0x100));
put32(same_round_image, text + 0x04, 0x00000000u);
put32(same_round_image, text + 0x08, jal(kLoad + 0x110));
put32(same_round_image, text + 0x0C, 0x00000000u);
put32(same_round_image, text + 0x10, 0x03E00008u);
put32(same_round_image, text + 0x14, 0x00000000u);
put32(same_round_image, text + 0x100, jal(kLoad + 0x110));
put32(same_round_image, text + 0x104, 0x00000000u);
put32(same_round_image, text + 0x108, 0x24420001u);
put32(same_round_image, text + 0x10C, 0x24420001u);
put32(same_round_image, text + 0x110, 0x03E00008u);
put32(same_round_image, text + 0x114, 0x00000000u);
auto same_round_exe = parse(same_round_image);
PSXRecomp::FunctionAnalyzer same_round_analyzer(same_round_exe);
const auto same_round_result =
same_round_analyzer.analyze_exact_entries({kLoad});
const auto same_round_starts = starts(same_round_result);
CHECK(same_round_starts ==
std::set<uint32_t>({kLoad, kLoad + 0x100}),
"same-round call target inside a discovered host stays absorbed");
bool same_round_hosted = false;
for (const auto& absorbed : same_round_result.absorbed_entries) {
same_round_hosted |= (absorbed.addr == kLoad + 0x110 &&
absorbed.host_start == kLoad + 0x100 &&
absorbed.addr < absorbed.host_end &&
absorbed.source_addr == kLoad + 0x100 &&
!absorbed.resolved_indirect);
}
CHECK(same_round_hosted,
"every analyzer-proven absorbed target names its final CFG host");
// Resolved constant-register calls carry the same reachable provenance as
// direct JALs and can therefore become final hosted aliases too.
auto indirect_alias_image = make_exe_buffer(0x1000);
put32(indirect_alias_image, text + 0x00, jal(kLoad + 0x100));
put32(indirect_alias_image, text + 0x04, 0x00000000u);
put32(indirect_alias_image, text + 0x08, 0x03E00008u);
put32(indirect_alias_image, text + 0x0C, 0x00000000u);
put32(indirect_alias_image, text + 0x100, 0x3C088001u);
put32(indirect_alias_image, text + 0x104, 0x25080110u);
put32(indirect_alias_image, text + 0x108, 0x0100F809u);
put32(indirect_alias_image, text + 0x10C, 0x00000000u);
put32(indirect_alias_image, text + 0x110, 0x03E00008u);
put32(indirect_alias_image, text + 0x114, 0x00000000u);
auto indirect_alias_exe = parse(indirect_alias_image);
PSXRecomp::FunctionAnalyzer indirect_alias_analyzer(indirect_alias_exe);
const auto indirect_alias_result =
indirect_alias_analyzer.analyze_exact_entries({kLoad});
bool indirect_alias_hosted = false;
for (const auto& absorbed : indirect_alias_result.absorbed_entries) {
indirect_alias_hosted |= (absorbed.addr == kLoad + 0x110 &&
absorbed.host_start == kLoad + 0x100 &&
absorbed.source_addr == kLoad + 0x108 &&
absorbed.resolved_indirect);
}
CHECK(indirect_alias_hosted,
"resolved indirect call target exports final alias provenance");
// The ordering can span rounds too: B is found first, then B discovers an
// earlier A whose reachable body contains B. Once A exists, ownership must
// be recomputed without retaining B as a stale hard cap.
auto later_round_image = make_exe_buffer(0x1000);
put32(later_round_image, text + 0x00, jal(kLoad + 0x120));
put32(later_round_image, text + 0x04, 0x00000000u);
put32(later_round_image, text + 0x08, 0x03E00008u);
put32(later_round_image, text + 0x0C, 0x00000000u);
put32(later_round_image, text + 0x100, 0x10000007u); // b 0x80010120
put32(later_round_image, text + 0x104, 0x00000000u);
put32(later_round_image, text + 0x120, jal(kLoad + 0x100));
put32(later_round_image, text + 0x124, 0x00000000u);
put32(later_round_image, text + 0x128, 0x03E00008u);
put32(later_round_image, text + 0x12C, 0x00000000u);
auto later_round_exe = parse(later_round_image);
PSXRecomp::FunctionAnalyzer later_round_analyzer(later_round_exe);
const auto later_round_starts = starts(
later_round_analyzer.analyze_exact_entries({kLoad}));
CHECK(later_round_starts ==
std::set<uint32_t>({kLoad, kLoad + 0x100}),
"later-discovered host retracts its stale interior hard cap");
// R discovers A and B; A initially owns B. Walking that expanded A then
// discovers X between them. X caps A before B and does not itself reach B,
// so final ownership must reconsider B and restore it as a real root.
auto invalidated_owner_image = make_exe_buffer(0x1000);
put32(invalidated_owner_image, text + 0x00, jal(kLoad + 0x100));
put32(invalidated_owner_image, text + 0x04, 0x00000000u);
put32(invalidated_owner_image, text + 0x08, jal(kLoad + 0x140));
put32(invalidated_owner_image, text + 0x0C, 0x00000000u);
put32(invalidated_owner_image, text + 0x10, 0x03E00008u);
put32(invalidated_owner_image, text + 0x14, 0x00000000u);
put32(invalidated_owner_image, text + 0x100,
0x08000000u | (((kLoad + 0x130) >> 2) & 0x03FFFFFFu));
put32(invalidated_owner_image, text + 0x104, 0x00000000u);
put32(invalidated_owner_image, text + 0x120, 0x03E00008u); // X
put32(invalidated_owner_image, text + 0x124, 0x00000000u);
put32(invalidated_owner_image, text + 0x130, 0x24420001u);
put32(invalidated_owner_image, text + 0x134, 0x24420001u);
put32(invalidated_owner_image, text + 0x138, 0x24420001u);
put32(invalidated_owner_image, text + 0x13C, 0x24420001u);
put32(invalidated_owner_image, text + 0x140, jal(kLoad + 0x120)); // B -> X
put32(invalidated_owner_image, text + 0x144, 0x00000000u);
put32(invalidated_owner_image, text + 0x148, 0x03E00008u);
put32(invalidated_owner_image, text + 0x14C, 0x00000000u);
auto invalidated_owner_exe = parse(invalidated_owner_image);
PSXRecomp::FunctionAnalyzer invalidated_owner_analyzer(
invalidated_owner_exe);
const auto invalidated_owner_result =
invalidated_owner_analyzer.analyze_exact_entries({kLoad});
const auto invalidated_owner_starts = starts(invalidated_owner_result);
CHECK(invalidated_owner_starts.count(kLoad + 0x100) &&
invalidated_owner_starts.count(kLoad + 0x120) &&
invalidated_owner_starts.count(kLoad + 0x140),
"R/A/X/B ownership invalidation restores B as a final root");
bool stale_b_alias = false;
for (const auto& absorbed : invalidated_owner_result.absorbed_entries)
stale_b_alias |= absorbed.addr == kLoad + 0x140;
CHECK(!stale_b_alias,
"R/A/X/B final ownership does not retain stale B alias");
// Producer boundaries are hard CFG ownership boundaries. A call into the
// next producer is rejected without approval and, when approved, becomes
// its own root rather than being absorbed by fallthrough from producer A.
auto producer_owner_image = make_exe_buffer(0x1000);
put32(producer_owner_image, text + 0x00, jal(kLoad + 0x100));
put32(producer_owner_image, text + 0x04, 0x00000000u);
put32(producer_owner_image, text + 0x08, jal(kLoad + 0x108));
put32(producer_owner_image, text + 0x0C, 0x00000000u);
put32(producer_owner_image, text + 0x10, 0x03E00008u);
put32(producer_owner_image, text + 0x14, 0x00000000u);
put32(producer_owner_image, text + 0x100, 0x24420001u);
put32(producer_owner_image, text + 0x104, 0x24420001u);
put32(producer_owner_image, text + 0x108, 0x03E00008u);
put32(producer_owner_image, text + 0x10C, 0x00000000u);
auto producer_owner_exe = parse(producer_owner_image);
const std::vector<std::pair<uint32_t, uint32_t>> ownership_ranges = {
{kLoad, kLoad + 0x108}, {kLoad + 0x108, kLoad + 0x1000}};
PSXRecomp::FunctionAnalyzer rejected_owner_analyzer(producer_owner_exe);
const auto rejected_owner = rejected_owner_analyzer.analyze_exact_entries(
{kLoad}, ownership_ranges, {});
CHECK(!starts(rejected_owner).count(kLoad + 0x108) &&
rejected_owner.absorbed_entries.empty(),
"rejected cross-producer call is neither root nor alias");
PSXRecomp::FunctionAnalyzer allowed_owner_analyzer(producer_owner_exe);
const auto allowed_owner = allowed_owner_analyzer.analyze_exact_entries(
{kLoad}, ownership_ranges, {kLoad + 0x108});
CHECK(starts(allowed_owner).count(kLoad + 0x108),
"approved cross-producer call remains a separate root");
bool cross_absorbed = false;
for (const auto& absorbed : allowed_owner.absorbed_entries)
cross_absorbed |= absorbed.addr == kLoad + 0x108;
CHECK(!cross_absorbed,
"approved cross-producer target is never absorbed across boundary");
// An uncovered composite gap has no producer identity. Two gap addresses
// both returning producer -1 must not become a synthetic shared producer.
auto producer_gap_image = make_exe_buffer(0x1000);
put32(producer_gap_image, text + 0x00, 0x03E00008u);
put32(producer_gap_image, text + 0x04, 0x00000000u);
put32(producer_gap_image, text + 0x60, 0x24020001u);
put32(producer_gap_image, text + 0x64, 0x03E00008u);
put32(producer_gap_image, text + 0x68, 0x00000000u);
const auto producer_gap_exe = parse(producer_gap_image);
PSXRecomp::FunctionAnalyzer producer_gap_analyzer(producer_gap_exe);
const std::vector<std::pair<uint32_t, uint32_t>> gap_ranges = {
{kLoad, kLoad + 0x40}, {kLoad + 0x80, kLoad + 0x100}};
const auto producer_gap_result = producer_gap_analyzer.analyze_exact_entries(
{kLoad, kLoad + 0x60}, gap_ranges, {});
CHECK(starts(producer_gap_result) == std::set<uint32_t>({kLoad}) &&
!producer_gap_result.exact_reachable_pcs.count(kLoad + 0x60),
"explicit roots in uncovered producer gaps fail closed");
// R reaches S, S calls B, and B calls earlier X. X then caps R before S,
// invalidating B's only source edge. Recomputing evidence oscillates, so the
// analyzer must fail closed to explicit R instead of retaining B forever.
auto stale_source_image = make_exe_buffer(0x1000);
put32(stale_source_image, text + 0x00,
0x08000000u | (((kLoad + 0x100) >> 2) & 0x03FFFFFFu));
put32(stale_source_image, text + 0x04, 0x00000000u);
put32(stale_source_image, text + 0x80, 0x03E00008u); // X
put32(stale_source_image, text + 0x84, 0x00000000u);
put32(stale_source_image, text + 0x100, jal(kLoad + 0x140)); // S -> B
put32(stale_source_image, text + 0x104, 0x00000000u);
put32(stale_source_image, text + 0x108, 0x03E00008u);
put32(stale_source_image, text + 0x10C, 0x00000000u);
put32(stale_source_image, text + 0x140, jal(kLoad + 0x80)); // B -> X
put32(stale_source_image, text + 0x144, 0x00000000u);
put32(stale_source_image, text + 0x148, 0x03E00008u);
put32(stale_source_image, text + 0x14C, 0x00000000u);
auto stale_source_exe = parse(stale_source_image);
PSXRecomp::FunctionAnalyzer stale_source_analyzer(stale_source_exe);
const auto stale_source_result =
stale_source_analyzer.analyze_exact_entries({kLoad});
CHECK(starts(stale_source_result) == std::set<uint32_t>({kLoad}) &&
stale_source_result.absorbed_entries.empty(),
"cyclic stale call evidence fails closed to explicit roots");
// beq rN,rN is unconditional: PC+8 is not reachable and a hidden JAL
// there cannot manufacture a function or absorbed alias.
auto always_branch_image = make_exe_buffer(0x1000);
put32(always_branch_image, text + 0x00,
(0x04u << 26) | (8u << 21) | (8u << 16) | 0x1Fu); // -> +0x80
put32(always_branch_image, text + 0x04, 0x00000000u);
put32(always_branch_image, text + 0x08, jal(kLoad + 0x40));
put32(always_branch_image, text + 0x0C, 0x00000000u);
put32(always_branch_image, text + 0x40, 0x03E00008u);
put32(always_branch_image, text + 0x44, 0x00000000u);
put32(always_branch_image, text + 0x80, 0x03E00008u);
put32(always_branch_image, text + 0x84, 0x00000000u);
auto always_branch_exe = parse(always_branch_image);
PSXRecomp::FunctionAnalyzer always_branch_analyzer(always_branch_exe);
const auto always_branch_result =
always_branch_analyzer.analyze_exact_entries({kLoad});
CHECK(starts(always_branch_result) == std::set<uint32_t>({kLoad}) &&
always_branch_result.absorbed_entries.empty(),
"always-taken branch excludes unreachable fallthrough call");
// bne rN,rN is never taken: its encoded target cannot contribute a hidden
// call edge, while its ordinary delay slot and PC+8 remain reachable.
auto never_branch_image = make_exe_buffer(0x1000);
put32(never_branch_image, text + 0x00,
(0x05u << 26) | (8u << 21) | (8u << 16) | 0x1Fu); // false -> +0x80
put32(never_branch_image, text + 0x04, 0x00000000u);
put32(never_branch_image, text + 0x08, 0x03E00008u);
put32(never_branch_image, text + 0x0C, 0x00000000u);
put32(never_branch_image, text + 0x40, 0x03E00008u);
put32(never_branch_image, text + 0x44, 0x00000000u);
put32(never_branch_image, text + 0x80, jal(kLoad + 0x40));
put32(never_branch_image, text + 0x84, 0x00000000u);
put32(never_branch_image, text + 0x88, 0x03E00008u);
put32(never_branch_image, text + 0x8C, 0x00000000u);
auto never_branch_exe = parse(never_branch_image);
PSXRecomp::FunctionAnalyzer never_branch_analyzer(never_branch_exe);
const auto never_branch_result =
never_branch_analyzer.analyze_exact_entries({kLoad});
CHECK(starts(never_branch_result) == std::set<uint32_t>({kLoad}) &&
never_branch_result.absorbed_entries.empty(),
"never-taken branch excludes unreachable target call");
// bgezal $zero is the BAL alias: the target is call evidence and the
// return continuation at PC+8 remains reachable.
auto bal_image = make_exe_buffer(0x1000);
put32(bal_image, text + 0x00,
(0x01u << 26) | (0x11u << 16) | 0x1Fu); // bal +0x80
put32(bal_image, text + 0x04, 0x00000000u);
put32(bal_image, text + 0x08, jal(kLoad + 0x40));
put32(bal_image, text + 0x0C, 0x00000000u);
put32(bal_image, text + 0x10, 0x03E00008u);
put32(bal_image, text + 0x14, 0x00000000u);
put32(bal_image, text + 0x40, 0x03E00008u);
put32(bal_image, text + 0x44, 0x00000000u);
put32(bal_image, text + 0x80, 0x03E00008u);
put32(bal_image, text + 0x84, 0x00000000u);
auto bal_exe = parse(bal_image);
PSXRecomp::FunctionAnalyzer bal_analyzer(bal_exe);
const auto bal_starts = starts(bal_analyzer.analyze_exact_entries({kLoad}));
CHECK(bal_starts.count(kLoad + 0x40) && bal_starts.count(kLoad + 0x80),
"BAL preserves call target and return-continuation discovery");
// A synthetic adjacent-producer envelope is not one linked image. The
// caller supplies the cross-boundary targets that passed its independent
// callable-CFG proof; other valid-looking targets stay with the interpreter.
PSXRecomp::FunctionAnalyzer composite_analyzer(exe);
const std::vector<std::pair<uint32_t, uint32_t>> producer_ranges = {
{kLoad, kLoad + 0x80}, {kLoad + 0x80, kLoad + 0x1000}};
const auto composite = starts(composite_analyzer.analyze_exact_entries(
{kLoad}, producer_ranges, {kLoad + 0x100}));
CHECK(composite.count(kLoad) && composite.count(kLoad + 0x100),
"approved cross-producer call target is discovered");
CHECK(!composite.count(kLoad + 0x300),
"unapproved cross-producer JAL target fails closed");
// A constant-register JR out of the current function is a statically
// proven tail call. Resolve the common lui/addiu/jr sequence without an
// execution-derived seed.
auto indirect_image = make_exe_buffer(0x1000, kLoad + 0x500);
put32(indirect_image, text + 0x400, 0x24020001u);
put32(indirect_image, text + 0x404, 0x03E00008u);
put32(indirect_image, text + 0x408, 0x00000000u);
put32(indirect_image, text + 0x500, 0x3C088001u); // lui $t0,0x8001
put32(indirect_image, text + 0x504, 0x25080400u); // addiu $t0,$t0,0x400
put32(indirect_image, text + 0x508, 0x01000008u); // jr $t0
put32(indirect_image, text + 0x50C, 0x00000000u);
auto indirect_exe = parse(indirect_image);
PSXRecomp::FunctionAnalyzer indirect_analyzer(indirect_exe);
const auto indirect = starts(
indirect_analyzer.analyze_exact_entries({kLoad + 0x500}));
CHECK(indirect.count(kLoad + 0x400) && indirect.count(kLoad + 0x500),
"constant-register JR discovers its frameless tail-call target");
// A jump enters at the low-half definition and skips the preceding LUI.
// A raw backward scan would combine them; reaching-definition proof must
// reject that synthetic indirect target.
auto split_constant_image = make_exe_buffer(0x1000, kLoad + 0x500);
put32(split_constant_image, text + 0x400, 0x03E00008u);
put32(split_constant_image, text + 0x404, 0x00000000u);
put32(split_constant_image, text + 0x500,
0x08000000u | (((kLoad + 0x518) >> 2) & 0x03FFFFFFu));
put32(split_constant_image, text + 0x504, 0x00000000u);
put32(split_constant_image, text + 0x514, 0x3C088001u);
put32(split_constant_image, text + 0x518, 0x25080400u);
put32(split_constant_image, text + 0x51C, 0x01000008u);
put32(split_constant_image, text + 0x520, 0x00000000u);
auto split_constant_exe = parse(split_constant_image);
PSXRecomp::FunctionAnalyzer split_constant_analyzer(split_constant_exe);
const auto split_constant = starts(
split_constant_analyzer.analyze_exact_entries({kLoad + 0x500}));
CHECK(!split_constant.count(kLoad + 0x400),
"indirect resolver rejects a definition skipped by an inbound edge");
// The LUI executes in a JAL delay slot, then the callee may clobber t0
// before returning to the low half. It is not a reaching definition for
// the later JALR even though the raw words are adjacent.
auto call_delay_image = make_exe_buffer(0x1000, kLoad + 0x500);
put32(call_delay_image, text + 0x300, 0x03E00008u);
put32(call_delay_image, text + 0x304, 0x00000000u);
put32(call_delay_image, text + 0x400, 0x03E00008u);
put32(call_delay_image, text + 0x404, 0x00000000u);
put32(call_delay_image, text + 0x500, jal(kLoad + 0x300));
put32(call_delay_image, text + 0x504, 0x3C088001u); // call delay LUI
put32(call_delay_image, text + 0x508, 0x25080400u);
put32(call_delay_image, text + 0x50C, 0x0100F809u);
put32(call_delay_image, text + 0x510, 0x00000000u);
put32(call_delay_image, text + 0x514, 0x03E00008u);
put32(call_delay_image, text + 0x518, 0x00000000u);
auto call_delay_exe = parse(call_delay_image);
PSXRecomp::FunctionAnalyzer call_delay_analyzer(call_delay_exe);
const auto call_delay = starts(
call_delay_analyzer.analyze_exact_entries({kLoad + 0x500}));
CHECK(call_delay.count(kLoad + 0x300) &&
!call_delay.count(kLoad + 0x400),
"indirect resolver rejects a LUI in a call delay slot");
// Ape Escape's save-screen dispatcher builds its table base across
// registers (lui t0; addiu s0,t0,lo). Prove the complete bounded switch,
// then make every adversarial mutation fail closed as one whole table.
auto ape_switch = make_ape_switch();
auto ape_switch_exe = parse(ape_switch.image);
PSXRecomp::ExactJumpTable ape_table;
CHECK(PSXRecomp::resolve_exact_bounded_jump_table(
ape_switch_exe, ape_switch.entry, kLoad + 0x1000u,
ape_switch.jr_pc, 2u, ape_table) &&
ape_table.table_base == ape_switch.table &&
ape_table.table_count == ape_switch.cases.size() &&
ape_table.targets.size() == ape_switch.cases.size(),
"Ape cross-register bounded switch resolves exactly");
PSXRecomp::FunctionAnalyzer ape_switch_analyzer(ape_switch_exe);
const auto ape_result = ape_switch_analyzer.analyze_exact_entries(
{ape_switch.entry});
bool all_ape_cases_reachable = true;
for (uint32_t target : ape_switch.cases)
all_ape_cases_reachable &= ape_result.exact_reachable_pcs.count(target) != 0;
CHECK(all_ape_cases_reachable,
"exact reachable discovery follows every validated Ape case");
auto ori_switch = make_ape_switch();
put32(ori_switch.image, text + 0x504, 0x35100A00u); // ori s0,t0,0x0a00
CHECK(!resolves_ape_switch(ori_switch, ori_switch.entry),
"unproven ORI table constants fail closed");
auto no_lw_nop_switch = make_ape_switch();
no_lw_nop_switch.jr_pc = kLoad + 0x524u;
put32(no_lw_nop_switch.image, text + 0x524, 0x00400008u);
put32(no_lw_nop_switch.image, text + 0x528, 0x00000000u);
CHECK(resolves_ape_switch(no_lw_nop_switch, no_lw_nop_switch.entry),
"zero-NOP LW-to-JR schedule matches Python parity");
auto two_lw_nop_switch = make_ape_switch();
two_lw_nop_switch.jr_pc = kLoad + 0x52Cu;
put32(two_lw_nop_switch.image, text + 0x528, 0x00000000u);
put32(two_lw_nop_switch.image, text + 0x52C, 0x00400008u);
CHECK(!resolves_ape_switch(two_lw_nop_switch,
two_lw_nop_switch.entry),
"two-NOP LW-to-JR schedule fails closed");
auto wrong_jr_switch = make_ape_switch();
put32(wrong_jr_switch.image, text + 0x528,
0x00400808u); // reserved rd field makes this noncanonical JR
CHECK(!resolves_ape_switch(wrong_jr_switch, wrong_jr_switch.entry),
"jump-table proof checks the exact JR encoding");
auto noncanonical_lui_switch = make_ape_switch();
put32(noncanonical_lui_switch.image, text + 0x500,
0x3C288001u); // LUI's reserved rs field is nonzero
CHECK(!resolves_ape_switch(noncanonical_lui_switch,
noncanonical_lui_switch.entry),
"jump-table proof rejects noncanonical LUI reserved fields");
auto cross_producer_switch = make_ape_switch();
CHECK(!resolves_ape_switch(cross_producer_switch,
cross_producer_switch.entry,
kLoad + 0x400u, kLoad + 0x900u),
"producer A cannot borrow table slots from adjacent producer B");
auto producer_case_escape = make_ape_switch();
put32(producer_case_escape.image, text + 0x540,
0x08000000u | (((kLoad + 0xB20u) >> 2) & 0x03FFFFFFu));
put32(producer_case_escape.image, text + 0x544, 0x00000000u);
put32(producer_case_escape.image, text + 0xB20,
0x08000000u | (((kLoad + 0x518u) >> 2) & 0x03FFFFFFu));
put32(producer_case_escape.image, text + 0xB24, 0x00000000u);
CHECK(!resolves_ape_switch(producer_case_escape,
producer_case_escape.entry,
kLoad + 0x400u, kLoad + 0xA20u),
"case walk cannot escape through an adjacent producer");
auto cross_producer_exe = parse(cross_producer_switch.image);
PSXRecomp::FunctionAnalyzer cross_producer_analyzer(cross_producer_exe);
const auto cross_producer_result =
cross_producer_analyzer.analyze_exact_entries(
{cross_producer_switch.entry},
{{kLoad + 0x400u, kLoad + 0x900u},
{kLoad + 0x900u, kLoad + 0x1000u}});
CHECK(!cross_producer_result.exact_reachable_pcs.count(kLoad + 0x540u),
"reachable discovery rejects a cross-producer table");
CHECK(!cross_producer_result.functions.empty() &&
cross_producer_result.functions.front().producer_lo ==
kLoad + 0x400u &&
cross_producer_result.functions.front().producer_hi ==
kLoad + 0x900u,
"exact functions retain producer bounds for code generation");
auto invalid_target_switch = make_ape_switch();
put32(invalid_target_switch.image, text + 0x550, 0xFFFFFFFFu);
CHECK(!resolves_ape_switch(invalid_target_switch,
invalid_target_switch.entry),
"one invalid target instruction rejects the entire table");
auto clobbered_switch = make_ape_switch();
put32(clobbered_switch.image, text + 0x508,
0x24100000u); // addiu s0,zero,0
CHECK(!resolves_ape_switch(clobbered_switch, clobbered_switch.entry),
"table-base clobber rejects the bounded switch");
auto skipped_def_switch = make_ape_switch();
put32(skipped_def_switch.image, text + 0x700,
0x08000000u |
(((kLoad + 0x504u) >> 2) & 0x03FFFFFFu));
put32(skipped_def_switch.image, text + 0x704, 0x00000000u);
CHECK(!resolves_ape_switch(skipped_def_switch,
skipped_def_switch.entry),
"later host bytes cannot jump into and skip a protected definition");
auto bad_bound_switch = make_ape_switch();
put32(bad_bound_switch.image, text + 0x50C,
0x2C820003u); // sltiu v0,a0,3: wrong index dependency
CHECK(!resolves_ape_switch(bad_bound_switch, bad_bound_switch.entry),
"unrelated SLTIU bound cannot validate a jump table");
auto outside_target_switch = make_ape_switch();
put32(outside_target_switch.image, text + 0xA04, kLoad + 0x2000u);
CHECK(!resolves_ape_switch(outside_target_switch,
outside_target_switch.entry),
"one out-of-image table target rejects the entire table");
// A packed BIOS thunk is also a backward-scan boundary. The frameless
// leaf after its delay slot must not be swallowed into the thunk run.
auto thunk_image = make_exe_buffer(0x1000);
put32(thunk_image, text + 0x600, 0x240A00B0u);
put32(thunk_image, text + 0x604, 0x01400008u);
put32(thunk_image, text + 0x608, 0x24090008u);
put32(thunk_image, text + 0x60C, 0x00000000u);
put32(thunk_image, text + 0x610, 0x24020001u);
put32(thunk_image, text + 0x614, 0x03E00008u);
put32(thunk_image, text + 0x618, 0x00000000u);
auto thunk_exe = parse(thunk_image);
PSXRecomp::FunctionAnalyzer thunk_analyzer(thunk_exe);
const auto thunk_starts = starts(thunk_analyzer.analyze());
CHECK(thunk_starts.count(kLoad + 0x600) &&
thunk_starts.count(kLoad + 0x610),
"packed BIOS thunk bounds the following frameless leaf");
PSXRecomp::FunctionAnalyzer seeded_analyzer(exe);
const auto seeded = starts(
seeded_analyzer.analyze_exact_entries({kLoad, kLoad + 0x200}));
CHECK(seeded.count(kLoad + 0x200),
"evidence-backed explicit callback seed is compiled");
auto bad = parse(image);
const auto original_size = bad.code_data.size();
CHECK(!PSXRecomp::apply_static_analysis_bound(bad, 0x1800, error),
"non-page-aligned truncation is rejected");
CHECK(bad.code_size() == 0x2000 && bad.code_data.size() == original_size,
"invalid bound leaves parsed image unchanged");
CHECK(!PSXRecomp::apply_static_analysis_bound(bad, 0x3000, error),
"oversized noncanonical bound is rejected");
CHECK(!PSXRecomp::apply_static_analysis_bound(bad, 0, error),
"zero bound is rejected");
auto excludes_entry = parse(image);
excludes_entry.header.initial_pc = kLoad + 0x1000;
CHECK(!PSXRecomp::apply_static_analysis_bound(excludes_entry, 0x1000, error),
"bound excluding entry point is rejected");
auto rounded = parse(make_exe_buffer(0x1800));
CHECK(PSXRecomp::apply_static_analysis_bound(rounded, 0x2000, error),
"canonical generated final-page reservation remains compatible");
CHECK(rounded.code_size() == 0x1800,
"page reservation does not widen static analysis payload");
// Retail PS-X EXEs may declare BSS inside a sector-rounded payload. The
// overlap is safe only when every overlapped file byte is padding zero.
auto zero_padded_bss = make_exe_buffer(0x1000);
put32(zero_padded_bss, 0x28, kLoad + 0xDBC); // 0x244-byte overlap
put32(zero_padded_bss, 0x2C, 0x400);
std::string zero_bss_error;
const auto zero_bss_exe = PSXRecomp::PS1ExeParser::parse_buffer(
zero_padded_bss, zero_bss_error);
CHECK(zero_bss_exe.has_value(),
"zero-only BSS/payload overlap is accepted");
auto nonzero_bss = zero_padded_bss;
nonzero_bss[text + 0xE00] = 1;
std::string nonzero_bss_error;
const auto nonzero_bss_exe = PSXRecomp::PS1ExeParser::parse_buffer(
nonzero_bss, nonzero_bss_error);
CHECK(!nonzero_bss_exe.has_value(),
"non-zero BSS/code overlap remains rejected");
CHECK(nonzero_bss_error.find("overlaps non-zero code/data") !=
std::string::npos,
"non-zero overlap reports the unsafe bytes");
auto before_code_bss = zero_padded_bss;
put32(before_code_bss, 0x28, kLoad - 4);
std::string before_code_error;
CHECK(!PSXRecomp::PS1ExeParser::parse_buffer(
before_code_bss, before_code_error).has_value(),
"BSS beginning before the payload remains rejected");
// The KSEG0 main-RAM decode window is 8 MiB: an image placed in a retail
// mirror (a mod engine copied to 0x80780000) is valid, one crossing the
// window end is not.
auto mirror_image = make_exe_buffer(0x1000);
put32(mirror_image, 0x10, 0x80780000u);
put32(mirror_image, 0x18, 0x80780000u);
std::string mirror_error;
CHECK(PSXRecomp::PS1ExeParser::parse_buffer(mirror_image, mirror_error).has_value(),
"image in the 4th RAM mirror is accepted");
put32(mirror_image, 0x10, 0x807FF800u);
put32(mirror_image, 0x18, 0x807FF800u);
CHECK(!PSXRecomp::PS1ExeParser::parse_buffer(mirror_image, mirror_error).has_value() &&
mirror_error.find("beyond 0x80800000") != std::string::npos,
"image crossing the RAM decode window end is rejected");
// A KUSEG header names the same RAM as KSEG0 but keeps its segment: the
// program runs at KUSEG PCs, so it compiles there
// (docs/SEGMENT_AWARE_CODE.md §5.3).
put32(mirror_image, 0x10, 0x00780000u);
put32(mirror_image, 0x18, 0x00780000u);
{
auto kuseg = PSXRecomp::PS1ExeParser::parse_buffer(mirror_image, mirror_error);
CHECK(kuseg.has_value() && kuseg->load_address() == 0x00780000u &&
kuseg->header.initial_pc == 0x00780000u &&
kuseg->link_segment() == PSXRecomp::kSegKUSEG &&
kuseg->phys_load_address() == 0x00780000u &&
kuseg->contains_phys(0x80780FFCu) &&
!kuseg->contains_phys(0x00781000u) &&
kuseg->read_word(0x00780000u).has_value() &&
!kuseg->read_word(0x80780000u).has_value(),
"a KUSEG header keeps its segment; bytes are addressed physically");
}
put32(mirror_image, 0x10, 0x007FF800u);
put32(mirror_image, 0x18, 0x007FF800u);
CHECK(!PSXRecomp::PS1ExeParser::parse_buffer(mirror_image, mirror_error).has_value() &&
mirror_error.find("beyond 0x00800000") != std::string::npos,
"a KUSEG image crossing its RAM window end is rejected");
// KSEG1 is accepted as an uncached home segment.
put32(mirror_image, 0x10, 0xA0010000u);
put32(mirror_image, 0x18, 0xA0010000u);
{
auto kseg1 = PSXRecomp::PS1ExeParser::parse_buffer(mirror_image, mirror_error);
CHECK(kseg1.has_value() && kseg1->link_segment() == PSXRecomp::kSegKSEG1,
"a KSEG1-linked header is accepted in its own segment");
}
// One image, one link segment.
put32(mirror_image, 0x10, 0x80010000u);
put32(mirror_image, 0x18, 0x00010000u);
CHECK(!PSXRecomp::PS1ExeParser::parse_buffer(mirror_image, mirror_error).has_value() &&
mirror_error.find("different segments") != std::string::npos,
"an entry in another segment than the load address is rejected");
// Outside main RAM (KSEG2, the BIOS ROM) is not a PS-X EXE target.
put32(mirror_image, 0x10, 0xBFC00000u);
put32(mirror_image, 0x18, 0xBFC00000u);
CHECK(!PSXRecomp::PS1ExeParser::parse_buffer(mirror_image, mirror_error).has_value() &&
mirror_error.find("Must be in main RAM") != std::string::npos,
"a load address outside the main-RAM windows is rejected");
if (failures) {
std::fprintf(stderr, "FAILED (%d)\n", failures);
return 1;
}
std::puts("ALL PASS");
return 0;
}