Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,9 @@ _UpgradeReport_Files/

Thumbs.db
Desktop.ini
.DS_Store
.DS_Store
/GameboyDotnet.SDL/Tests/
All success tests from GameboyDotnet.Tests.testsession
All tests from GameboyDotnet.Tests.testsession

.idea/
7 changes: 7 additions & 0 deletions GameboyDotnet.Core/BitState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace GameboyDotnet;

public enum BitState : byte
{
Lo = 0,
Hi = 1
}
2 changes: 1 addition & 1 deletion GameboyDotnet.Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public static class Constants
public const ushort TMARegister = 0xFF06;
public const ushort TACRegister = 0xFF07;


//LCD, PPU
public const ushort LCDControlRegister = 0xFF40;
public const ushort LcdStatusRegister = 0xFF41;
public const ushort SCYRegister = 0xFF42;
Expand Down
4 changes: 3 additions & 1 deletion GameboyDotnet.Core/Cycles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ public static class Cycles
public static bool CgbSpeedSwitch = false;
private static byte SpeedRatio => CgbSpeedSwitch ? (byte)2 : (byte)1;


public static byte DivFallingEdgeDetectorBitIndex => (byte)(CgbSpeedSwitch ? 5 : 4);
public static int CyclesPerSecond => 4194304 * (SpeedRatio);
public static int CyclesPerFrame => 70224 * SpeedRatio;
public static int CyclesPerScanline => 456 * SpeedRatio;
public static int OamScanMode2CyclesThreshold => 80 * SpeedRatio;
public static int VramMode3CyclesThreshold => (80+172) * SpeedRatio;
public static int HBlankMode0CyclesThreshold => (80+172+204) * SpeedRatio;
public static int VBlankMode1CyclesThreshold => (80+172+204) * SpeedRatio;
public static int DividerCycles => CyclesPerSecond/(16384 * SpeedRatio);
public static int DividerCycles => 256; //Total Cycle divided by 16384Hz or 32768Hz
}
1 change: 1 addition & 0 deletions GameboyDotnet.Core/Extensions/ByteExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public static byte SetBit(this byte b, int bitIndex)
public static byte ClearBit(this byte b, int bitIndex)
=> (byte)(b & ~(1 << bitIndex));


[Pure]
public static bool IsBitSet(this byte b, int bitIndex)
=> (b & (1 << bitIndex)) != 0;
Expand Down
52 changes: 52 additions & 0 deletions GameboyDotnet.Core/Gameboy.Dump.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Microsoft.Extensions.Logging;

namespace GameboyDotnet;

public partial class Gameboy
{
public byte[] DumpMemory()
{
var memoryDump = new byte[(0xFFFF + 1) + 12 + 2]; //Address space + 6 registers + 2 timers + 1 Ly
for(int i = 0; i < memoryDump.Length; i++)
{
memoryDump[i] = Cpu.MemoryController.ReadByte((ushort)i);
}
memoryDump[0xFFFF + 1] = (byte)(Cpu.Register.PC & 0xFF);
memoryDump[0xFFFF + 2] = (byte)(Cpu.Register.PC >> 8);
memoryDump[0xFFFF + 3] = (byte)(Cpu.Register.SP & 0xFF);
memoryDump[0xFFFF + 4] = (byte)(Cpu.Register.SP >> 8);
memoryDump[0xFFFF + 5] = Cpu.Register.A;
memoryDump[0xFFFF + 6] = Cpu.Register.B;
memoryDump[0xFFFF + 7] = Cpu.Register.C;
memoryDump[0xFFFF + 8] = Cpu.Register.D;
memoryDump[0xFFFF + 9] = Cpu.Register.E;
memoryDump[0xFFFF + 10] = Cpu.Register.H;
memoryDump[0xFFFF + 11] = Cpu.Register.L;
memoryDump[0xFFFF + 12] = Cpu.Register.F;
IsMemoryDumpRequested = false;

_logger.LogWarning("Memory dump created");

return memoryDump;
}

public void LoadMemoryDump(byte[] dump)
{
IsMemoryDumpRequested = true;
for (int i = 0; i <= 0xFFFF; i++)
{
Cpu.MemoryController.WriteByte((ushort)i, dump[i]);
}
Cpu.Register.PC = (ushort)(dump[0xFFFF + 1] | (dump[0xFFFF + 2] << 8));
Cpu.Register.SP = (ushort)(dump[0xFFFF + 3] | (dump[0xFFFF + 4] << 8));
Cpu.Register.A = dump[0xFFFF + 5];
Cpu.Register.B = dump[0xFFFF + 6];
Cpu.Register.C = dump[0xFFFF + 7];
Cpu.Register.D = dump[0xFFFF + 8];
Cpu.Register.E = dump[0xFFFF + 9];
Cpu.Register.H = dump[0xFFFF + 10];
Cpu.Register.L = dump[0xFFFF + 11];
Cpu.Register.F = dump[0xFFFF + 12];
IsMemoryDumpRequested = false;
}
}
6 changes: 6 additions & 0 deletions GameboyDotnet.Core/Gameboy.Events.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,10 @@ protected virtual void OnDisplayUpdated(EventArgs e)
{
DisplayUpdated.Invoke(this, e);
}

public event EventHandler FrameLimiterSwitched = null!;
protected virtual void OnFrameLimiterSwitched(EventArgs e)
{
FrameLimiterSwitched.Invoke(this, e);
}
}
61 changes: 35 additions & 26 deletions GameboyDotnet.Core/Gameboy.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Diagnostics;
using GameboyDotnet.Common;
using GameboyDotnet.Graphics;
using GameboyDotnet.Memory;
using GameboyDotnet.Processor;
using GameboyDotnet.Sound;
using GameboyDotnet.Timers;
using Microsoft.Extensions.Logging;

Expand All @@ -10,17 +12,24 @@ namespace GameboyDotnet;
public partial class Gameboy
{
private ILogger<Gameboy> _logger;
public MemoryController MemoryController { get; }
public Cpu Cpu { get; }
public Ppu Ppu { get; }
public MainTimer TimaTimer { get; } = new();
public DividerTimer DivTimer { get; } = new();
public bool IsDebugMode { get; private set; }
public Apu Apu { get; }
public TimaTimer TimaTimer { get; }
public DivTimer DivTimer { get; }
public bool IsFrameLimiterEnabled;
public bool IsMemoryDumpRequested;

public Gameboy(ILogger<Gameboy> logger)
{
_logger = logger;
Cpu = new Cpu(logger);
Ppu = new Ppu(Cpu.MemoryController);
Apu = new Apu();
MemoryController = new MemoryController(logger, Apu);
Cpu = new Cpu(logger, MemoryController);
Ppu = new Ppu(MemoryController);
TimaTimer = new TimaTimer(MemoryController);
DivTimer = new DivTimer(MemoryController);
}

public void LoadProgram(FileStream stream)
Expand All @@ -31,7 +40,8 @@ public void LoadProgram(FileStream stream)

public Task RunAsync(bool frameLimitEnabled, CancellationToken ctsToken)
{
var frameTimeTicks = TimeSpan.FromMilliseconds(16.75).Ticks;
IsFrameLimiterEnabled = frameLimitEnabled;
var frameTimeTicks = TimeSpan.FromMilliseconds(16.75).Ticks; //~59.7 Hz

var cyclesPerFrame = Cycles.CyclesPerFrame;
var currentCycles = 0;
Expand All @@ -42,32 +52,32 @@ public Task RunAsync(bool frameLimitEnabled, CancellationToken ctsToken)
{
var startTime = Stopwatch.GetTimestamp();
var targetTime = startTime + frameTimeTicks;
while (currentCycles < cyclesPerFrame)

while (currentCycles < cyclesPerFrame) //70224(Gameboy) or 140448 (Gameboy Color)
{
var tStates = Cpu.ExecuteNextOperation();
Ppu.PushPpuCycles(tStates);
TimaTimer.CheckAndIncrementTimer(ref tStates, Cpu.MemoryController);
DivTimer.CheckAndIncrementTimer(ref tStates, Cpu.MemoryController);
TimaTimer.CheckAndIncrementTimer(ref tStates);
DivTimer.CheckAndIncrementTimer(ref tStates);
Apu.PushApuCycles(ref tStates);
currentCycles += tStates;
}

UpdateJoypadState();

currentCycles -= cyclesPerFrame;
DisplayUpdated.Invoke(this, EventArgs.Empty);

// if (frameLimitEnabled)
// {
// var remainingTime = targetTime - Stopwatch.GetTimestamp();
// if (remainingTime > 0)
// {
// SpinWait.SpinUntil(() => Stopwatch.GetTimestamp() >= targetTime);
// }
// }
if(frameLimitEnabled)
Ppu.FrameBuffer.EnqueueFrame(Ppu.Lcd);

if (IsMemoryDumpRequested)
{
DumpMemory();
continue;
}

if(IsFrameLimiterEnabled)
{
while (Stopwatch.GetTimestamp() < targetTime)
{
//Wait in a tight loop for until target time is reached
//Wait in a tight loop until the target time is reached
}
}
}
Expand All @@ -81,10 +91,9 @@ public Task RunAsync(bool frameLimitEnabled, CancellationToken ctsToken)
return Task.CompletedTask;
}

public void SwitchDebugMode()
public void SwitchFramerateLimiter()
{
_logger.LogInformation("Switching debug mode to {IsDebugMode}", !IsDebugMode);
IsDebugMode = !IsDebugMode;
_logger = LoggerHelper.GetLogger<Gameboy>(IsDebugMode ? LogLevel.Debug : LogLevel.Information);
IsFrameLimiterEnabled = !IsFrameLimiterEnabled;
_logger.LogWarning("Frame limiter is now '{IsFrameLimiterEnabled}'", IsFrameLimiterEnabled ? "enabled" : "disabled");
}
}
1 change: 1 addition & 0 deletions GameboyDotnet.Core/GameboyDotnet.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>GameboyDotnet</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
Expand Down
36 changes: 36 additions & 0 deletions GameboyDotnet.Core/Graphics/FrameBuffer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Collections.Concurrent;
using System.Diagnostics;

namespace GameboyDotnet.Graphics;

public class FrameBuffer
{
private readonly ConcurrentQueue<byte[,]> _frameQueue = new();
private int _frameCount = 0;
private readonly Stopwatch _stopwatch = new();
public double Fps = 0;

public FrameBuffer()
{
_stopwatch.Start();
}

public void EnqueueFrame(Lcd lcd)
{
if (_frameQueue.Count < 10) // Prevent excessive buffering, but keep latency low
_frameQueue.Enqueue(lcd.Buffer);

Interlocked.Increment(ref _frameCount);

if (_stopwatch.ElapsedMilliseconds >= 1000)
{
Fps = Interlocked.Exchange(ref _frameCount, 0);
_stopwatch.Restart();
}
}

public bool TryDequeueFrame(out byte[,]? frame)
{
return _frameQueue.TryDequeue(out frame);
}
}
39 changes: 20 additions & 19 deletions GameboyDotnet.Core/Graphics/Lcd.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,30 @@ public class Lcd(MemoryController memoryController)
public const int ScreenHeight = 144;
public const int ScreenWidth = 160;
public byte[,] Buffer = new byte[160, 144];
public byte Lcdc => memoryController.ReadByte(Constants.LCDControlRegister);
private byte Lyc => memoryController.ReadByte(Constants.LYCompareRegister);

public byte Lcdc => memoryController.IoRegisters.MemorySpace[0x40];
public byte Lyc => memoryController.IoRegisters.MemorySpace[0x45];

public byte Ly
{
get => memoryController.ReadByte(Constants.LYRegister);
private set => memoryController.WriteByte(Constants.LYRegister, value);
get => memoryController.IoRegisters.MemorySpace[0x44];
private set => memoryController.IoRegisters.MemorySpace[0x44] = value;
}

public byte Stat
{
get => memoryController.ReadByte(Constants.LcdStatusRegister);
private set => memoryController.WriteByte(Constants.LcdStatusRegister, value);
// 0xFF41 & ~(0xFF00) = 0xFF41 & 0x00FF = 0x0041
get => memoryController.IoRegisters.MemorySpace[Constants.LcdStatusRegister & ~BankAddress.IoRegistersStart];
private set => memoryController.IoRegisters.MemorySpace[0x41] = value;
}

public byte Scx => memoryController.ReadByte(Constants.SCXRegister);
public byte Scy => memoryController.ReadByte(Constants.SCYRegister);
public byte Wy => memoryController.ReadByte(Constants.WYRegister);
public byte Wx => memoryController.ReadByte(Constants.WXRegister);
public byte Obp1 => memoryController.ReadByte(Constants.OBP1Register);
public byte Obp0 => memoryController.ReadByte(Constants.OBP0Register);
public byte Bgp => memoryController.ReadByte(Constants.BGPRegister);
public byte Scx => memoryController.IoRegisters.MemorySpace[0x43];
public byte Scy => memoryController.IoRegisters.MemorySpace[0x42];
public byte Wy => memoryController.IoRegisters.MemorySpace[0x4A];
public byte Wx => memoryController.IoRegisters.MemorySpace[0x4B];
public byte Obp1 => memoryController.IoRegisters.MemorySpace[0x49];
public byte Obp0 => memoryController.IoRegisters.MemorySpace[0x48];
public byte Bgp => memoryController.IoRegisters.MemorySpace[0x47];

public BgWindowDisplayPriority BgWindowDisplayPriority => Lcdc.IsBitSet(0)
? BgWindowDisplayPriority.High
Expand Down Expand Up @@ -62,7 +64,6 @@ public byte Stat
? WindowTileMapArea.Tilemap9C00
: WindowTileMapArea.Tilemap9800;


public void UpdatePpuMode(PpuMode currentPpuMode)
{
var stat = (byte)((Stat & 0b11111100) | (byte)currentPpuMode);
Expand Down Expand Up @@ -90,18 +91,18 @@ public void UpdatePpuMode(PpuMode currentPpuMode)

public void UpdateLy(byte ly)
{
var stat = Stat;
bool isLyEqualToLyc = ly == Lyc;

if (isLyEqualToLyc && stat.IsBitSet(6)) //LYC=LY stat interrupt enabled
if (isLyEqualToLyc && Stat.IsBitSet(6)) //LYC=LY stat interrupt enabled
{
RequestLcdInterrupt();
}

Ly = ly;

Stat = isLyEqualToLyc
? stat.SetBit(2)
: stat.ClearBit(2);
? Stat.SetBit(2)
: Stat.ClearBit(2);
}

private void RequestVBlankInterrupt()
Expand Down
Loading