From f4e279b9c8ee9fa1fffd4606d848e5acc18afdfa Mon Sep 17 00:00:00 2001 From: parsons Date: Sun, 10 May 2026 02:43:11 +0100 Subject: [PATCH] Got main system and VDP working! There is a display! --- Core/Cpu/Z80.cs | 54 +++++++++ Core/Io/SmsIoBus.cs | 20 ++-- Core/SmsMachine.cs | 78 ++++++++++--- Core/Video/SmsVdp.cs | 194 +++++++++++++++++++++++++++++++ Desktop/DebuggerForm.Designer.cs | 147 ++++++++++++----------- Desktop/DebuggerForm.cs | 32 ++++- Desktop/Form1.Designer.cs | 24 +++- Desktop/Form1.cs | 71 +++++++++-- 8 files changed, 516 insertions(+), 104 deletions(-) create mode 100644 Core/Video/SmsVdp.cs diff --git a/Core/Cpu/Z80.cs b/Core/Cpu/Z80.cs index 9c20215..943f318 100644 --- a/Core/Cpu/Z80.cs +++ b/Core/Cpu/Z80.cs @@ -1275,6 +1275,18 @@ namespace Core.Cpu switch (extendedOpcode) { + case 0x41: // OUT (C), B + _simpleIoBus.WritePort(BC.Word, BC.High); + return 12; + case 0x49: // OUT (C), C + _simpleIoBus.WritePort(BC.Word, BC.Low); + return 12; + case 0x61: // OUT (C), H + _simpleIoBus.WritePort(BC.Word, HL.High); + return 12; + case 0x69: // OUT (C), L + _simpleIoBus.WritePort(BC.Word, HL.Low); + return 12; case 0x43: // LD (nn), BC ushort dest43 = FetchWord(); WriteMemory(dest43, BC.Low); @@ -1494,6 +1506,48 @@ namespace Core.Cpu if ((n & 0x02) != 0) AF.Low |= 0x20; // Bit 5 from bit 1 return 16; } + case 0xA3: // OUTI + { + // 1. Read data from memory at HL + byte valA3 = ReadMemory(HL.Word); + + // 2. Decrement the B register + BC.High--; + + // 3. Output the data to the port specified by C + _simpleIoBus.WritePort(BC.Word, valA3); + + // 4. Increment the memory pointer + HL.Word++; + + // 5. Update Flags (N is always set. Z is set if B reached 0) + AF.Low |= 0x02; + if (BC.High == 0) AF.Low |= 0x40; + else AF.Low &= 0xBF; + + return 16; + } + case 0xB3: // OTIR + { + // This does exactly the same thing as OUTI, but loops until B == 0 + byte valB3 = ReadMemory(HL.Word); + BC.High--; + _simpleIoBus.WritePort(BC.Word, valB3); + HL.Word++; + + AF.Low |= 0x02; + if (BC.High != 0) + { + AF.Low &= 0xBF; // Z is reset + PC -= 2; // Loop back and execute ED B3 again! + return 21; + } + else + { + AF.Low |= 0x40; // Z is set + return 16; + } + } case 0xB0: // LDIR { byte val00 = ReadMemory(HL.Word); diff --git a/Core/Io/SmsIoBus.cs b/Core/Io/SmsIoBus.cs index 8a88fcc..fd413b8 100644 --- a/Core/Io/SmsIoBus.cs +++ b/Core/Io/SmsIoBus.cs @@ -1,11 +1,11 @@ using Core.Interfaces; +using Core.Video; namespace Core.Io { public class SmsIoBus : IIoBus { - // We will wire these up in the next phases! - // public Vdp VideoProcessor { get; set; } + public SmsVdp VideoProcessor { get; set; } // public Psg AudioProcessor { get; set; } // Joypad State (0xFF means no buttons pressed - the SMS uses Active-Low logic!) @@ -18,11 +18,17 @@ namespace Core.Io // hardware only physically wires up the bottom 8 bits. byte lowerPort = (byte)(port & 0xFF); + if (lowerPort == 0x7E) + { + // VDP V-Counter (Vertical Scanline Position) + return VideoProcessor.ReadVCounter(); + } + if (lowerPort >= 0x80 && lowerPort <= 0xBF) { - // VDP Read (Usually 0xBE for VRAM Data, 0xBF for Status Flags) - // return VideoProcessor.ReadPort(lowerPort); - return 0x00; + // Even ports (like 0xBE) are Data. Odd ports (like 0xBF) are Control. + if ((lowerPort & 0x01) == 0) return VideoProcessor.ReadDataPort(); + else return VideoProcessor.ReadControlPort(); } if (lowerPort == 0xDC) { @@ -49,8 +55,8 @@ namespace Core.Io } else if (lowerPort >= 0x80 && lowerPort <= 0xBF) { - // VDP Write (Usually 0xBE for VRAM Data, 0xBF for Control Registers) - // VideoProcessor.WritePort(lowerPort, value); + if ((lowerPort & 0x01) == 0) VideoProcessor.WriteDataPort(value); + else VideoProcessor.WriteControlPort(value); } else if (lowerPort <= 0x3F) { diff --git a/Core/SmsMachine.cs b/Core/SmsMachine.cs index f4b56bb..ed71cea 100644 --- a/Core/SmsMachine.cs +++ b/Core/SmsMachine.cs @@ -1,6 +1,9 @@ using Core.Cpu; using Core.Io; using Core.Memory; +using System; +using System.IO; +using System.Collections.Generic; namespace Core { @@ -9,18 +12,17 @@ namespace Core public Z80 Cpu { get; private set; } public SmsMemoryBus MemoryBus { get; private set; } public SmsIoBus IoBus { get; private set; } + public Core.Video.SmsVdp VideoProcessor { get; private set; } public ushort? Breakpoint { get; set; } = null; // NTSC SMS T-States per frame public const int TStatesPerFrame = 59736; - public long TotalFrameCount { get; private set; } = 0; - public double FramesPerSecond { get; private set; } = 0; - public double FrameTime { get; private set; } = 0; public SmsMachine() { MemoryBus = new SmsMemoryBus(); - IoBus = new SmsIoBus(); + VideoProcessor = new Core.Video.SmsVdp(); + IoBus = new SmsIoBus { VideoProcessor = this.VideoProcessor }; Cpu = new Z80(MemoryBus, IoBus); } @@ -34,8 +36,23 @@ namespace Core { MemoryBus.CleanRAMData(); Cpu.Reset(); + } - // We will reset the VDP and PSG here later! + public int StepMachine() + { + // 1. Tick the CPU + int tStates = Cpu.Step(); + + // 2. Tell the VDP how much time just passed + VideoProcessor.Update(tStates); + + // 3. Trigger interrupts if the VDP hit scanline 192 + if (VideoProcessor.InterruptPending) + { + tStates += Cpu.RequestInterrupt(); + } + + return tStates; } public void RunFrame() @@ -44,18 +61,51 @@ namespace Core while (currentFrameTStates < TStatesPerFrame) { - int tStates = Cpu.Step(); - currentFrameTStates += tStates; + currentFrameTStates += StepMachine(); + string filePath = "captured_data.txt"; - // --- FUTURE EXPANSION --- - // VideoProcessor.Update(tStates); - // AudioProcessor.Update(tStates); + // Mock data to loop through + //List sensorReadings = new List { Cpu.PC, Cpu.AF.Word, Cpu.BC.Word, Cpu.DE.Word, Cpu.HL.Word, Cpu.SP}; + //List type = new List {"PC: 0x", "AF: 0x", "BC: 0x", "DE: 0x", "HL: 0x", "SP: 0x" }; - // if (VideoProcessor.IsVBlanking && VideoProcessor.InterruptsEnabled) - // { - // Cpu.RequestInterrupt(); - // } + //try + //{ + // // 2. Initialize StreamWriter within a 'using' block + // // The 'true' parameter means "append" to the file. Use 'false' to overwrite. + // using (StreamWriter writer = new StreamWriter(filePath, append: true)) + // { + // foreach (int reading in sensorReadings) + // { + // string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + + // // 3. Construct your string and write it + // foreach (string _type in type) + // { + // string line = $"{timestamp} | {_type} {reading}"; + // writer.WriteLine(line); + // } + + + // // Optional: Console feedback + // //Console.WriteLine($"Logged: {line}"); + // } + // } + // // File is automatically closed and saved here + + // //Console.WriteLine("Data capture complete."); + //} + //catch (IOException e) + //{ + // Console.WriteLine($"An error occurred: {e.Message}"); + //} + + // THE TRIPWIRE: Check the breakpoint after EVERY single instruction! + if (Breakpoint.HasValue && Cpu.PC == Breakpoint.Value) + { + break; // Abort the frame loop immediately! + } } + } } } \ No newline at end of file diff --git a/Core/Video/SmsVdp.cs b/Core/Video/SmsVdp.cs new file mode 100644 index 0000000..78b05fd --- /dev/null +++ b/Core/Video/SmsVdp.cs @@ -0,0 +1,194 @@ +using System; + +namespace Core.Video +{ + public class SmsVdp + { + // The VDP's private memory! The CPU cannot touch these arrays directly. + public byte[] VRAM { get; private set; } = new byte[0x4000]; // 16KB Video RAM + public byte[] CRAM { get; private set; } = new byte[0x20]; // 32 Bytes Color Palette + public byte[] Registers { get; private set; } = new byte[16]; // 11 Hardware Control Registers + public int[] FrameBuffer { get; private set; } = new int[256 * 192]; + + // The Control Port State Machine (Port 0xBF) + private bool _isSecondControlByte = false; + private ushort _controlWord = 0; + private byte _readBuffer = 0; + + private int _tStateCounter = 0; + private int _currentScanline = 0; + private byte _statusRegister = 0x00; + + public bool InterruptPending => (_statusRegister & 0x80) != 0 && (Registers[1] & 0x20) != 0; + + public byte ReadDataPort() // Port 0xBE + { + _isSecondControlByte = false; // Reading data resets the control latch + byte value = _readBuffer; + _readBuffer = VRAM[_controlWord & 0x3FFF]; + _controlWord++; + return value; + } + + public byte ReadControlPort() // Port 0xBF + { + _isSecondControlByte = false; + byte currentStatus = _statusRegister; + + // CRITICAL HARDWARE QUIRK: Reading the status port physically + // clears the flags inside the chip! If we don't clear this, + // the interrupt line gets stuck on forever. + _statusRegister = 0x00; + + return currentStatus; + } + + public void WriteDataPort(byte value) // Port 0xBE + { + _isSecondControlByte = false; + _readBuffer = value; + + int address = _controlWord & 0x3FFF; + int command = (_controlWord >> 14) & 0x03; + + if (command == 3) // Code 3: Write to Color Palette (CRAM) + { + CRAM[address & 0x1F] = value; + } + else // Code 0, 1, 2: Write to VRAM + { + VRAM[address] = value; + } + _controlWord++; // Auto-increment so the Z80 can blast data fast + } + + public void WriteControlPort(byte value) // Port 0xBF + { + if (!_isSecondControlByte) + { + // First byte arrives: Store it in the lower 8 bits + _controlWord = (ushort)((_controlWord & 0xFF00) | value); + _isSecondControlByte = true; + } + else + { + // Second byte arrives: Store it in the upper 8 bits and execute! + _controlWord = (ushort)((_controlWord & 0x00FF) | (value << 8)); + _isSecondControlByte = false; + + int command = (_controlWord >> 14) & 0x03; + + if (command == 0) // Code 0: Prep for VRAM Read + { + _readBuffer = VRAM[_controlWord & 0x3FFF]; + _controlWord++; + } + else if (command == 2) // Code 2: Write to Internal VDP Register + { + int regIndex = value & 0x0F; + byte regData = (byte)(_controlWord & 0xFF); + + if (regIndex < 16) Registers[regIndex] = regData; + } + } + } + + public byte ReadVCounter() + { + // Note: On real NTSC hardware, the V-Counter jumps slightly around + // the VBlank period to keep the math 8-bit, but simply returning + // the raw current scanline is perfectly fine to get us booting! + return (byte)_currentScanline; + } + + public void Update(int tStates) + { + _tStateCounter += tStates; + + // 228 T-States per scanline + if (_tStateCounter >= 228) + { + _tStateCounter -= 228; + _currentScanline++; + + // Line 192 is the exact moment the screen finishes drawing! + if (_currentScanline == 192) + { + _statusRegister |= 0x80; // Set Bit 7 (VBlank Flag) to 1 + + RenderBackground(); // <--- DRAW THE FRAME! + } + + // End of the NTSC frame (262 lines) + if (_currentScanline > 261) + { + _currentScanline = 0; + } + } + } + + private void RenderBackground() + { + // The Name Table base address is stored in VDP Register 2. + // It tells us where in VRAM the 32x24 screen grid starts. + ushort nameTableBase = (ushort)((Registers[2] & 0x0E) << 10); + + // Loop through all 24 rows and 32 columns of the screen + for (int row = 0; row < 24; row++) + { + for (int col = 0; col < 32; col++) + { + // 1. Read the 16-bit Tile instruction from the Name Table + ushort nameTableAddr = (ushort)(nameTableBase + (row * 64) + (col * 2)); + byte lowByte = VRAM[nameTableAddr]; + byte highByte = VRAM[nameTableAddr + 1]; + ushort tileData = (ushort)((highByte << 8) | lowByte); + + // 2. Extract the Tile Index and Palette Info + int tileIndex = tileData & 0x01FF; + bool useSpritePalette = (tileData & 0x0800) != 0; + + // 3. Find the actual pixel data for this tile in VRAM + // Each 8x8 tile takes exactly 32 bytes in memory + ushort tileAddress = (ushort)(tileIndex * 32); + + // 4. Draw the 8x8 block of pixels! + for (int y = 0; y < 8; y++) + { + // The SMS uses 4 bitplanes to make a single row of pixels. + byte bp0 = VRAM[tileAddress + (y * 4) + 0]; + byte bp1 = VRAM[tileAddress + (y * 4) + 1]; + byte bp2 = VRAM[tileAddress + (y * 4) + 2]; + byte bp3 = VRAM[tileAddress + (y * 4) + 3]; + + for (int x = 0; x < 8; x++) + { + // Combine 1 bit from each bitplane to get a color index (0-15) + int shift = 7 - x; + int colorIndex = ((bp0 >> shift) & 1) | + (((bp1 >> shift) & 1) << 1) | + (((bp2 >> shift) & 1) << 2) | + (((bp3 >> shift) & 1) << 3); + + // Find the raw SMS color in CRAM + int paletteOffset = useSpritePalette ? 16 : 0; + byte smsColor = CRAM[paletteOffset + colorIndex]; + + // Translate SMS 00BBGGRR format to Windows 32-bit ARGB + int r = (smsColor & 0x03) * 85; + int g = ((smsColor >> 2) & 0x03) * 85; + int b = ((smsColor >> 4) & 0x03) * 85; + + // Calculate where this pixel goes on the final 256x192 screen + int pixelX = (col * 8) + x; + int pixelY = (row * 8) + y; + + FrameBuffer[(pixelY * 256) + pixelX] = (255 << 24) | (r << 16) | (g << 8) | b; + } + } + } + } + } + + } +} \ No newline at end of file diff --git a/Desktop/DebuggerForm.Designer.cs b/Desktop/DebuggerForm.Designer.cs index 1a8b7d6..ecf17d3 100644 --- a/Desktop/DebuggerForm.Designer.cs +++ b/Desktop/DebuggerForm.Designer.cs @@ -57,95 +57,95 @@ lblFrameTime = new Label(); richTextBox1 = new RichTextBox(); button1 = new Button(); - button2 = new Button(); + CpuRun = new Button(); SuspendLayout(); // // lblAF // lblAF.AutoSize = true; - lblAF.Location = new Point(10, 7); + lblAF.Location = new Point(12, 9); lblAF.Margin = new Padding(2, 0, 2, 0); lblAF.Name = "lblAF"; - lblAF.Size = new Size(26, 20); + lblAF.Size = new Size(33, 25); lblAF.TabIndex = 0; lblAF.Text = "AF"; // // lblBC // lblBC.AutoSize = true; - lblBC.Location = new Point(9, 48); + lblBC.Location = new Point(11, 60); lblBC.Margin = new Padding(2, 0, 2, 0); lblBC.Name = "lblBC"; - lblBC.Size = new Size(27, 20); + lblBC.Size = new Size(33, 25); lblBC.TabIndex = 1; lblBC.Text = "BC"; // // lblDE // lblDE.AutoSize = true; - lblDE.Location = new Point(10, 100); + lblDE.Location = new Point(12, 125); lblDE.Margin = new Padding(2, 0, 2, 0); lblDE.Name = "lblDE"; - lblDE.Size = new Size(28, 20); + lblDE.Size = new Size(34, 25); lblDE.TabIndex = 2; lblDE.Text = "DE"; // // lblHL // lblHL.AutoSize = true; - lblHL.Location = new Point(10, 150); + lblHL.Location = new Point(12, 188); lblHL.Margin = new Padding(2, 0, 2, 0); lblHL.Name = "lblHL"; - lblHL.Size = new Size(27, 20); + lblHL.Size = new Size(33, 25); lblHL.TabIndex = 3; lblHL.Text = "HL"; // // lblPC // lblPC.AutoSize = true; - lblPC.Location = new Point(9, 200); + lblPC.Location = new Point(11, 250); lblPC.Margin = new Padding(2, 0, 2, 0); lblPC.Name = "lblPC"; - lblPC.Size = new Size(26, 20); + lblPC.Size = new Size(33, 25); lblPC.TabIndex = 4; lblPC.Text = "PC"; // // lblSP // lblSP.AutoSize = true; - lblSP.Location = new Point(9, 252); + lblSP.Location = new Point(11, 315); lblSP.Margin = new Padding(2, 0, 2, 0); lblSP.Name = "lblSP"; - lblSP.Size = new Size(25, 20); + lblSP.Size = new Size(32, 25); lblSP.TabIndex = 6; lblSP.Text = "SP"; // // lblFlags // lblFlags.AutoSize = true; - lblFlags.Location = new Point(88, 52); + lblFlags.Location = new Point(110, 65); lblFlags.Margin = new Padding(2, 0, 2, 0); lblFlags.Name = "lblFlags"; - lblFlags.Size = new Size(43, 20); + lblFlags.Size = new Size(53, 25); lblFlags.TabIndex = 7; lblFlags.Text = "Flags"; // // lblTStates // lblTStates.AutoSize = true; - lblTStates.Location = new Point(88, 7); + lblTStates.Location = new Point(110, 9); lblTStates.Margin = new Padding(2, 0, 2, 0); lblTStates.Name = "lblTStates"; - lblTStates.Size = new Size(63, 20); + lblTStates.Size = new Size(75, 25); lblTStates.TabIndex = 8; lblTStates.Text = "T-States"; // // txtMemoryStart // - txtMemoryStart.Location = new Point(300, 21); + txtMemoryStart.Location = new Point(375, 26); txtMemoryStart.Margin = new Padding(2); txtMemoryStart.Name = "txtMemoryStart"; - txtMemoryStart.Size = new Size(121, 27); + txtMemoryStart.Size = new Size(150, 31); txtMemoryStart.TabIndex = 9; txtMemoryStart.Text = "Memory Start"; txtMemoryStart.TextAlign = HorizontalAlignment.Center; @@ -153,10 +153,10 @@ // // btnRefreshMemory // - btnRefreshMemory.Location = new Point(425, 21); + btnRefreshMemory.Location = new Point(531, 26); btnRefreshMemory.Margin = new Padding(2); btnRefreshMemory.Name = "btnRefreshMemory"; - btnRefreshMemory.Size = new Size(90, 27); + btnRefreshMemory.Size = new Size(112, 34); btnRefreshMemory.TabIndex = 14; btnRefreshMemory.Text = "Refresh Memory"; btnRefreshMemory.UseVisualStyleBackColor = true; @@ -164,109 +164,117 @@ // // txtMemoryView // - txtMemoryView.Location = new Point(88, 80); + txtMemoryView.Location = new Point(110, 100); txtMemoryView.Margin = new Padding(2); txtMemoryView.Name = "txtMemoryView"; - txtMemoryView.Size = new Size(443, 895); + txtMemoryView.Size = new Size(553, 1118); txtMemoryView.TabIndex = 15; txtMemoryView.Text = "Memory View Window"; // // lstDisassembly // lstDisassembly.FormattingEnabled = true; - lstDisassembly.Location = new Point(567, 8); + lstDisassembly.ItemHeight = 25; + lstDisassembly.Location = new Point(709, 10); lstDisassembly.Margin = new Padding(2); lstDisassembly.Name = "lstDisassembly"; - lstDisassembly.Size = new Size(252, 264); + lstDisassembly.Size = new Size(314, 329); lstDisassembly.TabIndex = 16; // // lstStack // lstStack.FormattingEnabled = true; - lstStack.Location = new Point(823, 8); + lstStack.ItemHeight = 25; + lstStack.Location = new Point(1029, 10); lstStack.Margin = new Padding(2); lstStack.Name = "lstStack"; - lstStack.Size = new Size(130, 264); + lstStack.Size = new Size(162, 329); lstStack.TabIndex = 17; // // label1 // label1.AutoSize = true; - label1.Location = new Point(568, 298); + label1.Location = new Point(710, 372); + label1.Margin = new Padding(4, 0, 4, 0); label1.Name = "label1"; - label1.Size = new Size(81, 20); + label1.Size = new Size(97, 25); label1.TabIndex = 19; label1.Text = "Breakpoint"; // // txtBreakpoint // - txtBreakpoint.Location = new Point(655, 295); + txtBreakpoint.Location = new Point(819, 369); + txtBreakpoint.Margin = new Padding(4); txtBreakpoint.Name = "txtBreakpoint"; - txtBreakpoint.Size = new Size(125, 27); + txtBreakpoint.Size = new Size(155, 31); txtBreakpoint.TabIndex = 20; // // label2 // label2.AutoSize = true; - label2.Location = new Point(233, 21); + label2.Location = new Point(291, 26); + label2.Margin = new Padding(4, 0, 4, 0); label2.Name = "label2"; - label2.Size = new Size(62, 20); + label2.Size = new Size(77, 25); label2.TabIndex = 21; label2.Text = "Address"; // // lblIX // lblIX.AutoSize = true; - lblIX.Location = new Point(9, 298); + lblIX.Location = new Point(11, 372); lblIX.Margin = new Padding(2, 0, 2, 0); lblIX.Name = "lblIX"; - lblIX.Size = new Size(22, 20); + lblIX.Size = new Size(28, 25); lblIX.TabIndex = 22; lblIX.Text = "IX"; // // lblIY // lblIY.AutoSize = true; - lblIY.Location = new Point(10, 344); + lblIY.Location = new Point(12, 430); lblIY.Margin = new Padding(2, 0, 2, 0); lblIY.Name = "lblIY"; - lblIY.Size = new Size(21, 20); + lblIY.Size = new Size(27, 25); lblIY.TabIndex = 23; lblIY.Text = "IY"; // // lblIff1 // lblIff1.AutoSize = true; - lblIff1.Location = new Point(567, 411); + lblIff1.Location = new Point(709, 514); + lblIff1.Margin = new Padding(4, 0, 4, 0); lblIff1.Name = "lblIff1"; - lblIff1.Size = new Size(35, 20); + lblIff1.Size = new Size(45, 25); lblIff1.TabIndex = 24; lblIff1.Text = "IFF1"; // // lblIff2 // lblIff2.AutoSize = true; - lblIff2.Location = new Point(567, 458); + lblIff2.Location = new Point(709, 572); + lblIff2.Margin = new Padding(4, 0, 4, 0); lblIff2.Name = "lblIff2"; - lblIff2.Size = new Size(35, 20); + lblIff2.Size = new Size(45, 25); lblIff2.TabIndex = 25; lblIff2.Text = "IFF2"; // // lblIE // lblIE.AutoSize = true; - lblIE.Location = new Point(568, 370); + lblIE.Location = new Point(710, 462); + lblIE.Margin = new Padding(4, 0, 4, 0); lblIE.Name = "lblIE"; - lblIE.Size = new Size(109, 20); + lblIE.Size = new Size(133, 25); lblIE.TabIndex = 26; lblIE.Text = "Interrupt Mode"; // // btnReset // - btnReset.Location = new Point(651, 327); + btnReset.Location = new Point(814, 409); btnReset.Margin = new Padding(2); btnReset.Name = "btnReset"; - btnReset.Size = new Size(132, 27); + btnReset.Size = new Size(165, 34); btnReset.TabIndex = 27; btnReset.Text = "Set Breakpoint"; btnReset.UseVisualStyleBackColor = true; @@ -281,69 +289,72 @@ // lblFrames // lblFrames.AutoSize = true; - lblFrames.Location = new Point(567, 682); + lblFrames.Location = new Point(709, 852); lblFrames.Margin = new Padding(2, 0, 2, 0); lblFrames.Name = "lblFrames"; - lblFrames.Size = new Size(124, 20); + lblFrames.Size = new Size(149, 25); lblFrames.TabIndex = 28; lblFrames.Text = "Frames Rendered"; // // lblFPS // lblFPS.AutoSize = true; - lblFPS.Location = new Point(659, 759); + lblFPS.Location = new Point(824, 949); lblFPS.Margin = new Padding(2, 0, 2, 0); lblFPS.Name = "lblFPS"; - lblFPS.Size = new Size(32, 20); + lblFPS.Size = new Size(41, 25); lblFPS.TabIndex = 29; lblFPS.Text = "FPS"; // // lblFrameTime // lblFrameTime.AutoSize = true; - lblFrameTime.Location = new Point(604, 718); + lblFrameTime.Location = new Point(755, 898); lblFrameTime.Margin = new Padding(2, 0, 2, 0); lblFrameTime.Name = "lblFrameTime"; - lblFrameTime.Size = new Size(87, 20); + lblFrameTime.Size = new Size(104, 25); lblFrameTime.TabIndex = 30; lblFrameTime.Text = "Frame Time"; // // richTextBox1 // richTextBox1.Enabled = false; - richTextBox1.Location = new Point(567, 509); + richTextBox1.Location = new Point(709, 636); + richTextBox1.Margin = new Padding(4); richTextBox1.Name = "richTextBox1"; richTextBox1.ReadOnly = true; - richTextBox1.Size = new Size(304, 128); + richTextBox1.Size = new Size(379, 159); richTextBox1.TabIndex = 32; richTextBox1.Text = "Sega Master System Memory Map:\n0x0000 - 0x3FFF: ROM Slot 0 (16KB)\n0x4000 - 0x7FFF: ROM Slot 1 (16KB)\n0x8000 - 0xBFFF: ROM Slot 2 (16KB)\n0xC000 - 0xDFFF: System RAM (8KB)\n0xE000 - 0xFFFF: RAM Mirror"; // // button1 // - button1.Location = new Point(555, 824); + button1.Location = new Point(694, 1030); + button1.Margin = new Padding(4); button1.Name = "button1"; - button1.Size = new Size(94, 29); + button1.Size = new Size(118, 36); button1.TabIndex = 33; button1.Text = "CPU Step"; button1.UseVisualStyleBackColor = true; button1.Click += btnStep_Click; // - // button2 + // CpuRun // - button2.Location = new Point(555, 883); - button2.Name = "button2"; - button2.Size = new Size(94, 29); - button2.TabIndex = 34; - button2.Text = "CPU Run"; - button2.UseVisualStyleBackColor = true; - button2.Click += btnStep_Click; + CpuRun.Location = new Point(694, 1104); + CpuRun.Margin = new Padding(4); + CpuRun.Name = "CpuRun"; + CpuRun.Size = new Size(118, 36); + CpuRun.TabIndex = 34; + CpuRun.Text = "CPU Run"; + CpuRun.UseVisualStyleBackColor = true; + CpuRun.Click += CpuRun_Click; // // DebuggerForm // - AutoScaleDimensions = new SizeF(8F, 20F); + AutoScaleDimensions = new SizeF(10F, 25F); AutoScaleMode = AutoScaleMode.Font; - ClientSize = new Size(965, 990); - Controls.Add(button2); + ClientSize = new Size(1206, 1238); + Controls.Add(CpuRun); Controls.Add(button1); Controls.Add(richTextBox1); Controls.Add(lblFrameTime); @@ -402,13 +413,13 @@ private Label lblIff2; private Label lblIE; private Button btnReset; - private System.Windows.Forms.Timer uiUpdateTimer; private Label lblFrames; private Label lblFPS; private Label lblFrameTime; private RichTextBox richTextBox1; private Button button1; - private Button button2; + private Button CpuRun; + public System.Windows.Forms.Timer uiUpdateTimer; //private TextBox textBox4; } } \ No newline at end of file diff --git a/Desktop/DebuggerForm.cs b/Desktop/DebuggerForm.cs index 76a0482..43e324f 100644 --- a/Desktop/DebuggerForm.cs +++ b/Desktop/DebuggerForm.cs @@ -27,11 +27,36 @@ namespace Desktop _mainForm = mainForm; } + private void CpuRun_Click(object sender, EventArgs e) + { + if (_mainForm.IsRunning) + { + // Stop the machine + _mainForm.StopEmulator(); + CpuRun.Text = "CPU Run"; + + // Stop the live UI updates and do one final manual refresh + uiUpdateTimer.Stop(); + UpdateDisplay(); + } + else + { + // Start the machine + _mainForm.StartEmulator(); + CpuRun.Text = "CPU Stop"; + + // Start the timer so the debugger screen updates automatically + // (Make sure your uiUpdateTimer Interval in the designer is set to something like 100ms) + uiUpdateTimer.Start(); + } + } + private void btnStep_Click(object sender, EventArgs e) { try { - _cpu.Step(); + // Ask the main form to step the WHOLE machine, not just the Z80! + _mainForm.StepEmulator(); UpdateDisplay(); } catch (Exception ex) @@ -57,7 +82,7 @@ namespace Desktop UpdateDisplay(); } - private void uiUpdateTimer_Tick(object sender, EventArgs e) + public void uiUpdateTimer_Tick(object sender, EventArgs e) { UpdateDisplay(); } @@ -180,6 +205,9 @@ namespace Desktop case 0x02: mnemonic = "LD (BC), A"; break; // --- 16-Bit Increments --- case 0x03: mnemonic = "INC BC"; break; + case 0x07: + mnemonic = "RLCA"; + break; case 0x08: mnemonic = "EX AF, AF'"; break; diff --git a/Desktop/Form1.Designer.cs b/Desktop/Form1.Designer.cs index 885ec6a..d87508f 100644 --- a/Desktop/Form1.Designer.cs +++ b/Desktop/Form1.Designer.cs @@ -29,32 +29,48 @@ private void InitializeComponent() { button1 = new Button(); + pbScreen = new PictureBox(); + ((System.ComponentModel.ISupportInitialize)pbScreen).BeginInit(); SuspendLayout(); // // button1 // - button1.Location = new Point(304, 268); + button1.Location = new Point(1294, 13); + button1.Margin = new Padding(4, 4, 4, 4); button1.Name = "button1"; - button1.Size = new Size(94, 29); + button1.Size = new Size(118, 36); button1.TabIndex = 0; button1.Text = "button1"; button1.UseVisualStyleBackColor = true; button1.Click += button1_Click; // + // pbScreen + // + pbScreen.Location = new Point(23, 59); + pbScreen.Name = "pbScreen"; + pbScreen.Size = new Size(1390, 760); + pbScreen.SizeMode = PictureBoxSizeMode.Zoom; + pbScreen.TabIndex = 1; + pbScreen.TabStop = false; + // // Form1 // - AutoScaleDimensions = new SizeF(8F, 20F); + AutoScaleDimensions = new SizeF(10F, 25F); AutoScaleMode = AutoScaleMode.Font; - ClientSize = new Size(800, 450); + ClientSize = new Size(1425, 842); + Controls.Add(pbScreen); Controls.Add(button1); + Margin = new Padding(4, 4, 4, 4); Name = "Form1"; Text = "Form1"; Click += button1_Click; + ((System.ComponentModel.ISupportInitialize)pbScreen).EndInit(); ResumeLayout(false); } #endregion private Button button1; + private PictureBox pbScreen; } } diff --git a/Desktop/Form1.cs b/Desktop/Form1.cs index 41e458f..ac8718e 100644 --- a/Desktop/Form1.cs +++ b/Desktop/Form1.cs @@ -1,12 +1,21 @@ using System.Reflection; using System.Reflection.PortableExecutable; using Core; +using System.Threading.Tasks; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; +using System.Windows.Forms; + namespace Desktop { public partial class Form1 : Form { private SmsMachine _machine = null!; private DebuggerForm _debugger; + private Bitmap _screenBitmap = new Bitmap(256, 192, PixelFormat.Format32bppArgb); + + + public bool IsRunning { get; private set; } = false; public ushort? Breakpoint { @@ -20,23 +29,67 @@ namespace Desktop _machine = new SmsMachine(); } + private void DrawScreen() + { + // Rapidly copy our VDP FrameBuffer into the Windows Bitmap + var data = _screenBitmap.LockBits(new Rectangle(0, 0, 256, 192), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); + Marshal.Copy(_machine.VideoProcessor.FrameBuffer, 0, data.Scan0, _machine.VideoProcessor.FrameBuffer.Length); + _screenBitmap.UnlockBits(data); + + // Update the PictureBox + pbScreen.Image = _screenBitmap; + } + + public void StartEmulator() + { + if (IsRunning) return; + IsRunning = true; + + // Fire off a background task so we don't freeze the Windows UI! + Task.Run(() => + { + while (IsRunning) + { + _machine.RunFrame(); + + Invoke((System.Windows.Forms.MethodInvoker)delegate { DrawScreen(); }); + + // Safety catch: If we hit a breakpoint while running, stop the loop! + if (_machine.Breakpoint.HasValue && _machine.Cpu.PC == _machine.Breakpoint.Value) + { + IsRunning = false; + + // Optional: Force the debugger UI to update immediately so you see it! + Invoke((System.Windows.Forms.MethodInvoker)delegate { _debugger?.uiUpdateTimer_Tick(null, EventArgs.Empty); }); + } + else + { + // Only throttle the speed if we are actively running + Thread.Sleep(16); + } + } + }); + } + + public void StopEmulator() + { + IsRunning = false; + } + + + public void StepEmulator() + { + _machine.StepMachine(); + } + private void button1_Click(object sender, EventArgs e) { // 1. Load a commercial Master System ROM! byte[] rom = File.ReadAllBytes(@"C:\Parsons\Local Code Projects\ParsonsMasterSystem2026\Desktop\ROMS\Golden Axe Warrior.sms"); - - - try - { // 2. Jam it into the Sega Mapper _machine.LoadCartridge(rom); - } - catch (Exception ex) - { - MessageBox.Show(ex.Message); - } // 3. Open the Debugger to look around if (_debugger == null || _debugger.IsDisposed)