Files
ZXSpectrum48K/Desktop/Form1.cs

389 lines
15 KiB
C#

using Core; // <-- This gives us access to SpectrumMachine
using Core.Io;
using System.Diagnostics;
using System.Drawing.Imaging;
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 = "";
// ====================================================================
// 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()
{
InitializeComponent();
PopulateIncludedTapsMenu();
this.DoubleBuffered = true;
this.ResizeRedraw = true;
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;
// Load the ROM and start the engine
byte[] romData = RomLoader.Load("48.rom");
_machine.LoadRom(romData);
_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)
{
// We must use BeginInvoke because the Machine is running on a background thread!
this.BeginInvoke((System.Windows.Forms.MethodInvoker)delegate
{
UpdateScreenBitmap(pixels);
this.Invalidate(); // Triggers OnPaint
});
}
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;
}
}
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";
_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();
}
}
}