Commit Graph

626 Commits

Author SHA1 Message Date
Matthew Stanley 76a53d9090 Invalidate per-PC code caches when a savestate replaces RAM
Several consumers cache a classification of the instruction at a given PC and
key that cache on g_dirty_ram_code_gen, per the contract stated where the
counter is defined (runtime/src/memory.c): "Consumers that cache per-PC
classifications of RAM instructions compare against this and re-derive after any
code change - a cached kind must never survive an overlay reload." Today that
counter is bumped from exactly two places, both on the instrumented store path:
a page's clean->dirty transition, and dirty_ram_mark_executable_range().

A savestate restore reaches neither. BS_SEC_RAM applies a raw memcpy over all
2 MB, so code identity at any address can change completely while no page makes
a clean->dirty transition and no executable range is marked. Every cache keyed
on the generation therefore keeps serving verdicts derived from the PREVIOUS
contents. The overlay loader's negative "no native owner" cache is the damaging
one, because overlay_loader_dispatch() consults it before rediscovery, so one
stale entry can pin an address to the wrong implementation for the rest of the
run.

Note the same restore already invalidates psx_kernel_bless_note_range(), so the
path is aware it must invalidate downstream caches - it only ever did one of
them.

Fix: add dirty_ram_invalidate_code_caches() and call it after the BS_SEC_RAM
memcpy. dirty_ram_set_bitmap_words() (the BS_SEC_DIRTY apply path) calls it too,
since a wholesale bitmap replacement changes which addresses are RAM-resident
code by the same argument. Bumping the generation is sufficient by construction:
each cache stores the generation it was derived under and re-derives on
mismatch, so this fixes every consumer at once instead of patching them
individually.

Scope, stated honestly: this closes a real latent correctness gap in the load
path and is worth having on its own. It is NOT a fix for the separate,
still-open bug where loading a gameplay state into a process that never entered
that stage leaves the player unable to move - that was measured and the overlay
cache is demonstrably NOT its cause (the dirty bitmap is identical between a
working and a broken instance, and covers only the kernel window, so the
player's code is static-dispatched in both). No behaviour change for a process
that never loads a state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 23:39:41 -07:00
Matthew Stanley 68667cd58b disc+bios: either file the user picks resolves, mounts, and verifies
Two path-resolution defects where the runtime silently accepted only one of
two equally-valid spellings of the same input.

1. Disc: picking the .cue threw away the disc's TOC.

normalize_disc_path_for_launch() swapped any .cue for its same-named .bin
unconditionally, and every disc entry point (game.toml, --disc, the cached
disc.cfg, the launcher's Change Disc, the first-run dialog) went through it.
That discarded the cue's table of contents -- every CD-DA track and pregap
with it -- even though iso_reader.cpp has full multi-FILE cue support that
the swap defeated before it was ever reached. In the other direction,
picking a redump audio-track bin mounted that track as the whole disc, which
cannot boot.

resolve_disc_path() (runtime/include/disc_path.h) now canonicalizes any pick:

  .cue that resolves            -> mount the cue (TOC intact)
  .cue with a missing bin       -> the payload it still names, else a
                                   stem-matched sibling image
  .bin/.iso/.img owned by a cue -> mount that cue
  bare image, no cue            -> mount it

identify_disc() resolves through the same code, so the header/serial/CRC32
verdict is read from the data track no matter which member was picked.

While here: the mount path and the verify path had two divergent private cue
parsers (quoted-only + case-sensitive vs quoted-or-bare + case-insensitive),
so they could disagree about the same file. Both now use one parser
(runtime/include/cue_sheet.h), which additionally handles CRLF, backslash
paths, and case-insensitive payload lookup for case-sensitive filesystems.

2. BIOS: the multi-BIOS rename made configs and dump folders incompatible.

SCPH*.toml name "US-PSX-SCPH1001.BIN" while every pre-existing dump folder,
bios.cfg and tools/regen_bios.sh default name "SCPH1001.BIN". Either
combination fataled with "cannot open BIOS file" despite the right ROM being
present. resolve_bios_rom() (recompiler/include/bios_rom_alias.h) matches on
the model token -- the last '-'-separated component of the stem -- so both
conventions resolve in both directions without a hardcoded alias table.
Applied at the config loader, the positional psxrecomp-bios CLI, and each
rung of the runtime's BIOS search.

The positional CLI also derived out_stem from the raw filename, so a
region-qualified ROM would have emitted US-PSX-SCPH1001_full.c -- a name
runtime.cmake cannot link. It now derives the model token.

Also fixes SCPH5552.toml, whose rom field omitted the bios/ directory that
its two siblings have.

Tests: test_disc_path_resolve (36 checks, five dump layouts) and
test_bios_rom_alias (15 checks, both directions + negative cases), both
registered in runtime/CMakeLists.txt. Registered the pre-existing
test_iso_reader_cdda, which was wired into no build.

Verified on real dumps: Ape Escape .cue and .bin both give
SCUS-94423 / NTSC-U / crc=C6F455BC; Tomba 2's multi-track dump gives
crc=1FB8C151 and a 2-track TOC from the .cue, the data bin, and the CD-DA
bin alike.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 22:17:52 -07:00
Matthew Stanley a8aa640b79 runtime: name the exact CLI flag when no file picker is available
On macOS/Linux there is no native GUI file picker, and in headless mode a
picker would block, so pick_runtime_file() bails when it can't resolve the
BIOS/disc interactively. The old message ("<title> requires a command-line
path on this platform") never said WHICH flag to pass, so a first-time
from-source user on Linux hit "no BIOS selected; exiting." with no idea what
to do.

Thread the responsible flag (--bios / --disc) into pick_runtime_file and print
an actionable message with the flag and a full example command, on both the
headless and the no-GUI-picker paths. Points at the game README's "Building
From Source" for the full run line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 13:02:44 -07:00
Matthew Stanley bfa760f43b build: fail early with a clear message when recompiled game C is missing
The runtime links generated/<serial>_{full,dispatch}.c, produced by a separate
recompiler step run before the build. Those paths are marked GENERATED so cmake
configure succeeds before that step has run — which meant skipping generation
surfaced only as a raw "cc1: fatal error: <file>: No such file or directory"
deep in the build, with no hint that a step was skipped or what produces the file
(exactly the trap a from-source builder on a fresh checkout hits).

Add runtime/check_generated_sources.cmake and wire it into
psxrecomp_add_runtime_target as a build-time dependency of the runtime target:
a missing game source now aborts at build start with the files named and the
exact fix (build recompiler -> extract EXE -> generate -> rebuild), and configure
emits an early non-fatal warning. Framework-wide, so every game inherits it.

Also fix the docs/BUILDING.md "build and run a game" recompiler invocation (the
framework is a submodule at psxrecomp/, not ../psxrecomp/) and add the
recompiler-build step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 09:36:14 -07:00
NyperYuhgard 2ebb2b1a6b Multi-Bios (seeds required) 2026-07-24 01:38:56 -05:00
Alex Vanderveen 09067f29dc fix title bar separator character 2026-07-23 19:05:30 -04:00
Matt Stanley 0920787d4b Merge pull request #74 from OpokXeno/fix/present-letterbox-uv-inset
present: fix short-band letterbox for native display modes + UV edge bleed
2026-07-23 13:11:58 -07:00
Matthew Stanley f86087ad94 Remove stale RmlUi launcher remnants 2026-07-23 00:29:22 -07:00
Alex Vanderveen 2b2573bd82 fix(pgo): soft-exit on MinGW SIGTERM/SIGINT for .gcda flush
Windows CI train uses MSYS2 kill; without handlers atexit/gcov never runs.
Also handle console Ctrl+C/Break/Close.
2026-07-23 02:57:05 -04:00
Alex Vanderveen 0fe35cebad fix(pgo): soft-exit on SIGTERM/SIGINT so gcov flushes .gcda
Default signal termination skips atexit, so -fprofile-generate never
writes profiles when train scripts kill the process.
2026-07-23 02:41:31 -04:00
Alex Vanderveen 980d170850 fix(netplay): match recomp-ui join(guest_bind) ABI
UI now passes an in/out guest UDP bind (prefer 7778). Accept the 4th
argument, reject :0, and stop advertising ephemeral 0.0.0.0:0 on online
join so Windows CI links and online guests get a dialable endpoint.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 01:50:15 -04:00
Alex Vanderveen c34c1460e0 Merge remote-tracking branch 'refs/remotes/origin/master' 2026-07-23 01:27:44 -04:00
Alex Vanderveen 1347e49dfc patch FMV playback, fix artifact flicker on cutover to new video 2026-07-23 01:22:11 -04:00
Matthew Stanley cece00296a feat: add adaptive native widescreen
Add a resize-driven aspect option, keep native-wide culling and spawning hooks engaged across live aspect changes, and widen host GP0 draw-area pruning only for the active wide framebuffer.

Credit OpokXeno's independently discovered draw-area pruning fix documented in PR #73.

Co-authored-by: Opok <novelasevilliousesp@gmail.com>
2026-07-22 21:59:31 -07:00
Alex Vanderveen 1a73e9826c patch hosted lobby launch game 2026-07-23 00:44:53 -04:00
Alex Vanderveen 8a362a36ad build: drop lib/recomp-ui; games vendor UI at repo root
Discover recomp-ui from CMAKE_SOURCE_DIR/recomp-ui (or RECOMP_UI_ROOT).
Keep lib/recomp-net in the engine.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 00:27:15 -04:00
Opok d6a52cb9d8 present: fix short-band letterbox for native modes + UV edge bleed
Two present-path fixes:

1. The short GP1(07h)-band letterbox (MotK 128-line FMV) fired for any
   frame under 240 lines, which also catches games whose native display
   mode is short full-time (Xenogears runs 320x216): every 4:3
   menu/load and every FMV got a ~10% vertical squash. No numeric
   threshold separates the cases (216 < 224 too), so split by purpose:
   15-bit game frames never shrink (adjustment removed from
   gl_renderer_present_vram), and the FMV paths keep the correction
   only for genuinely windowed video bands (gl_renderer_present, and
   the SDL path gated on pin_43 && depth24_frame) with the band under
   80% of the 240-line field. MotK's 128-line FMV keeps its aspect fix;
   native 216/224-line display modes fill the rect as before.

2. present_target_quad mapped quad corners to texel edges, so with
   GL_LINEAR the outermost dest pixels blend the border texel with
   neighboring VRAM outside the content rect (visible 1-2px edge
   stripes with linear present filtering, e.g. menu frames showing the
   adjacent framebuffer). Center-map the UVs with a half-texel inset;
   interior sampling is unchanged. Covers both callers (canonical VRAM
   present and wide-FBO present).
2026-07-23 00:35:36 +02:00
Matthew Stanley 27de42d073 vulkan: fix draw-area batching and enable builds by default 2026-07-22 12:07:32 -07:00
Matthew Stanley e558c971ab runtime: restore native Tomba FMV overlays 2026-07-22 00:35:26 -07:00
Matthew Stanley 1ea7e28b10 runtime: fix debug cycle throttle symbols 2026-07-21 22:37:43 -07:00
Matthew Stanley d482ab67a2 runtime: resume compiled code after patched FMV entries 2026-07-21 22:32:07 -07:00
Matthew Stanley 6c914a763f runtime: quiet high-frequency diagnostics during FMV 2026-07-21 22:24:55 -07:00
Matthew Stanley f1bdefad20 runtime: batch faithful FMV load-cycle charges 2026-07-21 22:23:15 -07:00
Matthew Stanley 87491cb035 launcher: integrate recomp-ui netplay flow 2026-07-21 21:39:28 -07:00
Matthew Stanley 0527806ce3 Repair netplay PR rebase for MotK bring-up 2026-07-21 21:38:05 -07:00
Alex Vanderveen b6850026a4 Restore missing cycle/icache link symbols for MotK.
Re-add watchdog/PC-sample fire wrappers and overlay icache_fetch_fn;
stop shadowing the inlined psx_advance_cycles with a missing extern.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 21:38:05 -07:00
Alex Vanderveen 30df7acaf2 Restore missing cycle/icache link symbols for MotK.
Re-add watchdog/PC-sample fire wrappers and overlay icache_fetch_fn;
pull inlined psx_advance_cycles via psx_cycles.h.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 21:38:05 -07:00
Alex Vanderveen fe7913cbca Fix psx_cycles / audio linkage for MotK CI.
Export service_to_now, rename advance to *_slow for the header inline,
and give g_audio_host_rate C linkage.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 21:38:05 -07:00
Alex Vanderveen 916b8ca7d3 Fix incomplete load/icache refactor for MotK CI.
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>
2026-07-21 21:38:05 -07:00
Alex Vanderveen 7cf9958135 FMV optimizations 2026-07-21 21:38:04 -07:00
Alex Vanderveen 0b890cdc13 lobby game version control 2026-07-21 21:38:04 -07:00
Alex Vanderveen 31015ceab1 feat: launcher polish and recomp-net lobby netplay
Wire MotK-style lobby (recomp-net submodule, WS client, match_caps),
netplay delay-sync, and launcher UX so host sim settings stay aligned
across peers.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 21:37:57 -07:00
Claudio Varandas 55120e102e runtime: implement COP0 software interrupts (CAUSE.IP0/IP1 vs SR.IM0/IM1)
Jackie Chan Stuntmaster (SLUS-00684) hard-blackscreens after the intro
logos: the game's exception dispatcher is re-entrant by design. Its
stage-1 handler saves SR/EPC to a single save slot, then executes
mtc0 CAUSE,0x100 + mtc0 SR,0x101 and relies on the resulting COP0
SOFTWARE interrupt (CAUSE.IP0 & SR.IM0 & IEc) to vector into its
stage-2, which acks the CD INT at the controller, delivers events, and
restores the saved SR. The runtime only ever considered the INTC line
(i_stat & i_mask) gated by SR.IM2, so the sw-int was never taken:
stage-2 never ran, CD INT3 stayed unacked (process_pending freezes the
completion countdown while irq_flag != 0), and the machine deadlocked
with the display disabled and SR parked at 0x101 (IM2 stripped -> all
hardware interrupts dead).

Three changes, all architectural (no per-game logic):

1. interrupts.c psx_check_interrupts(): a pending software interrupt
   (CAUSE & SR & 0x0300, plus IEc) is now a deliverable source. The
   fast path, the pending check, and the SR gate each consider it; the
   INTC path still requires IM2, the sw path only its own IM bit.
   Exception entry sets CAUSE.IP2 only when the hardware line is what
   is being delivered, so a pure sw-int presents the guest-written
   IP0/IP1 bits unmodified (the dispatcher discriminates its stages by
   exactly these bits).

2. interrupts.c: save/restore exception_jmpbuf across delivery. The
   sw-int nests inside the hardware-interrupt handler window; the
   nested setjmp used to overwrite the single global jmpbuf, so the
   OUTER handler's later RFE longjmp'd into the exited inner frame
   (host stack corruption, SIGSEGV).

3. dirty_ram_interp.c: deliver immediately after an interpreted MTC0
   to CAUSE or SR that makes a sw-int deliverable, with EPC = the
   committed next pc. Hardware vectors within the next instruction;
   waiting for the next block boundary opened a window in which a
   second hardware IRQ re-entered the (legitimately non-reentrant)
   guest dispatcher and destroyed its saved SR. precise_irq_deliverable
   gains the same sw-pending condition.

Verified on SLUS-00684: boots through BIOS/logos/FMV into rendered
3D gameplay at 60 fps, SR observed at the canonical 0x40000401, CD
irq_flag acked, MDEC decoding. BIOS-only boot unaffected.

Known scope limit: compiled (static) MTC0 sites do not yet trigger the
immediate check; the raising writes observed in the wild live in the
BIOS-installed, dirty-RAM-interpreted trampoline, which this covers.
2026-07-22 00:54:04 +01:00
Matthew Stanley 9228f32c00 runtime: keep Vulkan opt-in and remove old launcher
Remove the in-tree launcher implementation and its submodules now that game projects use recomp-ui for launcher support.

Keep Vulkan hidden from the launcher unless a game explicitly sets [video] offer_vulkan = true, and clamp stale Vulkan settings back to OpenGL when it is not offered.

Co-authored-by: Martin Penkava <mpenkava1337@gmail.com>
2026-07-21 16:31:36 -07:00
Matthew Stanley da64f44e82 launcher: expose the Vulkan renderer
Cycle the renderer selector through Software, OpenGL, and Vulkan while retaining OpenGL-only option gating.

Co-authored-by: Martin Penkava <mpenkava1337@gmail.com>
2026-07-21 15:55:16 -07:00
Matthew Stanley f1fd8ec848 vulkan: expose present blocking telemetry
Record queue-wait, acquire, and present durations plus presentation timestamps in the existing vk_perf ring.

Co-authored-by: Martin Penkava <mpenkava1337@gmail.com>
2026-07-21 15:55:11 -07:00
Matthew Stanley f5498f796c vulkan: prefer MAILBOX for tear-free presentation
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>
2026-07-21 15:55:06 -07:00
Matthew Stanley af9de64951 vulkan: reuse persistently mapped staging buffers
Cache best-fit host-visible staging allocations across queue-idle reclamation points and safely detach long-lived CPU-present buffers during teardown.

Co-authored-by: Martin Penkava <mpenkava1337@gmail.com>
2026-07-21 15:54:42 -07:00
Matthew Stanley b5e9f763fe vulkan: batch work into one command buffer
Keep a shared work command buffer open across logical GPU operations and submit it at the existing synchronization boundary.

Co-authored-by: Martin Penkava <mpenkava1337@gmail.com>
2026-07-21 15:54:29 -07:00
Matthew Stanley 92cbb1a327 vulkan: order back-to-back color passes
Add same-layout color attachment barriers before geometry and wide-surface render passes so prior writes are visible to LOAD and blend operations.

Co-authored-by: Martin Penkava <mpenkava1337@gmail.com>

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 15:54:17 -07:00
Matthew Stanley 7f9f59a2bb vulkan: wait for acquire before transfer writes
Include the transfer stage in the swapchain acquire wait mask because the present command buffer clears and blits the acquired image with transfer operations.

Co-authored-by: Martin Penkava <mpenkava1337@gmail.com>

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 15:54:12 -07:00
Matthew Stanley c25931e3b7 vulkan: align packed upload offsets
Pad each packed R16 upload region to an even texel count so subsequent VkBufferImageCopy offsets retain the required four-byte alignment.

Co-authored-by: Martin Penkava <mpenkava1337@gmail.com>

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 15:54:07 -07:00
Steve Gilissen 1ae1cfdaf6 runtime: add mod frame hooks
Add a small generic frame-hook surface for statically linked mod code. Mods can
register C callbacks with mod_register_frame_hook(); the runtime invokes them
after normal input sampling and before presentation/widescreen frame work.

Expose the SDL window symbol with C linkage so linked mod code can integrate
with SDL APIs such as window grab control.
2026-07-21 15:45:45 -07:00
Opok 4412bc7a7d widescreen: add per-primitive X-reject bound-load cull sites
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.
2026-07-21 15:02:20 -07:00
Matthew Stanley 2ba20f5af7 runtime: make FPS stderr logging opt-in 2026-07-21 14:53:47 -07:00
Opok a5fdf1ad9a widescreen: add side frustum-plane normal-X cull sites
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.
2026-07-21 14:38:55 -07:00
Matthew Stanley e4e27f02b5 runtime: optimize MDEC FMV hot paths
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>
2026-07-21 14:29:38 -07:00
Matthew Stanley b0e923b9b7 Exercise PR #48 savestate restore fixes
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>
2026-07-21 13:52:26 -07:00
Matthew Stanley d7815862e1 runtime: use same-stem BIN for selected CUE discs 2026-07-21 11:10:21 -07:00
Matthew Stanley 647b81752f runtime: tri-state fullscreen (windowed/borderless/exclusive)
The shared recomp-ui launcher already exposes fullscreen as a universal
tri-state (RecompLauncherCSettings.fullscreen: 0 off / 1 borderless /
2 exclusive), but the PSX runtime collapsed it to a bool everywhere and
only ever used SDL_WINDOW_FULLSCREEN_DESKTOP. Widen the runtime to carry
the full tri-state end to end, matching the SNES/Genesis engines:

- config_loader: UserSettings::fullscreen is now int (0/1/2) instead of
  bool. settings.toml persists it as a bare int; loading still accepts
  the old bool form (true -> 1) for back-compat with settings.toml files
  written before this change, then clamps to [0,2].
- main.cpp: removed every `!= 0` / `? 1 : 0` collapse between
  UserSettings, the seed struct, and RecompLauncherCSettings, so 0/1/2
  round-trips unchanged through the launcher and back into settings.toml.
- main.cpp: win_flags now maps 2 -> SDL_WINDOW_FULLSCREEN (exclusive
  mode change) and 1 -> SDL_WINDOW_FULLSCREEN_DESKTOP (borderless), via
  a small psx_fullscreen_flag_for_mode() helper shared with the hotkey.
- main.cpp: the Alt+Enter / Cmd+Ctrl+F hotkey now detects "currently
  fullscreen" by testing the SDL_WINDOW_FULLSCREEN bit (set in both
  SDL_WINDOW_FULLSCREEN and SDL_WINDOW_FULLSCREEN_DESKTOP) instead of
  only the DESKTOP flag, and toggles between windowed and the
  CONFIGURED tri-state mode rather than hardcoding DESKTOP.
- launcher.cpp/launcher.rml (the legacy RmlUi launcher, still built
  whenever PSX_LAUNCHER is on): fullscreen is now a 3-way cycle button
  (Off/Borderless/Exclusive) following the same cycle_*/*_label pattern
  as the existing CRT/texture-filter controls, instead of an On/Off
  toggle.

Validated with g++ -fsyntax-only (mingw64) against config_loader.cpp
standalone, and main.cpp with no launcher flags, PSX_LAUNCHER only,
RECOMP_LAUNCHER only (against the real recomp-ui RecompLauncherCSettings
header), and both together - all clean. launcher.cpp (RmlUi) was
reviewed by exact pattern-match against the file's existing cycle_crt/
crt_label code (lib/RmlUi isn't checked out in this worktree, so it
could not be compiled).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 22:34:58 -07:00