1use std::borrow::Cow;
27
28#[cfg(all(target_os = "linux", feature = "dmabuf"))]
29use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd};
30#[cfg(all(target_os = "linux", feature = "dmabuf"))]
31use std::sync::Arc;
32
33use bytes::Bytes;
34use moq_net::Timestamp;
35
36use yuv::{YuvChromaSubsampling, YuvConversionMode, YuvPlanarImageMut, rgba_to_yuv420};
37
38use crate::{Color, Error, Size};
39
40pub struct Frame {
47 pub timestamp: Timestamp,
51 pub surface: Surface,
53}
54
55impl Frame {
56 pub fn new(surface: Surface, timestamp: Timestamp) -> Self {
58 Self { timestamp, surface }
59 }
60
61 pub fn size(&self) -> Size {
63 Size::new(self.surface.width(), self.surface.height())
64 }
65
66 pub fn resize(&self, size: Size) -> Result<Frame, Error> {
73 self.resize_with(size, &crate::resize::Config::default())
74 }
75
76 pub fn resize_with(&self, size: Size, config: &crate::resize::Config) -> Result<Frame, Error> {
78 Ok(Frame {
79 timestamp: self.timestamp,
80 surface: self.surface.resize_with(size, config)?,
81 })
82 }
83}
84
85#[cfg(all(target_os = "linux", feature = "dmabuf"))]
90#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
91pub struct DrmFormat(u32);
92
93#[cfg(all(target_os = "linux", feature = "dmabuf"))]
94impl DrmFormat {
95 pub const NV12: Self = Self::from_bytes(*b"NV12");
97 pub const XRGB8888: Self = Self::from_bytes(*b"XR24");
99 pub const ARGB8888: Self = Self::from_bytes(*b"AR24");
101 pub const XBGR8888: Self = Self::from_bytes(*b"XB24");
103 pub const ABGR8888: Self = Self::from_bytes(*b"AB24");
105
106 pub const fn from_bytes(bytes: [u8; 4]) -> Self {
108 Self(u32::from_le_bytes(bytes))
109 }
110
111 pub const fn as_raw(self) -> u32 {
113 self.0
114 }
115}
116
117#[cfg(all(target_os = "linux", feature = "dmabuf"))]
119#[derive(Clone, Copy, Debug, PartialEq, Eq)]
120pub struct DmaBufPlane {
121 offset: u32,
122 stride: u32,
123}
124
125#[cfg(all(target_os = "linux", feature = "dmabuf"))]
126impl DmaBufPlane {
127 #[cfg(feature = "pipewire")]
128 pub(crate) const fn new(offset: u32, stride: u32) -> Self {
129 Self { offset, stride }
130 }
131
132 pub const fn offset(&self) -> u32 {
134 self.offset
135 }
136
137 pub const fn stride(&self) -> u32 {
139 self.stride
140 }
141}
142
143#[cfg(all(target_os = "linux", feature = "dmabuf"))]
149pub struct DmaBufExport {
150 fd: OwnedFd,
151 inner: Arc<dyn DmaBufFrame>,
152}
153
154#[cfg(all(target_os = "linux", feature = "dmabuf"))]
161const DMA_BUF_FENCE_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(500);
162
163#[cfg(all(target_os = "linux", feature = "dmabuf"))]
164pub(crate) fn wait_dma_buf_readable(fd: BorrowedFd<'_>) -> std::io::Result<()> {
165 let mut event = libc::pollfd {
166 fd: fd.as_raw_fd(),
167 events: libc::POLLIN,
168 revents: 0,
169 };
170 let deadline = std::time::Instant::now() + DMA_BUF_FENCE_TIMEOUT;
171 loop {
172 let remaining = deadline.saturating_duration_since(std::time::Instant::now());
175 if remaining.is_zero() {
176 return Err(std::io::Error::from(std::io::ErrorKind::TimedOut));
177 }
178 let result = unsafe {
181 libc::poll(
182 &mut event,
183 1,
184 remaining.as_millis().min(i32::MAX as u128) as libc::c_int,
185 )
186 };
187 if result > 0 && event.revents & libc::POLLIN != 0 {
188 return Ok(());
189 }
190 if result == 0 {
191 return Err(std::io::Error::from(std::io::ErrorKind::TimedOut));
192 }
193 if result < 0 {
194 let error = std::io::Error::last_os_error();
195 if error.kind() == std::io::ErrorKind::Interrupted {
196 continue;
197 }
198 return Err(error);
199 }
200 return Err(std::io::Error::other(format!(
201 "DMA-BUF poll returned events {:#x}",
202 event.revents
203 )));
204 }
205}
206
207#[cfg(all(target_os = "linux", feature = "dmabuf"))]
208impl DmaBufExport {
209 pub fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {
211 std::os::fd::AsFd::as_fd(&self.fd)
212 }
213
214 pub(crate) fn into_parts(self) -> (OwnedFd, Arc<dyn DmaBufFrame>) {
215 (self.fd, self.inner)
216 }
217}
218
219#[cfg(all(target_os = "linux", feature = "dmabuf"))]
220impl std::os::fd::AsFd for DmaBufExport {
221 fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {
222 std::os::fd::AsFd::as_fd(&self.fd)
223 }
224}
225
226#[cfg(all(target_os = "linux", feature = "dmabuf"))]
227impl std::fmt::Debug for DmaBufExport {
228 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229 f.debug_struct("DmaBufExport").finish_non_exhaustive()
230 }
231}
232
233#[cfg(all(target_os = "linux", feature = "dmabuf"))]
241#[derive(Clone)]
242pub struct DmaBuf {
243 format: DrmFormat,
244 modifier: u64,
245 width: u32,
246 height: u32,
247 planes: Vec<DmaBufPlane>,
248 color: Option<Color>,
249 inner: Arc<dyn DmaBufFrame>,
250}
251
252#[cfg(all(target_os = "linux", feature = "dmabuf"))]
253impl std::fmt::Debug for DmaBuf {
254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255 f.debug_struct("DmaBuf")
256 .field("format", &self.format)
257 .field("modifier", &format_args!("{:#x}", self.modifier))
258 .field("width", &self.width)
259 .field("height", &self.height)
260 .field("planes", &self.planes)
261 .finish_non_exhaustive()
262 }
263}
264
265#[cfg(all(target_os = "linux", feature = "dmabuf"))]
266impl DmaBuf {
267 #[cfg(feature = "pipewire")]
268 pub(crate) fn new(
269 format: DrmFormat,
270 modifier: u64,
271 width: u32,
272 height: u32,
273 planes: Vec<DmaBufPlane>,
274 color: Option<Color>,
275 inner: Arc<dyn DmaBufFrame>,
276 ) -> Result<Self, Error> {
277 Size::new(width, height).validate("DMA-BUF")?;
278 if planes.is_empty() {
279 return Err(Error::Codec(anyhow::anyhow!("DMA-BUF has no planes")));
280 }
281 Ok(Self {
282 format,
283 modifier,
284 width,
285 height,
286 planes,
287 color,
288 inner,
289 })
290 }
291
292 pub fn export(&self) -> std::io::Result<DmaBufExport> {
294 let fd = self.inner.export()?;
295 wait_dma_buf_readable(fd.as_fd())?;
296 Ok(DmaBufExport {
297 fd,
298 inner: self.inner.clone(),
299 })
300 }
301
302 pub const fn format(&self) -> DrmFormat {
304 self.format
305 }
306
307 pub const fn modifier(&self) -> u64 {
309 self.modifier
310 }
311
312 pub const fn width(&self) -> u32 {
314 self.width
315 }
316
317 pub const fn height(&self) -> u32 {
319 self.height
320 }
321
322 pub fn planes(&self) -> &[DmaBufPlane] {
324 &self.planes
325 }
326}
327
328#[cfg(all(target_os = "linux", feature = "dmabuf"))]
333pub(crate) trait DmaBufFrame: Send + Sync {
334 fn export(&self) -> std::io::Result<OwnedFd>;
335 fn download_i420(&self) -> Result<I420, Error>;
336}
337
338#[non_exhaustive]
357pub enum Surface {
358 #[cfg(target_os = "macos")]
361 PixelBuffer(macos::PixelBuffer),
362 #[cfg(target_os = "windows")]
364 Texture(d3d11::Texture),
365 #[cfg(all(target_os = "linux", feature = "nvidia"))]
368 Cuda(cuda::Frame),
369 #[cfg(all(target_os = "linux", feature = "dmabuf"))]
371 DmaBuf(DmaBuf),
372 I420(I420),
374}
375
376impl Surface {
377 pub fn width(&self) -> u32 {
379 match self {
380 #[cfg(target_os = "macos")]
381 Surface::PixelBuffer(s) => s.width,
382 #[cfg(target_os = "windows")]
383 Surface::Texture(t) => t.width,
384 #[cfg(all(target_os = "linux", feature = "nvidia"))]
385 Surface::Cuda(c) => c.width,
386 #[cfg(all(target_os = "linux", feature = "dmabuf"))]
387 Surface::DmaBuf(d) => d.width,
388 Surface::I420(i) => i.width,
389 }
390 }
391
392 pub fn height(&self) -> u32 {
394 match self {
395 #[cfg(target_os = "macos")]
396 Surface::PixelBuffer(s) => s.height,
397 #[cfg(target_os = "windows")]
398 Surface::Texture(t) => t.height,
399 #[cfg(all(target_os = "linux", feature = "nvidia"))]
400 Surface::Cuda(c) => c.height,
401 #[cfg(all(target_os = "linux", feature = "dmabuf"))]
402 Surface::DmaBuf(d) => d.height,
403 Surface::I420(i) => i.height,
404 }
405 }
406
407 pub fn rgba(rgba: &[u8], size: Size) -> Result<Self, Error> {
416 size.validate("RGBA frame")?;
417 let expected = size.pixels() as usize * 4;
418 if rgba.len() != expected {
419 return Err(Error::Codec(anyhow::anyhow!(
420 "RGBA buffer is {} bytes, expected {expected} for {size}",
421 rgba.len()
422 )));
423 }
424 Ok(Surface::I420(I420::from_rgba(
425 rgba,
426 size.width * 4,
427 size.width,
428 size.height,
429 )?))
430 }
431
432 pub fn resize(&self, size: Size) -> Result<Surface, Error> {
439 self.resize_with(size, &crate::resize::Config::default())
440 }
441
442 pub fn resize_with(&self, size: Size, config: &crate::resize::Config) -> Result<Surface, Error> {
444 let _ = config;
446 size.validate("resize to")?;
447 let Size { width, height } = size;
448
449 Ok(match self {
450 Surface::I420(i420) => Surface::I420(i420.resize(width, height)?),
451 #[cfg(target_os = "macos")]
452 Surface::PixelBuffer(pixels) if config.acceleration == crate::resize::Acceleration::Cpu => {
453 Surface::I420(pixels.download_i420()?.resize(width, height)?)
454 }
455 #[cfg(target_os = "macos")]
456 Surface::PixelBuffer(pixels) => match pixels.resize(width, height) {
457 Ok(scaled) => Surface::PixelBuffer(scaled),
458 Err(err) => {
461 static WARN_ONCE: std::sync::Once = std::sync::Once::new();
462 WARN_ONCE.call_once(|| tracing::warn!(%err, "GPU resize failed; falling back to the CPU"));
463 Surface::I420(pixels.download_i420()?.resize(width, height)?)
464 }
465 },
466 #[cfg(all(target_os = "linux", feature = "nvidia"))]
467 Surface::Cuda(cuda) if config.acceleration == crate::resize::Acceleration::Cpu => {
468 Surface::I420(cuda.download_i420()?.resize(width, height)?)
469 }
470 #[cfg(all(target_os = "linux", feature = "nvidia"))]
471 Surface::Cuda(cuda) => match cuda.resize(width, height) {
472 Ok(scaled) => Surface::Cuda(scaled),
473 Err(err) => {
476 static WARN_ONCE: std::sync::Once = std::sync::Once::new();
477 WARN_ONCE.call_once(|| tracing::warn!(%err, "GPU resize failed; falling back to the CPU"));
478 Surface::I420(cuda.download_i420()?.resize(width, height)?)
479 }
480 },
481 #[cfg(target_os = "windows")]
482 Surface::Texture(texture) if config.acceleration == crate::resize::Acceleration::Cpu => {
483 Surface::I420(texture.download_i420()?.resize(width, height)?)
484 }
485 #[cfg(target_os = "windows")]
486 Surface::Texture(texture) => match texture.resize(width, height) {
487 Ok(scaled) => Surface::Texture(scaled),
488 Err(err) => {
492 static WARN_ONCE: std::sync::Once = std::sync::Once::new();
493 WARN_ONCE.call_once(|| tracing::warn!(%err, "GPU resize failed; falling back to the CPU"));
494 Surface::I420(texture.download_i420()?.resize(width, height)?)
495 }
496 },
497 #[allow(unreachable_patterns)]
498 other => Surface::I420(other.to_i420()?.into_owned().resize(width, height)?),
499 })
500 }
501
502 pub fn into_i420(self) -> Result<Bytes, Error> {
512 match self {
513 Surface::I420(i420) => Ok(Bytes::from(i420.data)),
514 #[allow(unreachable_patterns)]
515 other => Ok(Bytes::from(other.to_i420()?.into_owned().data)),
516 }
517 }
518
519 pub fn into_rgba(self) -> Result<crate::convert::Rgba, Error> {
525 self.into_rgba_with(&crate::convert::Config::default())
526 }
527
528 pub fn into_rgba_with(self, config: &crate::convert::Config) -> Result<crate::convert::Rgba, Error> {
530 crate::convert::rgba(self, config)
531 }
532
533 #[cfg(target_os = "macos")]
547 pub fn into_pixel_buffer(
548 self,
549 ) -> Result<objc2_core_foundation::CFRetained<objc2_core_video::CVPixelBuffer>, Error> {
550 match self {
551 Surface::PixelBuffer(pixels) => Ok(pixels.buffer),
552 Surface::I420(i420) => macos::upload_i420(&i420),
553 }
554 }
555
556 pub fn color(&self) -> Option<Color> {
566 match self {
567 #[cfg(target_os = "macos")]
568 Surface::PixelBuffer(s) => s.color(),
569 #[cfg(target_os = "windows")]
570 Surface::Texture(_) => None,
571 #[cfg(all(target_os = "linux", feature = "nvidia"))]
572 Surface::Cuda(_) => None,
573 #[cfg(all(target_os = "linux", feature = "dmabuf"))]
574 Surface::DmaBuf(d) => d.color,
575 Surface::I420(i) => i.color(),
576 }
577 }
578
579 pub(crate) fn to_i420(&self) -> Result<Cow<'_, I420>, Error> {
581 match self {
582 #[cfg(target_os = "macos")]
583 Surface::PixelBuffer(s) => Ok(Cow::Owned(s.download_i420()?)),
584 #[cfg(target_os = "windows")]
585 Surface::Texture(t) => Ok(Cow::Owned(t.download_i420()?)),
586 #[cfg(all(target_os = "linux", feature = "nvidia"))]
587 Surface::Cuda(c) => Ok(Cow::Owned(c.download_i420()?)),
588 #[cfg(all(target_os = "linux", feature = "dmabuf"))]
589 Surface::DmaBuf(d) => Ok(Cow::Owned(d.inner.download_i420()?)),
590 Surface::I420(i) => Ok(Cow::Borrowed(i)),
591 }
592 }
593}
594
595#[derive(Clone)]
598pub struct I420 {
599 pub(crate) width: u32,
600 pub(crate) height: u32,
601 pub(crate) data: Vec<u8>,
603 pub(crate) color: Option<Color>,
608}
609
610impl I420 {
611 pub fn new(width: u32, height: u32, data: Vec<u8>) -> Result<Self, Error> {
618 crate::Size::new(width, height).validate("I420")?;
619 let expected = Self::len(width, height);
620 if data.len() != expected {
621 return Err(Error::Codec(anyhow::anyhow!(
622 "I420 {width}x{height} needs {expected} bytes, got {}",
623 data.len()
624 )));
625 }
626 Ok(Self {
627 width,
628 height,
629 data,
630 color: None,
631 })
632 }
633
634 pub fn width(&self) -> u32 {
636 self.width
637 }
638
639 pub fn height(&self) -> u32 {
641 self.height
642 }
643
644 pub fn data(&self) -> &[u8] {
646 &self.data
647 }
648
649 pub fn color(&self) -> Option<Color> {
657 self.color
658 }
659
660 pub fn with_color(mut self, color: Color) -> Self {
663 self.color = Some(color);
664 self
665 }
666
667 pub fn len(width: u32, height: u32) -> usize {
669 let luma = width as usize * height as usize;
670 luma + luma / 2
671 }
672
673 pub(crate) fn from_rgba(rgba: &[u8], stride: u32, width: u32, height: u32) -> Result<Self, Error> {
678 let color = Color::infer(Size::new(width, height));
679 let (range, matrix) = color.yuv();
680 let mut planar = YuvPlanarImageMut::alloc(width, height, YuvChromaSubsampling::Yuv420);
681 rgba_to_yuv420(&mut planar, rgba, stride, range, matrix, YuvConversionMode::Balanced)
682 .map_err(|e| Error::Codec(anyhow::anyhow!("rgba_to_yuv420 failed for {width}x{height}: {e}")))?;
683 Ok(Self::pack(&planar, width, height, Some(color)))
684 }
685
686 #[cfg(any(target_os = "windows", all(target_os = "linux", feature = "pipewire")))]
692 pub(crate) fn from_bgra(bgra: &[u8], stride: u32, width: u32, height: u32) -> Result<Self, Error> {
693 use yuv::bgra_to_yuv420;
694
695 let color = Color::infer(Size::new(width, height));
696 let (range, matrix) = color.yuv();
697 let mut planar = YuvPlanarImageMut::alloc(width, height, YuvChromaSubsampling::Yuv420);
698 bgra_to_yuv420(&mut planar, bgra, stride, range, matrix, YuvConversionMode::Balanced)
699 .map_err(|e| Error::Codec(anyhow::anyhow!("bgra_to_yuv420 failed for {width}x{height}: {e}")))?;
700 Ok(Self::pack(&planar, width, height, Some(color)))
701 }
702
703 pub(crate) fn from_planes(
709 y: &[u8],
710 u: &[u8],
711 v: &[u8],
712 y_stride: usize,
713 uv_stride: usize,
714 width: u32,
715 height: u32,
716 ) -> Self {
717 let (w, h) = (width as usize, height as usize);
718 let (cw, ch) = (w / 2, h / 2);
719
720 let mut data = vec![0u8; Self::len(width, height)];
721 let (luma, chroma) = data.split_at_mut(w * h);
722 let (u_dst, v_dst) = chroma.split_at_mut(cw * ch);
723
724 for row in 0..h {
725 luma[row * w..row * w + w].copy_from_slice(&y[row * y_stride..row * y_stride + w]);
726 }
727 for row in 0..ch {
728 u_dst[row * cw..row * cw + cw].copy_from_slice(&u[row * uv_stride..row * uv_stride + cw]);
729 v_dst[row * cw..row * cw + cw].copy_from_slice(&v[row * uv_stride..row * uv_stride + cw]);
730 }
731
732 Self {
733 width,
734 height,
735 data,
736 color: None,
737 }
738 }
739
740 #[cfg(all(target_os = "linux", feature = "capture"))]
744 pub(crate) fn from_rgb(rgb: &[u8], width: u32, height: u32) -> Result<Self, Error> {
745 use yuv::rgb_to_yuv420;
746
747 let color = Color::infer(Size::new(width, height));
748 let (range, matrix) = color.yuv();
749 let mut planar = YuvPlanarImageMut::alloc(width, height, YuvChromaSubsampling::Yuv420);
750 rgb_to_yuv420(&mut planar, rgb, width * 3, range, matrix, YuvConversionMode::Balanced)
751 .map_err(|e| Error::Codec(anyhow::anyhow!("rgb_to_yuv420 failed for {width}x{height}: {e}")))?;
752 Ok(Self::pack(&planar, width, height, Some(color)))
753 }
754
755 #[cfg(all(target_os = "linux", feature = "capture"))]
759 pub(crate) fn from_yuyv(yuyv: &[u8], stride: u32, width: u32, height: u32) -> Result<Self, Error> {
760 use yuv::{YuvPackedImage, yuyv422_to_yuv420};
761
762 let mut planar = YuvPlanarImageMut::alloc(width, height, YuvChromaSubsampling::Yuv420);
763 let packed = YuvPackedImage {
764 yuy: yuyv,
765 yuy_stride: stride,
766 width,
767 height,
768 };
769 yuyv422_to_yuv420(&mut planar, &packed)
770 .map_err(|e| Error::Codec(anyhow::anyhow!("yuyv422_to_yuv420 failed for {width}x{height}: {e}")))?;
771 Ok(Self::pack(&planar, width, height, None))
774 }
775
776 #[cfg(any(target_os = "windows", all(target_os = "linux", feature = "pipewire")))]
781 pub(crate) fn from_nv12(nv12: &[u8], width: u32, height: u32) -> Result<Self, Error> {
782 let (w, h) = (width as usize, height as usize);
783 let luma = w * h;
784 let chroma = luma / 4;
785 let need = luma + 2 * chroma;
786 if nv12.len() < need {
787 return Err(Error::Codec(anyhow::anyhow!(
788 "NV12 buffer too small: {} < {need} for {width}x{height}",
789 nv12.len()
790 )));
791 }
792
793 let mut data = vec![0u8; Self::len(width, height)];
794 data[..luma].copy_from_slice(&nv12[..luma]);
795 let (u_dst, v_dst) = data[luma..].split_at_mut(chroma);
796 deinterleave_uv(&nv12[luma..need], u_dst, v_dst);
797 Ok(Self {
798 width,
799 height,
800 data,
801 color: None,
802 })
803 }
804
805 pub(crate) fn resize(&self, width: u32, height: u32) -> Result<Self, Error> {
809 use std::cell::RefCell;
810
811 use fast_image_resize::images::{Image, ImageRef};
812 use fast_image_resize::{FilterType, PixelType, ResizeAlg, ResizeOptions, Resizer};
813
814 thread_local! {
818 static RESIZER: RefCell<Resizer> = RefCell::new(Resizer::new());
819 }
820
821 let options = ResizeOptions::new().resize_alg(ResizeAlg::Convolution(FilterType::Bilinear));
824
825 let plane = |resizer: &mut Resizer,
826 src: &[u8],
827 sw: u32,
828 sh: u32,
829 dst: &mut [u8],
830 dw: u32,
831 dh: u32|
832 -> Result<(), Error> {
833 let src = ImageRef::new(sw, sh, src, PixelType::U8)
834 .map_err(|e| Error::Codec(anyhow::anyhow!("resize source: {e}")))?;
835 let mut dst = Image::from_slice_u8(dw, dh, dst, PixelType::U8)
836 .map_err(|e| Error::Codec(anyhow::anyhow!("resize destination: {e}")))?;
837 resizer
838 .resize(&src, &mut dst, &options)
839 .map_err(|e| Error::Codec(anyhow::anyhow!("resize: {e}")))
840 };
841
842 let luma = width as usize * height as usize;
843 let mut data = vec![0u8; Self::len(width, height)];
844 let (y_dst, chroma) = data.split_at_mut(luma);
845 let (u_dst, v_dst) = chroma.split_at_mut(luma / 4);
846
847 RESIZER.with_borrow_mut(|resizer| {
848 plane(resizer, self.y(), self.width, self.height, y_dst, width, height)?;
849 let (sw2, sh2) = (self.width / 2, self.height / 2);
850 let (dw2, dh2) = (width / 2, height / 2);
851 plane(resizer, self.u(), sw2, sh2, u_dst, dw2, dh2)?;
852 plane(resizer, self.v(), sw2, sh2, v_dst, dw2, dh2)
853 })?;
854
855 Ok(Self {
857 width,
858 height,
859 data,
860 color: self.color,
861 })
862 }
863
864 fn pack(planar: &YuvPlanarImageMut<u8>, width: u32, height: u32, color: Option<Color>) -> Self {
870 let mut data = Vec::with_capacity(Self::len(width, height));
871 data.extend_from_slice(planar.y_plane.borrow());
872 data.extend_from_slice(planar.u_plane.borrow());
873 data.extend_from_slice(planar.v_plane.borrow());
874 Self {
875 width,
876 height,
877 data,
878 color,
879 }
880 }
881
882 fn luma_len(&self) -> usize {
883 self.width as usize * self.height as usize
884 }
885
886 fn chroma_len(&self) -> usize {
887 self.luma_len() / 4
888 }
889
890 pub fn y(&self) -> &[u8] {
892 &self.data[..self.luma_len()]
893 }
894
895 pub fn u(&self) -> &[u8] {
897 let start = self.luma_len();
898 &self.data[start..start + self.chroma_len()]
899 }
900
901 pub fn v(&self) -> &[u8] {
903 let start = self.luma_len() + self.chroma_len();
904 &self.data[start..start + self.chroma_len()]
905 }
906}
907
908#[cfg(any(target_os = "windows", all(target_os = "linux", feature = "nvidia")))]
911pub(crate) fn interleave_uv(u: &[u8], v: &[u8], uv: &mut [u8]) {
912 for (pair, (u, v)) in uv.chunks_exact_mut(2).zip(u.iter().zip(v)) {
913 pair[0] = *u;
914 pair[1] = *v;
915 }
916}
917
918#[cfg(any(target_os = "windows", all(target_os = "linux", feature = "pipewire")))]
921pub(crate) fn deinterleave_uv(uv: &[u8], u: &mut [u8], v: &mut [u8]) {
922 for (pair, (u, v)) in uv.chunks_exact(2).zip(u.iter_mut().zip(v)) {
923 *u = pair[0];
924 *v = pair[1];
925 }
926}
927
928#[cfg(any(target_os = "macos", target_os = "windows"))]
936struct Cache<K, T> {
937 values: std::collections::HashMap<K, std::sync::Arc<std::sync::Mutex<T>>>,
938 order: std::collections::VecDeque<K>,
939 capacity: usize,
940}
941
942#[cfg(any(target_os = "macos", target_os = "windows"))]
943impl<K: Clone + Eq + std::hash::Hash, T> Cache<K, T> {
944 fn new(capacity: usize) -> Self {
945 Self {
946 values: std::collections::HashMap::new(),
947 order: std::collections::VecDeque::new(),
948 capacity,
949 }
950 }
951
952 fn get_or_insert_with<E>(
953 &mut self,
954 key: K,
955 create: impl FnOnce() -> Result<T, E>,
956 ) -> Result<std::sync::Arc<std::sync::Mutex<T>>, E> {
957 if let Some(value) = self.values.get(&key).cloned() {
958 self.touch(&key);
959 return Ok(value);
960 }
961
962 let value = std::sync::Arc::new(std::sync::Mutex::new(create()?));
963 self.values.insert(key.clone(), std::sync::Arc::clone(&value));
964 self.touch(&key);
965 self.prune();
966 Ok(value)
967 }
968
969 fn touch(&mut self, key: &K) {
970 self.order.retain(|entry| entry != key);
971 self.order.push_back(key.clone());
972 }
973
974 fn prune(&mut self) {
975 let mut remaining = self.order.len();
976 while self.values.len() > self.capacity && remaining > 0 {
977 let key = self.order.pop_front().expect("remaining entries");
978 let idle = self
979 .values
980 .get(&key)
981 .is_some_and(|value| std::sync::Arc::strong_count(value) == 1);
982 if idle {
983 self.values.remove(&key);
984 } else {
985 self.order.push_back(key);
986 }
987 remaining -= 1;
988 }
989 }
990}
991
992#[cfg(all(test, any(target_os = "macos", target_os = "windows")))]
993mod cache_tests {
994 use super::Cache;
995
996 #[test]
997 fn evicts_the_least_recently_used_idle_value() {
998 let mut cache = Cache::new(2);
999
1000 let first = cache.get_or_insert_with((1, 1), || Ok::<_, ()>(())).unwrap();
1001 drop(first);
1002 let second = cache.get_or_insert_with((2, 2), || Ok::<_, ()>(())).unwrap();
1003 drop(second);
1004
1005 let first = cache
1006 .get_or_insert_with((1, 1), || Err::<(), _>("cached value was recreated"))
1007 .unwrap();
1008 drop(first);
1009 let third = cache.get_or_insert_with((3, 3), || Ok::<_, ()>(())).unwrap();
1010 drop(third);
1011
1012 assert!(cache.values.contains_key(&(1, 1)));
1013 assert!(!cache.values.contains_key(&(2, 2)));
1014 assert!(cache.values.contains_key(&(3, 3)));
1015 assert_eq!(cache.values.len(), 2);
1016 }
1017
1018 #[test]
1019 fn defers_eviction_until_an_active_value_is_released() {
1020 let mut cache = Cache::new(1);
1021 let first = cache.get_or_insert_with((1, 1), || Ok::<_, ()>(())).unwrap();
1022 let second = cache.get_or_insert_with((2, 2), || Ok::<_, ()>(())).unwrap();
1023 assert_eq!(cache.values.len(), 2);
1024
1025 drop(first);
1026 cache.prune();
1027 assert!(!cache.values.contains_key(&(1, 1)));
1028 assert!(cache.values.contains_key(&(2, 2)));
1029 assert_eq!(cache.values.len(), 1);
1030 drop(second);
1031 }
1032
1033 #[test]
1034 fn caches_failure_markers() {
1035 let mut attempts = 0;
1036 let mut cache = Cache::new(1);
1037 let failed = cache
1038 .get_or_insert_with(1, || {
1039 attempts += 1;
1040 Ok::<_, ()>(Err::<(), _>("unsupported"))
1041 })
1042 .unwrap();
1043 drop(failed);
1044 let failed = cache
1045 .get_or_insert_with(1, || {
1046 attempts += 1;
1047 Ok::<_, ()>(Ok::<_, &str>(()))
1048 })
1049 .unwrap();
1050
1051 assert_eq!(attempts, 1);
1052 assert!(failed.lock().unwrap().is_err());
1053 }
1054}
1055
1056#[cfg(target_os = "macos")]
1057pub mod macos {
1058 use std::ffi::c_void;
1063 use std::ptr;
1064 use std::ptr::NonNull;
1065 use std::sync::{LazyLock, Mutex};
1066
1067 use objc2_core_foundation::{CFDictionary, CFNumber, CFNumberType, CFRetained, CFString};
1068 use objc2_core_video::{
1069 CVPixelBuffer, CVPixelBufferCreate, CVPixelBufferGetBaseAddressOfPlane, CVPixelBufferGetBytesPerRowOfPlane,
1070 CVPixelBufferGetPixelFormatType, CVPixelBufferLockBaseAddress, CVPixelBufferLockFlags, CVPixelBufferPool,
1071 CVPixelBufferUnlockBaseAddress, kCVImageBufferYCbCrMatrix_ITU_R_601_4, kCVImageBufferYCbCrMatrix_ITU_R_709_2,
1072 kCVImageBufferYCbCrMatrixKey, kCVPixelBufferHeightKey, kCVPixelBufferIOSurfacePropertiesKey,
1073 kCVPixelBufferPixelFormatTypeKey, kCVPixelBufferWidthKey, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
1074 kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, kCVPixelFormatType_420YpCbCr8Planar,
1075 };
1076 use objc2_video_toolbox::VTPixelTransferSession;
1077
1078 use super::{Cache, I420};
1079 use crate::{Color, Error};
1080
1081 const LOCK_READ_ONLY: CVPixelBufferLockFlags = CVPixelBufferLockFlags(1);
1083
1084 const SCALER_CACHE_CAPACITY: usize = 16;
1087
1088 type ScalerCache = Mutex<Cache<(u32, u32), Scaler>>;
1092 static SCALERS: LazyLock<ScalerCache> = LazyLock::new(|| Mutex::new(Cache::new(SCALER_CACHE_CAPACITY)));
1093
1094 pub struct PixelBuffer {
1097 pub(crate) buffer: CFRetained<CVPixelBuffer>,
1098 pub(crate) width: u32,
1099 pub(crate) height: u32,
1100 }
1101
1102 unsafe impl Send for PixelBuffer {}
1112 unsafe impl Sync for PixelBuffer {}
1113
1114 impl PixelBuffer {
1115 pub fn buffer(&self) -> &CVPixelBuffer {
1118 &self.buffer
1119 }
1120
1121 pub fn width(&self) -> u32 {
1123 self.width
1124 }
1125
1126 pub fn height(&self) -> u32 {
1128 self.height
1129 }
1130
1131 pub(crate) fn new(buffer: CFRetained<CVPixelBuffer>, width: u32, height: u32) -> Self {
1132 Self { buffer, width, height }
1133 }
1134
1135 pub(crate) fn resize(&self, width: u32, height: u32) -> Result<Self, Error> {
1137 let scaler = {
1138 let mut scalers = SCALERS
1139 .lock()
1140 .map_err(|_| Error::Codec(anyhow::anyhow!("pixel-transfer scaler cache lock poisoned")))?;
1141 scalers.get_or_insert_with((width, height), || Scaler::new(width, height))?
1142 };
1143
1144 let result = scaler
1145 .lock()
1146 .map_err(|_| Error::Codec(anyhow::anyhow!("pixel-transfer scaler lock poisoned")))?
1147 .resize(self);
1148 drop(scaler);
1149 if let Ok(mut scalers) = SCALERS.lock() {
1150 scalers.prune();
1151 }
1152 result
1153 }
1154
1155 fn matrix(&self) -> Color {
1163 let inferred = Color::infer(crate::Size::new(self.width, self.height));
1164 let Some(value) = (unsafe { self.buffer.attachment(kCVImageBufferYCbCrMatrixKey, ptr::null_mut()) }) else {
1166 return inferred;
1167 };
1168 let Some(name) = value.downcast_ref::<CFString>() else {
1169 return inferred;
1170 };
1171
1172 if name == unsafe { kCVImageBufferYCbCrMatrix_ITU_R_709_2 } {
1175 Color::Bt709Limited
1176 } else if name == unsafe { kCVImageBufferYCbCrMatrix_ITU_R_601_4 } {
1177 Color::Bt601Limited
1178 } else {
1179 inferred
1182 }
1183 }
1184
1185 pub(crate) fn color(&self) -> Option<Color> {
1189 let format = CVPixelBufferGetPixelFormatType(&self.buffer);
1190 let limited = if format == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange {
1191 true
1192 } else if format == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange {
1193 false
1194 } else {
1195 return None;
1196 };
1197 Some(self.matrix().with_range(limited))
1198 }
1199
1200 pub(crate) fn download_i420(&self) -> Result<I420, Error> {
1207 let format = CVPixelBufferGetPixelFormatType(&self.buffer);
1208 if format != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
1209 && format != kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
1210 {
1211 return Err(Error::Codec(anyhow::anyhow!(
1212 "cannot download pixel format {format:#x}; expected NV12"
1213 )));
1214 }
1215
1216 let color = self.color();
1217
1218 let (w, h) = (self.width as usize, self.height as usize);
1219 let (cw, ch) = (w / 2, h / 2);
1220
1221 let status = unsafe { CVPixelBufferLockBaseAddress(&self.buffer, LOCK_READ_ONLY) };
1222 if status != 0 {
1223 return Err(Error::Codec(anyhow::anyhow!(
1224 "CVPixelBufferLockBaseAddress failed: {status}"
1225 )));
1226 }
1227 let _guard = UnlockGuard(&self.buffer);
1228
1229 let mut data = vec![0u8; I420::len(self.width, self.height)];
1230 let (luma, chroma) = data.split_at_mut(w * h);
1231 let (u_plane, v_plane) = chroma.split_at_mut(cw * ch);
1232
1233 let y_base = CVPixelBufferGetBaseAddressOfPlane(&self.buffer, 0) as *const u8;
1235 let y_stride = CVPixelBufferGetBytesPerRowOfPlane(&self.buffer, 0);
1236 for row in 0..h {
1237 unsafe {
1238 ptr::copy_nonoverlapping(y_base.add(row * y_stride), luma[row * w..].as_mut_ptr(), w);
1239 }
1240 }
1241
1242 let uv_base = CVPixelBufferGetBaseAddressOfPlane(&self.buffer, 1) as *const u8;
1244 let uv_stride = CVPixelBufferGetBytesPerRowOfPlane(&self.buffer, 1);
1245 for row in 0..ch {
1246 let src = unsafe { uv_base.add(row * uv_stride) };
1247 for col in 0..cw {
1248 unsafe {
1249 u_plane[row * cw + col] = *src.add(col * 2);
1250 v_plane[row * cw + col] = *src.add(col * 2 + 1);
1251 }
1252 }
1253 }
1254
1255 Ok(I420 {
1256 width: self.width,
1257 height: self.height,
1258 data,
1259 color,
1260 })
1261 }
1262 }
1263
1264 struct Scaler {
1266 session: CFRetained<VTPixelTransferSession>,
1267 pool: CFRetained<CVPixelBufferPool>,
1268 width: u32,
1269 height: u32,
1270 }
1271
1272 unsafe impl Send for Scaler {}
1276
1277 impl Scaler {
1278 fn new(width: u32, height: u32) -> Result<Self, Error> {
1279 let mut session_ptr: *mut VTPixelTransferSession = std::ptr::null_mut();
1280 let status = unsafe {
1281 VTPixelTransferSession::create(None, NonNull::new(&mut session_ptr).expect("stack pointer is non-null"))
1282 };
1283 let session = NonNull::new(session_ptr)
1284 .filter(|_| status == 0)
1285 .map(|ptr| unsafe { CFRetained::from_raw(ptr) })
1286 .ok_or_else(|| Error::Codec(anyhow::anyhow!("VTPixelTransferSessionCreate failed: {status}")))?;
1287
1288 let attributes = pool_attributes(width, height)?;
1289 let mut pool_ptr: *mut CVPixelBufferPool = std::ptr::null_mut();
1290 let status = unsafe {
1291 CVPixelBufferPool::create(
1292 None,
1293 None,
1294 Some(&attributes),
1295 NonNull::new(&mut pool_ptr).expect("stack pointer is non-null"),
1296 )
1297 };
1298 let pool = NonNull::new(pool_ptr)
1299 .filter(|_| status == 0)
1300 .map(|ptr| unsafe { CFRetained::from_raw(ptr) })
1301 .ok_or_else(|| Error::Codec(anyhow::anyhow!("CVPixelBufferPoolCreate failed: {status}")))?;
1302
1303 Ok(Self {
1304 session,
1305 pool,
1306 width,
1307 height,
1308 })
1309 }
1310
1311 fn resize(&mut self, source: &PixelBuffer) -> Result<PixelBuffer, Error> {
1312 let mut output_ptr: *mut CVPixelBuffer = std::ptr::null_mut();
1313 let status = unsafe {
1314 CVPixelBufferPool::create_pixel_buffer(
1315 None,
1316 &self.pool,
1317 NonNull::new(&mut output_ptr).expect("stack pointer is non-null"),
1318 )
1319 };
1320 let output = NonNull::new(output_ptr)
1321 .filter(|_| status == 0)
1322 .map(|ptr| unsafe { CFRetained::from_raw(ptr) })
1323 .ok_or_else(|| Error::Codec(anyhow::anyhow!("CVPixelBufferPoolCreatePixelBuffer failed: {status}")))?;
1324
1325 let status = unsafe { self.session.transfer_image(&source.buffer, &output) };
1326 if status != 0 {
1327 return Err(Error::Codec(anyhow::anyhow!(
1328 "VTPixelTransferSessionTransferImage failed: {status}"
1329 )));
1330 }
1331
1332 Ok(PixelBuffer::new(output, self.width, self.height))
1333 }
1334 }
1335
1336 fn pool_attributes(width: u32, height: u32) -> Result<CFRetained<CFDictionary>, Error> {
1338 let width =
1339 i32::try_from(width).map_err(|_| Error::Codec(anyhow::anyhow!("pixel-buffer width is too large")))?;
1340 let height =
1341 i32::try_from(height).map_err(|_| Error::Codec(anyhow::anyhow!("pixel-buffer height is too large")))?;
1342 let format = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange as i32;
1343
1344 let width = cf_number(width)?;
1345 let height = cf_number(height)?;
1346 let format = cf_number(format)?;
1347 let iosurface = unsafe {
1348 CFDictionary::new(
1349 None,
1350 std::ptr::null_mut(),
1351 std::ptr::null_mut(),
1352 0,
1353 &objc2_core_foundation::kCFTypeDictionaryKeyCallBacks,
1354 &objc2_core_foundation::kCFTypeDictionaryValueCallBacks,
1355 )
1356 }
1357 .ok_or_else(|| Error::Codec(anyhow::anyhow!("failed to build IOSurface attributes dictionary")))?;
1358
1359 let mut keys = [
1360 (unsafe { kCVPixelBufferPixelFormatTypeKey } as *const CFString).cast::<c_void>(),
1361 (unsafe { kCVPixelBufferWidthKey } as *const CFString).cast::<c_void>(),
1362 (unsafe { kCVPixelBufferHeightKey } as *const CFString).cast::<c_void>(),
1363 (unsafe { kCVPixelBufferIOSurfacePropertiesKey } as *const CFString).cast::<c_void>(),
1364 ];
1365 let mut values = [
1366 (format.as_ref() as *const CFNumber).cast::<c_void>(),
1367 (width.as_ref() as *const CFNumber).cast::<c_void>(),
1368 (height.as_ref() as *const CFNumber).cast::<c_void>(),
1369 (iosurface.as_ref() as *const CFDictionary).cast::<c_void>(),
1370 ];
1371 unsafe {
1372 CFDictionary::new(
1373 None,
1374 keys.as_mut_ptr(),
1375 values.as_mut_ptr(),
1376 4,
1377 &objc2_core_foundation::kCFTypeDictionaryKeyCallBacks,
1378 &objc2_core_foundation::kCFTypeDictionaryValueCallBacks,
1379 )
1380 }
1381 .ok_or_else(|| {
1382 Error::Codec(anyhow::anyhow!(
1383 "failed to build pixel-buffer pool attributes dictionary"
1384 ))
1385 })
1386 }
1387
1388 fn cf_number(value: i32) -> Result<CFRetained<CFNumber>, Error> {
1389 unsafe { CFNumber::new(None, CFNumberType::SInt32Type, (&value as *const i32).cast::<c_void>()) }
1390 .ok_or_else(|| Error::Codec(anyhow::anyhow!("failed to build CFNumber")))
1391 }
1392
1393 struct UnlockGuard<'a>(&'a CVPixelBuffer);
1394
1395 impl Drop for UnlockGuard<'_> {
1396 fn drop(&mut self) {
1397 unsafe { CVPixelBufferUnlockBaseAddress(self.0, LOCK_READ_ONLY) };
1398 }
1399 }
1400
1401 pub(crate) fn upload_i420(frame: &I420) -> Result<CFRetained<CVPixelBuffer>, Error> {
1407 let (w, h) = (frame.width as usize, frame.height as usize);
1408 let (cw, ch) = (w / 2, h / 2);
1409
1410 let mut ptr: *mut CVPixelBuffer = std::ptr::null_mut();
1411 let status = unsafe {
1412 CVPixelBufferCreate(
1413 None,
1414 w,
1415 h,
1416 kCVPixelFormatType_420YpCbCr8Planar,
1417 None,
1418 NonNull::new(&mut ptr).unwrap(),
1419 )
1420 };
1421 let buffer = NonNull::new(ptr)
1422 .filter(|_| status == 0)
1423 .map(|p| unsafe { CFRetained::from_raw(p) })
1424 .ok_or_else(|| Error::Codec(anyhow::anyhow!("CVPixelBufferCreate failed: {status}")))?;
1425
1426 let flags = CVPixelBufferLockFlags(0);
1427 let status = unsafe { CVPixelBufferLockBaseAddress(&buffer, flags) };
1428 if status != 0 {
1429 return Err(Error::Codec(anyhow::anyhow!(
1430 "CVPixelBufferLockBaseAddress failed: {status}"
1431 )));
1432 }
1433
1434 copy_plane(&buffer, 0, frame.y(), w, h);
1435 copy_plane(&buffer, 1, frame.u(), cw, ch);
1436 copy_plane(&buffer, 2, frame.v(), cw, ch);
1437
1438 unsafe { CVPixelBufferUnlockBaseAddress(&buffer, flags) };
1439 Ok(buffer)
1440 }
1441
1442 fn copy_plane(buffer: &CVPixelBuffer, plane: usize, src: &[u8], row_bytes: usize, rows: usize) {
1445 let base = CVPixelBufferGetBaseAddressOfPlane(buffer, plane) as *mut u8;
1446 let stride = CVPixelBufferGetBytesPerRowOfPlane(buffer, plane);
1447 for y in 0..rows {
1448 unsafe {
1449 let dst = base.add(y * stride);
1450 std::ptr::copy_nonoverlapping(src[y * row_bytes..].as_ptr(), dst, row_bytes);
1451 }
1452 }
1453 }
1454}
1455
1456#[cfg(all(target_os = "linux", feature = "nvidia"))]
1457pub mod cuda {
1458 use std::sync::{Arc, OnceLock};
1462
1463 use cudarc::driver::{CudaContext, CudaFunction, LaunchConfig, PushKernelArg, result};
1464
1465 use super::I420;
1466 use crate::Error;
1467
1468 const RESIZE_PTX: &str = include_str!("frame/nv12_resize.ptx");
1471
1472 struct Kernels {
1475 luma: CudaFunction,
1476 chroma: CudaFunction,
1477 }
1478
1479 fn kernels(ctx: &Arc<CudaContext>) -> Result<&'static Kernels, Error> {
1480 static KERNELS: OnceLock<Result<Kernels, String>> = OnceLock::new();
1481 KERNELS
1482 .get_or_init(|| {
1483 let module = ctx
1484 .load_module(cudarc::nvrtc::Ptx::from_src(RESIZE_PTX))
1485 .map_err(|e| format!("load nv12_resize PTX: {e:?}"))?;
1486 Ok(Kernels {
1487 luma: module
1488 .load_function("resize_luma")
1489 .map_err(|e| format!("load resize_luma: {e:?}"))?,
1490 chroma: module
1491 .load_function("resize_chroma")
1492 .map_err(|e| format!("load resize_chroma: {e:?}"))?,
1493 })
1494 })
1495 .as_ref()
1496 .map_err(|e| Error::Codec(anyhow::anyhow!("CUDA resize unavailable: {e}")))
1497 }
1498
1499 struct Buffer {
1504 ctx: Arc<CudaContext>,
1505 ptr: cudarc::driver::sys::CUdeviceptr,
1506 len: usize,
1507 }
1508
1509 impl Drop for Buffer {
1510 fn drop(&mut self) {
1511 if self.ctx.bind_to_thread().is_ok() {
1513 let _ = unsafe { result::free_sync(self.ptr) };
1515 }
1516 }
1517 }
1518
1519 #[derive(Clone)]
1527 pub struct Frame {
1528 buf: Arc<Buffer>,
1529 pub(crate) width: u32,
1530 pub(crate) height: u32,
1531 pub(crate) pitch: u32,
1533 }
1534
1535 impl Frame {
1536 pub(crate) fn alloc(ctx: &Arc<CudaContext>, width: u32, height: u32, pitch: u32) -> Result<Self, Error> {
1539 debug_assert!(pitch >= width && width.is_multiple_of(2) && height.is_multiple_of(2));
1540 let len = pitch as usize * height as usize * 3 / 2;
1541 ctx.bind_to_thread()
1542 .map_err(|e| Error::Codec(anyhow::anyhow!("CUDA bind: {e:?}")))?;
1543 let ptr = unsafe { result::malloc_sync(len) }
1546 .map_err(|e| Error::Codec(anyhow::anyhow!("CUDA alloc of {len} bytes: {e:?}")))?;
1547 Ok(Self {
1548 buf: Arc::new(Buffer {
1549 ctx: ctx.clone(),
1550 ptr,
1551 len,
1552 }),
1553 width,
1554 height,
1555 pitch,
1556 })
1557 }
1558
1559 pub(crate) fn device_ptr(&self) -> u64 {
1562 self.buf.ptr
1563 }
1564
1565 pub(crate) fn download_i420(&self) -> Result<I420, Error> {
1568 self.buf
1569 .ctx
1570 .bind_to_thread()
1571 .map_err(|e| Error::Codec(anyhow::anyhow!("CUDA bind: {e:?}")))?;
1572 let mut host = vec![0u8; self.buf.len];
1573 unsafe { result::memcpy_dtoh_sync(&mut host, self.buf.ptr) }
1576 .map_err(|e| Error::Codec(anyhow::anyhow!("CUDA download: {e:?}")))?;
1577
1578 let (w, h) = (self.width as usize, self.height as usize);
1579 let (cw, ch) = (w / 2, h / 2);
1580 let pitch = self.pitch as usize;
1581
1582 let mut data = vec![0u8; I420::len(self.width, self.height)];
1583 let (luma, chroma) = data.split_at_mut(w * h);
1584 let (u_dst, v_dst) = chroma.split_at_mut(cw * ch);
1585
1586 for row in 0..h {
1587 luma[row * w..row * w + w].copy_from_slice(&host[row * pitch..row * pitch + w]);
1588 }
1589 let uv_base = pitch * h;
1590 for row in 0..ch {
1591 let src = &host[uv_base + row * pitch..uv_base + row * pitch + w];
1592 for col in 0..cw {
1593 u_dst[row * cw + col] = src[col * 2];
1594 v_dst[row * cw + col] = src[col * 2 + 1];
1595 }
1596 }
1597
1598 Ok(I420 {
1599 width: self.width,
1600 height: self.height,
1601 data,
1602 color: None,
1605 })
1606 }
1607
1608 pub(crate) fn resize(&self, width: u32, height: u32) -> Result<Self, Error> {
1612 let ctx = &self.buf.ctx;
1613 let kernels = kernels(ctx)?;
1614
1615 let pitch = width.next_multiple_of(256);
1618 let dst = Self::alloc(ctx, width, height, pitch)?;
1619
1620 let stream = ctx.default_stream();
1621 let block = (16u32, 16, 1);
1622 let grid = |w: u32, h: u32| (w.div_ceil(16), h.div_ceil(16), 1);
1623 let launch_err = |plane: &str, e| Error::Codec(anyhow::anyhow!("CUDA resize {plane}: {e:?}"));
1624
1625 unsafe {
1631 stream
1632 .launch_builder(&kernels.luma)
1633 .arg(&self.buf.ptr)
1634 .arg(&self.pitch)
1635 .arg(&self.width)
1636 .arg(&self.height)
1637 .arg(&dst.buf.ptr)
1638 .arg(&pitch)
1639 .arg(&width)
1640 .arg(&height)
1641 .launch(LaunchConfig {
1642 grid_dim: grid(width, height),
1643 block_dim: block,
1644 shared_mem_bytes: 0,
1645 })
1646 }
1647 .map_err(|e| launch_err("luma", e))?;
1648
1649 let src_uv = self.buf.ptr + u64::from(self.pitch) * u64::from(self.height);
1652 let dst_uv = dst.buf.ptr + u64::from(pitch) * u64::from(height);
1653 let (src_pw, src_ph) = (self.width / 2, self.height / 2);
1654 let (dst_pw, dst_ph) = (width / 2, height / 2);
1655 unsafe {
1657 stream
1658 .launch_builder(&kernels.chroma)
1659 .arg(&src_uv)
1660 .arg(&self.pitch)
1661 .arg(&src_pw)
1662 .arg(&src_ph)
1663 .arg(&dst_uv)
1664 .arg(&pitch)
1665 .arg(&dst_pw)
1666 .arg(&dst_ph)
1667 .launch(LaunchConfig {
1668 grid_dim: grid(dst_pw, dst_ph),
1669 block_dim: block,
1670 shared_mem_bytes: 0,
1671 })
1672 }
1673 .map_err(|e| launch_err("chroma", e))?;
1674
1675 stream
1678 .synchronize()
1679 .map_err(|e| Error::Codec(anyhow::anyhow!("CUDA resize sync: {e:?}")))?;
1680 Ok(dst)
1681 }
1682 }
1683}
1684
1685#[cfg(target_os = "windows")]
1686pub mod d3d11 {
1687 use std::ffi::c_void;
1691 use std::ptr;
1692 use std::sync::{LazyLock, Mutex};
1693
1694 use windows::Win32::Foundation::{HMODULE, RECT};
1695 use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_HARDWARE;
1696 use windows::Win32::Graphics::Direct3D10::ID3D10Multithread;
1697 use windows::Win32::Graphics::Direct3D11::{
1698 D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE, D3D11_BIND_VIDEO_ENCODER, D3D11_BOX,
1699 D3D11_CPU_ACCESS_READ, D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_CREATE_DEVICE_VIDEO_SUPPORT,
1700 D3D11_FORMAT_SUPPORT, D3D11_FORMAT_SUPPORT_RENDER_TARGET, D3D11_FORMAT_SUPPORT_SHADER_SAMPLE,
1701 D3D11_FORMAT_SUPPORT_VIDEO_ENCODER, D3D11_MAP_READ, D3D11_MAPPED_SUBRESOURCE, D3D11_SDK_VERSION,
1702 D3D11_TEX2D_VPIV, D3D11_TEX2D_VPOV, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, D3D11_USAGE_STAGING,
1703 D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE, D3D11_VIDEO_PROCESSOR_COLOR_SPACE, D3D11_VIDEO_PROCESSOR_CONTENT_DESC,
1704 D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC, D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC_0,
1705 D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC, D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC_0, D3D11_VIDEO_PROCESSOR_STREAM,
1706 D3D11_VIDEO_USAGE_PLAYBACK_NORMAL, D3D11_VPIV_DIMENSION_TEXTURE2D, D3D11_VPOV_DIMENSION_TEXTURE2D,
1707 D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D, ID3D11VideoContext, ID3D11VideoDevice,
1708 ID3D11VideoProcessor, ID3D11VideoProcessorEnumerator, ID3D11VideoProcessorInputView,
1709 ID3D11VideoProcessorOutputView,
1710 };
1711 #[cfg(test)]
1712 use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_NV12;
1713 use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT, DXGI_RATIONAL, DXGI_SAMPLE_DESC};
1714 use windows::Win32::Media::MediaFoundation::{IMFDXGIBuffer, IMFSample};
1715 use windows::core::Interface;
1716
1717 use super::{Cache, I420};
1718 use crate::{Error, Size};
1719
1720 fn err(ctx: &str, e: windows::core::Error) -> Error {
1721 Error::Codec(anyhow::anyhow!("{ctx}: {e}"))
1722 }
1723
1724 pub(crate) fn create_device() -> Result<ID3D11Device, Error> {
1729 let mut device: Option<ID3D11Device> = None;
1730 unsafe {
1731 D3D11CreateDevice(
1732 None,
1733 D3D_DRIVER_TYPE_HARDWARE,
1734 HMODULE::default(),
1735 D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_VIDEO_SUPPORT,
1736 None,
1737 D3D11_SDK_VERSION,
1738 Some(&mut device),
1739 None,
1740 None,
1741 )
1742 .map_err(|e| err("D3D11CreateDevice", e))?;
1743 }
1744 let device = device.ok_or_else(|| Error::Codec(anyhow::anyhow!("D3D11CreateDevice returned null")))?;
1745
1746 let multithread = device
1747 .cast::<ID3D10Multithread>()
1748 .map_err(|e| err("query ID3D10Multithread", e))?;
1749 unsafe {
1750 let _ = multithread.SetMultithreadProtected(true);
1751 }
1752 Ok(device)
1753 }
1754
1755 pub struct Texture {
1761 pub(crate) device: ID3D11Device,
1762 pub(crate) texture: ID3D11Texture2D,
1763 pub(crate) width: u32,
1764 pub(crate) height: u32,
1765 }
1766
1767 impl Texture {
1768 pub(crate) fn copy_from_sample(
1791 device: &ID3D11Device,
1792 sample: &IMFSample,
1793 width: u32,
1794 height: u32,
1795 ) -> Result<Self, Error> {
1796 let (source, subresource) = resolve(sample)?;
1797
1798 let mut desc = D3D11_TEXTURE2D_DESC::default();
1800 unsafe { source.GetDesc(&mut desc) };
1801 let texture = alloc(device, width, height, desc.Format)?;
1802
1803 let region = D3D11_BOX {
1806 left: 0,
1807 top: 0,
1808 front: 0,
1809 right: width,
1810 bottom: height,
1811 back: 1,
1812 };
1813 let context = unsafe { device.GetImmediateContext() }.map_err(|e| err("GetImmediateContext", e))?;
1814 unsafe {
1815 context.CopySubresourceRegion(&texture, 0, 0, 0, 0, &source, subresource, Some(®ion));
1816 }
1817
1818 Ok(Self {
1819 device: device.clone(),
1820 texture,
1821 width,
1822 height,
1823 })
1824 }
1825
1826 pub fn texture(&self) -> &ID3D11Texture2D {
1835 &self.texture
1836 }
1837
1838 pub fn device(&self) -> &ID3D11Device {
1841 &self.device
1842 }
1843
1844 pub fn width(&self) -> u32 {
1846 self.width
1847 }
1848
1849 pub fn height(&self) -> u32 {
1851 self.height
1852 }
1853
1854 pub(crate) fn download_i420(&self) -> Result<I420, Error> {
1858 let context = unsafe { self.device.GetImmediateContext() }.map_err(|e| err("GetImmediateContext", e))?;
1859
1860 let mut desc = D3D11_TEXTURE2D_DESC::default();
1862 unsafe { self.texture.GetDesc(&mut desc) };
1863 desc.ArraySize = 1;
1864 desc.MipLevels = 1;
1865 desc.Usage = D3D11_USAGE_STAGING;
1866 desc.BindFlags = 0;
1867 desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ.0 as u32;
1868 desc.MiscFlags = 0;
1869
1870 let mut staging: Option<ID3D11Texture2D> = None;
1871 unsafe {
1872 self.device
1873 .CreateTexture2D(&desc, None, Some(&mut staging))
1874 .map_err(|e| err("CreateTexture2D (staging)", e))?;
1875 }
1876 let staging = staging.ok_or_else(|| Error::Codec(anyhow::anyhow!("CreateTexture2D returned null")))?;
1877
1878 unsafe {
1879 context.CopySubresourceRegion(&staging, 0, 0, 0, 0, &self.texture, 0, None);
1880 }
1881
1882 let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
1883 unsafe {
1884 context
1885 .Map(&staging, 0, D3D11_MAP_READ, 0, Some(&mut mapped))
1886 .map_err(|e| err("Map (staging)", e))?;
1887 }
1888 let _guard = UnmapGuard {
1889 context: &context,
1890 resource: &staging,
1891 };
1892
1893 let (w, h) = (self.width as usize, self.height as usize);
1894 let (cw, ch) = (w / 2, h / 2);
1895 let pitch = mapped.RowPitch as usize;
1896 let base = mapped.pData as *const u8;
1897 let tex_height = desc.Height as usize;
1903
1904 let mut data = vec![0u8; I420::len(self.width, self.height)];
1905 let (luma, chroma) = data.split_at_mut(w * h);
1906 let (u_plane, v_plane) = chroma.split_at_mut(cw * ch);
1907
1908 for row in 0..h {
1910 unsafe {
1911 ptr::copy_nonoverlapping(base.add(row * pitch), luma[row * w..].as_mut_ptr(), w);
1912 }
1913 }
1914 let uv_base = unsafe { base.add(pitch * tex_height) };
1916 for row in 0..ch {
1917 let src = unsafe { uv_base.add(row * pitch) };
1918 for col in 0..cw {
1919 unsafe {
1920 u_plane[row * cw + col] = *src.add(col * 2);
1921 v_plane[row * cw + col] = *src.add(col * 2 + 1);
1922 }
1923 }
1924 }
1925
1926 Ok(I420 {
1927 width: self.width,
1928 height: self.height,
1929 data,
1930 color: None,
1933 })
1934 }
1935
1936 pub(crate) fn resize(&self, width: u32, height: u32) -> Result<Self, Error> {
1946 let source = Size::new(self.width, self.height);
1947 let target = Size::new(width, height);
1948 let key = ScalerKey::new(&self.device, source, target);
1949
1950 let scaler = {
1951 let mut scalers = SCALERS
1952 .lock()
1953 .map_err(|_| Error::Codec(anyhow::anyhow!("video-processor cache lock poisoned")))?;
1954 scalers
1955 .get_or_insert_with(key, || {
1956 Ok::<_, std::convert::Infallible>(ScalerState::discover(&self.device, source, target))
1957 })
1958 .expect("scaler discovery is infallible")
1959 };
1960 let mut state = scaler
1961 .lock()
1962 .map_err(|_| Error::Codec(anyhow::anyhow!("video processor lock poisoned")))?;
1963 let result = match &*state {
1964 ScalerState::Ready(scaler) => scaler.scale(&self.texture),
1965 ScalerState::Unsupported { reason, .. } => {
1966 return Err(Error::Codec(anyhow::anyhow!("GPU resize is unsupported: {reason}")));
1967 }
1968 };
1969 let texture = match result {
1970 Ok(texture) => texture,
1971 Err(ScaleError::Unsupported(err)) => {
1972 *state = ScalerState::Unsupported {
1973 _device: self.device.clone(),
1974 reason: err.to_string(),
1975 };
1976 return Err(err);
1977 }
1978 Err(ScaleError::Transient(err)) => return Err(err),
1979 };
1980 drop(state);
1981 drop(scaler);
1982 if let Ok(mut scalers) = SCALERS.lock() {
1983 scalers.prune();
1984 }
1985
1986 Ok(Self {
1987 device: self.device.clone(),
1988 texture,
1989 width,
1990 height,
1991 })
1992 }
1993 }
1994
1995 const SCALER_CACHE_CAPACITY: usize = 16;
1998
1999 static SCALERS: LazyLock<Mutex<Cache<ScalerKey, ScalerState>>> =
2004 LazyLock::new(|| Mutex::new(Cache::new(SCALER_CACHE_CAPACITY)));
2005
2006 enum ScalerState {
2008 Ready(Scaler),
2009 Unsupported {
2010 _device: ID3D11Device,
2012 reason: String,
2013 },
2014 }
2015
2016 impl ScalerState {
2017 fn discover(device: &ID3D11Device, source: Size, target: Size) -> Self {
2018 match Scaler::new(device, source, target) {
2019 Ok(scaler) => Self::Ready(scaler),
2020 Err(err) => Self::Unsupported {
2021 _device: device.clone(),
2022 reason: err.to_string(),
2023 },
2024 }
2025 }
2026 }
2027
2028 #[derive(Clone, PartialEq, Eq, Hash)]
2035 struct ScalerKey {
2036 device: usize,
2037 source: Size,
2038 target: Size,
2039 }
2040
2041 impl ScalerKey {
2042 fn new(device: &ID3D11Device, source: Size, target: Size) -> Self {
2043 Self {
2044 device: device.as_raw() as usize,
2045 source,
2046 target,
2047 }
2048 }
2049 }
2050
2051 struct Scaler {
2054 device: ID3D11Device,
2056 video: ID3D11VideoDevice,
2057 context: ID3D11VideoContext,
2058 enumerator: ID3D11VideoProcessorEnumerator,
2059 processor: ID3D11VideoProcessor,
2060 target: Size,
2061 }
2062
2063 enum ScaleError {
2065 Unsupported(Error),
2066 Transient(Error),
2067 }
2068
2069 impl Scaler {
2070 fn new(device: &ID3D11Device, source: Size, target: Size) -> Result<Self, Error> {
2071 let video = device
2072 .cast::<ID3D11VideoDevice>()
2073 .map_err(|e| err("query ID3D11VideoDevice", e))?;
2074 let immediate = unsafe { device.GetImmediateContext() }.map_err(|e| err("GetImmediateContext", e))?;
2075 let context = immediate
2076 .cast::<ID3D11VideoContext>()
2077 .map_err(|e| err("query ID3D11VideoContext", e))?;
2078
2079 let rate = DXGI_RATIONAL {
2082 Numerator: 30,
2083 Denominator: 1,
2084 };
2085 let desc = D3D11_VIDEO_PROCESSOR_CONTENT_DESC {
2086 InputFrameFormat: D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE,
2087 InputFrameRate: rate,
2088 InputWidth: source.width,
2089 InputHeight: source.height,
2090 OutputFrameRate: rate,
2091 OutputWidth: target.width,
2092 OutputHeight: target.height,
2093 Usage: D3D11_VIDEO_USAGE_PLAYBACK_NORMAL,
2094 };
2095
2096 let enumerator = unsafe { video.CreateVideoProcessorEnumerator(&desc) }
2097 .map_err(|e| err("CreateVideoProcessorEnumerator", e))?;
2098 let processor =
2099 unsafe { video.CreateVideoProcessor(&enumerator, 0) }.map_err(|e| err("CreateVideoProcessor", e))?;
2100
2101 let full = RECT {
2102 left: 0,
2103 top: 0,
2104 right: source.width as i32,
2105 bottom: source.height as i32,
2106 };
2107 let scaled = RECT {
2108 left: 0,
2109 top: 0,
2110 right: target.width as i32,
2111 bottom: target.height as i32,
2112 };
2113 unsafe {
2114 context.VideoProcessorSetStreamFrameFormat(&processor, 0, D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE);
2115 context.VideoProcessorSetStreamSourceRect(&processor, 0, true, Some(&full));
2117 context.VideoProcessorSetStreamDestRect(&processor, 0, true, Some(&scaled));
2118 context.VideoProcessorSetStreamAutoProcessingMode(&processor, 0, false);
2122 let space = D3D11_VIDEO_PROCESSOR_COLOR_SPACE::default();
2126 context.VideoProcessorSetStreamColorSpace(&processor, 0, &space);
2127 context.VideoProcessorSetOutputColorSpace(&processor, &space);
2128 }
2129
2130 Ok(Self {
2131 device: device.clone(),
2132 video,
2133 context,
2134 enumerator,
2135 processor,
2136 target,
2137 })
2138 }
2139
2140 fn scale(&self, source: &ID3D11Texture2D) -> Result<ID3D11Texture2D, ScaleError> {
2142 let mut desc = D3D11_TEXTURE2D_DESC::default();
2143 unsafe { source.GetDesc(&mut desc) };
2144 let output = alloc(&self.device, self.target.width, self.target.height, desc.Format)
2145 .map_err(ScaleError::Transient)?;
2146
2147 let input_desc = D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC {
2148 FourCC: 0,
2149 ViewDimension: D3D11_VPIV_DIMENSION_TEXTURE2D,
2150 Anonymous: D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC_0 {
2151 Texture2D: D3D11_TEX2D_VPIV {
2152 MipSlice: 0,
2153 ArraySlice: 0,
2154 },
2155 },
2156 };
2157 let mut input: Option<ID3D11VideoProcessorInputView> = None;
2158 unsafe {
2159 self.video
2160 .CreateVideoProcessorInputView(source, &self.enumerator, &input_desc, Some(&mut input))
2161 .map_err(|e| ScaleError::Unsupported(err("CreateVideoProcessorInputView", e)))?;
2162 }
2163 let input =
2164 input.ok_or_else(|| ScaleError::Unsupported(Error::Codec(anyhow::anyhow!("input view is null"))))?;
2165
2166 let output_desc = D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC {
2167 ViewDimension: D3D11_VPOV_DIMENSION_TEXTURE2D,
2168 Anonymous: D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC_0 {
2169 Texture2D: D3D11_TEX2D_VPOV { MipSlice: 0 },
2170 },
2171 };
2172 let mut view: Option<ID3D11VideoProcessorOutputView> = None;
2173 unsafe {
2174 self.video
2175 .CreateVideoProcessorOutputView(&output, &self.enumerator, &output_desc, Some(&mut view))
2176 .map_err(|e| ScaleError::Unsupported(err("CreateVideoProcessorOutputView", e)))?;
2177 }
2178 let view =
2179 view.ok_or_else(|| ScaleError::Unsupported(Error::Codec(anyhow::anyhow!("output view is null"))))?;
2180
2181 let streams = [D3D11_VIDEO_PROCESSOR_STREAM {
2182 Enable: true.into(),
2183 OutputIndex: 0,
2184 InputFrameOrField: 0,
2185 PastFrames: 0,
2186 FutureFrames: 0,
2187 ppPastSurfaces: ptr::null_mut(),
2188 pInputSurface: std::mem::ManuallyDrop::new(Some(input)),
2189 ppFutureSurfaces: ptr::null_mut(),
2190 ppPastSurfacesRight: ptr::null_mut(),
2191 pInputSurfaceRight: std::mem::ManuallyDrop::new(None),
2192 ppFutureSurfacesRight: ptr::null_mut(),
2193 }];
2194 let result = unsafe { self.context.VideoProcessorBlt(&self.processor, &view, 0, &streams) };
2195 drop(std::mem::ManuallyDrop::into_inner(unsafe {
2199 ptr::read(&streams[0].pInputSurface)
2200 }));
2201 result.map_err(|e| ScaleError::Transient(err("VideoProcessorBlt", e)))?;
2202
2203 Ok(output)
2204 }
2205 }
2206
2207 fn alloc(device: &ID3D11Device, width: u32, height: u32, format: DXGI_FORMAT) -> Result<ID3D11Texture2D, Error> {
2210 let desc = D3D11_TEXTURE2D_DESC {
2211 Width: width,
2212 Height: height,
2213 MipLevels: 1,
2214 ArraySize: 1,
2215 Format: format,
2216 SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 },
2217 Usage: D3D11_USAGE_DEFAULT,
2218 BindFlags: bind_flags(device, format),
2219 CPUAccessFlags: 0,
2220 MiscFlags: 0,
2221 };
2222
2223 let mut texture: Option<ID3D11Texture2D> = None;
2224 unsafe {
2225 device
2226 .CreateTexture2D(&desc, None, Some(&mut texture))
2227 .map_err(|e| err("CreateTexture2D", e))?;
2228 }
2229 texture.ok_or_else(|| Error::Codec(anyhow::anyhow!("CreateTexture2D returned null")))
2230 }
2231
2232 #[cfg(test)]
2236 pub(crate) fn upload_i420(device: &ID3D11Device, frame: &I420) -> Result<Texture, Error> {
2237 let (width, height) = (frame.width, frame.height);
2238 let texture = alloc(device, width, height, DXGI_FORMAT_NV12)?;
2239
2240 let (w, h) = (width as usize, height as usize);
2241 let mut nv12 = vec![0u8; w * h * 3 / 2];
2242 let (luma, chroma) = nv12.split_at_mut(w * h);
2243 luma.copy_from_slice(frame.y());
2244 super::interleave_uv(frame.u(), frame.v(), chroma);
2245
2246 let context = unsafe { device.GetImmediateContext() }.map_err(|e| err("GetImmediateContext", e))?;
2247 unsafe {
2250 context.UpdateSubresource(
2251 &texture,
2252 0,
2253 None,
2254 nv12.as_ptr().cast::<c_void>(),
2255 width,
2256 nv12.len() as u32,
2257 );
2258 }
2259
2260 Ok(Texture {
2261 device: device.clone(),
2262 texture,
2263 width,
2264 height,
2265 })
2266 }
2267
2268 fn resolve(sample: &IMFSample) -> Result<(ID3D11Texture2D, u32), Error> {
2271 let buffer = unsafe { sample.GetBufferByIndex(0) }.map_err(|e| err("get sample buffer", e))?;
2272 let dxgi = buffer
2273 .cast::<IMFDXGIBuffer>()
2274 .map_err(|e| err("sample buffer is not a DXGI surface", e))?;
2275
2276 let mut raw: *mut c_void = ptr::null_mut();
2278 unsafe {
2279 dxgi.GetResource(&ID3D11Texture2D::IID, &mut raw)
2280 .map_err(|e| err("get DXGI resource", e))?;
2281 }
2282 let texture = unsafe { ID3D11Texture2D::from_raw(raw) };
2283 let subresource = unsafe { dxgi.GetSubresourceIndex() }.map_err(|e| err("get subresource index", e))?;
2284 Ok((texture, subresource))
2285 }
2286
2287 fn bind_flags(device: &ID3D11Device, format: DXGI_FORMAT) -> u32 {
2297 let support = unsafe { device.CheckFormatSupport(format) }.unwrap_or_default();
2298 let supports = |flag: D3D11_FORMAT_SUPPORT| support & flag.0 as u32 != 0;
2299
2300 let mut flags = 0;
2301 if supports(D3D11_FORMAT_SUPPORT_SHADER_SAMPLE) {
2302 flags |= D3D11_BIND_SHADER_RESOURCE.0 as u32;
2303 }
2304 if supports(D3D11_FORMAT_SUPPORT_RENDER_TARGET) {
2305 flags |= D3D11_BIND_RENDER_TARGET.0 as u32;
2306 }
2307 if supports(D3D11_FORMAT_SUPPORT_VIDEO_ENCODER) {
2308 flags |= D3D11_BIND_VIDEO_ENCODER.0 as u32;
2309 }
2310 flags
2311 }
2312
2313 #[cfg(test)]
2315 pub(crate) fn supports_nv12_render_target(device: &ID3D11Device) -> bool {
2316 let support = unsafe { device.CheckFormatSupport(DXGI_FORMAT_NV12) }.unwrap_or_default();
2317 support & D3D11_FORMAT_SUPPORT_RENDER_TARGET.0 as u32 != 0
2318 }
2319
2320 struct UnmapGuard<'a> {
2321 context: &'a ID3D11DeviceContext,
2322 resource: &'a ID3D11Texture2D,
2323 }
2324
2325 impl Drop for UnmapGuard<'_> {
2326 fn drop(&mut self) {
2327 unsafe { self.context.Unmap(self.resource, 0) };
2328 }
2329 }
2330}
2331
2332#[cfg(test)]
2333mod tests {
2334 #[test]
2343 fn only_a_real_color_conversion_labels_its_output() {
2344 use super::I420;
2345 use crate::{Color, Size};
2346
2347 let size = Size::new(64, 64);
2348 let rgba = vec![0u8; size.pixels() as usize * 4];
2349 let converted = I420::from_rgba(&rgba, size.width * 4, size.width, size.height).expect("rgba to i420");
2350 assert_eq!(
2351 converted.color(),
2352 Some(Color::Bt601Limited),
2353 "an RGB conversion knows the matrix it used"
2354 );
2355
2356 let resized = converted.resize(32, 32).expect("resize");
2358 assert_eq!(resized.color(), Some(Color::Bt601Limited), "resize preserves the space");
2359
2360 let raw = I420::new(64, 64, vec![0; I420::len(64, 64)]).expect("i420");
2362 assert_eq!(raw.color(), None);
2363 assert_eq!(raw.with_color(Color::Bt709Full).color(), Some(Color::Bt709Full));
2364 }
2365
2366 #[cfg(all(target_os = "linux", feature = "capture"))]
2371 #[test]
2372 fn yuyv_capture_keeps_its_color_space_open() {
2373 let (width, height) = (1280, 720);
2374 let yuyv = vec![0u8; width as usize * height as usize * 2];
2376 let frame = super::I420::from_yuyv(&yuyv, width * 2, width, height).expect("yuyv to i420");
2377 assert_eq!(frame.color(), None, "a chroma resample names no color space");
2378 }
2379
2380 #[test]
2384 fn i420_new_rejects_a_short_buffer() {
2385 use super::I420;
2386
2387 assert!(I420::new(64, 32, vec![0; I420::len(64, 32)]).is_ok());
2388 assert!(I420::new(64, 32, vec![0; I420::len(64, 32) - 1]).is_err());
2389 assert!(I420::new(64, 32, Vec::new()).is_err());
2390 assert!(I420::new(63, 32, vec![0; I420::len(63, 32)]).is_err());
2392 assert!(I420::new(0, 32, Vec::new()).is_err());
2393 }
2394
2395 use super::{Frame, I420, Surface};
2396 use crate::Size;
2397
2398 #[test]
2401 fn surface_rgba_rejects_a_mismatched_buffer() {
2402 let ok = vec![0x80u8; 64 * 32 * 4];
2403 assert!(Surface::rgba(&ok, Size::new(64, 32)).is_ok());
2404 assert!(Surface::rgba(&ok[..ok.len() - 4], Size::new(64, 32)).is_err());
2405 assert!(Surface::rgba(&ok, Size::new(32, 32)).is_err());
2406 assert!(Surface::rgba(&ok, Size::new(0, 32)).is_err());
2407 }
2408
2409 #[test]
2412 fn decoder_planes_discard_row_padding() {
2413 let y = [
2414 1, 2, 3, 4, 200, 201, 202, 5, 6, 7, 8, 203, 204, 205, 9, 10, 11, 12, 206, 207, 208, 13, 14, 15, 16, 209,
2415 210, 211,
2416 ];
2417 let u = [21, 22, 220, 221, 23, 24, 222, 223];
2418 let v = [31, 32, 230, 231, 33, 34, 232, 233];
2419
2420 let frame = I420::from_planes(&y, &u, &v, 7, 4, 4, 4);
2421 assert_eq!(frame.y(), &(1..=16).collect::<Vec<_>>());
2422 assert_eq!(frame.u(), &[21, 22, 23, 24]);
2423 assert_eq!(frame.v(), &[31, 32, 33, 34]);
2424 }
2425
2426 #[test]
2434 fn rgb_conversion_follows_the_size_heuristic() {
2435 use yuv::{YuvPlanarImage, yuv420_to_rgba};
2436
2437 use crate::Color;
2438
2439 let red = |size: Size| {
2440 let rgba = [255u8, 0, 0, 255].repeat(size.pixels() as usize);
2441 I420::from_rgba(&rgba, size.width * 4, size.width, size.height).unwrap()
2442 };
2443
2444 let decode = |i420: &I420| {
2447 let (w, h) = (i420.width, i420.height);
2448 let (range, matrix) = Color::infer(Size::new(w, h)).yuv();
2449 let planar = YuvPlanarImage {
2450 y_plane: i420.y(),
2451 y_stride: w,
2452 u_plane: i420.u(),
2453 u_stride: w / 2,
2454 v_plane: i420.v(),
2455 v_stride: w / 2,
2456 width: w,
2457 height: h,
2458 };
2459 let mut rgba = vec![0u8; (w * h * 4) as usize];
2460 yuv420_to_rgba(&planar, &mut rgba, w * 4, range, matrix).unwrap();
2461 let px = ((h / 2 * w + w / 2) * 4) as usize;
2462 [rgba[px], rgba[px + 1], rgba[px + 2]]
2463 };
2464
2465 for (size, expected) in [
2466 (Size::new(720, 480), Color::Bt601Limited),
2467 (Size::new(720, 576), Color::Bt601Limited),
2468 (Size::new(1280, 720), Color::Bt709Limited),
2469 (Size::new(1920, 1080), Color::Bt709Limited),
2470 ] {
2471 let i420 = red(size);
2472 assert_eq!(i420.color(), Some(expected), "{size} reported color");
2473
2474 let rgb = decode(&i420);
2477 assert!(
2478 rgb[1] <= 2 && rgb[2] <= 2,
2479 "{size} red came back as {rgb:?}, so the matrix and the label disagree"
2480 );
2481 }
2482 }
2483
2484 #[test]
2487 fn frame_size_follows_the_surface() {
2488 let rgba = vec![0x80u8; 64 * 32 * 4];
2489 let surface = Surface::rgba(&rgba, Size::new(64, 32)).unwrap();
2490
2491 let frame = Frame::new(surface, moq_net::Timestamp::from_micros(1234).unwrap());
2492 assert_eq!(frame.size(), Size::new(64, 32));
2493
2494 let scaled = frame.resize(Size::new(32, 16)).unwrap();
2495 assert_eq!(scaled.size(), Size::new(32, 16));
2496 assert_eq!(scaled.timestamp, frame.timestamp);
2497 }
2498
2499 #[cfg(target_os = "macos")]
2503 #[test]
2504 fn into_pixel_buffer_uploads_a_cpu_frame() {
2505 use objc2_core_video::{CVPixelBufferGetHeight, CVPixelBufferGetWidth};
2506
2507 let i420 = I420::new(64, 32, vec![0x80; I420::len(64, 32)]).unwrap();
2508 let frame = Frame::new(Surface::I420(i420), moq_net::Timestamp::from_micros(0).unwrap());
2509
2510 let buffer = frame.surface.into_pixel_buffer().expect("upload a CPU frame");
2511 assert_eq!(CVPixelBufferGetWidth(&buffer), 64);
2512 assert_eq!(CVPixelBufferGetHeight(&buffer), 32);
2513 }
2514
2515 fn gradient_i420(width: u32, height: u32) -> I420 {
2518 let (w, h) = (width as usize, height as usize);
2519 let (cw, ch) = (w / 2, h / 2);
2520 let mut data = vec![0u8; I420::len(width, height)];
2521 let (y, chroma) = data.split_at_mut(w * h);
2522 let (u, v) = chroma.split_at_mut(cw * ch);
2523 for row in 0..h {
2524 for col in 0..w {
2525 y[row * w + col] = ((col * 255) / w) as u8;
2526 }
2527 }
2528 for row in 0..ch {
2529 for col in 0..cw {
2530 u[row * cw + col] = ((row * 255) / ch) as u8;
2531 v[row * cw + col] = (((row + col) * 255) / (ch + cw)) as u8;
2532 }
2533 }
2534 I420 {
2535 width,
2536 height,
2537 data,
2538 color: None,
2539 }
2540 }
2541
2542 fn mae(a: &[u8], b: &[u8]) -> u64 {
2544 assert_eq!(a.len(), b.len());
2545 a.iter().zip(b).map(|(x, y)| x.abs_diff(*y) as u64).sum::<u64>() / a.len() as u64
2546 }
2547
2548 #[test]
2551 fn i420_resize_follows_gradients() {
2552 let src = gradient_i420(320, 240);
2553 let dst = src.resize(128, 96).unwrap();
2554 assert_eq!((dst.width, dst.height), (128, 96));
2555
2556 let expected = gradient_i420(128, 96);
2558 assert!(mae(dst.y(), expected.y()) < 4, "luma ramp drifted");
2559 assert!(mae(dst.u(), expected.u()) < 4, "u ramp drifted");
2560 assert!(mae(dst.v(), expected.v()) < 4, "v ramp drifted");
2561 }
2562
2563 #[cfg(target_os = "macos")]
2566 #[test]
2567 fn pixel_buffer_resize_matches_cpu() {
2568 let src_i420 = gradient_i420(320, 240);
2569 let src = Surface::PixelBuffer(nv12_surface(&src_i420));
2570 let scaled = src.resize(Size::new(160, 120)).unwrap();
2571 let Surface::PixelBuffer(scaled) = scaled else {
2572 panic!("VideoToolbox resize downloaded to the CPU");
2573 };
2574
2575 let gpu = scaled.download_i420().unwrap();
2576 let cpu = src_i420.resize(160, 120).unwrap();
2577
2578 assert_eq!((gpu.width, gpu.height), (160, 120));
2579 assert!(mae(gpu.y(), cpu.y()) < 4, "GPU and CPU luma disagree");
2580 assert!(mae(gpu.u(), cpu.u()) < 4, "GPU and CPU u disagree");
2581 assert!(mae(gpu.v(), cpu.v()) < 4, "GPU and CPU v disagree");
2582 }
2583
2584 #[cfg(target_os = "macos")]
2586 #[test]
2587 fn pixel_buffer_resize_can_force_the_cpu() {
2588 let config = crate::resize::Config {
2589 acceleration: crate::resize::Acceleration::Cpu,
2590 ..Default::default()
2591 };
2592 let source = Surface::PixelBuffer(nv12_surface(&gradient_i420(320, 240)));
2593 let scaled = source.resize_with(Size::new(160, 120), &config).unwrap();
2594
2595 assert!(matches!(scaled, Surface::I420(_)), "CPU resize stayed on the GPU");
2596 }
2597
2598 #[cfg(target_os = "macos")]
2601 #[test]
2602 fn pixel_buffer_converts_to_rgba() {
2603 let source = gradient_i420(322, 242);
2604 let expected = Surface::I420(source.clone()).into_rgba().unwrap();
2605 let actual = Surface::PixelBuffer(nv12_surface(&source)).into_rgba().unwrap();
2606
2607 assert_eq!(actual.width(), 322);
2608 assert_eq!(actual.height(), 242);
2609 assert_eq!(actual.stride(), 322 * 4);
2610 assert_eq!(actual.data(), expected.data());
2611 }
2612
2613 #[cfg(target_os = "macos")]
2616 fn nv12_surface(frame: &I420) -> super::macos::PixelBuffer {
2617 use std::ptr::{self, NonNull};
2618
2619 use objc2_core_foundation::CFRetained;
2620 use objc2_core_video::{
2621 CVPixelBuffer, CVPixelBufferCreate, CVPixelBufferGetBaseAddressOfPlane, CVPixelBufferGetBytesPerRowOfPlane,
2622 CVPixelBufferLockBaseAddress, CVPixelBufferLockFlags, CVPixelBufferUnlockBaseAddress,
2623 kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
2624 };
2625
2626 let mut raw: *mut CVPixelBuffer = ptr::null_mut();
2627 let status = unsafe {
2628 CVPixelBufferCreate(
2629 None,
2630 frame.width as usize,
2631 frame.height as usize,
2632 kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
2633 None,
2634 NonNull::new(&mut raw).expect("stack pointer is non-null"),
2635 )
2636 };
2637 assert_eq!(status, 0, "CVPixelBufferCreate failed");
2638 let buffer = unsafe { CFRetained::from_raw(NonNull::new(raw).expect("CoreVideo returned a buffer")) };
2639
2640 let flags = CVPixelBufferLockFlags(0);
2641 assert_eq!(unsafe { CVPixelBufferLockBaseAddress(&buffer, flags) }, 0);
2642 let width = frame.width as usize;
2643 let height = frame.height as usize;
2644 let y_base = CVPixelBufferGetBaseAddressOfPlane(&buffer, 0) as *mut u8;
2645 let y_stride = CVPixelBufferGetBytesPerRowOfPlane(&buffer, 0);
2646 for row in 0..height {
2647 unsafe {
2648 ptr::copy_nonoverlapping(frame.y()[row * width..].as_ptr(), y_base.add(row * y_stride), width);
2649 }
2650 }
2651
2652 let (chroma_width, chroma_height) = (width / 2, height / 2);
2653 let uv_base = CVPixelBufferGetBaseAddressOfPlane(&buffer, 1) as *mut u8;
2654 let uv_stride = CVPixelBufferGetBytesPerRowOfPlane(&buffer, 1);
2655 for row in 0..chroma_height {
2656 let output = unsafe { uv_base.add(row * uv_stride) };
2657 for col in 0..chroma_width {
2658 unsafe {
2659 *output.add(col * 2) = frame.u()[row * chroma_width + col];
2660 *output.add(col * 2 + 1) = frame.v()[row * chroma_width + col];
2661 }
2662 }
2663 }
2664 unsafe { CVPixelBufferUnlockBaseAddress(&buffer, flags) };
2665
2666 super::macos::PixelBuffer::new(buffer, frame.width, frame.height)
2667 }
2668
2669 #[cfg(target_os = "windows")]
2671 #[test]
2672 #[ignore = "D3D11 GPU reproducer; VideoProcessorBlt can hang on affected drivers"]
2673 fn d3d11_resize_defaults_to_the_gpu() {
2674 let Ok(device) = super::d3d11::create_device() else {
2675 eprintln!("skipping: no Direct3D11 hardware device");
2676 return;
2677 };
2678 let Ok(texture) = super::d3d11::upload_i420(&device, &gradient_i420(320, 240)) else {
2679 eprintln!("skipping: driver will not allocate a usable NV12 texture");
2680 return;
2681 };
2682 if !super::d3d11::supports_nv12_render_target(&device) {
2683 eprintln!("skipping: driver cannot render to NV12");
2684 return;
2685 }
2686
2687 let scaled = Surface::Texture(texture).resize(crate::Size::new(160, 120)).unwrap();
2688 assert!(
2689 matches!(scaled, Surface::Texture(_)),
2690 "Direct3D11 resize downloaded to the CPU"
2691 );
2692 assert_eq!((scaled.width(), scaled.height()), (160, 120));
2693 }
2694
2695 #[cfg(target_os = "windows")]
2697 #[test]
2698 fn d3d11_resize_can_force_the_cpu() {
2699 let Ok(device) = super::d3d11::create_device() else {
2700 eprintln!("skipping: no Direct3D11 hardware device");
2701 return;
2702 };
2703 let Ok(texture) = super::d3d11::upload_i420(&device, &gradient_i420(320, 240)) else {
2704 eprintln!("skipping: driver will not allocate a usable NV12 texture");
2705 return;
2706 };
2707
2708 let config = crate::resize::Config {
2709 acceleration: crate::resize::Acceleration::Cpu,
2710 ..Default::default()
2711 };
2712 let scaled = Surface::Texture(texture)
2713 .resize_with(crate::Size::new(160, 120), &config)
2714 .unwrap();
2715 assert!(matches!(scaled, Surface::I420(_)), "Direct3D11 resize ignored CPU mode");
2716 }
2717
2718 #[cfg(target_os = "windows")]
2722 #[test]
2723 #[ignore = "explicit D3D11 GPU probe; VideoProcessorBlt can hang on affected drivers"]
2724 fn d3d11_resize_matches_cpu() {
2725 let Ok(device) = super::d3d11::create_device() else {
2726 eprintln!("skipping: no Direct3D11 hardware device");
2727 return;
2728 };
2729 let source = gradient_i420(320, 240);
2730 let Ok(texture) = super::d3d11::upload_i420(&device, &source) else {
2731 eprintln!("skipping: driver will not allocate a usable NV12 texture");
2732 return;
2733 };
2734 if !super::d3d11::supports_nv12_render_target(&device) {
2735 eprintln!("skipping: driver cannot render to NV12");
2736 return;
2737 }
2738
2739 let gpu = texture.resize(160, 120).unwrap().download_i420().unwrap();
2740 let cpu = source.resize(160, 120).unwrap();
2741
2742 assert_eq!((gpu.width, gpu.height), (160, 120));
2743 assert!(mae(gpu.y(), cpu.y()) < 4, "GPU and CPU luma disagree");
2744 assert!(mae(gpu.u(), cpu.u()) < 4, "GPU and CPU u disagree");
2745 assert!(mae(gpu.v(), cpu.v()) < 4, "GPU and CPU v disagree");
2746 }
2747
2748 #[cfg(all(target_os = "linux", feature = "nvidia"))]
2751 #[test]
2752 fn cuda_resize_matches_cpu() {
2753 use std::sync::Arc;
2754
2755 use cudarc::driver::{CudaContext, result};
2756
2757 use super::cuda;
2758
2759 if unsafe { libloading::Library::new("libcuda.so.1") }.is_err() {
2761 return;
2762 }
2763 let Ok(ctx): Result<Arc<CudaContext>, _> = CudaContext::new(0) else {
2764 return;
2765 };
2766
2767 let (w, h) = (322u32, 242u32); let src_i420 = gradient_i420(w, h);
2769
2770 let pitch = 512u32;
2772 let frame = cuda::Frame::alloc(&ctx, w, h, pitch).unwrap();
2773 let mut host = vec![0u8; pitch as usize * h as usize * 3 / 2];
2774 for row in 0..h as usize {
2775 let dst = row * pitch as usize;
2776 host[dst..dst + w as usize].copy_from_slice(&src_i420.y()[row * w as usize..(row + 1) * w as usize]);
2777 }
2778 let (cw, ch) = (w as usize / 2, h as usize / 2);
2779 for row in 0..ch {
2780 let dst = (h as usize + row) * pitch as usize;
2781 for col in 0..cw {
2782 host[dst + 2 * col] = src_i420.u()[row * cw + col];
2783 host[dst + 2 * col + 1] = src_i420.v()[row * cw + col];
2784 }
2785 }
2786 unsafe { result::memcpy_htod_sync(frame.device_ptr(), &host) }.unwrap();
2788
2789 let scaled = frame.resize(160, 120).unwrap();
2790 let gpu = scaled.download_i420().unwrap();
2791 let cpu = src_i420.resize(160, 120).unwrap();
2792
2793 assert_eq!((gpu.width, gpu.height), (160, 120));
2794 assert!(mae(gpu.y(), cpu.y()) < 4, "GPU and CPU luma disagree");
2795 assert!(mae(gpu.u(), cpu.u()) < 4, "GPU and CPU u disagree");
2796 assert!(mae(gpu.v(), cpu.v()) < 4, "GPU and CPU v disagree");
2797 }
2798}