/* * Maria.cs * * The Maria display device. * * Derived from much of Dan Boris' work with 7800 emulation * within the MESS emulator. * * Thanks to Matthias Luedtke for correcting * the BuildLineRAM320B() method to correspond closer to real hardware. * (Matthias credited an insightful response by Eckhard Stolberg on a forum on * Atari Age circa June 2005.) * * Copyright © 2004-2012 Mike Murphy * */ using System; namespace EMU7800.Core { public sealed class Maria : IDevice { #region Constants const int INPTCTRL= 0x01, // Write: input port control (VBLANK in TIA) INPT0 = 0x08, // Read pot port: D7 INPT1 = 0x09, // Read pot port: D7 INPT2 = 0x0a, // Read pot port: D7 INPT3 = 0x0b, // Read pot port: D7 INPT4 = 0x0c, // Read P1 joystick trigger: D7 INPT5 = 0x0d, // Read P2 joystick trigger: D7 AUDC0 = 0x15, // Write: audio control 0 (D3-0) AUDC1 = 0x16, // Write: audio control 1 (D4-0) AUDF0 = 0x17, // Write: audio frequency 0 (D4-0) AUDF1 = 0x18, // Write: audio frequency 1 (D3-0) AUDV0 = 0x19, // Write: audio volume 0 (D3-0) AUDV1 = 0x1a, // Write: audio volume 1 (D3-0) BACKGRND= 0x20, // Background color P0C1 = 0x21, // Palette 0 - color 1 P0C2 = 0x22, // Palette 0 - color 2 P0C3 = 0x23, // Palette 0 - color 3 WSYNC = 0x24, // Wait for sync P1C1 = 0x25, // Palette 1 - color 1 P1C2 = 0x26, // Palette 1 - color 2 P1C3 = 0x27, // Palette 1 - color 3 MSTAT = 0x28, // Maria status P2C1 = 0x29, // Palette 2 - color 1 P2C2 = 0x2a, // Palette 2 - color 2 P2C3 = 0x2b, // Palette 2 - color 3 DPPH = 0x2c, // Display list list point high P3C1 = 0x2d, // Palette 3 - color 1 P3C2 = 0x2e, // Palette 3 - color 2 P3C3 = 0x2f, // Palette 3 - color 3 DPPL = 0x30, // Display list list point low P4C1 = 0x31, // Palette 4 - color 1 P4C2 = 0x32, // Palette 4 - color 2 P4C3 = 0x33, // Palette 4 - color 3 CHARBASE= 0x34, // Character base address P5C1 = 0x35, // Palette 5 - color 1 P5C2 = 0x36, // Palette 5 - color 2 P5C3 = 0x37, // Palette 5 - color 3 OFFSET = 0x38, // Future expansion (store zero here) P6C1 = 0x39, // Palette 6 - color 1 P6C2 = 0x3a, // Palette 6 - color 2 P6C3 = 0x3b, // Palette 6 - color 3 CTRL = 0x3c, // Maria control register P7C1 = 0x3d, // Palette 7 - color 1 P7C2 = 0x3e, // Palette 7 - color 2 P7C3 = 0x3f; // Palette 7 - color 3 const int CPU_TICKS_PER_AUDIO_SAMPLE = 57; #endregion #region Fields readonly byte[] LineRAM = new byte[0x200]; readonly byte[] Registers = new byte[0x40]; readonly Machine7800 M; readonly TIASound TIASound; ulong _startOfFrameCpuClock; int Scanline { get { return (int)(M.CPU.Clock - _startOfFrameCpuClock) / 114; } } int HPos { get { return (int)(M.CPU.Clock - _startOfFrameCpuClock) % 114; } } int FirstVisibleScanline, LastVisibleScanline; int _dmaClocks; bool _isPal; // For lightgun emulation. // Transient state, serialization unnecessary. ulong _lightgunFirstSampleCpuClock; int _lightgunFrameSamples, _lightgunSampledScanline, _lightgunSampledVisibleHpos; bool WM; ushort DLL; ushort DL; int Offset; int Holey; int Width; byte HPOS; int PaletteNo; bool INDMode; bool CtrlLock; // MARIA CNTL bool DMAEnabled; bool ColorKill; bool CWidth; bool BCntl; bool Kangaroo; byte RM; #endregion #region Public Members public void Reset() { CtrlLock = false; DMAEnabled = false; ColorKill = false; CWidth = false; BCntl = false; Kangaroo = false; RM = 0; TIASound.Reset(); Log("{0} reset", this); } public byte this[ushort addr] { get { return peek(addr); } set { poke(addr, value); } } public override string ToString() { return GetType().Name; } public void StartFrame() { _startOfFrameCpuClock = M.CPU.Clock + (ulong)(M.CPU.RunClocks / M.CPU.RunClocksMultiple); _lightgunFirstSampleCpuClock = 0; AssertDebug(M.CPU.RunClocks <= 0 && (M.CPU.RunClocks % M.CPU.RunClocksMultiple) == 0); AssertDebug((_startOfFrameCpuClock % (114 * (ulong)M.FrameBuffer.Scanlines)) == 0); TIASound.StartFrame(); } public int DoDMAProcessing() { OutputLineRAM(); var sl = Scanline; if (!DMAEnabled || sl < FirstVisibleScanline || sl >= LastVisibleScanline) return 0; _dmaClocks = 0; if (DMAEnabled && sl == FirstVisibleScanline) { // DMA TIMING: End of VBLANK: DMA Startup + long shutdown _dmaClocks += 15; DLL = WORD(Registers[DPPL], Registers[DPPH]); ConsumeNextDLLEntry(); } // DMA TIMING: DMA Startup, 5-9 cycles _dmaClocks += 5; BuildLineRAM(); if (--Offset < 0) { ConsumeNextDLLEntry(); // DMA TIMING: DMA Shutdown: Last line of zone, 10-13 cycles _dmaClocks += 10; } else { // DMA TIMING: DMA Shutdown: Other line of zone, 4-7 cycles _dmaClocks += 4; } return _dmaClocks; } public void EndFrame() { TIASound.EndFrame(); } #endregion #region Constructors private Maria() { } public Maria(Machine7800 m, int scanlines) { if (m == null) throw new ArgumentNullException("m"); M = m; InitializeVisibleScanlineValues(scanlines); TIASound = new TIASound(M, CPU_TICKS_PER_AUDIO_SAMPLE); } #endregion #region Scanline Builders void BuildLineRAM() { var dl = DL; // Iterate through Display List (DL) while (true) { var modeByte = DmaRead(dl + 1); if ((modeByte & 0x5f) == 0) break; INDMode = false; ushort graphaddr; if ((modeByte & 0x1f) == 0) { // Extended DL header var dl0 = DmaRead(dl++); // low address var dl1 = DmaRead(dl++); // mode var dl2 = DmaRead(dl++); // high address var dl3 = DmaRead(dl++); // palette(7-5)/width(4-0) var dl4 = DmaRead(dl++); // horizontal position graphaddr = WORD(dl0, dl2); WM = (dl1 & 0x80) != 0; INDMode = (dl1 & 0x20) != 0; PaletteNo = (dl3 & 0xe0) >> 3; Width = (~dl3 & 0x1f) + 1; HPOS = dl4; // DMA TIMING: DL 5 byte header _dmaClocks += 10; } else { // Normal DL header var dl0 = DmaRead(dl++); // low address var dl1 = DmaRead(dl++); // palette(7-5)/width(4-0) var dl2 = DmaRead(dl++); // high address var dl3 = DmaRead(dl++); // horizontal position graphaddr = WORD(dl0, dl2); PaletteNo = (dl1 & 0xe0) >> 3; Width = (~dl1 & 0x1f) + 1; HPOS = dl3; // DMA TIMING: DL 4 byte header _dmaClocks += 8; } // DMA TIMING: Graphic reads if (RM != 1) _dmaClocks += (Width * (INDMode ? (CWidth ? 9 : 6) : 3)); switch (RM) { case 0: if (WM) BuildLineRAM160B(graphaddr); else BuildLineRAM160A(graphaddr); break; case 1: continue; case 2: if (WM) BuildLineRAM320B(graphaddr); else BuildLineRAM320D(graphaddr); break; case 3: if (WM) BuildLineRAM320C(graphaddr); else BuildLineRAM320A(graphaddr); break; } } } void BuildLineRAM160A(ushort graphaddr) { var indbytes = (INDMode && CWidth) ? 2 : 1; var hpos = HPOS << 1; var dataaddr = (ushort)(graphaddr + (Offset << 8)); for (var i=0; i < Width; i++) { if (INDMode) { dataaddr = WORD(DmaRead(graphaddr + i), Registers[CHARBASE] + Offset); } for (var j=0; j < indbytes; j++) { if (Holey == 0x02 && ((dataaddr & 0x9000) == 0x9000) || Holey == 0x01 && ((dataaddr & 0x8800) == 0x8800)) { hpos += 8; dataaddr++; AssertDebug(!Kangaroo); continue; } int d = DmaRead(dataaddr++); var c = (d & 0xc0) >> 6; if (c != 0) { var val = (byte)(PaletteNo | c); LineRAM[hpos & 0x1ff] = LineRAM[(hpos+1) & 0x1ff] = val; } AssertDebug(c != 0 || c == 0 && !Kangaroo); hpos += 2; c = (d & 0x30) >> 4; if (c != 0) { var val = (byte)(PaletteNo | c); LineRAM[hpos & 0x1ff] = LineRAM[(hpos+1) & 0x1ff] = val; } AssertDebug(c != 0 || c == 0 && !Kangaroo); hpos += 2; c = (d & 0x0c) >> 2; if (c != 0) { var val = (byte)(PaletteNo | c); LineRAM[hpos & 0x1ff] = LineRAM[(hpos+1) & 0x1ff] = val; } AssertDebug(c != 0 || c == 0 && !Kangaroo); hpos += 2; c = d & 0x03; if (c != 0) { var val = (byte)(PaletteNo | c); LineRAM[hpos & 0x1ff] = LineRAM[(hpos+1) & 0x1ff] = val; } AssertDebug(c != 0 || c == 0 && !Kangaroo); hpos += 2; } } } void BuildLineRAM160B(ushort graphaddr) { var indbytes = (INDMode && CWidth) ? 2 : 1; var hpos = HPOS << 1; var dataaddr = (ushort)(graphaddr + (Offset << 8)); for (var i = 0; i < Width; i++) { if (INDMode) { dataaddr = WORD(DmaRead(graphaddr + i), Registers[CHARBASE] + Offset); } for (var j=0; j < indbytes; j++) { if (Holey == 0x02 && ((dataaddr & 0x9000) == 0x9000) || Holey == 0x01 && ((dataaddr & 0x8800) == 0x8800)) { hpos += 4; dataaddr++; continue; } int d = DmaRead(dataaddr++); var c = (d & 0xc0) >> 6; if (c != 0) { var p = ((PaletteNo >> 2) & 0x04) | ((d & 0x0c) >> 2); var val = (byte)((p << 2) | c); LineRAM[hpos & 0x1ff] = LineRAM[(hpos+1) & 0x1ff] = val; } else if (Kangaroo) { LineRAM[hpos & 0x1ff] = LineRAM[(hpos+1) & 0x1ff] = 0; } hpos += 2; c = (d & 0x30) >> 4; if (c != 0) { var p = ((PaletteNo >> 2) & 0x04) | (d & 0x03); var val = (byte)((p << 2) | c); LineRAM[hpos & 0x1ff] = LineRAM[(hpos+1) & 0x1ff] = val; } else if (Kangaroo) { LineRAM[hpos & 0x1ff] = LineRAM[(hpos+1) & 0x1ff] = 0; } hpos += 2; } } } void BuildLineRAM320A(ushort graphaddr) { var color = (byte)(PaletteNo | 2); var hpos = HPOS << 1; var dataaddr = (ushort)(graphaddr + (Offset << 8)); AssertDebug(!CWidth); for (var i = 0; i < Width; i++) { if (INDMode) { dataaddr = WORD(DmaRead(graphaddr + i), Registers[CHARBASE] + Offset); } if (Holey == 0x02 && ((dataaddr & 0x9000) == 0x9000) || Holey == 0x01 && ((dataaddr & 0x8800) == 0x8800)) { hpos += 8; dataaddr++; continue; } int d = DmaRead(dataaddr++); if ((d & 0x80) != 0) LineRAM[hpos & 0x1ff] = color; else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; hpos++; if ((d & 0x40) != 0) LineRAM[hpos & 0x1ff] = color; else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; hpos++; if ((d & 0x20) != 0) LineRAM[hpos & 0x1ff] = color; else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; hpos++; if ((d & 0x10) != 0) LineRAM[hpos & 0x1ff] = color; else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; hpos++; if ((d & 0x08) != 0) LineRAM[hpos & 0x1ff] = color; else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; hpos++; if ((d & 0x04) != 0) LineRAM[hpos & 0x1ff] = color; else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; hpos++; if ((d & 0x02) != 0) LineRAM[hpos & 0x1ff] = color; else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; hpos++; if ((d & 0x01) != 0) LineRAM[hpos & 0x1ff] = color; else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; hpos++; } } void BuildLineRAM320B(ushort graphaddr) { var indbytes = (INDMode && CWidth) ? 2 : 1; var hpos = HPOS << 1; var dataaddr = (ushort)(graphaddr + (Offset << 8)); for (var i = 0; i < Width; i++) { if (INDMode) { dataaddr = WORD(DmaRead(graphaddr + i), Registers[CHARBASE] + Offset); } for (var j=0; j < indbytes; j++) { if (Holey == 0x02 && ((dataaddr & 0x9000) == 0x9000) || Holey == 0x01 && ((dataaddr & 0x8800) == 0x8800)) { hpos += 4; dataaddr++; continue; } int d = DmaRead(dataaddr++); var c = ((d & 0x80) >> 6) | ((d & 0x08) >> 3); if (c != 0) { if ((d & 0xc0) != 0 || Kangaroo) LineRAM[hpos & 0x1ff] = (byte)(PaletteNo | c); } else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; else if ((d & 0xcc) != 0) LineRAM[hpos & 0x1ff] = 0; hpos++; c = ((d & 0x40) >> 5) | ((d & 0x04) >> 2); if (c != 0) { if ((d & 0xc0) != 0 || Kangaroo) LineRAM[hpos & 0x1ff] = (byte)(PaletteNo | c); } else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; else if ((d & 0xcc) != 0) LineRAM[hpos & 0x1ff] = 0; hpos++; c = ((d & 0x20) >> 4) | ((d & 0x02) >> 1); if (c != 0) { if ((d & 0x30) != 0 || Kangaroo) LineRAM[hpos & 0x1ff] = (byte)(PaletteNo | c); } else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; else if ((d & 0x33) != 0) LineRAM[hpos & 0x1ff] = 0; hpos++; c = ((d & 0x10) >> 3) | (d & 0x01); if (c != 0) { if ((d & 0x30) != 0 || Kangaroo) LineRAM[hpos & 0x1ff] = (byte)(PaletteNo | c); } else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; else if ((d & 0x33) != 0) LineRAM[hpos & 0x1ff] = 0; hpos++; } } } void BuildLineRAM320C(ushort graphaddr) { var hpos = HPOS << 1; var dataaddr = (ushort)(graphaddr + (Offset << 8)); AssertDebug(!CWidth); for (var i = 0; i < Width; i++) { if (INDMode) { dataaddr = WORD(DmaRead(graphaddr + i), Registers[CHARBASE] + Offset); } if (Holey == 0x02 && ((dataaddr & 0x9000) == 0x9000) || Holey == 0x01 && ((dataaddr & 0x8800) == 0x8800)) { hpos += 4; dataaddr++; continue; } int d = DmaRead(dataaddr++); var color = (byte)(((((d & 0x0c) >> 2) | ((PaletteNo >> 2) & 0x04)) << 2) | 2); if ((d & 0x80) != 0) LineRAM[hpos & 0x1ff] = color; else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; hpos++; if ((d & 0x40) != 0) LineRAM[hpos & 0x1ff] = color; else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; hpos++; color = (byte)((((d & 0x03) | ((PaletteNo >> 2) & 0x04)) << 2) | 2); if ((d & 0x20) != 0) LineRAM[hpos & 0x1ff] = color; else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; hpos++; if ((d & 0x10) != 0) LineRAM[hpos & 0x1ff] = color; else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; hpos++; } } void BuildLineRAM320D(ushort graphaddr) { var indbytes = (INDMode && CWidth) ? 2 : 1; var hpos = HPOS << 1; var dataaddr = (ushort)(graphaddr + (Offset << 8)); for (var i = 0; i < Width; i++) { if (INDMode) { dataaddr = WORD(DmaRead(graphaddr + i), Registers[CHARBASE] + Offset); } for (var j=0; j < indbytes; j++) { if (Holey == 0x02 && ((dataaddr & 0x9000) == 0x9000) || Holey == 0x01 && ((dataaddr & 0x8800) == 0x8800)) { hpos += 8; dataaddr++; continue; } int d = DmaRead(dataaddr++); var c = ((d & 0x80) >> 6) | (((PaletteNo >> 2) & 2) >> 1); if (c != 0) LineRAM[hpos & 0x1ff] = (byte)((PaletteNo & 0x10) | c); else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; hpos++; c = ((d & 0x40) >> 5) | ((PaletteNo >> 2) & 1); if (c != 0) LineRAM[hpos & 0x1ff] = (byte)((PaletteNo & 0x10) | c); else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; hpos++; c = ((d & 0x20) >> 4) | (((PaletteNo >> 2) & 2) >> 1); if (c != 0) LineRAM[hpos & 0x1ff] = (byte)((PaletteNo & 0x10) | c); else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; hpos++; c = ((d & 0x10) >> 3) | ((PaletteNo >> 2) & 1); if (c != 0) LineRAM[hpos & 0x1ff] = (byte)((PaletteNo & 0x10) | c); else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; hpos++; c = ((d & 0x08) >> 2) | (((PaletteNo >> 2) & 2) >> 1); if (c != 0) LineRAM[hpos & 0x1ff] = (byte)((PaletteNo & 0x10) | c); else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; hpos++; c = ((d & 0x04) >> 1) | ((PaletteNo >> 2) & 1); if (c != 0) LineRAM[hpos & 0x1ff] = (byte)((PaletteNo & 0x10) | c); else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; hpos++; c = (d & 0x02) | (((PaletteNo >> 2) & 2) >> 1); if (c != 0) LineRAM[hpos & 0x1ff] = (byte)((PaletteNo & 0x10) | c); else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; hpos++; c = ((d & 0x01) << 1) | ((PaletteNo >> 2) & 1); if (c != 0) LineRAM[hpos & 0x1ff] = (byte)((PaletteNo & 0x10) | c); else if (Kangaroo) LineRAM[hpos & 0x1ff] = 0; hpos++; } } } void OutputLineRAM() { var fbi = ((Scanline + 1) * M.FrameBuffer.VisiblePitch) % M.FrameBuffer.VideoBufferByteLength; for (int i = 0; i < M.FrameBuffer.VisiblePitch; i++) { var colorIndex = LineRAM[i]; M.FrameBuffer.VideoBuffer[fbi++] = Registers[BACKGRND + ((colorIndex & 3) == 0 ? 0 : colorIndex)]; if (fbi == M.FrameBuffer.VideoBufferByteLength) fbi = 0; } for (var i = 0; i < LineRAM.Length; i++) { LineRAM[i] = 0; } } #endregion #region Maria Peek byte peek(ushort addr) { addr &= 0x3f; var mi = M.InputState; switch(addr) { case MSTAT: var sl = Scanline; return (sl < FirstVisibleScanline || sl >= LastVisibleScanline) ? (byte)0x80 // VBLANK ON : (byte)0; // VBLANK OFF case INPT0: return mi.SampleCapturedControllerActionState(0, ControllerAction.Trigger) ? (byte)0x80 : (byte)0; // player1,button R case INPT1: return mi.SampleCapturedControllerActionState(0, ControllerAction.Trigger2) ? (byte)0x80 : (byte)0; // player1,button L case INPT2: return mi.SampleCapturedControllerActionState(1, ControllerAction.Trigger) ? (byte)0x80 : (byte)0; // player2,button R case INPT3: return mi.SampleCapturedControllerActionState(1, ControllerAction.Trigger2) ? (byte)0x80 : (byte)0; // player2,button L case INPT4: return SampleINPTLatched(4) ? (byte)0 : (byte)0x80; // player1,button L/R case INPT5: return SampleINPTLatched(5) ? (byte)0 : (byte)0x80; // player2,button L/R default: LogDebug("Maria: Unhandled peek at ${0:x4}, PC=${1:x4}", addr, M.CPU.PC); var retval = Registers[addr]; return retval; } } #endregion #region Maria Poke void poke(ushort addr, byte data) { addr &= 0x3f; switch (addr) { // INPUT PORT CONTROL // Only the first four bits of INPTCTRL are used: // D0: lock mode (after this bit has been set high, no more mode changes can be done until the console is turned off) // D1: 0=disable MARIA (only RIOT RAM is available); 1=enable MARIA (also enables system RAM) // D2: 0=enable BIOS at $8000-$FFFF (actually NTSC only uses 4KB and PAL uses 16KB); 1=disable BIOS and enable cartridge // D3: 0=disable TIA video pull-ups (video output is MARIA instead of TIA); 1=enable TIA video pull-ups (video output is TIA instead of MARIA) // case INPTCTRL: if (CtrlLock) { Log("Maria: INPTCTRL: LOCKED: Ignoring: ${0:x2}, PC=${1:x4}", data, M.CPU.PC); break; } CtrlLock = (data & (1 << 0)) != 0; var mariaEnable = (data & (1 << 1)) != 0; var biosDisable = (data & (1 << 2)) != 0; var tiaopEnable = (data & (1 << 3)) != 0; Log("Maria: INPTCTRL: ${0:x2}, PC=${1:x4}, lockMode={2}, mariaEnable={3} biosDisable={4} tiaOutput={5}", data, M.CPU.PC, CtrlLock, mariaEnable, biosDisable, tiaopEnable); if (biosDisable) { M.SwapOutBIOS(); } else { M.SwapInBIOS(); } break; case WSYNC: // Request a CPU preemption to service the delay request M.CPU.EmulatorPreemptRequest = true; break; case CTRL: ColorKill = (data & 0x80) != 0; DMAEnabled = (data & 0x60) == 0x40; CWidth = (data & 0x10) != 0; BCntl = (data & 0x08) != 0; Kangaroo = (data & 0x04) != 0; RM = (byte)(data & 0x03); break; case MSTAT: break; case CHARBASE: case DPPH: case DPPL: Registers[addr] = data; break; case BACKGRND: case P0C1: case P0C2: case P0C3: case P1C1: case P1C2: case P1C3: case P2C1: case P2C2: case P2C3: case P3C1: case P3C2: case P3C3: case P4C1: case P4C2: case P4C3: case P5C1: case P5C2: case P5C3: case P6C1: case P6C2: case P6C3: case P7C1: case P7C2: case P7C3: Registers[addr] = data; break; case AUDC0: case AUDC1: case AUDF0: case AUDF1: case AUDV0: case AUDV1: TIASound.Update(addr, data); break; case OFFSET: Log("Maria: OFFSET: ROM wrote ${0:x2}, PC=${1:x4} (reserved for future expansion)", data, M.CPU.PC); break; default: Registers[addr] = data; LogDebug("Maria: Unhandled poke:${0:x4} w/${1:x2}, PC=${2:x4}", addr, data, M.CPU.PC); break; } } #endregion #region Input Helpers bool SampleINPTLatched(int inpt) { var mi = M.InputState; var playerNo = inpt - 4; switch (playerNo == 0 ? mi.LeftControllerJack : mi.RightControllerJack) { case Controller.Joystick: return mi.SampleCapturedControllerActionState(playerNo, ControllerAction.Trigger); case Controller.ProLineJoystick: var portbline = 4 << (playerNo << 1); if ((M.PIA.DDRB & portbline) != 0 && (M.PIA.WrittenPortB & portbline) == 0) return false; return mi.SampleCapturedControllerActionState(playerNo, ControllerAction.Trigger) || mi.SampleCapturedControllerActionState(playerNo, ControllerAction.Trigger2); case Controller.Lightgun: // This is one area where always running fixed at the faster CPU frequency creates emulation challenges. // Fortunately since lightgun sampling is a dedicated activity on a frame, the job of compensating is tractable. // Track the number of samples this frame, the time of the first sample, and capture the lightgun location. if (_lightgunFirstSampleCpuClock == 0) { _lightgunFirstSampleCpuClock = M.CPU.Clock; _lightgunFrameSamples = 0; mi.SampleCapturedLightGunPosition(playerNo, out _lightgunSampledScanline, out _lightgunSampledVisibleHpos); } _lightgunFrameSamples++; // Magic Adjustment Factor // Seems sufficient to account for the timing impact of successive lightrun reads (i.e., 'slow' memory accesses.) // Obtained through through trial-and-error. const float magicAdjustmentFactor = 2.135f; var firstLightgunSampleMariaFrameClock = (int)((_lightgunFirstSampleCpuClock - _startOfFrameCpuClock) << 2); var mariaClocksSinceFirstLightgunSample = (int)((M.CPU.Clock - _lightgunFirstSampleCpuClock) << 2); var adjustmentMariaClocks = (int)Math.Round(_lightgunFrameSamples * magicAdjustmentFactor); var actualMariaFrameClock = firstLightgunSampleMariaFrameClock + mariaClocksSinceFirstLightgunSample + adjustmentMariaClocks; var actualScanline = actualMariaFrameClock / 456; var actualHpos = actualMariaFrameClock % 456; // Lightgun sampling looks intended to begin at the start of the scanline. // Compensate with another magic constant since we're always off by a fixed amount. actualHpos -= 62; if (actualHpos < 0) { actualHpos += 456; actualScanline--; } var sampledScanline = _lightgunSampledScanline; var sampledVisibleHpos = _lightgunSampledVisibleHpos; // Seems reasonable the gun sees more than a single pixel (more like a circle or oval) and triggers sooner accordingly. // These adjustments were obtained through trial-and-error. if (_isPal) { sampledScanline -= 19; } else { sampledScanline -= 16; sampledVisibleHpos += 4; } return (actualScanline >= sampledScanline) && (actualHpos >= (sampledVisibleHpos + 136 /* HBLANK clocks */)); } return false; } #endregion #region Serialization Members public Maria(DeserializationContext input, Machine7800 m, int scanlines) { if (input == null) throw new ArgumentNullException("input"); if (m == null) throw new ArgumentNullException("m"); M = m; InitializeVisibleScanlineValues(scanlines); TIASound = new TIASound(input, M, CPU_TICKS_PER_AUDIO_SAMPLE); var version = input.CheckVersion(1, 2); LineRAM = input.ReadExpectedBytes(512); if (version == 1) { // formerly persisted values, MariaPalette[8,4] for (var i = 0; i < 32; i++) input.ReadByte(); } Registers = input.ReadExpectedBytes(0x40); if (version == 1) { // formerly persisted value, Scanline input.ReadInt32(); } switch (version) { case 1: WM = (input.ReadByte() != 0); break; case 2: WM = input.ReadBoolean(); break; } DLL = input.ReadUInt16(); DL = input.ReadUInt16(); Offset = input.ReadInt32(); Holey = input.ReadInt32(); Width = input.ReadInt32(); HPOS = input.ReadByte(); PaletteNo = input.ReadByte(); INDMode = input.ReadBoolean(); if (version == 1) { // formerly persisted value (DLI) input.ReadBoolean(); } CtrlLock = input.ReadBoolean(); if (version == 1) { // formerly persisted value (VBLANK) input.ReadByte(); } DMAEnabled = input.ReadBoolean(); if (version == 1) { // formerly persisted value (DMAOn) input.ReadBoolean(); } ColorKill = input.ReadBoolean(); CWidth = input.ReadBoolean(); BCntl = input.ReadBoolean(); Kangaroo = input.ReadBoolean(); RM = input.ReadByte(); } public void GetObjectData(SerializationContext output) { if (output == null) throw new ArgumentNullException("output"); output.Write(TIASound); output.WriteVersion(2); output.Write(LineRAM); output.Write(Registers); output.Write(WM); output.Write(DLL); output.Write(DL); output.Write(Offset); output.Write(Holey); output.Write(Width); output.Write(HPOS); output.Write((byte)PaletteNo); output.Write(INDMode); output.Write(CtrlLock); output.Write(DMAEnabled); output.Write(ColorKill); output.Write(CWidth); output.Write(BCntl); output.Write(Kangaroo); output.Write(RM); } #endregion #region Helpers void ConsumeNextDLLEntry() { // Display List List (DLL) entry var dll0 = DmaRead(DLL++); // DLI, Holey, Offset var dll1 = DmaRead(DLL++); // High DL address var dll2 = DmaRead(DLL++); // Low DL address var dli = (dll0 & 0x80) != 0; Holey = (dll0 & 0x60) >> 5; Offset = dll0 & 0x0f; // Update current Display List (DL) DL = WORD(dll2, dll1); if (dli) { M.CPU.NMIInterruptRequest = true; // DMA TIMING: One tick between DMA Shutdown and DLI _dmaClocks += 1; } } void InitializeVisibleScanlineValues(int scanlines) { switch (scanlines) { case 262: // NTSC FirstVisibleScanline = 11; LastVisibleScanline = FirstVisibleScanline + 242; _isPal = false; break; case 312: // PAL FirstVisibleScanline = 11; LastVisibleScanline = FirstVisibleScanline + 292; _isPal = true; break; default: throw new ArgumentException("scanlines must be 262 or 312."); } } void Log(string format, params object[] args) { if (M == null || M.Logger == null) return; M.Logger.WriteLine(format, args); } // convenience overload static ushort WORD(int lsb, int msb) { return WORD((byte)lsb, (byte)msb); } static ushort WORD(byte lsb, byte msb) { return (ushort)(lsb | msb << 8); } // convenience overload byte DmaRead(int addr) { return DmaRead((ushort)addr); } byte DmaRead(ushort addr) { #if DEBUG if (addr < 0x1800) LogDebug("Maria: Questionable DMA read at ${0:x4} by PC=${1:x4}", addr, M.CPU.PC); #endif return M.Mem[addr]; } [System.Diagnostics.Conditional("DEBUG")] void LogDebug(string format, params object[] args) { if (M == null || M.Logger == null) return; M.Logger.WriteLine(format, args); } [System.Diagnostics.Conditional("DEBUG")] void AssertDebug(bool cond) { if (!cond) System.Diagnostics.Debugger.Break(); } #endregion } }