Commit Graph

535 Commits

Author SHA1 Message Date
Matthew Stanley 9037e7bd23 Add opt-in retained texture banks and enhancement memory snapshots 2026-09-13 16:50:26 -07:00
Aliet Exposito Garcia 72b3ead892 feat(OpenGL renderer): add post presentation gamma control
Some titles and enhancement mods/plugins might require display level gamma correction to improve shadow detail and overall visibility without altering guest rendering state. This introduces a public gamma adjustment interface for the OpenGL renderer via `gl_renderer_set_post_gamma`, that enables external plugins to adjust the display curve cleanly at runtime.

This adjustment applies exclusively to game content and temporal interpolation passes while leaving host interface elements, bezels, and previously composed frames untouched. Modifying the gamma setting immediately invalidates the presentation buffer so that changes become visible on screen without requiring new video memory writes from the guest system.
2026-09-13 01:30:10 -04:00
Alex Vanderveen b9ca600778 netplay: block list, account keys, list scope and automatch for the launcher
Brings the PSX runtime level with what recomp-ui's netplay pages now ask a
host for, and with the SNES runner that already answers them
(snes_lobby_client.c / snes_host_lobby.c):

- Chat lines, seat rows and players-online rows carry the server's opaque
  account key, so the launcher's ignore/block list survives a rename.
- psx_lobby_set_blocks pushes that list to the server as `set_blocks`, since
  refusing to pair or seat the two together is the server's job; recomp-ui
  re-sends it on every reconnect.
- list_scope_set: a LAN player no longer sees online rooms they have no
  connection for, and an online player no longer sees their own LAN rows.
- Automatch: rulesets discovery, the relay latency probe (14-byte LE frame,
  type 200 -> 201, nonce-matched), queue with the disc fingerprint, the
  accept gate with a locally counted deadline, requeue/cancel/cooldown, and
  claiming automatch's `error` codes only while a ticket is in flight.
  Leaving an automatch room after the match: it is the server's room, has
  no host to rematch with, and holding the seat blocks the next ticket.
- Account: init the auth client once per URL rather than every pump (the
  login worker reads the host), anchor the device key to the executable
  directory, and publish the signed-in handle as the lobby display name.

mods_enabled is asserted false on PSX: every netplay launch clears the mod
plan or refuses to start, and the caps a match runs are the ruleset's.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPXfGGhBBBxQPcEzaMsauF
2026-09-12 19:39:09 -04:00
Alex Vanderveen 615d35fe22 Merge remote-tracking branch 'origin/master' into feat/lobby-parity
# Conflicts:
#	tools/new_project_layout/project_studio/readme_metrics.py
2026-09-12 19:30:47 -04:00
Alex Vanderveen fddc8b8516 netplay: report chat lines for moderation, and pin recomp-net for it
PsxLobbyChatMsg gains the server's `mid` for a line, taken off `chat` and
`server_chat`, and psx_lobby_report_chat hands a list of them to
rnet_chat_report_build. The frame carries ids and never the text, so a
report cannot fabricate what somebody said. Same eight lines as the SNES
copy; what a report contains lives in recomp-net.

lib/recomp-net -> 859ba28 (support chat reporting), which is also where
rnet_account_set_secret_path comes from.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPXfGGhBBBxQPcEzaMsauF
2026-09-12 19:29:57 -04:00
Matthew Stanley 9576c6323f Refresh combined BIOS and dispatch validation against latest master 2026-09-11 23:39:47 -07:00
Matthew Stanley 63b205df78 Index immutable resident dispatch keys and clarify owner telemetry 2026-09-11 23:39:46 -07:00
Matthew Stanley b4ea4c37e9 Expand opt-in DMA arenas without aliasing CPU BIOS addresses 2026-09-11 23:05:07 -07:00
Matthew Stanley c5390e4265 Merge kernel patch ranges PR #346 for combined validation 2026-09-11 22:53:07 -07:00
Matthew Stanley 9550b3e579 Merge BIOS setup-host PR #343 for combined validation 2026-09-11 22:53:05 -07:00
Matthew Stanley 06a93dc8aa Forward widescreen hooks in native overlays and verify fallback intervals 2026-09-11 22:28:03 -07:00
Kevin 3cbe6c1cde kernel-bless: publish install-slot ranges so patched BIOS bodies run native
The BIOS exception handler ran interpreted for the life of every process,
1.22 billion instructions and 44.6% of all interpreted work in Breath of
Fire III, even though it is compiled and even though the stub that
unblessed it sat at exactly the address bios/SCPH1001.toml already
declared as an install slot. Two mechanisms existed and did not compose:
the emitter hooked the declared slot PC, while the runtime's bless
verifier memcmp'd the whole body including that slot, so the body was
MISMATCH forever and native code never reached the hook.

Three parts, none of which pays off alone:

1. Publish the declared ranges to the runtime. The emitter emits a
   PsxKernelPatchRange table next to the kernel body table and routes it
   through the backend descriptor; the bless verifier compares a body in
   segments that skip them. CLEAN now means "every byte outside every
   declared range still matches ROM", which is the claim the native code
   actually depends on.

2. Generalise the slot model to what the patches are. The old model
   assumed a 4-word lui/addiu/jalr/nop stub written over ROM zeros and
   fired on `word != 0`. Measured against both images, only one of five
   observed shapes fits: patchers also plant a `jr` over real ROM
   instructions, rewrite an 11-word prologue and insert an `mfc0 Cause`,
   and NOP out a driver routine. A slot is now a range with a length and
   an explicit `resume` (jalr / fallthrough / none), detected by
   comparing every word against its ROM-baked value.

3. Hand back from the interpreter at the range end. Kernel page 0 is
   permanently dirty, because the handler saves registers there on every
   exception, so straight-line interpretation would run the whole
   function after the patch. The emitter registers the resume PC as a
   continuation key and dirty_ram_interp surfaces there.

A PC inside a declared range must never be a native dispatch key: the
compiled bytes there are not what executes. Seven pre-existing
jal-return continuations violated that and wedged the boot at frame 0 in
an infinite dispatch loop (OpenBIOS RAM 0x357C). The emitter now drops
continuation keys inside a range so dispatch misses through to the
interpreter, and refuses to emit if a range covers a function entry,
which would silently remove a callable function instead.

Both profiles declare their measured ranges, and SCPH1001.toml finally
pins [program.image] sha256 to v2.2 (CRC32 37157331) so a v2.0 dump
refuses to generate rather than producing phantom discovery gaps.

Measured on Breath of Fire III (SLPS-00990, Psy-Q SDK 3.70), headless,
matched frames, PSX_KERNEL_PATCH_RANGES=0 for the OFF side:

                      OpenBIOS            SCPH-1001 v2.2
  bless mismatch      21 -> 0             74 -> 0
  kernel bodies       -87.7%/frame        -97.8%/frame
  all interp insns    -42.6%/frame        -94.2%/frame

The residual kernel-body work is exactly declared-words x entries on
every range, so nothing leaks past the hook. The A0/B0/C0 vector
trampolines are unchanged (+0.2%, noise): the profile excludes them by
design and they are a separate job. Card read traces are byte-identical
across the A/B on both images, all 32 entries, which exercises the two
ranges that replace the kernel's card handler.

Rule 18 holds throughout: the guest's patched instructions still execute
as written on the dirty-RAM interpreter. Only the code around them
changes backend. Nothing is synthesised.

Tests: runtime/tests/test_kernel_patch_ranges.c (new, the verifier's
accept/reject behaviour and the hand-back predicate) and the install-slot
range invariants in recompiler/tests/bios_address_model_test.cpp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 16:38:34 -04:00
Matthew Stanley 2113defcfc Fix overlay Clang exports, PGO, and scaffold cache 2026-09-10 17:23:03 -07:00
Alexandros Mandravillis c2f9c3d59a setup: detect any recompiled BIOS stem and stop the first-run setup loop
Focused port of the fork's fix/setup-host-bios-deadlock feature (10 commits) onto current upstream master, per the 2026-09-10 decision to port rather than rebase (the fork line and upstream share no merge base).

Content: the codegen host accepts any recompiled BIOS stem instead of only OpenBIOS/SCPH1001 (psx_bios_known_images.h); the runtime checks the setup host before validating a chosen BIOS and the first-run loop breaker reports a cause-specific reason; the CLI decodes subprocess output as UTF-8; --setup-selfcheck verifies a headless setup; the rbengine preflight names the recursive init. Two port conflicts resolved: the new CTest registrations in recompiler/CMakeLists.txt are kept (the names exist only in this change), and runtime.cmake takes the new recomp-ui guidance text. Cubic P3 finding fixed: the sibling PSX_NETPLAY FATAL_ERROR now also names --recursive.

Evidence: python syntax checks pass for psxrecomp_cli.py and the new tests; runtime/tests/test_codegen_host_bios_stems.py runs or skips only on a missing RECOMP_UI_ROOT. No full runtime build or retail route is claimed here. Developed with AI assistance (Cline, 2026-09-10).
2026-09-11 01:26:22 +03:00
Matthew Stanley 568513bd26 Suppress freeze detector during host pause loops 2026-09-08 18:09:52 -07:00
Matthew Stanley ae2fc168cc Fix native-wide textured dot HUD shift 2026-09-08 12:52:09 -07:00
Alex Vanderveen 69e31bcd39 netplay: use the shared auth module instead of a private copy
psx_netplay_auth.c/h are gone; recomp-net's rnet_auth is the one copy, and
the submodule is bumped to pick it up. Callers change name only
(psx_account_* -> rnet_account_*).

The duplication was a day old and had already cost a bug: the URL screen
that refused every OAuth authorize URL had to be fixed in this file and in
snesrecomp's copy separately, and the second was caught by remembering
rather than by anything structural.

Lobby client compiles with the same 12 pre-existing warnings as before and
no errors. main.cpp still cannot be compiled here -- it needs a generated
tree -- so its rename is verified by symbol cross-check only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015C38nLRmmyXHyJ7P1DiTxg
2026-09-08 09:56:11 -04:00
Matthew Stanley 912c44c75e Support native-wide HUD and tiled backgrounds with guarded culling 2026-09-07 22:40:23 -07:00
Alex Vanderveen a2b2c0f8f7 netplay: Discord sign-in and the device key
The runner half. psx_netplay_auth owns the HTTP, a worker thread and the
key; main.cpp is eight one-line adapters over it; queue_hello gains an
optional session field.

The key never crosses the wire. This runtime has no TLS -- wss:// is still
an explicit "not in this phase" -- so a permanent bearer credential sent
in cleartext would be captured once on a hostile network and reused
forever. Instead the server issues a nonce and this proves possession with
HMAC-SHA256 keyed by SHA-256 of the key. psx_sha256.c was already here, so
the HMAC is twenty lines and there is no new dependency.

netplay_secret is written 0600 beside the other runtime config, in the
form "<player_id> <secret>", and redeemed lazily on the first pump so a
machine that signed in once comes up signed in with no player action. A
stored key that no longer works (revoked, or a different server) is
dropped and the client falls back to guest rather than nagging every
launch. That file is the point of the exercise: sign in once on a PC and
the build you install on a handheld authenticates with no browser -- and
it is a password in a file, so it must not go into anything distributed.

Guest stays byte-identical. `session` is omitted entirely rather than sent
empty, so an unauthenticated hello is the exact frame this client has
always sent. The launcher wiring is guarded on RECOMP_LAUNCHER_HAS_ACCOUNT
so this builds against a recomp-ui that predates the callbacks too, and
the runner and the UI can land in either order.

Opening the browser is per platform (ShellExecute / xdg-open / open) since
there is no SDL_OpenURL here. The URL comes from our own lobby server but
still reaches a shell, so anything that is not a plain http(s) URL free of
shell metacharacters is refused rather than trusted.

Verified: psx_netplay_auth.c and psx_lobby_client.c both compile clean
under -Wall -Wextra, and every psx_account_* symbol main.cpp uses is
declared and defined. main.cpp itself could NOT be compiled here -- it
needs a generated tree (bios_rom_alias.h) -- so its changes are verified
by symbol cross-check and inspection only, not by a build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015C38nLRmmyXHyJ7P1DiTxg
2026-09-08 01:04:19 -04:00
Matthew Stanley 24c6c56cc0 Revert PR #327 interlaced row submission to restore retail BIOS boot
Matched Ape Escape retail SCPH1001 LLE builds identify 0661ce77 as the visible boot regression: parent 155e269b reaches game code, while 0661ce77 stalls near BIOS frame 375. Revert its four dependent follow-ups too; retain PR #326 readback bounds and PR #328 Vulkan aspect changes.

The row-by-row raster dispatcher amplifies an existing malformed DMA list into massive GL work. Keep the broader CPU/DMA arbitration experiment separate for compatibility testing. Document attribution, validation, and the temporarily restored interlace limitation.

Validation: full Ape target reaches title/menu and frame 11669 on retail LLE; OpenBIOS control reaches frame 3701. Eight focused tests pass; retained GL readback fixture passes 71 checks at both 1x and 4x after linking local SDL with iconv. Beads: beads-iisy; DMA follow-up: beads-eio.3.122.
2026-09-07 12:30:30 -07:00
Matthew Stanley 461dcb360a Merge remote-tracking branch 'origin/pr/328' into review/pr326plus-gpu-bundle 2026-09-07 09:36:05 -07:00
FábioAP 6ab221a598 Fix Vulkan renderer ignoring widescreen aspect ratio updates 2026-09-06 14:53:35 +01:00
Alexandros Mandravillis 5aee9c831a Preserve Vulkan wide overlay row clipping 2026-09-06 13:58:00 +03:00
Alexandros Mandravillis 439097184f Count perspective triangles once across interlaced row submissions 2026-09-06 13:34:03 +03:00
Alexandros Mandravillis 0661ce7773 Preserve active display field during interlaced raster drawing 2026-09-06 13:13:03 +03:00
Alex Vanderveen 0f1fd021a8 patch lobby chat 2026-09-06 03:31:56 -04:00
Alex Vanderveen ecc121ade6 lobby browser: spectator column, players online, gallery latency
- lobby_list rows carry allow_spectators / max_spectators / spectator_count
  (browser shows "No" or "1/4"), and every lobby_list carries a players
  array: everyone on the hub with country, room and a short connection tag
  so a client can pick out its own row without publishing whole ids.
- Spectator latency: the round-trip table was keyed by seat and bounds-
  checked against its size, so gallery seats (numbered from 64) were always
  dropped. Seats now map to table cells, player seats first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1dT6h1ZshMVUUVKTPxsCD
2026-09-06 03:31:38 -04:00
Alex Vanderveen 5e1b7d3332 netplay: host from the gallery, seat swaps across tables, LAN seat self-service, keybind fixes
- Host may sit in a spectator seat: session slot 0 stays the sim authority
  with a muted pad and no port; slot->port map keeps players on the port of
  their lobby seat (host first). Rematch path copies the same plan.
- Seat self-service on LAN (MOTK5 SEATMOVE / SWAPASK / SWAPANS / SWAPRES)
  relayed by the host; online swap requests accept a gallery seat as target.
- Lobby chat ring + join/leave lines; country codes from the server.
- Keybinds: a key drives one input per player (binding S to Cross unbinds it
  from Circle), and keybinds.ini is re-read after the rematch launcher too.
- LAN registry file anchored to the exe directory.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1dT6h1ZshMVUUVKTPxsCD
2026-09-06 03:31:38 -04:00
Matt Stanley b213858c4c Merge pull request #323 from Alexbeav/codex/sbi-setup
Preserve SBI companions and validate them before launch
2026-09-05 22:45:15 -07:00
Matt Stanley 87c06376fb Merge pull request #324 from kerokline/feat/dirty-pc-enrichment
dirty_ram per_pc: stamp resident-occupant CRC and caller RA on external entries
2026-09-05 22:40:28 -07:00
Matt Stanley 12c40a5b12 Merge pull request #321 from kerokline/fix/starvation-watchdog-wrap
runtime: fix starvation watchdog cross-thread wrap; label watchdog and console exits
2026-09-05 22:36:37 -07:00
Alex Vanderveen 8f266efe27 netplay: bring-your-own memory card, lobby chat, and the fullscreen lobby glue
Seat 1 (P2) can bring its LOCAL slot-1 card to a match: it uploads the card
to the host at launch over the new recomp-net MEMCARD op, the host installs
it as the match's slot-2 card, and the usual card broadcast carries it to
everyone. The host's real slot-2 file is rebound to netplay/guest_card2.mcd
for the session and restored on shutdown; guests already sandbox both slots.
Lobby side: a per-seat memcard_offer on set_ready (has_card, share), a host
allow flag in match_caps, and guest_memcard_active settled ONCE by the host
at start and shipped with the launch, so a toggle racing the start cannot
split the room. LAN carries the same over MOTK5 UPDATE / MEMCARD / JOIN
tail / a trailing START line. PSX_NET_GUEST_MEMCARD=1 for CLI sessions;
a host waiting on a card that never comes stalls at a named phase
(mc_guest_wait), not a silent "ok".

Two latent guest-sandbox bugs fixed on the way: a disabled slot 2 had no
bound path, so a host blob carrying a card for it failed to import and the
guest stalled forever; and leaving the sandbox re-bound a disabled slot to
the sandbox file. Both slots are now bound in the sandbox and restored per
slot (memcard_rebind_path, which can also unbind).

Lobby chat: the client keeps a ring fed by the server's echo (op:chat);
LAN rooms relay through the host (MOTK5 CHATREQ guest->host, MOTK5 CHAT
host->everyone). Wired to the launcher's chat_send/chat_count/chat_get.

Also carries the spectator-seat work that was in the tree alongside this
(spectator seats in the lobby client, spectator_wire_slot in the launch).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1dT6h1ZshMVUUVKTPxsCD
2026-09-05 21:18:20 -04:00
Kevin 6e760748c2 dirty_ram per_pc: stamp resident-occupant CRC and caller RA on external entries
Make a bare interpreted PC explainable from the session-long per_pc snapshot
alone. DirtyRamPcEntry gains occ_crc / occ_ok (the static overlay variant whose
code ranges span the PC, via a per-PC scan of the static-match cache, and
whether it validated at that moment) and last_ext_ra (gpr[31] at the most
recent external entry, i.e. the caller that reached this interior). Both are
stamped only on external entries -- arrivals from native dispatch, not interp
block chaining -- and emitted per row in dirty_ram_stats.per_pc as occ_crc,
occ_ok and ext_ra.

Reading the pair: occ_ok=1 is an interior gap inside live native code (an
alias seed fixes it); occ_ok=0 with a CRC means the spanning piece is resident
but CRC-missing, so it was compiled from another section's bytes or rewritten
at run time and no seed helps; 0 means nothing compiled spans the PC.

psx_overlay_resident_crc_at() records each static-match cache entry's code
span (lo_min/hi_max) so the scan is a range test; DLL-path titles fall back to
the loader's last residency hash. The debug-server reply buffer grows 32 -> 64
KiB for the wider rows; the tail reserve still cuts between rows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 18:11:13 -04:00
Alexandros Mandravillis 10eb22b775 Keep native SBI test assertions enabled in release builds 2026-09-05 21:47:40 +03:00
Alexandros Mandravillis 2d92d67592 Validate and import SBI companions in the native launcher 2026-09-05 21:34:14 +03:00
Kevin 430c93b8a4 runtime: fix starvation watchdog cross-thread wrap; label watchdog and console exits
starvation_watchdog_check() sampled the clock before loading the heartbeat,
but the heartbeat is also stamped by the debug-server IO thread on every
send() chunk. A stamp landing between the two reads made last > now, the
uint64 subtraction wrapped past any threshold, and a healthy runtime exited
with code 2 (BoF3 2026-09-02: 402 us of real staleness tripped the 4 s
watchdog within ~2 min whenever the area poller was attached).

- Load the heartbeat first, decide through the new pure helper
  starvation_watchdog_stale(), which refuses a negative gap by construction
  (same shape as the frame_pacing_sleep_ms fix).
- Make s_last_heartbeat_us _Atomic with relaxed store/load.
- Tag the exit origin: "starvation_watchdog" on the trip, console_close /
  console_ctrl_break / console_ctrl_c in the Win32 console handler, and
  signal_sigint / signal_sigterm in the soft-exit handler. These all used
  to reach psx_atexit_handler as atexit/unknown, indistinguishable from an
  SDL window close.
- Add starvation_watchdog_test (ctest) pinning the decision function.

Verified on BoF3 build-dbg: 10 min soak with the poller polling at 1 s and
the watchdog enabled at its 4 s default, no dump, process alive; forced
trip with PSX_STARVATION_TIMEOUT_US=1000 fires and the run report reads
exit_origin "starvation_watchdog".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 12:17:16 -04:00
Kevin 3b8b98fa29 runtime: fast-forward toggle shortcut + document the implicit Select chord
Turbo (Tab / [hotkeys] fast_forward_pad) is hold-to-run only. Add a
press-to-latch twin that feeds the same manual fast-forward block:

- host_keymap: HOST_KEYMAP_TURBO_TOGGLE, [KeyMap] TurboToggle, default F9.
- main.cpp: g_manual_turbo_latched flipped by the key edge or by a fourth
  host shortcut, PSX_ASSIST_BIND_FAST_FORWARD_TOGGLE ("Fast-forward
  toggle", [hotkeys] fast_forward_toggle_pad, unbound by default), polled
  next to Rewind / Save states through hotkey_pad_binding_down(). OSD shows
  "Fast forward: Nx (locked)" / "Fast forward: off". Threaded through every
  settings hop (settings.toml -> globals -> launcher seed -> LauncherSettings
  -> back), mirroring fast_forward_pad.
- config_loader: [hotkeys] fast_forward_toggle_pad read/write.
- README: describe the toggle and state that a single-button / trigger host
  shortcut is an implicit chord with Select (hotkey_pad_binding_down), which
  the launcher now labels as "select + ..." (recomp-ui companion change).
- test_rewind_toggle_combo.py guards the new sites.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 21:00:59 -04:00
Matthew Stanley 471ee0d34b dma: expose GPU linked-list observability
Add passive DMA2 ordering-table counters to dma_state so long or aborted linked-list walks are visible without relying on screenshots. The counters track starts, duplicate starts dropped while a walk is active, completions, cancels, last/max walk size and cycle cost, CHCR poll counts, the initiator PC, and the last eight cancel records.

This intentionally does not port PR #302's superseded walker changes or the PSX_GPU_LL_SYNC diagnostic lever; PR #304 remains the timing implementation.

Co-authored-by: Kevin Kline <9866117+kerokline@users.noreply.github.com>
2026-09-03 16:47:19 -07:00
Matthew Stanley 240cff542e Merge remote-tracking branch 'origin/pr/312' into integrate/303-312
# Conflicts:
#	tools/compile_overlays.py
2026-09-03 11:41:22 -07:00
Matt Stanley 1d414fc7b1 Merge pull request #314 from mstan/chore/repo-cleanup
chore: repo hygiene — purge dead scratch tooling, relocate root docs/fixtures/BIOS profiles
2026-09-03 11:30:32 -07:00
Matt Stanley 71f3c8c360 Merge pull request #304 from Alexbeav/pr/gpu-dma2-correctness-ff7
dma: preserve live linked-list reads with word-cost timing
2026-09-03 11:24:02 -07:00
Matthew Stanley e6ce5006b4 docs: move root and stale docs into docs/ and docs/internal/
The repository root carried seven markdown files that were not onboarding
material, plus two that had been superseded by the code they described. This
sorts them: reader-facing docs to docs/, working notes to docs/internal/, and
the two superseded ones merged into the docs that replaced them.

Moved to docs/:            WIDESCREEN.md, ENHANCEMENTS.md, TCP_COMMANDS.md
Moved to docs/internal/:   PRINCIPLES.md, ISSUES.md, DEBUG.md, GTE_LM_FIX.md,
                           and from docs/: bios-hardening.md, overlay-status.md,
                           overlay-plan.md, overlay-recompilation-design.md,
                           overlay-discovery.md, TOMBA2_EXC_CONSULT_RESPONSE.md,
                           CASE_A_AOT_GAP.md, audit_inventory.md
Moved to docs/internal/upstream/: UPSTREAM_2026-08-20.md

The root keeps only what belongs there: README.md, CONTRIBUTING.md, LICENSE,
CLAUDE.md, THIRD_PARTY_ATTRIBUTION.md, RELEASE_NOTES.md, the dotfiles and
psxrecomp_cli.py.

MERGES

SPLITGEN_MIGRATION.md folded into docs/BUILDING.md as "Split generated C". Its
migration instructions were stale in a way that mattered: it told every game
repo to hand-edit its CMakeLists and add a file(GLOB ...) over
<serial>_full*.c, but runtime/runtime.cmake now does that itself. GEN_FULL_GLOB
is a multi-value argument of psxrecomp_add_game_runtime(); the loop at
runtime.cmake:1985 makes each pattern absolute against CMAKE_CURRENT_SOURCE_DIR,
expands it with file(GLOB), and concatenates the hits into
GAME_GENERATED_FULL_C. So a title names the pattern and gets however many shards
a regeneration produced. The rewritten section describes that, the two-step
fallback when the globs match nothing (GEN_FULL_FALLBACK, else <serial>_full.c
derived from the dispatch marker by substituting _dispatch.c), and the one
caveat the code actually has: that file(GLOB) is plain, not CONFIGURE_DEPENDS,
so a regeneration which changes the shard COUNT needs a re-configure to be seen.

psxrecomp-toml-readme.md folded into docs/GAME_PROJECT_SETUP.md as "EXE-only
autofill (psxrecomp-toml)", placed after "Disc autofill (probe_disc.py)" and
before "Project Studio" so the two autofill paths sit together. Its documented
flags were checked against the usage text in recompiler/src/main_toml.cpp:28-42
and against the argument parser at :150-165 -- all seven options match with no
drift, so the flag list is reproduced as-is. Added a pointer that probe_disc.py
is preferable when a cue exists, since that tool does the same EXE-header work
plus disc identity and netplay gates.

FUNCTIONAL FIXES

tools/gen_tcp_commands.py wrote to REPO/"TCP_COMMANDS.md". After the move that
path does not exist, so the generator would have created a new stray root file
and --check would have failed against it forever. Retargeted to
REPO/"docs"/"TCP_COMMANDS.md", along with the seven docstring and message
strings that name the path.

Running it surfaced two further problems, both fixed here:

  * The doc was genuinely stale -- pre-existing drift, not introduced by this
    commit (the moved file is byte-identical to origin/master, and no
    debug-server command table is touched here). Four commands registered since
    the last regeneration were missing from the index -- cdrom_timing_dump,
    savestate_status, ws_ui_groups, xprobe_watch -- and scanline had gained a
    backend. Regenerated: 307 -> 311 commands, and the change is confined to the
    AUTOGEN block (the hand-written head and tail are byte-identical to master).
    tools/gen_tcp_commands.py --check now exits 0.
  * DOC.write_text() has no newline argument, so Python translated every newline
    to os.linesep. On Windows that rewrote all 585 lines of the file to CRLF and
    turned a four-line index update into a whole-file diff -- measured: 589 CRLF
    / 0 LF after a run, against 0 CRLF / 585 LF in the repo. Now written through
    an explicit newline="" handle, verified to leave the file at 0 CRLF.

runtime/include/debug_server.h:9 and runtime/src/debug_server.c:8 cited "TCP.md
and DEBUG.md". TCP.md has never existed in this repo under that name; both now
name docs/TCP_COMMANDS.md and docs/internal/DEBUG.md.

LINK AND REFERENCE FIXES

Every inbound reference to a moved file was located by grepping each basename
across the whole tracked tree, not just the markdown. Markdown hrefs in
README.md, CONTRIBUTING.md, docs/ARCHITECTURE.md, docs/COMPILING_OVERLAYS.md,
docs/AOT_OVERLAY_PLAN.md, docs/RELOCATION_MANIFEST_FORMAT.md,
docs/config_schema.md, docs/OVERLAY_XREF.md, docs/TCP_COMMANDS.md and several
docs/internal/ files now point at the new locations; where two files became
siblings under docs/internal/ the path was shortened rather than lengthened
(bios-hardening.md's reference to overlay-discovery.md, for instance).
docs/COMPILING_OVERLAYS.md's "in this folder" phrasing was wrong once two of the
three files it listed moved, so it now names internal/ explicitly.

24 source-comment mentions of ENHANCEMENTS.md sections G1.x across 19 files
(recompiler, runtime, cmake) were retargeted to docs/ENHANCEMENTS.md with the
section numbers untouched. docs/SHADOW_ENHANCEMENTS.md is a DIFFERENT file and
was deliberately excluded.

20 rationale strings citing docs/CASE_A_AOT_GAP.md across the four
recompiler/seeds/*.json files were updated. Done as a byte-level substring
replacement rather than a JSON round-trip: two of those files are stored with
CRLF endings and two with LF, and re-serializing them reformatted 5,700 lines.
The byte-level version changes exactly 20 lines and each file still parses.

References that look similar but point elsewhere were deliberately left alone,
and are worth naming so a future pass does not "fix" them:

  * "recomp-template PRINCIPLES.md" / "NES recomp template PRINCIPLES.md" in
    CLAUDE.md:43, docs/SHADOW_ENHANCEMENTS.md:28,
    docs/internal/HLE_SCHEDULER_CARVEOUT_PLAN.md:5,
    runtime/src/debug_server.c:4934 and tools/parity_diff.py:8 are a different
    repository's file. Only the five that mean this repo's PRINCIPLES.md were
    updated.
  * Every remaining bare ISSUES.md mention is a GAME repo's tracker
    (MegaManX6Recomp 2/7/8/9, TombaRecomp 6), not this repo's.
  * docs/disc-speed.md:4,54,56 and docs/internal/bios-hardening.md:5 name
    overlay-discovery as a BRANCH, not a file, so no path applies.

INDEXES

README.md's "New here?" table gains rows for docs/WIDESCREEN.md,
docs/ENHANCEMENTS.md and docs/TCP_COMMANDS.md. docs/internal/README.md gains
sections indexing all fourteen documents that moved in, plus an "Upstream
contributor reviews" section describing upstream/ and the new sweep file, plus a
"Removed" bullet for CYCLE_TIMING_ARCH.md (deleted in the previous commit)
pointing at FAITHFUL_TIMING_PLAN.md, CYCLE_MODEL_BEETLE.md and
PRECISE_IRQ_SLICE.md -- following the SLJIT_PERSIST_CACHE.md precedent already
in that file.

docs/ENHANCEMENTS.md:7 claimed framework bugs live in "game repos' ISSUES.md
until a framework tracker exists". A framework tracker does exist; it now says
so and names the Beads epic.

VERIFICATION

A link checker was written to scan every tracked *.md for relative links,
resolving each against the tree being scanned. Baseline at origin/master: 129
files, 137 relative links, 3 broken. After both commits: 127 files, 159 relative
links, 3 broken -- the SAME three, all pre-existing and untouched here:

    docs/DATA_SHARDS.md -> INSTANT_LOADS_PARKED.md   (never existed)
    docs/DATA_SHARDS.md -> INSTANT_LOADS_SPIKE2.md   (never existed)
    docs/NETPLAY.md     -> ../lib/recomp-net/README.md  (inside a submodule)

Zero new broken links. The checker's own coverage was checked against a raw grep
for markdown link openers: the 3-link difference is C++ lambdas inside code
blocks, correctly not treated as links.

Also verified: py_compile over all 113 tracked tools/*.py plus psxrecomp_cli.py;
all four seed JSONs still parse; the test-registration guard still reports 123
test files all registered; recompiler/tests/test_coverage_vault.py still passes;
and no file in this commit had its line endings flipped (checked mechanically
across all 50 modified files, after two files were caught and redone).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 07:39:07 -07:00
Alexandros Mandravillis 9dfb4174ad runtime: version GPU DMA snapshot format 2026-09-03 01:55:30 +03:00
Alexandros Mandravillis 99f775c839 runtime: close GPU DMA review gaps 2026-09-03 01:20:43 +03:00
Alexandros Mandravillis f424a8144d runtime: preserve live GPU DMA payload timing 2026-09-03 01:20:41 +03:00
Alex Vanderveen 85126d8d1b Revert "dma: charge DMA2 linked-list walks what the validated model charged" 2026-09-02 18:05:17 -04:00
Alex Vanderveen 39bcdf0d56 dma: charge DMA2 linked-list walks what the validated model charged
Slicing the GPU linked-list DMA across guest cycles (8af48ae9, PR #277) also
changed what a walk costs: 8 clocks for every node header plus 5 setup clocks
per non-empty node, where the pre-slicing path charged one clock per word via
schedule_delayed_complete(2, actual_words, DMA_GPU_CYCLES_PER_WORD).

An ordering table is mostly EMPTY nodes -- Final Fantasy VII's runs to
ot_max 4094 -- and each empty slot still paid the full 8-cycle header for a
packet that does not exist. Measured on FF7 (SCUS-94163):

  4,807 nodes / 12,757 words   52,384 cycles   vs   17,564   (3.0x)
 44,010 nodes / 117,507 words  499,187 cycles   vs  161,517   (3.1x)

An NTSC frame is 564,480 cycles, so a single walk reached 88% of a frame.
FF7's libgpu polls CHCR bit 24, finds the channel still busy after 790-890
reads, and force-clears the start bit at 0x800484F0 -- the documented
stuck-channel path. cancel_async_transfer() then abandons the walk partway
through, and the rest of the frame's geometry is never sent to GP0. In a
battle 12.6% of ordering tables were aborted this way; models rendered with
missing or shattered limbs.

The guest is behaving correctly: it inspected the busy bit before clearing it.
The slicing is also correct and worth keeping -- a guest really can change a
packet after starting DMA and before DMA reaches it, and only a sliced walker
models that. Only the cost was wrong.

Header is one clock, setup is free, so a walk costs exactly (nodes + words).
When setup is free the payload boundary is the same service as the header, so
the header phase falls through instead of returning; cycles_to_event() never
reports zero, and returning would spend an unaccounted scheduler cycle per
non-empty node. The packet is still read at its own boundary, so #277's
mid-walk mutation behaviour is unchanged and its test still passes.

(nodes + words) is not a new assertion -- it is the cost this runtime shipped
and was validated against. A non-zero per-node overhead may well be real on
hardware, but it needs a measurement behind it, not a substituted constant.

Observability, because none of the above was visible:

  dma_state gains a gpu_ot block -- starts, completes, cancels, starts_dropped,
  nodes/words/cycles of the last walk and the max, plus guest reads of CHCR(2)
  and a ring of the last 8 aborts carrying the guest PC, CHCR value, how far
  the walk got, and how many times the guest polled before giving up. The
  silent early return in start_async_gpu_linked_list() is now counted as
  starts_dropped rather than losing an ordering table with nothing said.

  PSX_GPU_LL_SYNC=1 drains a walk at start. Diagnostic A/B lever only: it
  completes immediately as well, so it is not a faithful restoration of the
  pre-slicing timing and must not be used as a fix.

The timing test now asserts the cost model rather than describing it: a table
of empty nodes must cost exactly one cycle per node. That is the check that
would have caught this.

Verified on FF7 SCUS-94163 from a save state: cycles_max 58,933 -> 16,972,
a 144-node/490-word walk costs exactly 490 cycles, and battle geometry renders
correctly with the walk still sliced.

Not fixed here: the same commit also broke gpu_frame_dump's func attribution.
GP0 writes now issue from dma_advance() on the cycle scheduler, so every
primitive is stamped with whatever the scheduler interrupted -- all 1,803
primitives in a captured battle frame attributed to BIOS 0x1FC02B50 instead of
the issuing game function. That disables gpu_frame_diff's "a function stopped
drawing" verdict and gpu_frame_layers entirely. Stamping the DMA initiator was
tried and does not work: g_debug_current_func_addr reads 0 at kick time. The
ra column still carries real game addresses as a fallback. Left as a separate
issue rather than shipped as a fix that does not fix it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9v7e6AVZe3eRP8VXey9Pz
2026-09-02 18:01:15 -04:00
Matthew Stanley c5bac02214 controller: re-clamp lock_mode across the launcher round-trip
Ape Escape declares `default_mode = "analog"` + `lock_mode = true`
(ApeEscapeRecomp/game.toml:208-209) and shipped with the left stick acting as
camera and the right stick doing nothing -- i.e. the pad booted DIGITAL on a
dual-analog title, and because lock_mode hides the pad-mode selector there was
no UI with which to fix it.

The root cause is in recomp-ui and is fixed in the companion PR (see below).
This is the host side of the same defect: three separate places in main() where
a locked title's declared pad mode could be lost or where a keyboard seat's
configured mode was destroyed on its way past the launcher.

WHAT WENT WRONG, END TO END
---------------------------
1. recomp-ui launcher_model.c honours the lock at line 545-546
   (`if (!pad_mode_selectable) pad_mode[p] = locked_pad_mode`) and then
   clobbers it 15 lines later with an ungated keyboard coercion. Player 1's
   device defaults to "keyboard" in a release build (main.cpp:11164), so
   pad_mode[0] came back as 2 (D-Pad) from every fresh install.
2. `launcher_model_set_pad_mode` and the default-for-source helper both
   early-return when the mode is not selectable -- correctly, since the mode
   is locked -- so once the value was 2 nothing could move it back. Attaching
   a real DualShock did not help.
3. Here, the launcher-exit readback took `player_mode[i] = ls.pad_mode[i]`
   verbatim and then persisted it into settings.toml, so DIGITAL beat the
   lock and kept beating it on every later launch.
4. `sio_set_pad_analog(slot, 0, ...)` then took the 4-byte pad_response_len
   branch in runtime/src/sio.c: the pad reports no analog bytes, so the right
   stick is dead and the left stick is folded onto the D-pad. Exactly the
   reported symptom.

The clamp at main.cpp:11869 already existed and already covered two holes
(launcher-less builds, and a settings.toml written before the game declared
lock_mode). It runs BEFORE the launcher, which is why the launcher round-trip
was a third hole it could not see.

WHAT THIS CHANGES
-----------------
runtime/include/launcher_device.h gains
`resolve_player_mode_after_launcher(launcher_mode, lock_mode, locked_mode,
mod_override_mode)`: one pure function that fixes the precedence of the three
sources of truth for a seat's pad mode -- an active mod controller-mode
override, then game.toml lock_mode, then whatever the launcher returned. Both
launcher-exit readback loops call it, and both call it before their persistence
write, so an unsupported mode can no longer be latched into settings.toml.

The mod-override arm is not decoration. A bare `if (ctrl_lock_mode)
player_mode[i] = ctrl_locked_mode[i];` -- which is the obvious fix and was the
first version of this change -- breaks mod controller-mode overrides on locked
titles after a lobby soft return:

  * `g_mod_controller_mode_override` is applied at main.cpp:13084-13085,
  * the `session_reboot:` label is BELOW it at 13159,
  * `goto session_reboot` (14914) therefore never re-runs the override block,
  * but the soft-return readback loop does run.

Before this change an override survived a rematch only because it round-tripped
through `ls.pad_mode[i]`, seeded at the soft-return entry from the post-override
`player_mode[i]`. An unconditional clamp cuts that path and silently drops the
override. `psx_mod_set_controller_mode_override` is public plugin API
(runtime/include/mod_plugins.h:229) with no shipped caller today, so this is
latent rather than live -- fixed anyway, because the next caller would have had
no way to tell why their override stopped applying to a locked title after a
rematch.

Three destructive coercions are also removed, all of the same shape -- a
keyboard seat having its configured mode overwritten with DIGITAL:

  * the two launcher-entry seeds (`ls.pad_mode[i] = (src == 1) ? DIGITAL :
    player_mode[i]`) told the launcher a lie about what the seat is configured
    for, and the launcher handed the lie straight back to be persisted;
  * the launcher-exit readback did the same in the other direction.

None of them was load-bearing: `effective_player_mode()` (main.cpp:4359-4362)
already returns DIGITAL for any seat whose kind is keyboard, whatever the
seat's stored mode says, and every consumer of a seat mode goes through it or
through `effective_player_mode_for_sio()` -- `refresh_player_devices`,
`capture_pad_slot`, and the netplay sample path were all checked. So a keyboard
seat still presents as a plain SCPH-1080 while the keyboard is driving it; the
difference is that its stored mode is no longer corrupted for the pad that
arrives later.

Note the unlocked case was never as bad, and this is worth writing down because
it is what made the bug look title-specific: for a selectable title,
`launcher_model_set_source` -> `apply_default_pad_mode_for_source` re-defaults a
gamepad seat to Analog, so picking a device in the launcher repairs the value.
That is precisely the repair a locked title cannot perform, because both of
those functions refuse to touch a locked mode.

TESTS
-----
There was zero coverage for any of this. (ApeEscapeRecomp's own
`ape_stick_response_test` covers the stick RESPONSE CURVE, not mode or axis
routing, and lives in the title repo -- it cannot see this.)

  * runtime/tests/test_launcher_pad_mode_resolve.cpp -- behaviour of the
    resolver: a locked title yields its declared mode for every launcher value
    including out-of-range ones (so persistence can never receive an
    unsupported mode), an active override outranks the lock, and the "no
    override" sentinel stays negative to match the direct apply site.
    Registered as launcher_pad_mode_resolve_test.
  * runtime/tests/test_launcher_pad_mode_wiring.py -- how it is WIRED: exactly
    two readback sites call the resolver, the resolution precedes each
    persistence write, the pre-launcher clamp still runs first, and
    `player_mode[i] = PSXRecompV4::PAD_MODE_DIGITAL;` appears nowhere. Verified
    to go red when the coercion is re-introduced at either site, not just when
    the helper is deleted. Registered as launcher_pad_mode_wiring alongside the
    other source-invariant guards in recompiler/CMakeLists.txt.

Both call sites were confirmed to be live code, not preprocessed-out, by
injecting a bad symbol name and reading the compiler errors back: main.cpp:12701
compiles under `-DRECOMP_LAUNCHER` and main.cpp:14665 additionally needs
`PSX_HAS_LOBBY_CLIENT` (i.e. `-DPSX_NETPLAY=ON`), so the second site is only
exercised in a netplay build and was verified in one.

Verified: launcher_pad_mode_resolve_test, launcher_device_roundtrip_test,
host_keymap_test, keyboard_pad_chord_test, sio_card_protocol_test all pass
(sio_dualshock_rumble_test is registered DISABLED); launcher_pad_mode_wiring and
every other python guard that reads main.cpp pass, except two that were already
red before this branch -- runtime_perf_diag_guards (registered DISABLED) and
launcher_vulkan_option, which dies in `Path.read_text()` with no encoding on a
cp1252 console. That last one is a real latent break in 13 guard scripts and
wants its own change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 14:11:25 -07:00
Alex Vanderveen 4cc04be3e6 rebump 2026-09-02 13:26:06 -04:00
Matthew Stanley 7c5b766fd5 overlay: ABI v22 game_text_native_ok shim (107 shards failed to link)
The emitter emits a direct psx_game_text_native_ok() stale-static guard at block
entry (code_generator.cpp:115) and before every direct jal (:2402). That function
is DEFINED IN THE GENERATED DISPATCH SHARD (main_psx.cpp:1620), which links into
the executable. An overlay DLL is a separate module and cannot resolve it, and
overlay_dispatch_preamble.c.inc had no shim for it.

Found by bumping Tomba 2 from a 254-commit-old pin to master and regenerating its
shard cache against the identical vault:

  before bump : built OK 2799, FAILED 1
  after bump  : built OK 2233, FAILED 109

107 of the 109 were the same error:

  ld.exe: undefined reference to `psx_game_text_native_ok'
  collect2.exe: error: ld returned 1 exit status

This is the fifth instance of one class, and overlay_api.h's own version history
records the previous four:

  v9  faithful-timing callbacks  overlay code failed to link
  v10 gte_read_ctrl              GTE-heavy overlays failed to LINK without shims,
                                 silently blocking new coverage
  v12 rfe_mark_escape            without the shim any rfe-containing overlay
                                 fails to LINK
  v14 gte_precision_store_word   the rfe / gte_read_ctrl class again

Every time the shape is identical: an emitter gains a runtime call, the overlay
ABI does not gain the shim, and the only symptom is shards that quietly fail to
build. The affected code then runs interpreted forever and nothing at runtime
says so. Severity is coverage, not correctness -- but that is exactly the defect
that let Ape Escape ship 100% interpreted without anyone noticing.

Fix follows the v10/v12/v14 pattern exactly:
  - overlay_api.h: append game_text_native_ok to OverlayCallbacks, bump
    PSX_OVERLAY_ABI_VERSION 21 -> 22, add the version-history note
  - overlay_dispatch_preamble.c.inc: shim forwarding to g_cbs
  - overlay_loader.c init_callbacks: host populates it from the dispatch shard

The NULL guard deliberately returns 0, not 1. Returning 0 routes the guest
through the dispatcher -- slower, never wrong. Returning 1 would let an overlay
run native code the guard was explicitly trying to reject.

Follow-up worth doing, tracked as beads-eio.3.97: five instances of one class
means the process is the bug, not the symbol. The emitter knows every helper name
it can emit and the preamble enumerates every shim; a test comparing those two
sets would have caught v9, v10, v12, v14 and this one at authoring time instead
of at some future title's link step.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 00:37:44 -07:00