diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs
index 2ed0ab4..cc94b8e 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs
@@ -552,96 +552,6 @@ public static partial class AcBinarySerializer
_position += 10;
}
- ///
- /// Patches a previously written VarUInt at the specified position.
- /// Works correctly only if the new value requires the same or fewer bytes.
- /// For property counts < 128, this is always 1 byte.
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void PatchVarUInt(int position, uint value)
- {
- // Fast path: single byte (covers 0-127, which is most property counts)
- if (value < 0x80)
- {
- _buffer[position] = (byte)value;
- return;
- }
-
- // Multi-byte case - need to shift buffer if new encoding is longer
- // For simplicity, we'll rewrite from the position
- // This is rare for property counts
- PatchVarUIntSlow(position, value);
- }
-
- [MethodImpl(MethodImplOptions.NoInlining)]
- private void PatchVarUIntSlow(int position, uint value)
- {
- // Calculate current size at position (read until no continuation bit)
- var currentSize = 0;
- var pos = position;
- while (pos < _position && (_buffer[pos] & 0x80) != 0)
- {
- currentSize++;
- pos++;
- }
- currentSize++; // Include final byte without continuation bit
-
- // Calculate new size needed
- var newSize = GetVarUIntSize(value);
-
- if (newSize == currentSize)
- {
- // Same size - just overwrite
- var tempPos = position;
- while (value >= 0x80)
- {
- _buffer[tempPos++] = (byte)(value | 0x80);
- value >>= 7;
- }
- _buffer[tempPos] = (byte)value;
- }
- else if (newSize < currentSize)
- {
- // New is smaller - shift data left
- var delta = currentSize - newSize;
- Array.Copy(_buffer, position + currentSize, _buffer, position + newSize, _position - position - currentSize);
- _position -= delta;
-
- var tempPos = position;
- while (value >= 0x80)
- {
- _buffer[tempPos++] = (byte)(value | 0x80);
- value >>= 7;
- }
- _buffer[tempPos] = (byte)value;
- }
- else
- {
- // New is larger - shift data right
- var delta = newSize - currentSize;
- EnsureCapacity(delta);
- Array.Copy(_buffer, position + currentSize, _buffer, position + newSize, _position - position - currentSize);
- _position += delta;
-
- var tempPos = position;
- while (value >= 0x80)
- {
- _buffer[tempPos++] = (byte)(value | 0x80);
- value >>= 7;
- }
- _buffer[tempPos] = (byte)value;
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static int GetVarUIntSize(uint value)
- {
- if (value < 0x80) return 1;
- if (value < 0x4000) return 2;
- if (value < 0x200000) return 3;
- if (value < 0x10000000) return 4;
- return 5;
- }
public void WriteVarInt(int value)
{
@@ -950,33 +860,19 @@ public static partial class AcBinarySerializer
#endregion
- #region Header and Metadata
-
- private int _headerPosition;
+ #region Header
// Marker-based interning: no footer needed
// Header: [version][flags][cacheCount (VarUInt, if caching enabled)]
// Body: data with markers (StringInternFirst, ObjectRefFirst, etc.)
- public void WriteHeaderPlaceholder()
+ ///
+ /// Writes the binary header directly. Call AFTER ScanForDuplicates (cacheCount is known).
+ /// No placeholder, no shift — single forward write.
+ /// Layout: [version (1b)][flags (1b)][cacheCount (VarUInt, if caching)]
+ ///
+ public void WriteHeader()
{
- // Header layout:
- // [0] version (1 byte)
- // [1] flags (1 byte)
- // [2+] cache count (VarUInt, max 5 bytes, if caching enabled)
- EnsureCapacity(HasCaching ? 7 : 2);
- _headerPosition = _position;
- _position += HasCaching ? 7 : 2; // Reserve max VarUInt size
- }
-
- public void FinalizeHeaderSections()
- {
- var cacheCount = GetCacheCount();
-
- // Rewrite markers for first occurrences (String→StringInternFirst, Object→ObjectRefFirst, etc.)
- //RewriteMarkers();
-
- // Write header
var flags = BinaryTypeCode.HeaderFlagsBase;
if (UseMetadata)
flags |= BinaryTypeCode.HeaderFlag_Metadata;
@@ -987,39 +883,15 @@ public static partial class AcBinarySerializer
if (HasCaching)
flags |= BinaryTypeCode.HeaderFlag_HasCacheCount;
- _buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
- _buffer[_headerPosition + 1] = flags;
+ WriteByte(AcBinarySerializerOptions.FormatVersion);
+ WriteByte(flags);
- // Write cache count and compact header if needed
if (HasCaching)
{
- var headerEnd = WriteVarUIntAt(_headerPosition + 2, (uint)cacheCount);
- var reserved = _headerPosition + 7;
- if (headerEnd < reserved)
- {
- // Shift body left to remove unused header bytes
- var shift = reserved - headerEnd;
- _buffer.AsSpan(reserved, _position - reserved).CopyTo(_buffer.AsSpan(headerEnd));
- _position -= shift;
- }
+ WriteVarUInt((uint)GetCacheCount());
}
}
- ///
- /// Writes VarUInt at specific position and returns new position.
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private int WriteVarUIntAt(int pos, uint value)
- {
- while (value >= 0x80)
- {
- _buffer[pos++] = (byte)(value | 0x80);
- value >>= 7;
- }
- _buffer[pos++] = (byte)value;
- return pos;
- }
-
#endregion
#region Reference Handling
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
index 4ebdd40..afa4a8c 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
@@ -90,9 +90,8 @@ public static partial class AcBinarySerializer
};
// Run serialization to trigger callbacks
- context.WriteHeaderPlaceholder();
+ context.WriteHeader();
WriteValue(value, runtimeType, context, 0);
- context.FinalizeHeaderSections();
return result;
}
@@ -220,6 +219,12 @@ public static partial class AcBinarySerializer
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] Serialize(T value) => Serialize(value, AcBinarySerializerOptions.Default);
+ ///
+ /// Serialize object to an IBufferWriter with default options.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Serialize(T value, IBufferWriter writer) => Serialize(value, writer, AcBinarySerializerOptions.Default);
+
///
/// Serialize object to binary with specified options.
///
@@ -379,15 +384,15 @@ public static partial class AcBinarySerializer
BinarySerializationContext.GrowBufferTotalBytes = 0;
#endif
var context = BinarySerializationContextPool.Get(options);
- context.WriteHeaderPlaceholder();
- // Two-pass serialization when caching is enabled:
+ // Two-pass serialization:
// 1. Scan pass: identify duplicates (strings + objects), assign CacheIndex
- // 2. Serialize pass: write data with references
+ // 2. Write header (cacheCount is now known - no placeholder, no shift)
+ // 3. Serialize pass: write body with references
ScanForDuplicates(value, runtimeType, context);
-
+ context.WriteHeader();
WriteValue(value, runtimeType, context, 0);
- context.FinalizeHeaderSections();
+
return context;
}
diff --git a/AyCode.Core/Serializers/Binaries/PooledBufferWriter.cs b/AyCode.Core/Serializers/Binaries/PooledBufferWriter.cs
new file mode 100644
index 0000000..144b352
--- /dev/null
+++ b/AyCode.Core/Serializers/Binaries/PooledBufferWriter.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Buffers;
+using System.Runtime.CompilerServices;
+
+namespace AyCode.Core.Serializers.Binaries;
+
+///
+/// High-performance ArrayPool-backed IBufferWriter.
+/// Designed for pooling and reuse - supports Reset() without deallocation.
+/// Unlike BCL ArrayBufferWriter, this uses ArrayPool for zero-alloc buffer management.
+///
+internal sealed class PooledBufferWriter : IBufferWriter, IDisposable
+{
+ private byte[] _buffer;
+ private int _written;
+
+ public PooledBufferWriter(int initialCapacity)
+ {
+ _buffer = ArrayPool.Shared.Rent(Math.Max(initialCapacity, 256));
+ }
+
+ /// Written data as ReadOnlySpan (no allocation).
+ public ReadOnlySpan WrittenSpan
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _buffer.AsSpan(0, _written);
+ }
+
+ /// Written data as ReadOnlyMemory.
+ public ReadOnlyMemory WrittenMemory
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _buffer.AsMemory(0, _written);
+ }
+
+ /// Number of bytes written so far.
+ public int WrittenCount
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _written;
+ }
+
+ /// Direct access to backing array for Unsafe.WriteUnaligned operations.
+ public byte[] RawBuffer
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _buffer;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Advance(int count) => _written += count;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Memory GetMemory(int sizeHint = 0)
+ {
+ EnsureCapacity(sizeHint);
+ return _buffer.AsMemory(_written);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Span GetSpan(int sizeHint = 0)
+ {
+ EnsureCapacity(sizeHint);
+ return _buffer.AsSpan(_written);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void EnsureCapacity(int sizeHint)
+ {
+ if (sizeHint <= _buffer.Length - _written) return;
+ Grow(sizeHint);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private void Grow(int sizeHint)
+ {
+ var newSize = Math.Max(_buffer.Length * 2, _written + Math.Max(sizeHint, 1));
+ var newBuffer = ArrayPool.Shared.Rent(newSize);
+ _buffer.AsSpan(0, _written).CopyTo(newBuffer);
+ ArrayPool.Shared.Return(_buffer);
+ _buffer = newBuffer;
+ }
+
+ /// Copy written data to a new exactly-sized array.
+ public byte[] ToArray()
+ {
+ var result = GC.AllocateUninitializedArray(_written);
+ _buffer.AsSpan(0, _written).CopyTo(result);
+ return result;
+ }
+
+ /// Copy written data to an external IBufferWriter (single memcpy).
+ public void CopyTo(IBufferWriter destination)
+ {
+ var span = destination.GetSpan(_written);
+ _buffer.AsSpan(0, _written).CopyTo(span);
+ destination.Advance(_written);
+ }
+
+ ///
+ /// Detach the backing buffer for zero-copy transfer.
+ /// Caller is responsible for returning the buffer to ArrayPool.
+ ///
+ public (byte[] Buffer, int Length) Detach()
+ {
+ var buf = _buffer;
+ var len = _written;
+ _buffer = ArrayPool.Shared.Rent(256);
+ _written = 0;
+ return (buf, len);
+ }
+
+ /// Reset for reuse (no deallocation).
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Reset() => _written = 0;
+
+ /// Reset with optional buffer resize if too small.
+ public void Reset(int newMinCapacity)
+ {
+ _written = 0;
+ if (_buffer.Length < newMinCapacity)
+ {
+ ArrayPool.Shared.Return(_buffer);
+ _buffer = ArrayPool.Shared.Rent(newMinCapacity);
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_buffer != null)
+ {
+ ArrayPool.Shared.Return(_buffer);
+ _buffer = null!;
+ }
+ }
+}