# AcBinarySerializer — TODO

This page covers planned work for the **binary serializer core** (format, SGen, options, deserialization context, buffer writer). Work specific to the **streaming I/O layer** (`AsyncPipeReaderInput` + `AsyncPipeWriterOutput`, multi-message wire framing, sliding-window buffer, producer-consumer synchronization) is tracked separately in [`BINARY_ASYNCPIPE_TODO.md`](BINARY_ASYNCPIPE_TODO.md).

## Priority legend
- **P0** blocker · **P1** important · **P2** nice-to-have · **P3** idea

> **Archived entries**: see `BINARY_TODO_2026_04.md` and `BINARY_TODO_2026_05.md` (year-month bucket archives per LLMP-DEC retention policy).
> Archive files are not auto-loaded — read on demand if relevant context is suspected (regression hint, supersession reference, ID lookup for archived entry).

## ACCORE-BIN-T-V3P9: ~~Prefix-tier VarUInt + String marker unification + FixStrCount constant~~
**Status:** Closed (2026-05-26) · **Priority:** ~~P1~~ · **Type:** ~~Performance + Wire format~~

~~Three coordinated changes (single wire-format-breaking iteration, no FormatVersion bump per user
decision — old payloads not deserializable with the new code):~~

### Resolution (2026-05-26)

**1. Prefix-tier VarUInt encoding (replaces LEB128).** `WriteVarUIntMultiByteUnsafe` and `ReadVarUInt`
in `AcBinarySerializer.BinarySerializationContext` + `AcBinaryDeserializer.BinaryDeserializationContext`
+ `BufferWriterBinaryOutput` rewritten from LEB128 continuation-loop to UTF-8-style prefix-tier
encoding. First-byte prefix bits (`0xxxxxxx` / `10xxxxxx` / `110xxxxx` / `1110xxxx` / `1111xxxx`)
determine total size in O(1); remaining bytes are fixed-size little-endian. Wire-size identical to
LEB128 across all 5 tiers (7/14/21/28/32 bit) — auto-inc IDs pay the same byte count. Decode wins:
1 if-ladder + 1 fix-sized load on 3+ byte tiers instead of N×ReadByte + N×shift + N×continuation-check.
Encoder uses `BitOperations.Log2` + single `Unsafe.WriteUnaligned<uint>` store for the 2/3/4-byte tiers
(branch count reduced from 4 to 1). See `BINARY_FORMAT.md` Variable-Length Encoding section for the
tier table.

**2. String marker unification (`StringLen8/16/32` → single `String` marker).** The 3-marker
magnitude-tier dispatch (167 `StringLen8` + 168 `StringLen16` + 169 `StringLen32`) was redundant
once VarUInt itself became prefix-tier. Collapsed into a single `String` marker at 167 with VarUInt
charLength + slot. Slots 168, 169 freed for future marker allocation. Wire layout:
`[String:1] [VarUInt(charLength - FixStrCount)] [excess slot:1|2|4] [UTF-8 bytes]`. The slot-size
(1/2/4) is still derived from `charLength` via `GetUniversalStringExcessSlotSize`. `BinaryTypeCode.cs`,
`WriteStringWithDispatch`, `ReadUniversalLongString*`, `TryReadStringProperty`, `AcBinaryDeserializer`
reader registration / PopulateProperty switch / Skip path, and `AcBinarySourceGenerator.GenReader`
emit all updated. `StringAscii` alias deleted.

**3. `FixStrCount` constant.** New `public const int FixStrCount = FixStrMaxLength + 1` in
`BinaryTypeCode.cs`. Single source of truth for the FixStr slot count (= 32), also the wire-format
offset for the long-form `String` marker's VarUInt charLength (`wireLen = charLength - FixStrCount`).
If the FixStr range ever expands (e.g. 32 → 64 slots), this constant updates the offset everywhere
consistently — both the writer (`charLength - FixStrCount`) and reader (`wireLen + FixStrCount`).

**4. `EnsureAvailable` micro-optimization.** Combined `if (!TInput.IsTrustedSingleSegment && ...)`
short-circuit (was two separate ifs). Single branch, better Tier-0 / cold-path / AOT codegen.
Hot-path JIT (Tier-1) was already CSE-equivalent.

**5. `ReadVarUInt` incremental-byte slow path.** The 4/5-byte tier slow path now accepts `b0/b1/b2`
as parameters (no re-read of bytes the inline fast path already consumed). Inline fast path covers
1/2/3-byte tiers (cross-segment safe via `ReadByte()` → `EnsureAvailable(1)` JIT-eliminate on
ArrayBinaryInput).

**6. `WriteStringWithDispatch` if-cascade reorganization.** Replaced 4 `isFixStr ? ... : ...`
ternaries with a single explicit `if (isFixStr) { ... } else { ... }` block. Each branch holds
its own constants (no cross-branch ternary CSE pressure on the JIT). Tier-0 / cold-path codegen
slightly cleaner; Tier-1 hot-path was already CSE-equivalent (bench-confirmed neutral, but smaller
IL — `?:` in C# is 4 separate `brfalse/brtrue` instructions, not 1).

### Acceptance criteria met

- ✅ Full solution build (`AyCode.Core.sln`) — 0 errors.
- ✅ Benchmark snapshot (Latin1Short, 2026-05-26 14:20): AcBinary vs MemoryPack Ser median **−3.1%**
  (was −1.8% before), Ser geo **−3.2%** (was −2.5%). Per-cell improvements 0.3..2.2% on Ser,
  2.2..3.7% on Deser. Bench file: `Test_Benchmark_Results/Benchmark/Console.FullBenchmark_Release_2026-05-26_14-20-29.LLM`.
- ✅ Doc-sync: `BINARY_FORMAT.md` Variable-Length Encoding + Strings + FixStr sections updated to
  reflect the new wire layout. Reserved-range table added (103..134, 168..169, 170..175 buckets).

### Wire-format breaking note

This change set is **wire-format breaking** — payloads serialized by the pre-V3P9 code (LEB128 +
StringLen8/16/32) are NOT deserializable by the new code. Per user decision the `FormatVersion`
header byte was NOT bumped (silent breaking; AcBinary is consumer-private, no cross-deployment
compatibility surface). If future versioned compat is desired, a `FormatVersion 1 → 2` bump would
be the conventional approach.

### SignalR-protocol-layer follow-on

A separate closed entry [`ACCORE-SBP-T-W7K4`](../../../AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/SIGNALR_BINARY_PROTOCOL_TODO.md#accore-sbp-t-w7k4-prefix-tier-varuint-protocol-side-parity--t--object-regression-cluster)
documents the SignalR-side parity fixes the V3P9 wire-format change exposed:
`AcBinaryHubProtocol.ReadVarUInt(ref SequenceReader<byte>)` standalone LEB128 → prefix-tier port,
plus a latent `T = object` generic-inference regression cluster surfaced by the V3P9 test breaks
(test count: 320 → 10, within baseline flaky variance).

## ACCORE-BIN-T-N4P8: ~~SGen reference-property null-check parity across all four emit branches~~
**Status:** Closed (2026-05-23) · **Priority:** ~~P1~~ · **Type:** ~~Bug fix~~

~~`AcBinarySourceGenerator.GenWriter.cs` had four reference-property emit branches with divergent null handling. `EmitDirectObjectWrite` (line ~828), `EmitDirectCollectionWrite` (line ~877), and the `IsObjectDeclaredType` branch (line ~295) unconditionally emitted `if ({a} == null) PropertySkip; else …`, with explicit comments acknowledging that runtime null is possible regardless of the nullable annotation. The fourth branch — the fallback `WriteObjectGenerated` call (line ~309) — was inconsistent: it null-checked only when `p.IsNullable=true`, leaving the `else` branch (non-nullable signature) without a null guard. Real-world impact: cross-assembly types without a parent-side generated writer (e.g. NopCommerce `Customer` declared as `Customer Customer { get; set; }` in `FruitBank.Common.Dtos.OrderDto`) fall onto the fallback branch with `IsNullable=false`; if the runtime value is null (EF lazy-load gap, projection omission, detached navigation property), the generated code emits `AcBinarySerializer.WriteObjectGenerated(obj.Customer, …)` which then `Unsafe.As<T>(value)` yields a null reference and the first property access NREs. The bug surfaced in SignalR chunked streaming: an `OrderItemPallet → OrderItemDto → OrderDto → Customer` graph with a null `Customer` produced a serialize-time `NullReferenceException` in `Customer_GeneratedWriter.WriteProperties:23` (the first `obj.Active` read).~~

### Resolution (2026-05-23)

Single-line semantic change in `AcBinarySourceGenerator.GenWriter.cs`: the `else if (p.IsNullable)` + `else` branches in the `PropertyTypeKind.Complex` fallback path (line ~309-315) collapsed into one always-null-check branch:

```csharp
else
{
    // Reference type properties can always be null at runtime regardless of nullable annotation
    // (mirrors EmitDirectObjectWrite / EmitDirectCollectionWrite — runtime can violate the
    // nullable-disabled contract via EF lazy-load, projection gaps, detached navigation properties).
    sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
    sb.AppendLine($"{i}else AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context);");
}
```

All four reference-property emit branches now share the same defensive null-check pattern.

**Reader-side compatibility:** no changes needed. `EmitReadProperty` (GenReader.cs ~137) already wraps every markered property in `if (tc != BinaryTypeCode.PropertySkip) { … }`, so the new marker is uniformly handled — the property stays at its default value (null for reference types).

**Acceptance criteria met:**
- ✅ Production stack-trace (`Customer_GeneratedWriter:23` NRE in chunked send via `OrderItemPallet → OrderItemDto → OrderDto → Customer`) no longer reproducible.
- ✅ All four SGen reference-property emit branches now consistently null-check (parity verified by code inspection).
- ✅ Regression tests added in `AcBinarySerializerSGenNullComplexPropertyTests.cs`: 8 test cases (2 methods × 4 DataRows = SGen/Reflection × FastMode/Default), covering both null-property and non-null-property paths with marker-byte verification (asserts `BinaryTypeCode.PropertySkip` presence in the wire-output for the null slot — confirms the fix took the PropertySkip path, not an unrelated null-safe code path).
- 🟢 Discovery vector: the companion CHUNK_ABORT fault-isolation feature ([`ACCORE-SBP-T-Q3M7`](../../../AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/SIGNALR_BINARY_PROTOCOL_TODO.md#accore-sbp-t-q3m7)) was developed in response to the production NRE, which in turn exposed this unprotected emit branch. With this fix, CHUNK_ABORT becomes a genuine safety-net for unrelated failures rather than a workaround for a self-inflicted serializer bug.

### Follow-up (2026-05-23): Collection-ag parity fix

After the Complex-branch fix landed, a second production CHUNK_ABORT surfaced from the same call chain (`OrderItemPallet → OrderItemDto → OrderDto → ...`), but the stack-trace now pointed to a *different* property — `OrderDto.OrderNotes` (a `List<Nop.Core.Domain.Orders.OrderNote>` with cross-assembly element type, no `[AcBinarySerializable]`) — and the NRE happened inside a `lambda_method` (`Expression.Compile`-d runtime accessor) reached via `AcBinarySerializer.WriteValueGenerated`. Re-audit of `GenWriter.cs` revealed **the exact same parity bug, but on the `PropertyTypeKind.Collection` branch** (~line 321): the `else if (p.IsNullable) … else …` shape, where the `else` (non-nullable signature) emitted `WriteValueGenerated({a}, …)` without a null guard. Production `OrderNotes` was non-nullable in signature, null at runtime (NopCommerce convention: no initializer).

**Audit completed** across all four `PropertyTypeKind` emit branches in `GenWriter.cs`:

- ✅ `PropertyTypeKind.Complex` (line ~282) — fixed in the original N4P8 above.
- ✅ `PropertyTypeKind.Collection` (line ~321) — fixed now: `else if (p.IsNullable) … else …` collapsed into one always-null-check branch; element-side fallback (`WriteValueGenerated`) now sees only non-null collections.
- ✅ `PropertyTypeKind.Dictionary` (line ~333) — unconditionally routes through `EmitDirectDictionaryWrite` (line ~1031), which already null-checks at line ~1037 with the same explicit comment.
- ✅ `default` (`EmitSkip`) — value-type primitives, no null possible.

The companion `WriteValueGenerated` bridge in `AcBinarySerializer.cs` shares the non-null contract with `WriteObjectGenerated`; both assume the caller emitted a null-check. The four-branch parity is now complete and verified by code inspection.

**Regression tests extended** in `AcBinarySerializerSGenNullComplexPropertyTests.cs` with four additional methods covering both new PropertyTypeKinds:

- `Serialize_SGenCollectionPropertyNull_DoesNotThrow_AndRoundTripsAsNull` + `…NonNull_RoundTripsCorrectly` — Collection-ag regression (the actual bug-fix coverage).
- `Serialize_SGenDictionaryPropertyNull_DoesNotThrow_AndRoundTripsAsNull` + `…NonNull_RoundTripsCorrectly` — Dictionary-ag pin (already null-safe at line ~1037, but previously had no regression test to lock that behaviour in against future refactors).

Each method uses the same 4-row `(useSgen, fastMode)` parameterization plus a marker-byte verification (`Assert.IsTrue(Array.IndexOf(bytes, BinaryTypeCode.PropertySkip) >= 0, …)`) on the null tests, confirming the writer took the PropertySkip path rather than an unrelated null-safe branch. **Total coverage: 24 test cases** (6 methods × 4 rows = Complex/Collection/Dictionary × null/non-null × SGen/Reflection × FastMode/Default). New models `SGenNullCollectionParent` and `SGenNullDictionaryParent` in `SGenNullComplexPropertyModels.cs` mirror the production shapes (non-nullable `List<NonGeneratedComplexCustomer>` / `Dictionary<string, NonGeneratedComplexCustomer>`).

## ACCORE-BIN-T-P6M4: Universal hotpath optimization guardrails + follow-up backlog
**Priority:** P1 · **Type:** Performance

AcBinary is a universal serializer. Hotpath work must avoid benchmark-only overfitting.

For each performance TODO, validate on representative workload mixes (ASCII-heavy, mixed Latin, multi-byte UTF-8; small/medium/large/deep payloads) and evaluate throughput + latency + allocation + wire-size together.

**Follow-up backlog (short):**
- Split oversized hot methods into inline-friendly dispatcher + cold helpers (writer/reader/populate).
- Add direct fast branches for the most frequent markers before generic table-dispatch.
- Reduce repeated `EnsureAvailable` checks by grouping fixed-width reads under one bounds check.
- Extend VarUInt fast-path coverage for common 3-byte cases on metadata/index/cache-id routes.
- Reorder populate/property-loop branches by runtime frequency (`PropertySkip`/`Null`/primitive fast-setters first).
- Minimize pool/clear overhead by avoiding unnecessary aggressive array clearing in hot lifecycle paths.
- Add early scan-pass short-circuit when options guarantee no ref/intern benefit.

## ACCORE-BIN-T-K9M3: Hoist wire codec primitives to context instance methods (ser + deser, feature-aware SGen emit)
**Priority:** P2 · **Type:** Refactor + Performance · **Related:** `ACCORE-BIN-T-P6M4` (hotpath guardrails), `BINARY_ISSUES.md#accore-bin-i-t7k3` (polymorph compile-time guard), `BINARY_SGEN_OPTIMIZATION.md` (per-property emit-condensation companion brainstorming)

### Motivation

Wire codec logic is currently triplicated:

1. **SGen-emit** inlines marker decode/encode at every property emit site (`StringInternFirstSmall`, `Object`/`ObjectRefFirst`/`Null`/`ObjectRef`/FixObj-slot dispatch, etc.).
2. **Runtime `TypeReaderTable`** dispatches via `static (ctx, _) => ReadXxx(ctx)` lambdas to per-marker `static` helpers in `AcBinaryDeserializer`.
3. **Cross-type populate** (`PopulateProperty` fallback) repeats the same per-marker switch.

Result: bug-fix risk (three copies drift), ad-hoc divergence (the polymorph `ObjectWithTypeName` emit was missing on the SGen side for months — ACCORE-BIN-I-T7K3), larger generated assemblies, longer JIT time. A single instance method on the context is the natural single-source-of-truth for each wire primitive.

### Pilot landed

`ReadAndRegisterInternedStringSmall` / `Medium` moved from `static` helpers on `AcBinaryDeserializer` to `internal` instance methods on `BinaryDeserializationContext`. All three call paths (TypeReaderTable lambdas, cross-type `PopulateProperty` switch, SGen-emit `EmitReadProp` case-body) now call `context.ReadAndRegister...()`. Generated case-body shrank from 12 lines to 3 per case — no perf regression (`[AggressiveInlining]` keeps the JIT/AOT inline footprint identical).

### Scope — both ser and deser

#### Phase A — Decode primitives (deser context)

- `ReadStringSmall` / `Medium` / `Big` (H2Q6 non-ASCII tiers).
- `ReadPlainStringAscii` (long ASCII tier).
- `ReadObject` family — careful: this branches on `targetType` and on the writer's runtime polymorphic slot table, both of which are call-site-context-specific. May not be a clean hoist; see "Caveat" below.

#### Phase B — Encode primitives (ser context)

- `WriteStringWithDispatch`, `WriteStringInternFirstWithDispatch` — already partly on the context, audit completeness.
- Marker-write helpers (`WriteObjectFullMarker*`) — already on the context post-T7K3.
- Audit: scan ser-side SGen-emit for any inline encode duplication that should move to the context.

#### Phase C — Feature-conditional SGen-emit

`EmitReadProp` (and the symmetric emit paths) may consult per-type `Enable*Feature` flags to **omit** case-branches for disabled features — but ONLY when the writer-side stripping is symmetric (the writer provably doesn't emit those markers either). Asymmetric stripping introduces a wire-misalignment bug; see `RefAwareEmitPredicate` in `AcBinarySourceGenerator.Models.cs` for the symmetric child-fact pattern.

| Disabled feature | Cases to skip in SGen reader emit | Writer-side symmetric? |
|---|---|---|
| `EnableInternStringFeature = false` | `StringInterned`, `StringInternFirstSmall`, `StringInternFirstMedium` | Yes — writer's scan-pass skips string-property iteration; `StringInternEligible` forced false for this type |
| `EnableMetadataFeature = false` | `ObjectWithMetadata`, `ObjectWithMetadataRefFirst` | Yes — writer's `WriteObject` skips the metadata marker branch when the type's `EnableMetadataFeature=false` |
| `EnablePolymorphDetectFeature = false` | (no extra emit — ACBIN002 compile-error already guards `object` property declarations) | Yes — guarded at compile time |

**NOT applicable here**: `EnableRefHandlingFeature = false`. The runtime writer emits `ObjectRef` / `ObjectRefFirst` markers based on child-type metadata (`UseTypeReferenceHandling(childMetadata)`), NOT the parent type's per-type flag. The parent-flag governs ONLY the parent's self-tracking scan-emit. Reader-side `ObjectRef*` case-emit on child properties of this type MUST stay present — gated on the child compile-time fact (`ChildNeedsRefScan` / `ElementNeedsRefScan` / `DictValueNeedsRefScan`) via `RefAwareEmitPredicate`.

After Phase C (revised scope): leaner generated code per opt-out type for the symmetric flags above; wire-misuse on those (e.g. mixed writer/reader feature configurations) surfaces as **explicit fail-fast** in the `default` switch arm — same philosophy as ACBIN002.

### Perf guardrails (NON-NEGOTIABLE)

The hoisting MUST NOT regress SGen hot-path performance. The pilot iteration was a net positive (less IL → faster cold-start JIT, smaller native code, identical inline body); this property has to hold for every subsequent hoist.

Rules of thumb:

- Every hoisted method MUST have `[MethodImpl(MethodImplOptions.AggressiveInlining)]`.
- Body must stay small (≤ ~30 IL instructions after compile) so the JIT/AOT actually inlines — verify via `dotnet jit-dasm` spot-check on representative callers.
- Single-purpose; no `if`-branches across distinct call-site contexts (those stay inline at the call site where the context-specific constants are visible).
- Benchmark verification before/after each hoist (`Console.FullBenchmark`).

### JIT / NativeAOT outlook

Modern .NET JIT (≥7) and NativeAOT both honour `AggressiveInlining` for small bodies → the hoisted methods inline back into the caller at compile time → **identical native code** to the previous inline-emit. The IL is smaller (less SGen-emit per file), which gives:

- Faster cold-start JIT (less IL to translate on first call per type).
- Smaller assemblies on disk (NativeAOT publish size shrinks).
- Smaller i-cache footprint per active hot type (since SGen-emit no longer balloons per property).

The generic `<TInput>` specialization remains: each `ArrayBinaryInput` / `SequenceBinaryInput` / `AsyncPipeReaderInput` still gets its own native body (`TInput.IsTrustedSingleSegment` constant-folds per specialization), so no overhead vs. the current state.

NativeAOT additionally prefers small, single-purpose methods: register-allocation (LSRA) is more effective, peephole / loop-unroll / dead-code passes run faster per method, and the published native image is denser. The previous "giant SGen-emitted `ReadProperties` body" pattern was actively hostile to AOT in this respect.

### Caveat — where NOT to hoist

Not every inline emit is a candidate. If the inline body carries compile-time constants (`typeof(TFoo)` literal, direct `Instance.ReadProperties` call on a concrete generated reader class, `nameof(prop)` constant), hoisting forces those into runtime parameters: constant-folding opportunity lost AND a direct call may become virtual via interface dispatch. The `Complex` property dispatch (`Object` → new T + `ReadProperties` direct call) is in this category and should stay inline at the SGen emit site.

Decision per primitive: can it be expressed as a context method that takes only wire-bytes-relevant inputs (no `targetType` literal, no per-property setter callback)? If yes → hoist. If no → keep inline.

### Acceptance

- Phase A: all shared decode primitives reachable as instance methods on `BinaryDeserializationContext`. TypeReaderTable + cross-type populate + SGen-emit all call them. SGen-generated case-body for each affected marker is ≤ 3 lines.
- Phase B: ser-side audit complete; any encode duplication closed by hoist or explicit "keep inline — see caveat" note in the SGen comment.
- Phase C: SGen-emit reader honours `Enable*Feature` flags. Verified by spot-checking generated `*.g.cs` files: an `EnableInternStringFeature=false` type's reader does NOT contain `StringInternFirstSmall` / `Medium` / `StringInterned` cases.
- Per-phase benchmark run (`Console.FullBenchmark`) confirms no hot-path regression (within noise floor).

## ACCORE-BIN-T-Q2N7: Re-evaluate DiscountProductMapping SGen exclusion
**Priority:** P3 · **Type:** Investigation · **Related:** `BINARY_ISSUES.md#accore-bin-i-f1w8`

Investigate whether the `new int Id` shadowing pattern can be handled by SGen (via base-class introspection, property-setter lookup on the base) to eliminate the runtime compiled-expression fallback for this entity class.

## ACCORE-BIN-T-W9F1: Generate `BinarySerializeTypeMetadata` / `BinaryDeserializeTypeMetadata` at compile time
**Priority:** P1 · **Type:** Performance · **Related:** `BINARY_ISSUES.md#accore-bin-i-n6q3`

Eliminate the dominant first-call cost (reflection + `Expression.Compile` in metadata ctor) for SGen types by emitting pre-built metadata from the source generator.

**Design outline:**

- `TypeMetadataBase` / `BinarySerializeTypeMetadata` / `BinaryDeserializeTypeMetadata` get a second constructor that accepts pre-computed values (hashes, `MinWriteSize`, `ComplexPropertyCount`, flags, `IsIId`, `IdAccessorType`, etc.). No reflection executes in this ctor.
- Source generator keeps its existing `s_typeNameHash` / `s_propertyHashes` static fields (hot-path access stays static, zero indirection) and passes the **same references** to the metadata — single source of truth, no duplicate computation.
- `ModuleInit` registers both the writer/reader **and** the pre-built metadata into a `GeneratedMetadataRegistry`. `GetWrapperSlow` consults this registry first, falling back to the reflection-based `MetadataFactory` for runtime-only types.
- Lazy `RuntimeInit()` pattern for `Expression.Compile` property accessors:
  - `TypeMetadataBase` gets `volatile bool _runtimeInitialized` + `internal void RuntimeInit()` (idempotent, no lock needed).
  - `GetWrapperSlow` calls `metadata.RuntimeInit()` only when `wrapper.GeneratedWriter == null || !Options.UseGeneratedCode` — SGen types skip it entirely (they never touch runtime accessors on their own metadata; non-SGen child types have their own metadata and run the factory path normally).
  - Hybrid mode stays correct: an SGen type on the SGen path never uses its own property accessors; a non-SGen child type's metadata runs the reflection ctor as today.
- `volatile` guards the flag; multiple contexts may race into `RuntimeInit`, second run is a no-op.

**Thread safety:** `GlobalMetadataCache` is `ConcurrentDictionary`; generated metadata is registered once at `ModuleInit`; wrapper construction is per-context and unchanged.

**Acceptance:**
- Cold benchmark: first `Serialize<T>` of a fresh SGen type shows no reflection / `Expression.Compile` on the call stack.
- Runtime fallback (`UseGeneratedCode=false`) still produces identical wire output and uses the full metadata accessors.
- Deserialize side has parity (same approach for `BinaryDeserializeTypeMetadata`).
- Existing tests pass; wire format unchanged.

## ACCORE-BIN-T-T5J8: JIT Tier 1 warmup for generated hot methods
**Priority:** P2 · **Type:** Performance · **Related:** `BINARY_ISSUES.md#accore-bin-i-n6q3`

After ACCORE-BIN-T-W9F1 lands, JIT of generated `WriteProperties` / `ScanObject` / `ScanForDuplicates` becomes the dominant residual first-call cost for SGen types. Options to evaluate (benchmark before committing):

- **`[MethodImpl(MethodImplOptions.AggressiveOptimization)]`** on the generated hot methods — skips Tier 0, compiles directly at Tier 1. Simple generator change. Trade-off: larger one-time JIT cost in exchange for eliminating the Tier 0→1 recompile step.
- **Background prewarm from `ModuleInit`**: `Task.Run(() => RuntimeHelpers.PrepareMethod(handle))` for each registered writer/reader method. Parallelizes JIT with app startup. Keep it opt-in (option flag) to avoid surprising consumers with extra startup threads.
- **ReadyToRun (R2R)** in consuming projects' publish config — pre-compiles IL to native at publish time. External to SGen, complementary. Document as a recommended publish setting.
- **Code chunking** (split generated methods exceeding a property threshold into sub-methods, e.g. `WriteProperties_Part1` / `_Part2`) — **measure first**. Only beneficial for unusually large types (20+ properties / nested collections). Call overhead can offset gains; JIT inliner may already handle reasonably-sized methods well.
- **`try` / `finally` audit on hot path** — On **.NET 9 (project's minimum target)**, JIT silently refuses to inline any method containing an EH region (`AggressiveInlining` is ignored). [.NET 10 partially lifts this for same-module try-finally — see [`dotnet/runtime#112998`](https://github.com/dotnet/runtime/pull/112998), merged 2025-03-20 — but `catch`, cross-module, and P/Invoke-stub cases stay blocked. Until project's minimum runtime moves to .NET 10, treat EH as an absolute inlining barrier; even after the upgrade, several sub-cases keep the rule.] Audit scope:
  - **Hand-written bridges**: `WriteValueGenerated` / `WriteObjectGenerated` / `WriteStringGenerated` / `ScanValueGenerated` and any helper called from generated `WriteProperties` for accidental `try/finally` / `using` blocks.
  - **SGen output template** (`AcBinarySourceGenerator.cs`): generated `WriteProperties` / `ScanObject` / `ScanForDuplicates` / `ReadObject` / `ReadProperties` MUST stay straight-line. Future feature additions ([CustomSerializer] / [CustomDeserializer] hooks, `OnSerializing` / `OnDeserialized` callbacks, validation attributes, rented-buffer `using` blocks) are tempting candidates for `try/catch/finally` — emit them in separate cold helpers, never inline into the generated hot method. A single accidental `try` block in `WriteProperties` makes the whole generated method non-inlinable, killing the SGen Root Fast Path benefit.
  - Resource cleanup (Pool/ArrayPool/Dispose) belongs in `Serialize<T>` entry-frame only, not in per-property helpers or generated hot methods. See `BINARY_IMPLEMENTATION.md` Rule #3 (Inlining barriers) and `BINARY_SGEN.md` (SGen Output Constraints).
- **`stackalloc` size discipline on hot path** — On **.NET 9**, methods containing `localloc` (any C# `stackalloc`) historically blocked inlining. Modern .NET allows inlining only for **fixed-size `stackalloc` ≤ 32 bytes outside loops** (see [`dotnet/runtime#7113`](https://github.com/dotnet/runtime/issues/7113)) — anything larger or loop-nested still blocks. Our typical scratch-buffer patterns (UTF-8 encoding scratch, ArrayPool fallbacks) sit far above 32 bytes (256+), so any helper containing such a `stackalloc` is non-inlinable. Combined with `try/finally` for `ArrayPool.Return` cleanup, the method is **doubly non-inlinable** on .NET 9. Plan accordingly: keep `stackalloc`-using helpers as deliberate cold call-frames, not as `AggressiveInlining` candidates.
- **Native AOT** — out of scope for this TODO; separate architectural decision with deployment-model implications.

**Acceptance:**
- Benchmark a realistic entity graph (≥ 3 referenced child types) and show first-call time within ~10% of steady-state after ACCORE-BIN-T-W9F1 + chosen mitigation(s).
- Document which combination is recommended for SignalR hot-path workloads vs. batch serialization.

## ACCORE-BIN-T-Z3K8: Replace `IId<T>` interface dependency with convention/attribute-based Id detection
**Priority:** P1 · **Type:** Refactor

The binary serializer currently detects Id-tracking properties via the `IId<T>` interface (`AyCode.Interfaces`). This couples the serializer to a framework-specific abstraction and forces consumer types to implement the interface for tracking participation. Move to a POCO-friendly detection scheme:

- **`IdDetectionMode.Convention`** (default) — convention-based; any property named `Id` is treated as the tracking key. Zero-friction onboarding.
- **`IdDetectionMode.Attribute`** — explicit; only properties marked with a serializer-native `[Id]` (or similar) attribute are tracked.
- **`[IgnoreId]`** attribute — escape hatch in `Convention` mode to exclude an Id-named property from tracking when the developer wants explicit opt-out.

**Implicit contract for `Convention` mode:** within a single class, the `Id` property must be type-level unique. Whether it semantically represents a primary key or a sequence number is irrelevant — the tracker keys by `(Type, Id)`, so per-type uniqueness is the only requirement. Violating this invariant typically signals a domain-modelling problem, not a serializer bug. Design rationale discussed in conversation 2026-04-27.

**Acceptance:**
- Binary serializer no longer references `IId<T>` in any execution path (no interface checks, no `where T : IId<TKey>` constraints in the serializer surface).
- Wire format unchanged.
- Existing consumers using `IId<T>`-implementing types still work transparently in `Convention` mode (their `Id` property is detected via convention).
- New consumers can use plain POCOs with no `AyCode.Interfaces` dependency.
- `IdDetectionMode` exposed on `AcBinaryOptions` (or successor options class post-rebrand).
- Default mode = `Convention`.

## ACCORE-BIN-T-N7V1: Replace `[JsonIgnore]` dependency with serializer-native ignore attribute
**Priority:** P2 · **Type:** Refactor

Property exclusion from binary serialization currently relies on `[JsonIgnore]` (Newtonsoft.Json). This couples the binary serializer to a third-party JSON library's attribute and is conceptually wrong — a binary serializer should not consult a JSON-specific marker for its exclusion semantics.

Define a serializer-native ignore attribute (working name `[BinaryIgnore]`; final name TBD pending broader rebrand). For backward compatibility during transition, also continue recognizing `[JsonIgnore]` with a deprecation note.

**Possible cross-cutting consideration:** if `Toon` and other future serializers also need property-exclusion, a single shared attribute (e.g., `[SerializerIgnore]` in a common abstractions package) may be cleaner than per-serializer attributes. Decide before naming finalizes — this may belong in `XCUT_TODO.md` rather than purely BINARY scope.

**Acceptance:**
- Native ignore attribute defined in the binary serializer's namespace (or shared abstractions package, pending the cross-cutting decision above).
- Both native attribute and `[JsonIgnore]` recognized during a transitional period; native attribute takes precedence on conflict.
- `[JsonIgnore]` recognition flagged for removal in a future major version (track in a follow-up cleanup TODO once consumer projects have migrated).
- No new code dependency on Newtonsoft.Json for property-exclusion logic.

## ACCORE-BIN-T-Y6R2: Implement projection serialization phase 1 (runtime path)
**Priority:** P1 · **Type:** Feature · **Related:** `../adr/0001-binary-projection-serialization.md` (canonical)

Implement the phase 1 runtime path of source→target projection serialization per ADR 0001. See the ADR for full context, decision rationale, alternatives, consequences, and acceptance criteria.

**Sibling rebrand-prep TODOs:** `ACCORE-BIN-T-Z3K8` (IId migration), `ACCORE-BIN-T-N7V1` (JsonIgnore replacement).

## ACCORE-BIN-T-K3W7: Rename `BufferWriterChunkSize` to reflect actual semantics
**Priority:** P3 · **Type:** Refactor · **Breaking:** Yes (public option API) · **Streaming impact:** see [`BINARY_ASYNCPIPE_TODO.md`](BINARY_ASYNCPIPE_TODO.md) for the streaming-side companion considerations (chunk-on-wire vs internal-buffer semantics)

The property name `BufferWriterChunkSize` is misleading: across the three output paths it does NOT consistently represent a "chunk".

| Output path | What `BufferWriterChunkSize` actually controls | Wire-format chunk? |
|---|---|---|
| `ArrayBinaryOutput` (Byte[] API) | Initial buffer capacity of the internal `byte[]` | No |
| `BufferWriterBinaryOutput` (IBufferWriter overload) | Internal buffer size — how much data accumulates before `Advance()` + new `GetMemory()` on the underlying writer | No |
| `AsyncPipeWriterOutput` (streaming) | Both internal buffer **and** wire-format chunk frame size for chunked framing | **Yes** (only here) |
| Receive side (`AsyncPipeReaderInput`) | Initial receive buffer = `BufferWriterChunkSize × 2` | No (just sizing hint) |

Only the streaming `AsyncPipeWriterOutput` path has a wire-format "chunk" concept (chunked framing for length-prefixed segments). On the other 75% of paths the property name reads as if the serializer were segmenting the payload, which is not what happens.

**Possible directions** (decide before implementing):

1. **Single rename, semantic-neutral** — `BufferWriterChunkSize` → `BufferWriterBufferSize` or `BufferWriterPageSize`. Minimal API surface change, single-property semantics preserved. Downside: still slightly off for the streaming path where there IS chunked framing.
2. **Two-property split** — `InternalBufferSize` (universal: how much data accumulates before Advance/Grow) + `StreamingChunkSize` (only meaningful for `AsyncPipeWriterOutput`; separate knob, defaults to `InternalBufferSize`). Cleanest semantics, most ceremony, slightly more options to document.
3. **Single rename, streaming-honest** — Keep as `BufferWriterChunkSize` but document explicitly that on non-streaming paths the value is repurposed as buffer size. Cheapest change (docs only). Downside: doesn't fix the underlying confusion the field name causes.

Pick one before touching code. Option 2 is the most correct but adds API surface; Option 1 is the pragmatic middle.

**Affected callers / docs to update on rename:**
- `AcBinarySerializerOptions.cs` (definition)
- `AcBinarySerializer.cs` × 3 sites (`ArrayBinaryOutput` ctor, `BufferWriterBinaryOutput` ctor, `AsyncPipeWriterOutput` ctor)
- `AcBinaryDeserializer.cs` × 1 site (receive-side initial capacity derivation)
- `AsyncPipeReaderInput.cs` — XML doc cross-refs
- `BINARY_WRITERS.md`, `BINARY_TODO.md` (this entry), `BINARY_ISSUES.md` (line 151 — already lists `BufferWriterChunkSize` among the struct-mutation issue's affected setters)
- Consumer-side: `AyCode.Services/SignalRs/AcBinaryHubProtocol.cs` ctor mutates `_options.BufferWriterChunkSize = options.BufferSize;` — see `BINARY_ISSUES.md#accore-bin-i-...` (struct-mutation context). Coordinate the rename with the struct-mutation fix to avoid two cross-cutting churn waves on the same property.

**Acceptance:**
- Property renamed (or split) per the chosen direction; all internal references updated.
- XML docs reflect the actual semantics on each output path (initial capacity / advance threshold / chunk frame size — whichever applies).
- Consumer-side usage in `AcBinaryHubProtocol` updated; if Option 2 is chosen, the protocol uses `StreamingChunkSize` (the streaming knob), not the universal one.
- Wire format unchanged. Default values unchanged (65535 / equivalent).
- Migration note in CHANGELOG / release notes since this is a breaking change to `AcBinarySerializerOptions`.

## ACCORE-BIN-T-M4D2: Add `ReadOnlyMemory<byte>` / `Memory<byte>` deserialize overloads
**Priority:** P3 · **Type:** Feature

The public `AcBinaryDeserializer.Deserialize` surface accepts `byte[]` (with optional offset/length) and `ReadOnlySequence<byte>`, but **not** `ReadOnlyMemory<byte>` / `Memory<byte>`. Consumers that hold a `ReadOnlyMemory<byte>` (cached payloads, message-broker frames, in-memory pipe slices) must call `.ToArray()` to round-trip through `byte[]` — unnecessary copy + GC alloc.

**Implementation:**
- `Deserialize<T>(ReadOnlyMemory<byte> data, AcBinarySerializerOptions options)` and the non-generic `Type`-based variant.
- Body: `MemoryMarshal.TryGetArray(data, out var seg)` → array-backed path delegates to `Deserialize<T>(seg.Array!, seg.Offset, seg.Count, options)` (zero-copy). Non-array-backed fallback (rare — custom `MemoryManager<T>` with native memory) copies into a pooled `byte[]`.
- `Memory<byte>` overload trivially delegates to the `ReadOnlyMemory<byte>` one (`Memory<byte>` is implicitly convertible).
- No new input-strategy struct needed — reuses existing `ArrayBinaryInput`.

**Acceptance:**
- Both overloads compile and pass round-trip tests against `byte[]`-equivalent input.
- Array-backed path measurably zero-alloc (BenchmarkDotNet allocation diagnoser).
- Non-array-backed path documented as fallback (separate `using var pooled = MemoryPool<byte>.Shared.Rent(...)` style copy).
- API doc-strings cross-reference the existing `byte[]` and `ReadOnlySequence<byte>` overloads.

## ACCORE-BIN-T-S7X3: Add `ReadOnlySpan<byte>` deserialize overload
**Priority:** P2 · **Type:** Feature · **Related:** `ACCORE-BIN-T-M4D2`

The MemoryPack-style `Deserialize<T>(ReadOnlySpan<byte>)` API enables direct deserialization from stack-allocated buffers (`stackalloc byte[256]`), pinned native memory (`fixed` blocks), and `ReadOnlyMemory<byte>.Span` slices without round-tripping through a heap-allocated `byte[]`. The current AcBinary surface lacks this entry point.

**Design tension:** the existing `IBinaryInputBase.Initialize(out byte[] buffer, ...)` contract returns a `byte[]` — a `ReadOnlySpan<byte>` cannot be stored in a regular struct field, only in a `ref struct` field. Two implementation paths to evaluate:

1. **`ref struct SpanBinaryInput`** + interface bump to support `ref byte buffer` / `int length` fields. **Pure** zero-copy from any span. Cost: `BinaryDeserializationContext<TInput>` and `IBinaryInputBase` need a parallel ref-struct-friendly track (the existing pooled context cannot hold a `ref struct`). Major surgery on the deser core.
2. **`MemoryMarshal.CreateReadOnlySpanFromNullTerminated`-style hack** — accept `ReadOnlySpan<byte>`, use `Unsafe.AsRef`/`MemoryMarshal.GetReference` to obtain a `ref byte`, then **copy into a pooled `byte[]`** before deserialization. **Not** zero-copy, defeats the purpose. Reject.
3. **Pinned-buffer trampoline** — accept `ReadOnlySpan<byte>`, allocate a `Memory<byte>` view via a `MemoryManager<byte>`-like wrapper, delegate to `ReadOnlyMemory<byte>` overload. Awkward, allocations per call. Reject.

**Recommendation:** option (1) is the only correct path, but it's a substantial refactor — measure first whether real consumer demand justifies the surgery. The current `byte[]`-based pool-pattern outperforms MemoryPack on the dominant use-cases per existing benchmarks; this overload addresses an API-surface gap, not a perf gap.

**Acceptance:**
- `Deserialize<T>(ReadOnlySpan<byte> data, AcBinarySerializerOptions options)` compiles and round-trips against `byte[]`-equivalent input.
- Zero-alloc path verified for `stackalloc`-source spans (BenchmarkDotNet allocation diagnoser).
- `IBinaryInputBase` (or successor interface) refactor preserves backward compatibility for existing `ArrayBinaryInput` / `SequenceBinaryInput` / `AsyncPipeReaderInputAdapter` consumers.
- Doc-strings cross-reference the `byte[]` / `ReadOnlyMemory<byte>` (`ACCORE-BIN-T-M4D2`) / `ReadOnlySequence<byte>` overloads with use-case guidance.

## ACCORE-BIN-T-T8K3: Add `SerializeAsync(Stream, T)` async overloads with mode-driven output strategy
**Priority:** P1 · **Type:** Feature · **Related:** `ACCORE-BIN-T-N9G6` (Type-based coordination)

The mainstream serializer ecosystem (System.Text.Json, MessagePack, Newtonsoft.Json, MemoryPack) all expose `SerializeAsync(Stream, T)` as a primary entry point — async file I/O, network response body, log streaming. **AcBinary's public API surface MUST include this overload** regardless of what we do internally; consumers expect a `Stream` parameter and don't navigate `PipeWriter.Create(stream)` workarounds. Market-entry-blocking otherwise.

### Mode-driven output strategy — three lanes for three workload shapes

AcBinary already models the three output strategies in `BinaryProtocolMode` (`AyCode.Services/SignalRs/BinaryProtocolMode.cs`) for the SignalR side. The same three-lane shape applies to the public `SerializeAsync(Stream)` API. Promote the concept to AcBinary core scope (e.g. `AcBinaryOutputMode` in `AyCode.Core/Serializers/Binaries/`) and let the SignalR `BinaryProtocolMode` either alias it or migrate to it. Migration timing: **the existing `BinaryProtocolMode` keeps shipping until the new public API is stabilized**; both names live for one major version, then `BinaryProtocolMode` becomes a `using`-alias.

| Mode | Output strategy | Peak memory | Pipeline parallelism | Use when |
|---|---|---|---|---|
| **`Bytes`** (default) | `Serialize(T) → byte[]` + `stream.WriteAsync(bytes)` | Full payload in `byte[]` (pooled) | No | Typical payloads (<10 MB), throughput-focus |
| **`Segment`** | `BufferWriterBinaryOutput` → `PipeWriter`, single closing flush | PipeWriter pause-threshold-bounded (~64 KB Kestrel default) | No | Mid-size payloads, zero-copy desired |
| **`AsyncSegment`** | `SerializeChunked(PipeWriter)`, per-chunk async flush | Chunk-size-bounded (~8 KB at default `BufferWriterChunkSize`) | Yes (on parallel-capable PipeWriter — Kestrel / `Pipe`) | Very large payloads (>10 MB), memory-tight hosts, parallel-capable transport |

### Honest performance positioning vs. MemoryPack — three real axes

MemoryPack's `SerializeAsync(Stream)` is **pseudo-streaming** — serializes the entire payload into a pool-allocated linked-list buffer first (`ReusableLinkedArrayBufferWriter`), then writes the completed buffer to the stream in a single closing fence. Peak memory ≈ payload size; no pipeline parallelism. AcBinary's `Bytes` mode is **architecturally similar** (single pooled contiguous `byte[]` vs. MemoryPack's linked-list) — comparable peak-memory cost, often faster on the wire due to one contiguous `WriteAsync` call.

AcBinary's `AsyncSegment` mode is **architecturally different** in three real ways MemoryPack cannot match:

| Axis | `Bytes` mode (default) | `AsyncSegment` mode | MemoryPack `SerializeAsync` |
|---|---|---|---|
| **Heap allocation per call** | Pooled `byte[]` rent (peak ≈ payload size) | **Truly zero** — `ArrayPool` + pooled context + `MemoryMarshal.TryGetArray` direct-buffer-write into the transport's own `byte[]` | Pool-allocated linked-list buffer per call (peak ≈ payload size) |
| **Peak managed memory** | ≈ payload size | ≈ chunk size (`BufferWriterChunkSize`, e.g. 4-8 KB) | ≈ payload size |
| **GC pressure** | Touches GC pool on every call | **Never touches GC** for the serialize itself | Touches GC pool on every call |
| **Pipeline parallelism** | No | Yes on parallel-capable PipeWriter (Kestrel transport, `new Pipe()`) | No |
| **GB-scale payload** | OOM risk on memory-tight hosts | Works | OOM risk |

The `AsyncSegment` zero-alloc claim is **literal**, not "almost zero": `AsyncPipeWriterOutput.AcquireChunk` calls `_pipeWriter.GetMemory(chunkSize)` and uses `MemoryMarshal.TryGetArray(memory, out segment)` to obtain the **transport's own internal `byte[]`** — the serializer writes directly into it. With `chunkSize` aligned to the transport's internal buffer (e.g. NamedPipe-server pipe-buffer-size), one chunk is one kernel-level transfer; no managed-side double-fragmentation.

### Throughput nuance — `AsyncSegment` cost on Stream-backed transports

`AsyncSegment` IS slightly slower than `Bytes` on `StreamPipeWriter`-backed transports (NamedPipe / FileStream / NetworkStream), but **not for the reason that initially seems obvious**:

- The cost is NOT "managed-side double-fragmentation on top of OS-level fragmentation" — that's not what happens. `MemoryMarshal.TryGetArray` zero-copy direct-buffer-writes mean the managed chunking is the **same** chunking the kernel does anyway, not redundant.
- The cost IS the per-chunk async-await round-trip (`SyncAwaitFlush(_lastFlush)` blocks until the kernel acknowledges the write), forced sequential by the `StreamPipeWriter._tailMemory` reset race (`ACCORE-BIN-I-...`). N async cycles vs 1 in `Bytes` mode.
- Empirically the gap is roughly **1.2-1.5x** on NamedPipe — not 2-5x. **The dominant cost on these transports is the transport itself** (Windows IRP / Linux FIFO syscall overhead), independent of the serializer mode.

When `AsyncSegment` wins outright:
- **GC-sensitive hot-paths** (server hubs, real-time game tick loops, mobile UI thread, embedded targets): zero-alloc + zero-GC-pressure beats a 1.2x throughput edge every time.
- **Memory-tight hosts** (mobile, WASM, container-trimmed, embedded): chunk-bounded peak memory is the only option.
- **GB-scale payloads**: `Bytes` OOMs; `AsyncSegment` works.
- **Kestrel transport / parallel-capable `Pipe`**: pipeline parallelism makes `AsyncSegment` **faster** than `Bytes` for medium-to-large payloads.

When `Bytes` wins outright:
- **Tipikus NuGet workload** (small-to-medium payload, throughput priority, GC-tolerant): one async cycle vs N is the simpler, faster path.
- **`MemoryStream`** (in-memory): one large `byte[]` copy decisively beats N managed chunks.

### Marketing claim — three-way honest comparison

> *"AcBinary offers a real choice. `Bytes` mode for typical throughput-priority workloads (matches MemoryPack's pseudo-streaming, often faster on the wire). `AsyncSegment` mode for the workloads MemoryPack cannot serve: zero-alloc serialize for GC-sensitive hot-paths, chunk-bounded peak memory for tight-budget hosts, GB-scale payloads, and pipeline parallelism on parallel-capable transports. You pick the mode; MemoryPack picks for you."*

This is honest — does not overclaim universal speed, does not hide the small `AsyncSegment` cost on Stream-backed transports, AND clearly surfaces the three differentiator axes (alloc / memory / parallelism) where AcBinary architecturally beats MemoryPack.

**Implementation outline:**

- New enum `AcBinaryOutputMode { Bytes = 0, Segment = 1, AsyncSegment = 2 }` in `AyCode.Core/Serializers/Binaries/`. Default `Bytes`.
- New mode field on `AcBinarySerializerOptions`: `AcBinaryOutputMode OutputMode { get; set; } = AcBinaryOutputMode.Bytes;`. (Note: subject to `ACCORE-BIN-I-L8N5` thread-safety treatment — defensive copy / immutable refactor coordination.)
- `public static ValueTask SerializeAsync<T>(T value, Stream stream, AcBinarySerializerOptions? options = null, bool leaveOpen = false, CancellationToken ct = default)`:
  - Switch on `options.OutputMode`:
    - `Bytes` → `var bytes = Serialize(value, options); await stream.WriteAsync(bytes, ct); ArrayPool.Return(bytes);`
    - `Segment` → `var pw = PipeWriter.Create(stream, new(leaveOpen: leaveOpen)); Serialize(value, pw, options); await pw.CompleteAsync();`
    - `AsyncSegment` → `var pw = PipeWriter.Create(stream, new(leaveOpen: leaveOpen)); SerializeChunked(value, pw, options); await pw.CompleteAsync();`
- `public static ValueTask SerializeAsync(object? value, Type type, Stream stream, ...)` — non-generic, same dispatch (coordinated with `ACCORE-BIN-T-N9G6`).
- `leaveOpen` parameter standard for stream-async serializers (System.Text.Json, MessagePack convention).
- The `Bytes` mode uses a pooled `byte[]` from `ArrayBinaryOutput` to keep alloc cost amortized.

**SignalR migration coordination:** the existing `BinaryProtocolMode` enum (in `AyCode.Services`) keeps shipping unchanged until the new public API is stabilized. After stabilization, `BinaryProtocolMode` becomes a deprecated alias of `AcBinaryOutputMode`, eventually removed in a major-bump. No SignalR-side churn during this TODO's implementation.

**Acceptance:**

- `SerializeAsync<T>` round-trips against `Deserialize<T>(byte[])` via `MemoryStream` in all three modes.
- Cancellation propagates correctly (`OperationCanceledException` on cancelled token mid-stream).
- **Throughput matrix benchmark**: 4 transports (`MemoryStream`, `FileStream`, `NamedPipeStream`, `NetworkStream`) × 3 modes × 3 payload sizes (small ~1 KB / medium ~100 KB / large ~10 MB). Results documented in `Test_Benchmark_Results/Benchmark/SerializeAsync_Stream_Modes.LLM` (or similar) and surfaced as a doc-string table for consumer guidance.
- **Memory-bounded benchmark**: 100 MB payload to `FileStream` in `AsyncSegment` mode → peak managed-heap delta ≤ 1 MB throughout. Same payload in `Bytes` mode → peak ~100 MB (expected, documented).
- API doc-string contains a "When to use which mode?" decision matrix; explicitly compares with MemoryPack's pseudo-streaming.
- `leaveOpen` parameter behaves per the System.Text.Json / MessagePack convention across all three modes.

## ACCORE-BIN-T-D7K4: Add `DeserializeAsync(Stream, T)` async overloads with mode-driven input strategy
**Priority:** P1 · **Type:** Feature · **Related:** [`ACCORE-BIN-T-T8K3`](#accore-bin-t-t8k3-add-serializeasyncstream-t-async-overloads-with-mode-driven-output-strategy) (companion write-side overload), `ACCORE-BIN-T-N9G6` (non-generic Type-based dispatch)

Companion to `T8K3` on the receive side. The mainstream serializer ecosystem (System.Text.Json, MessagePack, Newtonsoft.Json, MemoryPack) all expose `DeserializeAsync<T>(Stream)` — the symmetric counterpart of `SerializeAsync(Stream, T)`. **AcBinary's public API surface MUST include this overload** for parity; consumers expect a `Stream` parameter for receive paths (file load, HTTP response body, network stream) and don't navigate `PipeReader.Create(stream)` workarounds. Market-entry-blocking otherwise.

### Implementation: zero new `IBinaryInputBase` impl needed

The existing receive-side primitives cover the full strategy space via BCL `PipeReader.Create(stream)`:

| Mode | Input strategy | Peak memory | Pipeline parallelism | Use when |
|---|---|---|---|---|
| **`Bytes`** (default) | `await stream.CopyToAsync(MemoryStream)` → `Deserialize<T>(byte[])` (existing overload) | Full payload as `byte[]` (pooled) | No | Typical payloads (<10 MB), throughput-focus |
| **`Segment`** | `await PipeReader.Create(stream).ReadAsync()` → `Deserialize<T>(ReadOnlySequence<byte>)` (existing overload) | PipeReader pause-threshold-bounded (~64 KB) | No | Mid-size payloads, no full byte[] alloc desired |
| **`AsyncSegment`** | `AsyncPipeReaderInput` + `DrainFromAsync(PipeReader.Create(stream))` + `Deserialize<T>(input)` (existing overload) | Chunk-size-bounded (~8 KB) | Yes (producer drain Task in parallel with deser Task) | Very large payloads (>10 MB), memory-tight hosts |

The `AcBinaryOutputMode` enum (introduced by `T8K3`) is symmetric — it controls deser-input strategy as well. The same enum value picks the matching read path. **No new `IBinaryInputBase` implementation needed** — the trio of existing inputs (`ArrayBinaryInput`, `SequenceBinaryInput`, `AsyncPipeReaderInput`) already cover all three modes; the new overload is a thin shim that wraps the `Stream` and routes to the right existing overload.

### Public API shape

```csharp
public static ValueTask<T?> DeserializeAsync<T>(
    Stream stream,
    AcBinarySerializerOptions? options = null,
    bool leaveOpen = false,
    CancellationToken ct = default);

// Non-generic Type-based variant (coordinated with N9G6):
public static ValueTask<object?> DeserializeAsync(
    Stream stream,
    Type targetType,
    AcBinarySerializerOptions? options = null,
    bool leaveOpen = false,
    CancellationToken ct = default);
```

### Implementation outline (per mode)

```csharp
// Bytes mode (default — simplest path, sub-LOH-friendly fast path):
public static async ValueTask<T?> DeserializeAsync_Bytes<T>(Stream stream, ..., CancellationToken ct)
{
    var rented = ArrayPool<byte>.Shared.Rent((int)Math.Min(stream.CanSeek ? stream.Length : 4096, int.MaxValue));
    try
    {
        var totalRead = 0;
        int read;
        while ((read = await stream.ReadAsync(rented.AsMemory(totalRead), ct)) > 0)
        {
            totalRead += read;
            if (totalRead == rented.Length) { /* grow rented */ }
        }
        return Deserialize<T>(rented, 0, totalRead, options);
    }
    finally { ArrayPool<byte>.Shared.Return(rented); }
}

// Segment mode (PipeReader.Create wrapping, then drain to ReadOnlySequence):
public static async ValueTask<T?> DeserializeAsync_Segment<T>(Stream stream, ..., CancellationToken ct)
{
    var pipeReader = PipeReader.Create(stream, new(leaveOpen: leaveOpen));
    var result = await pipeReader.ReadAtLeastAsync(int.MaxValue, ct);   // drain whole stream
    var seq = result.Buffer;
    var obj = Deserialize<T>(seq, options);
    pipeReader.AdvanceTo(seq.End);
    await pipeReader.CompleteAsync();
    return obj;
}

// AsyncSegment mode (chunked streaming pipeline, parallel drain + deser):
public static async ValueTask<T?> DeserializeAsync_AsyncSegment<T>(Stream stream, ..., CancellationToken ct)
{
    using var input = new AsyncPipeReaderInput(options.BufferWriterChunkSize * 2, multiMessage: false);
    var pipeReader = PipeReader.Create(stream, new(leaveOpen: leaveOpen));
    var deserTask = Task.Run(() => Deserialize<T>(input, options), ct);
    await input.DrainFromAsync(pipeReader, ct);
    await pipeReader.CompleteAsync();
    return await deserTask;
}
```

### Honest performance positioning

Symmetric to T8K3's analysis:

- **`Bytes` mode**: simplest, single contiguous `byte[]` (pooled) → `Deserialize<T>(byte[])`. Comparable to MemoryPack's `DeserializeAsync` (which does similar full-buffer-then-deser). **Best for typical workloads.**
- **`Segment` mode**: zero-copy from PipeReader's natural `ReadOnlySequence<byte>` — no extra byte[] allocation. **Best for mid-size payloads where allocation matters but pipeline overlap doesn't.**
- **`AsyncSegment` mode**: producer-drain Task and consumer-deser Task in parallel via `AsyncPipeReaderInput`. Wall-clock = max(network-drain, deser-CPU) + small overlap-cost. **Best for large payloads + slow transports** (network, mobile, satellite — where transit dominates and overlap pays).

### Acceptance

- `DeserializeAsync<T>` round-trips against `SerializeAsync(Stream, T)` (`T8K3`) via `MemoryStream` in all three modes.
- Cancellation propagates correctly (`OperationCanceledException` on cancelled token mid-stream); partial-buffer state cleaned up; pooled byte[] returned even on cancellation.
- **Throughput matrix benchmark** (mirror of T8K3): 4 transports (`MemoryStream`, `FileStream`, `NamedPipeStream`, `NetworkStream`) × 3 modes × 3 payload sizes. Results documented in `Test_Benchmark_Results/Benchmark/DeserializeAsync_Stream_Modes.LLM`.
- **Memory-bounded benchmark**: 100 MB payload from `FileStream` in `AsyncSegment` mode → peak managed-heap delta ≤ 1 MB throughout. Same payload in `Bytes` mode → peak ~100 MB (expected, documented).
- API doc-string contains a "When to use which mode?" decision matrix; cross-references T8K3's symmetric write-side guidance.
- `leaveOpen` parameter behaves per the System.Text.Json / MessagePack convention across all three modes.

## ACCORE-BIN-T-R4P2: Expose low-level `ref Writer`-style API for custom formatters
**Priority:** P3 · **Type:** Feature

The MemoryPack-style `Serialize<T>(ref MemoryPackWriter writer, in T value)` low-level API enables:
- Custom formatters that compose write primitives without the full Serialize entry-point overhead.
- Nested-into-existing-stream scenarios where the caller already owns a writer-style cursor.
- Test harnesses that exercise specific wire-format paths in isolation.

Today's `BufferWriterBinaryOutput` standalone-mode partly fills this gap — exposing `WriteByte`, `WriteVarUInt`, `WriteStringUtf8`, etc. — but it is **not** a `ref struct`, **not** a documented low-level public API for external custom formatters, and the relationship with `BinarySerializationContext<TOutput>` is unclear from the consumer's perspective.

**Design tension** (decide before implementing):

1. **Promote `BufferWriterBinaryOutput` to documented public surface** — add doc, examples, supported usage patterns. Cheapest, but the standalone-mode is currently a side-feature, not a primary API; documenting it commits to its current shape.
2. **New `ref struct AcBinaryWriter` wrapper** around `BufferWriterBinaryOutput` (or a dedicated impl) — explicit "this is the low-level writer" signal. More API surface but clearer mental model. Aesthetic alignment with MemoryPack.
3. **Skip entirely** — the `IBufferWriter<byte>` overload is already lower-level than most consumers need; custom formatters can write to an `ArrayBufferWriter<byte>` and use `IBufferWriter`-style primitives. This is what `BufferWriterBinaryOutput` already does internally.

**Recommendation:** option 3 is honest — the existing `IBufferWriter<byte>` overload covers the use case, and adding a `ref struct AcBinaryWriter` is mostly aesthetic alignment with MemoryPack. Re-evaluate when there's a concrete custom-formatter request that the current API can't accommodate.

**Acceptance (if implemented):**
- `AcBinaryWriter ref struct` (or equivalent) compiles, supports the same write primitives as `BufferWriterBinaryOutput` standalone-mode.
- At least one example custom formatter ships in tests (e.g., a `Vector3` struct formatter).
- Doc-string clearly distinguishes when to use the low-level writer vs. the high-level `Serialize<T>` entry-point.

## ACCORE-BIN-T-U6Y8: Attribute-driven polymorphism via `[AcBinaryUnion]` + SGen (opt-in, AOT-friendly)
**Priority:** P1 (if AOT target required) / P2 (non-AOT only) · **Type:** Feature

**Related security work:** the runtime polymorphism path (AQN-based, the default this TODO keeps) is gated by `IAcTypeBinder` per the consolidated plan in [`BINARY_AQN_IMPLEMENTATION.md`](BINARY_AQN_IMPLEMENTATION.md). The `[AcBinaryUnion]` SGen path proposed here bypasses `Type.GetType` entirely (closed `switch (tag)` dispatch in code-gen), so it is AOT-friendly **and** inherently security-clean (no wire-supplied type-name resolution at all). The two TODOs are complementary, not overlapping — U6Y8 is the fast/AOT alternative; the binder secures the legacy AQN path.

**Design philosophy alignment:** AcBinary's market positioning is "JSON-style flexibility with MessagePack-class speed" — attributes are **opt-in optimization**, never required. The runtime polymorphism path (AQN-based, today's default) **stays the default** and continues to work for arbitrary unattributed types. This TODO adds a fast/AOT path **alongside** it, never replaces it.

AcBinary today handles polymorphism at runtime: the wire writes `ObjectWithTypeName(72)` + AQN string, and the deserializer calls `Type.GetType(aqn)` to resolve. This is **flexible** (no upfront declaration), but has three significant drawbacks for some consumers:

- **AOT-incompatible** — `Type.GetType(AQN)` requires reflection metadata that the Native AOT trimmer strips by default. The runtime polymorphism path **does not work at all** under Native AOT. Hard blocker for AOT-targeting consumers (Blazor WASM, MAUI mobile, container-trimmed deployments).
- **Slower** — AQN string parse + reflection lookup vs. a closed `switch (tag)` in code-gen.
- **Larger wire format** — full AQN string (often 100+ bytes) vs. a single-byte `tag`.

**Design — three coordinated pieces:**

### 1. New 5th bool parameter on `[AcBinarySerializable]`: `EnablePolymorphismFeature`

Mirrors the existing `EnableMetadataFeature` / `EnableIdTrackingFeature` / `EnableRefHandlingFeature` / `EnableInternStringFeature` pattern. Per-type opt-out / opt-in via attribute parameter.

```csharp
public AcBinarySerializableAttribute(
    bool enableMetadataFeature,
    bool enableIdTrackingFeature,
    bool enableRefHandlingFeature,
    bool enableInternStringFeature,
    bool enablePolymorphismFeature)   // ← ÚJ, default: true
```

Three behavior modes per type:
- `EnablePolymorphismFeature = false` → **disabled**. SGen never emits polymorphism dispatch for this type; runtime path also short-circuits — runtime type ≠ declared type is silently treated as declared (or throws, decision TBD). Use for hot-path closed types where polymorphism is impossible-by-design and the perf/AOT cost is unwanted.
- `EnablePolymorphismFeature = true` (default), no `[AcBinaryUnion]` → **runtime options control**. Behaves per `AcBinarySerializerOptions.PolymorphismMode` (Runtime/AQN today). This preserves the JSON-style flexibility for unattributed bases.
- `EnablePolymorphismFeature = true` + `[AcBinaryUnion(...)]` declared → **union-switch dispatch**. SGen emits a closed `switch (tag)` dispatch using the declared subtype set. Fast + AOT-friendly. Overrides the options-level default for this type.

### 2. New `[AcBinaryUnion(byte tag, Type subtype)]` attribute

Multiple instances per base class / interface declare the closed polymorphism set:

```csharp
[AcBinarySerializable]   // EnablePolymorphismFeature defaults to true
[AcBinaryUnion(0, typeof(Cat))]
[AcBinaryUnion(1, typeof(Dog))]
public abstract partial class Animal { ... }
```

SGen detects `[AcBinaryUnion]` on abstract / base type → emits the switch-based write/read dispatch instead of falling through to runtime AQN.

### 3. New `PolymorphismMode` enum on `AcBinarySerializerOptions`

Options-level default for unattributed polymorphism (i.e. the case where `EnablePolymorphismFeature = true` but no `[AcBinaryUnion]` is declared):

- `Runtime` (today's default) — AQN-based. Flexible, AOT-incompatible.
- `Throw` — fail fast on any polymorphic write that lacks a `[AcBinaryUnion]` attribute. AOT-friendly diagnostic mode for migration scenarios.

Note: there is **no `UnionAttribute`-only mode** — declaration is per-type via the attribute, not options-global. The options-level mode only governs the **fallback** when no `[AcBinaryUnion]` is present.

**Wire-format addition:**

New marker (e.g. `UnionTagBase = <TBD>`) + `[byte tag][inner Object]`, parallel to existing `ObjectWithTypeName(72)`. Slot number to be assigned avoiding clashes with existing 64–134 / 192–255 ranges.

**Implementation outline:**
- `AcBinarySerializableAttribute` — new ctor parameter `enablePolymorphismFeature`, all existing ctors default it to `true` (backward compatible).
- `AcBinaryUnionAttribute` — new attribute, `AttributeUsage(AttributeTargets.Class | Interface, AllowMultiple = true)`.
- Source generator — emit `WriteUnion<TBase>(value, ctx, depth)` and `ReadUnion<TBase>(ctx, depth)` static methods on the union-base type's generated writer/reader. Skipped entirely when `EnablePolymorphismFeature = false`.
- Wire-format new marker + `[byte tag][inner Object]` body.
- Runtime path: `WriteValueNonPrimitive` checks the wrapper's `PolymorphismFeatureEnabled` flag; when `false`, skips the `value.GetType() != declaredType` polymorphism branch entirely.

**Acceptance:**
- `EnablePolymorphismFeature = false`: SGen-emitted dispatch contains zero `is`-typeof / GetType branches; runtime path also short-circuits. Verify in JIT disassembly.
- `EnablePolymorphismFeature = true`, no union: runtime AQN polymorphism works as today (full backward compat); preserved JSON-style flexibility for unattributed bases.
- `EnablePolymorphismFeature = true` + `[AcBinaryUnion]`: AOT-test (Native AOT publish) compiles and round-trips a polymorphic graph — `Type.GetType()` is never invoked on this path.
- Benchmark: union-switch polymorphism measurably faster than AQN polymorphism on deser side (typed switch vs. reflection lookup).
- Wire format documented in `BINARY_FORMAT.md`; `BINARY_FEATURES.md` cross-references the attribute pattern; `BINARY_OPTIONS.md` documents `PolymorphismMode`. `AcBinarySerializableAttribute` doc-string explains all three behavior modes.

## ACCORE-BIN-T-B7H4: Implement `AcBinarySerializerOptions` thread-safety fix
**Priority:** P2 · **Type:** Refactor · **Related:** `BINARY_ISSUES.md#accore-bin-i-l8n5` (canonical issue)

The latent thread-safety problem documented in `ACCORE-BIN-I-L8N5` — mutable `set;` properties on `AcBinarySerializerOptions` shared across concurrent serialize/deserialize calls — needs a fix before AcBinary ships as a NuGet package. The package cannot constrain how consumers scope their options instances; defensive contract is needed in the serializer itself.

**Three candidate fix directions** (decide before implementing):

1. **Defensive copy on ingress** — add `AcBinarySerializerOptions Clone()` method (member-wise copy). Every API entry point that retains an options instance clones it on entry. External mutation to the original becomes invisible to the holder.
   - **Pro**: non-breaking. Existing consumer code unchanged. No major version bump required.
   - **Pro**: API surface change limited to one new `Clone()` method.
   - **Con**: per-call clone overhead (small, but non-zero). Cache keyed on options-identity becomes invalid for downstream code using reference equality.
   - **Con**: doesn't fix the **underlying** mutability — internal code can still race-mutate the cloned snapshot if a method retains both the snapshot and modifies it concurrently.

2. **Immutable record refactor** — `set;` → `init;` on all configuration properties. Mutation requires `with`-expression which produces a new instance.
   - **Pro**: type-system-strong guarantee. Race becomes a compile error, not a runtime corruption risk.
   - **Pro**: zero runtime overhead (init-only is compile-time check; record class semantics are unchanged at runtime).
   - **Con**: **breaking change** for any consumer doing `opts.UseGeneratedCode = false` after construction. Major version bump.
   - **Con**: source-generator coordination needed if SGen emits options-builder code that mutates properties.

3. **Read-only flag pattern** (à la `JsonSerializerOptions.MakeReadOnly()`) — mutable by default, holder calls `MakeReadOnly()` on entry; subsequent property setters throw `InvalidOperationException`.
   - **Pro**: BCL-precedent — Microsoft adopted it for `JsonSerializerOptions` in .NET 7 (`dotnet/runtime#74431`) for exactly this problem. Familiar pattern for consumers.
   - **Pro**: minimal API surface change (one new method + `IsReadOnly` flag property).
   - **Pro**: per-call overhead = single bool check per setter call. Negligible.
   - **Con**: opt-in by the holder — if a custom consumer-side wrapper forgets to call `MakeReadOnly()`, the safety hole stays open for that wrapper's clients. Documentation-driven safety, not type-system-driven.
   - **Con**: bypasses static-analysis tooling (the setter signature stays public; the throw is runtime). IDE doesn't surface "this property is currently read-only" in autocomplete.

**Recommendation:** **Option 3 (`MakeReadOnly` pattern)** is the BCL-precedent, lowest-friction migration path. Microsoft adopted it for `JsonSerializerOptions` in .NET 7 to solve the same problem; AcBinary should follow the same pattern for consistency with consumers' mental model and zero migration cost.

**Coordination with the existing `AcBinaryHubProtocol` setter side-effect** (the second risk surface in `ACCORE-BIN-I-L8N5`): the protocol ctor currently mutates the caller-provided options reference (`_options.BufferWriterChunkSize = options.BufferSize`). After the fix:
- **Option 1 (Clone)**: ctor mutates the **cloned** snapshot → no side-channel to the caller. Fix transparent.
- **Option 2 (Immutable)**: ctor cannot mutate; needs to construct a new options via `with`-expression. Breaking change in the ctor's options-handling.
- **Option 3 (MakeReadOnly)**: ctor mutates **before** calling `MakeReadOnly()` — same as today, but explicit "frozen" point afterwards. Caller-side mutation post-ctor is now a runtime throw.

**Implementation outline (Option 3 path):**

1. `AcBinarySerializerOptions.IsReadOnly { get; }` — public bool property.
2. `AcBinarySerializerOptions.MakeReadOnly()` — sets the flag; idempotent (no-op if already set).
3. All `set;` accessors guard: `if (IsReadOnly) throw new InvalidOperationException("AcBinarySerializerOptions has been made read-only and can no longer be mutated. Construct a new options instance instead.");`.
4. `AcBinarySerializer.Serialize<T>` entry (and all sibling entries — `Deserialize<T>`, `SerializeChunked`, etc.): `options.MakeReadOnly()` before any property read.
5. `AcBinaryHubProtocol` ctor: complete the `BufferWriterChunkSize` mutation **before** calling `options.MakeReadOnly()`. After ctor returns, the options instance is frozen for that protocol's lifetime.
6. Doc-string update on `AcBinarySerializerOptions` class header: explicit "thread-safety contract" section explaining the freeze-on-first-use semantics.

**Acceptance:**
- Concurrent stress test (16 threads × 1000 iterations) on a shared `AcBinarySerializerOptions` instance with property-mutation-attempts mid-iteration — all mutations after `MakeReadOnly()` throw `InvalidOperationException`; no silent corruption observed.
- Existing tests pass unchanged (the `MakeReadOnly` is opt-in for the serializer entries; tests that build options + use them once continue to work transparently).
- `BINARY_ISSUES.md#accore-bin-i-l8n5` Status updated to `Closed (YYYY-MM-DD)` with a `### Resolution` sub-section pointing to this TODO + the implementing commit.
- Doc-string on `AcBinarySerializerOptions` documents the freeze-on-first-use contract; `BINARY_FEATURES.md` or `BINARY_OPTIONS.md` cross-references the BCL-precedent (`JsonSerializerOptions.MakeReadOnly`).

## ACCORE-BIN-T-F8N3: Switch source-generator type-name hashing from simple-name to fully-qualified-name
**Priority:** P3 · **Type:** Refactor · **Related:** [`ACCORE-BIN-T-I3P8`](#accore-bin-t-i3p8-acbinarytypeid-attribute--explicit-type-id-override) (override mechanism for residual collisions)

The source generator's `ComputeFnvHash(typeSymbol.Name)` uses the **simple name only** (e.g. `"User"`, not `"MyApp.A.User"`). Cross-namespace types with the same simple name silently collide on `s_typeNameHash`. The hash is currently only consumed by the `WireMode=Metadata` inline metadata-write path (cross-version property compat) — the framework explicitly does NOT add wire-format type-id (per CLAUDE.md Rule #7: type-dispatch is consumer responsibility, see [`BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-t6v2`](BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-t6v2-single-fixed-type-per-long-lived-stream)). Within `UseMetadata`, the simple-name collision can still cause silent property-set mismatches between two types with the same short name in different namespaces — this TODO fixes that.

**Change scope** (`AcBinarySourceGenerator.cs`) — 4 call sites: `ComputeFnvHash(typeSymbol.Name)` → `ComputeFnvHash(typeSymbol.ToDisplayString())`:
- Self type-name hash (~line 358)
- Child type-name hash (~line 157)
- Element type-name hash (~line 254)
- Dict-value type-name hash (~line 311)

No runtime code changes; output regenerates with new constants on next build.

**Breaking change scope:** any saved binary stream that uses `WireMode=Metadata` and was produced by an older version embeds the old simple-name hash; consumers reading those streams with the new hash compute would mismatch and throw. Pre-1.0: acceptable. Post-1.0 would require a `WireMode=Metadata` format-version bump.

**Acceptance:**
- All `*_GeneratedWriter.g.cs` files regenerate with FQN-based `s_typeNameHash` values.
- Existing tests pass (auto-regen propagates; no manual hash literals in tests).
- Wire format identical for `WireMode=Compact` (no metadata embedded).
- `UseMetadata=true` paths produce different hashes — explicitly tested via round-trip.

## ACCORE-BIN-T-I3P8: `[AcBinaryTypeId(...)]` attribute — explicit type-id override
**Priority:** P3 · **Type:** Feature · **Related:** [`ACCORE-BIN-T-F8N3`](#accore-bin-t-f8n3-switch-source-generator-type-name-hashing-from-simple-name-to-fully-qualified-name) (FQN base hash being overridden)

Once `ACCORE-BIN-T-F8N3` reduces collision frequency by switching to FQN, residual FQN-hash collisions are still possible (32-bit hash space, birthday paradox). Currently the only consumer of `s_typeNameHash` is the `WireMode=Metadata` inline metadata-write path — a residual collision there causes a silent property-set mismatch.

`[AcBinaryTypeId(0x12345)]` attribute on a class:
- Source generator emits `s_typeNameHash = 0x12345` instead of computing FNV.
- Two types with the same `[AcBinaryTypeId(...)]` value → compile-time / first-use error.

**Useful for:**
- Resolving rare FQN-hash collisions deterministically (within `WireMode=Metadata`).
- Pinning a stable type-id across class renames (wire-compat across versions in `Metadata` mode).
- Future-proofing: if a Layer 1 consumer (hypothetically) builds a type-dispatch above AcBinary using `s_typeNameHash`, the same override mechanism applies.

**Acceptance:**
- New attribute class shipped alongside `[AcBinarySerializable]`.
- Generator honours the override (emits explicit constant instead of FNV result).
- Tests: rename a class with `[AcBinaryTypeId]` → `s_typeNameHash` unchanged.

## ACCORE-BIN-T-X2M5: Evaluate xxHash3 vs FNV-1a for type-name hashes
**Priority:** P3 · **Type:** Investigation · **Related:** [`ACCORE-BIN-T-F8N3`](#accore-bin-t-f8n3-switch-source-generator-type-name-hashing-from-simple-name-to-fully-qualified-name)

FNV-1a is currently used for both `s_typeNameHash` and `s_propertyHashes`. For compile-time hashing, performance is irrelevant. For collision resistance:
- FNV-1a 32-bit: ~50% collision at ~77K types (birthday paradox). Adequate for small/medium projects, marginal for large ones with many auto-generated types.
- xxHash3 32-bit: comparable mathematical properties to FNV-1a (both non-cryptographic).
- xxHash3 64-bit: dramatically better collision resistance (~50% at ~5B entries), at the cost of 8 wire bytes instead of 4.

**Trigger:** real collisions observed (1000+ types per assembly + cross-assembly aggregation), or community feedback indicating collision pain.

**Investigation questions** (no code change without a triggering pain signal):
1. Switch to xxHash3 32-bit (incremental improvement) — but doubles the change scope (touch property hashes too if uniformity desired).
2. Switch to xxHash3 64-bit (8 wire bytes instead of 4) — meaningful collision resistance, modest wire cost.
3. Stay on FNV-1a + force `[AcBinaryTypeId]` for collisions — minimal change, devops burden.

Investigation only — defer until pain signal arrives.

## ACCORE-BIN-T-K9E4: `[RequiresDynamicCode]` + `[RequiresUnreferencedCode]` on Runtime-only methods
**Priority:** P3 · **Type:** Refactor · **Related:** `BINARY_FEATURES.md#nativeaot-compatibility`

The Runtime path (factories in `AcSerializerCommon` + wrapper-based deserialize fallback in `AcBinaryDeserializer`) currently works under NativeAOT thanks to DAMs propagation + `RuntimeFeature.IsDynamicCodeSupported` guards, but the trimmer still emits warnings for the well-known blind spots (polymorphism via `obj.GetType()`, nested-type chain via generic argument extraction). The library suppresses these with `[UnconditionalSuppressMessage]` and documented justification.

A complementary signal would be to mark the Runtime entry points (or the factories themselves) with `[RequiresDynamicCode("AcBinary Runtime path uses Reflection.Emit / closed-generic instantiation; use [AcBinarySerializable] + SGen for NativeAOT.")]` and `[RequiresUnreferencedCode("...")]`. Effect:

- AOT publish in consumer's project surfaces a warning at the call site → consumer chooses SGen or accepts the Runtime cost
- Mirrors the System.Text.Json reflection-mode pattern (`[RequiresDynamicCode]` on `JsonSerializer.Serialize<T>` overloads)
- One-codebase, no NuGet split needed
- Cheap implementation — attribute placement only

**Coordination:** `[RequiresDynamicCode]` is contagious; every caller must either propagate it or suppress with `[UnconditionalSuppressMessage]`. Scope:
- Public `Serialize<T>` / `Deserialize<T>` entry points stay attribute-free (consumer-facing)
- Runtime fallback methods get the attribute (contained inside the library)
- The DAMs annotations we already have stay — they're orthogonal (one prevents trim, the other warns about JIT-only behavior)

**Acceptance:**
- Consumer's AOT publish surfaces a IL2026/IL3050 warning when `UseGeneratedCode=false` is set or an unattributed type is deserialized
- SGen path is warning-free
- Library compiles 0 warnings (suppressions added at the propagation barrier)
- `BINARY_FEATURES.md` NativeAOT Compatibility section updated to mention the explicit warning signal

## ACCORE-BIN-T-A2J7: Optional `AyCode.Core.Aot` NuGet variant (SGen-only build)
**Priority:** P3 · **Type:** Feature · **Related:** `BINARY_FEATURES.md#nativeaot-compatibility`, `ACCORE-BIN-T-K9E4`

Binary-size-sensitive AOT consumers (Blazor WASM, MAUI mobile, embedded, container-trimmed) benefit from a smaller library variant that strips the Runtime fallback path entirely. Estimated savings: ~80-150 KB of native code (~25-60 KB compressed wire size for WASM publish).

**Strippable code in the `.Aot` variant:**

| Component | LOC | Purpose | Removable in Aot? |
|---|---|---|---|
| `AcSerializerCommon.Create*` (7 factory methods + Expression-tree code) | ~150 | Runtime delegate compilation | ✅ Yes |
| `TypeMetadataBase` runtime metadata path (`CompiledConstructor`, IdGetters via Expression.Compile) | ~300 | Reflection-based metadata | ✅ Yes |
| `AcBinaryDeserializer` wrapper-based runtime fallback (`PopulateObjectPropertiesIndexed`, `ReadObjectCoreWithWrapper` non-SGen branches, `CreateInstance(type)` Activator-fallback) | ~500 | Runtime polymorphic dispatch | ✅ Yes |
| Property accessor runtime delegate fields (`_dynamicGetter`, typed getter/setter caches outside SGen) | ~150 | Boxed property access | ✅ Yes |
| `System.Linq.Expressions` transitive dependency | — | Expression-tree IL emission | ✅ Yes (when nothing else in graph uses it) |

**Implementation sketch** (avoid `#if`-erdő via file-level split):

```
AyCode.Core/Serializers/
  AcSerializerCommon.cs              // SGen-safe shared parts
  AcSerializerCommon.Runtime.cs      // 7 Create* factory methods only here
  AcBinaryDeserializer.cs            // SGen path
  AcBinaryDeserializer.Runtime.cs    // wrapper-based runtime fallback path
  TypeMetadataBase.cs                // SGen-safe metadata
  TypeMetadataBase.Runtime.cs        // Expression.Compile-based ctor + accessor wiring
```

Two `.csproj` files:
- `AyCode.Core.csproj` — full package (current); includes all files
- `AyCode.Core.Aot.csproj` — `<Compile Remove="**/*.Runtime.cs" />`; sets `<PackageId>AyCode.Core.Aot</PackageId>`; same version as full

**Trade-offs:**

- ✅ No `#if` directives in business code — physically separate file groups
- ✅ Source mostly shared via SDK include/exclude semantics
- ✅ DAMs annotations and trim-suppressions only land in the full package; `.Aot` variant is genuinely trim-clean by construction
- ✅ "Strict SGen" semantics in `.Aot`: a non-SGen type at deser time throws clearly instead of silently falling back. Marketing positioning: "guaranteed SGen path, no hidden slow lane".
- ⚠️ Two NuGet IDs, two changelogs, version sync (CI-automatable)
- ⚠️ Consumer must pick the right package — wrong choice = breaking switch later

**Coordination:**
- Land `ACCORE-BIN-T-K9E4` first (`[RequiresDynamicCode]` attributes) — if that pattern handles the consumer-side scenarios well, `.Aot` may not be needed
- The current Runtime fallback code is already well-isolated (mostly in `AcSerializerCommon` factories + `AcBinaryDeserializer` wrapper-based methods), so the file-split refactor is mechanically straightforward
- Marketing decision: is binary size a central pillar? If yes, `.Aot` is a NuGet differentiator; if not, `K9E4` alone is enough

**Acceptance:**
- `AyCode.Core.Aot.csproj` produces a NuGet ~25-60 KB smaller than `AyCode.Core` after compression
- `.Aot` build emits zero IL/AOT trim warnings (no suppressions needed because the Runtime path code is physically removed)
- Round-trip tests pass on `.Aot` for all SGen types
- `.Aot` throws a clear `InvalidOperationException` (not `MissingMethodException`) when a non-`[AcBinarySerializable]` type is encountered at deser time
- `BINARY_FEATURES.md` NativeAOT Compatibility section documents both packages and when to choose which

## ACCORE-BIN-T-V4N2: Cross-tier SIMD UTF-8 transcoder paths (AVX-512BW + Vector128 + multi-byte transcoder)
**Priority:** P2 · **Type:** Performance · **Related:** `EncodeUtf8SinglePass`, `DecodeUtf8SinglePass`, `CountUtf8Chars`

**Current SIMD hierarchy (post 2026-05-05 implementation):**

```
AVX-512BW (64 byte/iter)   → Server, Intel 11th gen client, AMD Zen 4+
Vector256 / AVX2 (32 byte) → AVX2 host (Intel 12-14th gen, AMD Zen 3 and earlier)
Vector128 (16 byte/iter)    → Apple Silicon NEON, WASM SIMD, legacy SSE2
scalar (1 byte/iter)        → no-SIMD fall-back
```

JIT/AOT path-selection via `[Intrinsic]` `IsSupported` static booleans — non-supported tiers constant-folded to dead code per host. Cascading tail handlers: a higher tier's tail (< 64 byte AVX-512 → < 32 byte Vector256 → < 16 byte Vector128 → scalar) is processed by the next-lower tier on the same iteration. No regression on any host.

**Implementation status:**

| Phase | Method | AVX-512BW | Vector256 | Vector128 | scalar |
|-------|--------|-----------|-----------|-----------|--------|
| 1 | `CountUtf8Chars` (decode 1st pass) | ✅ done | ✅ existing | ✅ done | ✅ existing |
| 2 | `EncodeUtf8SinglePass` Phase 1 (ASCII narrow) | ✅ done | ✅ existing | ✅ done | ✅ existing |
| 2.5 | `DecodeUtf8SinglePass` scalar run-length decoder (multi-byte baseline) | — | — | — | ⏳ TODO |
| 3a | `DecodeUtf8SinglePass` multi-byte transcoder (Vector512) | ⏳ TODO | bail-out only | bail-out only | ✅ existing |
| 3b | `DecodeUtf8SinglePass` multi-byte transcoder (Vector256) | — | 🔍 **deferred — see note** | bail-out only | ✅ existing |
| 3c | `DecodeUtf8SinglePass` multi-byte transcoder (Vector128) | — | — | ⏳ TODO | ✅ existing |

**Note on Phase 3b (Vector256 / AVX2) — deferred, not dropped.** AVX2 lacks the AVX-512BW primitives (`CompareEqualMask` producing a `__mmask` k-register, in-lane `vpermb`, mask-driven `vpcompressb`) that make the classify-mask-compress-widen pipeline efficient. The `Vector256.Shuffle` is cross-lane via two `vpshufb` (per-128-bit-lane), which complicates leader-byte extraction across multi-byte sequences spanning the lane boundary. The simdutf C++ project — the canonical reference for this algorithm class — implements only **SSE4 (16-byte)** and **AVX-512 (64-byte)** paths; it explicitly skips AVX2 because the implementation cost-benefit is unfavorable on this algorithm.

On AVX2 hosts, the Phase 3c (Vector128) transcoder runs as the primer multi-byte path AND as tail handler — covering AVX2 hosts with 16-byte/iter, which is already a significant win over the current scalar multi-byte branch. Phase 3b would require either:

1. Hand-rolling an AVX2-specific 32-byte algorithm with cross-lane permute workarounds (research-grade complexity, uncertain net win — could be SLOWER than the Vector128 path due to cross-lane shuffle latency)
2. Waiting for `Avx10v1` / `Avx10v2` to expose AVX-512BW-class primitives in 256-bit form (Intel's unified vector ISA — `Avx10v1` already in .NET 9, `Avx10v2` arrives with future Intel hardware)

**Re-evaluation triggers:** if benchmark on AVX2 hosts shows Phase 3c Vector128 path leaves > 10% Deser gap vs MemPack on multi-byte content; or if `Avx10v1` 256-bit primitives mature enough to make the algorithm tractable. Until then: **Phase 3b stays in the TODO as a research / future-work item** — not actively scheduled, but documented so a future contributor doesn't re-derive the AVX2 limitations.

**Phase 3 is the remaining gap — UTF-8 multi-byte decode on every host class**. ASCII path is already fast across all SIMD tiers (Vector256 + Vector128 prefix widen + `Encoding.Latin1.GetString` BCL fast path). The gap is on **multi-byte UTF-8 content** — Hungarian / Cyrillic / Greek (2-byte) and CJK BMP (3-byte) sequences — where the SIMD prefix bails out on the first non-ASCII byte and falls back to scalar bit-extract. The Repeated benchmark cell (Hungarian content) is the canonical witness; with all-Hungarian content (current bench data), Small / Repeated Deser cells trail MemPack by 6-14%.

**Why all 3 SIMD tiers (not just AVX-512BW)** — public NuGet package goal: i18n payloads must be fast on every supported host (cloud server, desktop, mobile, Blazor WASM), not only AVX-512-capable cloud servers. The saját scalar multi-byte branch is the bottleneck on **all** non-ASCII content regardless of host class. The BCL `Encoding.UTF8` falls back to a similar scalar path on multi-byte content (with virtual dispatch + EncoderFallback overhead), so even where the BCL has its own SIMD 2-byte handler (.NET 9 PR #92580), our trust-input scalar wins on net — but a saját SIMD multi-byte path would dominate on every host.

**Phase 3 approach — in-house multi-byte transcoder, three SIMD widths.** Single algorithm template (classify-mask-compress-widen pipeline) ported across Vector512 / Vector256 / Vector128 register widths. Algorithm designed and written in-house — no third-party port, no NuGet dependency:

- **Phase 3a — `DecodeUtf8SinglePass` Vector512 (AVX-512BW)**: 64-byte block fetch → classify each byte's UTF-8 sequence position via mask compares → byte-compression for length-resolution → widen to UTF-16 in two `Vector256<ushort>` lanes → store. ~3-5× speedup vs current scalar multi-byte branch on Hungarian / CJK content. Activates on AVX-512 hosts (cloud server, Intel 11th gen, AMD Zen 4+).
- **Phase 3b — `DecodeUtf8SinglePass` Vector256 (AVX2)**: same algorithm at 32-byte block. Smaller register space → fewer codepoints per iter, but ASCII bail-out gone → multi-byte content is now SIMD-handled. ~2-3× speedup. Activates on AVX2 hosts (Intel 12-14th gen, AMD Zen 3 and earlier).
- **Phase 3c — `DecodeUtf8SinglePass` Vector128 (NEON / SSE / WASM SIMD)**: same algorithm at 16-byte block. ~1.5-2× speedup. Activates on Apple Silicon / WASM / legacy x86 — covering the i18n production case for mobile (MAUI iOS / Android) and Blazor WASM.

The cascading tail-handler hierarchy (existing in Phase 1+2) carries over: AVX-512 → Vector256 → Vector128 → scalar tail. Each tier hands off the < N-byte tail to the next-lower tier.

**No .NET 11 / multi-targeting needed.** `Avx512BW`, `Vector256`, `Vector128` intrinsics all available in .NET 9 (and .NET 8). Implementation lands on the current `net9.0` target.

**Hardware reach (2026).** Per Wikipedia "CPUs with AVX-512":

- ✅ **Intel server**: Skylake-X (2017), Cascade Lake-X, Ice Lake-SP, **Sapphire Rapids (2023+)**, Emerald Rapids, Granite Rapids — near-universal in cloud (Azure, AWS, GCP)
- ✅ **Intel client 11th gen**: Tiger Lake (mobile, 2020), Rocket Lake (desktop, 2021), Ice Lake (mobile) — pre-Alder Lake era still supports AVX-512
- ❌ **Intel client 12-14th gen**: Alder Lake / Raptor Lake / Meteor Lake / Core Ultra — AVX-512 disabled at firmware level (E-core blocking) → falls back to Vector256
- ✅ **AMD Zen 4+**: Ryzen 7000 (2022), Ryzen 9000 (2024), EPYC Genoa (2022), EPYC Turin (2024)
- ❌ **AMD pre-Zen 4**: Zen 3 and earlier → falls back to Vector256
- ❌ **Apple Silicon / ARM**: NEON only → uses Vector128 (16 byte/iter)
- ❌ **Blazor WASM**: only 128-bit SIMD per WASM SIMD spec → uses Vector128 (16 byte/iter)

The Vector128 path is the **WASM and Apple Silicon target** — without it both platforms fell back to scalar (1 byte/iter). With Phase 1+2 landed, WASM and Apple Silicon now run the UTF-8 hot path at 16 byte/iter (16× scalar speedup on the count + ASCII narrow operations).

### Phase 2.5 — scalar run-length decoder (multi-byte baseline, pre-Phase 3 prototype) — **TESTED & REVERTED 2026-05-07**

**Status update (2026-05-07)**: Phase 2.5 was implemented and tested in two configurations:
- **Full run-length** (15:56:54 bench) — both 2-byte and 3-byte tiers used inner do-while loops. Result: +13.0 pp Deser regression on the Hungarian-mixed Repeated cell. Hypothesis confirmed (foreseen pre-implementation): rövid Magyar 2-byte runs (1-2 char average) make the run-detection overhead exceed the per-char payload; switch-jumptable per-char dispatch wins on this content shape.
- **Hybrid** (post-15:56:54) — 2-byte single decode, 3-byte run do-while only. Tested but bench-zaj instabilitás miatt unmeasurable signal. Reverted along with V4N4 method-split (2026-05-07).

The optimization-value signal proved below the bench noise floor on the available hardware. The 3-byte do-while CJK-content win remains a **theoretically valid** target — but cannot be objectively validated without the `ACCORE-BIN-T-C5R8` charset-parameterized benchmark workload (CJK option). Re-evaluate when CJK workload measurement becomes available.

**Re-evaluable as of 2026-05-07 per `ACCORE-BIN-T-D9X3`** — bench stabilization removes the noise-floor that made the original signal unmeasurable; retest before any code change. (Charset bias remains — pair with `ACCORE-BIN-T-C5R8` for CJK validation.)

**Retested 2026-05-08 — REGRESSION CONFIRMED** (Latin1Long charset, stabilized bench): adding the do-while inner loop on both 2-byte and 3-byte tiers in `DecodeUtf8SinglePass` produced **+5-8pp Deser regression on every cell** vs. the switch-jumptable baseline (Small +7.8pp, Medium +7.1pp, Large +5.5pp, Repeated +7.4pp, Deep +4.9pp). Reverted to switch-jumptable single-decode same day. The V4N2 entry's original prediction held: "Magyar mixed (KözösCímke, sötét — short alternating runs): 0-5% (run-detection overhead may eat the savings on short runs)" — Latin1Long suffix has 1-2 char average run length, well below the run-detection break-even point. **Phase 2.5 is dead on Magyar mixed.** CJK retest still untried, but Phase 2.5 is now obsoleted by `ACCORE-BIN-T-K7M3` (the decoder hot path runs `Utf8.ToUtf16` BCL static API, not `DecodeUtf8SinglePass`).

**Below: original Phase 2.5 design notes preserved as documentation.** Implementation details remain accurate even though the implementation was reverted.

---

Targets the `DecodeUtf8SinglePass` switch-jumptable per-char dispatch on multi-byte content. Current scalar Phase (jumptable) re-dispatches every char; a run-length-aware scalar decoder runs a tight branchless inner loop on homogeneous runs (long ASCII run, long 2-byte Latin/Cyrillic run, long 3-byte CJK BMP run), with the existing single-codepoint scalar branch as mixed-edge fallback.

**Algorithm sketch**:
```
while (s < src.Length)
{
    // 1) ASCII run (0xxxxxxx) — already handled by Phase 1 SIMD prefix; this is tail
    int asciiStart = s;
    while (s < src.Length && src[s] < 0x80) s++;
    if (s > asciiStart) { WriteAsciiRun(src.Slice(asciiStart, s-asciiStart), dst, ref d); continue; }

    // 2) 2-byte run (110xxxxx 10xxxxxx) — Hungarian / Cyrillic / Greek / Hebrew / Arabic
    int twoStart = s;
    while (s + 1 < src.Length && Is2ByteLead(src[s]) && IsCont(src[s+1])) s += 2;
    if (s > twoStart) { Decode2ByteRun(src.Slice(twoStart, s-twoStart), dst, ref d); continue; }

    // 3) 3-byte run (1110xxxx 10xxxxxx 10xxxxxx) — CJK BMP, other 3-byte BMP scripts
    int threeStart = s;
    while (s + 2 < src.Length && Is3ByteLead(src[s]) && IsCont(src[s+1]) && IsCont(src[s+2])) s += 3;
    if (s > threeStart) { Decode3ByteRun(src.Slice(threeStart, s-threeStart), dst, ref d); continue; }

    // 4) Mixed-edge fallback (typically 4-byte surrogate pair or single transition char)
    DecodeSingleCodePoint(src, ref s, dst, ref d);
}
```

**Why P2.5 — scalar baseline before SIMD multi-byte (Phase 3a-3c)**:
- 1-2h prototyping cost vs 6-10h Phase 3 SIMD work
- A/B benchmark on Repeated cell decides whether the run-length structure already wins on Magyar mixed (`KözösCímke` pattern) — if it does, Phase 3 lifts further; if not, Phase 3 SIMD is the only win path
- Documents the "switch-jumptable bottleneck on Hungarian benchmark" hypothesis without committing to the larger SIMD effort
- The `Decode2ByteRun` / `Decode3ByteRun` scalar-batch implementations also serve as algorithm references for the Phase 3 SIMD versions (clear semantics first, optimize after)

**Expected payoff** (per content class, ratio vs current switch-jumptable):
- Long CJK BMP (3-byte run, e.g. `你好世界` ×30): ~20-40% Deser improvement (long homogeneous run, biggest jumptable savings)
- Long 2-byte run (`árvíztűrő` ×10+): ~5-15% improvement
- Magyar mixed (`KözösCímke`, `sötét` — short alternating runs): 0-5% (run-detection overhead may eat the savings on short runs)
- Long ASCII (≥32 byte): 0% (Phase 1 SIMD prefix already handles)
- Emoji (4-byte): 0% (mixed-edge fallback unchanged)

**Risk** — the existing switch-jumptable JIT optimization is strong; Magyar mixed text (1-2 char runs) may not show net gain. Implementation must be **isolated prototype first** (alongside the live `DecodeUtf8SinglePass`, not replacing it), with A/B benchmark comparing the two before any switch.

**Acceptance (Phase 2.5)**:
- Repeated cell Compact Deser ratio ≤ 1.0 vs MemPack on AVX2 hosts (parity with current measurement, no regression)
- Round-trip tests pass on all UTF-8 content classes (ASCII / 2-byte / 3-byte BMP / 4-byte surrogate-pair)
- A/B benchmark shows ≥ 5% Deser improvement on Repeated OR ≥ 10% on Large cell — else Phase 2.5 stays in TODO as documented dead-end (negative result is also valuable: confirms the jumptable is fast enough, focus moves entirely to Phase 3)

### Phase 3 implementation outline

- Insert SIMD multi-byte branches at `DecodeUtf8SinglePass` entry, **before** the existing ASCII-prefix bail-out loops:
  ```
  if (Avx512BW.IsSupported && byteCount >= 64)        { Vector512MultiByteDecode(...) }
  if (Vector256.IsHardwareAccelerated && len-i >= 32) { Vector256MultiByteDecode(...) }
  if (Vector128.IsHardwareAccelerated && len-i >= 16) { Vector128MultiByteDecode(...) }
  // existing scalar tail
  ```
- Single algorithm template — classify-mask-compress-widen pipeline:
  1. Block load (Vector512 / Vector256 / Vector128)
  2. Classify each byte's UTF-8 sequence position via mask compares (start vs continuation, 1/2/3/4-byte sequence width)
  3. Compute output char count via popcount on start-byte mask + extra-char mask for 4-byte sequences
  4. Byte-compression for leader/continuation extraction (mask-driven `PermuteVar` / `Shuffle`)
  5. Combine leader + continuations into codepoints (shift + OR)
  6. Widen codepoints to UTF-16 chars (handle surrogate pairs for 4-byte sequences)
  7. Store output, advance src/dst pointers
- Block-boundary edge case: incomplete multi-byte sequence at block end → carry to next iter or hand off to lower tier / scalar tail
- Trust-input semantics maintained — no validate-pass instructions (reader input is valid UTF-8 by writer contract)
- `Avx512BW.X64.IsSupported` (64-bit-only intrinsics) checked separately if any code path requires the X64 sub-feature

### Why P2

- "i18n production deploy" perf gap on **every** host class — the public NuGet package contract requires fast multi-byte UTF-8 across cloud server, desktop, mobile, and Blazor WASM
- No NuGet dependency, no third-party code, no wire-format change, additive — pure CPU optimization
- Phase 1+2 delivered cross-tier ASCII / count SIMD coverage; Phase 3 closes the multi-byte CPU gap on **all** SIMD-capable hosts (not just AVX-512)
- Single algorithm template ported across 3 register widths — code volume manageable

### Acceptance

- Repeated Deser ratio ≤ 0.7 vs MemPack on AVX-512 hosts (Phase 3a)
- Repeated Deser ratio ≤ 0.8 vs MemPack on AVX2 hosts (Phase 3b)
- Repeated Deser ratio ≤ 0.85 vs MemPack on Apple Silicon / WASM (Phase 3c)
- Repeated Ser ratio ≤ 0.85 across all host classes
- Round-trip tests pass on all UTF-8 content classes (ASCII / 2-byte / 3-byte BMP / 4-byte surrogate-pair)
- `BINARY_FEATURES.md` documents the SIMD path selection across all four tiers

### Trigger

- Each SIMD width validated on a representative host before merge:
  - Phase 3a: AVX-512 host (developer's local AMD Zen 4+ desktop, Intel 11th gen, or server-class machine)
  - Phase 3b: AVX2 host (any modern x86 desktop / laptop without AVX-512)
  - Phase 3c: Apple Silicon (macOS / iOS / Mac Catalyst) AND Blazor WASM browser runtime
- Local `dotnet test` covers correctness; per-tier benchmarks measure the multi-byte speedup
- Phase 1+2 (AVX-512BW + Vector128 in `CountUtf8Chars` + `EncodeUtf8SinglePass` Phase 1) **landed 2026-05-05** — covered by existing round-trip tests, no regression on non-AVX-512 hosts (validated on AVX2-host bench)

## ACCORE-BIN-T-S5L8: Sentinel-length encoding for strings (wire-size optimization, both modes)
**Priority:** P3 · **Type:** Wire-format optimization · **Related:** `AcBinarySerializer.WriteString`, `AcBinaryDeserializer.ReadValue` string dispatch

The leading string-marker byte (`String` / `StringEmpty` / `Null`) exists primarily to distinguish null vs empty vs non-empty before dispatching. For **non-polymorphic, non-interned string properties** the marker can be replaced by a single sentinel-length VarUInt:

```
[VarUInt sentinelLength] [content bytes if applicable]
   sentinelLength == 0    → null
   sentinelLength == 1    → empty string
   sentinelLength == N+1  → string of N bytes/chars, content follows
```

MemoryPack-style encoding pattern. Applies to **both** Compact (UTF-8) and FastWire (UTF-16 raw) modes; the content following the sentinel differs by mode.

### Per-mode impact

**FastWire mode** — wire layout today: `[String marker][VarUInt charCount][UTF-16 raw bytes]`. Sentinel saves 1 byte per non-null string.

| TestData | Current FastWire wire | Estimated with sentinel | Δ |
|---|---|---|---|
| Small | 3122 B | ~3050 B | -2% |
| Medium | 10905 B | ~10500 B | -4% |
| Large | 68603 B | ~67000 B | -2% |
| Repeated | 16244 B | ~15700 B | -3% |
| Deep | 15514 B | ~14900 B | -4% |

Closes the +1.7-8.1% FastWire wire gap vs MemoryPack to near zero or favorable while keeping AcBinary FastWire's +9-20% speed advantage.

**Compact mode** — wire layout today varies by length:
- Short (≤31 byte): `[FixStr+length][UTF-8 bytes]` — already 1-byte marker, ties sentinel.
- Long (>31 byte): `[String marker][VarUInt byteCount][UTF-8 bytes]` — sentinel saves 1 byte (the marker).

Compact gain: **only on long strings** (>31 byte UTF-8). Estimated −1 byte per long string. Workload-dependent: if most strings are short or use interning, gain is small. If many long mixed-content strings, meaningful saving.

### Limitations (both modes)

- **Polymorphic `object` properties**: marker needed for type discrimination. Sentinel encoding only applies when the property type is statically `string` or `string?`.
- **Interning incompatible**: sentinel cannot express `StringInternFirst` / `StringInterned` markers (those carry cache-index semantics). Interned properties keep marker-based encoding. FastWire mode already disables interning by design (consistent); Compact mode needs per-property dispatch (interned → marker, non-interned → sentinel).
- **Compact-mode FixStr ties**: short strings (≤31 byte UTF-8) gain nothing in Compact (FixStr is already 1-byte marker+length). The optimization wins only on long strings in Compact.

### Implementation outline (rough — refine when implementing)

1. Writer: branch in `WriteString` on property metadata flags `(IsString, IsNotInterned, IsNotPolymorphic)`. If sentinel-eligible, emit `VarUInt sentinelLength` + content. Else fall through to existing marker-based encoding.
2. Reader: matching branch in property reader. If sentinel-eligible (per property metadata), read `VarUInt sentinelLength`, dispatch on 0/1/N+1.
3. SGen: emit sentinel-encoding variant for non-polymorphic non-interned `string` typed properties; emit existing marker-encoding for the rest.
4. Wire format version bump OR header flag indicating sentinel-encoding-active. (Cross-version compat policy decided when implementing.)

### Trigger

- After D-2 / decoder optimization / marker-dispatch land (compact-mode focus completes)
- When wire-size positioning becomes a primary pillar for NuGet release
- Re-evaluate scope at implementation time — exact gain in Compact depends on consumer workload (long-string ratio, interning patterns)

### Acceptance

- FastWire mode: AcBinary wire ≤ MemoryPack on at least 4 of 5 test cells
- Compact mode: long-string wire bytes -1 each, no regression on short or interned strings
- Speed benchmark: no regression vs current encoding (essentially zero CPU cost — sentinel is shifted bookkeeping)
- Cross-version compat: documented format version bump + clean fail on old reader / new wire mismatch
- Polymorphic + interned property test cases pass unchanged (use existing marker-based encoding)

## ACCORE-BIN-T-H7K3: Hungarian / multi-byte content Ser optimization (Repeated Strings cell)
**Priority:** P3 · **Type:** Performance · **Related:** `EncodeUtf8SinglePass` Phase 3 (scalar multi-byte encode), `ACCORE-BIN-T-E2F9` resolution
**Status:** Closed (2026-05-04) — Won't Fix (JIT-only artifact)

The Repeated Strings benchmark (Hungarian content: `"TermékNév_…"`, `"RaklapKód_…"`) still shows AcBinary Ser ratio ~1.04 vs MemPack across multiple runs (12-50-43 / 13-21-27 / 13-27-20 series). All other ASCII-heavy cells (Small/Medium/Large/Deep) sit in the 0.79-0.94 ratio range — Repeated is the outlier.

The Phase 3 scalar multi-byte branch in `EncodeUtf8SinglePass` (1-byte ASCII / 2-byte Latin-extended / 3-byte BMP / 4-byte surrogate-pair) processes Hungarian diacritics (`á`, `é`, `í`, `ő`, `ű`, etc.) as 2-byte UTF-8 sequences via scalar bit-extract. MemPack's UTF-8 encoder appears to use a SIMD-accelerated mixed-content lane that processes 2-byte sequences in parallel.

### Resolution

**AOT bench 13-40-29: Repeated Ser ratio = 0.96** (AcBinary 14.50 µs vs MemPack 15.05 µs, AcBinary GYORSABB by 4%). Deser ratio 0.91 (also faster).

The 1.04+ ratio observed in JIT-mode benchmarks (12-50-43, 13-21-27, 13-27-20) was a JIT tier-up artifact — the SGen-generated writer's hot path (which calls `EncodeUtf8SinglePass`) didn't reliably tier up to fully-optimized code within the 1000-iteration measurement window, while MemPack's writer apparently warmed up faster. Under NativeAOT publish (`-p:_IsPublishing=true`) the issue disappears completely — both writers are deterministically optimized at compile time.

No structural problem in the Phase 3 scalar branch. The investigation directions (Vector256 mixed-content lane, BCL `Utf8.FromUtf16` comparison) remain valid academic improvements but show no meaningful production-time win — closing as Won't Fix.

## ACCORE-BIN-T-S2X9: Markerless schema lane — drop per-property type markers for fixed-shape primitives (SGen)
**Priority:** P2 · **Type:** Wire-format extension · **Related:** `ACCORE-BIN-T-S5L8`, `ACCORE-BIN-T-W7N5`

AcBinary is **marker-driven**: every value on the wire carries a 1-byte type code, so the reader can dispatch generically (handles polymorphism, null, intern markers, type-name lookup, etc.). MemPack is **schema-driven**: the SGen reader knows at compile time that "field 3 is `int`, field 4 is `string`" and reads values directly with no type code, no run-time dispatch.

For fixed-shape primitive properties (`int`, `bool`, `double`, `Guid`, `DateTime`, …) on `[AcBinarySerializable]` types, the per-property type marker is pure overhead — the SGen-generated reader already has compile-time knowledge of the property type, so the marker only confirms what is already known. Dropping it on this narrow class of properties is a clean wire+CPU win without losing any of the polymorphism / null / intern flexibility that the marker provides for variable-shape values.

### Why P2 — `WireMode = Fast` wire-size parity (NuGet release narrative)

The `WireMode = Fast` lane currently produces **+1.7% to +8.1% larger wire** than MemPack across all benchmark cells (AOT bench 13-40-29: Small +52 byte, Medium +474, Large +3617, Repeated +1221, Deep +581). The gap is structural: UTF-16 raw-memcpy strings are 2 bytes/char fixed, while MemPack's UTF-8 is 1 byte/char on ASCII content. Touching the string-write path to fix this would either:
- Lose the raw-memcpy guarantee (post-encode ASCII-detect + branchy dispatch — kills the FastWire CPU advantage), or
- Add sentinel-encoding micro-savings (~3-5% wire) which don't close the structural gap.

**Markerless schema lane is the only path to wire-size parity that preserves the FastWire raw-memcpy hot path.** Per-primitive-property savings (1 byte for non-tiny `int`, `Guid`, `DateTime`, `decimal`, `double`, …) compound on DTO-heavy payloads. Estimated effect on benchmark cells:

| Cell | Current FastWire | MemPack | Estimated post-S2X9 FastWire | vs MemPack |
|------|------------------|---------|------------------------------|-----------|
| Small (~70 primitive prop) | 3122 | 3070 | ~3050 | -0.7% ✅ |
| Medium (~600 primitive prop) | 10905 | 10431 | ~10300 | -1.3% ✅ |
| Large (~6000 primitive prop) | 68603 | 64986 | ~63500 | -2.3% ✅ |
| Deep (~700 primitive prop) | 15514 | 14933 | ~14800 | -0.9% ✅ |

The Repeated cell is harder to predict (string-dominated payload, fewer primitives) — likely smaller win, may not fully close the +8.1% gap. Acceptable: the Repeated cell is a string-interning stress test, not a typical DTO workload.

**NuGet release narrative**: "FastMode beats MemoryPack on **both** wire size AND throughput across all benchmark cells" — currently we have to qualify this with "throughput-only on Compact + i18n workloads"; S2X9 removes the qualifier. This is high-leverage for the public bench shootout.

### Wire savings per property type

| Type | Current encoding | Markerless lane | Wire saved |
|------|------------------|-----------------|------------|
| `int` (TinyInt range −16..47) | TinyInt (1 byte) | VarInt (1 byte) | 0 |
| `int` (out-of-tiny) | `[Int32]` `[VarInt]` (2-6 bytes) | VarInt (1-5 bytes) | 1 byte |
| `bool` | `[True]` or `[False]` (1 byte) | 1 byte (0/1) | 0 |
| `Guid` | `[Guid]` `[16 bytes]` (17 bytes) | 16 bytes | 1 byte |
| `DateTime` | `[DateTime]` `[9 bytes]` (10 bytes) | 9 bytes | 1 byte |
| `DateTimeOffset` | `[DateTimeOffset]` `[10 bytes]` (11 bytes) | 10 bytes | 1 byte |
| `TimeSpan` | `[TimeSpan]` `[VarLong]` (2-9 bytes) | VarLong (1-9 bytes) | 1 byte |
| `decimal` | `[Decimal]` `[16 bytes]` (17 bytes) | 16 bytes | 1 byte |
| `double` | `[Float64]` `[8 bytes]` (9 bytes) | 8 bytes | 1 byte |

DTO-heavy payloads with many `Guid` / `DateTime` properties benefit the most — easily -10..-20% wire size on top of the existing -22..-33% advantage.

### CPU savings

Reader-side: SGen-generated code drops the per-property `ReadByte()` + `IsTinyInt` / `IsFixStr` / switch-case dispatch for primitive properties — direct `context.ReadInt32Unsafe()` / `ReadGuidUnsafe()` / etc. calls. Writer-side: drops the `WriteByte(typeCode)` per primitive. Effect amplifies on payloads with many primitive properties (Small/Medium benchmark cells) — independent of any JIT-vs-AOT measurement variance.

### Sketch — opt-in markerless lane, SGen-only

- New wire format flag (header `HeaderFlag_MarkerlessSchema = 0x10` or similar) → activates a property-positional lane.
- SGen-generated writer for `[AcBinarySerializable]` types: per primitive property, emits raw value (no marker). For variable-shape properties (string, complex, nullable, polymorphic) the existing marker-driven path stays.
- SGen-generated reader: per primitive property, calls `context.ReadInt32Unsafe()` / `ReadGuidUnsafe()` / etc. directly. Variable-shape properties keep the marker-read + dispatch.
- Heuristic: a property is markerless-eligible if `IsValueType && !IsNullable && type is in {int, bool, byte, short, long, float, double, DateTime, DateTimeOffset, Guid, TimeSpan, decimal}`. Anything else (string, list, nested object, nullable<T>) keeps the marker.

### Decision points

- **Backward compatibility**: header flag + version negotiation. Old readers see the flag set and either reject (clean fail) or fall back to marker-driven (if they support both lanes). Default `false` preserves current wire format.
- **Schema evolution fragility**: the markerless lane is positional, so adding/removing/reordering primitive properties breaks readers compiled against an older schema. Document this clearly — opt-in is for stable schemas only (DTO-frozen API contracts, internal SignalR messages with synchronized client/server SGen). For evolving schemas, marker-driven default stays.
- **Coordination with `ACCORE-BIN-T-S5L8`** (sentinel-length strings): the two could share the "no-marker per-call" infrastructure — markerless string lane uses sentinel-length VarUInt (null/empty/short distinguished by length value).

### Acceptance

- **Primary**: `WireMode = Fast` AcBinary wire size **≤ MemPack** across Small/Medium/Large/Deep AOT benchmark cells (AOT release-publish bench is the canonical measurement)
- Wire size: ≥ -10% on DTO-heavy payloads (Guid/DateTime-rich) vs current marker-driven format
- Round-trip on the markerless lane validated on representative DTO shapes (mixed primitive + string + nested object)
- Schema-evolution fragility documented in `BINARY_FEATURES.md` (alongside the existing `PropertySkip` / default-omission caveat from `ACCORE-BIN-I-D9Y2`)
- Opt-in flag with default `false` (preserves marker-driven default; consumers explicitly opt in for frozen-schema scenarios)

## ACCORE-BIN-T-V4N4: NativeAOT-specific inlining / codegen audit on hot UTF-8 path
**Priority:** P2 · **Type:** Performance · **Status:** Reverted (2026-05-07) — bench instability made the optimization signal unmeasurable · **Related:** `EncodeUtf8SinglePass`, `DecodeUtf8SinglePass`, `WriteStringWithDispatch`, `Utf8Transcoder` SIMD path

Hypothesis: NativeAOT (the benchmark target environment) does not match Tier 1 JIT optimization quality on the UTF-8 hot path, despite `[MethodImpl(AggressiveInlining)]` hints. Symptoms in 2026-05-05 / 2026-05-06 benchmarks:

- Repeated cell perzisztens 8-11% Compact ≤ MemPack lemaradás (Magyar content + repeated string pattern)
- Compact Ser/Deser cellán mozaikos eredmények run-to-run (4-7/10 cell wins, 3-6 noise/loss bands)
- Methodonkénti Compact gyorsítások a Medium/Large/Deep cellán **konzisztensek** (-22% to -28% vs MemPack), ami JIT/AOT inlining-eltérésnek tűnik a Repeated-en — ott a `WriteStringWithDispatch` short-lane sokszor hívódik 10× repeated string-en

**Suspect mechanisms (ranked by likelihood)**:

1. **AOT inline budget**. NativeAOT is more conservative than the Tier 1 JIT in respecting `AggressiveInlining` for large method bodies. `EncodeUtf8SinglePass` (~190 lines, 4 SIMD path + scalar), `DecodeUtf8SinglePass` (~120 lines), `GetUtf8ByteCount` (~120 lines) may exceed the AOT inline budget at hot call sites (`WriteStringWithDispatch` short-lane, `ReadString` decode callback). If the AOT compiler emits `call <method>` instead of inlining, every iteration of the Repeated 10-string loop pays the call overhead.

2. **`[Intrinsic]` `IsSupported` constant folding**. `Avx512BW.IsSupported`, `Vector512.IsHardwareAccelerated`, `Vector256.IsHardwareAccelerated`, `Vector128.IsHardwareAccelerated` should constant-fold per host on AOT. Verify via disasm — if any remain runtime checks, every iteration pays the branch cost (3 nested `if`-s in each Utf8Transcoder method).

3. **`Vector256.LessThan<ushort>` unsigned compare emulation**. No native `pcmpltw_unsigned` on AVX2; JIT/AOT lowers to `pminuw` + `pcmpeqw`. Cost amortized over many chars in long content but can dominate on short Magyar runs (`KözösCímke` ~6 runs of 2-3 chars). Less likely if (1) holds — the inlining hit dwarfs the per-instruction emulation cost.

4. **Method size cascade**. The Utf8Transcoder method bodies grew with the V4N3 `GetUtf8ByteCount` addition. Adjacent methods in the same source file may have lost inlining at SGen-generated callers due to AOT compilation-unit heuristics (file-locality affects inline cost models on some AOT codegen).

**Investigation steps (no code changes — diagnostic phase first)**:

1. NativeAOT publish dump:
   ```
   dotnet publish AyCode.Core.Serializers.Console -c Release -r win-x64 -p:PublishAot=true
   dumpbin /disasm <output.exe> > disasm.txt
   ```
2. Locate `EncodeUtf8SinglePass`, `DecodeUtf8SinglePass`, `GetUtf8ByteCount`, `CountUtf8Chars` symbols in the disasm
3. Verify constant folding on `IsSupported` checks — no run-time CMP/JMP at the path-selector branches; the dead branches eliminated
4. Verify inlining at `WriteStringWithDispatch` / `ReadString` callers — if `call <Utf8Transcoder.*>` instructions remain, inlining failed
5. Method size inspection — large method bodies hint at inline-eligibility issues; large prologue/epilogue at hot call sites is a tell
6. Cross-compare with Tier 1 JIT disasm (run with `DOTNET_TieredCompilation=0` + `DOTNET_TC_QuickJit=0` to force Tier 1, dump the JIT-tier disasm via WinDbg or `BenchmarkDotNet`'s `[DisassemblyDiagnoser]`) to confirm the gap is AOT-specific rather than algorithmic

**Possible fixes (Open until disasm confirms which apply)**:

- **A. Method split** — `EncodeUtf8SinglePass` → small dispatcher + per-tier inner methods (each Vector512 / Vector256 / Vector128 / scalar in its own AOT-inline-friendly small method). Same for `DecodeUtf8SinglePass`. The dispatcher stays small enough to inline at the hot call site; the dead-branch tier methods are never called on a given host.
- **B. `[MethodImpl(NoInlining)]` on cold tiers** — paradox tactic that can REDUCE the hot-path code emitted at the call site by preventing the AOT from speculatively considering the dead branches as inlining candidates.
- **C. Per-target ISA build** — if the benchmark environment has a fixed ISA (e.g. AVX2 baseline), use `<IlcInstructionSet>` in `csproj` to constant-fold the `IsSupported` checks at AOT compile time. Alternative: separate per-ISA AOT publish artifacts.
- **D. Manual hot-path inlining** — for the Repeated cell, hand-inline `EncodeUtf8SinglePass` short-string lane into `WriteStringWithDispatch` FixStr path (≤31 byte case). Trades code-size for hot-path speed.
- **E. Algorithm change** — if the AOT can't inline the SIMD bodies efficiently, a smaller scalar-only fast path for short strings (≤31 byte) bypassing the SIMD setup might be faster on AOT than on JIT (where Tier 1 is fine with the SIMD path inlined).

### Why P2

- Repeated benchmark cell is the canonical witness for the **i18n production deploy** narrative — public NuGet release narrative depends on parity-or-better against MemPack across all cells (cloud / desktop / mobile / Blazor WASM)
- AOT-specific tuning is high-leverage on the hot path — JIT-only optimizations will not match
- Disasm validation is the prerequisite for any of the fix directions; without it, any change is speculative and risks reintroducing 2c-style regression

### Acceptance

- Disasm report confirms (or refutes) inlining + constant-fold hypotheses on the hot UTF-8 path
- If hypotheses confirmed: the chosen fix delivers Repeated Compact Ser+Deser ratio ≤ 1.0 vs MemPack on the AOT benchmark target
- No regression on Small / Medium / Large / Deep cells (or net positive)
- Fix maintains cross-tier SIMD correctness (round-trip tests pass on all UTF-8 content classes); both `Utf8TranscoderTests` and the binary test suite stay green

### Trigger

- Pre-NuGet release: i18n claim cannot ship with an 8-11% gap on a representative cell
- Disasm + bench correlation step before any code change (no speculative refactoring)

### Resolution

Audit + targeted fix landolt 2026-05-07.

**Step 1 — disasm-elemzés** (`disasm.txt`, ~90 MB AOT-publish output):
- ✅ `Avx512BW.IsSupported` / `Vector{N}.IsHardwareAccelerated` **constant-folded** — csak 4 runtime check a teljes binary-ben (1 body + 3 call-site, kívül a Utf8Transcoder hot path-tól). Az AOT a target ISA szerint dead-branch-eliminálta.
- ✅ Reader tier-marker dispatch (`ReadStringSmall/Medium/Big`) **inline-olódott** a `TypeReaderTable` lambda-class static init-be — 0 method-call overhead a tier-on.
- ⚠️ **`WriteStringWithDispatch` NEM inline-olódott** — 3 generic specialization (`<ArrayBinaryOutput>`, `<AsyncPipeWriterOutput>`, `<BufferWriterBinaryOutput>`) különálló method body-val + 14+ `call <method>` instruction az `<ArrayBinaryOutput>` body-jában (a többi 2 specializációban hasonló volumen). Method size ~190 sor — meghaladja az AOT inline budget-et.
- ⚠️ **`ReadStringUtf8WithCharLen` NEM inline-olódott** — saját body, sok call-site.
- ❓ → ✅ `string.Create` callback `__DelegateCtor` — disasm szerint `test static; jne skip ctor` minta = **cache-elt static lambda**, lazy-init pattern. **0 hot-path overhead** (nem per-hívás alloc).

**Step 2 — method-split kísérlet** (15:13:39 bench):
- Writer split: dispatcher (`[AggressiveInlining]`) + `WriteStringSmallFast` (`[AggressiveInlining]`) + `WriteStringDispatchLong` (`[NoInlining]`) + `WriteStringFastWire` (`[NoInlining]`)
- Reader split: dispatcher (`[AggressiveInlining]`) + `ReadStringUtf8WithCharLenCore` (`[NoInlining]`)
- Bench: **regresszió** — Small Ser +29.6 pp, Repeated Ser +8.9 pp, Small Deser +16.6 pp.
- Disasm szerint a dispatcher + SmallFast **inline-olódott** (body symbol eltűnt) — code-bloat: 3 generic spec × ~30-50 SGen call-site × ~45 sor inlined kód = i-cache pressure a Repeated cell hot loop-on. Reader oldali dispatcher **NEM inline-olódott** (`[AggressiveInlining]` hint hatástalan), csak +1 call instruction.

**Step 3 — finomított fix** (15:29:21 bench, **Closed**):
- `WriteStringWithDispatch` dispatcher: **NO inline hint** (a fordítóra hagyva, AOT-ban stabilabb)
- `WriteStringSmallFast`: `[NoInlining]` (code-bloat eltünt — call-overhead-tel marad, de strukturálisan dedikált method)
- `WriteStringDispatchLong` + `WriteStringFastWire`: `[NoInlining]` cold path (megőrizve)
- `ReadStringUtf8WithCharLen` + `ReadStringUtf8WithCharLenCore` **összeolvasztva** vissza egy methoddá (split nem fizetett, +1 call eltünt)

**Bench (15:29:21) Compact vs MemPack arányok**:
- **Ser**: Small **0.915** (-8.5%), Medium 0.989 (≈), Large **0.915** (-8.5%), Repeated 1.019 (≈), Deep 0.981 (-1.9%) → 5/5 cell paritás-vagy-jobb
- **Deser**: Small 1.101 (+10.1%), Medium **0.953** (-4.7%), Large **0.894** (-10.6%), Repeated **0.962** (-3.8%), Deep **0.899** (-10.1%) → 4/5 cell win, csak Small +10%
- **Wire**: 5/5 cell -8% to -11% kisebb mint MemPack

**Tanulság**:
1. AOT-ban a `[AggressiveInlining]` **nem garantált** — a Writer dispatcher + SmallFast inline-olódott (code-bloat), de a Reader dispatcher NEM (hint hatástalan). A fordítóra bízás (no hint) stabilabb.
2. Method-split nem mindig nyer — a túl-aggresszív inline-olás code-bloat-ot okozhat (i-cache pressure), különösen sok SGen call-site mellett.
3. A `__DelegateCtor` cache-elt — `string.Create` callback nem hot-path overhead-forrás.
4. Strukturális struktúra megőrizve: `WriteStringDispatchLong` és `WriteStringFastWire` külön cold methodok (későbbi célzott optimalizációhoz alapot ad).

**Maradék gap**: Small Deser +10% — kis abszolút érték (~1 µs), nem release-blocker. A `ReadStringUtf8WithCharLen` body méretes (single method ~15 sor + lambda-state), AOT inline-budget határán. Tovább optimalizálható a V4N2 vagy W2C8 sprint-ben.

### Reverted (2026-05-07)

A V4N4 method-split — mind a 15:13:39 (`AggressiveInlining`) regressziós verzió, mind a 15:29:21 (`NoInlining`-on-SmallFast) finomított verzió — **visszavonva**. A subsequent benchmark futtatások (15:29:21 → 15:56:54 → ...) **drasztikus run-to-run varianciát** mutattak ugyanazon kódon: az AOT-codegen file-locality / inline-cost-modell mérés-érzékeny a `Utf8Transcoder.cs` body-méret változásaira, és a noise-floor a method-split feltételezett +1-3% Ser nyereségét eltakarja.

A revert visszaállítja a `WriteStringWithDispatch` egy-method állapotot (matches 09:39:09 baseline). A megőrzött elemek:
- **A irány packed-header store-ok** (`Unsafe.WriteUnaligned<ushort/uint/ulong>` Small/Medium/Big tier-on) — instruction-level optimalizáció, nem érintett az AOT-variance miatt
- **Overflow guard** (`O7G2` — `ThrowStringTooLong`) — defensive, különálló feature

A V4N4 audit **konklúziója** változatlan érvényes (constant-fold OK, reader tier-readers inline-olt a TypeReaderTable lambda-class static init-be, `__DelegateCtor` cache-elt). Az AOT inline-pressure-elemzés továbbra is releváns dokumentáció — csak a method-split mint fix nem volt mérhető-positív.

**Tanulság**: bench-driven optimalizáció csak akkor érvényesíthető, ha a noise-floor < a várható signal. AOT-on a bench-zaj jelentős (~5-15 pp run-to-run), ami a +1-3% perf-claim-eket eltakarja. **Profile-vezérelt** optimalizáció (CPU-profile + flame-graph + code-cache miss measurement) lenne a következő lépés, ha az inlining-pressure érdemi gap-ként marad.

**Re-evaluable as of 2026-05-07 per `ACCORE-BIN-T-D9X3`** — bench stabilization removes the noise-floor that made the original signal unmeasurable; retest before any code change.

**Obsoleted (2026-05-08) by `ACCORE-BIN-T-K7M3`** — the writer hot path no longer calls the custom `EncodeUtf8SinglePass` at all (`WriteStringWithDispatch` was switched to `Utf8.FromUtf16` BCL). The "AOT method-split / inlining audit" target (`Utf8Transcoder` body method-size in NativeAOT inline budget) is moot — the BCL `Utf8.FromUtf16` is a single static method with its own AOT-friendly inline footprint, and the audit's hypothesis space (Vector256 `IsSupported` constant-fold, lambda delegate cache) was correct for the prior code but no longer applies. The V4N4 disasm methodology remains a **valid technique** for future investigations of generic specialization / inline failures, but the specific hot-path target it analyzed is gone.

## ACCORE-BIN-T-L9Y3: FixArray marker tier — short-list count encoded in marker
**Priority:** P3 · **Type:** Wire-format optimization · **Status:** Open · **Related:** `Array` (66) marker, `VarUInt itemCount`, `ACCORE-BIN-T-H2Q6` marker reservation

Analog to `FixStr` — short list count (0-15) encoded in marker, eliminating the `VarUInt itemCount` byte for typical DTO collections (Tags, Categories, Items, Properties, Variations, etc. — any list whose size statistically lands in the 0-15 range).

### Wire format

**Current**: `[Array marker:1][VarUInt itemCount][items]` — header 2-6 byte
**FixArray**: `[FixArrayBase + N marker:1][items]` — header 1 byte (N = item count, 0-15)

Writer dispatch (in `WriteArray` / scan-pass list-writer equivalents):
- `itemCount ≤ 15` → `FixArrayBase + itemCount` marker (1 byte total header)
- `itemCount > 15` → existing `Array` marker + `VarUInt` count (2-6 byte total header)

### Marker reservation

**16 marker values** pre-reserved in the post-H2Q6 marker layout (see `ACCORE-BIN-T-H2Q6` "Marker address space reservation" table). The reservation guarantees that activating FixArray does NOT require another wire-format-version bump after H2Q6 lands at v3 — producers opt in to emit FixArray markers within the same v3 envelope, consumers extend their dispatch to decode them.

Activation steps when implementing:

1. Allocate `FixArrayBase` (16 contiguous values from the H2Q6-freed range)
2. Add `IsFixArray(byte marker)`, `DecodeFixArrayCount(byte marker)`, `EncodeFixArray(int count)` helpers in `BinaryTypeCode.cs`
3. Writer: branch in `WriteArray` and equivalent ScanPass list-writers, emit FixArray for `count ≤ 15`
4. Reader: extend marker dispatch in `ReadValue` / `SkipValue` / `ReadArray`
5. SGen: regenerate readers/writers with `IsFixArray` dispatch in the array-typed property paths
6. Round-trip tests for boundary `itemCount` values: 0, 1, 14, 15, 16, 17 (last tier transition)

### Why P3

- **Wire saving**: -1 byte per short list. Realistic per-cell estimates:
  - **Repeated** (10 OrderItem, ~50 list overall): ~50 byte / 28 KB = **~0.18%** wire reduction (marginal)
  - **Large** (5×5×5×10 nested, ~6000 list): ~6 KB / 118 KB = **~5%** wire reduction ✓
  - **Medium**: ~500 byte / 21 KB = **~2.4%** wire reduction
  - **Deep** (2×4×4×8 nested): similar to Medium, ~2-3% wire reduction
- **CPU saving**: marginal (~1-2 ns/list — `VarUInt` short-loop replaced by 1-byte marker decode). NOT a hot-path mover for the current Repeated-cell baseline gap.
- **Release-narrative value**: complements the post-H2Q6 wire-size advantage, particularly on deep-nested structures (Large benchmark). Sharpens the "smallest AND fastest" claim once the CPU gap closes via V4N2 Phase 3 + V4N4.

### Why not P2/P1 — and why not now

- The current `2026-05-06_13-10-30.LLM` baseline's primary problem is **CPU** (Compact +5-25% slower than MemPack on every cell), NOT wire size. FixArray addresses wire size, marginal CPU.
- Activation **after** H2Q6 + V4N2 Phase 3 + V4N4 is the natural sequence: CPU gap closes first, then wire-saver features sharpen the release narrative.
- The marker reservation lets us defer activation indefinitely without losing the address-space slot.

### Acceptance

- 16 marker values aligned in `BinaryTypeCode.cs` (`FixArrayBase..FixArrayMax`) with `IsFixArray`, `DecodeFixArrayCount`, `EncodeFixArray` helpers
- Writer + reader dispatch with boundary tests (count = 0, 1, 14, 15, 16, 17)
- SGen-regenerated readers/writers correctly dispatch via `IsFixArray` for array-typed properties
- Round-trip tests pass, no Ser/Deser regression vs current `Array` path
- Wire-size benchmark: ≥-2% on Medium, ≥-3% on Deep, ≥-4% on Large, no regression on any cell
- Documentation update in `BINARY_FORMAT.md` (new marker range + dispatch rules)

### Trigger

- After `ACCORE-BIN-T-H2Q6` lands (marker reservation must be active first)
- After CPU gap closes (V4N2 Phase 3 + V4N4) — wire-saver value clearer once "fast" is settled
- Pre-NuGet release housekeeping for the wire-size narrative (along with `S5L8` / `S2X9` if their scope justifies)

### Future extension (not part of this entry)

- **`FixDict` analog** — same pattern for `Dictionary` marker (67) with `kvCount` 0-15. Worth considering only if a benchmark workload demonstrates dictionary-heavy structures; the current bench data (Order DTOs) does not. **Defer until evidence.**
- **`FixArray 0-31`** — wider count range (32 markers). Marginal additional saving (16-31 elem list-ek ritkák); would consume nearly all freed marker space, leaving no slack for `S5L8`/`S2X9`. **Reject unless evidence warrants.**

## ACCORE-BIN-T-S6F2: Shift-mentes Small fast path in `WriteStringWithDispatch`
**Priority:** P3 · **Type:** Performance · **Status:** Reverted (2026-05-07, with V4N4 method-split) · **Related:** `WriteStringWithDispatch`, `BinaryTypeCode.StringSmall`, `ACCORE-BIN-T-V4N4`

The H2Q6 writer's post-encode tier choice runs a 3-way switch (`bytesWritten ≤ 255 → StringSmall`, `≤ 65535 → StringMedium`, `else StringBig`) and a header-write switch (3 / 5 / 9 byte) for every non-ASCII string. On the Repeated benchmark cell (Magyar content, ~10-15 char strings dominant) **99%+ of writes resolve to StringSmall** — the 3-way switch decision is statistically determinate from `charLength ≤ 63` alone (worst-case `charLength * 4 ≤ 252 ≤ 255` ⇒ Small tier guaranteed).

A specialized fast path for `charLength ≤ 63` could eliminate:
- The `int actualHeader; byte tierMarker;` runtime-resolved variables
- The 3-way `bytesWritten` switch
- The 3-way `actualHeader` header-write switch
- The `shift = reserveHeader - actualHeader` compute (always 0 in this branch)

Sketch:
```csharp
if (charLength <= 63)
{
    EnsureCapacity(3 + charLength * 4);
    var savedPos = _position;
    var encodeStart = savedPos + 3;
    var bytesWritten = Utf8Transcoder.EncodeUtf8SinglePass(value.AsSpan(), _buffer.AsSpan(encodeStart, charLength * 4));
    if (bytesWritten == charLength) { /* ASCII override — FixStrAscii inline */ }
    else
    {
        // StringSmall — 0 shift, inline header write (constant-folded)
        _buffer[savedPos] = BinaryTypeCode.StringSmall;
        Unsafe.WriteUnaligned<ushort>(ref _buffer[savedPos + 1],
            (ushort)(charLength | (bytesWritten << 8)));
        _position = savedPos + 3 + bytesWritten;
    }
    return;
}
// charLength > 63 → fall through to existing post-encode tier dispatch
```

### Why P3

- Repeated cell hot path benefit (~99% of writes on Magyar content are charLength ≤ 63)
- Estimated +1-3% Ser improvement on Repeated/Medium cells (where short non-ASCII strings dominate)
- Constant-folded tier choice + inline header write — no branch overhead vs. the generic post-encode path
- Trade-off: ~30 lines of duplicated specialized code; the generic post-encode path remains for charLength > 63 long-string scenarios

### Acceptance

- `WriteStringWithDispatch` Small fast path emits identical wire bytes as the generic path for `charLength ≤ 63` (round-trip parity)
- Benchmark on Repeated/Medium cells shows ≥ 1% Ser improvement vs. post-A-direction baseline (`2026-05-07_09-39-09.LLM` or later)
- No regression on Large/Deep cells (long-string path untouched)
- Round-trip tests pass on the boundary `charLength = 63` and `charLength = 64` cases

### Trigger

- After A-direction (header pack-write) bench result is conclusive
- Pre-NuGet release if the Repeated cell Compact-vs-MemPack Ser ratio still has measurable headroom

### Resolution

Integrált megvalósítás `ACCORE-BIN-T-V4N4` keretében (2026-05-07): a `WriteStringWithDispatch` 4-method-os split egyik tagja a `WriteStringSmallFast` — pontosan az S6F2 ide illeszkedő fast path. A 0-shift non-ASCII branch garantált (`charLength ≤ 63` ⇒ `bytesWritten ≤ 252 ≤ 255` ⇒ Small tier biztos, `reserveHeader = actualHeader = 3`).

Az inline-stratégia tanulsága (a V4N4 disasm-ből): a `WriteStringSmallFast` `[NoInlining]` jelölést kapott a végleges verzióban — az `[AggressiveInlining]` kísérlet code-bloat-ot okozott (3 generic spec × 30+ SGen call-site × inlined body = i-cache pressure a Repeated cell hot loop-on, +29.6 pp Ser regresszió a 15:13:39 bench-en). A `[NoInlining]`-tal az S6F2 logika érvényesül (constant-folded tier choice, 0 shift), csak +1 call instruction overhead-tel.

Bench (15:29:21): Compact Ser **5/5 cellán paritás-vagy-jobb** vs MemPack (Small -8.5%, Medium -1.1%, Large -8.5%, Repeated +1.9%, Deep -1.9%). Az S6F2 várt +1-3% Ser-javulás teljesült Small/Large cellákon, a Repeated/Deep paritás-szerű (a +1 call overhead kompenzálja a fast-path nyereséget rövid Magyar string-eken).

**Re-evaluable as of 2026-05-07 per `ACCORE-BIN-T-D9X3`** — together with the parent V4N4 method-split, the Small fast path is re-testable now that bench stabilization removes the noise-floor; retest before any code change.

## ACCORE-BIN-T-W2C8: WASM string-cache H2Q6 maximalizálás (`ReadStringUtf8Cached` MISS path)
**Priority:** P2 (WASM target) / P3 (otherwise) · **Type:** Performance · **Related:** `BinaryDeserializationContext.Read.cs::ReadStringUtf8Cached`, `ReadStringUtf8WithCharLen`, `Utf8Transcoder.DecodeUtf8SinglePass`

H2Q6's primary win is **1-pass decode** on the reader side: tier markers carry both `charLen` and `utf8Len`, so the reader allocates the target string with the known char count and decodes in a single pass via `string.Create(charLength, ..., DecodeUtf8SinglePass)`. This eliminates the `CountUtf8Chars` Pass 1 — the headline V4N3/H2Q6 win.

**The WASM string-cache path bypasses this win.** When `_useStringCaching` is true (Blazor WASM target), `ReadStringUtf8WithCharLen` dispatches to `ReadStringUtf8Cached(byteLength)` for short strings. On cache HIT, the cached instance is returned (zero decode — already optimal). **On cache MISS, the current `ReadStringUtf8Cached` falls back to `Utf8NoBom.GetString(slice)` — the BCL kétpasszos UTF-8 decoder.** The H2Q6 1-pass decode benefit is lost on every cache MISS.

Per-cell impact estimate on a WASM workload with hot-path strings (typical Blazor SignalR DTO traffic):
- Cache HIT rate ~30-50% on repeated property names + tags + categories
- Cache MISS rate ~50-70% on first occurrences + unique values
- MISS path = `Utf8NoBom.GetString` BCL call (virtual dispatch + EncoderFallback overhead) instead of `string.Create(charLength, ..., DecodeUtf8SinglePass)`

### Implementation outline

`ReadStringUtf8Cached` accepts both `charLength` and `byteLength` (or just compute charLength from the cache check / decode result). Cache HIT: `cached.Length == charLength` invariant check (UTF-16 char count, not UTF-8 byte count) + ASCII verification. Cache MISS: replace `Utf8NoBom.GetString(slice)` with `string.Create(charLength, (Buffer, Pos, Len), static (chars, state) => DecodeUtf8SinglePass(state.Buffer.AsSpan(state.Pos, state.Len), chars))`.

Cross-check: the existing `ComputeStringHashFull(slice)` and `VerifyAsciiUtf8Match(cached, slice)` operate on the raw UTF-8 bytes — these stay unchanged. Only the MISS-side string materialization needs the H2Q6-aware refactor.

### Why P2 (WASM-target) / P3 (otherwise)

- The non-WASM benchmark host (x64) doesn't enable `_useStringCaching` by default, so this optimization is invisible on the current bench
- On Blazor WASM, all interning + repeated-string-cached deserialization currently pays the BCL decode tax on cache MISS
- Estimated +5-15% Deser improvement on WASM workloads with significant cache MISS rate
- Direct extension of the H2Q6 win to the WASM execution profile

### Acceptance

- `ReadStringUtf8Cached` cache MISS path uses `string.Create(charLength, ..., DecodeUtf8SinglePass)` — no BCL `Utf8NoBom.GetString` on MISS
- Round-trip tests pass on cached + uncached short-string scenarios across all UTF-8 content classes (ASCII / Hungarian / CJK / emoji)
- WASM-target benchmark (Blazor profile) shows ≥ 5% Deser improvement vs. pre-W2C8 state on a representative hot-string-heavy DTO workload
- Cache HIT path performance unchanged (already optimal — no decode)
- Cache eviction / capacity behavior unchanged

### Trigger

- Pre-NuGet release if Blazor WASM is a primary supported scenario in the release narrative
- Or: when a WASM-fókuszú benchmark workload becomes the active perf measurement target

## ACCORE-BIN-T-F3W6: Dedicated FastWire string marker (split mode-shared `StringSmall`)
**Priority:** P3 · **Type:** Performance · **Related:** `WriteStringWithDispatch` FastWire branch, `ReadStringSmall` FastWire branch, `BinaryTypeCode.StringSmall`, H2Q6 marker reservation

The H2Q6 marker layout currently shares `StringSmall` (=91) between Compact and FastWire modes:
- **Compact** emits `[91][charLen:8][utf8Len:8][UTF-8 bytes]`
- **FastWire** emits `[91][VarUInt charCount][UTF-16 raw bytes]`

The reader dispatches on `context.FastWire` inside `ReadStringSmall`. Correct (the deserializer's mode is fixed per operation), but the mode-shared marker forces runtime branching at hot points:

- **Writer**: `if (FastWire)` at the top of `WriteStringWithDispatch` runs on every string write — runtime check on a path-dominant (Compact) call site
- **Reader**: `if (context.FastWire)` inside `ReadStringSmall` runs on every short non-ASCII string deserialization — Compact-side waste
- **SGen template**: every regenerated reader contains the FastWire-aware `case StringSmall:` block (more code per type, larger AOT binary)
- **JIT/AOT inlining**: the larger `WriteStringWithDispatch` / `ReadStringSmall` method bodies may exceed inline budgets at hot call sites — particularly under NativeAOT

A dedicated `StringFastWire` marker (one value from the H2Q6-freed 106-134 range — proposed allocation: **131**) splits the path:

- **Compact** stays on `StringSmall` (=91) → `ReadStringSmall` becomes Compact-only (no `if (FastWire)` branch, smaller method body)
- **FastWire** uses new `StringFastWire` → dedicated `ReadStringFastWire` reader, FastWire-only logic
- Writer's FastWire branch emits `StringFastWire` instead of `StringSmall`

### Wire format compatibility

The marker swap is internally consistent within the v3 envelope — producers that opt in to the dedicated FastWire marker emit it; readers expanded to handle both `StringSmall` and `StringFastWire` (transitional). Once all producers emit the dedicated marker, the old mode-shared dispatch in `ReadStringSmall` can be removed.

### Why P3 — "minden apró % számít"

- Estimated **+0.5-1% Ser** (writer branch elimination on Compact path)
- Estimated **+0.5-1% Deser** (reader smaller method body, better JIT/AOT inline-eligibility on Compact path; FastWire reader gets a tight dedicated path too)
- Compounds with other micro-opts across the hot path — small percentages add up
- Marker-space cost: **1 reserved value** consumed (general-reserve count drops from 4 to 3 in the H2Q6 reservation table)
- Risk: low — mechanical split; round-trip tested against both wire-format variants

### Implementation outline

1. `BinaryTypeCode.StringFastWire = 131` constant + helper updates (`IsString` range check + dispatch)
2. `WriteStringWithDispatch` FastWire branch emits `StringFastWire` (was `StringSmall`)
3. New `ReadStringFastWire<TInput>` static reader — `[VarUInt charCount][UTF-16 bytes]` decode, no Compact-mode branching
4. `ReadStringSmall<TInput>` simplified — Compact-only, drops `if (context.FastWire)` branch
5. `TypeReaderTable[StringFastWire]` registration
6. `SkipValue` `case StringFastWire:` — same skip layout as `StringSmall` FastWire branch (charCount VarUInt + 2 × charCount bytes)
7. SGen template `EmitReadString` — new `case StringFastWire:` block (FastWire-only branch); `case StringSmall:` simplified to Compact-only
8. Round-trip tests: separate FastWire and Compact wire format coverage

### Acceptance

- Round-trip parity on both Compact and FastWire wire formats (existing tests pass)
- Benchmark on FastWire mode shows ≥ 0.5% improvement vs. mode-shared baseline
- Compact mode shows no regression (likely marginal gain from simpler `ReadStringSmall`)
- AOT-published binary shows reduced generated reader size per `[AcBinarySerializable]` type (one less case-block + branch)
- Marker-space documented: `BinaryTypeCode.cs` reservation comment + H2Q6 entry's reservation table updated to reflect the F3W6 allocation

### Trigger

- Pre-NuGet release if every measurable percentage point on the Compact hot path matters for the "fastest" narrative
- Or: when the Compact/FastWire branch profile shows up in a NativeAOT inlining audit (`ACCORE-BIN-T-V4N4`)

### Roll-back fallback

If a future marker-space crunch arises (additional H2Q6 tiers, new compression markers, etc.), `F3W6` can be reverted by switching the writer back to emitting `StringSmall` on FastWire and re-introducing the mode-shared dispatch in `ReadStringSmall`. The original design is correctness-equivalent — the dedicated marker is purely an optimization. **If marker gondunk lesz, kivesszük.**

## ACCORE-BIN-T-B1D5: BenchmarkDotNet release-quality measurement project
**Priority:** P2 · **Type:** Tooling / release-narrative · **Status:** Open · **Related:** `AyCode.Core.Serializers.Console` (existing custom bench), NuGet release-narrative

The current `AyCode.Core.Serializers.Console` is a hand-rolled microbenchmark — fast dev-iteration loop (30-90s per run, custom markdown output, internal TestDataSet structure). It serves the inner optimization cycle well, **but is not industry-standard** for the public NuGet release narrative.

A parallel `BenchmarkDotNet`-based project would close that gap:

- **Industry-standard credibility**: BenchmarkDotNet is the canonical .NET benchmarking framework — MemoryPack, MessagePack, System.Text.Json all use it for their published numbers. AcBinary results expressed in BDN format are **directly comparable** to MemPack's own release notes.
- **Statistical rigor**: outlier detection (Tukey's fences), interquartile range, confidence intervals, multi-process iteration runs. The current custom bench reports median-of-5; BDN reports the full distribution + variance band — the difference between "looks fast on my machine" and "demonstrably fast under controlled conditions".
- **NuGet release surface**: BDN markdown tables drop straight into release notes / blog posts / NuGet `README.md` / `BINARY_FEATURES.md` "Performance vs MemoryPack" section. GitHub-friendly format, screenshot-friendly, reviewer-credible.
- **Diagnostic-plugin integration**:
  - `[MemoryDiagnoser]` — allocation per iteration (already a hot question for the Repeated cell)
  - `[EventPipeProfiler]` — CPU profile collection during the bench run, exportable to speedscope flame-graph
  - `[DisassemblyDiagnoser]` — per-method disasm dump, parallel to the manual `dumpbin` workflow used in V4N4
  - `[ThreadingDiagnoser]` — context switches, lock contention (relevant if pool-contention shows up under load)
- **Multi-runtime / multi-job**: a single project benchmarks against `RuntimeMoniker.Net90` (JIT) and `RuntimeMoniker.NativeAot90` simultaneously — same-shape table side-by-side.
- **CI integration potential**: BDN result format is machine-readable (JSON/CSV), enabling regression detection on PR diffs (later sprint).

### Implementation outline

1. **New project**: `AyCode.Core.Serializers.Benchmark` (or `.Bdn`) — separate csproj for clean BDN dependency isolation. AOT-publishable for the AOT job.
2. **TestDataSet bridge**: reuse the existing `TestDataFactory` / `TestDataSet` types from `AyCode.Core.Tests.TestModels` so the data-shape is identical to the custom bench.
3. **Benchmark class skeleton**:
   ```csharp
   [MemoryDiagnoser]
   [SimpleJob(RuntimeMoniker.Net90, baseline: true)]
   [SimpleJob(RuntimeMoniker.NativeAot90)]
   public class StringSerializationBenchmark
   {
       [Params("Small", "Medium", "Large", "Repeated", "Deep")]
       public string DataSet { get; set; } = "Small";

       private object _data = null!;
       private byte[] _compactWire = null!;
       private byte[] _mempackWire = null!;

       [GlobalSetup]
       public void Setup()
       {
           _data = TestDataFactory.Create(DataSet);
           _compactWire = AcBinarySerializer.Serialize(_data, AcBinarySerializerOptions.FastMode);
           _mempackWire = MemoryPackSerializer.Serialize(_data);
       }

       [Benchmark(Baseline = true)] public byte[] MemPack_Ser() => MemoryPackSerializer.Serialize(_data);
       [Benchmark] public byte[] AcBinary_Compact_Ser() => AcBinarySerializer.Serialize(_data, AcBinarySerializerOptions.FastMode);
       [Benchmark] public object? MemPack_Deser() => MemoryPackSerializer.Deserialize<TestOrder>(_mempackWire);
       [Benchmark] public object? AcBinary_Compact_Deser() => AcBinaryDeserializer.Deserialize<TestOrder>(_compactWire);
   }
   ```
4. **Multi-cell coverage**: separate benchmark classes per workload-shape (StringSerializationBenchmark, ObjectGraphBenchmark, NestedDeepBenchmark) — clean grouping in BDN output.
5. **NativeAOT-job config**: `<PublishAot>true</PublishAot>` conditionally (mirroring `Console` project pattern); BDN's NativeAOT job auto-publishes the bench-runner.
6. **Output**: GitHub-flavored Markdown export → `docs/BINARY/BENCHMARK_RESULTS.md` (or similar), versioned in the repo.

### Why P2 (pre-NuGet release)

- NuGet release narrative ("AcBinary fastest AND smallest binary serializer for .NET i18n payloads") needs **credible, industry-standard numbers**. Custom bench → "trust me, my numbers"; BDN → "here are the variance bands and the methodology".
- Direct comparison surface against MemPack's published BDN numbers (head-to-head on the same framework).
- Diagnostic-plugin integration (`[MemoryDiagnoser]` + `[EventPipeProfiler]`) opens up further targeted optimization work without separate tooling.

### Acceptance

- New `AyCode.Core.Serializers.Benchmark` project compiles + runs cleanly on both JIT (net9.0) and NativeAOT
- Reuses existing `TestDataFactory` / `TestDataSet` types — no test data duplication
- Produces a markdown table per workload-shape covering: MemPack baseline + AcBinary Compact + (optionally) AcBinary FastWire, both Ser and Deser
- BDN output saved to `docs/BINARY/BENCHMARK_RESULTS.md` (versioned per release)
- README.md / `BINARY_FEATURES.md` references the BDN-measured performance claim with the methodology link

### Trigger

- Pre-NuGet release: when the optimization sprint cluster (V4N2 / W2C8 / etc.) settles and the perf state is release-stable
- Or: when a credibility-sensitive presentation surface emerges (blog post, conference talk, GitHub README)

### Coexistence with the custom bench

The custom `Console` bench **is not replaced** — it remains the dev-iteration tool (fast feedback loop, 30-90s runs, hand-tuned markdown for chat-paste). BDN is the **release-grade** bench (3-10 min runs, statistical rigor, NuGet release output). Different tools for different audiences.

## ACCORE-BIN-T-P3X7: Profile-driven Compact-mode Ser optimalizációs roadmap (post-K7M3 hot-path analysis)
**Priority:** P2 · **Type:** Performance roadmap · **Status:** Open · **Related:** `ACCORE-BIN-T-K7M3` (BCL UTF-8 transcoder switch — előfeltétele), `ACCORE-BIN-T-D9X3` (bench stabilization), `ACCORE-BIN-T-S2X9` (markerless schema lane — primitív property-marker már kivezetve a SGen-ben), `ACCORE-BIN-T-V4N4` (audit methodológia hivatkozás)

A 2026-05-08 VS Performance Profiler session (4 sec range, AcBinary FastMode Serialize, Latin1Long charset, FastWire mode) konkrét hot-path-decomposition-t adott a K7M3 BCL-csere utáni állapotról. A string-encoding már nem akadály (a `Utf8.FromUtf16` SIMD-tuned), a fennmaradó AcBinary-specific overhead azonosítható.

### Profile session adatok (Self CPU%)

| Self CPU% | Function | Category |
|---|---|---|
| 39.77% | `System.Buffer._Memmove` | Közös MemPack-kel (UTF-16 raw + return-time `byte[]`-copy) — **NEM AcBinary-spec** |
| **10.03%** | `AcBinarySerializer.Serialize<T>` | Top-level (context-acquire, type lookup, return-alloc) |
| **7.48%** | `TestMeasurementPoint_GeneratedWriter.WriteProperties` | SGen template (legkisebb levél típus, ~12500 hívás Large cellán) |
| **5.31%** | `WriteStringWithDispatch` | String hot path |
| **3.23%** | `TestMeasurement_GeneratedWriter.WriteProperties` | SGen |
| **1.66%** | `WriteVarUIntMultiByteUnsafe` | VarUInt int-property encode |
| 1.10% | `TestPallet_GeneratedWriter.WriteProperties` | SGen |
| 0.39% | `TestOrderItem_GeneratedWriter.WriteProperties` | SGen |
| 0.32% | `SharedUser_GeneratedWriter.WriteProperties` | SGen |
| 0.05% | `ArrayBinaryOutput.Grow` | Buffer-grow (ritka, kicsi probléma) |

**Total SGen `WriteProperties` Self CPU**: ~12.6% — a leg nagyobb AcBinary-specific surface.

A `AcBinarySerializer.Serialize<T>` line-szintű drill-down (`AcBinarySerializer.cs:312-335`):
- `WriteObject(value, wrapper, context, 0)` Total: 28.05% — a teljes serializációs fa (SGen + Writer hot path)
- `context.Output.ToArray(context._buffer, context._position)` Total: **47.37%** — final `byte[]`-alloc + content-memcpy (= a 39.77% `_Memmove` Self nagy része)

### MemPack-összehasonlítás (referenciaként)

A MemPack `Serialize<T>(T value)` mechanizmus:
1. **`[ThreadStatic]`** writer-state — nincs pool-bérlés, nincs lock, nincs concurrent dictionary lookup
2. **`ReusableLinkedArrayBufferWriter`** — linked chunk-list (4 KB → 8 KB → 16 KB geometriai); buffer-grow = új chunk hozzáadása, **nincs memcpy a régi adaton**
3. **`ToArrayAndReset()`** — végén alloc + chunks → byte[] memcpy (közös overhead az AcBinary-vel)

Az AcBinary `AcquireArrayOutputContext(options)` pool-bérlés + lineáris `byte[]` `Array.Resize` + `Output.ToArray(...)` — két memcpy-cost (grow + return), de a grow ritka.

### Sorrendezett optimalizációs ötletek

#### A. SGen `WriteProperties` — ensure-capacity batching (várt: -1-3pp Ser, **revíziós becslés**)

Jelenlegi SGen-template per-property emit (mindenenkit külön ensure):
```csharp
context.WriteVarInt(obj.Id);                    // ensure(5) + write(1-5)
context.WriteByte(BinaryTypeCode.Object);        // ensure(1) + write(1)
context.WriteVarInt((int)obj.Status);            // ensure(5) + write(1-5)
context.WriteRaw(obj.Weight);                     // ensure(8) + write(8)
```

Csoportosított ensure pattern:
```csharp
context.EnsureCapacity(maxBytesForGroup);        // worst-case sum, 1× hívás
context.WriteVarIntUnsafe(obj.Id);                // no ensure (csak buffer write)
context.WriteByteUnsafe(BinaryTypeCode.Object);   // no ensure
context.WriteVarIntUnsafe((int)obj.Status);
context.WriteRawUnsafe(obj.Weight);
```

A `AcBinarySourceGenerator.cs` `WriteProperties` template-jét kell módosítani:
1. Property-listából contiguous primitív csoportok kinyerése (Object/Collection property-knél megszakítva — mély rekurzió, méret nem előre kiszámítható)
2. Csoportonként worst-case-size compute compile-time-on (a primitív type-ok mérete fix vagy worst-case ismert)
3. Egyetlen `EnsureCapacity(sum)` + bulk `*Unsafe` write-ok

`*Unsafe` írók szükségessége: `WriteVarUIntUnsafe` már létezik. **`WriteByteUnsafe`, `WriteRawUnsafe<T>`** valószínűleg hozzá kell adni a `BinarySerializationContext`-hez.

**Becslés-revízió (2026-05-08)**: az eredeti -4-6pp becslés felső volt. Egy `EnsureCapacity` inline-olva ~1-2 ns/call (a hot path-on a branch-prediction perfekt — sosem jut el a Grow-hoz). 10 property × 1.5 ns = ~15 ns / object megtakarítás batch-eléssel — Latin1Long Large cell 1250 instance × 13 ns = ~16 µs / 120 µs Ser ≈ **~13% felső**, de **csak az ensure-szám csökkenéséből**. A SGen `WriteProperties` Self CPU 12.6%-a **NEM csak** ensure-check; tartalmaz `HasPropertyFilter` branch-check, null-check + depth-check dispatch, `Unsafe.As<T>` cast, etc. — lásd **F**. Az ensure-batching önmagában reálisan **1-3pp Ser javulás**.

**Wire-formátum változatlan**, backward-kompatibilis, kis kockázat. Hatás minden cellán mérhető (TestOrder cell-szerkezet ~100+ primitív property per Object-instance).

#### B. `WriteStringWithDispatch` Compact ág batch-write (várt: -1-2pp Ser)

A FastWire ágat már `K7M3`-ban + a 2026-05-08 batch-write fixxel egyetlen ensure + direct-write-ra alakítottuk. A **Compact ág** ugyanaz a 3-step pattern (post-encode tier-shift `CopyTo` ha `actualHeader < reserveHeader`, plus header-write a tier alapján). A Compact ágon is alkalmazható batch-write — egyetlen `EnsureCapacity` a worst-case-tier-szel + direct header-write a `Utf8.FromUtf16` után.

#### C. Thread-static context (várt: -2-4pp Ser, NAGY refactor)

A `AcquireArrayOutputContext(options)` pool-bérlés overhead-jét mérsékelheti a MemPack `[ThreadStatic]` mintázat. A jelenlegi pool-bérlés:
- Pool dictionary lookup (lehet, lock-os)
- Context-state init / reset minden hívásnál

Thread-static cseréje:
- Per-thread cached context, nincs lock
- Context-reset minden hívásnál ugyanaz, de a `state` allokáció egyszer fut

**Refactor szempontok**:
- A `BinarySerializationContext` state-tárolása nem thread-safe önmagában — pool-bérlés vagy thread-static mind a single-thread haszálatot biztosítja
- Az `options` paraméter érintheti a state-init logikát — multi-options scenárió esetén a thread-static state-t reset-elni kell
- Concurrent serialize hívások (több thread egyidejű) — minden thread saját state-tel rendelkezne; nincs cross-thread sharing igény

#### D. Linked-array buffer chunk strategy (kicsi hatás, NAGY refactor)

A MemPack `ReusableLinkedArrayBufferWriter` linked chunk-list helyettesíti a lineáris `byte[]`-grow stratégiát. Buffer-grow = új chunk hozzáadása (no memcpy a régi adaton).

**A profile szerint a `ArrayBinaryOutput.Grow` Self CPU csak 0.05%** — a buffer-grow ritkán fut, a default kapacitás elég nagy a Large cell-hez. **Kicsi hatás, nagy refactor**. Alacsony prioritás.

#### F. SGen `HasPropertyFilter` lift-out a `WriteProperties` method elejére (várt: -2-4pp Ser)

A jelenlegi SGen-template **minden property-emit előtt** ellenőrzi a property-filter-t:
```csharp
public void WriteProperties<TOutput>(object value, ...)
{
    var obj = Unsafe.As<TestPallet>(value);

    if (context.HasPropertyFilter)                     // ← MINDEN property-en check!
    {
        var fc_Category = new BinaryPropertyFilterContext(obj, ..., "Category", ...);
        if (!context.PropertyFilter!(in fc_Category)) {
            context.WriteByte(BinaryTypeCode.PropertySkip);
            goto skip_Category;
        }
    }
    if (obj.Category == null) context.WriteByte(BinaryTypeCode.PropertySkip);
    else if (depth > context.MaxDepth) context.WriteByte(BinaryTypeCode.Null);
    else { context.WriteByte(BinaryTypeCode.Object); ...WriteProperties... }
    skip_Category:;

    if (context.HasPropertyFilter) { /* same for Inspector */ }   // ← újra!
    // ... 10× ismétlés property-listán
}
```

A `HasPropertyFilter` per-property branch-check **TestOrder benchmark workload-on mindig false** (a benchmark nem használ property-filter-t). De a check minden property-en lefut — kód-cache-ben benne van, branch-predict ugyan jó, **mégis CPU cycle**.

Optimalizáció — kétpályás SGen kódgenerálás:
```csharp
public void WriteProperties<TOutput>(object value, ..., int depth)
{
    var obj = Unsafe.As<TestPallet>(value);

    if (context.HasPropertyFilter)
    {
        WritePropertiesWithFilter(obj, context, depth);    // ritka path — full per-property check
        return;
    }

    // Fast path — NO filter check anywhere
    if (obj.Category == null) context.WriteByte(BinaryTypeCode.PropertySkip);
    else if (depth > context.MaxDepth) context.WriteByte(BinaryTypeCode.Null);
    else { ... }
    // (no skip_Category goto — never needed)

    context.WriteVarInt(obj.Id);                       // primitív, no filter check
    // ... rest of properties without HasPropertyFilter check
}

// Külön emit-elt method ritka path-ra:
private static void WritePropertiesWithFilter<TOutput>(TestPallet obj, ..., int depth)
{
    // Full per-property filter-aware kód (the current behavior)
}
```

A `AcBinarySourceGenerator.cs`-t kell módosítani:
1. A `WriteProperties` method elején egyetlen `HasPropertyFilter` check
2. Két különböző code-path emit:
   - **Fast path** (default — no filter): nincs per-property `if (context.HasPropertyFilter)` check, nincs filter-context allokáció + lambda-call, nincs `goto skip_X`
   - **Slow path** (filter aware — separate static method): a jelenlegi viselkedés

**Várt nyereség**: a fast path ~10 elimináció / object × 1-2 ns / branch ≈ ~15-20 ns / object. Latin1Long Large cell 1250 instance × 18 ns = ~22 µs / 120 µs Ser ≈ **~18% felső becslés**; reálisan **2-4pp Ser javulás** (a kód-bloat növekedés és a JIT inlinelés-ráhatás miatt mérséklődik).

**Kombinálható az A-val**: az **A + F** együtt **3-7pp javulás** célozható meg — a SGen `WriteProperties` 12.6% Self CPU jelentős csökkenése.

**Wire-formátum változatlan**, kód-méret kicsivel nő (két path-ot generál minden type-on), de a fast path a JIT-tel jobban inlinelhető.

#### G. SGen `WriteProperties` null/depth/object-ref kombinálás (kapcsolt az F-hez)

A komplex (Object) property-knél a 3-ágú dispatch:
```csharp
if (obj.X == null) context.WriteByte(BinaryTypeCode.PropertySkip);
else if (depth > context.MaxDepth) context.WriteByte(BinaryTypeCode.Null);
else { context.WriteByte(BinaryTypeCode.Object); X_GeneratedWriter.Instance.WriteProperties(...); }
```

Ez minden komplex property-en fut. Lehetséges optimalizáció: a `depth > MaxDepth` check egy method-szintű branch-szé alakítás (egyszer ellenőrizni a method elején, aztán a property-szintű ágat egyszerűsíteni). De ez **kis hatás** és a `MaxDepth` jellemzően nem érintő (a legtöbb workload-on `depth < MaxDepth`).

Alacsony prio, F-tel kombinált.

#### E. `WriteVarUIntMultiByteUnsafe` (1.66% Self) → fix-int (várható: -1pp Ser, **NEM javasolt önmagában**)

A `WriteVarInt` (signed int property-encode, ZigZag + VarUInt) kódolás a SGen-template-ekben gyakori (Id, Status, TrayCount, stb.). A multi-byte ág 1.66% Self CPU.

Fix-int (4 byte) cseréje wire-méret-növekedéssel jár (kis int-eken +3 byte / property), ami a wire-formátum kompaktság-előnyét rontja. **Csak `ACCORE-BIN-T-S2X9` markerless lane kontextusban** érdemes — ahol a property-marker eltávolításával együtt fix-int kicserélése wire-szempontból kompenzálódik.

### Közös, NEM AcBinary-spec overhead — nem optimalizálható

A `Buffer._Memmove` 39.77% Self CPU + a `Output.ToArray()` 47.37% Total **a return-time `byte[]`-alloc + content-memcpy**, ami minden `byte[] Serialize(T)` hívásnál fut. **Mindkét engine fizeti** (MemPack `ToArrayAndReset()` is alloc + memcpy a chunkokból). Az API contract (`byte[] Serialize(T)`) miatt elkerülhetetlen.

**Aki teljesítményt akar**, használja a `IBufferWriter<byte>` overload-ot (`AcBinaryBufferWriterBenchmark` vs `MemoryPackBufferWriterBenchmark` apples-to-apples a benchmarkban — mindkét engine ugyanezt csinálja).

### Acceptance (per-section)

- **A** (SGen ensure-batching): Latin1Long FastWire bench AcBinary Ser delta vs MemPack -1-3pp javulás minden cellán
- **F** (HasPropertyFilter lift-out): Latin1Long Ser delta -2-4pp; **A + F együtt** SGen `WriteProperties` Self CPU ≤ 8% (jelenleg ~12.6%)
- **G** (null/depth/object-ref kombinálás): kis hatás, F-tel kombinált
- **B** (WriteStringWithDispatch Compact batch-write): Latin1Long Compact bench AcBinary Ser delta vs MemPack ≤ +5% minden cellán
- **C** (Thread-static context): `Serialize<T>` Self CPU ≤ 6% (jelenleg ~10%)
- **D** (Linked-array): nem prioritás — buffer-grow Self CPU már ≤ 0.05%
- **E** (VarInt → fix-int): csak az `S2X9` markerless lane sprint kontextusában mérni

### Sorrend

1. **A + F kombinálva** — SGen `WriteProperties` template átfogó refactor (ensure-batching + HasPropertyFilter lift-out + esetleg G null/depth-combine). Együtt **~3-7pp Ser javulás** várt minden cellán. Izolált változtatás csak `AcBinarySourceGenerator.cs`-en, wire-format változatlan.
2. **B** — ~1-2pp javulás, ugyanaz a pattern mint a `K7M3` FastWire batch-write
3. **C** — ~2-4pp, de NAGY refactor (thread-safety, pool semantics felülvizsgálat)
4. **D** — alacsony prioritás (kis hatás, nagy refactor)
5. **E** — csak `S2X9` kontextusban

### Trigger

- **A + F** → most azonnal implementálható; ezek a SGen template-en belül kombinálandók (egyetlen template-átdolgozás kétségtelenül jobb mint külön refactor-körök). Minden továbbai mérés ettől függ.
- **B** → A+F után, hasonló pattern alkalmazása más writer-helyen
- **C** → ha a Serialize<T> Self CPU 10% továbbra is dominál A+F+B után
- **D, E** → opcionális, az A/F/B/C eredmények alapján

## ACCORE-BIN-T-Q5T2: Önleíró wire-formátum — duplikált object-marker-ek + UTF-16 string marker (per-type/property encoding choice)
**Priority:** P2 · **Type:** Architecture / Performance · **Status:** Open · **Related:** `ACCORE-BIN-T-P3X7` (profile-driven roadmap — kis-adat slowdown diagnózis), `ACCORE-BIN-T-K7M3` (BCL UTF-8 transcoder — előfeltétele), `ACCORE-BIN-T-S2X9` (markerless schema lane), `ACCORE-BIN-T-V4N2` (UTF-8 SIMD)

A 2026-05-08 design-session során merült fel mint válasz a kis-adat-slowdown problémára és az `if (FastWire)` / `if (UseMetadata)` runtime-branch-ek széles jelenlétére. Cél: a wire-mode kivezetése a globális header-ből, **per-object/per-property encoding-szabadság** attribute-tal, megőrizve a SGen↔Runtime wire-kompatibilitást.

### LLM Context (cold-start)

Egy fresh session olvasásához ez a kontextus elég:

**Wire-modell**: AcBinary két párhuzamos serializációs path-ot futtat — **SGen** (compile-time generált, `[AcBinarySerializable]` típusokra) és **Runtime** (reflection + `Expression.Compile`). **Mindkettő ugyanazt a wire-t produkálja és olvassa** (interop garancia, `BINARY_SGEN.md` "Hybrid Execution Model").

**Markerless body**: object scope-on belül a primitív property-k (int, long, double, …) **közvetlenül** írnak a wire-be, marker-byte nélkül. A reader a sorrendet compile-time schema-ból (SGen) vagy `OrderedProperties` metadata-ból (Runtime) tudja. A wire object-prefix-szel kezdődik (1-byte marker), majd markerless body.

**Meglévő object-marker család** (`AcBinarySerializer.BinarySerializationContext.cs` writer-ek + `AcBinaryDeserializer.cs` reader-dispatch switch):
- `Object` — sima first-occurrence
- `ObjectWithTypeName` — polimorf (`runtimeType != declaredType`)
- `ObjectFullMarkerIId` / `ObjectFullMarkerAll` — `RefHandling=IId|All` first-occurrence
- `ObjectRef` / `ObjectRefIId` — subsequent (csak ID, **NEM duplikálódik** — nincs primitív property körülötte)

**OPT-OUT minta** (jelenlegi konvenció): default SGen flexibilis — minden runtime-branch-et generál (pl. `if (context.UseRefHandling)`). Class-attribute disable-eli a feature-t → SGen omitti a branch-et → drasztikus optimum. Q5T2 ezt a mintát terjeszti ki **encoding-választásra**.

**Naming-konvenció**: PascalCase, suffix-variánsok (`Object` → `ObjectVarUInt`, `String` → `StringUtf16`). NEM `Object_NoZZ`, NEM `ObjVU`.

### Motiváció

A jelenlegi `AcBinaryOptions.WireMode` (FastMode vs Compact) **payload-szintű globális flag**:
- A kódban sok `if (FastWire) { ... } else { ... }` branch (lásd `WriteVarInt` 514. sor, `WriteStringWithDispatch`, `WriteValueNonPrimitive`, property-writers)
- A fejlesztő nem optimalizálhat granuláris szinten (pl. `[NoZZ]` egy hot type-ra, default másnak)
- Schema-evolúciós szempontból: ha a szerver attribute-ot változtat egy type-on, a klienseknek (akár régebbi verzió) **rekomp nélkül** olvasniuk kell az új wire-t

A `ACCORE-BIN-T-P3X7` profile-bench mérése szerint a kis-adat slowdown (Latin1Long Small +2.6%, Medium +1.5% AcBinary lassulás MemPack-hez képest) jelentős részben a VarUInt per-call overhead-ből származik (ZigZag shift + multi-byte branch loop). A type-szintű `[IntEncoding=VarUInt]` attribute-tal a fejlesztő a non-negative property-ket VarUInt-NoZigZag-ra állíthatja → ZigZag shift kiesik, kis-adatra mérhető nyereség.

### Wire-formátum design

**5 új `BinaryTypeCode` marker** (naming TBD: `*VarUInt` vagy `*NoZZ` suffix, implementációkor véglegesítendő):

| Új marker | Cél | Alkalmazási hely |
|---|---|---|
| `ObjectVarUInt` | Object scope primitive int/long/enum-jai NoZigZag VarUInt encoding-ban | sima object first-occurrence |
| `ObjectWithTypeNameVarUInt` | Polimorf first-occurrence NoZZ-variánsa | `runtimeType != declaredType` esetén |
| `ObjectFullMarkerIIdVarUInt` | `RefHandling=IId` first-occurrence NoZZ-variánsa | csak first; subsequent `ObjectRefIId` változatlan |
| `ObjectFullMarkerAllVarUInt` | `RefHandling=All` first-occurrence NoZZ-variánsa | csak first; subsequent `ObjectRef` változatlan |
| `StringUtf16` | UTF-16 encoded string content (property-szintű) | bárhol egy string property emit-jénél |

**Wire-példa**:
```
[ObjectVarUInt marker]                  ← scope-szintű: int-property-k VarUInt-NoZZ
  WriteVarUInt(obj.Id)                   ← markerless body, encoding a marker alapján
  WriteVarUInt(obj.Status)
  [String marker] UTF-8(obj.Notes)        ← default UTF-8
  [StringUtf16 marker] UTF-16(obj.Name)   ← property-szintű override
```

**Byte-szintű példa** (`Order { Id=42, Status=3, Notes="ok" }`, class-szintű `IntEncoding=VarUInt`):
- Default ZigZag wire: `[Object]` `[0x54]` (VarInt 42 ZigZag: `((42<<1)^(42>>31))=84`) `[0x06]` (VarInt 3 ZigZag: 6) `[String]` `[0x02]` `0x6F 0x6B`
- New VarUInt wire: `[ObjectVarUInt]` `[0x2A]` (VarUInt 42 raw: `0x2A`) `[0x03]` (VarUInt 3 raw: `0x03`) `[String]` `[0x02]` `0x6F 0x6B`
- Body-sorrend és byte-szám változatlan; csak az encoding-szabályok mások. Stringek ugyanúgy markered (UTF-8 default itt). String-encoding override esetén `[StringUtf16]` `[char-count]` `[2-byte-per-char]`.

A primitive property-k körüli wire **markerless marad** — a body-encoding-ot az object-marker határozza meg, nem per-property byte. Wire-bloat csak ott van, ahol most is van marker (object-prefix, string-marker).

### Attribute design

**Object-szintű** (mert object-marker is object-szintű):
```csharp
[AcBinarySerializable(IntEncoding = IntEncoding.VarUInt)]
public class Order { ... }
```

**Property-szintű** (csak string-en, mert string-marker is per-property):
```csharp
public class Order {
    [AcBinaryEncoding(StringEncoding.Utf16)]
    public string CustomerName { get; set; }
}
```

**Új public API elemek**:
- `AcBinaryEncodingAttribute` (target: `Class | Property`)
- `IntEncoding` enum (`Default` = ZigZag VarInt, `VarUInt` = NoZigZag)
- `StringEncoding` enum (`Default` = UTF-8, `Utf16` = UTF-16)
- `AcBinaryOptions.IntEncoding` és `AcBinaryOptions.StringEncoding` runtime fallback opciók

### Encoding-választás precedenciája (writer-side)

1. **Property attribute** (legerősebb) — pl. `[AcBinaryEncoding(StringEncoding.Utf16)]`
2. **Class attribute** — pl. `[AcBinarySerializable(IntEncoding=VarUInt)]`
3. **`AcBinaryOptions` runtime opció** — pl. `options.StringEncoding = Utf16`
4. **Built-in default** — ZigZag-VarInt + UTF-8

### Szerepkörök és path-ok

| Path | Encoding-választás |
|---|---|
| **SGen writer (with attribute)** | Compile-time pinned, hard-coded marker + encoding emit (NO runtime branch) — a meglévő OPT-OUT minta (mint `RefHandling`/`Interning` disable) |
| **SGen writer (no attribute)** | Runtime branch a `context.IntEncoding`/`context.StringEncoding` option-en — két path generálódik, runtime dönt |
| **SGen reader** | **Marker-dispatch** (NEM hard-coded marker-expect — runtime-on dönti el, hogy `Object` vagy `ObjectVarUInt` érkezett, és annak megfelelően olvas) |
| **Runtime writer (reflection-based)** | Reflection-attribute-read + option fallback + default fallback — ugyanaz a precedencia mint SGen-nél |
| **Runtime reader** | Marker-dispatch (universal — nincs attribute / option használat encoding-döntésre, csak a marker-byte) |

⚠️ **SGen reader marker-dispatch KÖTELEZŐ** (NEM hard-coded marker-expect). Konkrét scenario amit ez kezel:

> Szerver Runtime-mode-ban serializálja `Order`-t. Az `Order` osztályon a szerver-deploy óta **változott az attribute** (új deploy hozott `[IntEncoding=VarUInt]`-ot). Szerver Runtime writer reflection-ből olvassa az új attribute-ot → `ObjectVarUInt` markert emit-el a wire-be.
>
> Régi kliens **rekomp nélkül** kapja a payload-ot. Ha a kliens SGen reader-e hard-coded `Object`-marker-expect-tel olvasna → **panik / mismatch**.
>
> Marker-dispatch-szel a kliens helyesen dekódol bármelyik markert, függetlenül attól, hogy a kliens-oldali compile-time `Order` typebe-n volt-e az attribute.

Ez biztosítja a **"server-side attribute-change doesn't break clients"** garanciát.

### Kompatibilitási garanciák

| Interakció | Eredmény |
|---|---|
| SGen-write (NoZZ attr) → SGen-read | OK (marker-dispatch) |
| SGen-write (NoZZ attr) → Runtime-read | OK (marker-dispatch) |
| Runtime-write (option=NoZZ) → SGen-read | OK (marker-dispatch) |
| Runtime-write (option=NoZZ) → Runtime-read | OK (marker-dispatch) |
| Server-attribute-changed → old client (no recompile) | OK — kliens csak a marker-t olvassa |
| Mixed payload (egyik object NoZZ, másik default) | OK — minden object-marker önálló scope |

### Implementációs lépések

1. **`BinaryTypeCode` const-bővítés** — 5 új byte-érték (range-allokáció: a meglévő enum szervezése alapján a következő szabad slot-okba). Wire-format spec frissítés `BINARY_FORMAT.md`-ben.
2. **`AcBinaryEncodingAttribute` + `IntEncoding` + `StringEncoding` enum-ok** — új fájlok az `AyCode.Core/Serializers/Binaries/` mappában.
3. **`AcBinaryOptions.IntEncoding` + `AcBinaryOptions.StringEncoding`** opciók hozzáadása (default = `Default`).
4. **`WriteStringUtf16` / `ReadStringUtf16` context-helper-ek** — `MemoryMarshal.Cast<char,byte>` direct copy + length-prefix (VarUInt char-count).
5. **Runtime writer reflection** — `BinarySerializeTypeMetadata` cache: `IntEncoding`, `StringEncoding`-per-property flag-ek (attribute-alapján). Encoding-emit a precedencia szerint.
6. **SGen writer template** — attribute-feldolgozás `EmitWriteValue`-ban: ha attribute → compile-time hard-coded emit; ha nincs → runtime-branch emit a `context` option-en.
7. **SGen reader template** — `EmitReadValue` marker-dispatch-szel (object-marker scope-encoding-mode tracking + string-marker per-property dispatch).
8. **Runtime reader update** — object-marker dispatch a scope-encoding-state-be (pl. `BinaryDeserializationContext.CurrentIntEncoding`), string-marker per-property dispatch.
9. **Cross-mode tesztek** — minden write-read kombináció (SGen↔SGen, SGen↔Runtime, Runtime↔SGen, Runtime↔Runtime) minden encoding-kombinációban (default, attr-only, option-only, attr+option, mixed payload).
10. **Doc**: `BINARY_FORMAT.md` wire-format spec, `BINARY_OPTIONS.md` új opciók, `BINARY_SGEN.md` precedencia + szerepkörök táblázat.

### Acceptance

- 5 új BinaryTypeCode marker, naming-konvenció dokumentált
- `AcBinaryEncodingAttribute` + 2 enum + 2 opció extension working
- Round-trip teszt minden cross-mode kombinációban zöld
- Wire-bloat default-encoding-on **0 byte** (nincs új per-property marker)
- Latin1Long Small bench: AcBinary `[IntEncoding=VarUInt]` típuson a slowdown ≤ MemPack +0.5pp (jelenleg +2.6%)
- `BINARY_FORMAT.md`/`BINARY_OPTIONS.md`/`BINARY_SGEN.md` szinkronban a wire- és attribute-világgal
- A meglévő `WireMode=Fast/Compact` distinction-ek kompatibilisek maradnak (vagy migrálódnak az új encoding-attribute-okra — külön döntés implementációkor)

### Trigger / Sorrend

Implementáció **ne kezdődjön** azonnal — a `ACCORE-BIN-T-P3X7` A+F szekciói (SGen ensure-batching + HasPropertyFilter lift-out) **előbb mérendő**. Ha az A+F már lehozza a SGen `WriteProperties` Self CPU-t ≤ 8%-ra, és a kis-adat slowdown ettől már ≤ +1pp, akkor ez a Q5T2 entry **alacsony prioritásra** kerül. Ha a kis-adat slowdown az A+F után is megmarad → Q5T2 implementáció **érdemi**.

Egyéb prerekvizit: `ACCORE-BIN-T-W9F1` (compile-time metadata) szinkronizálás — a Runtime writer reflection-attribute-read-je beleilleszthető a generált metadata-ba, ezzel a runtime path is gyorsabb attribute-alapú encoding-választás-on.

### Open kérdések (implementációkor eldöntendő)

- **Marker naming**: `ObjectVarUInt` (semantic, az encoding alapján) vagy `ObjectNoZZ` (rövidebb)?
- **`[AcBinarySerializable]`-on belül** vegyük fel a `IntEncoding` paramétert, vagy **külön `[AcBinaryEncoding]` attribute** legyen object-szinten is (és a `[AcBinarySerializable]` változatlan)?
- **`AcBinaryOptions.WireMode` jövője**: a régi `Fast`/`Compact` enum migrálódjon az új `IntEncoding`/`StringEncoding`-ra (BC-break) vagy maradjon mint shortcut-default?

## ACCORE-BIN-T-J8R4: ASCII Ser path — combined store optimization in `WriteStringWithDispatch`
**Priority:** P2 · **Type:** Performance

`AcBinarySerializer.BinarySerializationContext.WriteStringWithDispatch` ASCII override branch (`bytesWritten == charLength`, `bytesWritten > 31`, currently around context.cs:919-930) emits separate marker store + `WriteVarUIntUnsafe` method-call. The non-ASCII `case <= 255` branch uses a combined 4-byte unaligned `uint` store for marker+charLen+utf8Len header.

Benchmark evidence: `AsciiShort` charset benchmark (Console.FullBenchmark_Release_2026-05-19_09-55-05) showed Ser median **+3.8%** (MemPack faster) — regression vs Latin1Long Ser median -2.0%. Repeated-strings cell +13%. The method-call frame to `WriteVarUIntUnsafe` is the suspected cost.

**Quick fix sketch**: split the ASCII path on `bytesWritten < 128` (VarUInt fits in 1 byte → 2-byte combined ushort header store, no method call) vs `>= 128` (current separate-write path, rare on workload). May also benefit from extending to a 4-byte combined store for `bytesWritten < 16384` (VarUInt fits in 2 bytes).

Validate on AsciiShort + AsciiLong + Latin1FixAscii (no regression on FixStrAscii path). Cross-check the Repeated +13% cell.

## ACCORE-BIN-T-M2L7: Per-mode `_typeReaders` table — eliminate residual mode-aware ternaries
**Priority:** P3 · **Type:** Performance · **Related:** `ACCORE-BIN-T-T8F6` (may subsume this — switch refactor inlines mode-dispatch on the `StringSmall` case directly)

After the `ReadStringSmall` → `ReadStringSmallCompact` / `ReadStringSmallFastWire` split, 3 call sites still inline `FastWire ? FW : Compact` ternary:

- `AcBinaryDeserializer.cs` — `Readers[BinaryTypeCode.StringSmall]` lambda (TypeReaderTable static init)
- `AcBinaryDeserializer.cs` — cross-type populate `PropertyAccessorType.String` case (~line 1055)
- `AcBinarySourceGenerator.GenReader.cs` — `EmitReadString` switch case `StringSmall` emit (~line 243)

**Sketch**: a `_typeReaders` instance array on `BinaryDeserializationContext<TInput>` initialized at `Reset/Initialize` time from one of two static tables (`TypeReaderTable<TInput>.Readers` vs lazy-built `FastWireReaders`). The FastWire variant differs from the default only in the `StringSmall` slot (clone of the default + 1 overwrite). Hot path: `context._typeReaders[typeCode]` — zero ternary.

Sibling SGen-emit pattern: emit `context._typeReaders` reference if the call site is in a switch case where the runtime mode can vary (Dictionary key/value, runtime populate). Direct `ReadStringSmallCompact()` stays in the SGen property hot-path (caller already short-circuits FastWire on a separate ag).

Mode-check cost: 1 reference assignment per `Initialize` call vs 1 branch per `StringSmall` marker dispatched. Net win on string-heavy workloads.

## ACCORE-BIN-T-T8F6: Replace `TypeReaderTable` delegate-array with switch dispatch in `ReadValue`
**Priority:** P2 · **Type:** Performance · **Related:** `ACCORE-BIN-T-M2L7` (subsumes — mode-dispatch becomes a case-local branch), `BINARY_STRICT_SGEN.md` (Phase 2 SGen-emit simplification — same direct-call philosophy)

`AcBinaryDeserializer.cs:74-144` builds a 256-slot `Func<BinaryDeserializationContext<TInput>, Type, object?>[]` dispatch table per `TInput` specialization. Every `ReadValue` marker dispatch is `Readers[typeCode](ctx, type)` — indirect call via delegate-instance, non-inlinable for JIT, opaque for NativeAOT (`IL_calli` lower).

**No concrete reason for the table over a switch** (reviewed: generic-`TInput` specialization works either way; FixObj 0-63 / FixStrAscii 135-166 ranges become `case >= 0 and < 64: slot = typeCode` trivially; no consumer-extensibility pattern; no per-context override; no multi-version dispatch; cross-type populate doesn't share the table). The pattern is likely historical — first prototype that worked, never refactored.

**Sketch**: `ReadValue` body becomes `switch (typeCode) { case BinaryTypeCode.Object: return ReadObject(ctx, type); ... }`. Range cases `case >= 0 and < BinaryTypeCode.SlotCount: ...` for FixObj, `case >= BinaryTypeCode.FixStrAsciiBase and <= BinaryTypeCode.FixStrAsciiMax: var length = ...; ...` for FixStrAscii. The `StringSmall` case inlines the mode-aware ternary directly (`context.FastWire ? ReadStringSmallFastWire() : ReadStringSmallCompact()`) — eliminates `M2L7`'s separate lazy-clone need.

**Expected gain**: ~3-7 ns/dispatch (delegate-invocation overhead eliminated), branch-predictor-friendly (frequent markers as first cases), inline-friendly for small case-bodies (`Null` → `return null`). NativeAOT publish size benefit: ~128 lambda-class metadata entries removed per `TInput` (×4 = ~512 closure objects).

**Mérni mielőtt belevágunk**: mikrobenchmark a `ReadValue` switch-variant vs delegate-variant 100K-iter loop-pal vegyes marker-mix-szel. Ha ≥2 ns/dispatch gain → érdemes; ha nincs különbség → marad ahogy van.

