BizHawk/BizHawk.Emulation.Cores/Consoles/Sega/SMS/VDP.cs

476 lines
13 KiB
C#

using System;
using System.Globalization;
using System.IO;
using BizHawk.Common;
using BizHawk.Emulation.Common;
using BizHawk.Emulation.Cores.Components.Z80;
namespace BizHawk.Emulation.Cores.Sega.MasterSystem
{
public enum VdpMode { SMS, GameGear }
// Emulates the Texas Instruments TMS9918 VDP.
public sealed partial class VDP : IVideoProvider
{
// VDP State
public byte[] VRAM = new byte[0x4000]; //16kb video RAM
public byte[] CRAM; // SMS = 32 bytes, GG = 64 bytes CRAM
public byte[] Registers = new byte[] { 0x06, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xF0, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00 };
public byte StatusByte;
const int Command_VramRead = 0x00;
const int Command_VramWrite = 0x40;
const int Command_RegisterWrite = 0x80;
const int Command_CramWrite = 0xC0;
bool VdpWaitingForLatchByte = true;
byte VdpLatch;
byte VdpBuffer;
ushort VdpAddress;
byte VdpCommand;
int TmsMode = 4;
bool VIntPending;
bool HIntPending;
int lineIntLinesRemaining;
SMS Sms;
VdpMode mode;
DisplayType DisplayType = DisplayType.NTSC;
Z80A Cpu;
public bool SpriteLimit;
public int IPeriod = 228;
public VdpMode VdpMode { get { return mode; } }
public int FrameHeight = 192;
public int ScanLine;
public int[] FrameBuffer = new int[256 * 192];
public int[] GameGearFrameBuffer = new int[160 * 144];
public bool Mode1Bit { get { return (Registers[1] & 16) > 0; } }
public bool Mode2Bit { get { return (Registers[0] & 2) > 0; } }
public bool Mode3Bit { get { return (Registers[1] & 8) > 0; } }
public bool Mode4Bit { get { return (Registers[0] & 4) > 0; } }
public bool ShiftSpritesLeft8Pixels { get { return (Registers[0] & 8) > 0; } }
public bool EnableLineInterrupts { get { return (Registers[0] & 16) > 0; } }
public bool LeftBlanking { get { return (Registers[0] & 32) > 0; } }
public bool HorizScrollLock { get { return (Registers[0] & 64) > 0; } }
public bool VerticalScrollLock { get { return (Registers[0] & 128) > 0; } }
public bool EnableDoubledSprites { get { return (Registers[1] & 1) > 0; } }
public bool EnableLargeSprites { get { return (Registers[1] & 2) > 0; } }
public bool EnableFrameInterrupts { get { return (Registers[1] & 32) > 0; } }
public bool DisplayOn { get { return (Registers[1] & 64) > 0; } }
public int SpriteAttributeTableBase { get { return ((Registers[5] >> 1) << 8) & 0x3FFF; } }
public int SpriteTileBase { get { return (Registers[6] & 4) > 0 ? 256 : 0; } }
public byte BackdropColor { get { return (byte)(16 + (Registers[7] & 15)); } }
int NameTableBase;
int ColorTableBase;
int PatternGeneratorBase;
int SpritePatternGeneratorBase;
int TmsPatternNameTableBase;
int TmsSpriteAttributeBase;
// preprocessed state assist stuff.
public int[] Palette = new int[32];
public byte[] PatternBuffer = new byte[0x8000];
byte[] ScanlinePriorityBuffer = new byte[256];
byte[] SpriteCollisionBuffer = new byte[256];
static readonly byte[] SMSPalXlatTable = { 0, 85, 170, 255 };
static readonly byte[] GGPalXlatTable = { 0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255 };
public VDP(SMS sms, Z80A cpu, VdpMode mode, DisplayType displayType)
{
Sms = sms;
Cpu = cpu;
this.mode = mode;
if (mode == VdpMode.SMS) CRAM = new byte[32];
if (mode == VdpMode.GameGear) CRAM = new byte[64];
DisplayType = displayType;
NameTableBase = CalcNameTableBase();
}
public byte ReadData()
{
VdpWaitingForLatchByte = true;
byte value = VdpBuffer;
VdpBuffer = VRAM[VdpAddress & 0x3FFF];
VdpAddress++;
return value;
}
public byte ReadVdpStatus()
{
VdpWaitingForLatchByte = true;
byte returnValue = StatusByte;
StatusByte &= 0x1F;
HIntPending = false;
VIntPending = false;
Cpu.Interrupt = false;
return returnValue;
}
public byte ReadVLineCounter()
{
if (DisplayType == DisplayType.NTSC)
{
if (FrameHeight == 192)
return VLineCounterTableNTSC192[ScanLine];
if (FrameHeight == 224)
return VLineCounterTableNTSC224[ScanLine];
return VLineCounterTableNTSC240[ScanLine];
}
else
{ // PAL
if (FrameHeight == 192)
return VLineCounterTablePAL192[ScanLine];
if (FrameHeight == 224)
return VLineCounterTablePAL224[ScanLine];
return VLineCounterTablePAL240[ScanLine];
}
}
public void WriteVdpControl(byte value)
{
if (VdpWaitingForLatchByte)
{
VdpLatch = value;
VdpWaitingForLatchByte = false;
VdpAddress = (ushort)((VdpAddress & 0xFF00) | value);
return;
}
VdpWaitingForLatchByte = true;
VdpAddress = (ushort)(((value & 63) << 8) | VdpLatch);
switch (value & 0xC0)
{
case 0x00: // read VRAM
VdpCommand = Command_VramRead;
VdpBuffer = VRAM[VdpAddress & 0x3FFF];
VdpAddress++;
break;
case 0x40: // write VRAM
VdpCommand = Command_VramWrite;
break;
case 0x80: // VDP register write
VdpCommand = Command_RegisterWrite;
int reg = value & 0x0F;
WriteRegister(reg, VdpLatch);
break;
case 0xC0: // write CRAM / modify palette
VdpCommand = Command_CramWrite;
break;
}
}
public void WriteVdpData(byte value)
{
VdpWaitingForLatchByte = true;
VdpBuffer = value;
if (VdpCommand == Command_CramWrite)
{
// Write Palette / CRAM
int mask = VdpMode == VdpMode.SMS ? 0x1F : 0x3F;
CRAM[VdpAddress & mask] = value;
UpdatePrecomputedPalette();
}
else
{
// Write VRAM and update pre-computed pattern buffer.
UpdatePatternBuffer((ushort)(VdpAddress & 0x3FFF), value);
VRAM[VdpAddress & 0x3FFF] = value;
}
VdpAddress++;
}
public void UpdatePrecomputedPalette()
{
if (mode == VdpMode.SMS)
{
for (int i = 0; i < 32; i++)
{
byte value = CRAM[i];
byte r = SMSPalXlatTable[(value & 0x03)];
byte g = SMSPalXlatTable[(value & 0x0C) >> 2];
byte b = SMSPalXlatTable[(value & 0x30) >> 4];
Palette[i] = Colors.ARGB(r, g, b);
}
}
else
{ // GameGear
for (int i = 0; i < 32; i++)
{
ushort value = (ushort)((CRAM[(i * 2) + 1] << 8) | CRAM[(i * 2) + 0]);
byte r = GGPalXlatTable[(value & 0x000F)];
byte g = GGPalXlatTable[(value & 0x00F0) >> 4];
byte b = GGPalXlatTable[(value & 0x0F00) >> 8];
Palette[i] = Colors.ARGB(r, g, b);
}
}
}
public int CalcNameTableBase()
{
if (FrameHeight == 192)
return 1024 * (Registers[2] & 0x0E);
return (1024 * (Registers[2] & 0x0C)) + 0x0700;
}
void CheckVideoMode()
{
if (Mode4Bit == false) // check old TMS modes
{
if (Mode1Bit) TmsMode = 1;
else if (Mode2Bit) TmsMode = 2;
else if (Mode3Bit) TmsMode = 3;
else TmsMode = 0;
}
else if (Mode4Bit && Mode2Bit) // if Mode4 and Mode2 set, then check extension modes
{
TmsMode = 4;
switch (Registers[1] & 0x18)
{
case 0x00:
case 0x18: // 192-line mode
if (FrameHeight != 192)
{
FrameHeight = 192;
FrameBuffer = new int[256 * 192];
NameTableBase = CalcNameTableBase();
}
break;
case 0x10: // 224-line mode
if (FrameHeight != 224)
{
FrameHeight = 224;
FrameBuffer = new int[256 * 224];
NameTableBase = CalcNameTableBase();
}
break;
case 0x08: // 240-line mode
if (FrameHeight != 240)
{
FrameHeight = 240;
FrameBuffer = new int[256 * 240];
NameTableBase = CalcNameTableBase();
}
break;
}
}
else
{ // default to standard 192-line mode4
TmsMode = 4;
if (FrameHeight != 192)
{
FrameHeight = 192;
FrameBuffer = new int[256 * 192];
NameTableBase = CalcNameTableBase();
}
}
}
void WriteRegister(int reg, byte data)
{
Registers[reg] = data;
switch (reg)
{
case 0: // Mode Control Register 1
CheckVideoMode();
Cpu.Interrupt = (EnableLineInterrupts && HIntPending);
break;
case 1: // Mode Control Register 2
CheckVideoMode();
Cpu.Interrupt = (EnableFrameInterrupts && VIntPending);
break;
case 2: // Name Table Base Address
NameTableBase = CalcNameTableBase();
TmsPatternNameTableBase = (Registers[2] << 10) & 0x3C00;
break;
case 3: // Color Table Base Address
ColorTableBase = (Registers[3] << 6) & 0x3FC0;
break;
case 4: // Pattern Generator Base Address
PatternGeneratorBase = (Registers[4] << 11) & 0x3800;
break;
case 5: // Sprite Attribute Table Base Address
// ??? should I move from my property to precalculated?
TmsSpriteAttributeBase = (Registers[5] << 7) & 0x3F80;
break;
case 6: // Sprite Pattern Generator Base Adderss
SpritePatternGeneratorBase = (Registers[6] << 11) & 0x3800;
break;
}
}
static readonly byte[] pow2 = { 1, 2, 4, 8, 16, 32, 64, 128 };
void UpdatePatternBuffer(ushort address, byte value)
{
// writing one byte affects 8 pixels due to stupid planar storage.
for (int i = 0; i < 8; i++)
{
byte colorBit = pow2[address % 4];
byte sourceBit = pow2[7 - i];
ushort dest = (ushort)(((address & 0xFFFC) * 2) + i);
if ((value & sourceBit) > 0) // setting bit
PatternBuffer[dest] |= colorBit;
else // clearing bit
PatternBuffer[dest] &= (byte)~colorBit;
}
}
void ProcessFrameInterrupt()
{
if (ScanLine == FrameHeight + 1)
{
StatusByte |= 0x80;
VIntPending = true;
}
if (VIntPending && EnableFrameInterrupts)
Cpu.Interrupt = true;
}
void ProcessLineInterrupt()
{
if (ScanLine <= FrameHeight)
{
if (lineIntLinesRemaining-- <= 0)
{
HIntPending = true;
if (EnableLineInterrupts)
Cpu.Interrupt = true;
lineIntLinesRemaining = Registers[0x0A];
}
return;
}
// else we're outside the active display period
lineIntLinesRemaining = Registers[0x0A];
}
public void ExecFrame(bool render)
{
int scanlinesPerFrame = DisplayType == DisplayType.NTSC ? 262 : 313;
SpriteLimit = Sms.Settings.SpriteLimit;
for (ScanLine = 0; ScanLine < scanlinesPerFrame; ScanLine++)
{
RenderCurrentScanline(render);
ProcessFrameInterrupt();
ProcessLineInterrupt();
Cpu.ExecuteCycles(IPeriod);
if (ScanLine == scanlinesPerFrame - 1)
ProcessGGScreen();
}
}
internal void RenderCurrentScanline(bool render)
{
// only mode 4 supports frameskip. deal with it
if (TmsMode == 4)
{
if (render)
RenderBackgroundCurrentLine(Sms.Settings.DispBG);
if (EnableDoubledSprites)
RenderSpritesCurrentLineDoubleSize(Sms.Settings.DispOBJ & render);
else
RenderSpritesCurrentLine(Sms.Settings.DispOBJ & render);
RenderLineBlanking(render);
}
else if (TmsMode == 2)
{
RenderBackgroundM2(Sms.Settings.DispBG);
RenderTmsSprites(Sms.Settings.DispOBJ);
}
else if (TmsMode == 0)
{
RenderBackgroundM0(Sms.Settings.DispBG);
RenderTmsSprites(Sms.Settings.DispOBJ);
}
}
public void SyncState(Serializer ser)
{
ser.BeginSection("VDP");
ser.Sync("StatusByte", ref StatusByte);
ser.Sync("WaitingForLatchByte", ref VdpWaitingForLatchByte);
ser.Sync("Latch", ref VdpLatch);
ser.Sync("ReadBuffer", ref VdpBuffer);
ser.Sync("VdpAddress", ref VdpAddress);
ser.Sync("Command", ref VdpCommand);
ser.Sync("HIntPending", ref HIntPending);
ser.Sync("VIntPending", ref VIntPending);
ser.Sync("LineIntLinesRemaining", ref lineIntLinesRemaining);
ser.Sync("Registers", ref Registers, false);
ser.Sync("CRAM", ref CRAM, false);
ser.Sync("VRAM", ref VRAM, false);
ser.EndSection();
if (ser.IsReader)
{
for (int i = 0; i < Registers.Length; i++)
WriteRegister(i, Registers[i]);
for (ushort i = 0; i < VRAM.Length; i++)
UpdatePatternBuffer(i, VRAM[i]);
UpdatePrecomputedPalette();
}
}
public int[] GetVideoBuffer()
{
if (mode == VdpMode.SMS || Sms.Settings.ShowClippedRegions)
return FrameBuffer;
return GameGearFrameBuffer;
}
public int VirtualWidth
{
get
{
if (mode == MasterSystem.VdpMode.SMS)
return 293;
else if (Sms.Settings.ShowClippedRegions)
return 256;
else
return 160;
}
}
public int VirtualHeight { get { return BufferHeight; } }
public int BufferWidth
{
get
{
if (mode == VdpMode.SMS || Sms.Settings.ShowClippedRegions)
return 256;
return 160; // GameGear
}
}
public int BufferHeight
{
get
{
if (mode == VdpMode.SMS || Sms.Settings.ShowClippedRegions)
return FrameHeight;
return 144; // GameGear
}
}
public int BackgroundColor
{
get { return Palette[BackdropColor]; }
}
}
}