Commit Graph

966 Commits

Author SHA1 Message Date
Alex Vanderveen d04f4fed73 lan mods: say WHY a MODOK report was dropped
A host whose seat never goes ready showed 'LAN rx MOTK5 MODOK' every 2 s and
nothing else, while the guest resent forever because the host's echo never
agreed. The MODOK handler has three early exits before its own trace — two
parse failures and a failed ae_np_read_lan_state — so the one case the trace
cannot explain is precisely the one that reaches none of it.

Name the reason on each, rate-limited to 3 s. The read-lan-state case also
points at the likely cause: ae_np_lan_file() is resolved against the process
working directory, so a host launched from a different cwd than the one that
wrote netplay_lan_lobby.txt reads nothing and rejects every peer report.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoSNLFj7kvarYHEV9PCZpV
2026-08-21 17:49:00 -04:00
Alex Vanderveen c445f75ce4 main/mod_plugins: backport psx_mod_set_bezel activation API
The title's bezel activation plugin (src/mods/wipeout3_bezel_plugin.c) calls
psx_mod_set_bezel(), which does not exist on this branch, so any build of the
game repo against it dies with 'call to undeclared function psx_mod_set_bezel'.

The renderer-side bezel plumbing and g_bezel_path already exist here; only the
mod-facing setter was missing. Straight backport of the setter and its header
declaration — no renderer changes, and the bounds checks are kept so a plugin
cannot hand an unbounded path to the loader.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoSNLFj7kvarYHEV9PCZpV
2026-08-21 16:48:00 -04:00
Alex Vanderveen 073114e93f mod_packages: an unevaluable image pin is not a target mismatch
An exported/release bundle ships no loose PS-X EXE, so exe_sha256 resolves
empty; comparing a manifest pin against "" rejected every image-pinned
package on exactly the installs players use — 'package does not target this
game/image: wipeout3.enhancement.framerate' — while the same package applied
fine in a dev checkout that happens to have the file. Skip a pin we cannot
evaluate and let the expected-byte guard at patch time verify; a pin we CAN
evaluate is still enforced.

Backport of the same fix from 225283a7 onto this branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoSNLFj7kvarYHEV9PCZpV
2026-08-21 16:35:04 -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
Alex Vanderveen 3cb2a60dc3 bump 2026-08-19 22:50:13 -04: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
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
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
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
tetrisgm e27eac7366 diagnostics: per-prim PGXP census, texcorr-miss census, live overclock; xlate fixes-only files
- 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>
2026-08-18 13:54:01 -07:00
tetrisgm 18272616a1 savestate: dynamic section count; refuse cross-mod-config loads; tex_pack section
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>
2026-08-18 13:53:46 -07:00
tetrisgm bd3f900213 ws_ui_group: temporal anchor hysteresis for blinking HUD widgets
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>
2026-08-18 13:53:46 -07:00
tetrisgm c2c426424c pgxp: size the shadow to live RAM on every enable; carry depth across rebuilds
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>
2026-08-18 13:53:23 -07:00
Alex Vanderveen 24fb4a2f0f bump 2026-08-18 15:29:44 -04:00
Alex Vanderveen 5b1f53984d chore: sync 2026-08-18 12:44:35 -04:00
Alex Vanderveen 774865aad9 wipeout mod support 2026-08-18 12:07:30 -04:00
Alex Vanderveen cb3bd493fc chore: sync 2026-08-18 08:49:24 -04:00
Alex Vanderveen 365662d823 chore: sync 2026-08-18 08:47:06 -04:00
tetrisgm 71e77f91cd boot_state: skip unknown sections instead of failing the whole load
`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>
2026-08-18 00:00:07 -07:00
tetrisgm dc00e5a1e8 tex_pack: position-exact dedup; savestate-section groundwork (dormant)
DEDUP: a re-upload of identical content at a DIFFERENT rect was deduped by
hash+size, keeping the stale rect -- origins computed from it were off by the
position drift. Dedup now requires the same position; the origin/src handed to
the renderer come from the live containing upload. NOTE: this did NOT cure the
strip vertical offset (top row cut, bottom duplicated) -- that symptom persists
on screen and its root cause is still unidentified. The change stands on its
own correctness, not as that fix.

SAVESTATE SECTION (BS_SEC_TEXPACK): written and applied correctly in isolation
(telemetry showed 1360 entries rebuilt from the state's own VRAM), but with the
section present the overall state load FAILS in a LATER section, cause not yet
isolated -- and old builds hard-reject unknown tags (`default: return 0`), so
the section also breaks backward compatibility. The boot_state wiring is
REVERTED; tex_pack_state_bytes/write/apply remain, dormant, for whoever picks
this up with the loader diagnosis. Until then: after loading a savestate,
textures the game does not re-upload stay low-res -- known limitation again.

The first-save defer fix verified working in passing: "deferring slot 4 -- no
safe resume PC (hint=0x00000000)" followed by a save at a real PC.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:58:03 -07:00
tetrisgm 1469550ce5 Two root causes: preserve mod state across rebuilds; never save a fabricated PC
STAGING (runtime.cmake): the framework catalog staging ran rm -rf on the whole
build-dir mods/ tree before recopying. But both catalogs own only a packages/
subtree -- mods/state.toml is USER STATE written at runtime, recording which
mods are enabled with which options. Every rebuild therefore silently disabled
every mod: 8 MB, widescreen, framerate reverting off again and again, which
presented as "the mods regressed" (aspect ratios stopped working, the game
fail-fasted on 0xB0 without its 8 MB). Scope the clear to mods/packages; the
deleted-mods-don't-survive guarantee is intact because that is all the catalogs
ship.

SAVESTATE (savestate.c): a save polled with resume_pc hint 0 substituted a
candidate from a chain ending in sticky-BB latches and $ra -- values that pass
the sanity check while being the wrong place to resume. The first save of a
session (hint not yet published) wrote such a state; it loaded, ran minutes off
the rails, and died at PC=0. Poison that looks valid at save time. hint==0 now
routes into the existing defer machinery: the save completes at the next poll
with a published block-leader PC, or fails loudly after the timeout. Verified:
first save of a fresh session now records pc=0x8015FAEC, no fallback line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:26:39 -07:00
tetrisgm 686ccd3c00 Revert the hold-capture throttle: it never helped
Measured properly this time, by disabling the capture outright rather than
comparing two runs:

    hold capture ON   present 18.49 ms
    hold capture OFF  present 18.86 ms

Within noise. The full-drawable copy is not the present cost, so throttling it
bought nothing and only made the held frame staler.

The earlier commit claimed "present_gpu_ms_avg 18.192 -> 3.853, a 4.7x cut".
That was not a fix. The two numbers came from different screens -- a busy one
before and a quiet one after -- and I attributed the difference to my change
without isolating it. Capturing every frame is restored.

Whatever the ~18 ms in present actually is remains unidentified. It is not the
hold capture and it is not scene rendering (5-9 ms separately accounted).

Also registers runtime/tests/test_host_refresh_sanitize.py, which arrived
unregistered in the feat/rbengine merge and blocked cmake configure via the
test-registration guard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:56:36 -07:00
tetrisgm d7e074a11d gl: stop re-capturing the whole drawable every frame
hold_capture_drawable did a full-drawable glCopyTexSubImage2D on the present
path, every frame. At 7680x4039 that is ~31M pixels, roughly 124 MB of copy per
frame, and it measured as most of the present cost at 8K internal.

It buys almost nothing at that rate. The held frame is only ever shown while the
emulator is STALLED or PAUSED -- the stall tick and the rewind pause are its
only two consumers -- where it is a still image. Capturing every 8th frame
leaves it at worst 8 frames stale, tens of milliseconds on a frozen picture, and
cuts the cost by 8.

Measured on the same scene, 8K internal, 7680-wide window:
    present_gpu_ms_avg   18.192  ->  3.853

The remaining frame cost is elsewhere: emu_cpu 42.3ms at 900% overclock and
scene_gpu 38.5ms at 8K. Those are the real walls, not the present.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 21:50:57 -07:00
tetrisgm 30652ac334 cpu: overclock by scaling the per-instruction cycle charge
The guest runs as native code, so "faster CPU" cannot mean "execute quicker" --
it means CHARGE LESS for the same work. Every device schedules off
psx_cycle_count and the CRTC fires VBlank on a fixed cycle period, so scaling
the per-instruction charge down by N lets the CPU complete N times the work
inside one frame while timers, SPU, CDROM and the refresh rate keep their
real-world rates. Scaling the VBlank period instead would slow everything
EXCEPT the CPU, which is backwards.

Q16 fixed point with a carried remainder, not a divide: this is the hottest path
in the runtime and integer division of a 1-2 cycle instruction cost would floor
to zero and lose all time. The accumulator keeps the total charge exact instead
of drifting by the truncation on every call. Stock (65536) short-circuits, so a
non-overclocked build pays one predictable compare.

[video] cpu_overclock, percent, 100 = stock. Needed because hueponik's
pal100full8 requires >900%.

STATUS, measured, not assumed: 100% and 300% boot and run. 1000% does NOT reach
a display -- and it is neither hung nor host-bound, frames advance steadily at
~39/s (455 -> 2574 over 54s) while the guest never enables the display. So the
guest's own boot path stalls at that ratio; the mechanism moves time correctly
but something in the boot sequence does not survive the CPU running 10x ahead of
its devices. Not yet isolated, and it is the blocker for pal100full8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 21:28:23 -07:00
tetrisgm f931893336 present: standardise the bezel gap across every mark
Padding was 10% of each axis, so the vertical gap scaled with each logo's own
height. These marks run from 1024x479 to 1024x997, which made the spacing
visibly different per team -- tight under the tall ones, loose under the wide
ones.

Both axes now pad by the same fraction of the WIDTH. Tiles are scaled so one
spans a fixed share of the margin width, so a pad measured in width units lands
as the same on-screen gap horizontally and vertically, and the same gap for
every mark regardless of its proportions. The logo is never distorted -- it is
centred in a larger transparent cell, and the tiling uses the padded cell's
aspect.

Doubled to 10% per side (20% between neighbours) per preference.

Verified with piranha.png (1024x509, the widest): vertical spacing now matches
horizontal, where before it was noticeably tighter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 21:12:34 -07:00
tetrisgm b2f504cd19 present: 10% spacing between bezel tiles
Baked into the texture rather than applied at draw time: GL_REPEAT tiles edge to
edge and has no notion of spacing, so the gap has to exist in the image. 5%
transparent margin on each side leaves 10% between neighbours, horizontally and
vertically. Padding both axes by the same fraction preserves the aspect, so the
tiling maths is unchanged.

Falls back to the unpadded image if the allocation fails, rather than dropping
the bezel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 21:10:10 -07:00
tetrisgm c33d576537 present: fix bezel tile aspect, and give the margins a backdrop
Two defects in the tiling from the previous commit.

ASPECT. The vertical repeat count DIVIDED by the image aspect where it needed to
multiply. A tile is (vw/tile_w) wide on screen, so it must be (vw/tile_w)/img
tall and the count is vh over that -- i.e. img * tile_w * vh/vw. Getting it
backwards squashed every mark by img^2, about 1.7x on these 1024x778 logos,
which is the stretched look. Circles are round again.

BACKDROP. The art is RGBA with a transparent field, so the gaps between tiles
showed whatever the drawable had been cleared to: black, which read as a hole
punched in the screen rather than as part of the design. Each margin is now
cleared to a colour derived from the art -- the alpha-weighted mean of its
opaque pixels, darkened -- and the mark is alpha-blended over it, so every team
brings its own colour. Alpha weighting keeps soft edges from dragging the mean
toward whatever the file pads its transparent regions with.

Verified at 7680x4039: "random" picked feisar.png, both margins carry round
tiled marks on a dark teal field.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 21:08:30 -07:00
tetrisgm 2c6de285d0 present: tile the bezel per margin, with team selection and a random default
Follows the earlier bezel work. Two changes to how the art is used:

TILING, per margin. A margin is tall and narrow; stretching one logo to fill it
is grotesque and cover-cropping shows a sliver. Tiling keeps every copy at its
authored proportions and fills any margin size, which is what a repeating mark
is for. The count derives from the margin WIDTH, so the logo reads at the same
size regardless of resolution or window shape, and GL_REPEAT does the work.

SELECTION. [video] bezel now takes:
    "random"   one of the bundled team logos, chosen once per launch and kept
               for the session -- a mark that changed mid-run would read as a
               glitch, so the choice is made at load and held;
    "qirex"    that team, by name;
    a path     your own image, relative to the disc directory;
    "off"      disabled.
Bundled art resolves next to the executable, so a stock install works without
touching the disc folder.

SVG was the obvious choice for crisp marks at 8K, but rasterising it needs
cairo, which is a native dependency a normal install will not have. The 1024px
PNGs are ample for a tiled mark and keep this working everywhere with nothing
to install.

Verified on a 7680-wide maximised window: "random" selected icaras.png and both
margins carry the tiled mark with the frame untouched between them.

Artwork: toolboxio (deviantart.com/toolboxio), re-vectored from in-game images
and released publicly; no formal licence attached. Collected via
awesome-wipeout.github.io. Credits ship alongside the images.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 21:00:04 -07:00
tetrisgm 4210c764b1 present: bezel artwork behind the frame ([video] bezel)
A still image drawn behind the game, filling whatever the letterbox or
pillarbox leaves over -- the trick vertical shmups use on horizontal displays,
where the dead space carries artwork so it reads as designed rather than as a
limitation. MAME calls the same idea bezel artwork.

Drawn across the whole drawable before the game quad, which is simpler and
safer than filling two margin strips: no margin arithmetic, it covers pillarbox
and letterbox alike, and the frame paints over the centre.

The reason this is the right shape and present_edge_fill was not: edge fill
stretches one edge COLUMN of the live frame across the margin. That suits a flat
menu backdrop and turns track geometry into horizontal smears, so it needs to
know whether the current frame is a menu -- a question it answers with
force_4_3, which is only ever set by the widescreen path. A still image has no
such question. It looks the same in a race and in a menu, which is the point.

Loaded after the GL context exists (the first attempt sat next to tex_pack_init,
which runs before the window, so g_gl_active was false and it silently never
loaded). Decoded with stb_image, already linked for the HD texture pack. A
relative path resolves against the disc directory, matching the pack convention,
so artwork can sit beside the disc.

Verified on a 7680-wide maximised window with 4:3 content: margins that were
pure black now carry the image on both sides, with the frame untouched in the
middle.

Known limits: the image is stretched to the drawable, so an aspect that does not
match the window will distort (cover-crop is a small follow-up), and this has
only been exercised in 4:3 pillarbox, not with the widescreen mod active.

For WipEout 3 the team wallpapers by MotorcycleEmptiness
(awesome-wipeout.github.io) suit this exactly; the shipped default is a
placeholder gradient used to verify the path, not final art.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 20:45:54 -07:00
tetrisgm 471ad4bf0e Revert "gl: fill pillarbox margins on ANY pillarboxed present"
The force_4_3 gate was not redundant. It is the proxy for "this frame is a menu
or backdrop", and present_edge_fill only makes sense there: it stretches ONE
edge column across the whole margin, which is fine for a flat backdrop and
disastrous for busy content. The function's own comment says so -- "wrong for
content whose edge pixels are busy" -- and I removed the gate anyway on the
grounds that the early-outs made it redundant. They gate on the feature flag and
on margins EXISTING, not on the frame being suitable.

In a race the edge columns are track geometry, so the margins filled with
horizontal smears of stretched scenery down both sides. Strictly worse than the
black bars it replaced.

The underlying complaint stands: in plain 4:3 with no widescreen mod, menu
backgrounds do not fill a wide window, because force_4_3 is only ever set by the
widescreen path. Fixing that properly needs a real "is this frame a flat
backdrop" test -- e.g. the edge column being near-uniform -- rather than
removing the only thing currently distinguishing menus from gameplay.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 20:25:42 -07:00
tetrisgm cb6c2926b9 gl: fill pillarbox margins on ANY pillarboxed present, not just 4:3-pinned ones
present_edge_fill was called only under `if (force_4_3)`, which means "a
widescreen game is pinning this frame to 4:3". A plain 4:3 game in a wide window
never satisfies it, so the margins stayed black while the feature reported
itself armed -- ws_menu_edge_fill answered on:1 and did nothing.

The condition was redundant as well as wrong: present_edge_fill already returns
early unless the feature flag is set AND there are margins to fill, so dropping
the gate cannot make it run anywhere it would not have been wanted.

Measured on a 7680-wide maximised window with 4:3 content: margins were
1152 px of pure black on each side, now carry the frame's own edge colour
across the full width.

This got much more visible once the window started maximising, because a 1280
window on an 8K panel had barely any margin to notice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 20:23:28 -07:00