An extra actor's saved bytes are now copied INTO the storage the game keeps its
player in for the duration of that actor's pass, then copied back out. Every
access then finds the right actor however it is addressed: through a pointer,
absolute, $gp, a pointer some global cached frames ago, or from streamed
overlay code the recompiler never saw.
That deletes [coop] actor_base_sites entirely. Enumerating every instruction
that forms the actor pointer needed per-game knowledge the list could never be
proven complete against, and silently missed whatever it did not cover: on MMX6
damage landed on player 1 until one more site was found by watching a companion
get chewed on off-camera. config_loader now THROWS on the retired key rather
than ignoring it, so a config written against the old model cannot quietly stop
working.
Per-actor state the engine keeps outside the struct is declared with
psx_coop_add_swap_region() / TCP coop_region, so extending the swap set never
needs new generated code. Which regions those are is a measurement, not a
guess: every recorded write now carries the actor context that made it.
Two bugs fixed along the way:
- The replay hook returned to the jal address (delay slot - 4). Only BLOCK
LEADERS are dispatch entries, and the jal is one only when it happens to sit
at a previous call's return address. MMX6's sprite-submission stage has its
jal at 0x80020D00 inside block_80020CF8, so that stage's replay had never
actually been happening. The generator now emits the enclosing leader.
- Input delivery no longer writes a shared input global behind player 1's back
(which left player 1 reading zero while a direction was held). Instead
psx_coop_set_raw_pad() puts each actor's RAW pad where the game's controller
poll leaves it, inside a declared swap region (enforced), and the engine's own
replayed pad stage decodes it. That matters because engines do not store the
pad in SIO bit order: MMX6 remaps every button, so an actor fed SIO-order bits
reads LEFT as JUMP. Wire order (byteswap of the inverted SIO word) is the
console's pad transfer format, not a per-game table.
Observability, all found by a measurement lying rather than by an audit:
- Enhancement-scratch stores were outside every write hook, so a RAM-claim
report said "nothing written here" about a region written every frame.
- The RAM read watch saw only 32-bit reads, so a 16-bit field read with LHU
produced zero hits, and it carried no PC at all. Block leaders now stamp
g_debug_last_block, which the .ranges manifest resolves to a function.
- The read watch could only be armed from a boot-time environment variable,
i.e. decide-what-to-watch-before-the-run. Added a live read_watch command.
- read_ram/write_ram raced the swap: the debug thread landed on whichever actor
was momentarily resident, which manufactured false conclusions rather than
noise. psx_coop_observe_ptr resolves an address to its owner's bytes.
Co-op off remains byte-identical: omit the [coop] block and nothing is
generated; leave the runtime unconfigured and every hook is inert.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three generic, default-off runtime capabilities that together let a 2D action
game run more than one simultaneously-controllable player actor. Nothing here
activates on its own: with no game opting in, every path is inert and behaviour
is unchanged.
1. Enhancement scratch RAM (memory.c, psx_runtime.h)
Guest-addressable memory ABOVE the 4x main-RAM mirror (phys >= 0x00800000),
a window that is unmapped on real hardware, so the game can never write it.
This exists because the obvious alternative does not work: MMX6's documented
free heap->stack gap turned out to be the CD-DMA stage-load arena, and a
reservation there was overwritten wholesale on the first stage load (measured
with the always-on RAM-write ring; DMA ch3 covered every word). The checks sit
AFTER each accessor's `phys < RAM_SIZE` fast path, so ordinary main-RAM
traffic pays nothing at all and a game that never calls configure() is
byte-identical. TCP: enh_scratch.
2. Per-slot debug input overrides (main.cpp, debug_server.{c,h})
set_input/press/clear_input take an optional `slot` (0 = port 1, default;
1 = port 2). Previously a single shared override meant arming one port
STARVED the other: sample_pad_into_sio early-returned, so the untargeted slot
froze holding its last injected value. State is now per-slot arrays, and a
slot with no override falls through to normal host sampling. Old wire format
is unchanged (absent `slot` => 0). Live-validated: both slots held
independently, selective clear, back-compat, bad-slot rejection.
3. Per-actor stage replay ([coop] config, code_generator, coop.c)
A game's per-frame player update is a SET OF STAGES in its frame pipeline,
only some of which are per-player. `replay_sites` names the delay slot of
each per-actor stage's jal; the generator rewrites the link the CPS emitter
just stored so the callee returns to its own jal and the stage repeats for
the next actor, then takes the natural link once the list is exhausted. That
is the guest's own return path -- deliberately NOT a native call into guest
code, which does not return under CPS and silently skips any restore, latching
the actor selection (observed live). `actor_base_sites` names every addiu that
forms the actor pointer so each pass operates on the selected actor, and
arg-taking stages get it in a0 on selection because they inherit it from the
pipeline driver, which runs only once per frame.
A CONTIGUOUS SPAN IS THE WRONG MODEL and the config cannot express one: the
pipeline interleaves per-actor and shared stages, and replaying a span that
swept in one shared stage ran it twice per frame and corrupted the game --
the player's spawn beam drew as a column of duplicated sprites and physics
stepped twice, dropping him through the floor.
Sites are opcode-validated and fail the build on mismatch; a partially
declared [coop] block throws. Omitting the block emits nothing at all, which
is the pristine standard-asset build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rename out-of-line load helpers to *_slow to match psx_cyc.h inlines,
declare psx_icache_fetch/reset, and wire overlay callbacks to slow paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
Let the frontend remain the sole frame pacer by requesting MAILBOX for tear-free delivery, with FIFO retained as the required fallback.
Co-authored-by: Martin Penkava <mpenkava1337@gmail.com>
Add opt-in [widescreen.cull] xclip_load_sites: a configured `lw rt,off(rs)`
loading a per-primitive X-reject bound (sltu-compared against ANDI-masked
u16 screen X) routes through psx_ws_xclip_bound() - INT32_MAX while the
margins are revealed (reject disabled; the wide-surface scissor clips the
overflow, wrapped off-left u16 coords pass), vanilla value at 4:3. Matched
native codegen emit and interpreter hook; overlay ABI v18 forwarder (NULL
preserves the vanilla loaded value); codegen ver 8. Parser, codegen and
overlay-mismatch regression coverage.
Split from #49: replaces the xclip_globals RAM poke with a recompile-time
instruction transform so native shards and the interpreter behave identically.
Add opt-in [widescreen.cull] plane_nx_sites: a configured `lw rt,off(rs)`
loading the X component of a side frustum-plane normal (sign-tested per
corner, dot = nx*px + nz*pz) routes through psx_ws_plane_nx() — scaled by
the inverse aspect factor (4*den)/(3*num) while the margins are revealed,
widening the plane cone by exactly atan((3*num)/(4*den)*tan(theta));
identity at 4:3. Matched native codegen emit and interpreter hook; overlay
ABI v17 forwarder (NULL preserves the vanilla normal); codegen ver 7.
Parser, codegen and overlay-mismatch regression coverage.
Split from #49: replaces the plane sign-test RAM pokes with a
recompile-time instruction transform so native shards and the interpreter
behave identically.
Reconstruct the shareable pieces from PR #58 onto current master: add contiguous MDEC DMA input bursts, reduce per-pixel/per-byte MDEC output overhead, add sparse IDCT fast paths, and keep the MDEC activity stamp sane across rematches.
Also quiet the heavy debug trace recorders while MDEC video is active so debug builds can query state without starving FMV playback.
Regression testing was done with non-debug OpenGL builds across multiple FMV-heavy titles. Debug-tool FMV playback was also exercised with the quiet gate enabled.
Co-authored-by: Alex Vanderveen <alex@technicallycomputers.ca>
Co-authored-by: Cursor <cursoragent@cursor.com>
Restore user savestates to their saved CPU PC instead of the game entry PC, and clear non-serialized CPU/GTE completion deadlines after loading a state so stale absolute guest-cycle deadlines cannot stall the first post-restore unit read.
Debounce load requests for 60 frames after a successful restore. This prevents repeated load requests from stacking multiple restore/recovery cycles into a long apparent softlock while keeping normal save/load behavior unchanged.
Regression testing was done. Caveat: restores can visibly pause, especially near CD/load transitions, and worst-case recovery may take several seconds; observed long recovery cases eventually resumed rather than crashing or permanently hanging.
Co-authored-by: Henrique Guedes <henrique.guedes@porttus.com>
Register configured range_sites with the dirty-RAM interpreter so overlay-resident classifiers use the same two-margin widening already emitted by native codegen.
Co-authored-by: Opok <novelasevilliousesp@gmail.com>
Add opt-in slti/sltiu far-bound scaling with matched native and interpreter semantics, correct immediate sign extension, and an overlay ABI forwarder for live aspect state.
Co-authored-by: Opok <novelasevilliousesp@gmail.com>
Landed while root-causing the Tomba 2 warm-cache boot death; each of these
was a hole that blocked attribution on the production binary:
- psx_fatal_halt: halt-and-serve is now keyed on the TCP listener being
live (debug_server_get_status) instead of the build flavor. A production
run with PSX_DEBUG_SERVER=1 + PSX_EXIT_HALT=1 now stays inspectable at
the fatal instead of exit(1)ing the evidence away.
- debug_server_get_status: was declared in debug_server.h but never
implemented; implemented (listener state, port, saved socket error).
- event_ring: EVENT_RING_ENABLED=1 unconditionally. The ring records
edges only; its production disable predated the production listener and
contradicted its own ALWAYS-ON design note.
- sp_ring (new): stack-domain (64 KB) transition ring at fntrace_record --
one entry per dispatch whose guest SP crossed a domain, i.e. the
provenance record for 'who installed this stack pointer'.
- disp_ring (new): 128-entry unconditional dispatch tail (target/ra/sp/
cycle) so the final control-flow sequence before a death is always
reconstructable post-mortem.
- overlay_loader: the native-call ring (overlay_native_ring recent[]) is
now recorded in production too -- each entry names the requested PC and
the claiming candidate CRC, which is what finally attributed the
wrong-variant native run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add an opt-in horizontal low-edge transform for subu rd,zero,rt classifiers, with matching native and dirty-RAM interpreter behavior and guarded overlay variants.
Co-authored-by: Opok <novelasevilliousesp@gmail.com>
Audit fixes for the async shard-publication pipeline (45a2dda):
- autocompile_shutdown(): stops the pipeline, kills the compiler tree via a
kill-on-close job object (assigned while the child is SUSPENDED so cmd.exe
cannot spawn grandchildren outside it), and JOINS the watcher + preparer —
never abandoning a thread inside the Windows loader lock at ExitProcess.
Wired first into shutdown_runtime(). Crash exits reap the tree via the job.
- Failed off-thread prepares retry (3 attempts, 100 ms) on the worker before
the artifact silently reverts to a synchronous emu-thread first-map; retry
and giveup counters exposed in autocompile_status_json.
- Capacity caps fail closed and LOUD: cand_register shouts once at CAND_CAP;
load_one_dll_prepared refuses at MAX_LOADED_DLLS before registering, so
dll_already_loaded can never lose track and double-register on rescan.
- Publication confinement is filesystem-resolved: published_path_valid
canonicalizes the cache root and the pipe path (GetFinalPathNameByHandle /
realpath), prefix-checks canonical forms, and the worker maps the canonical
string while holding a no-delete-share guard handle (TOCTOU). `..`/junction
spellings that passed the lexical check are now rejected — proven by the
new overlay_path_canon_test boundary test.
- test_autocompile_publication gains a shutdown-mid-run scenario asserting
prepared == committed + discarded with nothing queued; perf guards updated
to require validate -> guard-handle -> canonical first-map ordering.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two provably-correct framework fixes (independent of the AOT work) plus the
play-free overlay-extraction spike tooling and design doc.
Framework fixes (accurate, safe, complete):
- ABI v14: add gte_precision_store_word to OverlayCallbacks + preamble shim +
runtime population. The emitter emits a direct call for every swc2 (GTE
store-word); it is stateful runtime code (gte.cpp) so overlay DLLs must
forward or fail to LINK (undefined reference). Play-captured shards rarely
compiled a swc2; fuller static coverage surfaced the gap. Same pattern as
the v13 ws_player_x_bound / v10 gte_read_ctrl shims.
- Gate splitgen OFF in overlay mode (main_psx.cpp): the current recompiler's
unconditional split-TU output deletes <stem>_full.c and emits only shards,
which broke ALL overlay compilation (compile_overlays.py reads _full.c).
Overlays are single small TUs with no parallel-compile benefit; emit the
monolith. Restores overlay compile on the current tree.
AOT spike (docs/AOT_OVERLAY_PLAN.md + tools/aot_overlay_spike/, WIP):
- Play-free static overlay extraction from the disc image. Validated:
Tomba 1 reproduces 90% of played functions + ~1964 unplayed, live-verified
in-game (Dwarf Village runs native, zero runtime compile). Tomba 2 yields
661 functions byte-identical to the shipped/played vault (provably real),
all from the self-describing MAIN.EXE producer; the header-table BIN
producer needs loader-table RE (base recovery unreliable). Generality is
producer-dependent. Spike scripts are Tomba-specific proofs of concept
with hardcoded paths, not a finished tool.
- Correctness is guaranteed by the existing per-function code_crc dispatch
guard: a mis-positioned shard fails audit at compile or never fires at
runtime -> coverage loss, never wrong execution. Autocompile self-heals gaps.
CROSS-CUTTING: the ABI v14 change is part of the overlay codegen-hash source
set, so it ROLLS the cg tag (1f2ad026 -> e8484213). On master + a fw bump,
every title's overlay cache invalidates and needs regen+reshard. Branch only;
NOT pushed; other games' caches untouched. Needs user validation before merge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The tier mis-compiled and has been hard-gated off since 2026-06-25; overlay
gaps fall to gcc/tcc shards and the dirty-RAM interpreter, unchanged.
Re-do on master of the removal validated on the stale branch (6da8c02),
reconciled with everything master grew since the fork:
- Delete overlay_sljit.{c,h}, overlay_compile_worker.{c,h}, lib/sljit,
runtime/tests/sljit_*.c, and the runtime.cmake sources/include-dirs.
- Strip the sljit provider/backend from code_provider.{c,h},
overlay_backend.{c,h} ("sljit" in overlay_backend/PSX_OVERLAY_BACKEND now
degrades to auto so stale configs still boot), main.cpp, debug_server.c
(sljit_status/sljit_async/sljit_try TCP commands), overlay_loader.{c,h}
(live policy, JIT-on-miss, persisted-shard cache, async worker glue,
psx_sljit_call/cop2/memx host helpers — reachable only via the removed
cpu->sljit_helpers table), and cpu_state.h (SLJIT_HLP_* enum + the
sljit_helpers[] member; CPUState layout change reshards via the cg tag).
- Master-only entanglement resolved before deletion: 28165d6's Einhander WS
player-X bound emit existed in three paths (gcc code_generator, sljit,
dirty_ram_interp). The gcc shard link contract now forwards
psx_ws_player_x_bound (ABI v13, previous commit), so deleting the sljit
emit path loses no widescreen behavior; ffc6688's game-dispatch block
inside the dead psx_sljit_call helper goes with it.
- Reword comments that described sljit as a live backend (gpu.{c,h},
ws_*_detect.h, cosim_state.c, dirty_ram_interp.c, autocompile.h,
boot_state.h, config_loader.h, compile_overlays.py, and the CPS marker
comments the recompiler emits).
recompiler psxrecomp-game builds clean; game runtimes rebuilt + boot-verified
separately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
28165d6 (Einhander native-wide player bounds) rewrites configured
[widescreen] signed_x_bound LUI sites into psx_ws_player_x_bound(vanilla)
calls in code_generator.cpp — and that site loop runs in overlay_mode too, so
the call lands in overlay-resident code. But the shard link contract never
forwarded the symbol: any shard containing a bound site fails to LINK
(undefined psx_ws_player_x_bound) and that variant runs interpreted forever —
the same silent class as v10 gte_read_ctrl and v12 psx_rfe_mark_escape (now
loud under the shard observability suite, but still a coverage hole).
The clamp computes against the host's live widescreen state (gpu.c), so a
local copy can't work: add the ws_player_x_bound callback (appended last),
the preamble forwarder (identity fallback => correct 4:3 on a host that
predates it), and the loader init wiring. Bump the ABI to 13 so stale DLL
mixes are rejected and regenerate.
This also unblocks removing the sljit tier: sljit's emit_ws_player_x_bound
was the only other native path carrying the clamp; with the gcc shard path
forwarding it (and dirty_ram_interp already handling the site), the feature
no longer depends on sljit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re-land of the shard-observability suite from 6da8c02 (stale branch), merged
against current master (keeps master's overlay_ext() platform suffixes,
recompiler_patch hash sources, and the unconditional v12 rfe forwarder).
- compile_overlays.py: ShardStats tallies every shard outcome — per-shard
"SHARD FAIL [class]" lines, a SHARD BUILD SUMMARY, a machine-readable
"PSX_SHARD_RESULT ok=N failed=M skipped=K" line, and a non-zero exit when
any shard that should have built failed (previously a run that built zero
shards exited 0, indistinguishable from a clean run).
- New --check preflight: build every shard into a throwaway dir and report
pass/fail — answers "would this game's shards compile against the current
recompiler + headers?" without a runtime.
- Retain audit-failed interior-fragment C (<phys>_<interior>_fragment_FAILED.c)
for triage, plus a persistent doomed-interior memo (interior_fail_memo.txt)
keyed (phys,interior,region_crc) so autocompile stops re-walking
data-as-code fragments every cycle (~90x off the V8 fragment pass).
- Extract DISPATCH_PREAMBLE to runtime/include/overlay_dispatch_preamble.c.inc
(single source of truth for the shard link contract, loaded by
load_dispatch_preamble(); missing file is a hard error).
- codegen_hash_sources.cmake: fold the shard COMPILE surface (overlay_api.h,
cpu_state.h, the .inc) into the cg tag so header/ABI/link-contract drift
reshards same-tree instead of silently breaking every new shard compile.
- autocompile.c: parse PSX_SHARD_RESULT from the child-output tail, expose
shard_ok/shard_fail/shard_skipped/shard_fail_total over autocompile_status;
ALWAYS rescan on completion so shards that did build load even when the
run also reported failures.
Live proof on the stale branch: Tomba's first autocompile surfaced 9 shard
link failures (undefined psx_rfe_mark_escape — since fixed by v12) and V8
surfaced 91 benign data-as-code fragment failures, all previously silent.
Codegen hash changes => every title reshards once on next build (expected;
loader rejects/regenerates stale shards).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>