Framework fixes surfaced while bringing up a title that stresses overlays, HD
texture replacement and PGXP at a doubled CRTC rate. All are general; the
title's own configuration stays in its game repository.
CORRECTNESS
* Overlay DLL shim was deleted by the preprocessor. compile_overlays.py injected
overlay_dispatch_preamble.c.inc after the syntactically LAST #include. A
conditional i-cache include moved that anchor inside an #else, and DLL builds
define PSX_OVERLAY_DLL_BUILD, so the callback definitions preprocessed away and
every overlay DLL failed to link. Any title using overlays would silently fall
back to per-instruction interpretation. The shim is now anchored to the
unconditional #include "psx_runtime.h" and raises if that anchor is absent.
* Overlay capture kept stale PC bitmaps when executed code later became data, so
one variant's PC set could be compiled against another variant's bytes.
overlay_watch_note_write now retires the whole page's execution/dispatch epoch
when any executed word is overwritten; static demand is keyed by
(load_addr, size, bytes) and resolved only against matching range identities.
* VRAM-to-VRAM copies only invalidated the destination and never registered it as
a replacement candidate, so textures arriving by copy could not be dumped or
replaced at all. gr_copy_rect now calls tex_pack_on_copy with the same
(texture,palette) key semantics as CPU uploads.
* PGXP shared-edge seams: a per-primitive all-or-nothing gate cannot stop two
adjacent primitives disagreeing about a shared endpoint, so the edge is
submitted at two sub-pixel positions. Adds a frame-stamped canonical endpoint
cache keyed by final integer screen position: the first use fixes the
coordinates and every later occurrence reuses them.
* PGXP provenance: PGXPValue carries per-component X/Y/Z projection ids, so a
value assembled from different projections can no longer arm correction.
Destructive AND/XOR/NOR/SLT, link and COP0 writes now invalidate shadows, and
the SXYP shadow FIFO shifts with guest SXY.
* Precise NCLIP no longer replaces guest MAC0. It is telemetry only, so face
visibility stays bit-exact with the integer GTE.
* Removed two counters that were reported as correctness evidence but were never
incremented anywhere.
PERFORMANCE
* PSX_ENABLE_LTO plus a noinline guard on the shared cycle wrappers. Unguarded
ThinLTO clones hot interlock bodies and REGRESSES the hot path; guarded, it
becomes a win. Adds runtime/tests/bench_psx_runtime_hotpath.c, which verifies
variant equivalence by digest so codegen changes are measurable.
* Cycle accounting: emitters select specialized arity-0/1/2/3 helpers at
generation time instead of a generic dependency-mask loop; main-RAM loads
combine base/wait/completion into one exact charge. Exactness is covered by
pipeline-state, load-state, deadline-crossing and cross-device causality tests.
* HD texture invalidation replaced a linear scan of up to 8192 upload records per
VRAM write with a tile-refcount index; out-of-domain writes early-out.
* Translation/text-substitution dispatch is gated behind a monotonic mutation
epoch instead of running a callback on every dispatch.
* card_data_writes_check no longer runs on every RAM write.
CAPABILITY
* fixed_outer_aspect: the outer presentation aspect becomes a function of user
configuration only. Scene classification selects content transforms, never the
outer viewport, so menus/FMV stay centred undistorted inside a wide canvas.
VERIFICATION AND LIMITS
docs/UPSTREAM_2026-08-20.md audits each change against the diff, marks
unsupported claims UNKNOWN, and lists a suggested PR split and review focus.
Unit and structural tests accompany each item, but this body of work has NOT
been validated end-to-end against a fixed reproducible workload -- reviewers
should treat the timing-exactness and PGXP changes as the highest-risk areas.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Internal rendered-frame counter: GP1(05h) display-origin CHANGES
(the game finishing a frame and flipping buffers — the standard
emulator "internal FPS"). Shown beside the vblank rate in the title,
OSD, and [FPS] telemetry: "VBlank N/s X.XXx | Render M fps". The two
diverge under load and users could not tell which one they were
seeing (measured: races render exactly one frame per vblank; menus
idle far below their vblank rate).
- The per-scene 4:3 menu present (previous commit) is reverted per user
direction: uniform mod aspect everywhere; the game's own image is
never re-shaped between menu and gameplay.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Adaptive overclock REMOVED (user decision, and correctly so): trading
overclock down preserved vblank cadence but starved the game's render
loop (~30-50 real frames/s under a 110+ vblank counter) and modulated
the guest-time ratio continuously — an audible CDDA warble. The
DuckStation-standard model stands: static user overclock; a too-slow
host slows everything uniformly and audio underruns, never pitch-shifts.
- SDL3 audio: psx_sdl_audio_open reported the DEVICE format instead of
the stream's INPUT spec. SDL3 converts fed data FROM the requested
spec; telling the DRC host_rate=48000 while SDL consumed it as
44100 was a constant ~0.92x pitch-down plus perpetual ring overfill
(overflow_drops clicks) on every SDL3 build. Report the input spec —
the DRC stays the only resampler. (Audio-clocking audit confirmed the
bridge already implements the DuckStation contract: guest-clocked
production at 768 cycles/sample, device-rate pull, +-0.5% trim,
hold+fade on underrun; production stages must not be touched —
netplay/savestate invariants at main.cpp:3009/3071.)
- Present-aspect scene switch: with the widescreen mod engaged, menus/2D
(scene gate inactive, 45-frame hysteresis in gpu.c) present at the
game's own 4:3 so its NATURAL black bars stay, framed by the bezels;
gameplay presents at the mod aspect. New export
psx_ws_scene_wide_active().
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ADAPTIVE OVERCLOCK: the configured cpu_overclock is now a ceiling.
A proportional controller on an EMA of the wall vblank period trades
the effective overclock down (floor 200%, PSX_ADAPTIVE_OC env) when
the emu thread cannot retire the scaled work, and back up when there
is headroom — so the GUEST TIMELINE never dilates. Measured live
before: guest time at 0.4x wall in races (CD audio at 0.4x pitch, SIO
pad exchange lagging, race clock slow) while per-vblank game logic ran
fast on the vblanks that did arrive. After: 0.94-0.98x sustained
through scenes that previously dropped to 0.3-0.5x, CDDA at wall rate.
Frozen across intentional unpaced windows (turbo loads, HLE boot).
- dirty_ram_text_native_ok memo made page-granular: a diverging guarded
write now bumps only its page's generation; the memo keys on the sum
over a function's own range pages. CD streaming (hundreds of dirty
transitions per frame in races) no longer flushes every verdict
(was 6.8% of the emu thread with the memo never hitting).
- tex_pack_lookup_replacement memo with lazy upload validation: keyed
on (uv limits, clut, texpage); kills/repl growth invalidate hard,
upload ADDS are checked lazily against the prim rect and CLUT row so
continuous race streaming does not self-flush the memo (was 9.3%).
- Bezel: seam hairline removed (user request); the near-black gradient
and wide falloff stay exactly as before.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First real attribution (in-process 1 kHz IP sampler, 75k samples at
oc900, play build). Top sinks and their fixes:
- tex_pack_on_textured_prim (7.4%): per-prim mutex + CLUT hash + linear
scan over up to 8192 uploads — entirely matched-set/dump census, a
debug/authoring surface with no reader in play builds (substitution
lives in tex_pack_lookup_replacement). Gated out of play builds
unless dumping. Confirmed gone in the follow-up profile.
- text_xlate_on_dispatch (4.2% -> 0.8%): the a0..a3 record scan serves
capture (off in release) and STRING substitution; a fixes-only load
(vram patches / glyph labels) has an empty string table, so the scan
could never act. Early-out on empty table; the throttled patch blocks
still run.
- psx_pgxp_alu/muldiv (3.6%): ALU/MULDIV shadows feed only tier-2
cpu_mode and the correction consumers' chains; the exact-sign NCLIP
arm is LOAD/STORE/COP2 only. Hook bodies now gate on
(cpu_mode || full_hooks) — full_hooks maintained by
gpu_pgxp_rederive_enable — and the remaining per-instruction CALL
cost is removed by an inline g_pgxp_alu_armed check in the
PGXP_ALU/PGXP_MULDIV macros themselves. Correctness: a stale shadow
from the gated period is dropped by pv_validate at every consumer
(the engine's single safety invariant, verified at the MTC2 path).
Measured effect of the round at oc900 (before the macro gate):
sustained 0.73-0.80x -> 0.87-0.91x; streaming-window dip 0.35 -> 0.50;
post-window touching 1.00x. Fresh profile after: xlate and tex_pack
prim hook out of the top-15.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Env-gated self-profiler: samples the main (emu) thread's RIP at 1 kHz
for N seconds, histograms exe-relative 64-byte buckets, dumps the top
120 to psx_self_profile.txt (llvm-addr2line VA = 0x140000000+offset).
Play-build hot-spot attribution without ETL symbolization; zero cost
when unset. Motivation: internal-scale (16x vs 8x), cadence (vsync vs
pacer), and identity-memoization A/Bs all measured within noise, while
halving cpu_overclock nearly closed the fps gap - the cost is in guest
instruction throughput and needs real attribution, not hypotheses.
- Mixed-emitter files (FFE preamble + code_generator bodies) missed the
PSX_ICACHE_FETCH macro; both FFE preambles now define it too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1. Rewind degraded-mode fix: a capture deferred past its interval
(previous snapshot still compressing at capture time) retried with no
PBO prearm and fell into the synchronous full-pipeline VRAM readback
(8-17 ms on the emu thread). Load-heavy windows delay the compression
pump, making the degraded mode self-selecting exactly where frames
are already long — the 0.4x dip class. Arm the async PBO whenever a
capture is pending and no compression is in flight.
2. Native-vblank-rate mods no longer force vsync off for fps > 60. The
present-cadence policy is already an XOR (swap interval resolves to
the configured vsync only when the panel matches the CRTC rate within
2%, else 0 with the wall pacer engaged), so the force was pure
over-defense that locked the 120 Hz mod onto the wall pacer even on a
matching 120.0 Hz panel — where driver vsync absorbs the sub-frame
jitter the pacer converts into debt/catch-up oscillation (measured
99.7-122.5 fps seesaw at 0.83-1.02x). Non-matching panels behave
exactly as before.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A region-truncated final block that runs off the end of its function fell
off the C function body when no in-image fallthrough function existed:
the entry-switch prologue had consumed cpu->pc to 0, so the dispatcher
returned "handled" with pc==0 and the top-level trampoline read it as
"program ended". Witnessed live in the WipEout 3 ntsc120full8 static
bake: the 0xD5000 region variant's block_800D7FC0 ends at the region
edge (continuation 0x800D8004 lies outside the image); the moment CD
streaming loaded content matching that variant's identity ranges the
dispatcher routed 0x800D7FC0 into the truncated block and the run died —
the deterministic "execution completed, PC=0" at ~10 s into boot, ~frame
1221, byte-for-byte reproducible.
Both code_generator tail sites (plain functions and alias hosts) now
emit `cpu->pc = <end+4>; return;` when the last live block runs off the
image edge — the CPS tail-transfer the full_function_emitter already
emits for in-image fallthroughs. The dispatcher routes the published
continuation to whoever owns it (a neighboring region variant, AOT
text, or the interpreter). Faithful and class-level: no per-game
anything.
Diagnosis chain that cornered it (all landed as permanent tooling):
- pc0 escape journal site 14 (trampoline top-level exit) named the last
dispatched target: 0x800D7FC0.
- site 15 (dirty-dispatch handled-with-pc0 tripwire, now journaled with
a route tag) proved the producer was route 3 = the static overlay
dispatcher, first occurrence, not in exception context.
- Return-shape census of every candidate variant body showed no
return-with-pc==0 path — forcing the discovery that the dispatch case
for 0x800D7FC0 routes FIRST to the 0xD5000 variant, whose truncated
tail block falls off the end.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the deterministic top-level "execution completed, PC=0" ~10 s
into WipEout 3 boot with the static bake linked (the 120 Hz core bug).
Root cause (CPS-contract audit, H1): every emitted block-leader IRQ
check was `psx_check_interrupts_at(cpu, X); cpu->pc = X;` — whatever
resume PC the exception epilogue published (guest RFE to a non-EPC
target: longjmp-style card-ISR handlers, SetCustomExitFromException,
thread switch) was unconditionally overwritten by the static transfer.
The dirty interpreter honors these redirects ("Handler resumed
elsewhere — surface to dispatch"); the static bake moved the IRQ
take-points from interp pumps onto compiled leaders, where the redirect
was dropped — the guest's continuation vanished, downstream state was
never established, and a later jr through a null pointer surfaced as a
silent top-level PC=0 exit.
- code_generator/full_function_emitter: both check emitters (and the
install-slot hook) now emit the compiled analogue of the interp
contract: `if (cpu->pc != 0 && ((cpu->pc ^ X) & 0x1FFFFFFF)) return;`
after the check, surfacing the redirect to the trampoline. Applies to
branch/jr/jalr/jal/split sites in the AOT text, the recompiled BIOS,
and every overlay shard/static bake on regeneration.
- interrupts.c: the async-RFE resume latch (g_async_rfe_resume_pc,
"Tomba 2 frame-1997 fix") was documented but NEVER ASSIGNED — the
whole rescue in the traps/dirty sentinel gates was inert (audit H3).
Latch it at every exception entry that installs a real EPC.
- pc0 escape journal (always-on, play builds): every runtime site that
publishes/rescues a null PC records {site, frame, dispatch depth, ra,
epc, extras} in a 64-entry ring, dumped at the abnormal-exit print.
The play build's exit trace was blind (fntrace is debug-only:
fntrace_seq=0 in the death artifact); publishes are rare so the
always-on cost is a few stores.
- compile_overlays.py: PSX_STATIC_NO_ISOLATED=1 A/B guard to skip the
isolated-fragment pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root-causes the transient 1-frame sign-corner flicker at wide aspects
(sign faces blink when the exact-sign NCLIP path silently loses its
shadows for one frame):
- gte_nclip was the only shadow consumer skipping the engine's single
safety invariant (packed-word validation). A stale FIFO shadow (e.g.
after an SXYP MTC2 shifts the real FIFO but not shadows 12/13) could
invent a silently WRONG winding sign. New pgxp_get_gte_sxy_checked
validates the shadow's word against the live SXY register; staleness
now demotes to the honest integer fallback.
- Overlay shadow-diff REPLAY passes ran the pgxp memory hooks live
(record pass's stale FIFO shadows re-validated against different
vertices -> flags dropped, live slots overwritten -> arena-wide
provenance kill = one-frame integer-sign blink, plus false
SHDIFF_GTE_DATA divergences permanently demoting shards to interp).
gte_replay_side_effects_begin/end now bracket pgxp_suppress so replay
is shadow-silent, like the speculative validation passes. The nclip
precise path is additionally gated off inside the sandbox and
speculative passes.
- pgxp_set_enabled is now idempotent: a redundant re-derive (config
apply, correction toggle, debug A/B) no longer costs a generation
bump, which killed every live shadow and blinked one frame of
exact-sign coverage.
- Observability: free-running NCLIP precise/fallback/corrected counters
and a PGXPStats.invalidations counter (bumped at the generation-bump
site), both exposed in the geom_correction debug reply.
All changes are inert at 4:3 (ws_precise_nclip gate) and touch
host-only state exclusively.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pack directory's exclude.txt is game content (gitignored), so it
silently disappears whenever the pack is regenerated - which re-enabled
HD substitution of WipEout 3's four font atlases and produced the
doubled-glyph defect (sub-texel HD strokes under the game's two-pass
shadow draw; coverage.json proved atlas 4cb2ec4e substituting). The
config now carries the exclusions durably; they merge with any
pack-local exclude.txt, and semantics are unchanged (texel-source
exclusion only, applied at tex_pack_lookup_replacement).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pc=0 from the same-thread-restore epilogue means "the interrupted native
frame is still on the host stack below - return and it continues". At
dispatch depth <= 0 no such frame can exist and the 0 reaches the main
loop as a bogus GUEST_EXIT, so prefer the compiled interrupt resume PC
there (the same remedy the scheduler top-level-resume case already uses).
Honest scope note: this closes a real hole by construction, but does NOT
fix the observed WipEout 3 static-bake boot exit (deterministic
"execution completed, PC=0", ra=0x800D7FC0, epc=0x8015FAEC) - that death
reproduces unchanged, so its escape route is elsewhere; investigation
continues. Also: the top-level exit path now prints the exit registers
(ra/sp/epc/sr/func/last_store_pc/v0/a0/t9), which is how the crash was
attributed on play builds without debug tools.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A sweep of vblank-period-derived windows under the NTSC x2 CRTC (audit
2026-08-19) confirmed the high-stakes paths - SPU/XA/CDDA pitch and
cadence, guest timers, rumble, pacing - are guest-cycle-anchored and
correct at x2. Two windows that cover CYCLE-fixed guest work were
period-relative and silently halved:
- sio.c card ChangeThread-defer tail ("~2 VBlank periods" over the
libcard A6C10/B4E38 handshake): now stock-period relative.
- cdrom.c savestate CD boost window ("~3s at 60Hz", counted per sim
vblank): now scaled by the multiplier.
Remaining audited-but-unchanged exposures are opt-in QoL paths (FMV
auto-skip hold, turbo-load hysteresis, audio mute hangover, rewind
interval, pacer catch-up window) documented in the audit; and one latent
trap: the non-block-cycles fire_vblank_edge path steps timers/cdrom per
vblank edge and would run devices at 2x under the multiplier (shipping
builds compile it out). mdec.c's "cycles_per_frame = 338688" comment is
mislabeled (that is 10 ms, not a frame) - pre-existing, cycle-anchored,
unaffected by the multiplier.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The runtime pins the cache dir and captures path into the spawned
compile's environment so the cache can never drift from where the loader
reads - but not the FLAVOR. compile_overlays.py's --flavor defaults to 0
(play), so an instrumented (flavor 2) runtime spawned compiles whose
shards its own loader then rejected: the overlay_loader_status message
said it plainly - "this build reads ..._f2 but shards exist under ..._f0
-> ALL overlays run INTERPRETED". On mod-heavy titles (WipEout 3's
framerate packages patch most of the game text) that is the difference
between full speed and ~0.2x, misread as general slowness for weeks.
The runtime now exports PSX_OVERLAY_FLAVOR alongside the other pins, and
the tool honors it over --flavor, so each build population self-heals.
Measured after the fix (instrumented, WipEout 3 ntsc120full8): shard
loads 0 -> 8 (45 funcs), interpreted insns 6.6M/s -> 1.0M/s.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The unstick pump - a nest repair for Ape Escape LOAD that force-re-edges
I_MASK.7/IRQ7 after a LOAD-style probe abort - ran enabled by default for
every game. On WipEout 3 SE under the NTSC x2 CRTC its arming detector
misfires during the boot memcard check, and the forced IRQ storm keeps the
kernel card ISR acking idle bytes forever (traced: 40k+ card_ack 0x00 at
~1.3k-cycle spacing, all Sw/HwCARD completion events stuck ACTIVE, zero
delivered), so the game waits on the memcard screen indefinitely. A/B with
PSX_APE_CARD_UNSTICK=0 boots straight through.
Per-game hacks are config-owned: default OFF, enable with [runtime]
ape_card_unstick = true (Ape Escape's config should set it), and the
PSX_APE_CARD_UNSTICK env var still overrides in either direction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The swap-paces gate from the previous commit was a flag cached from
apply_present_cadence(), which can run before the GL renderer is live -
the gate never engaged and the unpaced-skip hole survived. Replace it
with psx_present_swap_paces(), evaluated by the renderer at every skip
decision, so no cached copy can go stale across init order or cadence
changes. The setter is gone.
Bezel: floor the tiled mark size at 20% of the window's short dimension.
A thin letterbox band no longer shrinks the marks into busy little rows -
they stay window-proportioned and the band crops them symmetrically, so
bands read like the side columns do.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pacing: when driver vsync owns the frame cadence the blocking Swap IS the
game's clock, but the unchanged-frame present skip bypassed it - every
skipped swap silently unpaced that frame. WipEout 3 at 30 Hz sim renders
every other vblank, so half the frames ran unpaced: the game sped to a
fluctuating ~2x with pitched-up CD audio and visible frameskip, EXCEPT while
the FPS readout was on, whose persistent OSD line forced a swap per frame
and masked the hole ("the moment I do fps readout off it goes stupid fast").
gl_renderer_set_swap_paces(), derived in apply_present_cadence(), disables
the skip whenever the swap paces. Real hardware scans out every vblank
regardless of change.
Bezel: replace the flat tiling with a mounted-screen treatment - deep-tinted
ground, the tiled marks at watermark subtlety (0.22), a distance-field shade
(rounded-rect SDF against the live viewport rect) running near-black beside
the game to a faint pattern at the outer edges, and a resolution-scaled
hairline seam terminating the viewport. The 200 ms fade-in from black on
viewport-rect changes folds into the shade endpoints. Game framebuffer
untouched; all of it derives procedurally from the viewport rect.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Manual fast-forward engage/release and turbo-loads engage/release now print
one [pace] line each with the host timestamp. Any unpaced or present-skip
mode flipping mid-play is exactly the class users report as "the speed is
busted", and the existing transition ring is only reachable through the
debug server - absent from play builds, which is where players actually
live. One line per transition self-documents the next repro.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two refinements that keep the game image the focal point and make the
padding read as environmental texture instead of a second UI layer:
- Padding shade: a per-rect gradient quad (new tiny fragment program) drawn
over the bezel backdrop+tiles, derived from the live viewport rectangle.
The padding is near-black (alpha 0.94) immediately beside the game and
eases out to a faint pattern (alpha 0.72) at the outer screen edges,
smoothstepped across the full padding span - wide and subtle, never a
narrow shadow. Columns shade along X, bands along Y, so it holds for any
viewport aspect. The game rect itself is never touched.
- Transition fade: when the viewport rect moves (menu aspect contraction,
window resize) the padding restarts from solid black and reveals the
resting gradient over ~200 ms (smoothstepped), so the eye follows the
contracting game image instead of a full-intensity pattern popping in on
frame one. The fade folds into the gradient endpoints (affine), so it is
one draw either way.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
present_bezel only drew the left/right margins and explicitly ignored the
vertical geometry, so a window taller than the content aspect (32:9 content
on a 16:9 display) framed the game with bezel art on the sides and raw black
bars above and below. Add bezel_draw_band -- the transpose of
bezel_draw_rect: one logo spans the band height with the same air factor,
repeating across at authored aspect -- and clear/draw the top and bottom
bands between the side columns so no pixel is painted twice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two bugs that together kept every mod-patched text page interpreted forever
(the WipEout 3 framerate mod's 74 divergent pages -> ~0.5M interpreted block
dispatches/sec in play):
- The [[widescreen.cull.widen]] emitter writes psx_ws_cull_slt_widen /
psx_ws_cull_slti_lower at configured sites, including in overlay-resident
code, but the overlay dispatch preamble never defined them: every shard
containing a widen site failed to link (undefined reference) and the
autocompile pipeline reported compile-error for the whole region. Mirror
both helpers into the preamble following the documented self-contained,
byte-identical convention.
- append_pgxp_hooks joins the hook onto the translated statement with a
space; the block-cycles MULT/DIV/MFLO/MFHI translations end in `#endif`,
and the preprocessor discards tokens after #endif (the "extra tokens"
warning class) - so those hooks silently never ran. Give the hook its own
line when the translation ends in a directive.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three changes that ride the existing PGXP dataflow engine:
- Arm the PGXP engine whenever a precision consumer needs it, not only when
a correction feature is on. gpu_pgxp_rederive_enable() derives the arm from
texture correction OR geometry correction OR the widescreen precise-NCLIP
opt-in, and every setter re-derives. Previously disabling both corrections
silently disabled the shadows precise_nclip depends on.
- gte_nclip: when the sub-pixel shadow of all three SXY FIFO entries is live,
take the winding SIGN from the exact double cross product and keep the
hardware integer magnitude. The X-squash scales areas by a positive factor,
so only integer rounding ever flips a sign; this removes both the vanished
trackside signs and the per-frame tile flicker at wide aspects without any
epsilon tolerance. The producer clamps shadow coords to +/-4096 px so a
projection blowup cannot wrap the 16.16 int32.
- Clamp rescue (prepare_precise_triangle): a vertex parked exactly on a GTE
saturation rail (+/-1024) is hardware-mangled; when the validated dataflow
shadow knows its true position, rasterize with that instead. Kills the
"polygon spike" streak class that 4:3 framing plus CRT overscan hid. Every
in-range vertex stays native, so normal geometry is bit-identical.
- dirty_ram_interp: fire psx_mod_function_entry on interp dispatch so
function-entry mod hooks do not depend on which backend executes a
mod-patched page.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- pgxp_census / pgxp_texcensus (debug server): per-primitive resolution
classes and UV-correction miss reasons with screen bboxes for the last
complete frame, double-buffered by frame parity. These attributed the
swimming-wall and flickering-ship classes down to individual packets.
- overclock command: live CPU overclock set/get for anchored same-scene
A/Bs (the 100%-vs-900% pacing measurements).
- text_xlate: load translation files that carry only vram_patch/glyph
entries (previously gated on a non-empty "entry" array), enabling
data-fix-only files like the WipEout 3 palette-gap patch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The file header's section count was HARDCODED to 16, so ANY appended
section made the loader stop early and poisoned every later section - the
long-standing "adding a section breaks a later one" trap. BsOut now counts
sections and backpatches the header (file via fseek, memory in place).
New BS_SEC_MODSET rides FIRST: the resolved mod-plan fingerprint. A load
whose fingerprint differs from the running plan is refused before any state
is applied, with a message naming both plans - previously such loads
half-applied and died at a null PC. Old saves without the section still
load (best effort), matching the skip-unknown policy.
BS_SEC_TEXPACK (dormant since the count trap) is enabled: tracked-upload
rects+hashes persist, pixels rebuilt from restored VRAM and hash-verified,
so HD texture substitution survives savestate loads. Full save->load->run
roundtrip verified live.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A run's anchor derives from its current members' bounds, so a widget whose
parts blink (WipEout 3's sub-10s countdown hides its digits every other
beat but keeps drawing "check") changes runs frame to frame and lurches
between thirds anchors at the blink rate. Remember the anchor each prim was
squashed about, keyed by texture key and quantized position, and let any
remembered member pin its whole run to last frame's anchor. Entries age out
after ~2.5 s and the identity folds in display_width, so moved elements,
relayouts and window resizes re-derive fresh anchors instead of pinning
stale ones.
Verified frame-exact on the race countdown: digits steady through both
blink phases, off-phase label fixed where the on-phase draws it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two coverage holes measured live on WipEout 3 SE:
- The shadow was allocated lazily ONCE; when the first enable ran before the
8 MB mod expanded RAM, a 2 MB-wrapped shadow aliased every extended-RAM
packet for the whole session (the player ship's packets resolved fully
native - flickering ship textures, dark track dashes). Re-size on any
enable whose live RAM size disagrees.
- Every half-rebuild recovery path (SH pair, LH/LHU, SLL/SRL/SRA by 16,
OR repack, ADD/SUB screen translate) preserved positions but dropped the
vertex depth, disarming perspective UVs on every packet the game builds
from halves ("swimming" wall panels). Depth now rides along whenever the
contributing sources agree; the truncation-agreement gate still owns
mixed-source rebuilds.
Adds pgxp_debug_shadow_class: which rung of the load ladder fails for a
word, for the texcensus diagnostic.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`default: return 0` made every savestate written by a build with one extra
section a poison pill for every build without it. Worse than unloadable: by the
time the apply loop hit the unknown tag it had already restored RAM, VRAM and
CPU state, so the refusal left a HALF-RESTORED machine that wedged or died at
PC=0 -- which presented as "loading a save crashed the whole thing", with the
restored-but-untracked VRAM showing low-res in the wreckage.
A sectioned state format exists precisely so readers can step over what they do
not know. Known-but-malformed sections still fail as before; only genuinely
unknown tags are skipped.
This also makes the states saved during the short-lived BS_SEC_TEXPACK window
load cleanly again (the extra section is now ignored), and it is the
forward-compatibility prerequisite for ever adding that section back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>