P1: quality_launcher_apply is now a pure preset -> rows mapping
(psxq::rows_from_runtime plus startup constants); it never writes the live
globals. The in-game menu publishes only thread-safe callbacks: that
mapper, and a re-detect that returns this session's detection (a fresh
probe needs the GL context and mutates the run's state, so it stays in the
pre-game launcher). Summary/reason strings and the detection are fixed on
the main thread at menu setup; settings_get reads only the main-thread
snapshot.
P2: psxq::MenuSelection keeps what the player selected versus what is live.
A pick's next-launch rows and netplay-deferred rows stay selected for the
next seed until the live value matches; other rows follow live. The host
classification moves to psxq::classify_launcher/row_differs (same rules).
Tests: quality_presets_test test_ingame_menu_host_path runs the review's
fixture through the host get/pick/apply/save path (Ultra -> High, then
Antialiasing = NONE; a governed edit is still Custom from High; a netplay-
deferred row stays selected). menu_quality_callbacks_test guards the
draw-thread callbacks against runtime writes and stateful calls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- [quality.*] accepts only the supported keys (quality_key_supported):
twelve player-editable ones (masked, copied, classified, applied) and
nine preset-only ones (no settings.toml form; applied on select). Any
other key, e.g. vsync or renderer, is a load error.
- Picking a preset now also applies bloom, texture LOD, anisotropy, FMV
chroma smoothing and dithering (quality_apply_video_globals).
- The frontends flag Custom on any tracked row; the host counts only rows
of keys the title governs (psxq::launcher_custom_edit), so Antialiasing
no longer turns Ultra into Custom when no preset sets it.
Tests cover the rejection, every player key's masking/copy, and the
classification cases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The boot launcher, soft-return launcher and the in-game menu's live
settings now save settings.toml through psxq::settle_save:
- a one-run PSX_QUALITY override never persists: governed keys and the
quality_* keys come from disk, so the previous preset or Custom values
survive even when the boot seed was built empty;
- a governed live edit (psxq::live_edit) or launcher Custom becomes
Custom from the current base, with the edited values, and survives
restart instead of being masked away;
- a launcher pick records the preset; unrelated edits keep it.
Tests reproduce both review cases through a settings.toml round trip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>