diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
index 1647e58..d4a27d5 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
@@ -661,9 +661,8 @@ public static partial class AcBinarySerializer
///
/// Writes a non-primitive value with a pre-resolved wrapper (from PropertyTypeWrappers cache).
/// Avoids GetWrapper dictionary lookup. Handles byte[], dictionary, collection, and complex objects.
- /// When polyRuntimeType is set, writes polymorphic prefix/combined markers.
///
- private static void WriteValueNonPrimitiveWithWrapper(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth, Type? polyRuntimeType = null)
+ private static void WriteValueNonPrimitiveWithWrapper(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth)
where TOutput : struct, IBinaryOutputBase
{
var type = wrapper.Metadata.SourceType;
@@ -677,7 +676,6 @@ public static partial class AcBinarySerializer
if (depth > context.MaxDepth)
{
- if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType);
context.WriteByte(BinaryTypeCode.Null);
return;
}
@@ -685,7 +683,6 @@ public static partial class AcBinarySerializer
// Handle byte arrays specially (value-like, no reference tracking)
if (value is byte[] byteArray)
{
- if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType);
WriteByteArray(byteArray, context);
return;
}
@@ -693,7 +690,6 @@ public static partial class AcBinarySerializer
// Handle dictionaries
if (value is IDictionary dictionary)
{
- if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType);
WriteDictionary(dictionary, context, depth);
return;
}
@@ -701,13 +697,61 @@ public static partial class AcBinarySerializer
// Handle collections/arrays
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
{
- if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType);
WriteArray(enumerable, wrapper, context, depth);
return;
}
- // Handle complex objects — combined poly+ref markers handled inside WriteObject
- WriteObject(value, wrapper, context, depth, polyRuntimeType);
+ // Handle complex objects
+ WriteObject(value, wrapper, context, depth);
+ }
+
+ ///
+ /// Polymorphic variant of WriteValueNonPrimitiveWithWrapper.
+ /// Cold path: polymorphism is rare. Writes poly prefix for non-object types,
+ /// delegates to WriteObjectPolymorphic for combined poly+ref marker handling.
+ ///
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static void WriteValueNonPrimitiveWithWrapperPoly(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth, Type polyRuntimeType)
+ where TOutput : struct, IBinaryOutputBase
+ {
+ var type = wrapper.Metadata.SourceType;
+
+ if (type.IsValueType)
+ {
+ if (TryWritePrimitive(value, value.GetType(), context))
+ return;
+ }
+
+ if (depth > context.MaxDepth)
+ {
+ context.WritePolymorphicPrefix(polyRuntimeType);
+ context.WriteByte(BinaryTypeCode.Null);
+ return;
+ }
+
+ if (value is byte[] byteArray)
+ {
+ context.WritePolymorphicPrefix(polyRuntimeType);
+ WriteByteArray(byteArray, context);
+ return;
+ }
+
+ if (value is IDictionary dictionary)
+ {
+ context.WritePolymorphicPrefix(polyRuntimeType);
+ WriteDictionary(dictionary, context, depth);
+ return;
+ }
+
+ if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
+ {
+ context.WritePolymorphicPrefix(polyRuntimeType);
+ WriteArray(enumerable, wrapper, context, depth);
+ return;
+ }
+
+ // Complex object — handles combined poly+ref markers
+ WriteObjectPolymorphic(value, wrapper, context, depth, polyRuntimeType);
}
///
@@ -1025,9 +1069,7 @@ public static partial class AcBinarySerializer
}
else
{
- // StringRef: write index reference only (no getter call, no string data)
- context.WriteByte(BinaryTypeCode.StringInterned);
- context.WriteVarUInt((uint)planEntry.CacheMapIndex);
+ WriteStringInternRef(context, planEntry.CacheMapIndex);
}
return;
}
@@ -1069,101 +1111,173 @@ public static partial class AcBinarySerializer
context.WriteBytes(value);
}
+ ///
+ /// String intern 2nd occurrence — cold path, just writes reference index.
+ ///
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static void WriteStringInternRef(BinarySerializationContext context, int cacheMapIndex)
+ where TOutput : struct, IBinaryOutputBase
+ {
+ context.WriteByte(BinaryTypeCode.StringInterned);
+ context.WriteVarUInt((uint)cacheMapIndex);
+ }
+
+ ///
+ /// Object ref 2nd occurrence — cold path, just writes reference index.
+ ///
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static void WriteObjectRef(BinarySerializationContext context, int cacheMapIndex)
+ where TOutput : struct, IBinaryOutputBase
+ {
+ context.WriteByte(BinaryTypeCode.ObjectRef);
+ context.WriteVarUInt((uint)cacheMapIndex);
+ }
+
#endregion
#region Complex Type Writers
- private static void WriteObject(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth, Type? polyRuntimeType = null)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void WriteObject(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth)
where TOutput : struct, IBinaryOutputBase
{
var metadata = wrapper.Metadata;
-
- // Per-type metadata flag: when EnableMetadataFeature=false on [AcBinarySerializable],
- // skip inline metadata and use markerless property write — even when global UseMetadata=true.
- // Deserializer must have the same attribute on the type (developer responsibility).
var useMetaForType = context.UseMetadata && metadata.EnableMetadataFeature;
- // UseMetadata: típus regisztrálása (első vs ismételt előfordulás tracking)
- var isFirstMetadataOccurrence = false;
- if (useMetaForType)
- {
- isFirstMetadataOccurrence = BinarySerializationContext.RegisterMetadataType(wrapper);
- }
-
- // Reference handling: consume pre-computed write plan entry from scan pass cursor
- var cachedObjectCacheIndex = -1; // -1 = not cached, 0+ = cache index for first write
+ // Only IId types with ref handling enabled go to cold path
if (context.UseTypeReferenceHandling(metadata))
{
- if (context.TryConsumeWritePlanEntry(out var planEntry))
- {
- ValidateWritePlanObject(in planEntry, value, wrapper);
- if (planEntry.IsFirst)
- {
- // First occurrence of a cached IId object — write full object + cache index
- cachedObjectCacheIndex = planEntry.CacheMapIndex;
- }
- else
- {
- // 2+ occurrence → write ObjectRef directly (no poly prefix needed —
- // object already in cache, deser knows the type)
- context.WriteByte(BinaryTypeCode.ObjectRef);
- context.WriteVarUInt((uint)planEntry.CacheMapIndex);
- return;
- }
- }
+ if (useMetaForType)
+ WriteObjectWithRefHandlingMeta(value, wrapper, context, depth);
+ else
+ WriteObjectWithRefHandling(value, wrapper, context, depth);
+ return;
}
- // Marker kiírása — polymorphic vs non-polymorphic paths
- if (polyRuntimeType != null)
+ if (useMetaForType)
{
- WritePolymorphicMarker(context, polyRuntimeType, cachedObjectCacheIndex);
- }
- else if (useMetaForType)
- {
- if (cachedObjectCacheIndex >= 0)
- {
- context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);
- context.WriteVarUInt((uint)cachedObjectCacheIndex);
- }
- else
- {
- context.WriteByte(BinaryTypeCode.ObjectWithMetadata);
- }
+ // UseMetadata: típus regisztrálása (első vs ismételt előfordulás tracking)
+ var isFirstMetadataOccurrence = BinarySerializationContext.RegisterMetadataType(wrapper);
+
+ // Marker kiírása — no ref handling, no cachedObjectCacheIndex
+ context.WriteByte(BinaryTypeCode.ObjectWithMetadata);
context.WriteInlineMetadata(wrapper.Metadata, isFirstMetadataOccurrence);
}
else
{
- if (cachedObjectCacheIndex >= 0)
+ // FixObj: assign slot on first occurrence this session
+ if (!wrapper.PolymorphicSeen)
{
- context.WriteByte(BinaryTypeCode.ObjectRefFirst);
- context.WriteVarUInt((uint)cachedObjectCacheIndex);
+ wrapper.PolymorphicSeen = true;
+ wrapper.PolymorphicCacheIndex = context._nextTypeSlot++;
+ }
+ if (wrapper.PolymorphicCacheIndex < BinaryTypeCode.SlotCount)
+ context.WriteByte((byte)wrapper.PolymorphicCacheIndex);
+ else
+ context.WriteByte(BinaryTypeCode.Object);
+ }
+
+ WriteObjectProperties(value, metadata, wrapper, context, depth, useMetaForType);
+ }
+
+ ///
+ /// WriteObject variant with reference handling, no metadata.
+ /// Cold path: only IId types with ref tracking enabled.
+ ///
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static void WriteObjectWithRefHandling(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth)
+ where TOutput : struct, IBinaryOutputBase
+ {
+ // Reference handling: consume pre-computed write plan entry from scan pass cursor
+ var cachedObjectCacheIndex = -1;
+ if (context.TryConsumeWritePlanEntry(out var planEntry))
+ {
+ ValidateWritePlanObject(in planEntry, value, wrapper);
+ if (planEntry.IsFirst)
+ {
+ cachedObjectCacheIndex = planEntry.CacheMapIndex;
}
else
{
- // FixObj: assign slot on first occurrence this session
- if (!wrapper.PolymorphicSeen)
- {
- wrapper.PolymorphicSeen = true;
- wrapper.PolymorphicCacheIndex = context._nextTypeSlot++;
- }
- if (wrapper.PolymorphicCacheIndex < BinaryTypeCode.SlotCount)
- context.WriteByte((byte)wrapper.PolymorphicCacheIndex);
- else
- context.WriteByte(BinaryTypeCode.Object);
+ WriteObjectRef(context, planEntry.CacheMapIndex);
+ return;
}
}
- // Write all properties (startIndex=0, including Id for IId types)
+ // Marker kiírása — no metadata
+ if (cachedObjectCacheIndex >= 0)
+ {
+ context.WriteByte(BinaryTypeCode.ObjectRefFirst);
+ context.WriteVarUInt((uint)cachedObjectCacheIndex);
+ }
+ else
+ {
+ if (!wrapper.PolymorphicSeen)
+ {
+ wrapper.PolymorphicSeen = true;
+ wrapper.PolymorphicCacheIndex = context._nextTypeSlot++;
+ }
+ if (wrapper.PolymorphicCacheIndex < BinaryTypeCode.SlotCount)
+ context.WriteByte((byte)wrapper.PolymorphicCacheIndex);
+ else
+ context.WriteByte(BinaryTypeCode.Object);
+ }
+
+ WriteObjectProperties(value, wrapper.Metadata, wrapper, context, depth, useMetaForType: false);
+ }
+
+ ///
+ /// WriteObject variant with reference handling + metadata.
+ /// Cold path: IId types with ref tracking + UseMetadata enabled.
+ ///
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static void WriteObjectWithRefHandlingMeta(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth)
+ where TOutput : struct, IBinaryOutputBase
+ {
+ var isFirstMetadataOccurrence = BinarySerializationContext.RegisterMetadataType(wrapper);
+
+ // Reference handling: consume pre-computed write plan entry from scan pass cursor
+ var cachedObjectCacheIndex = -1;
+ if (context.TryConsumeWritePlanEntry(out var planEntry))
+ {
+ ValidateWritePlanObject(in planEntry, value, wrapper);
+ if (planEntry.IsFirst)
+ {
+ cachedObjectCacheIndex = planEntry.CacheMapIndex;
+ }
+ else
+ {
+ WriteObjectRef(context, planEntry.CacheMapIndex);
+ return;
+ }
+ }
+
+ // Marker kiírása — with metadata
+ if (cachedObjectCacheIndex >= 0)
+ {
+ context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);
+ context.WriteVarUInt((uint)cachedObjectCacheIndex);
+ }
+ else
+ {
+ context.WriteByte(BinaryTypeCode.ObjectWithMetadata);
+ }
+ context.WriteInlineMetadata(wrapper.Metadata, isFirstMetadataOccurrence);
+
+ WriteObjectProperties(value, wrapper.Metadata, wrapper, context, depth, useMetaForType: true);
+ }
+
+ ///
+ /// Shared property writing loop — used by WriteObject, WriteObjectWithRefHandling, WriteObjectPolymorphic.
+ ///
+ private static void WriteObjectProperties(object value, BinarySerializeTypeMetadata metadata, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth, bool useMetaForType)
+ where TOutput : struct, IBinaryOutputBase
+ {
var nextDepth = depth + 1;
var properties = metadata.Properties;
var propCount = properties.Length;
var hasPropertyFilter = context.HasPropertyFilter;
- // Source-generated fast path: bypass the entire switch/delegate loop.
- // Reference handling is safe: ref tracking happens in WriteObject (before WriteProperties)
- // and child objects go through WriteValueGenerated → WriteObject → runtime ref tracking.
- // String interning is safe: generated code uses pre-computed interningFlags bit-check
- // matching runtime UseStringPropertyInterning — cursor alignment guaranteed for all modes.
if (context.UseGeneratedCode)
{
var generatedWriter = wrapper.GeneratedWriter;
@@ -1176,14 +1290,9 @@ public static partial class AcBinarySerializer
if (!useMetaForType)
{
- // Markerless loop: no extra branching per property for the common case.
- // Properties with ExpectedTypeCode write raw values (no type marker, no skip).
- // Properties without ExpectedTypeCode (bool, enum, string, object) use the standard path.
- // Also used when EnableMetadataFeature=false on the type (per-type metadata opt-out).
for (var i = 0; i < propCount; i++)
{
var prop = properties[i];
- //context.CurrentProperty = prop;
if (prop.ExpectedTypeCode.HasValue)
{
@@ -1201,11 +1310,9 @@ public static partial class AcBinarySerializer
}
else
{
- // UseMetadata=true loop — UNCHANGED, zero extra overhead
for (var i = 0; i < propCount; i++)
{
var prop = properties[i];
- //context.CurrentProperty = prop;
if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
{
@@ -1251,6 +1358,42 @@ public static partial class AcBinarySerializer
}
}
+ ///
+ /// Polymorphic object writing — handles combined poly+ref markers.
+ /// Cold path: polymorphism is rare, NoInlining acceptable.
+ /// Poly always implies UseMetadata=false (checked in WritePropertyOrSkip).
+ ///
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static void WriteObjectPolymorphic(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth, Type polyRuntimeType)
+ where TOutput : struct, IBinaryOutputBase
+ {
+ var metadata = wrapper.Metadata;
+
+ // Reference handling
+ var cachedObjectCacheIndex = -1;
+ if (context.UseTypeReferenceHandling(metadata))
+ {
+ if (context.TryConsumeWritePlanEntry(out var planEntry))
+ {
+ ValidateWritePlanObject(in planEntry, value, wrapper);
+ if (planEntry.IsFirst)
+ {
+ cachedObjectCacheIndex = planEntry.CacheMapIndex;
+ }
+ else
+ {
+ WriteObjectRef(context, planEntry.CacheMapIndex);
+ return;
+ }
+ }
+ }
+
+ // Poly marker (handles combined poly+ref)
+ WritePolymorphicMarker(context, polyRuntimeType, cachedObjectCacheIndex);
+
+ WriteObjectProperties(value, metadata, wrapper, context, depth, false);
+ }
+
///
/// Checks if a property value is null or default without boxing for value types.
///
@@ -1560,12 +1703,6 @@ public static partial class AcBinarySerializer
{
var runtimeType = value.GetType();
- // Polymorphic detection: when declared type ≠ runtime type, pass polyRuntimeType
- // to WriteValueNonPrimitiveWithWrapper → WriteObject for combined marker handling.
- // For collections: normal prefix pattern (68/70 + inner Array/Dict marker).
- // For objects: combined markers (69/71) when RefFirst, no prefix for ObjectRef.
- var isPoly = !context.UseMetadata && prop.IsPolymorphicCandidate && runtimeType != prop.PropertyType;
-
var complexIdx = prop.ComplexPropertyIndex;
if (complexIdx >= 0)
{
@@ -1575,12 +1712,16 @@ public static partial class AcBinarySerializer
propWrapper = context.GetWrapper(runtimeType);
parentWrapper.SetPropertyTypeWrapper(complexIdx, propWrapper);
}
- WriteValueNonPrimitiveWithWrapper(value, propWrapper, context, depth, isPoly ? runtimeType : null);
+ if (!context.UseMetadata && prop.IsPolymorphicCandidate && runtimeType != prop.PropertyType)
+ WriteValueNonPrimitiveWithWrapperPoly(value, propWrapper, context, depth, runtimeType);
+ else
+ WriteValueNonPrimitiveWithWrapper(value, propWrapper, context, depth);
}
else
{
// Non-complex in default case (nullable value type, etc.)
- if (isPoly) context.WritePolymorphicPrefix(runtimeType);
+ if (!context.UseMetadata && prop.IsPolymorphicCandidate && runtimeType != prop.PropertyType)
+ context.WritePolymorphicPrefix(runtimeType);
WriteValueNonPrimitive(value, runtimeType, context, depth);
}
}