using Core.Cpu; using Core.Io; using Core.Memory; using Core.Interfaces; using System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; namespace Core { public class SpectrumMachine { // The Hardware public Z80 Cpu { get; private set; } public MemoryBus Memory { get; private set; } public IO_Bus IoBus { get; private set; } public ULA Ula { get; private set; } public TapManager TapeDeck { get; private set; } // Audio (If BeeperDevice is in Desktop, you may need to move it to Core, // or inject it via an interface later. For now, assuming it's accessible). private readonly IAudioDevice _beeper; // State Flags private bool _isRunning = false; private bool _isPaused = false; private volatile bool _pendingReset = false; // Configuration public bool HighSpeedMode { get; set; } = false; public bool EnableFastLoad { get; set; } = true; public ushort? Breakpoint { get; set; } = null; // --- Diagnostic Stats for the Debugger --- public long TotalFrameCount { get; private set; } = 0; public double FramesPerSecond { get; private set; } = 0; public double FrameTime { get; private set; } = 0; // --- UI Communication Events --- // Form1 will listen to these to know when to redraw the screen or update the title bar public event Action? OnFrameReady; public event Action? OnStatsUpdated; // Passes FPS and TapeLoaded status public event Action? OnMachineCrashed; public SpectrumMachine(IAudioDevice beeperDevice) { _beeper = beeperDevice; Memory = new MemoryBus(); TapeDeck = new TapManager(); IoBus = new IO_Bus(TapeDeck); Ula = new ULA(Memory, IoBus); Memory.CrapRAMData(); // Note: You will need to pass the ROM bytes in from Form1, // since Core shouldn't be reading local desktop files directly. Cpu = new Z80(Memory, IoBus); Cpu.WaitStateCallback = Ula.GetContentionDelay; } public void LoadRom(byte[] romData) { Memory.LoadRom(romData); } public void Start() { if (_isRunning) return; _isRunning = true; _isPaused = false; Task.Run(EmulationLoop); } public void Pause() => _isPaused = true; public void Resume() => _isPaused = false; public void Reset() => _pendingReset = true; public void Stop() => _isRunning = false; private void EmulationLoop() { try { const int TStatesPerFrame = 69888; long nextScanlineTarget = Cpu.TotalTStates + TStatesPerFrame; var stopwatch = Stopwatch.StartNew(); var fpsStopwatch = Stopwatch.StartNew(); long scanlineCount = 0; long audioSampleCount = 0; double totalFrameTime = 0; bool wasHighSpeed = false; while (_isRunning) { if (_pendingReset) { Cpu.Reset(); Memory.CrapRAMData(); TotalFrameCount = 0; scanlineCount = 0; audioSampleCount = 0; nextScanlineTarget = TStatesPerFrame; stopwatch.Restart(); TapeDeck.Stop(); _pendingReset = false; } if (_isPaused) { stopwatch.Restart(); scanlineCount = 0; Thread.Sleep(10); continue; } if (Breakpoint.HasValue && Cpu.PC == Breakpoint.Value) { _isPaused = true; continue; } long tStatesBefore = Cpu.TotalTStates; // --- HARDWARE INTERCEPTS --- if (EnableFastLoad && Cpu.PC == 0x0556 && TapeDeck.HasBlocks) { HandleInstantTapeLoad(); Cpu.TotalTStates += 100; } Cpu.Step(); int elapsedTStates = (int)(Cpu.TotalTStates - tStatesBefore); TapeDeck.Update(elapsedTStates); if (HighSpeedMode) { wasHighSpeed = true; } else if (wasHighSpeed) { stopwatch.Restart(); fpsStopwatch.Restart(); scanlineCount = 0; audioSampleCount = (long)(Cpu.TotalTStates / 79.365); wasHighSpeed = false; } if (!HighSpeedMode) { while (Cpu.TotalTStates >= (long)(audioSampleCount * 79.365)) { bool finalAudioOutput = IoBus.BeeperState ^ TapeDeck.EarBit; _beeper.AddSample(finalAudioOutput); audioSampleCount++; } } if (Cpu.TotalTStates >= nextScanlineTarget) { if (!HighSpeedMode || (TotalFrameCount % 10 == 0)) { Ula.RenderScanline((int)scanlineCount % 312); } nextScanlineTarget += 224; scanlineCount++; if (scanlineCount % 312 == 0) { Cpu.RequestInterrupt(); Ula.CommitFrame(); TotalFrameCount++; // --- Fire the UI Events! --- if (!HighSpeedMode || (TotalFrameCount % 10 == 0)) { // Send the pixel array to whoever is listening (Form1) OnFrameReady?.Invoke(Ula.FrontBuffer); } if (!HighSpeedMode) { 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; bool hasTape = TapeDeck.HasBlocks; // Send the stats to whoever is listening (Form1) OnStatsUpdated?.Invoke(FramesPerSecond, hasTape); totalFrameTime = 0; } fpsStopwatch.Restart(); } } } } catch (Exception ex) { _isPaused = true; OnMachineCrashed?.Invoke(ex.Message); } } private void HandleInstantTapeLoad() { byte[] block = TapeDeck.GetNextBlock(); if (block == null) return; byte expectedFlag = Cpu.AF.High; if (block[0] != expectedFlag) { Cpu.AF.Low &= unchecked((byte)~0x01); ForceRet(); return; } int bytesToCopy = Cpu.DE.Word; int actualBytes = Math.Min(bytesToCopy, block.Length - 1); for (int i = 0; i < actualBytes; i++) { Memory.Write((ushort)(Cpu.IX.Word + i), block[i + 1]); } Cpu.IX.Word = (ushort)(Cpu.IX.Word + actualBytes); Cpu.DE.Word = (ushort)(bytesToCopy - actualBytes); Cpu.HL.Word = 0x0000; Cpu.AF.Word = 0x0041; ForceRet(); } private void ForceRet() { byte pcLow = Memory.Read(Cpu.SP); Cpu.SP++; byte pcHigh = Memory.Read(Cpu.SP); Cpu.SP++; Cpu.PC = (ushort)((pcHigh << 8) | pcLow); } // ==================================================================== // SNAPSHOT MANAGEMENT (.SNA) // ==================================================================== public void LoadSnapshot(byte[] snaData) { if (snaData.Length != 49179) throw new Exception("Invalid 48K SNA File Size!"); // 1. Load CPU Registers Cpu.I = snaData[0]; Cpu.HL_Prime.Word = (ushort)(snaData[1] | (snaData[2] << 8)); Cpu.DE_Prime.Word = (ushort)(snaData[3] | (snaData[4] << 8)); Cpu.BC_Prime.Word = (ushort)(snaData[5] | (snaData[6] << 8)); Cpu.AF_Prime.Word = (ushort)(snaData[7] | (snaData[8] << 8)); Cpu.HL.Word = (ushort)(snaData[9] | (snaData[10] << 8)); Cpu.DE.Word = (ushort)(snaData[11] | (snaData[12] << 8)); Cpu.BC.Word = (ushort)(snaData[13] | (snaData[14] << 8)); Cpu.IY.Word = (ushort)(snaData[15] | (snaData[16] << 8)); Cpu.IX.Word = (ushort)(snaData[17] | (snaData[18] << 8)); bool iff2 = (snaData[19] & 0x04) != 0; // Note: If IFF1/IFF2 have private setters in Z80.cs, you may need to // trigger an EI/DI instruction or temporarily unlock them. // Assuming they are public/internal for now based on previous code: Cpu.R = snaData[20]; Cpu.AF.Word = (ushort)(snaData[21] | (snaData[22] << 8)); Cpu.SP = (ushort)(snaData[23] | (snaData[24] << 8)); // Set Interrupt Mode (Assuming you add a setter to Z80.InterruptMode if it's private) // Cpu.InterruptMode = snaData[25]; // THE BUG FIX: Restore the ULA Border Color! IoBus.BorderColorIndex = snaData[26]; // 2. Load the 48K RAM Dump for (int i = 0; i < 49152; i++) { Memory.Write((ushort)(0x4000 + i), snaData[27 + i]); } // 3. Pop the Program Counter off the stack to resume byte pcLow = Memory.Read(Cpu.SP); Cpu.SP++; byte pcHigh = Memory.Read(Cpu.SP); Cpu.SP++; Cpu.PC = (ushort)((pcHigh << 8) | pcLow); } public void SaveSnapshot(string filepath) { // Back up the live state BEFORE modifying the stack ushort originalSP = Cpu.SP; byte originalMemLow = Memory.Read((ushort)(Cpu.SP - 2)); byte originalMemHigh = Memory.Read((ushort)(Cpu.SP - 1)); // Push the PC onto the stack Cpu.SP -= 2; Memory.Write(Cpu.SP, (byte)(Cpu.PC & 0xFF)); Memory.Write((ushort)(Cpu.SP + 1), (byte)(Cpu.PC >> 8)); using (System.IO.FileStream fs = new System.IO.FileStream(filepath, System.IO.FileMode.Create)) using (System.IO.BinaryWriter bw = new System.IO.BinaryWriter(fs)) { bw.Write(Cpu.I); bw.Write(Cpu.HL_Prime.Low); bw.Write(Cpu.HL_Prime.High); bw.Write(Cpu.DE_Prime.Low); bw.Write(Cpu.DE_Prime.High); bw.Write(Cpu.BC_Prime.Low); bw.Write(Cpu.BC_Prime.High); bw.Write(Cpu.AF_Prime.Low); bw.Write(Cpu.AF_Prime.High); bw.Write(Cpu.HL.Low); bw.Write(Cpu.HL.High); bw.Write(Cpu.DE.Low); bw.Write(Cpu.DE.High); bw.Write(Cpu.BC.Low); bw.Write(Cpu.BC.High); bw.Write(Cpu.IY.Low); bw.Write(Cpu.IY.High); bw.Write(Cpu.IX.Low); bw.Write(Cpu.IX.High); bw.Write((byte)(Cpu.IFF2 ? 0x04 : 0x00)); bw.Write(Cpu.R); bw.Write(Cpu.AF.Low); bw.Write(Cpu.AF.High); bw.Write((byte)(Cpu.SP & 0xFF)); bw.Write((byte)(Cpu.SP >> 8)); bw.Write((byte)Cpu.InterruptMode); bw.Write(IoBus.BorderColorIndex); for (int i = 0x4000; i <= 0xFFFF; i++) { bw.Write(Memory.Read((ushort)i)); } } // Restore the stack so the live game doesn't crash Cpu.SP = originalSP; Memory.Write((ushort)(originalSP - 2), originalMemLow); Memory.Write((ushort)(originalSP - 1), originalMemHigh); } } }