From 7aea6439dbe858d898db69983e64c796f58bcf9f Mon Sep 17 00:00:00 2001 From: Highbyte Date: Thu, 17 Sep 2026 21:14:27 +0200 Subject: [PATCH] Do not hold the CPU for a sprite enabled after the line's DMA compares The bus stall model counts a sprite that is about to start (enabled, DMA off, Y on the line) among the fetching ones, so that sprite 0's BA window is seen before the compare in cycle 55 has run. That prediction was kept for the whole line: a sprite enabled, or moved onto the line, after both compares got a window although it does not start there, and a read was held for a fetch that never happens. The prediction now ends with the second compare. VICE's spriteenable4 test programs enable sprites 0-2 in cycle 57 of the line their Y names and time the code that follows; they now match on PAL and NTSC. --- docs/systems/c64/libraries.md | 2 +- .../Video/Vic2BusStalls.cs | 11 ++++++++--- .../Commodore64/Video/Vic2BusStallTests.cs | 17 +++++++++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/docs/systems/c64/libraries.md b/docs/systems/c64/libraries.md index 65c6852fa..56c727bb5 100644 --- a/docs/systems/c64/libraries.md +++ b/docs/systems/c64/libraries.md @@ -59,7 +59,7 @@ VIC-II fetched during the stall reflects memory before the stalled instruction's The VIC-II also takes the bus from the CPU as on hardware: 40 cycles on every bad line (BA low from cycle 12, video matrix fetches in cycles 15-54) and two cycles per sprite with DMA on, BA -low three cycles ahead. A CPU read that falls inside such a window waits until the window ends; +low three cycles ahead (a sprite enabled, or moved onto the line, after the line's two DMA compares does not start there and takes no bus). A CPU read that falls inside such a window waits until the window ends; writes do not wait. Bad lines follow YSCROLL and the DEN bit as the VIC-II saw it during raster line $30: clearing DEN before that line switches the display, and its bad lines, off for the whole frame, clearing it later has no effect until the next frame, and setting it partway through line diff --git a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/Vic2BusStalls.cs b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/Vic2BusStalls.cs index f455ee6a1..a69be91d6 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/Vic2BusStalls.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/Vic2BusStalls.cs @@ -51,7 +51,7 @@ public ulong StallCyclesForRead(ulong busCycle, out ulong nextCheckBusCycle) var line = (int)(frameIndex / (ulong)_cyclesPerLine); var offset = (int)(frameIndex % (ulong)_cyclesPerLine); - BuildWindows(line); + BuildWindows(line, offset); for (var i = 0; i < _windowCount; i++) { @@ -90,7 +90,7 @@ public ulong StallCyclesForRead(ulong busCycle, out ulong nextCheckBusCycle) /// the previous line's sprite 3-7 DMA that runs into this line. Windows beyond the line are /// kept (a stall may run past the line end); overlapping and adjacent windows are merged. /// - private void BuildWindows(int line) + private void BuildWindows(int line, int offset) { _windowCount = 0; @@ -104,7 +104,12 @@ private void BuildWindows(int line) // The compare in cycle 55 switches DMA on in the same cycle sprite 0's window begins, so // a sprite about to start counts here; the read that hits the window is re-evaluated there. - var active = (byte)(_vic2.SpriteDmaMask | _vic2.SpriteDmaStartMask(line)); + // Only until the second compare has been made: a sprite enabled after it, or whose Y is + // written to the line after it, does not start on this line and takes no bus. + var secondCompareOffset = SpritePointerOffset(0) - 2; + var active = offset < secondCompareOffset + ? (byte)(_vic2.SpriteDmaMask | _vic2.SpriteDmaStartMask(line)) + : _vic2.SpriteDmaMask; for (var n = 0; n < 8; n++) if ((active & (1 << n)) != 0) AddWindow(SpritePointerOffset(n)); diff --git a/tests/Highbyte.DotNet6502.Systems.Tests/Commodore64/Video/Vic2BusStallTests.cs b/tests/Highbyte.DotNet6502.Systems.Tests/Commodore64/Video/Vic2BusStallTests.cs index 16c34e790..aacd643c5 100644 --- a/tests/Highbyte.DotNet6502.Systems.Tests/Commodore64/Video/Vic2BusStallTests.cs +++ b/tests/Highbyte.DotNet6502.Systems.Tests/Commodore64/Video/Vic2BusStallTests.cs @@ -256,6 +256,23 @@ public void A_y_position_written_after_the_raster_passed_it_does_not_start_dma() Assert.Equal(0, c64.Vic2.SpriteDmaMask); } + [Theory] + [InlineData(55, 3)] // enabled in time for the first compare: sprite 0 starts, a read in cycle 57 waits until 60 + [InlineData(57, 0)] // enabled after both compares: no DMA on this line, the bus stays free + public void A_sprite_enabled_after_the_compares_takes_no_bus_on_that_line(int enableCycle, int expectedStall) + { + // VICE's spriteenable4 test programs enable sprites 0-2 in cycle 57 of the line their Y + // names and time the code that follows: it is not held up. + var c64 = Build([0xEA]); + c64.Mem.Write(Vic2Addr.SPRITE_0_Y, 100); + PositionAt(c64, 100, enableCycle); + c64.Mem.Write(Vic2Addr.SPRITE_ENABLE, 0x01); + c64.Vic2.AdvanceRaster((ulong)(57 - enableCycle)); // the next bus cycle is cycle 57 + + Assert.Equal(2 + (ulong)expectedStall, Step(c64)); + Assert.Equal(expectedStall == 0 ? 0 : 1, c64.Vic2.SpriteDmaMask); + } + [Fact] public void Dma_switches_on_when_the_raster_reaches_the_sprite_y() {