mirror of https://github.com/Washi1337/Emux.git
456 lines
16 KiB
C#
456 lines
16 KiB
C#
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;
|
|
}
|
|
|
|
}
|
|
}
|