Essgee.Unity/Assets/Plugins/Essgee/Emulation/Video/Nintendo/CGBVideo.cs
2025-01-02 17:55:16 +08:00

540 lines
14 KiB
C#

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;
}
}
}
}