491 lines
17 KiB
C#
491 lines
17 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.ColecoVision
|
|
{
|
|
public sealed class TMS9918A : IVideoProvider
|
|
{
|
|
public byte[] VRAM = new byte[0x4000];
|
|
byte[] Registers = new byte[8];
|
|
byte StatusByte;
|
|
|
|
bool VdpWaitingForLatchByte = true;
|
|
byte VdpLatch;
|
|
ushort VdpAddress;
|
|
byte VdpBuffer;
|
|
int TmsMode;
|
|
|
|
bool Mode1Bit { get { return (Registers[1] & 16) > 0; } }
|
|
bool Mode2Bit { get { return (Registers[0] & 2) > 0; } }
|
|
bool Mode3Bit { get { return (Registers[1] & 8) > 0; } }
|
|
|
|
bool EnableDoubledSprites { get { return (Registers[1] & 1) > 0; } }
|
|
bool EnableLargeSprites { get { return (Registers[1] & 2) > 0; } }
|
|
bool EnableInterrupts { get { return (Registers[1] & 32) > 0; } }
|
|
bool DisplayOn { get { return (Registers[1] & 64) > 0; } }
|
|
bool Mode16k { get { return (Registers[1] & 128) > 0; } }
|
|
|
|
bool InterruptPending
|
|
{
|
|
get { return (StatusByte & 0x80) != 0; }
|
|
set { StatusByte = (byte)((StatusByte & ~0x02) | (value ? 0x80 : 0x00)); }
|
|
}
|
|
|
|
int ColorTableBase;
|
|
int PatternGeneratorBase;
|
|
int SpritePatternGeneratorBase;
|
|
int TmsPatternNameTableBase;
|
|
int TmsSpriteAttributeBase;
|
|
|
|
public void ExecuteFrame()
|
|
{
|
|
for (int scanLine = 0; scanLine < 262; scanLine++)
|
|
{
|
|
RenderScanline(scanLine);
|
|
|
|
if (scanLine == 192)
|
|
{
|
|
InterruptPending = true;
|
|
if (EnableInterrupts)
|
|
Cpu.NonMaskableInterrupt = true;
|
|
}
|
|
|
|
Cpu.ExecuteCycles(228);
|
|
}
|
|
}
|
|
|
|
public void WriteVdpControl(byte value)
|
|
{
|
|
if (VdpWaitingForLatchByte)
|
|
{
|
|
VdpLatch = value;
|
|
VdpWaitingForLatchByte = false;
|
|
VdpAddress = (ushort)((VdpAddress & 0x3F00) | value);
|
|
return;
|
|
}
|
|
|
|
VdpWaitingForLatchByte = true;
|
|
VdpAddress = (ushort)(((value & 63) << 8) | VdpLatch);
|
|
VdpAddress &= 0x3FFF;
|
|
switch (value & 0xC0)
|
|
{
|
|
case 0x00: // read VRAM
|
|
VdpBuffer = VRAM[VdpAddress];
|
|
VdpAddress++;
|
|
VdpAddress &= 0x3FFF;
|
|
break;
|
|
case 0x40: // write VRAM
|
|
break;
|
|
case 0x80: // VDP register write
|
|
int reg = value & 0x0F;
|
|
WriteRegister(reg, VdpLatch);
|
|
break;
|
|
}
|
|
}
|
|
|
|
public void WriteVdpData(byte value)
|
|
{
|
|
VdpWaitingForLatchByte = true;
|
|
VdpBuffer = value;
|
|
|
|
VRAM[VdpAddress] = value;
|
|
//if (!Mode16k)
|
|
// Console.WriteLine("VRAM written while not in 16k addressing mode!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
|
|
VdpAddress++;
|
|
VdpAddress &= 0x3FFF;
|
|
}
|
|
|
|
void WriteRegister(int reg, byte data)
|
|
{
|
|
if (reg >= 8) return;
|
|
|
|
Registers[reg] = data;
|
|
switch (reg)
|
|
{
|
|
case 0: // Mode Control Register 1
|
|
CheckVideoMode();
|
|
break;
|
|
case 1: // Mode Control Register 2
|
|
CheckVideoMode();
|
|
Cpu.NonMaskableInterrupt = (EnableInterrupts && InterruptPending);
|
|
break;
|
|
case 2: // Name Table Base Address
|
|
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
|
|
TmsSpriteAttributeBase = (Registers[5] << 7) & 0x3F80;
|
|
break;
|
|
case 6: // Sprite Pattern Generator Base Adderss
|
|
SpritePatternGeneratorBase = (Registers[6] << 11) & 0x3800;
|
|
break;
|
|
}
|
|
}
|
|
|
|
public byte ReadVdpStatus()
|
|
{
|
|
VdpWaitingForLatchByte = true;
|
|
byte returnValue = StatusByte;
|
|
StatusByte &= 0x1F;
|
|
Cpu.NonMaskableInterrupt = false;
|
|
|
|
return returnValue;
|
|
}
|
|
|
|
public byte ReadData()
|
|
{
|
|
VdpWaitingForLatchByte = true;
|
|
byte value = VdpBuffer;
|
|
VdpBuffer = VRAM[VdpAddress];
|
|
VdpAddress++;
|
|
VdpAddress &= 0x3FFF;
|
|
return value;
|
|
}
|
|
|
|
void CheckVideoMode()
|
|
{
|
|
if (Mode1Bit) TmsMode = 1;
|
|
else if (Mode2Bit) TmsMode = 2;
|
|
else if (Mode3Bit) TmsMode = 3;
|
|
else TmsMode = 0;
|
|
|
|
if (TmsMode == 1)
|
|
throw new Exception("TMS video mode 1! please tell vecna which game uses this!");
|
|
}
|
|
|
|
void RenderScanline(int scanLine)
|
|
{
|
|
if (scanLine >= 192)
|
|
return;
|
|
|
|
if (TmsMode == 2)
|
|
{
|
|
RenderBackgroundM2(scanLine);
|
|
RenderTmsSprites(scanLine);
|
|
}
|
|
else if (TmsMode == 0)
|
|
{
|
|
RenderBackgroundM0(scanLine);
|
|
RenderTmsSprites(scanLine);
|
|
}
|
|
else if (TmsMode == 3)
|
|
{
|
|
RenderBackgroundM3(scanLine);
|
|
RenderTmsSprites(scanLine);
|
|
}
|
|
// This may seem silly but if I ever implement mode 1, sprites are not rendered in that.
|
|
}
|
|
|
|
void RenderBackgroundM0(int scanLine)
|
|
{
|
|
if (DisplayOn == false)
|
|
{
|
|
Array.Clear(FrameBuffer, scanLine * 256, 256);
|
|
return;
|
|
}
|
|
|
|
int yc = scanLine / 8;
|
|
int yofs = scanLine % 8;
|
|
int FrameBufferOffset = scanLine * 256;
|
|
int PatternNameOffset = TmsPatternNameTableBase + (yc * 32);
|
|
int ScreenBGColor = PaletteTMS9918[Registers[7] & 0x0F];
|
|
|
|
for (int xc = 0; xc < 32; xc++)
|
|
{
|
|
int pn = VRAM[PatternNameOffset++];
|
|
int pv = VRAM[PatternGeneratorBase + (pn * 8) + yofs];
|
|
int colorEntry = VRAM[ColorTableBase + (pn / 8)];
|
|
int fgIndex = (colorEntry >> 4) & 0x0F;
|
|
int bgIndex = colorEntry & 0x0F;
|
|
int fgColor = fgIndex == 0 ? ScreenBGColor : PaletteTMS9918[fgIndex];
|
|
int bgColor = bgIndex == 0 ? ScreenBGColor : PaletteTMS9918[bgIndex];
|
|
|
|
FrameBuffer[FrameBufferOffset++] = ((pv & 0x80) > 0) ? fgColor : bgColor;
|
|
FrameBuffer[FrameBufferOffset++] = ((pv & 0x40) > 0) ? fgColor : bgColor;
|
|
FrameBuffer[FrameBufferOffset++] = ((pv & 0x20) > 0) ? fgColor : bgColor;
|
|
FrameBuffer[FrameBufferOffset++] = ((pv & 0x10) > 0) ? fgColor : bgColor;
|
|
FrameBuffer[FrameBufferOffset++] = ((pv & 0x08) > 0) ? fgColor : bgColor;
|
|
FrameBuffer[FrameBufferOffset++] = ((pv & 0x04) > 0) ? fgColor : bgColor;
|
|
FrameBuffer[FrameBufferOffset++] = ((pv & 0x02) > 0) ? fgColor : bgColor;
|
|
FrameBuffer[FrameBufferOffset++] = ((pv & 0x01) > 0) ? fgColor : bgColor;
|
|
}
|
|
}
|
|
|
|
void RenderBackgroundM2(int scanLine)
|
|
{
|
|
if (DisplayOn == false)
|
|
{
|
|
Array.Clear(FrameBuffer, scanLine * 256, 256);
|
|
return;
|
|
}
|
|
|
|
int yrow = scanLine / 8;
|
|
int yofs = scanLine % 8;
|
|
int FrameBufferOffset = scanLine * 256;
|
|
int PatternNameOffset = TmsPatternNameTableBase + (yrow * 32);
|
|
int PatternGeneratorOffset = (((Registers[4] & 4) << 11) & 0x2000);
|
|
int ColorOffset = (ColorTableBase & 0x2000);
|
|
int ScreenBGColor = PaletteTMS9918[Registers[7] & 0x0F];
|
|
|
|
for (int xc = 0; xc < 32; xc++)
|
|
{
|
|
int pn = VRAM[PatternNameOffset++] + ((yrow / 8) * 0x100);
|
|
int pv = VRAM[PatternGeneratorOffset + (pn * 8) + yofs];
|
|
int colorEntry = VRAM[ColorOffset + (pn * 8) + yofs];
|
|
int fgIndex = (colorEntry >> 4) & 0x0F;
|
|
int bgIndex = colorEntry & 0x0F;
|
|
int fgColor = fgIndex == 0 ? ScreenBGColor : PaletteTMS9918[fgIndex];
|
|
int bgColor = bgIndex == 0 ? ScreenBGColor : PaletteTMS9918[bgIndex];
|
|
|
|
FrameBuffer[FrameBufferOffset++] = ((pv & 0x80) > 0) ? fgColor : bgColor;
|
|
FrameBuffer[FrameBufferOffset++] = ((pv & 0x40) > 0) ? fgColor : bgColor;
|
|
FrameBuffer[FrameBufferOffset++] = ((pv & 0x20) > 0) ? fgColor : bgColor;
|
|
FrameBuffer[FrameBufferOffset++] = ((pv & 0x10) > 0) ? fgColor : bgColor;
|
|
FrameBuffer[FrameBufferOffset++] = ((pv & 0x08) > 0) ? fgColor : bgColor;
|
|
FrameBuffer[FrameBufferOffset++] = ((pv & 0x04) > 0) ? fgColor : bgColor;
|
|
FrameBuffer[FrameBufferOffset++] = ((pv & 0x02) > 0) ? fgColor : bgColor;
|
|
FrameBuffer[FrameBufferOffset++] = ((pv & 0x01) > 0) ? fgColor : bgColor;
|
|
}
|
|
}
|
|
|
|
void RenderBackgroundM3(int scanLine)
|
|
{
|
|
if (DisplayOn == false)
|
|
{
|
|
Array.Clear(FrameBuffer, scanLine * 256, 256);
|
|
return;
|
|
}
|
|
|
|
int yc = scanLine / 8;
|
|
bool top = (scanLine & 4) == 0; // am I in the top 4 pixels of an 8-pixel character?
|
|
int FrameBufferOffset = scanLine * 256;
|
|
int PatternNameOffset = TmsPatternNameTableBase + (yc * 32);
|
|
int ScreenBGColor = PaletteTMS9918[Registers[7] & 0x0F];
|
|
|
|
for (int xc = 0; xc < 32; xc++)
|
|
{
|
|
int pn = VRAM[PatternNameOffset++];
|
|
int pv = VRAM[PatternGeneratorBase + (pn * 8) + ((yc & 3) * 2) + (top ? 0 : 1)];
|
|
|
|
int lColorIndex = pv & 0xF;
|
|
int rColorIndex = pv >> 4;
|
|
int lColor = lColorIndex == 0 ? ScreenBGColor : PaletteTMS9918[lColorIndex];
|
|
int rColor = rColorIndex == 0 ? ScreenBGColor : PaletteTMS9918[rColorIndex];
|
|
|
|
FrameBuffer[FrameBufferOffset++] = lColor;
|
|
FrameBuffer[FrameBufferOffset++] = lColor;
|
|
FrameBuffer[FrameBufferOffset++] = lColor;
|
|
FrameBuffer[FrameBufferOffset++] = lColor;
|
|
FrameBuffer[FrameBufferOffset++] = rColor;
|
|
FrameBuffer[FrameBufferOffset++] = rColor;
|
|
FrameBuffer[FrameBufferOffset++] = rColor;
|
|
FrameBuffer[FrameBufferOffset ] = rColor;
|
|
}
|
|
}
|
|
|
|
byte[] ScanlinePriorityBuffer = new byte[256];
|
|
byte[] SpriteCollisionBuffer = new byte[256];
|
|
|
|
void RenderTmsSprites(int scanLine)
|
|
{
|
|
if (EnableDoubledSprites == false)
|
|
RenderTmsSpritesStandard(scanLine);
|
|
else
|
|
RenderTmsSpritesDouble(scanLine);
|
|
}
|
|
|
|
void RenderTmsSpritesStandard(int scanLine)
|
|
{
|
|
if (DisplayOn == false) return;
|
|
|
|
Array.Clear(ScanlinePriorityBuffer, 0, 256);
|
|
Array.Clear(SpriteCollisionBuffer, 0, 256);
|
|
|
|
bool LargeSprites = EnableLargeSprites;
|
|
|
|
int SpriteSize = 8;
|
|
if (LargeSprites) SpriteSize *= 2;
|
|
const int OneCellSize = 8;
|
|
|
|
int NumSpritesOnScanline = 0;
|
|
for (int i = 0; i < 32; i++)
|
|
{
|
|
int SpriteBase = TmsSpriteAttributeBase + (i * 4);
|
|
int y = VRAM[SpriteBase++];
|
|
int x = VRAM[SpriteBase++];
|
|
int Pattern = VRAM[SpriteBase++];
|
|
int Color = VRAM[SpriteBase];
|
|
|
|
if (y == 208) break; // terminator sprite
|
|
if (y > 224) y -= 256; // sprite Y wrap
|
|
y++; // inexplicably, sprites start on Y+1
|
|
if (y > scanLine || y + SpriteSize <= scanLine) continue; // sprite is not on this scanline
|
|
if ((Color & 0x80) > 0) x -= 32; // Early Clock adjustment
|
|
|
|
if (++NumSpritesOnScanline == 5)
|
|
{
|
|
StatusByte &= 0xE0; // Clear FS0-FS4 bits
|
|
StatusByte |= (byte)i; // set 5th sprite index
|
|
StatusByte |= 0x40; // set overflow bit
|
|
break;
|
|
}
|
|
|
|
if (LargeSprites) Pattern &= 0xFC; // 16x16 sprites forced to 4-byte alignment
|
|
int SpriteLine = scanLine - y;
|
|
|
|
// pv contains the VRAM byte holding the pattern data for this character at this scanline.
|
|
// each byte contains the pattern data for each the 8 pixels on this line.
|
|
// the bit-shift further down on PV pulls out the relevant horizontal pixel.
|
|
|
|
byte pv = VRAM[SpritePatternGeneratorBase + (Pattern * 8) + SpriteLine];
|
|
|
|
for (int xp = 0; xp < SpriteSize && x + xp < 256; xp++)
|
|
{
|
|
if (x + xp < 0) continue;
|
|
if (LargeSprites && xp == OneCellSize)
|
|
pv = VRAM[SpritePatternGeneratorBase + (Pattern * 8) + SpriteLine + 16];
|
|
|
|
if (Color != 0 && (pv & (1 << (7 - (xp & 7)))) > 0)
|
|
{
|
|
if (SpriteCollisionBuffer[x + xp] != 0)
|
|
StatusByte |= 0x20; // Set sprite collision flag
|
|
|
|
if (ScanlinePriorityBuffer[x + xp] == 0)
|
|
{
|
|
ScanlinePriorityBuffer[x + xp] = 1;
|
|
SpriteCollisionBuffer[x + xp] = 1;
|
|
FrameBuffer[(scanLine * 256) + x + xp] = PaletteTMS9918[Color & 0x0F];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void RenderTmsSpritesDouble(int scanLine)
|
|
{
|
|
if (DisplayOn == false) return;
|
|
|
|
Array.Clear(ScanlinePriorityBuffer, 0, 256);
|
|
Array.Clear(SpriteCollisionBuffer, 0, 256);
|
|
|
|
bool LargeSprites = EnableLargeSprites;
|
|
|
|
int SpriteSize = 8;
|
|
if (LargeSprites) SpriteSize *= 2;
|
|
SpriteSize *= 2; // because sprite magnification
|
|
const int OneCellSize = 16; // once 8-pixel cell, doubled, will take 16 pixels
|
|
|
|
int NumSpritesOnScanline = 0;
|
|
for (int i = 0; i < 32; i++)
|
|
{
|
|
int SpriteBase = TmsSpriteAttributeBase + (i * 4);
|
|
int y = VRAM[SpriteBase++];
|
|
int x = VRAM[SpriteBase++];
|
|
int Pattern = VRAM[SpriteBase++];
|
|
int Color = VRAM[SpriteBase];
|
|
|
|
if (y == 208) break; // terminator sprite
|
|
if (y > 224) y -= 256; // sprite Y wrap
|
|
y++; // inexplicably, sprites start on Y+1
|
|
if (y > scanLine || y + SpriteSize <= scanLine) continue; // sprite is not on this scanline
|
|
if ((Color & 0x80) > 0) x -= 32; // Early Clock adjustment
|
|
|
|
if (++NumSpritesOnScanline == 5)
|
|
{
|
|
StatusByte &= 0xE0; // Clear FS0-FS4 bits
|
|
StatusByte |= (byte)i; // set 5th sprite index
|
|
StatusByte |= 0x40; // set overflow bit
|
|
break;
|
|
}
|
|
|
|
if (LargeSprites) Pattern &= 0xFC; // 16x16 sprites forced to 4-byte alignment
|
|
int SpriteLine = scanLine - y;
|
|
SpriteLine /= 2; // because of sprite magnification
|
|
|
|
byte pv = VRAM[SpritePatternGeneratorBase + (Pattern * 8) + SpriteLine];
|
|
|
|
for (int xp = 0; xp < SpriteSize && x + xp < 256; xp++)
|
|
{
|
|
if (x + xp < 0) continue;
|
|
if (LargeSprites && xp == OneCellSize)
|
|
pv = VRAM[SpritePatternGeneratorBase + (Pattern * 8) + SpriteLine + 16];
|
|
|
|
if (Color != 0 && (pv & (1 << (7 - ((xp / 2) & 7)))) > 0) // xp/2 is due to sprite magnification
|
|
{
|
|
if (SpriteCollisionBuffer[x + xp] != 0)
|
|
StatusByte |= 0x20; // Set sprite collision flag
|
|
|
|
if (ScanlinePriorityBuffer[x + xp] == 0)
|
|
{
|
|
ScanlinePriorityBuffer[x + xp] = 1;
|
|
SpriteCollisionBuffer[x + xp] = 1;
|
|
FrameBuffer[(scanLine * 256) + x + xp] = PaletteTMS9918[Color & 0x0F];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Z80A Cpu;
|
|
public TMS9918A(Z80A cpu)
|
|
{
|
|
this.Cpu = cpu;
|
|
}
|
|
|
|
public int[] FrameBuffer = new int[256 * 192];
|
|
public int[] GetVideoBuffer() { return FrameBuffer; }
|
|
|
|
public int VirtualWidth { get { return 293; } }
|
|
public int VirtualHeight { get { return 192; } }
|
|
public int BufferWidth { get { return 256; } }
|
|
public int BufferHeight { get { return 192; } }
|
|
public int BackgroundColor { get { return 0; } }
|
|
|
|
int[] PaletteTMS9918 = new int[]
|
|
{
|
|
unchecked((int)0xFF000000),
|
|
unchecked((int)0xFF000000),
|
|
unchecked((int)0xFF47B73B),
|
|
unchecked((int)0xFF7CCF6F),
|
|
unchecked((int)0xFF5D4EFF),
|
|
unchecked((int)0xFF8072FF),
|
|
unchecked((int)0xFFB66247),
|
|
unchecked((int)0xFF5DC8ED),
|
|
unchecked((int)0xFFD76B48),
|
|
unchecked((int)0xFFFB8F6C),
|
|
unchecked((int)0xFFC3CD41),
|
|
unchecked((int)0xFFD3DA76),
|
|
unchecked((int)0xFF3E9F2F),
|
|
unchecked((int)0xFFB664C7),
|
|
unchecked((int)0xFFCCCCCC),
|
|
unchecked((int)0xFFFFFFFF)
|
|
};
|
|
|
|
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("Registers", ref Registers, false);
|
|
ser.Sync("VRAM", ref VRAM, false);
|
|
ser.EndSection();
|
|
|
|
if (ser.IsReader)
|
|
for (int i = 0; i < Registers.Length; i++)
|
|
WriteRegister(i, Registers[i]);
|
|
}
|
|
}
|
|
} |