Creator 2026-09-26: "the ticker should appear over the stream alerts video, not over
the entire preview window". It was a GLOBAL 1920x48 bar blitted last at a hardcoded
(0, 0) -- it covered the whole preview width, not the alert video.
The strip is now rendered at the alert box's own size and the box's origin travels on
the frame in a new VideoFrame.Placement ((int X, int Y)?). OriginX/OriginY default to
0 when Placement is null, so every full-canvas overlay (the branding flash) is
unaffected. Every ticker blit site now reads tickerFrame.OriginX/OriginY instead of a
literal 0, 0 -- SceneCompositor x2 and FramePump's static-bake Overlay path -- and
PreviewPane.xaml's AlertTickerElement binds the same rect (AlertTickerLeft/Top/
Width/Height), so the preview cannot drift from the recording the creator is judging.
Why the position rides on the frame rather than in a full-canvas frame: a 1920x1080
overlay is 8.3MB of large-object-heap garbage per tick, ~2.5GB churned over one 10s
alert at 30fps. A box-sized strip is ~370KB. The branding flash does render
full-canvas but RECYCLES one 8MB buffer and bumps Epoch, so it never allocates per
frame; the ticker allocates per call, so it has to stay small.
Also fixed: CopyStrip now clips ROWS to the target height. The pill rasterises at its
natural 48px, so an alert box shorter than that would have written past the end of the
target buffer once the strip became box-sized.
No alert box in the scene now means no ticker at all -- there is no global position
left for it, and this is the guard against the old bar quietly coming back.
Tests (3 new facts, 27 in the file):
ComposedOutput_PutsTheTickerInsideTheAlertBox_NotAtTheTopEdge -- renders through
the real SceneCompositor and asserts the pixels land at (620, 430) and NOT at the
top-left corner. The creator is judging compositing from local recordings, so the
OUTPUT side is the side that needed proving.
ASceneWithNoAlertBox_PublishesNoTicker
ABoxShorterThanTheStrip_ClipsRowsInsteadOfOverrunning
Updated AlertLayer_PublishesARealTickerFrameToThePreviewSink, which asserted the old
1920 width from a box with no geometry (defaulted to 1px); it now uses the product's
own default box (680x200 at 620,430, MainViewModel.Sources.cs:54-57) and pins the
frame size and origin.
Full suite 367/367.
Take 11 (c10ce06c) validated the off-UI architecture: typical frames land
work ~10ms + wait ~6.8ms = 16.7 exactly on the deadline; 212/300 best yet.
The ENTIRE remaining gap is periodic 35-65ms render spikes that WORSENED
across the take (189 -> 147) — the signature of gen2 GC pauses. Biggest
churner is structural: the screen capture minted a fresh ~8.3MB byte[] per
DWM frame (~500MB/s of LOH), a producer OBS never does (it owns fixed
surface pools).
- ScreenCaptureFrameSource: 4-deep buffer ring with size-matched slots (a
<=17ms consumer cannot be lapped at 60Hz) + reused downscale row scratch.
- VideoFrame.Epoch: monotonic per producer frame. The paste cache keys on
array IDENTITY, so recycled arrays MUST be distinguished — epoch joins the
PasteKey. Producers handing fresh arrays leave it 0 (key unchanged effect).
- Stats print 'gen2 +N' per 5s window: next take acquits or convicts GC
without another guess (rule: prove the stage).
- Test (the ONE): PasteCache_RecycledArrayWithNewEpoch_ReRasterizes_NotStaleHits
— same array, new content, bumped epoch; fails on the old key by
construction. 37/37 compositor/pump, clean build.
- Next suspect if gen2 stays hot: the 10Hz WebView2 capture (full-canvas PNG
decode + fresh arrays on the UI thread) — recorded, untouched.
Creator audio ask queued in the same working session (+40% post-mix master
gain before the -1dBFS limiter) lands as its own commit next.