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]; } } } }