using Core.Cpu; 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; private BeeperDevice _beeper = 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 public Form1() { InitializeComponent(); PopulateIncludedTapsMenu(); this.DoubleBuffered = true; this.ResizeRedraw = true; InitializeEmulator(); } private void InitializeEmulator() { try { _baseTitle = this.Text; _memoryBus = new MemoryBus(); _tapManager = new TapManager(); _simpleIoBus = new IO_Bus(_tapManager); _ula = new ULA(_memoryBus, _simpleIoBus); _beeper = new BeeperDevice(); _memoryBus.CrapRAMData(); byte[] romData = RomLoader.Load("48.rom"); _memoryBus.LoadRom(romData); _cpu = new Z80(_memoryBus, _simpleIoBus); _cpu.WaitStateCallback = _ula.GetContentionDelay; fastLoadToolStripMenuItem.Checked = _enableFastLoad; } catch (Exception ex) { MessageBox.Show($"Failed to initialize emulator:\n{ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } StartEmulationLoop(); } private void StartEmulationLoop() { if (_isRunning) return; _isRunning = true; _isPaused = false; bool wasHighSpeed = false; Task.Run(() => { 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); }); } }); } private void SaveSNAMenuItem_Click(object? sender, EventArgs e) { _isPaused = true; using (SaveFileDialog sfd = new SaveFileDialog()) { sfd.Filter = "Spectrum SNA Files|*.sna"; if (sfd.ShowDialog() == DialogResult.OK) { SaveSNA(sfd.FileName); } } _isPaused = false; } public void SaveSNA(string filepath) { // 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)) { // 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); } 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); 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); 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 _screenBitmap = bmp; } private void loadTAPToolStripMenuItem_Click(object sender, EventArgs e) { _isPaused = true; using (OpenFileDialog ofd = new OpenFileDialog()) { 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); } } 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) { //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.M: UpdateMatrix(7, 2, isPressed); break; case Keys.N: UpdateMatrix(7, 3, isPressed); break; case Keys.B: UpdateMatrix(7, 4, isPressed); break; } } private void playTapeToolStripMenuItem_Click(object sender, EventArgs e) { if (playTapeToolStripMenuItem.Text == "Play Tape") { playTapeToolStripMenuItem.Text = "Stop Tape"; _tapManager.Play(); tapePlaying = true; } else { playTapeToolStripMenuItem.Text = "Play Tape"; _tapManager.Stop(); tapePlaying = false; } } } }