Commit Graph

472 Commits

Author SHA1 Message Date
Alex Vanderveen 1982e7dcb5 netplay: host relay for online rooms (amends §108 "online is always SFU")
An online room this build hosts asks the lobby server for transport "host"
(match_caps.relay): the host carries the match on its own UDP port and
guests dial it; the server's relay (SFU) is the fallback when a guest's
probe fails (recomp-net-server WS_LOBBY.md "Host relay"). The mechanics are
recomp-net's recomp_net/host_relay.h, pinned here (lib/recomp-net b0751b7);
this engine's own lobby-client copy gets the same glue as rnet_lobby_client:
the cap (emitted only when asked), the launch's transport ->
PsxLobbyJoinInfo.transport_host (force_input_relay 0; the host accepts
first / hubs instead of dialling the guest's LAN address), each seat's
published path / path_fresh, host_relay_step() from psx_lobby_pump, and
unmap on leave / disconnect.

psx_netplay: transport_host takes the LAN / hub path instead of the "MotK
online needs the SFU" rule. main.cpp: relay_host_get/set + relay_status
for recomp-ui (Network Settings / MATCH SETTINGS / ROOM "Match Relay"),
transport_host on the launch and into PsxNetplayConfig, and the
host==guest-advertise SFU inference skipped on a host launch; all under
RECOMP_LAUNCHER_HAS_HOST_RELAY so older recomp-ui pins still build.

Compile-checked against workspace recomp-ui 58604b0 (MOTK scratch
configure: full build and link).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-01 19:10:30 -04:00
Alex Vanderveen 35d604ab80 netplay: matches the Retro hub negotiates -- ingest_launch and --netplay-query
The hub holds the lobby seat; the game settles everything itself
(retcomm-launcher docs/NETPLAY_HANDOFF.md):
- psx_lobby_ingest_record replays the hub's seat/room/launch messages
  through handle_server_json, so fill_launch answers as in-game.
- --netplay-query answers what this build's lobby would send: disc_fp,
  set_ready (BIOS offer), start (default caps; settled from a room).
- set_ready/start builders and the host start-caps settle are factored out
  unchanged and shared by in-game PLAY and the query.
- A handed-off match ends the process (the hub holds the room).

Two CTR processes from records, local relay: all 80 live digests equal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:10:30 -04:00
Matthew Stanley 2163035e9a netplay local viewport: projection renderer, hi-res half present, state gate
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 16:02:08 -07:00
Matthew Stanley 3b41a81365 netplay: debug-server input override drives headless peers
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 15:07:43 -07:00
Matthew Stanley 85c88d269c widescreen: auto_ui_anchor = "in_place" squashes each HUD run about its own centre
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 11:13:30 -07:00
Matthew Stanley 53521866b4 SDL3: separate Windows desktop borderless and exclusive fullscreen 2026-09-30 23:30:28 -07:00
Matthew Stanley 7c7133fbb4 Integrate MMX6 adaptive renderer with current framework master
Preserve current segmented dispatch, active-plan mod hooks and software scale limits. Extend function filters through the active plan, retain the last-store pointer with overlay ABI 25, and keep default config fingerprints stable. Refresh BIOS emitter stamps after byte-identical regeneration and reconcile regression fixtures.
2026-09-30 20:23:16 -07:00
Matthew Stanley ca14a118f0 fix(runtime): grow software present buffers for adaptive widths 2026-09-30 17:11:39 -07:00
Alex Vanderveen 170370a440 launcher: prepare a retail BIOS from the wizard with progress, not at Play
A buildable dump used to verify as usable and then block at Play behind a
modal while the module compiled. Now:

- ae_bios_verify reports a buildable, not-yet-cached dump as needs_regen,
  and the runtime wires recomp-ui's new bios_prepare_with_progress job
  (ae_bios_prepare) whenever the toolchain is present and no Generate host
  is: the launcher runs the build as a progress job from the wizard's
  "Prepare BIOS" button or the Play prompt, re-verifies, and resumes the
  wizard. Generate & rebuild is never offered for a BIOS in a bundled build.
- psx_bios_module.c gains a progress sink fed from the builder's output
  lines (dump / emit / compile / self-check / published map to stage
  percentages with readable messages); run_command streams lines on both
  platforms.
- The launch-time build stays as the fallback for --bios / --no-launcher.

Requires recomp-ui 9f86c49 (GameInfo.bios_prepare_with_progress).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-30 20:10:57 -04:00
Alex Vanderveen 60919f5a87 bios: build a retail backend from the player's own dump as a loadable module
Bundled releases link OpenBIOS only; retail-BIOS-derived C never ships.
"Bring your own BIOS" now works without it: when a dump matching a shipped
profile (psx_bios_known_images.h) is selected, the runtime builds that
backend on the player's machine with the overlay toolchain the release
already carries, caches it, and loads it as one more registered backend.

- runtime/include/psx_bios_module.h: the module contract. An overlay-style
  self-contained shared library (no imports from the executable) exporting
  psx_bios_module_{abi,codegen_hash,init,backend}; reaches the runtime only
  through OverlayCallbacks plus a new PsxBiosModuleCallbacks table (traps,
  dirty-RAM/kernel-bless probes, fntrace, log_probe) and pointer aliases of
  the globals the generated dispatcher touches. Its #defines rewrite the
  emitter's own extern lines, so the generated C compiles unchanged.
- runtime/include/psx_bios_module_glue.c.inc: module-side forwarders and the
  four exports, compiled after overlay_dispatch_preamble.c.inc.
- runtime/src/psx_bios_module.c: host side. Cache path
  <exe>/cache/bios/<os-arch>/bm<abi>_<codegen hash>_f<flavor>/<STEM>_<crc>,
  ABI + codegen-hash gated load, callback fill, and a blocking build via
  overlay_toolchain/bios_module_build.py (gcc via compiler.txt or PATH,
  else tcc), then psx_bios_register().
- psx_bios_backend.c: the registry is now a mutable table seeded from the
  CMake-generated psx_bios_builtin_registry by a constructor, with
  psx_bios_register(); every existing walker keeps working.
- main.cpp: validate_bios_for_launch acquires a module (announcing a first
  build) before declaring a mismatch; discovery accepts a buildable dump;
  the launcher verify reports it usable ("ready" / "compiled on first
  launch") with no Generate & rebuild; the BIOS row shows whenever the
  toolchain is present; bundled_only respects module support.
- emitter: the CPS-marker constructor is not emitted in a module build.
- overlay_api.h: OverlayCallbacks gains a struct tag;
  overlay_loader_callbacks() exposes the host table before loader init.
- tools/bios_module_build.py (ships in the toolchain): identity + profile
  SHA-256 gate, staged emitter tree, emit, three-TU compile, ctypes
  self-check, atomic publish. release_stage.py stages psxrecomp-bios, every
  bios/*.toml, the seeds and the builder into overlay_toolchain/.
- Tests: runtime/tests/test_bios_module_host.c against three fake modules
  (good / wrong ABI / wrong hash) on real dlopen; recompiler/tests/
  test_bios_module_build.py builds the bundled OpenBIOS end to end and reads
  the descriptor back; guards extended.

Verified on Twisted Metal 4 (bundled build + staged toolchain, headless):
first launch with --bios SCPH1001.BIN builds and loads the module and boots
on image=SCPH-1001; the second launch loads it from cache.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-30 19:26:29 -04:00
Matt Stanley fcc685ce1d Merge pull request #435 from RetroPortingToolKit/fix/isc-icache-invalidate
Apply cache-isolated (IsC) stores to the caches, as Beetle does
2026-09-30 15:00:19 -07:00
Alex Vanderveen 0b6baa49c4 launcher: never offer "Generate & rebuild" for a BIOS in a bundled build
A shipped bundled build links OpenBIOS only and carries no CLI, sources
or toolchain, yet the codegen host forced the BIOS row on and a retail
pick answered with needs_regen ("Generate & rebuild to switch"), a step
that cannot run there.

- The BIOS row is shown only when a choice can mean something: a retail
  backend is linked, nothing is linked yet (setup host), or the codegen
  host actually wired Generate (prepare_with_progress). main.cpp decides
  after host_apply instead of the host forcing has_bios.
- ae_bios_verify short-circuits a bundled-only build with a plain
  "runs its bundled OpenBIOS only" message before the identity checks,
  whose "this build expects SCPH-1001" wording names the pinned stem,
  not a linked backend, and sets needs_regen only when Generate exists.
- test_bios_selection_guards pins both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-30 17:54:53 -04:00
Alex Vanderveen 2b3f8c66c5 Merge pull request #445 from Alexbeav/up/netplay-exit-reason
netplay: say why a match ended, and end a boot mismatch in 3 s
2026-09-30 16:58:02 -04:00
Alex Vanderveen d8b644bf6d Merge pull request #444 from Alexbeav/up/netplay-bios-identity
netplay: settle the lobby BIOS by image, not by "any retail"
2026-09-30 16:52:27 -04:00
Alexandros Mandravillis 5c7a37146c netplay: say why a match ended, and end a boot mismatch in 3 s
A netplay match that ends early returns both players to the lobby. The
runtime knew why (netplay_soft_exit's origin) but only printed it to
stdout, so players saw the lobby again with no reason. A boot-digest
mismatch (peers booted different BIOS images or boot settings) first
held a black window for the full 20 s admit-stall watchdog.

netplay_soft_exit now records a sentence for its origin, and the soft
return to the lobby puts it on the launcher's status line through
last_error (the room and browser views already show it). The dig0 gate
timestamps a mismatch; once the same pair of digests has held for 3 s,
the watchdog ends the match with its own reason. A peer that gets the
other side's BYE while it sees the same mismatch reports the mismatch,
not "the other player left". A CLI match prints the sentence to stderr.

The origin-to-text map and the grace decision live in
netplay_exit_reason.c, covered by netplay_exit_reason_test. Builds on
the previous commit's psx_lobby_set_last_error.
2026-09-30 15:59:58 +03:00
Alexandros Mandravillis 5fbd11921a netplay: settle the lobby BIOS by image, not by "any retail"
The lobby's BIOS identity was a two-value token, openbios or scph1001,
and scph1001 meant "some retail dump". An SCPH-5552 peer and an
SCPH-1001 peer both offered it, the host settled it, each peer booted
its own image, and the rollback boot digest never matched: about 20 s
of black window, then both players back in the lobby with no reason
(reported in #243).

Each offer now carries the CRC-32 of the retail image that peer would
boot (online bios_offer.retail_crc; LAN JOIN line 6). The host settles
retail only when every seat offers the same CRC, else OpenBIOS when
every seat links it, else refuses to start and puts the reason on the
launcher's status line ("Players use different BIOS images (SCPH-5552
and SCPH-1001) ..."). The settled CRC ships in match_caps
(session_bios_crc) and on MOTK1 START line 7, and each peer boots only a
dump with that CRC. Rooms where every peer has the same image keep
working, including SCPH-5552-only builds with no OpenBIOS. Old peers
send no CRC, so a mixed old/new room falls back to OpenBIOS or refuses;
a session from an old host still accepts any retail dump. The lobby
server relays bios_offer and match_caps as opaque objects, so it needs
no change.

The rule lives in netplay_bios_settle.c, shared by the online and LAN
paths and covered by netplay_bios_settle_test. last_error grows to fit
one sentence, with psx_lobby_set_last_error to set it.
2026-09-30 15:58:52 +03:00
Shokunin 825a89a9c5 runtime: keep host stores out of IsC, as DMA is
Only CPU stores are cache-isolated. Mods, the FMV skip, debug pokes, the
bg2d ring fill and the exception sentinel write guest memory from the host;
they now go through psx_host_write_*, which bypasses isc_store the way DMA
does, so a host store inside a FlushCache window reaches memory instead of
rewriting an I-cache tag. The render-pass checkpoint restores the new depth
counter like the DMA depth. isc_store_test covers it (a lane-0 host store in
tag-test mode would otherwise mark a cold line valid).

Docs: the SCPH-1001 residual starts at kernel init 0x598, and without #429
the fix removes 113 of the 126 OpenBIOS flush cycles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:55:26 -07:00
Cyanoxide 4aa72128fa Make the depth24 trailing margin configurable per title 2026-09-29 20:00:35 +01:00
Matthew Stanley 58bdf987d9 Merge master into feat/internal-resolution: combine with #422/#423/#424
Keep #422's 1x GL_LINES batch (gated s_hr_scale == 1 && !s_hiw) beside the >1x quad batch; FLIP capture uses hiw_capture_size; render passes use s_hr_scale and are refused under s_hiw.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 11:23:28 -07:00
Matthew Stanley 2bf4739f44 Merge master into feat/internal-resolution-settings: regenerate TCP_COMMANDS.md
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 11:22:53 -07:00
Matt Stanley f8a6f4f185 Merge pull request #425 from RetroPortingToolKit/feat/internal-resolution-gl
Raise the GL internal-resolution ceiling to the driver's limits
2026-09-29 11:22:28 -07:00
Matthew Stanley 914d8cb305 Merge master into feat/frame-rate: reset_on_load onto #420's fingerprint struct
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 11:22:02 -07:00
Matt Stanley d5e41d7b7d Merge pull request #423 from RetroPortingToolKit/feat/frame-rate-blend
Add a flip-aware frame-interpolation source (opt-in)
2026-09-29 11:21:11 -07:00
Matt Stanley b2f72d7cce Merge pull request #422 from RetroPortingToolKit/feat/adaptive-widescreen
Add [widescreen.cull] bgez_sites and clip_edge_x_load_sites
2026-09-29 11:20:58 -07:00
Matt Stanley df4e905ee8 Merge pull request #428 from RetroPortingToolKit/fix/rematch-session-start
Run the full session start on the lobby rematch
2026-09-29 11:20:45 -07:00
Matt Stanley 4ea39cf942 Merge pull request #421 from RetroPortingToolKit/fix/mod-session-hygiene
Reset mod-owned presentation state at every session start
2026-09-29 11:20:12 -07:00
Shokunin bd65c814f7 Run the full session start on the lobby rematch
The lobby rematch re-enters the emulator through goto session_reboot,
below the first-boot session block. After its commit (offline) or
netplay clear it reset mod-owned presentation state, but it never ran
mod_runtime_activate_plugins() or the controller, load-acceleration and
disc-speed resets. So an offline rematch with mods enabled applied the
plan's main-EXE and disc patches and ran its VBlank callbacks, but no
activation callback ran, and the function-entry hook table, which the
commit empties and only activation rebuilds, stayed empty. It also
mounted the stock disc even when the plan had built a derived disc
image.

start_mod_session() now holds the first-boot sequence, unchanged:
clear the controller-mode overrides and presentation policies, load
acceleration and disc speed; run reset_mod_owned_presentation();
activate; then apply the netplay local viewport, the controller-mode
overrides and load acceleration. The first boot and the rematch both
call it after their commit or netplay clear, and both then take the
mounted disc from session_disc_path(), which returns the plan's derived
image when there is one and otherwise the stock disc. Activation still
precedes renderer and window creation on the rematch, because those
happen after session_reboot on every session. The rematch re-applies
its 4:3 Settings clamp before the session start, so a plugin's
activation or the netplay local viewport can still set the aspect.

A netplay rematch clears the plan, so activating it runs nothing and
the match stays vanilla. The first boot's behaviour is unchanged.

Tests: test_mod_owned_display_controls.py now requires activation and
the mod-owned reset to happen only inside start_mod_session, in the
order reset, activate, apply. It requires both paths to call it after
their commit and before the disc mount, and the rematch 4:3 clamp to
come before it. mod_runtime_test adds the lobby rematch sequence:
activating after a netplay clear runs no callback, VBlank plugin or
hook; the offline rematch's commit and activation re-run the
activation callback and VBlank plugin and arm exactly the plan's hook.
test_mod_load_acceleration.py follows the reset block into the lambda body.
Two comments that said a soft return never re-applies the controller-mode
override are corrected: test_launcher_pad_mode_wiring.py and
test_launcher_pad_mode_resolve.cpp. MOD_PACKAGES.md now describes the
rematch as a full session start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:42:17 -07:00
Shokunin 69e6b6b552 Render passes: refuse during fast-forward; say why passes are unavailable
Manual fast-forward presents every second VBlank, so the plan still saw
presents and kept rendering passes, spending host time the fast-forward
wants, although the API said turbo refuses them. main.cpp now records when
presentation is sped up (manual fast-forward, turbo-through-loads, FMV
auto-skip, TCP turbo) and the plan and passes refuse meanwhile.

psx_mod_render_pass_status() tells a plugin why passes cannot run, the
host-time budget aside: NO_PRESENTER, BACKEND (the renderer declines passes
in its current mode), DISABLED (after repeated faults), SESSION, FAST_FORWARD
or BUSY. The GL backend reports its part through
gl_renderer_pass_unavailable(), where a renderer mode that cannot host
passes reports BACKEND. psx_mod_set_frame_interpolation_blend() now also
takes effect when called from a hook after activation, so a plugin can show a
crossfade while passes are unavailable and return to HOLD afterwards.

Debug: PSX_RENDER_PASS_REFUSE=1 or TCP render_pass_refuse on=1 makes the
backend decline passes, to test a plugin's fallback; render_pass_stats
reports the status.

Default off: nothing reads the new flag or status unless a plugin uses
render passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:02:48 -07:00
Shokunin 82a835b20b frame_fingerprint reset_on_load for A/B runs from one savestate
frame_fingerprint reset_on_load=1 restarts the rolling hashes and the ring
when the next savestate load completes, so two runs that load the same state
(for example with and without a presentation mod) compare frame by frame
from the same guest point. Debug tooling only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:02:47 -07:00
Shokunin d22ebac9a4 Host-timed render passes for true in-between frames
psx_mod_render_pass_plan() / psx_mod_render_pass() let a trusted plugin
redraw its game between two logic ticks with the game's own draw code. A
pass is a sandboxed nested guest call:

- time is frozen: cycles are counted (GTE and mult/div deadlines keep
  working) but no device advances, no VBlank or device event fires and no
  interrupt is delivered; GPU linked-list DMA and delayed completions finish
  synchronously (psx_cycles freeze, interrupts, dma);
- stores go straight to RAM/scratchpad, bypassing code-page tracking,
  overlay watch and write traces; MMIO is an allow-list (GP0, GP1 DMA mode /
  info, GPU and OTC DMA, I_STAT/I_MASK) and SPU, CD, timer and other device
  stores are dropped and counted (memory.c);
- afterwards CPU (with the GTE), RAM, scratchpad, I-cache tags, interrupt,
  timer, DMA and GPU registers are restored exactly (render_pass.c, a GPU
  register checkpoint without savestate side effects), with a guest-cycle
  watchdog that rolls a runaway pass back;
- on OpenGL the declared display rect (hr colour + mask stencil, raw mirror,
  native-wide band, CPU VRAM rows) is backed up, drawn, captured and rolled
  back; writes outside it are journaled and restored, or refused.

The presenter keeps two generations of images (the game's own frame plus
one image per pass phase), promotes one when the flip-aware source sees the
display flip to its rect, and shows the newest image at each output
deadline's phase, crossfading across gaps when passes were shed. Presents
that fall due while passes run are made between them. The plan predicts the
output deadlines of the coming frame and sheds passes to fit a host-time
budget learnt from the presenter's idle time (PSX_RENDER_PASS_BUDGET).

PSX_MOD_FRAME_INTERPOLATION_HOLD repeats the newest frame where no pass
image applies, so a pass-driven title never shows anything later than stock.

Default off: nothing runs unless a plugin calls the API, and the plan
refuses in netplay, rollback, rewind, turbo, self-check resimulation, while
suspended (FMV) or without GL flip-aware interpolation. Pass writes are kept
out of frame_fingerprint. Debug: render_pass_stats, render_pass_dump,
PSX_RENDER_PASS_VERIFY=1 (state + VRAM hashes around every pass).

The freeze API lives in the runtime-only psx_cycle_freeze.h. psx_cycles.h is
part of the overlay codegen hash and stays byte-identical, so titles that
never use passes keep their overlay caches and savestates.

Tests: render_pass_plan_test (deadline-exact phases per rate, shedding,
selection), render_pass_freeze_test (10^6 frozen cycles, exact restore,
watchdog), render_pass_guards (sandbox choke points; the codegen-hashed headers stay
free of the pass API).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:02:47 -07:00
Shokunin 5ffd57d7f3 Flip-aware frame-interpolation source
psx_mod_set_frame_interpolation_source(PSX_MOD_FRAME_SOURCE_FLIP) makes the
OpenGL presenter rotate its blend history only when the guest really flips
(the displayed VRAM origin moves or the displayed rect is redrawn) and spread
one crossfade over the measured flip period. A game that flips every second
VBlank (R4 races) used to blend for one VBlank and hold for the next; it now
blends across its whole frame.

VBLANK stays the default. reset_mod_owned_presentation() puts it back at
every session start (first boot and the lobby rematch), next to the blend
mode, so titles that never call the setter keep today's code path and a
netplay session never inherits it. begin() is begin_phase(0, 1) and stays
bit-identical (test). gl_interp reports the source, the flip period,
captures and duplicate VBlanks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:02:43 -07:00
Shokunin ec7bc14ad9 GL: Fit to Window stays within the native-wide surface limit
A native-wide surface is g_wide_w*S wide and the driver limit bounds it
(1024 native columns at 16x on a 16384 limit, about 38:9 at a 320-px
display). An uncapped Fit to Window aspect could ask for more; wide_fbo_for
then refused the surface and the frame fell back to a 1x CPU present with
no native-wide margins.

update_adaptive_widescreen now narrows the Fit aspect to the widest the
surface holds (gl_renderer_fit_wide_aspect: psx_gl_max_wide_width at the
context's scale, psx_gl_fit_wide_aspect at the current display width,
gpu_ws_display_width). It changes nothing unless the surface would be
refused. The refusal stays for a fixed aspect on a wide display mode; its
comment now gives the right limit and what the frame falls back to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:52:23 -07:00
Shokunin bb2864bc60 Settings: an older launcher's Supersampling pick wins over a preset
A host built against a recomp-ui without the Internal resolution row
(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION undefined) still seeded
internal_resolution on first boot and applied it after the launcher.
From a fresh start the seed was "native", so a Supersampling pick of 3x
was reset to 1x and "native" was saved; on the netplay soft-return the
sticky preset was re-applied at session_reboot. The legacy row stopped
working.

Both launcher exits now go through two pure helpers in
internal_resolution.h:
- psx_ir_launcher_seed_supersampling: with the row, the factor as before;
  without it, the factor itself when no preset is configured (the
  historical round trip), else the preset's scale capped at the row's 4x.
- psx_ir_adopt_launcher: with the row, the row's preset as before;
  without it, a pick in the Supersampling row drops the preset, and an
  untouched row keeps a preset the player cannot see (game.toml or a
  hand-edited settings.toml). It also returns what settings.toml saves:
  no internal_resolution key unless a preset is kept.

internal_resolution_test drives seed -> launcher -> adopt for both
launcher kinds, first boot and soft-return, against a model of the
legacy row (clamp to 1..4, cycle); keeping the old sticky preset fails
it. gl_internal_scale_guards pins both exits to the helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:52:23 -07:00
Shokunin ecda594996 Settings: never persist the PSX_INTERNAL_RESOLUTION override
The environment override was written into g_video_internal_res, the
configured preset. On a netplay soft-return the launcher was seeded from
it and saved it to settings.toml, so one local peer started with
PSX_INTERNAL_RESOLUTION=8k left 8k in the settings.toml the other peer
shares: the case the override exists for.

Keep the override in g_video_internal_res_env. It still wins for the run
(scale, Match display, HiDPI window, video_info), but the launcher only
shows, and settings.toml only saves, the configured preset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:52:23 -07:00
Shokunin 7d49d4c491 Settings: PSX_INTERNAL_RESOLUTION override for one run
A preset id or a line count in PSX_INTERNAL_RESOLUTION overrides every
config layer, like the other validation env overrides (PSX_SCANLINES,
PSX_GEOMETRY_CORRECTION). Two local netplay peers share one settings.toml,
so this is how one runs at 4K beside another at Native. Also say
"measured when the game window opens" for Match display before the window
exists instead of logging a 1x request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:52:23 -07:00
Shokunin f34f91cc25 Settings: Internal resolution presets (Native ... 8K, Match display)
Add a player-facing internal resolution on top of the raised GL ceiling.
A preset is a target height; the runtime renders at the integer scale
S = ceil(target / reference_lines), reference 240 by default: 720p 3x,
1080p 5x (area-resolved to 1080), 1440p 6x, 4K 9x, 5K 12x, 8K 18x. The
backend then clamps S to what it can allocate.

- runtime/include/internal_resolution.h: the preset table, stable ids,
  the value encoding shared with recomp-ui (0 unset, 1 native, -1 match
  display, N target lines) and the pure resolver.
- config: [video] internal_resolution (a preset id or a number of lines)
  and resolution_reference_lines in game.toml; internal_resolution in
  settings.toml, written as a stable id beside supersampling = min(S, 4)
  for older runtimes. Precedence: game default < player's legacy factor <
  player's preset. settings.toml window_width now accepts up to 7680.
- main.cpp: resolve the preset before gpu_init; Match display re-resolves
  from the game window's monitor (SDL3 mode height x pixel density) before
  the GL context allocates. The launcher gets the vocabulary (with a
  synthesized "Nx (L lines)" entry for a legacy factor no preset matches)
  behind RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION, on first boot and on the
  netplay soft-return.
- The OpenGL game window gets SDL_WINDOW_HIGH_PIXEL_DENSITY (SDL2:
  ALLOW_HIGHDPI) only when the player chose more than native, so the
  drawable is in pixels on Retina; native keeps the window as it was.
- debug server: video_info (preset, requested and effective scale, GL
  limit and clamp, hr surface, window points vs pixels), screenshot_hires
  reads the GL hr FBO at full size, screenshot_wide_hires captures the
  native-wide band at internal resolution.
- docs: config_schema (keys, precedence), NETPLAY (a per-peer presentation
  setting), TCP_COMMANDS (index regenerated), ENHANCEMENTS IR1.

Default-off: with no internal_resolution anywhere the scale, the window
flags and the present path are unchanged.

Tests: internal_resolution_test (presets x reference 240/480 x backend
maxima, legacy mapping round trip), video_enhancement_settings_test (game
and settings keys, stable-id round trip, legacy key still written),
gl_internal_scale_guards (HiDPI only when opted in, launcher fields behind
the capability macro).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:52:23 -07:00
Shokunin 9bb983208e Settings: shorter Internal resolution note
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:52:23 -07:00
Shokunin 2211ecabf3 GL: true 8K past a 16384 texture limit with a high-resolution window
8K is 18x at a 240-line reference, and a full-VRAM surface at 18x is
18432 px wide: over the 16384 limit of Apple's GL, Intel and MoltenVK. A
title only needs its displayed frame at 18x, so past the full-VRAM limit
the GL backend now splits the work:

- The authoritative VRAM stays the ordinary hr surface at 1x, i.e. the
  native renderer unchanged: pack, CPU readback, render-to-texture and VRAM
  copies are exactly the native results (s_hr_scale; s_scale stays the
  presented scale gr_scale reports).
- A presentation-only surface at S covers the displayed columns W =
  [x0, x1) x all 512 rows (R4: 0..319, 5760x9216 at 18x, 405 MiB). Every
  GPU write touching W is mirrored into it: textured and flat batches,
  lines (quads), fills, uploads, the depth24 clear, and VRAM copies (hi to
  hi when the source is in W, otherwise the 1x source upscaled; both staged
  before drawing so overlapping copies read pre-copy pixels). It keeps its
  own stencil. A separate blit program carries the window projection, so
  the canonical shaders are untouched.
- W grows (64-column aligned) the first time a display rectangle outside it
  is presented, seeded by upscaling the 1x content. Present, hold-last,
  interpolation capture, screenshot_hires and the native-wide centre read
  it; a display it cannot hold presents at 1x.

It engages only when the full-VRAM surface cannot hold the request (driver
limit or budget), sized so a 320-wide window fits both; up to 16x on the
M4 nothing changes. PSX_GL_HIRES_WINDOW=0/1 disables or forces it. The
launcher note and video_info (hr_scale, windowed, hires_window_*, hires_fbo)
report it.

Default-off: at 1x and whenever the full-VRAM surface fits, the paths are
the previous ones (same native-VRAM and present digests on a fixed scene at
1x and 2x).

Tests: gl_scale_invariance_test now also forces the window at 2/3/5/9x and
requires its frame at internal resolution to be byte-identical to the
full-VRAM surface at the same scale (copies inside, into and across the
window edge, fills/uploads across it, mask, all blend modes) with native
VRAM identical to 1x, and runs it at 18x; gl_scale_limits_test covers the
window clamp; guards pin that it only engages past the full-VRAM limit.
Verified in R4 on an M4: the 8K preset renders 5760x4320 in a race (rear-
view mirror present) and 5760x8640 on a 480i menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:52:23 -07:00
Shokunin eee07fe910 GL: raise the internal-resolution ceiling to the driver's limits
The OpenGL backend capped supersampling at 4, the software mirror's limit,
although its hr surface lives on the GPU. Raise the compile-time ceiling to
32 (gpu_render.h) and decide the real scale at context init:

- Query GL_MAX_TEXTURE_SIZE, GL_MAX_RENDERBUFFER_SIZE and
  GL_MAX_VIEWPORT_DIMS and clamp S so the 1024*S x 512*S surface fits them
  and a 2 GiB memory budget (PSX_GL_VRAM_BUDGET_MB overrides it). Apple's
  GL on Metal reports 16384, so 16x is the most a full-VRAM surface can be.
  The clamp is logged with the limit that applied.
- If the surface still fails to allocate, retry one scale lower inside GL.
  Only a 1x failure drops the backend to software, as before.
- Draw lines above 1x as quads one native pixel thick covering both
  endpoints. glLineWidth(S) is capped at 1 on core profiles, so every line
  was one hr pixel thick (1/9 of native at 9x).
- Present a supersampled source with an area resolve when it is more than
  1.25x the output (1200 lines into a 1080 window, 480i menus), and inset the
  UV rect by half a texel instead of half a native pixel, which cropped and
  zoomed the image by (S-1)/2 texels per edge.
- Size the copy scratch to the largest copy (copies stage at its origin) and
  rebuild the mask stencil in tiles, only over the primitive bbox union, so a
  large S does not cost another full-size surface.
- Refuse a native-wide surface wider than the driver limit (uncapped Fit at
  a high scale) with one log line; the frame presents 4:3.
- main.cpp: the offline and dual-raster clamps no longer cap GL at the
  software 4x. The CPU present path presents GL frames at 1x (its readout is
  native) and its staging buffer stays sized for at most 4x.
- [video] supersampling accepts 1..32 in game.toml and settings.toml; the
  runtime clamps per backend.

Every change is gated on S > 1: at 1x the renderer is byte-identical (same
native VRAM and presented framebuffer digests as the previous source on a
fixed GP0 scene).

Tests: gl_scale_limits_test (clamp arithmetic), gl_internal_scale_guards
(source guards + runner parsing), and gl_scale_invariance_test, a hidden
real-GL run at 1,2,3,5,9x that requires identical guest-visible VRAM at
every scale, one-native-pixel lines at internal resolution, and a 32x
request that stays on GL at the driver's clamp (16x on Apple).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:52:22 -07:00
Shokunin 78bec7b55e Add [widescreen.cull] bgez_sites and clip_edge_x_load_sites
Two generic explicit cull-site kinds for native-wide titles whose screen-X
rejects the existing keys cannot express exactly:

- bgez_sites: `bgez SX, keep` in a signed per-vertex left-edge chain
  (`bgez x0,keep; ...; bltz xN,reject`). Emitted through psx_ws_cull_bgez
  (keep while SX >= -margin). bltz_sites alone still rejects a primitive
  whose first vertices sit in the revealed band; with both kinds the chain
  rejects exactly when every vertex is left of -margin.
- clip_edge_x_load_sites (+ clip_edge_width, default screen_w_imms[0]):
  lh/lhu/lw of a clip-rectangle X bound. psx_ws_clip_edge_x moves a loaded 0
  to -margin and a loaded width to width+margin and leaves interior bounds
  (mirrors, split-screen halves) unchanged. xclip_load_sites is lw-only and
  upper-bound-only, so it cannot express this.

Both are identity at 4:3, emit nothing when empty, and fail main-EXE
generation on an opcode mismatch (overlay variants stay vanilla). They enter
the overlay-cache hash only when non-empty, so every existing title keeps
its hash (pinned in the test against the previous layout).

Interpreter parity: the dirty-RAM interpreter now also honours explicit
bltz_sites, bgez_sites, branch_keep_sites and clip_edge_x_load_sites. Its
explicit site lists are stored sorted (binary-search lookup), the cap rises
from 64 to 256 per key, and an over-long list is logged instead of being
silently truncated.

Shared math lives in runtime/include/ws_cull_edge.h. New ctest
ws_cull_edge_codegen_test covers parsing, hash identity, emitted code,
the opcode guards and the helper math.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:39:26 -07:00
Matthew Stanley 3382f86c59 runtime: keep disc-picker dialog text ASCII-safe 2026-09-28 16:54:33 -07:00
Shokunin a87277bdad Reset mod-owned presentation state at every session start
The lobby rematch is the only in-process second session: an offline
session ends the process, and only a netplay match launched from the
lobby soft-returns (netplay_soft_exit -> psx_request_return_to_lobby).
The rematch path re-enters via goto session_reboot, below the first-boot
session block, so it reset no mod-owned state at all. The reachable leak:
a match with [netplay] local_viewport_aspect = "adaptive" leaves Fit
(g_ws_adaptive_view) on, and an offline rematch then runs adaptive
widescreen. The launcher also round-trips the match's fixed aspect
(16:9/21:9 from the local viewport) through ls.aspect_index.

reset_mod_owned_presentation() runs immediately before activation in the
first-boot session block (absorbing the existing blend reset) and in the
rematch path after its commit or netplay clear. Every call clears Fit, the
world and retained scene predicates, the adaptive backdrop preload, bezel
artwork, the blend mode and native VBlank pacing. Later calls also restore
vsync, frame interpolation and its rate, and automatic FMV skipping to
their first-session values, and clear the 8 MiB RAM request (memory_init()
re-latches it at session_reboot) and the texture-bank resolver and
batching flag. Netplay clears the plan, so no plugin has activated before
a rematch; everything past Fit is defensive. The renderer and fixed aspect
are launcher controls and process-lifetime allocations are left alone
(documented in MOD_PACKAGES.md).

The first-call capture lives in mod_session_baseline.h: its first call
records the pre-activation values (settings.toml, PSX_VSYNC, launcher) and
writes nothing back, so the first session, and every run that never
soft-returns, is unchanged. A static_assert keeps the restore honest: it
is only valid while interpolation and Skip FMVs have no launcher control.
Function-entry hooks need nothing here: the commit or netplay clear
empties the hook table and only activation rebuilds it.

The rematch path also re-applies the Settings aspect clamp (4:3 while
widescreen is mod-owned) before apply_netplay_local_viewport_aspect, so a
netplay rematch still gets its local viewport. The first-boot path keeps
the launcher/lobby aspect as before.

Not changed here: the rematch path still skips activation and the
controller, load and disc-speed resets of the first-boot block, so an
offline rematch with mods enabled applies the plan's patches and runs its
VBlank callbacks without activation.

Tests: mod_session_baseline_test (new, CTest) drives the first-call
capture and later restore with vsync on and off; it fails if the first
call writes anything. test_mod_owned_display_controls.py guards the
reset list, both call sites, reset-before-activation on the rematch path
and the rematch 4:3 re-clamp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:26:33 -07:00
Matthew Stanley 78534deff4 mods: [runtime] hide_hidden_mod_features opts the launcher into never showing hidden features
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 16:41:47 -07:00
Matthew Stanley f51617bb7d runtime: VBlank rate follows the GPU video mode (GP1(08h) bit 3), not the disc region
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 15:38:21 -07:00
Matthew Stanley f9109e21c6 Resolve relative bezel artwork paths against the exe directory
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 22:27:47 -07:00
Matt Stanley 9e9ec7b401 Merge pull request #383 from x6568tank/feat/mid-session-disc-change
Mount a different disc while the game runs
2026-09-24 15:15:43 -07:00
Matthew Stanley eec71b9787 fix: open the game window at a normal size instead of maximised
Default = largest multiple of 240 lines within 2/3 of the usable height;
an explicit window_width is still honoured.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 14:42:12 -07:00
Matthew Stanley 41e92d8a32 Merge current framework master preserving accepted MMX6 widescreen and performance work 2026-09-19 17:49:10 -07:00
Matthew Stanley c68bf6e085 Merge current master while preserving Tomba adaptive renderer 2026-09-19 16:23:00 -07:00
Matthew Stanley fa435a4a0c render: add uncapped adaptive viewport and stable world classification 2026-09-19 16:19:23 -07:00