Commit Graph

77 Commits

Author SHA1 Message Date
Matthew Stanley 1dd2afec07 Merge master and enable PS1 blend correction by default 2026-10-09 23:55:29 -07:00
Matt Stanley eb4c76fc47 Merge pull request #586 from RetroPortingToolKit/feat/gl-xbr-filter
GL: opt-in xBR edge-directed texture filter (texture_filtering = "xbr")
2026-10-09 23:19:31 -07:00
Shokunin 70c735944b GL accurate blending: unorm8-exact blend constants (NVIDIA), regen OpenBIOS stamp
NVIDIA quantises the constant blend colour to the 8-bit target (0.5 ->
127/255), putting B/2+F/2 and B+F/4 one step under a k*8 boundary, so
gl_blend_accuracy_test failed there (2016 mismatches). With accurate
blending on, use 128/255 and 64/255: exact in unorm8, and the small upward
bias stays inside the PS1 truncation bucket for every 5-bit operand, so
fixed-point and float blenders agree. Passes on Apple M-series GL and an
RTX 4080 SUPER (NVIDIA 610.88). Default path unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 19:18:36 -07:00
Shokunin 0e3c53720b GL: opt-in accurate blending + PS1 blend/mask accuracy test
Verifies the four PS1 semi-transparency modes, set-mask, mask check and
texel STP at 4x internal resolution with PGXP depth on and off, directly
and on the render thread, against the PS1 formulas and the software
rasterizer (new gl_blend_accuracy_test).

Finding: with the historical surface bases (fills and texels stored as
k*255/31, everything else as k*8) single-layer blends are off by one 5-bit
step in places (1312 of the test's checked pixels, e.g. B+F/4 over a fill).
[video] accurate_blending = true stores every VRAM write as k*8 so GL's
8-bit blend truncated by the readback is exactly the PS1 arithmetic; whites
of fills/texels then present as 248 like uploads already do. Default off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 19:18:16 -07:00
Shokunin b55d92124d GL: opt-in xBR edge-directed texture filter (texture_filtering = "xbr")
Filter mode 3 magnifies textures with an edge-directed rule after Hyllian's
xBR (written from the published description, no code reused): for the
texel corner nearest a sample, weighted YUV colour distances over a 5x5
neighbourhood decide whether an edge runs along the corner's diagonal; if
so the corner is cut with an anti-aliased boundary and takes the closer
side neighbour. Flat areas, straight edges, gradients, cutouts and 1:1 draws
stay nearest. Selectable from [video] texture_filtering, settings.toml,
PSX_TEXTURE_FILTER and the new texture_filter TCP command. Default unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 19:17:27 -07:00
Shokunin ee56f79087 Mod API: title-supplied host rumble (psx_mod_set_host_rumble)
Some titles compute vibration for every pad but only send it over SIO to a
DualShock (R4 with a presented NeGcon). A trusted mod can now hand the runtime
DualShock motor values per seat; the host pad gets the louder of that and the
guest's SIO motors. Values lapse 8 VBlanks after the last call and are cleared
at every session reset, so nothing can stick on. Opt-in: no title that does
not call it changes. PSX_RUMBLE_TRACE now also logs headless.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 23:25:25 -07:00
Matthew Stanley 50747ae16d Merge PR #585: GL opt-in texture LOD + anisotropic filtering
# Conflicts:
#	docs/TCP_COMMANDS.md
#	docs/config_schema.md
#	generated/OpenBIOS.emitter.sha
#	recompiler/src/config_loader.cpp
#	recompiler/src/config_loader.h
#	runtime/CMakeLists.txt
#	runtime/src/debug_server.c
#	runtime/src/main.cpp
2026-10-08 23:04:04 -07:00
Shokunin ab13278bdf GL: opt-in FMV chroma smoothing for 24-bit MDEC frames
[video] fmv_chroma_smoothing = true makes the OpenGL present of a 24-bit
(MDEC) frame keep each sample's luma and take its chroma from a [1 2 1]
tent over the 3x3 source pixels, hiding the 2x2 colour blocks of 4:2:0
video without softening luma. Pairs with the existing fmv_filter upscale.
Guest VRAM/MDEC output untouched; 15-bit presents and the default unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:37:27 -07:00
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
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 c061ea23ea docs: measured FXAA cost, HUD note
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 02:40:10 -07:00
Shokunin a090a4b83f docs: antialiasing_mode, supersample and the post_aa command
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 02:40:10 -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
Shokunin 46f2abebcf Docs: render thread design, sync points, interoperation, tests
docs/RENDER_THREAD.md: the record/replay model, why guest-visible VRAM
stays gpu.c's array (private upload source on the render thread), the
guest state captured at record time, every sync point with file:line,
the frame bound and present skipping, interoperation with savestates,
netplay, render passes, interpolation, widescreen, resolution changes,
screenshots, headless and Vulkan, what is out of scope, and the tests.
TCP_COMMANDS.md documents render_thread (index regenerated);
config_schema.md documents [video] render_thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 12:49:53 -07:00
Shokunin 812465df58 Let a title block local Rewind (psx_mod_set_rewind_blocked)
A title calls psx_mod_set_rewind_blocked(1) while it is in a mode that must
not be rewound, such as local split-screen multiplayer, and 0 when it ends.
While blocked, opening Rewind is refused with an OSD note, no history is
captured (the ring keeps what it has; a capture already due is dropped and an
open panel closes unloaded), and a title-allowed direct Rewind shortcut is not
in force: its button is not claimed and reaches the guest pad / title
transform as if Rewind were off. Netplay already refuses Rewind on its own.
The block clears at every mod/session start, so a title that never calls it
is unchanged.

The direct-allowance decision (allowed, and for Rewind: enabled and not
blocked) is now one pure helper, psx_hotkey_direct_active, used for both the
claim and the shortcut press, so an unclaimed Rewind button is never a
one-button shortcut (with Rewind off it falls back to Select + button rather
than popping the "Rewind is off" note on every press).

Tests: psx_hotkey_pad_test covers the allowance and a block taking effect
mid-press; rewind_toggle_combo_test guards the capture/open gates and reset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:44:18 -07:00
Matthew Stanley 6674024285 Merge master into feat/medievil-native-wide-guards
Reconciled with #470 (PGXP): gpu.c precise vertices take PGXP only under geometry correction and count it, then native-wide edge recovery; the PGXP triangle stat stays PGXP-only. Stubs and tests from both kept; stamps recomputed.
2026-10-02 20:00:23 -07:00
Matthew Stanley 30a4072c25 Merge master into feat/max-detail
Stamps recomputed with tools/bios_emitter_fingerprint.cmake after #480's restamp.
2026-10-02 19:22:24 -07:00
Matt Stanley 921d0b021d Merge pull request #467 from RetroPortingToolKit/feat/vs-split
GL: texture window per vertex; opt-in batching across texture windows
2026-10-02 19:20:52 -07:00
Matt Stanley 0347edd577 Merge pull request #455 from Alexbeav/up/settings-paths-relative
runtime: keep a portable game folder portable (relative settings paths)
2026-10-02 19:20:44 -07:00
Matthew Stanley c29e934308 Add guarded packed-coordinate culling for native-wide adapters 2026-10-02 14:12:20 -07:00
Shokunin dd5fdec6bb Add opt-in draw-distance clamps ([[draw_distance.clamp]])
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>
2026-10-01 11:36:48 -07: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
Alexandros Mandravillis 9057a5bc17 runtime: resolve persisted relative paths beside the executable
settings.toml ([disc] path, [bios] path, [memcard] dir/card1/card2) and
the disc.cfg / bios.cfg sidecars can hold relative paths, for example when
a player edits them by hand or a game folder is moved as a whole. The
runtime resolved them against the working directory. A game started from
a shortcut whose "Start in" differs, or from a frontend, then looked for
its disc, BIOS and memory cards in that directory: saves appeared missing
and new cards were created there.

Resolve every persisted path against the executable directory through two
adapters on host_resolve, the same anchor the bezel artwork path uses:
- resolve_persisted_disc_path() for [disc] path and disc.cfg;
- anchor_on_exe_dir() for bios.cfg, [bios] path, [memcard] dir, card1 and
  card2, and the card paths the launcher hands back.
Absolute and UNC paths are unchanged, empty values stay empty, and a
relative --disc on the command line still resolves from the caller's
working directory.

persisted_settings_paths_test compiles the real adapters from main.cpp
into a probe, runs it from an unrelated working directory, and checks that
every persisted-path consumer in main.cpp uses them.
2026-09-30 22:24:10 +03: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
Matthew Stanley 2bf4739f44 Merge master into feat/internal-resolution-settings: regenerate TCP_COMMANDS.md
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 11:22:53 -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 c7ae3c86b2 Guard bltz_sites and branch_keep_sites in the main EXE
docs/WIDESCREEN.md said every explicit screen-X cull kind rejects a wrong
opcode at build time, but only slti, slti_lower, bgez and clip-edge sites
did. A bltz_sites entry was used only when the word was bltz, and
branch_keep_sites wrapped whatever branch was there, so a mistyped address
generated silently and the edge stayed narrow.

One check, CodeGenerator::check_explicit_branch_site, now covers the three
branch kinds in the main EXE: bltz_sites needs bltz (REGIMM rt=0),
bgez_sites bgez (REGIMM rt=1), branch_keep_sites a conditional branch
(REGIMM, beq, bne, blez, bgtz). It runs in generate_branch_condition, in
translate_instruction (non-branch words) and at the block exit, which also
catches a jump at a listed address (jumps never reached either guard, for
bgez_sites too). psxrecomp-game prints the site and exits 1; overlay
variants stay vanilla as before.

ws_cull_edge_codegen_test and recompiler_error_exit cover each kind on a
wrong opcode, a non-branch and a jump, valid sites, and overlay variants;
both fail against the previous emitter. The codegen test also pins that a
matching overlay instruction emits the helpers (the preamble defines them).
Docs list the required instruction per kind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:00:17 -07:00
Shokunin bb2864bc60 Settings: an older launcher's Supersampling pick wins over a preset
A host built against a recomp-ui without the Internal resolution row
(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION undefined) still seeded
internal_resolution on first boot and applied it after the launcher.
From a fresh start the seed was "native", so a Supersampling pick of 3x
was reset to 1x and "native" was saved; on the netplay soft-return the
sticky preset was re-applied at session_reboot. The legacy row stopped
working.

Both launcher exits now go through two pure helpers in
internal_resolution.h:
- psx_ir_launcher_seed_supersampling: with the row, the factor as before;
  without it, the factor itself when no preset is configured (the
  historical round trip), else the preset's scale capped at the row's 4x.
- psx_ir_adopt_launcher: with the row, the row's preset as before;
  without it, a pick in the Supersampling row drops the preset, and an
  untouched row keeps a preset the player cannot see (game.toml or a
  hand-edited settings.toml). It also returns what settings.toml saves:
  no internal_resolution key unless a preset is kept.

internal_resolution_test drives seed -> launcher -> adopt for both
launcher kinds, first boot and soft-return, against a model of the
legacy row (clamp to 1..4, cycle); keeping the old sticky preset fails
it. gl_internal_scale_guards pins both exits to the helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:52:23 -07:00
Shokunin ecda594996 Settings: never persist the PSX_INTERNAL_RESOLUTION override
The environment override was written into g_video_internal_res, the
configured preset. On a netplay soft-return the launcher was seeded from
it and saved it to settings.toml, so one local peer started with
PSX_INTERNAL_RESOLUTION=8k left 8k in the settings.toml the other peer
shares: the case the override exists for.

Keep the override in g_video_internal_res_env. It still wins for the run
(scale, Match display, HiDPI window, video_info), but the launcher only
shows, and settings.toml only saves, the configured preset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:52:23 -07:00
Shokunin 7d49d4c491 Settings: PSX_INTERNAL_RESOLUTION override for one run
A preset id or a line count in PSX_INTERNAL_RESOLUTION overrides every
config layer, like the other validation env overrides (PSX_SCANLINES,
PSX_GEOMETRY_CORRECTION). Two local netplay peers share one settings.toml,
so this is how one runs at 4K beside another at Native. Also say
"measured when the game window opens" for Match display before the window
exists instead of logging a 1x request.

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 f34f91cc25 Settings: Internal resolution presets (Native ... 8K, Match display)
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>
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
Shokunin b13630128c Document explicit screen-X cull kinds; mark offer/adaptive_view inert
docs/WIDESCREEN.md gains an "Explicit screen-X cull sites" section (slti,
slti_lower, bltz, bgez, clip_edge_x_load, branch_keep: semantics, pairing,
guards, interpreter cap, cache identity) and replaces the stale launcher
"Adaptive" description: offer/offer_ultrawide/adaptive_view are parsed but
inert on PSX, and widescreen belongs to a title's mod catalog through
psx_mod_set_fixed_display_aspect / psx_mod_set_adaptive_display_aspect.
docs/config_schema.md lists the new keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:39:26 -07:00
Matthew Stanley 41e92d8a32 Merge current framework master preserving accepted MMX6 widescreen and performance work 2026-09-19 17:49:10 -07:00
Alexandros Mandravillis d17676a5c7 setup: read .chd through libchdr in the Python disc tools
The runtime mounts a .chd natively and the launcher lists it in the disc
picker, so a player could select one and see a valid disc identity, then have
first-run setup fail: verify-disc, prepare_disc.py and probe_disc.py hash and
parse raw 2352-byte track data, which a CHD stores compressed. Nothing on the
Python side could read it, and telling players to install MAME for chdman is
not an answer.

libchdr is already vendored and compiled for the runtime and the native media
CLI. Build it once more as a shared library in recompiler/ (CMake target chdr,
output next to the emitters, static CRT on Windows so a plain python.exe can
load it) and read the CHD from Python through ctypes. tools/psx_chd.py mirrors
the layout the runtime's iso_reader.cpp uses -- 2448-byte frames, 4-frame
track padding, stored versus virtual pregaps, big-endian audio -- and
reproduces the .bin files a Redump dump of the same disc contains, in either
of Redump's two layouts (one bin per track, or one bin for the disc).

verify-disc hashes those reconstructed track bytes against [prepare_disc]
instead of the container. prepare_disc.py writes the tracks into out_dir in
whichever layout the recorded digests match and then continues down its
normal cue path, so the data track is written once, SBI companions are looked
up next to the CHD, and the receipt names the CHD the player chose.
probe_disc.py extracts to a scratch directory and probes the cue. The CLI
builds target chdr on demand for a kit whose emitters predate it, and falls
back to the extract-it-first message when it cannot.

Verified against two discs from a Redump library converted with chdman: a
single-track MODE2 disc and a four-track disc with stored audio pregaps both
reproduce their catalogued track-1 size, MD5 and SHA-1 exactly; verify-disc
and prepare_disc.py accept the four-track one end to end. Tests build a
synthetic uncompressed v5 CHD around a cooked ISO image, so no retail data is
needed; the decode tests skip with a reason when no libchdr is built.
2026-09-16 11:49:59 +03:00
Matthew Stanley 33e3ed78eb Expand lower activation bounds with matching native and runtime semantics 2026-09-12 14:47:47 -07:00
Matthew Stanley c5390e4265 Merge kernel patch ranges PR #346 for combined validation 2026-09-11 22:53:07 -07:00
Kevin 3cbe6c1cde kernel-bless: publish install-slot ranges so patched BIOS bodies run native
The BIOS exception handler ran interpreted for the life of every process,
1.22 billion instructions and 44.6% of all interpreted work in Breath of
Fire III, even though it is compiled and even though the stub that
unblessed it sat at exactly the address bios/SCPH1001.toml already
declared as an install slot. Two mechanisms existed and did not compose:
the emitter hooked the declared slot PC, while the runtime's bless
verifier memcmp'd the whole body including that slot, so the body was
MISMATCH forever and native code never reached the hook.

Three parts, none of which pays off alone:

1. Publish the declared ranges to the runtime. The emitter emits a
   PsxKernelPatchRange table next to the kernel body table and routes it
   through the backend descriptor; the bless verifier compares a body in
   segments that skip them. CLEAN now means "every byte outside every
   declared range still matches ROM", which is the claim the native code
   actually depends on.

2. Generalise the slot model to what the patches are. The old model
   assumed a 4-word lui/addiu/jalr/nop stub written over ROM zeros and
   fired on `word != 0`. Measured against both images, only one of five
   observed shapes fits: patchers also plant a `jr` over real ROM
   instructions, rewrite an 11-word prologue and insert an `mfc0 Cause`,
   and NOP out a driver routine. A slot is now a range with a length and
   an explicit `resume` (jalr / fallthrough / none), detected by
   comparing every word against its ROM-baked value.

3. Hand back from the interpreter at the range end. Kernel page 0 is
   permanently dirty, because the handler saves registers there on every
   exception, so straight-line interpretation would run the whole
   function after the patch. The emitter registers the resume PC as a
   continuation key and dirty_ram_interp surfaces there.

A PC inside a declared range must never be a native dispatch key: the
compiled bytes there are not what executes. Seven pre-existing
jal-return continuations violated that and wedged the boot at frame 0 in
an infinite dispatch loop (OpenBIOS RAM 0x357C). The emitter now drops
continuation keys inside a range so dispatch misses through to the
interpreter, and refuses to emit if a range covers a function entry,
which would silently remove a callable function instead.

Both profiles declare their measured ranges, and SCPH1001.toml finally
pins [program.image] sha256 to v2.2 (CRC32 37157331) so a v2.0 dump
refuses to generate rather than producing phantom discovery gaps.

Measured on Breath of Fire III (SLPS-00990, Psy-Q SDK 3.70), headless,
matched frames, PSX_KERNEL_PATCH_RANGES=0 for the OFF side:

                      OpenBIOS            SCPH-1001 v2.2
  bless mismatch      21 -> 0             74 -> 0
  kernel bodies       -87.7%/frame        -97.8%/frame
  all interp insns    -42.6%/frame        -94.2%/frame

The residual kernel-body work is exactly declared-words x entries on
every range, so nothing leaks past the hook. The A0/B0/C0 vector
trampolines are unchanged (+0.2%, noise): the profile excludes them by
design and they are a separate job. Card read traces are byte-identical
across the A/B on both images, all 32 entries, which exercises the two
ranges that replace the kernel's card handler.

Rule 18 holds throughout: the guest's patched instructions still execute
as written on the dirty-RAM interpreter. Only the code around them
changes backend. Nothing is synthesised.

Tests: runtime/tests/test_kernel_patch_ranges.c (new, the verifier's
accept/reject behaviour and the hand-back predicate) and the install-slot
range invariants in recompiler/tests/bios_address_model_test.cpp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 16:38:34 -04:00
Alex Vanderveen 922e8c5a07 Rename the product from RetComM to Retro
The launcher and studio ship as Retro Launcher and Retro Studio now,
from the RetroPortingToolKit org, so the old product name is gone from
display text, documentation and release asset filenames.

The split is by case, and it is not cosmetic. CamelCase RetComM was
always the product; lowercase retcomm and RETCOMM_ are identifiers --
the C++ namespace, the binaries, the data directories, the CMake and
environment variables, the Cloudflare service names, the readme markers.
Renaming those would orphan existing installs for no benefit, so they
are untouched: 1101 lowercase retcomm and 205 RETCOMM_ still stand in
the launcher alone.

Four things are deliberately still spelled RetComM, and three of them
would have been real breakage:

- RetComM-Data is CamelCase but it is a directory on disk --
  <exe_dir>\RetComM-Data\{runtime,config,data} for a portable install.
  Renaming it strands every portable user's config, toolchains and
  catalog cache.

- TechnicallyComputers/RetComM-Launcher in readme_has_launcher() is the
  historical half of a both-slugs test. Renaming it makes it a duplicate
  of the line above and sends every port built before the transfer back
  to reporting its launcher section missing.

- _LEGACY_LAUNCHER_RE matched "^## RetComM Launcher". A sweep renamed it,
  which would have silently stopped it matching every README already out
  there; it now accepts both headings.

- The portable-stub lookup gained the pre-rename filenames back as
  fallbacks, because old zips stay on the releases page and a rollback
  downloads one.

Release assets become Retro-Launcher-* / Retro-Studio-*. That is safe
for installed clients: pick_launcher_asset() gates on *linux*/*windows*/
*macos* plus extension and arch, not on the product name, and the
Windows portable stub is identified by its RCM1 trailer rather than its
filename.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013DpaQqVguHFpBuCmdoFYWY
2026-09-09 17:32:19 -04:00
Matthew Stanley 912c44c75e Support native-wide HUD and tiled backgrounds with guarded culling 2026-09-07 22:40:23 -07:00
Alexandros Mandravillis 1c60d340dc Preserve and verify SBI companions during disc setup 2026-09-05 20:51:14 +03:00
Matthew Stanley e6ce5006b4 docs: move root and stale docs into docs/ and docs/internal/
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>
2026-09-03 07:39:07 -07:00
Alex Vanderveen 2ac6d181d9 Merge pull request #262 from mstan/master
upstream
2026-08-29 16:44:57 -04:00
Alex Vanderveen d14ca6917c bump 2026-08-29 01:50:12 -04:00
Alex Vanderveen 77095c589c runtime: make local rewind opt-in, and give it a settings key
Rewind was on for everyone and could only be turned off with PSX_REWIND=0 in
the environment. It now defaults off and is enabled from settings.toml
([video] rewind) or the launcher's Display card.

The cost it was charging silently: each snapshot is a whole machine — 2 MB main
RAM + 1 MB VRAM + 512 KB SPU RAM — serialized uncompressed into a buffer
malloc'd at a fixed 5 MiB cap that is never shrunk to the actual length, and one
is captured every rewind_interval frames (15 by default, tightening toward 4
while an FMV runs), each with a 128x96 thumbnail built pixel by pixel. At the
default depth of 50 a full ring is roughly 250 MB resident; at the maximum 200
it approaches 1 GB. That is a lot to hand a low-end host for a feature a session
may never open.

Turning it off genuinely avoids all of it: psx_rewind_configure() returns before
any allocation when rewind is not wanted, and both capture paths are guarded by
psx_rewind_enabled(). The only residue is a 450 KB BSS overlay panel whose pages
stay untouched.

The toggle applies live rather than at next launch — freeing the ring is the
whole point, so waiting for a restart would miss it. psx_rewind_shutdown()
already resets the overlay and drops a pending load, so tearing down mid-session
is safe.

PSX_REWIND still outranks the setting in both directions and now accepts 1 as
well as 0. Netplay rollback is a separate subsystem with its own ring and is
unaffected; rewind was already suppressed during netplay.

Because off is now the common case, psx_rewind_toggle() says so on the OSD
instead of leaving F8 / Select+R3 looking broken. It stays silent only in a
-DPSX_REWIND=OFF build, where there is nothing to enable.

Adds rewind_settings_test (default off, explicit off distinguishable from
absent, save/load round trip, tuning keys independent of the enable) and the
first documentation the feature has had, in config_schema.md.

Verified: recompiler tests build and pass; main.cpp type-checks against the new
recomp-ui ABI field; psx_rewind.c compiles both with and without
PSX_HAS_RBENGINE_SNAP; the existing test_rewind_toggle_combo.py still passes.
Not run end to end — this checkout has no BIOS dump to boot.

Needs recomp-ui a31ecbc for Settings.rewind_enabled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017AwqKPQFnK3x7xQbK4PQKj
2026-08-28 17:33:52 -04:00
Alexandros Mandravillis d596b0ec46 runtime: [runtime] overlay_region_floor config key; bundled toolchain drives gcc when available
Two shipping gaps left Gran Turismo's gameplay and FMV code on the
interpreter (0.55-0.64x real time) even though the recompiled boot EXE was
fine:

1. The overlay region floor defaults to the boot EXE text end. GT's secondary
   EXEs (GTOS/GTMENU/GTMAIN/GTEND) all load at 0x80010000, inside that range,
   so they were never overlay-cache eligible. The only way to lower the floor
   was the PSX_OVERLAY_REGION_FLOOR env var, which a shipped package cannot
   carry. Add `[runtime] overlay_region_floor` (hex string or integer), applied
   before the env override with the same kernel-window clamp.

2. The bundled overlay_toolchain/ was only used to build the tcc command.
   A dev/production box with gcc on PATH but no game.toml autocompile command
   got no compiler at all. Treat the bundle as a gcc toolchain provider too:
   gcc counts as available when the bundle is present and a gcc/cc/clang is
   on PATH, and the same bundle then drives compile_overlays.py with
   --compiler gcc. One package yields gcc shards on a dev box and tcc shards
   on a toolchain-less player box.

Measured on GT1 (SCUS-94194), headless boot -> intro FMV -> attract, with the
floor at 0x10000 and gcc autocompile: 1.27-1.38x real time vs 0.55-0.64x;
SPU production 27 kHz -> 59 kHz (the FMV audio distortion was starvation).

(cherry picked from commit 0d226e5069bdf1395c819110beec40367a44a699)
2026-08-26 21:43:30 +03:00