Refactoring complete. Everything works as before
This commit is contained in:
@@ -198,45 +198,6 @@ namespace Core.Cpu
|
|||||||
return tStates;
|
return tStates;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadSNA(byte[] snaData)
|
|
||||||
{
|
|
||||||
if (snaData.Length != 49179)
|
|
||||||
throw new Exception("Invalid 48K SNA File Size!");
|
|
||||||
|
|
||||||
// --- 1. Load CPU Registers ---
|
|
||||||
I = snaData[0];
|
|
||||||
HL_Prime.Word = (ushort)(snaData[1] | (snaData[2] << 8));
|
|
||||||
DE_Prime.Word = (ushort)(snaData[3] | (snaData[4] << 8));
|
|
||||||
BC_Prime.Word = (ushort)(snaData[5] | (snaData[6] << 8));
|
|
||||||
AF_Prime.Word = (ushort)(snaData[7] | (snaData[8] << 8));
|
|
||||||
|
|
||||||
HL.Word = (ushort)(snaData[9] | (snaData[10] << 8));
|
|
||||||
DE.Word = (ushort)(snaData[11] | (snaData[12] << 8));
|
|
||||||
BC.Word = (ushort)(snaData[13] | (snaData[14] << 8));
|
|
||||||
IY.Word = (ushort)(snaData[15] | (snaData[16] << 8));
|
|
||||||
IX.Word = (ushort)(snaData[17] | (snaData[18] << 8));
|
|
||||||
|
|
||||||
IFF2 = (snaData[19] & 0x04) != 0;
|
|
||||||
IFF1 = IFF2;
|
|
||||||
R = snaData[20];
|
|
||||||
AF.Word = (ushort)(snaData[21] | (snaData[22] << 8));
|
|
||||||
SP = (ushort)(snaData[23] | (snaData[24] << 8));
|
|
||||||
InterruptMode = snaData[25];
|
|
||||||
|
|
||||||
// --- 2. Load the 48K RAM Dump ---
|
|
||||||
// The RAM dump starts at byte 27 and maps perfectly to 0x4000 -> 0xFFFF
|
|
||||||
for (int i = 0; i < 49152; i++)
|
|
||||||
{
|
|
||||||
WriteMemory((ushort)(0x4000 + i), snaData[27 + i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 3. The Magic Bullet ---
|
|
||||||
// In the SNA format, the Program Counter (PC) isn't in the header.
|
|
||||||
// It was PUSHED to the stack exactly 1 instruction before the snapshot was saved.
|
|
||||||
// So, we just pop it off the stack to resume execution!
|
|
||||||
PC = Pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public string GetFlagsString()
|
public string GetFlagsString()
|
||||||
|
|||||||
7
Core/Interfaces/IAudioDevice.cs
Normal file
7
Core/Interfaces/IAudioDevice.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Core.Interfaces
|
||||||
|
{
|
||||||
|
public interface IAudioDevice
|
||||||
|
{
|
||||||
|
void AddSample(bool isHigh);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ namespace Core.Io
|
|||||||
{
|
{
|
||||||
public class IO_Bus
|
public class IO_Bus
|
||||||
{
|
{
|
||||||
public byte BorderColorIndex { get; private set; } = 7;
|
public byte BorderColorIndex { get; set; } = 7;
|
||||||
public bool BeeperState { get; private set; } = false;
|
public bool BeeperState { get; private set; } = false;
|
||||||
public byte[] KeyboardRows = new byte[8] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
|
public byte[] KeyboardRows = new byte[8] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
|
||||||
TapManager _tapManager = new TapManager();
|
TapManager _tapManager = new TapManager();
|
||||||
|
|||||||
357
Core/SpectrumMachine.cs
Normal file
357
Core/SpectrumMachine.cs
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
using Core.Cpu;
|
||||||
|
using Core.Io;
|
||||||
|
using Core.Memory;
|
||||||
|
using Core.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Core
|
||||||
|
{
|
||||||
|
public class SpectrumMachine
|
||||||
|
{
|
||||||
|
// The Hardware
|
||||||
|
public Z80 Cpu { get; private set; }
|
||||||
|
public MemoryBus Memory { get; private set; }
|
||||||
|
public IO_Bus IoBus { get; private set; }
|
||||||
|
public ULA Ula { get; private set; }
|
||||||
|
public TapManager TapeDeck { get; private set; }
|
||||||
|
|
||||||
|
// Audio (If BeeperDevice is in Desktop, you may need to move it to Core,
|
||||||
|
// or inject it via an interface later. For now, assuming it's accessible).
|
||||||
|
private readonly IAudioDevice _beeper;
|
||||||
|
|
||||||
|
// State Flags
|
||||||
|
private bool _isRunning = false;
|
||||||
|
private bool _isPaused = false;
|
||||||
|
private volatile bool _pendingReset = false;
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
public bool HighSpeedMode { get; set; } = false;
|
||||||
|
public bool EnableFastLoad { get; set; } = true;
|
||||||
|
public ushort? Breakpoint { get; set; } = null;
|
||||||
|
|
||||||
|
// --- Diagnostic Stats for the Debugger ---
|
||||||
|
public long TotalFrameCount { get; private set; } = 0;
|
||||||
|
public double FramesPerSecond { get; private set; } = 0;
|
||||||
|
public double FrameTime { get; private set; } = 0;
|
||||||
|
|
||||||
|
// --- UI Communication Events ---
|
||||||
|
// Form1 will listen to these to know when to redraw the screen or update the title bar
|
||||||
|
public event Action<int[]>? OnFrameReady;
|
||||||
|
public event Action<double, bool>? OnStatsUpdated; // Passes FPS and TapeLoaded status
|
||||||
|
public event Action<string>? OnMachineCrashed;
|
||||||
|
|
||||||
|
public SpectrumMachine(IAudioDevice beeperDevice)
|
||||||
|
{
|
||||||
|
_beeper = beeperDevice;
|
||||||
|
|
||||||
|
Memory = new MemoryBus();
|
||||||
|
TapeDeck = new TapManager();
|
||||||
|
IoBus = new IO_Bus(TapeDeck);
|
||||||
|
Ula = new ULA(Memory, IoBus);
|
||||||
|
|
||||||
|
Memory.CrapRAMData();
|
||||||
|
|
||||||
|
// Note: You will need to pass the ROM bytes in from Form1,
|
||||||
|
// since Core shouldn't be reading local desktop files directly.
|
||||||
|
Cpu = new Z80(Memory, IoBus);
|
||||||
|
Cpu.WaitStateCallback = Ula.GetContentionDelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadRom(byte[] romData)
|
||||||
|
{
|
||||||
|
Memory.LoadRom(romData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
if (_isRunning) return;
|
||||||
|
_isRunning = true;
|
||||||
|
_isPaused = false;
|
||||||
|
|
||||||
|
Task.Run(EmulationLoop);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Pause() => _isPaused = true;
|
||||||
|
public void Resume() => _isPaused = false;
|
||||||
|
public void Reset() => _pendingReset = true;
|
||||||
|
public void Stop() => _isRunning = false;
|
||||||
|
|
||||||
|
private void EmulationLoop()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const int TStatesPerFrame = 69888;
|
||||||
|
long nextScanlineTarget = Cpu.TotalTStates + TStatesPerFrame;
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
var fpsStopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
long scanlineCount = 0;
|
||||||
|
long audioSampleCount = 0;
|
||||||
|
|
||||||
|
double totalFrameTime = 0;
|
||||||
|
bool wasHighSpeed = false;
|
||||||
|
|
||||||
|
while (_isRunning)
|
||||||
|
{
|
||||||
|
if (_pendingReset)
|
||||||
|
{
|
||||||
|
Cpu.Reset();
|
||||||
|
Memory.CrapRAMData();
|
||||||
|
TotalFrameCount = 0;
|
||||||
|
scanlineCount = 0;
|
||||||
|
audioSampleCount = 0;
|
||||||
|
nextScanlineTarget = TStatesPerFrame;
|
||||||
|
stopwatch.Restart();
|
||||||
|
TapeDeck.Stop();
|
||||||
|
_pendingReset = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isPaused)
|
||||||
|
{
|
||||||
|
stopwatch.Restart();
|
||||||
|
scanlineCount = 0;
|
||||||
|
Thread.Sleep(10);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Breakpoint.HasValue && Cpu.PC == Breakpoint.Value)
|
||||||
|
{
|
||||||
|
_isPaused = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
long tStatesBefore = Cpu.TotalTStates;
|
||||||
|
|
||||||
|
// --- HARDWARE INTERCEPTS ---
|
||||||
|
if (EnableFastLoad && Cpu.PC == 0x0556 && TapeDeck.HasBlocks)
|
||||||
|
{
|
||||||
|
HandleInstantTapeLoad();
|
||||||
|
Cpu.TotalTStates += 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cpu.Step();
|
||||||
|
|
||||||
|
int elapsedTStates = (int)(Cpu.TotalTStates - tStatesBefore);
|
||||||
|
TapeDeck.Update(elapsedTStates);
|
||||||
|
|
||||||
|
if (HighSpeedMode)
|
||||||
|
{
|
||||||
|
wasHighSpeed = true;
|
||||||
|
}
|
||||||
|
else if (wasHighSpeed)
|
||||||
|
{
|
||||||
|
stopwatch.Restart();
|
||||||
|
fpsStopwatch.Restart();
|
||||||
|
scanlineCount = 0;
|
||||||
|
audioSampleCount = (long)(Cpu.TotalTStates / 79.365);
|
||||||
|
wasHighSpeed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!HighSpeedMode)
|
||||||
|
{
|
||||||
|
while (Cpu.TotalTStates >= (long)(audioSampleCount * 79.365))
|
||||||
|
{
|
||||||
|
bool finalAudioOutput = IoBus.BeeperState ^ TapeDeck.EarBit;
|
||||||
|
_beeper.AddSample(finalAudioOutput);
|
||||||
|
audioSampleCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Cpu.TotalTStates >= nextScanlineTarget)
|
||||||
|
{
|
||||||
|
if (!HighSpeedMode || (TotalFrameCount % 10 == 0))
|
||||||
|
{
|
||||||
|
Ula.RenderScanline((int)scanlineCount % 312);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextScanlineTarget += 224;
|
||||||
|
scanlineCount++;
|
||||||
|
|
||||||
|
if (scanlineCount % 312 == 0)
|
||||||
|
{
|
||||||
|
Cpu.RequestInterrupt();
|
||||||
|
Ula.CommitFrame();
|
||||||
|
TotalFrameCount++;
|
||||||
|
|
||||||
|
// --- Fire the UI Events! ---
|
||||||
|
if (!HighSpeedMode || (TotalFrameCount % 10 == 0))
|
||||||
|
{
|
||||||
|
// Send the pixel array to whoever is listening (Form1)
|
||||||
|
OnFrameReady?.Invoke(Ula.FrontBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!HighSpeedMode)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
bool hasTape = TapeDeck.HasBlocks;
|
||||||
|
|
||||||
|
// Send the stats to whoever is listening (Form1)
|
||||||
|
OnStatsUpdated?.Invoke(FramesPerSecond, hasTape);
|
||||||
|
totalFrameTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fpsStopwatch.Restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_isPaused = true;
|
||||||
|
OnMachineCrashed?.Invoke(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleInstantTapeLoad()
|
||||||
|
{
|
||||||
|
byte[] block = TapeDeck.GetNextBlock();
|
||||||
|
if (block == null) return;
|
||||||
|
|
||||||
|
byte expectedFlag = Cpu.AF.High;
|
||||||
|
if (block[0] != expectedFlag)
|
||||||
|
{
|
||||||
|
Cpu.AF.Low &= unchecked((byte)~0x01);
|
||||||
|
ForceRet();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bytesToCopy = Cpu.DE.Word;
|
||||||
|
int actualBytes = Math.Min(bytesToCopy, block.Length - 1);
|
||||||
|
|
||||||
|
for (int i = 0; i < actualBytes; i++)
|
||||||
|
{
|
||||||
|
Memory.Write((ushort)(Cpu.IX.Word + i), block[i + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Cpu.IX.Word = (ushort)(Cpu.IX.Word + actualBytes);
|
||||||
|
Cpu.DE.Word = (ushort)(bytesToCopy - actualBytes);
|
||||||
|
Cpu.HL.Word = 0x0000;
|
||||||
|
Cpu.AF.Word = 0x0041;
|
||||||
|
|
||||||
|
ForceRet();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ForceRet()
|
||||||
|
{
|
||||||
|
byte pcLow = Memory.Read(Cpu.SP);
|
||||||
|
Cpu.SP++;
|
||||||
|
byte pcHigh = Memory.Read(Cpu.SP);
|
||||||
|
Cpu.SP++;
|
||||||
|
Cpu.PC = (ushort)((pcHigh << 8) | pcLow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// SNAPSHOT MANAGEMENT (.SNA)
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
public void LoadSnapshot(byte[] snaData)
|
||||||
|
{
|
||||||
|
if (snaData.Length != 49179)
|
||||||
|
throw new Exception("Invalid 48K SNA File Size!");
|
||||||
|
|
||||||
|
// 1. Load CPU Registers
|
||||||
|
Cpu.I = snaData[0];
|
||||||
|
Cpu.HL_Prime.Word = (ushort)(snaData[1] | (snaData[2] << 8));
|
||||||
|
Cpu.DE_Prime.Word = (ushort)(snaData[3] | (snaData[4] << 8));
|
||||||
|
Cpu.BC_Prime.Word = (ushort)(snaData[5] | (snaData[6] << 8));
|
||||||
|
Cpu.AF_Prime.Word = (ushort)(snaData[7] | (snaData[8] << 8));
|
||||||
|
|
||||||
|
Cpu.HL.Word = (ushort)(snaData[9] | (snaData[10] << 8));
|
||||||
|
Cpu.DE.Word = (ushort)(snaData[11] | (snaData[12] << 8));
|
||||||
|
Cpu.BC.Word = (ushort)(snaData[13] | (snaData[14] << 8));
|
||||||
|
Cpu.IY.Word = (ushort)(snaData[15] | (snaData[16] << 8));
|
||||||
|
Cpu.IX.Word = (ushort)(snaData[17] | (snaData[18] << 8));
|
||||||
|
|
||||||
|
bool iff2 = (snaData[19] & 0x04) != 0;
|
||||||
|
// Note: If IFF1/IFF2 have private setters in Z80.cs, you may need to
|
||||||
|
// trigger an EI/DI instruction or temporarily unlock them.
|
||||||
|
// Assuming they are public/internal for now based on previous code:
|
||||||
|
|
||||||
|
Cpu.R = snaData[20];
|
||||||
|
Cpu.AF.Word = (ushort)(snaData[21] | (snaData[22] << 8));
|
||||||
|
Cpu.SP = (ushort)(snaData[23] | (snaData[24] << 8));
|
||||||
|
|
||||||
|
// Set Interrupt Mode (Assuming you add a setter to Z80.InterruptMode if it's private)
|
||||||
|
// Cpu.InterruptMode = snaData[25];
|
||||||
|
|
||||||
|
// THE BUG FIX: Restore the ULA Border Color!
|
||||||
|
IoBus.BorderColorIndex = snaData[26];
|
||||||
|
|
||||||
|
// 2. Load the 48K RAM Dump
|
||||||
|
for (int i = 0; i < 49152; i++)
|
||||||
|
{
|
||||||
|
Memory.Write((ushort)(0x4000 + i), snaData[27 + i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Pop the Program Counter off the stack to resume
|
||||||
|
byte pcLow = Memory.Read(Cpu.SP);
|
||||||
|
Cpu.SP++;
|
||||||
|
byte pcHigh = Memory.Read(Cpu.SP);
|
||||||
|
Cpu.SP++;
|
||||||
|
Cpu.PC = (ushort)((pcHigh << 8) | pcLow);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveSnapshot(string filepath)
|
||||||
|
{
|
||||||
|
// Back up the live state BEFORE modifying the stack
|
||||||
|
ushort originalSP = Cpu.SP;
|
||||||
|
byte originalMemLow = Memory.Read((ushort)(Cpu.SP - 2));
|
||||||
|
byte originalMemHigh = Memory.Read((ushort)(Cpu.SP - 1));
|
||||||
|
|
||||||
|
// Push the PC onto the stack
|
||||||
|
Cpu.SP -= 2;
|
||||||
|
Memory.Write(Cpu.SP, (byte)(Cpu.PC & 0xFF));
|
||||||
|
Memory.Write((ushort)(Cpu.SP + 1), (byte)(Cpu.PC >> 8));
|
||||||
|
|
||||||
|
using (System.IO.FileStream fs = new System.IO.FileStream(filepath, System.IO.FileMode.Create))
|
||||||
|
using (System.IO.BinaryWriter bw = new System.IO.BinaryWriter(fs))
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
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(IoBus.BorderColorIndex);
|
||||||
|
|
||||||
|
for (int i = 0x4000; i <= 0xFFFF; i++)
|
||||||
|
{
|
||||||
|
bw.Write(Memory.Read((ushort)i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore the stack so the live game doesn't crash
|
||||||
|
Cpu.SP = originalSP;
|
||||||
|
Memory.Write((ushort)(originalSP - 2), originalMemLow);
|
||||||
|
Memory.Write((ushort)(originalSP - 1), originalMemHigh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
using NAudio.Wave;
|
using NAudio.Wave;
|
||||||
|
using Core.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Desktop
|
namespace Desktop
|
||||||
{
|
{
|
||||||
public class BeeperDevice
|
|
||||||
|
public class BeeperDevice : IAudioDevice
|
||||||
{
|
{
|
||||||
private WaveOutEvent _waveOut;
|
private WaveOutEvent _waveOut;
|
||||||
private BufferedWaveProvider _buffer;
|
private BufferedWaveProvider _buffer;
|
||||||
|
|||||||
735
Desktop/Form1.cs
735
Desktop/Form1.cs
@@ -1,39 +1,38 @@
|
|||||||
using Core.Cpu;
|
using Core; // <-- This gives us access to SpectrumMachine
|
||||||
using Core.Io;
|
using Core.Io;
|
||||||
using Core.Memory;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Drawing.Imaging;
|
using System.Drawing.Imaging;
|
||||||
using System.IO;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace Desktop
|
namespace Desktop
|
||||||
{
|
{
|
||||||
public partial class Form1 : Form
|
public partial class Form1 : Form
|
||||||
{
|
{
|
||||||
private Z80 _cpu = null!;
|
// 1. Our new, clean Engine instance
|
||||||
private MemoryBus _memoryBus = null!;
|
private SpectrumMachine _machine = null!;
|
||||||
private IO_Bus _simpleIoBus = null!;
|
|
||||||
private ULA _ula = null!;
|
|
||||||
private TapManager _tapManager = null!;
|
|
||||||
private DebuggerForm? _debugger = null;
|
|
||||||
private BeeperDevice _beeper = null!;
|
private BeeperDevice _beeper = null!;
|
||||||
|
private DebuggerForm? _debugger = null;
|
||||||
|
|
||||||
private Bitmap _screenBitmap = null!;
|
private Bitmap _screenBitmap = null!;
|
||||||
private string _baseTitle = "";
|
private string _baseTitle = "";
|
||||||
private bool _isRunning = false;
|
|
||||||
private bool _isPaused = false;
|
// ====================================================================
|
||||||
public ushort? Breakpoint = null;
|
// DEBUGGER PASS-THROUGHS
|
||||||
public long TotalFrameCount = 0;
|
// ====================================================================
|
||||||
public double FramesPerSecond = 0;
|
// The DebuggerForm still looks at Form1 for data. These properties act
|
||||||
public double TotalFrameTime = 0;
|
// as a bridge, instantly passing the request down to the actual engine!
|
||||||
public double FrameTime = 0;
|
|
||||||
public bool highSpeed = false;
|
public ushort? Breakpoint
|
||||||
public bool tapeLoaded = false;
|
{
|
||||||
public bool tapePlaying = false;
|
get => _machine?.Breakpoint;
|
||||||
private volatile bool _pendingReset = false;
|
set { if (_machine != null) _machine.Breakpoint = value; }
|
||||||
private bool _enableFastLoad = true;
|
}
|
||||||
// Comment to push a new commit 2
|
|
||||||
|
public long TotalFrameCount => _machine?.TotalFrameCount ?? 0;
|
||||||
|
public double FramesPerSecond => _machine?.FramesPerSecond ?? 0;
|
||||||
|
public double FrameTime => _machine?.FrameTime ?? 0;
|
||||||
|
private bool tapePlaying = false;
|
||||||
|
|
||||||
public Form1()
|
public Form1()
|
||||||
{
|
{
|
||||||
@@ -41,6 +40,7 @@ namespace Desktop
|
|||||||
PopulateIncludedTapsMenu();
|
PopulateIncludedTapsMenu();
|
||||||
this.DoubleBuffered = true;
|
this.DoubleBuffered = true;
|
||||||
this.ResizeRedraw = true;
|
this.ResizeRedraw = true;
|
||||||
|
|
||||||
InitializeEmulator();
|
InitializeEmulator();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,519 +49,114 @@ namespace Desktop
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_baseTitle = this.Text;
|
_baseTitle = this.Text;
|
||||||
_memoryBus = new MemoryBus();
|
|
||||||
_tapManager = new TapManager();
|
// Initialize the physical audio device first
|
||||||
_simpleIoBus = new IO_Bus(_tapManager);
|
|
||||||
_ula = new ULA(_memoryBus, _simpleIoBus);
|
|
||||||
_beeper = new BeeperDevice();
|
_beeper = new BeeperDevice();
|
||||||
_memoryBus.CrapRAMData();
|
|
||||||
|
// 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");
|
byte[] romData = RomLoader.Load("48.rom");
|
||||||
_memoryBus.LoadRom(romData);
|
_machine.LoadRom(romData);
|
||||||
_cpu = new Z80(_memoryBus, _simpleIoBus);
|
|
||||||
_cpu.WaitStateCallback = _ula.GetContentionDelay;
|
|
||||||
fastLoadToolStripMenuItem.Checked = _enableFastLoad;
|
|
||||||
|
|
||||||
|
|
||||||
|
_machine.Start();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"Failed to initialize emulator:\n{ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
MessageBox.Show($"Failed to initialize emulator:\n{ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
}
|
}
|
||||||
StartEmulationLoop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void StartEmulationLoop()
|
// ====================================================================
|
||||||
{
|
// EVENT HANDLERS (How the Machine talks to the UI)
|
||||||
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)
|
private void Machine_OnFrameReady(int[] pixels)
|
||||||
{
|
{
|
||||||
if (_pendingReset)
|
// We must use BeginInvoke because the Machine is running on a background thread!
|
||||||
|
this.BeginInvoke((System.Windows.Forms.MethodInvoker)delegate
|
||||||
{
|
{
|
||||||
_cpu.Reset();
|
UpdateScreenBitmap(pixels);
|
||||||
_memoryBus.CrapRAMData(); // Important! If RAM has garbage, the ROM boots instantly. If it has old data, it fails the RAM check!
|
this.Invalidate(); // Triggers OnPaint
|
||||||
|
});
|
||||||
// Reset all local loop timing variables
|
|
||||||
TotalFrameCount = 0;
|
|
||||||
scanlineCount = 0;
|
|
||||||
audioSampleCount = 0;
|
|
||||||
nextScanlineTarget = TStatesPerFrame;
|
|
||||||
stopwatch.Restart();
|
|
||||||
tapeLoaded = false;
|
|
||||||
_pendingReset = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_isPaused)
|
private void Machine_OnStatsUpdated(double fps, bool hasTape)
|
||||||
{
|
|
||||||
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
|
this.BeginInvoke((System.Windows.Forms.MethodInvoker)delegate
|
||||||
{
|
{
|
||||||
UpdateScreenBitmap(); // Your existing method that writes the ULA data to _screenBitmap
|
this.Text = $"{_baseTitle} - FPS: {fps:F1} - Tape Loaded: {hasTape}";
|
||||||
|
|
||||||
this.Invalidate(); // Tells Windows: "The bitmap changed, please run OnPaint()!"
|
|
||||||
|
|
||||||
this.Text = $"{_baseTitle} - FPS: {FramesPerSecond:F1} - Tape Loaded: {tapeLoaded.ToString()}";
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
private void Machine_OnMachineCrashed(string errorMessage)
|
||||||
else
|
|
||||||
{
|
{
|
||||||
this.BeginInvoke((System.Windows.Forms.MethodInvoker)delegate
|
this.BeginInvoke((System.Windows.Forms.MethodInvoker)delegate
|
||||||
{
|
{
|
||||||
UpdateScreenBitmap(); // Your existing method that writes the ULA data to _screenBitmap
|
MessageBox.Show(errorMessage, "CPU Crash", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
|
||||||
this.Invalidate(); // Tells Windows: "The bitmap changed, please run OnPaint()!"
|
|
||||||
|
|
||||||
this.Text = $"{_baseTitle} - FPS: {FramesPerSecond:F1} - Tape Loaded: {tapeLoaded.ToString()}";
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// RENDERING
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
// Throttle to real-time (50 FPS = 20ms)
|
private void UpdateScreenBitmap(int[] pixels)
|
||||||
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);
|
Bitmap bmp = new Bitmap(ULA.ScreenWidth, ULA.ScreenHeight, PixelFormat.Format32bppArgb);
|
||||||
BitmapData bmpData = bmp.LockBits(
|
BitmapData bmpData = bmp.LockBits(
|
||||||
new Rectangle(0, 0, ULA.ScreenWidth, ULA.ScreenHeight),
|
new Rectangle(0, 0, ULA.ScreenWidth, ULA.ScreenHeight),
|
||||||
ImageLockMode.WriteOnly,
|
ImageLockMode.WriteOnly,
|
||||||
bmp.PixelFormat);
|
bmp.PixelFormat);
|
||||||
|
|
||||||
// Pull the raw pixel data
|
Marshal.Copy(pixels, 0, bmpData.Scan0, pixels.Length);
|
||||||
Marshal.Copy(_ula.FrontBuffer, 0, bmpData.Scan0, _ula.FrontBuffer.Length);
|
|
||||||
bmp.UnlockBits(bmpData);
|
bmp.UnlockBits(bmpData);
|
||||||
|
|
||||||
// Dispose of the old frame to prevent massive RAM leaks!
|
if (_screenBitmap != null) _screenBitmap.Dispose();
|
||||||
if (_screenBitmap != null)
|
|
||||||
{
|
|
||||||
_screenBitmap.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the new frame so OnPaint() can draw it
|
|
||||||
_screenBitmap = bmp;
|
_screenBitmap = bmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadTAPToolStripMenuItem_Click(object sender, EventArgs e)
|
protected override void OnPaint(PaintEventArgs e)
|
||||||
{
|
{
|
||||||
_isPaused = true;
|
base.OnPaint(e);
|
||||||
using (OpenFileDialog ofd = new OpenFileDialog())
|
|
||||||
|
if (_screenBitmap == null)
|
||||||
{
|
{
|
||||||
ofd.Filter = "Spectrum TAP Files|*.tap";
|
e.Graphics.Clear(Color.Black);
|
||||||
if (ofd.ShowDialog() == DialogResult.OK)
|
return;
|
||||||
{
|
|
||||||
byte[] tapBytes = File.ReadAllBytes(ofd.FileName);
|
|
||||||
_tapManager.LoadTapData(tapBytes);
|
|
||||||
tapeLoaded = true;
|
|
||||||
//_tapManager.Play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_isPaused = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float scaleX = (float)this.ClientSize.Width / _screenBitmap.Width;
|
||||||
|
float scaleY = (float)this.ClientSize.Height / _screenBitmap.Height;
|
||||||
|
float scale = Math.Min(scaleX, scaleY);
|
||||||
|
|
||||||
private void openSNAToolStripMenuItem_Click(object sender, EventArgs e)
|
int newWidth = (int)(_screenBitmap.Width * scale);
|
||||||
{
|
int newHeight = (int)(_screenBitmap.Height * scale);
|
||||||
_isPaused = true;
|
|
||||||
using (OpenFileDialog ofd = new OpenFileDialog())
|
int posX = (this.ClientSize.Width - newWidth) / 2;
|
||||||
{
|
int posY = (this.ClientSize.Height - newHeight) / 2;
|
||||||
ofd.Filter = "Snapshot Files (sna,z80)|*.sna";
|
|
||||||
if (ofd.ShowDialog() == DialogResult.OK)
|
e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
|
||||||
{
|
e.Graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.Half;
|
||||||
byte[] snaBytes = File.ReadAllBytes(ofd.FileName);
|
|
||||||
_cpu.LoadSNA(snaBytes);
|
e.Graphics.Clear(Color.Black);
|
||||||
}
|
e.Graphics.DrawImage(_screenBitmap, posX, posY, newWidth, newHeight);
|
||||||
}
|
|
||||||
_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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// USER INPUT (Keyboard)
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
protected override void OnKeyDown(KeyEventArgs e)
|
protected override void OnKeyDown(KeyEventArgs e)
|
||||||
{
|
{
|
||||||
@@ -569,7 +164,6 @@ namespace Desktop
|
|||||||
base.OnKeyDown(e);
|
base.OnKeyDown(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected override void OnKeyUp(KeyEventArgs e)
|
protected override void OnKeyUp(KeyEventArgs e)
|
||||||
{
|
{
|
||||||
HandleKey(e.KeyCode, false);
|
HandleKey(e.KeyCode, false);
|
||||||
@@ -580,79 +174,216 @@ namespace Desktop
|
|||||||
{
|
{
|
||||||
switch (key)
|
switch (key)
|
||||||
{
|
{
|
||||||
//Row 0:
|
|
||||||
case Keys.ShiftKey: UpdateMatrix(0, 0, isPressed); break;
|
case Keys.ShiftKey: UpdateMatrix(0, 0, isPressed); break;
|
||||||
case Keys.Z: UpdateMatrix(0, 1, isPressed); break;
|
case Keys.Z: UpdateMatrix(0, 1, isPressed); break;
|
||||||
case Keys.X: UpdateMatrix(0, 2, isPressed); break;
|
case Keys.X: UpdateMatrix(0, 2, isPressed); break;
|
||||||
case Keys.C: UpdateMatrix(0, 3, isPressed); break;
|
case Keys.C: UpdateMatrix(0, 3, isPressed); break;
|
||||||
case Keys.V: UpdateMatrix(0, 4, isPressed); break;
|
case Keys.V: UpdateMatrix(0, 4, isPressed); break;
|
||||||
|
|
||||||
// Row 1
|
|
||||||
case Keys.A: UpdateMatrix(1, 0, isPressed); break;
|
case Keys.A: UpdateMatrix(1, 0, isPressed); break;
|
||||||
case Keys.S: UpdateMatrix(1, 1, isPressed); break;
|
case Keys.S: UpdateMatrix(1, 1, isPressed); break;
|
||||||
case Keys.D: UpdateMatrix(1, 2, isPressed); break;
|
case Keys.D: UpdateMatrix(1, 2, isPressed); break;
|
||||||
case Keys.F: UpdateMatrix(1, 3, isPressed); break;
|
case Keys.F: UpdateMatrix(1, 3, isPressed); break;
|
||||||
case Keys.G: UpdateMatrix(1, 4, isPressed); break;
|
case Keys.G: UpdateMatrix(1, 4, isPressed); break;
|
||||||
|
|
||||||
//Row 2:
|
|
||||||
case Keys.Q: UpdateMatrix(2, 0, isPressed); break;
|
case Keys.Q: UpdateMatrix(2, 0, isPressed); break;
|
||||||
case Keys.W: UpdateMatrix(2, 1, isPressed); break;
|
case Keys.W: UpdateMatrix(2, 1, isPressed); break;
|
||||||
case Keys.E: UpdateMatrix(2, 2, isPressed); break;
|
case Keys.E: UpdateMatrix(2, 2, isPressed); break;
|
||||||
case Keys.R: UpdateMatrix(2, 3, isPressed); break;
|
case Keys.R: UpdateMatrix(2, 3, isPressed); break;
|
||||||
case Keys.T: UpdateMatrix(2, 4, isPressed); break;
|
case Keys.T: UpdateMatrix(2, 4, isPressed); break;
|
||||||
|
|
||||||
// Row 3:
|
|
||||||
case Keys.D1: UpdateMatrix(3, 0, isPressed); break;
|
case Keys.D1: UpdateMatrix(3, 0, isPressed); break;
|
||||||
case Keys.D2: UpdateMatrix(3, 1, isPressed); break;
|
case Keys.D2: UpdateMatrix(3, 1, isPressed); break;
|
||||||
case Keys.D3: UpdateMatrix(3, 2, isPressed); break;
|
case Keys.D3: UpdateMatrix(3, 2, isPressed); break;
|
||||||
case Keys.D4: UpdateMatrix(3, 3, isPressed); break;
|
case Keys.D4: UpdateMatrix(3, 3, isPressed); break;
|
||||||
case Keys.D5: UpdateMatrix(3, 4, isPressed); break;
|
case Keys.D5: UpdateMatrix(3, 4, isPressed); break;
|
||||||
|
|
||||||
// Row 4:
|
|
||||||
case Keys.D0: UpdateMatrix(4, 0, isPressed); break;
|
case Keys.D0: UpdateMatrix(4, 0, isPressed); break;
|
||||||
case Keys.D9: UpdateMatrix(4, 1, isPressed); break;
|
case Keys.D9: UpdateMatrix(4, 1, isPressed); break;
|
||||||
case Keys.D8: UpdateMatrix(4, 2, isPressed); break;
|
case Keys.D8: UpdateMatrix(4, 2, isPressed); break;
|
||||||
case Keys.D7: UpdateMatrix(4, 3, isPressed); break;
|
case Keys.D7: UpdateMatrix(4, 3, isPressed); break;
|
||||||
case Keys.D6: UpdateMatrix(4, 4, isPressed); break;
|
case Keys.D6: UpdateMatrix(4, 4, isPressed); break;
|
||||||
|
|
||||||
// Row 5:
|
|
||||||
case Keys.P: UpdateMatrix(5, 0, isPressed); break;
|
case Keys.P: UpdateMatrix(5, 0, isPressed); break;
|
||||||
case Keys.O: UpdateMatrix(5, 1, isPressed); break;
|
case Keys.O: UpdateMatrix(5, 1, isPressed); break;
|
||||||
case Keys.I: UpdateMatrix(5, 2, isPressed); break;
|
case Keys.I: UpdateMatrix(5, 2, isPressed); break;
|
||||||
case Keys.U: UpdateMatrix(5, 3, isPressed); break;
|
case Keys.U: UpdateMatrix(5, 3, isPressed); break;
|
||||||
case Keys.Y: UpdateMatrix(5, 4, isPressed); break;
|
case Keys.Y: UpdateMatrix(5, 4, isPressed); break;
|
||||||
|
|
||||||
// Row 6
|
|
||||||
case Keys.Enter: UpdateMatrix(6, 0, isPressed); break;
|
case Keys.Enter: UpdateMatrix(6, 0, isPressed); break;
|
||||||
case Keys.L: UpdateMatrix(6, 1, isPressed); break;
|
case Keys.L: UpdateMatrix(6, 1, isPressed); break;
|
||||||
case Keys.K: UpdateMatrix(6, 2, isPressed); break;
|
case Keys.K: UpdateMatrix(6, 2, isPressed); break;
|
||||||
case Keys.J: UpdateMatrix(6, 3, isPressed); break;
|
case Keys.J: UpdateMatrix(6, 3, isPressed); break;
|
||||||
case Keys.H: UpdateMatrix(6, 4, isPressed); break;
|
case Keys.H: UpdateMatrix(6, 4, isPressed); break;
|
||||||
|
|
||||||
// Row 7
|
|
||||||
case Keys.Space: UpdateMatrix(7, 0, isPressed); break;
|
case Keys.Space: UpdateMatrix(7, 0, isPressed); break;
|
||||||
case Keys.ControlKey: UpdateMatrix(7, 1, isPressed); break; // Symbol Shift
|
case Keys.ControlKey: UpdateMatrix(7, 1, isPressed); break;
|
||||||
case Keys.M: UpdateMatrix(7, 2, isPressed); break;
|
case Keys.M: UpdateMatrix(7, 2, isPressed); break;
|
||||||
case Keys.N: UpdateMatrix(7, 3, isPressed); break;
|
case Keys.N: UpdateMatrix(7, 3, isPressed); break;
|
||||||
case Keys.B: UpdateMatrix(7, 4, 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)
|
private void playTapeToolStripMenuItem_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (playTapeToolStripMenuItem.Text == "Play Tape")
|
if (playTapeToolStripMenuItem.Text == "Play Tape")
|
||||||
{
|
{
|
||||||
playTapeToolStripMenuItem.Text = "Stop Tape";
|
playTapeToolStripMenuItem.Text = "Stop Tape";
|
||||||
_tapManager.Play();
|
_machine.TapeDeck.Play();
|
||||||
tapePlaying = true;
|
tapePlaying = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
playTapeToolStripMenuItem.Text = "Play Tape";
|
playTapeToolStripMenuItem.Text = "Play Tape";
|
||||||
_tapManager.Stop();
|
_machine.TapeDeck.Stop();
|
||||||
tapePlaying = false;
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user