diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs
index 19c7f83..45e1eff 100644
--- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs
+++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs
@@ -1820,12 +1820,11 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
}
///
- /// Emits inline string read from type code. Handles all string wire formats.
- /// FixStr (range 34-65) is checked first as hot path for short strings.
- /// Remaining codes use switch for O(1) JIT jump-table dispatch:
- /// String=16, StringInterned=17, StringEmpty=18, StringInternFirst=19, Null=0.
- /// This eliminates the sequential if-else chain that penalized StringInterned
- /// (the hot path for repeated interned strings) with 4 comparisons.
+ /// Emits inline string read from type code. Handles all string wire formats:
+ /// FixStr (UTF-8 short, 103-134), FixStrAscii (ASCII short, 135-166), String (UTF-8 long, 91),
+ /// StringAscii (ASCII long, 167), StringInterned, StringEmpty, StringInternFirst, Null.
+ /// FixStr/FixStrAscii are checked first as hot paths for short strings — ASCII variant
+ /// dispatches to ReadAsciiBytesAsString (byte→char widen, no UTF-8 decode).
///
private static void EmitReadString(StringBuilder sb, string a, string tc, string i)
{
@@ -1835,8 +1834,14 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine($"{i} var flen = BinaryTypeCode.DecodeFixStrLength({tc});");
sb.AppendLine($"{i} {a} = flen == 0 ? string.Empty : context.ReadStringUtf8(flen);");
sb.AppendLine($"{i}}}");
- // Switch gives O(1) dispatch via JIT jump table for codes 0, 16-19.
- // StringInterned (17) is the hot path for repeated interned strings — no longer buried at 4th else-if.
+ // FixStrAscii — ASCII short strings, byte→char widen path (skips UTF-8 decode).
+ sb.AppendLine($"{i}else if (BinaryTypeCode.IsFixStrAscii({tc}))");
+ sb.AppendLine($"{i}{{");
+ sb.AppendLine($"{i} var falen = BinaryTypeCode.DecodeFixStrAsciiLength({tc});");
+ sb.AppendLine($"{i} {a} = falen == 0 ? string.Empty : context.ReadAsciiBytesAsString(falen);");
+ sb.AppendLine($"{i}}}");
+ // Switch gives O(1) dispatch via JIT jump table for the long markers.
+ // StringInterned is the hot path for repeated interned strings.
sb.AppendLine($"{i}else switch ({tc})");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} case BinaryTypeCode.StringInterned:");
@@ -1848,6 +1853,12 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine($"{i} {a} = slen == 0 ? string.Empty : context.ReadStringUtf8(slen);");
sb.AppendLine($"{i} break;");
sb.AppendLine($"{i} }}");
+ sb.AppendLine($"{i} case BinaryTypeCode.StringAscii:");
+ sb.AppendLine($"{i} {{");
+ sb.AppendLine($"{i} var salen = (int)context.ReadVarUInt();");
+ sb.AppendLine($"{i} {a} = salen == 0 ? string.Empty : context.ReadAsciiBytesAsString(salen);");
+ sb.AppendLine($"{i} break;");
+ sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} case BinaryTypeCode.StringInternFirst:");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.DisableStringCaching();");
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs
index 9fd809c..792b858 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs
@@ -418,6 +418,44 @@ public static partial class AcBinaryDeserializer
return DecodeUtf8(length);
}
+ ///
+ /// Reads ASCII bytes from the wire and widens them to a UTF-16
+ /// string. Caller MUST guarantee the payload is pure ASCII — typically by dispatching on a
+ /// FixStrAscii / StringAscii marker (the marker IS the ASCII-validity contract).
+ ///
+ ///
+ /// Skips the UTF-8 decoder entirely — every byte maps 1:1 to a char via simple widening.
+ /// Uses .GetString for the widen — Latin1 is byte→char
+ /// 1:1 (codepoints 0..255), and ASCII (0..127) is a strict subset, so for marker-validated
+ /// ASCII payloads the result is identical to a hand-rolled (char)b widen but uses
+ /// the BCL's SIMD-accelerated implementation (single-shot allocation + memcpy-class widen).
+ ///
+ /// Beats a string.Create + scalar callback widen by avoiding the lambda-state passing
+ /// and JIT-trust on auto-vectorization across the callback boundary.
+ ///
+ /// FastWire mode never emits ASCII markers — they're a Compact-mode-only optimization. If
+ /// FastWire encounters one (cross-mode wire mismatch), the read still works but the FastWire
+ /// raw-memcpy fast path doesn't apply.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public string ReadAsciiBytesAsString(int byteLength)
+ {
+ if (byteLength == 0) return string.Empty;
+
+ EnsureAvailable(byteLength);
+
+ // Cached short-string path (WASM optimization) — leverages full-content hash + Ascii.Equals
+ // verification (which is a no-op fast path on ASCII content).
+ if (_useStringCaching && byteLength <= _maxCachedStringLength)
+ {
+ return ReadStringUtf8Cached(byteLength);
+ }
+
+ var pos = _position;
+ _position += byteLength;
+ return Encoding.Latin1.GetString(_buffer, pos, byteLength);
+ }
+
///
/// Custom UTF-8 → UTF-16 string decoder.
///
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
index c41224e..209cf47 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
@@ -5,6 +5,7 @@ using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
+using System.IO.Pipelines;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
@@ -100,6 +101,7 @@ public static partial class AcBinaryDeserializer
readers[BinaryTypeCode.StringInterned] = static (ctx, _, _) => ctx.GetInternedString((int)ctx.ReadVarUInt());
readers[BinaryTypeCode.StringEmpty] = static (_, _, _) => string.Empty;
readers[BinaryTypeCode.StringInternFirst] = static (ctx, _, _) => ReadAndRegisterInternedString(ctx);
+ readers[BinaryTypeCode.StringAscii] = static (ctx, _, _) => ReadPlainStringAscii(ctx);
readers[BinaryTypeCode.DateTime] = static (ctx, _, _) => ctx.ReadDateTimeUnsafe();
readers[BinaryTypeCode.DateTimeOffset] = static (ctx, _, _) => ctx.ReadDateTimeOffsetUnsafe();
readers[BinaryTypeCode.TimeSpan] = static (ctx, _, _) => ctx.ReadTimeSpanUnsafe();
@@ -125,6 +127,14 @@ public static partial class AcBinaryDeserializer
readers[code] = CreateFixStrReader(length);
}
+ // Register FixStrAscii readers (135..166) — pure-ASCII short-string fast path.
+ // The marker IS the validity contract — reader byte→char widens without UTF-8 decode.
+ for (var code = BinaryTypeCode.FixStrAsciiBase; code <= BinaryTypeCode.FixStrAsciiMax; code++)
+ {
+ var length = BinaryTypeCode.DecodeFixStrAsciiLength(code);
+ readers[code] = CreateFixStrAsciiReader(length);
+ }
+
// Register FixObj slot readers (0..SlotCount-1)
for (var slot = 0; slot < BinaryTypeCode.SlotCount; slot++) readers[slot] = CreateFixObjReader(slot);
@@ -144,6 +154,18 @@ public static partial class AcBinaryDeserializer
return (ctx, _, _) => ctx.ReadStringUtf8(length);
}
+ ///
+ /// Creates a reader for FixStrAscii with the given byte length (also char count, ASCII = 1:1).
+ /// Skips UTF-8 decode — byte→char widen only. Marker enforces ASCII validity.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static TypeReader CreateFixStrAsciiReader(int length) where TInput : struct, IBinaryInputBase
+ {
+ if (length == 0) return static (_, _, _) => string.Empty;
+
+ return (ctx, _, _) => ctx.ReadAsciiBytesAsString(length);
+ }
+
///
/// Creates a reader for FixObj slot (0..SlotCount-1).
///
@@ -353,6 +375,57 @@ public static partial class AcBinaryDeserializer
}
}
+ ///
+ /// Drains a end-to-end into a fresh
+ /// and deserializes one message. Background Task.Run deserializes incrementally while
+ /// the calling thread drains the reader. For long-lived multi-message scenarios use the
+ /// overloads directly.
+ ///
+ public static async Task DeserializeFromPipeReaderAsync<[DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] T>(PipeReader reader, AcBinarySerializerOptions options, CancellationToken cancellationToken = default)
+ {
+ if (reader is null) throw new ArgumentNullException(nameof(reader));
+ using var input = new AsyncPipeReaderInput(options.BufferWriterChunkSize * 2);
+ var deserTask = Task.Run(() => Deserialize(input, options), cancellationToken);
+ await DrainPipeReaderToInputAsync(reader, input, cancellationToken).ConfigureAwait(false);
+ return await deserTask.ConfigureAwait(false);
+ }
+
+ ///
+ /// Non-generic Type-based counterpart to .
+ /// For runtime-typed scenarios (MVC formatters, plugin frameworks).
+ ///
+ public static async Task