Refactor ZWinderBuffer to allow use of arbitrary backing Streams (#2536)
This commit is contained in:
parent
bcf5347823
commit
f4e98fd9bd
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in New Issue