CRT Filter, Image Filtering, Enhanced Audio and Rewind become default-off mods (netplay = host_only,
outside the plan fingerprint); Internal resolution gains Auto in place of dynamic resolution.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PSX_DEBUG_TOOLS builds only (PSX_DEBUG_TOGGLES=0 turns them off). Keys flip one
rendering feature while the game runs and show the state on the OSD: 1 scale,
2 dynamic resolution, 3 widescreen, 4 Smooth motion, 5 texture filter,
6 wide mirror, 7 geometry correction, 8 perspective texturing, 0 summary,
9 PGXP depth, F10 PGXP colour, F11 PGXP seam, F12 PGXP CPU mode. The
debug_key TCP command presses one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `wide` (mode 2) is half a native px above 1x and nothing at 1x, as the
docs and `fine` already said.
- Expansion needs the triangle's depth mode 1 (opaque, 3D, depth buffer on,
not near-camera); without depth, painter order resolves the overlap and a
widened edge would draw over its neighbour.
- gl_renderer_set_pgxp_seam syncs with a live render thread (GL_RT_SYNC)
and counts as a PGXP renderer feature.
- gl_frame_gen_test: seam alone draws exactly like no features; at 1x
depth+seam draws like depth; at 3x it changes the image; the in-between
frame at phase 1 still equals the real one with seam on.
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>
gl_renderer_set_pgxp_color_perspective syncs with a live render thread
(GL_RT_SYNC) and counts as a PGXP renderer feature for the per-triangle
depth command. gl_frame_gen_test's PGXP pass runs depth, colour and both:
the scene's SZ now differs per vertex (colour) with rows ordered far to
the right (depth), and the in-between frame at phase 1 must equal the real
frame with each.
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>
Triangles with a vertex nearer than SZ 1024 (PSX_PGXP_DEPTH_NEAR) stay out of
the depth buffer, the tolerance gains an absolute 48 SZ, and every displayed
frame starts with a cleared depth buffer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 3D batch is two passes: colour tests against depth pulled toward the
camera by 2% of the distance (PSX_PGXP_DEPTH_TOL) and writes none; a
colourless pass then writes the true depth. Lane markings drawn after the
road a hair off its plane stay whole.
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>
Fixes Ape Escape terrain seams: CPU-built subdivision midpoints and lwl/lwr-copied
grid corners drew native next to precise neighbours. Adds pgxp_tri_ring/pgxp_store_ring.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Master moved the textured-dot test stubs into gpu_exec_stubs.inc; the PGXP stubs move there too. pgxp_note_rect_bypass stub takes (int). Stamps recomputed.
Many PS1 renderers drop a primitive whose depth index falls past the end
of their ordering table. Raising that limit can write past the table, so
a title can now list such guards in game.toml with the register that
carries the index and the last safe value. While a trusted mod has called
psx_mod_set_draw_distance_clamp(1), generated main-EXE code clamps that
register before the guard runs, so far geometry is kept in the farthest
slot instead of popping in later.
- Off by default and reset at every session start, so netplay and every
title that lists no sites run unchanged; with no sites the generated
code and declarations are byte-identical.
- Main executable only: the full instruction word is the guard,
generation fails on a mismatch or a load-delay hazard, captured
overlay code keeps its own code (the sites are not part of the overlay
config identity), and the dirty-RAM interpreter applies the clamp
where the game's text image holds the listed word.
- Compatibility: code_generator.cpp is a codegen-hash input, so the
codegen hash and the overlay-cache tag change for every title, sites
or not. Savestates from earlier builds are refused, and overlay shards
rebuild once on first visit. Generated code and behaviour are
unchanged for a title without sites or with the switch off.
- Tests: draw_distance_codegen_test (config rules, emission, guards,
declarations, overlay identity) and draw_distance_interp_test (store,
switch, interpreted guards); the session-reset guard covers the switch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
G1.11: the hook-flavor runtime, the mod armed at session start, dataflow
only, exact projection, and the rectangle shortcut. It covers what tri_mixed
can and cannot see, and the R4 census after every change:
- 1P: 99.91% dataflow, 0 of 389k drawn triangles mixed;
- menus, attract and 2P VS: every truncation reject is a vertex at the
saturation limit.
The wobble table drops the preserve-projection row, which is 0 by
construction.
G1.12: precise culling (the culled-row crack, the rule, why it is mod-only,
packet headroom, identity, cost under R4's default stack) and
[video] pgxp_mod_only.
TCP_COMMANDS: the new geom_correction fields and the pgxp verb's culling
knob. MOD_PACKAGES: the session arming takes the request; how a title
ships PGXP on. GAME_PROJECT_SETUP: pointer to pgxp_mod_only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Mirroring every batch into the window as it was flushed switched
framebuffers twice per batch. On Apple's GL (Metal underneath) each switch
ends a render pass, about 80 us: 10 ms of CPU per R4 frame at any window
scale, and the 8K preset ran at 21 fps (frame_perf total 47 ms,
cpu_flush 17.4 ms).
The window's copies of textured, flat and line draws are now queued
(vertices plus the state they were drawn with: blend key, set-mask,
mask-check, texture window, filter, sampled texture, draw area) and
replayed into the window in one pass at the next sync point: before the
raw mirror changes (pack, upload, depth24 clear), before any immediate
write to the window (fill, copy, upload, stencil rebuild), and before every
read of it (present, capture, growth, hires readback). Order within the
window is kept, so it equals an immediate mirror wherever it is observed.
tex_batch_draw_passes and mask_stencil take their mask state as
parameters (tex_draw_passes_ex, mask_stencil_ex) so the replay uses the
recorded state; the canonical pass calls them with the live state, the
same GL call sequence as before (native and 2x/4x digests unchanged).
Measured on an M4 in a race: 8K total 16.68 ms per present (60 Hz
cadence), cpu_flush 0.62 ms; forced window at 4K likewise. The invariance
test still finds the window's frame byte-identical to the full-VRAM
surface at 2/3/5/9x.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Add a player-facing internal resolution on top of the raised GL ceiling.
A preset is a target height; the runtime renders at the integer scale
S = ceil(target / reference_lines), reference 240 by default: 720p 3x,
1080p 5x (area-resolved to 1080), 1440p 6x, 4K 9x, 5K 12x, 8K 18x. The
backend then clamps S to what it can allocate.
- runtime/include/internal_resolution.h: the preset table, stable ids,
the value encoding shared with recomp-ui (0 unset, 1 native, -1 match
display, N target lines) and the pure resolver.
- config: [video] internal_resolution (a preset id or a number of lines)
and resolution_reference_lines in game.toml; internal_resolution in
settings.toml, written as a stable id beside supersampling = min(S, 4)
for older runtimes. Precedence: game default < player's legacy factor <
player's preset. settings.toml window_width now accepts up to 7680.
- main.cpp: resolve the preset before gpu_init; Match display re-resolves
from the game window's monitor (SDL3 mode height x pixel density) before
the GL context allocates. The launcher gets the vocabulary (with a
synthesized "Nx (L lines)" entry for a legacy factor no preset matches)
behind RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION, on first boot and on the
netplay soft-return.
- The OpenGL game window gets SDL_WINDOW_HIGH_PIXEL_DENSITY (SDL2:
ALLOW_HIGHDPI) only when the player chose more than native, so the
drawable is in pixels on Retina; native keeps the window as it was.
- debug server: video_info (preset, requested and effective scale, GL
limit and clamp, hr surface, window points vs pixels), screenshot_hires
reads the GL hr FBO at full size, screenshot_wide_hires captures the
native-wide band at internal resolution.
- docs: config_schema (keys, precedence), NETPLAY (a per-peer presentation
setting), TCP_COMMANDS (index regenerated), ENHANCEMENTS IR1.
Default-off: with no internal_resolution anywhere the scale, the window
flags and the present path are unchanged.
Tests: internal_resolution_test (presets x reference 240/480 x backend
maxima, legacy mapping round trip), video_enhancement_settings_test (game
and settings keys, stable-id round trip, legacy key still written),
gl_internal_scale_guards (HiDPI only when opted in, launcher fields behind
the capability macro).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The repository root carried seven markdown files that were not onboarding
material, plus two that had been superseded by the code they described. This
sorts them: reader-facing docs to docs/, working notes to docs/internal/, and
the two superseded ones merged into the docs that replaced them.
Moved to docs/: WIDESCREEN.md, ENHANCEMENTS.md, TCP_COMMANDS.md
Moved to docs/internal/: PRINCIPLES.md, ISSUES.md, DEBUG.md, GTE_LM_FIX.md,
and from docs/: bios-hardening.md, overlay-status.md,
overlay-plan.md, overlay-recompilation-design.md,
overlay-discovery.md, TOMBA2_EXC_CONSULT_RESPONSE.md,
CASE_A_AOT_GAP.md, audit_inventory.md
Moved to docs/internal/upstream/: UPSTREAM_2026-08-20.md
The root keeps only what belongs there: README.md, CONTRIBUTING.md, LICENSE,
CLAUDE.md, THIRD_PARTY_ATTRIBUTION.md, RELEASE_NOTES.md, the dotfiles and
psxrecomp_cli.py.
MERGES
SPLITGEN_MIGRATION.md folded into docs/BUILDING.md as "Split generated C". Its
migration instructions were stale in a way that mattered: it told every game
repo to hand-edit its CMakeLists and add a file(GLOB ...) over
<serial>_full*.c, but runtime/runtime.cmake now does that itself. GEN_FULL_GLOB
is a multi-value argument of psxrecomp_add_game_runtime(); the loop at
runtime.cmake:1985 makes each pattern absolute against CMAKE_CURRENT_SOURCE_DIR,
expands it with file(GLOB), and concatenates the hits into
GAME_GENERATED_FULL_C. So a title names the pattern and gets however many shards
a regeneration produced. The rewritten section describes that, the two-step
fallback when the globs match nothing (GEN_FULL_FALLBACK, else <serial>_full.c
derived from the dispatch marker by substituting _dispatch.c), and the one
caveat the code actually has: that file(GLOB) is plain, not CONFIGURE_DEPENDS,
so a regeneration which changes the shard COUNT needs a re-configure to be seen.
psxrecomp-toml-readme.md folded into docs/GAME_PROJECT_SETUP.md as "EXE-only
autofill (psxrecomp-toml)", placed after "Disc autofill (probe_disc.py)" and
before "Project Studio" so the two autofill paths sit together. Its documented
flags were checked against the usage text in recompiler/src/main_toml.cpp:28-42
and against the argument parser at :150-165 -- all seven options match with no
drift, so the flag list is reproduced as-is. Added a pointer that probe_disc.py
is preferable when a cue exists, since that tool does the same EXE-header work
plus disc identity and netplay gates.
FUNCTIONAL FIXES
tools/gen_tcp_commands.py wrote to REPO/"TCP_COMMANDS.md". After the move that
path does not exist, so the generator would have created a new stray root file
and --check would have failed against it forever. Retargeted to
REPO/"docs"/"TCP_COMMANDS.md", along with the seven docstring and message
strings that name the path.
Running it surfaced two further problems, both fixed here:
* The doc was genuinely stale -- pre-existing drift, not introduced by this
commit (the moved file is byte-identical to origin/master, and no
debug-server command table is touched here). Four commands registered since
the last regeneration were missing from the index -- cdrom_timing_dump,
savestate_status, ws_ui_groups, xprobe_watch -- and scanline had gained a
backend. Regenerated: 307 -> 311 commands, and the change is confined to the
AUTOGEN block (the hand-written head and tail are byte-identical to master).
tools/gen_tcp_commands.py --check now exits 0.
* DOC.write_text() has no newline argument, so Python translated every newline
to os.linesep. On Windows that rewrote all 585 lines of the file to CRLF and
turned a four-line index update into a whole-file diff -- measured: 589 CRLF
/ 0 LF after a run, against 0 CRLF / 585 LF in the repo. Now written through
an explicit newline="" handle, verified to leave the file at 0 CRLF.
runtime/include/debug_server.h:9 and runtime/src/debug_server.c:8 cited "TCP.md
and DEBUG.md". TCP.md has never existed in this repo under that name; both now
name docs/TCP_COMMANDS.md and docs/internal/DEBUG.md.
LINK AND REFERENCE FIXES
Every inbound reference to a moved file was located by grepping each basename
across the whole tracked tree, not just the markdown. Markdown hrefs in
README.md, CONTRIBUTING.md, docs/ARCHITECTURE.md, docs/COMPILING_OVERLAYS.md,
docs/AOT_OVERLAY_PLAN.md, docs/RELOCATION_MANIFEST_FORMAT.md,
docs/config_schema.md, docs/OVERLAY_XREF.md, docs/TCP_COMMANDS.md and several
docs/internal/ files now point at the new locations; where two files became
siblings under docs/internal/ the path was shortened rather than lengthened
(bios-hardening.md's reference to overlay-discovery.md, for instance).
docs/COMPILING_OVERLAYS.md's "in this folder" phrasing was wrong once two of the
three files it listed moved, so it now names internal/ explicitly.
24 source-comment mentions of ENHANCEMENTS.md sections G1.x across 19 files
(recompiler, runtime, cmake) were retargeted to docs/ENHANCEMENTS.md with the
section numbers untouched. docs/SHADOW_ENHANCEMENTS.md is a DIFFERENT file and
was deliberately excluded.
20 rationale strings citing docs/CASE_A_AOT_GAP.md across the four
recompiler/seeds/*.json files were updated. Done as a byte-level substring
replacement rather than a JSON round-trip: two of those files are stored with
CRLF endings and two with LF, and re-serializing them reformatted 5,700 lines.
The byte-level version changes exactly 20 lines and each file still parses.
References that look similar but point elsewhere were deliberately left alone,
and are worth naming so a future pass does not "fix" them:
* "recomp-template PRINCIPLES.md" / "NES recomp template PRINCIPLES.md" in
CLAUDE.md:43, docs/SHADOW_ENHANCEMENTS.md:28,
docs/internal/HLE_SCHEDULER_CARVEOUT_PLAN.md:5,
runtime/src/debug_server.c:4934 and tools/parity_diff.py:8 are a different
repository's file. Only the five that mean this repo's PRINCIPLES.md were
updated.
* Every remaining bare ISSUES.md mention is a GAME repo's tracker
(MegaManX6Recomp 2/7/8/9, TombaRecomp 6), not this repo's.
* docs/disc-speed.md:4,54,56 and docs/internal/bios-hardening.md:5 name
overlay-discovery as a BRANCH, not a file, so no path applies.
INDEXES
README.md's "New here?" table gains rows for docs/WIDESCREEN.md,
docs/ENHANCEMENTS.md and docs/TCP_COMMANDS.md. docs/internal/README.md gains
sections indexing all fourteen documents that moved in, plus an "Upstream
contributor reviews" section describing upstream/ and the new sweep file, plus a
"Removed" bullet for CYCLE_TIMING_ARCH.md (deleted in the previous commit)
pointing at FAITHFUL_TIMING_PLAN.md, CYCLE_MODEL_BEETLE.md and
PRECISE_IRQ_SLICE.md -- following the SLJIT_PERSIST_CACHE.md precedent already
in that file.
docs/ENHANCEMENTS.md:7 claimed framework bugs live in "game repos' ISSUES.md
until a framework tracker exists". A framework tracker does exist; it now says
so and names the Beads epic.
VERIFICATION
A link checker was written to scan every tracked *.md for relative links,
resolving each against the tree being scanned. Baseline at origin/master: 129
files, 137 relative links, 3 broken. After both commits: 127 files, 159 relative
links, 3 broken -- the SAME three, all pre-existing and untouched here:
docs/DATA_SHARDS.md -> INSTANT_LOADS_PARKED.md (never existed)
docs/DATA_SHARDS.md -> INSTANT_LOADS_SPIKE2.md (never existed)
docs/NETPLAY.md -> ../lib/recomp-net/README.md (inside a submodule)
Zero new broken links. The checker's own coverage was checked against a raw grep
for markdown link openers: the 3-link difference is C++ lambdas inside code
blocks, correctly not treated as links.
Also verified: py_compile over all 113 tracked tools/*.py plus psxrecomp_cli.py;
all four seed JSONs still parse; the test-registration guard still reports 123
test files all registered; recompiler/tests/test_coverage_vault.py still passes;
and no file in this commit had its line endings flipped (checked mechanically
across all 50 modified files, after two files were caught and redone).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>