- Filtered replacement sampling: with any texture filter but nearest, the
replacement is sampled through its mip chain (textureGrad), the footprint
clamped to the primitive's own UV bounds so atlas neighbours cannot bleed
in. Texture windows (repeat) and UVs outside the page keep hd_texel.
- Edge clamp: an out-of-bounds sample clamped to the primitive's UV bounds
takes the edge's sub-texel position too (keeping the outside fraction read
a 1 px sliver of the next glyph at sprite borders).
- GL cache: 4096 slots and a PSX_HD_GL_CACHE_MIB budget (default 2048 MiB)
with a direct-mapped hint table; a 4x pack of one race scene is several
hundred textures, and the old 256 slot / 128 MiB cache re-uploaded and
re-mipped them every frame.
- Invalidation: duck_texture_pack_invalidate returns early when a native
write touches no residency or cache entry (almost every draw under HD
authority lands on the frame buffer), and retire_unused walks the
residency lists once instead of once per source.
- Indexed replacements: a TTTTTTTT-index.png key resolves through the palette
in use at draw time, for packs whose textures are shared by several CLUTs.
- Debug: TCP {"cmd":"hd_uploads","x":..,"y":..} lists the texture uploads
covering a VRAM point (pack authoring).
Measured with a full HD HUD and course pack for R4: steady 60 fps in races
where the old cache dropped to the high 20s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rkyma8rbyPiccs9n2bkyG6
Reject invalid index products and unsafe paths, cap encoded/decoded residency, recover allocation and partial worker-start failures, release GL movie textures with their context, and serialize diagnostics through the presenter. Real SDL decode/restart/failure test passes and SDL2 C seam compiles warning-free. Guest/present timestamp ownership remains a separate design gate. Tracking: beads-eio.3.388.
An optional pack replaces a title's MDEC movies with high-definition frames
while the game keeps its own audio, timing and skip control:
- fmv_hd/<movie>/index.bin: header, then a luminance signature per source
frame; fmv_hd/<movie>/frames/NNNNNN.jpg: `sub` HD frames per source frame.
- Every presented 24-bit frame is matched by signature (mean absolute
difference), first around the last recognised frame, then over the whole
pack when tracking is lost; a near global match is also tried a few lines
up or down (each movie has its own vertical placement on screen) and the
offset kept. Worker threads decode the upcoming frames ahead into a small
RGBA ring.
- Format FMVHD002 records the native display the movie is shown in and its
position, so the HD frame lands exactly where the original was.
- The last HD frame is held briefly when the next one is not ready or not
recognised (no flicker back to the 4:3 original mid-movie), and is
uploaded to the GPU only when it changes.
- Debug: TCP {"cmd":"fmv_hd"} reports the pack, the matches and lost frames.
No pack: no effect. Pack frames are the user's own upscale of their own disc;
nothing is distributed with the runtime.
Used for R4's intro and ending movies (SeedVR2 upscales, 25 fps sources).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rkyma8rbyPiccs9n2bkyG6
Use a bounded atomic snapshot and presenter-owned reset across all backends. Readers sort their own snapshots; report host API completion cadence. Add deterministic and concurrent reader/reset coverage.
The GL swap records the interval between real swaps (ring of 4096, single
writer). TCP {"cmd":"perf","window_ms":N,"reset":0|1} returns count, fps,
mean / p50 / p95 / p99 / max interval and hitches (intervals over 1.5x the
median) for the last N ms; independent of guest vblank counting, so frame
generation, pacing and driver stalls are all visible. PSX_FPS_TELEMETRY adds
the same summary to the window title.
Used to measure R4 runs on a test bench (60.0 fps, p99 18 ms after the
cadence fix of the video-standard switch).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rkyma8rbyPiccs9n2bkyG6
Keep the requested internal resolution / supersample (shown and saved)
apart from what the backend last applied. When the live change is
unavailable (no GL, windowed high-resolution mode, CPU-authoritative dual
rendering, no context) or refused, the session scale is left as applied and
PSX_VIDEO_RESTART_RESOLUTION stays set, in both psx_video_apply_live and
the menu's restart bits (psx_host_video_restart_bits_backend, unit-tested
for the capable and refused cases). video_set rejects invalid presets and
factors without applying or saving, and reports applied values, capability
and pending restart. TCP command index regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
psx_video_current reported the live scale as the legacy supersampling
factor, so a second live change looked like a legacy edit and asked for
a relaunch. Report the boot value. Debug-tools builds gain video_set
(internal_resolution, supersample) to drive the live path in tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Extract the audited adapters and card transport onto master without the held selector/menu stack. Keep adapters and codecs tied to mod activation, use overlay ABI 33, retain rejected-load protection on codec registration, and refuse extended-card codecs in netplay before frame zero. Preserve default atomic save publication and guest-visible write failures.
* feat(widescreen): continue framebuffer-feedback sprites into native-wide bands
Titles tag a 15-bit sprite that copies a framebuffer 1:1 (gpu_ws_tag_feedback_rect / psx_mod_tag_feedback_rect). Where it touches the draw area's edge, gpu.c emits band spans and the new optional backend op wide_feedback_rect (GL incl. render thread, software) samples the source buffer's wide surface. Band texels count as bit 15 set, as the centre does once the trail blends.
The software rasterizer now writes the texel's bit 15 into the framebuffer, and wide_full can dump the surface alpha ("alpha":1).
* fix(widescreen): quantise band feedback like packed VRAM
The centre samples 5-bit packed VRAM each frame; the band copy read the 8-bit wide surface and modulated with /128, so the bands drifted ~3 grey levels brighter and left a step at the 4:3 edge. Truncate like PACK_FS and modulate like TEX_FS.
* Reject fully clipped sprites before extending feedback margins
* Complete isolated resident and PGXP fixture collaborators
* Preserve existing line endings in the review fixes
---------
Co-authored-by: Matthew Stanley <1379tech@gmail.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>
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>
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>
Windowed close ran shutdown_runtime() -> debug_server_shutdown(), which
closed the listen socket expecting that to break the I/O thread's blocking
accept(). Linux gives no such guarantee, so SDL_WaitThread joined forever
and the window sat dead until SIGKILL (SIGABRT variants seen when the racy
fd-close collided with thread teardown). SO_RCVTIMEO makes accept() return
every 200 ms to re-check s_io_running; the existing loop already treats the
timeout as retry/exit. Reproduced headless on Xvfb with xdotool windowclose;
core backtrace pinned the block at debug_server_shutdown+0x42 (WaitThread).
Exit after close: <1 s, code 0.
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>