Commit Graph

403 Commits

Author SHA1 Message Date
Matthew Stanley 63b205df78 Index immutable resident dispatch keys and clarify owner telemetry 2026-09-11 23:39:46 -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 93a93c8e07 Fix game runtime wrapper setup gating 2026-09-08 13:36:54 -07:00
Matt Stanley aeaa14a52d Merge pull request #335 from mstan/fix/mmx6-mod-disc-resolution-before-commit
Resolve launch disc before committing mods
2026-09-07 13:00:36 -07:00
Alex Vanderveen 3e038f289c launcher: seed personal memcards on netplay soft-return; fold card choices into rematch
After a match the soft-return launcher was built from a fresh settings struct
that never carried the memcard paths or enabled flags. With nothing to inspect
the Save panel painted a placeholder block pattern on both cards, which read as
foreign save data even though the personal card files were untouched (the
netplay sandbox is unbound and personal cards reloaded before the launcher
opens).

- soft_return_lobby: seed memcard_path[] from the personal cards (settings.toml
  path, else <memcard_dir>/cardN.mcd) and memcard_enabled[] like first boot.
- rematch writeback: fold ls.memcard_enabled/memcard_path back into the runtime
  slot config and settings.toml, mirroring the first-boot exit path, so a card
  toggled or browsed in from the rematch launcher reaches memcard_init_slots.
  --memcard-dir keeps winning over paths.
- enabled flag is passed as -1 for disabled (0 means "unset -> on" to the
  launcher) and read back as > 0, at both seed sites. Needs recomp-ui 028fa5c.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HE3VRJ7nBVP1x7x2Yw2Ho
2026-09-07 15:35:15 -04:00
Matthew Stanley 7e492fa5f8 Resolve launch disc before committing mods 2026-09-07 12:07:27 -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
Matthew Stanley a7d894fdce Merge remote-tracking branch 'origin/pr/329' into review/pr329
# Conflicts:
#	runtime/CMakeLists.txt
2026-09-07 09:33:16 -07:00
Alexandros Mandravillis d8725466bc Keep retry labels and trace declarations C compatible 2026-09-07 16:11:22 +03:00
Alexandros Mandravillis 7c11d57ea1 Preserve CUE track metadata at launch 2026-09-07 16:11:21 +03:00
FábioAP 6ab221a598 Fix Vulkan renderer ignoring widescreen aspect ratio updates 2026-09-06 14:53:35 +01: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
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
Alexandros Mandravillis 2d92d67592 Validate and import SBI companions in the native launcher 2026-09-05 21:34:14 +03: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
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
Matt Stanley 533816ecfb Merge pull request #307 from kerokline/feat/fast-forward-pad
runtime: controller host shortcut for fast-forward ([hotkeys] fast_forward_pad)
2026-09-03 07:30:35 -07:00
Matt Stanley a7459cddbe Make packaged overlay toolchain self-contained (#311) 2026-09-02 22:13:06 -07:00
Matt Stanley a923d21a23 Merge pull request #298 from mstan/fix/lock-mode-launcher-reclamp
controller: re-clamp lock_mode across the launcher round-trip (defense-in-depth; root cause in recomp-ui)
2026-09-02 20:01:48 -07:00
Matthew Stanley 94e3ef610d release: ONE staging implementation for both platforms (2 titles had no Linux release path)
tools/release_overlay_stage.ps1 exists because a packager that re-derived the
overlay cache tag in PowerShell went stale the moment the framework appended an
_f<flavor> field to it: the packager's filter matched nothing, a perfectly
regenerated cache staged ZERO shards, and the release shipped with no native
overlays (beads-eio.2.6). The fix was correct -- expose cache_tag() and route
the packager through it -- and it fixed Windows only.

Linux was never consolidated at all. Three title repositories each carry a
forked tools/package_appimage.sh, and each hand-builds the same tag string,
inside a python heredoc that has just imported the module that owns it:

    spec = importlib.util.spec_from_file_location('co', mod_path)
    m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
    print(<the tag format, reproduced by hand> % (m.codegen_ver(inc),
          m.codegen_hash(inc), m.overlay_config_hash(exe, gt)))

MEASURED 2026-09-02, reading each repository's origin/master (not a working
tree -- the Ape checkout is parked on codex/ape-issue3-diagnostic-prerelease,
behind master, and reading it gives the wrong answer, including a wrong line
count):

  repo             lines  calls cache_tag()  composes _f  stages toolchain
  ApeEscapeRecomp    416         no              yes            no
  Tomba2Recomp       436         no              NO             no
  MegaManX6Recomp    408         no              NO             no

ApeEscapeRecomp 4a17272 (2026-08-28, "release: stage flavored Linux overlay
cache") hand-appended the field to ONE of the three forks. It did not adopt
cache_tag(); it wrote cache_tag="${cg_tag}_f${overlay_flavor}" and switched the
filters to it. So Ape is correct today and one character from the same failure
on the next tag-shape change, and the other two are broken right now:
find -path "*/$cg_tag/*" requires a separator immediately after the tag, the
real directory is named ..._f0, so the pattern cannot match. shards evaluates
to 0 and the packager exits 1.

That is not hypothetical. Tomba 2 v0.0.9, released 2026-09-02, has exactly two
assets: Tomba2Recomp-v0.0.9-windows-x64.zip and its .sha256. There is no
AppImage in that release at all, which is what a packager that exits 1 looks
like from the outside.

Two implementations, one per platform, have to be kept in step by review. This
bug is the proof that review does not hold: the identical defect had ALREADY
been fixed on the other platform, and the reason cache_tag() exists is that
fix. So there is now ONE implementation.

  tools/release_stage.py            the implementation (new)
  tools/release_overlay_stage.ps1   thin wrapper, surface UNCHANGED
  tools/release_overlay_stage.sh    thin wrapper, new (Linux had none)

Both wrappers are argument marshalling. Neither contains a tag format string, a
shard filter, an extension list, an arch-abi segment or a catalog rule. Every
sh function forwards "$@" verbatim, so a new option needs no edit there at all.
No title's package_release.ps1 needs a change: every parameter, switch and
return value of Get-OverlayCgTag / Add-OverlayCache / Add-OverlayToolchain /
Add-ModCatalog / Get-PinnedArchive is preserved, verified by dot-sourcing the
module in PowerShell 5.1 and exercising all five.

WHAT THE SHARED IMPLEMENTATION DOES DIFFERENTLY

* The tag is cache_tag(). Not "a call that produces the same string" -- the
  call. release_stage.py does not know the tag's shape and must never learn it.
  It also takes the arch-abi and the shared-library suffix from
  compile_overlays.cache_arch_abi()/overlay_ext() rather than naming them, so
  ".so under linux-x64" and ".dll under win-x64" are one decision, made in the
  module the loader is kept in lockstep with, not two.

* Shards are selected by the tag DIRECTORY'S IDENTITY, not by a path substring.
  -path "*/<tag>/*" is simultaneously too loose (it accepts a foreign arch-abi)
  and too strict (it rejected the real _f0 directory and staged nothing). The
  staged layout is then asserted -- exactly one tag directory, and it must be
  this build's -- because the loader scans cache/<id>/<tier>/<arch-abi>/<tag>/
  literally, and a shard anywhere else is worth exactly as much as no shard,
  which no count can distinguish.

* Selection is an ALLOWLIST of {<ext>, .ranges, .resident}, so a new artifact
  kind in a producer's cache is excluded by default rather than shipped by
  default, and the staged tree is re-scanned afterwards to prove that
  .abi_<tag>.ok, *.c and *.pair-lock did not ship. The sweep memo is the one
  that matters: it tells a player's first launch that an ABI sweep it never ran
  already passed, so a stale shard gets loaded instead of rejected.

* --allow-no-cache is gone from the Linux surface entirely. It is the
  relaxation that let a packager staging nothing read as a successful build.
  What remains is --ship-without-overlay-cache-because '<reason>', which is
  unwieldy on purpose, prints the reason, and is not the documented path in any
  recipe. PowerShell's -AllowNoCache is kept (unmigrated title packagers still
  pass it) and maps onto that flag, so even the escape hatch is one
  implementation.

* The mod catalog is verified against the manifest the BUILD published.
  runtime.cmake's _psxrt_stage_mod_catalog already writes
  psx_mod_catalog_<target>.txt -- the sorted, deduplicated id list that build
  staged -- which is a strictly better authority than re-globbing source trees,
  and it handles Tomba 2's Italian catalog overriding framework psx.* packages
  at the same ids without a per-variant number.

LINUX GETS AN OVERLAY TOOLCHAIN, AND THE RUNTIME CAN NOW SEE IT

No AppImage packager has ever staged one (grep -c overlay_toolchain = 0 in all
three forks), so on Linux the shipped AOT cache was all a player ever got: the
capture-and-compile fail-safe could not fire, and an overlay outside the cache
was interpreted forever.

Staging one alone would not have worked. runtime/src/main.cpp gated autocompile
on overlay_toolchain/python/python.exe, and built the command from
psxrecomp-game.exe and tcc/tcc.exe -- hardcoded Windows spellings, so on Linux
the gate could never be true no matter what a packager staged. A bundled Linux
toolchain would have been dead weight the runtime never looked at. Those three
names are now per-platform; the layout is identical, only the spellings differ,
and they are kept in lockstep with release_stage.py's TOOLCHAIN_PY_REL /
TOOLCHAIN_RECOMPILER.

The Linux interpreter pin is a python-build-standalone CPython 3.13.1 --
deliberately the same version as the Windows embeddable-zip pin -- fetched and
SHA256-verified on every use by the same code path as the Windows one, so the
platforms cannot drift on retry, cache or verification behaviour either. The
_stripped asset (21 MB download, 78 MB unpacked) is used; compile_overlays.py
needs only the stdlib, verified by import. tinycc publishes no prebuilt Linux
binary, so there is no pinned tcc there and the bundle drives a gcc from PATH;
main.cpp already says so on stdout rather than silently doing nothing. The
SHA256 was measured by downloading the asset and matching the publisher's own
.sha256 sidecar, not copied from a listing.

THE FLAVOR IS NO LONGER GUESSED

PSX_OVERLAY_FLAVOR is the high half of overlay_abi() and the _f<n> field of the
tag. It is a property of the BINARY, decided in runtime.cmake (0 base, 2 for a
PGXP target), and it is NOT platform-dependent -- Windows and Linux builds of
the same target share it, so nobody should ever add a platform branch for it.
Every packager simply assumed 0, and that assumption is invisible when wrong: a
PGXP runtime reads ..._f2/ while the packager stages ..._f0/, the package ships
a cache the binary ignores completely, and nothing fails.

runtime.cmake now publishes psxrecomp_overlay_flavor-<target>.txt next to the
exe-name publication it already writes, for the same stated reason: so nothing
downstream re-derives it. release_stage.py cg-tag takes either --flavor or
--flavor-from-build, and there is no default -- a packager either states the
value or reads what the build published.

TESTS

runtime/tests/test_packagers_never_format_cache_tag.py asserts the rule the two
fixes above were supposed to establish: the tag format appears only in
tools/compile_overlays.py and runtime/src/overlay_loader.c (measured: 5
occurrences, all inside those two files -- note overlay_loader.c has four, at
1745, 1773, 1924 and 1930, the last two being the actual read path) and nowhere
else, and a release packager that touches the overlay cache must reference the
shared staging surface. It carries a positive control -- each owner must
actually contain the pattern -- because the investigation of this very bug once
reported "no cache present" from a probe that could not have found a cache at
all. It is also the SHARED check a title runs against its own tree
(--root <repo>), so the rule about not duplicating logic is not itself
duplicated per repository.

Run against Tomba2Recomp origin/master it reports, pre-fix:

  tools\package_appimage.sh:292 formats the overlay cache tag itself
  tools\package_appimage.sh is a release packager but does not reference the
  framework shared staging surface

runtime/tests/test_release_stage_cache.py pins the behaviour, hermetically, on
synthetic cache trees -- no recompiler, no captures, no build, which is the
point: this is the assertion that could have run in CI on the day the tag grew
a field. 15 cases, including that a tag one field short selects nothing AND
raises rather than warns, that a foreign tag namespace / arch-abi / the removed
sljit tier are not shipped, that the three forbidden artifact kinds never ship,
and that developer state (mods/installed, mods/state.toml) never ships.

Both are registered in runtime/CMakeLists.txt, so the configure-time
test-registration guard covers them and ctest runs them.

NOT DONE, deliberately: tools/package_release.ps1,
tools/package_release_macos.sh and tools/package_setup_host.sh each carry their
own hand-written mod-catalog staging. That is the same duplication defect one
layer over and they should route through stage-mods too, but it is a separate
change needing separate proof (macOS cannot be built or verified here), so the
new test scopes its packager assertion to packagers that touch the overlay
cache rather than pretending to cover them.

Bead: beads-eio.3.102

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 18:28:24 -07:00
Kevin 2ae781095a runtime: controller host shortcut for fast-forward ([hotkeys] fast_forward_pad)
Turbo was keyboard-only ([KeyMap] Turbo, Tab). Add a third host shortcut the
launcher can bind to a pad button or chord, next to Rewind and Save states:

- main.cpp: PSX_ASSIST_BIND_FAST_FORWARD + "Fast-forward" label; default
  chord select+L1 (PSX_HOTKEY_PAD_SELECT_L1 = 1528); the manual fast-forward
  block now runs while either the keyboard key or the pad binding is held,
  through the same hotkey_pad_binding_down() matcher the other two use.
  Threaded through every settings hop (settings.toml -> globals -> launcher
  seed -> LauncherSettings -> back), mirroring save_state_menu_pad.
- config_loader: [hotkeys] fast_forward_pad read/write, same pad_bind_value_ok
  bound as the siblings.
- test_rewind_toggle_combo.py guards the new sites; README documents the chord.

Launcher side: recomp-ui draws every advertised host shortcut instead of the
first two (companion PR).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 20:12:21 -04:00
Alexandros Mandravillis 99f775c839 runtime: close GPU DMA review gaps 2026-09-03 01:20:43 +03: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
Matthew Stanley 0731be9218 Merge remote-tracking branch 'origin/master' into HEAD 2026-09-01 13:17:24 -07:00
Matthew Stanley 1438876250 runtime: clean scanline branch mechanics 2026-09-01 13:17:19 -07:00
Kevin 92e5f4b2e2 runtime: wire scanlines through the launcher + settings persistence
Threads Settings.scanlines / scanline_strength_pct from the recomp-ui launcher
Display card through the same paths as screen_kind: seed the launcher from the
current state, apply the player's choice back to the video globals (pushing it
to the GL renderer immediately), and persist it to settings.toml. All launcher
struct accesses are gated on RECOMP_LAUNCHER_HAS_SCANLINES so the runtime still
builds against an older recomp-ui.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 09:53:22 -04:00
Kevin 97f58e19d7 runtime: present-time scanline post-process (host display enhancement)
Adds an optional scanline effect to the GL present path: a soft sinusoidal
beam pitched to the PS1 display-line grid (one dark line per native scanline),
applied in both the present and interpolation shaders and gated by output
scale so it never shimmers below ~2x. Orthogonal to the crt_filter colour LUT
(the two compose).

Controls: [video] scanlines / scanline_strength (game.toml + settings.toml),
PSX_SCANLINES / PSX_SCANLINE_STRENGTH env overrides, an F6 hotkey
(HOST_KEYMAP_SCANLINES, rebindable via [KeyMap] Scanlines), and a `scanline`
TCP command for live A/B. Launcher Display-card wiring follows separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 09:53:22 -04:00
Matthew Stanley fbf0427ba6 Fix display cadence refresh on monitor moves 2026-08-31 19:28:10 -07:00
Alexandros Mandravillis 4676707ae0 Preserve interlaced PAL scanout height 2026-08-31 09:15:39 +03:00
Alexandros Mandravillis 12f2060edb Present full depth24 scanout canvas 2026-08-31 09:15:39 +03:00
Alexandros Mandravillis a851e26d48 Preserve visible depth24 scanout rows 2026-08-31 09:15:39 +03:00
Alexandros Mandravillis 025193e2e8 Center depth24 scanout from display range 2026-08-31 09:15:38 +03:00
Matt Stanley 0ed754a3ce Merge pull request #273 from kerokline/fix/present-skip-unpaced-vsync-cadence
runtime: pace frames the GL renderer elides a swap for
2026-08-30 20:52:18 -07:00
Kevin a91884a4bb runtime: pace frames the GL renderer elides a swap for
The GL renderer may skip SDL_GL_SwapWindow when the displayed rect is
unchanged, so a double-buffered 30 Hz game does not swap an identical
front buffer on the intervening vblank. The frontend calls present once
per guest vblank, so eliding a swap also elides whatever that swap was
blocking on.

That is only safe while the wall-clock pacer owns cadence, because the
pacer runs per present regardless of what the renderer does. When driver
vsync owns cadence instead (~60 Hz panel, present_vsync_owns_cadence()),
the pacer is deliberately not run -- the two are XOR to avoid
double-blocking -- and the swap block becomes the only throttle. An
elided frame then blocks on nothing, and a game presenting every other
vblank advances two guest vblanks per block: exactly 2x real time.

Measured on BreathOfFire3Recomp (SLPS-00990, 60 Hz panel, OpenGL):
guest vblank counter sampled against wall clock ran 120.4 fps / 2.01x
before, 59.8 fps / 0.997x after. Swaps per second are unchanged at 60
either way -- the fix removes a free-running guest frame, not a swap --
and per-frame GPU cost is unchanged (present_gpu_ms_avg 12.2 -> 12.6,
scene_gpu_ms_avg 4.5 -> 4.1).

Gate both skip sites (VRAM and wide-FBO paths) on the cadence owner,
alongside the existing OSD and interpolation-cadence guards. Only the GL
renderer has this optimisation; the software and Vulkan paths always
present and were never affected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 14:20:09 -04:00
Matthew Stanley 47bda8172e runtime: fix savestates at dirty boundaries 2026-08-29 22:08:31 -07:00
Matt Stanley 09ec090d88 Merge pull request #259 from mstan/feat/multi-disc-launcher
Multi-disc: per-disc identity, launcher roster, and stop copying dumps into projects
2026-08-29 20:04:40 -07:00
Alex Vanderveen a44fb12575 runtime: tag savestates with the disc on a multi-image title
A savestate is whole-machine state. One taken on disc 2 and restored while
disc 1 is mounted resumes a guest that still believes it is reading disc 2,
and the slot list gives no hint: every disc of a set shares one entry_pc, and
entry_pc is the key the slot filenames already use. So the same slot number
meant a different machine on every disc, silently.

Savestates were already isolated per BIOS, as a directory. The disc joins that
as a FILENAME token inside it:

  <saves>/<bios>/state_<entry>_disc<N>_slot<NN>.pst

The BIOS stays a directory because a state is invalid across BIOS images
whatever disc it came from. The disc only has to not collide within one, and a
token does that without another level to browse, back up and migrate.

An empty token leaves the historical name byte-for-byte, so single-disc titles
keep every existing state -- the scope is set only when the roster has more
than one disc.

States saved before this predate the naming and are not listed. They are left
alone rather than migrated: nothing in a .pst says which disc it was taken on,
so placing them would be a guess, and guessing wrong reintroduces exactly the
mix-up this prevents. Startup names them and the directory so they can be
renamed deliberately.

Verified: slot 1 saved on disc 2 and on disc 3 produced disc2/disc3 files side
by side with no collision; booting disc 1 and loading slot 1 rejected with
"missing state_800110C0_disc1_slot01.pst" rather than restoring another disc's
machine; the pre-existing untagged slot 0 was untouched throughout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194TYJAqgyG9a9PJe9uUcgQ
2026-08-29 21:50:00 -04:00
Alex Vanderveen 1102d42022 runtime: [disc] selected is authoritative only when it is present
Launching from an external frontend with disc 2 or 3 chosen opened the game
on disc 1 every time, and the launcher preselected disc 1 with it.

settings.toml carried the right answer:

  [disc]
  path = ".../Final Fantasy VII (USA) (Disc 2).cue"

with no `selected` key. The frontend writes the index only when it can work
out the roster position, and it compares its stored choice path to the disc
path exactly -- a .cue/.bin or normalisation difference misses, so it writes
`path` alone and leaves the index out.

The resolver then treated a MISSING key as selected = 1 and did what it was
designed to do for a stale path: overrode disc 2 back to disc 1. `path` alone
selected a disc correctly before the index existed, so this was a regression
introduced with the index, not a pre-existing gap. A new field may add a way
to choose a disc; it must not take away the old one.

Now the index decides only when it is present. Absent it, the path decides and
the index is derived from it (stem-compared, because everything downstream has
been through normalize_disc_path_for_launch, which swaps a .cue for its .bin
-- exactly the drift that made the frontend's own match miss). An off-roster
path is not evidence for any disc and leaves the default alone.

Verified all three ways against the real install's state: path=disc2 with no
index boots SCUS-94164 and the launcher preselects Disc 2; path=disc2 with
selected=3 still boots SCUS-94165, so an explicit index keeps overriding a
stale path; the roster-derived index keeps a later save writing a consistent
pair.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194TYJAqgyG9a9PJe9uUcgQ
2026-08-29 21:39:21 -04:00
Alex Vanderveen 8f3645c2bc Merge pull request #268 from mstan/master
upstream
2026-08-29 21:07:42 -04:00
Alex Vanderveen 60203cc160 Merge feat/host-pacing-and-hotkey-focus into feat/multi-disc-launcher
Folds the two runtime fixes that were also open as their own PR (#260,
fix/host-frame-pacing) into this branch:

  - runtime: don't fast-forward while the window lacks input focus
  - runtime: pace the frame even when driver vsync should own the cadence

Both branches already shared this branch's base (d14ca691, c481b6cd,
10f443eb), so the merge adds only those two. They are patch-identical to
#260's commits (verified by git patch-id), so #260 is superseded rather
than dropped and can be closed without losing work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L1TpXRJve6WvwyZbSJAbCL
2026-08-29 20:30:39 -04:00
Matthew Stanley 3d1cb5013f runtime: keep PAL cadence off 60 Hz host sync 2026-08-29 17:23:52 -07:00
Biggs McWedge c473fb1555 Use correct timing for PAL games
This fix changes the value of `vblank_cycles` and `g_frame_period_ms` at startup to match what is expected if it detects the game is using a PAL disc image.
2026-08-30 00:36:44 +01:00
Biggs McWedge aba1b8bf43 Fix cdrom_set_disc_scex not being called with correct value
In `main()` in the game runtime, `cdrom_set_disc_scex()` is never called due to an empty serial/game ID being passed to `identify_disc()`. This fix passes the correct ID. It also passes the correct checksum for good measure.
2026-08-29 15:59:17 -07:00
Alex Vanderveen 2ac6d181d9 Merge pull request #262 from mstan/master
upstream
2026-08-29 16:44:57 -04:00
Alex Vanderveen 6d564c8eaf runtime: pace the frame even when driver vsync should own the cadence
present_should_wall_pace() skipped the wall-clock pacer entirely whenever
present_vsync_owns_cadence() was true, trusting the GL swap to block. A swap
that does not block then leaves the guest with NO speed limit at all and the
game simply free-runs -- indistinguishable, to a player, from a stuck
fast-forward.

Measured on FF7 (NVIDIA GL under a compositing WM), 45 s, nothing held,
telemetry off:

  driver vsync "owns cadence"   4771 frames  ~106 fps   1.77x
  PSX_VSYNC=0 (pacer forced on) 2651 frames  ~58.9 fps  1.00x
  this change, default settings 2643 frames  ~58.7 fps  0.98x

Running the pacer underneath vsync is safe because frame_pacer_wait() is
deadline-based rather than a fixed sleep: when the swap has already consumed
the period, now >= next_deadline and it returns immediately after advancing
the deadline. So it costs nothing where vsync works -- and in particular adds
no latency to the healthy path, which is why vsync was given sole ownership
in the first place -- while supplying the cap where vsync does not block.

The frame-interpolation exclusion is kept: that path subdivides this same
interval and would genuinely double-pace.

The startup line is corrected with it. "wall-clock pacer skipped" is how a
bring-up session decides pacing is accounted for, and it is what sent this
investigation hunting a phantom stuck fast-forward instead of an unpaced
present.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194TYJAqgyG9a9PJe9uUcgQ
2026-08-29 15:01:47 -04:00
Alex Vanderveen cf0714852b runtime: don't fast-forward while the window lacks input focus
Hold-to-turbo could run with no key held, indefinitely, and the game would
sit at the fast-forward multiplier until something disturbed it.

SDL_GetKeyboardState() returns a CACHED array that SDL updates from events.
A key whose key-UP is delivered to a different window stays latched there
with nothing to clear it -- and the canonical way to move focus away is
Alt+TAB, where TAB is exactly the fast-forward bind. Regaining focus makes
SDL reconcile the array, which is why the symptom appears to stop as soon as
you click back into the window and press anything (the report that surfaced
this blamed the FPS-readout key for "stopping the turbo"; it was the focus
change that came with reaching for it).

Gate the held-key read on real input focus. This is independently correct:
hold-to-turbo must not run while the player is typing in another
application. Edge-triggered hotkeys are unaffected -- they go through
host_keymap_match() on real key EVENTS, which are only delivered to the
focused window in the first place.

Measured on FF7, 45 s of gameplay with nothing held, telemetry off:
9678 guest frames before, 4771 after. Attribution was confirmed by changing
PSX_FAST_FORWARD_SPEED, which only manual_fast_forward_multiplier() reads and
which is only reached inside this branch -- the speed tracked it, so the
branch was demonstrably executing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194TYJAqgyG9a9PJe9uUcgQ
2026-08-29 15:01:33 -04:00