├── .gitignore ├── .idea └── .idea.GB.Net6 │ └── .idea │ ├── .gitignore │ ├── .name │ ├── encodings.xml │ ├── indexLayout.xml │ └── vcs.xml ├── GB.Console ├── GB.Console.csproj └── Program.cs ├── GB.Core ├── Controller │ ├── Button.cs │ ├── IButtonListener.cs │ ├── IController.cs │ ├── JoyPadButtonListener.cs │ ├── Joypad.cs │ └── NullController.cs ├── Cpu │ ├── CpuRegisters.cs │ ├── CpuState.cs │ ├── Flags.cs │ ├── InstructionSet │ │ ├── AluFunctions.cs │ │ ├── DataType.cs │ │ ├── InstructionBuilder.cs │ │ ├── OpCode.cs │ │ ├── Operand.cs │ │ └── Operation.cs │ ├── InterruptManager.cs │ ├── Processor.cs │ └── SpeedMode.cs ├── GB.Core.csproj ├── Gameboy.cs ├── Graphics │ ├── ColorPalette.cs │ ├── ColorPixelFifo.cs │ ├── CorruptionType.cs │ ├── DmgPixelFifo.cs │ ├── Fetcher.cs │ ├── Gpu.cs │ ├── GpuRegister.cs │ ├── IDisplay.cs │ ├── IPixelFifo.cs │ ├── IntQueue.cs │ ├── Lcdc.cs │ ├── NullDisplay.cs │ ├── Phase │ │ ├── HBlankPhase.cs │ │ ├── IGpuPhase.cs │ │ ├── OamSearch.cs │ │ ├── PixelTransfer.cs │ │ └── VBlankPhase.cs │ ├── SpriteBug.cs │ └── TileAttributes.cs ├── Gui │ ├── Emulator.cs │ ├── GameBoyMode.cs │ └── IRunnable.cs ├── IAddressSpace.cs ├── Memory │ ├── BootRom.cs │ ├── BootRomType.cs │ ├── Cartridge │ │ ├── Battery │ │ │ ├── FileBattery.cs │ │ │ ├── IBattery.cs │ │ │ └── NullBattery.cs │ │ ├── Cartridge.cs │ │ ├── CartridgeType.cs │ │ ├── CartridgeTypeExtensions.cs │ │ ├── GameboyType.cs │ │ ├── RTC │ │ │ ├── Clock.cs │ │ │ ├── IClock.cs │ │ │ ├── RealTimeClock.cs │ │ │ └── SystemClock.cs │ │ └── Type │ │ │ ├── Mbc1.cs │ │ │ ├── Mbc2.cs │ │ │ ├── Mbc3.cs │ │ │ ├── Mbc5.cs │ │ │ └── Rom.cs │ ├── Dma.cs │ ├── DmaAddressSpace.cs │ ├── GameboyColorRam.cs │ ├── Hdma.cs │ ├── IRegister.cs │ ├── MemoryRegisters.cs │ ├── Mmu.cs │ ├── Ram.cs │ ├── RegisterType.cs │ ├── ShadowAddressSpace.cs │ ├── UndocumentedGbcRegisters.cs │ └── VoidAddressSpace.cs ├── Serial │ ├── ISerialEndpoint.cs │ ├── NullSerialEndpoint.cs │ └── SerialPort.cs ├── Sound │ ├── FrequencySweep.cs │ ├── ISoundOutput.cs │ ├── LengthCounter.cs │ ├── Lfsr.cs │ ├── NullSoundOutput.cs │ ├── PolynomialCounter.cs │ ├── Sound.cs │ ├── SoundMode1.cs │ ├── SoundMode2.cs │ ├── SoundMode3.cs │ ├── SoundMode4.cs │ ├── SoundModeBase.cs │ └── VolumeEnvelope.cs ├── Timer.cs └── Utilities.cs ├── GB.Net.sln ├── GB.WASM ├── GB.WASM.csproj ├── Interop.cs ├── Program.cs ├── Properties │ ├── AssemblyInfo.cs │ └── launchSettings.json ├── Resources │ ├── Super Mario Land (World).gb │ └── Super Mario Land (World).sav ├── TestResources.Designer.cs ├── TestResources.resx ├── WebDisplay.cs ├── WebGame.cs ├── launch.bat └── wwwroot │ ├── .nojekyll │ ├── coi-serviceworker.min.js │ ├── index.html │ └── main.js ├── GB.WinForms ├── GB.WinForms.csproj ├── MainForm.Designer.cs ├── MainForm.cs ├── MainForm.resx ├── OsSpecific │ ├── BitmapDisplay.cs │ └── SoundOutput.cs └── Program.cs ├── LICENSE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Build Folders (you can keep bin if you'd like, to store dlls and pdbs) 2 | [Bb]in/ 3 | [Oo]bj/ 4 | wwwroot/[Ll]ib/ 5 | 6 | # Logs 7 | Logs/ 8 | 9 | # Generated files 10 | project.lock.json 11 | .vs/ 12 | 13 | # mstest test results 14 | TestResults 15 | 16 | ## Ignore Visual Studio temporary files, build results, and 17 | ## files generated by popular Visual Studio add-ons. 18 | 19 | # User-specific files 20 | *.suo 21 | *.user 22 | *.sln.docstates 23 | 24 | # Build results 25 | [Dd]ebug/ 26 | [Rr]elease/ 27 | x64/ 28 | *_i.c 29 | *_p.c 30 | *.ilk 31 | *.meta 32 | *.obj 33 | *.pch 34 | *.pdb 35 | *.pgc 36 | *.pgd 37 | *.rsp 38 | *.sbr 39 | *.tlb 40 | *.tli 41 | *.tlh 42 | *.tmp 43 | *.log 44 | *.vspscc 45 | *.vssscc 46 | .builds 47 | 48 | # Visual C++ cache files 49 | ipch/ 50 | *.aps 51 | *.ncb 52 | *.opensdf 53 | *.sdf 54 | 55 | # Visual Studio profiler 56 | *.psess 57 | *.vsp 58 | *.vspx 59 | 60 | # Guidance Automation Toolkit 61 | *.gpState 62 | 63 | # ReSharper is a .NET coding add-in 64 | _ReSharper* 65 | 66 | # NCrunch 67 | *.ncrunch* 68 | .*crunch*.local.xml 69 | 70 | # Installshield output folder 71 | [Ee]xpress 72 | 73 | # DocProject is a documentation generator add-in 74 | DocProject/buildhelp/ 75 | DocProject/Help/*.HxT 76 | DocProject/Help/*.HxC 77 | DocProject/Help/*.hhc 78 | DocProject/Help/*.hhk 79 | DocProject/Help/*.hhp 80 | DocProject/Help/Html2 81 | DocProject/Help/html 82 | 83 | # Click-Once directory 84 | publish 85 | 86 | # Publish Web Output 87 | *.Publish.xml 88 | 89 | # NuGet Packages Directory 90 | packages 91 | 92 | # Windows Azure Build Output 93 | csx 94 | *.build.csdef 95 | 96 | # Windows Store app package directory 97 | AppPackages/ 98 | 99 | # Others 100 | [Bb]in 101 | [Oo]bj 102 | sql 103 | TestResults 104 | [Tt]est[Rr]esult* 105 | *.Cache 106 | ClientBin 107 | [Ss]tyle[Cc]op.* 108 | ~$* 109 | *.dbmdl 110 | Generated_Code #added for RIA/Silverlight projects 111 | 112 | # Backup & report files from converting an old project file to a newer 113 | # Visual Studio version. Backup files are not needed, because we have git ;-) 114 | _UpgradeReport_Files/ 115 | Backup*/ 116 | UpgradeLog*.XML 117 | src/.vs/config/applicationhost.config 118 | 119 | src/OE_Tenant.Web.Host/wwwroot/Temp/Downloads 120 | src/OE_Tenant.Web.Mvc/wwwroot/Temp/Downloads 121 | src/OE_Tenant.Web.Mvc/Properties/PublishProfiles 122 | src/OE_Tenant.Web.Mvc/node_modules 123 | src/OE_Tenant.Web.Mvc/wwwroot/lib 124 | src/OE_Tenant.Web.Mvc/wwwroot/dist 125 | src/OE_Tenant.Web.Mvc/wwwroot/view-resources/Views/_Bundles 126 | src/OE_Tenant.Web.Mvc/wwwroot/view-resources/Areas/Admin/Views/_Bundles 127 | src/OE_Tenant.Web.Mvc/wwwroot/view-resources/**/*.min.* 128 | src/OE_Tenant.Web.Mvc/wwwroot/Common/**/*.min.* 129 | src/OE_Tenant.Web.Mvc/wwwroot/metronic/**/*.min.* 130 | src/OE_Tenant.Web.Mvc/package-lock.json 131 | 132 | src/OE_Tenant.Web.Public/wwwroot/lib 133 | src/OE_Tenant.Web.Public/wwwroot/dist 134 | src/OE_Tenant.Web.Public/wwwroot/Common/_Bundles 135 | src/OE_Tenant.Web.Public/wwwroot/view-resources/Views/_Bundles 136 | src/OE_Tenant.Web.Public/wwwroot/view-resources/**/*.min.* 137 | src/OE_Tenant.Web.Public/wwwroot/Common/**/*.min.* 138 | src/OE_Tenant.Web.Public/node_modules 139 | /.idea/.idea.Cbims.All 140 | /Cbims.App/Admin/wwwroot/lib 141 | -------------------------------------------------------------------------------- /.idea/.idea.GB.Net6/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Rider ignored files 5 | /contentModel.xml 6 | /projectSettingsUpdater.xml 7 | /.idea.GB.Net6.iml 8 | /modules.xml 9 | # Editor-based HTTP Client requests 10 | /httpRequests/ 11 | # Datasource local storage ignored files 12 | /dataSources/ 13 | /dataSources.local.xml 14 | -------------------------------------------------------------------------------- /.idea/.idea.GB.Net6/.idea/.name: -------------------------------------------------------------------------------- 1 | GB.Net6 -------------------------------------------------------------------------------- /.idea/.idea.GB.Net6/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/.idea.GB.Net6/.idea/indexLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/.idea.GB.Net6/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /GB.Console/GB.Console.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0-windows 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /GB.Console/Program.cs: -------------------------------------------------------------------------------- 1 | using GB.Core; 2 | using GB.Core.Controller; 3 | using GB.Core.Graphics; 4 | using GB.Core.Memory.Cartridge; 5 | using GB.Core.Serial; 6 | using GB.Core.Sound; 7 | 8 | var cartridge = Cartridge.FromFile(@"C:\temp\Super Mario Land (World).gb"); 9 | if (cartridge == null) 10 | { 11 | return; 12 | } 13 | 14 | var gb = new Gameboy(cartridge, new NullDisplay(), new NullController(), new NullSoundOutput(), new NullSerialEndpoint()); 15 | gb.Run(CancellationToken.None); 16 | -------------------------------------------------------------------------------- /GB.Core/Controller/Button.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Controller 2 | { 3 | public sealed class Button 4 | { 5 | public static Button Right = new Button(0x01, 0x10); 6 | public static Button Left = new Button(0x02, 0x10); 7 | public static Button Up = new Button(0x04, 0x10); 8 | public static Button Down = new Button(0x08, 0x10); 9 | public static Button A = new Button(0x01, 0x20); 10 | public static Button B = new Button(0x02, 0x20); 11 | public static Button Select = new Button(0x04, 0x20); 12 | public static Button Start = new Button(0x08, 0x20); 13 | 14 | public int Mask { get; } 15 | public int Line { get; } 16 | 17 | public Button(int mask, int line) 18 | { 19 | Mask = mask; 20 | Line = line; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /GB.Core/Controller/IButtonListener.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Controller 2 | { 3 | public interface IButtonListener 4 | { 5 | void OnButtonPress(Button button); 6 | void OnButtonRelease(Button button); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /GB.Core/Controller/IController.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Controller 2 | { 3 | public interface IController 4 | { 5 | void SetButtonListener(IButtonListener listener); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /GB.Core/Controller/JoyPadButtonListener.cs: -------------------------------------------------------------------------------- 1 | using GB.Core.Cpu; 2 | using System.Collections.Concurrent; 3 | 4 | namespace GB.Core.Controller 5 | { 6 | internal sealed class JoyPadButtonListener : IButtonListener 7 | { 8 | private readonly InterruptManager _interruptManager; 9 | private readonly ConcurrentDictionary _buttons; 10 | 11 | public JoyPadButtonListener(InterruptManager interruptManager, ConcurrentDictionary buttons) 12 | { 13 | _interruptManager = interruptManager; 14 | _buttons = buttons; 15 | } 16 | 17 | public void OnButtonPress(Button button) 18 | { 19 | _interruptManager.RequestInterrupt(InterruptManager.InterruptType.P1013); 20 | _buttons.TryAdd(button, button); 21 | } 22 | 23 | public void OnButtonRelease(Button button) 24 | { 25 | _buttons.TryRemove(button, out _); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /GB.Core/Controller/Joypad.cs: -------------------------------------------------------------------------------- 1 | using GB.Core.Cpu; 2 | using System.Collections.Concurrent; 3 | 4 | namespace GB.Core.Controller 5 | { 6 | internal sealed class Joypad : IAddressSpace 7 | { 8 | private readonly ConcurrentDictionary _buttons = new(); 9 | private int _p1; 10 | 11 | public Joypad(InterruptManager interruptManager, IController controller) 12 | { 13 | controller.SetButtonListener(new JoyPadButtonListener(interruptManager, _buttons)); 14 | } 15 | 16 | public bool Accepts(int address) 17 | { 18 | return address == 0xFF00; 19 | } 20 | 21 | public void SetByte(int address, int value) 22 | { 23 | _p1 = value & 0b00110000; 24 | } 25 | 26 | public int GetByte(int address) 27 | { 28 | var result = _p1 | 0b11001111; 29 | foreach (var b in _buttons.Keys) 30 | { 31 | if ((b.Line & _p1) == 0) 32 | { 33 | result &= 0xFF & ~b.Mask; 34 | } 35 | } 36 | 37 | return result; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /GB.Core/Controller/NullController.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Controller 2 | { 3 | public sealed class NullController : IController 4 | { 5 | public void SetButtonListener(IButtonListener listener) 6 | { 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /GB.Core/Cpu/CpuRegisters.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace GB.Core.Cpu 4 | { 5 | internal record CpuRegisters 6 | { 7 | public int A { get; set; } 8 | public Flags Flags { get; } = new Flags(); 9 | 10 | public int AF 11 | { 12 | get => A << 8 | Flags.Byte; 13 | set 14 | { 15 | A = value >> 8; 16 | Flags.SetFlagsByte(value); 17 | } 18 | } 19 | 20 | public int B { get; set; } 21 | public int C { get; set; } 22 | 23 | public int BC 24 | { 25 | get => B << 8 | C; 26 | set 27 | { 28 | B = value >> 8; 29 | C = value & 0xFF; 30 | } 31 | } 32 | 33 | public int D { get; set; } 34 | public int E { get; set; } 35 | 36 | public int DE 37 | { 38 | get => D << 8 | E; 39 | set 40 | { 41 | D = value >> 8; 42 | E = value & 0xFF; 43 | } 44 | } 45 | 46 | public int H { get; set; } 47 | public int L { get; set; } 48 | 49 | public int HL 50 | { 51 | get => H << 8 | L; 52 | set 53 | { 54 | H = value >> 8; 55 | L = value & 0xFF; 56 | } 57 | } 58 | 59 | public int SP { get; set; } 60 | public int PC { get; set; } 61 | 62 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 63 | internal void IncrementPC() 64 | { 65 | PC = (PC + 1) & 0xFFFF; 66 | } 67 | 68 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 69 | internal void IncrementSP() 70 | { 71 | SP = (SP + 1) & 0xFFFF; 72 | } 73 | 74 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 75 | internal void DecrementSP() 76 | { 77 | SP = (SP - 1) & 0xFFFF; 78 | } 79 | 80 | public override string ToString() 81 | { 82 | return $"AF={AF:X4} BC={BC:X4} DE={DE:X4} HL={HL:X4} SP={SP:X4} PC={PC:X4} {Flags}"; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /GB.Core/Cpu/CpuState.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Cpu 2 | { 3 | internal enum CpuState 4 | { 5 | Halted, 6 | Stopped, 7 | OpCode, 8 | ExtendedOpcode, 9 | Operands, 10 | Running, 11 | IRQ_ReadInterruptFlag, 12 | IRQ_ReadInterruptEnabled, 13 | IRQ_PushMSB, 14 | IRQ_PushLSB, 15 | IRQ_Jump 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /GB.Core/Cpu/Flags.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace GB.Core.Cpu 4 | { 5 | internal sealed class Flags 6 | { 7 | public const int ZeroPosition = 7; 8 | public const int NegativePosition = 6; 9 | public const int HalfCarryPosition = 5; 10 | public const int CarryPosition = 4; 11 | 12 | public int Byte { get; private set; } 13 | 14 | public bool IsZ() => Byte.GetBit(ZeroPosition); 15 | public bool IsN() => Byte.GetBit(NegativePosition); 16 | public bool IsH() => Byte.GetBit(HalfCarryPosition); 17 | public bool IsC() => Byte.GetBit(CarryPosition); 18 | public void SetZ(bool z) => Byte = Byte.SetBit(ZeroPosition, z); 19 | public void SetN(bool n) => Byte = Byte.SetBit(NegativePosition, n); 20 | public void SetH(bool h) => Byte = Byte.SetBit(HalfCarryPosition, h); 21 | public void SetC(bool c) => Byte = Byte.SetBit(CarryPosition, c); 22 | public void SetFlagsByte(int flags) => Byte = flags & 0xf0; 23 | 24 | public override string ToString() 25 | { 26 | var result = new StringBuilder(); 27 | result.Append(IsZ() ? 'Z' : '-'); 28 | result.Append(IsN() ? 'N' : '-'); 29 | result.Append(IsH() ? 'H' : '-'); 30 | result.Append(IsC() ? 'C' : '-'); 31 | result.Append("----"); 32 | return result.ToString(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /GB.Core/Cpu/InstructionSet/DataType.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Cpu.InstructionSet 2 | { 3 | internal enum DataType 4 | { 5 | d8, 6 | d16, 7 | r8, 8 | None 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /GB.Core/Cpu/InstructionSet/Operand.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Cpu.InstructionSet 2 | { 3 | internal record Operand 4 | { 5 | private static List KnownOperands; 6 | 7 | private Func? _reader; 8 | private Action? _writer; 9 | 10 | static Operand() 11 | { 12 | KnownOperands = new() 13 | { 14 | new Operand("A").SetHandlers((r, m, args) => r.A, (r, m, args, value) => r.A = value), 15 | new Operand("B").SetHandlers((r, m, args) => r.B, (r, m, args, value) => r.B = value), 16 | new Operand("C").SetHandlers((r, m, args) => r.C, (r, m, args, value) => r.C = value), 17 | new Operand("D").SetHandlers((r, m, args) => r.D, (r, m, args, value) => r.D = value), 18 | new Operand("E").SetHandlers((r, m, args) => r.E, (r, m, args, value) => r.E = value), 19 | new Operand("H").SetHandlers((r, m, args) => r.H, (r, m, args, value) => r.H = value), 20 | new Operand("L").SetHandlers((r, m, args) => r.L, (r, m, args, value) => r.L = value), 21 | 22 | new Operand("AF", 0, false, DataType.d16) 23 | .SetHandlers((r, m, args) => r.AF, (r, m, args, value) => r.AF = value), 24 | 25 | new Operand("BC", 0, false, DataType.d16) 26 | .SetHandlers((r, m, args) => r.BC, (r, m, args, value) => r.BC = value), 27 | 28 | new Operand("DE", 0, false, DataType.d16) 29 | .SetHandlers((r, m, args) => r.DE, (r, m, args, value) => r.DE = value), 30 | 31 | new Operand("HL", 0, false, DataType.d16) 32 | .SetHandlers((r, m, args) => r.HL, (r, m, args, value) => r.HL = value), 33 | 34 | new Operand("SP", 0, false, DataType.d16) 35 | .SetHandlers((r, m, args) => r.SP, (r, m, args, value) => r.SP = value), 36 | 37 | new Operand("PC", 0, false, DataType.d16) 38 | .SetHandlers((r, m, args) => r.PC, (r, m, args, value) => r.PC = value), 39 | 40 | new Operand("d8", 1, false, DataType.d8) 41 | .SetHandlers((r, m, args) => args[0]), 42 | 43 | new Operand("d16", 2, false, DataType.d16) 44 | .SetHandlers((r, m, args) => (args[1] << 8) | args[0]), 45 | 46 | new Operand("r8", 1, false, DataType.r8) 47 | .SetHandlers((r, m, args) => (args[0] & (1 << 7)) == 0 ? args[0] : args[0] - 0x100), 48 | 49 | new Operand("a16", 2, false, DataType.d16) 50 | .SetHandlers((r, m, args) => (args[1] << 8) | args[0]), 51 | 52 | new Operand("(BC)", 0, true, DataType.d8) 53 | .SetHandlers((r, m, args) => m.GetByte(r.BC), (r, m, args, value) => m.SetByte(r.BC, value)), 54 | 55 | new Operand("(DE)", 0, true, DataType.d8) 56 | .SetHandlers((r, m, args) => m.GetByte(r.DE), (r, m, args, value) => m.SetByte(r.DE, value)), 57 | 58 | new Operand("(HL)", 0, true, DataType.d8) 59 | .SetHandlers((r, m, args) => m.GetByte(r.HL), (r, m, args, value) => m.SetByte(r.HL, value)), 60 | 61 | new Operand("(a8)", 1, true, DataType.d8) 62 | .SetHandlers((r, m, args) => m.GetByte(0xFF00 | args[0]), (r, m, args, value) => m.SetByte(0xFF00 | args[0], value)), 63 | 64 | new Operand("(a16)", 2, true, DataType.d8) 65 | .SetHandlers((r, m, args) => m.GetByte((args[1] << 8) | args[0]), (r, m, args, value) => m.SetByte((args[1] << 8) | args[0], value)), 66 | 67 | new Operand("(C)", 0, true, DataType.d8) 68 | .SetHandlers((r, m, args) => m.GetByte(0xFF00 | r.C), (r, m, args, value) => m.SetByte(0xFF00 | r.C, value)) 69 | }; 70 | } 71 | 72 | public static Operand Parse(string name) 73 | { 74 | return KnownOperands.FirstOrDefault(x => x.Name == name) 75 | ?? throw new ArgumentException("Unknown operand", nameof(name)); 76 | } 77 | 78 | private Operand(string name) : this(name, 0, false, DataType.d8) 79 | { 80 | } 81 | 82 | private Operand(string name, int bytes, bool accessesMemory, DataType dataType) 83 | { 84 | Name = name; 85 | Bytes = bytes; 86 | AccessesMemory = accessesMemory; 87 | DataType = dataType; 88 | } 89 | 90 | public string Name { get; } 91 | public int Bytes { get; } 92 | public bool AccessesMemory { get; } 93 | public DataType DataType { get; } 94 | 95 | public int Read(CpuRegisters registers, IAddressSpace addressSpace, int[] args) 96 | { 97 | return _reader?.Invoke(registers, addressSpace, args) ?? 98 | throw new InvalidOperationException("Reader not set!"); 99 | } 100 | 101 | public void Write(CpuRegisters registers, IAddressSpace addressSpace, int[] args, int value) 102 | { 103 | _writer?.Invoke(registers, addressSpace, args, value); 104 | } 105 | 106 | private Operand SetHandlers(Func reader, Action? writer = null) 107 | { 108 | _reader = reader; 109 | _writer = writer; 110 | return this; 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /GB.Core/Cpu/InstructionSet/Operation.cs: -------------------------------------------------------------------------------- 1 | using GB.Core.Graphics; 2 | 3 | namespace GB.Core.Cpu.InstructionSet 4 | { 5 | internal abstract class Operation 6 | { 7 | public virtual bool ReadsMemory() => false; 8 | public virtual bool WritesMemory() => false; 9 | public virtual int Length() => 0; 10 | public virtual int Execute(CpuRegisters registers, IAddressSpace addressSpace, int[] args, int context) => context; 11 | public virtual bool ShouldProceed(CpuRegisters registers) => true; 12 | public virtual bool ForceFinishCycle() => false; 13 | public virtual void SwitchInterrupts(InterruptManager interruptManager) { } 14 | public virtual CorruptionType? CausesOamBug(CpuRegisters registers, int context) => null; 15 | 16 | public static bool InOamArea(int address) => address is >= 0xFE00 and <= 0xFEFF; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /GB.Core/Cpu/InterruptManager.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace GB.Core.Cpu 4 | { 5 | internal sealed class InterruptManager : IAddressSpace 6 | { 7 | private bool _ime; 8 | private readonly bool _gbc; 9 | private int _interruptFlag = 0xE1; 10 | private int _interruptEnabled; 11 | private int _pendingEnableInterrupts = -1; 12 | private int _pendingDisableInterrupts = -1; 13 | 14 | public InterruptManager(bool gameBoyColor) 15 | { 16 | _gbc = gameBoyColor; 17 | } 18 | 19 | public void EnableInterrupts(bool withDelay) 20 | { 21 | _pendingDisableInterrupts = -1; 22 | if (withDelay) 23 | { 24 | if (_pendingEnableInterrupts == -1) 25 | { 26 | _pendingEnableInterrupts = 1; 27 | } 28 | } 29 | else 30 | { 31 | _pendingEnableInterrupts = -1; 32 | _ime = true; 33 | } 34 | } 35 | 36 | public void DisableInterrupts(bool withDelay) 37 | { 38 | _pendingEnableInterrupts = -1; 39 | if (withDelay && _gbc) 40 | { 41 | if (_pendingDisableInterrupts == -1) 42 | { 43 | _pendingDisableInterrupts = 1; 44 | } 45 | } 46 | else 47 | { 48 | _pendingDisableInterrupts = -1; 49 | _ime = false; 50 | } 51 | } 52 | 53 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 54 | public void RequestInterrupt(InterruptType type) => _interruptFlag |= 1 << type.Ordinal; 55 | 56 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 57 | public void ClearInterrupt(InterruptType type) => _interruptFlag &= ~(1 << type.Ordinal); 58 | 59 | public void OnInstructionFinished() 60 | { 61 | if (_pendingEnableInterrupts != -1) 62 | { 63 | if (_pendingEnableInterrupts-- == 0) 64 | { 65 | EnableInterrupts(false); 66 | } 67 | } 68 | 69 | if (_pendingDisableInterrupts != -1) 70 | { 71 | if (_pendingDisableInterrupts-- == 0) 72 | { 73 | DisableInterrupts(false); 74 | } 75 | } 76 | } 77 | 78 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 79 | public bool IsIme() => _ime; 80 | 81 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 82 | public bool IsInterruptRequested() => (_interruptFlag & _interruptEnabled) != 0; 83 | 84 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 85 | public bool IsHaltBug() => (_interruptFlag & _interruptEnabled & 0x1F) != 0 && !_ime; 86 | 87 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 88 | public bool Accepts(int address) => address == 0xFF0F || address == 0xFFFF; 89 | 90 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 91 | public void SetByte(int address, int value) 92 | { 93 | switch (address) 94 | { 95 | case 0xFF0F: 96 | _interruptFlag = value | 0xE0; 97 | break; 98 | 99 | case 0xFFFF: 100 | _interruptEnabled = value; 101 | break; 102 | } 103 | } 104 | 105 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 106 | public int GetByte(int address) 107 | { 108 | switch (address) 109 | { 110 | case 0xFF0F: 111 | return _interruptFlag; 112 | 113 | case 0xFFFF: 114 | return _interruptEnabled; 115 | 116 | default: 117 | return 0xFF; 118 | } 119 | } 120 | 121 | public sealed class InterruptType 122 | { 123 | public static InterruptType None = new InterruptType(-1, -1); 124 | 125 | public static InterruptType VBlank = new InterruptType(0x0040, 0); 126 | public static InterruptType Lcdc = new InterruptType(0x0048, 1); 127 | public static InterruptType Timer = new InterruptType(0x0050, 2); 128 | public static InterruptType Serial = new InterruptType(0x0058, 3); 129 | public static InterruptType P1013 = new InterruptType(0x0060, 4); 130 | 131 | public int Ordinal { get; } 132 | 133 | public int Handler { get; } 134 | 135 | private InterruptType(int handler, int ordinal) 136 | { 137 | Handler = handler; 138 | Ordinal = ordinal; 139 | } 140 | 141 | public override string ToString() 142 | { 143 | return Ordinal switch 144 | { 145 | 0 => "VBlank", 146 | 1 => "LCD STAT", 147 | 2 => "Timer", 148 | 3 => "Serial", 149 | 4 => "Joypad", 150 | _ => "Invalid Interrupt!" 151 | }; 152 | } 153 | 154 | public static IEnumerable Values 155 | { 156 | get 157 | { 158 | yield return VBlank; 159 | yield return Lcdc; 160 | yield return Timer; 161 | yield return Serial; 162 | yield return P1013; 163 | } 164 | } 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /GB.Core/Cpu/SpeedMode.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace GB.Core.Cpu 4 | { 5 | internal sealed class SpeedMode : IAddressSpace 6 | { 7 | private bool _currentSpeed = false; 8 | private int _speed = 1; 9 | private bool _prepareTransition; 10 | 11 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 12 | public bool Accepts(int address) => address == 0xFF4D; 13 | 14 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 15 | public void SetByte(int address, int value) => _prepareTransition = (value & 0x01) != 0; 16 | 17 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 18 | public int GetByte(int address) => (_currentSpeed ? (1 << 7) : 0) | (_prepareTransition ? (1 << 0) : 0) | 0b01111110; 19 | 20 | public bool OnCpuStopped() 21 | { 22 | if (!_prepareTransition) 23 | { 24 | return false; 25 | } 26 | 27 | // Toggle the speed mode 28 | _currentSpeed = !_currentSpeed; 29 | _speed = _currentSpeed ? 2 : 1; 30 | _prepareTransition = false; 31 | return true; 32 | } 33 | 34 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 35 | public int GetSpeedMode() => _speed; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /GB.Core/GB.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /GB.Core/Gameboy.cs: -------------------------------------------------------------------------------- 1 | using GB.Core.Controller; 2 | using GB.Core.Cpu; 3 | using GB.Core.Graphics; 4 | using GB.Core.Gui; 5 | using GB.Core.Memory; 6 | using GB.Core.Memory.Cartridge; 7 | using GB.Core.Serial; 8 | using GB.Core.Sound; 9 | 10 | namespace GB.Core 11 | { 12 | public sealed class Gameboy : IRunnable 13 | { 14 | public const int TicksPerSec = 4_194_304; 15 | 16 | private readonly Processor _cpu; 17 | 18 | private readonly IDisplay _display; 19 | private readonly Gpu _gpu; 20 | private readonly Timer _timer; 21 | private readonly Dma _dma; 22 | private readonly Hdma _hdma; 23 | private readonly Sound.Sound _sound; 24 | private readonly SerialPort _serialPort; 25 | 26 | public bool Paused { get; set; } 27 | 28 | public Gameboy(Cartridge cartridge, IDisplay display, IController controller, ISoundOutput soundOutput, ISerialEndpoint serialEndpoint, bool enableBootRom = true, GameBoyMode gameBoyMode = GameBoyMode.AutoDetect) 29 | { 30 | _display = display; 31 | var gbc = cartridge.IsGameboyColor; 32 | 33 | switch (gameBoyMode) 34 | { 35 | case GameBoyMode.Color: 36 | gbc = true; 37 | break; 38 | case GameBoyMode.DMG: 39 | // Force into Color mode for cartridges that don't support the DMG, use DMG mode for universal cartridges. 40 | gbc = cartridge.GameboyType == GameboyType.GameboyColor; 41 | break; 42 | } 43 | 44 | var speedMode = new SpeedMode(); 45 | 46 | var interruptManager = new InterruptManager(gbc); 47 | 48 | _timer = new Timer(interruptManager, speedMode); 49 | var mmu = new Mmu(); 50 | 51 | var oamRam = new Ram(0xFE00, 0x00A0); 52 | 53 | _dma = new Dma(mmu, oamRam, speedMode); 54 | _gpu = new Gpu(_display, interruptManager, _dma, oamRam, gbc); 55 | _hdma = new Hdma(mmu); 56 | _sound = new Sound.Sound(soundOutput, gbc); 57 | _serialPort = new SerialPort(interruptManager, serialEndpoint, speedMode, gbc); 58 | 59 | mmu.AddCartridge(cartridge); 60 | mmu.AddGpu(_gpu); 61 | mmu.AddJoypad(new Joypad(interruptManager, controller)); 62 | mmu.AddInterruptManager(interruptManager); 63 | mmu.AddSerialPort(_serialPort); 64 | mmu.AddTimer(_timer); 65 | mmu.AddDma(_dma); 66 | mmu.AddSound(_sound); 67 | 68 | mmu.AddFirstRamBank(new Ram(0xC000, 0x1000)); 69 | if (gbc) 70 | { 71 | mmu.AddSpeedMode(speedMode); 72 | mmu.AddHdma(_hdma); 73 | mmu.AddSecondRamBank(new GameboyColorRam()); 74 | mmu.AddGbcRegisters(new UndocumentedGbcRegisters()); 75 | } 76 | else 77 | { 78 | mmu.AddSecondRamBank(new Ram(0xD000, 0x1000)); 79 | } 80 | 81 | mmu.AddHighRam(new Ram(0xFF80, 0x7F)); 82 | mmu.AddShadowRam(new ShadowAddressSpace(mmu, 0xE000, 0xC000, 0x1E00)); 83 | 84 | _cpu = new Processor(mmu, interruptManager, _gpu, _display, speedMode); 85 | 86 | interruptManager.DisableInterrupts(false); 87 | 88 | if (enableBootRom) 89 | { 90 | return; 91 | } 92 | 93 | _cpu.InitializeRegisters(gbc); 94 | cartridge.SetByte(0xFF50, 0xFF); 95 | } 96 | 97 | public void ToggleSoundChannel(int channel) 98 | { 99 | _sound.ToggleChannel(channel - 1); 100 | } 101 | 102 | public void Run(CancellationToken cancellationToken) 103 | { 104 | var requestedScreenRefresh = false; 105 | var lcdDisabled = false; 106 | 107 | while (!cancellationToken.IsCancellationRequested) 108 | { 109 | if (Paused) 110 | { 111 | Thread.Sleep(1000); 112 | continue; 113 | } 114 | 115 | var newMode = Tick(); 116 | if (newMode.HasValue) 117 | { 118 | _hdma.OnGpuUpdate(newMode.Value); 119 | } 120 | 121 | if (!lcdDisabled && !_gpu.IsLcdEnabled()) 122 | { 123 | lcdDisabled = true; 124 | _display.RequestRefresh(); 125 | _hdma.OnLcdSwitch(false); 126 | } 127 | else if (newMode == Gpu.Mode.VBlank) 128 | { 129 | requestedScreenRefresh = true; 130 | _display.RequestRefresh(); 131 | } 132 | 133 | if (lcdDisabled && _gpu.IsLcdEnabled()) 134 | { 135 | lcdDisabled = false; 136 | _display.WaitForRefresh(); 137 | _hdma.OnLcdSwitch(true); 138 | } 139 | else if (requestedScreenRefresh && newMode == Gpu.Mode.OamSearch) 140 | { 141 | requestedScreenRefresh = false; 142 | _display.WaitForRefresh(); 143 | } 144 | } 145 | } 146 | 147 | private Gpu.Mode? Tick() 148 | { 149 | if (_hdma.IsTransferInProgress()) 150 | { 151 | _hdma.Tick(); 152 | } 153 | else 154 | { 155 | _cpu.Tick(); 156 | } 157 | 158 | _timer.Tick(); 159 | _dma.Tick(); 160 | _sound.Tick(); 161 | _serialPort.Tick(); 162 | return _gpu.Tick(); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /GB.Core/Graphics/ColorPalette.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace GB.Core.Graphics 4 | { 5 | internal sealed class ColorPalette : IAddressSpace 6 | { 7 | private readonly int _indexAddress; 8 | private readonly int _dataAddress; 9 | private int _index; 10 | private bool _autoIncrement; 11 | 12 | private readonly int[][] _palettes; 13 | 14 | public ColorPalette(int offset) 15 | { 16 | _palettes = new int[8][]; 17 | for (var x = 0; x < 8; x++) 18 | { 19 | var row = new int[4]; 20 | for (var y = 0; y < 4; y++) 21 | { 22 | row[y] = 0; 23 | } 24 | 25 | _palettes[x] = row; 26 | } 27 | 28 | _indexAddress = offset; 29 | _dataAddress = offset + 1; 30 | } 31 | 32 | public bool Accepts(int address) => address == _indexAddress || address == _dataAddress; 33 | 34 | public void SetByte(int address, int value) 35 | { 36 | if (address == _indexAddress) 37 | { 38 | _index = value & 0x3F; 39 | _autoIncrement = (value & (1 << 7)) != 0; 40 | } 41 | else if (address == _dataAddress) 42 | { 43 | var color = _palettes[_index / 8][(_index % 8) / 2]; 44 | if (_index % 2 == 0) 45 | { 46 | color = (color & 0xFF00) | value; 47 | } 48 | else 49 | { 50 | color = (color & 0x00FF) | (value << 8); 51 | } 52 | _palettes[_index / 8][(_index % 8) / 2] = color; 53 | if (_autoIncrement) 54 | { 55 | _index = (_index + 1) & 0x3F; 56 | } 57 | } 58 | else 59 | { 60 | throw new InvalidOperationException(); 61 | } 62 | } 63 | 64 | public int GetByte(int address) 65 | { 66 | if (address == _indexAddress) 67 | { 68 | return _index | (_autoIncrement ? 0x80 : 0x00) | 0x40; 69 | } 70 | 71 | if (address != _dataAddress) 72 | { 73 | throw new ArgumentException(); 74 | } 75 | 76 | var color = _palettes[_index / 8][(_index % 8) / 2]; 77 | if (_index % 2 == 0) 78 | { 79 | return color & 0xFF; 80 | } 81 | 82 | return (color >> 8) & 0xFF; 83 | } 84 | 85 | public int[] GetPalette(int index) 86 | { 87 | return _palettes[index]; 88 | } 89 | 90 | public override string ToString() 91 | { 92 | var b = new StringBuilder(); 93 | for (var i = 0; i < 8; i++) 94 | { 95 | b.Append(i).Append(": "); 96 | 97 | var palette = GetPalette(i); 98 | 99 | foreach (var c in palette) 100 | { 101 | b.Append($"{c:X4}").Append(' '); 102 | } 103 | 104 | b[^1] = '\n'; 105 | } 106 | 107 | return b.ToString(); 108 | } 109 | 110 | public void FillWithFf() 111 | { 112 | for (var i = 0; i < 8; i++) 113 | { 114 | for (var j = 0; j < 4; j++) 115 | { 116 | _palettes[i][j] = 0x7FFF; 117 | } 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /GB.Core/Graphics/ColorPixelFifo.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Graphics 2 | { 3 | internal sealed class ColorPixelFifo : IPixelFifo 4 | { 5 | private readonly IntQueue _pixels = new(16); 6 | private readonly IntQueue _palettes = new(16); 7 | private readonly IntQueue _priorities = new(16); 8 | private readonly Lcdc _lcdc; 9 | private readonly IDisplay _display; 10 | private readonly ColorPalette _bgPalette; 11 | private readonly ColorPalette _oamPalette; 12 | 13 | public ColorPixelFifo(Lcdc lcdc, IDisplay display, ColorPalette bgPalette, ColorPalette oamPalette) 14 | { 15 | _lcdc = lcdc; 16 | _display = display; 17 | _bgPalette = bgPalette; 18 | _oamPalette = oamPalette; 19 | } 20 | 21 | public int GetLength() => _pixels.Size(); 22 | public void PutPixelToScreen() => _display.PutColorPixel(DequeuePixel()); 23 | 24 | private int DequeuePixel() 25 | { 26 | return GetColor(_priorities.Dequeue(), _palettes.Dequeue(), _pixels.Dequeue()); 27 | } 28 | 29 | public void DropPixel() => DequeuePixel(); 30 | 31 | public void Enqueue8Pixels(int[] pixelLine, TileAttributes tileAttributes) 32 | { 33 | foreach (var p in pixelLine) 34 | { 35 | _pixels.Enqueue(p); 36 | _palettes.Enqueue(tileAttributes.GetColorPaletteIndex()); 37 | _priorities.Enqueue(tileAttributes.IsPriority() ? 100 : -1); 38 | } 39 | } 40 | 41 | public void SetOverlay(int[] pixelLine, int offset, TileAttributes spriteAttr, int oamIndex) 42 | { 43 | for (var j = offset; j < pixelLine.Length; j++) 44 | { 45 | var p = pixelLine[j]; 46 | var i = j - offset; 47 | if (p == 0) 48 | { 49 | continue; // color 0 is always transparent 50 | } 51 | 52 | var oldPriority = _priorities.Get(i); 53 | 54 | var put = false; 55 | if (oldPriority is -1 or 100 && !_lcdc.IsBgAndWindowDisplay()) 56 | { 57 | // this one takes precedence 58 | put = true; 59 | } 60 | else if (oldPriority == 100) 61 | { 62 | // bg with priority 63 | put = _pixels.Get(i) == 0; 64 | } 65 | else if (oldPriority == -1 && !spriteAttr.IsPriority()) 66 | { 67 | // bg without priority 68 | put = true; 69 | } 70 | else if (oldPriority == -1 && spriteAttr.IsPriority() && _pixels.Get(i) == 0) 71 | { 72 | // bg without priority 73 | put = true; 74 | } 75 | else if (oldPriority is >= 0 and < 10) 76 | { 77 | // other sprite 78 | put = oldPriority > oamIndex; 79 | } 80 | 81 | if (put) 82 | { 83 | _pixels.Set(i, p); 84 | _palettes.Set(i, spriteAttr.GetColorPaletteIndex()); 85 | _priorities.Set(i, oamIndex); 86 | } 87 | } 88 | } 89 | 90 | public void Clear() 91 | { 92 | _pixels.Clear(); 93 | _palettes.Clear(); 94 | _priorities.Clear(); 95 | } 96 | 97 | private int GetColor(int priority, int palette, int color) 98 | { 99 | return priority is >= 0 and < 10 100 | ? _oamPalette.GetPalette(palette)[color] 101 | : _bgPalette.GetPalette(palette)[color]; 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /GB.Core/Graphics/CorruptionType.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Graphics 2 | { 3 | internal enum CorruptionType 4 | { 5 | INC_DEC, 6 | POP_1, 7 | POP_2, 8 | PUSH_1, 9 | PUSH_2, 10 | LD_HL 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /GB.Core/Graphics/DmgPixelFifo.cs: -------------------------------------------------------------------------------- 1 | using GB.Core.Memory; 2 | 3 | namespace GB.Core.Graphics 4 | { 5 | internal sealed class DmgPixelFifo : IPixelFifo 6 | { 7 | public IntQueue Pixels { get; } = new(16); 8 | private readonly IntQueue _palettes = new(16); 9 | private readonly IntQueue _pixelType = new(16); // 0 - bg, 1 - sprite 10 | 11 | private readonly IDisplay _display; 12 | private readonly MemoryRegisters _registers; 13 | 14 | public DmgPixelFifo(IDisplay display, MemoryRegisters registers) 15 | { 16 | _display = display; 17 | _registers = registers; 18 | } 19 | 20 | public int GetLength() => Pixels.Size(); 21 | public void PutPixelToScreen() => _display.PutDmgPixel(DequeuePixel()); 22 | public void DropPixel() => DequeuePixel(); 23 | 24 | public int DequeuePixel() 25 | { 26 | _pixelType.Dequeue(); 27 | return GetColor(_palettes.Dequeue(), Pixels.Dequeue()); 28 | } 29 | 30 | public void Enqueue8Pixels(int[] pixelLine, TileAttributes tileAttributes) 31 | { 32 | foreach (var p in pixelLine) 33 | { 34 | Pixels.Enqueue(p); 35 | _palettes.Enqueue(_registers.Get(GpuRegister.Bgp)); 36 | _pixelType.Enqueue(0); 37 | } 38 | } 39 | 40 | public void SetOverlay(int[] pixelLine, int offset, TileAttributes flags, int oamIndex) 41 | { 42 | var priority = flags.IsPriority(); 43 | var overlayPalette = _registers.Get(flags.GetDmgPalette()); 44 | 45 | for (var j = offset; j < pixelLine.Length; j++) 46 | { 47 | var p = pixelLine[j]; 48 | var i = j - offset; 49 | 50 | if (_pixelType.Get(i) == 1) 51 | { 52 | continue; 53 | } 54 | 55 | if (priority && Pixels.Get(i) == 0 || !priority && p != 0) 56 | { 57 | Pixels.Set(i, p); 58 | _palettes.Set(i, overlayPalette); 59 | _pixelType.Set(i, 1); 60 | } 61 | } 62 | } 63 | 64 | private static int GetColor(int palette, int colorIndex) => 0b11 & (palette >> (colorIndex * 2)); 65 | 66 | public void Clear() 67 | { 68 | Pixels.Clear(); 69 | _palettes.Clear(); 70 | _pixelType.Clear(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /GB.Core/Graphics/GpuRegister.cs: -------------------------------------------------------------------------------- 1 | using GB.Core.Memory; 2 | 3 | namespace GB.Core.Graphics 4 | { 5 | internal sealed class GpuRegister : IRegister 6 | { 7 | public static GpuRegister Stat = new GpuRegister(0xFF41, RegisterType.RW); 8 | public static GpuRegister Scy = new GpuRegister(0xFF42, RegisterType.RW); 9 | public static GpuRegister Scx = new GpuRegister(0xFF43, RegisterType.RW); 10 | public static GpuRegister Ly = new GpuRegister(0xFF44, RegisterType.R); 11 | public static GpuRegister Lyc = new GpuRegister(0xFF45, RegisterType.RW); 12 | public static GpuRegister Bgp = new GpuRegister(0xFF47, RegisterType.RW); 13 | public static GpuRegister Obp0 = new GpuRegister(0xFF48, RegisterType.RW); 14 | public static GpuRegister Obp1 = new GpuRegister(0xFF49, RegisterType.RW); 15 | public static GpuRegister Wy = new GpuRegister(0xFF4A, RegisterType.RW); 16 | public static GpuRegister Wx = new GpuRegister(0xFF4B, RegisterType.RW); 17 | public static GpuRegister Vbk = new GpuRegister(0xFF4F, RegisterType.W); 18 | 19 | public int Address { get; } 20 | public RegisterType Type { get; } 21 | 22 | public GpuRegister(int address, RegisterType type) 23 | { 24 | Address = address; 25 | Type = type; 26 | } 27 | 28 | public static IEnumerable Values() 29 | { 30 | yield return Stat; 31 | yield return Scy; 32 | yield return Scx; 33 | yield return Ly; 34 | yield return Lyc; 35 | yield return Bgp; 36 | yield return Obp0; 37 | yield return Obp1; 38 | yield return Wy; 39 | yield return Wx; 40 | yield return Vbk; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /GB.Core/Graphics/IDisplay.cs: -------------------------------------------------------------------------------- 1 | using GB.Core.Gui; 2 | 3 | namespace GB.Core.Graphics 4 | { 5 | public interface IDisplay : IRunnable 6 | { 7 | bool Enabled { get; set; } 8 | 9 | void PutDmgPixel(int color); 10 | void PutColorPixel(int gbcRgb); 11 | void RequestRefresh(); 12 | void WaitForRefresh(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /GB.Core/Graphics/IPixelFifo.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Graphics 2 | { 3 | internal interface IPixelFifo 4 | { 5 | int GetLength(); 6 | void PutPixelToScreen(); 7 | void DropPixel(); 8 | void Enqueue8Pixels(int[] pixels, TileAttributes tileAttributes); 9 | void SetOverlay(int[] pixelLine, int offset, TileAttributes flags, int oamIndex); 10 | void Clear(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /GB.Core/Graphics/IntQueue.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace GB.Core.Graphics 4 | { 5 | internal sealed class IntQueue 6 | { 7 | private readonly int[] _buffer; 8 | private int _head; 9 | private int _tail; 10 | private int _size; 11 | 12 | public IntQueue(int capacity) 13 | { 14 | _buffer = new int[capacity]; 15 | _head = 0; 16 | _tail = 0; 17 | _size = 0; 18 | } 19 | 20 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 21 | public int Size() => _size; 22 | 23 | public void Enqueue(int value) 24 | { 25 | if (_size == _buffer.Length) 26 | { 27 | throw new InvalidOperationException("Queue is full"); 28 | } 29 | 30 | _buffer[_tail] = value; 31 | _tail = (_tail + 1) % _buffer.Length; 32 | _size++; 33 | } 34 | 35 | public int Dequeue() 36 | { 37 | if (_size == 0) 38 | { 39 | throw new InvalidOperationException("Queue is empty"); 40 | } 41 | 42 | var value = _buffer[_head]; 43 | _head = (_head + 1) % _buffer.Length; 44 | _size--; 45 | return value; 46 | } 47 | 48 | public int Get(int index) 49 | { 50 | if (index < 0 || index >= _size) 51 | { 52 | throw new ArgumentOutOfRangeException(nameof(index)); 53 | } 54 | 55 | return _buffer[(_head + index) % _buffer.Length]; 56 | } 57 | 58 | public void Clear() 59 | { 60 | _head = 0; 61 | _tail = 0; 62 | _size = 0; 63 | } 64 | 65 | public void Set(int index, int value) 66 | { 67 | if (index < 0 || index >= _size) 68 | { 69 | throw new ArgumentOutOfRangeException(nameof(index)); 70 | } 71 | 72 | _buffer[(_head + index) % _buffer.Length] = value; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /GB.Core/Graphics/Lcdc.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace GB.Core.Graphics 4 | { 5 | internal sealed class Lcdc : IAddressSpace 6 | { 7 | private int _value = 0x91; 8 | 9 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 10 | public bool IsBgAndWindowDisplay() => (_value & 0x01) != 0; 11 | 12 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 13 | public bool IsObjDisplay() => (_value & 0x02) != 0; 14 | 15 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 16 | public int GetSpriteHeight() => (_value & 0x04) == 0 ? 8 : 16; 17 | 18 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 19 | public int GetBgTileMapDisplay() => (_value & 0x08) == 0 ? 0x9800 : 0x9C00; 20 | 21 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 22 | public int GetBgWindowTileData() => (_value & 0x10) == 0 ? 0x9000 : 0x8000; 23 | 24 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 25 | public bool IsBgWindowTileDataSigned() => (_value & 0x10) == 0; 26 | 27 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 28 | public bool IsWindowDisplay() => (_value & 0x20) != 0; 29 | 30 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 31 | public int GetWindowTileMapDisplay() => (_value & 0x40) == 0 ? 0x9800 : 0x9C00; 32 | 33 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 34 | public bool IsLcdEnabled() => (_value & 0x80) != 0; 35 | 36 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 37 | public bool Accepts(int address) => address == 0xFF40; 38 | 39 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 40 | public void SetByte(int address, int val) => _value = val; 41 | 42 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 43 | public int GetByte(int address) => _value; 44 | 45 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 46 | public void Set(int val) => _value = val; 47 | 48 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 49 | public int Get() => _value; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /GB.Core/Graphics/NullDisplay.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Graphics 2 | { 3 | public sealed class NullDisplay : IDisplay 4 | { 5 | public bool Enabled { get; set; } 6 | 7 | public void PutDmgPixel(int color) 8 | { 9 | } 10 | 11 | public void PutColorPixel(int gbcRgb) 12 | { 13 | } 14 | 15 | public void RequestRefresh() 16 | { 17 | } 18 | 19 | public void WaitForRefresh() 20 | { 21 | } 22 | 23 | public void Run(CancellationToken token) 24 | { 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /GB.Core/Graphics/Phase/HBlankPhase.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Graphics.Phase 2 | { 3 | internal sealed class HBlankPhase : IGpuPhase 4 | { 5 | private int _ticks; 6 | 7 | public HBlankPhase Start(int ticksInLine) 8 | { 9 | _ticks = ticksInLine; 10 | return this; 11 | } 12 | 13 | public bool Tick() 14 | { 15 | _ticks++; 16 | return _ticks < 456; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /GB.Core/Graphics/Phase/IGpuPhase.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Graphics.Phase 2 | { 3 | internal interface IGpuPhase 4 | { 5 | bool Tick(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /GB.Core/Graphics/Phase/OamSearch.cs: -------------------------------------------------------------------------------- 1 | using GB.Core.Memory; 2 | 3 | namespace GB.Core.Graphics.Phase 4 | { 5 | internal sealed class OamSearch : IGpuPhase 6 | { 7 | private enum State 8 | { 9 | ReadingY, 10 | ReadingX 11 | } 12 | 13 | public sealed record SpritePosition(int X, int Y, int Address); 14 | 15 | private readonly IAddressSpace _oemRam; 16 | private readonly MemoryRegisters _registers; 17 | private readonly SpritePosition?[] _sprites; 18 | private readonly Lcdc _lcdc; 19 | private int _spritePosIndex; 20 | private State _state; 21 | private int _spriteY; 22 | private int _spriteX; 23 | private int _i; 24 | 25 | public OamSearch(IAddressSpace oemRam, Lcdc lcdc, MemoryRegisters registers) 26 | { 27 | _oemRam = oemRam; 28 | _registers = registers; 29 | _lcdc = lcdc; 30 | _sprites = new SpritePosition[10]; 31 | } 32 | 33 | public OamSearch Start() 34 | { 35 | _spritePosIndex = 0; 36 | _state = State.ReadingY; 37 | _spriteY = 0; 38 | _spriteX = 0; 39 | _i = 0; 40 | for (var j = 0; j < _sprites.Length; j++) 41 | { 42 | _sprites[j] = null; 43 | } 44 | 45 | return this; 46 | } 47 | 48 | public bool Tick() 49 | { 50 | var spriteAddress = 0xFE00 + 4 * _i; 51 | switch (_state) 52 | { 53 | case State.ReadingY: 54 | _spriteY = _oemRam.GetByte(spriteAddress); 55 | _state = State.ReadingX; 56 | break; 57 | 58 | case State.ReadingX: 59 | _spriteX = _oemRam.GetByte(spriteAddress + 1); 60 | if (_spritePosIndex < _sprites.Length && Between(_spriteY, _registers.Get(GpuRegister.Ly) + 16, 61 | _spriteY + _lcdc.GetSpriteHeight())) 62 | { 63 | _sprites[_spritePosIndex++] = new SpritePosition(_spriteX, _spriteY, spriteAddress); 64 | } 65 | 66 | _i++; 67 | _state = State.ReadingY; 68 | break; 69 | } 70 | 71 | return _i < 40; 72 | } 73 | 74 | public SpritePosition?[] GetSprites() 75 | { 76 | return _sprites; 77 | } 78 | 79 | private static bool Between(int from, int x, int to) 80 | { 81 | return from <= x && x < to; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /GB.Core/Graphics/Phase/PixelTransfer.cs: -------------------------------------------------------------------------------- 1 | using GB.Core.Memory; 2 | 3 | namespace GB.Core.Graphics.Phase 4 | { 5 | internal sealed class PixelTransfer : IGpuPhase 6 | { 7 | private readonly IPixelFifo _fifo; 8 | private readonly Fetcher _fetcher; 9 | private readonly MemoryRegisters _r; 10 | private readonly Lcdc _lcdc; 11 | private readonly bool _gbc; 12 | private OamSearch.SpritePosition?[] _sprites = Array.Empty(); 13 | private int _droppedPixels; 14 | private int _x; 15 | private bool _window; 16 | 17 | public PixelTransfer(IAddressSpace videoRam0, IAddressSpace? videoRam1, IAddressSpace oemRam, IDisplay display, Lcdc lcdc, MemoryRegisters r, bool gbc, ColorPalette bgPalette, ColorPalette oamPalette) 18 | { 19 | _r = r; 20 | _lcdc = lcdc; 21 | _gbc = gbc; 22 | 23 | _fifo = gbc 24 | ? new ColorPixelFifo(lcdc, display, bgPalette, oamPalette) 25 | : new DmgPixelFifo(display, r); 26 | 27 | _fetcher = new Fetcher(_fifo, videoRam0, videoRam1, oemRam, lcdc, r, gbc); 28 | } 29 | 30 | public PixelTransfer Start(OamSearch.SpritePosition?[] sprites) 31 | { 32 | _sprites = sprites; 33 | _droppedPixels = 0; 34 | _x = 0; 35 | _window = false; 36 | 37 | _fetcher.Init(); 38 | if (_gbc || _lcdc.IsBgAndWindowDisplay()) 39 | { 40 | StartFetchingBackground(); 41 | } 42 | else 43 | { 44 | _fetcher.FetchingDisabled(); 45 | } 46 | 47 | return this; 48 | } 49 | 50 | public bool Tick() 51 | { 52 | _fetcher.Tick(); 53 | if (_lcdc.IsBgAndWindowDisplay() || _gbc) 54 | { 55 | if (_fifo.GetLength() <= 8) 56 | { 57 | return true; 58 | } 59 | 60 | if (_droppedPixels < _r.Get(GpuRegister.Scx) % 8) 61 | { 62 | _fifo.DropPixel(); 63 | _droppedPixels++; 64 | return true; 65 | } 66 | 67 | if (!_window && _lcdc.IsWindowDisplay() && _r.Get(GpuRegister.Ly) >= _r.Get(GpuRegister.Wy) && 68 | _x == _r.Get(GpuRegister.Wx) - 7) 69 | { 70 | _window = true; 71 | StartFetchingWindow(); 72 | return true; 73 | } 74 | } 75 | 76 | if (_lcdc.IsObjDisplay()) 77 | { 78 | if (_fetcher.SpriteInProgress()) 79 | { 80 | return true; 81 | } 82 | 83 | var spriteAdded = false; 84 | for (var i = 0; i < _sprites.Length; i++) 85 | { 86 | var s = _sprites[i]; 87 | if (s == null) 88 | { 89 | continue; 90 | } 91 | 92 | if (_x == 0 && s.X < 8) 93 | { 94 | _fetcher.AddSprite(s, 8 - s.X, i); 95 | spriteAdded = true; 96 | 97 | _sprites[i] = null; 98 | } 99 | else if (s.X - 8 == _x) 100 | { 101 | _fetcher.AddSprite(s, 0, i); 102 | spriteAdded = true; 103 | 104 | _sprites[i] = null; 105 | } 106 | 107 | if (spriteAdded) 108 | { 109 | return true; 110 | } 111 | } 112 | } 113 | 114 | _fifo.PutPixelToScreen(); 115 | if (++_x == 160) 116 | { 117 | return false; 118 | } 119 | 120 | return true; 121 | } 122 | 123 | private void StartFetchingBackground() 124 | { 125 | var bgX = _r.Get(GpuRegister.Scx) / 0x08; 126 | var bgY = (_r.Get(GpuRegister.Scy) + _r.Get(GpuRegister.Ly)) % 0x100; 127 | 128 | _fetcher.StartFetching(_lcdc.GetBgTileMapDisplay() + (bgY / 0x08) * 0x20, _lcdc.GetBgWindowTileData(), bgX, 129 | _lcdc.IsBgWindowTileDataSigned(), bgY % 0x08); 130 | } 131 | 132 | private void StartFetchingWindow() 133 | { 134 | var winX = (_x - _r.Get(GpuRegister.Wx) + 7) / 0x08; 135 | var winY = _r.Get(GpuRegister.Ly) - _r.Get(GpuRegister.Wy); 136 | 137 | _fetcher.StartFetching(_lcdc.GetWindowTileMapDisplay() + (winY / 0x08) * 0x20, _lcdc.GetBgWindowTileData(), 138 | winX, _lcdc.IsBgWindowTileDataSigned(), winY % 0x08); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /GB.Core/Graphics/Phase/VBlankPhase.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Graphics.Phase 2 | { 3 | internal sealed class VBlankPhase : IGpuPhase 4 | { 5 | private int _ticks; 6 | 7 | public VBlankPhase Start() 8 | { 9 | _ticks = 0; 10 | return this; 11 | } 12 | 13 | public bool Tick() 14 | { 15 | return ++_ticks < 456; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /GB.Core/Graphics/SpriteBug.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Graphics 2 | { 3 | internal static class SpriteBug 4 | { 5 | public static void CorruptOam(IAddressSpace addressSpace, CorruptionType type, int ticksInLine) 6 | { 7 | var cpuCycle = (ticksInLine + 1) / 4 + 1; 8 | switch (type) 9 | { 10 | case CorruptionType.INC_DEC: 11 | if (cpuCycle >= 2) 12 | { 13 | CopyValues(addressSpace, (cpuCycle - 2) * 8 + 2, (cpuCycle - 1) * 8 + 2, 6); 14 | } 15 | 16 | break; 17 | 18 | case CorruptionType.POP_1: 19 | if (cpuCycle >= 4) 20 | { 21 | CopyValues(addressSpace, (cpuCycle - 3) * 8 + 2, (cpuCycle - 4) * 8 + 2, 8); 22 | CopyValues(addressSpace, (cpuCycle - 3) * 8 + 8, (cpuCycle - 4) * 8 + 0, 2); 23 | CopyValues(addressSpace, (cpuCycle - 4) * 8 + 2, (cpuCycle - 2) * 8 + 2, 6); 24 | } 25 | 26 | break; 27 | 28 | case CorruptionType.POP_2: 29 | if (cpuCycle >= 5) 30 | { 31 | CopyValues(addressSpace, (cpuCycle - 5) * 8 + 0, (cpuCycle - 2) * 8 + 0, 8); 32 | } 33 | 34 | break; 35 | 36 | case CorruptionType.PUSH_1: 37 | if (cpuCycle >= 4) 38 | { 39 | CopyValues(addressSpace, (cpuCycle - 4) * 8 + 2, (cpuCycle - 3) * 8 + 2, 8); 40 | CopyValues(addressSpace, (cpuCycle - 3) * 8 + 2, (cpuCycle - 1) * 8 + 2, 6); 41 | } 42 | 43 | break; 44 | 45 | case CorruptionType.PUSH_2: 46 | if (cpuCycle >= 5) 47 | { 48 | CopyValues(addressSpace, (cpuCycle - 4) * 8 + 2, (cpuCycle - 3) * 8 + 2, 8); 49 | } 50 | 51 | break; 52 | 53 | case CorruptionType.LD_HL: 54 | if (cpuCycle >= 4) 55 | { 56 | CopyValues(addressSpace, (cpuCycle - 3) * 8 + 2, (cpuCycle - 4) * 8 + 2, 8); 57 | CopyValues(addressSpace, (cpuCycle - 3) * 8 + 8, (cpuCycle - 4) * 8 + 0, 2); 58 | CopyValues(addressSpace, (cpuCycle - 4) * 8 + 2, (cpuCycle - 2) * 8 + 2, 6); 59 | } 60 | 61 | break; 62 | default: 63 | throw new ArgumentOutOfRangeException(nameof(type), type, null); 64 | } 65 | } 66 | 67 | private static void CopyValues(IAddressSpace addressSpace, int from, int to, int length) 68 | { 69 | for (var i = length - 1; i >= 0; i--) 70 | { 71 | var b = addressSpace.GetByte(0xFE00 + from + i) % 0xFF; 72 | addressSpace.SetByte(0xFE00 + to + i, b); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /GB.Core/Graphics/TileAttributes.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace GB.Core.Graphics 4 | { 5 | internal sealed class TileAttributes 6 | { 7 | public static TileAttributes Empty { get; } 8 | private static readonly TileAttributes[] Attributes; 9 | private readonly int _value; 10 | 11 | static TileAttributes() 12 | { 13 | Attributes = new TileAttributes[256]; 14 | 15 | for (var i = 0; i < 256; i++) 16 | { 17 | Attributes[i] = new TileAttributes(i); 18 | } 19 | 20 | Empty = Attributes[0]; 21 | } 22 | 23 | private TileAttributes(int value) => _value = value; 24 | 25 | public static TileAttributes ValueOf(int value) => Attributes[value]; 26 | 27 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 28 | public bool IsPriority() => (_value & (1 << 7)) != 0; 29 | 30 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 31 | public bool IsYFlip() => (_value & (1 << 6)) != 0; 32 | 33 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 34 | public bool IsXFlip() => (_value & (1 << 5)) != 0; 35 | 36 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 37 | public GpuRegister GetDmgPalette() => (_value & (1 << 4)) == 0 ? GpuRegister.Obp0 : GpuRegister.Obp1; 38 | 39 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 40 | public int GetBank() => (_value & (1 << 3)) == 0 ? 0 : 1; 41 | 42 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 43 | public int GetColorPaletteIndex() => _value & 0x07; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /GB.Core/Gui/Emulator.cs: -------------------------------------------------------------------------------- 1 | using GB.Core.Controller; 2 | using GB.Core.Graphics; 3 | using GB.Core.Memory.Cartridge; 4 | using GB.Core.Serial; 5 | using GB.Core.Sound; 6 | 7 | namespace GB.Core.Gui 8 | { 9 | public sealed class Emulator : IRunnable 10 | { 11 | private Cartridge? _cartridge; 12 | 13 | public bool EnableBootRom { get; set; } = true; 14 | public GameBoyMode GameBoyMode { get; set; } = GameBoyMode.AutoDetect; 15 | 16 | public string? RomPath { get; set; } 17 | public Stream? RomStream { get; set; } 18 | 19 | public Gameboy? Gameboy { get; set; } 20 | 21 | public IDisplay Display { get; set; } = new NullDisplay(); 22 | public ISoundOutput SoundOutput { get; set; } = new NullSoundOutput(); 23 | public IController Controller { get; set; } = new NullController(); 24 | //public SerialEndpoint SerialEndpoint { get; set; } = new NullSerialEndpoint(); 25 | 26 | //public GameboyOptions Options { get; set; } 27 | public bool Active { get; set; } 28 | 29 | public Emulator(/*GameboyOptions options*/) 30 | { 31 | // Options = options; 32 | } 33 | 34 | public void Run(CancellationToken token) 35 | { 36 | //if (!Options.RomSpecified || !Options.RomFile.Exists) 37 | //{ 38 | // throw new ArgumentException("The ROM path doesn't exist: " + Options.RomFile); 39 | //} 40 | if (string.IsNullOrEmpty(RomPath) && RomStream is null) 41 | { 42 | throw new ArgumentException("Please choose a ROM."); 43 | } 44 | 45 | if (!string.IsNullOrEmpty(RomPath)) 46 | { 47 | _cartridge = Cartridge.FromFile(RomPath); 48 | } 49 | else if (RomStream != null) 50 | { 51 | _cartridge = Cartridge.FromStream(RomStream); 52 | } 53 | 54 | if (_cartridge is null) 55 | { 56 | throw new ArgumentException("The ROM path doesn't exist or points to an invalid ROM file: " + RomPath); 57 | } 58 | 59 | Gameboy = CreateGameboy(_cartridge); 60 | new Thread(() => Display.Run(token)).Start(); 61 | new Thread(() => Gameboy.Run(token)).Start(); 62 | 63 | Active = true; 64 | } 65 | 66 | public void Stop(CancellationTokenSource source) 67 | { 68 | if (!Active) 69 | { 70 | return; 71 | } 72 | 73 | source.Cancel(); 74 | Active = false; 75 | 76 | _cartridge?.SaveRam(); 77 | _cartridge?.Dispose(); 78 | 79 | RomPath = null; 80 | 81 | RomStream?.Dispose(); 82 | RomStream = null; 83 | } 84 | 85 | public void ToggleSoundChannel(int channel) 86 | { 87 | Gameboy?.ToggleSoundChannel(channel); 88 | } 89 | 90 | public void TogglePause() 91 | { 92 | if (Gameboy != null) 93 | { 94 | Gameboy.Paused = !Gameboy.Paused; 95 | } 96 | } 97 | 98 | private Gameboy CreateGameboy(Cartridge rom) 99 | { 100 | return new Gameboy(rom, Display, Controller, SoundOutput, new NullSerialEndpoint(), EnableBootRom, GameBoyMode); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /GB.Core/Gui/GameBoyMode.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Gui; 2 | 3 | public enum GameBoyMode 4 | { 5 | AutoDetect, 6 | DMG, 7 | Color 8 | } -------------------------------------------------------------------------------- /GB.Core/Gui/IRunnable.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Gui 2 | { 3 | public interface IRunnable 4 | { 5 | void Run(CancellationToken token); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /GB.Core/IAddressSpace.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core 2 | { 3 | internal interface IAddressSpace 4 | { 5 | bool Accepts(int address); 6 | void SetByte(int address, int value); 7 | int GetByte(int address); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /GB.Core/Memory/BootRomType.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core 2 | { 3 | internal enum BootRomType 4 | { 5 | DMG0, 6 | DMG, 7 | MGB, 8 | SGB, 9 | SGB2, 10 | CGB0, 11 | CGB, 12 | AGB 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /GB.Core/Memory/Cartridge/Battery/FileBattery.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | 4 | namespace GB.Core.Memory.Cartridge.Battery 5 | { 6 | internal sealed class FileBattery : IBattery 7 | { 8 | private readonly string _ramFilePath; 9 | private readonly FileStream? _file; 10 | 11 | public FileBattery(Cartridge cartridge) 12 | { 13 | if (string.IsNullOrEmpty(cartridge.FilePath)) 14 | { 15 | _ramFilePath = ""; 16 | _file = null; 17 | return; 18 | } 19 | 20 | _ramFilePath = Path.Combine( 21 | Path.GetDirectoryName(cartridge.FilePath)!, 22 | Path.GetFileNameWithoutExtension(cartridge.FilePath) + ".sav"); 23 | _file = new FileStream(_ramFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); 24 | } 25 | 26 | public void LoadRam(int[] ram) 27 | { 28 | if (_file is null) 29 | { 30 | return; 31 | } 32 | 33 | _file.Seek(0, SeekOrigin.Begin); 34 | using var reader = new BinaryReader(_file, Encoding.UTF8, true); 35 | LoadRam(reader, ram); 36 | } 37 | 38 | public void LoadRamWithClock(int[] ram, long[] clockData) 39 | { 40 | if (_file is null) 41 | { 42 | return; 43 | } 44 | 45 | _file.Seek(0, SeekOrigin.Begin); 46 | using var reader = new BinaryReader(_file, Encoding.UTF8, true); 47 | LoadRam(reader, ram); 48 | LoadClock(reader, clockData); 49 | } 50 | 51 | private void LoadRam(BinaryReader reader, int[] ram) 52 | { 53 | var i = 0; 54 | 55 | try 56 | { 57 | while (i < ram.Length) 58 | { 59 | ram[i++] = reader.ReadInt32(); 60 | } 61 | } 62 | catch (EndOfStreamException) { } 63 | catch (IOException) { } 64 | } 65 | 66 | private void LoadClock(BinaryReader reader, long[] clockData) 67 | { 68 | var i = 0; 69 | 70 | try 71 | { 72 | while (i < clockData.Length) 73 | { 74 | clockData[i++] = reader.ReadInt64(); 75 | } 76 | } 77 | catch (EndOfStreamException) { } 78 | catch (IOException) { } 79 | } 80 | 81 | public void SaveRam(int[] ram) 82 | { 83 | if (_file is null) 84 | { 85 | return; 86 | } 87 | 88 | _file.Seek(0, SeekOrigin.Begin); 89 | using var writer = new BinaryWriter(_file, Encoding.UTF8, true); 90 | SaveRam(writer, ram); 91 | } 92 | 93 | public void SaveRamWithClock(int[] ram, long[] clockData) 94 | { 95 | if (_file is null) 96 | { 97 | return; 98 | } 99 | 100 | _file.Seek(0, SeekOrigin.Begin); 101 | using var writer = new BinaryWriter(_file, Encoding.UTF8, true); 102 | SaveRam(writer, ram); 103 | SaveClock(writer, clockData); 104 | } 105 | 106 | private void SaveRam(BinaryWriter writer, int[] ram) 107 | { 108 | var i = 0; 109 | 110 | try 111 | { 112 | while (i < ram.Length) 113 | { 114 | writer.Write(ram[i++]); 115 | } 116 | } 117 | catch (IOException) { } 118 | } 119 | 120 | private void SaveClock(BinaryWriter writer, long[] clockData) 121 | { 122 | var i = 0; 123 | 124 | try 125 | { 126 | while (i < clockData.Length) 127 | { 128 | writer.Write(clockData[i++]); 129 | } 130 | } 131 | catch (IOException) { } 132 | } 133 | 134 | public void Dispose() 135 | { 136 | if (_file is null) 137 | { 138 | return; 139 | } 140 | 141 | _file.Close(); 142 | _file.Dispose(); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /GB.Core/Memory/Cartridge/Battery/IBattery.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Memory.Cartridge.Battery 2 | { 3 | internal interface IBattery : IDisposable 4 | { 5 | void LoadRam(int[] ram); 6 | void SaveRam(int[] ram); 7 | void LoadRamWithClock(int[] ram, long[] clockData); 8 | void SaveRamWithClock(int[] ram, long[] clockData); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /GB.Core/Memory/Cartridge/Battery/NullBattery.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Memory.Cartridge.Battery 2 | { 3 | internal sealed class NullBattery : IBattery 4 | { 5 | public void LoadRam(int[] ram) 6 | { 7 | } 8 | 9 | public void LoadRamWithClock(int[] ram, long[] clockData) 10 | { 11 | } 12 | 13 | public void SaveRam(int[] ram) 14 | { 15 | } 16 | 17 | public void SaveRamWithClock(int[] ram, long[] clockData) 18 | { 19 | } 20 | 21 | public void Dispose() { } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /GB.Core/Memory/Cartridge/CartridgeType.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Memory.Cartridge 2 | { 3 | internal enum CartridgeType 4 | { 5 | ROM = 0x00, 6 | ROM_MBC1 = 0x01, 7 | ROM_MBC1_RAM = 0x02, 8 | ROM_MBC1_RAM_BATTERY = 0x03, 9 | ROM_MBC2 = 0x05, 10 | ROM_MBC2_BATTERY = 0x06, 11 | ROM_RAM = 0x08, 12 | ROM_RAM_BATTERY = 0x09, 13 | ROM_MMM01 = 0x0b, 14 | ROM_MMM01_SRAM = 0x0c, 15 | ROM_MMM01_SRAM_BATTERY = 0x0d, 16 | ROM_MBC3_TIMER_BATTERY = 0x0f, 17 | ROM_MBC3_TIMER_RAM_BATTERY = 0x10, 18 | ROM_MBC3 = 0x11, 19 | ROM_MBC3_RAM = 0x12, 20 | ROM_MBC3_RAM_BATTERY = 0x13, 21 | ROM_MBC5 = 0x19, 22 | ROM_MBC5_RAM = 0x1a, 23 | ROM_MBC5_RAM_BATTERY = 0x01b, 24 | ROM_MBC5_RUMBLE = 0x1c, 25 | ROM_MBC5_RUMBLE_SRAM = 0x1d, 26 | ROM_MBC5_RUMBLE_SRAM_BATTERY = 0x1e 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /GB.Core/Memory/Cartridge/CartridgeTypeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace GB.Core.Memory.Cartridge 4 | { 5 | internal static class CartridgeTypeExtensions 6 | { 7 | public static IEnumerable Values(this CartridgeType src) 8 | { 9 | return Enum.GetValues(typeof(CartridgeType)).Cast(); 10 | } 11 | 12 | public static bool IsMbc1(this CartridgeType src) => src.NameContainsSegment("MBC1"); 13 | public static bool IsMbc2(this CartridgeType src) => src.NameContainsSegment("MBC2"); 14 | public static bool IsMbc3(this CartridgeType src) => src.NameContainsSegment("MBC3"); 15 | public static bool IsMbc5(this CartridgeType src) => src.NameContainsSegment("MBC5"); 16 | public static bool IsMmm01(this CartridgeType src) => src.NameContainsSegment("MMM01"); 17 | public static bool IsRam(this CartridgeType src) => src.NameContainsSegment("RAM"); 18 | public static bool IsSram(this CartridgeType src) => src.NameContainsSegment("SRAM"); 19 | public static bool IsTimer(this CartridgeType src) => src.NameContainsSegment("TIMER"); 20 | public static bool IsBattery(this CartridgeType src) => src.NameContainsSegment("BATTERY"); 21 | public static bool IsRumble(this CartridgeType src) => src.NameContainsSegment("RUMBLE"); 22 | private static bool NameContainsSegment(this CartridgeType src, string segment) 23 | { 24 | return new Regex("(^|_)" + Regex.Escape(segment) + "($|_)").IsMatch(src.ToString()); 25 | } 26 | 27 | public static CartridgeType GetById(int id) => (CartridgeType)id; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /GB.Core/Memory/Cartridge/GameboyType.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Memory.Cartridge 2 | { 3 | public enum GameboyType 4 | { 5 | Universal = 0x80, 6 | GameboyColor = 0xC0, 7 | Standard = 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /GB.Core/Memory/Cartridge/RTC/Clock.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Memory.Cartridge.RTC 2 | { 3 | internal static class Clock 4 | { 5 | public static IClock SystemClock { get; } = new SystemClock(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /GB.Core/Memory/Cartridge/RTC/IClock.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Memory.Cartridge.RTC 2 | { 3 | internal interface IClock 4 | { 5 | long CurrentTimeMillis(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /GB.Core/Memory/Cartridge/RTC/RealTimeClock.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Memory.Cartridge.RTC 2 | { 3 | internal sealed class RealTimeClock 4 | { 5 | private readonly IClock _clock; 6 | private long _offsetSec; 7 | private long _clockStart; 8 | private bool _halt; 9 | private long _latchStart; 10 | private int _haltSeconds; 11 | private int _haltMinutes; 12 | private int _haltHours; 13 | private int _haltDays; 14 | 15 | public RealTimeClock(IClock clock) 16 | { 17 | _clock = clock; 18 | _clockStart = clock.CurrentTimeMillis(); 19 | } 20 | 21 | public void Latch() 22 | { 23 | _latchStart = _clock.CurrentTimeMillis(); 24 | } 25 | 26 | public void Unlatch() 27 | { 28 | _latchStart = 0; 29 | } 30 | 31 | public int GetSeconds() 32 | { 33 | return (int)(ClockTimeInSec() % 60); 34 | } 35 | 36 | public int GetMinutes() 37 | { 38 | return (int)((ClockTimeInSec() % (60 * 60)) / 60); 39 | } 40 | 41 | public int GetHours() 42 | { 43 | return (int)((ClockTimeInSec() % (60 * 60 * 24)) / (60 * 60)); 44 | } 45 | 46 | public int GetDayCounter() 47 | { 48 | return (int)(ClockTimeInSec() % (60 * 60 * 24 * 512) / (60 * 60 * 24)); 49 | } 50 | 51 | public bool IsHalt() 52 | { 53 | return _halt; 54 | } 55 | 56 | public bool IsCounterOverflow() 57 | { 58 | return ClockTimeInSec() >= 60 * 60 * 24 * 512; 59 | } 60 | 61 | public void SetSeconds(int seconds) 62 | { 63 | if (!_halt) 64 | { 65 | return; 66 | } 67 | 68 | _haltSeconds = seconds; 69 | } 70 | 71 | public void SetMinutes(int minutes) 72 | { 73 | if (!_halt) 74 | { 75 | return; 76 | } 77 | 78 | _haltMinutes = minutes; 79 | } 80 | 81 | public void SetHours(int hours) 82 | { 83 | if (!_halt) 84 | { 85 | return; 86 | } 87 | 88 | _haltHours = hours; 89 | } 90 | 91 | public void SetDayCounter(int dayCounter) 92 | { 93 | if (!_halt) 94 | { 95 | return; 96 | } 97 | 98 | _haltDays = dayCounter; 99 | } 100 | 101 | public void SetHalt(bool halt) 102 | { 103 | if (halt && !_halt) 104 | { 105 | Latch(); 106 | _haltSeconds = GetSeconds(); 107 | _haltMinutes = GetMinutes(); 108 | _haltHours = GetHours(); 109 | _haltDays = GetDayCounter(); 110 | Unlatch(); 111 | } 112 | else if (!halt && _halt) 113 | { 114 | _offsetSec = _haltSeconds + _haltMinutes * 60 + _haltHours * 60 * 60 + _haltDays * 60 * 60 * 24; 115 | _clockStart = _clock.CurrentTimeMillis(); 116 | } 117 | 118 | _halt = halt; 119 | } 120 | 121 | public void ClearCounterOverflow() 122 | { 123 | while (IsCounterOverflow()) 124 | { 125 | _offsetSec -= 60 * 60 * 24 * 512; 126 | } 127 | } 128 | 129 | private long ClockTimeInSec() 130 | { 131 | var now = _latchStart == 0 ? _clock.CurrentTimeMillis() : _latchStart; 132 | return (now - _clockStart) / 1000 + _offsetSec; 133 | } 134 | 135 | public void Deserialize(long[] clockData) 136 | { 137 | var seconds = clockData[0]; 138 | var minutes = clockData[1]; 139 | var hours = clockData[2]; 140 | var days = clockData[3]; 141 | var daysHigh = clockData[4]; 142 | var timestamp = clockData[10]; 143 | 144 | _clockStart = timestamp * 1000; 145 | _offsetSec = seconds + minutes * 60 + hours * 60 * 60 + days * 24 * 60 * 60 + 146 | daysHigh * 256 * 24 * 60 * 60; 147 | } 148 | 149 | public long[] Serialize() 150 | { 151 | var clockData = new long[11]; 152 | Latch(); 153 | clockData[0] = clockData[5] = GetSeconds(); 154 | clockData[1] = clockData[6] = GetMinutes(); 155 | clockData[2] = clockData[7] = GetHours(); 156 | clockData[3] = clockData[8] = GetDayCounter() % 256; 157 | clockData[4] = clockData[9] = GetDayCounter() / 256; 158 | clockData[10] = _latchStart / 1000; 159 | Unlatch(); 160 | return clockData; 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /GB.Core/Memory/Cartridge/RTC/SystemClock.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Memory.Cartridge.RTC 2 | { 3 | internal sealed class SystemClock : IClock 4 | { 5 | public long CurrentTimeMillis() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /GB.Core/Memory/Cartridge/Type/Mbc2.cs: -------------------------------------------------------------------------------- 1 | using GB.Core.Memory.Cartridge.Battery; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace GB.Core.Memory.Cartridge.Type 9 | { 10 | internal sealed class Mbc2 : IAddressSpace 11 | { 12 | private readonly int[] _cartridge; 13 | private readonly int[] _ram; 14 | private readonly IBattery _battery; 15 | private int _selectedRomBank = 1; 16 | private bool _ramWriteEnabled; 17 | 18 | public Mbc2(int[] cartridge, CartridgeType type, IBattery battery, int romBanks) 19 | { 20 | _cartridge = cartridge; 21 | _ram = new int[0x0200]; 22 | for (var i = 0; i < _ram.Length; i++) 23 | { 24 | _ram[i] = 0xFF; 25 | } 26 | 27 | _battery = battery; 28 | battery.LoadRam(_ram); 29 | } 30 | 31 | public void SaveRam() 32 | { 33 | _battery.SaveRam(_ram); 34 | } 35 | 36 | public bool Accepts(int address) => address >= 0x0000 && address < 0x8000 || address >= 0xA000 && address < 0xC000; 37 | 38 | public void SetByte(int address, int value) 39 | { 40 | if (address >= 0x0000 && address < 0x2000) 41 | { 42 | if ((address & 0x0100) == 0) 43 | { 44 | _ramWriteEnabled = (value & 0b1010) != 0; 45 | if (!_ramWriteEnabled) 46 | { 47 | SaveRam(); 48 | } 49 | } 50 | } 51 | else if (address >= 0x2000 && address < 0x4000) 52 | { 53 | if ((address & 0x0100) != 0) 54 | { 55 | _selectedRomBank = value & 0b00001111; 56 | } 57 | } 58 | else if (address >= 0xA000 && address < 0xC000 && _ramWriteEnabled) 59 | { 60 | var ramAddress = GetRamAddress(address); 61 | if (ramAddress < _ram.Length) 62 | { 63 | _ram[ramAddress] = value & 0x0F; 64 | } 65 | } 66 | } 67 | 68 | public int GetByte(int address) 69 | { 70 | if (address >= 0x0000 && address < 0x4000) 71 | { 72 | return GetRomByte(0, address); 73 | } 74 | 75 | if (address >= 0x4000 && address < 0x8000) 76 | { 77 | return GetRomByte(_selectedRomBank, address - 0x4000); 78 | } 79 | 80 | if (address >= 0xA000 && address < 0xB000) 81 | { 82 | var ramAddress = GetRamAddress(address); 83 | if (ramAddress < _ram.Length) 84 | { 85 | return _ram[ramAddress]; 86 | } 87 | 88 | return 0xFF; 89 | } 90 | 91 | return 0xFF; 92 | } 93 | 94 | private int GetRomByte(int bank, int address) 95 | { 96 | var cartOffset = bank * 0x4000 + address; 97 | if (cartOffset < _cartridge.Length) 98 | { 99 | return _cartridge[cartOffset]; 100 | } 101 | 102 | return 0xFF; 103 | } 104 | 105 | private static int GetRamAddress(int address) => address - 0xA000; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /GB.Core/Memory/Cartridge/Type/Mbc5.cs: -------------------------------------------------------------------------------- 1 | using GB.Core.Memory.Cartridge.Battery; 2 | 3 | namespace GB.Core.Memory.Cartridge.Type 4 | { 5 | internal sealed class Mbc5 : IAddressSpace 6 | { 7 | private readonly int _ramBanks; 8 | private readonly int[] _cartridge; 9 | private readonly int[] _ram; 10 | private readonly IBattery _battery; 11 | private int _selectedRamBank; 12 | private int _selectedRomBank = 1; 13 | private bool _ramWriteEnabled; 14 | 15 | public Mbc5(int[] cartridge, CartridgeType type, IBattery battery, int romBanks, int ramBanks) 16 | { 17 | _cartridge = cartridge; 18 | _ramBanks = ramBanks; 19 | _ram = new int[0x2000 * Math.Max(_ramBanks, 1)]; 20 | for (var i = 0; i < _ram.Length; i++) 21 | { 22 | _ram[i] = 0xFF; 23 | } 24 | 25 | _battery = battery; 26 | battery.LoadRam(_ram); 27 | } 28 | 29 | public void SaveRam() 30 | { 31 | _battery.SaveRam(_ram); 32 | } 33 | 34 | public bool Accepts(int address) => address >= 0x0000 && address < 0x8000 || address >= 0xA000 && address < 0xC000; 35 | 36 | public void SetByte(int address, int value) 37 | { 38 | if (address >= 0x0000 && address < 0x2000) 39 | { 40 | _ramWriteEnabled = (value & 0b1010) != 0; 41 | if (!_ramWriteEnabled) 42 | { 43 | SaveRam(); 44 | } 45 | } 46 | else if (address >= 0x2000 && address < 0x3000) 47 | { 48 | _selectedRomBank = (_selectedRomBank & 0x100) | value; 49 | } 50 | else if (address >= 0x3000 && address < 0x4000) 51 | { 52 | _selectedRomBank = (_selectedRomBank & 0x0FF) | ((value & 1) << 8); 53 | } 54 | else if (address >= 0x4000 && address < 0x6000) 55 | { 56 | var bank = value & 0x0F; 57 | if (bank < _ramBanks) 58 | { 59 | _selectedRamBank = bank; 60 | } 61 | } 62 | else if (address >= 0xA000 && address < 0xC000 && _ramWriteEnabled) 63 | { 64 | var ramAddress = GetRamAddress(address); 65 | if (ramAddress < _ram.Length) 66 | { 67 | _ram[ramAddress] = value; 68 | } 69 | } 70 | } 71 | 72 | public int GetByte(int address) 73 | { 74 | if (address >= 0x0000 && address < 0x4000) 75 | { 76 | return GetRomByte(0, address); 77 | } 78 | 79 | if (address >= 0x4000 && address < 0x8000) 80 | { 81 | return GetRomByte(_selectedRomBank, address - 0x4000); 82 | } 83 | 84 | if (address >= 0xA000 && address < 0xC000) 85 | { 86 | var ramAddress = GetRamAddress(address); 87 | if (ramAddress < _ram.Length) 88 | { 89 | return _ram[ramAddress]; 90 | } 91 | 92 | return 0xFF; 93 | } 94 | 95 | throw new ArgumentException(address.ToString("X")); 96 | } 97 | 98 | private int GetRomByte(int bank, int address) 99 | { 100 | var cartOffset = bank * 0x4000 + address; 101 | if (cartOffset < _cartridge.Length) 102 | { 103 | return _cartridge[cartOffset]; 104 | } 105 | 106 | return 0xFF; 107 | } 108 | 109 | private int GetRamAddress(int address) => _selectedRamBank * 0x2000 + (address - 0xA000); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /GB.Core/Memory/Cartridge/Type/Rom.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Memory.Cartridge.Type 2 | { 3 | internal sealed class Rom : IAddressSpace 4 | { 5 | private readonly int[] _rom; 6 | 7 | public Rom(int[] rom, CartridgeType type, int romBanks, int ramBanks) 8 | { 9 | _rom = rom; 10 | } 11 | 12 | public bool Accepts(int address) => address >= 0x0000 && address < 0x8000 || address >= 0xA000 && address < 0xC000; 13 | 14 | public void SetByte(int address, int value) 15 | { 16 | } 17 | 18 | public int GetByte(int address) 19 | { 20 | if (address >= 0x0000 && address < 0x8000) 21 | { 22 | return _rom[address]; 23 | } 24 | 25 | return 0; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /GB.Core/Memory/Dma.cs: -------------------------------------------------------------------------------- 1 | using GB.Core.Cpu; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace GB.Core.Memory 5 | { 6 | internal sealed class Dma : IAddressSpace 7 | { 8 | private readonly IAddressSpace _addressSpace; 9 | private readonly IAddressSpace _oam; 10 | private readonly SpeedMode _speedMode; 11 | 12 | private bool _transferInProgress; 13 | private bool _restarted; 14 | private int _from; 15 | private int _ticks; 16 | private int _regValue = 0xFF; 17 | 18 | public Dma(IAddressSpace addressSpace, IAddressSpace oam, SpeedMode speedMode) 19 | { 20 | _addressSpace = new DmaAddressSpace(addressSpace); 21 | _speedMode = speedMode; 22 | _oam = oam; 23 | } 24 | 25 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 26 | public bool Accepts(int address) 27 | { 28 | return address == 0xFF46; 29 | } 30 | 31 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 32 | public void Tick() 33 | { 34 | if (!_transferInProgress) return; 35 | if (++_ticks < 648 / _speedMode.GetSpeedMode()) return; 36 | 37 | _transferInProgress = false; 38 | _restarted = false; 39 | _ticks = 0; 40 | 41 | for (var i = 0; i < 0xA0; i++) 42 | { 43 | _oam.SetByte(0xFE00 + i, _addressSpace.GetByte(_from + i)); 44 | } 45 | } 46 | 47 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 48 | public void SetByte(int address, int value) 49 | { 50 | _from = value * 0x100; 51 | _restarted = IsOamBlocked(); 52 | _ticks = 0; 53 | _transferInProgress = true; 54 | _regValue = value; 55 | } 56 | 57 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 58 | public int GetByte(int address) => _regValue; 59 | 60 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 61 | public bool IsOamBlocked() => _restarted || _transferInProgress && _ticks >= 5; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /GB.Core/Memory/DmaAddressSpace.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace GB.Core.Memory 4 | { 5 | internal sealed class DmaAddressSpace : IAddressSpace 6 | { 7 | private readonly IAddressSpace _addressSpace; 8 | 9 | public DmaAddressSpace(IAddressSpace addressSpace) 10 | { 11 | _addressSpace = addressSpace; 12 | } 13 | 14 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 15 | public bool Accepts(int address) => true; 16 | 17 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 18 | public void SetByte(int address, int value) { } 19 | 20 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 21 | public int GetByte(int address) => 22 | address < 0xE000 23 | ? _addressSpace.GetByte(address) 24 | : _addressSpace.GetByte(address - 0x2000); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /GB.Core/Memory/GameboyColorRam.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Memory 2 | { 3 | internal sealed class GameboyColorRam : IAddressSpace 4 | { 5 | private readonly int[] _ram = new int[7 * 0x1000]; 6 | private int _svbk; 7 | 8 | public bool Accepts(int address) => address is 0xFF70 or >= 0xD000 and < 0xE000; 9 | 10 | public void SetByte(int address, int value) 11 | { 12 | if (address == 0xFF70) 13 | { 14 | _svbk = value; 15 | } 16 | else 17 | { 18 | _ram[Translate(address)] = value; 19 | } 20 | } 21 | 22 | public int GetByte(int address) => address == 0xFF70 ? _svbk : _ram[Translate(address)]; 23 | 24 | private int Translate(int address) 25 | { 26 | var ramBank = _svbk & 0x7; 27 | if (ramBank == 0) 28 | { 29 | ramBank = 1; 30 | } 31 | 32 | var result = address - 0xD000 + (ramBank - 1) * 0x1000; 33 | if (result < 0 || result >= _ram.Length) 34 | { 35 | throw new ArgumentException(); 36 | } 37 | 38 | return result; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /GB.Core/Memory/Hdma.cs: -------------------------------------------------------------------------------- 1 | using GB.Core.Graphics; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace GB.Core.Memory 5 | { 6 | internal sealed class Hdma : IAddressSpace 7 | { 8 | private const int Hdma1 = 0xFF51; 9 | private const int Hdma2 = 0xFF52; 10 | private const int Hdma3 = 0xFF53; 11 | private const int Hdma4 = 0xFF54; 12 | private const int Hdma5 = 0xFF55; 13 | 14 | private readonly IAddressSpace _addressSpace; 15 | private readonly Ram _hdma1234 = new Ram(Hdma1, 4); 16 | private Gpu.Mode? _gpuMode; 17 | 18 | private bool _transferInProgress; 19 | private bool _hblankTransfer; 20 | private bool _lcdEnabled; 21 | 22 | private int _length; 23 | private int _src; 24 | private int _dst; 25 | private int _tick; 26 | 27 | public Hdma(IAddressSpace addressSpace) 28 | { 29 | _addressSpace = addressSpace; 30 | } 31 | 32 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 33 | public bool Accepts(int address) => address is >= Hdma1 and <= Hdma5; 34 | 35 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 36 | public void Tick() 37 | { 38 | if (!IsTransferInProgress()) 39 | { 40 | return; 41 | } 42 | 43 | if (++_tick < 0x20) 44 | { 45 | return; 46 | } 47 | 48 | for (var j = 0; j < 0x10; j++) 49 | { 50 | _addressSpace.SetByte(_dst + j, _addressSpace.GetByte(_src + j)); 51 | } 52 | 53 | _src += 0x10; 54 | _dst += 0x10; 55 | if (_length-- == 0) 56 | { 57 | _transferInProgress = false; 58 | _length = 0x7F; 59 | } 60 | else if (_hblankTransfer) 61 | { 62 | _gpuMode = null; // wait until next HBlank 63 | } 64 | } 65 | 66 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 67 | public void SetByte(int address, int value) 68 | { 69 | if (_hdma1234.Accepts(address)) 70 | { 71 | _hdma1234.SetByte(address, value); 72 | } 73 | else if (address == Hdma5) 74 | { 75 | if (_transferInProgress) 76 | { 77 | StopTransfer(); 78 | } 79 | else 80 | { 81 | StartTransfer(value); 82 | } 83 | } 84 | } 85 | 86 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 87 | public int GetByte(int address) 88 | { 89 | if (_hdma1234.Accepts(address)) 90 | { 91 | return 0xff; 92 | } 93 | 94 | if (address == Hdma5) 95 | { 96 | return (_transferInProgress ? 0 : (1 << 7)) | _length; 97 | } 98 | 99 | throw new ArgumentException(); 100 | } 101 | 102 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 103 | public void OnGpuUpdate(Gpu.Mode newGpuMode) => _gpuMode = newGpuMode; 104 | 105 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 106 | public void OnLcdSwitch(bool lcdEnabled) => _lcdEnabled = lcdEnabled; 107 | 108 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 109 | public bool IsTransferInProgress() 110 | { 111 | if (!_transferInProgress) 112 | { 113 | return false; 114 | } 115 | 116 | if (_hblankTransfer && (_gpuMode == Gpu.Mode.HBlank || !_lcdEnabled)) 117 | { 118 | return true; 119 | } 120 | 121 | return !_hblankTransfer; 122 | } 123 | 124 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 125 | private void StartTransfer(int reg) 126 | { 127 | _hblankTransfer = (reg & (1 << 7)) != 0; 128 | _length = reg & 0x7F; 129 | 130 | _src = (_hdma1234.GetByte(Hdma1) << 8) | (_hdma1234.GetByte(Hdma2) & 0xF0); 131 | _dst = ((_hdma1234.GetByte(Hdma3) & 0x1F) << 8) | (_hdma1234.GetByte(Hdma4) & 0xF0); 132 | _src &= 0xFFF0; 133 | _dst = (_dst & 0x1FFF) | 0x8000; 134 | 135 | _transferInProgress = true; 136 | } 137 | 138 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 139 | private void StopTransfer() => _transferInProgress = false; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /GB.Core/Memory/IRegister.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Memory 2 | { 3 | internal interface IRegister 4 | { 5 | int Address { get; } 6 | RegisterType Type { get; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /GB.Core/Memory/MemoryRegisters.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace GB.Core.Memory 4 | { 5 | internal sealed class MemoryRegisters : IAddressSpace 6 | { 7 | private readonly Dictionary _registers; 8 | private readonly Dictionary _values = new Dictionary(); 9 | private readonly RegisterType[] _allowsWrite = { RegisterType.W, RegisterType.RW }; 10 | private readonly RegisterType[] _allowsRead = { RegisterType.R, RegisterType.RW }; 11 | 12 | public MemoryRegisters(params IRegister[] registers) 13 | { 14 | var map = new Dictionary(); 15 | foreach (var r in registers) 16 | { 17 | if (map.ContainsKey(r.Address)) 18 | { 19 | throw new ArgumentException($"Two registers with the same address: {r.Address}"); 20 | } 21 | 22 | map.Add(r.Address, r); 23 | _values.Add(r.Address, 0); 24 | } 25 | 26 | _registers = map; 27 | } 28 | 29 | private MemoryRegisters(MemoryRegisters original) 30 | { 31 | _registers = original._registers; 32 | _values = new Dictionary(original._values); 33 | } 34 | 35 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 36 | public int Get(IRegister reg) 37 | { 38 | return _registers.ContainsKey(reg.Address) 39 | ? _values[reg.Address] 40 | : throw new ArgumentException("Not a valid register: " + reg); 41 | } 42 | 43 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 44 | public void Put(IRegister reg, int value) 45 | { 46 | _values[reg.Address] = _registers.ContainsKey(reg.Address) 47 | ? value 48 | : throw new ArgumentException("Not a valid register: " + reg); 49 | } 50 | 51 | public MemoryRegisters Freeze() => new MemoryRegisters(this); 52 | 53 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 54 | public int PreIncrement(IRegister reg) 55 | { 56 | if (!_registers.ContainsKey(reg.Address)) 57 | { 58 | throw new ArgumentException("Not a valid register: " + reg); 59 | } 60 | 61 | var value = _values[reg.Address] + 1; 62 | _values[reg.Address] = value; 63 | return value; 64 | } 65 | 66 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 67 | public bool Accepts(int address) => _registers.ContainsKey(address); 68 | 69 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 70 | public void SetByte(int address, int value) 71 | { 72 | var regType = _registers[address].Type; 73 | if (_allowsWrite.Contains(regType)) 74 | { 75 | _values[address] = value; 76 | } 77 | } 78 | 79 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 80 | public int GetByte(int address) 81 | { 82 | var regType = _registers[address].Type; 83 | return _allowsRead.Contains(regType) ? _values[address] : 0xff; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /GB.Core/Memory/Mmu.cs: -------------------------------------------------------------------------------- 1 | using GB.Core.Controller; 2 | using GB.Core.Cpu; 3 | using GB.Core.Graphics; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace GB.Core.Memory 7 | { 8 | internal sealed class Mmu : IAddressSpace 9 | { 10 | private static readonly IAddressSpace Void = new VoidAddressSpace(); 11 | 12 | private IAddressSpace? _cartridge; 13 | private IAddressSpace? _gpu; 14 | private IAddressSpace? _ramBank0; 15 | private IAddressSpace? _ramBank1; 16 | private IAddressSpace? _joypad; 17 | private IAddressSpace? _interruptManager; 18 | private IAddressSpace? _serialPort; 19 | private IAddressSpace? _timer; 20 | private IAddressSpace? _dma; 21 | private IAddressSpace? _sound; 22 | private IAddressSpace? _speedMode; 23 | private IAddressSpace? _hdma; 24 | private IAddressSpace? _gbcRegisters; 25 | private IAddressSpace? _highRam; 26 | private IAddressSpace? _shadowRam; 27 | 28 | public void AddCartridge(Cartridge.Cartridge cartridge) 29 | { 30 | _cartridge = cartridge; 31 | } 32 | 33 | public void AddGpu(Gpu gpu) 34 | { 35 | _gpu = gpu; 36 | } 37 | 38 | public void AddJoypad(Joypad joypad) 39 | { 40 | _joypad = joypad; 41 | } 42 | 43 | public void AddInterruptManager(InterruptManager interruptManager) 44 | { 45 | _interruptManager = interruptManager; 46 | } 47 | 48 | public void AddSerialPort(Serial.SerialPort serialPort) 49 | { 50 | _serialPort = serialPort; 51 | } 52 | 53 | public void AddTimer(Timer timer) 54 | { 55 | _timer = timer; 56 | } 57 | 58 | public void AddDma(Dma dma) 59 | { 60 | _dma = dma; 61 | } 62 | 63 | public void AddSound(Sound.Sound sound) 64 | { 65 | _sound = sound; 66 | } 67 | 68 | public void AddFirstRamBank(Ram ram) 69 | { 70 | _ramBank0 = ram; 71 | } 72 | 73 | public void AddSecondRamBank(Ram ram) 74 | { 75 | _ramBank1 = ram; 76 | } 77 | 78 | public void AddSecondRamBank(GameboyColorRam ram) 79 | { 80 | _ramBank1 = ram; 81 | } 82 | 83 | public void AddSpeedMode(SpeedMode speedMode) 84 | { 85 | _speedMode = speedMode; 86 | } 87 | 88 | public void AddHdma(Hdma hdma) 89 | { 90 | _hdma = hdma; 91 | } 92 | 93 | public void AddGbcRegisters(UndocumentedGbcRegisters gbcRegisters) 94 | { 95 | _gbcRegisters = gbcRegisters; 96 | } 97 | 98 | public void AddHighRam(Ram highRam) 99 | { 100 | _highRam = highRam; 101 | } 102 | 103 | public void AddShadowRam(ShadowAddressSpace shadowRam) 104 | { 105 | _shadowRam = shadowRam; 106 | } 107 | 108 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 109 | public bool Accepts(int address) => true; 110 | 111 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 112 | public void SetByte(int address, int value) => GetSpace(address).SetByte(address, value); 113 | 114 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 115 | public int GetByte(int address) => GetSpace(address).GetByte(address); 116 | 117 | private IAddressSpace GetSpace(int address) 118 | { 119 | switch (address) 120 | { 121 | case int c when (0x000 <= c && c <= 0x7FFF) || (0xA000 <= c && c <= 0xBFFF) || c == 0xFF50: 122 | return _cartridge!; 123 | case int v when 0x8000 <= v && v <= 0x9FFF: 124 | return _gpu!; 125 | case int r when 0xC000 <= r && r <= 0xCFFF: 126 | return _ramBank0!; 127 | case int r when 0xD000 <= r && r <= 0xDFFF: 128 | return _ramBank1!; 129 | case int s when 0xE000 <= s && s <= 0xFDFF: 130 | return _shadowRam!; 131 | case int v when 0xFE00 <= v && v <= 0xFE9F: 132 | return _gpu!; // OAM RAM 133 | case 0xFF00: 134 | return _joypad!; 135 | case 0xFF01: 136 | case 0xFF02: 137 | return _serialPort!; 138 | case 0xFF04: 139 | case 0xFF05: 140 | case 0xFF06: 141 | case 0xFF07: 142 | return _timer!; 143 | case 0xFF0F: // IF = interrupt flag 144 | return _interruptManager!; 145 | case int s1 when 0xFF10 <= s1 && s1 <= 0xFF14: 146 | case int s2 when 0xFF16 <= s2 && s2 <= 0xFF19: 147 | case int s3 when 0xFF1A <= s3 && s3 <= 0xFF1E: 148 | case int s4 when 0xFF20 <= s4 && s4 <= 0xFF26: 149 | case int s5 when 0xFF30 <= s5 && s5 <= 0xFF3F: 150 | return _sound!; 151 | case 0xFF40: // LCD Control 152 | case 0xFF41: // LCD Status 153 | case 0xFF42: // Scroll Y 154 | case 0xFF43: // Scroll X 155 | case 0xFF44: // LCD Y coord 156 | case 0xFF45: // LCD Y compare 157 | return _gpu!; 158 | case 0xFF46: 159 | return _dma!; 160 | case 0xFF47: // BG Palette 161 | case 0xFF48: // OBJ Palette 0 162 | case 0xFF49: // OBJ Palette 1 163 | case 0xFF4A: // Window Y pos 164 | case 0xFF4B: // Window X pos 165 | return _gpu!; 166 | case 0xFF4D: 167 | return _speedMode ?? Void; 168 | case int h when 0xFF51 <= h && h <= 0xFF55: 169 | return _hdma ?? Void; 170 | case 0xFF68: // Background color palette spec 171 | case 0xFF69: // Background color palette data 172 | case 0xFF6A: // Object color palette spec 173 | case 0xFF6B: // Object color palette data 174 | return _gpu!; 175 | case 0xFF6C: 176 | case 0xFF72: 177 | case 0xFF73: 178 | case 0xFF74: 179 | case 0xFF75: 180 | case 0xFF76: 181 | case 0xFF77: 182 | return _gbcRegisters ?? Void; 183 | case int h when 0xFF80 <= h && h <= 0xFFFE: 184 | return _highRam!; 185 | case 0xFFFF: // IE = interrupt enable 186 | return _interruptManager!; 187 | } 188 | 189 | return Void; 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /GB.Core/Memory/Ram.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace GB.Core.Memory 4 | { 5 | internal sealed class Ram : IAddressSpace 6 | { 7 | private readonly int[] _space; 8 | private readonly int _length; 9 | private readonly int _offset; 10 | 11 | public Ram(int offset, int length) 12 | { 13 | _space = new int[length]; 14 | _length = length; 15 | _offset = offset; 16 | } 17 | 18 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 19 | public bool Accepts(int address) => address >= _offset && address < _offset + _length; 20 | 21 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 22 | public void SetByte(int address, int value) => _space[address - _offset] = value; 23 | 24 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 25 | public int GetByte(int address) 26 | { 27 | var index = address - _offset; 28 | if (index < 0 || index >= _space.Length) 29 | { 30 | throw new IndexOutOfRangeException("Address: " + address); 31 | } 32 | 33 | return _space[index]; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /GB.Core/Memory/RegisterType.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Memory 2 | { 3 | internal enum RegisterType 4 | { 5 | R, 6 | W, 7 | RW 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /GB.Core/Memory/ShadowAddressSpace.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace GB.Core.Memory 4 | { 5 | internal sealed class ShadowAddressSpace : IAddressSpace 6 | { 7 | private readonly IAddressSpace _addressSpace; 8 | private readonly int _echoStart; 9 | private readonly int _targetStart; 10 | private readonly int _length; 11 | 12 | public ShadowAddressSpace(IAddressSpace addressSpace, int echoStart, int targetStart, int length) 13 | { 14 | _addressSpace = addressSpace; 15 | _echoStart = echoStart; 16 | _targetStart = targetStart; 17 | _length = length; 18 | } 19 | 20 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 21 | public bool Accepts(int address) => address >= _echoStart && address < _echoStart + _length; 22 | 23 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 24 | public void SetByte(int address, int value) => _addressSpace.SetByte(Translate(address), value); 25 | 26 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 27 | public int GetByte(int address) => _addressSpace.GetByte(Translate(address)); 28 | 29 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 30 | private int Translate(int address) => GetRelative(address) + _targetStart; 31 | 32 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 33 | private int GetRelative(int address) 34 | { 35 | var i = address - _echoStart; 36 | if (i < 0 || i >= _length) 37 | { 38 | throw new ArgumentException(); 39 | } 40 | 41 | return i; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /GB.Core/Memory/UndocumentedGbcRegisters.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Memory 2 | { 3 | internal sealed class UndocumentedGbcRegisters : IAddressSpace 4 | { 5 | private readonly Ram _ram = new(0xFF72, 6); 6 | private int _xFF6C; 7 | 8 | public UndocumentedGbcRegisters() 9 | { 10 | _xFF6C = 0xFE; 11 | _ram.SetByte(0xFF74, 0xFF); 12 | _ram.SetByte(0xFF75, 0x8F); 13 | } 14 | 15 | public bool Accepts(int address) => address == 0xFF6C || _ram.Accepts(address); 16 | 17 | public void SetByte(int address, int value) 18 | { 19 | switch (address) 20 | { 21 | case 0xFF6C: 22 | _xFF6C = 0xFE | (value & 1); 23 | break; 24 | 25 | case 0xFF72: 26 | case 0xFF73: 27 | case 0xFF74: 28 | _ram.SetByte(address, value); 29 | break; 30 | 31 | case 0xFF75: 32 | _ram.SetByte(address, 0x8F | (value & 0b01110000)); 33 | break; 34 | } 35 | } 36 | 37 | public int GetByte(int address) 38 | { 39 | if (address == 0xFF6C) 40 | { 41 | return _xFF6C; 42 | } 43 | 44 | if (!_ram.Accepts(address)) 45 | { 46 | throw new ArgumentException(); 47 | } 48 | 49 | return _ram.GetByte(address); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /GB.Core/Memory/VoidAddressSpace.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace GB.Core.Memory 4 | { 5 | internal sealed class VoidAddressSpace : IAddressSpace 6 | { 7 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 8 | public bool Accepts(int address) => true; 9 | 10 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 11 | public void SetByte(int address, int value) 12 | { 13 | if (address < 0 || address > 0xFFFF) 14 | { 15 | throw new ArgumentException($"Invalid address: 0x{address:X}"); 16 | } 17 | } 18 | 19 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 20 | public int GetByte(int address) 21 | { 22 | if (address < 0 || address > 0xFFFF) 23 | { 24 | throw new ArgumentException($"Invalid address: 0x{address:X}"); 25 | } 26 | 27 | return 0xFF; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /GB.Core/Serial/ISerialEndpoint.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Serial 2 | { 3 | public interface ISerialEndpoint 4 | { 5 | bool ExternalClockPulsed(); 6 | int Transfer(int outgoing); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /GB.Core/Serial/NullSerialEndpoint.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Serial 2 | { 3 | public sealed class NullSerialEndpoint : ISerialEndpoint 4 | { 5 | public bool ExternalClockPulsed() => false; 6 | public int Transfer(int outgoing) 7 | { 8 | return (outgoing << 1) & 0xFF; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /GB.Core/Serial/SerialPort.cs: -------------------------------------------------------------------------------- 1 | using GB.Core.Cpu; 2 | using System.Diagnostics; 3 | 4 | namespace GB.Core.Serial 5 | { 6 | internal sealed class SerialPort : IAddressSpace 7 | { 8 | private readonly ISerialEndpoint _serialEndpoint; 9 | private readonly InterruptManager _interruptManager; 10 | private readonly SpeedMode _speedMode; 11 | private readonly bool _gbc; 12 | private int _sb; 13 | private int _sc; 14 | private int _divider; 15 | private int _shiftClock; 16 | 17 | public SerialPort(InterruptManager interruptManager, ISerialEndpoint serialEndpoint, SpeedMode speedMode, bool gbc) 18 | { 19 | _interruptManager = interruptManager; 20 | _serialEndpoint = serialEndpoint; 21 | _speedMode = speedMode; 22 | _gbc = gbc; 23 | } 24 | 25 | public void Tick() 26 | { 27 | if (!TransferInProgress) 28 | { 29 | return; 30 | } 31 | 32 | if (++_divider >= Gameboy.TicksPerSec / 8192 / (FastMode ? 4 : 1) / _speedMode.GetSpeedMode()) 33 | { 34 | var clockPulsed = false; 35 | if (InternalClockEnabled || _serialEndpoint.ExternalClockPulsed()) 36 | { 37 | _shiftClock++; 38 | clockPulsed = true; 39 | } 40 | 41 | if (_shiftClock >= 8) 42 | { 43 | TransferInProgress = false; 44 | _interruptManager.RequestInterrupt(InterruptManager.InterruptType.Serial); 45 | return; 46 | } 47 | 48 | if (clockPulsed) 49 | { 50 | try 51 | { 52 | _sb = _serialEndpoint.Transfer(_sb); 53 | } 54 | catch (IOException e) 55 | { 56 | Debug.WriteLine($"Can't transfer byte {e}"); 57 | _sb = 0; 58 | } 59 | } 60 | 61 | _divider = 0; 62 | } 63 | } 64 | 65 | public bool Accepts(int address) 66 | { 67 | return address is 0xFF01 or 0xFF02; 68 | } 69 | 70 | public void SetByte(int address, int value) 71 | { 72 | if (address == 0xFF01 && !TransferInProgress) 73 | { 74 | _sb = value; 75 | } 76 | else if (address == 0xFF02) 77 | { 78 | TransferInProgress = value.GetBit(7); 79 | FastMode = value.GetBit(1); 80 | InternalClockEnabled = value.GetBit(0); 81 | } 82 | } 83 | 84 | public int GetByte(int address) 85 | { 86 | if (address == 0xFF01) 87 | { 88 | return TransferInProgress ? 0x00 : _sb; 89 | } 90 | if (address == 0xFF02) 91 | { 92 | return _sc | (_gbc ? 0b01111100 : 0b01111110); 93 | } 94 | throw new ArgumentException(); 95 | } 96 | 97 | private bool TransferInProgress 98 | { 99 | get => (_sc & (1 << 7)) != 0; 100 | set 101 | { 102 | if (value) 103 | { 104 | _sc = _sc.SetBit(7); 105 | _divider = 0; 106 | _shiftClock = 0; 107 | } 108 | else 109 | { 110 | _sc = _sc.ClearBit(7); 111 | } 112 | } 113 | } 114 | 115 | private bool FastMode 116 | { 117 | get => (_sc & 2) != 0; 118 | set 119 | { 120 | if (value) 121 | { 122 | _sc = _sc.SetBit(1); 123 | } 124 | else 125 | { 126 | _sc = _sc.ClearBit(1); 127 | } 128 | } 129 | } 130 | 131 | private bool InternalClockEnabled 132 | { 133 | get => (_sc & 1) != 0; 134 | set 135 | { 136 | if (value) 137 | { 138 | _sc = _sc.SetBit(0); 139 | } 140 | else 141 | { 142 | _sc = _sc.ClearBit(0); 143 | } 144 | } 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /GB.Core/Sound/FrequencySweep.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Sound 2 | { 3 | internal sealed class FrequencySweep 4 | { 5 | private static readonly int Divider = Gameboy.TicksPerSec / 128; 6 | 7 | // sweep parameters 8 | private int _period; 9 | private bool _negate; 10 | private int _shift; 11 | 12 | // current process variables 13 | private int _timer; 14 | private int _shadowFreq; 15 | private int _nr13; 16 | private int _nr14; 17 | private int _i; 18 | private bool _overflow; 19 | private bool _counterEnabled; 20 | private bool _negging; 21 | 22 | public void Start() 23 | { 24 | _counterEnabled = false; 25 | _i = 8192; 26 | } 27 | 28 | public void Trigger() 29 | { 30 | _negging = false; 31 | _overflow = false; 32 | 33 | _shadowFreq = _nr13 | ((_nr14 & 0b111) << 8); 34 | _timer = _period == 0 ? 8 : _period; 35 | _counterEnabled = _period != 0 || _shift != 0; 36 | 37 | if (_shift > 0) 38 | { 39 | Calculate(); 40 | } 41 | } 42 | 43 | public void SetNr10(int value) 44 | { 45 | _period = (value >> 4) & 0b111; 46 | _negate = (value & (1 << 3)) != 0; 47 | _shift = value & 0b111; 48 | if (_negging && !_negate) 49 | { 50 | _overflow = true; 51 | } 52 | } 53 | 54 | public void SetNr13(int value) => _nr13 = value; 55 | 56 | public void SetNr14(int value) 57 | { 58 | _nr14 = value; 59 | if ((value & (1 << 7)) != 0) 60 | { 61 | Trigger(); 62 | } 63 | } 64 | 65 | public int GetNr13() => _nr13; 66 | public int GetNr14() => _nr14; 67 | 68 | public void Tick() 69 | { 70 | _i++; 71 | 72 | if (_i != Divider) return; 73 | 74 | _i = 0; 75 | 76 | if (!_counterEnabled) return; 77 | 78 | _timer--; 79 | 80 | if (_timer != 0) return; 81 | 82 | _timer = _period == 0 ? 8 : _period; 83 | 84 | if (_period == 0) return; 85 | 86 | var newFreq = Calculate(); 87 | 88 | if (_overflow || _shift == 0) return; 89 | 90 | _shadowFreq = newFreq; 91 | _nr13 = _shadowFreq & 0xff; 92 | _nr14 = (_shadowFreq & 0x700) >> 8; 93 | 94 | Calculate(); 95 | } 96 | 97 | private int Calculate() 98 | { 99 | var freq = _shadowFreq >> _shift; 100 | if (_negate) 101 | { 102 | freq = _shadowFreq - freq; 103 | _negging = true; 104 | } 105 | else 106 | { 107 | freq = _shadowFreq + freq; 108 | } 109 | 110 | if (freq > 2047) 111 | { 112 | _overflow = true; 113 | } 114 | 115 | return freq; 116 | } 117 | 118 | public bool IsEnabled() => !_overflow; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /GB.Core/Sound/ISoundOutput.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Sound 2 | { 3 | public interface ISoundOutput 4 | { 5 | void Start(); 6 | void Stop(); 7 | void Play(int left, int right); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /GB.Core/Sound/LengthCounter.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Sound 2 | { 3 | internal sealed class LengthCounter 4 | { 5 | private long _i; 6 | private readonly int _divider = Gameboy.TicksPerSec / 256; 7 | private readonly int _fullLength; 8 | 9 | public bool Enabled { get; private set; } 10 | public int Length { get; private set; } 11 | 12 | public LengthCounter(int fullLength) 13 | { 14 | _fullLength = fullLength; 15 | } 16 | 17 | public void Start() 18 | { 19 | _i = 8192; 20 | } 21 | 22 | public void Tick() 23 | { 24 | _i++; 25 | 26 | if (_i == _divider) 27 | { 28 | _i = 0; 29 | if (Enabled && Length > 0) 30 | { 31 | Length--; 32 | } 33 | } 34 | } 35 | 36 | public void SetLength(int len) 37 | { 38 | Length = len == 0 ? _fullLength : len; 39 | } 40 | 41 | public void SetNr4(int value) 42 | { 43 | var enable = (value & (1 << 6)) != 0; 44 | var trigger = (value & (1 << 7)) != 0; 45 | 46 | if (Enabled) 47 | { 48 | if (Length == 0 && trigger) 49 | { 50 | if (enable && _i < _divider / 2) 51 | { 52 | SetLength(_fullLength - 1); 53 | } 54 | else 55 | { 56 | SetLength(_fullLength); 57 | } 58 | } 59 | } 60 | else if (enable) 61 | { 62 | if (Length > 0 && _i < _divider / 2) 63 | { 64 | Length--; 65 | } 66 | 67 | if (Length == 0 && trigger && _i < _divider / 2) 68 | { 69 | SetLength(_fullLength - 1); 70 | } 71 | } 72 | else 73 | { 74 | if (Length == 0 && trigger) 75 | { 76 | SetLength(_fullLength); 77 | } 78 | } 79 | 80 | Enabled = enable; 81 | } 82 | 83 | public override string ToString() 84 | { 85 | return $"LengthCounter[l={Length},f={_fullLength},c={_i},{(Enabled ? "enabled" : "disabled")}]"; 86 | } 87 | 88 | public void Reset() 89 | { 90 | Enabled = true; 91 | _i = 0; 92 | Length = 0; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /GB.Core/Sound/Lfsr.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Sound 2 | { 3 | internal sealed class Lfsr 4 | { 5 | public int Value { get; private set; } 6 | 7 | public Lfsr() => Reset(); 8 | public void Start() => Reset(); 9 | public void Reset() => Value = 0x7FFF; 10 | 11 | public int NextBit(bool widthMode7) 12 | { 13 | var xor = ((Value & 1) ^ ((Value & 2) >> 1)); 14 | Value >>= 1; 15 | Value |= xor << 14; 16 | 17 | if (widthMode7) 18 | { 19 | Value |= (xor << 6); 20 | Value &= 0x7F; 21 | } 22 | 23 | return 1 & ~Value; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /GB.Core/Sound/NullSoundOutput.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Sound 2 | { 3 | public sealed class NullSoundOutput : ISoundOutput 4 | { 5 | public void Start() 6 | { 7 | } 8 | 9 | public void Stop() 10 | { 11 | } 12 | 13 | public void Play(int left, int right) 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /GB.Core/Sound/PolynomialCounter.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Sound 2 | { 3 | internal sealed class PolynomialCounter 4 | { 5 | private int _i; 6 | private int _shiftedDivisor; 7 | 8 | public void SetNr43(int value) 9 | { 10 | var clockShift = value >> 4; 11 | 12 | var divisor = (value & 0b111) switch 13 | { 14 | 0 => 8, 15 | 1 => 16, 16 | 2 => 32, 17 | 3 => 48, 18 | 4 => 64, 19 | 5 => 80, 20 | 6 => 96, 21 | 7 => 112, 22 | _ => throw new InvalidOperationException() 23 | }; 24 | 25 | _shiftedDivisor = divisor << clockShift; 26 | _i = 1; 27 | } 28 | 29 | public bool Tick() 30 | { 31 | if (--_i == 0) 32 | { 33 | _i = _shiftedDivisor; 34 | return true; 35 | } 36 | else 37 | { 38 | return false; 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /GB.Core/Sound/Sound.cs: -------------------------------------------------------------------------------- 1 | using GB.Core.Memory; 2 | 3 | namespace GB.Core.Sound 4 | { 5 | internal sealed class Sound : IAddressSpace 6 | { 7 | private static readonly int[] Masks = { 8 | 0x80, 0x3F, 0x00, 0xFF, 0xBF, 9 | 0xFF, 0x3F, 0x00, 0xFF, 0xBF, 10 | 0x7F, 0xFF, 0x9F, 0xFF, 0xBF, 11 | 0xFF, 0xFF, 0x00, 0x00, 0xBF, 12 | 0x00, 0x00, 0x70, 13 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 14 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 15 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 16 | }; 17 | 18 | private readonly SoundModeBase[] _allModes = new SoundModeBase[4]; 19 | private readonly int[] _channels = new int[4]; 20 | private readonly Ram _ram = new(0xFF24, 0x03); 21 | private readonly ISoundOutput _output; 22 | private readonly bool[] _overridenEnabled = { true, true, true, true }; 23 | private bool _enabled; 24 | 25 | public Sound(ISoundOutput output, bool gbc) 26 | { 27 | _allModes[0] = new SoundMode1(gbc); 28 | _allModes[1] = new SoundMode2(gbc); 29 | _allModes[2] = new SoundMode3(gbc); 30 | _allModes[3] = new SoundMode4(gbc); 31 | _output = output; 32 | } 33 | 34 | public void Tick() 35 | { 36 | if (!_enabled) 37 | { 38 | return; 39 | } 40 | 41 | for (var i = 0; i < _allModes.Length; i++) 42 | { 43 | var abstractSoundMode = _allModes[i]; 44 | var channel = abstractSoundMode.TickChannel(); 45 | _channels[i] = channel; 46 | } 47 | 48 | var selection = _ram.GetByte(0xFF25); 49 | var left = 0; 50 | var right = 0; 51 | for (var i = 0; i < 4; i++) 52 | { 53 | if (!_overridenEnabled[i]) 54 | { 55 | continue; 56 | } 57 | 58 | if ((selection & (1 << i + 4)) != 0) 59 | { 60 | left += _channels[i]; 61 | } 62 | 63 | if ((selection & (1 << i)) != 0) 64 | { 65 | right += _channels[i]; 66 | } 67 | } 68 | 69 | var volumes = _ram.GetByte(0xFF24); 70 | left *= (volumes >> 4) & 0b111; 71 | right *= volumes & 0b111; 72 | 73 | _output.Play(left, right); 74 | } 75 | 76 | private IAddressSpace? GetAddressSpace(int address) 77 | { 78 | foreach (var m in _allModes) 79 | { 80 | if (m.Accepts(address)) 81 | { 82 | return m; 83 | } 84 | } 85 | 86 | if (_ram.Accepts(address)) 87 | { 88 | return _ram; 89 | } 90 | 91 | return null; 92 | } 93 | 94 | public bool Accepts(int address) => GetAddressSpace(address) != null; 95 | 96 | public void SetByte(int address, int value) 97 | { 98 | if (address == 0xFF26) 99 | { 100 | if ((value & (1 << 7)) == 0) 101 | { 102 | if (_enabled) 103 | { 104 | _enabled = false; 105 | Stop(); 106 | } 107 | } 108 | else 109 | { 110 | if (!_enabled) 111 | { 112 | _enabled = true; 113 | Start(); 114 | } 115 | } 116 | 117 | return; 118 | } 119 | 120 | var s = GetAddressSpace(address); 121 | if (s == null) 122 | { 123 | throw new ArgumentException(); 124 | } 125 | 126 | s.SetByte(address, value); 127 | } 128 | 129 | 130 | public int GetByte(int address) 131 | { 132 | int result; 133 | if (address == 0xFF26) 134 | { 135 | result = 0; 136 | for (var i = 0; i < _allModes.Length; i++) 137 | { 138 | result |= _allModes[i].IsEnabled() ? (1 << i) : 0; 139 | } 140 | 141 | result |= _enabled ? (1 << 7) : 0; 142 | } 143 | else 144 | { 145 | result = GetUnmaskedByte(address); 146 | } 147 | 148 | return result | Masks[address - 0xFF10]; 149 | } 150 | 151 | private int GetUnmaskedByte(int address) 152 | { 153 | var s = GetAddressSpace(address); 154 | if (s == null) 155 | { 156 | throw new ArgumentException(); 157 | } 158 | 159 | return s.GetByte(address); 160 | } 161 | 162 | private void Start() 163 | { 164 | for (var i = 0xFF10; i <= 0xFF25; i++) 165 | { 166 | var v = 0; 167 | // lengths should be preserved 168 | if (i == 0xFF11 || i == 0xFF16 || i == 0xFF20) 169 | { 170 | // channel 1, 2, 4 lengths 171 | v = GetUnmaskedByte(i) & 0b00111111; 172 | } 173 | else if (i == 0xFF1B) 174 | { 175 | // channel 3 length 176 | v = GetUnmaskedByte(i); 177 | } 178 | 179 | SetByte(i, v); 180 | } 181 | 182 | foreach (var m in _allModes) 183 | { 184 | m.Start(); 185 | } 186 | 187 | _output.Start(); 188 | } 189 | 190 | private void Stop() 191 | { 192 | _output.Stop(); 193 | foreach (var s in _allModes) 194 | { 195 | s.Stop(); 196 | } 197 | } 198 | 199 | public void ToggleChannel(int i) => _overridenEnabled[i] = !_overridenEnabled[i]; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /GB.Core/Sound/SoundMode1.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Sound 2 | { 3 | internal sealed class SoundMode1 : SoundModeBase 4 | { 5 | private int _freqDivider; 6 | private int _lastOutput; 7 | private int _i; 8 | private readonly FrequencySweep _frequencySweep; 9 | private readonly VolumeEnvelope _volumeEnvelope; 10 | 11 | public SoundMode1(bool gbc) : base(0xFF10, 64, gbc) 12 | { 13 | _frequencySweep = new FrequencySweep(); 14 | _volumeEnvelope = new VolumeEnvelope(); 15 | } 16 | 17 | public override void Start() 18 | { 19 | _i = 0; 20 | if (Gbc) 21 | { 22 | Length.Reset(); 23 | } 24 | 25 | Length.Start(); 26 | _frequencySweep.Start(); 27 | _volumeEnvelope.Start(); 28 | } 29 | 30 | protected override void Trigger() 31 | { 32 | _i = 0; 33 | _freqDivider = 1; 34 | _volumeEnvelope.Trigger(); 35 | } 36 | 37 | public override int TickChannel() 38 | { 39 | _volumeEnvelope.Tick(); 40 | 41 | var e = UpdateLength(); 42 | e = UpdateSweep() && e; 43 | e = DacEnabled && e; 44 | if (!e) 45 | { 46 | return 0; 47 | } 48 | 49 | if (--_freqDivider == 0) 50 | { 51 | ResetFreqDivider(); 52 | _lastOutput = ((GetDuty() & (1 << _i)) >> _i); 53 | _i = (_i + 1) % 8; 54 | } 55 | 56 | return _lastOutput * _volumeEnvelope.GetVolume(); 57 | } 58 | 59 | protected override void SetNr0(int value) 60 | { 61 | base.SetNr0(value); 62 | _frequencySweep.SetNr10(value); 63 | } 64 | 65 | protected override void SetNr1(int value) 66 | { 67 | base.SetNr1(value); 68 | Length.SetLength(64 - (value & 0b00111111)); 69 | } 70 | 71 | protected override void SetNr2(int value) 72 | { 73 | base.SetNr2(value); 74 | _volumeEnvelope.SetNr2(value); 75 | DacEnabled = (value & 0b11111000) != 0; 76 | ChannelEnabled &= DacEnabled; 77 | } 78 | 79 | protected override void SetNr3(int value) 80 | { 81 | base.SetNr3(value); 82 | _frequencySweep.SetNr13(value); 83 | } 84 | 85 | protected override void SetNr4(int value) 86 | { 87 | base.SetNr4(value); 88 | _frequencySweep.SetNr14(value); 89 | } 90 | 91 | protected override int GetNr3() 92 | { 93 | return _frequencySweep.GetNr13(); 94 | } 95 | 96 | protected override int GetNr4() 97 | { 98 | return (base.GetNr4() & 0b11111000) | (_frequencySweep.GetNr14() & 0b00000111); 99 | } 100 | 101 | private int GetDuty() 102 | { 103 | switch (GetNr1() >> 6) 104 | { 105 | case 0: 106 | return 0b00000001; 107 | case 1: 108 | return 0b10000001; 109 | case 2: 110 | return 0b10000111; 111 | case 3: 112 | return 0b01111110; 113 | default: 114 | throw new InvalidOperationException("Illegal state exception"); 115 | } 116 | } 117 | 118 | private void ResetFreqDivider() 119 | { 120 | _freqDivider = GetFrequency() * 4; 121 | } 122 | 123 | private bool UpdateSweep() 124 | { 125 | _frequencySweep.Tick(); 126 | if (ChannelEnabled && !_frequencySweep.IsEnabled()) 127 | { 128 | ChannelEnabled = false; 129 | } 130 | 131 | return ChannelEnabled; 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /GB.Core/Sound/SoundMode2.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Sound 2 | { 3 | internal sealed class SoundMode2 : SoundModeBase 4 | { 5 | private int _freqDivider; 6 | private int _lastOutput; 7 | private int _i; 8 | private readonly VolumeEnvelope _volumeEnvelope; 9 | 10 | public SoundMode2(bool gbc) : base(0xFF15, 64, gbc) 11 | { 12 | _volumeEnvelope = new VolumeEnvelope(); 13 | } 14 | 15 | public override void Start() 16 | { 17 | _i = 0; 18 | if (Gbc) 19 | { 20 | Length.Reset(); 21 | } 22 | 23 | Length.Start(); 24 | _volumeEnvelope.Start(); 25 | } 26 | 27 | protected override void Trigger() 28 | { 29 | _i = 0; 30 | _freqDivider = 1; 31 | _volumeEnvelope.Trigger(); 32 | } 33 | 34 | public override int TickChannel() 35 | { 36 | _volumeEnvelope.Tick(); 37 | 38 | var e = UpdateLength(); 39 | e = DacEnabled && e; 40 | if (!e) 41 | { 42 | return 0; 43 | } 44 | 45 | if (--_freqDivider == 0) 46 | { 47 | ResetFreqDivider(); 48 | _lastOutput = ((GetDuty() & (1 << _i)) >> _i); 49 | _i = (_i + 1) % 8; 50 | } 51 | 52 | return _lastOutput * _volumeEnvelope.GetVolume(); 53 | } 54 | 55 | protected override void SetNr1(int value) 56 | { 57 | base.SetNr1(value); 58 | Length.SetLength(64 - (value & 0b00111111)); 59 | } 60 | 61 | protected override void SetNr2(int value) 62 | { 63 | base.SetNr2(value); 64 | _volumeEnvelope.SetNr2(value); 65 | DacEnabled = (value & 0b11111000) != 0; 66 | ChannelEnabled &= DacEnabled; 67 | } 68 | 69 | private int GetDuty() 70 | { 71 | var i = GetNr1() >> 6; 72 | return i switch 73 | { 74 | 0 => 0b00000001, 75 | 1 => 0b10000001, 76 | 2 => 0b10000111, 77 | 3 => 0b01111110, 78 | _ => throw new InvalidOperationException("Illegal operation") 79 | }; 80 | } 81 | 82 | private void ResetFreqDivider() 83 | { 84 | _freqDivider = GetFrequency() * 4; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /GB.Core/Sound/SoundMode3.cs: -------------------------------------------------------------------------------- 1 | using GB.Core.Memory; 2 | 3 | namespace GB.Core.Sound 4 | { 5 | internal sealed class SoundMode3 : SoundModeBase 6 | { 7 | private static readonly int[] DmgWave = 8 | { 9 | 0x84, 0x40, 0x43, 0xAA, 0x2D, 0x78, 0x92, 0x3C, 10 | 0x60, 0x59, 0x59, 0xB0, 0x34, 0xB8, 0x2E, 0xDA 11 | }; 12 | 13 | private static readonly int[] CgbWave = 14 | { 15 | 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 16 | 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF 17 | }; 18 | 19 | private readonly Ram _waveRam = new Ram(0xFF30, 0x10); 20 | private int _freqDivider; 21 | private int _lastOutput; 22 | private int _i; 23 | private int _ticksSinceRead = 65536; 24 | private int _lastReadAddress; 25 | private int _buffer; 26 | private bool _triggered; 27 | 28 | public SoundMode3(bool gbc) : base(0xFF1A, 256, gbc) 29 | { 30 | foreach (var v in gbc ? CgbWave : DmgWave) 31 | { 32 | _waveRam.SetByte(0xFF30, v); 33 | } 34 | } 35 | 36 | public override bool Accepts(int address) => _waveRam.Accepts(address) || base.Accepts(address); 37 | 38 | public override int GetByte(int address) 39 | { 40 | if (!_waveRam.Accepts(address)) 41 | { 42 | return base.GetByte(address); 43 | } 44 | 45 | if (!IsEnabled()) 46 | { 47 | return _waveRam.GetByte(address); 48 | } 49 | 50 | if (_waveRam.Accepts(_lastReadAddress) && (Gbc || _ticksSinceRead < 2)) 51 | { 52 | return _waveRam.GetByte(_lastReadAddress); 53 | } 54 | 55 | return 0xff; 56 | } 57 | 58 | 59 | public override void SetByte(int address, int value) 60 | { 61 | if (!_waveRam.Accepts(address)) 62 | { 63 | base.SetByte(address, value); 64 | return; 65 | } 66 | 67 | if (!IsEnabled()) 68 | { 69 | _waveRam.SetByte(address, value); 70 | } 71 | else if (_waveRam.Accepts(_lastReadAddress) && (Gbc || _ticksSinceRead < 2)) 72 | { 73 | _waveRam.SetByte(_lastReadAddress, value); 74 | } 75 | } 76 | 77 | protected override void SetNr0(int value) 78 | { 79 | base.SetNr0(value); 80 | DacEnabled = (value & (1 << 7)) != 0; 81 | ChannelEnabled &= DacEnabled; 82 | } 83 | 84 | protected override void SetNr1(int value) 85 | { 86 | base.SetNr1(value); 87 | Length.SetLength(256 - value); 88 | } 89 | 90 | protected override void SetNr4(int value) 91 | { 92 | if (!Gbc && (value & (1 << 7)) != 0) 93 | { 94 | if (IsEnabled() && _freqDivider == 2) 95 | { 96 | var pos = _i / 2; 97 | if (pos < 4) 98 | { 99 | _waveRam.SetByte(0xFF30, _waveRam.GetByte(0xFF30 + pos)); 100 | } 101 | else 102 | { 103 | pos &= ~3; 104 | for (var j = 0; j < 4; j++) 105 | { 106 | _waveRam.SetByte(0xFF30 + j, _waveRam.GetByte(0xFF30 + ((pos + j) % 0x10))); 107 | } 108 | } 109 | } 110 | } 111 | 112 | base.SetNr4(value); 113 | } 114 | 115 | public override void Start() 116 | { 117 | _i = 0; 118 | _buffer = 0; 119 | if (Gbc) 120 | { 121 | Length.Reset(); 122 | } 123 | 124 | Length.Start(); 125 | } 126 | 127 | protected override void Trigger() 128 | { 129 | _i = 0; 130 | _freqDivider = 6; 131 | _triggered = !Gbc; 132 | if (Gbc) 133 | { 134 | GetWaveEntry(); 135 | } 136 | } 137 | 138 | public override int TickChannel() 139 | { 140 | _ticksSinceRead++; 141 | if (!UpdateLength()) 142 | { 143 | return 0; 144 | } 145 | 146 | if (!DacEnabled) 147 | { 148 | return 0; 149 | } 150 | 151 | if ((GetNr0() & (1 << 7)) == 0) 152 | { 153 | return 0; 154 | } 155 | 156 | _freqDivider--; 157 | 158 | if (_freqDivider == 0) 159 | { 160 | ResetFreqDivider(); 161 | if (_triggered) 162 | { 163 | _lastOutput = (_buffer >> 4) & 0x0F; 164 | _triggered = false; 165 | } 166 | else 167 | { 168 | _lastOutput = GetWaveEntry(); 169 | } 170 | 171 | _i = (_i + 1) % 32; 172 | } 173 | 174 | return _lastOutput; 175 | } 176 | 177 | private int GetVolume() => (GetNr2() >> 5) & 0b11; 178 | 179 | private int GetWaveEntry() 180 | { 181 | _ticksSinceRead = 0; 182 | _lastReadAddress = 0xFF30 + _i / 2; 183 | _buffer = _waveRam.GetByte(_lastReadAddress); 184 | 185 | var b = _buffer; 186 | if (_i % 2 == 0) 187 | { 188 | b = (b >> 4) & 0x0F; 189 | } 190 | else 191 | { 192 | b &= 0x0F; 193 | } 194 | 195 | return GetVolume() switch 196 | { 197 | 0 => 0, 198 | 1 => b, 199 | 2 => b >> 1, 200 | 3 => b >> 2, 201 | _ => throw new InvalidOperationException("Illegal state") 202 | }; 203 | } 204 | 205 | private void ResetFreqDivider() => _freqDivider = GetFrequency() * 2; 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /GB.Core/Sound/SoundMode4.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Sound 2 | { 3 | internal sealed class SoundMode4 : SoundModeBase 4 | { 5 | private int _lastResult; 6 | private readonly VolumeEnvelope _volumeEnvelope; 7 | private readonly PolynomialCounter _polynomialCounter; 8 | private readonly Lfsr _lfsr = new(); 9 | 10 | public SoundMode4(bool gbc) : base(0xFF1F, 64, gbc) 11 | { 12 | _volumeEnvelope = new VolumeEnvelope(); 13 | _polynomialCounter = new PolynomialCounter(); 14 | } 15 | 16 | public override void Start() 17 | { 18 | if (Gbc) 19 | { 20 | Length.Reset(); 21 | } 22 | 23 | Length.Start(); 24 | _lfsr.Start(); 25 | _volumeEnvelope.Start(); 26 | } 27 | 28 | 29 | protected override void Trigger() 30 | { 31 | _lfsr.Reset(); 32 | _volumeEnvelope.Trigger(); 33 | } 34 | 35 | public override int TickChannel() 36 | { 37 | _volumeEnvelope.Tick(); 38 | 39 | if (!UpdateLength()) 40 | { 41 | return 0; 42 | } 43 | 44 | if (!DacEnabled) 45 | { 46 | return 0; 47 | } 48 | 49 | if (_polynomialCounter.Tick()) 50 | { 51 | _lastResult = _lfsr.NextBit(((Nr3 >> 3) & 1) != 0); 52 | } 53 | 54 | return _lastResult * _volumeEnvelope.GetVolume(); 55 | } 56 | 57 | protected override void SetNr1(int value) 58 | { 59 | base.SetNr1(value); 60 | Length.SetLength(64 - (value & 0b00111111)); 61 | } 62 | 63 | protected override void SetNr2(int value) 64 | { 65 | base.SetNr2(value); 66 | _volumeEnvelope.SetNr2(value); 67 | DacEnabled = (value & 0b11111000) != 0; 68 | ChannelEnabled &= DacEnabled; 69 | } 70 | 71 | protected override void SetNr3(int value) 72 | { 73 | base.SetNr3(value); 74 | _polynomialCounter.SetNr43(value); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /GB.Core/Sound/SoundModeBase.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Sound 2 | { 3 | internal abstract class SoundModeBase : IAddressSpace 4 | { 5 | protected bool DacEnabled; 6 | protected bool ChannelEnabled; 7 | protected readonly int Offset; 8 | protected readonly bool Gbc; 9 | protected LengthCounter Length; 10 | 11 | protected int Nr0, Nr1, Nr2, Nr3, Nr4; 12 | 13 | protected virtual int GetNr0() => Nr0; 14 | protected virtual int GetNr1() => Nr1; 15 | protected virtual int GetNr2() => Nr2; 16 | protected virtual int GetNr3() => Nr3; 17 | protected virtual int GetNr4() => Nr4; 18 | 19 | protected virtual void SetNr0(int value) => Nr0 = value; 20 | protected virtual void SetNr1(int value) => Nr1 = value; 21 | protected virtual void SetNr2(int value) => Nr2 = value; 22 | protected virtual void SetNr3(int value) => Nr3 = value; 23 | 24 | protected SoundModeBase(int offset, int length, bool gbc) 25 | { 26 | Offset = offset; 27 | Length = new LengthCounter(length); 28 | Gbc = gbc; 29 | } 30 | 31 | public abstract int TickChannel(); 32 | protected abstract void Trigger(); 33 | 34 | public bool IsEnabled() => ChannelEnabled && DacEnabled; 35 | public virtual bool Accepts(int address) => address >= Offset && address < Offset + 5; 36 | 37 | public virtual void SetByte(int address, int value) 38 | { 39 | var offset = address - Offset; 40 | 41 | switch (offset) 42 | { 43 | case 0: 44 | SetNr0(value); 45 | break; 46 | 47 | case 1: 48 | SetNr1(value); 49 | break; 50 | 51 | case 2: 52 | SetNr2(value); 53 | break; 54 | 55 | case 3: 56 | SetNr3(value); 57 | break; 58 | 59 | case 4: 60 | SetNr4(value); 61 | break; 62 | } 63 | } 64 | 65 | public virtual int GetByte(int address) 66 | { 67 | var offset = address - Offset; 68 | 69 | return offset switch 70 | { 71 | 0 => GetNr0(), 72 | 1 => GetNr1(), 73 | 2 => GetNr2(), 74 | 3 => GetNr3(), 75 | 4 => GetNr4(), 76 | _ => throw new ArgumentException($"Illegal address for sound mode: {address:X}") 77 | }; 78 | } 79 | 80 | protected virtual void SetNr4(int value) 81 | { 82 | Nr4 = value; 83 | Length.SetNr4(value); 84 | if ((value & (1 << 7)) != 0) 85 | { 86 | ChannelEnabled = DacEnabled; 87 | Trigger(); 88 | } 89 | } 90 | 91 | protected virtual int GetFrequency() 92 | { 93 | return 2048 - (GetNr3() | ((GetNr4() & 0b111) << 8)); 94 | } 95 | 96 | public abstract void Start(); 97 | 98 | public void Stop() => ChannelEnabled = false; 99 | 100 | protected bool UpdateLength() 101 | { 102 | Length.Tick(); 103 | if (!Length.Enabled) 104 | { 105 | return ChannelEnabled; 106 | } 107 | 108 | if (ChannelEnabled && Length.Length == 0) 109 | { 110 | ChannelEnabled = false; 111 | } 112 | 113 | return ChannelEnabled; 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /GB.Core/Sound/VolumeEnvelope.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core.Sound 2 | { 3 | internal sealed class VolumeEnvelope 4 | { 5 | private int _initialVolume; 6 | private int _envelopeDirection; 7 | private int _sweep; 8 | private int _volume; 9 | private bool _finished; 10 | private int _i; 11 | 12 | public void SetNr2(int register) 13 | { 14 | _initialVolume = register >> 4; 15 | _envelopeDirection = (register & (1 << 3)) == 0 ? -1 : 1; 16 | _sweep = register & 0b111; 17 | } 18 | 19 | public bool IsEnabled() => _sweep > 0; 20 | 21 | public void Start() 22 | { 23 | _finished = true; 24 | _i = 8192; 25 | } 26 | 27 | public void Trigger() 28 | { 29 | _i = 0; 30 | _volume = _initialVolume; 31 | _finished = false; 32 | } 33 | 34 | public void Tick() 35 | { 36 | if (_finished) 37 | { 38 | return; 39 | } 40 | 41 | if ((_volume == 0 && _envelopeDirection == -1) || (_volume == 15 && _envelopeDirection == 1)) 42 | { 43 | _finished = true; 44 | return; 45 | } 46 | 47 | if (++_i == _sweep * Gameboy.TicksPerSec / 64) 48 | { 49 | _i = 0; 50 | _volume += _envelopeDirection; 51 | } 52 | } 53 | 54 | public int GetVolume() => IsEnabled() ? _volume : _initialVolume; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /GB.Core/Timer.cs: -------------------------------------------------------------------------------- 1 | using GB.Core.Cpu; 2 | 3 | namespace GB.Core 4 | { 5 | internal sealed class Timer : IAddressSpace 6 | { 7 | private readonly SpeedMode _speedMode; 8 | private readonly InterruptManager _interruptManager; 9 | private static readonly int[] FreqToBit = { 9, 3, 5, 7 }; 10 | 11 | private int _div; 12 | private int _tac; 13 | private int _tma; 14 | private int _tima; 15 | private bool _previousBit; 16 | private bool _overflow; 17 | private int _ticksSinceOverflow; 18 | 19 | public Timer(InterruptManager interruptManager, SpeedMode speedMode) 20 | { 21 | _interruptManager = interruptManager; 22 | _speedMode = speedMode; 23 | } 24 | 25 | public void Tick() 26 | { 27 | UpdateDiv((_div + 1) & 0xFFFF); 28 | if (!_overflow) 29 | { 30 | return; 31 | } 32 | 33 | _ticksSinceOverflow++; 34 | if (_ticksSinceOverflow == 4) 35 | { 36 | _interruptManager.RequestInterrupt(InterruptManager.InterruptType.Timer); 37 | } 38 | 39 | if (_ticksSinceOverflow == 5) 40 | { 41 | _tima = _tma; 42 | } 43 | 44 | if (_ticksSinceOverflow == 6) 45 | { 46 | _tima = _tma; 47 | _overflow = false; 48 | _ticksSinceOverflow = 0; 49 | } 50 | } 51 | 52 | private void IncTima() 53 | { 54 | _tima++; 55 | _tima %= 0x100; 56 | if (_tima == 0) 57 | { 58 | _overflow = true; 59 | _ticksSinceOverflow = 0; 60 | } 61 | } 62 | 63 | private void UpdateDiv(int newDiv) 64 | { 65 | _div = newDiv; 66 | int bitPos = FreqToBit[_tac & 0b11]; 67 | bitPos <<= _speedMode.GetSpeedMode() - 1; 68 | bool bit = (_div & (1 << bitPos)) != 0; 69 | bit &= (_tac & (1 << 2)) != 0; 70 | if (!bit && _previousBit) 71 | { 72 | IncTima(); 73 | } 74 | 75 | _previousBit = bit; 76 | } 77 | 78 | public bool Accepts(int address) => address >= 0xFF04 && address <= 0xFF07; 79 | 80 | public void SetByte(int address, int value) 81 | { 82 | switch (address) 83 | { 84 | case 0xFF04: // DIV 85 | UpdateDiv(0); 86 | break; 87 | 88 | case 0xFF05: // TIMA 89 | if (_ticksSinceOverflow < 5) 90 | { 91 | _tima = value; 92 | _overflow = false; 93 | _ticksSinceOverflow = 0; 94 | } 95 | 96 | break; 97 | 98 | case 0xFF06: // TMA 99 | _tma = value; 100 | break; 101 | 102 | case 0xFF07: // TAC 103 | _tac = value; 104 | break; 105 | } 106 | } 107 | 108 | public int GetByte(int address) 109 | { 110 | return address switch 111 | { 112 | 0xFF04 => _div >> 8, 113 | 0xFF05 => _tima, 114 | 0xFF06 => _tma, 115 | 0xFF07 => _tac | 0b11111000, 116 | _ => throw new ArgumentException() 117 | }; 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /GB.Core/Utilities.cs: -------------------------------------------------------------------------------- 1 | namespace GB.Core 2 | { 3 | internal static class Utilities 4 | { 5 | public static bool GetBit(this int byteValue, int position) => (byteValue & (1 << position)) != 0; 6 | 7 | public static int SetBit(this int byteValue, int position, bool value) => value ? SetBit(byteValue, position) : ClearBit(byteValue, position); 8 | public static int SetBit(this int byteValue, int position) => (byteValue | (1 << position)) & 0xff; 9 | public static int ClearBit(this int byteValue, int position) => ~(1 << position) & byteValue & 0xff; 10 | 11 | public static int ToWord(this IEnumerable data) 12 | { 13 | if (data == null || data.Count() != 2) 14 | { 15 | return 0; 16 | } 17 | 18 | return (data.Last() << 8) | data.First(); 19 | } 20 | 21 | public static int ToSigned(this int byteValue) => (byteValue & (1 << 7)) == 0 ? byteValue : byteValue - 0x100; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /GB.Net.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31808.319 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GB.Core", "GB.Core\GB.Core.csproj", "{22C0E572-4055-4BAA-B530-D1187FD96BD0}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GB.WinForms", "GB.WinForms\GB.WinForms.csproj", "{1E0DCDB1-B7FE-4C24-8597-75026445D437}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GB.WASM", "GB.WASM\GB.WASM.csproj", "{78225A0E-6CF9-4462-BF4A-508E12EA3E44}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" 13 | ProjectSection(SolutionItems) = preProject 14 | README.md = README.md 15 | EndProjectSection 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Release|Any CPU = Release|Any CPU 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {22C0E572-4055-4BAA-B530-D1187FD96BD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {22C0E572-4055-4BAA-B530-D1187FD96BD0}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {22C0E572-4055-4BAA-B530-D1187FD96BD0}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {22C0E572-4055-4BAA-B530-D1187FD96BD0}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {1E0DCDB1-B7FE-4C24-8597-75026445D437}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {1E0DCDB1-B7FE-4C24-8597-75026445D437}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {1E0DCDB1-B7FE-4C24-8597-75026445D437}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {1E0DCDB1-B7FE-4C24-8597-75026445D437}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {78225A0E-6CF9-4462-BF4A-508E12EA3E44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {78225A0E-6CF9-4462-BF4A-508E12EA3E44}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {78225A0E-6CF9-4462-BF4A-508E12EA3E44}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {78225A0E-6CF9-4462-BF4A-508E12EA3E44}.Release|Any CPU.Build.0 = Release|Any CPU 35 | EndGlobalSection 36 | GlobalSection(SolutionProperties) = preSolution 37 | HideSolutionNode = FALSE 38 | EndGlobalSection 39 | GlobalSection(ExtensibilityGlobals) = postSolution 40 | SolutionGuid = {24236F99-3BEA-489B-B30E-F088573F0665} 41 | EndGlobalSection 42 | EndGlobal 43 | -------------------------------------------------------------------------------- /GB.WASM/GB.WASM.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | true 5 | true 6 | true 7 | true 8 | true 9 | false 10 | false 11 | true 12 | 13 | true 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | True 29 | True 30 | TestResources.resx 31 | 32 | 33 | 34 | 35 | 36 | ResXFileCodeGenerator 37 | TestResources.Designer.cs 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /GB.WASM/Interop.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices.JavaScript; 3 | using System.Threading.Tasks; 4 | 5 | public partial class Interop 6 | { 7 | [JSImport("setupBuffer", "main.js")] 8 | internal static partial void SetupBuffer([JSMarshalAs] ArraySegment rgbaView, int width, int height); 9 | 10 | [JSImport("outputImage", "main.js")] 11 | internal static partial Task OutputImage(); 12 | 13 | [JSExport] 14 | internal static Task KeyDown(string keyCode) 15 | { 16 | Game.OnKeyDown(keyCode); 17 | return Task.CompletedTask; 18 | } 19 | 20 | [JSExport] 21 | internal static Task KeyUp(string keyCode) 22 | { 23 | Game.OnKeyUp(keyCode); 24 | return Task.CompletedTask; 25 | } 26 | 27 | public static WebGame Game { get; set; } 28 | } -------------------------------------------------------------------------------- /GB.WASM/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | Console.WriteLine("Loading..."); 4 | var gb = new WebGame(); 5 | Interop.Game = gb; 6 | 7 | Console.WriteLine("Starting the emulator..."); 8 | gb.StartGame(); 9 | -------------------------------------------------------------------------------- /GB.WASM/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | [assembly:System.Runtime.Versioning.SupportedOSPlatform("browser")] 5 | -------------------------------------------------------------------------------- /GB.WASM/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "GB.WASM": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "https://localhost:7211;http://localhost:5282", 10 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 11 | 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /GB.WASM/Resources/Super Mario Land (World).gb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcabus/gb-net/8854f7af2ae2e079bc281efe6bcfaf174a8c758a/GB.WASM/Resources/Super Mario Land (World).gb -------------------------------------------------------------------------------- /GB.WASM/Resources/Super Mario Land (World).sav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcabus/gb-net/8854f7af2ae2e079bc281efe6bcfaf174a8c758a/GB.WASM/Resources/Super Mario Land (World).sav -------------------------------------------------------------------------------- /GB.WASM/TestResources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace GB.WASM { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal sealed class TestResources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal TestResources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("GB.WASM.TestResources", typeof(TestResources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized resource of type System.Byte[]. 65 | /// 66 | internal static byte[] SUPERMARIOLAND { 67 | get { 68 | object obj = ResourceManager.GetObject("SUPERMARIOLAND", resourceCulture); 69 | return ((byte[])(obj)); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /GB.WASM/TestResources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | Resources\Super Mario Land (World).gb;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 123 | 124 | -------------------------------------------------------------------------------- /GB.WASM/WebDisplay.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Threading; 4 | using GB.Core.Graphics; 5 | 6 | public sealed class WebDisplay : IDisplay 7 | { 8 | public static readonly int DisplayWidth = 160; 9 | public static readonly int DisplayHeight = 144; 10 | public static readonly float AspectRatio = DisplayWidth / (DisplayHeight * 1f); 11 | 12 | public static readonly int[] Colors = { 0xe6f8da, 0x99c886, 0x437969, 0x051f2a }; 13 | private static readonly byte[] ScreenOffOutput; 14 | 15 | private int[] _rgb = new int[DisplayWidth * DisplayHeight]; 16 | private int[] _rgbPrevious = new int[DisplayWidth * DisplayHeight]; 17 | private readonly byte[] _imageBuffer = new byte[DisplayWidth * DisplayHeight * 4]; 18 | 19 | private ManualResetEvent _requestRefresh = new ManualResetEvent(false); 20 | private ManualResetEvent _refreshDone = new ManualResetEvent(false); 21 | private int _i; 22 | private CancellationToken _token = CancellationToken.None; 23 | private SynchronizationContext _synchronizationContext; 24 | 25 | static WebDisplay() 26 | { 27 | var (r, g, b) = ToRgb(Colors[0]); 28 | var pi = 0; 29 | var count = DisplayHeight * DisplayWidth; 30 | 31 | ScreenOffOutput = new byte[count * 4]; 32 | 33 | while (pi < count) 34 | { 35 | ScreenOffOutput[pi++] = (byte)r; 36 | ScreenOffOutput[pi++] = (byte)g; 37 | ScreenOffOutput[pi++] = (byte)b; 38 | ScreenOffOutput[pi++] = 255; 39 | } 40 | } 41 | 42 | public void Run(CancellationToken token) 43 | { 44 | Interop.SetupBuffer(new ArraySegment(_imageBuffer), DisplayWidth, DisplayHeight); 45 | _requestRefresh.Reset(); 46 | _refreshDone.Reset(); 47 | _token = token; 48 | _synchronizationContext = SynchronizationContext.Current ?? new SynchronizationContext(); 49 | Enabled = true; 50 | 51 | while (true) 52 | { 53 | _refreshDone.Reset(); 54 | WaitHandle.WaitAny([token.WaitHandle, _requestRefresh]); 55 | 56 | if (token.IsCancellationRequested) 57 | { 58 | break; 59 | } 60 | _requestRefresh.Reset(); 61 | 62 | FillAndDrawBuffer(); 63 | } 64 | } 65 | 66 | public bool Enabled { get; set; } 67 | 68 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 69 | public void PutDmgPixel(int color) 70 | { 71 | _rgb[_i++] = Colors[color]; 72 | _i %= _rgb.Length; 73 | } 74 | 75 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 76 | public void PutColorPixel(int gbcRgb) 77 | { 78 | if (_i >= _rgb.Length) 79 | { 80 | return; 81 | } 82 | _rgb[_i++] = TranslateGbcRgb(gbcRgb); 83 | } 84 | 85 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 86 | private static int TranslateGbcRgb(int gbcRgb) 87 | { 88 | var r = (gbcRgb >> 0) & 0x1f; 89 | var g = (gbcRgb >> 5) & 0x1f; 90 | var b = (gbcRgb >> 10) & 0x1f; 91 | var result = (r * 8) << 16; 92 | result |= (g * 8) << 8; 93 | result |= (b * 8) << 0; 94 | return result; 95 | } 96 | 97 | public void RequestRefresh() 98 | { 99 | _requestRefresh.Set(); 100 | } 101 | 102 | public void WaitForRefresh() 103 | { 104 | if (_token == CancellationToken.None || _token.IsCancellationRequested) 105 | { 106 | return; 107 | } 108 | WaitHandle.WaitAny([_token.WaitHandle, _refreshDone]); 109 | } 110 | 111 | private void FillAndDrawBuffer() 112 | { 113 | if (Enabled) 114 | { 115 | // double buffering, since _rgb is not synchronized 116 | _rgbPrevious = Interlocked.Exchange(ref _rgb, _rgbPrevious); 117 | 118 | var pi = 0; 119 | var i = 0; 120 | 121 | while (i < _rgbPrevious.Length) 122 | { 123 | var (r, g, b) = ToRgb(_rgbPrevious[i++]); 124 | _imageBuffer[pi++] = (byte)r; 125 | _imageBuffer[pi++] = (byte)g; 126 | _imageBuffer[pi++] = (byte)b; 127 | _imageBuffer[pi++] = 255; 128 | } 129 | } 130 | else 131 | { 132 | ScreenOffOutput.CopyTo(_imageBuffer, 0); 133 | } 134 | 135 | _synchronizationContext.Post(async _ => 136 | { 137 | await Interop.OutputImage(); 138 | _refreshDone.Set(); 139 | }, null); 140 | 141 | _i = 0; 142 | } 143 | 144 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 145 | public static (int, int, int) ToRgb(int pixel) 146 | { 147 | var b = pixel & 255; 148 | var g = (pixel >> 8) & 255; 149 | var r = (pixel >> 16) & 255; 150 | return (r, g, b); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /GB.WASM/WebGame.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading; 5 | using GB.Core.Controller; 6 | using GB.Core.Gui; 7 | using GB.Core.Sound; 8 | using GB.WASM; 9 | 10 | public sealed class WebGame : IController, IDisposable 11 | { 12 | private IButtonListener _listener; 13 | private readonly WebDisplay _display = new(); 14 | 15 | // private readonly ISoundOutput _soundOutput = new SoundOutput(); 16 | private readonly ISoundOutput _soundOutput = new NullSoundOutput(); 17 | private readonly Emulator _emulator; 18 | private readonly Dictionary _controls; 19 | 20 | private CancellationTokenSource _cancellation; 21 | 22 | public WebGame() 23 | { 24 | _emulator = new Emulator 25 | { 26 | Display = _display, 27 | SoundOutput = _soundOutput 28 | }; 29 | 30 | _controls = new Dictionary 31 | { 32 | {"KeyA", Button.Left}, 33 | {"ArrowLeft", Button.Left}, 34 | {"KeyD", Button.Right}, 35 | {"ArrowRight", Button.Right}, 36 | {"KeyW", Button.Up}, 37 | {"ArrowUp", Button.Up}, 38 | {"KeyS", Button.Down}, 39 | {"ArrowDown", Button.Down}, 40 | {"KeyK", Button.A}, 41 | {"Space", Button.A}, 42 | {"KeyO", Button.B}, 43 | {"Enter", Button.Start}, 44 | {"Backspace", Button.Select} 45 | }; 46 | 47 | _cancellation = new CancellationTokenSource(); 48 | 49 | _emulator.Controller = this; 50 | } 51 | 52 | public void StartGame() 53 | { 54 | StopEmulator(); 55 | 56 | _emulator.EnableBootRom = true; 57 | _emulator.RomStream = new MemoryStream(TestResources.SUPERMARIOLAND); 58 | _emulator.Run(_cancellation.Token); 59 | } 60 | 61 | public void StopEmulator() 62 | { 63 | if (!_emulator.Active) 64 | { 65 | return; 66 | } 67 | 68 | _emulator.Stop(_cancellation); 69 | _soundOutput.Stop(); 70 | 71 | _cancellation = new CancellationTokenSource(); 72 | _display.Enabled = false; 73 | } 74 | 75 | public void OnKeyDown(string keyCode) 76 | { 77 | var button = _controls.GetValueOrDefault(keyCode); 78 | if (button != null) 79 | { 80 | _listener?.OnButtonPress(button); 81 | } 82 | } 83 | 84 | public void OnKeyUp(string keyCode) 85 | { 86 | var button = _controls.GetValueOrDefault(keyCode); 87 | if (button != null) 88 | { 89 | _listener?.OnButtonRelease(button); 90 | } 91 | } 92 | 93 | void IController.SetButtonListener(IButtonListener listener) 94 | { 95 | _listener = listener; 96 | } 97 | 98 | public void Dispose() 99 | { 100 | _cancellation?.Dispose(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /GB.WASM/launch.bat: -------------------------------------------------------------------------------- 1 | dotnet publish -c Release 2 | dotnet serve -h "Cross-Origin-Opener-Policy:same-origin" -h "Cross-Origin-Embedder-Policy:require-corp" --directory bin\Release\net9.0\publish\wwwroot -------------------------------------------------------------------------------- /GB.WASM/wwwroot/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcabus/gb-net/8854f7af2ae2e079bc281efe6bcfaf174a8c758a/GB.WASM/wwwroot/.nojekyll -------------------------------------------------------------------------------- /GB.WASM/wwwroot/coi-serviceworker.min.js: -------------------------------------------------------------------------------- 1 | /*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */ 2 | let coepCredentialless=!1;"undefined"==typeof window?(self.addEventListener("install",(()=>self.skipWaiting())),self.addEventListener("activate",(e=>e.waitUntil(self.clients.claim()))),self.addEventListener("message",(e=>{e.data&&("deregister"===e.data.type?self.registration.unregister().then((()=>self.clients.matchAll())).then((e=>{e.forEach((e=>e.navigate(e.url)))})):"coepCredentialless"===e.data.type&&(coepCredentialless=e.data.value))})),self.addEventListener("fetch",(function(e){const o=e.request;if("only-if-cached"===o.cache&&"same-origin"!==o.mode)return;const s=coepCredentialless&&"no-cors"===o.mode?new Request(o,{credentials:"omit"}):o;e.respondWith(fetch(s).then((e=>{if(0===e.status)return e;const o=new Headers(e.headers);return o.set("Cross-Origin-Embedder-Policy",coepCredentialless?"credentialless":"require-corp"),coepCredentialless||o.set("Cross-Origin-Resource-Policy","cross-origin"),o.set("Cross-Origin-Opener-Policy","same-origin"),new Response(e.body,{status:e.status,statusText:e.statusText,headers:o})})).catch((e=>console.error(e))))}))):(()=>{const e=window.sessionStorage.getItem("coiReloadedBySelf");window.sessionStorage.removeItem("coiReloadedBySelf");const o="coepdegrade"==e,s={shouldRegister:()=>!e,shouldDeregister:()=>!1,coepCredentialless:()=>!0,coepDegrade:()=>!0,doReload:()=>window.location.reload(),quiet:!1,...window.coi},r=navigator,t=r.serviceWorker&&r.serviceWorker.controller;t&&!window.crossOriginIsolated&&window.sessionStorage.setItem("coiCoepHasFailed","true");const i=window.sessionStorage.getItem("coiCoepHasFailed");if(t){const e=s.coepDegrade()&&!(o||window.crossOriginIsolated);r.serviceWorker.controller.postMessage({type:"coepCredentialless",value:!(e||i&&s.coepDegrade())&&s.coepCredentialless()}),e&&(!s.quiet&&console.log("Reloading page to degrade COEP."),window.sessionStorage.setItem("coiReloadedBySelf","coepdegrade"),s.doReload("coepdegrade")),s.shouldDeregister()&&r.serviceWorker.controller.postMessage({type:"deregister"})}!1===window.crossOriginIsolated&&s.shouldRegister()&&(window.isSecureContext?r.serviceWorker?r.serviceWorker.register(window.document.currentScript.src).then((e=>{!s.quiet&&console.log("COOP/COEP Service Worker registered",e.scope),e.addEventListener("updatefound",(()=>{!s.quiet&&console.log("Reloading page to make use of updated COOP/COEP Service Worker."),window.sessionStorage.setItem("coiReloadedBySelf","updatefound"),s.doReload()})),e.active&&!r.serviceWorker.controller&&(!s.quiet&&console.log("Reloading page to make use of COOP/COEP Service Worker."),window.sessionStorage.setItem("coiReloadedBySelf","notcontrolling"),s.doReload())}),(e=>{!s.quiet&&console.error("COOP/COEP Service Worker failed to register:",e)})):!s.quiet&&console.error("COOP/COEP Service Worker not registered, perhaps due to private mode."):!s.quiet&&console.log("COOP/COEP Service Worker not registered, a secure context is required."))})(); -------------------------------------------------------------------------------- /GB.WASM/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | GB.WASM 8 | 9 | 10 | 37 | 38 | 39 | 40 |
41 | 42 |
43 |