diff --git a/src/BizHawk.Client.Common/config/PathEntryCollection.cs b/src/BizHawk.Client.Common/config/PathEntryCollection.cs index 544af4cd74..32a8066b47 100644 --- a/src/BizHawk.Client.Common/config/PathEntryCollection.cs +++ b/src/BizHawk.Client.Common/config/PathEntryCollection.cs @@ -202,7 +202,7 @@ namespace BizHawk.Client.Common CommonEntriesFor(VSystemID.Raw.Arcade, basePath: Path.Combine(".", "Arcade")), - CommonEntriesFor(VSystemID.Raw.C64, basePath: Path.Combine(".", "C64"), omitSaveRAM: true), + CommonEntriesFor(VSystemID.Raw.C64, basePath: Path.Combine(".", "C64")), CommonEntriesFor(VSystemID.Raw.ChannelF, basePath: Path.Combine(".", "Channel F"), omitSaveRAM: true), diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index b97726f931..4d384a20b0 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -26,6 +26,7 @@ using BizHawk.Emulation.Common.Base_Implementations; using BizHawk.Emulation.Cores; using BizHawk.Emulation.Cores.Arcades.MAME; using BizHawk.Emulation.Cores.Calculators.TI83; +using BizHawk.Emulation.Cores.Computers.Commodore64; using BizHawk.Emulation.Cores.Consoles.NEC.PCE; using BizHawk.Emulation.Cores.Consoles.Nintendo.Ares64; using BizHawk.Emulation.Cores.Consoles.Nintendo.QuickNES; @@ -1921,7 +1922,7 @@ namespace BizHawk.Client.EmuHawk byte[] sram; // some cores might not know how big the saveram ought to be, so just send it the whole file - if (Emulator is MGBAHawk || Emulator is NeoGeoPort || (Emulator is NES && (Emulator as NES).BoardName == "FDS")) + if (Emulator is C64 or MGBAHawk or NeoGeoPort or NES { BoardName: "FDS" }) { sram = File.ReadAllBytes(saveRamPath); } diff --git a/src/BizHawk.Common/DeltaSerializer.cs b/src/BizHawk.Common/DeltaSerializer.cs index 2c4bb11844..b51b2dda39 100644 --- a/src/BizHawk.Common/DeltaSerializer.cs +++ b/src/BizHawk.Common/DeltaSerializer.cs @@ -5,30 +5,42 @@ namespace BizHawk.Common { /// /// Serializes deltas between data, mainly for ROM like structures which are actually writable, and therefore the differences need to be saved - /// Uses simple RLE encoding to keep the size down + /// Uses a simple delta format in order to keep size down + /// DELTA FORMAT DETAILS FOLLOWS + /// The format comprises of an indeterminate amount of blocks. These blocks start with a 4 byte header. This header is read as a native endian 32-bit two's complement signed integer. + /// If the header is positive, then the header indicates the amount of bytes which are identical between the original and current spans. + /// Positive headers are blocks by themselves, so the next header will proceed immediately after a positive header. + /// If the header is negative, then the header indicates the negation of the amount of bytes which differ between the original and current spans. + /// A negative header will have the negated header amount of bytes proceed it, which will be the bitwise XOR between the original and differing bytes. + /// A header of -0x80000000 is considered ill-formed. + /// This format does not stipulate requirements for whether blocks of non-differing bytes necessarily will use a positive header. + /// Thus, an implementation is free to use negative headers only, although without combination of positive headers, this will obviously not have great results wrt final size. + /// More practically, an implementation may want to avoid using positive headers when the block is rather small (e.g. smaller than the header itself, and thus not shrinking the result). + /// Subsequently, it may not mind putting some identical bytes within the negative header's block. + /// XORing the same values result in 0, so doing this will not leave trace of the original data. /// public static class DeltaSerializer { - public static ReadOnlySpan GetDelta(ReadOnlySpan original, ReadOnlySpan data) + public static ReadOnlySpan GetDelta(ReadOnlySpan original, ReadOnlySpan current) where T : unmanaged { var orignalAsBytes = MemoryMarshal.AsBytes(original); - var dataAsBytes = MemoryMarshal.AsBytes(data); + var currentAsBytes = MemoryMarshal.AsBytes(current); - if (orignalAsBytes.Length != dataAsBytes.Length) + if (orignalAsBytes.Length != currentAsBytes.Length) { - throw new InvalidOperationException($"{nameof(orignalAsBytes.Length)} must equal {nameof(dataAsBytes.Length)}"); + throw new InvalidOperationException($"{nameof(orignalAsBytes.Length)} must equal {nameof(currentAsBytes.Length)}"); } var index = 0; - var end = dataAsBytes.Length; + var end = currentAsBytes.Length; var ret = new byte[end + 4].AsSpan(); // worst case scenario size (i.e. everything is different) var retSize = 0; while (index < end) { var blockStart = index; - while (index < end && orignalAsBytes[index] == dataAsBytes[index]) + while (index < end && orignalAsBytes[index] == currentAsBytes[index]) { index++; } @@ -40,7 +52,7 @@ namespace BizHawk.Common var different = 0; while (index < end && same < 8) // in case we hit end of span before, this does nothing and different is 0 { - if (orignalAsBytes[index] != dataAsBytes[index]) + if (orignalAsBytes[index] != currentAsBytes[index]) { different++; same = 0; // note: same is set to 0 on first iteration @@ -68,7 +80,7 @@ namespace BizHawk.Common retSize += 4; for (var i = blockStart; i < index - same; i++) { - ret[retSize++] = (byte)(orignalAsBytes[i] ^ dataAsBytes[i]); + ret[retSize++] = (byte)(orignalAsBytes[i] ^ currentAsBytes[i]); } } @@ -76,7 +88,7 @@ namespace BizHawk.Common { if (same == 8) { - while (index < end && orignalAsBytes[index] == dataAsBytes[index]) + while (index < end && orignalAsBytes[index] == currentAsBytes[index]) { index++; } @@ -97,19 +109,19 @@ namespace BizHawk.Common return ret.Slice(0, retSize); } - public static void ApplyDelta(ReadOnlySpan original, Span data, ReadOnlySpan delta) + public static void ApplyDelta(ReadOnlySpan original, Span current, ReadOnlySpan delta) where T : unmanaged { var orignalAsBytes = MemoryMarshal.AsBytes(original); - var dataAsBytes = MemoryMarshal.AsBytes(data); + var currentAsBytes = MemoryMarshal.AsBytes(current); - if (orignalAsBytes.Length != dataAsBytes.Length) + if (orignalAsBytes.Length != currentAsBytes.Length) { - throw new InvalidOperationException($"{nameof(orignalAsBytes.Length)} must equal {nameof(dataAsBytes.Length)}"); + throw new InvalidOperationException($"{nameof(orignalAsBytes.Length)} must equal {nameof(currentAsBytes.Length)}"); } var dataPos = 0; - var dataEnd = dataAsBytes.Length; + var dataEnd = currentAsBytes.Length; var deltaPos = 0; var deltaEnd = delta.Length; @@ -126,14 +138,14 @@ namespace BizHawk.Common { header = -header; - if (dataEnd - dataPos < header || deltaEnd - deltaPos < header) + if (header == int.MinValue || dataEnd - dataPos < header || deltaEnd - deltaPos < header) { throw new InvalidOperationException("Corrupt delta header!"); } for (var i = 0; i < header; i++) { - dataAsBytes[dataPos + i] = (byte)(orignalAsBytes[dataPos + i] ^ delta[deltaPos + i]); + currentAsBytes[dataPos + i] = (byte)(orignalAsBytes[dataPos + i] ^ delta[deltaPos + i]); } deltaPos += header; @@ -145,7 +157,7 @@ namespace BizHawk.Common throw new InvalidOperationException("Corrupt delta header!"); } - orignalAsBytes.Slice(dataPos, header).CopyTo(dataAsBytes.Slice(dataPos, header)); + orignalAsBytes.Slice(dataPos, header).CopyTo(currentAsBytes.Slice(dataPos, header)); } dataPos += header; diff --git a/src/BizHawk.Common/Serializer.cs b/src/BizHawk.Common/Serializer.cs index cced1c3d10..01f86ff22c 100644 --- a/src/BizHawk.Common/Serializer.cs +++ b/src/BizHawk.Common/Serializer.cs @@ -748,18 +748,18 @@ namespace BizHawk.Common } } - public void SyncDelta(string name, T[] original, T[] data) + public void SyncDelta(string name, T[] original, T[] current) where T : unmanaged { if (IsReader) { var delta = Array.Empty(); Sync(name, ref delta, useNull: false); - DeltaSerializer.ApplyDelta(original, data, delta); + DeltaSerializer.ApplyDelta(original, current, delta); } else { - var delta = DeltaSerializer.GetDelta(original, data).ToArray(); // TODO: don't create array here (need .net update to write span to binary writer) + var delta = DeltaSerializer.GetDelta(original, current).ToArray(); // TODO: don't create array here (need .net update to write span to binary writer) Sync(name, ref delta, useNull: false); } } diff --git a/src/BizHawk.Emulation.Cores/Computers/Commodore64/C64.IStatable.cs b/src/BizHawk.Emulation.Cores/Computers/Commodore64/C64.IStatable.cs index 58bc19667f..51a61d24b8 100644 --- a/src/BizHawk.Emulation.Cores/Computers/Commodore64/C64.IStatable.cs +++ b/src/BizHawk.Emulation.Cores/Computers/Commodore64/C64.IStatable.cs @@ -15,7 +15,8 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64 ser.Sync(nameof(CurrentDisk), ref _currentDisk); if (oldDisk != _currentDisk) { - InitDisk(); + // don't use InitDisk here, no need to load in soon to be overwritten deltas + InitMedia(_roms[_currentDisk]); } ser.Sync("PreviousDiskPressed", ref _prevPressed); ser.Sync("NextDiskPressed", ref _nextPressed); diff --git a/src/BizHawk.Emulation.Cores/Computers/Commodore64/C64.Motherboard.cs b/src/BizHawk.Emulation.Cores/Computers/Commodore64/C64.Motherboard.cs index 2fcb376412..2f0001e7e4 100644 --- a/src/BizHawk.Emulation.Cores/Computers/Commodore64/C64.Motherboard.cs +++ b/src/BizHawk.Emulation.Cores/Computers/Commodore64/C64.Motherboard.cs @@ -129,7 +129,7 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64 { case C64.DiskDriveType.Commodore1541: case C64.DiskDriveType.Commodore1541II: - DiskDrive = new Drive1541(ClockNumerator, ClockDenominator); + DiskDrive = new Drive1541(ClockNumerator, ClockDenominator, () => _c64.CurrentDisk); Serial.Connect(DiskDrive); break; } diff --git a/src/BizHawk.Emulation.Cores/Computers/Commodore64/C64.cs b/src/BizHawk.Emulation.Cores/Computers/Commodore64/C64.cs index 43e1bec4e1..703a7f51a7 100644 --- a/src/BizHawk.Emulation.Cores/Computers/Commodore64/C64.cs +++ b/src/BizHawk.Emulation.Cores/Computers/Commodore64/C64.cs @@ -30,6 +30,12 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64 _cyclesPerFrame = _board.Vic.CyclesPerFrame; _memoryCallbacks = new MemoryCallbackSystem(new[] { "System Bus" }); + if (_board.DiskDrive != null) + { + _board.DiskDrive.InitSaveRam(_roms.Count); + ser.Register(_board.DiskDrive); + } + InitMedia(_roms[_currentDisk]); HardReset(); @@ -172,6 +178,7 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64 private void IncrementDisk() { + _board.DiskDrive.SaveDeltas(); _currentDisk++; if (CurrentDisk >= _roms.Count) { @@ -183,6 +190,7 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64 private void DecrementDisk() { + _board.DiskDrive.SaveDeltas(); _currentDisk--; if (_currentDisk < 0) { @@ -195,12 +203,14 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64 private void InitDisk() { InitMedia(_roms[_currentDisk]); + _board.DiskDrive.LoadDeltas(); } public void SetDisk(int discNum) { if (_currentDisk != discNum) { + _board.DiskDrive.SaveDeltas(); _currentDisk = discNum; InitDisk(); } diff --git a/src/BizHawk.Emulation.Cores/Computers/Commodore64/Media/Disk.cs b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Media/Disk.cs index ff9bfe31ae..3644ff4805 100644 --- a/src/BizHawk.Emulation.Cores/Computers/Commodore64/Media/Disk.cs +++ b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Media/Disk.cs @@ -26,7 +26,6 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Media _tracks = new int[trackCapacity][]; FillMissingTracks(); _originalMedia = _tracks.Select(t => (int[])t.Clone()).ToArray(); - _usedTracks = new bool[trackCapacity]; Valid = true; } @@ -49,7 +48,6 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Media FillMissingTracks(); Valid = true; _originalMedia = _tracks.Select(t => (int[])t.Clone()).ToArray(); - _usedTracks = new bool[trackCapacity]; } private int[] ConvertToFluxTransitions(int density, byte[] bytes, int fluxBitOffset) @@ -135,29 +133,41 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Media } } + public void AttachTracker(bool[] usedTracks) + { + if (_tracks.Length != usedTracks.Length) + { + throw new InvalidOperationException("track and tracker length mismatch! (this should be impossible, please report)"); + } + + _usedTracks = usedTracks; + } + + /// + /// Generic update of the deltas stored in Drive1541's ISaveRam implementation. + /// deltaUpdateCallback will be called for each track which has been possibly dirtied + /// + /// callback + public void DeltaUpdate(Action deltaUpdateCallback) + { + for (var i = 0; i < _tracks.Length; i++) + { + if (_usedTracks[i]) + { + deltaUpdateCallback(i, _originalMedia[i], _tracks[i]); + } + } + } + public int[] GetDataForTrack(int halftrack) { - _usedTracks[halftrack] = true; // TODO: probably can be smarter about this with the WriteProtected flag + _usedTracks[halftrack] = true; return _tracks[halftrack]; } public void SyncState(Serializer ser) { ser.Sync(nameof(WriteProtected), ref WriteProtected); - var oldUsedTracks = _usedTracks; // Sync changes reference if loading state (we don't care in the saving state case) - ser.Sync(nameof(_usedTracks), ref _usedTracks, useNull: false); - - for (var i = 0; i < _tracks.Length; i++) - { - if (_usedTracks[i]) - { - ser.SyncDelta($"MediaState{i}", _originalMedia[i], _tracks[i]); - } - else if (ser.IsReader && oldUsedTracks[i]) // _tracks[i] might be different, but in the state it wasn't, so just copy _originalMedia[i] - { - _originalMedia[i].AsSpan().CopyTo(_tracks[i]); - } - } } } } diff --git a/src/BizHawk.Emulation.Cores/Computers/Commodore64/Serial/Drive1541.cs b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Serial/Drive1541.cs index 1710949cf0..22fd03870a 100644 --- a/src/BizHawk.Emulation.Cores/Computers/Commodore64/Serial/Drive1541.cs +++ b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Serial/Drive1541.cs @@ -1,13 +1,15 @@ using System; +using System.IO; using BizHawk.Common; +using BizHawk.Emulation.Common; using BizHawk.Emulation.Cores.Components.M6502; using BizHawk.Emulation.Cores.Computers.Commodore64.Media; using BizHawk.Emulation.Cores.Computers.Commodore64.MOS; namespace BizHawk.Emulation.Cores.Computers.Commodore64.Serial { - public sealed partial class Drive1541 : SerialPortDevice + public sealed partial class Drive1541 : SerialPortDevice, ISaveRam { private Disk _disk; private int _bitHistory; @@ -51,7 +53,7 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Serial public void WriteMemory(ushort address, byte value) => _drive.Write(address, value); } - public Drive1541(int clockNum, int clockDen) + public Drive1541(int clockNum, int clockDen, Func getCurrentDiskNumber) { DriveRom = new Chip23128(); _cpu = new MOS6502X(new CpuLink(this)) @@ -65,6 +67,7 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Serial _cpuClockNum = clockNum; _driveCpuClockNum = clockDen * 16000000; // 16mhz + _getCurrentDiskNumber = getCurrentDiskNumber; } public override void SyncState(Serializer ser) @@ -101,9 +104,6 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Serial ser.Sync("SystemDriveCpuRatioDifference", ref _ratioDifference); ser.Sync("DriveLightOffTime", ref _driveLightOffTime); - // set _trackImageData back to the correct reference - _trackImageData = _disk?.GetDataForTrack(_trackNumber); - ser.Sync("DiskDensityCounter", ref _diskDensityCounter); ser.Sync("DiskSupplementaryCounter", ref _diskSupplementaryCounter); ser.Sync("DiskFluxReversalDetected", ref _diskFluxReversalDetected); @@ -123,6 +123,34 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Serial ser.Sync("DiskWriteLatch", ref _diskWriteLatch); ser.Sync("DiskOutputBits", ref _diskOutputBits); ser.Sync("DiskWriteProtected", ref _diskWriteProtected); + + if (ser.IsReader) + { + ResetDeltas(); + } + else + { + SaveDeltas(); + } + + for (var i = 0; i < _usedDiskTracks.Length; i++) + { + ser.Sync($"_usedDiskTracks{i}", ref _usedDiskTracks[i], useNull: false); + for (var j = 0; j < 84; j++) + { + ser.Sync($"DiskDeltas{i},{j}", ref _diskDeltas[i, j], useNull: true); + } + } + + _disk?.AttachTracker(_usedDiskTracks[_getCurrentDiskNumber()]); + + if (ser.IsReader) + { + LoadDeltas(); + } + + // set _trackImageData back to the correct reference + _trackImageData = _disk?.GetDataForTrack(_trackNumber); } public override void ExecutePhase() @@ -206,6 +234,7 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Serial public void InsertMedia(Disk disk) { _disk = disk; + _disk?.AttachTracker(_usedDiskTracks[_getCurrentDiskNumber()]); UpdateMediaData(); } @@ -229,5 +258,96 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Serial _trackImageData = null; _diskBits = 0; } + + // ISaveRam implementation + + // this is some extra state used to keep savestate size down, as most tracks don't get used + // we keep it here for all disks as we need to remember it when swapping disks around + // _usedDiskTracks.Length also doubles as a way to remember the disk count + private bool[][] _usedDiskTracks; + private byte[,][] _diskDeltas; + private readonly Func _getCurrentDiskNumber; + + public void InitSaveRam(int diskCount) + { + _usedDiskTracks = new bool[diskCount][]; + _diskDeltas = new byte[diskCount, 84][]; + for (var i = 0; i < diskCount; i++) + { + _usedDiskTracks[i] = new bool[84]; + } + } + + public bool SaveRamModified => true; + + public byte[] CloneSaveRam() + { + SaveDeltas(); // update the current deltas + + using var ms = new MemoryStream(); + using var bw = new BinaryWriter(ms); + bw.Write(_usedDiskTracks.Length); + for (var i = 0; i < _usedDiskTracks.Length; i++) + { + bw.WriteByteBuffer(_usedDiskTracks[i].ToUByteBuffer()); + for (var j = 0; j < 84; j++) + { + bw.WriteByteBuffer(_diskDeltas[i, j]); + } + } + + return ms.ToArray(); + } + + public void StoreSaveRam(byte[] data) + { + using var ms = new MemoryStream(data, false); + using var br = new BinaryReader(ms); + + var ndisks = br.ReadInt32(); + if (ndisks != _usedDiskTracks.Length) + { + throw new InvalidOperationException("Disk count mismatch!"); + } + + ResetDeltas(); + + for (var i = 0; i < _usedDiskTracks.Length; i++) + { + _usedDiskTracks[i] = br.ReadByteBuffer(returnNull: false)!.ToBoolBuffer(); + for (var j = 0; j < 84; j++) + { + _diskDeltas[i, j] = br.ReadByteBuffer(returnNull: true); + } + } + + _disk?.AttachTracker(_usedDiskTracks[_getCurrentDiskNumber()]); + LoadDeltas(); // load up new deltas + _usedDiskTracks[_getCurrentDiskNumber()][_trackNumber] = true; // make sure this gets set to true now + } + + public void SaveDeltas() + { + _disk?.DeltaUpdate((tracknum, original, current) => + { + _diskDeltas[_getCurrentDiskNumber(), tracknum] = DeltaSerializer.GetDelta(original, current).ToArray(); + }); + } + + public void LoadDeltas() + { + _disk?.DeltaUpdate((tracknum, original, current) => + { + DeltaSerializer.ApplyDelta(original, current, _diskDeltas[_getCurrentDiskNumber(), tracknum]); + }); + } + + private void ResetDeltas() + { + _disk?.DeltaUpdate(static (_, original, current) => + { + original.AsSpan().CopyTo(current); + }); + } } }