From 99aeef7994ad73b49dce18c438365fd2db6feaa1 Mon Sep 17 00:00:00 2001 From: gramps Date: Sat, 5 Sep 2026 14:25:26 -0700 Subject: [PATCH] =?UTF-8?q?docs:=20HANDOFF=20=E2=80=94=20rollback=20record?= =?UTF-8?q?=20+=20roll-forward=20plan=20(2026-09-04=20unfuck=20session)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HANDOFF.md | 204 +++++++++++++++++++++++++++-------------------------- 1 file changed, 104 insertions(+), 100 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index 1b2215e..65f5cf5 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -2,112 +2,112 @@ ## 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`. +**`main` HEAD = `c46f3a1` (#9)** — tree identical to `fbc8562` (slice 8, single-loop pump). Working tree +CLEAN. This is the user-confirmed **last working commit** — output recordings were tear-free here and +time-lapse again (the 7fcb2ad-era pacing, expected pre-buffer-ring). -## What just happened (2026-09-04, Unit A slice 2) +**`today-slices` branch @ `ce87490` (#17)** — preserves ALL 17 commits of the 7fcb2ad→ce87490 chain +(1 `716a77f` … 17 `ce87490`), including the two-loop producer, encoder staging fix, chat fix, e2e +harness. Nothing is lost; never hard-reset away from it without preserving. -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: +## ROLLBACK RECORD (2026-09-04) -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. +- **Rolled back to #9 `c46f3a1`** via `git reset --hard` on `main` (user-directed) after the user + confirmed #10 introduced the tearing. Rolling forward WITHOUT #10 (per user directive) starts from + this commit. +- **`today-slices` @ `ce87490`** preserves the entire 17-commit chain — the fallback recovery point. -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). +## The commit list (for picking this up anytime) -**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. +Numbered oldest→newest (#0 = the working baseline the user picked: `7fcb2ad` "zero known failures era +begins"). **#10 is the breaking commit** (two-loop producer = the recorded-output tearing). #9 is the +last working commit. \(SHAs = full 40-char hashes below\): + +| # | SHA | Subject | +|---|-----|---------| +| 0 | `7fcb2ad…` | baseline: "zero known failures era begins" (last build the user trusted before the chain) | +| 1 | `716a77f` | take-3 starvation fix (deadline pacing + row-blit) | +| 2 | `432adfd` | slice 2 — integer bilinear + pump scratch pool | +| 3 | `1c48849` | slice 3 — chat raster cache | +| 4 | `27bf743` | GUID build stamp | +| 5 | `6af2026` | slice 5 — paste cache | +| 6 | `09a866e` | slice 6 — sleep quantum fix (timeBeginPeriod + spin tail) | +| 7 | `eb4c379` | slice 7 — pump off UI thread | +| 8 | `fbc8562` | slice 8 — screen-capture buffer ring + Epoch + gen2 stat | +| 9 | `c46f3a1` | **docs — LAST WORKING (tree = slice 8); main sits here now** | +| 10 | `8260474` | **⚠ BREAKS: two-loop producer — introduced the output tearing; never lands** | +| 11 | `6fd1d9c` | #11 — release counter in wordmark + capture/camera/web rings 4→8 (FramePump hunk = two-loop-only, drop) | +| 12 | `d1126dd` | #12 — compositor per-element raster reuse + MJPEG camera negotiation (FramePump hunk = two-loop-only, drop) | +| 13 | `e4621c1` | #13 — docs (HANDOFF rewrite) | +| 14 | `b4f6a92` | #14 — docs (recording audit) | +| 15 | `7bf00a9` | #15 — e2e producer-smoothness bar (thresholds tuned to two-loop; re-tune on single loop) | +| 16 | `1a808ae` | #16 — chat fix: empty-buffer chat box composites | +| 17 | `ce87490` | #17 — encoder staging snapshot (the genuine tear cure; belt-and-suspenders on the single loop) | + +Full SHAs: 0=`7fcb2ad…`(baseline) · 1=`716a77f` · 2=`432adfd` · 3=`1c48849` · 4=`27bf743` · 5=`6af2026` +· 6=`09a866e` · 7=`eb4c379` · 8=`fbc8562` · 9=`c46f3a1` · 10=`8260474` · 11=`6fd1d9c` · 12=`d1126dd` +· 13=`e4621c1` · 14=`b4f6a92` · 15=`7bf00a9` · 16=`1a808ae` · 17=`ce87490`. + +Roll-forward plan (user's method, ONE at a time, user tests each): apply #11 → #17, skipping #10 +entirely; drop each commit's two-loop-only FramePump hunks. + +## What just happened (2026-09-04, bisect verdict) + +The user bisected the 17-commit chain by directing resets of `main` + builds + live runs. **Verdict:** + +- **#10 `8260474` (slice 9, the two-loop producer) INTRODUCED the screen tearing over the layered + elements in the recorded output file.** #9 (`c46f3a1`) does NOT have it. Tomm every time. +- **Mechanism (closed):** RenderLoopAsync owns a private 4-deep crop ring and publishes the newest + composite; OutputLoopAsync calls `SubmitFrameAsync(published.Frame)` — and at #10 the encoder still + writes `frame.BgraPixels` DIRECTLY into the async stdin pipe (no copy). When the encoder/pipe stalls + >~50ms, the free-running render loop laps the 4-deep ring and REPAINTS the array being drained → + the recorded frame is a mix of two composites = the tearing. Preview is immune (synchronous + WriteableBitmap copy). The #10 code comment admits it: "torn frame if the renderer laps the ring + during a >~50ms encoder stall — bounded cosmetic risk". #9's single serial loop cannot tear by + construction (same thread owns render→submit→release). +- The genuine fix for the tear is `Service c87490`'s core: snapshot `frame.BgraPixels` into an + encoder-owned staging buffer inside `SubmitFrameAsync` BEFORE the async write. It was bundled into + #17 and rejected wholesale with it. + +**DECISION (user directive):** roll forward from #9 WITHOUT landing #10's two-loop producer. +**User's method:** apply #11→#17 one at a time; FIRST tell him what the commit does/fixes; HE tests; +we proceed or drop. Do exactly what is asked, no more, no suggestions, until the code is "unfucked". + +## Verified contents of #10 (for the roll-forward) + +**Unsafe (the tear, never lands):** `Services/Encoder/FramePump.cs` — the whole two-loop rewrite. +**Safe, producer-side allocation only (no render/submit-path contact):** +- `Services/Audio/AudioMixer.cs` — `MasterOutputGain = 1.4f` pre-limiter (+40% creator ask; paired + `AudioPipelineTests` bound 0.55→0.7). +- `Services/MediaCaptureFrameSource.cs` — 4-deep camera ring + `Epoch` (ends ~110-220MB/s LOH churn). +- `Services/WebView2Manager.cs` — reused canvas scratch + 3-deep out-ring + reused WriteableBitmap + (ends two fresh arrays + a new bitmap per capture tick). + +## What was attempted / aborted this session + +Cherry-pick of `6fd1d9c` (#11) onto #9 was started, hit conflicts, and was ABORTED on the user's order. +Cleanup done: `cherry-pick --abort` fails for `-n` (no sequencer state) → `git reset --hard HEAD`. +Tree verified clean at #9. **Learned (record before re-applying):** #11 on the #9 base = release +counter (#11's "non-ring half", [TopBar.xaml, ytLive.csproj, BuildStampTests.cs]) + ScreenCapture ring +4→8 + NEW camera ring + NEW WebView2 pooling (these blocks are ADDED fresh in #11 at depth 8, not just +widened) + docs. #11's FramePump hunk (`ring = new byte[8][]`) is two-loop-only → DROP. #12's FramePump +hunk (stats formatting) is two-loop-only (`TryReportStats`) → DROP. #13/#14 are docs-only. #15 e2e bar +was thresholded against two-loop measurements. #16 (chat) and #17 (encoder staging) are pump-independent +and apply cleanly. ## 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` 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. +1. **#11 `6fd1d9c`** — tell user what it does (release counter; capture rings 4→8 + new camera/web + rings), apply via cherry-pick with the two-loop-only FramePump hunk dropped; user tests; proceed or drop. +2. **#12 `d1126dd`** — compositor raster reuse + MJPEG camera negotiation + stats formatting (FramePump + hunk dropped). +3. **#13 `e4621c1`**, **#14 `b4f6a92`** — docs. +4. **#15 `7bf00a9`** — e2e smoothness bar (re-tune thresholds to single-loop measurements if needed). +5. **#16 `1a808ae`** — chat empty-buffer composite fix + test. +6. **#17 `ce87490`** — encoder staging snapshot (the genuine tear cure — applies on the single loop as + belt-and-suspenders). +7. **Unit B (top bar + session logic)** — spec + settled decisions are in the old HANDOFF history on + `today-slices`; don't re-ask. ## Landmines @@ -119,8 +119,12 @@ RealApp boot-smoke. Scope-check passed. - 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. +- Kill the app (`/mnt/c/Windows/System32/taskkill.exe /F /IM ytLive.exe`) before any build that + replaces the running binary. ## @ 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. +**Do EXACTLY what the user asks, no more, no suggestions.** He is driving the unfuck personally: +commit-by-commit roll-forward, he tests each, proceed or drop. Keep responses SHORT. He is high-voltage — +do not spin, do not theorize, do not touch docs/commits he didn't order. Commit every work unit; push on +his word only. \ No newline at end of file