using System; using System.Threading; using Emux.GameBoy.Cpu; namespace Emux.GameBoy.Graphics { public unsafe class GameBoyGpu { public const int FrameWidth = 160; public const int FrameHeight = 144; public const int ScanLineOamCycles = 80; public const int ScanLineVramCycles = 172; public const int HBlankCycles = 204; public const int OneLineCycles = 456; public const int VBlankCycles = 456 * 10; public const int FullFrameCycles = 70224; private readonly byte[] _frameBuffer = new byte[3 * FrameWidth * FrameHeight]; private readonly GameBoy _device; private readonly byte[] _vram = new byte[0x2000]; private readonly byte[] _oam = new byte[0xA0]; private readonly Color[] _colors = { new Color(255,255,255), new Color(192,192,192), new Color(96,96,96), new Color(0,0,0), }; private int _modeClock; private LcdControlFlags _lcdc; public LcdControlFlags Lcdc { get { return _lcdc; } set { if ((value & LcdControlFlags.EnableLcd) == 0) { Array.Clear(_frameBuffer, 0, _frameBuffer.Length); VideoOutput.RenderFrame(_frameBuffer); LY = 0; Stat = (LcdStatusFlags) 0x80; _modeClock = 0; } else if ((_lcdc & LcdControlFlags.EnableLcd) == 0) { _modeClock = 0; if (LY == LYC) Stat |= LcdStatusFlags.Coincidence; } _lcdc = value; } } public LcdStatusFlags Stat; public byte ScY; public byte ScX; public byte LY; public byte LYC; public byte Bgp; public byte ObjP0; public byte ObjP1; public byte WY; public byte WX; public GameBoyGpu(GameBoy device) { if (device == null) throw new ArgumentNullException(nameof(device)); _device = device; VideoOutput = new EmptyVideoOutput(); } public IVideoOutput VideoOutput { get; set; } public void WriteOam(byte address, byte value) { _oam[address] = value; } public void ImportOam(byte[] oamData) { Buffer.BlockCopy(oamData, 0, _oam, 0, oamData.Length); } public byte ReadOam(byte address) { return _oam[address]; } public void WriteVRam(ushort address, byte value) { _vram[address] = value; } public byte ReadVRam(int address) { return _vram[address]; } public void WriteRegister(byte address, byte value) { switch (address) { case 0x40: Lcdc = (LcdControlFlags) value; return; case 0x41: Stat = (LcdStatusFlags) (value & 0b01111000); return; case 0x42: ScY = value; return; case 0x43: ScX = value; return; case 0x44: LY = value; return; case 0x45: LYC = value; return; case 0x47: Bgp = value; return; case 0x48: ObjP0 = value; return; case 0x49: ObjP1 = value; return; case 0x4A: WY = value; return; case 0x4B: WX = value; return; } throw new ArgumentOutOfRangeException(nameof(address)); } public byte ReadRegister(byte address) { switch (address) { case 0x40: return (byte) _lcdc; case 0x41: return (byte) Stat; case 0x42: return ScY; case 0x43: return ScX; case 0x44: return LY; case 0x45: return LYC; case 0x47: return Bgp; case 0x48: return ObjP0; case 0x49: return ObjP1; case 0x4A: return WY; case 0x4B: return WX; } throw new ArgumentOutOfRangeException(nameof(address)); } public void GpuStep(int cycles) { if ((_lcdc & LcdControlFlags.EnableLcd) == 0) return; _modeClock += cycles; unchecked { LcdStatusFlags stat = Stat; var currentMode = stat & LcdStatusFlags.ModeMask; switch (currentMode) { case LcdStatusFlags.ScanLineOamMode: if (_modeClock >= ScanLineOamCycles) { _modeClock -= ScanLineOamCycles; currentMode = LcdStatusFlags.ScanLineVRamMode; } break; case LcdStatusFlags.ScanLineVRamMode: if (_modeClock >= ScanLineVramCycles) { _modeClock -= ScanLineVramCycles; currentMode = LcdStatusFlags.HBlankMode; if ((stat & LcdStatusFlags.HBlankModeInterrupt) == LcdStatusFlags.HBlankModeInterrupt) _device.Cpu.Registers.IF |= InterruptFlags.LcdStat; RenderScan(); } break; case LcdStatusFlags.HBlankMode: if (_modeClock >= HBlankCycles) { _modeClock -= HBlankCycles; LY++; if (LY == FrameHeight - 1) { currentMode = LcdStatusFlags.VBlankMode; VideoOutput.RenderFrame(_frameBuffer); _device.Cpu.Registers.IF |= InterruptFlags.VBlank; if ((stat & LcdStatusFlags.VBlankModeInterrupt) == LcdStatusFlags.VBlankModeInterrupt) _device.Cpu.Registers.IF |= InterruptFlags.LcdStat; } else { currentMode = LcdStatusFlags.ScanLineOamMode; } } break; case LcdStatusFlags.VBlankMode: if (_modeClock >= OneLineCycles) { _modeClock -= OneLineCycles; LY++; if (LY > FrameHeight + 9) { currentMode = LcdStatusFlags.ScanLineOamMode; LY = 0; if ((stat & LcdStatusFlags.OamBlankModeInterrupt) == LcdStatusFlags.OamBlankModeInterrupt) _device.Cpu.Registers.IF |= InterruptFlags.LcdStat; } } break; } stat &= (LcdStatusFlags) ~0b111; stat |= currentMode; if (LY == LYC) stat |= LcdStatusFlags.Coincidence; Stat = stat; } } private void RenderScan() { if ((_lcdc & LcdControlFlags.EnableBackground) == LcdControlFlags.EnableBackground) RenderBackgroundScan(); if ((_lcdc & LcdControlFlags.EnableWindow) == LcdControlFlags.EnableWindow) RenderWindowScan(); if ((_lcdc & LcdControlFlags.EnableSprites) == LcdControlFlags.EnableSprites) RenderSpritesScan(); } private void RenderBackgroundScan() { // Move to correct tile map address. int tileMapAddress = (_lcdc & LcdControlFlags.BgTileMapSelect) == LcdControlFlags.BgTileMapSelect ? 0x1C00 : 0x1800; int tileMapLine = ((LY + ScY) & 0xFF) >> 3; tileMapAddress += tileMapLine * 0x20; // Move to correct tile data address. int tileDataAddress = (_lcdc & LcdControlFlags.BgWindowTileDataSelect) == LcdControlFlags.BgWindowTileDataSelect ? 0x0000 : 0x0800; int tileDataOffset = ((LY + ScY) & 7) * 2; tileDataAddress += tileDataOffset; int x = ScX; // Read first tile data to render. byte[] currentTileData = new byte[2]; CopyTileData(tileMapAddress, x >> 3, tileDataAddress, currentTileData); // Render scan line. for (int outputX = 0; outputX < FrameWidth; outputX++, x++) { if ((x & 7) == 0) { // Read next tile data to render. CopyTileData(tileMapAddress, x >> 3, tileDataAddress, currentTileData); } var color = DeterminePixelColor(x, currentTileData, Bgp); _frameBuffer[LY * FrameWidth * 3 + outputX * 3] = color.R; _frameBuffer[LY * FrameWidth * 3 + outputX * 3 + 1] = color.G; _frameBuffer[LY * FrameWidth * 3 + outputX * 3 + 2] = color.B; } } private void CopyTileData(int tileMapAddress, int tileIndex, int tileDataAddress, byte[] buffer) { byte dataIndex = _vram[(ushort) (tileMapAddress + tileIndex)]; if ((_lcdc & LcdControlFlags.BgWindowTileDataSelect) != LcdControlFlags.BgWindowTileDataSelect) { // Index is signed number in [-128..127] => compensate for it. dataIndex = unchecked((byte) ((sbyte) dataIndex + 0x80)); } Buffer.BlockCopy(_vram, tileDataAddress + (dataIndex << 4), buffer, 0, 2); } private void RenderWindowScan() { if (LY >= WY) { // Move to correct tile map address. int tileMapAddress = (_lcdc & LcdControlFlags.WindowTileMapSelect) == LcdControlFlags.WindowTileMapSelect ? 0x1C00 : 0x1800; int tileMapLine = ((LY - WY) & 0xFF) >> 3; tileMapAddress += tileMapLine * 0x20; // Move to correct tile data address. int tileDataAddress = (_lcdc & LcdControlFlags.BgWindowTileDataSelect) == LcdControlFlags.BgWindowTileDataSelect ? 0x0000 : 0x0800; int tileDataOffset = ((LY - WY) & 7) * 2; tileDataAddress += tileDataOffset; int x = 0; byte[] currentTileData = new byte[2]; // Render scan line. for (int outputX = WX - 7; outputX < FrameWidth; outputX++, x++) { if ((x & 7) == 0) { // Read next tile data to render. CopyTileData(tileMapAddress, x >> 3, tileDataAddress, currentTileData); } var color = DeterminePixelColor(x, currentTileData, Bgp); _frameBuffer[LY * FrameWidth * 3 + outputX * 3] = color.R; _frameBuffer[LY * FrameWidth * 3 + outputX * 3 + 1] = color.G; _frameBuffer[LY * FrameWidth * 3 + outputX * 3 + 2] = color.B; } } } private void RenderSpritesScan() { fixed (byte* ptr = _oam) { // GameBoy only supports 10 sprites in one scan line. int spritesCount = 0; for (int i = 0; i < 40 && spritesCount < 10; i++) { var data = ((SpriteData*) ptr)[i]; int absoluteY = data.Y - 16; // Check if sprite is on current scan line. if (absoluteY <= LY && LY < absoluteY + 8) { // TODO: take order into account. spritesCount++; // Check if actually on the screen. if (data.X > 0 && data.X < FrameWidth + 8) { byte palette = (data.Flags & SpriteDataFlags.UsePalette1) == SpriteDataFlags.UsePalette1 ? ObjP1 : ObjP0; // Read tile data. int rowIndex = LY - absoluteY; // Flip sprite vertically if specified. if ((data.Flags & SpriteDataFlags.YFlip) == SpriteDataFlags.YFlip) rowIndex = 7 - rowIndex; byte[] currentTileData = new byte[2]; Buffer.BlockCopy(_vram, (ushort)(0x0000 + (data.TileDataIndex << 4) + rowIndex * 2), currentTileData, 0, 2); // Render sprite. for (int x = 0; x < 8; x++) { int absoluteX = data.X - 8; // Flip sprite horizontally if specified. if ((data.Flags & SpriteDataFlags.XFlip) != SpriteDataFlags.XFlip) absoluteX += x; else absoluteX += 7 - x; if (absoluteX >= 0 && absoluteX < FrameWidth) { int colorIndex = DetermineColorIndex(x, currentTileData, palette); // TODO: take priority into account. // Check for transparent color. if (colorIndex != 0) { var color = _colors[colorIndex]; _frameBuffer[LY * FrameWidth * 3 + absoluteX * 3] = color.R; _frameBuffer[LY * FrameWidth * 3 + absoluteX * 3 + 1] = color.G; _frameBuffer[LY * FrameWidth * 3 + absoluteX * 3 + 2] = color.B; } } } } } } } } private Color DeterminePixelColor(int x, byte[] tileRowData, byte palette) { return _colors[DetermineColorIndex(x, tileRowData, palette)]; } private static int DetermineColorIndex(int x, byte[] tileRowData, byte palette) { int bitIndex = 7 - (x & 7); int paletteIndex = (((tileRowData[0] >> bitIndex) & 1) << 1) | ((tileRowData[1] >> bitIndex) & 1); int colorIndex = (palette >> (paletteIndex * 2)) & 3; return colorIndex; } public void Reset() { _modeClock = 0; LY = 0; ScY = 0; ScX = 0; Stat = (LcdStatusFlags) 0x85; } } }