Commit Graph

847 Commits

Author SHA1 Message Date
Shokunin cf756f0b2d GL: opt-in texture LOD + anisotropic filtering for world textures
[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>
2026-10-08 14:01:07 -07:00
Matthew Stanley 62d26bff82 Fix redraw policy, HD reload residency, AA ownership and FG budget publication
Address objective review blockers in #563, #567, #576 and #580 on the combined renderer stack. Preserve redraw/default policy, bounded same-pack learned uploads, GL context ownership, and coherent current-window budget accounting. Add focused CPU, real-GL, concurrent-publication and authority-transition regressions.
2026-10-08 11:16:22 -07:00
Matthew Stanley c87b8deab6 Merge commit '33a4ff56b0f769cf37f2804d00dce2e4cb106408' into integrate/pr558plus-fixes-20261008
# Conflicts:
#	runtime/include/frame_gen.h
#	runtime/src/gpu_gl_renderer.c
#	runtime/src/main.cpp
2026-10-08 11:00:53 -07:00
Matthew Stanley 699cc5bb3e Merge commit 'cb8a2e617dd5edbe1dc3dafbfc82f1a48bf3d6f6' into integrate/pr558plus-fixes-20261008
# Conflicts:
#	docs/TCP_COMMANDS.md
#	generated/OpenBIOS.emitter.sha
#	runtime/src/main.cpp
2026-10-08 11:00:25 -07:00
Matthew Stanley 0662437c56 Merge commit '2b517f3a0529742647b4c12c1a6f3d4818b29d84' into integrate/pr558plus-fixes-20261008
# Conflicts:
#	runtime/include/gpu_render.h
#	runtime/src/gpu_gl_renderer.c
#	runtime/src/gpu_render.c
#	runtime/tests/gpu_hd_texture_stubs.inc
#	runtime/tests/test_gl_hd_texture_pack.c
2026-10-08 10:59:40 -07:00
Matthew Stanley a03b1cf413 Merge commit '08cf54a99a8155191623b54e4f3a5b5ae1249d1e' into integrate/pr558plus-fixes-20261008 2026-10-08 10:58:36 -07:00
Shokunin 2b517f3a05 HD + render thread: keep the A0 hold across HD activation and savestate loads
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>
2026-10-08 03:41:54 -07:00
Shokunin 8f839e56f7 Render thread: publish the HD-authoritative VRAM at sync points
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>
2026-10-08 03:33:18 -07:00
Shokunin 64292bb23d Render thread: HD texture residency in command order
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>
2026-10-08 03:33:17 -07:00
Shokunin 7d06942eb0 Dynamic resolution floor "display": the output's own lines
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>
2026-10-08 02:40:17 -07:00
Shokunin 14feec22bf post_aa debug command: time the pass (PSX_POST_AA_TIME=N)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 02:40:10 -07:00
Shokunin 976da1a812 Opt-in post-process anti-aliasing (FXAA) and a supersample factor
[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>
2026-10-08 02:39:59 -07:00
Shokunin f3739b8495 Smooth motion: reprojection opt-in per title, redraw the default
[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>
2026-10-08 02:39:42 -07:00
Shokunin 4085710323 Smooth motion: real frames cheaper; reprojection: HUD needle, split screen, outliers
- 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>
2026-10-08 02:39:42 -07:00
Shokunin b0a89e4ede Smooth motion: in-between frames by reprojecting the real frame
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>
2026-10-08 02:39:42 -07:00
Shokunin e723060a5d Merge feat/pgxp-seam (#558-#560) for the PGXP depth API the reprojection test uses
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 02:39:42 -07:00
Shokunin 28fdd27b66 PGXP seam expansion for opaque 3D polygons (opt-in)
[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>
2026-10-08 02:39:11 -07:00
Shokunin a3293573d1 PGXP perspective-correct Gouraud colour (opt-in)
[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>
2026-10-08 02:39:03 -07:00
Shokunin dc2cd301d3 PGXP depth: no cost when off; render-thread-safe setters; Smooth motion honours depth
- 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>
2026-10-08 02:38:58 -07:00
Shokunin 2a36efcacb PGXP depth buffer for opaque 3D polygons (opt-in)
[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>
2026-10-08 02:38:41 -07:00
Shokunin 33a4ff56b0 Dynamic resolution: count Smooth motion's measured in-between frames in the load
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>
2026-10-08 00:22:20 -07:00
Shokunin b66d5c8672 Frame report: busy shares, dynres level, Smooth motion rates; PSX_HOLD_PADS
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>
2026-10-08 00:22:20 -07:00
Shokunin 705b8d8c86 Smooth motion: draw the first in-between frame while a third of the budget is free
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>
2026-10-07 23:51:35 -07:00
Shokunin 08cf54a99a HD residency: bind the sidecar to the pack; bound the restage re-hash
- The .hdres sidecar (version 2) carries the pack's identity, a hash of its
  sorted replacement keys (hd_texture_pack_identity); a load with another
  pack is refused, as the docs say.
- The learned upload rectangles are bounded by area as well as count: a
  restage (load, rewind) re-hashes at most one VRAM's worth of words.
- gl_hd_texture_pack_test: residency is refused with a different pack;
  before the load a textured draw shows native artwork, after it the same
  draw shows the HD replacement and counts as an applied draw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 22:51:31 -07:00
Shokunin 6799806481 HD texture packs: re-admit learned uploads after a VRAM restage
Rewind and in-session savestate loads have no sidecar. Each Beetle-keyed
upload rectangle seen in the session is remembered (at most 256); after a
restage it becomes resident again only if the restored words still hash to
a replacement key, so the HUD keeps its replacement across loads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 22:48:04 -07:00
Shokunin 8acc3d8093 HD texture packs: keep upload residency across savestates
A savestate restage cleared every upload identity, so a texture uploaded
once (a race HUD) lost its replacement after any load until the game
uploaded it again. Saving a slot now also writes the Beetle-format residency
beside it (*.hdres: host metadata, never in the .pst), bound to a CRC of
the whole native VRAM. A load restores it only over exactly that VRAM, so
no identity is fabricated; anything else falls back as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 22:48:04 -07:00
Shokunin 8948e9584c Smooth motion: plan the frames the clock shows; vrr in settings.toml; atomics
- 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>
2026-10-07 22:34:36 -07:00
Shokunin 0bebface12 Smooth motion: budget dynres for the in-between frames, time them from the game frame's arrival, keep small views put, move HUD needles
- 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>
2026-10-07 21:49:54 -07:00
Shokunin 3c64a13347 Smooth motion at any refresh; keep matching after a display-size change
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>
2026-10-07 21:49:54 -07:00
Shokunin 2b7f1db676 VRR present mode: [video] vsync = "vrr"
Variable refresh (G-Sync/FreeSync, ProMotion): present each frame when it is
ready (swap interval 0). Smooth motion targets the panel's maximum refresh at
the current size (psx_sdl_display_max_refresh) and never exceeds it.
PSX_HOST_REFRESH_HZ / PSX_HOST_REFRESH_MAX_HZ simulate a display for headless
tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 21:49:54 -07:00
Shokunin 91cd6db705 guest_cycle_scale: one CPU charge for every tier, declarative gate, rollback
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>
2026-10-07 21:41:39 -07:00
Shokunin 1e6e43639a Title timing constant: [timing] guest_cycle_scale (and _gated)
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>
2026-10-07 21:41:34 -07:00
Matthew Stanley 7bcb11916a GPU packet keys keep the enhancement aperture apart from the main RAM it aliases
The UI prepass re-read aperture node payloads through the RAM-folded key, so
THPS2's relocated prim arena discarded the HUD squash every frame.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit fd6267e952)
2026-10-07 18:20:59 -07:00
Matthew Stanley c55ddc4f29 Widescreen HUD size: proportional option shrinks the auto-UI HUD beyond 16:9
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 41a9889566)
2026-10-07 18:20:59 -07:00
Matthew Stanley 9032b1005e Always-on host CPU sampler ring (TCP host_profile)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 0901bb8b1e)
2026-10-07 18:20:58 -07:00
Matthew Stanley dd64bc6611 TCP input injection for port 2 (set_input/press/clear_input port=2)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 66cc81fe63)
2026-10-07 18:20:57 -07:00
Matthew Stanley 4b4fddb769 Present finite 2D backdrop scenes at 4:3 in native-wide
A full-screen rect backdrop that stops short of the reveal vetoes polygon overhang (Tomba village, beads-eio.4.28).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 17:46:17 -07:00
Shokunin 3b7f8ca195 Mods: package-relative default for file and folder resources
A [[resource]] may name default = "<path>" inside its package. While the
player has selected nothing, that path resolves (a required resource with a
default no longer blocks launch); a selection always wins. Lets a title ship
an HD texture pack (psx.hd-textures "pack" folder) in its own package.
Unsafe paths and verified/input/shared resources are rejected at scan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:01:34 -07:00
Matthew Stanley b620b13837 Integrate current framework master without dropping texture fixes 2026-10-07 14:39:05 -07:00
Matthew Stanley ed398cc37e Improve DuckStation texture capture and replacement fidelity 2026-10-07 13:40:57 -07:00
Shokunin 7b741b10f4 Frame generation: shared corners are one vertex; no motion rounding
The remaining tunnel cracks came from vertex sources, not from missing
depth: 46 % of R4 lookups were marked ambiguous because the same 3D point
is projected several times per frame (corners shared by separate meshes,
other functions), so those vertices fell back to a triangle-mean guess.
- The same point projected again is one vertex (the smaller identity, so
  the choice does not follow draw order).
- Up to two different points on one pixel are kept; gpu.c picks the one
  projected by the same function as the triangle's other vertices, else
  the one nearest their depth.
- Per-vertex motion is no longer rounded to 1/8 px: at high internal
  scales the rounding opened hairlines along long T-junction seams.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 12:57:02 -07:00
Shokunin ba66fa6cac Frame generation: close cracks, skip frames the camera passes through
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>
2026-10-07 12:57:02 -07:00
Shokunin dc4197c053 Frame generation: in-between frames from an in-between camera
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>
2026-10-07 12:57:02 -07:00
Shokunin 0269264da2 Frame generation: plan ceiling for render-thread overload, stalls judged by wait
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>
2026-10-07 12:57:02 -07:00
Shokunin 816d8e47f4 Frame generation: warm cost estimate, re-probe, guest-paced breaker 2026-10-07 12:57:02 -07:00
Shokunin bfbe8489de Dynamic resolution: fast first descent on the render thread
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>
2026-10-07 12:56:48 -07:00
Shokunin f10db8a0f0 Present thread: swap off the render thread ([video] present_thread)
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>
2026-10-07 12:56:48 -07:00
Shokunin 4f97482f8c Frame generation on the render thread from recorded draw lists
[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>
2026-10-07 12:53:09 -07:00
Shokunin af5e6be7d2 Dynamic resolution: allocate the hr surface at the current level too
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>
2026-10-07 12:52:53 -07:00
Shokunin 9e1591fc12 Dynamic resolution: allocate native-wide surfaces at the current level
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>
2026-10-07 12:52:52 -07:00