Files
ParsonsMasterSystem2026/Core/Video/SmsVdp.cs

281 lines
11 KiB
C#

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!
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; // Top 2 rows (Y < 16)
bool lockColScroll = (Registers[0] & 0x40) != 0; // Right 8 columns (X >= 192)
for (int screenY = 0; screenY < 192; screenY++)
{
for (int screenX = 0; screenX < 256; screenX++)
{
// Apply Vertical Scrolling (Depends on X for column locking!)
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 (Depends on Y for row locking!)
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 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 Tile Index and Palette Info
int tileIndex = tileData & 0x01FF;
bool useSpritePalette = (tileData & 0x0800) != 0;
// 3. Find the tile data in VRAM
ushort tileAddress = (ushort)(tileIndex * 32);
// 4. Fetch the 4 bitplanes
byte bp0 = VRAM[tileAddress + (tileY * 4) + 0];
byte bp1 = VRAM[tileAddress + (tileY * 4) + 1];
byte bp2 = VRAM[tileAddress + (tileY * 4) + 2];
byte bp3 = VRAM[tileAddress + (tileY * 4) + 3];
// 5. Extract color index
int shift = 7 - tileX;
int colorIndex = ((bp0 >> shift) & 1) |
(((bp1 >> shift) & 1) << 1) |
(((bp2 >> shift) & 1) << 2) |
(((bp3 >> shift) & 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;
FrameBuffer[(screenY * 256) + screenX] = (255 << 24) | (r << 16) | (g << 8) | b;
}
}
}
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;
// 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;
}
}
}
}
}
}