Implemented SNA files. More OpCodes. Chuckie Egg Title SCreen!

This commit is contained in:
2026-04-19 00:26:00 +01:00
parent 717c431b9c
commit ed64eb2ebe
4 changed files with 204 additions and 23 deletions

View File

@@ -89,27 +89,40 @@ namespace Core.Cpu
if (!IFF1) return 0; if (!IFF1) return 0;
// 2. Acknowledge the interrupt by immediately disabling further interrupts // 2. Acknowledge the interrupt by immediately disabling further interrupts
// This prevents an endless loop of interrupts triggering each other!
IFF1 = false; IFF1 = false;
IFF2 = 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); Push(PC);
// 4. Jump to the Interrupt Service Routine // --- Interrupt Mode Dispatch ---
// The ZX Spectrum standard is Mode 1, which maps directly to 0x0038
if (InterruptMode == 1) if (InterruptMode == 1)
{ {
// IM 1: Hardcoded jump to ROM address 0x0038
PC = 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 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!"); 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 // Helper method to calculate if a byte has an Even Parity of 1s
@@ -146,6 +159,45 @@ namespace Core.Cpu
return tStates; 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() private void HandleInstantTapeLoad()
{ {
// 1. Grab the next block from the virtual cassette // 1. Grab the next block from the virtual cassette
@@ -175,10 +227,13 @@ namespace Core.Cpu
IX.Word = (ushort)(IX.Word + bytesToCopy); IX.Word = (ushort)(IX.Word + bytesToCopy);
DE.Word = 0; DE.Word = 0;
// 5. Set the Carry Flag to 1 (Success) // 5. Simulate the Checksum Match (Accumulator becomes 0)
AF.Low |= 0x01; 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(); ExecuteRet();
} }
@@ -522,6 +577,36 @@ namespace Core.Cpu
AF.High = (byte)result; 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) private void AddA(byte operand)
{ {
byte a = AF.High; byte a = AF.High;
@@ -625,6 +710,9 @@ namespace Core.Cpu
AF.Word = AF_Prime.Word; AF.Word = AF_Prime.Word;
AF_Prime.Word = tempAF; AF_Prime.Word = tempAF;
return 4; 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 0x0C: BC.Low = Inc8(BC.Low); return 4; // INC C
case 0x12: // LD (DE), A case 0x12: // LD (DE), A
_memory.Write(DE.Word, AF.High); _memory.Write(DE.Word, AF.High);
@@ -801,20 +889,19 @@ namespace Core.Cpu
return 7; return 7;
case 0x28: // JR Z, e case 0x28: // JR Z, e
offset = (sbyte)FetchByte(); offset = (sbyte)FetchByte();
// Check if the Zero Flag is set
// Check if the Zero Flag (Bit 6) IS set
if ((AF.Low & 0x40) != 0) if ((AF.Low & 0x40) != 0)
{ {
PC = (ushort)(PC + offset); PC = (ushort)(PC + offset);
return 12; // Jump taken return 12;
} }
return 7; // Jump not taken return 7;
case 0x2A: // LD HL, (nn) case 0x2A: // LD HL, (nn)
{ {
ushort srcAddress = FetchWord(); ushort srcAddress = FetchWord();
HL.Low = _memory.Read(srcAddress); HL.Low = _memory.Read(srcAddress);
HL.High = _memory.Read((ushort)(srcAddress + 1)); HL.High = _memory.Read((ushort)(srcAddress + 1));
return 16; // Takes 16 T-States return 16;
} }
case 0x2B: // DEC HL case 0x2B: // DEC HL
HL.Word--; HL.Word--;
@@ -1345,6 +1432,9 @@ namespace Core.Cpu
switch (extendedOpcode) switch (extendedOpcode)
{ {
case 0x42: // SBC HL, BC
Sbc16(BC.Word);
return 15;
case 0x43: // LD (nn), BC case 0x43: // LD (nn), BC
ushort dest43 = FetchWord(); ushort dest43 = FetchWord();
_memory.Write(dest43, BC.Low); _memory.Write(dest43, BC.Low);
@@ -1381,11 +1471,17 @@ namespace Core.Cpu
case 0x47: // LD I, A case 0x47: // LD I, A
I = AF.High; I = AF.High;
return 9; return 9;
case 0x4A: // ADC HL, BC
Adc16(BC.Word);
return 15;
case 0x4B: // LD BC, (nn) case 0x4B: // LD BC, (nn)
ushort src4B = FetchWord(); ushort src4B = FetchWord();
BC.Low = _memory.Read(src4B); BC.Low = _memory.Read(src4B);
BC.High = _memory.Read((ushort)(src4B + 1)); 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 case 0x52: // SBC HL, DE
Sbc16(DE.Word); Sbc16(DE.Word);
return 15; return 15;
@@ -1397,11 +1493,20 @@ namespace Core.Cpu
case 0x56: // IM 1 case 0x56: // IM 1
InterruptMode = 1; InterruptMode = 1;
return 8; return 8;
case 0x5A: // ADC HL, DE
Adc16(DE.Word);
return 15;
case 0x5B: // LD DE, (nn) case 0x5B: // LD DE, (nn)
ushort src5B = FetchWord(); ushort src5B = FetchWord();
DE.Low = _memory.Read(src5B); DE.Low = _memory.Read(src5B);
DE.High = _memory.Read((ushort)(src5B + 1)); DE.High = _memory.Read((ushort)(src5B + 1));
return 20; 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 case 0x72: // SBC HL, SP
int carryIn = AF.Low & 0x01; int carryIn = AF.Low & 0x01;
int hlVal = HL.Word; int hlVal = HL.Word;
@@ -1462,7 +1567,10 @@ namespace Core.Cpu
return 12; return 12;
case 0x79: // OUT (C), A case 0x79: // OUT (C), A
_simpleIoBus.WritePort(BC.Word, AF.High); _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) case 0x7B: // LD SP, (nn)
// 1. Fetch the absolute 16-bit memory address from the instruction stream // 1. Fetch the absolute 16-bit memory address from the instruction stream
byte addrLow = FetchByte(); byte addrLow = FetchByte();
@@ -1540,6 +1648,7 @@ namespace Core.Cpu
private int ExecuteCBPrefix() private int ExecuteCBPrefix()
{ {
byte cbOpcode = FetchByte(); byte cbOpcode = FetchByte();
bool oldCarry = false;
// Extract the exact same mathematical properties // Extract the exact same mathematical properties
int operation = cbOpcode >> 6; // 00 = Shift, 01 = BIT, 10 = RES, 11 = SET 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 // Shift left, and loop the falling bit back into Bit 0
val = (byte)((val << 1) | (carryOut ? 1 : 0)); val = (byte)((val << 1) | (carryOut ? 1 : 0));
break; 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) case 4: // SLA (Shift Left Arithmetic)
// 1. Grab Bit 7 before it falls off to set the Carry flag // 1. Grab Bit 7 before it falls off to set the Carry flag
carryOut = (val & 0x80) != 0; carryOut = (val & 0x80) != 0;
@@ -1610,6 +1746,20 @@ namespace Core.Cpu
// (In C#, a standard left shift automatically pads Bit 0 with a 0) // (In C#, a standard left shift automatically pads Bit 0 with a 0)
val = (byte)(val << 1); val = (byte)(val << 1);
break; 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) case 7: // SRL (Shift Right Logical)
// 1. Grab Bit 0 before it falls off to set the Carry flag // 1. Grab Bit 0 before it falls off to set the Carry flag
carryOut = (val & 0x01) != 0; carryOut = (val & 0x01) != 0;

View File

@@ -323,6 +323,7 @@ namespace Desktop
case 0x08: case 0x08:
mnemonic = "EX AF, AF'"; mnemonic = "EX AF, AF'";
break; break;
case 0x0A: mnemonic = "LD A, (BC)"; break;
case 0x12: mnemonic = "LD (DE), A"; break; case 0x12: mnemonic = "LD (DE), A"; break;
case 0x13: mnemonic = "INC DE"; break; case 0x13: mnemonic = "INC DE"; break;
case 0x33: mnemonic = "INC SP"; break; case 0x33: mnemonic = "INC SP"; break;
@@ -391,9 +392,7 @@ namespace Desktop
break; break;
case 0x18: case 0x18:
sbyte dUnconditional = (sbyte)_memoryBus.Read((ushort)(currentPc + 1)); 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); ushort targetAddressUnconditional = (ushort)(currentPc + 2 + dUnconditional);
mnemonic = $"JR 0x{targetAddressUnconditional:X4}"; mnemonic = $"JR 0x{targetAddressUnconditional:X4}";
instructionLength = 2; instructionLength = 2;
break; break;
@@ -981,6 +980,7 @@ namespace Desktop
switch (extendedOp) switch (extendedOp)
{ {
case 0x42: mnemonic = "SBC HL, BC"; instructionLength = 2; break;
case 0x43: case 0x43:
ushort bcAddr = (ushort)(_memoryBus.Read((ushort)(currentPc + 2)) | (_memoryBus.Read((ushort)(currentPc + 3)) << 8)); ushort bcAddr = (ushort)(_memoryBus.Read((ushort)(currentPc + 2)) | (_memoryBus.Read((ushort)(currentPc + 3)) << 8));
mnemonic = $"LD (0x{bcAddr:X4}), BC"; mnemonic = $"LD (0x{bcAddr:X4}), BC";
@@ -994,11 +994,17 @@ namespace Desktop
mnemonic = "LD I, A"; mnemonic = "LD I, A";
instructionLength = 2; // 0xED + 0x47 instructionLength = 2; // 0xED + 0x47
break; break;
// Inside your ED prefix switch statement in the debugger:
case 0x4A: mnemonic = "ADC HL, BC"; instructionLength = 2; break;
case 0x4B: case 0x4B:
ushort addr4B = (ushort)(_memoryBus.Read((ushort)(currentPc + 2)) | (_memoryBus.Read((ushort)(currentPc + 3)) << 8)); ushort addr4B = (ushort)(_memoryBus.Read((ushort)(currentPc + 2)) | (_memoryBus.Read((ushort)(currentPc + 3)) << 8));
mnemonic = $"LD BC, (0x{addr4B:X4})"; mnemonic = $"LD BC, (0x{addr4B:X4})";
instructionLength = 4; instructionLength = 4;
break; break;
case 0x4D:
mnemonic = "RETI";
instructionLength = 2;
break;
case 0x52: case 0x52:
mnemonic = "SBC HL, DE"; mnemonic = "SBC HL, DE";
instructionLength = 2; // ED 52 instructionLength = 2; // ED 52
@@ -1012,11 +1018,14 @@ namespace Desktop
mnemonic = "IM 1"; mnemonic = "IM 1";
instructionLength = 2; instructionLength = 2;
break; break;
case 0x5A: mnemonic = "ADC HL, DE"; instructionLength = 2; break;
case 0x5B: case 0x5B:
ushort addr5B = (ushort)(_memoryBus.Read((ushort)(currentPc + 2)) | (_memoryBus.Read((ushort)(currentPc + 3)) << 8)); ushort addr5B = (ushort)(_memoryBus.Read((ushort)(currentPc + 2)) | (_memoryBus.Read((ushort)(currentPc + 3)) << 8));
mnemonic = $"LD DE, (0x{addr5B:X4})"; mnemonic = $"LD DE, (0x{addr5B:X4})";
instructionLength = 4; instructionLength = 4;
break; break;
case 0x6A: mnemonic = "ADC HL, HL"; instructionLength = 2; break;
case 0x62: mnemonic = "SBC HL, HL"; instructionLength = 2; break;
case 0x73: case 0x73:
ushort addr73 = (ushort)(_memoryBus.Read((ushort)(currentPc + 2)) | (_memoryBus.Read((ushort)(currentPc + 3)) << 8)); ushort addr73 = (ushort)(_memoryBus.Read((ushort)(currentPc + 2)) | (_memoryBus.Read((ushort)(currentPc + 3)) << 8));
mnemonic = $"LD (0x{addr73:X4}), SP"; mnemonic = $"LD (0x{addr73:X4}), SP";
@@ -1030,6 +1039,7 @@ namespace Desktop
mnemonic = "IN A, (C)"; mnemonic = "IN A, (C)";
instructionLength = 2; instructionLength = 2;
break; break;
case 0x7A: mnemonic = "ADC HL, SP"; instructionLength = 2; break;
case 0x7B: // LD SP, (nn) case 0x7B: // LD SP, (nn)
ushort nn = (ushort)(_memoryBus.Read((ushort)(currentPc + 2)) | (_memoryBus.Read((ushort)(currentPc + 3)) << 8)); ushort nn = (ushort)(_memoryBus.Read((ushort)(currentPc + 2)) | (_memoryBus.Read((ushort)(currentPc + 3)) << 8));
mnemonic = $"LD SP, (0x{nn:X4})"; mnemonic = $"LD SP, (0x{nn:X4})";

View File

@@ -32,6 +32,7 @@
menuStrip1 = new MenuStrip(); menuStrip1 = new MenuStrip();
fileToolStripMenuItem = new ToolStripMenuItem(); fileToolStripMenuItem = new ToolStripMenuItem();
openToolStripMenuItem = new ToolStripMenuItem(); openToolStripMenuItem = new ToolStripMenuItem();
openSnapshotToolStripMenuItem = new ToolStripMenuItem();
((System.ComponentModel.ISupportInitialize)picScreen).BeginInit(); ((System.ComponentModel.ISupportInitialize)picScreen).BeginInit();
menuStrip1.SuspendLayout(); menuStrip1.SuspendLayout();
SuspendLayout(); SuspendLayout();
@@ -59,7 +60,7 @@
// //
// fileToolStripMenuItem // fileToolStripMenuItem
// //
fileToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { openToolStripMenuItem }); fileToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { openToolStripMenuItem, openSnapshotToolStripMenuItem });
fileToolStripMenuItem.Name = "fileToolStripMenuItem"; fileToolStripMenuItem.Name = "fileToolStripMenuItem";
fileToolStripMenuItem.Size = new Size(54, 29); fileToolStripMenuItem.Size = new Size(54, 29);
fileToolStripMenuItem.Text = "File"; fileToolStripMenuItem.Text = "File";
@@ -67,10 +68,17 @@
// openToolStripMenuItem // openToolStripMenuItem
// //
openToolStripMenuItem.Name = "openToolStripMenuItem"; openToolStripMenuItem.Name = "openToolStripMenuItem";
openToolStripMenuItem.Size = new Size(158, 34); openToolStripMenuItem.Size = new Size(270, 34);
openToolStripMenuItem.Text = "Open"; openToolStripMenuItem.Text = "Open TAP";
openToolStripMenuItem.Click += loadTAPToolStripMenuItem_Click; openToolStripMenuItem.Click += loadTAPToolStripMenuItem_Click;
// //
// openSnapshotToolStripMenuItem
//
openSnapshotToolStripMenuItem.Name = "openSnapshotToolStripMenuItem";
openSnapshotToolStripMenuItem.Size = new Size(270, 34);
openSnapshotToolStripMenuItem.Text = "Open Snapshot";
openSnapshotToolStripMenuItem.Click += openSNAToolStripMenuItem_Click;
//
// Form1 // Form1
// //
AutoScaleDimensions = new SizeF(10F, 25F); AutoScaleDimensions = new SizeF(10F, 25F);
@@ -96,5 +104,6 @@
private MenuStrip menuStrip1; private MenuStrip menuStrip1;
private ToolStripMenuItem fileToolStripMenuItem; private ToolStripMenuItem fileToolStripMenuItem;
private ToolStripMenuItem openToolStripMenuItem; private ToolStripMenuItem openToolStripMenuItem;
private ToolStripMenuItem openSnapshotToolStripMenuItem;
} }
} }

View File

@@ -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 so the Debugger's background thread can call it 50 times a second
public void RenderScreen() public void RenderScreen()