Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[C64] Fix memory performance issues #4152

Merged
merged 3 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 12 additions & 108 deletions src/BizHawk.Emulation.Cores/Computers/Commodore64/Media/Disk.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using System.Linq;

using BizHawk.Common;

namespace BizHawk.Emulation.Cores.Computers.Commodore64.Media
Expand All @@ -9,21 +9,18 @@ public sealed class Disk
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 bool[] _usedTracks;
private readonly DiskTrack[] _tracks;
public bool Valid;
public bool WriteProtected;

/// <summary>
/// Create a blank, unformatted disk.
/// </summary>
public Disk(int trackCapacity)
public Disk(int trackCount)
{
WriteProtected = false;
_tracks = new int[trackCapacity][];
_tracks = new DiskTrack[trackCount];
FillMissingTracks();
_originalMedia = _tracks.Select(t => (int[])t.Clone()).ToArray();
Valid = true;
}

Expand All @@ -37,76 +34,16 @@ public Disk(int trackCapacity)
public Disk(IList<byte[]> trackData, IList<int> trackNumbers, IList<int> trackDensities, int trackCapacity)
{
WriteProtected = true;
_tracks = new int[trackCapacity][];
_tracks = new DiskTrack[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<long>();
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()
Expand All @@ -116,52 +53,19 @@ private void FillMissingTracks()
{
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];
}
}
}

public void AttachTracker(bool[] usedTracks)
{
if (_tracks.Length != usedTracks.Length)
{
throw new InvalidOperationException("track and tracker length mismatch! (this should be impossible, please report)");
_tracks[i] ??= new();
}

_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;
return _tracks[halftrack];
}
public IReadOnlyList<DiskTrack> Tracks
=> _tracks;

public void SyncState(Serializer ser)
{
Expand Down
164 changes: 164 additions & 0 deletions src/BizHawk.Emulation.Cores/Computers/Commodore64/Media/DiskTrack.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
using System.Buffers;

namespace BizHawk.Emulation.Cores.Computers.Commodore64.Media;

/// <summary>
/// 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.
/// </summary>
public sealed class DiskTrack
{
/// <summary>
/// The master clock rate for synchronization.
/// </summary>
private const int ClockRateHz = 16000000;

/// <summary>
/// Number of bytes per element in the Bits array.
/// </summary>
private const int BytesPerEntry = sizeof(int);

/// <summary>
/// Number of bits contained in a single value of the Bits array.
/// </summary>
public const int FluxBitsPerEntry = BytesPerEntry * 8;

/// <summary>
/// The number of flux transition bits stored for each track.
/// </summary>
private const int FluxBitsPerTrack = ClockRateHz / 5;

/// <summary>
/// The fixed size of the Bits array.
/// </summary>
private const int FluxEntriesPerTrack = FluxBitsPerTrack / FluxBitsPerEntry;

private int[] _bits = new int[FluxEntriesPerTrack];
private int[] _original = new int[FluxEntriesPerTrack];

/// <summary>
/// Current state of the disk, which may be changed from the original media.
/// </summary>
public Span<int> Bits => _bits;

/// <summary>
/// Fixed state of the original media, from which deltas will be calculated.
/// </summary>
public ReadOnlySpan<int> Original => _original;

/// <summary>
/// Create a clone of the DiskTrack.
/// </summary>
/// <returns>
/// A new DiskTrack with an identical copy of <see cref="Bits"/>.
/// </returns>
public DiskTrack Clone()
{
var clone = new DiskTrack();
Bits.CopyTo(clone._bits.AsSpan());
clone._original = _original;
return clone;
}

/// <summary>
/// Check to see if the original bits and current bits are equivalent.
/// </summary>
/// <returns>
/// True only if the content differs.
/// </returns>
public bool IsModified()
=> !_original.AsSpan().SequenceEqual(_bits);

/// <summary>
/// Resets this track to the state of the original media.
/// </summary>
public void Reset()
{
_original.CopyTo(_bits.AsSpan());
}

/// <summary>
/// Write an entry to <see cref="Bits"/>.
/// </summary>
/// <param name="index">
/// Index of the entry to write.
/// </param>
/// <param name="bits">
/// The new content of the entry.
/// </param>
/// <returns>
/// True only if data in <see cref="Bits"/> has been altered.
/// </returns>
public bool Write(int index, int bits)
{
// We only need to update delta if the bits actually changed.

if (_bits[index] == bits) return false;

_bits[index] = bits;
return true;
}

public void ReadFromGCR(int density, ReadOnlySpan<byte> 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<byte>.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;
}
}

_bits.CopyTo(_original.AsSpan());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ private void AdvanceRng()

private void ExecuteFlux()
{
var track = _disk?.Tracks[_trackNumber];
var bits = track == null ? Span<int>.Empty : track.Bits;

// This actually executes the main 16mhz clock
while (_clocks > 0)
{
Expand All @@ -55,19 +58,21 @@ private void ExecuteFlux()
{
if (_diskBitsLeft <= 0)
{
if (_diskWriteEnabled)
_trackImageData[_diskByteOffset] = _diskOutputBits;
if (_diskWriteEnabled && track.Write(_diskByteOffset, _diskOutputBits))
{
_dirtyDiskTracks[_getCurrentDiskNumber()][_trackNumber] = true;
}

_diskByteOffset++;

if (_diskByteOffset == Disk.FluxEntriesPerTrack)
_diskByteOffset = 0;

if (!_diskWriteEnabled)
_diskBits = _trackImageData[_diskByteOffset];
_diskBits = bits[_diskByteOffset];

_diskOutputBits = 0;
_diskBitsLeft = Disk.FluxBitsPerEntry;
_diskBitsLeft = DiskTrack.FluxBitsPerEntry;
}
}
_diskOutputBits >>= 1;
Expand Down
Loading
Loading