R4 races present new content every other vblank, so at a saturated 4K
widescreen frame two vblanks behind was common and about half the
presents were dropped. Skipping is a catch-up step, not a frame-rate
policy: one skipped present is always followed by a shown one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One frame behind is the normal pipelined state when the render thread is
the slower side; skipping on it dropped nearly every present at 4K
widescreen. rt_frames_ahead() counts the complete frames queued after
the one being replayed; the backend skips only at two, where the
emulation thread is at the in-flight bound waiting on the render thread.
The unit test checks the count never exceeds the bound and that both
cases occur under a slow consumer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main.cpp's adaptive aspect asks it every native-wide frame; it only does
arithmetic on the driver limit and the internal scale, which change only
while the emulation thread holds the context. As a sync point it held
every widescreen frame on the emulation thread.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gl_render_thread_test runs a scripted stream through the facade twice per
scale (1x, 4x): every value the guest reads back, native VRAM, the frame
at internal resolution and the native-wide surface must match with the
render thread off and on. The stream has mask set/check, a mask-checked
upload (per-pixel readback sync point), back-to-back uploads of one rect
with a catch-up between gpu.c's writes and its commit, ring wraps,
readback frames, presents, and widescreen tags that change per primitive
and are poisoned after each call. A slow render-thread present keeps the
replay a frame behind. It fails for each of: reading tags live, sharing
the upload source with the guest array, dropping the stream state.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[video] render_thread (default false; PSX_RENDER_THREAD=0/1 overrides,
PSX_RENDER_THREAD_FRAMES bounds frames in flight, default 2) starts the
OpenGL render thread at the first vblank, after the context and every
startup GL call exist. Each vblank then closes a frame right after its
present and before wall-clock pacing, so the render thread draws while
the guest waits. Netplay, frame interpolation, Vulkan and software keep
the synchronous path.
TCP: render_thread reports records, frames produced/consumed, presents
and stale presents skipped, sync points with their recent reasons,
backpressure and render busy time. Display-ring and present-image-ring
readers sync first (the render thread fills them).
config_loader is a BIOS emitter input: OpenBIOS fingerprint regenerated
(tools/regen_bios.sh --config bios/OpenBIOS.toml; the C is unchanged).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the render thread running, the backend's facade table records every
state change, primitive, fill, copy, CPU->VRAM upload (payload copied)
and native-wide call, and the present, into the command ring; the render
thread replays them through the unchanged glb_* functions. With it off
gl_backend_get returns the existing table and nothing new runs.
Sync points: every other GL entry point reached on the emulation thread
(readbacks, savestate restage, render passes, OpenXR, settings,
diagnostics) drains the ring and takes the context (GL_RT_SYNC); the
emulation thread then runs the rest of that frame directly, exactly as
without the thread, and hands the context back at the frame boundary when
the frame may run asynchronously again (no depth24 display, dual-raster,
frame interpolation, render pass, netplay or OpenXR).
Guest-visible VRAM stays gpu.c's array: while the render thread owns the
context, uploads stage from a private copy that receives each payload in
command order. Guest state the backend reads while drawing (per-primitive
widescreen tags, the native-wide fast-path latch) is captured at record
time; host overlays travel with the present; a present that a newer
recorded frame already supersedes is skipped. The emulation thread answers
projective/wide-present/draw-area queries from mirrors of the recorded
calls. The per-frame display-ring readback is queued (fbo_peek_deferred)
instead of being a sync point.
The GL fixtures link render_thread.c and stub the two facade hooks; the
scale fixture also gains the gpu_ws_background_requires_full_composite
stub it was already missing (it failed to link on master).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Backend-neutral core for the opt-in [video] render_thread: the emulation
thread appends records to a single-producer/single-consumer ring and the
render thread replays them in order with the graphics context current.
rt_frame_end bounds closed frames in flight (backpressure), the render
thread can tell a frame is stale, and rt_acquire is the sync point:
drain, then move the context to the emulation thread until rt_release.
No lock is held while a record executes.
render_thread_test drives it with a fake executor and context token:
ordering and payload integrity across ring wraps, oversize refusal, the
in-flight bound under a slow consumer, stale detection, randomized
acquire/release interleaving and stop while held (TSan clean).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Native-wide mirrors now ride the window queue on the full-VRAM surface too
(wide_queue_live), share the queue tail (hiw_flush_tail) and scissor per
command; the guards check those forms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stale-part stencil rebuild shares the draw-area rows with the band
scissor; the guard checks the clamp where it now lives.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each mirrored batch switched to the wide surface and back, ending two
Metal render passes over the hr (10240x5120 at 10x) and wide surfaces;
cost grew with the allocation, so dynamic resolution's down steps did not
lower it. Mirrors now ride the windowed mode's queue (one pass per wide
surface, at the same sync points: uploads, pack, clears, stencil
rebuilds, render-pass copies, presents and dumps). GL_LINES mirrors (1x)
flush it and draw at once. Queue entries carry their scissor and palette
texture.
PSX_GL_WIDE_QUEUE=0 and PSX_GL_WIDE_STENCIL_FULL=1 restore the previous
paths for A/B; gl_scale_invariance_test gains a "wmask" mode (wide mask
stencil, centre splice on/off) and checks native VRAM, frame at S and the
wide surface are identical with either off, for wmask, lines, twin and
the scene at 1x/3x/9x and in the window mode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At S > 1 every GP0(E6h) mask-check enable after an unchecked opaque draw
rebuilt each native-wide surface's whole stencil from alpha, tiled. At a
10x allocation that is many render passes over 4270x5120 surfaces per
enable; R4 2P VS enables the check several times a frame and spent ~85%
of its emulation thread waiting on Metal command buffers there (16:9 at
10x: 17.7 guest Hz).
Track per wide surface the rect where the stencil may differ from alpha
(unchecked mirror draws by bbox, the centre splice, scale-step rescales,
render-pass restores) and rebuild only that. While the centre is spliced
from hr at every present its stencil is never observed, so the centre
part is left stale and rebuilt only if a mask-checked mirror draw meets it
with the splice off. Native (1x) keeps the whole-surface rebuild.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gl_internal_scale_guards checks the hr-window handling where it now lives
(present_vram_impl, present_wide_fbo_impl, the float present_target_quad);
dyn_apply refuses outright in the windowed high-resolution mode, which
dyn_eligible already excludes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A step controller (dynamic_resolution.c) lowers or raises the GL internal scale from frame-time telemetry between a floor and the configured ceiling; off unless a title or the player enables it. Tested by dynamic_resolution_test (synthetic load) and the real-GL gl_scale_invariance_test step modes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gl_renderer_set_dynamic_resolution(1) keeps the hr surface and the
native-wide surfaces allocated at the scale context init gave them (the
ceiling) and lets the internal scale step to any integer level from 1 to
it while the game runs. A lower level renders into the surfaces'
lower-left VRAM*S corner; every viewport, scissor, pack, copy and capture
already takes the scale at use, so only allocation and the present's
normalised UVs read the allocated size. Off (the default) nothing is
allocated differently and no step can run.
A step runs after a present, never inside a render pass:
- it takes the top-left sample of every native block of the old surface
(the sample the pack reads) into a 1x image and reseeds the new corner
from it, so the guest-visible VRAM is unchanged byte for byte;
- it rescales the displayed rect and the draw area from their old pixels,
so neither the frame on screen nor the one being drawn shows 1x blocks;
- it rescales the native-wide margins in place, in row bands ordered so
no source row is overwritten before it is read;
- it marks the mask stencil stale (rebuilt at once while mask checking is
on) and drops the render-pass backup and images.
Reallocating per step instead measured 30-106 ms at 7x-10x on an Apple
M4, mostly the first touch of the new surface. Steps are not offered in
the windowed high-resolution mode or with dual raster.
gl_scale_invariance_test gains "steps" runs: rendering after a step into
a ceiling-sized surface equals the fixed-scale run at that level; a chain
of steps (9x down to 1x and back, 5x) leaves the native VRAM unchanged at
every step, the two rects and the wide margins exact nearest rescales of
their old pixels, everything else the old top-left samples, and mask
checking right after a step correct; the window mode refuses steps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preserve negotiated content and dual-raster pass gates. Retain both fixture assertion sets and HD-off seams. Regenerate both BIOS with actual native emitter. Fix type-only rollback corrections and exclude extra-pad changes from digital-only promote gates.
A tip-extend made each seat reload some ring snap between load and the new
prefer tick: the initiator one it had just saved mid-frame during the
replay, a follower still short of prefer a pre-episode live snap. Neither
restore is cycle-exact with a straight run, so seats drifted apart (R4
2-peer under load: POST / resim core diverge after FOLLOW rereplays).
- A seat still replaying and not past prefer just continues to the raised
target: every tick it finished ran on unchanged rows.
- A seat past prefer replays again from the episode load (the pinned
frame-boundary baseline every seat matched).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The episode kept one peer-baseline / ready / POST latch, so with three or
four seats the first reply answered for everyone, and the one early
BASELINE stash was overwritten by the next seat's. In R4's 4-peer run with
the host loaded every frame, two guests replayed a light-tip episode and
resumed while the host had not opened it; host and guests then waited on
each other (pcap_freeze / wire_hole) until the stall timeout.
- BASELINE, ready-ACK and POST go through rnet_rb_quorum: the episode moves
on only when every other occupied seat has answered, and a digest from
any seat that differs fails the comparison. Early BASELINEs stash per
seat. Two seats behave as before.
- Light tip skips the ready RTT only with one peer; with more, every seat's
ready is how the others know it rolled back to the same load.
- A follower that has not simulated the load tick yet defers and joins when
it gets there (no NACK, no zombie-load refusal for that join).
- Seal, ready and verify timeouts restart while a seat still owing a reply
advances its input tip: they fire only when nothing moves.
- psx_netplay_peer_seats(); pins recomp-net rb_quorum.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four-peer rollback desynced under load (R4, host behind): the guests
predicted the slow host's pad, hash-confirmed the tick among themselves
through the one shared peer ring, then promoted the host's real pad into
history only (hc-silent promote) while the host simulated it. The host
stayed forked; its recovery episode was then refused as a "zombie load"
behind the guests' (wrongly) agreed tip.
- Hash confirm is a quorum over every occupied peer seat
(rnet_hc_set_peer_mask / note_peer_from, recomp-net) for three or more
seats; two seats keep the one-peer chain.
- A peer more than 4 ticks behind the others' inputs sheds its own view
(psx_mod_render_local_view status FAST_FORWARD, held 60 ticks), so the
presentation-only pass goes before the simulation slips.
PSX_NET_LOCAL_VIEW_SHED=0 keeps it.
- The admit-stall watchdog counts time without progress: a remote input tip
that advances restarts it, so a slow peer is waited for.
PSX_NET_ADMIT_STALL_MS (20000) and PSX_NET_LIVENESS_MS (1500) configure it.
- Pins recomp-net fix/rollback-span-stall.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Load modern texpage/texupload PNG replacements independently of DuckStation source. Integrate pack folders and options with mods, preserve native VRAM, and validate GL rendering, format matching, dump safety, and upload invalidation.