Files
LlamaCasty/HANDOFF.md
T
gramps 6af2026906 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.
2026-09-04 11:44:19 -07:00

7.8 KiB

HANDOFF — Session State

Branch / Commit State

main — Unit A (render starvation, slice 2) committed locally this session; push on the user's word (his pattern: says "push" explicitly). Slice 1 (deadline pacing + row-blit, take-3 fix) is already on origin/main as 716a77f.

What just happened (2026-09-04, Unit A slice 2)

Take 4 verdict: pacing HELD (no stall cliff, sync intact — user confirmed game+webcam in-sync) but render stayed 58.9ms (budget 16.7) → file still ~3.4x time-lapse. Root: the 2M-iteration managed row walk + 8.3MB fresh buffer every tick. Shipped:

  1. VideoFrame.IsOpaque producer-contract flag — set ONLY by ScreenCaptureFrameSource + MediaCaptureFrameSource (DWM/MF fill alpha 255). Full-canvas aligned blit of an opaque frame = ONE Buffer.BlockCopy; black pre-fill skipped when the backdrop covers.
  2. Integer fixed-point bilinear in BlitContent general path (webcam: round/mirror/scaled) — no divisions, no Math.Round; ±1 of the float reference (tests allow ±2).
  3. Pump scratch pool — AcquireScratch/ReleaseScratch (max 4, length-keyed, owned-by-reference so bake-cache/social-bar/static-art arrays can never be captured). Release strictly AFTER SubmitFrameAsync returns (stdin write copies). FakeEncoder now snapshots frames like the real encoder (holds would race legitimate recycling).
  4. Dead code kill: the per-tick fromScene render in the transition branch fed NOTHING (BlendFrame uses TransitionService.FromFrame captured at Start) — removed with the fromSceneProvider seam + MainViewModel.cs call site. Transition cost halves as a side effect.

Bugs caught by the pixel probes before shipping (see MyMistakes take-4 follow-ups): first Bilinear double-shifted (both stages scaled → solid-255 sampled to ~1 → general path drew nothing); sentinel 0xAB collided with a legitimate x+y value; pacing-fake synchronous completion hangs vstest (known, re-trod).

Verification: clean build 0 warnings (both projects); per-class vstest 59/59 (FramePump 11 incl. the new pooling test, SceneCompositor incl. NEW Composite_OpaqueFullCover..., SceneGraph, SocialBar, StretchMath, Camera/ScreenCapture/MediaVideoSource producers, WebcamOutputKey, SessionTeardown) + SourceNaming RealApp boot-smoke. Scope-check passed.

OPEN — next, in order

  1. Take 5 happened (2026-09-04 10:49): 138/300 frames per 5s, avg render 25.5ms — blits fixed but the resolver's RenderChatBox full-rasterized the chat box EVERY tick whenever the message buffer was non-empty (the buffer survives sessions — a signed-out record-only take paid chat render cost!). Slice 3 shipped same day: ChatOverlayLayer content-versioned cache — raster on message/config change, blit the cached frame every tick (OBS text-source pattern). Tests: ChatOverlayLayerCacheTests (the ONE, RealApp) + full regression green (62 across touched classes), clean build 0 warnings.
  2. Takes 7/8 + slice 5 (2026-09-04): the stamp (f190587b) settled attribution — chat cache REAL (resolve ≈ 0) but render stayed 26-27ms: the compositor re-rasterized every non-opaque layer per tick (his Live scene: chat+web+image+cam ≈ 680k samples @ ~38ns). Slice 5: BlitCachedLayer — one raster per (source-array, rect, round/mirror), paste at integer pos with opacity; static layers now cost row-blends, only content changes resample; drag/opacity changes are paste params, not cache keys. Tests: PasteCache_... + 85/85 across compositor/pump/chat/capture/session classes; clean build 0 warnings. (Cam bypasses the cache via IsOpaque; revisit if take 9 is borderline. Follow-ups unchanged: vertical-tier alloc, debounced chat re-render on bursts.)
  3. Take 9 (user, ~30s record-only): confirm the wordmark superscript matches the startup.log Build line, then stats must show ≈300/300, avg render ≤ ~8ms and playback 1x. If yes → recording saga CLOSED, Unit B starts. If render ~10-15 → cam-resample cache (drop the IsOpaque bypass); if n/300 sags only during chat bursts → debounced off-tick raster slice.
  4. Unit B — the top bar + session logic (user spec 2026-09-04 re-sent twice + decisions settled in Q&A):
    • Two-line top bar. Line 1: center = REC + LIVE pills (text renamed from ON-AIR; pills become mutually-exclusive RADIOS — record-OR-stream ruling), right = avatar + Login/Logout button (no account status light). Line 2: centered primary Start (grayed while NO pill armed — INVERTS the 2026-09-01 "unarmed Start records" rule; fix the map when landing) that becomes the Stop/End button while active.
    • Avatar right-click → Change Account (creator: "standard google thing ... on a portrait right-click"). Login = SignInCommand direct (context menu on Start dies; "Choose Record Folder" lives in gear → App Settings only).
    • LIVE pill stays login-gated (CanToggleOnAir exists ✓ 3a.viii).
    • REC+Start → Microsoft.Win32.SaveFileDialog (InitialDirectory = settings folder, default name ty-…-0000.mp4, NATIVE overwrite prompt covers exists/validate, Enter confirms). Cancel → abort + disarm pill (lit pill with no session is a lie). Up-front naming RETIRES the stop-time rename modal (assumption stated; user's dialog answer was about Go-Live confirmation).
    • LIVE+Start → Go-Live dialog stays as preflight: prefilled from Text-drawer Broadcast.*, unfilled fields visibly prompted, explicit confirm → PrepareAndStartLiveAsync (user: "going live is scary — confirmation allows back-out + testing up to go-live").
    • Bottom-bar metrics init/maintain: ResetHealth + HealthUpdated exist — verify on take 6.
    • F6 "start/end" hotkey routes through HandleHotkey — check it honors the new grayed-Start gate.
    • Login button text: "Login" (disconnected, LIVE pill greyed) → "Logout" (connected, avatar appears left of it). The account status LIGHT is deleted per spec 2a.
    • Primary button: grayed "Start" when NO pill armed (inverts 2026-09-01 rule — fix the map in the landing commit); enabled when either armed; REC path = file dialog flow; LIVE path = go-live after dialog confirm; becomes the Stop/End face while active (user's "Stop button never active" complaint gets a hermetic test pinning visibility+CanExecute).
    • ONE integration test (hermetic): pills↔button state machine + record-path seam (an internal static Func<SaveFileDialog-ish prompt> override seam mirroring RegistrarOverride — never pop real dialogs in tests).
  5. Follow-ups recorded (don't fix opportunistically): vertical-tier BilinearScale per-frame alloc; cosmetic FramePump: encoder stop failed: No process is associated double-stop race; 1440p capture downscale alloc.

Landmines

  • testhost shares startup.log with the app — filter by time when triaging.
  • Stale testhost/exe locks the DLL (MSB3027): taskkill /F /IM testhost.exe / ytLive.exe first.
  • Do NOT run full-suite vstest (WASAPI hang, pre-existing); flow = clean build + per-class + scope-check.
  • Pacing-seam fakes MUST await/yield (sync-completed task → pump runs inline on StartAsync → hang).
  • FakeEncoder snapshots submitted frames — keep any new fake encoder honest about buffer recycling.
  • Multi-stage fixed-point: shift only at the end (MyMistakes 2026-09-04).
  • Real-MainWindow tests: LayoutPathOverride + temp DB mandatory; VolumePushOverride for volume.
  • ffmpeg: month-end pinned build; Startup.log "Recording saved:" lines show the real final path.

@ User note

Recording fix FIRST (his order), UX queue right behind — spec + settled decisions above, don't re-ask. Keep responses SHORT; one integration test per change; commit every unit; push on his word only.