From 86452af2d48e2d1883dbfd67316e371f2a593bfe Mon Sep 17 00:00:00 2001 From: Melodi <62381002+Melodi17@users.noreply.github.com> Date: Mon, 4 Aug 2025 00:58:48 +1000 Subject: Initial commit --- .DS_Store | Bin 0 -> 8196 bytes .config/dotnet-tools.json | 36 +++++ .fleet/run.json | 11 ++ .gitattributes | 2 + .gitignore | 5 + .idea/.idea.Chip8Emu2/.idea/.gitignore | 13 ++ .idea/.idea.Chip8Emu2/.idea/.name | 1 + .idea/.idea.Chip8Emu2/.idea/encodings.xml | 4 + .idea/.idea.Chip8Emu2/.idea/indexLayout.xml | 8 + .idea/.idea.Chip8Emu2/.idea/vcs.xml | 6 + .idea/.idea.Q1Emu/.idea/.gitignore | 13 ++ .idea/.idea.Q1Emu/.idea/encodings.xml | 4 + .idea/.idea.Q1Emu/.idea/indexLayout.xml | 8 + .idea/.idea.Q1Emu/.idea/misc.xml | 10 ++ .idea/.idea.Q1Emu/.idea/vcs.xml | 6 + Assembler/Assembler.cs | 106 +++++++++++++ Chip/Bus.cs | 55 +++++++ Chip/BusDevice.cs | 50 +++++++ Chip/DisplayDevice.cs | 76 ++++++++++ Chip/Q1Cpu.cs | 152 +++++++++++++++++++ Chip/Q1CpuAddressingModes.cs | 150 +++++++++++++++++++ Chip/Q1CpuInstructions.cs | 225 ++++++++++++++++++++++++++++ Chip/RamDevice.cs | 49 ++++++ Chip8Emu2.sln.DotSettings.user | 2 + Content/Content.mgcb | 15 ++ DisplayEngine.cs | 72 +++++++++ GlobalUsings.cs | 12 ++ Icon.bmp | Bin 0 -> 262282 bytes Icon.ico | Bin 0 -> 147541 bytes Make.cs | 48 ++++++ Program.cs | 76 ++++++++++ Q1Emu.csproj | 38 +++++ Q1Emu.sln | 16 ++ Q1Emu.sln.DotSettings.user | 2 + ROMs/.DS_Store | Bin 0 -> 6148 bytes ROMs/test.q1 | Bin 0 -> 10 bytes app.manifest | 43 ++++++ 37 files changed, 1314 insertions(+) create mode 100644 .DS_Store create mode 100644 .config/dotnet-tools.json create mode 100644 .fleet/run.json create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .idea/.idea.Chip8Emu2/.idea/.gitignore create mode 100644 .idea/.idea.Chip8Emu2/.idea/.name create mode 100644 .idea/.idea.Chip8Emu2/.idea/encodings.xml create mode 100644 .idea/.idea.Chip8Emu2/.idea/indexLayout.xml create mode 100644 .idea/.idea.Chip8Emu2/.idea/vcs.xml create mode 100644 .idea/.idea.Q1Emu/.idea/.gitignore create mode 100644 .idea/.idea.Q1Emu/.idea/encodings.xml create mode 100644 .idea/.idea.Q1Emu/.idea/indexLayout.xml create mode 100644 .idea/.idea.Q1Emu/.idea/misc.xml create mode 100644 .idea/.idea.Q1Emu/.idea/vcs.xml create mode 100644 Assembler/Assembler.cs create mode 100644 Chip/Bus.cs create mode 100644 Chip/BusDevice.cs create mode 100644 Chip/DisplayDevice.cs create mode 100644 Chip/Q1Cpu.cs create mode 100644 Chip/Q1CpuAddressingModes.cs create mode 100644 Chip/Q1CpuInstructions.cs create mode 100644 Chip/RamDevice.cs create mode 100644 Chip8Emu2.sln.DotSettings.user create mode 100644 Content/Content.mgcb create mode 100644 DisplayEngine.cs create mode 100644 GlobalUsings.cs create mode 100644 Icon.bmp create mode 100644 Icon.ico create mode 100644 Make.cs create mode 100644 Program.cs create mode 100644 Q1Emu.csproj create mode 100644 Q1Emu.sln create mode 100644 Q1Emu.sln.DotSettings.user create mode 100644 ROMs/.DS_Store create mode 100644 ROMs/test.q1 create mode 100644 app.manifest diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..7ae4f61 Binary files /dev/null and b/.DS_Store differ diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..84ff03d --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,36 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-mgcb": { + "version": "3.8.2.1105", + "commands": [ + "mgcb" + ] + }, + "dotnet-mgcb-editor": { + "version": "3.8.2.1105", + "commands": [ + "mgcb-editor" + ] + }, + "dotnet-mgcb-editor-linux": { + "version": "3.8.2.1105", + "commands": [ + "mgcb-editor-linux" + ] + }, + "dotnet-mgcb-editor-windows": { + "version": "3.8.2.1105", + "commands": [ + "mgcb-editor-windows" + ] + }, + "dotnet-mgcb-editor-mac": { + "version": "3.8.2.1105", + "commands": [ + "mgcb-editor-mac" + ] + } + } +} \ No newline at end of file diff --git a/.fleet/run.json b/.fleet/run.json new file mode 100644 index 0000000..5ae6c96 --- /dev/null +++ b/.fleet/run.json @@ -0,0 +1,11 @@ +{ + "configurations": [ + { + "type": "dotnet", + "name": "Dotnet configuration", + "projectPath": "$PROJECT_DIR$", + "solutionPath": "$PROJECT_DIR$", + "args": [] + } + ] +} \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..add57be --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ \ No newline at end of file diff --git a/.idea/.idea.Chip8Emu2/.idea/.gitignore b/.idea/.idea.Chip8Emu2/.idea/.gitignore new file mode 100644 index 0000000..f0180a3 --- /dev/null +++ b/.idea/.idea.Chip8Emu2/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/contentModel.xml +/projectSettingsUpdater.xml +/.idea.Chip8Emu2.iml +/modules.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.Chip8Emu2/.idea/.name b/.idea/.idea.Chip8Emu2/.idea/.name new file mode 100644 index 0000000..95e8088 --- /dev/null +++ b/.idea/.idea.Chip8Emu2/.idea/.name @@ -0,0 +1 @@ +Chip8Emu2 \ No newline at end of file diff --git a/.idea/.idea.Chip8Emu2/.idea/encodings.xml b/.idea/.idea.Chip8Emu2/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.Chip8Emu2/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.Chip8Emu2/.idea/indexLayout.xml b/.idea/.idea.Chip8Emu2/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.Chip8Emu2/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.Chip8Emu2/.idea/vcs.xml b/.idea/.idea.Chip8Emu2/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/.idea.Chip8Emu2/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/.idea.Q1Emu/.idea/.gitignore b/.idea/.idea.Q1Emu/.idea/.gitignore new file mode 100644 index 0000000..99ea8f4 --- /dev/null +++ b/.idea/.idea.Q1Emu/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/.idea.Q1Emu.iml +/projectSettingsUpdater.xml +/modules.xml +/contentModel.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.Q1Emu/.idea/encodings.xml b/.idea/.idea.Q1Emu/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.Q1Emu/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.Q1Emu/.idea/indexLayout.xml b/.idea/.idea.Q1Emu/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.Q1Emu/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.Q1Emu/.idea/misc.xml b/.idea/.idea.Q1Emu/.idea/misc.xml new file mode 100644 index 0000000..5207dea --- /dev/null +++ b/.idea/.idea.Q1Emu/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/.idea.Q1Emu/.idea/vcs.xml b/.idea/.idea.Q1Emu/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/.idea.Q1Emu/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Assembler/Assembler.cs b/Assembler/Assembler.cs new file mode 100644 index 0000000..14a33fd --- /dev/null +++ b/Assembler/Assembler.cs @@ -0,0 +1,106 @@ +namespace Q1Emu.Assembler; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +public class Assembler +{ + public readonly BinaryWriter Writer; + public readonly StringReader Reader; + + public Assembler(StringReader reader, BinaryWriter writer) + { + this.Reader = reader; + this.Writer = writer; + } + + public void Assemble() + { + while (this.Reader.ReadLine() is { } line) + { + string trimmedLine = line.Trim(); + if (string.IsNullOrEmpty(trimmedLine) || trimmedLine.StartsWith(';')) + continue; + + this.ParseInstruction(trimmedLine); + } + } + + private void ParseInstruction(string instruction) + { + string[] parts = instruction.Split(' ', 2); + string opcode = parts[0].ToUpperInvariant(); + + parts = parts.Length > 1 ? parts[1].Split(',') : []; + if (parts.Length > 2) + throw new ArgumentException($"Invalid instruction format: {instruction}"); + + u8 m1 = 0, m2 = 0; + if (parts.Length > 0) + { + m1 = this.ParseOperand(parts[0]); + if (parts.Length > 1) + m2 = this.ParseOperand(parts[1]); + } + + switch (opcode) + { + case "NOP": + this.Writer.Write((u8) 0x00); + break; + + default: + throw new NotSupportedException($"Unsupported opcode: {opcode}"); + } + } + + private u8 ParseOperand(string operand) + { + operand = operand.Trim(); + + bool IsRegister(string text) + { + if (text.Length > 1 && text[0] == 'V') + return true; + + string[] hardcodedRegisters = + [ + "AX", + "AH", + "AL", + "DX", + "DH", + "DL", + "LX", + "PC", + "SP", + ]; + + return hardcodedRegisters.Contains(text); + } + + if (operand.StartsWith("@")) + { + return 0x03; // Indirect + } + + if (operand.StartsWith("[") && operand.EndsWith("]")) + { + string inner = operand[1..^1].Trim(); + if (inner.Contains("+")) + return 0x07; // Register indirect with offset + + if (IsRegister(inner)) + return 0x06; // Register indirect + + return 0x02; // Direct + } + + if (IsRegister(operand)) + return 0x05; // Register + + return 0x01; // Immediate + } +} \ No newline at end of file diff --git a/Chip/Bus.cs b/Chip/Bus.cs new file mode 100644 index 0000000..e8cf5ad --- /dev/null +++ b/Chip/Bus.cs @@ -0,0 +1,55 @@ +namespace Q1Emu.Chip; + +using System.Collections.Generic; +using System.Linq; + +public class Bus : BusDevice +{ + public List Devices; + + public Bus(u16 addressableStart, u16 addressableEnd) : base(addressableStart, addressableEnd) + { + this.Devices = []; + } + private BusDevice? GetDevice(u16 address) + { + BusDevice? device = this.Devices.FirstOrDefault(x => address >= x.AddressableStart && address < x.AddressableEnd); + return device; + } + + public override u8 Read(u16 address) + { + BusDevice? device = GetDevice(address); + if (device == null) + return 0; + + return device.Read(address); + } + + public override void Write(u16 address, u8 value) + { + BusDevice? device = GetDevice(address); + if (device == null) + return; + + device.Write(address, value); + } + + public override ref8 ReadSeq(u16 address, u16 length) + { + BusDevice? device = this.GetDevice(address); + if (device == null) + return []; + + return device.ReadSeq(address, length); + } + + public override void WriteSeq(u16 address, u16 length, ref8 value) + { + BusDevice? device = this.GetDevice(address); + if (device == null) + return; + + device.WriteSeq(address, length, value); + } +} \ No newline at end of file diff --git a/Chip/BusDevice.cs b/Chip/BusDevice.cs new file mode 100644 index 0000000..e1f559c --- /dev/null +++ b/Chip/BusDevice.cs @@ -0,0 +1,50 @@ +namespace Q1Emu.Chip; + +using System.Runtime.CompilerServices; + +public abstract class BusDevice +{ + public BusDevice(u16 addressableStart, u16 addressableEnd) + { + this.AddressableStart = addressableStart; + this.AddressableEnd = addressableEnd; + } + + public Bus Bus { get; set; } + public abstract u8 Read(u16 address); + public abstract void Write(u16 address, u8 value); + + public abstract ref8 ReadSeq(u16 address, u16 length); + public abstract void WriteSeq(u16 address, u16 length, ref8 value); + + public u16 ReadWord(u16 address) + { + ref8 data = this.ReadSeq(address, 2); + u16 value = Make.u16(data); + return value; + } + + public void WriteWord(u16 address, u16 value) + { + ref8 data = Make.ref8(value); + this.WriteSeq(address, 2, data); + } + + public virtual void Clock() { } + public virtual void Reset() { } + + public void AttachToBus(Bus bus) + { + this.Bus = bus; + bus.Devices.Add(this); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected u16 GetRelativeAddress(u16 address) + { + return (u16) (address - this.AddressableStart); + } + + public u16 AddressableStart { get; } + public u16 AddressableEnd { get; } +} \ No newline at end of file diff --git a/Chip/DisplayDevice.cs b/Chip/DisplayDevice.cs new file mode 100644 index 0000000..ea34cfb --- /dev/null +++ b/Chip/DisplayDevice.cs @@ -0,0 +1,76 @@ +namespace Q1Emu.Chip; + +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +public class DisplayDevice : RamDevice +{ + public Texture2D? OutputTexture; + private readonly u16 _paletteSize; + private readonly u16 _displayWidth; + private readonly u16 _displayHeight; + + public DisplayDevice(u16 paletteSize, u16 displayWidth, u16 displayHeight, u16 addressableStart) + : base(addressableStart, DisplayDevice.DetermineSize(paletteSize, displayWidth, displayHeight)) + { + this._paletteSize = paletteSize; + this._displayWidth = displayWidth; + this._displayHeight = displayHeight; + } + + private static u16 DetermineSize(u16 paletteSize, u16 displayWidth, u16 displayHeight) + { + return (u16) ((paletteSize * 3) + (displayWidth * displayHeight)); + } + + public override void Clock() + { + Color[] palette = this.ReadPalette(); + ref8 pixelData = this.ReadSeq((u16) (this.AddressableStart + this._paletteSize * 3), (u16) (this._displayWidth * this._displayHeight)); + + Color[] textureData = new Color[this._displayWidth * this._displayHeight]; + for (u16 i = 0; i < textureData.Length; i++) + { + u8 pixelValue = pixelData[i]; + textureData[i] = palette[pixelValue]; + } + + if (this.OutputTexture != null) + this.OutputTexture.SetData(textureData); + } + + private Color[] ReadPalette() + { + ref8 paletteData = this.ReadSeq(this.AddressableStart, (u16) (this._paletteSize * 3)); + Color[] colors = new Color[this._paletteSize + 1]; + colors[0] = Color.Transparent; + + for (u16 i = 0; i < this._paletteSize; i++) + { + u16 offset = (u16) (i * 3); + byte r = paletteData[offset]; + byte g = paletteData[offset + 1]; + byte b = paletteData[offset + 2]; + colors[(u16) (i + 1)] = new Color(r, g, b); + } + + return colors; + } + + public void SetPalette(Color[] palette) + { + if (palette.Length != this._paletteSize) + throw new ArgumentException($"Palette must have {this._paletteSize} colors."); + + ref8 paletteData = this.ReadSeq(this.AddressableStart, (u16) (this._paletteSize * 3)); + for (u16 i = 0; i < this._paletteSize; i++) + { + u16 offset = (u16) (i * 3); + paletteData[offset] = palette[i].R; + paletteData[offset + 1] = palette[i].G; + paletteData[offset + 2] = palette[i].B; + } + } +} \ No newline at end of file diff --git a/Chip/Q1Cpu.cs b/Chip/Q1Cpu.cs new file mode 100644 index 0000000..1124578 --- /dev/null +++ b/Chip/Q1Cpu.cs @@ -0,0 +1,152 @@ +using System; + +namespace Q1Emu.Chip; + +using System.Collections.Generic; + +public partial class Q1Cpu +{ + public const u16 StackStart = 0x30; + public const u16 StackLength = 16 * 2; // 16 16-bit values + + public Random Random; + + public u16 AX = 0; // Accumulator register + public u16 DX = 0; // Data register + public u16 LX = 0; // Logical register + + public u16 SP { get; set; } // Stack pointer + public u16 PC { get; set; } // Program counter + + public u16[] V { get; set; } // Variable registers (64 total) + public u16 I { get; set; } // Index register + public u16 Cycles { get; set; } // Number of cycles + + public Bus Bus { get; set; } + + public Q1Cpu() + { + this.LoadInstructionLookup(); + + this.Reset(); + } + + public void Reset() + { + this.V = new u16[16]; + + this.SP = StackStart; + + this.I = 0; + } + + public void Clock() + { + u16 instruction = this.FetchInstruction(out u8 op, out u8 m1, out u8 m2, out _); + + bool ex = m2 > 0; + switch (m1, ex) + { + case (0x00, false): + this.InstructionLookup[op].Item1(); // Implicit + break; + case (> 0x00, false): + this.InstructionLookup[op].Item2(m1); // Simple addressing mode + break; + case (0x00, true): + this.InstructionLookup[op].Item3(); // Extended implicit + break; + case (> 0x00, true): + this.InstructionLookup[op].Item4(m1, m2); // Extended addressing mode + break; + } + + this.Cycles++; + } + public u16 FetchInstruction(out u8 op, out u8 m1, out u8 m2, out u8 length) + { + u16 instruction = this.Bus.ReadWord(this.PC); + this.PC += 2; + + op = (u8) ((instruction >> 8) & 0xFF); // 256 opcodes + m1 = (u8) ((instruction >> 4) & 0x0F); // 16 modes + m2 = (u8) (instruction & 0x0F); // 16 modes + + length = (u8) (this.AddressingModeLength(m1) + this.AddressingModeLength(m2)); + return instruction; + } + + private void PushStack(u16 value) + { + if (this.SP + 2 >= StackStart + StackLength) + throw new InvalidOperationException("Stack overflow"); + + this.Bus.WriteSeq(this.SP, 2, Make.ref8(value)); + this.SP += 2; + } + + private u16 PopStack() + { + if (this.SP - 2 < StackStart) + throw new InvalidOperationException("Stack underflow"); + + this.SP -= 2; + return Make.u16(this.Bus.ReadSeq(this.SP, 2)); + } + + private u16 FetchRegister(u8 regIndex) + { + return regIndex switch + { + <= 0x3F => this.V[regIndex], + + 0x40 => this.AX, + 0x41 => (u16) (this.AX >> 8), // High byte of AX + 0x42 => (u16) (this.AX & 0xFF), // Low byte of AX + + 0x43 => this.DX, + 0x44 => (u16) (this.DX >> 8), // High byte of DX + 0x45 => (u16) (this.DX & 0xFF), // Low byte of DX + + 0x46 => this.LX, + 0x50 => this.PC, + 0x51 => this.SP, + + _ => throw new InvalidOperationException($"Unknown register index: {regIndex:X2}"), + }; + } + + private void StoreRegister(u8 regIndex, u16 value) + { + switch (regIndex) + { + case <= 0x3F: + this.V[regIndex] = value; + break; + + case 0x40: + this.AX = value; break; + case 0x41: + this.AX = (u16) ((this.AX & 0x00FF) | (value << 8)); break; // Set high byte + case 0x42: + this.AX = (u16) ((this.AX & 0xFF00) | (value & 0x00FF)); break; // Set low byte + + case 0x43: + this.DX = value; break; + case 0x44: + this.DX = (u16) ((this.DX & 0x00FF) | (value << 8)); break; // Set high byte + case 0x45: + this.DX = (u16) ((this.DX & 0xFF00) | (value & 0x00FF)); break; // Set low byte + + case 0x46: + this.LX = value; break; + case 0x50: + this.PC = value; break; + case 0x51: + this.SP = value; break; + + default: + throw new InvalidOperationException($"Unknown register index: {regIndex:X2}"); + } + } +} \ No newline at end of file diff --git a/Chip/Q1CpuAddressingModes.cs b/Chip/Q1CpuAddressingModes.cs new file mode 100644 index 0000000..0d664f7 --- /dev/null +++ b/Chip/Q1CpuAddressingModes.cs @@ -0,0 +1,150 @@ +namespace Q1Emu.Chip; + +using System; +using System.Collections.Generic; + +public partial class Q1Cpu +{ + public u16 FetchData(u8 mode) + { + return mode switch + { + 0x0 => this.AddressingMode_Implicit(), + 0x1 => this.AddressingMode_Immediate(), + 0x2 => this.AddressingMode_Direct(), + 0x3 => this.AddressingMode_Indirect(), + 0x4 => this.AddressingMode_Relative(), + 0x5 => this.AddressingMode_Register(), + 0x6 => this.AddressingMode_RegisterIndirect(), + 0x7 => this.AddressingMode_RegisterIndirectOffset(), + _ => throw new InvalidOperationException($"Unknown addressing mode: {mode:X2}"), + }; + } + + public void StoreData(u8 mode, u16 value) + { + Console.WriteLine($"StoreData: mode={mode:X8}, value={value:X16}"); + if (mode != 0x05) + { + u16 address = this.FetchData(mode); + Console.WriteLine($"StoreData: address={address:X16}"); + this.Bus.WriteWord(address, value); + } + else + { + u8 regIndex = this.Bus.Read(this.PC); + this.PC += 1; + + this.StoreRegister(regIndex, value); + } + } + + public void TransformData(u8 mode, Func transform) + { + if (mode != 0x05) + { + u16 address = this.FetchData(mode); + u16 value = this.Bus.ReadWord(address); + u16 transformedValue = transform(value); + this.Bus.WriteWord(address, transformedValue); + } + else + { + u8 regIndex = this.Bus.Read(this.PC); + this.PC += 1; + + u16 value = this.FetchRegister(regIndex); + u16 transformedValue = transform(value); + this.StoreRegister(regIndex, transformedValue); + } + } + + public u8 AddressingModeLength(u8 mode) + { + return mode switch + { + 0x0 => 0, // Implicit + 0x1 => 2, // Immediate + 0x2 => 2, // Direct + 0x3 => 2, // Indirect + 0x4 => 2, // Relative + 0x5 => 1, // Register + 0x6 => 1, // Register indirect + 0x7 => 3, // Register indirect with offset + _ => throw new InvalidOperationException($"Unknown addressing mode: {mode:X2}"), + }; + } + + public u16 AddressingMode_Implicit() + { + return 0; + } + + + public u16 AddressingMode_Immediate() + { + u16 value = this.Bus.ReadWord(this.PC); + this.PC += 2; + + return value; + } + + public u16 AddressingMode_Direct() + { + u16 address = this.Bus.ReadWord(this.PC); + this.PC += 2; + + u16 value = this.Bus.ReadWord(address); + return value; + } + + public u16 AddressingMode_Indirect() + { + u16 address = this.Bus.ReadWord(this.PC); + this.PC += 2; + + u16 indirectAddress = this.Bus.ReadWord(address); + u16 value = this.Bus.ReadWord(indirectAddress); + return value; + } + + public u16 AddressingMode_Relative() + { + u16 offset = this.Bus.ReadWord(this.PC); + this.PC += 2; + + u16 address = (u16) (this.PC + offset); + return address; + } + + public u16 AddressingMode_Register() + { + u8 regIndex = this.Bus.Read(this.PC); + this.PC += 1; + + return this.FetchRegister(regIndex); + } + + public u16 AddressingMode_RegisterIndirect() + { + u8 regIndex = this.Bus.Read(this.PC); + this.PC += 1; + + u16 address = this.FetchRegister(regIndex); + u16 value = this.Bus.ReadWord(address); + return value; + } + + public u16 AddressingMode_RegisterIndirectOffset() + { + u8 regIndex = this.Bus.Read(this.PC); + this.PC += 1; + + u16 offset = this.Bus.ReadWord(this.PC); + this.PC += 2; + + u16 address = (u16) (this.FetchRegister(regIndex) + offset); + u16 value = this.Bus.ReadWord(address); + return value; + } +} \ No newline at end of file diff --git a/Chip/Q1CpuInstructions.cs b/Chip/Q1CpuInstructions.cs new file mode 100644 index 0000000..8b79a1d --- /dev/null +++ b/Chip/Q1CpuInstructions.cs @@ -0,0 +1,225 @@ +namespace Q1Emu.Chip; + +using System; +using System.Collections.Generic; + +public partial class Q1Cpu +{ + public Dictionary, Action, Action)> InstructionLookup; + private void LoadInstructionLookup() + { + this.InstructionLookup = new Dictionary, Action, Action)> + { + { 0x00, (this.Instruction_NOP, this.Instruction_JMP, this.Instruction_HALT, this.Instruction_MOV) }, + { 0x01, (this.Instruction_RET, this.Instruction_CALL, this.Instruction_SUS, this.Instruction_CMP) }, + { 0x02, (this.Instruction_BR, this.Instruction_BZ, this.Instruction_BZ_LX, this.Instruction_LT) }, + { 0x03, (this.Instruction_NOP, this.Instruction_NOP, this.Instruction_NOP, this.Instruction_GT) }, + + { 0x04, (this.Instruction_NOT_LX, this.Instruction_NOT, this.Instruction_NOP, this.Instruction_AND) }, + { 0x05, (this.Instruction_INV_DX, this.Instruction_INV, this.Instruction_NOP, this.Instruction_OR) }, + { 0x06, (this.Instruction_SHL_DX, this.Instruction_SHL, this.Instruction_NOP, this.Instruction_XOR) }, + { 0x07, (this.Instruction_SHR_DX, this.Instruction_SHR, this.Instruction_NOP, this.Instruction_NOP) }, + + { 0x08, (this.Instruction_INC_AX, this.Instruction_INC, this.Instruction_NOP, this.Instruction_ADD) }, + { 0x09, (this.Instruction_DEC_AX, this.Instruction_DEC, this.Instruction_NOP, this.Instruction_SUB) }, + { 0x0A, (this.Instruction_NOP, this.Instruction_NOP, this.Instruction_NOP, this.Instruction_DIV) }, + { 0x0B, (this.Instruction_NOP, this.Instruction_NOP, this.Instruction_NOP, this.Instruction_MUL) }, + { 0x0C, (this.Instruction_NOP, this.Instruction_NOP, this.Instruction_NOP, this.Instruction_MOD) }, + }; + } + private void Instruction_ADD(u8 m1, u8 m2) + { + u16 v1 = this.FetchData(m1); + u16 v2 = this.FetchData(m2); + + u16 result = (u16) (v1 + v2); + this.AX = result; + } + private void Instruction_SUB(u8 m1, u8 m2) + { + u16 v1 = this.FetchData(m1); + u16 v2 = this.FetchData(m2); + + u16 result = (u16)(v1 - v2); + this.AX = result; + } + private void Instruction_MUL(u8 m1, u8 m2) + { + u16 v1 = this.FetchData(m1); + u16 v2 = this.FetchData(m2); + + u16 result = (u16) (v1 * v2); + this.AX = result; + } + private void Instruction_DIV(u8 m1, u8 m2) + { + u16 v1 = this.FetchData(m1); + u16 v2 = this.FetchData(m2); + + if (v2 == 0) + throw new DivideByZeroException("Cannot perform division by zero."); + u16 result = (u16)(v1 / v2); + this.AX = result; + } + private void Instruction_MOD(u8 m1, u8 m2) + { + u16 v1 = this.FetchData(m1); + u16 v2 = this.FetchData(m2); + + if (v2 == 0) + throw new DivideByZeroException("Cannot perform modulo operation with divisor zero."); + u16 result = (u16)(v1 % v2); + this.AX = result; + } + private void Instruction_INC(u8 obj) + { + this.TransformData(obj, value => (u16)(value + 1)); + } + private void Instruction_DEC(u8 obj) + { + this.TransformData(obj, value => (u16)(value - 1)); + } + private void Instruction_INC_AX() + { + this.AX = (u16)(this.AX + 1); + } + private void Instruction_DEC_AX() + { + this.AX = (u16)(this.AX - 1); + } + private void Instruction_SHL(u8 obj) + { + this.TransformData(obj, value => (u16)(value << 1)); + } + private void Instruction_SHR(u8 obj) + { + this.TransformData(obj, value => (u16)(value >> 1)); + } + private void Instruction_SHL_DX() + { + this.DX = (u16)(this.DX << 1); + } + private void Instruction_SHR_DX() + { + this.DX = (u16)(this.DX >> 1); + } + private void Instruction_XOR(u8 m1, u8 m2) + { + u16 v1 = this.FetchData(m1); + u16 v2 = this.FetchData(m2); + + u16 result = (u16)(v1 ^ v2); + this.DX = result; + } + private void Instruction_OR(u8 m1, u8 m2) + { + u16 v1 = this.FetchData(m1); + u16 v2 = this.FetchData(m2); + + u16 result = (u16)(v1 | v2); + this.DX = result; + } + private void Instruction_INV(u8 obj) + { + u16 value = this.FetchData(obj); + u16 result = (u16)~value; + this.DX = result; + } + private void Instruction_INV_DX() + { + this.DX = (u16)~this.DX; + } + private void Instruction_AND(u8 m1, u8 m2) + { + u16 v1 = this.FetchData(m1); + u16 v2 = this.FetchData(m2); + + u16 result = (u16)(v1 & v2); + this.DX = result; + } + private void Instruction_NOT(u8 obj) + { + u16 value = this.FetchData(obj); + u16 result = value == 0 ? (u16)1 : (u16)0; + this.LX = result; + } + private void Instruction_NOT_LX() + { + this.LX = this.LX == 0 ? (u16)1 : (u16)0; + } + private void Instruction_GT(u8 m1, u8 m2) + { + u16 v1 = this.FetchData(m1); + u16 v2 = this.FetchData(m2); + + this.LX = (u16)(v1 > v2 ? 1 : 0); + } + private void Instruction_LT(u8 m1, u8 m2) + { + u16 v1 = this.FetchData(m1); + u16 v2 = this.FetchData(m2); + + this.LX = (u16)(v1 < v2 ? 1 : 0); + } + + private void Instruction_BR() + { + this.FetchInstruction(out _, out _, out _, out u8 length); + this.PC += length; + } + private void Instruction_BZ_LX() + { + if (this.LX == 0) + this.Instruction_BR(); + } + private void Instruction_BZ(u8 obj) + { + u16 value = this.FetchData(obj); + if (value == 0) + this.Instruction_BR(); + } + private void Instruction_CMP(u8 m1, u8 m2) + { + u16 v1 = this.FetchData(m1); + u16 v2 = this.FetchData(m2); + + // Set LX based on comparison + this.LX = (u16)(v1 == v2 ? 1 : 0); + } + private void Instruction_MOV(u8 m1, u8 m2) + { + u16 value = this.FetchData(m1); + this.StoreData(m2, value); + } + + public void Instruction_NOP() { } + public void Instruction_NOP(u8 m1) { } + public void Instruction_NOP(u8 m1, u8 m2) { } + + public void Instruction_JMP(u8 m1) + { + this.PC = this.FetchData(m1); + } + + public void Instruction_RET() + { + this.PC = this.PopStack(); + } + + public void Instruction_CALL(u8 m1) + { + u16 address = this.FetchData(m1); + this.PushStack(this.PC); + this.PC = address; + } + + public void Instruction_HALT() + { + throw new NotImplementedException("Halt instruction not implemented yet."); + } + + public void Instruction_SUS() + { + this.PC -= 2; + } +} \ No newline at end of file diff --git a/Chip/RamDevice.cs b/Chip/RamDevice.cs new file mode 100644 index 0000000..6811d22 --- /dev/null +++ b/Chip/RamDevice.cs @@ -0,0 +1,49 @@ +namespace Q1Emu.Chip; + +using System; +using System.Runtime.CompilerServices; + +public class RamDevice : BusDevice +{ + public u8[] Memory; + + public RamDevice(u16 start, u16 size) : base(start, (u16) (start + size)) + { + this.Memory = new u8[size]; + } + + public override void Reset() + { + this.Memory = new u8[this.AddressableEnd - this.AddressableStart]; + } + + public override void Clock() + { + // No clocking needed for RAM + } + + public override u8 Read(u16 address) + { + u16 relativeAddress = this.GetRelativeAddress(address); + return this.Memory[relativeAddress]; + } + + public override void Write(u16 address, u8 value) + { + u16 relativeAddress = this.GetRelativeAddress(address); + this.Memory[relativeAddress] = value; + } + + public override ref8 ReadSeq(u16 address, u16 length) + { + u16 relativeAddress = this.GetRelativeAddress(address); + // Console.WriteLine($"{address:X4} {relativeAddress:X4} {length:X4}"); + return new ref8(this.Memory, relativeAddress, length); + } + + public override void WriteSeq(u16 address, u16 length, ref8 value) + { + ref8 destination = this.ReadSeq(address, length); + value.CopyTo(destination); + } +} \ No newline at end of file diff --git a/Chip8Emu2.sln.DotSettings.user b/Chip8Emu2.sln.DotSettings.user new file mode 100644 index 0000000..d66483f --- /dev/null +++ b/Chip8Emu2.sln.DotSettings.user @@ -0,0 +1,2 @@ + + ForceIncluded \ No newline at end of file diff --git a/Content/Content.mgcb b/Content/Content.mgcb new file mode 100644 index 0000000..ddc4c36 --- /dev/null +++ b/Content/Content.mgcb @@ -0,0 +1,15 @@ + +#----------------------------- Global Properties ----------------------------# + +/outputDir:bin/$(Platform) +/intermediateDir:obj/$(Platform) +/platform:DesktopGL +/config: +/profile:Reach +/compress:False + +#-------------------------------- References --------------------------------# + + +#---------------------------------- Content ---------------------------------# + diff --git a/DisplayEngine.cs b/DisplayEngine.cs new file mode 100644 index 0000000..f77598f --- /dev/null +++ b/DisplayEngine.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using Q1Emu.Chip; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; + +namespace Q1Emu; + +public class DisplayEngine : Game +{ + private readonly Action _onScreenTextureLoaded; + private GraphicsDeviceManager _graphics; + private SpriteBatch _spriteBatch; + + private Texture2D _texture; + public int Scaling { get; } + public int Width { get; } + public int Height { get; } + + public DisplayEngine(int width, int height, int scaling, Action onScreenTextureLoaded) + { + this.Width = width; + this.Height = height; + this.Scaling = scaling; + this._onScreenTextureLoaded = onScreenTextureLoaded; + + _graphics = new GraphicsDeviceManager(this); + Content.RootDirectory = "Content"; + IsMouseVisible = true; + } + + protected override void Initialize() + { + this._texture = new Texture2D(GraphicsDevice, Width, Height); + this._onScreenTextureLoaded?.Invoke(this._texture); + // set window size + _graphics.PreferredBackBufferWidth = Width * Scaling; + _graphics.PreferredBackBufferHeight = Height * Scaling; + _graphics.ApplyChanges(); + + base.Initialize(); + } + + protected override void LoadContent() + { + _spriteBatch = new SpriteBatch(GraphicsDevice); + } + + protected override void Update(GameTime gameTime) + { + var state = Keyboard.GetState(); + + if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || state.IsKeyDown(Keys.Escape)) + Exit(); + + base.Update(gameTime); + } + + protected override void Draw(GameTime gameTime) + { + GraphicsDevice.Clear(Color.CornflowerBlue); + + _spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, DepthStencilState.None, RasterizerState.CullNone); + // Draw the texture to the screen + _spriteBatch.Draw(_texture, new Rectangle(0, 0, Width * Scaling, Height * Scaling), Color.White); + + _spriteBatch.End(); + + base.Draw(gameTime); + } +} \ No newline at end of file diff --git a/GlobalUsings.cs b/GlobalUsings.cs new file mode 100644 index 0000000..ff38a75 --- /dev/null +++ b/GlobalUsings.cs @@ -0,0 +1,12 @@ +// Integer type aliases +global using i8 = sbyte; // -128 to 127 +global using u8 = byte; // 0 to 255 +global using i16 = short; // -32,768 to 32,767 +global using u16 = ushort; // 0 to 65,535 +global using i32 = int; // -2,147,483,648 to 2,147,483,647 +global using u32 = uint; // 0 to 4,294,967,295 +global using i64 = long; // -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 +global using u64 = ulong; // 0 to 18,446,744,073,709,551,615 + +global using ref8 = System.Span; +global using data8 = byte[]; diff --git a/Icon.bmp b/Icon.bmp new file mode 100644 index 0000000..2b48165 Binary files /dev/null and b/Icon.bmp differ diff --git a/Icon.ico b/Icon.ico new file mode 100644 index 0000000..7d9dec1 Binary files /dev/null and b/Icon.ico differ diff --git a/Make.cs b/Make.cs new file mode 100644 index 0000000..6999c4d --- /dev/null +++ b/Make.cs @@ -0,0 +1,48 @@ +namespace Q1Emu; + +using System; + +public static class Make +{ + public static u16 u16(u8 high, u8 low) + { + return (u16) ((high << 8) | low); + } + + public static u16 u16(ref8 data) + { + if (data.Length < 2) + throw new ArgumentException("Data must contain at least 2 bytes to form a u16."); + + return u16(data[0], data[1]); + } + + public static u32 u32(u8 b1, u8 b2, u8 b3, u8 b4) + { + return (u32) ((b1 << 24) | (b2 << 16) | (b3 << 8) | b4); + } + + public static u32 u32(ref8 data) + { + if (data.Length < 4) + throw new ArgumentException("Data must contain at least 4 bytes to form a u32."); + + return u32(data[0], data[1], data[2], data[3]); + } + + public static ref8 ref8(u16 value) + { + u8 high = (u8) (value >> 8); + u8 low = (u8) (value & 0xFF); + return new ref8([high, low], 0, 2); + } + + public static ref8 ref8(u32 value) + { + u8 b1 = (u8) (value >> 24); + u8 b2 = (u8) (value >> 16); + u8 b3 = (u8) (value >> 8); + u8 b4 = (u8) (value & 0xFF); + return new ref8([b1, b2, b3, b4], 0, 4); + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..fab84f7 --- /dev/null +++ b/Program.cs @@ -0,0 +1,76 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using Microsoft.Xna.Framework; +using Q1Emu.Chip; +using Q1Emu; + +internal class Program +{ + public static void Main(string[] args) + { + if (args.Length > 0) + { + string filePath = args[0]; + if (filePath.EndsWith(".q1")) + { + Run(filePath); + } + else + { + Program.AssembleAndRun(filePath); + } + } + else + { + Console.WriteLine("Usage: Q1Emu "); + Console.WriteLine("Example: Q1Emu game.q1"); + Console.WriteLine("Example: Q1Emu game.asm"); + } + } + private static void AssembleAndRun(string filePath) + { + + } + private static void Run(string filePath) + { + Q1Cpu cpu = new(); + Bus bus = new(u16.MinValue, u16.MaxValue); + RamDevice ram = new(0x1FFF, 1024 * 8); + File.ReadAllBytes(filePath).CopyTo(ram.Memory, 0); + + cpu.PC = ram.AddressableStart; + + DisplayDevice display = new(16, 64, 32, 0x3FFF); + display.SetPalette([ + Color.Red, Color.Green, Color.Blue, Color.Yellow, + Color.Cyan, Color.Magenta, Color.White, Color.Black, + Color.Gray, Color.Orange, Color.Purple, Color.Brown, + Color.LightGray, Color.DarkGray, Color.LightBlue, Color.LightGreen]); + + cpu.Bus = bus; + ram.AttachToBus(bus); + + int frameRate = 600; + Thread t = new(() => + { + while (true) + { + cpu.Clock(); + display.Clock(); + Thread.Sleep(1000 / frameRate); + } + }); + t.IsBackground = true; + t.Start(); + + using DisplayEngine game = new(64, 32, 10, + tex => + { + display.OutputTexture = tex; + Console.WriteLine("Display texture initialized."); + }); + game.Run(); + } +} \ No newline at end of file diff --git a/Q1Emu.csproj b/Q1Emu.csproj new file mode 100644 index 0000000..f57964b --- /dev/null +++ b/Q1Emu.csproj @@ -0,0 +1,38 @@ + + + WinExe + net8.0 + Major + false + false + Chip8Emu2 + enable + + + app.manifest + Icon.ico + + + + + + + + Icon.ico + + + Icon.bmp + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Q1Emu.sln b/Q1Emu.sln new file mode 100644 index 0000000..801cf19 --- /dev/null +++ b/Q1Emu.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Q1Emu", "Q1Emu.csproj", "{04CE77A9-E0DC-4CAB-9382-5F84B4275B6B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {04CE77A9-E0DC-4CAB-9382-5F84B4275B6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04CE77A9-E0DC-4CAB-9382-5F84B4275B6B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04CE77A9-E0DC-4CAB-9382-5F84B4275B6B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04CE77A9-E0DC-4CAB-9382-5F84B4275B6B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Q1Emu.sln.DotSettings.user b/Q1Emu.sln.DotSettings.user new file mode 100644 index 0000000..1f447ab --- /dev/null +++ b/Q1Emu.sln.DotSettings.user @@ -0,0 +1,2 @@ + + ForceIncluded \ No newline at end of file diff --git a/ROMs/.DS_Store b/ROMs/.DS_Store new file mode 100644 index 0000000..28ca9c8 Binary files /dev/null and b/ROMs/.DS_Store differ diff --git a/ROMs/test.q1 b/ROMs/test.q1 new file mode 100644 index 0000000..d0c089f Binary files /dev/null and b/ROMs/test.q1 differ diff --git a/app.manifest b/app.manifest new file mode 100644 index 0000000..d9c2d8d --- /dev/null +++ b/app.manifest @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + + -- cgit v1.3.1