Files
LlamaCasty/HANDOFF.md
T
gramps fbc8562cf4 perf(capture): slice 8 — buffer ring + paste-cache Epoch; gen2 visibility (take-11 spikes)
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.
2026-09-04 12:36:14 -07:00

127 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/10 + slices 6-7 (2026-09-04):** slice 6 broke the 15.6ms Task.Delay sleep quantum (timeBeginPeriod + bulk-sleep + 2ms spin tail + wait stat); take 10's `wait 10ms after render 22ms` then exposed the FINAL structural bug: the pump loop's await-continuations inherited the UI SynchronizationContext — the "WPF-free" producer had been rendering ON THE DISPATCHER, queued behind the live preview, the whole time (explains every 'zero change' complaint). Slice 7: Task.Run the loop (OBS pattern) + StaticPixelCache lock + chat raster-miss marshalled to dispatcher + SustainedLowLatency GC + `worst render` stat + webcam through paste cache. Test `Pump_Produces_OffTheStartingContext`. 70/70 green, clean build. 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 11 ran + slice 8 (2026-09-04, committed with the audio unit below):** off-UI loop
WORKED — typical frames land exactly on the deadline (work ~10 + wait ~6.8 = 16.7; 212/300).
Remaining gap = periodic 35-65ms spikes growing across the take = gen2 GC pauses; the capture
path minted a fresh ~8.3MB byte[] per DWM frame (~500MB/s LOH). Slice 8: 4-deep buffer ring
in ScreenCaptureFrameSource (size-matched slots, reused row scratch) + `VideoFrame.Epoch`
joins the paste-cache key so recycled arrays can never false-hit + `gen2 +N` per stats window
(suspect-or-acquit — never guess at the tail again). Test `PasteCache_RecycledArrayWithNewEpoch_
ReRasterizes_NotStaleHits` fails on the old key by construction. 37/37 compositor/pump green.
4b. **AUDIO (creator ask 2026-09-04):** post-mix volume up ~40% — constant master gain applied
in AudioMixer.FillAndMix BEFORE the −1 dBFS limiter (limiter still owns the ceiling: the
gain can't add clipping, it just makes the limiter bind sooner on hot material). If the
creator reports pumping on loud game audio → the constant is the tuning knob (or per-input
defaults need raising instead — discuss with data).
5. **Take 12 (user, ~30s record + talk):** stats: `gen2 +N` should be ~0-1/window, `worst render`
→ ~16-20ms, n/300 → 300; playback honest speed; file audio ~40% hotter than before. If gen2
still >2/window: next suspect is the 10Hz WebView2 capture (full-canvas PNG decode + fresh
arrays on the UI thread) — throttle or move it, cited pattern first.
6. **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).
3. **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.