<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: sxyyds</title>
    <description>The latest articles on DEV Community by sxyyds (@sxyyds).</description>
    <link>https://dev.to/sxyyds</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4119337%2F1493c353-7ed7-42f8-a9d4-5431fa423e10.png</url>
      <title>DEV Community: sxyyds</title>
      <link>https://dev.to/sxyyds</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sxyyds"/>
    <language>en</language>
    <item>
      <title>Zero-Copy Windows Graphics Capture: Holding the Frame Lease All the Way to the Encoder</title>
      <dc:creator>sxyyds</dc:creator>
      <pubDate>Thu, 10 Sep 2026 13:23:53 +0000</pubDate>
      <link>https://dev.to/sxyyds/zero-copy-windows-graphics-capture-holding-the-frame-lease-all-the-way-to-the-encoder-33cp</link>
      <guid>https://dev.to/sxyyds/zero-copy-windows-graphics-capture-holding-the-frame-lease-all-the-way-to-the-encoder-33cp</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive pipeline, and where the copies hide
&lt;/h2&gt;

&lt;p&gt;The obvious composition on Windows looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WGC frame pool -&amp;gt; map to staging texture -&amp;gt; Map()/memcpy to CPU
  -&amp;gt; CPU color convert -&amp;gt; Unmap() back to GPU -&amp;gt; encoder input
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each arrow is a round trip over PCIe and a full-frame copy. The goal instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WGC frame pool -&amp;gt; [the one required copy/transform] -&amp;gt; NV12 texture
  -&amp;gt; IMFDXGIBuffer -&amp;gt; hardware MFT ProcessInput
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To get there you have to hold three things at once: the frame lease, the GPU device, and the encoder input contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Holding the frame pool lease
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Direct3D11CaptureFramePool&lt;/code&gt; hands you &lt;code&gt;Direct3D11CaptureFrame&lt;/code&gt; objects via &lt;code&gt;frame_arrived&lt;/code&gt; callbacks. Two properties of the API shape everything downstream:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A frame object is a lease, not a copy.&lt;/strong&gt; 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 &lt;code&gt;ID3D11Texture2D*&lt;/code&gt; and let the frame go, you're now "capturing" whatever the pool writes there next.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Callbacks are serialized in the common case.&lt;/strong&gt; Whatever you do inside &lt;code&gt;frame_arrived&lt;/code&gt; blocks the delivery of the next frame. A slow callback doesn't create backlog; it creates frame drops.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one required copy/transform, and making it do extra work
&lt;/h2&gt;

&lt;p&gt;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: &lt;strong&gt;crop and scale must ride along inside that same pass&lt;/strong&gt; — the "fused ROI mailbox".&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;For the conversion itself there are two backends, and the choice is a contract, not a preference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic plane-RTV backend&lt;/strong&gt;: 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VideoProcessor (&lt;code&gt;ID3D11VideoProcessor&lt;/code&gt;)&lt;/strong&gt;: driver-defined scaling. Faster on some adapters, but by definition vendor-specific — so scaled VP frames publish &lt;em&gt;full damage&lt;/em&gt;, because a precise damage claim would be a lie.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Color: the part everyone gets wrong silently
&lt;/h2&gt;

&lt;p&gt;Capture color on Windows is a minefield of implicit assumptions. FluxCap makes every one of them explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sRGB BGRA8 for the standard path;&lt;/li&gt;
&lt;li&gt;scRGB FP16 for wide-gamut sources;&lt;/li&gt;
&lt;li&gt;HDR10 (P010 + PQ/BT.2020) negotiated when &lt;code&gt;IDXGIOutput6&lt;/code&gt; reports an HDR display — format detection is automatic, but the published color-space field is always a statement about &lt;em&gt;the pixels in the texture&lt;/em&gt;, never about the monitor's native gamut.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the bus metadata (part 2), the color space travels with the frame. A consumer never has to guess what &lt;code&gt;0.7, 0.2, 0.1&lt;/code&gt; means.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handing the texture to the encoder
&lt;/h2&gt;

&lt;p&gt;The D3D11-aware hardware MFT path is &lt;code&gt;IMFDXGIDeviceManager&lt;/code&gt; + external allocation. You create the encoder with &lt;code&gt;MF_SA_D3D11_AWARE&lt;/code&gt;, set your device manager, and then submit input via &lt;code&gt;IMFDXGIBuffer&lt;/code&gt; — a media-buffer wrapper whose entire point is that &lt;code&gt;ProcessInput&lt;/code&gt; receives your &lt;code&gt;ID3D11Texture2D&lt;/code&gt; with a subresource index, not a copy.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;IMFDXGIBuffer&lt;/code&gt; texture/subresource identity immediately before every &lt;code&gt;ProcessInput&lt;/code&gt; call (L2), and treats anything below the MFT boundary as explicitly unobservable rather than implicitly absent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measured
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;WGC window capture → NV12 → H.264, 240 fps media timeline:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Surface&lt;/th&gt;
&lt;th&gt;submit→packet P50&lt;/th&gt;
&lt;th&gt;P95&lt;/th&gt;
&lt;th&gt;Packets&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1280x720-class&lt;/td&gt;
&lt;td&gt;0.84 ms&lt;/td&gt;
&lt;td&gt;0.94 ms&lt;/td&gt;
&lt;td&gt;600/600, zero drops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1080p-class&lt;/td&gt;
&lt;td&gt;1.61 ms&lt;/td&gt;
&lt;td&gt;1.75 ms&lt;/td&gt;
&lt;td&gt;600/600, zero drops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4K-requested (DWM-clipped)&lt;/td&gt;
&lt;td&gt;3.05 ms&lt;/td&gt;
&lt;td&gt;3.28 ms&lt;/td&gt;
&lt;td&gt;600/600, zero drops&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;DXGI_ERROR_ACCESS_LOST&lt;/code&gt; recovery as a ticketed epoch state machine, and the L1–L4 copy-evidence model with the qualification tooling that makes results tamper-evident.&lt;/p&gt;

&lt;p&gt;The library is FluxCap (MIT, C++20, stable C ABI + C++ GPU API): &lt;a href="https://github.com/sxyyds/fluxcap" rel="noopener noreferrer"&gt;https://github.com/sxyyds/fluxcap&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>windows</category>
      <category>directx</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
