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()
|
||||
{
|
||||
Rewinder?.Dispose();
|
||||
|
||||
if (Config.Rewind.EnabledSmall)
|
||||
{
|
||||
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;
|
||||
}
|
||||
Rewinder = Emulator.HasSavestates() && Config.Rewind.EnabledSmall // TODO: replace this with just a single "enabled"
|
||||
? new Zwinder(600, Emulator.AsStatable(), Config.Rewind)
|
||||
: null;
|
||||
}
|
||||
|
||||
private FirmwareManager FirmwareManager => GlobalWin.FirmwareManager;
|
||||
|
|
Loading…
Reference in New Issue