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

875 lines
23 KiB
C#

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<SizeScreenEventArgs> SizeScreen;
public virtual void OnSizeScreen(SizeScreenEventArgs e) { SizeScreen?.Invoke(this, e); }
public virtual event EventHandler<RenderScreenEventArgs> RenderScreen;
public virtual void OnRenderScreen(RenderScreenEventArgs e) { RenderScreen?.Invoke(this, e); }
public virtual event EventHandler<EventArgs> 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;
}
}
}
}