using System; namespace Core.Video { public class SmsVdp { // The VDP's private memory! The CPU cannot touch these arrays directly. public byte[] VRAM { get; private set; } = new byte[0x4000]; // 16KB Video RAM public byte[] CRAM { get; private set; } = new byte[0x20]; // 32 Bytes Color Palette public byte[] Registers { get; private set; } = new byte[16]; // 11 Hardware Control Registers public int[] FrameBuffer { get; private set; } = new int[256 * 192]; // The Control Port State Machine (Port 0xBF) private bool _isSecondControlByte = false; private ushort _controlWord = 0; private byte _readBuffer = 0; private int _tStateCounter = 0; private int _currentScanline = 0; private byte _statusRegister = 0x00; public bool InterruptPending => (_statusRegister & 0x80) != 0 && (Registers[1] & 0x20) != 0; public byte ReadDataPort() // Port 0xBE { _isSecondControlByte = false; // Reading data resets the control latch byte value = _readBuffer; _readBuffer = VRAM[_controlWord & 0x3FFF]; _controlWord++; return value; } public byte ReadControlPort() // Port 0xBF { _isSecondControlByte = false; byte currentStatus = _statusRegister; // CRITICAL HARDWARE QUIRK: Reading the status port physically // clears the flags inside the chip! If we don't clear this, // the interrupt line gets stuck on forever. _statusRegister = 0x00; return currentStatus; } public void WriteDataPort(byte value) // Port 0xBE { _isSecondControlByte = false; _readBuffer = value; int address = _controlWord & 0x3FFF; int command = (_controlWord >> 14) & 0x03; if (command == 3) // Code 3: Write to Color Palette (CRAM) { CRAM[address & 0x1F] = value; } else // Code 0, 1, 2: Write to VRAM { VRAM[address] = value; } _controlWord++; // Auto-increment so the Z80 can blast data fast } public void WriteControlPort(byte value) // Port 0xBF { if (!_isSecondControlByte) { // First byte arrives: Store it in the lower 8 bits _controlWord = (ushort)((_controlWord & 0xFF00) | value); _isSecondControlByte = true; } else { // Second byte arrives: Store it in the upper 8 bits and execute! _controlWord = (ushort)((_controlWord & 0x00FF) | (value << 8)); _isSecondControlByte = false; int command = (_controlWord >> 14) & 0x03; if (command == 0) // Code 0: Prep for VRAM Read { _readBuffer = VRAM[_controlWord & 0x3FFF]; _controlWord++; } else if (command == 2) // Code 2: Write to Internal VDP Register { int regIndex = value & 0x0F; byte regData = (byte)(_controlWord & 0xFF); if (regIndex < 16) Registers[regIndex] = regData; } } } public byte ReadVCounter() { // Note: On real NTSC hardware, the V-Counter jumps slightly around // the VBlank period to keep the math 8-bit, but simply returning // the raw current scanline is perfectly fine to get us booting! return (byte)_currentScanline; } public void Update(int tStates) { _tStateCounter += tStates; // 228 T-States per scanline if (_tStateCounter >= 228) { _tStateCounter -= 228; _currentScanline++; // Line 192 is the exact moment the screen finishes drawing! if (_currentScanline == 192) { _statusRegister |= 0x80; // Set Bit 7 (VBlank Flag) to 1 RenderBackground(); // <--- DRAW THE FRAME! } // End of the NTSC frame (262 lines) if (_currentScanline > 261) { _currentScanline = 0; } } } private void RenderBackground() { // The Name Table base address is stored in VDP Register 2. // It tells us where in VRAM the 32x24 screen grid starts. ushort nameTableBase = (ushort)((Registers[2] & 0x0E) << 10); // Loop through all 24 rows and 32 columns of the screen for (int row = 0; row < 24; row++) { for (int col = 0; col < 32; col++) { // 1. Read the 16-bit Tile instruction from the Name Table ushort nameTableAddr = (ushort)(nameTableBase + (row * 64) + (col * 2)); byte lowByte = VRAM[nameTableAddr]; byte highByte = VRAM[nameTableAddr + 1]; ushort tileData = (ushort)((highByte << 8) | lowByte); // 2. Extract the Tile Index and Palette Info int tileIndex = tileData & 0x01FF; bool useSpritePalette = (tileData & 0x0800) != 0; // 3. Find the actual pixel data for this tile in VRAM // Each 8x8 tile takes exactly 32 bytes in memory ushort tileAddress = (ushort)(tileIndex * 32); // 4. Draw the 8x8 block of pixels! for (int y = 0; y < 8; y++) { // The SMS uses 4 bitplanes to make a single row of pixels. byte bp0 = VRAM[tileAddress + (y * 4) + 0]; byte bp1 = VRAM[tileAddress + (y * 4) + 1]; byte bp2 = VRAM[tileAddress + (y * 4) + 2]; byte bp3 = VRAM[tileAddress + (y * 4) + 3]; for (int x = 0; x < 8; x++) { // Combine 1 bit from each bitplane to get a color index (0-15) int shift = 7 - x; int colorIndex = ((bp0 >> shift) & 1) | (((bp1 >> shift) & 1) << 1) | (((bp2 >> shift) & 1) << 2) | (((bp3 >> shift) & 1) << 3); // Find the raw SMS color in CRAM int paletteOffset = useSpritePalette ? 16 : 0; byte smsColor = CRAM[paletteOffset + colorIndex]; // Translate SMS 00BBGGRR format to Windows 32-bit ARGB int r = (smsColor & 0x03) * 85; int g = ((smsColor >> 2) & 0x03) * 85; int b = ((smsColor >> 4) & 0x03) * 85; // Calculate where this pixel goes on the final 256x192 screen int pixelX = (col * 8) + x; int pixelY = (row * 8) + y; FrameBuffer[(pixelY * 256) + pixelX] = (255 << 24) | (r << 16) | (g << 8) | b; } } } } } } }