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>
This commit is contained in:
Shokunin
2026-09-29 22:42:57 -07:00
parent 085af5d304
commit 6a76eb452b
15 changed files with 1208 additions and 121 deletions
+15 -1
View File
@@ -2449,6 +2449,13 @@ std::string CodeGenerator::translate_basic_block(
std::vector<std::pair<uint32_t,uint32_t>> targets; // {runtime_addr, rom_addr}
for (const auto& [runtime_addr, rom_addr] :
exact_table.targets) {
// A case value is the PC the jr reaches. A value
// in another segment than this body's reaches the
// same bytes as another code identity (§5.4): it
// is left to the default tail-transfer, which
// dispatches it with its full PC, instead of
// continuing here with this segment's PCs.
if (foreign_segment_case(runtime_addr, rom_addr)) continue;
if (seen.insert(rom_addr).second &&
(cfg.blocks.count(rom_addr) || extra_labels_.count(rom_addr))) {
targets.push_back({runtime_addr, rom_addr});
@@ -3042,6 +3049,9 @@ void CodeGenerator::scan_jr_tables(
if (tb==0 || tc==0) continue;
for (const auto& target_pair : exact_table.targets) {
uint32_t t = target_pair.second;
// Not a local edge: dispatched with its own segment (see the
// switch emission in translate_basic_block).
if (foreign_segment_case(target_pair.first, t)) continue;
if (t >= cfg.function_start && t < cfg.function_end) {
out_edges[baddr].push_back(t);
if (!cfg.blocks.count(t))
@@ -3537,7 +3547,8 @@ void CodeGenerator::emit_unaligned_helpers(std::ostream& ss, bool as_inline) con
ss << "\n";
}
std::string CodeGenerator::build_shared_decls_header(const std::vector<GeneratedFunction>& gen_funcs) const {
std::string CodeGenerator::build_shared_decls_header(const std::vector<GeneratedFunction>& gen_funcs,
const std::vector<std::string>& extra_decls) const {
std::stringstream h;
h << "/* Generated by PSXRecomp - shared declarations for split full.c shards. DO NOT EDIT. */\n";
h << "#pragma once\n";
@@ -3548,6 +3559,7 @@ std::string CodeGenerator::build_shared_decls_header(const std::vector<Generated
h << "/* Forward declarations for all recompiled functions */\n";
for (const auto& gf : gen_funcs) h << gf.signature << ";\n";
for (const auto& decl : alias_body_decls_) h << decl << ";\n";
for (const auto& decl : extra_decls) h << decl << ";\n";
h << "\n";
}
return h.str();
@@ -3801,6 +3813,8 @@ std::string CodeGenerator::generate_file(
// dispatch generator consumes this so synthesized fallthrough functions
// receive the same byte-precise native-validity ranges as ordinary entries.
last_ranges_manifest_ = generate_ranges_manifest(functions_mut, cfgs_mut);
last_functions_ = std::move(functions_mut);
last_cfgs_ = std::move(cfgs_mut);
return ss.str();
}