perf(compositor): slice 5 — paste cache for non-opaque layers (take-7/8 data)
The stamped build settled what slices 3-4 could not: chat cache works (resolve ~0.0ms) but render stayed 26-27ms -> 124-135/300. The cost was the compositor re-rasterizing EVERY layer every tick: this Live scene re-samples chat (159k) + web widget (271k) + image (95k) + cam (156k) ~ 680k px @ ~38ns — for layers whose pixels do not change between chat/web/cam updates. OBS shape: cache the surface, paste per tick. BlitCachedLayer rasterizes a non-opaque layer ONCE into an element-space, transparent-based frame keyed by (source-array identity, src W/H, ceil'd dst rect, round, mirror), then pastes: integer position, row alpha-blend, opacity applied at paste. Producers hand out fresh immutable arrays -> array-identity keys cannot serve stale content; dict bounded (48, clears whole). Drag/opacity live in paste params, not keys, so editing stops triggering resamples too. Opaque backdrop keeps the memcpy path; the webcam keeps the direct path via its IsOpaque flag (revisit if take 9 is borderline). ONE integration test: PasteCache_RepeatRender_IsByteIdentical_And_ContentChange- Propagates (byte-exact raster-vs-paste incl. round-clip margins, new-array propagation); existing pixel suite guards sampler semantics. 85/85 across compositor/pump/chat/capture/session classes, clean build 0 warnings. Also: BuildStampTests.cs was written last commit but never staged — its own scope-check slip, added here (the run had used the on-disk file; tracked now). Docs same commit: ai.md slice 5 + stale 'general path only 130k' claim corrected, TASKS.md take-9 gate, MyMistakes recipe (prove the stage; a fix that doesn't move the stat wasn't the bottleneck), HANDOFF. take 9 expectation: 300/300, render <= ~8ms -> saga closes, Unit B starts.
This commit is contained in:
@@ -707,8 +707,8 @@ seam:** `Func<Scene?>`, `Func<SceneElement, VideoFrame?>` resolver, `Func<Compos
|
||||
derivative-work rule): 1:1 aligned blits take a row-walk fast path (bilinear at scale 1 + integer
|
||||
offset is the identity) with per-pixel alpha branch and integer fixed-point blend; `BlitOverlay`
|
||||
clips to the intersection rect; a full-cover live backdrop skips the opaque-black pre-fill. The
|
||||
general per-pixel path stays for scaled/round/mirrored elements (webcam) — pixel-identical, and it is
|
||||
only ~130k px. Test: `Pump_Paces_To_The_Deadline_Compensating_Render_Cost` records the requested wait
|
||||
general per-pixel path handled scaled/round/mirrored elements. Test:
|
||||
`Pump_Paces_To_The_Deadline_Compensating_Render_Cost` records the requested wait
|
||||
via the pacing seam (a seam fake must genuinely await — a synchronous completed task runs the whole
|
||||
pump loop on `StartAsync`'s continuation and hangs the run; MyMistakes).
|
||||
- **Slice 2 of the render fix (2026-09-04, take 4):** the pacing held (file no longer truncated at
|
||||
@@ -745,6 +745,24 @@ seam:** `Func<Scene?>`, `Func<SceneElement, VideoFrame?>` resolver, `Func<Compos
|
||||
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).
|
||||
- **Slice 5 — paste cache for non-opaque layers (2026-09-04, take 7/8 data):** the render/resolve
|
||||
split (added in the stamp commit) finally named the last thing honestly: `resolve ≈ 0` but
|
||||
`render 26-27ms` (124-135/300, ≈2.2x) — the chat fix HAD worked; the remaining cost was the
|
||||
compositor re-rasterizing EVERY layer per tick even when its pixels never changed (this creator's
|
||||
Live scene: chat 159k + web widget 271k + image 95k + webcam 156k px ≈ 680k samples @ ~38ns each).
|
||||
OBS's actual shape: sources cache their surface, the compositor pastes. `SceneCompositor` now
|
||||
routes non-opaque layers through `BlitCachedLayer`: a layer rasterizes ONCE into an element-space,
|
||||
TRANSPARENT-based frame keyed by (source-array identity, source W×H, ceil'd dst rect, round,
|
||||
mirror), then every later tick PASTES it (integer position, row alpha-blend, opacity applied at
|
||||
paste). Producers hand out fresh immutable arrays, so array-identity keys can never serve stale
|
||||
content; dict bounded at 48, cleared wholesale on overflow. Only changing content (webcam device
|
||||
frames, web capture ticks, chat messages) resamples; the opaque backdrop keeps its memcpy path.
|
||||
Position/opacity drags are now near-free (no resample — paste params, not cache keys).
|
||||
ONE integration test: `PasteCache_RepeatRender_IsByteIdentical_And_ContentChangePropagates`
|
||||
(raster-vs-paste byte equality incl. round-clip margins + new-array propagation); the whole
|
||||
existing pixel suite guards sampler semantics. Take 9 must show `≈300/300, avg render ≤ ~8ms`;
|
||||
if it lands there, the recording saga closes. Follow-ups unchanged: vertical-tier scale alloc,
|
||||
debounced chat re-render under message bursts.
|
||||
- **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