[video] frame_generation_method = "redraw" (default) | "reprojection",
PSX_FRAME_GEN_METHOD overrides; replaces frame_generation_redraw.
- Redraw runs and allocates nothing of reprojection: no snapshots, depth
image, programs, HUD matching; the cost-spike clamp and the dynamic
resolution load formula apply to reprojection only. Switching to redraw
or turning Smooth motion off frees what reprojection held.
- Uniform locations and PSX_RP_DEBUG/PSX_RP_NOPREV are read once with the
programs.
- Each method gets its own breaker settings, set again on a switch;
frame_gen_hold takes a kind (step down / over budget), not a string.
- The warp's depth is cleared before the cars are drawn (PGXP depth tested
them LEQUAL against it).
- The warp restores the renderer's texture bindings on units 1 and 2 and
never leaves a sampler on texture 0.
- gl_frame_gen_test: redraw asserts nothing of reprojection ran or was
allocated (its phase 0.5 digest matches master's); reprojection runs
check the warp's pixels on a parallax pan (vs the redraw of the pair, no
holes, HUD exact), real frames unchanged, and no warp depth with PGXP.
- FRAME_GENERATION.md: the key, the default, reprojection and its
R4-tuned object rules.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each in-between frame warps the newer real frame's finished image by the
in-between camera instead of drawing the scene again:
- per game frame: a snapshot of the list's finished image (taken when the
list closes; R4 flips to a buffer before drawing into it, so the live
surface is not a stable source) and a 2x native image of camera 1/z,
pixel kind and view rasterized from the recorded triangles in painter
order (world / car-or-dome / 2D-HUD-small-view);
- per in-between frame: one grid draw (a vertex per native pixel) that
unprojects world pixels with their depth, moves them by the view's
in-between camera (fg_view_affine) and samples the snapshot at full
resolution, nearest wins; cells over depth edges or non-world pixels are
left to the snapshot underneath; 2D/HUD pixels are put back; cars are
drawn again at their own in-between position.
Measured: ~0.3-0.8 ms CPU and ~0.3-2 ms GPU per in-between frame (was 2-4
ms each), A/G/B max out-of-range 0.3-3 %, worst ratio 1.1-1.5.
[video] frame_generation_redraw / PSX_FRAME_GEN_REDRAW=1 keeps the old path.
Planner/holds: dynres 'over budget' no longer stops cheap reprojected
frames; guest-late trips ignored while they are cheap; allocation and
shader compilation are kept out of the cost estimate. frame_gen JSON:
rp_ms, rp_zn, reprojected, np (why a flip planned none); debug buffer 4 KiB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Dynamic resolution budgets the real frames with the share of the game
frame Smooth motion leaves them (gl_renderer_frame_gen_real_share), so the
scale drops until the in-between frames fit the display refresh.
- In-between frames are timed from the game frame's arrival, not from the end
of its composition; the last one per game frame is no longer dropped by a
quarter-interval margin.
- Views under 15 % of the largest (a rear-view mirror) stay as the real frame
drew them: a wrong mirror camera fit threw its lane marks across the
screen as a dashed line.
- HUD: 2D triangles both frames draw with the same key that moved a little
(tachometer needle) are interpolated; changed digits stay as drawn.
- frame_gen JSON: end_ahead / end_bp / end_phase counters.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In-between frames go on one global grid at the display refresh (or the
VRR panel maximum), never faster, across game frames; the interpolation
phase comes from the host clock, so 90/100/144/165/240 Hz and other rates
that are no multiple of 30 work. Up to 15 generated frames per game frame.
Fix: a display buffer that changed size at the same origin (e.g. the first
frame after a save-state load) left a stale entry that every list closed
with, so no two frames ever matched and Smooth motion stayed off for the
rest of the session. The entry is replaced now. frame_gen JSON reports
why the last flip planned nothing (noplan_why).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #561. Guest time now has two entry points (psx_cycles.h): the
CPU charge psx_cpu_charge() (instruction base, load fudge/region/completion,
i-cache refills, BIOS HLE costs), which applies [timing] guest_cycle_scale,
and the unscaled device advance psx_advance_cycles() (DMA, idle skips,
mul/div and GTE deadline waits). The emitters' block-mode charges emit
psx_cpu_charge; overlay DLLs accumulate it raw and publish through the
cpu_charge callback (was advance_cycles, same slot) into the same host
function, so nothing is scaled at the DLL boundary. The batched
psx_cyc_charge path, the dirty-RAM interpreter, psx_cyc_readmem, i-cache
refills and bios_hle.c all charge through it. At scale 1 it is exactly
psx_advance_cycles.
Gate in psxrecomp: [timing] guest_cycle_scale_gate, RAM predicates judged
at every VBlank edge (deterministic, works online, no mod needed), plus
psx_mod_set_guest_cycle_scale_gate() in mod_plugins.h for custom logic or
to tie the scale to a mod (guest_cycle_scale_gated = true; every mod
session reset shuts it). The carried fraction and both gate states are a
boot_state section (BS_SEC_GCS, written only when the scale is not 1), so
savestates and the netplay rollback snapshot re-simulate the same cycles.
psx_guest_cycle_scale_gate_test pins tier identity, the gates and rollback
re-simulation against the real psx_cycles.c.
OpenBIOS regenerated (emitter stamp); retail SCPH1001 is not.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No CPU is emulated: recompiled code charges the guest clock a fixed cost
per instruction. guest_cycle_scale = N charges 1/N of it (instruction,
load, mul/div, GTE latencies, overlay DLL batches) while VBlank, timers,
CD, SPU and DMA keep hardware time, so the game never runs out of frame
time. A title constant in game.toml (1 = faithful, 1..64), never a player
setting; guest_cycle_scale_gated applies it only while the title's mod
opens the gate (psx_guest_cycle_scale_gate_open). PSX_GUEST_CYCLE_SCALE
overrides for testing. A scale other than 1 is folded into the netplay
content fingerprint as a safety net.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[video] pgxp_depth_buffer (default off; PSX_PGXP_DEPTH overrides) and
pgxp_depth_threshold. gpu.c hands the GL backend each triangle's GTE SZ when
all three vertices are dataflow-precise (gr_set_depth_triangle); the backend
draws those opaque triangles with a LEQUAL depth test and write. 2D,
unproven, semi-transparent polygons, lines and rectangles never test or
write. Depth mode is a batch key and rides in every high-resolution-window,
native-wide and render-thread replay command. Clears are colourless
depth-only draws through the flat batch: after a drawing-area change or a
fill, and when the average SZ rises by the threshold (4096, as DuckStation).
Generated (Smooth motion) frames draw without depth.
Docs: ENHANCEMENTS.md G1.14.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under HD texture-pack authority (and dual-raster netplay) every draw was
also rasterized into CPU VRAM unless a render pass was active. Replayed
draws of a generated frame were not exempt, so each in-between frame wrote
interpolated geometry into the guest's native VRAM. They are presentation
only, like a pass. Fixture: compose under HD authority, VRAM unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gpu.c's precise-triangle path references Smooth motion's vertex sources;
the software-only fixture never reaches them (abort if it does).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gpu.c asks the GL backend and gte.cpp for Smooth motion's vertex sources;
master's native_wide_projection_test (cd9981cd) links gpu.c with these stubs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pairing triangles of consecutive draw lists by key and draw order paired
the wrong ones whenever the game reordered, culled or switched LODs (R4:
a grandstand of one repeated texture turned into a staircase; on a
synthetic version 225 of 226 pairs were wrong). No primitive is paired by
draw order any more.
gte.cpp notes each RTPS/RTPT output (only while generation is on) with an
identity (issuing ra + model-space vertex), its camera-space position and
H, indexed by screen word; gpu.c records each polygon vertex's source
ahead of the triangle (RTH_FG_SRC, never drawn). Per view the camera
motion between the two frames is fitted from the paired camera-space
vertices (RANSAC + least squares); one real frame is redrawn from the
in-between camera: world vertices re-projected, vertices moving against
the world (cars) lerped in camera space, CPU-built vertices moved with
their triangle, HUD/2D unchanged. Moving forward the older frame is
redrawn; uncovered areas show the newer real frame. A verdict rejects
the generated frame (the real one shows) without projections, with few
pairs/inliers, or with a motion no camera makes. Real frames are
unchanged (gl_frame_gen_test).
PSX_PRESENT_SHOT_BURST=N captures the next N composed frames, real and
generated, for checking in-between frames against their neighbours.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A render thread a frame behind lowers the plan's ceiling by one (recovering
one per 2 s) instead of tripping the breaker; the guest waiting on the queue
trips it only when the wait was a stall (a quarter of the game frame). The
real-frame cost plans with CPU time; the GPU span is reported only. The
generated-frame blit copies colour only and marks the stencil stale.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both swap paths go through gl_window_swap. The temporal-blending ownership
guard allows the present thread's context (pt_begin only). The frame
generation fixture used setenv, which MinGW lacks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Armed at init (session start, render-thread start, a new resolution) and on
a savestate load, game entry or window resize. While armed, an overrun jumps
straight to the highest level whose pure-area prediction is at or below
0.78, then is judged like any step; a fit after a step, an up step, a failed
judgement or 6 s disarm it and single steps fine-tune. A fixed cost only
makes the target dearer than predicted, so a jump never lands below the
level that fits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opt-in under the render thread. Composed frames go to a ring of offscreen
slots at the window's size (binds of framebuffer 0 on the composing context
land on the current slot); a present thread with a second, shared context on
the same window waits on the slot's fence, copies it 1:1 and swaps. The
compositor's wait moves off rendering; the composing side waits only while
every slot is queued or on screen. Falls back to direct swaps when the shared
context or sync objects are unavailable.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
master's HD texture packs (#557 base) added gpu_hd_textures_* calls to the GL
renderer and the gpu_render.c facade; the fixture runs without a pack. The
command index is regenerated for frame_gen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[video] frame_generation (opt-in, with render_thread): the render thread keeps
each game frame's replayed draw list, matches the triangles of the last two
(frame_gen.c), and presents in-between frames at the display refresh by
redrawing the newer list into separate surfaces with matched vertices
interpolated. It never runs guest code. Real frames are composed at their
flip and presented after the generated ones, with the same pixels and
order. Generation uses only measured surplus: a plan from render-thread CPU,
generated-frame cost and swap time, a breaker after backpressure or late
guest frames, and a pause while dynamic resolution is over budget. The
render-thread core gains a timed tick hook. Frame-cost queries pause around
generation, so dynamic resolution measures real frames only.
Tests: frame_gen_test, render_thread_test (tick), gl_frame_gen_test (real
frames identical off/on; phase 0/1 equal the endpoints). Docs:
docs/FRAME_GENERATION.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hr (VRAM) surface stayed allocated at the ceiling. On GL-on-Metal every
render pass loads and stores the whole attachment, so its cost followed the
ceiling rather than the level: R4 2P VS at 5x ran 40-45 Hz for its first
seconds with a 10x ceiling and 60 Hz with surfaces allocated at 5x. Allocate
a new hr surface at each step and reseed it from the 1x image and the saved
rects; a failed allocation falls back to the in-place step down, or refuses
a step up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A native-wide surface was allocated at the dynamic-resolution ceiling and
rendered into its lower-left corner. On GL-on-Metal every render pass loads
and stores the whole attachment, so the wide surfaces cost the ceiling's
bandwidth at every level. Allocate them at the level they render at and
reallocate at each step, rescaling the presented rows and margins from the
old surface. The limit check stays at the ceiling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The step record's replay sat inside the debug-tools block, so release
builds dropped every recorded step. gl_render_thread_test gains a dynres
mode: the same level steps between frames with the thread off (applied at
once) and on (recorded), identical guest readbacks, VRAM, frame and wide
surface, and the render thread's frame cost measured. Also drop the
duplicate gpu_ws_background_requires_full_composite stub the #508 merge
left in the scale-invariance fixture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
master's HD texture packs (#557 base) added gpu_hd_textures_* calls to the GL
renderer and the gpu_render.c facade; the fixture runs without a pack.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
render_thread_test is registered on MinGW (winpthreads provides the
fixture's pthreads and nanosleep; the core takes its Win32 path).
gl_render_thread_test gains the MinGW link path run_gl_scale_invariance.py
uses on test/gl-scale-invariance-windows (static SDL3, opengl32 and its
Win32 libraries, LTO, probe.exe), and a host with no window or GL 3.3 core
context (headless, Windows over SSH) exits 77 from a preflight run, reported
by CTest as a skip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The OSD query at the two present-skip sites now goes through
ov_needs_present(), which falls through to host_osd_needs_present()
unless the render thread is replaying a present with its recorded
overlay state.
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>
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>
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>
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.