357 lines
13 KiB
C#
357 lines
13 KiB
C#
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.BorderColourIndex = 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.BorderColourIndex);
|
|
|
|
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);
|
|
}
|
|
}
|
|
} |