Skip to main content

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

1use std::{any::Any, 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::Direct3D12::{ID3D12Device, ID3D12Fence, ID3D12Resource},
8    core::Interface,
9};
10
11use crate::{
12    buffer::MediaBuffer,
13    control::ControlMsg,
14    element::{Element, ElementType, Sink, element_pp_log},
15    elements::{SubmitError, filter::decoder::d3d12va_decoder::d3d12va_texture},
16    error::Result,
17    pool::UnboundObjectPoolRef,
18};
19
20/// One CPU-resident image plane — data pointer, byte length, and row
21/// stride. Deliberately a plain, GPU-vendor-agnostic struct (no
22/// dependency on any particular rendering crate's own type) — unlike
23/// [`D3d12FrameRenderer::submit_nv12_texture`]'s COM types, this is the
24/// one part of the trait that isn't D3D12-specific, so
25/// [`D3d12FrameRenderer`] implementors only need this crate to know
26/// anything about their concrete rendering setup for the CPU-upload path.
27#[derive(Clone, Copy)]
28pub struct RawPlane {
29    pub data: *const u8,
30    pub len: usize,
31    pub stride: usize,
32}
33
34/// What [`D3d12Renderer`] needs from an actual DX12 window/rendering
35/// implementation — deliberately the *only* thing this crate knows about
36/// D3D12 rendering. Not GPU-vendor-agnostic despite `submit_yuv420p`'s
37/// plain `RawPlane` args: `submit_nv12_texture`'s zero-copy path takes
38/// `ID3D12Resource`/`ID3D12Fence` directly, so this trait (and any
39/// element built on it) is inherently D3D12-only — a Vulkan/CUDA renderer
40/// would need its own trait, not an impl of this one. `D3d12Renderer`
41/// itself only depends on this trait (plus the `windows` COM types the
42/// zero-copy path needs to pass through) — not on `renderer_engine` or
43/// any other concrete rendering crate. A caller wanting to actually
44/// render implements this for its own window/rendering stack; this
45/// repository's examples use `examples/render/render_common` for that
46/// implementation, outside the `media-pp` crate itself.
47pub trait D3d12FrameRenderer: Send {
48    /// The `ID3D12Device` this implementation actually renders/submits
49    /// with. [`D3d12Renderer`] reads this once at construction to guard
50    /// [`D3d12FrameRenderer::submit_nv12_texture`]'s zero-copy path: a
51    /// texture from a different device is invalid to draw from at all
52    /// (not just wrong-looking), so it's checked against this rather than
53    /// trusted.
54    fn device(&self) -> ID3D12Device;
55
56    /// # Safety
57    /// All plane pointers must be readable for the given length and
58    /// remain valid until this call returns.
59    unsafe fn submit_yuv420p(
60        &self,
61        y: RawPlane,
62        u: RawPlane,
63        v: RawPlane,
64        width: u32,
65        height: u32,
66    ) -> std::result::Result<(), SubmitError>;
67
68    /// # Safety
69    /// `texture` must be a valid `ID3D12Resource` on the same
70    /// `ID3D12Device` this renderer was created with, laid out as NV12.
71    /// `fence` must only reach `fence_value` once the GPU work that
72    /// produced `texture`'s contents has completed.
73    unsafe fn submit_nv12_texture(
74        &self,
75        texture: ID3D12Resource,
76        fence: ID3D12Fence,
77        fence_value: u64,
78        width: u32,
79        height: u32,
80        keep_alive: Box<dyn Any + Send>,
81    ) -> std::result::Result<(), SubmitError>;
82
83    fn resize(&self, width: u32, height: u32) -> std::result::Result<(), SubmitError>;
84}
85
86/// Errors specific to `D3d12Renderer`. Converts into the crate-wide `Error`
87/// via `?` (see [`crate::error::Error`]).
88#[derive(Debug, ThisError)]
89pub enum D3d12RendererError {
90    #[error("failed to submit frame: {0:?}")]
91    Submit(SubmitError),
92
93    #[error("failed to resize: {0:?}")]
94    Resize(SubmitError),
95
96    #[error(
97        "D3d12Renderer only handles YUV420P frames (CPU) or D3D12 frames \
98         (from D3d12vaDecoder), got {0:?}"
99    )]
100    UnsupportedFormat(ffmpeg::format::Pixel),
101
102    #[error(
103        "frame claimed the D3D12 pixel format but has no AVD3D12VAFrame \
104         payload — must come from D3d12vaDecoder"
105    )]
106    InvalidD3d12Frame,
107
108    #[error(
109        "a Pixel::D3D12 frame's texture lives on a different ID3D12Device \
110         than this D3d12Renderer was created with — the producer \
111         (D3d12vaDecoder/D3d12Upload) and the D3d12FrameRenderer impl \
112         must share the same device for zero-copy to be valid"
113    )]
114    DeviceMismatch,
115}
116
117/// Terminal sink that submits decoded video frames to a caller-supplied
118/// [`D3d12FrameRenderer`]. Only built with the `d3d12` feature —
119/// every consumer that doesn't need to render to a window pulls in
120/// neither this nor the `windows` dependency it needs for the zero-copy
121/// path.
122///
123/// Handles two kinds of input, dispatched on `frame.format()`:
124///   - `Pixel::YUV420P`: CPU-decoded (e.g. from `SwDecoder`) — copies
125///     pixel bytes to the GPU via `D3d12FrameRenderer::submit_yuv420p`.
126///   - `Pixel::D3D12`: GPU-decoded (from `D3d12vaDecoder`) — zero-copy,
127///     draws straight from the decoder's own texture via
128///     `D3d12FrameRenderer::submit_nv12_texture`.
129pub struct D3d12Renderer {
130    pp_log: PpLog,
131    name: Arc<str>,
132    inner: Box<dyn D3d12FrameRenderer>,
133    /// Captured once from `inner.device()` at construction — the
134    /// reference `submit_d3d12_frame` checks every zero-copy frame's
135    /// actual device against. Fetched from `inner` itself rather than
136    /// taken as a separate constructor parameter: a second
137    /// independently-supplied device would just be another value the
138    /// caller could get wrong, proving nothing about what `inner` really
139    /// renders with.
140    device: ID3D12Device,
141}
142
143impl D3d12Renderer {
144    /// `renderer` is whatever the caller's own [`D3d12FrameRenderer`]
145    /// implementation is — already constructed and pointed at a real
146    /// window/device by the time it gets here. This element doesn't
147    /// create or own a window itself.
148    pub fn new(name: impl Into<String>, renderer: Box<dyn D3d12FrameRenderer>) -> Self {
149        let name: Arc<str> = name.into().into();
150        let pp_log = element_pp_log(ElementType::D3d12Renderer, &name, None);
151        pp_info!(pp_log: &pp_log, "created");
152        let device = renderer.device();
153        Self {
154            name,
155            pp_log,
156            inner: renderer,
157            device,
158        }
159    }
160
161    /// Call when the target window resizes.
162    pub fn resize(&self, width: u32, height: u32) -> Result<()> {
163        self.inner
164            .resize(width, height)
165            .inspect_err(|error| pp_error!(self, "resize failed: {error:?}"))
166            .map_err(D3d12RendererError::Resize)?;
167        pp_info!(self, "resized: {width}x{height}");
168        Ok(())
169    }
170
171    fn submit_yuv420p_frame(&self, frame: &ffmpeg::frame::Video) -> Result<()> {
172        let plane = |index: usize| RawPlane {
173            data: frame.data(index).as_ptr(),
174            len: frame.data(index).len(),
175            stride: frame.stride(index),
176        };
177
178        // Safety: `plane(0..3)` point into `frame`'s own buffers, which
179        // outlive this call — `submit_yuv420p` only reads them before
180        // returning.
181        unsafe {
182            self.inner
183                .submit_yuv420p(plane(0), plane(1), plane(2), frame.width(), frame.height())
184                .map_err(D3d12RendererError::Submit)?;
185        }
186        Ok(())
187    }
188
189    fn submit_d3d12_frame(
190        &self,
191        frame: Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>>,
192    ) -> Result<()> {
193        let (texture_raw, fence_raw, fence_value) =
194            d3d12va_texture(&frame).ok_or(D3d12RendererError::InvalidD3d12Frame)?;
195        let width = frame.width();
196        let height = frame.height();
197
198        // Safety: `texture_raw`/`fence_raw` are borrowed raw COM pointers
199        // — still owned by `frame`'s own hw frame pool reference, not by
200        // us. `.clone()` (`AddRef`) gives us our own independently
201        // ref-counted handle, valid for as long as *we* hold it,
202        // regardless of what `frame`/ffmpeg later does with its copy.
203        let (texture, fence) = unsafe {
204            let texture = ID3D12Resource::from_raw_borrowed(&texture_raw)
205                .expect("AVD3D12VAFrame.texture must not be null")
206                .clone();
207            let fence = ID3D12Fence::from_raw_borrowed(&fence_raw)
208                .expect("AVD3D12VAFrame.sync_ctx.fence must not be null")
209                .clone();
210            (texture, fence)
211        };
212
213        // The producer (`D3d12vaDecoder`/`D3d12Upload`) and `self.inner`
214        // are independent constructions that only *should* share a
215        // device by convention — verify it, since drawing a different
216        // device's texture is invalid, not just wrong output.
217        let mut texture_device: Option<ID3D12Device> = None;
218        unsafe { texture.GetDevice(&mut texture_device) }
219            .map_err(|_| D3d12RendererError::DeviceMismatch)?;
220        let texture_device = texture_device.ok_or(D3d12RendererError::DeviceMismatch)?;
221        if texture_device.as_raw() != self.device.as_raw() {
222            return Err(D3d12RendererError::DeviceMismatch.into());
223        }
224
225        // `frame` (an `Arc`) is what keeps the underlying D3D12 texture
226        // memory from being recycled by the decoder's frame pool while
227        // the renderer still has it queued to draw — independent of, and
228        // in addition to, the `texture`/`fence` COM references above.
229        unsafe {
230            self.inner
231                .submit_nv12_texture(texture, fence, fence_value, width, height, Box::new(frame))
232                .map_err(D3d12RendererError::Submit)?;
233        }
234        Ok(())
235    }
236}
237
238impl Element for D3d12Renderer {
239    fn name(&self) -> Arc<str> {
240        self.name.clone()
241    }
242
243    fn element_type(&self) -> ElementType {
244        ElementType::D3d12Renderer
245    }
246
247    fn pp_log(&self) -> &PpLog {
248        &self.pp_log
249    }
250
251    fn pp_log_mut(&mut self) -> &mut PpLog {
252        &mut self.pp_log
253    }
254}
255
256impl Sink for D3d12Renderer {
257    fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
258        let MediaBuffer::Video(frame) = buf else {
259            return Ok(());
260        };
261
262        match frame.format() {
263            ffmpeg::format::Pixel::YUV420P => self
264                .submit_yuv420p_frame(&frame)
265                .inspect_err(|error| pp_error!(self, "submit_yuv420p_frame failed: {error}")),
266            ffmpeg::format::Pixel::D3D12 => self
267                .submit_d3d12_frame(frame)
268                .inspect_err(|error| pp_error!(self, "submit_d3d12_frame failed: {error}")),
269            other => {
270                pp_error!(self, "unsupported pixel format: {other:?}");
271                Err(D3d12RendererError::UnsupportedFormat(other).into())
272            }
273        }
274    }
275
276    fn control(&mut self, _msg: ControlMsg) -> Result<()> {
277        // Terminal, nothing to flush or forward — a paused/stopped window
278        // just stops receiving new frames (see `Queue`'s worker loop) and
279        // keeps showing whatever was submitted last.
280        Ok(())
281    }
282}