Files
LlamaCasty/TASKS/task-38-window-backdrop.md
T
gramps 096bf6debe docs(memory): split capture (backdrop, webcam, effects, vertical tier) out of ai.md
Unit 2 of slimming the mandatory read. Verbatim move again - the four capture
blocks leave ai.md unchanged in content, and one stub carries the law.

- docs/ai/capture.md: 'Screen backdrop capture', 'Webcam capture', 'Webcam
  background effects' and 'Vertical (9:16) tier camera placement', byte-
  identical, grouped under backdrop / webcam / vertical headings, with a
  'Related in ai.md' footer.
- ai.md stub keeps: the locked-Background model and NormalizeBackgrounds;
  ActiveBackgroundImage is fed by static art only (2026-08-23 regression);
  window:/picker: keys are session-scoped and a live pin beats auto-detect;
  the settled WinRT interop paths (TryGetDataUnsafe, from-DXGIDevice); the
  VideoFrame seam, own-catch subtype ladder and no fallback camera; one app-
  level webcam identity with live-lock attainability; CameraManager as the one
  effects processing point with per-session mask smoothing; geometry-only
  vertical tier and the model-level rect lock.
- docs/ai/index.md gains the capture.md row; Services/index.md (2) and
  TASKS/task-38-window-backdrop.md repointed.

ai.md 1,975 -> 1,558 lines. No source touched; no push.
2026-10-26 10:26:53 -07:00

5.2 KiB

TASK 38 — Capture Window… backdrop (in-app window picker submenu)

Status: ✅ Done — shipped 2026-09-21 (Good Dog: ONE integration test)

Idea (creator ruling 2026-09-21)

Add an in-app "Capture Window…" submenu (PreviewPane backdrop context menu + the Live layer-row menu) listing every visible top-level window owned by another process. Picking one pins that window as the full-bleed Live backdrop at layer 0 — the same render path as desktop/game capture, captured via the pre-existing window:<hwnd> key in ScreenCaptureSourceFactory.Resolve (no new capture class, no compositor change).

Rulings locked in the plan:

  1. In-app submenu, not the OS GraphicsCapturePicker — the picker dialog is a different flow ("Change Capture…"); this is a catalog of open windows.
  2. Captured window is the Live backdrop at layer 0; the app's own window never goes OS-fullscreen.
  3. Session-scoped, never persisted — like picker: keys. "A new session is a new session": a persisted window:<hwnd> key must NOT be resurrected on reload; an HWND can be recycled by the OS, so resurrecting it could capture an unrelated window (the exact hazard that makes persistence wrong).
  4. Pin wins while alive: while the pinned window still enumerates, the auto fullscreen-game / desktop detector must not steal the backdrop.
  5. On window death → auto-fallback through the existing chain (fullscreen game → desktop → static art). No dead ends.
  6. Manual "Refresh Window List" menu item included (creator picked the manual refresh option).

What shipped

  • Services/Win32WindowEnumerator.cs — real EnumWindows implementation of the IWindowEnumerator seam. Filters: visible, non-cloaked (DWMWA_CLOAKED), non-empty rect, other-process (never offers the app's own UI), non-empty title, non-empty process name; ordered by process name then title. Mirrors Win32FullScreenDetector's window predicates so picker and capture-detector agree.
  • ViewModels/MainViewModel.WindowBackdrop.cs (new partial) — OpenWindows (ObservableCollection<WindowInfo>), RefreshWindowsCommand, CaptureWindowCommand (mirrors RelayCommand pattern; routes to RedesignateBackgroundAsync(info.CaptureKey)), IsTransientCaptureKey (window: + picker:), IsAliveWindowPin, and OnScreenCaptureFailed (the transient-key-session-failure → auto-reacquire heal).
  • MainViewModel.Background.cs — _windowEnumerator field + WindowEnumeratorOverride test seam (mirrors CameraEnumeratorOverride); ReacquireScreenCaptures now strips transient window:/picker: keys before auto-detection; OnFullscreenMonitorChanged skips redesignation while a live window pin is active ("pin wins while alive").
  • MainViewModel.cs — ctor wires _windowEnumerator, RefreshWindowsCommand, CaptureWindowCommand, initial RefreshOpenWindows(), and routes CaptureFailed → OnScreenCaptureFailed.
  • UI: "Capture Window…" + "Refresh Window List" added to both the PreviewPane backdrop context menu (mirrors the "Capture Desktop" submenu binding pattern) and the Live layer-row menu (LeftPanel.xaml, Tag→IsBackground MultiBinding visibility like its siblings).
  • ONE integration test: ytLive.Tests/WindowBackdropIntegrationTests.cs (RealApp + temp DB + fake IWindowEnumerator via the static seam) — asserts (a) the seam feeds the submenu, (b) a persisted window:0x… key heals away on reload (never resurrected), (c) pinning an un-capturable window falls back to auto (no dead window: pin).

Pre-existing corruption fixed (out-of-scope prerequisite)

ytLive.Tests/PillRadioTests.cs:105 carried a committed stray token (Assert.Equal("Start", vm.PrimaryStartButtonLabel soil);) that broke the entire test-project compile — the file was unwritable and NO tests could run. Removed the stray soil token (only change to the file). This is the fix for the "committed syntax error" landmine.

Model / naming facts

  • Source.CaptureKey is persisted (window:<hwnd>, monitor:<n>, picker:<displayname>); Source.IsBackground marks the one Background per scene. See docs/ai/capture.md → "Screen backdrop capture".
  • WindowInfo (Models) — Title, ProcessName, Hwnd (long), ProcessId; Label => "{Title} — {ProcessName}"; CaptureKey => $"window:0x{Hwnd:X}".
  • IWindowEnumerator (Services) — IReadOnlyList<WindowInfo> Enumerate(); seam like IFullScreenDetector.
  • The catastrophic-accident guard lives ONLY at reload/reacquire. Mid-session, when the pinned window's capture session dies, ScreenCaptureManager.CaptureFailed → OnScreenCaptureFailed (transient key) → ReacquireScreenCaptures() → strips + auto-fallback.

Acceptance

  • Right-click live backdrop → "Capture Window…" lists open non-app windows with "Title — Process" labels.
  • Picking one makes it the full-bleed Live backdrop.
  • While the window lives, the fullscreen-game detector does not steal the backdrop.
  • Window dies → auto-fallback (game → desktop → static), no dead-end window: pin.
  • Closing/reopening the app never resurrects a prior session's window: key.
  • "Refresh Window List" re-enumerates.