Commit Graph

1504 Commits

Author SHA1 Message Date
Matthew Stanley d500554bd8 mods: expose the scanned-out display size to trusted plugins
A plugin that draws its own overlay primitives needs to know where the
right-hand edge of the screen is, and it cannot work that out for itself.
GPUSTAT carries the horizontal-resolution bits, so a plugin can recover
the coarse mode width, but the visible width also depends on the GP1(06h)
X1/X2 range -- and that register is write-only and mirrored nowhere a
plugin can read.

Ape Escape is the worked example. It scans out 384 (368-mode, dot clock 7,
horizontal range 539..3227) while its mode width is 368, and the usual
assumption of 320 is wrong by 64. A HUD row right-aligned to 316 lands 68
pixels short of the edge, which is exactly what the Quick Gadget Select
mod does today.

psx_mod_display_width()/psx_mod_display_height() return what the presenter
uses, straight from gpu_get_display_info(). Zero means the geometry is not
established yet, and the header tells callers to skip drawing rather than
substitute a guess -- an overlay in the wrong place is worse than an
overlay that waits a frame.

mod_runtime_test covers the passthrough with a stubbed GPU, including the
384-vs-368 case that motivates it and the zero case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 16:59:26 -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
Matt Stanley 353ed1b4b6 Merge pull request #151 from mstan/docs/retire-sljit-references
docs: retire stale sljit references, attribute TinyCC
2026-08-16 23:49:26 -07:00
Alex Vanderveen 5d954866d5 Merge branch 'master' of https://github.com/mstan/psxrecomp 2026-08-16 22:35:41 -04:00
Alex Vanderveen 6aed4c5c70 Update recomp-net 2026-08-16 22:35:40 -04:00
Alex Vanderveen 95657b6dfd bump 2026-08-16 22:32:41 -04:00
Matthew Stanley 55cec09acd docs: retire stale sljit references, attribute TinyCC
The sljit Tier-2 in-process JIT was replaced by the tcc tier in c2a3f6ad
(2026-06-25) and removed outright in 5b7e69b4 (2026-07-15). That removal
reworded the C-level comments but touched no markdown, so the prose still
described sljit as a live, default-on backend.

Backends today are static -> gcc -> tcc: gcc is the development default when
it is on PATH, tcc is the toolchain-free path for players. Both are external
compilers invoked as a subprocess that produce a DLL; neither is an in-process
JIT. docs/ARCHITECTURE.md, docs/BUILDING.md, docs/EXECUTION_MODEL.md,
docs/COMPILING_OVERLAYS.md and README.md already said this correctly and are
unchanged here.

- THIRD_PARTY_ATTRIBUTION.md: the sljit entry claimed code vendored at
  lib/sljit/ and compiled into the runtime. That directory does not exist.
  Replaced with a TinyCC entry, which was missing entirely despite tcc being
  the compiler intended to reach end users.
- docs/internal/SLJIT_PERSIST_CACHE.md: deleted. It documented the persisted
  shard cache of a subsystem that no longer exists, as "implemented" and
  "default-on". docs/internal/README.md updated to point at the current model
  instead of a dead link.
- docs/ASYNC_OVERLAY_COMPILE.md: superseded-in-part header. Deliberately NOT
  rewritten sljit->tcc: the passages about serializing .sljit blobs,
  overlay_sljit_deserialize and an in-memory JIT registry describe a mechanism
  tcc never used, so renaming it would trade one false claim for another. The
  async worker/file-handoff design and the CPS work remain accurate.
- docs/OVERLAY_CACHE_V2.md: sljit dropped from the dispatch priority chain.
- ISSUES.md: Issue #7 closed as superseded — the backend it reported against
  was deleted rather than fixed.
- docs/internal/COSIM_ORACLE.md: sljit_helpers left CPUState with the tier.
- Dated session logs (RECURSION_BUG.md 25.5, BACKDROP_PRELOAD.md,
  ecosystem-watch.md) keep their narrative and get a pointer to the removal.

One thing this does not fix: docs assert the tcc toolchain is "downloaded at
release-packaging time and bundled beside the game exe". runtime/src/main.cpp
does look for <exe_dir>/overlay_toolchain/, but no script in this repository
creates it. The attribution entry now states only what the code does and notes
that if packaging supplies the bundle, it happens outside this repo and must
carry the TinyCC notice. Flagged rather than papered over.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 18:16:34 -07: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 b0433c20c5 Merge pull request #149 from mstan/feat/vsync_hotfix
Stop vsync and the wall-clock pacer from double-blocking presents.
2026-08-16 14:01:45 -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
Matt Stanley 696d05643d Merge pull request #147 from mstan/codex/netplay-local-viewport
Add opt-in netplay local viewport extraction
2026-08-15 21:42:59 -07:00
Matthew Stanley bdd7ade6e8 Merge remote-tracking branch 'origin/master' into codex/netplay-local-viewport 2026-08-15 21:40:56 -07:00