diff --git a/src/BizHawk.Client.Common/rewind/Zwinder.cs b/src/BizHawk.Client.Common/rewind/Zwinder.cs
new file mode 100644
index 0000000000..55d9f43a3d
--- /dev/null
+++ b/src/BizHawk.Client.Common/rewind/Zwinder.cs
@@ -0,0 +1,319 @@
+using System;
+using System.IO;
+using BizHawk.Emulation.Common;
+
+namespace BizHawk.Client.Common
+{
+ ///
+ /// A simple ring buffer rewinder
+ ///
+ public class Zwinder : IRewinder
+ {
+ /*
+ Main goals:
+ 1. No copies, ever. States are deposited directly to, and read directly from, one giant ring buffer.
+ As a consequence, there is no multithreading because there is nothing to thread.
+ 2. Support for arbitrary and changeable state sizes. Frequency is calculated dynamically.
+ 3. No delta compression. Keep it simple. If there are cores that benefit heavily from delta compression, we should
+ maintain a separate rewinder alongside this one that is customized for those cores.
+ */
+
+ /// size of rewinder backing store in bytes
+ /// desired frame length (number of emulated frames you can go back before running out of buffer)
+ public Zwinder(long targetSize, int targetFrameLength, IBinaryStateable stateSource)
+ {
+ if (targetSize < 65536)
+ throw new ArgumentOutOfRangeException(nameof(targetSize));
+ if (targetFrameLength < 1)
+ throw new ArgumentOutOfRangeException(nameof(targetFrameLength));
+
+ Size = 1L << (int)Math.Floor(Math.Log(targetSize, 2));
+ _sizeMask = Size - 1;
+ _buffer = new byte[Size];
+ Active = true;
+ _stateSource = stateSource;
+ _targetFrameLength = targetFrameLength;
+ _states = new StateInfo[STATEMASK + 1];
+ }
+
+ ///
+ /// Number of states that could be in the state ringbuffer, Mask for the state ringbuffer
+ ///
+ private const int STATEMASK = 16383;
+
+ ///
+ /// How many states are actually in the state ringbuffer
+ ///
+ public int Count => (_nextStateIndex - _firstStateIndex) & STATEMASK;
+
+ public float FullnessRatio => Used / (float)Size;
+
+ ///
+ /// total number of bytes used
+ ///
+ ///
+ public long Used
+ {
+ get
+ {
+ if (Count == 0)
+ return 0;
+ return (_states[HeadStateIndex].Start + _states[HeadStateIndex].Size - _states[_firstStateIndex].Start) & _sizeMask;
+ }
+ }
+
+ ///
+ /// Total size of the _buffer
+ ///
+ ///
+ public long Size { get; }
+ private readonly long _sizeMask;
+ private byte[] _buffer;
+
+ private readonly int _targetFrameLength;
+
+ private struct StateInfo
+ {
+ public long Start;
+ public int Size;
+ public int Frame;
+ }
+ private StateInfo[] _states;
+ private int _firstStateIndex;
+ private int _nextStateIndex;
+ private int HeadStateIndex => (_nextStateIndex - 1) & STATEMASK;
+
+ private IBinaryStateable _stateSource;
+
+ ///
+ /// TODO: This is not a frequency, it's the reciprocal
+ ///
+ public int RewindFrequency => ComputeIdealRewindInterval();
+
+ public bool Active { get; private set; }
+
+ private int ComputeIdealRewindInterval()
+ {
+ if (Count == 0)
+ return 1; // shrug
+
+ // assume that the most recent state size is representative of stuff
+ var sizeRatio = Size / (float)_states[HeadStateIndex].Size;
+ var frameRatio = _targetFrameLength / sizeRatio;
+
+ return (int)Math.Round(frameRatio);
+ }
+
+ private bool ShouldCapture(int frame)
+ {
+ if (Count == 0)
+ return true;
+ var frameDiff = frame - _states[HeadStateIndex].Frame;
+ if (frameDiff < 1)
+ // non-linear time is from a combination of other state changing mechanisms and the rewinder
+ // not much we can say here, so just take a state
+ return true;
+
+ return frameDiff >= ComputeIdealRewindInterval();
+ }
+
+ public void Capture(int frame)
+ {
+ if (!Active || !ShouldCapture(frame))
+ return;
+
+ var start = (_states[HeadStateIndex].Start + _states[HeadStateIndex].Size) & _sizeMask;
+
+ var stream = new SaveStateStream(
+ _buffer,
+ start,
+ Size, _sizeMask);
+ _stateSource.SaveStateBinary(new BinaryWriter(stream));
+
+ // invalidate states if we're at the state ringbuffer size limit, or if they were overridden in the bytebuffer
+ var length = stream.Length;
+ while (Count == STATEMASK || Count > 0 && ((_states[_firstStateIndex].Start - start) & _sizeMask) < length)
+ _firstStateIndex = (_firstStateIndex + 1) & STATEMASK;
+
+ _states[_nextStateIndex].Frame = frame;
+ _states[_nextStateIndex].Start = start;
+ _states[_nextStateIndex].Size = (int)length;
+ _nextStateIndex = (_nextStateIndex + 1) & STATEMASK;
+
+ Console.WriteLine($"Size: {Size >> 20}MiB, Used: {Used >> 20}MiB, States: {Count}");
+ }
+
+ public void Resume()
+ {
+ Active = true;
+ }
+
+ public bool Rewind(int frames)
+ {
+ if (!Active)
+ return false;
+ // this is supposed to rewind to the previous saved frame
+ // It's only ever called with a value of 1 from the frontend?
+
+ frames = Math.Min(frames, Count);
+ if (frames == 0)
+ return false; // no states saved
+ int loadIndex = (_nextStateIndex - frames) & STATEMASK;
+
+ _stateSource.LoadStateBinary(
+ new BinaryReader(
+ new LoadStateStream(_buffer, _states[loadIndex].Start, _states[loadIndex].Size, _sizeMask)));
+
+ _nextStateIndex = loadIndex;
+ Console.WriteLine($"Size: {Size >> 20}MiB, Used: {Used >> 20}MiB, States: {Count}");
+ return true;
+ }
+
+ public void Suspend()
+ {
+ Active = false;
+ }
+
+ public void Dispose()
+ {
+ _buffer = null;
+ _states = null;
+ _stateSource = null;
+ }
+
+ private class SaveStateStream : Stream
+ {
+ public SaveStateStream(byte[] buffer, long offset, long maxSize, long mask)
+ {
+ _buffer = buffer;
+ _offset = offset;
+ _maxSize = maxSize;
+ _mask = mask;
+ }
+
+ private byte[] _buffer;
+ private readonly long _offset;
+ private long _maxSize;
+ private long _position;
+ private readonly long _mask;
+
+ public override bool CanRead => false;
+ public override bool CanSeek => false;
+ public override bool CanWrite => true;
+ public override long Length => _position;
+
+ public override long Position { get => _position; set => throw new IOException(); }
+
+ public override void Flush()
+ {}
+
+ public override int Read(byte[] buffer, int offset, int count) => throw new IOException();
+ public override long Seek(long offset, SeekOrigin origin) => throw new IOException();
+ public override void SetLength(long value) => throw new IOException();
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ long n = Math.Min(_maxSize - _position, count);
+ if (n != count)
+ throw new IOException("A single state cannot be bigger than the buffer!");
+ if (n > 0)
+ {
+ var start = (_position + _offset) & _mask;
+ var end = (start + n) & _mask;
+ if (end < start)
+ {
+ long m = _buffer.LongLength - start;
+ Array.Copy(buffer, offset, _buffer, start, m);
+ offset += (int)m;
+ n -= m;
+ _position += m;
+ start = 0;
+ }
+ if (n > 0)
+ {
+ Array.Copy(buffer, offset, _buffer, start, n);
+ _position += n;
+ }
+ }
+ }
+
+ public override void WriteByte(byte value)
+ {
+ if (_position < _maxSize)
+ {
+ _buffer[(_position++ + _offset) & _mask] = value;
+ }
+ else
+ {
+ throw new IOException("A single state cannot be bigger than the buffer!");
+ }
+ }
+ }
+
+ private class LoadStateStream : Stream
+ {
+ public LoadStateStream(byte[] buffer, long offset, long size, long mask)
+ {
+ _buffer = buffer;
+ _offset = offset;
+ _size = size;
+ _mask = mask;
+ }
+
+ private byte[] _buffer;
+ private readonly long _offset;
+ private long _size;
+ private long _position;
+ private readonly long _mask;
+
+ public override bool CanRead => true;
+ public override bool CanSeek => false;
+ public override bool CanWrite => false;
+ public override long Length => _size;
+ public override long Position
+ {
+ get => _position;
+ set => throw new IOException();
+ }
+ public override void Flush()
+ {}
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ long n = Math.Min(_size - _position, count);
+ if (n > 0)
+ {
+ var start = (_position + _offset) & _mask;
+ var end = (start + n) & _mask;
+ if (end < start)
+ {
+ long m = _buffer.LongLength - start;
+ Array.Copy(_buffer, start, buffer, offset, m);
+ offset += (int)m;
+ n -= m;
+ _position += m;
+ start = 0;
+ }
+ if (n > 0)
+ {
+ Array.Copy(_buffer, start, buffer, offset, n);
+ _position += n;
+ }
+ }
+ return (int)n;
+ }
+
+ public override int ReadByte()
+ {
+ if (_position < _size)
+ return _buffer[(_position++ + _offset) & _mask];
+ else
+ return -1;
+ }
+
+ public override long Seek(long offset, SeekOrigin origin) => throw new IOException();
+ public override void SetLength(long value) => throw new IOException();
+ public override void Write(byte[] buffer, int offset, int count) => throw new IOException();
+ }
+ }
+}
diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs
index 797d0d8250..3cf9d0c18a 100644
--- a/src/BizHawk.Client.EmuHawk/MainForm.cs
+++ b/src/BizHawk.Client.EmuHawk/MainForm.cs
@@ -865,8 +865,9 @@ namespace BizHawk.Client.EmuHawk
public void CreateRewinder()
{
Rewinder?.Dispose();
- Rewinder = Emulator.HasSavestates()
- ? new Rewinder(Emulator.AsStatable(), Config.Rewind)
+ Rewinder = Emulator.HasSavestates() && Config.Rewind.EnabledSmall // TODO: replace this with just a single "enabled"?
+ ? new Zwinder(1024 * 1024 * 32, 600, Emulator.AsStatable())
+ // ? new Rewinder(Emulator.AsStatable(), Config.Rewind)
: null;
}