perf(pump): slice 18 — C4 blit-on-change composite cache

ty-1841: capture fixed (band ~20 updates/s, no tears) but FramePump stalls
on EVERY iteration (totalMs 21-44, render=full-render split=0 elements=6
dynamic=4) — the render ceiling, ~22-28 composites/s, caps the desktop in a
60fps file. SceneGraph can't help: the live backdrop is element 0 and cannot
be baked (a cached capture goes stale), so the cache lives at the pump.

RenderFull wraps both full-render call sites: BuildFullRenderSignature hashes
the full input identity (options rect, social bar, per-element layout/visual
bits + the frame each element would resolve through the SAME resolver seam,
using array identity + Epoch + CropBounds); unchanged identity reuses the last
composite with one Buffer.BlockCopy (~3ms) instead of a ~30ms re-composite.
Cache buffer is a separate long-lived array, written pre-burn/pre-recycle
(the caller burns the frame counter and recycles scratch AFTER render).
Engagement gated on the 1:1 config (the only deployed tier). Telemetry
surfaces `cache NR/WH` on the 5s stats line.

Same shape as OBS (sources cache their surface, the scene blits on update) —
docs.obsproject.com/backend-design, the pattern this repo cites since the
2026-09-04 paste-cache slice.

Good Dog: FullRenderCache_StaticInputs_RenderOnce_Then_Reuse_UntilInputChanges
(static scene renders ONCE + byte-identical reuse; new frame+Epoch invalidates).
297/297 green, 0 warnings, verify.sh scope-locked (FramePump.cs, FramePumpTests.cs
+ ai.md/HANDOFF/MyMistakes). No push — device re-verify next.
This commit is contained in:
2026-09-15 07:57:09 -07:00
parent b37b8a30f9
commit 64a5a6d06f
5 changed files with 378 additions and 67 deletions
+20
View File
@@ -668,3 +668,23 @@ tick's worth after muting, and if the pipe still carries pre-mute buffered bytes
pipe first or assert on a multi-tick window. Do not blame a sync change for this — verify the grain
against the clean tree in the same mode before touching the mixer.
---
### Slice-18 follow-up (2026-09-15) — C4 composite cache: test fakes must mirror production's STABLE scene and per-tick purity
The C4 blit-on-change cache keys on a hash of the resolved inputs, INCLUDING per-element reference
identity (`RuntimeHelpers.GetHashCode(element)`). Two test-setup habits silently broke/starved it:
1. **`NewPump`'s default scene is fresh per tick** (`() => BackgroundScene()` — fine for pacing
tests) — churned the element refs, so the signature NEVER matched and the cache looked broken
(301 "renders" instead of 1). Production hands a STABLE `StagedScene`. **Rule: any FramePump test
that asserts per-frame content or cache behavior must pass `scene: () => scene` with one Scene
instance; the fresh-scene default is only for pacing/diagnostic tests.**
2. **A call-count resolver flip (`flip++ % 2`) double-advances under a cache-aware pump** — the
tick resolves TWICE (signature pass + compositor pass). Key per-tick content on the burned
`OutputIndex` (stable until submit, which happens AFTER render) or on `encoder.Frames.Count`
instead. A stable-per-tick token, not a call counter, is the deterministic alternation.
3. New frames that must invalidate the cache need BOTH a fresh array AND a fresh Epoch (array
identity alone is unchanged for a mutated-in-place array; `Epoch` is the monotonic generation
marker the compositor's paste key and the C4 signature share).