The directional-shadow topology rebuilt on every streaming-churn frame
and was the measured body of the run-hitch stalls (701 of 708 baseline
stalls alloc-correlated):
- The draw sort comparer's enum-vs-enum CompareTo bound to
Enum.CompareTo(object) and boxed BOTH operands on every comparison —
a constant ~38.9 MB of garbage per topology rebuild (~4M boxes),
handing the GC a forced gen0 collection mid-frame. The full ~100k-draw
sort is replaced outright: draws hash-group by exact DrawKey in one
O(n) pass over retained chained-index arrays, and only the
few-thousand DISTINCT group keys sort (order-preserving packed
material|cull|firstIndex|baseVertex + count|slot|layer|foliage keys,
first-appearance tie-break) — bit-identical emission order to the old
stable sort, near-zero allocation, and no per-draw comparisons at all.
- The caster frame sorts 4-byte indices keyed on SortKey.Value instead
of shuffling multi-hundred-byte records through a boxing comparer.
- Owner-approved pipelining: on a frame whose shadow inputs just changed
(the same frame already paying frame-view/landscape rebuilds), the
caster-frame and prepared-draws topology rebuilds defer to the next
quieter frame, capped at two consecutive deferrals — inside the GPU
fence depth, so retained draws never reference a released arena range.
First build, generation change, caster BuildSequence change, and
journal overflow force the immediate path; deferred refreshes skip
identity-mismatched journal rows.
Owner-accepted in both presentation modes: stall frames 5.8/s -> ~0.45/s
uncapped (0.49/s capped), median stall 20.3 -> 13.7 ms, >25 ms frames
near zero, 275 fps uncapped baseline restored. Allocation gate: a warmed
topology rebuild must allocate <2 KiB (DirectionalShadowPreparedDrawTests).
docs/ISSUES.md carries the full evidence trail; the residual
content-proportional rebuild milliseconds are filed as the
incremental-topology successor, and the pre-existing town-view scaling
latch is filed as #432.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>