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:
nattthebear 2021-05-08 08:47:12 -04:00 committed by GitHub
parent 3ea71a2dda
commit 547bf6d308
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 348 additions and 3 deletions

View File

@ -4,6 +4,7 @@
</PropertyGroup>
<Import Project="../MainSlnCommon.props" />
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>

View File

@ -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;

View File

@ -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();
}
}
}

View File

@ -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>

View File

@ -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
View 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;
}
}

2
src/BizHawk.Client.EmuHawk/config/RewindConfig.cs Normal file → Executable file
View File

@ -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;