fix(pump+compositor): take-3 starvation — deadline pacing + row-blit fast paths

Two defects made the producer 17x slow (37s record -> 2.1s/127-frame file,
rawvideo stamps by arrival): FramePump slept the FULL interval after each
render (period = render+submit+interval) and SceneCompositor did per-pixel
float sampling + Math.Round blends over all 2.07M master pixels, scanning the
whole destination per overlay (258ms avg render vs 1.5ms submit).

Both solutions are established, not invented — researched before coding per
the derivative-work rule:
- deadline pacing: OBS libobs/media-io/video-io.c video_thread (nextTick +=
  intervalTicks, sleep only the remainder, rebase on overrun, never burst)
- row blits: libyuv pattern (BSD-3, chromium.googlesource.com/libyuv/libyuv)
  — 1:1 aligned identity fast path, per-pixel alpha branch, integer
  fixed-point blend, overlay clipped to the intersection rect, skip the dead
  black pre-fill when the backdrop covers

ONE integration test: Pump_Paces_To_The_Deadline_Compensating_Render_Cost
(lands after a fake-seam lesson: pacing fakes must await, not complete
synchronously, or the pump loop runs inline on StartAsync and hangs vstest).
Clean build 0 warnings; FramePumpTests 10/10, SceneCompositor/SceneGraph/
SocialBar/StretchMath 20/20. Docs same commit: ai.md pipeline section,
TASKS.md TASK 18 (webcam-in-output + rename modal verified from take 3),
MyMistakes recipe, HANDOFF rewritten. Take 4 pending on the user's machine.
This commit is contained in:
2026-09-03 09:02:45 -07:00
parent 7fcb2ad3da
commit 716a77f61a
7 changed files with 306 additions and 75 deletions
+18
View File
@@ -692,6 +692,24 @@ seam:** `Func<Scene?>`, `Func<SceneElement, VideoFrame?>` resolver, `Func<Compos
loaded from the `LayoutStore` cache at startup and set fresh by `PrepareAndStartLiveAsync` before the pump
starts. **The pump reads the URL once at startup**, which is why go-live ensures the stream BEFORE
`StartAsync`.
- **Deadline pacing + row-blit render (take-3 starvation fix, 2026-09-03):** take 3 recorded 30s into a
2.1s file and the 5s stats line named the cause in one number — `17/300 frames per 5s, avg render
258.1ms, avg submit 1.5ms`. Two defects, both fixed: (1) the pump slept the FULL interval after each
render, so period = render + interval — OBS's `video_thread` (libobs/media-io/video-io.c) pattern
replaces it: absolute `nextTick += intervalTicks` deadline, sleep only the remainder, and on overrun
skip the wait AND the missed ticks (rebase, never burst stale frames). (2) the compositor did
per-pixel float sampling + `Math.Round` blending over all 2.07M master pixels (backdrop) and scanned
the whole destination per overlay (a 64px social-bar strip cost 2M iterations). `SceneCompositor` now
follows the libyuv pattern (BSD-3, chromium.googlesource.com/libyuv/libyuv — cited per the
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
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). **Next slice if take 4's
stats show it:** 8.3MB fresh `byte[]` per frame → LOH churn/GC stalls — pool the master buffer if avg
render lands ≤ budget but frames still hiccup.
- **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