Skip to content

EMI to overlay extract, cached/mapped lookup table, "resident-occupant" memo - #289

Draft
kerokline wants to merge 4 commits into
mstan:masterfrom
kerokline:fix/static-overlay-residency-signal
Draft

EMI to overlay extract, cached/mapped lookup table, "resident-occupant" memo#289
kerokline wants to merge 4 commits into
mstan:masterfrom
kerokline:fix/static-overlay-residency-signal

Conversation

@kerokline

Copy link
Copy Markdown
Contributor

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.

kerokline and others added 4 commits August 31, 2026 06:27
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>
@kerokline

Copy link
Copy Markdown
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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant