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>
Matched Ape Escape retail SCPH1001 LLE builds identify 0661ce77 as the visible boot regression: parent 155e269b reaches game code, while 0661ce77 stalls near BIOS frame 375. Revert its four dependent follow-ups too; retain PR #326 readback bounds and PR #328 Vulkan aspect changes.
The row-by-row raster dispatcher amplifies an existing malformed DMA list into massive GL work. Keep the broader CPU/DMA arbitration experiment separate for compatibility testing. Document attribution, validation, and the temporarily restored interlace limitation.
Validation: full Ape target reaches title/menu and frame 11669 on retail LLE; OpenBIOS control reaches frame 3701. Eight focused tests pass; retained GL readback fixture passes 71 checks at both 1x and 4x after linking local SDL with iconv. Beads: beads-iisy; DMA follow-up: beads-eio.3.122.
Three changes, one fix and two pieces of visibility.
1. ws_ui_group_assign gains a third join: consecutive in the ordering-table
walk AND touching. WipEout 3's speed/shield readout ends at x=288 and the
meter bar it belongs to starts at x=306 with a different CLUT, so neither
shared columns nor a matching key could join them. The readout stood alone,
its bbox midpoint 256.5 landed in the middle third against a screen centre of
254, and it stayed pinned near centre while its own meters went to the right
edge -- a 2 px gap in 4:3 opening to 159 framebuffer pixels at 32:9.
No anchor threshold can fix that: the genuinely centred "pit ... check"
message sits FARTHER RIGHT (midpoint 300.5) than the mis-anchored readout, so
any threshold reaching the readout drags the message with it. Submission order
is the only signal that separates them, and the array already carries it.
Simulated against seven captures spanning three aspects and two code states,
this forms three unions and changes exactly one anchor: [225,288] centre
becomes [225,486] right. The other two unions ([208,240]+[240,361],
[364,447]+[450,473]) are within an anchor. Confirmed in-race at 32:9: 7 runs,
lap readout left, speed readout and meters together on the right.
2. ws_ui_groups reports why primitives were NOT admitted to the partition.
Each gate in ws_ui_prepass_add is a silent return, so a primitive left at its
raw 4:3 X while its neighbours are squashed -- which is how a HUD mark ends up
stranded mid-screen -- was indistinguishable from one never drawn. In-race at
32:9: opcode 367, not_axis 410, everything else 0.
3. geom_correction reports perspective-UV arming with a real denominator.
perspective_triangles could previously only be compared against gp0_draw,
which counts untextured primitives that are correctly never armed, so it read
as a coverage figure without being one. texcorr.attempts counts exactly the
textured triangles reaching the predicate. In-race at 32:9: 199599 submitted,
96.15% armed, 3.85% rejected for a missing per-vertex Z, 0% for correction
disabled or a missing packet address.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 572b9210d8)
auto_ui_squash squashes each spatial run of UI primitives about that run's own
anchor, so a HUD element split across two runs gets two anchors and comes apart
as the frame widens -- elements drifting toward opposite edges, glyphs sliding
off the background box they sit on.
Diagnosing that required knowing which run each primitive landed in, and nothing
exposed it. The two things an observer could reach were both insufficient:
group.key is a hash of CLUT/texpage/Y-band/family, so unequal keys say two prims
differ without saying WHICH component differed; and the anchor takes only three
values (left/centre/right thirds), so equal anchors do not prove two prims
actually co-grouped rather than coincidentally landing on the same third.
ws_ui_groups dumps, for the last prepass, each primitive's op / key / raw key
inputs (y, h, and the derived 24px band and poly-vs-rect family) / union-find
root / final anchor, plus the frame-level active, squash, dense, rank, disp_x,
disp_w and join_gap. Root plus raw inputs together answer "did these two merge,
and if not, what split them".
Carries two small support changes:
- WsUiGroupItem gains a diagnostic `root`, written by ws_ui_group_assign in all
three paths (dense, alloc-failure fallback, and the union-find path). Never
read by the transform path.
- WsUiPrepassItem retains y/h/op, which the key otherwise folds away.
Read-only and query-driven, per the ring-buffer rule: capture is continuous and
the observer asks for a window later.
(cherry picked from commit 6de1ad4a05)