// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; namespace EpicGames.Horde.Compute { /// /// In-process buffer used to store compute messages /// public abstract class ComputeBuffer : IDisposable { /// /// Maximum number of chunks in a buffer /// public const int MaxChunks = 16; /// /// Maximum number of readers /// public const int MaxReaders = 16; internal ComputeBufferDetail _detail; /// /// Constructor /// /// Resources shared between instances of the buffer internal ComputeBuffer(ComputeBufferDetail detail) { _detail = detail; } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Overridable dispose method /// protected virtual void Dispose(bool disposing) { if (_detail != null) { _detail.Release(); _detail = null!; } } /// /// Creates a new reader for this buffer /// public ComputeBufferReader CreateReader() { int readerIdx = _detail.CreateReader(); _detail.AddRef(); return new ComputeBufferReader(_detail, readerIdx); } /// /// Writer for this buffer /// public ComputeBufferWriter CreateWriter() { _detail.CreateWriter(); _detail.AddRef(); return new ComputeBufferWriter(_detail); } /// /// Creates a new reference to the underlying buffer. The underlying resources will only be destroyed once all instances are disposed of. /// public abstract ComputeBuffer AddRef(); } /// /// Read interface for a compute buffer /// public sealed class ComputeBufferReader : IDisposable { ComputeBufferDetail _buffer; readonly int _readerIdx; internal ComputeBufferDetail Detail => _buffer; internal ComputeBufferReader(ComputeBufferDetail buffer, int readerIdx) { _buffer = buffer; _readerIdx = readerIdx; } /// /// Create a new reader instance using the same underlying buffer /// public ComputeBufferReader AddRef() { _buffer.AddRef(); _buffer.AddReaderRef(_readerIdx); return new ComputeBufferReader(_buffer, _readerIdx); } /// public void Dispose() { if (_buffer != null) { _buffer.ReleaseReaderRef(_readerIdx); _buffer.Release(); _buffer = null!; } } /// /// Detaches this reader from the underlying buffer /// public void Detach() => _buffer.DetachReader(_readerIdx); /// /// Whether this buffer is complete (no more data will be added) /// public bool IsComplete => _buffer.IsComplete(_readerIdx); /// /// Updates the read position /// /// Size of data that was read public void AdvanceReadPosition(int length) => _buffer.AdvanceReadPosition(_readerIdx, length); /// /// Gets the next data to read /// /// Memory to read from public ReadOnlyMemory GetReadBuffer() => _buffer.GetReadBuffer(_readerIdx); /// /// Read from a buffer into another buffer /// /// Memory to receive the read data /// Cancellation token for the operation /// Number of bytes read public async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { for (; ; ) { ReadOnlyMemory readMemory = GetReadBuffer(); if (IsComplete || readMemory.Length > 0) { int length = Math.Min(readMemory.Length, buffer.Length); readMemory.Slice(0, length).CopyTo(buffer); AdvanceReadPosition(length); return length; } await WaitToReadAsync(1, cancellationToken); } } /// /// Wait for data to be available, or for the buffer to be marked as complete /// /// Minimum amount of data to read /// Cancellation token for the operation /// True if new data is available, false if the buffer is complete public ValueTask WaitToReadAsync(int minLength, CancellationToken cancellationToken = default) => _buffer.WaitToReadAsync(_readerIdx, minLength, cancellationToken); } /// /// Buffer that can receive data from a remote machine. /// public sealed class ComputeBufferWriter : IDisposable { ComputeBufferDetail _detail; internal ComputeBufferDetail Detail => _detail; internal ComputeBufferWriter(ComputeBufferDetail detail) => _detail = detail; /// /// Create a new writer instance using the same underlying buffer /// public ComputeBufferWriter AddRef() { _detail.AddRef(); _detail.AddWriterRef(); return new ComputeBufferWriter(_detail); } /// public void Dispose() { if (_detail != null) { _detail.ReleaseWriterRef(); _detail.Release(); _detail = null!; } } /// public void AdvanceWritePosition(int size) => _detail.AdvanceWritePosition(size); /// /// Gets memory to write to /// /// Memory to be written to public Memory GetWriteBuffer() => _detail.GetWriteBuffer(); /// /// Mark the output to this buffer as complete /// /// Whether the writer was marked as complete. False if the writer has already been marked as complete. public bool MarkComplete() => _detail.MarkComplete(); /// /// Writes data into a buffer from a memory block /// /// The data to write /// Cancellation token for the operation public async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { while (buffer.Length > 0) { Memory writeMemory = GetWriteBuffer(); if (writeMemory.Length >= buffer.Length) { buffer.CopyTo(writeMemory); AdvanceWritePosition(buffer.Length); break; } await WaitToWriteAsync(buffer.Length, cancellationToken); } } /// /// Gets memory to write to /// /// Minimum size of the desired write buffer /// Cancellation token for the operation /// Memory to be written to public ValueTask WaitToWriteAsync(int minLength, CancellationToken cancellationToken = default) => _detail.WaitToWriteAsync(minLength, cancellationToken); } /// /// State shared between buffer instances /// [DebuggerTypeProxy(typeof(BufferDebugProxy))] internal abstract class ComputeBufferDetail : IDisposable { internal const int HeaderSize = (2 + ComputeBuffer.MaxReaders + ComputeBuffer.MaxChunks) * sizeof(ulong); /// /// Write state for a chunk /// protected internal enum WriteState { /// /// Writer has moved to the next chunk /// MovedToNext = 0, /// /// Chunk is still being appended to /// Writing = 2, /// /// This chunk marks the end of the stream /// Complete = 3, } /// /// Stores the state of a chunk in a 64-bit value, which can be updated atomically /// protected internal record struct ChunkState(ulong Value) { // Written length of this chunk public readonly int Length => (int)(Value & 0x7fffffff); // Set of flags which are set for each reader that still has to read from a chunk public readonly int ReaderFlags => (int)((Value >> 31) & 0x7fffffff); // State of the writer public readonly WriteState WriteState => (WriteState)(Value >> 62); // Constructor public ChunkState(WriteState writerState, int readerFlags, int length) : this(((ulong)writerState << 62) | ((ulong)readerFlags << 31) | (uint)length) { } // Test whether a particular reader is still referencing the chunk public readonly bool HasReaderFlag(int readerIdx) => (Value & (1UL << (31 + readerIdx))) != 0; /// public override readonly string ToString() => $"{WriteState}, Length: {Length}, Readers: {ReaderFlags}"; } /// /// Wraps a pointer to the state of a chunk /// protected internal readonly unsafe struct ChunkStatePtr { readonly ulong* _data; public ChunkStatePtr(ulong* data) => _data = data; // Current value of the chunk state public ChunkState Get() => new ChunkState(Interlocked.CompareExchange(ref *_data, 0, 0)); // Set the current state public void Set(ChunkState value) => Interlocked.Exchange(ref *_data, value.Value); // Attempt to update the chunk state public bool TryUpdate(ChunkState prevState, ChunkState nextState) => Interlocked.CompareExchange(ref *_data, nextState.Value, prevState.Value) == prevState.Value; // Append data to the chunk public void Append(int length) => Interlocked.Add(ref *_data, (ulong)length); // Move to the next chunk public void MarkComplete() => Interlocked.Or(ref *_data, new ChunkState(WriteState.Complete, 0, 0).Value); // Start reading the chunk with the given reader public void StartReading(int readerIdx) => Interlocked.Or(ref *_data, new ChunkState(0, 1 << readerIdx, 0).Value); // Clear the reader flag public void FinishReading(int readerIdx) => Interlocked.And(ref *_data, ~new ChunkState(0, 1 << readerIdx, 0).Value); // Move to the next chunk public void FinishWriting() => Interlocked.And(ref *_data, ~new ChunkState(WriteState.Writing, 0, 0).Value); /// public override string ToString() => Get().ToString(); } /// /// State of a reader /// protected internal record struct ReaderState(ulong Value) { public ReaderState(int chunkIdx, int offset, int refCount, bool detached) : this((ulong)(uint)offset | ((ulong)(uint)chunkIdx << 32) | ((ulong)(uint)refCount << 40) | ((ulong)((detached ? (1UL << 63) : 0)))) { } public readonly int Offset => (int)(Value & 0xffffffff); public readonly int ChunkIdx => (int)((Value >> 32) & 0xff); public readonly int RefCount => (int)((Value >> 40) & 0x7fff); public readonly bool Detached => (Value & (1UL << 63)) != 0; /// public override readonly string ToString() => $"Chunk: {ChunkIdx}, Offset: {Offset}, RefCount: {RefCount}, Detached: {Detached}"; } /// /// Wraps a pointer to the state of a writer /// protected internal readonly unsafe struct ReaderStatePtr { readonly ulong* _data; public ReaderStatePtr(ulong* data) => _data = data; // Current value of the chunk state public ReaderState Get() => new ReaderState(Interlocked.CompareExchange(ref *_data, 0, 0)); // Update current state public void Set(ReaderState value) => Interlocked.Exchange(ref *_data, value.Value); // Compare and swap public bool TryUpdate(ReaderState prevState, ReaderState nextState) => Interlocked.CompareExchange(ref *_data, nextState.Value, prevState.Value) == prevState.Value; } /// /// State of the writer /// protected internal record struct WriterState(ulong Value) { public WriterState(int chunkIdx, int readerFlags, int refCount, bool hasWrapped) : this((ulong)(uint)chunkIdx | ((ulong)(uint)readerFlags << 32) | ((ulong)(uint)refCount << 48) | (hasWrapped ? (1UL << 63) : 0)) { } public readonly int ChunkIdx => (int)(Value & 0x7fffffff); public readonly int ReaderFlags => (int)(Value >> 32) & 0xffff; public readonly int RefCount => (int)(Value >> 48) & 0x7fff; public readonly bool HasWrapped => (Value & (1UL << 63)) != 0; /// public override readonly string ToString() => $"Chunk: {ChunkIdx}, ReaderFlags: {ReaderFlags}, RefCount: {RefCount}, HasWrapped: {HasWrapped}"; } /// /// Wraps a pointer to the state of a writer /// protected internal readonly unsafe struct WriterStatePtr { readonly ulong* _data; public WriterStatePtr(ulong* data) => _data = data; // Get the current value public WriterState Get() => new WriterState(Interlocked.CompareExchange(ref *_data, 0, 0)); // Set the current value public void Set(WriterState state) => Interlocked.Exchange(ref *_data, state.Value); // Compare and swap public bool TryUpdate(WriterState prevState, WriterState nextState) => Interlocked.CompareExchange(ref *_data, nextState.Value, prevState.Value) == prevState.Value; } /// /// Tracked state of the buffer /// protected internal readonly unsafe struct HeaderPtr { readonly ulong* _data; public HeaderPtr(ulong* data) => _data = data; public HeaderPtr(ulong* data, int numReaders, int numChunks, int chunkLength) { _data = data; data[0] = ((ulong)(uint)chunkLength << 32) | ((ulong)(uint)numChunks << 16) | (uint)numReaders; GetChunkStatePtr(0).Set(new ChunkState(WriteState.Writing, 0, 0)); } public int NumReaders => (int)(_data[0] & 0xffff); public int NumChunks => (int)((_data[0] >> 16) & 0xffff); public int ChunkLength => (int)(_data[0] >> 32); public WriterStatePtr GetWriterStatePtr() => new WriterStatePtr(_data + 1); public ReaderStatePtr GetReaderStatePtr(int readerIdx) => new ReaderStatePtr(_data + 2 + readerIdx); public ChunkStatePtr GetChunkStatePtr(int chunkIdx) => new ChunkStatePtr(_data + 2 + ComputeBuffer.MaxReaders + chunkIdx); } class BufferDebugProxy { public int ChunkLength { get; } public int RefCount { get; } public WriterState Writer { get; } public ReaderState[] Readers { get; } public ChunkState[] Chunks { get; } public BufferDebugProxy(ComputeBufferDetail buffer) { RefCount = buffer._refCount; HeaderPtr headerPtr = buffer._headerPtr; ChunkLength = headerPtr.ChunkLength; Writer = headerPtr.GetWriterStatePtr().Get(); Chunks = new ChunkState[headerPtr.NumChunks]; for (int chunkIdx = 0; chunkIdx < headerPtr.NumChunks; chunkIdx++) { Chunks[chunkIdx] = headerPtr.GetChunkStatePtr(chunkIdx).Get(); } Readers = new ReaderState[headerPtr.NumReaders]; for (int readerIdx = 0; readerIdx < headerPtr.NumReaders; readerIdx++) { Readers[readerIdx] = headerPtr.GetReaderStatePtr(readerIdx).Get(); } } } HeaderPtr _headerPtr; Memory[] _chunks; int _refCount = 1; /// /// Constructor /// protected ComputeBufferDetail(HeaderPtr headerPtr, Memory[] chunks) { _headerPtr = headerPtr; _chunks = chunks; } /// /// Increment the reference count on this object /// public void AddRef() { Interlocked.Increment(ref _refCount); } /// /// Decrement the reference count on this object, and dispose of it once it reaches zero /// public void Release() { if (Interlocked.Decrement(ref _refCount) == 0) { Dispose(); } } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Overridable dispose method /// protected virtual void Dispose(bool disposing) { _headerPtr = default; _chunks = Array.Empty>(); } /// /// Signals a read event /// public abstract void SetReadEvent(int readerIdx); /// /// Signals read events for every reader /// public void SetAllReadEvents() { for (int readerIdx = 0; readerIdx < _headerPtr.NumReaders; readerIdx++) { SetReadEvent(readerIdx); } } /// /// Waits for a read event to be signalled /// public abstract Task WaitToReadAsync(int readerIdx, CancellationToken cancellationToken); /// /// Signals the write event /// public abstract void SetWriteEvent(); /// /// Waits for the write event to be signalled /// public abstract Task WaitToWriteAsync(CancellationToken cancellationToken); /// /// Allocate a new reader /// public int CreateReader() { for (int readerIdx = 0; readerIdx < _headerPtr.NumReaders; readerIdx++) { ReaderStatePtr readerStatePtr = _headerPtr.GetReaderStatePtr(readerIdx); for (; ; ) { ReaderState readerState = readerStatePtr.Get(); if (readerState.RefCount > 0) { break; } if (readerStatePtr.TryUpdate(readerState, new ReaderState(0, 0, 1, false))) { WriterStatePtr writerStatePtr = _headerPtr.GetWriterStatePtr(); for (; ; ) { WriterState writerState = writerStatePtr.Get(); if (writerState.HasWrapped) { throw new InvalidOperationException("Cannot create a new reader after writer has wrapped back to the first chunk"); } if (writerStatePtr.TryUpdate(writerState, new WriterState(writerState.ChunkIdx, writerState.ReaderFlags | (1 << readerIdx), writerState.RefCount, writerState.HasWrapped))) { for (int writeChunkIdx = 0; writeChunkIdx <= writerState.ChunkIdx; writeChunkIdx++) { _headerPtr.GetChunkStatePtr(writeChunkIdx).StartReading(readerIdx); } return readerIdx; } } } } } throw new InvalidOperationException("Unable to allocate reader; all available readers are in use."); } public void AddReaderRef(int readerIdx) { ReaderStatePtr readerStatePtr = _headerPtr.GetReaderStatePtr(readerIdx); for (; ; ) { ReaderState readerState = readerStatePtr.Get(); if (readerState.RefCount == 0) { throw new InvalidOperationException("Refcount for reader is zero"); } if (readerStatePtr.TryUpdate(readerState, new ReaderState(readerState.ChunkIdx, readerState.Offset, readerState.RefCount + 1, readerState.Detached))) { break; } } } public void ReleaseReaderRef(int readerIdx) { ReaderStatePtr readerStatePtr = _headerPtr.GetReaderStatePtr(readerIdx); for (; ; ) { ReaderState readerState = readerStatePtr.Get(); if (readerState.RefCount == 0) { throw new InvalidOperationException("Refcount for reader is already zero"); } if (readerState.RefCount == 1) { for (int idx = 0; idx < _headerPtr.NumChunks; idx++) { _headerPtr.GetChunkStatePtr(idx).FinishReading(readerIdx); } } if (readerStatePtr.TryUpdate(readerState, new ReaderState(readerState.ChunkIdx, readerState.Offset, readerState.RefCount - 1, readerState.Detached))) { break; } } } public void CreateWriter() { WriterStatePtr writerStatePtr = _headerPtr.GetWriterStatePtr(); for (; ; ) { WriterState writerState = writerStatePtr.Get(); if (writerState.RefCount > 0) { throw new InvalidOperationException("Writer has already been created for this buffer"); } if (writerStatePtr.TryUpdate(writerState, new WriterState(writerState.ChunkIdx, writerState.ReaderFlags, 1, writerState.HasWrapped))) { ChunkStatePtr chunkStatePtr = _headerPtr.GetChunkStatePtr(writerState.ChunkIdx); for (; ; ) { ChunkState chunkState = chunkStatePtr.Get(); if (chunkStatePtr.TryUpdate(chunkState, new ChunkState(WriteState.Writing, chunkState.ReaderFlags, chunkState.Length))) { break; } } break; } } } public void AddWriterRef() { WriterStatePtr writerStatePtr = _headerPtr.GetWriterStatePtr(); for (; ; ) { WriterState writerState = writerStatePtr.Get(); if (writerState.RefCount == 0) { throw new InvalidOperationException("Writer does not exist for this buffer"); } if (writerStatePtr.TryUpdate(writerState, new WriterState(writerState.ChunkIdx, writerState.ReaderFlags, writerState.RefCount + 1, writerState.HasWrapped))) { break; } } } public void ReleaseWriterRef() { WriterStatePtr writerStatePtr = _headerPtr.GetWriterStatePtr(); for (; ; ) { WriterState writerState = writerStatePtr.Get(); if (writerState.RefCount == 0) { throw new InvalidOperationException("Writer does not exist for this buffer"); } if (writerState.RefCount == 1) { MarkComplete(); } if (writerStatePtr.TryUpdate(writerState, new WriterState(writerState.ChunkIdx, writerState.ReaderFlags, writerState.RefCount - 1, writerState.HasWrapped))) { break; } } } /// public bool IsComplete(int readerIdx) { ReaderState readerState = _headerPtr.GetReaderStatePtr(readerIdx).Get(); if (readerState.Detached) { return true; } ChunkState chunkState = _headerPtr.GetChunkStatePtr(readerState.ChunkIdx).Get(); return chunkState.WriteState == WriteState.Complete && readerState.Offset == chunkState.Length; } /// public void DetachReader(int readerIdx) { ReaderStatePtr readerStatePtr = _headerPtr.GetReaderStatePtr(readerIdx); for (; ; ) { ReaderState readerState = readerStatePtr.Get(); if (readerStatePtr.TryUpdate(readerState, new ReaderState(readerState.ChunkIdx, readerState.Offset, readerState.RefCount, true))) { SetReadEvent(readerIdx); break; } } } /// public void AdvanceReadPosition(int readerIdx, int offset) { ReaderStatePtr readerStatePtr = _headerPtr.GetReaderStatePtr(readerIdx); for (; ; ) { ReaderState readerState = readerStatePtr.Get(); if (readerStatePtr.TryUpdate(readerState, new ReaderState(readerState.ChunkIdx, readerState.Offset + offset, readerState.RefCount, readerState.Detached))) { SetReadEvent(readerIdx); break; } } } /// public ReadOnlyMemory GetReadBuffer(int readerIdx) { ReaderState readerState = _headerPtr.GetReaderStatePtr(readerIdx).Get(); ChunkStatePtr chunkStatePtr = _headerPtr.GetChunkStatePtr(readerState.ChunkIdx); ChunkState chunkState = chunkStatePtr.Get(); if (chunkState.HasReaderFlag(readerIdx)) { return _chunks[readerState.ChunkIdx].Slice(readerState.Offset, chunkState.Length - readerState.Offset); } else { return ReadOnlyMemory.Empty; } } /// public async ValueTask WaitToReadAsync(int readerIdx, int minLength, CancellationToken cancellationToken = default) { for (; ; ) { ReaderStatePtr readerStatePtr = _headerPtr.GetReaderStatePtr(readerIdx); ReaderState readerState = readerStatePtr.Get(); if (readerState.Detached) { return false; } ChunkStatePtr chunkStatePtr = _headerPtr.GetChunkStatePtr(readerState.ChunkIdx); ChunkState chunkState = chunkStatePtr.Get(); if (!chunkState.HasReaderFlag(readerIdx)) { // Wait until the current chunk is readable await WaitToReadAsync(readerIdx, cancellationToken); } else if (readerState.Offset + minLength <= chunkState.Length) { // We have enough data in the chunk to be able to read a message return true; } else if (chunkState.WriteState == WriteState.Writing) { // Wait until there is more data in the chunk await WaitToReadAsync(readerIdx, cancellationToken); } else if (readerState.Offset < chunkState.Length || chunkState.WriteState == WriteState.Complete) { // Cannot read the requested amount of data from this chunk. return false; } else if (chunkState.WriteState == WriteState.MovedToNext) { // Move to the next chunk chunkStatePtr.FinishReading(readerIdx); SetWriteEvent(); int nextChunkIdx = readerState.ChunkIdx + 1; if (nextChunkIdx == _headerPtr.NumChunks) { nextChunkIdx = 0; } readerStatePtr.TryUpdate(readerState, new ReaderState(nextChunkIdx, 0, readerState.RefCount, readerState.Detached)); } else { throw new NotImplementedException($"Invalid write state for buffer: {chunkState.WriteState}"); } } } /// public bool MarkComplete() { WriterState writerState = _headerPtr.GetWriterStatePtr().Get(); ChunkStatePtr chunkStatePtr = _headerPtr.GetChunkStatePtr(writerState.ChunkIdx); if (chunkStatePtr.Get().WriteState != WriteState.Complete) { chunkStatePtr.MarkComplete(); SetAllReadEvents(); return true; } return false; } /// public void AdvanceWritePosition(int size) { if (size > 0) { WriterState writerState = _headerPtr.GetWriterStatePtr().Get(); ChunkStatePtr chunkStatePtr = _headerPtr.GetChunkStatePtr(writerState.ChunkIdx); ChunkState chunkState = chunkStatePtr.Get(); Debug.Assert(chunkState.WriteState == WriteState.Writing); chunkStatePtr.Append(size); SetAllReadEvents(); } } /// public Memory GetWriteBuffer() { WriterState writerState = _headerPtr.GetWriterStatePtr().Get(); ChunkState chunkState = _headerPtr.GetChunkStatePtr(writerState.ChunkIdx).Get(); if (chunkState.WriteState == WriteState.Writing) { return _chunks[writerState.ChunkIdx].Slice(chunkState.Length); } else { return Memory.Empty; } } /// public async ValueTask WaitToWriteAsync(int minSize, CancellationToken cancellationToken = default) { if (minSize > _headerPtr.ChunkLength) { throw new ArgumentException("Requested read size is larger than chunk size.", nameof(minSize)); } // Get the current chunk we're writing to WriterState writerState = _headerPtr.GetWriterStatePtr().Get(); int writeChunkIdx = writerState.ChunkIdx; ChunkStatePtr writeChunkStatePtr = _headerPtr.GetChunkStatePtr(writeChunkIdx); // Check if we can append to this chunk ChunkState chunkState = writeChunkStatePtr.Get(); if (chunkState.WriteState == WriteState.Writing) { int length = chunkState.Length; if (length + minSize <= _headerPtr.ChunkLength) { return; } writeChunkStatePtr.FinishWriting(); SetAllReadEvents(); } if (chunkState.WriteState == WriteState.Complete) { return; } // Otherwise get the next chunk to write to int nextWriteChunkIdx = writeChunkIdx + 1; if (nextWriteChunkIdx == _chunks.Length) { nextWriteChunkIdx = 0; } // Wait until all readers have finished with the chunk, and we can update the writer to match ChunkStatePtr nextWriteChunkStatePtr = _headerPtr.GetChunkStatePtr(nextWriteChunkIdx); for (; ; ) { ChunkState nextWriteChunkState = nextWriteChunkStatePtr.Get(); if (nextWriteChunkState.ReaderFlags != 0) { await WaitToWriteAsync(cancellationToken); } else if (nextWriteChunkStatePtr.TryUpdate(nextWriteChunkState, new ChunkState(WriteState.Writing, writerState.ReaderFlags, 0))) { WriterStatePtr writerStatePtr = _headerPtr.GetWriterStatePtr(); if (writerStatePtr.TryUpdate(writerState, new WriterState(nextWriteChunkIdx, writerState.ReaderFlags, writerState.RefCount, nextWriteChunkIdx == 0))) { break; } else { writerState = writerStatePtr.Get(); } } } } } }