Files
LlamaCasty/HANDOFF.md
T

127 lines
9.7 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. **Slices 6-7 (2026-09-04, committed):** slice 6 broke the 15.6ms Task.Delay sleep quantum
(timeBeginPeriod + bulk-sleep + 2ms spin tail + `avg wait` stat — the quantum had capped the
producer at ~27fps and made two real render wins read as "zero change"). Slice 7 then resolved
take 10's impossible math (`render 22 + wait 10` vs a 16.7 deadline): the pump's continuations
inherited the UI SynchronizationContext — the loop had been rendering on the dispatcher behind
the live preview the whole saga. Task.Run the loop (OBS pattern), StaticPixelCache locked, chat
raster-miss marshalled to the dispatcher, SustainedLowLatency GC, `worst render` stat, webcam
through the paste cache. Tests Pump_Paces/Pump_Produces_OffTheStartingContext; 70/70 green.
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.