using System; using Core.Interfaces; using Core.Io; namespace Core.Cpu { public partial class Z80 { //T-State counter public long TotalTStates { get; set; } public int InterruptMode { get; private set; } = 0; // Defaults to 0 on power-up // Interrupt Flip-Flops public bool IFF1 { get; private set; } = false; public bool IFF2 { get; private set; } = false; // Main Register Set public RegisterPair AF; public RegisterPair BC; public RegisterPair DE; public RegisterPair HL; // Alternate Register Set public RegisterPair AF_Prime; public RegisterPair BC_Prime; public RegisterPair DE_Prime; public RegisterPair HL_Prime; // Index Registers public RegisterPair IX; public RegisterPair IY; // Special Purpose Registers public ushort PC; // Program Counter public ushort SP; // Stack Pointer public byte I; // Interrupt Vector public byte R; // Memory Refresh // The Memory Bus private readonly IMemory _memory; private readonly IO_Bus _simpleIoBus; public TapManager _tapManager; public Z80(IMemory memory, IO_Bus ioBus, TapManager tapManager) { _memory = memory; _simpleIoBus = ioBus; _tapManager = tapManager; Reset(); } public void Reset() { PC = 0x0000; SP = 0xFFFF; // The Z80 initializes SP to 0xFFFF on boot // Main Registers AF.Word = 0; BC.Word = 0; DE.Word = 0; HL.Word = 0; // Alternate Registers AF_Prime.Word = 0; BC_Prime.Word = 0; DE_Prime.Word = 0; HL_Prime.Word = 0; // Index Registers IX.Word = 0; IY.Word = 0; // Internal Registers I = 0; R = 0; // Hardware State IFF1 = false; IFF2 = false; InterruptMode = 0; TotalTStates = 0; //_memory.CleanRAMData(); } public int RequestInterrupt() { // 1. If the ROM has disabled interrupts (DI), ignore the request 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 Push(PC); // 4. Jump to the Interrupt Service Routine // The ZX Spectrum standard is Mode 1, which maps directly to 0x0038 if (InterruptMode == 1) { PC = 0x0038; } else { // (Games will use Mode 2 later, but the ROM uses Mode 1) 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 private bool CalculateParity(byte b) { int bits = 0; for (int i = 0; i < 8; i++) { if ((b & (1 << i)) != 0) bits++; } return (bits % 2) == 0; } // Placeholder for your hardware I/O private byte ReadPort(ushort portAddress) { return _simpleIoBus.ReadPort(portAddress); } public int Step() { if (PC == 0x0556 && _tapManager.HasBlocks) { HandleInstantTapeLoad(); return 100; // Return a dummy number of T-States for the hijacking } // Fetch the next opcode and increment the Program Counter byte opcode = _memory.Read(PC++); int tStates = ExecuteOpcode(opcode); TotalTStates += tStates; // Decode and execute return tStates; } private void HandleInstantTapeLoad() { // 1. Grab the next block from the virtual cassette byte[] block = _tapManager.GetNextBlock(); if (block == null) return; // Tape ended unexpectedly // 2. Verify the block type. // The ROM passes the expected flag (0x00 for Header, 0xFF for Data) in the A register. byte expectedFlag = AF.High; if (block[0] != expectedFlag) { // Tape loading error! Simulate a failure by clearing the Carry flag and returning. AF.Low &= unchecked((byte)~0x01); ExecuteRet(); return; } // 3. Copy the data block straight into the Spectrum's RAM! // DE holds the number of bytes the ROM *wants*. We copy that much, skipping the Flag byte. int bytesToCopy = DE.Word; for (int i = 0; i < bytesToCopy; i++) { _memory.Write((ushort)(IX.Word + i), block[i + 1]); } // 4. Update the registers exactly how the ROM would after a successful load IX.Word = (ushort)(IX.Word + bytesToCopy); DE.Word = 0; // 5. Set the Carry Flag to 1 (Success) AF.Low |= 0x01; // 6. Simulate a standard 'RET' instruction to exit the LD-BYTES routine safely ExecuteRet(); } // A quick helper to simulate a RET instruction manually private void ExecuteRet() { byte pcLow = _memory.Read(SP); SP++; byte pcHigh = _memory.Read(SP); SP++; PC = (ushort)((pcHigh << 8) | pcLow); } // Reads a 16-bit word from the current PC (Little-Endian) and advances PC by 2 private ushort FetchWord() { byte low = _memory.Read(PC++); byte high = _memory.Read(PC++); return (ushort)((high << 8) | low); } private byte FetchByte() { return _memory.Read(PC++); } public string GetFlagsString() { byte f = AF.Low; return $"S:{(f >> 7) & 1} " + $"Z:{(f >> 6) & 1} " + $"Y:{(f >> 5) & 1} " + // Undocumented flag $"H:{(f >> 4) & 1} " + $"X:{(f >> 3) & 1} " + // Undocumented flag $"P/V:{(f >> 2) & 1} " + $"N:{(f >> 1) & 1} " + $"C:{f & 1}"; } private void Sub(byte value) { byte a = AF.High; int result = a - value; // Save the result back to the Accumulator AF.High = (byte)result; // --- Update Flags (F Register) --- AF.Low = 0; // Clear all flags // Sign Flag (Bit 7) if ((result & 0x80) != 0) AF.Low |= 0x80; // Zero Flag (Bit 6) if ((byte)result == 0) AF.Low |= 0x40; // Half-Carry Flag (Bit 4) - Set if borrow from bit 4 if (((a & 0x0F) - (value & 0x0F)) < 0) AF.Low |= 0x10; // Overflow Flag (Bit 2) - Set if operands have different signs and result sign changes if ((((a ^ value) & 0x80) != 0) && (((a ^ result) & 0x80) != 0)) AF.Low |= 0x04; // Subtract Flag (Bit 1) - ALWAYS set for CP/SUB AF.Low |= 0x02; // Carry Flag (Bit 0) - Set if the overall result dropped below 0 if (result < 0) AF.Low |= 0x01; } private void Sbc(byte value) { byte a = AF.High; byte carry = (byte)(AF.Low & 0x01); // Get the current Carry flag (Bit 0) // Calculate the raw integer result to check for borrows/underflows int result = a - value - carry; // Update the Accumulator AF.High = (byte)result; // --- Update Flags (F Register) --- AF.Low = 0; // Clear all flags // Sign Flag (Bit 7) if ((result & 0x80) != 0) AF.Low |= 0x80; // Zero Flag (Bit 6) if ((byte)result == 0) AF.Low |= 0x40; // Half-Carry Flag (Bit 4) - Set if borrow from bit 4 if (((a & 0x0F) - (value & 0x0F) - carry) < 0) AF.Low |= 0x10; // Overflow Flag (Bit 2) - Set if operands have different signs and result sign changes if ((((a ^ value) & 0x80) != 0) && (((a ^ result) & 0x80) != 0)) AF.Low |= 0x04; // Subtract Flag (Bit 1) - ALWAYS set for subtraction AF.Low |= 0x02; // Carry Flag (Bit 0) - Set if the overall result dropped below 0 if (result < 0) AF.Low |= 0x01; } private void Sbc16(ushort value) { int hl = HL.Word; int carry = AF.Low & 0x01; // Calculate the raw integer result to check for underflows int result = hl - value - carry; // Update the HL register HL.Word = (ushort)result; // --- Update Flags (F Register) --- AF.Low = 0; // Clear all flags // Sign Flag (Bit 7) - Set if the 16-bit result is negative (bit 15 is 1) if ((result & 0x8000) != 0) AF.Low |= 0x80; // Zero Flag (Bit 6) - Set if the entire 16-bit result is exactly 0 if ((ushort)result == 0) AF.Low |= 0x40; // Half-Carry Flag (Bit 4) - Set if borrow from bit 11 if (((hl & 0x0FFF) - (value & 0x0FFF) - carry) < 0) AF.Low |= 0x10; // Overflow Flag (Bit 2) - Set if operands have different signs and result sign changes if ((((hl ^ value) & 0x8000) != 0) && (((hl ^ result) & 0x8000) != 0)) AF.Low |= 0x04; // Subtract Flag (Bit 1) - ALWAYS set for subtraction AF.Low |= 0x02; // Carry Flag (Bit 0) - Set if the overall 16-bit result dropped below 0 if (result < 0) AF.Low |= 0x01; } private byte Dec8(byte value) { byte result = (byte)(value - 1); // Store the existing Carry flag so we can preserve it byte carry = (byte)(AF.Low & 0x01); // Clear all flags AF.Low = 0; // Sign Flag (Bit 7) if ((result & 0x80) != 0) AF.Low |= 0x80; // Zero Flag (Bit 6) if (result == 0) AF.Low |= 0x40; // Half-Carry Flag (Bit 4) - Set if borrow from bit 4 (happens if the lower nibble was 0) if ((value & 0x0F) == 0) AF.Low |= 0x10; // Parity/Overflow Flag (Bit 2) - Set if the original value was 0x80 (maximum negative) if (value == 0x80) AF.Low |= 0x04; // Subtract Flag (Bit 1) - ALWAYS SET for decrements AF.Low |= 0x02; // Restore the original Carry Flag (Bit 0) AF.Low |= carry; return result; } private byte Inc8(byte value) { byte result = (byte)(value + 1); // Store the existing Carry flag so we can preserve it byte carry = (byte)(AF.Low & 0x01); // Clear all flags AF.Low = 0; // Sign Flag (Bit 7) if ((result & 0x80) != 0) AF.Low |= 0x80; // Zero Flag (Bit 6) if (result == 0) AF.Low |= 0x40; // Half-Carry Flag (Bit 4) - Set if carry from bit 3 (happens if lower nibble was 0x0F) if ((value & 0x0F) == 0x0F) AF.Low |= 0x10; // Parity/Overflow Flag (Bit 2) - Set if the original value was 0x7F (maximum positive) if (value == 0x7F) AF.Low |= 0x04; // Subtract Flag (Bit 1) - ALWAYS 0 for increments (already 0 because we cleared AF.Low) // Restore the original Carry Flag (Bit 0) AF.Low |= carry; return result; } private void Cp(byte value) { byte a = AF.High; int result = a - value; // --- Update Flags (F Register) --- AF.Low = 0; // Clear all flags // Sign Flag (Bit 7) if ((result & 0x80) != 0) AF.Low |= 0x80; // Zero Flag (Bit 6) if ((byte)result == 0) AF.Low |= 0x40; // Half-Carry Flag (Bit 4) - Set if borrow from bit 4 if (((a & 0x0F) - (value & 0x0F)) < 0) AF.Low |= 0x10; // Overflow Flag (Bit 2) - Set if operands have different signs and result sign changes if ((((a ^ value) & 0x80) != 0) && (((a ^ result) & 0x80) != 0)) AF.Low |= 0x04; // Subtract Flag (Bit 1) - ALWAYS set for CP/SUB AF.Low |= 0x02; // Carry Flag (Bit 0) - Set if the overall result dropped below 0 if (result < 0) AF.Low |= 0x01; } private void And(byte value) { AF.High = (byte)(AF.High & value); // --- Update Flags --- AF.Low = 0; // Clear all flags // Sign Flag (Bit 7) - Set if the highest bit is 1 if ((AF.High & 0x80) != 0) AF.Low |= 0x80; // Zero Flag (Bit 6) - Set if the result is 0 if (AF.High == 0) AF.Low |= 0x40; // Half-Carry Flag (Bit 4) - ALWAYS SET to 1 for Z80 AND instructions! AF.Low |= 0x10; // Parity Flag (Bit 2) - Set if the result has an even number of 1 bits if (HasEvenParity(AF.High)) AF.Low |= 0x04; // Subtract Flag (N) and Carry Flag (C) are ALWAYS 0 } private void Or(byte value) { AF.High = (byte)(AF.High | value); // --- Update Flags --- AF.Low = 0; // Clear all flags (H, N, and C are always 0 for OR) // Sign Flag (Bit 7) - Set if the highest bit is 1 if ((AF.High & 0x80) != 0) AF.Low |= 0x80; // Zero Flag (Bit 6) - Set if the result is 0 if (AF.High == 0) AF.Low |= 0x40; // Parity Flag (Bit 2) - Set if the result has an even number of 1 bits if (HasEvenParity(AF.High)) AF.Low |= 0x04; } private void Xor(byte value) { // The caret (^) is the C# Bitwise XOR operator AF.High = (byte)(AF.High ^ value); // --- Update Flags --- AF.Low = 0; // Clear all flags (H, N, and C are always 0 for XOR) // Sign Flag (Bit 7) - Set if the highest bit is 1 if ((AF.High & 0x80) != 0) AF.Low |= 0x80; // Zero Flag (Bit 6) - Set if the result is 0 if (AF.High == 0) AF.Low |= 0x40; // Parity Flag (Bit 2) - Set if the result has an even number of 1 bits if (HasEvenParity(AF.High)) AF.Low |= 0x04; } private void Add16(ushort value) { int hl = HL.Word; int result = hl + value; // Update the HL register HL.Word = (ushort)result; AF.Low &= 0xEC; // Half-Carry Flag (Bit 4) - Set if there is a carry from bit 11 if (((hl & 0x0FFF) + (value & 0x0FFF)) > 0x0FFF) AF.Low |= 0x10; // Carry Flag (Bit 0) - Set if the result overflows 16 bits if (result > 0xFFFF) AF.Low |= 0x01; } private void Add(byte value) { byte a = AF.High; int result = a + value; // Save the result back to the Accumulator AF.High = (byte)result; // --- Update Flags (F Register) --- AF.Low = 0; // Clear all flags (This also correctly resets the N flag to 0) // Sign Flag (Bit 7) if ((result & 0x80) != 0) AF.Low |= 0x80; // Zero Flag (Bit 6) if ((byte)result == 0) AF.Low |= 0x40; // Half-Carry Flag (Bit 4) - Set if carry from bit 3 if (((a & 0x0F) + (value & 0x0F)) > 0x0F) AF.Low |= 0x10; // Overflow/Parity Flag (Bit 2) - For addition, overflow happens if two numbers // with the SAME sign are added and produce a result with a DIFFERENT sign. if ((((a ^ ~value) & 0x80) != 0) && (((a ^ result) & 0x80) != 0)) AF.Low |= 0x04; // Carry Flag (Bit 0) - Set if the result is greater than 255 if (result > 0xFF) AF.Low |= 0x01; } private void AdcA(byte operand) { int aVal = AF.High; int carryIn = AF.Low & 0x01; int result = aVal + operand + carryIn; byte newFlags = 0; if ((result & 0x80) != 0) newFlags |= 0x80; // S Flag if ((result & 0xFF) == 0) newFlags |= 0x40; // Z Flag if (((aVal & 0x0F) + (operand & 0x0F) + carryIn) > 0x0F) newFlags |= 0x10; // H Flag if ((((aVal ^ ~operand) & (aVal ^ result)) & 0x80) != 0) newFlags |= 0x04; // P/V Flag if (result > 0xFF) newFlags |= 0x01; // C Flag AF.Low = newFlags; AF.High = (byte)result; } private void AddA(byte operand) { byte a = AF.High; int result = a + operand; AF.High = (byte)result; // --- Update Flags --- AF.Low = 0; // Clear all flags initially (Forces N to 0) // Sign Flag (Bit 7) if ((AF.High & 0x80) != 0) AF.Low |= 0x80; // Zero Flag (Bit 6) if (AF.High == 0) AF.Low |= 0x40; // Half-Carry Flag (Bit 4) - Check if bits 0-3 overflowed if (((a & 0x0F) + (operand & 0x0F)) > 0x0F) AF.Low |= 0x10; // Parity/Overflow Flag (Bit 2) bool sameSign = ((a ^ operand) & 0x80) == 0; // Did inputs have the same sign? bool changedSign = ((a ^ AF.High) & 0x80) != 0; // Did the result's sign flip? if (sameSign && changedSign) AF.Low |= 0x04; // Carry Flag (Bit 0) - Check if the whole 8-bit addition overflowed if (result > 0xFF) AF.Low |= 0x01; } private bool HasEvenParity(byte value) { int bits = 0; for (int i = 0; i < 8; i++) { if ((value & (1 << i)) != 0) bits++; } return (bits % 2) == 0; } private void Push(ushort value) { // High byte goes first SP--; _memory.Write(SP, (byte)(value >> 8)); // Low byte goes second SP--; _memory.Write(SP, (byte)(value & 0xFF)); } private ushort Pop() { // The Z80 is Little-Endian. Low byte comes off the stack first. byte low = _memory.Read(SP++); // High byte comes off second. byte high = _memory.Read(SP++); return (ushort)((high << 8) | low); } private int ExecuteOpcode(byte opcode) { sbyte offset = 0; switch (opcode) { case 0x00: // NOP return 4; case 0x01: // LD BC, nn BC.Word = FetchWord(); return 10; case 0x02: // LD (BC), A _memory.Write(BC.Word, AF.High); return 7; case 0x03: // INC BC BC.Word++; return 6; // --- 8-Bit Increments --- case 0x04: BC.High = Inc8(BC.High); return 4; // INC B case 0x07: // RLCA // 1. Grab the top bit (Bit 7) before it rotates byte topBit = (byte)(AF.High >> 7); // 2. Shift A left by 1, and drop the top bit into the newly empty Bit 0 AF.High = (byte)((AF.High << 1) | topBit); // --- RLCA Specific Flag Rules --- // Preserve S (Bit 7), Z (Bit 6), and P/V (Bit 2). byte newFlags = (byte)(AF.Low & 0xC4); // H (Bit 4) and N (Bit 1) are forcefully reset to 0 (which the bitwise AND above just did). // C (Bit 0): Set if the bit that rotated off the top was a 1 if (topBit != 0) newFlags |= 0x01; AF.Low = newFlags; return 4; // 4 T-States case 0x08: // EX AF, AF' ushort tempAF = AF.Word; AF.Word = AF_Prime.Word; AF_Prime.Word = tempAF; return 4; case 0x0C: BC.Low = Inc8(BC.Low); return 4; // INC C case 0x12: // LD (DE), A _memory.Write(DE.Word, AF.High); return 7; case 0x14: DE.High = Inc8(DE.High); return 4; // INC D case 0x1C: DE.Low = Inc8(DE.Low); return 4; // INC E case 0x1E: DE.Low = FetchByte(); // LD E, n return 7; case 0x24: HL.High = Inc8(HL.High); return 4; // INC H case 0x2C: HL.Low = Inc8(HL.Low); return 4; // INC L case 0x2E: // LD L, n HL.Low = FetchByte(); return 7; case 0x34: _memory.Write(HL.Word, Inc8(_memory.Read(HL.Word))); return 11; // INC (HL) takes 11 T-States case 0x3C: AF.High = Inc8(AF.High); return 4; // INC A // --- 8-Bit Decrements --- case 0x05: BC.High = Dec8(BC.High); return 4; // DEC B case 0x0D: BC.Low = Dec8(BC.Low); return 4; // DEC C case 0x15: DE.High = Dec8(DE.High); return 4; // DEC D case 0x1D: DE.Low = Dec8(DE.Low); return 4; // DEC E case 0x25: HL.High = Dec8(HL.High); return 4; // DEC H case 0x2D: HL.Low = Dec8(HL.Low); return 4; // DEC L case 0x2F: // CPL // Flip all bits in the Accumulator AF.High = (byte)(~AF.High); // Set Half-Carry (Bit 4) and Subtract (Bit 1). // Bitwise OR forces them to 1 while perfectly preserving S, Z, P/V, and C. AF.Low |= 0x12; return 4; case 0x35: _memory.Write(HL.Word, Dec8(_memory.Read(HL.Word))); return 11; // DEC (HL) takes 11 T-States case 0x3D: AF.High = Dec8(AF.High); return 4; // DEC A case 0x06: // LD B, n BC.High = FetchByte(); return 7; // --- ADD HL, rr (16-bit Addition) --- case 0x09: Add16(BC.Word); return 11; case 0x19: Add16(DE.Word); return 11; case 0x29: Add16(HL.Word); // This perfectly multiplies HL by 2! return 11; case 0x39: Add16(SP); return 11; case 0x0B: // DEC BC BC.Word--; return 6; case 0x0E: // LD C, n BC.Low = FetchByte(); return 7; case 0x0F: // RRCA { // 1. Grab the bit that is about to fall off byte bit0 = (byte)(AF.High & 0x01); // 2. Shift right, and force the old Bit 0 into the Bit 7 position AF.High = (byte)((AF.High >> 1) | (bit0 << 7)); // 3. Update Flags // S (0x80), Z (0x40), and P/V (0x04) are completely PRESERVED. // H (0x10) and N (0x02) are forcefully RESET to 0. // ANDing with 0xC4 (Binary 1100 0100) does exactly this. AF.Low &= 0xC4; // Set the Carry Flag (Bit 0) to whatever fell off AF.Low |= bit0; return 4; } case 0x10: // DJNZ d sbyte djnzOffset = (sbyte)FetchByte(); BC.High--; // Decrement the B register if (BC.High != 0) { PC = (ushort)(PC + djnzOffset); return 13; // Jump taken } return 8; // Loop finished, no jump case 0x11: //LD DE, nn DE.Word = FetchWord(); return 10; case 0x13: // INC DE DE.Word++; return 6; case 0x16: // LD D, n DE.High = FetchByte(); return 7; case 0x18: // JR d sbyte jumpDistance = (sbyte)FetchByte(); // PC has already been incremented by FetchByte(), so it is // pointing exactly where it needs to be for the relative addition. PC = (ushort)(PC + jumpDistance); return 12; case 0x1A: // LD A, (DE) AF.High = _memory.Read(DE.Word); return 7; case 0x1B: // DEC DE DE.Word--; return 6; case 0x1F: // RRA { // 1. Grab the current Carry Flag (0 or 1) byte oldCarry = (byte)(AF.Low & 0x01); // 2. Grab the bit that is about to fall off the Accumulator byte bit0 = (byte)(AF.High & 0x01); // 3. Shift right, and force the OLD Carry flag into the Bit 7 position AF.High = (byte)((AF.High >> 1) | (oldCarry << 7)); // 4. Update Flags // S (0x80), Z (0x40), and P/V (0x04) are PRESERVED exactly as they are. // H (0x10) and N (0x02) are forcefully RESET to 0. AF.Low &= 0xC4; // Set the new Carry Flag (Bit 0) to whatever fell off the Accumulator AF.Low |= bit0; return 4; } case 0x20: // JR NZ, e offset = (sbyte)FetchByte(); if ((AF.Low & 0x40) == 0) { PC = (ushort)(PC + offset); return 12; } return 7; case 0x21: // LD HL, nn HL.Word = FetchWord(); return 10; case 0x22: // LD (nn), HL ushort dest22 = FetchWord(); _memory.Write(dest22, HL.Low); _memory.Write((ushort)(dest22 + 1), HL.High); return 16; case 0x23: // INC HL HL.Word++; return 6; case 0x26: // LD H, n HL.High = FetchByte(); return 7; case 0x28: // JR Z, e offset = (sbyte)FetchByte(); // Check if the Zero Flag (Bit 6) IS set if ((AF.Low & 0x40) != 0) { PC = (ushort)(PC + offset); return 12; // Jump taken } return 7; // Jump not taken 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 } case 0x2B: // DEC HL HL.Word--; return 6; case 0x30: // JR NC, e offset = (sbyte)FetchByte(); // Check if the Carry Flag (Bit 0) is NOT set if ((AF.Low & 0x01) == 0) { PC = (ushort)(PC + offset); return 12; // Jump taken } return 7; // Jump not taken case 0x32: // LD (nn), A { ushort destAddress = FetchWord(); _memory.Write(destAddress, AF.High); return 13; } case 0x33: // INC SP SP++; return 6; case 0x36: // LD (HL), n byte nValue = FetchByte(); _memory.Write(HL.Word, nValue); return 10; case 0x37: // SCF AF.Low |= 0x01; // Force Carry Flag (Bit 0) to 1 AF.Low &= 0xED; return 4; case 0x38: // JR C, d sbyte jrCOffset = (sbyte)FetchByte(); // Check if the Carry Flag (Bit 0) IS set (1) if ((AF.Low & 0x01) != 0) { PC = (ushort)(PC + jrCOffset); return 12; } return 7; case 0x3A: // LD A, (nn) ushort address3A = FetchWord(); AF.High = _memory.Read(address3A); return 13; case 0x3B: // DEC SP SP--; return 6; case 0x3E: //LD A, n AF.High = FetchByte(); return 7; case 0x3F: // CCF bool previousCarry = (AF.Low & 0x01) != 0; AF.Low ^= 0x01; // Invert Carry Flag (Bit 0) AF.Low &= 0xFD; // Force Subtract Flag (N, Bit 1) to 0 // Set Half-Carry (H, Bit 4) to the previous Carry state if (previousCarry) AF.Low |= 0x10; else AF.Low &= 0xEF; return 4; case 0x40: //BC.High = BC.High; return 4; case 0x41: BC.High = BC.Low; return 4; case 0x42: BC.High = DE.High; return 4; case 0x43: BC.High = DE.Low; return 4; case 0x44: BC.High = HL.High; return 4; case 0x45: BC.High = HL.Low; return 4; case 0x46: BC.High = _memory.Read(HL.Word); return 7; case 0x47: BC.High = AF.High; return 4; // --- LD C, r --- case 0x48: BC.Low = BC.High; return 4; case 0x49: //BC.Low = BC.Low; return 4; case 0x4A: BC.Low = DE.High; return 4; case 0x4B: BC.Low = DE.Low; return 4; case 0x4C: BC.Low = HL.High; return 4; case 0x4D: BC.Low = HL.Low; return 4; case 0x4E: BC.Low = _memory.Read(HL.Word); return 7; case 0x4F: BC.Low = AF.High; return 4; // --- LD D, r --- case 0x50: DE.High = BC.High; return 4; case 0x51: DE.High = BC.Low; return 4; case 0x52: //DE.High = DE.High; return 4; case 0x53: DE.High = DE.Low; return 4; case 0x54: DE.High = HL.High; return 4; case 0x55: DE.High = HL.Low; return 4; case 0x56: DE.High = _memory.Read(HL.Word); return 7; case 0x57: DE.High = AF.High; return 4; // --- LD E, r --- case 0x58: DE.Low = BC.High; return 4; case 0x59: DE.Low = BC.Low; return 4; case 0x5A: DE.Low = DE.High; return 4; case 0x5B: //DE.Low = DE.Low; return 4; case 0x5C: DE.Low = HL.High; return 4; case 0x5D: DE.Low = HL.Low; return 4; case 0x5E: DE.Low = _memory.Read(HL.Word); return 7; case 0x5F: DE.Low = AF.High; return 4; // --- LD H, r --- case 0x60: HL.High = BC.High; return 4; case 0x61: HL.High = BC.Low; return 4; case 0x62: HL.High = DE.High; return 4; case 0x63: HL.High = DE.Low; return 4; case 0x64: //HL.High = HL.High; return 4; case 0x65: HL.High = HL.Low; return 4; case 0x66: HL.High = _memory.Read(HL.Word); return 7; case 0x67: HL.High = AF.High; return 4; // --- LD L, r --- case 0x68: HL.Low = BC.High; return 4; case 0x69: HL.Low = BC.Low; return 4; case 0x6A: HL.Low = DE.High; return 4; case 0x6B: HL.Low = DE.Low; return 4; case 0x6C: HL.Low = HL.High; return 4; case 0x6D: //HL.Low = HL.Low; return 4; case 0x6E: HL.Low = _memory.Read(HL.Word); return 7; case 0x6F: HL.Low = AF.High; return 4; // --- LD (HL), r --- (Note: 0x76 is HALT, so it is skipped here) case 0x70: _memory.Write(HL.Word, BC.High); return 7; case 0x71: _memory.Write(HL.Word, BC.Low); return 7; case 0x72: _memory.Write(HL.Word, DE.High); return 7; case 0x73: _memory.Write(HL.Word, DE.Low); return 7; case 0x74: _memory.Write(HL.Word, HL.High); return 7; case 0x75: _memory.Write(HL.Word, HL.Low); return 7; case 0x77: _memory.Write(HL.Word, AF.High); return 7; // --- LD A, r --- case 0x78: AF.High = BC.High; return 4; case 0x79: AF.High = BC.Low; return 4; case 0x7A: AF.High = DE.High; return 4; case 0x7B: AF.High = DE.Low; return 4; case 0x7C: AF.High = HL.High; return 4; case 0x7D: AF.High = HL.Low; return 4; case 0x7E: AF.High = _memory.Read(HL.Word); return 7; case 0x7F: //AF.High = AF.High; return 4; case 0x80: Add(BC.High); return 4; // ADD A, B case 0x81: Add(BC.Low); return 4; // ADD A, C case 0x82: Add(DE.High); return 4; // ADD A, D case 0x83: Add(DE.Low); return 4; // ADD A, E case 0x84: Add(HL.High); return 4; // ADD A, H case 0x85: Add(HL.Low); return 4; // ADD A, L case 0x86: Add(_memory.Read(HL.Word)); return 7; // ADD A, (HL) case 0x87: Add(AF.High); return 4; // ADD A, A // --- ADC A, Register Family --- case 0x88: AdcA(BC.High); return 4; // ADC A, B case 0x89: AdcA(BC.Low); return 4; // ADC A, C case 0x8A: AdcA(DE.High); return 4; // ADC A, D case 0x8B: AdcA(DE.Low); return 4; // ADC A, E case 0x8C: AdcA(HL.High); return 4; // ADC A, H case 0x8D: AdcA(HL.Low); return 4; // ADC A, L case 0x8F: AdcA(AF.High); return 4; // ADC A, A // --- ADC A, Memory --- case 0x8E: // ADC A, (HL) AdcA(_memory.Read(HL.Word)); return 7; // --- ADC A, Immediate --- case 0xCE: // ADC A, n AdcA(FetchByte()); return 7; case 0x90: Sub(BC.High); return 4; // SUB B case 0x91: Sub(BC.Low); return 4; // SUB C case 0x92: Sub(DE.High); return 4; // SUB D case 0x93: Sub(DE.Low); return 4; // SUB E case 0x94: Sub(HL.High); return 4; // SUB H case 0x95: Sub(HL.Low); return 4; // SUB L case 0x96: Sub(_memory.Read(HL.Word)); return 7; // SUB (HL) case 0x97: Sub(AF.High); return 4; // SUB A // --- SBC A, r --- case 0x98: Sbc(BC.High); return 4; // SBC A, B case 0x99: Sbc(BC.Low); return 4; // SBC A, C case 0x9A: Sbc(DE.High); return 4; // SBC A, D case 0x9B: Sbc(DE.Low); return 4; // SBC A, E case 0x9C: Sbc(HL.High); return 4; // SBC A, H case 0x9D: Sbc(HL.Low); return 4; // SBC A, L case 0x9E: Sbc(_memory.Read(HL.Word)); return 7; // SBC A, (HL) case 0x9F: Sbc(AF.High); return 4; // SBC A, A case 0xA0: And(BC.High); return 4; // AND B case 0xA1: And(BC.Low); return 4; // AND C case 0xA2: And(DE.High); return 4; // AND D case 0xA3: And(DE.Low); return 4; // AND E case 0xA4: And(HL.High); return 4; // AND H case 0xA5: And(HL.Low); return 4; // AND L case 0xA6: And(_memory.Read(HL.Word)); return 7; // AND (HL) case 0xA7: And(AF.High); return 4; // AND A case 0xA8: Xor(BC.High); return 4; // XOR B case 0xA9: Xor(BC.Low); return 4; // XOR C case 0xAA: Xor(DE.High); return 4; // XOR D case 0xAB: Xor(DE.Low); return 4; // XOR E case 0xAC: Xor(HL.High); return 4; // XOR H case 0xAD: Xor(HL.Low); return 4; // XOR L case 0xAE: Xor(_memory.Read(HL.Word)); return 7; // XOR (HL) case 0xAF: Xor(AF.High); return 4; // XOR A // --- OR r --- case 0xB0: Or(BC.High); return 4; // OR B case 0xB1: Or(BC.Low); return 4; // OR C case 0xB2: Or(DE.High); return 4; // OR D case 0xB3: Or(DE.Low); return 4; // OR E case 0xB4: Or(HL.High); return 4; // OR H case 0xB5: Or(HL.Low); return 4; // OR L case 0xB6: Or(_memory.Read(HL.Word)); return 7; // OR (HL) case 0xB7: Or(AF.High); return 4; // OR A // --- CP r --- case 0xB8: Cp(BC.High); return 4; // CP B case 0xB9: Cp(BC.Low); return 4; // CP C case 0xBA: Cp(DE.High); return 4; // CP D case 0xBB: Cp(DE.Low); return 4; // CP E case 0xBC: Cp(HL.High); return 4; // CP H case 0xBD: Cp(HL.Low); return 4; // CP L case 0xBE: Cp(_memory.Read(HL.Word)); return 7; // CP (HL) case 0xBF: Cp(AF.High); return 4; // CP A // --- Conditional Returns (11 T-States if taken, 5 if not) --- case 0xC0: // RET NZ if ((AF.Low & 0x40) == 0) { PC = Pop(); return 11; } return 5; case 0xE0: // RET PO (Parity Odd / No Overflow) if ((AF.Low & 0x04) == 0) { PC = Pop(); return 11; } return 5; case 0xE8: // RET PE (Parity Even / Overflow) if ((AF.Low & 0x04) != 0) { PC = Pop(); return 11; } return 5; case 0xF0: // RET P (Sign Positive) if ((AF.Low & 0x80) == 0) { PC = Pop(); return 11; } return 5; case 0xF8: // RET M (Sign Minus) if ((AF.Low & 0x80) != 0) { PC = Pop(); return 11; } return 5; case 0xC1: // POP BC BC.Word = Pop(); return 10; // --- Absolute Conditional Jumps (Always 10 T-States) --- case 0xC2: // JP NZ, nn { ushort addr = FetchWord(); if ((AF.Low & 0x40) == 0) PC = addr; return 10; } case 0xCA: // JP Z, nn { ushort addr = FetchWord(); if ((AF.Low & 0x40) != 0) PC = addr; return 10; } case 0xD2: // JP NC, nn { ushort addr = FetchWord(); if ((AF.Low & 0x01) == 0) PC = addr; return 10; } case 0xDA: // JP C, nn { ushort addr = FetchWord(); if ((AF.Low & 0x01) != 0) PC = addr; return 10; } case 0xDB: // IN A, (n) // 1. Fetch the immediate port offset byte byte portOffsetDB = FetchByte(); // 2. The Z80 puts 'A' on the top 8 bits, and 'n' on the bottom 8 bits ushort portAddressDB = (ushort)((AF.High << 8) | portOffsetDB); // 3. Read from the I/O bus and store the result straight into the Accumulator AF.High = _simpleIoBus.ReadPort(portAddressDB); return 11; case 0xE2: // JP PO, nn (Parity Odd / No Overflow) { ushort addr = FetchWord(); if ((AF.Low & 0x04) == 0) PC = addr; return 10; } case 0xEA: // JP PE, nn (Parity Even / Overflow) { ushort addr = FetchWord(); if ((AF.Low & 0x04) != 0) PC = addr; return 10; } case 0xF2: // JP P, nn (Sign Positive) { ushort addr = FetchWord(); if ((AF.Low & 0x80) == 0) PC = addr; return 10; } case 0xFA: // JP M, nn (Sign Minus) { ushort addr = FetchWord(); if ((AF.Low & 0x80) != 0) PC = addr; return 10; } case 0xC3: PC = FetchWord(); return 10; // --- Absolute Conditional Calls (17 T-States if taken, 10 if not) --- case 0xC4: // CALL NZ, nn { ushort addr = FetchWord(); if ((AF.Low & 0x40) == 0) { Push(PC); PC = addr; return 17; } return 10; } case 0xCC: // CALL Z, nn { ushort addr = FetchWord(); if ((AF.Low & 0x40) != 0) { Push(PC); PC = addr; return 17; } return 10; } case 0xD4: // CALL NC, nn { ushort addr = FetchWord(); if ((AF.Low & 0x01) == 0) { Push(PC); PC = addr; return 17; } return 10; } case 0xDC: // CALL C, nn { ushort addr = FetchWord(); if ((AF.Low & 0x01) != 0) { Push(PC); PC = addr; return 17; } return 10; } case 0xE4: // CALL PO, nn (Parity Odd) { ushort addr = FetchWord(); if ((AF.Low & 0x04) == 0) { Push(PC); PC = addr; return 17; } return 10; } case 0xEC: // CALL PE, nn (Parity Even) { ushort addr = FetchWord(); if ((AF.Low & 0x04) != 0) { Push(PC); PC = addr; return 17; } return 10; } case 0xF4: // CALL P, nn (Sign Positive) { ushort addr = FetchWord(); if ((AF.Low & 0x80) == 0) { Push(PC); PC = addr; return 17; } return 10; } case 0xFC: // CALL M, nn (Sign Minus) { ushort addr = FetchWord(); if ((AF.Low & 0x80) != 0) { Push(PC); PC = addr; return 17; } return 10; } case 0xc5: //push bc Push(BC.Word); return 11; case 0xC6: // ADD A, n Add(FetchByte()); return 7; // --- RST Instructions (11 T-States) --- // An RST is effectively a 1-byte CALL to a fixed Page 0 address. case 0xC7: Push(PC); PC = 0x0000; return 11; // RST 00h (Equivalent to a hardware reset) case 0xCF: Push(PC); PC = 0x0008; return 11; // RST 08h (Spectrum Error handler) case 0xD7: Push(PC); PC = 0x0010; return 11; // RST 10h (Spectrum Print Character) case 0xDF: Push(PC); PC = 0x0018; return 11; // RST 18h (Spectrum Collect Next Char) case 0xE7: Push(PC); PC = 0x0020; return 11; // RST 20h (Spectrum Collect Next Char/Space) case 0xEF: Push(PC); PC = 0x0028; return 11; // RST 28h (Spectrum Floating Point Calculator) case 0xF7: Push(PC); PC = 0x0030; return 11; // RST 30h (Spectrum Make BC Spaces) case 0xFF: Push(PC); PC = 0x0038; return 11; // RST 38h (Maskable Interrupt Handler) case 0xC8: // RET Z // Check if the Zero Flag (Bit 6) IS set if ((AF.Low & 0x40) != 0) { PC = Pop(); return 11; // Condition met, took the return } return 5; // Condition not met, skipped case 0xC9: // RET PC = Pop(); return 10; case 0xCB: return ExecuteCBPrefix(); case 0xCD: // CALL nn ushort callAddress = FetchWord(); Push(PC); PC = callAddress; return 17; case 0xD0: // RET NC // Check if the Carry Flag (Bit 0) is NOT set (0) if ((AF.Low & 0x01) == 0) { PC = Pop(); return 11; // Condition met, took the return } return 5; // Condition not met, skipped case 0xD1: // POP DE DE.Word = Pop(); return 10; case 0xD3: // OUT (n), A byte portOffset = FetchByte(); // The Z80 puts 'A' on the top 8 bits, and 'n' on the bottom 8 bits of the port address ushort portAddress = (ushort)((AF.High << 8) | portOffset); _simpleIoBus.WritePort(portAddress, AF.High); return 11; case 0xd5: //push bc Push(DE.Word); return 11; case 0xD6: // SUB n Sub(FetchByte()); return 7; case 0xD8: // RET C // Check if the Carry Flag (Bit 0) IS set (1) if ((AF.Low & 0x01) != 0) { PC = Pop(); return 11; // Condition met, took the return } return 5; case 0xD9: // EXX ushort tempBC = BC.Word; BC.Word = BC_Prime.Word; BC_Prime.Word = tempBC; ushort tempDE = DE.Word; DE.Word = DE_Prime.Word; DE_Prime.Word = tempDE; ushort tempHL = HL.Word; HL.Word = HL_Prime.Word; HL_Prime.Word = tempHL; return 4; case 0xDD: return ExecuteDDPrefix(); case 0xDE: // SBC A, n Sbc(FetchByte()); return 7; case 0xE1: // POP HL HL.Word = Pop(); return 10; case 0xE3: // EX (SP), HL // 1. Read the 16-bit value currently on top of the stack byte spLow = _memory.Read(SP); byte spHigh = _memory.Read((ushort)(SP + 1)); // 2. Write the current HL registers onto the stack in its place _memory.Write(SP, HL.Low); _memory.Write((ushort)(SP + 1), HL.High); // 3. Update HL with the data we pulled off the stack HL.Low = spLow; HL.High = spHigh; return 19; case 0xe5: //push bc Push(HL.Word); return 11; case 0xE6: // AND n And(FetchByte()); return 7; case 0xE9: // JP (HL) PC = HL.Word; return 4; // Takes 4 T-States case 0xEB: // EX DE, HL ushort tempEx = DE.Word; DE.Word = HL.Word; HL.Word = tempEx; return 4; // Takes 4 T-States case 0xED: return ExecuteExtendedPrefix(); case 0xEE: // XOR n byte xorImm = FetchByte(); // Perform the bitwise XOR against the Accumulator AF.High = (byte)(AF.High ^ xorImm); // --- Update Flags --- // Start with a clean slate of 0 to perfectly clear C, H, and N! newFlags = 0; if ((AF.High & 0x80) != 0) newFlags |= 0x80; // Sign Flag if (AF.High == 0) newFlags |= 0x40; // Zero Flag if (CalculateParity(AF.High)) newFlags |= 0x04; // Parity/Overflow Flag AF.Low = newFlags; return 7; // Takes 7 T-States case 0xF1: // POP AF AF.Word = Pop(); return 10; case 0xF3: // DI (Disable Interrupts) IFF1 = false; IFF2 = false; return 4; case 0xf5: //push bc Push(AF.Word); return 11; case 0xF6: // OR n Or(FetchByte()); return 7; case 0xF9: // LD SP, HL SP = HL.Word; // (Use SP.Word = HL.Word if you made SP a RegisterPair) return 6; case 0xFB: // EI IFF1 = true; IFF2 = true; return 4; case 0xFD: return ExecuteFDPrefix(); case 0xFE: // CP n Cp(FetchByte()); return 7; default: throw new NotImplementedException($"Opcode 0x{opcode:X2} at PC 0x{(PC - 1):X4} is not implemented."); } } private int ExecuteExtendedPrefix() //ED { // Fetch the actual extended instruction byte extendedOpcode = _memory.Read(PC++); byte val = 0; byte newFlags = 0; int result = 0; switch (extendedOpcode) { case 0x43: // LD (nn), BC ushort dest43 = FetchWord(); _memory.Write(dest43, BC.Low); _memory.Write((ushort)(dest43 + 1), BC.High); return 20; case 0x44: // NEG int aBefore = AF.High; result = 0 - aBefore; newFlags = 0; // S Flag (Bit 7): Set if the result is negative if ((result & 0x80) != 0) newFlags |= 0x80; // Z Flag (Bit 6): Set if the result is exactly zero if ((result & 0xFF) == 0) newFlags |= 0x40; // H Flag (Bit 4): Set if there was a borrow from Bit 3 if ((0 - (aBefore & 0x0F)) < 0) newFlags |= 0x10; // P/V Flag (Bit 2): Overflow happens ONLY if A was 0x80 (-128) if (aBefore == 0x80) newFlags |= 0x04; // N Flag (Bit 1): Always set to 1 for NEG newFlags |= 0x02; // C Flag (Bit 0): Set if A was not 0 before the operation if (aBefore != 0) newFlags |= 0x01; AF.Low = newFlags; AF.High = (byte)result; return 8; // 8 T-States case 0x47: // LD I, A I = AF.High; return 9; 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 case 0x52: // SBC HL, DE Sbc16(DE.Word); return 15; case 0x53: // LD (nn), DE ushort dest53 = FetchWord(); _memory.Write(dest53, DE.Low); _memory.Write((ushort)(dest53 + 1), DE.High); return 20; case 0x56: // IM 1 InterruptMode = 1; return 8; case 0x5B: // LD DE, (nn) ushort src5B = FetchWord(); DE.Low = _memory.Read(src5B); DE.High = _memory.Read((ushort)(src5B + 1)); return 20; case 0x72: // SBC HL, SP int carryIn = AF.Low & 0x01; int hlVal = HL.Word; int spVal = SP; // Perform the full 16-bit subtraction including the carry flag result = hlVal - spVal - carryIn; newFlags = 0; // S Flag (Bit 7): Set if the 16-bit result is negative (Bit 15 is 1) if ((result & 0x8000) != 0) newFlags |= 0x80; // Z Flag (Bit 6): Set if the full 16-bit result is exactly 0 if ((result & 0xFFFF) == 0) newFlags |= 0x40; // H Flag (Bit 4): Set if there was a borrow from Bit 11 if (((hlVal & 0x0FFF) - (spVal & 0x0FFF) - carryIn) < 0) newFlags |= 0x10; // P/V Flag (Bit 2): Set on Overflow // Overflow happens if the signs of the operands are different, // AND the sign of the result is different from the original HL if ((((hlVal ^ spVal) & (hlVal ^ result)) & 0x8000) != 0) newFlags |= 0x04; // N Flag (Bit 1): Always set to 1 for a subtraction newFlags |= 0x02; // C Flag (Bit 0): Set if the total result underflows 0 (Borrow from Bit 15) if (result < 0) newFlags |= 0x01; AF.Low = newFlags; HL.Word = (ushort)result; return 15; // 15 T-States case 0x73: // LD (nn), SP ushort dest73 = FetchWord(); _memory.Write(dest73, (byte)SP); _memory.Write((ushort)(dest73 + 1), (byte)(SP >> 8)); return 20; case 0x78: // IN A, (C) // Read from the hardware port using the full BC register as the address byte portVal78 = ReadPort(BC.Word); AF.High = portVal78; // --- Update Flags --- // S (Bit 7), Z (Bit 6), P/V (Bit 2) are set based on the input. // H (Bit 4) and N (Bit 1) are RESET. // C (Bit 0) is PRESERVED. newFlags = (byte)(AF.Low & 0x01); // Preserve Carry if ((portVal78 & 0x80) != 0) newFlags |= 0x80; // Sign Flag if (portVal78 == 0) newFlags |= 0x40; // Zero Flag if (CalculateParity(portVal78)) newFlags |= 0x04; // Parity/Overflow Flag AF.Low = newFlags; return 12; case 0x79: // OUT (C), A _simpleIoBus.WritePort(BC.Word, AF.High); return 12; // 12 T-States case 0x7B: // LD SP, (nn) // 1. Fetch the absolute 16-bit memory address from the instruction stream byte addrLow = FetchByte(); byte addrHigh = FetchByte(); ushort address7B = (ushort)((addrHigh << 8) | addrLow); // 2. Read the 16-bit value stored at that exact memory location (Little-Endian!) byte spLow = _memory.Read(address7B); byte spHigh = _memory.Read((ushort)(address7B + 1)); // 3. Load the resulting 16-bit value directly into the Stack Pointer SP = (ushort)((spHigh << 8) | spLow); return 20; case 0xB0: // LDIR // 1. Read byte from (HL) val = _memory.Read(HL.Word); // 2. Write byte to (DE) _memory.Write(DE.Word, val); // 3. Increment memory pointers, Decrement byte counter HL.Word++; DE.Word++; BC.Word--; // 4. Update Flags // Preserve S (0x80), Z (0x40), and C (0x01). // H (0x10) and N (0x02) are always reset to 0. AF.Low &= 0xC1; // P/V Flag (Bit 2) is set to 1 if BC is not 0 if (BC.Word != 0) { AF.Low |= 0x04; // Rewind the PC so the CPU executes this instruction again! PC -= 2; return 21; // Looping } return 16; case 0xB8: // LDDR // 1. Read byte from (HL) val = _memory.Read(HL.Word); // 2. Write byte to (DE) _memory.Write(DE.Word, val); // 3. Decrement all three pointers HL.Word--; DE.Word--; BC.Word--; // 4. Update Flags // Preserve S (0x80), Z (0x40), and C (0x01). // H (0x10) and N (0x02) are always reset to 0. AF.Low &= 0xC1; // P/V Flag (Bit 2) is set to 1 if BC is not 0 if (BC.Word != 0) { AF.Low |= 0x04; // Rewind the PC so the CPU executes this instruction again! PC -= 2; return 21; // Looping } return 16; // Finished! default: throw new NotImplementedException($"Extended ED Opcode 0x{extendedOpcode:X2} at PC 0x{(PC - 1):X4} is not implemented."); } } private int ExecuteCBPrefix() { byte cbOpcode = FetchByte(); // Extract the exact same mathematical properties int operation = cbOpcode >> 6; // 00 = Shift, 01 = BIT, 10 = RES, 11 = SET int bitIndex = (cbOpcode >> 3) & 0x07; // Extracts a number 0-7 int regIndex = cbOpcode & 0x07; // Extracts register index 0-7 byte bitMask = (byte)(1 << bitIndex); // --- PHASE 1: Fetch the target value --- byte val = 0; switch (regIndex) { case 0: val = BC.High; break; case 1: val = BC.Low; break; case 2: val = DE.High; break; case 3: val = DE.Low; break; case 4: val = HL.High; break; case 5: val = HL.Low; break; case 6: val = _memory.Read(HL.Word); break; // The 0x110 (HL) exception case 7: val = AF.High; break; } // --- PHASE 2: Perform the bitwise math --- switch (operation) { case 1: // ALL BIT Instructions AF.Low &= 0x01; // Preserve ONLY Carry AF.Low |= 0x10; // Set Half-Carry if ((val & bitMask) == 0) { AF.Low |= 0x44; // Set Zero and P/V } else if (bitIndex == 7) { AF.Low |= 0x80; // If testing Bit 7 and it is 1, set Sign } // BIT (HL) takes 12 T-States. Standard register BIT takes 8. return (regIndex == 6) ? 12 : 8; case 2: // ALL RES Instructions val &= (byte)(~bitMask); break; // Proceed to write-back case 3: // ALL SET Instructions val |= bitMask; break; // Proceed to write-back case 0: // ALL Shift/Rotate Instructions // The specific shift type is in the same bits we previously used for 'bitIndex' int shiftType = (cbOpcode >> 3) & 0x07; bool carryOut = false; switch (shiftType) { case 0: // RLC // Grab Bit 7 to see if it's going to fall off carryOut = (val & 0x80) != 0; // Shift left, and loop the falling bit back into Bit 0 val = (byte)((val << 1) | (carryOut ? 1 : 0)); break; case 7: // SRL (Shift Right Logical) // 1. Grab Bit 0 before it falls off to set the Carry flag carryOut = (val & 0x01) != 0; // 2. Shift the byte right by 1. // (In C#, a standard right shift on a positive byte automatically pads Bit 7 with a 0) val = (byte)(val >> 1); break; // (We will add RRC, RL, RR, SLA, SRA, here as the ROM asks for them!) default: throw new NotImplementedException($"CB Shift instruction type {shiftType} not implemented!"); } // --- Update Flags --- // All CB Shift instructions calculate flags the exact same way! // They set S, Z, P/V, and C. They forcefully clear H and N. byte newFlags = 0; if (carryOut) newFlags |= 0x01; // C Flag if ((val & 0x80) != 0) newFlags |= 0x80; // S Flag if (val == 0) newFlags |= 0x40; // Z Flag if (CalculateParity(val)) newFlags |= 0x04; // P/V Flag AF.Low = newFlags; // Apply the new flags break; // Proceed to the write-back phase default: throw new Exception("Invalid CB operation."); } // --- PHASE 3: Write back the modified value (RES and SET only) --- switch (regIndex) { case 0: BC.High = val; break; case 1: BC.Low = val; break; case 2: DE.High = val; break; case 3: DE.Low = val; break; case 4: HL.High = val; break; case 5: HL.Low = val; break; case 6: _memory.Write(HL.Word, val); break; case 7: AF.High = val; break; } // RES/SET (HL) takes 15 T-States. Standard register RES/SET takes 8. return (regIndex == 6) ? 15 : 8; } private int ExecuteDDPrefix() { byte ddOpcode = FetchByte(); // Fetch the actual instruction after 0xDD switch (ddOpcode) { case 0x09: // ADD IX, BC int result = IX.Word + BC.Word; // --- 16-Bit Flag Calculation --- // Preserve S (Bit 7), Z (Bit 6), and P/V (Bit 2). byte newFlags = (byte)(AF.Low & 0xC4); // Half-Carry (H - Bit 4): Set if carry from Bit 11 if (((IX.Word & 0x0FFF) + (BC.Word & 0x0FFF)) > 0x0FFF) newFlags |= 0x10; // Carry (C - Bit 0): Set if the total result overflows 16 bits if (result > 0xFFFF) newFlags |= 0x01; // (N - Bit 1 is left at 0 because the bitwise AND above cleared it) AF.Low = newFlags; IX.Word = (ushort)result; return 15; case 0x21: // LD IX, nn byte low = FetchByte(); byte high = FetchByte(); IX.Word = (ushort)((high << 8) | low); return 14; case 0x36: // LD (IX+d), n // 1. Fetch the displacement byte first sbyte offset36 = (sbyte)FetchByte(); // 2. Fetch the immediate 8-bit value second byte n36 = FetchByte(); // 3. Calculate the exact memory address (IX + offset) ushort address36 = (ushort)(IX.Word + offset36); // 4. Write the immediate value directly into memory _memory.Write(address36, n36); return 19; case 0x56: // LD D, (IX+d) // 1. Fetch the displacement byte and cast it to a signed sbyte sbyte offset56 = (sbyte)FetchByte(); // 2. Calculate the exact memory address (IX + offset) ushort address56 = (ushort)(IX.Word + offset56); // 3. Read the byte from memory and drop it into the D register (High byte of DE) DE.High = _memory.Read(address56); return 19; case 0x5E: // LD E, (IX+d) // 1. Fetch the displacement byte and cast it to a signed sbyte sbyte offset5E = (sbyte)FetchByte(); // 2. Calculate the exact memory address (IX + offset) ushort address5E = (ushort)(IX.Word + offset5E); // 3. Read the byte from memory and drop it into the E register DE.Low = _memory.Read(address5E); return 19; case 0x6E: // LD L, (IX+d) // 1. Fetch the displacement byte and cast it to a signed sbyte sbyte offset6E = (sbyte)FetchByte(); // 2. Calculate the exact memory address (IX + offset) ushort address6E = (ushort)(IX.Word + offset6E); // 3. Read the byte from memory and drop it into the L register (Low byte of HL) HL.Low = _memory.Read(address6E); return 19; case 0x74: // LD (IX+d), H // 1. Fetch the displacement byte and cast it to a signed sbyte sbyte offset74 = (sbyte)FetchByte(); // 2. Calculate the exact memory address (IX + offset) ushort address74 = (ushort)(IX.Word + offset74); // 3. Write the contents of the L register (Low byte of HL) into memory _memory.Write(address74, HL.High); return 19; case 0x75: // LD (IX+d), L // 1. Fetch the displacement byte and cast it to a signed sbyte sbyte offset75 = (sbyte)FetchByte(); // 2. Calculate the exact memory address (IX + offset) ushort address75 = (ushort)(IX.Word + offset75); // 3. Write the contents of the L register (Low byte of HL) into memory _memory.Write(address75, HL.Low); return 19; case 0x7E: // LD A, (IX+d) // 1. Fetch the displacement byte and cast it to a signed sbyte sbyte offset7E = (sbyte)FetchByte(); // 2. Calculate the exact memory address (IX + offset) ushort address7E = (ushort)(IX.Word + offset7E); // 3. Read the byte from memory and drop it straight into the Accumulator (A) AF.High = _memory.Read(address7E); return 19; case 0xBE: // CP (IX+d) // 1. Fetch the displacement byte and calculate the address sbyte offsetBE = (sbyte)FetchByte(); ushort addressBE = (ushort)(IX.Word + offsetBE); // 2. Read the value from memory byte cpVal = _memory.Read(addressBE); // 3. Perform the phantom subtraction int aVal = AF.High; result = aVal - cpVal; // --- 8-Bit Compare Flag Calculation (Identical to SUB) --- newFlags = 0; // S Flag (Bit 7): Set if the phantom result is negative if ((result & 0x80) != 0) newFlags |= 0x80; // Z Flag (Bit 6): Set if A perfectly matches the memory value (A - value == 0) if ((result & 0xFF) == 0) newFlags |= 0x40; // H Flag (Bit 4): Set if there was a borrow from Bit 3 if (((aVal & 0x0F) - (cpVal & 0x0F)) < 0) newFlags |= 0x10; // P/V Flag (Bit 2): Set on Overflow if ((((aVal ^ cpVal) & (aVal ^ result)) & 0x80) != 0) newFlags |= 0x04; // N Flag (Bit 1): Always set to 1 for Subtractions/Compares newFlags |= 0x02; // C Flag (Bit 0): Set if A was smaller than the memory value if (aVal < cpVal) newFlags |= 0x01; AF.Low = newFlags; // CRITICAL: Notice we do NOT update AF.High! The Accumulator is preserved. return 19; // 19 T-States case 0xE1: // POP IX // 1. Read the low byte from the top of the stack byte popLow = _memory.Read(SP); SP++; // Move stack pointer up // 2. Read the high byte byte popHigh = _memory.Read(SP); SP++; // Move stack pointer up again // 3. Combine them and store in IX IX.Word = (ushort)((popHigh << 8) | popLow); return 14; case 0xE5: // PUSH IX // 1. Decrement the stack pointer and write the HIGH byte SP--; _memory.Write(SP, IX.High); // 2. Decrement the stack pointer again and write the LOW byte SP--; _memory.Write(SP, IX.Low); return 15; case 0xE9: // JP (IX) PC = IX.Word; return 8; default: throw new NotImplementedException($"DD Prefix opcode 0x{ddOpcode:X2} not implemented!"); } } private int ExecuteFDPrefix() { byte opcode = FetchByte(); ushort targetAddress = 0; byte memVal = 0; switch (opcode) { case 0x21: // LD IY, nn IY.Word = FetchWord(); return 14; case 0x34: // INC (IY+d) // 1. Fetch displacement and calculate memory address sbyte offset34 = (sbyte)FetchByte(); ushort address34 = (ushort)(IY.Word + offset34); // 2. Read the value from memory byte valBefore = _memory.Read(address34); // 3. Increment the value int result = valBefore + 1; // --- 8-Bit Increment Flag Calculation --- // CRITICAL: We must preserve the existing Carry flag (Bit 0)! byte newFlags = (byte)(AF.Low & 0x01); // S Flag (Bit 7): Set if the result is negative if ((result & 0x80) != 0) newFlags |= 0x80; // Z Flag (Bit 6): Set if the result is exactly zero (wrapped from 255 to 0) if ((result & 0xFF) == 0) newFlags |= 0x40; // H Flag (Bit 4): Set if there was a carry out of Bit 3 if ((valBefore & 0x0F) == 0x0F) newFlags |= 0x10; // P/V Flag (Bit 2): Set on Overflow // For INC, overflow ONLY happens if we increment 0x7F (+127) and it wraps to 0x80 (-128) if (valBefore == 0x7F) newFlags |= 0x04; // N Flag (Bit 1): Always reset to 0 for an increment // (Our bitwise AND at the top already cleared it) AF.Low = newFlags; // 4. Write the modified value back to memory _memory.Write(address34, (byte)result); return 23; // 23 T-States case 0x35: // DEC (IY+d) sbyte offset = (sbyte)FetchByte(); targetAddress = (ushort)(IY.Word + offset); // Read, decrement using your existing helper, and write back memVal = _memory.Read(targetAddress); byte decVal = Dec8(memVal); _memory.Write(targetAddress, decVal); return 23; case 0x36: // LD (IY+d), n { sbyte offset36 = (sbyte)FetchByte(); byte nValue = FetchByte(); targetAddress = (ushort)(IY.Word + offset36); _memory.Write(targetAddress, nValue); return 19; // Takes 19 T-States } case 0x46: // LD B, (IY+d) { sbyte displacement = (sbyte)FetchByte(); targetAddress = (ushort)(IY.Word + displacement); BC.High = _memory.Read(targetAddress); return 19; // Takes 19 T-States } case 0x4E: // LD C, (IY+d) // 1. Fetch the displacement byte and cast it to a signed sbyte sbyte offset4E = (sbyte)FetchByte(); // 2. Calculate the final address (IY + offset) ushort address4E = (ushort)(IY.Word + offset4E); // 3. Read the memory and store it in C BC.Low = _memory.Read(address4E); return 19; case 0x56: // LD D, (IY+d) // 1. Fetch the displacement byte and cast it to a signed sbyte sbyte offset56 = (sbyte)FetchByte(); // 2. Calculate the final address (IY + offset) ushort address56 = (ushort)(IY.Word + offset56); // 3. Read the memory and store it in D (the high byte of DE) DE.High = _memory.Read(address56); return 19; case 0x5E: // LD E, (IY+d) // 1. Fetch the displacement byte and cast it to a signed sbyte sbyte offset5E = (sbyte)FetchByte(); // 2. Calculate the final address (IY + offset) ushort address5E = (ushort)(IY.Word + offset5E); // 3. Read the memory and store it in E (the low byte of DE) DE.Low = _memory.Read(address5E); return 19; case 0x6E: // LD L, (IY+d) sbyte displacementVal = (sbyte)FetchByte(); ushort targetAddr = (ushort)(IY.Word + displacementVal); HL.Low = _memory.Read(targetAddr); return 19; case 0x71: // LD (IY+d), C { sbyte offset71 = (sbyte)FetchByte(); targetAddress = (ushort)(IY.Word + offset71); // Write the C register (low byte of BC) to memory _memory.Write(targetAddress, BC.Low); return 19; // Takes 19 T-States } case 0x72: // LD (IY+d), D // 1. Fetch the displacement byte and cast it to a signed sbyte sbyte offset72 = (sbyte)FetchByte(); // 2. Calculate the exact memory address (IY + offset) ushort address72 = (ushort)(IY.Word + offset72); // 3. Write the contents of the D register (High byte of DE) into memory _memory.Write(address72, DE.High); return 19; // 19 T-States case 0x74: // LD (IY+d), H // 1. Fetch the displacement byte and cast it to a signed sbyte sbyte offset74 = (sbyte)FetchByte(); // 2. Calculate the exact memory address (IY + offset) ushort address74 = (ushort)(IY.Word + offset74); // 3. Write the contents of the H register into memory at that address _memory.Write(address74, HL.High); return 19; case 0x75: // LD (IY+d), L sbyte offset75 = (sbyte)FetchByte(); targetAddress = (ushort)(IY.Word + offset75); // Write the low byte of HL to memory _memory.Write(targetAddress, HL.Low); return 19; case 0x86: // ADD A, (IY+d) { sbyte displacementAdd = (sbyte)FetchByte(); ushort targetAddressAdd = (ushort)(IY.Word + displacementAdd); byte valueToAdd = _memory.Read(targetAddressAdd); AddA(valueToAdd); return 19; } case 0x96: // SUB (IY+d) // 1. Fetch the displacement byte and calculate the address sbyte offset96 = (sbyte)FetchByte(); ushort address96 = (ushort)(IY.Word + offset96); // 2. Read the value from memory byte subVal = _memory.Read(address96); // 3. Perform the subtraction from the Accumulator int aVal = AF.High; result = aVal - subVal; // --- 8-Bit Subtraction Flag Calculation --- newFlags = 0; // S Flag (Bit 7): Set if the result is negative if ((result & 0x80) != 0) newFlags |= 0x80; // Z Flag (Bit 6): Set if the result is exactly zero if ((result & 0xFF) == 0) newFlags |= 0x40; // H Flag (Bit 4): Set if there was a borrow from Bit 3 if (((aVal & 0x0F) - (subVal & 0x0F)) < 0) newFlags |= 0x10; // P/V Flag (Bit 2): Set on Overflow // (Happens if subtracting a negative from a positive gives a negative, or vice versa) if ((((aVal ^ subVal) & (aVal ^ result)) & 0x80) != 0) newFlags |= 0x04; // N Flag (Bit 1): Always set to 1 for a subtraction newFlags |= 0x02; // C Flag (Bit 0): Set if a borrow was needed (Accumulator was smaller than memory value) if (aVal < subVal) newFlags |= 0x01; AF.Low = newFlags; AF.High = (byte)result; return 19; case 0xBE: // CP (IY+d) // 1. Fetch the displacement byte and calculate the address using IY sbyte offsetBE = (sbyte)FetchByte(); ushort addressBE = (ushort)(IY.Word + offsetBE); // 2. Read the value from memory byte cpVal = _memory.Read(addressBE); // 3. Perform the phantom subtraction aVal = AF.High; result = aVal - cpVal; // --- 8-Bit Compare Flag Calculation (Identical to SUB) --- newFlags = 0; // S Flag (Bit 7): Set if the phantom result is negative if ((result & 0x80) != 0) newFlags |= 0x80; // Z Flag (Bit 6): Set if A perfectly matches the memory value if ((result & 0xFF) == 0) newFlags |= 0x40; // H Flag (Bit 4): Set if there was a borrow from Bit 3 if (((aVal & 0x0F) - (cpVal & 0x0F)) < 0) newFlags |= 0x10; // P/V Flag (Bit 2): Set on Overflow if ((((aVal ^ cpVal) & (aVal ^ result)) & 0x80) != 0) newFlags |= 0x04; // N Flag (Bit 1): Always set to 1 for Subtractions/Compares newFlags |= 0x02; // C Flag (Bit 0): Set if A was smaller than the memory value if (aVal < cpVal) newFlags |= 0x01; AF.Low = newFlags; return 19; // 19 T-States case 0xCB: // The FD CB nested prefix { sbyte displacement = (sbyte)FetchByte(); byte cbOpcode = FetchByte(); targetAddress = (ushort)(IY.Word + displacement); memVal = _memory.Read(targetAddress); // Extract the mathematical properties of the opcode int operation = cbOpcode >> 6; // 01 = BIT, 10 = RES, 11 = SET int bitIndex = (cbOpcode >> 3) & 0x07; // Extracts a number 0-7 byte bitMask = (byte)(1 << bitIndex); // Creates the bitmask (e.g., 0x01, 0x02, 0x80) switch (operation) { case 1: // ALL BIT Instructions AF.Low &= 0x01; // Preserve ONLY Carry AF.Low |= 0x10; // Set Half-Carry if ((memVal & bitMask) == 0) { AF.Low |= 0x44; // Set Zero (Bit 6) and P/V (Bit 2) } else if (bitIndex == 7) { AF.Low |= 0x80; // If testing Bit 7 and it is 1, set Sign (Bit 7) } return 20; case 2: // ALL RES Instructions memVal &= (byte)(~bitMask); // Invert mask and AND it to clear the bit _memory.Write(targetAddress, memVal); return 23; case 3: // ALL SET Instructions memVal |= bitMask; // OR the mask to force the bit to 1 _memory.Write(targetAddress, memVal); return 23; case 0: // Shift/Rotate instructions will go here later throw new NotImplementedException($"FD CB Shift/Rotate opcode {cbOpcode:X2} at PC 0x{(PC - 1):X4} not implemented!"); default: throw new Exception("Invalid bitwise operation."); } } default: throw new NotImplementedException($"FD prefix opcode {opcode:X2} at PC 0x{(PC - 2):X4} not implemented!"); } } } } //sbyte offsetCB = (sbyte)FetchByte(); // This is the '01' //byte bitOpcode = FetchByte(); // This is the 'CE' //targetAddress = (ushort)(IY.Word + offsetCB); //switch (bitOpcode) //{ // case 0x46: // BIT 0, (IY+d) // byte memValBit0 = _memory.Read(targetAddress); // // Preserve the existing Carry Flag (Bit 0) // byte newFlags = (byte)(AF.Low & 0x01); // newFlags |= 0x10; // Force Half-Carry (Bit 4) to 1 // // Test Bit 0. If it is 0, turn ON the Zero Flag (Bit 6) // if ((memValBit0 & 0x01) == 0) // { // newFlags |= 0x40; // } // AF.Low = newFlags; // return 20; // case 0x4E: // BIT 1, (IY+d) // { // byte memValBit = _memory.Read(targetAddress); // // Check if bit 1 is 0 // bool bitIsZero = (memValBit & 0x02) == 0; // // Preserve the Carry flag (Bit 0), clear everything else // AF.Low &= 0x01; // // Set Half-Carry (Bit 4) - Standard Z80 behavior for BIT // AF.Low |= 0x10; // if (bitIsZero) // { // AF.Low |= 0x40; // Set Zero Flag (Bit 6) // AF.Low |= 0x04; // Set P/V Flag (Bit 2) // } // return 20; // } // case 0x66: // BIT 4, (IY+d) // byte memValBit4 = _memory.Read(targetAddress); // // Preserve ONLY the Carry flag (Bit 0). This clears N and everything else. // AF.Low &= 0x01; // // Set Half-Carry (Bit 4) - Standard Z80 behavior for BIT instructions // AF.Low |= 0x10; // // Test Bit 4 (0x10 is Binary 0001 0000) // if ((memValBit4 & 0x10) == 0) // { // // If the bit is 0, turn ON the Zero Flag (Bit 6) // AF.Low |= 0x40; // // Parity/Overflow (Bit 2) is historically set along with the Zero flag on BIT // AF.Low |= 0x04; // } // // (Note: The Sign Flag remains 0 because we are not testing Bit 7) // return 20; // case 0x6E: // BIT 5, (IY+d) // byte memValBit5 = _memory.Read(targetAddress); // // Preserve ONLY the Carry flag (Bit 0). This clears N and everything else. // AF.Low &= 0x01; // // Set Half-Carry (Bit 4) - Standard Z80 behavior for BIT instructions // AF.Low |= 0x10; // // Test Bit 5 (0x20 is Binary 0010 0000) // if ((memValBit5 & 0x20) == 0) // { // // If the bit is 0, turn ON the Zero Flag (Bit 6) // AF.Low |= 0x40; // // Parity/Overflow (Bit 2) is historically set along with the Zero flag on BIT // AF.Low |= 0x04; // } // // (Note: The Sign Flag remains 0 because we are not testing Bit 7) // return 20; // case 0x76: // BIT 6, (IY+d) // byte memValBit6 = _memory.Read(targetAddress); // // Preserve ONLY the Carry flag (Bit 0). This clears N and everything else. // AF.Low &= 0x01; // // Set Half-Carry (Bit 4) - Standard Z80 behavior for BIT // AF.Low |= 0x10; // // Test Bit 6 (0x40 is Binary 0100 0000) // if ((memValBit6 & 0x40) == 0) // { // // If the bit is 0, turn ON the Zero Flag (Bit 6) // AF.Low |= 0x40; // // Parity/Overflow (Bit 2) is historically set along with the Zero flag on BIT // AF.Low |= 0x04; // } // // (Note: The Sign Flag remains 0 because we are not testing Bit 7) // return 20; // case 0x86: // RES 0, (IY+d) // byte memValRes0 = _memory.Read(targetAddress); // // 0xFE is Binary 1111 1110. // // ANDing preserves all bits except Bit 0, which becomes 0. // memValRes0 &= 0xFE; // _memory.Write(targetAddress, memValRes0); // return 23; // Takes 23 T-States // case 0x8E: // RES 1, (IY+d) // byte memValRes = _memory.Read(targetAddress); // // 0xFD is Binary 1111 1101. // // ANDing with this preserves all bits except Bit 1, which becomes 0. // memValRes &= 0xFD; // _memory.Write(targetAddress, memValRes); // return 23; // case 0xA6: // RES 4, (IY+d) // byte memValRes4 = _memory.Read(targetAddress); // // 0xEF is Binary 1110 1111 // // ANDing preserves all bits except Bit 4, which becomes 0. // memValRes4 &= 0xEF; // _memory.Write(targetAddress, memValRes4); // return 23; // case 0xAE: // RES 5, (IY+d) // byte memValRes5 = _memory.Read(targetAddress); // // 0xDF is Binary 1101 1111 // // ANDing perfectly preserves all other bits while forcing Bit 5 to 0 // memValRes5 &= 0xDF; // _memory.Write(targetAddress, memValRes5); // return 23; // case 0xC6: // SET 0, (IY+d) // byte memValSet0 = _memory.Read(targetAddress); // // 0x01 is Binary 0000 0001 // // ORing forces Bit 0 to 1 and perfectly preserves all other bits // memValSet0 |= 0x01; // _memory.Write(targetAddress, memValSet0); // return 23; // Takes 23 T-States // case 0xCE: // SET 1, (IY+d) // memVal = _memory.Read(targetAddress); // memVal |= 0x02; // 0x02 is Binary 0000 0010 (Bit 1) // _memory.Write(targetAddress, memVal); // return 23; // case 0xE6: // SET 4, (IY+d) // byte memValSet4 = _memory.Read(targetAddress); // // 0x10 is Binary 0001 0000 // // ORing perfectly preserves all other bits while forcing Bit 4 to 1 // memValSet4 |= 0x10; // _memory.Write(targetAddress, memValSet4); // return 23; // case 0xEE: // SET 5, (IY+d) // byte memValSet5 = _memory.Read(targetAddress); // // 0x20 is Binary 0010 0000 // // ORing perfectly preserves all other bits while forcing Bit 5 to 1 // memValSet5 |= 0x20; // _memory.Write(targetAddress, memValSet5); // return 23; // default: // throw new NotImplementedException($"FD CB opcode {bitOpcode:X2} at PC 0x{(PC - 1):X4} not implemented!"); //}