diff options
| author | Melodi <62381002+Melodi17@users.noreply.github.com> | 2025-08-04 00:58:48 +1000 |
|---|---|---|
| committer | Melodi <62381002+Melodi17@users.noreply.github.com> | 2025-08-04 00:58:48 +1000 |
| commit | 86452af2d48e2d1883dbfd67316e371f2a593bfe (patch) | |
| tree | ed9ffb784ea016ce3da63fc7ee45ac73e30bac94 | |
Initial commit
37 files changed, 1314 insertions, 0 deletions
diff --git a/.DS_Store b/.DS_Store Binary files differnew file mode 100644 index 0000000..7ae4f61 --- /dev/null +++ b/.DS_Store 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" /> +</project>
\ 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="UserContentModel"> + <attachedFolders /> + <explicitIncludes /> + <explicitExcludes /> + </component> +</project>
\ 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="$PROJECT_DIR$" vcs="Git" /> + </component> +</project>
\ 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" /> +</project>
\ 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="UserContentModel"> + <attachedFolders /> + <explicitIncludes /> + <explicitExcludes /> + </component> +</project>
\ 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="MaterialThemeProjectNewConfig"> + <option name="metadata"> + <MTProjectMetadataState> + <option name="userId" value="2ed817c6:198067820d1:-7fff" /> + </MTProjectMetadataState> + </option> + </component> +</project>
\ 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="" vcs="Git" /> + </component> +</project>
\ 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<BusDevice> 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<u16, u16> 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<u8, (Action, Action<u8>, Action, Action<u8, u8>)> InstructionLookup; + private void LoadInstructionLookup() + { + this.InstructionLookup = new Dictionary<u8, (Action, Action<u8>, Action, Action<u8, u8>)> + { + { 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 @@ +<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> + <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AString_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fc06673f4846f4984af370f3a7898b653d83800_003F85_003F06ad3dad_003FString_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
\ 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<Texture2D> _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<Texture2D> 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<byte>; +global using data8 = byte[]; diff --git a/Icon.bmp b/Icon.bmp Binary files differnew file mode 100644 index 0000000..2b48165 --- /dev/null +++ b/Icon.bmp diff --git a/Icon.ico b/Icon.ico Binary files differnew file mode 100644 index 0000000..7d9dec1 --- /dev/null +++ b/Icon.ico @@ -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 <file.q1|file.asm>"); + 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 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <OutputType>WinExe</OutputType> + <TargetFramework>net8.0</TargetFramework> + <RollForward>Major</RollForward> + <PublishReadyToRun>false</PublishReadyToRun> + <TieredCompilation>false</TieredCompilation> + <RootNamespace>Chip8Emu2</RootNamespace> + <Nullable>enable</Nullable> + </PropertyGroup> + <PropertyGroup> + <ApplicationManifest>app.manifest</ApplicationManifest> + <ApplicationIcon>Icon.ico</ApplicationIcon> + </PropertyGroup> + <ItemGroup> + <None Remove="Icon.ico"/> + <None Remove="Icon.bmp"/> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Icon.ico"> + <LogicalName>Icon.ico</LogicalName> + </EmbeddedResource> + <EmbeddedResource Include="Icon.bmp"> + <LogicalName>Icon.bmp</LogicalName> + </EmbeddedResource> + </ItemGroup> + <ItemGroup> + <PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.2.1105"/> + <PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.2.1105"/> + </ItemGroup> + <ItemGroup> + <Folder Include="ROMs\" /> + </ItemGroup> + <Target Name="RestoreDotnetTools" BeforeTargets="Restore"> + <Message Text="Restoring dotnet tools" Importance="High"/> + <Exec Command="dotnet tool restore"/> + </Target> +</Project>
\ 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 @@ +<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> + <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AGame_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F49bab08708434a08802510b75a8ea3691b3400_003F26_003F86c71a55_003FGame_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
\ No newline at end of file diff --git a/ROMs/.DS_Store b/ROMs/.DS_Store Binary files differnew file mode 100644 index 0000000..28ca9c8 --- /dev/null +++ b/ROMs/.DS_Store diff --git a/ROMs/test.q1 b/ROMs/test.q1 Binary files differnew file mode 100644 index 0000000..d0c089f --- /dev/null +++ b/ROMs/test.q1 diff --git a/app.manifest b/app.manifest new file mode 100644 index 0000000..d9c2d8d --- /dev/null +++ b/app.manifest @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> + <assemblyIdentity version="1.0.0.0" name="Chip8Emu2"/> + <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"> + <security> + <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3"> + <requestedExecutionLevel level="asInvoker" uiAccess="false" /> + </requestedPrivileges> + </security> + </trustInfo> + + <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <!-- A list of the Windows versions that this application has been tested on and is + is designed to work with. Uncomment the appropriate elements and Windows will + automatically selected the most compatible environment. --> + + <!-- Windows Vista --> + <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" /> + + <!-- Windows 7 --> + <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" /> + + <!-- Windows 8 --> + <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" /> + + <!-- Windows 8.1 --> + <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" /> + + <!-- Windows 10 --> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" /> + + </application> + </compatibility> + + <application xmlns="urn:schemas-microsoft-com:asm.v3"> + <windowsSettings> + <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> + <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> + </windowsSettings> + </application> + +</assembly> |
