Skip to content

feat(windows): per-window capture into a Flutter texture - #1

Merged
wenkaifan0720 merged 2 commits into
mainfrom
feat/windows-capture
Jul 25, 2026
Merged

feat(windows): per-window capture into a Flutter texture#1
wenkaifan0720 merged 2 commits into
mainfrom
feat/windows-capture

Conversation

@wenkaifan0720

Copy link
Copy Markdown
Collaborator

Implements the remaining v1 slice for the Windows backend, so both platforms now satisfy the full platform-interface contract.

Capture

Live capture via Windows.Graphics.Capture, delivered to Flutter as DXGI shared handles — pixels never round-trip through the CPU, matching what the macOS backend achieves with CVPixelBuffers.

Each frame is copied into one of two BGRA textures created with D3D11_RESOURCE_MISC_SHARED; the engine opens that handle directly. The pair rotates so the engine is never reading the surface being overwritten. The frame pool resizes itself when a source reflows, which is what keeps a live stream sharp after the user drags a window edge.

One-shot grabs (captureSingleFrame, captureSingleDisplayFrame) run on a worker with a 2s bound and encode through WIC, hopping back to the platform thread via a message-only-window task runner — a MethodResult cannot be completed off that thread.

Engagement events

This closes an honesty gap: the backend previously emitted only engaged and disengaged-on-release, so an externally moved or closed window silently desynced. SetWinEventHook now pushes the same three events the macOS backend does:

  • alignmentBroken on external move/resize
  • disengaged(windowClosed) on destroy
  • disengaged(thirdApp | hostActivated) on focus change

Engage and conform mask their own moves for 1.5s, mirroring macOS's selfMutationDeadline. Minimize stays a poll, as it is on macOS. detectSeparateWindows is implemented rather than stubbed to empty.

Verification

Ran against real windows on Windows 11: 21 checks covering enumeration, both one-shot paths, stream lifecycle, geometry, and the event stream.

The decisive check rendered a live Texture and then re-captured the harness's own window — the result shows one window composited inside another, which proves the shared-handle path end to end rather than inferring it from a frame counter. That test also caught IsCursorCaptureEnabled and IsBorderRequired being properties rather than methods, so the ApiInformation::IsMethodPresent feature check was silently leaving cursor capture on.

Builds clean in Debug and Release under /W4 /WX; all four packages analyze with no issues.

Notes for review

  • Display ids are HMONITOR values. Windows has no CGDirectDisplayID equivalent, so captureSingleDisplayFrame takes the monitor handle, and accepts '0' for the primary display.
  • getContentMeasurement reports zero insets by design — DWM composites shadows outside the window, so there is no transparent margin to crop. A consumer treating zero as "not yet measured" rather than "nothing to trim" would behave differently here than on macOS.
  • reconfigureStream reports rather than applies, since the capture pool resizes itself.
  • Shared-file edits (root README.md, pubspec.yaml description) are prose only; the macOS backend is untouched.

🤖 Generated with Claude Code

wenkaifan0720 and others added 2 commits July 24, 2026 14:23
Implements the remaining v1 slice for the Windows backend: live capture
via Windows.Graphics.Capture, one-shot PNG grabs, and the engagement
events the README already claimed were there.

Capture never round-trips pixels through the CPU. Each frame is copied
into one of two BGRA textures created with D3D11_RESOURCE_MISC_SHARED and
the engine opens that shared handle directly; the pair rotates so the
engine is never reading the surface being overwritten. The frame pool
resizes itself when a source reflows, which is what keeps a live stream
sharp after the user drags a window edge.

One-shot grabs run on a worker with a 2s bound and encode through WIC,
hopping back to the platform thread via a message-only-window task runner
— a MethodResult cannot be completed off that thread.

Engagement events close an honesty gap: the backend previously emitted
only engaged and disengaged-on-release, so an externally moved or closed
window silently desynced. SetWinEventHook now pushes the same three
events the macOS backend does — alignmentBroken on external move/resize,
disengaged(windowClosed) on destroy, and disengaged(thirdApp |
hostActivated) on focus change — with engage and conform masking their
own moves for 1.5s, mirroring macOS's selfMutationDeadline. Minimize
stays a poll, as it is on macOS. detectSeparateWindows is implemented
rather than stubbed to empty.

Verified against real windows by rendering a live Texture and then
re-capturing the harness's own window: the result shows one window
composited inside another, which is what proves the shared-handle path
end to end rather than inferring it from a frame counter. That test also
caught IsCursorCaptureEnabled and IsBorderRequired being properties
rather than methods, so the ApiInformation::IsMethodPresent feature check
was silently leaving cursor capture on.

Builds clean in Debug and Release under /W4 /WX.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
C++/WinRT routes its coroutine support through <experimental/coroutine>
when built as C++17. MSVC 14.51 hard-errors on that header (STL1011)
ahead of removing it, which broke CI on the VS 18 runner while building
fine on a local 14.44 toolchain.

Raising just this target to C++20 makes C++/WinRT select the standard
<coroutine> header instead, rather than silencing the deprecation with
_SILENCE_EXPERIMENTAL_COROUTINE_DEPRECATION_WARNINGS — the experimental
header is going away, so suppressing it only moves the break later.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wenkaifan0720
wenkaifan0720 merged commit 0c6f518 into main Jul 25, 2026
2 checks passed
@wenkaifan0720
wenkaifan0720 deleted the feat/windows-capture branch July 25, 2026 00:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant