Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/systems/c64/libraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,12 @@ public interface IVic2CycleRenderer
{
/// <summary>Draw every cycle between where the renderer last stopped and where the VIC-II is now.</summary>
void CatchUpToVic2();

/// <summary>
/// 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 <paramref name="upToPixel"/>
/// (counted from the line's first cycle), which the read reports, and leave out of the line's
/// remaining collisions those before <paramref name="clearedToPixel"/>, which it clears.
/// </summary>
void LatchSpriteBackgroundCollisions(int rasterLine, int upToPixel, int clearedToPixel);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ public interface IVic2RasterizerPixelGenerator
/// <summary>Draw the pixels of every cycle between where the generator last stopped and where the VIC-II is now.</summary>
void CatchUpToVic2();

/// <summary>See <see cref="IVic2CycleRenderer.LatchSpriteBackgroundCollisions"/>. A generator that does not resolve collisions from its pixels ignores it.</summary>
void LatchSpriteBackgroundCollisions(int rasterLine, int upToPixel, int clearedToPixel) { }

/// <summary>Finish the frame: the last line, the end-of-frame sprite pass, register resync.</summary>
void OnEndFrame();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
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;
Expand Down Expand Up @@ -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);
}

/// <summary>
/// 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.
/// </summary>
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<byte> 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()
Expand Down Expand Up @@ -864,7 +964,9 @@ private void DrawSpritesForLine(int screenLine)
var clipEndX = _lineClearEndXs[pixelArrayY];
var pixelsPerLine = (int)_cyclesPerLine * 8;
byte backgroundCollisions = 0;
Span<byte> 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)
Expand Down Expand Up @@ -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);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public interface IVic2SpriteManager
public ReadOnlySpan<byte> 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<byte> pixels);
public void EndLineSpriteCollisions(int rasterLine);
public void LatchLineSpriteCollisions(int rasterLine, int fromPixel, int toPixel);

/// <summary>
/// Captures the per-line sprite trigger-input snapshot (enable mask + Y). Called once per raster
Expand All @@ -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
/// <see cref="Vic2.AdvanceRaster"/> when <see cref="PerLineCollisionEnabled"/>. Sprite-to-sprite
/// collisions come from the line's output runs instead (<see cref="EndLineSpriteCollisions"/>).
/// collisions come from the line's output runs instead (<see cref="LatchLineSpriteCollisions"/>).
/// </summary>
public void AccumulatePerLineCollisions(int rasterLine);

Expand Down
Loading
Loading