Tidy up EightBit.GameBoy namespace definition.
Signed-off-by: Adrian Conlon <Adrian.conlon@gmail.com>
This commit is contained in:
parent
0ada703504
commit
5a7a3b5019
|
@ -1,19 +1,16 @@
|
|||
// <copyright file="AbstractColourPalette.cs" company="Adrian Conlon">
|
||||
// Copyright (c) Adrian Conlon. All rights reserved.
|
||||
// </copyright>
|
||||
namespace EightBit
|
||||
namespace EightBit.GameBoy
|
||||
{
|
||||
namespace GameBoy
|
||||
public class AbstractColourPalette
|
||||
{
|
||||
public class AbstractColourPalette
|
||||
private readonly uint[] colours = new uint[4];
|
||||
|
||||
protected AbstractColourPalette()
|
||||
{
|
||||
private readonly uint[] colours = new uint[4];
|
||||
|
||||
protected AbstractColourPalette()
|
||||
{
|
||||
}
|
||||
|
||||
public uint Colour(int index) => this.colours[index];
|
||||
}
|
||||
|
||||
public uint Colour(int index) => this.colours[index];
|
||||
}
|
||||
}
|
||||
|
|
789
LR35902/Bus.cs
789
LR35902/Bus.cs
|
@ -1,439 +1,436 @@
|
|||
// <copyright file="Bus.cs" company="Adrian Conlon">
|
||||
// Copyright (c) Adrian Conlon. All rights reserved.
|
||||
// </copyright>
|
||||
namespace EightBit
|
||||
namespace EightBit.GameBoy
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace GameBoy
|
||||
public abstract class Bus : EightBit.Bus
|
||||
{
|
||||
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<Rom> gameRomBanks = new List<Rom>(); // 0x0000 - 0x3fff, 0x4000 - 0x7fff (switchable)
|
||||
private readonly List<Ram> ramBanks = new List<Ram>(); // 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()
|
||||
{
|
||||
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;
|
||||
this.IO = new IoRegisters(this);
|
||||
this.CPU = new LR35902(this);
|
||||
}
|
||||
|
||||
private readonly Rom bootRom = new Rom(0x100); // 0x0000 - 0x00ff
|
||||
private readonly List<Rom> gameRomBanks = new List<Rom>(); // 0x0000 - 0x3fff, 0x4000 - 0x7fff (switchable)
|
||||
private readonly List<Ram> ramBanks = new List<Ram>(); // 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
|
||||
public LR35902 CPU { get; }
|
||||
|
||||
private bool enabledLCD = false;
|
||||
public Ram VRAM { get; } = new Ram(0x2000);
|
||||
|
||||
private bool disabledGameRom = false;
|
||||
public Ram OAMRAM { get; } = new Ram(0xa0);
|
||||
|
||||
private bool rom = false;
|
||||
private bool banked = false;
|
||||
private bool ram = false;
|
||||
private bool battery = false;
|
||||
public IoRegisters IO { get; }
|
||||
|
||||
private bool higherRomBank = true;
|
||||
private bool ramBankSwitching = false;
|
||||
public bool GameRomDisabled => this.disabledGameRom;
|
||||
|
||||
private int romBank = 1;
|
||||
private int ramBank = 0;
|
||||
public bool GameRomEnabled => !this.GameRomDisabled;
|
||||
|
||||
protected Bus()
|
||||
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.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();
|
||||
this.gameRomBanks[bank].Load(path, 0, bankSize * bank, bankSize);
|
||||
}
|
||||
|
||||
public int RunRasterLines()
|
||||
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)
|
||||
{
|
||||
this.enabledLCD = (this.IO.Peek(IoRegisters.LCDC) & (byte)LcdcControl.LcdEnable) != 0;
|
||||
this.IO.ResetLY();
|
||||
return this.RunRasterLines(Display.RasterHeight);
|
||||
return new MemoryMapping(this.bootRom, 0x0000, Mask.Mask16, AccessLevel.ReadOnly);
|
||||
}
|
||||
|
||||
public int RunVerticalBlankLines()
|
||||
if ((address < 0x4000) && this.GameRomEnabled)
|
||||
{
|
||||
var lines = TotalLineCount - Display.RasterHeight;
|
||||
return this.RunVerticalBlankLines(lines);
|
||||
return new MemoryMapping(this.gameRomBanks[0], 0x0000, 0xffff, AccessLevel.ReadOnly);
|
||||
}
|
||||
|
||||
public override MemoryMapping Mapping(ushort address)
|
||||
if ((address < 0x8000) && this.GameRomEnabled)
|
||||
{
|
||||
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);
|
||||
return new MemoryMapping(this.gameRomBanks[this.romBank], 0x4000, 0xffff, AccessLevel.ReadOnly);
|
||||
}
|
||||
|
||||
protected override void OnWrittenByte()
|
||||
if (address < 0xa000)
|
||||
{
|
||||
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;
|
||||
}
|
||||
return new MemoryMapping(this.VRAM, 0x8000, 0xffff, AccessLevel.ReadWrite);
|
||||
}
|
||||
|
||||
private void ValidateCartridgeType()
|
||||
if (address < 0xc000)
|
||||
{
|
||||
this.rom = this.banked = this.ram = this.battery = false;
|
||||
|
||||
// ROM type
|
||||
switch (this.gameRomBanks[0].Peek(0x147))
|
||||
if (this.ramBanks.Count == 0)
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
return new MemoryMapping(this.unmapped2000, 0xa000, 0xffff, AccessLevel.ReadOnly);
|
||||
}
|
||||
else
|
||||
{
|
||||
count += this.CPU.Run(CyclesPerLine);
|
||||
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);
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
return count;
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
// <copyright file="CartridgeType.cs" company="Adrian Conlon">
|
||||
// Copyright (c) Adrian Conlon. All rights reserved.
|
||||
// </copyright>
|
||||
namespace EightBit
|
||||
namespace EightBit.GameBoy
|
||||
{
|
||||
namespace GameBoy
|
||||
public enum CartridgeType
|
||||
{
|
||||
public enum CartridgeType
|
||||
{
|
||||
ROM = 0,
|
||||
ROM_MBC1 = 1,
|
||||
ROM_MBC1_RAM = 2,
|
||||
ROM_MBC1_RAM_BATTERY = 3,
|
||||
}
|
||||
ROM = 0,
|
||||
ROM_MBC1 = 1,
|
||||
ROM_MBC1_RAM = 2,
|
||||
ROM_MBC1_RAM_BATTERY = 3,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,42 +1,39 @@
|
|||
// <copyright file="CharacterDefinition.cs" company="Adrian Conlon">
|
||||
// Copyright (c) Adrian Conlon. All rights reserved.
|
||||
// </copyright>
|
||||
namespace EightBit
|
||||
namespace EightBit.GameBoy
|
||||
{
|
||||
namespace GameBoy
|
||||
public sealed class CharacterDefinition
|
||||
{
|
||||
public sealed class CharacterDefinition
|
||||
private readonly Ram vram;
|
||||
private readonly ushort address;
|
||||
|
||||
public CharacterDefinition(Ram vram, ushort address)
|
||||
{
|
||||
private readonly Ram vram;
|
||||
private readonly ushort address;
|
||||
this.vram = vram;
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public CharacterDefinition(Ram vram, ushort 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)
|
||||
{
|
||||
this.vram = vram;
|
||||
this.address = address;
|
||||
var mask = 1 << bit;
|
||||
|
||||
var bitLow = (planeLow & mask) != 0 ? 1 : 0;
|
||||
var bitHigh = (planeHigh & mask) != 0 ? 0b10 : 0;
|
||||
|
||||
returned[7 - bit] = bitHigh | bitLow;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return returned;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
// <copyright file="ColourShades.cs" company="Adrian Conlon">
|
||||
// Copyright (c) Adrian Conlon. All rights reserved.
|
||||
// </copyright>
|
||||
namespace EightBit
|
||||
namespace EightBit.GameBoy
|
||||
{
|
||||
namespace GameBoy
|
||||
public enum ColourShades
|
||||
{
|
||||
public enum ColourShades
|
||||
{
|
||||
Off,
|
||||
Light,
|
||||
Medium,
|
||||
Dark,
|
||||
}
|
||||
Off,
|
||||
Light,
|
||||
Medium,
|
||||
Dark,
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,176 +1,173 @@
|
|||
// <copyright file="Display.cs" company="Adrian Conlon">
|
||||
// Copyright (c) Adrian Conlon. All rights reserved.
|
||||
// </copyright>
|
||||
namespace EightBit
|
||||
namespace EightBit.GameBoy
|
||||
{
|
||||
namespace GameBoy
|
||||
public sealed class Display
|
||||
{
|
||||
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)
|
||||
{
|
||||
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;
|
||||
this.colours = colours;
|
||||
this.bus = bus;
|
||||
this.oam = oam;
|
||||
this.vram = vram;
|
||||
}
|
||||
|
||||
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 uint[] Pixels { get; } = new uint[PixelCount];
|
||||
|
||||
public Display(AbstractColourPalette colours, Bus bus, Ram oam, Ram vram)
|
||||
public void Render()
|
||||
{
|
||||
this.scanLine = this.bus.IO.Peek(IoRegisters.LY);
|
||||
if (this.scanLine < RasterHeight)
|
||||
{
|
||||
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)
|
||||
{
|
||||
this.control = this.bus.IO.Peek(IoRegisters.LCDC);
|
||||
if ((this.control & (byte)LcdcControl.LcdEnable) != 0)
|
||||
if ((this.control & (byte)LcdcControl.DisplayBackground) != 0)
|
||||
{
|
||||
if ((this.control & (byte)LcdcControl.DisplayBackground) != 0)
|
||||
{
|
||||
this.RenderBackground();
|
||||
}
|
||||
this.RenderBackground();
|
||||
}
|
||||
|
||||
if ((this.control & (byte)LcdcControl.ObjectEnable) != 0)
|
||||
{
|
||||
this.RenderObjects();
|
||||
}
|
||||
if ((this.control & (byte)LcdcControl.ObjectEnable) != 0)
|
||||
{
|
||||
this.RenderObjects();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadObjectAttributes()
|
||||
public void LoadObjectAttributes()
|
||||
{
|
||||
for (var i = 0; i < 40; ++i)
|
||||
{
|
||||
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)))
|
||||
{
|
||||
this.objectAttributes[i] = new ObjectAttribute(this.oam, (ushort)(4 * i));
|
||||
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 int[] CreatePalette(ushort address)
|
||||
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)
|
||||
{
|
||||
var raw = this.bus.IO.Peek(address);
|
||||
return new int[4]
|
||||
{
|
||||
raw & 0b11,
|
||||
(raw & 0b1100) >> 2,
|
||||
(raw & 0b110000) >> 4,
|
||||
(raw & 0b11000000) >> 6,
|
||||
};
|
||||
cy = ~cy & flipMaskY;
|
||||
}
|
||||
|
||||
private void RenderBackground()
|
||||
var rowDefinition = definition.Get(cy);
|
||||
|
||||
var lineAddress = y * RasterWidth;
|
||||
for (var cx = 0; cx < width; ++cx)
|
||||
{
|
||||
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 x = drawX + (flipX ? ~cx & flipMaskX : cx);
|
||||
if (x >= RasterWidth)
|
||||
{
|
||||
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;
|
||||
break;
|
||||
}
|
||||
|
||||
var rowDefinition = definition.Get(cy);
|
||||
|
||||
var lineAddress = y * RasterWidth;
|
||||
for (var cx = 0; cx < width; ++cx)
|
||||
var colour = rowDefinition[cx];
|
||||
if (!allowTransparencies || (allowTransparencies && (colour > 0)))
|
||||
{
|
||||
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]);
|
||||
}
|
||||
var outputPixel = lineAddress + x;
|
||||
this.Pixels[outputPixel] = this.colours.Colour(palette[colour]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
// <copyright file="Interrupts.cs" company="Adrian Conlon">
|
||||
// Copyright (c) Adrian Conlon. All rights reserved.
|
||||
// </copyright>
|
||||
namespace EightBit
|
||||
namespace EightBit.GameBoy
|
||||
{
|
||||
namespace GameBoy
|
||||
// IF and IE flags
|
||||
[System.Flags]
|
||||
public enum Interrupts
|
||||
{
|
||||
// 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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,397 +1,394 @@
|
|||
// <copyright file="IoRegisters.cs" company="Adrian Conlon">
|
||||
// Copyright (c) Adrian Conlon. All rights reserved.
|
||||
// </copyright>
|
||||
namespace EightBit
|
||||
namespace EightBit.GameBoy
|
||||
{
|
||||
namespace GameBoy
|
||||
using System;
|
||||
|
||||
public sealed class IoRegisters : EightBit.Ram
|
||||
{
|
||||
using System;
|
||||
public const int BASE = 0xFF00;
|
||||
|
||||
public sealed class IoRegisters : EightBit.Ram
|
||||
// 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 readonly Register16 dmaAddress = new Register16();
|
||||
|
||||
private int timerCounter = 0;
|
||||
private int timerRate = 0;
|
||||
|
||||
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)
|
||||
{
|
||||
public const int BASE = 0xFF00;
|
||||
this.bus = bus;
|
||||
this.bus.ReadingByte += this.Bus_ReadingByte;
|
||||
this.bus.WrittenByte += this.Bus_WrittenByte;
|
||||
}
|
||||
|
||||
// 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
|
||||
public event EventHandler<LcdStatusModeEventArgs> DisplayStatusModeUpdated;
|
||||
|
||||
// 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
|
||||
public bool BootRomDisabled { get; private set; } = false;
|
||||
|
||||
// Interrupt Flags
|
||||
public const int IF = 0xF; // R/W Mask5
|
||||
public const int IE = 0xFF; // R/W Mask5
|
||||
public bool BootRomEnabled => !this.BootRomDisabled;
|
||||
|
||||
// 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 int TimerClock => this.Peek((ushort)TAC) & (byte)Mask.Mask2;
|
||||
|
||||
public const int WAVE_PATTERN_RAM_START = 0x30;
|
||||
public const int WAVE_PATTERN_RAM_END = 0x3F;
|
||||
public bool TimerEnabled => !this.TimerDisabled;
|
||||
|
||||
// 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
|
||||
public bool TimerDisabled => (this.Peek((ushort)TAC) & (byte)Bits.Bit2) == 0;
|
||||
|
||||
// Boot rom control
|
||||
public const int BOOT_DISABLE = 0x50;
|
||||
|
||||
private readonly Bus bus;
|
||||
private readonly Register16 divCounter = new Register16(0xab, 0xcc);
|
||||
private readonly Register16 dmaAddress = new Register16();
|
||||
|
||||
private int timerCounter = 0;
|
||||
private int timerRate = 0;
|
||||
|
||||
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)
|
||||
public int TimerClockTicks
|
||||
{
|
||||
get
|
||||
{
|
||||
this.bus = bus;
|
||||
this.bus.ReadingByte += this.Bus_ReadingByte;
|
||||
this.bus.WrittenByte += this.Bus_WrittenByte;
|
||||
}
|
||||
|
||||
public event EventHandler<LcdStatusModeEventArgs> 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 int TimerClockTicks
|
||||
{
|
||||
get
|
||||
switch (this.TimerClock)
|
||||
{
|
||||
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 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 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);
|
||||
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
|
||||
}
|
||||
|
||||
this.Poke((ushort)TIMA, Chip.LowByte(updated));
|
||||
throw new InvalidOperationException("Invalid timer clock specification");
|
||||
}
|
||||
}
|
||||
|
||||
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 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);
|
||||
}
|
||||
|
||||
public void IncrementLY() => this.Poke((ushort)LY, (byte)((this.Peek((ushort)LY) + 1) % GameBoy.Bus.TotalLineCount));
|
||||
this.Poke((ushort)TIMA, Chip.LowByte(updated));
|
||||
}
|
||||
|
||||
public void ResetLY() => this.Poke((ushort)LY, 0);
|
||||
public void IncrementLY() => this.Poke((ushort)LY, (byte)((this.Peek((ushort)LY) + 1) % GameBoy.Bus.TotalLineCount));
|
||||
|
||||
public void UpdateLcdStatusMode(LcdStatusMode mode)
|
||||
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)
|
||||
{
|
||||
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 -= cycles;
|
||||
if (this.timerCounter <= 0)
|
||||
{
|
||||
this.timerCounter += this.timerRate;
|
||||
this.IncrementTIMA();
|
||||
}
|
||||
this.timerCounter += this.timerRate;
|
||||
this.IncrementTIMA();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyMask(ushort address, byte masking) => this.Poke(address, (byte)(this.Peek(address) | ~masking));
|
||||
private void ApplyMask(ushort address, byte masking) => this.Poke(address, (byte)(this.Peek(address) | ~masking));
|
||||
|
||||
private void TriggerKeypadInterrupt() => this.TriggerInterrupt(Interrupts.KeypadPressed);
|
||||
private void TriggerKeypadInterrupt() => this.TriggerInterrupt(Interrupts.KeypadPressed);
|
||||
|
||||
private void Bus_WrittenByte(object sender, System.EventArgs e)
|
||||
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 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;
|
||||
// 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)));
|
||||
}
|
||||
|
||||
case SB: // R/W
|
||||
case SC: // R/W
|
||||
break;
|
||||
break;
|
||||
case SB:
|
||||
break;
|
||||
case SC:
|
||||
this.ApplyMask(port, (byte)(Bits.Bit7 | Bits.Bit0));
|
||||
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;
|
||||
// Timer control
|
||||
case DIV:
|
||||
case TIMA:
|
||||
case TMA:
|
||||
break;
|
||||
case TAC:
|
||||
this.ApplyMask(port, (byte)Mask.Mask3);
|
||||
break;
|
||||
|
||||
case IF: // R/W
|
||||
break;
|
||||
// Interrupt Flags
|
||||
case IF:
|
||||
this.ApplyMask(port, (byte)Mask.Mask5);
|
||||
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;
|
||||
// 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;
|
||||
|
||||
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;
|
||||
}
|
||||
default:
|
||||
this.ApplyMask(port, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,26 +11,27 @@ namespace LR35902.BlarggTest
|
|||
var configuration = new Configuration();
|
||||
|
||||
#if DEBUG
|
||||
configuration.DebugMode = true;
|
||||
//configuration.DebugMode = true;
|
||||
#endif
|
||||
//configuration.DebugMode = true;
|
||||
|
||||
var computer = new Computer(configuration);
|
||||
|
||||
computer.Plug("blargg/cpu_instrs.gb"); // Passed
|
||||
////computer.plug("blargg/01-special.gb"); // Passed
|
||||
////computer.plug("blargg/02-interrupts.gb"); // Passed
|
||||
////computer.plug("blargg/03-op sp,hl.gb"); // Passed
|
||||
////computer.plug("blargg/04-op r,imm.gb"); // Passed
|
||||
////computer.plug("blargg/05-op rp.gb"); // Passed
|
||||
////computer.plug("blargg/06-ld r,r.gb"); // Passed
|
||||
////computer.plug("blargg/07-jr,jp,call,ret,rst.gb"); // Passed
|
||||
////computer.plug("blargg/08-misc instrs.gb"); // Passed
|
||||
////computer.plug("blargg/09-op r,r.gb"); // Passed
|
||||
////computer.plug("blargg/10-bit ops.gb"); // Passed
|
||||
////computer.plug("blargg/11-op a,(hl).gb"); // Passed
|
||||
////computer.Plug("blargg/01-special.gb"); // Passed
|
||||
////computer.Plug("blargg/02-interrupts.gb"); // Passed
|
||||
////computer.Plug("blargg/03-op sp,hl.gb"); // Passed
|
||||
////computer.Plug("blargg/04-op r,imm.gb"); // Passed
|
||||
////computer.Plug("blargg/05-op rp.gb"); // Passed
|
||||
////computer.Plug("blargg/06-ld r,r.gb"); // Passed
|
||||
////computer.Plug("blargg/07-jr,jp,call,ret,rst.gb"); // Passed
|
||||
////computer.Plug("blargg/08-misc instrs.gb"); // Passed
|
||||
////computer.Plug("blargg/09-op r,r.gb"); // Passed
|
||||
////computer.Plug("blargg/10-bit ops.gb"); // Passed
|
||||
////computer.Plug("blargg/11-op a,(hl).gb"); // Passed
|
||||
|
||||
////computer.plug("blargg/instr_timing.gb"); // Failed #255
|
||||
////computer.plug("blargg/interrupt_time.gb"); // Failed
|
||||
////computer.Plug("blargg/instr_timing.gb"); // Failed #255
|
||||
////computer.Plug("blargg/interrupt_time.gb"); // Failed
|
||||
|
||||
computer.RaisePOWER();
|
||||
computer.Run();
|
||||
|
|
2189
LR35902/LR35902.cs
2189
LR35902/LR35902.cs
File diff suppressed because it is too large
Load Diff
|
@ -1,16 +1,13 @@
|
|||
// <copyright file="LcdStatusMode.cs" company="Adrian Conlon">
|
||||
// Copyright (c) Adrian Conlon. All rights reserved.
|
||||
// </copyright>
|
||||
namespace EightBit
|
||||
namespace EightBit.GameBoy
|
||||
{
|
||||
namespace GameBoy
|
||||
public enum LcdStatusMode
|
||||
{
|
||||
public enum LcdStatusMode
|
||||
{
|
||||
HBlank = 0b00,
|
||||
VBlank = 0b01,
|
||||
SearchingOamRam = 0b10,
|
||||
TransferringDataToLcd = 0b11,
|
||||
}
|
||||
HBlank = 0b00,
|
||||
VBlank = 0b01,
|
||||
SearchingOamRam = 0b10,
|
||||
TransferringDataToLcd = 0b11,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
// <copyright file="LcdStatusModeEventArgs.cs" company="Adrian Conlon">
|
||||
// Copyright (c) Adrian Conlon. All rights reserved.
|
||||
// </copyright>
|
||||
namespace EightBit
|
||||
namespace EightBit.GameBoy
|
||||
{
|
||||
namespace GameBoy
|
||||
using System;
|
||||
|
||||
public class LcdStatusModeEventArgs : EventArgs
|
||||
{
|
||||
using System;
|
||||
public LcdStatusModeEventArgs(LcdStatusMode value) => this.LcdStatusMode = value;
|
||||
|
||||
public class LcdStatusModeEventArgs : EventArgs
|
||||
{
|
||||
public LcdStatusModeEventArgs(LcdStatusMode value) => this.LcdStatusMode = value;
|
||||
|
||||
public LcdStatusMode LcdStatusMode { get; }
|
||||
}
|
||||
public LcdStatusMode LcdStatusMode { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,19 @@
|
|||
// <copyright file="LcdcControl.cs" company="Adrian Conlon">
|
||||
// Copyright (c) Adrian Conlon. All rights reserved.
|
||||
// </copyright>
|
||||
namespace EightBit
|
||||
namespace EightBit.GameBoy
|
||||
{
|
||||
namespace GameBoy
|
||||
[System.Flags]
|
||||
public enum LcdcControl
|
||||
{
|
||||
[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,
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,43 +1,40 @@
|
|||
// <copyright file="ObjectAttribute.cs" company="Adrian Conlon">
|
||||
// Copyright (c) Adrian Conlon. All rights reserved.
|
||||
// </copyright>
|
||||
namespace EightBit
|
||||
namespace EightBit.GameBoy
|
||||
{
|
||||
namespace GameBoy
|
||||
public class ObjectAttribute
|
||||
{
|
||||
public class ObjectAttribute
|
||||
public 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!
|
||||
}
|
||||
|
||||
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!
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
// <copyright file="StatusBits.cs" company="Adrian Conlon">
|
||||
// Copyright (c) Adrian Conlon. All rights reserved.
|
||||
// </copyright>
|
||||
namespace EightBit
|
||||
namespace EightBit.GameBoy
|
||||
{
|
||||
namespace GameBoy
|
||||
[System.Flags]
|
||||
public enum StatusBits
|
||||
{
|
||||
[System.Flags]
|
||||
public enum StatusBits
|
||||
{
|
||||
None = 0,
|
||||
CF = Bits.Bit4,
|
||||
HC = Bits.Bit5,
|
||||
NF = Bits.Bit6,
|
||||
ZF = Bits.Bit7,
|
||||
}
|
||||
None = 0,
|
||||
CF = Bits.Bit4,
|
||||
HC = Bits.Bit5,
|
||||
NF = Bits.Bit6,
|
||||
ZF = Bits.Bit7,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue