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>
Review (#599): fn and ctx were two separate stores read on the render
thread, so an unregister could pair a live fn with a NULL ctx. They are now
one immutable record behind an acquire/release atomic pointer. The GL
fixture runners compile a fixed source list; gpu_gl_renderer.c now calls
psx_host_overlay_draw, so every run_gl_*.py links host_overlay.c.
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>
Review (#598): resuming left the frame pacer's deadline in the past, so it
paid a pause back as up to ~12 unpaced frames. Resume now runs
present_session_reset, an audio cycle resync and a dynamic-resolution hold,
as a savestate load does. host_pause on=1 headless was a no-op that answered
ok; the headless frame path now enters host_pause_loop too.
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 window shows the live XR eye textures while a session runs, so the headset view can be watched on the monitor. Default is the left eye filled to the window; PSX_VR_DESKTOP_MIRROR selects 0 (keep the legacy desktop presentation, the side-by-side pair), 1 left (default), 2 side-by-side, 3 right. Uses the exact textures submitted to the runtime.
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>
Maintainer review of #584: PS1 dithering is hardware behaviour, so it is
now on by default whenever the guest sets GP0(E1h) bit 9 ([video]
dithering = "on"; "scaled" stays opt-in, "off" is the old true-colour
output).
Eligibility is decided per primitive from its command and carried in the
vertex instead of being inferred from the colour:
- Gouraud polygons and shaded lines: geometry vertex a_col.a + 2*131072
(decoded in GEO_VS, v_dith);
- texture-modulated polygons: a_semi + 8 (v_dith in TEX_VS);
- flat polygons/lines, raw-texture polygons and every rectangle: never.
This fixes the two reproduced bugs (Gouraud with 5-bit-exact colours was
skipped; textured rectangles were dithered). Bit changes no longer flush:
eligibility is fixed when the primitive is queued; mode changes still drain.
gl_dithering_test covers each case (Gouraud incl. equal colours, shaded
line, modulated and neutral-modulated textured polys dithered; flat tri,
flat rect, flat line, raw poly, modulated and raw textured rects never;
bit clear and off unchanged), plus scaled 4x == 1x, render thread, PGXP
depth, native-wide and the accurate-blending basis. 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>
pfx_stage_load checked loaded before failed, so a preset whose pass did not
compile was rebuilt on every present. failed is checked first; setting
[video] post_shader again (same path or not) reloads and clears it. The GL
fixture presents once more and checks no rebuild happened; the parser
fixture writes its sample files in binary mode (LF byte-exact on Windows).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Uniform locations are looked up once per program (each lookup can be a
driver round trip on a threaded GL; the executor runs per present,
generated frames included).
- State save queries only capability flags and the viewport. The mask pass
no longer clears (it draws 0 then 1), so the clear colour is never
touched; the present's window target and texture unit 0 are left bound.
The GL fixture checks exactly that contract.
- postfx_chain_test: a preset with CRLF line ends parses the same, and the
import check writes its shader in binary mode (it failed on Windows).
- docs/POST_EFFECTS.md: `generated reuse`.
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>
The GL fixtures (scale invariance, texture filter / bloom, render thread,
frame gen, readback) include mod_texture_banks.c, which now asks
psx_mod_host_output_scope(); define it as 0 (no host-output hook runs there).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review (#654): forget the ring's names after gl_renderer_render_thread_stop
(the worker presents and captures on its own thread, and stop drains it
and hands the context back), before the context is deleted. The call is
unconditional (an empty stub without debug tools). The fixture adds a
render-thread pass: frames captured on the worker and drained, more left
queued at shutdown, then checks the worker stopped first and every name
and pending readback is gone. Fails if the reset runs before the stop.
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>
Hidden-GL fixture: presents build the ring, shutdown must forget every
name and pending readback, and a second context rebuilds live objects.
Fails without the fix (names kept). run_gl_texture_filter.py gains
--debug-tools to compile the ring in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a renderer restart (lobby rematch, in-game menu -> match) the
capture read back into a stale pack-buffer name, i.e. into client memory
at offset 0, and crashed in glReadPixels (debug-tools builds).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tcc 0.9.27's lib/bcheck.c uses __malloc_hook, removed in glibc 2.34, so
the pinned build failed on Ubuntu 24.04 (lib/bcheck.c:738). Shards never use
-b; the build now passes BCHECK_O= unconditionally, keeping every other
libtcc1.a object. The bundle smoke also clears LIBRARY_PATH, CC and
GCC_EXEC_PREFIX and checks shards import no bounds-checking symbols.
Verified: linux_tcc_bundle_smoke_test passes on Ubuntu 24.04 x86_64
(glibc 2.39, gcc + libc6-dev installed, so host headers exist but are not
used) and on Steam Runtime sniper (glibc 2.31).
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>
Review 2 (mstan): psx_mod_read_* in a host-output hook now peek RAM /
scratchpad / BIOS ROM (psx_peek_word) and read MMIO as 0, so SIO data / stat
reads no longer pop RX or age the ACK. The CPUState copy's bus pointers are
replaced: reads peek, writes are dropped, so cpu->write_* cannot reach the
shared guest. psx_mod_call_guest_uncharged_counted clears *counted_cycles on
refusal. netplay_view_mods_test now runs the real sio.c behind the bus: the
host-output callback reads SIO_DATA / SIO_STAT through both the mod API and
cpu->read_*, and stores through cpu->write_* (RAM and SIO_CTRL); it checks 0
MMIO bus accesses, SIO stat / ctrl / RX unchanged, RAM unchanged. Other tests
linking mod_runtime.cpp get a psx_peek_word stub.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
savestate save/load with slot >= SAVESTATE_SLOTS (12) was refused with the
generic message that blames LLE runs, which sent a test chasing the wrong
cause. It now replies "slot N out of range (0..11)". Guarded in
test_savestate_status_protocol.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SIGPROF can land on a thread that is still being created. On macOS arm64
pthread_self() there hits a pointer-authentication trap (EXC_BREAKPOINT in
on_prof), which killed debug-tools builds at boot when a second instance
started lobby threads. The handler now compares the interrupted stack
pointer with the emulation thread's stack range; Linux keeps pthread_equal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A raw dump with trailing bytes can be a multiple of 2048 and mounted as a cooked ISO
(Legend of Dragoon disc 1: PVD read as zeros, BIOS SystemError B/906).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
gpu_ws_background_requires_full_composite and gpu_timeline_note were defined
twice after a merge, so every GL fixture that includes this file
(gl_post_aa, gl_bloom, ...) failed to compile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review (mstan): a host-output callback could change the shared CPU (GPR2 set
to 0x12345678 survived an online psx_mod_function_entry). Each host-output
callback now gets a private CPUState copy whose changes are discarded, and
while one runs (psx_mod_host_output_scope) every guest-mutating mod service
refuses: memory writes, host_write_ram, call_guest*, DMA / SPU / VRAM
uploads, guest and GPU-DMA allocations, texture packet memory, the
cycle-scale gate and finish_function. netplay_view_mods_test reproduces the
review case (GPR2 + PC changed in the callback) and checks the shared CPU is
byte-identical after the hook; it fails without the copy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- compile_overlays: the bundle layout now compiles with -nostdinc and only
the bundle's own headers (tcc's include/, then musl's libc-include/), so a
host with libc6-dev cannot change what a shard compiles against.
- runtime/tests/test_linux_tcc_bundle_smoke.py (ctest
linux_tcc_bundle_smoke_test): stages the bundle exactly as a release does
(pinned tcc 0.9.27 built from source, musl 1.2.5 headers, runtime
headers), compiles an emitter-shaped overlay shard through
compile_overlays' own tcc path (preamble shim, call_by_address rewrite,
exports) for PGXP flavors 0 and 2 with PATH emptied, loads the .so and
checks overlay_abi() and the exported function. Linux x86_64 only (skips
77 elsewhere). Passes in the Steam Runtime sniper SDK image.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by the clean-box smoke: every shard failed with "';' expected (got
va_list)". With the adaptation, 26 overlay shards compile on a Debian box
with no compiler and no libc headers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tinycc publishes no Linux binary, so Linux release toolchains had no tcc and
a player without a compiler -- every Steam Deck: SteamOS has none and a
read-only root -- kept overlays interpreted. stage_toolchain now builds the
pinned, unmodified tcc 0.9.27 source on the packaging machine and stages it
as overlay_toolchain/tcc/ (tcc, libtcc1.a, tcc's include/) with musl 1.2.5's
MIT-licensed C headers in libc-include/ (both sources SHA-256 pinned).
compile_overlays.py recognises that layout and calls tcc with -B<dir>
-nostdlib -isystem <dir>/libc-include <dir>/libtcc1.a, so nothing from the
player's system is needed; shard libc symbols resolve against the game's
glibc at dlopen. Windows and system tcc invocations are unchanged. The
TinyCC notice names the Linux build and the musl headers.
Tests: test_release_stage_cache.py (Linux pins, notice, bundle args).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SPU's guest-clock phase was applied lazily at the next catch-up, after
the guest had run on, so the phase was taken against a later clock and
lost; a rollback replay rendered SPU samples at other guest cycles than
the live run (voice / SPU RAM split with an identical CPU path,
PSX_RB_DET_SELFTEST). boot_state writes CLOCK before SPU, so the load
anchors it immediately.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plugin that only reads guest state to drive this player's host output
(rumble, haptics) can now declare netplay = "host_output". Online it stays
on per player: its function-entry hooks run in the shared simulation while
the rest of the own-view plan is parked, a completion request is ignored and
every psx_mod_write_* it attempts is refused, so it can never diverge peers.
Offline it is an ordinary plugin. netplay_view_mods_test covers both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>