EMI to overlay extract, cached/mapped lookup table, "resident-occupant" memo - #289
Draft
kerokline wants to merge 4 commits into
Draft
EMI to overlay extract, cached/mapped lookup table, "resident-occupant" memo#289kerokline wants to merge 4 commits into
kerokline wants to merge 4 commits into
Conversation
The static overlay path had no residency signal. overlay_page_gen only advances for pages set in overlay_watch_bitmap, and the sole callers of overlay_watch_set_range were the DLL loader's cand_register() and rebuild_lazy_manifest_index() -- both inert for an AOT-static title. So for a game dispatching through generated/overlays_static.c, every code range was unwatched, overlay_watch_pagegen_sum returned a constant, and the generation fast path in psx_overlay_static_code_matches answered every dispatch after the first from its cached result. The CRC gate was therefore consulted once per variant per process, not once per code change: the only thing that ever cleared the cache was a savestate restore. A band that swapped occupants kept dispatching the previously-validated variant. Arm the watch over a variant's ranges on the cold path, before hashing them. CD DMA reaches RAM word-by-word through psx_write_word, so an overlay load now advances the generation of every page it touches and the next dispatch re-hashes. Arming only sets bitmap bits and does not advance a generation, so the gen_sum already computed for the cache write stays valid. This is also the prerequisite for memoizing the resident variant per band: a generation change is the load event, and until now that event never fired. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
psx_overlay_dispatch is consulted on every interpreted entry, and the overwhelming majority of those calls are for addresses that are compiled nowhere. Measured on Breath of Fire III during a battle transition: ~264,000 calls/sec, of which roughly 1,160 out of every 1,161 found nothing. Emitting one `case` per address makes that miss path a binary search tree over tens of thousands of sparse cases, plus a jump table far larger than cache. So the cost scaled with how much overlay code was compiled even on screens where none of it ran, and frame rate tracked dispatch-case COUNT rather than variant chain depth -- which is why adding bands cost frame rate for nothing. Replace the switch with a compile-time open-addressed hash table. The miss path becomes hash, one load, compare, return. Two parallel uint32 arrays keep a miss inside a single table: the address array answers "is this mine?" without touching the entry or variant arrays. Load factor is held at <= 0.5. Behaviour is unchanged: on identical captures the emitted address set and variant count match the switch exactly (11,913 addresses / 12,522 variants for a three-band build; 10,545 / 10,545 for two-band), and per-address variant order is preserved, so the CRC gate still chooses the resident occupant. Verified offline: the C and Python hash functions agree on all 524,288 word-aligned addresses in the 2 MB RAM window; every table entry resolves to its own index; variant runs tile the variant array exactly; and no non-entry address produces a false hit. Measured headless, three bands, same savestate and same 200 s protocol: throughput 106.5 -> 113.2 emulated fps (+6.3%), p1 93.9 -> 105.2. At the transition the switch build absorbed 160,970 misses/sec at 63.9 fps while the table build absorbed 307,978/sec at 131.3 fps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On hardware the game CD-reads a file to a fixed address and jal's straight into it: identity is implicit in control flow and costs nothing. We rediscover it by walking a band's occupants and CRC-gating each in turn. On a deep band that walk dominates -- the memory-card screen measured 41.25 checks per hit, roughly 41 failed gates before the resident one. Remember which occupant last satisfied each address and try it first. The memo is a hint, never an authority: every call still passes through psx_overlay_static_code_matches(), so a band that swapped occupants fails the memo and falls into the full walk, which then re-seeds it. Correctness is therefore unchanged -- the memo only reorders candidates. Measured headless over an identical 140 s boot window, all ten bands compiled: build throughput chk/hit variant misses all-bands 99.0 1.479 368,479 all-bands + memo 131.4 1.069 81,505 +33% throughput, 4.5x fewer wasted gate calls. It is neutral where chains are already shallow (a savestate-loaded combat workload measured 1.019 chk/hit before the memo and showed no gain), so it pays exactly where it was aimed. Together with the O(1) lookup this overturns the earlier "do not compile all bands" result: all ten bands now beat a three-band build on both workloads measured -- 131.4 vs 107.9 at boot, and parity on the savestate workload -- because more compiled code means more native execution once dispatch is cheap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…y-residency-signal
Contributor
Author
|
Left in draft so you could review - Its an alternate overlay derivation path, but it shouldn't impact the older path, its just if someone finds it would work for whatever game they're working through. In draft for the moment while I get further in the game (to see if performance stays steady) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR: For Breath of Fire 3 and 4, the .EMI layout is the game's own decomposition; runtime loses the relational grouping and leaves you reverse-engineering it later.
The framework's usual overlay bringup records overlays while someone plays. For most titles that's the only option. For Breath of Fire, and potentially others, there is a another route in the .EMI files.
Capture records overlays as they're hit: an executed address, the bytes resident at the time, reached along whatever path the player walked. Thread that across a playthrough of an RPG — town, field, encounter, menu, shop, dungeon, boss, and every branch between — and you accumulate a long, tangled chain of entrance points strung along the game's flow. It's a real record of what happened. But every node in it is keyed by address and play-path, never by module. You'd know the game went from one segment to the next; you would not know that these forty entry points are all one area script, those nineteen are one character's module, these belong to the shop system. The reason segments belong together is the one thing capture can't see — it's metadata in the disc's archive layout, gone the instant the bytes land in RAM.
Three things make that especially costly here: the branching factor is enormous, address reuse is aggressive (181 different area scripts share a single RAM address, indistinguishable to capture), and coverage is unprovable — you can never be sure you visited every optional area. The result is behavior without architecture: a trace that the game did these transitions, but no answer to "what are its subsystems?" — the exact unit you need for modding, performance work, or extension.
Breath of Fire III's .EMI containers already carry that answer. Each one's table of contents states every section's RAM destination, and the sections are stored contiguously — so the overlay set, and its grouping, is readable offline as a build input.
Compiling from that flat list initially introduced a good bit of latency while transitioning between gamestates, hence the need for the hashmap. The initial idea, steps taken, and per-session rationale, are covered in the attached OVERLAY_EXTRACTION.md
OVERLAY_EXTRACTION.md
§§8–12.
Blast radius: static-overlay path only. The runtime change (aa6fa2c) is confined to psx_overlay_static_code_matches(), which is unreachable without PSX_HAS_OVERLAY_DISPATCH; both codegen changes (69d783f, 7015317) are confined to generate_overlay_dispatch(), emitted only under --static. The DLL-loader capture path used by default bringup method is not modified — capture-only titles compile and dispatch exactly as before.