Files
LlamaCasty/HANDOFF.md
T
gramps 1c48849853 perf(chat): slice 3 — raster-on-change cache in ChatOverlayLayer (take-5 fix)
Take 5: render 58.9 -> 25.5ms (138/300, still ~2.2x). The blits were fixed; the
resolver was not: ResolveOutputFrame -> RenderChatBox ran a FULL WPF raster
(FormattedText + RenderTargetBitmap + CopyPixels + channel swap) EVERY tick
whenever the chat buffer was non-empty — and the buffer survives sessions, so
even a signed-out record-only take paid it. Established answer (OBS text
sources): re-render on change, blit the cache every tick.

ChatOverlayLayer: content version bumped from Messages.CollectionChanged
(covers adds, the 500-cap removal, the fade Clear from any caller) + a config
key (size + all Chat* appearance props); RenderFrame returns the cached
VideoFrame by identity until either changes (compositor only reads cached
frames). Conservative ordering (version latched BEFORE render) makes a
mid-render message re-render next tick, never serve stale.

ONE integration test: ChatOverlayLayerCacheTests (RealApp, real renderer):
Same() for unchanged inputs, NotSame() on message/config change, null on
empty. Full regression green (62 across touched classes), clean build 0
warnings. Accepted cost pending take 6: one ~15-25ms tick per arriving
message; if live-chat bursts sag n/300, next slice = debounced off-tick
re-render. Docs same commit: ai.md pipeline section, TASKS.md TASK 18,
MyMistakes recipe (raster-on-change + session-surviving-buffer trap),
HANDOFF (take 6 -> then Unit B, spec unchanged).
2026-09-04 11:01:51 -07:00

6.4 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. Take 6 (user, ~30s record-only): expect ≈300/300 frames per 5s, avg render ≤ ~10ms. If chat-burst drops one frame per message (avg fine but n/300 sags during live chat): debounced off-tick re-render is the next slice. If steady at ~300 with render <10 → recording saga CLOSED, go Unit B (below).
  3. Unit B — the top bar + session logic (user spec 2026-09-04, decisions settled):
    • 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.
    • 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).
  4. 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.