diff --git a/src/BizHawk.Client.Common/BizHawk.Client.Common.csproj b/src/BizHawk.Client.Common/BizHawk.Client.Common.csproj
index fae488fd55..ae44f5f6a6 100644
--- a/src/BizHawk.Client.Common/BizHawk.Client.Common.csproj
+++ b/src/BizHawk.Client.Common/BizHawk.Client.Common.csproj
@@ -4,6 +4,7 @@
+ true
disable
diff --git a/src/BizHawk.Client.Common/config/RewindConfig.cs b/src/BizHawk.Client.Common/config/RewindConfig.cs
index 36bc5b43bf..de56f35c0d 100644
--- a/src/BizHawk.Client.Common/config/RewindConfig.cs
+++ b/src/BizHawk.Client.Common/config/RewindConfig.cs
@@ -7,6 +7,13 @@
///
bool UseCompression { get; }
+ ///
+ /// Gets a value indicating whether or not to delta compress savestates before storing them
+ ///
+ ///
+ // TODO: This is in here for frontend reasons, but the buffer itself doesn't interact with this.
+ bool UseDelta { get; }
+
///
/// Buffer space to use in MB
///
@@ -29,6 +36,7 @@
public class RewindConfig : IRewindSettings
{
public bool UseCompression { get; set; }
+ public bool UseDelta { get; set; }
public bool Enabled { get; set; } = true;
public long BufferSize { get; set; } = 512; // in mb
public int TargetFrameLength { get; set; } = 600;
diff --git a/src/BizHawk.Client.Common/rewind/ZeldaWinder.cs b/src/BizHawk.Client.Common/rewind/ZeldaWinder.cs
new file mode 100644
index 0000000000..ecdb257f8a
--- /dev/null
+++ b/src/BizHawk.Client.Common/rewind/ZeldaWinder.cs
@@ -0,0 +1,308 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using BizHawk.Common;
+using BizHawk.Emulation.Common;
+
+namespace BizHawk.Client.Common
+{
+ ///
+ /// A rewinder that uses Zelda compression, built on top of a ring buffer
+ ///
+ public class ZeldaWinder : IRewinder
+ {
+ private const int IS_GAP = unchecked((int)0x80000000);
+
+ private readonly ZwinderBuffer _buffer;
+ private readonly IStatable _stateSource;
+
+ private byte[] _master = new byte[0];
+ private int _masterFrame = -1;
+ private int _masterLength = 0;
+ private byte[] _scratch = new byte[0];
+ private int _count;
+
+ private Task _activeTask = null;
+ private bool _active;
+
+ private void Sync()
+ {
+ _activeTask?.Wait();
+ _activeTask = null;
+ }
+ private void Work(Action work)
+ {
+ _activeTask = Task.Run(work);
+ }
+
+ public ZeldaWinder(IStatable stateSource, IRewindSettings settings)
+ {
+ _buffer = new ZwinderBuffer(settings);
+ _stateSource = stateSource;
+ _active = true;
+ }
+
+ ///
+ /// How many states are actually in the state ringbuffer
+ ///
+ public int Count { get { Sync(); return _count; } }
+
+ public float FullnessRatio { get { Sync(); return _buffer.Used / (float)_buffer.Size; } }
+
+ ///
+ /// Total size of the _buffer
+ ///
+ ///
+ public long Size => _buffer.Size;
+
+ ///
+ /// TODO: This is not a frequency, it's the reciprocal
+ ///
+ public int RewindFrequency { get { Sync(); return _buffer.RewindFrequency; } }
+
+ public bool Active
+ {
+ get { Sync(); return _active; }
+ private set { Sync(); _active = value; }
+ }
+
+ public void Suspend()
+ {
+ Active = false;
+ }
+
+ public void Resume()
+ {
+ Active = true;
+ }
+
+ public void Dispose()
+ {
+ Sync();
+ _buffer.Dispose();
+ }
+
+ public void Clear()
+ {
+ Sync();
+ _buffer.InvalidateEnd(0);
+ }
+
+ public unsafe void Capture(int frame)
+ {
+ Sync();
+ if (!_active)
+ return;
+ if (_masterFrame == -1)
+ {
+ var sss = new SaveStateStream(this);
+ _stateSource.SaveStateBinary(new BinaryWriter(sss));
+ (_master, _scratch) = (_scratch, _master);
+ _masterLength = (int)sss.Position;
+ _masterFrame = frame;
+ _count++;
+ return;
+ }
+ if (!_buffer.WillCapture(_masterFrame))
+ return;
+
+ {
+ var sss = new SaveStateStream(this);
+ _stateSource.SaveStateBinary(new BinaryWriter(sss));
+
+ Work(() =>
+ {
+ _buffer.Capture(_masterFrame, underlyingStream_ =>
+ {
+ var zeldas = SpanStream.GetOrBuild(underlyingStream_);
+ if (_master.Length < _scratch.Length)
+ {
+ var replacement = new byte[_scratch.Length];
+ Array.Copy(_master, replacement, _master.Length);
+ _master = replacement;
+ }
+
+ var lengthHolder = _masterLength;
+ var lengthHolderSpan = new ReadOnlySpan(&lengthHolder, 4);
+
+ zeldas.Write(lengthHolderSpan);
+
+ fixed (byte* older_ = _master)
+ fixed (byte* newer_ = _scratch)
+ {
+ int* older = (int*)older_;
+ int* newer = (int*)newer_;
+ int lastIndex = (Math.Min(_masterLength, (int)sss.Position) + 3) / 4;
+ int lastOldIndex = (_masterLength + 3) / 4;
+ int* olderEnd = older + lastIndex;
+
+ int* from = older;
+ int* to = older;
+
+ while (older < olderEnd)
+ {
+ if (*older++ == *newer++)
+ {
+ if (to < from)
+ {
+ // Save on [to, from]
+ lengthHolder = (int)(from - to);
+ zeldas.Write(lengthHolderSpan);
+ zeldas.Write(new ReadOnlySpan(to, lengthHolder * 4));
+ }
+ to = older;
+ }
+ else
+ {
+ if (from < to)
+ {
+ // encode gap [from, to]
+ lengthHolder = (int)(to - from) | IS_GAP;
+ zeldas.Write(lengthHolderSpan);
+ }
+ from = older;
+ }
+ }
+ if (from < to)
+ {
+ // encode gap [from, to]
+ lengthHolder = (int)(to - from) | IS_GAP;
+ zeldas.Write(lengthHolderSpan);
+ }
+ if (lastOldIndex > lastIndex)
+ {
+ from += lastOldIndex - lastIndex;
+ }
+ if (to < from)
+ {
+ // Save on [to, from]
+ lengthHolder = (int)(from - to);
+ zeldas.Write(lengthHolderSpan);
+ zeldas.Write(new ReadOnlySpan(to, lengthHolder * 4));
+ }
+ }
+
+ (_master, _scratch) = (_scratch, _master);
+ _masterLength = (int)sss.Position;
+ _masterFrame = frame;
+ _count++;
+ });
+ });
+ }
+ }
+
+ private unsafe void RefillMaster(ZwinderBuffer.StateInformation state)
+ {
+ var lengthHolder = 0;
+ var lengthHolderSpan = new Span(&lengthHolder, 4);
+ var zeldas = SpanStream.GetOrBuild(state.GetReadStream());
+ zeldas.Read(lengthHolderSpan);
+ _masterLength = lengthHolder;
+ fixed (byte* buffer_ = _master)
+ {
+ int* buffer = (int*)buffer_;
+ while (zeldas.Read(lengthHolderSpan) == 4)
+ {
+ if ((lengthHolder & IS_GAP) != 0)
+ {
+ buffer += lengthHolder & ~IS_GAP;
+ }
+ else
+ {
+ zeldas.Read(new Span(buffer, lengthHolder * 4));
+ buffer += lengthHolder;
+ }
+ }
+ }
+ _masterFrame = state.Frame;
+ }
+
+ public bool Rewind(int frameToAvoid)
+ {
+ Sync();
+ if (!_active || _count == 0)
+ return false;
+
+ if (_masterFrame == frameToAvoid && _count > 1)
+ {
+ var index = _buffer.Count - 1;
+ RefillMaster(_buffer.GetState(index));
+ _buffer.InvalidateEnd(index);
+ _stateSource.LoadStateBinary(new BinaryReader(new MemoryStream(_master, 0, _masterLength, false)));
+ _count--;
+ }
+ else
+ {
+ _stateSource.LoadStateBinary(new BinaryReader(new MemoryStream(_master, 0, _masterLength, false)));
+ Work(() =>
+ {
+ var index = _buffer.Count - 1;
+ if (index >= 0)
+ {
+ RefillMaster(_buffer.GetState(index));
+ _buffer.InvalidateEnd(index);
+ }
+ else
+ {
+ _masterFrame = -1;
+ }
+ _count--;
+ });
+ }
+ return true;
+ }
+
+ private class SaveStateStream : Stream, ISpanStream
+ {
+ public SaveStateStream(ZeldaWinder owner)
+ {
+ _owner = owner;
+ }
+ private ZeldaWinder _owner;
+ private byte[] _dest
+ {
+ get => _owner._scratch;
+ set => _owner._scratch = value;
+ }
+ private int _position;
+ 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)
+ {
+ Write(new ReadOnlySpan(buffer, offset, count));
+ }
+ private void MaybeResize(int requestedSize)
+ {
+ if (requestedSize > _dest.Length)
+ {
+ var replacement = new byte[(Math.Max(_dest.Length * 2, requestedSize) + 3) & ~3];
+ Array.Copy(_dest, replacement, _dest.Length);
+ _dest = replacement;
+ }
+ }
+ public void Write(ReadOnlySpan buffer)
+ {
+ var requestedSize = _position + buffer.Length;
+ MaybeResize(requestedSize);
+ buffer.CopyTo(new Span(_dest, _position, buffer.Length));
+ _position = requestedSize;
+ }
+ public override void WriteByte(byte value)
+ {
+ var requestedSize = _position + 1;
+ MaybeResize(requestedSize);
+ _dest[_position] = value;
+ _position = requestedSize;
+ }
+ public int Read(Span buffer) => throw new IOException();
+ }
+ }
+}
diff --git a/src/BizHawk.Client.Common/rewind/ZwinderBuffer.cs b/src/BizHawk.Client.Common/rewind/ZwinderBuffer.cs
index 789a54e134..710e2cd2d2 100644
--- a/src/BizHawk.Client.Common/rewind/ZwinderBuffer.cs
+++ b/src/BizHawk.Client.Common/rewind/ZwinderBuffer.cs
@@ -164,6 +164,17 @@ namespace BizHawk.Client.Common
return frameDiff >= ComputeIdealRewindInterval();
}
+ ///
+ /// Predict whether Capture() will capture a state. Useful if expensive work needs to happen before
+ /// Capture() is called.
+ ///
+ /// The same frame number to be passed to capture
+ /// Whether capture will happen, assuming Capture() is passed the same frame and force = false
+ public bool WillCapture(int frame)
+ {
+ return ShouldCapture(frame);
+ }
+
///
/// Maybe captures a state, if the conditions are favorable
///
diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs
index 0ffc66c84a..893597f09f 100644
--- a/src/BizHawk.Client.EmuHawk/MainForm.cs
+++ b/src/BizHawk.Client.EmuHawk/MainForm.cs
@@ -949,7 +949,9 @@ namespace BizHawk.Client.EmuHawk
{
Rewinder?.Dispose();
Rewinder = Emulator.HasSavestates() && Config.Rewind.Enabled
- ? new Zwinder(Emulator.AsStatable(), Config.Rewind)
+ ? Config.Rewind.UseDelta
+ ? new ZeldaWinder(Emulator.AsStatable(), Config.Rewind)
+ : new Zwinder(Emulator.AsStatable(), Config.Rewind)
: null;
AddOnScreenMessage(Rewinder?.Active == true ? "Rewind started" : "Rewind disabled");
}
diff --git a/src/BizHawk.Client.EmuHawk/config/RewindConfig.Designer.cs b/src/BizHawk.Client.EmuHawk/config/RewindConfig.Designer.cs
old mode 100644
new mode 100755
index 99492c84d5..2e2182b23c
--- a/src/BizHawk.Client.EmuHawk/config/RewindConfig.Designer.cs
+++ b/src/BizHawk.Client.EmuHawk/config/RewindConfig.Designer.cs
@@ -41,6 +41,7 @@
this.label6 = new BizHawk.WinForms.Controls.LocLabelEx();
this.FullnessLabel = new BizHawk.WinForms.Controls.LocLabelEx();
this.groupBox4 = new System.Windows.Forms.GroupBox();
+ this.cbDeltaCompression = new System.Windows.Forms.CheckBox();
this.TargetFrameLengthNumeric = new System.Windows.Forms.NumericUpDown();
this.label2 = new BizHawk.WinForms.Controls.LocLabelEx();
this.EstTimeLabel = new BizHawk.WinForms.Controls.LocLabelEx();
@@ -118,7 +119,7 @@
this.UseCompression.Name = "UseCompression";
this.UseCompression.Size = new System.Drawing.Size(306, 17);
this.UseCompression.TabIndex = 5;
- this.UseCompression.Text = "Use compression (economizes buffer usage at cost of CPU)";
+ this.UseCompression.Text = "Use zlib compression (economizes buffer usage at cost of CPU)";
this.UseCompression.UseVisualStyleBackColor = true;
this.UseCompression.CheckedChanged += new System.EventHandler(this.UseCompression_CheckedChanged);
//
@@ -183,6 +184,7 @@
//
// groupBox4
//
+ this.groupBox4.Controls.Add(this.cbDeltaCompression);
this.groupBox4.Controls.Add(this.TargetFrameLengthNumeric);
this.groupBox4.Controls.Add(this.label2);
this.groupBox4.Controls.Add(this.label4);
@@ -202,11 +204,21 @@
this.groupBox4.Controls.Add(this.StateSizeLabel);
this.groupBox4.Location = new System.Drawing.Point(12, 12);
this.groupBox4.Name = "groupBox4";
- this.groupBox4.Size = new System.Drawing.Size(371, 205);
+ this.groupBox4.Size = new System.Drawing.Size(371, 218);
this.groupBox4.TabIndex = 2;
this.groupBox4.TabStop = false;
this.groupBox4.Text = "RewindSettings";
//
+ // cbDeltaCompression
+ //
+ this.cbDeltaCompression.AutoSize = true;
+ this.cbDeltaCompression.Location = new System.Drawing.Point(15, 193);
+ this.cbDeltaCompression.Name = "cbDeltaCompression";
+ this.cbDeltaCompression.Size = new System.Drawing.Size(149, 17);
+ this.cbDeltaCompression.TabIndex = 35;
+ this.cbDeltaCompression.Text = "Use delta compression (economizes buffer usage at cost of CPU)";
+ this.cbDeltaCompression.UseVisualStyleBackColor = true;
+ //
// TargetFrameLengthNumeric
//
this.TargetFrameLengthNumeric.Location = new System.Drawing.Point(125, 135);
@@ -538,5 +550,6 @@
private BizHawk.WinForms.Controls.LocLabelEx label20;
private System.Windows.Forms.NumericUpDown TargetFrameLengthNumeric;
private BizHawk.WinForms.Controls.LocLabelEx label2;
+ private System.Windows.Forms.CheckBox cbDeltaCompression;
}
}
\ No newline at end of file
diff --git a/src/BizHawk.Client.EmuHawk/config/RewindConfig.cs b/src/BizHawk.Client.EmuHawk/config/RewindConfig.cs
old mode 100644
new mode 100755
index a5c064afef..4de55031b7
--- a/src/BizHawk.Client.EmuHawk/config/RewindConfig.cs
+++ b/src/BizHawk.Client.EmuHawk/config/RewindConfig.cs
@@ -47,6 +47,7 @@ namespace BizHawk.Client.EmuHawk
RewindEnabledBox.Checked = _config.Rewind.Enabled;
UseCompression.Checked = _config.Rewind.UseCompression;
+ cbDeltaCompression.Checked = _config.Rewind.UseDelta;
BufferSizeUpDown.Value = Math.Max(_config.Rewind.BufferSize, BufferSizeUpDown.Minimum);
TargetFrameLengthNumeric.Value = Math.Max(_config.Rewind.TargetFrameLength, TargetFrameLengthNumeric.Minimum);
StateSizeLabel.Text = FormatKB(_avgStateSize);
@@ -111,6 +112,7 @@ namespace BizHawk.Client.EmuHawk
_config.Rewind.Enabled = PutRewindSetting(_config.Rewind.Enabled, RewindEnabledBox.Checked);
_config.Rewind.BufferSize = PutRewindSetting(_config.Rewind.BufferSize, (int)BufferSizeUpDown.Value);
_config.Rewind.TargetFrameLength = PutRewindSetting(_config.Rewind.TargetFrameLength, (int)TargetFrameLengthNumeric.Value);
+ _config.Rewind.UseDelta = PutRewindSetting(_config.Rewind.UseDelta, cbDeltaCompression.Checked);
// These settings are not used by DoRewindSettings
_config.Savestates.CompressionLevelNormal = (int)nudCompression.Value;