621 Commits
Author SHA1 Message Date
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
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 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 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 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 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
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 b66a6282ee test(installer): invoke Windows RetroArch directly for qualification (#1065) 2026-10-22 02:26:54 -04:00
Joe Mattiello cf036d7432 fix(installer): preserve portable data and qualify published package updates (#1059)
* fix(installer): retain portable data and qualify published packages

* docs: record published Mac and clean Linux qualification evidence

* test(packaging): add clean Intel macOS qualification lane

* fix(ci): use distro audio packages and authenticated qualification metadata

* test(packaging): qualify downloaded Windows setup upgrade and uninstall

* test(packaging): require real upgrades, core loading and immutable candidate evidence

* test(installer): run Linux core qualification with a virtual GL display

* test(installer): resolve release tags for qualification provenance
2026-10-21 19:37:32 -04:00
Joseph Mattiello f7249c4d01 fix(blitter): follow destination lanes for computed Z with DSTA2 2026-10-20 14:32:09 -04:00
Joe Mattiello 3cfe437271 fix(risc): persist and wrap the MMULT matrix address counter (#1048)
* fix(risc): persist and wrap the MMULT matrix address counter

* test(risc): keep MMULT harness under the test ABI guard
2026-10-20 13:17:32 -04:00
Joe Mattiello 8287d41676 fix(tom): expose live PIT counters and hardware register readback (#1047)
* fix(tom): expose live PIT counters and hardware register readback

* test(tom): refresh Jagniccc baseline for live PIT readback

* test(tom): keep readback harness under the test ABI guard
2026-10-20 04:14:00 -04:00
Joe Mattiello 73b0dcf07a test(blitter): cover greater-than Z comparisons and add a bootable ROM (#1049) 2026-10-20 03:43:26 -04:00
Joe MattielloandClaude Opus 5.5 7ed64fa940 fix(loader): enter an erased-header cart at $802000, not $800000 (#979)
Phase Zero (2000) (PD) differs from the signed, verified [a1] dump in
660 bytes: the encrypted boot block and the $400-$40B universal header
are erased to $FF.  In HLE, cart+$404 = $FFFFFFFF is not a valid
vector, so the loader fell back to $800000 -- $FFFF, a line-F trap --
and the 68K ping-ponged between it and the HLE RTE stub from frame 1
(black screen for the whole run).  The program is at $802000, the
address [a1]'s vector names.

An erased vector ($FFFFFFFF) with a 68K entry at $802000 now enters
there.  Any other invalid vector keeps the $800000 fallback (raw code
at the cart base, the case the existing comment describes).

HLE frames 300 and 600 are pixel-identical to the [a1] HLE reference.
Loader census, 699-file corpus: Phase Zero (PD) (3 copies) moves, and
so does the Developer CD BIOS image when loaded directly as content
(also an erased $404, entry $802000 = MOVEA.L #,A7): it now enters at
$802000 like the retail CD BIOS row.  The CD boot path stages BIOS
images through external_cd_bios, not this loader, so CD boots are
unaffected.  Real-BIOS mode is unchanged (it ignores jaguarRunAddress);
the BIOS reject there is authentic (no boot block).

Refs #974

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-15 20:20:07 -04:00
Joe MattielloandClaude Opus 5.5 30366d311a fix(loader): place BJL RAM images and $802000-based images where they were linked (#978)
* fix(loader): read GEMDOS/DRI 601A executables and JAGR type 2 with their own layouts

Mandelbrot Demo (PD) is a plain GEMDOS/DRI executable: 601A, text $EBE,
symbols $2CD8, a zero relocation long, and no "JAG" at $1C.  It was
parsed as JST_WTFOMGBBQ, whose load address is the little-endian long at
$1C -- here the first instruction, $46FC2700, read as $0027FC46 -- so
the program was copied outside main RAM and the 68K parked in the
ILLEGAL guard at $1000 from frame 1.  A 601A file whose sizes add up to
the file is now JST_DRI: text+data go to the base the raw-binary scorer
infers, else the BJL upload address $4000, and a GEMDOS relocation
table, if present, is applied against that base.  Files that are not
self-consistent keep the old reading.

Jaguar Server INTRO / INTROMOD are JAGR type 2 ($0002 at $20): load
$22, length $26, code at $2A, entered at the load address.  The loader
assumed type 3 (run $2A, code $2E), so it took the first code long
$23FC03D0 as the run address and dropped four code bytes.

Loader census over the 699-file private corpus: only these rows move
(Mandelbrot x3 copies -> $4000; INTRO/INTROMOD x3 copies -> $4FF6).
HLE frames: the Mandelbrot set and the Jaguar/cube/ATARI intro render,
matching the triage's hand-wrapped loads.

Refs #971

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

* fix(loader): place BJL RAM images and $802000-based images where they were linked

Issue #972 (raw-binary round 2):
- Hubble Fade, Hubble Nebula and Drumpad2 are headerless BJL programs
  with one absolute reference or none in their first 2 KiB, so the raw
  scorer (bar of 8) could not place them and they fell through to the
  sub-1 MiB cartridge fallback: HLE ran from cart+$404 / $800000 and the
  boot ROM rejected them in BIOS mode.  A sub-1 MiB image with no $400
  marker that opens with a 68K instruction and has no encrypted boot
  block (byte 0 $F6-$FE) now loads as a RAM program at $4000.
- Painter (bin) is the payload of the JAGR Painter, linked at $10000.
  $10000 is a new, last candidate.  Containment gave $4000 a 14-13 edge
  through one LEA $4000,A0 (a RAM buffer below the image); a JMP abs.L
  that lands on an abs.L instruction the scorer decoded now counts as
  entry evidence (its JMP $10012 hits the LEA $1FFFFC,A7 at offset $12
  under $10000 only).  JSRs onto decoded instructions are not counted:
  over the corpus that matched only by coincidence.

Issue #973 (offset 0 = $802000):
- BadCode4 (Metal)/(Modified) "(rom)" open with a stub that reads its
  own header through cart addresses ($802054/$802048/$80204C) and copies
  the (bin) build to $4000.  Two such abs.L loads in the first $40 bytes
  -> JST_ALPINE, whatever the size.
- Rayman Demo is exactly 2 MiB with no $400 universal header; its
  startup JSRs to $802xxx.  An exact-MiB image without the marker whose
  raw-scorer verdict is $802000 -> JST_ALPINE.  Only Rayman Demo and
  Phase Zero (PD) lack the marker among exact-MiB files in the corpus,
  and Phase Zero is all $FF up front (no verdict), so it stays a cart.

Loader census, 699-file private corpus: the only rows that move are
those titles (all copies).  HLE frames match the triage's hand-fixed
loads (Hubble photographs, Painter title menu, Rayman gameplay, Bad Ball
2001 title); Drumpad2 goes from 0 to 154,968 non-silent samples after one
button press (black screen is authentic, like Drumpad).  These titles no
longer run the boot ROM in BIOS mode (RAM programs / Alpine images).

BadCode4 (Metal) (bin) [a1] stays unresolved by design: it is the (bin)
build with its first $1A0 bytes (big-endian mode, stack, video timing
init) cut and a different 176-byte prologue; its JSR targets line up
with subroutine starts only at base $41A0, and loaded there it waits
forever for a VBL counter.  An incomplete dump, not a loader bug.

Refs #972, #973

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-15 16:18:57 -04:00
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 4e0bd1cfb4 fix(risc): DIV uses the netlist's 33-bit working value (#895)
jag_sim DIVIDE.NET (TOM and JERRY identical): an unsigned non-restoring
divider whose working value is the 32-bit dividend_hi register plus the
last_neg sign flop -- both adders carry a 33rd bit "to correctly
determine the sign, as the arguments are 32-bit unsigned values".  The
core's loop kept a 32-bit working value and took the sign from bit 31,
so every divisor above $80000000 gave garbage ($FCC00000/$FCC00000 =
$F9A9342A instead of 1, found in Xenowings' joypad reader), as did
16.16 divides whose quotient overflows.  For divisors up to $80000000
with an in-range quotient the result is bit-identical to before
(checked over 20M random cases).

- src/tom/risc_div.h: risc_divide(), the netlist algorithm, header-only
  like dsp_acc40.h.  Used by gpu_opcode_div, dsp_opcode_div and the
  dead pipelined DSP_div (DSPExecP is never defined), so no two paths
  can disagree.
- The DSP's divide-by-zero guard goes: the hardware has no special case,
  every step subtracts 0, giving $FFFFFFFF (unchanged) with the dividend
  in D_REMAIN (was 0).
- Tests: test_dsp_unit DIV cases incl. remainders (an even quotient ends
  on a negative step: remainder - divisor, as DIVIDE.NET documents);
  test_gpu_ops $FCC00000/$FCC00000.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 19:43:14 -04:00
Joseph MattielloandClaude Opus 5.5 0fc2c84c44 fix(dsp): an indexed store whose field 1 names the divide target waits for the quotient (#895)
Xenowings' sound effects were cut off after one or two samples.  Its DSP
voice setup computes each effect's playback step with

    div   r12,r7           ; $F1BAD2
    store r7,(r14+7)       ; $F1BAD4, C4E7: field 1 = 7

and the bug-2 model from #825 made every indexed store right behind a
divide write the OLD value -- here the undivided $01588800 instead of the
step $5F1 -- so each voice ran off the end of its sample at once.  The
joypad reader (`div r9,r8 / store r8,(r14+1)`, C428) does need the old
value (#811).

The netlists separate the two (jag_sim netlists/jerry, identical in tom/):
on an indexed op's first tick the destination port carries the implicit
base r14/r15 (DSP_EXEC.NET Impdai/Dstanwi) and the source port the raw
field-1 bits (Srcanwi := JOIN (srcopin, srctopi)); SBOARD.NET:127-130
compares both against pending writes with the source compare enabled by
`srcrrd + precomp`, so for the immediate forms the 5-bit OFFSET is
compared as a register number.  A match holds the store until the divide
writes back (SBOARD.NET:380-384 Divdwait).  The data register itself is
read one tick later on the compdld wait state (EXECON.NET:173-189), which
nothing scoreboards -- that is bug 2.  So the store sees the old value
unless its base register or its field 1 (index register or immediate
offset) names the divide's destination.  Offset 7 = r7 at the effect
site: interlock, quotient.  Offset 1 at the joypad site: old value.

- DSPDivStorePeek (both live DSP paths): exempt a store whose field 1
  equals its data register, for all four opcodes (previously only the
  register-index forms 60/61).
- Timing model (default off): new dsp_pipe_flags bit 4 for 43/44/49/50,
  field 1 waits on pending results / the ALU interlock like a read, but
  takes no register-file read port.
- test_dsp_unit: `div ; store r2,(r14+2)` waits 16 and stores the
  quotient with the model on and off.  Fails before this change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 19:41:53 -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 MattielloandClaude Sonnet 5.5 21ed4aa02a fix(debug): OP snapshot captures the whole display list; texreplace reload test
The snapshot now reads the entire list at the top of the first active halfline of
the next field (links followed, both arms of every branch, each object once,
cap 1024) instead of recording the OP's own first non-empty walk, which on yarc
was only a branch and a stop.  It also removes the per-object test from the OP
loop: the hot path pays one global test per OPProcessList call.

test_debug_iface asserts the yarc snapshot holds a bitmap and a stop, and that
texreplace_reload finds a pack installed after the game loaded (base path and
option survive the reload), sees it removed, and stays off with the option off.

Refs #652 Refs #833

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-13 20:54:07 -04:00
Joseph Mattiello f634be7a50 Merge release/v3.8.1 into feat/debug-wp1-iface 2026-10-13 20:34:50 -04:00
Joseph MattielloandClaude Sonnet 5.5 e873883e33 feat(debug): core side of the app debug interface (vjdebug.c) with socket-less halt pump
src/debug/vjdebug.c implements struct vj_debug_iface: side-effect-free peek
(JaguarReadByte DEBUGGER), policy-checked poke, both RISC register banks, RISC
disassembly, attach/halt-pump/breakpoints on the existing GDB halt machinery
(a host can attach without a socket; GDBHalt pumps it and never blocks when
nothing could resume it), a one-walk OP display-list snapshot taken before the
OP rewrites HEIGHT/DATA, a blitter launch log ring, TexReplaceReload, and the
frame counter.

The interface is handed over once via VJ_ENV_SET_DEBUG_IFACE, only to a
frontend that set VJ_CAP_DEBUG (RetroArch never does).  Detached cost: one
global test per OPProcessList call and one per blitter launch.  Statics reset
on unload and deinit.

test/test_debug_iface.c drives it on yarc.j64: who gets the interface, peek ==
jaguarMainRAM, the poke policy, halt/step/continue/detach through a scripted
host, breakpoint re-hit, OP snapshot, blit log, and an identical frame + RAM
hash with everything switched on.

Refs #652 Refs #833

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-13 20:23:06 -04:00
Joe Mattiello ec82de6b12 Merge pull request #908 from libretro/test/v3.8.1-timing
Timing campaign S0/S1/S2 into v3.8.1 (S1 experimental, default off)
2026-10-13 20:03:58 -04:00
Joseph MattielloandClaude Sonnet 5.5 d5adfdee61 feat(core): frontend BIOS queries, BIOS_STAGED events and a cart-ROM no-content boot
Adds VJ_ENV_GET_BIOS_PATH / VJ_ENV_GET_BOOT_TARGET behind a new VJ_CAP_BIOS
capability on the private frontend channel (ABI unchanged), so the standalone
app can name the exact CD BIOS / cart boot ROM file to run and choose which
BIOS a no-content boot enters (CD BIOS, or the cart boot ROM against an empty
slot).  The core still validates every file and falls back to its own search
and the built-in images; RetroArch never answers HELLO, so nothing changes
there.  VJ_EVT_BIOS_STAGED / VJ_EVT_BIOS_REJECTED say which image was staged.

Refs #838 Refs #833

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-13 18:37:37 -04:00
Joseph Mattiello 5063515947 Merge remote-tracking branch 'libretro/feat/timing-s1-dsp-scoreboard' into test/v3.8.1-timing
# Conflicts:
#	libretro_core_options.h
2026-10-13 18:06:53 -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 Sonnet 5.5 5b0d08e695 test(core): frontend event channel opt-in contract and clock-scale parsing
test_frontend_ext: a refusing frontend gets one HELLO and zero events, an
answering one gets LINK_UP, a caps-less answer is silent, the latch resets
between sessions; plus the clock-scale value table (listed values unchanged,
percent form clamped, junk stays stock). Harness gains an env hook; the test ABI
exports the two clock-scale globals.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-13 16:43:05 -04:00
Joe Mattiello 8b2c94f69f Merge pull request #891 from libretro/fix/idle-skip-under-overclock
perf(risc): run the idle-loop fast-forward under any RISC clock scale
2026-10-13 13:53:50 -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 Opus 5.5 62c73e1e4d fix(dsp): keep the DSP out of the VJ_DRAM_SCALE stress multiplier
Maintainer decision on #892.  VJ_DRAM_SCALE (busArbiter.contention_scale)
is the #406 liveness-sweep knob for the GPU/68K/OP/refresh charges; S1
let it multiply the DSP's new bus cost too, and at scales >= 10 that
starved Doom's MiniLoop on the dspfinished handshake ($F1B034 polled
~1,600-1,700 times per field) so dram_scale_sweep.sh fell below its
liveness floor.  The DSP now always pays its real 1x bus cost; the knob
scales exactly what it scaled before S1.  No effect at the default
scale of 1.

Refs #313

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 02:27:03 -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 Sonnet 5.5 46b10a4599 build(app): macOS ships the zip only; drop the DMG from packaging and the installer
The zip holds the notarized, stapled Virtual Jaguar.app as its only top-level
entry; CI unpacks it and re-verifies signature and staple. install.sh loses its
DMG path (--app-format dmg is now rejected) and its tests follow.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-13 00:27:58 -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 Opus 5.5 3ed984458d test(dsp): let the audio pair and acid suite run with the pipeline model on
test_audio_clipping gains the same --option K=V passthrough
test_audio_presence already has, so both halves of the audio pair can
be run with virtualjaguar_gpu_pipeline_timing enabled.  The acid
runner gains ACID_PIPELINE_TIMING, the same opt-in shape as
ACID_DRAM_TIMING (default disabled, so ordinary runs are unchanged).

Refs #313

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 23:44:39 -04:00
Joseph MattielloandClaude Sonnet 5.5 a20867bd2d fix(tools): installer review fixes and #839 package layout
- install.ps1: the interactive channel choice is a local assignment ($script:
  missed the parameter under & ([scriptblock]::Create(...))); silence progress
  for 5.1; portable zip is one top folder, its CONTENTS go to the destination;
  setup default args are /VERYSILENT /NORESTART /SUPPRESSMSGBOXES and the Inno
  AppId uninstall key is checked first.
- install.sh: AppImage menu entry and icon use io.github.libretro.VirtualJaguar
  (hicolor tree extracted from the AppImage); FUSE probe checks /sbin/ldconfig
  and multiarch dirs without globs (zsh); a nightly receipt beats the project
  version Info.plist/dpkg report, so a release is still offered over a nightly.
- tests and CI: portable file:// URLs, narrower dry-run snapshots, 5.1-safe
  child-process error handling, nightly-vs-release checks, documented
  PSScriptAnalyzer exclusions.

Refs #840

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-12 23:27:41 -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
Joseph MattielloandClaude Opus 5.5 05b83a4f4e feat(dsp): pipeline scoreboard + external-bus timing model (#313)
Mirror the GPU pipeline/gateway model (virtualjaguar_gpu_pipeline_timing)
onto the DSP, under the same option, and give the DSP the external-access
charge hook it never had (bus_arbiter.h's documented asymmetry, campaign
hypothesis H-C).

The DSP is "a variant of the GPU" with the same pipe-lining (JTRM v8
p.97), and the netlists agree at gate level: jag_sim jerry/SBOARD.NET and
EXECON.NET are byte-identical to the tom/ files, DSP_MEM.NET's local
arbitration matches GPU_MEM.NET, and DSP_GATE.NET is the GPU gateway
minus the phrase path.  The GPU's netlist/Verilator-pinned rules (R2 RAW,
R3 flags, R5 local load, R9 external load L=7+D, R15 divide, R16 MOVEI,
R6/R7 indexed, R12/R13 jump refill, the section-14 write-back port
conflict) therefore carry over by netlist identity.  DSP differences:

- local space is $F18000-$F1FFFF (DSP_MEM.NET), so GPU RAM is external;
- external memory sits behind JERRY's 16-bit interface (JTRM v8 p.97,
  pin note p.124): a long transfer is two word cycles (JMEM.NET), priced
  by the new bus_arbiter_dsp_access() as word + 1-clock pause + page-hit
  word for DRAM, two full cycles for ROM/I/O;
- indexed-store data is not score-boarded (TOM/JERRY bug 2, v8 p.134),
  consistent with the existing dsp_div_store_fixups data effect.

Inferred, not simulated (no JERRY Verilator rig): the JERRY<->TOM bus
handshake is costed as the GPU's idle-bus grant (2), and the split
pause as 1 clock.

With the model on, every DSP opcode's base cost is 1 (R1) instead of the
legacy dsp_opcode_cycles[] table, whose load/div/mmult entries are the
hand-tuned stand-ins the rules now replace.  Bus waits are wall time and
are rescaled under risc_clock_scale (bus_arbiter.h contract); interlocks
are core time.  With dram_timing on and the model off, DSP external
accesses pay the same immediate self-cost the GPU does.  Both options
off: the DSP path is unchanged.

Model state is transient (one transfer + one divide), zeroed on reset,
interrupt dispatch and option toggle -- the GPU model's convention.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 23:26:49 -04:00
Joseph MattielloandClaude Sonnet 5.5 a4d8635beb feat(tools): install.sh/install.ps1 install and update the core, the app, or both
install.sh gains --what core|app|both (and --core/--app/--both), --app-dest,
--app-format, --force, --allow-unsigned and the VJ_INSTALL_BASE_URL /
VJ_INSTALL_RETROARCH / VJ_INSTALL_NO_TTY overrides.  The app is the macOS zip
or DMG (/Applications, else ~/Applications), the Linux AppImage
(~/.local/bin + .desktop) or the .deb via apt/dpkg, picked from the userland,
not the kernel.  Updates are detected from the machine (Info.plist, dpkg, the
AppImage path) with an install receipt as a hash cache for nightlies.  Every
download, core included, is checked against SHA256SUMS.txt; a channel with no
app build says so instead of erroring.  Unattended runs stay core-only unless
this script installed the app.  Gatekeeper protections are never stripped
silently.  Also: --auto-update off no longer reinstalls first, and --help works
under zsh.

install.ps1 is the Windows twin (5.1 and 7, irm | iex friendly): core into the
cores folder discovered from retroarch.cfg / registry with the word size read
from the retroarch.exe PE header, app as the portable zip (+ Start-menu
shortcut) or the silent setup.exe, -DryRun, -Force, SHA-256 verification.

test/installer/ holds hermetic suites for both over a fake file:// release
tree; .github/workflows/installer.yml runs them (plus shellcheck and
PSScriptAnalyzer) on Linux, macOS and Windows PowerShell 5.1 / 7.

Refs #840

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-12 23:11:30 -04:00
Joseph MattielloandClaude Opus 5.5 d8605f6855 revert: BIOS-mode power-on RAM back to the zero fill; rebaseline 3 JaguarDemos
Reverts 50da840.  The $FF fill passed JaguarDemos but the release cart
matrix (Studio, 155 titles) then moved 8 BIOS rows backward against
v3.7.1, and 11 against the zero fill: BadCode4 x3, Jaguar Server
INTRO/INTROMOD/KEYB/50 Hz/60 Hz, Mandelbrot, Gorf 2000 and Painter gain
gpu_pc_escape/dsp_pc_escape/gpu_wedge/inframe_hang.  They expect zeroed
BSS.  The CD matrix was unchanged.  No uniform fill fits every title;
zero has 0 backward rows, so v3.8.0 ships it (maintainer's call).

JaguarDemos BASELINE: JaGol, algorip and solidWaves PASS -> FAIL.
JaGol seeds its Game of Life from power-on RAM (almost empty field on
zero RAM).  algorip renders identically under every fill but the probe's
4x4 grid misses it; solidWaves draws nothing under any fill.  All three
old PASSes came from random RAM shown before the intro drew.  The README
records why; #876 tracks the RAM model, #877 the probe sampling.

Refs #856, #875

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 20:48:56 -04:00
Joseph MattielloandClaude Opus 5.5 d3795e6fdc fix(jaggd): zero GD_DirRead's record on a bad handle, refuse SD paths with no card root
Two findings from the release PR review (#875):

- GD_DirRead copied its 278-byte stack record into emulated RAM on every
  call, but JGDFSDirRead fills it only on success or end of directory.  A
  stale or bogus handle leaked host stack bytes to the ROM (which can then
  write them to the SD folder).  The record is now zeroed first.
- jgdfs_host_path() joined "" + "/" + rel when no card root was set, so a
  savestate's handles could reopen an absolute host path on restore
  (JGDFSStateLoad does not go through jgdfs_root_ready).  It now refuses to
  build any path while the card is off or has no root.

test_jaggd_fs: a state whose saved path reaches the card through the host
root must not reopen with the root cleared (fails before, passes after).

Refs #783

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 20:15:14 -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