Every Windows screen-capture library claims "zero-copy". This series is about building one — FluxCap, a C++20/D3D11 capture library — and about what that claim can actually mean when the last stage of your pipeline runs below the runtime API, where you can't see anything. In this part: how a Windows Graphics Capture frame travels to a hardware encoder without ever becoming CPU pixels.
Part 1 of a 3-part series on building FluxCap, a Windows capture library. Part 2 covers the cross-process frame bus; Part 3 covers device-loss recovery and the evidence model.
The naive pipeline, and where the copies hide
The obvious composition on Windows looks like this:
WGC frame pool -> map to staging texture -> Map()/memcpy to CPU
-> CPU color convert -> Unmap() back to GPU -> encoder input
Each arrow is a round trip over PCIe and a full-frame copy. The goal instead:
WGC frame pool -> [the one required copy/transform] -> NV12 texture
-> IMFDXGIBuffer -> hardware MFT ProcessInput
To get there you have to hold three things at once: the frame lease, the GPU device, and the encoder input contract.
Holding the frame pool lease
Direct3D11CaptureFramePool hands you Direct3D11CaptureFrame objects via frame_arrived callbacks. Two properties of the API shape everything downstream:
-
A frame object is a lease, not a copy. The texture you get is owned by the frame pool. You're renting it until the frame object is destroyed — at which point the pool reuses the slot. If you store the
ID3D11Texture2D*and let the frame go, you're now "capturing" whatever the pool writes there next. -
Callbacks are serialized in the common case. Whatever you do inside
frame_arrivedblocks the delivery of the next frame. A slow callback doesn't create backlog; it creates frame drops.
So the producer side is: acquire the frame, resolve its mailbox geometry, do exactly one GPU pass, commit to the bus (next post), release — all without anything expensive inside the callback.
The one required copy/transform, and making it do extra work
You cannot have literally zero GPU passes: WGC hands you BGRA8 (or scRGB FP16), and the hardware H.264/HEVC/AV1 encoder MFT wants NV12 (or P010 for HDR). There is exactly one required conversion, and FluxCap's discipline is: crop and scale must ride along inside that same pass — the "fused ROI mailbox".
That's why the library negotiates the frame pool with a fixed size and treats the source surface as a geometry problem: a 640x640 bus texture fed by a centered 320x320 ROI is one planar conversion from the source rectangle, not crop-then-convert-then-scale. Any architecture where crop/scale/convert are separate passes has already spent the copy budget that the claim "zero redundant copies" refers to.
For the conversion itself there are two backends, and the choice is a contract, not a preference:
- Deterministic plane-RTV backend: the same integer taps as the CPU damage mapper. Bit-reproducible geometry, damage contracts that can be replayed, compile-time-checked rotation mapping. This is what the qualification tooling runs.
-
VideoProcessor (
ID3D11VideoProcessor): driver-defined scaling. Faster on some adapters, but by definition vendor-specific — so scaled VP frames publish full damage, because a precise damage claim would be a lie.
This "fail toward the weaker, honest claim" pattern repeats all over the codebase, and I think it's the right instinct for anything that's supposed to be auditable.
Color: the part everyone gets wrong silently
Capture color on Windows is a minefield of implicit assumptions. FluxCap makes every one of them explicit:
- sRGB BGRA8 for the standard path;
- scRGB FP16 for wide-gamut sources;
- HDR10 (P010 + PQ/BT.2020) negotiated when
IDXGIOutput6reports an HDR display — format detection is automatic, but the published color-space field is always a statement about the pixels in the texture, never about the monitor's native gamut.
In the bus metadata (part 2), the color space travels with the frame. A consumer never has to guess what 0.7, 0.2, 0.1 means.
Handing the texture to the encoder
The D3D11-aware hardware MFT path is IMFDXGIDeviceManager + external allocation. You create the encoder with MF_SA_D3D11_AWARE, set your device manager, and then submit input via IMFDXGIBuffer — a media-buffer wrapper whose entire point is that ProcessInput receives your ID3D11Texture2D with a subresource index, not a copy.
The subtlety is that "the MFT accepted my texture" is not "the MFT encoded from my texture". The MFT is allowed to internally stage, convert, or re-layout. From user mode you cannot observe its boundary. This is exactly where most "zero-copy" claims quietly overreach, and it's why part 3 of this series introduces an evidence-level model instead of a boolean — the short version is that the library verifies the exact IMFDXGIBuffer texture/subresource identity immediately before every ProcessInput call (L2), and treats anything below the MFT boundary as explicitly unobservable rather than implicitly absent.
Measured
All numbers are from one laptop (RTX 5060 Laptop, Ryzen 9 8945HX, Windows 11 build 26200, single 2560x1600 display) and are valid only for that tuple — the repo's benchmark doc says the same, more forcefully.
WGC window capture → NV12 → H.264, 240 fps media timeline:
| Surface | submit→packet P50 | P95 | Packets |
|---|---|---|---|
| 1280x720-class | 0.84 ms | 0.94 ms | 600/600, zero drops |
| 1080p-class | 1.61 ms | 1.75 ms | 600/600, zero drops |
| 4K-requested (DWM-clipped) | 3.05 ms | 3.28 ms | 600/600, zero drops |
The 4K row is honest about a fun fact: a 3840x2160 window on a 2560x1600 desktop gets clipped by DWM, so the "real 4K" numbers in the repo are synthetic saturation numbers (5,896 BGRA→NV12 ops/s, 194 sequential encode packets/s at 4K) until someone plugs in a 4K display.
What's next
Part 2 takes the texture out of the process: a cross-process shared-texture bus with D3D11 timeline fences, per-slot provenance metadata, and a CPU ownership protocol where interlocked operations pair the GPU and CPU domains. Part 3 covers DXGI_ERROR_ACCESS_LOST recovery as a ticketed epoch state machine, and the L1–L4 copy-evidence model with the qualification tooling that makes results tamper-evident.
The library is FluxCap (MIT, C++20, stable C ABI + C++ GPU API): https://github.com/sxyyds/fluxcap
Top comments (0)