Commit Graph

1535 Commits

Author SHA1 Message Date
Alex Vanderveen ffa11fa2dc cdrom: a Read that continues the current stream must not restart the drive
Legend of Mana's land-creation effect rendered as hard-edged coloured
wedges instead of a soft bloom. The cause was in the CD-ROM controller,
not the renderer.

The game streams a file by issuing Setloc(next)+ReadN roughly 10,800
guest cycles after each sector arrives. Every ReadN called
start_read_stream(), which clears the sector buffer -- destroying the
sector that had already been delivered and announced but not yet drained
by the guest. Measured at the palette load:

  buffer      psx-runtime   DuckStation
  0x0E1F18      125111        125111
  0x0E2718      125113        125112   <- the effect's colour palette
  0x0DCF18      125113        125113

Sector 125112 carries the effect's 5-colour palette (0x0888F8 =
(248,136,8), 0xB0F8F8 = (248,248,176), verified against the disc image).
Destroyed before the guest could take it, its buffer received 125113's
raw file bytes instead, and the colour routine faithfully scaled those
into 216 distinct vertex colours -- the wedges. The table now holds 5.

DuckStation only calls BeginReading -- the function that clears the
buffers and re-arms the read-start latency -- when the request actually
moves the drive:

  if ((!setloc_pending || setloc_position.ToLBA() == GetNextSectorToBeRead())
      && (drive_state == Reading || ...))
    "Ignoring read command with pending/same setloc, already reading"
  else
    BeginReading();

A Read issued while the drive already streams the sector the pending
Setloc names is the game saying "keep going", not "start over". The
command is still ACKed with the same INT3; only the restart is skipped.

Two things fall out of it. The un-drained sector survives, so the game
receives the consecutive sectors it expects (109,110,111,112,113 rather
than 109,111,111,113,113). And each request stops paying
initial_read_delay_cycles() again: inter-sector gaps drop from ~456,000
cycles to 225,792 -- exactly the 2x cadence -- so this load takes 2 guest
frames instead of 13, matching DuckStation. int1_pended goes 10 -> 0.

Also included, developed alongside and verified in the same runs:

  - An 8-slot sector buffer ring, as the hardware has (nocash;
    DuckStation NUM_SECTOR_BUFFERS = 8). A delivered sector fills the
    NEXT slot and the INT1 that announces it latches the read slot, so a
    sector arriving mid-drain no longer clobbers the FIFO. The read
    pointer follows the slot each INT1 announces, captured when the INT1
    is RAISED: this drive free-runs, so latching "newest" at
    presentation time handed the guest a sector seven ahead of the one
    it asked for.
  - The seamless no-IRQ refill advances the read pointer, because it is
    a continuation of the drain already in progress. Without it a
    multi-sector DMA starves mid-transfer (measured: 1,128,960 starved
    drains).
  - A pended data-ready is released on a short schedule rather than
    inside the guest's ack store, matching DuckStation's
    QueueDeliverAsyncInterrupt.
  - Flow control for accelerated (non-1x) reads: never overwrite a
    sector the guest has not consumed while running faster than
    hardware, capped at the authentic sector period. Inert at authentic
    timing.

New cdrom_state counters ring_starved and ring_dropped, plus
cdrom_timing_dump for per-sector deadline/delivery records, are the
regression tripwires: ring_starved and int1_lost must stay 0.

Verified on Legend of Mana (SLUS-01013): the effect renders correctly,
the palette table holds 5 colour words, sectors arrive at the 2x
cadence, and no notification is lost.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 20:05:08 -07:00
Matt Stanley d1df20514f Merge pull request #206 from mstan/fix/pr199-headless-no-recomp-ui-setup-host
runtime: fix headless generated-game builds without recomp-ui
2026-08-24 17:26:39 -07:00
Matt Stanley 64cd95f57d Merge pull request #200 from Alexbeav/fix/zero-padded-bss-overlap
recompiler: allow zero-padded PS-X EXE BSS overlap
2026-08-24 17:26:36 -07:00
Matthew Stanley 69f851f0f3 runtime: omit setup host sources without recomp-ui 2026-08-24 17:25:24 -07:00
Matt Stanley 83f99071aa Merge pull request #186 from mstan/split/pr169-recompiler-image-edge-fallthrough
recompiler: tail-transfer image-edge fallthroughs
2026-08-24 17:08:08 -07:00
Matt Stanley 7f76691724 Merge pull request #203 from mstan/fix/bezel-mod-file-picker
mods: make bezel artwork user-selected
2026-08-24 17:04:35 -07:00
Matthew Stanley 84a901289d mods: make bezel artwork user-selected 2026-08-24 17:03:52 -07:00
Matt Stanley 001af57c7f Merge pull request #190 from mstan/split/pr169-presentation-bezel
presentation: add configurable GL bezel artwork
2026-08-24 15:17:43 -07:00
Matthew Stanley 9ba703e5ed Merge remote-tracking branch 'origin/master' into split/pr169-presentation-bezel
# Conflicts:
#	runtime/runtime.cmake
2026-08-24 15:16:40 -07:00
Matthew Stanley c8e3dac678 presentation: expose bezel artwork as a mod 2026-08-24 15:15:15 -07:00
Matt Stanley 57ba490fff Merge pull request #185 from mstan/split/pr169-builtin-mod-staging-allowlist
release: allowlist staged built-in mod packages
2026-08-24 12:36:29 -07:00
Matt Stanley e5c721a042 Merge pull request #182 from mstan/split/pr169-overlay-autocompile-flavor
autocompile: pass overlay flavor to spawned compiler
2026-08-24 10:43:48 -07:00
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
Pegasus 1f34ed5108 recompiler: allow zero-padded PS-X EXE BSS overlap 2026-08-24 14:36:15 +03:00
Tomasz Finc 2bd2208c8b Fix psxrecomp_add_game_runtime for --no-recomp-ui headless builds
psxrecomp_add_game_runtime() (added in #110) is unusable with
PSX_RECOMP_UI=OFF and real generated game C linked — a documented,
supported config ("PSX_RECOMP_UI OFF for headless/generated builds",
BUILDING.md) that nothing currently exercises. Found while bringing up
a new title with recomp-ui intentionally left out initially.

Two independent breaks in that combination:

1. main.cpp calls psx_game_codegen_forward_if_built() from main()
   whenever PSX_HAS_GAME_CODEGEN is set (i.e. any time real game C is
   linked), but only *declares* it under the narrower RECOMP_LAUNCHER
   guard. Any no-launcher build with generated game C fails with
   "use of undeclared identifier". Introduced a precise
   PSX_HAS_CODEGEN_SETUP_HOST macro (set only when a codegen-setup
   host is actually linked) and re-guarded both the declaration and
   the call site on it instead of the mismatched pair of guards.

2. psxrecomp_codegen_host.c was unconditionally added to every game
   target's extras, but it unconditionally #includes recomp_launcher.h
   from the recomp-ui submodule. Any --no-recomp-ui build fails with
   "recomp_launcher.h file not found". Gated its inclusion on
   PSX_RECOMP_UI.

Verification:
- Built and ran a new title (headless: PSX_RECOMP_UI=OFF, real
  generated game C, no launcher) end to end: OpenBIOS -> game entry ->
  continuous gameplay, clean atexit shutdown, no crash.
- Regression-checked the one existing title that actually calls
  psxrecomp_add_game_runtime() (TombaRecomp; every other shipped title
  calls the lower-level psxrecomp_add_runtime_target directly and
  never reaches this code): built TombaRecomp's real production config
  (PSX_RECOMP_UI=ON, ENABLE_SETUP_WIZARD, recomp-ui launcher, mod
  catalog, setup-host/no-disc mode) against this branch via
  -DPSXRECOMP_ROOT override, no submodule surgery. Clean build, ran to
  the launcher with no regressions. Both changes are conditionally
  gated on flags Tomba always sets the opposite way, so this is
  expected and was confirmed empirically, not just by inspection.

Not covered by this PR: CMAKE_CXX_STANDARD 17 is also never inherited
by a game repo that include()s runtime.cmake directly (as opposed to
add_subdirectory()-ing runtime/CMakeLists.txt), which silently
compiles text_xlate.cpp's std::filesystem usage against whatever the
host compiler's default standard is. Six of seven other title repos
work around this by setting it themselves; happy to send a follow-up
if that's wanted here instead of per-repo.
2026-08-23 23:12:24 -07:00
tetrisgm c78a8511b7 present: bezel artwork behind the frame ([video] bezel)
A still image drawn behind the game, filling whatever the letterbox or
pillarbox leaves over -- the trick vertical shmups use on horizontal displays,
where the dead space carries artwork so it reads as designed rather than as a
limitation. MAME calls the same idea bezel artwork.

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 13:33:58 -07:00
Matthew Stanley 8ddcafb1c4 tools: extend new project setup workflow 2026-08-23 13:24:06 -07:00
Matthew Stanley 48bc21f08d recompiler: tail-transfer image-edge fallthroughs 2026-08-23 13:20:50 -07:00
tetrisgm d10d76563a release: allowlist title framework packages
(cherry picked from commit 03aba66423)
2026-08-23 13:19:09 -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 063b5210d9 autocompile: pin the overlay flavor through the spawn env (PSX_OVERLAY_FLAVOR)
The runtime pins the cache dir and captures path into the spawned
compile's environment so the cache can never drift from where the loader
reads - but not the FLAVOR. compile_overlays.py's --flavor defaults to 0
(play), so an instrumented (flavor 2) runtime spawned compiles whose
shards its own loader then rejected: the overlay_loader_status message
said it plainly - "this build reads ..._f2 but shards exist under ..._f0
-> ALL overlays run INTERPRETED". On mod-heavy titles (WipEout 3's
framerate packages patch most of the game text) that is the difference
between full speed and ~0.2x, misread as general slowness for weeks.

The runtime now exports PSX_OVERLAY_FLAVOR alongside the other pins, and
the tool honors it over --flavor, so each build population self-heals.

Measured after the fix (instrumented, WipEout 3 ntsc120full8): shard
loads 0 -> 8 (45 funcs), interpreted insns 6.6M/s -> 1.0M/s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 45f9d0f866)
2026-08-23 13:15:48 -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