Commit Graph

54 Commits

Author SHA1 Message Date
Matthew Stanley bceadc6124 Add DuckStation-compatible texture pack mod and gameplay dumping
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.
2026-10-06 23:24:47 -07:00
Matthew Stanley d7fb33eb85 Add opt-in CD-DA playlists, media preparers, netplay render passes, hidden OpenGL headless and netplay_status
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 22:52:51 -07:00
Matthew Stanley b1899a2f51 Merge branch 'review/pr496-fix' into review/pr497-fix
# Conflicts:
#	runtime/src/debug_server.c
2026-10-04 11:03:22 -07:00
Matthew Stanley 8403bdf9a5 Merge branch 'review/pr493-fix' into review/pr496-fix
# Conflicts:
#	runtime/include/mod_plugins.h
#	runtime/src/main.cpp
#	runtime/src/render_pass.c
#	runtime/tests/test_gl_readback_region.c
#	runtime/tests/test_gte_register_access.cpp
#	runtime/tests/test_render_pass_sandbox.c
2026-10-04 10:57:48 -07:00
Matthew Stanley 0ae34cd0de Merge remote-tracking branch 'origin/master' into review/pr492-fix
# Conflicts:
#	runtime/include/gpu_gl_renderer.h
#	runtime/src/debug_server.c
#	runtime/src/render_pass.c
#	runtime/tests/test_render_pass_abort.c
#	runtime/tests/test_render_pass_sandbox.c
2026-10-04 10:41:19 -07:00
Matthew Stanley cd9981cda8 Clip proven native-wide world faces across camera and screen planes 2026-10-03 23:51:14 -07:00
Matthew Stanley d2c8f03e76 Add guarded native draw replay, motion interpolation and stable world filtering 2026-10-03 23:46:25 -07:00
Yves ec82c05216 openxr: add an experimental Win32/OpenGL backend
Adapted from the original feature commits, retaining their author credit:
[22bdc9e9](https://github.com/FractalEngineer/psxrecomp/commit/22bdc9e90d14f7a04b79c83b77611c4eb980fef4)
[5a1264b7](https://github.com/FractalEngineer/psxrecomp/commit/5a1264b79b69b5d75f7b8507e305b1e13a911a50)
[ac6f84ba](https://github.com/FractalEngineer/psxrecomp/commit/ac6f84ba039b1d27bb1e93d3b6d05390dfa42ff9)
[734977c9](https://github.com/FractalEngineer/psxrecomp/commit/734977c93e80789e9210a262daa35a88f41c40d4)
[04714837](https://github.com/FractalEngineer/psxrecomp/commit/04714837245ebe68abbc462ac6822a3818eedf67)
[5bafeebf](https://github.com/FractalEngineer/psxrecomp/commit/5bafeebf3f0c2eaaa38212f960b9f6f88351cb74)

Source scope and current-master validation: docs/internal/upstream/experimental-openxr-win32-gl.md

Co-authored-by: Yves <calibratedbeats@gmail.com>
2026-10-04 05:56:08 +08:00
Yves 305b5485b9 render passes: add atomic stereo pairs and scoped camera views
Adapted from the original feature commits, retaining their author credit:
[0a955971](https://github.com/FractalEngineer/psxrecomp/commit/0a95597129cc2ee6d70397a8da9511831575eae4)
[0a4cf21f](https://github.com/FractalEngineer/psxrecomp/commit/0a4cf21fac88ef425ba0edd25871d7dd20ee0903)
[22bdc9e9](https://github.com/FractalEngineer/psxrecomp/commit/22bdc9e90d14f7a04b79c83b77611c4eb980fef4)
[734977c9](https://github.com/FractalEngineer/psxrecomp/commit/734977c93e80789e9210a262daa35a88f41c40d4)

Source scope and current-master validation: docs/internal/upstream/atomic-stereo-render-views.md

Co-authored-by: Yves <calibratedbeats@gmail.com>
2026-10-04 05:54:05 +08:00
Yves dedbe47c97 render passes: expose refusal and framebuffer failure details over TCP
Adapted from the original feature commits, retaining their author credit:
[257a88b0](https://github.com/FractalEngineer/psxrecomp/commit/257a88b06d26bb99a9e6066b89f839af41090118)

Source scope and current-master validation: docs/internal/upstream/render-pass-refusal-diagnostics.md

Co-authored-by: Yves <calibratedbeats@gmail.com>
2026-10-04 05:54:03 +08:00
Shokunin 67da069f9c GL: texture window per vertex; opt-in texture-window batching
The OpenGL textured program read the GP0(E2h) texture window from a
uniform, so every window change ended the open textured batch (batch
reason 5). Titles that tile far textures with per-primitive windows pay
for that in draws: Ridge Racer Type 4's split screen changes the window
about 515 times a game frame and drew about 180 batches, against 17 with
batching on. On the native-wide path every batch that reaches the margins
is drawn again into the wide surface; under heavy load on an M4, 2P with
widescreen at Match display ran at 39-43 guest VBlanks/s and 1P at 50,
against 50-58 and 57 with batching on.

The window now rides in each textured vertex (a_twin, the E2h low 20 bits
as one float) like texpage and CLUT already do, and the shader decodes it
there; the uniform and the window-replay queue's copy of it are gone.
With the default unchanged, batches still split at every window change,
so every vertex carries the batch's window and the image is the same as
before: the new test's oracle passes on the unmodified renderer too, with
the same digests at 1x, 4x, 9x and in the windowed 9x/18x mode.

[video] texture_window_batching = true (game.toml only, default off) lets
primitives with different windows share a batch. The image is the same:
an opaque batch draws its colour in one painter-ordered pass and
semi-transparent primitives are still drawn one per batch. While mask
checking is on the window stays a batch key, because an opaque batch
fixes the stencil for its STP=1 texels after its colour pass.
PSX_GL_TEXWIN_BATCH=0|1 overrides it for a run and the TCP command
gl_texwin_batch switches it live. Software and Vulkan are unchanged.

Tests: gl_scale_invariance_test gains mode "twin" (alternating windows
over opaque, semi-transparent, set-mask, mask-check and bilinear prims,
into the native-wide margins): off and on give the same native VRAM,
frame at S and wide surface, on draws 17 batches instead of 46 and flushes
only for the 5 mask-check window changes. Each run also checks an oracle
that does not depend on the renderer: raw rects through all four windows
into a 4-bit and a 15-bit page must give, pixel for pixel in the native
VRAM and at the centre of each pixel at S, the texel the PS1 window rule
picks from the VRAM as read back. A decode or packing error both settings
share fails it (checked with (v_twin >> 11) and with off_y left out of the
packed window). video_enhancement_settings_test checks the key's default
and opt-in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 04:15:01 -07:00
Matthew Stanley 7c7133fbb4 Integrate MMX6 adaptive renderer with current framework master
Preserve current segmented dispatch, active-plan mod hooks and software scale limits. Extend function filters through the active plan, retain the last-store pointer with overlay ABI 25, and keep default config fingerprints stable. Refresh BIOS emitter stamps after byte-identical regeneration and reconcile regression fixtures.
2026-09-30 20:23:16 -07:00
Matthew Stanley 58bdf987d9 Merge master into feat/internal-resolution: combine with #422/#423/#424
Keep #422's 1x GL_LINES batch (gated s_hr_scale == 1 && !s_hiw) beside the >1x quad batch; FLIP capture uses hiw_capture_size; render passes use s_hr_scale and are refused under s_hiw.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 11:23:28 -07:00
Matt Stanley f8a6f4f185 Merge pull request #425 from RetroPortingToolKit/feat/internal-resolution-gl
Raise the GL internal-resolution ceiling to the driver's limits
2026-09-29 11:22:28 -07:00
Shokunin 4bb58c2a81 Render passes: measure a stale pass-cost estimate again
Only passes that run are measured. The first passes of a race can all run in
a transient (a busy host, code run for the first time): in earlier Grand
Prix runs the three warm-up passes measured 13-15 ms against a steady 3-7 ms
at the same setting. An estimate that high prices most plans out, no pass
runs to correct it, and a plugin that crossfades while passes are shed (R4
Frame Rate) stayed on the crossfade for most or all of the run: 4, 9 and 29
passes in 22 s at 300 Hz, 7 in a verify run at 120 Hz, against about 1400
when the first passes were fast.

render_pass_cost_note_plan() (render_pass_plan.c) counts the plans that
wanted passes since the last measured pass. After 30 (about a second of a
30 Hz game) the warm-up restarts, so plans ask for one pass until three have
been measured again. When the new median is more than 3/4 of the old
estimate, the old one was right (a size that is truly too expensive, or a
host at its limit) and the next wait doubles, up to 960 plans, so these
one-pass warm-ups stay rare; a re-measure that finds the estimate stale
keeps the wait at 30. render_pass_stats reports cost_rewarms.

render_pass_plan_test covers the restart after 30 plans, an estimate that
passes keep measuring never restarting, the minimum wait after a stale
estimate, and the doubling waits when re-measures confirm the estimate
(47 ms at 4K, 10 ms re-measured as 9); render_pass_guards pins the planner's
call and its one-pass re-plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:19:08 -07:00
Shokunin 3d705f73c3 Render passes: hold a late frame's newest image until the flip
A generation's timeline is fixed at plan time (t_len = plan period x
VBlank). When the game frame stayed up longer (a 30 Hz tick that lags to
three VBlanks, or host-interval drift), pass_gen_present expired the
generation past phase 1 + 0.5/period and the presenter fell back to its own
capture, which in FLIP mode is the frame's phase-0 image: the output ran
0 -> .25 -> .5 -> .75, stepped back to 0, then jumped to the next frame.

render_pass_gen_select() (render_pass_plan.c) now keeps render_pass_select's
hold of the newest image past phase 1 and only expires a generation after
RENDER_PASS_GEN_HOLD_MAX (4) frame lengths without a flip, i.e. a game that
stopped flipping. render_pass_stats reports late_presents (presents more
than 1/64 of a frame past its planned end). The plan keeps the plugin's
period: its phases were placed on the output deadlines of a frame of that
length, and the flip tracker's period is the previous frame's, which cannot
predict a sporadic lag.

render_pass_plan_test covers phases 1 to 4 (hold) and past 4 / NaN
(expire); render_pass_guards pins pass_gen_present to the rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:02:48 -07:00
Shokunin 673e75ec86 Render passes: reuse the VRAM backup for later passes of a frame
Every pass backed up the display rect (hr colour + stencil and raw mirror
blits, CPU rows) although the previous pass of the same frame had just
restored exactly that backup. On Apple's OpenGL each blit ends the render
pass and submits a command buffer, which was about half of a pass's begin
time. A pass now reuses the previous pass's backup when render_pass.c
vouches that no guest code ran since its restore (same plan, guest clock and
store count unchanged) and the rect, scale and native-wide source match; the
first pass of every frame still takes a fresh backup.

R4 Grand Prix race, 240 Hz interpolated: average pass begin 0.28 -> 0.16 ms.
render_pass_stats reports backups_reused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:02:48 -07:00
Shokunin 69e6b6b552 Render passes: refuse during fast-forward; say why passes are unavailable
Manual fast-forward presents every second VBlank, so the plan still saw
presents and kept rendering passes, spending host time the fast-forward
wants, although the API said turbo refuses them. main.cpp now records when
presentation is sped up (manual fast-forward, turbo-through-loads, FMV
auto-skip, TCP turbo) and the plan and passes refuse meanwhile.

psx_mod_render_pass_status() tells a plugin why passes cannot run, the
host-time budget aside: NO_PRESENTER, BACKEND (the renderer declines passes
in its current mode), DISABLED (after repeated faults), SESSION, FAST_FORWARD
or BUSY. The GL backend reports its part through
gl_renderer_pass_unavailable(), where a renderer mode that cannot host
passes reports BACKEND. psx_mod_set_frame_interpolation_blend() now also
takes effect when called from a hook after activation, so a plugin can show a
crossfade while passes are unavailable and return to HOLD afterwards.

Debug: PSX_RENDER_PASS_REFUSE=1 or TCP render_pass_refuse on=1 makes the
backend decline passes, to test a plugin's fallback; render_pass_stats
reports the status.

Default off: nothing reads the new flag or status unless a plugin uses
render passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:02:48 -07:00
Shokunin 856cbabffe Render passes: make pass image textures as slots fill
pass_slot_cap() limits a generation to what two generations fit in 256 MiB,
but pass_gen_textures() allocated all 17 slots of each generation at
internal-resolution size as soon as passes ran, whatever the cap: 34
textures, about 167 MB at 4x and 1.5 GB at 12x for at most two used slots.

Slot textures are now made on first use, never past the cap, so the budget
holds at any internal scale; a size change frees the old set, and context
teardown frees every pass texture, backup and journal FBO and forgets their
names (a new context no longer reuses dead ones). render_pass_stats reports
image_textures and image_bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:02:48 -07:00
Shokunin d22ebac9a4 Host-timed render passes for true in-between frames
psx_mod_render_pass_plan() / psx_mod_render_pass() let a trusted plugin
redraw its game between two logic ticks with the game's own draw code. A
pass is a sandboxed nested guest call:

- time is frozen: cycles are counted (GTE and mult/div deadlines keep
  working) but no device advances, no VBlank or device event fires and no
  interrupt is delivered; GPU linked-list DMA and delayed completions finish
  synchronously (psx_cycles freeze, interrupts, dma);
- stores go straight to RAM/scratchpad, bypassing code-page tracking,
  overlay watch and write traces; MMIO is an allow-list (GP0, GP1 DMA mode /
  info, GPU and OTC DMA, I_STAT/I_MASK) and SPU, CD, timer and other device
  stores are dropped and counted (memory.c);
- afterwards CPU (with the GTE), RAM, scratchpad, I-cache tags, interrupt,
  timer, DMA and GPU registers are restored exactly (render_pass.c, a GPU
  register checkpoint without savestate side effects), with a guest-cycle
  watchdog that rolls a runaway pass back;
- on OpenGL the declared display rect (hr colour + mask stencil, raw mirror,
  native-wide band, CPU VRAM rows) is backed up, drawn, captured and rolled
  back; writes outside it are journaled and restored, or refused.

The presenter keeps two generations of images (the game's own frame plus
one image per pass phase), promotes one when the flip-aware source sees the
display flip to its rect, and shows the newest image at each output
deadline's phase, crossfading across gaps when passes were shed. Presents
that fall due while passes run are made between them. The plan predicts the
output deadlines of the coming frame and sheds passes to fit a host-time
budget learnt from the presenter's idle time (PSX_RENDER_PASS_BUDGET).

PSX_MOD_FRAME_INTERPOLATION_HOLD repeats the newest frame where no pass
image applies, so a pass-driven title never shows anything later than stock.

Default off: nothing runs unless a plugin calls the API, and the plan
refuses in netplay, rollback, rewind, turbo, self-check resimulation, while
suspended (FMV) or without GL flip-aware interpolation. Pass writes are kept
out of frame_fingerprint. Debug: render_pass_stats, render_pass_dump,
PSX_RENDER_PASS_VERIFY=1 (state + VRAM hashes around every pass).

The freeze API lives in the runtime-only psx_cycle_freeze.h. psx_cycles.h is
part of the overlay codegen hash and stays byte-identical, so titles that
never use passes keep their overlay caches and savestates.

Tests: render_pass_plan_test (deadline-exact phases per rate, shedding,
selection), render_pass_freeze_test (10^6 frozen cycles, exact restore,
watchdog), render_pass_guards (sandbox choke points; the codegen-hashed headers stay
free of the pass API).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:02:47 -07:00
Shokunin 5ffd57d7f3 Flip-aware frame-interpolation source
psx_mod_set_frame_interpolation_source(PSX_MOD_FRAME_SOURCE_FLIP) makes the
OpenGL presenter rotate its blend history only when the guest really flips
(the displayed VRAM origin moves or the displayed rect is redrawn) and spread
one crossfade over the measured flip period. A game that flips every second
VBlank (R4 races) used to blend for one VBlank and hold for the next; it now
blends across its whole frame.

VBLANK stays the default. reset_mod_owned_presentation() puts it back at
every session start (first boot and the lobby rematch), next to the blend
mode, so titles that never call the setter keep today's code path and a
netplay session never inherits it. begin() is begin_phase(0, 1) and stays
bit-identical (test). gl_interp reports the source, the flip period,
captures and duplicate VBlanks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:02:43 -07:00
Shokunin ec7bc14ad9 GL: Fit to Window stays within the native-wide surface limit
A native-wide surface is g_wide_w*S wide and the driver limit bounds it
(1024 native columns at 16x on a 16384 limit, about 38:9 at a 320-px
display). An uncapped Fit to Window aspect could ask for more; wide_fbo_for
then refused the surface and the frame fell back to a 1x CPU present with
no native-wide margins.

update_adaptive_widescreen now narrows the Fit aspect to the widest the
surface holds (gl_renderer_fit_wide_aspect: psx_gl_max_wide_width at the
context's scale, psx_gl_fit_wide_aspect at the current display width,
gpu_ws_display_width). It changes nothing unless the surface would be
refused. The refusal stays for a fixed aspect on a wide display mode; its
comment now gives the right limit and what the frame falls back to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:52:23 -07:00
Shokunin 84dbc2982a GL: keep side-by-side buffers at full resolution; bound copy staging
Windowed high-resolution mode kept one window over the union of displayed
columns. Two 512-wide buffers side by side need 18432 px at 18x, past a
16384 GPU limit, so the second buffer was refused and every other frame
presented at 1x.

- The window is now up to four tiles, each its own surface over a column
  range. A new display first tries one surface over the union of all
  displays (the old behaviour, so R4's 320-wide window is unchanged), then
  a merge with the tiles it overlaps, then a tile of its own. Every
  mirrored write (queued draws, fills, uploads, copies, the depth24 clear,
  stencil rebuilds) goes to each tile it touches; present, captures,
  screenshot_hires and the native-wide centre read the tile holding the
  display. A display no tile can hold within the GPU limit, the memory
  budget (all tiles together) or the tile cap still presents at 1x, logged.
- A copy into the window stages its S-scaled source in a scratch of its
  own, in column chunks that fit the GPU limit, walked in memmove order so
  an overlapping copy still reads pre-copy pixels. The shared scratch no
  longer grows past the hr surface for it.
- Staging textures commit a new size only after the driver accepted the
  storage; a size past the limit is refused with a log line and the
  texture keeps its old storage (scratch_ensure used to record the size
  before a glTexImage2D that could fail).
- PSX_GL_MAX_DIM lowers the GPU limit the backend plans with (testing a
  layout for a smaller GPU). video_info adds display_x/display_y,
  hires_window_tiles and hires_window_mib.
- gl_scale_invariance_test: side-by-side runs flip two 512-wide buffers
  and require one surface at 5x and two tiles at 9x (PSX_GL_MAX_DIM=8192)
  to equal the full-VRAM surface, including copies between the buffers
  and 1000-column copies; at 18x both buffers must read back at S. A
  scratch request past the limit must be refused and leave the texture
  and its recorded size intact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:52:23 -07:00
Shokunin 2211ecabf3 GL: true 8K past a 16384 texture limit with a high-resolution window
8K is 18x at a 240-line reference, and a full-VRAM surface at 18x is
18432 px wide: over the 16384 limit of Apple's GL, Intel and MoltenVK. A
title only needs its displayed frame at 18x, so past the full-VRAM limit
the GL backend now splits the work:

- The authoritative VRAM stays the ordinary hr surface at 1x, i.e. the
  native renderer unchanged: pack, CPU readback, render-to-texture and VRAM
  copies are exactly the native results (s_hr_scale; s_scale stays the
  presented scale gr_scale reports).
- A presentation-only surface at S covers the displayed columns W =
  [x0, x1) x all 512 rows (R4: 0..319, 5760x9216 at 18x, 405 MiB). Every
  GPU write touching W is mirrored into it: textured and flat batches,
  lines (quads), fills, uploads, the depth24 clear, and VRAM copies (hi to
  hi when the source is in W, otherwise the 1x source upscaled; both staged
  before drawing so overlapping copies read pre-copy pixels). It keeps its
  own stencil. A separate blit program carries the window projection, so
  the canonical shaders are untouched.
- W grows (64-column aligned) the first time a display rectangle outside it
  is presented, seeded by upscaling the 1x content. Present, hold-last,
  interpolation capture, screenshot_hires and the native-wide centre read
  it; a display it cannot hold presents at 1x.

It engages only when the full-VRAM surface cannot hold the request (driver
limit or budget), sized so a 320-wide window fits both; up to 16x on the
M4 nothing changes. PSX_GL_HIRES_WINDOW=0/1 disables or forces it. The
launcher note and video_info (hr_scale, windowed, hires_window_*, hires_fbo)
report it.

Default-off: at 1x and whenever the full-VRAM surface fits, the paths are
the previous ones (same native-VRAM and present digests on a fixed scene at
1x and 2x).

Tests: gl_scale_invariance_test now also forces the window at 2/3/5/9x and
requires its frame at internal resolution to be byte-identical to the
full-VRAM surface at the same scale (copies inside, into and across the
window edge, fills/uploads across it, mask, all blend modes) with native
VRAM identical to 1x, and runs it at 18x; gl_scale_limits_test covers the
window clamp; guards pin that it only engages past the full-VRAM limit.
Verified in R4 on an M4: the 8K preset renders 5760x4320 in a race (rear-
view mirror present) and 5760x8640 on a 480i menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:52:23 -07:00
Shokunin eee07fe910 GL: raise the internal-resolution ceiling to the driver's limits
The OpenGL backend capped supersampling at 4, the software mirror's limit,
although its hr surface lives on the GPU. Raise the compile-time ceiling to
32 (gpu_render.h) and decide the real scale at context init:

- Query GL_MAX_TEXTURE_SIZE, GL_MAX_RENDERBUFFER_SIZE and
  GL_MAX_VIEWPORT_DIMS and clamp S so the 1024*S x 512*S surface fits them
  and a 2 GiB memory budget (PSX_GL_VRAM_BUDGET_MB overrides it). Apple's
  GL on Metal reports 16384, so 16x is the most a full-VRAM surface can be.
  The clamp is logged with the limit that applied.
- If the surface still fails to allocate, retry one scale lower inside GL.
  Only a 1x failure drops the backend to software, as before.
- Draw lines above 1x as quads one native pixel thick covering both
  endpoints. glLineWidth(S) is capped at 1 on core profiles, so every line
  was one hr pixel thick (1/9 of native at 9x).
- Present a supersampled source with an area resolve when it is more than
  1.25x the output (1200 lines into a 1080 window, 480i menus), and inset the
  UV rect by half a texel instead of half a native pixel, which cropped and
  zoomed the image by (S-1)/2 texels per edge.
- Size the copy scratch to the largest copy (copies stage at its origin) and
  rebuild the mask stencil in tiles, only over the primitive bbox union, so a
  large S does not cost another full-size surface.
- Refuse a native-wide surface wider than the driver limit (uncapped Fit at
  a high scale) with one log line; the frame presents 4:3.
- main.cpp: the offline and dual-raster clamps no longer cap GL at the
  software 4x. The CPU present path presents GL frames at 1x (its readout is
  native) and its staging buffer stays sized for at most 4x.
- [video] supersampling accepts 1..32 in game.toml and settings.toml; the
  runtime clamps per backend.

Every change is gated on S > 1: at 1x the renderer is byte-identical (same
native VRAM and presented framebuffer digests as the previous source on a
fixed GP0 scene).

Tests: gl_scale_limits_test (clamp arithmetic), gl_internal_scale_guards
(source guards + runner parsing), and gl_scale_invariance_test, a hidden
real-GL run at 1,2,3,5,9x that requires identical guest-visible VRAM at
every scale, one-native-pixel lines at internal resolution, and a 32x
request that stays on GL at the driver's clamp (16x on Apple).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:52:22 -07:00
Matthew Stanley 73fd940294 Retain streamed tile textures with live palettes and stable scene bounds 2026-09-19 20:02:29 -07:00
Matthew Stanley 9037e7bd23 Add opt-in retained texture banks and enhancement memory snapshots 2026-09-13 16:50:26 -07:00
Aliet Exposito Garcia 72b3ead892 feat(OpenGL renderer): add post presentation gamma control
Some titles and enhancement mods/plugins might require display level gamma correction to improve shadow detail and overall visibility without altering guest rendering state. This introduces a public gamma adjustment interface for the OpenGL renderer via `gl_renderer_set_post_gamma`, that enables external plugins to adjust the display curve cleanly at runtime.

This adjustment applies exclusively to game content and temporal interpolation passes while leaving host interface elements, bezels, and previously composed frames untouched. Modifying the gamma setting immediately invalidates the presentation buffer so that changes become visible on screen without requiring new video memory writes from the guest system.
2026-09-13 01:30:10 -04:00
Kevin 97f58e19d7 runtime: present-time scanline post-process (host display enhancement)
Adds an optional scanline effect to the GL present path: a soft sinusoidal
beam pitched to the PS1 display-line grid (one dark line per native scanline),
applied in both the present and interpolation shaders and gated by output
scale so it never shimmers below ~2x. Orthogonal to the crt_filter colour LUT
(the two compose).

Controls: [video] scanlines / scanline_strength (game.toml + settings.toml),
PSX_SCANLINES / PSX_SCANLINE_STRENGTH env overrides, an F6 hotkey
(HOST_KEYMAP_SCANLINES, rebindable via [KeyMap] Scanlines), and a `scanline`
TCP command for live A/B. Launcher Display-card wiring follows separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 09:53:22 -04:00
Matthew Stanley 88f07bc9e3 runtime: keep temporal blending on the render thread 2026-08-27 13:21:17 -07:00
Matthew Stanley 15099cc5b6 presentation: add opt-in FMV filter setting 2026-08-24 20:28:43 -07:00
tetrisgm c78a8511b7 present: bezel artwork behind the frame ([video] bezel)
A still image drawn behind the game, filling whatever the letterbox or
pillarbox leaves over -- the trick vertical shmups use on horizontal displays,
where the dead space carries artwork so it reads as designed rather than as a
limitation. MAME calls the same idea bezel artwork.

Drawn across the whole drawable before the game quad, which is simpler and
safer than filling two margin strips: no margin arithmetic, it covers pillarbox
and letterbox alike, and the frame paints over the centre.

The reason this is the right shape and present_edge_fill was not: edge fill
stretches one edge COLUMN of the live frame across the margin. That suits a flat
menu backdrop and turns track geometry into horizontal smears, so it needs to
know whether the current frame is a menu -- a question it answers with
force_4_3, which is only ever set by the widescreen path. A still image has no
such question. It looks the same in a race and in a menu, which is the point.

Loaded after the GL context exists (the first attempt sat next to tex_pack_init,
which runs before the window, so g_gl_active was false and it silently never
loaded). Decoded with stb_image, already linked for the HD texture pack. A
relative path resolves against the disc directory, matching the pack convention,
so artwork can sit beside the disc.

Verified on a 7680-wide maximised window with 4:3 content: margins that were
pure black now carry the image on both sides, with the frame untouched in the
middle.

Known limits: the image is stretched to the drawable, so an aspect that does not
match the window will distort (cover-crop is a small follow-up), and this has
only been exercised in 4:3 pillarbox, not with the widescreen mod active.

For WipEout 3 the team wallpapers by MotorcycleEmptiness
(awesome-wipeout.github.io) suit this exactly; the shipped default is a
placeholder gradient used to verify the path, not final art.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 13:33:58 -07:00
Alex Vanderveen 0feb7709f6 rollback opengl render path, ICE patches, resim optimizations 2026-08-03 05:04:48 -04:00
Alex Vanderveen 9769e9fbc2 rollback patches 2026-08-02 00:08:39 -04:00
Alex Vanderveen 92efb5c4fd no mods in MP, minor patches 2026-07-29 02:40:50 -04:00
Alex Vanderveen 070fc805c4 Merge origin/master into feat/ui-5p.
Keep 5p/ICE netplay host + lobby client; take master's cycle batching,
SDL3 defaults, and dropped vendored lib/recomp-ui.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 20:17:14 -04:00
Matthew Stanley 3c67a52f95 opengl: add motion-adaptive interpolation blend 2026-07-28 14:27:40 -07:00
Alex Vanderveen fbad0be1b9 launcher performance, load state performance, game launch performance
it stops checking the entire PSX disc for full hash check every time the launcher runs, assuming the user's hash checked it when selecting the disc the first time and saving it to settings, so it just validates the file name and size instead, then it shares lifecycle with the launcher and the game to load the game faster, and several optimizations were made to loadstate paths which I think in the end relied on reanchoring host timings to the restored clock that came with the state
2026-07-27 11:03:10 -04:00
Alex Vanderveen 31015ceab1 feat: launcher polish and recomp-net lobby netplay
Wire MotK-style lobby (recomp-net submodule, WS client, match_caps),
netplay delay-sync, and launcher UX so host sim settings stay aligned
across peers.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 21:37:57 -07:00
Matthew Stanley d7284bc4d4 build: declare GL wide-fast control 2026-07-14 23:04:55 -07:00
Matthew Stanley fb585541ec opengl: sustain full-rate gameplay and FMV presentation 2026-07-11 14:07:17 -07:00
Matthew Stanley e9b4ed5bbf gl: frame_perf CPU-side attribution - flush/wide wall time + plumbing counters
Tomba2 16:9 GL reproduced the reported collapse (12-14fps) and the GPU-side
split EXONERATED the GPU: scene_gpu ~10ms, mirror ~0.2ms, while emu_cpu is
~82ms (vs ~15ms in squash). gl_ws_ablate=1 (mirror passes fully off) leaves
it at ~75ms, so the wedge is CPU work outside the mirror draws. The GL
submission paths run INSIDE guest emulation (GP0/DMA processing), so driver
CPU cost lands in emu_cpu where no GPU timer sees it.

frame_perf now reports, per wide/4:3 frame class: CPU wall inside
flush_tex_batch (cpu_flush_ms_avg), CPU wall inside the glb_wide_* entry
points (cpu_wide_ms_avg), batches/frame, wide target sets/frame, and wide
FBO creations/frame (a nonzero steady value = surface recreation churn).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 20:23:06 -07:00
Matthew Stanley f5362f4ce9 gl: frame_perf mirror-pass GPU split + gl_ws_ablate ablation toggles
Native-wide perf attribution instrumentation (permanent):
- Each wide-mirror pass (tex-batch mirror, flat-geometry mirror, overlay
  full-width rect, wide_clear) is bracketed with a GL_TIMESTAMP pair
  (glQueryCounter; TIME_ELAPSED cannot nest inside the frame scene query).
  Pairs pool per buffered frame (1024/frame), summed at readback into
  mirror_gpu_ms + mirror_passes per sample; frame_perf now reports
  mirror_gpu_ms_avg/max, canon_gpu_ms_avg (scene minus mirror),
  mirror_passes_avg and mirror_pass_us for wide frames.
- New debug cmd gl_ws_ablate mode=<0..3>: 0 normal, 1 skip mirror passes
  entirely, 2 mirror state churn without draws, 3 mirror draws stay on the
  hr FBO (no per-pass FBO rebind; content-corrupting perf probe).

First MMX6 16:9 measurements with the split: mirror passes cost 20-125us
EACH (FBO ping-pong penalty per pass) but MMX6 2D batches to ~3-15
passes/frame so 16:9 attract holds ~60fps on this branch; the per-pass
cost x ~900 immediate flat prims is exactly the Tomba2 3D 60ms scene.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 19:59:53 -07:00
Matthew Stanley 2d2f4aebb4 gl: fix black-frame flicker (ISSUES #7) — exact pending-upload rects, not a union
Root cause (proven via the new present ring + gl_coh_ring correlation):
flush_cpu_upload() merged all pending CPU->VRAM writes into ONE union
bounding box. A frame with two disjoint uploads (sprite column + tile row —
routine MMX6 texture streaming) produced a union spanning the display
framebuffers, which the flush painted from the STALE CPU VRAM mirror (the
FBO is authoritative under GL) — stomping live frames with black for
exactly two presents (one per double-buffer parity). Scene-dependence of
the uploads made it look config/timing dependent; the software renderer is
immune because its CPU array is authoritative.

Fix: exact pending-rect list (16 rects; merges only when zero uncovered
pixels are added — containment or same-band extension; wrap-aware GP0(A0)
transfers split into up to 4 exact rects instead of the old "wrapped: take
all" full-VRAM union; overflow -> order-preserving flush-all; pre-context
union retained where the CPU array is authoritative). Merge rule verified
by a 20k-randomized-rect host test (0 stale / 0 missing painted pixels).

Observability (always-on): gl_present_ring records every SwapWindow (path,
src/letterbox rects, drained glGetError, wall-ms, backbuffer + blit-source
pixel samples) — the instrument that gave the 1:1 black-capture
correlation. debug_server_send_fmt now heap-formats responses >64KB (they
previously truncated into unparseable JSON, breaking large ring dumps).
(debug_server.c also carries the Vulkan screenshot_file sync hunk from the
concurrent VK bring-up; see the next commit.)

Validated: ~18-min MMX6 GL attract soak, ~1600 window captures across 3+
attract cycles — zero isolated black frames, zero GL errors (pre-fix:
19/40 captures black at the intro). Tomba1 build-gl boots clean; its full
attract soak follows. ENHANCEMENTS.md added (framework enhancement ledger,
R1 OpenGL / R2 Vulkan state).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 16:26:48 -07:00
Matthew Stanley 0ac63b655c vulkan: 3rd renderer backend (WIP) — emitter, GPU-direct present, full-frame readback
Vulkan backend alongside software + OpenGL: geometry/textured-prim
pipelines, GPU-authoritative VRAM image, deferred-submit + drain-at-present
perf model, GPU-direct present (vk_renderer_present_vram), and a
full-VRAM-pack readback path (ensure_cpu rect_adds the whole VRAM rect
before pack_flush so screenshots/guest VRAM reads reflect live content,
not just the incremental pack-dirty rect — readback-only, no per-frame cost).

Runtime plumbing: --debug-port / --renderer / --window-title CLI overrides
(multi-instance fleet), latency_ring, SPIR-V shaders + embed_spirv.py,
config_loader renderer selection, debug_server screenshot wiring.

All on feat/vulkan-renderer; snapshot in backup/vulkan-wip-20260617.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 14:56:12 -07:00
Matthew Stanley 8d58b494b2 frame_perf: textured-vs-flat primitive split (tex_frac)
Measured Tomba village = 93% textured prims, so draw batching must target the
textured path (per-prim uv-limits/texpage/clut/depth move to vertex attributes
so consecutive textured prims coalesce into one draw). Flat batching alone
would touch only 7% of prims.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 08:33:10 -07:00
Matthew Stanley 1964447438 frame_perf: per-frame scene primitive count + per-draw time
Adds prims/frame and per_prim_us to the frame_perf aggregate. Proved Tomba's
village is draw-call bound: ~1000 prims/frame at ~14us of GPU time PER
glDrawArrays (a 1-triangle draw should be <1us) — pipeline-flush overhead from
re-binding state between every primitive. 16:9 is 2x slower purely because the
native-wide double-draw issues 2x the draw calls. Points squarely at draw
batching (group consecutive same-state prims) as the fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 01:06:20 -07:00
Matthew Stanley 1e3244db4a frame_perf: per-frame GPU/CPU phase timing (GL timer queries)
Always-on per-frame ring in the GL renderer: two GL_TIME_ELAPSED queries
bracket each frame's scene draws vs the present clear+blit (true GPU time,
independent of CPU/GPU overlap), plus SDL perf-counter CPU wall time
(present-to-present total, present call). Exposed via the debug server
"frame_perf" command, aggregated over the recent ring and split by 4:3 vs
native-wide so a 16:9-vs-4:3 A/B reads off one query.

Built to answer where a frame goes without guessing. First use proved
Tomba's 16:9 village lag is the native-wide DOUBLE-DRAW (every primitive
re-issued into the wide FBO), NOT supersampling (SS2==SS1) and NOT the
compositor (present_gpu ~1ms): scene_gpu 16:9=28ms vs 4:3=17ms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 01:00:23 -07:00
Matthew Stanley fff384da0c perf(GL widescreen): GPU-direct wide present (drop the per-frame readback)
The GL native-wide present (Option B) did glFinish + glReadPixels of the whole
wide FBO to the CPU and re-uploaded it EVERY frame — a hard GPU pipeline stall +
round-trip that made OpenGL 16:9 run far slower than the software renderer (whose
wide path is pure CPU, no GPU sync). Confirmed by isolation: GL 4:3 smooth, GL
16:9 slow, independent of SS/AA.

Add gl_renderer_present_wide_fbo: blit the displayed buffer's wide FBO straight to
the window with glBlitFramebuffer (GPU-side, V-flipped, into the letterbox),
exactly like the canonical gl_renderer_present_vram — no readback, no glFinish,
no re-upload. main.cpp routes wide GL frames here (falling back to the CPU readout
only if the wide surface doesn't exist yet). USER-VALIDATED smooth. The CPU
readback path (glb_render_wide_display) stays as the fallback + the wide_shot
probe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 21:35:32 -07:00
Matthew Stanley 265183872d widescreen: FMV 4:3 pillarbox + menu-2D coherence + HUD edge anchoring
- FMV: 24-bit frames AND streamed 15-bit MDEC video (Tomba plays its
  movies 15-bit; depth24 alone missed them — user-reported) present
  pinned to native 4:3. Detector = colour MDEC decode within the last
  30 vblanks (mdec_recently_active); texture-decompression luma
  decodes do not stamp. Both GL present paths take force_4_3; SW path
  centers a 4:3 dst rect. User-verified on the intro movies.
- Menu garble: full-2D screens (no tagged prims in flight for 2
  frames) now squash lines, polylines, flat rects and mono sprites
  too — dialog borders are drawn with these and stuck out past the
  squashed SPRT boxes. Full-width TILEs (fades) stay exempt; in-game
  non-textured prims stay untouched (may be world-anchored).
- HUD positioning: in-game untagged SPRTs pivot by thirds — outer-
  third elements anchor to their screen edge (16:9 corner position,
  native proportions), middle third to centre. Full-2D screens keep
  the single centre pivot so composites stay coherent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 12:52:15 -07:00