Commit Graph

1097 Commits

Author SHA1 Message Date
Matthew Stanley 9abb83de8f Merge PR 283 after PR 282 revert for regression validation
# Conflicts:
#	recompiler/CMakeLists.txt
2026-08-31 13:50:04 -07:00
Matthew Stanley a4721966bc Revert "Merge pull request #282 from Alexbeav/pr/cdrom-seek-semantics"
This reverts commit 6003e63fbc, reversing
changes made to 59b9658c21.
2026-08-31 13:43:27 -07:00
Matt Stanley be19a02902 Merge pull request #285 from Alexbeav/pr/zero-bios-registry
Make zero-BIOS registries portable to MSVC
2026-08-31 12:06:01 -07:00
Alexandros Mandravillis eb12cb2411 Fix zero-BIOS MSVC registry build 2026-08-31 09:16:14 +03:00
Alexandros Mandravillis e7c29ac286 Cover both recomp UI fallbacks 2026-08-31 09:15:51 +03:00
Alexandros Mandravillis fe6a06d298 Preserve explicit recomp UI root 2026-08-31 09:15:51 +03:00
Alexandros Mandravillis cc1967cf5a Keep 15-bit scanout independent 2026-08-31 09:15:39 +03:00
Alexandros Mandravillis 4676707ae0 Preserve interlaced PAL scanout height 2026-08-31 09:15:39 +03:00
Alexandros Mandravillis 0ff78cc4ca Blank display ranges outside active video 2026-08-31 09:15:39 +03:00
Alexandros Mandravillis 87ecd78165 Keep scanout fixture title neutral 2026-08-31 09:15:39 +03:00
Alexandros Mandravillis 12f2060edb Present full depth24 scanout canvas 2026-08-31 09:15:39 +03:00
Alexandros Mandravillis a851e26d48 Preserve visible depth24 scanout rows 2026-08-31 09:15:39 +03:00
Alexandros Mandravillis c159c8603b Handle invalid display ranges 2026-08-31 09:15:38 +03:00
Alexandros Mandravillis 025193e2e8 Center depth24 scanout from display range 2026-08-31 09:15:38 +03:00
Matthew Stanley 94b64b0b4b Merge master into CD-ROM seek semantics PR 2026-08-30 20:53:43 -07:00
Matt Stanley 59b9658c21 Merge pull request #279 from Alexbeav/codex/cdrom-timed-reinsert-upstream
cdrom: model timed tray cycle for reinsert
2026-08-30 20:52:24 -07:00
Matt Stanley ac9be0b6c9 Merge pull request #277 from Alexbeav/pr/gpu-dma2-slicing
runtime: slice GPU linked-list DMA
2026-08-30 20:52:21 -07:00
Matt Stanley 0ed754a3ce Merge pull request #273 from kerokline/fix/present-skip-unpaced-vsync-cadence
runtime: pace frames the GL renderer elides a swap for
2026-08-30 20:52:18 -07:00
Matt Stanley 0cbd435349 Merge pull request #271 from Alexbeav/pr/stale-static-guards
codegen+runtime: stop executing stale static code when a game overlays its own text
2026-08-30 20:52:11 -07:00
Matt Stanley 68a2b3819a Merge pull request #270 from Alexbeav/pr/cosim-icount-checkpoints
cosim: key checkpoints on retired-instruction count, not guest cycles
2026-08-30 20:43:23 -07:00
Alexandros Mandravillis d43442bfe7 Clarify CD-ROM test fixture usage 2026-08-31 02:20:05 +03:00
Alexandros Mandravillis 12a3eb7d11 Complete CD-ROM reset and snapshot guards 2026-08-31 02:20:05 +03:00
Alexandros Mandravillis 25e7624a0d Harden CD-ROM seek state coverage 2026-08-31 02:20:05 +03:00
Alexandros Mandravillis 3b837f4e2d Fix CD-ROM seek and read handoff 2026-08-31 02:20:04 +03:00
Alexandros Mandravillis fb02451aab cdrom: model timed tray cycle for reinsert 2026-08-30 23:24:47 +03:00
Alexandros Mandravillis 8af48ae986 runtime: slice GPU linked-list DMA 2026-08-30 23:17:24 +03:00
Kevin a91884a4bb runtime: pace frames the GL renderer elides a swap for
The GL renderer may skip SDL_GL_SwapWindow when the displayed rect is
unchanged, so a double-buffered 30 Hz game does not swap an identical
front buffer on the intervening vblank. The frontend calls present once
per guest vblank, so eliding a swap also elides whatever that swap was
blocking on.

That is only safe while the wall-clock pacer owns cadence, because the
pacer runs per present regardless of what the renderer does. When driver
vsync owns cadence instead (~60 Hz panel, present_vsync_owns_cadence()),
the pacer is deliberately not run -- the two are XOR to avoid
double-blocking -- and the swap block becomes the only throttle. An
elided frame then blocks on nothing, and a game presenting every other
vblank advances two guest vblanks per block: exactly 2x real time.

Measured on BreathOfFire3Recomp (SLPS-00990, 60 Hz panel, OpenGL):
guest vblank counter sampled against wall clock ran 120.4 fps / 2.01x
before, 59.8 fps / 0.997x after. Swaps per second are unchanged at 60
either way -- the fix removes a free-running guest frame, not a swap --
and per-frame GPU cost is unchanged (present_gpu_ms_avg 12.2 -> 12.6,
scene_gpu_ms_avg 4.5 -> 4.1).

Gate both skip sites (VRAM and wide-FBO paths) on the cadence owner,
alongside the existing OSD and interpolation-cadence guards. Only the GL
renderer has this optimisation; the software and Vulkan paths always
present and were never affected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 14:20:09 -04:00
Alexandros Mandravillis f07f801944 runtime: validate stale-static ranges in full — drop the exec_pc clip
dirty_ram_text_native_ok_ranges_from clipped validation to
[exec_pc, end), assuming a continuation never fetches bytes behind its
resume PC. False for any entry with a backward edge: a post-call
continuation re-enters mid-function and loops back into the clipped
region, executing stale static code. CMR2's overlay engine carries a
second copy of the vertex-transform kernel at 0x80016B34 that forked
exactly this way (r16 +0xA0 static vs +0x100 live, the same 0x60
delta as the first locus) after the inter-piece transfer guards
closed the fallthrough edge.

Validation now covers every emitted range of the entry regardless of
resume PC. A genuinely patched prologue blocks the whole entry to the
dirty-RAM interpreter -- correct, merely slower.
2026-08-30 18:15:23 +03:00
Alexandros Mandravillis 4d9060a54e codegen: stale-static guard on inter-piece host transfers
A game that overlays its own text at runtime (CMR2 streams transform-
loop variants with patched projection constants over the boot EXE)
kept executing the stale static translation through inter-piece host
transfers: dispatch validates an entry's emitted ranges, but split
function pieces chain via direct host calls (fallthrough to split
piece / next function, legacy non-CPS split branch/jump/continuation
transfers, non-CPS direct jal) with no revalidation. A clean entry
piece falls through natively into a stale sibling whose live bytes an
overlay replaced -- the CMR2 attract/gameplay wedge corruption
(first divergence: one ADDI immediate, screen-center 160 static vs
256 live, at 0x800199D4, retirement 3,203,596,289, frame ~11963).

Emit at every such transfer:
  if (!psx_game_text_native_ok(T)) { cpu->pc = T; return; }
publishing the PC and unwinding to the trampoline, whose dispatch
takes the sanctioned dirty-RAM-interpreter fallback over live bytes.
The non-CPS direct jal falls back to call_by_address instead.

dirty_ram_text_native_ok_ranges_from gains a page-clean fast path
(skip the memcmp when no touched page is guard-modified or runtime-
dirty) so the guards are near-free on pristine pages.

cosim.py: print full A/B cpu dumps in cpudiff mode (values, not just
diffs).
2026-08-30 18:14:32 +03:00
Alexandros Mandravillis 4aebb349da cosim: key checkpoints on retired-instruction count, not guest cycles
The cycle-keyed design hashed+parked at the first cosim_instr boundary
AFTER the stride cycle was crossed; the two backends can reach that
crossing at different retirement positions, so the oracle compared
different architectural states (observed as a +5-cycle park skew and
phantom CPU-register divergences mid-CPS).

Checkpoints are now taken inside cosim_instr, keyed on the retirement
count itself (a branch and its delay slot retire as one event in both
backends). Checkpoint k = state after k*stride retirements in BOTH
instances by construction. The guest-cycle clock stays inside the state
hash, so real cycle-accounting drift is still detected -- now pinned to
an exact instruction window instead of corrupting the alignment.

cosim.py: stride/--max/--start-cycle now in retirement units; the cycle
skew warning is now reported as a real timing divergence. launch() gives
instance B its own exe (the mods root is exe-dir-relative, so a shared
exe races on mods/state.toml publish regardless of cwd separation) and
refuses a stale COSIM_CWD_B exe.

Gates on CMR2 (SLUS-01222): compiled/compiled and interp/interp both
clean over 100M retirements with identical cross-pair chains; injected
1-bit RAM fault detected at the next checkpoint.
2026-08-30 18:14:24 +03:00
Matthew Stanley 47bda8172e runtime: fix savestates at dirty boundaries 2026-08-29 22:08:31 -07:00
Matt Stanley 09ec090d88 Merge pull request #259 from mstan/feat/multi-disc-launcher
Multi-disc: per-disc identity, launcher roster, and stop copying dumps into projects
2026-08-29 20:04:40 -07:00
Alex Vanderveen a44fb12575 runtime: tag savestates with the disc on a multi-image title
A savestate is whole-machine state. One taken on disc 2 and restored while
disc 1 is mounted resumes a guest that still believes it is reading disc 2,
and the slot list gives no hint: every disc of a set shares one entry_pc, and
entry_pc is the key the slot filenames already use. So the same slot number
meant a different machine on every disc, silently.

Savestates were already isolated per BIOS, as a directory. The disc joins that
as a FILENAME token inside it:

  <saves>/<bios>/state_<entry>_disc<N>_slot<NN>.pst

The BIOS stays a directory because a state is invalid across BIOS images
whatever disc it came from. The disc only has to not collide within one, and a
token does that without another level to browse, back up and migrate.

An empty token leaves the historical name byte-for-byte, so single-disc titles
keep every existing state -- the scope is set only when the roster has more
than one disc.

States saved before this predate the naming and are not listed. They are left
alone rather than migrated: nothing in a .pst says which disc it was taken on,
so placing them would be a guess, and guessing wrong reintroduces exactly the
mix-up this prevents. Startup names them and the directory so they can be
renamed deliberately.

Verified: slot 1 saved on disc 2 and on disc 3 produced disc2/disc3 files side
by side with no collision; booting disc 1 and loading slot 1 rejected with
"missing state_800110C0_disc1_slot01.pst" rather than restoring another disc's
machine; the pre-existing untagged slot 0 was untouched throughout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194TYJAqgyG9a9PJe9uUcgQ
2026-08-29 21:50:00 -04:00
Matt Stanley f24b7e5d19 Merge pull request #266 from TheRealBiggs/fix/pal-timing
Fix PAL timing
2026-08-29 18:40:33 -07:00
Alex Vanderveen 1102d42022 runtime: [disc] selected is authoritative only when it is present
Launching from an external frontend with disc 2 or 3 chosen opened the game
on disc 1 every time, and the launcher preselected disc 1 with it.

settings.toml carried the right answer:

  [disc]
  path = ".../Final Fantasy VII (USA) (Disc 2).cue"

with no `selected` key. The frontend writes the index only when it can work
out the roster position, and it compares its stored choice path to the disc
path exactly -- a .cue/.bin or normalisation difference misses, so it writes
`path` alone and leaves the index out.

The resolver then treated a MISSING key as selected = 1 and did what it was
designed to do for a stale path: overrode disc 2 back to disc 1. `path` alone
selected a disc correctly before the index existed, so this was a regression
introduced with the index, not a pre-existing gap. A new field may add a way
to choose a disc; it must not take away the old one.

Now the index decides only when it is present. Absent it, the path decides and
the index is derived from it (stem-compared, because everything downstream has
been through normalize_disc_path_for_launch, which swaps a .cue for its .bin
-- exactly the drift that made the frontend's own match miss). An off-roster
path is not evidence for any disc and leaves the default alone.

Verified all three ways against the real install's state: path=disc2 with no
index boots SCUS-94164 and the launcher preselects Disc 2; path=disc2 with
selected=3 still boots SCUS-94165, so an explicit index keeps overriding a
stale path; the roster-derived index keeps a later save writing a consistent
pair.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194TYJAqgyG9a9PJe9uUcgQ
2026-08-29 21:39:21 -04:00
Alex Vanderveen 8f3645c2bc Merge pull request #268 from mstan/master
upstream
2026-08-29 21:07:42 -04:00
Alex Vanderveen 60203cc160 Merge feat/host-pacing-and-hotkey-focus into feat/multi-disc-launcher
Folds the two runtime fixes that were also open as their own PR (#260,
fix/host-frame-pacing) into this branch:

  - runtime: don't fast-forward while the window lacks input focus
  - runtime: pace the frame even when driver vsync should own the cadence

Both branches already shared this branch's base (d14ca691, c481b6cd,
10f443eb), so the merge adds only those two. They are patch-identical to
#260's commits (verified by git patch-id), so #260 is superseded rather
than dropped and can be closed without losing work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L1TpXRJve6WvwyZbSJAbCL
2026-08-29 20:30:39 -04:00
Matthew Stanley 3d1cb5013f runtime: keep PAL cadence off 60 Hz host sync 2026-08-29 17:23:52 -07:00
Biggs McWedge c473fb1555 Use correct timing for PAL games
This fix changes the value of `vblank_cycles` and `g_frame_period_ms` at startup to match what is expected if it detects the game is using a PAL disc image.
2026-08-30 00:36:44 +01:00
Biggs McWedge 96c3a46ebd Make VBLANK_CYCLES editable at runtime
This change replaces the constant `VBLANK_CYCLES` with a global variable that can be edited at runtime. This allows the timing to be changed by other code, which is needed for PAL support.
2026-08-30 00:35:58 +01:00
Matt Stanley 3aeef99f47 Merge pull request #258 from mstan/fix/hermetic-toolchain-deps
build: find dependencies the hermetic toolchain can actually compile
2026-08-29 16:35:17 -07:00
Matt Stanley 900ecd6537 Merge pull request #257 from mstan/fix/runtime-exe-name-single-source
build: let CMake own the runtime executable name
2026-08-29 16:34:50 -07:00
Matthew Stanley 9ebd8b374d build: publish final runtime executable name 2026-08-29 16:29:05 -07:00
Biggs McWedge aba1b8bf43 Fix cdrom_set_disc_scex not being called with correct value
In `main()` in the game runtime, `cdrom_set_disc_scex()` is never called due to an empty serial/game ID being passed to `identify_disc()`. This fix passes the correct ID. It also passes the correct checksum for good measure.
2026-08-29 15:59:17 -07:00
Alex Vanderveen 7edfe486d6 Merge pull request #264 from mstan/master
upstream
2026-08-29 16:45:55 -04:00
Alex Vanderveen 720c4f32cd Merge pull request #263 from mstan/master
upstream
2026-08-29 16:45:30 -04:00
Alex Vanderveen 2ac6d181d9 Merge pull request #262 from mstan/master
upstream
2026-08-29 16:44:57 -04:00
Alex Vanderveen 6d564c8eaf runtime: pace the frame even when driver vsync should own the cadence
present_should_wall_pace() skipped the wall-clock pacer entirely whenever
present_vsync_owns_cadence() was true, trusting the GL swap to block. A swap
that does not block then leaves the guest with NO speed limit at all and the
game simply free-runs -- indistinguishable, to a player, from a stuck
fast-forward.

Measured on FF7 (NVIDIA GL under a compositing WM), 45 s, nothing held,
telemetry off:

  driver vsync "owns cadence"   4771 frames  ~106 fps   1.77x
  PSX_VSYNC=0 (pacer forced on) 2651 frames  ~58.9 fps  1.00x
  this change, default settings 2643 frames  ~58.7 fps  0.98x

Running the pacer underneath vsync is safe because frame_pacer_wait() is
deadline-based rather than a fixed sleep: when the swap has already consumed
the period, now >= next_deadline and it returns immediately after advancing
the deadline. So it costs nothing where vsync works -- and in particular adds
no latency to the healthy path, which is why vsync was given sole ownership
in the first place -- while supplying the cap where vsync does not block.

The frame-interpolation exclusion is kept: that path subdivides this same
interval and would genuinely double-pace.

The startup line is corrected with it. "wall-clock pacer skipped" is how a
bring-up session decides pacing is accounted for, and it is what sent this
investigation hunting a phantom stuck fast-forward instead of an unpaced
present.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194TYJAqgyG9a9PJe9uUcgQ
2026-08-29 15:01:47 -04:00
Alex Vanderveen cf0714852b runtime: don't fast-forward while the window lacks input focus
Hold-to-turbo could run with no key held, indefinitely, and the game would
sit at the fast-forward multiplier until something disturbed it.

SDL_GetKeyboardState() returns a CACHED array that SDL updates from events.
A key whose key-UP is delivered to a different window stays latched there
with nothing to clear it -- and the canonical way to move focus away is
Alt+TAB, where TAB is exactly the fast-forward bind. Regaining focus makes
SDL reconcile the array, which is why the symptom appears to stop as soon as
you click back into the window and press anything (the report that surfaced
this blamed the FPS-readout key for "stopping the turbo"; it was the focus
change that came with reaching for it).

Gate the held-key read on real input focus. This is independently correct:
hold-to-turbo must not run while the player is typing in another
application. Edge-triggered hotkeys are unaffected -- they go through
host_keymap_match() on real key EVENTS, which are only delivered to the
focused window in the first place.

Measured on FF7, 45 s of gameplay with nothing held, telemetry off:
9678 guest frames before, 4771 after. Attribution was confirmed by changing
PSX_FAST_FORWARD_SPEED, which only manual_fast_forward_multiplier() reads and
which is only reached inside this branch -- the speed tracked it, so the
branch was demonstrably executing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194TYJAqgyG9a9PJe9uUcgQ
2026-08-29 15:01:33 -04:00
Alex Vanderveen c481b6cd4e runtime: expose the resolved window-icon path for the launcher
recomp-ui takes a host-supplied icon path (GameInfo.window_icon_path) so the
launcher window carries the same art as the game window. Publish the path
psx_apply_window_icon() already resolves, rather than letting the launcher
repeat the search — repeating it is what would let the two drift apart the
next time the candidate list changes.

Resolved once and cached: both the game window and the launcher ask, and the
answer cannot change within a run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194TYJAqgyG9a9PJe9uUcgQ
2026-08-29 11:04:30 -04:00