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);
+ });
+ }
}
}