Commit Graph

1517 Commits

Author SHA1 Message Date
Matt Stanley 13fa00cb5a Merge pull request #188 from mstan/split/pr169-new-project-setup-tooling
tools: extend new project setup workflow
2026-08-24 10:43:23 -07:00
Matt Stanley cdad44353b Merge pull request #178 from mstan/split/pr169-host-upstream-docs
docs: add host optimization contract and upstream audit notes
2026-08-24 10:39:49 -07:00
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
Matt Stanley 3c1899e13f Merge pull request #180 from mstan/split/pr169-savestate-status-debug-receipt
debug: expose savestate completion status
2026-08-24 10:20:20 -07:00
Matt Stanley b3bd62f40e Merge pull request #177 from mstan/split/pr169-pad-hotkey-range
config: accept controller chord hotkeys from settings
2026-08-24 10:20:06 -07:00
Matthew Stanley 8ddcafb1c4 tools: extend new project setup workflow 2026-08-23 13:24:06 -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 7a872bc018 docs: add host optimization and upstream audit notes 2026-08-23 13:09:19 -07:00
Alex Vanderveen b8b82b753c Fix the pad-hotkey range check rejecting every chord
[hotkeys] rewind_pad / save_state_menu_pad accept the recomp-ui pad encoding,
where 1000 + a button bitmask is a CHORD. The parse guarded on `n < 256`,
which admits single buttons and rejects every chord there is — including the
two values this same file writes back out (1272 = select+r3, 2040 =
select+r1). A saved chord was silently dropped on load and the defaults
quietly reinstated, so the pad hotkeys were never actually configurable.

The bound predates the chord encoding; the runtime's own
normalize_hotkey_pad_binding has handled combos correctly all along.

Replaced with pad_bind_value_ok(), sharing one documented bound with the
encoding it validates. Verified against a live boot with rewind_pad = 1017
(select+cross): parsed has=0 value=1272 before, has=1 value=1017 after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 165fae0aa1)
2026-08-23 13:07:39 -07:00
Matt Stanley 13a56626d3 Merge pull request #176 from mstan/fix/observability-window-traps
observability: window traps that read as results, and a per-game watch list
2026-08-23 11:58:16 -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
Matthew Stanley 239052f166 docs: explain generated project framework requirement 2026-08-23 11:54:10 -07:00
Matt Stanley b38edca8e2 Merge pull request #173 from mstan/fix/emitter-directive-line-guard
recompiler: guard every same-line appender off preprocessor-directive lines
2026-08-22 11:28:29 -07:00
Matthew Stanley eac1a61e43 recompiler: guard every same-line appender off preprocessor-directive lines
PR #171 fixed the PGXP hooks, which were space-joined onto emissions that
end on a bare #endif (the PSX_ENABLE_BLOCK_CYCLES muldiv stall/latency
blocks) and were therefore swallowed as "extra tokens" and silently
discarded -- 3,187 hooks per title in Ape Escape alone, plus 1,024 in the
BIOS. It fixed the instance. Two same-line appenders of the same class
were left behind:

  1. strict_translator.cpp -- the deferred-load PGXP_LOAD hook is still
     space-joined onto c_code_deferred. All five c_code_deferred producers
     currently end in plain statements, so it is benign TODAY, but the
     moment a block-cycles annotation lands on deferred loads it breaks
     exactly the way #171 just did, and just as silently.

  2. code_generator.cpp -- the trailing disassembly comment is appended to
     the emission unconditionally. Reachable whenever append_pgxp_hooks
     returned early and left the emission ending on a directive, e.g.
     `mfhi $zero` / `mflo $zero` (rd == 0 takes an early return). The
     comment is then dropped and -Wendif-labels fires.

Both now route through one predicate,
emission_ends_on_preprocessor_directive(), so the hazard is closed by
construction rather than one site at a time. Site 1 takes an unconditional
newline (always valid where the space was); site 2 only breaks the line
when the emission actually ends on a directive, so ordinary instructions
keep their inline comment and the generated diff stays minimal.

Measured effect on emitted code, against merged master:
  - SCPH1001 BIOS: byte-identical
  - OpenBIOS: 2 sites, both the deferred PGXP_LOAD moving to its own line,
    identical token sequence
  - Ape / Tomba 1 / Tomba 2 / MMX6: every function token-identical
    (3095/6891/2726/4023 functions each, none added, none removed, zero
    body changes)

Adds emitter_directive_line_test (10 checks, registered with add_test) so
the predicate cannot regress: it pins both real emission shapes that end on
#endif and, just as importantly, the negative cases -- a false positive
would inject stray newlines into every instruction.

ctest before and after this change fails the identical 6 pre-existing tests
(aot_overlay_discovery, launcher_vulkan_option, mod_load_acceleration,
release_zip, sdl3_main_single_include, vk_present_wait_stage); no new
failures. full_function_emitter_test and l2_structural_test both pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 01:39:05 -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
Henrique Guedes 6db6358006 pgxp: join emitted hooks with a newline, never a space
append_pgxp_hooks spliced every PGXP_* call onto the tail of the
instruction's emitted code with a plain space. When that emission ends
with a preprocessor directive — the PSX_ENABLE_BLOCK_CYCLES muldiv
stall/latency block ends with a bare `#endif` — the hook lands ON the
directive line, where the preprocessor discards it as extra tokens
(-Wendif-labels warns). Net effect: in a PGXP-flavored build with block
cycles enabled, every MULT/DIV/MFHI/MFLO hook silently vanished, so
hi/lo results never propagated precision.

Join with "\n" at every hook site instead. A newline is always valid
where the space was, so unaffected emissions are byte-equivalent after
recompilation, and directive-terminated emissions now keep their hooks.
2026-08-21 21:33:50 -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 1814847732 Merge pull request #157 from Alexbeav/fix/exc-sp-straddle-return
recompiler: recognize exception-stack sp straddle at polled return boundary
2026-08-20 15:32:24 -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 4673db2056 Merge pull request #162 from Unchiga/lwlr-load-delay
Forward a pending load into an LWL/LWR merge
2026-08-20 15:16:58 -07:00
Matthew Stanley c77f8adcc0 recompiler: forward a pending load into LWL/LWR in the full-function emitter too
The CFG emitter now points an LWL/LWR merge operand at the deferred
psx_ldd_<addr> temp, but full_function_emitter.cpp (BIOS / resident
functions) had the identical defect and was left untouched.

There, insn_reads_gpr already reports LWL/LWR as reading rt (it is the merge
base), so a `lw rt, ...` followed by `lwl/lwr rt, ...` was recorded as a
dependent pair and the load's writeback deferred. The successor then merged
cpu->gpr[rt] -- still the PRE-load value -- and because LWL/LWR
architecturally writes rt, the flush-vs-discard rule turned the deferred
writeback into `(void)psx_ldd_<addr>`. So the fetched word was dropped
entirely and the merge combined a stale register: silently wrong code, no
diagnostic.

This mirrors the CFG fix: record such pairs in ldd_lwlr_forward, exclude them
from the flush/discard normalisation, rewrite the successor's single
psx_old_rt merge-base initializer to read the temp, and emit a note instead
of a writeback. If strict_translator's emitted shape ever stops matching, the
emitter throws rather than quietly producing a stale merge.

Not currently live: neither generated BIOS contains a `(void)psx_ldd_` site
today, so this is latent-bug hardening. It matters because the interpreter
DOES forward here (dirty_ram_interp.c's s_ld_pend LWL/LWR exemption, added
after it wedged Tomba 2 at boot), so leaving the emitter unfixed keeps native
and interpreted execution in disagreement -- the divergence class that
produced the OpenBIOS memcard corruption.

Also adds recompiler/tests/test_lwlr_load_delay_forward.py, registered in
recompiler/CMakeLists.txt. It synthesises a PS-EXE with the dependent
lw/lwr pair and asserts the merge operand names the temp and that the
deferred load is not discarded. Verified to FAIL on master (merge operand
cpu->gpr[8], plus a discard) and pass with the fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 15:12:55 -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 4f0eb49ab6 Merge pull request #161 from rickguedes/fix/overlay-fallthrough-pc
recompiler: publish PC at CPS unit-edge fall-through instead of falling off
2026-08-20 14:29:07 -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 c5488ebcb8 Merge pull request #155 from ClaudioVarandas/fix/kuseg-dispatch-lookup
recompiler: match dispatch entries for KUSEG-addressed games
2026-08-19 22:14:16 -07: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
Matt Stanley af01841d1c Merge pull request #158 from Alexbeav/perf/xlate-capture-default-off
runtime: default string-inventory capture off (26-35% throughput on streaming titles)
2026-08-19 22:13:28 -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
Henrique Guedes b549fde61f recompiler: scope the load-delay temp past the PGXP load wrapper
The MIPS-I load-delay pair defers a dependent load's register writeback
past its successor by assigning into a psx_ldd_<addr> temp. The temp was
declared inline at the load statement — but with PGXP enabled that
statement has already been wrapped in its own
`{ uint32_t _pgxa = ...; <load> PGXP_LOAD(...); }` block, so the
declaration went out of scope before the writeback, which is emitted
after the successor. gcc rejected the shard ("'psx_ldd_...' undeclared"),
the whole overlay compile run exited non-zero, and after enough
consecutive failures autocompile latched degraded: nothing was promoted
to native and every freshly loaded RAM image ran interpreted for the
rest of the session.

Declare the temp at the top of the pair block that encloses both
statements, and point the PGXP load hook at the temp: with the writeback
deferred, the loaded value lives in the temp while the GPR still holds
the old value the successor must read — the hook previously recorded
that stale pre-load value. Delay semantics are unchanged, and without
PGXP the emission differs only in where the declaration sits.

Found on Ehrgeiz (SLUS-00809): a dependent lw pair in a kernel-window
overlay failed every autocompile run (19/19); load-screen code ran
interpreted at 1.5-9 fps for minutes per load. With the fix the same
captures compile clean and loads hold 60 fps.
2026-08-19 16:48:48 -03:00
Henrique Guedes ddbd0d522d recompiler: publish PC at CPS unit-edge fall-through instead of falling off
A basic block that ends with no control flow, no successors, and no known
function at the next address (typical of a partial overlay capture whose
discovery ended mid-stream) emitted only the interrupt check and then fell
off the end of the generated C function. Under CPS the continuation-entry
prologue has already cleared cpu->pc, so the flat trampoline reads the
return as a normal guest exit and shuts down silently (exit 0, no crash
report) instead of continuing at the fall-through address.

Emit `cpu->pc = next_addr; return;` — the same contract every other CPS
block terminator follows — so the trampoline can route the continuation
through another overlay variant, static code, or the dirty-RAM interp.
Legacy (PSX_CPS=0) codegen is unchanged.

Found on Ehrgeiz (SLUS-00809): a truncated 0x3C-byte capture variant of a
0x144-byte TOP-menu function made every cold boot exit at PC=0 after ~4.6M
dispatches, while savestate resumes masked it. With the fix the same
capture store boots to gameplay.
2026-08-19 15:28:39 -03:00
Unchiga 50c730cadf Forward a pending load into an LWL/LWR merge
LWL/LWR take their destination register as both the merge base and the
destination, and they read that base late enough to receive the forwarded
result of an immediately preceding load. They are therefore not subject to the
MIPS-I load delay: after

    lw   $t0, 12($a3)
    lwr  $t0, 10($a3)

the LWR merges into the word the LW just fetched, not the stale $t0.

The load-delay pair emitter defers a load's writeback into a psx_ldd_<addr>
temporary, so an LWL/LWR successor was merging into cpu->gpr[rt], which still
held the pre-load value. The merge operand now names the deferred temporary
when the successor is an LWL/LWR writing the same register, and the ordinary
writeback is suppressed there because the merge has already consumed it.

Only the consumer side needs this. LWL/LWR as the producer never defer, so
their writeback is already immediate.

Verified against a full regeneration of a title that exercises the pair: 16
forwarding sites, and every emitted merge operand identical to the reference
output built with this fix in place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 07:58:58 -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
Alexandros Mandravillis 3f91c30a1b recompiler: recognize exception-stack sp straddle at polled return boundary
An IRQ taken exactly at a polled call's return boundary swaps sp between
the task stack and the kernel exception/scratchpad stack, so a genuine
return (ra == stop_addr) can arrive with sp != sp_at_call in either
direction. The strict same-frame gate misread that as recursion and
re-dispatched the same delivery until the depth-257 guard tripped.

Accept the return only when ra matches the stop address and exactly one
of the two sp values lies in the scratchpad exception window; recursion
(different ra) and same-stack wild arrivals still take the strict gate,
so the Tomba Bug D protection is unchanged.

Fixes mission-1 freezes in Medal of Honor, MoH Underground, and Driver 2
(depth guard trip at 257 after tens of thousands of deliveries);
validated on those three plus Jersey Devil as control and by an
independent BIOS regen chain.
2026-08-19 16:46:00 +03:00
Claudio Varandas 926a9e9e99 recompiler: match dispatch entries for KUSEG-addressed games
PS1 segments alias the same physical RAM. Most PS-X EXE headers carry KSEG
addresses, but a header may legally carry KUSEG ones instead, and the guest
then executes with a KUSEG PC. Alien Resurrection (SLUS-00633) is such a
title: its header holds load address 0x00010000 and entry PC 0x000155A4,
and get_registers on a live run reports PC=0x00010170.

The emitted dispatch table is keyed by the recompiler's KSEG-normalized
addresses, and psx_game_find_entry() compared those keys against the raw
incoming address. For a KUSEG PC the comparison can never succeed, because
0x0001xxxx is always below 0x8001xxxx: the binary search walks to the start
of the table and returns no entry. Every dispatch then falls through to the
interpreter.

The failure is silent and easy to misread. psx_game_address_in_text() does
mask, so it still reports the address as game text and the interpreter runs
the real RAM bytes via clean_game_text_miss. The game boots and renders
correctly, just entirely interpreted. Because no entry is ever found, the
text-image guard is not reached either, so its diagnostics stay at zero
(text_native_blocked=0, text_exact_mismatches=0) while dirty_ram_stats
reports over a billion interpreted instructions and native_handoffs=0 --
which reads as "nothing was rejected" rather than "nothing was looked up".

Compare 29-bit physical addresses on both sides of the search, and sort the
table by that same masked key so the binary-search invariant holds by
construction rather than by every entry happening to share a segment.

Adds tests/test_kuseg_dispatch_lookup.py: synthesizes a KUSEG-addressed
PS-EXE, runs the emitter, and asserts the lookup masks both the incoming
address and the stored key, and that the emitted table is ordered by
physical address.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 00:31:56 +01:00
Alex Vanderveen 9913b146cd chore: sync 2026-08-18 12:20:20 -04:00