Several consumers cache a classification of the instruction at a given PC and
key that cache on g_dirty_ram_code_gen, per the contract stated where the
counter is defined (runtime/src/memory.c): "Consumers that cache per-PC
classifications of RAM instructions compare against this and re-derive after any
code change - a cached kind must never survive an overlay reload." Today that
counter is bumped from exactly two places, both on the instrumented store path:
a page's clean->dirty transition, and dirty_ram_mark_executable_range().
A savestate restore reaches neither. BS_SEC_RAM applies a raw memcpy over all
2 MB, so code identity at any address can change completely while no page makes
a clean->dirty transition and no executable range is marked. Every cache keyed
on the generation therefore keeps serving verdicts derived from the PREVIOUS
contents. The overlay loader's negative "no native owner" cache is the damaging
one, because overlay_loader_dispatch() consults it before rediscovery, so one
stale entry can pin an address to the wrong implementation for the rest of the
run.
Note the same restore already invalidates psx_kernel_bless_note_range(), so the
path is aware it must invalidate downstream caches - it only ever did one of
them.
Fix: add dirty_ram_invalidate_code_caches() and call it after the BS_SEC_RAM
memcpy. dirty_ram_set_bitmap_words() (the BS_SEC_DIRTY apply path) calls it too,
since a wholesale bitmap replacement changes which addresses are RAM-resident
code by the same argument. Bumping the generation is sufficient by construction:
each cache stores the generation it was derived under and re-derives on
mismatch, so this fixes every consumer at once instead of patching them
individually.
Scope, stated honestly: this closes a real latent correctness gap in the load
path and is worth having on its own. It is NOT a fix for the separate,
still-open bug where loading a gameplay state into a process that never entered
that stage leaves the player unable to move - that was measured and the overlay
cache is demonstrably NOT its cause (the dirty bitmap is identical between a
working and a broken instance, and covers only the kernel window, so the
player's code is static-dispatched in both). No behaviour change for a process
that never loads a state.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two path-resolution defects where the runtime silently accepted only one of
two equally-valid spellings of the same input.
1. Disc: picking the .cue threw away the disc's TOC.
normalize_disc_path_for_launch() swapped any .cue for its same-named .bin
unconditionally, and every disc entry point (game.toml, --disc, the cached
disc.cfg, the launcher's Change Disc, the first-run dialog) went through it.
That discarded the cue's table of contents -- every CD-DA track and pregap
with it -- even though iso_reader.cpp has full multi-FILE cue support that
the swap defeated before it was ever reached. In the other direction,
picking a redump audio-track bin mounted that track as the whole disc, which
cannot boot.
resolve_disc_path() (runtime/include/disc_path.h) now canonicalizes any pick:
.cue that resolves -> mount the cue (TOC intact)
.cue with a missing bin -> the payload it still names, else a
stem-matched sibling image
.bin/.iso/.img owned by a cue -> mount that cue
bare image, no cue -> mount it
identify_disc() resolves through the same code, so the header/serial/CRC32
verdict is read from the data track no matter which member was picked.
While here: the mount path and the verify path had two divergent private cue
parsers (quoted-only + case-sensitive vs quoted-or-bare + case-insensitive),
so they could disagree about the same file. Both now use one parser
(runtime/include/cue_sheet.h), which additionally handles CRLF, backslash
paths, and case-insensitive payload lookup for case-sensitive filesystems.
2. BIOS: the multi-BIOS rename made configs and dump folders incompatible.
SCPH*.toml name "US-PSX-SCPH1001.BIN" while every pre-existing dump folder,
bios.cfg and tools/regen_bios.sh default name "SCPH1001.BIN". Either
combination fataled with "cannot open BIOS file" despite the right ROM being
present. resolve_bios_rom() (recompiler/include/bios_rom_alias.h) matches on
the model token -- the last '-'-separated component of the stem -- so both
conventions resolve in both directions without a hardcoded alias table.
Applied at the config loader, the positional psxrecomp-bios CLI, and each
rung of the runtime's BIOS search.
The positional CLI also derived out_stem from the raw filename, so a
region-qualified ROM would have emitted US-PSX-SCPH1001_full.c -- a name
runtime.cmake cannot link. It now derives the model token.
Also fixes SCPH5552.toml, whose rom field omitted the bios/ directory that
its two siblings have.
Tests: test_disc_path_resolve (36 checks, five dump layouts) and
test_bios_rom_alias (15 checks, both directions + negative cases), both
registered in runtime/CMakeLists.txt. Registered the pre-existing
test_iso_reader_cdda, which was wired into no build.
Verified on real dumps: Ape Escape .cue and .bin both give
SCUS-94423 / NTSC-U / crc=C6F455BC; Tomba 2's multi-track dump gives
crc=1FB8C151 and a 2-track TOC from the .cue, the data bin, and the CD-DA
bin alike.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
On macOS/Linux there is no native GUI file picker, and in headless mode a
picker would block, so pick_runtime_file() bails when it can't resolve the
BIOS/disc interactively. The old message ("<title> requires a command-line
path on this platform") never said WHICH flag to pass, so a first-time
from-source user on Linux hit "no BIOS selected; exiting." with no idea what
to do.
Thread the responsible flag (--bios / --disc) into pick_runtime_file and print
an actionable message with the flag and a full example command, on both the
headless and the no-GUI-picker paths. Points at the game README's "Building
From Source" for the full run line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The runtime links generated/<serial>_{full,dispatch}.c, produced by a separate
recompiler step run before the build. Those paths are marked GENERATED so cmake
configure succeeds before that step has run — which meant skipping generation
surfaced only as a raw "cc1: fatal error: <file>: No such file or directory"
deep in the build, with no hint that a step was skipped or what produces the file
(exactly the trap a from-source builder on a fresh checkout hits).
Add runtime/check_generated_sources.cmake and wire it into
psxrecomp_add_runtime_target as a build-time dependency of the runtime target:
a missing game source now aborts at build start with the files named and the
exact fix (build recompiler -> extract EXE -> generate -> rebuild), and configure
emits an early non-fatal warning. Framework-wide, so every game inherits it.
Also fix the docs/BUILDING.md "build and run a game" recompiler invocation (the
framework is a submodule at psxrecomp/, not ../psxrecomp/) and add the
recompiler-build step.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
UI now passes an in/out guest UDP bind (prefer 7778). Accept the 4th
argument, reject :0, and stop advertising ephemeral 0.0.0.0:0 on online
join so Windows CI links and online guests get a dialable endpoint.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add a resize-driven aspect option, keep native-wide culling and spawning hooks engaged across live aspect changes, and widen host GP0 draw-area pruning only for the active wide framebuffer.
Credit OpokXeno's independently discovered draw-area pruning fix documented in PR #73.
Co-authored-by: Opok <novelasevilliousesp@gmail.com>
Discover recomp-ui from CMAKE_SOURCE_DIR/recomp-ui (or RECOMP_UI_ROOT).
Keep lib/recomp-net in the engine.
Co-authored-by: Cursor <cursoragent@cursor.com>
Two present-path fixes:
1. The short GP1(07h)-band letterbox (MotK 128-line FMV) fired for any
frame under 240 lines, which also catches games whose native display
mode is short full-time (Xenogears runs 320x216): every 4:3
menu/load and every FMV got a ~10% vertical squash. No numeric
threshold separates the cases (216 < 224 too), so split by purpose:
15-bit game frames never shrink (adjustment removed from
gl_renderer_present_vram), and the FMV paths keep the correction
only for genuinely windowed video bands (gl_renderer_present, and
the SDL path gated on pin_43 && depth24_frame) with the band under
80% of the 240-line field. MotK's 128-line FMV keeps its aspect fix;
native 216/224-line display modes fill the rect as before.
2. present_target_quad mapped quad corners to texel edges, so with
GL_LINEAR the outermost dest pixels blend the border texel with
neighboring VRAM outside the content rect (visible 1-2px edge
stripes with linear present filtering, e.g. menu frames showing the
adjacent framebuffer). Center-map the UVs with a half-texel inset;
interior sampling is unchanged. Covers both callers (canonical VRAM
present and wide-FBO present).
Left-edge counterpart to slti_sites: emit psx_ws_cull_bltz at configured
bltz PCs. detect_cull_bltz_sites only classifies left-edge bltz for
auto_screen_x-qualified functions, so an X-only funnel wired via explicit
slti_sites had no left-edge widen. bltz_sites closes that gap. Identity at
4:3; regen-class.
Re-add watchdog/PC-sample fire wrappers and overlay icache_fetch_fn;
stop shadowing the inlined psx_advance_cycles with a missing extern.
Co-authored-by: Cursor <cursoragent@cursor.com>
Re-add watchdog/PC-sample fire wrappers and overlay icache_fetch_fn;
pull inlined psx_advance_cycles via psx_cycles.h.
Co-authored-by: Cursor <cursoragent@cursor.com>
Export service_to_now, rename advance to *_slow for the header inline,
and give g_audio_host_rate C linkage.
Co-authored-by: Cursor <cursoragent@cursor.com>
Rename out-of-line load helpers to *_slow to match psx_cyc.h inlines,
declare psx_icache_fetch/reset, and wire overlay callbacks to slow paths.
Co-authored-by: Cursor <cursoragent@cursor.com>