194 lines
7.5 KiB
C#
194 lines
7.5 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!
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
} |