diff --git a/src/BizHawk.Emulation.Cores/Computers/Commodore64/Media/Disk.cs b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Media/Disk.cs index 5405b07856..d148d1971e 100644 --- a/src/BizHawk.Emulation.Cores/Computers/Commodore64/Media/Disk.cs +++ b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Media/Disk.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using System.Linq; + using BizHawk.Common; namespace BizHawk.Emulation.Cores.Computers.Commodore64.Media @@ -9,8 +9,7 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Media public const int FluxBitsPerEntry = 32; public const int FluxBitsPerTrack = 16000000 / 5; public const int FluxEntriesPerTrack = FluxBitsPerTrack / FluxBitsPerEntry; - private readonly int[][] _tracks; - private readonly int[][] _originalMedia; + private readonly DiskTrack[] _tracks; private bool[] _usedTracks; public bool Valid; public bool WriteProtected; @@ -18,12 +17,12 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Media /// /// Create a blank, unformatted disk. /// - public Disk(int trackCapacity) + public Disk(int trackCount) { WriteProtected = false; - _tracks = new int[trackCapacity][]; + _tracks = new DiskTrack[trackCount]; + _usedTracks = new bool[trackCount]; FillMissingTracks(); - _originalMedia = _tracks.Select(t => (int[])t.Clone()).ToArray(); Valid = true; } @@ -37,76 +36,17 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Media public Disk(IList trackData, IList trackNumbers, IList trackDensities, int trackCapacity) { WriteProtected = true; - _tracks = new int[trackCapacity][]; + _tracks = new DiskTrack[trackCapacity]; + _usedTracks = new bool[trackCapacity]; for (var i = 0; i < trackData.Count; i++) { - _tracks[trackNumbers[i]] = ConvertToFluxTransitions(trackDensities[i], trackData[i], 0); + var track = new DiskTrack(); + track.ReadFromGCR(trackDensities[i], trackData[i], 0); + _tracks[trackNumbers[i]] = track; } FillMissingTracks(); Valid = true; - _originalMedia = _tracks.Select(t => (int[])t.Clone()).ToArray(); - } - - private int[] ConvertToFluxTransitions(int density, byte[] bytes, int fluxBitOffset) - { - var paddedLength = bytes.Length; - switch (density) - { - case 3: - paddedLength = Math.Max(bytes.Length, 7692); - break; - case 2: - paddedLength = Math.Max(bytes.Length, 7142); - break; - case 1: - paddedLength = Math.Max(bytes.Length, 6666); - break; - case 0: - paddedLength = Math.Max(bytes.Length, 6250); - break; - } - - paddedLength++; - var paddedBytes = new byte[paddedLength]; - Array.Copy(bytes, paddedBytes, bytes.Length); - for (var i = bytes.Length; i < paddedLength; i++) - { - paddedBytes[i] = 0xAA; - } - var result = new int[FluxEntriesPerTrack]; - var lengthBits = (paddedLength * 8) - 7; - var offsets = new List(); - var remainingBits = lengthBits; - - const long bitsNum = FluxEntriesPerTrack * FluxBitsPerEntry; - long bitsDen = lengthBits; - - for (var i = 0; i < paddedLength; i++) - { - var byteData = paddedBytes[i]; - for (var j = 0; j < 8; j++) - { - var offset = fluxBitOffset + ((i * 8 + j) * bitsNum / bitsDen); - var byteOffset = (int)(offset / FluxBitsPerEntry); - var bitOffset = (int)(offset % FluxBitsPerEntry); - offsets.Add(offset); - result[byteOffset] |= ((byteData & 0x80) != 0 ? 1 : 0) << bitOffset; - byteData <<= 1; - remainingBits--; - if (remainingBits <= 0) - { - break; - } - } - - if (remainingBits <= 0) - { - break; - } - } - - return result; } private void FillMissingTracks() @@ -116,52 +56,35 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Media { if (_tracks[i] == null && _tracks[i - 1] != null) { - _tracks[i] = new int[FluxEntriesPerTrack]; - Array.Copy(_tracks[i - 1], _tracks[i], FluxEntriesPerTrack); + _tracks[i] = _tracks[i - 1].Clone(); } } // Fill vacant tracks for (var i = 0; i < _tracks.Length; i++) { - if (_tracks[i] == null) - { - _tracks[i] = new int[FluxEntriesPerTrack]; - } + _tracks[i] ??= new(); } } - 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) + public void DeltaUpdate(Action deltaUpdateCallback) { for (var i = 0; i < _tracks.Length; i++) { if (_usedTracks[i]) { - deltaUpdateCallback(i, _originalMedia[i], _tracks[i]); + deltaUpdateCallback(i, _tracks[i]); } } } - public int[] GetDataForTrack(int halftrack) - { - _usedTracks[halftrack] = true; - return _tracks[halftrack]; - } + public DiskTrack GetTrack(int trackNumber) + => _tracks[trackNumber]; public void SyncState(Serializer ser) { diff --git a/src/BizHawk.Emulation.Cores/Computers/Commodore64/Media/DiskTrack.cs b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Media/DiskTrack.cs new file mode 100644 index 0000000000..6484f23726 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Media/DiskTrack.cs @@ -0,0 +1,217 @@ +using System.Buffers; + +using BizHawk.Common; + +namespace BizHawk.Emulation.Cores.Computers.Commodore64.Media; + +/// +/// Represents the magnetic flux transitions for one rotation of floppy disk media. Each bit represents +/// the transition of the signal level from 1 to 0, or from 0 to 1. +/// +public sealed class DiskTrack +{ + /// + /// The master clock rate for synchronization. + /// + private const int ClockRateHz = 16000000; + + /// + /// Number of bytes per element in the Bits array. + /// + private const int BytesPerEntry = sizeof(int); + + /// + /// Number of bits contained in a single value of the Bits array. + /// + private const int FluxBitsPerEntry = BytesPerEntry * 8; + + /// + /// The number of flux transition bits stored for each track. + /// + private const int FluxBitsPerTrack = ClockRateHz / 5; + + /// + /// The fixed size of the Bits array. + /// + private const int FluxEntriesPerTrack = FluxBitsPerTrack / FluxBitsPerEntry; + + /// + /// The number of bytes contained in the cached delta, for use with save states. + /// + private const int DeltaBytesPerTrack = FluxEntriesPerTrack * BytesPerEntry + 4; + + private int[] _bits = new int[FluxEntriesPerTrack]; + private int[] _original = new int[FluxEntriesPerTrack]; + private byte[] _delta = new byte[DeltaBytesPerTrack]; + private bool _dirty = true; + private bool _modified = false; + + /// + /// Current state of the disk, which may be changed from the original media. + /// + public ReadOnlySpan Bits => _bits; + + /// + /// Fixed state of the original media, from which deltas will be calculated. + /// + public ReadOnlySpan Original => _original; + + /// + /// The compressed difference between + /// + public byte[] Delta => _delta; + + /// + /// If true, the delta needs to be recalculated. + /// + public bool IsDirty => _dirty; + + /// + /// If true, the track data has been modified. + /// + public bool IsModified => _modified; + + /// + /// Create a clone of the DiskTrack. + /// + /// + /// A new DiskTrack with an identical copy of . + /// + public DiskTrack Clone() + { + var clone = new DiskTrack(); + Bits.CopyTo(clone._bits.AsSpan()); + clone._original = _original; + return clone; + } + + /// + /// Prepare the property. + /// + /// + /// The new value of . + /// + private bool CheckModified() + => _modified = !_original.AsSpan().SequenceEqual(_bits); + + /// + /// Apply a compressed delta over the original media. + /// + /// + /// Compressed delta data. + /// + public void ApplyDelta(ReadOnlySpan delta) + { + DeltaSerializer.ApplyDelta(_original, _bits, delta); + _delta = delta.ToArray(); + _dirty = false; + CheckModified(); + } + + /// + /// Updates the delta for this track. + /// + /// + /// True if the delta has updated, false otherwise. + /// + public bool UpdateDelta() + { + if (!_dirty) return false; + + _delta = DeltaSerializer.GetDelta(_original, _bits).ToArray(); + _dirty = false; + return true; + } + + /// + /// Resets this track to the state of the original media. + /// + public void Reset() + { + _original.CopyTo(_bits.AsSpan()); + _delta = Array.Empty(); + _dirty = false; + } + + /// + /// Synchronize state. + /// + /// + /// Serializer with which to synchronize. + /// + public void SyncState(Serializer ser, string deltaId) + { + ser.Sync(deltaId, ref _delta, useNull: true); + } + + public void Write(int index, int bits) + { + // We only need to update delta if the bits actually changed. + + if (_bits[index] == bits) return; + + _bits[index] = bits; + _dirty = true; + } + + public void ReadFromGCR(int density, ReadOnlySpan bytes, int fluxBitOffset) + { + // There are four levels of track density correlated with the four different clock dividers + // in the 1541 disk drive. Outer tracks have more surface area, so a technique is used to read + // bits at a higher rate. + + var paddedLength = density switch + { + 3 => Math.Max(bytes.Length, 7692), + 2 => Math.Max(bytes.Length, 7142), + 1 => Math.Max(bytes.Length, 6666), + 0 => Math.Max(bytes.Length, 6250), + _ => bytes.Length + }; + + // One extra byte is added at the end to break up tracks so that if the data is perfectly + // aligned in an unfortunate way, loaders don't seize up trying to find data. Some copy protections + // will read the same track repeatedly to account for variations in drive mechanics, and this should get + // the more temperamental ones to load eventually. + + paddedLength++; + + // It is possible that there are more or fewer bits than the specification due to any number + // of reasons (e.g. copy protection, tiny variations in motor speed) so we pad out with the "default" + // bit pattern. + + using var paddedBytesMem = MemoryPool.Shared.Rent(paddedLength); + var paddedBytes = paddedBytesMem.Memory.Span.Slice(0, paddedLength); + bytes.CopyTo(paddedBytes); + paddedBytes.Slice(bytes.Length).Fill(0xAA); + + var lengthBits = paddedLength * 8 - 7; + var remainingBits = lengthBits; + + const long bitsNum = FluxEntriesPerTrack * FluxBitsPerEntry; + long bitsDen = lengthBits; + + for (var i = 0; i < paddedLength; i++) + { + var byteData = paddedBytes[i]; + for (var j = 0; j < 8; j++) + { + var offset = fluxBitOffset + ((i * 8 + j) * bitsNum / bitsDen); + var byteOffset = (int)(offset / FluxBitsPerEntry); + var bitOffset = (int)(offset % FluxBitsPerEntry); + _bits[byteOffset] |= (byteData >> 7) << bitOffset; + byteData <<= 1; + remainingBits--; + if (remainingBits <= 0) + { + break; + } + } + + if (remainingBits <= 0) + { + break; + } + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Computers/Commodore64/Serial/Drive1541.FluxTransitions.cs b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Serial/Drive1541.FluxTransitions.cs index 2e72c0f3e6..1bf4bc397b 100644 --- a/src/BizHawk.Emulation.Cores/Computers/Commodore64/Serial/Drive1541.FluxTransitions.cs +++ b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Serial/Drive1541.FluxTransitions.cs @@ -38,6 +38,9 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Serial private void ExecuteFlux() { + var track = _disk.GetTrack(_trackNumber); + var bits = track.Bits; + // This actually executes the main 16mhz clock while (_clocks > 0) { @@ -56,7 +59,7 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Serial if (_diskBitsLeft <= 0) { if (_diskWriteEnabled) - _trackImageData[_diskByteOffset] = _diskOutputBits; + track.Write(_diskByteOffset, _diskOutputBits); _diskByteOffset++; @@ -64,7 +67,7 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Serial _diskByteOffset = 0; if (!_diskWriteEnabled) - _diskBits = _trackImageData[_diskByteOffset]; + _diskBits = bits[_diskByteOffset]; _diskOutputBits = 0; _diskBitsLeft = Disk.FluxBitsPerEntry; @@ -197,6 +200,8 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Serial _diskDensityCounter++; _diskCycle = (_diskCycle + 1) & 0xF; } + + if (_diskWriteEnabled && track.UpdateDelta()) SaveDelta(_trackNumber, track.Delta); } } } diff --git a/src/BizHawk.Emulation.Cores/Computers/Commodore64/Serial/Drive1541.SaveRam.cs b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Serial/Drive1541.SaveRam.cs index 6786ffc72c..2ca4cb2f32 100644 --- a/src/BizHawk.Emulation.Cores/Computers/Commodore64/Serial/Drive1541.SaveRam.cs +++ b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Serial/Drive1541.SaveRam.cs @@ -13,20 +13,24 @@ public sealed partial class Drive1541 : ISaveRam // 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][]; + _diskDeltas = new byte[diskCount][][]; for (var i = 0; i < diskCount; i++) { _usedDiskTracks[i] = new bool[84]; + _diskDeltas[i] = new byte[84][]; + for (var j = 0; j < 84; j++) + { + _diskDeltas[i][j] = Array.Empty(); + } } } - public bool SaveRamModified => true; + public bool SaveRamModified { get; private set; } = false; public byte[] CloneSaveRam() { @@ -35,13 +39,13 @@ public sealed partial class Drive1541 : ISaveRam using var ms = new MemoryStream(); using var bw = new BinaryWriter(ms); bw.Write(_usedDiskTracks.Length); - for (var i = 0; i < _usedDiskTracks.Length; i++) + for (var diskNumber = 0; diskNumber < _usedDiskTracks.Length; diskNumber++) { - bw.WriteByteBuffer(_usedDiskTracks[i] + bw.WriteByteBuffer(_usedDiskTracks[diskNumber] .ToUByteBuffer()); - for (var j = 0; j < 84; j++) + for (var trackNumber = 0; trackNumber < 84; trackNumber++) { - bw.WriteByteBuffer(_diskDeltas[i, j]); + bw.WriteByteBuffer(_diskDeltas[diskNumber][trackNumber]); } } @@ -66,38 +70,54 @@ public sealed partial class Drive1541 : ISaveRam _usedDiskTracks[i] = br.ReadByteBuffer(returnNull: false)!.ToBoolBuffer(); for (var j = 0; j < 84; j++) { - _diskDeltas[i, j] = br.ReadByteBuffer(returnNull: true); + _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) => + _disk?.DeltaUpdate((tracknum, track) => { - _diskDeltas[_getCurrentDiskNumber(), tracknum] = DeltaSerializer.GetDelta(original, current) - .ToArray(); + SaveDelta(tracknum, track.Delta); }); } public void LoadDeltas() { - _disk?.DeltaUpdate((tracknum, original, current) => + _disk?.DeltaUpdate((tracknum, track) => { - DeltaSerializer.ApplyDelta(original, current, _diskDeltas[_getCurrentDiskNumber(), tracknum]); + LoadDelta(tracknum, track.Delta); }); } private void ResetDeltas() { - _disk?.DeltaUpdate(static (_, original, current) => + _disk?.DeltaUpdate(static (_, track) => { - original.AsSpan() - .CopyTo(current); + track.Reset(); }); } + + private void SaveDelta(int trackNumber, byte[] delta) + { + SaveRamModified = true; + _diskDeltas[_getCurrentDiskNumber()][trackNumber] = delta; + } + + private void LoadDelta(int trackNumber, byte[] delta) + { + _diskDeltas[_getCurrentDiskNumber()][trackNumber] = delta; + _disk.GetTrack(trackNumber).ApplyDelta(delta); + } + + private void ResetDelta(int trackNumber) + { + SaveRamModified = true; + _diskDeltas[_getCurrentDiskNumber()][trackNumber] = Array.Empty(); + _disk.GetTrack(trackNumber).Reset(); + } } diff --git a/src/BizHawk.Emulation.Cores/Computers/Commodore64/Serial/Drive1541.cs b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Serial/Drive1541.cs index b9aaa241f3..c78a532163 100644 --- a/src/BizHawk.Emulation.Cores/Computers/Commodore64/Serial/Drive1541.cs +++ b/src/BizHawk.Emulation.Cores/Computers/Commodore64/Serial/Drive1541.cs @@ -7,6 +7,7 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Serial { public sealed partial class Drive1541 : SerialPortDevice { + private byte[][][] _diskDeltas; private Disk _disk; private int _bitHistory; private int _bitsRemainingInLatchedByte; @@ -24,7 +25,6 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Serial private int _cpuClockNum; private int _ratioDifference; private int _driveLightOffTime; - private int[] _trackImageData; public Func ReadIec = () => 0xFF; public Action DebuggerStep; public readonly Chip23128 DriveRom; @@ -129,24 +129,19 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Serial SaveDeltas(); } - for (var i = 0; i < _usedDiskTracks.Length; i++) + for (var diskNumber = 0; diskNumber < _usedDiskTracks.Length; diskNumber++) { - ser.Sync($"_usedDiskTracks{i}", ref _usedDiskTracks[i], useNull: false); - for (var j = 0; j < 84; j++) + ser.Sync($"_usedDiskTracks{diskNumber}", ref _usedDiskTracks[diskNumber], useNull: false); + for (var trackNumber = 0; trackNumber < 84; trackNumber++) { - ser.Sync($"DiskDeltas{i},{j}", ref _diskDeltas[i, j], useNull: true); + ser.Sync($"DiskDeltas{diskNumber},{trackNumber}", ref _diskDeltas[diskNumber][trackNumber], 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() @@ -230,7 +225,6 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Serial public void InsertMedia(Disk disk) { _disk = disk; - _disk?.AttachTracker(_usedDiskTracks[_getCurrentDiskNumber()]); UpdateMediaData(); } @@ -238,8 +232,8 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Serial { if (_disk != null) { - _trackImageData = _disk.GetDataForTrack(_trackNumber); - _diskBits = _trackImageData[_diskByteOffset] >> (Disk.FluxBitsPerEntry - _diskBitsLeft); + var track = _disk.GetTrack(_trackNumber); + _diskBits = track.Bits[_diskByteOffset] >> (Disk.FluxBitsPerEntry - _diskBitsLeft); _diskWriteProtected = _disk.WriteProtected; } else @@ -251,7 +245,6 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Serial public void RemoveMedia() { _disk = null; - _trackImageData = null; _diskBits = 0; }