From 83ea4bfb1cd72cb0d286888a86d3698335ca7935 Mon Sep 17 00:00:00 2001 From: saxxonpike Date: Sat, 4 Jan 2025 01:37:04 -0600 Subject: [PATCH 1/2] [C64] Fix a situation where the SID filter stage would pass through voices selected to be filtered while no filters are actually enabled --- .../Computers/Commodore64/FFT.cs | 61 +++++++++++-------- .../Computers/Commodore64/MOS/Sid.cs | 20 ++++-- 2 files changed, 53 insertions(+), 28 deletions(-) diff --git a/src/BizHawk.Emulation.Cores/Computers/Commodore64/FFT.cs b/src/BizHawk.Emulation.Cores/Computers/Commodore64/FFT.cs index 68514b60bf1..403c7e6e02e 100644 --- a/src/BizHawk.Emulation.Cores/Computers/Commodore64/FFT.cs +++ b/src/BizHawk.Emulation.Cores/Computers/Commodore64/FFT.cs @@ -6,41 +6,54 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64 { public class RealFFT { - private readonly int _length; - private readonly int[] _ip; - private readonly double[] _w; + private int _length; + private int[] _ip = Array.Empty(); + private double[] _w = Array.Empty(); - public readonly double ForwardScaleFactor; - public readonly double ReverseScaleFactor; - public readonly double CorrectionScaleFactor; + public double ForwardScaleFactor { get; private set; } + public double ReverseScaleFactor { get; private set; } + public double CorrectionScaleFactor { get; private set; } public RealFFT(int length) + { + Resize(length); + } + + public void Resize(int length) { if (length < 2 || (length & (length - 1)) != 0) { throw new ArgumentException("FFT length must be at least 2 and a power of 2.", nameof(length)); } + ForwardScaleFactor = length; + ReverseScaleFactor = 0.5d; + CorrectionScaleFactor = 1.0d / (ForwardScaleFactor * ReverseScaleFactor); _length = length; - _ip = new int[2 + (1 << (Convert.ToInt32(Math.Log(Math.Max(length / 4, 1), 2)) / 2))]; - _w = new double[length / 2]; - ForwardScaleFactor = length; - ReverseScaleFactor = 0.5; - CorrectionScaleFactor = 1.0 / (ForwardScaleFactor * ReverseScaleFactor); + var ipLength = 2 + (1 << (Convert.ToInt32(Math.Log(Math.Max(length / 4, 1), 2)) / 2)); + if (_ip.Length < ipLength) + Array.Resize(ref _ip, ipLength); + + var wLength = length / 2; + if (_w.Length < wLength) + Array.Resize(ref _w, wLength); + + _ip.AsSpan().Clear(); + _w.AsSpan().Clear(); } - public void ComputeForward(double[] buff) + public void ComputeForward(Span buff) { Compute(buff, false); } - public void ComputeReverse(double[] buff) + public void ComputeReverse(Span buff) { Compute(buff, true); } - private void Compute(double[] buff, bool reverse) + private void Compute(Span buff, bool reverse) { if (buff.Length < _length) { @@ -50,7 +63,7 @@ private void Compute(double[] buff, bool reverse) rdft(_length, reverse, buff, _ip, _w); } - private static void rdft(int n, bool rev, double[] a, int[] ip, double[] w) + private static void rdft(int n, bool rev, Span a, Span ip, Span w) { int nw, nc; double xi; @@ -102,7 +115,7 @@ private static void rdft(int n, bool rev, double[] a, int[] ip, double[] w) /* -------- initializing routines -------- */ - private static void makewt(int nw, int[] ip, double[] w) + private static void makewt(int nw, Span ip, Span w) { int j, nwh; double delta, x, y; @@ -133,7 +146,7 @@ private static void makewt(int nw, int[] ip, double[] w) } } - private static void makect(int nc, int[] ip, double[] c, int nw) + private static void makect(int nc, Span ip, Span c, int nw) { int j, nch; double delta; @@ -155,7 +168,7 @@ private static void makect(int nc, int[] ip, double[] c, int nw) /* -------- child routines -------- */ - private static void bitrv2(int n, int[] ip, double[] a) + private static void bitrv2(int n, Span ip, Span a) { int j, j1, k, k1, l, m, m2; double xr, xi, yr, yi; @@ -263,7 +276,7 @@ private static void bitrv2(int n, int[] ip, double[] a) } } - private static void cftfsub(int n, double[] a, double[] w) + private static void cftfsub(int n, Span a, Span w) { int j, j1, j2, j3, l; double x0r, x0i, x1r, x1i, x2r, x2i, x3r, x3i; @@ -319,7 +332,7 @@ private static void cftfsub(int n, double[] a, double[] w) } } - private static void cftbsub(int n, double[] a, double[] w) + private static void cftbsub(int n, Span a, Span w) { int j, j1, j2, j3, l; double x0r, x0i, x1r, x1i, x2r, x2i, x3r, x3i; @@ -375,7 +388,7 @@ private static void cftbsub(int n, double[] a, double[] w) } } - private static void cft1st(int n, double[] a, double[] w) + private static void cft1st(int n, Span a, Span w) { int j, k1, k2; double wk1r, wk1i, wk2r, wk2i, wk3r, wk3i; @@ -480,7 +493,7 @@ private static void cft1st(int n, double[] a, double[] w) } } - private static void cftmdl(int n, int l, double[] a, double[] w) + private static void cftmdl(int n, int l, Span a, Span w) { int j, j1, j2, j3, k, k1, k2, m, m2; double wk1r, wk1i, wk2r, wk2i, wk3r, wk3i; @@ -611,7 +624,7 @@ private static void cftmdl(int n, int l, double[] a, double[] w) } } - private static void rftfsub(int n, double[] a, int nc, double[] c, int nw) + private static void rftfsub(int n, Span a, int nc, Span c, int nw) { int j, k, kk, ks, m; double wkr, wki, xr, xi, yr, yi; @@ -636,7 +649,7 @@ private static void rftfsub(int n, double[] a, int nc, double[] c, int nw) } } - private static void rftbsub(int n, double[] a, int nc, double[] c, int nw) + private static void rftbsub(int n, Span a, int nc, Span c, int nw) { int j, k, kk, ks, m; double wkr, wki, xr, xi, yr, yi; diff --git a/src/BizHawk.Emulation.Cores/Computers/Commodore64/MOS/Sid.cs b/src/BizHawk.Emulation.Cores/Computers/Commodore64/MOS/Sid.cs index 75a2dd5c153..6d9e4dec0e4 100644 --- a/src/BizHawk.Emulation.Cores/Computers/Commodore64/MOS/Sid.cs +++ b/src/BizHawk.Emulation.Cores/Computers/Commodore64/MOS/Sid.cs @@ -259,16 +259,28 @@ public void filter_operator() } } - _fft = new RealFFT(nsamp_2); + if (_fft == null) + _fft = new RealFFT(nsamp_2); + else + _fft.Resize(nsamp_2); // eventually this will settle on a single buffer size and stop reallocating if (_fftBuffer.Length < nsamp_2) Array.Resize(ref _fftBuffer, nsamp_2); - // linearly interpolate the original sample set into the new denser sample set - for (double i = 0; i < nsamp_2; i++) + // If no filters are enabled, filtered output will be silent. + if (!_filterSelectLoPass && !_filterSelectHiPass && !_filterSelectBandPass) { - _fftBuffer[(int)i] = _outputBufferFiltered[(int)Math.Floor((i / (nsamp_2-1) * (nsamp - 1))) + _filterIndex]; + for (var i = 0; i < nsamp_2; i++) + _fftBuffer[i] = 0; + } + else + { + // linearly interpolate the original sample set into the new denser sample set + for (double i = 0; i < nsamp_2; i++) + { + _fftBuffer[(int)i] = _outputBufferFiltered[(int)Math.Floor((i / (nsamp_2-1) * (nsamp - 1))) + _filterIndex]; + } } // now we have everything we need to perform the FFT From 99ca3be22a9b4331fb2544a3adb57eeb70195dee Mon Sep 17 00:00:00 2001 From: saxxonpike Date: Fri, 3 Jan 2025 00:32:02 -0600 Subject: [PATCH 2/2] [C64] EasyFlash overhaul - Implement the AM29F040B flash device as its own component - Fix save data for games that use the standard EasyAPI driver - Fix "Briley Witch Chronicles 2": the flash driver checks AutoSelect register 02 to see if the cartridge is write-protected - Rom is stored as bytes instead of ints, saves a lot of memory - EasyFlash now has proper hard reset and clock operations - SaveRam deltas fixed --- .../Commodore64/C64.IMemoryDomains.cs | 6 + .../Computers/Commodore64/C64.cs | 9 + .../Commodore64/Cartridge/Am29F040B.cs | 315 +++++++++++++++ .../Commodore64/Cartridge/CartridgeChip.cs | 8 + .../Commodore64/Cartridge/CartridgeDevice.cs | 15 +- .../Commodore64/Cartridge/CartridgePort.cs | 10 + .../Commodore64/Cartridge/Mapper0020.cs | 358 +++++++++--------- 7 files changed, 542 insertions(+), 179 deletions(-) create mode 100644 src/BizHawk.Emulation.Cores/Computers/Commodore64/Cartridge/Am29F040B.cs create mode 100644 src/BizHawk.Emulation.Cores/Computers/Commodore64/Cartridge/CartridgeChip.cs diff --git a/src/BizHawk.Emulation.Cores/Computers/Commodore64/C64.IMemoryDomains.cs b/src/BizHawk.Emulation.Cores/Computers/Commodore64/C64.IMemoryDomains.cs index 942b495ff4e..e063931d7e4 100644 --- a/src/BizHawk.Emulation.Cores/Computers/Commodore64/C64.IMemoryDomains.cs +++ b/src/BizHawk.Emulation.Cores/Computers/Commodore64/C64.IMemoryDomains.cs @@ -11,6 +11,7 @@ private void SetupMemoryDomains() { bool diskDriveEnabled = _board.DiskDrive != null; bool tapeDriveEnabled = _board.TapeDrive != null; + bool cartEnabled = _board.CartPort.IsConnected; var domains = new List { @@ -41,6 +42,11 @@ private void SetupMemoryDomains() }); } + if (cartEnabled) + { + domains.AddRange(_board.CartPort.CreateMemoryDomains()); + } + _memoryDomains = new MemoryDomainList(domains); ((BasicServiceProvider)ServiceProvider).Register(_memoryDomains); } diff --git a/src/BizHawk.Emulation.Cores/Computers/Commodore64/C64.cs b/src/BizHawk.Emulation.Cores/Computers/Commodore64/C64.cs index 7a187cf9919..b6dea718c96 100644 --- a/src/BizHawk.Emulation.Cores/Computers/Commodore64/C64.cs +++ b/src/BizHawk.Emulation.Cores/Computers/Commodore64/C64.cs @@ -36,6 +36,12 @@ public C64(CoreLoadParameters lp) } InitMedia(_roms[_currentDisk]); + + if (_board.CartPort.SaveRam is { } cartSaveRam) + { + ser.Register(cartSaveRam); + } + HardReset(); switch (SyncSettings.VicType) @@ -329,6 +335,9 @@ private void InitMedia(byte[] rom) if (cart != null) { _board.CartPort.Connect(cart); + if (_board.CartPort.SaveRam != null) + { + } } break; case C64Format.TAP: diff --git a/src/BizHawk.Emulation.Cores/Computers/Commodore64/Cartridge/Am29F040B.cs b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Cartridge/Am29F040B.cs new file mode 100644 index 00000000000..59e118ffef6 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Cartridge/Am29F040B.cs @@ -0,0 +1,315 @@ +using BizHawk.Common; +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Computers.Commodore64.Cartridge; + +/// +/// AMD flash chip used for EasyFlash emulation. +/// +public class Am29F040B +{ + // Source: + // https://www.mouser.com/datasheet/2/196/spansion_inc_am29f040b_eol_21445e8-3004346.pdf + // + // Flash erase suspend/resume are not implemented. + + public const int ImageSize = 1 << 19; + public const int ImageMask = ImageSize - 1; + private const int SectorSize = 1 << 16; + private const int SectorMask = SectorSize - 1; + private const int RegisterMask = (1 << 11) - 1; + + private const byte ToggleBit2 = 1 << 2; + private const byte ErrorBit = 1 << 5; + private const byte ToggleBit = 1 << 6; + private const byte PollingBit = 1 << 7; + private const byte EraseBit = 1 << 3; + + private const int WriteLatency = 7; + private const int EraseSectorLatency = 1000000; + private const int EraseChipLatency = 8000000; + private const int EraseValue = 0xFF; + + private const byte ManufacturerCode = 0x01; + private const byte DeviceCode = 0xA4; + private const byte WriteProtect = 0x00; // can be set to 1 to tell software it is write-protected + + private enum Sequence + { + None, + Start, + Complete, + Command + } + + private enum Mode + { + Read, + Erase, + AutoSelect, + Write + } + + private enum Register + { + Command0 = 0x0555, + Command1 = 0x02AA + } + + private enum Signal + { + Command0 = 0xAA, + Command1 = 0x55, + Erase = 0x80, + AutoSelect = 0x90, + Program = 0xA0, + ChipErase = 0x10, + SectorErase = 0x30, + Reset = 0xF0 + } + + private int _busyTimeRemaining; + private int _status; + private byte[] _data = new byte[ImageSize]; + private Mode _mode; + private Sequence _sequence; + private bool _returnStatus; + private int _startAddress; + private int _endAddress; + private bool _errorPending; + private bool _dataDirty; + + public MemoryDomain CreateMemoryDomain(string name) => + new MemoryDomainByteArray( + name: name, + endian: MemoryDomain.Endian.Little, + data: _data, + writable: true, + wordSize: 1 + ); + + public void Clock() + { + if (_busyTimeRemaining <= 0) + return; + + _busyTimeRemaining--; + + if (_busyTimeRemaining != 0) + return; + + _status ^= PollingBit; + + if (_errorPending) + { + _errorPending = false; + _status |= ErrorBit; + } + } + + /// + /// Synchronize state. + /// + /// + /// State serializer. + /// + /// + /// True only if the raw data should be synchronized. If false, + /// the caller is responsible for synchronizing deltas. + /// + public void SyncState(Serializer ser, bool withData) + { + ser.Sync("BusyTimeRemaining", ref _busyTimeRemaining); + ser.Sync("Status", ref _status); + ser.SyncEnum("Mode", ref _mode); + ser.SyncEnum("Sequence", ref _sequence); + ser.Sync("ReturnStatus", ref _returnStatus); + ser.Sync("StartAddress", ref _startAddress); + ser.Sync("EndAddress", ref _endAddress); + ser.Sync("ErrorPending", ref _errorPending); + ser.Sync("DataDirty", ref _dataDirty); + + if (withData) + ser.Sync("Data", ref _data, false); + } + + public void Reset() + { + _busyTimeRemaining = 0; + _status = 0; + _mode = Mode.Read; + _sequence = Sequence.None; + _errorPending = false; + _startAddress = 0; + _endAddress = ImageMask; + } + + public Span Data => + _data.AsSpan(); + + public int Peek(int addr) => + _data[addr & ImageMask] & 0xFF; + + public int Poke(int addr, int val) + { + var newData = val & 0xFF; + _dataDirty |= _data[addr & ImageMask] != newData; + return _data[addr & ImageMask] = unchecked((byte)newData); + } + + // From the datasheet: + // Address bits A18-A11 = X = Don’t Care for all address + // commands except for Program Address (PA), Sector Address (SA), Read + // Address (RA), and AutoSelect sector protect verify. + + public int Read(int addr) + { + int data; + + if (_busyTimeRemaining > 0) + { + if (addr >= _startAddress && addr <= _endAddress) + _status ^= ToggleBit2; + + _status ^= ToggleBit; + return _status; + } + + // Some commands allow one read of status before going back to read mode. + // Areas being written or erased will always return status during modification. + if (_returnStatus && addr >= _startAddress && addr <= _endAddress) + { + _returnStatus = false; + return _status; + } + + // Read manufacturer registers or memory. + switch (_mode) + { + case Mode.AutoSelect: + { + switch (addr & 0xFF) + { + case 0x00: + data = ManufacturerCode; + break; + case 0x01: + data = DeviceCode; + break; + case 0x02: + data = WriteProtect; + break; + default: + data = 0xFF; + break; + } + break; + } + default: + { + data = _data[addr & ImageMask]; + break; + } + } + + return data; + } + + public void Write(int addr, int data) + { + switch (_mode, _sequence, (Register)(addr & RegisterMask), (Signal)data) + { + case (Mode.Write, _, _, _): + { + _mode = Mode.Read; + _sequence = Sequence.None; + + if (_busyTimeRemaining > 0) + break; + + var originalData = _data[addr & ImageMask]; + var newData = originalData & data & 0xFF; + _dataDirty |= newData != originalData; + _errorPending = data != newData; + _data[addr & ImageMask] = unchecked((byte)newData); + _busyTimeRemaining = WriteLatency; // 7-30us + _status = (data & 0x80) ^ PollingBit; + _returnStatus = true; + _startAddress = _endAddress = addr; + break; + } + case (_, _, Register.Command0, Signal.Command0): + { + _sequence = Sequence.Start; + break; + } + case (_, Sequence.Start, Register.Command1, Signal.Command1): + { + _sequence = Sequence.Complete; + break; + } + case (_, Sequence.Complete, Register.Command0, Signal.Erase): + { + _mode = Mode.Erase; + _sequence = Sequence.None; + break; + } + case (Mode.Erase, Sequence.Complete, Register.Command0, Signal.ChipErase): + { + _mode = Mode.Read; + _sequence = Sequence.None; + + if (_busyTimeRemaining > 0) + break; + + _busyTimeRemaining = EraseChipLatency; // 8-64sec + _data.AsSpan().Fill(EraseValue); + _dataDirty = true; + _returnStatus = true; + _status = EraseBit; // bit 7 = complete + break; + } + case (Mode.Erase, Sequence.Complete, _, Signal.SectorErase): + { + _mode = Mode.Read; + _sequence = Sequence.None; + + if (_busyTimeRemaining > 0) + break; + + _busyTimeRemaining = EraseSectorLatency; // ~1sec + _data.AsSpan(addr & ~SectorMask, SectorSize).Fill(0xFF); + _dataDirty = true; + _returnStatus = true; + _status = EraseBit; // bit 7 = complete + break; + } + case (Mode.Read, Sequence.Complete, Register.Command0, Signal.AutoSelect): + { + _mode = Mode.AutoSelect; + _sequence = Sequence.None; + break; + } + case (Mode.Read, Sequence.Complete, Register.Command0, Signal.Program): + { + _mode = Mode.Write; + break; + } + case (_, _, _, Signal.Reset): + { + _mode = Mode.Read; + _sequence = Sequence.None; + break; + } + } + } + + public bool IsDataDirty => _dataDirty; + + public bool CheckDataDirty() + { + var result = _dataDirty; + _dataDirty = false; + return result; + } +} \ No newline at end of file diff --git a/src/BizHawk.Emulation.Cores/Computers/Commodore64/Cartridge/CartridgeChip.cs b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Cartridge/CartridgeChip.cs new file mode 100644 index 00000000000..e370bce84b3 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Cartridge/CartridgeChip.cs @@ -0,0 +1,8 @@ +namespace BizHawk.Emulation.Cores.Computers.Commodore64.Cartridge; + +public class CartridgeChip +{ + public int Address; + public int Bank; + public int[] Data; +} \ No newline at end of file diff --git a/src/BizHawk.Emulation.Cores/Computers/Commodore64/Cartridge/CartridgeDevice.cs b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Cartridge/CartridgeDevice.cs index 693dfc36a85..e61507d16a4 100644 --- a/src/BizHawk.Emulation.Cores/Computers/Commodore64/Cartridge/CartridgeDevice.cs +++ b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Cartridge/CartridgeDevice.cs @@ -106,7 +106,7 @@ public static CartridgeDevice Load(byte[] crtFile) result = new Mapper0013(chipAddress, chipBank, chipData); break; case 0x0020: // EasyFlash - result = new Mapper0020(chipAddress, chipBank, chipData); + result = new Mapper0020(BuildChipList(chipAddress, chipBank, chipData)); break; case 0x002B: // Prophet 64 result = new Mapper002B(chipAddress, chipBank, chipData); @@ -119,6 +119,16 @@ public static CartridgeDevice Load(byte[] crtFile) return result; } + private static List BuildChipList(IList addresses, IList banks, IList data) => + Enumerable.Range(0, addresses.Count) + .Select(i => new CartridgeChip + { + Address = addresses[i], + Bank = banks[i], + Data = data[i] + }) + .ToList(); + private static int ReadCRTShort(BinaryReader reader) { return (reader.ReadByte() << 8) | @@ -256,6 +266,9 @@ public virtual void WriteDF00(int addr, int val) { } + public virtual IEnumerable CreateMemoryDomains() => + Array.Empty(); + private bool _driveLightEnabled; private bool _driveLightOn; diff --git a/src/BizHawk.Emulation.Cores/Computers/Commodore64/Cartridge/CartridgePort.cs b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Cartridge/CartridgePort.cs index 8c1e0c27e3a..6025cd059f4 100644 --- a/src/BizHawk.Emulation.Cores/Computers/Commodore64/Cartridge/CartridgePort.cs +++ b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Cartridge/CartridgePort.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using BizHawk.Common; using BizHawk.Emulation.Common; @@ -133,6 +134,15 @@ public void SyncState(Serializer ser) ser.EndSection(); } + public ISaveRam SaveRam => _cartridgeDevice as ISaveRam; + + public IEnumerable CreateMemoryDomains() + { + if (_connected) + return _cartridgeDevice.CreateMemoryDomains(); + return Array.Empty(); + } + public bool DriveLightEnabled => _connected && _cartridgeDevice.DriveLightEnabled; public bool DriveLightOn => _connected && _cartridgeDevice.DriveLightOn; diff --git a/src/BizHawk.Emulation.Cores/Computers/Commodore64/Cartridge/Mapper0020.cs b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Cartridge/Mapper0020.cs index 65b20a04e9c..04cc2f1d767 100644 --- a/src/BizHawk.Emulation.Cores/Computers/Commodore64/Cartridge/Mapper0020.cs +++ b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Cartridge/Mapper0020.cs @@ -1,135 +1,170 @@ using System.Collections.Generic; +using System.IO; using System.Linq; - using BizHawk.Common; +using BizHawk.Emulation.Common; namespace BizHawk.Emulation.Cores.Computers.Commodore64.Cartridge { - // EasyFlash cartridge - // No official games came on one of these but there - // are a few dumps from GameBase64 that use this mapper - - // There are 64 banks total, DE00 is bank select. - // Selecing a bank will select both Lo and Hi ROM. - // DE02 will switch exrom/game bits: bit 0=game, - // bit 1=exrom, bit 2=for our cases, always set true. - // These two registers are write only. - - // This cartridge always starts up in Ultimax mode, - // with Game set high and ExRom set low. - - // There is also 256 bytes RAM at DF00-DFFF. - - // We emulate having the AM29F040 chip. - internal sealed class Mapper0020 : CartridgeDevice + /// + /// Implements the EasyFlash cartridge format. + /// + /// The most common EasyFlash implementation uses 2x AM29F040 programmable ROMs + /// and a 256-byte memory. + /// + /// The address bus is 19 bits wide. Bits 18-13 are set by the "bank" + /// register (implemented as a separate bank of flip-flops on the board) and + /// bits 12-0 are set from the system bus. "RomH" and "RomL" are directly + /// tied to the respective chip-enable signals for each flash ROM, which means + /// that address range $8000-$9FFF will correspond to one flash ROM, and $A000-$BFFF + /// (or $E000-$FFFF in UltiMax configuration) will correspond to the other. + /// + /// Control registers are mapped to $DE00 and $DE02. The 256-byte RAM is mapped to $DF00-$DFFF. + /// + /// + /// Two registers can be accessed: + /// + /// $DE00 - bank register (bits: 00BBBBBB) + /// B = bank ($00-$3F) + /// + /// $DE02 - control register (bits: L0000MXG) + /// L = light control + /// M = Game pin control; 1=software controlled, 0=onboard jumper controlled + /// X = ExRom pin level; 1=low, 0=high + /// G = Game pin level; 1=low, 0=high + /// + internal sealed class Mapper0020 : CartridgeDevice, ISaveRam { - private int _bankOffset = 63 << 13; + private readonly byte[] _originalMediaA; // 8000 + private readonly byte[] _originalMediaB; // A000 - private int[] _banksA = new int[64 << 13]; // 8000 - private int[] _banksB = new int[64 << 13]; // A000 + private byte[] _deltaA; // 8000 + private byte[] _deltaB; // A000 - private readonly int[] _originalMediaA; // 8000 - private readonly int[] _originalMediaB; // A000 + private readonly Am29F040B _chipA = new(); + private readonly Am29F040B _chipB = new(); + + private bool _saveRamDirty; private bool _boardLed; - private bool _jumper; - private int _stateBits; - private int[] _ram = new int[256]; - - private bool _commandLatch55; - private bool _commandLatchAa; - - private int _internalRomState; + private byte[] _ram = new byte[256]; + private int _bankNumber; - public Mapper0020(IList newAddresses, IList newBanks, IList newData) + public Mapper0020(IReadOnlyList chips) { DriveLightEnabled = true; - var count = newAddresses.Count; + var count = chips.Count; // force ultimax mode (the cart SHOULD set this // otherwise on load, according to the docs) pinGame = false; pinExRom = true; - // for safety, initialize all banks to dummy - for (var i = 0; i < 64 * 0x2000; i++) - { - _banksA[i] = 0xFF; - _banksB[i] = 0xFF; - } - // load in all banks - for (var i = 0; i < count; i++) + foreach (var chip in chips) { - switch (newAddresses[i]) + switch (chip.Address) { case 0x8000: - Array.Copy(newData[i], 0, _banksA, newBanks[i] * 0x2000, 0x2000); + chip.Data.Select(b => unchecked((byte)b)) + .ToArray() + .CopyTo(_chipA.Data.Slice(chip.Bank * 0x2000, 0x2000)); break; case 0xA000: case 0xE000: - Array.Copy(newData[i], 0, _banksB, newBanks[i] * 0x2000, 0x2000); + chip.Data.Select(b => unchecked((byte)b)) + .ToArray() + .CopyTo(_chipB.Data.Slice(chip.Bank * 0x2000, 0x2000)); break; } } // default to bank 0 - BankSet(0); - - // internal operation settings - _commandLatch55 = false; - _commandLatchAa = false; - _internalRomState = 0; + _bankNumber = 0; // back up original media - _originalMediaA = _banksA.Select(d => d).ToArray(); - _originalMediaB = _banksB.Select(d => d).ToArray(); + _originalMediaA = _chipA.Data.ToArray(); + _originalMediaB = _chipB.Data.ToArray(); + } + + public override void HardReset() + { + _chipA.Reset(); + _chipB.Reset(); + base.HardReset(); + } + + private void FlushSaveRam() + { + if (_chipA.CheckDataDirty() || _deltaA == null) + _deltaA = DeltaSerializer.GetDelta(_originalMediaA, _chipA.Data).ToArray(); + + if (_chipB.CheckDataDirty() || _deltaB == null) + _deltaB = DeltaSerializer.GetDelta(_originalMediaB, _chipB.Data).ToArray(); + + _saveRamDirty = false; } protected override void SyncStateInternal(Serializer ser) { - ser.Sync("BankOffset", ref _bankOffset); + if (!ser.IsReader) + FlushSaveRam(); + + ser.Sync("BankNumber", ref _bankNumber); ser.Sync("BoardLed", ref _boardLed); ser.Sync("Jumper", ref _jumper); ser.Sync("StateBits", ref _stateBits); ser.Sync("RAM", ref _ram, useNull: false); - ser.Sync("CommandLatch55", ref _commandLatchAa); - ser.Sync("CommandLatchAA", ref _commandLatchAa); - ser.Sync("InternalROMState", ref _internalRomState); - ser.SyncDelta("MediaStateA", _originalMediaA, _banksA); - ser.SyncDelta("MediaStateB", _originalMediaB, _banksB); - DriveLightOn = _boardLed; - } + ser.Sync("MediaStateA", ref _deltaA, useNull: false); + ser.Sync("MediaStateB", ref _deltaB, useNull: false); - private void BankSet(int index) - { - _bankOffset = (index & 0x3F) << 13; - } + ser.BeginSection("FlashA"); + _chipA.SyncState(ser, withData: false); + ser.EndSection(); - public override int Peek8000(int addr) - { - addr &= 0x1FFF; - return _banksA[addr | _bankOffset]; - } + ser.BeginSection("FlashB"); + _chipB.SyncState(ser, withData: false); + ser.EndSection(); - public override int PeekA000(int addr) - { - addr &= 0x1FFF; - return _banksB[addr | _bankOffset]; + if (ser.IsReader) + { + if (_deltaA != null) + DeltaSerializer.ApplyDelta(_originalMediaA, _chipA.Data, _deltaA); + + if (_deltaB != null) + DeltaSerializer.ApplyDelta(_originalMediaB, _chipB.Data, _deltaB); + } + + DriveLightOn = _boardLed; } + private int CalculateBankOffset(int addr) => + (addr & 0x1FFF) | (_bankNumber << 13); + + public override int Peek8000(int addr) => + _chipA.Peek(CalculateBankOffset(addr)); + + public override int PeekA000(int addr) => + _chipB.Peek(CalculateBankOffset(addr)); + public override int PeekDE00(int addr) { // normally you can't read these regs // but Peek is provided here for debug reasons // and may not stay around addr &= 0x02; - return addr == 0x00 ? _bankOffset >> 13 : _stateBits; + return addr == 0x00 ? _bankNumber : _stateBits; } + public override void Poke8000(int addr, int val) => + _chipA.Poke(addr, val); + + public override void PokeA000(int addr, int val) => + _chipB.Poke(addr, val); + public override int PeekDF00(int addr) { addr &= 0xFF; @@ -141,7 +176,7 @@ public override void PokeDE00(int addr, int val) addr &= 0x02; if (addr == 0x00) { - BankSet(val); + _bankNumber = val & 0x3F; } else { @@ -152,18 +187,14 @@ public override void PokeDE00(int addr, int val) public override void PokeDF00(int addr, int val) { addr &= 0xFF; - _ram[addr] = val & 0xFF; + _ram[addr] = unchecked((byte)val); } - public override int Read8000(int addr) - { - return ReadInternal(addr & 0x1FFF, _banksA); - } + public override int Read8000(int addr) => + _chipA.Read(CalculateBankOffset(addr)); - public override int ReadA000(int addr) - { - return ReadInternal(addr & 0x1FFF, _banksB); - } + public override int ReadA000(int addr) => + _chipB.Read(CalculateBankOffset(addr)); public override int ReadDF00(int addr) { @@ -171,33 +202,6 @@ public override int ReadDF00(int addr) return _ram[addr]; } - private int ReadInternal(int addr, int[] bank) - { - switch (_internalRomState) - { - case 0x80: - break; - case 0x90: - switch (addr & 0x1FFF) - { - case 0x0000: - return 0x01; - case 0x0001: - return 0xA4; - case 0x0002: - return 0x00; - } - - break; - case 0xA0: - break; - case 0xF0: - break; - } - - return bank[addr | _bankOffset]; - } - private void StateSet(int val) { _stateBits = val &= 0x87; @@ -212,83 +216,23 @@ private void StateSet(int val) pinExRom = (val & 0x02) == 0; _boardLed = (val & 0x80) != 0; - _internalRomState = 0; DriveLightOn = _boardLed; } public override void Write8000(int addr, int val) { - WriteInternal(addr, val); - } + if (pinGame || !pinExRom) + return; - public override void WriteA000(int addr, int val) - { - WriteInternal(addr | 0x2000, val); + _chipA.Write(CalculateBankOffset(addr), val); } - private void WriteInternal(int addr, int val) + public override void WriteA000(int addr, int val) { if (pinGame || !pinExRom) - { return; - } - if (val == 0xF0) // any address, resets flash - { - _internalRomState = 0; - _commandLatch55 = false; - _commandLatchAa = false; - } - else if (_internalRomState != 0x00 && _internalRomState != 0xF0) - { - switch (_internalRomState) - { - case 0xA0: - if ((addr & 0x2000) == 0) - { - addr &= 0x1FFF; - _banksA[addr | _bankOffset] = val & 0xFF; - } - else - { - addr &= 0x1FFF; - _banksB[addr | _bankOffset] = val & 0xFF; - } - - break; - } - } - else if (addr == 0x0555) // $8555 - { - if (!_commandLatchAa) - { - if (val == 0xAA) - { - _commandLatch55 = true; - } - } - else - { - // process EZF command - _internalRomState = val; - } - } - else if (addr == 0x02AA) // $82AA - { - if (_commandLatch55 && val == 0x55) - { - _commandLatchAa = true; - } - else - { - _commandLatch55 = false; - } - } - else - { - _commandLatch55 = false; - _commandLatchAa = false; - } + _chipB.Write(CalculateBankOffset(addr), val); } public override void WriteDE00(int addr, int val) @@ -296,7 +240,7 @@ public override void WriteDE00(int addr, int val) addr &= 0x02; if (addr == 0x00) { - BankSet(val); + _bankNumber = val & 0x3F; } else { @@ -306,7 +250,65 @@ public override void WriteDE00(int addr, int val) public override void WriteDF00(int addr, int val) { - _ram[addr] = val & 0xFF; + _ram[addr] = unchecked((byte)val); + } + + public override void ExecutePhase() + { + _chipA.Clock(); + _chipB.Clock(); + _saveRamDirty |= _chipA.IsDataDirty | _chipB.IsDataDirty; + } + + public override IEnumerable CreateMemoryDomains() + { + yield return _chipA.CreateMemoryDomain("EF LoROM"); + yield return _chipB.CreateMemoryDomain("EF HiROM"); + + yield return new MemoryDomainByteArray( + name: "EF RAM", + endian: MemoryDomain.Endian.Little, + data: _ram, + writable: true, + wordSize: 1 + ); } + + public byte[] CloneSaveRam() + { + FlushSaveRam(); + + using var result = new MemoryStream(); + using var writer = new BinaryWriter(result); + + writer.Write(_deltaA.Length); + writer.Write(_deltaA); + writer.Write(_deltaB.Length); + writer.Write(_deltaB); + writer.Flush(); + + _saveRamDirty = false; + return result.ToArray(); + } + + /// + /// Applies a SaveRam block to the flash memory. + /// + public void StoreSaveRam(byte[] data) + { + using var stream = new MemoryStream(data); + using var reader = new BinaryReader(stream); + + var deltaASize = reader.ReadInt32(); + _deltaA = reader.ReadBytes(deltaASize); + var deltaBSize = reader.ReadInt32(); + _deltaB = reader.ReadBytes(deltaBSize); + + DeltaSerializer.ApplyDelta(_originalMediaA, _chipA.Data, _deltaA); + DeltaSerializer.ApplyDelta(_originalMediaB, _chipB.Data, _deltaB); + _saveRamDirty = false; + } + + public bool SaveRamModified => _saveRamDirty; } }