[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>
- Dynamic resolution: Smooth motion's share inflates the measured cost
instead of shrinking the period, so guest-bound detection works again
(it had pinned the level at the ceiling).
- Cost estimate: a sample more than 4x the estimate counts as 4x (one
stall no longer stops generation for many seconds); compiling the
reprojection programs is outside the timed frame.
- Reprojection source: the surface snapshotted at the real frame's present
(two slots: newer and older); the HUD and mirror of both lists are
'keep' in the depth image, drawn last, so a HUD drawn after its list
closed is kept too. Tunnel A/G/B worst ratio 2.9 -> 1.3.
- HUD needles: 2D triangles that move between the frames are drawn at
their in-between position; under them the older frame's dial shows.
- Split screen: a view whose camera cannot be fitted keeps the real image
instead of rejecting the frame (FgCamParams.keep_partial).
- Car redraw skips object triangles before the first world triangle (the
sky dome). Redundant base copies skipped when reprojecting.
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>
fg_plan drew the first in-between frame (to measure its cost) only while the
real frame left half the budget. On Windows with the display ring, R4's 30 Hz
real frame costs 2 x 7.2 ms of 33.4: the first frame was never drawn, its cost
never measured, and Smooth motion generated nothing (0 frames/s; macOS, at
2 x 5.7 ms, was under the line). A third of the budget is enough.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- settings.toml parses [video] vsync = "vrr" and a launcher save writes it
back as "vrr" (was dropped / saved as "on"); every mode round-trips in
video_enhancement_settings_test.
- fg_slots counts the display intervals the clock (fg_clock_phase) fills,
with the same 0.985 phase limit. fg_plan_hz and the dynamic resolution
share use it: 59.94 Hz content on a 60 Hz panel (2.002 intervals) plans
and budgets one in-between frame, as shown, not two.
- The real share read on the emulation thread uses atomics: s_fg_flip_vb,
s_fg_swap_ema and a published copy of the generated-frame cost estimate.
- fg_hud_lerp accepts a match at the 8th 2D triangle of the window.
- docs/FRAME_GENERATION.md: the plan's slots and the vrr mode.
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>
Corner and tunnel runs showed light cracks along seams and near the view
edge in generated frames:
- weld: every placed vertex at one screen position of a view moves alike
(the same corner arrives from several projections);
- a vertex without a projection on an edge between placed vertices moves
as that point of the edge (T-junctions from the game's polygon splits);
- a vertex the in-between camera passes is projected at a clamped depth,
and such a game frame is not generated (no near-plane clipping: tunnel
ceilings, overhead bridges);
- debug: "clamped", "guessed" (CPU-built vertices placed by a triangle
mean over a >1 px motion spread).
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>
[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>