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; } = new int[ScreenWidth * ScreenHeight]; public int[] FrontBuffer { get; private set; } = new int[ScreenWidth * ScreenHeight]; 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; } } public void CommitFrame() { Array.Copy(FrameBuffer, FrontBuffer, FrameBuffer.Length); } } }