Refactor ZWinderBuffer to allow use of arbitrary backing Streams (#2536)

This commit is contained in:
nattthebear 2020-12-27 18:36:42 -05:00 committed by GitHub
parent bcf5347823
commit f4e98fd9bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 143 additions and 64 deletions

View File

@ -1,6 +1,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using BizHawk.Common;
namespace BizHawk.BizInvoke namespace BizHawk.BizInvoke
{ {
@ -8,7 +9,7 @@ namespace BizHawk.BizInvoke
/// Create a stream that allows read/write over a set of unmanaged memory pointers /// Create a stream that allows read/write over a set of unmanaged memory pointers
/// The validity and lifetime of those pointers is YOUR responsibility /// The validity and lifetime of those pointers is YOUR responsibility
/// </summary> /// </summary>
public class MemoryViewStream : Stream public unsafe class MemoryViewStream : Stream, ISpanStream
{ {
public MemoryViewStream(bool readable, bool writable, long ptr, long length) public MemoryViewStream(bool readable, bool writable, long ptr, long length)
{ {
@ -50,20 +51,38 @@ namespace BizHawk.BizInvoke
public override void Flush() {} public override void Flush() {}
public override int Read(byte[] buffer, int offset, int count) private byte* CurrentPointer() => (byte*)Z.SS(_ptr + _pos);
public int Read(Span<byte> buffer)
{ {
if (!_readable) if (!_readable)
throw new InvalidOperationException(); throw new IOException();
if (count < 0 || offset + count > buffer.Length)
throw new ArgumentOutOfRangeException();
EnsureNotDisposed(); EnsureNotDisposed();
var count = (int)Math.Min(buffer.Length, _length - _pos);
count = (int)Math.Min(count, _length - _pos); new ReadOnlySpan<byte>(CurrentPointer(), count).CopyTo(buffer);
Marshal.Copy(Z.SS(_ptr + _pos), buffer, offset, count);
_pos += count; _pos += count;
return count; return count;
} }
public override int Read(byte[] buffer, int offset, int count)
{
return Read(new Span<byte>(buffer, offset, count));
}
public override int ReadByte()
{
if (_pos < _length)
{
var ret = *CurrentPointer();
_pos++;
return ret;
}
else
{
return -1;
}
}
public override long Seek(long offset, SeekOrigin origin) public override long Seek(long offset, SeekOrigin origin)
{ {
long newpos; long newpos;
@ -86,19 +105,36 @@ namespace BizHawk.BizInvoke
public override void SetLength(long value) public override void SetLength(long value)
{ {
throw new InvalidOperationException(); throw new IOException();
}
public void Write(ReadOnlySpan<byte> buffer)
{
if (!_writable)
throw new IOException();
EnsureNotDisposed();
if (_pos + buffer.Length > _length)
throw new IOException("End of non-resizable stream");
buffer.CopyTo(new Span<byte>(CurrentPointer(), buffer.Length));
_pos += buffer.Length;
} }
public override void Write(byte[] buffer, int offset, int count) public override void Write(byte[] buffer, int offset, int count)
{ {
if (!_writable) Write(new ReadOnlySpan<byte>(buffer, offset, count));
throw new InvalidOperationException(); }
if (count < 0 || _pos + count > _length || offset + count > buffer.Length)
throw new ArgumentOutOfRangeException();
EnsureNotDisposed();
Marshal.Copy(buffer, offset, Z.SS(_ptr + _pos), count); public override void WriteByte(byte value)
_pos += count; {
if (_pos < _length)
{
*CurrentPointer() = value;
_pos++;
}
else
{
throw new IOException("End of non-resizable stream");
}
} }
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)

View File

@ -8,7 +8,7 @@
bool UseCompression { get; } bool UseCompression { get; }
/// <summary> /// <summary>
/// Max amount of buffer space to use in MB /// Buffer space to use in MB
/// </summary> /// </summary>
long BufferSize { get; } long BufferSize { get; }
@ -16,6 +16,14 @@
/// Desired frame length (number of emulated frames you can go back before running out of buffer) /// Desired frame length (number of emulated frames you can go back before running out of buffer)
/// </summary> /// </summary>
int TargetFrameLength { get; } int TargetFrameLength { get; }
public enum BackingStoreType
{
Memory,
TempFile,
}
public BackingStoreType BackingStore { get; }
} }
public class RewindConfig : IRewindSettings public class RewindConfig : IRewindSettings
@ -24,5 +32,6 @@
public bool Enabled { get; set; } = true; public bool Enabled { get; set; } = true;
public long BufferSize { get; set; } = 512; // in mb public long BufferSize { get; set; } = 512; // in mb
public int TargetFrameLength { get; set; } = 600; public int TargetFrameLength { get; set; } = 600;
public IRewindSettings.BackingStoreType BackingStore { get; set; } = IRewindSettings.BackingStoreType.Memory;
} }
} }

View File

@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq;
using BizHawk.BizInvoke; using BizHawk.BizInvoke;
using BizHawk.Common; using BizHawk.Common;
@ -26,8 +28,29 @@ namespace BizHawk.Client.Common
Size = 1L << (int)Math.Floor(Math.Log(targetSize, 2)); Size = 1L << (int)Math.Floor(Math.Log(targetSize, 2));
_sizeMask = Size - 1; _sizeMask = Size - 1;
_buffer = new MemoryBlock((ulong)Size); switch (settings.BackingStore)
_buffer.Protect(_buffer.Start, _buffer.Size, MemoryBlock.Protection.RW); {
case IRewindSettings.BackingStoreType.Memory:
{
var buffer = new MemoryBlock((ulong)Size);
buffer.Protect(buffer.Start, buffer.Size, MemoryBlock.Protection.RW);
_disposables.Add(buffer);
_backingStore = new MemoryViewStream(true, true, (long)buffer.Start, (long)buffer.Size);
_disposables.Add(_backingStore);
break;
}
case IRewindSettings.BackingStoreType.TempFile:
{
var filename = TempFileManager.GetTempFilename("ZwinderBuffer");
var filestream = new FileStream(filename, FileMode.Create, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.DeleteOnClose);
filestream.SetLength(Size);
_backingStore = filestream;
_disposables.Add(filestream);
break;
}
default:
throw new Exception();
}
_targetFrameLength = settings.TargetFrameLength; _targetFrameLength = settings.TargetFrameLength;
_states = new StateInfo[STATEMASK + 1]; _states = new StateInfo[STATEMASK + 1];
_useCompression = settings.UseCompression; _useCompression = settings.UseCompression;
@ -35,9 +58,12 @@ namespace BizHawk.Client.Common
public void Dispose() public void Dispose()
{ {
_buffer.Dispose(); foreach (var d in (_disposables as IEnumerable<IDisposable>).Reverse())
d.Dispose();
_disposables.Clear();
} }
private readonly List<IDisposable> _disposables = new List<IDisposable>();
/// <summary> /// <summary>
/// Number of states that could be in the state ringbuffer, Mask for the state ringbuffer /// Number of states that could be in the state ringbuffer, Mask for the state ringbuffer
@ -67,7 +93,6 @@ namespace BizHawk.Client.Common
public long Size { get; } public long Size { get; }
private readonly long _sizeMask; private readonly long _sizeMask;
private readonly MemoryBlock _buffer;
private readonly int _targetFrameLength; private readonly int _targetFrameLength;
@ -78,6 +103,8 @@ namespace BizHawk.Client.Common
public int Frame; public int Frame;
} }
private readonly Stream _backingStore;
private readonly StateInfo[] _states; private readonly StateInfo[] _states;
private int _firstStateIndex; private int _firstStateIndex;
private int _nextStateIndex; private int _nextStateIndex;
@ -164,7 +191,7 @@ namespace BizHawk.Client.Common
? (_states[_firstStateIndex].Start - start) & _sizeMask ? (_states[_firstStateIndex].Start - start) & _sizeMask
: Size; : Size;
}; };
var stream = new SaveStateStream(_buffer, start, _sizeMask, initialMaxSize, notifySizeReached); var stream = new SaveStateStream(_backingStore, start, _sizeMask, initialMaxSize, notifySizeReached);
if (_useCompression) if (_useCompression)
{ {
@ -186,7 +213,7 @@ namespace BizHawk.Client.Common
private Stream MakeLoadStream(int index) private Stream MakeLoadStream(int index)
{ {
Stream stream = new LoadStateStream(_buffer, _states[index].Start, _states[index].Size, _sizeMask); Stream stream = new LoadStateStream(_backingStore, _states[index].Start, _states[index].Size, _sizeMask);
if (_useCompression) if (_useCompression)
stream = new DeflateStream(stream, CompressionMode.Decompress, leaveOpen: true); stream = new DeflateStream(stream, CompressionMode.Decompress, leaveOpen: true);
return stream; return stream;
@ -256,18 +283,16 @@ namespace BizHawk.Client.Common
{ {
var startByte = _states[_firstStateIndex].Start; var startByte = _states[_firstStateIndex].Start;
var endByte = (_states[HeadStateIndex].Start + _states[HeadStateIndex].Size) & _sizeMask; var endByte = (_states[HeadStateIndex].Start + _states[HeadStateIndex].Size) & _sizeMask;
// TODO: Use spans to avoid these extra copies in .net core var destStream = SpanStream.GetOrBuild(writer.BaseStream);
if (startByte > endByte) if (startByte > endByte)
{ {
{ _backingStore.Position = startByte;
var stream = _buffer.GetStream(_buffer.Start + (ulong)startByte, (ulong)(Size - startByte), false); WaterboxUtils.CopySome(_backingStore, writer.BaseStream, Size - startByte);
stream.CopyTo(writer.BaseStream);
}
startByte = 0; startByte = 0;
} }
{ {
var stream = _buffer.GetStream(_buffer.Start + (ulong)startByte, (ulong)(endByte - startByte), false); _backingStore.Position = startByte;
stream.CopyTo(writer.BaseStream); WaterboxUtils.CopySome(_backingStore, writer.BaseStream, endByte - startByte);
} }
} }
} }
@ -284,9 +309,8 @@ namespace BizHawk.Client.Common
_states[i].Start = nextByte; _states[i].Start = nextByte;
nextByte += _states[i].Size; nextByte += _states[i].Size;
} }
// TODO: Use spans to avoid this extra copy in .net core _backingStore.Position = 0;
var dest = _buffer.GetStream(_buffer.Start, (ulong)nextByte, true); WaterboxUtils.CopySome(reader.BaseStream, _backingStore, nextByte);
WaterboxUtils.CopySome(reader.BaseStream, dest, nextByte);
} }
public static ZwinderBuffer Create(BinaryReader reader) public static ZwinderBuffer Create(BinaryReader reader)
@ -314,7 +338,7 @@ namespace BizHawk.Client.Common
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
/// <param name="buffer">The ringbuffer to write into</param> /// <param name="backingStore">The ringbuffer to write into</param>
/// <param name="offset">Offset into the buffer to start writing (and treat as position 0 in the stream)</param> /// <param name="offset">Offset into the buffer to start writing (and treat as position 0 in the stream)</param>
/// <param name="mask">Buffer size mask, used to wrap values in the ringbuffer correctly</param> /// <param name="mask">Buffer size mask, used to wrap values in the ringbuffer correctly</param>
/// <param name="notifySize"> /// <param name="notifySize">
@ -325,16 +349,18 @@ namespace BizHawk.Client.Common
/// or abort processing with an IOException. This must fail if size is going to exceed buffer.Length, as nothing else /// or abort processing with an IOException. This must fail if size is going to exceed buffer.Length, as nothing else
/// is preventing that case. /// is preventing that case.
/// </param> /// </param>
public SaveStateStream(MemoryBlock buffer, long offset, long mask, long notifySize, Func<long> notifySizeReached) public SaveStateStream(Stream backingStore, long offset, long mask, long notifySize, Func<long> notifySizeReached)
{ {
_ptr = (byte*)Z.US(buffer.Start); _backingStore = backingStore;
_backingStoreSS = SpanStream.GetOrBuild(backingStore);
_offset = offset; _offset = offset;
_mask = mask; _mask = mask;
_notifySize = notifySize; _notifySize = notifySize;
_notifySizeReached = notifySizeReached; _notifySizeReached = notifySizeReached;
} }
private readonly byte* _ptr; private readonly Stream _backingStore;
private readonly ISpanStream _backingStoreSS;
private readonly long _offset; private readonly long _offset;
private readonly long _mask; private readonly long _mask;
private long _position; private long _position;
@ -371,25 +397,20 @@ namespace BizHawk.Client.Common
{ {
var start = (_position + _offset) & _mask; var start = (_position + _offset) & _mask;
var end = (start + n) & _mask; var end = (start + n) & _mask;
_backingStore.Position = start;
if (end < start) if (end < start)
{ {
long m = BufferLength - start; long m = BufferLength - start;
_backingStoreSS.Write(buffer.Slice(0, (int)m));
// Array.Copy(buffer, offset, _buffer, start, m);
buffer.Slice(0, (int)m).CopyTo(new Span<byte>(_ptr + start, (int)m));
// offset += (int)m;
buffer = buffer.Slice((int)m); buffer = buffer.Slice((int)m);
n -= m; n -= m;
_position += m; _position += m;
start = 0; start = 0;
_backingStore.Position = start;
} }
if (n > 0) if (n > 0)
{ {
// Array.Copy(buffer, offset, _buffer, start, n); _backingStoreSS.Write(buffer);
buffer.CopyTo(new Span<byte>(_ptr + start, (int)n));
_position += n; _position += n;
} }
} }
@ -400,21 +421,26 @@ namespace BizHawk.Client.Common
long requestedSize = _position + 1; long requestedSize = _position + 1;
while (requestedSize > _notifySize) while (requestedSize > _notifySize)
_notifySize = _notifySizeReached(); _notifySize = _notifySizeReached();
_ptr[(_position++ + _offset) & _mask] = value; _backingStore.WriteByte(value);
_position++;
if (_position + _offset == BufferLength)
_backingStore.Position = 0;
} }
} }
private unsafe class LoadStateStream : Stream, ISpanStream private unsafe class LoadStateStream : Stream, ISpanStream
{ {
public LoadStateStream(MemoryBlock buffer, long offset, long size, long mask) public LoadStateStream(Stream backingStore, long offset, long size, long mask)
{ {
_ptr = (byte*)Z.US(buffer.Start); _backingStore = backingStore;
_backingStoreSS = SpanStream.GetOrBuild(backingStore);
_offset = offset; _offset = offset;
_size = size; _size = size;
_mask = mask; _mask = mask;
} }
private readonly byte* _ptr; private readonly Stream _backingStore;
private readonly ISpanStream _backingStoreSS;
private readonly long _offset; private readonly long _offset;
private readonly long _size; private readonly long _size;
private long _position; private long _position;
@ -430,8 +456,7 @@ namespace BizHawk.Client.Common
get => _position; get => _position;
set => throw new IOException(); set => throw new IOException();
} }
public override void Flush() public override void Flush() {}
{}
public override int Read(byte[] buffer, int offset, int count) public override int Read(byte[] buffer, int offset, int count)
{ {
@ -446,24 +471,22 @@ namespace BizHawk.Client.Common
{ {
var start = (_position + _offset) & _mask; var start = (_position + _offset) & _mask;
var end = (start + n) & _mask; var end = (start + n) & _mask;
_backingStore.Position = start;
if (end < start) if (end < start)
{ {
long m = BufferLength - start; long m = BufferLength - start;
if (_backingStoreSS.Read(buffer.Slice(0, (int)m)) != (int)m)
// Array.Copy(_buffer, start, buffer, offset, m); throw new IOException("Unexpected end of underlying buffer");
new ReadOnlySpan<byte>(_ptr + start, (int)m).CopyTo(buffer);
// offset += (int)m;
buffer = buffer.Slice((int)m); buffer = buffer.Slice((int)m);
n -= m; n -= m;
_position += m; _position += m;
start = 0; start = 0;
_backingStore.Position = start;
} }
if (n > 0) if (n > 0)
{ {
// Array.Copy(_buffer, start, buffer, offset, n); if (_backingStoreSS.Read(buffer.Slice(0, (int)n)) != (int)n)
new ReadOnlySpan<byte>(_ptr + start, (int)n).CopyTo(buffer); throw new IOException("Unexpected end of underlying buffer");
_position += n; _position += n;
} }
} }
@ -472,9 +495,20 @@ namespace BizHawk.Client.Common
public override int ReadByte() public override int ReadByte()
{ {
return _position < _size if (_position < _size)
? _ptr[(_position++ + _offset) & _mask] {
: -1; var ret = _backingStore.ReadByte();
if (ret == -1)
throw new IOException("Unexpected end of underlying buffer");
_position++;
if (_position + _offset == BufferLength)
_backingStore.Position = 0;
return ret;
}
else
{
return -1;
}
} }
public override long Seek(long offset, SeekOrigin origin) => throw new IOException(); public override long Seek(long offset, SeekOrigin origin) => throw new IOException();