remove old rewinder code
This commit is contained in:
parent
84c691021b
commit
dec20a4683
|
@ -1,114 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace BizHawk.Client.Common
|
|
||||||
{
|
|
||||||
public class RewindThreader : IDisposable
|
|
||||||
{
|
|
||||||
private readonly bool _isThreaded;
|
|
||||||
private readonly Action<byte[]> _performCapture;
|
|
||||||
private readonly Action<int> _performRewind;
|
|
||||||
private readonly BlockingCollection<Job> _jobs = new BlockingCollection<Job>(16);
|
|
||||||
private readonly ConcurrentStack<byte[]> _stateBufferPool = new ConcurrentStack<byte[]>();
|
|
||||||
private readonly EventWaitHandle _rewindCompletedEvent;
|
|
||||||
private readonly Thread _thread;
|
|
||||||
|
|
||||||
public RewindThreader(Action<byte[]> performCapture, Action<int> performRewind, bool isThreaded)
|
|
||||||
{
|
|
||||||
_isThreaded = isThreaded;
|
|
||||||
_performCapture = performCapture;
|
|
||||||
_performRewind = performRewind;
|
|
||||||
|
|
||||||
if (_isThreaded)
|
|
||||||
{
|
|
||||||
_rewindCompletedEvent = new EventWaitHandle(false, EventResetMode.AutoReset);
|
|
||||||
_thread = new Thread(ThreadProc) { IsBackground = true };
|
|
||||||
_thread.Start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (!_isThreaded)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_jobs.CompleteAdding();
|
|
||||||
_thread.Join();
|
|
||||||
_rewindCompletedEvent.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Rewind(int frames)
|
|
||||||
{
|
|
||||||
if (!_isThreaded)
|
|
||||||
{
|
|
||||||
_performRewind(frames);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_jobs.Add(new Job
|
|
||||||
{
|
|
||||||
Type = JobType.Rewind,
|
|
||||||
Frames = frames
|
|
||||||
});
|
|
||||||
_rewindCompletedEvent.WaitOne();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Capture(byte[] coreSavestate)
|
|
||||||
{
|
|
||||||
if (!_isThreaded)
|
|
||||||
{
|
|
||||||
_performCapture(coreSavestate);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] savestateCopy;
|
|
||||||
while (_stateBufferPool.TryPop(out savestateCopy) && savestateCopy.Length != coreSavestate.Length)
|
|
||||||
{
|
|
||||||
savestateCopy = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
savestateCopy ??= new byte[coreSavestate.Length];
|
|
||||||
|
|
||||||
Buffer.BlockCopy(coreSavestate, 0, savestateCopy, 0, coreSavestate.Length);
|
|
||||||
|
|
||||||
_jobs.Add(new Job
|
|
||||||
{
|
|
||||||
Type = JobType.Capture,
|
|
||||||
CoreState = savestateCopy
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ThreadProc()
|
|
||||||
{
|
|
||||||
foreach (Job job in _jobs.GetConsumingEnumerable())
|
|
||||||
{
|
|
||||||
if (job.Type == JobType.Capture)
|
|
||||||
{
|
|
||||||
_performCapture(job.CoreState);
|
|
||||||
_stateBufferPool.Push(job.CoreState);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (job.Type == JobType.Rewind)
|
|
||||||
{
|
|
||||||
_performRewind(job.Frames);
|
|
||||||
_rewindCompletedEvent.Set();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum JobType
|
|
||||||
{
|
|
||||||
Capture, Rewind
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class Job
|
|
||||||
{
|
|
||||||
public JobType Type { get; set; }
|
|
||||||
public byte[] CoreState { get; set; }
|
|
||||||
public int Frames { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,396 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using BizHawk.Emulation.Common;
|
|
||||||
|
|
||||||
namespace BizHawk.Client.Common
|
|
||||||
{
|
|
||||||
public class Rewinder : IRewinder
|
|
||||||
{
|
|
||||||
private readonly IStatable _statableCore;
|
|
||||||
|
|
||||||
private const int MaxByteArraySize = 0x7FFFFFC7; // .NET won't let us allocate more than this in one array
|
|
||||||
|
|
||||||
private readonly StreamBlobDatabase _rewindBuffer;
|
|
||||||
private byte[] _rewindBufferBacking;
|
|
||||||
private long _memoryLimit = MaxByteArraySize;
|
|
||||||
private readonly RewindThreader _rewindThread;
|
|
||||||
private byte[] _lastState = new byte[0];
|
|
||||||
private readonly bool _rewindDeltaEnable;
|
|
||||||
private bool _lastRewindLoadedState;
|
|
||||||
private byte[] _deltaBuffer = new byte[0];
|
|
||||||
|
|
||||||
public bool Active => RewindEnabled && !_suspend;
|
|
||||||
|
|
||||||
private bool RewindEnabled { get; }
|
|
||||||
|
|
||||||
private bool _suspend;
|
|
||||||
|
|
||||||
public float FullnessRatio => _rewindBuffer?.FullnessRatio ?? 0;
|
|
||||||
|
|
||||||
public int Count => _rewindBuffer?.Count ?? 0;
|
|
||||||
|
|
||||||
public long Size => _rewindBuffer?.Size ?? 0;
|
|
||||||
|
|
||||||
public int RewindFrequency { get; }
|
|
||||||
|
|
||||||
public Rewinder(IStatable statableCore, IRewindSettings settings)
|
|
||||||
{
|
|
||||||
_statableCore = statableCore ?? throw new ArgumentNullException("Rewinder requires a statable core.");
|
|
||||||
|
|
||||||
int stateSize = _statableCore.CloneSavestate().Length;
|
|
||||||
|
|
||||||
if (stateSize >= settings.LargeStateSize)
|
|
||||||
{
|
|
||||||
RewindEnabled = settings.EnabledLarge;
|
|
||||||
RewindFrequency = settings.FrequencyLarge;
|
|
||||||
}
|
|
||||||
else if (stateSize >= settings.MediumStateSize)
|
|
||||||
{
|
|
||||||
RewindEnabled = settings.EnabledMedium;
|
|
||||||
RewindFrequency = settings.FrequencyMedium;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
RewindEnabled = settings.EnabledSmall;
|
|
||||||
RewindFrequency = settings.FrequencySmall;
|
|
||||||
}
|
|
||||||
|
|
||||||
_rewindDeltaEnable = settings.UseDelta;
|
|
||||||
|
|
||||||
if (Active)
|
|
||||||
{
|
|
||||||
var capacity = settings.BufferSize * 1024L * 1024L;
|
|
||||||
_rewindBuffer = new StreamBlobDatabase(settings.OnDisk, capacity, BufferManage);
|
|
||||||
_rewindThread = new RewindThreader(CaptureInternal, RewindInternal, settings.IsThreaded);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Suspend()
|
|
||||||
{
|
|
||||||
_suspend = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Resume()
|
|
||||||
{
|
|
||||||
_suspend = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Clear()
|
|
||||||
{
|
|
||||||
_rewindBuffer?.Clear();
|
|
||||||
_lastState = new byte[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] BufferManage(byte[] inbuf, ref long size, bool allocate)
|
|
||||||
{
|
|
||||||
if (!allocate)
|
|
||||||
{
|
|
||||||
_rewindBufferBacking = inbuf;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
size = Math.Min(size, _memoryLimit);
|
|
||||||
|
|
||||||
// if we have an appropriate buffer free, return it
|
|
||||||
var buf = _rewindBufferBacking;
|
|
||||||
_rewindBufferBacking = null;
|
|
||||||
if (buf != null && buf.LongLength == size)
|
|
||||||
{
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise, allocate it
|
|
||||||
do
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return new byte[size];
|
|
||||||
}
|
|
||||||
catch (OutOfMemoryException)
|
|
||||||
{
|
|
||||||
size /= 2;
|
|
||||||
_memoryLimit = size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while (size > 1);
|
|
||||||
throw new OutOfMemoryException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Capture(int frame)
|
|
||||||
{
|
|
||||||
if (!Active)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_rewindThread == null || frame % RewindFrequency != 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_rewindThread.Capture(_statableCore.SaveStateBinary());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CaptureInternal(byte[] coreSavestate)
|
|
||||||
{
|
|
||||||
if (_rewindDeltaEnable)
|
|
||||||
{
|
|
||||||
CaptureStateDelta(coreSavestate);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CaptureStateNonDelta(coreSavestate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CaptureStateNonDelta(byte[] state)
|
|
||||||
{
|
|
||||||
long offset = _rewindBuffer.Enqueue(0, state.Length + 1);
|
|
||||||
var stream = _rewindBuffer.Stream;
|
|
||||||
stream.Position = offset;
|
|
||||||
|
|
||||||
// write the header for a non-delta frame
|
|
||||||
stream.WriteByte(1); // Full state = true
|
|
||||||
stream.Write(state, 0, state.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateLastState(byte[] state, int index, int length)
|
|
||||||
{
|
|
||||||
if (_lastState.Length != length)
|
|
||||||
{
|
|
||||||
_lastState = new byte[length];
|
|
||||||
}
|
|
||||||
|
|
||||||
Buffer.BlockCopy(state, index, _lastState, 0, length);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateLastState(byte[] state)
|
|
||||||
{
|
|
||||||
UpdateLastState(state, 0, state.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe void CaptureStateDelta(byte[] currentState)
|
|
||||||
{
|
|
||||||
// Keep in mind that everything captured here is intended to be played back in
|
|
||||||
// reverse. The goal is, given the current state, how to get back to the previous
|
|
||||||
// state. That's why the data portion of the delta comes from the previous state,
|
|
||||||
// and also why the previous state is used if we have to bail out and capture the
|
|
||||||
// full state instead.
|
|
||||||
if (currentState.Length != _lastState.Length)
|
|
||||||
{
|
|
||||||
// If the state sizes mismatch, capture a full state rather than trying to do anything clever
|
|
||||||
goto CaptureFullState;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentState.Length == 0)
|
|
||||||
{
|
|
||||||
// handle empty states as a "full" (empty) state
|
|
||||||
goto CaptureFullState;
|
|
||||||
}
|
|
||||||
|
|
||||||
int index = 0;
|
|
||||||
int stateLength = Math.Min(currentState.Length, _lastState.Length);
|
|
||||||
bool inChangeSequence = false;
|
|
||||||
int changeSequenceStartOffset = 0;
|
|
||||||
int lastChangeSequenceStartOffset = 0;
|
|
||||||
|
|
||||||
if (_deltaBuffer.Length < stateLength + 1)
|
|
||||||
{
|
|
||||||
_deltaBuffer = new byte[stateLength + 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
_deltaBuffer[index++] = 0; // Full state = false (i.e. delta)
|
|
||||||
|
|
||||||
fixed (byte* pCurrentState = ¤tState[0])
|
|
||||||
fixed (byte* pLastState = &_lastState[0])
|
|
||||||
for (int i = 0; i < stateLength; i++)
|
|
||||||
{
|
|
||||||
bool thisByteMatches = *(pCurrentState + i) == *(pLastState + i);
|
|
||||||
|
|
||||||
if (inChangeSequence == false)
|
|
||||||
{
|
|
||||||
if (thisByteMatches)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
inChangeSequence = true;
|
|
||||||
changeSequenceStartOffset = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (thisByteMatches || i == stateLength - 1)
|
|
||||||
{
|
|
||||||
const int MaxHeaderSize = 10;
|
|
||||||
int length = i - changeSequenceStartOffset + (thisByteMatches ? 0 : 1);
|
|
||||||
|
|
||||||
if (index + length + MaxHeaderSize >= stateLength)
|
|
||||||
{
|
|
||||||
// If the delta ends up being larger than the full state, capture the full state instead
|
|
||||||
goto CaptureFullState;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Offset Delta
|
|
||||||
VLInteger.WriteUnsigned((uint)(changeSequenceStartOffset - lastChangeSequenceStartOffset), _deltaBuffer, ref index);
|
|
||||||
|
|
||||||
// Length
|
|
||||||
VLInteger.WriteUnsigned((uint)length, _deltaBuffer, ref index);
|
|
||||||
|
|
||||||
// Data
|
|
||||||
Buffer.BlockCopy(_lastState, changeSequenceStartOffset, _deltaBuffer, index, length);
|
|
||||||
index += length;
|
|
||||||
|
|
||||||
inChangeSequence = false;
|
|
||||||
lastChangeSequenceStartOffset = changeSequenceStartOffset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_rewindBuffer.Push(new ArraySegment<byte>(_deltaBuffer, 0, index));
|
|
||||||
|
|
||||||
UpdateLastState(currentState);
|
|
||||||
return;
|
|
||||||
|
|
||||||
CaptureFullState:
|
|
||||||
CaptureStateNonDelta(_lastState);
|
|
||||||
UpdateLastState(currentState);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Rewind(int frames)
|
|
||||||
{
|
|
||||||
if (!Active || _rewindThread == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_rewindThread.Rewind(frames);
|
|
||||||
|
|
||||||
return _lastRewindLoadedState;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RewindInternal(int frames)
|
|
||||||
{
|
|
||||||
_lastRewindLoadedState = false;
|
|
||||||
|
|
||||||
for (int i = 0; i < frames; i++)
|
|
||||||
{
|
|
||||||
// Always leave the first item in the rewind buffer. For full states, once there's
|
|
||||||
// one item remaining, we've already gone back as far as possible because the code
|
|
||||||
// to load the previous state has already peeked at the first item after removing
|
|
||||||
// the second item. We want to hold on to the first item anyway since it's a copy
|
|
||||||
// of the current state (see comment in the following method). For deltas, since
|
|
||||||
// each one records how to get back to the previous state, once we've gone back to
|
|
||||||
// the second item, it's already resulted in the first state being loaded. The
|
|
||||||
// first item is just a junk entry with the initial value of _lastState (0 bytes).
|
|
||||||
if (_rewindBuffer.Count <= 1)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
LoadPreviousState();
|
|
||||||
_lastRewindLoadedState = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private MemoryStream GetPreviousStateMemoryStream()
|
|
||||||
{
|
|
||||||
if (_rewindDeltaEnable)
|
|
||||||
{
|
|
||||||
// When capturing deltas, the most recent state is stored in _lastState, and the
|
|
||||||
// last item in the rewind buffer gets us back to the previous state.
|
|
||||||
return _rewindBuffer.PopMemoryStream();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// When capturing full states, the last item in the rewind buffer is the most
|
|
||||||
// recent state, so we need to get the item before it.
|
|
||||||
_rewindBuffer.Pop();
|
|
||||||
return _rewindBuffer.PeekMemoryStream();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note that in both cases, after loading the state, we still have a copy of it
|
|
||||||
// either in _lastState or as the last item in the rewind buffer. This is good
|
|
||||||
// because once we resume capturing, the first capture doesn't happen until
|
|
||||||
// stepping forward to the following frame, which would result in a gap if we
|
|
||||||
// didn't still have a copy of the current state here.
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LoadPreviousState()
|
|
||||||
{
|
|
||||||
using var reader = new BinaryReader(GetPreviousStateMemoryStream());
|
|
||||||
byte[] buf = ((MemoryStream)reader.BaseStream).GetBuffer();
|
|
||||||
bool fullState = reader.ReadByte() == 1;
|
|
||||||
if (_rewindDeltaEnable)
|
|
||||||
{
|
|
||||||
if (fullState)
|
|
||||||
{
|
|
||||||
UpdateLastState(buf, 1, buf.Length - 1);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
int index = 1;
|
|
||||||
int offset = 0;
|
|
||||||
|
|
||||||
while (index < buf.Length)
|
|
||||||
{
|
|
||||||
int offsetDelta = (int)VLInteger.ReadUnsigned(buf, ref index);
|
|
||||||
int length = (int)VLInteger.ReadUnsigned(buf, ref index);
|
|
||||||
|
|
||||||
offset += offsetDelta;
|
|
||||||
|
|
||||||
Buffer.BlockCopy(buf, index, _lastState, offset, length);
|
|
||||||
index += length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_statableCore.LoadStateBinary(_lastState);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!fullState)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
_statableCore.LoadStateBinary(reader);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Clear();
|
|
||||||
_rewindBuffer?.Dispose();
|
|
||||||
_rewindThread?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class VLInteger
|
|
||||||
{
|
|
||||||
public static void WriteUnsigned(uint value, byte[] data, ref int index)
|
|
||||||
{
|
|
||||||
// This is optimized for good performance on both the x86 and x64 JITs. Don't change anything without benchmarking.
|
|
||||||
do
|
|
||||||
{
|
|
||||||
var x = value & 0x7FU;
|
|
||||||
value >>= 7;
|
|
||||||
data[index++] = (byte)((value != 0U ? 0x80U : 0U) | x);
|
|
||||||
}
|
|
||||||
while (value != 0U);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static uint ReadUnsigned(byte[] data, ref int index)
|
|
||||||
{
|
|
||||||
// This is optimized for good performance on both the x86 and x64 JITs. Don't change anything without benchmarking.
|
|
||||||
var value = 0U;
|
|
||||||
var shiftCount = 0;
|
|
||||||
bool isLastByte; // Negating the comparison and moving it earlier in the loop helps a lot on x86 for some reason
|
|
||||||
do
|
|
||||||
{
|
|
||||||
var x = (uint)data[index++];
|
|
||||||
isLastByte = (x & 0x80U) == 0U;
|
|
||||||
value |= (x & 0x7FU) << shiftCount;
|
|
||||||
shiftCount += 7;
|
|
||||||
}
|
|
||||||
while (!isLastByte);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,339 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
using BizHawk.Common;
|
|
||||||
|
|
||||||
namespace BizHawk.Client.Common
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Manages a ring buffer of storage which can continually chow its own tail to keep growing forward.
|
|
||||||
/// Probably only useful for the rewind buffer
|
|
||||||
/// </summary>
|
|
||||||
public class StreamBlobDatabase : IDisposable
|
|
||||||
{
|
|
||||||
private readonly StreamBlobDatabaseBufferManager _mBufferManage;
|
|
||||||
private readonly LinkedList<ListItem> _mBookmarks = new LinkedList<ListItem>();
|
|
||||||
private readonly long _mCapacity;
|
|
||||||
|
|
||||||
private byte[] _mAllocatedBuffer;
|
|
||||||
private LinkedListNode<ListItem> _mHead, _mTail;
|
|
||||||
|
|
||||||
public StreamBlobDatabase(bool onDisk, long capacity, StreamBlobDatabaseBufferManager mBufferManage)
|
|
||||||
{
|
|
||||||
_mBufferManage = mBufferManage;
|
|
||||||
_mCapacity = capacity;
|
|
||||||
if (onDisk)
|
|
||||||
{
|
|
||||||
var path = TempFileManager.GetTempFilename("rewindbuf");
|
|
||||||
|
|
||||||
// I checked the DeleteOnClose operation to make sure it cleans up when the process is aborted, and it seems to.
|
|
||||||
// Otherwise we would have a more complex tempfile management problem here.
|
|
||||||
// 4KB buffer chosen due to similarity to .net defaults, and fear of anything larger making hiccups for small systems (we could try asyncing this stuff though...)
|
|
||||||
Stream = new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None, 4 * 1024, FileOptions.DeleteOnClose);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_mAllocatedBuffer = _mBufferManage(null, ref _mCapacity, true);
|
|
||||||
Stream = new MemoryStream(_mAllocatedBuffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the amount of the buffer that's used
|
|
||||||
/// </summary>
|
|
||||||
public long Size { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the current fullness ratio (Size/Capacity). Note that this wont reach 100% due to the buffer size not being a multiple of a fixed savestate size.
|
|
||||||
/// </summary>
|
|
||||||
public float FullnessRatio => (float)((double)Size / (double)_mCapacity);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the number of frames stored here
|
|
||||||
/// </summary>
|
|
||||||
public int Count => _mBookmarks.Count;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the underlying stream to
|
|
||||||
/// </summary>
|
|
||||||
public Stream Stream { get; private set; }
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Stream?.Dispose();
|
|
||||||
Stream = null;
|
|
||||||
if (_mAllocatedBuffer != null)
|
|
||||||
{
|
|
||||||
long capacity = 0;
|
|
||||||
_mBufferManage(_mAllocatedBuffer, ref capacity, false);
|
|
||||||
_mAllocatedBuffer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Clear()
|
|
||||||
{
|
|
||||||
_mHead = _mTail = null;
|
|
||||||
Size = 0;
|
|
||||||
_mBookmarks.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The push and pop semantics are for historical reasons and not resemblance to normal definitions
|
|
||||||
/// </summary>
|
|
||||||
public void Push(ArraySegment<byte> seg)
|
|
||||||
{
|
|
||||||
var buf = seg.Array;
|
|
||||||
int len = seg.Count;
|
|
||||||
long offset = Enqueue(0, len);
|
|
||||||
Stream.Position = offset;
|
|
||||||
Stream.Write(buf, seg.Offset, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The push and pop semantics are for historical reasons and not resemblance to normal definitions
|
|
||||||
/// </summary>
|
|
||||||
public MemoryStream PopMemoryStream()
|
|
||||||
{
|
|
||||||
return CreateMemoryStream(Pop());
|
|
||||||
}
|
|
||||||
|
|
||||||
public MemoryStream PeekMemoryStream()
|
|
||||||
{
|
|
||||||
return CreateMemoryStream(Peek());
|
|
||||||
}
|
|
||||||
|
|
||||||
private MemoryStream CreateMemoryStream(ListItem item)
|
|
||||||
{
|
|
||||||
var buf = new byte[item.Length];
|
|
||||||
Stream.Position = item.Index;
|
|
||||||
Stream.Read(buf, 0, item.Length);
|
|
||||||
return new MemoryStream(buf, 0, item.Length, false, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long Enqueue(int timestamp, int amount)
|
|
||||||
{
|
|
||||||
Size += amount;
|
|
||||||
|
|
||||||
if (_mHead == null)
|
|
||||||
{
|
|
||||||
_mTail = _mHead = _mBookmarks.AddFirst(new ListItem(timestamp, 0, amount));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
long target = _mHead.Value.EndExclusive + amount;
|
|
||||||
if (_mTail != null && target <= _mTail.Value.Index)
|
|
||||||
{
|
|
||||||
// there's room to add a new head before the tail
|
|
||||||
_mHead = _mBookmarks.AddAfter(_mHead, new ListItem(timestamp, _mHead.Value.EndExclusive, amount));
|
|
||||||
goto CLEANUP;
|
|
||||||
}
|
|
||||||
|
|
||||||
// maybe the tail is earlier than the head
|
|
||||||
if (_mTail != null && _mTail.Value.Index <= _mHead.Value.Index)
|
|
||||||
{
|
|
||||||
if (target <= _mCapacity)
|
|
||||||
{
|
|
||||||
// there's room to add a new head before the end of capacity
|
|
||||||
_mHead = _mBookmarks.AddAfter(_mHead, new ListItem(timestamp, _mHead.Value.EndExclusive, amount));
|
|
||||||
goto CLEANUP;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// nope, tail is after head. we'll have to clobber from the tail..
|
|
||||||
_mHead = _mBookmarks.AddAfter(_mHead, new ListItem(timestamp, _mHead.Value.EndExclusive, amount));
|
|
||||||
goto CLEANUP;
|
|
||||||
}
|
|
||||||
|
|
||||||
PLACEATSTART:
|
|
||||||
// no room before the tail, or before capacity. head needs to wrap around.
|
|
||||||
_mHead = _mBookmarks.AddAfter(_mHead, new ListItem(timestamp, 0, amount));
|
|
||||||
|
|
||||||
CLEANUP:
|
|
||||||
// while the head impinges on tail items, discard them
|
|
||||||
for (;;)
|
|
||||||
{
|
|
||||||
if (_mTail == null)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_mHead.Value.Index.RangeToExclusive(_mHead.Value.EndExclusive).Contains(_mTail.Value.Index) && _mHead != _mTail)
|
|
||||||
{
|
|
||||||
var nextTail = _mTail.Next;
|
|
||||||
Size -= _mTail.Value.Length;
|
|
||||||
_mBookmarks.Remove(_mTail);
|
|
||||||
_mTail = nextTail;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// one final check: in case we clobbered from the tail to make room and ended up after the capacity, we need to try again
|
|
||||||
// this has to be done this way, because we need one cleanup pass to purge all the tail items before the capacity;
|
|
||||||
// and then again to purge tail items impinged by this new item at the beginning
|
|
||||||
if (_mHead.Value.EndExclusive > _mCapacity)
|
|
||||||
{
|
|
||||||
var temp = _mHead.Previous;
|
|
||||||
_mBookmarks.Remove(_mHead);
|
|
||||||
_mHead = temp;
|
|
||||||
goto PLACEATSTART;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _mHead.Value.Index;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <exception cref="InvalidOperationException">empty</exception>
|
|
||||||
public ListItem Pop()
|
|
||||||
{
|
|
||||||
if (_mHead == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Attempted to {nameof(Pop)} from an empty data structure");
|
|
||||||
}
|
|
||||||
|
|
||||||
var ret = _mHead.Value;
|
|
||||||
Size -= ret.Length;
|
|
||||||
LinkedListNode<ListItem> nextHead = _mHead.Previous;
|
|
||||||
_mBookmarks.Remove(_mHead);
|
|
||||||
if (_mHead == _mTail)
|
|
||||||
{
|
|
||||||
_mTail = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_mHead = nextHead ?? _mBookmarks.Last;
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <exception cref="InvalidOperationException">empty</exception>
|
|
||||||
public ListItem Peek()
|
|
||||||
{
|
|
||||||
if (_mHead == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Attempted to {nameof(Peek)} from an empty data structure");
|
|
||||||
}
|
|
||||||
|
|
||||||
return _mHead.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <exception cref="InvalidOperationException">empty</exception>
|
|
||||||
public ListItem Dequeue()
|
|
||||||
{
|
|
||||||
if (_mTail == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Attempted to {nameof(Dequeue)} from an empty data structure");
|
|
||||||
}
|
|
||||||
|
|
||||||
var ret = _mTail.Value;
|
|
||||||
Size -= ret.Length;
|
|
||||||
var nextTail = _mTail.Next;
|
|
||||||
_mBookmarks.Remove(_mTail);
|
|
||||||
if (_mTail == _mHead)
|
|
||||||
{
|
|
||||||
_mHead = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_mTail = nextTail ?? _mBookmarks.First;
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
//-------- tests ---------
|
|
||||||
public void AssertMonotonic()
|
|
||||||
{
|
|
||||||
if (_mTail == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ts = _mTail.Value.Timestamp;
|
|
||||||
LinkedListNode<ListItem> curr = _mTail;
|
|
||||||
for (;;)
|
|
||||||
{
|
|
||||||
if (curr == null)
|
|
||||||
{
|
|
||||||
curr = _mBookmarks.First;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
System.Diagnostics.Debug.Assert(curr.Value.Timestamp >= ts);
|
|
||||||
if (curr == _mHead)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ts = curr.Value.Timestamp;
|
|
||||||
curr = curr.Next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ListItem
|
|
||||||
{
|
|
||||||
public ListItem(int timestamp, long index, int length)
|
|
||||||
{
|
|
||||||
Timestamp = timestamp;
|
|
||||||
Index = index;
|
|
||||||
Length = length;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Timestamp { get; }
|
|
||||||
public long Index { get; }
|
|
||||||
public int Length { get; }
|
|
||||||
|
|
||||||
public long EndExclusive => Index + Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] Test_BufferManage(byte[] inbuf, ref long size, bool allocate)
|
|
||||||
{
|
|
||||||
if (allocate)
|
|
||||||
{
|
|
||||||
// if we have an appropriate buffer free, return it
|
|
||||||
if (testRewindFellationBuf != null && testRewindFellationBuf.LongLength == size)
|
|
||||||
{
|
|
||||||
var ret = testRewindFellationBuf;
|
|
||||||
testRewindFellationBuf = null;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise, allocate it
|
|
||||||
return new byte[size];
|
|
||||||
}
|
|
||||||
|
|
||||||
testRewindFellationBuf = inbuf;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] testRewindFellationBuf;
|
|
||||||
|
|
||||||
private static void Test(string[] args)
|
|
||||||
{
|
|
||||||
var sbb = new StreamBlobDatabase(false, 1024, Test_BufferManage);
|
|
||||||
Random r = new Random(0);
|
|
||||||
byte[] temp = new byte[1024];
|
|
||||||
int trials = 0;
|
|
||||||
for (;;)
|
|
||||||
{
|
|
||||||
int len = r.Next(1024) + 1;
|
|
||||||
if (r.Next(100) == 0)
|
|
||||||
{
|
|
||||||
len = 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
ArraySegment<byte> seg = new ArraySegment<byte>(temp, 0, len);
|
|
||||||
Console.WriteLine("{0} - {1}", trials, seg.Count);
|
|
||||||
if (seg.Count == 1024)
|
|
||||||
{
|
|
||||||
Console.Write("*************************");
|
|
||||||
}
|
|
||||||
|
|
||||||
trials++;
|
|
||||||
sbb.Push(seg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public delegate byte[] StreamBlobDatabaseBufferManager(byte[] existingBuffer, ref long capacity, bool allocate);
|
|
||||||
}
|
|
|
@ -865,18 +865,9 @@ namespace BizHawk.Client.EmuHawk
|
||||||
public void CreateRewinder()
|
public void CreateRewinder()
|
||||||
{
|
{
|
||||||
Rewinder?.Dispose();
|
Rewinder?.Dispose();
|
||||||
|
Rewinder = Emulator.HasSavestates() && Config.Rewind.EnabledSmall // TODO: replace this with just a single "enabled"
|
||||||
if (Config.Rewind.EnabledSmall)
|
? new Zwinder(600, Emulator.AsStatable(), Config.Rewind)
|
||||||
{
|
: null;
|
||||||
Rewinder = Emulator.HasSavestates() && Config.Rewind.EnabledSmall // TODO: replace this with just a single "enabled"?
|
|
||||||
? new Zwinder(600, Emulator.AsStatable(), Config.Rewind)
|
|
||||||
// ? new Rewinder(Emulator.AsStatable(), Config.Rewind)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Rewinder = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private FirmwareManager FirmwareManager => GlobalWin.FirmwareManager;
|
private FirmwareManager FirmwareManager => GlobalWin.FirmwareManager;
|
||||||
|
|
Loading…
Reference in New Issue