From dcbb505145d4761e46ba055d4ad366a8057e2b12 Mon Sep 17 00:00:00 2001 From: Marc Parsons Date: Tue, 21 Apr 2026 15:34:10 +0100 Subject: [PATCH] ULA Implemented. Scanline renderer so cycle accurate --- Core/Cpu/Z80.cs | 296 +++++++++++++++++-------------- Core/Io/ULA.cs | 128 +++++++++++++ Desktop/DebuggerForm.Designer.cs | 55 ++++-- Desktop/DebuggerForm.cs | 14 +- Desktop/Form1.Designer.cs | 2 +- Desktop/Form1.cs | 239 +++++++++---------------- 6 files changed, 424 insertions(+), 310 deletions(-) create mode 100644 Core/Io/ULA.cs diff --git a/Core/Cpu/Z80.cs b/Core/Cpu/Z80.cs index a44bd0a..54b8466 100644 --- a/Core/Cpu/Z80.cs +++ b/Core/Cpu/Z80.cs @@ -43,6 +43,9 @@ namespace Core.Cpu private readonly IO_Bus _simpleIoBus; public TapManager _tapManager; + //External Timing interface + public Func? WaitStateCallback { get; set; } + //Misc Variables byte newFlags = 0; int result = 0; @@ -88,6 +91,15 @@ namespace Core.Cpu //_memory.CleanRAMData(); } + private void ApplyWaitStates(ushort address) + { + // If a system (like a ULA) is attached and listening, ask it for the delay + if (WaitStateCallback != null) + { + TotalTStates += WaitStateCallback(address, TotalTStates); + } + } + public int RequestInterrupt() { InterruptRequested = true; @@ -116,8 +128,8 @@ namespace Core.Cpu 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)); + byte pcLow = ReadMemory(vectorAddress); + byte pcHigh = ReadMemory((ushort)(vectorAddress + 1)); // C. Jump to the custom game routine! PC = (ushort)((pcHigh << 8) | pcLow); @@ -131,6 +143,38 @@ namespace Core.Cpu } } + // 1. For fetching opcodes and immediate values (Advances PC) + public byte FetchByte() + { + ApplyWaitStates(PC); + byte data = _memory.Read(PC); + PC++; + return data; + } + + // 2. For fetching 16-bit immediate values + private ushort FetchWord() + { + // By using FetchByte twice, we perfectly apply wait states to BOTH memory reads! + byte low = FetchByte(); + byte high = FetchByte(); + return (ushort)((high << 8) | low); + } + + // 3. For standard memory reads (e.g., LD A, (HL)) + public byte ReadMemory(ushort address) + { + ApplyWaitStates(address); + return _memory.Read(address); + } + + // 4. For standard memory writes (e.g., LD (HL), A) + public void WriteMemory(ushort address, byte data) + { + ApplyWaitStates(address); + _memory.Write(address, data); + } + // Helper method to calculate if a byte has an Even Parity of 1s private bool CalculateParity(byte b) { @@ -157,7 +201,7 @@ namespace Core.Cpu } // Fetch the next opcode and increment the Program Counter - byte opcode = _memory.Read(PC++); + byte opcode = ReadMemory(PC++); int tStates = ExecuteOpcode(opcode); TotalTStates += tStates; @@ -194,7 +238,7 @@ namespace Core.Cpu // 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]); + WriteMemory((ushort)(0x4000 + i), snaData[27 + i]); } // --- 3. The Magic Bullet --- @@ -226,7 +270,7 @@ namespace Core.Cpu int bytesToCopy = DE.Word; for (int i = 0; i < bytesToCopy; i++) { - _memory.Write((ushort)(IX.Word + i), block[i + 1]); + WriteMemory((ushort)(IX.Word + i), block[i + 1]); } // 4. Update the registers exactly how the ROM would after a successful load @@ -246,25 +290,15 @@ namespace Core.Cpu // A quick helper to simulate a RET instruction manually private void ExecuteRet() { - byte pcLow = _memory.Read(SP); + byte pcLow = ReadMemory(SP); SP++; - byte pcHigh = _memory.Read(SP); + byte pcHigh = ReadMemory(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() { @@ -674,20 +708,20 @@ namespace Core.Cpu { // High byte goes first SP--; - _memory.Write(SP, (byte)(value >> 8)); + WriteMemory(SP, (byte)(value >> 8)); // Low byte goes second SP--; - _memory.Write(SP, (byte)(value & 0xFF)); + WriteMemory(SP, (byte)(value & 0xFF)); } private ushort Pop() { // The Z80 is Little-Endian. Low byte comes off the stack first. - byte low = _memory.Read(SP++); + byte low = ReadMemory(SP++); // High byte comes off second. - byte high = _memory.Read(SP++); + byte high = ReadMemory(SP++); return (ushort)((high << 8) | low); } @@ -704,7 +738,7 @@ namespace Core.Cpu BC.Word = FetchWord(); return 10; case 0x02: // LD (BC), A - _memory.Write(BC.Word, AF.High); + WriteMemory(BC.Word, AF.High); return 7; case 0x03: // INC BC BC.Word++; @@ -736,11 +770,11 @@ namespace Core.Cpu AF_Prime.Word = tempAF; return 4; case 0x0A: //LD A (BC) - AF.High = _memory.Read(BC.Word); + AF.High = ReadMemory(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); + WriteMemory(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 @@ -752,7 +786,7 @@ namespace Core.Cpu HL.Low = FetchByte(); return 7; case 0x34: - _memory.Write(HL.Word, Inc8(_memory.Read(HL.Word))); + WriteMemory(HL.Word, Inc8(ReadMemory(HL.Word))); return 11; // INC (HL) takes 11 T-States case 0x3C: AF.High = Inc8(AF.High); return 4; // INC A @@ -773,7 +807,7 @@ namespace Core.Cpu return 4; case 0x35: - _memory.Write(HL.Word, Dec8(_memory.Read(HL.Word))); + WriteMemory(HL.Word, Dec8(ReadMemory(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 @@ -864,7 +898,7 @@ namespace Core.Cpu return 12; case 0x1A: // LD A, (DE) - AF.High = _memory.Read(DE.Word); + AF.High = ReadMemory(DE.Word); return 7; case 0x1B: // DEC DE DE.Word--; @@ -903,8 +937,8 @@ namespace Core.Cpu return 10; case 0x22: // LD (nn), HL ushort dest22 = FetchWord(); - _memory.Write(dest22, HL.Low); - _memory.Write((ushort)(dest22 + 1), HL.High); + WriteMemory(dest22, HL.Low); + WriteMemory((ushort)(dest22 + 1), HL.High); return 16; case 0x23: // INC HL HL.Word++; @@ -971,8 +1005,8 @@ namespace Core.Cpu case 0x2A: // LD HL, (nn) { ushort srcAddress = FetchWord(); - HL.Low = _memory.Read(srcAddress); - HL.High = _memory.Read((ushort)(srcAddress + 1)); + HL.Low = ReadMemory(srcAddress); + HL.High = ReadMemory((ushort)(srcAddress + 1)); return 16; } case 0x2B: // DEC HL @@ -991,7 +1025,7 @@ namespace Core.Cpu case 0x32: // LD (nn), A { ushort destAddress = FetchWord(); - _memory.Write(destAddress, AF.High); + WriteMemory(destAddress, AF.High); return 13; } case 0x33: // INC SP @@ -999,7 +1033,7 @@ namespace Core.Cpu return 6; case 0x36: // LD (HL), n byte nValue = FetchByte(); - _memory.Write(HL.Word, nValue); + WriteMemory(HL.Word, nValue); return 10; case 0x37: // SCF AF.Low |= 0x01; // Force Carry Flag (Bit 0) to 1 @@ -1016,7 +1050,7 @@ namespace Core.Cpu return 7; case 0x3A: // LD A, (nn) ushort address3A = FetchWord(); - AF.High = _memory.Read(address3A); + AF.High = ReadMemory(address3A); return 13; case 0x3B: // DEC SP SP--; @@ -1041,7 +1075,7 @@ namespace Core.Cpu 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 0x46: BC.High = ReadMemory(HL.Word); return 7; case 0x47: BC.High = AF.High; return 4; // --- LD C, r --- @@ -1052,7 +1086,7 @@ namespace Core.Cpu 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 0x4E: BC.Low = ReadMemory(HL.Word); return 7; case 0x4F: BC.Low = AF.High; return 4; // --- LD D, r --- @@ -1063,7 +1097,7 @@ namespace Core.Cpu 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 0x56: DE.High = ReadMemory(HL.Word); return 7; case 0x57: DE.High = AF.High; return 4; // --- LD E, r --- @@ -1074,7 +1108,7 @@ namespace Core.Cpu 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 0x5E: DE.Low = ReadMemory(HL.Word); return 7; case 0x5F: DE.Low = AF.High; return 4; // --- LD H, r --- @@ -1085,7 +1119,7 @@ namespace Core.Cpu 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 0x66: HL.High = ReadMemory(HL.Word); return 7; case 0x67: HL.High = AF.High; return 4; // --- LD L, r --- @@ -1096,16 +1130,16 @@ namespace Core.Cpu 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 0x6E: HL.Low = ReadMemory(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 0x70: WriteMemory(HL.Word, BC.High); return 7; + case 0x71: WriteMemory(HL.Word, BC.Low); return 7; + case 0x72: WriteMemory(HL.Word, DE.High); return 7; + case 0x73: WriteMemory(HL.Word, DE.Low); return 7; + case 0x74: WriteMemory(HL.Word, HL.High); return 7; + case 0x75: WriteMemory(HL.Word, HL.Low); return 7; case 0x76: //HALT if (!InterruptRequested) { @@ -1117,7 +1151,7 @@ namespace Core.Cpu InterruptRequested = false; return 4; } - case 0x77: _memory.Write(HL.Word, AF.High); return 7; + case 0x77: WriteMemory(HL.Word, AF.High); return 7; // --- LD A, r --- case 0x78: AF.High = BC.High; return 4; @@ -1126,7 +1160,7 @@ namespace Core.Cpu 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 0x7E: AF.High = ReadMemory(HL.Word); return 7; case 0x7F: //AF.High = AF.High; return 4; case 0x80: Add(BC.High); return 4; // ADD A, B @@ -1135,7 +1169,7 @@ namespace Core.Cpu 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 0x86: Add(ReadMemory(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 @@ -1148,7 +1182,7 @@ namespace Core.Cpu // --- ADC A, Memory --- case 0x8E: // ADC A, (HL) - AdcA(_memory.Read(HL.Word)); + AdcA(ReadMemory(HL.Word)); return 7; // --- ADC A, Immediate --- @@ -1161,7 +1195,7 @@ namespace Core.Cpu 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 0x96: Sub(ReadMemory(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 @@ -1170,7 +1204,7 @@ namespace Core.Cpu 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 0x9E: Sbc(ReadMemory(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 @@ -1178,7 +1212,7 @@ namespace Core.Cpu 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 0xA6: And(ReadMemory(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 @@ -1186,7 +1220,7 @@ namespace Core.Cpu 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 0xAE: Xor(ReadMemory(HL.Word)); return 7; // XOR (HL) case 0xAF: Xor(AF.High); return 4; // XOR A // --- OR r --- @@ -1196,7 +1230,7 @@ namespace Core.Cpu 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 0xB6: Or(ReadMemory(HL.Word)); return 7; // OR (HL) case 0xB7: Or(AF.High); return 4; // OR A // --- CP r --- @@ -1206,7 +1240,7 @@ namespace Core.Cpu 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 0xBE: Cp(ReadMemory(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 @@ -1431,12 +1465,12 @@ namespace Core.Cpu 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)); + byte spLow = ReadMemory(SP); + byte spHigh = ReadMemory((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); + WriteMemory(SP, HL.Low); + WriteMemory((ushort)(SP + 1), HL.High); // 3. Update HL with the data we pulled off the stack HL.Low = spLow; @@ -1508,7 +1542,7 @@ namespace Core.Cpu private int ExecuteExtendedPrefix() //ED { // Fetch the actual extended instruction - byte extendedOpcode = _memory.Read(PC++); + byte extendedOpcode = FetchByte(); byte val = 0; switch (extendedOpcode) @@ -1518,8 +1552,8 @@ namespace Core.Cpu return 15; case 0x43: // LD (nn), BC ushort dest43 = FetchWord(); - _memory.Write(dest43, BC.Low); - _memory.Write((ushort)(dest43 + 1), BC.High); + WriteMemory(dest43, BC.Low); + WriteMemory((ushort)(dest43 + 1), BC.High); return 20; case 0x44: // NEG int aBefore = AF.High; @@ -1557,8 +1591,8 @@ namespace Core.Cpu return 15; case 0x4B: // LD BC, (nn) ushort src4B = FetchWord(); - BC.Low = _memory.Read(src4B); - BC.High = _memory.Read((ushort)(src4B + 1)); + BC.Low = ReadMemory(src4B); + BC.High = ReadMemory((ushort)(src4B + 1)); return 20; case 0x4D: // RETI Does not affect IFF1 or IFF2 PC = Pop(); @@ -1568,8 +1602,8 @@ namespace Core.Cpu return 15; case 0x53: // LD (nn), DE ushort dest53 = FetchWord(); - _memory.Write(dest53, DE.Low); - _memory.Write((ushort)(dest53 + 1), DE.High); + WriteMemory(dest53, DE.Low); + WriteMemory((ushort)(dest53 + 1), DE.High); return 20; case 0x56: // IM 1 InterruptMode = 1; @@ -1579,8 +1613,8 @@ namespace Core.Cpu return 15; case 0x5B: // LD DE, (nn) ushort src5B = FetchWord(); - DE.Low = _memory.Read(src5B); - DE.High = _memory.Read((ushort)(src5B + 1)); + DE.Low = ReadMemory(src5B); + DE.High = ReadMemory((ushort)(src5B + 1)); return 20; case 0x5E: // IM 2 // Set the CPU's internal interrupt mode state @@ -1649,8 +1683,8 @@ namespace Core.Cpu 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)); + WriteMemory(dest73, (byte)SP); + WriteMemory((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 @@ -1684,8 +1718,8 @@ namespace Core.Cpu 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)); + byte spLow = ReadMemory(address7B); + byte spHigh = ReadMemory((ushort)(address7B + 1)); // 3. Load the resulting 16-bit value directly into the Stack Pointer SP = (ushort)((spHigh << 8) | spLow); @@ -1693,10 +1727,10 @@ namespace Core.Cpu return 20; case 0xA0: // LDI // 1. Read byte from (HL) - val = _memory.Read(HL.Word); + val = ReadMemory(HL.Word); // 2. Write byte to (DE) - _memory.Write(DE.Word, val); + WriteMemory(DE.Word, val); // 3. Increment memory pointers, Decrement byte counter HL.Word++; @@ -1717,10 +1751,10 @@ namespace Core.Cpu return 16; case 0xB0: // LDIR // 1. Read byte from (HL) - val = _memory.Read(HL.Word); + val = ReadMemory(HL.Word); // 2. Write byte to (DE) - _memory.Write(DE.Word, val); + WriteMemory(DE.Word, val); // 3. Increment memory pointers, Decrement byte counter HL.Word++; @@ -1744,10 +1778,10 @@ namespace Core.Cpu return 16; case 0xB8: // LDDR // 1. Read byte from (HL) - val = _memory.Read(HL.Word); + val = ReadMemory(HL.Word); // 2. Write byte to (DE) - _memory.Write(DE.Word, val); + WriteMemory(DE.Word, val); // 3. Decrement all three pointers HL.Word--; @@ -1797,7 +1831,7 @@ namespace Core.Cpu 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 6: val = ReadMemory(HL.Word); break; // The 0x110 (HL) exception case 7: val = AF.High; break; } @@ -1930,7 +1964,7 @@ namespace Core.Cpu 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 6: WriteMemory(HL.Word, val); break; case 7: AF.High = val; break; } @@ -1969,10 +2003,10 @@ namespace Core.Cpu ushort address22 = (ushort)((addrHigh22 << 8) | addrLow22); // 2. Write the LOW byte of IX to the exact address - _memory.Write(address22, IX.Low); + WriteMemory(address22, IX.Low); // 3. Write the HIGH byte of IX to the address + 1 - _memory.Write((ushort)(address22 + 1), IX.High); + WriteMemory((ushort)(address22 + 1), IX.High); return 20; case 0x23: // INC IX @@ -1998,10 +2032,10 @@ namespace Core.Cpu ushort address2A = (ushort)((addrHigh2A << 8) | addrLow2A); // 2. Read the LOW byte from that specific memory location - byte ixLow = _memory.Read(address2A); + byte ixLow = ReadMemory(address2A); // 3. Read the HIGH byte from the next consecutive memory location - byte ixHigh = _memory.Read((ushort)(address2A + 1)); + byte ixHigh = ReadMemory((ushort)(address2A + 1)); // 4. Combine them and drop them into the IX register pair IX.Word = (ushort)((ixHigh << 8) | ixLow); @@ -2026,13 +2060,13 @@ namespace Core.Cpu ushort address34 = (ushort)(IX.Word + offset34); // 3. Read the value from memory - byte val34 = _memory.Read(address34); + byte val34 = ReadMemory(address34); // 4. Pass it through your helper to increment and set the flags perfectly byte result34 = Inc8(val34); // 5. Write the incremented value back to memory - _memory.Write(address34, result34); + WriteMemory(address34, result34); return 23; case 0x35: // DEC (IX+d) @@ -2043,13 +2077,13 @@ namespace Core.Cpu ushort address35 = (ushort)(IX.Word + offset35); // 3. Read the value from memory - byte val35 = _memory.Read(address35); + byte val35 = ReadMemory(address35); // 4. Pass it through your helper to decrement and set the flags byte result35 = Dec8(val35); // 5. Write the decremented value back to memory - _memory.Write(address35, result35); + WriteMemory(address35, result35); return 23; case 0x36: // LD (IX+d), n @@ -2063,7 +2097,7 @@ namespace Core.Cpu ushort address36 = (ushort)(IX.Word + offset36); // 4. Write the immediate value directly into memory - _memory.Write(address36, n36); + WriteMemory(address36, n36); return 19; case 0x46: // LD B, (IX+d) @@ -2074,7 +2108,7 @@ namespace Core.Cpu ushort address46 = (ushort)(IX.Word + offset46); // 3. Read the byte from memory and drop it into the C register (Low byte of BC) - BC.High = _memory.Read(address46); + BC.High = ReadMemory(address46); return 19; case 0x4E: // LD C, (IX+d) @@ -2085,7 +2119,7 @@ namespace Core.Cpu ushort address4E = (ushort)(IX.Word + offset4E); // 3. Read the byte from memory and drop it into the C register (Low byte of BC) - BC.Low = _memory.Read(address4E); + BC.Low = ReadMemory(address4E); return 19; case 0x56: // LD D, (IX+d) @@ -2096,7 +2130,7 @@ namespace Core.Cpu 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); + DE.High = ReadMemory(address56); return 19; case 0x5E: // LD E, (IX+d) @@ -2107,7 +2141,7 @@ namespace Core.Cpu ushort address5E = (ushort)(IX.Word + offset5E); // 3. Read the byte from memory and drop it into the E register - DE.Low = _memory.Read(address5E); + DE.Low = ReadMemory(address5E); return 19; case 0x66: // LD H, (IX+d) @@ -2118,7 +2152,7 @@ namespace Core.Cpu ushort address66 = (ushort)(IX.Word + offset66); // 3. Read the byte from memory and drop it into the H register (High byte of HL) - HL.High = _memory.Read(address66); + HL.High = ReadMemory(address66); return 19; case 0x67: // LD IXH, A @@ -2145,7 +2179,7 @@ namespace Core.Cpu 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); + HL.Low = ReadMemory(address6E); return 19; case 0x72: // LD (IX+d), D @@ -2156,7 +2190,7 @@ namespace Core.Cpu ushort address72 = (ushort)(IX.Word + offset72); // 3. Write the D register (DE.High) to memory - _memory.Write(address72, DE.High); + WriteMemory(address72, DE.High); return 19; // 19 T-States case 0x73: // LD (IX+d), E @@ -2167,7 +2201,7 @@ namespace Core.Cpu ushort address73 = (ushort)(IX.Word + offset73); // 3. Write the E register (DE.Low) to memory - _memory.Write(address73, DE.Low); + WriteMemory(address73, DE.Low); return 19; @@ -2179,7 +2213,7 @@ namespace Core.Cpu 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); + WriteMemory(address74, HL.High); return 19; case 0x75: // LD (IX+d), L @@ -2190,7 +2224,7 @@ namespace Core.Cpu 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); + WriteMemory(address75, HL.Low); return 19; case 0x77: // LD (IX+d), A @@ -2201,7 +2235,7 @@ namespace Core.Cpu ushort address77 = (ushort)(IX.Word + offset77); // 3. Write the Accumulator (AF.High) into memory at that address - _memory.Write(address77, AF.High); + WriteMemory(address77, AF.High); return 19; case 0x7C: // LD A, IXH @@ -2216,7 +2250,7 @@ namespace Core.Cpu 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); + AF.High = ReadMemory(address7E); return 19; case 0x86: // ADD A, (IX+d) @@ -2224,7 +2258,7 @@ namespace Core.Cpu ushort address86 = (ushort)(IX.Word + offset86); // Read the memory and pass it straight into your flawless helper! - Add(_memory.Read(address86)); + Add(ReadMemory(address86)); return 19; case 0x96: // SUB (IX+d) @@ -2232,7 +2266,7 @@ namespace Core.Cpu ushort address96 = (ushort)(IX.Word + offset96); // Read the memory and pass it straight into your flawless helper! - Sub(_memory.Read(address96)); + Sub(ReadMemory(address96)); return 19; case 0xBE: // CP (IX+d) // 1. Fetch the displacement byte and calculate the address @@ -2240,7 +2274,7 @@ namespace Core.Cpu ushort addressBE = (ushort)(IX.Word + offsetBE); // 2. Read the value from memory - byte cpVal = _memory.Read(addressBE); + byte cpVal = ReadMemory(addressBE); // 3. Perform the phantom subtraction int aVal = AF.High; @@ -2281,7 +2315,7 @@ namespace Core.Cpu byte cbOpcode = FetchByte(); ushort targetAddress = (ushort)(IX.Word + displacement); - byte memVal = _memory.Read(targetAddress); + byte memVal = ReadMemory(targetAddress); // Extract the mathematical properties of the opcode int operation = cbOpcode >> 6; // 01 = BIT, 10 = RES, 11 = SET @@ -2312,11 +2346,11 @@ namespace Core.Cpu } case 0xE1: // POP IX // 1. Read the low byte from the top of the stack - byte popLow = _memory.Read(SP); + byte popLow = ReadMemory(SP); SP++; // Move stack pointer up // 2. Read the high byte - byte popHigh = _memory.Read(SP); + byte popHigh = ReadMemory(SP); SP++; // Move stack pointer up again // 3. Combine them and store in IX @@ -2326,11 +2360,11 @@ namespace Core.Cpu case 0xE5: // PUSH IX // 1. Decrement the stack pointer and write the HIGH byte SP--; - _memory.Write(SP, IX.High); + WriteMemory(SP, IX.High); // 2. Decrement the stack pointer again and write the LOW byte SP--; - _memory.Write(SP, IX.Low); + WriteMemory(SP, IX.Low); return 15; case 0xE9: // JP (IX) @@ -2359,7 +2393,7 @@ namespace Core.Cpu ushort address34 = (ushort)(IY.Word + offset34); // 2. Read the value from memory - byte valBefore = _memory.Read(address34); + byte valBefore = ReadMemory(address34); // 3. Increment the value int result = valBefore + 1; @@ -2387,7 +2421,7 @@ namespace Core.Cpu AF.Low = newFlags; // 4. Write the modified value back to memory - _memory.Write(address34, (byte)result); + WriteMemory(address34, (byte)result); return 23; // 23 T-States case 0x35: // DEC (IY+d) @@ -2395,9 +2429,9 @@ namespace Core.Cpu targetAddress = (ushort)(IY.Word + offset); // Read, decrement using your existing helper, and write back - memVal = _memory.Read(targetAddress); + memVal = ReadMemory(targetAddress); byte decVal = Dec8(memVal); - _memory.Write(targetAddress, decVal); + WriteMemory(targetAddress, decVal); return 23; case 0x36: // LD (IY+d), n { @@ -2405,7 +2439,7 @@ namespace Core.Cpu byte nValue = FetchByte(); targetAddress = (ushort)(IY.Word + offset36); - _memory.Write(targetAddress, nValue); + WriteMemory(targetAddress, nValue); return 19; // Takes 19 T-States } case 0x46: // LD B, (IY+d) @@ -2413,7 +2447,7 @@ namespace Core.Cpu sbyte displacement = (sbyte)FetchByte(); targetAddress = (ushort)(IY.Word + displacement); - BC.High = _memory.Read(targetAddress); + BC.High = ReadMemory(targetAddress); return 19; // Takes 19 T-States } case 0x4E: // LD C, (IY+d) @@ -2424,7 +2458,7 @@ namespace Core.Cpu ushort address4E = (ushort)(IY.Word + offset4E); // 3. Read the memory and store it in C - BC.Low = _memory.Read(address4E); + BC.Low = ReadMemory(address4E); return 19; case 0x56: // LD D, (IY+d) @@ -2435,7 +2469,7 @@ namespace Core.Cpu 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); + DE.High = ReadMemory(address56); return 19; case 0x5E: // LD E, (IY+d) @@ -2446,14 +2480,14 @@ namespace Core.Cpu 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); + DE.Low = ReadMemory(address5E); return 19; case 0x6E: // LD L, (IY+d) sbyte displacementVal = (sbyte)FetchByte(); ushort targetAddr = (ushort)(IY.Word + displacementVal); - HL.Low = _memory.Read(targetAddr); + HL.Low = ReadMemory(targetAddr); return 19; case 0x71: // LD (IY+d), C { @@ -2461,7 +2495,7 @@ namespace Core.Cpu targetAddress = (ushort)(IY.Word + offset71); // Write the C register (low byte of BC) to memory - _memory.Write(targetAddress, BC.Low); + WriteMemory(targetAddress, BC.Low); return 19; // Takes 19 T-States } case 0x72: // LD (IY+d), D @@ -2472,7 +2506,7 @@ namespace Core.Cpu 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); + WriteMemory(address72, DE.High); return 19; // 19 T-States case 0x74: // LD (IY+d), H @@ -2483,20 +2517,20 @@ namespace Core.Cpu ushort address74 = (ushort)(IY.Word + offset74); // 3. Write the contents of the H register into memory at that address - _memory.Write(address74, HL.High); + WriteMemory(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); + WriteMemory(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); + byte valueToAdd = ReadMemory(targetAddressAdd); AddA(valueToAdd); return 19; @@ -2507,7 +2541,7 @@ namespace Core.Cpu ushort address96 = (ushort)(IY.Word + offset96); // 2. Read the value from memory - byte subVal = _memory.Read(address96); + byte subVal = ReadMemory(address96); // 3. Perform the subtraction from the Accumulator int aVal = AF.High; @@ -2545,7 +2579,7 @@ namespace Core.Cpu ushort addressBE = (ushort)(IY.Word + offsetBE); // 2. Read the value from memory - byte cpVal = _memory.Read(addressBE); + byte cpVal = ReadMemory(addressBE); // 3. Perform the phantom subtraction aVal = AF.High; @@ -2580,7 +2614,7 @@ namespace Core.Cpu sbyte displacement = (sbyte)FetchByte(); byte cbOpcode = FetchByte(); targetAddress = (ushort)(IY.Word + displacement); - memVal = _memory.Read(targetAddress); + memVal = ReadMemory(targetAddress); // Extract the mathematical properties of the opcode int operation = cbOpcode >> 6; // 01 = BIT, 10 = RES, 11 = SET @@ -2605,12 +2639,12 @@ namespace Core.Cpu case 2: // ALL RES Instructions memVal &= (byte)(~bitMask); // Invert mask and AND it to clear the bit - _memory.Write(targetAddress, memVal); + WriteMemory(targetAddress, memVal); return 23; case 3: // ALL SET Instructions memVal |= bitMask; // OR the mask to force the bit to 1 - _memory.Write(targetAddress, memVal); + WriteMemory(targetAddress, memVal); return 23; case 0: diff --git a/Core/Io/ULA.cs b/Core/Io/ULA.cs new file mode 100644 index 0000000..96afc34 --- /dev/null +++ b/Core/Io/ULA.cs @@ -0,0 +1,128 @@ +using Core.Memory; +using System; + +namespace Core.Io +{ + public class ULA + { + private readonly MemoryBus _memoryBus; + private readonly IO_Bus _simpleIoBus; + + // The ULA owns the frame buffer now! + public const int ScreenWidth = 320; + public const int ScreenHeight = 256; // Perfectly cropped size + public int[] FrameBuffer { get; private set; } + + private int _ulaFrameCount = 0; + + // The hardware color palette belongs to the ULA, not the Windows Form! + private readonly int[] SpectrumColors = new int[] + { + unchecked((int)0xFF000000), unchecked((int)0xFF0000D7), + unchecked((int)0xFFD70000), unchecked((int)0xFFD700D7), + unchecked((int)0xFF00D700), unchecked((int)0xFF00D7D7), + unchecked((int)0xFFD7D700), unchecked((int)0xFFD7D7D7), + + unchecked((int)0xFF000000), unchecked((int)0xFF0000FF), + unchecked((int)0xFFFF0000), unchecked((int)0xFFFF00FF), + unchecked((int)0xFF00FF00), unchecked((int)0xFF00FFFF), + unchecked((int)0xFFFFFF00), unchecked((int)0xFFFFFFFF) + }; + + public ULA(MemoryBus memoryBus, IO_Bus ioBus) + { + _memoryBus = memoryBus; + _simpleIoBus = ioBus; + FrameBuffer = new int[ScreenWidth * ScreenHeight]; + } + + // We will wire this up to the Z80 in Phase 3 so the CPU stays system-agnostic! + public int GetContentionDelay(ushort address, long currentTStates) + { + if (address < 0x4000 || address > 0x7FFF) return 0; + long frameT = currentTStates % 69888; + if (frameT < 14336 || frameT >= 57344) return 0; + int lineT = (int)(frameT % 224); + if (lineT < 14 || lineT > 141) return 0; + + int delayOffset = (lineT - 14) % 8; + int[] delayPattern = { 6, 5, 4, 3, 2, 1, 0, 0 }; + return delayPattern[delayOffset]; + } + + // The perfectly cropped, 320x256 Scanline Renderer + public void RenderScanline(int scanline) + { + // 1. Drop the invisible lines instantly (VBlank/Overscan) + if (scanline < 32 || scanline > 287) return; + + // 2. Calculate our visual Y coordinate (0 to 255) for the bitmap array + int renderY = scanline - 32; + int currentBorderColor = SpectrumColors[_simpleIoBus.BorderColorIndex]; + + // --- Are we in the Top or Bottom Border? --- + if (scanline < 64 || scanline > 255) + { + int yOffset = renderY * ScreenWidth; + for (int x = 0; x < ScreenWidth; x++) + { + FrameBuffer[yOffset + x] = currentBorderColor; + } + return; + } + + // --- We are in the Visible Display Area (Lines 64 to 255) --- + int y = scanline - 64; + + // Handle Flash Phase (only increment once per frame when y == 0) + if (y == 0) _ulaFrameCount++; + bool invertFlashPhase = (_ulaFrameCount % 32) >= 16; + + int rowStartIndex = renderY * ScreenWidth; + + // Draw the 32 pixels of left border + for (int b = 0; b < 32; b++) FrameBuffer[rowStartIndex + b] = currentBorderColor; + + int third = y / 64; + int characterRow = (y % 64) / 8; + int pixelRow = y % 8; + + // Draw the 32 horizontal character blocks of the visible screen + for (int col = 0; col < 32; col++) + { + ushort pixelAddress = (ushort)(0x4000 | (third << 11) | (pixelRow << 8) | (characterRow << 5) | col); + ushort attrAddress = (ushort)(0x5800 + (y / 8) * 32 + col); + + byte pixels = _memoryBus.Read(pixelAddress); + byte attr = _memoryBus.Read(attrAddress); + + int ink = attr & 0x07; + int paper = (attr >> 3) & 0x07; + int brightOffset = (attr & 0x40) != 0 ? 8 : 0; + bool isFlashSet = (attr & 0x80) != 0; + + int inkColor = SpectrumColors[ink + brightOffset]; + int paperColor = SpectrumColors[paper + brightOffset]; + + if (isFlashSet && invertFlashPhase) + { + int temp = inkColor; inkColor = paperColor; paperColor = temp; + } + + // Draw the 8 pixels in this block + for (int bit = 0; bit < 8; bit++) + { + bool isPixelSet = (pixels & (1 << (7 - bit))) != 0; + int renderX = 32 + (col * 8) + bit; + FrameBuffer[rowStartIndex + renderX] = isPixelSet ? inkColor : paperColor; + } + } + + // Draw the 32 pixels of right border + for (int b = ScreenWidth - 32; b < ScreenWidth; b++) + { + FrameBuffer[rowStartIndex + b] = currentBorderColor; + } + } + } +} \ No newline at end of file diff --git a/Desktop/DebuggerForm.Designer.cs b/Desktop/DebuggerForm.Designer.cs index 211b446..b1fc976 100644 --- a/Desktop/DebuggerForm.Designer.cs +++ b/Desktop/DebuggerForm.Designer.cs @@ -39,7 +39,6 @@ lblTStates = new Label(); txtMemoryStart = new TextBox(); btnStep = new Button(); - btnRun = new Button(); btnRefreshMemory = new Button(); txtMemoryView = new RichTextBox(); lstDisassembly = new ListBox(); @@ -56,6 +55,9 @@ lblIE = new Label(); btnReset = new Button(); uiUpdateTimer = new System.Windows.Forms.Timer(components); + lblFrames = new Label(); + lblFPS = new Label(); + lblFrameTime = new Label(); SuspendLayout(); // // lblAF @@ -160,16 +162,6 @@ btnStep.UseVisualStyleBackColor = true; btnStep.Click += btnStep_Click; // - // btnRun - // - btnRun.Location = new Point(119, 418); - btnRun.Margin = new Padding(2); - btnRun.Name = "btnRun"; - btnRun.Size = new Size(90, 27); - btnRun.TabIndex = 13; - btnRun.Text = "Run"; - btnRun.UseVisualStyleBackColor = true; - // // btnRefreshMemory // btnRefreshMemory.Location = new Point(425, 21); @@ -215,7 +207,7 @@ btnExit.Name = "btnExit"; btnExit.Size = new Size(90, 27); btnExit.TabIndex = 18; - btnExit.Text = "Full Exit"; + btnExit.Text = "Exit"; btnExit.UseVisualStyleBackColor = true; // // label1 @@ -304,13 +296,47 @@ // uiUpdateTimer // uiUpdateTimer.Enabled = true; + uiUpdateTimer.Interval = 1; uiUpdateTimer.Tick += uiUpdateTimer_Tick; // + // lblFrames + // + lblFrames.AutoSize = true; + lblFrames.Location = new Point(538, 320); + lblFrames.Margin = new Padding(2, 0, 2, 0); + lblFrames.Name = "lblFrames"; + lblFrames.Size = new Size(124, 20); + lblFrames.TabIndex = 28; + lblFrames.Text = "Frames Rendered"; + // + // lblFPS + // + lblFPS.AutoSize = true; + lblFPS.Location = new Point(630, 397); + lblFPS.Margin = new Padding(2, 0, 2, 0); + lblFPS.Name = "lblFPS"; + lblFPS.Size = new Size(32, 20); + lblFPS.TabIndex = 29; + lblFPS.Text = "FPS"; + // + // lblFrameTime + // + lblFrameTime.AutoSize = true; + lblFrameTime.Location = new Point(575, 356); + lblFrameTime.Margin = new Padding(2, 0, 2, 0); + lblFrameTime.Name = "lblFrameTime"; + lblFrameTime.Size = new Size(87, 20); + lblFrameTime.TabIndex = 30; + lblFrameTime.Text = "Frame Time"; + // // DebuggerForm // AutoScaleDimensions = new SizeF(8F, 20F); AutoScaleMode = AutoScaleMode.Font; ClientSize = new Size(928, 454); + Controls.Add(lblFrameTime); + Controls.Add(lblFPS); + Controls.Add(lblFrames); Controls.Add(btnReset); Controls.Add(lblIE); Controls.Add(lblIff2); @@ -325,7 +351,6 @@ Controls.Add(lstDisassembly); Controls.Add(txtMemoryView); Controls.Add(btnRefreshMemory); - Controls.Add(btnRun); Controls.Add(btnStep); Controls.Add(txtMemoryStart); Controls.Add(lblTStates); @@ -355,7 +380,6 @@ private Label lblTStates; private TextBox txtMemoryStart; private Button btnStep; - private Button btnRun; private Button btnRefreshMemory; private RichTextBox txtMemoryView; private ListBox lstStack; @@ -372,6 +396,9 @@ private Label lblIE; private Button btnReset; private System.Windows.Forms.Timer uiUpdateTimer; + private Label lblFrames; + private Label lblFPS; + private Label lblFrameTime; //private TextBox textBox4; } } \ No newline at end of file diff --git a/Desktop/DebuggerForm.cs b/Desktop/DebuggerForm.cs index 40a8f0e..c378158 100644 --- a/Desktop/DebuggerForm.cs +++ b/Desktop/DebuggerForm.cs @@ -11,8 +11,6 @@ namespace Desktop private readonly Z80 _cpu; private readonly MemoryBus _memoryBus; private readonly Form1 _mainForm; - private bool _isRunning = false; - private ushort? _breakpoint = null; public DebuggerForm(Z80 cpu, MemoryBus memoryBus, Form1 mainForm) { @@ -61,15 +59,12 @@ namespace Desktop private void uiUpdateTimer_Tick(object sender, EventArgs e) { - // Every 100ms, take a snapshot of the CPU state and draw it to the screen. - // This makes the registers look like they are spinning matrix-style while running! UpdateDisplay(); } - // This is the master function that pulls state from the CPU + // Current Emulator State private void UpdateDisplay() { - // 1. Update Registers (Formatting as 4-character Hex strings) lblAF.Text = $"AF: {_cpu.AF.Word:X4}"; lblBC.Text = $"BC: {_cpu.BC.Word:X4}"; lblDE.Text = $"DE: {_cpu.DE.Word:X4}"; @@ -81,12 +76,11 @@ namespace Desktop lblIff1.Text = $"IFF1: {_cpu.IFF1}"; lblIff2.Text = $"IFF2: {_cpu.IFF2}"; lblIE.Text = $"Interrupt Mode: {_cpu.InterruptMode}"; - - // 2. Update Flags & T-States lblFlags.Text = $"Flags: {_cpu.GetFlagsString()}"; lblTStates.Text = $"T-States: {_cpu.TotalTStates}"; - - // 3. Update Memory Viewer + lblFrames.Text = $"Frames Rendered: {_mainForm.TotalFrameCount}"; + lblFrameTime.Text = $"Frame Time: {((float)_mainForm.FrameTime):F1}ms"; + lblFPS.Text = $"FPS: {_mainForm.FramesPerSecond:F2}"; UpdateMemoryView(); UpdateStackView(); UpdateDisassemblyView(); diff --git a/Desktop/Form1.Designer.cs b/Desktop/Form1.Designer.cs index 076b9ad..6f6e4d2 100644 --- a/Desktop/Form1.Designer.cs +++ b/Desktop/Form1.Designer.cs @@ -112,7 +112,7 @@ // debuggerToolStripMenuItem // debuggerToolStripMenuItem.Name = "debuggerToolStripMenuItem"; - debuggerToolStripMenuItem.Size = new Size(224, 26); + debuggerToolStripMenuItem.Size = new Size(159, 26); debuggerToolStripMenuItem.Text = "Debugger"; debuggerToolStripMenuItem.Click += openDebuggerToolStripMenuItem_Click; // diff --git a/Desktop/Form1.cs b/Desktop/Form1.cs index 82eb89a..2393f0e 100644 --- a/Desktop/Form1.cs +++ b/Desktop/Form1.cs @@ -1,8 +1,8 @@ -using System; -using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; -using System.Windows.Forms; +using System.Diagnostics; +using System.Threading; +using System.IO; using Core.Cpu; using Core.Io; using Core.Memory; @@ -14,37 +14,18 @@ namespace Desktop private Z80 _cpu = null!; private MemoryBus _memoryBus = null!; private IO_Bus _simpleIoBus = null!; + private ULA _ula = null!; private TapManager _tapManager = null!; - private int _ulaFrameCount = 0; + private DebuggerForm? _debugger = null; + private string _baseTitle = ""; private bool _isRunning = false; private bool _isPaused = false; - private bool _resetFlag = false; - public ushort? Breakpoint = null; // Public so the debugger can set it! - private DebuggerForm _debugger = null; - - // The 16 physical colors of the ZX Spectrum (ARGB format) - private readonly int[] SpectrumColors = new int[] -{ - // Normal Colors (Bright = 0) - unchecked((int)0xFF000000), // 0: Black - unchecked((int)0xFF0000D7), // 1: Blue - unchecked((int)0xFFD70000), // 2: Red - unchecked((int)0xFFD700D7), // 3: Magenta - unchecked((int)0xFF00D700), // 4: Green - unchecked((int)0xFF00D7D7), // 5: Cyan - unchecked((int)0xFFD7D700), // 6: Yellow - unchecked((int)0xFFD7D7D7), // 7: White - - // Bright Colors (Bright = 1) - unchecked((int)0xFF000000), // 8: Bright Black - unchecked((int)0xFF0000FF), // 9: Bright Blue - unchecked((int)0xFFFF0000), // 10: Bright Red - unchecked((int)0xFFFF00FF), // 11: Bright Magenta - unchecked((int)0xFF00FF00), // 12: Bright Green - unchecked((int)0xFF00FFFF), // 13: Bright Cyan - unchecked((int)0xFFFFFF00), // 14: Bright Yellow - unchecked((int)0xFFFFFFFF) // 15: Bright White -}; + public ushort? Breakpoint = null; + public long TotalFrameCount = 0; + public double FramesPerSecond = 0; + public double TotalFrameTime = 0; + public double FrameTime = 0; + public Form1() { @@ -56,18 +37,17 @@ namespace Desktop { try { + _baseTitle = this.Text; _memoryBus = new MemoryBus(); _simpleIoBus = new IO_Bus(); + _ula = new ULA(_memoryBus, _simpleIoBus); _tapManager = new TapManager(); _memoryBus.CrapRAMData(); byte[] romData = RomLoader.Load("48.rom"); _memoryBus.LoadRom(romData); - _cpu = new Z80(_memoryBus, _simpleIoBus, _tapManager); + _cpu.WaitStateCallback = _ula.GetContentionDelay; - // Pass 'this' so the DebuggerForm can talk back to this main window - //DebuggerForm debugger = new DebuggerForm(_cpu, _memoryBus, this); - //debugger.Show(); } catch (Exception ex) { @@ -81,28 +61,22 @@ namespace Desktop if (_isRunning) return; _isRunning = true; _isPaused = false; - Task.Run(() => { try { const int TStatesPerFrame = 69888; - long nextFrameTargetTStates = _cpu.TotalTStates + TStatesPerFrame; - var stopwatch = System.Diagnostics.Stopwatch.StartNew(); - long frameCount = 0; + long nextScanlineTarget = _cpu.TotalTStates + TStatesPerFrame; + var stopwatch = Stopwatch.StartNew(); + var fpsStopwatch = Stopwatch.StartNew(); + long scanlineCount = 0; while (_isRunning) - { - //if(_resetFlag) - //{ - // _resetFlag = false; - // stopwatch.Reset(); - // nextFrameTargetTStates = _cpu.TotalTStates + TStatesPerFrame; - // frameCount = 0; - //} + { + if (_isPaused) { - System.Threading.Thread.Sleep(10); // Don't melt the host CPU while paused + Thread.Sleep(10); continue; } @@ -110,7 +84,6 @@ namespace Desktop if (Breakpoint.HasValue && _cpu.PC == Breakpoint.Value) { _isPaused = true; - // Optional: You could force the debugger to open here! continue; } @@ -118,22 +91,43 @@ namespace Desktop _cpu.Step(); // --- Check for End of Frame --- - if (_cpu.TotalTStates >= nextFrameTargetTStates) + if (_cpu.TotalTStates >= nextScanlineTarget) { - _cpu.RequestInterrupt(); - nextFrameTargetTStates += TStatesPerFrame; - frameCount++; + // Tell the ULA to draw one line of pixels + _ula.RenderScanline((int)scanlineCount % 312); - // Render the screen - this.Invoke((MethodInvoker)delegate { RenderScreen(); }); + nextScanlineTarget += 224; // Advance target by ONE line (224 T-States) + scanlineCount++; - // Throttle to real-time (50 FPS = 20ms) - long targetTimeMs = frameCount * 20; - long elapsedMs = stopwatch.ElapsedMilliseconds; - - if (elapsedMs < targetTimeMs) + // Hit the bottom of the screen (Line 312)? + if (scanlineCount % 312 == 0) { - System.Threading.Thread.Sleep((int)(targetTimeMs - elapsedMs)); + _cpu.RequestInterrupt(); // 50Hz interrupt + + this.Invoke((MethodInvoker)delegate + { + UpdateScreenBitmap(); + this.Text = $"{_baseTitle} - FPS: {FramesPerSecond:F1}"; + }); + TotalFrameCount++; + + // Throttle to real-time (50 FPS = 20ms) + long targetTimeMs = (scanlineCount / 312) * 20; + long elapsedMs = stopwatch.ElapsedMilliseconds; + + if (elapsedMs < targetTimeMs) + { + Thread.Sleep((int)(targetTimeMs - elapsedMs)); + } + TotalFrameTime += fpsStopwatch.Elapsed.TotalMilliseconds; + if (TotalFrameCount % 50 == 0) + { + FramesPerSecond = 1000.0 / (TotalFrameTime / 50.0); + FrameTime = TotalFrameTime / 50.0; + TotalFrameTime = 0; + } + + fpsStopwatch.Restart(); } } } @@ -141,13 +135,33 @@ namespace Desktop catch (Exception ex) { _isPaused = true; - this.Invoke((MethodInvoker)delegate { + this.Invoke((MethodInvoker)delegate + { MessageBox.Show(ex.Message, "CPU Crash", MessageBoxButtons.OK, MessageBoxIcon.Error); }); } }); } + + + private void UpdateScreenBitmap() + { + // Build the bitmap + Bitmap bmp = new Bitmap(ULA.ScreenWidth, ULA.ScreenHeight, PixelFormat.Format32bppArgb); + BitmapData bmpData = bmp.LockBits( + new Rectangle(0, 0, ULA.ScreenWidth, ULA.ScreenHeight), + ImageLockMode.WriteOnly, + bmp.PixelFormat); + + // Pull the raw pixel data + Marshal.Copy(_ula.FrameBuffer, 0, bmpData.Scan0, _ula.FrameBuffer.Length); + bmp.UnlockBits(bmpData); + + if (picScreen.Image != null) picScreen.Image.Dispose(); + picScreen.Image = bmp; + } + private void loadTAPToolStripMenuItem_Click(object sender, EventArgs e) { using (OpenFileDialog ofd = new OpenFileDialog()) @@ -155,13 +169,8 @@ namespace Desktop ofd.Filter = "Spectrum TAP Files|*.tap"; if (ofd.ShowDialog() == DialogResult.OK) { - // The Desktop UI reads the file from the hard drive - byte[] tapBytes = System.IO.File.ReadAllBytes(ofd.FileName); - - // The pure Core logic processes the bytes + byte[] tapBytes = File.ReadAllBytes(ofd.FileName); _cpu._tapManager.LoadTapData(tapBytes); - - //MessageBox.Show("Tape inserted! Type LOAD \"\" and press Enter.", "Tape Deck"); } } } @@ -169,10 +178,10 @@ namespace Desktop { using (OpenFileDialog ofd = new OpenFileDialog()) { - ofd.Filter = "Spectrum Snapshot Files (*.sna)|*.sna"; + ofd.Filter = "Snapshot Files (sna,z80)|*.sna"; if (ofd.ShowDialog() == DialogResult.OK) { - byte[] snaBytes = System.IO.File.ReadAllBytes(ofd.FileName); + byte[] snaBytes = File.ReadAllBytes(ofd.FileName); _cpu.LoadSNA(snaBytes); } } @@ -189,7 +198,6 @@ namespace Desktop private void btnReset_Click(object sender, EventArgs e) { - //_resetFlag = true; _isPaused = true; _cpu.Reset(); _memoryBus.CleanRAMData(); @@ -213,84 +221,7 @@ namespace Desktop _debugger.BringToFront(); } } - - // Public so the Debugger's background thread can call it 50 times a second - public void RenderScreen() - { - _ulaFrameCount++; - bool invertFlashPhase = (_ulaFrameCount % 32) >= 16; - - // --- NEW: Expanded screen size (32px border on all sides) --- - const int screenWidth = 320; - const int screenHeight = 256; - const int borderSize = 32; - - int[] pixelData = new int[screenWidth * screenHeight]; - - // --- NEW: Fill the background with the Border Color --- - // (Note: The hardware border always uses standard brightness, never bright) - int currentBorderColor = SpectrumColors[_simpleIoBus.BorderColorIndex]; - Array.Fill(pixelData, currentBorderColor); - - // Loop through the 6144 bytes of Pixel RAM - for (int offset = 0; offset < 6144; offset++) - { - ushort address = (ushort)(0x4000 + offset); - byte pixels = _memoryBus.Read(address); - - int y = ((offset & 0x0700) >> 8) | - ((offset & 0x00E0) >> 2) | - ((offset & 0x1800) >> 5); - - int x = (offset & 0x001F) * 8; - - int attrRow = y / 8; - int attrCol = x / 8; - ushort attrAddress = (ushort)(0x5800 + (attrRow * 32) + attrCol); - byte attr = _memoryBus.Read(attrAddress); - - int ink = attr & 0x07; - int paper = (attr >> 3) & 0x07; - int brightOffset = (attr & 0x40) != 0 ? 8 : 0; - bool isFlashSet = (attr & 0x80) != 0; - - int inkColor = SpectrumColors[ink + brightOffset]; - int paperColor = SpectrumColors[paper + brightOffset]; - - if (isFlashSet && invertFlashPhase) - { - int temp = inkColor; - inkColor = paperColor; - paperColor = temp; - } - - // Draw the 8 pixels - for (int bit = 0; bit < 8; bit++) - { - bool isPixelSet = (pixels & (1 << (7 - bit))) != 0; - - // --- NEW: Add the 32px border offset to our X and Y calculations! --- - int renderY = y + borderSize; - int renderX = x + borderSize + bit; - - // Map it to our new, wider pixel array - pixelData[(renderY * screenWidth) + renderX] = isPixelSet ? inkColor : paperColor; - } - } - - // --- NEW: Update Bitmap dimensions to match the new 320x256 array --- - Bitmap bmp = new Bitmap(screenWidth, screenHeight, PixelFormat.Format32bppArgb); - BitmapData bmpData = bmp.LockBits( - new Rectangle(0, 0, screenWidth, screenHeight), - ImageLockMode.WriteOnly, - bmp.PixelFormat); - - Marshal.Copy(pixelData, 0, bmpData.Scan0, pixelData.Length); - bmp.UnlockBits(bmpData); - - if (picScreen.Image != null) picScreen.Image.Dispose(); - picScreen.Image = bmp; - } + private void UpdateMatrix(int row, int col, bool isPressed) { if (isPressed) @@ -305,14 +236,14 @@ namespace Desktop } } - // Hook this to Form1's KeyDown event + protected override void OnKeyDown(KeyEventArgs e) { HandleKey(e.KeyCode, true); base.OnKeyDown(e); } - // Hook this to Form1's KeyUp event + protected override void OnKeyUp(KeyEventArgs e) { HandleKey(e.KeyCode, false); @@ -370,15 +301,15 @@ namespace Desktop case Keys.L: UpdateMatrix(6, 1, isPressed); break; case Keys.K: UpdateMatrix(6, 2, isPressed); break; case Keys.J: UpdateMatrix(6, 3, isPressed); break; - case Keys.H: UpdateMatrix(6, 4, isPressed); break; + case Keys.H: UpdateMatrix(6, 4, isPressed); break; // Row 7 case Keys.Space: UpdateMatrix(7, 0, isPressed); break; case Keys.ControlKey: UpdateMatrix(7, 1, isPressed); break; // Symbol Shift case Keys.M: UpdateMatrix(7, 2, isPressed); break; case Keys.N: UpdateMatrix(7, 3, isPressed); break; - case Keys.B: UpdateMatrix(7, 4, isPressed); break; + case Keys.B: UpdateMatrix(7, 4, isPressed); break; } - } + } } } \ No newline at end of file