Essgee.Unity/Assets/Plugins/Essgee/Emulation/Video/TMS99xxA.cs

882 lines
29 KiB
C#
Raw Normal View History

2025-01-02 17:55:16 +08:00
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
using Essgee.EventArguments;
using Essgee.Utilities;
using static Essgee.Emulation.Utilities;
namespace Essgee.Emulation.Video
{
/* Texas Instruments TMS99xxA family */
public class TMS99xxA : IVideo
{
public const int NumTotalScanlinesPal = 313;
public const int NumTotalScanlinesNtsc = 262;
public const int NumActiveScanlines = 192;
public const int NumActivePixelsPerScanline = 256;
protected const string layerBackgroundOptionName = "GraphicsLayersShowBackground";
protected const string layerSpritesOptionName = "GraphicsLayersShowSprites";
protected const string layerBordersOptionName = "GraphicsLayersShowBorders";
protected readonly int numTotalPixelsPerScanline = 342;
protected virtual int numTotalScanlines => (isPalChip ? NumTotalScanlinesPal : NumTotalScanlinesNtsc);
protected int topBorderSize, verticalActiveDisplaySize, bottomBorderSize;
protected int scanlineTopBorder, scanlineActiveDisplay, scanlineBottomBorder;
protected int numVisibleScanlines;
protected int leftBorderSize, horizontalActiveDisplaySize, rightBorderSize;
protected int pixelLeftBorder, pixelActiveDisplay, pixelRightBorder;
protected int numVisiblePixels;
public virtual (int X, int Y, int Width, int Height) Viewport => (0, 0, numVisiblePixels, numVisibleScanlines);
public virtual event EventHandler<SizeScreenEventArgs> SizeScreen;
public virtual void OnSizeScreen(SizeScreenEventArgs e) { SizeScreen?.Invoke(this, e); }
public virtual event EventHandler<RenderScreenEventArgs> RenderScreen;
public virtual void OnRenderScreen(RenderScreenEventArgs e) { RenderScreen?.Invoke(this, e); }
public virtual event EventHandler<EventArgs> EndOfScanline;
public virtual void OnEndOfScanline(EventArgs e) { EndOfScanline?.Invoke(this, e); }
protected const int NumSprites = 32;
protected const int NumSpritesPerLine = 4;
protected double clockRate, refreshRate;
protected bool isPalChip;
[StateRequired]
protected byte[] registers, vram;
[StateRequired]
protected (int Number, int Y, int X, int Pattern, int Attribute)[][] spriteBuffer;
protected ushort vramMask16k => 0x3FFF;
protected ushort vramMask4k => 0x0FFF;
[StateRequired]
protected bool isSecondControlWrite;
[StateRequired]
protected ushort controlWord;
[StateRequired]
protected byte readBuffer;
protected byte codeRegister => (byte)((controlWord >> 14) & 0x03);
protected ushort addressRegister
{
get { return (ushort)(controlWord & 0x3FFF); }
set { controlWord = (ushort)((controlWord & 0xC000) | (value & 0x3FFF)); }
}
[Flags]
protected enum StatusFlags : byte
{
None = 0,
SpriteCollision = (1 << 5),
SpriteOverflow = (1 << 6),
FrameInterruptPending = (1 << 7)
}
[StateRequired]
protected StatusFlags statusFlags;
protected bool isSpriteCollision
{
get { return ((statusFlags & StatusFlags.SpriteCollision) == StatusFlags.SpriteCollision); }
set { statusFlags = ((statusFlags & ~StatusFlags.SpriteCollision) | (value ? StatusFlags.SpriteCollision : StatusFlags.None)); }
}
protected bool isSpriteOverflow
{
get { return ((statusFlags & StatusFlags.SpriteOverflow) == StatusFlags.SpriteOverflow); }
set { statusFlags = ((statusFlags & ~StatusFlags.SpriteOverflow) | (value ? StatusFlags.SpriteOverflow : StatusFlags.None)); }
}
protected bool isFrameInterruptPending
{
get { return ((statusFlags & StatusFlags.FrameInterruptPending) == StatusFlags.FrameInterruptPending); }
set { statusFlags = ((statusFlags & ~StatusFlags.FrameInterruptPending) | (value ? StatusFlags.FrameInterruptPending : StatusFlags.None)); }
}
protected bool isFrameInterruptEnabled => IsBitSet(registers[0x01], 5);
[StateRequired]
public InterruptState InterruptLine { get; set; }
[StateRequired]
protected int currentScanline;
protected bool isDisplayBlanked => !IsBitSet(registers[0x01], 6);
protected bool is16kVRAMEnabled => IsBitSet(registers[0x01], 7);
protected bool isBitM1Set => IsBitSet(registers[0x01], 4);
protected bool isBitM2Set => IsBitSet(registers[0x00], 1);
protected bool isBitM3Set => IsBitSet(registers[0x01], 3);
protected virtual bool isModeGraphics1 => !(isBitM1Set || isBitM2Set || isBitM3Set);
protected virtual bool isModeText => (isBitM1Set && !(isBitM2Set || isBitM3Set));
protected virtual bool isModeGraphics2 => (isBitM2Set && !(isBitM1Set || isBitM3Set));
protected virtual bool isModeMulticolor => (isBitM3Set && !(isBitM1Set || isBitM2Set));
protected bool isLargeSprites => IsBitSet(registers[0x01], 1);
protected bool isZoomedSprites => IsBitSet(registers[0x01], 0);
protected virtual ushort nametableBaseAddress => (ushort)((registers[0x02] & 0x0F) << 10);
protected virtual ushort spriteAttribTableBaseAddress => (ushort)((registers[0x05] & 0x7F) << 7);
protected virtual ushort spritePatternGenBaseAddress => (ushort)((registers[0x06] & 0x07) << 11);
protected byte backgroundColor => (byte)(registers[0x07] & 0x0F);
protected byte textColor => (byte)((registers[0x07] >> 4) & 0x0F);
/* http://www.smspower.org/Development/Palette */
readonly byte[][] colorValuesBgr = new byte[][]
{
/* B G R */
new byte[] { 0x00, 0x00, 0x00 }, /* Transparent */
new byte[] { 0x00, 0x00, 0x00 }, /* Black */
new byte[] { 0x3B, 0xB7, 0x47 }, /* Medium green */
new byte[] { 0x6F, 0xCF, 0x7C }, /* Light green */
new byte[] { 0xFF, 0x4E, 0x5D }, /* Dark blue */
new byte[] { 0xFF, 0x72, 0x80 }, /* Light blue */
new byte[] { 0x47, 0x62, 0xB6 }, /* Dark red */
new byte[] { 0xED, 0xC8, 0x5D }, /* Cyan */
new byte[] { 0x48, 0x6B, 0xD7 }, /* Medium red */
new byte[] { 0x6C, 0x8F, 0xFB }, /* Light red */
new byte[] { 0x41, 0xCD, 0xC3 }, /* Dark yellow */
new byte[] { 0x76, 0xDA, 0xD3 }, /* Light yellow */
new byte[] { 0x2F, 0x9F, 0x3E }, /* Dark green */
new byte[] { 0xC7, 0x64, 0xB6 }, /* Magenta */
new byte[] { 0xCC, 0xCC, 0xCC }, /* Gray */
new byte[] { 0xFF, 0xFF, 0xFF } /* White */
};
protected const byte screenUsageEmpty = 0;
protected const byte screenUsageSprite = (1 << 0);
protected const byte screenUsageBackground = (1 << 1);
[StateRequired]
protected byte[] screenUsage;
[StateRequired]
protected int cycleCount;
protected byte[] outputFramebuffer;
protected int clockCyclesPerLine;
public (string Name, string Description)[] RuntimeOptions => new (string name, string description)[]
{
(layerBackgroundOptionName, "Background"),
(layerSpritesOptionName, "Sprites"),
(layerBordersOptionName, "Borders"),
};
protected bool layerBackgroundForceEnable, layerSpritesForceEnable, layerBordersForceEnable;
public TMS99xxA()
{
registers = new byte[0x08];
vram = new byte[0x4000];
spriteBuffer = new (int Number, int Y, int X, int Pattern, int Attribute)[NumActiveScanlines][];
for (int i = 0; i < spriteBuffer.Length; i++) spriteBuffer[i] = new (int Number, int Y, int X, int Pattern, int Attribute)[NumSpritesPerLine];
layerBackgroundForceEnable = true;
layerSpritesForceEnable = true;
layerBordersForceEnable = true;
}
public object GetRuntimeOption(string name)
{
switch (name)
{
case layerBackgroundOptionName: return layerBackgroundForceEnable;
case layerSpritesOptionName: return layerSpritesForceEnable;
case layerBordersOptionName: return layerBordersForceEnable;
default: return null;
}
}
public void SetRuntimeOption(string name, object value)
{
switch (name)
{
case layerBackgroundOptionName: layerBackgroundForceEnable = (bool)value; break;
case layerSpritesOptionName: layerSpritesForceEnable = (bool)value; break;
case layerBordersOptionName: layerBordersForceEnable = (bool)value; break;
}
}
public virtual void Startup()
{
Reset();
Debug.Assert(clockRate != 0.0, "Clock rate is zero", "{0} clock rate is not configured", GetType().FullName);
Debug.Assert(refreshRate != 0.0, "Refresh rate is zero", "{0} refresh rate is not configured", GetType().FullName);
}
public virtual void Shutdown()
{
//
}
public virtual void Reset()
{
for (int i = 0; i < registers.Length; i++) registers[i] = 0;
for (int i = 0; i < vram.Length; i++) vram[i] = 0;
for (int i = 0; i < spriteBuffer.Length; i++)
for (int j = 0; j < spriteBuffer[i].Length; j++)
spriteBuffer[i][j] = (-1, 0, 0, 0, 0);
isSecondControlWrite = false;
controlWord = 0x0000;
readBuffer = 0;
statusFlags = StatusFlags.None;
// TODO/FIXME: begin on random scanline (i.e. Vcounter for SMS/GG) on reset, http://www.smspower.org/forums/post62735#62735
currentScanline = new Random().Next(scanlineTopBorder, numVisibleScanlines);
ClearScreenUsage();
cycleCount = 0;
}
public void SetClockRate(double clock)
{
clockRate = clock;
ReconfigureTimings();
}
public void SetRefreshRate(double refresh)
{
refreshRate = refresh;
isPalChip = (refreshRate <= 50.0);
ReconfigureTimings();
}
public virtual void SetRevision(int rev)
{
Debug.Assert(rev == 0, "Invalid revision", "{0} revision is invalid; only rev 0 is valid", GetType().FullName);
}
protected virtual void ReconfigureTimings()
{
/* Calculate cycles/line */
clockCyclesPerLine = (int)Math.Round((clockRate / refreshRate) / numTotalScanlines);
/* Create arrays */
screenUsage = new byte[numVisiblePixels * numVisibleScanlines];
outputFramebuffer = new byte[(numVisiblePixels * numVisibleScanlines) * 4];
/* Scanline parameters */
if (!isPalChip)
{
topBorderSize = 27;
verticalActiveDisplaySize = 192;
bottomBorderSize = 24;
}
else
{
topBorderSize = 54;
verticalActiveDisplaySize = 192;
bottomBorderSize = 48;
}
scanlineTopBorder = 0;
scanlineActiveDisplay = (scanlineTopBorder + topBorderSize);
scanlineBottomBorder = (scanlineActiveDisplay + verticalActiveDisplaySize);
numVisibleScanlines = (topBorderSize + verticalActiveDisplaySize + bottomBorderSize);
/* Pixel parameters */
leftBorderSize = 13;
horizontalActiveDisplaySize = 256;
rightBorderSize = 15;
pixelLeftBorder = 0;
pixelActiveDisplay = (pixelLeftBorder + leftBorderSize);
pixelRightBorder = (pixelActiveDisplay + horizontalActiveDisplaySize);
numVisiblePixels = (leftBorderSize + horizontalActiveDisplaySize + rightBorderSize);
OnSizeScreen(new SizeScreenEventArgs(numVisiblePixels, numVisibleScanlines));
}
public virtual void Step(int clockCyclesInStep)
{
InterruptLine = ((isFrameInterruptEnabled && isFrameInterruptPending) ? InterruptState.Assert : InterruptState.Clear);
cycleCount += clockCyclesInStep;
if (cycleCount >= clockCyclesPerLine)
{
OnEndOfScanline(EventArgs.Empty);
CheckSpriteOverflow(currentScanline);
RenderLine(currentScanline);
if (currentScanline == (scanlineBottomBorder + 1))
isFrameInterruptPending = true;
currentScanline++;
if (currentScanline == numTotalScanlines)
{
currentScanline = 0;
ClearScreenUsage();
PrepareRenderScreen();
}
ParseSpriteTable(currentScanline);
cycleCount -= clockCyclesPerLine;
if (cycleCount <= -clockCyclesPerLine) cycleCount = 0;
}
}
protected virtual void PrepareRenderScreen()
{
OnRenderScreen(new RenderScreenEventArgs(numVisiblePixels, numVisibleScanlines, outputFramebuffer.Clone() as byte[]));
}
protected virtual void ClearScreenUsage()
{
for (int i = 0; i < screenUsage.Length; i++)
screenUsage[i] = screenUsageEmpty;
}
protected virtual void RenderLine(int y)
{
if (y >= scanlineTopBorder && y < scanlineActiveDisplay)
{
if (layerBordersForceEnable) SetLine(y, backgroundColor);
else SetLine(y, 0x00, 0x00, 0x00);
}
else if (y >= scanlineActiveDisplay && y < scanlineBottomBorder)
{
if (layerBackgroundForceEnable)
{
if (isModeGraphics1)
RenderLineGraphics1Background(y);
else if (isModeGraphics2)
RenderLineGraphics2Background(y);
else if (isModeMulticolor)
RenderLineMulticolorBackground(y);
else if (isModeText)
RenderLineTextBackground(y);
}
else
SetLine(y, 0x00, 0x00, 0x00);
if (layerSpritesForceEnable)
{
if (!isModeText)
RenderLineSprites(y);
}
RenderBorders(y);
}
else if (y >= scanlineBottomBorder && y < numVisibleScanlines)
{
if (layerBordersForceEnable) SetLine(y, backgroundColor);
else SetLine(y, 0x00, 0x00, 0x00);
}
}
protected virtual void RenderBorders(int y)
{
for (int x = pixelLeftBorder; x < pixelActiveDisplay; x++)
{
if (layerBordersForceEnable) SetPixel(y, x, backgroundColor);
else SetPixel(y, x, 0x00, 0x00, 0x00);
}
for (int x = pixelRightBorder; x < numVisiblePixels; x++)
{
if (layerBordersForceEnable) SetPixel(y, x, backgroundColor);
else SetPixel(y, x, 0x00, 0x00, 0x00);
}
}
protected void SetLine(int y, ushort colorValue)
{
for (int x = 0; x < numVisiblePixels; x++)
SetPixel(y, x, colorValue);
}
protected virtual void SetLine(int y, byte b, byte g, byte r)
{
for (int x = 0; x < numVisiblePixels; x++)
SetPixel(y, x, b, g, r);
}
protected void SetPixel(int y, int x, ushort colorValue)
{
WriteColorToFramebuffer(colorValue, ((y * numVisiblePixels) + (x % numVisiblePixels)) * 4);
}
protected virtual void SetPixel(int y, int x, byte b, byte g, byte r)
{
WriteColorToFramebuffer(b, g, r, ((y * numVisiblePixels) + (x % numVisiblePixels)) * 4);
}
protected byte GetScreenUsageFlag(int y, int x)
{
return screenUsage[(y * numVisiblePixels) + (x % numVisiblePixels)];
}
protected bool IsScreenUsageFlagSet(int y, int x, byte flag)
{
return ((GetScreenUsageFlag(y, x) & flag) == flag);
}
protected void SetScreenUsageFlag(int y, int x, byte flag)
{
screenUsage[(y * numVisiblePixels) + (x % numVisiblePixels)] |= flag;
}
protected void ClearScreenUsageFlag(int y, int x, byte flag)
{
screenUsage[(y * numVisiblePixels) + (x % numVisiblePixels)] &= (byte)~flag;
}
protected void RenderLineGraphics1Background(int y)
{
/* Determine coordinates in active display */
int activeDisplayY = (y - scanlineActiveDisplay);
int numColumns = 32;
ushort patternGeneratorBaseAddress = (ushort)((registers[0x04] & 0x07) << 11);
ushort colorTableBaseAddress = (ushort)(registers[0x03] << 6);
for (int column = 0; column < numColumns; column++)
{
/* Calculate nametable address, fetch character number */
ushort nametableAddress = (ushort)(nametableBaseAddress + ((activeDisplayY / 8) * numColumns) + column);
byte characterNumber = ReadVram(nametableAddress);
/* Fetch pixel and color data for current pixel line (1 byte, 8 pixels) */
byte pixelLineData = ReadVram((ushort)(patternGeneratorBaseAddress + (characterNumber * 8) + (activeDisplayY % 8)));
byte pixelLineColor = ReadVram((ushort)(colorTableBaseAddress + (characterNumber / 8)));
/* Extract background and foreground color indices */
byte[] colorIndicesBackgroundForeground = new byte[2];
colorIndicesBackgroundForeground[0] = (byte)(pixelLineColor & 0x0F);
colorIndicesBackgroundForeground[1] = (byte)(pixelLineColor >> 4);
for (int pixel = 0; pixel < 8; pixel++)
{
/* Fetch color index for current pixel (bit clear means background, bit set means foreground color) */
byte c = colorIndicesBackgroundForeground[((pixelLineData >> (7 - pixel)) & 0x01)];
/* Color index 0 is transparent, use background color */
if (c == 0 || isDisplayBlanked) c = backgroundColor;
/* Record screen usage, write to framebuffer */
int x = pixelActiveDisplay + (column * 8) + pixel;
if (GetScreenUsageFlag(y, x) == screenUsageEmpty)
{
SetPixel(y, x, c);
SetScreenUsageFlag(y, x, screenUsageBackground);
}
}
}
}
protected void RenderLineGraphics2Background(int y)
{
/* Determine coordinates in active display */
int activeDisplayY = (y - scanlineActiveDisplay);
int numColumns = (NumActivePixelsPerScanline / 8);
/* Calculate some base addresses */
ushort patternGeneratorBaseAddress = (ushort)((registers[0x04] & 0x04) << 11);
ushort colorTableBaseAddress = (ushort)((registers[0x03] & 0x80) << 6);
for (int column = 0; column < numColumns; column++)
{
/* Calculate nametable address */
ushort nametableAddress = (ushort)(nametableBaseAddress + ((activeDisplayY / 8) * numColumns) + column);
/* Calculate character number and masks */
ushort characterNumber = (ushort)(((activeDisplayY / 64) << 8) | ReadVram(nametableAddress));
ushort characterNumberDataMask = (ushort)(((registers[0x04] & 0x03) << 8) | 0xFF);
ushort characterNumberColorMask = (ushort)(((registers[0x03] & 0x7F) << 3) | 0x07);
/* Fetch pixel and color data for current pixel line (1 byte, 8 pixels) */
byte pixelLineData = ReadVram((ushort)(patternGeneratorBaseAddress + ((characterNumber & characterNumberDataMask) * 8) + (activeDisplayY % 8)));
byte pixelLineColor = ReadVram((ushort)(colorTableBaseAddress + ((characterNumber & characterNumberColorMask) * 8) + (activeDisplayY % 8)));
/* Extract background and foreground color indices */
byte[] colorIndicesBackgroundForeground = new byte[2];
colorIndicesBackgroundForeground[0] = (byte)(pixelLineColor & 0x0F);
colorIndicesBackgroundForeground[1] = (byte)(pixelLineColor >> 4);
for (int pixel = 0; pixel < 8; pixel++)
{
/* Fetch color index for current pixel (bit clear means background, bit set means foreground color) */
byte c = colorIndicesBackgroundForeground[((pixelLineData >> (7 - pixel)) & 0x01)];
/* Color index 0 is transparent, use background color */
if (c == 0 || isDisplayBlanked) c = backgroundColor;
/* Record screen usage, write to framebuffer */
int x = pixelActiveDisplay + (column * 8) + pixel;
if (GetScreenUsageFlag(y, x) == screenUsageEmpty)
{
SetPixel(y, x, c);
SetScreenUsageFlag(y, x, screenUsageBackground);
}
}
}
}
protected void RenderLineMulticolorBackground(int y)
{
// TODO: check accuracy w/ games besides Smurfs Paint & Play Workshop (if there are any)
/* Determine coordinates in active display */
int activeDisplayY = (y - scanlineActiveDisplay);
int numColumns = 32;
int row = (activeDisplayY / 8);
ushort patternGeneratorBaseAddress = (ushort)((registers[0x04] & 0x07) << 11);
for (int column = 0; column < numColumns; column++)
{
/* Calculate nametable address, fetch character number */
ushort nametableAddress = (ushort)(nametableBaseAddress + (row * numColumns) + column);
byte characterNumber = ReadVram(nametableAddress);
/* Calculate pattern generator address, fetch pattern data */
ushort patternGeneratorAddress = (ushort)(patternGeneratorBaseAddress + (characterNumber * 8) + ((row & 0x03) * 2) + ((activeDisplayY / 4) % 2));
byte patternData = ReadVram(patternGeneratorAddress);
for (int block = 0; block < 8; block += 4)
{
/* Fetch color index for current 4x4 block */
byte c = (byte)((patternData >> (4 - block)) & 0x0F);
/* Color index 0 is transparent, use background color */
if (c == 0 || isDisplayBlanked) c = backgroundColor;
for (int pixel = 0; pixel < 4; pixel++)
{
/* Record screen usage, write to framebuffer */
int x = pixelActiveDisplay + (column * 8) + (block + pixel);
if (GetScreenUsageFlag(y, x) == screenUsageEmpty)
{
SetPixel(y, x, c);
SetScreenUsageFlag(y, x, screenUsageBackground);
}
}
}
}
}
protected void RenderLineTextBackground(int y)
{
/* Determine coordinates in active display */
int activeDisplayY = (y - scanlineActiveDisplay);
int numColumns = 40;
ushort patternGeneratorBaseAddress = (ushort)((registers[0x04] & 0x07) << 11);
/* Get background and text color indices */
byte[] colorIndicesBackgroundForeground = new byte[2];
colorIndicesBackgroundForeground[0] = backgroundColor;
colorIndicesBackgroundForeground[1] = textColor;
/* Draw left and right 8px borders */
for (int pixel = 0; pixel < 8; pixel++)
{
int x = pixelActiveDisplay + pixel;
SetPixel(y, x + 0, backgroundColor);
SetPixel(y, x + 8 + (numColumns * 6), backgroundColor);
}
/* Draw text columns */
for (int column = 0; column < numColumns; column++)
{
/* Calculate nametable address, fetch character number */
ushort nametableAddress = (ushort)(nametableBaseAddress + ((activeDisplayY / 8) * numColumns) + column);
byte characterNumber = ReadVram(nametableAddress);
/* Fetch pixel data for current pixel line (1 byte, 8 pixels) */
byte pixelLineData = ReadVram((ushort)(patternGeneratorBaseAddress + (characterNumber * 8) + (activeDisplayY % 8)));
for (int pixel = 0; pixel < 6; pixel++)
{
/* Fetch color index for current pixel (bit clear means background, bit set means text color) */
byte c = (isDisplayBlanked ? backgroundColor : colorIndicesBackgroundForeground[((pixelLineData >> (7 - pixel)) & 0x01)]);
/* Record screen usage, write to framebuffer */
int x = pixelActiveDisplay + 8 + (column * 6) + pixel;
if (GetScreenUsageFlag(y, x) == screenUsageEmpty)
{
SetPixel(y, x, c);
SetScreenUsageFlag(y, x, screenUsageBackground);
}
}
}
}
protected virtual void WriteSpriteNumberToStatus(int spriteNumber)
{
statusFlags &= (StatusFlags.FrameInterruptPending | StatusFlags.SpriteOverflow | StatusFlags.SpriteCollision);
statusFlags |= (StatusFlags)spriteNumber;
}
protected virtual void CheckSpriteOverflow(int y)
{
/* Ensure current scanline is within active display */
if (y >= scanlineActiveDisplay && y < scanlineBottomBorder)
{
int activeDisplayY = (y - scanlineActiveDisplay);
/* If last sprite in buffer is valid, sprite overflow occured */
int lastSpriteInBuffer = spriteBuffer[activeDisplayY][NumSpritesPerLine - 1].Number;
if (lastSpriteInBuffer != -1)
{
isSpriteOverflow = true;
/* Store sprite number in status register */
WriteSpriteNumberToStatus(lastSpriteInBuffer);
}
}
}
protected virtual void ParseSpriteTable(int y)
{
if (y < scanlineActiveDisplay || y >= scanlineBottomBorder) return;
/* Determine coordinates in active display */
int activeDisplayY = (y - scanlineActiveDisplay);
/* Clear sprite list for current line */
for (int i = 0; i < spriteBuffer[activeDisplayY].Length; i++) spriteBuffer[activeDisplayY][i] = (-1, 0, 0, 0, 0);
/* Determine sprite size & get zoomed sprites adjustment */
int zoomShift = (isZoomedSprites ? 1 : 0);
int spriteHeight = ((isLargeSprites ? 16 : 8) << zoomShift);
int numValidSprites = 0;
for (int sprite = 0; sprite < NumSprites; sprite++)
{
int yCoordinate = ReadVram((ushort)(spriteAttribTableBaseAddress + (sprite * 4)));
/* Ignore following if Y coord is 208 */
if (yCoordinate == 208)
{
/* Store first "illegal sprite" number in status register */
WriteSpriteNumberToStatus(sprite);
return;
}
/* Modify Y coord as needed */
yCoordinate++;
if (yCoordinate > NumActiveScanlines + 32) yCoordinate -= 256;
/* Ignore this sprite if on incorrect lines */
if (activeDisplayY < yCoordinate || activeDisplayY >= (yCoordinate + spriteHeight)) continue;
/* Check if maximum number of sprites per line is reached */
numValidSprites++;
if (numValidSprites > NumSpritesPerLine) return;
/* Mark sprite for rendering */
int xCoordinate = ReadVram((ushort)(spriteAttribTableBaseAddress + (sprite * 4) + 1));
int patternNumber = ReadVram((ushort)(spriteAttribTableBaseAddress + (sprite * 4) + 2));
int attributes = ReadVram((ushort)(spriteAttribTableBaseAddress + (sprite * 4) + 3));
spriteBuffer[activeDisplayY][numValidSprites - 1] = (sprite, yCoordinate, xCoordinate, patternNumber, attributes);
}
/* Because we didn't bow out before already, store total number of sprites in status register */
WriteSpriteNumberToStatus(NumSprites - 1);
}
protected void RenderLineSprites(int y)
{
if (y < scanlineActiveDisplay || y >= scanlineBottomBorder) return;
/* Determine coordinates in active display */
int activeDisplayY = (y - scanlineActiveDisplay);
/* Determine sprite size & get zoomed sprites adjustment */
int spriteSize = (isLargeSprites ? 16 : 8);
int zoomShift = (isZoomedSprites ? 1 : 0);
int numSpritePixels = (spriteSize << zoomShift);
foreach (var sprite in spriteBuffer[activeDisplayY])
{
if (sprite.Number == -1) continue;
if (!isDisplayBlanked)
{
int yCoordinate = sprite.Y;
int xCoordinate = sprite.X;
int patternNumber = sprite.Pattern;
int attributes = sprite.Attribute;
/* Fetch sprite information, extract attributes */
bool earlyClock = ((attributes & 0x80) == 0x80);
int spriteColor = (attributes & 0x0F);
/* Adjust according to registers/attributes */
if (earlyClock) xCoordinate -= 32;
if (isLargeSprites) patternNumber &= 0xFC;
for (int pixel = 0; pixel < numSpritePixels; pixel++)
{
/* Check if sprite is outside active display, else continue to next sprite */
if ((xCoordinate + pixel) < 0 || (xCoordinate + pixel) >= NumActivePixelsPerScanline) continue;
/* Determine coordinate inside sprite */
int inSpriteXCoord = (pixel >> zoomShift) % spriteSize;
int inSpriteYCoord = ((activeDisplayY - yCoordinate) >> zoomShift) % spriteSize;
/* Calculate address and fetch pixel data */
ushort spritePatternAddress = spritePatternGenBaseAddress;
spritePatternAddress += (ushort)(patternNumber << 3);
spritePatternAddress += (ushort)inSpriteYCoord;
if (inSpriteXCoord >= 8) spritePatternAddress += 16;
byte pixelLineData = ReadVram(spritePatternAddress);
/* Check if pixel from pattern needs to be drawn, else continue to next sprite */
if (((pixelLineData >> (7 - (inSpriteXCoord % 8))) & 0x01) == 0x00) continue;
int x = pixelActiveDisplay + (xCoordinate + pixel);
if (IsScreenUsageFlagSet(y, x, screenUsageSprite))
{
/* If sprite was already at this location, set sprite collision flag */
isSpriteCollision = true;
}
else
{
/* If color isn't transparent, draw pixel to framebuffer */
if (spriteColor != 0)
SetPixel(y, x, (ushort)spriteColor);
}
/* Note that there is a sprite here regardless */
SetScreenUsageFlag(y, x, screenUsageSprite);
}
}
}
}
protected virtual void WriteColorToFramebuffer(byte b, byte g, byte r, int address)
{
outputFramebuffer[address + 0] = b;
outputFramebuffer[address + 1] = g;
outputFramebuffer[address + 2] = r;
outputFramebuffer[address + 3] = 0xFF;
}
protected virtual void WriteColorToFramebuffer(ushort colorValue, int address)
{
outputFramebuffer[address + 0] = colorValuesBgr[colorValue & 0x0F][0];
outputFramebuffer[address + 1] = colorValuesBgr[colorValue & 0x0F][1];
outputFramebuffer[address + 2] = colorValuesBgr[colorValue & 0x0F][2];
outputFramebuffer[address + 3] = 0xFF;
}
protected virtual byte ReadVram(ushort address)
{
if (is16kVRAMEnabled)
return vram[address & vramMask16k];
else
return vram[address & vramMask4k];
}
protected virtual void WriteVram(ushort address, byte value)
{
if (is16kVRAMEnabled)
vram[address & vramMask16k] = value;
else
vram[address & vramMask4k] = value;
}
protected virtual byte ReadDataPort()
{
isSecondControlWrite = false;
statusFlags = StatusFlags.None;
byte data = readBuffer;
readBuffer = ReadVram(addressRegister);
addressRegister++;
return data;
}
protected virtual byte ReadControlPort()
{
byte statusCurrent = (byte)statusFlags;
statusFlags = StatusFlags.None;
isSecondControlWrite = false;
InterruptLine = InterruptState.Clear;
return statusCurrent;
}
public virtual byte ReadPort(byte port)
{
if ((port & 0x01) == 0x00)
return ReadDataPort();
else
return ReadControlPort();
}
protected virtual void WriteDataPort(byte value)
{
isSecondControlWrite = false;
readBuffer = value;
WriteVram(addressRegister, value);
addressRegister++;
}
protected virtual void WriteControlPort(byte value)
{
if (!isSecondControlWrite)
controlWord = (ushort)((controlWord & 0xFF00) | (value << 0));
else
{
controlWord = (ushort)((controlWord & 0x00FF) | (value << 8));
switch (codeRegister)
{
case 0x00: readBuffer = ReadVram(addressRegister); addressRegister++; break;
case 0x01: break;
case 0x02: WriteRegister((byte)((controlWord >> 8) & 0x0F), (byte)(controlWord & 0x00FF)); break;
case 0x03: break;
}
}
isSecondControlWrite = !isSecondControlWrite;
}
public virtual void WritePort(byte port, byte value)
{
if ((port & 0x01) == 0x00)
WriteDataPort(value);
else
WriteControlPort(value);
}
protected virtual void WriteRegister(byte register, byte value)
{
// TODO: confirm register mirroring
registers[register & 0x07] = value;
}
}
}