Decoupled High-Frequency Delivery and the Serialization Toolchain β ADR-092
Decoupled High-Frequency Delivery and the Serialization Toolchain β ADR-092
Date: 2026-08-02 Β· Written in English per the 2026-08-01 convention change.
Provenance: implementing Β§8.2βs per-viewer snapshot. Building the first real wire payload turned three of Β§8.1βs implicit assumptions into decisions that had to be made. Two of them Β§8.1 had answered the wrong way, and the third it had not asked. As with R6 and R7, the title names where the ruling came from rather than implying an eighth review round.
ADR-092 β What Rides the 20Hz Packet Is Decided by Measurement, and One Compiler Owns the Wire
Status: Accepted Β· Date: 2026-08-02 Β· Amends: 090 Β· Depends-on: 029 (SignalR is the transport whose message size this bounds) Β· interacts with 058
Context
Β§8.1 assigned formats β FlatBuffers for core updates, MessagePack for cartridge
payloads βembedded opaquely as [ubyte] within the FlatBuffer packetβ β and Β§9.6
declared Descent.Vtt.Protocol the only source of truth for the wire. Neither
statement had ever been executed, because until 2026-08-02 nothing produced a
per-viewer snapshot.
Executing them produced three findings.
The embedded payload is unaffordable at tick frequency. Measured on Β§5.2βs
reference interest set of 200 entities: a transform-only snapshot is 6,468
bytes, and the same snapshot with every entity carrying Q-060βs 4,096-byte
attribute ceiling is 825,668 bytes. At Q-001βs 20Hz that is ~16.5 MB/s per
viewer, against a budget ADR-014 exists to protect. The 128Γ factor is not a
tuning problem; it is a category error about what belongs on a tick.
The figures above are as measured on 2026-08-02 and are deliberately not updated. This is a decision-time record, and what was measured when the ruling was made is part of what it records.
DisclosedEntitygainedraw_zandrotationon 2026-08-09, taking the struct from 32 to 40 bytes; the reference snapshot is now 8,068 bytes, the attributes-at-ceiling comparison 827,268, and the factor ~103Γ rather than 128Γ. The ruling is unaffected β the argument is about cadence rather than about the size of the ratio, and two orders of magnitude remain two orders of magnitude. The live derivation isQ-056inQuantity_Registry.md, which is where a reader should build from.
The fog mask has the same shape and was never examined. Q-010 fixes a packed
chunk at 16 KiB. Riding the snapshot, one chunk alone is ~320 KiB/s per viewer at
20Hz β more than fifty times the entire transform payload it would be attached to.
And the single-source claim had no compiler. Β§9.6 requires one .fbs set to
generate both the C# and the TypeScript artefacts. FlatSharp is the convenient
.NET answer and emits C# only, so choosing it means the TypeScript artefact is
hand-written β which is exactly what the claim forbids, arrived at by a route that
looks like tooling convenience rather than a wire decision.
Decision
-
The tick-driven snapshot carries transform state and nothing else. Identity, position, observer flag, the two ticks and their derived disclosure tick. A field for anything else is not merely unused; it is an invitation, and the measurement above is what it costs when accepted.
-
Cartridge attributes are a separate, event-driven, low-frequency message. Sent when a stat changes or when a client opens a sheet β never per tick, and never for every entity in an interest set because one of them was mutated. Β§8.1βs βembedded opaquely as
[ubyte]within the FlatBuffer packetβ is withdrawn: the embedding is what made the frequency coupling invisible. -
The fog mask travels out of band, as its own message.
Q-010βs chunk is sized for storage and for the geometry crate, not for a tick, and its cadence is already governed separately byQ-068β¦Q-071βs ladder. Bundling it into a packet delivered at transform frequency would silently re-couple two cadences that Β§5.3 deliberately separates. -
flatcis the wire compiler. FlatSharp and hand-written wire types are refused. The single-source property is cross-language or it is nothing: a C#-only generator satisfies the letter of βgeneratedβ while leaving the half the claim exists to protect β agreement between the two hosts β to review. -
flatcand the C# runtime package are version-matched, and the runtime pins the pair. Generated code assertsFlatBufferConstants.FLATBUFFERS_<version>, which a mismatched runtime does not define, so a skew is a compile error rather than a subtle one. The NuGet runtime trails the compiler releases, so the newest runtime, not the newest compiler, decides the version; installing the compilerβs default is what breaks the build. -
Generated sources are never committed. They live in
obj/and are produced every build. A committed generated file is a second source of truth: it can be edited, it can go stale against the schema beside it, and neither shows up in review as a wire change. -
Q-056is set at 32,768 and is one decision with clause 1. It admits 1,021 entities, 5.1Γ Β§5.2βs reference interest set. Raising it without re-examining what the packet carries restores the 128Γ term it exists to exclude β the cap and the payload rule are not independently tunable.
Alternatives Considered and Why Rejected
- Keep attributes embedded and cap the snapshot instead. Rejected: a cap large enough for the attribute-bearing case is ~825 KB, which does not bound anything useful; a cap small enough to be a bound refuses the packet. The size is a symptom of the coupling, not the problem.
- Send attributes embedded but only for changed entities. Rejected β it makes
the packet size depend on mutation rate, so the worst case is a busy combat round
and the measurement that sized
Q-056would describe the quiet case. A bound that holds only when nothing is happening is not a bound. - FlatSharp for C#,
flatcfor TypeScript. Rejected: two generators reading one schema is two interpretations of it, and their disagreements would appear as wire bugs between hosts β the precise failure the single-source rule exists to prevent, reintroduced one layer down. - Pin the compiler and let the runtime float. Rejected: the runtime is the scarcer artefact, and inverting the pin means the build breaks whenever Google ships a compiler, which is the direction nobody notices until CI is red.
Consequences (including negative)
- Negative: a second delivery path exists now. Attributes and the fog mask each need their own message, ordering story and loss behaviour. That is three paths where Β§8.1 described one, and each is a place a disclosure filter must be applied again β Β§14.7βs per-viewer property covers the snapshot and does not automatically cover the other two.
- Negative: a build-time binary dependency.
flatcmust be installed on every developer machine and in CI, pinned. This is the cost of the cross-language claim, accepted deliberately. - Negative: the version pin now lives in three places.
FLATC_VERSIONin CI,FlatcExpectedVersionand thePackageReferenceinDescent.Vtt.Protocol.csproj. The csproj fails the build when the compiler and the runtime disagree, so drift between them is caught rather than silent β but the three must be bumped in one commit, and the standing instruction to keep them synced is recorded at theTODO(flatbuffers-version-bump)beside them. (Superseded note, kept because the reasoning still applies: this clause originally recorded that the pin had no enforcement point,.github/workflows/in the backend being an empty directory. Backend CI landed 2026-08-02.) - Positive: the snapshotβs size is now a property of the interest set alone, so
Q-056bounds something stable rather than something that moves with cartridge design.
Enforcement
schemas/snapshot.fbshas noattributesvector, so clause 1 is structural rather than reviewed.SnapshotSizeMeasurementTestsasserts the measured tail stays insideQ-056, so a wider default AOI, a fatter entity struct, or attributes returning to the packet fails against the registry.Descent.Vtt.Protocol.csprojerrors with a named message whenflatcis absent, rather than surfacing a shell βcommand not foundβ that reads like a malformed schema.- Backend CI, live since 2026-08-02 (
.github/workflows/ci.yml). It installsflatcat the pinned version from the release binary β notapt, whoseflatbuffers-compileronubuntu-latestis 23.x and cannot satisfy clause 5 β verifies the reported version before building, and runs the .NET suites and the Rust gates against the crate revision this repository pins. Clauses 4β6 no longer rest on developer discipline, which ADR-045 says is not enforcement.
Β§11 Index Line
ADR-092: The tick-driven snapshot carries transform state only β cartridge attributes and the Q-010 fog mask each travel as their own event-driven message, because embedding them costs 128Γ and 50Γ respectively at 20Hz; flatc is the sole wire compiler (FlatSharp is refused for emitting C# only), the NuGet runtime pins the compiler version, generated sources are never committed, and Q-056's 32,768 is one decision with the payload rule rather than an independent cap. (Amends ADR-090.)