* 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]>
* 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]>
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]>
* 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]>
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]>
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]>
* 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
* 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
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]>
* 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]>
* 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]>
* 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]>
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]>
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]>
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]>
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]>
- 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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
- 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]>
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]>
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]>
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]>
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]>
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]>