From 1e40bc9082f0cfc39d12e370a1973fa86a48425c Mon Sep 17 00:00:00 2001 From: goyuken Date: Wed, 12 Dec 2012 19:39:17 +0000 Subject: [PATCH] move 7800 into separate cootie-solution --- BizHawk.Emulation/BizHawk.Emulation.csproj | 70 +- .../Consoles/Atari/7800/Atari7800.cs | 16 - EMU7800/Class1.cs | 11 + EMU7800/Core/AddressSpace.cs | 154 ++ EMU7800/Core/Bios7800.cs | 65 + EMU7800/Core/BufferElement.cs | 55 + EMU7800/Core/Cart.cs | 170 +++ EMU7800/Core/Cart7808.cs | 50 + EMU7800/Core/Cart7816.cs | 50 + EMU7800/Core/Cart7832.cs | 50 + EMU7800/Core/Cart7832P.cs | 90 ++ EMU7800/Core/Cart7848.cs | 50 + EMU7800/Core/Cart78AB.cs | 67 + EMU7800/Core/Cart78AC.cs | 86 ++ EMU7800/Core/Cart78S4.cs | 89 ++ EMU7800/Core/Cart78S9.cs | 70 + EMU7800/Core/Cart78SG.cs | 94 ++ EMU7800/Core/Cart78SGP.cs | 118 ++ EMU7800/Core/CartA16K.cs | 86 ++ EMU7800/Core/CartA16KR.cs | 100 ++ EMU7800/Core/CartA2K.cs | 56 + EMU7800/Core/CartA32K.cs | 88 ++ EMU7800/Core/CartA32KR.cs | 104 ++ EMU7800/Core/CartA4K.cs | 47 + EMU7800/Core/CartA8K.cs | 87 ++ EMU7800/Core/CartA8KR.cs | 103 ++ EMU7800/Core/CartCBS12K.cs | 100 ++ EMU7800/Core/CartDC8K.cs | 55 + EMU7800/Core/CartDPC.cs | 341 +++++ EMU7800/Core/CartMN16K.cs | 136 ++ EMU7800/Core/CartPB8K.cs | 103 ++ EMU7800/Core/CartTV8K.cs | 90 ++ EMU7800/Core/CartType.cs | 43 + EMU7800/Core/ConsoleSwitch.cs | 11 + EMU7800/Core/Controller.cs | 14 + EMU7800/Core/ControllerAction.cs | 17 + EMU7800/Core/DeserializationContext.cs | 245 +++ EMU7800/Core/Emu7800Exception.cs | 19 + EMU7800/Core/Emu7800SerializationException.cs | 19 + EMU7800/Core/FontRenderer.cs | 170 +++ EMU7800/Core/FrameBuffer.cs | 79 + EMU7800/Core/HSC7800.cs | 85 ++ EMU7800/Core/IDevice.cs | 16 + EMU7800/Core/ILogger.cs | 10 + EMU7800/Core/InputState.cs | 368 +++++ EMU7800/Core/LICENSE.TXT | 339 +++++ EMU7800/Core/M6502.cs | 1099 +++++++++++++ EMU7800/Core/M6502DASM.cs | 242 +++ EMU7800/Core/Machine2600.cs | 112 ++ EMU7800/Core/Machine2600NTSC.cs | 30 + EMU7800/Core/Machine2600PAL.cs | 30 + EMU7800/Core/Machine7800.cs | 244 +++ EMU7800/Core/Machine7800NTSC.cs | 30 + EMU7800/Core/Machine7800PAL.cs | 30 + EMU7800/Core/MachineBase.cs | 331 ++++ EMU7800/Core/MachineInput.cs | 42 + EMU7800/Core/MachineType.cs | 19 + EMU7800/Core/Maria.cs | 1156 ++++++++++++++ EMU7800/Core/MariaTables.cs | 180 +++ EMU7800/Core/NullDevice.cs | 78 + EMU7800/Core/NullLogger.cs | 21 + EMU7800/Core/PIA.cs | 348 +++++ EMU7800/Core/PokeySound.cs | 437 ++++++ EMU7800/Core/RAM6116.cs | 65 + EMU7800/Core/SerializationContext.cs | 230 +++ EMU7800/Core/TIA.cs | 1355 +++++++++++++++++ EMU7800/Core/TIASound.cs | 361 +++++ EMU7800/Core/TIATables.cs | 424 ++++++ EMU7800/EMU7800.csproj | 127 ++ EMU7800/EMU7800.sln | 20 + EMU7800/Win/GameProgram.cs | 76 + EMU7800/Win/GameProgramLibrary.cs | 383 +++++ 72 files changed, 11674 insertions(+), 82 deletions(-) create mode 100644 EMU7800/Class1.cs create mode 100644 EMU7800/Core/AddressSpace.cs create mode 100644 EMU7800/Core/Bios7800.cs create mode 100644 EMU7800/Core/BufferElement.cs create mode 100644 EMU7800/Core/Cart.cs create mode 100644 EMU7800/Core/Cart7808.cs create mode 100644 EMU7800/Core/Cart7816.cs create mode 100644 EMU7800/Core/Cart7832.cs create mode 100644 EMU7800/Core/Cart7832P.cs create mode 100644 EMU7800/Core/Cart7848.cs create mode 100644 EMU7800/Core/Cart78AB.cs create mode 100644 EMU7800/Core/Cart78AC.cs create mode 100644 EMU7800/Core/Cart78S4.cs create mode 100644 EMU7800/Core/Cart78S9.cs create mode 100644 EMU7800/Core/Cart78SG.cs create mode 100644 EMU7800/Core/Cart78SGP.cs create mode 100644 EMU7800/Core/CartA16K.cs create mode 100644 EMU7800/Core/CartA16KR.cs create mode 100644 EMU7800/Core/CartA2K.cs create mode 100644 EMU7800/Core/CartA32K.cs create mode 100644 EMU7800/Core/CartA32KR.cs create mode 100644 EMU7800/Core/CartA4K.cs create mode 100644 EMU7800/Core/CartA8K.cs create mode 100644 EMU7800/Core/CartA8KR.cs create mode 100644 EMU7800/Core/CartCBS12K.cs create mode 100644 EMU7800/Core/CartDC8K.cs create mode 100644 EMU7800/Core/CartDPC.cs create mode 100644 EMU7800/Core/CartMN16K.cs create mode 100644 EMU7800/Core/CartPB8K.cs create mode 100644 EMU7800/Core/CartTV8K.cs create mode 100644 EMU7800/Core/CartType.cs create mode 100644 EMU7800/Core/ConsoleSwitch.cs create mode 100644 EMU7800/Core/Controller.cs create mode 100644 EMU7800/Core/ControllerAction.cs create mode 100644 EMU7800/Core/DeserializationContext.cs create mode 100644 EMU7800/Core/Emu7800Exception.cs create mode 100644 EMU7800/Core/Emu7800SerializationException.cs create mode 100644 EMU7800/Core/FontRenderer.cs create mode 100644 EMU7800/Core/FrameBuffer.cs create mode 100644 EMU7800/Core/HSC7800.cs create mode 100644 EMU7800/Core/IDevice.cs create mode 100644 EMU7800/Core/ILogger.cs create mode 100644 EMU7800/Core/InputState.cs create mode 100644 EMU7800/Core/LICENSE.TXT create mode 100644 EMU7800/Core/M6502.cs create mode 100644 EMU7800/Core/M6502DASM.cs create mode 100644 EMU7800/Core/Machine2600.cs create mode 100644 EMU7800/Core/Machine2600NTSC.cs create mode 100644 EMU7800/Core/Machine2600PAL.cs create mode 100644 EMU7800/Core/Machine7800.cs create mode 100644 EMU7800/Core/Machine7800NTSC.cs create mode 100644 EMU7800/Core/Machine7800PAL.cs create mode 100644 EMU7800/Core/MachineBase.cs create mode 100644 EMU7800/Core/MachineInput.cs create mode 100644 EMU7800/Core/MachineType.cs create mode 100644 EMU7800/Core/Maria.cs create mode 100644 EMU7800/Core/MariaTables.cs create mode 100644 EMU7800/Core/NullDevice.cs create mode 100644 EMU7800/Core/NullLogger.cs create mode 100644 EMU7800/Core/PIA.cs create mode 100644 EMU7800/Core/PokeySound.cs create mode 100644 EMU7800/Core/RAM6116.cs create mode 100644 EMU7800/Core/SerializationContext.cs create mode 100644 EMU7800/Core/TIA.cs create mode 100644 EMU7800/Core/TIASound.cs create mode 100644 EMU7800/Core/TIATables.cs create mode 100644 EMU7800/EMU7800.csproj create mode 100644 EMU7800/EMU7800.sln create mode 100644 EMU7800/Win/GameProgram.cs create mode 100644 EMU7800/Win/GameProgramLibrary.cs diff --git a/BizHawk.Emulation/BizHawk.Emulation.csproj b/BizHawk.Emulation/BizHawk.Emulation.csproj index e35b6096ec..1d57848153 100644 --- a/BizHawk.Emulation/BizHawk.Emulation.csproj +++ b/BizHawk.Emulation/BizHawk.Emulation.csproj @@ -59,6 +59,10 @@ false + + False + ..\BizHawk.MultiClient\output\dll\EMU7800.dll + 3.5 @@ -151,72 +155,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/BizHawk.Emulation/Consoles/Atari/7800/Atari7800.cs b/BizHawk.Emulation/Consoles/Atari/7800/Atari7800.cs index 8bd3628b26..8d1bff7122 100644 --- a/BizHawk.Emulation/Consoles/Atari/7800/Atari7800.cs +++ b/BizHawk.Emulation/Consoles/Atari/7800/Atari7800.cs @@ -173,22 +173,8 @@ namespace BizHawk.Emulation public void HardReset() { _lagcount = 0; - // show mapper class on romstatusdetails - /* - CoreComm.RomStatusDetails = - string.Format("{0}\r\nSHA1:{1}\r\nMD5:{2}\r\nMapper Impl \"{3}\"", - game.Name, - Util.BytesToHexString(System.Security.Cryptography.SHA1.Create().ComputeHash(rom)), - Util.BytesToHexString(System.Security.Cryptography.MD5.Create().ComputeHash(rom)), - "TODO");*/ cart = Cart.Create(rom, GameInfo.CartType); - - //int[] bob = new int[] { 0, 0, 0 }; - //FileStream fs = new FileStream("C:\\dummy", FileMode.Create, FileAccess.ReadWrite); //TODO: I don't see what this context is used for, see if it can be whacked or pass in a null - //BinaryReader blah = new BinaryReader(fs); - //DeserializationContext george = new DeserializationContext(blah); - ILogger logger = new ConsoleLogger(); HSC7800 hsc7800 = new HSC7800(hsbios, hsram); Bios7800 bios7800 = new Bios7800(bios); @@ -201,8 +187,6 @@ namespace BizHawk.Emulation GameInfo.RController, logger); - //theMachine = new Machine7800NTSC(cart, null, null, logger); - //TODO: clean up, the hs and bios are passed in, the bios has an object AND byte array in the core, and naming is inconsistent theMachine.Reset(); if (avProvider != null) avProvider.Dispose(); diff --git a/EMU7800/Class1.cs b/EMU7800/Class1.cs new file mode 100644 index 0000000000..1b18b6adf6 --- /dev/null +++ b/EMU7800/Class1.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EMU7800 +{ + public class Class1 + { + } +} diff --git a/EMU7800/Core/AddressSpace.cs b/EMU7800/Core/AddressSpace.cs new file mode 100644 index 0000000000..17b8b44f00 --- /dev/null +++ b/EMU7800/Core/AddressSpace.cs @@ -0,0 +1,154 @@ +/* + * AddressSpace.cs + * + * The class representing the memory map or address space of a machine. + * + * Copyright © 2003, 2011 Mike Murphy + * + */ +using System; + +namespace EMU7800.Core +{ + public sealed class AddressSpace + { + public MachineBase M { get; private set; } + + readonly int AddrSpaceShift; + readonly int AddrSpaceSize; + readonly int AddrSpaceMask; + + readonly int PageShift; + readonly int PageSize; + + readonly IDevice[] MemoryMap; + IDevice Snooper; + + public byte DataBusState { get; private set; } + + public override string ToString() + { + return "AddressSpace"; + } + + public byte this[ushort addr] + { + get + { + if (Snooper != null) + { + // here DataBusState is just facilitating a dummy read to the snooper device + // the read operation may have important side effects within the device + DataBusState = Snooper[addr]; + } + var pageno = (addr & AddrSpaceMask) >> PageShift; + var dev = MemoryMap[pageno]; + DataBusState = dev[addr]; + return DataBusState; + } + set + { + DataBusState = value; + if (Snooper != null) + { + Snooper[addr] = DataBusState; + } + var pageno = (addr & AddrSpaceMask) >> PageShift; + var dev = MemoryMap[pageno]; + dev[addr] = DataBusState; + } + } + + public void Map(ushort basea, ushort size, IDevice device) + { + if (device == null) + throw new ArgumentNullException("device"); + + for (int addr = basea; addr < basea + size; addr += PageSize) + { + var pageno = (addr & AddrSpaceMask) >> PageShift; + MemoryMap[pageno] = device; + } + + LogDebug("{0}: Mapped {1} to ${2:x4}:${3:x4}", this, device, basea, basea + size - 1); + } + + public void Map(ushort basea, ushort size, Cart cart) + { + if (cart == null) + throw new ArgumentNullException("cart"); + + cart.Attach(M); + var device = (IDevice)cart; + if (cart.RequestSnooping) + { + Snooper = device; + } + Map(basea, size, device); + } + + #region Constructors + + private AddressSpace() + { + } + + public AddressSpace(MachineBase m, int addrSpaceShift, int pageShift) + { + if (m == null) + throw new ArgumentNullException("m"); + + M = m; + + AddrSpaceShift = addrSpaceShift; + AddrSpaceSize = 1 << AddrSpaceShift; + AddrSpaceMask = AddrSpaceSize - 1; + + PageShift = pageShift; + PageSize = 1 << PageShift; + + MemoryMap = new IDevice[1 << addrSpaceShift >> PageShift]; + IDevice nullDev = new NullDevice(M); + for (var pageno=0; pageno < MemoryMap.Length; pageno++) + { + MemoryMap[pageno] = nullDev; + } + } + + #endregion + + #region Serialization Members + + public AddressSpace(DeserializationContext input, MachineBase m, int addrSpaceShift, int pageShift) : this(m, addrSpaceShift, pageShift) + { + if (input == null) + throw new ArgumentNullException("input"); + + input.CheckVersion(1); + DataBusState = input.ReadByte(); + } + + public void GetObjectData(SerializationContext output) + { + if (output == null) + throw new ArgumentNullException("output"); + + output.WriteVersion(1); + output.Write(DataBusState); + } + + #endregion + + #region Helpers + + [System.Diagnostics.Conditional("DEBUG")] + void LogDebug(string format, params object[] args) + { + if (M == null || M.Logger == null) + return; + M.Logger.WriteLine(format, args); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/Bios7800.cs b/EMU7800/Core/Bios7800.cs new file mode 100644 index 0000000000..c02b3a648f --- /dev/null +++ b/EMU7800/Core/Bios7800.cs @@ -0,0 +1,65 @@ +/* + * BIOS7800.cs + * + * The BIOS of the Atari 7800. + * + * Copyright © 2004 Mike Murphy + * + */ +using System; + +namespace EMU7800.Core +{ + public sealed class Bios7800 : IDevice + { + readonly byte[] ROM; + readonly ushort Mask; + + public ushort Size { get { return (ushort)ROM.Length; } } + + public void Reset() { } + + public byte this[ushort addr] + { + get { return ROM[addr & Mask]; } + set { } + } + + public Bios7800(byte[] rom) + { + if (rom == null) + throw new ArgumentNullException("rom"); + if (rom.Length != 4096 && rom.Length != 16384) + throw new ArgumentException("ROM size not 4096 or 16384", "rom"); + + ROM = rom; + Mask = (ushort)ROM.Length; + Mask--; + } + + #region Serialization Members + + public Bios7800(DeserializationContext input) + { + if (input == null) + throw new ArgumentNullException("input"); + + input.CheckVersion(1); + ROM = input.ReadExpectedBytes(4096, 16384); + + Mask = (ushort)ROM.Length; + Mask--; + } + + public void GetObjectData(SerializationContext output) + { + if (output == null) + throw new ArgumentNullException("output"); + + output.WriteVersion(1); + output.Write(ROM); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/BufferElement.cs b/EMU7800/Core/BufferElement.cs new file mode 100644 index 0000000000..86a79b196b --- /dev/null +++ b/EMU7800/Core/BufferElement.cs @@ -0,0 +1,55 @@ +namespace EMU7800.Core +{ + /// + /// Frames are composed of s, + /// that group bytes into machine words for efficient array processing. + /// Bytes are packed in logical little endian order. + /// + public struct BufferElement + { + /// + /// The number of bytes contained within a . + /// + public const int SIZE = 4; // 2^SHIFT + + /// + /// The mask value applied against a byte array index to access the individual bytes within a . + /// + public const int MASK = 3; // SIZE - 1 + + /// + /// The left shift value to convert a byte array index to a array index. + /// + public const int SHIFT = 2; + + uint _data; + + /// + /// A convenience accessor for reading/writing individual bytes within this . + /// + /// + public byte this[int offset] + { + get + { + var i = (offset & MASK) << 3; + return (byte)(_data >> i); + } + set + { + var i = (offset & MASK) << 3; + var d = (uint)value << i; + var di = (uint)0xff << i; + _data = _data & ~di | d; + } + } + + /// + /// Zeros out all bytes of this . + /// + public void ClearAll() + { + _data = 0; + } + } +} diff --git a/EMU7800/Core/Cart.cs b/EMU7800/Core/Cart.cs new file mode 100644 index 0000000000..bd0e6a2aea --- /dev/null +++ b/EMU7800/Core/Cart.cs @@ -0,0 +1,170 @@ +/* + * Cart.cs + * + * An abstraction of a game cart. Attributable to Kevin Horton's Bankswitching + * document, the Stella source code, and Eckhard Stolberg's 7800 Bankswitching Guide. + * + * Copyright © 2003, 2004, 2010, 2011 Mike Murphy + * + */ +using System; + +namespace EMU7800.Core +{ + public abstract class Cart : IDevice + { + static int _multicartBankSelector; + + protected MachineBase M { get; set; } + protected internal byte[] ROM { get; set; } + + #region IDevice Members + + public virtual void Reset() { } + + public abstract byte this[ushort addr] { get; set; } + + #endregion + + public virtual void Attach(MachineBase m) + { + if (m == null) + throw new ArgumentNullException("m"); + if (M != null && M != m) + throw new InvalidOperationException("Cart already attached to a different machine."); + M = m; + } + + public virtual void StartFrame() + { + } + + public virtual void EndFrame() + { + } + + protected internal virtual bool RequestSnooping + { + get { return false; } + } + + /// + /// Creates an instance of the specified cart. + /// + /// + /// + /// Specified CartType is unexpected. + public static Cart Create(byte[] romBytes, CartType cartType) + { + if (cartType == CartType.None) + { + switch (romBytes.Length) + { + case 2048: + cartType = CartType.A2K; + break; + case 4096: + cartType = CartType.A4K; + break; + case 8192: + cartType = CartType.A8K; + break; + case 16384: + cartType = CartType.A16K; + break; + case 32768: + cartType = CartType.A32K; + break; + } + } + + switch (cartType) + { + case CartType.A2K: return new CartA2K(romBytes); + case CartType.A4K: return new CartA4K(romBytes); + case CartType.A8K: return new CartA8K(romBytes); + case CartType.A8KR: return new CartA8KR(romBytes); + case CartType.A16K: return new CartA16K(romBytes); + case CartType.A16KR: return new CartA16KR(romBytes); + case CartType.DC8K: return new CartDC8K(romBytes); + case CartType.PB8K: return new CartPB8K(romBytes); + case CartType.TV8K: return new CartTV8K(romBytes); + case CartType.CBS12K: return new CartCBS12K(romBytes); + case CartType.A32K: return new CartA32K(romBytes); + case CartType.A32KR: return new CartA32KR(romBytes); + case CartType.MN16K: return new CartMN16K(romBytes); + case CartType.DPC: return new CartDPC(romBytes); + case CartType.M32N12K: return new CartA2K(romBytes, _multicartBankSelector++); + case CartType.A7808: return new Cart7808(romBytes); + case CartType.A7816: return new Cart7816(romBytes); + case CartType.A7832P: return new Cart7832P(romBytes); + case CartType.A7832: return new Cart7832(romBytes); + case CartType.A7848: return new Cart7848(romBytes); + case CartType.A78SGP: return new Cart78SGP(romBytes); + case CartType.A78SG: return new Cart78SG(romBytes, false); + case CartType.A78SGR: return new Cart78SG(romBytes, true); + case CartType.A78S9: return new Cart78S9(romBytes); + case CartType.A78S4: return new Cart78S4(romBytes, false); + case CartType.A78S4R: return new Cart78S4(romBytes, true); + case CartType.A78AB: return new Cart78AB(romBytes); + case CartType.A78AC: return new Cart78AC(romBytes); + default: + throw new Emu7800Exception("Unexpected CartType: " + cartType); + } + } + + protected void LoadRom(byte[] romBytes, int multicartBankSize, int multicartBankNo) + { + if (romBytes == null) + throw new ArgumentNullException("romBytes"); + + ROM = new byte[multicartBankSize]; + Buffer.BlockCopy(romBytes, multicartBankSize*multicartBankNo, ROM, 0, multicartBankSize); + } + + protected void LoadRom(byte[] romBytes, int minSize) + { + if (romBytes == null) + throw new ArgumentNullException("romBytes"); + + if (romBytes.Length >= minSize) + { + ROM = romBytes; + } + else + { + ROM = new byte[minSize]; + Buffer.BlockCopy(romBytes, 0, ROM, 0, romBytes.Length); + } + } + + protected void LoadRom(byte[] romBytes) + { + LoadRom(romBytes, romBytes.Length); + } + + protected Cart() + { + } + + #region Serialization Members + + protected Cart(DeserializationContext input) + { + if (input == null) + throw new ArgumentNullException("input"); + + input.CheckVersion(1); + } + + public virtual void GetObjectData(SerializationContext output) + { + if (output == null) + throw new ArgumentNullException("output"); + + output.WriteVersion(1); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/Cart7808.cs b/EMU7800/Core/Cart7808.cs new file mode 100644 index 0000000000..7fa1c277bc --- /dev/null +++ b/EMU7800/Core/Cart7808.cs @@ -0,0 +1,50 @@ +namespace EMU7800.Core +{ + /// + /// Atari 7800 non-bankswitched 8KB cartridge + /// + public sealed class Cart7808 : Cart + { + // + // Cart Format Mapping to ROM Address Space + // 0x0000:0x2000 0xE000:0x2000 (repeated downward to 0x4000) + // + + #region IDevice Members + + public override byte this[ushort addr] + { + get { return ROM[addr & 0x1fff]; } + set { } + } + + #endregion + + private Cart7808() + { + } + + public Cart7808(byte[] romBytes) + { + LoadRom(romBytes, 0x2000); + } + + #region Serialization Members + + public Cart7808(DeserializationContext input, MachineBase m) : base(input) + { + input.CheckVersion(1); + LoadRom(input.ReadExpectedBytes(0x2000), 0x2000); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(ROM); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/Cart7816.cs b/EMU7800/Core/Cart7816.cs new file mode 100644 index 0000000000..fe93e39585 --- /dev/null +++ b/EMU7800/Core/Cart7816.cs @@ -0,0 +1,50 @@ +namespace EMU7800.Core +{ + /// + /// Atari 7800 non-bankswitched 16KB cartridge + /// + public sealed class Cart7816 : Cart + { + // + // Cart Format Mapping to ROM Address Space + // 0x0000:0x4000 0xC000:0x4000 (repeated downward to 0x4000) + // + + #region IDevice Members + + public override byte this[ushort addr] + { + get { return ROM[addr & 0x3fff]; } + set { } + } + + #endregion + + private Cart7816() + { + } + + public Cart7816(byte[] romBytes) + { + LoadRom(romBytes, 0x4000); + } + + #region Serialization Members + + public Cart7816(DeserializationContext input, MachineBase m) : base(input) + { + input.CheckVersion(1); + LoadRom(input.ReadExpectedBytes(0x4000), 0x4000); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(ROM); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/Cart7832.cs b/EMU7800/Core/Cart7832.cs new file mode 100644 index 0000000000..601e2f83f8 --- /dev/null +++ b/EMU7800/Core/Cart7832.cs @@ -0,0 +1,50 @@ +namespace EMU7800.Core +{ + /// + /// Atari 7800 non-bankswitched 32KB cartridge + /// + public sealed class Cart7832 : Cart + { + // + // Cart Format Mapping to ROM Address Space + // 0x0000:0x8000 0x8000:0x8000 (repeated downward until 0x4000) + // + + #region IDevice Members + + public override byte this[ushort addr] + { + get { return ROM[addr & 0x7fff]; } + set { } + } + + #endregion + + private Cart7832() + { + } + + public Cart7832(byte[] romBytes) + { + LoadRom(romBytes, 0x8000); + } + + #region Serialization Members + + public Cart7832(DeserializationContext input, MachineBase m) : base(input) + { + input.CheckVersion(1); + LoadRom(input.ReadExpectedBytes(0x8000), 0x8000); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(ROM); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/Cart7832P.cs b/EMU7800/Core/Cart7832P.cs new file mode 100644 index 0000000000..c3a24be76e --- /dev/null +++ b/EMU7800/Core/Cart7832P.cs @@ -0,0 +1,90 @@ +namespace EMU7800.Core +{ + /// + /// Atari 7800 non-bankswitched 32KB cartridge w/Pokey + /// + public sealed class Cart7832P : Cart + { + // + // Cart Format Mapping to ROM Address Space + // 0x4000:0x4000 Pokey + // 0x0000:0x8000 0x8000:0x8000 + // + PokeySound _pokeySound; + + #region IDevice Members + + public override void Reset() + { + base.Reset(); + if (_pokeySound != null) + _pokeySound.Reset(); + } + + public override byte this[ushort addr] + { + get + { + return ((addr & 0xF000) == 0x4000 && _pokeySound != null) ? _pokeySound.Read(addr) : ROM[addr & 0x7fff]; + } + set + { + if ((addr & 0xF000) == 0x4000 && _pokeySound != null) + _pokeySound.Update(addr, value); + } + } + + #endregion + + public override void Attach(MachineBase m) + { + base.Attach(m); + if (_pokeySound == null) + _pokeySound = new PokeySound(M); + } + + public override void StartFrame() + { + if (_pokeySound != null) + _pokeySound.StartFrame(); + } + + public override void EndFrame() + { + if (_pokeySound != null) + _pokeySound.EndFrame(); + } + + private Cart7832P() + { + } + + public Cart7832P(byte[] romBytes) + { + LoadRom(romBytes, 0x8000); + } + + #region Serialization Members + + public Cart7832P(DeserializationContext input, MachineBase m) : base(input) + { + input.CheckVersion(1); + LoadRom(input.ReadExpectedBytes(0x8000), 0x8000); + _pokeySound = input.ReadOptionalPokeySound(m); + } + + public override void GetObjectData(SerializationContext output) + { + if (_pokeySound == null) + throw new Emu7800SerializationException("Cart7832P must be attached before serialization."); + + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(ROM); + output.WriteOptional(_pokeySound); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/Cart7848.cs b/EMU7800/Core/Cart7848.cs new file mode 100644 index 0000000000..b9536da7eb --- /dev/null +++ b/EMU7800/Core/Cart7848.cs @@ -0,0 +1,50 @@ +namespace EMU7800.Core +{ + /// + /// Atari 7800 non-bankswitched 48KB cartridge + /// + public sealed class Cart7848 : Cart + { + // + // Cart Format Mapping to ROM Address Space + // 0x0000:0xc000 0x4000:0xc000 + // + + #region IDevice Members + + public override byte this[ushort addr] + { + get { return ROM[addr - 0x4000]; } + set { } + } + + #endregion + + private Cart7848() + { + } + + public Cart7848(byte[] romBytes) + { + LoadRom(romBytes, 0xc000); + } + + #region Serialization Members + + public Cart7848(DeserializationContext input, MachineBase m) : base(input) + { + input.CheckVersion(1); + LoadRom(input.ReadExpectedBytes(0xc000), 0xc000); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(ROM); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/Cart78AB.cs b/EMU7800/Core/Cart78AB.cs new file mode 100644 index 0000000000..6b4b6b53a7 --- /dev/null +++ b/EMU7800/Core/Cart78AB.cs @@ -0,0 +1,67 @@ +namespace EMU7800.Core +{ + /// + /// Atari 7800 Absolute bankswitched cartridge + /// + public sealed class Cart78AB : Cart + { + // + // Cart Format Mapping to ROM Address Space + // Bank0: 0x00000:0x4000 + // Bank1: 0x04000:0x4000 0x4000:0x4000 Bank0-1 (0 on startup) + // Bank2: 0x08000:0x4000 0x8000:0x4000 Bank2 + // Bank3: 0x0c000:0x4000 0xc000:0x4000 Bank3 + // + readonly int[] Bank = new int[4]; + + #region IDevice Members + + public override byte this[ushort addr] + { + get { return ROM[ (Bank[addr >> 14] << 14) | (addr & 0x3fff) ]; } + set + { + if ((addr >> 14) == 2) + { + Bank[1] = (value - 1) & 1; + } + } + } + + #endregion + + private Cart78AB() + { + } + + public Cart78AB(byte[] romBytes) + { + Bank[1] = 0; + Bank[2] = 2; + Bank[3] = 3; + LoadRom(romBytes, 0x10000); + } + + #region Serialization Members + + public Cart78AB(DeserializationContext input, MachineBase m) : base(input) + { + var version = input.CheckVersion(1, 2); + LoadRom(input.ReadBytes()); + Bank = input.ReadIntegers(4); + if (version == 1) + input.ReadInt32(); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(2); + output.Write(ROM); + output.Write(Bank); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/Cart78AC.cs b/EMU7800/Core/Cart78AC.cs new file mode 100644 index 0000000000..fcf854c8b5 --- /dev/null +++ b/EMU7800/Core/Cart78AC.cs @@ -0,0 +1,86 @@ +namespace EMU7800.Core +{ + /// + /// Atari 7800 Activision bankswitched cartridge + /// + public sealed class Cart78AC : Cart + { + // + // Cart Format Mapping to ROM Address Space + // Bank0 : 0x00000:0x2000 + // Bank1 : 0x02000:0x2000 + // Bank2 : 0x04000:0x2000 0x4000:0x2000 Bank13 + // Bank3 : 0x06000:0x2000 0x6000:0x2000 Bank12 + // Bank4 : 0x08000:0x2000 0x8000:0x2000 Bank15 + // Bank5 : 0x0a000:0x2000 0xa000:0x2000 Bank(2*n) n in [0-7], n=0 on startup + // Bank6 : 0x0c000:0x2000 0xc000:0x2000 Bank(2*n+1) + // Bank7 : 0x0e000:0x2000 0xe000:0x2000 Bank14 + // Bank8 : 0x10000:0x2000 + // Bank9 : 0x12000:0x2000 + // Bank10: 0x14000:0x2000 + // Bank11: 0x16000:0x2000 + // Bank12: 0x18000:0x2000 + // Bank13: 0x1a000:0x2000 + // Bank14: 0x1c000:0x2000 + // Bank15: 0x1e000:0x2000 + // + // Banks are actually 16KB, but handled as 8KB for implementation ease. + // + readonly int[] Bank = new int[8]; + + #region IDevice Members + + public override byte this[ushort addr] + { + get + { + return ROM[ (Bank[addr >> 13] << 13) | (addr & 0x1fff) ]; + } + set + { + if ((addr & 0xfff0) == 0xff80) + { + Bank[5] = (addr & 7) << 1; + Bank[6] = Bank[5] + 1; + } + } + } + + #endregion + + private Cart78AC() + { + } + + public Cart78AC(byte[] romBytes) + { + Bank[2] = 13; + Bank[3] = 12; + Bank[4] = 15; + Bank[5] = 0; + Bank[6] = 1; + Bank[7] = 14; + LoadRom(romBytes, 0x20000); + } + + #region Serialization Members + + public Cart78AC(DeserializationContext input, MachineBase m) : base(input) + { + input.CheckVersion(1); + LoadRom(input.ReadBytes()); + Bank = input.ReadIntegers(8); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(ROM); + output.Write(Bank); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/Cart78S4.cs b/EMU7800/Core/Cart78S4.cs new file mode 100644 index 0000000000..7920f19713 --- /dev/null +++ b/EMU7800/Core/Cart78S4.cs @@ -0,0 +1,89 @@ +namespace EMU7800.Core +{ + /// + /// Atari 7800 SuperGame S4 bankswitched cartridge + /// + public sealed class Cart78S4 : Cart + { + // + // Cart Format Mapping to ROM Address Space + // Bank0: 0x00000:0x4000 + // Bank1: 0x04000:0x4000 0x4000:0x4000 Bank2 + // Bank2: 0x08000:0x4000 0x8000:0x4000 Bank0 (0 on startup) + // Bank3: 0x0c000:0x4000 0xc000:0x4000 Bank3 + // + // Banks 0-3 are the same as banks 4-7 + // + readonly byte[] RAM; + readonly int[] Bank = new int[4]; + + #region IDevice Members + + public override byte this[ushort addr] + { + get + { + if (RAM != null && addr >= 0x6000 && addr <= 0x7fff) + { + return RAM[addr & 0x1fff]; + } + return ROM[(Bank[addr >> 14] << 14) | (addr & 0x3fff)]; + } + set + { + if (RAM != null && addr >= 0x6000 && addr <= 0x7fff) + { + RAM[addr & 0x1fff] = value; + } + else if ((addr >> 14) == 2) + { + Bank[2] = value & 3; + } + } + } + + #endregion + + private Cart78S4() + { + } + + public Cart78S4(byte[] romBytes, bool needRAM) + { + if (needRAM) + { + RAM = new byte[0x2000]; + } + + LoadRom(romBytes, 0xffff); + + Bank[1] = 2; + Bank[2] = 0; + Bank[3] = 3; + } + + #region Serialization Members + + public Cart78S4(DeserializationContext input, MachineBase m) : base(input) + { + var version = input.CheckVersion(1, 2); + LoadRom(input.ReadBytes()); + Bank = input.ReadIntegers(4); + if (version == 1) + input.ReadInt32(); + RAM = input.ReadOptionalBytes(); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(2); + output.Write(ROM); + output.Write(Bank); + output.WriteOptional(RAM); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/Cart78S9.cs b/EMU7800/Core/Cart78S9.cs new file mode 100644 index 0000000000..63b7ff0394 --- /dev/null +++ b/EMU7800/Core/Cart78S9.cs @@ -0,0 +1,70 @@ +namespace EMU7800.Core +{ + /// + /// Atari 7800 SuperGame S9 bankswitched cartridge + /// + public sealed class Cart78S9 : Cart + { + // + // Cart Format Mapping to ROM Address Space + // Bank0: 0x00000:0x4000 + // Bank1: 0x04000:0x4000 0x4000:0x4000 Bank0 + // Bank2: 0x08000:0x4000 0x8000:0x4000 Bank0-8 (1 on startup) + // Bank3: 0x0c000:0x4000 0xc000:0x4000 Bank8 + // Bank4: 0x10000:0x4000 + // Bank5: 0x14000:0x4000 + // Bank6: 0x18000:0x4000 + // Bank7: 0x1c000:0x4000 + // Bank8: 0x20000:0x4000 + // + readonly int[] Bank = new int[4]; + + #region IDevice Members + + public override byte this[ushort addr] + { + get { return ROM[ (Bank[addr >> 14] << 14) | (addr & 0x3fff) ]; } + set + { + if ((addr >> 14) == 2 && value < 8) + { + Bank[2] = (value + 1); + } + } + } + + #endregion + + private Cart78S9() + { + } + + public Cart78S9(byte[] romBytes) + { + Bank[1] = 0; + Bank[2] = 1; + Bank[3] = 8; + LoadRom(romBytes, 0x24000); + } + + #region Serialization Members + + public Cart78S9(DeserializationContext input, MachineBase m) : base(input) + { + input.CheckVersion(1); + LoadRom(input.ReadBytes()); + Bank = input.ReadIntegers(4); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(ROM); + output.Write(Bank); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/Cart78SG.cs b/EMU7800/Core/Cart78SG.cs new file mode 100644 index 0000000000..71dcdcfb64 --- /dev/null +++ b/EMU7800/Core/Cart78SG.cs @@ -0,0 +1,94 @@ +namespace EMU7800.Core +{ + /// + /// Atari 7800 SuperGame bankswitched cartridge + /// + public sealed class Cart78SG : Cart + { + // + // Cart Format Mapping to ROM Address Space + // Bank0: 0x00000:0x4000 + // Bank1: 0x04000:0x4000 0x4000:0x4000 Bank6 + // Bank2: 0x08000:0x4000 0x8000:0x4000 Bank0-7 (0 on startup) + // Bank3: 0x0c000:0x4000 0xc000:0x4000 Bank7 + // Bank4: 0x10000:0x4000 + // Bank5: 0x14000:0x4000 + // Bank6: 0x18000:0x4000 + // Bank7: 0x1c000:0x4000 + // + readonly int[] Bank = new int[4]; + readonly byte[] RAM; + + #region IDevice Members + + public override byte this[ushort addr] + { + get + { + var bankNo = addr >> 14; + if (RAM != null && bankNo == 1) + { + return RAM[addr & 0x3fff]; + } + return ROM[ (Bank[bankNo] << 14) | (addr & 0x3fff) ]; + } + set + { + var bankNo = addr >> 14; + if (bankNo == 2) + { + Bank[2] = value & 7; + } + else if (RAM != null && bankNo == 1) + { + RAM[addr & 0x3fff] = value; + } + } + } + + #endregion + + private Cart78SG() + { + } + + public Cart78SG(byte[] romBytes, bool needRAM) + { + if (needRAM) + { + // This works for titles that use 8KB instead of 16KB + RAM = new byte[0x4000]; + } + + Bank[1] = 6; + Bank[2] = 0; + Bank[3] = 7; + + LoadRom(romBytes, 0x20000); + } + + #region Serialization Members + + public Cart78SG(DeserializationContext input, MachineBase m) : base(input) + { + var version = input.CheckVersion(1, 2); + LoadRom(input.ReadBytes()); + Bank = input.ReadIntegers(4); + if (version == 1) + input.ReadInt32(); + RAM = input.ReadOptionalBytes(0x4000); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(2); + output.Write(ROM); + output.Write(Bank); + output.WriteOptional(RAM); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/Cart78SGP.cs b/EMU7800/Core/Cart78SGP.cs new file mode 100644 index 0000000000..65feda2f84 --- /dev/null +++ b/EMU7800/Core/Cart78SGP.cs @@ -0,0 +1,118 @@ +namespace EMU7800.Core +{ + /// + /// Atari 7800 SuperGame bankswitched cartridge w/Pokey + /// + public sealed class Cart78SGP : Cart + { + // + // Cart Format Mapping to ROM Address Space + // Bank0: 0x00000:0x4000 + // Bank1: 0x04000:0x4000 0x4000:0x4000 Pokey + // Bank2: 0x08000:0x4000 0x8000:0x4000 Bank0-7 (0 on startup) + // Bank3: 0x0c000:0x4000 0xc000:0x4000 Bank7 + // Bank4: 0x10000:0x4000 + // Bank5: 0x14000:0x4000 + // Bank6: 0x18000:0x4000 + // Bank7: 0x1c000:0x4000 + // + readonly int[] _bank = new int[4]; + PokeySound _pokeySound; + + #region IDevice Members + + public override void Reset() + { + base.Reset(); + if (_pokeySound != null) + _pokeySound.Reset(); + } + + public override byte this[ushort addr] + { + get + { + var bankNo = addr >> 14; + switch (bankNo) + { + case 1: + return (_pokeySound != null) ? _pokeySound.Read(addr) : (byte)0; + default: + return ROM[(_bank[bankNo] << 14) | (addr & 0x3fff)]; + } + } + set + { + var bankNo = addr >> 14; + switch (bankNo) + { + case 1: + if (_pokeySound != null) + _pokeySound.Update(addr, value); + break; + case 2: + _bank[2] = value & 7; + break; + } + } + } + + #endregion + + public override void Attach(MachineBase m) + { + base.Attach(m); + if (_pokeySound == null) + _pokeySound = new PokeySound(M); + } + + public override void StartFrame() + { + if (_pokeySound != null) + _pokeySound.StartFrame(); + } + + public override void EndFrame() + { + if (_pokeySound != null) + _pokeySound.EndFrame(); + } + + private Cart78SGP() + { + } + + public Cart78SGP(byte[] romBytes) + { + _bank[2] = 0; + _bank[3] = 7; + + LoadRom(romBytes, 0x20000); + } + + #region Serialization Members + + public Cart78SGP(DeserializationContext input, MachineBase m) : base(input) + { + input.CheckVersion(1); + LoadRom(input.ReadBytes()); + _bank = input.ReadIntegers(4); + _pokeySound = input.ReadOptionalPokeySound(m); + } + + public override void GetObjectData(SerializationContext output) + { + if (_pokeySound == null) + throw new Emu7800SerializationException("Cart78SGP must be attached before serialization."); + + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(ROM); + output.Write(_bank); + output.WriteOptional(_pokeySound); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/CartA16K.cs b/EMU7800/Core/CartA16K.cs new file mode 100644 index 0000000000..69bcc68700 --- /dev/null +++ b/EMU7800/Core/CartA16K.cs @@ -0,0 +1,86 @@ +namespace EMU7800.Core +{ + /// + /// Atari standard 16KB bankswitched carts + /// + public sealed class CartA16K : Cart + { + // + // Cart Format Mapping to ROM Address Space + // Bank1: 0x0000:0x1000 0x1000:0x1000 Bank selected by accessing 0x1ff9-0x1ff9 + // Bank2: 0x1000:0x1000 + // Bank3: 0x2000:0x1000 + // Bank4: 0x3000:0x1000 + // + ushort BankBaseAddr; + + int Bank + { + set { BankBaseAddr = (ushort)(value * 0x1000); } + } + + #region IDevice Members + + public override void Reset() + { + Bank = 0; + } + + public override byte this[ushort addr] + { + get + { + addr &= 0x0fff; + UpdateBank(addr); + return ROM[BankBaseAddr + addr]; + } + set + { + addr &= 0x0fff; + UpdateBank(addr); + } + } + + #endregion + + private CartA16K() + { + } + + public CartA16K(byte[] romBytes) + { + LoadRom(romBytes, 0x4000); + Bank = 0; + } + + void UpdateBank(ushort addr) + { + if (addr < 0x0ff6 || addr > 0x0ff9) + {} + else + { + Bank = addr - 0x0ff6; + } + } + + #region Serialization Members + + public CartA16K(DeserializationContext input, MachineBase m) : base(input) + { + input.CheckVersion(1); + LoadRom(input.ReadExpectedBytes(0x4000), 0x4000); + BankBaseAddr = input.ReadUInt16(); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(ROM); + output.Write(BankBaseAddr); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/CartA16KR.cs b/EMU7800/Core/CartA16KR.cs new file mode 100644 index 0000000000..cd5f307bce --- /dev/null +++ b/EMU7800/Core/CartA16KR.cs @@ -0,0 +1,100 @@ +namespace EMU7800.Core +{ + /// + /// Atari standard 16KB bankswitched carts with 128 bytes of RAM + /// + public sealed class CartA16KR : Cart + { + // + // Cart Format Mapping to ROM Address Space + // Bank1: 0x0000:0x1000 0x1000:0x1000 Bank selected by accessing 0x1ff9-0x1ff9 + // Bank2: 0x1000:0x1000 + // Bank3: 0x2000:0x1000 + // Bank4: 0x3000:0x1000 + // Shadows ROM + // 0x1000:0x0080 RAM write port + // 0x1080:0x0080 RAM read port + // + ushort BankBaseAddr; + readonly byte[] RAM; + + int Bank + { + set { BankBaseAddr = (ushort)(value * 0x1000); } + } + + #region IDevice Members + + public override void Reset() + { + Bank = 0; + } + + public override byte this[ushort addr] + { + get + { + addr &= 0x0fff; + if (addr < 0x0100 && addr >= 0x0080) + { + return RAM[addr & 0x7f]; + } + UpdateBank(addr); + return ROM[BankBaseAddr + addr]; + } + set + { + addr &= 0x0fff; + if (addr < 0x0080) + { + RAM[addr & 0x7f] = value; + return; + } + UpdateBank(addr); + } + } + + #endregion + + private CartA16KR() + { + } + + public CartA16KR(byte[] romBytes) + { + LoadRom(romBytes, 0x4000); + Bank = 0; + RAM = new byte[0x80]; + } + + void UpdateBank(ushort addr) + { + if (addr < 0x0ff6 || addr > 0x0ff9) + {} + else + { + Bank = addr - 0x0ff6; + } + } + + #region Serialization Members + + public CartA16KR(DeserializationContext input, MachineBase m) : base(input) + { + input.CheckVersion(1); + LoadRom(input.ReadExpectedBytes(0x4000), 0x4000); + BankBaseAddr = input.ReadUInt16(); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(ROM); + output.Write(BankBaseAddr); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/CartA2K.cs b/EMU7800/Core/CartA2K.cs new file mode 100644 index 0000000000..14674c4138 --- /dev/null +++ b/EMU7800/Core/CartA2K.cs @@ -0,0 +1,56 @@ +namespace EMU7800.Core +{ + /// + /// Atari standard 2KB carts (no bankswitching) + /// + public sealed class CartA2K : Cart + { + // + // Cart Format Mapping to ROM Address Space + // 0x0000:0x0800 0x1000:0x0800 + // 0x1800:0x0800 (1st 2k bank repeated) + // + + #region IDevice Members + + public override byte this[ushort addr] + { + get { return ROM[addr & 0x07ff]; } + set { } + } + + #endregion + + private CartA2K() + { + } + + public CartA2K(byte[] romBytes) + { + LoadRom(romBytes, 0x0800); + } + + public CartA2K(byte[] romBytes, int multicartBankSelector) + { + LoadRom(romBytes, 0x800, multicartBankSelector & 0x1f); + } + + #region Serialization Members + + public CartA2K(DeserializationContext input, MachineBase m) : base(input) + { + input.CheckVersion(1); + LoadRom(input.ReadExpectedBytes(0x0800), 0x0800); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(ROM); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/CartA32K.cs b/EMU7800/Core/CartA32K.cs new file mode 100644 index 0000000000..d3140ac915 --- /dev/null +++ b/EMU7800/Core/CartA32K.cs @@ -0,0 +1,88 @@ +namespace EMU7800.Core +{ + /// + /// Atari standard 32KB bankswitched carts + /// + public sealed class CartA32K : Cart + { + // + // Cart Format Mapping to ROM Address Space + // Bank1: 0x0000:0x1000 0x1000:0x1000 Bank selected by accessing 0x0ff4-0x0ffc + // Bank2: 0x1000:0x1000 + // Bank3: 0x2000:0x1000 + // Bank4: 0x3000:0x1000 + // Bank5: 0x4000:0x1000 + // Bank6: 0x5000:0x1000 + // Bank7: 0x6000:0x1000 + // Bank8: 0x7000:0x1000 + // + ushort BankBaseAddr; + + int Bank + { + set { BankBaseAddr = (ushort)(value * 0x1000); } + } + + #region IDevice Members + + public override void Reset() + { + Bank = 7; + } + + public override byte this[ushort addr] + { + get + { + addr &= 0x0fff; + UpdateBank(addr); + return ROM[BankBaseAddr + addr]; + } + set + { + addr &= 0x0fff; + UpdateBank(addr); + } + } + + #endregion + + private CartA32K() + { + } + + public CartA32K(byte[] romBytes) + { + LoadRom(romBytes, 0x8000); + Bank = 7; + } + + void UpdateBank(ushort addr) + { + if (addr < 0x0ffc && addr >= 0x0ff4) + { + Bank = addr - 0x0ff4; + } + } + + #region Serialization Members + + public CartA32K(DeserializationContext input, MachineBase m) : base(input) + { + input.CheckVersion(1); + LoadRom(input.ReadExpectedBytes(0x8000), 0x8000); + BankBaseAddr = input.ReadUInt16(); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(ROM); + output.Write(BankBaseAddr); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/CartA32KR.cs b/EMU7800/Core/CartA32KR.cs new file mode 100644 index 0000000000..74e43461cb --- /dev/null +++ b/EMU7800/Core/CartA32KR.cs @@ -0,0 +1,104 @@ +namespace EMU7800.Core +{ + /// + /// Atari standard 32KB bankswitched carts with 128 bytes of RAM + /// + public sealed class CartA32KR : Cart + { + // + // Cart Format Mapping to ROM Address Space + // Bank1: 0x0000:0x1000 0x1000:0x1000 Bank selected by accessing 0x0ff4-0x0ffc + // Bank2: 0x1000:0x1000 + // Bank3: 0x2000:0x1000 + // Bank4: 0x3000:0x1000 + // Bank5: 0x4000:0x1000 + // Bank6: 0x5000:0x1000 + // Bank7: 0x6000:0x1000 + // Bank8: 0x7000:0x1000 + // Shadows ROM + // 0x1000:0x80 RAM write port + // 0x1080:0x80 RAM read port + // + ushort BankBaseAddr; + readonly byte[] RAM; + + int Bank + { + set { BankBaseAddr = (ushort)(value * 0x1000); } + } + + #region IDevice Members + + public override void Reset() + { + Bank = 7; + } + + public override byte this[ushort addr] + { + get + { + addr &= 0x0fff; + if (addr >= 0x0080 && addr < 0x0100) + { + return RAM[addr & 0x007f]; + } + UpdateBank(addr); + return ROM[BankBaseAddr + addr]; + } + set + { + addr &= 0x0fff; + if (addr < 0x0080) + { + RAM[addr & 0x007f] = value; + return; + } + UpdateBank(addr); + } + } + + #endregion + + private CartA32KR() + { + } + + public CartA32KR(byte[] romBytes) + { + LoadRom(romBytes, 0x8000); + RAM = new byte[0x80]; + Bank = 7; + } + + void UpdateBank(ushort addr) + { + if (addr < 0x0ffc && addr >= 0x0ff4 ) + { + Bank = addr - 0x0ff4; + } + } + + #region Serialization Members + + public CartA32KR(DeserializationContext input, MachineBase m) : base(input) + { + input.CheckVersion(1); + LoadRom(input.ReadExpectedBytes(0x8000), 0x8000); + RAM = input.ReadExpectedBytes(0x80); + BankBaseAddr = input.ReadUInt16(); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(ROM); + output.Write(RAM); + output.Write(BankBaseAddr); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/CartA4K.cs b/EMU7800/Core/CartA4K.cs new file mode 100644 index 0000000000..17726a3c2b --- /dev/null +++ b/EMU7800/Core/CartA4K.cs @@ -0,0 +1,47 @@ +namespace EMU7800.Core +{ + /// + /// Atari standard 4KB carts (no bankswitching) + /// + public sealed class CartA4K : Cart + { + #region IDevice Members + + public override void Reset() { } + + public override byte this[ushort addr] + { + get { return ROM[addr & 0x0fff]; } + set { } + } + + #endregion + + private CartA4K() + { + } + + public CartA4K(byte[] romBytes) + { + LoadRom(romBytes, 0x1000); + } + + #region Serialization Members + + public CartA4K(DeserializationContext input, MachineBase m) : base(input) + { + input.CheckVersion(1); + LoadRom(input.ReadExpectedBytes(0x1000), 0x1000); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(ROM); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/CartA8K.cs b/EMU7800/Core/CartA8K.cs new file mode 100644 index 0000000000..c8f531e5cb --- /dev/null +++ b/EMU7800/Core/CartA8K.cs @@ -0,0 +1,87 @@ +namespace EMU7800.Core +{ + /// + /// Atari standard 8KB bankswitched carts + /// + public sealed class CartA8K : Cart + { + // + // Cart Format Mapping to ROM Address Space + // Bank1: 0x0000:0x1000 0x1000:0x1000 Bank selected by accessing 0x1ff8,0x1ff9 + // Bank2: 0x1000:0x1000 + // + ushort BankBaseAddr; + + int Bank + { + set { BankBaseAddr = (ushort)(value * 0x1000); } + } + + #region IDevice Members + + public override void Reset() + { + Bank = 1; + } + + public override byte this[ushort addr] + { + get + { + addr &= 0x0fff; + UpdateBank(addr); + return ROM[BankBaseAddr + addr]; + } + set + { + addr &= 0x0fff; + UpdateBank(addr); + } + } + + #endregion + + private CartA8K() + { + } + + public CartA8K(byte[] romBytes) + { + LoadRom(romBytes, 0x2000); + Bank = 1; + } + + void UpdateBank(ushort addr) + { + switch(addr) + { + case 0x0ff8: + Bank = 0; + break; + case 0x0ff9: + Bank = 1; + break; + } + } + + #region Serialization Members + + public CartA8K(DeserializationContext input, MachineBase m) : base(input) + { + input.CheckVersion(1); + LoadRom(input.ReadExpectedBytes(0x2000), 0x2000); + BankBaseAddr = input.ReadUInt16(); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(ROM); + output.Write(BankBaseAddr); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/CartA8KR.cs b/EMU7800/Core/CartA8KR.cs new file mode 100644 index 0000000000..a0fd89e034 --- /dev/null +++ b/EMU7800/Core/CartA8KR.cs @@ -0,0 +1,103 @@ +namespace EMU7800.Core +{ + /// + /// Atari standard 8KB bankswitched carts with 128 bytes of RAM + /// + public sealed class CartA8KR : Cart + { + // + // Cart Format Mapping to ROM Address Space + // Bank1: 0x0000:0x1000 0x1000:0x1000 Bank selected by accessing 0x1ff8,0x1ff9 + // Bank2: 0x1000:0x1000 + // Shadows ROM + // 0x1000:0x0080 RAM write port + // 0x1080:0x0080 RAM read port + // + ushort BankBaseAddr; + readonly byte[] RAM; + + int Bank + { + set { BankBaseAddr = (ushort)(value * 0x1000); } + } + + #region IDevice Members + + public override void Reset() + { + Bank = 1; + } + + public override byte this[ushort addr] + { + get + { + addr &= 0x0fff; + if (addr < 0x0100 && addr >= 0x0080) + { + return RAM[addr & 0x7f]; + } + UpdateBank(addr); + return ROM[BankBaseAddr + addr]; + } + set + { + addr &= 0x0fff; + if (addr < 0x0080) + { + RAM[addr & 0x7f] = value; + return; + } + UpdateBank(addr); + } + } + + #endregion + + private CartA8KR() + { + } + + public CartA8KR(byte[] romBytes) + { + LoadRom(romBytes, 0x2000); + Bank = 1; + RAM = new byte[0x80]; + } + + void UpdateBank(ushort addr) + { + switch(addr) + { + case 0x0ff8: + Bank = 0; + break; + case 0x0ff9: + Bank = 1; + break; + } + } + + #region Serialization Members + + public CartA8KR(DeserializationContext input, MachineBase m) : base(input) + { + input.CheckVersion(1); + LoadRom(input.ReadExpectedBytes(0x2000), 0x2000); + RAM = input.ReadExpectedBytes(0x80); + BankBaseAddr = input.ReadUInt16(); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(ROM); + output.Write(RAM); + output.Write(BankBaseAddr); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/CartCBS12K.cs b/EMU7800/Core/CartCBS12K.cs new file mode 100644 index 0000000000..9b0d7bbece --- /dev/null +++ b/EMU7800/Core/CartCBS12K.cs @@ -0,0 +1,100 @@ +namespace EMU7800.Core +{ + /// + /// CBS RAM Plus 12KB bankswitched carts with 128 bytes of RAM. + /// + public sealed class CartCBS12K : Cart + { + // + // Cart Format Mapping to ROM Address Space + // Bank1: 0x0000:0x1000 Bank1:0x1000:0x1000 Select Segment: 0ff8-0ffa + // Bank2: 0x1000:0x1000 + // Bank3: 0x2000:0x1000 + // Shadows ROM + // 0x1000:0x80 RAM write port + // 0x1080:0x80 RAM read port + // + ushort BankBaseAddr; + readonly byte[] RAM; + + int Bank + { + set { BankBaseAddr = (ushort)(value * 0x1000); } + } + + #region IDevice Members + + public override void Reset() + { + Bank = 2; + } + + public override byte this[ushort addr] + { + get + { + addr &= 0x0fff; + if (addr < 0x0200 && addr >= 0x0100) + { + return RAM[addr & 0xff]; + } + UpdateBank(addr); + return ROM[BankBaseAddr + addr]; + } + set + { + addr &= 0x0fff; + if (addr < 0x0100) + { + RAM[addr & 0xff] = value; + return; + } + UpdateBank(addr); + } + } + + #endregion + + private CartCBS12K() + { + } + + public CartCBS12K(byte[] romBytes) + { + LoadRom(romBytes, 0x3000); + Bank = 2; + RAM = new byte[0x100]; + } + + void UpdateBank(ushort addr) + { + if (addr < 0x0ff8 || addr > 0x0ffa) { } + else + { + Bank = addr - 0x0ff8; + } + } + + #region Serialization Members + + public CartCBS12K(DeserializationContext input, MachineBase m) : base(input) + { + input.CheckVersion(1); + LoadRom(input.ReadExpectedBytes(0x3000), 0x3000); + RAM = input.ReadExpectedBytes(0x100); + BankBaseAddr = input.ReadUInt16(); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(ROM); + output.Write(RAM); + output.Write(BankBaseAddr); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/CartDC8K.cs b/EMU7800/Core/CartDC8K.cs new file mode 100644 index 0000000000..0b3808e24c --- /dev/null +++ b/EMU7800/Core/CartDC8K.cs @@ -0,0 +1,55 @@ +namespace EMU7800.Core +{ + /// + /// Activison's Robot Tank and Decathlon 8KB bankswitching cart. + /// + public sealed class CartDC8K : Cart + { + // + // Cart Format Mapping to ROM Address Space + // Bank1: 0x0000:0x1000 0x1000:0x1000 Bank selected by A13=0/1? + // Bank2: 0x1000:0x1000 + // + // This does what the Stella code does, which is to follow A13 to determine + // the bank. Since A0-A12 are the only significant bits on the program + // counter, I am unsure how the cart/hardware could utilize this. + // + + #region IDevice Members + + public override byte this[ushort addr] + { + get { return (addr & 0x2000) == 0 ? ROM[addr & 0x0fff + 0x1000] : ROM[addr & 0x0fff]; } + set { } + } + + #endregion + + private CartDC8K() + { + } + + public CartDC8K(byte[] romBytes) + { + LoadRom(romBytes, 0x2000); + } + + #region Serialization Members + + public CartDC8K(DeserializationContext input, MachineBase m) : base(input) + { + input.CheckVersion(1); + LoadRom(input.ReadExpectedBytes(0x2000), 0x2000); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(ROM); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/CartDPC.cs b/EMU7800/Core/CartDPC.cs new file mode 100644 index 0000000000..4695f64df7 --- /dev/null +++ b/EMU7800/Core/CartDPC.cs @@ -0,0 +1,341 @@ +namespace EMU7800.Core +{ + /// + /// Pitfall II cartridge. + /// There are two 4k banks, 2k display bank, and the DPC chip. + /// For complete details on the DPC chip see David P. Crane's United States Patent Number 4,644,495. + /// + public sealed class CartDPC : Cart + { + // + // Cart Format Mapping to ROM Address Space + // Bank1: 0x0000:0x1000 0x1000:0x1000 Bank selected by accessing 0x1ff8,0x1ff9 + // Bank2: 0x1000:0x1000 + // + const ushort DisplayBaseAddr = 0x2000; + ushort BankBaseAddr; + + readonly byte[] MusicAmplitudes = new byte[] { 0x00, 0x04, 0x05, 0x09, 0x06, 0x0a, 0x0b, 0x0f }; + + readonly byte[] Tops = new byte[8]; + readonly byte[] Bots = new byte[8]; + readonly ushort[] Counters = new ushort[8]; + readonly byte[] Flags = new byte[8]; + readonly bool[] MusicMode = new bool[3]; + + ulong LastSystemClock; + double FractionalClocks; + + byte _ShiftRegister; + + int Bank + { + set { BankBaseAddr = (ushort)(value * 0x1000); } + } + + // + // Generate a sequence of pseudo-random numbers 255 numbers long + // by emulating an 8-bit shift register with feedback taps at + // bits 4, 3, 2, and 0. + byte ShiftRegister + { + get + { + var a = _ShiftRegister; + a &= (1 << 0); + + var x = _ShiftRegister; + x &= (1 << 2); + x >>= 2; + a ^= x; + + x = _ShiftRegister; + x &= (1 << 3); + x >>= 3; + a ^= x; + + x = _ShiftRegister; + x &= (1 << 4); + x >>= 4; + a ^= x; + + a <<= 7; + _ShiftRegister >>= 1; + _ShiftRegister |= a; + + return _ShiftRegister; + } + set { _ShiftRegister = value; } + } + + #region IDevice Members + + public override void Reset() + { + Bank = 1; + LastSystemClock = 3*M.CPU.Clock; + FractionalClocks = 0.0; + ShiftRegister = 1; + } + + public override byte this[ushort addr] + { + get + { + addr &= 0x0fff; + if (addr < 0x0040) + { + return ReadPitfall2Reg(addr); + } + UpdateBank(addr); + return ROM[BankBaseAddr + addr]; + } + set + { + addr &= 0x0fff; + if (addr >= 0x0040 && addr < 0x0080) + { + WritePitfall2Reg(addr, value); + } + else + { + UpdateBank(addr); + } + } + } + + #endregion + + private CartDPC() + { + } + + public CartDPC(byte[] romBytes) + { + LoadRom(romBytes, 0x2800); + Bank = 1; + } + + void UpdateBank(ushort addr) + { + switch(addr) + { + case 0x0ff8: + Bank = 0; + break; + case 0x0ff9: + Bank = 1; + break; + } + } + + byte ReadPitfall2Reg(ushort addr) + { + byte result; + + var i = addr & 0x07; + var fn = (addr >> 3) & 0x07; + + // Update flag register for selected data fetcher + if ((Counters[i] & 0x00ff) == Tops[i]) + { + Flags[i] = 0xff; + } + else if ((Counters[i] & 0x00ff) == Bots[i]) + { + Flags[i] = 0x00; + } + + switch (fn) + { + case 0x00: + if (i < 4) + { + // This is a random number read + result = ShiftRegister; + break; + } + // Its a music read + UpdateMusicModeDataFetchers(); + + byte j = 0; + if (MusicMode[0] && Flags[5] != 0) + { + j |= 0x01; + } + if (MusicMode[1] && Flags[6] != 0) + { + j |= 0x02; + } + if (MusicMode[2] && Flags[7] != 0) + { + j |= 0x04; + } + result = MusicAmplitudes[j]; + break; + // DFx display data read + case 0x01: + result = ROM[DisplayBaseAddr + 0x7ff - Counters[i]]; + break; + // DFx display data read AND'd w/flag + case 0x02: + result = ROM[DisplayBaseAddr + 0x7ff - Counters[i]]; + result &= Flags[i]; + break; + // DFx flag + case 0x07: + result = Flags[i]; + break; + default: + result = 0; + break; + } + + // Clock the selected data fetcher's counter if needed + if (i < 5 || i >= 5 && MusicMode[i - 5] == false) + { + Counters[i]--; + Counters[i] &= 0x07ff; + } + + return result; + } + + void UpdateMusicModeDataFetchers() + { + var sysClockDelta = 3*M.CPU.Clock - LastSystemClock; + LastSystemClock = 3*M.CPU.Clock; + + var OSCclocks = ((15750.0 * sysClockDelta) / 1193191.66666667) + FractionalClocks; + + var wholeClocks = (int)OSCclocks; + FractionalClocks = OSCclocks - wholeClocks; + if (wholeClocks <= 0) + { + return; + } + + for (var i=0; i < 3; i++) + { + var r = i + 5; + if (!MusicMode[i]) continue; + + var top = Tops[r] + 1; + var newLow = Counters[r] & 0x00ff; + + if (Tops[r] != 0) + { + newLow -= (wholeClocks % top); + if (newLow < 0) + { + newLow += top; + } + } + else + { + newLow = 0; + } + + if (newLow <= Bots[r]) + { + Flags[r] = 0x00; + } + else if (newLow <= Tops[r]) + { + Flags[r] = 0xff; + } + + Counters[r] = (ushort)((Counters[r] & 0x0700) | (ushort)newLow); + } + } + + void WritePitfall2Reg(ushort addr, byte val) + { + var i = addr & 0x07; + var fn = (addr >> 3) & 0x07; + + switch (fn) + { + // DFx top count + case 0x00: + Tops[i] = val; + Flags[i] = 0x00; + break; + // DFx bottom count + case 0x01: + Bots[i] = val; + break; + // DFx counter low + case 0x02: + Counters[i] &= 0x0700; + if (i >= 5 && MusicMode[i - 5]) + { + // Data fetcher is in music mode so its low counter value + // should be loaded from the top register not the poked value + Counters[i] |= Tops[i]; + } + else + { + // Data fetcher is either not a music mode data fetcher or it + // isn't in music mode so it's low counter value should be loaded + // with the poked value + Counters[i] |= val; + } + break; + // DFx counter high + case 0x03: + Counters[i] &= 0x00ff; + Counters[i] |= (ushort)((val & 0x07) << 8); + // Execute special code for music mode data fetchers + if (i >= 5) + { + MusicMode[i - 5] = (val & 0x10) != 0; + // NOTE: We are not handling the clock source input for + // the music mode data fetchers. We're going to assume + // they always use the OSC input. + } + break; + // Random Number Generator Reset + case 0x06: + ShiftRegister = 1; + break; + } + } + + #region Serialization Members + + public CartDPC(DeserializationContext input, MachineBase m) : base(input) + { + input.CheckVersion(1); + LoadRom(input.ReadExpectedBytes(0x2800), 0x2800); + BankBaseAddr = input.ReadUInt16(); + Tops = input.ReadExpectedBytes(8); + Bots = input.ReadExpectedBytes(8); + Counters = input.ReadUnsignedShorts(8); + Flags = input.ReadExpectedBytes(8); + MusicMode = input.ReadBooleans(3); + LastSystemClock = input.ReadUInt64(); + FractionalClocks = input.ReadDouble(); + _ShiftRegister = input.ReadByte(); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(ROM); + output.Write(BankBaseAddr); + output.Write(Tops); + output.Write(Bots); + output.Write(Counters); + output.Write(Flags); + output.Write(MusicMode); + output.Write(LastSystemClock); + output.Write(FractionalClocks); + output.Write(_ShiftRegister); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/CartMN16K.cs b/EMU7800/Core/CartMN16K.cs new file mode 100644 index 0000000000..fbcaf99bca --- /dev/null +++ b/EMU7800/Core/CartMN16K.cs @@ -0,0 +1,136 @@ +namespace EMU7800.Core +{ + /// + /// M-Network 16KB bankswitched carts with 2KB RAM. + /// + public sealed class CartMN16K : Cart + { + // + // Cart Format Mapping to ROM Address Space + // Segment1: 0x0000:0x0800 Bank1:0x1000:0x0800 Select Seg: 1fe0-1fe6, 1fe7=RAM Seg1 + // Segment2: 0x0800:0x0800 Bank2:0x1800:0x0800 Always Seg8 + // Segment3: 0x1000:0x0800 + // Segment4: 0x1800:0x0800 + // Segment5: 0x2000:0x0800 + // Segment6: 0x2800:0x0800 + // Segment7: 0x3000:0x0800 + // Segment8: 0x3800:0x0800 + // + // RAM RAM Segment1 when 1fe7 select is accessed + // Segment1: 0x0000:0x0400 0x1000-0x13FF write port + // Segment2: 0x0400:0x0400 0x1400-0x17FF read port + // + // RAM Segment2: 1ff8-1ffb selects 256-byte block + // 0x1800-0x18ff write port + // 0x1900-0x19ff read port + // + ushort BankBaseAddr, BankBaseRAMAddr; + bool RAMBankOn; + readonly byte[] RAM; + + int Bank + { + set + { + BankBaseAddr = (ushort)(value << 11); // multiply by 2048 + RAMBankOn = (value == 0x07); + } + } + + int BankRAM + { + set { BankBaseRAMAddr = (ushort) (value << 8); } // multiply by 256 + } + + #region IDevice Members + + public override void Reset() + { + Bank = 0; + BankRAM = 0; + } + + public override byte this[ushort addr] + { + get + { + addr &= 0x0fff; + UpdateBanks(addr); + if (RAMBankOn && addr >= 0x0400 && addr < 0x0800) + { + return RAM[addr & 0x03ff]; + } + if (addr >= 0x0900 && addr < 0x0a00) + { + return RAM[0x400 + BankBaseRAMAddr + (addr & 0xff)]; + } + return addr < 0x0800 ? ROM[BankBaseAddr + (addr & 0x07ff)] : ROM[0x3800 + (addr & 0x07ff)]; + } + set + { + addr &= 0x0fff; + UpdateBanks(addr); + if (RAMBankOn && addr < 0x0400) + { + RAM[addr & 0x03ff] = value; + } + else if (addr >= 0x0800 && addr < 0x0900) + { + RAM[0x400 + BankBaseRAMAddr + (addr & 0xff)] = value; + } + } + } + + #endregion + + private CartMN16K() + { + } + + public CartMN16K(byte[] romBytes) + { + LoadRom(romBytes, 0x4000); + RAM = new byte[0x800]; + Bank = 0; + BankRAM = 0; + } + + void UpdateBanks(ushort addr) + { + if (addr >= 0x0fe0 && addr < 0x0fe8) + { + Bank = addr & 0x07; + } + else if (addr >= 0x0fe8 && addr < 0x0fec) + { + BankRAM = addr & 0x03; + } + } + + #region Serialization Members + + public CartMN16K(DeserializationContext input, MachineBase m) : base(input) + { + input.CheckVersion(1); + LoadRom(input.ReadExpectedBytes(0x4000), 0x4000); + RAM = input.ReadExpectedBytes(0x800); + BankBaseAddr = input.ReadUInt16(); + BankBaseRAMAddr = input.ReadUInt16(); + RAMBankOn = input.ReadBoolean(); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(ROM); + output.Write(RAM); + output.Write(BankBaseAddr); + output.Write(BankBaseRAMAddr); + output.Write(RAMBankOn); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/CartPB8K.cs b/EMU7800/Core/CartPB8K.cs new file mode 100644 index 0000000000..150bca0932 --- /dev/null +++ b/EMU7800/Core/CartPB8K.cs @@ -0,0 +1,103 @@ +namespace EMU7800.Core +{ + /// + /// Parker Brothers 8KB bankswitched carts. + /// + public sealed class CartPB8K : Cart + { + // + // Cart Format Mapping to ROM Address Space + // Segment1: 0x0000:0x0400 Bank1:0x1000:0x0400 Select Segment: 1fe0-1fe7 + // Segment2: 0x0400:0x0400 Bank2:0x1400:0x0400 Select Segment: 1fe8-1ff0 + // Segment3: 0x0800:0x0400 Bank3:0x1800:0x0400 Select Segment: 1ff0-1ff8 + // Segment4: 0x0c00:0x0400 Bank4:0x1c00:0x0400 Always Segment8 + // Segment5: 0x1000:0x0400 + // Segment6: 0x1400:0x0400 + // Segment7: 0x1800:0x0400 + // Segment8: 0x1c00:0x0400 + // + readonly ushort[] SegmentBase; + + #region IDevice Members + + public override void Reset() + { + SegmentBase[0] = ComputeSegmentBase(4); + SegmentBase[1] = ComputeSegmentBase(5); + SegmentBase[2] = ComputeSegmentBase(6); + } + + public override byte this[ushort addr] + { + get + { + addr &= 0x0fff; + UpdateSegmentBases(addr); + return ROM[SegmentBase[addr >> 10] + (addr & 0x03ff)]; + } + set + { + addr &= 0x0fff; + UpdateSegmentBases(addr); + } + } + + #endregion + + private CartPB8K() + { + } + + public CartPB8K(byte[] romBytes) + { + LoadRom(romBytes, 0x2000); + SegmentBase = new ushort[4]; + SegmentBase[0] = ComputeSegmentBase(4); + SegmentBase[1] = ComputeSegmentBase(5); + SegmentBase[2] = ComputeSegmentBase(6); + SegmentBase[3] = ComputeSegmentBase(7); + } + + static ushort ComputeSegmentBase(int slice) + { + return (ushort)(slice << 10); // multiply by 1024 + } + + void UpdateSegmentBases(ushort addr) + { + if (addr < 0xfe0 || addr >= 0x0ff8) { } + else if (addr >= 0x0fe0 && addr < 0x0fe8) + { + SegmentBase[0] = ComputeSegmentBase(addr & 0x07); + } + else if (addr >= 0x0fe8 && addr < 0x0ff0) + { + SegmentBase[1] = ComputeSegmentBase(addr & 0x07); + } + else if (addr >= 0x0ff0 && addr < 0x0ff8) + { + SegmentBase[2] = ComputeSegmentBase(addr & 0x07); + } + } + + #region Serialization Members + + public CartPB8K(DeserializationContext input, MachineBase m) : base(input) + { + input.CheckVersion(1); + LoadRom(input.ReadExpectedBytes(0x2000), 0x2000); + SegmentBase = input.ReadUnsignedShorts(); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(ROM); + output.Write(SegmentBase); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/CartTV8K.cs b/EMU7800/Core/CartTV8K.cs new file mode 100644 index 0000000000..cfeaa9d038 --- /dev/null +++ b/EMU7800/Core/CartTV8K.cs @@ -0,0 +1,90 @@ +namespace EMU7800.Core +{ + /// + /// Tigervision 8KB bankswitched carts + /// + public sealed class CartTV8K : Cart + { + // + // Cart Format Mapping to ROM Address Space + // Segment1: 0x0000:0x0800 0x1000:0x0800 Selected segment via $003F + // Segment2: 0x0800:0x0800 0x1800:0x0800 Always last segment + // Segment3: 0x1000:0x0800 + // Segment4: 0x1800:0x0800 + // + ushort BankBaseAddr; + readonly ushort LastBankBaseAddr; + + byte Bank + { + set + { + BankBaseAddr = (ushort)(0x0800 * value); + BankBaseAddr %= (ushort)ROM.Length; + } + } + + protected internal override bool RequestSnooping + { + get { return true; } + } + + #region IDevice Members + + public override void Reset() + { + Bank = 0; + } + + public override byte this[ushort addr] + { + get + { + addr &= 0x0fff; + return addr < 0x0800 ? ROM[BankBaseAddr + (addr & 0x07ff)] : ROM[LastBankBaseAddr + (addr & 0x07ff)]; + } + set + { + if (addr <= 0x003f) + { + Bank = value; + } + } + } + + #endregion + + private CartTV8K() + { + } + + public CartTV8K(byte[] romBytes) + { + LoadRom(romBytes, 0x1000); + Bank = 0; + LastBankBaseAddr = (ushort)(ROM.Length - 0x0800); + } + + #region Serialization Members + + public CartTV8K(DeserializationContext input, MachineBase m) : base(input) + { + input.CheckVersion(1); + LoadRom(input.ReadExpectedBytes(0x1000), 0x1000); + BankBaseAddr = input.ReadUInt16(); + LastBankBaseAddr = input.ReadUInt16(); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(ROM); + output.Write(BankBaseAddr); + output.Write(LastBankBaseAddr); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/CartType.cs b/EMU7800/Core/CartType.cs new file mode 100644 index 0000000000..a334cb5034 --- /dev/null +++ b/EMU7800/Core/CartType.cs @@ -0,0 +1,43 @@ +/* + * CartType.cs + * + * Defines the set of all known Game Cartridges. + * + * 2010 Mike Murphy + * + */ +namespace EMU7800.Core +{ + public enum CartType + { + None, + A2K, // Atari 2kb cart + TV8K, // Tigervision 8kb bankswitched cart + A4K, // Atari 4kb cart + PB8K, // Parker Brothers 8kb bankswitched cart + MN16K, // M-Network 16kb bankswitched cart + A16K, // Atari 16kb bankswitched cart + A16KR, // Atari 16kb bankswitched cart w/128 bytes RAM + A8K, // Atari 8KB bankswitched cart + A8KR, // Atari 8KB bankswitched cart w/128 bytes RAM + A32K, // Atari 32KB bankswitched cart + A32KR, // Atari 32KB bankswitched cart w/128 bytes RAM + CBS12K, // CBS' RAM Plus bankswitched cart w/256 bytes RAM + DC8K, // Special Activision cart (Robot Tank and Decathlon) + DPC, // Pitfall II DPC cart + M32N12K, // 32N1 Multicart: 32x2KB + A7808, // Atari7800 non-bankswitched 8KB cart + A7816, // Atari7800 non-bankswitched 16KB cart + A7832, // Atari7800 non-bankswitched 32KB cart + A7832P, // Atari7800 non-bankswitched 32KB cart w/Pokey + A7848, // Atari7800 non-bankswitched 48KB cart + A78SG, // Atari7800 SuperGame cart + A78SGP, // Atari7800 SuperGame cart w/Pokey + A78SGR, // Atari7800 SuperGame cart w/RAM + A78S9, // Atari7800 SuperGame cart, nine banks + A78S4, // Atari7800 SuperGame cart, four banks + A78S4R, // Atari7800 SuperGame cart, four banks, w/RAM + A78AB, // F18 Hornet cart (Absolute) + A78AC, // Double dragon cart (Activision) + }; +} diff --git a/EMU7800/Core/ConsoleSwitch.cs b/EMU7800/Core/ConsoleSwitch.cs new file mode 100644 index 0000000000..7a07ba7eb5 --- /dev/null +++ b/EMU7800/Core/ConsoleSwitch.cs @@ -0,0 +1,11 @@ +namespace EMU7800.Core +{ + internal enum ConsoleSwitch + { + GameReset, + GameSelect, + GameBW, + LeftDifficultyA, + RightDifficultyA, + } +} \ No newline at end of file diff --git a/EMU7800/Core/Controller.cs b/EMU7800/Core/Controller.cs new file mode 100644 index 0000000000..bc21cbcbd6 --- /dev/null +++ b/EMU7800/Core/Controller.cs @@ -0,0 +1,14 @@ +namespace EMU7800.Core +{ + public enum Controller + { + None, + Joystick, + Paddles, + Keypad, + Driving, + BoosterGrip, + ProLineJoystick, + Lightgun, + } +} \ No newline at end of file diff --git a/EMU7800/Core/ControllerAction.cs b/EMU7800/Core/ControllerAction.cs new file mode 100644 index 0000000000..bad0cc82ee --- /dev/null +++ b/EMU7800/Core/ControllerAction.cs @@ -0,0 +1,17 @@ +namespace EMU7800.Core +{ + public enum ControllerAction + { + Up, + Down, + Left, + Right, + Trigger, // Interpretation: 7800 RFire; 2600 Fire, BoosterGrip top + Trigger2, // Interpretation: 7800 LFire, BoosterGrip trigger + Keypad1, Keypad2, Keypad3, + Keypad4, Keypad5, Keypad6, + Keypad7, Keypad8, Keypad9, + KeypadA, Keypad0, KeypadP, + Driving0, Driving1, Driving2, Driving3, + } +} \ No newline at end of file diff --git a/EMU7800/Core/DeserializationContext.cs b/EMU7800/Core/DeserializationContext.cs new file mode 100644 index 0000000000..d3f8acb806 --- /dev/null +++ b/EMU7800/Core/DeserializationContext.cs @@ -0,0 +1,245 @@ +using System; +using System.IO; +using System.Linq; + +namespace EMU7800.Core +{ + /// + /// A context for deserializing objects. + /// + public class DeserializationContext + { + #region Fields + + readonly BinaryReader _binaryReader; + + #endregion + + public bool ReadBoolean() + { + return _binaryReader.ReadBoolean(); + } + + public byte ReadByte() + { + return _binaryReader.ReadByte(); + } + + public ushort ReadUInt16() + { + return _binaryReader.ReadUInt16(); + } + + public int ReadInt32() + { + return _binaryReader.ReadInt32(); + } + + public uint ReadUInt32() + { + return _binaryReader.ReadUInt32(); + } + + public long ReadInt64() + { + return _binaryReader.ReadInt64(); + } + + public ulong ReadUInt64() + { + return _binaryReader.ReadUInt64(); + } + + public double ReadDouble() + { + return _binaryReader.ReadDouble(); + } + + public BufferElement ReadBufferElement() + { + var be = new BufferElement(); + for (var i = 0; i < BufferElement.SIZE; i++) + be[i] = ReadByte(); + return be; + } + + public byte[] ReadBytes() + { + var count = _binaryReader.ReadInt32(); + if (count <= 0) + return new byte[0]; + if (count > 0x40000) + throw new Emu7800SerializationException("Byte array length too large."); + return _binaryReader.ReadBytes(count); + } + + public byte[] ReadExpectedBytes(params int[] expectedSizes) + { + var count = _binaryReader.ReadInt32(); + if (!expectedSizes.Any(t => t == count)) + throw new Emu7800SerializationException("Byte array length incorrect."); + return _binaryReader.ReadBytes(count); + } + + public byte[] ReadOptionalBytes(params int[] expectedSizes) + { + var hasBytes = _binaryReader.ReadBoolean(); + return (hasBytes) ? ReadExpectedBytes(expectedSizes) : null; + } + + public ushort[] ReadUnsignedShorts(params int[] expectedSizes) + { + var bytes = ReadExpectedBytes(expectedSizes.Select(t => t << 1).ToArray()); + var ushorts = new ushort[bytes.Length >> 1]; + Buffer.BlockCopy(bytes, 0, ushorts, 0, bytes.Length); + return ushorts; + } + + public int[] ReadIntegers(params int[] expectedSizes) + { + var bytes = ReadExpectedBytes(expectedSizes.Select(t => t << 2).ToArray()); + var integers = new int[bytes.Length >> 2]; + Buffer.BlockCopy(bytes, 0, integers, 0, bytes.Length); + return integers; + } + + public uint[] ReadUnsignedIntegers(params int[] expectedSizes) + { + var bytes = ReadExpectedBytes(expectedSizes.Select(t => t << 2).ToArray()); + var uints = new uint[bytes.Length >> 2]; + Buffer.BlockCopy(bytes, 0, uints, 0, bytes.Length); + return uints; + } + + public bool[] ReadBooleans(params int[] expectedSizes) + { + var bytes = ReadExpectedBytes(expectedSizes); + var booleans = new bool[bytes.Length]; + for (var i = 0; i < bytes.Length; i++) + booleans[i] = (bytes[i] != 0); + return booleans; + } + + public int CheckVersion(params int[] validVersions) + { + var magicNumber = _binaryReader.ReadInt32(); + if (magicNumber != 0x78000087) + throw new Emu7800SerializationException("Magic number not found."); + var version = _binaryReader.ReadInt32(); + if (!validVersions.Any(t => t == version)) + throw new Emu7800SerializationException("Invalid version number found."); + return version; + } + + public MachineBase ReadMachine() + { + var typeName = _binaryReader.ReadString(); + if (string.IsNullOrWhiteSpace(typeName)) + throw new Emu7800SerializationException("Invalid type name."); + + var type = Type.GetType(typeName); + if (type == null) + throw new Emu7800SerializationException("Unable to resolve type name: " + typeName); + + return (MachineBase)Activator.CreateInstance(type, new object[] { this }); + } + + public AddressSpace ReadAddressSpace(MachineBase m, int addrSpaceShift, int pageShift) + { + var addressSpace = new AddressSpace(this, m, addrSpaceShift, pageShift); + return addressSpace; + } + + public M6502 ReadM6502(MachineBase m, int runClocksMultiple) + { + var cpu = new M6502(this, m, runClocksMultiple); + return cpu; + } + + public Maria ReadMaria(Machine7800 m, int scanlines) + { + var maria = new Maria(this, m, scanlines); + return maria; + } + + public PIA ReadPIA(MachineBase m) + { + var pia = new PIA(this, m); + return pia; + } + + public TIA ReadTIA(MachineBase m) + { + var tia = new TIA(this, m); + return tia; + } + + public TIASound ReadTIASound(MachineBase m, int cpuClocksPerSample) + { + var tiaSound = new TIASound(this, m, cpuClocksPerSample); + return tiaSound; + } + + public RAM6116 ReadRAM6116() + { + var ram6116 = new RAM6116(this); + return ram6116; + } + + public InputState ReadInputState() + { + var inputState = new InputState(this); + return inputState; + } + + public HSC7800 ReadOptionalHSC7800() + { + var exist = ReadBoolean(); + return exist ? new HSC7800(this) : null; + } + + public Bios7800 ReadOptionalBios7800() + { + var exist = ReadBoolean(); + return exist ? new Bios7800(this) : null; + } + + public PokeySound ReadOptionalPokeySound(MachineBase m) + { + var exist = ReadBoolean(); + return exist ? new PokeySound(this, m) : null; + } + + public Cart ReadCart(MachineBase m) + { + var typeName = _binaryReader.ReadString(); + if (string.IsNullOrWhiteSpace(typeName)) + throw new Emu7800SerializationException("Invalid type name."); + + var type = Type.GetType(typeName); + if (type == null) + throw new Emu7800SerializationException("Unable to resolve type name: " + typeName); + + return (Cart)Activator.CreateInstance(type, new object[] { this, m }); + } + + #region Constructors + + private DeserializationContext() + { + } + + /// + /// Instantiates a new instance of . + /// + /// + internal DeserializationContext(BinaryReader binaryReader) + { + if (binaryReader == null) + throw new ArgumentNullException("binaryReader"); + _binaryReader = binaryReader; + } + + #endregion + } +} diff --git a/EMU7800/Core/Emu7800Exception.cs b/EMU7800/Core/Emu7800Exception.cs new file mode 100644 index 0000000000..18de9a42a4 --- /dev/null +++ b/EMU7800/Core/Emu7800Exception.cs @@ -0,0 +1,19 @@ +using System; + +namespace EMU7800.Core +{ + public class Emu7800Exception : Exception + { + internal Emu7800Exception() + { + } + + internal Emu7800Exception(string message) : base(message) + { + } + + internal Emu7800Exception(string message, Exception ex) : base(message, ex) + { + } + } +} diff --git a/EMU7800/Core/Emu7800SerializationException.cs b/EMU7800/Core/Emu7800SerializationException.cs new file mode 100644 index 0000000000..4f0827d13c --- /dev/null +++ b/EMU7800/Core/Emu7800SerializationException.cs @@ -0,0 +1,19 @@ +using System; + +namespace EMU7800.Core +{ + public class Emu7800SerializationException : Emu7800Exception + { + private Emu7800SerializationException() + { + } + + internal Emu7800SerializationException(string message) : base(message) + { + } + + internal Emu7800SerializationException(string message, Exception ex) : base(message, ex) + { + } + } +} diff --git a/EMU7800/Core/FontRenderer.cs b/EMU7800/Core/FontRenderer.cs new file mode 100644 index 0000000000..f0737b98ff --- /dev/null +++ b/EMU7800/Core/FontRenderer.cs @@ -0,0 +1,170 @@ +/* + * FontRenderer + * + * A simple font renderer for displaying text during emulation. Font data and + * rendering algorithm courtesy of Bradford W. Mott's Stella source. + * + * Copyright © 2004 Mike Murphy + * + */ + +using System; + +namespace EMU7800.Core +{ + /// + /// A simple font renderer for displaying text during emulation. + /// + public class FontRenderer + { + static readonly uint[] AlphaFontData = + { + 0x699f999, // A + 0xe99e99e, // B + 0x6988896, // C + 0xe99999e, // D + 0xf88e88f, // E + 0xf88e888, // F + 0x698b996, // G + 0x999f999, // H + 0x7222227, // I + 0x72222a4, // J + 0x9accaa9, // K + 0x888888f, // L + 0x9ff9999, // M + 0x9ddbb99, // N + 0x6999996, // O + 0xe99e888, // P + 0x69999b7, // Q + 0xe99ea99, // R + 0x6986196, // S + 0x7222222, // T + 0x9999996, // U + 0x9999966, // V + 0x9999ff9, // W + 0x99fff99, // X + 0x9996244, // Y + 0xf12488f // Z + }; + + static readonly uint[] DigitFontData = + { + 0x69bd996, // 0 + 0x2622227, // 1 + 0x691248f, // 2 + 0x6916196, // 3 + 0xaaaf222, // 4 + 0xf88e11e, // 5 + 0x698e996, // 6 + 0xf112244, // 7 + 0x6996996, // 8 + 0x6997196 // 9 + }; + + /// + /// Draw specified text at specified position using the specified foreground and background colors. + /// + /// + /// + /// + /// + /// + /// + /// text must be non-null. + public void DrawText(FrameBuffer frameBuffer, string text, int xoffset, int yoffset, byte fore, byte back) + { + if (text == null) + throw new ArgumentNullException("text"); + + var textchars = text.ToUpper().ToCharArray(); + + for (var i = 0; i < text.Length + 1; i++) + { + for (var j = 0; j < 9; j++) + { + var pos = (j + yoffset) * frameBuffer.VisiblePitch + i * 5; + for (var k = 0; k < 5; k++) + { + while (pos >= frameBuffer.VideoBufferByteLength) + { + pos -= frameBuffer.VideoBufferByteLength; + } + while (pos < 0) + { + pos += frameBuffer.VideoBufferByteLength; + } + frameBuffer.VideoBuffer[pos >> BufferElement.SHIFT][pos++] = back; + } + } + } + + for (var i = 0; i < text.Length; i++) + { + var c = textchars[i]; + uint fdata; + + switch (c) + { + case '/': + case '\\': + fdata = 0x0122448; + break; + case '(': + fdata = 0x2488842; + break; + case ')': + fdata = 0x4211124; + break; + case '.': + fdata = 0x0000066; + break; + case ':': + fdata = 0x0660660; + break; + case '-': + fdata = 0x0007000; + break; + default: + if (c >= 'A' && c <= 'Z') + { + fdata = AlphaFontData[c - 'A']; + } + else if (c >= '0' && c <= '9') + { + fdata = DigitFontData[c - '0']; + } + else + { + fdata = 0; + } + break; + } + + var ypos = 8; + for (var j = 0; j < 32; j++) + { + var xpos = j & 3; + if (xpos == 0) + { + ypos--; + } + + var pos = (ypos + yoffset) * frameBuffer.VisiblePitch + (4 - xpos) + xoffset; + while (pos >= frameBuffer.VideoBufferByteLength) + { + pos -= frameBuffer.VideoBufferByteLength; + } + while (pos < 0) + { + pos += frameBuffer.VideoBufferByteLength; + } + if (((fdata >> j) & 1) != 0) + { + frameBuffer.VideoBuffer[pos >> BufferElement.SHIFT][pos] = fore; + } + } + xoffset += 5; + } + } + } +} diff --git a/EMU7800/Core/FrameBuffer.cs b/EMU7800/Core/FrameBuffer.cs new file mode 100644 index 0000000000..6074518ab9 --- /dev/null +++ b/EMU7800/Core/FrameBuffer.cs @@ -0,0 +1,79 @@ +using System; + +namespace EMU7800.Core +{ + public class FrameBuffer + { + /// + /// Number of visible pixels on a single horizontal line. + /// + public int VisiblePitch { get; private set; } + + /// + /// Number of s that represent VisiblePitch. + /// + public int VideoBufferElementVisiblePitch { get; private set; } + + /// + /// Number of visible scan lines. + /// + public int Scanlines { get; private set; } + + /// + /// The number of bytes contained by VideoBuffer. + /// + public int VideoBufferByteLength { get; private set; } + + /// + /// The number of s contained by VideoBuffer + /// + public int VideoBufferElementLength { get; private set; } + + /// + /// The number of bytes contained by SoundBuffer. + /// + public int SoundBufferByteLength { get; private set; } + + /// + /// The number of s contained by SoundBuffer + /// + public int SoundBufferElementLength { get; private set; } + + /// + /// The buffer containing computed pixel data. + /// + public BufferElement[] VideoBuffer { get; private set; } + + /// + /// The buffer containing computed PCM audio data. + /// + public BufferElement[] SoundBuffer { get; private set; } + + #region Constructors + + private FrameBuffer() + { + } + + internal FrameBuffer(int visiblePitch, int scanLines) + { + if (visiblePitch < 0) + throw new ArgumentException("visiblePitch must be non-negative."); + if (scanLines < 0) + throw new ArgumentException("scanLines must be non-negative."); + + VisiblePitch = visiblePitch; + VideoBufferElementVisiblePitch = VisiblePitch >> BufferElement.SHIFT; + Scanlines = scanLines; + VideoBufferByteLength = VisiblePitch * Scanlines; + VideoBufferElementLength = VideoBufferElementVisiblePitch * Scanlines; + SoundBufferByteLength = Scanlines << 1; + SoundBufferElementLength = SoundBufferByteLength >> BufferElement.SHIFT; + + VideoBuffer = new BufferElement[VideoBufferElementLength + (64 >> BufferElement.SHIFT)]; + SoundBuffer = new BufferElement[SoundBufferElementLength + (64 >> BufferElement.SHIFT)]; + } + + #endregion + } +} diff --git a/EMU7800/Core/HSC7800.cs b/EMU7800/Core/HSC7800.cs new file mode 100644 index 0000000000..4186e66097 --- /dev/null +++ b/EMU7800/Core/HSC7800.cs @@ -0,0 +1,85 @@ +/* + * HSC7800.cs + * + * The 7800 High Score cartridge--courtesy of Matthias . + * + */ +using System; + +namespace EMU7800.Core +{ + public sealed class HSC7800 : IDevice + { + readonly byte[] ROM; + readonly ushort Mask; + + public static ushort Size { get; private set; } + + #region IDevice Members + + public void Reset() + { + } + + public byte this[ushort addr] + { + get { return ROM[addr & Mask]; } + set { } + } + + #endregion + + public RAM6116 SRAM { get; private set; } + + #region Constructors + + private HSC7800() + { + } + + public HSC7800(byte[] hscRom, byte[] ram) + { + if (hscRom == null) + throw new ArgumentNullException("hscRom"); + if (ram == null) + throw new ArgumentNullException("ram"); + if (hscRom.Length != 4096) + throw new ArgumentException("ROM size not 4096", "hscRom"); + + ROM = hscRom; + SRAM = new RAM6116(ram); + + Size = Mask = (ushort)ROM.Length; + Mask--; + } + + #endregion + + #region Serialization Members + + public HSC7800(DeserializationContext input) + { + if (input == null) + throw new ArgumentNullException("input"); + + input.CheckVersion(1); + ROM = input.ReadExpectedBytes(4096); + SRAM = input.ReadRAM6116(); + + Size = Mask = (ushort)ROM.Length; + Mask--; + } + + public void GetObjectData(SerializationContext output) + { + if (output == null) + throw new ArgumentNullException("output"); + + output.WriteVersion(1); + output.Write(ROM); + output.Write(SRAM); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/IDevice.cs b/EMU7800/Core/IDevice.cs new file mode 100644 index 0000000000..38a8b83980 --- /dev/null +++ b/EMU7800/Core/IDevice.cs @@ -0,0 +1,16 @@ +/* + * IDevice.cs + * + * Defines interface for devices accessable via the AddressSpace class. + * + * Copyright © 2003, 2011 Mike Murphy + * + */ +namespace EMU7800.Core +{ + public interface IDevice + { + void Reset(); + byte this[ushort addr] { get; set; } + } +} diff --git a/EMU7800/Core/ILogger.cs b/EMU7800/Core/ILogger.cs new file mode 100644 index 0000000000..919c3b0b25 --- /dev/null +++ b/EMU7800/Core/ILogger.cs @@ -0,0 +1,10 @@ +namespace EMU7800.Core +{ + public interface ILogger + { + void WriteLine(string format, params object[] args); + void WriteLine(object value); + void Write(string format, params object[] args); + void Write(object value); + } +} diff --git a/EMU7800/Core/InputState.cs b/EMU7800/Core/InputState.cs new file mode 100644 index 0000000000..f78c0b9c1a --- /dev/null +++ b/EMU7800/Core/InputState.cs @@ -0,0 +1,368 @@ +/* + * InputState.cs + * + * Class containing the input state of the console and its controllers, + * mapping emulator input devices to external input. + * + * Copyright © 2003-2010 Mike Murphy + * + */ +using System; + +namespace EMU7800.Core +{ + public class InputState + { + #region Fields + + const int + PaddleOhmMin = 100000, + PaddleOhmMax = 800000; + + const int + LeftControllerJackIndex = 0, + RightControllerJackIndex = 1, + ConsoleSwitchIndex = 2, + ControllerActionStateIndex = 3, + OhmsIndex = ControllerActionStateIndex + 4, + LightgunPositionIndex = ControllerActionStateIndex + 4, + InputStateSize = ControllerActionStateIndex + 8 + 1; + + // For driving controllers + readonly byte[] _rotGrayCodes = new byte[] { 0x0f, 0x0d, 0x0c, 0x0e }; + readonly int[] _rotState = new int[2]; + + readonly int[] _nextInputState = new int[InputStateSize]; + readonly int[] _inputState = new int[InputStateSize]; + + #endregion + + #region Public Members + + /// + /// Enables the incoming input state buffer to be populated prior to the start of the frame. + /// Useful for input playback senarios. + /// + /// Return value is ignored. + public Func InputAdvancing { get; set; } + + /// + /// Enables access to the input state buffer. + /// Useful for input recording senarios. + /// + /// Return value is ignored. + public Func InputAdvanced { get; set; } + + public void CaptureInputState() + { + if (InputAdvancing != null) + InputAdvancing(_nextInputState); + Buffer.BlockCopy(_nextInputState, 0, _inputState, 0, InputStateSize * sizeof(int)); + if (InputAdvanced != null) + InputAdvanced(_inputState); + } + + public Controller LeftControllerJack + { + get { return (Controller)_nextInputState[LeftControllerJackIndex]; } + set { _nextInputState[LeftControllerJackIndex] = (int)value; } + } + + public Controller RightControllerJack + { + get { return (Controller)_nextInputState[RightControllerJackIndex]; } + set { _nextInputState[RightControllerJackIndex] = (int)value; } + } + + public bool IsGameBWConsoleSwitchSet + { + get { return (_nextInputState[ConsoleSwitchIndex] & (1 << (int) ConsoleSwitch.GameBW)) != 0; } + } + + public bool IsLeftDifficultyAConsoleSwitchSet + { + get { return (_nextInputState[ConsoleSwitchIndex] & (1 << (int)ConsoleSwitch.LeftDifficultyA)) != 0; } + } + + public bool IsRightDifficultyAConsoleSwitchSet + { + get { return (_nextInputState[ConsoleSwitchIndex] & (1 << (int)ConsoleSwitch.RightDifficultyA)) != 0; } + } + + public void RaiseInput(int playerNo, MachineInput input, bool down) + { + switch (input) + { + case MachineInput.Fire: + SetControllerActionState(playerNo, ControllerAction.Trigger, down); + break; + case MachineInput.Fire2: + SetControllerActionState(playerNo, ControllerAction.Trigger2, down); + break; + case MachineInput.Left: + SetControllerActionState(playerNo, ControllerAction.Left, down); + if (down) SetControllerActionState(playerNo, ControllerAction.Right, false); + break; + case MachineInput.Up: + SetControllerActionState(playerNo, ControllerAction.Up, down); + if (down) SetControllerActionState(playerNo, ControllerAction.Down, false); + break; + case MachineInput.Right: + SetControllerActionState(playerNo, ControllerAction.Right, down); + if (down) SetControllerActionState(playerNo, ControllerAction.Left, false); + break; + case MachineInput.Down: + SetControllerActionState(playerNo, ControllerAction.Down, down); + if (down) SetControllerActionState(playerNo, ControllerAction.Up, false); + break; + case MachineInput.NumPad7: + SetControllerActionState(playerNo, ControllerAction.Keypad7, down); + break; + case MachineInput.NumPad8: + SetControllerActionState(playerNo, ControllerAction.Keypad8, down); + break; + case MachineInput.NumPad9: + SetControllerActionState(playerNo, ControllerAction.Keypad9, down); + break; + case MachineInput.NumPad4: + SetControllerActionState(playerNo, ControllerAction.Keypad4, down); + break; + case MachineInput.NumPad5: + SetControllerActionState(playerNo, ControllerAction.Keypad5, down); + break; + case MachineInput.NumPad6: + SetControllerActionState(playerNo, ControllerAction.Keypad6, down); + break; + case MachineInput.NumPad1: + SetControllerActionState(playerNo, ControllerAction.Keypad1, down); + break; + case MachineInput.NumPad2: + SetControllerActionState(playerNo, ControllerAction.Keypad2, down); + break; + case MachineInput.NumPad3: + SetControllerActionState(playerNo, ControllerAction.Keypad3, down); + break; + case MachineInput.NumPadMult: + SetControllerActionState(playerNo, ControllerAction.KeypadA, down); + break; + case MachineInput.NumPad0: + SetControllerActionState(playerNo, ControllerAction.Keypad0, down); + break; + case MachineInput.NumPadHash: + SetControllerActionState(playerNo, ControllerAction.KeypadP, down); + break; + case MachineInput.Driving0: + SetControllerActionState(playerNo, ControllerAction.Driving0, true); + SetControllerActionState(playerNo, ControllerAction.Driving1, false); + SetControllerActionState(playerNo, ControllerAction.Driving2, false); + SetControllerActionState(playerNo, ControllerAction.Driving3, false); + break; + case MachineInput.Driving1: + SetControllerActionState(playerNo, ControllerAction.Driving0, false); + SetControllerActionState(playerNo, ControllerAction.Driving1, true); + SetControllerActionState(playerNo, ControllerAction.Driving2, false); + SetControllerActionState(playerNo, ControllerAction.Driving3, false); + break; + case MachineInput.Driving2: + SetControllerActionState(playerNo, ControllerAction.Driving0, false); + SetControllerActionState(playerNo, ControllerAction.Driving1, false); + SetControllerActionState(playerNo, ControllerAction.Driving2, true); + SetControllerActionState(playerNo, ControllerAction.Driving3, false); + break; + case MachineInput.Driving3: + SetControllerActionState(playerNo, ControllerAction.Driving0, false); + SetControllerActionState(playerNo, ControllerAction.Driving1, false); + SetControllerActionState(playerNo, ControllerAction.Driving2, false); + SetControllerActionState(playerNo, ControllerAction.Driving3, true); + break; + case MachineInput.Reset: + SetConsoleSwitchState(ConsoleSwitch.GameReset, down); + break; + case MachineInput.Select: + SetConsoleSwitchState(ConsoleSwitch.GameSelect, down); + break; + case MachineInput.Color: + if (down) ToggleConsoleSwitchState(ConsoleSwitch.GameBW); + break; + case MachineInput.LeftDifficulty: + if (down) ToggleConsoleSwitchState(ConsoleSwitch.LeftDifficultyA); + break; + case MachineInput.RightDifficulty: + if (down) ToggleConsoleSwitchState(ConsoleSwitch.RightDifficultyA); + break; + } + } + + public void RaisePaddleInput(int playerNo, int valMax, int val) + { + var ohms = PaddleOhmMax - (PaddleOhmMax - PaddleOhmMin) / valMax * val; + _nextInputState[OhmsIndex + (playerNo & 3)] = ohms; + } + + public void RaiseLightgunPos(int playerNo, int scanline, int hpos) + { + var i = LightgunPositionIndex + ((playerNo & 1) << 1); + _nextInputState[i++] = scanline; + _nextInputState[i] = hpos; + } + + public void ClearAllInput() + { + _nextInputState[ConsoleSwitchIndex] = 0; + ClearLeftJackInput(); + ClearRightJackInput(); + } + + public void ClearInputByPlayer(int playerNo) + { + _nextInputState[OhmsIndex + (playerNo & 3)] = 0; + _nextInputState[ControllerActionStateIndex + (playerNo & 3)] = 0; + _nextInputState[LightgunPositionIndex + ((playerNo & 1) << 1)] = _nextInputState[LightgunPositionIndex + ((playerNo & 1) << 1) + 1] = 0; + } + + public void ClearLeftJackInput() + { + _nextInputState[OhmsIndex] = _nextInputState[OhmsIndex + 1] = 0; + _nextInputState[ControllerActionStateIndex] = 0; + switch (LeftControllerJack) + { + case Controller.Paddles: + _nextInputState[ControllerActionStateIndex] = _nextInputState[ControllerActionStateIndex + 1] = 0; + break; + default: + _nextInputState[ControllerActionStateIndex] = 0; + break; + } + _nextInputState[LightgunPositionIndex] = _nextInputState[LightgunPositionIndex + 1] = 0; + } + + public void ClearRightJackInput() + { + _nextInputState[OhmsIndex + 2] = _nextInputState[OhmsIndex + 3] = 0; + switch (RightControllerJack) + { + case Controller.Paddles: + _nextInputState[ControllerActionStateIndex + 2] = _nextInputState[ControllerActionStateIndex + 3] = 0; + break; + default: + _nextInputState[ControllerActionStateIndex + 1] = 0; + break; + } + _nextInputState[LightgunPositionIndex + 2] = _nextInputState[LightgunPositionIndex + 3] = 0; + } + + #endregion + + #region Serialization Members + + public InputState() + { + } + + public InputState(DeserializationContext input) + { + if (input == null) + throw new ArgumentNullException("input"); + + input.CheckVersion(1); + _rotState = input.ReadIntegers(2); + _nextInputState = input.ReadIntegers(InputStateSize); + _inputState = input.ReadIntegers(InputStateSize); + } + + public void GetObjectData(SerializationContext output) + { + if (output == null) + throw new ArgumentNullException("output"); + + output.WriteVersion(1); + output.Write(_rotState); + output.Write(_nextInputState); + output.Write(_inputState); + } + + #endregion + + #region Internal Members + + internal bool SampleCapturedConsoleSwitchState(ConsoleSwitch consoleSwitch) + { + return (_inputState[ConsoleSwitchIndex] & (1 << (int)consoleSwitch)) != 0; + } + + internal bool SampleCapturedControllerActionState(int playerno, ControllerAction action) + { + return (_inputState[ControllerActionStateIndex + (playerno & 3)] & (1 << (int)action)) != 0; + } + + internal int SampleCapturedOhmState(int playerNo) + { + return _inputState[OhmsIndex + (playerNo & 3)]; + } + + internal void SampleCapturedLightGunPosition(int playerNo, out int scanline, out int hpos) + { + var i = LightgunPositionIndex + ((playerNo & 1) << 1); + scanline = _inputState[i++]; + hpos = _inputState[i]; + } + + internal byte SampleCapturedDrivingState(int playerNo) + { + if (SampleCapturedControllerActionState(playerNo, ControllerAction.Driving0)) + _rotState[playerNo] = 0; + else if (SampleCapturedControllerActionState(playerNo, ControllerAction.Driving1)) + _rotState[playerNo] = 1; + else if (SampleCapturedControllerActionState(playerNo, ControllerAction.Driving2)) + _rotState[playerNo] = 2; + else if (SampleCapturedControllerActionState(playerNo, ControllerAction.Driving3)) + _rotState[playerNo] = 3; + return _rotGrayCodes[_rotState[playerNo]]; + } + + #endregion + + #region Object Overrides + + public override string ToString() + { + return GetType().Name; + } + + #endregion + + #region Helpers + + void SetControllerActionState(int playerNo, ControllerAction action, bool value) + { + if (value) + { + _nextInputState[ControllerActionStateIndex + (playerNo & 3)] |= (1 << (int)action); + } + else + { + _nextInputState[ControllerActionStateIndex + (playerNo & 3)] &= ~(1 << (int)action); + } + } + + void SetConsoleSwitchState(ConsoleSwitch consoleSwitch, bool value) + { + if (value) + { + _nextInputState[ConsoleSwitchIndex] |= (byte)(1 << (byte)consoleSwitch); + } + else + { + _nextInputState[ConsoleSwitchIndex] &= (byte)~(1 << (byte)consoleSwitch); + } + } + + void ToggleConsoleSwitchState(ConsoleSwitch consoleSwitch) + { + var consoleSwitchState = (_nextInputState[ConsoleSwitchIndex] & (1 << (int) consoleSwitch)) != 0; + SetConsoleSwitchState(consoleSwitch, !consoleSwitchState); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/LICENSE.TXT b/EMU7800/Core/LICENSE.TXT new file mode 100644 index 0000000000..d5354cb734 --- /dev/null +++ b/EMU7800/Core/LICENSE.TXT @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + Appendix: How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) 19yy + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111 USA + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) 19yy name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/EMU7800/Core/M6502.cs b/EMU7800/Core/M6502.cs new file mode 100644 index 0000000000..53df83446d --- /dev/null +++ b/EMU7800/Core/M6502.cs @@ -0,0 +1,1099 @@ +/* + * M6502.cs + * + * CPU emulator for the MOS Technology 6502 microprocessor. + * + * Copyright © 2003-2005 Mike Murphy + * + */ +using System; + +namespace EMU7800.Core +{ + public sealed class M6502 + { + delegate void OpcodeHandler(); + + OpcodeHandler[] Opcodes; + + const ushort + // non-maskable interrupt vector + NMI_VEC = 0xfffa, + // reset vector + RST_VEC = 0xfffc, + // interrupt request vector + IRQ_VEC = 0xfffe; + + readonly MachineBase M; + AddressSpace Mem { get { return M.Mem; } } + + public ulong Clock { get; set; } + public int RunClocks { get; set; } + public int RunClocksMultiple { get; private set; } + + public bool EmulatorPreemptRequest { get; set; } + public bool Jammed { get; set; } + public bool IRQInterruptRequest { get; set; } + public bool NMIInterruptRequest { get; set; } + + // 16-bit register + // program counter + public ushort PC { get; set; } + + // 8-bit registers + // accumulator + public byte A { get; set; } + // x index register + public byte X { get; set; } + // y index register + public byte Y { get; set; } + // stack pointer + public byte S { get; set; } + // processor status + public byte P { get; set; } + + public void Reset() + { + Jammed = false; + + // clear the stack + S = 0xff; + + fI = fZ = true; + + // reset the program counter + PC = WORD(Mem[RST_VEC], Mem[RST_VEC + 1]); + + clk(6); + + Log("{0} (PC:${1:x4}) reset", this, PC); + } + + public override String ToString() + { + return "M6502 CPU"; + } + + public void Execute() + { + EmulatorPreemptRequest = false; + + while (RunClocks > 0 && !EmulatorPreemptRequest && !Jammed) + { + if (NMIInterruptRequest) + { + InterruptNMI(); + NMIInterruptRequest = false; + } + else if (IRQInterruptRequest) + { + InterruptIRQ(); + IRQInterruptRequest = false; + } + else + { + Opcodes[Mem[PC++]](); + } + } + } + + private M6502() + { + InstallOpcodes(); + + Clock = 0; + RunClocks = 0; + RunClocksMultiple = 1; + + // initialize processor status, bit 5 is always set + P = 1 << 5; + } + + public M6502(MachineBase m, int runClocksMultiple) : this() + { + if (m == null) + throw new ArgumentNullException("m"); + if (runClocksMultiple <= 0) + throw new ArgumentException("runClocksMultiple must be greater than zero."); + + M = m; + RunClocksMultiple = runClocksMultiple; + } + + static byte MSB(ushort u16) + { + return (byte)(u16 >> 8); + } + + static byte LSB(ushort u16) + { + return (byte)u16; + } + + static ushort WORD(byte lsb, byte msb) + { + return (ushort)(lsb | msb << 8); + } + + // Processor Status Flag Bits + // + + // Flag bit setters and getters + void fset(byte flag, bool value) + { + P = (byte)(value ? P | flag : P & ~flag); + } + + bool fget(byte flag) + { + return (P & flag) != 0; + } + + // Carry: set if the add produced a carry, if the subtraction + // produced a borrow. Also used in shift instructions. + bool fC + { + get { return fget(1 << 0); } + set { fset(1 << 0, value); } + } + + // Zero: set if the result of the last operation was zero + bool fZ + { + get { return fget(1 << 1); } + set { fset(1 << 1, value); } + } + + // Irq Disable: set if maskable interrupts are disabled + bool fI + { + get { return fget(1 << 2); } + set { fset(1 << 2, value); } + } + + // Decimal Mode: set if decimal mode active + bool fD + { + get { return fget(1 << 3); } + set { fset(1 << 3, value); } + } + + // Brk: set if an interrupt caused by a BRK instruction, + // reset if caused by an internal interrupt + bool fB + { + get { return fget(1 << 4); } + set { fset(1 << 4, value); } + } + + // Overflow: set if the addition of two-like-signed numbers + // or the subtraction of two unlike-signed numbers + // produces a result greater than +127 or less than -128. + bool fV + { + get { return fget(1 << 6); } + set { fset(1 << 6, value); } + } + + // Negative: set if bit 7 of the accumulator is set + bool fN + { + get { return fget(1 << 7); } + set { fset(1 << 7, value); } + } + + void set_fNZ(byte u8) + { + fN = (u8 & 0x80) != 0; + fZ = (u8 & 0xff) == 0; + } + + byte pull() + { + S++; + return Mem[(ushort)(0x0100 + S)]; + } + + void push(byte data) + { + Mem[(ushort)(0x0100 + S)] = data; + S--; + } + + void clk(int ticks) + { + Clock += (ulong)ticks; + RunClocks -= (ticks*RunClocksMultiple); + } + + void InterruptNMI() + { + push(MSB(PC)); + push(LSB(PC)); + fB = false; + push(P); + fI = true; + PC = WORD(Mem[NMI_VEC], Mem[NMI_VEC + 1]); + clk(7); + } + + void InterruptIRQ() + { + if (IRQInterruptRequest && !fI) + { + push(MSB(PC)); + push(LSB(PC)); + fB = false; + push(P); + fI = true; + PC = WORD(Mem[IRQ_VEC], Mem[IRQ_VEC + 1]); + } + clk(7); + } + + void br(bool cond, ushort ea) + { + if (cond) + { + clk( (MSB(PC) == MSB(ea)) ? 1 : 2 ); + PC = ea; + } + } + + + // Relative: Bxx $aa (branch instructions only) + ushort aREL() + { + var bo = (sbyte)Mem[PC]; + PC++; + return (ushort)(PC + bo); + } + + // Zero Page: $aa + ushort aZPG() + { + return WORD(Mem[PC++], 0x00); + } + + // Zero Page Indexed,X: $aa,X + ushort aZPX() + { + return WORD((byte)(Mem[PC++] + X), 0x00); + } + + // Zero Page Indexed,Y: $aa,Y + ushort aZPY() + { + return WORD((byte)(Mem[PC++] + Y), 0x00); + } + + // Absolute: $aaaa + ushort aABS() + { + var lsb = Mem[PC++]; + var msb = Mem[PC++]; + return WORD(lsb, msb); + } + + // Absolute Indexed,X: $aaaa,X + ushort aABX(int eclk) + { + var ea = aABS(); + if (LSB(ea) + X > 0xff) + { + clk(eclk); + } + return (ushort)(ea + X); + } + + // Absolute Indexed,Y: $aaaa,Y + ushort aABY(int eclk) + { + var ea = aABS(); + if (LSB(ea) + Y > 0xff) + { + clk(eclk); + } + return (ushort)(ea + Y); + } + + // Indexed Indirect: ($aa,X) + ushort aIDX() + { + var zpa = (byte)(Mem[PC++] + X); + var lsb = Mem[zpa++]; + var msb = Mem[zpa]; + return WORD(lsb, msb); + } + + // Indirect Indexed: ($aa),Y + ushort aIDY(int eclk) + { + var zpa = Mem[PC++]; + var lsb = Mem[zpa++]; + var msb = Mem[zpa]; + if (lsb + Y > 0xff) + { + clk(eclk); + } + return (ushort)(WORD(lsb, msb) + Y); + } + + // Indirect Absolute: ($aaaa) (only used by JMP) + ushort aIND() + { + var ea = aABS(); + var lsb = Mem[ea]; + ea = WORD((byte)(LSB(ea) + 1), MSB(ea)); // NMOS 6502/7 quirk: does not fetch across page boundaries + var msb = Mem[ea]; + return WORD(lsb, msb); + } + + // aACC = Accumulator + // aIMM = Immediate + // aIMP = Implied + + // ADC: Add with carry + void iADC(byte mem) + { + var c = fC ? 1 : 0; + var sum = A + mem + c; + fV = (~(A ^ mem) & (A ^ (sum & 0xff)) & 0x80) != 0; + if (fD) + { + // NMOS 6502/7 quirk: The N, V, and Z flags reflect the binary result, not the BCD result + var lo = (A & 0xf) + (mem & 0xf) + c; + var hi = (A >> 4) + (mem >> 4); + if (lo > 9) + { + lo += 6; + hi++; + } + if (hi > 9) + { + hi += 6; + } + A = (byte)((lo & 0xf) | (hi << 4)); + fC = (hi & 0x10) != 0; + } + else + { + A = (byte)sum; + fC = (sum & 0x100) != 0; + } + set_fNZ((byte)sum); + } + + // AND: Logical and + void iAND(byte mem) + { + A &= mem; + set_fNZ(A); + } + + // ASL: Arithmetic shift left: C <- [7][6][5][4][3][2][1][0] <- 0 + byte iASL(byte mem) + { + fC = (mem & 0x80) != 0; + mem <<= 1; + set_fNZ(mem); + return mem; + } + + // BIT: Bit test + void iBIT(byte mem) + { + fN = (mem & 0x80) != 0; + fV = (mem & 0x40) != 0; + fZ = (mem & A) == 0; + } + + // BRK Force Break (cause software interrupt) + void iBRK() + { + PC++; + fB = true; + push(MSB(PC)); + push(LSB(PC)); + push(P); + fI = true; + var lsb = Mem[IRQ_VEC]; + var msb = Mem[IRQ_VEC+1]; + PC = WORD(lsb, msb); + } + + // CLC: Clear carry flag + void iCLC() + { + fC = false; + } + + // CLD: Clear decimal mode + void iCLD() + { + fD = false; + } + + // CLI: Clear interrupt disable */ + void iCLI() + { + fI = false; + } + + // CLV: Clear overflow flag + void iCLV() + { + fV = false; + } + + // CMP: Compare accumulator + void iCMP(byte mem) + { + fC = A >= mem; + set_fNZ((byte)(A - mem)); + } + + // CPX: Compare index X + void iCPX(byte mem) + { + fC = X >= mem; + set_fNZ((byte)(X - mem)); + } + + // CPY: Compare index Y + void iCPY(byte mem) + { + fC = Y >= mem; + set_fNZ((byte)(Y - mem)); + } + + // DEC: Decrement memory + byte iDEC(byte mem) + { + mem--; + set_fNZ(mem); + return mem; + } + + // DEX: Decrement index x + void iDEX() + { + X--; + set_fNZ(X); + } + + // DEY: Decrement index y + void iDEY() + { + Y--; + set_fNZ(Y); + } + + // EOR: Logical exclusive or + void iEOR(byte mem) + { + A ^= mem; + set_fNZ(A); + } + + // INC: Increment memory + byte iINC(byte mem) + { + mem++; + set_fNZ(mem); + return mem; + } + + // INX: Increment index x + void iINX() + { + X++; + set_fNZ(X); + } + + // INY: Increment index y + void iINY() + { + Y++; + set_fNZ(Y); + } + + // JMP Jump to address + void iJMP(ushort ea) + { + PC = ea; + } + + // JSR Jump to subroutine + void iJSR(ushort ea) + { + PC--; // NMOS 6502/7 quirk: iRTS compensates + push(MSB(PC)); + push(LSB(PC)); + PC = ea; + } + + // LDA: Load accumulator + void iLDA(byte mem) + { + A = mem; + set_fNZ(A); + } + + // LDX: Load index X + void iLDX(byte mem) + { + X = mem; + set_fNZ(X); + } + + // LDY: Load index Y + void iLDY(byte mem) + { + Y = mem; + set_fNZ(Y); + } + + // LSR: Logic shift right: 0 -> [7][6][5][4][3][2][1][0] -> C + byte iLSR(byte mem) + { + fC = (mem & 0x01) != 0; + mem >>= 1; + set_fNZ(mem); + return mem; + } + + // NOP: No operation + void iNOP() + { + if (M.NOPRegisterDumping) + { + Log("NOP: {0}", M6502DASM.GetRegisters(this)); + } + } + + // ORA: Logical inclusive or + void iORA(byte mem) + { + A |= mem; + set_fNZ(A); + } + + // PHA: Push accumulator + void iPHA() + { + push(A); + } + + // PHP: Push processor status (flags) + void iPHP() + { + push(P); + } + + // PLA: Pull accumuator + void iPLA() + { + A = pull(); + set_fNZ(A); + } + + // PLP: Pull processor status (flags) + void iPLP() + { + P = pull(); + fB = true; + } + + // ROL: Rotate left: new C <- [7][6][5][4][3][2][1][0] <- C + byte iROL(byte mem) + { + var d0 = (byte)(fC ? 0x01 : 0x00); + + fC = (mem & 0x80) != 0; + mem <<= 1; + mem |= d0; + set_fNZ(mem); + return mem; + } + + // ROR: Rotate right: C -> [7][6][5][4][3][2][1][0] -> new C + byte iROR(byte mem) + { + var d7 = (byte)(fC ? 0x80 : 0x00); + + fC = (mem & 0x01) != 0; + mem >>= 1; + mem |= d7; + set_fNZ(mem); + return mem; + } + + // RTI: Return from interrupt + void iRTI() + { + P = pull(); + var lsb = pull(); + var msb = pull(); + PC = WORD(lsb, msb); + fB = true; + } + + // RTS: Return from subroutine + void iRTS() + { + var lsb = pull(); + var msb = pull(); + PC = WORD(lsb, msb); + PC++; // NMOS 6502/7 quirk: iJSR compensates + } + + // SBC: Subtract with carry (borrow) + void iSBC(byte mem) + { + var c = fC ? 0 : 1; + var sum = A - mem - c; + fV = ((A ^ mem) & (A ^ (sum & 0xff)) & 0x80) != 0; + if (fD) + { + // NMOS 6502/7 quirk: The N, V, and Z flags reflect the binary result, not the BCD result + var lo = (A & 0xf) - (mem & 0xf) - c; + var hi = (A >> 4) - (mem >> 4); + if ((lo & 0x10) != 0) + { + lo -= 6; + hi--; + } + if ((hi & 0x10) != 0) + { + hi -= 6; + } + A = (byte)((lo & 0xf) | (hi << 4)); + } + else + { + A = (byte)sum; + } + fC = (sum & 0x100) == 0; + set_fNZ((byte)sum); + } + + // SEC: Set carry flag + void iSEC() + { + fC = true; + } + + // SED: Set decimal mode + void iSED() + { + fD = true; + } + + // SEI: Set interrupt disable + void iSEI() + { + fI = true; + } + + // STA: Store accumulator + byte iSTA() + { + return A; + } + + // STX: Store index X + byte iSTX() + { + return X; + } + + // STY: Store index Y + byte iSTY() + { + return Y; + } + + // TAX: Transfer accumlator to index X + void iTAX() + { + X = A; + set_fNZ(X); + } + + // TAY: Transfer accumlator to index Y + void iTAY() + { + Y = A; + set_fNZ(Y); + } + + // TSX: Transfer stack to index X + void iTSX() + { + X = S; + set_fNZ(X); + } + + // TXA: Transfer index X to accumlator + void iTXA() + { + A = X; + set_fNZ(A); + } + + // TXS: Transfer index X to stack + void iTXS() + { + S = X; + // No flags set..! Weird, huh? + } + + // TYA: Transfer index Y to accumulator + void iTYA() + { + A = Y; + set_fNZ(A); + } + + // Illegal opcodes + + // KIL: Jam the processor + void iKIL() + { + Jammed = true; + Log("{0}: Processor jammed!", this); + } + + // LAX: Load accumulator and index x + void iLAX(byte mem) + { + A = X = mem; + set_fNZ(A); + } + + // ISB: Increment and subtract with carry + void iISB(byte mem) + { + mem++; + iSBC(mem); + } + + // RLA: Rotate left and logical and accumulator + // new C <- [7][6][5][4][3][2][1][0] <- C + void iRLA(byte mem) + { + var d0 = (byte)(fC ? 0x01 : 0x00); + + fC = (mem & 0x80) != 0; + mem <<= 1; + mem |= d0; + + A &= mem; + set_fNZ(A); + } + + // SAX: logical and accumulator with index X and store + byte iSAX() + { + return (byte)(A & X); + } + + void InstallOpcodes() + { + Opcodes = new OpcodeHandler[0x100]; + ushort EA; + + Opcodes[0x65] = delegate { EA = aZPG(); clk(3); iADC(Mem[EA]); }; + Opcodes[0x75] = delegate { EA = aZPX(); clk(4); iADC(Mem[EA]); }; + Opcodes[0x61] = delegate { EA = aIDX(); clk(6); iADC(Mem[EA]); }; + Opcodes[0x71] = delegate { EA = aIDY(1); clk(5); iADC(Mem[EA]); }; + Opcodes[0x79] = delegate { EA = aABY(1); clk(4); iADC(Mem[EA]); }; + Opcodes[0x6d] = delegate { EA = aABS(); clk(4); iADC(Mem[EA]); }; + Opcodes[0x7d] = delegate { EA = aABX(1); clk(4); iADC(Mem[EA]); }; + Opcodes[0x69] = delegate { /*aIMM*/ clk(2); iADC(Mem[PC++]); }; + + Opcodes[0x25] = delegate { EA = aZPG(); clk(3); iAND(Mem[EA]); }; // may be 2 clk + Opcodes[0x35] = delegate { EA = aZPX(); clk(4); iAND(Mem[EA]); }; // may be 3 clk + Opcodes[0x21] = delegate { EA = aIDX(); clk(6); iAND(Mem[EA]); }; + Opcodes[0x31] = delegate { EA = aIDY(1); clk(5); iAND(Mem[EA]); }; + Opcodes[0x2d] = delegate { EA = aABS(); clk(4); iAND(Mem[EA]); }; + Opcodes[0x39] = delegate { EA = aABY(1); clk(4); iAND(Mem[EA]); }; + Opcodes[0x3d] = delegate { EA = aABX(1); clk(4); iAND(Mem[EA]); }; + Opcodes[0x29] = delegate { /*aIMM*/ clk(2); iAND(Mem[PC++]); }; + + Opcodes[0x06] = delegate { EA = aZPG(); clk(5); Mem[EA] = iASL(Mem[EA]); }; + Opcodes[0x16] = delegate { EA = aZPX(); clk(6); Mem[EA] = iASL(Mem[EA]); }; + Opcodes[0x0e] = delegate { EA = aABS(); clk(6); Mem[EA] = iASL(Mem[EA]); }; + Opcodes[0x1e] = delegate { EA = aABX(0); clk(7); Mem[EA] = iASL(Mem[EA]); }; + Opcodes[0x0a] = delegate { /*aACC*/ clk(2); A = iASL(A); }; + + Opcodes[0x24] = delegate { EA = aZPG(); clk(3); iBIT(Mem[EA]); }; + Opcodes[0x2c] = delegate { EA = aABS(); clk(4); iBIT(Mem[EA]); }; + + Opcodes[0x10] = delegate { EA = aREL(); clk(2); br(!fN, EA); /* BPL */ }; + Opcodes[0x30] = delegate { EA = aREL(); clk(2); br( fN, EA); /* BMI */ }; + Opcodes[0x50] = delegate { EA = aREL(); clk(2); br(!fV, EA); /* BVC */ }; + Opcodes[0x70] = delegate { EA = aREL(); clk(2); br( fV, EA); /* BVS */ }; + Opcodes[0x90] = delegate { EA = aREL(); clk(2); br(!fC, EA); /* BCC */ }; + Opcodes[0xb0] = delegate { EA = aREL(); clk(2); br( fC, EA); /* BCS */ }; + Opcodes[0xd0] = delegate { EA = aREL(); clk(2); br(!fZ, EA); /* BNE */ }; + Opcodes[0xf0] = delegate { EA = aREL(); clk(2); br( fZ, EA); /* BEQ */ }; + + Opcodes[0x00] = delegate { /*aIMP*/ clk(7); iBRK(); }; + + Opcodes[0x18] = delegate { /*aIMP*/ clk(2); iCLC(); }; + + Opcodes[0xd8] = delegate { /*aIMP*/ clk(2); iCLD(); }; + + Opcodes[0x58] = delegate { /*aIMP*/ clk(2); iCLI(); }; + + Opcodes[0xb8] = delegate { /*aIMP*/ clk(2); iCLV(); }; + + Opcodes[0xc5] = delegate { EA = aZPG(); clk(3); iCMP(Mem[EA]); }; + Opcodes[0xd5] = delegate { EA = aZPX(); clk(4); iCMP(Mem[EA]); }; + Opcodes[0xc1] = delegate { EA = aIDX(); clk(6); iCMP(Mem[EA]); }; + Opcodes[0xd1] = delegate { EA = aIDY(1); clk(5); iCMP(Mem[EA]); }; + Opcodes[0xcd] = delegate { EA = aABS(); clk(4); iCMP(Mem[EA]); }; + Opcodes[0xdd] = delegate { EA = aABX(1); clk(4); iCMP(Mem[EA]); }; + Opcodes[0xd9] = delegate { EA = aABY(1); clk(4); iCMP(Mem[EA]); }; + Opcodes[0xc9] = delegate { /*aIMM*/ clk(2); iCMP(Mem[PC++]); }; + + Opcodes[0xe4] = delegate { EA = aZPG(); clk(3); iCPX(Mem[EA]); }; + Opcodes[0xec] = delegate { EA = aABS(); clk(4); iCPX(Mem[EA]); }; + Opcodes[0xe0] = delegate { /*aIMM*/ clk(2); iCPX(Mem[PC++]); }; + + Opcodes[0xc4] = delegate { EA = aZPG(); clk(3); iCPY(Mem[EA]); }; + Opcodes[0xcc] = delegate { EA = aABS(); clk(4); iCPY(Mem[EA]); }; + Opcodes[0xc0] = delegate { /*aIMM*/ clk(2); iCPY(Mem[PC++]); }; + + Opcodes[0xc6] = delegate { EA = aZPG(); clk(5); Mem[EA] = iDEC(Mem[EA]); }; + Opcodes[0xd6] = delegate { EA = aZPX(); clk(6); Mem[EA] = iDEC(Mem[EA]); }; + Opcodes[0xce] = delegate { EA = aABS(); clk(6); Mem[EA] = iDEC(Mem[EA]); }; + Opcodes[0xde] = delegate { EA = aABX(0); clk(7); Mem[EA] = iDEC(Mem[EA]); }; + + Opcodes[0xca] = delegate { /*aIMP*/ clk(2); iDEX(); }; + + Opcodes[0x88] = delegate { /*aIMP*/ clk(2); iDEY(); }; + + Opcodes[0x45] = delegate { EA = aZPG(); clk(3); iEOR(Mem[EA]); }; + Opcodes[0x55] = delegate { EA = aZPX(); clk(4); iEOR(Mem[EA]); }; + Opcodes[0x41] = delegate { EA = aIDX(); clk(6); iEOR(Mem[EA]); }; + Opcodes[0x51] = delegate { EA = aIDY(1); clk(5); iEOR(Mem[EA]); }; + Opcodes[0x4d] = delegate { EA = aABS(); clk(4); iEOR(Mem[EA]); }; + Opcodes[0x5d] = delegate { EA = aABX(1); clk(4); iEOR(Mem[EA]); }; + Opcodes[0x59] = delegate { EA = aABY(1); clk(4); iEOR(Mem[EA]); }; + Opcodes[0x49] = delegate { /*aIMM*/ clk(2); iEOR(Mem[PC++]); }; + + Opcodes[0xe6] = delegate { EA = aZPG(); clk(5); Mem[EA] = iINC(Mem[EA]); }; + Opcodes[0xf6] = delegate { EA = aZPX(); clk(6); Mem[EA] = iINC(Mem[EA]); }; + Opcodes[0xee] = delegate { EA = aABS(); clk(6); Mem[EA] = iINC(Mem[EA]); }; + Opcodes[0xfe] = delegate { EA = aABX(0); clk(7); Mem[EA] = iINC(Mem[EA]); }; + + Opcodes[0xe8] = delegate { /*aIMP*/ clk(2); iINX(); }; + + Opcodes[0xc8] = delegate { /*aIMP*/ clk(2); iINY(); }; + + Opcodes[0xa5] = delegate { EA = aZPG(); clk(3); iLDA(Mem[EA]); }; + Opcodes[0xb5] = delegate { EA = aZPX(); clk(4); iLDA(Mem[EA]); }; + Opcodes[0xa1] = delegate { EA = aIDX(); clk(6); iLDA(Mem[EA]); }; + Opcodes[0xb1] = delegate { EA = aIDY(1); clk(5); iLDA(Mem[EA]); }; + Opcodes[0xad] = delegate { EA = aABS(); clk(4); iLDA(Mem[EA]); }; + Opcodes[0xbd] = delegate { EA = aABX(1); clk(4); iLDA(Mem[EA]); }; + Opcodes[0xb9] = delegate { EA = aABY(1); clk(4); iLDA(Mem[EA]); }; + Opcodes[0xa9] = delegate { /*aIMM*/ clk(2); iLDA(Mem[PC++]); }; + + Opcodes[0xa6] = delegate { EA = aZPG(); clk(3); iLDX(Mem[EA]); }; + Opcodes[0xb6] = delegate { EA = aZPY(); clk(4); iLDX(Mem[EA]); }; + Opcodes[0xae] = delegate { EA = aABS(); clk(4); iLDX(Mem[EA]); }; + Opcodes[0xbe] = delegate { EA = aABY(1); clk(4); iLDX(Mem[EA]); }; + Opcodes[0xa2] = delegate { /*aIMM*/ clk(2); iLDX(Mem[PC++]); }; + + Opcodes[0xa4] = delegate { EA = aZPG(); clk(3); iLDY(Mem[EA]); }; + Opcodes[0xb4] = delegate { EA = aZPX(); clk(4); iLDY(Mem[EA]); }; + Opcodes[0xac] = delegate { EA = aABS(); clk(4); iLDY(Mem[EA]); }; + Opcodes[0xbc] = delegate { EA = aABX(1); clk(4); iLDY(Mem[EA]); }; + Opcodes[0xa0] = delegate { /*aIMM*/ clk(2); iLDY(Mem[PC++]); }; + + Opcodes[0x46] = delegate { EA = aZPG(); clk(5); Mem[EA] = iLSR(Mem[EA]); }; + Opcodes[0x56] = delegate { EA = aZPX(); clk(6); Mem[EA] = iLSR(Mem[EA]); }; + Opcodes[0x4e] = delegate { EA = aABS(); clk(6); Mem[EA] = iLSR(Mem[EA]); }; + Opcodes[0x5e] = delegate { EA = aABX(0); clk(7); Mem[EA] = iLSR(Mem[EA]); }; + Opcodes[0x4a] = delegate { /*aACC*/ clk(2); A = iLSR(A); }; + + Opcodes[0x4c] = delegate { EA = aABS(); clk(3); iJMP(EA); }; + Opcodes[0x6c] = delegate { EA = aIND(); clk(5); iJMP(EA); }; + + Opcodes[0x20] = delegate { EA = aABS(); clk(6); iJSR(EA); }; + + Opcodes[0xea] = delegate { /*aIMP*/ clk(2); iNOP(); }; + + Opcodes[0x05] = delegate { EA = aZPG(); clk(3); iORA(Mem[EA]); }; // may be 2 clk + Opcodes[0x15] = delegate { EA = aZPX(); clk(4); iORA(Mem[EA]); }; // may be 3 clk + Opcodes[0x01] = delegate { EA = aIDX(); clk(6); iORA(Mem[EA]); }; + Opcodes[0x11] = delegate { EA = aIDY(1); clk(5); iORA(Mem[EA]); }; + Opcodes[0x0d] = delegate { EA = aABS(); clk(4); iORA(Mem[EA]); }; + Opcodes[0x1d] = delegate { EA = aABX(1); clk(4); iORA(Mem[EA]); }; + Opcodes[0x19] = delegate { EA = aABY(1); clk(4); iORA(Mem[EA]); }; + Opcodes[0x09] = delegate { /*aIMM*/ clk(2); iORA(Mem[PC++]); }; + + Opcodes[0x48] = delegate { /*aIMP*/ clk(3); iPHA(); }; + + Opcodes[0x68] = delegate { /*aIMP*/ clk(4); iPLA(); }; + + Opcodes[0x08] = delegate { /*aIMP*/ clk(3); iPHP(); }; + + Opcodes[0x28] = delegate { /*aIMP*/ clk(4); iPLP(); }; + + Opcodes[0x26] = delegate { EA = aZPG(); clk(5); Mem[EA] = iROL(Mem[EA]); }; + Opcodes[0x36] = delegate { EA = aZPX(); clk(6); Mem[EA] = iROL(Mem[EA]); }; + Opcodes[0x2e] = delegate { EA = aABS(); clk(6); Mem[EA] = iROL(Mem[EA]); }; + Opcodes[0x3e] = delegate { EA = aABX(0); clk(7); Mem[EA] = iROL(Mem[EA]); }; + Opcodes[0x2a] = delegate { /*aACC*/ clk(2); A = iROL(A); }; + + Opcodes[0x66] = delegate { EA = aZPG(); clk(5); Mem[EA] = iROR(Mem[EA]); }; + Opcodes[0x76] = delegate { EA = aZPX(); clk(6); Mem[EA] = iROR(Mem[EA]); }; + Opcodes[0x6e] = delegate { EA = aABS(); clk(6); Mem[EA] = iROR(Mem[EA]); }; + Opcodes[0x7e] = delegate { EA = aABX(0); clk(7); Mem[EA] = iROR(Mem[EA]); }; + Opcodes[0x6a] = delegate { /*aACC*/ clk(2); A = iROR(A); }; + + Opcodes[0x40] = delegate { /*aIMP*/ clk(6); iRTI(); }; + + Opcodes[0x60] = delegate { /*aIMP*/ clk(6); iRTS(); }; + + Opcodes[0xe5] = delegate { EA = aZPG(); clk(3); iSBC(Mem[EA]); }; + Opcodes[0xf5] = delegate { EA = aZPX(); clk(4); iSBC(Mem[EA]); }; + Opcodes[0xe1] = delegate { EA = aIDX(); clk(6); iSBC(Mem[EA]); }; + Opcodes[0xf1] = delegate { EA = aIDY(1); clk(5); iSBC(Mem[EA]); }; + Opcodes[0xed] = delegate { EA = aABS(); clk(4); iSBC(Mem[EA]); }; + Opcodes[0xfd] = delegate { EA = aABX(1); clk(4); iSBC(Mem[EA]); }; + Opcodes[0xf9] = delegate { EA = aABY(1); clk(4); iSBC(Mem[EA]); }; + Opcodes[0xe9] = delegate { /*aIMM*/ clk(2); iSBC(Mem[PC++]); }; + + Opcodes[0x38] = delegate { /*aIMP*/ clk(2); iSEC(); }; + + Opcodes[0xf8] = delegate { /*aIMP*/ clk(2); iSED(); }; + + Opcodes[0x78] = delegate { /*aIMP*/ clk(2); iSEI(); }; + + Opcodes[0x85] = delegate { EA = aZPG(); clk(3); Mem[EA] = iSTA(); }; + Opcodes[0x95] = delegate { EA = aZPX(); clk(4); Mem[EA] = iSTA(); }; + Opcodes[0x81] = delegate { EA = aIDX(); clk(6); Mem[EA] = iSTA(); }; + Opcodes[0x91] = delegate { EA = aIDY(0); clk(6); Mem[EA] = iSTA(); }; + Opcodes[0x8d] = delegate { EA = aABS(); clk(4); Mem[EA] = iSTA(); }; + Opcodes[0x99] = delegate { EA = aABY(0); clk(5); Mem[EA] = iSTA(); }; + Opcodes[0x9d] = delegate { EA = aABX(0); clk(5); Mem[EA] = iSTA(); }; + + Opcodes[0x86] = delegate { EA = aZPG(); clk(3); Mem[EA] = iSTX(); }; + Opcodes[0x96] = delegate { EA = aZPY(); clk(4); Mem[EA] = iSTX(); }; + Opcodes[0x8e] = delegate { EA = aABS(); clk(4); Mem[EA] = iSTX(); }; + + Opcodes[0x84] = delegate { EA = aZPG(); clk(3); Mem[EA] = iSTY(); }; + Opcodes[0x94] = delegate { EA = aZPX(); clk(4); Mem[EA] = iSTY(); }; + Opcodes[0x8c] = delegate { EA = aABS(); clk(4); Mem[EA] = iSTY(); }; + + Opcodes[0xaa] = delegate { /*aIMP*/ clk(2); iTAX(); }; + + Opcodes[0xa8] = delegate { /*aIMP*/ clk(2); iTAY(); }; + + Opcodes[0xba] = delegate { /*aIMP*/ clk(2); iTSX(); }; + + Opcodes[0x8a] = delegate { /*aIMP*/ clk(2); iTXA(); }; + + Opcodes[0x9a] = delegate { /*aIMP*/ clk(2); iTXS(); }; + + Opcodes[0x98] = delegate { /*aIMP*/ clk(2); iTYA(); }; + + // Illegal opcodes + foreach (int opCode in new ushort[] { 0x02, 0x12, 0x22, 0x32, 0x42, 0x52, 0x62, 0x72, 0x92, 0xb2, 0xd2, 0xf2 }) + { + Opcodes[opCode] = delegate { clk(2); iKIL(); }; + } + Opcodes[0x3f] = delegate { EA = aABX(0); clk(4); iRLA(Mem[EA]); }; + Opcodes[0xa7] = delegate { EA = aZPX(); clk(3); iLAX(Mem[EA]); }; + Opcodes[0xb3] = delegate { EA = aIDY(0); clk(6); iLAX(Mem[EA]); }; + Opcodes[0xef] = delegate { EA = aABS(); clk(6); iISB(Mem[EA]); }; + Opcodes[0x0c] = delegate { EA = aABS(); clk(2); iNOP(); }; + foreach (int opCode in new ushort[] { 0x1c, 0x3c, 0x5c, 0x7c, 0x9c, 0xdc, 0xfc }) + { + Opcodes[opCode] = delegate { EA = aABX(0); clk(2); iNOP(); }; + } + Opcodes[0x83] = delegate { EA = aIDX(); clk(6); Mem[EA] = iSAX(); }; + Opcodes[0x87] = delegate { EA = aZPG(); clk(3); Mem[EA] = iSAX(); }; + Opcodes[0x8f] = delegate { EA = aABS(); clk(4); Mem[EA] = iSAX(); }; + Opcodes[0x97] = delegate { EA = aZPY(); clk(4); Mem[EA] = iSAX(); }; + Opcodes[0xa3] = delegate { EA = aIDX(); clk(6); iLAX(Mem[EA]); }; + Opcodes[0xb7] = delegate { EA = aZPY(); clk(4); iLAX(Mem[EA]); }; + Opcodes[0xaf] = delegate { EA = aABS(); clk(5); iLAX(Mem[EA]); }; + Opcodes[0xbf] = delegate { EA = aABY(0); clk(6); iLAX(Mem[EA]); }; + Opcodes[0xff] = delegate { EA = aABX(0); clk(7); iISB(Mem[EA]); }; + + OpcodeHandler opNULL = () => Log("{0}:**UNKNOWN OPCODE: ${1:x2} at ${2:x4}\n", this, Mem[(ushort)(PC - 1)], PC - 1); + + for (var i=0; i < Opcodes.Length; i++) + { + if (Opcodes[i] == null) + { + Opcodes[i] = opNULL; + } + } + } + + #region Serialization Members + + public M6502(DeserializationContext input, MachineBase m, int runClocksMultiple) : this(m, runClocksMultiple) + { + if (input == null) + throw new ArgumentNullException("input"); + + input.CheckVersion(1); + Clock = input.ReadUInt64(); + RunClocks = input.ReadInt32(); + RunClocksMultiple = input.ReadInt32(); + EmulatorPreemptRequest = input.ReadBoolean(); + Jammed = input.ReadBoolean(); + IRQInterruptRequest = input.ReadBoolean(); + NMIInterruptRequest = input.ReadBoolean(); + PC = input.ReadUInt16(); + A = input.ReadByte(); + X = input.ReadByte(); + Y = input.ReadByte(); + S = input.ReadByte(); + P = input.ReadByte(); + } + + public void GetObjectData(SerializationContext output) + { + if (output == null) + throw new ArgumentNullException("output"); + + output.WriteVersion(1); + output.Write(Clock); + output.Write(RunClocks); + output.Write(RunClocksMultiple); + output.Write(EmulatorPreemptRequest); + output.Write(Jammed); + output.Write(IRQInterruptRequest); + output.Write(NMIInterruptRequest); + output.Write(PC); + output.Write(A); + output.Write(X); + output.Write(Y); + output.Write(S); + output.Write(P); + } + + #endregion + + #region Helpers + + void Log(string format, params object[] args) + { + if (M == null || M.Logger == null) + return; + M.Logger.WriteLine(format, args); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/M6502DASM.cs b/EMU7800/Core/M6502DASM.cs new file mode 100644 index 0000000000..4f36060edc --- /dev/null +++ b/EMU7800/Core/M6502DASM.cs @@ -0,0 +1,242 @@ +/* + * M6502DASM.cs + * + * Provides disassembly services. + * + * Copyright © 2003, 2004 Mike Murphy + * + */ +using System; +using System.Text; + +namespace EMU7800.Core +{ + public static class M6502DASM + { + // Instruction Mnemonics + enum m : uint + { + ADC = 1, AND, ASL, + BIT, BCC, BCS, BEQ, BMI, BNE, BPL, BRK, BVC, BVS, + CLC, CLD, CLI, CLV, CMP, CPX, CPY, + DEC, DEX, DEY, + EOR, + INC, INX, INY, + JMP, JSR, + LDA, LDX, LDY, LSR, + NOP, + ORA, + PLA, PLP, PHA, PHP, + ROL, ROR, RTI, RTS, + SEC, SEI, STA, SBC, SED, STX, STY, + TAX, TAY, TSX, TXA, TXS, TYA, + + // Illegal/undefined opcodes + isb, + kil, + lax, + rla, + sax, + top + } + + // Addressing Modes + enum a : uint + { + REL, // Relative: $aa (branch instructions only) + ZPG, // Zero Page: $aa + ZPX, // Zero Page Indexed X: $aa,X + ZPY, // Zero Page Indexed Y: $aa,Y + ABS, // Absolute: $aaaa + ABX, // Absolute Indexed X: $aaaa,X + ABY, // Absolute Indexed Y: $aaaa,Y + IDX, // Indexed Indirect: ($aa,X) + IDY, // Indirect Indexed: ($aa),Y + IND, // Indirect Absolute: ($aaaa) (JMP only) + IMM, // Immediate: #aa + IMP, // Implied + ACC // Accumulator + } + + static readonly m[] MnemonicMatrix = { +// 0 1 2 3 4 5 6 7 8 9 A B C D E F +/*0*/ m.BRK, m.ORA, m.kil, 0, 0, m.ORA, m.ASL, 0, m.PHP, m.ORA, m.ASL, 0, m.top, m.ORA, m.ASL, 0,/*0*/ +/*1*/ m.BPL, m.ORA, m.kil, 0, 0, m.ORA, m.ASL, 0, m.CLC, m.ORA, 0, 0, m.top, m.ORA, m.ASL, 0,/*1*/ +/*2*/ m.JSR, m.AND, m.kil, 0, m.BIT, m.AND, m.ROL, 0, m.PLP, m.AND, m.ROL, 0, m.BIT, m.AND, m.ROL, 0,/*2*/ +/*3*/ m.BMI, m.AND, m.kil, 0, 0, m.AND, m.ROL, 0, m.SEC, m.AND, 0, 0, m.top, m.AND, m.ROL, m.rla,/*3*/ +/*4*/ m.RTI, m.EOR, m.kil, 0, 0, m.EOR, m.LSR, 0, m.PHA, m.EOR, m.LSR, 0, m.JMP, m.EOR, m.LSR, 0,/*4*/ +/*5*/ m.BVC, m.EOR, m.kil, 0, 0, m.EOR, m.LSR, 0, m.CLI, m.EOR, 0, 0, m.top, m.EOR, m.LSR, 0,/*5*/ +/*6*/ m.RTS, m.ADC, m.kil, 0, 0, m.ADC, m.ROR, 0, m.PLA, m.ADC, m.ROR, 0, m.JMP, m.ADC, m.ROR, 0,/*6*/ +/*7*/ m.BVS, m.ADC, m.kil, 0, 0, m.ADC, m.ROR, 0, m.SEI, m.ADC, 0, 0, m.top, m.ADC, m.ROR, 0,/*7*/ +/*8*/ 0, m.STA, 0, m.sax, m.STY, m.STA, m.STX, m.sax, m.DEY, 0, m.TXA, 0, m.STY, m.STA, m.STX, m.sax,/*8*/ +/*9*/ m.BCC, m.STA, m.kil, 0, m.STY, m.STA, m.STX, m.sax, m.TYA, m.STA, m.TXS, 0, m.top, m.STA, 0, 0,/*9*/ +/*A*/ m.LDY, m.LDA, m.LDX, m.lax, m.LDY, m.LDA, m.LDX, m.lax, m.TAY, m.LDA, m.TAX, 0, m.LDY, m.LDA, m.LDX, m.lax,/*A*/ +/*B*/ m.BCS, m.LDA, m.kil, m.lax, m.LDY, m.LDA, m.LDX, m.lax, m.CLV, m.LDA, m.TSX, 0, m.LDY, m.LDA, m.LDX, m.lax,/*B*/ +/*C*/ m.CPY, m.CMP, 0, 0, m.CPY, m.CMP, m.DEC, 0, m.INY, m.CMP, m.DEX, 0, m.CPY, m.CMP, m.DEC, 0,/*C*/ +/*D*/ m.BNE, m.CMP, m.kil, 0, 0, m.CMP, m.DEC, 0, m.CLD, m.CMP, 0, 0, m.top, m.CMP, m.DEC, 0,/*D*/ +/*E*/ m.CPX, m.SBC, 0, 0, m.CPX, m.SBC, m.INC, 0, m.INX, m.SBC, m.NOP, 0, m.CPX, m.SBC, m.INC, m.isb,/*E*/ +/*F*/ m.BEQ, m.SBC, m.kil, 0, 0, m.SBC, m.INC, 0, m.SED, m.SBC, 0, 0, m.top, m.SBC, m.INC, m.isb /*F*/ +}; + + static readonly a[] AddressingModeMatrix = { +// 0 1 2 3 4 5 6 7 8 9 A B C D E F +/*0*/ a.IMP, a.IDX, a.IMP, 0, 0, a.ZPG, a.ZPG, 0, a.IMP, a.IMM, a.ACC, 0, a.ABS, a.ABS, a.ABS, 0,/*0*/ +/*1*/ a.REL, a.IDY, a.IMP, 0, 0, a.ZPG, a.ZPG, 0, a.IMP, a.ABY, 0, 0, a.ABS, a.ABX, a.ABX, 0,/*1*/ +/*2*/ a.ABS, a.IDX, a.IMP, 0, a.ZPG, a.ZPG, a.ZPG, 0, a.IMP, a.IMM, a.ACC, 0, a.ABS, a.ABS, a.ABS, 0,/*2*/ +/*3*/ a.REL, a.IDY, a.IMP, 0, 0, a.ZPG, a.ZPG, 0, a.IMP, a.ABY, 0, 0, a.ABS, a.ABX, a.ABX, a.ABX,/*3*/ +/*4*/ a.IMP, a.IDY, a.IMP, 0, 0, a.ZPG, a.ZPG, 0, a.IMP, a.IMM, a.ACC, 0, a.ABS, a.ABS, a.ABS, 0,/*4*/ +/*5*/ a.REL, a.IDY, a.IMP, 0, 0, a.ZPG, a.ZPG, 0, a.IMP, a.ABY, 0, 0, a.ABS, a.ABX, a.ABX, 0,/*5*/ +/*6*/ a.IMP, a.IDX, a.IMP, 0, 0, a.ZPG, a.ZPG, 0, a.IMP, a.IMM, a.ACC, 0, a.IND, a.ABS, a.ABS, 0,/*6*/ +/*7*/ a.REL, a.IDY, a.IMP, 0, 0, a.ZPX, a.ZPX, 0, a.IMP, a.ABY, 0, 0, a.ABS, a.ABX, a.ABX, 0,/*7*/ +/*8*/ 0, a.IDY, 0, a.IDX, a.ZPG, a.ZPG, a.ZPG, a.ZPG, a.IMP, 0, a.IMP, 0, a.ABS, a.ABS, a.ABS, a.ABS,/*8*/ +/*9*/ a.REL, a.IDY, a.IMP, 0, a.ZPX, a.ZPX, a.ZPY, a.ZPY, a.IMP, a.ABY, a.IMP, 0, a.ABS, a.ABX, 0, 0,/*9*/ +/*A*/ a.IMM, a.IND, a.IMM, a.IDX, a.ZPG, a.ZPG, a.ZPG, a.ZPX, a.IMP, a.IMM, a.IMP, 0, a.ABS, a.ABS, a.ABS, a.ABS,/*A*/ +/*B*/ a.REL, a.IDY, a.IMP, a.IDY, a.ZPX, a.ZPX, a.ZPY, a.ZPY, a.IMP, a.ABY, a.IMP, 0, a.ABX, a.ABX, a.ABY, a.ABY,/*B*/ +/*C*/ a.IMM, a.IDX, 0, 0, a.ZPG, a.ZPG, a.ZPG, 0, a.IMP, a.IMM, a.IMP, 0, a.ABS, a.ABS, a.ABS, 0,/*C*/ +/*D*/ a.REL, a.IDY, a.IMP, 0, 0, a.ZPX, a.ZPX, 0, a.IMP, a.ABY, 0, 0, a.ABS, a.ABX, a.ABX, 0,/*D*/ +/*E*/ a.IMM, a.IDX, 0, 0, a.ZPG, a.ZPG, a.ZPG, 0, a.IMP, a.IMM, a.IMP, 0, a.ABS, a.ABS, a.ABS, a.ABS,/*E*/ +/*F*/ a.REL, a.IDY, a.IMP, 0, 0, a.ZPX, a.ZPX, 0, a.IMP, a.ABY, 0, 0, a.ABS, a.ABX, a.ABX, a.ABX /*F*/ +}; + + public static string GetRegisters(M6502 cpu) + { + var dSB = new StringBuilder(); + dSB.Append(String.Format( + "PC:{0:x4} A:{1:x2} X:{2:x2} Y:{3:x2} S:{4:x2} P:", + cpu.PC, cpu.A, cpu.X, cpu.Y, cpu.S)); + + const string flags = "nv0bdizcNV1BDIZC"; + + for (var i = 0; i < 8; i++) + { + dSB.Append(((cpu.P & (1 << (7 - i))) == 0) ? flags[i] : flags[i + 8]); + } + return dSB.ToString(); + } + + public static string Disassemble(AddressSpace addrSpace, ushort atAddr, ushort untilAddr) + { + var dSB = new StringBuilder(); + var dPC = atAddr; + while (atAddr < untilAddr) + { + dSB.AppendFormat("{0:x4}: ", dPC); + var len = GetInstructionLength(addrSpace, dPC); + for (var i = 0; i < 3; i++) + { + if (i < len) + { + dSB.AppendFormat("{0:x2} ", addrSpace[atAddr++]); + } + else + { + dSB.Append(" "); + } + } + dSB.AppendFormat("{0,-15}{1}", RenderOpCode(addrSpace, dPC), Environment.NewLine); + dPC += (ushort)len; + } + if (dSB.Length > 0) + { + dSB.Length--; // Trim trailing newline + } + return dSB.ToString(); + } + + public static string MemDump(AddressSpace addrSpace, ushort atAddr, ushort untilAddr) + { + var dSB = new StringBuilder(); + var len = untilAddr - atAddr; + while (len-- >= 0) + { + dSB.AppendFormat("{0:x4}: ", atAddr); + for (var i = 0; i < 8; i++) + { + dSB.AppendFormat("{0:x2} ", addrSpace[atAddr++]); + if (i == 3) + { + dSB.Append(" "); + } + } + dSB.Append("\n"); + } + if (dSB.Length > 0) + { + dSB.Length--; // Trim trailing newline + } + return dSB.ToString(); + } + + public static string RenderOpCode(AddressSpace addrSpace, ushort PC) + { + var num_operands = GetInstructionLength(addrSpace, PC) - 1; + var PC1 = (ushort)(PC + 1); + string addrmodeStr; + + switch (AddressingModeMatrix[addrSpace[PC]]) + { + case a.REL: + addrmodeStr = String.Format("${0:x4}", (ushort)(PC + (sbyte)(addrSpace[PC1]) + 2)); + break; + case a.ZPG: + case a.ABS: + addrmodeStr = RenderEA(addrSpace, PC1, num_operands); + break; + case a.ZPX: + case a.ABX: + addrmodeStr = RenderEA(addrSpace, PC1, num_operands) + ",X"; + break; + case a.ZPY: + case a.ABY: + addrmodeStr = RenderEA(addrSpace, PC1, num_operands) + ",Y"; + break; + case a.IDX: + addrmodeStr = "(" + RenderEA(addrSpace, PC1, num_operands) + ",X)"; + break; + case a.IDY: + addrmodeStr = "(" + RenderEA(addrSpace, PC1, num_operands) + "),Y"; + break; + case a.IND: + addrmodeStr = "(" + RenderEA(addrSpace, PC1, num_operands) + ")"; + break; + case a.IMM: + addrmodeStr = "#" + RenderEA(addrSpace, PC1, num_operands); + break; + default: + // a.IMP, a.ACC + addrmodeStr = string.Empty; + break; + } + + return string.Format("{0} {1}", MnemonicMatrix[addrSpace[PC]], addrmodeStr); + } + + static int GetInstructionLength(AddressSpace addrSpace, ushort PC) + { + switch (AddressingModeMatrix[addrSpace[PC]]) + { + case a.ACC: + case a.IMP: + return 1; + case a.REL: + case a.ZPG: + case a.ZPX: + case a.ZPY: + case a.IDX: + case a.IDY: + case a.IMM: + return 2; + default: + return 3; + } + } + + static string RenderEA(AddressSpace addrSpace, ushort PC, int bytes) + { + var lsb = addrSpace[PC]; + var msb = (bytes == 2) ? addrSpace[(ushort)(PC + 1)] : (byte)0; + var ea = (ushort)(lsb | (msb << 8)); + return string.Format((bytes == 1) ? "${0:x2}" : "${0:x4}", ea); + } + } +} \ No newline at end of file diff --git a/EMU7800/Core/Machine2600.cs b/EMU7800/Core/Machine2600.cs new file mode 100644 index 0000000000..bb62a02692 --- /dev/null +++ b/EMU7800/Core/Machine2600.cs @@ -0,0 +1,112 @@ +/* + * Machine2600.cs + * + * The realization of a 2600 machine. + * + * Copyright © 2003, 2004 Mike Murphy + * + */ +namespace EMU7800.Core +{ + public class Machine2600 : MachineBase + { + #region Fields + + protected TIA TIA { get; set; } + + #endregion + + public override void Reset() + { + base.Reset(); + TIA.Reset(); + PIA.Reset(); + CPU.Reset(); + } + + public override void ComputeNextFrame(FrameBuffer frameBuffer) + { + base.ComputeNextFrame(frameBuffer); + TIA.StartFrame(); + CPU.RunClocks = (FrameBuffer.Scanlines + 3) * 76; + while (CPU.RunClocks > 0 && !CPU.Jammed) + { + if (TIA.WSYNCDelayClocks > 0) + { + CPU.Clock += (ulong)TIA.WSYNCDelayClocks / 3; + CPU.RunClocks -= TIA.WSYNCDelayClocks / 3; + TIA.WSYNCDelayClocks = 0; + } + if (TIA.EndOfFrame) + { + break; + } + CPU.Execute(); + } + TIA.EndFrame(); + } + + public Machine2600(Cart cart, ILogger logger, int slines, int startl, int fHZ, int sRate, int[] p) + : base(logger, slines, startl, fHZ, sRate, p, 160) + { + Mem = new AddressSpace(this, 13, 6); // 2600: 13bit, 64byte pages + + CPU = new M6502(this, 1); + + TIA = new TIA(this); + for (ushort i = 0; i < 0x1000; i += 0x100) + { + Mem.Map(i, 0x0080, TIA); + } + + PIA = new PIA(this); + for (ushort i = 0x0080; i < 0x1000; i += 0x100) + { + Mem.Map(i, 0x0080, PIA); + } + + Cart = cart; + Mem.Map(0x1000, 0x1000, Cart); + } + + #region Serialization Members + + public Machine2600(DeserializationContext input, int[] palette) : base(input, palette) + { + input.CheckVersion(1); + + Mem = input.ReadAddressSpace(this, 13, 6); // 2600: 13bit, 64byte pages + + CPU = input.ReadM6502(this, 1); + + TIA = input.ReadTIA(this); + for (ushort i = 0; i < 0x1000; i += 0x100) + { + Mem.Map(i, 0x0080, TIA); + } + + PIA = input.ReadPIA(this); + for (ushort i = 0x0080; i < 0x1000; i += 0x100) + { + Mem.Map(i, 0x0080, PIA); + } + + Cart = input.ReadCart(this); + Mem.Map(0x1000, 0x1000, Cart); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(Mem); + output.Write(CPU); + output.Write(TIA); + output.Write(PIA); + output.Write(Cart); + } + + #endregion + } +} diff --git a/EMU7800/Core/Machine2600NTSC.cs b/EMU7800/Core/Machine2600NTSC.cs new file mode 100644 index 0000000000..52c5a5b395 --- /dev/null +++ b/EMU7800/Core/Machine2600NTSC.cs @@ -0,0 +1,30 @@ +namespace EMU7800.Core +{ + public sealed class Machine2600NTSC : Machine2600 + { + public override string ToString() + { + return GetType().Name; + } + + public Machine2600NTSC(Cart cart, ILogger logger) + : base(cart, logger, 262, 16, 60, 31440 /* NTSC_SAMPLES_PER_SEC */, TIATables.NTSCPalette) + { + } + + #region Serialization Members + + public Machine2600NTSC(DeserializationContext input) : base(input, TIATables.NTSCPalette) + { + input.CheckVersion(1); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + output.WriteVersion(1); + } + + #endregion + } +} diff --git a/EMU7800/Core/Machine2600PAL.cs b/EMU7800/Core/Machine2600PAL.cs new file mode 100644 index 0000000000..ab32b28ba5 --- /dev/null +++ b/EMU7800/Core/Machine2600PAL.cs @@ -0,0 +1,30 @@ +namespace EMU7800.Core +{ + public sealed class Machine2600PAL : Machine2600 + { + public override string ToString() + { + return GetType().Name; + } + + public Machine2600PAL(Cart cart, ILogger logger) + : base(cart, logger, 312, 32, 50, 31200 /* PAL_SAMPLES_PER_SEC */, TIATables.PALPalette) + { + } + + #region Serialization Members + + public Machine2600PAL(DeserializationContext input) : base(input, TIATables.PALPalette) + { + input.CheckVersion(1); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + output.WriteVersion(1); + } + + #endregion + } +} diff --git a/EMU7800/Core/Machine7800.cs b/EMU7800/Core/Machine7800.cs new file mode 100644 index 0000000000..af20d4af33 --- /dev/null +++ b/EMU7800/Core/Machine7800.cs @@ -0,0 +1,244 @@ +/* + * Machine7800.cs + * + * The realization of a 7800 machine. + * + * Copyright © 2003-2005 Mike Murphy + * + */ +namespace EMU7800.Core +{ + public class Machine7800 : MachineBase + { + #region Fields + + protected Maria Maria { get; set; } + protected RAM6116 RAM1 { get; set; } + protected RAM6116 RAM2 { get; set; } + protected Bios7800 BIOS { get; private set; } + protected HSC7800 HSC { get; private set; } + + #endregion + + public void SwapInBIOS() + { + if (BIOS == null) + return; + Mem.Map((ushort)(0x10000 - BIOS.Size), BIOS.Size, BIOS); + } + + public void SwapOutBIOS() + { + if (BIOS == null) + return; + Mem.Map((ushort)(0x10000 - BIOS.Size), BIOS.Size, Cart); + } + + public override void Reset() + { + base.Reset(); + SwapInBIOS(); + if (HSC != null) + HSC.Reset(); + Cart.Reset(); + Maria.Reset(); + PIA.Reset(); + CPU.Reset(); + } + + public override void ComputeNextFrame(FrameBuffer frameBuffer) + { + base.ComputeNextFrame(frameBuffer); + + AssertDebug(CPU.Jammed || CPU.RunClocks <= 0 && (CPU.RunClocks % CPU.RunClocksMultiple) == 0); + AssertDebug(CPU.Jammed || ((CPU.Clock + (ulong)(CPU.RunClocks / CPU.RunClocksMultiple)) % (114 * (ulong)FrameBuffer.Scanlines)) == 0); + + ulong startOfScanlineCpuClock = 0; + + Maria.StartFrame(); + Cart.StartFrame(); + for (var i = 0; i < FrameBuffer.Scanlines && !CPU.Jammed; i++) + { + AssertDebug(CPU.RunClocks <= 0 && (CPU.RunClocks % CPU.RunClocksMultiple) == 0); + var newStartOfScanlineCpuClock = CPU.Clock + (ulong)(CPU.RunClocks / CPU.RunClocksMultiple); + + AssertDebug(startOfScanlineCpuClock == 0 || newStartOfScanlineCpuClock == startOfScanlineCpuClock + 114); + startOfScanlineCpuClock = newStartOfScanlineCpuClock; + + CPU.RunClocks += (7 * CPU.RunClocksMultiple); + var remainingRunClocks = (114 - 7) * CPU.RunClocksMultiple; + + CPU.Execute(); + if (CPU.Jammed) + break; + if (CPU.EmulatorPreemptRequest) + { + Maria.DoDMAProcessing(); + var remainingCpuClocks = 114 - (CPU.Clock - startOfScanlineCpuClock); + CPU.Clock += remainingCpuClocks; + CPU.RunClocks = 0; + continue; + } + + var dmaClocks = Maria.DoDMAProcessing(); + + // CHEAT: Ace of Aces: Title screen has a single scanline flicker without this. Maria DMA clock counting probably not 100% accurate. + if (i == 203 && FrameBuffer.Scanlines == 262 /*NTSC*/ || i == 228 && FrameBuffer.Scanlines == 312 /*PAL*/) + if (dmaClocks == 152 && remainingRunClocks == 428 && (CPU.RunClocks == -4 || CPU.RunClocks == -8)) + dmaClocks -= 4; + + // Unsure exactly what to do if Maria DMA processing extends past the current scanline. + // For now, throw away half remaining until we are within the current scanline. + // KLAX initialization starts DMA without initializing the DLL data structure. + // Maria processing then runs away causing an invalid CPU opcode to be executed that jams the machine. + // So Maria must give up at some point, but not clear exactly how. + // Anyway, this makes KLAX work without causing breakage elsewhere. + while ((CPU.RunClocks + remainingRunClocks) < dmaClocks) + { + dmaClocks >>= 1; + } + + // Assume the CPU waits until the next div4 boundary to proceed after DMA processing. + if ((dmaClocks & 3) != 0) + { + dmaClocks += 4; + dmaClocks -= (dmaClocks & 3); + } + + CPU.Clock += (ulong)(dmaClocks / CPU.RunClocksMultiple); + CPU.RunClocks -= dmaClocks; + + CPU.RunClocks += remainingRunClocks; + + CPU.Execute(); + if (CPU.Jammed) + break; + if (CPU.EmulatorPreemptRequest) + { + var remainingCpuClocks = 114 - (CPU.Clock - startOfScanlineCpuClock); + CPU.Clock += remainingCpuClocks; + CPU.RunClocks = 0; + } + } + Cart.EndFrame(); + Maria.EndFrame(); + } + + public Machine7800(Cart cart, Bios7800 bios, HSC7800 hsc, ILogger logger, int scanlines, int startl, int fHZ, int sRate, int[] p) + : base(logger, scanlines, startl, fHZ, sRate, p, 320) + { + Mem = new AddressSpace(this, 16, 6); // 7800: 16bit, 64byte pages + + CPU = new M6502(this, 4); + + Maria = new Maria(this, scanlines); + Mem.Map(0x0000, 0x0040, Maria); + Mem.Map(0x0100, 0x0040, Maria); + Mem.Map(0x0200, 0x0040, Maria); + Mem.Map(0x0300, 0x0040, Maria); + + PIA = new PIA(this); + Mem.Map(0x0280, 0x0080, PIA); + Mem.Map(0x0480, 0x0080, PIA); + Mem.Map(0x0580, 0x0080, PIA); + + RAM1 = new RAM6116(); + RAM2 = new RAM6116(); + Mem.Map(0x1800, 0x0800, RAM1); + Mem.Map(0x2000, 0x0800, RAM2); + + Mem.Map(0x0040, 0x00c0, RAM2); // page 0 shadow + Mem.Map(0x0140, 0x00c0, RAM2); // page 1 shadow + Mem.Map(0x2800, 0x0800, RAM2); // shadow1 + Mem.Map(0x3000, 0x0800, RAM2); // shadow2 + Mem.Map(0x3800, 0x0800, RAM2); // shadow3 + + BIOS = bios; + HSC = hsc; + + if (HSC != null) + { + Mem.Map(0x1000, 0x800, HSC.SRAM); + Mem.Map(0x3000, 0x1000, HSC); + Logger.WriteLine("7800 Highscore Cartridge Installed"); + } + + Cart = cart; + Mem.Map(0x4000, 0xc000, Cart); + } + + #region Serialization Members + + public Machine7800(DeserializationContext input, int[] palette, int scanlines) : base(input, palette) + { + input.CheckVersion(1); + + Mem = input.ReadAddressSpace(this, 16, 6); // 7800: 16bit, 64byte pages + + CPU = input.ReadM6502(this, 4); + + Maria = input.ReadMaria(this, scanlines); + Mem.Map(0x0000, 0x0040, Maria); + Mem.Map(0x0100, 0x0040, Maria); + Mem.Map(0x0200, 0x0040, Maria); + Mem.Map(0x0300, 0x0040, Maria); + + PIA = input.ReadPIA(this); + Mem.Map(0x0280, 0x0080, PIA); + Mem.Map(0x0480, 0x0080, PIA); + Mem.Map(0x0580, 0x0080, PIA); + + RAM1 = input.ReadRAM6116(); + RAM2 = input.ReadRAM6116(); + Mem.Map(0x1800, 0x0800, RAM1); + Mem.Map(0x2000, 0x0800, RAM2); + + Mem.Map(0x0040, 0x00c0, RAM2); // page 0 shadow + Mem.Map(0x0140, 0x00c0, RAM2); // page 1 shadow + Mem.Map(0x2800, 0x0800, RAM2); // shadow1 + Mem.Map(0x3000, 0x0800, RAM2); // shadow2 + Mem.Map(0x3800, 0x0800, RAM2); // shadow3 + + BIOS = input.ReadOptionalBios7800(); + HSC = input.ReadOptionalHSC7800(); + + if (HSC != null) + { + Mem.Map(0x1000, 0x800, HSC.SRAM); + Mem.Map(0x3000, 0x1000, HSC); + } + + Cart = input.ReadCart(this); + Mem.Map(0x4000, 0xc000, Cart); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + + output.WriteVersion(1); + output.Write(Mem); + output.Write(CPU); + output.Write(Maria); + output.Write(PIA); + output.Write(RAM1); + output.Write(RAM2); + output.WriteOptional(BIOS); + output.WriteOptional(HSC); + output.Write(Cart); + } + + #endregion + + #region Helpers + + [System.Diagnostics.Conditional("DEBUG")] + void AssertDebug(bool cond) + { + if (!cond) + System.Diagnostics.Debugger.Break(); + } + + #endregion + } +} diff --git a/EMU7800/Core/Machine7800NTSC.cs b/EMU7800/Core/Machine7800NTSC.cs new file mode 100644 index 0000000000..933336740f --- /dev/null +++ b/EMU7800/Core/Machine7800NTSC.cs @@ -0,0 +1,30 @@ +namespace EMU7800.Core +{ + public sealed class Machine7800NTSC : Machine7800 + { + public override string ToString() + { + return GetType().Name; + } + + public Machine7800NTSC(Cart cart, Bios7800 bios, HSC7800 hsc, ILogger logger) + : base(cart, bios, hsc, logger, 262, 16, 60, 31440 /* NTSC_SAMPLES_PER_SEC */, MariaTables.NTSCPalette) + { + } + + #region Serialization Members + + public Machine7800NTSC(DeserializationContext input) : base(input, MariaTables.NTSCPalette, 262) + { + input.CheckVersion(1); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + output.WriteVersion(1); + } + + #endregion + } +} diff --git a/EMU7800/Core/Machine7800PAL.cs b/EMU7800/Core/Machine7800PAL.cs new file mode 100644 index 0000000000..4de452f892 --- /dev/null +++ b/EMU7800/Core/Machine7800PAL.cs @@ -0,0 +1,30 @@ +namespace EMU7800.Core +{ + public sealed class Machine7800PAL : Machine7800 + { + public override string ToString() + { + return GetType().Name; + } + + public Machine7800PAL(Cart cart, Bios7800 bios, HSC7800 hsc, ILogger logger) + : base(cart, bios, hsc, logger, 312, 34, 50, 31200 /* PAL_SAMPLES_PER_SEC */, MariaTables.PALPalette) + { + } + + #region Serialization Members + + public Machine7800PAL(DeserializationContext input) : base(input, MariaTables.PALPalette, 312) + { + input.CheckVersion(1); + } + + public override void GetObjectData(SerializationContext output) + { + base.GetObjectData(output); + output.WriteVersion(1); + } + + #endregion + } +} diff --git a/EMU7800/Core/MachineBase.cs b/EMU7800/Core/MachineBase.cs new file mode 100644 index 0000000000..41abf497be --- /dev/null +++ b/EMU7800/Core/MachineBase.cs @@ -0,0 +1,331 @@ +/* +/* + * MachineBase.cs + * + * Abstraction of an emulated machine. + * + * Copyright © 2003, 2004 Mike Murphy + * + */ +using System; +using System.IO; +using System.Reflection; + +namespace EMU7800.Core +{ + public abstract class MachineBase + { + #region Fields + + ILogger _Logger; + FrameBuffer _FrameBuffer; + + bool _MachineHalt; + int _FrameHZ; + readonly int _VisiblePitch, _Scanlines; + + protected Cart Cart { get; set; } + + #endregion + + #region Internal Properties + + internal FrameBuffer FrameBuffer + { + get + { + AssertDebug(_FrameBuffer != null); + return _FrameBuffer; + } + } + + #endregion + + #region Public Properties + + /// + /// The machine's Central Processing Unit. + /// + public M6502 CPU { get; protected set; } + + /// + /// The machine's Address Space. + /// + public AddressSpace Mem { get; protected set; } + + /// + /// The machine's Peripheral Interface Adaptor device. + /// + public PIA PIA { get; protected set; } + + /// + /// Reports whether the machine has been halted due to an internal condition or error. + /// + public bool MachineHalt + { + get { return _MachineHalt; } + internal set { if (value) _MachineHalt = true; } + } + + /// + /// The machine input state. + /// + public InputState InputState { get; private set; } + + /// + /// The current frame number. + /// + public long FrameNumber { get; private set; } + + /// + /// The first scanline that is visible. + /// + public int FirstScanline { get; private set; } + + /// + /// Frame rate. + /// + public int FrameHZ + { + get { return _FrameHZ < 1 ? 1 : _FrameHZ; } + set { _FrameHZ = value < 1 ? 1 : value; } + } + + /// + /// Number of sound samples per second. + /// + public int SoundSampleFrequency { get; private set; } + + /// + /// The color palette for the configured machine. + /// + public int[] Palette { get; internal set; } + + /// + /// Dumps CPU registers to the log when NOP instructions are encountered. + /// + public bool NOPRegisterDumping { get; set; } + + /// + /// The configured logger sink. + /// + public ILogger Logger + { + get { return _Logger ?? (_Logger = new NullLogger()); } + set { _Logger = value; } + } + + #endregion + + #region Public Methods + + /// + /// Creates an instance of the specified machine. + /// + /// + /// + /// 7800 BIOS, optional. + /// 7800 High Score cart, optional. + /// Left controller, optional. + /// Right controller, optional. + /// + /// Cart must not be null. + /// Specified MachineType is unexpected. + public static MachineBase Create(MachineType machineType, Cart cart, Bios7800 bios, HSC7800 hsc, Controller p1, Controller p2, ILogger logger) + { + if (cart == null) + throw new ArgumentNullException("cart"); + + MachineBase m; + switch (machineType) + { + case MachineType.A2600NTSC: + m = new Machine2600NTSC(cart, logger); + break; + case MachineType.A2600PAL: + m = new Machine2600PAL(cart, logger); + break; + case MachineType.A7800NTSC: + m = new Machine7800NTSC(cart, bios, hsc, logger); + break; + case MachineType.A7800PAL: + m = new Machine7800PAL(cart, bios, hsc, logger); + break; + default: + throw new Emu7800Exception("Unexpected MachineType: " + machineType); + } + + m.InputState.LeftControllerJack = p1; + m.InputState.RightControllerJack = p2; + + m.Reset(); + + return m; + } + + /// + /// Deserialize a from the specified stream. + /// + /// + /// + /// + public static MachineBase Deserialize(BinaryReader binaryReader) + { + var context = new DeserializationContext(binaryReader); + MachineBase m; + try + { + m = context.ReadMachine(); + } + catch (Emu7800SerializationException) + { + throw; + } + catch (TargetInvocationException ex) + { + // TargetInvocationException wraps exceptions that unwind an Activator.CreateInstance() frame. + throw new Emu7800SerializationException("Serialization stream does not describe a valid machine.", ex.InnerException); + } + catch (Exception ex) + { + throw new Emu7800SerializationException("Serialization stream does not describe a valid machine.", ex); + } + return m; + } + + /// + /// Resets the state of the machine. + /// + public virtual void Reset() + { + Logger.WriteLine("Machine {0} reset ({1} HZ {2} scanlines)", this, FrameHZ, _Scanlines); + FrameNumber = 0; + _MachineHalt = false; + InputState.ClearAllInput(); + } + + /// + /// Computes the next machine frame, updating contents of the provided . + /// + /// The framebuffer to contain the computed output. + /// + /// frameBuffer is incompatible with machine. + public virtual void ComputeNextFrame(FrameBuffer frameBuffer) + { + if (MachineHalt) + return; + + InputState.CaptureInputState(); + + _FrameBuffer = frameBuffer; + FrameNumber++; + + for (var i = 0; i < _FrameBuffer.SoundBufferElementLength; i++) + _FrameBuffer.SoundBuffer[i].ClearAll(); + } + + /// + /// Create a with compatible dimensions for this machine. + /// + public FrameBuffer CreateFrameBuffer() + { + var fb = new FrameBuffer(_VisiblePitch, _Scanlines); + return fb; + } + + /// + /// Serialize the state of the machine to the specified stream. + /// + /// + /// + /// + public void Serialize(BinaryWriter binaryWriter) + { + var context = new SerializationContext(binaryWriter); + try + { + context.Write(this); + } + catch (Emu7800SerializationException) + { + throw; + } + catch (Exception ex) + { + throw new Emu7800SerializationException("Problem serializing specified machine.", ex); + } + } + + #endregion + + #region Constructors + + private MachineBase() + { + } + + protected MachineBase(ILogger logger, int scanLines, int firstScanline, int fHZ, int soundSampleFreq, int[] palette, int vPitch) : this() + { + InputState = new InputState(); + Logger = logger; + _Scanlines = scanLines; + FirstScanline = firstScanline; + FrameHZ = fHZ; + SoundSampleFrequency = soundSampleFreq; + Palette = palette; + _VisiblePitch = vPitch; + } + + #endregion + + #region Serialization Members + + protected MachineBase(DeserializationContext input, int[] palette) + { + if (input == null) + throw new ArgumentNullException("input"); + if (palette == null) + throw new ArgumentNullException("palette"); + if (palette.Length != 0x100) + throw new ArgumentException("palette incorrect size, must be 256."); + + input.CheckVersion(1); + _MachineHalt = input.ReadBoolean(); + _FrameHZ = input.ReadInt32(); + _VisiblePitch = input.ReadInt32(); + _Scanlines = input.ReadInt32(); + FirstScanline = input.ReadInt32(); + SoundSampleFrequency = input.ReadInt32(); + NOPRegisterDumping = input.ReadBoolean(); + InputState = input.ReadInputState(); + + Palette = palette; + Logger = null; + } + + public virtual void GetObjectData(SerializationContext output) + { + if (output == null) + throw new ArgumentNullException("output"); + + output.WriteVersion(1); + output.Write(_MachineHalt); + output.Write(_FrameHZ); + output.Write(_VisiblePitch); + output.Write(_Scanlines); + output.Write(FirstScanline); + output.Write(SoundSampleFrequency); + output.Write(NOPRegisterDumping); + output.Write(InputState); + } + + #endregion + + [System.Diagnostics.Conditional("DEBUG")] + void AssertDebug(bool cond) + { + if (!cond) + System.Diagnostics.Debugger.Break(); + } + } +} diff --git a/EMU7800/Core/MachineInput.cs b/EMU7800/Core/MachineInput.cs new file mode 100644 index 0000000000..05fe19c556 --- /dev/null +++ b/EMU7800/Core/MachineInput.cs @@ -0,0 +1,42 @@ +/* + * HostInput.cs + * + * Copyright © 2009 Mike Murphy + * + */ +namespace EMU7800.Core +{ + public enum MachineInput + { + End, + Pause, + Mute, + Fire, + Fire2, + Left, + Right, + Up, + Down, + NumPad1, NumPad2, NumPad3, + NumPad4, NumPad5, NumPad6, + NumPad7, NumPad8, NumPad9, + NumPadMult, NumPad0, NumPadHash, + Driving0, Driving1, Driving2, Driving3, + Reset, + Select, + Color, + LeftDifficulty, + RightDifficulty, + SetKeyboardToPlayer1, + SetKeyboardToPlayer2, + SetKeyboardToPlayer3, + SetKeyboardToPlayer4, + PanLeft, PanRight, PanUp, PanDown, + SaveMachine, + TakeScreenshot, + LeftPaddleSwap, + GameControllerSwap, + RightPaddleSwap, + ShowFrameStats, + } +} diff --git a/EMU7800/Core/MachineType.cs b/EMU7800/Core/MachineType.cs new file mode 100644 index 0000000000..0fcfdd2d52 --- /dev/null +++ b/EMU7800/Core/MachineType.cs @@ -0,0 +1,19 @@ +/* + * MachineType.cs + * + * The set of known machines. + * + * Copyright © 2010 Mike Murphy + * + */ +namespace EMU7800.Core +{ + public enum MachineType + { + None, + A2600NTSC, + A2600PAL, + A7800NTSC, + A7800PAL + }; +} diff --git a/EMU7800/Core/Maria.cs b/EMU7800/Core/Maria.cs new file mode 100644 index 0000000000..41685d7714 --- /dev/null +++ b/EMU7800/Core/Maria.cs @@ -0,0 +1,1156 @@ +/* + * 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 bufferElement = new BufferElement(); + var fbi = ((Scanline + 1) * M.FrameBuffer.VideoBufferElementVisiblePitch) % M.FrameBuffer.VideoBufferElementLength; + + for (int i = 0, s = 0; i < M.FrameBuffer.VideoBufferElementVisiblePitch; i++) + { + for (var j = 0; j < BufferElement.SIZE; j++, s++) + { + var colorIndex = LineRAM[s]; + bufferElement[j] = Registers[BACKGRND + ((colorIndex & 3) == 0 ? 0 : colorIndex)]; + } + M.FrameBuffer.VideoBuffer[fbi] = bufferElement; + if (++fbi == M.FrameBuffer.VideoBufferElementLength) + 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 + } +} diff --git a/EMU7800/Core/MariaTables.cs b/EMU7800/Core/MariaTables.cs new file mode 100644 index 0000000000..aa8745ee64 --- /dev/null +++ b/EMU7800/Core/MariaTables.cs @@ -0,0 +1,180 @@ +/* + * MariaTables.cs + * + * Palette tables for the Maria class. + * All derived from Dan Boris' 7800/MAME code. + * + * Copyright © 2004 Mike Murphy + * + */ +namespace EMU7800.Core +{ + public static class MariaTables + { + public static readonly int[] NTSCPalette = + { + 0x000000, 0x1c1c1c, 0x393939, 0x595959, // Grey + 0x797979, 0x929292, 0xababab, 0xbcbcbc, + 0xcdcdcd, 0xd9d9d9, 0xe6e6e6, 0xececec, + 0xf2f2f2, 0xf8f8f8, 0xffffff, 0xffffff, + + 0x391701, 0x5e2304, 0x833008, 0xa54716, // Gold + 0xc85f24, 0xe37820, 0xff911d, 0xffab1d, + 0xffc51d, 0xffce34, 0xffd84c, 0xffe651, + 0xfff456, 0xfff977, 0xffff98, 0xffff98, + + 0x451904, 0x721e11, 0x9f241e, 0xb33a20, // Orange + 0xc85122, 0xe36920, 0xff811e, 0xff8c25, + 0xff982c, 0xffae38, 0xffc545, 0xffc559, + 0xffc66d, 0xffd587, 0xffe4a1, 0xffe4a1, + + 0x4a1704, 0x7e1a0d, 0xb21d17, 0xc82119, // Red Orange + 0xdf251c, 0xec3b38, 0xfa5255, 0xfc6161, + 0xff706e, 0xff7f7e, 0xff8f8f, 0xff9d9e, + 0xffabad, 0xffb9bd, 0xffc7ce, 0xffc7ce, + + 0x050568, 0x3b136d, 0x712272, 0x8b2a8c, // Pink + 0xa532a6, 0xb938ba, 0xcd3ecf, 0xdb47dd, + 0xea51eb, 0xf45ff5, 0xfe6dff, 0xfe7afd, + 0xff87fb, 0xff95fd, 0xffa4ff, 0xffa4ff, + + 0x280479, 0x400984, 0x590f90, 0x70249d, // Purple + 0x8839aa, 0xa441c3, 0xc04adc, 0xd054ed, + 0xe05eff, 0xe96dff, 0xf27cff, 0xf88aff, + 0xff98ff, 0xfea1ff, 0xfeabff, 0xfeabff, + + 0x35088a, 0x420aad, 0x500cd0, 0x6428d0, // Purple Blue + 0x7945d0, 0x8d4bd4, 0xa251d9, 0xb058ec, + 0xbe60ff, 0xc56bff, 0xcc77ff, 0xd183ff, + 0xd790ff, 0xdb9dff, 0xdfaaff, 0xdfaaff, + + 0x051e81, 0x0626a5, 0x082fca, 0x263dd4, // Blue1 + 0x444cde, 0x4f5aee, 0x5a68ff, 0x6575ff, + 0x7183ff, 0x8091ff, 0x90a0ff, 0x97a9ff, + 0x9fb2ff, 0xafbeff, 0xc0cbff, 0xc0cbff, + + 0x0c048b, 0x2218a0, 0x382db5, 0x483ec7, // Blue2 + 0x584fda, 0x6159ec, 0x6b64ff, 0x7a74ff, + 0x8a84ff, 0x918eff, 0x9998ff, 0xa5a3ff, + 0xb1aeff, 0xb8b8ff, 0xc0c2ff, 0xc0c2ff, + + 0x1d295a, 0x1d3876, 0x1d4892, 0x1c5cac, // Light Blue + 0x1c71c6, 0x3286cf, 0x489bd9, 0x4ea8ec, + 0x55b6ff, 0x70c7ff, 0x8cd8ff, 0x93dbff, + 0x9bdfff, 0xafe4ff, 0xc3e9ff, 0xc3e9ff, + + 0x2f4302, 0x395202, 0x446103, 0x417a12, // Turquoise + 0x3e9421, 0x4a9f2e, 0x57ab3b, 0x5cbd55, + 0x61d070, 0x69e27a, 0x72f584, 0x7cfa8d, + 0x87ff97, 0x9affa6, 0xadffb6, 0xadffb6, + + 0x0a4108, 0x0d540a, 0x10680d, 0x137d0f, // Green Blue + 0x169212, 0x19a514, 0x1cb917, 0x1ec919, + 0x21d91b, 0x47e42d, 0x6ef040, 0x78f74d, + 0x83ff5b, 0x9aff7a, 0xb2ff9a, 0xb2ff9a, + + 0x04410b, 0x05530e, 0x066611, 0x077714, // Green + 0x088817, 0x099b1a, 0x0baf1d, 0x48c41f, + 0x86d922, 0x8fe924, 0x99f927, 0xa8fc41, + 0xb7ff5b, 0xc9ff6e, 0xdcff81, 0xdcff81, + + 0x02350f, 0x073f15, 0x0c4a1c, 0x2d5f1e, // Yellow Green + 0x4f7420, 0x598324, 0x649228, 0x82a12e, + 0xa1b034, 0xa9c13a, 0xb2d241, 0xc4d945, + 0xd6e149, 0xe4f04e, 0xf2ff53, 0xf2ff53, + + 0x263001, 0x243803, 0x234005, 0x51541b, // Orange Green + 0x806931, 0x978135, 0xaf993a, 0xc2a73e, + 0xd5b543, 0xdbc03d, 0xe1cb38, 0xe2d836, + 0xe3e534, 0xeff258, 0xfbff7d, 0xfbff7d, + + 0x401a02, 0x581f05, 0x702408, 0x8d3a13, // Light Orange + 0xab511f, 0xb56427, 0xbf7730, 0xd0853a, + 0xe19344, 0xeda04e, 0xf9ad58, 0xfcb75c, + 0xffc160, 0xffc671, 0xffcb83, 0xffcb83 + }; + + public static readonly int[] PALPalette = + { + 0x000000, 0x1c1c1c, 0x393939, 0x595959, // Grey + 0x797979, 0x929292, 0xababab, 0xbcbcbc, + 0xcdcdcd, 0xd9d9d9, 0xe6e6e6, 0xececec, + 0xf2f2f2, 0xf8f8f8, 0xffffff, 0xffffff, + + 0x263001, 0x243803, 0x234005, 0x51541b, // Orange Green + 0x806931, 0x978135, 0xaf993a, 0xc2a73e, + 0xd5b543, 0xdbc03d, 0xe1cb38, 0xe2d836, + 0xe3e534, 0xeff258, 0xfbff7d, 0xfbff7d, + + 0x263001, 0x243803, 0x234005, 0x51541b, // Orange Green + 0x806931, 0x978135, 0xaf993a, 0xc2a73e, + 0xd5b543, 0xdbc03d, 0xe1cb38, 0xe2d836, + 0xe3e534, 0xeff258, 0xfbff7d, 0xfbff7d, + + 0x401a02, 0x581f05, 0x702408, 0x8d3a13, // Light Orange + 0xab511f, 0xb56427, 0xbf7730, 0xd0853a, + 0xe19344, 0xeda04e, 0xf9ad58, 0xfcb75c, + 0xffc160, 0xffc671, 0xffcb83, 0xffcb83, + + 0x391701, 0x5e2304, 0x833008, 0xa54716, // Gold + 0xc85f24, 0xe37820, 0xff911d, 0xffab1d, + 0xffc51d, 0xffce34, 0xffd84c, 0xffe651, + 0xfff456, 0xfff977, 0xffff98, 0xffff98, + + 0x451904, 0x721e11, 0x9f241e, 0xb33a20, // Orange + 0xc85122, 0xe36920, 0xff811e, 0xff8c25, + 0xff982c, 0xffae38, 0xffc545, 0xffc559, + 0xffc66d, 0xffd587, 0xffe4a1, 0xffe4a1, + + 0x4a1704, 0x7e1a0d, 0xb21d17, 0xc82119, // Red Orange + 0xdf251c, 0xec3b38, 0xfa5255, 0xfc6161, + 0xff706e, 0xff7f7e, 0xff8f8f, 0xff9d9e, + 0xffabad, 0xffb9bd, 0xffc7ce, 0xffc7ce, + + 0x050568, 0x3b136d, 0x712272, 0x8b2a8c, // Pink + 0xa532a6, 0xb938ba, 0xcd3ecf, 0xdb47dd, + 0xea51eb, 0xf45ff5, 0xfe6dff, 0xfe7afd, + 0xff87fb, 0xff95fd, 0xffa4ff, 0xffa4ff, + + 0x280479, 0x400984, 0x590f90, 0x70249d, // Purple + 0x8839aa, 0xa441c3, 0xc04adc, 0xd054ed, + 0xe05eff, 0xe96dff, 0xf27cff, 0xf88aff, + 0xff98ff, 0xfea1ff, 0xfeabff, 0xfeabff, + + 0x051e81, 0x0626a5, 0x082fca, 0x263dd4, // Blue1 + 0x444cde, 0x4f5aee, 0x5a68ff, 0x6575ff, + 0x7183ff, 0x8091ff, 0x90a0ff, 0x97a9ff, + 0x9fb2ff, 0xafbeff, 0xc0cbff, 0xc0cbff, + + 0x0c048b, 0x2218a0, 0x382db5, 0x483ec7, // Blue2 + 0x584fda, 0x6159ec, 0x6b64ff, 0x7a74ff, + 0x8a84ff, 0x918eff, 0x9998ff, 0xa5a3ff, + 0xb1aeff, 0xb8b8ff, 0xc0c2ff, 0xc0c2ff, + + 0x1d295a, 0x1d3876, 0x1d4892, 0x1c5cac, // Light Blue + 0x1c71c6, 0x3286cf, 0x489bd9, 0x4ea8ec, + 0x55b6ff, 0x70c7ff, 0x8cd8ff, 0x93dbff, + 0x9bdfff, 0xafe4ff, 0xc3e9ff, 0xc3e9ff, + + 0x2f4302, 0x395202, 0x446103, 0x417a12, // Turquoise + 0x3e9421, 0x4a9f2e, 0x57ab3b, 0x5cbd55, + 0x61d070, 0x69e27a, 0x72f584, 0x7cfa8d, + 0x87ff97, 0x9affa6, 0xadffb6, 0xadffb6, + + 0x0a4108, 0x0d540a, 0x10680d, 0x137d0f, // Green Blue + 0x169212, 0x19a514, 0x1cb917, 0x1ec919, + 0x21d91b, 0x47e42d, 0x6ef040, 0x78f74d, + 0x83ff5b, 0x9aff7a, 0xb2ff9a, 0xb2ff9a, + + 0x04410b, 0x05530e, 0x066611, 0x077714, // Green + 0x088817, 0x099b1a, 0x0baf1d, 0x48c41f, + 0x86d922, 0x8fe924, 0x99f927, 0xa8fc41, + 0xb7ff5b, 0xc9ff6e, 0xdcff81, 0xdcff81, + + 0x02350f, 0x073f15, 0x0c4a1c, 0x2d5f1e, // Yellow Green + 0x4f7420, 0x598324, 0x649228, 0x82a12e, + 0xa1b034, 0xa9c13a, 0xb2d241, 0xc4d945, + 0xd6e149, 0xe4f04e, 0xf2ff53, 0xf2ff53 + }; + } +} diff --git a/EMU7800/Core/NullDevice.cs b/EMU7800/Core/NullDevice.cs new file mode 100644 index 0000000000..d0c691543d --- /dev/null +++ b/EMU7800/Core/NullDevice.cs @@ -0,0 +1,78 @@ +/* + * NullDevice.cs + * + * Default memory mappable device. + * + * Copyright © 2003, 2004 Mike Murphy + * + */ +using System; + +namespace EMU7800.Core +{ + public sealed class NullDevice : IDevice + { + MachineBase M { get; set; } + + #region IDevice Members + + public void Reset() + { + Log("{0} reset", this); + } + + public byte this[ushort addr] + { + get + { + LogDebug("NullDevice: Peek at ${0:x4}, PC=${1:x4}", addr, M.CPU.PC); + return 0; + } + set + { + LogDebug("NullDevice: Poke at ${0:x4},${1:x2}, PC=${2:x4}", addr, value, M.CPU.PC); + } + } + + #endregion + + public override String ToString() + { + return "NullDevice"; + } + + #region Constructors + + private NullDevice() + { + } + + public NullDevice(MachineBase m) + { + if (m == null) + throw new ArgumentNullException("m"); + M = m; + } + + #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 + } +} \ No newline at end of file diff --git a/EMU7800/Core/NullLogger.cs b/EMU7800/Core/NullLogger.cs new file mode 100644 index 0000000000..1604d3b883 --- /dev/null +++ b/EMU7800/Core/NullLogger.cs @@ -0,0 +1,21 @@ +namespace EMU7800.Core +{ + public class NullLogger : ILogger + { + public void WriteLine(string format, params object[] args) + { + } + + public void WriteLine(object value) + { + } + + public void Write(string format, params object[] args) + { + } + + public void Write(object value) + { + } + } +} diff --git a/EMU7800/Core/PIA.cs b/EMU7800/Core/PIA.cs new file mode 100644 index 0000000000..1da65519db --- /dev/null +++ b/EMU7800/Core/PIA.cs @@ -0,0 +1,348 @@ +/* + * PIA.cs + * + * The Peripheral Interface Adapter (6532) device. + * a.k.a. RIOT (RAM I/O Timer?) + * + * Copyright © 2003, 2004, 2012 Mike Murphy + * + */ +using System; + +namespace EMU7800.Core +{ + public sealed class PIA : IDevice + { + readonly MachineBase M; + + readonly byte[] RAM = new byte[0x80]; + + ulong TimerTarget; + int TimerShift; + bool IRQEnabled, IRQTriggered; + + public byte DDRA { get; private set; } + public byte DDRB { get; private set; } + + public byte WrittenPortA { get; private set; } + public byte WrittenPortB { get; private set; } + + #region IDevice Members + + public void Reset() + { + // Some games will loop/hang on $0284 if these are initialized to zero + TimerShift = 10; + TimerTarget = M.CPU.Clock + (ulong)(0xff << TimerShift); + + IRQEnabled = false; + IRQTriggered = false; + + DDRA = 0; + + Log("{0} reset", this); + } + + public byte this[ushort addr] + { + get { return peek(addr); } + set { poke(addr, value); } + } + + #endregion + + public override string ToString() + { + return "PIA/RIOT M6532"; + } + + #region Constructors + + private PIA() + { + } + + public PIA(MachineBase m) + { + if (m == null) + throw new ArgumentNullException("m"); + M = m; + } + + #endregion + + byte peek(ushort addr) + { + if ((addr & 0x200) == 0) + { + return RAM[addr & 0x7f]; + } + + switch ((byte)(addr & 7)) + { + case 0: // SWCHA: Controllers + return ReadPortA(); + case 1: // SWCHA DDR: 0=input, 1=output + return DDRA; + case 2: // SWCHB: Console switches (on 7800, PB2 & PB4 are used) + return ReadPortB(); + case 3: // SWCHB DDR: 0=input, 1=output + return 0; + case 4: // INTIM + case 6: + return ReadTimerRegister(); + case 5: // INTFLG + case 7: + return ReadInterruptFlag(); + default: + LogDebug("PIA: Unhandled peek ${0:x4}, PC=${1:x4}", addr, M.CPU.PC); + return 0; + } + } + + void poke(ushort addr, byte data) + { + if ((addr & 0x200) == 0) + { + RAM[addr & 0x7f] = data; + return; + } + + // A2 Distinguishes I/O registers from the Timer + if ((addr & 0x04) != 0) + { + if ((addr & 0x10) != 0) + { + IRQEnabled = (addr & 0x08) != 0; + SetTimerRegister(data, addr & 3); + } + else + { + LogDebug("PIA: Timer: Unhandled poke ${0:x4} w/${1:x2}, PC=${2:x4}", addr, data, M.CPU.PC); + } + } + else + { + switch ((byte)(addr & 3)) + { + case 0: // SWCHA: Port A + WritePortA(data); + break; + case 1: // SWACNT: Port A DDR + DDRA = data; + break; + case 2: // SWCHB: Port B + WritePortB(data); + break; + case 3: // SWBCNT: Port B DDR + DDRB = data; + break; + } + } + } + + // 0: TIM1T: set 1 clock interval ( 838 nsec/interval) + // 1: TIM8T: set 8 clock interval ( 6.7 usec/interval) + // 2: TIM64T: set 64 clock interval ( 53.6 usec/interval) + // 3: T1024T: set 1024 clock interval (858.2 usec/interval) + void SetTimerRegister(byte data, int interval) + { + IRQTriggered = false; + TimerShift = new[] { 0, 3, 6, 10 }[interval]; + TimerTarget = M.CPU.Clock + (ulong)(data << TimerShift); + } + + byte ReadTimerRegister() + { + IRQTriggered = false; + var delta = (int)(TimerTarget - M.CPU.Clock); + if (delta >= 0) + { + return (byte)(delta >> TimerShift); + } + if (delta != -1) + { + IRQTriggered = true; + } + return (byte)(delta >= -256 ? delta : 0); + } + + byte ReadInterruptFlag() + { + var delta = (int)(TimerTarget - M.CPU.Clock); + return (byte)((delta >= 0 || IRQEnabled && IRQTriggered) ? 0x00 : 0x80); + } + + // PortA: Controller Jacks + // + // Left Jack Right Jack + // ------------- ------------- + // \ 1 2 3 4 5 / \ 1 2 3 4 5 / + // \ 6 7 8 9 / \ 6 7 8 9 / + // --------- --------- + // + // pin 1 D4 PIA SWCHA D0 PIA SWCHA + // pin 2 D5 PIA SWCHA D1 PIA SWCHA + // pin 3 D6 PIA SWCHA D2 PIA SWCHA + // pin 4 D7 PIA SWCHA D3 PIA SWCHA + // pin 5 D7 TIA INPT1 (Dumped) D7 TIA INPT3 (Dumped) 7800: Right Fire + // pin 6 D7 TIA INPT4 (Latched) D7 TIA INPT5 (Latched) 2600: Fire + // pin 7 +5 +5 + // pin 8 GND GND + // pin 9 D7 TIA INPT0 (Dumped) D7 TIA INPT2 (Dumped) 7800: Left Fire + // + byte ReadPortA() + { + var porta = 0; + var mi = M.InputState; + + switch (mi.LeftControllerJack) + { + case Controller.Joystick: + case Controller.ProLineJoystick: + case Controller.BoosterGrip: + porta |= mi.SampleCapturedControllerActionState(0, ControllerAction.Up) ? 0 : (1 << 4); + porta |= mi.SampleCapturedControllerActionState(0, ControllerAction.Down) ? 0 : (1 << 5); + porta |= mi.SampleCapturedControllerActionState(0, ControllerAction.Left) ? 0 : (1 << 6); + porta |= mi.SampleCapturedControllerActionState(0, ControllerAction.Right) ? 0 : (1 << 7); + break; + case Controller.Driving: + porta |= mi.SampleCapturedDrivingState(0) << 4; + break; + case Controller.Paddles: + porta |= mi.SampleCapturedControllerActionState(0, ControllerAction.Trigger) ? 0 : (1 << 7); + porta |= mi.SampleCapturedControllerActionState(1, ControllerAction.Trigger) ? 0 : (1 << 6); + break; + case Controller.Lightgun: + porta |= mi.SampleCapturedControllerActionState(0, ControllerAction.Trigger) ? (1 << 4) : 0; + break; + } + + switch (mi.RightControllerJack) + { + case Controller.Joystick: + case Controller.ProLineJoystick: + case Controller.BoosterGrip: + porta |= mi.SampleCapturedControllerActionState(1, ControllerAction.Up) ? 0 : (1 << 0); + porta |= mi.SampleCapturedControllerActionState(1, ControllerAction.Down) ? 0 : (1 << 1); + porta |= mi.SampleCapturedControllerActionState(1, ControllerAction.Left) ? 0 : (1 << 2); + porta |= mi.SampleCapturedControllerActionState(1, ControllerAction.Right) ? 0 : (1 << 3); + break; + case Controller.Driving: + porta |= mi.SampleCapturedDrivingState(1); + break; + case Controller.Paddles: + porta |= mi.SampleCapturedControllerActionState(2, ControllerAction.Trigger) ? 0 : (1 << 3); + porta |= mi.SampleCapturedControllerActionState(3, ControllerAction.Trigger) ? 0 : (1 << 2); + break; + case Controller.Lightgun: + porta |= mi.SampleCapturedControllerActionState(1, ControllerAction.Trigger) ? (1 << 0) : 0; + break; + } + + return (byte)porta; + } + + void WritePortA(byte porta) + { + WrittenPortA = (byte)((porta & DDRA) | (WrittenPortA & (~DDRA))); + } + + void WritePortB(byte portb) + { + WrittenPortB = (byte)((portb & DDRB) | (WrittenPortB & (~DDRB))); + } + + // PortB: Console Switches + // + // D0 Game Reset 0=on + // D1 Game Select 0=on + // D2 (used on 7800) + // D3 Console Color 1=Color, 0=B/W + // D4 (used on 7800) + // D5 (unused) + // D6 Left Difficulty A 1=A (pro), 0=B (novice) + // D7 Right Difficulty A 1=A (pro), 0=B (novice) + // + byte ReadPortB() + { + var portb = 0; + var mi = M.InputState; + + portb |= mi.SampleCapturedConsoleSwitchState(ConsoleSwitch.GameReset) ? 0 : (1 << 0); + portb |= mi.SampleCapturedConsoleSwitchState(ConsoleSwitch.GameSelect) ? 0 : (1 << 1); + portb |= mi.SampleCapturedConsoleSwitchState(ConsoleSwitch.GameBW) ? 0 : (1 << 3); + portb |= mi.SampleCapturedConsoleSwitchState(ConsoleSwitch.LeftDifficultyA) ? (1 << 6) : 0; + portb |= mi.SampleCapturedConsoleSwitchState(ConsoleSwitch.RightDifficultyA) ? (1 << 7) : 0; + + return (byte)portb; + } + + #region Serialization Members + + public PIA(DeserializationContext input, MachineBase m) : this(m) + { + if (input == null) + throw new ArgumentNullException("input"); + + var version = input.CheckVersion(1, 2); + RAM = input.ReadExpectedBytes(0x80); + TimerTarget = input.ReadUInt64(); + TimerShift = input.ReadInt32(); + IRQEnabled = input.ReadBoolean(); + IRQTriggered = input.ReadBoolean(); + DDRA = input.ReadByte(); + WrittenPortA = input.ReadByte(); + if (version > 1) + { + DDRB = input.ReadByte(); + WrittenPortB = input.ReadByte(); + } + } + + public void GetObjectData(SerializationContext output) + { + if (output == null) + throw new ArgumentNullException("output"); + + output.WriteVersion(2); + output.Write(RAM); + output.Write(TimerTarget); + output.Write(TimerShift); + output.Write(IRQEnabled); + output.Write(IRQTriggered); + output.Write(DDRA); + output.Write(WrittenPortA); + output.Write(DDRB); + output.Write(WrittenPortB); + } + + #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); + } + + [System.Diagnostics.Conditional("DEBUG")] + void AssertDebug(bool cond) + { + if (!cond) + System.Diagnostics.Debugger.Break(); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/PokeySound.cs b/EMU7800/Core/PokeySound.cs new file mode 100644 index 0000000000..02466de2c6 --- /dev/null +++ b/EMU7800/Core/PokeySound.cs @@ -0,0 +1,437 @@ +/* + * PokeySound.cs + * + * Emulation of the audio features of the Atari Pot Keyboard Integrated Circuit (POKEY, C012294). + * + * Implementation inspired by prior works of Greg Stanton (ProSystem Emulator) and Ron Fries. + * + * Copyright © 2012 Mike Murphy + * + */ +using System; + +namespace EMU7800.Core +{ + public sealed class PokeySound + { + #region Constants and Tables + + const int + AUDF1 = 0x00, // write reg: channel 1 frequency + AUDC1 = 0x01, // write reg: channel 1 generator + AUDF2 = 0x02, // write reg: channel 2 frequency + AUDC2 = 0x03, // write reg: channel 2 generator + AUDF3 = 0x04, // write reg: channel 3 frequency + AUDC3 = 0x05, // write reg: channel 3 generator + AUDF4 = 0x06, // write reg: channel 4 frequency + AUDC4 = 0x07, // write reg: channel 4 generator + AUDCTL = 0x08, // write reg: control over audio channels + SKCTL = 0x0f, // write reg: control over serial port + RANDOM = 0x0a; // read reg: random number generator value + + const int + AUDCTL_POLY9 = 0x80, // make 17-bit poly counter into a 9-bit poly counter + AUDCTL_CH1_179 = 0x40, // clocks channel 1 with 1.79 MHz, instead of 64 kHz + AUDCTL_CH3_179 = 0x20, // clocks channel 3 with 1.79 MHz, instead of 64 kHz + AUDCTL_CH1_CH2 = 0x10, // clock channel 2 with channel 1, instead of 64 kHz (16-bit) + AUDCTL_CH3_CH4 = 0x08, // clock channel 4 with channel 3, instead of 64 kHz (16-bit) + AUDCTL_CH1_FILTER = 0x04, // inserts high-pass filter into channel 1, clocked by channel 3 + AUDCTL_CH2_FILTER = 0x02, // inserts high-pass filter into channel 2, clocked by channel 4 + AUDCTL_CLOCK_15 = 0x01; // change normal clock base from 64 kHz to 15 kHz + + const int + AUDC_NOTPOLY5 = 0x80, + AUDC_POLY4 = 0x40, + AUDC_PURE = 0x20, + AUDC_VOLUME_ONLY = 0x10, + AUDC_VOLUME_MASK = 0x0f; + + const int + DIV_64 = 28, + DIV_15 = 114, + POLY9_SIZE = 0x01ff, + POLY17_SIZE = 0x0001ffff, + POKEY_FREQ = 1787520, + SKCTL_RESET = 3; + + const int CPU_TICKS_PER_AUDIO_SAMPLE = 57; + + readonly byte[] _poly04 = { 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0 }; + readonly byte[] _poly05 = { 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1 }; + readonly byte[] _poly17 = new byte[POLY9_SIZE]; // should be POLY17_SIZE, but instead wrapping around to conserve storage + + readonly Random _random = new Random(); + + #endregion + + #region Object State + + readonly MachineBase M; + + readonly int _pokeyTicksPerSample; + int _pokeyTicks; + + ulong _lastUpdateCpuClock; + int _bufferIndex; + + readonly byte[] _audf = new byte[4]; + readonly byte[] _audc = new byte[4]; + byte _audctl, _skctl; + + int _baseMultiplier; + int _poly04Counter; + int _poly05Counter; + int _poly17Counter, _poly17Size; + + readonly int[] _divideMax = new int[4]; + readonly int[] _divideCount = new int[4]; + readonly byte[] _output = new byte[4]; + readonly byte[] _outvol = new byte[4]; + + #endregion + + #region Public Members + + public void Reset() + { + _poly04Counter = _poly05Counter = _poly17Counter = _audctl = _skctl = 0; + + _baseMultiplier = DIV_64; + _poly17Size = POLY17_SIZE; + + _pokeyTicks = 0; + + for (var ch = 0; ch < 4; ch++) + { + _outvol[ch] = _output[ch] = _audc[ch] = _audf[ch] = 0; + _divideCount[ch] = Int32.MaxValue; + _divideMax[ch] = Int32.MaxValue; + } + } + + public void StartFrame() + { + _lastUpdateCpuClock = M.CPU.Clock; + _bufferIndex = 0; + } + + public void EndFrame() + { + RenderSamples(M.FrameBuffer.SoundBufferByteLength - _bufferIndex); + } + + public byte Read(ushort addr) + { + addr &= 0xf; + + switch (addr) + { + // If the 2 least significant bits of SKCTL are 0, the random number generator is disabled (return all 1s.) + // Ballblazer music relies on this. + case RANDOM: + return (_skctl & SKCTL_RESET) == 0 ? (byte)0xff : (byte)_random.Next(0xff); + default: + return 0; + } + } + + public void Update(ushort addr, byte data) + { + if (M.CPU.Clock > _lastUpdateCpuClock) + { + var updCpuClocks = (int)(M.CPU.Clock - _lastUpdateCpuClock); + var samples = updCpuClocks / CPU_TICKS_PER_AUDIO_SAMPLE; + RenderSamples(samples); + _lastUpdateCpuClock += (ulong)(samples * CPU_TICKS_PER_AUDIO_SAMPLE); + } + + addr &= 0xf; + + switch (addr) + { + case AUDF1: + _audf[0] = data; + ResetChannel1(); + if ((_audctl & AUDCTL_CH1_CH2) != 0) + ResetChannel2(); + break; + case AUDC1: + _audc[0] = data; + ResetChannel1(); + break; + case AUDF2: + _audf[1] = data; + ResetChannel2(); + break; + case AUDC2: + _audc[1] = data; + ResetChannel2(); + break; + case AUDF3: + _audf[2] = data; + ResetChannel3(); + if ((_audctl & AUDCTL_CH3_CH4) != 0) + ResetChannel4(); + break; + case AUDC3: + _audc[2] = data; + ResetChannel3(); + break; + case AUDF4: + _audf[3] = data; + ResetChannel4(); + break; + case AUDC4: + _audc[3] = data; + ResetChannel4(); + break; + case AUDCTL: + _audctl = data; + _poly17Size = ((_audctl & AUDCTL_POLY9) != 0) ? POLY9_SIZE : POLY17_SIZE; + _baseMultiplier = ((_audctl & AUDCTL_CLOCK_15) != 0) ? DIV_15 : DIV_64; + ResetChannel1(); + ResetChannel2(); + ResetChannel3(); + ResetChannel4(); + break; + case SKCTL: + _skctl = data; + break; + } + } + + #endregion + + #region Constructors + + private PokeySound() + { + _random.NextBytes(_poly17); + for (var i = 0; i < _poly17.Length; i++) + _poly17[i] &= 0x01; + + Reset(); + } + + public PokeySound(MachineBase m) : this() + { + if (m == null) + throw new ArgumentNullException("m"); + + M = m; + + // Add 8-bits of fractional representation to reduce distortion on output + _pokeyTicksPerSample = (POKEY_FREQ << 8) / M.SoundSampleFrequency; + } + + #endregion + + #region Serialization Members + + public PokeySound(DeserializationContext input, MachineBase m) : this(m) + { + if (input == null) + throw new ArgumentNullException("input"); + + input.CheckVersion(1); + _lastUpdateCpuClock = input.ReadUInt64(); + _bufferIndex = input.ReadInt32(); + _audf = input.ReadBytes(); + _audc = input.ReadBytes(); + _audctl = input.ReadByte(); + _skctl = input.ReadByte(); + _output = input.ReadBytes(); + _outvol = input.ReadBytes(); + _divideMax = input.ReadIntegers(4); + _divideCount = input.ReadIntegers(4); + _pokeyTicks = input.ReadInt32(); + _pokeyTicksPerSample = input.ReadInt32(); + _baseMultiplier = input.ReadInt32(); + _poly04Counter = input.ReadInt32(); + _poly05Counter = input.ReadInt32(); + _poly17Counter = input.ReadInt32(); + _poly17Size = input.ReadInt32(); + } + + public void GetObjectData(SerializationContext output) + { + if (output == null) + throw new ArgumentNullException("output"); + + output.WriteVersion(1); + output.Write(_lastUpdateCpuClock); + output.Write(_bufferIndex); + output.Write(_audf); + output.Write(_audc); + output.Write(_audctl); + output.Write(_skctl); + output.Write(_output); + output.Write(_outvol); + output.Write(_divideMax); + output.Write(_divideCount); + output.Write(_pokeyTicks); + output.Write(_pokeyTicksPerSample); + output.Write(_baseMultiplier); + output.Write(_poly04Counter); + output.Write(_poly05Counter); + output.Write(_poly17Counter); + output.Write(_poly17Size); + } + + #endregion + + #region Helpers + + void RenderSamples(int count) + { + const int POKEY_SAMPLE = 4; + var poly17Length = (_poly17Size > _poly17.Length ? _poly17.Length : _poly17Size); + + while (count > 0 && _bufferIndex < M.FrameBuffer.SoundBufferByteLength) + { + var nextEvent = POKEY_SAMPLE; + var wholeTicksToConsume = (_pokeyTicks >> 8); + + for (var ch = 0; ch < 4; ch++) + { + if (_divideCount[ch] <= wholeTicksToConsume) + { + wholeTicksToConsume = _divideCount[ch]; + nextEvent = ch; + } + } + + for (var ch = 0; ch < 4; ch++) + _divideCount[ch] -= wholeTicksToConsume; + + _pokeyTicks -= (wholeTicksToConsume << 8); + + if (nextEvent == POKEY_SAMPLE) + { + _pokeyTicks += _pokeyTicksPerSample; + + byte sample = 0; + for (var ch = 0; ch < 4; ch++) + sample += _outvol[ch]; + + M.FrameBuffer.SoundBuffer[_bufferIndex >> BufferElement.SHIFT][_bufferIndex++] += sample; + count--; + + continue; + } + + _divideCount[nextEvent] += _divideMax[nextEvent]; + + _poly04Counter += wholeTicksToConsume; + _poly04Counter %= _poly04.Length; + + _poly05Counter += wholeTicksToConsume; + _poly05Counter %= _poly05.Length; + + _poly17Counter += wholeTicksToConsume; + _poly17Counter %= poly17Length; + + if ((_audc[nextEvent] & AUDC_NOTPOLY5) != 0 || _poly05[_poly05Counter] != 0) + { + if ((_audc[nextEvent] & AUDC_PURE) != 0) + _output[nextEvent] ^= 1; + else if ((_audc[nextEvent] & AUDC_POLY4) != 0) + _output[nextEvent] = _poly04[_poly04Counter]; + else + _output[nextEvent] = _poly17[_poly17Counter]; + } + + _outvol[nextEvent] = (_output[nextEvent] != 0) ? (byte)(_audc[nextEvent] & AUDC_VOLUME_MASK) : (byte)0; + } + } + + // As defined in the manual, the exact divider values are different depending on the frequency and resolution: + // 64 kHz or 15 kHz AUDF + 1 + // 1 MHz, 8-bit AUDF + 4 + // 1 MHz, 16-bit AUDF[CHAN1] + 256 * AUDF[CHAN2] + 7 + + void ResetChannel1() + { + var val = ((_audctl & AUDCTL_CH1_179) != 0) ? (_audf[0] + 4) : ((_audf[0] + 1) * _baseMultiplier); + if (val != _divideMax[0]) + { + _divideMax[0] = val; + if (val < _divideCount[0]) + _divideCount[0] = val; + } + UpdateVolumeSettingsForChannel(0); + } + + void ResetChannel2() + { + int val; + if ((_audctl & AUDCTL_CH1_CH2) != 0) + { + val = ((_audctl & AUDCTL_CH1_179) != 0) ? (_audf[1] * 256 + _audf[0] + 7) : ((_audf[1] * 256 + _audf[0] + 1) * _baseMultiplier); + } + else + { + val = ((_audf[1] + 1) * _baseMultiplier); + } + if (val != _divideMax[1]) + { + _divideMax[1] = val; + if (val < _divideCount[1]) + _divideCount[1] = val; + } + UpdateVolumeSettingsForChannel(1); + } + + void ResetChannel3() + { + var val = ((_audctl & AUDCTL_CH3_179) != 0) ? (_audf[2] + 4) : ((_audf[2] + 1) * _baseMultiplier); + if (val != _divideMax[2]) + { + _divideMax[2] = val; + if (val < _divideCount[2]) + _divideCount[2] = val; + } + UpdateVolumeSettingsForChannel(2); + } + + void ResetChannel4() + { + int val; + if ((_audctl & AUDCTL_CH3_CH4) != 0) + { + val = ((_audctl & AUDCTL_CH3_179) != 0) ? (_audf[3] * 256 + _audf[2] + 7) : ((_audf[3] * 256 + _audf[2] + 1) * _baseMultiplier); + } + else + { + val = ((_audf[3] + 1) * _baseMultiplier); + } + if (val != _divideMax[3]) + { + _divideMax[3] = val; + if (val < _divideCount[3]) + _divideCount[3] = val; + } + UpdateVolumeSettingsForChannel(3); + } + + void UpdateVolumeSettingsForChannel(int ch) + { + if (((_audc[ch] & AUDC_VOLUME_ONLY) != 0) || ((_audc[ch] & AUDC_VOLUME_MASK) == 0) || (_divideMax[ch] < (_pokeyTicksPerSample >> 8))) + { + _outvol[ch] = (byte)(_audc[ch] & AUDC_VOLUME_MASK); + _divideCount[ch] = Int32.MaxValue; + _divideMax[ch] = Int32.MaxValue; + } + } + + [System.Diagnostics.Conditional("DEBUG")] + void LogDebug(string format, params object[] args) + { + if (M == null || M.Logger == null) + return; + M.Logger.WriteLine(format, args); + } + + #endregion + } +} diff --git a/EMU7800/Core/RAM6116.cs b/EMU7800/Core/RAM6116.cs new file mode 100644 index 0000000000..66d5296f99 --- /dev/null +++ b/EMU7800/Core/RAM6116.cs @@ -0,0 +1,65 @@ +/* + * RAM6116.cs + * + * Implements a 6116 RAM device found in the 7800. + * + * Copyright © 2004 Mike Murphy + * + */ +using System; + +namespace EMU7800.Core +{ + public sealed class RAM6116 : IDevice + { + readonly byte[] RAM; + + #region IDevice Members + + public void Reset() {} + + public byte this[ushort addr] + { + get { return RAM[addr & 0x07ff]; } + set { RAM[addr & 0x07ff] = value; } + } + + #endregion + + #region Constructors + + public RAM6116() + { + RAM = new byte[0x800]; + } + + public RAM6116(byte[] ram) + { + RAM = ram; + } + + #endregion + + #region Serialization Members + + public RAM6116(DeserializationContext input) + { + if (input == null) + throw new ArgumentNullException("input"); + + input.CheckVersion(1); + RAM = input.ReadExpectedBytes(0x800); + } + + public void GetObjectData(SerializationContext output) + { + if (output == null) + throw new ArgumentNullException("output"); + + output.WriteVersion(1); + output.Write(RAM); + } + + #endregion + } +} \ No newline at end of file diff --git a/EMU7800/Core/SerializationContext.cs b/EMU7800/Core/SerializationContext.cs new file mode 100644 index 0000000000..1c018e41ce --- /dev/null +++ b/EMU7800/Core/SerializationContext.cs @@ -0,0 +1,230 @@ +using System; +using System.IO; + +namespace EMU7800.Core +{ + /// + /// A context for serializing objects. + /// + public class SerializationContext + { + #region Fields + + readonly BinaryWriter _binaryWriter; + + #endregion + + public void Write(byte value) + { + _binaryWriter.Write(value); + } + + public void Write(ushort value) + { + _binaryWriter.Write(value); + } + + public void Write(int value) + { + _binaryWriter.Write(value); + } + + public void Write(uint value) + { + _binaryWriter.Write(value); + } + + public void Write(long value) + { + _binaryWriter.Write(value); + } + + public void Write(ulong value) + { + _binaryWriter.Write(value); + } + + public void Write(bool value) + { + _binaryWriter.Write(value); + } + + public void Write(double value) + { + _binaryWriter.Write(value); + } + + public void Write(BufferElement bufferElement) + { + for (var i = 0; i < BufferElement.SIZE; i++) + Write(bufferElement[i]); + } + + public void Write(byte[] bytes) + { + _binaryWriter.Write(bytes.Length); + if (bytes.Length > 0) + _binaryWriter.Write(bytes); + } + + public void Write(ushort[] ushorts) + { + var bytes = new byte[ushorts.Length << 1]; + Buffer.BlockCopy(ushorts, 0, bytes, 0, bytes.Length); + Write(bytes); + } + + public void Write(int[] ints) + { + var bytes = new byte[ints.Length << 2]; + Buffer.BlockCopy(ints, 0, bytes, 0, bytes.Length); + Write(bytes); + } + + public void Write(uint[] uints) + { + var bytes = new byte[uints.Length << 2]; + Buffer.BlockCopy(uints, 0, bytes, 0, bytes.Length); + Write(bytes); + } + + public void Write(bool[] booleans) + { + var bytes = new byte[booleans.Length]; + for (var i = 0; i < bytes.Length; i++) + { + bytes[i] = (byte)(booleans[i] ? 0xff : 0x00); + } + Write(bytes); + } + + public void Write(MachineBase m) + { + WriteTypeName(m); + m.GetObjectData(this); + } + + public void Write(AddressSpace mem) + { + mem.GetObjectData(this); + } + + public void Write(M6502 cpu) + { + cpu.GetObjectData(this); + } + + public void Write(PIA pia) + { + pia.GetObjectData(this); + } + + public void Write(TIA tia) + { + tia.GetObjectData(this); + } + + public void Write(TIASound tiaSound) + { + tiaSound.GetObjectData(this); + } + + public void Write(Maria maria) + { + maria.GetObjectData(this); + } + + public void Write(Cart cart) + { + WriteTypeName(cart); + cart.GetObjectData(this); + } + + public void Write(RAM6116 ram6116) + { + ram6116.GetObjectData(this); + } + + public void Write(InputState inputState) + { + inputState.GetObjectData(this); + } + + public void WriteVersion(int version) + { + Write(0x78000087); + Write(version); + } + + public void WriteOptional(byte[] bytes) + { + var hasBytes = (bytes != null); + _binaryWriter.Write(hasBytes); + if (!hasBytes) + return; + _binaryWriter.Write(bytes.Length); + if (bytes.Length > 0) + _binaryWriter.Write(bytes); + } + + public void WriteOptional(HSC7800 hsc7800) + { + var exist = (hsc7800 != null); + Write(exist); + if (!exist) + return; + hsc7800.GetObjectData(this); + } + + public void WriteOptional(Bios7800 bios7800) + { + var exist = (bios7800 != null); + Write(exist); + if (!exist) + return; + bios7800.GetObjectData(this); + } + + public void WriteOptional(PokeySound pokeySound) + { + var exist = (pokeySound != null); + Write(exist); + if (!exist) + return; + pokeySound.GetObjectData(this); + } + + #region Constructors + + private SerializationContext() + { + } + + /// + /// Instantiates a new instance of . + /// + /// + internal SerializationContext(BinaryWriter binaryWriter) + { + if (binaryWriter == null) + throw new ArgumentNullException("binaryWriter"); + _binaryWriter = binaryWriter; + } + + #endregion + + #region Helpers + + void WriteTypeName(object o) + { + if (o == null) + throw new Emu7800SerializationException("Type unexpectedly null."); + var typeName = o.GetType().FullName; + if (string.IsNullOrWhiteSpace(typeName)) + throw new Emu7800SerializationException("Unable to discover type name."); + _binaryWriter.Write(typeName); + } + + #endregion + } +} diff --git a/EMU7800/Core/TIA.cs b/EMU7800/Core/TIA.cs new file mode 100644 index 0000000000..6af0602101 --- /dev/null +++ b/EMU7800/Core/TIA.cs @@ -0,0 +1,1355 @@ +/* + * 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) + { + var i = FrameBufferIndex++; + FrameBufferElement[i] = fbyte; + if ((i & BufferElement.MASK) == BufferElement.MASK) + { + M.FrameBuffer.VideoBuffer[i >> BufferElement.SHIFT] = FrameBufferElement; + 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(1); + 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(1); + 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 + } +} \ No newline at end of file diff --git a/EMU7800/Core/TIASound.cs b/EMU7800/Core/TIASound.cs new file mode 100644 index 0000000000..f9a57b5635 --- /dev/null +++ b/EMU7800/Core/TIASound.cs @@ -0,0 +1,361 @@ +/* + * TIASound.cs + * + * Sound emulation for the 2600. Based upon TIASound © 1997 by Ron Fries. + * + * Copyright © 2003, 2004 Mike Murphy + * + */ + +/*****************************************************************************/ +/* */ +/* License Information and Copyright Notice */ +/* ======================================== */ +/* */ +/* TiaSound is Copyright(c) 1997 by Ron Fries */ +/* */ +/* This library is free software; you can redistribute it and/or modify it */ +/* under the terms of version 2 of the GNU Library General Public License */ +/* as published by the Free Software Foundation. */ +/* */ +/* This library is distributed in the hope that it will be useful, but */ +/* WITHOUT ANY WARRANTY; without even the implied warranty of */ +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library */ +/* General Public License for more details. */ +/* To obtain a copy of the GNU Library General Public License, write to the */ +/* Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ +/* */ +/* Any permitted reproduction of these routines, in whole or in part, must */ +/* bear this legend. */ +/* */ +/*****************************************************************************/ + +using System; + +namespace EMU7800.Core +{ + public sealed class TIASound + { + #region Constants and Tables + + // Clock Source Clock Modifier Source Pattern + const int + SET_TO_1 = 0x00, // 0 0 0 0 3.58 Mhz/114 none (pure) none + //POLY4 = 0x01, // 0 0 0 1 3.58 Mhz/114 none (pure) 4-bit poly + //DIV31_POLY4 = 0x02, // 0 0 1 0 3.58 Mhz/114 divide by 31 4-bit poly + //POLY5_POLY4 = 0x03, // 0 0 1 1 3.58 Mhz/114 5-bit poly 4-bit poly + //PURE = 0x04, // 0 1 0 0 3.58 Mhz/114 none (pure) pure (~Q) + //PURE2 = 0x05, // 0 1 0 1 3.58 Mhz/114 none (pure) pure (~Q) + //DIV31_PURE = 0x06, // 0 1 1 0 3.58 Mhz/114 divide by 31 pure (~Q) + //POLY5_2 = 0x07, // 0 1 1 1 3.58 Mhz/114 5-bit poly pure (~Q) + POLY9 = 0x08; // 1 0 0 0 3.58 Mhz/114 none (pure) 9-bit poly + //POLY5 = 0x09, // 1 0 0 1 3.58 Mhz/114 none (pure) 5-bit poly + //DIV31_POLY5 = 0x0a, // 1 0 1 0 3.58 Mhz/114 divide by 31 5-bit poly + //POLY5_POLY5 = 0x0b, // 1 0 1 1 3.58 Mhz/114 5-bit poly 5-bit poly + //DIV3_PURE = 0x0c, // 1 1 0 0 1.19 Mhz/114 none (pure) pure (~Q) + //DIV3_PURE2 = 0x0d, // 1 1 0 1 1.19 Mhz/114 none (pure) pure (~Q) + //DIV93_PURE = 0x0e, // 1 1 1 0 1.19 Mhz/114 divide by 31 pure (~Q) + //DIV3_POLY5 = 0x0f; // 1 1 1 1 1.19 Mhz/114 5-bit poly pure (~Q) + + const int + AUDC0 = 0x15, // audio control 0 (D3-0) + AUDC1 = 0x16, // audio control 1 (D4-0) + AUDF0 = 0x17, // audio frequency 0 (D4-0) + AUDF1 = 0x18, // audio frequency 1 (D3-0) + AUDV0 = 0x19, // audio volume 0 (D3-0) + AUDV1 = 0x1a; // audio volume 1 (D3-0) + + // The 4bit and 5bit patterns are the identical ones used in the tia chip. + readonly byte[] Bit4 = new byte[] { 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0 }; // 2^4 - 1 = 15 + readonly byte[] Bit5 = new byte[] { 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1 }; // 2^5 - 1 = 31 + + // [Ron] treated the 'Div by 31' counter as another polynomial because of + // the way it operates. It does not have a 50% duty cycle, but instead + // has a 13:18 ratio (of course, 13+18 = 31). This could also be + // implemented by using counters. + readonly byte[] Div31 = new byte[] { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + + // Rather than have a table with 511 entries, I use a random number + readonly byte[] Bit9 = new byte[511]; // 2^9 - 1 = 511 + + readonly int[] P4 = new int[2]; // Position counter for the 4-bit POLY array + readonly int[] P5 = new int[2]; // Position counter for the 5-bit POLY array + readonly int[] P9 = new int[2]; // Position counter for the 9-bit POLY array + + readonly int[] DivByNCounter = new int[2]; // Divide by n counter, one for each channel + readonly int[] DivByNMaximum = new int[2]; // Divide by n maximum, one for each channel + + readonly int _cpuClocksPerSample; + + #endregion + + #region Object State + + readonly MachineBase M; + + // The TIA Sound registers + readonly byte[] AUDC = new byte[2]; + readonly byte[] AUDF = new byte[2]; + readonly byte[] AUDV = new byte[2]; + + // The last output volume for each channel + readonly byte[] OutputVol = new byte[2]; + + // Used to determine how much sound to render + ulong LastUpdateCPUClock; + + int BufferIndex; + + #endregion + + #region Public Members + + public void Reset() + { + for (var chan = 0; chan < 2; chan++) + { + OutputVol[chan] = 0; + DivByNCounter[chan] = 0; + DivByNMaximum[chan] = 0; + AUDC[chan] = 0; + AUDF[chan] = 0; + AUDV[chan] = 0; + P4[chan] = 0; + P5[chan] = 0; + P9[chan] = 0; + } + } + + public void StartFrame() + { + LastUpdateCPUClock = M.CPU.Clock; + BufferIndex = 0; + } + + public void EndFrame() + { + RenderSamples(M.FrameBuffer.SoundBufferByteLength - BufferIndex); + } + + public void Update(ushort addr, byte data) + { + if (M.CPU.Clock > LastUpdateCPUClock) + { + var updCPUClocks = (int)(M.CPU.Clock - LastUpdateCPUClock); + var samples = updCPUClocks / _cpuClocksPerSample; + RenderSamples(samples); + LastUpdateCPUClock += (ulong)(samples * _cpuClocksPerSample); + } + + byte chan; + + switch (addr) + { + case AUDC0: + AUDC[0] = (byte)(data & 0x0f); + chan = 0; + break; + case AUDC1: + AUDC[1] = (byte)(data & 0x0f); + chan = 1; + break; + case AUDF0: + AUDF[0] = (byte)(data & 0x1f); + chan = 0; + break; + case AUDF1: + AUDF[1] = (byte)(data & 0x1f); + chan = 1; + break; + case AUDV0: + AUDV[0] = (byte)(data & 0x0f); + chan = 0; + break; + case AUDV1: + AUDV[1] = (byte)(data & 0x0f); + chan = 1; + break; + default: + return; + } + + byte new_divn_max; + + if (AUDC[chan] == SET_TO_1) + { + // indicate the clock is zero so no process will occur + new_divn_max = 0; + // and set the output to the selected volume + OutputVol[chan] = AUDV[chan]; + } + else + { + // otherwise calculate the 'divide by N' value + new_divn_max = (byte)(AUDF[chan] + 1); + // if bits D2 & D3 are set, then multiply the 'div by n' count by 3 + if ((AUDC[chan] & 0x0c) == 0x0c) + { + new_divn_max *= 3; + } + } + + // only reset those channels that have changed + if (new_divn_max != DivByNMaximum[chan]) + { + DivByNMaximum[chan] = new_divn_max; + + // if the channel is now volume only or was volume only... + if (DivByNCounter[chan] == 0 || new_divn_max == 0) + { + // reset the counter (otherwise let it complete the previous) + DivByNCounter[chan] = new_divn_max; + } + } + } + + #endregion + + #region Constructors + + private TIASound() + { + var r = new Random(); + r.NextBytes(Bit9); + for (var i = 0; i < Bit9.Length; i++) + { + Bit9[i] &= 0x01; + } + } + + public TIASound(MachineBase m, int cpuClocksPerSample) : this() + { + if (m == null) + throw new ArgumentNullException("m"); + if (cpuClocksPerSample <= 0) + throw new ArgumentException("cpuClocksPerSample must be positive."); + + M = m; + _cpuClocksPerSample = cpuClocksPerSample; + } + + #endregion + + #region Serialization Members + + public TIASound(DeserializationContext input, MachineBase m, int cpuClocksPerSample) : this(m, cpuClocksPerSample) + { + if (input == null) + throw new ArgumentNullException("input"); + + input.CheckVersion(1); + Bit9 = input.ReadExpectedBytes(511); + P4 = input.ReadIntegers(2); + P5 = input.ReadIntegers(2); + P9 = input.ReadIntegers(2); + DivByNCounter = input.ReadIntegers(2); + DivByNMaximum = input.ReadIntegers(2); + AUDC = input.ReadExpectedBytes(2); + AUDF = input.ReadExpectedBytes(2); + AUDV = input.ReadExpectedBytes(2); + OutputVol = input.ReadExpectedBytes(2); + LastUpdateCPUClock = input.ReadUInt64(); + BufferIndex = input.ReadInt32(); + } + + public void GetObjectData(SerializationContext output) + { + if (output == null) + throw new ArgumentNullException("output"); + + output.WriteVersion(1); + output.Write(Bit9); + output.Write(P4); + output.Write(P5); + output.Write(P9); + output.Write(DivByNCounter); + output.Write(DivByNMaximum); + output.Write(AUDC); + output.Write(AUDF); + output.Write(AUDV); + output.Write(OutputVol); + output.Write(LastUpdateCPUClock); + output.Write(BufferIndex); + } + + #endregion + + #region Helpers + + void RenderSamples(int count) + { + for (; BufferIndex < M.FrameBuffer.SoundBufferByteLength && count-- > 0; BufferIndex++) + { + if (DivByNCounter[0] > 1) + { + DivByNCounter[0]--; + } + else if (DivByNCounter[0] == 1) + { + DivByNCounter[0] = DivByNMaximum[0]; + ProcessChannel(0); + } + if (DivByNCounter[1] > 1) + { + DivByNCounter[1]--; + } + else if (DivByNCounter[1] == 1) + { + DivByNCounter[1] = DivByNMaximum[1]; + ProcessChannel(1); + } + + M.FrameBuffer.SoundBuffer[BufferIndex >> BufferElement.SHIFT][BufferIndex] += (byte)(OutputVol[0] + OutputVol[1]); + } + } + + void ProcessChannel(int chan) + { + // the P5 counter has multiple uses, so we inc it here + if (++P5[chan] >= 31) + { // POLY5 size: 2^5 - 1 = 31 + P5[chan] = 0; + } + + // check clock modifier for clock tick + if ((AUDC[chan] & 0x02) == 0 || + ((AUDC[chan] & 0x01) == 0 && Div31[P5[chan]] == 1) || + ((AUDC[chan] & 0x01) == 1 && Bit5[P5[chan]] == 1)) + { + if ((AUDC[chan] & 0x04) != 0) + { // pure modified clock selected + OutputVol[chan] = (OutputVol[chan] != 0) ? (byte)0 : AUDV[chan]; + } + else if ((AUDC[chan] & 0x08) != 0) + { // check for poly5/poly9 + if (AUDC[chan] == POLY9) + { // check for poly9 + if (++P9[chan] >= 511) + { // poly9 size: 2^9 - 1 = 511 + P9[chan] = 0; + } + OutputVol[chan] = (Bit9[P9[chan]] == 1) ? AUDV[chan] : (byte)0; + } + else + { // must be poly5 + OutputVol[chan] = (Bit5[P5[chan]] == 1) ? AUDV[chan] : (byte)0; + } + } + else + { // poly4 is the only remaining possibility + if (++P4[chan] >= 15) + { // POLY4 size: 2^4 - 1 = 15 + P4[chan] = 0; + } + OutputVol[chan] = (Bit4[P4[chan]] == 1) ? AUDV[chan] : (byte)0; + } + } + } + + #endregion + } +} diff --git a/EMU7800/Core/TIATables.cs b/EMU7800/Core/TIATables.cs new file mode 100644 index 0000000000..4a38a2b482 --- /dev/null +++ b/EMU7800/Core/TIATables.cs @@ -0,0 +1,424 @@ +/* + * TIATables.cs + * + * Mask tables for the Television Interface Adaptor class. All derived from + * Bradford Mott's Stella code. + * + * Copyright © 2003, 2004 Mike Murphy + * + */ +namespace EMU7800.Core +{ + public static class TIATables + { + public static readonly TIACxPairFlags[] CollisionMask = BuildCollisionMaskTable(); + public static readonly uint[][] PFMask = BuildPFMaskTable(); + public static readonly bool[][] BLMask = BuildBLMaskTable(); + public static readonly bool[][][] MxMask = BuildMxMaskTable(); + public static readonly byte[][][] PxMask = BuildPxMaskTable(); + public static readonly byte[] GRPReflect = BuildGRPReflectTable(); + + public static readonly int[] NTSCPalette = + { + 0x000000, 0x000000, 0x4a4a4a, 0x4a4a4a, + 0x6f6f6f, 0x6f6f6f, 0x8e8e8e, 0x8e8e8e, + 0xaaaaaa, 0xaaaaaa, 0xc0c0c0, 0xc0c0c0, + 0xd6d6d6, 0xd6d6d6, 0xececec, 0xececec, + + 0x484800, 0x484800, 0x69690f, 0x69690f, + 0x86861d, 0x86861d, 0xa2a22a, 0xa2a22a, + 0xbbbb35, 0xbbbb35, 0xd2d240, 0xd2d240, + 0xe8e84a, 0xe8e84a, 0xfcfc54, 0xfcfc54, + + 0x7c2c00, 0x7c2c00, 0x904811, 0x904811, + 0xa26221, 0xa26221, 0xb47a30, 0xb47a30, + 0xc3903d, 0xc3903d, 0xd2a44a, 0xd2a44a, + 0xdfb755, 0xdfb755, 0xecc860, 0xecc860, + + 0x901c00, 0x901c00, 0xa33915, 0xa33915, + 0xb55328, 0xb55328, 0xc66c3a, 0xc66c3a, + 0xd5824a, 0xd5824a, 0xe39759, 0xe39759, + 0xf0aa67, 0xf0aa67, 0xfcbc74, 0xfcbc74, + + 0x940000, 0x940000, 0xa71a1a, 0xa71a1a, + 0xb83232, 0xb83232, 0xc84848, 0xc84848, + 0xd65c5c, 0xd65c5c, 0xe46f6f, 0xe46f6f, + 0xf08080, 0xf08080, 0xfc9090, 0xfc9090, + + 0x840064, 0x840064, 0x97197a, 0x97197a, + 0xa8308f, 0xa8308f, 0xb846a2, 0xb846a2, + 0xc659b3, 0xc659b3, 0xd46cc3, 0xd46cc3, + 0xe07cd2, 0xe07cd2, 0xec8ce0, 0xec8ce0, + + 0x500084, 0x500084, 0x68199a, 0x68199a, + 0x7d30ad, 0x7d30ad, 0x9246c0, 0x9246c0, + 0xa459d0, 0xa459d0, 0xb56ce0, 0xb56ce0, + 0xc57cee, 0xc57cee, 0xd48cfc, 0xd48cfc, + + 0x140090, 0x140090, 0x331aa3, 0x331aa3, + 0x4e32b5, 0x4e32b5, 0x6848c6, 0x6848c6, + 0x7f5cd5, 0x7f5cd5, 0x956fe3, 0x956fe3, + 0xa980f0, 0xa980f0, 0xbc90fc, 0xbc90fc, + + 0x000094, 0x000094, 0x181aa7, 0x181aa7, + 0x2d32b8, 0x2d32b8, 0x4248c8, 0x4248c8, + 0x545cd6, 0x545cd6, 0x656fe4, 0x656fe4, + 0x7580f0, 0x7580f0, 0x8490fc, 0x8490fc, + + 0x001c88, 0x001c88, 0x183b9d, 0x183b9d, + 0x2d57b0, 0x2d57b0, 0x4272c2, 0x4272c2, + 0x548ad2, 0x548ad2, 0x65a0e1, 0x65a0e1, + 0x75b5ef, 0x75b5ef, 0x84c8fc, 0x84c8fc, + + 0x003064, 0x003064, 0x185080, 0x185080, + 0x2d6d98, 0x2d6d98, 0x4288b0, 0x4288b0, + 0x54a0c5, 0x54a0c5, 0x65b7d9, 0x65b7d9, + 0x75cceb, 0x75cceb, 0x84e0fc, 0x84e0fc, + + 0x004030, 0x004030, 0x18624e, 0x18624e, + 0x2d8169, 0x2d8169, 0x429e82, 0x429e82, + 0x54b899, 0x54b899, 0x65d1ae, 0x65d1ae, + 0x75e7c2, 0x75e7c2, 0x84fcd4, 0x84fcd4, + + 0x004400, 0x004400, 0x1a661a, 0x1a661a, + 0x328432, 0x328432, 0x48a048, 0x48a048, + 0x5cba5c, 0x5cba5c, 0x6fd26f, 0x6fd26f, + 0x80e880, 0x80e880, 0x90fc90, 0x90fc90, + + 0x143c00, 0x143c00, 0x355f18, 0x355f18, + 0x527e2d, 0x527e2d, 0x6e9c42, 0x6e9c42, + 0x87b754, 0x87b754, 0x9ed065, 0x9ed065, + 0xb4e775, 0xb4e775, 0xc8fc84, 0xc8fc84, + + 0x303800, 0x303800, 0x505916, 0x505916, + 0x6d762b, 0x6d762b, 0x88923e, 0x88923e, + 0xa0ab4f, 0xa0ab4f, 0xb7c25f, 0xb7c25f, + 0xccd86e, 0xccd86e, 0xe0ec7c, 0xe0ec7c, + + 0x482c00, 0x482c00, 0x694d14, 0x694d14, + 0x866a26, 0x866a26, 0xa28638, 0xa28638, + 0xbb9f47, 0xbb9f47, 0xd2b656, 0xd2b656, + 0xe8cc63, 0xe8cc63, 0xfce070, 0xfce070 + }; + + public static readonly int[] PALPalette = + { + 0x000000, 0x000000, 0x2b2b2b, 0x2b2b2b, + 0x525252, 0x525252, 0x767676, 0x767676, + 0x979797, 0x979797, 0xb6b6b6, 0xb6b6b6, + 0xd2d2d2, 0xd2d2d2, 0xececec, 0xececec, + + 0x000000, 0x000000, 0x2b2b2b, 0x2b2b2b, + 0x525252, 0x525252, 0x767676, 0x767676, + 0x979797, 0x979797, 0xb6b6b6, 0xb6b6b6, + 0xd2d2d2, 0xd2d2d2, 0xececec, 0xececec, + + 0x805800, 0x000000, 0x96711a, 0x2b2b2b, + 0xab8732, 0x525252, 0xbe9c48, 0x767676, + 0xcfaf5c, 0x979797, 0xdfc06f, 0xb6b6b6, + 0xeed180, 0xd2d2d2, 0xfce090, 0xececec, + + 0x445c00, 0x000000, 0x5e791a, 0x2b2b2b, + 0x769332, 0x525252, 0x8cac48, 0x767676, + 0xa0c25c, 0x979797, 0xb3d76f, 0xb6b6b6, + 0xc4ea80, 0xd2d2d2, 0xd4fc90, 0xececec, + + 0x703400, 0x000000, 0x89511a, 0x2b2b2b, + 0xa06b32, 0x525252, 0xb68448, 0x767676, + 0xc99a5c, 0x979797, 0xdcaf6f, 0xb6b6b6, + 0xecc280, 0xd2d2d2, 0xfcd490, 0xececec, + + 0x006414, 0x000000, 0x1a8035, 0x2b2b2b, + 0x329852, 0x525252, 0x48b06e, 0x767676, + 0x5cc587, 0x979797, 0x6fd99e, 0xb6b6b6, + 0x80ebb4, 0xd2d2d2, 0x90fcc8, 0xececec, + + 0x700014, 0x000000, 0x891a35, 0x2b2b2b, + 0xa03252, 0x525252, 0xb6486e, 0x767676, + 0xc95c87, 0x979797, 0xdc6f9e, 0xb6b6b6, + 0xec80b4, 0xd2d2d2, 0xfc90c8, 0xececec, + + 0x005c5c, 0x000000, 0x1a7676, 0x2b2b2b, + 0x328e8e, 0x525252, 0x48a4a4, 0x767676, + 0x5cb8b8, 0x979797, 0x6fcbcb, 0xb6b6b6, + 0x80dcdc, 0xd2d2d2, 0x90ecec, 0xececec, + + 0x70005c, 0x000000, 0x841a74, 0x2b2b2b, + 0x963289, 0x525252, 0xa8489e, 0x767676, + 0xb75cb0, 0x979797, 0xc66fc1, 0xb6b6b6, + 0xd380d1, 0xd2d2d2, 0xe090e0, 0xececec, + + 0x003c70, 0x000000, 0x195a89, 0x2b2b2b, + 0x2f75a0, 0x525252, 0x448eb6, 0x767676, + 0x57a5c9, 0x979797, 0x68badc, 0xb6b6b6, + 0x79ceec, 0xd2d2d2, 0x88e0fc, 0xececec, + + 0x580070, 0x000000, 0x6e1a89, 0x2b2b2b, + 0x8332a0, 0x525252, 0x9648b6, 0x767676, + 0xa75cc9, 0x979797, 0xb76fdc, 0xb6b6b6, + 0xc680ec, 0xd2d2d2, 0xd490fc, 0xececec, + + 0x002070, 0x000000, 0x193f89, 0x2b2b2b, + 0x2f5aa0, 0x525252, 0x4474b6, 0x767676, + 0x578bc9, 0x979797, 0x68a1dc, 0xb6b6b6, + 0x79b5ec, 0xd2d2d2, 0x88c8fc, 0xececec, + + 0x340080, 0x000000, 0x4a1a96, 0x2b2b2b, + 0x5f32ab, 0x525252, 0x7248be, 0x767676, + 0x835ccf, 0x979797, 0x936fdf, 0xb6b6b6, + 0xa280ee, 0xd2d2d2, 0xb090fc, 0xececec, + + 0x000088, 0x000000, 0x1a1a9d, 0x2b2b2b, + 0x3232b0, 0x525252, 0x4848c2, 0x767676, + 0x5c5cd2, 0x979797, 0x6f6fe1, 0xb6b6b6, + 0x8080ef, 0xd2d2d2, 0x9090fc, 0xececec, + + 0x000000, 0x000000, 0x2b2b2b, 0x2b2b2b, + 0x525252, 0x525252, 0x767676, 0x767676, + 0x979797, 0x979797, 0xb6b6b6, 0xb6b6b6, + 0xd2d2d2, 0xd2d2d2, 0xececec, 0xececec, + + 0x000000, 0x000000, 0x2b2b2b, 0x2b2b2b, + 0x525252, 0x525252, 0x767676, 0x767676, + 0x979797, 0x979797, 0xb6b6b6, 0xb6b6b6, + 0xd2d2d2, 0xd2d2d2, 0xececec, 0xececec + }; + + static uint[][] BuildPFMaskTable() + { + var tabl = new uint[2][]; + tabl[0] = new uint[160]; + tabl[1] = new uint[160]; + + for (var i = 0; i < 20; i++) + { + uint mask = 0; + if (i < 4) + { + mask = (uint)(1 << i); + } + else if (i < 12) + { + mask = (uint)(1 << (11 + 4 - i)); + } + else if (i < 20) + { + mask = (uint)(1 << i); + } + for (var j = 0; j < 4; j++) + { + // for non-reflected mode + tabl[0][4 * i + j] = mask; + tabl[0][80 + 4 * i + j] = mask; + + // for reflected mode + tabl[1][4 * i + j] = mask; + tabl[1][159 - 4 * i - j] = mask; + } + } + return tabl; + } + + static bool[][] BuildBLMaskTable() + { + var tabl = new bool[4][]; + for (var size = 0; size < 4; size++) + { + tabl[size] = new bool[160]; + for (var i = 0; i < 160; i++) + { + tabl[size][i] = false; + } + for (var i = 0; i < (1 << size); i++) + { + tabl[size][i] = true; + } + } + return tabl; + } + + static bool[][][] BuildMxMaskTable() + { + var tabl = new bool[4][][]; + for (var i = 0; i < 4; i++) + { + tabl[i] = new bool[8][]; + for (var j = 0; j < 8; j++) + { + tabl[i][j] = new bool[160]; + for (var k = 0; k < 160; k++) + { + tabl[i][j][k] = false; + } + } + } + + for (var size = 0; size < 4; size++) + { + for (var i = 0; i < (1 << size); i++) + { + tabl[size][0][i] = true; + + tabl[size][1][i] = true; + tabl[size][1][i + 16] = true; + + tabl[size][2][i] = true; + tabl[size][2][i + 32] = true; + + tabl[size][3][i] = true; + tabl[size][3][i + 16] = true; + tabl[size][3][i + 32] = true; + + tabl[size][4][i] = true; + tabl[size][4][i + 64] = true; + + tabl[size][5][i] = true; + + tabl[size][6][i] = true; + tabl[size][6][i + 32] = true; + tabl[size][6][i + 64] = true; + + tabl[size][7][i] = true; + } + } + return tabl; + } + + static byte[][][] BuildPxMaskTable() + { + // [suppress mode, nusiz, pixel] + // suppress=1: suppress on + // suppress=0: suppress off + var tabl = new byte[2][][]; //2 8 160 + tabl[0] = new byte[8][]; + tabl[1] = new byte[8][]; + for (var nusiz = 0; nusiz < 8; nusiz++) + { + tabl[0][nusiz] = new byte[160]; + tabl[1][nusiz] = new byte[160]; + for (var hpos = 0; hpos < 160; hpos++) + { + // nusiz: + // 0: one copy + // 1: two copies-close + // 2: two copies-med + // 3: three copies-close + // 4: two copies-wide + // 5: double size player + // 6: 3 copies medium + // 7: quad sized player + tabl[0][nusiz][hpos] = tabl[1][nusiz][hpos] = 0; + if (nusiz >= 0 && nusiz <= 4 || nusiz == 6) + { + if (hpos >= 0 && hpos < 8) + { + tabl[0][nusiz][hpos] = (byte)(1 << (7 - hpos)); + } + } + if (nusiz == 1 || nusiz == 3) + { + if (hpos >= 16 && hpos < 24) + { + tabl[0][nusiz][hpos] = (byte)(1 << (23 - hpos)); + tabl[1][nusiz][hpos] = (byte)(1 << (23 - hpos)); + } + } + if (nusiz == 2 || nusiz == 3 || nusiz == 6) + { + if (hpos >= 32 && hpos < 40) + { + tabl[0][nusiz][hpos] = (byte)(1 << (39 - hpos)); + tabl[1][nusiz][hpos] = (byte)(1 << (39 - hpos)); + } + } + if (nusiz == 4 || nusiz == 6) + { + if (hpos >= 64 && hpos < 72) + { + tabl[0][nusiz][hpos] = (byte)(1 << (71 - hpos)); + tabl[1][nusiz][hpos] = (byte)(1 << (71 - hpos)); + } + } + if (nusiz == 5) + { + if (hpos >= 0 && hpos < 16) + { + tabl[0][nusiz][hpos] = (byte)(1 << ((15 - hpos) >> 1)); + } + } + if (nusiz == 7) + { + if (hpos >= 0 && hpos < 32) + { + tabl[0][nusiz][hpos] = (byte)(1 << ((31 - hpos) >> 2)); + } + } + } + + var shift = nusiz == 5 || nusiz == 7 ? 2 : 1; + while (shift-- > 0) + { + for (var i = 159; i > 0; i--) + { + tabl[0][nusiz][i] = tabl[0][nusiz][i - 1]; + tabl[1][nusiz][i] = tabl[1][nusiz][i - 1]; + } + tabl[0][nusiz][0] = tabl[1][nusiz][0] = 0; + } + } + return tabl; + } + + static byte[] BuildGRPReflectTable() + { + var tabl = new byte[256]; + + for (var i = 0; i < 256; i++) + { + var s = (byte)i; + var r = (byte)0; + for (var j = 0; j < 8; j++) + { + r <<= 1; + r |= (byte)(s & 1); + s >>= 1; + } + tabl[i] = r; + } + return tabl; + } + + static bool tstCx(int i, TIACxFlags cxf1, TIACxFlags cxf2) + { + var f1 = (int)cxf1; + var f2 = (int)cxf2; + return ((i & f1) != 0) && ((i & f2) != 0); + } + + static TIACxPairFlags[] BuildCollisionMaskTable() + { + var tabl = new TIACxPairFlags[64]; + + for (var i = 0; i < 64; i++) + { + tabl[i] = 0; + if (tstCx(i, TIACxFlags.M0, TIACxFlags.P1)) { tabl[i] |= TIACxPairFlags.M0P1; } + if (tstCx(i, TIACxFlags.M0, TIACxFlags.P0)) { tabl[i] |= TIACxPairFlags.M0P0; } + if (tstCx(i, TIACxFlags.M1, TIACxFlags.P0)) { tabl[i] |= TIACxPairFlags.M1P0; } + if (tstCx(i, TIACxFlags.M1, TIACxFlags.P1)) { tabl[i] |= TIACxPairFlags.M1P1; } + if (tstCx(i, TIACxFlags.P0, TIACxFlags.PF)) { tabl[i] |= TIACxPairFlags.P0PF; } + if (tstCx(i, TIACxFlags.P0, TIACxFlags.BL)) { tabl[i] |= TIACxPairFlags.P0BL; } + if (tstCx(i, TIACxFlags.P1, TIACxFlags.PF)) { tabl[i] |= TIACxPairFlags.P1PF; } + if (tstCx(i, TIACxFlags.P1, TIACxFlags.BL)) { tabl[i] |= TIACxPairFlags.P1BL; } + if (tstCx(i, TIACxFlags.M0, TIACxFlags.PF)) { tabl[i] |= TIACxPairFlags.M0PF; } + if (tstCx(i, TIACxFlags.M0, TIACxFlags.BL)) { tabl[i] |= TIACxPairFlags.M0BL; } + if (tstCx(i, TIACxFlags.M1, TIACxFlags.PF)) { tabl[i] |= TIACxPairFlags.M1PF; } + if (tstCx(i, TIACxFlags.M1, TIACxFlags.BL)) { tabl[i] |= TIACxPairFlags.M1BL; } + if (tstCx(i, TIACxFlags.BL, TIACxFlags.PF)) { tabl[i] |= TIACxPairFlags.BLPF; } + if (tstCx(i, TIACxFlags.P0, TIACxFlags.P1)) { tabl[i] |= TIACxPairFlags.P0P1; } + if (tstCx(i, TIACxFlags.M0, TIACxFlags.M1)) { tabl[i] |= TIACxPairFlags.M0M1; } + } + return tabl; + } + } +} diff --git a/EMU7800/EMU7800.csproj b/EMU7800/EMU7800.csproj new file mode 100644 index 0000000000..000b40745f --- /dev/null +++ b/EMU7800/EMU7800.csproj @@ -0,0 +1,127 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {E01193DF-F104-4B95-9D1B-FAD830F6F620} + Library + Properties + EMU7800 + EMU7800 + v4.0 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + copy /y $(TargetDir)$(TargetFileName) $(ProjectDir)..\Bizhawk.MultiClient\output\dll\$(TargetFileName) + + + + \ No newline at end of file diff --git a/EMU7800/EMU7800.sln b/EMU7800/EMU7800.sln new file mode 100644 index 0000000000..f9ab26fb2e --- /dev/null +++ b/EMU7800/EMU7800.sln @@ -0,0 +1,20 @@ + +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EMU7800", "EMU7800.csproj", "{E01193DF-F104-4B95-9D1B-FAD830F6F620}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E01193DF-F104-4B95-9D1B-FAD830F6F620}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E01193DF-F104-4B95-9D1B-FAD830F6F620}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E01193DF-F104-4B95-9D1B-FAD830F6F620}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E01193DF-F104-4B95-9D1B-FAD830F6F620}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/EMU7800/Win/GameProgram.cs b/EMU7800/Win/GameProgram.cs new file mode 100644 index 0000000000..be65e225a0 --- /dev/null +++ b/EMU7800/Win/GameProgram.cs @@ -0,0 +1,76 @@ +/* + * GameProgram.cs + * + * Represents attribute data associated with ROMs + * + * Copyright 2003, 2004, 2010 © Mike Murphy + * + */ + +/* + * unlike EMU7800 Core stuff, this has been hacked around a bit + */ + +using System.Text; +using EMU7800.Core; + +namespace EMU7800.Win +{ + public class GameProgram + { + public string MD5 { get; set; } + public string Title { get; set; } + public string Manufacturer { get; set; } + public string Author { get; set; } + public string Year { get; set; } + public string ModelNo { get; set; } + public string Rarity { get; set; } + public CartType CartType { get; set; } + public MachineType MachineType { get; set; } + public Controller LController { get; set; } + public Controller RController { get; set; } + public string HelpUri { get; set; } + + public string DiscoveredRomFullName { get; set; } + + public override string ToString() + { + var s = new StringBuilder("GameSettings:\n"); + s.AppendFormat(" MD5: {0}\n", MD5); + s.AppendFormat(" Title: {0}\n", Title); + s.AppendFormat(" Manufacturer: {0}\n", Manufacturer); + s.AppendFormat(" Author: {0}\n", Author); + s.AppendFormat(" Year: {0}\n", Year); + s.AppendFormat(" ModelNo: {0}\n", ModelNo); + s.AppendFormat(" Rarity: {0}\n", Rarity); + s.AppendFormat(" CartType: {0}\n", CartType); + s.AppendFormat(" MachineType: {0}\n", MachineType); + s.AppendFormat(" LController: {0}\n", LController); + s.AppendFormat(" RController: {0}\n", RController); + s.AppendFormat(" HelpUri: {0}", HelpUri); + if (DiscoveredRomFullName != null) s.AppendFormat("\n Discovered Rom Filename: {0}", DiscoveredRomFullName); + return s.ToString(); + } + + public GameProgram(string md5) + { + MD5 = md5; + } + + /// + /// not in db, so guess + /// + /// + /// + public static GameProgram GetCompleteGuess(string md5) + { + GameProgram ret = new GameProgram(md5); + ret.Title = "UNKNOWN"; + //ret.CartType = CartType.A7848; // will be guessed for us + ret.MachineType = MachineType.A7800NTSC; + ret.LController = Controller.Joystick; + ret.RController = Controller.Joystick; + return ret; + } + } +} \ No newline at end of file diff --git a/EMU7800/Win/GameProgramLibrary.cs b/EMU7800/Win/GameProgramLibrary.cs new file mode 100644 index 0000000000..4834fd3979 --- /dev/null +++ b/EMU7800/Win/GameProgramLibrary.cs @@ -0,0 +1,383 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using EMU7800.Core; + +/* + * unlike EMU7800 Core stuff, this has been hacked around significantly + */ + +namespace EMU7800.Win +{ + public class GameProgramLibrary : Dictionary + { + #region Fields + + const string + BIOS78_NTSC_MD5 = "0763f1ffb006ddbe32e52d497ee848ae", + BIOS78_NTSC_ALTERNATE_MD5 = "b32526ea179dc9ab9b2e5f8a2662b298", + BIOS78_PAL_MD5 = "397bb566584be7b9764e7a68974c4263", + HSC78_MD5 = "c8a73288ab97226c52602204ab894286"; + /* + readonly IDictionary _misc7800DiscoveredRoms = new Dictionary(); + */ + // these enums are used for matching column names in the .csv file + enum Column + { + None, + MD5, + Title, + Manufacturer, + Author, + Year, + ModelNo, + Rarity, + CartType, + MachineType, + LController, + RController, + HelpUri + }; + + //const string RomPropertiesFileName = "ROMProperties.csv"; + + static readonly Dictionary _columnPositions = new Dictionary + { + { Column.MD5, -1}, + { Column.Title, -1}, + { Column.Manufacturer, -1}, + { Column.Author, -1 }, + { Column.Year, - 1}, + { Column.ModelNo, -1}, + { Column.Rarity, -1}, + { Column.CartType, -1}, + { Column.MachineType, -1}, + { Column.LController, -1}, + { Column.RController, -1}, + { Column.HelpUri, -1}, + }; + + //readonly RomFileAccessor _romFileAccessor = new RomFileAccessor(); + readonly MD5CryptoServiceProvider _cryptoProvider = new MD5CryptoServiceProvider(); + readonly StringBuilder _sb = new StringBuilder(); + //readonly ILogger _logger; + + #endregion + + public static GameProgramLibrary EMU7800DB = null; + + #region Constructors + + private GameProgramLibrary() + { + } + + public GameProgramLibrary(TextReader r)//, ILogger logger) + { + //if (logger == null) + // throw new ArgumentNullException("logger"); + + //_logger = logger; + + //var settings = new GlobalSettings(logger); + //var fn = Path.Combine(settings.BaseDirectory, RomPropertiesFileName); + + Clear(); + //StreamReader r = null; + try + { + //r = new StreamReader(fn); + InitializeColumnPos(r.ReadLine()); + + while (true) + { + var line = r.ReadLine(); + if (line == null) + break; + var gp = CreateGameSettingsFromLine(line); + if (gp == null) + continue; + if (ContainsKey(gp.MD5)) + Console.WriteLine("7800DB: Duplicate MD5 key found: {0}", gp.MD5); else Add(gp.MD5, gp); + } + r.Close(); + } + catch (Exception ex) + { + //if (Util.IsCriticalException(ex)) + throw; + //_logger.WriteLine(ex); + } + finally + { + if (r != null) + r.Dispose(); + } + + Console.WriteLine("7800DB: {0} entries loaded.", Count); + } + + #endregion + + #region Game Program Accessors + + public GameProgram TryRecognizeRom(byte[] bytes) + { + //if (string.IsNullOrWhiteSpace(fullName)) + // throw new ArgumentException("fullName"); + + //var bytes = _romFileAccessor.GetRomBytes(fullName); + if (bytes == null) + return null; + + var md5 = ComputeMD5Digest(bytes); + if (string.IsNullOrWhiteSpace(md5)) + return null; + + var gp = GetGameProgramFromMd5(md5); + if (gp == null) + gp = GameProgram.GetCompleteGuess(md5); + //gp.DiscoveredRomFullName = fullName; + if (gp.CartType == CartType.None) + { + switch (gp.MachineType) + { + case MachineType.A2600NTSC: + case MachineType.A2600PAL: + switch (bytes.Length) + { + case 2048: gp.CartType = CartType.A2K; break; + case 4096: gp.CartType = CartType.A4K; break; + case 8192: gp.CartType = CartType.A8K; break; + case 16384: gp.CartType = CartType.A16K; break; + } + break; + case MachineType.A7800NTSC: + case MachineType.A7800PAL: + switch (bytes.Length) + { + case 8192: gp.CartType = CartType.A7808; break; + case 16384: gp.CartType = CartType.A7816; break; + case 32768: gp.CartType = CartType.A7832; break; + case 49152: gp.CartType = CartType.A7848; break; + } + break; + } + } + return gp; + /* + if (md5.Equals(HSC78_MD5, StringComparison.OrdinalIgnoreCase)) + { + if (!_misc7800DiscoveredRoms.ContainsKey(md5)) + _misc7800DiscoveredRoms.Add(md5, fullName); + _logger.WriteLine("Found 7800 Highscore Cart: {0}", fullName); + return null; + } + if (md5.Equals(BIOS78_NTSC_MD5, StringComparison.OrdinalIgnoreCase)) + { + if (!_misc7800DiscoveredRoms.ContainsKey(md5)) + _misc7800DiscoveredRoms.Add(md5, fullName); + _logger.WriteLine("Found 7800 NTSC BIOS: {0}", fullName); + return null; + } + if (md5.Equals(BIOS78_NTSC_ALTERNATE_MD5, StringComparison.OrdinalIgnoreCase)) + { + if (!_misc7800DiscoveredRoms.ContainsKey(md5)) + _misc7800DiscoveredRoms.Add(md5, fullName); + _logger.WriteLine("Found incorrect but widely used 7800 NTSC BIOS: {0}", fullName); + return null; + } + if (md5.Equals(BIOS78_PAL_MD5, StringComparison.OrdinalIgnoreCase)) + { + if (!_misc7800DiscoveredRoms.ContainsKey(md5)) + _misc7800DiscoveredRoms.Add(md5, fullName); + _logger.WriteLine("Found 7800 PAL BIOS: {0}", fullName); + return null; + } + */ + } + + /* + public GameProgram GetGameProgramFromFullName(string fullName) + { + var bytes = _romFileAccessor.GetRomBytes(fullName); + if (bytes == null) + throw new ArgumentException("File not readable: {0}", fullName); + var md5 = ComputeMD5Digest(bytes); + return !string.IsNullOrWhiteSpace(md5) ? GetGameProgramFromMd5(md5) : null; + } + */ + public GameProgram GetGameProgramFromMd5(string md5) + { + if (string.IsNullOrWhiteSpace(md5)) + throw new ArgumentNullException("md5"); + GameProgram gp; + return TryGetValue(md5, out gp) ? gp : null; + } + /* + public byte[] GetRomBytes(string fullName) + { + return _romFileAccessor.GetRomBytes(fullName); + } + + public byte[] Get78HighScoreCartBytes() + { + string fullName; + if (!_misc7800DiscoveredRoms.TryGetValue(HSC78_MD5, out fullName)) + return null; + return _romFileAccessor.GetRomBytes(fullName); + } + + public byte[] Get78BiosBytes(MachineType machineType) + { + string fullName = null; + switch (machineType) + { + case MachineType.A7800NTSC: + if (!_misc7800DiscoveredRoms.TryGetValue(BIOS78_NTSC_MD5, out fullName)) + _misc7800DiscoveredRoms.TryGetValue(BIOS78_NTSC_ALTERNATE_MD5, out fullName); + break; + case MachineType.A7800PAL: + _misc7800DiscoveredRoms.TryGetValue(BIOS78_PAL_MD5, out fullName); + break; + } + if (string.IsNullOrWhiteSpace(fullName)) + return null; + return _romFileAccessor.GetRomBytes(fullName); + }*/ + + #endregion + + #region Game Progam Related Utilities + /* + public string ComputeMD5Digest(string fullName) + { + var bytes = _romFileAccessor.GetRomBytes(fullName); + if (bytes == null) + throw new ArgumentException("File not readable: {0}", fullName); + return ComputeMD5Digest(bytes); + } + */ + #endregion + + #region Helpers + + static void InitializeColumnPos(string line) + { + var colno = 0; + var columnNames = line.Split(','); + + foreach (var columnName in columnNames) + { + var col = ToColumn(columnName); + if (col != Column.None) + _columnPositions[col] = colno; + colno++; + } + + if (_columnPositions[Column.MD5] < 0) + throw new ApplicationException("ROMProperties.csv: Required column missing: MD5"); + if (_columnPositions[Column.CartType] < 0) + throw new ApplicationException("ROMProperties.csv: Required column missing: CartType"); + if (_columnPositions[Column.MachineType] < 0) + throw new ApplicationException("ROMProperties.csv: Required column missing: MachineType"); + if (_columnPositions[Column.LController] < 0) + throw new ApplicationException("ROMProperties.csv: Required column missing: LController"); + if (_columnPositions[Column.RController] < 0) + throw new ApplicationException("ROMProperties.csv: Required column missing: RController"); + } + + static GameProgram CreateGameSettingsFromLine(string line) + { + var row = new string[13]; + var linesplit = line.Split(','); + for (var i = 0; i < row.Length && i < linesplit.Length; i++) + row[i] = linesplit[i]; + + var md5 = row[_columnPositions[Column.MD5]]; + var gp = new GameProgram(md5) + { + Title = _columnPositions[Column.Title] >= 0 ? row[_columnPositions[Column.Title]] : string.Empty, + Manufacturer = _columnPositions[Column.Manufacturer] >= 0 ? row[_columnPositions[Column.Manufacturer]] : string.Empty, + Author = _columnPositions[Column.Author] >= 0 ? row[_columnPositions[Column.Author]] : string.Empty, + Year = _columnPositions[Column.Year] >= 0 ? row[_columnPositions[Column.Year]] : string.Empty, + ModelNo = _columnPositions[Column.ModelNo] >= 0 ? row[_columnPositions[Column.ModelNo]] : string.Empty, + Rarity = _columnPositions[Column.Rarity] >= 0 ? row[_columnPositions[Column.Rarity]] : string.Empty, + CartType = ToCartType(row[_columnPositions[Column.CartType]]), + MachineType = ToMachineType(row[_columnPositions[Column.MachineType]]) + }; + + gp.LController = ToController(row[_columnPositions[Column.LController]]); + gp.RController = ToController(row[_columnPositions[Column.RController]]); + + if (gp.LController == Controller.None) + gp.LController = GetDefaultController(gp.MachineType); + if (gp.RController == Controller.None) + gp.RController = GetDefaultController(gp.MachineType); + + if (_columnPositions[Column.HelpUri] < row.Length) + { + var helpUri = row[_columnPositions[Column.HelpUri]].Trim(); + if (!helpUri.Length.Equals(0)) + gp.HelpUri = helpUri; + } + + return gp; + } + + static Controller GetDefaultController(MachineType machineType) + { + switch (machineType) + { + case MachineType.A7800NTSC: + case MachineType.A7800PAL: + return Controller.ProLineJoystick; + default: + return Controller.Joystick; + } + } + + static Column ToColumn(string columnName) + { + Column result; + return Enum.TryParse(columnName, true, out result) ? result : Column.None; + } + + static CartType ToCartType(string cartType) + { + CartType result; + return Enum.TryParse(cartType, true, out result) ? result : CartType.None; + } + + static MachineType ToMachineType(string machineType) + { + MachineType result; + return Enum.TryParse(machineType, true, out result) ? result : MachineType.None; + } + + static Controller ToController(string controller) + { + Controller result; + return Enum.TryParse(controller, true, out result) ? result : Controller.None; + } + + string ComputeMD5Digest(byte[] bytes) + { + return (bytes != null) ? StringifyMD5(_cryptoProvider.ComputeHash(bytes)) : null; + } + + string StringifyMD5(byte[] bytes) + { + if (bytes == null || bytes.Length < 16) + return string.Empty; + _sb.Length = 0; + for (var i = 0; i < 16; i++) + _sb.AppendFormat("{0:x2}", bytes[i]); + return _sb.ToString(); + } + + #endregion + } +}