Commit Graph

19 Commits

Author SHA1 Message Date
Kevin b0e923dcd0 code_generator: resolve self-limited pointer tables with an unchecked index
The bounded-jump-table resolver requires the guest's own bounds check
(`sltiu/beq`) and index scaling (`sll`), because those are what prove the
table's extent. A decoder that keeps its state as a pre-scaled byte offset
and dispatches through `lui/ori base; addu base,offset; lw; jr` has neither,
so the jr fell back to a bare CPS tail-transfer and every entry it reached
that was not already a dispatch key ran on the dirty-RAM interpreter. On
Breath of Fire III that is the file decoder's state dispatch at
`0x80164AB0` (five entries; the first entry is the word right after the
table), the last boot-EXE row a play session still interpreted after the
computed-stride resolver.

resolve_self_limited_jump_table recovers the extent from the table's own
layout instead: words are taken from the base while each is a 4-aligned
in-function code address outside every delay slot, and the table ends
where its lowest target begins, since a pointer table cannot overlap the
code it points at. As with the stride resolver the emitter switches on
the actual runtime target with the CPS tail-transfer as default, so the
recovered entries only add native cases; an index past the recovered
extent still takes the fallback. Wired as the third fallback at both
code-generator sites and in the exact-entry discovery walker.

On the title's boot EXE it matches exactly one site (five entries); every
other shard is byte-identical. The synthetic test gains a second function
with this shape.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 14:16:20 -05:00
Kevin 6c98726a66 code_generator: resolve computed-stride jumps into unrolled runs (Duff's device)
A `jr R` with `R = base + index * stride` and no table in memory is how
unrolled copy loops enter their body in the middle (Duff's device). The
bounded-jump-table resolver only knows the `sltiu/beq; sll; addu; lw; jr`
chain, so the emitter fell back to a bare CPS tail-transfer; the trampoline
then found no dispatch key at the interior PC (straight-line code, no block
leader) and the dirty-RAM interpreter ran the rest of the copy on every
call. On Breath of Fire III that is the file decompressor's literal-copy
loop (`0x80164D1C`, 32 groups of `lbu/nop/sb`), which fires on every load
and was the whole non-kernel interpreted residual of a play session once
the kernel patch ranges (#346) and the call vectors (#381) ran native.

resolve_computed_stride_jump walks the chain back from the jr through
nearest definitions in straight-line code: `addu R, B, S` with S from
`sll S, I, k` or the two-sll sum `sll P,I,a; sll Q,I,b; addu S,P,Q`, and
then requires the code after the delay slot to be N >= 2 shape-identical
groups of `stride` bytes (same opcodes and register fields, immediates
free, no control flow inside a group). Targets are run_start + k * stride
for k = 0..N. The base register's value is never assumed: the emitter
switches on the actual runtime target with the CPS tail-transfer as the
default, so the recovered cases can only add native entries, never
redirect a jump that lands elsewhere. Wired into both code-generator
sites (interior labels + the switch) and the exact-entry discovery walker.

On the title's boot EXE it matches exactly one site, the decompressor loop
(33 entries); every other jr is untouched.

Adds recompiler/tests/test_computed_stride_jump_codegen.py (synthetic
PS-EXE with the idiom; asserts the switch, every case and the interior
labels), registered with CTest.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 14:16:20 -05:00
Matthew Stanley fe2c0046ba Recognize bounded MIPS switches with scheduled table constants 2026-09-11 13:38:06 -07:00
Matthew Stanley 504db26445 feat: add bounded indirect overlay discovery 2026-07-19 17:15:17 -07:00
Matthew Stanley 91c9aea91f aot: make discovery and shard publication fail closed 2026-07-18 14:08:45 -07:00
Matthew Stanley 495bf2de53 aot: constrain calls across composite producers 2026-07-18 02:56:42 -07:00
Matthew Stanley bd705bfcec Add bounded reachable main-EXE discovery
Adapt Douglas's opt-in reachable discovery onto current exact/static dispatch. Validate configured static-analysis bounds before truncation, preserve the whole-image default and canonical page reservations, and leave unresolved indirect or out-of-bound targets to runtime fallback.

Source: https://github.com/douglasjv/mm8

Source-commit: https://github.com/douglasjv/mm8/commit/8415f3a95458c41c7f48fbe36caaf0ee82730720

Co-authored-by: douglasjv <vanderveerdouglas@gmail.com>
2026-07-13 21:56:43 -07:00
Matthew Stanley 598d1dd551 Revert "recompiler: roll back AE pre-prologue function-start discovery (Tomba regression)"
This reverts commit b9e38416ca.
2026-06-25 21:29:35 -07:00
Matthew Stanley b9e38416ca recompiler: roll back AE pre-prologue function-start discovery (Tomba regression)
The Ape Escape forward-port (e0c819c) added pre-prologue setup detection to
function_analysis (find_preprologue_setup_start / preprologue_window_is_valid +
is_bios_dispatch_thunk), extending function STARTS backward to absorb preceding
lui/lw global-load setup. On Tomba this mis-bounds a function in the Village-of-
All-Beginnings init path (function count 5562 -> 5492, ~70 functions absorbed),
producing a main-loop softlock: New Game / loading the Village save wedges with
the main loop spinning on the 0xCF0 SIO poll, never rendering the area (a "blue
screen"). Backend-independent (gcc + tcc), not overlay/interp related.

This worked in v0.5.x (pin 5007c46, pre-AE). Revert function_analysis.{cpp,h} to
that state. Confirmed: the 0xCF0 wedge signature disappears (repro cf0_frac
1.00 -> 0.02) and Tomba boots/plays again. Ape Escape is not a public release, so
rolling back its discovery changes is safe; MMX6's existing release is unaffected
(its binary was already built), but a future MMX6 rebuild should be re-verified
against this discovery. A follow-up can re-introduce the pre-prologue heuristic
correctly (game-gated or with a tighter boundary check) without the over-extension.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:53:05 -07:00
Matthew Stanley 70c1eb8a51 Merge master (Bug B pacing fix + overlay cache campaign) into dev/ape-escape
Brings the ape-escape framework (real-EPC interrupt delivery,
g_psx_call_depth safe-point gating, continuation-entry codegen, DualShock
controller config, exception/entryint/irq-check rings, interp
dispatch_until + exit logging + static-text fallback) up to master
035a9fa (frame-pacing TOCTOU fix, vsync self-heal, watchdog classifier,
overlay JIT cache + autocapture/autocompile, Bug A interp call contract,
prod build hygiene).

Key reconciliations (ambiguity resolved toward master per plan):
- interrupts.c: real-EPC delivery (psx_check_interrupts_at) is the
  primary entry; the legacy psx_check_interrupts wrapper keeps the
  0x80000048 sentinel fallback (ape tip restored it), so old-flavor
  generated code keeps working interrupt delivery. Master's bottom
  compat shim removed (would have recursed). Both observability paths
  kept (event-ring gate outcomes + ape irq-check ring; new
  GATE_CALL_DEPTH/GATE_BAD_RESUME kinds).
- Claimed-IRQ cooldown: master's per-source CLAIMED_PROGRESS_QUANTUM=8
  (SIO exempt) over ape's unconditional 0; ape's 2026-06-05 starvation
  note kept as a MERGE NOTE — first knob to A/B if title-LOAD regresses.
  (User-validated: Tomba title-LOAD slot1/slot2 renders and plays on
  this merge — the historical ape-fw blackout is gone.)
- dirty_ram_interp.c: ape's dispatch_until_impl structure (stop_addr,
  static-text fallback, exit-log ring) with master's Bug A call contract
  (psx_dispatch_game_compiled + overlay_loader_call_native at interp
  jal/jalr), overlay loader/fingerprint entry checks, freeze-gated flow
  ring, chain-target attribution, and interp-active bracketing on the
  public wrappers. Ape tip's final local-flow rule kept: dirty game-text
  NEVER chains locally (the tip reversed its own earlier widening).
- freeze_heartbeat.c: classifiers composed — per-frame storm threshold
  (master) + progress-counter gating and 6s window (ape) + slow_frames
  kind (master), thresholds rescaled to the window.
- code_generator.cpp: ape's continuation-entry switch + master's
  refactored scan_jr_tables.
- debug_server.c: ape originals win over master's ported copies
  (bioscall ring, card trace with the richer address set); master-only
  tools kept (dirty_*_dump_file, card_trace filter handler).
- dma.c/cdrom.c: master's overlay-capture path + ape's DMA/command
  history diagnostics, both wired.

Builds: recompiler + Tomba runtime (legacy gen x merged runtime).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 22:05:13 -07:00
Matthew Stanley 1bbac45615 recompiler: overlapping-alias emission + static data-table seed scanning
Coverage campaign step 2. Interior code addresses reached through
function-pointer tables (e.g. Tomba's entity event pumps: 0x8004DF00
family inside func_8004DD1C, table at 0x80013F18) previously dispatch-
missed into the dirty-RAM interpreter, and seeding them truncated the
host function (mid-function-seed softlock class).

Discovery (main_psx): iterative loop classifying every candidate entry
against the current function set — function start: covered; interior:
ALIAS entry (host never capped); gap: forced entry (explicit seeds
trusted; scanned pointers need jr-ra/prologue boundary evidence). The
generic scanner sweeps all data words outside code ranges for text
pointers with table evidence (an adjacent text-pointer word).

Emission (code_generator/control_flow): aliases grouped per host into
ONE shared static body — entry switch + the union of blocks reachable
from any entry (successors + jr jump-table edges) — plus a dispatchable
wrapper per entry. Sibling entries are injected as block leaders so all
group CFGs are identical. Aliases are excluded from the split pre-pass
containment so they never truncate hosts; the pre-pass cap is raised
16 -> 256 with a loud non-convergence warning (16 no longer converged
once promotions widened the function set).

Tomba (SCUS-94236): 1441 alias entries in 108 host groups, 25 scan-
promoted entries; dispatch 2855 -> 5562; generated C 12 MB -> 15.3 MB
(naive per-alias duplication was 80 MB). Verified on the overworld
route: 0x4Dxxx family gone from dirty_ram_stats per-PC (was ~0.5M
insns/session), 0x8004DF00 called natively once/frame from
ra=0x800462C4 (fntrace), dispatch_miss_total=0, aborts=0, user-visible
gameplay clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 13:47:17 -07:00
Matthew Stanley 00b1b02a07 overlay: evidence-scoped --overlay recompiler mode (Inc1-A) + design doc
Restores entry-scoped overlay analysis (analyze_exact_entries +
split_mid_function_targets=false) as the MANDATORY mode for overlay inputs,
renamed --overlay-exact -> --overlay: the ingester's contract, set
unconditionally by compile_overlays.py for every overlay, never a human toggle.
Evidence-scoped discovery compiles only proven seeds + reachable code and keeps
branch/jump-table targets as in-parent labels — no whole-byte sweep, so it no
longer decodes embedded data tables as code.

Effect on the captured Tomba overlay: the data-as-code explosion collapses from
thousands of UNSUPPORTED to 122 clean functions with 42 residual unsupported
opcodes + 8 bad targets. Those residuals are now a per-function-audit concern
(Inc1-B), not a wholesale region failure. Main-EXE codegen is unchanged (Tomba
still regenerates 2854 functions; split pre-pass still runs in non-overlay mode).

Also adds docs/overlay-recompilation-design.md — the converged design of record
(precision-over-recall; compiled!=registered!=callable; write-invalidation over
CRC; internal-labels-in-parent; the RAM-write-path audit result; the three
implementation increments).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 13:42:10 -07:00
Matthew Stanley 520eebff5f overlay: remove --overlay-exact; compile overlays via normal analysis (pivot)
Rips out the --overlay-exact recompiler path (analyze_exact_entries,
split_mid_function_targets, the CLI flag, and the compile_overlays.py flag),
returning the recompiler to master's naive whole-blob analysis. Overlay
compilation now splits mid-function branch targets into standalone functions —
the original behavior that produced black-on-load / blue-on-new-game — so we
can faithfully reproduce the failure before fixing it.

Pivot in approach: rather than excluding mid-function seeds at scan time, the
plan is to capture everything (the full overlay image is already dumped to
overlay_captures.json) and have the ingester detect each seed's containing
parent function at encode time and merge/ignore mid-function seeds, while
flagging entries reached only via computed/indirect jumps as jump-risk.

The runtime overlay loader/capture system (b2929aa) is unchanged. The 5
recompiler files are restored byte-exact to master (d22c9ce); config_loader's
overlay_cache flag is retained.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 12:49:47 -07:00
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
Matthew Stanley 42bb73b6aa Ape Escape support: recompiler + runtime improvements (Codex session)
Recompiler:
- function_analysis: pointer-table entry discovery (scan executable word tables
  for function pointers, count as pointer_table_entry_count)
- function_analysis: generalize pre-prologue setup detection (multi-instruction
  windows, any load opcode, zero-immediate loads, branch-in-window guard)
- full_function_emitter: use psx_check_interrupts() (not _at) at interrupt
  safe points; fix g_psx_call_depth as extern (definition moved to interrupts.c)
- main_bios: add add_bios_vector_target_seeds() — augments seeds from ROM-
  resident BIOS vector tables so handler functions are always discovered
- main_psx: print pointer_table_entry_count in analysis summary

Runtime:
- dirty_ram_interp: RFE in delay slot handled correctly (exec_rfe + psx_rfe_return,
  not fall-through); exec_delay_slot returns int so callers can detect early-exit;
  all branch/jump delay-slot callers updated
- dirty_ram_interp: RFE in exec_one calls psx_rfe_return and returns 1
- dirty_ram_interp: interrupt check after nonlocal calls when cpu->pc == 0
- interrupts: g_psx_call_depth definition moved here (was in dirty_ram_interp);
  psx_check_interrupts handles pc==0/misaligned with sentinel 0x80000048
- cdrom: data_fifo_ready() helper; sector stream can refill buffer while IRQ is
  asserted for in-callback DMA; data-ready status bit uses data_fifo_ready()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 23:18:15 -07:00
Matthew Stanley ec7e482691 Ape Escape support: framework WIP snapshot (safe haven)
Uncommitted framework work on codex/ape-escape-support reaching the
post-boot DualShock-required screen. Recompiler codegen (code_generator,
full_function_emitter, function_analysis, config_loader) + runtime
(cdrom, sio, dirty_ram_interp, dma, iso_reader, mdec, gpu, memory,
traps, debug_server, fntrace, freeze_heartbeat, psx_cycles) and headers.

Committed as a restore point before rebuilding Tomba against this
framework. Build dirs (recompiler/build-codex*) intentionally excluded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:48:38 -07:00
Matthew Stanley ec4306fbb3 mmx6 framework: CD/DMA/MDEC + codegen fixes (Tomba builds+plays, MMX6 boots to intro)
Framework work on codex/mmx6-support that makes Tomba build and play
(title screen, 0 dispatch misses) and MMX6 boot through BIOS to its
intro. Touches recompiler codegen (code_generator, function_analysis)
and runtime CD-ROM/DMA/MDEC/GPU/SIO timing (cdrom, dma, mdec, gpu,
psx_cycles, traps, fntrace, debug_server) + headers + runtime.cmake.

Milestone snapshot for collapsing onto master. Build dir excluded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:06:02 -07:00
Matthew Stanley 25474410d0 recompiler: state-continuation discovery + psxrecomp-game build target
Add Pass 2.6 to FunctionAnalyzer: detect JAL targets whose callee starts
with `sw $ra, imm($a0)` (SaveState-style context-save helpers) and force
the post-delay-slot PC into the dispatch table. Recovers continuation
entries that aren't reached by JAL prologue or jr $ra discovery.

Extend find_function_start to recognize `lui rt; lw rt, off(rt)` 8 bytes
before jr $ra as a prologue marker. Rewrite analyze_function block-build
from std::set boundaries to a sorted std::vector + binary_search.

main_psx CLI: --seeds, --out-dir, --strict, --inspect; out_dir support.
Add psxrecomp-game executable target alongside PSXRecomp.

code_generator: reg_name() now emits cpu->gpr[N] consistently (matching
what the rest of the codegen already emitted — the old reg_names[] table
returning cpu->v0/cpu->ra was unreachable because CPUState has no named
fields, only gpr[32]). LWL/LWR/SWL/SWR moved from cpu->lwl(...) member
form to psx_lwl(cpu, ...) free-function form for game-EXE codegen reuse.

Remove a stray fmt::print left in build_basic_blocks that spammed
stdout on every regen — per the no-stdio-debug rule (memory v4).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:50:01 -07:00
Matthew Stanley 7c40d33db3 Phase 1 complete: recompiler proven correct on SCPH1001.BIN
Phase 1a (boot slice): 30 instructions at 0xBFC00000, compiles clean.
Phase 1b (instruction coverage): 54 opcodes implemented in strict_translator,
  44,629 walker-reachable instructions covered, 0 missing.
Phase 1c (function discovery): 76 functions from 2 seeds, 2,988 instructions,
  199 call edges, 27 indirect jumps. All validated through StrictTranslator.
Phase 1d (indirect control flow): 27 sites classified — 21 A0/B0/C0 dispatch
  stubs, 4 computed calls, 2 computed tail calls. Ghidra cross-referenced
  with 0 disagreements.
Phase 1e (relocation): ROM-to-RAM copy at FUN_bfc00420 proven with both
  Ghidra (static) and DuckStation (runtime). 69 address aliases, 68/69
  function entries match byte-for-byte in RAM. Normalization rule documented.

Includes PRINCIPLES.md and DEBUG.md adapted from recomp-template.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 21:26:04 -07:00