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]; private bool[] _priorityBuffer = new bool[256 * 192]; // Tracks priority pixels! // 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! RenderSprites(); } // End of the NTSC frame (262 lines) if (_currentScanline > 261) { _currentScanline = 0; } } } private void RenderBackground() { ushort nameTableBase = (ushort)((Registers[2] & 0x0E) << 10); byte scrollX = Registers[8]; byte scrollY = Registers[9]; bool lockRowScroll = (Registers[0] & 0x80) != 0; bool lockColScroll = (Registers[0] & 0x40) != 0; // Clear the priority mask for the new frame! Array.Clear(_priorityBuffer, 0, _priorityBuffer.Length); for (int screenY = 0; screenY < 192; screenY++) { for (int screenX = 0; screenX < 256; screenX++) { int effectiveScrollY = scrollY; if (lockColScroll && screenX >= 192) effectiveScrollY = 0; int vdpY = (screenY + effectiveScrollY) % 224; int row = vdpY / 8; int tileY = vdpY % 8; int effectiveScrollX = scrollX; if (lockRowScroll && screenY < 16) effectiveScrollX = 0; int vdpX = (screenX - effectiveScrollX) & 0xFF; int col = vdpX / 8; int tileX = vdpX % 8; // 1. Read the 16-bit Tile instruction 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 ALL THE HARDWARE BITS! int tileIndex = tileData & 0x01FF; bool flipH = (tileData & 0x0200) != 0; // Bit 9 bool flipV = (tileData & 0x0400) != 0; // Bit 10 bool useSpritePalette = (tileData & 0x0800) != 0; // Bit 11 bool priority = (tileData & 0x1000) != 0; // Bit 12 // 3. Apply Vertical Flip (Read from the bottom of the tile instead of the top) int readY = flipV ? (7 - tileY) : tileY; ushort tileAddress = (ushort)(tileIndex * 32); byte bp0 = VRAM[tileAddress + (readY * 4) + 0]; byte bp1 = VRAM[tileAddress + (readY * 4) + 1]; byte bp2 = VRAM[tileAddress + (readY * 4) + 2]; byte bp3 = VRAM[tileAddress + (readY * 4) + 3]; // 4. Apply Horizontal Flip (Shift from right-to-left instead of left-to-right) int readX = flipH ? tileX : (7 - tileX); int colorIndex = ((bp0 >> readX) & 1) | (((bp1 >> readX) & 1) << 1) | (((bp2 >> readX) & 1) << 2) | (((bp3 >> readX) & 1) << 3); int paletteOffset = useSpritePalette ? 16 : 0; byte smsColor = CRAM[paletteOffset + colorIndex]; int r = (smsColor & 0x03) * 85; int g = ((smsColor >> 2) & 0x03) * 85; int b = ((smsColor >> 4) & 0x03) * 85; int screenAddress = (screenY * 256) + screenX; FrameBuffer[screenAddress] = (255 << 24) | (r << 16) | (g << 8) | b; // 5. FLAG THE PRIORITY PIXEL! // If this tile has priority AND the pixel isn't transparent (color 0), // tell the sprite renderer not to draw over it! if (priority && colorIndex != 0) { _priorityBuffer[screenAddress] = true; } } } } private void RenderSprites() { // 1. Find the Sprite Attribute Table (SAT) // Register 5 contains the base address bits (Mask 0x7E, shifted by 7) ushort satBaseAddress = (ushort)((Registers[5] & 0x7E) << 7); // 2. Register 6 determines where the Sprite Tile graphics are stored in VRAM ushort spritePatternBase = (ushort)((Registers[6] & 0x04) << 11); // 3. Register 1 determines sprite size (8x8 or 8x16) bool is8x16 = (Registers[1] & 0x02) != 0; // The SMS can draw a maximum of 64 sprites for (int i = 0; i < 64; i++) { // Read the Y coordinate from the first part of the SAT byte y = VRAM[satBaseAddress + i]; // HARDWARE QUIRK: If Y == 208 in standard 192-line mode, // it acts as a "Stop Drawing" marker. The VDP aborts the rest of the list! if (y == 208) break; // The X coordinates and Tile Indices are interleaved starting at SAT + 0x80 byte x = VRAM[satBaseAddress + 0x80 + (i * 2)]; byte tileIndex = VRAM[satBaseAddress + 0x80 + (i * 2) + 1]; // If sprites are 8x16, the Tile Index always drops the lowest bit (forces even alignment) if (is8x16) tileIndex = (byte)(tileIndex & 0xFE); // Calculate the pixel height for the drawing loop int spriteHeight = is8x16 ? 16 : 8; // Draw the 8x8 (or 8x16) sprite block for (int py = 0; py < spriteHeight; py++) { // Master System Sprites are physically shifted down 1 pixel on the CRT int screenY = y + 1 + py; // If this row of the sprite is off the bottom of the screen, skip it if (screenY >= 192) continue; // Calculate where the 4 bitplanes are for this specific row of the sprite ushort tileAddress = (ushort)(spritePatternBase + (tileIndex * 32) + (py * 4)); byte bp0 = VRAM[tileAddress + 0]; byte bp1 = VRAM[tileAddress + 1]; byte bp2 = VRAM[tileAddress + 2]; byte bp3 = VRAM[tileAddress + 3]; for (int px = 0; px < 8; px++) { int screenX = x + px; // If this pixel is off the right side of the screen, skip it if (screenX >= 256) continue; int shift = 7 - px; int colorIndex = ((bp0 >> shift) & 1) | (((bp1 >> shift) & 1) << 1) | (((bp2 >> shift) & 1) << 2) | (((bp3 >> shift) & 1) << 3); // HARDWARE TRANSPARENCY: // If the color index is 0, DO NOT draw it! Let the background show through. if (colorIndex == 0) continue; // If the background tile at this exact pixel claimed priority, hide the sprite! if (_priorityBuffer[(screenY * 256) + screenX]) continue; // Sprites ALWAYS use the second half of CRAM (Palette 1: Indices 16-31) byte smsColor = CRAM[16 + colorIndex]; int r = (smsColor & 0x03) * 85; int g = ((smsColor >> 2) & 0x03) * 85; int b = ((smsColor >> 4) & 0x03) * 85; // Because we only draw non-zero pixels, this safely overwrites the // background FrameBuffer exactly where the sprite stands! FrameBuffer[(screenY * 256) + screenX] = (255 << 24) | (r << 16) | (g << 8) | b; } } } } } }