2505 Commits
Author SHA1 Message Date
Joe Mattiello 34696082db Merge pull request #1146 from libretro/release/v3.11.0
release: v3.11.0 — emulation thread on by default
v3.11.0
2026-11-01 18:55:52 -08:00
Joseph Mattiello 09e9c17a60 Merge remote-tracking branch 'libretro/develop' into release/v3.11.0 prerelease 2026-11-01 21:08:32 -05:00
Joe MattielloandClaude Opus 5.5 862f77376f fix(app): book inline steps on the vblank grid, not on swap returns (#1141) (#1145)
* fix(app): book inline steps on the vblank grid, not on swap returns (#1141)

Inline mode with the swap interval at 1 fed the pacer the real time between steps.
A step starts when its swap returned plus whatever the loop did since, which is
0.3-1 ms of jitter on a quiet host. With the 60.05 Hz core's backlog phase creeping
across a period boundary every ~18 s, that jitter flipped takes near each boundary:
bursts of repeats and dropped frames that tracked frame cost and host state
(Cybermorph 16 -> 22-25 repeats a minute once #1117 made its frames cheaper,
51-62 on a loaded host).

SwapGrid (pacing.h) tracks the display's vblank grid from swaps that blocked, as
LateLatch does for the threaded loop. It hands stepEmulation a correction, so the
backlog gains the time between the swaps' vblanks. The tracked phase follows the
real vblanks, so the sum stays real time: no speed change, a 59.94 Hz panel
announced as 60 Hz included. When not locked (vsync off or released, non-blocking
swaps, VRR) the correction is 0, as before. The threaded path is untouched.

Tests: pacing_test gains swap-return jitter in its simulated loop (real time vs
grid, 60 / 59.94 / 120 Hz, PAL) and unit tests for SwapGrid's lock, unlock,
telescoping corrections and relock.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): keep the inline grid log inside the inline branch (no_threaded_limits)

The log line added a 53rd 'am I threaded?' check to app.cpp; it now sits in
the inline branch that already holds the grid update.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-01 18:08:16 -08:00
Joseph MattielloandClaude Opus 5.5 8ad0d2b644 docs: v3.11.0 release validation
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 20:14:20 -05:00
Joseph MattielloandClaude Opus 5.5 5a976df863 chore: prepare v3.11.0 release
Version v3.11.0 in Makefile, .info and version_fallback.h; release notes and
WHATSNEW. Savestates stay v17 (state.h unchanged since v3.10.0).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 19:34:02 -05:00
Joe Mattiello a6769bb43e Merge pull request #1143 from libretro/gl-d3d12-first
fix(windows): bundled Mesa uses Direct3D 12 before falling back to llvmpipe (#1104)
2026-11-01 18:12:27 -05:00
Joseph MattielloandClaude Fable 5.1 0bb66be5cb fix(windows): reject the Basic Render Driver by vendor id and name, not only by flag
The hosted ARM64 runner enumerates "Microsoft Basic Render Driver"
through IDXGIFactory1 without DXGI_ADAPTER_FLAG_SOFTWARE, so the probe
accepted it as hardware and the retry ran on WARP again.  Also treat
Microsoft's vendor id 0x1414 and a description containing "Basic Render"
as software.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-01 18:09:05 -05:00
Joseph MattielloandClaude Fable 5.1 4e39bcdd20 fix(windows): pick d3d12 only for a hardware Direct3D 12 adapter, llvmpipe otherwise
Left to itself, Mesa's d3d12 driver takes the Microsoft Basic Render
Driver (WARP) when that is the only D3D12 adapter: on the hosted ARM64
runner the retry came up as "D3D12 (Microsoft Basic Render Driver)" and
the app exited with status 87 before its first screenshot, where the
same runner renders fine on llvmpipe.  WARP is software D3D12 under a GL
translation, so it is the slower software path anyway.

The launcher now enumerates DXGI adapters (dxgi.dll and d3d12.dll loaded
by hand, no link dependency), skips DXGI_ADAPTER_FLAG_SOFTWARE ones and
asks D3D12CreateDevice at feature level 11_0.  First hit: GALLIUM_DRIVER=
d3d12 plus MESA_D3D12_DEFAULT_ADAPTER_NAME pinned to that adapter.  No
hit: GALLIUM_DRIVER=llvmpipe and LIBGL_ALWAYS_SOFTWARE=true as #1138 had.
A GALLIUM_DRIVER already in the environment wins.  Both outcomes are
logged on stderr and the GL test workflow asserts one of the two lines.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-01 18:09:05 -05:00
Joseph MattielloandClaude Fable 5.1 c69181a112 ci(windows-gl-test): run on any pull request that touches the Windows GL path
The branches filter limited the qualification to PRs against develop, so a
PR stacked on another PR's branch (this one on #1138) got no run at all.
The path filter is what selects the workflow; the base branch is not.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-01 18:09:05 -05:00
Joseph MattielloandClaude Fable 5.1 76c25f3cd7 fix(windows): let the bundled Mesa use Direct3D 12 before falling back to llvmpipe
The automatic retry forced GALLIUM_DRIVER=llvmpipe and
LIBGL_ALWAYS_SOFTWARE=true into the child's environment, so every
machine that landed in the fallback (a stock Windows on ARM install, a
VM, a Remote Desktop session, the basic display adapter: anything where
WGL hands out the GDI Generic 1.1 driver) rasterized the UI and the
shader presets on the CPU, in competition with the emulation thread.

The MSYS2 Mesa we stage is built with the d3d12 Gallium driver as well:
OpenGL on top of the Direct3D 12 driver that every stock Windows 10/11
install has, the same layer Microsoft ships as the OpenGL Compatibility
Pack.  Mesa's WGL frontend takes d3d12 when a D3D12 adapter exists and
llvmpipe otherwise, so the child now leaves the choice to Mesa and only
sets SDL_OPENGL_LIBRARY and the retry guard.  GALLIUM_DRIVER and
LIBGL_ALWAYS_SOFTWARE pass through, so a user or the GL test workflow
can still pin a driver.  The existing "[gl] renderer=" log line names
the one that came up.

The GL test workflow accepts either renderer after the automatic retry
(hosted runners have no D3D12 adapter and will show llvmpipe); its
explicit software-path qualification still pins llvmpipe.  Docs and the
test-build note say what the fallback now is.  The software-opengl/
folder keeps its name.

Stacked on #1138.  Refs #1104.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-01 18:09:05 -05:00
Joseph Mattiello 11b9d2103f fix(windows): automatically retry graphics startup with bundled software OpenGL 2026-11-01 18:09:04 -05:00
Joseph Mattiello 1e1fdc55a3 fix(ci): record the qualification source without an MSYS Git dependency 2026-11-01 18:09:04 -05:00
Joseph Mattiello 6e10606315 fix(ci): qualify Mesa WGL in a separate portable app folder 2026-11-01 18:09:04 -05:00
Joseph Mattiello ee72df3bba fix(ci): install Python and isolate the software OpenGL provider 2026-11-01 18:09:04 -05:00
Joseph Mattiello 0853ebda86 test(app): skip real-context tests when the runner lacks OpenGL 3.3 2026-11-01 18:09:04 -05:00
Joseph Mattiello 0f8a8a4c49 fix(ci): normalize Windows paths when staging Mesa dependencies 2026-11-01 18:09:04 -05:00
Joseph Mattiello c6579e48ad fix(app): resolve Windows OpenGL through the active SDL driver 2026-11-01 18:09:04 -05:00
Joe MattielloandClaude Fable 5.1 bb2ee2c0ea perf(gpu): direct-threaded dispatch inside GPUExec (#569) (#1140)
GPUExec dispatched every emulated instruction through executeOpcode(): a
call, a return and the register spills either side of them, per op.  The
function could never be inlined into the loop: it holds label addresses
(computed goto), and it is recursive -- jump/jr run their delay slot
through it.

GPUExec now dispatches through its own computed-goto table to labels
inside GPUExec, where the (GPU_OP_HOT) handlers are inlined; the loop's
post-op bookkeeping follows unchanged.  executeOpcode() stays exactly as
it was for the delay slot.  One X-macro, GPU_OPCODE_LIST, generates both
tables and both label blocks, so the two copies cannot drift (it also
replaces executeOpcode's hand-written 64-entry table).  Non-GNU compilers
keep the executeOpcode() call.

Emulated output unchanged: frame_hash_ab CSVs byte-identical to develop
over 300 Doom gameplay frames and 400 yarc frames; gpu_idle_probe_falsify
passes (the idle probe keys on index 53 after dispatch, as before).

Measured on top of #1137, Doom gameplay from one savestate:
- cachegrind (ir_ab.sh --ref, 300 frames): Ir -4.07%, D1 miss -4.13%,
  I1 miss +2.80% (GPUExec is larger), LL +0.16%.
- Mac Studio M4 Max wall, test_benchmark 900 frames, A/B/B/A x3:
  avg 3.60 -> 3.38 ms (-6.1%), p99 4.3 -> 4.0 ms, every B run faster
  than every A run; yarc 3.39 -> 3.34 ms (-1.6%).

Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-11-01 16:21:07 -05:00
Joe MattielloandClaude Fable 5.1 b242c67911 fix(gpu): movei fast path, write the upper bound without adding to gpu_pc (#569) (#1139)
* fix(gpu): movei fast path, write the upper bound without adding to gpu_pc

Review of #1137: `gpu_pc + 4 <= GPU_WORK_RAM_BASE + 0x1000` wraps for a pc
within 4 bytes of 0xFFFFFFFF (a wild jump target), so such a pc passed the
range check and indexed gpu_ram_8 far out of bounds.  Compare gpu_pc
against GPU_WORK_RAM_BASE + 0x0FFC instead: the same set of in-RAM
addresses, no arithmetic on the pc.  No behaviour change for any valid pc
(frame hashes identical on Doom gameplay).

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* test(gpu): movei immediates at both ends of local RAM (#569)

Two placements of movei at the end of the 4 KB pin the fast-path bound
from both sides: the immediate in the last two words (pc == base +
0xFFC after the opcode, the last position the fast path may take) and
the immediate one word past the end (general path; a bound off by one
the other way reads gpu_ram_8[0x1000..0x1003], which the sanitizer CI
job turns into a failure).  Asked for in the review of #1139.

A pc of 0xFFFFFFFC itself cannot be exercised: the opcode fetched there
goes through JaguarReadWord, which masks to 24 bits and lands in
unmapped space, so no movei can be made to execute at that address.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-11-01 16:19:10 -05:00
4feed33e2d feat(app): schedule the emulation thread as a real-time thread (#1026) (#1135)
* feat(app): schedule the emulation thread as a real-time thread (#967)

The frame loop is a periodic deadline job; ask the OS to schedule it as
one when the thread starts (app/src/thread_realtime.h): macOS
THREAD_TIME_CONSTRAINT_POLICY (period one frame, computation 30%,
constraint 60%, preemptible; runs ahead of every QoS class and on
performance cores, so it replaces the user-interactive QoS raise, which
stays as the fallback), Windows THREAD_PRIORITY_TIME_CRITICAL plus the
MMCSS "Games" task, Linux SCHED_RR at a low real-time priority where
RLIMIT_RTPRIO or rtkit allows it.

Why: on an M2 Max at a 1-minute load of 250-950 the same Doom gameplay
frame cost 5.4 ms with a performance core of its own and 18-20 ms
without one (58-86 involuntary context switches a frame; the game ran
at 54.6 of 60.05 fps).  Never fatal: every refusal leaves the thread as
it was, and the log line "[emu] emulation thread on; scheduling ..."
says what it got.  --emu-priority normal skips it for one run (A/B).

emu_thread_unit: testRealtimeNote (asked for by default, off on request,
note always set before start() returns).

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* Mark realtime variable as maybe unused

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

* Change library loading method for avrt.dll

Updated LoadLibraryW to LoadLibraryExW for better library search.

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-11-01 15:10:16 -05:00
Joe MattielloandClaude Fable 5.1 d559e2f031 perf(gpu): inline the hot opcode handlers and fetch movei's immediate from local RAM (#569) (#1137)
A sampling profile of a GPU-bound frame (Doom gameplay, the GPU renderer
running its full 26.6 MHz budget at 442k ops/frame) put 63% of core time
in the RISC interpreter and 30% of that in GPUExec's own loop body, and
showed 32 of the 64 opcode handlers as separate symbols: every load,
store, movei, add/sub and shift was a call, a return and a reload of the
operand globals per emulated instruction, because at -O3 the compiler
declined the INLINE hint for them.  movei fetched its 32-bit immediate
through two GPUReadWord calls (range-check chains the compiler cannot see
through), ~7% of the frame on its own.

- GPU_OP_HOT: __attribute__((always_inline)) (/__forceinline on MSVC) on
  the 32 small, non-recursive handlers.  jump/jr stay out of line (they run
  their delay slot through executeOpcode() itself) and so do div, mmult,
  normi and pack (large, rare).
- movei: when both immediate words lie in local RAM, read them from
  gpu_ram_8 the way the exec loop reads the opcode; an immediate
  straddling the end of local RAM takes the old path.

Emulated output unchanged: frame_hash_ab CSVs byte-identical to develop
over 300 Doom gameplay frames (ir_ab's accuracy gate agrees).

Measured, Doom gameplay from the same savestate:
- cachegrind (ir_ab.sh --ref, 300 frames): Ir -2.00%, D1 miss -0.89%,
  I1 miss +0.22%, LL -0.06%.
- Mac Studio M4 Max wall clock, test_benchmark 900 frames, A/B/B/A x3:
  develop avg 4.11 ms / p50 4.11 -> branch avg 3.85 / p50 3.86 (-6.5%).
- make test on the Studio: 56103/56103 pass.

Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-11-01 01:33:39 -04:00
a4634e0819 test(tools): ir_ab.sh IR_STATE, a savestate loaded before the measured frames (#1136)
* test(tools): ir_ab.sh IR_STATE, a savestate loaded before the measured frames

Every ir_ab workload so far was a boot sequence or a demo.  A perf change
aimed at a game's render loop needs the Ir count of GAMEPLAY, which is a
savestate away: IR_STATE names a raw core savestate (the app's .state
files, or test_benchmark --save-state output); it is copied into the work
dir on the host so the container run sees it, and frame_hash_ab gets
--load-state for both arms.  Pair it with IR_ROM of the same game.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* Fix comment typo in ir_ab.sh

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-11-01 01:32:30 -04:00
Joe MattielloandClaude Opus 5.5 4900c62dcc build(app): CMake presets, and warn when no librashader runtime will ship (#1132)
* build(app): CMake presets, and warn when no librashader runtime will ship

- `app/CMakePresets.json` adds three presets: release, debug and nightly (the nightly channel, where the emulation thread defaults on). Each builds in `app/build-<preset>/`. Run them from `app/`: `cmake --preset release && cmake --build --preset release -j`.
- VJ_APP_FETCH_LIBRASHADER can stay OFF in the cache from an earlier offline or packaging configure, and the default never comes back. Configure now warns about that where a prebuilt exists (Windows, Apple silicon). Otherwise a local build ships no runtime and the Shader section reports the library missing. Configure also names the prebuilt runtime it picked.
- Ignore `app/CMakeUserPresets.json`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* build(app): the release and debug presets set the dev channel

A cached VJ_BUILD_CHANNEL=nightly from an earlier -D survived the presets, which set no channel.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-01 01:30:53 -04:00
Joe MattielloandClaude Fable 5.1 7637eeda23 feat(app): every build channel defaults the emulation thread On (#1039 5c) (#1134)
The 4.0 flip from the phase 5 plan: emuThreadDefaultFor() is true for
release, nightly and dev (an unknown channel string stays Off), and
VJ_EMU_THREAD_DEFAULT_VALUE is 1 for every channel.  The channel still
names the build in About and the log.  Unchanged: a stored ui.emu_thread
wins, --emu-thread off runs one start inline, a failed thread start falls
back inline, headless runs stay inline unless --emu-thread on.

Called by the maintainer with G1-G3 met and G4/G5 still open; the phase 5
plan and the design doc's section 8 record that.  The default smoke tests
now expect "default on" in every channel; the unit test pins all three
channels On and unknown ones Off.

Refs #1039, #967.

Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-11-01 01:30:39 -04:00
Joe MattielloandClaude Opus 5.5 dfc09f083d docs(app): phase 5 evidence, the G3 halt and phase 3 gate rows (#1039) (#1133)
- Halt, mode A vs mode B: 0 underruns in each.
- Phase 3 Studio gate: two quiet-host passes (halted p99 about 18 ms). One run under load failed, and one was skipped.

Refs #1039

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-01 00:41:39 -04:00
Joe Mattiello c849128d48 feat(packaging): prepare Steam and sandboxed Mac App Store builds (#1112)
* feat(packaging): prepare Steam depots and Mac store metadata

* feat(macos): add sandboxed Mac App Store build and signing

* fix(macos): persist sandbox access when re-adding a library
2026-10-31 22:41:49 -04:00
Joe Mattiello aaa20f8e9a feat(app): prepare Windows Store MSIX packaging and listing (#1111)
* feat(app): prepare Microsoft Store MSIX packages and listing draft

* test(windows): qualify installed MSIX and capture Store screenshots

* fix(windows): resolve qualification graphics from MSYS2 output

* test(windows): retain installed app logs and reuse qualified builds

* fix(windows): read installed process exit status reliably

* test(windows): keep MSIX CI focused on installed runtime qualification

* fix(windows): harden Store SDK discovery and workflow triggers
2026-10-31 22:41:37 -04:00
Joe MattielloandClaude Opus 5.5 efa7141796 docs(app): phase 5 qualification record and how to report a threading problem (#1039) (#1131)
- Fill the phase 5 plan's evidence table:
  - G1 and G2 from #1130.
  - G3: the mode B bar and its transitions (#1128).
  - A same-host A/B of mode A against the phase 1 build. It finds no mode A regression; the extra Cybermorph repeats appear inline too.
  - The inline halt arm, which --perf-halt cannot drive.
  - G3 halt A vs B, the phase 3 gate, G4 and G5 are still pending.
- Correct G3's gate label: emu_thread_gate_check.sh is the phase 3 Studio gate.
- README: a "Reporting a threading problem" section. It names the About line, the Frame pacing "Now:" line, the display, an --emu-thread off comparison and the log prefixes to include.

Refs #1039

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-31 21:17:15 -04:00
Joe MattielloandClaude Opus 5.5 43383a050a docs(app): emulation thread phase 5 plan, qualify the stable default (#1039) (#1129)
Gates G1-G5 before releases default the emulation thread On: threaded
twins for the 20 app tests without one, every CI platform, the Studio
60 Hz measurement, Windows/Linux/120 Hz reports and a two-week nightly
window. Three PRs: 5a twins, 5b evidence record, 5c the flip (last).
The inline loop is retired only after a default-on release ships.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-31 15:01:58 -04:00
Joe MattielloandClaude Opus 5.5 06fa8a7193 test(app): threaded twins for the remaining app tests (#1039) (#1130)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-31 15:01:42 -04:00
Joe MattielloandClaude Opus 5.5 fe80b36a2b feat(app): Frame pacing "Match display" (mode B) (#1038) (#1128)
With the emulation thread on, Settings > Video > Frame pacing offers
Jaguar rate (default, mode A) or Match display (mode B). In mode B each
emulated frame starts on a display tick, the audio rate follows the
display, and the UI swaps as soon as the tick's frame is ready.

- video.pacing setting, --pacing jaguar|display (one run, not saved),
  Video tab and TV rows with live state, reset class.
- LateLatch measures the real vblank period (59.94 Hz panels that
  report 60 lock correctly) and gives a last-chance wait for heavy
  frames.
- The UI evaluates DisplayLock after each swap, publishes the tick grid
  after draining input, and re-evaluates on display/fullscreen events.
- The emulation loop runs frames on ticks, with a backstop 2 ms past a
  late tick and mode A when the grid is stale or fast-forwarding.
- latchWait ends on the tick's frame publish.
- One notice per lasting fallback reason; link play forces mode A.
- Perf trace columns mode, tick_seq, ticks_dropped, lock_reason.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-31 03:50:19 -04:00
Joe MattielloandClaude Opus 5.5 507d11f4c9 feat(app): mode B's pure parts: display lock, tick pacing, audio base ratio (#1038) (#1125)
Phase 4a of the emulation thread plan (docs/app-emulation-thread-plan-
phase4.md).  Nothing calls these yet; 4b wires them in.

- DisplayLock (new, header-only): one verdict per swap.  It locks when
  k = round(display / core) puts display / k within 0.3% of the core's
  rate, measured from the vblank period: NTSC on 60 / 59.94 / 120 /
  119.88 Hz, PAL on 50 / 100 Hz; 144 and 75 Hz fall back for both.
  Each fallback has its own reason (setting, no vsync, latch unlocked,
  ratio, fast-forward, link play).  The display conditions must hold
  120 swaps in a row to enter mode B, and B ends on the first swap they
  fail; fast-forward and link play switch at once.
- PacingClock tick path: one frame every k ticks.  More than one frame
  behind drops the owed ticks and runs one catch-up frame.  After a drop
  it runs one extra frame per k ticks until the audio queue is back at
  its target: the frame that ends a stall and the catch-up both queue
  audio, leaving the queue near half the target, which the trim alone
  takes ~6 s to restore.  Keyed on the drop, not a queue level: at a
  20 ms target one frame's audio is most of the target, and a level rule
  fired in steady state.  A catch-up frame counts as its tick's extra,
  so no tick runs 3 frames.  The first tick call enters B; the first
  nextDue() after B resets the mode-A grid, so nothing owed crosses a
  switch.
- AudioOut::setBaseRatio: the stream plays at (display / k) / core and
  update(true) trims +/-0.5% around it.  It writes the SDL stream's
  ratio itself, so leaving B (base 1) really puts the stream back at 1
  for mode A's update(false), which never touches it; clear() and a
  fresh open() return to the base, not to 1.

Tests: display_lock_unit (lock table, each reason, hysteresis);
pacing_clock_unit gains mode B through one loop shared with mode A: a
60.05 Hz core skips 3 frames a minute on 60 Hz and 7 on 59.94 Hz in
mode A, none in mode B; k=1/k=2; a stall; refill after a 200 ms stall;
steady state never refilling at audio.target_ms 20 / 64 / 200 ms with
jittery pulls; A -> B -> A switches.  audio_out_unit asserts the stream's own ratio.
Mutating each rule fails at least one check.

The plan doc gains the refill rule.

Refs #1038

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-30 21:25:38 -04:00
Joe MattielloandClaude Opus 5.5 6afa35e6b4 test(app): give-up checks only rule out a hang, not a slow runner (#1126)
testCallGivesUp asserted call() returned within 300 ms of the predicate
turning true; a starved macOS runner (every 1 ms sleep took 8 ms) took
319 ms and failed PR #1125's Packages / macOS universal job.  call() has
no timeout, so the only bug the ceiling can catch is a predicate that is
never consulted, which hangs.  Use the file's 2 s no-hang bound there and
in testAdvanceGivesUp (500 ms, same class); the lower bounds, which prove
the wait lasted until the predicate turned true, are unchanged.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-30 21:23:47 -04:00
Joe MattielloandClaude Opus 5.5 3eb9fef4fc ci: docs-only PRs skip the C/C++ and regression builds (#1124)
#1122 changed two files under docs/ and still ran the full C/C++ CI
matrix and the regression tests.  Both workflows' pull_request triggers
now ignore docs/** and **.md, as app.yml already does with its own
paths list.  No check on develop is required, so a skipped workflow does
not hold a PR.  The cd-boot-matrix format check that reads a doc lives in
pages.yml, which keeps its own filter.  Release PRs always carry code;
workflow_dispatch still runs either workflow on any branch.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-30 20:05:46 -04:00
Joe MattielloandClaude Opus 5.5 fbfb205399 test(netlink): jlink negotiate/discover ports below Linux's ephemeral range (#1123)
test_jlink_negotiate failed "setup: confirmed before testing the drop" on
both Linux jobs of #1122's first CI run (a docs-only PR) and passed on the
rerun.  Two things in the test made that hard to read:

- Its ports (45000-48002) and test_jlink_discover's (43000-46999) sit
  inside Linux's ephemeral range (32768-60999), where a runner's own
  outgoing connections can hold them, and the two bands overlapped each
  other although the comment said they were distinct.  They move to
  30000-31302 and 31400-32699: below every OS's ephemeral floor and clear
  of test_jlink_tcp's 17000-24999 and the app netplay tests' 20000-29999
  (the rule from #356).
- The fake peer ignored a failed bind()/listen(), so a taken port showed
  up later as "not confirmed".  It now fails by name with strerror, and a
  failed "confirmed" check prints whether the TCP link came up and what
  the last confirm sendto returned.

Whether a port collision caused the #1122 failure is not established;
the next failure will say which leg broke.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-30 20:05:06 -04:00
Joe MattielloandClaude Opus 5.5 2ec71554a3 docs(app): emulation thread phase 4 plan, mode B display lock (#1038) (#1122)
Plan for #1038 in two PRs: 4a the pure parts (DisplayLock verdict,
PacingClock tick path, AudioOut base ratio, fake-vsync tests) and 4b
the wiring (Video > Frame pacing setting, swap ticks, fallback notice,
link play forcing mode A, latch wake on frame publish, Studio
measurement).

Decisions agreed 2026-10-30: lock when |display/k - core| / core is
under 0.3% of the measured vblank period; 120 Hz validated by fake
ticks plus Studio 60 Hz runs; mode A while linked; wake on publish;
default stays Jaguar rate.

The design doc's mode B section named 144 Hz as a lock case.  It is
not (144 / 60.05 = 2.40); amended with the lock table's rule.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-30 17:05:53 -04:00
Joe MattielloandClaude Opus 5.5 d8f73d3553 perf(dsp): idle skip admits Iron Soldier 2's wait loop (#952) (#1121)
* perf(dsp): idle skip admits Iron Soldier 2's wait loop (#952)

Iron Soldier 2's DSP spends nearly all of its time in a 12-word loop
at $F1B0A6 that waits for the I2S interrupt handler to move a buffer
pointer in the alternate register bank:

  nop ; addq #1,r19 ; movei #$F1B730,r14 ; movefa r28,r28
  sub r31,r28 ; jr NN,+2 ; sharq #2,r28 ; addq #31,r28
  moveq #0,r30 ; cmpq #8,r28 ; jr C,head ; nop

The idle skip rejected it three ways over; each is now admitted, no
wider than needed:

- Reach.  The probe only ran for a closing jr at most 7 words back.
  It now covers the jr's full reach (15 words).
- An internal forward jr (`jr NN,+2`).  A body may hold ONE jr whose
  target is a decoded instruction after its delay slot and at most the
  closing jr, with a delay slot that is neither movei nor jr, and no
  exit jr alongside it.  That gives exactly two routes: not taken
  (1 taken jr, F = idleBodyCount + 1 opcodes) or taken (2 taken jrs,
  F - K, K = instructions skipped).  Both probe iterations must show the
  same pair, and it must be one of these.  Any detour has to fall
  through the closing jr and come back, costing at least 4 more than
  the legal route with the same taken-jr count (leaving can only add
  taken jrs), so it can never match.  The dataflow walk still checks
  the skippable instructions and forgets what they wrote at the join.
- A counter on the flag-setting addq (`addq #1,r19`).  A counter may
  now use addq/subq onto itself when its flags are dead: overwritten
  in full (Z, N and C, per the dsp_opcode_* bodies) on every route
  before anything reads a flag.  Flags no instruction reads cannot
  carry the counter's growth into a branch.

Probing every jr up to 15 words back means loops that never qualify
reach the probe more often (Doom +0.25% host instructions per frame),
so the probe now remembers the reject-memo entry it hit or added last
and returns at once, with no probe in flight, when the same loop comes
round again -- exactly what the memo scan would conclude.  Doom's cost
is then +0.11%.

dsp_idle_probe_falsify gains six programs: E/E2 (Iron Soldier 2's
loop word for word, internal jr taken and not taken) fire and still
leave when the polled register moves; F (counter Z read by the closing
jr), G (counter carry read by addc), H (taken internal jr, fall-through,
undecoded store and a jump back, matching F opcodes with two taken
jrs) and I (exit and internal jr together) must not.  With the
taken-jr pairing, the dead-flag rule or the exit/internal ban removed,
H, F+G and I fire respectively.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(dsp): dead-flag scan stops before the closing jr's delay slot (#952)

The closing jr has no entry of its own in idleBodyIdx, but its delay
slot is the last entry, so dsp_idle_flags_dead() walked into that slot
without seeing the branch read the flags first.  A flag killer there
(`addq #1,r19 ; jr NE,head ; cmpq #0,r0`) made the counter's Z look
dead, and the skip extrapolated straight past the loop's exit.  The
scan now stops short of the delay slot.

dsp_idle_probe_falsify gains program J, that loop at its own address
(F's reject-memo entry would otherwise mask it); it fires without this
fix and is rejected with it.

Reported by the Codex review on #1121.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-30 00:30:01 -04:00
Joe Mattiello 8700a9336c feat(cheats): convert nested constant password tables (#1120) 2026-10-29 20:06:57 -04:00
Joe Mattiello 82d18eb5d4 feat(cheats): ignore unreferenced static constant tables (#1119) 2026-10-28 22:22:12 -04:00
Joe MattielloandClaude Opus 5.5 db90f8e3ac perf(risc): idle skip no longer switches off under blit memoization (#952) (#1118)
The GPU and DSP idle-loop fast-forward was off whenever blit
memoization was enabled (or in verify mode).  Only blitMemoRecording
needs to gate it: while a blit is being recorded, the JaguarRead
family notes every read, and skipped loads would change what the memo
records.  Recording is set only around BlitMemoLaunch's synchronous
engine run (a GPU store to B_CMD can launch one with GPUExec on the
stack, but no GPU or DSP instruction executes until it returns), so
the check is belt-and-braces.

blitMemoMode on its own leaves the read hook off, and the write hook
(main-RAM page generations) is never reached by an admitted loop body:
the DSP admits no store, and the GPU admits only long stores into GPU
local RAM, which GPUWriteLong serves without any memo hook.  The
skipped iterations leave the memo exactly as interpreting them would.

The [perf] suppression warning, the option sublabel's "Inactive"
note, the blit_memo option text and the settings guide no longer list
blit memoization as an idle-skip suppressor.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-28 19:00:39 -04:00
Joe MattielloandClaude Opus 5.5 aaa9b5946c perf(dsp): idle skip admits loops with a forward exit jr (#952) (#1117)
Cybermorph's DSP spends ~91% of its instructions in a two-bit register
poll (btst ; jr NE,out ; nop ; btst ; jr EQ,head) that idle skip
rejected only for its second branch. Admit a jr whose target lies past
the closing jr's delay slot, and back it with a taken-jr count: each
probe iteration must take exactly one jr, the closing one, which closes
the detour route the opcode-count check alone cannot see.

Cybermorph RISC 2x: p50 5.49 -> 2.34 ms, p99 14.34 -> 11.13 ms on an M4
Max; host instructions per frame -45%. Skip on/off byte-identical across
all 160 corpus carts; four titles newly skip. dsp_idle_probe_falsify
gains Cybermorph's shape (must fire and still exit) and an exit-detour
exploit (must not fire; fires with the check removed).

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-28 19:00:12 -04:00
Joe Mattiello 615a5588d4 feat(cheats): accept constant locals in registration metadata (#1113)
* feat(cheats): accept constant locals in registration metadata

* fix(cheats): inspect macro expansions in purity checks
2026-10-28 18:58:57 -04:00
Joe Mattielloanddependabot[bot] 325f35b2b6 ci: bump actions/checkout, setup-node, setup-python to v7 (Dependabot #1107, #1108, #1109) (#1110)
* ci: Bump actions/checkout from 4 to 7

Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
(cherry picked from commit 9ec2dac144)

* ci: Bump actions/setup-node from 6 to 7

Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6 to 7.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
(cherry picked from commit 078e514404)

* ci: Bump actions/setup-python from 5 to 7

Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v7)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
(cherry picked from commit 2e676d167a)

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-26 18:32:39 -04:00
Joe Mattiello a5593e55d9 test(tom): preserve Atari Karts HC regression coverage and doc correction (#1096)
* test(tom): guard Atari Karts racing line after HC fix (#1085)

* ci: allow scoped temporary Linux runners for Karts validation
2026-10-26 10:06:58 -04:00
Joseph Mattiello d604c4ade6 add codex xcrun cmd
Signed-off-by: Joseph Mattiello <[email protected]>
2026-10-26 02:04:28 -04:00
Joe Mattiello fd06d26f75 feat(app): add raw memory bitmap viewer and width guessing (#1095)
* feat(app): add raw memory bitmap viewer and width guessing

* fix(app): preserve synthetic OP previews across content switches
2026-10-26 00:54:02 -04:00
Joe Mattiello ee4952328d feat(cheats): convert read-only integer lookup tables (#1097) 2026-10-26 00:53:41 -04:00
Joe MattielloandClaude Opus 5.5 1fb655012e fix(tom): derive HC readback from emulated time so AvP marines attack (#1094)
HC ($F00004) free-runs 0..HP once per half-line on hardware (JTRM), so
its low bits at a CPU read are effectively random.  Alien vs Predator
uses them as its marines' fire roll -- `move.w $F00004,d0 / and #7 /
cmp #2` at 68K $015ADA -- while its Predators roll the game's software
RNG.  The core returned a per-read counter reset to 0 / (HP+1)/2 every
half-line, so a once-per-AI-tick read always saw $0000 or $05A6: low
bits 0 or 6, never 2.  Marines walked up to an Alien player and never
fired (30 of 30 rolls measured from the corridor savestate).

The readback is now the elapsed fraction of the current half-line:
time to the next half-line event at slice start, less how far the 68K
has run into the slice (JaguarSliceElapsedUs, stall and clock scale
included), scaled to HP+1 ticks.  Byte reads of $F00004/5 use it too.
tomRam8[HC] is unchanged (the OP's second-half-line test reads bit 10),
and the read-phase counter is gone; nothing new is serialized.

Measured after: the same marine rolls all eight HC&7 values (2 on 12
of 111), crouches, fires, and the Alien's health drops.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-25 23:08:14 -04:00
Joe Mattiello c1653da97b feat(cheats): expand bounded integer settings into freeze choices (#1091)
* feat(cheats): expand bounded integer settings into freeze choices

* fix(cheats): retain cartridge identity in integer analysis failures
2026-10-25 22:23:21 -04:00
Joe MattielloandClaude Opus 5.5 98212bf95d fix(app): don't block in the GL swap when the display was asleep at launch (#1081) (#1089)
On macOS SDL emulates vsync with a CVDisplayLink: SDL_GL_SwapWindow waits on a
condition variable that the link's callback signals each refresh. The link is
made from the active displays when the GL context is created. With the display
asleep there are none, the link never ticks (not even after the display wakes)
and the first swap blocks forever, the emulation with it when it runs inline.
A display that falls asleep after launch does not block: the link survives.

SwapGuard (pure) decides whether the swap interval may be 1: never for a run
whose context was made with the display asleep, and (defensively) not while it
sleeps later. App::guardSwap() asks the platform layer (CGDisplayIsAsleep /
an empty CGGetActiveDisplayList) every 250 ms and before the swap's timestamps,
and every UI swap goes through it. Interval 0 is the existing vsync-off path,
so the emulation keeps its own clock while nothing can be seen.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-24 07:21:33 -04:00