using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Diagnostics; using Essgee.Exceptions; using Essgee.EventArguments; using Essgee.Utilities; using static Essgee.Emulation.CPU.SM83; namespace Essgee.Emulation.Video.Nintendo { public class DMGVideo : IVideo { protected const int displayActiveWidth = 160; protected const int displayActiveHeight = 144; protected const int numDisplayPixels = displayActiveWidth * displayActiveHeight; protected const int displayTotalHeight = 154; protected const int numOamSlots = 40; protected const int maxSpritesPerLine = 10; protected const int mode2Boundary = 80; protected const int mode3Boundary = mode2Boundary + 168; protected const string layerBackgroundOptionName = "GraphicsLayersShowBackground"; protected const string layerWindowOptionName = "GraphicsLayersShowWindow"; protected const string layerSpritesOptionName = "GraphicsLayersShowSprites"; protected virtual int numSkippedFramesLcdOn => 4; protected Action[] modeFunctions; protected readonly MemoryReadDelegate memoryReadDelegate; protected readonly RequestInterruptDelegate requestInterruptDelegate; public virtual (int X, int Y, int Width, int Height) Viewport => (0, 0, displayActiveWidth, displayActiveHeight); 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 double clockRate, refreshRate; // [StateRequired] protected byte[,] vram; [StateRequired] protected byte[] oam; // FF40 - LCDC protected bool lcdEnable, wndMapSelect, wndEnable, bgWndTileSelect, bgMapSelect, objSize, objEnable, bgEnable; // FF41 - STAT protected bool lycLyInterrupt, m2OamInterrupt, m1VBlankInterrupt, m0HBlankInterrupt, coincidenceFlag; protected byte modeNumber; // FF42 - SCY protected byte scrollY; // FF43 - SCX protected byte scrollX; // FF44 - LY protected byte ly; // FF45 - LYC protected byte lyCompare; // FF46 - DMA protected byte oamDmaStart; // FF47 - BGP protected byte bgPalette; // FF48 - OBP0 protected byte obPalette0; // FF49 - OBP1 protected byte obPalette1; // FF4A - WY protected byte windowY; // FF4B - WX protected byte windowX; // protected int numSpritesOnLine, skipFrames; protected bool statIrqSignal, vBlankReady; protected int[] spritesOnLine; readonly byte[][] colorValuesBgr = new byte[][] { /* B G R */ new byte[] { 0xF8, 0xF8, 0xF8 }, /* White */ new byte[] { 0x9B, 0x9B, 0x9B }, /* Light gray */ new byte[] { 0x3E, 0x3E, 0x3E }, /* Dark gray */ new byte[] { 0x1F, 0x1F, 0x1F }, /* Black */ }; protected const byte screenUsageEmpty = 0; protected const byte screenUsageBackground = 1 << 0; protected const byte screenUsageWindow = 1 << 1; protected const byte screenUsageSprite = 1 << 2; protected byte[,] screenUsageFlags, screenUsageSpriteXCoords, screenUsageSpriteSlots; protected int cycleCount, cycleDotPause, currentScanline; protected byte[] outputFramebuffer; protected int clockCyclesPerLine; public bool IsDoubleSpeed { get; set; } // public (string Name, string Description)[] RuntimeOptions => new (string name, string description)[] { (layerBackgroundOptionName, "Background"), (layerWindowOptionName, "Window"), (layerSpritesOptionName, "Sprites"), }; protected bool layerBackgroundForceEnable, layerWindowForceEnable, layerSpritesForceEnable; public DMGVideo(MemoryReadDelegate memoryRead, RequestInterruptDelegate requestInterrupt) { vram = new byte[1, 0x2000]; oam = new byte[0xA0]; // modeFunctions = new Action[] { StepHBlank, StepVBlank, StepOAMSearch, StepLCDTransfer }; spritesOnLine = new int[maxSpritesPerLine]; memoryReadDelegate = memoryRead; requestInterruptDelegate = requestInterrupt; layerBackgroundForceEnable = true; layerWindowForceEnable = true; layerSpritesForceEnable = true; } public object GetRuntimeOption(string name) { switch (name) { case layerBackgroundOptionName: return layerBackgroundForceEnable; case layerWindowOptionName: return layerWindowForceEnable; case layerSpritesOptionName: return layerSpritesForceEnable; default: return null; } } public void SetRuntimeOption(string name, object value) { switch (name) { case layerBackgroundOptionName: layerBackgroundForceEnable = (bool)value; break; case layerWindowOptionName: layerWindowForceEnable = (bool)value; break; case layerSpritesOptionName: layerSpritesForceEnable = (bool)value; break; } } public virtual void Startup() { Reset(); if (memoryReadDelegate == null) throw new EmulationException("DMGVideo: Memory read delegate is null"); if (requestInterruptDelegate == null) throw new EmulationException("DMGVideo: Request interrupt delegate is null"); 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 (var i = 0; i < vram.GetLength(0); i++) vram[0, i] = 0; for (var i = 0; i < oam.Length; i++) oam[i] = 0; for (var i = (byte)0x40; i < 0x4C; i++) { // skip OAM dma if (i != 0x46) WritePort(i, 0x00); } numSpritesOnLine = skipFrames = 0; statIrqSignal = vBlankReady = false; for (var i = 0; i < spritesOnLine.Length; i++) spritesOnLine[i] = -1; ClearScreenUsage(); cycleCount = cycleDotPause = currentScanline = 0; } public void SetClockRate(double clock) { clockRate = clock; ReconfigureTimings(); } public void SetRefreshRate(double refresh) { refreshRate = refresh; 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) / displayTotalHeight); /* Create arrays */ screenUsageFlags = new byte[displayActiveWidth, displayActiveHeight]; screenUsageSpriteXCoords = new byte[displayActiveWidth, displayActiveHeight]; screenUsageSpriteSlots = new byte[displayActiveWidth, displayActiveHeight]; outputFramebuffer = new byte[numDisplayPixels * 4]; for (var y = 0; y < displayActiveHeight; y++) SetLine(y, 0xFF, 0xFF, 0xFF); } public virtual void Step(int clockCyclesInStep) { for (var c = 0; c < clockCyclesInStep; c++) { if (lcdEnable) { /* LCD enabled, handle LCD modes */ modeFunctions[modeNumber](); } else { /* LCD disabled */ modeNumber = 0; cycleCount = 0; cycleDotPause = 0; currentScanline = 0; ly = 0; } } } /* TODO: dot clock pause! -- https://gbdev.io/pandocs/#properties-of-stat-modes * - non-zero (SCX % 8) causes pause of that many dots * - active window causes pause of *at least* 6 dots? * - sprite dot pauses?? * - influence of CGB double-speed mode on this? * Required for: * - GBVideoPlayer2, pixel column alignment & garbage on left screen edge * - snorpung/pocket demo, vertical scroller right edge of scroll area * - Prehistorik Man, scroller alignment (ex. level start "START" text should be centered on screen) * - ...probably more? */ protected virtual void StepVBlank() { // TODO: *should* be 4, but Altered Space hangs w/ any value lower than 8? if (cycleCount == 8 && vBlankReady) { requestInterruptDelegate(InterruptSource.VBlank); vBlankReady = false; } /* V-blank */ cycleCount++; if (cycleCount == clockCyclesPerLine) EndVBlank(); } protected virtual void EndVBlank() { /* End of scanline reached */ OnEndOfScanline(EventArgs.Empty); currentScanline++; ly = (byte)currentScanline; /* Check for & request STAT interrupts */ CheckAndRequestStatInterupt(); if (currentScanline == displayTotalHeight - 1) { // TODO: specific cycle this happens? /* LY reports as 0 on line 153 */ ly = 0; // TODO: verify if STAT/LYC interrupt is supposed to happen here? currently breaks Shantae's sprites if done //CheckAndRequestStatInterupt(); } else if (currentScanline == displayTotalHeight) { /* End of V-blank reached */ modeNumber = 2; currentScanline = 0; ly = 0; CheckAndRequestStatInterupt(); ClearScreenUsage(); } cycleCount = 0; } protected virtual void StepOAMSearch() { /* OAM search */ if ((cycleCount % 2) == 0) { /* Get object Y coord */ var objIndex = cycleCount >> 1; var objY = oam[(objIndex << 2) + 0] - 16; /* Check if object is on current scanline & maximum number of objects was not exceeded, then increment counter */ if (currentScanline >= objY && currentScanline < (objY + (objSize ? 16 : 8)) && numSpritesOnLine < maxSpritesPerLine) { var objX = oam[(objIndex << 2) + 1] - 8; cycleDotPause += 11 - Math.Min(5, (objX + (objX >= windowX ? (255 - windowX) : scrollX)) % 8); // TODO: correct? spritesOnLine[numSpritesOnLine++] = objIndex; } } /* Increment cycle count & check for next LCD mode */ cycleCount++; if (cycleCount == mode2Boundary) EndOAMSearch(); } protected virtual void EndOAMSearch() { // 1) GBVideoPlayer2 & Prehistorik Man alignment, 2) GBVideoPlayer2 alignment cycleDotPause += (6 << (IsDoubleSpeed ? 1 : 0)) + (scrollX % 8); modeNumber = 3; CheckAndRequestStatInterupt(); } protected virtual void StepLCDTransfer() { /* Data transfer to LCD */ /* Render pixels */ RenderPixel(currentScanline, cycleCount - mode2Boundary - cycleDotPause); /* Increment cycle count & check for next LCD mode */ cycleCount++; // 3) snorpung/pocket scroller width if (cycleCount == mode3Boundary + cycleDotPause + (wndEnable ? 12 : 0)) EndLCDTransfer(); } protected virtual void EndLCDTransfer() { modeNumber = 0; CheckAndRequestStatInterupt(); } protected virtual void StepHBlank() { /* H-blank */ /* Increment cycle count & check for next LCD mode */ cycleCount++; if (cycleCount == clockCyclesPerLine) EndHBlank(); } protected virtual void EndHBlank() { /* End of scanline reached */ OnEndOfScanline(EventArgs.Empty); currentScanline++; ly = (byte)currentScanline; CheckAndRequestStatInterupt(); for (var i = 0; i < spritesOnLine.Length; i++) spritesOnLine[i] = -1; numSpritesOnLine = 0; if (currentScanline == displayActiveHeight) { modeNumber = 1; CheckAndRequestStatInterupt(); /* Reached V-blank, request V-blank interrupt */ vBlankReady = true; if (skipFrames > 0) skipFrames--; /* Submit screen for rendering */ OnRenderScreen(new RenderScreenEventArgs(displayActiveWidth, displayActiveHeight, outputFramebuffer.Clone() as byte[])); } else { modeNumber = 2; CheckAndRequestStatInterupt(); } cycleCount = 0; cycleDotPause = 0; } protected void CheckAndRequestStatInterupt() { if (!lcdEnable) return; var oldSignal = statIrqSignal; statIrqSignal = false; if (modeNumber == 0 && m0HBlankInterrupt) statIrqSignal = true; if (modeNumber == 1 && m1VBlankInterrupt) statIrqSignal = true; if (modeNumber == 2 && m2OamInterrupt) statIrqSignal = true; coincidenceFlag = (ly == lyCompare); if (coincidenceFlag && lycLyInterrupt) statIrqSignal = true; if (!oldSignal && statIrqSignal) requestInterruptDelegate(InterruptSource.LCDCStatus); } protected virtual 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; if (bgEnable) { RenderBackground(y, x); if (wndEnable) RenderWindow(y, x); } else SetPixel(y, x, 0xFF, 0xFF, 0xFF); if (objEnable) RenderSprites(y, x); } protected virtual 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); // Calculate tile address & get pixel color index var tileAddress = tileBase + (tileNumber << 4) + ((yTransformed & 7) << 1); var ba = (vram[0, tileAddress + 0] >> (7 - (xTransformed % 8))) & 0b1; var bb = (vram[0, tileAddress + 1] >> (7 - (xTransformed % 8))) & 0b1; var c = (byte)((bb << 1) | ba); // If color is not 0, note that a BG pixel exists here if (c != 0) screenUsageFlags[x, y] |= screenUsageBackground; // Draw pixel if (layerBackgroundForceEnable) SetPixel(y, x, (byte)((bgPalette >> (c << 1)) & 0x03)); else SetPixel(y, x, (byte)(bgPalette & 0x03)); } protected virtual 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); // Calculate tile address & get pixel color index var tileAddress = tileBase + (tileNumber << 4) + ((yTransformed & 7) << 1); var ba = (vram[0, tileAddress + 0] >> (7 - (xTransformed % 8))) & 0b1; var bb = (vram[0, tileAddress + 1] >> (7 - (xTransformed % 8))) & 0b1; var c = (byte)((bb << 1) | ba); // If color is not 0, note that a Window pixel exists here if (c != 0) screenUsageFlags[x, y] |= screenUsageWindow; // Draw pixel if (layerWindowForceEnable) SetPixel(y, x, (byte)((bgPalette >> (c << 1)) & 0x03)); else SetPixel(y, x, (byte)(bgPalette & 0x03)); } protected virtual void RenderSprites(int y, int x) { var objHeight = objSize ? 16 : 8; // Iterate over sprite on line for (var s = 0; s < numSpritesOnLine; 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 objPalNumber = (objAttributes >> 4) & 0b1; // 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 pal = objPalNumber == 0 ? obPalette0 : obPalette1; var ba = (vram[0, tileAddress + 0] >> xShift) & 0b1; var bb = (vram[0, 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; // Draw pixel if (layerSpritesForceEnable) SetPixel(y, x, (byte)((pal >> (c << 1)) & 0x03)); } } } } protected virtual bool HasSpritePriority(int y, int x, int objSlot) { // Get new sprite X coord var objX = (byte)(oam[(objSlot * 4) + 1] - 8); // Get potentially existing sprite X coord and slot var prevX = screenUsageSpriteXCoords[x, y]; var prevSlot = screenUsageSpriteSlots[x, y]; // If existing sprite has lower X coord -or- both sprites have same X coord BUT existing sprite has lower slot, new sprite does not have priority if (prevX < objX || (prevX == objX && prevSlot < objSlot)) return false; // Get new sprite OBJ-to-BG priority attribute var objIsBehindBg = ((oam[(objSlot * 4) + 3] >> 7) & 0b1) == 0b1; // If new sprite is shown behind BG/Window -and- a BG/Window pixel has already been drawn, new sprite does not have priority if (objIsBehindBg && (IsScreenUsageFlagSet(y, x, screenUsageBackground) || IsScreenUsageFlagSet(y, x, screenUsageWindow))) return false; // New sprite has priority return true; } protected void SetLine(int y, byte c) { for (int x = 0; x < displayActiveWidth; x++) SetPixel(y, x, c); } protected void SetLine(int y, byte b, byte g, byte r) { for (int x = 0; x < displayActiveWidth; x++) SetPixel(y, x, b, g, r); } protected void SetPixel(int y, int x, byte c) { WriteColorToFramebuffer(c, ((y * displayActiveWidth) + (x % displayActiveWidth)) * 4); } protected void SetPixel(int y, int x, byte b, byte g, byte r) { WriteColorToFramebuffer(b, g, r, ((y * displayActiveWidth) + (x % displayActiveWidth)) * 4); } protected virtual void WriteColorToFramebuffer(byte c, int address) { outputFramebuffer[address + 0] = colorValuesBgr[c & 0x03][0]; outputFramebuffer[address + 1] = colorValuesBgr[c & 0x03][1]; outputFramebuffer[address + 2] = colorValuesBgr[c & 0x03][2]; outputFramebuffer[address + 3] = 0xFF; } 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 ClearScreenUsage() { for (var y = 0; y < displayActiveHeight; y++) { for (var x = 0; x < displayActiveWidth; x++) { screenUsageFlags[x, y] = screenUsageEmpty; screenUsageSpriteXCoords[x, y] = 255; screenUsageSpriteSlots[x, y] = numOamSlots; } } } protected bool IsScreenUsageFlagSet(int y, int x, byte flag) { return (screenUsageFlags[x, y] & flag) == flag; } public virtual byte ReadVram(ushort address) { if (modeNumber != 3) return vram[0, address & (vram.Length - 1)]; else return 0xFF; } public virtual void WriteVram(ushort address, byte value) { if (modeNumber != 3) vram[0, address & (vram.Length - 1)] = value; } public virtual byte ReadOam(ushort address) { if (modeNumber != 2 && modeNumber != 3) return oam[address - 0xFE00]; else return 0xFF; } public virtual void WriteOam(ushort address, byte value) { if (modeNumber != 2 && modeNumber != 3) oam[address - 0xFE00] = value; } public virtual byte ReadPort(byte port) { switch (port) { case 0x40: // LCDC return (byte)( (lcdEnable ? (1 << 7) : 0) | (wndMapSelect ? (1 << 6) : 0) | (wndEnable ? (1 << 5) : 0) | (bgWndTileSelect ? (1 << 4) : 0) | (bgMapSelect ? (1 << 3) : 0) | (objSize ? (1 << 2) : 0) | (objEnable ? (1 << 1) : 0) | (bgEnable ? (1 << 0) : 0)); case 0x41: // STAT return (byte)( 0x80 | (lycLyInterrupt ? (1 << 6) : 0) | (m2OamInterrupt ? (1 << 5) : 0) | (m1VBlankInterrupt ? (1 << 4) : 0) | (m0HBlankInterrupt ? (1 << 3) : 0) | (coincidenceFlag ? (1 << 2) : 0) | ((modeNumber & 0b11) << 0)); case 0x42: // SCY return scrollY; case 0x43: // SCX return scrollX; case 0x44: // LY return ly; case 0x45: // LYC return lyCompare; case 0x46: // DMA return oamDmaStart; case 0x47: // BGP return bgPalette; case 0x48: // OBP0 return obPalette0; case 0x49: // OBP1 return obPalette1; case 0x4A: // WY return windowY; case 0x4B: //WX return windowX; default: return 0xFF; } } public virtual void WritePort(byte port, byte value) { switch (port) { case 0x40: // LCDC { var newLcdEnable = ((value >> 7) & 0b1) == 0b1; if (lcdEnable != newLcdEnable) { modeNumber = 2; currentScanline = 0; ly = 0; CheckAndRequestStatInterupt(); if (newLcdEnable) skipFrames = numSkippedFramesLcdOn; } lcdEnable = newLcdEnable; wndMapSelect = ((value >> 6) & 0b1) == 0b1; wndEnable = ((value >> 5) & 0b1) == 0b1; bgWndTileSelect = ((value >> 4) & 0b1) == 0b1; bgMapSelect = ((value >> 3) & 0b1) == 0b1; objSize = ((value >> 2) & 0b1) == 0b1; objEnable = ((value >> 1) & 0b1) == 0b1; bgEnable = ((value >> 0) & 0b1) == 0b1; } break; case 0x41: // STAT lycLyInterrupt = ((value >> 6) & 0b1) == 0b1; m2OamInterrupt = ((value >> 5) & 0b1) == 0b1; m1VBlankInterrupt = ((value >> 4) & 0b1) == 0b1; m0HBlankInterrupt = ((value >> 3) & 0b1) == 0b1; CheckAndRequestStatInterupt(); // TODO: correct? if (lcdEnable && modeNumber == 1 && currentScanline != 0) requestInterruptDelegate(InterruptSource.LCDCStatus); break; case 0x42: // SCY scrollY = value; break; case 0x43: // SCX scrollX = value; break; case 0x44: // LY break; case 0x45: // LYC lyCompare = value; CheckAndRequestStatInterupt(); break; case 0x46: // DMA oamDmaStart = value; for (int src = 0, dst = oamDmaStart << 8; src < 0xA0; src++, dst++) oam[src] = memoryReadDelegate((ushort)dst); break; case 0x47: // BGP bgPalette = value; break; case 0x48: // OBP0 obPalette0 = value; break; case 0x49: // OBP1 obPalette1 = value; break; case 0x4A: // WY windowY = value; break; case 0x4B: // WX windowX = value; break; } } } }