Files
ZXSpectrum48K/Core/SpectrumMachine.cs

441 lines
17 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 enum MachineModel
{
Spectrum48K,
SpectrumPlus2A
}
public class SpectrumMachine
{
public MachineModel CurrentModel { get; private set; } = MachineModel.Spectrum48K;
// 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 void SetMachineModel(MachineModel newModel)
{
if (CurrentModel == newModel) return; // Do nothing if it's the same
CurrentModel = newModel;
// Flag the main loop to perform a hard reset on the next frame
_pendingReset = true;
}
public SpectrumMachine(IAudioDevice beeperDevice)
{
_beeper = beeperDevice;
Memory = new MemoryBus();
TapeDeck = new TapManager();
IoBus = new IO_Bus(TapeDeck, Memory);
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, int bankIndex) // <-- Add the bank parameter
{
Memory.LoadRom(romData, bankIndex);
}
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();
Memory.ResetPaging(CurrentModel);
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))
{
// Mix the speaker and the tape ear port
bool finalAudioOutput = IoBus.BeeperState ^ TapeDeck.EarBit;
// Blast the 1-bit Beeper AND the 3-Channel Synth into the audio buffer!
_beeper.AddSample(
finalAudioOutput,
IoBus.AyChip.FreqA,
IoBus.AyChip.VolA,
IoBus.AyChip.FreqB,
IoBus.AyChip.VolB,
IoBus.AyChip.FreqC,
IoBus.AyChip.VolC
);
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)
// ====================================================================
// ====================================================================
// SNAPSHOT MANAGEMENT (.SNA)
// ====================================================================
public void SaveSnapshot(string filepath)
{
ushort originalSP = Cpu.SP;
byte originalMemLow = Memory.Read((ushort)(Cpu.SP - 2));
byte originalMemHigh = Memory.Read((ushort)(Cpu.SP - 1));
// Push PC for 48K compatibility
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))
{
// --- 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);
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);
if (CurrentModel == MachineModel.Spectrum48K)
{
// --- 48K FORMAT (49,179 Bytes) ---
for (int i = 0x4000; i <= 0xFFFF; i++) bw.Write(Memory.Read((ushort)i));
}
else
{
// --- 128K FORMAT (131,103 Bytes) ---
int activeBank = Memory.CurrentRamBankSlot3;
bw.Write(Memory.GetRamBank(5)); // 0x4000 - 0x7FFF
bw.Write(Memory.GetRamBank(2)); // 0x8000 - 0xBFFF
bw.Write(Memory.GetRamBank(activeBank)); // 0xC000 - 0xFFFF
// 128K Extensions
bw.Write((byte)(Cpu.PC & 0xFF));
bw.Write((byte)(Cpu.PC >> 8));
bw.Write(Memory.Port7FFD);
bw.Write((byte)0); // TR-DOS ROM (0 = unused)
// Write the remaining 5 unpaged banks
for (int i = 0; i < 8; i++)
{
if (i != 5 && i != 2 && i != activeBank)
{
bw.Write(Memory.GetRamBank(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);
}
public void LoadSnapshot(byte[] snaData)
{
bool is128K = snaData.Length == 131103;
if (snaData.Length != 49179 && !is128K)
throw new Exception($"Invalid SNA File Size! Got {snaData.Length} bytes.");
// Force the machine into the correct mode
SetMachineModel(is128K ? MachineModel.SpectrumPlus2A : MachineModel.Spectrum48K);
// 1. Load Header
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));
if ((snaData[19] & 0x04) != 0) { Cpu.ExecuteOpcode(0xFB); /* Fake EI */ }
else { Cpu.ExecuteOpcode(0xF3); /* Fake DI */ }
Cpu.R = snaData[20];
Cpu.AF.Word = (ushort)(snaData[21] | (snaData[22] << 8));
Cpu.SP = (ushort)(snaData[23] | (snaData[24] << 8));
if (snaData[25] == 0) Cpu.ExecuteOpcode(0x46); // IM 0
else if (snaData[25] == 1) Cpu.ExecuteOpcode(0x56); // IM 1
else if (snaData[25] == 2) Cpu.ExecuteOpcode(0x5E); // IM 2
IoBus.BorderColourIndex = snaData[26];
// 2. Load RAM
if (!is128K)
{
for (int i = 0; i < 49152; i++) Memory.Write((ushort)(0x4000 + i), snaData[27 + i]);
byte pcLow = Memory.Read(Cpu.SP); Cpu.SP++;
byte pcHigh = Memory.Read(Cpu.SP); Cpu.SP++;
Cpu.PC = (ushort)((pcHigh << 8) | pcLow);
}
else
{
Cpu.PC = (ushort)(snaData[49179] | (snaData[49180] << 8));
byte port7FFD = snaData[49181];
// THE TOASTRACK FIX: Force Amstrad ROMs into Toastrack compatibility mode!
IoBus.WritePort(0x1FFD, 0x04);
IoBus.WritePort(0x7FFD, port7FFD);
int activeBank = port7FFD & 0x07;
// Load the 3 visible memory chunks
Array.Copy(snaData, 27, Memory.GetRamBank(5), 0, 16384);
Array.Copy(snaData, 16411, Memory.GetRamBank(2), 0, 16384);
Array.Copy(snaData, 32795, Memory.GetRamBank(activeBank), 0, 16384);
// Load the remaining 5 hidden memory banks
int offset = 49183;
for (int i = 0; i < 8; i++)
{
if (i != 5 && i != 2 && i != activeBank)
{
Array.Copy(snaData, offset, Memory.GetRamBank(i), 0, 16384);
offset += 16384;
}
}
}
}
}
}