430 lines
16 KiB
C#
430 lines
16 KiB
C#
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<int>();
|
|
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 Reset()
|
|
{
|
|
_tStateCounter = 0;
|
|
_currentScanline = 0;
|
|
_lineCounter = 0;
|
|
_statusRegister = 0x00;
|
|
_controlWord = 0;
|
|
_isSecondControlByte = false;
|
|
_readBuffer = 0;
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
} |