Windows.Graphics.Capture: Frame-Pool Lifetimes, Resize, and Backpressure
Build a Windows.Graphics.Capture pipeline that drains frames safely, handles resize and device changes, bounds downstream work, and shuts down cleanly.
Windows.Graphics.Capture is a consent-mediated way to receive frames from a selected display or application window. In a continuous pipeline, GraphicsCaptureItem, GraphicsCaptureSession, Direct3D11CaptureFramePool, and each Direct3D11CaptureFrame have different responsibilities and lifetimes. The difficult bugs usually come from letting a borrowed frame outlive its pool checkout, processing on the wrong thread, retaining work faster than it can be consumed, or resizing resources in the middle of a callback.
This guide focuses on the D3D11 frame-pool lifecycle in C# with WinRT projections. Check GraphicsCaptureSession.IsSupported() at runtime and obtain the target through a user-authorized route, such as the system picker. A WinUI 3 desktop app must initialize that picker with its owner HWND; a UWP app must declare the Graphics Capture capability in its package manifest. Handle unsupported hardware and picker cancellation explicitly.
Pick the callback model deliberately
Direct3D11CaptureFramePool.Create is the dispatcher-associated option. The Microsoft Win32 sample creates a DispatcherQueue for it, and the screen-capture guide warns that doing heavy work for FrameArrived on the UI thread can make the application unresponsive. Use this model when your application already owns a suitable dispatcher and the callback can promptly hand work to another stage.
CreateFreeThreaded removes the DispatcherQueue dependency and raises FrameArrived on the pool’s internal worker thread. It was introduced in Windows 10 version 1809, build 17763, with UniversalApiContract v7. The base Direct3D11CaptureFramePool and GraphicsCaptureSession arrived in Windows 10 version 1803, build 17134, with contract v6. If supporting 1803, check ApiInformation.IsMethodPresent before calling CreateFreeThreaded, or provide a dispatcher-backed path. An SDK reference does not make a method exist on an older runtime.
The free-threaded callback is not a UI thread. Do not touch XAML controls or composition objects that require UI-thread affinity from it. In WinUI 3, enqueue those updates through the window’s DispatcherQueue.TryEnqueue. Keep device creation and Direct3D resources compatible with the thread model you use, and serialize access to a shared immediate context if other application code can use it concurrently.
Start with an explicit owner and matching size
Create the D3D device first, wrap it as the WinRT IDirect3DDevice expected by the capture API, and record who owns it. Then use the selected item’s current Size to create the pool, register both event handlers, create the session, and start capture. The snippet assumes Windows 10 version 1809 or later; on 1803, use a dispatcher-backed path. _device is already a WinRT Direct3D device, and production code should validate nonzero dimensions and unwind partial setup if a step throws.
private const int BufferCount = 3;
private static readonly DirectXPixelFormat PixelFormat =
DirectXPixelFormat.B8G8R8A8UIntNormalized;
private void Start(GraphicsCaptureItem item)
{
if (!GraphicsCaptureSession.IsSupported())
throw new NotSupportedException("Graphics capture is unavailable.");
_item = item;
_bufferSize = item.Size;
if (_bufferSize.Width <= 0 || _bufferSize.Height <= 0)
throw new InvalidOperationException("The capture target has no usable size.");
_framePool = Direct3D11CaptureFramePool.CreateFreeThreaded(
_device, PixelFormat, BufferCount, _bufferSize);
_framePool.FrameArrived += OnFrameArrived;
_item.Closed += OnItemClosed;
_session = _framePool.CreateCaptureSession(_item);
_session.StartCapture();
}
The value 3 is an example, not a universal optimum. numberOfBuffers describes how many buffers the pool stores; it is not a guarantee that an application-side encoder, analyzer, or disk writer can keep up. Choose a small measured pool and instrument frame age, processing duration, and downstream queue depth. A larger pool may absorb a brief scheduling delay but also retains more image memory and can deliver older work. The API documentation does not define a universal drop-versus-stall policy for every overload pattern, so do not build correctness around undocumented behavior.
Check out frames briefly and copy before asynchronous work
TryGetNextFrame() returns the next checked-out frame, or null when the pool is empty. Dispose every non-null frame on every path. Disposal returns its buffer to the pool. Microsoft specifically advises against keeping either the frame object or its underlying surface after the frame is disposed. If a consumer needs asynchronous processing, copy the pixels or perform a GPU copy into an application-owned resource while the frame is valid; then transfer that owned representation, not the borrowed capture surface.
FrameArrived means that a frame was stored in the pool. A handler should drain the frames it intends to consume rather than assume one event maps permanently to one application operation. The example is deliberately synchronous at the boundary: CopyToOwnedResource must finish the resource handoff before the using scope ends. A real implementation must provide that function and its D3D synchronization rules.
private readonly SemaphoreSlim _callbackGate = new(1, 1);
private int _stopping;
private int _stopRequested;
private int _teardownStarted;
private void OnFrameArrived(Direct3D11CaptureFramePool sender, object args)
{
if (Volatile.Read(ref _stopping) != 0)
return;
_callbackGate.Wait(); // This callback runs off the UI thread for CreateFreeThreaded.
SizeInt32? resizeTo = null;
try
{
if (Volatile.Read(ref _stopping) != 0)
return;
const int maxFramesPerCallback = 4;
for (var processed = 0; processed < maxFramesPerCallback; processed++)
{
if (Volatile.Read(ref _stopping) != 0)
break;
using var frame = sender.TryGetNextFrame();
if (frame is null)
break;
var contentSize = frame.ContentSize;
if (contentSize.Width != _bufferSize.Width ||
contentSize.Height != _bufferSize.Height)
{
resizeTo = contentSize;
continue; // Dispose this old-pool frame; do not treat clipped pixels as complete.
}
CopyToOwnedResource(frame.Surface, contentSize);
}
if (resizeTo is SizeInt32 newSize && Volatile.Read(ref _stopping) == 0)
{
RebuildSizeDependentOutput(newSize);
sender.Recreate(_device, PixelFormat, BufferCount, newSize);
_bufferSize = newSize;
}
}
catch (Exception error)
{
ReportCaptureFailure(error); // Must be bounded and safe from this worker thread.
Interlocked.Exchange(ref _stopRequested, 1);
}
finally
{
_callbackGate.Release();
}
if (Volatile.Read(ref _stopRequested) != 0)
StopAndReportObserved(); // Called only after the callback gate is released.
}
// Intentional async-void boundary for this synchronous event callback. Catch
// teardown faults here, and make ReportCleanupFailure non-throwing.
private async void StopAndReportObserved()
{
// Claim teardown synchronously, before the first await. All stop causes
// must route through this helper so concurrent callbacks coalesce.
if (Interlocked.Exchange(ref _teardownStarted, 1) != 0)
return;
try
{
await StopAndReportAsync().ConfigureAwait(false);
}
catch (Exception cleanupError)
{
ReportCleanupFailure(cleanupError);
}
}
The sample serializes callback work and processes no frame whose ContentSize differs from the current pool dimensions. The actual surface has the size supplied at pool creation or recreation; when content is smaller, the unused area can contain undefined data, and larger content can be clipped. A renderer that intentionally supports letterboxing or cropping should instead use ContentSize to select a valid rectangle and clear or constrain the rest of its output.
If a downstream stage is slower than capture, use an explicit bounded queue after copying into owned storage. For live preview or interactive analysis, a latest-frame policy is often more useful than preserving stale work: replace or discard queued owned frames and dispose any dropped GPU/CPU resources. For archival recording, dropping frames may be unacceptable; measure sustained throughput, use a deliberate encoder queue and timestamp policy, and surface overload rather than quietly claiming lossless capture. Never enqueue unbounded Direct3D11CaptureFrame objects or references to their borrowed surfaces.
Recreate resources on size or device changes
Monitor ContentSize while frames arrive. Window resizing, display changes, or a device replacement can make the pool’s original dimensions or device unsuitable. Resize or rebuild app-owned outputs first, retire the current capture frame, then call Recreate with the desired device, pixel format, buffer count, and size. Microsoft documents that recreation discards all frames already in the pool. If those frames matter, process pending work before recreation; otherwise, explicitly count them as discarded. Also rebuild dependent swap chains, textures, and device-bound resources when the D3D device itself changes. Recreating the frame pool does not repair application-owned resources automatically.
GraphicsCaptureItem.Closed is a terminal signal for the chosen target, not merely a window-size event. Microsoft notes that an app may replace its target window and raise Closed even if a user does not see the window disappear. Stop that session and ask the user to select a new item if capture should continue. Do not infer that a replacement window is automatically the same authorized target.
Make stop, cancellation, and errors idempotent
A reliable stop path sets a stopping flag, removes FrameArrived and Closed subscriptions, closes the session, waits off the UI thread for an in-flight callback to leave its gate, and only then disposes the pool and dependent resources. This prevents callbacks from touching a disposed pool. The sample claims teardown with Interlocked.Exchange before its first await, so concurrent callbacks coalesce into one cleanup attempt; route every stop cause through that same helper. Distinguish user stop, target closure, device removal, callback failure, and process shutdown; do not silently restart without a valid newly authorized item.
An event handler must not block indefinitely on a consumer queue. The sample caps each callback at four frames, checks the stop flag while draining, and releases the gate promptly; choose a small per-callback frame or time budget appropriate to the workload so a continuously refilled pool cannot starve resize or teardown work. Error reporting from the callback should be bounded and thread-safe. Schedule teardown after leaving the callback gate. The async void helper is a deliberate synchronous-event boundary: it awaits the teardown task and catches its exceptions, while ReportCleanupFailure must itself be non-throwing. If StartCapture or pool creation fails partway through, release the session, pool, event registrations, and D3D resources already acquired. If device removal occurs, recreate the D3D device and every dependent resource before restarting with a valid item; a frame-pool Recreate call is not device recovery by itself.
Validate the lifecycle under stress
Test the exact Windows build, SDK, graphics driver, target type, and app model you ship. Exercise picker cancellation, unsupported hardware, an item closing during processing, rapid resize, device removal/recreation, a slow consumer, repeated Stop clicks, shutdown during an event callback, and failure at each startup step. Confirm that frame age and queue depth remain bounded, every frame is disposed, no capture surface escapes its checkout lifetime, and no callback updates UI state directly. Test HDR separately: the Microsoft guide warns that an SDR B8G8R8A8_UNORM pipeline may clip HDR content; a complete HDR path may require a different format and appropriate destination/tone-mapping choices.
In short: dispose each frame promptly, copy before asynchronous retention, serialize resize and teardown against callbacks, and make overload visible. Those boundaries keep borrowed buffers from turning into stale work or a use-after-dispose graphics bug.
Related:
- Windows Performance Recorder and Analyzer: A Defensible Trace Workflow
- ScreenCaptureKit on macOS: Stream Ownership, Frame Delivery, and Recovery
Sources:
- Screen capture (Microsoft Learn)
- Direct3D11CaptureFramePool class (Microsoft Learn)
- Direct3D11CaptureFramePool.CreateFreeThreaded method (Microsoft Learn)
- Direct3D11CaptureFrame class (Microsoft Learn)
- GraphicsCaptureItem.Closed event (Microsoft Learn)
- ApiInformation.IsMethodPresent (Microsoft Learn)
- Microsoft Windows UI Composition Win32 screen-capture sample
- Microsoft Win32 screen-capture sample README