Files
psxrecomp/tools/dump_overlays.py
T
Matthew Stanley b2929aa504 overlay: restore overlay DLL cache + capture (faithful port of codex/cache-discovery)
Reinstates the runtime-loaded overlay DLL cache (Layer A) and the offline
overlay compilation pipeline that were built on the pruned codex/cache-discovery
branch (tip 2bbafc2) and never merged. Ported onto current master; rebuilt
rather than history-restored.

Runtime (off by default, opt in via [runtime] overlay_cache = true):
- overlay_loader.c/.h, overlay_api.h: CRC32-keyed DLL cache at
  cache/<game_id>/<region>_<crc>.dll. On a dirty-RAM dispatch miss the loader
  LoadLibrary's the matching DLL, wires runtime callbacks via overlay_init(),
  enumerates func_XXXXXXXX exports, and dispatches them BEFORE the interpreter.
- overlay_capture.c/.h: write-once capture set; records overlay DMA bytes and
  writes overlay_captures.json (execution-verified seeds) at clean exit.
- dma.c: capture + cd_dma_log hook at CD DMA completion (async model).
- cdrom.c: cdrom_get_setloc_lba() for disc-position correlation.
- dirty_ram_interp.c: g_dirty_ram_exec_pc_table (exec-verified PCs), a
  decodability guard on JAL-into-dirty targets, and the overlay dispatch hooks.
- debug_server.c: cd_read_log + overlay_loader_status TCP commands.
- config_loader: [runtime] overlay_cache flag (default false).

Recompiler (compile_overlays.py / extract_overlays.py drive these):
- main_psx.cpp --overlay-exact: analyze only explicit entries + direct JAL
  targets and set split_mid_function_targets=false, so overlay basic-block
  targets are NOT split into bogus standalone functions (the mid-function-seed
  failure mode that produced black-on-load / blue-on-new-game).
- function_analysis.cpp: analyze_exact_entries(); code_generator.cpp: gate the
  mid-function split pre-pass behind split_mid_function_targets (default true,
  so normal main-EXE codegen is unchanged - Tomba still regens to 2854 funcs).

Deltas from the original: save states dropped (never worked); overlay enable is
runtime-config-gated (off by default) instead of a build-time define; loader
stderr logging converted to the overlay_loader_status TCP path (CLAUDE.md Rule 3).

Recovered from _backups/2026-06-06-consolidation/psxrecomp-all-PRE.bundle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 12:32:19 -07:00

150 lines
5.2 KiB
Python

#!/usr/bin/env python3
"""
dump_overlays.py — Extract executed overlay regions from a live runtime.
Queries overlay_dump (dirty_ram bitmap scan) and cd_read_log (CD DMA
transfer ring) to produce overlay_map.jsonl entries with:
crc32 — CRC32 of the RAM bytes (for verification)
load_addr — physical RAM address where the overlay lives
size — byte count of the region
start_lba — disc LBA of the first sector of this overlay file
The start_lba lets extract_overlays.py seek directly to the correct disc
position without brute-force scanning the whole disc image.
Usage:
python3 tools/dump_overlays.py [--port PORT] [--lo PHYS] [--dir DIR] [--out JSONL]
Defaults:
port 4470
lo 0x98000 (above main EXE text)
dir logs/SCUS-94236/overlays (relative to cwd)
out logs/SCUS-94236/overlay_map.jsonl
"""
import socket, json, sys, os, argparse
def send_cmd(port, cmd, **kwargs):
payload = {'id': 1, 'cmd': cmd}
payload.update(kwargs)
s = socket.create_connection(('127.0.0.1', port), timeout=10)
s.sendall(json.dumps(payload).encode() + b'\n')
data = b''
s.settimeout(30.0)
while True:
chunk = s.recv(65536)
if not chunk: break
data += chunk
try:
json.loads(data.decode())
break
except Exception:
pass
s.close()
return json.loads(data.decode())
def find_lba_for_region(cd_log, phys_addr, size):
"""Return the disc start_lba for an overlay region if known, else -1.
Groups consecutive CD DMA entries (same setloc_lba, dest advancing by
sector size) and returns the lba of the group whose dest range covers
phys_addr for at least `size` bytes.
"""
SECTOR = 2048
entries = cd_log.get('entries', [])
if not entries:
return -1
# Build file-load groups: consecutive entries where dest advances by 2048
# and lba is the same (all sectors of one file share the SetLoc LBA).
groups = []
cur = None
for e in entries:
dest = int(e['dest'], 16)
lba = e['lba']
sz = e['size']
if cur and cur['lba'] == lba and dest == cur['end']:
cur['end'] += sz
cur['total'] += sz
else:
if cur:
groups.append(cur)
cur = {'lba': lba, 'start': dest, 'end': dest + sz, 'total': sz}
if cur:
groups.append(cur)
# Find a group whose start <= phys_addr and end >= phys_addr + size
for g in groups:
if g['start'] <= phys_addr and g['end'] >= phys_addr + size:
# LBA offset for the exact start of the overlay within this file
offset_sectors = (phys_addr - g['start']) // SECTOR
return g['lba'] + offset_sectors
# Partial overlap — return the group start LBA as best guess
if g['start'] <= phys_addr < g['end']:
offset_sectors = (phys_addr - g['start']) // SECTOR
return g['lba'] + offset_sectors
return -1
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('--port', type=int, default=4470)
ap.add_argument('--lo', default='0x98000')
ap.add_argument('--dir', default='logs/SCUS-94236/overlays')
ap.add_argument('--out', default='logs/SCUS-94236/overlay_map.jsonl')
args = ap.parse_args()
os.makedirs(args.dir, exist_ok=True)
os.makedirs(os.path.dirname(args.out) or '.', exist_ok=True)
print('Querying overlay_dump...')
result = send_cmd(args.port, 'overlay_dump', lo=args.lo, dir=args.dir)
if not result.get('ok'):
print(f'ERROR: {result.get("error")}'); sys.exit(1)
regions = result.get('regions', [])
print('Querying cd_read_log...')
cd_log = send_cmd(args.port, 'cd_read_log', tail=4096)
print(f'Found {len(regions)} overlay region(s), {cd_log.get("total", 0)} CD DMA entries')
# Load existing manifest to avoid duplicates
existing = set()
if os.path.exists(args.out):
with open(args.out, encoding='utf-8') as f:
for line in f:
try:
e = json.loads(line)
existing.add((e.get('crc32'), e.get('load_addr')))
except Exception:
pass
new_count = 0
with open(args.out, 'a', encoding='utf-8') as f:
for r in regions:
key = (r['crc32'], r['addr'])
phys_addr = int(r['addr'], 16)
lba = find_lba_for_region(cd_log, phys_addr, r['size'])
lba_str = f'0x{lba:X}' if lba >= 0 else 'unknown'
if key in existing:
print(f' skip {r["addr"]} {r["size"]:>8} bytes lba={lba_str} (already logged)')
continue
entry = {
'crc32': r['crc32'],
'load_addr': r['addr'],
'size': r['size'],
'start_lba': lba_str,
}
f.write(json.dumps(entry) + '\n')
existing.add(key)
new_count += 1
print(f' new {r["addr"]} {r["size"]:>8} bytes lba={lba_str} crc32={r["crc32"]}')
print(f'\n{new_count} new overlay(s) appended to {args.out}')
if __name__ == '__main__':
main()