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:
@@ -1090,6 +1090,43 @@ Full suite 290/291 passing, the sole failure the pre-existing compositor pixel t
|
||||
`PublishGate_TryPublish_OnlyStrictlyNewerWins`. Full suite **296/296 green, 0
|
||||
warnings**. NOT YET DEVICE-VERIFIED; target: telemetry frames/s jumps ≥ ~30-40 and the
|
||||
band audit drops below ~50% frozen. No push.
|
||||
- **Slice 18 — C4: the FramePump blit-on-change composite cache (2026-09-15, take ty-1841
|
||||
verdict):** the 1841 take proved capture fixed (band ~20 fresh updates/s, no tears, pacing
|
||||
clean) but logged a FramePump **stall on EVERY iteration** (`totalMs 21-44`,
|
||||
`render=full-render split=0 elements=6 dynamic=4`, worst render 166ms startup spike) →
|
||||
~22-28 composites/s hard-caps the desktop inside the 60fps file. The SceneGraph split
|
||||
CANNOT fix this wired: `GetSplitPoint` returns **0** because the live-capture backdrop is
|
||||
element 0 and must stay dynamic — a baked capture goes stale — so every tick runs the full
|
||||
re-composite even when no input changed (the compositor's per-layer paste cache already makes
|
||||
identical frames identical per-element; the waste is re-COMPOSITING the whole 8.3MB frame).
|
||||
Same shape as OBS (sources cache their surface, the compositor renders on update only —
|
||||
the pattern this file has cited since the 2026-09-04 paste-cache slice) at the FRAME level.
|
||||
`FramePump.RenderFull` now wraps both full-render call sites (no-SceneGraph + split=0
|
||||
fallback): it computes `BuildFullRenderSignature` (a deterministic hash of the crop/output
|
||||
dims, the social bar identity + top, and — per element, MIRRORING the compositor's own
|
||||
resolution — the element ref, layout/visual bits, and the frame each element would resolve
|
||||
through the SAME resolver seam, using buffer-array identity + Epoch + CropBounds; a changed
|
||||
frame forces a re-composite, an unchanged one never serves stale bytes) and, when it matches
|
||||
the last composite, reuses the cache with ONE `Buffer.BlockCopy` (~3ms) instead. The cache
|
||||
buffer is a **separate long-lived array** — never the scratch pool — because the caller burns
|
||||
the frame counter and recycles the scratch AFTER `RenderScene` returns; the cache is written
|
||||
from the rendered scratch BEFORE returning (pre-burn, pre-recycle). Engagement is gated on
|
||||
the 1:1 config (`SourceRect == Output`, the only deployed tier); the vertical tier's cached
|
||||
fresh-scale-buffer follow-up stays exactly where it was. Telemetry: internal `CacheHits`/
|
||||
`CacheRenders` counters surface as `cache {renders}R/{hits}H` on the 5s stats line, and the
|
||||
ticks' key is an internal `OutputIndex` (burned counter, stable across one tick's resolver
|
||||
passes). **Good Dog test**
|
||||
`FullRenderCache_StaticInputs_RenderOnce_Then_Reuse_UntilInputChanges`: static scene →
|
||||
exactly ONE composite (renders==1, hits>0, byte-identical content above the burn strip),
|
||||
then a new frame (new array + monotonic Epoch) invalidates and propagates. Existing pool test
|
||||
now passes a STABLE scene (production hands `StagedScene`; a fresh scene per tick churns the
|
||||
element refs inside the signature and hides the cache — the tests that care about cache
|
||||
behavior must mirror production) and keys its red/blue alternation on `OutputIndex`/frame
|
||||
count so the tick's two resolver passes (signature + render) don't double-advance a call-count
|
||||
flip. Full suite **297/297 green, 0 warnings**; verify.sh scope-locked to FramePump.cs +
|
||||
FramePumpTests.cs + docs. Target next take: `cache` hits dominate on TV holds, `avg render`
|
||||
drops toward the BlockCopy, stalls vanish, then the band audit + clap re-measure decide push.
|
||||
No push.
|
||||
- **Stop ordering matters:** `StopAsync` stops the encoder — since slice 10 it FLUSHES the pending
|
||||
queue (`Channel.TryComplete` → drain writes the leftovers, closes stdin → EOF → ffmpeg finalizes+exits;
|
||||
an accepted frame is never lost) — **before** awaiting the loop. The old reverse-order deadlock was
|
||||
|
||||
Reference in New Issue
Block a user