Reworks the `dis --db` static-analysis pipeline. Everything here is derived
from the XEX alone; no run traces.
Completeness
- `.pdata` is now authoritative for function boundaries rather than a hint:
all 23,073 linker-declared RUNTIME_FUNCTIONs are emitted and `end_address`
is the declared end, not a prologue-walk guess. Adds tail-call targets
(with a preceding-terminator fallback for the ~450 KB of .text `.pdata`
leaves unclaimed): 12,156 -> 25,676 functions.
- New `jumptables.rs`: 394 switch tables / 6,356 case edges recovered from
941 `bctr` sites. Table words are flagged `instructions.is_data` and kept
out of the xref pass, and render as `.long` with case labels in `-o`.
- New `rtti.rs`: RTTI is not stripped in this title. 611 type descriptors,
653 locators, 1,491 base-class records, giving a real inheritance graph
with mdisp/pdisp/vdisp. Verified against the STL:
std::out_of_range : std::logic_error : std::exception.
- New `xdbf.rs` + `xenia-xex/resources.rs`: the XDBF/SPA package the XEX
names via its resource table -- 24 achievements (1000G, matching the
project's independently derived figure), 888 localized strings across 6
locales, 25 PNGs, title name/id/version. Located through the entry table,
not a magic scan; a scan finds a phantom 7th string table where the entry
table declares 6, which shifts every scan-derived language index.
- Strings scan extended to `.data`; metadata grows from 5 keys to 41.
Correctness
- `instructions.function` was a rolling window -- set at each function start
and never cleared -- so every word in a `.pdata` gap was attributed to the
preceding function. 55,227 rows were wrong and the bogus 100% attribution
hid the gap. Now cleared at `end_address`; 0 wrong, honest 97.0%.
- Shift-JIS scanning accepted half-width katakana, which turned IEEE-754
float tables into "Japanese" (`3f 66 66 66` = 0.9f reads as "fff"), and
emitted escaped bytes rather than text. 837 mostly-noise rows -> 115 real
strings, decoded via encoding_rs, with resync so a run starting one byte
early reports the true address instead of mangling the first character.
- `indirect_dispatch_candidates` was a cross product, not a resolution: at
vptr_offset 0 the (offset, slot) match hits nearly every class, so 6,556
of 6,983 sites produced 1.80M of 1.81M rows (one site claiming 764
callees) and the derived ind_call edges were 84% of the xrefs table. Adds
`--max-indirect-candidates` (default 16); over-ceiling sites keep their
row with a truthful count and a new `truncated` flag but emit no candidate
rows and no xrefs. 1.81M -> 4,199 candidates, xrefs 2.16M -> 357k.
- xenia-xex: `TLS_INFO` and `DEFAULT_STACK_SIZE` header keys were swapped.
`get_stack_size()` would have returned the TLS descriptor's file offset;
it has no callers today, so nothing regressed.
Performance
- DuckDB autocommits per statement, so row-at-a-time INSERT paid a
transaction + WAL flush per row: 221k rows took 20 minutes and the 1.8M
candidate rows ~59 more, for an 81-minute build that never finished. One
big transaction fixes the time but not the cause (uncommitted state grew
to 16.6 GB RSS). Every table above a few thousand rows now uses the
Appender; each converted sink documents why its key cannot collide or
dedupes first. 2m20s wall, 486 MB peak, DB 635 MB -> 318 MB.
Also: 6 new SQL views, `zq.py` gains switch/switches/classes/class/str/
xdbf/ach, and an `analysis_report` example that runs the passes without
building a database. 80 tests pass (29 new).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>