using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Diagnostics; using Essgee.Emulation.CPU; using Essgee.Exceptions; using Essgee.EventArguments; using Essgee.Utilities; using static Essgee.Emulation.CPU.SM83; using static Essgee.Emulation.Utilities; namespace Essgee.Emulation.Video.Nintendo { public class CGBVideo : DMGVideo { protected override int numSkippedFramesLcdOn => 1; // FF4F - VBK byte vramBank; // FF51 - HDMA1 byte dmaSourceHi; // FF52 - HDMA2 byte dmaSourceLo; // FF53 - HDMA3 byte dmaDestinationHi; // FF54 - HDMA4 byte dmaDestinationLo; // FF55 - HDMA5 byte dmaTransferBlockLength; bool dmaTransferIsHDMA; // FF68 - BCPS byte bgPaletteIndex; bool bgPaletteAutoIncrement; // FF69 - BCPD // FF6A - OCPS byte objPaletteIndex; bool objPaletteAutoIncrement; // FF6B - OCPD // byte[] bgPaletteData, objPaletteData; ushort dmaSourceAddress, dmaDestinationAddress; int dmaTransferByteLength; bool hdmaIsActive; byte hdmaBytesLeft; public int GDMAWaitCycles { get; set; } protected const byte screenUsageBackgroundHighPriority = (1 << 3); public CGBVideo(MemoryReadDelegate memoryRead, RequestInterruptDelegate requestInterrupt) : base(memoryRead, requestInterrupt) { vram = new byte[2, 0x2000]; // bgPaletteData = new byte[64]; objPaletteData = new byte[64]; } public override void Reset() { base.Reset(); vramBank = 0; dmaTransferBlockLength = 0; dmaTransferIsHDMA = false; bgPaletteAutoIncrement = true; objPaletteAutoIncrement = true; for (var i = 0; i < bgPaletteData.Length; i += 2) { bgPaletteData[i + 1] = 0x7F; bgPaletteData[i + 0] = 0xFF; } for (var i = 0; i < objPaletteData.Length; i += 2) { objPaletteData[i + 1] = 0x7F; objPaletteData[i + 0] = 0xFF; } dmaSourceAddress = dmaDestinationAddress = 0; dmaTransferByteLength = 0; hdmaIsActive = false; hdmaBytesLeft = 0; } // protected override void StepHBlank() { /* Check and perform HDMA */ if (hdmaIsActive && dmaTransferIsHDMA) { if (hdmaBytesLeft > 0 && (cycleCount % 2) == 0) { WriteVram(dmaDestinationAddress, memoryReadDelegate(dmaSourceAddress)); dmaDestinationAddress++; dmaSourceAddress++; dmaDestinationAddress &= 0x1FFF; dmaTransferByteLength--; hdmaBytesLeft--; if (dmaTransferByteLength == 0) hdmaIsActive = false; UpdateDMAStatus(); } } /* Increment cycle count & check for next LCD mode */ cycleCount++; if (cycleCount == clockCyclesPerLine) { if (hdmaIsActive) hdmaBytesLeft = (byte)Math.Min(0x10, 0x10 - (dmaTransferByteLength % 16)); EndHBlank(); } } // protected override void RenderPixel(int y, int x) { if (x < 0 || x >= displayActiveWidth || y < 0 || y >= displayActiveHeight) return; if (skipFrames > 0) { SetPixel(y, x, 0xFF, 0xFF, 0xFF); return; } screenUsageFlags[x, y] = screenUsageEmpty; RenderBackground(y, x); if (wndEnable) RenderWindow(y, x); if (objEnable) RenderSprites(y, x); } protected override void RenderBackground(int y, int x) { // Get base addresses var tileBase = (ushort)(bgWndTileSelect ? 0x0000 : 0x0800); var mapBase = (ushort)(bgMapSelect ? 0x1C00 : 0x1800); // Calculate tilemap address & get tile var yTransformed = (byte)(scrollY + y); var xTransformed = (byte)(scrollX + x); var mapAddress = mapBase + ((yTransformed >> 3) << 5) + (xTransformed >> 3); var tileNumber = vram[0, mapAddress]; if (!bgWndTileSelect) tileNumber = (byte)(tileNumber ^ 0x80); // Get & extract tile attributes var tileAttribs = vram[1, mapAddress]; var tileBgPalette = tileAttribs & 0b111; var tileVramBank = (tileAttribs >> 3) & 0b1; var tileHorizontalFlip = ((tileAttribs >> 5) & 0b1) == 0b1; var tileVerticalFlip = ((tileAttribs >> 6) & 0b1) == 0b1; var tileBgHasPriority = ((tileAttribs >> 7) & 0b1) == 0b1; // Calculate tile address & get pixel color index var xShift = tileHorizontalFlip ? (xTransformed % 8) : (7 - (xTransformed % 8)); var yShift = tileVerticalFlip ? (7 - (yTransformed & 7)) : (yTransformed & 7); var tileAddress = tileBase + (tileNumber << 4) + (yShift << 1); var ba = (vram[tileVramBank, tileAddress + 0] >> xShift) & 0b1; var bb = (vram[tileVramBank, tileAddress + 1] >> xShift) & 0b1; var c = (byte)((bb << 1) | ba); // If color is not 0, note that a BG pixel (normal or high-priority) exists here if (c != 0) screenUsageFlags[x, y] |= tileBgHasPriority ? screenUsageBackgroundHighPriority : screenUsageBackground; // Calculate color address in palette & draw pixel if (layerBackgroundForceEnable) { var paletteAddress = (tileBgPalette << 3) + ((c & 0b11) << 1); SetPixel(y, x, (ushort)((bgPaletteData[paletteAddress + 1] << 8) | bgPaletteData[paletteAddress + 0])); } else SetPixel(y, x, (ushort)((bgPaletteData[1] << 8) | bgPaletteData[0])); } protected override void RenderWindow(int y, int x) { // Check if current coords are inside window if (y < windowY) return; if (x < (windowX - 7)) return; // Get base addresses var tileBase = (ushort)(bgWndTileSelect ? 0x0000 : 0x0800); var mapBase = (ushort)(wndMapSelect ? 0x1C00 : 0x1800); // Calculate tilemap address & get tile var yTransformed = (byte)(y - windowY); var xTransformed = (byte)((7 - windowX) + x); var mapAddress = mapBase + ((yTransformed >> 3) << 5) + (xTransformed >> 3); var tileNumber = vram[0, mapAddress]; if (!bgWndTileSelect) tileNumber = (byte)(tileNumber ^ 0x80); // Get & extract tile attributes var tileAttribs = vram[1, mapAddress]; var tileBgPalette = tileAttribs & 0b111; var tileVramBank = (tileAttribs >> 3) & 0b1; var tileHorizontalFlip = ((tileAttribs >> 5) & 0b1) == 0b1; var tileVerticalFlip = ((tileAttribs >> 6) & 0b1) == 0b1; var tileBgHasPriority = ((tileAttribs >> 7) & 0b1) == 0b1; // Calculate tile address & get pixel color index var xShift = tileHorizontalFlip ? (xTransformed % 8) : (7 - (xTransformed % 8)); var yShift = tileVerticalFlip ? (7 - (yTransformed & 7)) : (yTransformed & 7); var tileAddress = tileBase + (tileNumber << 4) + (yShift << 1); var ba = (vram[tileVramBank, tileAddress + 0] >> xShift) & 0b1; var bb = (vram[tileVramBank, tileAddress + 1] >> xShift) & 0b1; var c = (byte)((bb << 1) | ba); // If color is not 0, note that a Window pixel (normal or high-priority) exists here if (c != 0) screenUsageFlags[x, y] |= tileBgHasPriority ? screenUsageBackgroundHighPriority : screenUsageWindow; // TODO correct? // Calculate color address in palette & draw pixel if (layerWindowForceEnable) { var paletteAddress = (tileBgPalette << 3) + ((c & 0b11) << 1); SetPixel(y, x, (ushort)((bgPaletteData[paletteAddress + 1] << 8) | bgPaletteData[paletteAddress + 0])); } else SetPixel(y, x, (ushort)((bgPaletteData[1] << 8) | bgPaletteData[0])); } protected override void RenderSprites(int y, int x) { var objHeight = objSize ? 16 : 8; // Iterate over sprite on line backwards for (var s = numSpritesOnLine - 1; s >= 0; s--) { var i = spritesOnLine[s]; // Get sprite Y coord & if sprite is not on current scanline, continue to next slot var objY = (short)(oam[(i * 4) + 0] - 16); if (y < objY || y >= (objY + objHeight)) continue; // Get sprite X coord, tile number & attributes var objX = (byte)(oam[(i * 4) + 1] - 8); var objTileNumber = oam[(i * 4) + 2]; var objAttributes = oam[(i * 4) + 3]; // Extract attributes var objFlipY = ((objAttributes >> 6) & 0b1) == 0b1; var objFlipX = ((objAttributes >> 5) & 0b1) == 0b1; var objVramBank = (objAttributes >> 3) & 0b1; var objPalNumber = (objAttributes >> 0) & 0b111; // Iterate over pixels for (var px = 0; px < 8; px++) { // If sprite pixel X coord does not equal current rendering X coord, continue to next pixel if (x != (byte)(objX + px)) continue; // Calculate tile address var xShift = objFlipX ? (px % 8) : (7 - (px % 8)); var yShift = objFlipY ? (7 - ((y - objY) % 8)) : ((y - objY) % 8); if (objSize) { objTileNumber &= 0xFE; if ((objFlipY && y < (objY + 8)) || (!objFlipY && y >= (objY + 8))) objTileNumber |= 0x01; } var tileAddress = (objTileNumber << 4) + (yShift << 1); // Get palette & bitplanes var ba = (vram[objVramBank, tileAddress + 0] >> xShift) & 0b1; var bb = (vram[objVramBank, tileAddress + 1] >> xShift) & 0b1; // Combine to color index, continue drawing if color is not 0 var c = (byte)((bb << 1) | ba); if (c != 0) { // If sprite does not have priority i.e. if sprite should not be drawn, continue to next pixel if (!HasSpritePriority(y, x, i)) continue; screenUsageFlags[x, y] |= screenUsageSprite; screenUsageSpriteSlots[x, y] = (byte)i; screenUsageSpriteXCoords[x, y] = objX; // Calculate color address in palette & draw pixel if (layerSpritesForceEnable) { var paletteAddress = (objPalNumber << 3) + ((c & 0b11) << 1); SetPixel(y, x, (ushort)((objPaletteData[paletteAddress + 1] << 8) | objPaletteData[paletteAddress + 0])); } } } } } protected override bool HasSpritePriority(int y, int x, int objSlot) { // If BG and window have priority, check further conditions if (bgEnable) { // Get new sprite OBJ-to-BG priority attribute var objIsBehindBg = ((oam[(objSlot * 4) + 3] >> 7) & 0b1) == 0b1; // If high-priority BG pixel has already been drawn, -or- new sprite is shown behind BG/Window -and- a BG/Window pixel has already been drawn, new sprite does not have priority if (IsScreenUsageFlagSet(y, x, screenUsageBackgroundHighPriority) || (objIsBehindBg && (IsScreenUsageFlagSet(y, x, screenUsageBackground) || IsScreenUsageFlagSet(y, x, screenUsageWindow)))) return false; } // New sprite has priority return true; } protected void SetPixel(int y, int x, ushort c) { WriteColorToFramebuffer(c, ((y * displayActiveWidth) + (x % displayActiveWidth)) * 4); } private void WriteColorToFramebuffer(ushort c, int address) { RGBCGBtoBGRA8888(c, ref outputFramebuffer, address); } // private void RunGDMA() { while (--dmaTransferByteLength >= 0) { if ((dmaSourceAddress >= 0x0000 && dmaSourceAddress <= 0x7FFF) || (dmaSourceAddress >= 0xA000 && dmaSourceAddress <= 0xDFFF)) WriteVram(dmaDestinationAddress, memoryReadDelegate(dmaSourceAddress)); dmaDestinationAddress++; dmaSourceAddress++; dmaDestinationAddress &= 0x1FFF; } UpdateDMAStatus(); } private void UpdateDMAStatus() { dmaTransferBlockLength = (byte)((dmaTransferByteLength >> 4) & 0xFF); dmaSourceHi = (byte)((dmaSourceAddress >> 8) & 0x1F); dmaSourceLo = (byte)((dmaSourceAddress >> 0) & 0xF0); dmaDestinationHi = (byte)((dmaDestinationAddress >> 8) & 0xFF); dmaDestinationLo = (byte)((dmaDestinationAddress >> 0) & 0xF0); } // public override byte ReadVram(ushort address) { if (modeNumber != 3) return vram[vramBank, address & 0x1FFF]; else return 0xFF; } public override void WriteVram(ushort address, byte value) { if (modeNumber != 3) vram[vramBank, address & 0x1FFF] = value; } public override byte ReadPort(byte port) { switch (port) { case 0x4F: // VBK return (byte)( 0xFE | (vramBank & 0b1)); case 0x55: // HDMA5 return dmaTransferBlockLength; case 0x68: // BCPS return (byte)( 0x40 | (bgPaletteIndex & 0x3F) | (bgPaletteAutoIncrement ? (1 << 7) : 0)); case 0x69: // BCPD if (modeNumber != 3) return bgPaletteData[bgPaletteIndex]; else return 0xFF; case 0x6A: // OCPS return (byte)( 0x40 | (objPaletteIndex & 0x3F) | (objPaletteAutoIncrement ? (1 << 7) : 0)); case 0x6B: // OCPD if (modeNumber != 3) return objPaletteData[objPaletteIndex]; else return 0xFF; default: return base.ReadPort(port); } } public override void WritePort(byte port, byte value) { switch (port) { case 0x41: // STAT lycLyInterrupt = ((value >> 6) & 0b1) == 0b1; m2OamInterrupt = ((value >> 5) & 0b1) == 0b1; m1VBlankInterrupt = ((value >> 4) & 0b1) == 0b1; m0HBlankInterrupt = ((value >> 3) & 0b1) == 0b1; CheckAndRequestStatInterupt(); break; case 0x4F: // VBK vramBank = (byte)(value & 0b1); break; case 0x51: // HDMA1 dmaSourceHi = value; break; case 0x52: // HDMA2 dmaSourceLo = (byte)(value & 0xF0); break; case 0x53: // HDMA3 dmaDestinationHi = (byte)(value & 0x1F); break; case 0x54: // HDMA4 dmaDestinationLo = (byte)(value & 0xF0); break; case 0x55: // HDMA5 dmaTransferBlockLength = (byte)(value & 0x7F); dmaTransferIsHDMA = ((value >> 7) & 0b1) == 0b1; // Check for HDMA cancellation if (!dmaTransferIsHDMA && hdmaIsActive) hdmaIsActive = false; else { // Calculate DMA addresses & length dmaTransferByteLength = (dmaTransferBlockLength + 1) << 4; dmaSourceAddress = (ushort)((dmaSourceHi << 8 | dmaSourceLo) & 0xFFF0); dmaDestinationAddress = (ushort)((dmaDestinationHi << 8 | dmaDestinationLo) & 0x1FF0); // Run General-Purpose DMA if (!dmaTransferIsHDMA) { GDMAWaitCycles = 8 * (dmaTransferBlockLength + 1); RunGDMA(); } else hdmaIsActive = true; } break; case 0x68: // BCPS bgPaletteIndex = (byte)(value & 0x3F); bgPaletteAutoIncrement = ((value >> 7) & 0b1) == 0b1; break; case 0x69: // BCPD if (modeNumber != 3) bgPaletteData[bgPaletteIndex] = value; if (bgPaletteAutoIncrement) { bgPaletteIndex++; bgPaletteIndex &= 0x3F; } break; case 0x6A: // OCPS objPaletteIndex = (byte)(value & 0x3F); objPaletteAutoIncrement = ((value >> 7) & 0b1) == 0b1; break; case 0x6B: // OCPD if (modeNumber != 3) objPaletteData[objPaletteIndex] = value; if (objPaletteAutoIncrement) { objPaletteIndex++; objPaletteIndex &= 0x3F; } break; default: base.WritePort(port, value); break; } } } }