485 lines
19 KiB
C#
485 lines
19 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
|
|
}
|
|
} |