diff --git a/Core/Cpu/Z80.cs b/Core/Cpu/Z80.cs index 2bddbad..d2dc994 100644 --- a/Core/Cpu/Z80.cs +++ b/Core/Cpu/Z80.cs @@ -198,45 +198,6 @@ namespace Core.Cpu return tStates; } - public void LoadSNA(byte[] snaData) - { - if (snaData.Length != 49179) - throw new Exception("Invalid 48K SNA File Size!"); - - // --- 1. Load CPU Registers --- - I = snaData[0]; - HL_Prime.Word = (ushort)(snaData[1] | (snaData[2] << 8)); - DE_Prime.Word = (ushort)(snaData[3] | (snaData[4] << 8)); - BC_Prime.Word = (ushort)(snaData[5] | (snaData[6] << 8)); - AF_Prime.Word = (ushort)(snaData[7] | (snaData[8] << 8)); - - HL.Word = (ushort)(snaData[9] | (snaData[10] << 8)); - DE.Word = (ushort)(snaData[11] | (snaData[12] << 8)); - BC.Word = (ushort)(snaData[13] | (snaData[14] << 8)); - IY.Word = (ushort)(snaData[15] | (snaData[16] << 8)); - IX.Word = (ushort)(snaData[17] | (snaData[18] << 8)); - - IFF2 = (snaData[19] & 0x04) != 0; - IFF1 = IFF2; - R = snaData[20]; - AF.Word = (ushort)(snaData[21] | (snaData[22] << 8)); - SP = (ushort)(snaData[23] | (snaData[24] << 8)); - InterruptMode = snaData[25]; - - // --- 2. Load the 48K RAM Dump --- - // The RAM dump starts at byte 27 and maps perfectly to 0x4000 -> 0xFFFF - for (int i = 0; i < 49152; i++) - { - WriteMemory((ushort)(0x4000 + i), snaData[27 + i]); - } - - // --- 3. The Magic Bullet --- - // In the SNA format, the Program Counter (PC) isn't in the header. - // It was PUSHED to the stack exactly 1 instruction before the snapshot was saved. - // So, we just pop it off the stack to resume execution! - PC = Pop(); - } - public string GetFlagsString() diff --git a/Core/Interfaces/IAudioDevice.cs b/Core/Interfaces/IAudioDevice.cs new file mode 100644 index 0000000..8604841 --- /dev/null +++ b/Core/Interfaces/IAudioDevice.cs @@ -0,0 +1,7 @@ +namespace Core.Interfaces +{ + public interface IAudioDevice + { + void AddSample(bool isHigh); + } +} \ No newline at end of file diff --git a/Core/Io/IO_Bus.cs b/Core/Io/IO_Bus.cs index 9c9f2b1..58d2e99 100644 --- a/Core/Io/IO_Bus.cs +++ b/Core/Io/IO_Bus.cs @@ -6,7 +6,7 @@ namespace Core.Io { public class IO_Bus { - public byte BorderColorIndex { get; private set; } = 7; + public byte BorderColorIndex { get; set; } = 7; public bool BeeperState { get; private set; } = false; public byte[] KeyboardRows = new byte[8] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; TapManager _tapManager = new TapManager(); diff --git a/Core/SpectrumMachine.cs b/Core/SpectrumMachine.cs new file mode 100644 index 0000000..8f8264a --- /dev/null +++ b/Core/SpectrumMachine.cs @@ -0,0 +1,357 @@ +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); + } + } +} \ No newline at end of file diff --git a/Desktop/BeeperDevice.cs b/Desktop/BeeperDevice.cs index 394e271..3e431a5 100644 --- a/Desktop/BeeperDevice.cs +++ b/Desktop/BeeperDevice.cs @@ -1,9 +1,11 @@ using NAudio.Wave; +using Core.Interfaces; using System; namespace Desktop { - public class BeeperDevice + + public class BeeperDevice : IAudioDevice { private WaveOutEvent _waveOut; private BufferedWaveProvider _buffer; diff --git a/Desktop/Form1.cs b/Desktop/Form1.cs index d3e6613..44522f4 100644 --- a/Desktop/Form1.cs +++ b/Desktop/Form1.cs @@ -1,39 +1,38 @@ -using Core.Cpu; +using Core; // <-- This gives us access to SpectrumMachine using Core.Io; -using Core.Memory; using System.Diagnostics; using System.Drawing.Imaging; -using System.IO; using System.Reflection; using System.Runtime.InteropServices; -using System.Threading; namespace Desktop { public partial class Form1 : Form { - private Z80 _cpu = null!; - private MemoryBus _memoryBus = null!; - private IO_Bus _simpleIoBus = null!; - private ULA _ula = null!; - private TapManager _tapManager = null!; - private DebuggerForm? _debugger = null; + // 1. Our new, clean Engine instance + private SpectrumMachine _machine = null!; private BeeperDevice _beeper = null!; + private DebuggerForm? _debugger = null; + private Bitmap _screenBitmap = null!; private string _baseTitle = ""; - private bool _isRunning = false; - private bool _isPaused = false; - public ushort? Breakpoint = null; - public long TotalFrameCount = 0; - public double FramesPerSecond = 0; - public double TotalFrameTime = 0; - public double FrameTime = 0; - public bool highSpeed = false; - public bool tapeLoaded = false; - public bool tapePlaying = false; - private volatile bool _pendingReset = false; - private bool _enableFastLoad = true; - // Comment to push a new commit 2 + + // ==================================================================== + // DEBUGGER PASS-THROUGHS + // ==================================================================== + // The DebuggerForm still looks at Form1 for data. These properties act + // as a bridge, instantly passing the request down to the actual engine! + + public ushort? Breakpoint + { + get => _machine?.Breakpoint; + set { if (_machine != null) _machine.Breakpoint = value; } + } + + public long TotalFrameCount => _machine?.TotalFrameCount ?? 0; + public double FramesPerSecond => _machine?.FramesPerSecond ?? 0; + public double FrameTime => _machine?.FrameTime ?? 0; + private bool tapePlaying = false; public Form1() { @@ -41,6 +40,7 @@ namespace Desktop PopulateIncludedTapsMenu(); this.DoubleBuffered = true; this.ResizeRedraw = true; + InitializeEmulator(); } @@ -49,519 +49,114 @@ namespace Desktop try { _baseTitle = this.Text; - _memoryBus = new MemoryBus(); - _tapManager = new TapManager(); - _simpleIoBus = new IO_Bus(_tapManager); - _ula = new ULA(_memoryBus, _simpleIoBus); + + // Initialize the physical audio device first _beeper = new BeeperDevice(); - _memoryBus.CrapRAMData(); + + // Spin up the entire Spectrum inside the Core! + _machine = new SpectrumMachine(_beeper); + + // --- WIRE UP THE UI EVENTS --- + // Tell the machine: "Whenever a frame is done, hand it to my UpdateScreen method!" + _machine.OnFrameReady += Machine_OnFrameReady; + _machine.OnStatsUpdated += Machine_OnStatsUpdated; + _machine.OnMachineCrashed += Machine_OnMachineCrashed; + + // Sync the UI checkmarks with the Machine's default settings + fastLoadToolStripMenuItem.Checked = _machine.EnableFastLoad; + HighSpeedToolStripMenuItem.Checked = _machine.HighSpeedMode; + + // Load the ROM and start the engine byte[] romData = RomLoader.Load("48.rom"); - _memoryBus.LoadRom(romData); - _cpu = new Z80(_memoryBus, _simpleIoBus); - _cpu.WaitStateCallback = _ula.GetContentionDelay; - fastLoadToolStripMenuItem.Checked = _enableFastLoad; - + _machine.LoadRom(romData); + _machine.Start(); } catch (Exception ex) { MessageBox.Show($"Failed to initialize emulator:\n{ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } - StartEmulationLoop(); } - private void StartEmulationLoop() + // ==================================================================== + // EVENT HANDLERS (How the Machine talks to the UI) + // ==================================================================== + + private void Machine_OnFrameReady(int[] pixels) { - if (_isRunning) return; - _isRunning = true; - _isPaused = false; - bool wasHighSpeed = false; - Task.Run(() => + // We must use BeginInvoke because the Machine is running on a background thread! + this.BeginInvoke((System.Windows.Forms.MethodInvoker)delegate { - try - { - const int TStatesPerFrame = 69888; - long nextScanlineTarget = _cpu.TotalTStates + TStatesPerFrame; - var stopwatch = Stopwatch.StartNew(); - var fpsStopwatch = Stopwatch.StartNew(); - long scanlineCount = 0; - long audioSampleCount = 0; - - while (_isRunning) - { - if (_pendingReset) - { - _cpu.Reset(); - _memoryBus.CrapRAMData(); // Important! If RAM has garbage, the ROM boots instantly. If it has old data, it fails the RAM check! - - // Reset all local loop timing variables - TotalFrameCount = 0; - scanlineCount = 0; - audioSampleCount = 0; - nextScanlineTarget = TStatesPerFrame; - stopwatch.Restart(); - tapeLoaded = false; - _pendingReset = false; - } - - if (_isPaused) - { - stopwatch.Restart(); - scanlineCount = 0; - Thread.Sleep(10); - continue; - } - - // --- Breakpoint Check --- - if (Breakpoint.HasValue && _cpu.PC == Breakpoint.Value) - { - _isPaused = true; - continue; - } - - long tStatesBefore = _cpu.TotalTStates; - - // --- HARDWARE INTERCEPTS --- - if (_enableFastLoad && _cpu.PC == 0x0556 && _tapManager.HasBlocks) - { - HandleInstantTapeLoad(); - _cpu.TotalTStates += 100; - } - - // --- Execute Instruction --- - _cpu.Step(); - - int elapsedTStates = (int)(_cpu.TotalTStates - tStatesBefore); - _tapManager.Update(elapsedTStates); - - if (highSpeed) - { - wasHighSpeed = true; - } - else if (wasHighSpeed) - { - stopwatch.Restart(); - fpsStopwatch.Restart(); - scanlineCount = 0; - audioSampleCount = (long)(_cpu.TotalTStates / 79.365); // Snap audio to the current T-State time - wasHighSpeed = false; - } - - //Process audio at the correct time - if (!highSpeed) - { - while (_cpu.TotalTStates >= (long)(audioSampleCount * 79.365)) - { - bool finalAudioOutput = _simpleIoBus.BeeperState ^ _tapManager.EarBit; - _beeper.AddSample(finalAudioOutput); - audioSampleCount++; - } - } - - // --- Check for End of Frame --- - if (_cpu.TotalTStates >= nextScanlineTarget) - { - // Tell the ULA to draw one line of pixels - if (!highSpeed || (TotalFrameCount % 10 == 0)) - { - _ula.RenderScanline((int)scanlineCount % 312); - } - - nextScanlineTarget += 224; // Advance target by ONE line (224 T-States) - scanlineCount++; - - // Hit the bottom of the screen (Line 312)? - if (scanlineCount % 312 == 0) - { - _cpu.RequestInterrupt(); // 50Hz interrupt - _ula.CommitFrame(); - TotalFrameCount++; - - if (highSpeed) - { - if (TotalFrameCount % 10 == 0) - { - this.BeginInvoke((System.Windows.Forms.MethodInvoker)delegate - { - UpdateScreenBitmap(); // Your existing method that writes the ULA data to _screenBitmap - - this.Invalidate(); // Tells Windows: "The bitmap changed, please run OnPaint()!" - - this.Text = $"{_baseTitle} - FPS: {FramesPerSecond:F1} - Tape Loaded: {tapeLoaded.ToString()}"; - }); - } - - } - else - { - this.BeginInvoke((System.Windows.Forms.MethodInvoker)delegate - { - UpdateScreenBitmap(); // Your existing method that writes the ULA data to _screenBitmap - - this.Invalidate(); // Tells Windows: "The bitmap changed, please run OnPaint()!" - - this.Text = $"{_baseTitle} - FPS: {FramesPerSecond:F1} - Tape Loaded: {tapeLoaded.ToString()}"; - }); - } - - - // Throttle to real-time (50 FPS = 20ms) - if (!highSpeed) - { - 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(); - } - } - } - } - catch (Exception ex) - { - _isPaused = true; - this.Invoke((System.Windows.Forms.MethodInvoker)delegate - { - MessageBox.Show(ex.Message, "CPU Crash", MessageBoxButtons.OK, MessageBoxIcon.Error); - - }); - } + UpdateScreenBitmap(pixels); + this.Invalidate(); // Triggers OnPaint }); } - private void SaveSNAMenuItem_Click(object? sender, EventArgs e) + private void Machine_OnStatsUpdated(double fps, bool hasTape) { - _isPaused = true; - using (SaveFileDialog sfd = new SaveFileDialog()) + this.BeginInvoke((System.Windows.Forms.MethodInvoker)delegate { - sfd.Filter = "Spectrum SNA Files|*.sna"; - if (sfd.ShowDialog() == DialogResult.OK) - { - SaveSNA(sfd.FileName); - } - } - _isPaused = false; + this.Text = $"{_baseTitle} - FPS: {fps:F1} - Tape Loaded: {hasTape}"; + }); } - public void SaveSNA(string filepath) + private void Machine_OnMachineCrashed(string errorMessage) { - // 0. Back up the live state BEFORE modifying it - ushort originalSP = _cpu.SP; - byte originalMemLow = _memoryBus.Read((ushort)(_cpu.SP - 2)); - byte originalMemHigh = _memoryBus.Read((ushort)(_cpu.SP - 1)); - - // 1. We must push the PC onto the stack before saving! - _cpu.SP -= 2; - _memoryBus.Write(_cpu.SP, (byte)(_cpu.PC & 0xFF)); // Low byte - _memoryBus.Write((ushort)(_cpu.SP + 1), (byte)(_cpu.PC >> 8)); // High byte - - using (FileStream fs = new FileStream(filepath, FileMode.Create)) - using (BinaryWriter bw = new BinaryWriter(fs)) + this.BeginInvoke((System.Windows.Forms.MethodInvoker)delegate { - // 2. Write the 27-byte Header - 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); - - // IFF2 determines the interrupt state in SNA - 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(_simpleIoBus.BorderColorIndex); - - // 3. Dump the 48K RAM - for (int i = 0x4000; i <= 0xFFFF; i++) - { - bw.Write(_memoryBus.Read((ushort)i)); - } - } - - // 4. RESTORE the emulator's state so the game can resume safely - _cpu.SP = originalSP; - _memoryBus.Write((ushort)(originalSP - 2), originalMemLow); - _memoryBus.Write((ushort)(originalSP - 1), originalMemHigh); + MessageBox.Show(errorMessage, "CPU Crash", MessageBoxButtons.OK, MessageBoxIcon.Error); + }); } - private void PopulateIncludedTapsMenu() + // ==================================================================== + // RENDERING + // ==================================================================== + + private void UpdateScreenBitmap(int[] pixels) { - Assembly assembly = Assembly.GetExecutingAssembly(); - string[] resourceNames = assembly.GetManifestResourceNames(); - - foreach (string resourceName in resourceNames) - { - if (resourceName.Contains("Desktop.ROMS.TAP.") && resourceName.EndsWith(".TAP", StringComparison.OrdinalIgnoreCase)) - { - - string[] parts = resourceName.Split('.'); - string displayName = parts[parts.Length - 2]; - ToolStripMenuItem item = new ToolStripMenuItem(displayName); - - item.Tag = resourceName; - - item.Click += IncludedTapMenuItem_Click; - - tAPToolStripMenuItem1.DropDownItems.Add(item); - } - } - } - - private void fastLoadToolStripMenuItem_Click(object sender, EventArgs e) - { - this.fastLoadToolStripMenuItem.Checked = !this.fastLoadToolStripMenuItem.Checked; - - _enableFastLoad = this.fastLoadToolStripMenuItem.Checked; - } - - private void IncludedTapMenuItem_Click(object? sender, EventArgs e) - { - if (sender is ToolStripMenuItem item && item.Tag is string resourceName) - { - _isPaused = true; - - try - { - Assembly assembly = Assembly.GetExecutingAssembly(); - - // Open a stream directly into the binary file - using (Stream? stream = assembly.GetManifestResourceStream(resourceName)) - { - if (stream == null) throw new Exception("Could not find embedded resource."); - - // Copy the binary stream into a byte array - using (MemoryStream ms = new MemoryStream()) - { - stream.CopyTo(ms); - byte[] tapBytes = ms.ToArray(); - - // Feed it directly to your existing TapManager! - _tapManager.LoadTapData(tapBytes); - tapeLoaded = true; - //_tapManager.Play(); - } - } - } - catch (Exception ex) - { - MessageBox.Show($"Failed to load built-in TAP:\n{ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - } - finally - { - _isPaused = false; - } - } - } - - protected override void OnPaint(PaintEventArgs e) - { - base.OnPaint(e); - - // If we don't have a screen bitmap yet, just paint the background black and exit - if (_screenBitmap == null) - { - e.Graphics.Clear(Color.Black); - return; - } - - // 1. Calculate the scaling factor to fit the window while preserving aspect ratio - float scaleX = (float)this.ClientSize.Width / _screenBitmap.Width; - float scaleY = (float)this.ClientSize.Height / _screenBitmap.Height; - float scale = Math.Min(scaleX, scaleY); // Pick the smallest scale so it fits inside - - int newWidth = (int)(_screenBitmap.Width * scale); - int newHeight = (int)(_screenBitmap.Height * scale); - - // 2. Center the image horizontally and vertically (Pillarboxing/Letterboxing) - int posX = (this.ClientSize.Width - newWidth) / 2; - int posY = (this.ClientSize.Height - newHeight) / 2; - - // 3. RETRO MAGIC: Force Nearest-Neighbor interpolation for razor-sharp pixels! - e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor; - - // PixelOffsetMode.Half is a GDI+ trick required to make NearestNeighbor perfectly accurate - e.Graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.Half; - - // 4. Paint the black borders (if any) and draw the screen - e.Graphics.Clear(Color.Black); - e.Graphics.DrawImage(_screenBitmap, posX, posY, newWidth, newHeight); - } - - private void HandleInstantTapeLoad() - { - byte[] block = _tapManager.GetNextBlock(); // Your original Queue.Dequeue() method - if (block == null) return; - - byte expectedFlag = _cpu.AF.High; - if (block[0] != expectedFlag) - { - // Block mismatch (e.g. found data when looking for a header) - // Clear the carry flag to simulate a tape loading error - _cpu.AF.Low &= unchecked((byte)~0x01); - ForceRet(); - return; - } - - int bytesToCopy = _cpu.DE.Word; - - // Safety check just in case the TAP file is malformed - int actualBytes = Math.Min(bytesToCopy, block.Length - 1); - - // Directly inject the payload into the RAM - for (int i = 0; i < actualBytes; i++) - { - _memoryBus.Write((ushort)(_cpu.IX.Word + i), block[i + 1]); - } - - // --- Update Registers to match a PERFECT ROM Load --- - _cpu.IX.Word = (ushort)(_cpu.IX.Word + actualBytes); - _cpu.DE.Word = (ushort)(bytesToCopy - actualBytes); // Should hit 0 - _cpu.HL.Word = 0x0000; // The ROM zeroes this out after calculating checksums - - // Checksum Zero (A = 0x00), Success Flag / Zero Flag Set (F = 0x41) - _cpu.AF.Word = 0x0041; - - ForceRet(); - } - - private void ForceRet() - { - // Pop the return address off the stack just like a real RET instruction - byte pcLow = _memoryBus.Read(_cpu.SP); - _cpu.SP++; - byte pcHigh = _memoryBus.Read(_cpu.SP); - _cpu.SP++; - _cpu.PC = (ushort)((pcHigh << 8) | pcLow); - } - - - 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.FrontBuffer, 0, bmpData.Scan0, _ula.FrontBuffer.Length); + Marshal.Copy(pixels, 0, bmpData.Scan0, pixels.Length); bmp.UnlockBits(bmpData); - // Dispose of the old frame to prevent massive RAM leaks! - if (_screenBitmap != null) - { - _screenBitmap.Dispose(); - } - - // Save the new frame so OnPaint() can draw it + if (_screenBitmap != null) _screenBitmap.Dispose(); _screenBitmap = bmp; } - private void loadTAPToolStripMenuItem_Click(object sender, EventArgs e) + protected override void OnPaint(PaintEventArgs e) { - _isPaused = true; - using (OpenFileDialog ofd = new OpenFileDialog()) + base.OnPaint(e); + + if (_screenBitmap == null) { - ofd.Filter = "Spectrum TAP Files|*.tap"; - if (ofd.ShowDialog() == DialogResult.OK) - { - byte[] tapBytes = File.ReadAllBytes(ofd.FileName); - _tapManager.LoadTapData(tapBytes); - tapeLoaded = true; - //_tapManager.Play(); - } - } - _isPaused = false; - } - - - private void openSNAToolStripMenuItem_Click(object sender, EventArgs e) - { - _isPaused = true; - using (OpenFileDialog ofd = new OpenFileDialog()) - { - ofd.Filter = "Snapshot Files (sna,z80)|*.sna"; - if (ofd.ShowDialog() == DialogResult.OK) - { - byte[] snaBytes = File.ReadAllBytes(ofd.FileName); - _cpu.LoadSNA(snaBytes); - } - } - _isPaused = false; - } - - private void btnHighSpeedToggle_Click(object sender, EventArgs e) - { - this.HighSpeedToolStripMenuItem.Checked = !this.HighSpeedToolStripMenuItem.Checked; - highSpeed = this.HighSpeedToolStripMenuItem.Checked ? true : false; - } - private void btnRun_Click(object sender, EventArgs e) => _isPaused = false; - - private void btnPause_Click(object sender, EventArgs e) => _isPaused = true; - - private void btnStep_Click(object sender, EventArgs e) - { - if (_isPaused) _cpu.Step(); - } - - private void btnReset_Click(object sender, EventArgs e) - { - _pendingReset = true; - } - - private void btnExit_Click(object sender, EventArgs e) - { - Environment.Exit(0); - } - - private void openDebuggerToolStripMenuItem_Click(object sender, EventArgs e) - { - if (_debugger == null || _debugger.IsDisposed) - { - _debugger = new DebuggerForm(_cpu, _memoryBus, this); - _debugger.Show(); - } - else - { - _debugger.BringToFront(); - } - } - - private void UpdateMatrix(int row, int col, bool isPressed) - { - if (isPressed) - { - // Clear the bit to 0 (Active-Low = Pressed) - _simpleIoBus.KeyboardRows[row] &= (byte)~(1 << col); - } - else - { - // Set the bit back to 1 (Unpressed) - _simpleIoBus.KeyboardRows[row] |= (byte)(1 << col); + e.Graphics.Clear(Color.Black); + return; } + + float scaleX = (float)this.ClientSize.Width / _screenBitmap.Width; + float scaleY = (float)this.ClientSize.Height / _screenBitmap.Height; + float scale = Math.Min(scaleX, scaleY); + + int newWidth = (int)(_screenBitmap.Width * scale); + int newHeight = (int)(_screenBitmap.Height * scale); + + int posX = (this.ClientSize.Width - newWidth) / 2; + int posY = (this.ClientSize.Height - newHeight) / 2; + + e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor; + e.Graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.Half; + + e.Graphics.Clear(Color.Black); + e.Graphics.DrawImage(_screenBitmap, posX, posY, newWidth, newHeight); } + // ==================================================================== + // USER INPUT (Keyboard) + // ==================================================================== protected override void OnKeyDown(KeyEventArgs e) { @@ -569,7 +164,6 @@ namespace Desktop base.OnKeyDown(e); } - protected override void OnKeyUp(KeyEventArgs e) { HandleKey(e.KeyCode, false); @@ -580,79 +174,216 @@ namespace Desktop { switch (key) { - //Row 0: case Keys.ShiftKey: UpdateMatrix(0, 0, isPressed); break; case Keys.Z: UpdateMatrix(0, 1, isPressed); break; case Keys.X: UpdateMatrix(0, 2, isPressed); break; case Keys.C: UpdateMatrix(0, 3, isPressed); break; case Keys.V: UpdateMatrix(0, 4, isPressed); break; - // Row 1 case Keys.A: UpdateMatrix(1, 0, isPressed); break; case Keys.S: UpdateMatrix(1, 1, isPressed); break; case Keys.D: UpdateMatrix(1, 2, isPressed); break; case Keys.F: UpdateMatrix(1, 3, isPressed); break; case Keys.G: UpdateMatrix(1, 4, isPressed); break; - //Row 2: case Keys.Q: UpdateMatrix(2, 0, isPressed); break; case Keys.W: UpdateMatrix(2, 1, isPressed); break; case Keys.E: UpdateMatrix(2, 2, isPressed); break; case Keys.R: UpdateMatrix(2, 3, isPressed); break; case Keys.T: UpdateMatrix(2, 4, isPressed); break; - // Row 3: case Keys.D1: UpdateMatrix(3, 0, isPressed); break; case Keys.D2: UpdateMatrix(3, 1, isPressed); break; case Keys.D3: UpdateMatrix(3, 2, isPressed); break; case Keys.D4: UpdateMatrix(3, 3, isPressed); break; case Keys.D5: UpdateMatrix(3, 4, isPressed); break; - // Row 4: case Keys.D0: UpdateMatrix(4, 0, isPressed); break; case Keys.D9: UpdateMatrix(4, 1, isPressed); break; case Keys.D8: UpdateMatrix(4, 2, isPressed); break; case Keys.D7: UpdateMatrix(4, 3, isPressed); break; case Keys.D6: UpdateMatrix(4, 4, isPressed); break; - // Row 5: case Keys.P: UpdateMatrix(5, 0, isPressed); break; case Keys.O: UpdateMatrix(5, 1, isPressed); break; case Keys.I: UpdateMatrix(5, 2, isPressed); break; case Keys.U: UpdateMatrix(5, 3, isPressed); break; case Keys.Y: UpdateMatrix(5, 4, isPressed); break; - // Row 6 case Keys.Enter: UpdateMatrix(6, 0, isPressed); break; 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; - // Row 7 case Keys.Space: UpdateMatrix(7, 0, isPressed); break; - case Keys.ControlKey: UpdateMatrix(7, 1, isPressed); break; // Symbol Shift + case Keys.ControlKey: UpdateMatrix(7, 1, isPressed); break; case Keys.M: UpdateMatrix(7, 2, isPressed); break; case Keys.N: UpdateMatrix(7, 3, isPressed); break; case Keys.B: UpdateMatrix(7, 4, isPressed); break; } } + private void UpdateMatrix(int row, int col, bool isPressed) + { + // Talk directly to the machine's IO Bus! + if (isPressed) + _machine.IoBus.KeyboardRows[row] &= (byte)~(1 << col); + else + _machine.IoBus.KeyboardRows[row] |= (byte)(1 << col); + } + + // ==================================================================== + // MENU CONTROLS + // ==================================================================== + + private void btnRun_Click(object sender, EventArgs e) => _machine.Resume(); + private void btnPause_Click(object sender, EventArgs e) => _machine.Pause(); + private void btnReset_Click(object sender, EventArgs e) => _machine.Reset(); + private void btnExit_Click(object sender, EventArgs e) => Environment.Exit(0); + + private void btnStep_Click(object sender, EventArgs e) + { + // Only allow stepping if we know the main loop is paused + _machine.Cpu.Step(); + } + + private void btnHighSpeedToggle_Click(object sender, EventArgs e) + { + HighSpeedToolStripMenuItem.Checked = !HighSpeedToolStripMenuItem.Checked; + _machine.HighSpeedMode = HighSpeedToolStripMenuItem.Checked; + } + + private void fastLoadToolStripMenuItem_Click(object sender, EventArgs e) + { + fastLoadToolStripMenuItem.Checked = !fastLoadToolStripMenuItem.Checked; + _machine.EnableFastLoad = fastLoadToolStripMenuItem.Checked; + } + private void playTapeToolStripMenuItem_Click(object sender, EventArgs e) { if (playTapeToolStripMenuItem.Text == "Play Tape") { playTapeToolStripMenuItem.Text = "Stop Tape"; - _tapManager.Play(); + _machine.TapeDeck.Play(); tapePlaying = true; } else { playTapeToolStripMenuItem.Text = "Play Tape"; - _tapManager.Stop(); + _machine.TapeDeck.Stop(); tapePlaying = false; } - } + + private void loadTAPToolStripMenuItem_Click(object sender, EventArgs e) + { + _machine.Pause(); + using (OpenFileDialog ofd = new OpenFileDialog()) + { + ofd.Filter = "Spectrum TAP Files|*.tap"; + if (ofd.ShowDialog() == DialogResult.OK) + { + byte[] tapBytes = File.ReadAllBytes(ofd.FileName); + _machine.TapeDeck.LoadTapData(tapBytes); + } + } + _machine.Resume(); + } + + private void PopulateIncludedTapsMenu() + { + Assembly assembly = Assembly.GetExecutingAssembly(); + string[] resourceNames = assembly.GetManifestResourceNames(); + + foreach (string resourceName in resourceNames) + { + if (resourceName.Contains("Desktop.ROMS.TAP.") && resourceName.EndsWith(".TAP", StringComparison.OrdinalIgnoreCase)) + { + string[] parts = resourceName.Split('.'); + string displayName = parts[parts.Length - 2]; + ToolStripMenuItem item = new ToolStripMenuItem(displayName) { Tag = resourceName }; + item.Click += IncludedTapMenuItem_Click; + tAPToolStripMenuItem1.DropDownItems.Add(item); + } + } + } + + private void IncludedTapMenuItem_Click(object? sender, EventArgs e) + { + if (sender is ToolStripMenuItem item && item.Tag is string resourceName) + { + _machine.Pause(); + try + { + Assembly assembly = Assembly.GetExecutingAssembly(); + using (Stream? stream = assembly.GetManifestResourceStream(resourceName)) + { + if (stream == null) throw new Exception("Could not find embedded resource."); + using (MemoryStream ms = new MemoryStream()) + { + stream.CopyTo(ms); + _machine.TapeDeck.LoadTapData(ms.ToArray()); + } + } + } + catch (Exception ex) + { + MessageBox.Show($"Failed to load built-in TAP:\n{ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + _machine.Resume(); + } + } + } + + private void openDebuggerToolStripMenuItem_Click(object sender, EventArgs e) + { + if (_debugger == null || _debugger.IsDisposed) + { + _debugger = new DebuggerForm(_machine.Cpu, _machine.Memory, this); + _debugger.Show(); + } + else + { + _debugger.BringToFront(); + } + } + + // ==================================================================== + // SNAPSHOTS (Temporarily patched to use _machine hardware) + // ==================================================================== + + private void openSNAToolStripMenuItem_Click(object sender, EventArgs e) + { + _machine.Pause(); + using (OpenFileDialog ofd = new OpenFileDialog()) + { + ofd.Filter = "Snapshot Files (sna,z80)|*.sna"; + if (ofd.ShowDialog() == DialogResult.OK) + { + byte[] snaBytes = File.ReadAllBytes(ofd.FileName); + _machine.LoadSnapshot(snaBytes); + } + } + _machine.Resume(); + } + + private void SaveSNAMenuItem_Click(object? sender, EventArgs e) + { + _machine.Pause(); + using (SaveFileDialog sfd = new SaveFileDialog()) + { + sfd.Filter = "Spectrum SNA Files|*.sna"; + if (sfd.ShowDialog() == DialogResult.OK) + { + _machine.SaveSnapshot(sfd.FileName); + } + } + _machine.Resume(); + } + + } } \ No newline at end of file