diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs
index d92b245..1f4996f 100644
--- a/AyCode.Core.Serializers.Console/Program.cs
+++ b/AyCode.Core.Serializers.Console/Program.cs
@@ -124,10 +124,10 @@ public static class Program
/// Aggregation rule: if ALL tagged types have the feature enabled → true; if any tagged type
/// disables it → false (a single disabling type suppresses the feature on the type-graph).
///
- private static readonly (bool refHandling, bool internString, bool metadata, bool idTracking) _attrFlags
+ private static readonly (bool refHandling, bool internString, bool metadata, bool idTracking, bool propertyFilter) _attrFlags
= ScanAcBinaryAttributeFlags();
- private static (bool refHandling, bool internString, bool metadata, bool idTracking) ScanAcBinaryAttributeFlags()
+ private static (bool refHandling, bool internString, bool metadata, bool idTracking, bool propertyFilter) ScanAcBinaryAttributeFlags()
{
var attrs = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => { try { return a.GetTypes(); } catch { return Array.Empty(); } })
@@ -135,13 +135,14 @@ public static class Program
.Where(a => a != null)
.ToList();
- if (attrs.Count == 0) return (false, false, false, false);
+ if (attrs.Count == 0) return (false, false, false, false, false);
return (
- refHandling: attrs.All(a => a!.EnableRefHandlingFeature),
- internString: attrs.All(a => a!.EnableInternStringFeature),
- metadata: attrs.All(a => a!.EnableMetadataFeature),
- idTracking: attrs.All(a => a!.EnableIdTrackingFeature));
+ refHandling: attrs.All(a => a!.EnableRefHandlingFeature),
+ internString: attrs.All(a => a!.EnableInternStringFeature),
+ metadata: attrs.All(a => a!.EnableMetadataFeature),
+ idTracking: attrs.All(a => a!.EnableIdTrackingFeature),
+ propertyFilter: attrs.All(a => a!.EnablePropertyFilterFeature));
}
///
@@ -153,10 +154,16 @@ public static class Program
///
private static string BuildAcBinaryOptionsDescription(AcBinarySerializerOptions options, string extra = "")
{
+ // PropertyFilter: opt-side is "Set"/"None" depending on whether a callback is registered (the callback
+ // itself isn't a meaningful display value); attr-side is the cross-type-aggregated bool (true = every
+ // tagged type has the feature enabled, false = at least one type opted out via
+ // [AcBinarySerializable(enablePropertyFilterFeature: false)] → SGen-emit + Runtime hot-loop both gate).
+ var propFilterOpt = options.PropertyFilter == null ? "None" : "Set";
return $"WireMode={options.WireMode}, " +
$"RefHandling={options.ReferenceHandling}(opt) | {_attrFlags.refHandling} (attr), " +
$"Interning={options.UseStringInterning}(opt) | {_attrFlags.internString} (attr), " +
$"Metadata={options.UseMetadata}(opt) | {_attrFlags.metadata} (attr), " +
+ $"PropertyFilter={propFilterOpt}(opt) | {_attrFlags.propertyFilter} (attr), " +
$"SGen={options.UseGeneratedCode}, " +
$"Compression={options.UseCompression}{extra}";
}
diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs
index 7be815a..8badf04 100644
--- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs
+++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs
@@ -41,7 +41,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
// attribute properties so consumers can opt out per type. Until then, keep both `false` for
// benchmark-vs-MemPack measurements; flip to `true` for production where the features are needed.
// ────────────────────────────────────────────────────────────────────────────────────────────
- private const bool UsePropertyFilter = false;
+ // UsePropertyFilter const removed — replaced by `[AcBinarySerializable(EnablePropertyFilterFeature = ...)]`
+ // attribute flag, propagated through SerializableClassInfo.EnablePropertyFilter to EmitProp/EmitScanProp.
private const bool UsePolymorphType = false;
private static readonly DiagnosticDescriptor CircularReferenceWarning = new(
@@ -82,6 +83,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
var enableRefHandling = true;
var enableInternString = true;
var enableMetadata = true;
+ var enablePropertyFilter = true;
var binarySerializableAttr = typeSymbol.GetAttributes().FirstOrDefault(a =>
a.AttributeClass?.ToDisplayString() == AttributeName);
if (binarySerializableAttr != null)
@@ -94,15 +96,25 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
enableRefHandling = all;
enableInternString = all;
enableMetadata = all;
+ enablePropertyFilter = all;
}
else if (binarySerializableAttr.ConstructorArguments.Length == 4)
{
- // Four bool ctor: (metadata, idTracking, refHandling, internString)
+ // Four bool ctor: (metadata, idTracking, refHandling, internString) — filter defaults to true
enableMetadata = (bool)binarySerializableAttr.ConstructorArguments[0].Value!;
enableIdTracking = (bool)binarySerializableAttr.ConstructorArguments[1].Value!;
enableRefHandling = (bool)binarySerializableAttr.ConstructorArguments[2].Value!;
enableInternString = (bool)binarySerializableAttr.ConstructorArguments[3].Value!;
}
+ else if (binarySerializableAttr.ConstructorArguments.Length == 5)
+ {
+ // Five bool ctor: (metadata, idTracking, refHandling, internString, propertyFilter)
+ enableMetadata = (bool)binarySerializableAttr.ConstructorArguments[0].Value!;
+ enableIdTracking = (bool)binarySerializableAttr.ConstructorArguments[1].Value!;
+ enableRefHandling = (bool)binarySerializableAttr.ConstructorArguments[2].Value!;
+ enableInternString = (bool)binarySerializableAttr.ConstructorArguments[3].Value!;
+ enablePropertyFilter = (bool)binarySerializableAttr.ConstructorArguments[4].Value!;
+ }
}
foreach (var p in GetAllSerializablePropertySymbols(typeSymbol))
@@ -384,7 +396,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
var typeNameHash = ComputeFnvHash(typeSymbol.Name);
var propertyNameHashes = properties.Select(prop => ComputeFnvHash(prop.Name)).ToArray();
var selfScanFlags = ComputeNeedsScan(typeSymbol);
- return new SerializableClassInfo(namespaceName, className, typeSymbol.ToDisplayString(), properties, isIId, idTypeName, enableRefHandling, typeNameHash, propertyNameHashes, enableMetadata, selfScanFlags.needsIdScan, selfScanFlags.needsAllRefScan, selfScanFlags.needsInternScan);
+ return new SerializableClassInfo(namespaceName, className, typeSymbol.ToDisplayString(), properties, isIId, idTypeName, enableRefHandling, typeNameHash, propertyNameHashes, enableMetadata, enablePropertyFilter, selfScanFlags.needsIdScan, selfScanFlags.needsAllRefScan, selfScanFlags.needsInternScan);
}
private static void Execute(ImmutableArray classes, SourceProductionContext context)
@@ -537,7 +549,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
foreach (var p in ci.Properties)
{
sb.AppendLine();
- EmitProp(sb, p, " ", ci.FullTypeName, ci.EnableMetadata);
+ EmitProp(sb, p, " ", ci.FullTypeName, ci.EnableMetadata, ci.EnablePropertyFilter);
}
sb.AppendLine(" }");
@@ -668,7 +680,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
{
sb.AppendLine();
hasAnyScanProp = true;
- EmitScanProp(sb, p, " ", ci.FullTypeName);
+ EmitScanProp(sb, p, " ", ci.FullTypeName, ci.EnablePropertyFilter);
}
if (!hasAnyScanProp)
@@ -679,7 +691,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine(" }");
}
- private static void EmitProp(StringBuilder sb, PropInfo p, string i, string fullTypeName, bool enableMetadata)
+ private static void EmitProp(StringBuilder sb, PropInfo p, string i, string fullTypeName, bool enableMetadata, bool enablePropertyFilter)
{
var a = $"obj.{p.Name}";
@@ -699,9 +711,10 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
// All non-markerless properties: emit PropertyFilter guard
// When filter returns false, write PropertySkip and skip the property write.
- // Gated by `UsePropertyFilter` (TEMPORARY const) — `false` skips emit entirely → leaner
- // generated code on benchmark workloads where no property-filter is ever set.
- if (UsePropertyFilter)
+ // Gated by `[AcBinarySerializable(EnablePropertyFilterFeature = ...)]` — when false, the entire
+ // filter-check block is omitted from emit (no `HasPropertyFilter` test, no filter-context allocation,
+ // no lambda-call) → leaner generated code on hot-path types that never use a property-filter.
+ if (enablePropertyFilter)
{
sb.AppendLine($"{i}if (context.HasPropertyFilter)");
sb.AppendLine($"{i}{{");
@@ -911,7 +924,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
/// Complex (no SGen): fallback to ScanValueGenerated (runtime wrapper lookup).
/// Collection: iterate elements with same patterns.
///
- private static void EmitScanProp(StringBuilder sb, PropInfo p, string i, string fullTypeName)
+ private static void EmitScanProp(StringBuilder sb, PropInfo p, string i, string fullTypeName, bool enablePropertyFilter)
{
// Compile-time proven: no scan work for this property — skip entirely (including PropertyFilter guard)
if (!HasScanWork(p)) return;
@@ -920,8 +933,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
// PropertyFilter: must match write pass — if filter skips property, scan must skip too
// Only for non-markerless properties (matching EmitProp behavior).
- // Gated by `UsePropertyFilter` (TEMPORARY const) — same A/B flag as the writer pass.
- if (UsePropertyFilter && !IsMarkerless(p.TypeKind))
+ // Gated by `[AcBinarySerializable(EnablePropertyFilterFeature = ...)]` — same gate as the writer pass.
+ if (enablePropertyFilter && !IsMarkerless(p.TypeKind))
{
sb.AppendLine($"{i}if (context.HasPropertyFilter)");
sb.AppendLine($"{i}{{");
@@ -2996,6 +3009,10 @@ internal sealed class SerializableClassInfo
public int[] PropertyNameHashes { get; }
/// When false, skip inline metadata and use markerless property write for this type.
public bool EnableMetadata { get; }
+ /// True if EnablePropertyFilterFeature is enabled — controls per-property HasPropertyFilter
+ /// guard emission in WriteProperties / ScanObject. When false, the filter check is omitted entirely
+ /// → leaner generated code on the hot path (typical for high-throughput types that never use a filter).
+ public bool EnablePropertyFilter { get; }
/// When true, type subtree has IId types needing scan (active in OnlyId + All).
public bool NeedsIdScan { get; }
/// When true, type subtree has non-IId ref tracking (active only in All mode).
@@ -3006,8 +3023,8 @@ internal sealed class SerializableClassInfo
public bool NeedsRefScan => NeedsIdScan || NeedsAllRefScan;
/// Derived: any scan axis active.
public bool NeedsScan => NeedsIdScan || NeedsAllRefScan || NeedsInternScan;
- public SerializableClassInfo(string ns, string cn, string ftn, List p, bool isIId, string? idTypeName, bool enableRefHandling, int typeNameHash, int[] propertyNameHashes, bool enableMetadata, bool needsIdScan, bool needsAllRefScan, bool needsInternScan)
- { Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; IsIId = isIId; IdTypeName = idTypeName; EnableRefHandling = enableRefHandling; TypeNameHash = typeNameHash; PropertyNameHashes = propertyNameHashes; EnableMetadata = enableMetadata; NeedsIdScan = needsIdScan; NeedsAllRefScan = needsAllRefScan; NeedsInternScan = needsInternScan; }
+ public SerializableClassInfo(string ns, string cn, string ftn, List p, bool isIId, string? idTypeName, bool enableRefHandling, int typeNameHash, int[] propertyNameHashes, bool enableMetadata, bool enablePropertyFilter, bool needsIdScan, bool needsAllRefScan, bool needsInternScan)
+ { Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; IsIId = isIId; IdTypeName = idTypeName; EnableRefHandling = enableRefHandling; TypeNameHash = typeNameHash; PropertyNameHashes = propertyNameHashes; EnableMetadata = enableMetadata; EnablePropertyFilter = enablePropertyFilter; NeedsIdScan = needsIdScan; NeedsAllRefScan = needsAllRefScan; NeedsInternScan = needsInternScan; }
}
internal sealed class PropInfo
diff --git a/AyCode.Core/Serializers/Attributes/AcBinarySerializableAttribute.cs b/AyCode.Core/Serializers/Attributes/AcBinarySerializableAttribute.cs
index e00d186..b637060 100644
--- a/AyCode.Core/Serializers/Attributes/AcBinarySerializableAttribute.cs
+++ b/AyCode.Core/Serializers/Attributes/AcBinarySerializableAttribute.cs
@@ -19,6 +19,19 @@ public sealed class AcBinarySerializableAttribute : Attribute
public bool EnableRefHandlingFeature { get; }
public bool EnableInternStringFeature { get; }
+ ///
+ /// When true (default): the SGen-emitted writer/scan code includes the per-property
+ /// HasPropertyFilter branch + filter-context allocation + lambda-call. Required for runtime
+ /// to take effect on this type.
+ /// When false: the SGen-emitted writer/scan code omits the filter check entirely
+ /// for every property of this type — leaner generated code on the hot path. Use this when the
+ /// type is never serialized with a property-filter (typical for high-throughput message types).
+ /// Filter callbacks set on AcBinarySerializerOptions.PropertyFilter will be silently ignored
+ /// for this type's properties — only set false if you can guarantee no consumer relies on
+ /// per-property filtering for this specific type.
+ ///
+ public bool EnablePropertyFilterFeature { get; }
+
public AcBinarySerializableAttribute() : this(true)
{
}
@@ -29,13 +42,15 @@ public sealed class AcBinarySerializableAttribute : Attribute
EnableIdTrackingFeature = enableAllFeatures;
EnableRefHandlingFeature = enableAllFeatures;
EnableInternStringFeature = enableAllFeatures;
+ EnablePropertyFilterFeature = enableAllFeatures;
}
- public AcBinarySerializableAttribute(bool enableMetadataFeature, bool enableIdTrackingFeature, bool enableRefHandlingFeature, bool enableInternStringFeature)
+ public AcBinarySerializableAttribute(bool enableMetadataFeature, bool enableIdTrackingFeature, bool enableRefHandlingFeature, bool enableInternStringFeature, bool enablePropertyFilterFeature)
{
EnableMetadataFeature = enableMetadataFeature;
EnableIdTrackingFeature = enableIdTrackingFeature;
EnableRefHandlingFeature = enableRefHandlingFeature;
EnableInternStringFeature = enableInternStringFeature;
+ EnablePropertyFilterFeature = enablePropertyFilterFeature;
}
}
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs
index 3348370..d120a31 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs
@@ -30,8 +30,10 @@ public static partial class AcBinaryDeserializer
get
{
if (_position < _bufferLength) return false;
- // Multi-segment: try advancing to next segment before declaring end.
- // ArrayBinaryInput: TryAdvanceSegment => false (JIT eliminates, same as before).
+ // Trusted-single-segment fast path — JIT folds the constant: for ArrayBinaryInput
+ // the branch becomes `if (true) return true;` and the TryAdvanceSegment call below is
+ // dead-code-eliminated. For multi-segment / streaming inputs, the call is preserved.
+ if (TInput.IsTrustedSingleSegment) return true;
return !Input.TryAdvanceSegment(ref _buffer, ref _position, ref _bufferLength, 1);
}
}
@@ -47,27 +49,18 @@ public static partial class AcBinaryDeserializer
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte ReadByte()
{
- if (_position >= _bufferLength)
- {
- // Multi-segment: try advancing to next segment before giving up.
- // ArrayBinaryInput: TryAdvanceSegment => false (JIT eliminates this branch).
- if (!Input.TryAdvanceSegment(ref _buffer, ref _position, ref _bufferLength, 1))
- throw new AcBinaryDeserializationException("Unexpected end of binary payload.", _position);
- }
-
+ // Routes through EnsureAvailable to leverage the trusted-single-segment JIT-eliminate guard.
+ // ArrayBinaryInput: EnsureAvailable body fully eliminated → just `_buffer[_position++]`.
+ // Multi-segment / streaming: bounds-check + TryAdvanceSegment kept (cross-segment safe).
+ EnsureAvailable(1);
return _buffer[_position++];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte PeekByte()
{
- if (_position >= _bufferLength)
- {
- // Multi-segment: try advancing to next segment before giving up.
- if (!Input.TryAdvanceSegment(ref _buffer, ref _position, ref _bufferLength, 1))
- throw new AcBinaryDeserializationException("Unexpected end of binary payload.", _position);
- }
-
+ // Same trusted-single-segment fast path as ReadByte (no _position advance).
+ EnsureAvailable(1);
return _buffer[_position];
}
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs
index c7368e3..34834e8 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs
@@ -830,57 +830,86 @@ public static partial class AcBinarySerializer
WriteVarUIntUnsafe((uint)bytesWritten);
_position += bytesWritten;
}
+
+ return;
}
- else
+
+ switch (bytesWritten)
{
- // Non-ASCII — post-encode tier choice from bytesWritten (smallest fitting tier wins)
- int actualHeader;
- byte tierMarker;
- switch (bytesWritten)
+ // Non-ASCII — post-encode tier choice (smallest fitting tier wins). One if-else chain
+ // per tier; each branch handles shift + header-store + position update inline.
+ //
+ // Combined header-store optimization (shift > 0 only):
+ // When the actual tier downgrades from the predicted reserve (e.g. Medium predicted
+ // from charLength but Small actual from bytesWritten), the body needs a left-shift
+ // memcpy. We write the FULL combined header (uint for Small, ulong for Medium) at
+ // savedPos BEFORE the body memcpy — the slack byte(s) at the high end of the store
+ // get overwritten by the subsequent memcpy → 1 store instead of 1+N for the header.
+ // When shift == 0 (predicted tier matches actual), the body is already at its final
+ // position; a combined store would corrupt the body's first byte(s), so we fall
+ // back to separate marker + packed-header stores.
+ //
+ // Big tier (9-byte header) always has shift == 0 (reserveHeader == actualHeader == 9)
+ // because Big is the largest tier — no downgrade path possible. The 1-byte marker +
+ // 8-byte ulong-packed pattern is already minimal (no slack issue, 9 dedicated bytes).
+ case <= 255:
{
- case <= 255:
- actualHeader = 3;
- tierMarker = BinaryTypeCode.StringSmall;
- break;
- case <= 65535:
- actualHeader = 5;
- tierMarker = BinaryTypeCode.StringMedium;
- break;
- default:
- actualHeader = 9;
- tierMarker = BinaryTypeCode.StringBig;
- break;
+ // Small tier: 3-byte header [marker:1][charLen:8][utf8Len:8]
+ var shift = reserveHeader - 3;
+ if (shift > 0)
+ {
+ // Combined uint store: 4 bytes physical, 3 bytes meaningful, 1 byte slack
+ // at savedPos+3 — overwritten by body memcpy below.
+ var packedFull = (uint)BinaryTypeCode.StringSmall
+ | ((uint)charLength << 8)
+ | ((uint)bytesWritten << 16);
+ Unsafe.WriteUnaligned(ref _buffer[savedPos], packedFull);
+ _buffer.AsSpan(encodeStart, bytesWritten).CopyTo(_buffer.AsSpan(savedPos + 3, bytesWritten));
+ }
+ else
+ {
+ // shift == 0: body in place at savedPos+3 = encodeStart. Combined uint store
+ // would corrupt body's first byte; use separate 1-byte marker + 2-byte packed.
+ _buffer[savedPos] = BinaryTypeCode.StringSmall;
+ var packedHl = (ushort)(charLength | (bytesWritten << 8));
+ Unsafe.WriteUnaligned(ref _buffer[savedPos + 1], packedHl);
+ }
+ _position = savedPos + 3 + bytesWritten;
+ break;
}
-
- var nonAsciiShift = reserveHeader - actualHeader;
- if (nonAsciiShift > 0) _buffer.AsSpan(encodeStart, bytesWritten).CopyTo(_buffer.AsSpan(encodeStart - nonAsciiShift, bytesWritten));
-
- _buffer[savedPos] = tierMarker;
- switch (actualHeader)
+ case <= 65535:
{
- case 3:
+ // Medium tier: 5-byte header [marker:1][charLen:16][utf8Len:16]
+ var shift = reserveHeader - 5;
+ if (shift > 0)
{
- // A-direction: pack charLen:8 | utf8Len:8 → single ushort store
- var packed = (ushort)(charLength | (bytesWritten << 8));
- Unsafe.WriteUnaligned(ref _buffer[savedPos + 1], packed);
- break;
+ // Combined ulong store: 8 bytes physical, 5 bytes meaningful, 3 bytes slack
+ // at savedPos+5..7 — overwritten by body memcpy below.
+ var packedFull = (ulong)BinaryTypeCode.StringMedium
+ | ((ulong)(uint)charLength << 8)
+ | ((ulong)(uint)bytesWritten << 24);
+ Unsafe.WriteUnaligned(ref _buffer[savedPos], packedFull);
+ _buffer.AsSpan(encodeStart, bytesWritten).CopyTo(_buffer.AsSpan(savedPos + 5, bytesWritten));
}
- case 5:
+ else
{
- // A-direction: pack charLen:16 | utf8Len:16 → single uint store, LE
- var packed = (uint)charLength | ((uint)bytesWritten << 16);
- Unsafe.WriteUnaligned(ref _buffer[savedPos + 1], packed);
- break;
- }
- default:
- {
- // A-direction: pack charLen:32 | utf8Len:32 → single ulong store, LE
- var packed = (ulong)(uint)charLength | ((ulong)(uint)bytesWritten << 32);
- Unsafe.WriteUnaligned(ref _buffer[savedPos + 1], packed);
- break;
+ // shift == 0: separate 1-byte marker + 4-byte packed.
+ _buffer[savedPos] = BinaryTypeCode.StringMedium;
+ var packedHl = (uint)charLength | ((uint)bytesWritten << 16);
+ Unsafe.WriteUnaligned(ref _buffer[savedPos + 1], packedHl);
}
+ _position = savedPos + 5 + bytesWritten;
+ break;
+ }
+ default:
+ {
+ // Big tier: 9-byte header [marker:1][charLen:32][utf8Len:32]. shift always 0.
+ _buffer[savedPos] = BinaryTypeCode.StringBig;
+ var packed = (ulong)(uint)charLength | ((ulong)(uint)bytesWritten << 32);
+ Unsafe.WriteUnaligned(ref _buffer[savedPos + 1], packed);
+ _position = savedPos + 9 + bytesWritten;
+ break;
}
- _position = savedPos + actualHeader + bytesWritten;
}
}
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
index f635814..534b0ab 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
@@ -1663,7 +1663,11 @@ public static partial class AcBinarySerializer
{
var properties = wrapper.Metadata.Properties;
var propCount = properties.Length;
- var hasPropertyFilter = context.HasPropertyFilter;
+ // Combined runtime+attribute gate: if the type opted out of the filter feature via
+ // [AcBinarySerializable(EnablePropertyFilterFeature = false)], the runtime PropertyFilter is
+ // ignored for this type — short-circuit eliminates the per-property `ShouldSerializeProperty`
+ // call entirely. Mirrors the SGen-side gate in `EmitProp`.
+ var hasPropertyFilter = context.HasPropertyFilter && wrapper.Metadata.EnablePropertyFilterFeature;
for (var i = 0; i < propCount; i++)
{
@@ -1684,7 +1688,8 @@ public static partial class AcBinarySerializer
{
var properties = wrapper.Metadata.Properties;
var propCount = properties.Length;
- var hasPropertyFilter = context.HasPropertyFilter;
+ // Combined runtime+attribute gate — see WritePropertiesWithMeta for the rationale.
+ var hasPropertyFilter = context.HasPropertyFilter && wrapper.Metadata.EnablePropertyFilterFeature;
for (var i = 0; i < propCount; i++)
{
diff --git a/AyCode.Core/Serializers/TypeMetadataBase.cs b/AyCode.Core/Serializers/TypeMetadataBase.cs
index 703362e..6d974e0 100644
--- a/AyCode.Core/Serializers/TypeMetadataBase.cs
+++ b/AyCode.Core/Serializers/TypeMetadataBase.cs
@@ -138,6 +138,19 @@ public abstract class TypeMetadataBase
///
public bool EnableMetadataFeature { get; }
+ ///
+ /// When false, this type's runtime write loop skips the per-property
+ /// PropertyFilter callback check entirely — runtime-set filters on
+ ///
+ /// are silently ignored for this type.
+ /// Read from [AcBinarySerializable(EnablePropertyFilterFeature = ...)]; defaults to true.
+ /// Mirrors the SGen-side gate in AcBinarySourceGenerator.EmitProp: when false, the SGen
+ /// emit omits the filter-check block entirely. Runtime-side this flag combines with
+ /// context.HasPropertyFilter via short-circuit AND to drop the per-property branch
+ /// for filter-disabled types.
+ ///
+ public bool EnablePropertyFilterFeature { get; }
+
///
/// True if this type is a primitive, string, enum, Guid, DateTime, etc.
/// Pre-computed to avoid IsPrimitiveOrStringFast() calls in hot path.
@@ -240,6 +253,7 @@ public abstract class TypeMetadataBase
var serializableAttr = type.GetCustomAttribute(inherit: false);
EnableMetadataFeature = serializableAttr == null || serializableAttr.EnableMetadataFeature;
+ EnablePropertyFilterFeature = serializableAttr == null || serializableAttr.EnablePropertyFilterFeature;
if (serializableAttr is { EnableIdTrackingFeature: false })
{
IsIId = false;