- Restart bits: a group waits only when the edit touches it (next differs
from the live value) and the result differs from boot. Dynamic
resolution moves the live internal resolution off boot, so an in-game
texture filter change relaunched the game. test_menu_video_snapshot
covers both cases.
- Return to the frontend from a local game opens the launcher as at
boot; only a lobby match start (the runtime's pending-launch exit, or
the provider's lobby view asking once its match started) keeps the
launch and resumes the room.
- Default provider: with no title choice, the UI library's provider is
used when it ships one (recomp_ui_psx_menu.h), so recomp-ui stays the
default; PsxMenuHost.default_selection tells it to keep the title's
default presentation.
- Debug: menu_host reports provider, open, pause, capture and snapshot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up on #661 (#636):
- test_depth24_present_hold_vblank.py reads sources as UTF-8 (cp1252 on
native Windows Python raised UnicodeDecodeError).
- The present skip is now guest state: set at guest VBlank, read (not
consumed) by the present and packed into the GPU snap's hold word (bit 8;
older snaps derive it from the count). The executable test showed the
host-side latch made the present of a restored tick differ from live on
the last held VBlank (hold 0, skip 1), exposing stale trailing FMV
columns after a rollback load.
- Depth24 scanout staging and trailing-column blanking move from main.cpp
into gpu_depth24_stage_scanout (gpu.c), so the test renders through the
same code every CPU depth24 present runs.
- New depth24_present_hold_exec_test: drives real gpu.c through a 15-bit to
24-bit FMV, GP1(07h) retargets and mode transitions; compares hold, skip,
upload span and snapshot bytes across windowed/headless/deferred presents,
rollback replay from every tick and disk loads, and checks staged frames
never show stale movie pixels while the new movie stays visible.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
psxrecomp owns the contract between the runtime and any in-game menu
frontend. A provider (PsxMenuProvider: attach, event, is_open, set_open,
draw, detach) is registered by the title with psx_menu_set_provider();
none is registered by default, so a title's UI and pin are unchanged.
The runtime offers every provider the same host (PsxMenuHost): pause
(P1, refused in netplay), video get/apply on PsxHostVideoSettings (P4),
mod switch/option set (P5, via the new psx_mod_feature_enable /
psx_mod_feature_set_option shared with the launcher ABI provider),
reload bindings, exit (quit, back to the frontend loop in-process, or a
quick relaunch with the original arguments), disc/tray slots filled by
the tray policy (psx_menu_set_disc_ops), and optional helpers on the
shared launcher ABI both frontends already speak (game info with the
mod catalogue and lobby, settings get/apply with the handoff's filter
translations).
Policies live here, the same for every provider: the lobby is pumped
during play and a started match ends the local session and starts in
the frontend loop (pending launch, seats and signals kept, pumping
stopped so the match port stays free), the menu is closed and its pause
dropped first; the provider detaches before its GL context goes away at
session teardown and attaches to the next one. --launcher sets
RECOMP_LAUNCHER_PREGAME for frontends that boot into the game;
PSX_HIDDEN_WINDOW is a hidden-window test hook that keeps the frontend.
Nothing here includes or names a frontend's own headers or types.
Tests: psx_menu_host_test, host_overlay_test (relaunch quoting).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
psx_mod_set_option_live records a session override for a committed
feature's option and calls the plugin's new option-changed callback in
that plugin's context; psx_mod_option_value answers the override. It
returns 1 when applied live, 0 when the change waits for a restart, -1
during netplay (the plan is negotiated between peers). Nothing changes
unless an overlay calls it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#655 landed the same PSX_TEXTURE_FILTER_* enum and translation in
video_texture_filter.h/.c; drop this PR's duplicate (link error) and
include that header.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review (#601/#603): the live path and the launcher handoff both collapsed
the texture filter to a boolean (value ? 1 : 0), so neither could carry xBR
(#586) and they could drift from settings.toml. PSX_TEXTURE_FILTER_* is now
the one enum (equal to [video] texture_filtering / gr_set_texture_filter),
and psx_texture_filter_from_ui is the one translation used by the launcher
handoff (seed, UserSettings, live globals) and the in-game apply. Unit-
tested in host_overlay_test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review (#601):
- psx_video_apply_live had no netplay guard. In a match only presentation
(fullscreen, scanlines, linear present) applies live; dynamic resolution,
frame generation and the filters are saved and reported with the new
PSX_VIDEO_RESTART_NETPLAY bit (next session).
- internal_resolution and present_thread are written to UserSettings like
the other restart-only fields, so the choice survives a restart.
- The live-apply code sat inside #ifndef PSX_NO_DEBUG_TOOLS but is used by
the launcher path, so Release + launcher did not compile. It now lives
outside the debug-tools block.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
psx_video_apply_live applies the [video] keys a running game can change
(fullscreen, texture and FMV filter, scanlines, linear present, dynamic
resolution, frame generation) through the existing live setters, saves
them to settings.toml, and reports renderer / internal resolution /
thread changes as restart-only. Nothing calls it without a provider.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
psx_host_set_input_sink: an overlay provider sees host SDL events first,
in the event drain and the host pause loop, and may consume them.
psx_host_set_ui_capture: while on, local game input reads released and
centred (netplay included, so peers receive "nothing pressed") and the
external-input gate stays closed. Debug builds gain host_key, which pushes
a real SDL key event for scripted runs. Inert without a provider.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review (#599): who keeps the callback context alive until render-thread
calls are quiescent. Draw calls now run inside an in-flight count, and
set_draw_cb (replace or NULL) swaps the record and waits until no call
that read the old one is running, then frees it. When it returns, the
registrant may free ctx and its GL objects; it must unregister before
destroying them or the context. A change from inside the callback does
not wait. Documented in host_overlay.h. Tests: a reader thread drawing
while the owner unregisters and frees (300 rounds; fails without the
wait), and self-unregister from the callback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
psx_host_overlay_set_draw_cb: the OpenGL backend calls it after the game
frame and host OSD layers are composed, before present_shot capture and
the swap, on the thread that owns the GL context. Without a registered
callback nothing changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
psx_host_pause_push/pop: a nesting, host-owned pause. While it is held the
runtime freezes the guest at the vblank present (as the save-state menu
does), keeps presenting the last frame and pauses audio. Push refuses
during netplay. Nothing pushes unless an overlay provider (or the debug
server's new host_pause command) asks, so behaviour is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The batched scaled charge fell through to the scale-1 batch path, which
also defers while g_psx_cyc_bb_defer is nonzero and routes through the VLC
local sink. Both are host call-stack state (a longjmp escape leaks or
resets the depth), so which charge published the batch -- and so which
clock each device saw -- differed between hosts. R4 2P rollback Mac<->PC
with modern controls split by a few cycles in live play (clk/tim fork at
~3296 with cpu/ram equal), then baseline mismatches and a realign storm
dropped the match; with the batch off the same pair matched.
The scaled path now batches on the guest clock only (device deadline or
the 64-cycle soft cap). New check: the service log is identical with
bb_defer 0 or 338 and with a local sink installed (fails before). Re-stamp
the OpenBIOS emitter sha (psx_cyc.h is in the codegen hash).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing is measured about an unknown GPU, so the fallback no longer assumes
Ultra's extra work (supersampling above the display, title Ultra extras);
Low would drop Smooth motion and the PGXP extras on what is usually capable
new hardware. Medium keeps the display's resolution with dynamic
resolution under it. Covers unnamed renderers, Linux with only an AMD PCI
id, and unnamed nouveau chips. docs/QUALITY_PRESETS.md states plainly that
autodetect is a static heuristic over name, threads and memory with no
benchmark, and that the dynamic systems keep the frame rate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With [timing] guest_cycle_scale active, every one-cycle psx_cyc_charge went
straight to psx_cpu_charge and psx_advance_cycles, bypassing the batch the
scale-1 path uses. [timing] guest_cycle_scale_batch (PSX_GCS_BATCH=0/1)
scales the charge in place, with the same fractional carry and order as
psx_cpu_charge, then batches it like the scale-1 path. It flushes at the same
observation points. Default off.
R4 exactness, frame_fingerprint off vs on, 9000 frames each: headless boot
through intro, title and attract; a 1P Grand Prix race; and a 2P VS race.
cyc, mmio, mc and qc are identical on every frame. sp, wr, pc and ws are
identical at the end. The only differences are single-frame VBlank straddles
of wc/sc (never two frames in a row). 0 dispatch misses.
Speed, release builds, races at 60 Hz:
- M4 (macOS sample): psx_cpu_charge 4.3 % -> 0 % of emulation-thread samples;
busy 46.0 -> 44.4 % (2 pairs).
- 9800X3D: no measurable change (30.0 vs 30.5 %; one core 44.6 vs 44.2 %).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1. Detection never runs under --headless / --hidden-window (CI, scripted
runs): a saved preset applies, else the title's [video]; nothing is
written. A detection is saved only when the GL probe succeeded; a result
from the OS name alone is used for that run and the next launch probes
again (a failed probe could otherwise pin "unknown GPU" -> Ultra).
2. Linux without a GL probe: the classifier now reads the sysfs PCI id
(Steam Deck parts, Intel integrated vs Arc, NVIDIA).
3. AMD/Intel rules rewritten against real renderer strings: "Radeon RX
Vega 11 Graphics" and other Vega/"Radeon Graphics"/R5-R7/680M-890M APUs
are integrated (Low); RX/Pro/W cards and Strix Halo are Ultra; the Intel
Arc iGPU is Low, Arc A/B cards Ultra. quality_presets_test holds a
44-row table of Windows, Mesa (incl. Steam Deck LCD/OLED, zink) and macOS
GL_RENDERER strings, plus probe-failure and headless cases.
5. generated/OpenBIOS.emitter.sha re-stamped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner decision: Ultra on first launch, with dynamic resolution, Smooth
motion from surplus and the adaptive aspect as the live safety net. Apple
silicon, discrete GPUs, unknown GPUs and 8 GB machines now detect as Ultra;
software GL, Steam Deck, mobile, Intel HD/UHD and small AMD iGPUs stay Low,
mid iGPUs Medium; fewer than 4 threads or under 6 GB still caps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A title may declare [quality.low|medium|high|ultra] tables of [video] keys.
On first launch, and when the hardware fingerprint changes while a preset is
in force, psxrecomp detects the CPU, threads, RAM and GPU (OS facts; a hidden
1x1 GL window reads GL_RENDERER only when a detection is due and the OS did
not name the GPU), picks the matching preset and saves quality_preset,
quality_base, quality_hardware and quality_detected to settings.toml.
- Each preset is the whole game.toml re-parsed with its keys over [video], so
every [video] key works in a preset with its usual validation.
- A preset in force owns its keys: saved settings.toml values for them are
ignored. Custom (the player changed a governed setting in the launcher)
applies the player's values over quality_base and is never overridden;
new hardware under Custom is only reported.
- Launcher: GameInfo.quality_* (offered mask, detected preset, hardware
summary, quality_apply / quality_redetect callbacks) and
Settings.quality_preset/quality_base, behind RECOMP_LAUNCHER_HAS_QUALITY_PRESETS.
- PSX_QUALITY=<preset> for one run, PSX_QUALITY_REDETECT=1.
- Titles without [quality.*] are unchanged.
Rules (quality_tier.c) and the decision are unit-tested in
quality_presets_test; docs/QUALITY_PRESETS.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The detector cannot prove a loop has no later counter-dependent exit, so
the extension now skips only at block-edge PCs the title lists after an
audit. Unlisted sites are never skipped; an empty list disables it. Docs
record R4's v_wait audit as the reference; tests cover unlisted sites.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Last-store tracking: memory.c owns the record and writes it only while
the extension is on (g_psx_track_last_store); main.cpp hands it and the
host writer (psx_host_write_word) to psx_cycles.c through
psx_idle_skip_set_store_tracker / _set_host_writer, so psx_cycles.c no
longer references memory.c (fixes the tests that link one without the
other).
- Exact from boot: the device distance is now measured from the current
cycle (devices report it from their last service, up to a few hundred
cycles behind), and several edges per pass are accepted only together
with a counter store. Before this, R4's boot diverged at frame 962 (an
OpenBIOS multi-edge poll and a late VBlank IRQ after a v_wait skip).
- tests/test_idle_skip_store_counter.c: a v_wait trace skips (whole
passes, counter lowered by the host writer, never to the timeout, lands
before the event); an extra store, an MMIO read, a decrement of 2, a
store to MMIO, the option off and no host writer do not.
- generated/OpenBIOS.emitter.sha re-stamped.
R4 frame_fingerprint off vs on, 9000 frames each: boot through intro and
attract, a 1P race and a 2P VS race: cyc, mmio, mc, sp, sc and qc
identical on every frame, 0 dispatch misses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dispatch template emitted since mod-guest-functions landed declares
extern g_psx_mod_guest_functions / psx_mod_dispatch_guest_function in
every BIOS dispatcher, but the module name-mapping block and
PsxBiosModuleCallbacks never carried them, so a freshly built retail
backend (e.g. SCPH1001) failed RTLD_NOW with an undefined symbol —
both in the builder self-check and in the real dlopen. Add the two
mappings, table entries, glue dummy/forwarder, and host wiring; bump
the ABI so stale caches cannot load. Verified: SCPH-1001 module builds,
self-checks, and boots SLPS-00756 on Linux/gcc.
Smooth motion lost most of its generated frames with bloom on (R4 race,
120 Hz: ~30% of bloom-off). Measured and fixed in order of cost:
- Depth-stencil blits (CPU milliseconds each on macOS) are gone:
generation surfaces no longer copy the marks and are not marked while
drawn (generated frames reuse the newer real frame's mask); the
native-wide centre copy is colour only again and the mask takes the
centre's marks from the hr surface; the mask itself is resolved in the
presented surface's own pixels through an R8 target that shares the
surface's depth-stencil renderbuffer, then mapped to the window.
- Marking needs one pass per batch, not two: 3D batches only set, 2D
opaque ones only clear, semi-transparent ones only set; only batches of
unknown make (queued replays, lines) still do both.
- No full-size copy of the image: `source_down` (a blit straight to the
pass's size) feeds the bright pass, and the composite screen-blends onto
the window (`blend screen`; unlit pixels are discarded, so they keep their
exact value). The debug split/bloom-only views go with the copy.
M4, R4 race, render thread + reprojection, PSX_FRAME_GEN_FORCE, a host
loaded by other work (absolute rates low): bloom pass on real / generated
frames 1.2 / 0.7 ms at 1080p, 1.3 / 0.9 ms at 1440p, 2.6 / 1.6 ms at 4K
(glFinish-bracketed); generated frames with bloom 79-80% of bloom-off at
all three (was ~30%). A 1/8-resolution blur chain was tried and kept no
more frames, so the 1/4 chain (the look) stays.
Tests: parser (blend, source_down, bloom preset reads no full copy), GL
fixture (source_down + blend screen chain exact; HUD and mask checks
unchanged), FXAA unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #584: the default must not make guest-visible VRAM depend on the
internal scale. Mode 1 ("on", default; "scaled" kept as an alias) now
puts the PS1 pattern on the native VRAM grid; mode 2 ("fine") is the
elective per-internal-pixel pattern. gl_dithering_test checks the default
mode's native VRAM is identical at 4x and 1x (equal-colour Gouraud now
averages 15.50 in the default at every scale). Docs keep the
semi-transparent approximation explicit. OpenBIOS stamp regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The OpenGL renderer has always drawn shaded and texture-modulated pixels
at 8 bits per channel with no dither pattern (true colour). That stays the
default. 'on' applies the PS1 4x4 offset matrix and 5-bit truncation per
internal-resolution pixel; 'scaled' puts the pattern on the native grid,
so it reads like the console at any scale and the native VRAM is the same
at every internal resolution.
GP0(E1h) bit 9 reaches the backend through a new optional facade hook
(gr_set_dither) recorded for the render thread and Smooth motion redraws.
With the mode off nothing flushes and no uniform changes. Flat, raw and
other 5-bit-exact pixels are never touched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- config: [video] post_shader is a std::filesystem::path (like memcard_dir);
the frontend turns it into UTF-8 bytes for the GL API. Assigning the path
to a std::string did not compile on MinGW.
- .pfx `generated reuse`: a Smooth motion generated frame keeps that pass's
target from the last real frame. The bloom preset marks its bright and
blur passes so; generated frames run only the masked composite (over their
own picture and mask). bloom debug reports real/generated pass times and
reused passes.
- Mask marking no longer queries GL state per batch: each call site puts
back the blend and mask-bit stencil it set (no driver round trips on the
render thread).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Post effects now run as chains of full-screen fragment passes on one
executor (gl_postfx.inc), as asked in review. A chain is a small .pfx preset
(postfx_chain.c, GL-free): passes with a shader (a file next to the preset or
builtin:<name>), inputs (source, the optional primitive-derived mask,
previous or a named earlier pass), a scale, format, filter and uniforms.
The executor owns target allocation and lifetime, program build/cache,
uniform binding (u_rcp, u_size, u_org) and GL state save/restore.
Stages, in order, each from a fresh copy of the image: the built-in "bloom"
chain ([video] bloom), a user chain ([video] post_shader / PSX_POST_SHADER,
elective, off by default), and the built-in "fxaa"/"fxaa_hq" chain
([video] antialiasing_mode). FXAA folds in as a one-pass chain and its
output is byte-identical to the former fixed pass; it still runs on real
frames only. Generated (Smooth motion) frames run bloom and the user chain.
The "may glow" mask is an optional pipeline input: draws keep stencil bit 1
only while an active chain reads `mask`, and the window-sized mask texture is
built once per present for the chains that use it.
Tests: postfx_chain_test (format, built-ins, errors, file import, no GL);
gl_bloom_test gains an imported two-pass chain (byte-exact invert), the mask
input (HUD unlit, 3D lit), a bad preset, a pass that does not compile, and GL
state restored with bloom + a mask chain + FXAA on, all on the caller thread
and the render thread. docs/POST_EFFECTS.md documents the format.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[video] bloom = 0.0 (off, default) .. 2.0 adds a glow around lights on the
OpenGL backend. PSX_BLOOM overrides it for one run; {"cmd":"bloom"} reads or
changes strength, threshold and a debug view live. It is a plain [video] key,
so [quality.*] presets can switch it off on low-end machines.
The HUD never glows: with bloom on every textured and untextured draw is
followed by two stencil-only passes of the same vertices that keep a
"may glow" mark in stencil bit 1 (bit 0 stays the mask-bit mirror). Opaque
primitives clear it under their coverage, then PGXP-proven 3D primitives and
additive semi-transparent texels/primitives (modes 1 and 3) set it, painter
ordered like the colour; 3D and non-3D primitives never share a batch while
bloom is on. Semi-transparent primitives leave the mark alone and the PGXP
depth-clear pass (colour writes off) marks nothing. Without PGXP geometry only
additive primitives glow.
The pass runs on the composed game image before AA and the OSD: the presented
surface's marks are blitted into a quarter-resolution stencil, a soft-knee
bright pass runs under it, three separable Gaussian iterations spread it and a
screen blend adds it back. The marks follow the picture: native-wide centre
copies and Smooth motion surfaces (redraw and reprojection) copy stencil with
colour, and generated frames get the same pass. Off: nothing runs, no state
changes, and the composed frame is byte-identical to master.
test_gl_bloom (gl_bloom_test): off is byte-identical before/after bloom and
with --dump against master; on, a bright 2D panel and a panel over a lit 3D
polygon are byte-identical to off, an additive and a 3D polygon glow; guest
VRAM is unchanged; on this thread and on the render thread.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SteamGamepadUI=1 is also set when Steam launches from Big Picture on an
ordinary desktop, which is broader than Steam Deck / gamescope. Detection is
now XDG_CURRENT_DESKTOP=gamescope, GAMESCOPE_WAYLAND_DISPLAY or SteamDeck=1
(plus --game-mode / PSX_GAME_MODE); game_mode_test and docs updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Detected from a gamescope session (XDG_CURRENT_DESKTOP, GAMESCOPE_WAYLAND_DISPLAY)
or Steam's gamepad UI (SteamGamepadUI=1); --game-mode / --no-game-mode and
PSX_GAME_MODE override. When on and a disc is set up, the launcher is
skipped and the game opens borderless fullscreen for that run (the saved
Fullscreen choice is untouched); the first run still shows the launcher so
the disc can be picked. Desktop sessions, --headless and --hidden-window are
unchanged. game_mode_test covers detection and precedence; docs/GAME_MODE.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The seed path collapsed ls.texture_filter to a boolean, so a launcher's
xBR (3) was saved and applied as bilinear, and the rematch paths passed
the raw value through unchecked. psx_texture_filter_from_ui is now the
one translation (in-range values pass, others are nearest). Test:
video_texture_filter_test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fixture now completes a pad exchange (sio_tick) so SIO holds a pending RX
byte with RX_RDY set and the ACK visible-read counter > 0, and asserts both
fields directly after the host-output callback (sio_peek_ack_visible_reads,
side-effect free, added for this). Without the fix the callback's reads
clear RX_RDY and age the ACK counter, and the test fails on those checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The settings field lives in recomp-launcher's header (R4's provider); the
runtime reads it only behind RECOMP_LAUNCHER_HAS_JOGCON_FORCE_FEEDBACK, so a
recomp-ui build compiles unchanged. PSX_HAS_JOGCON_FFB_PLAYER_SETTING lets
the in-game overlay's apply_settings call the live hook without depending on
this PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>