Version v3.11.0 in Makefile, .info and version_fallback.h; release notes and
WHATSNEW. Savestates stay v17 (state.h unchanged since v3.10.0).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* 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
The release grew from a fix release into the desktop app debut, debug tools and timing
work, so it ships as v3.9.0. Version bumped in all three places; release notes renamed.
Co-Authored-By: Claude Opus 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]>
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]>
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]>
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_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]>
Bump CORE_BASE_VERSION / display_version to v3.8.0 in the Makefile,
the .info file and version_fallback.h (check-info-version.sh OK).
Add docs/RELEASE_NOTES_v3.8.0.md and the v3.8.0 WHATSNEW section.
Matrices were regenerated and gated in #874 (0 backward rows vs v3.7.1).
Refs #746
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
Native Demo (bin) is a headerless raw binary linked at $5000 (its .jag
twin's JAGR header says load=run=$5000). Every one of its 15 absolute
references also fits a $4000 window and it has no JSR/JMP entry-point
hits, so InferRawBinaryLoadAddress's containment tie went to $4000 and
the image ran 4 KiB low: GPU runaway at frame 1, gpu_pc_escape,
video_stall, black video in both boot modes.
What separates the bases is that its startup LEAs descriptor tables
({dest, size, src}) whose first long is the Jaguar I/O destination of a
GPU/DSP upload ($F03000, $F1B000). Under $5000 three LEA targets start
with such a long; under $4000 none do. Exact containment ties now go to
the base with more of these hits; with no hits the historical winner
stands.
Scorer A/B over the 141 resolvable cart-matrix images: only Native Demo
(bin) changes base. After: GAME_CODE "video, silent" in HLE and BIOS,
same as the .jag version. Ruiner Pinball (the other #866 title) is a
separate GPU store-alignment issue and is not addressed here.
Refs #866
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
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]>
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]>
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]>
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]>
Club Drive was silent under HLE. DSPSetIRQLine vectored the DSP
synchronously inside the I2S event callback; the 68K, which runs its
whole slice before DSPExec, then wrote D_PC=$F1B030 while DSPGO=1 (JTRM v8
p.110: D_PC writes only while the DSP is idle) and landed on the vector
with zero ISR instructions run; the game's init wrote IMASK=1 back and no
interrupt was ever taken again (DSP spins at $F1B120, LTXD 0). Pure I2S
phase luck: 5/97 first-tick phases silent on develop, 0/97 with the fix.
DSPSetIRQLine now latches the interrupt (IMASKCleared, already
serialized) and the DSP takes it at its next instruction boundary in DSP
time; a host D_FLAGS write ORs into the latch instead of dropping it.
The handoff-state hypotheses (R31, bank seeding) were tested and refuted.
Club Drive HLE: RMS 0.0 -> 2948.6, onset frame 6. Corpus A/B vs develop:
160/160 framebuffer streams identical in both modes; cart matrix audio
changed only for Club Drive HLE (better) and Cybermorph HLE (+5 samples,
neutral). Audio presence + clipping unchanged on Wolf3D, Doom, WMCJ,
IS1/IS2, Skyhammer, Atari Karts, Raiden, T2K, Zoop. test_dsp_unit 7/9/9a
updated to the new contract; Club Drive HLE presence check added.
Refs #744
Co-authored-by: Claude Opus 5.5 <[email protected]>
- 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]>
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]>
#844 merged with three red Linux jobs (a merge-when-green script raced
CI registration):
- build-Linux x86_64 (Clang) and ASan/UBSan: the new test_jaggd_fs rule
compiled with -std=c99, under which glibc hides lstat/strdup/fileno/
ftruncate used by jaggd_fs.c and the linked libretro-common sources;
Clang rejects the implicit declarations. Build it with -std=gnu99.
macOS headers expose them regardless, which is why it passed locally.
- clang-tidy: the <save dir>/jaggd-sd join used strcpy/strcat. Use
strlcpy/strlcat with truncation checks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The GameDrive's file/dir functions were stubs returning -1, so software
that keeps its data on the SD card (JagOS 'Spot', streamed assets,
screenshots) could not run. Blob functions 9-16, 20-23 and 26 now work
against <save dir>/jaggd-sd (system dir fallback), per the new core
option virtualjaguar_jgd_sd (Read/write default, Read-only, Disabled).
- Call path: each of those blob slots is one backdoor write, $C000 |
function. The core services the call during that write: it reads the
68K's d0/d1/a0/a1 and leaves the result in d0 via m68k_set_reg. Safe
because the 68K's JERRY writes are synchronous and the stub's
move.w #imm,abs.l never touches d0; only a 68K writer triggers it.
Paths and buffers move through JaguarRead/WriteByte, bounds-checked.
- src/core/jaggd_fs.c: FatFs semantics, matching RetroHQ's published
gdbios.h (facts only, none of their files vendored): FA_* open
dispositions, write/create needs a writable card, short reads succeed,
FILINFO records big-endian (22/278 bytes). End of directory returns -1
with a zeroed record. Sandbox: .., drive letters, FAT-invalid chars
and symlinks refused; components matched case-insensitively.
- Savestate v17 (first bump of the v3.8.0 cycle): trailing "JGF1" chunk
with card-relative path, access and position per handle. Restore
reopens with the original access only (never create/truncate/append),
re-seeks a handle already open on the same file, closes deleted ones.
- Blob grows 208 -> 338 bytes (limit 510).
Tests: test_jgd's probe now drives the file API through the real blob
(size 7 and the read buffer are values only the host can produce;
sandbox escape returns -1); new host test test_jaggd_fs covers every
disposition, read-only cards, sandbox refusals, FILINFO layout,
directories, the handle limit and savestate restore.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
State-driven: wait for the 652-wide main menu, press B, wait for the
LOADING MISSION DATA screen, then require a lit 326-wide playfield
frame within 900 frames. Fails on develop (every gameplay frame black,
best 1.1% lit), passes with the bug-13 late write-back (94% lit).
Only a handful of gameplay frames render today: a 68K self-modifying
integrity check at $350C needs the 68000 instruction prefetch, which
the core does not model, and sends the game back to its loader. The
check says so and is meant to be tightened once that lands.
Refs #811
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A no-content session never exposed SAVE_RAM (no_game_active), so Memory
Track writes made through a disc inserted via disk control were never
handed to the frontend. Two causes:
1. SAVE_RAM was gated on no_game_active, a #646-era rule ("no cartridge,
no EEPROM"). #726 made no-content boot a CD session with the Memory
Track plugged in from frame 0, so the gate is stale. It now follows
jaguarMemTrackInserted. It is deliberately not keyed on a mounted
disc: frontends size and load SAVE_RAM once after retro_load_game and
libretro has no change notification, so exposing it first after an
insert would write a file that is never read back.
2. The insert path never reinstalled the NVM BIOS module. The boot resets
RAM, wiping the '_NVM' cookie; retro_load_game and retro_reset
reinstall it, disk_set_eject_state did not. Found by the new test.
Tests: test_disk_control cases 6/7/8; test_boot_config's #646 size-0
assertion becomes the new contract. mtMem joins the test export lists.
No savestate change.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Run-ahead diverged mid-load on the real-BIOS CD path (Hover Strike,
warmup 400): video replay differed 44 frames after the rollback and the
state never reconverged. Two statics sat outside the savestate blob:
- jagcd_bios.c cdBootStubInjected gates the 68K boot-stub injection at
$050176. A rollback to a pre-injection state kept the flag set by the
replay that had already injected, so the replay never injected the
stub and the game never started.
- cdrom.c fifoRefillAccum is the error-diffusion remainder that picks
the next FIFO refill interval (2 or 3 ticks); stale, it shifted every
later refill by one tick (fifoFillDelay 1 vs 2 in the state diff).
cdPrevShouldIRQ, the CD IRQ edge detector, is saved with it.
Appended as two magic-tagged chunks ("CDB1", "CDX1") strictly after the
HLE chunk; STATE_SIZE unchanged. This is the v3.7.1 savestate bump,
v15 -> v16: v15 and older states still load and leave the live values
alone (v3.7.0 behaviour; forcing "not injected" would re-arm the BIOS
hooks over a mid-game state's RAM). Verified with a genuine v15 file
written by a v3.7.0 build.
Adds test_state_compat rows (v16 chunk restore, v15 load, damaged
accumulator), a ROM-gated real-BIOS Hover Strike run-ahead check next to
the HLE one, and VJ_RUNAHEAD_KEEP=1 to keep the three state files for
offline diffing.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>