* 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]>
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]>
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]>
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]>
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]>
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]>
A Doom link tic is 12 one-way single-byte hops (6 pairs, ping-pong). Two
parts of the old behaviour made the tic rate depend on the vsync phase
between the two devices, collapsing to 5 tics/s at bad phases even on a
fast link: JLinkAwaitReply only engaged after we transmitted, so a side that
only ANSWERS spun on ASISTAT with no wait, finished its frame in ~2 ms and
sat in the frontend while the partner's byte arrived; every such hop then
cost a whole video frame.
uart.c now counts consecutive ASISTAT reads with nothing to send or receive
and, past 24 in a frame, calls JLinkAwaitIdle(), which blocks like the reply
wait (same frame budget and timeout) so the byte is seen the moment it
arrives. A silent partner backs off exponentially (1..32 frames without idle
waits); any received byte resets it. Real transports on the JagLink device
only (not loopback, not the voice modem).
Simulator, Doom 2P, tics/s averaged over 4 vsync phases, before -> after
(emu 2 ms, 5 us poll_receive):
2 ms one-way 27.5 -> 27.5 2.5 ms 19.8 -> 26.0 3 ms 16.3 -> 20.8
4 ms 7.5 -> 19.2 Wi-Fi A (2 ms+3 jitter+2% spikes) 8.9 -> 16.0
Wi-Fi B (3 ms+8 jitter+3% spikes) 5.4 -> 9.0 3 ms with emu 6 ms 10 -> 15
Also: a once-per-5-s INFO log line while a link carries traffic (hops/s, mean
hop time, wait per hop, slipped-hop share, timeouts, budget cuts, emulation
cost, idle-wait hits) so the numbers can be read from the RetroArch log on
iOS; netlink_sim gains --btrace/--log/--loss-pct/--rto-ms/--udp-rto-ms/
--corrupt and the clock patch gains a byte-event hook; netlink_btrace_report.py
breaks hop time down by cause and analyses the Doom tic structure.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
check_variables() re-runs whenever ANY core option changes, and it
re-applied 'BIOS (Cartridges)' and 'PAL' every time. retro_load_game()
resolves vjs.useJaguarBIOS after its option read (a GPU-only/jagcrypt cart
forces the boot ROM on; CD boot mode sets it from bootConfig.showBootROM),
and the DSP reads that flag at runtime. So on such a cart, touching an
unrelated option silently turned the boot-ROM flag back off mid-session.
PAL rewrote vjs.hardwareTypeNTSC live, so the clock macros switched
standards while the AV timing (negotiated only at load) did not.
Both are now applied only before content is loaded, the same way
internal_resolution is. After load a change only logs a one-shot "takes
effect on restart" notice. 'BIOS (Cartridges)' gains the "Restart
required" sublabel marker, and moves from the live list to the load-only
list in test_option_explain case 8; settings guide section 7 lists it.
Test: test_cart_needs_bios gains a mid-session arm. It loads a GPU-only cart
with bios=HLE and pal=disabled, changes an unrelated option, then PAL
itself, and checks that useJaguarBIOS stays 1 and the standard stays NTSC.
develop fails 3 of its 4 checks (useJaguarBIOS=0 after the unrelated
change, NTSC flipped); with the fix all pass.
Closes#858
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
Doom (2P) over RetroArch netplay stuttered, and toggling Network Link
Latency Hiding changed nothing. Measured in a deterministic two-instance
netpacket simulation (test/tools/netlink_sim.c: virtual clock, RetroArch
netpacket semantics read from netplay_frontend.c): Doom's tic exchange is a
chain of single-byte hops, each side's byte depending on the other's, so the
link only advances while the core is blocked in JLinkAwaitReply. The
per-frame wait budget was 2*EWMA+2 ms clamped to [4,15] ms and ignored what
the rest of the frame costs: with a few ms of emulation the frame finished
past its vsync, the frontend skipped the next one, and the core ran at 30 Hz
(audio at half rate) with ~13 ms of dead time per frame. Every frame overran
at 3 ms/way (30 overruns/s); a slower device overran by more.
The budget is now what is left of ONE frame after the measured emulation cost
(EWMA of frame time minus time spent waiting, taken in retro_run just before
the frontend calls: audio_batch_cb blocks on a full audio buffer and video_cb
on vsync, neither is emulation) and a 3 ms margin for those calls. The margin
widens by 1 ms (max 8) whenever a frame that waited is still followed by a
late frame start, and decays after 60 clean frames, so an unmodelled frontend
cost (hi-res present on iOS) corrects itself. The per-reply timeout stays
adaptive, so a silent peer still costs little.
Sim, Doom 2P deathmatch, wait enabled, old -> new image updates/s
(emu = virtual emulation ms per frame):
3 ms/way, emu 2: 14.5 -> 22.2, overruns 30/s -> 0
3 ms/way, emu 6: 11.1 -> 22.2, overruns 30/s -> 0
3 ms/way, emu 10: 11.2 -> 9.6, overruns 30/s -> 0 (waits only ~4 ms)
Wi-Fi jitter+spikes: 8.6/8.2/8.1 -> 9.6/10.7/9.8 over 3 seeds, overruns
28/s -> 0
4 ms of unmodelled post-hook cost: overruns 30/s -> 0.5/s via the margin
Wait disabled is ~9 image updates/s on every link, so on a jittery link the
old wait was no better than off: that is why the toggle appeared to do
nothing. Network spikes themselves stay (a byte-synchronous protocol cannot
hide them).
Also: netlink_delay_proxy gains --jitter-ms/--spike-pct/--spike-ms/--seed
(Wi-Fi model), netlink_game gains --trace, and test/tools/netlink_sim.c +
netlink_sim_clockpatch.py record the simulator (measurement-only clock hook,
never applied to src/). No regression since 693f42a (#250): identical results
in the sim, and nothing in the arm/pump/flush path changed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
The cart boot matrix scored real-BIOS runs from frame 0, so the identical
~490-frame boot logo animation and jingle counted as every title's "video"
and "audio", and carts the boot ROM rejects (red "Jaguar" screen) read as
"BIOS works".
cart_boot_probe now finds the boot-ROM -> cart handoff itself (68K or any of
its last 1024 instructions, via pcQueue, in $800000-$DFFFFF) and restarts
scored_* counters there; --post-handoff N runs N frames past it (--frames is
the cap) and stops early once the boot ROM sits on its BRA.S * halt loop.
Headerless RAM-loaded .jag files never run the boot ROM (bios_ran=0): whole
run scored, never a reject.
The classifier moves to test/tools/cart_classify.sh (so a unit test can feed
it synthetic logs) and gains BIOS_REJECT (boot ROM ran, no handoff, parked on
BRA.S *) and ? (bios_trap) (cart got control, then PC fell back into the
boot ROM: a crash, not a reject). Crash-watchdog signatures stay whole-run:
an early escape is a real bug (#853), not excused.
matrix_diff.py ranks BIOS_REJECT equal to GAME_CODE: GAME_CODE -> BIOS_REJECT
is a lateral scoring correction, and HLE GAME_CODE / BIOS BIOS_REJECT is not
an HLE-vs-BIOS asymmetry. BIOS runs get FRAMES+700 frames and a 150s default
timeout. docs/cart-boot-matrix.md prose updated; table not regenerated.
Closes#852
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- 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]>
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]>
Xenowings (#811) ignored all in-game input. Its DSP joypad reader ends
with
div r9,r8 ; r8 = active-low joypad bits, inverted
store r8,(r14+1) ; -> $F1C3A8, the game's pad variable
JTRM Rev 8 p.134, TOM/JERRY bug 2 "Scoreboard Failure on Indexed
Addressing Mode Stores" ("applies to both Tom & Jerry"): the data of
store instructions 49, 50, 60 and 61 is not scoreboarded, so when it is
the result of a divide still in flight the store writes the register's
old contents. On silicon $F1C3A8 gets the joypad bits. This core
wrote the quotient instead, so the game read a value that did not track
the pad and the ship never moved: 600 gameplay frames with held input
hashed the same as a run with no input.
Model it for the documented pattern only: a divide immediately followed
by an indexed store whose data register is the divide's destination
(and is not one of its address registers) stores the pre-divide value.
The armed state lives in DSPExec locals and the loop runs that one
instruction past its budget, as the bug-13 load fix-up does, so nothing
outlives a slice and there is no savestate change. The idle-loop
fast-forward rejects a loop in which it fired. Test-ABI counter
dsp_div_store_fixups counts applications.
Not modeled: a store later within the divide latency, the external-load
variant of bug 2, a divide in a branch delay slot, and the GPU side.
test/tools/xenowings_ingame_video now also runs a forked no-input
reference arm and holds RIGHT from the first lit gameplay frame; the
frame-hash streams must diverge within 600 frames. Before: 0 frames
differ (FAIL). After: 835 differ, first at +366 (PASS); the ship moves,
fires and scores.
Depends on the DSP bug-13 fix (#822) and the 68000 prefetch model (#823).
Refs #811
Co-Authored-By: Claude Opus 5.5 <[email protected]>
GPUIdleLoopProbe proves a loop idle by walking its body in program order,
so for `load (rX),rY / moveq #n,rY` it concludes the overwrite wins.
Since #819 the interpreter lets an external load's data land last (JTRM
Rev 8 p.136, TOM bug 13), so for such a loop the dataflow proof describes
a different loop than the one that runs (e.g. a later load whose address
comes from the overwritten register can really hit register space).
Snapshot gpu_load_wb_fixups at S0 and reject (memoised) when it moved by
S2. Cold path, once per probe; the per-instruction loop is untouched.
Same shape as the DSP guard.
Test: gpu_idle_probe_falsify gains a late-load loop that must not fire
(fails on develop, passes here) and a no-overwrite control that must
still fire, so the guard keys on the fix-up, not on external loads.
Refs #811
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
UBSan's function-type check (CI sanitizer job) aborts on calling the
core's bool-returning retro_serialize through an int-returning pointer.
Refs #800
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
Add docs/jtrm-errata.md: where the manual is wrong/inconsistent/hardware
differs (A), where it is silent and the answer came from netlists, Verilator,
MiSTer or games (B), Atari's own bug lists with a modeled? column (C), and the
doc mistakes we already paid for (D). Point docs/agent/hardware.md and
CLAUDE.md at it.
Correct, against JTRM Rev 8 / Rev 10 / Software Reference v2.4 / Hardware Bugs
& Warnings, the distilled docs that CLAUDE.md tells agents to trust:
- jtrm-gpu-dsp.md: 27 wrong opcode numbers (14 instructions missing), SH direction, MULT/IMULT
signedness, PACK/UNPACK, DSP-only opcodes (SAT32S 42, MIRROR 48), DSP flags
and ctrl layout (int 5 at bits 16/17, INT_LAT5 at bit 16), wave table
geometry, BUS_HOG/DMAEN gotchas
- jtrm-register-map.md / jtrm-blitter.md: LFU encodings, PITCH 3, window width
encoding, SRCZ1/SRCZ2 and the Rev 2.2 footnote, MEMCON1/MEMCON2 bit
placement, ZMODE, GOURZ, G_HIDATA, D_FLAGS, misattributed bug list
- jtrm-clocks-timing.md: video timing and pixel-divisor tables vs v10 p.6/p.8,
CLK1/2/3 formulas, MEMCON register table
- jtrm-jerry.md, jtrm-object-processor.md: wavetable entry count, STOPOBJ,
unsourced VC=$FFFF claim
- comments only (no behaviour change): stale PIT clock comment in jerry.c,
blitter status value comment, bcompen_basic.s "differs from JTRM" claim,
test_texdump.c LFU label, and several tests that cited our own source as the
hardware spec
Code that contradicts the manual (DSP DIV_OFFSET bit, STOP object interrupt
gating, test_texdump LFU constant) is recorded as follow-ups, not changed.
Refs #820
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>