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