Add in ISaveRam implementation for C64, using the deltas of disks.

Add in better docs for `DeltaSerializer`.
Fix C64 not remembering disk changes when swapping disks (swapping disks essentially just reset the disk previously)
This commit is contained in:
CasualPokePlayer 2023-04-04 04:28:47 -07:00
parent ece5d2548a
commit b8f3f089f2
9 changed files with 201 additions and 47 deletions

View File

@ -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),

View File

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

View File

@ -5,30 +5,42 @@ namespace BizHawk.Common
{
/// <summary>
/// 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.
/// </summary>
public static class DeltaSerializer
{
public static ReadOnlySpan<byte> GetDelta<T>(ReadOnlySpan<T> original, ReadOnlySpan<T> data)
public static ReadOnlySpan<byte> GetDelta<T>(ReadOnlySpan<T> original, ReadOnlySpan<T> 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<T>(ReadOnlySpan<T> original, Span<T> data, ReadOnlySpan<byte> delta)
public static void ApplyDelta<T>(ReadOnlySpan<T> original, Span<T> current, ReadOnlySpan<byte> 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;

View File

@ -748,18 +748,18 @@ namespace BizHawk.Common
}
}
public void SyncDelta<T>(string name, T[] original, T[] data)
public void SyncDelta<T>(string name, T[] original, T[] current)
where T : unmanaged
{
if (IsReader)
{
var delta = Array.Empty<byte>();
Sync(name, ref delta, useNull: false);
DeltaSerializer.ApplyDelta<T>(original, data, delta);
DeltaSerializer.ApplyDelta<T>(original, current, delta);
}
else
{
var delta = DeltaSerializer.GetDelta<T>(original, data).ToArray(); // TODO: don't create array here (need .net update to write span to binary writer)
var delta = DeltaSerializer.GetDelta<T>(original, current).ToArray(); // TODO: don't create array here (need .net update to write span to binary writer)
Sync(name, ref delta, useNull: false);
}
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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<ISaveRam>(_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();
}

View File

@ -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;
}
/// <summary>
/// Generic update of the deltas stored in Drive1541's ISaveRam implementation.
/// deltaUpdateCallback will be called for each track which has been possibly dirtied
/// </summary>
/// <param name="deltaUpdateCallback">callback</param>
public void DeltaUpdate(Action<int, int[], int[]> 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]);
}
}
}
}
}

View File

@ -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<int> getCurrentDiskNumber)
{
DriveRom = new Chip23128();
_cpu = new MOS6502X<CpuLink>(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<int> _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<int>(original, current).ToArray();
});
}
public void LoadDeltas()
{
_disk?.DeltaUpdate((tracknum, original, current) =>
{
DeltaSerializer.ApplyDelta<int>(original, current, _diskDeltas[_getCurrentDiskNumber(), tracknum]);
});
}
private void ResetDeltas()
{
_disk?.DeltaUpdate(static (_, original, current) =>
{
original.AsSpan().CopyTo(current);
});
}
}
}