Files
psxrecomp/recompiler/tests/emitter_runtime_pc_test.cpp
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

646 lines
31 KiB
C++

// Every guest-visible PC the emitters bake goes through runtime_pc()
// (docs/SEGMENT_AWARE_CODE.md §5.2, rollout PR B).
//
// Game/overlay emitter: CodeGenerator::runtime_pc(addr) = code_seg | phys.
// The same synthetic function is compiled at KSEG0 compile addresses with the
// code segment left at its default, set to KSEG0 explicitly, to KUSEG and to
// KSEG1. The checks:
// - the default and an explicit KSEG0 give byte-identical output, and the
// default follows the image (a KSEG1 image keeps its own segment), so the
// refactor changes nothing for any existing input;
// - with KUSEG or KSEG1, every PC in guest-visible state carries that
// segment: links (including a jalr in a branch delay slot), fetch tags,
// interrupt resume PCs, CPS exits (split j, fallthrough past the unit
// edge, image-edge tail transfer) and continuation keys, stale-static
// guards (named and addressed), store-PC stamps, the reserved-instruction
// EPC, the slice resume PC, call_by_address targets, the call-contract
// return PC, the VSync-query hook's base PC, alias-group entry keys and
// wrapper entries, and the unknown-dispatch PC of data stubs;
// - identity keys (func_/block_ names, debug_server_log_call_entry, the
// cyc_watch block observer, psx_native_bad_entry's owner) keep the
// compile address;
// - nothing else changes: with every 0x........ constant reduced to its
// physical address, the KUSEG output equals the KSEG0 output;
// - a KSEG1 code segment charges a fetch before every instruction, exactly
// as compiling the same bytes at KSEG1 addresses does (#429's rule, now
// keyed on the runtime PC; PR D's KSEG1 variants rely on it).
// Each is checked in CPS mode (the default), CPS overlay mode and legacy
// (PSX_CPS=0) mode.
//
// BIOS translator: StrictTranslator::translate(d, runtime_pc) stamps stores,
// the syscall EPC and the break/unaligned-access PCs (immediate and deferred
// loads) with the runtime PC while the psx_ldd_ temporaries keep the ROM
// address. The syscall form follows PSX_CPS at process start, so ctest runs
// this binary twice (emitter_runtime_pc_test and _legacy).
//
// Game dispatch rows (game_dispatch_emitter.cpp, PR C): each row's key and
// resume PC go through runtime_pc() while its func_ name keeps the compile
// identity, and the emitted lookup requires the exact PC. A real executable
// compiles in its own link segment, where runtime_pc() is the identity, so
// only this test (code segment != compile segment) sees the routes; the
// ledger and test_kuseg_dispatch_lookup.py check the emitted lookup end to end.
// Two rows on one physical word are an error within one compile; a segment
// variant's compile (PR D, §5.4) adds rows on its home rows' words, and the
// lookup then searches the word's rows for the exact PC.
#include "code_generator.h"
#include "control_flow.h"
#include "game_dispatch_emitter.h"
#include "mips_decoder.h"
#include "strict_translator.h"
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <map>
#include <regex>
#include <set>
#include <string>
#include <vector>
namespace {
int failures = 0;
void check(bool condition, const std::string& message) {
if (!condition) {
std::fprintf(stderr, "FAIL: %s\n", message.c_str());
++failures;
}
}
std::string hex(uint32_t v) {
char buf[16];
std::snprintf(buf, sizeof buf, "%08X", v);
return buf;
}
// CodeGenerator reads PSX_CPS when it is constructed.
void set_env(const char* name, const char* value) {
#ifdef _WIN32
_putenv_s(name, value);
#else
setenv(name, value, 1);
#endif
}
void append_word(std::vector<uint8_t>& bytes, uint32_t word) {
bytes.push_back(static_cast<uint8_t>(word));
bytes.push_back(static_cast<uint8_t>(word >> 8));
bytes.push_back(static_cast<uint8_t>(word >> 16));
bytes.push_back(static_cast<uint8_t>(word >> 24));
}
constexpr uint32_t kPhys = 0x00010000u;
constexpr uint32_t KUSEG = 0x00000000u, KSEG0 = 0x80000000u, KSEG1 = 0xA0000000u;
// Functions (compile offsets from kPhys):
// +00 main: jal/bgezal/jalr links and CPS continuations, stores for
// store-PC stamps (the sh at +10 is a widescreen backdrop site and the
// sb at +14 a persisted-option site, which emit their own stamps);
// bgezal's target +28 is a mid-function block leader.
// +40 callee, also the VSync-query HLE function.
// +48 a reserved word (RI exception).
// +50 j to the known function +40; +58 j into the middle of +00.
// +60 plain sh/sb stores and no control flow: falls through into +68.
// +68 a jalr in the delay slot of an always-taken beq.
// +7C, +84 end in jal / jalr whose return lies in the next (known) function.
// +8C a conditional branch to the known +40, falling into the known +94.
// +94 a conditional branch into the middle of +00, falling into +9C.
// +9C, +A4 end in jal / jalr whose return lies in the next function; the jal
// targets +B4.
// +AC a data section; +B4 undecodable words (the PSX_EMIT_PROD data stub).
// +BC host with the alias entries +C0 and +C4; it runs off the image edge.
// +9C, +A4, +AC and +B4 are left out of the known-function set, so edges into
// them go through call_by_address.
const uint32_t kProgram[] = {
0x27BDFFF8u, // +00 addiu $sp, $sp, -8
0xAFBF0004u, // +04 sw $ra, 4($sp)
0x0C004010u, // +08 jal +40
0x00000000u, // +0C nop
0xA7A80002u, // +10 sh $t0, 2($sp)
0xA3A80003u, // +14 sb $t0, 3($sp)
0x04110003u, // +18 bgezal $zero, +28
0x00000000u, // +1C nop
0x0100F809u, // +20 jalr $t0
0x00000000u, // +24 nop
0xABA80004u, // +28 swl $t0, 4($sp)
0xBBA80004u, // +2C swr $t0, 4($sp)
0xEBA80000u, // +30 swc2 $8, 0($sp)
0x8FBF0004u, // +34 lw $ra, 4($sp)
0x03E00008u, // +38 jr $ra
0x27BD0008u, // +3C addiu $sp, $sp, 8
0x03E00008u, // +40 jr $ra (callee)
0x00000000u, // +44 nop
0x50000001u, // +48 beql (reserved on the R3000A: RI exception)
0x00000000u, // +4C nop
0x08004010u, // +50 j +40 (known function)
0x00000000u, // +54 nop
0x08004001u, // +58 j +04 (mid-function)
0x00000000u, // +5C nop
0xA7A80006u, // +60 sh $t0, 6($sp) (plain store sites: +10/+14
0xA3A80007u, // +64 sb $t0, 7($sp) are hooked; falls into +68)
0x10000002u, // +68 beq $zero, $zero, +74
0x0100F809u, // +6C jalr $t0 (in the delay slot)
0x00000000u, // +70 nop
0x03E00008u, // +74 jr $ra
0x00000000u, // +78 nop
0x0C004010u, // +7C jal +40 (returns into +84)
0x00000000u, // +80 nop
0x0100F809u, // +84 jalr $t0 (returns into +8C)
0x00000000u, // +88 nop
0x1500FFECu, // +8C bne $t0, $zero, +40
0x00000000u, // +90 nop
0x1500FFDBu, // +94 bne $t0, $zero, +04
0x00000000u, // +98 nop
0x0C00402Du, // +9C jal +B4 (not known; returns into +A4)
0x00000000u, // +A0 nop
0x0100F809u, // +A4 jalr $t0 (returns into +AC)
0x00000000u, // +A8 nop
0xFFFFFFFFu, // +AC data
0xFFFFFFFFu, // +B0 data
0x44000000u, // +B4 COP1: no translation (TODO words)
0x44000000u, // +B8 COP1
0x25080001u, // +BC addiu $t0, $t0, 1 (alias host)
0xAFA80000u, // +C0 sw $t0, 0($sp) (alias entry)
0x25080001u, // +C4 addiu $t0, $t0, 1 (alias entry)
0x0C004010u, // +C8 jal +40
0x00000000u, // +CC nop
0x25080001u, // +D0 addiu $t0, $t0, 1 (runs off the image edge)
};
constexpr uint32_t kCount = sizeof(kProgram) / sizeof(kProgram[0]);
enum class Mode { Cps, CpsOverlay, Legacy };
const char* mode_name(Mode m) {
return m == Mode::Cps ? "cps" : m == Mode::CpsOverlay ? "cps-overlay" : "legacy";
}
struct FuncSpec {
uint32_t start, end;
uint32_t alias_host; // nonzero: an alias entry of the host at this offset
bool data; // a data section
};
const FuncSpec kFuncs[] = {
{0x00u, 0x40u, 0, false}, {0x40u, 0x48u, 0, false}, {0x48u, 0x50u, 0, false},
{0x50u, 0x58u, 0, false}, {0x58u, 0x60u, 0, false}, {0x60u, 0x68u, 0, false},
{0x68u, 0x7Cu, 0, false}, {0x7Cu, 0x84u, 0, false}, {0x84u, 0x8Cu, 0, false},
{0x8Cu, 0x94u, 0, false}, {0x94u, 0x9Cu, 0, false}, {0x9Cu, 0xA4u, 0, false},
{0xA4u, 0xACu, 0, false}, {0xACu, 0xB4u, 0, true}, {0xB4u, 0xBCu, 0, false},
{0xBCu, 0xD4u, 0, false}, {0xC0u, 0xD4u, 0xBCu, false}, {0xC4u, 0xD4u, 0xBCu, false},
};
constexpr uint32_t kVsyncFunc = 0x40u;
const std::set<uint32_t> kNotKnown = {0x9Cu, 0xA4u, 0xACu, 0xB4u};
constexpr uint32_t kBackdropSite = 0x10u; // sh
constexpr uint32_t kPersistSite = 0x14u; // sb
// Words with no emitted fetch of their own: the reserved word raises RI
// before its timing (the emitter's existing order) and the word after it is
// unreachable; data and undecodable words become stubs.
const std::set<uint32_t> kNoFetch = {0x48u, 0x4Cu, 0xACu, 0xB0u, 0xB4u, 0xB8u};
struct Compiled {
std::string code; // every generated function
std::string dispatch; // the game dispatch source
};
// seg_override < 0: leave the code segment at its default.
Compiled generate_all(uint32_t image_seg, long long seg_override, Mode mode) {
set_env("PSX_CPS", mode == Mode::Legacy ? "0" : "1");
const uint32_t base = image_seg | kPhys;
PSXRecomp::PS1Executable exe{};
exe.header.load_address = base;
exe.header.initial_pc = base;
exe.header.file_size = kCount * 4u;
for (uint32_t w : kProgram) append_word(exe.code_data, w);
PSXRecomp::CodeGenConfig config{};
config.emit_comments = true;
config.indent = " ";
config.overlay_mode = (mode == Mode::CpsOverlay);
config.vsync_query_hle_funcs[base + kVsyncFunc] = {0x80090000u, 0x80090004u,
0x80090008u, 0x8009000Cu};
config.ws_backdrop_x_sites.insert(base + kBackdropSite);
config.persist_init_store_sites.insert(base + kPersistSite);
PSXRecomp::ControlFlowAnalyzer analyzer(exe);
PSXRecomp::CodeGenerator generator(exe, config);
if (seg_override >= 0) generator.set_code_segment(static_cast<uint32_t>(seg_override));
std::vector<PSXRecomp::Function> funcs;
std::set<uint32_t> known;
for (const FuncSpec& fs : kFuncs) {
PSXRecomp::Function f{};
f.start_addr = base + fs.start;
f.end_addr = base + fs.end;
f.size = fs.end - fs.start;
f.name = "func_" + hex(f.start_addr);
f.is_data_section = fs.data;
if (fs.alias_host) {
f.alias_walk_lo = base + fs.alias_host;
for (const FuncSpec& o : kFuncs)
if (o.alias_host == fs.alias_host) f.alias_group_entries.push_back(base + o.start);
}
funcs.push_back(f);
if (!kNotKnown.count(fs.start)) known.insert(f.start_addr);
}
generator.set_known_functions(known);
std::map<uint32_t, PSXRecomp::ControlFlowGraph> cfgs;
for (const auto& f : funcs)
if (!f.is_data_section) cfgs[f.start_addr] = analyzer.analyze_function(f);
Compiled out;
std::set<uint32_t> dispatch_addrs;
for (const auto& gf : generator.generate_all_functions(funcs, cfgs)) {
out.code += gf.full_code;
// main_psx.cpp's rule: dispatchable func_ entries only.
if (gf.dispatchable && gf.function_name.rfind("func_", 0) == 0)
dispatch_addrs.insert(static_cast<uint32_t>(
std::stoul(gf.function_name.substr(5, 8), nullptr, 16)));
}
std::string error;
if (!PSXRecomp::emit_game_dispatch(generator, exe, dispatch_addrs,
generator.generate_ranges_manifest(funcs, cfgs),
out.dispatch, error))
check(false, std::string("emit_game_dispatch: ") + error);
return out;
}
std::string generate(uint32_t image_seg, long long seg_override, Mode mode) {
return generate_all(image_seg, seg_override, mode).code;
}
// Constants of one site class, in emission order.
std::vector<uint32_t> constants(const std::string& code, const std::string& pattern) {
std::vector<uint32_t> out;
const std::regex re(pattern);
for (auto it = std::sregex_iterator(code.begin(), code.end(), re);
it != std::sregex_iterator(); ++it) {
for (size_t g = 1; g < it->size(); ++g) {
if ((*it)[g].matched) {
out.push_back(static_cast<uint32_t>(std::stoul((*it)[g].str(), nullptr, 16)));
break;
}
}
}
return out;
}
// Every 0x........ constant (literal or in a comment) reduced to physical.
std::string to_phys(const std::string& code) {
static const std::regex re("0x([0-9A-F]{8})");
std::string out;
auto last = code.cbegin();
for (auto it = std::sregex_iterator(code.begin(), code.end(), re);
it != std::sregex_iterator(); ++it) {
out.append(last, code.cbegin() + it->position());
const uint32_t v = static_cast<uint32_t>(std::stoul((*it)[1].str(), nullptr, 16));
out += "0x" + hex(v & 0x1FFFFFFFu);
last = code.cbegin() + it->position() + it->length();
}
out.append(last, code.cend());
return out;
}
struct Site {
const char* id;
const char* pattern;
bool routed; // true: carries the code segment; false: compile identity
};
const Site kSites[] = {
{"link", R"(= 0x([0-9A-F]{8})u; /\* (?:jal link|jalr link|branch-and-link))", true},
{"cps-jal-note", R"(/\* CPS jal -> 0x([0-9A-F]{8}) \*/)", true},
{"delay-slot-link", R"(= 0x([0-9A-F]{8}); cpu->pc = _jt_)", true},
{"fetch-tag", R"(psx_icache_fetch\(cpu, 0x([0-9A-F]{8})u\))", true},
{"irq-resume", R"(psx_check_interrupts_at\(cpu, 0x([0-9A-F]{8})u\))", true},
{"exit-pc", R"(cpu->pc = 0x([0-9A-F]{8})u;)", true},
{"continuation-key", R"(case 0x([0-9A-F]{8})u:)", true},
{"store-pc", R"(g_debug_last_store_pc = 0x([0-9A-F]{8})u;)", true},
{"ri-epc", R"(cpu->cop0\[14\] = 0x([0-9A-F]{8})u;)", true},
{"slice-pc", R"(psx_slice_block\(cpu, 0x([0-9A-F]{8})u)", true},
{"call-by-address", R"(call_by_address\(cpu, 0x([0-9A-F]{8})u\))", true},
{"call-contract", R"(psx_call_contract\(cpu, 0x([0-9A-F]{8})u)", true},
{"stale-guard", R"(psx_game_text_native_ok\(0x([0-9A-F]{8})u\))", true},
{"vsync-hle", R"(psx_vsync_query_hle_enter\(cpu, 0x([0-9A-F]{8})u,)", true},
{"alias-entry", R"(psx_alias_body_[0-9A-F]{8}\(cpu, 0x([0-9A-F]{8})u\))", true},
{"unknown-dispatch", R"(psx_unknown_dispatch\(cpu, 0x([0-9A-F]{8})u,)", true},
{"cosim", R"(cosim_(?:block|instr)\(0x([0-9A-F]{8})u\))", true},
{"func-name", R"(func_([0-9A-F]{8}))", false},
{"block-label", R"(block_([0-9A-F]{8}))", false},
{"call-entry-hook", R"(debug_server_log_call_entry\(0x([0-9A-F]{8})u\))", false},
{"cyc-observe", R"(debug_server_cyc_observe\(0x([0-9A-F]{8})u\))", false},
{"bad-entry-owner", R"(psx_native_bad_entry\(cpu, 0x([0-9A-F]{8})u)", false},
};
// Emitted text that must be present, per mode, so a route cannot vanish
// unnoticed: each names one emission site.
struct Marker {
const char* text;
bool cps, overlay, legacy;
};
const Marker kMarkers[] = {
{"/* CPS taken: split */", true, true, false},
{"/* CPS not taken: split */", true, true, false},
{"/* taken: split piece */", false, false, true},
{"/* not taken: split piece */", false, false, true},
{"/* taken: split (mid-func) */", false, false, true},
{"/* not taken: split (mid-func) */", false, false, true},
{"psx_ws_backdrop_x(", true, true, true},
{"psx_game_option_store(", true, true, true},
{"/* CPS j: split */", true, true, false},
{"/* external jal */", false, false, true},
{"/* jal cont: split piece */", false, false, true},
{"/* jalr cont: split piece */", false, false, true},
{"/* jal cont: split */", false, false, true},
{"/* jalr cont: split */", false, false, true},
{"/* j to split piece */", false, false, true},
{"/* j to split (mid-func) */", false, false, true},
{"/* fallthrough to split piece */", true, true, true},
{"/* fallthrough to next function */", true, true, true},
{"/* CPS fallthrough past unit edge */", true, true, false},
{"/* image-edge fallthrough: tail-transfer */", true, true, true},
{"/* alias entry into host", true, true, true},
{"switch (entry) {", true, true, true},
{"/* jalr */", true, true, true},
{"/* branch-and-link before delay slot */", true, true, true},
};
void check_game_emitter(Mode mode) {
const std::string m = mode_name(mode);
const std::string def = generate(KSEG0, -1, mode);
check(def == generate(KSEG0, KSEG0, mode),
m + ": an explicit KSEG0 code segment changed the output");
// A KSEG1 image keeps its own segment by default (the pre-B behaviour).
const std::string k1_image = generate(KSEG1, -1, mode);
check(k1_image == generate(KSEG1, KSEG1, mode),
m + ": the default code segment does not follow the image");
for (const Marker& mk : kMarkers) {
const bool want = mode == Mode::Cps ? mk.cps
: mode == Mode::CpsOverlay ? mk.overlay : mk.legacy;
if (want)
check(def.find(mk.text) != std::string::npos,
m + ": the program no longer reaches \"" + mk.text + "\"");
}
for (uint32_t seg : {KUSEG, KSEG1}) {
const std::string sn = seg == KUSEG ? "KUSEG" : "KSEG1";
const std::string code = generate(KSEG0, seg, mode);
// KSEG1 adds per-instruction fetches (checked below); KUSEG is
// cached like KSEG0, so only constant segments may differ.
if (seg == KUSEG)
check(to_phys(code) == to_phys(def),
m + "/" + sn + ": output differs from KSEG0 beyond constant segments");
std::set<std::string> seen;
for (const Site& s : kSites) {
const auto vals = constants(code, s.pattern);
if (!vals.empty()) seen.insert(s.id);
for (uint32_t v : vals) {
// Continuation keys also include jump-table case values in
// general; this program has none, so every key is a PC.
const uint32_t want = s.routed ? seg : KSEG0;
if (s.routed && v == 0) continue;
check((v & 0xE0000000u) == want,
m + "/" + sn + ": " + s.id + " 0x" + hex(v) + " is not in " +
(s.routed ? sn : std::string("the compile segment")));
}
}
// Every class this mode emits must actually have been seen.
std::vector<std::string> want_seen = {
"link", "delay-slot-link", "fetch-tag", "irq-resume", "exit-pc",
"continuation-key", "store-pc", "ri-epc", "slice-pc", "stale-guard",
"alias-entry", "unknown-dispatch", "func-name", "block-label",
"call-entry-hook", "cyc-observe"};
if (mode != Mode::Legacy) want_seen.push_back("cps-jal-note");
if (mode == Mode::Legacy) {
want_seen.push_back("call-by-address");
want_seen.push_back("call-contract");
}
if (mode == Mode::CpsOverlay) want_seen.push_back("bad-entry-owner");
// The VSync-query hook's hand-timed body charges cached fetches only,
// so an uncached code segment runs the compiled instructions instead.
if (seg == KUSEG) want_seen.push_back("vsync-hle");
else check(seen.count("vsync-hle") == 0,
m + "/" + sn + ": the VSync-query hook is emitted for uncached code");
for (const auto& id : want_seen)
check(seen.count(id) == 1, m + "/" + sn + ": no " + id + " site emitted");
}
check(def.find("psx_vsync_query_hle_enter(cpu, 0x" + hex(KSEG0 | (kPhys + kVsyncFunc)) +
"u,") != std::string::npos,
m + ": the KSEG0 VSync-query hook is missing or moved");
// KSEG1 code segment at KSEG0 compile addresses charges exactly what the
// same bytes compiled at KSEG1 addresses charge: a fetch per instruction.
const auto k1_tags = constants(generate(KSEG0, KSEG1, mode),
R"(psx_icache_fetch\(cpu, 0x([0-9A-F]{8})u\))");
const auto k1_image_tags = constants(k1_image,
R"(psx_icache_fetch\(cpu, 0x([0-9A-F]{8})u\))");
check(k1_tags == k1_image_tags,
m + ": a KSEG1 code segment does not charge like a KSEG1 image");
std::set<uint32_t> charged(k1_tags.begin(), k1_tags.end());
for (uint32_t i = 0; i < kCount; ++i) {
if (kNoFetch.count(i * 4u)) continue;
check(charged.count(KSEG1 | (kPhys + i * 4u)) == 1,
m + ": KSEG1 instruction +" + hex(i * 4u) + " has no fetch of its own");
}
const auto k0_tags = constants(def, R"(psx_icache_fetch\(cpu, 0x([0-9A-F]{8})u\))");
check(k0_tags.size() < k1_tags.size(),
m + ": the cached KSEG0 body lost the line-leader rule");
}
// Dispatch rows: {key, resume, range index, range count, func_ owner}.
struct Row {
uint32_t addr, resume, owner;
};
std::vector<Row> dispatch_rows(const std::string& src) {
std::vector<Row> rows;
static const std::regex re(
R"(\{0x([0-9A-F]{8})u, 0x([0-9A-F]{8})u, \d+u, \d+u, func_([0-9A-F]{8})\})");
for (auto it = std::sregex_iterator(src.begin(), src.end(), re);
it != std::sregex_iterator(); ++it)
rows.push_back({static_cast<uint32_t>(std::stoul((*it)[1].str(), nullptr, 16)),
static_cast<uint32_t>(std::stoul((*it)[2].str(), nullptr, 16)),
static_cast<uint32_t>(std::stoul((*it)[3].str(), nullptr, 16))});
return rows;
}
void check_dispatch_rows(Mode mode) {
const std::string m = mode_name(mode) + std::string("/dispatch");
const std::string def = generate_all(KSEG0, -1, mode).dispatch;
check(def == generate_all(KSEG0, KSEG0, mode).dispatch,
m + ": an explicit KSEG0 code segment changed the dispatch source");
const auto def_rows = dispatch_rows(def);
check(def_rows.size() >= 10, m + ": too few dispatch rows parsed");
// The lookup indexes the physical word, then requires the exact PC.
check(def.find("if (!index || k_psx_game_dispatch[index - 1u].addr != addr) return 0;") !=
std::string::npos,
m + ": the lookup does not require the exact PC");
for (uint32_t seg : {KUSEG, KSEG1}) {
const std::string sn = seg == KUSEG ? "KUSEG" : "KSEG1";
const std::string src = generate_all(KSEG0, seg, mode).dispatch;
const auto rows = dispatch_rows(src);
check(rows.size() == def_rows.size(), m + "/" + sn + ": row count changed");
bool continuation = false;
for (size_t i = 0; i < rows.size() && i < def_rows.size(); ++i) {
const Row& r = rows[i];
check((r.addr & 0xE0000000u) == seg,
m + "/" + sn + ": row key 0x" + hex(r.addr) + " is not in " + sn);
check(r.resume == 0 || (r.resume & 0xE0000000u) == seg,
m + "/" + sn + ": resume PC 0x" + hex(r.resume) + " is not in " + sn);
check((r.owner & 0xE0000000u) == KSEG0 && r.owner == def_rows[i].owner,
m + "/" + sn + ": func_" + hex(r.owner) + " is not the compile identity");
check((r.addr & 0x1FFFFFFFu) == (def_rows[i].addr & 0x1FFFFFFFu),
m + "/" + sn + ": row 0x" + hex(r.addr) + " moved");
continuation |= r.resume != 0;
}
if (mode != Mode::Legacy)
check(continuation, m + "/" + sn + ": no continuation row (resume PC) emitted");
if (seg == KUSEG)
check(to_phys(src) == to_phys(def),
m + "/" + sn + ": dispatch differs from KSEG0 beyond constant segments");
}
}
// One compile has one code segment, so two rows on one physical word within
// it are the same PC: a build error, never a row that silently shadows the
// other. Segment variants (§5.4) are separate compiles, and their rows share
// the home rows' words: adjacent, in PC order, looked up by exact PC. Two
// compiles in one segment would name the same PCs, which is an error again.
void check_dispatch_duplicate_word() {
const uint32_t base = KSEG0 | kPhys;
PSXRecomp::PS1Executable exe{};
exe.header.load_address = base;
exe.header.initial_pc = base;
exe.header.file_size = kCount * 4u;
for (uint32_t w : kProgram) append_word(exe.code_data, w);
PSXRecomp::CodeGenConfig config{};
PSXRecomp::CodeGenerator generator(exe, config);
std::string out, error;
check(PSXRecomp::emit_game_dispatch(generator, exe, {base, base + 0x20u}, "", out, error) &&
error.empty() && !out.empty(),
"dispatch: rows on distinct words must emit");
for (uint32_t alias : {KUSEG | kPhys, KSEG1 | (kPhys + 0x20u)}) {
out.clear();
error.clear();
check(!PSXRecomp::emit_game_dispatch(generator, exe, {base, base + 0x20u, alias}, "",
out, error),
"dispatch: two rows on one physical word (0x" + hex(alias) + ") must be an error");
check(error.find("share a physical word") != std::string::npos,
"dispatch: the duplicate-word error must say why, got: " + error);
}
PSXRecomp::PS1Executable kseg1 = exe;
kseg1.header.load_address = KSEG1 | kPhys;
kseg1.header.initial_pc = KSEG1 | kPhys;
PSXRecomp::CodeGenerator variant(kseg1, config);
const std::vector<PSXRecomp::GameDispatchUnit> units{
{&generator, {base, base + 0x20u}}, {&variant, {KSEG1 | kPhys}}};
out.clear();
error.clear();
check(PSXRecomp::emit_game_dispatch(units, exe, "", out, error) && error.empty(),
"dispatch: a variant compile's row may share its home row's word: " + error);
const auto home_row = out.find("{0x" + hex(base) + "u, 0x00000000u");
const auto variant_row = out.find("{0x" + hex(KSEG1 | kPhys) + "u, 0x00000000u");
const auto variant_line =
variant_row == std::string::npos
? std::string()
: out.substr(variant_row, out.find('\n', variant_row) - variant_row);
check(home_row != std::string::npos && variant_row != std::string::npos &&
home_row < variant_row &&
variant_line.find("func_" + hex(KSEG1 | kPhys) + "}") != std::string::npos,
"dispatch: the variant row follows the home row on their word and names its "
"body: " + variant_line);
check(out.find("k_psx_game_dispatch[row].addr == addr") != std::string::npos,
"dispatch: a shared word is searched for the exact PC");
const std::vector<PSXRecomp::GameDispatchUnit> twice{
{&generator, {base}}, {&variant, {KSEG1 | kPhys}}, {&variant, {KSEG1 | kPhys}}};
out.clear();
error.clear();
check(!PSXRecomp::emit_game_dispatch(twice, exe, "", out, error) &&
error.find("are the same PC 0x" + hex(KSEG1 | kPhys)) != std::string::npos,
"dispatch: two compiles in one segment name the same PC, an error: " + error);
}
void check_strict_translator(bool strict_cps) {
// SCPH-1001 Kernel Part 2 runs at 0x500 from ROM 0xBFC10000: the store at
// ROM 0xBFC10A00 executes at 0x00000F00 (memory.c's RAM-0 filter key).
const uint32_t rom = 0xBFC10A00u, rt_pc = 0x00000F00u;
struct Case {
uint32_t raw;
const char* want; // substring that must carry the runtime PC
};
// The syscall form is latched from PSX_CPS when the process starts.
const char* syscall_want = strict_cps
? "cpu->pc = 0x00000F00u; if (psx_syscall(cpu, cpu->gpr[2])) return;"
: "cpu->pc = 0x00000F00u; psx_syscall(cpu, cpu->gpr[2]); return;";
const Case cases[] = {
{0xAC880000u, "g_debug_last_store_pc = 0x00000F00u"}, // sw
{0xA4880000u, "psx_unaligned_access(cpu, psx_addr, 0x00000F00u)"}, // sh
{0xA0880000u, "g_debug_last_store_pc = 0x00000F00u"}, // sb
{0xA8880000u, "g_debug_last_store_pc = 0x00000F00u"}, // swl
{0xB8880000u, "g_debug_last_store_pc = 0x00000F00u"}, // swr
{0xE8880000u, "g_debug_last_store_pc = 0x00000F00u"}, // swc2
{0x8C880000u, "psx_unaligned_access(cpu, psx_addr, 0x00000F00u)"}, // lw
{0x84880000u, "psx_unaligned_access(cpu, psx_addr, 0x00000F00u)"}, // lh
{0x94880000u, "psx_unaligned_access(cpu, psx_addr, 0x00000F00u)"}, // lhu
{0x0000000Cu, syscall_want}, // syscall
{0x0000000Du, "0x00000F00u); return;"}, // break
};
for (const Case& c : cases) {
const auto d = PSXRecomp::MipsDecoder::decode(c.raw, rom);
const auto tr = PSXRecompV4::StrictTranslator::translate(d, rt_pc);
check(tr.supported, "translator: 0x" + hex(c.raw) + " unsupported");
check(tr.c_code.find(c.want) != std::string::npos,
"translator: 0x" + hex(c.raw) + " lacks \"" + c.want + "\": " + tr.c_code);
check(tr.c_code.find("0xBFC10A00u") == std::string::npos,
"translator: 0x" + hex(c.raw) + " still bakes the ROM address: " + tr.c_code);
// The one-argument form is the in-place case (runtime == ROM).
const auto same = PSXRecompV4::StrictTranslator::translate(d);
check(same.c_code == PSXRecompV4::StrictTranslator::translate(d, rom).c_code,
"translator: translate(d) != translate(d, d.address) for 0x" + hex(c.raw));
// A deferred load's temporary is an identity key: it keeps the ROM
// address, while its unaligned-access PC is the runtime PC.
if (!tr.c_code_deferred.empty())
check(tr.c_code_deferred.find("psx_ldd_BFC10A00") != std::string::npos &&
tr.c_code_deferred.find("psx_unaligned_access(cpu, psx_addr, 0x00000F00u)") !=
std::string::npos &&
tr.c_code_deferred.find("0xBFC10A00u") == std::string::npos,
"translator: deferred 0x" + hex(c.raw) +
" must name psx_ldd_<ROM> and fault at the runtime PC: " +
tr.c_code_deferred);
}
int deferred = 0;
for (uint32_t raw : {0x8C880000u, 0x84880000u, 0x94880000u})
deferred += !PSXRecompV4::StrictTranslator::translate(
PSXRecomp::MipsDecoder::decode(raw, rom), rt_pc)
.c_code_deferred.empty();
check(deferred == 3, "translator: lw/lh/lhu no longer emit a deferred form");
}
} // namespace
int main() {
// strict_translator.cpp latches PSX_CPS at static initialisation, before
// check_game_emitter() starts setting it per mode.
const char* cps_env = std::getenv("PSX_CPS");
const bool strict_cps = cps_env == nullptr || cps_env[0] != '0';
const char* mode = std::getenv("PSX_CODEGEN_CYCLE_PER_INSN");
if (mode != nullptr && mode[0] == '0') {
std::puts("SKIP: PSX_CODEGEN_CYCLE_PER_INSN=0 emits no fetch charges");
return 0;
}
check_game_emitter(Mode::Cps);
check_game_emitter(Mode::CpsOverlay);
check_game_emitter(Mode::Legacy);
check_dispatch_rows(Mode::Cps);
check_dispatch_rows(Mode::Legacy);
check_dispatch_duplicate_word();
check_strict_translator(strict_cps);
if (failures != 0) {
std::fprintf(stderr, "emitter_runtime_pc_test: %d failure(s)\n", failures);
return 1;
}
std::puts("PASS: every baked guest PC follows the code segment; identity keys and "
"KSEG0 output unchanged");
return 0;
}