From 6f9649c7e7dc9fbd3c3be2ed9621662e72118b84 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 30 Apr 2026 14:14:13 -0700 Subject: [PATCH] decompressed: per-variant synthetic link identities for pattern fragments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Path 2 of the pattern-fragment dispatch architecture: each variant of a [[input.decompressed_section_pattern]] now gets a unique link-time ram_addr from a synthetic vram pool (0xC0000000+, KSEG2/KSEG3 — unused by N64 software so it can't collide with engine-resident sections like RSP at 0xA4000000+). Why: when multiple variants share a single canonical link bucket (e.g. all stadium_models pattern variants at 0x8FF00000), runtime fragment-vaddr resolution via gFragments[id] is single-pointer and ambiguous when more than one variant is host-resident at the same time. Per-variant synthetic ram_addrs make each variant's RELOC_HI16 / RELOC_LO16 emit produce a unique 0xCXXXXXXX literal at runtime, giving variant-internal references unambiguous identity without depending on caller PC, host stack walks, or data-context tracking. Implementation: - add_decompressed_section accepts an override_link_ram_addr param. The bytes-encoded `vram` (= canonical link bucket) is passed to parse_fragment_relocs and discover_function_bounds (so jump tables resolve correctly against the body's encoded references), while section.ram_addr is set to the override. The two roles of vram are cleanly separated. - New original_pattern_id field on Section. Populated for synthetic- link variants with the original game-side fragment id derived from the pattern's canonical bucket (e.g. 0xEF for stadium_models). Lets the runtime candidate filter know which game id should include this synthetic section as a candidate, eliminating cross- pattern hash-collision misregistration. - main.cpp emit: section_load_table now writes original_pattern_id into the SectionTableEntry initializer. - decompressed.cpp pattern loop: every unique variant now gets synthetic ram_addr = 0xC0000000 + variant_idx * 0x100000 (1 MB stride, ~286 KB largest observed variant). For Stadium's 279 unique variants the pool occupies 0xC0000000..0xCDB00000, well within the runtime-side 512-bucket capacity. Co-Authored-By: Claude Opus 4.7 (1M context) --- include/recompiler/context.h | 10 ++++++ src/decompressed.cpp | 68 ++++++++++++++++++++++++++++++++++-- src/main.cpp | 4 +-- 3 files changed, 77 insertions(+), 5 deletions(-) diff --git a/include/recompiler/context.h b/include/recompiler/context.h index afc7515..a53c174 100644 --- a/include/recompiler/context.h +++ b/include/recompiler/context.h @@ -113,6 +113,16 @@ namespace N64Recomp { // recomp_overlays.inl's SectionTableEntry.content_hash. Computed // as FNV-1a-64 of the first 0x40 bytes of the section's body. uint64_t content_hash = 0; + // Original game-side fragment id for pattern variants whose + // section.ram_addr was reassigned to a synthetic per-variant + // identity (e.g. a pattern variant for stadium_models keeps + // original_pattern_id=0xEF even though its ram_addr is in the + // synthetic-vram pool). Allows the runtime + // register_runtime_fragment to know "this synthetic-pool section + // is a candidate for game id X" without having to derive it from + // ram_addr (which no longer encodes the original id). + // 0xFFFFFFFF means "not a pattern variant" (no override applies). + uint32_t original_pattern_id = 0xFFFFFFFFu; }; struct ReferenceSection { diff --git a/src/decompressed.cpp b/src/decompressed.cpp index cfc42e4..8f65ce5 100644 --- a/src/decompressed.cpp +++ b/src/decompressed.cpp @@ -480,8 +480,23 @@ size_t add_decompressed_section(Context& context, uint32_t vram, const std::string& section_name, bool relocatable, - uint64_t content_hash) + uint64_t content_hash, + uint32_t override_link_ram_addr = 0, + uint32_t original_pattern_id = 0xFFFFFFFFu) { + // `vram` is the BYTES-ENCODED vram — what the body's R_MIPS_HI16/LO16 + // / R_MIPS_32 / J/JAL targets are encoded relative to. The CFG walker + // and reloc parser need this value (otherwise jump-table entries + // resolve to the wrong section, etc.). + // + // `override_link_ram_addr` (if non-zero) is the section's LINK + // IDENTITY — what gets stored in section.ram_addr and used at + // runtime as section_addresses[N]'s initial value. For pattern + // variants we want this DIFFERENT from `vram` so multiple variants + // can have unique link identities while sharing the canonical + // bytes-encoded vram. + const uint32_t link_ram_addr = + (override_link_ram_addr != 0) ? override_link_ram_addr : vram; if (blob.size() < 0x20) { std::fprintf(stderr, "decompressed: section %s blob smaller than FRAGMENT header\n", @@ -544,13 +559,19 @@ size_t add_decompressed_section(Context& context, Section section{}; section.rom_addr = synthetic_rom; - section.ram_addr = vram; + // Section identity (link_ram_addr) may differ from the bytes-encoded + // vram (`vram`) for pattern variants that get assigned a synthetic + // per-variant link identity. The reloc parser stays with the + // bytes-encoded vram so target_section_offset values are correct + // intra-section byte distances. + section.ram_addr = link_ram_addr; section.size = reloc_offset; section.bss_size = 0; section.name = section_name; section.executable = true; section.relocatable = relocatable; section.content_hash = content_hash; + section.original_pattern_id = original_pattern_id; if (!parse_fragment_relocs(blob, vram, section_index, section)) { return size_t(-1); @@ -816,6 +837,29 @@ bool synthesize_decompressed_patterns( std::unordered_map seen_hashes; size_t added = 0; size_t deduped = 0; + // Path 2: every unique pattern variant gets its own synthetic + // per-variant ram_addr in the 0xC0000000+ sentinel pool. The + // bytes-encoded vram (parsing/CFG) stays at p.vram for ALL + // variants — only section.ram_addr (the link identity) changes, + // so each variant's RELOC_HI16/LO16 macros emit a unique + // 0xCXXXXXXX literal at runtime and the synthetic resolver + // can translate that literal back to the variant's runtime + // RDRAM buffer. + // + // Pool placement: 0xC0000000 is KSEG2, unused by N64 software, + // so the sentinel is "obviously invalid as an N64 vaddr" and + // can only be handled by the recomp synthetic resolver. KSEG1 + // (0xA0000000) was tried first but collides with the engine's + // RSP code section at 0xA4000040. + // + // Stride 0x00100000 = 1 MB per variant. With Stadium's 219 + // variants, the pool occupies 0xC0000000..0xCDB00000 — well + // within the 256-slot capacity (kSyntheticBucketCount on the + // runtime side). If the variant count grows past 256 in some + // future game, both sides need to bump the pool size. + const uint32_t kSyntheticPoolBase = 0xC0000000u; + const uint32_t kSyntheticPoolStride = 0x00100000u; + size_t probe_variant_idx = 0; for (auto& [wrap_off, body] : hits) { const size_t window = std::min(HASH_WINDOW, body.size()); const uint64_t content_hash = @@ -827,11 +871,29 @@ bool synthesize_decompressed_patterns( } seen_hashes.emplace(content_hash, wrap_off); + // Original game-side fragment id derived from the pattern's + // canonical bucket. All variants of this pattern share the + // same original id (e.g. 0xEF for stadium_models). Stored + // on each section so the runtime can filter synthetic + // candidates to the matching id and avoid cross-pattern + // hash-collision misregistration. + const uint32_t orig_id = + ((p.vram & 0x0FF00000u) >> 0x14) - 0x10u; + + // Per-variant synthetic link identity. The bytes-encoded + // vram (used for parsing/CFG) stays at p.vram — only this + // link identity changes per variant. + const uint32_t variant_link_addr = + kSyntheticPoolBase + + uint32_t(probe_variant_idx) * kSyntheticPoolStride; + probe_variant_idx++; + const std::string section_name = fmt::format( "{}__rom_{:X}", base_name, wrap_off); size_t si = add_decompressed_section( context, body, wrap_off, p.vram, - section_name, p.relocatable, content_hash); + section_name, p.relocatable, content_hash, + variant_link_addr, orig_id); if (si == size_t(-1)) { // Hard failure: the section's bytes can't be bounded // by our CFG walk (or some other unrecoverable parse diff --git a/src/main.cpp b/src/main.cpp index fd65bff..cd83aa0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1027,10 +1027,10 @@ int main(int argc, char** argv) { std::string section_relocs_array_size = section_relocs.empty() ? "0" : fmt::format("ARRLEN({})", section_relocs_array_name); // Write the section's table entry. - section_load_table += fmt::format(" {{ .rom_addr = 0x{0:08X}, .ram_addr = 0x{1:08X}, .size = 0x{2:08X}, .funcs = {3}, .num_funcs = ARRLEN({3}), .relocs = {4}, .num_relocs = {5}, .index = {6}, .content_hash = 0x{7:016X}ull }},\n", + section_load_table += fmt::format(" {{ .rom_addr = 0x{0:08X}, .ram_addr = 0x{1:08X}, .size = 0x{2:08X}, .funcs = {3}, .num_funcs = ARRLEN({3}), .relocs = {4}, .num_relocs = {5}, .index = {6}, .content_hash = 0x{7:016X}ull, .original_pattern_id = 0x{8:08X}u }},\n", section.rom_addr, section.ram_addr, section.size, section_funcs_array_name, section_relocs_array_name, section_relocs_array_size, section_index, - section.content_hash); + section.content_hash, section.original_pattern_id); // Write the section's functions. fmt::print(overlay_file, "static FuncEntry {}[] = {{\n", section_funcs_array_name);