From ed64eb2ebe3d73022ef163189a0ea730d7802391 Mon Sep 17 00:00:00 2001 From: Marc Parsons Date: Sun, 19 Apr 2026 00:26:00 +0100 Subject: [PATCH] Implemented SNA files. More OpCodes. Chuckie Egg Title SCreen! --- Core/Cpu/Z80.cs | 186 ++++++++++++++++++++++++++++++++++---- Desktop/DebuggerForm.cs | 14 ++- Desktop/Form1.Designer.cs | 15 ++- Desktop/Form1.cs | 12 +++ 4 files changed, 204 insertions(+), 23 deletions(-) diff --git a/Core/Cpu/Z80.cs b/Core/Cpu/Z80.cs index f5fa896..fefe95f 100644 --- a/Core/Cpu/Z80.cs +++ b/Core/Cpu/Z80.cs @@ -89,27 +89,40 @@ namespace Core.Cpu if (!IFF1) return 0; // 2. Acknowledge the interrupt by immediately disabling further interrupts - // This prevents an endless loop of interrupts triggering each other! IFF1 = false; IFF2 = false; - // 3. Push the current Program Counter to the stack so we can return + // 3. Push the current Program Counter to the stack so we can return later Push(PC); - // 4. Jump to the Interrupt Service Routine - // The ZX Spectrum standard is Mode 1, which maps directly to 0x0038 + // --- Interrupt Mode Dispatch --- if (InterruptMode == 1) { + // IM 1: Hardcoded jump to ROM address 0x0038 PC = 0x0038; + return 13; // IM 1 hardware call takes 13 T-States + } + else if (InterruptMode == 2) + { + // IM 2: Dynamic Vectored Interrupts + + // A. Form the pointer address: High byte is 'I', Low byte is the floating bus (0xFF) + ushort vectorAddress = (ushort)((I << 8) | 0xFF); + + // B. Read the actual 16-bit ISR address from that location in memory (Little-Endian) + byte pcLow = _memory.Read(vectorAddress); + byte pcHigh = _memory.Read((ushort)(vectorAddress + 1)); + + // C. Jump to the custom game routine! + PC = (ushort)((pcHigh << 8) | pcLow); + + return 19; // IM 2 hardware call takes 19 T-States } else { - // (Games will use Mode 2 later, but the ROM uses Mode 1) + // (IM 0 is theoretically possible but essentially unused on the standard Spectrum) throw new NotImplementedException($"Interrupt Mode {InterruptMode} not implemented!"); } - - // 5. The CPU takes exactly 13 T-States to process this hardware CALL - return 13; } // Helper method to calculate if a byte has an Even Parity of 1s @@ -146,6 +159,45 @@ namespace Core.Cpu return tStates; } + public void LoadSNA(byte[] snaData) + { + if (snaData.Length != 49179) + throw new Exception("Invalid 48K SNA File Size!"); + + // --- 1. Load CPU Registers --- + I = snaData[0]; + HL_Prime.Word = (ushort)(snaData[1] | (snaData[2] << 8)); + DE_Prime.Word = (ushort)(snaData[3] | (snaData[4] << 8)); + BC_Prime.Word = (ushort)(snaData[5] | (snaData[6] << 8)); + AF_Prime.Word = (ushort)(snaData[7] | (snaData[8] << 8)); + + HL.Word = (ushort)(snaData[9] | (snaData[10] << 8)); + DE.Word = (ushort)(snaData[11] | (snaData[12] << 8)); + BC.Word = (ushort)(snaData[13] | (snaData[14] << 8)); + IY.Word = (ushort)(snaData[15] | (snaData[16] << 8)); + IX.Word = (ushort)(snaData[17] | (snaData[18] << 8)); + + IFF2 = (snaData[19] & 0x04) != 0; + IFF1 = IFF2; + R = snaData[20]; + AF.Word = (ushort)(snaData[21] | (snaData[22] << 8)); + SP = (ushort)(snaData[23] | (snaData[24] << 8)); + InterruptMode = snaData[25]; + + // --- 2. Load the 48K RAM Dump --- + // The RAM dump starts at byte 27 and maps perfectly to 0x4000 -> 0xFFFF + for (int i = 0; i < 49152; i++) + { + _memory.Write((ushort)(0x4000 + i), snaData[27 + i]); + } + + // --- 3. The Magic Bullet --- + // In the SNA format, the Program Counter (PC) isn't in the header. + // It was PUSHED to the stack exactly 1 instruction before the snapshot was saved. + // So, we just pop it off the stack to resume execution! + PC = Pop(); + } + private void HandleInstantTapeLoad() { // 1. Grab the next block from the virtual cassette @@ -175,10 +227,13 @@ namespace Core.Cpu IX.Word = (ushort)(IX.Word + bytesToCopy); DE.Word = 0; - // 5. Set the Carry Flag to 1 (Success) - AF.Low |= 0x01; + // 5. Simulate the Checksum Match (Accumulator becomes 0) + AF.High = 0x00; - // 6. Simulate a standard 'RET' instruction to exit the LD-BYTES routine safely + // 6. Set Carry Flag (Bit 0) for Success, and Zero Flag (Bit 6) for Checksum Match + AF.Low |= 0x41; // 0x41 is binary 0100 0001 (Zero and Carry both set) + + // 7. Simulate a standard 'RET' instruction to exit the LD-BYTES routine safely ExecuteRet(); } @@ -522,6 +577,36 @@ namespace Core.Cpu AF.High = (byte)result; } + private void Adc16(ushort value) + { + int hl = HL.Word; + int carry = AF.Low & 0x01; + + // Calculate the raw integer result to check for overflows + int result = hl + value + carry; + + // --- Update Flags (F Register) --- + byte newFlags = 0; // Clear all flags (which forces N to 0, correctly!) + + // Sign Flag (Bit 7) - Set if the 16-bit result is negative (bit 15 is 1) + if ((result & 0x8000) != 0) newFlags |= 0x80; + + // Zero Flag (Bit 6) - Set if the entire 16-bit result is exactly 0 + if ((result & 0xFFFF) == 0) newFlags |= 0x40; + + // Half-Carry Flag (Bit 4) - Set if there is a carry out of bit 11 + if (((hl & 0x0FFF) + (value & 0x0FFF) + carry) > 0x0FFF) newFlags |= 0x10; + + // Overflow Flag (Bit 2) - Set if operands have the SAME sign, but result sign changes + if ((((hl ^ ~value) & 0x8000) != 0) && (((hl ^ result) & 0x8000) != 0)) newFlags |= 0x04; + + // Carry Flag (Bit 0) - Set if the overall 16-bit result overflowed 0xFFFF + if (result > 0xFFFF) newFlags |= 0x01; + + AF.Low = newFlags; + HL.Word = (ushort)result; + } + private void AddA(byte operand) { byte a = AF.High; @@ -625,6 +710,9 @@ namespace Core.Cpu AF.Word = AF_Prime.Word; AF_Prime.Word = tempAF; return 4; + case 0x0A: //LD A (BC) + AF.High = _memory.Read(BC.Word); + return 7; case 0x0C: BC.Low = Inc8(BC.Low); return 4; // INC C case 0x12: // LD (DE), A _memory.Write(DE.Word, AF.High); @@ -801,20 +889,19 @@ namespace Core.Cpu return 7; case 0x28: // JR Z, e offset = (sbyte)FetchByte(); - - // Check if the Zero Flag (Bit 6) IS set + // Check if the Zero Flag is set if ((AF.Low & 0x40) != 0) { PC = (ushort)(PC + offset); - return 12; // Jump taken + return 12; } - return 7; // Jump not taken + return 7; case 0x2A: // LD HL, (nn) { ushort srcAddress = FetchWord(); HL.Low = _memory.Read(srcAddress); HL.High = _memory.Read((ushort)(srcAddress + 1)); - return 16; // Takes 16 T-States + return 16; } case 0x2B: // DEC HL HL.Word--; @@ -1345,6 +1432,9 @@ namespace Core.Cpu switch (extendedOpcode) { + case 0x42: // SBC HL, BC + Sbc16(BC.Word); + return 15; case 0x43: // LD (nn), BC ushort dest43 = FetchWord(); _memory.Write(dest43, BC.Low); @@ -1381,11 +1471,17 @@ namespace Core.Cpu case 0x47: // LD I, A I = AF.High; return 9; + case 0x4A: // ADC HL, BC + Adc16(BC.Word); + return 15; case 0x4B: // LD BC, (nn) ushort src4B = FetchWord(); BC.Low = _memory.Read(src4B); BC.High = _memory.Read((ushort)(src4B + 1)); - return 20; // Takes 20 T-States + return 20; + case 0x4D: // RETI Does not affect IFF1 or IFF2 + PC = Pop(); + return 14; case 0x52: // SBC HL, DE Sbc16(DE.Word); return 15; @@ -1397,11 +1493,20 @@ namespace Core.Cpu case 0x56: // IM 1 InterruptMode = 1; return 8; + case 0x5A: // ADC HL, DE + Adc16(DE.Word); + return 15; case 0x5B: // LD DE, (nn) ushort src5B = FetchWord(); DE.Low = _memory.Read(src5B); DE.High = _memory.Read((ushort)(src5B + 1)); return 20; + case 0x62: // SBC HL, HL + Sbc16(HL.Word); + return 15; + case 0x6A: // ADC HL, HL + Adc16(HL.Word); + return 15; case 0x72: // SBC HL, SP int carryIn = AF.Low & 0x01; int hlVal = HL.Word; @@ -1462,7 +1567,10 @@ namespace Core.Cpu return 12; case 0x79: // OUT (C), A _simpleIoBus.WritePort(BC.Word, AF.High); - return 12; // 12 T-States + return 12; + case 0x7A: // ADC HL, SP + Adc16(SP); + return 15; case 0x7B: // LD SP, (nn) // 1. Fetch the absolute 16-bit memory address from the instruction stream byte addrLow = FetchByte(); @@ -1540,6 +1648,7 @@ namespace Core.Cpu private int ExecuteCBPrefix() { byte cbOpcode = FetchByte(); + bool oldCarry = false; // Extract the exact same mathematical properties int operation = cbOpcode >> 6; // 00 = Shift, 01 = BIT, 10 = RES, 11 = SET @@ -1602,6 +1711,33 @@ namespace Core.Cpu // Shift left, and loop the falling bit back into Bit 0 val = (byte)((val << 1) | (carryOut ? 1 : 0)); break; + case 1: // RRC (Rotate Right Circular) + // 1. Grab Bit 0 before it falls off to set the Carry flag and loop to Bit 7 + carryOut = (val & 0x01) != 0; + + // 2. Shift right by 1, and loop the falling bit directly back into Bit 7 + val = (byte)((val >> 1) | (carryOut ? 0x80 : 0x00)); + break; + case 2: // RL (Rotate Left through Carry) + // 1. Grab the CURRENT Carry flag from the AF register + oldCarry = (AF.Low & 0x01) != 0; + + // 2. Grab Bit 7 before it falls off to become the NEW Carry flag + carryOut = (val & 0x80) != 0; + + // 3. Shift left by 1, and drop the OLD carry flag directly into Bit 0 + val = (byte)((val << 1) | (oldCarry ? 0x01 : 0x00)); + break; + case 3: // RR (Rotate Right through Carry) + // 1. Grab the CURRENT Carry flag from the AF register + oldCarry = (AF.Low & 0x01) != 0; + + // 2. Grab Bit 0 before it falls off to become the NEW Carry flag + carryOut = (val & 0x01) != 0; + + // 3. Shift right by 1, and drop the OLD carry flag into Bit 7 + val = (byte)((val >> 1) | (oldCarry ? 0x80 : 0x00)); + break; case 4: // SLA (Shift Left Arithmetic) // 1. Grab Bit 7 before it falls off to set the Carry flag carryOut = (val & 0x80) != 0; @@ -1610,6 +1746,20 @@ namespace Core.Cpu // (In C#, a standard left shift automatically pads Bit 0 with a 0) val = (byte)(val << 1); break; + case 5: // SRA (Shift Right Arithmetic) + // 1. Grab Bit 0 before it falls off to set the Carry flag + carryOut = (val & 0x01) != 0; + + // 2. Grab the current Sign bit (Bit 7) so we can preserve it + byte signBit = (byte)(val & 0x80); + + // 3. Shift the byte right by 1. + // (Because 'val' is unsigned, C# naturally pads the top with a 0) + val = (byte)(val >> 1); + + // 4. Force the preserved sign bit back into Bit 7 + val |= signBit; + break; case 7: // SRL (Shift Right Logical) // 1. Grab Bit 0 before it falls off to set the Carry flag carryOut = (val & 0x01) != 0; diff --git a/Desktop/DebuggerForm.cs b/Desktop/DebuggerForm.cs index 0cd7524..3230fe3 100644 --- a/Desktop/DebuggerForm.cs +++ b/Desktop/DebuggerForm.cs @@ -323,6 +323,7 @@ namespace Desktop case 0x08: mnemonic = "EX AF, AF'"; break; + case 0x0A: mnemonic = "LD A, (BC)"; break; case 0x12: mnemonic = "LD (DE), A"; break; case 0x13: mnemonic = "INC DE"; break; case 0x33: mnemonic = "INC SP"; break; @@ -391,9 +392,7 @@ namespace Desktop break; case 0x18: sbyte dUnconditional = (sbyte)_memoryBus.Read((ushort)(currentPc + 1)); - // Calculate the target address based on the PC *after* this 2-byte instruction ushort targetAddressUnconditional = (ushort)(currentPc + 2 + dUnconditional); - mnemonic = $"JR 0x{targetAddressUnconditional:X4}"; instructionLength = 2; break; @@ -981,6 +980,7 @@ namespace Desktop switch (extendedOp) { + case 0x42: mnemonic = "SBC HL, BC"; instructionLength = 2; break; case 0x43: ushort bcAddr = (ushort)(_memoryBus.Read((ushort)(currentPc + 2)) | (_memoryBus.Read((ushort)(currentPc + 3)) << 8)); mnemonic = $"LD (0x{bcAddr:X4}), BC"; @@ -994,11 +994,17 @@ namespace Desktop mnemonic = "LD I, A"; instructionLength = 2; // 0xED + 0x47 break; + // Inside your ED prefix switch statement in the debugger: + case 0x4A: mnemonic = "ADC HL, BC"; instructionLength = 2; break; case 0x4B: ushort addr4B = (ushort)(_memoryBus.Read((ushort)(currentPc + 2)) | (_memoryBus.Read((ushort)(currentPc + 3)) << 8)); mnemonic = $"LD BC, (0x{addr4B:X4})"; instructionLength = 4; break; + case 0x4D: + mnemonic = "RETI"; + instructionLength = 2; + break; case 0x52: mnemonic = "SBC HL, DE"; instructionLength = 2; // ED 52 @@ -1012,11 +1018,14 @@ namespace Desktop mnemonic = "IM 1"; instructionLength = 2; break; + case 0x5A: mnemonic = "ADC HL, DE"; instructionLength = 2; break; case 0x5B: ushort addr5B = (ushort)(_memoryBus.Read((ushort)(currentPc + 2)) | (_memoryBus.Read((ushort)(currentPc + 3)) << 8)); mnemonic = $"LD DE, (0x{addr5B:X4})"; instructionLength = 4; break; + case 0x6A: mnemonic = "ADC HL, HL"; instructionLength = 2; break; + case 0x62: mnemonic = "SBC HL, HL"; instructionLength = 2; break; case 0x73: ushort addr73 = (ushort)(_memoryBus.Read((ushort)(currentPc + 2)) | (_memoryBus.Read((ushort)(currentPc + 3)) << 8)); mnemonic = $"LD (0x{addr73:X4}), SP"; @@ -1030,6 +1039,7 @@ namespace Desktop mnemonic = "IN A, (C)"; instructionLength = 2; break; + case 0x7A: mnemonic = "ADC HL, SP"; instructionLength = 2; break; case 0x7B: // LD SP, (nn) ushort nn = (ushort)(_memoryBus.Read((ushort)(currentPc + 2)) | (_memoryBus.Read((ushort)(currentPc + 3)) << 8)); mnemonic = $"LD SP, (0x{nn:X4})"; diff --git a/Desktop/Form1.Designer.cs b/Desktop/Form1.Designer.cs index 4c01ab9..39f7353 100644 --- a/Desktop/Form1.Designer.cs +++ b/Desktop/Form1.Designer.cs @@ -32,6 +32,7 @@ menuStrip1 = new MenuStrip(); fileToolStripMenuItem = new ToolStripMenuItem(); openToolStripMenuItem = new ToolStripMenuItem(); + openSnapshotToolStripMenuItem = new ToolStripMenuItem(); ((System.ComponentModel.ISupportInitialize)picScreen).BeginInit(); menuStrip1.SuspendLayout(); SuspendLayout(); @@ -59,7 +60,7 @@ // // fileToolStripMenuItem // - fileToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { openToolStripMenuItem }); + fileToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { openToolStripMenuItem, openSnapshotToolStripMenuItem }); fileToolStripMenuItem.Name = "fileToolStripMenuItem"; fileToolStripMenuItem.Size = new Size(54, 29); fileToolStripMenuItem.Text = "File"; @@ -67,10 +68,17 @@ // openToolStripMenuItem // openToolStripMenuItem.Name = "openToolStripMenuItem"; - openToolStripMenuItem.Size = new Size(158, 34); - openToolStripMenuItem.Text = "Open"; + openToolStripMenuItem.Size = new Size(270, 34); + openToolStripMenuItem.Text = "Open TAP"; openToolStripMenuItem.Click += loadTAPToolStripMenuItem_Click; // + // openSnapshotToolStripMenuItem + // + openSnapshotToolStripMenuItem.Name = "openSnapshotToolStripMenuItem"; + openSnapshotToolStripMenuItem.Size = new Size(270, 34); + openSnapshotToolStripMenuItem.Text = "Open Snapshot"; + openSnapshotToolStripMenuItem.Click += openSNAToolStripMenuItem_Click; + // // Form1 // AutoScaleDimensions = new SizeF(10F, 25F); @@ -96,5 +104,6 @@ private MenuStrip menuStrip1; private ToolStripMenuItem fileToolStripMenuItem; private ToolStripMenuItem openToolStripMenuItem; + private ToolStripMenuItem openSnapshotToolStripMenuItem; } } diff --git a/Desktop/Form1.cs b/Desktop/Form1.cs index 2c83384..7a14a07 100644 --- a/Desktop/Form1.cs +++ b/Desktop/Form1.cs @@ -88,6 +88,18 @@ namespace Desktop } } } + private void openSNAToolStripMenuItem_Click(object sender, EventArgs e) + { + using (OpenFileDialog ofd = new OpenFileDialog()) + { + ofd.Filter = "Spectrum Snapshot Files (*.sna)|*.sna"; + if (ofd.ShowDialog() == DialogResult.OK) + { + byte[] snaBytes = System.IO.File.ReadAllBytes(ofd.FileName); + _cpu.LoadSNA(snaBytes); + } + } + } // Public so the Debugger's background thread can call it 50 times a second public void RenderScreen()