410 lines
16 KiB
C#
410 lines
16 KiB
C#
using System.Drawing.Imaging;
|
|
using System.Runtime.InteropServices;
|
|
using System.Diagnostics;
|
|
using System.Threading;
|
|
using System.IO;
|
|
using Core.Cpu;
|
|
using Core.Io;
|
|
using Core.Memory;
|
|
|
|
namespace Desktop
|
|
{
|
|
public partial class Form1 : Form
|
|
{
|
|
private Z80 _cpu = null!;
|
|
private MemoryBus _memoryBus = null!;
|
|
private IO_Bus _simpleIoBus = null!;
|
|
private ULA _ula = null!;
|
|
private TapManager _tapManager = null!;
|
|
private DebuggerForm? _debugger = null;
|
|
private BeeperDevice _beeper = null!;
|
|
private string _baseTitle = "";
|
|
private bool _isRunning = false;
|
|
private bool _isPaused = false;
|
|
public ushort? Breakpoint = null;
|
|
public long TotalFrameCount = 0;
|
|
public double FramesPerSecond = 0;
|
|
public double TotalFrameTime = 0;
|
|
public double FrameTime = 0;
|
|
public bool highSpeed = false;
|
|
public bool tapeLoaded = false;
|
|
private volatile bool _pendingReset = false;
|
|
|
|
|
|
public Form1()
|
|
{
|
|
InitializeComponent();
|
|
InitializeEmulator();
|
|
}
|
|
|
|
private void InitializeEmulator()
|
|
{
|
|
try
|
|
{
|
|
_baseTitle = this.Text;
|
|
_memoryBus = new MemoryBus();
|
|
_tapManager = new TapManager();
|
|
_simpleIoBus = new IO_Bus(_tapManager);
|
|
_ula = new ULA(_memoryBus, _simpleIoBus);
|
|
_beeper = new BeeperDevice();
|
|
_memoryBus.CrapRAMData();
|
|
byte[] romData = RomLoader.Load("48.rom");
|
|
_memoryBus.LoadRom(romData);
|
|
_cpu = new Z80(_memoryBus, _simpleIoBus, _tapManager);
|
|
_cpu.WaitStateCallback = _ula.GetContentionDelay;
|
|
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"Failed to initialize emulator:\n{ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
StartEmulationLoop();
|
|
}
|
|
|
|
private void StartEmulationLoop()
|
|
{
|
|
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)
|
|
{
|
|
if (_pendingReset)
|
|
{
|
|
_cpu.Reset();
|
|
_memoryBus.CrapRAMData(); // Important! If RAM has garbage, the ROM boots instantly. If it has old data, it fails the RAM check!
|
|
|
|
// Reset all local loop timing variables
|
|
TotalFrameCount = 0;
|
|
scanlineCount = 0;
|
|
audioSampleCount = 0;
|
|
nextScanlineTarget = TStatesPerFrame;
|
|
stopwatch.Restart();
|
|
|
|
_pendingReset = false;
|
|
}
|
|
|
|
if (_isPaused)
|
|
{
|
|
stopwatch.Restart();
|
|
scanlineCount = 0;
|
|
Thread.Sleep(10);
|
|
continue;
|
|
}
|
|
|
|
// --- Breakpoint Check ---
|
|
if (Breakpoint.HasValue && _cpu.PC == Breakpoint.Value)
|
|
{
|
|
_isPaused = true;
|
|
continue;
|
|
}
|
|
|
|
long tStatesBefore = _cpu.TotalTStates;
|
|
|
|
// --- 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
|
|
TotalFrameCount++;
|
|
|
|
if (highSpeed)
|
|
{
|
|
if (TotalFrameCount % 10 == 0)
|
|
{
|
|
this.BeginInvoke((MethodInvoker)delegate
|
|
{
|
|
UpdateScreenBitmap();
|
|
this.Text = $"{_baseTitle} - FPS: {FramesPerSecond:F1}";
|
|
});
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
this.Invoke((MethodInvoker)delegate
|
|
{
|
|
UpdateScreenBitmap();
|
|
this.Text = $"{_baseTitle} - FPS: {FramesPerSecond:F1} - Tape Loaded: {tapeLoaded.ToString()}";
|
|
});
|
|
}
|
|
|
|
|
|
// Throttle to real-time (50 FPS = 20ms)
|
|
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((MethodInvoker)delegate
|
|
{
|
|
MessageBox.Show(ex.Message, "CPU Crash", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
|
|
private void UpdateScreenBitmap()
|
|
{
|
|
// Build the bitmap
|
|
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);
|
|
|
|
// Pull the raw pixel data
|
|
Marshal.Copy(_ula.FrameBuffer, 0, bmpData.Scan0, _ula.FrameBuffer.Length);
|
|
bmp.UnlockBits(bmpData);
|
|
|
|
if (picScreen.Image != null) picScreen.Image.Dispose();
|
|
picScreen.Image = bmp;
|
|
}
|
|
|
|
private void loadTAPToolStripMenuItem_Click(object sender, EventArgs e)
|
|
{
|
|
_isPaused = true;
|
|
using (OpenFileDialog ofd = new OpenFileDialog())
|
|
{
|
|
ofd.Filter = "Spectrum TAP Files|*.tap";
|
|
if (ofd.ShowDialog() == DialogResult.OK)
|
|
{
|
|
byte[] tapBytes = File.ReadAllBytes(ofd.FileName);
|
|
_cpu._tapManager.LoadTapData(tapBytes);
|
|
tapeLoaded = true;
|
|
}
|
|
}
|
|
_isPaused = false;
|
|
}
|
|
|
|
private void fastLoadingToolStripMenuItem_Click(object sender, EventArgs e)
|
|
{
|
|
// Toggle the checkmark UI
|
|
fastLoadingToolStripMenuItem.Checked = !fastLoadingToolStripMenuItem.Checked;
|
|
|
|
// Tell the CPU to enable or disable the ROM hijack
|
|
_cpu.EnableFastLoad = fastLoadingToolStripMenuItem.Checked;
|
|
}
|
|
private void openSNAToolStripMenuItem_Click(object sender, EventArgs e)
|
|
{
|
|
_isPaused = true;
|
|
using (OpenFileDialog ofd = new OpenFileDialog())
|
|
{
|
|
ofd.Filter = "Snapshot Files (sna,z80)|*.sna";
|
|
if (ofd.ShowDialog() == DialogResult.OK)
|
|
{
|
|
byte[] snaBytes = File.ReadAllBytes(ofd.FileName);
|
|
_cpu.LoadSNA(snaBytes);
|
|
}
|
|
}
|
|
_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;
|
|
_isPaused = true;
|
|
_cpu.Reset();
|
|
_memoryBus.CleanRAMData();
|
|
_isPaused = false;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
|
|
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)
|
|
{
|
|
//Row 0:
|
|
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;
|
|
|
|
// Row 1
|
|
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;
|
|
|
|
//Row 2:
|
|
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;
|
|
|
|
// Row 3:
|
|
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;
|
|
|
|
// Row 4:
|
|
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;
|
|
|
|
// Row 5:
|
|
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;
|
|
|
|
// Row 6
|
|
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;
|
|
|
|
// Row 7
|
|
case Keys.Space: UpdateMatrix(7, 0, isPressed); break;
|
|
case Keys.ControlKey: UpdateMatrix(7, 1, isPressed); break; // Symbol Shift
|
|
case Keys.M: UpdateMatrix(7, 2, isPressed); break;
|
|
case Keys.N: UpdateMatrix(7, 3, isPressed); break;
|
|
case Keys.B: UpdateMatrix(7, 4, isPressed); break;
|
|
}
|
|
}
|
|
}
|
|
} |