diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000..074151b
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,4 @@
+# Copilot Instructions
+
+## Project Guidelines
+- Ne ajánlj visszalépést/eltávolítást megoldásként — keress megoldást a problémára.
\ No newline at end of file
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs
index c36ca8f..d703e2d 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs
@@ -341,6 +341,16 @@ public static partial class AcBinarySerializer
Output.Grow(ref _buffer, ref _position, ref _bufferEnd, additionalBytes);
}
+ ///
+ /// Ensures the buffer has enough space for the specified number of bytes.
+ /// Called before property writes to avoid mid-object Grow() calls.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void ReserveCapacity(int bytes)
+ {
+ EnsureCapacity(bytes);
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteByte(byte value)
{
@@ -349,10 +359,24 @@ public static partial class AcBinarySerializer
_buffer[_position++] = value;
}
+ /// Writes a single byte without capacity check. Caller must ensure buffer space.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteByteUnsafe(byte value)
+ {
+ _buffer[_position++] = value;
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteTwoBytes(byte b1, byte b2)
{
EnsureCapacity(2);
+ WriteTwoBytesUnsafe(b1, b2);
+ }
+
+ /// Writes two bytes without capacity check. Caller must ensure buffer space.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteTwoBytesUnsafe(byte b1, byte b2)
+ {
_buffer[_position++] = b1;
_buffer[_position++] = b2;
}
@@ -361,6 +385,13 @@ public static partial class AcBinarySerializer
public void WriteBytes(ReadOnlySpan data)
{
EnsureCapacity(data.Length);
+ WriteBytesUnsafe(data);
+ }
+
+ /// Writes a span of bytes without capacity check. Caller must ensure buffer space.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteBytesUnsafe(ReadOnlySpan data)
+ {
data.CopyTo(_buffer.AsSpan(_position));
_position += data.Length;
}
@@ -368,17 +399,29 @@ public static partial class AcBinarySerializer
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteRaw(T value) where T : unmanaged
{
- var size = Unsafe.SizeOf();
- EnsureCapacity(size);
+ EnsureCapacity(Unsafe.SizeOf());
+ WriteRawUnsafe(value);
+ }
+
+ /// Writes an unmanaged value without capacity check. Caller must ensure buffer space.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteRawUnsafe(T value) where T : unmanaged
+ {
Unsafe.WriteUnaligned(ref _buffer[_position], value);
- _position += size;
+ _position += Unsafe.SizeOf();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteTypeCodeAndRaw(byte typeCode, T value) where T : unmanaged
{
- var size = 1 + Unsafe.SizeOf();
- EnsureCapacity(size);
+ EnsureCapacity(1 + Unsafe.SizeOf());
+ WriteTypeCodeAndRawUnsafe(typeCode, value);
+ }
+
+ /// Writes a type code + unmanaged value without capacity check. Caller must ensure buffer space.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteTypeCodeAndRawUnsafe(byte typeCode, T value) where T : unmanaged
+ {
_buffer[_position++] = typeCode;
Unsafe.WriteUnaligned(ref _buffer[_position], value);
_position += Unsafe.SizeOf();
@@ -400,6 +443,24 @@ public static partial class AcBinarySerializer
return;
}
EnsureCapacity(5);
+ WriteVarUIntMultiByteUnsafe(value);
+ }
+
+ /// Writes a VarUInt without capacity check. Caller must ensure at least 5 bytes of buffer space.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteVarUIntUnsafe(uint value)
+ {
+ if (value < 0x80)
+ {
+ _buffer[_position++] = (byte)value;
+ return;
+ }
+ WriteVarUIntMultiByteUnsafe(value);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private void WriteVarUIntMultiByteUnsafe(uint value)
+ {
while (value >= 0x80)
{
_buffer[_position++] = (byte)(value | 0x80);
@@ -416,6 +477,14 @@ public static partial class AcBinarySerializer
WriteVarUInt(encoded);
}
+ /// Writes a zigzag-encoded VarInt without capacity check. Caller must ensure at least 5 bytes of buffer space.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteVarIntUnsafe(int value)
+ {
+ var encoded = (uint)((value << 1) ^ (value >> 31));
+ WriteVarUIntUnsafe(encoded);
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarULong(ulong value)
{
@@ -428,6 +497,24 @@ public static partial class AcBinarySerializer
return;
}
EnsureCapacity(10);
+ WriteVarULongMultiByteUnsafe(value);
+ }
+
+ /// Writes a VarULong without capacity check. Caller must ensure at least 10 bytes of buffer space.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteVarULongUnsafe(ulong value)
+ {
+ if (value < 0x80)
+ {
+ _buffer[_position++] = (byte)value;
+ return;
+ }
+ WriteVarULongMultiByteUnsafe(value);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private void WriteVarULongMultiByteUnsafe(ulong value)
+ {
while (value >= 0x80)
{
_buffer[_position++] = (byte)(value | 0x80);
@@ -444,6 +531,14 @@ public static partial class AcBinarySerializer
WriteVarULong(encoded);
}
+ /// Writes a zigzag-encoded VarLong without capacity check. Caller must ensure at least 10 bytes of buffer space.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteVarLongUnsafe(long value)
+ {
+ var encoded = (ulong)((value << 1) ^ (value >> 63));
+ WriteVarULongUnsafe(encoded);
+ }
+
#endregion
#region Specialized Types — inline
@@ -452,6 +547,13 @@ public static partial class AcBinarySerializer
public void WriteDecimalBits(decimal value)
{
EnsureCapacity(16);
+ WriteDecimalBitsUnsafe(value);
+ }
+
+ /// Writes 16-byte decimal bits without capacity check. Caller must ensure buffer space.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteDecimalBitsUnsafe(decimal value)
+ {
Span bits = stackalloc int[4];
decimal.TryGetBits(value, bits, out _);
MemoryMarshal.AsBytes(bits).CopyTo(_buffer.AsSpan(_position, 16));
@@ -462,6 +564,13 @@ public static partial class AcBinarySerializer
public void WriteDateTimeBits(DateTime value)
{
EnsureCapacity(9);
+ WriteDateTimeBitsUnsafe(value);
+ }
+
+ /// Writes 9-byte DateTime (Ticks + Kind) without capacity check. Caller must ensure buffer space.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteDateTimeBitsUnsafe(DateTime value)
+ {
Unsafe.WriteUnaligned(ref _buffer[_position], value.Ticks);
_buffer[_position + 8] = (byte)value.Kind;
_position += 9;
@@ -471,6 +580,13 @@ public static partial class AcBinarySerializer
public void WriteGuidBits(Guid value)
{
EnsureCapacity(16);
+ WriteGuidBitsUnsafe(value);
+ }
+
+ /// Writes 16-byte Guid without capacity check. Caller must ensure buffer space.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteGuidBitsUnsafe(Guid value)
+ {
value.TryWriteBytes(_buffer.AsSpan(_position, 16));
_position += 16;
}
@@ -479,6 +595,13 @@ public static partial class AcBinarySerializer
public void WriteDateTimeOffsetBits(DateTimeOffset value)
{
EnsureCapacity(10);
+ WriteDateTimeOffsetBitsUnsafe(value);
+ }
+
+ /// Writes 10-byte DateTimeOffset (UtcTicks + Offset) without capacity check. Caller must ensure buffer space.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteDateTimeOffsetBitsUnsafe(DateTimeOffset value)
+ {
Unsafe.WriteUnaligned(ref _buffer[_position], value.UtcTicks);
Unsafe.WriteUnaligned(ref _buffer[_position + 8], (short)value.Offset.TotalMinutes);
_position += 10;
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs
index 8b12614..62fc178 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs
@@ -118,6 +118,14 @@ public static partial class AcBinarySerializer
}
+ ///
+ /// Pre-computed minimum byte size for writing all properties of this type.
+ /// Markerless properties contribute their max encoded size (e.g. VarInt=5, double=8).
+ /// Markered properties contribute 1 byte (minimum marker: PropertySkip/Null/True/False).
+ /// Used by EnsureCapacity before property writes to avoid mid-object buffer Grow().
+ ///
+ public int MinWriteSize { get; }
+
public BinarySerializeTypeMetadata(Type type, Func ignorePropertyFilter) : base(type,ignorePropertyFilter)
{
// Use pre-computed WritableProperties directly - no method call overhead!
@@ -130,6 +138,7 @@ public static partial class AcBinarySerializer
Properties = new BinaryPropertyAccessor[orderedProperties.Length];
var complexCount = 0;
+ var minWriteSize = 0;
for (var i = 0; i < orderedProperties.Length; i++)
{
@@ -142,8 +151,13 @@ public static partial class AcBinarySerializer
{
accessor.ComplexPropertyIndex = complexCount++;
}
+
+ // Accumulate min write size per property
+ minWriteSize += ComputePropertyMaxWriteSize(accessor);
}
+ MinWriteSize = minWriteSize;
+
// Set scan optimization flags
ComplexPropertyCount = complexCount;
HasComplexProperties = complexCount > 0;
@@ -161,6 +175,41 @@ public static partial class AcBinarySerializer
}
}
+ ///
+ /// Returns the worst-case byte count for a markered property write (skip-or-write pattern).
+ /// Most types: type_code(1) + markerless_max. Boolean/Enum are special.
+ /// Used as the single source of truth for MinWriteSize — covers both UseMetadata=true
+ /// (all properties markered) and UseMetadata=false (ExpectedTypeCode properties markerless,
+ /// which is strictly smaller than markered).
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int GetMarkeredMaxSize(PropertyAccessorType accessorType) => accessorType switch
+ {
+ PropertyAccessorType.Boolean => 1, // PropertySkip or True (single byte)
+ PropertyAccessorType.Enum => 7, // Enum(1) + Int32(1) + VarInt(5)
+ PropertyAccessorType.Int32 => 6, // marker(1) + VarInt(5)
+ PropertyAccessorType.Int64 => 11, // marker(1) + VarLong(10)
+ PropertyAccessorType.Double => 9, // marker(1) + 8
+ PropertyAccessorType.Single => 5, // marker(1) + 4
+ PropertyAccessorType.Decimal => 17, // marker(1) + 16
+ PropertyAccessorType.DateTime => 10, // marker(1) + Ticks(8) + Kind(1)
+ PropertyAccessorType.Guid => 17, // marker(1) + 16
+ PropertyAccessorType.Byte => 2, // marker(1) + 1
+ PropertyAccessorType.Int16 => 3, // marker(1) + 2
+ PropertyAccessorType.UInt16 => 3, // marker(1) + 2
+ PropertyAccessorType.UInt32 => 6, // marker(1) + VarUInt(5)
+ PropertyAccessorType.UInt64 => 11, // marker(1) + VarULong(10)
+ _ => 1 // String/Complex/Collection: safe writes
+ };
+
+ ///
+ /// Computes the maximum byte size a single property write can consume.
+ /// Always uses markered max — covers both UseMetadata=true (all properties markered via
+ /// WritePropertyOrSkipUnsafe) and UseMetadata=false (markerless, which is strictly ≤ markered).
+ ///
+ private static int ComputePropertyMaxWriteSize(BinaryPropertyAccessor accessor)
+ => GetMarkeredMaxSize(accessor.AccessorType);
+
private static Type? FindGeneratedSerializerType(Type type)
{
var generatedTypeName = $"{type.FullName}_AcBinarySerializer";