From f4e98fd9bd419dc71dc8bb25a74eac635ddd9afb Mon Sep 17 00:00:00 2001 From: nattthebear Date: Sun, 27 Dec 2020 18:36:42 -0500 Subject: [PATCH] Refactor ZWinderBuffer to allow use of arbitrary backing Streams (#2536) --- src/BizHawk.BizInvoke/MemoryViewStream.cs | 68 +++++++--- .../config/RewindConfig.cs | 11 +- .../rewind/ZwinderBuffer.cs | 128 +++++++++++------- 3 files changed, 143 insertions(+), 64 deletions(-) diff --git a/src/BizHawk.BizInvoke/MemoryViewStream.cs b/src/BizHawk.BizInvoke/MemoryViewStream.cs index f69e71e035..3a5e9a535e 100644 --- a/src/BizHawk.BizInvoke/MemoryViewStream.cs +++ b/src/BizHawk.BizInvoke/MemoryViewStream.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Runtime.InteropServices; +using BizHawk.Common; namespace BizHawk.BizInvoke { @@ -8,7 +9,7 @@ namespace BizHawk.BizInvoke /// Create a stream that allows read/write over a set of unmanaged memory pointers /// The validity and lifetime of those pointers is YOUR responsibility /// - public class MemoryViewStream : Stream + public unsafe class MemoryViewStream : Stream, ISpanStream { public MemoryViewStream(bool readable, bool writable, long ptr, long length) { @@ -50,20 +51,38 @@ namespace BizHawk.BizInvoke 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 buffer) { if (!_readable) - throw new InvalidOperationException(); - if (count < 0 || offset + count > buffer.Length) - throw new ArgumentOutOfRangeException(); + throw new IOException(); EnsureNotDisposed(); - - count = (int)Math.Min(count, _length - _pos); - Marshal.Copy(Z.SS(_ptr + _pos), buffer, offset, count); + var count = (int)Math.Min(buffer.Length, _length - _pos); + new ReadOnlySpan(CurrentPointer(), count).CopyTo(buffer); _pos += count; return count; } + public override int Read(byte[] buffer, int offset, int count) + { + return Read(new Span(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) { long newpos; @@ -86,19 +105,36 @@ namespace BizHawk.BizInvoke public override void SetLength(long value) { - throw new InvalidOperationException(); + throw new IOException(); + } + + public void Write(ReadOnlySpan buffer) + { + if (!_writable) + throw new IOException(); + EnsureNotDisposed(); + if (_pos + buffer.Length > _length) + throw new IOException("End of non-resizable stream"); + buffer.CopyTo(new Span(CurrentPointer(), buffer.Length)); + _pos += buffer.Length; } public override void Write(byte[] buffer, int offset, int count) { - if (!_writable) - throw new InvalidOperationException(); - if (count < 0 || _pos + count > _length || offset + count > buffer.Length) - throw new ArgumentOutOfRangeException(); - EnsureNotDisposed(); + Write(new ReadOnlySpan(buffer, offset, count)); + } - Marshal.Copy(buffer, offset, Z.SS(_ptr + _pos), count); - _pos += count; + public override void WriteByte(byte value) + { + if (_pos < _length) + { + *CurrentPointer() = value; + _pos++; + } + else + { + throw new IOException("End of non-resizable stream"); + } } protected override void Dispose(bool disposing) diff --git a/src/BizHawk.Client.Common/config/RewindConfig.cs b/src/BizHawk.Client.Common/config/RewindConfig.cs index 9c743b9086..36bc5b43bf 100644 --- a/src/BizHawk.Client.Common/config/RewindConfig.cs +++ b/src/BizHawk.Client.Common/config/RewindConfig.cs @@ -8,7 +8,7 @@ bool UseCompression { get; } /// - /// Max amount of buffer space to use in MB + /// Buffer space to use in MB /// long BufferSize { get; } @@ -16,6 +16,14 @@ /// Desired frame length (number of emulated frames you can go back before running out of buffer) /// int TargetFrameLength { get; } + + public enum BackingStoreType + { + Memory, + TempFile, + } + + public BackingStoreType BackingStore { get; } } public class RewindConfig : IRewindSettings @@ -24,5 +32,6 @@ public bool Enabled { get; set; } = true; public long BufferSize { get; set; } = 512; // in mb public int TargetFrameLength { get; set; } = 600; + public IRewindSettings.BackingStoreType BackingStore { get; set; } = IRewindSettings.BackingStoreType.Memory; } } diff --git a/src/BizHawk.Client.Common/rewind/ZwinderBuffer.cs b/src/BizHawk.Client.Common/rewind/ZwinderBuffer.cs index 0a3dc7dfaf..ecaba4dc2e 100644 --- a/src/BizHawk.Client.Common/rewind/ZwinderBuffer.cs +++ b/src/BizHawk.Client.Common/rewind/ZwinderBuffer.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.IO; using System.IO.Compression; +using System.Linq; using BizHawk.BizInvoke; using BizHawk.Common; @@ -26,8 +28,29 @@ namespace BizHawk.Client.Common Size = 1L << (int)Math.Floor(Math.Log(targetSize, 2)); _sizeMask = Size - 1; - _buffer = new MemoryBlock((ulong)Size); - _buffer.Protect(_buffer.Start, _buffer.Size, MemoryBlock.Protection.RW); + switch (settings.BackingStore) + { + 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; _states = new StateInfo[STATEMASK + 1]; _useCompression = settings.UseCompression; @@ -35,9 +58,12 @@ namespace BizHawk.Client.Common public void Dispose() { - _buffer.Dispose(); + foreach (var d in (_disposables as IEnumerable).Reverse()) + d.Dispose(); + _disposables.Clear(); } + private readonly List _disposables = new List(); /// /// 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; } private readonly long _sizeMask; - private readonly MemoryBlock _buffer; private readonly int _targetFrameLength; @@ -78,6 +103,8 @@ namespace BizHawk.Client.Common public int Frame; } + private readonly Stream _backingStore; + private readonly StateInfo[] _states; private int _firstStateIndex; private int _nextStateIndex; @@ -164,7 +191,7 @@ namespace BizHawk.Client.Common ? (_states[_firstStateIndex].Start - start) & _sizeMask : Size; }; - var stream = new SaveStateStream(_buffer, start, _sizeMask, initialMaxSize, notifySizeReached); + var stream = new SaveStateStream(_backingStore, start, _sizeMask, initialMaxSize, notifySizeReached); if (_useCompression) { @@ -186,7 +213,7 @@ namespace BizHawk.Client.Common 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) stream = new DeflateStream(stream, CompressionMode.Decompress, leaveOpen: true); return stream; @@ -256,18 +283,16 @@ namespace BizHawk.Client.Common { var startByte = _states[_firstStateIndex].Start; 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) { - { - var stream = _buffer.GetStream(_buffer.Start + (ulong)startByte, (ulong)(Size - startByte), false); - stream.CopyTo(writer.BaseStream); - } + _backingStore.Position = startByte; + WaterboxUtils.CopySome(_backingStore, writer.BaseStream, Size - startByte); startByte = 0; } { - var stream = _buffer.GetStream(_buffer.Start + (ulong)startByte, (ulong)(endByte - startByte), false); - stream.CopyTo(writer.BaseStream); + _backingStore.Position = startByte; + WaterboxUtils.CopySome(_backingStore, writer.BaseStream, endByte - startByte); } } } @@ -284,9 +309,8 @@ namespace BizHawk.Client.Common _states[i].Start = nextByte; nextByte += _states[i].Size; } - // TODO: Use spans to avoid this extra copy in .net core - var dest = _buffer.GetStream(_buffer.Start, (ulong)nextByte, true); - WaterboxUtils.CopySome(reader.BaseStream, dest, nextByte); + _backingStore.Position = 0; + WaterboxUtils.CopySome(reader.BaseStream, _backingStore, nextByte); } public static ZwinderBuffer Create(BinaryReader reader) @@ -314,7 +338,7 @@ namespace BizHawk.Client.Common /// /// /// - /// The ringbuffer to write into + /// 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 /// @@ -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 /// is preventing that case. /// - public SaveStateStream(MemoryBlock buffer, long offset, long mask, long notifySize, Func notifySizeReached) + public SaveStateStream(Stream backingStore, long offset, long mask, long notifySize, Func notifySizeReached) { - _ptr = (byte*)Z.US(buffer.Start); + _backingStore = backingStore; + _backingStoreSS = SpanStream.GetOrBuild(backingStore); _offset = offset; _mask = mask; _notifySize = notifySize; _notifySizeReached = notifySizeReached; } - - private readonly byte* _ptr; + + private readonly Stream _backingStore; + private readonly ISpanStream _backingStoreSS; private readonly long _offset; private readonly long _mask; private long _position; @@ -371,25 +397,20 @@ namespace BizHawk.Client.Common { var start = (_position + _offset) & _mask; var end = (start + n) & _mask; + _backingStore.Position = start; if (end < start) { long m = BufferLength - start; - - // Array.Copy(buffer, offset, _buffer, start, m); - buffer.Slice(0, (int)m).CopyTo(new Span(_ptr + start, (int)m)); - - // offset += (int)m; + _backingStoreSS.Write(buffer.Slice(0, (int)m)); buffer = buffer.Slice((int)m); - n -= m; _position += m; start = 0; + _backingStore.Position = start; } if (n > 0) { - // Array.Copy(buffer, offset, _buffer, start, n); - buffer.CopyTo(new Span(_ptr + start, (int)n)); - + _backingStoreSS.Write(buffer); _position += n; } } @@ -400,21 +421,26 @@ namespace BizHawk.Client.Common long requestedSize = _position + 1; while (requestedSize > _notifySize) _notifySize = _notifySizeReached(); - _ptr[(_position++ + _offset) & _mask] = value; + _backingStore.WriteByte(value); + _position++; + if (_position + _offset == BufferLength) + _backingStore.Position = 0; } } 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; _size = size; _mask = mask; } - private readonly byte* _ptr; + private readonly Stream _backingStore; + private readonly ISpanStream _backingStoreSS; private readonly long _offset; private readonly long _size; private long _position; @@ -430,8 +456,7 @@ namespace BizHawk.Client.Common get => _position; set => throw new IOException(); } - public override void Flush() - {} + public override void Flush() {} public override int Read(byte[] buffer, int offset, int count) { @@ -446,24 +471,22 @@ namespace BizHawk.Client.Common { var start = (_position + _offset) & _mask; var end = (start + n) & _mask; + _backingStore.Position = start; if (end < start) { long m = BufferLength - start; - - // Array.Copy(_buffer, start, buffer, offset, m); - new ReadOnlySpan(_ptr + start, (int)m).CopyTo(buffer); - - // offset += (int)m; + if (_backingStoreSS.Read(buffer.Slice(0, (int)m)) != (int)m) + throw new IOException("Unexpected end of underlying buffer"); buffer = buffer.Slice((int)m); - n -= m; _position += m; start = 0; + _backingStore.Position = start; } if (n > 0) { - // Array.Copy(_buffer, start, buffer, offset, n); - new ReadOnlySpan(_ptr + start, (int)n).CopyTo(buffer); + if (_backingStoreSS.Read(buffer.Slice(0, (int)n)) != (int)n) + throw new IOException("Unexpected end of underlying buffer"); _position += n; } } @@ -472,9 +495,20 @@ namespace BizHawk.Client.Common public override int ReadByte() { - return _position < _size - ? _ptr[(_position++ + _offset) & _mask] - : -1; + if (_position < _size) + { + 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();