1350 lines
44 KiB
C#
1350 lines
44 KiB
C#
/*
|
|
* TIA.cs
|
|
*
|
|
* The Television Interface Adaptor device.
|
|
*
|
|
* Copyright © 2003-2008 Mike Murphy
|
|
*
|
|
*/
|
|
using System;
|
|
|
|
namespace EMU7800.Core
|
|
{
|
|
#region Collision Flags
|
|
|
|
[Flags]
|
|
public enum TIACxFlags
|
|
{
|
|
PF = 1 << 0,
|
|
BL = 1 << 1,
|
|
M0 = 1 << 2,
|
|
M1 = 1 << 3,
|
|
P0 = 1 << 4,
|
|
P1 = 1 << 5
|
|
};
|
|
|
|
[Flags]
|
|
public enum TIACxPairFlags
|
|
{
|
|
M0P1 = 1 << 0,
|
|
M0P0 = 1 << 1,
|
|
M1P0 = 1 << 2,
|
|
M1P1 = 1 << 3,
|
|
P0PF = 1 << 4,
|
|
P0BL = 1 << 5,
|
|
P1PF = 1 << 6,
|
|
P1BL = 1 << 7,
|
|
M0PF = 1 << 8,
|
|
M0BL = 1 << 9,
|
|
M1PF = 1 << 10,
|
|
M1BL = 1 << 11,
|
|
BLPF = 1 << 12,
|
|
P0P1 = 1 << 13,
|
|
M0M1 = 1 << 14
|
|
};
|
|
|
|
#endregion
|
|
|
|
public sealed class TIA : IDevice
|
|
{
|
|
#region Constants
|
|
|
|
const int
|
|
VSYNC = 0x00, // Write: vertical sync set-clear (D1)
|
|
VBLANK = 0x01, // Write: vertical blank set-clear (D7-6,D1)
|
|
WSYNC = 0x02, // Write: wait for leading edge of hrz. blank (strobe)
|
|
RSYNC = 0x03, // Write: reset hrz. sync counter (strobe)
|
|
NUSIZ0 = 0x04, // Write: number-size player-missle 0 (D5-0)
|
|
NUSIZ1 = 0x05, // Write: number-size player-missle 1 (D5-0)
|
|
COLUP0 = 0x06, // Write: color-lum player 0 (D7-1)
|
|
COLUP1 = 0x07, // Write: color-lum player 1 (D7-1)
|
|
COLUPF = 0x08, // Write: color-lum playfield (D7-1)
|
|
COLUBK = 0x09, // Write: color-lum background (D7-1)
|
|
CTRLPF = 0x0a, // Write: cntrl playfield ballsize & coll. (D5-4,D2-0)
|
|
REFP0 = 0x0b, // Write: reflect player 0 (D3)
|
|
REFP1 = 0x0c, // Write: reflect player 1 (D3)
|
|
PF0 = 0x0d, // Write: playfield register byte 0 (D7-4)
|
|
PF1 = 0x0e, // Write: playfield register byte 1 (D7-0)
|
|
PF2 = 0x0f, // Write: playfield register byte 2 (D7-0)
|
|
RESP0 = 0x10, // Write: reset player 0 (strobe)
|
|
RESP1 = 0x11, // Write: reset player 1 (strobe)
|
|
RESM0 = 0x12, // Write: reset missle 0 (strobe)
|
|
RESM1 = 0x13, // Write: reset missle 1 (strobe)
|
|
RESBL = 0x14, // Write: reset ball (strobe)
|
|
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)
|
|
GRP0 = 0x1b, // Write: graphics player 0 (D7-0)
|
|
GRP1 = 0x1c, // Write: graphics player 1 (D7-0)
|
|
ENAM0 = 0x1d, // Write: graphics (enable) missle 0 (D1)
|
|
ENAM1 = 0x1e, // Write: graphics (enable) missle 1 (D1)
|
|
ENABL = 0x1f, // Write: graphics (enable) ball (D1)
|
|
HMP0 = 0x20, // Write: horizontal motion player 0 (D7-4)
|
|
HMP1 = 0x21, // Write: horizontal motion player 1 (D7-4)
|
|
HMM0 = 0x22, // Write: horizontal motion missle 0 (D7-4)
|
|
HMM1 = 0x23, // Write: horizontal motion missle 1 (D7-4)
|
|
HMBL = 0x24, // Write: horizontal motion ball (D7-4)
|
|
VDELP0 = 0x25, // Write: vertical delay player 0 (D0)
|
|
VDELP1 = 0x26, // Write: vertical delay player 1 (D0)
|
|
VDELBL = 0x27, // Write: vertical delay ball (D0)
|
|
RESMP0 = 0x28, // Write: reset missle 0 to player 0 (D1)
|
|
RESMP1 = 0x29, // Write: reset missle 1 to player 1 (D1)
|
|
HMOVE = 0x2a, // Write: apply horizontal motion (strobe)
|
|
HMCLR = 0x2b, // Write: clear horizontal motion registers (strobe)
|
|
CXCLR = 0x2c, // Write: clear collision latches (strobe)
|
|
|
|
CXM0P = 0x00, // Read collision: D7=(M0,P1); D6=(M0,P0)
|
|
CXM1P = 0x01, // Read collision: D7=(M1,P0); D6=(M1,P1)
|
|
CXP0FB = 0x02, // Read collision: D7=(P0,PF); D6=(P0,BL)
|
|
CXP1FB = 0x03, // Read collision: D7=(P1,PF); D6=(P1,BL)
|
|
CXM0FB = 0x04, // Read collision: D7=(M0,PF); D6=(M0,BL)
|
|
CXM1FB = 0x05, // Read collision: D7=(M1,PF); D6=(M1,BL)
|
|
CXBLPF = 0x06, // Read collision: D7=(BL,PF); D6=(unused)
|
|
CXPPMM = 0x07, // Read collision: D7=(P0,P1); D6=(M0,M1)
|
|
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
|
|
|
|
const int CPU_TICKS_PER_AUDIO_SAMPLE = 38;
|
|
|
|
#endregion
|
|
|
|
#region Data Structures
|
|
|
|
readonly byte[] RegW = new byte[0x40];
|
|
readonly MachineBase M;
|
|
readonly TIASound TIASound;
|
|
|
|
delegate void PokeOpTyp(ushort addr, byte data);
|
|
|
|
PokeOpTyp[] PokeOp;
|
|
|
|
#endregion
|
|
|
|
#region Position Counters
|
|
|
|
// backing fields for properties--dont reference directly
|
|
int _HSync, _P0, _P1, _M0, _M1, _BL, _HMoveCounter;
|
|
|
|
// Horizontal Sync Counter
|
|
// this represents HSync of the last rendered CLK
|
|
// has period of 57 counts, 0-56, at 1/4 CLK (57*4=228 CLK)
|
|
// provide all horizontal timing for constructing a valid TV signal
|
|
// other movable object counters can be reset out-of-phase with HSync, hence %228 and not %57
|
|
int HSync
|
|
{
|
|
get { return _HSync; }
|
|
set { _HSync = value % 228; }
|
|
}
|
|
|
|
// determines the difference between HSync and PokeOpHSync
|
|
int PokeOpHSyncDelta
|
|
{
|
|
get { return (int)(Clock - LastEndClock); }
|
|
}
|
|
|
|
// this represents the current HSync
|
|
int PokeOpHSync
|
|
{
|
|
get { return (HSync + PokeOpHSyncDelta) % 228; }
|
|
}
|
|
|
|
// scanline last rendered to
|
|
int ScanLine;
|
|
|
|
// current position in the frame buffer
|
|
int FrameBufferIndex;
|
|
|
|
// bytes are batched here for writing to the FrameBuffer
|
|
//BufferElement FrameBufferElement;
|
|
|
|
// signals when to start an HMOVE
|
|
ulong StartHMOVEClock;
|
|
|
|
// indicates where in the HMOVE operation it is
|
|
int HMoveCounter
|
|
{
|
|
get { return _HMoveCounter; }
|
|
set { _HMoveCounter = value < 0 ? -1 : value & 0xf; }
|
|
}
|
|
|
|
// true when there is an HMOVE executing on the current scanline
|
|
bool HMoveLatch;
|
|
|
|
// represents the TIA color clock (CLK)
|
|
// computed off of the CPU clock, but in real life, the CPU is driven by the color CLK signal
|
|
ulong Clock
|
|
{
|
|
get { return 3 * M.CPU.Clock; }
|
|
}
|
|
|
|
// represents the first CLK of the unrendered scanline segment
|
|
ulong StartClock;
|
|
|
|
// represents the last CLK of the previously rendered scanline segment
|
|
ulong LastEndClock
|
|
{
|
|
get { return StartClock - 1; }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Player 0 Object
|
|
|
|
// Player 0 Horizontal Position Counter
|
|
// has period of 40 counts, 0-39, at 1/4 CLK (40*4=160 CLK=visible scanline length)
|
|
// player position counter controls the position of the respective player graphics object on each scanline
|
|
// can be reset out-of-phase with HSync, hence %160 and not %40
|
|
int P0
|
|
{
|
|
get { return _P0; }
|
|
set { _P0 = value % 160; }
|
|
}
|
|
|
|
// HMOVE "more motion required" latch
|
|
bool P0mmr;
|
|
|
|
// Player 0 graphics registers
|
|
byte EffGRP0, OldGRP0;
|
|
|
|
// Player 0 stretch mode
|
|
int P0type;
|
|
|
|
// 1=currently suppressing copy 1 on Player 0
|
|
int P0suppress;
|
|
|
|
#endregion
|
|
|
|
#region Player 1 Object
|
|
|
|
// Player 1 Horizontal Position Counter (identical to P0)
|
|
int P1
|
|
{
|
|
get { return _P1; }
|
|
set { _P1 = value % 160; }
|
|
}
|
|
|
|
// HMOVE "more motion required" latch
|
|
bool P1mmr;
|
|
|
|
// Player 1 graphics registers
|
|
byte EffGRP1, OldGRP1;
|
|
|
|
// Player 1 stretch mode
|
|
int P1type;
|
|
|
|
// 1=currently suppressing copy 1 on Player 1
|
|
int P1suppress;
|
|
|
|
#endregion
|
|
|
|
#region Missile 0 Object
|
|
|
|
// Missile 0 Horizontal Position Counter
|
|
// similar to player position counters
|
|
int M0
|
|
{
|
|
get { return _M0; }
|
|
set { _M0 = value % 160; }
|
|
}
|
|
|
|
// HMOVE "more motion required" latch
|
|
bool M0mmr;
|
|
|
|
int M0type, M0size;
|
|
|
|
bool m0on;
|
|
|
|
#endregion
|
|
|
|
#region Missle 1 Object
|
|
|
|
// Missile 1 Horizontal Position Counter (identical to M0)
|
|
int M1
|
|
{
|
|
get { return _M1; }
|
|
set { _M1 = value % 160; }
|
|
}
|
|
|
|
// HMOVE "more motion required" latch
|
|
bool M1mmr;
|
|
|
|
int M1type, M1size;
|
|
|
|
bool m1on;
|
|
|
|
#endregion
|
|
|
|
#region Ball Object
|
|
|
|
// Ball Horizontal Position Counter
|
|
// similar to player position counters
|
|
int BL
|
|
{
|
|
get { return _BL; }
|
|
set { _BL = value % 160; }
|
|
}
|
|
|
|
// HMOVE "more motion required" latch
|
|
bool BLmmr;
|
|
|
|
bool OldENABL;
|
|
|
|
int BLsize;
|
|
|
|
bool blon;
|
|
|
|
#endregion
|
|
|
|
#region Misc
|
|
|
|
uint PF210;
|
|
int PFReflectionState;
|
|
|
|
// color-luminance for background, playfield, player 0, player 1
|
|
byte colubk, colupf, colup0, colup1;
|
|
|
|
bool vblankon, scoreon, pfpriority;
|
|
|
|
bool DumpEnabled;
|
|
ulong DumpDisabledCycle;
|
|
|
|
TIACxPairFlags Collisions;
|
|
|
|
#endregion
|
|
|
|
#region Public Members
|
|
|
|
public int WSYNCDelayClocks { get; set; }
|
|
public bool EndOfFrame { get; private set; }
|
|
|
|
public void Reset()
|
|
{
|
|
for (var i = 0; i < RegW.Length; i++)
|
|
{
|
|
RegW[i] = 0;
|
|
}
|
|
vblankon = scoreon = pfpriority = false;
|
|
m0on = m1on = blon = false;
|
|
colubk = colupf = colup0 = colup1 = 0;
|
|
PFReflectionState = 0;
|
|
|
|
StartClock = Clock;
|
|
HSync = -1;
|
|
P0 = P1 = M0 = M1 = BL = -1;
|
|
P0mmr = P1mmr = M0mmr = M1mmr = BLmmr = false;
|
|
StartHMOVEClock = ulong.MaxValue;
|
|
HMoveCounter = -1;
|
|
|
|
FrameBufferIndex = 0;
|
|
|
|
TIASound.Reset();
|
|
|
|
Log("{0} reset", this);
|
|
}
|
|
|
|
public override String ToString()
|
|
{
|
|
return "TIA 1A";
|
|
}
|
|
|
|
public void StartFrame()
|
|
{
|
|
WSYNCDelayClocks = 0;
|
|
EndOfFrame = false;
|
|
ScanLine = 0;
|
|
FrameBufferIndex %= 160;
|
|
RenderFromStartClockTo(Clock);
|
|
TIASound.StartFrame();
|
|
}
|
|
|
|
public byte this[ushort addr]
|
|
{
|
|
get { return peek(addr); }
|
|
set { poke(addr, value); }
|
|
}
|
|
|
|
public void EndFrame()
|
|
{
|
|
TIASound.EndFrame();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Constructors
|
|
|
|
private TIA()
|
|
{
|
|
BuildPokeOpTable();
|
|
}
|
|
|
|
public TIA(MachineBase m) : this()
|
|
{
|
|
if (m == null)
|
|
throw new ArgumentNullException("m");
|
|
|
|
M = m;
|
|
TIASound = new TIASound(M, CPU_TICKS_PER_AUDIO_SAMPLE);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Scanline Segment Renderer
|
|
|
|
void RenderFromStartClockTo(ulong endClock)
|
|
{
|
|
|
|
RenderClock:
|
|
if (StartClock >= endClock)
|
|
return;
|
|
|
|
++HSync;
|
|
|
|
if (StartClock == StartHMOVEClock)
|
|
{
|
|
// turn on HMOVE
|
|
HMoveLatch = true;
|
|
HMoveCounter = 0xf;
|
|
P0mmr = P1mmr = M0mmr = M1mmr = BLmmr = true;
|
|
}
|
|
else if (HSync == 0)
|
|
{
|
|
// just wrapped around, clear late HBLANK
|
|
HMoveLatch = false;
|
|
}
|
|
|
|
// position counters are incremented during the visible portion of the scanline
|
|
if (HSync >= 68 + (HMoveLatch ? 8 : 0))
|
|
{
|
|
++P0; ++P1; ++M0; ++M1; ++BL;
|
|
}
|
|
|
|
// HMOVE compare, once every 1/4 CLK when on
|
|
if (HMoveCounter >= 0 && (HSync & 3) == 0)
|
|
{
|
|
if (((HMoveCounter ^ RegW[HMP0]) & 0xf) == 0xf) P0mmr = false;
|
|
if (((HMoveCounter ^ RegW[HMP1]) & 0xf) == 0xf) P1mmr = false;
|
|
if (((HMoveCounter ^ RegW[HMM0]) & 0xf) == 0xf) M0mmr = false;
|
|
if (((HMoveCounter ^ RegW[HMM1]) & 0xf) == 0xf) M1mmr = false;
|
|
if (((HMoveCounter ^ RegW[HMBL]) & 0xf) == 0xf) BLmmr = false;
|
|
if (HMoveCounter >= 0) HMoveCounter--;
|
|
}
|
|
|
|
// HMOVE increment, once every 1/4 CLK, 2 CLK after first compare when on
|
|
if (HMoveCounter < 0xf && (HSync & 3) == 2)
|
|
{
|
|
if (HSync < 68 + (HMoveLatch ? 8 : 0))
|
|
{
|
|
if (P0mmr) ++P0;
|
|
if (P1mmr) ++P1;
|
|
if (M0mmr) ++M0;
|
|
if (M1mmr) ++M1;
|
|
if (BLmmr) ++BL;
|
|
}
|
|
}
|
|
|
|
if (HSync == 68 + 76) PFReflectionState = RegW[CTRLPF] & 0x01;
|
|
|
|
var fbyte = (byte)0;
|
|
var fbyte_colupf = colupf;
|
|
TIACxFlags cxflags = 0;
|
|
|
|
if (vblankon || HSync < 68 + (HMoveLatch ? 8 : 0)) goto WritePixel;
|
|
|
|
fbyte = colubk;
|
|
|
|
var colupfon = false;
|
|
if ((PF210 & TIATables.PFMask[PFReflectionState][HSync - 68]) != 0)
|
|
{
|
|
if (scoreon) fbyte_colupf = (HSync - 68) < 80 ? colup0 : colup1;
|
|
colupfon = true;
|
|
cxflags |= TIACxFlags.PF;
|
|
}
|
|
if (blon && BL >= 0 && TIATables.BLMask[BLsize][BL])
|
|
{
|
|
colupfon = true;
|
|
cxflags |= TIACxFlags.BL;
|
|
}
|
|
if (!pfpriority && colupfon)
|
|
{
|
|
fbyte = fbyte_colupf;
|
|
}
|
|
if (m1on && M1 >= 0 && TIATables.MxMask[M1size][M1type][M1])
|
|
{
|
|
fbyte = colup1;
|
|
cxflags |= TIACxFlags.M1;
|
|
}
|
|
if (P1 >= 0 && (TIATables.PxMask[P1suppress][P1type][P1] & EffGRP1) != 0)
|
|
{
|
|
fbyte = colup1;
|
|
cxflags |= TIACxFlags.P1;
|
|
}
|
|
if (m0on && M0 >= 0 && TIATables.MxMask[M0size][M0type][M0])
|
|
{
|
|
fbyte = colup0;
|
|
cxflags |= TIACxFlags.M0;
|
|
}
|
|
if (P0 >= 0 && (TIATables.PxMask[P0suppress][P0type][P0] & EffGRP0) != 0)
|
|
{
|
|
fbyte = colup0;
|
|
cxflags |= TIACxFlags.P0;
|
|
}
|
|
if (pfpriority && colupfon)
|
|
{
|
|
fbyte = fbyte_colupf;
|
|
}
|
|
|
|
WritePixel:
|
|
Collisions |= TIATables.CollisionMask[(int)cxflags];
|
|
|
|
if (HSync >= 68)
|
|
{
|
|
M.FrameBuffer.VideoBuffer[FrameBufferIndex++] = fbyte;
|
|
if (FrameBufferIndex == M.FrameBuffer.VideoBufferByteLength)
|
|
FrameBufferIndex = 0;
|
|
if (HSync == 227)
|
|
ScanLine++;
|
|
}
|
|
|
|
if (P0 >= 156) P0suppress = 0;
|
|
if (P1 >= 156) P1suppress = 0;
|
|
|
|
// denote this CLK has been completed by incrementing to the next
|
|
StartClock++;
|
|
|
|
goto RenderClock;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region TIA Peek
|
|
|
|
byte peek(ushort addr)
|
|
{
|
|
var retval = 0;
|
|
addr &= 0xf;
|
|
|
|
RenderFromStartClockTo(Clock);
|
|
|
|
switch (addr)
|
|
{
|
|
case CXM0P:
|
|
retval |= ((Collisions & TIACxPairFlags.M0P1) != 0 ? 0x80 : 0);
|
|
retval |= ((Collisions & TIACxPairFlags.M0P0) != 0 ? 0x40 : 0);
|
|
break;
|
|
case CXM1P:
|
|
retval |= ((Collisions & TIACxPairFlags.M1P0) != 0 ? 0x80 : 0);
|
|
retval |= ((Collisions & TIACxPairFlags.M1P1) != 0 ? 0x40 : 0);
|
|
break;
|
|
case CXP0FB:
|
|
retval |= ((Collisions & TIACxPairFlags.P0PF) != 0 ? 0x80 : 0);
|
|
retval |= ((Collisions & TIACxPairFlags.P0BL) != 0 ? 0x40 : 0);
|
|
break;
|
|
case CXP1FB:
|
|
retval |= ((Collisions & TIACxPairFlags.P1PF) != 0 ? 0x80 : 0);
|
|
retval |= ((Collisions & TIACxPairFlags.P1BL) != 0 ? 0x40 : 0);
|
|
break;
|
|
case CXM0FB:
|
|
retval |= ((Collisions & TIACxPairFlags.M0PF) != 0 ? 0x80 : 0);
|
|
retval |= ((Collisions & TIACxPairFlags.M0BL) != 0 ? 0x40 : 0);
|
|
break;
|
|
case CXM1FB:
|
|
retval |= ((Collisions & TIACxPairFlags.M1PF) != 0 ? 0x80 : 0);
|
|
retval |= ((Collisions & TIACxPairFlags.M1BL) != 0 ? 0x40 : 0);
|
|
break;
|
|
case CXBLPF:
|
|
retval |= ((Collisions & TIACxPairFlags.BLPF) != 0 ? 0x80 : 0);
|
|
break;
|
|
case CXPPMM:
|
|
retval |= ((Collisions & TIACxPairFlags.P0P1) != 0 ? 0x80 : 0);
|
|
retval |= ((Collisions & TIACxPairFlags.M0M1) != 0 ? 0x40 : 0);
|
|
break;
|
|
case INPT0:
|
|
retval = DumpedInputPort(SampleINPT(0));
|
|
break;
|
|
case INPT1:
|
|
retval = DumpedInputPort(SampleINPT(1));
|
|
break;
|
|
case INPT2:
|
|
retval = DumpedInputPort(SampleINPT(2));
|
|
break;
|
|
case INPT3:
|
|
retval = DumpedInputPort(SampleINPT(3));
|
|
break;
|
|
case INPT4:
|
|
var scanline = ScanLine;
|
|
var hpos = PokeOpHSync - 68;
|
|
if (hpos < 0)
|
|
{
|
|
hpos += 228;
|
|
scanline--;
|
|
}
|
|
if (SampleINPTLatched(4, scanline, hpos))
|
|
{
|
|
retval &= 0x7f;
|
|
}
|
|
else
|
|
{
|
|
retval |= 0x80;
|
|
}
|
|
break;
|
|
case INPT5:
|
|
scanline = ScanLine;
|
|
hpos = PokeOpHSync - 68;
|
|
if (hpos < 0)
|
|
{
|
|
hpos += 228;
|
|
scanline--;
|
|
}
|
|
if (SampleINPTLatched(5, scanline, hpos))
|
|
{
|
|
retval &= 0x7f;
|
|
}
|
|
else
|
|
{
|
|
retval |= 0x80;
|
|
}
|
|
break;
|
|
}
|
|
return (byte)(retval | (M.Mem.DataBusState & 0x3f));
|
|
}
|
|
|
|
byte DumpedInputPort(int resistance)
|
|
{
|
|
byte retval = 0;
|
|
|
|
if (resistance == 0)
|
|
{
|
|
retval = 0x80;
|
|
}
|
|
else if (DumpEnabled || resistance == Int32.MaxValue)
|
|
{
|
|
retval = 0x00;
|
|
}
|
|
else
|
|
{
|
|
var chargeTime = 1.6 * resistance * 0.01e-6;
|
|
var needed = (ulong)(chargeTime * M.FrameBuffer.Scanlines * 228 * M.FrameHZ / 3);
|
|
if (M.CPU.Clock > DumpDisabledCycle + needed)
|
|
{
|
|
retval = 0x80;
|
|
}
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region TIA Poke
|
|
|
|
void poke(ushort addr, byte data)
|
|
{
|
|
addr &= 0x3f;
|
|
|
|
var endClock = Clock;
|
|
|
|
// writes to the TIA may take a few extra CLKs to actually affect TIA state
|
|
// such a delay would seem to be applicable across all possible TIA writes
|
|
// without hardware to confirm conclusively, this is updated only as seemingly required
|
|
switch (addr)
|
|
{
|
|
case GRP0:
|
|
case GRP1:
|
|
// stem in the T in activision logo on older titles
|
|
endClock += 1;
|
|
break;
|
|
case PF0:
|
|
case PF1:
|
|
case PF2:
|
|
// +2 prevents minor notching in berzerk walls
|
|
// +4 prevents shield fragments floating in fuzz field in yars revenge,
|
|
// but creates minor artifact in chopper command
|
|
switch (PokeOpHSync & 3)
|
|
{
|
|
case 0: endClock += 4; break;
|
|
case 1: endClock += 3; break;
|
|
case 2: endClock += 2; break;
|
|
case 3: endClock += 5; break;
|
|
}
|
|
break;
|
|
}
|
|
|
|
RenderFromStartClockTo(endClock);
|
|
|
|
PokeOp[addr](addr, data);
|
|
}
|
|
|
|
static void opNULL(ushort addr, byte data)
|
|
{
|
|
}
|
|
|
|
void opVSYNC(ushort addr, byte data)
|
|
{
|
|
// Many games don't appear to supply 3 scanlines of
|
|
// VSYNC in accordance with the Atari documentation.
|
|
// Enduro turns on VSYNC, then turns it off twice.
|
|
// Centipede turns off VSYNC several times, in addition to normal usage.
|
|
// One of the Atari Bowling ROMs turns it off, but never turns it on.
|
|
// So, we always end the frame if VSYNC is turned on and then off.
|
|
// We also end the frame if VSYNC is turned off after scanline 258 to accomodate Bowling.
|
|
if ((data & 0x02) == 0)
|
|
{
|
|
if ((RegW[VSYNC] & 0x02) != 0 || ScanLine > 258)
|
|
{
|
|
EndOfFrame = true;
|
|
M.CPU.EmulatorPreemptRequest = true;
|
|
}
|
|
}
|
|
|
|
RegW[VSYNC] = data;
|
|
}
|
|
|
|
void opVBLANK(ushort addr, byte data)
|
|
{
|
|
if ((RegW[VBLANK] & 0x80) == 0)
|
|
{
|
|
// dump to ground is clear and will be set
|
|
// thus discharging all INPTx capacitors
|
|
if ((data & 0x80) != 0)
|
|
{
|
|
DumpEnabled = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// dump to ground is set and will be cleared
|
|
// thus starting all INPTx capacitors charging
|
|
if ((data & 0x80) == 0)
|
|
{
|
|
DumpEnabled = false;
|
|
DumpDisabledCycle = M.CPU.Clock;
|
|
}
|
|
}
|
|
RegW[VBLANK] = data;
|
|
vblankon = (data & 0x02) != 0;
|
|
}
|
|
|
|
void opWSYNC(ushort addr, byte data)
|
|
{
|
|
// on scanline=44, tetris seems to occasionally not get a WSYNC in until 3 clk in on scanline=45 causing jitter
|
|
if (PokeOpHSync > 0)
|
|
{
|
|
// report the number of remaining system clocks on the scanline to delay the CPU
|
|
WSYNCDelayClocks = 228 - PokeOpHSync;
|
|
// request a CPU preemption to service the delay request (only if there is a delay necessary)
|
|
M.CPU.EmulatorPreemptRequest = true;
|
|
}
|
|
}
|
|
|
|
void opRSYNC(ushort addr, byte data)
|
|
{
|
|
LogDebug("TIA RSYNC: frame={0} scanline={0} hsync={0}", M.FrameNumber, ScanLine, PokeOpHSync);
|
|
}
|
|
|
|
void opNUSIZ0(ushort addr, byte data)
|
|
{
|
|
RegW[NUSIZ0] = (byte)(data & 0x37);
|
|
|
|
M0size = (RegW[NUSIZ0] & 0x30) >> 4;
|
|
M0type = RegW[NUSIZ0] & 0x07;
|
|
P0type = M0type;
|
|
|
|
P0suppress = 0;
|
|
}
|
|
|
|
void opNUSIZ1(ushort addr, byte data)
|
|
{
|
|
RegW[NUSIZ1] = (byte)(data & 0x37);
|
|
|
|
M1size = (RegW[NUSIZ1] & 0x30) >> 4;
|
|
M1type = RegW[NUSIZ1] & 0x07;
|
|
P1type = M1type;
|
|
|
|
P1suppress = 0;
|
|
}
|
|
|
|
void opCOLUBK(ushort addr, byte data)
|
|
{
|
|
colubk = data;
|
|
}
|
|
|
|
void opCOLUPF(ushort addr, byte data)
|
|
{
|
|
colupf = data;
|
|
}
|
|
|
|
void opCOLUP0(ushort addr, byte data)
|
|
{
|
|
colup0 = data;
|
|
}
|
|
|
|
void opCOLUP1(ushort addr, byte data)
|
|
{
|
|
colup1 = data;
|
|
}
|
|
|
|
void opCTRLPF(ushort addr, byte data)
|
|
{
|
|
RegW[CTRLPF] = data;
|
|
|
|
BLsize = (data & 0x30) >> 4;
|
|
scoreon = (data & 0x02) != 0;
|
|
pfpriority = (data & 0x04) != 0;
|
|
}
|
|
|
|
void SetEffGRP0()
|
|
{
|
|
var grp0 = RegW[VDELP0] != 0 ? OldGRP0 : RegW[GRP0];
|
|
EffGRP0 = RegW[REFP0] != 0 ? TIATables.GRPReflect[grp0] : grp0;
|
|
}
|
|
|
|
void SetEffGRP1()
|
|
{
|
|
var grp1 = RegW[VDELP1] != 0 ? OldGRP1 : RegW[GRP1];
|
|
EffGRP1 = RegW[REFP1] != 0 ? TIATables.GRPReflect[grp1] : grp1;
|
|
}
|
|
|
|
void Setblon()
|
|
{
|
|
blon = RegW[VDELBL] != 0 ? OldENABL : RegW[ENABL] != 0;
|
|
}
|
|
|
|
void opREFP0(ushort addr, byte data)
|
|
{
|
|
RegW[REFP0] = (byte)(data & 0x08);
|
|
SetEffGRP0();
|
|
}
|
|
|
|
void opREFP1(ushort addr, byte data)
|
|
{
|
|
RegW[REFP1] = (byte)(data & 0x08);
|
|
SetEffGRP1();
|
|
}
|
|
|
|
void opPF(ushort addr, byte data)
|
|
{
|
|
RegW[addr] = data;
|
|
PF210 = (uint)((RegW[PF2] << 12) | (RegW[PF1] << 4) |((RegW[PF0] >> 4) & 0x0f));
|
|
}
|
|
|
|
void opRESP0(ushort addr, byte data)
|
|
{
|
|
if (PokeOpHSync < 68)
|
|
{
|
|
P0 = 0;
|
|
}
|
|
else if (HMoveLatch && PokeOpHSync >= 68 && PokeOpHSync < 76)
|
|
{
|
|
// this is an attempt to model observed behavior--may not be completely correct
|
|
// only three hsync values are really possible:
|
|
// 69: parkerbros actionman
|
|
// 72: activision grandprix
|
|
// 75: barnstorming, riverraid
|
|
P0 = -((PokeOpHSync - 68) >> 1);
|
|
}
|
|
else
|
|
{
|
|
P0 = -4;
|
|
}
|
|
P0 -= PokeOpHSyncDelta;
|
|
P0suppress = 1;
|
|
}
|
|
|
|
void opRESP1(ushort addr, byte data)
|
|
{
|
|
if (PokeOpHSync < 68)
|
|
{
|
|
P1 = 0;
|
|
}
|
|
else if (HMoveLatch && PokeOpHSync >= 68 && PokeOpHSync < 76)
|
|
{
|
|
// this is an attempt to model observed behavior--may not be completely correct
|
|
// only three hsync values are really possible:
|
|
// 69: parkerbros actionman
|
|
// 72: parkerbros actionman
|
|
// 75: parkerbros actionman
|
|
P1 = -((PokeOpHSync - 68) >> 1);
|
|
}
|
|
else
|
|
{
|
|
P1 = -4;
|
|
}
|
|
P1 -= PokeOpHSyncDelta;
|
|
P1suppress = 1;
|
|
}
|
|
|
|
void opRESM0(ushort addr, byte data)
|
|
{
|
|
// -2 to mirror M1
|
|
M0 = PokeOpHSync < 68 ? -2 : -4;
|
|
M0 -= PokeOpHSyncDelta;
|
|
}
|
|
|
|
void opRESM1(ushort addr, byte data)
|
|
{
|
|
// -2 cleans up edges on activision pitfall ii
|
|
M1 = PokeOpHSync < 68 ? -2 : -4;
|
|
M1 -= PokeOpHSyncDelta;
|
|
}
|
|
|
|
void opRESBL(ushort addr, byte data)
|
|
{
|
|
// -2 cleans up edges on activision boxing
|
|
// -4 confirmed via activision choppercommand; used to clean up edges
|
|
BL = PokeOpHSync < 68 ? -2 : -4;
|
|
BL -= PokeOpHSyncDelta;
|
|
}
|
|
|
|
void opAUD(ushort addr, byte data)
|
|
{
|
|
RegW[addr] = data;
|
|
TIASound.Update(addr, data);
|
|
}
|
|
|
|
void opGRP0(ushort addr, byte data)
|
|
{
|
|
RegW[GRP0] = data;
|
|
OldGRP1 = RegW[GRP1];
|
|
|
|
SetEffGRP0();
|
|
SetEffGRP1();
|
|
}
|
|
|
|
void opGRP1(ushort addr, byte data)
|
|
{
|
|
RegW[GRP1] = data;
|
|
OldGRP0 = RegW[GRP0];
|
|
|
|
OldENABL = RegW[ENABL] != 0;
|
|
|
|
SetEffGRP0();
|
|
SetEffGRP1();
|
|
Setblon();
|
|
}
|
|
|
|
void opENAM0(ushort addr, byte data)
|
|
{
|
|
RegW[ENAM0] = (byte)(data & 0x02);
|
|
m0on = RegW[ENAM0] != 0 && RegW[RESMP0] == 0;
|
|
}
|
|
|
|
void opENAM1(ushort addr, byte data)
|
|
{
|
|
RegW[ENAM1] = (byte)(data & 0x02);
|
|
m1on = RegW[ENAM1] != 0 && RegW[RESMP1] == 0;
|
|
}
|
|
|
|
void opENABL(ushort addr, byte data)
|
|
{
|
|
RegW[ENABL] = (byte)(data & 0x02);
|
|
Setblon();
|
|
}
|
|
|
|
void SetHmr(int hmr, byte data)
|
|
{
|
|
// marshal via >>4 for compare convenience
|
|
RegW[hmr] = (byte)((data ^ 0x80) >> 4);
|
|
}
|
|
|
|
void opHM(ushort addr, byte data)
|
|
{
|
|
SetHmr(addr, data);
|
|
}
|
|
|
|
void opVDELP0(ushort addr, byte data)
|
|
{
|
|
RegW[VDELP0] = (byte)(data & 0x01);
|
|
SetEffGRP0();
|
|
}
|
|
|
|
void opVDELP1(ushort addr, byte data)
|
|
{
|
|
RegW[VDELP1] = (byte)(data & 0x01);
|
|
SetEffGRP1();
|
|
}
|
|
|
|
void opVDELBL(ushort addr, byte data)
|
|
{
|
|
RegW[VDELBL] = (byte)(data & 0x01);
|
|
Setblon();
|
|
}
|
|
|
|
void opRESMP0(ushort addr, byte data)
|
|
{
|
|
if (RegW[RESMP0] != 0 && (data & 0x02) == 0)
|
|
{
|
|
var middle = 4;
|
|
switch (RegW[NUSIZ0] & 0x07)
|
|
{
|
|
case 0x05: middle <<= 1; break; // double size
|
|
case 0x07: middle <<= 2; break; // quad size
|
|
}
|
|
M0 = P0 - middle;
|
|
}
|
|
RegW[RESMP0] = (byte)(data & 0x02);
|
|
m0on = RegW[ENAM0] != 0 && RegW[RESMP0] == 0;
|
|
}
|
|
|
|
void opRESMP1(ushort addr, byte data)
|
|
{
|
|
if (RegW[RESMP1] != 0 && (data & 0x02) == 0)
|
|
{
|
|
var middle = 4;
|
|
switch (RegW[NUSIZ1] & 0x07)
|
|
{
|
|
case 0x05: middle <<= 1; break; // double size
|
|
case 0x07: middle <<= 2; break; // quad size
|
|
}
|
|
M1 = P1 - middle;
|
|
}
|
|
RegW[RESMP1] = (byte)(data & 0x02);
|
|
m1on = RegW[ENAM1] != 0 && RegW[RESMP1] == 0;
|
|
}
|
|
|
|
void opHMOVE(ushort addr, byte data)
|
|
{
|
|
P0suppress = 0;
|
|
P1suppress = 0;
|
|
StartHMOVEClock = Clock + 3;
|
|
|
|
// Activision Spiderfighter Cheat (Score and Logo)
|
|
// Delaying the start of the HMOVE here results in it completing on the next scanline (to have visible effect.)
|
|
// HMOVEing during the visible scanline probably has extra consequences,
|
|
// however, it seems not many carts try to do this.
|
|
if (PokeOpHSync == 201) StartHMOVEClock++; // any increment >0 works
|
|
}
|
|
|
|
void opHMCLR(ushort addr, byte data)
|
|
{
|
|
SetHmr(HMP0, 0);
|
|
SetHmr(HMP1, 0);
|
|
SetHmr(HMM0, 0);
|
|
SetHmr(HMM1, 0);
|
|
SetHmr(HMBL, 0);
|
|
}
|
|
|
|
void opCXCLR(ushort addr, byte data)
|
|
{
|
|
Collisions = 0;
|
|
}
|
|
|
|
void BuildPokeOpTable()
|
|
{
|
|
PokeOp = new PokeOpTyp[64];
|
|
for (var i = 0; i < PokeOp.Length; i++)
|
|
{
|
|
PokeOp[i] = opNULL;
|
|
}
|
|
PokeOp[VSYNC] = opVSYNC;
|
|
PokeOp[VBLANK] = opVBLANK;
|
|
PokeOp[WSYNC] = opWSYNC;
|
|
PokeOp[RSYNC] = opRSYNC;
|
|
PokeOp[NUSIZ0] = opNUSIZ0;
|
|
PokeOp[NUSIZ1] = opNUSIZ1;
|
|
PokeOp[COLUP0] = opCOLUP0;
|
|
PokeOp[COLUP1] = opCOLUP1;
|
|
PokeOp[COLUPF] = opCOLUPF;
|
|
PokeOp[COLUBK] = opCOLUBK;
|
|
PokeOp[CTRLPF] = opCTRLPF;
|
|
PokeOp[REFP0] = opREFP0;
|
|
PokeOp[REFP1] = opREFP1;
|
|
PokeOp[PF0] = opPF;
|
|
PokeOp[PF1] = opPF;
|
|
PokeOp[PF2] = opPF;
|
|
PokeOp[RESP0] = opRESP0;
|
|
PokeOp[RESP1] = opRESP1;
|
|
PokeOp[RESM0] = opRESM0;
|
|
PokeOp[RESM1] = opRESM1;
|
|
PokeOp[RESBL] = opRESBL;
|
|
PokeOp[AUDC0] = opAUD;
|
|
PokeOp[AUDC1] = opAUD;
|
|
PokeOp[AUDF0] = opAUD;
|
|
PokeOp[AUDF1] = opAUD;
|
|
PokeOp[AUDV0] = opAUD;
|
|
PokeOp[AUDV1] = opAUD;
|
|
PokeOp[GRP0] = opGRP0;
|
|
PokeOp[GRP1] = opGRP1;
|
|
PokeOp[ENAM0] = opENAM0;
|
|
PokeOp[ENAM1] = opENAM1;
|
|
PokeOp[ENABL] = opENABL;
|
|
PokeOp[HMP0] = opHM;
|
|
PokeOp[HMP1] = opHM;
|
|
PokeOp[HMM0] = opHM;
|
|
PokeOp[HMM1] = opHM;
|
|
PokeOp[HMBL] = opHM;
|
|
PokeOp[VDELP0] = opVDELP0;
|
|
PokeOp[VDELP1] = opVDELP1;
|
|
PokeOp[VDELBL] = opVDELBL;
|
|
PokeOp[RESMP0] = opRESMP0;
|
|
PokeOp[RESMP1] = opRESMP1;
|
|
PokeOp[HMOVE] = opHMOVE;
|
|
PokeOp[HMCLR] = opHMCLR;
|
|
PokeOp[CXCLR] = opCXCLR;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Input Helpers
|
|
|
|
int SampleINPT(int inpt)
|
|
{
|
|
var mi = M.InputState;
|
|
|
|
switch (inpt <= 1 ? mi.LeftControllerJack : mi.RightControllerJack)
|
|
{
|
|
case Controller.Paddles:
|
|
// playerno = inpt
|
|
return mi.SampleCapturedOhmState(inpt & 3);
|
|
case Controller.ProLineJoystick:
|
|
// playerno = inpt/2
|
|
switch (inpt & 3)
|
|
{
|
|
case 0: return mi.SampleCapturedControllerActionState(0, ControllerAction.Trigger) ? 0 : Int32.MaxValue;
|
|
case 1: return mi.SampleCapturedControllerActionState(0, ControllerAction.Trigger2) ? 0 : Int32.MaxValue;
|
|
case 2: return mi.SampleCapturedControllerActionState(1, ControllerAction.Trigger) ? 0 : Int32.MaxValue;
|
|
case 3: return mi.SampleCapturedControllerActionState(1, ControllerAction.Trigger2) ? 0 : Int32.MaxValue;
|
|
}
|
|
break;
|
|
case Controller.BoosterGrip:
|
|
// playerno = inpt
|
|
return mi.SampleCapturedControllerActionState(inpt & 3, ControllerAction.Trigger2) ? 0 : Int32.MaxValue;
|
|
case Controller.Keypad:
|
|
return SampleKeypadStateDumped(inpt & 3);
|
|
}
|
|
return int.MaxValue;
|
|
}
|
|
|
|
bool SampleINPTLatched(int inpt, int scanline, int hpos)
|
|
{
|
|
var mi = M.InputState;
|
|
var playerNo = inpt - 4;
|
|
|
|
switch (playerNo == 0 ? mi.LeftControllerJack : mi.RightControllerJack)
|
|
{
|
|
case Controller.Joystick:
|
|
case Controller.ProLineJoystick:
|
|
case Controller.Driving:
|
|
case Controller.BoosterGrip:
|
|
return mi.SampleCapturedControllerActionState(playerNo, ControllerAction.Trigger);
|
|
case Controller.Keypad:
|
|
return SampleKeypadStateLatched(playerNo);
|
|
case Controller.Lightgun:
|
|
int sampledScanline, sampledHpos;
|
|
mi.SampleCapturedLightGunPosition(playerNo, out sampledScanline, out sampledHpos);
|
|
return ((scanline - 4) >= sampledScanline && (hpos - 23) >= sampledHpos);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool SampleKeypadStateLatched(int deviceno)
|
|
{
|
|
ControllerAction action;
|
|
|
|
if ((M.PIA.WrittenPortA & 0x01) == 0)
|
|
{
|
|
action = ControllerAction.Keypad3;
|
|
}
|
|
else if ((M.PIA.WrittenPortA & 0x02) == 0)
|
|
{
|
|
action = ControllerAction.Keypad6;
|
|
}
|
|
else if ((M.PIA.WrittenPortA & 0x04) == 0)
|
|
{
|
|
action = ControllerAction.Keypad9;
|
|
}
|
|
else if ((M.PIA.WrittenPortA & 0x08) == 0)
|
|
{
|
|
action = ControllerAction.KeypadP;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return M.InputState.SampleCapturedControllerActionState(deviceno, action);
|
|
}
|
|
|
|
int SampleKeypadStateDumped(int inpt)
|
|
{
|
|
ControllerAction action;
|
|
|
|
if ((M.PIA.WrittenPortA & 0x01) == 0)
|
|
{
|
|
action = (inpt & 1) == 0 ? ControllerAction.Keypad1 : ControllerAction.Keypad2;
|
|
}
|
|
else if ((M.PIA.WrittenPortA & 0x02) == 0)
|
|
{
|
|
action = (inpt & 1) == 0 ? ControllerAction.Keypad4 : ControllerAction.Keypad5;
|
|
}
|
|
else if ((M.PIA.WrittenPortA & 0x04) == 0)
|
|
{
|
|
action = (inpt & 1) == 0 ? ControllerAction.Keypad7 : ControllerAction.Keypad8;
|
|
}
|
|
else if ((M.PIA.WrittenPortA & 0x08) == 0)
|
|
{
|
|
action = (inpt & 1) == 0 ? ControllerAction.KeypadA : ControllerAction.Keypad0;
|
|
}
|
|
else
|
|
{
|
|
return Int32.MaxValue;
|
|
}
|
|
|
|
// playerno = inpt/2
|
|
return M.InputState.SampleCapturedControllerActionState(inpt >> 1, action) ? Int32.MaxValue : 0;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Serialization Members
|
|
|
|
public TIA(DeserializationContext input, MachineBase m) : this()
|
|
{
|
|
if (input == null)
|
|
throw new ArgumentNullException("input");
|
|
if (m == null)
|
|
throw new ArgumentNullException("m");
|
|
|
|
M = m;
|
|
TIASound = input.ReadTIASound(M, CPU_TICKS_PER_AUDIO_SAMPLE);
|
|
|
|
input.CheckVersion(2);
|
|
RegW = input.ReadExpectedBytes(0x40);
|
|
HSync = input.ReadInt32();
|
|
HMoveCounter = input.ReadInt32();
|
|
ScanLine = input.ReadInt32();
|
|
FrameBufferIndex = input.ReadInt32();
|
|
//FrameBufferElement = input.ReadBufferElement();
|
|
StartHMOVEClock = input.ReadUInt64();
|
|
HMoveLatch = input.ReadBoolean();
|
|
StartClock = input.ReadUInt64();
|
|
P0 = input.ReadInt32();
|
|
P0mmr = input.ReadBoolean();
|
|
EffGRP0 = input.ReadByte();
|
|
OldGRP0 = input.ReadByte();
|
|
P0type = input.ReadInt32();
|
|
P0suppress = input.ReadInt32();
|
|
P1 = input.ReadInt32();
|
|
P1mmr = input.ReadBoolean();
|
|
EffGRP1 = input.ReadByte();
|
|
OldGRP1 = input.ReadByte();
|
|
P1type = input.ReadInt32();
|
|
P1suppress = input.ReadInt32();
|
|
M0 = input.ReadInt32();
|
|
M0mmr = input.ReadBoolean();
|
|
M0type = input.ReadInt32();
|
|
M0size = input.ReadInt32();
|
|
m0on = input.ReadBoolean();
|
|
M1 = input.ReadInt32();
|
|
M1mmr = input.ReadBoolean();
|
|
M1type = input.ReadInt32();
|
|
M1size = input.ReadInt32();
|
|
m1on = input.ReadBoolean();
|
|
BL = input.ReadInt32();
|
|
BLmmr = input.ReadBoolean();
|
|
OldENABL = input.ReadBoolean();
|
|
BLsize = input.ReadInt32();
|
|
blon = input.ReadBoolean();
|
|
PF210 = input.ReadUInt32();
|
|
PFReflectionState = input.ReadInt32();
|
|
colubk = input.ReadByte();
|
|
colupf = input.ReadByte();
|
|
colup0 = input.ReadByte();
|
|
colup1 = input.ReadByte();
|
|
vblankon = input.ReadBoolean();
|
|
scoreon = input.ReadBoolean();
|
|
pfpriority = input.ReadBoolean();
|
|
DumpEnabled = input.ReadBoolean();
|
|
DumpDisabledCycle = input.ReadUInt64();
|
|
Collisions = (TIACxPairFlags)input.ReadInt32();
|
|
WSYNCDelayClocks = input.ReadInt32();
|
|
EndOfFrame = input.ReadBoolean();
|
|
}
|
|
|
|
public void GetObjectData(SerializationContext output)
|
|
{
|
|
if (output == null)
|
|
throw new ArgumentNullException("output");
|
|
|
|
output.Write(TIASound);
|
|
|
|
output.WriteVersion(2);
|
|
output.Write(RegW);
|
|
output.Write(HSync);
|
|
output.Write(HMoveCounter);
|
|
output.Write(ScanLine);
|
|
output.Write(FrameBufferIndex);
|
|
//output.Write(FrameBufferElement);
|
|
output.Write(StartHMOVEClock);
|
|
output.Write(HMoveLatch);
|
|
output.Write(StartClock);
|
|
output.Write(P0);
|
|
output.Write(P0mmr);
|
|
output.Write(EffGRP0);
|
|
output.Write(OldGRP0);
|
|
output.Write(P0type);
|
|
output.Write(P0suppress);
|
|
output.Write(P1);
|
|
output.Write(P1mmr);
|
|
output.Write(EffGRP1);
|
|
output.Write(OldGRP1);
|
|
output.Write(P1type);
|
|
output.Write(P1suppress);
|
|
output.Write(M0);
|
|
output.Write(M0mmr);
|
|
output.Write(M0type);
|
|
output.Write(M0size);
|
|
output.Write(m0on);
|
|
output.Write(M1);
|
|
output.Write(M1mmr);
|
|
output.Write(M1type);
|
|
output.Write(M1size);
|
|
output.Write(m1on);
|
|
output.Write(BL);
|
|
output.Write(BLmmr);
|
|
output.Write(OldENABL);
|
|
output.Write(BLsize);
|
|
output.Write(blon);
|
|
output.Write(PF210);
|
|
output.Write(PFReflectionState);
|
|
output.Write(colubk);
|
|
output.Write(colupf);
|
|
output.Write(colup0);
|
|
output.Write(colup1);
|
|
output.Write(vblankon);
|
|
output.Write(scoreon);
|
|
output.Write(pfpriority);
|
|
output.Write(DumpEnabled);
|
|
output.Write(DumpDisabledCycle);
|
|
output.Write((int)Collisions);
|
|
output.Write(WSYNCDelayClocks);
|
|
output.Write(EndOfFrame);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helpers
|
|
|
|
void Log(string format, params object[] args)
|
|
{
|
|
if (M == null || M.Logger == null)
|
|
return;
|
|
M.Logger.WriteLine(format, args);
|
|
}
|
|
|
|
[System.Diagnostics.Conditional("DEBUG")]
|
|
void LogDebug(string format, params object[] args)
|
|
{
|
|
if (M == null || M.Logger == null)
|
|
return;
|
|
M.Logger.WriteLine(format, args);
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
} |