Files
ZXSpectrum48K/Desktop/Form1.cs

493 lines
20 KiB
C#

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);
}
if (resourceName.Contains("Desktop.ROMS.Snapshot.") && resourceName.EndsWith(".sna", StringComparison.OrdinalIgnoreCase))
{
string[] parts = resourceName.Split('.');
string displayName = parts[parts.Length - 2];
ToolStripMenuItem item = new ToolStripMenuItem(displayName) { Tag = resourceName };
item.Click += IncludedTapMenuItem_Click;
SNAPToolStripMenuItem1.DropDownItems.Add(item);
}
if (resourceName.Contains("Desktop.ROMS.TZX.") && resourceName.EndsWith(".tzx", StringComparison.OrdinalIgnoreCase))
{
string[] parts = resourceName.Split('.');
string displayName = parts[parts.Length - 2];
ToolStripMenuItem item = new ToolStripMenuItem(displayName) { Tag = resourceName };
item.Click += IncludedTapMenuItem_Click;
TZXToolStripMenuItem1.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();
// Open the embedded resource stream once
using (Stream? stream = assembly.GetManifestResourceStream(resourceName))
{
if (stream == null) throw new Exception($"Could not find embedded resource: {resourceName}");
// Copy the stream into a memory stream to extract the raw byte array
using (MemoryStream ms = new MemoryStream())
{
stream.CopyTo(ms);
byte[] fileBytes = ms.ToArray();
// Route the byte array to the correct emulator component
if (resourceName.EndsWith(".tap", StringComparison.OrdinalIgnoreCase))
{
_machine.TapeDeck.LoadTapData(fileBytes);
}
else if (resourceName.EndsWith(".sna", StringComparison.OrdinalIgnoreCase))
{
_machine.LoadSnapshot(fileBytes);
}
else if (resourceName.EndsWith(".tzx", StringComparison.OrdinalIgnoreCase))
{
// 1. Pass the raw bytes to our static parser
List<TzxBlock> tzxBlocks = TzxParser.Parse(fileBytes);
// 2. Hand the cleanly parsed blocks over to the Tape Deck
_machine.TapeDeck.LoadTzxData(tzxBlocks);
}
}
}
}
catch (Exception ex)
{
MessageBox.Show($"Failed to load built-in resource:\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;
}
}
}
}