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
+30
View File
@@ -110,6 +110,7 @@ set(PSXRECOMP_GAME_SOURCES
src/config_loader.cpp
src/recompiler_patch.cpp
src/game_dispatch_emitter.cpp
src/segment_variants.cpp
src/main_psx.cpp
)
@@ -626,6 +627,35 @@ if(BUILD_TESTING)
set_tests_properties(emitter_runtime_pc_test_legacy PROPERTIES
ENVIRONMENT "PSX_CPS=0")
# Segment variants (docs/SEGMENT_AWARE_CODE.md §5.4): the planner's
# direct-edge closure per segment, the segment view and the variant's
# codegen config.
add_executable(segment_variants_test
tests/segment_variants_test.cpp
src/segment_variants.cpp
src/ps1_exe_parser.cpp
src/mips_decoder.cpp
src/control_flow.cpp
src/function_analysis.cpp
src/code_generator.cpp
src/pgxp_hook_emitter.cpp
src/annotations.cpp
src/bios_address_model.cpp
src/config_loader.cpp
src/recompiler_patch.cpp
)
target_include_directories(segment_variants_test PRIVATE
include
src
lib/rabbitizer/include
lib/rabbitizer/cplusplus/include
lib/fmt/include
lib/toml11
lib/ELFIO
)
target_link_libraries(segment_variants_test PRIVATE fmt rabbitizer)
add_test(NAME segment_variants_test COMMAND segment_variants_test)
add_executable(recompiler_patch_cxx17_header_test
tests/recompiler_patch_cxx17_header_test.cpp
)
+25 -1
View File
@@ -308,7 +308,14 @@ public:
// and forward declarations for every recompiled function plus every
// overlapping-alias shared body. Byte-for-byte the same declaration set
// the monolith emits inline, just hoisted into a header.
std::string build_shared_decls_header(const std::vector<GeneratedFunction>& gen_funcs) const;
// `extra_decls` adds the alias-body prototypes of other generators whose
// functions share these shards (segment variants, §5.4).
std::string build_shared_decls_header(const std::vector<GeneratedFunction>& gen_funcs,
const std::vector<std::string>& extra_decls = {}) const;
// Prototypes of the psx_alias_body_XXXXXXXX() bodies the most recent
// generate_all_functions() emitted.
const std::vector<std::string>& alias_body_decls() const { return alias_body_decls_; }
// Generate the per-function code-range manifest consumed by the overlay
// loader's per-entry validity hash (design §8). For each function, emits its
@@ -325,6 +332,12 @@ public:
// generate_file() call, including functions synthesized by its split pass.
const std::string& last_ranges_manifest() const { return last_ranges_manifest_; }
// The final function set and CFGs the most recent generate_file() call
// emitted, after its split pass: what a segment variant's direct-edge
// closure is planned over (docs/SEGMENT_AWARE_CODE.md §5.4).
const std::vector<Function>& last_functions() const { return last_functions_; }
const std::map<uint32_t, ControlFlowGraph>& last_cfgs() const { return last_cfgs_; }
// Set known functions for this compilation unit (for linking)
void set_known_functions(const std::set<uint32_t>& functions) {
known_functions_ = functions;
@@ -352,6 +365,15 @@ public:
uint32_t runtime_pc(uint32_t compile_addr) const {
return code_seg_ | (compile_addr & kPhysMask);
}
// A jump-table case whose value (the PC the jr reaches) names the same
// bytes as `image_addr` in another segment than this body's. Such a case
// is not a local edge: the body bakes its own segment's PCs, so the value
// is dispatched with its full PC instead (§5.4). Never true when the table
// holds this body's own segment, which is every title's home compile.
bool foreign_segment_case(uint32_t case_value, uint32_t image_addr) const {
return (case_value & kPhysMask) == (image_addr & kPhysMask) &&
case_value != runtime_pc(image_addr);
}
private:
const PS1Executable& exe_;
@@ -359,6 +381,8 @@ private:
uint32_t code_seg_; // set from the image in the constructor; see set_code_segment()
std::set<uint32_t> known_functions_; // Addresses of functions in this compilation unit
std::string last_ranges_manifest_;
std::vector<Function> last_functions_;
std::map<uint32_t, ControlFlowGraph> last_cfgs_;
std::set<uint32_t> extra_labels_; // Mid-block addresses that need inline labels (jump table targets)
const AnnotationTable* annotations_ = nullptr;
+10
View File
@@ -18,6 +18,16 @@ 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 {
+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();
}
+86 -39
View File
@@ -16,6 +16,20 @@ bool emit_game_dispatch(const CodeGenerator& codegen,
const std::string& ranges_manifest,
std::string& out,
std::string& error) {
return emit_game_dispatch(std::vector<GameDispatchUnit>{{&codegen, dispatch_addrs}},
exe, ranges_manifest, out, error);
}
bool emit_game_dispatch(const std::vector<GameDispatchUnit>& units,
const PS1Executable& exe,
const std::string& ranges_manifest,
std::string& out,
std::string& error) {
if (units.empty() || !units.front().codegen) {
error = "emit_game_dispatch: no compile to dispatch";
return false;
}
const bool cps = units.front().codegen->cps_enabled();
std::ostringstream ds;
ds << "/* Generated by PSXRecomp - dynamic dispatch table */\n";
ds << "#include \"psx_runtime.h\"\n\n";
@@ -25,8 +39,10 @@ bool emit_game_dispatch(const CodeGenerator& codegen,
// Forward declarations
ds << "/* Forward declarations for all recompiled functions */\n";
for (uint32_t addr : dispatch_addrs) {
ds << fmt::format("extern void func_{:08X}(CPUState* cpu);\n", addr);
for (const auto& unit : units) {
for (uint32_t addr : unit.dispatch_addrs) {
ds << fmt::format("extern void func_{:08X}(CPUState* cpu);\n", addr);
}
}
ds << "\n";
@@ -46,45 +62,57 @@ bool emit_game_dispatch(const CodeGenerator& codegen,
// GCC lowers X4's roughly 60K cases to a multi-megabyte linear compare
// chain. Binary search stays O(log N) at every optimizer level and
// avoids emitting the same case set twice.
// `key` and `resume` are the PCs the guest executes, routed through the
// unit's runtime_pc() (docs/SEGMENT_AWARE_CODE.md §5.2); `addr` and
// `owner` are compile identities (func_ names, .ranges owners). resume 0
// means "entry at the prologue", not a PC.
struct DispatchRecord {
uint32_t addr;
uint32_t key;
uint32_t resume;
uint32_t owner;
uint32_t range_index;
uint32_t range_count;
};
std::vector<DispatchRecord> records;
records.reserve(dispatch_addrs.size() +
(codegen.cps_enabled() ? codegen.cps_continuations().size() : 0));
for (uint32_t addr : dispatch_addrs)
records.push_back({addr, 0, addr, 0, 0});
if (codegen.cps_enabled()) {
const auto& conts = codegen.cps_continuations();
for (const auto& [cont, owner] : conts) {
if (dispatch_addrs.count(cont)) continue;
// A continuation owned by a fail-closed stub is no more
// executable than the stub entry itself.
if (!dispatch_addrs.count(owner)) continue;
records.push_back({cont, cont, owner, 0, 0});
for (const auto& unit : units) {
const CodeGenerator& codegen = *unit.codegen;
for (uint32_t addr : unit.dispatch_addrs)
records.push_back({addr, codegen.runtime_pc(addr), 0, addr, 0, 0});
if (cps) {
const auto& conts = codegen.cps_continuations();
for (const auto& [cont, owner] : conts) {
if (unit.dispatch_addrs.count(cont)) continue;
// A continuation owned by a fail-closed stub is no more
// executable than the stub entry itself.
if (!unit.dispatch_addrs.count(owner)) continue;
records.push_back({cont, codegen.runtime_pc(cont), codegen.runtime_pc(cont),
owner, 0, 0});
}
}
}
// Sort by PHYSICAL address: the lookup indexes (or binary-searches) the
// physical word and then compares the full PC. One compile has one code
// segment, so the physical order is the raw order and every physical word
// has at most one row; a second row for a word would be a variant of
// another segment (§5.4), which this table does not hold yet.
// Sort by PHYSICAL word, then by the full PC: the lookup indexes (or
// binary-searches) the physical word and then requires the exact PC. A
// word has one row per segment compiled for it, adjacent: the home
// compile's and those of its segment variants (§5.4). Within one compile
// the code segment is fixed, so two rows on one word are the same PC.
std::sort(records.begin(), records.end(),
[](const DispatchRecord& a, const DispatchRecord& b) {
return (a.addr & 0x1FFFFFFFu) < (b.addr & 0x1FFFFFFFu);
const uint32_t pa = a.key & 0x1FFFFFFFu, pb = b.key & 0x1FFFFFFFu;
return pa != pb ? pa < pb : a.key < b.key;
});
bool shared_words = false;
for (size_t i = 1; i < records.size(); ++i) {
if ((records[i].addr & 0x1FFFFFFFu) == (records[i - 1].addr & 0x1FFFFFFFu)) {
if (records[i].key == records[i - 1].key) {
error = fmt::format(
"dispatch rows 0x{:08X} and 0x{:08X} share a physical word; one "
"compile has one code segment (docs/SEGMENT_AWARE_CODE.md §5.5)",
records[i - 1].addr, records[i].addr);
"dispatch rows 0x{:08X} and 0x{:08X} are the same PC 0x{:08X}: they "
"share a physical word in one code segment "
"(docs/SEGMENT_AWARE_CODE.md §5.5)",
records[i - 1].addr, records[i].addr, records[i].key);
return false;
}
if ((records[i].key & 0x1FFFFFFFu) == (records[i - 1].key & 0x1FFFFFFFu))
shared_words = true;
}
// Attach the exact CFG instruction ranges from the manifest to every
@@ -159,15 +187,10 @@ bool emit_game_dispatch(const CodeGenerator& codegen,
ds << " PsxGameDispatchFn fn;\n";
ds << "} PsxGameDispatchEntry;\n\n";
ds << "static const PsxGameDispatchEntry k_psx_game_dispatch[] = {\n";
// Row keys and resume PCs are the PCs the guest executes, so they go
// through the emitter's runtime_pc() (docs/SEGMENT_AWARE_CODE.md
// §5.2); func_ names stay the compile identity. resume 0 means "entry
// at the prologue", not a PC.
for (const auto& rec : records) {
ds << fmt::format(" {{0x{:08X}u, 0x{:08X}u, {}u, {}u, func_{:08X}}},\n",
codegen.runtime_pc(rec.addr),
rec.resume ? codegen.runtime_pc(rec.resume) : 0u,
rec.range_index, rec.range_count, rec.owner);
rec.key, rec.resume, rec.range_index, rec.range_count,
rec.owner);
}
ds << "};\n";
ds << fmt::format("#define PSX_GAME_DISPATCH_COUNT {}u\n\n", records.size());
@@ -177,16 +200,17 @@ bool emit_game_dispatch(const CodeGenerator& codegen,
// callers below still check live instruction ranges on every dispatch.
// Bound the indexed span to one PS1 RAM image; wider tables retain the
// binary search. No mutable cache or flag.
const uint32_t lookup_lo = records.empty() ? 0u : (records.front().addr & 0x1FFFFFFFu);
const uint32_t lookup_hi = records.empty() ? 0u : (records.back().addr & 0x1FFFFFFFu);
const uint32_t lookup_lo = records.empty() ? 0u : (records.front().key & 0x1FFFFFFFu);
const uint32_t lookup_hi = records.empty() ? 0u : (records.back().key & 0x1FFFFFFFu);
bool indexed_lookup = !records.empty() && lookup_hi - lookup_lo < 0x200000u;
for (size_t i = 0; i < records.size(); ++i) {
if (records[i].addr & 3u) indexed_lookup = false;
if (records[i].key & 3u) indexed_lookup = false;
}
if (indexed_lookup) {
// Each word indexes its FIRST row; a word's other segments follow it.
std::vector<uint32_t> index((lookup_hi - lookup_lo) / 4u + 1u, 0u);
for (size_t i = 0; i < records.size(); ++i)
index[((records[i].addr & 0x1FFFFFFFu) - lookup_lo) / 4u] = (uint32_t)i + 1u;
for (size_t i = records.size(); i-- > 0;)
index[((records[i].key & 0x1FFFFFFFu) - lookup_lo) / 4u] = (uint32_t)i + 1u;
ds << "/* Immutable physical-word index; zero denotes a dispatch miss. */\n";
ds << "static const " << (records.size() <= 65535u ? "uint16_t" : "uint32_t")
<< " k_psx_game_dispatch_index[] = {\n";
@@ -206,7 +230,30 @@ bool emit_game_dispatch(const CodeGenerator& codegen,
ds << " * interprets it and records it, and never runs this body for it. */\n";
ds << "static const PsxGameDispatchEntry* psx_game_find_entry(uint32_t addr) {\n";
ds << " const uint32_t want = addr & 0x1FFFFFFFu;\n";
if (indexed_lookup) {
if (shared_words) {
// Segment variants (§5.4): a word may have a row per compiled
// segment, adjacent and in PC order. Find the word's first row, then
// the exact PC among its rows.
if (indexed_lookup) {
ds << fmt::format(" const uint32_t offset = want - 0x{:08X}u;\n", lookup_lo);
ds << fmt::format(" if ((want & 3u) || offset > 0x{:X}u) return 0;\n", lookup_hi - lookup_lo);
ds << " const uint32_t index = k_psx_game_dispatch_index[offset >> 2];\n";
ds << " if (!index) return 0;\n";
ds << " uint32_t row = index - 1u;\n";
} else {
ds << " uint32_t lo = 0, hi = PSX_GAME_DISPATCH_COUNT;\n";
ds << " while (lo < hi) { /* first row whose word is >= want */\n";
ds << " uint32_t mid = lo + (hi - lo) / 2;\n";
ds << " if ((k_psx_game_dispatch[mid].addr & 0x1FFFFFFFu) < want) lo = mid + 1;\n";
ds << " else hi = mid;\n";
ds << " }\n";
ds << " uint32_t row = lo;\n";
}
ds << " for (; row < PSX_GAME_DISPATCH_COUNT &&\n";
ds << " (k_psx_game_dispatch[row].addr & 0x1FFFFFFFu) == want; ++row)\n";
ds << " if (k_psx_game_dispatch[row].addr == addr) return &k_psx_game_dispatch[row];\n";
ds << " return 0;\n";
} else if (indexed_lookup) {
ds << fmt::format(" const uint32_t offset = want - 0x{:08X}u;\n", lookup_lo);
ds << fmt::format(" if ((want & 3u) || offset > 0x{:X}u) return 0;\n", lookup_hi - lookup_lo);
ds << " const uint32_t index = k_psx_game_dispatch_index[offset >> 2];\n";
@@ -248,7 +295,7 @@ bool emit_game_dispatch(const CodeGenerator& codegen,
ds << " if (entry->range_count == 0 || !dirty_ram_text_native_ok_ranges_from(\n";
ds << " &k_psx_game_code_ranges[entry->range_index].lo, entry->range_count, addr)) return 0;\n";
ds << " psx_check_interrupts_dispatch_entry(cpu, addr);\n";
if (codegen.cps_enabled())
if (cps)
ds << " cpu->pc = entry->resume_pc;\n";
ds << " /* load_accel: cycle-faithful VSync(-1) query (config-gated). */\n";
ds << " {\n";
@@ -274,7 +321,7 @@ bool emit_game_dispatch(const CodeGenerator& codegen,
ds << " return psx_game_find_entry(addr) != 0;\n";
ds << "}\n";
if (codegen.cps_enabled()) {
if (cps) {
// RECURSION_BUG.md §25 — mark CPS mode at startup so runtime code that
// routes CPS continuations (overlay_loader.c) sees the contract.
// Static ctor: no clash with the BIOS dispatch's marker.
+26 -8
View File
@@ -17,21 +17,39 @@
#include <cstdint>
#include <set>
#include <string>
#include <vector>
#include "code_generator.h"
#include "ps1_exe_parser.h"
namespace PSXRecomp {
// Emits the dispatch source for `dispatch_addrs` (compile addresses of the
// dispatchable func_ entries) and the generator's CPS continuations. Row keys
// and resume PCs go through codegen.runtime_pc() (§5.2); func_ names keep the
// compile identity. `ranges_manifest` is the emitted .ranges text, whose F/R
// records give each row its exact instruction ranges.
// One compile whose bodies the table dispatches: the home compile, or a
// segment variant (§5.4). `dispatch_addrs` are the compile addresses of its
// dispatchable func_ entries; its CPS continuations come from the generator.
struct GameDispatchUnit {
const CodeGenerator* codegen;
std::set<uint32_t> dispatch_addrs;
};
// Emits the dispatch source for every unit's entries and CPS continuations.
// Row keys and resume PCs go through the unit's runtime_pc() (§5.2); func_
// names keep the compile identity. `ranges_manifest` is the emitted .ranges
// text of all units, whose F/R records give each row its exact instruction
// ranges. Rows are sorted by physical word; one word may carry a row per
// compiled segment (the home body and its variants), and the lookup requires
// the exact PC. The first unit's CPS mode applies to the table.
//
// Returns false with `error` set when two rows would share a physical word:
// one compile has one code segment, so that can only be a caller error until
// per-segment variants (§5.4) share the table.
// Returns false with `error` set when two rows name the same PC. Within one
// compile that means two rows on one physical word: one compile has one code
// segment.
bool emit_game_dispatch(const std::vector<GameDispatchUnit>& units,
const PS1Executable& exe,
const std::string& ranges_manifest,
std::string& out,
std::string& error);
// The single-compile form (no segment variants).
bool emit_game_dispatch(const CodeGenerator& codegen,
const PS1Executable& exe,
const std::set<uint32_t>& dispatch_addrs,
+112 -21
View File
@@ -5,6 +5,7 @@
#include <fstream>
#include <initializer_list>
#include <map>
#include <memory>
#include <sstream>
#include <string>
#include <algorithm>
@@ -17,6 +18,7 @@
#include "control_flow.h"
#include "code_generator.h"
#include "game_dispatch_emitter.h"
#include "segment_variants.h"
#include "annotations.hpp"
#include "config_loader.h"
#include "rabbitizer.hpp"
@@ -773,10 +775,10 @@ static int psxrecomp_game_main(int argc, char** argv) {
* §5.3, §5.4). The bounds are checked by physical address: a seed in the
* image's link segment (0x000100E4 for a KUSEG-linked EXE, 0x800100E4
* for a KSEG0 one) is an entry of the image. A seed for the same bytes in
* another segment asks for a segment variant (§5.4), which is not
* compiled yet: it is listed below and never folded into the home body,
* so that PC stays a loud segment miss at run time. Seeds outside the
* image are counted. */
* another segment asks for a segment variant (§5.4): it is never folded
* into the home body; after the home compile, the home functions it
* reaches through direct edges are compiled again in its segment (see
* plan_segment_variants below). Seeds outside the image are counted. */
std::vector<uint32_t> file_seeds;
std::vector<std::pair<uint32_t, std::string>> variant_seeds; // PC, line kind
size_t outside_seeds = 0;
@@ -942,6 +944,20 @@ static int psxrecomp_game_main(int argc, char** argv) {
if (phys < (seed_lo & PSXRecomp::kPhysMask) ||
phys >= (seed_lo & PSXRecomp::kPhysMask) + (seed_hi - seed_lo)) {
++outside_seeds;
} else if (!PSXRecomp::maps_physical(addr)) {
// Only KUSEG below 0x20000000, KSEG0 and KSEG1 alias the
// image's bytes; a variant in any other segment would
// give dispatch rows to PCs hardware cannot fetch as RAM.
fmt::print(stderr,
"ERROR: seed 0x{:08X} is in segment 0x{:08X}, which does not "
"map physical memory (only KUSEG below 0x20000000, KSEG0 and "
"KSEG1 do), so it names no code of this image and cannot ask "
"for a segment variant "
"(docs/SEGMENT_AWARE_CODE.md §5.4). The image's bytes at that "
"physical address are 0x{:08X} in its link segment.\n",
addr, addr & PSXRecomp::kSegmentMask,
exe->link_segment() | phys);
return 1;
} else if ((addr & PSXRecomp::kSegmentMask) != exe->link_segment()) {
variant_seeds.emplace_back(
addr, interior ? "interior" : trusted_root ? "dispatch_root"
@@ -998,13 +1014,20 @@ static int psxrecomp_game_main(int argc, char** argv) {
: seg == PSXRecomp::kSegKSEG0 ? "KSEG0"
: seg == PSXRecomp::kSegKSEG1 ? "KSEG1" : "an unmapped segment";
};
fmt::print("WARNING: {} seed(s) name this image's bytes in another segment "
"than its link segment {}. Each asks for a segment variant "
"(docs/SEGMENT_AWARE_CODE.md §5.4), which is not compiled "
"yet: that PC runs interpreted and is recorded as a segment "
"miss. A seed for the image's own code is written in {}.\n",
variant_seeds.size(), seg_name(exe->link_segment()),
seg_name(exe->link_segment()));
if (overlay_mode) {
fmt::print("WARNING: {} seed(s) name this image's bytes in another "
"segment than its link segment {}. Overlay compiles have "
"no segment variants yet (docs/SEGMENT_AWARE_CODE.md §5.7): "
"nothing is compiled for them.\n",
variant_seeds.size(), seg_name(exe->link_segment()));
} else {
fmt::print("{} seed(s) name this image's bytes in another segment than "
"its link segment {}. Each asks for a segment variant "
"(docs/SEGMENT_AWARE_CODE.md §5.4), compiled after the home "
"compile. A seed for the image's own code is written in {}.\n",
variant_seeds.size(), seg_name(exe->link_segment()),
seg_name(exe->link_segment()));
}
for (const auto& [addr, kind] : variant_seeds) {
fmt::print(" variant seed 0x{:08X} ({}, {}): home PC would be 0x{:08X}\n",
addr, kind, seg_name(addr & PSXRecomp::kSegmentMask),
@@ -1563,6 +1586,70 @@ static int psxrecomp_game_main(int argc, char** argv) {
}
}
// Segment variants (docs/SEGMENT_AWARE_CODE.md §5.4). Each segment named
// by a variant seed gets the direct-edge closure of the home functions
// its seeds reach, compiled again through a view of the image in that
// segment: its names, rows and every PC it bakes are that segment's, and
// a KSEG1 variant charges a fetch per instruction (§5.6). The bodies join
// the home shards, and their rows join the home dispatch table. Overlay
// compiles have none yet (§5.7).
std::vector<PSXRecomp::GeneratedFunction> all_gen_funcs = codegen.last_gen_funcs();
std::vector<std::string> variant_alias_decls;
std::string all_ranges_manifest = codegen.last_ranges_manifest().empty()
? codegen.generate_ranges_manifest(analysis_result.functions, all_cfgs)
: codegen.last_ranges_manifest();
std::vector<PSXRecomp::GameDispatchUnit> dispatch_units{{&codegen, dispatch_addrs}};
std::vector<std::unique_ptr<PSXRecomp::PS1Executable>> variant_views;
std::vector<std::unique_ptr<PSXRecomp::CodeGenerator>> variant_codegens;
if (!overlay_mode && !variant_seeds.empty()) {
std::vector<uint32_t> requested;
for (const auto& [addr, kind] : variant_seeds) {
(void)kind;
requested.push_back(addr);
}
const auto plans = PSXRecomp::plan_segment_variants(
*exe, codegen.last_functions(), codegen.last_cfgs(),
codegen.cps_continuations(), requested);
for (const auto& plan : plans) {
fmt::print("\n=== Segment variant 0x{:08X} (docs/SEGMENT_AWARE_CODE.md §5.4) ===\n",
plan.segment);
for (const auto& [pc, why] : plan.unplaced) {
fmt::print(stderr,
"ERROR: variant seed 0x{:08X} cannot be compiled: {}. Seed the home "
"PC first, or drop the variant seed.\n", pc, why);
}
if (!plan.unplaced.empty()) return 1;
fmt::print(" {} seed(s), direct-edge closure of {} function(s)\n",
plan.seeds.size(), plan.functions.size());
variant_views.push_back(std::make_unique<PSXRecomp::PS1Executable>(
PSXRecomp::segment_view(*exe, plan.segment)));
const PSXRecomp::PS1Executable& view = *variant_views.back();
PSXRecomp::ControlFlowAnalyzer view_cfg(view);
const auto variant_cfgs = view_cfg.analyze_all_functions(plan.functions);
variant_codegens.push_back(std::make_unique<PSXRecomp::CodeGenerator>(
view, PSXRecomp::rebase_codegen_config(codegen_config, *exe, plan.segment)));
PSXRecomp::CodeGenerator& vgen = *variant_codegens.back();
(void)vgen.generate_file(plan.functions, variant_cfgs);
std::set<uint32_t> variant_addrs;
for (const auto& gf : vgen.last_gen_funcs()) {
all_gen_funcs.push_back(gf);
if (gf.dispatchable && gf.function_name.rfind("func_", 0) == 0 &&
gf.function_name.size() == 13)
variant_addrs.insert(static_cast<uint32_t>(
std::strtoul(gf.function_name.c_str() + 5, nullptr, 16)));
}
for (const auto& decl : vgen.alias_body_decls()) variant_alias_decls.push_back(decl);
// The variant's F/R records, without the manifest's header comment.
std::istringstream vr(vgen.last_ranges_manifest());
for (std::string line; std::getline(vr, line);) {
if (!line.empty() && line[0] != '#') all_ranges_manifest += line + "\n";
}
dispatch_units.push_back({&vgen, variant_addrs});
fmt::print(" {} bodies, {} dispatch entries\n",
vgen.last_gen_funcs().size(), variant_addrs.size());
}
}
// Split-TU output: instead of one monolithic <exe_stem>_full.c, write a
// shared declarations header plus N shards bucketed by cumulative line
// count, so the game build can compile the shards in parallel. The
@@ -1587,7 +1674,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
std::filesystem::path decls_filename = out_dir / (exe_stem + "_decls.h");
{
const std::string decls =
codegen.build_shared_decls_header(codegen.last_gen_funcs());
codegen.build_shared_decls_header(all_gen_funcs, variant_alias_decls);
if (write_file_if_changed(decls_filename, decls)) {
fmt::print("✓ Saved shared decls header to {}\n", decls_filename.string());
} else {
@@ -1597,7 +1684,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
// 2. Bucket generated functions into shards by cumulative line count.
const int SHARD_LINE_BUDGET = 40000;
const std::vector<PSXRecomp::GeneratedFunction>& gen_funcs_for_shards = codegen.last_gen_funcs();
const std::vector<PSXRecomp::GeneratedFunction>& gen_funcs_for_shards = all_gen_funcs;
std::string decls_basename = exe_stem + "_decls.h";
int shard_index = 0;
@@ -1654,9 +1741,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
// Per-function code-range manifest (design §8): consumed by the overlay
// loader's per-entry validity hash. Emitted alongside _full.c for every
// build; only the overlay path actually loads it.
const std::string ranges_manifest = codegen.last_ranges_manifest().empty()
? codegen.generate_ranges_manifest(analysis_result.functions, all_cfgs)
: codegen.last_ranges_manifest();
const std::string& ranges_manifest = all_ranges_manifest;
{
std::filesystem::path ranges_filename = out_dir / (exe_stem + "_full.ranges");
if (write_file_if_changed(ranges_filename, ranges_manifest)) {
@@ -1676,18 +1761,24 @@ static int psxrecomp_game_main(int argc, char** argv) {
fmt::print("Generating dispatch table: {}\n", dispatch_filename.string());
std::string dispatch_source, dispatch_error;
if (!PSXRecomp::emit_game_dispatch(codegen, *exe, dispatch_addrs,
if (!PSXRecomp::emit_game_dispatch(dispatch_units, *exe,
ranges_manifest, dispatch_source,
dispatch_error)) {
fmt::print(stderr, "ERROR: {}\n", dispatch_error);
return 1;
}
// Entries of the home compile, plus those of its segment variants.
size_t variant_entries = 0;
for (size_t u = 1; u < dispatch_units.size(); ++u)
variant_entries += dispatch_units[u].dispatch_addrs.size();
const std::string variant_note = variant_entries
? fmt::format(", plus {} in segment variants", variant_entries) : std::string();
if (write_file_if_changed(dispatch_filename, dispatch_source)) {
fmt::print("✓ Dispatch table written ({} entries)\n\n",
dispatch_addrs.size());
fmt::print("✓ Dispatch table written ({} entries{})\n\n",
dispatch_addrs.size(), variant_note);
} else {
fmt::print("✓ Dispatch table unchanged ({} entries)\n\n",
dispatch_addrs.size());
fmt::print("✓ Dispatch table unchanged ({} entries{})\n\n",
dispatch_addrs.size(), variant_note);
}
}
+202
View File
@@ -0,0 +1,202 @@
#include "segment_variants.h"
#include <algorithm>
#include <set>
#include "fmt/format.h"
namespace PSXRecomp {
PS1Executable segment_view(const PS1Executable& exe, uint32_t seg) {
PS1Executable view = exe;
seg &= kSegmentMask;
view.header.load_address = seg | (exe.header.load_address & kPhysMask);
view.header.initial_pc = seg | (exe.header.initial_pc & kPhysMask);
return view;
}
std::vector<uint32_t> direct_successors(const ControlFlowGraph& cfg) {
std::set<uint32_t> out;
auto leave = [&](uint32_t target) {
if (target != 0 && !cfg.blocks.count(target)) out.insert(target);
};
for (const auto& [addr, block] : cfg.blocks) {
(void)addr;
const ControlFlowInstr& x = block.exit_instr;
switch (x.type) {
case ControlFlowType::Branch:
leave(x.target);
leave(x.address + 8u);
break;
case ControlFlowType::Jump:
leave(x.target);
break;
case ControlFlowType::JumpLink:
leave(x.target);
leave(x.address + 8u);
break;
case ControlFlowType::JumpLinkReg:
leave(x.address + 8u);
break;
case ControlFlowType::None:
if (block.successors.empty()) leave(block.end_addr + 4u);
break;
default: // jr / jr $ra: indirect, through dispatch
break;
}
}
return {out.begin(), out.end()};
}
namespace {
Function rebase_function(const Function& f, uint32_t seg) {
auto at = [seg](uint32_t a) { return a ? (seg | (a & kPhysMask)) : 0u; };
Function v = f;
v.start_addr = at(f.start_addr);
v.end_addr = seg | (f.end_addr & kPhysMask);
v.name = fmt::format("func_{:08X}", v.start_addr);
v.alias_walk_lo = at(f.alias_walk_lo);
for (uint32_t& e : v.alias_group_entries) e = at(e);
v.producer_lo = at(f.producer_lo);
v.producer_hi = f.producer_hi ? (seg | (f.producer_hi & kPhysMask)) : 0u;
return v;
}
} // namespace
std::vector<SegmentVariantPlan> plan_segment_variants(
const PS1Executable& exe,
const std::vector<Function>& home_functions,
const std::map<uint32_t, ControlFlowGraph>& home_cfgs,
const std::map<uint32_t, uint32_t>& home_continuations,
const std::vector<uint32_t>& seeds) {
const uint32_t link = exe.link_segment();
std::map<uint32_t, const Function*> by_start;
std::map<uint32_t, std::vector<uint32_t>> alias_groups; // host -> members
for (const Function& f : home_functions) {
by_start.emplace(f.start_addr, &f);
if (f.alias_walk_lo != 0) alias_groups[f.alias_walk_lo].push_back(f.start_addr);
}
std::map<uint32_t, SegmentVariantPlan> plans;
std::map<uint32_t, std::set<uint32_t>> roots;
for (uint32_t pc : seeds) {
const uint32_t seg = pc & kSegmentMask;
SegmentVariantPlan& plan = plans[seg];
plan.segment = seg;
plan.seeds.push_back(pc);
const uint32_t home = link | (pc & kPhysMask);
if (!maps_physical(pc)) {
plan.unplaced.emplace_back(pc, fmt::format(
"segment 0x{:08X} does not map physical memory (only KUSEG below "
"0x20000000, KSEG0 and KSEG1 do), so it is no alias of the home PC "
"0x{:08X}", seg, home));
continue;
}
uint32_t root = 0;
if (by_start.count(home)) {
root = home;
} else {
auto cont = home_continuations.find(home);
if (cont != home_continuations.end() && by_start.count(cont->second))
root = cont->second;
}
if (root == 0) {
plan.unplaced.emplace_back(pc, fmt::format(
"its home PC 0x{:08X} is not a dispatch entry of the home compile "
"(no function starts there and it is no CPS continuation)", home));
continue;
}
roots[seg].insert(root);
}
std::vector<SegmentVariantPlan> out;
for (auto& [seg, plan] : plans) {
std::set<uint32_t> included;
std::vector<uint32_t> work(roots[seg].begin(), roots[seg].end());
while (!work.empty()) {
const uint32_t start = work.back();
work.pop_back();
if (!included.insert(start).second) continue;
const Function* f = by_start.at(start);
// Alias entries share one emitted body with their host's other
// entries: compile the group together.
if (f->alias_walk_lo != 0) {
for (uint32_t member : alias_groups[f->alias_walk_lo]) work.push_back(member);
}
auto cfg = home_cfgs.find(start);
if (cfg == home_cfgs.end()) continue;
for (uint32_t succ : direct_successors(cfg->second)) {
if (by_start.count(succ)) work.push_back(succ);
}
}
for (uint32_t start : included) plan.functions.push_back(rebase_function(*by_start.at(start), seg));
std::sort(plan.functions.begin(), plan.functions.end(),
[](const Function& a, const Function& b) { return a.start_addr < b.start_addr; });
out.push_back(std::move(plan));
}
return out;
}
CodeGenConfig rebase_codegen_config(const CodeGenConfig& cfg,
const PS1Executable& exe, uint32_t seg) {
seg &= kSegmentMask;
const uint32_t link = exe.link_segment();
auto at = [&](uint32_t a) {
return (a != 0 && exe.contains_phys(a) && (a & kSegmentMask) == link)
? (seg | (a & kPhysMask)) : a;
};
auto move_set = [&](std::set<uint32_t>& s) {
std::set<uint32_t> moved;
for (uint32_t a : s) moved.insert(at(a));
s = std::move(moved);
};
CodeGenConfig v = cfg;
move_set(v.ws_sprite_tag_funcs);
move_set(v.data_shard_funcs);
move_set(v.mod_function_entry_funcs);
move_set(v.hot_funcs);
move_set(v.load_charge_batch_funcs);
move_set(v.ws_backdrop_unsquash_funcs);
move_set(v.ws_cull_bias_sites);
move_set(v.ws_cull_range_sites);
move_set(v.ws_cull_a1_sites);
move_set(v.ws_cull_screen_x_sites);
move_set(v.ws_cull_slti_sites);
move_set(v.ws_cull_slti_lower_sites);
move_set(v.ws_cull_bltz_sites);
move_set(v.ws_cull_bgez_sites);
move_set(v.ws_cull_clip_edge_x_load_sites);
move_set(v.ws_cull_negsub_sites);
move_set(v.ws_cull_vxrange_sites);
move_set(v.ws_cull_depth_sites);
move_set(v.ws_cull_plane_nx_sites);
move_set(v.ws_cull_xclip_load_sites);
move_set(v.ws_cull_nclip_keep_sites);
move_set(v.ws_cull_nclip_exact_sites);
move_set(v.ws_cull_branch_keep_sites);
move_set(v.ws_backdrop_x_sites);
move_set(v.persist_init_store_sites);
{
std::map<uint32_t, std::array<uint32_t, 4>> moved;
for (const auto& [func, data] : v.vsync_query_hle_funcs) moved[at(func)] = data;
v.vsync_query_hle_funcs = std::move(moved);
}
v.ws_bg2d_init_func = at(v.ws_bg2d_init_func);
v.ws_bg2d_count_site = at(v.ws_bg2d_count_site);
v.ws_bg2d_startcol_site = at(v.ws_bg2d_startcol_site);
v.ws_bg2d_startx_site = at(v.ws_bg2d_startx_site);
v.ws_bg2d_stream_left_site = at(v.ws_bg2d_stream_left_site);
v.ws_bg2d_stream_right_site = at(v.ws_bg2d_stream_right_site);
v.ws_bg2d_bufbase_site = at(v.ws_bg2d_bufbase_site);
v.ws_bg2d_cap_site = at(v.ws_bg2d_cap_site);
// Physically matched, kept as written: ws_signed_x_bound_sites,
// ws_cull_keep_sites, ws_cull_angle_sites, ws_aspect_cone.sites.
// The split pre-pass ran on the home compile: the closure is already made
// of its final pieces.
v.split_mid_function_targets = false;
return v;
}
} // namespace PSXRecomp
+80
View File
@@ -0,0 +1,80 @@
#pragma once
// Per-segment compiled variants of static game code
// (docs/SEGMENT_AWARE_CODE.md §5.4).
//
// A PC carries a segment, and a compiled body bakes the segment it was
// compiled for into every link, EPC, fetch tag and store PC. The home compile
// is the image's link segment. A seed that names the image's bytes in another
// segment (a segment-qualified seed, §10 decision 3) asks for a variant: the
// home functions that seed reaches through direct edges (calls, tail jumps,
// split pieces, fallthroughs, CPS continuations), compiled again in that
// segment. Direct control flow never changes segment (§2), so that closure is
// everything the variant can reach without going through dispatch.
//
// A variant is compiled through a segment view of the image: the same bytes
// with the header moved into the variant's segment. Its code identity is then
// the variant VA (§5.1: func_A00100F0), the CodeGenerator's code segment is the
// view's, and every PC it bakes is in that segment. A KSEG1 variant is charged
// a fetch per instruction by #429's rule (§5.6) with no emitter change.
#include <cstdint>
#include <map>
#include <string>
#include <utility>
#include <vector>
#include "code_generator.h"
#include "control_flow.h"
#include "function_analysis.h"
#include "ps1_exe_parser.h"
namespace PSXRecomp {
// The image seen from segment `seg`: the header's load address and entry move
// into `seg`, the bytes do not change. Addresses in the view are variant VAs.
PS1Executable segment_view(const PS1Executable& exe, uint32_t seg);
// One requested segment.
struct SegmentVariantPlan {
uint32_t segment = 0;
// The closure, rebased into `segment` and sorted by start address.
std::vector<Function> functions;
// The requested PCs, as written.
std::vector<uint32_t> seeds;
// Seeds that name no home dispatch entry (a function entry or a CPS
// continuation of the home compile), or that lie in a segment that does
// not map physical memory (maps_physical()), with the reason. Nothing is
// compiled for them.
std::vector<std::pair<uint32_t, std::string>> unplaced;
};
// The direct-edge successors of a home function that leave it: branch and
// jump targets and not-taken paths outside its blocks, call targets, call
// returns split into another piece, and a fall-through past its last block.
// Indirect transfers are not edges: they go through dispatch with the full PC.
std::vector<uint32_t> direct_successors(const ControlFlowGraph& cfg);
// Plans a variant per segment named by `seeds` (PCs outside the link segment
// that name the image's bytes). `home_functions` and `home_cfgs` are the home
// compile's final function set (CodeGenerator::last_functions / last_cfgs),
// and `home_continuations` its CPS continuation -> owner map. A seed at a home
// function entry roots that function; a seed at a CPS continuation roots its
// owner (the variant has the continuation too); an alias entry brings its
// whole alias group, which shares one body. Plans are in segment order.
std::vector<SegmentVariantPlan> plan_segment_variants(
const PS1Executable& exe,
const std::vector<Function>& home_functions,
const std::map<uint32_t, ControlFlowGraph>& home_cfgs,
const std::map<uint32_t, uint32_t>& home_continuations,
const std::vector<uint32_t>& seeds);
// The codegen config for a variant in `seg`. Config code sites that the
// emitter matches exactly are written in the link segment (main_psx refuses
// any other spelling, §5.3); each one that names the image's bytes moves into
// `seg`, so the variant gets every hook and substitution the home body gets.
// Kinds matched by physical address need no move and are kept as written.
CodeGenConfig rebase_codegen_config(const CodeGenConfig& cfg,
const PS1Executable& exe, uint32_t seg);
} // namespace PSXRecomp
+39 -3
View File
@@ -39,6 +39,9 @@
// 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"
@@ -497,9 +500,11 @@ void check_dispatch_rows(Mode mode) {
}
}
// One compile has one code segment, so two rows on one physical word (a
// variant of another segment, §5.4) are a build error until PR D's variants
// share the table: never a row that silently shadows the other.
// 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{};
@@ -522,6 +527,37 @@ void check_dispatch_duplicate_word() {
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) {
+273
View File
@@ -0,0 +1,273 @@
// Segment variants of static game code (docs/SEGMENT_AWARE_CODE.md §5.4,
// rollout PR D): the planner that turns segment-qualified seeds into per-
// segment direct-edge closures, the segment view of an image, and the codegen
// config a variant compiles with.
//
// The planner works on a hand-built KSEG0 image whose functions are given
// explicitly (so the test does not depend on discovery heuristics):
// F0 0x00 jal F1 / j F2 a call and a tail jump
// U 0x10 jr $ra reached by nothing
// F1 0x18 jr $ra F0's callee
// F2 0x20 beq -> F4, then runs a split branch out of the function and a
// off its end into F3 fall-through past its last block
// F3 0x2C jr $ra
// F4 0x38 jr $ra
// H 0x40 host with alias entries A1 0x44 and A2 0x48 (one shared body)
// F5 0x50 beq -> F1 as its last pair: taken and not taken both leave
// F6 0x58 jr $ra F5's not-taken path
// It checks:
// - a seed at a function entry roots that function; its closure follows
// calls, tail jumps, out-of-function branch targets and fall-throughs,
// and nothing else (U and H stay out);
// - a seed at a CPS continuation roots its owner;
// - an alias entry brings its whole alias group, not its host;
// - a seed that names no home dispatch entry is reported, not guessed, and
// so is a seed in a segment that does not map physical memory
// (0x20000000-0x7FFFFFFF, KSEG2);
// - plans come out per segment, in segment order, rebased: start/end,
// name, alias host, alias group and producer range all move into the
// segment;
// - direct_successors lists exactly the edges that leave a function.
// segment_view moves the header and keeps the bytes, and
// rebase_codegen_config moves the exact-match code sites that name the image
// into the variant's segment, keeps other addresses and the physically
// matched kinds as written, and turns the split pre-pass off.
#include "segment_variants.h"
#include <cstdint>
#include <cstdio>
#include <map>
#include <set>
#include <string>
#include <vector>
namespace {
int failures = 0;
void check(bool ok, const std::string& what) {
if (!ok) {
std::printf("FAIL: %s\n", what.c_str());
++failures;
}
}
constexpr uint32_t B = 0x80010000u;
constexpr uint32_t KUSEG = 0x00000000u, KSEG1 = 0xA0000000u;
uint32_t jal(uint32_t t) { return 0x0C000000u | ((t >> 2) & 0x03FFFFFFu); }
uint32_t j(uint32_t t) { return 0x08000000u | ((t >> 2) & 0x03FFFFFFu); }
uint32_t beqz(uint32_t pc, uint32_t t) { // beq a0, zero, t
return 0x10800000u | (((t - (pc + 4u)) >> 2) & 0xFFFFu);
}
constexpr uint32_t JR_RA = 0x03E00008u, NOP = 0u, ADDIU_V0 = 0x24020001u;
PSXRecomp::PS1Executable image() {
std::vector<uint32_t> w(0x60 / 4, NOP);
auto at = [&](uint32_t off, uint32_t word) { w[off / 4] = word; };
at(0x00, jal(B + 0x18)); at(0x08, j(B + 0x20)); // F0
at(0x10, JR_RA); // U
at(0x18, JR_RA); // F1
at(0x20, beqz(B + 0x20, B + 0x38)); at(0x28, ADDIU_V0); // F2
at(0x2C, JR_RA); // F3
at(0x38, JR_RA); // F4
at(0x40, ADDIU_V0); at(0x44, ADDIU_V0); at(0x48, JR_RA); // H, A1, A2
at(0x50, beqz(B + 0x50, B + 0x18)); // F5
at(0x58, JR_RA); // F6
PSXRecomp::PS1Executable exe{};
exe.header.load_address = B;
exe.header.initial_pc = B;
exe.header.file_size = static_cast<uint32_t>(w.size() * 4u);
for (uint32_t word : w)
for (int i = 0; i < 4; ++i) exe.code_data.push_back(static_cast<uint8_t>(word >> (8 * i)));
return exe;
}
PSXRecomp::Function fn(uint32_t lo, uint32_t hi, uint32_t alias_host = 0,
std::vector<uint32_t> group = {}) {
PSXRecomp::Function f;
f.start_addr = lo;
f.end_addr = hi;
f.size = hi - lo;
f.has_prologue = f.has_epilogue = false;
f.stack_frame_size = 0;
f.name = "func_" + std::to_string(lo);
f.alias_walk_lo = alias_host;
f.alias_group_entries = std::move(group);
return f;
}
std::string hex(uint32_t v) {
char b[16];
std::snprintf(b, sizeof b, "%08X", v);
return b;
}
std::set<uint32_t> starts(const PSXRecomp::SegmentVariantPlan& p) {
std::set<uint32_t> out;
for (const auto& f : p.functions) out.insert(f.start_addr);
return out;
}
} // namespace
int main() {
const auto exe = image();
const std::vector<uint32_t> group{B + 0x44, B + 0x48};
const std::vector<PSXRecomp::Function> home{
fn(B + 0x00, B + 0x10), fn(B + 0x10, B + 0x18), fn(B + 0x18, B + 0x20),
fn(B + 0x20, B + 0x2C), fn(B + 0x2C, B + 0x34), fn(B + 0x38, B + 0x40),
fn(B + 0x40, B + 0x50), fn(B + 0x44, B + 0x50, B + 0x40, group),
fn(B + 0x48, B + 0x50, B + 0x40, group), fn(B + 0x50, B + 0x58),
fn(B + 0x58, B + 0x60)};
PSXRecomp::ControlFlowAnalyzer analyzer(exe);
const auto cfgs = analyzer.analyze_all_functions(home);
// direct_successors: the edges that leave a function, and only those.
{
const auto f0 = PSXRecomp::direct_successors(cfgs.at(B));
check(f0 == std::vector<uint32_t>{B + 0x18, B + 0x20},
"F0 leaves by its jal and its j, got " + std::to_string(f0.size()));
const auto f2 = PSXRecomp::direct_successors(cfgs.at(B + 0x20));
check(f2 == std::vector<uint32_t>{B + 0x2C, B + 0x38},
"F2 leaves by its branch target and by running off its end");
check(PSXRecomp::direct_successors(cfgs.at(B + 0x18)).empty(),
"a jr $ra is no direct edge");
check(PSXRecomp::direct_successors(cfgs.at(B + 0x50)) ==
std::vector<uint32_t>{B + 0x18, B + 0x58},
"F5's final branch leaves both taken and not taken");
}
const std::map<uint32_t, uint32_t> conts{{B + 0x08, B}};
const std::vector<uint32_t> seeds{KSEG1 | B, KUSEG | ((B + 0x44) & 0x1FFFFFFFu),
KSEG1 | ((B + 0x08) & 0x1FFFFFFFu),
KSEG1 | ((B + 0x04) & 0x1FFFFFFFu)};
const auto plans = PSXRecomp::plan_segment_variants(exe, home, cfgs, conts, seeds);
check(plans.size() == 2 && plans[0].segment == KUSEG && plans[1].segment == KSEG1,
"one plan per requested segment, in segment order");
if (plans.size() == 2) {
const auto& ku = plans[0];
const auto& k1 = plans[1];
const uint32_t p = B & 0x1FFFFFFFu;
check(starts(ku) == std::set<uint32_t>{p + 0x44, p + 0x48},
"an alias entry brings its alias group, not its host");
check(starts(k1) == std::set<uint32_t>{KSEG1 | p, KSEG1 | (p + 0x18), KSEG1 | (p + 0x20),
KSEG1 | (p + 0x2C), KSEG1 | (p + 0x38)},
"the KSEG1 closure is F0 with its call, tail jump, branch target and "
"fall-through, and neither U nor H");
check(k1.seeds.size() == 3 && ku.seeds.size() == 1, "every seed is listed with its plan");
check(k1.unplaced.size() == 1 && k1.unplaced[0].first == (KSEG1 | (p + 4)) &&
k1.unplaced[0].second.find(hex(B + 4)) != std::string::npos,
"a seed inside a function body names no dispatch entry and is reported "
"with its home PC");
bool sorted = true, rebased = true;
for (size_t i = 0; i < k1.functions.size(); ++i) {
const auto& f = k1.functions[i];
if (i && k1.functions[i - 1].start_addr >= f.start_addr) sorted = false;
if ((f.end_addr & 0xE0000000u) != KSEG1 || f.name != "func_" + hex(f.start_addr))
rebased = false;
}
check(sorted && rebased, "KSEG1 functions are sorted, with rebased ends and names");
for (const auto& f : ku.functions) {
check(f.alias_walk_lo == p + 0x40 &&
f.alias_group_entries == std::vector<uint32_t>{p + 0x44, p + 0x48} &&
f.name == "func_" + hex(f.start_addr),
"alias host and group move into the variant's segment");
}
}
// A branch whose not-taken path runs past the function brings the next.
{
const auto f5 = PSXRecomp::plan_segment_variants(
exe, home, cfgs, conts, {KSEG1 | ((B + 0x50) & 0x1FFFFFFFu)});
const uint32_t p = B & 0x1FFFFFFFu;
check(f5.size() == 1 &&
starts(f5[0]) == std::set<uint32_t>{KSEG1 | (p + 0x18), KSEG1 | (p + 0x50),
KSEG1 | (p + 0x58)},
"F5's closure is F1 (taken) and F6 (not taken)");
}
// Only KUSEG below 0x20000000, KSEG0 and KSEG1 map physical memory: a seed
// in 0x20000000-0x7FFFFFFF or in KSEG2 is no alias of the image's bytes.
// It is reported with its home PC and compiles nothing.
{
const uint32_t p = B & 0x1FFFFFFFu;
const auto bad = PSXRecomp::plan_segment_variants(
exe, home, cfgs, conts, {0x20000000u | p, 0xC0000000u | p, 0x60000000u | p});
bool refused = bad.size() == 3;
for (const auto& plan : bad) {
refused = refused && plan.functions.empty() && plan.unplaced.size() == 1 &&
plan.unplaced[0].first == (plan.segment | p) &&
plan.unplaced[0].second.find("does not map physical memory") !=
std::string::npos &&
plan.unplaced[0].second.find(hex(B)) != std::string::npos;
}
check(refused, "a seed in a segment that does not map physical memory is "
"reported with its home PC and compiles nothing");
check(PSXRecomp::maps_physical(0x1FFFFFFCu) && !PSXRecomp::maps_physical(0x20000000u) &&
!PSXRecomp::maps_physical(0x7FFFFFFCu) && PSXRecomp::maps_physical(0x80000000u) &&
PSXRecomp::maps_physical(0xBFFFFFFCu) && !PSXRecomp::maps_physical(0xC0000000u) &&
!PSXRecomp::maps_physical(0xFFFE0130u),
"maps_physical is KUSEG below 0x20000000, KSEG0 and KSEG1");
}
// A seed at a CPS continuation alone roots its owner.
{
const auto only = PSXRecomp::plan_segment_variants(
exe, home, cfgs, conts, {KSEG1 | ((B + 0x08) & 0x1FFFFFFFu)});
check(only.size() == 1 && only[0].unplaced.empty() &&
starts(only[0]).count(KSEG1 | (B & 0x1FFFFFFFu)),
"a continuation seed roots the function that owns it");
}
// segment_view: the header moves, the bytes stay.
{
const auto view = PSXRecomp::segment_view(exe, KSEG1);
check(view.load_address() == (KSEG1 | (B & 0x1FFFFFFFu)) &&
view.entry_point() == (KSEG1 | (B & 0x1FFFFFFFu)) &&
view.link_segment() == KSEG1 && view.code_data == exe.code_data,
"the KSEG1 view moves the header and keeps the bytes");
check(view.read_word(KSEG1 | ((B + 0x18) & 0x1FFFFFFFu)) == exe.read_word(B + 0x18),
"the view reads the image's words at its own VAs");
}
// rebase_codegen_config: exact-match sites naming the image move; the
// rest is kept as written.
{
PSXRecomp::CodeGenConfig cfg;
cfg.hot_funcs = {B + 0x18, 0x80090000u};
cfg.mod_function_entry_funcs = {B};
cfg.ws_cull_bias_sites = {B + 0x28};
cfg.persist_init_store_sites = {B + 0x28};
cfg.vsync_query_hle_funcs[B + 0x20] = {1u, 2u, 3u, 4u};
cfg.ws_bg2d_count_site = B + 0x28;
cfg.ws_bg2d_init_func = 0;
PSXRecompV4::WidescreenSignedBoundSite bound{};
bound.address = B + 0x28;
cfg.ws_signed_x_bound_sites = {bound};
const auto v = PSXRecomp::rebase_codegen_config(cfg, exe, KSEG1);
const uint32_t p = B & 0x1FFFFFFFu;
check(v.hot_funcs == std::set<uint32_t>{KSEG1 | (p + 0x18), 0x80090000u},
"an in-image site moves into the segment; an address outside the image stays");
check(v.mod_function_entry_funcs == std::set<uint32_t>{KSEG1 | p} &&
v.ws_cull_bias_sites == std::set<uint32_t>{KSEG1 | (p + 0x28)} &&
v.persist_init_store_sites == std::set<uint32_t>{KSEG1 | (p + 0x28)},
"hook and site sets move");
check(v.vsync_query_hle_funcs.size() == 1 &&
v.vsync_query_hle_funcs.count(KSEG1 | (p + 0x20)) &&
v.vsync_query_hle_funcs.at(KSEG1 | (p + 0x20))[3] == 4u,
"the VSync-query function moves; its data addresses do not");
check(v.ws_bg2d_count_site == (KSEG1 | (p + 0x28)) && v.ws_bg2d_init_func == 0,
"scalar sites move; an unset one stays unset");
check(v.ws_signed_x_bound_sites.size() == 1 &&
v.ws_signed_x_bound_sites[0].address == B + 0x28,
"a physically matched site is kept as written");
check(!v.split_mid_function_targets && cfg.split_mid_function_targets,
"a variant compiles the home compile's final pieces: no split pre-pass");
}
if (failures) {
std::printf("%d check(s) failed\n", failures);
return 1;
}
std::printf("PASS: segment variants plan direct-edge closures per segment, rebased\n");
return 0;
}
+261 -35
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
"""Recompiler codegen regression test: a KUSEG-linked EXE compiles for KUSEG,
and dispatch is exact (docs/SEGMENT_AWARE_CODE.md §5.3, §5.5).
segment-qualified seeds compile segment variants, and dispatch is exact
(docs/SEGMENT_AWARE_CODE.md §5.3, §5.4, §5.5).
PS1 segments alias the same physical RAM, but a PC carries its segment into
every link value, EPC and I-cache tag. Most PS-X EXE headers carry KSEG0
@@ -16,8 +17,19 @@ KSEG0 or KSEG1 alias ran it too. Now:
PC, so an alias PC with no body of its own misses; psx_game_address_in_text
stays physical, which sends that segment miss to the interpreter;
- a seed in the link segment is an entry; a seed for the same bytes in
another segment is reported as a variant request (§5.4), never folded into
the home body and never silently dropped;
another segment is a variant request (§5.4), never folded into the home
body: the home functions it reaches through direct edges are compiled
again in its segment, with their own names, rows and baked PCs (a cached
variant is the home C with the segment swapped; a KSEG1 one is charged a
fetch per instruction), and several rows then share a physical word in
both lookup forms. A jump-table case whose value is in another segment
than its body tail-transfers instead of continuing locally, in a variant
and in a home compile alike. A variant closure that reaches a data stub
compiles it with no dispatch row, and one that reaches an alias entry
compiles its whole alias group, whose shared body every shard can call
through the declarations header. A variant seed that names no home
dispatch entry, or that lies in a segment that does not map physical
memory (0x20000000-0x7FFFFFFF, KSEG2), stops the build;
- a header whose entry and load address are in different segments is
rejected, a KSEG1-linked image compiles as uncached (a fetch charge per
instruction), and a config code site in a foreign segment is refused.
@@ -126,6 +138,18 @@ def compile_and_run(compiler, harness, sources, include):
subprocess.run([binary], check=True, cwd=tmp)
def compile_only(compiler, sources, includes):
"""Compile `sources` without linking (syntax and declarations only)."""
if os.path.basename(compiler).lower() in ("cl", "cl.exe", "clang-cl", "clang-cl.exe"):
command = [compiler, "/nologo", "/Zs"] + ["/I" + i for i in includes] + sources
else:
command = [compiler, "-std=c11", "-fsyntax-only",
"-Werror=implicit-function-declaration"] + \
[a for i in includes for a in ("-I", i)] + sources
r = subprocess.run(command, capture_output=True, text=True)
return r.returncode == 0, r.stdout + r.stderr
def run(recompiler, args, cwd=ROOT):
return subprocess.run([recompiler] + args, capture_output=True, text=True, cwd=cwd)
@@ -155,6 +179,50 @@ def generate(recompiler, tmp, name, entry, load, seeds, body=None):
return dsrc, full, r.stdout
def bodies_of(full):
"""{func_ name: body text} of the emitted C."""
return {m.group(1): m.group(2) for m in re.finditer(
r"^void (func_[0-9A-F]{8})\(CPUState\* cpu\)\s*\{(.*?)^\}", full, re.M | re.S)}
def to_segment(text, seg):
"""Every 8-digit hex token of the image moved into `seg` (constants,
labels and names alike)."""
return re.sub(r"(?<![0-9A-Fa-f])([0-9A-F])([0-9A-F]{7})(?![0-9A-Fa-f])",
lambda m: "%08X" % (seg | (int(m.group(0), 16) & PHYS_MASK))
if (int(m.group(0), 16) & PHYS_MASK) >> 16 == (LOAD >> 16) else m.group(0),
text)
def build_switch_body(load=LOAD, case_seg=None):
"""A bounded jump table in an image at `load`: sltiu/beq guard, lw of a
table of case PCs in `case_seg` (default: the image's own segment), jr.
Each case branches to one shared jr $ra."""
if case_seg is None:
case_seg = load & SEG_MASK
body = w([0x3C080000 | (load >> 16), 0x25100100, 0, 0x2C620003, 0x10400010, 0,
0x00031080, 0x00501021, 0x8C420000, 0, 0x00400008, 0,
0x24020001, 0x10000007, 0, 0x24020002, 0x10000004, 0,
0x24020003, 0x10000001, 0, 0x03E00008, 0])
body += b"\x00" * (0x100 - len(body))
phys = load & PHYS_MASK
return body + w([case_seg | (phys + 0x30), case_seg | (phys + 0x3C), case_seg | (phys + 0x48)])
def build_stub_body():
"""A function whose jal target decodes as data: the target is a data stub
(psx_unknown_dispatch, no dispatch row), in the home compile and in a
variant that reaches it."""
return w([0x27BDFFF8, 0xAFBF0004, jal(LOAD + 0x20), 0, 0x8FBF0004, 0x27BD0008,
0x03E00008, 0, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF])
def build_alias_body(n=40):
"""One straight-line host function with interior seeds at +0x10 and +0x20:
an alias group (one shared psx_alias_body_ body, a wrapper per entry)."""
return w([0x24420001] * n + [0x03E00008, 0])
def rows_of(src):
tbl = re.search(r"k_psx_game_dispatch\[\] = \{(.*?)\n\};", src, re.DOTALL)
if not tbl:
@@ -185,24 +253,28 @@ def main():
check("Loaded 2 extra function addresses" in log,
"KUSEG seeds in the link segment must be loaded (log: %r)"
% re.findall(r"Loaded .*", log))
check("variant seed 0x80010020 (entry, KSEG0): home PC would be 0x00010020" in log,
"a KSEG0 seed for a KUSEG image must be reported as a variant request")
check("variant seed 0x80010020 (entry, KSEG0): home PC would be 0x00010020" in log
and "Segment variant 0x80000000" in log,
"a KSEG0 seed for a KUSEG image must be compiled as a variant request")
rows = rows_of(src)
check(len(rows) >= 2, "expected at least two dispatch entries, got %d" % len(rows))
check(all((a & SEG_MASK) == KUSEG and (r == 0 or (r & SEG_MASK) == KUSEG)
for a, r, _ in rows),
"every row key and resume PC is a KUSEG PC: %s" % rows)
check(all((int(fn[5:], 16) & SEG_MASK) == KUSEG and (r != 0 or fn == "func_%08X" % a)
for a, r, fn in rows),
"func_ names are the KUSEG identity: %s" % rows)
check(len(rows) >= 3, "expected at least three dispatch entries, got %d" % len(rows))
check(all((a & SEG_MASK) == (int(fn[5:], 16) & SEG_MASK) and
(r == 0 or (r & SEG_MASK) == (a & SEG_MASK)) for a, r, fn in rows),
"every row key and resume PC is in its body's segment: %s" % rows)
check(all(r != 0 or fn == "func_%08X" % a for a, r, fn in rows),
"entry rows name their own body: %s" % rows)
check([(a, fn) for a, _, fn in rows if (a & SEG_MASK) != KUSEG] ==
[(KSEG0 | (LOAD + 0x20), "func_80010020")],
"the only row outside KUSEG is the requested KSEG0 variant: %s" % rows)
check({a for a, r, _ in rows} >= {LOAD, LOAD + 0x20}, "both functions have rows")
check("void func_00010000(CPUState* cpu)" in full and
"void func_00010020(CPUState* cpu)" in full,
"bodies are named by their KUSEG PC (§5.1)")
check("cpu->gpr[31] = 0x00010010u;" in full, "the jal links a KUSEG PC")
check("psx_icache_fetch(cpu, 0x00010000u)" in full, "fetch tags are KUSEG")
check(not re.search(r"0x8001[0-9A-F]{4}u", full),
"no KSEG0 PC of the image appears in the KUSEG body")
bodies = bodies_of(full)
check({"func_00010000", "func_00010020", "func_80010020"} <= set(bodies),
"bodies are named by their PC, the variant's included (§5.1): %s" % sorted(bodies))
home = bodies["func_00010000"] + bodies["func_00010020"]
check("cpu->gpr[31] = 0x00010010u;" in home, "the jal links a KUSEG PC")
check("psx_icache_fetch(cpu, 0x00010000u)" in home, "fetch tags are KUSEG")
check(not re.search(r"0x8001[0-9A-F]{4}u", home),
"no KSEG0 PC of the image appears in the KUSEG bodies")
keys = [a & PHYS_MASK for a, _, _ in rows]
check(keys == sorted(keys), "dispatch table is sorted by physical address")
@@ -212,20 +284,154 @@ def main():
body = m.group(1)
check("0x1FFFFFFFu" in body and "k_psx_game_dispatch_index" in body,
"small resident tables index the physical word")
check(".addr != addr" in body, "the lookup requires the exact PC")
check("k_psx_game_dispatch[row].addr == addr" in body,
"a word with rows in two segments is searched for the exact PC")
# -- segment variants: the direct-edge closure, per segment (§5.4) ----
vsrc, vfull, vlog = generate(
args.recompiler, tmp, "variants", LOAD, LOAD,
[LOAD, LOAD + 0x20, KSEG0 | LOAD, KSEG1 | LOAD, KSEG0 | (LOAD + 0x10)])
vrows = {a: (r, fn) for a, r, fn in rows_of(vsrc)}
for seg in (KSEG0, KSEG1):
name = "func_%08X" % (seg | LOAD)
want = {seg | LOAD: (0, name), seg | (LOAD + 0x10): (seg | (LOAD + 0x10), name),
seg | (LOAD + 0x20): (0, "func_%08X" % (seg | (LOAD + 0x20)))}
got = {a: v for a, v in vrows.items() if (a & SEG_MASK) == seg}
check(got == want, "the 0x%08X variant is A, its call return and its jal callee B "
"(the closure), with rows in that segment: %s" % (seg, got))
vbodies = bodies_of(vfull)
for fn in ("func_00010000", "func_00010020"):
v0 = "func_%08X" % (KSEG0 | int(fn[5:], 16))
check(to_segment(vbodies[fn], KSEG0) == vbodies[v0],
"a cached variant is the home C with the segment swapped (%s)" % v0)
v1 = vbodies["func_%08X" % (KSEG1 | int(fn[5:], 16))]
insns = re.findall(r"/\* 0x([0-9A-F]{8}): ", v1)
fetched = set(re.findall(r"psx_icache_fetch\(cpu, 0x([0-9A-F]{8})u\)", v1))
check(insns and all(i in fetched and i.startswith("A") for i in insns),
"every instruction of a KSEG1 variant is charged its own fetch (§5.6)")
check(not re.search(r"0x[08]001[0-9A-F]{4}u", v1),
"a KSEG1 variant bakes no KUSEG or KSEG0 PC of the image")
# A variant seed must name a home dispatch entry.
psx = os.path.join(tmp, "variants.psx")
bad_seeds = os.path.join(tmp, "unplaced.seeds.txt")
with open(bad_seeds, "w") as f:
f.write("0x%08X\n0x%08X\n" % (LOAD, KSEG0 | (LOAD + 4)))
r = run(args.recompiler, [psx, "--seeds", bad_seeds, "--out-dir",
os.path.join(tmp, "unplaced")])
check(r.returncode != 0 and "variant seed 0x80010004 cannot be compiled" in r.stderr,
"a variant seed with no home dispatch entry stops the build:\n%s"
% (r.stderr or r.stdout)[-2000:])
# A jump-table case is local only in its own segment: the KSEG0
# variant of a KUSEG switch tail-transfers every KUSEG case value.
_, sfull, _ = generate(args.recompiler, tmp, "switch", LOAD, LOAD,
[LOAD, KSEG0 | LOAD], body=build_switch_body())
sbodies = bodies_of(sfull)
check("/* jump table 0x00010100" in sbodies["func_00010000"] and
re.search(r"case 0x00010030u:\s*\n(.*\n)*?\s*goto block_00010030;",
sbodies["func_00010000"]),
"the home body switches locally on its own segment's case PCs")
v = sbodies["func_80010000"]
check("jump table" not in v and not re.search(r"case 0x0001[0-9A-F]{4}u", v) and
"cpu->pc = _jt_80010028; return;" in v,
"the KSEG0 variant tail-transfers a KUSEG case value to dispatch")
# A home compile follows the same rule. Its tables normally hold its
# own segment's PCs and stay local switches; a table whose values are
# another segment's PCs of the image's bytes (a KSEG0 image whose table
# holds KUSEG PCs) dispatches those cases with their full PC, where
# they are segment misses, instead of continuing in the KSEG0 body.
for case_seg, local in ((KSEG0, True), (KUSEG, False)):
name = "switch_k0_" + ("own" if local else "kuseg")
_, hfull, _ = generate(args.recompiler, tmp, name, KSEG0 | LOAD, KSEG0 | LOAD,
[KSEG0 | LOAD], body=build_switch_body(KSEG0 | LOAD, case_seg))
h = bodies_of(hfull)["func_80010000"]
if local:
check("/* jump table 0x80010100" in h and
re.search(r"case 0x80010030u:\s*\n(.*\n)*?\s*goto block_80010030;", h),
"a KSEG0 home body switches locally on its own segment's case PCs")
else:
check("jump table" not in h and not re.search(r"case 0x0001[0-9A-F]{4}u", h) and
"cpu->pc = _jt_80010028; return;" in h,
"a KSEG0 home body tail-transfers a KUSEG case value to dispatch")
# Only KUSEG below 0x20000000, KSEG0 and KSEG1 map the image's bytes. A
# seed in 0x20000000-0x7FFFFFFF or in KSEG2 names no code of the image:
# it stops the build with the link-segment spelling of its bytes.
for bad in (0x20000000 | LOAD, 0xC0000000 | LOAD):
bad_seeds = os.path.join(tmp, "badseg.seeds.txt")
with open(bad_seeds, "w") as f:
f.write("0x%08X\n0x%08X\n" % (LOAD, bad))
r = run(args.recompiler, [psx, "--seeds", bad_seeds, "--out-dir",
os.path.join(tmp, "badseg")])
check(r.returncode != 0 and
("seed 0x%08X is in segment 0x%08X, which does not map physical memory"
% (bad, bad & SEG_MASK)) in r.stderr and
("are 0x%08X in its link segment" % LOAD) in r.stderr and
"Segment variant" not in r.stdout,
"a seed in a segment that does not map physical memory stops the build:\n%s"
% (r.stderr or r.stdout)[-2000:])
# A variant closure that reaches a data stub compiles the stub in its
# segment, and the stub gets no dispatch row, as at home.
dsrc, dfull, _ = generate(args.recompiler, tmp, "stub", LOAD, LOAD, [LOAD, KSEG0 | LOAD],
body=build_stub_body())
drows = {a for a, _, _ in rows_of(dsrc)}
check("psx_unknown_dispatch(cpu, 0x80010020u, 0x00010020u);" in
bodies_of(dfull).get("func_80010020", ""),
"the variant's jal target is compiled as its data stub")
check((KSEG0 | LOAD) in drows and not drows & {LOAD + 0x20, KSEG0 | (LOAD + 0x20)},
"a data stub has no dispatch row, at home or in a variant: %s"
% sorted("%08X" % a for a in drows))
# A variant seed at an alias entry compiles its alias group: one shared
# psx_alias_body_ in the variant's segment and a wrapper per entry.
# The shards share one declarations header; every alias body a shard
# defines is declared there, so a wrapper in another shard can call it
# (compiled below against the header alone), and every emitted file
# compiles.
_, afull, _ = generate(args.recompiler, tmp, "alias", LOAD, LOAD,
[LOAD, LOAD + 0x10, LOAD + 0x20, KSEG0 | (LOAD + 0x10)],
body=build_alias_body())
abodies = bodies_of(afull)
check("psx_alias_body_80010000(cpu, 0x80010010u);" in abodies.get("func_80010010", "") and
"psx_alias_body_80010000(cpu, 0x80010020u);" in abodies.get("func_80010020", ""),
"an alias entry's variant brings its whole alias group: %s" % sorted(abodies))
adir = os.path.join(tmp, "alias")
hosts = sorted(set(re.findall(r"^void (psx_alias_body_[0-9A-F]{8})\(", afull, re.M)))
check(hosts == ["psx_alias_body_00010000", "psx_alias_body_80010000"],
"the home and the variant alias bodies are emitted: %s" % hosts)
if args.compiler:
probe = os.path.join(tmp, "alias_decls_probe.c")
with open(probe, "w") as f:
f.write('#include "alias.psx_decls.h"\n'
"void (*const alias_bodies_probe[])(CPUState*, uint32_t) = {%s};\n"
% ", ".join(hosts))
sources = [probe] + sorted(os.path.join(adir, n) for n in os.listdir(adir)
if n.endswith(".c"))
ok, out = compile_only(args.compiler, sources, [adir, os.path.join(ROOT, "runtime", "include")])
check(ok, "every alias body is declared in the shared header and the variant "
"output compiles:\n" + out[-3000:])
# -- a table spanning 2 MiB or more takes the binary-search form ------
wide_src, _, _ = generate(args.recompiler, tmp, "kuseg_wide", LOAD, LOAD,
[LOAD, LOAD + WIDE_SPAN], body=build_wide_body())
[LOAD, LOAD + WIDE_SPAN, KSEG1 | (LOAD + WIDE_SPAN)],
body=build_wide_body())
wide_rows = rows_of(wide_src)
check({LOAD, LOAD + WIDE_SPAN} <= {a for a, _, _ in wide_rows} and
all((a & SEG_MASK) == KUSEG for a, _, _ in wide_rows),
"the wide KUSEG image has KUSEG rows at both ends: %s" % wide_rows)
check({LOAD, LOAD + WIDE_SPAN, KSEG1 | (LOAD + WIDE_SPAN)} <=
{a for a, _, _ in wide_rows} and
all((a & SEG_MASK) == KUSEG for a, _, _ in wide_rows
if a != KSEG1 | (LOAD + WIDE_SPAN)),
"the wide KUSEG image has KUSEG rows at both ends and one KSEG1 variant: %s"
% wide_rows)
wm = re.search(r"static const PsxGameDispatchEntry\* psx_game_find_entry"
r"\(uint32_t addr\) \{(.*?)\n\}", wide_src, re.DOTALL)
check(wm and "k_psx_game_dispatch_index" not in wm.group(1) and
"while (lo < hi)" in wm.group(1),
"a table spanning 2 MiB or more binary-searches the physical word")
"while (lo < hi)" in wm.group(1) and
"k_psx_game_dispatch[row].addr == addr" in wm.group(1),
"a table spanning 2 MiB or more binary-searches the physical word, "
"then the exact PC among the word's rows")
# -- JAL-driven discovery takes the call's segment, not KSEG0 --------
jsrc, _, jlog = generate(args.recompiler, tmp, "kuseg_jal", LOAD, LOAD, [],
@@ -272,6 +478,16 @@ def main():
if ok:
check(r.returncode == 0, "a link-segment config site is accepted:\n%s"
% (r.stderr or r.stdout)[-2000:])
# The seeds ask for a KSEG0 variant of that function: a config
# code site applies to every segment compiled for its bytes.
hot = ""
for n in os.listdir(os.path.join(tmp, "cfg_%08X" % site)):
if n.endswith(".c"):
with open(os.path.join(tmp, "cfg_%08X" % site, n)) as f:
hot += f.read()
for fn in ("func_00010020", "func_80010020"):
check("__attribute__((hot))\n#endif\nvoid %s(CPUState* cpu)" % fn in hot,
"the link-segment hot_funcs site reaches %s" % fn)
else:
check(r.returncode != 0 and
"recompiler.hot_funcs: 0x80010020 -> 0x00010020" in r.stderr,
@@ -402,12 +618,14 @@ int main(void) {
expected = &k_psx_game_dispatch[i];
assert(psx_game_find_entry(addr) == expected);
assert(psx_game_is_function_entry(addr) == (expected != 0));
if (segments[s] != 0x00000000u) assert(!expected);
/* Outside KUSEG only the requested variant has a body. */
if (segments[s] != 0x00000000u) assert(!expected || addr == 0x80010020u);
if (addr == 0x80010020u) assert(expected && expected->addr == addr);
int has_home = 0;
for (unsigned i = 0; i < PSX_GAME_DISPATCH_COUNT; ++i)
if ((k_psx_game_dispatch[i].addr & 0x1FFFFFFFu) == phys) has_home = 1;
if (has_home) assert(psx_game_address_in_text(addr));
if (has_home && segments[s] != 0x00000000u) {
if (has_home && !expected) {
int old_calls = calls, old_irqs = irqs;
cpu.pc = 0xDEADBEEFu;
assert(!psx_game_text_native_ok(addr));
@@ -448,7 +666,7 @@ int main(void) {
assert(!psx_game_find_entry(0xFFFFFFFFu));
assert(!psx_game_find_entry(0));
}
assert(home_hits >= 4); /* both functions, both geometries */
assert(home_hits >= 6); /* both functions and the variant, both geometries */
return 0;
}
"""
@@ -484,6 +702,8 @@ int main(void) {
assert(PSX_GAME_DISPATCH_COUNT >= 2);
for (unsigned i = 0; i < PSX_GAME_DISPATCH_COUNT; ++i) {
const uint32_t phys = k_psx_game_dispatch[i].addr & 0x1FFFFFFFu;
/* Each word once: a variant's row follows its home row's word. */
if (i && (k_psx_game_dispatch[i - 1].addr & 0x1FFFFFFFu) == phys) continue;
for (unsigned s = 0; s < 3; ++s) {
for (int d = -4; d <= 4; d += 4) {
const uint32_t addr = segments[s] | (phys + (uint32_t)d);
@@ -492,7 +712,9 @@ int main(void) {
if (k_psx_game_dispatch[j].addr == addr) expected = &k_psx_game_dispatch[j];
assert(psx_game_find_entry(addr) == expected);
assert(psx_game_is_function_entry(addr) == (expected != 0));
if (segments[s] != 0x00000000u) assert(!expected);
/* Outside KUSEG only the requested KSEG1 variant has a body. */
if (segments[s] != 0x00000000u)
assert(!expected || addr == (0xA0000000u | (0x00010000u + 0x200000u)));
const int before = calls;
assert(psx_dispatch_game_compiled(&cpu, addr) == (expected != 0));
assert(calls == before + (expected != 0));
@@ -506,11 +728,15 @@ int main(void) {
"""
compile_and_run(args.compiler, wide, [], include)
print("KUSEG-linked EXE test passed (%d KUSEG rows, exact lookup in the indexed and "
"binary-search forms, aliases miss, variant seeds reported, KSEG1 image charged "
"per instruction, foreign config sites and directives refused, physically "
"matched sites accepted, mirror PCs never run compiled code in 2 or 8 MiB "
"geometry)" % len(rows))
print("KUSEG-linked EXE test passed (%d rows, exact lookup in the indexed and "
"binary-search forms with variant rows, unrequested aliases miss, variants "
"compiled as their direct-edge closure, cached variants equal the home C, KSEG1 "
"variants and images charged per instruction, foreign jump-table cases "
"dispatched at home and in variants, data stubs rowless, variant alias groups "
"declared for every shard, seeds outside the physical segments refused, foreign "
"config sites and directives refused, physically matched "
"sites accepted, mirror PCs never run compiled code in 2 or 8 MiB geometry)"
% len(rows))
if __name__ == "__main__":
+24 -7
View File
@@ -45,10 +45,7 @@ SEG_MASK, PHYS_MASK = 0xE0000000, 0x1FFFFFFF
# id -> (design section, what is missing). Remove an entry in the change
# that closes it; the test fails until the ledger matches.
KNOWN_GAPS = {
"segment-variants": ("5.4", "a seeded KSEG1 entry gets no body of its own"),
"kseg1-fetch-charge": ("5.6", "compiled KSEG1 code does not charge +4 per fetch"),
}
KNOWN_GAPS = {}
# Closed ids that stay as regression guards: each reports a new gap if the
# property breaks again. id -> design section.
@@ -66,6 +63,10 @@ REGRESSION_GUARDS = {
"home-seed-accepted": "5.3",
"alias-fetch-coherence": "5.3",
"segment-miss": "5.5",
# PR D: segment-qualified seeds compile per-segment variants, and KSEG1
# variants charge a fetch per instruction.
"segment-variants": "5.4",
"kseg1-fetch-charge": "5.6",
}
@@ -431,6 +432,19 @@ def main():
gaps.add("segment-variants")
notes["segment-variants"] = ", ".join(
"0x%08X->%s" % (q, "0x%08X" % found[q][0] if found[q] else "miss") for q in bad)
else:
# A variant body is its segment's code identity: every PC it bakes
# (links, fetch tags, IRQ resume PCs, exits and continuation keys,
# store PCs) is in that segment, and so is its name.
foreign = sorted({pc for q in variant_q for gid, pattern in PC_SITES.items()
for pc in pc_constants(bodies[found[q][2]], pattern)
if (pc & SEG_MASK) != (q & SEG_MASK)} |
{int(found[q][2][5:], 16) for q in variant_q
if (int(found[q][2][5:], 16) & SEG_MASK) != (q & SEG_MASK)})
if foreign:
gaps.add("segment-variants")
notes["segment-variants"] = "%d PCs baked in another segment, e.g. 0x%08X" % (
len(foreign), foreign[0])
wrong_home = [q for q in home_q if not found[q] or found[q][0] != q]
stray = [q for q in miss_q if found[q]]
if stray or wrong_home:
@@ -445,9 +459,11 @@ def main():
if (pc & SEG_MASK) != link})
if gid == "resume-pc-segment":
# The dispatch rows' keys and resume PCs are PCs too (§5.2):
# dispatch sets cpu->pc = resume_pc before entering a body.
off = sorted(set(off) | {pc for a, r, _ in rows for pc in (a, r)
if pc and (pc & SEG_MASK) != link})
# dispatch sets cpu->pc = resume_pc before entering a body, so
# each row's key and resume PC are in the segment of the body
# it enters (the home body's, or a variant's, §5.4).
off = sorted(set(off) | {pc for a, r, fn in rows for pc in (a, r)
if pc and (pc & SEG_MASK) != (int(fn[5:], 16) & SEG_MASK)})
if off:
gaps.add(gid)
notes[gid] = "%d constants, e.g. 0x%08X" % (len(off), off[0])
@@ -506,6 +522,7 @@ def main():
run_recompiler(args.bios_recompiler, ["--config", openbios_toml, "--out-dir", tmp])
with open(os.path.join(tmp, "OpenBIOS_full.c")) as f:
bios_c = f.read()
total, uncharged = bios_uncharged_kseg1(bios_c)
if total == 0:
model_fail.append("OpenBIOS: no KSEG1 instructions found (parser drift?)")
+16 -4
View File
@@ -6,10 +6,11 @@ A seed is a PC, so it carries a segment (docs/SEGMENT_AWARE_CODE.md §5.3,
§5.4). dirty_ram_stats keys its rows by physical address, so the tool writes
each one in the title's link segment: the segment of its EXE's load address
(KSEG0 0x8001xxxx for most titles, KUSEG 0x0001xxxx for Kula World or Alien
Resurrection). The recompiler reports a seed in any other segment as a
variant request and does not compile it as an entry. Segment misses (TCP
`segment_misses`: static text entered in a segment with no body of its own)
are written with their full PC; each is a variant request (§5.4).
Resurrection). The recompiler takes a seed in any other segment as a variant
request, not an entry: it compiles the home body's direct-edge closure again
in that segment (§5.4). Segment misses (TCP `segment_misses`: static text
entered in a segment with no body of its own) are written with their full PC,
so the next regeneration compiles a variant for each.
The link segment comes from --segment, from --game-toml ([game].load_address,
or the header of its [game].exe), or from game_text_lo when that is written as
@@ -41,6 +42,13 @@ SEG_MASK = 0xE0000000
SEGMENTS = {"kuseg": 0x00000000, "kseg0": 0x80000000, "kseg1": 0xA0000000}
def maps_physical(pc):
"""KUSEG below 0x20000000, KSEG0 and KSEG1 map physical memory; a PC
anywhere else addresses no RAM, so it names no code of the image, and the
recompiler refuses it as a seed."""
return pc < 0x20000000 or (pc & SEG_MASK) in (0x80000000, 0xA0000000)
def parse_segment(text):
key = text.strip().lower()
if key in SEGMENTS:
@@ -145,6 +153,10 @@ def main(argv=None):
if sm.get("ok"):
for row in sm.get("summary", []):
vaddr = int(row["pc"], 16)
if not maps_physical(vaddr):
print("warning: segment miss 0x%08X is in a segment that does not map "
"physical memory; not a seed (the recompiler refuses it)" % vaddr)
continue
if lo_phys <= (vaddr & PHYS_MASK) < hi_phys and vaddr not in known:
new_entries.append((vaddr, row["count"],
"segment miss: %d hits, home %s (variant request)"
+9 -2
View File
@@ -8,7 +8,9 @@ every dirty_ram miss as KSEG0 could never close a KUSEG-linked title's misses
(Kula World, Alien Resurrection). dirty_ram_stats rows are physical; the tool
takes the segment from --segment, --game-toml or a segmented game_text_lo,
refuses to guess from a physical one, and writes `segment_misses` rows with
their full PC.
their full PC. A row in a segment that does not map physical memory
(0x20000000-0x7FFFFFFF, KSEG2) is reported and not written: the recompiler
refuses such a seed.
A fake debug server answers the two commands the tool sends. Its
`segment_misses` rows use the format string of the runtime's handler, read
@@ -34,7 +36,9 @@ PER_PC = [ # physical, as dirty_ram_stats keys them
{"pc": "0x00010200", "hits": 3, "insns": 12},
{"pc": "0x000C0000", "hits": 5, "insns": 7}, # outside the text range
]
SEGMENT_MISSES = [(0x800100F0, 0x000100F0, 4), (0xA0010124, 0x00010124, 1)]
SEGMENT_MISSES = [(0x800100F0, 0x000100F0, 4), (0xA0010124, 0x00010124, 1),
# 0x20000000-0x7FFFFFFF does not map physical memory: no seed.
(0x200100F0, 0x000100F0, 1)]
class FakeDebugServer(threading.Thread):
@@ -115,6 +119,9 @@ def main():
text = f.read()
check("home 0x000100F0 (variant request)" in text,
"a segment miss is written as a variant request: %r" % text)
check("segment miss 0x200100F0 is in a segment that does not map" in r.stdout,
"a segment miss outside the physical segments is reported, not "
"written: %r" % r.stdout)
# A second run appends nothing: known seeds are exact PCs.
r = run_tool(server.port, out, "--segment", "kuseg")
check(r.returncode == 0 and "No new addresses" in r.stdout and