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

83 lines
5.2 KiB
Markdown

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