Commit Graph
330 Commits
Author SHA1 Message Date
Joe MattielloandClaude Opus 5.5 96dc445fab test(matrix): score a black run whose 68K is stuck in an exception loop as a crash (#980)
* test(matrix): score a black run whose 68K is stuck in an exception loop as a crash

The cart matrix called a run GAME_CODE when the final 68K PC was in
RAM, cart space or the boot ROM.  Two crash states pass that test and
were published as "Runs; video undetermined headlessly":

  - parked in the loader's ILLEGAL guard ($1000: BRA.S *, vector 4),
    e.g. Mandelbrot Demo, Painter (bin), Gorf 2000, the Jaguar Server
    examples;
  - an RTE ping-pong between one faulting PC and the HLE RTE stub at
    $404, e.g. Rayman Demo, Phase Zero (PD), Hubble Nebula.

The probe now reads the pcQueue ring and the RAM vector table every
frame and reports trap_frames (consecutive frames, up to the last, in
which every ring PC is inside an RTE / BRA.S * handler that one of
vectors 2-11 names, or is one single other PC), trap_pc (the faulting
PC; for a parked guard, the PC saved in the exception frame) and
trap_handler.  cart_classify.sh turns 30+ such frames into
"? (trap_loop)" when the run also never put a picture up.  With video
the row stays GAME_CODE and the notes say the 68K is parked: BJL demos
(DEMO1, DEMO1B, DEMO1C, Ladybug) end their 68K main on purpose and let
the GPU/OP run the demo.  A legitimate idle loop is not caught: Jaguar
Server JAGOS's 14-PC wait loop reads trap_frames=0.  The classifier's
final-PC extraction is now anchored to the field so trap_pc= cannot
match it.

matrix_diff reads GAME_CODE -> "? (trap_loop)" as backward (rank 1);
the baseline refresh that lands with this records those rows.

Refs #975

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

* docs(matrix): refresh the cart matrix baseline for ? (trap_loop)

Full cart sweep on the corpus host (155 titles, both modes, FAST
blitter, develop + the classifier change).  Against the previous
baseline, matrix_diff reports 29 cells GAME_CODE -> "? (trap_loop)":
the 18 HLE rows the matrix triage found crashed in frame 1-60 and the
BIOS cells of the 11 RAM-loaded ones (the boot ROM is not used there,
so they mirror HLE).  These are the scoring being corrected, not a
regression: every one had 0 lit frames and a 68K parked in the $1000
ILLEGAL guard or ping-ponging with the HLE RTE stub.  Each is ticketed:

  #971  Mandelbrot Demo, Jaguar Server INTRO / INTROMOD
  #972  Painter (bin), Hubble Nebula (HLE)
  #973  Rayman Demo x2, BadCode4 (Metal)/(Modified) (rom)
  #974  Phase Zero (PD) (HLE)
  #975  authentic, no loader fix possible: Jaguar Server 50HZ / 60HZ /
        KEYB (helpers that need the resident server), Gorf 2000 (a
        stale-A0 address error in the game's own startup), BadCode4
        (Metal) (bin) [a1] (an incomplete build), Assassin (for BJL),
        FFYL [a2], Kasumi Ninja [a1] (bad / altered dumps)

Seven GPU/OP-driven BJL demos (DEMO1 x2, DEMO1B x2, DEMO1C, Ladybug
x2) keep GAME_CODE with video; their notes now say the 68K is parked.
Every other row is unchanged apart from the build stamp.  The legend
gains the new stage.

Refs #975

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-15 16:07:33 -04:00
Joe Mattiello 8bc5b029ad Merge pull request #966 from libretro/test/952-frame-time-tooling
test(frame-cost): frame-time distribution tooling, pacing_trace, VJ_PERF_CSV (refs #952)
2026-10-15 02:17:53 -04:00
Joe MattielloandClaude Opus 5.5 6879ca0084 perf(gpu): idle skip admits invariant local-RAM stores and the full jr reach (#957)
* perf(gpu): idle skip admits invariant local-RAM stores and the full jr reach

Doom's frame-wait loop (movei/moveq/store/movei/load/or/jr Z, 20 bytes)
never reached the GPU idle skip: the probe only looked at taken jr's up
to 14 bytes back, and gpu_idle_op_admitted rejected every store, so
Doom showed 0 fires and ~411k interpreted GPU opcodes per frame at 1x
(822k at RISC 2x) for no extra frames.

Admit long stores (store, store_r14/r15_indexed, store_r14/r15_ri) in an
idle loop when every register the store reads has zero per-iteration
delta (so address and value are the same each pass), the EA is
long-aligned inside GPU local RAM $F03000-$F03FFC (JTRM v8 p.36), the
written long is not a long any load in the body reads, and it does not
overlap the loop's own code. The probe's interpreted iterations already
performed the identical store, so the skip needs none of its own.

Widen the candidate window from 14 to the jr's architectural reach (30
bytes) and the decoded body limit to match.

Refs #952

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

* test(gpu): widen bytes before shifting in gpu_idle_probe_falsify

gpu_ram[...] << 24 promotes to int and overflows for bytes >= 0x80 (UBSan:
invalid-shift-base on $AA). Cast each byte to uint32_t first.

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-15 00:57:37 -04:00
Joseph MattielloandClaude Opus 5.5 0aa9584615 test(frame-cost): --no-arm keeps idle skip live (an armed vjtrace gates it off)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 00:37:45 -04:00
Joseph MattielloandClaude Opus 5.5 01954f1879 test(frame-cost): p95, over-2x-budget count and GPU idle-skip columns
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 00:37:45 -04:00
Joseph MattielloandClaude Opus 5.5 cc43d6f15e test(tools): corpus_ab_sweep AUDIO=1 -- A/B the audio stream too
frame_hash_ab --audio-hash appends a per-frame FNV-1a of the samples the
core delivered that frame, so corpus_ab_sweep.sh can report a title whose
audio changes while its video does not -- the class a DSP change most
often produces (#895).  ARGS passes extra frame_hash_ab arguments (--bios,
blitter option) to both arms.  Default output is unchanged without the
flag.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 19:44:58 -04:00
Joseph MattielloandClaude Opus 5.5 e672fea526 test(tools): xenowings_sfx_probe -- record Xenowings audio with fire held (#895)
State-driven like xenowings_ingame_video (menu -> B -> loading screen ->
first lit gameplay frame), then holds a fire button and writes the audio
to a 48 kHz WAV with a per-window RMS/peak/video-hash line, so music-only
and fire-held windows can be compared and a fix A/B'd by ear.  Can also
dump DSP and main RAM at a chosen frame (used to disassemble the DSP
sound-effect setup and joypad reader for #895) and prints a DSP PC
histogram.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 19:41:41 -04:00
Joseph MattielloandClaude Sonnet 5.5 a2f12297dc fix(gdb): make the stub work with stock gdb out of the box (#936)
- serve a real m68k target.xml for the 68K (architecture + the 18 'g'
  registers); the empty qXfer reply aborted gdb with "conflicting
  enabled responses"
- enumerate only the 68K as a gdb thread (one tdesc per connection);
  GPU/DSP halts are reported as a thread-1 stop plus an O console line
- implement D, vKill and k: reply, disarm, drop only the client, resume
  a parked halt; the emulator and the listener keep running
- drop the stale #ifndef ALPINE_FUNCTIONS gate that refused every
  Z2/Z3/Z4; the watch sites in jaguar.c were always compiled in
- stream long monitor output as O packets; clamp the snprintf
  accumulation in monitor regs/trace (it could write past its buffer);
  monitor trace [N] now shows the NEWEST N entries
- tests: engine unit tests, a live-core probe, and a stock-gdb script

Packet handling only: no per-instruction or per-memory-access path is
touched.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-14 02:52:49 -04:00
Joseph Mattiello ca73bea1b9 Merge remote-tracking branch 'libretro/feat/timing-s2-contention' into test/v3.8.1-timing 2026-10-13 18:04:25 -04:00
Joseph MattielloandClaude Opus 5.5 b10dd473cc test(tools): declare retro_serialize as bool in idle_skip_scale_ab
UBSan function-type-mismatch: the tool called retro_serialize through
int (*)(void *, size_t); the libretro ABI returns bool.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 11:52:48 -04:00
Joseph MattielloandClaude Sonnet 5.5 86136035a4 perf(risc): run the idle-loop fast-forward under any RISC clock scale
riscClockScalePct != 100 switched virtualjaguar_risc_idle_skip off in
DSPExec/GPUExec (and was reported as a suppressor by libretro.c). It was a
precaution from the #569 design, not a found hazard: SCALE_RISC_CYCLES is
applied where the budget is handed out (JaguarExecuteNew), and the probe
measures cost/opcost from live budget deltas, so the skip already works in
the scaled domain. The one scale-dependent per-instruction term
(gpu_stall_scale_accum) only moves on a nonzero gpu_bus_stall, which is
structurally 0 in an admitted loop body.

Drop the gate, the IDLE_SUP_CLOCK suppressor/sublabel/log text, and the
user-facing 'overclock switches idle-skip off' claims. Add
test/tools/idle_skip_scale_ab (skip on vs off per-frame state digest at
0.5x/1.5x/2x, wired into make test, --require-fires so it cannot pass
vacuously) and repoint test_option_explain cases 3/5/9 at dram_timing.

Refs #569 #378

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-13 01:58:41 -04:00
Joseph MattielloandClaude Opus 5.5 4bdc77972a docs(timing): S2 write-up -- report the CF canary failure, rerun disclosure, provenance tags
Records the Checkered Flag canary as a failed sanity check (knife edge kept
as a hypothesis), discloses the one re-run of the injection's low level,
scopes VJ_S2_INJECT to GPU waits, tags the ARB.NET excerpts as relayed by
a summarizing fetch (not verbatim, not simulated), and labels the 68K
overshoot mechanism as read from code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 01:26:17 -04:00
Joseph MattielloandClaude Opus 5.5 e8abe4c198 test(timing): S2 contention model arithmetic and VJ_S2_INJECT cross-check
test/tools/contention_model.py multiplies the census traffic by the fixed
latency function (low/central/high per term).  VJ_S2_INJECT=1|2|3 (VJ_TRACE
builds only) charges GPU external accesses that same function so the
emulator maps the predicted time onto the present rate.  The 68K census
hook now sits after the existing charge so release objects are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 00:41:41 -04:00
Joseph MattielloandClaude Opus 5.5 20a654f7a5 test(timing): census DSP instruction split and RISC disassembly in the probe
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 00:24:58 -04:00
Joseph MattielloandClaude Opus 5.5 82cdf0fdeb test(timing): S2 per-halfline bus-traffic census (TEST_EXPORTS only)
Counts every bus master's external traffic per halfline, independent of
the dram_timing option (whose charge sites are gated, which is how an
earlier census read zero GPU external accesses with timing off): 68K bus
cycles and clocks split work/spin by PC window, GPU external transactions
split by the gpu_busy windows, DSP 16-bit transactions, the OP occupancy
rule ungated plus RELEASE-bit counts, blitter B_CMD/BUSHI counts and
(BENCH_PROFILE) phrase traffic.  Nothing charges emulated time.  Compiled
only under VJ_TRACE; release objects unchanged.

test/tools/bus_census_probe drives it and dumps per-halfline CSV plus
hot-PC lists for spin-window verification.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 00:17:56 -04:00
Joseph MattielloandClaude Sonnet 5.5 d8f3438fbc test(timing): S0 Doom pace gate and Checkered Flag overcharge canary
test_doom_pace_gate: attract-demo fields/flip + gap histogram + GPU busy-time
per flip over fields 600-1500, tracking gate against the recorded baseline with
the 4.0 fields/flip silicon bar beside it. test_cf_canary: frame-cap budget and
fields per game frame from the mid-race savestate. Both exit 77 (recorded as a
SKIP) when the private ROM/state is absent. Baselines in
test/tools/timing_gate_baseline.txt. Wired into make test. Timing-campaign S0
(#408, #401).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-12 23:27:38 -04:00
Joe Mattiello 0a820e50d4 Merge pull request #873 from libretro/fix/866-gpu-store-align
fix(gpu): long stores force alignment in every addressing form (#866)
2026-10-12 00:07:02 -04:00
Joe Mattiello e32e5ac587 Merge pull request #871 from libretro/perf/netlink-hop-cost
perf(netlink): idle-side reply wait + frame-slot margin floor + on-device hop stats (#860)
2026-10-11 23:45:40 -04:00
Joseph MattielloandClaude Opus 5.5 d960422e2a test(gpu): Ruiner Pinball GPU-escape regression (#866)
ROM-gated, good dump only (CRC 5B6BB205). Fails if the GPU PC is sampled
outside local RAM within 600 frames, in HLE and --bios. Without the
store (r15+rn) alignment fix it escapes to $F1B128 at frame 56 (HLE) and
frame 547 (BIOS); with it, both stay in local RAM.

Refs #866

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 23:44:40 -04:00
Joseph MattielloandClaude Opus 5.5 4841d260f7 perf(netlink): wait for unprompted link bytes, add on-device hop diagnostic (#860)
A Doom link tic is 12 one-way single-byte hops (6 pairs, ping-pong).  Two
parts of the old behaviour made the tic rate depend on the vsync phase
between the two devices, collapsing to 5 tics/s at bad phases even on a
fast link: JLinkAwaitReply only engaged after we transmitted, so a side that
only ANSWERS spun on ASISTAT with no wait, finished its frame in ~2 ms and
sat in the frontend while the partner's byte arrived; every such hop then
cost a whole video frame.

uart.c now counts consecutive ASISTAT reads with nothing to send or receive
and, past 24 in a frame, calls JLinkAwaitIdle(), which blocks like the reply
wait (same frame budget and timeout) so the byte is seen the moment it
arrives.  A silent partner backs off exponentially (1..32 frames without idle
waits); any received byte resets it.  Real transports on the JagLink device
only (not loopback, not the voice modem).

Simulator, Doom 2P, tics/s averaged over 4 vsync phases, before -> after
(emu 2 ms, 5 us poll_receive):
  2 ms one-way 27.5 -> 27.5   2.5 ms 19.8 -> 26.0   3 ms 16.3 -> 20.8
  4 ms 7.5 -> 19.2   Wi-Fi A (2 ms+3 jitter+2% spikes) 8.9 -> 16.0
  Wi-Fi B (3 ms+8 jitter+3% spikes) 5.4 -> 9.0   3 ms with emu 6 ms 10 -> 15

Also: a once-per-5-s INFO log line while a link carries traffic (hops/s, mean
hop time, wait per hop, slipped-hop share, timeouts, budget cuts, emulation
cost, idle-wait hits) so the numbers can be read from the RetroArch log on
iOS; netlink_sim gains --btrace/--log/--loss-pct/--rto-ms/--udp-rto-ms/
--corrupt and the clock patch gains a byte-event hook; netlink_btrace_report.py
breaks hop time down by cause and analyses the Doom tic structure.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 23:01:35 -04:00
Joseph MattielloandClaude Opus 5.5 8c22f67b10 fix(blitter): charge only the 68000 for WMCJ's blits so gameplay stops tearing
White Men Can't Jump flickered heavily in gameplay at default settings, and
almost not at all with the 68K at 3x and the RISC chips at 2x.

Mechanism: the game triple-buffers. Each field it clears a back buffer at
about halfline 67, the GPU draws it with about 40 blits, and the 68000's VI
handler swaps the OP object pointer at halfline 507. The titledb row from
#827 enables blitter bus timing, and that model also made the GPU wait out
every one of its own blits at the worst-case cost (a DRAM row change on
every access). With that wait the draw started late (median halfline 244)
and in 86 of 125 fields was still running at the swap. The OP then showed
half-built frames: horizontal tears, and sprites missing for a field.

Disabling only the GPU-side charge produces output identical to having
blitter timing off entirely (median start at halfline 67, draw finished by
halfline 200, no spill past the swap). The intro logo fix still holds: boot
frames 0-300 hash identically and the logo is 43.8% lit. That fix needs
only the 68000 to be frozen during its own fade copies (JTRM v8 p.8 and
p.69).

The change adds blitter_timing value "m68k" ("68000 only"). GPU accesses
neither wait nor close the busy window, and GPU-kicked blits do not open
one. "enabled" keeps its old meaning, so no other title changes by default.
The WMCJ titledb row now selects "m68k".

Flicker metric over the attract-demo match, two saved start points of 600
frames each (frames where a third of the picture reverts to its state two
frames earlier / single-tear spliced frames):
  stock row (enabled)   68/12  95/15
  fixed row (m68k)      21/2   23/4
  m68k 3x + RISC 2x     12/2   16/1

Test: test/tools/fb_flicker_probe gates frames 1250-1850 from boot (limits
54 big, 6 splice; fixed 40/2). A control run with blitter_timing=enabled
must exceed the limits (68/12). test_titledb pins the row value.

Closes #736

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 23:01:24 -04:00
Joseph MattielloandClaude Opus 5.5 b3f4218b5a fix(core): zero-fill power-on RAM in BIOS mode too (#856)
Fight For Your Life (1996) trapped under the real boot ROM: it sizes a
byte fill from RAM $F9A90, which nothing ever writes. BIOS mode filled
power-on RAM with a per-word xorshift PRNG, so the read returned
$1B90172F. The fill overran from $F9F58 to $FA5C7, destroying the GPU
parameter block at $FA260 (written by the 68K at frame 685). The GPU
kernel read a garbage loop count at $F030B0, its output pointer swept RAM,
and its store at $F031CE put $022F0057 at $1D8870. The 68K copy loop at
$81A7B8 then took an address error into the boot ROM's $E005DC pad (frame
1032). HLE zero-fills and never hit it.

Bisected by RAM swap to the single longword $F9A90: zeroing only it fixes
the run, and $FFFFFFFF is benign too; only the PRNG word breaks it.
MEMCON1/2, 4 MB banking, the #851 mirror and GPU/DSP boot state are
exonerated.

DRAM power-on contents are undefined (MiSTer does not clear main SDRAM),
but real parts come up in near-uniform runs, not independent random words,
and the per-word fill was an upstream VJ modelling choice. The old
comment's premise that the BIOS clears most of RAM is also false:
frame-610 dumps show $3D000-$FE000 untouched. So both modes now start
from the same zeroed RAM (RAM[0..7] and any RAM-loaded image are still
preserved), as the maintainer chose.

Test: test_ffyl_856 runs the good dump (CRC $B14C4753, checked; the [a1]
bad dump is an authentic BIOS reject) under the boot ROM for 1400 frames.
It requires the handoff and fails if the 68K parks on $E005DC. develop
fails it at frame 1032 (handoff 611); with the fix it passes, ending in
game code at $8037DA.

Closes #856

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 17:23:55 -04:00
Joseph MattielloandClaude Opus 5.5 da241c9406 fix(libretro): latch the BIOS and PAL options at content load (#858)
check_variables() re-runs whenever ANY core option changes, and it
re-applied 'BIOS (Cartridges)' and 'PAL' every time. retro_load_game()
resolves vjs.useJaguarBIOS after its option read (a GPU-only/jagcrypt cart
forces the boot ROM on; CD boot mode sets it from bootConfig.showBootROM),
and the DSP reads that flag at runtime. So on such a cart, touching an
unrelated option silently turned the boot-ROM flag back off mid-session.
PAL rewrote vjs.hardwareTypeNTSC live, so the clock macros switched
standards while the AV timing (negotiated only at load) did not.

Both are now applied only before content is loaded, the same way
internal_resolution is. After load a change only logs a one-shot "takes
effect on restart" notice. 'BIOS (Cartridges)' gains the "Restart
required" sublabel marker, and moves from the live list to the load-only
list in test_option_explain case 8; settings guide section 7 lists it.

Test: test_cart_needs_bios gains a mid-session arm. It loads a GPU-only cart
with bios=HLE and pal=disabled, changes an unrelated option, then PAL
itself, and checks that useJaguarBIOS stays 1 and the standard stays NTSC.
develop fails 3 of its 4 checks (useJaguarBIOS=0 after the unrelated
change, NTSC flipped); with the fix all pass.

Closes #858

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 01:58:27 -04:00
Joe Mattiello 5756a3e7e8 Merge pull request #862 from libretro/fix/853-music-demo-bios
fix(dsp): instructions behind a D_FLAGS store use the pre-store register bank (#853)
2026-10-11 01:36:52 -04:00
Joseph MattielloandClaude Opus 5.5 83dbed262a fix(dsp): instructions behind a D_FLAGS store use the pre-store register bank (#853)
Music Demo (2002) (ScatoLOGIC) enters its DSP program at $F1B020 with a
store to D_FLAGS (REGPAGE=1 + I2S interrupt enable) immediately followed by
`movei #$F1B00C,r31` -- the stack pointer its bank-0 I2S handler uses via
`load (r31),r28`.  DSPWriteLong switched the register bank inside the
store, so the movei landed in bank 1, bank-0 r31 stayed 0, the first
interrupt pushed to $FFFFFFFC and the handler returned to garbage:
dsp_pc_escape at frame 5 under the real BIOS ($FFFFFFEF), and a runaway to
$C1903000 in HLE that the watchdog's 24-bit mask hid.

CAUSE / MODEL
JTRM v8 p.109 (p.59 for the GPU): "writing a value to the flag bits and
making use of those flag bits in the following instruction will not work
properly due to pipe-lining effects" -- at least one other instruction must
lie between.  The instruction already in flight behind the store still runs
with the OLD flags.  d220aa2 modelled that only for `jump` (Wolfenstein 3D's
`store; jump (r3)` epilogue, via dspPreStoreBank).  This generalises it:

- A DSP-issued D_FLAGS store no longer repoints dsp_reg/dsp_alternate_reg.
  The switch is deferred and committed (DSPUpdateRegisterBanks) when the
  existing retire delay runs out, so every operand of the instruction
  behind the store -- sources and destination, movei/moveta/movefa
  included -- uses the pre-store bank.  No per-operand check: the only
  hot-path change is the commit inside the already-present
  `if (dspFlagsRetireDelay)` countdown.  dspPreStoreBank is gone; jump's
  plain RM now reads the pre-store bank naturally.
- A taken jump/jr commits before running its inline delay slot (the second
  instruction behind the store, so Wolf3D's delay slot keeps the new bank
  exactly as before) and again at the redirect (Doom's `jump (r15); store`
  epilogue at $F1B028: the target still sees the new bank).
- Interrupt dispatch and host (68K/GPU) D_FLAGS writes already call
  DSPUpdateRegisterBanks, so they commit too.
- Window length unchanged: DSP_FLAGS_RETIRE_DELAY = 2 (the store + one
  instruction), JTRM v8's "at least one".  Software Reference v2.4 p.46
  (two; four for an indexed STORE) and Hardware Bugs & Warnings #11 (two
  NOPs) are documented as programmer margins; the indexed-store extra
  latency is not modelled.  Music Demo needs exactly one slot.
- Savestate: a window that straddles a frame boundary now leaves the live
  bank != the flags-implied bank, so the remaining delay rides in bits 1-2
  of the DSP chunk's existing active_bank byte (v17 extended in place, no
  layout change; older states hold 0/1 = no window).  Before this, a load
  silently dropped the window -- a run-ahead hole for Wolf3D's jump too.

The pipelined DSP executors (DSPExecP/P2/Comp) are declared but never
defined, so DSPExec is the only path.

GPU: same gap, not fixed here.  GPUWriteLong's G_FLAGS case swaps banks and
dispatches interrupts inside the store with no retire model at all; adding
one is the whole d220aa2 + this change again, so it is a follow-up.

TESTS
- test/test_dsp_unit.c Tests 17-21 (CI, no ROM): store REGPAGE then movei /
  move -> old bank, next instruction -> new bank; Wolf3D store+jump shape
  (target from old bank, delay slot new bank); Doom jump+store-in-slot
  shape (target new bank); a DSPStateSave/Load mid-window keeps the old
  bank.  Against the d14bb22 core Tests 17, 18 and 21 fail, 19/20 pass.
- test/tools/test_music_demo_dsp_bank (private ROM, skipped by name when
  absent), HLE and --bios, 1200 frames: DSP PC stays in local RAM,
  unmasked.  d14bb22: FAIL at frame 5 in both modes.  This: PASS both.

A/B (d14bb22 built in a pristine worktree vs this, dylibs copied outside
both trees; per-frame framebuffer FNV + per-frame audio sample count,
non-silent count, peak and mean-square; 1200 frames each):
- HLE, all 160 cart ROMs in the private corpus: 160/160 byte-identical,
  audio and video, including Wolfenstein 3D, Doom (3 dumps) and Music Demo
  itself (its audio is generated inside the ISR, which reloads r31, so the
  bug was inaudible).  Crash-watchdog line counts identical everywhere.
- BIOS mode (--bios), same 160 ROMs: 160/160 byte-identical, audio and
  video.  The only crash-watchdog change in either mode: Music Demo BIOS,
  dsp_pc_escape at frames 5 and 606 -> none.

Suite: `make TEST_EXPORTS=1 test` exit 0; test_audio_clipping ran
(Skyhammer RMS 3083.3, Iron Soldier 2 2598.3, 0.000% saturated) and
test_audio_presence ran (Iron Soldier 1 and risc=2x PASS).  c89-lint clean.

Headless identity is not a listen: a RetroArch check by ear is still owed
(DSP change, every title runs through it).

Closes #853

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 00:45:23 -04:00
Joseph MattielloandClaude Opus 5.5 73e957db01 fix(netlink): fit the reply wait into one video frame instead of overrunning the vsync
Doom (2P) over RetroArch netplay stuttered, and toggling Network Link
Latency Hiding changed nothing.  Measured in a deterministic two-instance
netpacket simulation (test/tools/netlink_sim.c: virtual clock, RetroArch
netpacket semantics read from netplay_frontend.c): Doom's tic exchange is a
chain of single-byte hops, each side's byte depending on the other's, so the
link only advances while the core is blocked in JLinkAwaitReply.  The
per-frame wait budget was 2*EWMA+2 ms clamped to [4,15] ms and ignored what
the rest of the frame costs: with a few ms of emulation the frame finished
past its vsync, the frontend skipped the next one, and the core ran at 30 Hz
(audio at half rate) with ~13 ms of dead time per frame.  Every frame overran
at 3 ms/way (30 overruns/s); a slower device overran by more.

The budget is now what is left of ONE frame after the measured emulation cost
(EWMA of frame time minus time spent waiting, taken in retro_run just before
the frontend calls: audio_batch_cb blocks on a full audio buffer and video_cb
on vsync, neither is emulation) and a 3 ms margin for those calls.  The margin
widens by 1 ms (max 8) whenever a frame that waited is still followed by a
late frame start, and decays after 60 clean frames, so an unmodelled frontend
cost (hi-res present on iOS) corrects itself.  The per-reply timeout stays
adaptive, so a silent peer still costs little.

Sim, Doom 2P deathmatch, wait enabled, old -> new image updates/s
(emu = virtual emulation ms per frame):
  3 ms/way, emu 2:      14.5 -> 22.2, overruns 30/s -> 0
  3 ms/way, emu 6:      11.1 -> 22.2, overruns 30/s -> 0
  3 ms/way, emu 10:     11.2 ->  9.6, overruns 30/s -> 0 (waits only ~4 ms)
  Wi-Fi jitter+spikes:  8.6/8.2/8.1 -> 9.6/10.7/9.8 over 3 seeds, overruns
                        28/s -> 0
  4 ms of unmodelled post-hook cost: overruns 30/s -> 0.5/s via the margin
Wait disabled is ~9 image updates/s on every link, so on a jittery link the
old wait was no better than off: that is why the toggle appeared to do
nothing.  Network spikes themselves stay (a byte-synchronous protocol cannot
hide them).

Also: netlink_delay_proxy gains --jitter-ms/--spike-pct/--spike-ms/--seed
(Wi-Fi model), netlink_game gains --trace, and test/tools/netlink_sim.c +
netlink_sim_clockpatch.py record the simulator (measurement-only clock hook,
never applied to src/).  No regression since 693f42a (#250): identical results
in the sim, and nothing in the arm/pump/flush path changed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 00:44:05 -04:00
Joseph MattielloandClaude Opus 5.5 be2185875e feat(libretro): restart-required sublabels, per-content option hiding, demotion OSD test (#850)
Follow-ups to #841 (#847).

- Restart marker: the info (sublabel) of every option that is read only at
  content load now starts "Restart required: applied when content is
  loaded." Static strings in libretro_core_options.h, so no re-push and no
  amendment slot is spent. Verified per option against where libretro.c
  consumes it: internal_resolution (latched in retro_load_game),
  enhancement_hooks (latched; check_variables only compares and logs),
  gdb_stub/bind/port/wait/halt_timeout (read only inside retro_load_game),
  bios_type (vjs.biosType used only by cart boot-ROM staging), cd_bios_type,
  cd_boot_mode and memory_track (consumed in open_disc_and_resolve_boot /
  CD BIOS staging), jgd (JGDLoadROM, only from the cart ROM loader) and pal
  (documented restart-only; see below). Redundant "requires a restart"
  sentences dropped. `bios` is deliberately NOT marked: check_variables()
  rewrites vjs.useJaguarBIOS live.
- Option hiding: with content loaded, the cartridge-only options (BIOS,
  Cart BIOS Type, GameDrive + SD card, enhancement hooks, blit memo) are
  hidden for CD content, alongside the existing CD-only hiding for carts.
  They share the one show_cart_options gate, so the visibility_force_push
  after a definitions re-push re-hides them. CD-only (not RAM-loaded
  executables). All SET_CORE_OPTIONS_DISPLAY calls go through
  option_display_push(), which reads the return value (libretro.h: "call
  is available"), latches a refusal and stops asking; reset in retro_deinit.
- Harness: records SET_CORE_OPTIONS_DISPLAY (a definitions push resets
  every row to visible, as RetroArch's rebuilt manager does), option
  descs, and can refuse the call. test_option_visibility now runs its disc
  half against the in-tree synth_jagcd.chd, so CI covers the CD side.
- test_option_explain cases 7-10, all on in-tree yarc.j64: runtime
  demotion OSD notice (synthetic enhancement row + underrun feed: one
  notice with the demotion reason, one re-push, nothing repeated, no
  SET_VARIABLE), the restart marker swept over every registered option,
  visibility surviving a re-push, and a refusing frontend.

Not changed: pal and bios still have live writes in check_variables()
(vjs.hardwareTypeNTSC / vjs.useJaguarBIOS), so a mid-session toggle
half-applies; out of scope here.

Closes #850

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 00:26:46 -04:00
Joseph MattielloandClaude Opus 5.5 972339444d fix(core): mirror 1, 2 and 4 MB cartridge images across the cart window (#851)
Flip Out (2 MB) executes `cmpa.l $A02000.l,a2 / blt / illegal` at $11EEAA.
A cart only drives the address lines it has, so a 2 MB image repeats at
$A00000 and the read returns the cart's own word at $2000 and the branch is
taken.  We returned 0 (jagMemSpace past the loaded image is zero), so the
ILLEGAL fired: HLE looped ILLEGAL/RTE at $11EEB4 from about frame 320, and
BIOS mode trapped to the boot ROM's $E005DC vector at about frame 810.

Ground truth: the MiSTer Jaguar core (Jaguar.sv) ANDs a cart_mask into
abus_out[22:20], armed only for an image of exactly 1, 2 or 4 MiB (mask
000/001/011); every other size is left unmirrored ("not exact=don't
modify").  The JTRM is silent, and jag_sim's cart model has no size-based
decode.  So only 1/2/4 MiB images (measured after a copier header is
stripped) are replicated, at load time, so the read path is unchanged:
1 MB repeats every MB, 2 MB at $A00000/$C00000, 4 MB once at $C00000.
Copies stop at $DFFF00 (the CDROM overlay), MemTrack's claimed window still
wins because MTClaimsRead is tested ahead of the ROM read, and a GameDrive
banked image is untouched.

jagMemSpace is static, so the cart window past the image is now cleared at
load and again in JaguarDone: a previous title's mirror (or the CD BIOS)
used to survive a reload in the same process.  JaguarMirrorCart() is
re-run after TitleHookApplyROM patches so the repeats carry the patch.

Tests: test_cart_format gains 7 mirror cases (distinct markers per
megabyte, 1/2/4 MB mirror, 3 MB and 64 KB do not, header-stripped size,
no leak across reloads); test_flipout_851 steps Flip Out for 1000 frames in
HLE and BIOS and fails if the 68K lands on the ILLEGAL or a trap landing pad
(skipped, and recorded in the skip ledger, without the private ROM).
Corpus A/B (160 carts, 600 frames, HLE, both blitters): only Flip Out
changes, plus a 1-pixel difference in Fight For Your Life (4 MB) where the
blitter reads past the end of the image and now sees the wrapped ROM bytes
instead of zero.

Closes #851

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 22:17:09 -04:00
Joseph MattielloandClaude Opus 5.5 e8be4e6817 feat(tools): score BIOS-mode cart boot rows from the boot-ROM handoff, add BIOS_REJECT
The cart boot matrix scored real-BIOS runs from frame 0, so the identical
~490-frame boot logo animation and jingle counted as every title's "video"
and "audio", and carts the boot ROM rejects (red "Jaguar" screen) read as
"BIOS works".

cart_boot_probe now finds the boot-ROM -> cart handoff itself (68K or any of
its last 1024 instructions, via pcQueue, in $800000-$DFFFFF) and restarts
scored_* counters there; --post-handoff N runs N frames past it (--frames is
the cap) and stops early once the boot ROM sits on its BRA.S * halt loop.
Headerless RAM-loaded .jag files never run the boot ROM (bios_ran=0): whole
run scored, never a reject.

The classifier moves to test/tools/cart_classify.sh (so a unit test can feed
it synthetic logs) and gains BIOS_REJECT (boot ROM ran, no handoff, parked on
BRA.S *) and ? (bios_trap) (cart got control, then PC fell back into the
boot ROM: a crash, not a reject). Crash-watchdog signatures stay whole-run:
an early escape is a real bug (#853), not excused.

matrix_diff.py ranks BIOS_REJECT equal to GAME_CODE: GAME_CODE -> BIOS_REJECT
is a lateral scoring correction, and HLE GAME_CODE / BIOS BIOS_REJECT is not
an HLE-vs-BIOS asymmetry. BIOS runs get FRAMES+700 frames and a 150s default
timeout. docs/cart-boot-matrix.md prose updated; table not regenerated.

Closes #852

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 20:29:53 -04:00
Joe MattielloandClaude Opus 5.5 8ed4fa939c feat(libretro): explain per-title presets and overrides in the frontend (#841) (#847)
- OSD (SET_MESSAGE_EXT via netlink_osd) at load naming applied per-title
  presets and their class ("White Men Can't Jump: Blitter Bus Timing on
  (compatibility preset)"), and when the enhancement profile withholds or
  demotes presets. Latched per load.
- Self-describing sublabels: a static copy of the options v2 definitions
  with amended info strings (preset applied / preset withheld + reason /
  idle-skip "Inactive: suppressed by <option>"), re-pushed via
  SET_CORE_OPTIONS_V2 only when the amendment set changes (load end, or
  check_variables after load), never from the update-display callback;
  originals restored on unload. Never calls SET_VARIABLE.
- RetroArch source check: a re-push re-detects per-game/per-folder option
  files (runloop_init_core_options_path) and re-reads values, so user
  overrides survive.
- Harness captures OSD texts / option info / SET_VARIABLE; new
  test/tools/test_option_explain (28 checks; WMCJ, AvP, yarc control,
  mid-session).

Closes #841

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-10 17:52:12 -04:00
Joseph MattielloandClaude Opus 5.5 3539fbb65b feat(tools): corpus gate -- matrix_diff.py, never-backward cart rule, pre-tag checklist (#749)
test/tools/matrix_diff.py OLD.md NEW.md compares two cart or CD boot
matrices per row. Stage rank (low->high): -1 ?(core_error|build_mismatch|
no_reg), 0 LOAD_FAIL, 1 HARNESS_HANG/?/?(pc_escape|timeout|BIOS band),
2..6 BIOS_INTRO, BOOT_STUB, GAME_CODE, MENU, IN_GAME. At equal rank a new
crash-watchdog signature (from the notes/Watchdog cell) is backward.
Prints regressed / still-asymmetric / improved / added / removed; exits
1 on any backward move, 2 on parse error. test/test_matrix_diff.sh (43
checks) runs in make test. The cart matrix header (generator + doc) now
carries the never-backward rule; docs/release-process.md gains the
pre-tag checklist (regenerate both matrices on the RC on the corpus
machine, diff vs the previous tag, a backward row blocks the tag until
ticketed and deferred, improved list feeds release notes).

Sanity: cart v3.7.0 -> v3.7.1 shows 5 regressed cells (Music Demo BIOS
GAME_CODE -> ?(pc_escape); inframe_hang newly visible on 4 homebrew).

Closes #749

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 14:36:59 -04:00
Joseph MattielloandClaude Opus 5.5 429e3b2c2e feat(titledb): key Jaguar CD titles by boot-stub CRC (#747)
CD titles could carry no per-title row: the DB was cartridge-CRC only.
Disc rows now live in their own table, keyed by the CRC32 of the boot
stub CDIntfExtractBootStub() returns. The issue proposed the
sessions/tracks/sectors layout instead; measured across all 53 corpus
images it is neither stable (CDI rips report +150 sectors against the
CUE/CHD of the same pressing in 11 of 12 pairs) nor unique (eight
homebrew discs share 2/6/18044). The stub CRC is identical across CUE,
CHD and CDI for every title whose stub extracts, and separates Vid Grid
from Vid Grid Rev 1. Its limits are documented: seven homebrew discs
share one loader, Songbird re-releases share the original's stub, and
discs with no extractable stub have no key.

- Separate table, so a cart CRC can never match a disc row or the
  reverse; 0 is never a key in either lookup. TitleDBContentCRC() stays
  cartridge-only, so texture dump/replace paths are unchanged for CD.
- The disc is probed (open, extract, close) before the option reads on
  the load path, so a disc row has a cart row's full reach, including
  cd_boot_mode and load-latched options. A disk-control insert re-keys
  and re-reads options before resolving the boot; a failed insert
  restores the previous disc's key.
- Every CD load logs its boot-stub CRC so row authors can read the key
  off the RetroArch log. The table ships empty.

Tests: test_titledb disc-table integrity and namespace isolation;
test_pertitle_db 15/16 (a synthetic disc row on Baldies' key resolves
the real-BIOS strategy; a non-matching key leaves HLE) and 17 (CUE, CHD
and CDI rips of Baldies all log $82B88060); test_disk_control 9/10 (the
same row applies on an insert after a no-content boot; control without
it). Case 15 fails with the probe removed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 23:29:03 -04:00
Joseph MattielloandClaude Opus 5.5 b92cb1a827 feat(titledb): compatibility row class with mandatory citation (#748)
Per-title pairs now carry a class. ENHANCEMENT (the zero-fill default,
so every existing two-field row is unchanged) stays governed by the
enhancement profile. COMPATIBILITY is what a title needs to work --
boot mode, a timing model -- and is never dropped by the profile.

A compatibility pair must carry a cite string naming its measurement
and the mechanism ticket it stands in for as "#N"; test_titledb fails
the table without one, so the row's retirement is findable when the
mechanism lands.

White Men Can't Jump's blitter_timing row (#736, PR #827) becomes the
first compatibility row, standing in for blitter timing on by default
(#408). test_pertitle_db cases 13/14 pin the contract end to end with a
synthetic row: the performance profile does not drop a compatibility
pair, and an explicit user value still beats one. Docs: the hooks
"is my idea a hook?" table and the settings guide, whose "every preset
is presentation-only" rule stopped being true with the WMCJ row.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 22:02:58 -04:00
Joseph MattielloandClaude Opus 5.5 2f17c675b1 fix(gpu): NORMI normalises to bit 23, not 22 (#611)
NORMI returns the right shift that normalises an unsigned integer "as an
IEEE 32-bit floating point value" (JTRM v8 p.52), so the top set bit lands
on bit 23, the hidden-bit position: NORMI 1 = -23, NORMI $800000 = 0,
NORMI $80000000 = 8. TOM's ARITH.NET "normalisation integer generator"
encodes exactly that, and gives -32 for a zero source. The old code
normalised to bit 22 and returned one too many for every input (and 0 for
a zero source).

Club Drive clips polygons against the screen with a four-entry corner table
indexed by (NORMI(outcode) << 3) & $1F, so every inserted screen corner was
rotated by one. A polygon crossing two screen edges (common while steering)
came out non-monotonic in Y; the edge walker bailed to the command
interpreter while a span callback still borrowed its return stack (r10) as
scratch, and the interpreter's return popped a garbage address into main
RAM. From the user's mid-race savestate, holding accelerate and steering
left escaped at frame 82; with the fix all six driving patterns run 2400
frames clean.

Tests:
- test_gpu_ops: NORMI cases from the netlist decode (the old expectation,
  9 for $80000000, encoded the bug).
- test/tools/test_club_drive_611: ROM- and savestate-gated; drives from
  states/Club Drive (1994).state3 and requires the GPU PC to stay in local
  RAM for 300 frames with the picture moving. Fails on develop at frame 82.
- Retire Native Demo (bin) as a test_blitter_hung reproducer: its GPU runs
  into data at frame 1 on develop, and the corrected NORMI changes that
  garbage path so it no longer issues the never-ending blit. The non-approx
  path stays covered by the synthetic test on both engines; Music Demo
  --bios still covers the approx path.

The DSP has the identical NORMI block and off-by-one; that half is fixed
separately on fix/dsp-normi-off-by-one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 23:20:49 -04:00
Joe Mattiello b34fd4bdb3 Merge pull request #827 from libretro/fix/736-wmcj
fix(titledb): turn on blitter bus timing for White Men Can't Jump (#736)
2026-10-07 22:32:06 -04:00
Joseph MattielloandClaude Opus 5.5 ada7055b47 fix(file): load $5000-linked raw binaries at $5000 (#818)
The raw-binary loader scored candidate bases only by whether absolute
JSR/JMP/LEA/MOVE targets fall inside [base, base+size).  $5000 was not
a candidate, and a $4000 window also contains almost every target of a
$5000-linked image, so such images loaded $1000 low: Chroma-Luma (bin)
ran its font data from its first JSR, the 68K hit ILLEGAL and parked at
$1000 (#800's guard), and the screen stayed black in HLE and BIOS mode
(RAM-loaded images never take the boot-ROM path, so both modes share
the failure).

Add $5000 and break overlapping-window ties on entry-point consistency:
a JSR/JMP abs.L target that is the image start or the word after an
RTS/RTE/RTR is a subroutine start under the right base and a
coincidence under a wrong one.  Among bases that clear the containment
bar, the most entry hits wins; with no hits anywhere the containment
winner stands, so images without the signal do not move.

Across the 155-title cart corpus exactly six load decisions change, all
$4000 -> $5000 and all from black/parked to running video: Chroma-Luma
(bin), JagMania (Jul 8), JagMarble (2000), FORCE Design Legion Force
intro, Joypad-TeamTap Tester [a1], and Music Demo (ScatoLOGIC), whose
#794 blitter spray in BIOS mode was this misload.  Music Demo is
therefore no longer a hung-blitter reproducer; it moves to the new
ROM-gated test_raw_binary_boot check next to the two #818 titles.

Closes #818

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 21:03:53 -04:00
Joseph MattielloandClaude Opus 5.5 b81857855b fix(titledb): turn on blitter bus timing for White Men Can't Jump (#736)
The game races its own GPU decoder against the blitter. $17BD8 starts
the GPU decoding the first logo into $0DCF00 and returns without
waiting. The fade-in at $0138AE then makes 64 pairs of SRCSHADE copies
out of that buffer, and each copy waits only for B_CMD to read idle
($015F0C).

On hardware every copy freezes the 68000. It is the lowest-priority bus
master (JTRM Rev 8 p.8), and the blitter keeps requesting the bus until
the operation completes (p.69), so the fade outlasts the decode. With
zero-time blits the whole fade ran in two fields. At that point the
decoder had finished 111 of 200 rows (RAM dumps at f19/f20/f21), and
the last copy kept 49 of them. The results:

- the Trimark logo showed only its apex (6.4% of the screen lit,
  against 43.8% for the full logo);
- the High Voltage logo never appeared;
- the spinning-ball title animation was lost.

All three happened in both HLE and real-BIOS boot.

The existing Blitter Bus Timing model charges the 68000 exactly that
bus time. This commit adds a titledb row that turns the model on for
this CRC and does not change the model itself. Fast and accurate
blitters are hash-identical on this title, so neither engine is at
fault. TOM bug 9 does not apply, because B_CMD $41802601 sets GOURZ.

With default options the frame-hash stream is byte-identical to an
explicit virtualjaguar_blitter_timing=enabled run, in both modes.
test_audio_clipping and test_audio_presence give the same result on
WMCJ before and after (RMS 1541.7, clean). A driven 6000-frame run
still reaches gameplay with no new static run.

The new test/tools/wmcj_intro_logo is ROM-gated and runs at default
options. It finds the first picture the game holds for 60 frames and
needs it to be at least 25% lit. Develop fails it at 6.4%, and so does
this build with per-title defaults disabled.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 20:35:06 -04:00
Joe Mattiello ab77d16b49 Merge pull request #825 from libretro/fix/811-xenowings-input
fix(dsp): indexed store after a divide writes the old data (JTRM bug 2, #811 input)
2026-10-07 13:13:08 -04:00
Joseph MattielloandClaude Opus 5.5 0e5e14ef6d fix(dsp): indexed store after a divide writes the old data (JTRM bug 2)
Xenowings (#811) ignored all in-game input.  Its DSP joypad reader ends
with

    div   r9,r8          ; r8 = active-low joypad bits, inverted
    store r8,(r14+1)     ; -> $F1C3A8, the game's pad variable

JTRM Rev 8 p.134, TOM/JERRY bug 2 "Scoreboard Failure on Indexed
Addressing Mode Stores" ("applies to both Tom & Jerry"): the data of
store instructions 49, 50, 60 and 61 is not scoreboarded, so when it is
the result of a divide still in flight the store writes the register's
old contents.  On silicon $F1C3A8 gets the joypad bits.  This core
wrote the quotient instead, so the game read a value that did not track
the pad and the ship never moved: 600 gameplay frames with held input
hashed the same as a run with no input.

Model it for the documented pattern only: a divide immediately followed
by an indexed store whose data register is the divide's destination
(and is not one of its address registers) stores the pre-divide value.
The armed state lives in DSPExec locals and the loop runs that one
instruction past its budget, as the bug-13 load fix-up does, so nothing
outlives a slice and there is no savestate change.  The idle-loop
fast-forward rejects a loop in which it fired.  Test-ABI counter
dsp_div_store_fixups counts applications.

Not modeled: a store later within the divide latency, the external-load
variant of bug 2, a divide in a branch delay slot, and the GPU side.

test/tools/xenowings_ingame_video now also runs a forked no-input
reference arm and holds RIGHT from the first lit gameplay frame; the
frame-hash streams must diverge within 600 frames.  Before: 0 frames
differ (FAIL).  After: 835 differ, first at +366 (PASS); the ship moves,
fires and scores.

Depends on the DSP bug-13 fix (#822) and the 68000 prefetch model (#823).

Refs #811

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 12:37:11 -04:00
Joe Mattiello 4a5cfa4263 Merge pull request #817 from libretro/fix/800-accurate-blitter-hang
fix: homebrew 68K runaway into the blitter (#800); never-ending blits hang the blitter, not the host (#794)
2026-10-07 09:01:10 -04:00
Joe Mattiello 7d9d496aee Merge pull request #824 from libretro/fix/gpu-bug13-idle-guard
fix(gpu): reject idle loops where a late load write-back fired (#811)
2026-10-07 08:19:53 -04:00
Joe Mattiello b9e0929da6 Merge pull request #823 from libretro/fix/811-m68k-prefetch
fix(m68k): model the prefetch queue for self-modifying code (#811)
2026-10-07 07:16:15 -04:00
Joseph MattielloandClaude Sonnet 5.5 74b3496f11 fix(gpu): reject idle loops where a late load write-back fired (#811)
GPUIdleLoopProbe proves a loop idle by walking its body in program order,
so for `load (rX),rY / moveq #n,rY` it concludes the overwrite wins.
Since #819 the interpreter lets an external load's data land last (JTRM
Rev 8 p.136, TOM bug 13), so for such a loop the dataflow proof describes
a different loop than the one that runs (e.g. a later load whose address
comes from the overwritten register can really hit register space).

Snapshot gpu_load_wb_fixups at S0 and reject (memoised) when it moved by
S2. Cold path, once per probe; the per-instruction loop is untouched.
Same shape as the DSP guard.

Test: gpu_idle_probe_falsify gains a late-load loop that must not fire
(fails on develop, passes here) and a no-overwrite control that must
still fire, so the guard keys on the fix-up, not on external loads.

Refs #811

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-07 07:07:33 -04:00
Joseph MattielloandClaude Opus 5.5 4388ad7605 test(blitter): declare retro_(un)serialize as returning bool in test_blitter_hung
UBSan's function-type check (CI sanitizer job) aborts on calling the
core's bool-returning retro_serialize through an int-returning pointer.

Refs #800

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 06:30:18 -04:00
Joseph MattielloandClaude Opus 5.5 d3bc33e001 fix(blitter): a blit hardware never finishes hangs the blitter, not the host
Phrase-mode blits below 8bpp never end on hardware.  INNER.NET builds the
inner-counter decrement (Inc0-Inc3) from the 8/16/32bpp decodes, so below
8bpp it is dstxp[0].  Phrase-mode X add truncates to a phrase boundary
(JTRM v8 p.71), so after the first write the counter never moves and
inner0 is never reached.  The synchronous accurate engine reproduced that
faithfully and never returned from retro_run (Music Demo --bios, #794;
Native Demo (bin) HLE).

Policy: hang the emulated blitter, never the host.

- BlitterNeverEnds() (blitter_mmio.c) is the single exact predicate.  The
  accurate engine, the dispatch path and the crash watchdog
  (CrashDetectBlitNeverEnds now delegates to it) all use it.
- The accurate engine runs the stuck step for exactly one destination
  wrap period, then returns.  That is 65536/ppp phrase writes (1024, 2048
  or 4096 at 1/2/4bpp), 65536 with the Y-add bit, +1 when the start X is
  not phrase-aligned.  Bounded at 65,536 writes.  It sets a stop flag only
  after the inner0/mask logic, so no write mask changes.  The collapsed
  pattern-fill and copy paths are bypassed for these blits.
  - The result is EXACT when repeating the step is idempotent on memory.
    Every observed homebrew hang has that shape: DEMO1B $2208, Chroma
    $2704, Native Demo $2718 (LFU clear, no accumulators).
  - Otherwise it is a defined one-pass approximation:
    BlitterNeverEndsApprox() flags Gouraud/Z/shade accumulators,
    comparator masks, ADDDSEL, LFU feeding back D, or LFU fed by a
    separately stepping source (Music Demo, JagMania).  The watchdog line
    now says never_ends=1 approx=1 for those.
- Dispatch then sets a sticky "blitter hung" flag, under both engines.
  - B_CMD status reads busy (JTRM v8 p.75): IDLE clear, outer-INNER and
    inner-DWRITE set, and the stuck inner count in bits 16-31.
  - Further B_CMD starts are ignored, because OUTER.NET leaves idle only
    on go (innert[0] = idle AND go).
  - The flag clears only in BlitterReset (reset / power-on).
  - Known limit: hardware still latches B_COUNT/B_CMD/pointer writes
    mid-blit (FDSYNC on countld/cmdld), so a B_COUNT write with inner 0
    would un-stick it.  JTRM p.70 calls that undefined; this model does
    not resume a hung blit.

Savestate: v16 extended in place, no bump.  A "BLH1" chunk (hung flag and
stuck count, 7 bytes) is appended strictly after #814's CDX1, with a room
check before writing.  It is read only for version >= 16.  A missing or
wrong magic, or an older state, loads as not hung.

Tests:
- test/tools/test_blitter_hung, ROM-free, accurate and fast engines.
  - The synthetic never-ending blit returns.
  - Exactly the 8 KB wrap period is written and the next phrase is
    untouched.
  - Status reads busy with count $DC, and a second start writes nothing.
  - serialize -> retro_reset clears the flag -> unserialize restores it.
- ROM-gated runs of Music Demo --bios and Native Demo (bin): 600 frames
  each, and the blitter ends hung.
- test_crash_detect_inframe gains 10 approx rows.

Closes #794
Refs #800

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 06:30:17 -04:00
Joseph MattielloandClaude Opus 5.5 c1eb2b10a0 fix(hle): park ILLEGAL for uploaded executables so homebrew stops hanging the accurate blitter
Seven homebrew carts froze retro_run under the default (Accurate) blitter.
The blitter was the victim, not the cause:

- Raw BJL binaries and .abs images carry no vector table, and many end
  their 68K main with ILLEGAL (the Alpine/BJL "drop into the debugger"
  idiom; the GPU/OP keep running the demo).  HLE installed RTE stubs only
  for cartridges, and file.c guarded only Alpine/JagServer images, so the
  trap went through a zero vector to $0, ran the vector table as code and
  walked the stack down through $0, $FFxxxx and into TOM.
- There the 68K's own exception frames land on B_COUNT/B_CMD ($F0A238
  reaches $F02238 through TOMWriteWord's $FF3FFF mask): SR becomes B_CMD's
  low word ($2208 = S|IPL2|N for DEMO1B), the pushed PC becomes B_COUNT,
  and the write starts a blit with the zeroed A1 registers -> phrase
  mode, 1bpp, X=0.
- That blit never ends on real hardware either.  INNER.NET's inner-counter
  decrement (Inc0-Inc3) only uses the 8/16/32 bpp decodes beyond bit 0, so
  below 8bpp it is dstxp[0]; phrase-mode X add truncates to a phrase
  boundary (JTRM v8 p.71), so after the first write the counter never
  moves.  BlitterMidsummer2 matches the netlist term for term, which is
  why it spun forever; the Fast engine just shrugs the garbage off.

Fix: JaguarReset parks vector 4 at a BRA.S * loop at $1000 for every
non-cartridge, non-CD image -- the guard file.c already used for Alpine
and JagServer, now re-armed on every reset so retro_reset()'s RAM clear
cannot wipe it.  Only vector 4 is parked; skipped if the image covers the
vector or the loop.  No blitter semantics change, no savestate change.

Also:
- crash_detect: inframe_hang now also fires (never_ends=1) for a blit
  whose inner counter cannot decrement (phrase mode below 8bpp).  DEMO1B's
  blit (14.4M pixels) was under the 2^24 pixel rule and logged nothing.
  The "inner 0 = one step" assumption now cites INNER.NET's zero/underflow
  detect.  CrashDetectNoteBlit takes the destination flags and X.
- cart_boot_matrix.sh: CART_MATRIX_PROBE_ARGS knob so the matrix can sweep
  the Accurate blitter.  Folded into the row-cache id and default log dir,
  and refused without an explicit CART_MATRIX_OUT, so it can never reuse
  or publish over the Fast rows.
- test/tools/test_upload_illegal_park: ROM-free synthetic raw binary that
  ends in ILLEGAL must park at $1000, also after retro_reset (the old core
  hangs on it); plus a ROM-gated DEMO1B run under the Accurate blitter.
  test_crash_detect_inframe gains 11 never_ends rows.

Before/after, Accurate blitter, 600 frames: before, all 7 carts timed out
in HLE (120 s cap) and 6 of 7 in real-BIOS mode (90 s cap; JagMania
(Jul 8) completed); after, all 7 complete in both boot modes.
Chroma-Luma (bin) and JagMania (Jul 8) no longer hang but stay broken:
their raw-binary load address is inferred wrong (a JSR lands in font data
/ mid-instruction), a separate loader bug.

Native Demo (bin) HLE, already broken (68K wild by frame 2, GPU runaway at
frame 1), now takes a different chaotic path that reaches the same
never-ending blit at frame 170, so it hangs under Accurate where it used
to end on a black screen.  That is the #794 class below.

Music Demo (ScatoLOGIC) --bios (#794) is a cartridge in real-BIOS mode,
so this guard does not apply; its hanging blit is the same never-ending
1bpp phrase-mode shape and is now named by never_ends=1.

Closes #800
Refs #794

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 06:28:41 -04:00
Joe Mattiello 5330fad16f Merge pull request #821 from libretro/docs/jtrm-errata
docs: JTRM errata reference and verified corrections to the distilled docs (#820)
2026-10-07 05:49:26 -04:00
Joseph MattielloandClaude Opus 5.5 153d1793d5 fix(m68k): model the prefetch queue for self-modifying code
The 68000 holds the two instruction words after the one it is executing.
A write to a word already in that queue does not change what runs next;
the CPU executes the stale copy.  Xenowings' anti-tamper check depends on
it: `ori.l #$04000400,(a1)` turns the next two `addq.l #1,d6` into
`addq.l #3,d6`, and the check (d6 == $12071971) only passes if the old
pair runs.  This core fetched every word straight from memory, so d6
ended at $12071975 and the game jumped back to its loader a few frames
into gameplay (#811).

Model only what the queue makes observable, at no cost on the fetch path:

- The main-RAM write paths compare the address against a 32-byte window
  around regs.pc; on a hit, M68KPrefetchSnoop() records the word as it
  was before the write.
- At the next dispatch, the recorded words the queue really held are kept
  and swapped back into RAM for exactly that one instruction, then
  restored.  A stale IRC word survives to the following dispatch only
  when the instruction was one word long and did not branch.  The slow
  path is out of line so m68k_execute's loop keeps its size.
- Which words are queued depends on the bus-cycle order.  The
  read-modify-write forms, MOVE to -(An), PEA (An) and ADDX/SUBX -(An)
  prefetch before writing (next_pc and next_pc+2 queued).  MOVE to any
  other destination, MOVEM, MOVEP, LINK and the other PEA modes write
  first (next_pc only).  The order comes from Clock Signal's
  microcode-derived 68000 (TomHarte/CLK 68000Implementation.hpp:
  Perform_np, MOVE_bw, MOVEMtoM, LINKw, MOVEPtoM), not from the generated
  handlers, whose fill_prefetch placement is wrong for MOVE.
- Exceptions, interrupts, reset and an external PC change flush the
  queue.
- Queued words go into savestates through regs.prefetch_pc and
  regs.prefetch, which were always serialized and never used.  Old
  states load as "nothing queued", and STATE_VERSION is unchanged.

test/test_m68k_prefetch is ROM-free.  It covers both bus-order classes
(MOVE.W onto next_pc is stale, onto next_pc+2 is fresh; ORI.W onto
next_pc+2 is stale), a branch away and back, a second pass through patched
code, a data read of a patched word, and a savestate taken with words
queued.  On the old core it fails 5 of 9 checks.
xenowings_ingame_video now requires 600 consecutive lit gameplay frames
and no return to the loader.  The old core gives a streak of 2 and 10
returns to the loader; this change gives a streak of 990 and none.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 05:25:24 -04:00
Joe Mattiello 85b0b90f8d Merge pull request #819 from libretro/fix/811-xenowings-video
fix(gpu): late write-back of external loads (JTRM TOM bug 13)
2026-10-07 04:32:08 -04:00
Joseph MattielloandClaude Sonnet 5.5 29998fee28 docs: JTRM errata reference and verified corrections to the distilled docs (#820)
Add docs/jtrm-errata.md: where the manual is wrong/inconsistent/hardware
differs (A), where it is silent and the answer came from netlists, Verilator,
MiSTer or games (B), Atari's own bug lists with a modeled? column (C), and the
doc mistakes we already paid for (D). Point docs/agent/hardware.md and
CLAUDE.md at it.

Correct, against JTRM Rev 8 / Rev 10 / Software Reference v2.4 / Hardware Bugs
& Warnings, the distilled docs that CLAUDE.md tells agents to trust:
- jtrm-gpu-dsp.md: 27 wrong opcode numbers (14 instructions missing), SH direction, MULT/IMULT
  signedness, PACK/UNPACK, DSP-only opcodes (SAT32S 42, MIRROR 48), DSP flags
  and ctrl layout (int 5 at bits 16/17, INT_LAT5 at bit 16), wave table
  geometry, BUS_HOG/DMAEN gotchas
- jtrm-register-map.md / jtrm-blitter.md: LFU encodings, PITCH 3, window width
  encoding, SRCZ1/SRCZ2 and the Rev 2.2 footnote, MEMCON1/MEMCON2 bit
  placement, ZMODE, GOURZ, G_HIDATA, D_FLAGS, misattributed bug list
- jtrm-clocks-timing.md: video timing and pixel-divisor tables vs v10 p.6/p.8,
  CLK1/2/3 formulas, MEMCON register table
- jtrm-jerry.md, jtrm-object-processor.md: wavetable entry count, STOPOBJ,
  unsourced VC=$FFFF claim
- comments only (no behaviour change): stale PIT clock comment in jerry.c,
  blitter status value comment, bcompen_basic.s "differs from JTRM" claim,
  test_texdump.c LFU label, and several tests that cited our own source as the
  hardware spec

Code that contradicts the manual (DSP DIV_OFFSET bit, STOP object interrupt
gating, test_texdump LFU constant) is recorded as follow-ups, not changed.

Refs #820

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-07 04:19:11 -04:00