Skip to main content

media_pp\elements\sink\renderer\windows/
d3d11_renderer.rs

1use std::sync::Arc;
2
3use crate::pp_log::{PpLog, pp_error, pp_info};
4use ffmpeg_next as ffmpeg;
5use thiserror::Error as ThisError;
6use windows::{
7    Win32::Graphics::{
8        Direct3D11::{ID3D11Device, ID3D11Texture2D},
9        Dxgi::Common::{DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_NV12},
10    },
11    core::Interface,
12};
13
14use crate::{
15    buffer::MediaBuffer,
16    control::ControlMsg,
17    element::{Element, ElementType, Sink, element_pp_log},
18    elements::{SubmitError, filter::decoder::d3d11va_decoder::d3d11va_texture},
19    error::Result,
20    pool::UnboundObjectPoolRef,
21};
22
23/// What [`D3d11Renderer`] needs from an actual DX11 window/rendering
24/// implementation — the D3D11 sibling of
25/// [`crate::elements::D3d12FrameRenderer`], deliberately **not** an impl of
26/// that trait (it's documented as inherently D3D12-only). Unlike the D3D12
27/// trait, neither submit method here takes a fence *or* a `keep_alive` —
28/// see [`crate::elements::D3d11Renderer`]'s own docs on why a single
29/// shared `ID3D11Device` needs no explicit GPU-side synchronization at
30/// all, and this doc comment's own note below on why lifetime-keeping is
31/// unnecessary too. Both paths here are zero-copy (no CPU-upload method
32/// the way `D3d12FrameRenderer::submit_yuv420p` is one): everything in
33/// this crate's D3D11 stack already produces GPU-resident `Pixel::D3D11`
34/// textures (see [`crate::elements::D3d11Upload`]/
35/// [`crate::elements::DxgiCaptureSource`]'s GPU capture mode/
36/// [`crate::elements::D3d11Decoder`]), so there's no CPU-side pixel data
37/// left to upload by the time a frame reaches here.
38///
39/// No `keep_alive` parameter (unlike `D3d12FrameRenderer::submit_nv12_texture`):
40/// D3D11's own COM+driver contract already defers actually freeing a
41/// resource's GPU memory until the GPU has finished any outstanding work
42/// that reads it, *regardless* of when the app-level reference count hits
43/// zero — this is precisely the abstraction D3D12 (deliberately) doesn't
44/// provide, which is why that side needs the caller to keep the source
45/// frame alive by hand via an explicit fence. Here, once
46/// `D3d11Renderer::submit_d3d11_frame`'s local `texture` clone (and
47/// whatever `Arc<UnboundObjectPoolRef<..>>` produced it) drops, the
48/// runtime — not this crate — is what keeps the actual texture memory
49/// valid for as long as the GPU still needs it.
50pub trait D3d11FrameRenderer: Send {
51    /// The `ID3D11Device` this implementation actually renders/submits
52    /// with. [`D3d11Renderer`] reads this once at construction to guard
53    /// every submit against a texture from a different device — same
54    /// reasoning as `D3d12Renderer`'s own device-mismatch guard.
55    fn device(&self) -> ID3D11Device;
56
57    /// `texture` is a plain packed-BGRA surface — from
58    /// [`crate::elements::DxgiCaptureSource`]'s GPU capture mode or
59    /// [`crate::elements::D3d11Upload`] fed a BGRA source. `array_index` is
60    /// always `0` for these producers (neither ever builds an array
61    /// texture) — see `submit_nv12_texture`'s own docs on why it's a
62    /// parameter here at all.
63    ///
64    /// # Safety
65    /// `texture` must be a valid `ID3D11Texture2D` on the same
66    /// `ID3D11Device` this renderer was created with, `DXGI_FORMAT_B8G8R8A8_UNORM`,
67    /// with `array_index < ` its `ArraySize`.
68    unsafe fn submit_bgra_texture(
69        &self,
70        texture: ID3D11Texture2D,
71        array_index: u32,
72        width: u32,
73        height: u32,
74    ) -> std::result::Result<(), SubmitError>;
75
76    /// `texture` is an NV12 surface — from [`crate::elements::D3d11Decoder`]
77    /// or [`crate::elements::D3d11Upload`] fed an NV12 source. `array_index`
78    /// is which slice of `texture` this frame actually is: libavcodec's own
79    /// D3D11VA hwaccel decode pools frames as slices of one shared **array**
80    /// texture (unlike `D3d11Upload`, which always builds a fresh
81    /// non-array, single-slice texture per frame — `array_index` is always
82    /// `0` there) — see `d3d11va_texture`'s own docs.
83    ///
84    /// # Safety
85    /// `texture` must be a valid `ID3D11Texture2D` on the same
86    /// `ID3D11Device` this renderer was created with, `DXGI_FORMAT_NV12`,
87    /// with `array_index < ` its `ArraySize`.
88    unsafe fn submit_nv12_texture(
89        &self,
90        texture: ID3D11Texture2D,
91        array_index: u32,
92        width: u32,
93        height: u32,
94    ) -> std::result::Result<(), SubmitError>;
95
96    fn resize(&self, width: u32, height: u32) -> std::result::Result<(), SubmitError>;
97}
98
99/// Errors specific to `D3d11Renderer`. Converts into the crate-wide `Error`
100/// via `?` (see [`crate::error::Error`]).
101#[derive(Debug, ThisError)]
102pub enum D3d11RendererError {
103    #[error("failed to submit frame: {0:?}")]
104    Submit(SubmitError),
105
106    #[error("failed to resize: {0:?}")]
107    Resize(SubmitError),
108
109    #[error("D3d11Renderer only handles Pixel::D3D11 frames, got {0:?}")]
110    UnsupportedFormat(ffmpeg::format::Pixel),
111
112    #[error(
113        "frame claimed the D3D11 pixel format but carries no texture — must \
114         come from D3d11Upload/D3d11Decoder/DxgiCaptureSource's GPU mode"
115    )]
116    InvalidD3d11Frame,
117
118    #[error(
119        "D3d11Renderer only draws DXGI_FORMAT_B8G8R8A8_UNORM or DXGI_FORMAT_NV12 textures, got {0:?}"
120    )]
121    UnsupportedTextureFormat(DXGI_FORMAT),
122
123    #[error(
124        "a Pixel::D3D11 frame's texture lives on a different ID3D11Device \
125         than this D3d11Renderer was created with — every D3D11 element in \
126         one pipeline must share exactly one device for zero-copy to be \
127         valid"
128    )]
129    DeviceMismatch,
130
131    #[error("D3D11 texture array index {index} is outside ArraySize {array_size}")]
132    InvalidArrayIndex { index: isize, array_size: u32 },
133
134    #[error("windows error: {0}")]
135    Windows(#[from] windows::core::Error),
136}
137
138/// Terminal sink that submits `Pixel::D3D11` video frames to a
139/// caller-supplied [`D3d11FrameRenderer`] — the D3D11 sibling of
140/// [`crate::elements::D3d12Renderer`]. Only built with the
141/// `d3d11` feature.
142///
143/// Every producer in this crate's D3D11 stack
144/// ([`crate::elements::D3d11Upload`], [`crate::elements::D3d11Decoder`],
145/// [`crate::elements::DxgiCaptureSource`]'s GPU capture mode) is meant to
146/// share **one** `ID3D11Device` (and its one immediate context) with
147/// whatever [`D3d11FrameRenderer`] impl this wraps. That single-context
148/// requirement is what makes zero-copy here need **no explicit fence**,
149/// unlike [`crate::elements::D3d12Renderer`]'s `submit_nv12_texture`
150/// (which needs one because the D3D12 decoder and renderer are genuinely
151/// different devices/queues with nothing else to serialize them): an
152/// `ID3D11Device` created without `D3D11_CREATE_DEVICE_SINGLETHREADED` has
153/// its immediate context auto-serialized by the runtime across threads,
154/// and as long as every element funnels its GPU commands through that one
155/// context, the driver executes them in submission order — no separate
156/// sync object needed. This only holds because everything shares the
157/// *same* context; a second `ID3D11Device` in the mix would need its own
158/// explicit synchronization, same as the D3D12 case.
159///
160/// Dispatches on the *texture's own* `DXGI_FORMAT` (via `GetDesc`), not on
161/// any extra tag carried by the frame. `D3d11Upload` and GPU screen capture
162/// wrap manually-created textures, while `D3d11Decoder` receives textures
163/// from FFmpeg's D3D11VA frame pool; reading the actual texture description
164/// gives all of those producer paths one reliable source of truth for the
165/// pixel layout.
166pub struct D3d11Renderer {
167    pp_log: PpLog,
168    name: Arc<str>,
169    inner: Box<dyn D3d11FrameRenderer>,
170    /// Captured once from `inner.device()` at construction — see
171    /// `D3d12Renderer`'s own `device` field docs for why (fetched from
172    /// `inner` itself rather than a separate constructor parameter).
173    device: ID3D11Device,
174}
175
176impl D3d11Renderer {
177    /// `renderer` is whatever the caller's own [`D3d11FrameRenderer`]
178    /// implementation is — already constructed and pointed at a real
179    /// window/device by the time it gets here.
180    pub fn new(name: impl Into<String>, renderer: Box<dyn D3d11FrameRenderer>) -> Self {
181        let name: Arc<str> = name.into().into();
182        let pp_log = element_pp_log(ElementType::D3d11Renderer, &name, None);
183        pp_info!(pp_log: &pp_log, "created");
184        let device = renderer.device();
185        Self {
186            name,
187            pp_log,
188            inner: renderer,
189            device,
190        }
191    }
192
193    /// Call when the target window resizes.
194    pub fn resize(&self, width: u32, height: u32) -> Result<()> {
195        self.inner
196            .resize(width, height)
197            .inspect_err(|error| pp_error!(self, "resize failed: {error:?}"))
198            .map_err(D3d11RendererError::Resize)?;
199        pp_info!(self, "resized: {width}x{height}");
200        Ok(())
201    }
202
203    fn submit_d3d11_frame(
204        &self,
205        frame: Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>>,
206    ) -> Result<()> {
207        let (texture_raw, index) =
208            d3d11va_texture(&frame).ok_or(D3d11RendererError::InvalidD3d11Frame)?;
209        let width = frame.width();
210        let height = frame.height();
211
212        // Safety: `texture_raw` is a borrowed raw `ID3D11Texture2D*` —
213        // still owned by `frame`'s own buffer reference, not by us.
214        // `.clone()` (`AddRef`) gives us an independently ref-counted
215        // handle, valid for as long as we hold it.
216        let texture = unsafe {
217            ID3D11Texture2D::from_raw_borrowed(&texture_raw)
218                .expect("D3d11 frame's texture pointer must not be null")
219                .clone()
220        };
221
222        // Same reasoning as `D3d12Renderer::submit_d3d12_frame`'s own
223        // device check: the producer and `self.inner` are independent
224        // constructions that only *should* share a device by convention —
225        // verify it.
226        let texture_device = unsafe { texture.GetDevice() }.map_err(D3d11RendererError::from)?;
227        if texture_device.as_raw() != self.device.as_raw() {
228            return Err(D3d11RendererError::DeviceMismatch.into());
229        }
230
231        let mut desc = Default::default();
232        unsafe { texture.GetDesc(&mut desc) };
233        if index < 0 || index as u64 >= u64::from(desc.ArraySize) {
234            let error = D3d11RendererError::InvalidArrayIndex {
235                index,
236                array_size: desc.ArraySize,
237            };
238            pp_error!(self, "{error}");
239            return Err(error.into());
240        }
241        let array_index = index as u32;
242
243        // No `keep_alive` to pass through here — see `D3d11FrameRenderer`'s
244        // own docs on why D3D11's driver-deferred resource destruction
245        // makes that unnecessary, unlike `D3d12Renderer`. `frame` itself
246        // just drops normally at the end of this function.
247        match desc.Format {
248            DXGI_FORMAT_B8G8R8A8_UNORM => unsafe {
249                self.inner
250                    .submit_bgra_texture(texture, array_index, width, height)
251                    .map_err(D3d11RendererError::Submit)?;
252            },
253            DXGI_FORMAT_NV12 => unsafe {
254                self.inner
255                    .submit_nv12_texture(texture, array_index, width, height)
256                    .map_err(D3d11RendererError::Submit)?;
257            },
258            other => return Err(D3d11RendererError::UnsupportedTextureFormat(other).into()),
259        }
260        Ok(())
261    }
262}
263
264impl Element for D3d11Renderer {
265    fn name(&self) -> Arc<str> {
266        self.name.clone()
267    }
268
269    fn element_type(&self) -> ElementType {
270        ElementType::D3d11Renderer
271    }
272
273    fn pp_log(&self) -> &PpLog {
274        &self.pp_log
275    }
276
277    fn pp_log_mut(&mut self) -> &mut PpLog {
278        &mut self.pp_log
279    }
280}
281
282impl Sink for D3d11Renderer {
283    fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
284        let MediaBuffer::Video(frame) = buf else {
285            return Ok(());
286        };
287
288        if frame.format() != ffmpeg::format::Pixel::D3D11 {
289            let format = frame.format();
290            pp_error!(self, "unsupported pixel format: {format:?}");
291            return Err(D3d11RendererError::UnsupportedFormat(format).into());
292        }
293        self.submit_d3d11_frame(frame)
294            .inspect_err(|error| pp_error!(self, "submit_d3d11_frame failed: {error}"))
295    }
296
297    fn control(&mut self, _msg: ControlMsg) -> Result<()> {
298        // Terminal, nothing to flush or forward — same reasoning as
299        // `D3d12Renderer::control`.
300        Ok(())
301    }
302}