Files
LlamaCasty/HANDOFF.md
T
gramps 09a866e6a1 perf(pump): slice 6 — break the 15.6ms sleep quantum (the REAL ceiling behind takes 7-9)
Take 9's numbers were decisive: paste cache moved work to ~25ms/frame but the
period stayed ~37ms. The missing ~12ms per tick is Task.Delay rounding every
sub-tick request up to the Windows system-clock tick (~15.6ms default —
documented: learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.delay).
A frame finishing 3ms early requested 3ms and slept 15.6. Producer capped at
~27fps no matter how fast the compositor got — which is why two real render
fixes read as 'zero change' in playback. Game-loop/OBS canon for this
(stackoverflow.com/questions/5441464; learn.microsoft.com/en-us/windows/win32/
api/timeapi/nf-timeapi-timebeginperiod): raise the timer resolution for the
session, sleep only the bulk of the remainder, and SPIN the last ~2ms across
the deadline.

- FramePump: timeBeginPeriod(1) on entering the pump loop, timeEndPeriod(1) in
  the finally; pacing = bulk _pacingDelay(ahead - 2ms) + bounded Thread.SpinWait
  tail; blown deadlines rebase unchanged (never burst).
- Stats now report avg wait: render+submit+wait must equal the period — the
  accounting is closed, no stage can hide in an unmeasured gap again.
- Webcam dropped its IsOpaque paste-cache bypass: it re-sampled ~156k px every
  tick even between identical device frames; cached paste beats the sampler on
  hits, costs the same on misses.

52/52 per-class green (pacing + pixel suites unchanged — output byte-stable),
clean build 0 warnings. Docs same commit (ai.md slice 6, TASKS.md take-10
gate, MyMistakes #3 + renumber, HANDOFF). User's top-bar spec remains next in
queue (Unit B) — re-sent many times, captured, no open questions.
2026-09-04 12:02:24 -07:00

8.6 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 ran + slice 6 (2026-09-04, THE FINDING): paste cache moved render 26.5→22.4ms yet period stayed ~37ms — the gap is Task.Delay's ~15.6ms Windows sleep quantum padding every sub-tick wait. This is why takes 7→9 read as "zero change" despite real wins: the sleep floor dominated. Fixed (media-app canon, cited in code + MyMistakes #3): timeBeginPeriod(1) for the pump's life (paired End in finally), sleep the bulk, SPIN the last 2ms across the deadline; stats gained avg wait so render+submit+wait ≈ period (accounting closed — nothing can hide). Webcam now routes through the paste cache too (bypass re-sampled 156k px even between identical device frames). 52/52 per-class green, clean build 0 warnings, committed this slice.
  4. Take 10 (user, ~30s record-only): read the superscript; stats must show ≈300/300, avg render ~22, wait ~0-3 — honest 60fps IF render+submit ≤ ~16.7. If wait is near zero and n/300 sits at ~200, the remaining gap is pure render 22ms → next slice = per-phase compositor timing (the stats can split blit phases the same way they split resolve; that is the honest path, not a guess). If take 10 lands at ~300/300 → recording saga CLOSED.
  5. 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).
  6. 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.