using Core; using Core.Io; using System.Drawing.Imaging; using Vortice.XInput; using System.Reflection; using System.Runtime.InteropServices; namespace Desktop { public partial class Form1 : Form { // 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 = ""; 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() { InitializeComponent(); this.DoubleBuffered = true; this.ResizeRedraw = true; } protected override void OnLoad(EventArgs e) { base.OnLoad(e); // Double-check we aren't in the designer just to be paranoid if (!this.DesignMode) { PopulateIncludedTapsMenu(); InitializeEmulator(); } } private void InitializeEmulator() { try { _baseTitle = this.Text; // Initialize the physical audio device first _beeper = new BeeperDevice(); // 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; _machine.LoadRom(RomLoader.Load("plus3-0.rom"), 0); _machine.LoadRom(RomLoader.Load("plus3-1.rom"), 1); _machine.LoadRom(RomLoader.Load("plus3-2.rom"), 2); _machine.LoadRom(RomLoader.Load("plus3-3.rom"), 3); _machine.LoadRom(RomLoader.Load("48.rom"), 4); _machine.Start(); } catch (Exception ex) { MessageBox.Show($"Failed to initialize emulator:\n{ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } // ==================================================================== // EVENT HANDLERS (How the Machine talks to the UI) // ==================================================================== private void Machine_OnFrameReady(int[] pixels) { PollGamepad(); this.BeginInvoke((System.Windows.Forms.MethodInvoker)delegate { UpdateScreenBitmap(pixels); this.Invalidate(); }); } private void Machine_OnStatsUpdated(double fps, bool hasTape) { this.BeginInvoke((System.Windows.Forms.MethodInvoker)delegate { this.Text = $"{_baseTitle} - FPS: {fps:F1} - Tape Loaded: {hasTape}"; }); } private void Machine_OnMachineCrashed(string errorMessage) { this.BeginInvoke((System.Windows.Forms.MethodInvoker)delegate { MessageBox.Show(errorMessage, "CPU Crash", MessageBoxButtons.OK, MessageBoxIcon.Error); }); } // ==================================================================== // RENDERING // ==================================================================== private void UpdateScreenBitmap(int[] pixels) { 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); Marshal.Copy(pixels, 0, bmpData.Scan0, pixels.Length); bmp.UnlockBits(bmpData); if (_screenBitmap != null) _screenBitmap.Dispose(); _screenBitmap = bmp; } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); if (_screenBitmap == null) { 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) { HandleKey(e.KeyCode, true); base.OnKeyDown(e); } protected override void OnKeyUp(KeyEventArgs e) { HandleKey(e.KeyCode, false); base.OnKeyUp(e); } private void HandleKey(Keys key, bool isPressed) { switch (key) { 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; 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; 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; 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; 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; 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; 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; case Keys.Space: UpdateMatrix(7, 0, isPressed); break; 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; // --- EXTENDED +2A KEYS (Macros) --- case Keys.Left: UpdateMatrix(0, 0, isPressed); // CAPS SHIFT UpdateMatrix(3, 4, isPressed); // 5 break; case Keys.Down: UpdateMatrix(0, 0, isPressed); // CAPS SHIFT UpdateMatrix(4, 4, isPressed); // 6 break; case Keys.Up: UpdateMatrix(0, 0, isPressed); // CAPS SHIFT UpdateMatrix(4, 3, isPressed); // 7 break; case Keys.Right: UpdateMatrix(0, 0, isPressed); // CAPS SHIFT UpdateMatrix(4, 2, isPressed); // 8 break; case Keys.Back: UpdateMatrix(0, 0, isPressed); // CAPS SHIFT UpdateMatrix(4, 0, isPressed); // 0 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); } private void spectrum48KToolStripMenuItem_Click(object sender, EventArgs e) { spectrum48KToolStripMenuItem.Checked = true; spectrum128KPlus2AToolStripMenuItem.Checked = false; _machine.SetMachineModel(MachineModel.Spectrum48K); } private void spectrum128KPlus2AToolStripMenuItem_Click(object sender, EventArgs e) { spectrum48KToolStripMenuItem.Checked = false; spectrum128KPlus2AToolStripMenuItem.Checked = true; _machine.SetMachineModel(MachineModel.SpectrumPlus2A); } 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"; _machine.TapeDeck.Play(); tapePlaying = true; } else { playTapeToolStripMenuItem.Text = "Play Tape"; _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(); } //Control pad method private void PollGamepad() { // Only check Player 1 (Index 0) if (XInput.GetState(0, out State state)) { byte kempston = 0x00; Gamepad gamepad = state.Gamepad; // Map D-Pad to Kempston Bits if ((gamepad.Buttons & GamepadButtons.DPadRight) != 0) kempston |= 0x01; // Bit 0 if ((gamepad.Buttons & GamepadButtons.DPadLeft) != 0) kempston |= 0x02; // Bit 1 if ((gamepad.Buttons & GamepadButtons.DPadDown) != 0) kempston |= 0x04; // Bit 2 if ((gamepad.Buttons & GamepadButtons.DPadUp) != 0) kempston |= 0x08; // Bit 3 // Map A Button (or whichever you prefer) to Fire if ((gamepad.Buttons & GamepadButtons.A) != 0) kempston |= 0x10; // Bit 4 // Send the final byte down to the emulator core! _machine.IoBus.KempstonState = kempston; } else { // Controller disconnected, zero it out _machine.IoBus.KempstonState = 0x00; } } } }