Commit Graph

1059 Commits

Author SHA1 Message Date
Alex Vanderveen 87699f5ab9 bump 2026-08-23 15:44:58 -04:00
Alex Vanderveen 84901d3131 Merge PR #156 (feat/ws-exact-nclip) into feat/rbengine
Widescreen precision: exact-sign NCLIP, GTE clamp rescue,
backend-invariant entry hooks — plus perf round 1, pacing fixes,
self-profiler, and IRQ-redirect recompiler fixes from the branch.

Conflict resolutions:
- runtime/src/gpu_gl_renderer.c (bezel_tiles_col): combine both bezel
  mark-sizing fixes — height-anchored size with PSX_BEZEL_SCALE
  (rbengine, fixes ultrawide growth) floored by the caller's
  window-proportioned mark_px (PR, fixes thin-margin shrink); an
  explicit env override wins in both directions.
- runtime/src/main.cpp: keep both additions — netplay_resolve_link_mode
  (rbengine) and the PSX_SELF_PROFILE sampler (PR).
- runtime/tests/test_fps_telemetry_opt_in.py: re-pin the structural
  guard's format needles to the PR's dual speed readout strings
  (VBlank/Render/Display); the opt-in guards are unchanged.

Verified: psxrecomp-game rebuilds; gpu_gl_renderer.c compiles clean;
ctest failure set identical to pre-merge HEAD (no regressions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TdzpVVH912V9ktt4n4kntj
2026-08-20 14:48:29 -04:00
Alex Vanderveen 45e6bc9cab Merge origin/master into feat/rbengine
Conflict resolutions:
- recompiler/src/main_psx.cpp: keep both — canonicalize via
  psx_ram_canon_code_addr (rbengine 8MB mirror fold), then compare
  29-bit physical addresses (master KUSEG dispatch fix, PR #155).
- runtime/src/main.cpp: keep both — text-base pin for high-loading
  EXEs (rbengine) plus PSX_OVERLAY_REGION_FLOOR env override (master).
- runtime/src/text_xlate.cpp: keep rbengine's build-flavour capture
  default (PSX_NO_DEBUG_TOOLS) which subsumes master's default-off fix.

Verified: psxrecomp-game rebuilds; test_kuseg_dispatch_lookup.py passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TdzpVVH912V9ktt4n4kntj
2026-08-20 14:39:42 -04:00
Alex Vanderveen a198b2b768 bump 2026-08-20 11:24:16 -04:00
Matt Stanley 95366a6504 Merge pull request #164 from rickguedes/fix/overlay-close-input-bleed
runtime: swallow the overlay-closing press so it doesn't bleed into the game
2026-08-19 22:13:38 -07:00
Matt Stanley 79766c6b8b Merge pull request #159 from Alexbeav/fix/launcher-cli-disc-prefill
runtime: seed launcher initial disc from CLI --disc override
2026-08-19 22:13:35 -07:00
Matt Stanley 75bede67fc Merge pull request #160 from Alexbeav/feat/overlay-region-floor-env
runtime: PSX_OVERLAY_REGION_FLOOR override for streamed-over text ranges
2026-08-19 22:13:31 -07:00
Alex Vanderveen 29bbabb800 bump 2026-08-19 23:40:24 -04:00
tetrisgm 4f0b07b149 main/gpu: dual speed readout (vblank rate + internal render fps); menu aspect switch reverted
- 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>
2026-08-19 20:15:24 -07:00
Alex Vanderveen 3cb2a60dc3 bump 2026-08-19 22:50:13 -04:00
tetrisgm 1c9869cfa2 main: file-scope extern C decl for psx_ws_scene_wide_active
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 19:49:09 -07:00
tetrisgm 17a96507b4 main: extern C linkage for psx_ws_scene_wide_active
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 19:39:50 -07:00
tetrisgm 42109c580a present/audio: menu 4:3 with natural bars; SDL3 stream-rate fix; adaptive overclock removed
- 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>
2026-08-19 19:32:24 -07:00
tetrisgm 8ec702dae7 runtime: adaptive overclock pins guest time to wall; page-granular text-ok memo; tex_pack lookup memo; bezel seam removed
- 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>
2026-08-19 19:14:17 -07:00
Alex Vanderveen 4076b26dd3 bump 2026-08-19 22:03:29 -04:00
Alex Vanderveen 7bf316d330 bump 2026-08-19 21:46:38 -04:00
tetrisgm 8ebfdb4a3d perf: profile-guided round 1 — tex_pack prim bookkeeping, xlate scan, PGXP ALU gating
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>
2026-08-19 17:06:49 -07:00
tetrisgm 5b08379cbe main: PSX_SELF_PROFILE 1kHz IP sampler; emitters: PSX_ICACHE_FETCH macro in both preambles
- 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>
2026-08-19 15:54:31 -07:00
Henrique Guedes 20b9ba6cc8 runtime: swallow the overlay-closing press so it doesn't bleed into the game
Closing the save-state menu (Square save / Circle back / toggle key) or
the rewind filmstrip resumes the guest while the closing press is still
held on the pad, so the game samples it as gameplay input - saving a
state in a fighter throws a punch, and since the queued save executes
after the guest resumes, that punch gets baked into the snapshot.

Loads already solve exactly this with savestate_input_guard_arm(),
which feeds neutral pads until the held input is released (min 90 ms,
capped at 700 ms). Arm the same guard whenever either host pause loop
hands control back to the guest, covering every close path: save,
cancel/back, Esc, the toggle hotkey, and rewind accept/cancel.
2026-08-19 18:38:23 -03:00
tetrisgm 124f8cbe27 present/rewind: two 120Hz pacing fixes from the frame-path audit
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>
2026-08-19 14:21:50 -07:00
tetrisgm 3b6986827e recompiler: image-edge fallthrough must tail-transfer — the WipEout 3 static-bake boot exit root cause
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>
2026-08-19 13:55:50 -07:00
tetrisgm 6ead85c9da recompiler/runtime: honor IRQ redirects at compiled leaders; revive async-RFE latch; pc0 escape journal
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>
2026-08-19 10:39:34 -07:00
tetrisgm ada331a401 pgxp/gte: exact-sign NCLIP hardening — validate-on-read, shadow-silent replay, idempotent enable
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>
2026-08-19 10:39:10 -07:00
tetrisgm d9f31e1efc tex_pack: config-owned exclusions ([video] hd_texture_exclude)
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>
2026-08-19 10:07:30 -07:00
tetrisgm 2edce1b66f interrupts: never publish pc=0 from same-thread restore at top-level depth; exit-reg diagnostics
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>
2026-08-19 10:03:48 -07:00
tetrisgm 58c1f40276 Normalize wall-time windows for CRTC multiplier != 1 (x2 audit findings)
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>
2026-08-19 10:03:26 -07:00
Alexandros Mandravillis 4a30c5cc01 runtime: add PSX_OVERLAY_REGION_FLOOR override for streamed-over text ranges
Some titles stream level code over pages inside their own static text
range (Driver 2 streams mission code over its boot text), leaving those
pages permanently diverged: below the overlay floor they are excluded
from overlay capture and fall into single-instruction dispatch thrash
(hundreds of thousands of one-instruction dispatches per window).

PSX_OVERLAY_REGION_FLOOR lowers the floor per title so diverged text
pages become overlay-cache candidates and chain through local flow.
Value is clamped above the kernel window; default behavior is unchanged
when the variable is unset. On Driver 2 this took text-blocked
interpreter work from 278M to 37M instructions per window.
2026-08-19 16:47:15 +03:00
Alexandros Mandravillis af9202ff4e runtime: seed launcher initial disc from CLI --disc override
Passing --disc suppressed the remembered settings/disc.cfg disc for the
launcher's initial state (the has_disc_path gate) without seeding the
launcher itself, so the window opened with "No disc selected" and the
player had to re-pick the disc on every launch even though a valid disc
was named on the command line.

Seed resolved_disc from the validated CLI path before the launcher
window opens. resolve_disc_for_runtime still applies the override
authoritatively for the runtime after the launcher returns, so
precedence is unchanged.
2026-08-19 16:46:49 +03:00
Alexandros Mandravillis 91ea482b17 runtime: default string-inventory capture off, PSX_XLATE_CAPTURE=1 re-enables
The always-on Shift-JIS string-inventory scan walks every guest store
argument even when no translation table is loaded. Measured cost:
26-35% of whole-lane throughput on streaming-heavy titles (three GT2
race lanes; reproduced on Driver 2, where disabling it took one lane
from 19 to 43 vblank/s).

Capture now defaults off; PSX_XLATE_CAPTURE=1 turns the scan back on
for translation authoring. Substitution (armed table + language) is
unaffected either way.
2026-08-19 16:46:26 +03:00
Alex Vanderveen b7f3642725 psx-dual-link wipeout support 2026-08-19 09:12:06 -04:00
tetrisgm 45f9d0f866 autocompile: pin the overlay flavor through the spawn env (PSX_OVERLAY_FLAVOR)
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>
2026-08-19 02:56:27 -07:00
tetrisgm ae7925c1b5 sio: make the Ape Escape card-IRQ unstick pump opt-in ([runtime] ape_card_unstick)
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>
2026-08-19 02:18:27 -07:00
tetrisgm 4fc71ddf68 Present-skip pacing gate: query live instead of caching; floor bezel mark size
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>
2026-08-19 01:37:45 -07:00
tetrisgm 708ae95ae1 Present: never skip the swap when vsync owns cadence; bezel "mounted screen" treatment
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>
2026-08-19 01:27:59 -07:00
tetrisgm 548737941f Log pacing-mode transitions to stdout
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>
2026-08-19 01:09:45 -07:00
tetrisgm dcb649d555 Bezel: procedural padding shade + viewport-transition fade-in
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>
2026-08-19 01:09:17 -07:00
tetrisgm c6ad1a074a Bezel: fill letterbox bands (top/bottom margins), not just pillarbox columns
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>
2026-08-19 00:47:34 -07:00
tetrisgm d31953fe6b Fix overlay autocompile: missing cull-widen ABI symbols; PGXP hooks swallowed by #endif
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>
2026-08-19 00:30:29 -07:00
tetrisgm 249057f93f Widescreen precision: exact-sign NCLIP + GTE clamp rescue; backend-invariant entry hooks
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>
2026-08-18 23:30:17 -07:00
Alex Vanderveen 3172bd1f7a psx_link_shm: dead-side reclaim, exit release, generation re-pairing, no wall escape
- A claimed side whose owner's heartbeat is stale is a corpse: reclaim it
  and bump pair_gen so a surviving peer re-latches its epoch against the
  new process instead of the dead one's timeline. Fixes both launches being
  refused after earlier killed runs left present[] set (user-observed).
- atexit release marks our side free on clean quit; crashes fall back to
  the heartbeat reclaim.
- A fresh claim with no live peer resets rings/lines/pairing so every
  session starts on a clean wire.
- The nap loop's 3 s wall-clock give-up is gone: a live-but-stalled peer
  (loads, pause) is exactly when lockstep must wait; the escape let one
  side run 4M+ cycles past the bound. Peer death remains the only exit.

Headless secondary: PSX_HEADLESS=1 on the B instance (existing CI mode --
no window/audio, full guest) pairs and runs; validated with corpse-reclaim
scenario end to end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 20:29:43 -04:00
Alex Vanderveen 355605ffc0 psx_link 'shm': two-process serial link — full-speed link play
Each console is a complete single-console runtime in its own process (own
core, own window, own pad); the SIO1 cable is a pair of SPSC rings in a
shared-memory segment, paced by the bounded-skew barrier the fiber dual
mode validated. In-process dual halves the frame rate by construction; the
threads-in-one-process alternative needs ~590 device statics and 66 config
setters made per-machine. Two processes get each console a core for free
and upgrade to two PCs later by swapping the transport.

Design points (psx_link_shm.h):
- LINK-TIME: processes boot at different wall times, so due-cycles run on
  local-cycles-minus-epoch, epoch latched when the pair forms.
- Ops stay pure (psx_link.h determinism contract); wall clock only in the
  barrier poll (peer-death detection -> unplugged-cable semantics).
- Lookahead defaults to ~one PAL frame: below that, each side's ordinary
  pacer idle (clock frozen up to 8 ms/frame) stalls the peer -- measured
  0.57x at a quarter frame, 0.30x at the fiber-tuned 8192. Pair speed is
  min(A,B) by design; menus/races pair at 1.0x, phase-shifted intro FMV
  dips compound and that is correct lockstep behaviour.
- PSX_SIO1_BACKEND=shm PSX_LINK_SHM_ROLE=a|b selects; PSX_LINK_SHM_NAME,
  _LOOKAHEAD, _LATENCY tune. [runtime.link] backend="shm" also accepted.
- Savestate during link play serializes an empty wire (in-flight chars
  lost; handshakes retransmit). Cross-process co-checkpointing is future
  work.

Verified: two instances pair, exchange lines state, hold skew within the
bound, and run at min-of-pair speed with ~3s total barrier wait per 45s of
phase-shifted intro FMV; two UNLINKED instances hold 1.00x simultaneously
(16-core host), isolating all coupling cost to lockstep semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 20:19:33 -04:00
Alex Vanderveen b9b30a8ed4 bezel: fixed logo size anchored to window height, not margin width
One logo used to span 78% of the margin's width, so the mark grew with
every extra pixel of pillarbox -- enormous on ultrawide, a sliver on a
barely-wider-than-4:3 window. Anchor the tile to the window height
(default 18%, PSX_BEZEL_SCALE overrides), clamped to 78% of the margin so
narrow margins keep the old air-either-side look. A wider window now shows
more breathing room around the same-size column of marks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 19:49:48 -04:00
Alex Vanderveen a6f5dc1737 tex_pack: idle the upload tracker when replacement has no pack entries
hd_textures=true with no pack installed still paid per upload: mutex,
overlap invalidation, CRC32 of the rect, and a stored pixel copy. FMV is a
stream of MDEC macroblock uploads, so the empty-pack tracker showed up in
the stock FMV profile (tex_pack_on_upload -> invalidate_locked -> free).
tex_pack_active() now requires entries (or dump mode).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 19:38:58 -04:00
Alex Vanderveen b05075a534 text_xlate: keep the dispatch hook out of the hot path for fixes-only translation sets
g_apply_armed covers every apply mechanism, but the dispatch hook's only
apply work is string swapping + throttled in-place RAM patches; vram_patch
blocks apply through the VRAM upload hook. WipEout 3 ships a fixes-only set
(track palette gap), which left the hook armed with an empty string table --
the a0..a3 record scan hashed four registers per dispatch against a table
that could never hit, measured at 20% of a stock FMV run (top profile entry).
New g_table_n atomic + early-out when no dispatch-relevant work exists.
Stock intro: mean speed 0.83x -> 0.96x, half-speed samples 44 -> 23.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 19:15:13 -04:00
Alex Vanderveen 5c77c40550 depth24 bars: clear the row bitmap on the MDEC idle->streaming edge
The pre-movie publisher screens are 24-bit STILLS in the same depth24
session as the movie, so the depth-transition clear never separates them and
their rows leaked into the movie's letterbox bars (SCEA text strip
alternating with black under the Psygnosis FMV). A still does not stream
MDEC; a movie does — on the idle->streaming edge (10-vblank hysteresis so
low-fps movies do not flap) the written-row bitmap resets, turning
everything a still left behind into black bars while the movie re-marks its
own rows with every decoded frame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 19:00:44 -04:00
Alex Vanderveen 1f02ca1855 depth24: present never-written band rows as black — kills the pre-FMV splash flicker
ensure_cpu at depth24 entry made the letterbox bars TRUTHFUL, but the truth
is leftover 15-bit screen bytes (the pre-movie SCEA splash), and a
double-buffered movie holds that leftover at different rows in its two bands
— so the bars alternate content every couple of frames. Track which rows
have received a CPU->VRAM rect since the current depth24 session began
(s_d24_row_written, cleared at every depth transition) and present unwritten
rows as black in gpu_depth24_present_row. Savestate restore marks all rows
(restored VRAM is authoritative); reset clears.

First frame caveat: a title that uploads frame 1 before flipping GP1 depth24
shows one black movie-frame interval instead of the splash remnant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 18:57:23 -04:00
Alex Vanderveen 4982e44421 Skip FMVs: settings-owned, offered in recomp-ui/retcomm-launcher
skip_fmv_offered flips to true: [video] auto_skip_fmv in settings.toml now
actually drives g_auto_skip_fmv (the clamp that zeroed it is retained only
for a future offered=false profile). recomp-ui's Skip FMVs row and
retcomm-launcher's checkbox already read/write the key end to end — the
runtime was the only link refusing the value.

The builtin psx.skip_fmv activation-plugin mod (and its
psx.enhancement.skip-fmv package) is removed: one feature, one control. A
state.toml still naming the package resolves it as unknown and is skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 18:51:41 -04:00
Alex Vanderveen 37a6e19df8 stock smoothness: cached vblank period, PAL-on-60Hz auto frame interpolation, PSX_FMV_INTERP
- gpu_vblank_period_cycles() is consulted per basic-block edge and profiled at
  ~3% exclusive recomputing a constant; now cached, refreshed at its four cold
  writers (GP1 display mode, CRTC multiplier, period override, snapshot parse).
- PAL content through 60 Hz driver vsync duplicates ten frames a second; when
  the user has not set frame_interpolation, default it ON for PAL discs on a
  ~60 Hz panel. PSX_FRAME_INTERP=0|1 overrides for A/B.
- PSX_FMV_INTERP=1 keeps interpolation running through FMV/MDEC phases (the
  unconditional suspension is what makes 25 fps PAL video judder).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 18:24:57 -04:00
Alex Vanderveen 85d167d559 Merge PR6 psxrecomp head (e27eac7) into feat/rbengine
Conflict resolutions:
- boot_state.h: section-tag collision at 0x11 — master shipped BS_SEC_SIO1
  there (all v5 states + dual-console switch blobs), so the branch's
  BS_SEC_TEXPACK/BS_SEC_MODSET renumber to 0x12/0x13. PR-branch dev states
  are the only casualty.
- boot_state.c: adopt the branch's backpatched true section count (placeholder
  + write_section_raw counter), which subsumes master's constant-minus-
  exclusions arithmetic; keep master's section-exclude guards on CPU/RAM
  writes and the branch's MODSET-first mod-plan guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 17:33:50 -04:00
Alex Vanderveen 12cac92d4b pgxp emitter newline invariant; dual CPU banking + SR/CAUSE re-registration; PSX_DUAL_CONSOLE=0 veto fix
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 17:31:21 -04:00