diff --git a/src/BizHawk.Client.Common/rewind/IRewinder.cs b/src/BizHawk.Client.Common/rewind/IRewinder.cs
index 66ec52a1a1..aa7e0b9fe9 100644
--- a/src/BizHawk.Client.Common/rewind/IRewinder.cs
+++ b/src/BizHawk.Client.Common/rewind/IRewinder.cs
@@ -12,7 +12,10 @@ namespace BizHawk.Client.Common
bool Active { get; }
void Capture(int frame);
- bool Rewind(int frames);
+ ///
+ /// Rewind 1 saved frame, if possible
+ ///
+ bool Rewind();
void Suspend();
void Resume();
diff --git a/src/BizHawk.Client.Common/rewind/Zwinder.cs b/src/BizHawk.Client.Common/rewind/Zwinder.cs
index d073481ca6..e15c89f778 100644
--- a/src/BizHawk.Client.Common/rewind/Zwinder.cs
+++ b/src/BizHawk.Client.Common/rewind/Zwinder.cs
@@ -18,33 +18,21 @@ namespace BizHawk.Client.Common
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.
*/
+
+ private readonly ZwinderBuffer _buffer;
+ private readonly IBinaryStateable _stateSource;
+
public Zwinder(IBinaryStateable stateSource, IRewindSettings settings)
{
- long targetSize = settings.BufferSize * 1024 * 1024;
- if (settings.TargetFrameLength < 1)
- {
- throw new ArgumentOutOfRangeException(nameof(settings.TargetFrameLength));
- }
-
- Size = 1L << (int)Math.Floor(Math.Log(targetSize, 2));
- _sizeMask = Size - 1;
- _buffer = new byte[Size];
- Active = true;
+ _buffer = new ZwinderBuffer(settings);
_stateSource = stateSource;
- _targetFrameLength = settings.TargetFrameLength;
- _states = new StateInfo[StateMask + 1];
- _useCompression = settings.UseCompression;
+ Active = true;
}
- ///
- /// 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 int Count => _buffer.Count;
public float FullnessRatio => Used / (float)Size;
@@ -52,21 +40,13 @@ namespace BizHawk.Client.Common
/// total number of bytes used
///
///
- public long Used => Count == 0
- ? 0
- : (_states[HeadStateIndex].Start
- + _states[HeadStateIndex].Size
- - _states[_firstStateIndex].Start
- ) & _sizeMask;
+ public long Used => _buffer.Used;
///
/// Total size of the _buffer
///
///
- public long Size { get; }
-
- private readonly long _sizeMask;
- private byte[] _buffer;
+ public long Size => _buffer.Size;
private readonly int _targetFrameLength;
@@ -77,110 +57,28 @@ namespace BizHawk.Client.Common
public int Frame;
}
- private StateInfo[] _states;
- private int _firstStateIndex;
- private int _nextStateIndex;
- private int HeadStateIndex => (_nextStateIndex - 1) & StateMask;
-
- private readonly bool _useCompression;
-
- private IBinaryStateable _stateSource;
-
///
/// TODO: This is not a frequency, it's the reciprocal
///
- public int RewindFrequency => ComputeIdealRewindInterval();
+ public int RewindFrequency => _buffer.RewindFrequency;
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;
-
- var idealInterval = (int)Math.Round(frameRatio);
- return Math.Max(idealInterval, 1);
- }
-
- 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))
+ if (!Active)
return;
-
- var start = (_states[HeadStateIndex].Start + _states[HeadStateIndex].Size) & _sizeMask;
- var initialMaxSize = Count > 0
- ? (_states[_firstStateIndex].Start - start) & _sizeMask
- : Size;
- Func notifySizeReached = () =>
- {
- if (Count == 0)
- throw new IOException("A single state must not be larger than the buffer");
- _firstStateIndex = (_firstStateIndex + 1) & StateMask;
- return Count > 0
- ? (_states[_firstStateIndex].Start - start) & _sizeMask
- : Size;
- };
- var stream = new SaveStateStream(_buffer, start, _sizeMask, initialMaxSize, notifySizeReached);
-
- if (_useCompression)
- {
- using var compressor = new DeflateStream(stream, CompressionLevel.Fastest, leaveOpen: true);
- _stateSource.SaveStateBinary(new BinaryWriter(compressor));
- }
- else
- {
- _stateSource.SaveStateBinary(new BinaryWriter(stream));
- }
-
- _states[_nextStateIndex].Frame = frame;
- _states[_nextStateIndex].Start = start;
- _states[_nextStateIndex].Size = (int)stream.Length;
- _nextStateIndex = (_nextStateIndex + 1) & StateMask;
-
- Console.WriteLine($"Size: {Size >> 20}MiB, Used: {Used >> 20}MiB, States: {Count}");
+ _buffer.Capture(frame, s => _stateSource.SaveStateBinary(new BinaryWriter(s)));
}
- public bool Rewind(int frames)
+ public bool Rewind()
{
- if (!Active)
+ if (!Active || Count == 0)
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;
-
- var stream = new LoadStateStream(_buffer, _states[loadIndex].Start, _states[loadIndex].Size, _sizeMask);
- _stateSource.LoadStateBinary(_useCompression
- ? new BinaryReader(new DeflateStream(stream, CompressionMode.Decompress, leaveOpen: true))
- : new BinaryReader(stream));
-
- _nextStateIndex = loadIndex;
- Console.WriteLine($"Size: {Size >> 20}MiB, Used: {Used >> 20}MiB, States: {Count}");
+ var index = Count - 1;
+ var state = _buffer.GetState(index);
+ _stateSource.LoadStateBinary(new BinaryReader(state.GetReadStream()));
+ _buffer.InvalidateEnd(index);
return true;
}
@@ -194,159 +92,9 @@ namespace BizHawk.Client.Common
Active = true;
}
-
public void Dispose()
{
- _buffer = null;
- _states = null;
- _stateSource = null;
- }
-
- private class SaveStateStream : Stream
- {
- ///
- ///
- ///
- /// The ringbuffer to write into
- /// Offset into the buffer to start writing (and treat as position 0 in the stream)
- /// Buffer size mask, used to wrap values in the ringbuffer correctly
- ///
- /// If the stream will exceed this size, notifySizeReached must be called before clobbering any data
- ///
- ///
- /// The callback that will be called when notifySize is about to be exceeded. Can either return a new larger notifySize,
- /// or abort processing with an IOException. This must fail if size is going to exceed buffer.Length, as nothing else
- /// is preventing that case.
- ///
- public SaveStateStream(byte[] buffer, long offset, long mask, long notifySize, Func notifySizeReached)
- {
- _buffer = buffer;
- _offset = offset;
- _mask = mask;
- _notifySize = notifySize;
- _notifySizeReached = notifySizeReached;
- }
-
- private readonly byte[] _buffer;
- private readonly long _offset;
- private readonly long _mask;
- private long _position;
- private long _notifySize;
- private readonly Func _notifySizeReached;
-
- 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 requestedSize = _position + count;
- while (requestedSize > _notifySize)
- _notifySize = _notifySizeReached();
- long n = 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, 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)
- {
- long requestedSize = _position + 1;
- while (requestedSize > _notifySize)
- _notifySize = _notifySizeReached();
- _buffer[(_position++ + _offset) & _mask] = value;
- }
- }
-
- private class LoadStateStream : Stream
- {
- public LoadStateStream(byte[] buffer, long offset, long size, long mask)
- {
- _buffer = buffer;
- _offset = offset;
- _size = size;
- _mask = mask;
- }
-
- private readonly byte[] _buffer;
- private readonly long _offset;
- private readonly 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);
- int ret = (int)n;
- 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 ret;
- }
-
- public override int ReadByte()
- {
- return _position < _size
- ? _buffer[(_position++ + _offset) & _mask]
- : -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();
+ // this possess no resources to dispose of, but other IRewinder impls might
}
}
}
diff --git a/src/BizHawk.Client.Common/rewind/ZwinderBuffer.cs b/src/BizHawk.Client.Common/rewind/ZwinderBuffer.cs
new file mode 100644
index 0000000000..cb692e1fa9
--- /dev/null
+++ b/src/BizHawk.Client.Common/rewind/ZwinderBuffer.cs
@@ -0,0 +1,400 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using BizHawk.Emulation.Common;
+
+namespace BizHawk.Client.Common
+{
+ public class ZwinderBuffer : IBinaryStateable
+ {
+ /*
+ 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 multi-threading 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.
+ */
+ public ZwinderBuffer(IRewindSettings settings)
+ {
+ long targetSize = settings.BufferSize * 1024 * 1024;
+ if (settings.TargetFrameLength < 1)
+ {
+ throw new ArgumentOutOfRangeException(nameof(settings.TargetFrameLength));
+ }
+
+ Size = 1L << (int)Math.Floor(Math.Log(targetSize, 2));
+ _sizeMask = Size - 1;
+ _buffer = new byte[Size];
+ _targetFrameLength = settings.TargetFrameLength;
+ _states = new StateInfo[StateMask + 1];
+ _useCompression = settings.UseCompression;
+ }
+
+ ///
+ /// 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 => Count == 0
+ ? 0
+ : (_states[HeadStateIndex].Start
+ + _states[HeadStateIndex].Size
+ - _states[_firstStateIndex].Start
+ ) & _sizeMask;
+
+ ///
+ /// Total size of the _buffer
+ ///
+ ///
+ public long Size { get; }
+
+ private readonly long _sizeMask;
+ private readonly byte[] _buffer;
+
+ private readonly int _targetFrameLength;
+
+ private struct StateInfo
+ {
+ public long Start;
+ public int Size;
+ public int Frame;
+ }
+
+ private readonly StateInfo[] _states;
+ private int _firstStateIndex;
+ private int _nextStateIndex;
+ private int HeadStateIndex => (_nextStateIndex - 1) & StateMask;
+
+ private readonly bool _useCompression;
+
+ ///
+ /// TODO: This is not a frequency, it's the reciprocal
+ ///
+ public int RewindFrequency => ComputeIdealRewindInterval();
+
+ 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;
+
+ var idealInterval = (int)Math.Round(frameRatio);
+ return Math.Max(idealInterval, 1);
+ }
+
+ 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();
+ }
+
+ ///
+ /// Maybe captures a state, if the conditions are favorable
+ ///
+ /// frame number to capture
+ /// will be called with the stream if capture is to be performed
+ ///
+ /// If provided, will be called with the index of states that are about to be removed. This will happen during
+ /// calls to Write() inside `callback`, and any reuse of the old state will have to happen immediately
+ ///
+ public void Capture(int frame, Action callback, Action indexInvalidated = null, bool force = false)
+ {
+ if (!force && !ShouldCapture(frame))
+ return;
+
+ var start = (_states[HeadStateIndex].Start + _states[HeadStateIndex].Size) & _sizeMask;
+ var initialMaxSize = Count > 0
+ ? (_states[_firstStateIndex].Start - start) & _sizeMask
+ : Size;
+ Func notifySizeReached = () =>
+ {
+ if (Count == 0)
+ throw new IOException("A single state must not be larger than the buffer");
+ indexInvalidated?.Invoke(0);
+ _firstStateIndex = (_firstStateIndex + 1) & StateMask;
+ return Count > 0
+ ? (_states[_firstStateIndex].Start - start) & _sizeMask
+ : Size;
+ };
+ var stream = new SaveStateStream(_buffer, start, _sizeMask, initialMaxSize, notifySizeReached);
+
+ if (_useCompression)
+ {
+ using var compressor = new DeflateStream(stream, CompressionLevel.Fastest, leaveOpen: true);
+ callback(compressor);
+ }
+ else
+ {
+ callback(stream);
+ }
+
+ _states[_nextStateIndex].Frame = frame;
+ _states[_nextStateIndex].Start = start;
+ _states[_nextStateIndex].Size = (int)stream.Length;
+ _nextStateIndex = (_nextStateIndex + 1) & StateMask;
+
+ Console.WriteLine($"Size: {Size >> 20}MiB, Used: {Used >> 20}MiB, States: {Count}");
+ }
+
+ private Stream MakeLoadStream(int index)
+ {
+ Stream stream = new LoadStateStream(_buffer, _states[index].Start, _states[index].Size, _sizeMask);
+ if (_useCompression)
+ stream = new DeflateStream(stream, CompressionMode.Decompress, leaveOpen: true);
+ return stream;
+ }
+
+ public class StateInformation
+ {
+ private readonly int _index;
+ public int Frame => _parent._states[_index].Frame;
+ public int Size => _parent._states[_index].Size;
+ private readonly ZwinderBuffer _parent;
+ public Stream GetReadStream()
+ {
+ return _parent.MakeLoadStream(_index);
+ }
+ internal StateInformation(ZwinderBuffer parent, int index)
+ {
+ _index = index;
+ _parent = parent;
+ }
+ }
+
+ ///
+ /// Retrieve information about a state from 0..Count - 1.
+ /// The information contained within is valid only until the collection is modified.
+ ///
+ ///
+ ///
+ public StateInformation GetState(int index)
+ {
+ if ((uint)index >= (uint)Count)
+ throw new IndexOutOfRangeException();
+ return new StateInformation(this, (index + _firstStateIndex) & StateMask);
+ }
+
+ ///
+ /// Invalidate states from GetState(index) on to the end of the buffer, so that Count == index afterwards
+ ///
+ ///
+ public void InvalidateEnd(int index)
+ {
+ if ((uint)index > (uint)Count)
+ throw new IndexOutOfRangeException();
+ _nextStateIndex = (index + _firstStateIndex) & StateMask;
+ Console.WriteLine($"Size: {Size >> 20}MiB, Used: {Used >> 20}MiB, States: {Count}");
+ }
+
+ public void SaveStateBinary(BinaryWriter writer)
+ {
+ writer.Write(Size);
+ writer.Write(_sizeMask);
+ writer.Write(_targetFrameLength);
+ writer.Write(_useCompression);
+
+ writer.Write(_buffer);
+ foreach (var s in _states)
+ {
+ writer.Write(s.Start);
+ writer.Write(s.Size);
+ writer.Write(s.Frame);
+ }
+ writer.Write(_firstStateIndex);
+ writer.Write(_nextStateIndex);
+ }
+
+ public void LoadStateBinary(BinaryReader reader)
+ {
+ if (reader.ReadInt64() != Size) throw new InvalidOperationException("Bad format");
+ if (reader.ReadInt64() != _sizeMask) throw new InvalidOperationException("Bad format");
+ if (reader.ReadInt32() != _targetFrameLength) throw new InvalidOperationException("Bad format");
+ if (reader.ReadBoolean() != _useCompression) throw new InvalidOperationException("Bad format");
+
+ reader.Read(_buffer, 0, _buffer.Length);
+ for (var i = 0; i < _states.Length; i++)
+ {
+ _states[i].Start = reader.Read();
+ _states[i].Size = reader.Read();
+ _states[i].Frame = reader.Read();
+ }
+ _firstStateIndex = reader.ReadInt32();
+ _nextStateIndex = reader.ReadInt32();
+ }
+
+ private class SaveStateStream : Stream
+ {
+ ///
+ ///
+ ///
+ /// The ringbuffer to write into
+ /// Offset into the buffer to start writing (and treat as position 0 in the stream)
+ /// Buffer size mask, used to wrap values in the ringbuffer correctly
+ ///
+ /// If the stream will exceed this size, notifySizeReached must be called before clobbering any data
+ ///
+ ///
+ /// The callback that will be called when notifySize is about to be exceeded. Can either return a new larger notifySize,
+ /// or abort processing with an IOException. This must fail if size is going to exceed buffer.Length, as nothing else
+ /// is preventing that case.
+ ///
+ public SaveStateStream(byte[] buffer, long offset, long mask, long notifySize, Func notifySizeReached)
+ {
+ _buffer = buffer;
+ _offset = offset;
+ _mask = mask;
+ _notifySize = notifySize;
+ _notifySizeReached = notifySizeReached;
+ }
+
+ private readonly byte[] _buffer;
+ private readonly long _offset;
+ private readonly long _mask;
+ private long _position;
+ private long _notifySize;
+ private readonly Func _notifySizeReached;
+
+ 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 requestedSize = _position + count;
+ while (requestedSize > _notifySize)
+ _notifySize = _notifySizeReached();
+ long n = 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, 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)
+ {
+ long requestedSize = _position + 1;
+ while (requestedSize > _notifySize)
+ _notifySize = _notifySizeReached();
+ _buffer[(_position++ + _offset) & _mask] = value;
+ }
+ }
+
+ private class LoadStateStream : Stream
+ {
+ public LoadStateStream(byte[] buffer, long offset, long size, long mask)
+ {
+ _buffer = buffer;
+ _offset = offset;
+ _size = size;
+ _mask = mask;
+ }
+
+ private readonly byte[] _buffer;
+ private readonly long _offset;
+ private readonly 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);
+ int ret = (int)n;
+ 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 ret;
+ }
+
+ public override int ReadByte()
+ {
+ return _position < _size
+ ? _buffer[(_position++ + _offset) & _mask]
+ : -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 aa5a73189c..b818896663 100644
--- a/src/BizHawk.Client.EmuHawk/MainForm.cs
+++ b/src/BizHawk.Client.EmuHawk/MainForm.cs
@@ -3823,6 +3823,7 @@ namespace BizHawk.Client.EmuHawk
CommitCoreSettingsToConfig();
Rewinder?.Dispose();
+ Rewinder = null;
if (MovieSession.Movie.IsActive()) // Note: this must be called after CommitCoreSettingsToConfig()
{
@@ -4392,7 +4393,7 @@ namespace BizHawk.Client.EmuHawk
if (isRewinding)
{
- runFrame = Rewinder.Rewind(1) && Emulator.Frame > 1;
+ runFrame = Rewinder.Rewind() && Emulator.Frame > 1;
if (runFrame && MovieSession.Movie.IsRecording())
{