Some PlayStation 1 titles implement trivial rejection by folding the sign bits of projected vertex screen coordinates across triangle or quad primitives with a chain of AND instructions. Without adjusting these checks during widescreen presentation, geometry in the newly revealed horizontal area is still rejected by the original 4:3 bounds. Rather than embedding title specific instruction patterns directly into the recompiler, this introduces a generalized `[[widescreen.cull.sxy]]` configuration schema within `game.toml`.
Game configurations can now declare multi vertex trivial rejection sequences by their exact AND instruction words, vertex registers and the branch that consumes the result. The loader derives the expected chain from those words and rejects any other shape. Each AND keeps its original value except for the horizontal sign bit, which survives only while that vertex lies beyond the revealed left edge, so vertical and right side rejection are preserved and 4:3 output is bit identical. Shifted coordinate lower edge comparisons reuse the existing `screen_x_sites` handling through a new `slti rt, x, 0` form instead of adding a separate site family.
Full parity is maintained between recompiled code and the dirty RAM interpreter by sharing the same runtime helper and validating every fold and consumer instruction word on both paths, leaving mismatching overlay variants untouched. This eliminates hardcoded game logic from the recompiler while giving titles clean geometry retention in expanded aspect ratios.
* feat(widescreen): continue framebuffer-feedback sprites into native-wide bands
Titles tag a 15-bit sprite that copies a framebuffer 1:1 (gpu_ws_tag_feedback_rect / psx_mod_tag_feedback_rect). Where it touches the draw area's edge, gpu.c emits band spans and the new optional backend op wide_feedback_rect (GL incl. render thread, software) samples the source buffer's wide surface. Band texels count as bit 15 set, as the centre does once the trail blends.
The software rasterizer now writes the texel's bit 15 into the framebuffer, and wide_full can dump the surface alpha ("alpha":1).
* fix(widescreen): quantise band feedback like packed VRAM
The centre samples 5-bit packed VRAM each frame; the band copy read the 8-bit wide surface and modulated with /128, so the bands drifted ~3 grey levels brighter and left a step at the 4:3 edge. Truncate like PACK_FS and modulate like TEX_FS.
* Reject fully clipped sprites before extending feedback margins
* Complete isolated resident and PGXP fixture collaborators
* Preserve existing line endings in the review fixes
---------
Co-authored-by: Matthew Stanley <1379tech@gmail.com>
Renderers that test a 16-bit screen X with an unsigned compare against a width kept in a register (often a loaded clip bound) reject every negative X, so widening the bound only reveals the right band. A screen_x site on sltu now routes through psx_ws_cull_sltu_band: -4m <= (int16)SX < W while widened, identity at 4:3. Four margins keep large near polygons whose corners all lie past one margin; an unbounded left side overflowed a renderer's fixed primitive lists. Covered by ws_cull_screen_x_hi_codegen_test; documented in WIDESCREEN.md.
Review follow-up on #661 (#636):
- test_depth24_present_hold_vblank.py reads sources as UTF-8 (cp1252 on
native Windows Python raised UnicodeDecodeError).
- The present skip is now guest state: set at guest VBlank, read (not
consumed) by the present and packed into the GPU snap's hold word (bit 8;
older snaps derive it from the count). The executable test showed the
host-side latch made the present of a restored tick differ from live on
the last held VBlank (hold 0, skip 1), exposing stale trailing FMV
columns after a rollback load.
- Depth24 scanout staging and trailing-column blanking move from main.cpp
into gpu_depth24_stage_scanout (gpu.c), so the test renders through the
same code every CPU depth24 present runs.
- New depth24_present_hold_exec_test: drives real gpu.c through a 15-bit to
24-bit FMV, GP1(07h) retargets and mode transitions; compares hold, skip,
upload span and snapshot bytes across windowed/headless/deferred presents,
rollback replay from every tick and disk loads, and checks staged frames
never show stale movie pixels while the new movie stays visible.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A full-screen rect backdrop that stops short of the reveal vetoes polygon overhang (Tomba village, beads-eio.4.28).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Preserve negotiated content and dual-raster pass gates. Retain both fixture assertion sets and HD-off seams. Regenerate both BIOS with actual native emitter. Fix type-only rollback corrections and exclude extra-pad changes from digital-only promote gates.
A [[plugin]] may declare netplay = "local_view": it only changes what one
player sees. A netplay session now keeps the player's own features whose every
contribution is such a plugin (no writes, overlays or derived discs) instead of
clearing them, without saving state.toml. They activate as usual, but their
hooks run only inside psx_mod_render_local_view (psx_mod_local_view_scope) and
their vblank/savestate callbacks not at all. With such a plan the widescreen
cull margin is the stock 0 in the shared game and this peer's own margin only
inside the sandboxed own-view draw; squash is not used. The canonical frame,
snapshots and digests stay identical on every peer whatever each player chose,
so nothing is negotiated, and each keeps their own aspect.
Tests: netplay_view_mods_test (flag, plan filter, parked hooks, no vblank,
state.toml untouched), ws_local_view_margin_test (margin scope), render-pass
sandbox asserts the scope around a local view.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
A renderer that keeps a screen X as SX << 16 tests the right edge against
a lui-held W << 16 and the left edge with bltz on the shifted value. No
explicit [widescreen.cull] kind fits either: bltz_sites compares the whole
register with -margin, and a lui at a screen_x site was a build error.
screen_x_sites now also takes
lui rt, W -> rt = (W + margin) << 16
bltz x, outside -> taken while SX < -margin (x = SX << 16)
Both are the identity at 4:3. The helpers are defined for native code and
for overlay shards; their math is in ws_cull_edge.h. The two prototypes are
written only for a config that lists screen_x_sites, so the generated code
of every other title is unchanged.
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.
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>
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>
Two generic explicit cull-site kinds for native-wide titles whose screen-X
rejects the existing keys cannot express exactly:
- bgez_sites: `bgez SX, keep` in a signed per-vertex left-edge chain
(`bgez x0,keep; ...; bltz xN,reject`). Emitted through psx_ws_cull_bgez
(keep while SX >= -margin). bltz_sites alone still rejects a primitive
whose first vertices sit in the revealed band; with both kinds the chain
rejects exactly when every vertex is left of -margin.
- clip_edge_x_load_sites (+ clip_edge_width, default screen_w_imms[0]):
lh/lhu/lw of a clip-rectangle X bound. psx_ws_clip_edge_x moves a loaded 0
to -margin and a loaded width to width+margin and leaves interior bounds
(mirrors, split-screen halves) unchanged. xclip_load_sites is lw-only and
upper-bound-only, so it cannot express this.
Both are identity at 4:3, emit nothing when empty, and fail main-EXE
generation on an opcode mismatch (overlay variants stay vanilla). They enter
the overlay-cache hash only when non-empty, so every existing title keeps
its hash (pinned in the test against the previous layout).
Interpreter parity: the dirty-RAM interpreter now also honours explicit
bltz_sites, bgez_sites, branch_keep_sites and clip_edge_x_load_sites. Its
explicit site lists are stored sorted (binary-search lookup), the cap rises
from 64 to 256 per key, and an over-long list is logged instead of being
silently truncated.
Shared math lives in runtime/include/ws_cull_edge.h. New ctest
ws_cull_edge_codegen_test covers parsing, hash identity, emitted code,
the opcode guards and the helper math.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>