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)
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)
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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>
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>
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>
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>
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>
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>
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>