Files
psxrecomp/recompiler/include/ps1_exe_parser.h
Shokunin 6a76eb452b recompiler: compile segment variants of game code from segment-qualified seeds
A seed that names the image's bytes in another segment than its link
segment (0xA00100F0 for a KUSEG-linked EXE, 0x00012345 for a KSEG0 one)
now compiles a segment variant (docs/SEGMENT_AWARE_CODE.md §5.4): the home
functions the seed reaches through direct edges (calls, tail jumps, split
pieces, fall-throughs, call returns; a seed at a CPS continuation roots its
owner, an alias entry its alias group) are compiled again through a view of
the image in that segment. The variant's names, rows and every PC it bakes
are that segment's, and a KSEG1 variant charges a fetch per instruction
(#429's rule through B's runtime_pc()). A cached variant is the home C with
the segment swapped. Exact-match config sites move into the variant's
segment, so a variant gets every hook the home body gets. A seed that names
no home dispatch entry stops the build, and so does a seed in a segment
that does not map physical memory (0x20000000-0x7FFFFFFF or KSEG2: Beetle's
addr_mask leaves them unmasked, so they address no RAM and are no alias of
the image's bytes).
tools/collect_game_misses.py reports such a segment miss instead of writing
it as a seed.

The dispatch table holds a row per compiled segment of a word, adjacent and
in PC order; when any word has more than one row the lookup (indexed and
binary search) searches the word's rows for the exact PC. Two rows for one
PC remain a build error. A title without variants gets the same dispatch
source as before.

A jump-table case whose value is in another segment than its body is no
longer a local case: the default tail-transfer dispatches it with its full
PC. This applies to every compile. In a variant it is the common case (a
KSEG0 variant of KUSEG-linked code). A home or overlay compile has one only
when its table holds another segment's PCs of its own bytes, for example a
KSEG0-compiled overlay shard of code that runs at KUSEG. Such a case used to
continue in the KSEG0 body with KSEG0 links and fetch tags. Now it
dispatches: a game-text PC with no row in its segment is a recorded segment
miss, and #417's gate sends an overlay PC to the interpreter; both run it
at its own PC (PR E returns overlay code to native per-segment shards). R4's
game C and its 26 overlay shard sources are unchanged.

Ledger: segment-variants and kseg1-fetch-charge close (KNOWN_GAPS is empty)
and become regression guards; resume-pc-segment now requires each row's key
and resume PC to be in its body's segment, and segment-variants checks every
PC a variant body bakes. New segment_variants_test; kuseg_dispatch_lookup
and emitter_runtime_pc_test cover variant rows, closure, equality with the
home C, per-instruction KSEG1 charging, foreign jump-table cases at home and
in variants, data stubs (no row), alias groups declared for every shard and
seeds outside the physical segments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:24:07 -07:00

232 lines
9.9 KiB
C++

#pragma once
#include <cstdint>
#include <vector>
#include <string>
#include <optional>
#include <filesystem>
namespace PSXRecomp {
// Segments of the MIPS address space (docs/SEGMENT_AWARE_CODE.md §2). A PC
// carries one: it reaches link values, EPCs and I-cache tags, and KSEG1
// fetches are uncached. Data loads and stores reduce all three to the same
// physical address.
inline constexpr uint32_t kSegmentMask = 0xE0000000u;
inline constexpr uint32_t kPhysMask = 0x1FFFFFFFu;
inline constexpr uint32_t kSegKUSEG = 0x00000000u;
inline constexpr uint32_t kSegKSEG0 = 0x80000000u;
inline constexpr uint32_t kSegKSEG1 = 0xA0000000u;
// True when `pc` lies in one of the three windows that map the physical
// address space: KUSEG below 0x20000000, KSEG0 and KSEG1. The rest of KUSEG
// (0x20000000-0x7FFFFFFF) and KSEG2 are not mirrors of it: Beetle's addr_mask
// leaves them unmasked, so they address no RAM or BIOS ROM, and a PC there
// never names the bytes at `pc & kPhysMask`.
inline constexpr bool maps_physical(uint32_t pc) {
return pc < 0x20000000u || (pc & kSegmentMask) == kSegKSEG0 ||
(pc & kSegmentMask) == kSegKSEG1;
}
// PS-X EXE header structure (2048 bytes)
#pragma pack(push, 1)
struct PS1ExeHeader {
// Offset 0x00-0x0F: Magic + padding
char magic[8]; // "PS-X EXE"
uint32_t pad0;
uint32_t pad1;
// Offset 0x10-0x1F: Core addresses
uint32_t initial_pc; // Entry point
uint32_t initial_gp; // Global pointer
uint32_t load_address; // RAM load address
uint32_t file_size; // Executable size (bytes)
// Offset 0x20-0x2F: Memfill (BSS section)
uint32_t unknown0;
uint32_t unknown1;
uint32_t memfill_start;
uint32_t memfill_size;
// Offset 0x30-0x3F: Stack setup
uint32_t initial_sp; // Stack pointer
uint32_t initial_fp; // Frame pointer
uint32_t stack_base;
uint32_t stack_offset;
// Offset 0x40-0x7FF: Reserved (2048 - 64 bytes already used = 1984 bytes)
uint8_t reserved[1984];
// Helper methods
uint32_t end_address() const {
return load_address + file_size;
}
bool entry_in_range() const {
return initial_pc >= load_address &&
initial_pc < end_address();
}
uint32_t bss_end() const {
return memfill_start + memfill_size;
}
};
#pragma pack(pop)
static_assert(sizeof(PS1ExeHeader) == 2048, "PS1ExeHeader must be exactly 2048 bytes");
// --------------------------------------------------------------------------
// Analysis-bound tag: how many TRAILING bytes of the payload are guard words
// --------------------------------------------------------------------------
//
// Overlay capture DELIBERATELY appends a coherent guard instruction past the
// end of a dirty-page run (runtime/src/overlay_capture.c, write_json_window:
// `size += 4u`, pinned by runtime/tests/test_interpreter_perf_guards.py). A
// MIPS branch sitting at the run's final word (...FFC) always executes its
// delay slot in the NEXT page (...000); supplying that one word lets overlay
// codegen emit exact semantics and lets the range manifest hash/watch it.
//
// So the guard word is a legal DELAY-SLOT SOURCE but an ILLEGAL BLOCK LEADER:
// the word AFTER it does not exist in the image, so a control transfer AT the
// guard word could never have its mandatory delay slot emitted. The
// recompiler must therefore carry two distinct bounds — a READ bound (the
// whole payload, so a delay slot at the guard word resolves) and an ANALYSIS
// bound (short of the guard words, so discovery and block construction can
// never lead there).
//
// The producer that wrapped the capture states the count explicitly, in a
// magic-tagged field in the otherwise-zero tail of the 2048-byte PS-X EXE
// header (tools/compile_overlays.py, make_psxexe). The recompiler is TOLD
// rather than inferring the count from the payload size: an inference from
// `size % 4096 == 4` fits every capture written by the current format but
// would silently shift underneath any future change to capture granularity,
// and pre-2026-07-25 captures are page-exact with no guard word at all.
//
// An absent or malformed tag means ZERO guard bytes, which is exactly what
// every genuine PS-X EXE (the BIOS, a retail main executable) is: those images
// have no guard word, their analysis bound equals their read bound, and their
// analysis is bit-for-bit unchanged by this mechanism.
namespace exe_tag {
// Offsets are into the 2048-byte header. Retail headers use only
// [0x00, 0x4C) plus an ASCII region string ending well before 0x100; the tail
// is zero-filled. The 8-byte magic makes a false positive on a real EXE
// effectively impossible even if some image did carry bytes there.
inline constexpr uint32_t kGuardMagicOffset = 0x7E0u;
inline constexpr uint32_t kGuardCountOffset = 0x7E8u; // uint32, little-endian
inline constexpr char kGuardMagic[8] =
{'P', 'S', 'X', 'R', 'G', 'R', 'D', '1'};
// A guard run exists to cover branch delay slots at a page edge; one word is
// what the capture format appends. Cap the accepted value at a page so a
// corrupt tag can never shrink analysis to nothing.
inline constexpr uint32_t kMaxGuardBytes = 4096u;
} // namespace exe_tag
// Decode the analysis-bound tag. Returns 0 when the tag is absent, when the
// magic does not match, or when the declared count is not a sane, strictly
// interior, instruction-aligned trailer of `file_size` bytes of payload.
uint32_t decode_analysis_guard_bytes(const PS1ExeHeader& header,
uint32_t file_size);
// Parsed PS1 executable with code data
class PS1Executable {
public:
PS1ExeHeader header;
std::vector<uint8_t> code_data; // Raw binary (file_size bytes)
// Trailing bytes of code_data that exist ONLY to supply architectural
// delay slots to instructions inside the analysis bound. Read-legal,
// analysis-illegal. See the exe_tag comment above. Zero for every image
// that is not an overlay capture wrapped by compile_overlays.
uint32_t analysis_guard_bytes = 0;
// Computed properties. Addresses are the header's, in its link segment:
// the parser does not rewrite them (docs/SEGMENT_AWARE_CODE.md §5.3).
uint32_t load_address() const { return header.load_address; }
uint32_t entry_point() const { return header.initial_pc; }
uint32_t code_size() const { return header.file_size; }
// The segment the program is linked for and runs in: KUSEG, KSEG0 or
// KSEG1. validate_header() requires the load address and the entry to
// share it. Code identity is the full virtual address in this segment
// (§5.1); an address in another segment names the same bytes but a
// different code identity (a segment variant, §5.4).
uint32_t link_segment() const { return header.load_address & kSegmentMask; }
// Physical helpers for byte addressing: the same RAM in every segment.
uint32_t phys_load_address() const { return header.load_address & kPhysMask; }
bool contains_phys(uint32_t address) const {
const uint32_t phys = address & kPhysMask;
return phys >= phys_load_address() &&
phys - phys_load_address() < header.file_size;
}
// READ bound: one past the last byte the image physically supplies. Used
// by read_word so a mandatory delay slot living at a guard word resolves.
uint32_t end_address() const { return header.end_address(); }
// ANALYSIS bound: one past the last byte that may be DISCOVERED, made a
// block leader, or made a block's exit instruction. Equal to
// end_address() unless the producer declared trailing guard words.
uint32_t analysis_end_address() const {
const uint32_t hi = end_address();
return (analysis_guard_bytes < hi - load_address())
? hi - analysis_guard_bytes
: hi;
}
bool has_analysis_guard() const { return analysis_guard_bytes != 0; }
// Access code as 32-bit words (for MIPS disassembly).
//
// Deliberately bounded by end_address(), NOT analysis_end_address(): the
// entire point of a guard word is that it is readable so that a transfer
// at the last analysable word can emit its delay slot.
std::optional<uint32_t> read_word(uint32_t address) const {
if (address < load_address() || address >= end_address()) {
return std::nullopt; // Out of range
}
uint32_t offset = address - load_address();
if (offset + 3 >= code_data.size()) {
return std::nullopt; // Partial word
}
// Little-endian read
return (uint32_t)code_data[offset] |
((uint32_t)code_data[offset+1] << 8) |
((uint32_t)code_data[offset+2] << 16) |
((uint32_t)code_data[offset+3] << 24);
}
// Validate executable is well-formed
bool validate(std::string& error_msg) const;
};
// Parser class
class PS1ExeParser {
public:
// Parse PS1-EXE from file
static std::optional<PS1Executable> parse_file(
const std::filesystem::path& path,
std::string& error_msg
);
// Parse PS1-EXE from memory buffer
static std::optional<PS1Executable> parse_buffer(
const std::vector<uint8_t>& buffer,
std::string& error_msg
);
// Validate header (public for PS1Executable::validate)
static bool validate_header(const PS1ExeHeader& header, std::string& error_msg);
};
// Restrict a parsed main executable to a title-verified static-analysis size.
// The original disc image is not modified; only this in-memory analysis view
// is shortened. Returns false without mutation when the configured bound is
// invalid. A canonical page-rounded size above the header size is accepted as
// an existing runtime reservation and leaves the analysis view unchanged.
bool apply_static_analysis_bound(PS1Executable& exe,
uint32_t configured_size,
std::string& error_msg);
} // namespace PSXRecomp