Skip to content

denise: sprite DMA fetches land in a hardware-true display-latch view#174

Merged
LinuxJedi merged 2 commits into
mainfrom
fix/sprite-dma-denise-latch-writethrough
Jul 13, 2026
Merged

denise: sprite DMA fetches land in a hardware-true display-latch view#174
LinuxJedi merged 2 commits into
mainfrom
fix/sprite-dma-denise-latch-writethrough

Conversation

@LinuxJedi

Copy link
Copy Markdown
Owner

Problem

Hamazing's kaleidoscope scene (~132s) rendered full-height vertical bars over the right 40% of the display. Bisect landed on the armed-latch redisplay change (66476e3), but that only exposed an older modelling gap: sprite DMA fetches never landed in the live Denise sprite registers - they only poked the Agnus-side FSM.

The demo arms 8 sprites manually in an early scene (bar patterns in DATB), runs a DMA-driven transition whose null-terminator CTL fetches disarm every channel on real hardware, then re-arms with the SPRxDATA=$0000 idiom and turns SPREN off for the kaleidoscope. Real Denise: armed with A=B=0, invisible. Copperline: the latches still held the manual bar patterns from thousands of frames earlier, and the armed-latch redisplay painted them.

Fix

Denise keeps two views of the sprite registers:

  • spr* stays the CPU/Copper write shadow; the calibrated manual-sprite replay and live collision paths read it and are bit-identical to before. (Seeding the replay from hardware-true registers instead regressed oldsprtimdat, which a real-A500 photo in the vAmigaTS tree pins.)
  • New spr_hw_* fields are the registers as the hardware holds them: last writer wins across manual writes and DMA fetches (a DATA fetch arms, the vstop control fetch - including the 0/0 terminator - disarms). The DMA-idle latched redisplay seeds from this view.

Write-through only fires in the authoritative sprite-DMA pass for a line: pre-display lines are computed twice, and the pre-display replay at the display start owns them (the provisional live pass can fetch bogus data lines when SPREN is enabled near the frame wrap, before the vertical-blank reset's control fetch nulls stale comparators - that fetch-order divergence itself is left as a known gap).

STATE_VERSION 27 -> 28 (Denise and RenderRegisterSnapshot gained the hardware-view fields).

Regression probe

New timing-test/sprprobe-latch golden renders the scene-switch sequence as a static display: exactly two bars must survive (a full manual re-arm and a DATA-only re-arm showing the persisted DATB latch); the arm-with-zero channel and the untouched channel must stay invisible. Cross-checked pixel-for-pixel against vAmiga, verified static (40s == 44s shot).

Verification

  • Hamazing from boot: clean at 132.6s, byte-identical to the pre-fix build at 10 other timestamps covering every scene of the demo (only the corrupted region changed).
  • vAmigaTS Denise/Sprites family: 214/214 renders byte-identical to the pre-fix build.
  • Full suite green: 1379 unit tests (including a new sprite_dma_fetches_land_in_hardware_latch_view bus test), all 19 golden probes, savestate round-trip determinism. cargo clippy and cargo fmt --check clean.
  • docs/internals/video.md and timing-test/README.md updated.

Sprite DMA fetches write the same SPRxPOS/CTL/DATA/DATB registers a
CPU/Copper write hits, but the emulated fetches only poked the Agnus-side
FSM: the Denise display latches never saw them, so denise.spr_armed and
the data latches stayed whatever the last manual write left, forever.
Since the armed-latch redisplay landed (armed sprite latches keep
serializing across DMA-idle frames), that stale state became visible:
software that runs a DMA sprite scene, lets the null terminator's CTL
fetch disarm every channel, turns SPREN off, and re-arms with the
SPRxDATA=$0000 idiom expects invisible sprites (the latches hold the
DMA-written zeros on real hardware), not a full-height redisplay of a
manual pattern written thousands of frames earlier.

Denise now keeps two views of the sprite registers:

- sprpos/sprctl/sprdata/sprdatb/spr_armed stay the CPU/Copper write
  shadow. The render replay's manual-sprite model and the live collision
  path are calibrated against it and are bit-identical to before (seeding
  the replay from hardware-true registers instead regressed the vAmigaTS
  oldsprtimdat render, which a real-A500 photo pins).
- spr_hw_pos/ctl/data/datb/armed are the registers as the hardware holds
  them: last writer wins across manual writes AND DMA fetches (a DATA
  fetch arms, the vstop control fetch - including the 0/0 terminator -
  disarms). The DMA-idle latched redisplay seeds from this view.

Only the authoritative sprite-DMA pass for a line writes the hardware
view through: pre-display lines are computed twice (a provisional live
pass, then the pre-display replay at the display start re-runs them with
the frame's final DMACON/SPRxPT event timeline and owns the result), and
the provisional pass can fetch bogus data lines when SPREN is enabled
near the frame wrap, before the vertical-blank reset's control fetch
nulls stale comparators.

STATE_VERSION 27 -> 28: Denise and RenderRegisterSnapshot gained the
hardware-view fields.

Regression example: Hamazing's kaleidoscope scene (~132s) painted the
transition scene's sprite bars over the right 40% of the display; it now
renders clean from boot, byte-identical to before at every other scene.
The vAmigaTS Denise/Sprites family (214 renders) is byte-identical to
the pre-fix build.
sprprobe-latch renders the Hamazing scene-switch sequence as a static
display: full-height bars armed manually with SPREN off, a DMA phase
whose null-terminator CTL fetch disarms every channel and overwrites
SPR0's data words, then SPREN off again with three manual re-arms. The
steady frame must show exactly two bars: SPR2 (full manual re-arm after
the DMA disarm) and SPR6 (DATA-only re-arm - its DATB latch still holds
the phase-A word because control fetches never touch the data latches).
SPR0's arm-with-zero must stay invisible (both data latches hold
DMA-written zeros) and SPR3, left alone, must stay disarmed; a bar at
either position is a latch write-through regression.

DMACON is toggled on line $136, after the last display line, to keep the
probe off the SPREN-enable frame-wrap edge where the provisional
pre-display sprite pass diverges from the authoritative replay.

Cross-checked against vAmiga (tools/vamiga-ref.sh): the steady render's
bar positions, widths, and colour runs match pixel for pixel. Verified
static (40s shot == 44s shot). Wired into tests/probe_golden.rs and
blessed.
@LinuxJedi LinuxJedi merged commit 3a374a9 into main Jul 13, 2026
8 checks passed
@LinuxJedi LinuxJedi deleted the fix/sprite-dma-denise-latch-writethrough branch July 13, 2026 08:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant