Keep both netplay_peer_frontier.h and netplay_fmv_confirm.h includes; advance lib/recomp-net to a48d355 (main, includes #38).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
PSX_RB_DET_SELFTEST on R4 4P showed a replayed save of tick T differing
from the live save of T. Each cause, and the fix:
- Replay's finish_frame poll passed no PC hint (Live used the IRQ resume
chain); it now uses psx_netplay_snap_resume_hint.
- Devices are serviced lazily up to the next event, so root counters and
cycles_since_vblank lagged the clock by a host-path amount at save; a
load re-anchors them at the clock and drops the lag. The save now
publishes pending cycle charges and services every device to the clock
first, and renders the SPU up to it.
- GTE / mul-div completion stamps and the load-delay give-back were
re-anchored at the restored clock on every load, so a stall in flight at
the snap point cost the live run cycles but the replay none. New
BS_SEC_CPUTIM carries them relative to the clock.
- The SPU CD-input FIFO was outside the snap and wiped on rollback loads;
new BS_SEC_SPUCD carries the queued frames and the load keeps them.
- LZCR is serialized as derived from LZCS (the raw backing stays 0 until
the first LZCS write; guests only ever read the derived value).
Older snaps without the new sections load as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replay's finish_frame saved its tick snap with no PC hint while Live used
the IRQ resume / last check / cpu->pc chain; the same tick's snap could
carry a different resume PC (psx_netplay_snap_resume_hint now serves both).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Live realign restores the snap at T, sets the session to T+1 and
longjmps back into the guest from the admit spin. The resume ran before
T+1 was admitted, so that frame latched no inputs and did not advance;
the next frame was then counted as T+1 and the realigned seat ran one
frame ahead of its tick label from there on (ring saves after a NACK
realign carried the next tick's clock, 2x the cycles in the first
tick). Only the realigned seat shifted, so it forked from every other
seat; with four seats this produced the "resim core diverge" splits
and the later hc-fork / hang cascade.
flush_resume now admits the tick first on the Live realign path and
keeps spinning until it can.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With three or more seats two guests can mispredict the same tick and both
BEGIN. The tie-break (lower initiator slot wins) ran only on initiators,
so a follower followed whichever BEGIN arrived first and stashed the rest.
On a 4-seat WAN race the host followed slot 3's epoch while slot 1's won;
the seats split across epochs, each waited for baselines that never came,
and the match ended after 20 s at rb_baseline.
The epoch id names its initiator, so a follower now applies the same rule
to the episode it follows (netplay_rb_concurrent_begin): yield during the
handshake and follow the winner, or drop the loser's BEGIN. A follower
seeing a new epoch from the initiator it already follows (abort and
reopen, the only case two seats can produce) is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two rollback bookkeeping bugs that only show with more than two seats
over a real link:
- g_peer_resolved_through took the highest RESOLVED from any peer. With
three seats the fastest follower stood in for the others, so the host
opened loads past the slower followers' frontiers (3P WAN: load 2400 vs
frontiers 2344 and 2319); every follower refused, episodes aborted and
the session diverged at 2496 before the race started. Adverts are now
kept per seat (netplay_peer_frontier.h) and the lowest across the
peer roster is the frontier.
- The §55 bisect cap (next load < L after a baseline mismatch at L) was
only cleared by an episode commit. An abort followed by a Live heal
left it armed for the rest of the session; once its loads left the
snapshot ring every episode was refused with "no confirmed snap"
(4P WAN: cap 1928, agreed 3136, ring 2656..3166) and a later fork at
3150 was never repaired. A live hash-confirmed agreed ADVANCE at or past
the cap now retires it.
Bumps lib/recomp-net for RB_RESOLVED sender attribution and rb_* queue
retransmit coalescing (RetroPortingToolKit/recomp-net fix/rb-ctrl-queue-dedupe).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Enhancement arenas (GPU DMA aperture, Expansion-1 mod memory) were untracked, so Crash widescreen drew every vertex native with no depth. Adds depth stats/clear causes to pgxp, depth to pgxp_tri_ring, input_route_start at_frame, integer number keys in game.toml.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Games that read ENVX right after KEYON saw 0 until the per-frame pump and keyed the voice off (Crash spin sfx at 21:9).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The post-FMV lockstep gate counted a tick as agreed only when sim-1 was
already hash-confirmed. A peer digest arrives one RTT after its tick, so
the peer running ahead (the host, cross-machine) never saw sim-1 confirmed,
reset the streak every tick and logged 'rb DESYNC - FMV lockstep MAX
unmatched' after every movie while every digest matched. Each newly
confirmed tick past the movie's end now counts once; only a real mismatch
resets the streak.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In a netplay match psx_mod_set_host_rumble now takes only this peer's own
guest port (psx_netplay_local_port) and drives this peer's input pad with
it; other ports and calls made while rolling back are ignored, so a
netplay = "host_output" plugin can report every port each tick.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Some titles compute vibration for every pad but only send it over SIO to a
DualShock (R4 with a presented NeGcon). A trusted mod can now hand the runtime
DualShock motor values per seat; the host pad gets the louder of that and the
guest's SIO motors. Values lapse 8 VBlanks after the last call and are cleared
at every session reset, so nothing can stick on. Opt-in: no title that does
not call it changes. PSX_RUMBLE_TRACE now also logs headless.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[video] fmv_chroma_smoothing = true makes the OpenGL present of a 24-bit
(MDEC) frame keep each sample's luma and take its chroma from a [1 2 1]
tent over the 3x3 source pixels, hiding the 2x2 colour blocks of 4:2:0
video without softening luma. Pairs with the existing fmv_filter upscale.
Guest VRAM/MDEC output untouched; 15-bit presents and the default unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[video] texture_lod = "mipmap" emulates mip levels for PS1 texture pages,
which are decoded per fetch from VRAM and so have no GL mip chain: the
screen-space UV footprint of a minified, proven-3D primitive is box-averaged
(opaque texels of the fragment's STP class only), and anisotropic_filtering
(1..16) spreads that many taps along the footprint's major axis. Every tap
clamps to the primitive's UV bounds or texture window, so packed atlases
never bleed. The mode rides in the textured batch's filter key; off (the
default), sprites/HUD and magnified textures are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Track an open GP0(A0) upload in the renderer whatever the HD state
(facade gr_vram_upload_begin/commit/set_open): HD activation mid-A0
waits for the commit instead of reading the FBO over received words,
and a full-VRAM load no longer ends the hold; gpu.c restores it from
gp0_state on load and ends it on GP1(01h). Sync the render thread
before flushing dumps or reading HD diagnostics, suppress dumps from
generated frames, use _putenv on MinGW, drop a stale log. Adversarial
fixtures for both cases in full-VRAM and high-resolution-window modes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With an HD texture pack the CPU raster is native VRAM's authority, and on
the render thread it writes the private copy: gpu.c's array never saw those
draws, so guest readbacks, mask checks and savestates read stale VRAM. Each
acquire now copies the private result into gpu.c's array (the analogue of
ensure_cpu's FBO readback), leaving the words of a GP0(A0) payload that is
still streaming. Fixture: a recorded fill/draw is visible to a readback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HD texture packs matched draws on the render thread but tracked uploads and
invalidations on the emulation thread, hashing gpu.c's array: a data race
(crash seen in hd_texture_pack_match) and wrong identities whenever the
emulation thread had already recorded a later upload.
The facade now sends residency changes through an optional backend hook
(hd_texture_note). The render-thread backend records them as RTH_HD_NOTE
after the upload/draw they follow; the replay hashes the render thread's own
VRAM copy, which the HD module now follows with the SW rasterizer
(gpu_hd_textures_bind_vram). Pack (re)configuration and HD mode changes are
sync points. Other backends apply the change immediately as before.
The HD GL fixture gains a render-thread case (fails without this change)
and links again: the scale fixture's facade stubs no longer collide with
the real gpu_render.c it includes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dynamic_resolution_min = "display" (game.toml, settings.toml,
PSX_DYNRES_MIN) floors the controller at the selected internal resolution
without the supersample factor, so with supersample > 1 it only trades
supersampling and never renders below the output. psx_dynres_floor_scale
in internal_resolution.h, tested in test_internal_resolution. OpenBIOS
emitter stamp regenerated (config_loader changed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[video] antialiasing_mode = "off" | "fxaa" | "fxaa_hq" (PSX_AA_MODE)
filters the composed game image after the game quad and before hold-last
capture, OSD and swap; guest VRAM, readbacks and the Smooth motion /
frame generation sources are untouched. Off (default) never runs the pass
and is byte-identical to master.
[video] supersample = 1.0..4.0 (PSX_SUPERSAMPLE) multiplies the internal-
resolution target (Match display or a line preset), so the existing area
resolve does ordered-grid SSAA; dynamic resolution takes it as its ceiling.
Debug server: post_aa reads/sets the mode live. Real-GL fixture
gl_post_aa_test; supersample checks in test_internal_resolution.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[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>
[video] pgxp_seam = "off" | "fine" | "wide" (default off; PSX_PGXP_SEAM,
PSX_PGXP_SEAM_PX). Above 1x, a vertex that sits on a neighbour's edge only to
the PS1's precision leaves a hairline crack. Opaque 3D triangles move each
edge outward (mitred, limited at sharp corners): fine = 1 output px, wide =
half a native px; UVs, colour, q and SZ extrapolate with the barycentric
coordinates of the new corners so textures do not slide.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[video] pgxp_color_correction (default off; PSX_PGXP_COLOR overrides). Gouraud
colour on PGXP 3D triangles interpolates with 1/SZ; textured triangles with
perspective UVs share their w. The flag rides in the per-vertex PGXP code the
depth buffer already carries (sz + 65536).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- gpu.c sends gr_set_depth_triangle only while a PGXP renderer feature is
on (gl_renderer_pgxp_render_wanted): no RTH_DEPTH per triangle otherwise.
- The SZ rides in the textured vertex's unused colour alpha, so TEXV stays
26 floats for every title.
- gl_renderer_set_pgxp_depth / _threshold / pgxp_render_stats sync with a
live render thread (GL_RT_SYNC) like their neighbours.
- A depth-mode change is its own batch-break reason (7); batch_diag returns
all eight reasons.
- Smooth-motion in-between frames replay RTH_DEPTH and test depth from
their own clear; gl_frame_gen_test gains a PGXP pass (overlapping 3D
triangles out of painter order) where the in-between frame must match
the real one.
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>
The render-thread controller's load now adds the generated frames actually
drawn in each sample, each at the mean measured cost of the timed ones (GPU
time or CPU wall, the larger), to the real frames' cost. Dynamic resolution
sees the whole render-thread spend: it steps up while there is room for the
real and the in-between frames, and counts the in-between frames against the
budget once they crowd the real ones, without assuming a full panel rate of
them at an estimated cost.
Generation's over-budget pause now looks at the real frames' part of the
load only (the last window's load less the generated frames' share), so the
in-between frames do not switch themselves off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PSX_FRAME_REPORT_MS (the release-build speed readout) now also prints the
emulation thread's busy share (the window less its pacer waits and its waits
on the render queue), the render thread's busy share, the dynamic resolution
level and load, and Smooth motion's generated / real presents per second.
PSX_HOLD_PADS="p1[,p2]" holds raw pad words (active low, 0xBFFF = Cross) on
ports 1 and 2 after every live sample, so a release build (no debug server)
can drive a race from a savestate (PSX_LOAD_SLOT) for timing runs. Unset,
both cost one cached branch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>