Add delta compression rewinder (#2733)
Good size reduction for some cores, less useful for others. Typically moderately slower. Has some threading built in (cannot be disabled). Example speeds with PSX core: 104 fps with delta rewinder 112 fps normal rewinder (no compression) 124 fps without rewinder
This commit is contained in:
parent
3ea71a2dda
commit
547bf6d308
|
@ -4,6 +4,7 @@
|
|||
</PropertyGroup>
|
||||
<Import Project="../MainSlnCommon.props" />
|
||||
<PropertyGroup>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
|
|
|
@ -7,6 +7,13 @@
|
|||
/// </summary>
|
||||
bool UseCompression { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not to delta compress savestates before storing them
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
// TODO: This is in here for frontend reasons, but the buffer itself doesn't interact with this.
|
||||
bool UseDelta { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Buffer space to use in MB
|
||||
/// </summary>
|
||||
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A rewinder that uses Zelda compression, built on top of a ring buffer
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How many states are actually in the state ringbuffer
|
||||
/// </summary>
|
||||
public int Count { get { Sync(); return _count; } }
|
||||
|
||||
public float FullnessRatio { get { Sync(); return _buffer.Used / (float)_buffer.Size; } }
|
||||
|
||||
/// <summary>
|
||||
/// Total size of the _buffer
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public long Size => _buffer.Size;
|
||||
|
||||
/// <summary>
|
||||
/// TODO: This is not a frequency, it's the reciprocal
|
||||
/// </summary>
|
||||
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<byte>(&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<byte>(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<byte>(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<byte>(&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<byte>(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<byte>(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<byte> buffer)
|
||||
{
|
||||
var requestedSize = _position + buffer.Length;
|
||||
MaybeResize(requestedSize);
|
||||
buffer.CopyTo(new Span<byte>(_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<byte> buffer) => throw new IOException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -164,6 +164,17 @@ namespace BizHawk.Client.Common
|
|||
return frameDiff >= ComputeIdealRewindInterval();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Predict whether Capture() will capture a state. Useful if expensive work needs to happen before
|
||||
/// Capture() is called.
|
||||
/// </summary>
|
||||
/// <param name="frame">The same frame number to be passed to capture</param>
|
||||
/// <returns>Whether capture will happen, assuming Capture() is passed the same frame and force = false</returns>
|
||||
public bool WillCapture(int frame)
|
||||
{
|
||||
return ShouldCapture(frame);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maybe captures a state, if the conditions are favorable
|
||||
/// </summary>
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
17
src/BizHawk.Client.EmuHawk/config/RewindConfig.Designer.cs
generated
Normal file → Executable file
17
src/BizHawk.Client.EmuHawk/config/RewindConfig.Designer.cs
generated
Normal file → Executable file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue