diff --git a/docs/systems/c64/libraries.md b/docs/systems/c64/libraries.md
index ab9cd1efc..080ced4dd 100644
--- a/docs/systems/c64/libraries.md
+++ b/docs/systems/c64/libraries.md
@@ -104,7 +104,11 @@ that pixel like the background colours do. Sprite-to-sprite collisions come from
sprite-to-background collisions, from the runs' opaque pixels against the foreground pixels the
graphics sequencer output on the line (a set bit, or a 10/11 pair in multicolour; nothing while
the vertical border flip-flop is set). The collision interrupt flags latch whether or not the
-source is enabled, and reading a collision register clears the collisions, not the flag. Where
+source is enabled, and reading a collision register clears the collisions, not the flag. A
+collision register read in the middle of a line reports the collisions of the sprite pixels the
+beam has passed, up to four pixels before the start of the read's cycle, and clears the register
+as that cycle ends, so the collisions of the twelve pixels in between are lost and those of the
+rest of the line show in the next read. Where
sprites overlap, the lowest-numbered one with an opaque pixel is shown and its own priority bit
alone decides against the foreground graphics, so a sprite in front of the graphics does not show
through a higher-priority sprite that is behind them. The
diff --git a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/IVic2CycleRenderer.cs b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/IVic2CycleRenderer.cs
index ecaf49703..29e68b12c 100644
--- a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/IVic2CycleRenderer.cs
+++ b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/IVic2CycleRenderer.cs
@@ -11,4 +11,12 @@ public interface IVic2CycleRenderer
{
/// Draw every cycle between where the renderer last stopped and where the VIC-II is now.
void CatchUpToVic2();
+
+ ///
+ /// A read of the sprite-to-background collision register in the middle of a raster line:
+ /// latch the collisions of the line's sprite pixels before
+ /// (counted from the line's first cycle), which the read reports, and leave out of the line's
+ /// remaining collisions those before , which it clears.
+ ///
+ void LatchSpriteBackgroundCollisions(int rasterLine, int upToPixel, int clearedToPixel);
}
diff --git a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/IVic2RasterizerPixelGenerator.cs b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/IVic2RasterizerPixelGenerator.cs
index 17d0bdcd5..ac93a7613 100644
--- a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/IVic2RasterizerPixelGenerator.cs
+++ b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/IVic2RasterizerPixelGenerator.cs
@@ -12,6 +12,9 @@ public interface IVic2RasterizerPixelGenerator
/// Draw the pixels of every cycle between where the generator last stopped and where the VIC-II is now.
void CatchUpToVic2();
+ /// See . A generator that does not resolve collisions from its pixels ignores it.
+ void LatchSpriteBackgroundCollisions(int rasterLine, int upToPixel, int clearedToPixel) { }
+
/// Finish the frame: the last line, the end-of-frame sprite pass, register resync.
void OnEndFrame();
}
diff --git a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/Vic2Rasterizer.cs b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/Vic2Rasterizer.cs
index 9bcde9354..31c2fb4f4 100644
--- a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/Vic2Rasterizer.cs
+++ b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/Vic2Rasterizer.cs
@@ -124,6 +124,9 @@ public void CatchUpToVic2()
_pixelGenerator.CatchUpToVic2();
}
+ public void LatchSpriteBackgroundCollisions(int rasterLine, int upToPixel, int clearedToPixel)
+ => _pixelGenerator.LatchSpriteBackgroundCollisions(rasterLine, upToPixel, clearedToPixel);
+
//public void OnEndScanline(int y)
//{
// ScanlineCompleted?.Invoke(this, y);
diff --git a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/Vic2RasterizerSequencerPixelGenerator.cs b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/Vic2RasterizerSequencerPixelGenerator.cs
index c7511401e..7340a112a 100644
--- a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/Vic2RasterizerSequencerPixelGenerator.cs
+++ b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/Vic2RasterizerSequencerPixelGenerator.cs
@@ -669,15 +669,26 @@ private void FinishLineRuns()
/// Write screen data for all clock cycles since last time this method was called.
/// Instructions can take different amount of cycles to execute, so this method is called after each instruction to update the screen data and will catch up on what's to do since last time it was called.
///
- public void CatchUpToVic2()
+ public void CatchUpToVic2() => CatchUpTo(_c64.Vic2.CyclesConsumedCurrentVblank);
+
+ // Draws the cycles before endCycle (a cycle count into the frame).
+ private void CatchUpTo(ulong endCycle)
{
+ // Already there, or one cycle beyond it (the cycle of a collision register read, drawn
+ // before it ended). Further behind means a new frame has begun: start over from there.
+ if (endCycle == _lastCyclesConsumedCurrentVblank || endCycle + 1 == _lastCyclesConsumedCurrentVblank)
+ return;
+ if (endCycle < _lastCyclesConsumedCurrentVblank)
+ {
+ _lastCyclesConsumedCurrentVblank = endCycle;
+ return;
+ }
if (_registerWritesOverflowed)
ResyncColorRegisters();
// Loop cycles since last time we processed (each instruction). The line and the cycle
// within it are derived once and then counted along: a division per cycle would be a
// large share of the frame on its own.
- var endCycle = _c64.Vic2.CyclesConsumedCurrentVblank;
var cycleCurrentVblank = _lastCyclesConsumedCurrentVblank;
var rasterLine = (int)(cycleCurrentVblank / _cyclesPerLine);
var cycleOnScreenLine = cycleCurrentVblank - (ulong)rasterLine * _cyclesPerLine;
@@ -820,7 +831,96 @@ public void CatchUpToVic2()
_registerWriteNext = 0;
}
- _lastCyclesConsumedCurrentVblank = _c64.Vic2.CyclesConsumedCurrentVblank;
+ _lastCyclesConsumedCurrentVblank = endCycle;
+ }
+
+ // The raster line whose sprite-to-background collisions reads of the register have latched
+ // or cleared up to a pixel; the line's end latches the rest.
+ private int _backgroundCollisionsLatchedLine = -1;
+ private int _backgroundCollisionsLatchedToPixel;
+
+ public void LatchSpriteBackgroundCollisions(int rasterLine, int upToPixel, int clearedToPixel)
+ {
+ if (!_perLineSprites)
+ return;
+ // First the cycles before the read's, which can end the previous line and latch what was
+ // left of its collisions.
+ CatchUpToVic2();
+ var spriteManager = _c64.Vic2.SpriteManager;
+ var fromPixel = _backgroundCollisionsLatchedLine == rasterLine ? _backgroundCollisionsLatchedToPixel : 0;
+ var hasPixels = upToPixel > fromPixel && spriteManager.LineSpriteRunMask(rasterLine) != 0;
+ if (hasPixels)
+ {
+ // The graphics under the last of those pixels are the ones the cycle of the read
+ // shows: a read changes nothing, so that cycle can be drawn before it has ended.
+ var cycles = _c64.Vic2.CyclesConsumedCurrentVblank;
+ if (cycles % _cyclesPerLine != 0 && cycles % _cyclesPerLine + 1 < _cyclesPerLine)
+ CatchUpTo(cycles + 1);
+ }
+ _backgroundCollisionsLatchedLine = rasterLine;
+ _backgroundCollisionsLatchedToPixel = Math.Max(fromPixel, clearedToPixel);
+ var screenLine = rasterLine < _rasterToScreenLine.Length ? _rasterToScreenLine[rasterLine] : rasterLine;
+ if (!hasPixels || screenLine != _lastScreenLineDataUpdate)
+ return; // nothing output yet, or not a line that is drawn
+ byte backgroundCollisions = 0;
+ for (int spriteIndex = 0; spriteIndex < SPRITE_COUNT; spriteIndex++)
+ {
+ var runs = Math.Min(spriteManager.LineSpriteRunCount(rasterLine, spriteIndex), SPRITE_RUNS);
+ for (int run = 0; run < runs && (backgroundCollisions & (1 << spriteIndex)) == 0; run++)
+ {
+ var startPixel = spriteManager.LineSpriteRunStart(rasterLine, spriteIndex, run);
+ var vicX = _xCoordinateAtLineStart + startPixel;
+ if (vicX >= (int)_cyclesPerLine * 8)
+ vicX -= (int)_cyclesPerLine * 8;
+ if (SpriteRunHitsForeground(rasterLine, spriteIndex, run, startPixel, SpriteScreenX(vicX),
+ spriteManager.LineSpriteRunFlags(rasterLine, spriteIndex, run), spriteManager.LineSpriteRunLength(rasterLine, spriteIndex, run),
+ spriteManager.LineSpriteRunStretch(rasterLine, spriteIndex, run), _lineClearStartX, _lineClearEndX, fromPixel, upToPixel))
+ {
+ backgroundCollisions |= (byte)(1 << spriteIndex);
+ }
+ }
+ }
+ if (backgroundCollisions != 0)
+ spriteManager.AddSpriteToBackgroundCollisions(backgroundCollisions);
+ }
+
+ ///
+ /// Whether a sprite run of the line being drawn has an opaque pixel, among those of the line's
+ /// pixels fromPixel..toPixel-1, where the graphics sequencer output a foreground pixel (a set
+ /// bit, or a 10/11 pair in multicolour; nothing while the vertical border flip-flop is set,
+ /// when the sequencer's output is off), resolved where the border flip-flop was clear.
+ ///
+ private bool SpriteRunHitsForeground(int rasterLine, int spriteIndex, int run, int startPixel, int screenX, byte runFlags, int length, int stretch,
+ int clipStartX, int clipEndX, int fromPixel, int toPixel)
+ {
+ var decoded = (runFlags & Vic2SpriteManager.RunFlagDecoded) != 0;
+ var pixelCount = decoded ? length : Math.Min(length + stretch, RUN_PIXELS);
+ var first = Math.Max(0, fromPixel - startPixel);
+ var end = toPixel == int.MaxValue ? pixelCount : Math.Min(pixelCount, toPixel - startPixel);
+ var from = Math.Max(Math.Max(0, clipStartX), screenX + first);
+ var to = Math.Min(Math.Min(_width, clipEndX), screenX + end);
+ // Most lines have no foreground under the sprite (a sprite-only demo, the borders, blank
+ // cells): one vectorised search before any decoding.
+ var firstForeground = to > from ? _lineFgCodes.AsSpan(from, to - from).IndexOfAnyExcept(CODE_NONE) : -1;
+ if (firstForeground < 0)
+ return false;
+ return SpriteRunHitsForegroundFrom(rasterLine, spriteIndex, run, screenX, runFlags, length, stretch, from + firstForeground, to);
+ }
+
+ private bool SpriteRunHitsForegroundFrom(int rasterLine, int spriteIndex, int run, int screenX, byte runFlags, int length, int stretch, int from, int to)
+ {
+ var spriteManager = _c64.Vic2.SpriteManager;
+ Span codes = stackalloc byte[RUN_PIXELS];
+ if ((runFlags & Vic2SpriteManager.RunFlagDecoded) != 0)
+ spriteManager.LineSpriteRunPixels(rasterLine, spriteIndex, run).Slice(0, length).CopyTo(codes);
+ else
+ Vic2SpriteManager.ExpandRunPixels(spriteManager.LineSpriteRunData(rasterLine, spriteIndex, run), runFlags, length, stretch, codes);
+ for (var x = from; x < to; x++)
+ {
+ if (_lineFgCodes[x] != CODE_NONE && (codes[x - screenX] & Vic2SpriteManager.RunPixelValueMask) != 0)
+ return true;
+ }
+ return false;
}
public void OnEndFrame()
@@ -864,7 +964,9 @@ private void DrawSpritesForLine(int screenLine)
var clipEndX = _lineClearEndXs[pixelArrayY];
var pixelsPerLine = (int)_cyclesPerLine * 8;
byte backgroundCollisions = 0;
- Span codes = stackalloc byte[RUN_PIXELS];
+ // The line's pixels whose collisions a read of the register has latched or cleared already.
+ var collisionsFromPixel = _backgroundCollisionsLatchedLine == _slRasterLine ? _backgroundCollisionsLatchedToPixel : 0;
+ _backgroundCollisionsLatchedLine = -1;
for (int spriteIndex = 0; spriteIndex < SPRITE_COUNT; spriteIndex++)
{
if ((runMask & (1 << spriteIndex)) == 0)
@@ -910,34 +1012,12 @@ private void DrawSpritesForLine(int screenLine)
_lineSpriteClipStartX[index] = clipStartX;
_lineSpriteClipEndX[index] = clipEndX;
- // Sprite-to-background collision: an opaque sprite pixel where the graphics
- // sequencer output a foreground pixel on this line (a set bit, or a 10/11 pair in
- // multicolour; nothing while the vertical border flip-flop is set, when the
- // sequencer's output is off). The line's codes are still those of the line that
- // has just ended, resolved where the border flip-flop was clear.
- if ((backgroundCollisions & (1 << spriteIndex)) == 0)
+ // Sprite-to-background collision, from the line's codes, which are still those of
+ // the line that has just ended.
+ if ((backgroundCollisions & (1 << spriteIndex)) == 0
+ && SpriteRunHitsForeground(_slRasterLine, spriteIndex, run, startPixel, screenX, runFlags, length, stretch, clipStartX, clipEndX, collisionsFromPixel, int.MaxValue))
{
- var pixelCount = decoded ? length : Math.Min(length + stretch, RUN_PIXELS);
- var from = Math.Max(Math.Max(0, clipStartX), screenX);
- var to = Math.Min(Math.Min(_width, clipEndX), screenX + pixelCount);
- // Most lines have no foreground under the sprite (a sprite-only demo, the
- // borders, blank cells): one vectorised search before any decoding.
- var firstForeground = to > from ? _lineFgCodes.AsSpan(from, to - from).IndexOfAnyExcept(CODE_NONE) : -1;
- if (firstForeground >= 0)
- {
- if (decoded)
- _lineSpriteRunCodes.AsSpan(index * RUN_PIXELS, length).CopyTo(codes);
- else
- Vic2SpriteManager.ExpandRunPixels(row, runFlags, length, stretch, codes);
- for (var x = from + firstForeground; x < to; x++)
- {
- if (_lineFgCodes[x] != CODE_NONE && (codes[x - screenX] & Vic2SpriteManager.RunPixelValueMask) != 0)
- {
- backgroundCollisions |= (byte)(1 << spriteIndex);
- break;
- }
- }
- }
+ backgroundCollisions |= (byte)(1 << spriteIndex);
}
}
}
diff --git a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/IVic2SpriteManager.cs b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/IVic2SpriteManager.cs
index 6da789988..fd2715860 100644
--- a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/IVic2SpriteManager.cs
+++ b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/IVic2SpriteManager.cs
@@ -87,7 +87,7 @@ public interface IVic2SpriteManager
public ReadOnlySpan LineSpriteRunPixels(int rasterLine, int sprite, int run);
public void AddLineSpriteRun(int rasterLine, int sprite, int run, int startPixel, uint rowBits, int length, int stretch, byte flags);
public void AddLineSpriteDecodedRun(int rasterLine, int sprite, int run, int startPixel, ReadOnlySpan pixels);
- public void EndLineSpriteCollisions(int rasterLine);
+ public void LatchLineSpriteCollisions(int rasterLine, int fromPixel, int toPixel);
///
/// Captures the per-line sprite trigger-input snapshot (enable mask + Y). Called once per raster
@@ -100,7 +100,7 @@ public interface IVic2SpriteManager
/// Accumulates sprite-to-background collisions for a single raster line into the collision
/// store, using the sprites' positions at the line's start. Called once per raster line from
/// when . Sprite-to-sprite
- /// collisions come from the line's output runs instead ().
+ /// collisions come from the line's output runs instead ().
///
public void AccumulatePerLineCollisions(int rasterLine);
diff --git a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/Vic2.cs b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/Vic2.cs
index 9b2a45366..6c346dc45 100644
--- a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/Vic2.cs
+++ b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/Vic2.cs
@@ -818,15 +818,63 @@ private void JournalSpriteRegisterWrite(ushort address, byte value, int visibleP
/// cycle 58. A row that no match shifts out stays in the register for the next line.
///
private void EndLineSprites(ushort line)
+ {
+ DeriveLineSprites(line, _cyclesPerLine * 8, endOfLine: true);
+ _spriteXJournalCount = 0;
+ _spriteCollisionsLatchedToPixel = 0;
+ }
+
+ // A read of the collision register reports the sprite pixels up to four before the start of
+ // its cycle (VICE's sprite-sprite-collision-cycle test program) and clears the register as the
+ // cycle ends: the collisions of the twelve pixels in between are lost (VICE's spritevssprite
+ // test program, which reads the register twice, four cycles apart).
+ private const int SpriteCollisionReadPixel = -4;
+ private const int SpriteCollisionClearPixel = 8;
+
+ // The pixel of the current line up to which its sprite-to-sprite collisions have been latched
+ // or cleared by reads of the register; the line's end latches the rest.
+ private int _spriteCollisionsLatchedToPixel;
+
+ ///
+ /// Latches the current line's collisions up to the beam position of the collision register
+ /// read in progress, so the read reports, and clears, the collisions of the pixels output so
+ /// far and no others: the sprite-to-sprite ones here, the sprite-to-background ones
+ /// () in the renderer that resolves the line's pixels.
+ ///
+ private void LatchSpriteCollisionsUpToCurrentAccess(bool withBackground)
+ {
+ if (!SpriteManager.PerLineCollisionEnabled || _currentRasterLineInternal == ushort.MaxValue)
+ return;
+ if (withBackground && (!SpriteManager.BackgroundCollisionsFromRenderer || C64.Vic2CycleRenderer == null))
+ return;
+ var cycle = (int)(CyclesConsumedCurrentVblank - (ulong)_currentRasterLineInternal * (ulong)_cyclesPerLine);
+ // Not beyond the display decision of cycle 58: what is output after it depends on the
+ // fetches for the next line, which are known when the line ends.
+ var lastPixel = (_cyclesPerLine - 6) * 8;
+ var upToPixel = Math.Min(cycle * 8 + SpriteCollisionReadPixel, lastPixel);
+ var clearedToPixel = Math.Min(cycle * 8 + SpriteCollisionClearPixel, lastPixel);
+ // The runs up to there, and with them the sprite-to-sprite collisions, which a read of the
+ // other register leaves in place.
+ if (upToPixel > _spriteCollisionsLatchedToPixel)
+ DeriveLineSprites(_currentRasterLineInternal, upToPixel, endOfLine: false);
+ if (withBackground)
+ C64.Vic2CycleRenderer!.LatchSpriteBackgroundCollisions(_currentRasterLineInternal, upToPixel, clearedToPixel);
+ else
+ _spriteCollisionsLatchedToPixel = Math.Max(_spriteCollisionsLatchedToPixel, clearedToPixel);
+ }
+
+ ///
+ /// The line's runs that start before , and its collisions up to
+ /// there. At the line's end the rows left in the data registers are kept for the next line;
+ /// during the line (a read of the collision register) nothing is kept but the collisions.
+ ///
+ private void DeriveLineSprites(ushort line, int upToPixel, bool endOfLine)
{
var nextLine = line + 1 >= _totalHeight ? 0 : line + 1;
var displayBefore = SpriteManager.LineSpriteDisplayMask(line);
var displayAfter = SpriteManager.LineSpriteDisplayMask(nextLine);
if ((displayBefore | displayAfter | _spriteShiftRegisterMask) == 0)
- {
- _spriteXJournalCount = 0; // nothing could have been output: no runs, no collisions
- return;
- }
+ return; // nothing could have been output: no runs, no collisions
var pixelsPerLine = _cyclesPerLine * 8;
var xAtLineStart = Vic2Model.XCoordinateAtLineStart;
// Cycle 58 (1-based): the display decision for the next line is taken.
@@ -884,7 +932,7 @@ private void EndLineSprites(ushort line)
var matchPixel = x - xAtLineStart;
if (matchPixel < 0)
matchPixel += pixelsPerLine;
- if (x < pixelsPerLine && matchPixel >= pixel && matchPixel < segmentEnd)
+ if (x < pixelsPerLine && matchPixel >= pixel && matchPixel < segmentEnd && matchPixel < upToPixel)
{
if (matchPixel >= loadPixel && !isLoaded)
{
@@ -945,15 +993,18 @@ private void EndLineSprites(ushort line)
}
pixel = segmentEnd;
}
+ if (!endOfLine)
+ continue;
if (!isLoaded)
register = loaded;
_spriteShiftRegister[n] = register;
if (register != 0)
registerMask |= bit;
}
- _spriteShiftRegisterMask = registerMask;
- _spriteXJournalCount = 0;
- SpriteManager.EndLineSpriteCollisions(line);
+ if (endOfLine)
+ _spriteShiftRegisterMask = registerMask;
+ SpriteManager.LatchLineSpriteCollisions(line, _spriteCollisionsLatchedToPixel, endOfLine ? int.MaxValue : upToPixel);
+ _spriteCollisionsLatchedToPixel = upToPixel;
}
private static uint SpriteRowBits(ReadOnlySpan row) => (uint)(row[0] << 16 | row[1] << 8 | row[2]);
@@ -1055,6 +1106,7 @@ public void SpriteToSpriteCollisionStore(ushort address, byte value)
public byte SpriteToSpriteCollisionLoad(ushort address)
{
+ LatchSpriteCollisionsUpToCurrentAccess(withBackground: false);
var val = SpriteManager.SpriteToSpriteCollisionStore;
SpriteManager.SpriteToSpriteCollisionStore = 0; // Collision state is cleared after reading
SpriteManager.SpriteToSpriteCollisionIRQBlock = false; // Enable IRQs to be able to triggered again
@@ -1067,6 +1119,7 @@ public void SpriteToBackgroundCollisionStore(ushort address, byte value)
}
public byte SpriteToBackgroundCollisionLoad(ushort address)
{
+ LatchSpriteCollisionsUpToCurrentAccess(withBackground: true);
var val = SpriteManager.SpriteToBackgroundCollisionStore;
SpriteManager.SpriteToBackgroundCollisionStore = 0; // Collision state is cleared after reading
SpriteManager.SpriteToBackgroundCollisionIRQBlock = false; // Enable IRQs to be able to triggered again
diff --git a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/Vic2SpriteManager.cs b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/Vic2SpriteManager.cs
index f035c633c..e8b8fa513 100644
--- a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/Vic2SpriteManager.cs
+++ b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/Vic2SpriteManager.cs
@@ -248,12 +248,14 @@ public static int ExpandRunPixels(uint row, byte flags, int length, int stretch,
}
///
- /// The sprite-to-sprite collisions of a line that has just ended, from the runs the VIC-II
- /// derived: two sprites collide where both output an opaque pixel. Evaluated at the line's
- /// end, so the register shows the collision after the line's pixels, as on the chip, not
- /// before the CPU has run the line's code.
+ /// The sprite-to-sprite collisions of a line's pixels from up to
+ /// (exclusive, counted from the line's first cycle), from the runs
+ /// the VIC-II derived: two sprites collide where both output an opaque pixel. Evaluated when
+ /// the line ends, and up to the beam when the register is read during it, so the register
+ /// shows a collision after its pixels, as on the chip, not before the CPU has run the line's
+ /// code, and a read clears only what the beam has passed.
///
- public void EndLineSpriteCollisions(int rasterLine)
+ public void LatchLineSpriteCollisions(int rasterLine, int fromPixel, int toPixel)
{
var runMask = _lineSpriteRunMasks[rasterLine];
if (runMask == 0 || (runMask & (runMask - 1)) == 0)
@@ -277,6 +279,7 @@ public void EndLineSpriteCollisions(int rasterLine)
: RunOpaqueMask(LineSpriteRunData(rasterLine, n, r), (flags & RunFlagXExpand) != 0, (flags & RunFlagMultiColor) != 0,
LineSpriteRunLength(rasterLine, n, r), LineSpriteRunStretch(rasterLine, n, r));
starts[i] = LineSpriteRunStart(rasterLine, n, r);
+ masks[i] &= PixelWindowMask(fromPixel - starts[i], toPixel - starts[i]);
}
}
var collided = false;
@@ -316,6 +319,15 @@ public void EndLineSpriteCollisions(int rasterLine)
private const int RunMaskBits = RunPixelCapacity; // up to 48 shown pixels and 7 repeated
+ // The bits from..to-1 of a run mask (positions relative to the run's first pixel).
+ private static ulong PixelWindowMask(int from, int to)
+ {
+ if (to <= 0 || from >= 64 || to <= from)
+ return 0;
+ var upTo = to >= 64 ? ulong.MaxValue : (1ul << to) - 1;
+ return from <= 0 ? upTo : upTo & ~((1ul << from) - 1);
+ }
+
// The opaque pixels of a decoded run as a mask, bit 0 its first pixel.
private static ulong DecodedRunOpaqueMask(ReadOnlySpan pixels)
{
@@ -602,7 +614,7 @@ public void AccumulatePerLineCollisions(int rasterLine)
}
// Sprite-to-sprite collisions are evaluated when the line ends, from the output runs the
- // VIC-II derives with the X compare per pixel (EndLineSpriteCollisions).
+ // VIC-II derives with the X compare per pixel (LatchLineSpriteCollisions).
// Mid-frame collision IRQ: raise as soon as a new collision is latched on this raster line
// (the CPU services it on the next instruction boundary, like the raster IRQ), instead of
diff --git a/tests/Highbyte.DotNet6502.Systems.Tests/Commodore64/Video/Vic2CollisionReadTimingTests.cs b/tests/Highbyte.DotNet6502.Systems.Tests/Commodore64/Video/Vic2CollisionReadTimingTests.cs
new file mode 100644
index 000000000..cfc17a156
--- /dev/null
+++ b/tests/Highbyte.DotNet6502.Systems.Tests/Commodore64/Video/Vic2CollisionReadTimingTests.cs
@@ -0,0 +1,125 @@
+using Highbyte.DotNet6502.Systems.Commodore64;
+using Highbyte.DotNet6502.Systems.Commodore64.Config;
+using Highbyte.DotNet6502.Systems.Commodore64.Render.Rasterizer;
+using Highbyte.DotNet6502.Systems.Commodore64.Video;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Highbyte.DotNet6502.Systems.Tests.Commodore64.Video;
+
+///
+/// The sprite collision registers read in the middle of a raster line. A collision is latched as
+/// its pixels are output, so a read reports those the beam has passed and no later ones: the
+/// sprite pixels up to four before the start of the read's cycle. The read clears the register as
+/// its cycle ends, and the collisions of the twelve pixels in between are lost. What the line
+/// outputs after that is latched as usual and shows in the next read.
+/// Cycles are 0-based offsets into the line. On PAL the line starts at X coordinate 404 of 504,
+/// so a sprite at X 100 outputs its first pixel as pixel 200 of the line, the first of cycle 25.
+///
+public class Vic2CollisionReadTimingTests
+{
+ private const int CyclesPerLine = 63;
+ private const int SpriteLine = 52; // the first line the sprites are displayed on
+ private const int SpriteX = 100; // pixel 200 of the line
+
+ [Theory]
+ [InlineData(25, 0)] // reports pixels up to 195
+ [InlineData(26, 3)] // reports pixels up to 203
+ public void A_sprite_collision_read_reports_the_pixels_the_beam_has_passed(int readCycle, int expected)
+ {
+ var c64 = BuildWithSprites(SpriteX, foreground: false);
+ AdvanceTo(c64, SpriteLine, readCycle);
+ Assert.Equal(expected, c64.Mem.Read(Vic2Addr.SPRITE_TO_SPRITE_COLLISION));
+ }
+
+ [Theory]
+ [InlineData(SpriteX, 0)] // pixel 200: between what the read at cycle 25 reports and the end of its cycle, lost
+ [InlineData(SpriteX + 7, 0)] // pixel 207: the last one lost
+ [InlineData(SpriteX + 8, 3)] // pixel 208: after the read's cycle, latched for the next read
+ public void A_sprite_collision_read_clears_the_register_as_its_cycle_ends(int spriteX, int expectedInNextRead)
+ {
+ var c64 = BuildWithSprites(spriteX, foreground: false);
+ AdvanceTo(c64, SpriteLine, 25);
+ Assert.Equal(0, c64.Mem.Read(Vic2Addr.SPRITE_TO_SPRITE_COLLISION));
+ AdvanceTo(c64, SpriteLine, 30);
+ Assert.Equal(expectedInNextRead, c64.Mem.Read(Vic2Addr.SPRITE_TO_SPRITE_COLLISION));
+ // The line's end latches nothing that a read has reported or cleared.
+ AdvanceTo(c64, SpriteLine + 1, 10);
+ Assert.Equal(0, c64.Mem.Read(Vic2Addr.SPRITE_TO_SPRITE_COLLISION));
+ }
+
+ [Fact]
+ public void A_sprite_collision_not_read_during_the_line_is_latched_when_the_line_ends()
+ {
+ var c64 = BuildWithSprites(SpriteX, foreground: false);
+ AdvanceTo(c64, SpriteLine + 1, 10);
+ Assert.Equal(3, c64.Mem.Read(Vic2Addr.SPRITE_TO_SPRITE_COLLISION));
+ }
+
+ [Theory]
+ [InlineData(25, 0)]
+ [InlineData(26, 1)]
+ public void A_background_collision_read_reports_the_pixels_the_beam_has_passed(int readCycle, int expected)
+ {
+ var c64 = BuildWithSprites(SpriteX, foreground: true);
+ AdvanceTo(c64, SpriteLine, readCycle);
+ Assert.Equal(expected, c64.Mem.Read(Vic2Addr.SPRITE_TO_BACKGROUND_COLLISION));
+ }
+
+ [Theory]
+ [InlineData(SpriteX + 7, 0)]
+ [InlineData(SpriteX + 8, 1)]
+ public void A_background_collision_read_clears_the_register_as_its_cycle_ends(int spriteX, int expectedInNextRead)
+ {
+ var c64 = BuildWithSprites(spriteX, foreground: true);
+ AdvanceTo(c64, SpriteLine, 25);
+ Assert.Equal(0, c64.Mem.Read(Vic2Addr.SPRITE_TO_BACKGROUND_COLLISION));
+ AdvanceTo(c64, SpriteLine, 30);
+ Assert.Equal(expectedInNextRead, c64.Mem.Read(Vic2Addr.SPRITE_TO_BACKGROUND_COLLISION));
+ // The line's end latches nothing that a read has reported or cleared. (The generator
+ // resolves a line's remaining collisions when it draws the first cycle of the next.)
+ AdvanceTo(c64, SpriteLine + 1, 10);
+ ((Vic2Rasterizer)c64.RenderProvider!).CatchUpToVic2();
+ Assert.Equal(0, c64.Mem.Read(Vic2Addr.SPRITE_TO_BACKGROUND_COLLISION));
+ }
+
+ ///
+ /// Sprites 0 and 1 at the same position, one line high in effect: a single pixel, the
+ /// leftmost, in every row, displayed from . With
+ /// only sprite 0, over idle graphics that are foreground in
+ /// every column (YSCROLL 7 makes line 55 the first bad line, so the display window's lines
+ /// before it show the byte at $3FFF). The raster is left at the start of the sprite line.
+ ///
+ private static C64 BuildWithSprites(int spriteX, bool foreground)
+ {
+ var c64 = C64.BuildC64(new C64Config
+ {
+ LoadROMs = false,
+ C64Model = "C64PAL",
+ Vic2Model = "PAL",
+ RenderProviderType = typeof(Vic2Rasterizer),
+ Vic2RasterizerPerLineSprites = true,
+ }, NullLoggerFactory.Instance);
+ c64.Mem.Write(0xD011, 0x1F);
+ c64.Mem.Write(0xD016, 0xC8);
+ c64.Mem.Write(0xD018, 0x14); // the screen, and with it the sprite pointers, at $0400
+ c64.Vic2.Vic2Mem[0x3FFF] = (byte)(foreground ? 0xFF : 0x00);
+ const int pointer = 13; // $0340
+ for (var row = 0; row < 21; row++)
+ c64.Vic2.Vic2Mem[(ushort)(pointer * 64 + row * 3)] = 0x80;
+ c64.Vic2.Vic2Mem[0x07F8] = pointer;
+ c64.Vic2.Vic2Mem[0x07F9] = pointer;
+ c64.Mem.Write(Vic2Addr.SPRITE_0_X, (byte)spriteX);
+ c64.Mem.Write(Vic2Addr.SPRITE_0_X + 2, (byte)spriteX);
+ c64.Mem.Write(Vic2Addr.SPRITE_0_Y, SpriteLine - 1);
+ c64.Mem.Write(Vic2Addr.SPRITE_0_Y + 2, SpriteLine - 1);
+ c64.Mem.Write(Vic2Addr.SPRITE_ENABLE, (byte)(foreground ? 0x01 : 0x03));
+ AdvanceTo(c64, SpriteLine, 0);
+ return c64;
+ }
+
+ private static void AdvanceTo(C64 c64, int line, int cycle)
+ {
+ var target = (ulong)(line * CyclesPerLine + cycle);
+ c64.Vic2.AdvanceRaster(target - c64.Vic2.CyclesConsumedCurrentVblank);
+ }
+}