idle_skip only skipped loops with zero stores and a single interrupt-check
edge, so it never fired on PsyQ libetc's v_wait -- the VSync(0)/VSync(n)
spin most PsyQ titles run each frame -- which decrements a stack timeout
word every pass and crosses two block edges.
New opt-in on top of idle_skip, [runtime] idle_skip_store_counters
(PSX_IDLE_SKIP_EXT=0/1 for one run):
- an iteration may span up to 8 block edges; the first edge is the anchor
and the edge count must be stable;
- one word store per iteration to the same RAM/scratchpad address whose
value drops by exactly 1 (a register holding it may drop with it).
Skips lower the counter by k (never to <= 1) and, for multi-edge loops, land
on the anchor strictly before the next device event so an IRQ is taken at
the same edge as without the skip. memory.c records the last word store's
address and value for the detector.
R4 race, 3600 frames from one savestate, held input: cyc, mmio, mc, sp, sc
and qc identical every frame; wc differs only by the elided counter stores.
Release build on an Apple M4: emulation-thread busy 47.5% -> 34.9% at 60 Hz.
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>
find_project_root walked up from a release's game.toml to the first
ancestor with a root marker. With a global ~/.gitignore (common) and the
game under ~/Games/<title>, that was the home folder, so [runtime]
memcard_dir = "saves" put memory cards and savestates in ~/saves instead
of beside the game. A folder whose licenses/psxrecomp-LICENSE was staged
by package_game_release.sh now stops the walk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PSX_EXECUTION_ID hashed raw bytes of the contract files and generated
bodies. A Windows checkout with core.autocrlf=true (the Git for Windows
default) has CRLF in every text file, so a Mac and a PC build of the same
commits (R4 #35, psxrecomp 2a4824fc) got different identities and the
netplay content gate refused the session with "content mismatch". LF
files hash exactly as before.
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>