Files
LlamaCasty/HANDOFF.md
T
gramps 27bf74389d diag(build): per-build GUID stamp + resolve/blit timing split (take-6 attribution failure)
Take 6 measured render 35-41ms — WORSE than take 5's 25.5 — and the run could
not be attributed to a binary: exe mtime != build contents (incremental builds
serve stale exes; a source edit without rebuild is a silent old binary). Three
takes of a perf saga had been judged against builds nobody could prove.

- ytLive.csproj GenerateBuildStamp target: fresh GUID per compile (writes
  obj/BuildStamp.g.cs -> Helpers/BuildStamp.Id/BuiltLocal). Deliberately defeats
  incremental lies: every 'dotnet build' recompiles the app project.
- Wordmark shows the id as a superscript (TopBar.xaml, x:Static, 9px grey
  BaselineAlignment=Superscript); startup.log records 'Build <id> (compiled
  <time>)' so every take is cross-readable with the visible UI.
- FramePump stats split the tick: 'avg render Xms (resolve Y), avg submit Z' —
  the resolver is timed separately (wrapper resolver on per-tick paths; bake
  keeps the raw one) so take 7 names the hot half of 'render' with data.
- Fixed a latent transition-clock bug found on the way: lastTick now restarts
  every frame (the branch rework had restarted it only during transitions,
  letting a transition begun after idle complete instantly on its first Tick).

ONE integration test family: BuildStampTests (unit: shape) +
BuildStampDisplayTests (RealApp, namescoped FindName on TopBar proves the
wordmark SHOWS the id). 47/47 per-class green, clean build 0 warnings. Docs
same commit. User's top-bar/session spec (re-sent twice) + settled Q&A
decisions folded into HANDOFF Unit B — next work unit after take 7 verdict.
2026-09-04 11:26:46 -07:00

7.7 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. BUILD STAMP shipped (slice 4, same day): take 6 came back 35-41ms — WORSE than take 5 — and attribution was impossible (exe timestamp ≠ binary contents; incremental builds served unverified exes). Every build now stamps a fresh GUID (ytLive.csproj GenerateBuildStamp → Helpers/BuildStamp), shown as the wordmark superscript (build id) + Build xxxxxxxx (compiled ...) in startup.log; stats report avg render Xms (resolve Y) so the hot half of the tick is named. Tests BuildStampTests + BuildStampDisplayTests (real window, namescoped FindName).
  3. Take 7 (user, ~30s record-only): FIRST read the superscript + startup.log Build line — the take is meaningless without it. Expect slice 3 in any post-stamp build: avg render ≤ ~10ms (resolve ≪ render), ≈300/300. If resolve dominates → resolver-side surprise (chat config thrash / web frame); if blit dominates → measure the general-path element sizes. If chat-burst sags n/300: debounced off-tick re-render slice. If clean → recording saga CLOSED, Unit B.
  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.