diff --git a/LR35902/AbstractColourPalette.cs b/LR35902/AbstractColourPalette.cs
new file mode 100644
index 0000000..1d27adc
--- /dev/null
+++ b/LR35902/AbstractColourPalette.cs
@@ -0,0 +1,18 @@
+//
+// Copyright (c) Adrian Conlon. All rights reserved.
+//
+namespace EightBit
+{
+ namespace GameBoy
+ {
+ public class AbstractColourPalette
+ {
+ private readonly uint[] colours = new uint[4];
+
+ protected AbstractColourPalette()
+ { }
+
+ public uint Colour(int index) => this.colours[index];
+ }
+ }
+}
diff --git a/LR35902/CartridgeType.cs b/LR35902/CartridgeType.cs
new file mode 100644
index 0000000..e5e5b44
--- /dev/null
+++ b/LR35902/CartridgeType.cs
@@ -0,0 +1,16 @@
+//
+// Copyright (c) Adrian Conlon. All rights reserved.
+//
+namespace EightBit
+{
+ namespace GameBoy
+ {
+ public enum CartridgeType
+ {
+ ROM = 0,
+ ROM_MBC1 = 1,
+ ROM_MBC1_RAM = 2,
+ ROM_MBC1_RAM_BATTERY = 3,
+ }
+ }
+}
diff --git a/LR35902/CharacterDefinition.cs b/LR35902/CharacterDefinition.cs
new file mode 100644
index 0000000..d146ea7
--- /dev/null
+++ b/LR35902/CharacterDefinition.cs
@@ -0,0 +1,42 @@
+//
+// Copyright (c) Adrian Conlon. All rights reserved.
+//
+namespace EightBit
+{
+ namespace GameBoy
+ {
+ public sealed class CharacterDefinition
+ {
+ private readonly Ram vram;
+ private readonly ushort address;
+
+ public CharacterDefinition(Ram vram, ushort address)
+ {
+ this.vram = vram;
+ this.address = address;
+ }
+
+ public int[] Get(int row)
+ {
+ var returned = new int[8];
+
+ var planeAddress = (ushort)(this.address + (row * 2));
+
+ var planeLow = this.vram.Peek(planeAddress);
+ var planeHigh = this.vram.Peek(++planeAddress);
+
+ for (var bit = 0; bit < 8; ++bit)
+ {
+ var mask = 1 << bit;
+
+ var bitLow = (planeLow & mask) != 0 ? 1 : 0;
+ var bitHigh = (planeHigh & mask) != 0 ? 0b10 : 0;
+
+ returned[7 - bit] = bitHigh | bitLow;
+ }
+
+ return returned;
+ }
+ }
+ }
+}
diff --git a/LR35902/ColourShades.cs b/LR35902/ColourShades.cs
new file mode 100644
index 0000000..b500d9f
--- /dev/null
+++ b/LR35902/ColourShades.cs
@@ -0,0 +1,16 @@
+//
+// Copyright (c) Adrian Conlon. All rights reserved.
+//
+namespace EightBit
+{
+ namespace GameBoy
+ {
+ public enum ColourShades
+ {
+ Off,
+ Light,
+ Medium,
+ Dark
+ }
+ }
+}
diff --git a/LR35902/Display.cs b/LR35902/Display.cs
new file mode 100644
index 0000000..3e9be27
--- /dev/null
+++ b/LR35902/Display.cs
@@ -0,0 +1,178 @@
+//
+// Copyright (c) Adrian Conlon. All rights reserved.
+//
+namespace EightBit
+{
+ namespace GameBoy
+ {
+ public sealed class Display
+ {
+ public static readonly int BufferWidth = 256;
+ public static readonly int BufferHeight = 256;
+ public static readonly int BufferCharacterWidth = BufferWidth / 8;
+ public static readonly int BufferCharacterHeight = BufferHeight / 8;
+ public static readonly int RasterWidth = 160;
+ public static readonly int RasterHeight = 144;
+ public static readonly int PixelCount = RasterWidth * RasterHeight;
+
+ private readonly Bus bus;
+ private readonly Ram oam;
+ private readonly Ram vram;
+ private readonly AbstractColourPalette colours;
+ private readonly ObjectAttribute[] objectAttributes = new ObjectAttribute[40];
+ private byte control;
+ private byte scanLine = 0;
+
+ public Display(AbstractColourPalette colours, Bus bus, Ram oam, Ram vram)
+ {
+ this.colours = colours;
+ this.bus = bus;
+ this.oam = oam;
+ this.vram = vram;
+ }
+
+ public uint[] Pixels { get; } = new uint[PixelCount];
+
+ public void Render()
+ {
+ this.scanLine = this.bus.IO.Peek(IoRegisters.LY);
+ if (this.scanLine < RasterHeight)
+ {
+ this.control = this.bus.IO.Peek(IoRegisters.LCDC);
+ if ((this.control & (byte)LcdcControl.LcdEnable) != 0)
+ {
+ if ((this.control & (byte)LcdcControl.DisplayBackground) != 0)
+ {
+ this.RenderBackground();
+ }
+
+ if ((this.control & (byte)LcdcControl.ObjectEnable) != 0)
+ {
+ this.RenderObjects();
+ }
+ }
+ }
+ }
+
+ public void LoadObjectAttributes()
+ {
+ for (var i = 0; i < 40; ++i)
+ {
+ this.objectAttributes[i] = new ObjectAttribute(this.oam, (ushort)(4 * i));
+ }
+ }
+
+ private int[] CreatePalette(ushort address)
+ {
+ var raw = this.bus.IO.Peek(address);
+ return new int[4]
+ {
+ raw & 0b11,
+ (raw & 0b1100) >> 2,
+ (raw & 0b110000) >> 4,
+ (raw & 0b11000000) >> 6,
+ };
+ }
+
+ private void RenderBackground()
+ {
+ var palette = this.CreatePalette(IoRegisters.BGP);
+
+ var window = (this.control & (byte)LcdcControl.WindowEnable) != 0;
+ var bgArea = (this.control & (byte)LcdcControl.BackgroundCodeAreaSelection) != 0 ? 0x1c00 : 0x1800;
+ var bgCharacters = (this.control & (byte)LcdcControl.BackgroundCharacterDataSelection) != 0 ? 0 : 0x800;
+
+ var wx = this.bus.IO.Peek(IoRegisters.WX);
+ var wy = this.bus.IO.Peek(IoRegisters.WY);
+
+ var offsetX = window ? wx - 7 : 0;
+ var offsetY = window ? wy : 0;
+
+ var scrollX = this.bus.IO.Peek(IoRegisters.SCX);
+ var scrollY = this.bus.IO.Peek(IoRegisters.SCY);
+
+ this.RenderBackground(bgArea, bgCharacters, offsetX - scrollX, offsetY - scrollY, palette);
+ }
+
+ private void RenderBackground(int bgArea, int bgCharacters, int offsetX, int offsetY, int[] palette)
+ {
+ var row = (this.scanLine - offsetY) / 8;
+ var address = bgArea + (row * BufferCharacterWidth);
+
+ for (var column = 0; column < BufferCharacterWidth; ++column)
+ {
+ var character = this.vram.Peek((ushort)address++);
+ var definition = new CharacterDefinition(this.vram, (ushort)(bgCharacters + (16 * character)));
+ this.RenderTile(8, (column * 8) + offsetX, (row * 8) + offsetY, false, false, false, palette, definition);
+ }
+ }
+
+ private void RenderObjects()
+ {
+ var objBlockHeight = (this.control & (byte)LcdcControl.ObjectBlockCompositionSelection) != 0 ? 16 : 8;
+
+ var palettes = new int[2][];
+ palettes[0] = this.CreatePalette(IoRegisters.OBP0);
+ palettes[1] = this.CreatePalette(IoRegisters.OBP1);
+
+ var characterAddressMultiplier = objBlockHeight == 8 ? 16 : 8;
+
+ for (var i = 0; i < 40; ++i)
+ {
+ var current = this.objectAttributes[i];
+
+ var spriteY = current.PositionY;
+ var drawY = spriteY - 16;
+
+ if ((this.scanLine >= drawY) && (this.scanLine < (drawY + objBlockHeight)))
+ {
+ var spriteX = current.PositionX;
+ var drawX = spriteX - 8;
+
+ var sprite = current.Pattern;
+ var definition = new CharacterDefinition(this.vram, (ushort)(characterAddressMultiplier * sprite));
+ var palette = palettes[current.Palette];
+ var flipX = current.FlipX;
+ var flipY = current.FlipY;
+
+ this.RenderTile(objBlockHeight, drawX, drawY, flipX, flipY, true, palette, definition);
+ }
+ }
+ }
+
+ private void RenderTile(int height, int drawX, int drawY, bool flipX, bool flipY, bool allowTransparencies, int[] palette, CharacterDefinition definition)
+ {
+ const int width = 8;
+ const int flipMaskX = width - 1;
+ var flipMaskY = height - 1;
+
+ var y = this.scanLine;
+
+ var cy = y - drawY;
+ if (flipY)
+ {
+ cy = ~cy & flipMaskY;
+ }
+
+ var rowDefinition = definition.Get(cy);
+
+ var lineAddress = y * RasterWidth;
+ for (var cx = 0; cx < width; ++cx)
+ {
+ var x = drawX + (flipX ? ~cx & flipMaskX : cx);
+ if (x >= RasterWidth)
+ {
+ break;
+ }
+
+ var colour = rowDefinition[cx];
+ if (!allowTransparencies || (allowTransparencies && (colour > 0)))
+ {
+ var outputPixel = lineAddress + x;
+ this.Pixels[outputPixel] = this.colours.Colour(palette[colour]);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/LR35902/GameboyBus.cs b/LR35902/GameboyBus.cs
new file mode 100644
index 0000000..550b7e6
--- /dev/null
+++ b/LR35902/GameboyBus.cs
@@ -0,0 +1,434 @@
+//
+// Copyright (c) Adrian Conlon. All rights reserved.
+//
+using System;
+using System.Collections.Generic;
+
+namespace EightBit
+{
+ namespace GameBoy
+ {
+ public abstract class Bus : EightBit.Bus
+ {
+ public const int CyclesPerSecond = 4 * 1024 * 1024;
+ public const int FramesPerSecond = 60;
+ public const int CyclesPerFrame = CyclesPerSecond / FramesPerSecond;
+ public const int TotalLineCount = 154;
+ public const int CyclesPerLine = CyclesPerFrame / TotalLineCount;
+ public const int RomPageSize = 0x4000;
+
+ private readonly Rom bootRom = new Rom(0x100); // 0x0000 - 0x00ff
+ private readonly List gameRomBanks; // 0x0000 - 0x3fff, 0x4000 - 0x7fff (switchable)
+ private readonly List ramBanks; // 0xa000 - 0xbfff (switchable)
+ private readonly UnusedMemory unmapped2000 = new UnusedMemory(0x2000, 0xff); // 0xa000 - 0xbfff
+ private readonly Ram lowInternalRam = new Ram(0x2000); // 0xc000 - 0xdfff (mirrored at 0xe000)
+ private readonly UnusedMemory unmapped60 = new UnusedMemory(0x60, 0xff); // 0xfea0 - 0xfeff
+ private readonly Ram highInternalRam = new Ram(0x80); // 0xff80 - 0xffff
+
+ private bool enabledLCD = false;
+
+ private bool disabledGameRom = false;
+
+ private bool rom = false;
+ private bool banked = false;
+ private bool ram = false;
+ private bool battery = false;
+
+ private bool higherRomBank = true;
+ private bool ramBankSwitching = false;
+
+ private int romBank = 1;
+ private int ramBank = 0;
+
+ protected Bus()
+ {
+ this.IO = new IoRegisters(this);
+ this.CPU = new LR35902(this);
+ }
+
+ public LR35902 CPU { get; }
+
+ public Ram VRAM { get; } = new Ram(0x2000);
+
+ public Ram OAMRAM { get; } = new Ram(0xa0);
+
+ public IoRegisters IO { get; }
+
+ public bool GameRomDisabled => this.disabledGameRom;
+
+ public bool GameRomEnabled => !this.GameRomDisabled;
+
+ public override void RaisePOWER()
+ {
+ base.RaisePOWER();
+ this.CPU.RaisePOWER();
+ this.CPU.RaiseINT();
+ this.Reset();
+ }
+
+ public override void LowerPOWER()
+ {
+ this.CPU.LowerPOWER();
+ base.LowerPOWER();
+ }
+
+ public void Reset()
+ {
+ this.IO.Reset();
+ this.CPU.LowerRESET();
+ }
+
+ public void DisableGameRom() => this.disabledGameRom = true;
+
+ public void EnableGameRom() => this.disabledGameRom = false;
+
+ public void LoadBootRom(string path) => this.bootRom.Load(path);
+
+ public void LoadGameRom(string path)
+ {
+ const int bankSize = 0x4000;
+ this.gameRomBanks.Clear();
+ this.gameRomBanks.Add(new Rom());
+ var size = this.gameRomBanks[0].Load(path, 0, 0, bankSize);
+ var banks = size / bankSize;
+ for (var bank = 1; bank < banks; ++bank)
+ {
+ this.gameRomBanks.Add(new Rom());
+ this.gameRomBanks[bank].Load(path, 0, bankSize * bank, bankSize);
+ }
+
+ this.ValidateCartridgeType();
+ }
+
+ public int RunRasterLines()
+ {
+ this.enabledLCD = (this.IO.Peek(IoRegisters.LCDC) & (byte)LcdcControl.LcdEnable) != 0;
+ this.IO.ResetLY();
+ return this.RunRasterLines(Display.RasterHeight);
+ }
+
+ public int RunVerticalBlankLines()
+ {
+ var lines = TotalLineCount - Display.RasterHeight;
+ return this.RunVerticalBlankLines(lines);
+ }
+
+ public override MemoryMapping Mapping(ushort address)
+ {
+ if ((address < 0x100) && this.IO.BootRomEnabled)
+ {
+ return new MemoryMapping(this.bootRom, 0x0000, Mask.Mask16, AccessLevel.ReadOnly);
+ }
+
+ if ((address < 0x4000) && this.GameRomEnabled)
+ {
+ return new MemoryMapping(this.gameRomBanks[0], 0x0000, 0xffff, AccessLevel.ReadOnly);
+ }
+
+ if ((address < 0x8000) && this.GameRomEnabled)
+ {
+ return new MemoryMapping(this.gameRomBanks[this.romBank], 0x4000, 0xffff, AccessLevel.ReadOnly);
+ }
+
+ if (address < 0xa000)
+ {
+ return new MemoryMapping(this.VRAM, 0x8000, 0xffff, AccessLevel.ReadWrite);
+ }
+
+ if (address < 0xc000)
+ {
+ if (this.ramBanks.Count == 0)
+ {
+ return new MemoryMapping(this.unmapped2000, 0xa000, 0xffff, AccessLevel.ReadOnly);
+ }
+ else
+ {
+ return new MemoryMapping(this.ramBanks[this.ramBank], 0xa000, 0xffff, AccessLevel.ReadWrite);
+ }
+ }
+
+ if (address < 0xe000)
+ {
+ return new MemoryMapping(this.lowInternalRam, 0xc000, 0xffff, AccessLevel.ReadWrite);
+ }
+
+ if (address < 0xfe00)
+ {
+ return new MemoryMapping(this.lowInternalRam, 0xe000, 0xffff, AccessLevel.ReadWrite); // Low internal RAM mirror
+ }
+
+ if (address < 0xfea0)
+ {
+ return new MemoryMapping(this.OAMRAM, 0xfe00, 0xffff, AccessLevel.ReadWrite);
+ }
+
+ if (address < IoRegisters.BASE)
+ {
+ return new MemoryMapping(this.unmapped60, 0xfea0, 0xffff, AccessLevel.ReadOnly);
+ }
+
+ if (address < 0xff80)
+ {
+ return new MemoryMapping(this.IO, IoRegisters.BASE, 0xffff, AccessLevel.ReadWrite);
+ }
+
+ return new MemoryMapping(this.highInternalRam, 0xff80, 0xffff, AccessLevel.ReadWrite);
+ }
+
+ private void ValidateCartridgeType()
+ {
+ this.rom = this.banked = this.ram = this.battery = false;
+
+ // ROM type
+ switch (this.gameRomBanks[0].Peek(0x147))
+ {
+ case (byte)CartridgeType.ROM:
+ this.rom = true;
+ break;
+ case (byte)CartridgeType.ROM_MBC1:
+ this.rom = this.banked = true;
+ break;
+ case (byte)CartridgeType.ROM_MBC1_RAM:
+ this.rom = this.banked = this.ram = true;
+ break;
+ case (byte)CartridgeType.ROM_MBC1_RAM_BATTERY:
+ this.rom = this.banked = this.ram = this.battery = true;
+ break;
+ default:
+ throw new InvalidOperationException("Unhandled cartridge ROM type");
+ }
+
+ // ROM size
+ {
+ var gameRomBanks = 0;
+ var romSizeSpecification = this.Peek(0x148);
+ switch (romSizeSpecification)
+ {
+ case 0x52:
+ gameRomBanks = 72;
+ break;
+ case 0x53:
+ gameRomBanks = 80;
+ break;
+ case 0x54:
+ gameRomBanks = 96;
+ break;
+ default:
+ if (romSizeSpecification > 6)
+ {
+ throw new InvalidOperationException("Invalid ROM size specification");
+ }
+
+ gameRomBanks = 1 << (romSizeSpecification + 1);
+ if (gameRomBanks != this.gameRomBanks.Count)
+ {
+ throw new InvalidOperationException("ROM size specification mismatch");
+ }
+
+ break;
+ }
+
+ // RAM size
+ {
+ var ramSizeSpecification = this.gameRomBanks[0].Peek(0x149);
+ switch (ramSizeSpecification)
+ {
+ case 0:
+ break;
+ case 1:
+ this.ramBanks.Clear();
+ this.ramBanks.Add(new Ram(2 * 1024));
+ break;
+ case 2:
+ this.ramBanks.Clear();
+ this.ramBanks.Add(new Ram(8 * 1024));
+ break;
+ case 3:
+ this.ramBanks.Clear();
+ for (var i = 0; i < 4; ++i)
+ {
+ this.ramBanks.Add(new Ram(8 * 1024));
+ }
+
+ break;
+ case 4:
+ this.ramBanks.Clear();
+ for (var i = 0; i < 16; ++i)
+ {
+ this.ramBanks.Add(new Ram(8 * 1024));
+ }
+
+ break;
+ default:
+ throw new InvalidOperationException("Invalid RAM size specification");
+ }
+ }
+ }
+ }
+
+ protected override void OnWrittenByte()
+ {
+ base.OnWrittenByte();
+
+ var address = this.Address.Word;
+ var value = this.Data;
+
+ switch (address & 0xe000)
+ {
+ case 0x0000:
+ // Register 0: RAMCS gate data
+ if (this.ram)
+ {
+ throw new InvalidOperationException("Register 0: RAMCS gate data: Not handled!");
+ }
+ break;
+ case 0x2000:
+ // Register 1: ROM bank code
+ if (this.banked && this.higherRomBank)
+ {
+ //assert((address >= 0x2000) && (address < 0x4000));
+ //assert((value > 0) && (value < 0x20));
+ this.romBank = value & (byte)Mask.Mask5;
+ }
+ break;
+ case 0x4000:
+ // Register 2: ROM bank selection
+ if (this.banked)
+ {
+ throw new InvalidOperationException("Register 2: ROM bank selection: Not handled!");
+ }
+ break;
+ case 0x6000:
+ // Register 3: ROM/RAM change
+ if (this.banked)
+ {
+ switch (value & (byte)Mask.Mask1)
+ {
+ case 0:
+ this.higherRomBank = true;
+ this.ramBankSwitching = false;
+ break;
+ case 1:
+ this.higherRomBank = false;
+ this.ramBankSwitching = true;
+ break;
+ default:
+ throw new InvalidOperationException("Unreachable");
+ }
+ }
+ break;
+ }
+ }
+
+ private int RunRasterLines(int lines)
+ {
+ var count = 0;
+ var allowed = CyclesPerLine;
+ for (var line = 0; line < lines; ++line)
+ {
+ var executed = this.RunRasterLine(allowed);
+ count += executed;
+ allowed = CyclesPerLine - (executed - CyclesPerLine);
+ }
+
+ return count;
+ }
+
+ private int RunVerticalBlankLines(int lines)
+ {
+ /*
+ Vertical Blank interrupt is triggered when the LCD
+ controller enters the VBL screen mode (mode 1, LY=144).
+ This happens once per frame, so this interrupt is
+ triggered 59.7 times per second. During this period the
+ VRAM and OAM can be accessed freely, so it's the best
+ time to update graphics (for example, use the OAM DMA to
+ update sprites for next frame, or update tiles to make
+ animations).
+ This period lasts 4560 clocks in normal speed mode and
+ 9120 clocks in double speed mode. That's exactly the
+ time needed to draw 10 scanlines.
+ The VBL interrupt isn't triggered when the LCD is
+ powered off or on, even when it was on VBL mode.
+ It's only triggered when the VBL period starts.
+ */
+ if (this.enabledLCD)
+ {
+ this.IO.UpdateLcdStatusMode(LcdStatusMode.VBlank);
+ if ((this.IO.Peek(IoRegisters.STAT) & (byte)Bits.Bit4) != 0)
+ {
+ this.IO.TriggerInterrupt(Interrupts.DisplayControlStatus);
+ }
+
+ this.IO.TriggerInterrupt(Interrupts.VerticalBlank);
+ }
+ return this.RunRasterLines(lines);
+ }
+
+ private int RunRasterLine(int limit)
+ {
+ /*
+ A scanline normally takes 456 clocks (912 clocks in double speed
+ mode) to complete. A scanline starts in mode 2, then goes to
+ mode 3 and, when the LCD controller has finished drawing the
+ line (the timings depend on lots of things) it goes to mode 0.
+ During lines 144-153 the LCD controller is in mode 1.
+ Line 153 takes only a few clocks to complete (the exact
+ timings are below). The rest of the clocks of line 153 are
+ spent in line 0 in mode 1!
+
+ During mode 0 and mode 1 the CPU can access both VRAM and OAM.
+ During mode 2 the CPU can only access VRAM, not OAM.
+ During mode 3 OAM and VRAM can't be accessed.
+ In GBC mode the CPU can't access Palette RAM(FF69h and FF6Bh)
+ during mode 3.
+ A scanline normally takes 456 clocks(912 clocks in double speed mode) to complete.
+ A scanline starts in mode 2, then goes to mode 3 and , when the LCD controller has
+ finished drawing the line(the timings depend on lots of things) it goes to mode 0.
+ During lines 144 - 153 the LCD controller is in mode 1.
+ Line 153 takes only a few clocks to complete(the exact timings are below).
+ The rest of the clocks of line 153 are spent in line 0 in mode 1!
+ */
+
+ var count = 0;
+ if (this.enabledLCD)
+ {
+ if (((this.IO.Peek(IoRegisters.STAT) & (byte)Bits.Bit6) != 0) && (this.IO.Peek(IoRegisters.LYC) == this.IO.Peek(IoRegisters.LY)))
+ {
+ this.IO.TriggerInterrupt(Interrupts.DisplayControlStatus);
+ }
+
+ // Mode 2, OAM unavailable
+ this.IO.UpdateLcdStatusMode(LcdStatusMode.SearchingOamRam);
+ if ((this.IO.Peek(IoRegisters.STAT) & (byte)Bits.Bit5) != 0)
+ {
+ this.IO.TriggerInterrupt(Interrupts.DisplayControlStatus);
+ }
+
+ count += this.CPU.Run(80); // ~19us
+
+ // Mode 3, OAM/VRAM unavailable
+ this.IO.UpdateLcdStatusMode(LcdStatusMode.TransferringDataToLcd);
+ count += this.CPU.Run(170); // ~41us
+
+ // Mode 0
+ this.IO.UpdateLcdStatusMode(LcdStatusMode.HBlank);
+ if ((this.IO.Peek(IoRegisters.STAT) & (byte)Bits.Bit3) != 0)
+ {
+ this.IO.TriggerInterrupt(Interrupts.DisplayControlStatus);
+ }
+
+ count += this.CPU.Run(limit - count); // ~48.6us
+
+ this.IO.IncrementLY();
+ }
+ else
+ {
+ count += this.CPU.Run(CyclesPerLine);
+ }
+
+ return count;
+ }
+ }
+ }
+}
diff --git a/LR35902/Interrupts.cs b/LR35902/Interrupts.cs
new file mode 100644
index 0000000..ed56783
--- /dev/null
+++ b/LR35902/Interrupts.cs
@@ -0,0 +1,20 @@
+//
+// Copyright (c) Adrian Conlon. All rights reserved.
+//
+namespace EightBit
+{
+ namespace GameBoy
+ {
+ // IF and IE flags
+ [System.Flags]
+ public enum Interrupts
+ {
+ None = 0,
+ VerticalBlank = Bits.Bit0, // VBLANK
+ DisplayControlStatus = Bits.Bit1, // LCDC Status
+ TimerOverflow = Bits.Bit2, // Timer Overflow
+ SerialTransfer = Bits.Bit3, // Serial Transfer
+ KeypadPressed = Bits.Bit4 // Hi-Lo transition of P10-P13
+ }
+ }
+}
diff --git a/LR35902/IoRegisters.cs b/LR35902/IoRegisters.cs
new file mode 100644
index 0000000..1828177
--- /dev/null
+++ b/LR35902/IoRegisters.cs
@@ -0,0 +1,396 @@
+//
+// Copyright (c) Adrian Conlon. All rights reserved.
+//
+namespace EightBit
+{
+ namespace GameBoy
+ {
+ using System;
+
+ public sealed class IoRegisters : EightBit.Ram
+ {
+ public const int BASE = 0xFF00;
+
+ // Port/Mode Registers
+ public const int P1 = 0x0; // R/W Mask5
+ public const int SB = 0x1; // R/W Mask8
+ public const int SC = 0x2; // R/W Bit7 | Bit0
+
+ // Timer control
+ public const int DIV = 0x4; // R/W Mask8
+ public const int TIMA = 0x5; // R/W Mask8
+ public const int TMA = 0x6; // R/W Mask8
+ public const int TAC = 0x7; // R/W Mask3
+
+ // Interrupt Flags
+ public const int IF = 0xF; // R/W Mask5
+ public const int IE = 0xFF; // R/W Mask5
+
+ // Sound Registers
+ public const int NR10 = 0x10; // R/W Mask7
+ public const int NR11 = 0x11; // R/W Bit7 | Bit6
+ public const int NR12 = 0x12; // R/W Mask8
+ public const int NR13 = 0x13; // W 0
+ public const int NR14 = 0x14; // R/W Bit6
+ public const int NR21 = 0x16; // R/W Bit7 | Bit6
+ public const int NR22 = 0x17; // R/W Mask8
+ public const int NR23 = 0x18; // W 0
+ public const int NR24 = 0x19; // R/W Bit6
+ public const int NR30 = 0x1A; // R/W Bit7
+ public const int NR31 = 0x1B; // R/W Mask8
+ public const int NR32 = 0x1C; // R/W Bit6 | Bit5
+ public const int NR33 = 0x1D; // W 0
+ public const int NR34 = 0x1E; // R/W Bit6
+ public const int NR41 = 0x20; // R/W Mask6
+ public const int NR42 = 0x21; // R/W Mask8
+ public const int NR43 = 0x22; // R/W Mask8
+ public const int NR44 = 0x23; // R/W Bit6
+ public const int NR50 = 0x24; // R/W Mask8
+ public const int NR51 = 0x25; // R/W Mask8
+ public const int NR52 = 0x26; // R/W Mask8 Mask8
+
+ public const int WAVE_PATTERN_RAM_START = 0x30;
+ public const int WAVE_PATTERN_RAM_END = 0x3F;
+
+ // LCD Display Registers
+ public const int LCDC = 0x40; // R/W Mask8
+ public const int STAT = 0x41; // R/W Mask7
+ public const int SCY = 0x42; // R/W Mask8
+ public const int SCX = 0x43; // R/W Mask8
+ public const int LY = 0x44; // R Mask8 zeroed
+ public const int LYC = 0x45; // R/W Mask8
+ public const int DMA = 0x46; // W 0
+ public const int BGP = 0x47; // R/W Mask8
+ public const int OBP0 = 0x48; // R/W Mask8
+ public const int OBP1 = 0x49; // R/W Mask8
+ public const int WY = 0x4A; // R/W Mask8
+ public const int WX = 0x4B; // R/W Mask8
+
+ // Boot rom control
+ public const int BOOT_DISABLE = 0x50;
+
+ private readonly Bus bus;
+ private readonly Register16 divCounter = new Register16(0xab, 0xcc);
+ private int timerCounter = 0;
+ private int timerRate = 0;
+
+ private readonly Register16 dmaAddress = new Register16();
+ private bool dmaTransferActive = false;
+
+ private bool scanP15 = false;
+ private bool scanP14 = false;
+
+ private bool p15 = true; // misc keys
+ private bool p14 = true; // direction keys
+ private bool p13 = true; // down/start
+ private bool p12 = true; // up/select
+ private bool p11 = true; // left/b
+ private bool p10 = true; // right/a
+
+ public IoRegisters(Bus bus)
+ : base(0x80)
+ {
+ this.bus = bus;
+ this.bus.ReadingByte += this.Bus_ReadingByte;
+ this.bus.WrittenByte += this.Bus_WrittenByte;
+ }
+
+ public event EventHandler DisplayStatusModeUpdated;
+
+ public bool BootRomDisabled { get; private set; } = false;
+
+ public bool BootRomEnabled => !this.BootRomDisabled;
+
+ public int TimerClock => this.Peek((ushort)TAC) & (byte)Mask.Mask2;
+
+ public bool TimerEnabled => !this.TimerDisabled;
+
+ public bool TimerDisabled => (this.Peek((ushort)TAC) & (byte)Bits.Bit2) == 0;
+
+ public void Reset()
+ {
+ this.Poke((ushort)NR52, 0xf1);
+ this.Poke((ushort)LCDC, (byte)(LcdcControl.DisplayBackground | LcdcControl.BackgroundCharacterDataSelection | LcdcControl.LcdEnable));
+ this.divCounter.Word = 0xabcc;
+ this.timerCounter = 0;
+ }
+
+ public void TransferDma()
+ {
+ if (this.dmaTransferActive)
+ {
+ this.bus.OAMRAM.Poke(this.dmaAddress.Low, this.bus.Peek(this.dmaAddress));
+ this.dmaTransferActive = ++this.dmaAddress.Low < 0xa0;
+ }
+ }
+
+ public void TriggerInterrupt(Interrupts cause) => this.Poke((ushort)IF, (byte)(this.Peek((ushort)IF) | (byte)cause));
+
+ public void CheckTimers(int cycles)
+ {
+ this.IncrementDIV(cycles);
+ this.CheckTimer(cycles);
+ }
+
+ public int TimerClockTicks
+ {
+ get
+ {
+ switch (this.TimerClock)
+ {
+ case 0b00:
+ return 1024; // 4.096 Khz
+ case 0b01:
+ return 16; // 262.144 Khz
+ case 0b10:
+ return 64; // 65.536 Khz
+ case 0b11:
+ return 256; // 16.384 Khz
+ }
+ throw new InvalidOperationException("Invalid timer clock specification");
+ }
+ }
+
+ public void IncrementDIV(int cycles)
+ {
+ this.divCounter.Word += (ushort)cycles;
+ this.Poke((ushort)DIV, this.divCounter.High);
+ }
+
+ public void IncrementTIMA()
+ {
+ var updated = this.Peek((ushort)TIMA) + 1;
+ if ((updated & (int)Bits.Bit8) != 0)
+ {
+ this.TriggerInterrupt(Interrupts.TimerOverflow);
+ updated = this.Peek((ushort)TMA);
+ }
+
+ this.Poke((ushort)TIMA, Chip.LowByte(updated));
+ }
+
+ public void IncrementLY() => this.Poke((ushort)LY, (byte)((this.Peek((ushort)LY) + 1) % GameBoy.Bus.TotalLineCount));
+
+ public void ResetLY() => this.Poke((ushort)LY, 0);
+
+ public void UpdateLcdStatusMode(LcdStatusMode mode)
+ {
+ var current = this.Peek((ushort)STAT) & unchecked((byte)~Mask.Mask2);
+ this.Poke((ushort)STAT, (byte)(current | (int)mode));
+ this.OnDisplayStatusModeUpdated(mode);
+ }
+
+ public void DisableBootRom() => this.BootRomDisabled = true;
+
+ public void EnableBootRom() => this.BootRomDisabled = false;
+
+ public void PressRight()
+ {
+ this.p14 = this.p10 = false;
+ this.TriggerKeypadInterrupt();
+ }
+
+ public void ReleaseRight() => this.p14 = this.p10 = true;
+
+ public void PressLeft()
+ {
+ this.p14 = this.p11 = false;
+ this.TriggerKeypadInterrupt();
+ }
+
+ public void ReleaseLeft() => this.p14 = this.p11 = true;
+
+ public void PressUp()
+ {
+ this.p14 = this.p12 = false;
+ this.TriggerKeypadInterrupt();
+ }
+
+ public void ReleaseUp() => this.p14 = this.p12 = true;
+
+ public void PressDown()
+ {
+ this.p14 = this.p13 = false;
+ this.TriggerKeypadInterrupt();
+ }
+
+ public void ReleaseDown() => this.p14 = this.p13 = true;
+
+ public void PressA()
+ {
+ this.p15 = this.p10 = false;
+ this.TriggerKeypadInterrupt();
+ }
+
+ public void ReleaseA() => this.p15 = this.p10 = true;
+
+ public void PressB()
+ {
+ this.p15 = this.p11 = false;
+ this.TriggerKeypadInterrupt();
+ }
+
+ public void ReleaseB() => this.p15 = this.p11 = true;
+
+ public void PressSelect()
+ {
+ this.p15 = this.p12 = false;
+ this.TriggerKeypadInterrupt();
+ }
+
+ public void ReleaseSelect() => this.p15 = this.p12 = true;
+
+ public void PressStart()
+ {
+ this.p15 = this.p13 = false;
+ this.TriggerKeypadInterrupt();
+ }
+
+ public void ReleaseStart() => this.p15 = this.p13 = true;
+
+ private void OnDisplayStatusModeUpdated(LcdStatusMode mode) => this.DisplayStatusModeUpdated?.Invoke(this, new LcdStatusModeEventArgs(mode));
+
+ private void CheckTimer(int cycles)
+ {
+ if (this.TimerEnabled)
+ {
+ this.timerCounter -= cycles;
+ if (this.timerCounter <= 0)
+ {
+ this.timerCounter += this.timerRate;
+ this.IncrementTIMA();
+ }
+ }
+ }
+
+ private void ApplyMask(ushort address, byte masking) => this.Poke(address, (byte)(this.Peek(address) | ~masking));
+
+ private void TriggerKeypadInterrupt() => this.TriggerInterrupt(Interrupts.KeypadPressed);
+
+ private void Bus_WrittenByte(object sender, System.EventArgs e)
+ {
+ var address = this.bus.Address.Word;
+ var value = this.bus.Data;
+ var port = (ushort)(address - BASE);
+
+ switch (port)
+ {
+ case P1:
+ this.scanP14 = (value & (byte)Bits.Bit4) == 0;
+ this.scanP15 = (value & (byte)Bits.Bit5) == 0;
+ break;
+
+ case SB: // R/W
+ case SC: // R/W
+ break;
+
+ case DIV: // R/W
+ this.Poke(port, 0);
+ this.timerCounter = this.divCounter.Word = 0;
+ break;
+ case TIMA: // R/W
+ case TMA: // R/W
+ break;
+ case TAC: // R/W
+ this.timerRate = this.TimerClockTicks;
+ break;
+
+ case IF: // R/W
+ break;
+
+ case LCDC:
+ case STAT:
+ case SCY:
+ case SCX:
+ break;
+ case DMA:
+ this.dmaAddress.Word = Chip.PromoteByte(value);
+ this.dmaTransferActive = true;
+ break;
+ case LY: // R/O
+ this.Poke(port, 0);
+ break;
+ case BGP:
+ case OBP0:
+ case OBP1:
+ case WY:
+ case WX:
+ break;
+
+ case BOOT_DISABLE:
+ this.BootRomDisabled = value != 0;
+ break;
+ }
+ }
+
+ private void Bus_ReadingByte(object sender, System.EventArgs e)
+ {
+ var address = this.bus.Address.Word;
+ var io = (address >= BASE) && (address < 0xff80);
+ if (io)
+ {
+ var port = (ushort)(address - BASE);
+ switch (port)
+ {
+ // Port/Mode Registers
+ case P1:
+ {
+ var p14 = this.scanP14 && !this.p14;
+ var p15 = this.scanP15 && !this.p15;
+ var live = p14 || p15;
+ var p10 = live && this.p10 ? 1 : 0;
+ var p11 = live && this.p11 ? 1 : 0;
+ var p12 = live && this.p12 ? 1 : 0;
+ var p13 = live && this.p13 ? 1 : 0;
+ this.Poke(port,
+ (byte)(p10 | (p11 << 1) | (p12 << 2) | (p13 << 3)
+ | (int)(Bits.Bit4 | Bits.Bit5 | Bits.Bit6 | Bits.Bit7)));
+ }
+ break;
+ case SB:
+ break;
+ case SC:
+ this.ApplyMask(port, (byte)(Bits.Bit7 | Bits.Bit0));
+ break;
+
+ // Timer control
+ case DIV:
+ case TIMA:
+ case TMA:
+ break;
+ case TAC:
+ this.ApplyMask(port, (byte)Mask.Mask3);
+ break;
+
+ // Interrupt Flags
+ case IF:
+ this.ApplyMask(port, (byte)Mask.Mask5);
+ break;
+
+ // LCD Display Registers
+ case LCDC:
+ break;
+ case STAT:
+ this.ApplyMask(port, (byte)Mask.Mask7);
+ break;
+ case SCY:
+ case SCX:
+ case LY:
+ case LYC:
+ case DMA:
+ case BGP:
+ case OBP0:
+ case OBP1:
+ case WY:
+ case WX:
+ break;
+
+ default:
+ this.ApplyMask(port, 0);
+ break;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/LR35902/LR35902.cs b/LR35902/LR35902.cs
new file mode 100644
index 0000000..6e11cd3
--- /dev/null
+++ b/LR35902/LR35902.cs
@@ -0,0 +1,1130 @@
+//
+// Copyright (c) Adrian Conlon. All rights reserved.
+//
+
+namespace EightBit
+{
+ namespace GameBoy
+ {
+ using System;
+
+ public class LR35902 : IntelProcessor
+ {
+ private readonly Bus bus;
+ private bool prefixCB = false;
+
+ public LR35902(Bus bus)
+ : base(bus) => this.bus = bus;
+
+ public event EventHandler ExecutingInstruction;
+
+ public event EventHandler ExecutedInstruction;
+
+ public int ClockCycles => this.Cycles * 4;
+
+ public override Register16 AF { get; } = new Register16((int)Mask.Mask16);
+
+ public override Register16 BC { get; } = new Register16((int)Mask.Mask16);
+
+ public override Register16 DE { get; } = new Register16((int)Mask.Mask16);
+
+ public override Register16 HL { get; } = new Register16((int)Mask.Mask16);
+
+ private bool IME { get; set; } = false;
+
+ private bool Stopped { get; set; } = false;
+
+ public override int Execute()
+ {
+ var decoded = this.GetDecodedOpCode(this.OpCode);
+
+ var x = decoded.X;
+ var y = decoded.Y;
+ var z = decoded.Z;
+
+ var p = decoded.P;
+ var q = decoded.Q;
+
+ if (this.prefixCB)
+ {
+ this.ExecuteCB(x, y, z);
+ }
+ else
+ {
+ this.ExecuteOther(x, y, z, p, q);
+ }
+
+ return this.ClockCycles;
+ }
+
+ public override int Step()
+ {
+ this.OnExecutingInstruction();
+ this.prefixCB = false;
+ this.ResetCycles();
+ if (this.Powered)
+ {
+ var interruptEnable = this.Bus.Peek(IoRegisters.BASE + IoRegisters.IE);
+ var interruptFlags = this.bus.IO.Peek(IoRegisters.IF);
+
+ var masked = interruptEnable & interruptFlags;
+ if (masked != 0)
+ {
+ if (this.IME)
+ {
+ this.bus.IO.Poke(IoRegisters.IF, 0);
+ this.LowerINT();
+ var index = Chip.FindFirstSet(masked);
+ this.Bus.Data = (byte)(0x38 + (index << 3));
+ }
+ else
+ {
+ if (this.Halted)
+ {
+ this.Proceed();
+ }
+ }
+ }
+
+ if (this.RESET.Lowered())
+ {
+ this.HandleRESET();
+ }
+ else if (this.INT.Lowered())
+ {
+ this.HandleINT();
+ }
+ else if (this.Halted)
+ {
+ this.Execute(0); // NOP
+ }
+ else
+ {
+ this.Execute(this.FetchByte());
+ }
+
+ this.bus.IO.CheckTimers(this.ClockCycles);
+ this.bus.IO.TransferDma();
+ }
+
+ this.OnExecutedInstruction();
+ return this.ClockCycles;
+ }
+
+ protected virtual void OnExecutingInstruction() => this.ExecutingInstruction?.Invoke(this, EventArgs.Empty);
+
+ protected virtual void OnExecutedInstruction() => this.ExecutedInstruction?.Invoke(this, EventArgs.Empty);
+
+ protected override void HandleRESET()
+ {
+ base.HandleRESET();
+ this.DI();
+ this.SP.Word = (ushort)(Mask.Mask16 - 1);
+ this.Tick(4);
+ }
+
+ protected override void HandleINT()
+ {
+ base.HandleINT();
+ this.RaiseHALT();
+ this.DI();
+ this.Restart(this.Bus.Data);
+ this.Tick(4);
+ }
+
+ private static byte SetBit(byte f, StatusBits flag) => SetBit(f, (byte)flag);
+
+ private static byte SetBit(byte f, StatusBits flag, int condition) => SetBit(f, (byte)flag, condition);
+
+ private static byte SetBit(byte f, StatusBits flag, bool condition) => SetBit(f, (byte)flag, condition);
+
+ private static byte ClearBit(byte f, StatusBits flag) => ClearBit(f, (byte)flag);
+
+ private static byte ClearBit(byte f, StatusBits flag, int condition) => ClearBit(f, (byte)flag, condition);
+
+ private static byte AdjustZero(byte input, byte value) => ClearBit(input, StatusBits.ZF, value);
+
+ private static byte AdjustHalfCarryAdd(byte input, byte before, byte value, int calculation) => SetBit(input, StatusBits.HC, CalculateHalfCarryAdd(before, value, calculation));
+
+ private static byte AdjustHalfCarrySub(byte input, byte before, byte value, int calculation) => SetBit(input, StatusBits.HC, CalculateHalfCarrySub(before, value, calculation));
+
+ private void DI() => this.IME = false;
+
+ private void EI() => this.IME = true;
+
+ private void Stop() => this.Stopped = true;
+
+ private void Start() => this.Stopped = false;
+
+ private byte R(int r)
+ {
+ switch (r)
+ {
+ case 0:
+ return this.B;
+ case 1:
+ return this.C;
+ case 2:
+ return this.D;
+ case 3:
+ return this.E;
+ case 4:
+ return this.H;
+ case 5:
+ return this.L;
+ case 6:
+ return this.BusRead(this.HL.Word);
+ case 7:
+ return this.A;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(r));
+ }
+ }
+
+ private void R(int r, byte value)
+ {
+ switch (r)
+ {
+ case 0:
+ this.B = value;
+ break;
+ case 1:
+ this.C = value;
+ break;
+ case 2:
+ this.D = value;
+ break;
+ case 3:
+ this.E = value;
+ break;
+ case 4:
+ this.H = value;
+ break;
+ case 5:
+ this.L = value;
+ break;
+ case 6:
+ this.BusWrite(this.HL.Word, value);
+ break;
+ case 7:
+ this.A = value;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(r));
+ }
+ }
+
+ private Register16 RP(int rp)
+ {
+ switch (rp)
+ {
+ case 0:
+ return this.BC;
+ case 1:
+ return this.DE;
+ case 2:
+ return this.HL;
+ case 3:
+ return this.SP;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(rp));
+ }
+ }
+
+ private Register16 RP2(int rp)
+ {
+ switch (rp)
+ {
+ case 0:
+ return this.BC;
+ case 1:
+ return this.DE;
+ case 2:
+ return this.HL;
+ case 3:
+ return this.AF;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(rp));
+ }
+ }
+
+ private void ExecuteCB(int x, int y, int z)
+ {
+ switch (x)
+ {
+ case 0: // rot[y] r[z]
+ {
+ var operand = this.R(z);
+ switch (y)
+ {
+ case 0:
+ operand = this.RLC(operand);
+ break;
+ case 1:
+ operand = this.RRC(operand);
+ break;
+ case 2:
+ operand = this.RL(operand);
+ break;
+ case 3:
+ operand = this.RR(operand);
+ break;
+ case 4:
+ operand = this.SLA(operand);
+ break;
+ case 5:
+ operand = this.SRA(operand);
+ break;
+ case 6: // GB: SWAP r
+ operand = this.Swap(operand);
+ break;
+ case 7:
+ operand = this.SRL(operand);
+ break;
+ default:
+ throw new InvalidOperationException("Unreachable code block reached");
+ }
+
+ this.Tick(2);
+ this.R(z, operand);
+ this.F = AdjustZero(this.F, operand);
+ if (z == 6)
+ {
+ this.Tick(2);
+ }
+
+ break;
+ }
+
+ case 1: // BIT y, r[z]
+ this.Bit(y, this.R(z));
+ this.Tick(2);
+ if (z == 6)
+ {
+ this.Tick(2);
+ }
+
+ break;
+
+ case 2: // RES y, r[z]
+ this.R(z, Res(y, this.R(z)));
+ this.Tick(2);
+ if (z == 6)
+ {
+ this.Tick(2);
+ }
+
+ break;
+
+ case 3: // SET y, r[z]
+ this.R(z, Set(y, this.R(z)));
+ this.Tick(2);
+ if (z == 6)
+ {
+ this.Tick(2);
+ }
+
+ break;
+
+ default:
+ throw new InvalidOperationException("Unreachable code block reached");
+ }
+ }
+
+ private void ExecuteOther(int x, int y, int z, int p, int q)
+ {
+ switch (x)
+ {
+ case 0:
+ switch (z)
+ {
+ case 0: // Relative jumps and assorted ops
+ switch (y)
+ {
+ case 0: // NOP
+ this.Tick(4);
+ break;
+ case 1: // GB: LD (nn),SP
+ this.Bus.Address.Word = this.FetchWord().Word;
+ this.SetWord(this.SP);
+ this.Tick(5);
+ break;
+ case 2: // GB: STOP
+ this.Stop();
+ this.Tick();
+ break;
+ case 3: // JR d
+ this.JumpRelative((sbyte)this.FetchByte());
+ this.Tick(4);
+ break;
+ case 4: // JR cc,d
+ case 5:
+ case 6:
+ case 7:
+ if (this.JumpRelativeConditionalFlag(y - 4))
+ {
+ this.Tick();
+ }
+
+ this.Tick(2);
+ break;
+ default:
+ throw new InvalidOperationException("Unreachable code block reached");
+ }
+
+ break;
+
+ case 1: // 16-bit load immediate/add
+ switch (q)
+ {
+ case 0: // LD rp,nn
+ this.RP(p).Word = this.FetchWord().Word;
+ this.Tick(3);
+ break;
+
+ case 1: // ADD HL,rp
+ this.Add(this.HL, this.RP(p));
+ this.Tick(2);
+ break;
+
+ default:
+ throw new NotSupportedException("Invalid operation mode");
+ }
+
+ break;
+
+ case 2: // Indirect loading
+ switch (q)
+ {
+ case 0:
+ switch (p)
+ {
+ case 0: // LD (BC),A
+ this.BusWrite(this.BC, this.A);
+ this.Tick(2);
+ break;
+
+ case 1: // LD (DE),A
+ this.BusWrite(this.DE, this.A);
+ this.Tick(2);
+ break;
+
+ case 2: // GB: LDI (HL),A
+ this.BusWrite(this.HL.Word++, this.A);
+ this.Tick(2);
+ break;
+
+ case 3: // GB: LDD (HL),A
+ this.BusWrite(this.HL.Word--, this.A);
+ this.Tick(2);
+ break;
+
+ default:
+ throw new NotSupportedException("Invalid operation mode");
+ }
+
+ break;
+
+ case 1:
+ switch (p)
+ {
+ case 0: // LD A,(BC)
+ this.A = this.BusRead(this.BC);
+ this.Tick(2);
+ break;
+
+ case 1: // LD A,(DE)
+ this.A = this.BusRead(this.DE);
+ this.Tick(2);
+ break;
+
+ case 2: // GB: LDI A,(HL)
+ this.A = this.BusRead(this.HL.Word++);
+ this.Tick(2);
+ break;
+
+ case 3: // GB: LDD A,(HL)
+ this.A = this.BusRead(this.HL.Word--);
+ this.Tick(2);
+ break;
+
+ default:
+ throw new NotSupportedException("Invalid operation mode");
+ }
+
+ break;
+
+ default:
+ throw new NotSupportedException("Invalid operation mode");
+ }
+
+ break;
+
+ case 3: // 16-bit INC/DEC
+ switch (q)
+ {
+ case 0: // INC rp
+ ++this.RP(p).Word;
+ break;
+
+ case 1: // DEC rp
+ --this.RP(p).Word;
+ break;
+
+ default:
+ throw new NotSupportedException("Invalid operation mode");
+ }
+
+ this.Tick(2);
+ break;
+
+ case 4: // 8-bit INC
+ this.R(y, this.Increment(this.R(y)));
+ this.Tick();
+ if (y == 6)
+ {
+ this.Tick(2);
+ }
+
+ break;
+
+ case 5: // 8-bit DEC
+ this.R(y, this.Decrement(this.R(y)));
+ this.Tick();
+ if (y == 6)
+ {
+ this.Tick(2);
+ }
+
+ break;
+
+ case 6: // 8-bit load immediate
+ this.R(y, this.FetchByte());
+ this.Tick(2);
+ break;
+
+ case 7: // Assorted operations on accumulator/flags
+ switch (y)
+ {
+ case 0:
+ this.A = this.RLC(this.A);
+ break;
+ case 1:
+ this.A = this.RRC(this.A);
+ break;
+ case 2:
+ this.A = this.RL(this.A);
+ break;
+ case 3:
+ this.A = this.RR(this.A);
+ break;
+ case 4:
+ this.DAA();
+ break;
+ case 5:
+ this.Cpl();
+ break;
+ case 6:
+ this.SCF();
+ break;
+ case 7:
+ this.CCF();
+ break;
+ default:
+ throw new NotSupportedException("Invalid operation mode");
+ }
+
+ this.Tick(4);
+ break;
+
+ default:
+ throw new NotSupportedException("Invalid operation mode");
+ }
+
+ break;
+
+ case 1: // 8-bit loading
+ if (z == 6 && y == 6)
+ {
+ this.Halt(); // Exception (replaces LD (HL), (HL))
+ }
+ else
+ {
+ this.R(y, this.R(z));
+ if ((y == 6) || (z == 6))
+ {
+ this.Tick(); // M operations
+ }
+ }
+
+ this.Tick();
+ break;
+
+ case 2: // Operate on accumulator and register/memory location
+ switch (y)
+ {
+ case 0: // ADD A,r
+ this.A = this.Add(this.A, this.R(z));
+ break;
+ case 1: // ADC A,r
+ this.A = this.ADC(this.A, this.R(z));
+ break;
+ case 2: // SUB r
+ this.A = this.Subtract(this.A, this.R(z));
+ break;
+ case 3: // SBC A,r
+ this.A = this.SBC(this.A, this.R(z));
+ break;
+ case 4: // AND r
+ this.AndR(this.R(z));
+ break;
+ case 5: // XOR r
+ this.XorR(this.R(z));
+ break;
+ case 6: // OR r
+ this.OrR(this.R(z));
+ break;
+ case 7: // CP r
+ this.Compare(this.R(z));
+ break;
+ default:
+ throw new NotSupportedException("Invalid operation mode");
+ }
+
+ this.Tick();
+ if (z == 6)
+ {
+ this.Tick();
+ }
+
+ break;
+ case 3:
+ switch (z)
+ {
+ case 0: // Conditional return
+ switch (y)
+ {
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ if (this.ReturnConditionalFlag(y))
+ {
+ this.Tick(3);
+ }
+
+ this.Tick(2);
+ break;
+
+ case 4: // GB: LD (FF00 + n),A
+ this.BusWrite((ushort)(IoRegisters.BASE + this.FetchByte()), this.A);
+ this.Tick(3);
+ break;
+
+ case 5:
+ { // GB: ADD SP,dd
+ var before = this.SP.Word;
+ var value = (sbyte)this.FetchByte();
+ var result = before + value;
+ this.SP.Word = (ushort)result;
+ var carried = before ^ value ^ (result & (int)Mask.Mask16);
+ this.F = ClearBit(this.F, StatusBits.ZF | StatusBits.NF);
+ this.F = SetBit(this.F, StatusBits.CF, carried & (int)Bits.Bit8);
+ this.F = SetBit(this.F, StatusBits.HC, carried & (int)Bits.Bit4);
+ }
+
+ this.Tick(4);
+ break;
+
+ case 6: // GB: LD A,(FF00 + n)
+ this.A = this.BusRead((ushort)(IoRegisters.BASE + this.FetchByte()));
+ this.Tick(3);
+ break;
+
+ case 7:
+ { // GB: LD HL,SP + dd
+ var before = this.SP.Word;
+ var value = (sbyte)this.FetchByte();
+ var result = before + value;
+ this.HL.Word = (ushort)result;
+ var carried = before ^ value ^ (result & (int)Mask.Mask16);
+ this.F = ClearBit(this.F, StatusBits.ZF | StatusBits.NF);
+ this.F = SetBit(this.F, StatusBits.CF, carried & (int)Bits.Bit8);
+ this.F = SetBit(this.F, StatusBits.HC, carried & (int)Bits.Bit4);
+ }
+
+ this.Tick(3);
+ break;
+
+ default:
+ throw new NotSupportedException("Invalid operation mode");
+ }
+
+ break;
+ case 1: // POP & various ops
+ switch (q)
+ {
+ case 0: // POP rp2[p]
+ this.RP2(p).Word = this.PopWord().Word;
+ this.Tick(3);
+ break;
+ case 1:
+ switch (p)
+ {
+ case 0: // RET
+ this.Return();
+ this.Tick(4);
+ break;
+ case 1: // GB: RETI
+ this.RetI();
+ this.Tick(4);
+ break;
+ case 2: // JP HL
+ this.Jump(this.HL.Word);
+ this.Tick();
+ break;
+ case 3: // LD SP,HL
+ this.SP.Word = this.HL.Word;
+ this.Tick(2);
+ break;
+ default:
+ throw new NotSupportedException("Invalid operation mode");
+ }
+
+ break;
+
+ default:
+ throw new NotSupportedException("Invalid operation mode");
+ }
+
+ break;
+ case 2: // Conditional jump
+ switch (y)
+ {
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ this.JumpConditionalFlag(y);
+ this.Tick(3);
+ break;
+ case 4: // GB: LD (FF00 + C),A
+ this.BusWrite((ushort)(IoRegisters.BASE + this.C), this.A);
+ this.Tick(2);
+ break;
+ case 5: // GB: LD (nn),A
+ this.Bus.Address.Word = this.MEMPTR.Word = this.FetchWord().Word;
+ this.BusWrite(this.A);
+ this.Tick(4);
+ break;
+ case 6: // GB: LD A,(FF00 + C)
+ this.A = this.BusRead((ushort)(IoRegisters.BASE + this.C));
+ this.Tick(2);
+ break;
+ case 7: // GB: LD A,(nn)
+ this.Bus.Address.Word = this.MEMPTR.Word = this.FetchWord().Word;
+ this.A = this.BusRead();
+ this.Tick(4);
+ break;
+ default:
+ throw new NotSupportedException("Invalid operation mode");
+ }
+ break;
+ case 3: // Assorted operations
+ switch (y)
+ {
+ case 0: // JP nn
+ this.Jump(this.FetchWord().Word);
+ this.Tick(4);
+ break;
+ case 1: // CB prefix
+ this.prefixCB = true;
+ this.Execute(this.FetchByte());
+ break;
+ case 6: // DI
+ this.DI();
+ this.Tick();
+ break;
+ case 7: // EI
+ this.EI();
+ this.Tick();
+ break;
+ }
+
+ break;
+
+ case 4: // Conditional call: CALL cc[y], nn
+ if (this.CallConditionalFlag(y))
+ {
+ this.Tick(3);
+ }
+
+ this.Tick(3);
+ break;
+
+ case 5: // PUSH & various ops
+ switch (q)
+ {
+ case 0: // PUSH rp2[p]
+ this.PushWord(this.RP2(p));
+ this.Tick(4);
+ break;
+
+ case 1:
+ switch (p)
+ {
+ case 0: // CALL nn
+ this.Call(this.FetchWord().Word);
+ this.Tick(6);
+ break;
+ }
+
+ break;
+
+ default:
+ throw new NotSupportedException("Invalid operation mode");
+ }
+
+ break;
+
+ case 6: // Operate on accumulator and immediate operand: alu[y] n
+ switch (y)
+ {
+ case 0: // ADD A,n
+ this.A = this.Add(this.A, this.FetchByte());
+ break;
+ case 1: // ADC A,n
+ this.A = this.ADC(this.A, this.FetchByte());
+ break;
+ case 2: // SUB n
+ this.A = this.Subtract(this.A, this.FetchByte());
+ break;
+ case 3: // SBC A,n
+ this.A = this.SBC(this.A, this.FetchByte());
+ break;
+ case 4: // AND n
+ this.AndR(this.FetchByte());
+ break;
+ case 5: // XOR n
+ this.XorR(this.FetchByte());
+ break;
+ case 6: // OR n
+ this.OrR(this.FetchByte());
+ break;
+ case 7: // CP n
+ this.Compare(this.FetchByte());
+ break;
+ default:
+ throw new NotSupportedException("Invalid operation mode");
+ }
+
+ this.Tick(2);
+ break;
+
+ case 7: // Restart: RST y * 8
+ this.Restart((byte)(y << 3));
+ this.Tick(4);
+ break;
+
+ default:
+ throw new NotSupportedException("Invalid operation mode");
+ }
+
+ break;
+ }
+ }
+
+ private byte Increment(byte operand)
+ {
+ this.F = ClearBit(this.F, StatusBits.NF);
+ this.F = AdjustZero(this.F, ++operand);
+ this.F = ClearBit(this.F, StatusBits.HC, LowNibble(operand));
+ return operand;
+ }
+
+ private byte Decrement(byte operand)
+ {
+ this.F = SetBit(this.F, StatusBits.NF);
+ this.F = ClearBit(this.F, StatusBits.HC, LowNibble(operand));
+ this.F = AdjustZero(this.F, --operand);
+ return operand;
+ }
+
+ private bool JumpConditionalFlag(int flag)
+ {
+ switch (flag)
+ {
+ case 0: // NZ
+ return this.JumpConditional((this.F & (byte)StatusBits.ZF) == 0);
+ case 1: // Z
+ return this.JumpConditional((this.F & (byte)StatusBits.ZF) != 0);
+ case 2: // NC
+ return this.JumpConditional((this.F & (byte)StatusBits.CF) == 0);
+ case 3: // C
+ return this.JumpConditional((this.F & (byte)StatusBits.CF) != 0);
+ default:
+ throw new ArgumentOutOfRangeException(nameof(flag));
+ }
+ }
+
+ private bool JumpRelativeConditionalFlag(int flag)
+ {
+ switch (flag)
+ {
+ case 0: // NZ
+ return this.JumpRelativeConditional((this.F & (byte)StatusBits.ZF) == 0);
+ case 1: // Z
+ return this.JumpRelativeConditional((this.F & (byte)StatusBits.ZF) != 0);
+ case 2: // NC
+ return this.JumpRelativeConditional((this.F & (byte)StatusBits.CF) == 0);
+ case 3: // C
+ return this.JumpRelativeConditional((this.F & (byte)StatusBits.CF) != 0);
+ default:
+ throw new ArgumentOutOfRangeException(nameof(flag));
+ }
+ }
+
+ private bool ReturnConditionalFlag(int flag)
+ {
+ switch (flag)
+ {
+ case 0: // NZ
+ return this.ReturnConditional((this.F & (byte)StatusBits.ZF) == 0);
+ case 1: // Z
+ return this.ReturnConditional((this.F & (byte)StatusBits.ZF) != 0);
+ case 2: // NC
+ return this.ReturnConditional((this.F & (byte)StatusBits.CF) == 0);
+ case 3: // C
+ return this.ReturnConditional((this.F & (byte)StatusBits.CF) != 0);
+ default:
+ throw new ArgumentOutOfRangeException(nameof(flag));
+ }
+ }
+
+ private bool CallConditionalFlag(int flag)
+ {
+ switch (flag)
+ {
+ case 0: // NZ
+ return this.CallConditional((this.F & (byte)StatusBits.ZF) == 0);
+ case 1: // Z
+ return this.CallConditional((this.F & (byte)StatusBits.ZF) != 0);
+ case 2: // NC
+ return this.CallConditional((this.F & (byte)StatusBits.CF) == 0);
+ case 3: // C
+ return this.CallConditional((this.F & (byte)StatusBits.CF) != 0);
+ default:
+ throw new ArgumentOutOfRangeException(nameof(flag));
+ }
+ }
+
+
+ private void Add(Register16 operand, Register16 value)
+ {
+ this.MEMPTR.Word = operand.Word;
+
+ var result = this.MEMPTR.Word + value.Word;
+
+ operand.Word = (ushort)result;
+
+ this.F = ClearBit(this.F, StatusBits.NF);
+ this.F = SetBit(this.F, StatusBits.CF, result & (int)Bits.Bit16);
+ this.F = AdjustHalfCarryAdd(this.F, this.MEMPTR.High, value.High, operand.High);
+ }
+
+ private byte Add(byte operand, byte value, int carry = 0)
+ {
+ this.MEMPTR.Word = (ushort)(operand + value + carry);
+
+ this.F = AdjustHalfCarryAdd(this.F, operand, value, this.MEMPTR.Low);
+
+ operand = this.MEMPTR.Low;
+
+ this.F = ClearBit(this.F, StatusBits.NF);
+ this.F = SetBit(this.F, StatusBits.CF, this.MEMPTR.Word & (ushort)Bits.Bit8);
+ this.F = AdjustZero(this.F, operand);
+
+ return operand;
+ }
+
+ private byte ADC(byte operand, byte value) => this.Add(operand, value, (this.F & (byte)StatusBits.CF) >> 4);
+
+ private byte Subtract(byte operand, byte value, int carry = 0)
+ {
+ this.MEMPTR.Word = (ushort)(operand - value - carry);
+
+ this.F = AdjustHalfCarrySub(this.F, operand, value, this.MEMPTR.Low);
+
+ var result = operand = this.MEMPTR.Low;
+
+ this.F = SetBit(this.F, StatusBits.NF);
+ this.F = SetBit(this.F, StatusBits.CF, this.MEMPTR.High & (byte)Bits.Bit0);
+ this.F = AdjustZero(this.F, operand);
+
+ return result;
+ }
+
+ private byte SBC(byte operand, byte value) => this.Subtract(operand, value, (this.F & (byte)StatusBits.CF) >> 4);
+
+ private byte AndR(byte operand, byte value)
+ {
+ this.F = SetBit(this.F, StatusBits.HC);
+ this.F = ClearBit(this.F, StatusBits.CF | StatusBits.NF);
+ this.F = AdjustZero(this.F, operand &= value);
+ return operand;
+ }
+
+ private void AndR(byte value) => this.A = this.AndR(this.A, value);
+
+ private byte XorR(byte operand, byte value)
+ {
+ this.F = ClearBit(this.F, StatusBits.HC | StatusBits.CF | StatusBits.NF);
+ this.F = AdjustZero(this.F, operand ^= value);
+ return operand;
+ }
+
+ private void XorR(byte value) => this.A = this.XorR(this.A, value);
+
+ private byte OrR(byte operand, byte value)
+ {
+ this.F = ClearBit(this.F, StatusBits.HC | StatusBits.CF | StatusBits.NF);
+ this.F = AdjustZero(this.F, operand |= value);
+ return operand;
+ }
+
+ private void OrR(byte value) => this.A = this.OrR(this.A, value);
+
+ private void Compare(byte value) => this.Subtract(this.A, value);
+
+ private byte RLC(byte operand)
+ {
+ this.F = ClearBit(this.F, StatusBits.NF | StatusBits.HC | StatusBits.ZF);
+ var carry = operand & (byte)Bits.Bit7;
+ this.F = SetBit(this.F, StatusBits.CF, carry);
+ return (byte)((operand << 1) | (carry >> 7));
+ }
+
+ private byte RRC(byte operand)
+ {
+ this.F = ClearBit(this.F, StatusBits.NF | StatusBits.HC | StatusBits.ZF);
+ var carry = operand & (byte)Bits.Bit0;
+ this.F = SetBit(this.F, StatusBits.CF, carry);
+ return (byte)((operand >> 1) | (carry << 7));
+ }
+
+ private byte RL(byte operand)
+ {
+ this.F = ClearBit(this.F, StatusBits.NF | StatusBits.HC | StatusBits.ZF);
+ var carry = this.F & (byte)StatusBits.CF;
+ this.F = SetBit(this.F, StatusBits.CF, operand & (byte)Bits.Bit7);
+ return (byte)((operand << 1) | (carry >> 4)); // CF at Bit4
+ }
+
+ private byte RR(byte operand)
+ {
+ this.F = ClearBit(this.F, StatusBits.NF | StatusBits.HC | StatusBits.ZF);
+ var carry = this.F & (byte)StatusBits.CF;
+ this.F = SetBit(this.F, StatusBits.CF, operand & (byte)Bits.Bit0);
+ return (byte)((operand >> 1) | (carry << 3)); // CF at Bit4
+ }
+
+ private byte SLA(byte operand)
+ {
+ this.F = ClearBit(this.F, StatusBits.NF | StatusBits.HC | StatusBits.ZF);
+ this.F = SetBit(this.F, StatusBits.CF, operand & (byte)Bits.Bit7);
+ return (byte)(operand << 1);
+ }
+
+ private byte SRA(byte operand)
+ {
+ this.F = ClearBit(this.F, StatusBits.NF | StatusBits.HC | StatusBits.ZF);
+ this.F = SetBit(this.F, StatusBits.CF, operand & (byte)Bits.Bit0);
+ return (byte)((operand >> 1) | (operand & (byte)Bits.Bit7));
+ }
+
+ private byte Swap(byte operand)
+ {
+ this.F = ClearBit(this.F, StatusBits.NF | StatusBits.HC | StatusBits.CF);
+ return (byte)(PromoteNibble(operand) | DemoteNibble(operand));
+ }
+
+ private byte SRL(byte operand)
+ {
+ this.F = ClearBit(this.F, StatusBits.NF | StatusBits.HC | StatusBits.ZF);
+ this.F = SetBit(this.F, StatusBits.CF, operand & (byte)Bits.Bit0);
+ return (byte)((operand >> 1) & ~(byte)Bits.Bit7);
+ }
+
+ private void Bit(int n, byte operand)
+ {
+ var carry = this.F & (byte)StatusBits.CF;
+ this.AndR(operand, (byte)(1 << n));
+ this.F = SetBit(this.F, StatusBits.CF, carry);
+ }
+
+ private static byte Res(int n, byte operand) => (byte)(operand & ~(1 << n));
+
+ private static byte Set(int n, byte operand) => (byte)(operand | (1 << n));
+
+ private void DAA()
+ {
+ int updated = this.A;
+
+ if ((this.F & (byte)StatusBits.NF) != 0)
+ {
+ if ((this.F & (byte)StatusBits.HC) != 0)
+ {
+ updated = LowByte(updated - 6);
+ }
+
+ if ((this.F & (byte)StatusBits.CF) != 0)
+ {
+ updated -= 0x60;
+ }
+ }
+ else
+ {
+ if (((this.F & (byte)StatusBits.HC) != 0) || LowNibble((byte)updated) > 9)
+ {
+ updated += 6;
+ }
+
+ if (((this.F & (byte)StatusBits.CF) != 0) || updated > 0x9F)
+ {
+ updated += 0x60;
+ }
+ }
+
+ this.F = ClearBit(this.F, (byte)StatusBits.HC | (byte)StatusBits.ZF);
+ this.F = SetBit(this.F, StatusBits.CF, ((this.F & (byte)StatusBits.CF) != 0) || ((updated & (int)Bits.Bit8) != 0));
+ this.A = LowByte(updated);
+
+ this.F = AdjustZero(this.F, this.A);
+ }
+
+ private void Cpl()
+ {
+ this.F = SetBit(this.F, StatusBits.HC | StatusBits.NF);
+ this.A = (byte)~this.A;
+ }
+
+ private void SCF()
+ {
+ this.F = SetBit(this.F, StatusBits.CF);
+ this.F = ClearBit(this.F, StatusBits.HC | StatusBits.NF);
+ }
+
+ private void CCF()
+ {
+ this.F = ClearBit(this.F, StatusBits.NF | StatusBits.HC);
+ this.F = ClearBit(this.F, StatusBits.CF, this.F & (byte)StatusBits.CF);
+ }
+
+ private void RetI()
+ {
+ this.Return();
+ this.EI();
+ }
+ }
+ }
+}
diff --git a/LR35902/LR35902.csproj b/LR35902/LR35902.csproj
new file mode 100644
index 0000000..76d44e2
--- /dev/null
+++ b/LR35902/LR35902.csproj
@@ -0,0 +1,67 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {01F61A1D-CB4A-4EA3-96EF-222F831DF483}
+ Library
+ Properties
+ EightBit
+ LR35902
+ v4.7.2
+ 512
+ true
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {6ebf8857-62a3-4ef4-af21-c1844031d7e4}
+ EightBit
+
+
+
+
\ No newline at end of file
diff --git a/LR35902/LcdStatusMode.cs b/LR35902/LcdStatusMode.cs
new file mode 100644
index 0000000..9e31ba3
--- /dev/null
+++ b/LR35902/LcdStatusMode.cs
@@ -0,0 +1,16 @@
+//
+// Copyright (c) Adrian Conlon. All rights reserved.
+//
+namespace EightBit
+{
+ namespace GameBoy
+ {
+ public enum LcdStatusMode
+ {
+ HBlank = 0b00,
+ VBlank = 0b01,
+ SearchingOamRam = 0b10,
+ TransferringDataToLcd = 0b11
+ }
+ }
+}
diff --git a/LR35902/LcdStatusModeEventArgs.cs b/LR35902/LcdStatusModeEventArgs.cs
new file mode 100644
index 0000000..94da61b
--- /dev/null
+++ b/LR35902/LcdStatusModeEventArgs.cs
@@ -0,0 +1,17 @@
+//
+// Copyright (c) Adrian Conlon. All rights reserved.
+//
+namespace EightBit
+{
+ namespace GameBoy
+ {
+ using System;
+
+ public class LcdStatusModeEventArgs : EventArgs
+ {
+ public LcdStatusModeEventArgs(LcdStatusMode value) => this.LcdStatusMode = value;
+
+ public LcdStatusMode LcdStatusMode { get; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/LR35902/LcdcControl.cs b/LR35902/LcdcControl.cs
new file mode 100644
index 0000000..0021089
--- /dev/null
+++ b/LR35902/LcdcControl.cs
@@ -0,0 +1,22 @@
+//
+// Copyright (c) Adrian Conlon. All rights reserved.
+//
+namespace EightBit
+{
+ namespace GameBoy
+ {
+ [System.Flags]
+ public enum LcdcControl
+ {
+ None = 0,
+ DisplayBackground = Bits.Bit0,
+ ObjectEnable = Bits.Bit1,
+ ObjectBlockCompositionSelection = Bits.Bit2,
+ BackgroundCodeAreaSelection = Bits.Bit3,
+ BackgroundCharacterDataSelection = Bits.Bit4,
+ WindowEnable = Bits.Bit5,
+ WindowCodeAreaSelection = Bits.Bit6,
+ LcdEnable = Bits.Bit7
+ }
+ }
+}
diff --git a/LR35902/ObjectAttribute.cs b/LR35902/ObjectAttribute.cs
new file mode 100644
index 0000000..e38766b
--- /dev/null
+++ b/LR35902/ObjectAttribute.cs
@@ -0,0 +1,40 @@
+//
+// Copyright (c) Adrian Conlon. All rights reserved.
+//
+namespace EightBit
+{
+ namespace GameBoy
+ {
+ public class ObjectAttribute
+ {
+ public ObjectAttribute()
+ {
+ }
+
+ public ObjectAttribute(Ram ram, ushort address)
+ {
+ this.PositionY = ram.Peek(address);
+ this.PositionX = ram.Peek(++address);
+ this.Pattern = ram.Peek(++address);
+ this.Flags = ram.Peek(++address);
+ }
+
+ public byte PositionY { get; }
+ public byte PositionX { get; }
+ public byte Pattern { get; }
+ public byte Flags { get; }
+
+ public int Priority => this.Flags & (byte)Bits.Bit7;
+
+ public bool HighPriority => this.Priority != 0;
+
+ public bool LowPriority => this.Priority == 0;
+
+ public bool FlipY => (this.Flags & (byte)Bits.Bit6) != 0;
+
+ public bool FlipX => (this.Flags & (byte)Bits.Bit5) != 0;
+
+ public int Palette => (this.Flags & (byte)Bits.Bit4) >> 3; // TODO: Check this!
+ }
+ }
+}
diff --git a/LR35902/Properties/AssemblyInfo.cs b/LR35902/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..348642f
--- /dev/null
+++ b/LR35902/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("LR35902")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("LR35902")]
+[assembly: AssemblyCopyright("Copyright © 2019")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("01f61a1d-cb4a-4ea3-96ef-222f831df483")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/LR35902/StatusBits.cs b/LR35902/StatusBits.cs
new file mode 100644
index 0000000..3240fc2
--- /dev/null
+++ b/LR35902/StatusBits.cs
@@ -0,0 +1,18 @@
+//
+// Copyright (c) Adrian Conlon. All rights reserved.
+//
+namespace EightBit
+{
+ namespace GameBoy
+ {
+ [System.Flags]
+ public enum StatusBits
+ {
+ None = 0,
+ CF = Bits.Bit4,
+ HC = Bits.Bit5,
+ NF = Bits.Bit6,
+ ZF = Bits.Bit7,
+ }
+ }
+}