From fadedc4d7cabe0ab610ae1cfe972aad88145aa81 Mon Sep 17 00:00:00 2001 From: Szymon Sandura Date: Mon, 10 Feb 2025 21:13:09 +0100 Subject: [PATCH 01/11] PPU memory access speed up; Added early save state support; --- GameboyDotnet.Core/Constants.cs | 2 +- GameboyDotnet.Core/Gameboy.Dump.cs | 49 ++++++ GameboyDotnet.Core/Gameboy.Events.cs | 6 + GameboyDotnet.Core/Gameboy.cs | 39 +++-- GameboyDotnet.Core/GameboyDotnet.Core.csproj | 1 + GameboyDotnet.Core/Graphics/Lcd.cs | 39 ++--- GameboyDotnet.Core/Graphics/Ppu.cs | 20 +-- .../Memory/BuildingBlocks/FixedBank.cs | 1 + .../Memory/BuildingBlocks/IoBank.cs | 54 +------ GameboyDotnet.Core/Memory/MemoryController.cs | 25 ++- .../Processor/Cpu.InterruptHandling.cs | 29 ++++ .../Processor/Cpu.OperationBlocks.Block0.cs | 23 ++- GameboyDotnet.Core/Processor/Cpu.cs | 35 +---- GameboyDotnet.Core/Processor/CpuRegister.cs | 146 ++++++++---------- GameboyDotnet.SDL/Arial.ttf | Bin 0 -> 275572 bytes GameboyDotnet.SDL/GameboyDotnet.SDL.csproj | 11 +- GameboyDotnet.SDL/Program.cs | 33 +++- GameboyDotnet.SDL/Renderer.cs | 63 +++++++- GameboyDotnet.SDL/SaveStates/SaveDumper.cs | 36 +++++ 19 files changed, 365 insertions(+), 247 deletions(-) create mode 100644 GameboyDotnet.Core/Gameboy.Dump.cs create mode 100644 GameboyDotnet.Core/Processor/Cpu.InterruptHandling.cs create mode 100644 GameboyDotnet.SDL/Arial.ttf create mode 100644 GameboyDotnet.SDL/SaveStates/SaveDumper.cs 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/Gameboy.Dump.cs b/GameboyDotnet.Core/Gameboy.Dump.cs new file mode 100644 index 0000000..0cfdff3 --- /dev/null +++ b/GameboyDotnet.Core/Gameboy.Dump.cs @@ -0,0 +1,49 @@ +namespace GameboyDotnet; + +public partial class Gameboy +{ + public byte[] DumpMemory() + { + IsMemoryDumpingActive = true; + var memoryDump = new byte[(0xFFFF + 1) + 12 + 2]; //Address space + 6 registers + 2 timers + for(int i = 0; i < memoryDump.Length; i++) + { + memoryDump[i] = Cpu.MemoryController.ReadByte((ushort)i); + } + IsMemoryDumpingActive = false; + 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; + + return memoryDump; + } + + public void LoadMemoryDump(byte[] dump) + { + IsMemoryDumpingActive = 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]; + IsMemoryDumpingActive = 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..6a88d48 100644 --- a/GameboyDotnet.Core/Gameboy.cs +++ b/GameboyDotnet.Core/Gameboy.cs @@ -14,7 +14,8 @@ public partial class Gameboy public Ppu Ppu { get; } public MainTimer TimaTimer { get; } = new(); public DividerTimer DivTimer { get; } = new(); - public bool IsDebugMode { get; private set; } + public bool IsFrameLimiterEnabled; + internal bool IsMemoryDumpingActive; public Gameboy(ILogger logger) { @@ -31,13 +32,21 @@ public void LoadProgram(FileStream stream) public Task RunAsync(bool frameLimitEnabled, CancellationToken ctsToken) { + IsFrameLimiterEnabled = frameLimitEnabled; var frameTimeTicks = TimeSpan.FromMilliseconds(16.75).Ticks; var cyclesPerFrame = Cycles.CyclesPerFrame; var currentCycles = 0; + var frameCounter = 0; while (!ctsToken.IsCancellationRequested) { + if (IsMemoryDumpingActive && frameCounter == 0) + { + Task.Delay(TimeSpan.FromSeconds(1), ctsToken).RunSynchronously(); + continue; + } + try { var startTime = Stopwatch.GetTimestamp(); @@ -51,23 +60,20 @@ public Task RunAsync(bool frameLimitEnabled, CancellationToken ctsToken) currentCycles += tStates; } UpdateJoypadState(); - + + frameCounter++; currentCycles -= cyclesPerFrame; DisplayUpdated.Invoke(this, EventArgs.Empty); - - // if (frameLimitEnabled) - // { - // var remainingTime = targetTime - Stopwatch.GetTimestamp(); - // if (remainingTime > 0) - // { - // SpinWait.SpinUntil(() => Stopwatch.GetTimestamp() >= targetTime); - // } - // } - if(frameLimitEnabled) + if(frameCounter % 60 == 0) + { + frameCounter = 0; + } + + 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 +87,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/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..db772d3 100644 --- a/GameboyDotnet.Core/Graphics/Ppu.cs +++ b/GameboyDotnet.Core/Graphics/Ppu.cs @@ -101,15 +101,17 @@ private void PushScanlineToBuffer() 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; @@ -221,10 +223,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..2238467 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 ReadOnlySpan 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..0c977c5 100644 --- a/GameboyDotnet.Core/Memory/BuildingBlocks/IoBank.cs +++ b/GameboyDotnet.Core/Memory/BuildingBlocks/IoBank.cs @@ -6,9 +6,6 @@ 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; @@ -21,29 +18,6 @@ public IoBank(int startAddress, int endAddress, string name, ILogger lo 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)) @@ -69,35 +43,11 @@ public override void WriteByte(ref ushort address, ref byte value) public override byte ReadByte(ref ushort address) { - if (address == Constants.IERegister) - { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("ReadByte returning cached value of InterruptEnableRegister: {value:X}", _interruptEnableRegister); - - return _interruptEnableRegister; - } - - if (address == Constants.IFRegister) - { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("ReadByte returning cached value of InterruptFlagRegister: {value:X}", _interruptFlagRegister); - - return _interruptFlagRegister; - } - - if (address == Constants.LCDControlRegister) - { - if(_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("ReadByte returning cached value of LCDControlRegister: {value:X}", _lcdcRegister); - - return _lcdcRegister; - } - if(address == 0xFF00) { if(_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("ReadByte returning cached value of LCDControlRegister: {value:X}", _joypadRegister); - + _logger.LogDebug("ReadByte returning cached value of Joypad register: {value:X}", _joypadRegister); + return _joypadRegister; } diff --git a/GameboyDotnet.Core/Memory/MemoryController.cs b/GameboyDotnet.Core/Memory/MemoryController.cs index a28246a..9f89123 100644 --- a/GameboyDotnet.Core/Memory/MemoryController.cs +++ b/GameboyDotnet.Core/Memory/MemoryController.cs @@ -10,13 +10,20 @@ public class MemoryController { private readonly FixedBank[] _memoryMap = new FixedBank[0xFFFF + 1]; public SwitchableBank RomBankNn; - public SwitchableBank Vram = new(BankAddress.VramStart, BankAddress.VramEnd, nameof(Vram), bankSizeInBytes: 8192, numberOfBanks: 2); + + public SwitchableBank Vram = new(BankAddress.VramStart, BankAddress.VramEnd, nameof(Vram), bankSizeInBytes: 8192, + numberOfBanks: 2); + public readonly FixedBank Wram0 = new(BankAddress.Wram0Start, BankAddress.Wram0End, nameof(Wram0)); - public readonly SwitchableBank Wram1 = new(BankAddress.Wram1Start, BankAddress.Wram1End, nameof(Wram1), bankSizeInBytes: 4096, numberOfBanks: 8); + + public readonly SwitchableBank Wram1 = new(BankAddress.Wram1Start, BankAddress.Wram1End, nameof(Wram1), + bankSizeInBytes: 4096, numberOfBanks: 8); + public readonly FixedBank Oam = new(BankAddress.OamStart, BankAddress.OamEnd, nameof(Oam)); 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)); @@ -39,8 +46,9 @@ public void LoadProgram(Stream stream) var currentPosition = stream.Read(bank0, 0, 16384); RomBankNn = MbcFactory.CreateMbc(bank0[0x147], bank0[0x148], 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++) { @@ -62,9 +70,10 @@ 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 +83,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,7 +153,7 @@ 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++)); 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..6ca8c0b 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 @@ -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)); @@ -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; @@ -283,6 +291,7 @@ public partial class Cpu { if(_logger.IsEnabled(LogLevel.Debug)) _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); @@ -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.cs b/GameboyDotnet.Core/Processor/Cpu.cs index 858d740..ee5010e 100644 --- a/GameboyDotnet.Core/Processor/Cpu.cs +++ b/GameboyDotnet.Core/Processor/Cpu.cs @@ -1,5 +1,4 @@ -using System.Numerics; -using GameboyDotnet.Components.Cpu; +using GameboyDotnet.Components.Cpu; using GameboyDotnet.Extensions; using GameboyDotnet.Memory; using Microsoft.Extensions.Logging; @@ -51,34 +50,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 +64,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.SDL/Arial.ttf b/GameboyDotnet.SDL/Arial.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7ff88f22869126cc992030f18e0eeff65ec8bbac GIT binary patch literal 275572 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 YR_Tt=P6 z!@wLs-sQ-D`A=f1`UCQ+kg@7Hz?@vs2O#?u)VmTMS1k~8^`+olG1n9U>Mm&umW%o8 z*~6LXt@<3IsGj@w=UjAu1+-Tn~xTFjap zFa;p*9pvAMF7Jf@o!G{mpNqLG3?_mr!FDlgcZj+BOELEb0pqxr`M9@6%>C&9{*%EI z;GmcX+Jho7>(I$M###-p_2-G%&<5NoX5(ajwoCfK#elvZV$2UCLk<1aOcS%Y0+9d6 z7BP>~&!g9gd8{e;Ud$8mfN?yzTFjQEVxFSkr|SX6{S3UfwiWXn{XSO>(C_oqc^+Ob z)QZ{WfUm{81h1Fq^Q9R8y}m?0ujB&yeuchXVH~gQ6|7h>GR+Y@QavVnde`Jfq7sxp#NX@i>VER zQ$Yn-3E*4%otQ`}7z|1QdWxXu$k$@=3vr8IK3jagv$)S_$rf%voee6$O0l?$VL4Ze zDu-(IA^_T=oK_yrsRxf1eh3&kt>=SDwWg}Ywc#V7+>=J9#HnGO^5o_!;vCb#}6#%|xz;`^l znZS6?+yZupb#`a5CZW505vve6rhX~bw8>)4px#X4vl@e3Fak^hjA<5f&DssV2FO>! z*h@wN+Le@nn*s8cARpU6t27ye0ezLis}x?P^jQk8(iLDUfY-U`@!bAk23Q8RfX~G$ zYX}%m8S<4a1&p=qQ?bh7Th2U{Gsg0(K@C8!6(Mjum;~sf0(mQtcXkjkuGtd-`kq}4 z-W6+(19AX!G3R1%A3)c01+)jF!2+-rybcbEH7^S=j(Ln@-WsqS{3O==*5EWSN38Q2 zg9%^<_(iP0v;o7wJg^$P4EBq4ei)nzD!@wc0{Bj>3sS*gPzqLnt>A017BmI@!3?kr zpr-}ji*;dJz}PC`e-UzB49`m#*Cp`1eyoUG^bg_i`f2HnUr-`*R89XG`wUfoVj=byW=lU|S zmZ7(09|GjM!4H}O@^0u2?h$J_>E-BUIdzs(=SJ$>NIy5y|Bbx>I=m4XZe;8?UM$v% z&H%Y?suJtwtbVtF2q9vx;%9x(<9P)@>ufTCrAJ zpa4{hbvtt3j?A~C$2G*cPFrgp0B?(R2Xfuf0l@!`C4f42rhrQDmRNW72g}4-8xKZ+ z8c-|N-4g(H?q;la?-1)A0a@TQumHgSUh?jxuX`U8>%P`t4)|EC`>A*TrQijz9%u-L z0ml6R^S_QZ>y87oTSvd^o&)>Ds%{K&!7Q*Gz-K+WT#pXduK?S@L9sS;29p4J8{oI$ zJFzx41%1FYfNnN2?oC0^1C)X_;B~PcL}w3n1(U&2K;I9gf)St^(C5Rw0c{>e@9ZzE z&G2|+oLG;B0b_WS{vNLp>q+F=QYO|@=ZW?7Cb6C&e`^8QCDyaFf3{Yv=g5D)NURq| ziuEF6dGS-Rw%sGvOYOm9V)31V^~x%-wl5XlW5RckZ3D)bd;NaD&j;}b0{VC82-zm@@V8WkhtgiOk)J|;USAt#}u z-=CS4nU!O5LdS%TF+EMDF*)6Hx~H@u%b4sHwJY3oYJO_IN$H%>F)O82tA-}4tFq>3 zrhm<#|J6Z*t{ly@VYb_{i!mw6sY{B>5s45^MG760GK$j`qEEy3aXdM;XX{nnnDS;mCgMHa2m^ZKoHp)(lLt};#I6mxOI_cBdlC^e^#NuHgP zmXecdR$M>yn4~_}CuFsIubpX?(yq;WO&c|Ru1lLXZQ3YQUkjl|_P*R7Olitw)UxTE-Cs4UGx&1%jLYJ<{UH z%nq@(WXBnkp*XUJrm|ab{5D%9`Iko>=Em#kFs<9OG3_{%Rd#m$`idA^H6%S$C{M7K z7+1?!aWt&Ut3!tjYU&w193&5;7Ndh`;TdGC?Ehd`|G!-((Y~?f-#@+eahUcA^y#S! z;_%6{Y|%5ddun&qSM*%ARnN>Ut*<5CDIB_T0-QOjk2(iMw%Bfl&$f9w&+y)$FB7)N zPr(T92MR`N^*4Z#UnO4Rp$U=z4M_-^D2dQ|E=`hp#FHfnS|55)Qlvh#fuukiN&{#k z=mBXg4WUh>5wt1v7ilI$3^plziMw4G!@+q*Pdj*0A-4$>Cd(WN=ko_Ht8hVqsbXcy=Y z!n*^Y$4VzCzlDr^&#xPm9xq*>{PGdX?pmU+0@U-c4=zN(8Jx^vq|Kif~rG)qeQVLxl=SDu}IaH;UQVzXHDxeq3 zY@Xk~MCL#jx^$7ujeIDV$~@?0G9S8F&Vybqe~Ek`RdPP`3b}xsE1@6ol=l}n)4%EHL|a-B=Bmqo;v$z@QU&!u!Z^nIQPyd1hhsv^7PCbIq`etM(BNV6ZC$UJ|H&}UnjRft7Rp0J@j4KAh$v{$|~q4Ssi(YryXyPyv-Aj zN*|Ur&>Fb|x>@doKH}0xWi9c?GQGy`hskXyutIyo1oj|LFh~J5cFkv82XCTK)1_g=<>A&(G$ zO&){3E{{k4#g(C96up|G)3kzx^NX)4%wSefnpA z;M4V|Wk>q-y?@}-|M452{^@_<(?34s)60(V=^y=pPyfjE=^y>Zr+?`B^bhO!^bcL1 z{-NvBKdj@^KlqJL-{bo9J)%B+&;LN5{?7kLKK+e9efpn1{m*^+|I7RI|G7T>kM`+* z`t;xK)Bp78x=(-MfBrtbUxp;vP2?L80k)(;JAvWg9B>V|75LqHno|NU1W$r}z%RLW zlMPFc&8;E0!VNY~o6*%x=6T79XSvCZXN>p4p{IM{z*D?xy}W8&j`Ol}`g`Fq9lS6l zvuh>HH^wJ+-O{hI-B@-2OG=@}+A2wWk(4H@>_$=zEE`eXoLoC)W1Fn5w?1h*Jl18| zhA$7&B3o>;A+dhfe(_es`i8HL(yXtoue=iLtBv*QcfGaWsn&qE{DtPkWu zYY$J9B|-awTfvjyRqze)TYEU)=iF`Wrq+AX3G@Z$fLpFWOs^2(lVowCM}|ol*V0atk*aE+%B!g z`eoduKq>GOdyCjxQVEuTRiGO9Y4a9s-lEMm@G^Ky zI)hv=0tBrc8|bFSdUZoq|FnLMt?kzHeB+j8y<)xKhA&&sx#3IJvu^kzVJphFS;J2Q%T5l*wOM%Iw zvMtE7+13X6%nk39)j`RfmX@0}02>P{$?A0?G03gqTeGa(tYtTnRFZY|QesN77G6nA zN!EE65mS;iV-7JTSq0OGDao2}4lyNJ!$%W?)L1JYZgWgp_u<`ag9rppq)Tpa1OW-d;|QhINumzXj;EU<_2lyV0EUaWSLCD z9#Zb6yGd$L^N*8M?xLodt{YOv(ky91R)@4lOg(M&P}%EHRBlq*$gBxzC&O=G>g2TCa(F(J);IO+wBBB=ZrbLdw9bf-?M2%oY`avq z(~MRw!!g}!Oi^x!z_P%&!0^EFfv$lLf%HIHpk<&%pkXj2m>jGZ3>)LQ$jIvM*+Z%WBhDPR-k7V$lUr>qt}!xtT#bon z)}j{GDFeoB=2_nQi>_*+VcSJljUO*f8_((6v~NnE`aK5^{2emM&73qa`|#29a5TGV zcFXEzLq?6OzNh8*>aH4zv>ZRA`r1+9iQ_i&d-;8X25#o3^919^ZMOTE?*@(3e7n!U z@#BZon6Yj>37c=Jhs6+3FW5@LT2I2kR$jdoUcF4(Q?HE%)QgLgOt)TUT%23aF!gcFx%cM>wggT8YWt&@PTjNTtQ{Bg9lbTAERuHO~=921G zNj1&gDz4<~-71};RW3hN<#M+t+mptvqXagIdt!AG_fV&fBL8)?uzz;8*?7YE{E34K zGX_n{7*q%*RbMfusA+ZO{&+12SKC>WCi9j!!o0%j@fn2!tMfAkhS#6qwmYhL zq86W!F>t+195i~|`iZ%P12>$Id%~cMyn*94o;2dP?nm@>`Juj!8}U1RjZkLCX0kOm_W@ z{*qlWyL`4Z9aKEf`zz-lt75iJg-5eB?{^QX464q}8(3b!kAyV zkGHE^4H<3W3Pw*r4HGAfdxY(hUL?xLGr)50?w7~7={T~*O9?|RkD+3AG#1rEMKpA) zYLoJqI%2G{(GspIw_m^9zI{&YeL}CEJ-T;0?%1wfI(N$H*de=pySB$=g!s5%!0&Tx%SeYo8G|QXcv`h@ra83PLA zJ9d!u@d-o|h*dYqC|z%w^f7M4YBH$TdP{Er6OZIP}V z#*c>wZLQ3~ld1;O^_9%j5OtE()}ry_s?8$0WW{H?qw>b*xueWU)56tp8T~Vgs-{h1 zYMNJ7%gFiZ8=5!I-OLxV%?E|6Mvu!#ukPC-V|?Dg)b$OeYUKQl&2q!djx6ceVSRFa zPqFLkB}F5l#JZ6}lY=2thg9YFU;;SC z8(LKtFeAm)aRVk*CHK;NhG?pNnaLU9svmgw%%qI3zC1D~FPh`eO#VT%Q0?T9sa2c2 zSiCwrySjaQHIcvo<`fzGxan>kJItxEGBQe&+1RKzml5bTZ+x##s6RbjC-{n*T$xOw zy7KgKUOFt3TWpZrPB=Yll9p_Vl{6ZoC6%#~L(L~;V97kh%nEK)9n3oPmz3PN!JwjE z)u!>kC@%EMhm6V?g3k&Ms+ttF+99KlNO|R|Q1nJwG*;bUz&N{wMV`j27PedJxvp3h z{MfiqwUf!8-!7M8y`-M&miuIy-h4jL)p98k`XxTs5gGuO?DCIU}5$QMK86+IqUG zbP(RoGn|^pBUiMj9(?6^G*)DKb@V+V%|LVCUDC|S;;9bd&b@F~pRl$#vRBJBv_8i) zYNFsrlXAZ)<}UP;^0e7Ut99H--o*WZCOi*uGw+YOjypDfo=bRvJMbepedJ$fnngD8 zw8m|Gp1F-Xz)j=~?gl*~jZIUYD7cV!WBrX*i+DDrHP2a$;JJdU%xRI?JkjvJbBT24 z$%$D~YAPe+B3DP2M%Hl8e6#%m&#WZy{7F9NE0M2#Z*v#DBQ37y&iwmkY21U7OD~nw zyoIL{R`5O^6Pe1BDK<}{%tZ!=Cox_zTdZuj7s@|OQ**vO04}#js(Gf~=4qC*coJd- zZ-eS)PO{Q{6C*>p@7tI@=E3zw?zBI|S;PJEcTLE*FR~`Gk7r^!$SI6xlWaFz?Al*1 zs_l#3_I6)p5onFO-b&hqD^`U(o?{RuPkjJRc z<_V9hc;etk9s%rOPB&+oBHnOx8Sgu~kvC@TFng_j)@W-w@0%*J&$So@2wqIyS4VU+8-lbBbV|_%lSOF@mJ<#B_rF+{r-12-vH-_mu!Zy*%4)dY;j86HIFO?wGW%d%z2kj4>=DbrXf}Xqjmicb=t@YjOds^Rj7`T{^U@srM{nz%t z?yi;E#kI?7H`H$8S&&A|A)9-?1nZ6Td7RUj?`2rrI{6#V^EG7#+M7P+G&FOLnP$#4 z^U&l%-iLOFEBgbyq3jj&ck>N0C-M$2l{v@iX7#s*bDnJ#TIcdvd8xI@ddvF74)8{; zB)gH_-ag4b%PzDl?D_UGyV`!4H*$Sw@3#+fMx1yj&1ubh>9Tnh=Q+GnY9;Tz`iF0# z?a2p^Etdabf|@;T5HW>>jJaMYU7*dKfyY|43mAhw(EGW;!wL8Dd7s zG^>lp(BIJEv1sY-JR@H@k1@9GaGvK6nFZE2{*Y|oBWzC*^Q_(3$+lmTckTC0z`0F! zI`O=-?JMgpdjwYSxYNfsPSWk0^iI?5>eMtYR^?*Ip+2`aQFG#$VD>WH_mz8 z@L$x-Sjlws{_**CS0|nDLcGdEeA+{-?nm$g{p_LSG{tgG!!E|)Ay#lM$4@v|P%&%# z47}KO+2kK>)yPy|J%iT~abBt&$#YnD@`Mlnzbmqor`X=$S)=pexRz&Au93B7QSD!( zlyCapVQru08*IJm8yx9qRax&?qpW2|&M(@|G);MG<#Ty}XVUuk9+fKR@4VWzZ{$jz z%xT24b*+(XGT$eB%qYL6hm-9sa%}A|Ykg#}&3`!Xz0b2`cSX_+{|_KCgD2D;lRE-F z$qQur-93Wp@er!5zsuQ9xikW&1X{{m-&h%EE|Up7E`PqlZYjCWeNsl7dr0>qd_>z* ze++mZ^af)=bD()cK_0-T$QbHw*0%5{RX%Q5E)#-jQsNsM`IY{b`JR_4U?uU@&PTG= z-&1CiUPHSl`H%eQ7pJ%DaznSuU@>UX#BYhk3iL~n+jmHJ9;<`9i;((w0cJR3Z z=(vxupOWL9^2i5_Wjs7j1((vt2*SZ&2z43|_6L`l=Vh^ZKC+r}Lb-(gFVlQ5FdCi= zpGB1SrCl5HE+O6=IrO`rB#;i;TK7p$tD!tb*opC<;ff-5<$~zE8wf z@a3`UX*W30DUe~L4LTp?+axPA4TicXkNoP~%uNTL%%to*|1!o=fWGlE7SDKEUrI-R zrd)`94TS$iU?n{Fy7o{Yqv=0~`o}t-xHfbt=_~2$o0y(-zDZxiypCk<4ie|*8!`%< z#Jp638MGfp|D9ClI`3v|ZBOccOr41e@|(E8&e7FoZKLhsl^M0e)rUh_O`WUI>mEV} zG*TI2aP3U(4p86gEJ8$wckb5Ja`NX{FyVqLa z#(xu1u5}KFrUVP@gk{V2h3L^Q;B($M`{=SQE9WG(tb?Z+jFU6DszD zAF!j_u}ST#yZUGNFC)GfsPDjDBfr4Qv(Fdh4)U&u*=H8jv&R58l=%JZ?8S9aJ zFi~~l&V@)*y)F=MZeNej(`(XIa*4f@{ereVR_eRgi@tIOvcKr&mBWWJKY6}0WVQbb z{vq`keC-w~K!%CP@}$ZG&+ft5k{E1+pLkQcus^)b*GS^2x60K)uG~faWp3N)e1t<6 zfqV&Iv%{#P{BCpG~@bov5k+hHVeId8{_>{*Qx-C#CxB17g z<{HUbU#{FiyD=&Y?Q|b>s&l=Z$@*H1Z(5Ac(f!g8--O5k`(DN~55ErVO6>Jse)my{ z4yRL3_2c>2%NXAX`yR<6O!r^UHDC7=*RvPQmO=jMaus>J)mz%q?-k@NWc_w#?OslM zT|2r*5cPY{A1;H>mEA+yVYyxX6GU)cE(Cqy9?h})R&IV zYpm6um{rd^VZSZ4zFKA%o2)Ktsj-aAf7ntr-E56ZNo`Te(P^Qv^#aYd0^cZ{+5 zAmkqUzlF}B*3&SoA#3qey*~f9GX3w?XMQgPXDJ`&YiSgC<~v2KKgAu66Sd%^!KZMgv>nD>gat@Vt8YN2iaT*MFC`Hp(4!5wn9>=wB)e z6%OBaD2`4CUzCLk|Fe{Vqtnh^wEI_>AnolHt}Tjdb4R89!?|Vxs|`BfEnD!qQgJB# zDr*^NTep3pT;%|F?XrHYO;W5lRNf8$4PyG%HK=+;2Dg1|eq!?y3uE=23GCapg9%(K zwo7NiQH1?sVa$eN{vwvvQ2j++J9m8vzU7EIhu6g6wZU3(+edv+-TME3inDgMf#<<< z|F_4bcW=~3@brlIcd-xY%Rcf=_9Jg)Gy?LZ>&`s7U| zyhY@|55&vJf73(6a$3l$=)STUc@IU~(w)n1l-K^i3%pm3mHi7ZIMDV7_-63HG~(Za z3y8l*_!QxC>VFQOg@jLg<-eXodJcGu^cUpM0ON=+A#6n00W<(9v|pys{YY%z{C_Vz z^0Tb(sL!_lF3p}LA34(qA0a$H_SxaT4rBBAZ$kEtM|>uZ&Huj(8Hf8>&y=KYx^FK5Og3858-HJ!MGu(Ylw_{j;rm*k}l!HQqj!etDTJ-@mvaa>bEO z(!Dc#)j@=}5ia)o-0uv=AlD?WkpnUPrtH)^Bs30+ZCj4tA0MzB$MM^aZM&~=aETXB z2=aagzdsnTd;utrWswX7;{(9}|2@g)r!@!E;@@`!y*NzbH51lB9$BEO=0^|y*M}90 z6KZ>hD!~K)C)~ZXD}jpUb90?gf?rQ$qH=)Y z(eZzA1QPT>(u5EXy18%e2nKz~9Z#?Pxm;a|(7SsE{BiC8b&x(Qz?(N1L9|Habfwmu z_;>~v^d}J5@$NwyJ!w>R5{!%U#l`bn9YaP_2(IGmkcrYzU(liN+;9U8dHn_j!p%c~xIhraph6nC96aqR3!pu_F^U;Ye4N{hFCj55I%l!K zb{y=8SCH6tAYk*N6DTI=9dR15eQ{dDhY)dbny;08UfpQTI8FKaz{U>3aeVr3X57Q$ z;!?Fy+Y^mBR2y;+Z%%Dn6E1J`=J*v1{HVia$$P?%Is$65e|Rt%0Uz?%K4`$tLZmO2 zl%rHpM@DQN={aYMwvb<)x|+Yw^f}^0{W^P%$2FfO)&df zg@MH*j1vdHSltkuLJ0wCAs0@|a;<@0odjL1YGt;r83a|s@{WMbn)cZ+^{E#n6Q6

L%Yj0kwC)JKzW*Z{;59WN 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≪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 NC1tZOiJIot{vZ5_jpG0S literal 0 HcmV?d00001 diff --git a/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj b/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj index 99b6da9..159a402 100644 --- a/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj +++ b/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj @@ -62,16 +62,7 @@ PreserveNewest - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - + PreserveNewest diff --git a/GameboyDotnet.SDL/Program.cs b/GameboyDotnet.SDL/Program.cs index 2a20e29..890fa52 100644 --- a/GameboyDotnet.SDL/Program.cs +++ b/GameboyDotnet.SDL/Program.cs @@ -1,6 +1,8 @@ -using GameboyDotnet; +using System.Diagnostics; +using GameboyDotnet; using GameboyDotnet.Common; using GameboyDotnet.SDL; +using GameboyDotnet.SDL.SaveStates; using Microsoft.Extensions.Configuration; using static SDL2.SDL; @@ -36,9 +38,16 @@ { Interlocked.Increment(ref framesRequested); }; +gameboy.FrameLimiterSwitched += (_, _) => +{ + framesRequested = 0; +}; Task.Run(() => gameboy.RunAsync(emulatorSettings.FrameLimitEnabled, cts.Token)); +int frameCounter = 0; +long lastFpsUpdate = Stopwatch.GetTimestamp(); + // Main SDL loop while (running && !cts.IsCancellationRequested) { @@ -47,10 +56,14 @@ switch (e.type) { case SDL_EventType.SDL_KEYDOWN: + if (e.key.keysym.sym is SDL_Keycode.SDLK_F5) + SaveDumper.SaveState(gameboy, romPath); + if (e.key.keysym.sym is SDL_Keycode.SDLK_F8) + SaveDumper.LoadState(gameboy, romPath); + if (e.key.keysym.sym is SDL_Keycode.SDLK_p) + gameboy.SwitchFramerateLimiter(); 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)) @@ -63,11 +76,19 @@ break; } } - + if (framesRequested > 0) { - Interlocked.Decrement(ref framesRequested); - Renderer.RenderStates(ref renderer, gameboy.Ppu.Lcd, ref window); + long now = Stopwatch.GetTimestamp(); + if ((now - lastFpsUpdate) >= TimeSpan.FromSeconds(1).Ticks) // 1 second has passed + { + lastFpsUpdate = now; + framesRequested = 0; + } + + string bufferedFramesText = $"Buffered frames: {framesRequested}"; + Renderer.RenderStates(ref renderer, gameboy.Ppu.Lcd, ref window, bufferedFramesText); + framesRequested--; } } diff --git a/GameboyDotnet.SDL/Renderer.cs b/GameboyDotnet.SDL/Renderer.cs index 1589ceb..ff13e59 100644 --- a/GameboyDotnet.SDL/Renderer.cs +++ b/GameboyDotnet.SDL/Renderer.cs @@ -17,7 +17,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, Lcd lcd, ref IntPtr window, string fpsText) { try { @@ -33,10 +35,22 @@ 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++) @@ -66,6 +80,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) @@ -89,7 +106,7 @@ public static (nint renderer, nint window) InitializeRendererAndWindow(ILogger Date: Mon, 10 Feb 2025 23:16:59 +0100 Subject: [PATCH 02/11] Implemented single frame buffer --- GameboyDotnet.Core/Gameboy.Dump.cs | 15 +++++---- GameboyDotnet.Core/Gameboy.cs | 19 ++++------- GameboyDotnet.Core/Graphics/FrameBuffer.cs | 37 +++++++++++++++++++++ GameboyDotnet.Core/Graphics/Ppu.cs | 1 + GameboyDotnet.SDL/GameboyDotnet.SDL.csproj | 3 ++ GameboyDotnet.SDL/Program.cs | 38 +++++++--------------- GameboyDotnet.SDL/Renderer.cs | 6 ++-- 7 files changed, 71 insertions(+), 48 deletions(-) create mode 100644 GameboyDotnet.Core/Graphics/FrameBuffer.cs diff --git a/GameboyDotnet.Core/Gameboy.Dump.cs b/GameboyDotnet.Core/Gameboy.Dump.cs index 0cfdff3..91fb2d5 100644 --- a/GameboyDotnet.Core/Gameboy.Dump.cs +++ b/GameboyDotnet.Core/Gameboy.Dump.cs @@ -1,16 +1,16 @@ -namespace GameboyDotnet; +using Microsoft.Extensions.Logging; + +namespace GameboyDotnet; public partial class Gameboy { public byte[] DumpMemory() { - IsMemoryDumpingActive = true; - var memoryDump = new byte[(0xFFFF + 1) + 12 + 2]; //Address space + 6 registers + 2 timers + 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); } - IsMemoryDumpingActive = false; memoryDump[0xFFFF + 1] = (byte)(Cpu.Register.PC & 0xFF); memoryDump[0xFFFF + 2] = (byte)(Cpu.Register.PC >> 8); memoryDump[0xFFFF + 3] = (byte)(Cpu.Register.SP & 0xFF); @@ -23,13 +23,16 @@ public byte[] DumpMemory() 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) { - IsMemoryDumpingActive = true; + IsMemoryDumpRequested = true; for (int i = 0; i <= 0xFFFF; i++) { Cpu.MemoryController.WriteByte((ushort)i, dump[i]); @@ -44,6 +47,6 @@ public void LoadMemoryDump(byte[] dump) Cpu.Register.H = dump[0xFFFF + 10]; Cpu.Register.L = dump[0xFFFF + 11]; Cpu.Register.F = dump[0xFFFF + 12]; - IsMemoryDumpingActive = false; + IsMemoryDumpRequested = false; } } \ No newline at end of file diff --git a/GameboyDotnet.Core/Gameboy.cs b/GameboyDotnet.Core/Gameboy.cs index 6a88d48..4f1a749 100644 --- a/GameboyDotnet.Core/Gameboy.cs +++ b/GameboyDotnet.Core/Gameboy.cs @@ -15,7 +15,7 @@ public partial class Gameboy public MainTimer TimaTimer { get; } = new(); public DividerTimer DivTimer { get; } = new(); public bool IsFrameLimiterEnabled; - internal bool IsMemoryDumpingActive; + public bool IsMemoryDumpRequested; public Gameboy(ILogger logger) { @@ -37,16 +37,9 @@ public Task RunAsync(bool frameLimitEnabled, CancellationToken ctsToken) var cyclesPerFrame = Cycles.CyclesPerFrame; var currentCycles = 0; - var frameCounter = 0; while (!ctsToken.IsCancellationRequested) { - if (IsMemoryDumpingActive && frameCounter == 0) - { - Task.Delay(TimeSpan.FromSeconds(1), ctsToken).RunSynchronously(); - continue; - } - try { var startTime = Stopwatch.GetTimestamp(); @@ -60,13 +53,13 @@ public Task RunAsync(bool frameLimitEnabled, CancellationToken ctsToken) currentCycles += tStates; } UpdateJoypadState(); - - frameCounter++; currentCycles -= cyclesPerFrame; - DisplayUpdated.Invoke(this, EventArgs.Empty); - if(frameCounter % 60 == 0) + Ppu.FrameBuffer.EnqueueFrame(Ppu.Lcd); + + if (IsMemoryDumpRequested) { - frameCounter = 0; + DumpMemory(); + continue; } if(IsFrameLimiterEnabled) diff --git a/GameboyDotnet.Core/Graphics/FrameBuffer.cs b/GameboyDotnet.Core/Graphics/FrameBuffer.cs new file mode 100644 index 0000000..a1a38d2 --- /dev/null +++ b/GameboyDotnet.Core/Graphics/FrameBuffer.cs @@ -0,0 +1,37 @@ +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 SpeedPercentage = 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) + { + int fps = Interlocked.Exchange(ref _frameCount, 0); + SpeedPercentage = fps / 60.0 * 100.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/Ppu.cs b/GameboyDotnet.Core/Graphics/Ppu.cs index db772d3..c7a7059 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; diff --git a/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj b/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj index 159a402..0011304 100644 --- a/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj +++ b/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj @@ -65,6 +65,9 @@ PreserveNewest + + PreserveNewest + diff --git a/GameboyDotnet.SDL/Program.cs b/GameboyDotnet.SDL/Program.cs index 890fa52..93a1344 100644 --- a/GameboyDotnet.SDL/Program.cs +++ b/GameboyDotnet.SDL/Program.cs @@ -1,51 +1,44 @@ using System.Diagnostics; using GameboyDotnet; using GameboyDotnet.Common; +using GameboyDotnet.Graphics; using GameboyDotnet.SDL; using GameboyDotnet.SDL.SaveStates; 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) .Build(); - 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) = Renderer.InitializeRendererAndWindow(logger, emulatorSettings); + +var gameboy = new Gameboy(logger); 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); -}; -gameboy.FrameLimiterSwitched += (_, _) => -{ - framesRequested = 0; -}; + Task.Run(() => gameboy.RunAsync(emulatorSettings.FrameLimitEnabled, cts.Token)); -int frameCounter = 0; long lastFpsUpdate = Stopwatch.GetTimestamp(); // Main SDL loop @@ -57,7 +50,7 @@ { case SDL_EventType.SDL_KEYDOWN: if (e.key.keysym.sym is SDL_Keycode.SDLK_F5) - SaveDumper.SaveState(gameboy, romPath); + gameboy.IsMemoryDumpRequested = true; if (e.key.keysym.sym is SDL_Keycode.SDLK_F8) SaveDumper.LoadState(gameboy, romPath); if (e.key.keysym.sym is SDL_Keycode.SDLK_p) @@ -77,18 +70,11 @@ } } - if (framesRequested > 0) + if(gameboy.Ppu.FrameBuffer.TryDequeueFrame(out var frame)) { - long now = Stopwatch.GetTimestamp(); - if ((now - lastFpsUpdate) >= TimeSpan.FromSeconds(1).Ticks) // 1 second has passed - { - lastFpsUpdate = now; - framesRequested = 0; - } - - string bufferedFramesText = $"Buffered frames: {framesRequested}"; - Renderer.RenderStates(ref renderer, gameboy.Ppu.Lcd, ref window, bufferedFramesText); - framesRequested--; + //Format double to string with 1 decimal place + string bufferedFramesText = $"Speed: {gameboy.Ppu.FrameBuffer.SpeedPercentage:0.0}% / {gameboy.Ppu.FrameBuffer.SpeedPercentage * 0.6:0} FPS"; + Renderer.RenderStates(ref renderer, ref window, frame!, bufferedFramesText); } } diff --git a/GameboyDotnet.SDL/Renderer.cs b/GameboyDotnet.SDL/Renderer.cs index ff13e59..4685194 100644 --- a/GameboyDotnet.SDL/Renderer.cs +++ b/GameboyDotnet.SDL/Renderer.cs @@ -19,7 +19,7 @@ public static class Renderer private static IntPtr _font; - public static void RenderStates(ref IntPtr renderer, Lcd lcd, ref IntPtr window, string fpsText) + public static void RenderStates(ref IntPtr renderer, ref IntPtr window, byte[,] frame, string fpsText) { try { @@ -57,9 +57,9 @@ public static void RenderStates(ref IntPtr renderer, Lcd lcd, ref IntPtr window, { 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, From fcc25a89dec7946d566b78855da865946fc8e297 Mon Sep 17 00:00:00 2001 From: Szymon Sandura Date: Wed, 12 Feb 2025 19:24:09 +0100 Subject: [PATCH 03/11] Fixed FrameBuffer calculations --- GameboyDotnet.Core/Graphics/FrameBuffer.cs | 5 ++--- GameboyDotnet.Core/Sound/Apu.cs | 9 +++++++++ GameboyDotnet.SDL/GameboyDotnet.SDL.csproj | 3 +++ GameboyDotnet.SDL/Program.cs | 23 ++++++++++++---------- GameboyDotnet.SDL/appsettings.json | 2 +- 5 files changed, 28 insertions(+), 14 deletions(-) create mode 100644 GameboyDotnet.Core/Sound/Apu.cs diff --git a/GameboyDotnet.Core/Graphics/FrameBuffer.cs b/GameboyDotnet.Core/Graphics/FrameBuffer.cs index a1a38d2..b649da9 100644 --- a/GameboyDotnet.Core/Graphics/FrameBuffer.cs +++ b/GameboyDotnet.Core/Graphics/FrameBuffer.cs @@ -8,7 +8,7 @@ public class FrameBuffer private readonly ConcurrentQueue _frameQueue = new(); private int _frameCount = 0; private readonly Stopwatch _stopwatch = new(); - public double SpeedPercentage = 0; + public double Fps = 0; public FrameBuffer() { @@ -24,8 +24,7 @@ public void EnqueueFrame(Lcd lcd) if (_stopwatch.ElapsedMilliseconds >= 1000) { - int fps = Interlocked.Exchange(ref _frameCount, 0); - SpeedPercentage = fps / 60.0 * 100.0; + Fps = Interlocked.Exchange(ref _frameCount, 0); _stopwatch.Restart(); } } diff --git a/GameboyDotnet.Core/Sound/Apu.cs b/GameboyDotnet.Core/Sound/Apu.cs new file mode 100644 index 0000000..f99b989 --- /dev/null +++ b/GameboyDotnet.Core/Sound/Apu.cs @@ -0,0 +1,9 @@ +namespace GameboyDotnet.Sound; + +public class Apu +{ + public void Step(ref byte tStates) + { + + } +} \ No newline at end of file diff --git a/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj b/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj index 0011304..3a98e0c 100644 --- a/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj +++ b/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj @@ -68,6 +68,9 @@ PreserveNewest + + PreserveNewest + diff --git a/GameboyDotnet.SDL/Program.cs b/GameboyDotnet.SDL/Program.cs index 93a1344..45bc43a 100644 --- a/GameboyDotnet.SDL/Program.cs +++ b/GameboyDotnet.SDL/Program.cs @@ -39,8 +39,6 @@ Task.Run(() => gameboy.RunAsync(emulatorSettings.FrameLimitEnabled, cts.Token)); -long lastFpsUpdate = Stopwatch.GetTimestamp(); - // Main SDL loop while (running && !cts.IsCancellationRequested) { @@ -49,12 +47,18 @@ switch (e.type) { case SDL_EventType.SDL_KEYDOWN: - if (e.key.keysym.sym is SDL_Keycode.SDLK_F5) - gameboy.IsMemoryDumpRequested = true; - if (e.key.keysym.sym is SDL_Keycode.SDLK_F8) - SaveDumper.LoadState(gameboy, romPath); - if (e.key.keysym.sym is SDL_Keycode.SDLK_p) - gameboy.SwitchFramerateLimiter(); + 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; + } if (keyboardMapper.TryGetGameboyKey(e.key.keysym.sym, out var keyPressed)) gameboy.PressButton(keyPressed); break; @@ -72,8 +76,7 @@ if(gameboy.Ppu.FrameBuffer.TryDequeueFrame(out var frame)) { - //Format double to string with 1 decimal place - string bufferedFramesText = $"Speed: {gameboy.Ppu.FrameBuffer.SpeedPercentage:0.0}% / {gameboy.Ppu.FrameBuffer.SpeedPercentage * 0.6:0} FPS"; + string bufferedFramesText = $"Speed: {gameboy.Ppu.FrameBuffer.Fps/ 60.0 * 100.0:0.0}% / {gameboy.Ppu.FrameBuffer.Fps:0} FPS"; Renderer.RenderStates(ref renderer, ref window, frame!, bufferedFramesText); } } diff --git a/GameboyDotnet.SDL/appsettings.json b/GameboyDotnet.SDL/appsettings.json index 0445cc9..9ea5cd9 100644 --- a/GameboyDotnet.SDL/appsettings.json +++ b/GameboyDotnet.SDL/appsettings.json @@ -4,7 +4,7 @@ "WindowWidth": 1200, "WindowHeight": 700, "FrameLimitEnabled": true, - "RomPath": "Tests/Roms/Test.gb", + "RomPath": "Tests/Roms/Pokemon - Red Version.gb", "Keymap": { "SDLK_UP": "Up", "SDLK_LEFT": "Left", From d9a59f58d7763dd254554623f4a2ccf9dc659d94 Mon Sep 17 00:00:00 2001 From: Szymon Sandura Date: Thu, 13 Feb 2025 00:38:24 +0100 Subject: [PATCH 04/11] Improved memory access in timers --- GameboyDotnet.Core/Gameboy.cs | 17 ++++++++++++----- GameboyDotnet.Core/Sound/Apu.cs | 8 +++++--- GameboyDotnet.Core/Timers/DividerTimer.cs | 7 +++---- GameboyDotnet.Core/Timers/MainTimer.cs | 7 ++++--- GameboyDotnet.SDL/appsettings.json | 2 +- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/GameboyDotnet.Core/Gameboy.cs b/GameboyDotnet.Core/Gameboy.cs index 4f1a749..b82dd81 100644 --- a/GameboyDotnet.Core/Gameboy.cs +++ b/GameboyDotnet.Core/Gameboy.cs @@ -2,6 +2,7 @@ using GameboyDotnet.Common; using GameboyDotnet.Graphics; using GameboyDotnet.Processor; +using GameboyDotnet.Sound; using GameboyDotnet.Timers; using Microsoft.Extensions.Logging; @@ -12,8 +13,9 @@ public partial class Gameboy private ILogger _logger; public Cpu Cpu { get; } public Ppu Ppu { get; } - public MainTimer TimaTimer { get; } = new(); - public DividerTimer DivTimer { get; } = new(); + public Apu Apu { get; } + public MainTimer TimaTimer { get; } + public DividerTimer DivTimer { get; } public bool IsFrameLimiterEnabled; public bool IsMemoryDumpRequested; @@ -22,6 +24,9 @@ public Gameboy(ILogger logger) _logger = logger; Cpu = new Cpu(logger); Ppu = new Ppu(Cpu.MemoryController); + Apu = new Apu(Cpu.MemoryController); + TimaTimer = new MainTimer(Cpu.MemoryController); + DivTimer = new DividerTimer(Cpu.MemoryController); } public void LoadProgram(FileStream stream) @@ -33,7 +38,7 @@ public void LoadProgram(FileStream stream) public Task RunAsync(bool frameLimitEnabled, CancellationToken ctsToken) { IsFrameLimiterEnabled = frameLimitEnabled; - var frameTimeTicks = TimeSpan.FromMilliseconds(16.75).Ticks; + var frameTimeTicks = TimeSpan.FromMilliseconds(16.75).Ticks; //~59.7 Hz var cyclesPerFrame = Cycles.CyclesPerFrame; var currentCycles = 0; @@ -44,12 +49,14 @@ public Task RunAsync(bool frameLimitEnabled, CancellationToken ctsToken) { var startTime = Stopwatch.GetTimestamp(); var targetTime = startTime + frameTimeTicks; + while (currentCycles < cyclesPerFrame) { 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(); diff --git a/GameboyDotnet.Core/Sound/Apu.cs b/GameboyDotnet.Core/Sound/Apu.cs index f99b989..735284a 100644 --- a/GameboyDotnet.Core/Sound/Apu.cs +++ b/GameboyDotnet.Core/Sound/Apu.cs @@ -1,8 +1,10 @@ -namespace GameboyDotnet.Sound; +using GameboyDotnet.Memory; -public class Apu +namespace GameboyDotnet.Sound; + +public class Apu(MemoryController memoryController) { - public void Step(ref byte tStates) + public void PushApuCycles(ref byte tStates) { } diff --git a/GameboyDotnet.Core/Timers/DividerTimer.cs b/GameboyDotnet.Core/Timers/DividerTimer.cs index b72e5c4..a17b84f 100644 --- a/GameboyDotnet.Core/Timers/DividerTimer.cs +++ b/GameboyDotnet.Core/Timers/DividerTimer.cs @@ -1,13 +1,12 @@ -using GameboyDotnet.Components; -using GameboyDotnet.Memory; +using GameboyDotnet.Memory; namespace GameboyDotnet.Timers; -public class DividerTimer +public class DividerTimer(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) diff --git a/GameboyDotnet.Core/Timers/MainTimer.cs b/GameboyDotnet.Core/Timers/MainTimer.cs index b7273ca..25b3dd7 100644 --- a/GameboyDotnet.Core/Timers/MainTimer.cs +++ b/GameboyDotnet.Core/Timers/MainTimer.cs @@ -4,13 +4,14 @@ namespace GameboyDotnet.Timers; -public class MainTimer +public class MainTimer(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; diff --git a/GameboyDotnet.SDL/appsettings.json b/GameboyDotnet.SDL/appsettings.json index 9ea5cd9..867960b 100644 --- a/GameboyDotnet.SDL/appsettings.json +++ b/GameboyDotnet.SDL/appsettings.json @@ -4,7 +4,7 @@ "WindowWidth": 1200, "WindowHeight": 700, "FrameLimitEnabled": true, - "RomPath": "Tests/Roms/Pokemon - Red Version.gb", + "RomPath": "Tests/Roms/Pokemon Red.gb", "Keymap": { "SDLK_UP": "Up", "SDLK_LEFT": "Left", From 266928a023b0e55cd8364025057f72b86a90ded9 Mon Sep 17 00:00:00 2001 From: Szymon Sandura Date: Sun, 16 Feb 2025 11:43:13 +0100 Subject: [PATCH 05/11] Fixed formatting --- GameboyDotnet.Core/Cycles.cs | 4 +++- .../Extensions/ByteExtensions.cs | 1 + GameboyDotnet.Core/Graphics/Ppu.cs | 3 --- .../Memory/BuildingBlocks/FixedBank.cs | 2 +- .../Memory/BuildingBlocks/Mbc.cs | 2 +- GameboyDotnet.Core/Memory/MemoryController.cs | 3 ++- .../Processor/Cpu.OperationBlocks.Block2.cs | 19 +++++++++++++------ .../Processor/Cpu.OperationBlocks.Block3.cs | 1 + .../Timers/{DividerTimer.cs => DivTimer.cs} | 3 ++- GameboyDotnet.SDL/GameboyDotnet.SDL.csproj | 9 +++++++++ GameboyDotnet.SDL/appsettings.json | 2 +- 11 files changed, 34 insertions(+), 15 deletions(-) rename GameboyDotnet.Core/Timers/{DividerTimer.cs => DivTimer.cs} (86%) 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/Graphics/Ppu.cs b/GameboyDotnet.Core/Graphics/Ppu.cs index c7a7059..9e53a5a 100644 --- a/GameboyDotnet.Core/Graphics/Ppu.cs +++ b/GameboyDotnet.Core/Graphics/Ppu.cs @@ -52,7 +52,6 @@ public void PushPpuCycles(byte cpuCycles) { PushScanlineToBuffer(); } - break; case PpuMode.HBlankMode0: if (_cyclesCounter >= Cycles.HBlankMode0CyclesThreshold) @@ -60,7 +59,6 @@ public void PushPpuCycles(byte cpuCycles) _ly++; _cyclesCounter -= Cycles.HBlankMode0CyclesThreshold; } - break; case PpuMode.VBlankMode1: if (_cyclesCounter >= Cycles.VBlankMode1CyclesThreshold) @@ -72,7 +70,6 @@ public void PushPpuCycles(byte cpuCycles) _ly = 0; } } - break; } diff --git a/GameboyDotnet.Core/Memory/BuildingBlocks/FixedBank.cs b/GameboyDotnet.Core/Memory/BuildingBlocks/FixedBank.cs index 2238467..c33f617 100644 --- a/GameboyDotnet.Core/Memory/BuildingBlocks/FixedBank.cs +++ b/GameboyDotnet.Core/Memory/BuildingBlocks/FixedBank.cs @@ -4,7 +4,7 @@ public class FixedBank { public int StartAddress { get; init; } = 0; public int EndAddress { get; init; } = 0; - public ReadOnlySpan MemorySpaceView => MemorySpace; + public Span MemorySpaceView => MemorySpace; public byte[] MemorySpace; public string Name { get; init; } 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/MemoryController.cs b/GameboyDotnet.Core/Memory/MemoryController.cs index 9f89123..da8a145 100644 --- a/GameboyDotnet.Core/Memory/MemoryController.cs +++ b/GameboyDotnet.Core/Memory/MemoryController.cs @@ -65,7 +65,7 @@ public void LoadProgram(Stream stream) break; } } - + public byte ReadByte(ushort address) { if (address is >= 0xE000 and <= 0xFDFF) @@ -160,6 +160,7 @@ private void DmaTransfer(ref byte value) } } + private void InitializeMemoryMap() { for (int i = 0; i <= 65535; i++) 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/Timers/DividerTimer.cs b/GameboyDotnet.Core/Timers/DivTimer.cs similarity index 86% rename from GameboyDotnet.Core/Timers/DividerTimer.cs rename to GameboyDotnet.Core/Timers/DivTimer.cs index a17b84f..1697e05 100644 --- a/GameboyDotnet.Core/Timers/DividerTimer.cs +++ b/GameboyDotnet.Core/Timers/DivTimer.cs @@ -2,13 +2,14 @@ namespace GameboyDotnet.Timers; -public class DividerTimer(MemoryController memoryController) +public class DivTimer(MemoryController memoryController) { private int _dividerCycleCounter; internal void CheckAndIncrementTimer(ref byte tStates) { _dividerCycleCounter += tStates; + if (_dividerCycleCounter >= Cycles.DividerCycles) { _dividerCycleCounter -= Cycles.DividerCycles; diff --git a/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj b/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj index 3a98e0c..b8b95ba 100644 --- a/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj +++ b/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj @@ -71,6 +71,15 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + diff --git a/GameboyDotnet.SDL/appsettings.json b/GameboyDotnet.SDL/appsettings.json index 867960b..ce49a6b 100644 --- a/GameboyDotnet.SDL/appsettings.json +++ b/GameboyDotnet.SDL/appsettings.json @@ -4,7 +4,7 @@ "WindowWidth": 1200, "WindowHeight": 700, "FrameLimitEnabled": true, - "RomPath": "Tests/Roms/Pokemon Red.gb", + "RomPath": "Tests/Roms/Super Mario Land 2 - 6 Golden Coins.gb", "Keymap": { "SDLK_UP": "Up", "SDLK_LEFT": "Left", From 3e596b86fb9b5ce070457598ad9ef2be7be40db5 Mon Sep 17 00:00:00 2001 From: Szymon Sandura Date: Sun, 16 Feb 2025 11:44:15 +0100 Subject: [PATCH 06/11] APU and Square2 fragments; Configured SDL Audio --- GameboyDotnet.Core/BitState.cs | 7 + GameboyDotnet.Core/Gameboy.cs | 10 +- GameboyDotnet.Core/Sound/Apu.cs | 107 +++++++++++++- GameboyDotnet.Core/Sound/ApuRegisters.cs | 22 +++ GameboyDotnet.Core/Sound/AudioBuffer.cs | 29 ++++ .../Sound/Channels/SquareChannel2.cs | 131 ++++++++++++++++++ .../Timers/{MainTimer.cs => TimaTimer.cs} | 2 +- GameboyDotnet.SDL/Program.cs | 12 +- GameboyDotnet.SDL/SdlAudio.cs | 73 ++++++++++ .../{Renderer.cs => SdlRenderer.cs} | 5 +- GameboyDotnet.SDL/pkmn.ttf | Bin 0 -> 290240 bytes 11 files changed, 382 insertions(+), 16 deletions(-) create mode 100644 GameboyDotnet.Core/BitState.cs create mode 100644 GameboyDotnet.Core/Sound/ApuRegisters.cs create mode 100644 GameboyDotnet.Core/Sound/AudioBuffer.cs create mode 100644 GameboyDotnet.Core/Sound/Channels/SquareChannel2.cs rename GameboyDotnet.Core/Timers/{MainTimer.cs => TimaTimer.cs} (96%) create mode 100644 GameboyDotnet.SDL/SdlAudio.cs rename GameboyDotnet.SDL/{Renderer.cs => SdlRenderer.cs} (98%) create mode 100644 GameboyDotnet.SDL/pkmn.ttf 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/Gameboy.cs b/GameboyDotnet.Core/Gameboy.cs index b82dd81..61cc3ec 100644 --- a/GameboyDotnet.Core/Gameboy.cs +++ b/GameboyDotnet.Core/Gameboy.cs @@ -14,8 +14,8 @@ public partial class Gameboy public Cpu Cpu { get; } public Ppu Ppu { get; } public Apu Apu { get; } - public MainTimer TimaTimer { get; } - public DividerTimer DivTimer { get; } + public TimaTimer TimaTimer { get; } + public DivTimer DivTimer { get; } public bool IsFrameLimiterEnabled; public bool IsMemoryDumpRequested; @@ -25,8 +25,8 @@ public Gameboy(ILogger logger) Cpu = new Cpu(logger); Ppu = new Ppu(Cpu.MemoryController); Apu = new Apu(Cpu.MemoryController); - TimaTimer = new MainTimer(Cpu.MemoryController); - DivTimer = new DividerTimer(Cpu.MemoryController); + TimaTimer = new TimaTimer(Cpu.MemoryController); + DivTimer = new DivTimer(Cpu.MemoryController); } public void LoadProgram(FileStream stream) @@ -56,7 +56,7 @@ public Task RunAsync(bool frameLimitEnabled, CancellationToken ctsToken) Ppu.PushPpuCycles(tStates); TimaTimer.CheckAndIncrementTimer(ref tStates); DivTimer.CheckAndIncrementTimer(ref tStates); - // Apu.PushApuCycles(ref tStates); + Apu.PushApuCycles(ref tStates); currentCycles += tStates; } UpdateJoypadState(); diff --git a/GameboyDotnet.Core/Sound/Apu.cs b/GameboyDotnet.Core/Sound/Apu.cs index 735284a..9c00bba 100644 --- a/GameboyDotnet.Core/Sound/Apu.cs +++ b/GameboyDotnet.Core/Sound/Apu.cs @@ -1,11 +1,112 @@ -using GameboyDotnet.Memory; +using GameboyDotnet.Extensions; +using GameboyDotnet.Memory; +using GameboyDotnet.Sound.Channels; +using GameboyDotnet.Timers; namespace GameboyDotnet.Sound; -public class Apu(MemoryController memoryController) +public class Apu { - public void PushApuCycles(ref byte tStates) + public AudioBuffer AudioBuffer { get; init; } + public SquareChannel2 SquareChannel2 { get; init; } + + private const int ApuTStatesPerCpuCycle = 4; + private byte _dividerRegister => _memoryController.IoRegisters.MemorySpaceView[0x04]; + private BitState _currentDividerRegisterBitState = BitState.Lo; + private int _apuTCyclesCounter = 0; + private int _divApuCounter = 0; + + private readonly MemoryController _memoryController; + + public Apu(MemoryController memoryController) + { + AudioBuffer = new AudioBuffer(); + _memoryController = memoryController; + + //SquareChannel1 + SquareChannel2 = new SquareChannel2(memoryController, AudioBuffer); + //WaveChannel3 + //NoiseChannel4 + } + + public void PushApuCycles(ref byte tCycles) + { + _apuTCyclesCounter += tCycles / ApuTStatesPerCpuCycle; + + if (_apuTCyclesCounter > 2048) + { + _apuTCyclesCounter -= 2048; + } + + var divApuTicked = DivFallingEdgeDetected(); + if (divApuTicked) + { + StepFrameSequencer(ref tCycles); + } + + SquareChannel2.Step(ref tCycles); + } + + public void ResetFrameSequencer() + { + } + + private void StepFrameSequencer(ref byte tCycles) + { + _divApuCounter = (_divApuCounter + 1) & 0b111; //Wrap to 7 + + switch (_divApuCounter) + { + case 0: + UpdateLengthCounters(ref tCycles); + break; + case 1: + break; + case 2: + UpdateLengthCounters(ref tCycles); + UpdateSweep(); + break; + case 3: + break; + case 4: + UpdateLengthCounters(ref tCycles); + break; + case 5: + break; + case 6: + UpdateLengthCounters(ref tCycles); + UpdateSweep(); + break; + case 7: + UpdateVolumeEnvelope(ref tCycles); + break; + } + } + + private void UpdateVolumeEnvelope(ref byte tCycles) + { + SquareChannel2.UpdateVolume(ref tCycles); + } + + private void UpdateSweep() { + // throw new NotImplementedException(); + } + + private void UpdateLengthCounters(ref byte tCycles) + { + SquareChannel2.UpdateLengthTimer(ref tCycles); + } + + private bool DivFallingEdgeDetected() + { + var previousDividerRegisterBitState = _currentDividerRegisterBitState; + + _currentDividerRegisterBitState = + _dividerRegister.IsBitSet(Cycles.DivFallingEdgeDetectorBitIndex) + ? BitState.Hi + : BitState.Lo; + return previousDividerRegisterBitState == BitState.Hi && _currentDividerRegisterBitState == BitState.Lo; } } \ No newline at end of file diff --git a/GameboyDotnet.Core/Sound/ApuRegisters.cs b/GameboyDotnet.Core/Sound/ApuRegisters.cs new file mode 100644 index 0000000..8d79128 --- /dev/null +++ b/GameboyDotnet.Core/Sound/ApuRegisters.cs @@ -0,0 +1,22 @@ +using GameboyDotnet.Memory; + +namespace GameboyDotnet.Sound; + +public class ApuRegisters(MemoryController memoryController) +{ + /// + /// Bit 7 Audio On/Off + /// + public byte NR52AudioMasterControl => memoryController.IoRegisters.MemorySpaceView[0x26]; + + /// + /// Bits 7-4 CH4-CH1 left, Bits 3-0 CH4-CH1 right + /// + public byte NR51SoundPanning => memoryController.IoRegisters.MemorySpaceView[0x25]; + + /// + /// 7 - VIN left (safe to ignore for now), (654) - left volume, 3- VIN right, (210) - Right Volume + /// Value of 0 is treated as 1 (very quiet), value of 7 is then like full 8 (no reduction) + /// + public byte NR50MasterVolume => memoryController.IoRegisters.MemorySpaceView[0x24]; +} \ 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..5ca1013 --- /dev/null +++ b/GameboyDotnet.Core/Sound/AudioBuffer.cs @@ -0,0 +1,29 @@ +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; + private int CurrentBufferIndex = 0; + + public void EnqueueSample(float sample) + { + _sampleBuffer[CurrentBufferIndex] = sample; + CurrentBufferIndex++; + + if (CurrentBufferIndex >= BufferSize) + { + _sampleQueue.Enqueue(_sampleBuffer); + CurrentBufferIndex = 0; + } + } + + public bool TryDequeueSamples(out float[]? samples) + { + return _sampleQueue.TryDequeue(out samples); + } + } +} diff --git a/GameboyDotnet.Core/Sound/Channels/SquareChannel2.cs b/GameboyDotnet.Core/Sound/Channels/SquareChannel2.cs new file mode 100644 index 0000000..1e9a63f --- /dev/null +++ b/GameboyDotnet.Core/Sound/Channels/SquareChannel2.cs @@ -0,0 +1,131 @@ +using GameboyDotnet.Extensions; +using GameboyDotnet.Memory; + +namespace GameboyDotnet.Sound.Channels; + +public class SquareChannel2(MemoryController memoryController, AudioBuffer audioBuffer) +{ + public byte Nr21Channel2LengthTimerAndDutyCycle => memoryController.IoRegisters.MemorySpaceView[0x21]; + + public byte Nr22Channel2VolumeAndEnvelope => memoryController.IoRegisters.MemorySpaceView[0x22]; + + public byte Nr23Channel2PeriodLow => memoryController.IoRegisters.MemorySpaceView[0x23]; + + public byte Nr24Channel2PeriodHighAndControl => memoryController.IoRegisters.MemorySpaceView[0x24]; + + private byte[][] DutyCycles = + [ + [0, 0, 0, 0, 0, 0, 0, 1], //12,5% + [0, 0, 0, 0, 0, 0, 1, 1], //25% + [0, 0, 0, 0, 1, 1, 1, 1], //50% + [1, 1, 1, 1, 1, 1, 0, 0] //72,5% + ]; + + /// + /// Increments at 256Hz frequency, same cycle as DIV-APU, when it reaches 64, the channel is turned off + /// + public int LengthTimer; + + public int PeriodDividerTimer = 0; + public int GetPeriodValueFromRegisters => ((Nr24Channel2PeriodHighAndControl & 0b111) << 8) | Nr23Channel2PeriodLow; + public int VolumeEnvelopeTimer = 0; + + private int DutyCycleStep = 0; + private int VolumeLevel; + + private int SampleRateCounter = 87; //Gather samples each 87 cycles, so we'll get about 44,1Khz + private BitState _triggerBit = BitState.Lo; + + public void Step(ref byte tCycles) + { + var isPeriodDividerCountdownFinished = UpdatePeriodDividerTimer(ref tCycles); + + if (isPeriodDividerCountdownFinished) + { + DutyCycleStep = (DutyCycleStep + 1) & 0b111; //Wrap after 7 + } + + UpdateSampleState(ref tCycles); + } + + private void UpdateSampleState(ref byte tCycles) + { + SampleRateCounter -= tCycles; + if (SampleRateCounter <= 0) + { + SampleRateCounter += 87; //TODO: Read from audioBuffer.SampleRate and determine the number + + //Normalize 0-15 from Gameboy to 0.0f-1.0f range + var normalizedVolumeFactor = Math.Clamp((float)VolumeLevel / 15, 0.0f, 1.0f); + + //Value from 0 to 3 + var dutyCyclesIndex = (Nr21Channel2LengthTimerAndDutyCycle & 0b11_00_00_00) >> 6; + + var sample = DutyCycles[dutyCyclesIndex][DutyCycleStep] == 1 + ? 1.0f * normalizedVolumeFactor + : -1.0f * normalizedVolumeFactor; //TODO: Should low value be also multiplied? + + Console.WriteLine($"Sample: {sample} (Volume: {VolumeLevel}"); + + audioBuffer.EnqueueSample(sample); + } + } + + private bool UpdatePeriodDividerTimer(ref byte tCycles) + { + PeriodDividerTimer -= tCycles; + if (PeriodDividerTimer > 0) + { + return false; + } + + //TODO: Double check the math on resetting PeriodTimer's value + PeriodDividerTimer += (2048 - GetPeriodValueFromRegisters) * 4; + + return true; + } + + public void UpdateLengthTimer(ref byte tCycles) + { + //Check if LengthTimer is enabled + if(Nr24Channel2PeriodHighAndControl.IsBitSet(6)) + { + LengthTimer -= tCycles; + if (LengthTimer <= 0) + { + //Reset the timer to initial value from register + LengthTimer += Nr21Channel2LengthTimerAndDutyCycle & 0b111111; + } + } + } + + public void UpdateVolume(ref byte tCycles) + { + VolumeEnvelopeTimer -= tCycles; + VolumeLevel = 2; + + if (VolumeEnvelopeTimer <= 0) + { + //4194304Hz / 64Hz = 65536 tCycles per update + VolumeEnvelopeTimer += 65536; + + var volumeAndEnvelopeRegister = Nr22Channel2VolumeAndEnvelope; + + var volumeSweepPace = volumeAndEnvelopeRegister & 0b111; + + //VolumeSweepPace = 0 means the volume sweep is disabled + if (volumeSweepPace == 0) + return; + + var isEnvelopeDirectionRising = volumeAndEnvelopeRegister.IsBitSet(3); + + VolumeLevel = Math.Clamp( + VolumeLevel += isEnvelopeDirectionRising + ? volumeSweepPace + : -volumeSweepPace, + 0, 15); + + Console.WriteLine($"Updated Volume: {VolumeLevel}"); + } + } +} \ No newline at end of file diff --git a/GameboyDotnet.Core/Timers/MainTimer.cs b/GameboyDotnet.Core/Timers/TimaTimer.cs similarity index 96% rename from GameboyDotnet.Core/Timers/MainTimer.cs rename to GameboyDotnet.Core/Timers/TimaTimer.cs index 25b3dd7..e576fe6 100644 --- a/GameboyDotnet.Core/Timers/MainTimer.cs +++ b/GameboyDotnet.Core/Timers/TimaTimer.cs @@ -4,7 +4,7 @@ namespace GameboyDotnet.Timers; -public class MainTimer(MemoryController memoryController) +public class TimaTimer(MemoryController memoryController) { private int _tStatesCounter; private const int TimaControlIORegisterOffset = 0x07; diff --git a/GameboyDotnet.SDL/Program.cs b/GameboyDotnet.SDL/Program.cs index 45bc43a..4c1bed1 100644 --- a/GameboyDotnet.SDL/Program.cs +++ b/GameboyDotnet.SDL/Program.cs @@ -21,9 +21,12 @@ : Path.Combine(Directory.GetCurrentDirectory(), emulatorSettings.RomPath); //Initialize SDL renderer and window -var (renderer, window) = Renderer.InitializeRendererAndWindow(logger, emulatorSettings); +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); @@ -69,7 +72,7 @@ case SDL_EventType.SDL_QUIT: cts.Cancel(); running = false; - Renderer.Destroy(renderer, window); + SdlRenderer.Destroy(renderer, window); break; } } @@ -77,8 +80,9 @@ if(gameboy.Ppu.FrameBuffer.TryDequeueFrame(out var frame)) { string bufferedFramesText = $"Speed: {gameboy.Ppu.FrameBuffer.Fps/ 60.0 * 100.0:0.0}% / {gameboy.Ppu.FrameBuffer.Fps:0} FPS"; - Renderer.RenderStates(ref renderer, ref window, frame!, bufferedFramesText); + SdlRenderer.RenderStates(ref renderer, ref window, frame!, bufferedFramesText); } } -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/SdlAudio.cs b/GameboyDotnet.SDL/SdlAudio.cs new file mode 100644 index 0000000..8222e34 --- /dev/null +++ b/GameboyDotnet.SDL/SdlAudio.cs @@ -0,0 +1,73 @@ +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 = 1, //TODO: Mono or stereo? + samples = 512, + 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]; + + for (int i = 0; i < sampleCount; i++) + { + if (!_audioBuffer.TryDequeueSamples(out float[]? sampleBatch) || sampleBatch == null) + { + samples[i] = 0.0f; + } + else + { + samples[i] = sampleBatch[i % sampleBatch.Length]; + } + } + + 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 98% rename from GameboyDotnet.SDL/Renderer.cs rename to GameboyDotnet.SDL/SdlRenderer.cs index 4685194..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; @@ -94,7 +93,7 @@ public static void RenderStates(ref IntPtr renderer, ref IntPtr window, byte[,] 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()); } diff --git a/GameboyDotnet.SDL/pkmn.ttf b/GameboyDotnet.SDL/pkmn.ttf new file mode 100644 index 0000000000000000000000000000000000000000..966b30198fa79208d588d464c2f0fdde106b93eb GIT binary patch literal 290240 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 Dk1=JR literal 0 HcmV?d00001 From 915880958a9f946aa2126540deaeafd751762b33 Mon Sep 17 00:00:00 2001 From: Szymon Sandura Date: Sun, 18 May 2025 12:45:29 +0200 Subject: [PATCH 07/11] Implemented Square Channel 2 --- GameboyDotnet.Core/Gameboy.cs | 16 +- GameboyDotnet.Core/Graphics/Ppu.cs | 5 +- .../Memory/BuildingBlocks/IoBank.cs | 50 +++++- GameboyDotnet.Core/Memory/Mbc/Mbc1.cs | 4 +- GameboyDotnet.Core/Memory/MemoryController.cs | 79 +++++----- .../Processor/Cpu.OperationBlocks.Block0.cs | 34 ++-- .../Processor/Cpu.OperationBlocks.Block1.cs | 1 + GameboyDotnet.Core/Processor/Cpu.cs | 7 +- GameboyDotnet.Core/Sound/Apu.cs | 147 ++++++++++++------ GameboyDotnet.Core/Sound/AudioBuffer.cs | 18 ++- .../Channels/BuildingBlocks/BaseChannel.cs | 126 +++++++++++++++ .../BuildingBlocks/BaseSquareChannel.cs | 50 ++++++ .../Sound/Channels/SquareChannel1.cs | 5 + .../Sound/Channels/SquareChannel2.cs | 128 +-------------- GameboyDotnet.Core/Timers/DivTimer.cs | 3 +- GameboyDotnet.Core/Timers/TimaTimer.cs | 1 + GameboyDotnet.SDL/GameboyDotnet.SDL.csproj | 4 +- GameboyDotnet.SDL/Program.cs | 2 +- GameboyDotnet.SDL/SdlAudio.cs | 27 ++-- GameboyDotnet.SDL/appsettings.json | 2 +- GameboyDotnet.Tests/Examples.cs | 48 ++++++ 21 files changed, 486 insertions(+), 271 deletions(-) create mode 100644 GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseChannel.cs create mode 100644 GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseSquareChannel.cs create mode 100644 GameboyDotnet.Core/Sound/Channels/SquareChannel1.cs create mode 100644 GameboyDotnet.Tests/Examples.cs diff --git a/GameboyDotnet.Core/Gameboy.cs b/GameboyDotnet.Core/Gameboy.cs index 61cc3ec..18633fd 100644 --- a/GameboyDotnet.Core/Gameboy.cs +++ b/GameboyDotnet.Core/Gameboy.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using GameboyDotnet.Common; using GameboyDotnet.Graphics; +using GameboyDotnet.Memory; using GameboyDotnet.Processor; using GameboyDotnet.Sound; using GameboyDotnet.Timers; @@ -11,6 +12,7 @@ namespace GameboyDotnet; public partial class Gameboy { private ILogger _logger; + public MemoryController MemoryController { get; } public Cpu Cpu { get; } public Ppu Ppu { get; } public Apu Apu { get; } @@ -22,11 +24,12 @@ public partial class Gameboy public Gameboy(ILogger logger) { _logger = logger; - Cpu = new Cpu(logger); - Ppu = new Ppu(Cpu.MemoryController); - Apu = new Apu(Cpu.MemoryController); - TimaTimer = new TimaTimer(Cpu.MemoryController); - DivTimer = new DivTimer(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) @@ -50,7 +53,7 @@ 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); @@ -59,6 +62,7 @@ public Task RunAsync(bool frameLimitEnabled, CancellationToken ctsToken) Apu.PushApuCycles(ref tStates); currentCycles += tStates; } + UpdateJoypadState(); currentCycles -= cyclesPerFrame; Ppu.FrameBuffer.EnqueueFrame(Ppu.Lcd); diff --git a/GameboyDotnet.Core/Graphics/Ppu.cs b/GameboyDotnet.Core/Graphics/Ppu.cs index 9e53a5a..c0435b4 100644 --- a/GameboyDotnet.Core/Graphics/Ppu.cs +++ b/GameboyDotnet.Core/Graphics/Ppu.cs @@ -91,8 +91,7 @@ private void PushScanlineToBuffer() { RenderBackgroundOrWindow(); } - - + if (Lcd.ObjDisplay == ObjDisplay.Enabled) RenderObjects(); } @@ -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; diff --git a/GameboyDotnet.Core/Memory/BuildingBlocks/IoBank.cs b/GameboyDotnet.Core/Memory/BuildingBlocks/IoBank.cs index 0c977c5..88bf73c 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; @@ -9,10 +10,12 @@ public class IoBank : FixedBank 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; } @@ -37,7 +40,50 @@ public override void WriteByte(ref ushort address, ref byte value) _joypadRegister = (byte)(value & 0xF0 | (byte)(ButtonStates & 0x0F)); } } - + + switch (address) + { + case 0xFF26: + _apu.UpdatePowerState(ref value); + return; + break; + case 0xFF25: + _apu.UpdateChannelPanningStates(ref value); + return; + case 0xFF24: + _apu.UpdateVolumeControlStates(ref value); + return; + case 0xFF10: + //TODO: Channel 1 Sweep + break; + case 0xFF11: + //TODO: Channel 1 length/duty + break; + case 0xFF12: + //TODO: Channel 1 volume & envelope + break; + case 0xFF13: + //TODO: Channel 1 period low + break; + case 0xFF14: + //TODO: Channel 1 period high & control + break; + case 0xFF16: + _apu.SquareChannel2.UpdateLengthDuty(ref value); + return; + case 0xFF17: + _apu.SquareChannel2.UpdateVolumeEnvelope(ref value); + return; + case 0xFF18: + _apu.SquareChannel2.UpdatePeriodLow(ref value); + return; + case 0xFF19: + _apu.SquareChannel2.UpdatePeriodHighControl(ref value); + return; + default: + break; + } + base.WriteByte(ref address, ref value); } 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 da8a145..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; @@ -10,31 +11,23 @@ public class MemoryController { private readonly FixedBank[] _memoryMap = new FixedBank[0xFFFF + 1]; public SwitchableBank RomBankNn; - - public SwitchableBank Vram = new(BankAddress.VramStart, BankAddress.VramEnd, nameof(Vram), bankSizeInBytes: 8192, - numberOfBanks: 2); - + public SwitchableBank Vram = new(BankAddress.VramStart, BankAddress.VramEnd, nameof(Vram), bankSizeInBytes: 8192, numberOfBanks: 2); public readonly FixedBank Wram0 = new(BankAddress.Wram0Start, BankAddress.Wram0End, nameof(Wram0)); - - public readonly SwitchableBank Wram1 = new(BankAddress.Wram1Start, BankAddress.Wram1End, nameof(Wram1), - bankSizeInBytes: 4096, numberOfBanks: 8); - + public readonly SwitchableBank Wram1 = new(BankAddress.Wram1Start, BankAddress.Wram1End, nameof(Wram1), bankSizeInBytes: 4096, numberOfBanks: 8); public readonly FixedBank Oam = new(BankAddress.OamStart, BankAddress.OamEnd, nameof(Oam)); 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(); } @@ -44,7 +37,7 @@ 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(); //Load the first bank @@ -185,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.OperationBlocks.Block0.cs b/GameboyDotnet.Core/Processor/Cpu.OperationBlocks.Block0.cs index 6ca8c0b..8dc4e31 100644 --- a/GameboyDotnet.Core/Processor/Cpu.OperationBlocks.Block0.cs +++ b/GameboyDotnet.Core/Processor/Cpu.OperationBlocks.Block0.cs @@ -22,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); @@ -35,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); @@ -47,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); @@ -59,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); @@ -71,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); @@ -83,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); @@ -95,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); @@ -108,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) @@ -131,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) { @@ -153,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) @@ -173,7 +173,7 @@ public partial class Cpu private (byte instructionBytesLength, byte durationTStates) RotateLeftRegisterA(ref byte opCode) { if(_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("{opCode:X2} - Rotating left register A", opCode); + _logger.LogDebug("{OpCode:X2} - Rotating left register A", opCode); var oldCarryFlag = Register.CarryFlag; (Register.ZeroFlag, Register.NegativeFlag, Register.HalfCarryFlag) = (false, false, false); @@ -188,7 +188,7 @@ public partial class Cpu private (byte instructionBytesLength, byte durationTStates) RotateRightRegisterA(ref byte opCode) { if(_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("{opCode:X2} - Rotating right register A", opCode); + _logger.LogDebug("{OpCode:X2} - Rotating right register A", opCode); var oldCarryFlag = Register.CarryFlag; (Register.ZeroFlag, Register.NegativeFlag, Register.HalfCarryFlag) = (false, false, false); @@ -204,7 +204,7 @@ public partial class Cpu private (byte instructionBytesLength, byte durationTStates) RotateLeftRegisterAThroughCarry(ref byte opCode) { if(_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("{opCode:X2} - Rotating left register A through carry", opCode); + _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; @@ -218,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)); @@ -232,7 +232,7 @@ public partial class Cpu private (byte instructionBytesLength, byte durationTStates) DecimalAdjustAccumulator(ref byte opCode) { if(_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("{opCode:X2} - Decimal adjust accumulator (DAA)", opCode); + _logger.LogDebug("{OpCode:X2} - Decimal adjust accumulator (DAA)", opCode); byte adjust = 0; @@ -290,7 +290,7 @@ 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); @@ -305,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); 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.cs b/GameboyDotnet.Core/Processor/Cpu.cs index ee5010e..07e69b0 100644 --- a/GameboyDotnet.Core/Processor/Cpu.cs +++ b/GameboyDotnet.Core/Processor/Cpu.cs @@ -1,6 +1,7 @@ using GameboyDotnet.Components.Cpu; using GameboyDotnet.Extensions; using GameboyDotnet.Memory; +using GameboyDotnet.Sound; using Microsoft.Extensions.Logging; namespace GameboyDotnet.Processor; @@ -12,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() @@ -27,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 diff --git a/GameboyDotnet.Core/Sound/Apu.cs b/GameboyDotnet.Core/Sound/Apu.cs index 9c00bba..b5fc99d 100644 --- a/GameboyDotnet.Core/Sound/Apu.cs +++ b/GameboyDotnet.Core/Sound/Apu.cs @@ -8,105 +8,160 @@ namespace GameboyDotnet.Sound; public class Apu { public AudioBuffer AudioBuffer { get; init; } - public SquareChannel2 SquareChannel2 { get; init; } + public SquareChannel1 SquareChannel1 { get; private set; } + public SquareChannel2 SquareChannel2 { get; private set; } + public bool IsAudioOn { get; private set; } + public byte LeftMasterVolume { get; private set; } + public byte RightMasterVolume { get; private set; } - private const int ApuTStatesPerCpuCycle = 4; - private byte _dividerRegister => _memoryController.IoRegisters.MemorySpaceView[0x04]; - private BitState _currentDividerRegisterBitState = BitState.Lo; - private int _apuTCyclesCounter = 0; - private int _divApuCounter = 0; - private readonly MemoryController _memoryController; + private BitState _currentDividerRegisterBitState = BitState.Lo; + private int _frameSequencerCyclesTimer = 8192; + private int _frameSequencerPosition = 0; + private int _maxDigitalSumOfOutputPerChannel = 15 * 4; //4 channels, 0-15 volume level each - public Apu(MemoryController memoryController) + public Apu() { AudioBuffer = new AudioBuffer(); - _memoryController = memoryController; - - //SquareChannel1 - SquareChannel2 = new SquareChannel2(memoryController, AudioBuffer); - //WaveChannel3 - //NoiseChannel4 + SquareChannel1 = new SquareChannel1(AudioBuffer); + SquareChannel2 = new SquareChannel2(AudioBuffer); } + private int SampleCounter = 87; + public void PushApuCycles(ref byte tCycles) { - _apuTCyclesCounter += tCycles / ApuTStatesPerCpuCycle; - - if (_apuTCyclesCounter > 2048) - { - _apuTCyclesCounter -= 2048; - } - - var divApuTicked = DivFallingEdgeDetected(); - if (divApuTicked) + for (int i = tCycles; i > 0; i--) { - StepFrameSequencer(ref tCycles); + StepFrameSequencer(); + SquareChannel1.Step(); + SquareChannel2.Step(); + //WaveChannel.Step(); + //NoiseChannel.Step(); + + SampleCounter--; + if(SampleCounter > 0) + continue; + + SampleCounter = 87; + if (IsAudioOn) + { + int leftSum = 0; + int rightSum = 0; + + if (SquareChannel1.IsLeftSpeakerOn) leftSum += SquareChannel1.CurrentOutput; + if (SquareChannel1.IsRightSpeakerOn) rightSum += SquareChannel1.CurrentOutput; + if (SquareChannel2.IsLeftSpeakerOn) leftSum += SquareChannel2.CurrentOutput; + if (SquareChannel2.IsRightSpeakerOn) rightSum += SquareChannel2.CurrentOutput; + // if(WaveChannel.IsLeftSpeakerOn) leftSample += WaveChannel.CurrentSample; + // if(WaveChannel.IsRightSpeakerOn) rightSample += WaveChannel.CurrentSample; + // if(NoiseChannel.IsLeftSpeakerOn) leftSample += NoiseChannel.CurrentSample; + // if(NoiseChannel.IsRightSpeakerOn) rightSample += NoiseChannel.CurrentSample; + + //Normalize digital [0-15]*4 channels to [0-2f], then shift to [-1f, 1f] + float normalizedLeft = (float)leftSum / _maxDigitalSumOfOutputPerChannel *2f - 1f; + float normalizedRight = (float)rightSum / _maxDigitalSumOfOutputPerChannel *2f - 1f; + AudioBuffer.EnqueueSample(leftSum, rightSum); + } } - - SquareChannel2.Step(ref tCycles); } public void ResetFrameSequencer() { } - private void StepFrameSequencer(ref byte tCycles) + private void StepFrameSequencer() { - _divApuCounter = (_divApuCounter + 1) & 0b111; //Wrap to 7 + _frameSequencerCyclesTimer--; + + if (_frameSequencerCyclesTimer > 0) + return; + + _frameSequencerCyclesTimer = 8192; - switch (_divApuCounter) + _frameSequencerPosition = (_frameSequencerPosition + 1) & 0b111; //Wrap to 7 + + switch (_frameSequencerPosition) { case 0: - UpdateLengthCounters(ref tCycles); + TickLengthCounters(); break; case 1: break; case 2: - UpdateLengthCounters(ref tCycles); - UpdateSweep(); + TickLengthCounters(); + TickSweep(); break; case 3: break; case 4: - UpdateLengthCounters(ref tCycles); + TickLengthCounters(); break; case 5: break; case 6: - UpdateLengthCounters(ref tCycles); - UpdateSweep(); + TickLengthCounters(); + TickSweep(); break; case 7: - UpdateVolumeEnvelope(ref tCycles); + TickVolumeEnvelope(); break; } } - private void UpdateVolumeEnvelope(ref byte tCycles) + private void TickVolumeEnvelope() { - SquareChannel2.UpdateVolume(ref tCycles); + SquareChannel2.UpdateVolume(); } - private void UpdateSweep() + private void TickSweep() { // throw new NotImplementedException(); } - private void UpdateLengthCounters(ref byte tCycles) + private void TickLengthCounters() { - SquareChannel2.UpdateLengthTimer(ref tCycles); + SquareChannel2.TickLengthTimer(); } - private bool DivFallingEdgeDetected() + private bool DivFallingEdgeDetected(ref byte dividerValue) { var previousDividerRegisterBitState = _currentDividerRegisterBitState; - - _currentDividerRegisterBitState = - _dividerRegister.IsBitSet(Cycles.DivFallingEdgeDetectorBitIndex) + + _currentDividerRegisterBitState = + dividerValue.IsBitSet(Cycles.DivFallingEdgeDetectorBitIndex) ? BitState.Hi : BitState.Lo; - + return previousDividerRegisterBitState == BitState.Hi && _currentDividerRegisterBitState == BitState.Lo; } + + public void UpdatePowerState(ref byte value) + { + if (IsAudioOn && !value.IsBitSet(7)) + { + IsAudioOn = false; + } + else if (!IsAudioOn && value.IsBitSet(7)) + { + IsAudioOn = true; + } + } + + public void UpdateChannelPanningStates(ref byte value) + { + //TODO: Implement other channels + SquareChannel2.IsLeftSpeakerOn = value.IsBitSet(4); + SquareChannel2.IsRightSpeakerOn = value.IsBitSet(0); + SquareChannel2.IsLeftSpeakerOn = value.IsBitSet(5); + SquareChannel2.IsRightSpeakerOn = value.IsBitSet(1); + } + + public void UpdateVolumeControlStates(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/AudioBuffer.cs b/GameboyDotnet.Core/Sound/AudioBuffer.cs index 5ca1013..096507a 100644 --- a/GameboyDotnet.Core/Sound/AudioBuffer.cs +++ b/GameboyDotnet.Core/Sound/AudioBuffer.cs @@ -6,17 +6,23 @@ public class AudioBuffer { private readonly ConcurrentQueue _sampleQueue = new(); private readonly float[] _sampleBuffer = new float[BufferSize]; - private const int BufferSize = 1024; + private const int BufferSize = 1024 * 2; private int CurrentBufferIndex = 0; - public void EnqueueSample(float sample) + public void EnqueueSample(float leftSample, float rightSample) { - _sampleBuffer[CurrentBufferIndex] = sample; - CurrentBufferIndex++; - + _sampleBuffer[CurrentBufferIndex++] = leftSample; + _sampleBuffer[CurrentBufferIndex++] = rightSample; + if (CurrentBufferIndex >= BufferSize) { - _sampleQueue.Enqueue(_sampleBuffer); + CurrentBufferIndex = 0; + float[] block = new float[BufferSize]; + Array.Copy(_sampleBuffer, block, BufferSize); + + if(_sampleQueue.Count < 10) + _sampleQueue.Enqueue(block); + CurrentBufferIndex = 0; } } diff --git a/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseChannel.cs b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseChannel.cs new file mode 100644 index 0000000..f178d07 --- /dev/null +++ b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseChannel.cs @@ -0,0 +1,126 @@ +using GameboyDotnet.Extensions; + +namespace GameboyDotnet.Sound.Channels.BuildingBlocks; + +public abstract class BaseChannel(AudioBuffer audioBuffer) +{ + public bool IsChannelOn; + public bool IsRightSpeakerOn; + public bool IsLeftSpeakerOn; + + //NRx1 + public int InitialLengthTimer; + + //NRX2 + public int VolumeEnvelopeSweepPace; + public bool VolumeEnvelopeDirection; + public int InitialVolume; + + //NRX3 + public byte PeriodLow; + + //NRX4 + public byte PeriodHigh; + public bool IsLengthEnabled; // Bit 6 + + public int GetPeriodValueFromRegisters => ((PeriodHigh & 0b111) << 8) | PeriodLow; + + public int LengthTimer; + public int PeriodDividerTimer = 0; + public int VolumeEnvelopeTimer = 0; + protected int VolumeLevel; + + + public int CurrentOutput; + + public abstract void Step(); + + protected abstract void StepSampleState(); + + protected virtual bool StepPeriodDividerTimer() + { + PeriodDividerTimer--; + if (PeriodDividerTimer > 0) + { + return false; + } + + //TODO: Double check the math on resetting PeriodTimer's value + PeriodDividerTimer += (2048 - GetPeriodValueFromRegisters) * 4; + + return true; + } + + + + public void TickLengthTimer() + { + //Check if LengthTimer is enabled + if (IsLengthEnabled) + { + LengthTimer--; + if (LengthTimer <= 0) + { + IsChannelOn = false; + } + } + } + + public void UpdateVolume() + { + int period = (VolumeEnvelopeSweepPace == 0) ? 8 : VolumeEnvelopeSweepPace; + + VolumeEnvelopeTimer--; + + if (VolumeEnvelopeTimer <= 0) + { + VolumeEnvelopeTimer = period; + + if (VolumeEnvelopeDirection && VolumeLevel < 15) + VolumeLevel++; + else if (!VolumeEnvelopeDirection && VolumeLevel > 0) + VolumeLevel--; + } + } + + public virtual void UpdateLengthDuty(ref byte value) + { + InitialLengthTimer = value & 0b0011_1111; + LengthTimer = 64 - InitialLengthTimer; + } + + public void UpdateVolumeEnvelope(ref byte value) + { + InitialVolume = value & 0b1111_0000 >> 4; + VolumeEnvelopeDirection = value.IsBitSet(3); + VolumeEnvelopeSweepPace = value & 0b111; + } + + public void UpdatePeriodLow(ref byte value) + { + PeriodLow = value; + } + + public void UpdatePeriodHighControl(ref byte value) + { + IsLengthEnabled = value.IsBitSet(6); + PeriodHigh = value; + + if (value.IsBitSet(7)) + { + Trigger(); + } + } + + protected virtual void Trigger() + { + IsChannelOn = true; + + if (LengthTimer == 0) + LengthTimer = 64; + + PeriodDividerTimer = (2048 - GetPeriodValueFromRegisters) * 4; + VolumeEnvelopeTimer = (VolumeEnvelopeSweepPace == 0) ? 8 : VolumeEnvelopeSweepPace; + VolumeLevel = InitialVolume; + } +} diff --git a/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseSquareChannel.cs b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseSquareChannel.cs new file mode 100644 index 0000000..587de13 --- /dev/null +++ b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseSquareChannel.cs @@ -0,0 +1,50 @@ +namespace GameboyDotnet.Sound.Channels.BuildingBlocks; + +public class BaseSquareChannel(AudioBuffer audioBuffer) : BaseChannel(audioBuffer) +{ + 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() + { + var isPeriodDividerCountdownFinished = StepPeriodDividerTimer(); + + if (isPeriodDividerCountdownFinished) + { + DutyCycleStep = (DutyCycleStep + 1) & 0b111; //Wrap after 7 + } + + StepSampleState(); + } + + protected override void StepSampleState() + { + CurrentOutput = DutyCycles[WaveDutyIndex][DutyCycleStep] == 1 && IsChannelOn + ? VolumeLevel // Integer 0–15 + : 0; + } + + public override void UpdateLengthDuty(ref byte value) + { + WaveDutyIndex = (value & 0b1100_0000) >> 6; + base.UpdateLengthDuty(ref value); + } + + protected override void Trigger() + { + if (!IsChannelOn) + DutyCycleStep = 0; + + base.Trigger(); + } +} \ 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..32c198b --- /dev/null +++ b/GameboyDotnet.Core/Sound/Channels/SquareChannel1.cs @@ -0,0 +1,5 @@ +using GameboyDotnet.Sound.Channels.BuildingBlocks; + +namespace GameboyDotnet.Sound.Channels; + +public class SquareChannel1(AudioBuffer audioBuffer) : BaseSquareChannel(audioBuffer); \ No newline at end of file diff --git a/GameboyDotnet.Core/Sound/Channels/SquareChannel2.cs b/GameboyDotnet.Core/Sound/Channels/SquareChannel2.cs index 1e9a63f..26d172d 100644 --- a/GameboyDotnet.Core/Sound/Channels/SquareChannel2.cs +++ b/GameboyDotnet.Core/Sound/Channels/SquareChannel2.cs @@ -1,131 +1,7 @@ using GameboyDotnet.Extensions; using GameboyDotnet.Memory; +using GameboyDotnet.Sound.Channels.BuildingBlocks; namespace GameboyDotnet.Sound.Channels; -public class SquareChannel2(MemoryController memoryController, AudioBuffer audioBuffer) -{ - public byte Nr21Channel2LengthTimerAndDutyCycle => memoryController.IoRegisters.MemorySpaceView[0x21]; - - public byte Nr22Channel2VolumeAndEnvelope => memoryController.IoRegisters.MemorySpaceView[0x22]; - - public byte Nr23Channel2PeriodLow => memoryController.IoRegisters.MemorySpaceView[0x23]; - - public byte Nr24Channel2PeriodHighAndControl => memoryController.IoRegisters.MemorySpaceView[0x24]; - - private byte[][] DutyCycles = - [ - [0, 0, 0, 0, 0, 0, 0, 1], //12,5% - [0, 0, 0, 0, 0, 0, 1, 1], //25% - [0, 0, 0, 0, 1, 1, 1, 1], //50% - [1, 1, 1, 1, 1, 1, 0, 0] //72,5% - ]; - - ///

- /// Increments at 256Hz frequency, same cycle as DIV-APU, when it reaches 64, the channel is turned off - /// - public int LengthTimer; - - public int PeriodDividerTimer = 0; - public int GetPeriodValueFromRegisters => ((Nr24Channel2PeriodHighAndControl & 0b111) << 8) | Nr23Channel2PeriodLow; - public int VolumeEnvelopeTimer = 0; - - private int DutyCycleStep = 0; - private int VolumeLevel; - - private int SampleRateCounter = 87; //Gather samples each 87 cycles, so we'll get about 44,1Khz - private BitState _triggerBit = BitState.Lo; - - public void Step(ref byte tCycles) - { - var isPeriodDividerCountdownFinished = UpdatePeriodDividerTimer(ref tCycles); - - if (isPeriodDividerCountdownFinished) - { - DutyCycleStep = (DutyCycleStep + 1) & 0b111; //Wrap after 7 - } - - UpdateSampleState(ref tCycles); - } - - private void UpdateSampleState(ref byte tCycles) - { - SampleRateCounter -= tCycles; - if (SampleRateCounter <= 0) - { - SampleRateCounter += 87; //TODO: Read from audioBuffer.SampleRate and determine the number - - //Normalize 0-15 from Gameboy to 0.0f-1.0f range - var normalizedVolumeFactor = Math.Clamp((float)VolumeLevel / 15, 0.0f, 1.0f); - - //Value from 0 to 3 - var dutyCyclesIndex = (Nr21Channel2LengthTimerAndDutyCycle & 0b11_00_00_00) >> 6; - - var sample = DutyCycles[dutyCyclesIndex][DutyCycleStep] == 1 - ? 1.0f * normalizedVolumeFactor - : -1.0f * normalizedVolumeFactor; //TODO: Should low value be also multiplied? - - Console.WriteLine($"Sample: {sample} (Volume: {VolumeLevel}"); - - audioBuffer.EnqueueSample(sample); - } - } - - private bool UpdatePeriodDividerTimer(ref byte tCycles) - { - PeriodDividerTimer -= tCycles; - if (PeriodDividerTimer > 0) - { - return false; - } - - //TODO: Double check the math on resetting PeriodTimer's value - PeriodDividerTimer += (2048 - GetPeriodValueFromRegisters) * 4; - - return true; - } - - public void UpdateLengthTimer(ref byte tCycles) - { - //Check if LengthTimer is enabled - if(Nr24Channel2PeriodHighAndControl.IsBitSet(6)) - { - LengthTimer -= tCycles; - if (LengthTimer <= 0) - { - //Reset the timer to initial value from register - LengthTimer += Nr21Channel2LengthTimerAndDutyCycle & 0b111111; - } - } - } - - public void UpdateVolume(ref byte tCycles) - { - VolumeEnvelopeTimer -= tCycles; - VolumeLevel = 2; - - if (VolumeEnvelopeTimer <= 0) - { - //4194304Hz / 64Hz = 65536 tCycles per update - VolumeEnvelopeTimer += 65536; - - var volumeAndEnvelopeRegister = Nr22Channel2VolumeAndEnvelope; - - var volumeSweepPace = volumeAndEnvelopeRegister & 0b111; - - //VolumeSweepPace = 0 means the volume sweep is disabled - if (volumeSweepPace == 0) - return; - - var isEnvelopeDirectionRising = volumeAndEnvelopeRegister.IsBitSet(3); - - VolumeLevel = Math.Clamp( - VolumeLevel += isEnvelopeDirectionRising - ? volumeSweepPace - : -volumeSweepPace, - 0, 15); - - Console.WriteLine($"Updated Volume: {VolumeLevel}"); - } - } -} \ No newline at end of file +public class SquareChannel2(AudioBuffer audioBuffer) : BaseSquareChannel(audioBuffer); \ No newline at end of file diff --git a/GameboyDotnet.Core/Timers/DivTimer.cs b/GameboyDotnet.Core/Timers/DivTimer.cs index 1697e05..2c3c8b2 100644 --- a/GameboyDotnet.Core/Timers/DivTimer.cs +++ b/GameboyDotnet.Core/Timers/DivTimer.cs @@ -11,8 +11,9 @@ 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/TimaTimer.cs b/GameboyDotnet.Core/Timers/TimaTimer.cs index e576fe6..93982dd 100644 --- a/GameboyDotnet.Core/Timers/TimaTimer.cs +++ b/GameboyDotnet.Core/Timers/TimaTimer.cs @@ -27,6 +27,7 @@ internal void CheckAndIncrementTimer(ref byte durationTStates) 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/GameboyDotnet.SDL.csproj b/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj index b8b95ba..6b442de 100644 --- a/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj +++ b/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj @@ -68,7 +68,7 @@ PreserveNewest - + PreserveNewest @@ -77,7 +77,7 @@ PreserveNewest - + PreserveNewest diff --git a/GameboyDotnet.SDL/Program.cs b/GameboyDotnet.SDL/Program.cs index 4c1bed1..9bed201 100644 --- a/GameboyDotnet.SDL/Program.cs +++ b/GameboyDotnet.SDL/Program.cs @@ -12,6 +12,7 @@ .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .Build(); + var emulatorSettings = new EmulatorSettings(); configuration.GetSection("EmulatorSettings").Bind(emulatorSettings); var logger = LoggerHelper.GetLogger(emulatorSettings.LogLevel); @@ -39,7 +40,6 @@ running = false; }; - Task.Run(() => gameboy.RunAsync(emulatorSettings.FrameLimitEnabled, cts.Token)); // Main SDL loop diff --git a/GameboyDotnet.SDL/SdlAudio.cs b/GameboyDotnet.SDL/SdlAudio.cs index 8222e34..4bce21f 100644 --- a/GameboyDotnet.SDL/SdlAudio.cs +++ b/GameboyDotnet.SDL/SdlAudio.cs @@ -23,8 +23,8 @@ public void Initialize() { freq = 48000, format = AUDIO_F32SYS, //TODO: Check which format should be set - channels = 1, //TODO: Mono or stereo? - samples = 512, + channels = 2, + samples = 1024, callback = _audioCallbackDelegate }; @@ -33,7 +33,7 @@ public void Initialize() device: null, iscapture: 0, ref desiredSpec, - out _, + out _, allowed_changes: 0); if (_audioDevice <= 0) @@ -41,27 +41,28 @@ public void Initialize() 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]; - - for (int i = 0; i < sampleCount; i++) + int offset = 0; + + while(offset < sampleCount) { if (!_audioBuffer.TryDequeueSamples(out float[]? sampleBatch) || sampleBatch == null) { - samples[i] = 0.0f; + break; } - else - { - samples[i] = sampleBatch[i % sampleBatch.Length]; - } - } + int samplesToCopy = Math.Min(sampleBatch.Length, sampleCount - offset); + Array.Copy(sampleBatch, 0, samples, offset, samplesToCopy); + offset += samplesToCopy; + } + Marshal.Copy(samples, 0, stream, samples.Length); } diff --git a/GameboyDotnet.SDL/appsettings.json b/GameboyDotnet.SDL/appsettings.json index ce49a6b..f8d2672 100644 --- a/GameboyDotnet.SDL/appsettings.json +++ b/GameboyDotnet.SDL/appsettings.json @@ -4,7 +4,7 @@ "WindowWidth": 1200, "WindowHeight": 700, "FrameLimitEnabled": true, - "RomPath": "Tests/Roms/Super Mario Land 2 - 6 Golden Coins.gb", + "RomPath": "Tests/Roms/Super Mario Land 2.gb", "Keymap": { "SDLK_UP": "Up", "SDLK_LEFT": "Left", diff --git a/GameboyDotnet.Tests/Examples.cs b/GameboyDotnet.Tests/Examples.cs new file mode 100644 index 0000000..fc1b1c6 --- /dev/null +++ b/GameboyDotnet.Tests/Examples.cs @@ -0,0 +1,48 @@ +using System.Text; +using GameboyDotnet.Components.Cpu; +using GameboyDotnet.Memory; + +namespace GameboyDotnet.Tests; + +public class Examples +{ + private readonly CpuRegister Register = new(); + private readonly MemoryController MemoryController; + + public byte Fetch() + { + var nextInstructionCode = MemoryController.ReadByte(Register.PC); + return nextInstructionCode; + } + + public void DecodeAndExecute(byte instructionCode) + { + switch (instructionCode) + { + case 0x3C: + IncA(); + break; + default: + throw new NotImplementedException(); + } + } + + public void IncA() + { + //Increment + Register.A++; + + //Set flags + Register.NegativeFlag = false; + Register.CarryFlag = Register.A == 0; + Register.HalfCarryFlag = (Register.A & 0x0F) == 0; + + //Move to next instruction + Register.PC++; + } + + public void Nop() + { + + } +} \ No newline at end of file From 27d97c2f7af7f591834144137c85febd2b84f6d4 Mon Sep 17 00:00:00 2001 From: Szymon Sandura Date: Mon, 19 May 2025 14:58:26 +0200 Subject: [PATCH 08/11] Added Square Channel 1 and Wave3 Channel --- .../Memory/BuildingBlocks/IoBank.cs | 75 +++++++--- GameboyDotnet.Core/Sound/Apu.cs | 128 +++++++++++------- GameboyDotnet.Core/Sound/ApuRegisters.cs | 22 --- GameboyDotnet.Core/Sound/AudioBuffer.cs | 2 +- .../Channels/BuildingBlocks/BaseChannel.cs | 104 +++++++++----- .../BuildingBlocks/BaseSquareChannel.cs | 47 +++++-- .../Sound/Channels/NoiseChannel.cs | 33 +++++ .../Sound/Channels/SquareChannel1.cs | 59 +++++++- .../Sound/Channels/SquareChannel2.cs | 6 +- .../Sound/Channels/WaveChannel.cs | 87 ++++++++++++ GameboyDotnet.SDL/GameboyDotnet.SDL.csproj | 3 + GameboyDotnet.SDL/Program.cs | 33 ++++- GameboyDotnet.SDL/Tests/Roms/dmg_sound.gb | Bin 0 -> 65536 bytes 13 files changed, 457 insertions(+), 142 deletions(-) delete mode 100644 GameboyDotnet.Core/Sound/ApuRegisters.cs create mode 100644 GameboyDotnet.Core/Sound/Channels/NoiseChannel.cs create mode 100644 GameboyDotnet.Core/Sound/Channels/WaveChannel.cs create mode 100644 GameboyDotnet.SDL/Tests/Roms/dmg_sound.gb diff --git a/GameboyDotnet.Core/Memory/BuildingBlocks/IoBank.cs b/GameboyDotnet.Core/Memory/BuildingBlocks/IoBank.cs index 88bf73c..bdc1b93 100644 --- a/GameboyDotnet.Core/Memory/BuildingBlocks/IoBank.cs +++ b/GameboyDotnet.Core/Memory/BuildingBlocks/IoBank.cs @@ -41,45 +41,81 @@ public override void WriteByte(ref ushort address, ref byte value) } } + //Check if audio registers are in read only state (except FF26 - Power Control) + if (!_apu.IsAudioOn && address is >= 0xFF10 and < 0xFF26) + return; + + if (address >= 0xFF30 && address <= 0xFF3F) + { + _apu.WaveChannel.WriteWaveRam(ref address, ref value); + return; + } + switch (address) { case 0xFF26: - _apu.UpdatePowerState(ref value); + _apu.SetPowerState(ref value); return; - break; case 0xFF25: - _apu.UpdateChannelPanningStates(ref value); + _apu.SetChannelPanningStates(ref value); return; case 0xFF24: - _apu.UpdateVolumeControlStates(ref value); + _apu.SetVolumeControlStates(ref value); return; case 0xFF10: - //TODO: Channel 1 Sweep + _apu.SquareChannel1.SetSweepState(ref value); break; case 0xFF11: - //TODO: Channel 1 length/duty + _apu.SquareChannel1.SetLengthTimer(ref value); break; case 0xFF12: - //TODO: Channel 1 volume & envelope + _apu.SquareChannel1.SetVolumeRegister(ref value); break; case 0xFF13: - //TODO: Channel 1 period low + _apu.SquareChannel1.SetPeriodLowOrRandomnessRegister(ref value); break; case 0xFF14: - //TODO: Channel 1 period high & control + _apu.SquareChannel1.SetPeriodHighControl(ref value); break; case 0xFF16: - _apu.SquareChannel2.UpdateLengthDuty(ref value); - return; + _apu.SquareChannel2.SetLengthTimer(ref value); + break; case 0xFF17: - _apu.SquareChannel2.UpdateVolumeEnvelope(ref value); - return; + _apu.SquareChannel2.SetVolumeRegister(ref value); + break; case 0xFF18: - _apu.SquareChannel2.UpdatePeriodLow(ref value); - return; + _apu.SquareChannel2.SetPeriodLowOrRandomnessRegister(ref value); + break; case 0xFF19: - _apu.SquareChannel2.UpdatePeriodHighControl(ref value); - return; + _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; } @@ -97,6 +133,11 @@ public override byte ReadByte(ref ushort address) return _joypadRegister; } + if (address >= 0xFF30 && address <= 0xFF3F) + { + return _apu.WaveChannel.ReadWaveRam(ref address); + } + return base.ReadByte(ref address); } } \ No newline at end of file diff --git a/GameboyDotnet.Core/Sound/Apu.cs b/GameboyDotnet.Core/Sound/Apu.cs index b5fc99d..1137322 100644 --- a/GameboyDotnet.Core/Sound/Apu.cs +++ b/GameboyDotnet.Core/Sound/Apu.cs @@ -1,7 +1,5 @@ using GameboyDotnet.Extensions; -using GameboyDotnet.Memory; using GameboyDotnet.Sound.Channels; -using GameboyDotnet.Timers; namespace GameboyDotnet.Sound; @@ -10,21 +8,23 @@ public 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 bool IsAudioOn { get; private set; } public byte LeftMasterVolume { get; private set; } public byte RightMasterVolume { get; private set; } - - - private BitState _currentDividerRegisterBitState = BitState.Lo; + private int _frameSequencerCyclesTimer = 8192; private int _frameSequencerPosition = 0; - private int _maxDigitalSumOfOutputPerChannel = 15 * 4; //4 channels, 0-15 volume level each + 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(AudioBuffer); + SquareChannel2 = new SquareChannel2(); + WaveChannel = new WaveChannel(AudioBuffer); + NoiseChannel = new NoiseChannel(); } private int SampleCounter = 87; @@ -36,38 +36,57 @@ public void PushApuCycles(ref byte tCycles) StepFrameSequencer(); SquareChannel1.Step(); SquareChannel2.Step(); - //WaveChannel.Step(); - //NoiseChannel.Step(); + WaveChannel.Step(); + NoiseChannel.Step(); SampleCounter--; - if(SampleCounter > 0) + if (SampleCounter > 0) continue; - - SampleCounter = 87; + + SampleCounter = 87; if (IsAudioOn) { - int leftSum = 0; - int rightSum = 0; - - if (SquareChannel1.IsLeftSpeakerOn) leftSum += SquareChannel1.CurrentOutput; - if (SquareChannel1.IsRightSpeakerOn) rightSum += SquareChannel1.CurrentOutput; - if (SquareChannel2.IsLeftSpeakerOn) leftSum += SquareChannel2.CurrentOutput; - if (SquareChannel2.IsRightSpeakerOn) rightSum += SquareChannel2.CurrentOutput; - // if(WaveChannel.IsLeftSpeakerOn) leftSample += WaveChannel.CurrentSample; - // if(WaveChannel.IsRightSpeakerOn) rightSample += WaveChannel.CurrentSample; - // if(NoiseChannel.IsLeftSpeakerOn) leftSample += NoiseChannel.CurrentSample; - // if(NoiseChannel.IsRightSpeakerOn) rightSample += NoiseChannel.CurrentSample; - - //Normalize digital [0-15]*4 channels to [0-2f], then shift to [-1f, 1f] - float normalizedLeft = (float)leftSum / _maxDigitalSumOfOutputPerChannel *2f - 1f; - float normalizedRight = (float)rightSum / _maxDigitalSumOfOutputPerChannel *2f - 1f; - AudioBuffer.EnqueueSample(leftSum, rightSum); + MixAndPushSamples(); } } } - public void ResetFrameSequencer() + private void MixAndPushSamples() { + int leftSum = 0; + int rightSum = 0; + + if (SquareChannel1 is { IsDacEnabled: true, IsChannelOn: true, IsDebugEnabled: true }) + { + if (SquareChannel1.IsLeftSpeakerOn) leftSum += SquareChannel1.CurrentOutput; + if (SquareChannel1.IsRightSpeakerOn) rightSum += SquareChannel1.CurrentOutput; + } + + if(SquareChannel2 is { IsDacEnabled: true, IsChannelOn: true, IsDebugEnabled: true }) + { + if (SquareChannel2.IsLeftSpeakerOn) leftSum += SquareChannel2.CurrentOutput; + if (SquareChannel2.IsRightSpeakerOn) rightSum += SquareChannel2.CurrentOutput; + } + + if(WaveChannel is { IsDacEnabled: true, IsChannelOn: true, IsDebugEnabled: true }) + { + if (WaveChannel.IsLeftSpeakerOn) leftSum += WaveChannel.CurrentOutput; + if (WaveChannel.IsRightSpeakerOn) rightSum += WaveChannel.CurrentOutput; + } + + if(NoiseChannel is { IsDacEnabled: true, IsChannelOn: true, IsDebugEnabled: true }) + { + if (NoiseChannel.IsLeftSpeakerOn) leftSum += NoiseChannel.CurrentOutput; + if (NoiseChannel.IsRightSpeakerOn) rightSum += NoiseChannel.CurrentOutput; + } + + //Apply master volume panning + leftSum *= LeftMasterVolume; + rightSum *= RightMasterVolume; + //Normalize digital [0-15]*4 channels to [0-2f], then shift to [-1f, 1f] + float normalizedLeft = (float)leftSum / MaxDigitalSumOfOutputPerStereoChannel * 2f - 1f; + float normalizedRight = (float)rightSum / MaxDigitalSumOfOutputPerStereoChannel * 2f - 1f; + AudioBuffer.EnqueueSample(normalizedLeft, normalizedRight); } private void StepFrameSequencer() @@ -111,53 +130,62 @@ private void StepFrameSequencer() private void TickVolumeEnvelope() { - SquareChannel2.UpdateVolume(); + SquareChannel1.TickVolumeEnvelopeTimer(); + SquareChannel2.TickVolumeEnvelopeTimer(); + WaveChannel.TickVolumeEnvelopeTimer(); + NoiseChannel.TickVolumeEnvelopeTimer(); } private void TickSweep() { - // throw new NotImplementedException(); + SquareChannel1.TickSweep(); } private void TickLengthCounters() { - SquareChannel2.TickLengthTimer(); - } - - private bool DivFallingEdgeDetected(ref byte dividerValue) - { - var previousDividerRegisterBitState = _currentDividerRegisterBitState; - - _currentDividerRegisterBitState = - dividerValue.IsBitSet(Cycles.DivFallingEdgeDetectorBitIndex) - ? BitState.Hi - : BitState.Lo; - - return previousDividerRegisterBitState == BitState.Hi && _currentDividerRegisterBitState == BitState.Lo; + SquareChannel1.StepLengthTimer(); + SquareChannel2.StepLengthTimer(); + WaveChannel.StepLengthTimer(); + NoiseChannel.StepLengthTimer(); } - public void UpdatePowerState(ref byte value) + 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 = 8192; + _frameSequencerPosition = 0; } } - public void UpdateChannelPanningStates(ref byte value) + public void SetChannelPanningStates(ref byte value) { - //TODO: Implement other channels - SquareChannel2.IsLeftSpeakerOn = value.IsBitSet(4); - SquareChannel2.IsRightSpeakerOn = value.IsBitSet(0); + 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 UpdateVolumeControlStates(ref byte value) + public void SetVolumeControlStates(ref byte value) { //Ignores VIN input, bits 7 and 3 //Value of 0 means 'very quiet', 7 means full volume diff --git a/GameboyDotnet.Core/Sound/ApuRegisters.cs b/GameboyDotnet.Core/Sound/ApuRegisters.cs deleted file mode 100644 index 8d79128..0000000 --- a/GameboyDotnet.Core/Sound/ApuRegisters.cs +++ /dev/null @@ -1,22 +0,0 @@ -using GameboyDotnet.Memory; - -namespace GameboyDotnet.Sound; - -public class ApuRegisters(MemoryController memoryController) -{ - /// - /// Bit 7 Audio On/Off - /// - public byte NR52AudioMasterControl => memoryController.IoRegisters.MemorySpaceView[0x26]; - - /// - /// Bits 7-4 CH4-CH1 left, Bits 3-0 CH4-CH1 right - /// - public byte NR51SoundPanning => memoryController.IoRegisters.MemorySpaceView[0x25]; - - /// - /// 7 - VIN left (safe to ignore for now), (654) - left volume, 3- VIN right, (210) - Right Volume - /// Value of 0 is treated as 1 (very quiet), value of 7 is then like full 8 (no reduction) - /// - public byte NR50MasterVolume => memoryController.IoRegisters.MemorySpaceView[0x24]; -} \ No newline at end of file diff --git a/GameboyDotnet.Core/Sound/AudioBuffer.cs b/GameboyDotnet.Core/Sound/AudioBuffer.cs index 096507a..407ebfe 100644 --- a/GameboyDotnet.Core/Sound/AudioBuffer.cs +++ b/GameboyDotnet.Core/Sound/AudioBuffer.cs @@ -6,7 +6,7 @@ public class AudioBuffer { private readonly ConcurrentQueue _sampleQueue = new(); private readonly float[] _sampleBuffer = new float[BufferSize]; - private const int BufferSize = 1024 * 2; + private const int BufferSize = 1024*2; private int CurrentBufferIndex = 0; public void EnqueueSample(float leftSample, float rightSample) diff --git a/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseChannel.cs b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseChannel.cs index f178d07..f323fbf 100644 --- a/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseChannel.cs +++ b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseChannel.cs @@ -2,8 +2,12 @@ namespace GameboyDotnet.Sound.Channels.BuildingBlocks; -public abstract class BaseChannel(AudioBuffer audioBuffer) +public abstract class BaseChannel() { + //Debug + public bool IsDebugEnabled = true; + + //Shadow registers public bool IsChannelOn; public bool IsRightSpeakerOn; public bool IsLeftSpeakerOn; @@ -15,18 +19,19 @@ public abstract class BaseChannel(AudioBuffer audioBuffer) public int VolumeEnvelopeSweepPace; public bool VolumeEnvelopeDirection; public int InitialVolume; + public bool IsDacEnabled; //NRX3 - public byte PeriodLow; + public byte PeriodLowOrRandomness; //NRX4 public byte PeriodHigh; public bool IsLengthEnabled; // Bit 6 - public int GetPeriodValueFromRegisters => ((PeriodHigh & 0b111) << 8) | PeriodLow; + public int GetPeriodValueFromRegisters => ((PeriodHigh & 0b111) << 8) | PeriodLowOrRandomness; public int LengthTimer; - public int PeriodDividerTimer = 0; + public int PeriodTimer = 0; public int VolumeEnvelopeTimer = 0; protected int VolumeLevel; @@ -35,47 +40,67 @@ public abstract class BaseChannel(AudioBuffer audioBuffer) public abstract void Step(); - protected abstract void StepSampleState(); + protected abstract void RefreshOutputState(); - protected virtual bool StepPeriodDividerTimer() + public virtual void Reset() { - PeriodDividerTimer--; - if (PeriodDividerTimer > 0) + IsChannelOn = false; + IsRightSpeakerOn = false; + IsLeftSpeakerOn = false; + InitialLengthTimer = 0; + VolumeEnvelopeSweepPace = 0; + VolumeEnvelopeDirection = false; + 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; } - //TODO: Double check the math on resetting PeriodTimer's value - PeriodDividerTimer += (2048 - GetPeriodValueFromRegisters) * 4; + ResetPeriodTimer(); return true; } - - - public void TickLengthTimer() + public void StepLengthTimer() { + if (!IsChannelOn) + return; + //Check if LengthTimer is enabled - if (IsLengthEnabled) + if (IsLengthEnabled && LengthTimer > 0) { LengthTimer--; - if (LengthTimer <= 0) + if (LengthTimer == 0) { IsChannelOn = false; } } } - public void UpdateVolume() + public void TickVolumeEnvelopeTimer() { - int period = (VolumeEnvelopeSweepPace == 0) ? 8 : VolumeEnvelopeSweepPace; - + if (VolumeEnvelopeSweepPace == 0) + return; + VolumeEnvelopeTimer--; if (VolumeEnvelopeTimer <= 0) { - VolumeEnvelopeTimer = period; - + VolumeEnvelopeTimer = VolumeEnvelopeSweepPace; + if (VolumeEnvelopeDirection && VolumeLevel < 15) VolumeLevel++; else if (!VolumeEnvelopeDirection && VolumeLevel > 0) @@ -83,28 +108,34 @@ public void UpdateVolume() } } - public virtual void UpdateLengthDuty(ref byte value) + public virtual void SetLengthTimer(ref byte value) { InitialLengthTimer = value & 0b0011_1111; LengthTimer = 64 - InitialLengthTimer; } - public void UpdateVolumeEnvelope(ref byte value) + public virtual void SetVolumeRegister(ref byte value) { - InitialVolume = value & 0b1111_0000 >> 4; + InitialVolume = (value & 0b1111_0000) >> 4; VolumeEnvelopeDirection = value.IsBitSet(3); VolumeEnvelopeSweepPace = value & 0b111; + + IsDacEnabled = (InitialVolume != 0 || VolumeEnvelopeSweepPace != 0); + if (!IsDacEnabled) + { + IsChannelOn = false; + } } - public void UpdatePeriodLow(ref byte value) + public virtual void SetPeriodLowOrRandomnessRegister(ref byte value) { - PeriodLow = value; + PeriodLowOrRandomness = value; } - public void UpdatePeriodHighControl(ref byte value) + public void SetPeriodHighControl(ref byte value) { IsLengthEnabled = value.IsBitSet(6); - PeriodHigh = value; + PeriodHigh = (byte)(value & 0b111); if (value.IsBitSet(7)) { @@ -114,13 +145,22 @@ public void UpdatePeriodHighControl(ref byte value) protected virtual void Trigger() { - IsChannelOn = true; + IsChannelOn = IsDacEnabled; if (LengthTimer == 0) - LengthTimer = 64; - - PeriodDividerTimer = (2048 - GetPeriodValueFromRegisters) * 4; - VolumeEnvelopeTimer = (VolumeEnvelopeSweepPace == 0) ? 8 : VolumeEnvelopeSweepPace; + { + ResetLengthTimerValue(); + } + + ResetPeriodTimer(); + VolumeEnvelopeTimer = VolumeEnvelopeSweepPace; 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 index 587de13..12f3c63 100644 --- a/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseSquareChannel.cs +++ b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseSquareChannel.cs @@ -1,6 +1,6 @@ namespace GameboyDotnet.Sound.Channels.BuildingBlocks; -public class BaseSquareChannel(AudioBuffer audioBuffer) : BaseChannel(audioBuffer) +public abstract class BaseSquareChannel() : BaseChannel() { protected byte[][] DutyCycles = [ @@ -14,37 +14,58 @@ public class BaseSquareChannel(AudioBuffer audioBuffer) : BaseChannel(audioBuffe //NR11-NR21 public int WaveDutyIndex = 0; - + public override void Step() { - var isPeriodDividerCountdownFinished = StepPeriodDividerTimer(); + if (!IsChannelOn) + return; - if (isPeriodDividerCountdownFinished) + var isPeriodTimerFinished = StepPeriodTimer(); + + if (isPeriodTimerFinished) { DutyCycleStep = (DutyCycleStep + 1) & 0b111; //Wrap after 7 + RefreshOutputState(); } + } - StepSampleState(); + public override void Reset() + { + base.Reset(); + DutyCycleStep = 0; + WaveDutyIndex = 0; } - - protected override void StepSampleState() + + protected override void RefreshOutputState() { + if (!IsChannelOn) + return; + CurrentOutput = DutyCycles[WaveDutyIndex][DutyCycleStep] == 1 && IsChannelOn - ? VolumeLevel // Integer 0–15 + ? VolumeLevel // (Hi-state) 1 * [0–15] : 0; } - public override void UpdateLengthDuty(ref byte value) + public override void SetLengthTimer(ref byte value) { WaveDutyIndex = (value & 0b1100_0000) >> 6; - base.UpdateLengthDuty(ref value); + base.SetLengthTimer(ref value); } protected override void Trigger() { - if (!IsChannelOn) - DutyCycleStep = 0; - + DutyCycleStep = 0; base.Trigger(); } + + protected override void ResetLengthTimerValue() + { + LengthTimer = 64; + } + + protected override void ResetPeriodTimer() + { + int lowerBitsOfPeriodDividerTimer = PeriodTimer & 0b11; + PeriodTimer = ((2048 - GetPeriodValueFromRegisters) * 4 & ~0b11) | lowerBitsOfPeriodDividerTimer; + } } \ 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..b3d8505 --- /dev/null +++ b/GameboyDotnet.Core/Sound/Channels/NoiseChannel.cs @@ -0,0 +1,33 @@ +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; + } +} \ No newline at end of file diff --git a/GameboyDotnet.Core/Sound/Channels/SquareChannel1.cs b/GameboyDotnet.Core/Sound/Channels/SquareChannel1.cs index 32c198b..0bb8526 100644 --- a/GameboyDotnet.Core/Sound/Channels/SquareChannel1.cs +++ b/GameboyDotnet.Core/Sound/Channels/SquareChannel1.cs @@ -1,5 +1,60 @@ -using GameboyDotnet.Sound.Channels.BuildingBlocks; +using GameboyDotnet.Extensions; +using GameboyDotnet.Sound.Channels.BuildingBlocks; namespace GameboyDotnet.Sound.Channels; -public class SquareChannel1(AudioBuffer audioBuffer) : BaseSquareChannel(audioBuffer); \ No newline at end of file +public class SquareChannel1(AudioBuffer audioBuffer) : BaseSquareChannel() +{ + private int _sweepTimer; + private bool _isSweepEnabled; + private int _periodShadowRegister; + + private byte _currentPaceValue; + private byte _requestedPaceValue; + private bool _direction; //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 (!IsChannelOn) + return; + + //TODO: Implement Sweep + } + + public void SetSweepState(ref byte value) + { + _requestedPaceValue = (byte)((value & 0b0111_0000) >> 4); + if (_requestedPaceValue == 0) + { + _isSweepEnabled = false; + return; + } + + _direction = value.IsBitSet(3); + _individualStep = (byte)(value & 0b0000_0111); + } + + protected override void Trigger() + { + if (!IsChannelOn) + { + _periodShadowRegister = PeriodTimer; + _sweepTimer = _requestedPaceValue; + _isSweepEnabled = true; + } + + base.Trigger(); + } +} \ No newline at end of file diff --git a/GameboyDotnet.Core/Sound/Channels/SquareChannel2.cs b/GameboyDotnet.Core/Sound/Channels/SquareChannel2.cs index 26d172d..0c11d7d 100644 --- a/GameboyDotnet.Core/Sound/Channels/SquareChannel2.cs +++ b/GameboyDotnet.Core/Sound/Channels/SquareChannel2.cs @@ -1,7 +1,5 @@ -using GameboyDotnet.Extensions; -using GameboyDotnet.Memory; -using GameboyDotnet.Sound.Channels.BuildingBlocks; +using GameboyDotnet.Sound.Channels.BuildingBlocks; namespace GameboyDotnet.Sound.Channels; -public class SquareChannel2(AudioBuffer audioBuffer) : BaseSquareChannel(audioBuffer); \ No newline at end of file +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..6da55f4 --- /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() + { + //Instead of multiplying by [0f, 1f, 0.5f, 0.25f] we can use right bit shifting by (VolumeIndex - 1) + CurrentOutput = IsChannelOn && WaveVolumeIndex != 0 + ? _waveSampleBuffer[_waveFormCurrentIndex] >> (WaveVolumeIndex - 1) + : 0; + } + + protected override void ResetLengthTimerValue() + { + LengthTimer = 256; + } + + public void SetDacStatus(ref byte value) + { + //TODO: Double check + IsDacEnabled = value.IsBitSet(7); + if (IsDacEnabled) + { + LengthTimer = InitialLengthTimer; + IsChannelOn = true; + } + else + { + 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]; + } +} \ No newline at end of file diff --git a/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj b/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj index 6b442de..a1707c1 100644 --- a/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj +++ b/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj @@ -80,6 +80,9 @@ PreserveNewest + + PreserveNewest + diff --git a/GameboyDotnet.SDL/Program.cs b/GameboyDotnet.SDL/Program.cs index 9bed201..e72273d 100644 --- a/GameboyDotnet.SDL/Program.cs +++ b/GameboyDotnet.SDL/Program.cs @@ -42,6 +42,9 @@ Task.Run(() => gameboy.RunAsync(emulatorSettings.FrameLimitEnabled, cts.Token)); +var userActionText = string.Empty; +var userActionTextFrameCounter = 0; + // Main SDL loop while (running && !cts.IsCancellationRequested) { @@ -61,6 +64,26 @@ 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; } if (keyboardMapper.TryGetGameboyKey(e.key.keysym.sym, out var keyPressed)) gameboy.PressButton(keyPressed); @@ -80,7 +103,15 @@ if(gameboy.Ppu.FrameBuffer.TryDequeueFrame(out var frame)) { string bufferedFramesText = $"Speed: {gameboy.Ppu.FrameBuffer.Fps/ 60.0 * 100.0:0.0}% / {gameboy.Ppu.FrameBuffer.Fps:0} FPS"; - SdlRenderer.RenderStates(ref renderer, ref window, frame!, bufferedFramesText); + if(userActionTextFrameCounter > 0) + { + userActionTextFrameCounter--; + } + else + { + userActionText = string.Empty; + } + SdlRenderer.RenderStates(ref renderer, ref window, frame!, string.Join(" \n ", bufferedFramesText, userActionText)); } } diff --git a/GameboyDotnet.SDL/Tests/Roms/dmg_sound.gb b/GameboyDotnet.SDL/Tests/Roms/dmg_sound.gb new file mode 100644 index 0000000000000000000000000000000000000000..fe9131044031c45f404642f5dc8a9ec43ecd2065 GIT binary patch literal 65536 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 Date: Sat, 6 Sep 2025 14:13:46 +0200 Subject: [PATCH 09/11] Refactored APU functions --- .gitignore | 7 +- GameboyDotnet.Core/Sound/Apu.AudioMixer.cs | 50 +++++++++ .../Sound/Apu.FrameSequencer.cs | 64 +++++++++++ .../Sound/Apu.RegistersEvents.cs | 51 +++++++++ GameboyDotnet.Core/Sound/Apu.cs | 106 +----------------- .../Channels/BuildingBlocks/BaseChannel.cs | 20 +++- .../BuildingBlocks/BaseSquareChannel.cs | 3 +- .../Sound/Channels/NoiseChannel.cs | 6 + .../Sound/Channels/WaveChannel.cs | 6 + GameboyDotnet.Core/Sound/FrequencyFilters.cs | 23 ++++ GameboyDotnet.Core/Sound/RingBuffer.cs | 6 + GameboyDotnet.SDL/GameboyDotnet.SDL.csproj | 3 + GameboyDotnet.SDL/appsettings.json | 2 +- 13 files changed, 234 insertions(+), 113 deletions(-) create mode 100644 GameboyDotnet.Core/Sound/Apu.AudioMixer.cs create mode 100644 GameboyDotnet.Core/Sound/Apu.FrameSequencer.cs create mode 100644 GameboyDotnet.Core/Sound/Apu.RegistersEvents.cs create mode 100644 GameboyDotnet.Core/Sound/FrequencyFilters.cs create mode 100644 GameboyDotnet.Core/Sound/RingBuffer.cs 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/Sound/Apu.AudioMixer.cs b/GameboyDotnet.Core/Sound/Apu.AudioMixer.cs new file mode 100644 index 0000000..667fb41 --- /dev/null +++ b/GameboyDotnet.Core/Sound/Apu.AudioMixer.cs @@ -0,0 +1,50 @@ +using GameboyDotnet.Sound.Channels; + +namespace GameboyDotnet.Sound; + +public partial class Apu +{ + private float _capacitorLeft = 0f; + private float _capacitorRight = 0f; + private (float leftPcmSample, float rightPcmSample) MixAudioChannelsToStereoSample() + { + int leftSum = 0; + int rightSum = 0; + + if (SquareChannel1 is { IsDacEnabled: true, IsChannelOn: true, IsDebugEnabled: true }) + { + if (SquareChannel1.IsLeftSpeakerOn) leftSum += SquareChannel1.CurrentOutput; + if (SquareChannel1.IsRightSpeakerOn) rightSum += SquareChannel1.CurrentOutput; + } + + if(SquareChannel2 is { IsDacEnabled: true, IsChannelOn: true, IsDebugEnabled: true }) + { + if (SquareChannel2.IsLeftSpeakerOn) leftSum += SquareChannel2.CurrentOutput; + if (SquareChannel2.IsRightSpeakerOn) rightSum += SquareChannel2.CurrentOutput; + } + + if(WaveChannel is { IsDacEnabled: true, IsChannelOn: true, IsDebugEnabled: true }) + { + if (WaveChannel.IsLeftSpeakerOn) leftSum += WaveChannel.CurrentOutput; + if (WaveChannel.IsRightSpeakerOn) rightSum += WaveChannel.CurrentOutput; + } + + if(NoiseChannel is { IsDacEnabled: true, IsChannelOn: true, IsDebugEnabled: true }) + { + if (NoiseChannel.IsLeftSpeakerOn) leftSum += NoiseChannel.CurrentOutput; + if (NoiseChannel.IsRightSpeakerOn) rightSum += NoiseChannel.CurrentOutput; + } + + //Apply master volume panning + leftSum *= LeftMasterVolume; + rightSum *= RightMasterVolume; + + 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..0886bb8 --- /dev/null +++ b/GameboyDotnet.Core/Sound/Apu.FrameSequencer.cs @@ -0,0 +1,64 @@ +namespace GameboyDotnet.Sound; + +public partial class Apu +{ + private void StepFrameSequencer() + { + _frameSequencerCyclesTimer--; + + if (_frameSequencerCyclesTimer > 0) + return; + + _frameSequencerCyclesTimer = 8192; + + _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..e98dc73 --- /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 = 8192; + _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 index 1137322..edbe158 100644 --- a/GameboyDotnet.Core/Sound/Apu.cs +++ b/GameboyDotnet.Core/Sound/Apu.cs @@ -3,7 +3,7 @@ namespace GameboyDotnet.Sound; -public class Apu +public partial class Apu { public AudioBuffer AudioBuffer { get; init; } public SquareChannel1 SquareChannel1 { get; private set; } @@ -88,108 +88,4 @@ private void MixAndPushSamples() float normalizedRight = (float)rightSum / MaxDigitalSumOfOutputPerStereoChannel * 2f - 1f; AudioBuffer.EnqueueSample(normalizedLeft, normalizedRight); } - - private void StepFrameSequencer() - { - _frameSequencerCyclesTimer--; - - if (_frameSequencerCyclesTimer > 0) - return; - - _frameSequencerCyclesTimer = 8192; - - _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(); - } - - 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 = 8192; - _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/Channels/BuildingBlocks/BaseChannel.cs b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseChannel.cs index f323fbf..cbc0ae8 100644 --- a/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseChannel.cs +++ b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseChannel.cs @@ -108,11 +108,7 @@ public void TickVolumeEnvelopeTimer() } } - public virtual void SetLengthTimer(ref byte value) - { - InitialLengthTimer = value & 0b0011_1111; - LengthTimer = 64 - InitialLengthTimer; - } + public abstract void SetLengthTimer(ref byte value); public virtual void SetVolumeRegister(ref byte value) { @@ -134,9 +130,19 @@ public virtual void SetPeriodLowOrRandomnessRegister(ref byte 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(); @@ -150,6 +156,10 @@ protected virtual void Trigger() if (LengthTimer == 0) { ResetLengthTimerValue(); + // TODO: if (IsLengthEnabled && FrameSequencerWillNotClockLengthThisStep()) + // { + // LengthTimer--; + // } } ResetPeriodTimer(); diff --git a/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseSquareChannel.cs b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseSquareChannel.cs index 12f3c63..e82f18a 100644 --- a/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseSquareChannel.cs +++ b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseSquareChannel.cs @@ -49,7 +49,8 @@ protected override void RefreshOutputState() public override void SetLengthTimer(ref byte value) { WaveDutyIndex = (value & 0b1100_0000) >> 6; - base.SetLengthTimer(ref value); + InitialLengthTimer = value & 0b0011_1111; + LengthTimer = 64 - InitialLengthTimer; } protected override void Trigger() diff --git a/GameboyDotnet.Core/Sound/Channels/NoiseChannel.cs b/GameboyDotnet.Core/Sound/Channels/NoiseChannel.cs index b3d8505..9c00fb3 100644 --- a/GameboyDotnet.Core/Sound/Channels/NoiseChannel.cs +++ b/GameboyDotnet.Core/Sound/Channels/NoiseChannel.cs @@ -30,4 +30,10 @@ 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/WaveChannel.cs b/GameboyDotnet.Core/Sound/Channels/WaveChannel.cs index 6da55f4..0c6603a 100644 --- a/GameboyDotnet.Core/Sound/Channels/WaveChannel.cs +++ b/GameboyDotnet.Core/Sound/Channels/WaveChannel.cs @@ -84,4 +84,10 @@ 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..09fa81a --- /dev/null +++ b/GameboyDotnet.Core/Sound/FrequencyFilters.cs @@ -0,0 +1,23 @@ +namespace GameboyDotnet.Sound; + +public static class FrequencyFilters +{ + /// + /// Debug only + /// + /// + public static bool IsHighPassFilterActive = true; + + public static float HighPassFilter(this float pcmSample, ref float capacitor) + { + if (!IsHighPassFilterActive) + { + return pcmSample; + } + + float output = pcmSample - capacitor; + // capacitor slowly charges to 'in' via their difference + capacitor = pcmSample - output * 0.995948f; + return output; + } +} \ 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.SDL/GameboyDotnet.SDL.csproj b/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj index a1707c1..f68be54 100644 --- a/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj +++ b/GameboyDotnet.SDL/GameboyDotnet.SDL.csproj @@ -83,6 +83,9 @@ PreserveNewest + + PreserveNewest + diff --git a/GameboyDotnet.SDL/appsettings.json b/GameboyDotnet.SDL/appsettings.json index f8d2672..867960b 100644 --- a/GameboyDotnet.SDL/appsettings.json +++ b/GameboyDotnet.SDL/appsettings.json @@ -4,7 +4,7 @@ "WindowWidth": 1200, "WindowHeight": 700, "FrameLimitEnabled": true, - "RomPath": "Tests/Roms/Super Mario Land 2.gb", + "RomPath": "Tests/Roms/Pokemon Red.gb", "Keymap": { "SDLK_UP": "Up", "SDLK_LEFT": "Left", From 16c1eddcb7fb24b7524e7296d0606a7e7f625004 Mon Sep 17 00:00:00 2001 From: Szymon Sandura Date: Sun, 7 Sep 2025 15:26:22 +0200 Subject: [PATCH 10/11] Added frequency sweep and some obscure sound behaviors; Fixed Dac condition; --- GameboyDotnet.Core/Sound/Apu.AudioMixer.cs | 38 ++++----- .../Sound/Apu.FrameSequencer.cs | 7 +- .../Sound/Apu.RegistersEvents.cs | 2 +- GameboyDotnet.Core/Sound/Apu.cs | 46 ++--------- .../Channels/BuildingBlocks/BaseChannel.cs | 41 ++++++---- .../BuildingBlocks/BaseSquareChannel.cs | 5 ++ .../BuildingBlocks/EnvelopeDirection.cs | 7 ++ .../Sound/Channels/SquareChannel1.cs | 78 +++++++++++++++++-- .../Sound/Channels/WaveChannel.cs | 10 +-- GameboyDotnet.Core/Sound/FrequencyFilters.cs | 20 +++-- GameboyDotnet.SDL/Program.cs | 6 ++ GameboyDotnet.SDL/appsettings.json | 2 +- 12 files changed, 157 insertions(+), 105 deletions(-) create mode 100644 GameboyDotnet.Core/Sound/Channels/BuildingBlocks/EnvelopeDirection.cs diff --git a/GameboyDotnet.Core/Sound/Apu.AudioMixer.cs b/GameboyDotnet.Core/Sound/Apu.AudioMixer.cs index 667fb41..cbf79bb 100644 --- a/GameboyDotnet.Core/Sound/Apu.AudioMixer.cs +++ b/GameboyDotnet.Core/Sound/Apu.AudioMixer.cs @@ -1,43 +1,33 @@ using GameboyDotnet.Sound.Channels; +using GameboyDotnet.Sound.Channels.BuildingBlocks; namespace GameboyDotnet.Sound; public partial class Apu { - private float _capacitorLeft = 0f; - private float _capacitorRight = 0f; - private (float leftPcmSample, float rightPcmSample) MixAudioChannelsToStereoSample() + private (float leftPcmSample, float rightPcmSample) MixAudioChannelsToStereoSamples() { int leftSum = 0; int rightSum = 0; + bool isAnyDacEnabled = false; - if (SquareChannel1 is { IsDacEnabled: true, IsChannelOn: true, IsDebugEnabled: true }) + foreach (var channel in AvailableChannels) { - if (SquareChannel1.IsLeftSpeakerOn) leftSum += SquareChannel1.CurrentOutput; - if (SquareChannel1.IsRightSpeakerOn) rightSum += SquareChannel1.CurrentOutput; - } - - if(SquareChannel2 is { IsDacEnabled: true, IsChannelOn: true, IsDebugEnabled: true }) - { - if (SquareChannel2.IsLeftSpeakerOn) leftSum += SquareChannel2.CurrentOutput; - if (SquareChannel2.IsRightSpeakerOn) rightSum += SquareChannel2.CurrentOutput; - } - - if(WaveChannel is { IsDacEnabled: true, IsChannelOn: true, IsDebugEnabled: true }) - { - if (WaveChannel.IsLeftSpeakerOn) leftSum += WaveChannel.CurrentOutput; - if (WaveChannel.IsRightSpeakerOn) rightSum += WaveChannel.CurrentOutput; - } - - if(NoiseChannel is { IsDacEnabled: true, IsChannelOn: true, IsDebugEnabled: true }) - { - if (NoiseChannel.IsLeftSpeakerOn) leftSum += NoiseChannel.CurrentOutput; - if (NoiseChannel.IsRightSpeakerOn) rightSum += NoiseChannel.CurrentOutput; + 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)); } diff --git a/GameboyDotnet.Core/Sound/Apu.FrameSequencer.cs b/GameboyDotnet.Core/Sound/Apu.FrameSequencer.cs index 0886bb8..10a65d9 100644 --- a/GameboyDotnet.Core/Sound/Apu.FrameSequencer.cs +++ b/GameboyDotnet.Core/Sound/Apu.FrameSequencer.cs @@ -2,6 +2,11 @@ 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--; @@ -9,7 +14,7 @@ private void StepFrameSequencer() if (_frameSequencerCyclesTimer > 0) return; - _frameSequencerCyclesTimer = 8192; + _frameSequencerCyclesTimer = FrameSequencerCyclesPerFrame; _frameSequencerPosition = (_frameSequencerPosition + 1) & 0b111; //Wrap to 7 diff --git a/GameboyDotnet.Core/Sound/Apu.RegistersEvents.cs b/GameboyDotnet.Core/Sound/Apu.RegistersEvents.cs index e98dc73..142d522 100644 --- a/GameboyDotnet.Core/Sound/Apu.RegistersEvents.cs +++ b/GameboyDotnet.Core/Sound/Apu.RegistersEvents.cs @@ -21,7 +21,7 @@ public void SetPowerState(ref byte value) else if (!IsAudioOn && value.IsBitSet(7)) { IsAudioOn = true; - _frameSequencerCyclesTimer = 8192; + _frameSequencerCyclesTimer = FrameSequencerCyclesPerFrame; _frameSequencerPosition = 0; } } diff --git a/GameboyDotnet.Core/Sound/Apu.cs b/GameboyDotnet.Core/Sound/Apu.cs index edbe158..accf13d 100644 --- a/GameboyDotnet.Core/Sound/Apu.cs +++ b/GameboyDotnet.Core/Sound/Apu.cs @@ -1,5 +1,6 @@ using GameboyDotnet.Extensions; using GameboyDotnet.Sound.Channels; +using GameboyDotnet.Sound.Channels.BuildingBlocks; namespace GameboyDotnet.Sound; @@ -10,12 +11,11 @@ public partial class Apu 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 int _frameSequencerCyclesTimer = 8192; - private int _frameSequencerPosition = 0; private const int MaxDigitalSumOfOutputPerStereoChannel = 15 * 4 * 7; //4 channels, 0-15 volume level each, 0-7 Left/Right Master volume level public Apu() @@ -25,6 +25,7 @@ public Apu() SquareChannel2 = new SquareChannel2(); WaveChannel = new WaveChannel(AudioBuffer); NoiseChannel = new NoiseChannel(); + AvailableChannels = [SquareChannel1, SquareChannel2, WaveChannel, NoiseChannel]; } private int SampleCounter = 87; @@ -46,46 +47,9 @@ public void PushApuCycles(ref byte tCycles) SampleCounter = 87; if (IsAudioOn) { - MixAndPushSamples(); + var (leftSample, rightSample) = MixAudioChannelsToStereoSamples(); + AudioBuffer.EnqueueSample(leftSample, rightSample); } } } - - private void MixAndPushSamples() - { - int leftSum = 0; - int rightSum = 0; - - if (SquareChannel1 is { IsDacEnabled: true, IsChannelOn: true, IsDebugEnabled: true }) - { - if (SquareChannel1.IsLeftSpeakerOn) leftSum += SquareChannel1.CurrentOutput; - if (SquareChannel1.IsRightSpeakerOn) rightSum += SquareChannel1.CurrentOutput; - } - - if(SquareChannel2 is { IsDacEnabled: true, IsChannelOn: true, IsDebugEnabled: true }) - { - if (SquareChannel2.IsLeftSpeakerOn) leftSum += SquareChannel2.CurrentOutput; - if (SquareChannel2.IsRightSpeakerOn) rightSum += SquareChannel2.CurrentOutput; - } - - if(WaveChannel is { IsDacEnabled: true, IsChannelOn: true, IsDebugEnabled: true }) - { - if (WaveChannel.IsLeftSpeakerOn) leftSum += WaveChannel.CurrentOutput; - if (WaveChannel.IsRightSpeakerOn) rightSum += WaveChannel.CurrentOutput; - } - - if(NoiseChannel is { IsDacEnabled: true, IsChannelOn: true, IsDebugEnabled: true }) - { - if (NoiseChannel.IsLeftSpeakerOn) leftSum += NoiseChannel.CurrentOutput; - if (NoiseChannel.IsRightSpeakerOn) rightSum += NoiseChannel.CurrentOutput; - } - - //Apply master volume panning - leftSum *= LeftMasterVolume; - rightSum *= RightMasterVolume; - //Normalize digital [0-15]*4 channels to [0-2f], then shift to [-1f, 1f] - float normalizedLeft = (float)leftSum / MaxDigitalSumOfOutputPerStereoChannel * 2f - 1f; - float normalizedRight = (float)rightSum / MaxDigitalSumOfOutputPerStereoChannel * 2f - 1f; - AudioBuffer.EnqueueSample(normalizedLeft, normalizedRight); - } } \ No newline at end of file diff --git a/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseChannel.cs b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseChannel.cs index cbc0ae8..1beca6c 100644 --- a/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseChannel.cs +++ b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseChannel.cs @@ -16,8 +16,8 @@ public abstract class BaseChannel() public int InitialLengthTimer; //NRX2 - public int VolumeEnvelopeSweepPace; - public bool VolumeEnvelopeDirection; + public int VolumeEnvelopePace; //also referred to as: Volume Envelop Period + public EnvelopeDirection VolumeEnvelopeDirection; public int InitialVolume; public bool IsDacEnabled; @@ -48,8 +48,8 @@ public virtual void Reset() IsRightSpeakerOn = false; IsLeftSpeakerOn = false; InitialLengthTimer = 0; - VolumeEnvelopeSweepPace = 0; - VolumeEnvelopeDirection = false; + VolumeEnvelopePace = 0; + VolumeEnvelopeDirection = EnvelopeDirection.Descending; InitialVolume = 0; IsDacEnabled = false; PeriodLowOrRandomness = 0; @@ -69,7 +69,15 @@ protected virtual bool StepPeriodTimer() return false; } - ResetPeriodTimer(); + // Use preserving version for non-trigger reloads in square channels + if (this is BaseSquareChannel squareChannel) + { + squareChannel.ResetPeriodTimerPreserveLowerBits(); + } + else + { + ResetPeriodTimer(); + } return true; } @@ -92,18 +100,19 @@ public void StepLengthTimer() public void TickVolumeEnvelopeTimer() { - if (VolumeEnvelopeSweepPace == 0) - return; + //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 = VolumeEnvelopeSweepPace; + VolumeEnvelopeTimer = effectiveVolumeEnvelopePace; - if (VolumeEnvelopeDirection && VolumeLevel < 15) + if (VolumeEnvelopeDirection is EnvelopeDirection.Ascending && VolumeLevel < 15) VolumeLevel++; - else if (!VolumeEnvelopeDirection && VolumeLevel > 0) + else if (VolumeEnvelopeDirection is EnvelopeDirection.Descending && VolumeLevel > 0) VolumeLevel--; } } @@ -113,10 +122,11 @@ public void TickVolumeEnvelopeTimer() public virtual void SetVolumeRegister(ref byte value) { InitialVolume = (value & 0b1111_0000) >> 4; - VolumeEnvelopeDirection = value.IsBitSet(3); - VolumeEnvelopeSweepPace = value & 0b111; - - IsDacEnabled = (InitialVolume != 0 || VolumeEnvelopeSweepPace != 0); + 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; @@ -163,7 +173,8 @@ protected virtual void Trigger() } ResetPeriodTimer(); - VolumeEnvelopeTimer = VolumeEnvelopeSweepPace; + int effectiveVolumeEnvelopePace = VolumeEnvelopePace == 0 ? 8 : VolumeEnvelopePace; + VolumeEnvelopeTimer = effectiveVolumeEnvelopePace; VolumeLevel = InitialVolume; } diff --git a/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseSquareChannel.cs b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseSquareChannel.cs index e82f18a..ed18478 100644 --- a/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseSquareChannel.cs +++ b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseSquareChannel.cs @@ -65,6 +65,11 @@ protected override void ResetLengthTimerValue() } protected override void ResetPeriodTimer() + { + PeriodTimer = (2048 - GetPeriodValueFromRegisters) * 4; + } + + public void ResetPeriodTimerPreserveLowerBits() { int lowerBitsOfPeriodDividerTimer = PeriodTimer & 0b11; PeriodTimer = ((2048 - GetPeriodValueFromRegisters) * 4 & ~0b11) | lowerBitsOfPeriodDividerTimer; 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/SquareChannel1.cs b/GameboyDotnet.Core/Sound/Channels/SquareChannel1.cs index 0bb8526..45fb60c 100644 --- a/GameboyDotnet.Core/Sound/Channels/SquareChannel1.cs +++ b/GameboyDotnet.Core/Sound/Channels/SquareChannel1.cs @@ -11,7 +11,7 @@ public class SquareChannel1(AudioBuffer audioBuffer) : BaseSquareChannel() private byte _currentPaceValue; private byte _requestedPaceValue; - private bool _direction; //True = Addition, False = Subtraction + private EnvelopeDirection _frequencyEnvelopeDirection; //True = Addition, False = Subtraction private byte _individualStep; @@ -27,12 +27,46 @@ public override void Reset() public void TickSweep() { - if (!IsChannelOn) + 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); @@ -42,19 +76,47 @@ public void SetSweepState(ref byte value) return; } - _direction = value.IsBitSet(3); + _frequencyEnvelopeDirection = value.IsBitSet(3) ? EnvelopeDirection.Descending : EnvelopeDirection.Ascending; _individualStep = (byte)(value & 0b0000_0111); } protected override void Trigger() { - if (!IsChannelOn) + // 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) { - _periodShadowRegister = PeriodTimer; - _sweepTimer = _requestedPaceValue; - _isSweepEnabled = true; + int newFrequency = CalculatePeriodAfterSweep(); + if (newFrequency > 2047) + { + IsChannelOn = false; + } } base.Trigger(); } + + private int CalculatePeriodAfterSweep() + { + int periodSweep = _periodShadowRegister >> _individualStep; + + if (_frequencyEnvelopeDirection is EnvelopeDirection.Ascending) + { + return _periodShadowRegister + periodSweep; + } + else + { + return _periodShadowRegister - periodSweep; + } + } } \ No newline at end of file diff --git a/GameboyDotnet.Core/Sound/Channels/WaveChannel.cs b/GameboyDotnet.Core/Sound/Channels/WaveChannel.cs index 0c6603a..f6288e7 100644 --- a/GameboyDotnet.Core/Sound/Channels/WaveChannel.cs +++ b/GameboyDotnet.Core/Sound/Channels/WaveChannel.cs @@ -26,7 +26,7 @@ public override void Step() protected override void RefreshOutputState() { - //Instead of multiplying by [0f, 1f, 0.5f, 0.25f] we can use right bit shifting by (VolumeIndex - 1) + //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; @@ -39,14 +39,8 @@ protected override void ResetLengthTimerValue() public void SetDacStatus(ref byte value) { - //TODO: Double check IsDacEnabled = value.IsBitSet(7); - if (IsDacEnabled) - { - LengthTimer = InitialLengthTimer; - IsChannelOn = true; - } - else + if (!IsDacEnabled) { IsChannelOn = false; } diff --git a/GameboyDotnet.Core/Sound/FrequencyFilters.cs b/GameboyDotnet.Core/Sound/FrequencyFilters.cs index 09fa81a..20492f1 100644 --- a/GameboyDotnet.Core/Sound/FrequencyFilters.cs +++ b/GameboyDotnet.Core/Sound/FrequencyFilters.cs @@ -8,16 +8,24 @@ public static class FrequencyFilters /// public static bool IsHighPassFilterActive = true; - public static float HighPassFilter(this float pcmSample, ref float capacitor) + 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 pcmSample; + return; } - float output = pcmSample - capacitor; - // capacitor slowly charges to 'in' via their difference - capacitor = pcmSample - output * 0.995948f; - return output; + 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.SDL/Program.cs b/GameboyDotnet.SDL/Program.cs index e72273d..13aee1c 100644 --- a/GameboyDotnet.SDL/Program.cs +++ b/GameboyDotnet.SDL/Program.cs @@ -4,6 +4,7 @@ using GameboyDotnet.Graphics; using GameboyDotnet.SDL; using GameboyDotnet.SDL.SaveStates; +using GameboyDotnet.Sound; using Microsoft.Extensions.Configuration; using static SDL2.SDL; @@ -84,6 +85,11 @@ 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); diff --git a/GameboyDotnet.SDL/appsettings.json b/GameboyDotnet.SDL/appsettings.json index 867960b..f8d2672 100644 --- a/GameboyDotnet.SDL/appsettings.json +++ b/GameboyDotnet.SDL/appsettings.json @@ -4,7 +4,7 @@ "WindowWidth": 1200, "WindowHeight": 700, "FrameLimitEnabled": true, - "RomPath": "Tests/Roms/Pokemon Red.gb", + "RomPath": "Tests/Roms/Super Mario Land 2.gb", "Keymap": { "SDLK_UP": "Up", "SDLK_LEFT": "Left", From f4c7c79e8d9f53e37397095516ee97fe29c5c73c Mon Sep 17 00:00:00 2001 From: Szymon Sandura Date: Sun, 7 Sep 2025 15:38:40 +0200 Subject: [PATCH 11/11] Fixed condition of two lower period bits preserving in Square Channels --- .../Channels/BuildingBlocks/BaseChannel.cs | 11 +---------- .../BuildingBlocks/BaseSquareChannel.cs | 13 +++++++++++++ .../Sound/Channels/SquareChannel1.cs | 19 +++++++++---------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseChannel.cs b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseChannel.cs index 1beca6c..18feabf 100644 --- a/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseChannel.cs +++ b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseChannel.cs @@ -69,16 +69,7 @@ protected virtual bool StepPeriodTimer() return false; } - // Use preserving version for non-trigger reloads in square channels - if (this is BaseSquareChannel squareChannel) - { - squareChannel.ResetPeriodTimerPreserveLowerBits(); - } - else - { - ResetPeriodTimer(); - } - + ResetPeriodTimer(); return true; } diff --git a/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseSquareChannel.cs b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseSquareChannel.cs index ed18478..926d32c 100644 --- a/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseSquareChannel.cs +++ b/GameboyDotnet.Core/Sound/Channels/BuildingBlocks/BaseSquareChannel.cs @@ -29,6 +29,19 @@ public override void Step() } } + protected override bool StepPeriodTimer() + { + PeriodTimer--; + if (PeriodTimer > 0) + { + return false; + } + + ResetPeriodTimerPreserveLowerBits(); + + return true; + } + public override void Reset() { base.Reset(); diff --git a/GameboyDotnet.Core/Sound/Channels/SquareChannel1.cs b/GameboyDotnet.Core/Sound/Channels/SquareChannel1.cs index 45fb60c..09825e6 100644 --- a/GameboyDotnet.Core/Sound/Channels/SquareChannel1.cs +++ b/GameboyDotnet.Core/Sound/Channels/SquareChannel1.cs @@ -13,8 +13,7 @@ public class SquareChannel1(AudioBuffer audioBuffer) : BaseSquareChannel() private byte _requestedPaceValue; private EnvelopeDirection _frequencyEnvelopeDirection; //True = Addition, False = Subtraction private byte _individualStep; - - + public override void Reset() { base.Reset(); @@ -108,15 +107,15 @@ protected override void Trigger() private int CalculatePeriodAfterSweep() { - int periodSweep = _periodShadowRegister >> _individualStep; - - if (_frequencyEnvelopeDirection is EnvelopeDirection.Ascending) + if (!FrequencyFilters.IsHighPassFilterActive) { - return _periodShadowRegister + periodSweep; - } - else - { - return _periodShadowRegister - periodSweep; + return _periodShadowRegister; } + + int periodSweep = _periodShadowRegister >> _individualStep; + + return _frequencyEnvelopeDirection is EnvelopeDirection.Ascending + ? _periodShadowRegister + periodSweep + : _periodShadowRegister - periodSweep; } } \ No newline at end of file