Commit Graph

953 Commits

Author SHA1 Message Date
Matt Stanley 6abb113c59 Merge pull request #184 from mstan/split/pr169-window-dpi-maximize
sdl: make window startup DPI-aware and maximize fit-to-display
2026-08-24 10:20:44 -07:00
tetrisgm 16b439876c sdl: maximise the window instead of computing the frame size
clamp_window_aspect fits the CLIENT area to the usable bounds, but a window is
client plus title bar and borders. On a display whose usable height it exactly
matched, the result was a window taller than the screen: client 5504x4128,
window 5550x4263, positioned at y = -112 and hanging off the top.

Deriving the decoration size first does not work either. SDL_GetWindowBordersSize
reports nothing useful before the window has been shown, so a post-creation
correction based on it silently did not apply -- the window stayed exactly as
wrong, which is worse than not trying.

The window manager already solves this exactly, so ask it: SDL_MaximizeWindow
fits the work area with decorations and taskbar included. Measured on a
7680x4320 panel, client area is now 7680x4039 against 5504x4128 before.

Only when the width was not explicitly chosen -- an explicit window_width is a
deliberate decision and is left alone.

Suggested by the user after two failed attempts at doing the arithmetic here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit d6d0946703)
2026-08-23 13:17:48 -07:00
tetrisgm 9ad208c34a sdl: declare per-monitor DPI awareness before SDL_Init
Without it Windows virtualises everything the process sees. On a 7680x4320
panel at 400% scaling SDL_GetDisplayUsableBounds reports 1920x1032, so
clamp_window_aspect fits the window to roughly 1376 LOGICAL pixels and it opens
as a small box that the desktop compositor then upscales.

The internal render resolution is unaffected, which is what makes this
expensive rather than cosmetic: the game renders at supersampling 16 and the
result is thrown away scaling a 1376-wide window up to an 8K display. Choosing a
high internal resolution and getting a small blurry window is the exact opposite
of the intent.

permonitorv2 makes SDL report physical pixels, so the window sizes against the
real panel and the drawable matches it 1:1. Set before both SDL_Init call sites,
guarded on the hints existing so it builds against older SDL.

Also changes g_video_win_w's default from a hardcoded 1280 to 0, meaning "fit
the display", clamped to the usable bounds and the configured aspect. An
explicit width still wins. 1280 was a reasonable default when a window was a
window; it is not one on a 4K or 8K panel.

Diagnosed after reporting the display as 1920x1080 from a non-DPI-aware query
and telling the user their panel could not be larger. It is 7680x4320; the
measurement was virtualised, not the display.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 236778ed7b)
2026-08-23 13:17:09 -07:00
tetrisgm e301a57279 debug: expose savestate completion receipt
(cherry picked from commit c7a3d59e1a)
2026-08-23 13:12:47 -07:00
Matthew Stanley 2cbbbfc4da observability: window traps that read as results, and a per-game watch list
Three related defects in the interp observability rings. All three share a
failure mode that is worse than an error: the tool reports an empty result
that is indistinguishable from a real finding. "total: 0" reads as proof the
events never happened.

1. callret_watch armed on `lo != 0`, making address 0 an off switch rather
   than a legal window floor. `lo=0 hi=0x200000` — the obvious spelling of
   "watch the whole address space" — replied ok and recorded nothing.
   Measured on one build, identical but for the floor:

       lo=0x00000000 hi=0x00200000  ->  total 0
       lo=0x00000004 hi=0x00200000  ->  total 468732

   Four bytes of window between an empty ring and half a million call
   resolutions. This cost real time during beads-eio.3.59: an
   interp->compiled call route was briefly believed absent when the ring was
   simply never on.

   Arm on a NON-EMPTY window (hi > lo) instead. `lo=0` is now an ordinary
   floor. Note that `{"lo":"0"}` was the DOCUMENTED disarm spelling
   (TCP_COMMANDS.md), so that contract is kept explicitly rather than broken:
   lo=0 with no hi still disarms, and now says so in the reply. Added
   `disarm=true` as the unambiguous spelling. A `lo` with no `hi` is refused
   outright instead of silently inheriting the previous ceiling (usually 0,
   i.e. an empty window) — that was the second way to arm nothing by accident.

2. s3_smear_watch had the same `lo`-nonzero gate, so lo=0 with a real hi
   recorded nothing while answering ok. Same fix. It already cleared `hi`
   whenever `hi` was omitted, so `{"lo":"0"}` naturally lands on an empty
   window and still disarms — no special case needed there.

   Both commands now report `armed`, and callret_watch's dump carries `armed`
   plus the active window, so an empty read can always be told apart from an
   unarmed one.

3. The xprobe JAL/JALR watched-target list was six guest addresses from two
   specific titles COMPILED INTO the shared runtime (an MMX6 card-load flow
   and a Tomba target). Every other game therefore watched nothing, silently:
   the `watched` dump came back empty and gave no hint that the build was
   incapable of watching anything. Same defect class as beads-eio.3.21, and
   the direct reason the JAL call site was unusable while measuring
   beads-eio.3.59 — only the JALR callret ring could be used there.

   The list is now configured, never compiled in: `xprobe_watch` TCP command
   to get/set, or PSX_XPROBE_WATCH in the environment so targets are watched
   from instruction zero without a rebuild. Default EMPTY — no title is
   privileged — and a read with nothing watched says so rather than looking
   like a negative result. Matched on the normalised address so KSEG and
   physical forms both work.

Verified against Ape Escape, headless, RelWithDebInfo:
  - PSX_XPROBE_WATCH=0x8002C0E4 -> watching that target from boot
  - xprobe_watch targets=<two addrs> -> count 2; the `watched` dump then
    carries 41 JALR records for 0x8002C0E4 and 21 for 0x8002C14C, with full
    frame/cycle/src/sp/ra context. This title could previously watch NOTHING.
  - xprobe_watch targets= (empty) -> count 0 plus an explanatory note
  - callret_watch lo=0 hi=0x200000 -> armed true, total 77484
  - callret_watch lo=0            -> disarmed, legacy spelling named
  - callret_watch lo=0x40000      -> refused, window left unchanged
  - callret_watch disarm=1        -> disarmed
  - s3_smear_watch lo=0 hi=0x200000 -> armed true (previously silent)
  - pre-fix build, callret lo=0 hi=0x200000 -> plain ok, total 0 (the trap)

TCP_COMMANDS.md regenerated for the new command. Note the regen also picks
up rows that had drifted on master beforehand: `--check` already failed on
pristine master (index said 292, the servers register 304), which is why the
count line jumps 292 -> 305 rather than by one.
2026-08-23 11:57:33 -07:00
Matt Stanley cf918c7be5 Merge pull request #171 from rickguedes/fix/pgxp-hook-after-endif
pgxp: emitted hooks after muldiv land on #endif lines and get discarded (+ link iphlpapi on Windows)
2026-08-22 00:59:32 -07:00
Matt Stanley 13cf125554 Merge pull request #170 from ClaudioVarandas/fix/spu-pitch-zero-holds
fix (spu): a pitch of 0 holds the voice instead of playing at full speed
2026-08-22 00:59:22 -07:00
Henrique Guedes aebb11b3d8 build: link iphlpapi into Windows runtime targets
main.cpp includes iphlpapi.h and calls GetAdaptersAddresses for the
launcher's netplay local-address discovery, but no CMake file links the
library. MinGW builds link regardless; MSVC does not link iphlpapi
implicitly, so every MSVC runtime build fails at link with an
unresolved GetAdaptersAddresses.
2026-08-21 21:39:12 -03:00
Claudio Varandas c35fea6b25 spu: cover a pitch of 0 with a test
The coercion this branch removes had stood since the original SPU commit with
nothing to catch it: no test touched a pitch of zero, and accuracy/axis5_spu.md
lists pitch as implemented without mentioning it.

Four cases, staged directly against spu.c the way
test_spu_end_without_repeat.c does:

  - pitch 0 holds -- phase, sample index, cur_addr and the emitted sample all
    unchanged across 500 output samples, and the voice stays active
  - a held voice does not walk into the blocks that follow it, which is the
    shape the bug took in practice
  - pitch 0x1000 still decodes the next block after 28 samples, and 0x0800
    advances half as fast, so the test cannot pass by the voice being dead or
    the accumulator being treated as a switch
  - the envelope still steps while the voice is held, and a key-off still
    releases and retires it -- holding the sample must not trade one stuck
    voice for another

Three consecutive blocks are staged with different sample data so an incorrect
forward walk changes the output audibly instead of reading more of the same.

Checked in both directions: 14/14 pass with the fix, and reintroducing the
coercion fails exactly the five checks that describe it.
2026-08-21 23:34:25 +01:00
Claudio Varandas 69da235063 spu: a pitch of 0 holds the voice instead of playing at full speed
VxPITCH is added to a 12-bit fractional phase accumulator, so a pitch of zero
means the sample counter does not advance and the voice holds its current
sample. spu.c coerced zero to 0x1000 instead -- full-rate 44.1 kHz playback --
so a voice the game had parked streamed forward through SPU RAM, decoding
whatever followed it as ADPCM. DuckStation and Beetle both just add the pitch
to the counter, so zero holds there.

Alien Resurrection (SLUS-00633) parks its ambience when the pause or save menu
opens: pitch 0, envelope left frozen mid-sustain, no key-off. Measured in the
menu, both voices of the hard-panned ambience pair read pitch 0x0000, env
0x5FFF in sustain, zero key-ons over five seconds -- and cur_addr at 0x49160
and 0x49210 against a repeat address of 0x489E0, about 0x790 bytes past their
own loop point and still advancing. A voice with pitch 0 that advances at all
is the defect. The menus turned into a continuous mid-band buzz, 79-82% of the
output energy between 500 Hz and 2 kHz, which is what garbage ADPCM sounds
like. Removing the coercion silences them; verified in play.

The audio pipeline was healthy throughout -- zero underruns, zero overflow
drops, buffer at target -- because nothing was wrong with it. The SPU rendered
faithfully what it had been told to render.

Safe by construction: with phase not advancing the block-step loop simply does
not run, adsr_run still steps the envelope every output sample, and a later
key-off still releases and deactivates the voice.

The line dates to 9b332ba, the original SPU implementation. Nothing documents
it, accuracy/axis5_spu.md lists pitch as implemented without mentioning it, and
no test covers a pitch of zero.
2026-08-21 23:21:42 +01:00
Matt Stanley 1dc58357a9 Merge pull request #167 from Alexbeav/fix/full-range-straightline-handoff
fix(runtime): validate full AOT entry before straight-line handoff
2026-08-20 20:44:17 -07:00
Matt Stanley 60465617a2 Merge pull request #166 from rickguedes/fix/spu-voice-address-alignment
spu: voice start/repeat addresses ignore bit0 (16-byte-aligned fetch)
2026-08-20 15:18:41 -07:00
Matt Stanley 43fb799c3f Merge pull request #163 from rickguedes/fix/load-delay-pgxp-scope
autocompile: PGXP load-delay shard fix, host-time capture pacing, wedge visibility
2026-08-20 14:29:11 -07:00
Matt Stanley d56bd0247c Merge pull request #165 from rickguedes/fix/cdrom-cdda-report-irq
cdrom: CD-DA position report interrupts (Setmode bit2) during Play
2026-08-20 11:48:01 -07:00
Alexandros Mandravillis 73eb746479 fix(runtime): validate full AOT entry before straight-line handoff
The dirty-RAM interpreter reused a suffix-aware validity check when resuming generated code after a patched call site. If an entry prologue had been overwritten but its resume suffix still matched, execution could enter stale AOT code and fall through to another generated piece without redispatch.

Emit a whole-entry predicate for straight-line handoffs, retain suffix checks for explicit guest transfers, and fail closed for older generated dispatchers until regeneration.

Validated with the MGS PAL 6,875-frame route, manual codec-to-heliport gameplay, focused continuation-guard tests, and a current-upstream recompiler build.
2026-08-20 14:51:26 +03: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
Rick Guedes 537b4e182d spu: voice start/repeat addresses ignore bit0 (16-byte-aligned fetch)
The ADPCM start and repeat address registers count 8-byte units, but
hardware ignores bit0 — a voice's block fetch is always 16-byte
aligned. DuckStation implements exactly this (Voice::KeyOn,
adpcm_start_address & ~1u, and the same masking on the repeat-address
jump); Beetle aligns identically. psx-spx documents the 8-byte units
and is silent on the low bit.

Honoring the raw bit makes a voice keyed at an odd 8-unit address
decode from mid-block: sample bytes get parsed as headers, typically
hitting a spurious END+REPEAT within a few blocks. Twisted Metal III
(SCUS-94249) depends on the alignment — its sound-bank tables produce
odd start addresses (samples are packed at 8-byte granularity) whose
real streams begin at the aligned twin. Unaligned, its explosion
one-shots collapsed into ~10 ms looping blips; offline ADPCM decode of
the aligned twin recovers the full 1.3 s sample, and in-game audio
matches a reference emulator after the fix.

Masked at the two intake points: the KEYON latch of the current and
repeat addresses, and the live repeat-register write.
2026-08-19 21:01:06 -03:00
Rick Guedes b919f2a401 cdrom: CD-DA position report interrupts (Setmode bit2) during Play
The controller never generated position reports, so a driver that
mutes, starts Play in report mode, and demutes only once the first
report confirms playback waits forever with CD audio muted.

Per psx-spx "Command 03h - Play": INT1(stat, track, index, mm/amm,
ss+80h/ass, sect/asect, peaklo, peakhi), sent on every 10th frame of
absolute time — asect BCD 00/20/40/60h carry absolute MSF, 10/30/50/70h
track-relative MSF with bit7 set on the seconds byte. Peak bytes hold
the sector's per-channel peak level, measured pre-mute (the controller
keeps processing audio while muted); the L/R toggle bit15 is
effectively stuck on PSX and stays 0. A report that would collide with
an unacknowledged IRQ is dropped — hardware does not queue reports.

Verified on Twisted Metal III (SCUS-94249), whose CD-DA music driver
uses exactly this handshake: with reports flowing it demutes on the
same frame its Play command lands, and all 36 soundtrack tracks play.
2026-08-19 20:59:54 -03: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
Henrique Guedes b46e77911b runtime: report which gate autocapture is parked on
A latched overlay_autocapture_tick() gate is invisible from outside:
triggers, futility and pressure counters simply stop moving, which
reads as "nothing to do" while in reality no new overlay variant will
ever be captured again and every freshly loaded RAM image stays
interpreted for the rest of the session. Observed live: a full disk
made the capture manifest write fail and requeue forever, and the only
outward symptom was the game getting slower with every new area.

Record which gate the tick last returned at and when the last pressure
sample completed, and surface both through autocompile_status as an
additive "autocapture" object: wedged (enabled yet no sample for 30 s),
gate, ms_since_sample, and the raw write-job/provider state. No
behavior change — observability only.
2026-08-19 17:07:12 -03:00
Henrique Guedes bbdf7bb01f runtime: pace autocapture gates on host time as well as vblanks
overlay_autocapture_tick()'s pressure-sample and cooldown gates counted
only emulated vblanks (120 / 300 frames, ~2 s / ~5 s at full speed). The
situation autocapture exists to fix — a fresh RAM image with no native
coverage, running in the interpreter — is exactly the situation where
vblanks stop arriving, so the recovery loop was throttled by the very
stall it should end: at an observed 0.26 fps, one pressure sample every
~8 minutes of wall clock and one capture fire every ~19.

Give both gates host-time twins at their 60 Hz equivalents
(AUTOCAP_CHECK_MS 2000, AUTOCAP_COOLDOWN_MS 5000) and open each gate on
whichever budget expires first. At full speed the budgets coincide, so
healthy-title cadence is unchanged; behavior differs only while the
guest runs slower than real time. The futility backoff multiplies the
host cooldown exactly as it multiplies the frame cooldown.
2026-08-19 17:06:03 -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 1181c9c9e9 fix(runtime): restore missing brace after present-cadence log
The vsync hotfix replaced the closing brace of the non-headless SDL
init else with log_present_cadence(), leaving main.cpp unbalanced.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-16 18:08:58 -04:00
Alex Vanderveen 353da99795 Merge origin/master into feat/vsync_hotfix.
Keep vsync/pacer XOR and skip the full-period wait while manual fast-forward already paces at the turbo multiplier.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-16 14:01:35 -04:00
Alex Vanderveen 0a911fed53 Stop vsync and the wall-clock pacer from double-blocking presents.
Linux compositors wait a full refresh on SwapBuffers even after the pacer has already slept ~16.7 ms, which locked NTSC titles at 30 Hz / 0.50x with the CPU idle. Vsync now owns cadence only on ~60 Hz panels; otherwise the pacer holds 59.94 Hz and swap interval is forced to 0.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-16 13:13:02 -04:00
Matthew Stanley ad55fecdcf Gate WipEout local viewport native-wide to split frames 2026-08-15 22:53:24 -07:00
Matthew Stanley 41c232fb47 Fix precise NCLIP on PGXP-backed GTE shadows 2026-08-15 21:46:47 -07:00
Matthew Stanley bdd7ade6e8 Merge remote-tracking branch 'origin/master' into codex/netplay-local-viewport 2026-08-15 21:40:56 -07:00
Matthew Stanley c18d820fb8 Correct split viewport projection aspect 2026-08-15 21:02:38 -07:00
Matthew Stanley d31ce5ac6d pgxp: framework-owned PGXP Precision mod, every game, default off
psx.enhancement.pgxp (game_id \"*\") in the builtin catalog: one Mods-page
toggle that arms sub-pixel vertex precision + perspective-correct
texturing with the user-validated defaults (tolerance clamp 0.5px), plus
a cpu-mode option. Default off = the faithful floor (G1.9). Fully
effective on PGXP-variant binaries; reduced-but-safe coverage elsewhere
(the always-emitted swc2 tier + interpreters, validate-on-read).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 20:52:21 -07:00
Matthew Stanley b2eeb0f866 Add opt-in netplay local viewport extraction 2026-08-15 20:51:23 -07:00
Matthew Stanley 7993f412c0 Add opt-in controller device defaults 2026-08-15 20:09:37 -07:00
Matthew Stanley ed9ec01abe pgxp: default tolerance clamp 0.5px (user-validated seam fix)
Interactive isolation on Ape Escape (in-game, live TCP toggles): OFF =
baseline jiggle; geometry ON unclamped = sparse hairline background-bleed
seams along corrected/uncorrected shared edges (both user-reported
artifacts vanished with the toggle off - the THPS2 line class, not wild
vertices); ON with tolerance 0.5 = seams gone, only sub-half-pixel
misalignment remains. Ship 0.5 as the default; negative still disables.

Also documents the screenshot_hires scale-2 windowed readback defect
(tiled/black PNG - row pitch) found during the session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 20:03:08 -07:00
Matthew Stanley 200b5bf003 Add opt-in widescreen culling keep hooks 2026-08-15 19:42:43 -07:00
Matthew Stanley e3824b47f5 pgxp: auto-clone the _pgxp variant inside psxrecomp_add_runtime_target
Titles that call psxrecomp_add_runtime_target directly (Ape Escape)
never went through the game wrapper, so the variant only existed for
wrapper callers. The clone now replays ARGN recursively at the end of
the target builder itself under PSX_PGXP_VARIANT, which covers every
caller; the wrapper folds an existing clone into its tail config and
the framework CMakeLists drops its now-redundant explicit block.

First real-title census (Ape Escape, worktree build vs this framework,
memory-mode only, boot->title->attract): 79-90% dataflow hits per
10s window vs the 5.2% position-cache ceiling, 0 value mismatches,
0 truncation rejects, every dataflow hit carrying depth; residual
native is 2D HUD/sprite content that must stay native. The G1.9
census gate is passed; visual screenshot_hires A/B requires a windowed
run (headless has no hi-res mirror - scale:1 fallback).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 19:17:30 -07:00
Matthew Stanley 38bdeeaade pgxp: emit hook macros + dual build targets + flavor-namespaced caches (phase 2)
Emitter side of the value-propagation engine (ENHANCEMENTS.md G1.10):

- recompiler/src/pgxp_hook_emitter.{h,cpp} (new, in the codegen-hash
  surface): ONE hook grammar shared by BOTH emitters, wrapping loads/
  stores/COP2 transfers and precision-carrying ALU ops (moves, add/sub,
  or-merge, shifts, LUI, HI/LO, muldiv) with PGXP_*() macros. Clobberable
  sources are captured into locals before the statement. Precision-
  destroying ops (AND/XOR/SLT/...) are deliberately unhooked - validate-
  on-read drops their stale shadows for free.
- code_generator.cpp: central append in translate_instruction; LWC2/SWC2
  emissions capture the RAW loaded/stored word (a masked GTE register
  write must validate against the word as loaded).
- strict_translator.cpp: translate() wraps translate_impl()'s c_code with
  the shared grammar; deferred load-delay variants hook the psx_ldd temp.
- BIOS regenerated, fingerprints verified CHANGED (SCPH1001 + OpenBIOS;
  42.6k/12.2k macro sites). Base runtime + 33/33 tests green against it.
- Gating: macros expand only under -DPSX_PGXP=1. runtime.cmake grows a
  PGXP option on psxrecomp_add_runtime_target (defines PSX_PGXP=1 +
  PSX_OVERLAY_FLAVOR=2, _pgxp exe suffix) and PSX_PGXP_VARIANT to build
  the sibling target from the same generated tree (framework runtime +
  game wrapper). No ABI version bump needed: the appended PGXPHooks
  table is NULL-tolerant and the flavor half of the ABI tag rejects any
  host/DLL flavor mix.
- Shard caches: flavor folded into the cache tag (cg<V>_<hash>_gc<hash>
  _f<flavor>) in overlay_loader.c + compile_overlays.py so base and pgxp
  caches never share a directory; compile_overlays.py passes
  -DPSX_PGXP=1 when flavor bit 2 is set; overlay preamble forwards
  psx_pgxp_* through the table. PSX_OVERLAY_CODEGEN_VER 9 -> 10.
- Env overrides PSX_GEOMETRY_CORRECTION / PSX_PERSPECTIVE_TEXTURING /
  PSX_PGXP_CPU_MODE (validation path: free-running headless boots arm
  from the first vertex); census grew produced/swc2_stores and the pgxp
  verb reports suppress/active gate state.

Validated: PSXRecomp_pgxp.exe boots the recompiled BIOS with hooks armed
(engine active, per-vertex lookups flowing; the BIOS shell has no RTPS
content so produced=0 there is correct - coverage measurement moves to a
3D title in phase 3).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 19:03:36 -07:00
Matthew Stanley 2ba74f3b4b pgxp: land clean-room value-propagation engine (phases 0+1, issue #92)
The G1.2/G1.3 mechanism: per-word RAM/scratchpad + per-GPR + per-GTE-reg
shadows carrying sub-pixel projections BY PROVENANCE, validated against
the actual guest word on every read (never guessed from rounded position
- the measured 93.1%-ambiguous G1.4 dead end). Clean-room from psx-spx +
public design docs; vendored duckstation (CC BY-NC-ND) and beetle-psx
(GPL) trees remain black-box oracles only - no code ported.

- runtime/src/pgxp.cpp + pgxp.h: engine, per-vertex GetPreciseVertex
  (dataflow -> ambiguity-gated position cache -> native) with truncation
  agreement + tolerance safeguards; stats; test accessors.
- pgxp_hooks.h: 5-entry hook funnel; PGXP_*() macros no-op unless
  -DPSX_PGXP=1 (codegen-time gate over ONE generated tree); appended
  PGXPHooks* table member in OverlayCallbacks (ABI bump ships with the
  Phase-2 emitter change).
- Both interpreters wired: loads/stores/COP2 + precision-carrying
  arithmetic (moves, add/sub, or-merge, 16-bit shifts, LUI, HI/LO).
- gte.cpp: RTPS feeds the GTE reg shadows; retired s_precise_sxy FIFO,
  hashed s_precision_store, and plain-store invalidation (redundant
  under validate-on-read; store hot path gets 3 calls back).
- gpu.c: prepare_precise_triangle rewritten per-vertex; perspective UVs
  share the same provenance and compose per triangle.
- [video] pgxp_cpu_mode / pgxp_tolerance keys; TCP pgxp verb live-
  toggles geometry/texture/cpu_mode/tolerance; geom_correction census
  grew the pgxp dataflow counters (G1.9 gate metric).
- tests: new pgxp_test (registered); gte_register_access_test updated
  to precise invalidation contract. 33/33 enabled tests green.

Also fixed while landing (both broke a fresh master tree):
- runtime.cmake: once-only psx_game_version.txt stamp (multi-target
  file(GENERATE) collision broke every fresh single-config configure).
- test_overlay_capture_retry: SDL3 rot (SDL_SetMainReady gone, inverted
  SDL_Init check) -> psx_sdl_init.

Tracked as beads-eio.3.48; postmortem + this landing: ENHANCEMENTS.md
G1-G1.10.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 18:34:47 -07:00
Matthew Stanley 813829a401 Keep GTE gameplay HUD groups edge anchored 2026-08-15 18:20:09 -07:00
Matt Stanley d4cc25dd4d Merge pull request #144 from Alexbeav/workbench/libcrypt-sbi-v1-20260815
runtime: add SBI subchannel replacements for LibCrypt
2026-08-15 16:00:04 -07:00
Matthew Stanley 3de7f9cb52 Bump boot-state version for SBI subchannel state 2026-08-15 15:35:21 -07:00
Matt Stanley 331181dc59 Merge pull request #145 from Alexbeav/workbench/hod-upstream-control-20260815
build: keep SDL3 main implementation in one translation unit
2026-08-15 15:33:39 -07:00
Matt Stanley 4573d00cac Merge pull request #143 from Alexbeav/workbench/fear-dirty-text-regression-upstream-20260815
Guard CPU text writes from overlay admission
2026-08-15 15:33:14 -07:00
Matthew Stanley 6bc9f1fe29 Declare bilinear shader shift uniform 2026-08-15 14:52:35 -07:00
Matthew Stanley adb09981fa Fix bilinear sampling seams 2026-08-15 14:46:26 -07:00
Alexandros Mandravillis 992f5b0bf0 build: keep SDL3 main implementation in one translation unit 2026-08-15 12:19:16 +03:00
Alexandros Mandravillis cebe738fd1 runtime: add SBI subchannel replacements for LibCrypt 2026-08-15 12:15:23 +03:00