perf(chat): slice 3 — raster-on-change cache in ChatOverlayLayer (take-5 fix)
Take 5: render 58.9 -> 25.5ms (138/300, still ~2.2x). The blits were fixed; the resolver was not: ResolveOutputFrame -> RenderChatBox ran a FULL WPF raster (FormattedText + RenderTargetBitmap + CopyPixels + channel swap) EVERY tick whenever the chat buffer was non-empty — and the buffer survives sessions, so even a signed-out record-only take paid it. Established answer (OBS text sources): re-render on change, blit the cache every tick. ChatOverlayLayer: content version bumped from Messages.CollectionChanged (covers adds, the 500-cap removal, the fade Clear from any caller) + a config key (size + all Chat* appearance props); RenderFrame returns the cached VideoFrame by identity until either changes (compositor only reads cached frames). Conservative ordering (version latched BEFORE render) makes a mid-render message re-render next tick, never serve stale. ONE integration test: ChatOverlayLayerCacheTests (RealApp, real renderer): Same() for unchanged inputs, NotSame() on message/config change, null on empty. Full regression green (62 across touched classes), clean build 0 warnings. Accepted cost pending take 6: one ~15-25ms tick per arriving message; if live-chat bursts sag n/300, next slice = debounced off-tick re-render. Docs same commit: ai.md pipeline section, TASKS.md TASK 18, MyMistakes recipe (raster-on-change + session-surviving-buffer trap), HANDOFF (take 6 -> then Unit B, spec unchanged).
This commit is contained in:
@@ -730,6 +730,18 @@ seam:** `Func<Scene?>`, `Func<SceneElement, VideoFrame?>` resolver, `Func<Compos
|
||||
reference would race legitimate recycling. Take 5 must show `avg render ≤ ~10ms, ≈300/300`; the
|
||||
vertical tier's final 1080×1920 `BilinearScale` still allocates fresh per frame (same GC lesson when
|
||||
someone streams vertical — recorded as a follow-up, not silently "done").
|
||||
- **Slice 3 — chat raster-on-change (2026-09-04, take 5):** take 5 measured `138/300, avg render
|
||||
25.5ms` — the per-tick blits were cheap now, but `ResolveOutputFrame` → `RenderChatBox` ran a FULL
|
||||
WPF raster (`ChatBoxRenderer`: FormattedText + `RenderTargetBitmap` + CopyPixels + channel swap)
|
||||
EVERY tick whenever the message buffer was non-empty — and the buffer survives between sessions,
|
||||
so even a signed-out record-only take paid it. Established answer, same as OBS text sources:
|
||||
**raster on change, blit the cache every tick.** `ChatOverlayLayer` now keeps a content version
|
||||
(`Messages.CollectionChanged` → `_contentVersion++`) plus a config key (box size + all Chat*
|
||||
props); `RenderFrame` returns the cached `VideoFrame` by identity until either changes (the
|
||||
compositor only ever reads a cached frame). Test: `ChatOverlayLayerCacheTests` (RealApp, real
|
||||
renderer — `Same()` for unchanged inputs, `NotSame()` on message/config change, null on empty).
|
||||
Accepted cost pending take 6: one slow tick (~15-25ms) per arriving message; if chat-burst frame
|
||||
loss shows up, the next slice moves the re-render off-tick (debounced, dispatcher-side).
|
||||
- **Stop ordering matters:** `StopAsync` stops the encoder (closes stdin → EOF → ffmpeg finalizes+exits)
|
||||
**before** awaiting the loop, because closing stdin unblocks a write stuck on pipe backpressure — the
|
||||
reverse order would deadlock. `ProcessFailed` self-stops the pump. `Failed` while live flips
|
||||
|
||||
Reference in New Issue
Block a user