Files
ZXSpectrum48K/Core/Io/ULA.cs

128 lines
4.9 KiB
C#

using Core.Memory;
using System;
namespace Core.Io
{
public class ULA
{
private readonly MemoryBus _memoryBus;
private readonly IO_Bus _simpleIoBus;
// The ULA owns the frame buffer now!
public const int ScreenWidth = 320;
public const int ScreenHeight = 256; // Perfectly cropped size
public int[] FrameBuffer { get; private set; }
private int _ulaFrameCount = 0;
// The hardware color palette belongs to the ULA, not the Windows Form!
private readonly int[] SpectrumColors = new int[]
{
unchecked((int)0xFF000000), unchecked((int)0xFF0000D7),
unchecked((int)0xFFD70000), unchecked((int)0xFFD700D7),
unchecked((int)0xFF00D700), unchecked((int)0xFF00D7D7),
unchecked((int)0xFFD7D700), unchecked((int)0xFFD7D7D7),
unchecked((int)0xFF000000), unchecked((int)0xFF0000FF),
unchecked((int)0xFFFF0000), unchecked((int)0xFFFF00FF),
unchecked((int)0xFF00FF00), unchecked((int)0xFF00FFFF),
unchecked((int)0xFFFFFF00), unchecked((int)0xFFFFFFFF)
};
public ULA(MemoryBus memoryBus, IO_Bus ioBus)
{
_memoryBus = memoryBus;
_simpleIoBus = ioBus;
FrameBuffer = new int[ScreenWidth * ScreenHeight];
}
// We will wire this up to the Z80 in Phase 3 so the CPU stays system-agnostic!
public int GetContentionDelay(ushort address, long currentTStates)
{
if (address < 0x4000 || address > 0x7FFF) return 0;
long frameT = currentTStates % 69888;
if (frameT < 14336 || frameT >= 57344) return 0;
int lineT = (int)(frameT % 224);
if (lineT < 14 || lineT > 141) return 0;
int delayOffset = (lineT - 14) % 8;
int[] delayPattern = { 6, 5, 4, 3, 2, 1, 0, 0 };
return delayPattern[delayOffset];
}
// The perfectly cropped, 320x256 Scanline Renderer
public void RenderScanline(int scanline)
{
// 1. Drop the invisible lines instantly (VBlank/Overscan)
if (scanline < 32 || scanline > 287) return;
// 2. Calculate our visual Y coordinate (0 to 255) for the bitmap array
int renderY = scanline - 32;
int currentBorderColor = SpectrumColors[_simpleIoBus.BorderColorIndex];
// --- Are we in the Top or Bottom Border? ---
if (scanline < 64 || scanline > 255)
{
int yOffset = renderY * ScreenWidth;
for (int x = 0; x < ScreenWidth; x++)
{
FrameBuffer[yOffset + x] = currentBorderColor;
}
return;
}
// --- We are in the Visible Display Area (Lines 64 to 255) ---
int y = scanline - 64;
// Handle Flash Phase (only increment once per frame when y == 0)
if (y == 0) _ulaFrameCount++;
bool invertFlashPhase = (_ulaFrameCount % 32) >= 16;
int rowStartIndex = renderY * ScreenWidth;
// Draw the 32 pixels of left border
for (int b = 0; b < 32; b++) FrameBuffer[rowStartIndex + b] = currentBorderColor;
int third = y / 64;
int characterRow = (y % 64) / 8;
int pixelRow = y % 8;
// Draw the 32 horizontal character blocks of the visible screen
for (int col = 0; col < 32; col++)
{
ushort pixelAddress = (ushort)(0x4000 | (third << 11) | (pixelRow << 8) | (characterRow << 5) | col);
ushort attrAddress = (ushort)(0x5800 + (y / 8) * 32 + col);
byte pixels = _memoryBus.Read(pixelAddress);
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 in this block
for (int bit = 0; bit < 8; bit++)
{
bool isPixelSet = (pixels & (1 << (7 - bit))) != 0;
int renderX = 32 + (col * 8) + bit;
FrameBuffer[rowStartIndex + renderX] = isPixelSet ? inkColor : paperColor;
}
}
// Draw the 32 pixels of right border
for (int b = ScreenWidth - 32; b < ScreenWidth; b++)
{
FrameBuffer[rowStartIndex + b] = currentBorderColor;
}
}
}
}