using System; using System.Diagnostics; using System.IO; 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[] smsCRAM { get; private set; } = new byte[0x20]; // Master System - 32 Bytes colour Palette public byte[] ggCRAM { get; private set; } = new byte[0x40]; // GameGear - 64 Bytes colour 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! // Hardware Latches private int _latchedHScroll = 0; private int _latchedVScroll = 0; // 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 int _lineCounter = 0; private byte _statusRegister = 0x00; public bool IsGameGear { get; set; } = false; public bool InterruptPending => ((_statusRegister & 0x80) != 0 && (Registers[1] & 0x20) != 0) || // VBlank ((_statusRegister & 0x40) != 0 && (Registers[0] & 0x10) != 0); // Line Interrupt public byte ReadDataPort() // Port 0xBE { _isSecondControlByte = false; // Reading data resets the control latch byte value = _readBuffer; _readBuffer = VRAM[_controlWord & 0x3FFF]; IncrementVdpAddress(); return value; } public byte ReadControlPort() // Port 0xBF { _isSecondControlByte = false; byte currentStatus = _statusRegister; // 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 Colour Palette (CRAM) { if (IsGameGear) { ggCRAM[address & 0x3F] = value; // GG has 64 bytes of CRAM } else { smsCRAM[address & 0x1F] = value; // SMS has 32 bytes of CRAM } } else // THE FIX: Code 0, 1, 2: Write graphics to VRAM! { VRAM[address] = value; } // THE FIX: The pointer MUST auto-increment so the CPU can blast data fast! IncrementVdpAddress(); } 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]; IncrementVdpAddress(); } 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; } } } private void IncrementVdpAddress() { // The VDP address register is only 14 bits // When it increments past 0x3FFF, it rolls over to 0x0000. // It can't overflow and corrupt the 2-bit command register (bits 14 and 15). ushort address = (ushort)(_controlWord & 0x3FFF); ushort command = (ushort)(_controlWord & 0xC000); address = (ushort)((address + 1) & 0x3FFF); _controlWord = (ushort)(command | address); } public byte ReadVCounter() { // NTSC Math: 262 lines. Counts 0 to 218, jumps to 213 (0xD5), counts to 255. if (_currentScanline <= 218) return (byte)_currentScanline; else return (byte)(_currentScanline - 6); } public byte ReadHCounter() { // The Master System H-Counter counts from 0x00 to 0x93, then jumps forward to 0xE9, ending at 0xFF. // 1 T-State = 1.5 pixels. The H-Counter increments every 2 pixels. // So H = T * 0.75 int h = (int)(_tStateCounter * 0.75); if (h <= 0x93) { return (byte)h; } else { // Emulate the hardware jump! return (byte)(h - 0x94 + 0xE9); } } public void Update(int tStates) { _tStateCounter += tStates; if (_tStateCounter >= 228) { _tStateCounter -= 228; // 1. RENDER THE CURRENT LINE FIRST! // The CPU just finished spending 228 cycles on this exact line. // We draw it now using whatever scroll values the CPU set during that time. if (_currentScanline < 192) { RenderScanline(_currentScanline); } // 2. CHECK LINE INTERRUPTS // Now that the line is drawn, we check if we need to alert the CPU for the NEXT line. if (_currentScanline <= 192) { _lineCounter--; if (_lineCounter < 0) { _lineCounter = Registers[10]; // Reload counter _statusRegister |= 0x40; // Set Line Interrupt Flag (Bit 6) } } else { _lineCounter = Registers[10]; // Reload outside active display } // 3. MOVE TO THE NEXT LINE _currentScanline++; int maxLines = 262; if (_currentScanline > maxLines -1) { _currentScanline = 0; } // 4. TRIGGER VBLANK // The Master System sets the VBlank flag at the exact start of scanline 192. if (_currentScanline == 192) { _statusRegister |= 0x80; } } } private void RenderScanline(int screenY) { // If the display is disabled, fill the line with black and exit if ((Registers[1] & 0x40) == 0) { for (int x = 0; x < 256; x++) FrameBuffer[(screenY * 256) + x] = unchecked((int)0xFF000000); return; } // --- 1. RENDER BACKGROUND LINE --- ushort nameTableBase = (ushort)((Registers[2] & 0x0E) << 10); int scrollX = Registers[8]; int scrollY = Registers[9]; // THE FIX: The bits are now in the correct order! bool lockColScroll = (Registers[0] & 0x80) != 0; // Bit 7: Locks right 8 columns (Fixes R-Type!) bool lockRowScroll = (Registers[0] & 0x40) != 0; // Bit 6: Locks top 2 rows (Fixes Bart!) bool maskLeftCol = (Registers[0] & 0x20) != 0; // Bit 5: Hides leftmost column (Fixes Sonic 2!) for (int screenX = 0; screenX < 256; screenX++) { // --- LEFT COLUMN MASKING (OVERSCAN CURTAIN) --- if (maskLeftCol && screenX < 8) { // REPLACE THE R/G/B MATH WITH THIS SINGLE LINE: int bgAddress = (screenY * 256) + screenX; FrameBuffer[bgAddress] = GetRgbColour(16, Registers[7] & 0x0F); _priorityBuffer[bgAddress] = true; continue; } // Apply Vertical Scrolling (R-Type HUD protection) int effectiveScrollY = scrollY; if (lockColScroll && screenX >= 192) effectiveScrollY = 0; int vdpY = (screenY + effectiveScrollY) % 224; int row = vdpY / 8; int tileY = vdpY % 8; // Apply Horizontal Scrolling (Bart's sky protection) int effectiveScrollX = scrollX; if (lockRowScroll && screenY < 16) effectiveScrollX = 0; int vdpX = (screenX - effectiveScrollX) & 0xFF; int col = vdpX / 8; int tileX = vdpX % 8; ushort nameTableAddr = (ushort)(nameTableBase + (row * 64) + (col * 2)); ushort tileData = (ushort)((VRAM[nameTableAddr + 1] << 8) | VRAM[nameTableAddr]); int tileIndex = tileData & 0x01FF; bool flipH = (tileData & 0x0200) != 0; bool flipV = (tileData & 0x0400) != 0; bool useSpritePalette = (tileData & 0x0800) != 0; bool priority = (tileData & 0x1000) != 0; 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]; int readX = flipH ? tileX : (7 - tileX); int colourIndex = ((bp0 >> readX) & 1) | (((bp1 >> readX) & 1) << 1) | (((bp2 >> readX) & 1) << 2) | (((bp3 >> readX) & 1) << 3); int paletteOffset = useSpritePalette ? 16 : 0; int finalColour = GetRgbColour(paletteOffset, colourIndex); int screenAddress = (screenY * 256) + screenX; // Draw background and reset priority mask for this exact pixel FrameBuffer[screenAddress] = finalColour; _priorityBuffer[screenAddress] = (priority && colourIndex != 0); } // --- 2. RENDER SPRITE LINE --- ushort satBaseAddress = (ushort)((Registers[5] & 0x7E) << 7); ushort spritePatternBase = (ushort)((Registers[6] & 0x04) << 11); bool is8x16 = (Registers[1] & 0x02) != 0; bool shiftSpritesLeft = (Registers[0] & 0x08) != 0; int spriteHeight = is8x16 ? 16 : 8; // Step A: Find the visible sprites for THIS specific line var visibleSprites = new System.Collections.Generic.List(); for (int i = 0; i < 64; i++) { byte y = VRAM[satBaseAddress + i]; if (y == 208) break; // End of Sprite List int spriteY = y + 1; // Physical hardware 1-pixel shift if (screenY >= spriteY && screenY < spriteY + spriteHeight) { visibleSprites.Add(i); // HARDWARE QUIRK: VDP stops drawing after 8 sprites on a single line! if (visibleSprites.Count == 8) break; } } // Step B: Draw them backward so Sprite 0 (highest priority) draws LAST and stays on top for (int v = visibleSprites.Count - 1; v >= 0; v--) { int i = visibleSprites[v]; byte y = VRAM[satBaseAddress + i]; byte x = VRAM[satBaseAddress + 0x80 + (i * 2)]; byte tileIndex = VRAM[satBaseAddress + 0x80 + (i * 2) + 1]; if (is8x16) tileIndex = (byte)(tileIndex & 0xFE); // Calculate which row of the sprite we are physically on int py = screenY - (y + 1); 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 (shiftSpritesLeft) screenX -= 8; if (screenX < 0 || screenX >= 256) continue; if (_priorityBuffer[(screenY * 256) + screenX]) continue; int shift = 7 - px; int colourIndex = ((bp0 >> shift) & 1) | (((bp1 >> shift) & 1) << 1) | (((bp2 >> shift) & 1) << 2) | (((bp3 >> shift) & 1) << 3); if (colourIndex == 0) continue; // REPLACE THE R/G/B MATH WITH THIS SINGLE LINE: FrameBuffer[(screenY * 256) + screenX] = GetRgbColour(16, colourIndex); } } } public int GetRgbColour(int paletteOffset, int colourIndex) { //Debug.WriteLine(_isGameGear); int r, g, b; if (IsGameGear) { // Game Gear: 2 bytes per colour. Format: ----BBBBGGGGRRRR int cramIndex = (paletteOffset + colourIndex) * 2; ushort ggcolour = (ushort)(ggCRAM[cramIndex] | (ggCRAM[cramIndex + 1] << 8)); // Extract 4-bit values (0-15) and map them to standard 8-bit values (0-255). // We multiply by 17 because 255 / 15 = 17! r = (ggcolour & 0x0F) * 17; g = ((ggcolour >> 4) & 0x0F) * 17; b = ((ggcolour >> 8) & 0x0F) * 17; } else { // Master System: 1 byte per colour. Format: --BBGGRR byte smscolour = smsCRAM[paletteOffset + colourIndex]; // Extract 2-bit values (0-3) and map them to standard 8-bit values (0-255). // We multiply by 85 because 255 / 3 = 85! r = (smscolour & 0x03) * 85; g = ((smscolour >> 2) & 0x03) * 85; b = ((smscolour >> 4) & 0x03) * 85; } return (255 << 24) | (r << 16) | (g << 8) | b; } public void SaveState(BinaryWriter bw) { bw.Write(VRAM); bw.Write(smsCRAM); bw.Write(ggCRAM); bw.Write(Registers); bw.Write(_isSecondControlByte); bw.Write(_controlWord); bw.Write(_readBuffer); bw.Write(_tStateCounter); bw.Write(_currentScanline); bw.Write(_lineCounter); bw.Write(_statusRegister); // ADD THESE: bw.Write(_latchedHScroll); bw.Write(_latchedVScroll); bw.Write(IsGameGear); } public void LoadState(BinaryReader br) { Array.Copy(br.ReadBytes(VRAM.Length), VRAM, VRAM.Length); Array.Copy(br.ReadBytes(smsCRAM.Length), smsCRAM, smsCRAM.Length); Array.Copy(br.ReadBytes(ggCRAM.Length), ggCRAM, ggCRAM.Length); Array.Copy(br.ReadBytes(Registers.Length), Registers, Registers.Length); _isSecondControlByte = br.ReadBoolean(); _controlWord = br.ReadUInt16(); _readBuffer = br.ReadByte(); _tStateCounter = br.ReadInt32(); _currentScanline = br.ReadInt32(); _lineCounter = br.ReadInt32(); _statusRegister = br.ReadByte(); // ADD THESE: _latchedHScroll = br.ReadInt32(); _latchedVScroll = br.ReadInt32(); IsGameGear = br.ReadBoolean(); } } }