diff --git a/.gitignore b/.gitignore index 9636812..fb05cee 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,9 @@ _UpgradeReport_Files/ Thumbs.db Desktop.ini -.DS_Store \ No newline at end of file +.DS_Store +/GameboyDotnet.SDL/Tests/ +All success tests from GameboyDotnet.Tests.testsession +All tests from GameboyDotnet.Tests.testsession + +.idea/ diff --git a/GameboyDotnet.Core/BitState.cs b/GameboyDotnet.Core/BitState.cs new file mode 100644 index 0000000..9a3d4be --- /dev/null +++ b/GameboyDotnet.Core/BitState.cs @@ -0,0 +1,7 @@ +namespace GameboyDotnet; + +public enum BitState : byte +{ + Lo = 0, + Hi = 1 +} \ No newline at end of file diff --git a/GameboyDotnet.Core/Constants.cs b/GameboyDotnet.Core/Constants.cs index ba9e09a..d22bdbb 100644 --- a/GameboyDotnet.Core/Constants.cs +++ b/GameboyDotnet.Core/Constants.cs @@ -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; diff --git a/GameboyDotnet.Core/Cycles.cs b/GameboyDotnet.Core/Cycles.cs index d2f838d..dca20c3 100644 --- a/GameboyDotnet.Core/Cycles.cs +++ b/GameboyDotnet.Core/Cycles.cs @@ -5,6 +5,8 @@ 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; @@ -12,5 +14,5 @@ public static class Cycles 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 } \ No newline at end of file diff --git a/GameboyDotnet.Core/Extensions/ByteExtensions.cs b/GameboyDotnet.Core/Extensions/ByteExtensions.cs index 7c3a9dc..cac5458 100644 --- a/GameboyDotnet.Core/Extensions/ByteExtensions.cs +++ b/GameboyDotnet.Core/Extensions/ByteExtensions.cs @@ -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; diff --git a/GameboyDotnet.Core/Gameboy.Dump.cs b/GameboyDotnet.Core/Gameboy.Dump.cs new file mode 100644 index 0000000..91fb2d5 --- /dev/null +++ b/GameboyDotnet.Core/Gameboy.Dump.cs @@ -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; + } +} \ No newline at end of file diff --git a/GameboyDotnet.Core/Gameboy.Events.cs b/GameboyDotnet.Core/Gameboy.Events.cs index 873733d..9f4fa7b 100644 --- a/GameboyDotnet.Core/Gameboy.Events.cs +++ b/GameboyDotnet.Core/Gameboy.Events.cs @@ -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); + } } \ No newline at end of file diff --git a/GameboyDotnet.Core/Gameboy.cs b/GameboyDotnet.Core/Gameboy.cs index 9e7888b..18633fd 100644 --- a/GameboyDotnet.Core/Gameboy.cs +++ b/GameboyDotnet.Core/Gameboy.cs @@ -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; @@ -10,17 +12,24 @@ namespace GameboyDotnet; public partial class Gameboy { private ILogger _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 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) @@ -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; @@ -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 } } } @@ -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(IsDebugMode ? LogLevel.Debug : LogLevel.Information); + IsFrameLimiterEnabled = !IsFrameLimiterEnabled; + _logger.LogWarning("Frame limiter is now '{IsFrameLimiterEnabled}'", IsFrameLimiterEnabled ? "enabled" : "disabled"); } } \ No newline at end of file diff --git a/GameboyDotnet.Core/GameboyDotnet.Core.csproj b/GameboyDotnet.Core/GameboyDotnet.Core.csproj index 2ae7c16..2567f7e 100644 --- a/GameboyDotnet.Core/GameboyDotnet.Core.csproj +++ b/GameboyDotnet.Core/GameboyDotnet.Core.csproj @@ -5,6 +5,7 @@ enable enable GameboyDotnet + true diff --git a/GameboyDotnet.Core/Graphics/FrameBuffer.cs b/GameboyDotnet.Core/Graphics/FrameBuffer.cs new file mode 100644 index 0000000..b649da9 --- /dev/null +++ b/GameboyDotnet.Core/Graphics/FrameBuffer.cs @@ -0,0 +1,36 @@ +using System.Collections.Concurrent; +using System.Diagnostics; + +namespace GameboyDotnet.Graphics; + +public class FrameBuffer +{ + private readonly ConcurrentQueue _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); + } +} \ No newline at end of file diff --git a/GameboyDotnet.Core/Graphics/Lcd.cs b/GameboyDotnet.Core/Graphics/Lcd.cs index e72bc18..72915ef 100644 --- a/GameboyDotnet.Core/Graphics/Lcd.cs +++ b/GameboyDotnet.Core/Graphics/Lcd.cs @@ -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 @@ -62,7 +64,6 @@ public byte Stat ? WindowTileMapArea.Tilemap9C00 : WindowTileMapArea.Tilemap9800; - public void UpdatePpuMode(PpuMode currentPpuMode) { var stat = (byte)((Stat & 0b11111100) | (byte)currentPpuMode); @@ -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() diff --git a/GameboyDotnet.Core/Graphics/Ppu.cs b/GameboyDotnet.Core/Graphics/Ppu.cs index c53a5d4..c0435b4 100644 --- a/GameboyDotnet.Core/Graphics/Ppu.cs +++ b/GameboyDotnet.Core/Graphics/Ppu.cs @@ -8,6 +8,7 @@ namespace GameboyDotnet.Graphics; public class Ppu(MemoryController memoryController) { + public FrameBuffer FrameBuffer { get; } = new(); private int _cyclesCounter; private byte _ly; @@ -51,7 +52,6 @@ public void PushPpuCycles(byte cpuCycles) { PushScanlineToBuffer(); } - break; case PpuMode.HBlankMode0: if (_cyclesCounter >= Cycles.HBlankMode0CyclesThreshold) @@ -59,7 +59,6 @@ public void PushPpuCycles(byte cpuCycles) _ly++; _cyclesCounter -= Cycles.HBlankMode0CyclesThreshold; } - break; case PpuMode.VBlankMode1: if (_cyclesCounter >= Cycles.VBlankMode1CyclesThreshold) @@ -71,7 +70,6 @@ public void PushPpuCycles(byte cpuCycles) _ly = 0; } } - break; } @@ -93,23 +91,24 @@ private void PushScanlineToBuffer() { RenderBackgroundOrWindow(); } - - + if (Lcd.ObjDisplay == ObjDisplay.Enabled) RenderObjects(); } private void RenderObjects() { + var oamMemoryView = memoryController.Oam.MemorySpaceView; var objectsCount = 0; - for (ushort oamAddress = BankAddress.OamStart; - oamAddress <= BankAddress.OamEnd; - oamAddress += 4) + for (ushort oamOffset = 0; + oamOffset < 160; + oamOffset += 4) { - var y = memoryController.ReadByte(oamAddress) - 16; - var x = memoryController.ReadByte(oamAddress.Add(1)) - 8; - var tileNumber = memoryController.ReadByte(oamAddress.Add(2)); - var attributes = ExtractObjectAttributes(ref oamAddress); + var y = oamMemoryView[oamOffset] - 16; + var x = oamMemoryView[oamOffset.Add(1)] - 8; + var tileNumber = oamMemoryView[oamOffset.Add(2)]; + var objAttributes = oamMemoryView[oamOffset.Add(3)]; + var attributes = ExtractObjectAttributes(ref objAttributes); var objSize = (byte)Lcd.ObjSize; const byte spriteWidth = 8; @@ -171,7 +170,7 @@ private void RenderBackgroundOrWindow() : (ushort)Lcd.BgTileMapArea; var yPos = isWindow ? _ly.Subtract(wy) : _ly.Add(scy); - var tileLineIndex = (byte)((yPos & 7) * 2); + var tileLineIndex = (byte)((yPos & 0b111) * 2); var tileRowIndex = (ushort)(yPos / 8 * 32); ushort tileData = 0; @@ -221,10 +220,8 @@ private static byte GetPaletteColorByPixelColor(ref byte palette, ref ushort pix } private (bool objToBackgroundPriority, bool yFlipped, bool xFlipped, byte palette) ExtractObjectAttributes( - ref ushort oamAddress) + ref byte objectAttributes) { - var objectAttributes = memoryController.ReadByte(oamAddress.Add(3)); - return ( objectAttributes.IsBitSet(7), objectAttributes.IsBitSet(6), diff --git a/GameboyDotnet.Core/Memory/BuildingBlocks/FixedBank.cs b/GameboyDotnet.Core/Memory/BuildingBlocks/FixedBank.cs index 88da3f8..c33f617 100644 --- a/GameboyDotnet.Core/Memory/BuildingBlocks/FixedBank.cs +++ b/GameboyDotnet.Core/Memory/BuildingBlocks/FixedBank.cs @@ -4,6 +4,7 @@ public class FixedBank { public int StartAddress { get; init; } = 0; public int EndAddress { get; init; } = 0; + public Span MemorySpaceView => MemorySpace; public byte[] MemorySpace; public string Name { get; init; } diff --git a/GameboyDotnet.Core/Memory/BuildingBlocks/IoBank.cs b/GameboyDotnet.Core/Memory/BuildingBlocks/IoBank.cs index 65ed80c..bdc1b93 100644 --- a/GameboyDotnet.Core/Memory/BuildingBlocks/IoBank.cs +++ b/GameboyDotnet.Core/Memory/BuildingBlocks/IoBank.cs @@ -1,4 +1,5 @@ using GameboyDotnet.Extensions; +using GameboyDotnet.Sound; using Microsoft.Extensions.Logging; namespace GameboyDotnet.Memory.BuildingBlocks; @@ -6,44 +7,20 @@ namespace GameboyDotnet.Memory.BuildingBlocks; public class IoBank : FixedBank { private readonly ILogger _logger; - private byte _interruptEnableRegister = 0x00; - private byte _interruptFlagRegister = 0x00; - private byte _lcdcRegister = 0x00; private byte _joypadRegister = 0xFF; public byte DpadStates = 0xF; public byte ButtonStates = 0xF; + private Apu _apu; - public IoBank(int startAddress, int endAddress, string name, ILogger logger) + public IoBank(int startAddress, int endAddress, string name, ILogger logger, Apu apu) : base(startAddress, endAddress, name) { + _apu = apu; _logger = logger; } public override void WriteByte(ref ushort address, ref byte value) { - if (address == Constants.IERegister) - { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("WriteByte refreshing InterruptEnable cache with value: {value:X}", value); - - _interruptEnableRegister = value; - } - - if (address == Constants.IFRegister) - { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("WriteByte refreshing InterruptFlag cache with value: {value:X}", value); - - _interruptFlagRegister = value; - } - - if (address == Constants.LCDControlRegister) - { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("WriteByte refreshing LCDControlRegister cache with value: {value:X}", value); - - _lcdcRegister = value; - } if(address == Constants.JoypadRegister) { if (_logger.IsEnabled(LogLevel.Debug)) @@ -63,42 +40,102 @@ public override void WriteByte(ref ushort address, ref byte value) _joypadRegister = (byte)(value & 0xF0 | (byte)(ButtonStates & 0x0F)); } } + + //Check if audio registers are in read only state (except FF26 - Power Control) + if (!_apu.IsAudioOn && address is >= 0xFF10 and < 0xFF26) + return; - base.WriteByte(ref address, ref value); - } - - public override byte ReadByte(ref ushort address) - { - if (address == Constants.IERegister) + if (address >= 0xFF30 && address <= 0xFF3F) { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("ReadByte returning cached value of InterruptEnableRegister: {value:X}", _interruptEnableRegister); - - return _interruptEnableRegister; + _apu.WaveChannel.WriteWaveRam(ref address, ref value); + return; } - - if (address == Constants.IFRegister) + + switch (address) { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("ReadByte returning cached value of InterruptFlagRegister: {value:X}", _interruptFlagRegister); - - return _interruptFlagRegister; + case 0xFF26: + _apu.SetPowerState(ref value); + return; + case 0xFF25: + _apu.SetChannelPanningStates(ref value); + return; + case 0xFF24: + _apu.SetVolumeControlStates(ref value); + return; + case 0xFF10: + _apu.SquareChannel1.SetSweepState(ref value); + break; + case 0xFF11: + _apu.SquareChannel1.SetLengthTimer(ref value); + break; + case 0xFF12: + _apu.SquareChannel1.SetVolumeRegister(ref value); + break; + case 0xFF13: + _apu.SquareChannel1.SetPeriodLowOrRandomnessRegister(ref value); + break; + case 0xFF14: + _apu.SquareChannel1.SetPeriodHighControl(ref value); + break; + case 0xFF16: + _apu.SquareChannel2.SetLengthTimer(ref value); + break; + case 0xFF17: + _apu.SquareChannel2.SetVolumeRegister(ref value); + break; + case 0xFF18: + _apu.SquareChannel2.SetPeriodLowOrRandomnessRegister(ref value); + break; + case 0xFF19: + _apu.SquareChannel2.SetPeriodHighControl(ref value); + break; + case 0xFF1A: + _apu.WaveChannel.SetDacStatus(ref value); + break; + case 0xFF1B: + _apu.WaveChannel.SetLengthTimer(ref value); + break; + case 0xFF1C: + _apu.WaveChannel.SetVolumeRegister(ref value); + break; + case 0xFF1D: + _apu.WaveChannel.SetPeriodLowOrRandomnessRegister(ref value); + break; + case 0xFF1E: + _apu.WaveChannel.SetPeriodHighControl(ref value); + break; + case 0xFF20: + _apu.NoiseChannel.SetLengthTimer(ref value); + break; + case 0xFF21: + _apu.NoiseChannel.SetVolumeRegister(ref value); + break; + case 0xFF22: + _apu.NoiseChannel.SetPeriodLowOrRandomnessRegister(ref value); + break; + case 0xFF23: + _apu.NoiseChannel.SetPeriodHighControl(ref value); + break; + default: + break; } + + base.WriteByte(ref address, ref value); + } - if (address == Constants.LCDControlRegister) + public override byte ReadByte(ref ushort address) + { + if(address == 0xFF00) { if(_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("ReadByte returning cached value of LCDControlRegister: {value:X}", _lcdcRegister); - - return _lcdcRegister; + _logger.LogDebug("ReadByte returning cached value of Joypad register: {value:X}", _joypadRegister); + + return _joypadRegister; } - if(address == 0xFF00) + if (address >= 0xFF30 && address <= 0xFF3F) { - if(_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("ReadByte returning cached value of LCDControlRegister: {value:X}", _joypadRegister); - - return _joypadRegister; + return _apu.WaveChannel.ReadWaveRam(ref address); } return base.ReadByte(ref address); diff --git a/GameboyDotnet.Core/Memory/BuildingBlocks/Mbc.cs b/GameboyDotnet.Core/Memory/BuildingBlocks/Mbc.cs index 789d436..a54df24 100644 --- a/GameboyDotnet.Core/Memory/BuildingBlocks/Mbc.cs +++ b/GameboyDotnet.Core/Memory/BuildingBlocks/Mbc.cs @@ -3,7 +3,7 @@ public class MemoryBankController(string name, int bankSizeInBytes, int numberOfBanks) : SwitchableBank(BankAddress.RomBank0Start, BankAddress.RomBankNnEnd, name, bankSizeInBytes, numberOfBanks) { - protected SwitchableBank ExternalRam = new(BankAddress.ExternalRamStart, BankAddress.ExternalRamEnd, nameof(ExternalRam), bankSizeInBytes: 8192, numberOfBanks: 512); + public SwitchableBank ExternalRam = new(BankAddress.ExternalRamStart, BankAddress.ExternalRamEnd, nameof(ExternalRam), bankSizeInBytes: 8192, numberOfBanks: 512); protected bool ExternalRamEnabled { get; set; } protected int RomBankingMode { get; set; } diff --git a/GameboyDotnet.Core/Memory/Mbc/Mbc1.cs b/GameboyDotnet.Core/Memory/Mbc/Mbc1.cs index ed2b80e..6ba66a0 100644 --- a/GameboyDotnet.Core/Memory/Mbc/Mbc1.cs +++ b/GameboyDotnet.Core/Memory/Mbc/Mbc1.cs @@ -53,7 +53,9 @@ public override byte ReadByte(ref ushort address) { <= BankAddress.RomBank0End => MemorySpace[address - StartAddress], >= BankAddress.ExternalRamStart and <= BankAddress.ExternalRamEnd - => ExternalRamEnabled ? ExternalRam.ReadByte(ref address) : (byte)0xFF, + => ExternalRamEnabled + ? ExternalRam.ReadByte(ref address) + : (byte)0xFF, _ => MemorySpace[CurrentBank * BankSizeInBytes + address - BankAddress.RomBankNnStart] }; } diff --git a/GameboyDotnet.Core/Memory/MemoryController.cs b/GameboyDotnet.Core/Memory/MemoryController.cs index a28246a..9f6847e 100644 --- a/GameboyDotnet.Core/Memory/MemoryController.cs +++ b/GameboyDotnet.Core/Memory/MemoryController.cs @@ -2,6 +2,7 @@ using GameboyDotnet.Extensions; using GameboyDotnet.Memory.BuildingBlocks; using GameboyDotnet.Memory.Mbc; +using GameboyDotnet.Sound; using Microsoft.Extensions.Logging; namespace GameboyDotnet.Memory; @@ -17,17 +18,16 @@ public class MemoryController public readonly FixedBank NotUsable = new(BankAddress.NotUsableStart, BankAddress.NotUsableEnd, nameof(NotUsable)); public readonly IoBank IoRegisters; public readonly FixedBank HRam = new(BankAddress.HRamStart, BankAddress.HRamEnd, nameof(HRam)); - public readonly FixedBank InterruptEnableRegister = new( - BankAddress.InterruptEnableRegisterStart, BankAddress.InterruptEnableRegisterEnd, - nameof(InterruptEnableRegister)); + + public readonly FixedBank InterruptEnableRegister = new(BankAddress.InterruptEnableRegisterStart, BankAddress.InterruptEnableRegisterEnd, nameof(InterruptEnableRegister)); private readonly ILogger _logger; - public MemoryController(ILogger logger) + public MemoryController(ILogger logger, Apu apu) { _logger = logger; RomBankNn = new Mbc0Mock(nameof(RomBankNn), bankSizeInBytes: 16384, numberOfBanks: 2); - IoRegisters = new IoBank(BankAddress.IoRegistersStart, BankAddress.IoRegistersEnd, nameof(IoRegisters), logger); + IoRegisters = new IoBank(BankAddress.IoRegistersStart, BankAddress.IoRegistersEnd, nameof(IoRegisters), logger, apu); InitializeMemoryMap(); InitializeBootStates(); } @@ -37,10 +37,11 @@ public void LoadProgram(Stream stream) //Load first bank to read cartridge header var bank0 = new byte[16384]; var currentPosition = stream.Read(bank0, 0, 16384); - RomBankNn = MbcFactory.CreateMbc(bank0[0x147], bank0[0x148], bank0[0x149]); + RomBankNn = MbcFactory.CreateMbc(cartridgeType: bank0[0x147], romSizeByte: bank0[0x148], ramSize: bank0[0x149]); InitializeMemoryMap(); - bank0.CopyTo(RomBankNn.MemorySpace, 0); + //Load the first bank + bank0.CopyTo(RomBankNn.MemorySpace, 0); //Load the rest of the banks for (int i = 1; i < RomBankNn.NumberOfBanks; i++) { @@ -57,14 +58,15 @@ public void LoadProgram(Stream stream) break; } } - + public byte ReadByte(ushort address) { if (address is >= 0xE000 and <= 0xFDFF) { + _logger.LogDebug("Reading from Echo Ram"); address -= 0x2000; //Adjust address for Echo Ram -> Wram } - + var memoryBank = _memoryMap[address]; return memoryBank.ReadByte(ref address); @@ -74,10 +76,10 @@ public void WriteByte(ushort address, byte value) { if (address is >= 0xE000 and <= 0xFDFF) { - address -= 0x2000; + _logger.LogDebug("Writing to Echo Ram"); + address -= 0x2000; //Adjust address for Echo Ram -> Wram } - //Adjust address for Echo Ram -> Wram - + if (address == Constants.DMARegister) { DmaTransfer(ref value); @@ -144,13 +146,14 @@ public void DecrementByte(ushort address) private void DmaTransfer(ref byte value) { - ushort sourceAddress = (ushort)(value << 8); //0xXX00 + ushort sourceAddress = (ushort)(value << 8); //0x_XX00 for (ushort i = 0xFE00; i <= 0xFE9F; i++) { WriteByte(i, ReadByte(sourceAddress++)); } } + private void InitializeMemoryMap() { for (int i = 0; i <= 65535; i++) @@ -175,33 +178,33 @@ private void InitializeMemoryMap() private void InitializeBootStates() { - WriteByte(Constants.JoypadRegister, 0xCF); - WriteByte(0xFF02, 0x7E); - WriteByte(0xFF04, 0xAB); //18? - WriteByte(0xFF07, 0xF8); - WriteByte(0xFF0F, 0xE1); - WriteByte(0xFF10, 0x80); - WriteByte(0xFF11, 0xBF); - WriteByte(0xFF12, 0xF3); - WriteByte(0xFF13, 0xFF); - WriteByte(0xFF14, 0xBF); - WriteByte(0xFF16, 0x3F); - WriteByte(0xFF18, 0xFF); - WriteByte(0xFF19, 0xBF); - WriteByte(0xFF1A, 0x7F); - WriteByte(0xFF1B, 0xFF); - WriteByte(0xFF1C, 0x9F); - WriteByte(0xFF1D, 0xFF); - WriteByte(0xFF1E, 0xBF); - WriteByte(0xFF20, 0xFF); - WriteByte(0xFF23, 0xBF); - WriteByte(0xFF24, 0x77); - WriteByte(0xFF25, 0xF3); - WriteByte(0xFF26, 0xF1); - WriteByte(0xFF40, 0x91); - WriteByte(0xFF41, 0x01); - WriteByte(0xFF44, 0x90); - WriteByte(0xFF47, 0xFC); - WriteByte(0xFF4D, 0xFF); + WriteByte(address: Constants.JoypadRegister, value: 0xCF); + WriteByte(address: 0xFF02, value: 0x7E); + WriteByte(address: 0xFF04, value: 0xAB); + WriteByte(address: 0xFF07, value: 0xF8); + WriteByte(address: 0xFF0F, value: 0xE1); + WriteByte(address: 0xFF10, value: 0x80); + WriteByte(address: 0xFF11, value: 0xBF); + WriteByte(address: 0xFF12, value: 0xF3); + WriteByte(address: 0xFF13, value: 0xFF); + WriteByte(address: 0xFF14, value: 0xBF); + WriteByte(address: 0xFF16, value: 0x3F); + WriteByte(address: 0xFF18, value: 0xFF); + WriteByte(address: 0xFF19, value: 0xBF); + WriteByte(address: 0xFF1A, value: 0x7F); + WriteByte(address: 0xFF1B, value: 0xFF); + WriteByte(address: 0xFF1C, value: 0x9F); + WriteByte(address: 0xFF1D, value: 0xFF); + WriteByte(address: 0xFF1E, value: 0xBF); + WriteByte(address: 0xFF20, value: 0xFF); + WriteByte(address: 0xFF23, value: 0xBF); + WriteByte(address: 0xFF24, value: 0x77); + WriteByte(address: 0xFF25, value: 0xF3); + WriteByte(address: 0xFF26, value: 0xF1); + WriteByte(address: 0xFF40, value: 0x91); + WriteByte(address: 0xFF41, value: 0x01); + WriteByte(address: 0xFF44, value: 0x90); + WriteByte(address: 0xFF47, value: 0xFC); + WriteByte(address: 0xFF4D, value: 0xFF); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/GameboyDotnet.Core/Processor/Cpu.InterruptHandling.cs b/GameboyDotnet.Core/Processor/Cpu.InterruptHandling.cs new file mode 100644 index 0000000..b733373 --- /dev/null +++ b/GameboyDotnet.Core/Processor/Cpu.InterruptHandling.cs @@ -0,0 +1,29 @@ +using System.Numerics; +using GameboyDotnet.Extensions; + +namespace GameboyDotnet.Processor; + +public partial class Cpu +{ + private static readonly ushort[] InterruptAddresses = { 0x0040, 0x0048, 0x0050, 0x0058, 0x0060 }; + + private bool HandleInterrupt() + { + var interruptFlags = MemoryController.ReadByte(Constants.IFRegister); + var interruptEnable = MemoryController.ReadByte(Constants.IERegister); + var interrupt = (byte)(interruptFlags & interruptEnable); + + if (interrupt == 0 || !Register.InterruptsMasterEnabled) //TODO: https://gbdev.io/pandocs/halt.html + return false; + + IsHalted = false; + var interruptIndex = BitOperations.TrailingZeroCount(interrupt); + Register.InterruptsMasterEnabled = false; + MemoryController.WriteByte(Constants.IFRegister, interruptFlags.ClearBit(interruptIndex)); + PushStack(Register.PC); + + Register.PC = InterruptAddresses[interruptIndex]; + + return true; + } +} \ No newline at end of file diff --git a/GameboyDotnet.Core/Processor/Cpu.OperationBlocks.Block0.cs b/GameboyDotnet.Core/Processor/Cpu.OperationBlocks.Block0.cs index d90377f..8dc4e31 100644 --- a/GameboyDotnet.Core/Processor/Cpu.OperationBlocks.Block0.cs +++ b/GameboyDotnet.Core/Processor/Cpu.OperationBlocks.Block0.cs @@ -1,4 +1,5 @@ -using GameboyDotnet.Extensions; +using System.Runtime.CompilerServices; +using GameboyDotnet.Extensions; using Microsoft.Extensions.Logging; // ReSharper disable InconsistentNaming @@ -21,7 +22,7 @@ public partial class Cpu byte r16) { if(_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("{opCode:X2} - Loading immediate 16 bit value into register, r16 value: {r16} ", opCode, r16); + _logger.LogDebug("{OpCode:X2} - Loading immediate 16 bit value into register, r16 value: {R16} ", opCode, r16); var immediate16Bit = MemoryController.ReadWord(Register.PC.Add(1)); Register.SetRegisterByR16(r16, immediate16Bit); @@ -34,7 +35,7 @@ public partial class Cpu private (byte instructionBytesLength, byte durationTStates) LoadRegisterAIntoR16Mem(ref byte opCode, byte r16mem) { if(_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("{opCode:X2} - Loading register A into memory, r16mem value: {r16mem} ", opCode, r16mem); + _logger.LogDebug("{OpCode:X2} - Loading register A into memory, r16mem value: {r16mem} ", opCode, r16mem); MemoryController.WriteByte(address: Register.GetRegisterValueByR16Mem(r16mem), Register.A); return (1, 8); @@ -46,7 +47,7 @@ public partial class Cpu private (byte instructionBytesLength, byte durationTStates) LoadR16MemIntoRegisterA(ref byte opCode, byte r16mem) { if(_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("{opCode:X2} - Loading memory into register A, r16mem value: {r16mem} ", opCode, r16mem); + _logger.LogDebug("{OpCode:X2} - Loading memory into register A, r16mem value: {r16mem} ", opCode, r16mem); Register.A = MemoryController.ReadByte(address: Register.GetRegisterValueByR16Mem(r16mem)); return (1, 8); @@ -58,7 +59,7 @@ public partial class Cpu private (byte instructionBytesLength, byte durationTStates) LoadSPIntoImmediateMemory(ref byte opCode) { if(_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("{opCode:X2} - Loading SP into memory", opCode); + _logger.LogDebug("{OpCode:X2} - Loading SP into memory", opCode); MemoryController.WriteWord(MemoryController.ReadWord(Register.PC.Add(1)), Register.SP); return (3, 20); @@ -70,7 +71,7 @@ public partial class Cpu private (byte instructionBytesLength, byte durationTStates) IncrementR16(ref byte opCode, byte r16) { if(_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("{opCode:X2} - Incrementing 16 bit register, r16 value: {r16} ", opCode, r16); + _logger.LogDebug("{OpCode:X2} - Incrementing 16 bit register, r16 value: {R16} ", opCode, r16); Register.SetRegisterByR16(r16, Register.GetRegisterValueByR16(r16).Add(1)); return (1, 8); @@ -82,7 +83,7 @@ public partial class Cpu private (byte instructionBytesLength, byte durationTStates) DecrementR16(ref byte opCode, byte r16) { if(_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("{opCode:X2} - Decrementing 16 bit register, r16 value: {r16} ", opCode, r16); + _logger.LogDebug("{OpCode:X2} - Decrementing 16 bit register, r16 value: {R16} ", opCode, r16); Register.SetRegisterByR16(r16, Register.GetRegisterValueByR16(r16).Subtract(1)); return (1, 8); @@ -94,7 +95,7 @@ public partial class Cpu private (byte instructionBytesLength, byte durationTStates) AddR16ToHL(ref byte opCode, byte r16) { if(_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("{opCode:X2} - Adding 16 bit register to HL, r16 value: {r16} ", opCode, r16); + _logger.LogDebug("{OpCode:X2} - Adding 16 bit register to HL, r16 value: {R16} ", opCode, r16); Set16BitAddCarryFlags(Register.HL, Register.GetRegisterValueByR16(r16)); Register.HL += Register.GetRegisterValueByR16(r16); @@ -107,7 +108,7 @@ public partial class Cpu private (byte instructionBytesLength, byte durationTStates) IncrementR8(ref byte opCode, byte r8) { if(_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("{opCode:X2} - Incrementing 8 bit register, r8 value: {r8} ", opCode, r8); + _logger.LogDebug("{OpCode:X2} - Incrementing 8 bit register, r8 value: {r8} ", opCode, r8); //6 = [HL], which requires a direct memory read and write if (r8 == Constants.R8_HL_Index) @@ -130,7 +131,7 @@ public partial class Cpu private (byte instructionBytesLength, byte durationTStates) DecrementR8(ref byte opCode, byte r8) { if(_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("{opCode:X2} - Decrementing 8 bit register, r8 value: {r8} ", opCode, r8); + _logger.LogDebug("{OpCode:X2} - Decrementing 8 bit register, r8 value: {r8} ", opCode, r8); if (r8 == Constants.R8_HL_Index) { @@ -152,7 +153,7 @@ public partial class Cpu private (byte instructionBytesLength, byte durationTStates) LoadImmediate8BitIntoR8(ref byte opCode, byte r8) { if(_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("{opCode:X2} - Loading immediate 8 bit value into register, r8 value: {r8} ", opCode, r8); + _logger.LogDebug("{OpCode:X2} - Loading immediate 8 bit value into register, r8 value: {r8} ", opCode, r8); var immediate8Bit = MemoryController.ReadByte(Register.PC.Add(1)); if (r8 == Constants.R8_HL_Index) @@ -171,7 +172,9 @@ public partial class Cpu /// private (byte instructionBytesLength, byte durationTStates) RotateLeftRegisterA(ref byte opCode) { - _logger.LogDebug("{opCode:X2} - Rotating left register A", opCode); + if(_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("{OpCode:X2} - Rotating left register A", opCode); + var oldCarryFlag = Register.CarryFlag; (Register.ZeroFlag, Register.NegativeFlag, Register.HalfCarryFlag) = (false, false, false); Register.CarryFlag = (Register.A & 0b1000_0000) != 0; //most significant bit @@ -184,7 +187,9 @@ public partial class Cpu /// private (byte instructionBytesLength, byte durationTStates) RotateRightRegisterA(ref byte opCode) { - _logger.LogDebug("{opCode:X2} - Rotating right register A", opCode); + if(_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("{OpCode:X2} - Rotating right register A", opCode); + var oldCarryFlag = Register.CarryFlag; (Register.ZeroFlag, Register.NegativeFlag, Register.HalfCarryFlag) = (false, false, false); Register.CarryFlag = (Register.A & 0b0000_0001) != 0; //least significant bit @@ -198,7 +203,9 @@ public partial class Cpu /// private (byte instructionBytesLength, byte durationTStates) RotateLeftRegisterAThroughCarry(ref byte opCode) { - _logger.LogDebug("{opCode:X2} - Rotating left register A through carry", opCode); + if(_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("{OpCode:X2} - Rotating left register A through carry", opCode); + (Register.ZeroFlag, Register.NegativeFlag, Register.HalfCarryFlag) = (false, false, false); Register.CarryFlag = (Register.A & 0b1000_0000) != 0; Register.A = (byte)(Register.A << 1 | (Register.CarryFlag ? 1 : 0)); @@ -211,7 +218,7 @@ public partial class Cpu /// private (byte instructionBytesLength, byte durationTStates) RotateRightRegisterAThroughCarry(ref byte opCode) { - _logger.LogDebug("{opCode:X2} - Rotating right register A through carry", opCode); + _logger.LogDebug("{OpCode:X2} - Rotating right register A through carry", opCode); (Register.ZeroFlag, Register.NegativeFlag, Register.HalfCarryFlag) = (false, false, false); Register.CarryFlag = (Register.A & 0b0000_0001) != 0; Register.A = (byte)((Register.A >> 1) | (Register.CarryFlag ? 0b1000_0000 : 0)); @@ -224,7 +231,8 @@ public partial class Cpu /// private (byte instructionBytesLength, byte durationTStates) DecimalAdjustAccumulator(ref byte opCode) { - _logger.LogDebug("{opCode:X2} - Decimal adjust accumulator (DAA)", opCode); + if(_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("{OpCode:X2} - Decimal adjust accumulator (DAA)", opCode); byte adjust = 0; @@ -282,7 +290,8 @@ public partial class Cpu private (byte instructionBytesLength, byte durationTStates) JumpRelativeImmediate8bit(ref byte opCode) { if(_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("{opCode:X2} - Jumping relative to immediate signed 8 bit", opCode); + _logger.LogDebug("{OpCode:X2} - Jumping relative to immediate signed 8 bit", opCode); + var immediate8Bit = (sbyte)MemoryController.ReadByte(Register.PC.Add(1)); Register.PC = (ushort)(Register.PC + 2 + immediate8Bit); return (0, 12); @@ -296,7 +305,7 @@ public partial class Cpu ref byte opCode) { if(_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("{opCode:X2} - Jumping relative to immediate signed 8 bit with condition", opCode); + _logger.LogDebug("{OpCode:X2} - Jumping relative to immediate signed 8 bit with condition", opCode); if (CheckCondition(ref opCode)) return JumpRelativeImmediate8bit(ref opCode); @@ -309,7 +318,9 @@ public partial class Cpu /// private (byte instructionBytesLength, byte durationTStates) Stop(ref byte opCode) { - _logger.LogDebug("{opcode:X} - STOP - Stopping CPU", opCode); + if(_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("{opcode:X} - STOP - Stopping CPU", opCode); + IsHalted = true; //TODO: Implement GBC mode switch if needed return (1, 4); diff --git a/GameboyDotnet.Core/Processor/Cpu.OperationBlocks.Block1.cs b/GameboyDotnet.Core/Processor/Cpu.OperationBlocks.Block1.cs index 76bc02d..a2d3e1e 100644 --- a/GameboyDotnet.Core/Processor/Cpu.OperationBlocks.Block1.cs +++ b/GameboyDotnet.Core/Processor/Cpu.OperationBlocks.Block1.cs @@ -38,6 +38,7 @@ public partial class Cpu { return Halt(); } + if (destinationR8 == Constants.R8_HL_Index) { MemoryController.WriteByte(Register.HL, Register.GetRegisterValueByR8(sourceR8)); diff --git a/GameboyDotnet.Core/Processor/Cpu.OperationBlocks.Block2.cs b/GameboyDotnet.Core/Processor/Cpu.OperationBlocks.Block2.cs index c8f6231..09139a6 100644 --- a/GameboyDotnet.Core/Processor/Cpu.OperationBlocks.Block2.cs +++ b/GameboyDotnet.Core/Processor/Cpu.OperationBlocks.Block2.cs @@ -12,6 +12,7 @@ public partial class Cpu { if(_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("{opcode:X2} - ADD A, {getSourceR8:X}", opCode, r8); + var value = r8 == Constants.R8_HL_Index ? MemoryController.ReadByte(Register.HL) : Register.GetRegisterValueByR8(r8); @@ -29,6 +30,7 @@ public partial class Cpu { if(_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("{opcode:X2} - ADC A, {getSourceR8:X}", opCode, r8); + var value = r8 == Constants.R8_HL_Index ? MemoryController.ReadByte(Register.HL) : Register.GetRegisterValueByR8(r8); @@ -46,6 +48,7 @@ public partial class Cpu { if(_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("{opcode:X2} - SUB A, {getSourceR8:X}", opCode, r8); + var value = r8 == Constants.R8_HL_Index ? MemoryController.ReadByte(Register.HL) : Register.GetRegisterValueByR8(r8); @@ -62,14 +65,15 @@ public partial class Cpu { if(_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("{opcode:X2} - SBC A, {getSourceR8:X}", opCode, r8); - var value = r8 == 0b110 + + var value = r8 == Constants.R8_HL_Index ? MemoryController.ReadByte(Register.HL) : Register.GetRegisterValueByR8(r8); var carryFlag = (byte)(Register.CarryFlag ? 1 : 0); Set8BitSubtractCompareFlags(Register.A, value, carryFlag); Register.A = Register.A.Subtract(value).Subtract(carryFlag); - return (1, (byte)(r8 == 0b110 ? 8 : 4)); + return (1, (byte)(r8 == Constants.R8_HL_Index ? 8 : 4)); } /// @@ -80,13 +84,13 @@ public partial class Cpu if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("{opcode:X2} - AND A, {r8R8:X}", opCode, r8); - var value = r8 == 0b110 + var value = r8 == Constants.R8_HL_Index ? MemoryController.ReadByte(Register.HL) : Register.GetRegisterValueByR8(r8); Register.A = (byte)(Register.A & value); Set8BitAndFlags(); - return (1, (byte)(r8 == 0b110 ? 8 : 4)); + return (1, (byte)(r8 == Constants.R8_HL_Index ? 8 : 4)); } /// @@ -96,6 +100,7 @@ public partial class Cpu { if(_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("{opcode:X2} - XOR A, {r8R8:X}", opCode, r8); + var value = r8 == Constants.R8_HL_Index ? MemoryController.ReadByte(Register.HL) : Register.GetRegisterValueByR8(r8); @@ -112,19 +117,21 @@ public partial class Cpu { if(_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("{opcode:X2} - OR A, {r8R8:X}", opCode, r8); - var value = r8 == 0b110 + + var value = r8 == Constants.R8_HL_Index ? MemoryController.ReadByte(Register.HL) : Register.GetRegisterValueByR8(r8); Register.A = (byte)(Register.A | value); Set8BitOrXorFlags(); - return (1, (byte)(r8 == 0b110 ? 8 : 4)); + return (1, (byte)(r8 == Constants.R8_HL_Index ? 8 : 4)); } private (byte instructionBytesLength, byte durationTStates) CompareR8WithA(ref byte opCode, byte r8) { if(_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("{opcode:X2} - CP A, {r8R8:X}", opCode, r8); + var value = r8 == Constants.R8_HL_Index ? MemoryController.ReadByte(Register.HL) : Register.GetRegisterValueByR8(r8); diff --git a/GameboyDotnet.Core/Processor/Cpu.OperationBlocks.Block3.cs b/GameboyDotnet.Core/Processor/Cpu.OperationBlocks.Block3.cs index 21a5363..60e845e 100644 --- a/GameboyDotnet.Core/Processor/Cpu.OperationBlocks.Block3.cs +++ b/GameboyDotnet.Core/Processor/Cpu.OperationBlocks.Block3.cs @@ -12,6 +12,7 @@ public partial class Cpu { if(_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("{opcode:X2} - ADD A, r8", opCode); + var value = MemoryController.ReadByte(Register.PC.Add(1)); Set8BitAddCarryFlags(Register.A, value); Register.A = Register.A.Add(value); diff --git a/GameboyDotnet.Core/Processor/Cpu.cs b/GameboyDotnet.Core/Processor/Cpu.cs index 858d740..07e69b0 100644 --- a/GameboyDotnet.Core/Processor/Cpu.cs +++ b/GameboyDotnet.Core/Processor/Cpu.cs @@ -1,7 +1,7 @@ -using System.Numerics; -using GameboyDotnet.Components.Cpu; +using GameboyDotnet.Components.Cpu; using GameboyDotnet.Extensions; using GameboyDotnet.Memory; +using GameboyDotnet.Sound; using Microsoft.Extensions.Logging; namespace GameboyDotnet.Processor; @@ -13,10 +13,10 @@ public partial class Cpu public bool IsHalted { get; set; } private readonly ILogger _logger; - public Cpu(ILogger logger) + public Cpu(ILogger logger, MemoryController memoryController) { _logger = logger; - MemoryController = new MemoryController(logger); + MemoryController = memoryController; } public byte ExecuteNextOperation() @@ -28,7 +28,7 @@ public byte ExecuteNextOperation() return 1; } - var opCode = MemoryController.ReadByte(Register.PC); + var opCode = MemoryController.ReadByte(address: Register.PC); var operationBlock = (opCode & 0b11000000) >> 6; var operationSize = operationBlock switch @@ -51,34 +51,6 @@ public byte ExecuteNextOperation() return (byte)(operationSize.durationTStates + (interruptHandled ? 5 : 0)); } - private bool HandleInterrupt() - { - var interruptFlags = MemoryController.ReadByte(Constants.IFRegister); - var interruptEnable = MemoryController.ReadByte(Constants.IERegister); - var interrupt = (byte)(interruptFlags & interruptEnable); - - if (interrupt == 0 || !Register.InterruptsMasterEnabled) //TODO: https://gbdev.io/pandocs/halt.html - return false; - - IsHalted = false; - var interruptIndex = BitOperations.TrailingZeroCount(interrupt); - Register.InterruptsMasterEnabled = false; - MemoryController.WriteByte(Constants.IFRegister, interruptFlags.ClearBit(interruptIndex)); - PushStack(Register.PC); - - Register.PC = (1 << interruptIndex) switch - { - 0x01 => 0x0040, - 0x02 => 0x0048, - 0x04 => 0x0050, - 0x08 => 0x0058, - 0x10 => 0x0060, - _ => throw new ArgumentOutOfRangeException(nameof(interrupt), interrupt, "Invalid interrupt") - }; - - return true; - } - private bool CheckCondition(ref byte opCode) { if(_logger.IsEnabled(LogLevel.Debug)) @@ -93,8 +65,8 @@ private bool CheckCondition(ref byte opCode) _ => throw new ArgumentOutOfRangeException() }; } - - + + // private void LogTestOutput(StreamWriter writer) // { // var pcmem0 = MemoryController.ReadByte(Register.PC); diff --git a/GameboyDotnet.Core/Processor/CpuRegister.cs b/GameboyDotnet.Core/Processor/CpuRegister.cs index 6203355..fc8032d 100644 --- a/GameboyDotnet.Core/Processor/CpuRegister.cs +++ b/GameboyDotnet.Core/Processor/CpuRegister.cs @@ -9,37 +9,46 @@ namespace GameboyDotnet.Components.Cpu; /// public class CpuRegister { + private readonly byte[] R8LookupTable; + + public CpuRegister() + { + R8LookupTable = new byte[8]; //[B, C, D, E, H, L, NULL, A] + AF = 0x01B0; + BC = 0x0013; + DE = 0x00D8; + HL = 0x014D; + } + /// /// IME - Interrupt Master Enable flag /// - public bool InterruptsMasterEnabled { get; set; } - public bool IMEPending { get; set; } - - private ushort _af { get; set; } = 0x01B0; + public bool InterruptsMasterEnabled; - private ushort _bc { get; set; } = 0x0013; - - private ushort _de { get; set; } = 0x00D8; - - private ushort _hl { get; set; } = 0x014D; + public bool IMEPending; + /// /// Stack pointer, always accessed as 16-bit /// - public ushort SP { get; set; } = 0xFFFE; + public ushort SP = 0xFFFE; /// /// Program counter, always accessed as 16-bit /// - public ushort PC { get; set; } = 0x0100; + public ushort PC = 0x0100; /// /// Contains accumulator and flags, splits into A and F /// public ushort AF { - get => _af; - set => _af = (ushort)((value & 0xFFF0) | (_af & 0x00FF)); + get => (ushort)(A << 8 | F); + set + { + A = (byte)(value >> 8); + F = (byte)(value & 0x00FF); + } } /// @@ -47,18 +56,14 @@ public ushort AF /// public byte A { - get => (byte)(_af >> 8); - set => _af = (ushort)((_af & 0x00FF) | (value << 8)); + get => R8LookupTable[7]; + set => R8LookupTable[7] = value; } /// /// Low part of AF 16-bit register /// - public byte F - { - get => (byte)(_af & 0x00FF); - set => _af = (ushort)((_af & 0xFF00) | (value & 0xF0)); - } + public byte F { get; set; } /// /// Zero flag, aka 'z' flag @@ -101,8 +106,12 @@ public bool CarryFlag /// public ushort BC { - get => _bc; - set => _bc = value; + get => (ushort)(B << 8 | C); + set + { + B = (byte)(value >> 8); + C = (byte)(value & 0x00FF); + } } /// @@ -110,8 +119,8 @@ public ushort BC /// public byte B { - get => (byte)(_bc >> 8); - set => _bc = (ushort)((_bc & 0x00FF) | (value << 8)); + get => R8LookupTable[0]; + set => R8LookupTable[0] = value; } /// @@ -119,8 +128,8 @@ public byte B /// public byte C { - get => (byte)(_bc & 0x00FF); - set => _bc = (ushort)((_bc & 0xFF00) | value); + get => R8LookupTable[1]; + set => R8LookupTable[1] = value; } /// @@ -128,8 +137,12 @@ public byte C /// public ushort DE { - get => _de; - set => _de = value; + get => (ushort)(D << 8 | E); + set + { + D = (byte)(value >> 8); + E = (byte)(value & 0x00FF); + } } /// @@ -137,8 +150,8 @@ public ushort DE /// public byte D { - get => (byte)(_de >> 8); - set => _de = (ushort)((_de & 0x00FF) | (value << 8)); + get => R8LookupTable[2]; + set => R8LookupTable[2] = value; } /// @@ -146,8 +159,8 @@ public byte D /// public byte E { - get => (byte)(_de & 0x00FF); - set => _de = (ushort)((_de & 0xFF00) | value); + get => R8LookupTable[3]; + set => R8LookupTable[3] = value; } /// @@ -155,8 +168,12 @@ public byte E /// public ushort HL { - get => _hl; - set => _hl = value; + get => (ushort)(H << 8 | L); + set + { + H = (byte)(value >> 8); + L = (byte)(value & 0x00FF); + } } /// @@ -164,8 +181,8 @@ public ushort HL /// public byte H { - get => (byte)(_hl >> 8); - set => _hl = (ushort)((_hl & 0x00FF) | (value << 8)); + get => R8LookupTable[4]; + set => R8LookupTable[4] = value; } /// @@ -173,8 +190,8 @@ public byte H /// public byte L { - get => (byte)(_hl & 0x00FF); - set => _hl = (ushort)((_hl & 0xFF00) | value); + get => R8LookupTable[5]; + set => R8LookupTable[5] = value; } public void SetRegisterByR16(int r16, ushort value) @@ -195,7 +212,7 @@ public void SetRegisterByR16(int r16, ushort value) break; } } - + public ushort GetRegisterValueByR16(int r16) { switch (r16) @@ -212,11 +229,10 @@ public ushort GetRegisterValueByR16(int r16) throw new ArgumentOutOfRangeException(nameof(r16), r16, "Invalid r16 value"); } } - + public ushort GetRegisterValueByR16Mem(int r16mem) { ushort value = 0; - switch (r16mem) { case 0: @@ -238,49 +254,17 @@ public ushort GetRegisterValueByR16Mem(int r16mem) public byte GetRegisterValueByR8(byte r8) { - return r8 switch - { - 0 => B, - 1 => C, - 2 => D, - 3 => E, - 4 => H, - 5 => L, - 6 => throw new ArgumentOutOfRangeException(nameof(r8), "Cannot access memory directly with this method, use MemoryController instead"), - 7 => A, - _ => throw new ArgumentOutOfRangeException(nameof(r8), r8, "Invalid r8 value") - }; + if (r8 is > 7 or 6) + throw new ArgumentOutOfRangeException(nameof(r8), r8, "Invalid r8 value"); + + return R8LookupTable[r8]; } public void SetRegisterByR8(byte r8, byte value) { - switch (r8) - { - case 0: - B = value; - break; - case 1: - C = value; - break; - case 2: - D = value; - break; - case 3: - E = value; - break; - case 4: - H = value; - break; - case 5: - L = value; - break; - case 6: - throw new ArgumentOutOfRangeException(nameof(r8), "Cannot access memory directly with this method, use MemoryController instead"); - case 7: - A = value; - break; - default: - throw new ArgumentOutOfRangeException(nameof(r8), r8, "Invalid r8 value"); - } + if (r8 is > 7 or 6) + throw new ArgumentOutOfRangeException(nameof(r8), r8, "Invalid r8 value"); + + R8LookupTable[r8] = value; } } \ No newline at end of file diff --git a/GameboyDotnet.Core/Sound/Apu.AudioMixer.cs b/GameboyDotnet.Core/Sound/Apu.AudioMixer.cs new file mode 100644 index 0000000..cbf79bb --- /dev/null +++ b/GameboyDotnet.Core/Sound/Apu.AudioMixer.cs @@ -0,0 +1,40 @@ +using GameboyDotnet.Sound.Channels; +using GameboyDotnet.Sound.Channels.BuildingBlocks; + +namespace GameboyDotnet.Sound; + +public partial class Apu +{ + private (float leftPcmSample, float rightPcmSample) MixAudioChannelsToStereoSamples() + { + int leftSum = 0; + int rightSum = 0; + bool isAnyDacEnabled = false; + + foreach (var channel in AvailableChannels) + { + if (channel.IsDacEnabled) + isAnyDacEnabled = true; + + if (channel is { IsChannelOn: true, IsDebugEnabled: true }) + { + if (channel.IsLeftSpeakerOn) leftSum += channel.CurrentOutput; + if (channel.IsRightSpeakerOn) rightSum += channel.CurrentOutput; + } + } + + //Apply master volume panning + leftSum *= LeftMasterVolume; + rightSum *= RightMasterVolume; + var (leftSample, rightSample) = (NormalizeToPcmSample(leftSum), NormalizeToPcmSample(rightSum)); + FrequencyFilters.ApplyHighPassFilter(ref leftSample, ref rightSample, isAnyDacEnabled); + + return (NormalizeToPcmSample(leftSum), NormalizeToPcmSample(rightSum)); + } + + private float NormalizeToPcmSample(float sample) + { + //Normalize digital [0-15]*4 channels to [0-2f], then shift to [-1f, 1f] + return sample / MaxDigitalSumOfOutputPerStereoChannel * 2f - 1f; + } +} \ No newline at end of file diff --git a/GameboyDotnet.Core/Sound/Apu.FrameSequencer.cs b/GameboyDotnet.Core/Sound/Apu.FrameSequencer.cs new file mode 100644 index 0000000..10a65d9 --- /dev/null +++ b/GameboyDotnet.Core/Sound/Apu.FrameSequencer.cs @@ -0,0 +1,69 @@ +namespace GameboyDotnet.Sound; + +public partial class Apu +{ + //Frame sequencer is ticked at 512Hz, this should keep it steady between DMG and GBC modes + private static readonly int FrameSequencerCyclesPerFrame = Cycles.CyclesPerSecond/512; + private int _frameSequencerCyclesTimer = FrameSequencerCyclesPerFrame; + private int _frameSequencerPosition = 0; + + private void StepFrameSequencer() + { + _frameSequencerCyclesTimer--; + + if (_frameSequencerCyclesTimer > 0) + return; + + _frameSequencerCyclesTimer = FrameSequencerCyclesPerFrame; + + _frameSequencerPosition = (_frameSequencerPosition + 1) & 0b111; //Wrap to 7 + + switch (_frameSequencerPosition) + { + case 0: + TickLengthCounters(); + break; + case 1: + break; + case 2: + TickLengthCounters(); + TickSweep(); + break; + case 3: + break; + case 4: + TickLengthCounters(); + break; + case 5: + break; + case 6: + TickLengthCounters(); + TickSweep(); + break; + case 7: + TickVolumeEnvelope(); + break; + } + } + + private void TickVolumeEnvelope() + { + SquareChannel1.TickVolumeEnvelopeTimer(); + SquareChannel2.TickVolumeEnvelopeTimer(); + WaveChannel.TickVolumeEnvelopeTimer(); + NoiseChannel.TickVolumeEnvelopeTimer(); + } + + private void TickSweep() + { + SquareChannel1.TickSweep(); + } + + private void TickLengthCounters() + { + SquareChannel1.StepLengthTimer(); + SquareChannel2.StepLengthTimer(); + WaveChannel.StepLengthTimer(); + NoiseChannel.StepLengthTimer(); + } +} \ No newline at end of file diff --git a/GameboyDotnet.Core/Sound/Apu.RegistersEvents.cs b/GameboyDotnet.Core/Sound/Apu.RegistersEvents.cs new file mode 100644 index 0000000..142d522 --- /dev/null +++ b/GameboyDotnet.Core/Sound/Apu.RegistersEvents.cs @@ -0,0 +1,51 @@ +using GameboyDotnet.Extensions; +using GameboyDotnet.Sound.Channels; + +namespace GameboyDotnet.Sound; + +public partial class Apu +{ + public void SetPowerState(ref byte value) + { + if (IsAudioOn && !value.IsBitSet(7)) + { + IsAudioOn = false; + LeftMasterVolume = 0; + RightMasterVolume = 0; + + SquareChannel1.Reset(); + SquareChannel2.Reset(); + WaveChannel.Reset(); + NoiseChannel.Reset(); + } + else if (!IsAudioOn && value.IsBitSet(7)) + { + IsAudioOn = true; + _frameSequencerCyclesTimer = FrameSequencerCyclesPerFrame; + _frameSequencerPosition = 0; + } + } + + public void SetChannelPanningStates(ref byte value) + { + SquareChannel1.IsLeftSpeakerOn = value.IsBitSet(4); + SquareChannel1.IsRightSpeakerOn = value.IsBitSet(0); + + SquareChannel2.IsLeftSpeakerOn = value.IsBitSet(5); + SquareChannel2.IsRightSpeakerOn = value.IsBitSet(1); + + WaveChannel.IsLeftSpeakerOn = value.IsBitSet(6); + WaveChannel.IsRightSpeakerOn = value.IsBitSet(2); + + NoiseChannel.IsLeftSpeakerOn = value.IsBitSet(7); + NoiseChannel.IsRightSpeakerOn = value.IsBitSet(3); + } + + public void SetVolumeControlStates(ref byte value) + { + //Ignores VIN input, bits 7 and 3 + //Value of 0 means 'very quiet', 7 means full volume + LeftMasterVolume = (byte)((value & 0b0111_0000) >> 4); + RightMasterVolume = (byte)(value & 0b0000_0111); + } +} \ No newline at end of file diff --git a/GameboyDotnet.Core/Sound/Apu.cs b/GameboyDotnet.Core/Sound/Apu.cs new file mode 100644 index 0000000..accf13d --- /dev/null +++ b/GameboyDotnet.Core/Sound/Apu.cs @@ -0,0 +1,55 @@ +using GameboyDotnet.Extensions; +using GameboyDotnet.Sound.Channels; +using GameboyDotnet.Sound.Channels.BuildingBlocks; + +namespace GameboyDotnet.Sound; + +public partial class Apu +{ + public AudioBuffer AudioBuffer { get; init; } + public SquareChannel1 SquareChannel1 { get; private set; } + public SquareChannel2 SquareChannel2 { get; private set; } + public WaveChannel WaveChannel { get; private set; } + public NoiseChannel NoiseChannel { get; private set; } + public BaseChannel[] AvailableChannels { get; private set; } + public bool IsAudioOn { get; private set; } + public byte LeftMasterVolume { get; private set; } + public byte RightMasterVolume { get; private set; } + + private const int MaxDigitalSumOfOutputPerStereoChannel = 15 * 4 * 7; //4 channels, 0-15 volume level each, 0-7 Left/Right Master volume level + + public Apu() + { + AudioBuffer = new AudioBuffer(); + SquareChannel1 = new SquareChannel1(AudioBuffer); + SquareChannel2 = new SquareChannel2(); + WaveChannel = new WaveChannel(AudioBuffer); + NoiseChannel = new NoiseChannel(); + AvailableChannels = [SquareChannel1, SquareChannel2, WaveChannel, NoiseChannel]; + } + + private int SampleCounter = 87; + + public void PushApuCycles(ref byte tCycles) + { + for (int i = tCycles; i > 0; i--) + { + StepFrameSequencer(); + SquareChannel1.Step(); + SquareChannel2.Step(); + WaveChannel.Step(); + NoiseChannel.Step(); + + SampleCounter--; + if (SampleCounter > 0) + continue; + + SampleCounter = 87; + if (IsAudioOn) + { + var (leftSample, rightSample) = MixAudioChannelsToStereoSamples(); + AudioBuffer.EnqueueSample(leftSample, rightSample); + } + } + } +} \ No newline at end of file diff --git a/GameboyDotnet.Core/Sound/AudioBuffer.cs b/GameboyDotnet.Core/Sound/AudioBuffer.cs new file mode 100644 index 0000000..407ebfe --- /dev/null +++ b/GameboyDotnet.Core/Sound/AudioBuffer.cs @@ -0,0 +1,35 @@ +using System.Collections.Concurrent; + +namespace GameboyDotnet.Sound +{ + public class AudioBuffer + { + private readonly ConcurrentQueue _sampleQueue = new(); + private readonly float[] _sampleBuffer = new float[BufferSize]; + private const int BufferSize = 1024*2; + private int CurrentBufferIndex = 0; + + public void EnqueueSample(float leftSample, float rightSample) + { + _sampleBuffer[CurrentBufferIndex++] = leftSample; + _sampleBuffer[CurrentBufferIndex++] = rightSample; + + if (CurrentBufferIndex >= BufferSize) + { + CurrentBufferIndex = 0; + float[] block = new float[BufferSize]; + Array.Copy(_sampleBuffer, block, BufferSize); + + if(_sampleQueue.Count < 10) + _sampleQueue.Enqueue(block); + + CurrentBufferIndex = 0; + } + } + + public bool TryDequeueSamples(out float[]? samples) + { + return _sampleQueue.TryDequeue(out samples); + } + } +} diff --git a/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseChannel.cs b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseChannel.cs new file mode 100644 index 0000000..18feabf --- /dev/null +++ b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseChannel.cs @@ -0,0 +1,178 @@ +using GameboyDotnet.Extensions; + +namespace GameboyDotnet.Sound.Channels.BuildingBlocks; + +public abstract class BaseChannel() +{ + //Debug + public bool IsDebugEnabled = true; + + //Shadow registers + public bool IsChannelOn; + public bool IsRightSpeakerOn; + public bool IsLeftSpeakerOn; + + //NRx1 + public int InitialLengthTimer; + + //NRX2 + public int VolumeEnvelopePace; //also referred to as: Volume Envelop Period + public EnvelopeDirection VolumeEnvelopeDirection; + public int InitialVolume; + public bool IsDacEnabled; + + //NRX3 + public byte PeriodLowOrRandomness; + + //NRX4 + public byte PeriodHigh; + public bool IsLengthEnabled; // Bit 6 + + public int GetPeriodValueFromRegisters => ((PeriodHigh & 0b111) << 8) | PeriodLowOrRandomness; + + public int LengthTimer; + public int PeriodTimer = 0; + public int VolumeEnvelopeTimer = 0; + protected int VolumeLevel; + + + public int CurrentOutput; + + public abstract void Step(); + + protected abstract void RefreshOutputState(); + + public virtual void Reset() + { + IsChannelOn = false; + IsRightSpeakerOn = false; + IsLeftSpeakerOn = false; + InitialLengthTimer = 0; + VolumeEnvelopePace = 0; + VolumeEnvelopeDirection = EnvelopeDirection.Descending; + InitialVolume = 0; + IsDacEnabled = false; + PeriodLowOrRandomness = 0; + PeriodHigh = 0; + IsLengthEnabled = false; + LengthTimer = 0; + PeriodTimer = 0; + VolumeEnvelopeTimer = 0; + VolumeLevel = 0; + } + + protected virtual bool StepPeriodTimer() + { + PeriodTimer--; + if (PeriodTimer > 0) + { + return false; + } + + ResetPeriodTimer(); + return true; + } + + public void StepLengthTimer() + { + if (!IsChannelOn) + return; + + //Check if LengthTimer is enabled + if (IsLengthEnabled && LengthTimer > 0) + { + LengthTimer--; + if (LengthTimer == 0) + { + IsChannelOn = false; + } + } + } + + public void TickVolumeEnvelopeTimer() + { + //https://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware - Obscure Behavior + //The volume envelope and sweep timers treat a period of 0 as 8. + int effectiveVolumeEnvelopePace = VolumeEnvelopePace == 0 ? 8 : VolumeEnvelopePace; + + VolumeEnvelopeTimer--; + + if (VolumeEnvelopeTimer <= 0) + { + VolumeEnvelopeTimer = effectiveVolumeEnvelopePace; + + if (VolumeEnvelopeDirection is EnvelopeDirection.Ascending && VolumeLevel < 15) + VolumeLevel++; + else if (VolumeEnvelopeDirection is EnvelopeDirection.Descending && VolumeLevel > 0) + VolumeLevel--; + } + } + + public abstract void SetLengthTimer(ref byte value); + + public virtual void SetVolumeRegister(ref byte value) + { + InitialVolume = (value & 0b1111_0000) >> 4; + VolumeEnvelopeDirection = value.IsBitSet(3) ? EnvelopeDirection.Ascending : EnvelopeDirection.Descending; + VolumeEnvelopePace = value & 0b111; + + // DAC is enabled if any of the upper 5 bits are set (0xF8 mask) + IsDacEnabled = (value & 0b1111_1000) != 0; + if (!IsDacEnabled) + { + IsChannelOn = false; + } + } + + public virtual void SetPeriodLowOrRandomnessRegister(ref byte value) + { + PeriodLowOrRandomness = value; + } + + public void SetPeriodHighControl(ref byte value) + { + bool oldLengthEnabled = IsLengthEnabled; + IsLengthEnabled = value.IsBitSet(6); + PeriodHigh = (byte)(value & 0b111); + + //TODO: if(!oldLengthEnabled && IsLengthEnabled && FrameSequencerWillNotClockLengthThisSteps && LengthTimer >0) + // { + // LengthTimer--; + // if (LengthTimer == 0) + // { + // IsChannelOn = false; + // } + // } + + if (value.IsBitSet(7)) + { + Trigger(); + } + } + + protected virtual void Trigger() + { + IsChannelOn = IsDacEnabled; + + if (LengthTimer == 0) + { + ResetLengthTimerValue(); + // TODO: if (IsLengthEnabled && FrameSequencerWillNotClockLengthThisStep()) + // { + // LengthTimer--; + // } + } + + ResetPeriodTimer(); + int effectiveVolumeEnvelopePace = VolumeEnvelopePace == 0 ? 8 : VolumeEnvelopePace; + VolumeEnvelopeTimer = effectiveVolumeEnvelopePace; + VolumeLevel = InitialVolume; + } + + protected abstract void ResetLengthTimerValue(); + + protected virtual void ResetPeriodTimer() + { + PeriodTimer = (2048 - GetPeriodValueFromRegisters) * 4; + } +} diff --git a/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseSquareChannel.cs b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseSquareChannel.cs new file mode 100644 index 0000000..926d32c --- /dev/null +++ b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseSquareChannel.cs @@ -0,0 +1,90 @@ +namespace GameboyDotnet.Sound.Channels.BuildingBlocks; + +public abstract class BaseSquareChannel() : BaseChannel() +{ + protected byte[][] DutyCycles = + [ + [0, 0, 0, 0, 0, 0, 0, 1], //12,5% + [1, 0, 0, 0, 0, 0, 0, 1], //25% + [1, 0, 0, 0, 0, 1, 1, 1], //50% + [0, 1, 1, 1, 1, 1, 1, 0] //72,5% + ]; + + public int DutyCycleStep = 0; + + //NR11-NR21 + public int WaveDutyIndex = 0; + + public override void Step() + { + if (!IsChannelOn) + return; + + var isPeriodTimerFinished = StepPeriodTimer(); + + if (isPeriodTimerFinished) + { + DutyCycleStep = (DutyCycleStep + 1) & 0b111; //Wrap after 7 + RefreshOutputState(); + } + } + + protected override bool StepPeriodTimer() + { + PeriodTimer--; + if (PeriodTimer > 0) + { + return false; + } + + ResetPeriodTimerPreserveLowerBits(); + + return true; + } + + public override void Reset() + { + base.Reset(); + DutyCycleStep = 0; + WaveDutyIndex = 0; + } + + protected override void RefreshOutputState() + { + if (!IsChannelOn) + return; + + CurrentOutput = DutyCycles[WaveDutyIndex][DutyCycleStep] == 1 && IsChannelOn + ? VolumeLevel // (Hi-state) 1 * [0–15] + : 0; + } + + public override void SetLengthTimer(ref byte value) + { + WaveDutyIndex = (value & 0b1100_0000) >> 6; + InitialLengthTimer = value & 0b0011_1111; + LengthTimer = 64 - InitialLengthTimer; + } + + protected override void Trigger() + { + DutyCycleStep = 0; + base.Trigger(); + } + + protected override void ResetLengthTimerValue() + { + LengthTimer = 64; + } + + protected override void ResetPeriodTimer() + { + PeriodTimer = (2048 - GetPeriodValueFromRegisters) * 4; + } + + public void ResetPeriodTimerPreserveLowerBits() + { + int lowerBitsOfPeriodDividerTimer = PeriodTimer & 0b11; + PeriodTimer = ((2048 - GetPeriodValueFromRegisters) * 4 & ~0b11) | lowerBitsOfPeriodDividerTimer; + } +} \ No newline at end of file diff --git a/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/EnvelopeDirection.cs b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/EnvelopeDirection.cs new file mode 100644 index 0000000..2e8116f --- /dev/null +++ b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/EnvelopeDirection.cs @@ -0,0 +1,7 @@ +namespace GameboyDotnet.Sound.Channels.BuildingBlocks; + +public enum EnvelopeDirection +{ + Descending = 0, + Ascending = 1 +} \ No newline at end of file diff --git a/GameboyDotnet.Core/Sound/Channels/NoiseChannel.cs b/GameboyDotnet.Core/Sound/Channels/NoiseChannel.cs new file mode 100644 index 0000000..9c00fb3 --- /dev/null +++ b/GameboyDotnet.Core/Sound/Channels/NoiseChannel.cs @@ -0,0 +1,39 @@ +using GameboyDotnet.Sound.Channels.BuildingBlocks; + +namespace GameboyDotnet.Sound.Channels; + +public class NoiseChannel() : BaseChannel() +{ + public override void Step() + { + if (!IsChannelOn) + return; + + RefreshOutputState(); + } + + protected override void RefreshOutputState() + { + + } + + protected override void ResetLengthTimerValue() + { + LengthTimer = 64; + } + + /// + /// Period low bits values are used for frequency and randomness + /// + /// + public override void SetPeriodLowOrRandomnessRegister(ref byte value) + { + PeriodLowOrRandomness = value; + } + + public override void SetLengthTimer(ref byte value) + { + InitialLengthTimer = value; + LengthTimer = 64 - InitialLengthTimer; + } +} \ No newline at end of file diff --git a/GameboyDotnet.Core/Sound/Channels/SquareChannel1.cs b/GameboyDotnet.Core/Sound/Channels/SquareChannel1.cs new file mode 100644 index 0000000..09825e6 --- /dev/null +++ b/GameboyDotnet.Core/Sound/Channels/SquareChannel1.cs @@ -0,0 +1,121 @@ +using GameboyDotnet.Extensions; +using GameboyDotnet.Sound.Channels.BuildingBlocks; + +namespace GameboyDotnet.Sound.Channels; + +public class SquareChannel1(AudioBuffer audioBuffer) : BaseSquareChannel() +{ + private int _sweepTimer; + private bool _isSweepEnabled; + private int _periodShadowRegister; + + private byte _currentPaceValue; + private byte _requestedPaceValue; + private EnvelopeDirection _frequencyEnvelopeDirection; //True = Addition, False = Subtraction + private byte _individualStep; + + public override void Reset() + { + base.Reset(); + _sweepTimer = 0; + _isSweepEnabled = false; + _periodShadowRegister = 0; + _currentPaceValue = 0; + _requestedPaceValue = 0; + } + + public void TickSweep() + { + if (!_isSweepEnabled) + return; + + _sweepTimer--; + if (_sweepTimer <= 0) + { + //https://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware - Obscure Behavior + //The volume envelope and sweep timers treat a period of 0 as 8. + int effectivePace = _currentPaceValue == 0 ? 8 : _currentPaceValue; + _sweepTimer = effectivePace; + + if (_currentPaceValue != 0) + { + int newPeriodValue = CalculatePeriodAfterSweep(); + + if (newPeriodValue <= 2047 && _individualStep != 0) + { + _periodShadowRegister = newPeriodValue; + UpdatePeriodRegisters(newPeriodValue); + + //Check if next sweep will overflow the period value, then shutdown channel if true + int secondNewPeriod = CalculatePeriodAfterSweep(); + if (secondNewPeriod > 2047) + { + IsChannelOn = false; + } + } + } + } + + //TODO: Implement Sweep + } + + private void UpdatePeriodRegisters(int newPeriodValue) + { + PeriodLowOrRandomness = (byte)(newPeriodValue & 0xFF); + PeriodHigh = (byte)((PeriodHigh & 0xF8) | ((newPeriodValue >> 8) & 0x07)); + ResetPeriodTimer(); + } + + public void SetSweepState(ref byte value) + { + _requestedPaceValue = (byte)((value & 0b0111_0000) >> 4); + if (_requestedPaceValue == 0) + { + _isSweepEnabled = false; + return; + } + + _frequencyEnvelopeDirection = value.IsBitSet(3) ? EnvelopeDirection.Descending : EnvelopeDirection.Ascending; + _individualStep = (byte)(value & 0b0000_0111); + } + + protected override void Trigger() + { + // Square 1's frequency is copied to shadow register + _periodShadowRegister = GetPeriodValueFromRegisters; + + // Sweep timer is reloaded + int effectivePace = _requestedPaceValue == 0 ? 8 : _requestedPaceValue; + _sweepTimer = effectivePace; + _currentPaceValue = _requestedPaceValue; + + // Internal enabled flag is set if either sweep period or shift are non-zero + _isSweepEnabled = (_requestedPaceValue != 0 || _individualStep != 0); + + // If sweep shift is non-zero, perform calculation and overflow check immediately + if (_individualStep != 0) + { + int newFrequency = CalculatePeriodAfterSweep(); + if (newFrequency > 2047) + { + IsChannelOn = false; + } + } + + base.Trigger(); + } + + private int CalculatePeriodAfterSweep() + { + if (!FrequencyFilters.IsHighPassFilterActive) + { + return _periodShadowRegister; + } + + int periodSweep = _periodShadowRegister >> _individualStep; + + return _frequencyEnvelopeDirection is EnvelopeDirection.Ascending + ? _periodShadowRegister + periodSweep + : _periodShadowRegister - periodSweep; + } +} \ No newline at end of file diff --git a/GameboyDotnet.Core/Sound/Channels/SquareChannel2.cs b/GameboyDotnet.Core/Sound/Channels/SquareChannel2.cs new file mode 100644 index 0000000..0c11d7d --- /dev/null +++ b/GameboyDotnet.Core/Sound/Channels/SquareChannel2.cs @@ -0,0 +1,5 @@ +using GameboyDotnet.Sound.Channels.BuildingBlocks; + +namespace GameboyDotnet.Sound.Channels; + +public class SquareChannel2() : BaseSquareChannel(); \ No newline at end of file diff --git a/GameboyDotnet.Core/Sound/Channels/WaveChannel.cs b/GameboyDotnet.Core/Sound/Channels/WaveChannel.cs new file mode 100644 index 0000000..f6288e7 --- /dev/null +++ b/GameboyDotnet.Core/Sound/Channels/WaveChannel.cs @@ -0,0 +1,87 @@ +using GameboyDotnet.Extensions; +using GameboyDotnet.Sound.Channels.BuildingBlocks; + +namespace GameboyDotnet.Sound.Channels; + +public class WaveChannel(AudioBuffer audioBuffer) : BaseChannel() +{ + public byte WaveVolumeIndex = 0; + private readonly byte[] _waveRam = new byte[16]; + private readonly byte[] _waveSampleBuffer = new byte[32]; + private int _waveFormCurrentIndex = 0; + + public override void Step() + { + if (!IsChannelOn) + return; + + var isPeriodTimerFinished = StepPeriodTimer(); + + if (isPeriodTimerFinished) + { + _waveFormCurrentIndex = (_waveFormCurrentIndex + 1) & 0b1111; //Wrap after 15 + RefreshOutputState(); + } + } + + protected override void RefreshOutputState() + { + //This effectively maps WaveVolumeIndex to proper multipliers [0f, 1f, 0.5f, 0.25f] while avoiding multiplication + CurrentOutput = IsChannelOn && WaveVolumeIndex != 0 + ? _waveSampleBuffer[_waveFormCurrentIndex] >> (WaveVolumeIndex - 1) + : 0; + } + + protected override void ResetLengthTimerValue() + { + LengthTimer = 256; + } + + public void SetDacStatus(ref byte value) + { + IsDacEnabled = value.IsBitSet(7); + if (!IsDacEnabled) + { + IsChannelOn = false; + } + } + + public override void SetVolumeRegister(ref byte value) + { + WaveVolumeIndex = (byte)((value & 0b0110_0000) >> 5); + } + + protected override void ResetPeriodTimer() + { + PeriodTimer = (2048 - GetPeriodValueFromRegisters) * 2; + } + + public override void Reset() + { + VolumeLevel = 0; + _waveFormCurrentIndex = 0; + base.Reset(); + } + + public void WriteWaveRam(ref ushort address, ref byte value) + { + var ramAddress = address - 0xFF30; + _waveRam[ramAddress] = value; + + //Wave byte contains 2 samples (4 bits each), extract upper and lower 4 bits + _waveSampleBuffer[ramAddress * 2] = (byte)((value & 0b1111_0000) >> 4); + _waveSampleBuffer[ramAddress * 2 + 1] = (byte)(value & 0b0000_1111); + } + + public byte ReadWaveRam(ref ushort address) + { + var ramAddress = address - 0xFF30; + return _waveRam[ramAddress]; + } + + public override void SetLengthTimer(ref byte value) + { + InitialLengthTimer = value; + LengthTimer = 256 - InitialLengthTimer; + } +} \ No newline at end of file diff --git a/GameboyDotnet.Core/Sound/FrequencyFilters.cs b/GameboyDotnet.Core/Sound/FrequencyFilters.cs new file mode 100644 index 0000000..20492f1 --- /dev/null +++ b/GameboyDotnet.Core/Sound/FrequencyFilters.cs @@ -0,0 +1,31 @@ +namespace GameboyDotnet.Sound; + +public static class FrequencyFilters +{ + /// + /// Debug only + /// + /// + public static bool IsHighPassFilterActive = true; + + private static float _capacitorLeft = 0f; + private static float _capacitorRight = 0f; + private const float CapacitorChargingRatio = 0.995948f; + + public static void ApplyHighPassFilter(ref float leftSample, ref float rightSample, bool isAnyDacEnabled = true) + { + if (!IsHighPassFilterActive) + { + return; + } + + float leftOutput = 0.0f; + float rightOutput = 0.0f; + + leftOutput = leftSample - _capacitorLeft; + rightOutput = rightSample - _capacitorRight; + + _capacitorLeft = leftSample - leftOutput * CapacitorChargingRatio; + _capacitorRight = rightSample - rightOutput * CapacitorChargingRatio; + } +} \ No newline at end of file diff --git a/GameboyDotnet.Core/Sound/RingBuffer.cs b/GameboyDotnet.Core/Sound/RingBuffer.cs new file mode 100644 index 0000000..795807d --- /dev/null +++ b/GameboyDotnet.Core/Sound/RingBuffer.cs @@ -0,0 +1,6 @@ +namespace GameboyDotnet.Sound; + +public class RingBuffer +{ + // public +} \ No newline at end of file diff --git a/GameboyDotnet.Core/Timers/DividerTimer.cs b/GameboyDotnet.Core/Timers/DivTimer.cs similarity index 62% rename from GameboyDotnet.Core/Timers/DividerTimer.cs rename to GameboyDotnet.Core/Timers/DivTimer.cs index b72e5c4..2c3c8b2 100644 --- a/GameboyDotnet.Core/Timers/DividerTimer.cs +++ b/GameboyDotnet.Core/Timers/DivTimer.cs @@ -1,18 +1,19 @@ -using GameboyDotnet.Components; -using GameboyDotnet.Memory; +using GameboyDotnet.Memory; namespace GameboyDotnet.Timers; -public class DividerTimer +public class DivTimer(MemoryController memoryController) { private int _dividerCycleCounter; - internal void CheckAndIncrementTimer(ref byte tStates, MemoryController memoryController) + internal void CheckAndIncrementTimer(ref byte tStates) { _dividerCycleCounter += tStates; + if (_dividerCycleCounter >= Cycles.DividerCycles) - { + { _dividerCycleCounter -= Cycles.DividerCycles; + memoryController.IncrementByte(Constants.DIVRegister); } } diff --git a/GameboyDotnet.Core/Timers/MainTimer.cs b/GameboyDotnet.Core/Timers/TimaTimer.cs similarity index 85% rename from GameboyDotnet.Core/Timers/MainTimer.cs rename to GameboyDotnet.Core/Timers/TimaTimer.cs index b7273ca..93982dd 100644 --- a/GameboyDotnet.Core/Timers/MainTimer.cs +++ b/GameboyDotnet.Core/Timers/TimaTimer.cs @@ -4,13 +4,14 @@ namespace GameboyDotnet.Timers; -public class MainTimer +public class TimaTimer(MemoryController memoryController) { private int _tStatesCounter; + private const int TimaControlIORegisterOffset = 0x07; - internal void CheckAndIncrementTimer(ref byte durationTStates, MemoryController memoryController) + internal void CheckAndIncrementTimer(ref byte durationTStates) { - var timerControl = memoryController.ReadByte(Constants.TACRegister); + var timerControl = memoryController.IoRegisters.MemorySpaceView[TimaControlIORegisterOffset]; if (!timerControl.IsBitSet(2)) //Timer is disabled, do nothing return; @@ -26,6 +27,7 @@ internal void CheckAndIncrementTimer(ref byte durationTStates, MemoryController if (timerValue == 0) //Overflow, TIMA = TMA, then request an interrupt { memoryController.WriteByte(Constants.TIMARegister, memoryController.ReadByte(Constants.TMARegister)); + memoryController.WriteByte( Constants.IFRegister, memoryController.ReadByte(Constants.IFRegister).SetBit(2) diff --git a/GameboyDotnet.SDL/Arial.ttf b/GameboyDotnet.SDL/Arial.ttf new file mode 100644 index 0000000..7ff88f2 Binary files /dev/null and b/GameboyDotnet.SDL/Arial.ttf differ diff --git a/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj b/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj index 99b6da9..f68be54 100644 --- a/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj +++ b/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj @@ -62,16 +62,28 @@ PreserveNewest - + PreserveNewest - + PreserveNewest PreserveNewest - + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + PreserveNewest diff --git a/GameboyDotnet.SDL/Program.cs b/GameboyDotnet.SDL/Program.cs index 2a20e29..13aee1c 100644 --- a/GameboyDotnet.SDL/Program.cs +++ b/GameboyDotnet.SDL/Program.cs @@ -1,9 +1,14 @@ -using GameboyDotnet; +using System.Diagnostics; +using GameboyDotnet; using GameboyDotnet.Common; +using GameboyDotnet.Graphics; using GameboyDotnet.SDL; +using GameboyDotnet.SDL.SaveStates; +using GameboyDotnet.Sound; using Microsoft.Extensions.Configuration; using static SDL2.SDL; +// Load emulator settings from appsettings.json var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) @@ -12,33 +17,35 @@ var emulatorSettings = new EmulatorSettings(); configuration.GetSection("EmulatorSettings").Bind(emulatorSettings); var logger = LoggerHelper.GetLogger(emulatorSettings.LogLevel); - -var (renderer, window) = Renderer.InitializeRendererAndWindow(logger, emulatorSettings); -var gameboy = new Gameboy(logger); var keyboardMapper = new KeyboardMapper(emulatorSettings.Keymap); var romPath = Path.IsPathRooted(emulatorSettings.RomPath) ? Path.Combine(emulatorSettings.RomPath) : Path.Combine(Directory.GetCurrentDirectory(), emulatorSettings.RomPath); +//Initialize SDL renderer and window +var (renderer, window) = SdlRenderer.InitializeRendererAndWindow(logger, emulatorSettings); + +var gameboy = new Gameboy(logger); +var audioPlayer = new SdlAudio(gameboy.Apu.AudioBuffer); +audioPlayer.Initialize(); + var stream = File.OpenRead(romPath); gameboy.LoadProgram(stream); var cts = new CancellationTokenSource(); bool running = true; -int framesRequested = 0; gameboy.ExceptionOccured += (_, _) => { cts.Cancel(); running = false; }; -gameboy.DisplayUpdated += (_, _) => -{ - Interlocked.Increment(ref framesRequested); -}; Task.Run(() => gameboy.RunAsync(emulatorSettings.FrameLimitEnabled, cts.Token)); +var userActionText = string.Empty; +var userActionTextFrameCounter = 0; + // Main SDL loop while (running && !cts.IsCancellationRequested) { @@ -47,10 +54,45 @@ switch (e.type) { case SDL_EventType.SDL_KEYDOWN: + switch (e.key.keysym.sym) + { + case SDL_Keycode.SDLK_F5: + gameboy.IsMemoryDumpRequested = true; + break; + case SDL_Keycode.SDLK_F8: + SaveDumper.LoadState(gameboy, romPath); + break; + case SDL_Keycode.SDLK_p: + gameboy.SwitchFramerateLimiter(); + break; + case SDL_Keycode.SDLK_1: + gameboy.Apu.SquareChannel1.IsDebugEnabled = !gameboy.Apu.SquareChannel1.IsDebugEnabled; + userActionText = $"CH1: {(gameboy.Apu.SquareChannel1.IsDebugEnabled ? "on" : "off")}"; + userActionTextFrameCounter = 120; + break; + case SDL_Keycode.SDLK_2: + gameboy.Apu.SquareChannel2.IsDebugEnabled = !gameboy.Apu.SquareChannel2.IsDebugEnabled; + userActionText = $"CH2: {(gameboy.Apu.SquareChannel2.IsDebugEnabled ? "on" : "off")}"; + userActionTextFrameCounter = 120; + break; + case SDL_Keycode.SDLK_3: + gameboy.Apu.WaveChannel.IsDebugEnabled = !gameboy.Apu.WaveChannel.IsDebugEnabled; + userActionText = $"CH3: {(gameboy.Apu.WaveChannel.IsDebugEnabled ? "on" : "off")}"; + userActionTextFrameCounter = 120; + break; + case SDL_Keycode.SDLK_4: + gameboy.Apu.NoiseChannel.IsDebugEnabled = !gameboy.Apu.NoiseChannel.IsDebugEnabled; + userActionText = $"CH4: {(gameboy.Apu.NoiseChannel.IsDebugEnabled ? "on" : "off")}"; + userActionTextFrameCounter = 120; + break; + case SDL_Keycode.SDLK_5: + FrequencyFilters.IsHighPassFilterActive = !FrequencyFilters.IsHighPassFilterActive; + userActionText = $"High-Pass filter: {(FrequencyFilters.IsHighPassFilterActive ? "on" : "off")}"; + userActionTextFrameCounter = 120; + break; + } if (keyboardMapper.TryGetGameboyKey(e.key.keysym.sym, out var keyPressed)) gameboy.PressButton(keyPressed); - if (e.key.keysym.sym is SDL_Keycode.SDLK_p) - gameboy.SwitchDebugMode(); break; case SDL_EventType.SDL_KEYUP: if (keyboardMapper.TryGetGameboyKey(e.key.keysym.sym, out var keyReleased)) @@ -59,16 +101,25 @@ case SDL_EventType.SDL_QUIT: cts.Cancel(); running = false; - Renderer.Destroy(renderer, window); + SdlRenderer.Destroy(renderer, window); break; } } - - if (framesRequested > 0) + + if(gameboy.Ppu.FrameBuffer.TryDequeueFrame(out var frame)) { - Interlocked.Decrement(ref framesRequested); - Renderer.RenderStates(ref renderer, gameboy.Ppu.Lcd, ref window); + string bufferedFramesText = $"Speed: {gameboy.Ppu.FrameBuffer.Fps/ 60.0 * 100.0:0.0}% / {gameboy.Ppu.FrameBuffer.Fps:0} FPS"; + if(userActionTextFrameCounter > 0) + { + userActionTextFrameCounter--; + } + else + { + userActionText = string.Empty; + } + SdlRenderer.RenderStates(ref renderer, ref window, frame!, string.Join(" \n ", bufferedFramesText, userActionText)); } } -Renderer.Destroy(renderer, window); \ No newline at end of file +audioPlayer.Cleanup(); +SdlRenderer.Destroy(renderer, window); \ No newline at end of file diff --git a/GameboyDotnet.SDL/SaveStates/SaveDumper.cs b/GameboyDotnet.SDL/SaveStates/SaveDumper.cs new file mode 100644 index 0000000..d9bc9fc --- /dev/null +++ b/GameboyDotnet.SDL/SaveStates/SaveDumper.cs @@ -0,0 +1,36 @@ +namespace GameboyDotnet.SDL.SaveStates; + +public static class SaveDumper +{ + public static void SaveState(Gameboy gameboy, string romPath) + { + var savePath = romPath.Replace(".gb", ".gbsav"); + try + { + File.WriteAllBytes(savePath, gameboy.DumpMemory()); + Console.WriteLine("Saved state"); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + + } + + public static void LoadState(Gameboy gameboy, string romPath) + { + var savePath = romPath.Replace(".gb", ".gbsav"); + try + { + var saveState = File.ReadAllBytes(savePath); + gameboy.LoadMemoryDump(saveState); + Console.WriteLine("Loaded save state"); + } + catch(Exception e) + { + Console.WriteLine(e); + throw; + } + } +} \ No newline at end of file diff --git a/GameboyDotnet.SDL/SdlAudio.cs b/GameboyDotnet.SDL/SdlAudio.cs new file mode 100644 index 0000000..4bce21f --- /dev/null +++ b/GameboyDotnet.SDL/SdlAudio.cs @@ -0,0 +1,74 @@ +using System.Runtime.InteropServices; +using GameboyDotnet.Sound; +using static SDL2.SDL; + +namespace GameboyDotnet.SDL; + +public class SdlAudio +{ + private readonly AudioBuffer _audioBuffer; + private uint _audioDevice; + private SDL_AudioCallback _audioCallbackDelegate; + + public SdlAudio(AudioBuffer audioBuffer) + { + _audioBuffer = audioBuffer; + } + + public void Initialize() + { + _audioCallbackDelegate = AudioCallbackHandler; + + var desiredSpec = new SDL_AudioSpec + { + freq = 48000, + format = AUDIO_F32SYS, //TODO: Check which format should be set + channels = 2, + samples = 1024, + callback = _audioCallbackDelegate + }; + + //Device: null uses default device + _audioDevice = SDL_OpenAudioDevice( + device: null, + iscapture: 0, + ref desiredSpec, + out _, + allowed_changes: 0); + + if (_audioDevice <= 0) + { + Console.WriteLine($"SDL OpenAudioDevice Error: {SDL_GetError()}"); + return; + } + + SDL_PauseAudioDevice(_audioDevice, 0); + } + + private void AudioCallbackHandler(IntPtr userdata, IntPtr stream, int len) + { + int sampleCount = len / sizeof(float); + float[] samples = new float[sampleCount]; + int offset = 0; + + while(offset < sampleCount) + { + if (!_audioBuffer.TryDequeueSamples(out float[]? sampleBatch) || sampleBatch == null) + { + break; + } + + int samplesToCopy = Math.Min(sampleBatch.Length, sampleCount - offset); + Array.Copy(sampleBatch, 0, samples, offset, samplesToCopy); + offset += samplesToCopy; + } + + Marshal.Copy(samples, 0, stream, samples.Length); + } + + public void Cleanup() + { + SDL_CloseAudioDevice(_audioDevice); + SDL_Quit(); + } +} diff --git a/GameboyDotnet.SDL/Renderer.cs b/GameboyDotnet.SDL/SdlRenderer.cs similarity index 63% rename from GameboyDotnet.SDL/Renderer.cs rename to GameboyDotnet.SDL/SdlRenderer.cs index 1589ceb..3891ca4 100644 --- a/GameboyDotnet.SDL/Renderer.cs +++ b/GameboyDotnet.SDL/SdlRenderer.cs @@ -1,12 +1,11 @@ using System.Diagnostics; -using GameboyDotnet.Graphics; using Microsoft.Extensions.Logging; using SDL2; using static SDL2.SDL; namespace GameboyDotnet.SDL; -public static class Renderer +public static class SdlRenderer { private const int ScreenWidth = 160; private const int ScreenHeight = 144; @@ -17,7 +16,9 @@ public static class Renderer private static readonly SDL_Color DarkGray = new() { r = 85, g = 85, b = 85, a = 255 }; private static readonly SDL_Color Black = new() { r = 0, g = 0, b = 0, a = 255 }; - public static void RenderStates(ref nint renderer, Lcd lcd, ref nint window) + private static IntPtr _font; + + public static void RenderStates(ref IntPtr renderer, ref IntPtr window, byte[,] frame, string fpsText) { try { @@ -33,19 +34,31 @@ public static void RenderStates(ref nint renderer, Lcd lcd, ref nint window) // Calculate scaled window size int scaledWidth = (int)(ScreenWidth * scale); int scaledHeight = (int)(ScreenHeight * scale); - - SDL_SetRenderDrawColor(renderer, White.r, White.g, White.b, 255); // Clear the renderer - SDL_RenderClear(renderer); + SDL_RenderSetLogicalSize(renderer, scaledWidth, scaledHeight); + SDL_SetRenderDrawColor(renderer, DarkGray.r, DarkGray.g, DarkGray.b, 128); // Clear the renderer + SDL_RenderClear(renderer); //Clear everything with black color + + // Draw the Gameboy screen area as a white rectangle + // Draw the Gameboy screen area as a white rectangle + SDL_Rect gameboyScreenRect = new SDL_Rect + { + x = 0, + y = 0, + w = scaledWidth, + h = scaledHeight + }; + SDL_SetRenderDrawColor(renderer, White.r, White.g, White.b, White.a); + SDL_RenderFillRect(renderer, ref gameboyScreenRect); // Draw the scanlines for (int y = 0; y < ScreenHeight; y++) { for (int x = 0; x < ScreenWidth; x++) { - if (lcd.Buffer[x, y] != 0) + if (frame[x, y] != 0) { - var color = lcd.Buffer[x, y] switch + var color = frame[x, y] switch { 0 => White, 1 => LightGray, @@ -66,6 +79,9 @@ public static void RenderStates(ref nint renderer, Lcd lcd, ref nint window) } } + // Display FPS + RenderText(ref renderer, fpsText, 15, 50); + SDL_RenderPresent(renderer); // Render the frame; } catch (Exception ex) @@ -77,7 +93,7 @@ public static void RenderStates(ref nint renderer, Lcd lcd, ref nint window) public static (nint renderer, nint window) InitializeRendererAndWindow(ILogger logger, EmulatorSettings emulatorSettings) { - if (SDL_Init(SDL_INIT_VIDEO) < 0) + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) { logger.LogCritical("There was an issue initializing SDL. {SDL_GetError()}", SDL_GetError()); } @@ -89,7 +105,7 @@ public static (nint renderer, nint window) InitializeRendererAndWindow(ILogger