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 SizeScreen; public virtual void OnSizeScreen(SizeScreenEventArgs e) { SizeScreen?.Invoke(this, e); } public virtual event EventHandler RenderScreen; public virtual void OnRenderScreen(RenderScreenEventArgs e) { RenderScreen?.Invoke(this, e); } public virtual event EventHandler 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; } } }