diff --git a/src/BizHawk.Client.Common/rewind/RewindThreader.cs b/src/BizHawk.Client.Common/rewind/RewindThreader.cs deleted file mode 100644 index 713919e8c7..0000000000 --- a/src/BizHawk.Client.Common/rewind/RewindThreader.cs +++ /dev/null @@ -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 _performCapture; - private readonly Action _performRewind; - private readonly BlockingCollection _jobs = new BlockingCollection(16); - private readonly ConcurrentStack _stateBufferPool = new ConcurrentStack(); - private readonly EventWaitHandle _rewindCompletedEvent; - private readonly Thread _thread; - - public RewindThreader(Action performCapture, Action 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; } - } - } -} diff --git a/src/BizHawk.Client.Common/rewind/Rewinder.cs b/src/BizHawk.Client.Common/rewind/Rewinder.cs deleted file mode 100644 index 632ab1b734..0000000000 --- a/src/BizHawk.Client.Common/rewind/Rewinder.cs +++ /dev/null @@ -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(_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; - } - } -} diff --git a/src/BizHawk.Client.Common/rewind/StreamBlobDatabase.cs b/src/BizHawk.Client.Common/rewind/StreamBlobDatabase.cs deleted file mode 100644 index bcf95c6f22..0000000000 --- a/src/BizHawk.Client.Common/rewind/StreamBlobDatabase.cs +++ /dev/null @@ -1,339 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; - -using BizHawk.Common; - -namespace BizHawk.Client.Common -{ - /// - /// Manages a ring buffer of storage which can continually chow its own tail to keep growing forward. - /// Probably only useful for the rewind buffer - /// - public class StreamBlobDatabase : IDisposable - { - private readonly StreamBlobDatabaseBufferManager _mBufferManage; - private readonly LinkedList _mBookmarks = new LinkedList(); - private readonly long _mCapacity; - - private byte[] _mAllocatedBuffer; - private LinkedListNode _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); - } - } - - /// - /// Gets the amount of the buffer that's used - /// - public long Size { get; private set; } - - /// - /// 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. - /// - public float FullnessRatio => (float)((double)Size / (double)_mCapacity); - - /// - /// Gets the number of frames stored here - /// - public int Count => _mBookmarks.Count; - - /// - /// Gets the underlying stream to - /// - 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(); - } - - /// - /// The push and pop semantics are for historical reasons and not resemblance to normal definitions - /// - public void Push(ArraySegment seg) - { - var buf = seg.Array; - int len = seg.Count; - long offset = Enqueue(0, len); - Stream.Position = offset; - Stream.Write(buf, seg.Offset, len); - } - - /// - /// The push and pop semantics are for historical reasons and not resemblance to normal definitions - /// - 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; - } - - /// empty - 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 nextHead = _mHead.Previous; - _mBookmarks.Remove(_mHead); - if (_mHead == _mTail) - { - _mTail = null; - } - - _mHead = nextHead ?? _mBookmarks.Last; - - return ret; - } - - /// empty - public ListItem Peek() - { - if (_mHead == null) - { - throw new InvalidOperationException($"Attempted to {nameof(Peek)} from an empty data structure"); - } - - return _mHead.Value; - } - - /// empty - 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 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 seg = new ArraySegment(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); -} diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index 6c58b99b21..2580ded0d9 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -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;