diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index 5186ec9..e9f491c 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -297,7 +297,11 @@ public static partial class AcBinaryDeserializer var context = DeserializationContextPool.Get(options); context.InitInput(input); try { return (T?)DeserializeCore(context, targetType); } - finally { DeserializationContextPool.Return(context); } + finally + { + context.Input.Release(); + DeserializationContextPool.Return(context); + } } /// @@ -315,7 +319,11 @@ public static partial class AcBinaryDeserializer var context = DeserializationContextPool.Get(options); context.InitInput(input); try { return DeserializeCore(context, targetType); } - finally { DeserializationContextPool.Return(context); } + finally + { + context.Input.Release(); + DeserializationContextPool.Return(context); + } } /// diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index 52acdb7..18f8a2e 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -155,7 +155,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// /// Default: 65536 (64 KB) /// - public int BufferWriterChunkSize { get; init; } = 65536; + public int BufferWriterChunkSize { get; set; } = 65536; /// /// Optional property-level filter invoked before metadata registration and serialization. diff --git a/AyCode.Core/Serializers/Binaries/ArrayBinaryInput.cs b/AyCode.Core/Serializers/Binaries/ArrayBinaryInput.cs index 8514427..2217061 100644 --- a/AyCode.Core/Serializers/Binaries/ArrayBinaryInput.cs +++ b/AyCode.Core/Serializers/Binaries/ArrayBinaryInput.cs @@ -49,4 +49,10 @@ public struct ArrayBinaryInput : IBinaryInputBase [MethodImpl(MethodImplOptions.NoInlining)] public bool TryAdvanceSegment(ref byte[] buffer, ref int position, ref int bufferLength, int needed) => false; + + /// + /// No-op — ArrayBinaryInput has no rented buffers to release. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Release() { } } diff --git a/AyCode.Core/Serializers/Binaries/IBinaryInputBase.cs b/AyCode.Core/Serializers/Binaries/IBinaryInputBase.cs index 8220343..da05cf4 100644 --- a/AyCode.Core/Serializers/Binaries/IBinaryInputBase.cs +++ b/AyCode.Core/Serializers/Binaries/IBinaryInputBase.cs @@ -31,4 +31,11 @@ public interface IBinaryInputBase /// [MethodImpl(MethodImplOptions.NoInlining)] bool TryAdvanceSegment(ref byte[] buffer, ref int position, ref int bufferLength, int needed); + + /// + /// Releases any rented buffers (e.g. ArrayPool scratch in SequenceBinaryInput). + /// Must be called after deserialization completes. + /// For ArrayBinaryInput: no-op. + /// + void Release(); } diff --git a/AyCode.Core/Serializers/Binaries/SequenceBinaryInput.cs b/AyCode.Core/Serializers/Binaries/SequenceBinaryInput.cs index 72a7026..8affdbd 100644 --- a/AyCode.Core/Serializers/Binaries/SequenceBinaryInput.cs +++ b/AyCode.Core/Serializers/Binaries/SequenceBinaryInput.cs @@ -7,57 +7,42 @@ namespace AyCode.Core.Serializers.Binaries; /// /// Binary input that reads directly from a ReadOnlySequence (e.g. SignalR pipe, network stream). -/// Processes segments one-by-one without linearizing the entire payload. +/// Iterates segments lazily via TryGet — no upfront ArraySegment[] allocation. /// -/// For values that span segment boundaries (e.g. a 4-byte int split across 2 segments), -/// copies the overlapping bytes into a scratch buffer and reads from there. +/// The context's _buffer always points to the current segment's backing byte[] (zero-copy). +/// Cross-boundary values (straddling two+ segments) are copied into a small ArrayPool scratch buffer. +/// After the scratch read, _afterCrossBoundary restores the context to the segment's backing array. /// -/// Mirrors BufferWriterBinaryOutput pattern from the serializer side. +/// Typical overhead for a 225KB payload with 4096-byte segments: +/// ~224.5KB zero-copy reads, ~500 bytes scratch copies (at ~55 segment boundaries). /// public struct SequenceBinaryInput : IBinaryInputBase { - // Pre-extracted segments from the ReadOnlySequence. - // Using ArraySegment avoids holding onto ReadOnlyMemory (which can't get byte[] without TryGetArray). - private readonly ArraySegment[] _segments; - private int _currentSegment; + private ReadOnlySequence _sequence; + private SequencePosition _nextPosition; - // Scratch buffer for cross-boundary reads — dynamically sized for large reads (strings, byte arrays) + // ArrayPool scratch for cross-boundary reads — lazy rent, reused across boundaries private byte[]? _scratchBuffer; - - // After a cross-boundary read, the next TryAdvanceSegment must load - // the remainder of _currentSegment (already adjusted) without incrementing. private bool _afterCrossBoundary; + // After cross-boundary: saved state of the last touched segment for restore + private byte[]? _savedBuffer; + private int _savedPosition; + private int _savedBufferLength; + /// - /// Creates a SequenceBinaryInput from a multi-segment ReadOnlySequence. - /// Pre-extracts all segments as ArraySegment for fast iteration. + /// Creates a SequenceBinaryInput from a ReadOnlySequence. + /// Does NOT pre-extract segments — iterates lazily via TryGet. /// public SequenceBinaryInput(ReadOnlySequence sequence) { - var segmentCount = 0; - foreach (var _ in sequence) - segmentCount++; - - _segments = new ArraySegment[segmentCount]; - var i = 0; - foreach (var memory in sequence) - { - if (MemoryMarshal.TryGetArray(memory, out var segment)) - { - _segments[i++] = segment; - } - else - { - // Non-array-backed memory: copy to a temp array - var temp = new byte[memory.Length]; - memory.Span.CopyTo(temp); - _segments[i++] = new ArraySegment(temp, 0, temp.Length); - } - } - - _currentSegment = 0; + _sequence = sequence; + _nextPosition = sequence.Start; _scratchBuffer = null; _afterCrossBoundary = false; + _savedBuffer = null; + _savedPosition = 0; + _savedBufferLength = 0; } /// @@ -66,13 +51,10 @@ public struct SequenceBinaryInput : IBinaryInputBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Initialize(out byte[] buffer, out int position, out int bufferLength) { - if (_segments.Length == 0) + if (!_sequence.TryGet(ref _nextPosition, out var memory)) throw new AcBinaryDeserializationException("Empty sequence — no segments to read."); - var seg = _segments[0]; - buffer = seg.Array!; - position = seg.Offset; - bufferLength = seg.Offset + seg.Count; + ExtractArray(memory, out buffer, out position, out bufferLength); } /// @@ -82,78 +64,121 @@ public struct SequenceBinaryInput : IBinaryInputBase [MethodImpl(MethodImplOptions.NoInlining)] public bool TryAdvanceSegment(ref byte[] buffer, ref int position, ref int bufferLength, int needed) { - // After cross-boundary scratch read: load the remainder of the current segment - // (already adjusted in TryReadCrossBoundary) without incrementing. + // After cross-boundary scratch read: restore to the last touched segment's backing array if (_afterCrossBoundary) { _afterCrossBoundary = false; - var seg = _segments[_currentSegment]; - buffer = seg.Array!; - position = seg.Offset; - bufferLength = seg.Offset + seg.Count; - return seg.Count > 0; + buffer = _savedBuffer!; + position = _savedPosition; + bufferLength = _savedBufferLength; + return position < bufferLength; } - // Calculate remaining bytes in current segment var remaining = bufferLength - position; if (remaining > 0 && remaining < needed) { - // Cross-boundary read: value spans two segments + // Cross-boundary: value spans segment boundary return TryReadCrossBoundary(ref buffer, ref position, ref bufferLength, needed, remaining); } // Current segment fully consumed — advance to next - _currentSegment++; - if (_currentSegment >= _segments.Length) + return TryLoadNextSegment(ref buffer, ref position, ref bufferLength); + } + + /// + /// Returns the ArrayPool scratch buffer if one was rented. + /// Must be called after deserialization completes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Release() + { + if (_scratchBuffer != null) + { + ArrayPool.Shared.Return(_scratchBuffer); + _scratchBuffer = null; + } + } + + /// + /// Loads the next segment from the sequence via TryGet. + /// Extracts the backing byte[] for zero-copy access. + /// + private bool TryLoadNextSegment(ref byte[] buffer, ref int position, ref int bufferLength) + { + if (!_sequence.TryGet(ref _nextPosition, out var memory) || memory.Length == 0) return false; - var seg2 = _segments[_currentSegment]; - buffer = seg2.Array!; - position = seg2.Offset; - bufferLength = seg2.Offset + seg2.Count; + ExtractArray(memory, out buffer, out position, out bufferLength); return true; } /// - /// Handles a read that spans two segments by copying the overlapping bytes - /// into a scratch buffer, then setting up the context to read from it. - /// After this read, the next EnsureAvailable will advance to the remainder of the new segment. + /// Handles a read that spans N segments by copying the overlapping bytes + /// into an ArrayPool scratch buffer. After this read, the next TryAdvanceSegment + /// restores the context to the last touched segment's backing array. /// private bool TryReadCrossBoundary(ref byte[] buffer, ref int position, ref int bufferLength, int needed, int remaining) { - _currentSegment++; - if (_currentSegment >= _segments.Length) - return false; + // Rent scratch (or reuse if large enough) + if (_scratchBuffer == null || _scratchBuffer.Length < needed) + { + if (_scratchBuffer != null) + ArrayPool.Shared.Return(_scratchBuffer); + _scratchBuffer = ArrayPool.Shared.Rent(needed); + } - var nextSeg = _segments[_currentSegment]; - var fromNext = Math.Min(needed - remaining, nextSeg.Count); - var scratchNeeded = remaining + fromNext; - - // Dynamically size scratch buffer — handles large reads (strings, byte arrays) - if (_scratchBuffer == null || _scratchBuffer.Length < scratchNeeded) - _scratchBuffer = new byte[Math.Max(32, scratchNeeded)]; - - // Copy tail of current segment + // 1) Copy tail of current segment Buffer.BlockCopy(buffer, position, _scratchBuffer, 0, remaining); + var filled = remaining; - // Copy head of next segment - Buffer.BlockCopy(nextSeg.Array!, nextSeg.Offset, _scratchBuffer, remaining, fromNext); + // 2) Copy from subsequent segments until we have enough + while (filled < needed) + { + if (!_sequence.TryGet(ref _nextPosition, out var memory) || memory.Length == 0) + return false; - // Set up context to read from scratch buffer + ExtractArray(memory, out var segArray, out var segOffset, out var segBufferLength); + var segCount = segBufferLength - segOffset; + var take = Math.Min(needed - filled, segCount); + Buffer.BlockCopy(segArray, segOffset, _scratchBuffer, filled, take); + filled += take; + + // Save last touched segment for _afterCrossBoundary restore + _savedBuffer = segArray; + _savedPosition = segOffset + take; + _savedBufferLength = segBufferLength; + } + + // Context reads from scratch buffer buffer = _scratchBuffer; position = 0; - bufferLength = scratchNeeded; - - // Adjust the current segment to skip the bytes we already copied. - // The _afterCrossBoundary flag ensures the next TryAdvanceSegment - // loads this remainder without incrementing _currentSegment. - _segments[_currentSegment] = new ArraySegment( - nextSeg.Array!, - nextSeg.Offset + fromNext, - nextSeg.Count - fromNext); - + bufferLength = filled; _afterCrossBoundary = true; return true; } + + /// + /// Extracts the backing byte[] from a ReadOnlyMemory segment. + /// Array-backed (99.9%): zero-copy reference to backing array. + /// Non-array-backed (native memory): copies to a managed byte[]. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ExtractArray(ReadOnlyMemory memory, out byte[] buffer, out int position, out int bufferLength) + { + if (MemoryMarshal.TryGetArray(memory, out var segment)) + { + buffer = segment.Array!; + position = segment.Offset; + bufferLength = segment.Offset + segment.Count; + } + else + { + var temp = new byte[memory.Length]; + memory.Span.CopyTo(temp); + buffer = temp; + position = 0; + bufferLength = temp.Length; + } + } } diff --git a/AyCode.Core/docs/BINARY_ISSUES.md b/AyCode.Core/docs/BINARY_ISSUES.md new file mode 100644 index 0000000..a3b6cc2 --- /dev/null +++ b/AyCode.Core/docs/BINARY_ISSUES.md @@ -0,0 +1,58 @@ +# Binary Serializer — Known Issues & Limitations + +## Deserialization + +### DESER-1: Non-array-backed memory — per-segment copy + +**Status:** By design +**Affects:** `SequenceBinaryInput` +**Path:** `ExtractArray()` fallback when `MemoryMarshal.TryGetArray` fails + +When `ReadOnlySequence` segments are backed by native memory (not managed `byte[]`), each segment is copied into a `new byte[]`. This is unavoidable — the context requires `byte[]` for `Unsafe.ReadUnaligned`, `AsSpan`, and `Encoding.GetString`. + +**Impact:** Negligible. Non-array-backed `ReadOnlyMemory` is extremely rare (custom `MemoryManager` with native memory, memory-mapped files). All standard .NET pools (`ArrayPool`, `MemoryPool.Shared`, Kestrel pipe) are array-backed. + +### DESER-2: Cross-boundary scratch buffer is not pooled across calls + +**Status:** Acceptable +**Affects:** `SequenceBinaryInput._scratchBuffer` + +The scratch buffer is `ArrayPool.Rent`-ed on first cross-boundary read and reused within a single deserialization. It is `Return`-ed in `Release()` after deserialization completes. However, the next deserialization will rent again. + +**Impact:** Minimal. `ArrayPool.Shared` reuses buffers efficiently. The scratch is typically small (4-16 bytes for fixed-width boundary reads). Large scratch (>4KB) only occurs when a string or byte[] straddles a segment boundary. + +**Possible optimization:** Store the scratch buffer on the pooled `BinaryDeserializationContext` and reuse across deserializations. Low priority — `ArrayPool` overhead is negligible. + +### DESER-3: ReadBytes always copies + +**Status:** By design +**Affects:** `BinaryDeserializationContext.ReadBytes(int length)` + +`ReadBytes` allocates a new `byte[]` and copies from the buffer. This is unavoidable because the caller owns the returned array, and the source buffer (pipe segment or serialized data) may be recycled. + +### DESER-4: ReadStringUtf8 requires contiguous buffer + +**Status:** By design +**Affects:** `BinaryDeserializationContext.ReadStringUtf8(int length)` + +`Encoding.GetString` and `Ascii.IsValid` require contiguous memory. For multi-segment reads, `EnsureAvailable` copies cross-boundary bytes into the scratch buffer first. This is the same approach `SequenceReader` uses internally. + +**Possible optimization:** Span-by-span UTF-8 decode for cross-boundary strings (like MessagePack). Low priority — most strings are shorter than a segment (4KB). + +## Serialization + +### SER-1: BufferWriterBinaryOutput fallback path allocates per-chunk + +**Status:** Acceptable +**Affects:** `BufferWriterBinaryOutput.AcquireChunk` fallback + +When `MemoryMarshal.TryGetArray` fails on `IBufferWriter.GetMemory()` (native memory-backed writer), a `byte[]` is rented from `ArrayPool` per chunk and copied to the writer on `Grow`/`Flush`. Same as DESER-1 — non-array-backed writers are extremely rare. + +## Source Generator (SGen) + +### SGEN-1: CS8625 warnings for non-nullable reference types + +**Status:** Known +**Affects:** Generated reader code + +The source generator emits `null` assignments for non-nullable reference type properties during deserialization (before the value is read from the stream). This produces CS8625 warnings. Functionally harmless — the property is always assigned before use. diff --git a/AyCode.Core/docs/BINARY_WRITERS.md b/AyCode.Core/docs/BINARY_WRITERS.md index 9d02311..ed0cf9e 100644 --- a/AyCode.Core/docs/BINARY_WRITERS.md +++ b/AyCode.Core/docs/BINARY_WRITERS.md @@ -86,3 +86,18 @@ Most important architectural decision in the output layer. **Current:** writes on `BinarySerializationContext` (sealed class, hot path). Output struct handles only `Initialize`/`Grow`/`Flush` (cold path). **Rule:** Do NOT move write methods to output. Measure with full benchmark suite before proposing changes. + +## IBinaryInputBase (Read Side Mirror) + +Deserialization mirrors the output pattern. `IBinaryInputBase` provides buffer lifecycle; all read methods live on `BinaryDeserializationContext`. + +```csharp +void Initialize(out byte[] buffer, out int position, out int bufferLength); +bool TryAdvanceSegment(ref byte[] buffer, ref int position, ref int bufferLength, int needed); +void Release(); +``` + +- **ArrayBinaryInput:** single `byte[]`, `TryAdvanceSegment => false` (JIT-eliminated), `Release` no-op. +- **SequenceBinaryInput:** lazy `TryGet` iteration over `ReadOnlySequence`. Context `_buffer` points to segment backing `byte[]` (zero-copy). Cross-boundary: `ArrayPool` scratch, N-segment loop. `Release` returns scratch to pool. + +> Known issues and limitations: `BINARY_ISSUES.md` diff --git a/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs b/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs index ab243be..c03b955 100644 --- a/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs +++ b/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs @@ -60,6 +60,7 @@ public class AcBinaryHubProtocol : IHubProtocol public AcBinaryHubProtocol(AcBinarySerializerOptions options) { _options = options; + _options.BufferWriterChunkSize = 4096; } /// diff --git a/AyCode.Services/SignalRs/README.md b/AyCode.Services/SignalRs/README.md index bdf7b5f..39d8c75 100644 --- a/AyCode.Services/SignalRs/README.md +++ b/AyCode.Services/SignalRs/README.md @@ -4,6 +4,7 @@ Custom binary SignalR protocol, client infrastructure, message tagging, and seri > **Architecture:** For full dispatch flow, tag system, and tech debt documentation see `AyCode.Services/docs/SIGNALR.md`. > **Binary protocol:** For wire format, zero-copy pipeline, and three-path read logic see `AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md`. +> **Known issues:** `AyCode.Services/docs/SIGNALR_ISSUES.md` ## Key Files diff --git a/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md b/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md index 6621193..00e6e25 100644 --- a/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md +++ b/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md @@ -148,7 +148,11 @@ Zero-copy when possible: if single-segment and backing array matches exactly → `struct SequenceBinaryInput : IBinaryInputBase` — reads from `ReadOnlySequence` without linearizing. Lazy iteration via `ReadOnlySequence.TryGet` — zero constructor allocation, no pre-extracted segment array. -Cross-boundary reads (e.g. 4-byte int split across 2 segments) use a small scratch buffer (32 bytes). Remainder tracking via `_remainderArray/Offset/Count` — no segment array mutation. +The context's `_buffer` always points directly to the current segment's backing `byte[]` (zero-copy). Cross-boundary reads (value straddling segment boundary) copy only the affected bytes into a small `ArrayPool`-rented scratch buffer. After the scratch read, `_afterCrossBoundary` flag restores the context to the next segment's backing array. + +Typical overhead for 225KB payload with 4096-byte segments: ~224.5KB zero-copy, ~500 bytes scratch copy at ~55 boundaries. The scratch buffer is rented once (lazy, on first boundary) and reused across all boundaries. `Release()` returns it to `ArrayPool` after deserialization. + +> Known issues: `AyCode.Core/docs/BINARY_ISSUES.md` ## Config diff --git a/AyCode.Services/docs/SIGNALR_ISSUES.md b/AyCode.Services/docs/SIGNALR_ISSUES.md new file mode 100644 index 0000000..c0da6b5 --- /dev/null +++ b/AyCode.Services/docs/SIGNALR_ISSUES.md @@ -0,0 +1,57 @@ +# SignalR — Known Issues & Limitations + +## Protocol + +### PROTO-1: Server-side IsRawBytesData pre-serialize + +**Status:** Planned removal +**Affects:** `AcWebSignalRHubBase.SendMessageToClient` + +The server forwards the client's `IsRawBytesData` flag in the response `SignalParams`. This causes the protocol to return raw `byte[]` instead of deserializing. The original design pre-serialized on the server side, but with the zero-copy typed deserialization path (`SignalDataType`), this is redundant. + +**Plan:** Remove `IsRawBytesData` forwarding from server response path. The client should use `SignalDataType` for typed deserialization and explicit `byte[]` type for raw data. + +### PROTO-2: Parameter serialization is per-parameter + +**Status:** Known performance concern +**Affects:** `SignalParams.SetParameterValues` / `GetParameterValues` + +Each parameter is individually serialized via `ToBinary()` / `BinaryTo(Type)` — N context pool acquire/release cycles. For many small primitives (int, bool, string) the per-call overhead may exceed a single bulk serialization. + +**Possible optimization:** Batch fast-path — single serialization context for all parameters. Benchmark first. + +### PROTO-3: Parameter serialization is AcBinary only + +**Status:** Limitation +**Affects:** `SignalParams.SetParameterValues` / `GetParameterValues` + +Uses `ToBinary()` / `BinaryTo()` exclusively. JSON parameter support would require dispatching on `DataSerializerType` + `AcJsonSerializer` reference. Low priority — binary is the primary transport. + +## Transport + +### TRANS-1: BufferWriterChunkSize defaults to 64KB for SignalR + +**Status:** Optimization opportunity +**Affects:** `AyCodeBinaryHubProtocol` default constructor, write path + +The default `BufferWriterChunkSize` is 65536 (from `AcBinarySerializerOptions.Default`). For SignalR/Kestrel, 4096 aligns better with the transport's internal segment size, reducing latency-to-first-byte. + +**Plan:** Set `BufferWriterChunkSize = 4096` in `AyCodeBinaryHubProtocol` default constructor. The options property already exists (`AcBinarySerializerOptions.BufferWriterChunkSize`). Non-SignalR paths keep 64KB default. + +### TRANS-2: WebSocket buffer sizes are hardcoded + +**Status:** Acceptable +**Affects:** `AcSignalRClientBase` connection setup + +Transport max message size (30MB) and application buffer (30MB) are hardcoded. Sufficient for current payloads but not configurable per-deployment. + +## DataSource + +### DS-1: GetAll returns raw byte[] for populate/merge + +**Status:** By design +**Affects:** `AcSignalRDataSource.LoadDataSourceAsync` + +The `GetAll` path uses `IsRawBytesData = true` to receive raw `byte[]` from the protocol, then deserializes into the existing list via `PopulateMerge`. This avoids allocating a temporary `List` for merge. The extra copy (pipe → byte[]) is the trade-off. + +**Possible optimization:** Direct typed deserialization with merge support in the deserializer (PopulateMerge from `ReadOnlySequence`). Requires deserializer API changes. diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index 506c6b8..9fa783b 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -32,7 +32,7 @@ For full specification see `AyCode.Core/docs/BINARY_FORMAT.md`. | **FixStr** | Compact string marker (103–134). Encodes type + length in one byte for ASCII strings ≤31 bytes. | | **TinyInt** | Compact integer marker (192–255). Encodes small integers (−16 to 47) in a single byte. | | **VarInt / VarUInt** | Variable-length integer encoding. LEB128 for unsigned, ZigZag + LEB128 for signed. | -| **SequenceBinaryInput** | `struct : IBinaryInputBase` for reading from `ReadOnlySequence` (multi-segment pipe data). Lazy iteration via `TryGet` — zero constructor allocation. Cross-boundary reads use scratch buffer. Used by `AcBinaryDeserializer.Deserialize(ReadOnlySequence)` for multi-segment data. | +| **SequenceBinaryInput** | `struct : IBinaryInputBase` for reading from `ReadOnlySequence` (multi-segment pipe data). Lazy iteration via `TryGet` — zero constructor allocation. Context `_buffer` points directly to segment backing `byte[]` (zero-copy). Cross-boundary values use `ArrayPool`-rented scratch buffer (rent once, reuse, `Release()` returns). N-segment loop handles values spanning any number of segments. | | **ArrayBinaryInput** | `struct : IBinaryInputBase` for reading from contiguous `byte[]`. Zero-copy when pipe is single-segment. Default fast-path for deserialization. | | **HeaderFlags** | Byte at stream position 1 encoding serialization options: metadata, reference handling mode, cache count presence. Base `0x90`. | | **Two-Phase Serialization** | Scan pass detects multi-referenced objects, serialize pass writes output using reference table. Required for `ReferenceHandling.All`. |