ULA Implemented. Scanline renderer so cycle accurate

This commit is contained in:
2026-04-21 15:34:10 +01:00
parent ad3a0b5040
commit dcbb505145
6 changed files with 424 additions and 310 deletions

View File

@@ -1,8 +1,8 @@
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.Diagnostics;
using System.Threading;
using System.IO;
using Core.Cpu;
using Core.Io;
using Core.Memory;
@@ -14,37 +14,18 @@ namespace Desktop
private Z80 _cpu = null!;
private MemoryBus _memoryBus = null!;
private IO_Bus _simpleIoBus = null!;
private ULA _ula = null!;
private TapManager _tapManager = null!;
private int _ulaFrameCount = 0;
private DebuggerForm? _debugger = null;
private string _baseTitle = "";
private bool _isRunning = false;
private bool _isPaused = false;
private bool _resetFlag = false;
public ushort? Breakpoint = null; // Public so the debugger can set it!
private DebuggerForm _debugger = null;
// The 16 physical colors of the ZX Spectrum (ARGB format)
private readonly int[] SpectrumColors = new int[]
{
// Normal Colors (Bright = 0)
unchecked((int)0xFF000000), // 0: Black
unchecked((int)0xFF0000D7), // 1: Blue
unchecked((int)0xFFD70000), // 2: Red
unchecked((int)0xFFD700D7), // 3: Magenta
unchecked((int)0xFF00D700), // 4: Green
unchecked((int)0xFF00D7D7), // 5: Cyan
unchecked((int)0xFFD7D700), // 6: Yellow
unchecked((int)0xFFD7D7D7), // 7: White
// Bright Colors (Bright = 1)
unchecked((int)0xFF000000), // 8: Bright Black
unchecked((int)0xFF0000FF), // 9: Bright Blue
unchecked((int)0xFFFF0000), // 10: Bright Red
unchecked((int)0xFFFF00FF), // 11: Bright Magenta
unchecked((int)0xFF00FF00), // 12: Bright Green
unchecked((int)0xFF00FFFF), // 13: Bright Cyan
unchecked((int)0xFFFFFF00), // 14: Bright Yellow
unchecked((int)0xFFFFFFFF) // 15: Bright White
};
public ushort? Breakpoint = null;
public long TotalFrameCount = 0;
public double FramesPerSecond = 0;
public double TotalFrameTime = 0;
public double FrameTime = 0;
public Form1()
{
@@ -56,18 +37,17 @@ namespace Desktop
{
try
{
_baseTitle = this.Text;
_memoryBus = new MemoryBus();
_simpleIoBus = new IO_Bus();
_ula = new ULA(_memoryBus, _simpleIoBus);
_tapManager = new TapManager();
_memoryBus.CrapRAMData();
byte[] romData = RomLoader.Load("48.rom");
_memoryBus.LoadRom(romData);
_cpu = new Z80(_memoryBus, _simpleIoBus, _tapManager);
_cpu.WaitStateCallback = _ula.GetContentionDelay;
// Pass 'this' so the DebuggerForm can talk back to this main window
//DebuggerForm debugger = new DebuggerForm(_cpu, _memoryBus, this);
//debugger.Show();
}
catch (Exception ex)
{
@@ -81,28 +61,22 @@ namespace Desktop
if (_isRunning) return;
_isRunning = true;
_isPaused = false;
Task.Run(() =>
{
try
{
const int TStatesPerFrame = 69888;
long nextFrameTargetTStates = _cpu.TotalTStates + TStatesPerFrame;
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
long frameCount = 0;
long nextScanlineTarget = _cpu.TotalTStates + TStatesPerFrame;
var stopwatch = Stopwatch.StartNew();
var fpsStopwatch = Stopwatch.StartNew();
long scanlineCount = 0;
while (_isRunning)
{
//if(_resetFlag)
//{
// _resetFlag = false;
// stopwatch.Reset();
// nextFrameTargetTStates = _cpu.TotalTStates + TStatesPerFrame;
// frameCount = 0;
//}
{
if (_isPaused)
{
System.Threading.Thread.Sleep(10); // Don't melt the host CPU while paused
Thread.Sleep(10);
continue;
}
@@ -110,7 +84,6 @@ namespace Desktop
if (Breakpoint.HasValue && _cpu.PC == Breakpoint.Value)
{
_isPaused = true;
// Optional: You could force the debugger to open here!
continue;
}
@@ -118,22 +91,43 @@ namespace Desktop
_cpu.Step();
// --- Check for End of Frame ---
if (_cpu.TotalTStates >= nextFrameTargetTStates)
if (_cpu.TotalTStates >= nextScanlineTarget)
{
_cpu.RequestInterrupt();
nextFrameTargetTStates += TStatesPerFrame;
frameCount++;
// Tell the ULA to draw one line of pixels
_ula.RenderScanline((int)scanlineCount % 312);
// Render the screen
this.Invoke((MethodInvoker)delegate { RenderScreen(); });
nextScanlineTarget += 224; // Advance target by ONE line (224 T-States)
scanlineCount++;
// Throttle to real-time (50 FPS = 20ms)
long targetTimeMs = frameCount * 20;
long elapsedMs = stopwatch.ElapsedMilliseconds;
if (elapsedMs < targetTimeMs)
// Hit the bottom of the screen (Line 312)?
if (scanlineCount % 312 == 0)
{
System.Threading.Thread.Sleep((int)(targetTimeMs - elapsedMs));
_cpu.RequestInterrupt(); // 50Hz interrupt
this.Invoke((MethodInvoker)delegate
{
UpdateScreenBitmap();
this.Text = $"{_baseTitle} - FPS: {FramesPerSecond:F1}";
});
TotalFrameCount++;
// Throttle to real-time (50 FPS = 20ms)
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();
}
}
}
@@ -141,13 +135,33 @@ namespace Desktop
catch (Exception ex)
{
_isPaused = true;
this.Invoke((MethodInvoker)delegate {
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)
{
using (OpenFileDialog ofd = new OpenFileDialog())
@@ -155,13 +169,8 @@ namespace Desktop
ofd.Filter = "Spectrum TAP Files|*.tap";
if (ofd.ShowDialog() == DialogResult.OK)
{
// The Desktop UI reads the file from the hard drive
byte[] tapBytes = System.IO.File.ReadAllBytes(ofd.FileName);
// The pure Core logic processes the bytes
byte[] tapBytes = File.ReadAllBytes(ofd.FileName);
_cpu._tapManager.LoadTapData(tapBytes);
//MessageBox.Show("Tape inserted! Type LOAD \"\" and press Enter.", "Tape Deck");
}
}
}
@@ -169,10 +178,10 @@ namespace Desktop
{
using (OpenFileDialog ofd = new OpenFileDialog())
{
ofd.Filter = "Spectrum Snapshot Files (*.sna)|*.sna";
ofd.Filter = "Snapshot Files (sna,z80)|*.sna";
if (ofd.ShowDialog() == DialogResult.OK)
{
byte[] snaBytes = System.IO.File.ReadAllBytes(ofd.FileName);
byte[] snaBytes = File.ReadAllBytes(ofd.FileName);
_cpu.LoadSNA(snaBytes);
}
}
@@ -189,7 +198,6 @@ namespace Desktop
private void btnReset_Click(object sender, EventArgs e)
{
//_resetFlag = true;
_isPaused = true;
_cpu.Reset();
_memoryBus.CleanRAMData();
@@ -213,84 +221,7 @@ namespace Desktop
_debugger.BringToFront();
}
}
// Public so the Debugger's background thread can call it 50 times a second
public void RenderScreen()
{
_ulaFrameCount++;
bool invertFlashPhase = (_ulaFrameCount % 32) >= 16;
// --- NEW: Expanded screen size (32px border on all sides) ---
const int screenWidth = 320;
const int screenHeight = 256;
const int borderSize = 32;
int[] pixelData = new int[screenWidth * screenHeight];
// --- NEW: Fill the background with the Border Color ---
// (Note: The hardware border always uses standard brightness, never bright)
int currentBorderColor = SpectrumColors[_simpleIoBus.BorderColorIndex];
Array.Fill(pixelData, currentBorderColor);
// Loop through the 6144 bytes of Pixel RAM
for (int offset = 0; offset < 6144; offset++)
{
ushort address = (ushort)(0x4000 + offset);
byte pixels = _memoryBus.Read(address);
int y = ((offset & 0x0700) >> 8) |
((offset & 0x00E0) >> 2) |
((offset & 0x1800) >> 5);
int x = (offset & 0x001F) * 8;
int attrRow = y / 8;
int attrCol = x / 8;
ushort attrAddress = (ushort)(0x5800 + (attrRow * 32) + attrCol);
byte attr = _memoryBus.Read(attrAddress);
int ink = attr & 0x07;
int paper = (attr >> 3) & 0x07;
int brightOffset = (attr & 0x40) != 0 ? 8 : 0;
bool isFlashSet = (attr & 0x80) != 0;
int inkColor = SpectrumColors[ink + brightOffset];
int paperColor = SpectrumColors[paper + brightOffset];
if (isFlashSet && invertFlashPhase)
{
int temp = inkColor;
inkColor = paperColor;
paperColor = temp;
}
// Draw the 8 pixels
for (int bit = 0; bit < 8; bit++)
{
bool isPixelSet = (pixels & (1 << (7 - bit))) != 0;
// --- NEW: Add the 32px border offset to our X and Y calculations! ---
int renderY = y + borderSize;
int renderX = x + borderSize + bit;
// Map it to our new, wider pixel array
pixelData[(renderY * screenWidth) + renderX] = isPixelSet ? inkColor : paperColor;
}
}
// --- NEW: Update Bitmap dimensions to match the new 320x256 array ---
Bitmap bmp = new Bitmap(screenWidth, screenHeight, PixelFormat.Format32bppArgb);
BitmapData bmpData = bmp.LockBits(
new Rectangle(0, 0, screenWidth, screenHeight),
ImageLockMode.WriteOnly,
bmp.PixelFormat);
Marshal.Copy(pixelData, 0, bmpData.Scan0, pixelData.Length);
bmp.UnlockBits(bmpData);
if (picScreen.Image != null) picScreen.Image.Dispose();
picScreen.Image = bmp;
}
private void UpdateMatrix(int row, int col, bool isPressed)
{
if (isPressed)
@@ -305,14 +236,14 @@ namespace Desktop
}
}
// Hook this to Form1's KeyDown event
protected override void OnKeyDown(KeyEventArgs e)
{
HandleKey(e.KeyCode, true);
base.OnKeyDown(e);
}
// Hook this to Form1's KeyUp event
protected override void OnKeyUp(KeyEventArgs e)
{
HandleKey(e.KeyCode, false);
@@ -370,15 +301,15 @@ namespace Desktop
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.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;
case Keys.B: UpdateMatrix(7, 4, isPressed); break;
}
}
}
}
}