diff --git a/src/GameBoy.Core/Video/Ppu.State.cs b/src/GameBoy.Core/Video/Ppu.State.cs new file mode 100644 index 0000000..09c67ac --- /dev/null +++ b/src/GameBoy.Core/Video/Ppu.State.cs @@ -0,0 +1,36 @@ +using System; +using System.IO; + +namespace GameBoy.Video +{ + public sealed partial class Ppu + { + public void SaveState(BinaryWriter writer) + { + writer.Write(lcdc); writer.Write(statEnables); + writer.Write(scy); writer.Write(scx); + writer.Write(ly); writer.Write(lyc); + writer.Write(bgp); writer.Write(obp0); writer.Write(obp1); + writer.Write(wy); writer.Write(wx); + writer.Write(dotCounter); writer.Write(mode); writer.Write(windowLine); + writer.Write(statLine); writer.Write(FrameReady); + writer.Write(vram); + writer.Write(oam); + writer.Write(Framebuffer); + } + + public void LoadState(BinaryReader reader) + { + lcdc = reader.ReadByte(); statEnables = reader.ReadByte(); + scy = reader.ReadByte(); scx = reader.ReadByte(); + ly = reader.ReadByte(); lyc = reader.ReadByte(); + bgp = reader.ReadByte(); obp0 = reader.ReadByte(); obp1 = reader.ReadByte(); + wy = reader.ReadByte(); wx = reader.ReadByte(); + dotCounter = reader.ReadInt32(); mode = reader.ReadInt32(); windowLine = reader.ReadInt32(); + statLine = reader.ReadBoolean(); FrameReady = reader.ReadBoolean(); + Array.Copy(reader.ReadBytes(vram.Length), vram, vram.Length); + Array.Copy(reader.ReadBytes(oam.Length), oam, oam.Length); + Array.Copy(reader.ReadBytes(Framebuffer.Length), Framebuffer, Framebuffer.Length); + } + } +} diff --git a/src/GameBoy.Core/Video/Ppu.cs b/src/GameBoy.Core/Video/Ppu.cs new file mode 100644 index 0000000..2da5508 --- /dev/null +++ b/src/GameBoy.Core/Video/Ppu.cs @@ -0,0 +1,193 @@ +using GameBoy.Cpu; + +namespace GameBoy.Video +{ + public sealed partial class Ppu + { + public const int ScreenWidth = 160; + public const int ScreenHeight = 144; + + private const int OamScanDots = 80; + private const int DrawDots = 172; + private const int DotsPerLine = 456; + private const int VisibleLines = 144; + private const int TotalLines = 154; + + private readonly InterruptController interrupts; + private readonly byte[] vram = new byte[0x2000]; + private readonly byte[] oam = new byte[0xA0]; + + public readonly byte[] Framebuffer = new byte[ScreenWidth * ScreenHeight]; + + private byte lcdc; + private byte statEnables; + private byte scy, scx; + private byte ly, lyc; + private byte bgp, obp0, obp1; + private byte wy, wx; + + private int dotCounter; + private int mode = 2; + private int windowLine; + private bool statLine; + + public bool FrameReady { get; private set; } + + public Ppu(InterruptController interrupts) + { + this.interrupts = interrupts; + } + + public void ConsumeFrame() => FrameReady = false; + + private bool LcdEnabled => (lcdc & 0x80) != 0; + + public void Step() + { + if (!LcdEnabled) + return; + + dotCounter++; + if (dotCounter == DotsPerLine) + { + dotCounter = 0; + ly++; + if (ly == TotalLines) + { + ly = 0; + windowLine = 0; + } + + if (ly < VisibleLines) + mode = 2; + else if (ly == VisibleLines) + { + mode = 1; + FrameReady = true; + interrupts.Request(Interrupt.VBlank); + } + } + else if (ly < VisibleLines) + { + if (dotCounter == OamScanDots) + { + mode = 3; + RenderScanline(); + } + else if (dotCounter == OamScanDots + DrawDots) + { + mode = 0; + } + } + + UpdateStatInterrupt(); + } + + private void UpdateStatInterrupt() + { + bool lycEqual = ly == lyc; + bool line = + (mode == 0 && (statEnables & 0x08) != 0) || + (mode == 2 && (statEnables & 0x20) != 0) || + (mode == 1 && (statEnables & 0x10) != 0) || + (lycEqual && (statEnables & 0x40) != 0); + + if (line && !statLine) + interrupts.Request(Interrupt.LcdStat); + statLine = line; + } + + public byte ReadVram(ushort address) + { + if (LcdEnabled && mode == 3) + return 0xFF; + return vram[address - 0x8000]; + } + + public void WriteVram(ushort address, byte value) + { + if (LcdEnabled && mode == 3) + return; + vram[address - 0x8000] = value; + } + + public byte ReadOam(ushort address) + { + if (LcdEnabled && (mode == 2 || mode == 3)) + return 0xFF; + return oam[address - 0xFE00]; + } + + public void WriteOam(ushort address, byte value) + { + if (LcdEnabled && (mode == 2 || mode == 3)) + return; + oam[address - 0xFE00] = value; + } + + public void WriteOamDma(int index, byte value) + { + oam[index] = value; + } + + public byte ReadRegister(ushort address) + { + switch (address) + { + case 0xFF40: return lcdc; + case 0xFF41: return (byte)(0x80 | statEnables | (ly == lyc ? 0x04 : 0x00) | mode); + case 0xFF42: return scy; + case 0xFF43: return scx; + case 0xFF44: return ly; + case 0xFF45: return lyc; + case 0xFF47: return bgp; + case 0xFF48: return obp0; + case 0xFF49: return obp1; + case 0xFF4A: return wy; + case 0xFF4B: return wx; + default: return 0xFF; + } + } + + public void WriteRegister(ushort address, byte value) + { + switch (address) + { + case 0xFF40: + bool wasOn = LcdEnabled; + lcdc = value; + if (wasOn && !LcdEnabled) + TurnOff(); + else if (!wasOn && LcdEnabled) + TurnOn(); + break; + case 0xFF41: statEnables = (byte)(value & 0x78); break; + case 0xFF42: scy = value; break; + case 0xFF43: scx = value; break; + case 0xFF45: lyc = value; break; + case 0xFF47: bgp = value; break; + case 0xFF48: obp0 = value; break; + case 0xFF49: obp1 = value; break; + case 0xFF4A: wy = value; break; + case 0xFF4B: wx = value; break; + } + } + + private void TurnOff() + { + ly = 0; + dotCounter = 0; + mode = 0; + windowLine = 0; + statLine = false; + } + + private void TurnOn() + { + ly = 0; + dotCounter = 0; + mode = 2; + windowLine = 0; + } + } +}