1use std::borrow::Cow;
24
25use bytes::Bytes;
26use moq_net::Timestamp;
27
28use yuv::{YuvChromaSubsampling, YuvConversionMode, YuvPlanarImageMut, rgba_to_yuv420};
29
30use crate::{Color, Error, Size};
31
32pub struct Frame {
39 pub timestamp: Timestamp,
43 pub surface: Surface,
45}
46
47impl Frame {
48 pub fn new(surface: Surface, timestamp: Timestamp) -> Self {
50 Self { timestamp, surface }
51 }
52
53 pub fn size(&self) -> Size {
55 Size::new(self.surface.width(), self.surface.height())
56 }
57
58 pub fn resize(&self, size: Size) -> Result<Frame, Error> {
65 self.resize_with(size, &crate::resize::Config::default())
66 }
67
68 pub fn resize_with(&self, size: Size, config: &crate::resize::Config) -> Result<Frame, Error> {
70 Ok(Frame {
71 timestamp: self.timestamp,
72 surface: self.surface.resize_with(size, config)?,
73 })
74 }
75}
76
77#[non_exhaustive]
96pub enum Surface {
97 #[cfg(target_os = "macos")]
100 PixelBuffer(macos::PixelBuffer),
101 #[cfg(target_os = "windows")]
103 Texture(d3d11::Texture),
104 #[cfg(all(target_os = "linux", feature = "nvdec"))]
107 Cuda(cuda::Frame),
108 I420(I420),
110}
111
112impl Surface {
113 pub fn width(&self) -> u32 {
115 match self {
116 #[cfg(target_os = "macos")]
117 Surface::PixelBuffer(s) => s.width,
118 #[cfg(target_os = "windows")]
119 Surface::Texture(t) => t.width,
120 #[cfg(all(target_os = "linux", feature = "nvdec"))]
121 Surface::Cuda(c) => c.width,
122 Surface::I420(i) => i.width,
123 }
124 }
125
126 pub fn height(&self) -> u32 {
128 match self {
129 #[cfg(target_os = "macos")]
130 Surface::PixelBuffer(s) => s.height,
131 #[cfg(target_os = "windows")]
132 Surface::Texture(t) => t.height,
133 #[cfg(all(target_os = "linux", feature = "nvdec"))]
134 Surface::Cuda(c) => c.height,
135 Surface::I420(i) => i.height,
136 }
137 }
138
139 pub fn rgba(rgba: &[u8], size: Size) -> Result<Self, Error> {
148 size.validate("RGBA frame")?;
149 let expected = size.pixels() as usize * 4;
150 if rgba.len() != expected {
151 return Err(Error::Codec(anyhow::anyhow!(
152 "RGBA buffer is {} bytes, expected {expected} for {size}",
153 rgba.len()
154 )));
155 }
156 Ok(Surface::I420(I420::from_rgba(
157 rgba,
158 size.width * 4,
159 size.width,
160 size.height,
161 )?))
162 }
163
164 pub fn resize(&self, size: Size) -> Result<Surface, Error> {
171 self.resize_with(size, &crate::resize::Config::default())
172 }
173
174 pub fn resize_with(&self, size: Size, config: &crate::resize::Config) -> Result<Surface, Error> {
176 let _ = config;
178 size.validate("resize to")?;
179 let Size { width, height } = size;
180
181 Ok(match self {
182 Surface::I420(i420) => Surface::I420(i420.resize(width, height)?),
183 #[cfg(target_os = "macos")]
184 Surface::PixelBuffer(pixels) if config.acceleration == crate::resize::Acceleration::Cpu => {
185 Surface::I420(pixels.download_i420()?.resize(width, height)?)
186 }
187 #[cfg(target_os = "macos")]
188 Surface::PixelBuffer(pixels) => match pixels.resize(width, height) {
189 Ok(scaled) => Surface::PixelBuffer(scaled),
190 Err(err) => {
193 static WARN_ONCE: std::sync::Once = std::sync::Once::new();
194 WARN_ONCE.call_once(|| tracing::warn!(%err, "GPU resize failed; falling back to the CPU"));
195 Surface::I420(pixels.download_i420()?.resize(width, height)?)
196 }
197 },
198 #[cfg(all(target_os = "linux", feature = "nvdec"))]
199 Surface::Cuda(cuda) if config.acceleration == crate::resize::Acceleration::Cpu => {
200 Surface::I420(cuda.download_i420()?.resize(width, height)?)
201 }
202 #[cfg(all(target_os = "linux", feature = "nvdec"))]
203 Surface::Cuda(cuda) => match cuda.resize(width, height) {
204 Ok(scaled) => Surface::Cuda(scaled),
205 Err(err) => {
208 static WARN_ONCE: std::sync::Once = std::sync::Once::new();
209 WARN_ONCE.call_once(|| tracing::warn!(%err, "GPU resize failed; falling back to the CPU"));
210 Surface::I420(cuda.download_i420()?.resize(width, height)?)
211 }
212 },
213 #[cfg(target_os = "windows")]
214 Surface::Texture(texture) if config.acceleration == crate::resize::Acceleration::Cpu => {
215 Surface::I420(texture.download_i420()?.resize(width, height)?)
216 }
217 #[cfg(target_os = "windows")]
218 Surface::Texture(texture) => match texture.resize(width, height) {
219 Ok(scaled) => Surface::Texture(scaled),
220 Err(err) => {
224 static WARN_ONCE: std::sync::Once = std::sync::Once::new();
225 WARN_ONCE.call_once(|| tracing::warn!(%err, "GPU resize failed; falling back to the CPU"));
226 Surface::I420(texture.download_i420()?.resize(width, height)?)
227 }
228 },
229 #[allow(unreachable_patterns)]
230 other => Surface::I420(other.to_i420()?.into_owned().resize(width, height)?),
231 })
232 }
233
234 pub fn into_i420(self) -> Result<Bytes, Error> {
244 match self {
245 Surface::I420(i420) => Ok(Bytes::from(i420.data)),
246 #[allow(unreachable_patterns)]
247 other => Ok(Bytes::from(other.to_i420()?.into_owned().data)),
248 }
249 }
250
251 #[cfg(target_os = "macos")]
265 pub fn into_pixel_buffer(
266 self,
267 ) -> Result<objc2_core_foundation::CFRetained<objc2_core_video::CVPixelBuffer>, Error> {
268 match self {
269 Surface::PixelBuffer(pixels) => Ok(pixels.buffer),
270 Surface::I420(i420) => macos::upload_i420(&i420),
271 }
272 }
273
274 pub fn color(&self) -> Option<Color> {
284 match self {
285 #[cfg(target_os = "macos")]
286 Surface::PixelBuffer(s) => s.color(),
287 #[cfg(target_os = "windows")]
288 Surface::Texture(_) => None,
289 #[cfg(all(target_os = "linux", feature = "nvdec"))]
290 Surface::Cuda(_) => None,
291 Surface::I420(i) => i.color(),
292 }
293 }
294
295 pub(crate) fn to_i420(&self) -> Result<Cow<'_, I420>, Error> {
297 match self {
298 #[cfg(target_os = "macos")]
299 Surface::PixelBuffer(s) => Ok(Cow::Owned(s.download_i420()?)),
300 #[cfg(target_os = "windows")]
301 Surface::Texture(t) => Ok(Cow::Owned(t.download_i420()?)),
302 #[cfg(all(target_os = "linux", feature = "nvdec"))]
303 Surface::Cuda(c) => Ok(Cow::Owned(c.download_i420()?)),
304 Surface::I420(i) => Ok(Cow::Borrowed(i)),
305 }
306 }
307}
308
309#[derive(Clone)]
312pub struct I420 {
313 pub(crate) width: u32,
314 pub(crate) height: u32,
315 pub(crate) data: Vec<u8>,
317 pub(crate) color: Option<Color>,
322}
323
324impl I420 {
325 pub fn new(width: u32, height: u32, data: Vec<u8>) -> Result<Self, Error> {
332 crate::Size::new(width, height).validate("I420")?;
333 let expected = Self::len(width, height);
334 if data.len() != expected {
335 return Err(Error::Codec(anyhow::anyhow!(
336 "I420 {width}x{height} needs {expected} bytes, got {}",
337 data.len()
338 )));
339 }
340 Ok(Self {
341 width,
342 height,
343 data,
344 color: None,
345 })
346 }
347
348 pub fn width(&self) -> u32 {
350 self.width
351 }
352
353 pub fn height(&self) -> u32 {
355 self.height
356 }
357
358 pub fn data(&self) -> &[u8] {
360 &self.data
361 }
362
363 pub fn color(&self) -> Option<Color> {
371 self.color
372 }
373
374 pub fn with_color(mut self, color: Color) -> Self {
377 self.color = Some(color);
378 self
379 }
380
381 pub fn len(width: u32, height: u32) -> usize {
383 let luma = width as usize * height as usize;
384 luma + luma / 2
385 }
386
387 pub(crate) fn from_rgba(rgba: &[u8], stride: u32, width: u32, height: u32) -> Result<Self, Error> {
392 let color = Color::infer(Size::new(width, height));
393 let (range, matrix) = color.yuv();
394 let mut planar = YuvPlanarImageMut::alloc(width, height, YuvChromaSubsampling::Yuv420);
395 rgba_to_yuv420(&mut planar, rgba, stride, range, matrix, YuvConversionMode::Balanced)
396 .map_err(|e| Error::Codec(anyhow::anyhow!("rgba_to_yuv420 failed for {width}x{height}: {e}")))?;
397 Ok(Self::pack(&planar, width, height, Some(color)))
398 }
399
400 #[cfg(any(target_os = "windows", all(target_os = "linux", feature = "pipewire")))]
406 pub(crate) fn from_bgra(bgra: &[u8], stride: u32, width: u32, height: u32) -> Result<Self, Error> {
407 use yuv::bgra_to_yuv420;
408
409 let color = Color::infer(Size::new(width, height));
410 let (range, matrix) = color.yuv();
411 let mut planar = YuvPlanarImageMut::alloc(width, height, YuvChromaSubsampling::Yuv420);
412 bgra_to_yuv420(&mut planar, bgra, stride, range, matrix, YuvConversionMode::Balanced)
413 .map_err(|e| Error::Codec(anyhow::anyhow!("bgra_to_yuv420 failed for {width}x{height}: {e}")))?;
414 Ok(Self::pack(&planar, width, height, Some(color)))
415 }
416
417 pub(crate) fn from_planes(
423 y: &[u8],
424 u: &[u8],
425 v: &[u8],
426 y_stride: usize,
427 uv_stride: usize,
428 width: u32,
429 height: u32,
430 ) -> Self {
431 let (w, h) = (width as usize, height as usize);
432 let (cw, ch) = (w / 2, h / 2);
433
434 let mut data = vec![0u8; Self::len(width, height)];
435 let (luma, chroma) = data.split_at_mut(w * h);
436 let (u_dst, v_dst) = chroma.split_at_mut(cw * ch);
437
438 for row in 0..h {
439 luma[row * w..row * w + w].copy_from_slice(&y[row * y_stride..row * y_stride + w]);
440 }
441 for row in 0..ch {
442 u_dst[row * cw..row * cw + cw].copy_from_slice(&u[row * uv_stride..row * uv_stride + cw]);
443 v_dst[row * cw..row * cw + cw].copy_from_slice(&v[row * uv_stride..row * uv_stride + cw]);
444 }
445
446 Self {
447 width,
448 height,
449 data,
450 color: None,
451 }
452 }
453
454 #[cfg(target_os = "linux")]
458 pub(crate) fn from_rgb(rgb: &[u8], width: u32, height: u32) -> Result<Self, Error> {
459 use yuv::rgb_to_yuv420;
460
461 let color = Color::infer(Size::new(width, height));
462 let (range, matrix) = color.yuv();
463 let mut planar = YuvPlanarImageMut::alloc(width, height, YuvChromaSubsampling::Yuv420);
464 rgb_to_yuv420(&mut planar, rgb, width * 3, range, matrix, YuvConversionMode::Balanced)
465 .map_err(|e| Error::Codec(anyhow::anyhow!("rgb_to_yuv420 failed for {width}x{height}: {e}")))?;
466 Ok(Self::pack(&planar, width, height, Some(color)))
467 }
468
469 #[cfg(target_os = "linux")]
473 pub(crate) fn from_yuyv(yuyv: &[u8], stride: u32, width: u32, height: u32) -> Result<Self, Error> {
474 use yuv::{YuvPackedImage, yuyv422_to_yuv420};
475
476 let mut planar = YuvPlanarImageMut::alloc(width, height, YuvChromaSubsampling::Yuv420);
477 let packed = YuvPackedImage {
478 yuy: yuyv,
479 yuy_stride: stride,
480 width,
481 height,
482 };
483 yuyv422_to_yuv420(&mut planar, &packed)
484 .map_err(|e| Error::Codec(anyhow::anyhow!("yuyv422_to_yuv420 failed for {width}x{height}: {e}")))?;
485 Ok(Self::pack(&planar, width, height, None))
488 }
489
490 #[cfg(target_os = "windows")]
495 pub(crate) fn from_nv12(nv12: &[u8], width: u32, height: u32) -> Result<Self, Error> {
496 let (w, h) = (width as usize, height as usize);
497 let luma = w * h;
498 let chroma = luma / 4;
499 let need = luma + 2 * chroma;
500 if nv12.len() < need {
501 return Err(Error::Codec(anyhow::anyhow!(
502 "NV12 buffer too small: {} < {need} for {width}x{height}",
503 nv12.len()
504 )));
505 }
506
507 let mut data = vec![0u8; Self::len(width, height)];
508 data[..luma].copy_from_slice(&nv12[..luma]);
509 let (u_dst, v_dst) = data[luma..].split_at_mut(chroma);
510 deinterleave_uv(&nv12[luma..need], u_dst, v_dst);
511 Ok(Self {
512 width,
513 height,
514 data,
515 color: None,
516 })
517 }
518
519 pub(crate) fn resize(&self, width: u32, height: u32) -> Result<Self, Error> {
523 use std::cell::RefCell;
524
525 use fast_image_resize::images::{Image, ImageRef};
526 use fast_image_resize::{FilterType, PixelType, ResizeAlg, ResizeOptions, Resizer};
527
528 thread_local! {
532 static RESIZER: RefCell<Resizer> = RefCell::new(Resizer::new());
533 }
534
535 let options = ResizeOptions::new().resize_alg(ResizeAlg::Convolution(FilterType::Bilinear));
538
539 let plane = |resizer: &mut Resizer,
540 src: &[u8],
541 sw: u32,
542 sh: u32,
543 dst: &mut [u8],
544 dw: u32,
545 dh: u32|
546 -> Result<(), Error> {
547 let src = ImageRef::new(sw, sh, src, PixelType::U8)
548 .map_err(|e| Error::Codec(anyhow::anyhow!("resize source: {e}")))?;
549 let mut dst = Image::from_slice_u8(dw, dh, dst, PixelType::U8)
550 .map_err(|e| Error::Codec(anyhow::anyhow!("resize destination: {e}")))?;
551 resizer
552 .resize(&src, &mut dst, &options)
553 .map_err(|e| Error::Codec(anyhow::anyhow!("resize: {e}")))
554 };
555
556 let luma = width as usize * height as usize;
557 let mut data = vec![0u8; Self::len(width, height)];
558 let (y_dst, chroma) = data.split_at_mut(luma);
559 let (u_dst, v_dst) = chroma.split_at_mut(luma / 4);
560
561 RESIZER.with_borrow_mut(|resizer| {
562 plane(resizer, self.y(), self.width, self.height, y_dst, width, height)?;
563 let (sw2, sh2) = (self.width / 2, self.height / 2);
564 let (dw2, dh2) = (width / 2, height / 2);
565 plane(resizer, self.u(), sw2, sh2, u_dst, dw2, dh2)?;
566 plane(resizer, self.v(), sw2, sh2, v_dst, dw2, dh2)
567 })?;
568
569 Ok(Self {
571 width,
572 height,
573 data,
574 color: self.color,
575 })
576 }
577
578 fn pack(planar: &YuvPlanarImageMut<u8>, width: u32, height: u32, color: Option<Color>) -> Self {
584 let mut data = Vec::with_capacity(Self::len(width, height));
585 data.extend_from_slice(planar.y_plane.borrow());
586 data.extend_from_slice(planar.u_plane.borrow());
587 data.extend_from_slice(planar.v_plane.borrow());
588 Self {
589 width,
590 height,
591 data,
592 color,
593 }
594 }
595
596 fn luma_len(&self) -> usize {
597 self.width as usize * self.height as usize
598 }
599
600 fn chroma_len(&self) -> usize {
601 self.luma_len() / 4
602 }
603
604 pub fn y(&self) -> &[u8] {
606 &self.data[..self.luma_len()]
607 }
608
609 pub fn u(&self) -> &[u8] {
611 let start = self.luma_len();
612 &self.data[start..start + self.chroma_len()]
613 }
614
615 pub fn v(&self) -> &[u8] {
617 let start = self.luma_len() + self.chroma_len();
618 &self.data[start..start + self.chroma_len()]
619 }
620}
621
622#[cfg(any(target_os = "windows", all(target_os = "linux", feature = "nvenc")))]
625pub(crate) fn interleave_uv(u: &[u8], v: &[u8], uv: &mut [u8]) {
626 for (pair, (u, v)) in uv.chunks_exact_mut(2).zip(u.iter().zip(v)) {
627 pair[0] = *u;
628 pair[1] = *v;
629 }
630}
631
632#[cfg(target_os = "windows")]
635pub(crate) fn deinterleave_uv(uv: &[u8], u: &mut [u8], v: &mut [u8]) {
636 for (pair, (u, v)) in uv.chunks_exact(2).zip(u.iter_mut().zip(v)) {
637 *u = pair[0];
638 *v = pair[1];
639 }
640}
641
642#[cfg(any(target_os = "macos", target_os = "windows"))]
650struct Cache<K, T> {
651 values: std::collections::HashMap<K, std::sync::Arc<std::sync::Mutex<T>>>,
652 order: std::collections::VecDeque<K>,
653 capacity: usize,
654}
655
656#[cfg(any(target_os = "macos", target_os = "windows"))]
657impl<K: Clone + Eq + std::hash::Hash, T> Cache<K, T> {
658 fn new(capacity: usize) -> Self {
659 Self {
660 values: std::collections::HashMap::new(),
661 order: std::collections::VecDeque::new(),
662 capacity,
663 }
664 }
665
666 fn get_or_insert_with<E>(
667 &mut self,
668 key: K,
669 create: impl FnOnce() -> Result<T, E>,
670 ) -> Result<std::sync::Arc<std::sync::Mutex<T>>, E> {
671 if let Some(value) = self.values.get(&key).cloned() {
672 self.touch(&key);
673 return Ok(value);
674 }
675
676 let value = std::sync::Arc::new(std::sync::Mutex::new(create()?));
677 self.values.insert(key.clone(), std::sync::Arc::clone(&value));
678 self.touch(&key);
679 self.prune();
680 Ok(value)
681 }
682
683 fn touch(&mut self, key: &K) {
684 self.order.retain(|entry| entry != key);
685 self.order.push_back(key.clone());
686 }
687
688 fn prune(&mut self) {
689 let mut remaining = self.order.len();
690 while self.values.len() > self.capacity && remaining > 0 {
691 let key = self.order.pop_front().expect("remaining entries");
692 let idle = self
693 .values
694 .get(&key)
695 .is_some_and(|value| std::sync::Arc::strong_count(value) == 1);
696 if idle {
697 self.values.remove(&key);
698 } else {
699 self.order.push_back(key);
700 }
701 remaining -= 1;
702 }
703 }
704}
705
706#[cfg(all(test, any(target_os = "macos", target_os = "windows")))]
707mod cache_tests {
708 use super::Cache;
709
710 #[test]
711 fn evicts_the_least_recently_used_idle_value() {
712 let mut cache = Cache::new(2);
713
714 let first = cache.get_or_insert_with((1, 1), || Ok::<_, ()>(())).unwrap();
715 drop(first);
716 let second = cache.get_or_insert_with((2, 2), || Ok::<_, ()>(())).unwrap();
717 drop(second);
718
719 let first = cache
720 .get_or_insert_with((1, 1), || Err::<(), _>("cached value was recreated"))
721 .unwrap();
722 drop(first);
723 let third = cache.get_or_insert_with((3, 3), || Ok::<_, ()>(())).unwrap();
724 drop(third);
725
726 assert!(cache.values.contains_key(&(1, 1)));
727 assert!(!cache.values.contains_key(&(2, 2)));
728 assert!(cache.values.contains_key(&(3, 3)));
729 assert_eq!(cache.values.len(), 2);
730 }
731
732 #[test]
733 fn defers_eviction_until_an_active_value_is_released() {
734 let mut cache = Cache::new(1);
735 let first = cache.get_or_insert_with((1, 1), || Ok::<_, ()>(())).unwrap();
736 let second = cache.get_or_insert_with((2, 2), || Ok::<_, ()>(())).unwrap();
737 assert_eq!(cache.values.len(), 2);
738
739 drop(first);
740 cache.prune();
741 assert!(!cache.values.contains_key(&(1, 1)));
742 assert!(cache.values.contains_key(&(2, 2)));
743 assert_eq!(cache.values.len(), 1);
744 drop(second);
745 }
746
747 #[test]
748 fn caches_failure_markers() {
749 let mut attempts = 0;
750 let mut cache = Cache::new(1);
751 let failed = cache
752 .get_or_insert_with(1, || {
753 attempts += 1;
754 Ok::<_, ()>(Err::<(), _>("unsupported"))
755 })
756 .unwrap();
757 drop(failed);
758 let failed = cache
759 .get_or_insert_with(1, || {
760 attempts += 1;
761 Ok::<_, ()>(Ok::<_, &str>(()))
762 })
763 .unwrap();
764
765 assert_eq!(attempts, 1);
766 assert!(failed.lock().unwrap().is_err());
767 }
768}
769
770#[cfg(target_os = "macos")]
771pub mod macos {
772 use std::ffi::c_void;
777 use std::ptr;
778 use std::ptr::NonNull;
779 use std::sync::{LazyLock, Mutex};
780
781 use objc2_core_foundation::{CFDictionary, CFNumber, CFNumberType, CFRetained, CFString};
782 use objc2_core_video::{
783 CVPixelBuffer, CVPixelBufferCreate, CVPixelBufferGetBaseAddressOfPlane, CVPixelBufferGetBytesPerRowOfPlane,
784 CVPixelBufferGetPixelFormatType, CVPixelBufferLockBaseAddress, CVPixelBufferLockFlags, CVPixelBufferPool,
785 CVPixelBufferUnlockBaseAddress, kCVImageBufferYCbCrMatrix_ITU_R_601_4, kCVImageBufferYCbCrMatrix_ITU_R_709_2,
786 kCVImageBufferYCbCrMatrixKey, kCVPixelBufferHeightKey, kCVPixelBufferIOSurfacePropertiesKey,
787 kCVPixelBufferPixelFormatTypeKey, kCVPixelBufferWidthKey, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
788 kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, kCVPixelFormatType_420YpCbCr8Planar,
789 };
790 use objc2_video_toolbox::VTPixelTransferSession;
791
792 use super::{Cache, I420};
793 use crate::{Color, Error};
794
795 const LOCK_READ_ONLY: CVPixelBufferLockFlags = CVPixelBufferLockFlags(1);
797
798 const SCALER_CACHE_CAPACITY: usize = 16;
801
802 type ScalerCache = Mutex<Cache<(u32, u32), Scaler>>;
806 static SCALERS: LazyLock<ScalerCache> = LazyLock::new(|| Mutex::new(Cache::new(SCALER_CACHE_CAPACITY)));
807
808 pub struct PixelBuffer {
811 pub(crate) buffer: CFRetained<CVPixelBuffer>,
812 pub(crate) width: u32,
813 pub(crate) height: u32,
814 }
815
816 unsafe impl Send for PixelBuffer {}
826 unsafe impl Sync for PixelBuffer {}
827
828 impl PixelBuffer {
829 pub fn buffer(&self) -> &CVPixelBuffer {
832 &self.buffer
833 }
834
835 pub fn width(&self) -> u32 {
837 self.width
838 }
839
840 pub fn height(&self) -> u32 {
842 self.height
843 }
844
845 pub(crate) fn new(buffer: CFRetained<CVPixelBuffer>, width: u32, height: u32) -> Self {
846 Self { buffer, width, height }
847 }
848
849 pub(crate) fn resize(&self, width: u32, height: u32) -> Result<Self, Error> {
851 let scaler = {
852 let mut scalers = SCALERS
853 .lock()
854 .map_err(|_| Error::Codec(anyhow::anyhow!("pixel-transfer scaler cache lock poisoned")))?;
855 scalers.get_or_insert_with((width, height), || Scaler::new(width, height))?
856 };
857
858 let result = scaler
859 .lock()
860 .map_err(|_| Error::Codec(anyhow::anyhow!("pixel-transfer scaler lock poisoned")))?
861 .resize(self);
862 drop(scaler);
863 if let Ok(mut scalers) = SCALERS.lock() {
864 scalers.prune();
865 }
866 result
867 }
868
869 fn matrix(&self) -> Color {
877 let inferred = Color::infer(crate::Size::new(self.width, self.height));
878 let Some(value) = (unsafe { self.buffer.attachment(kCVImageBufferYCbCrMatrixKey, ptr::null_mut()) }) else {
880 return inferred;
881 };
882 let Some(name) = value.downcast_ref::<CFString>() else {
883 return inferred;
884 };
885
886 if name == unsafe { kCVImageBufferYCbCrMatrix_ITU_R_709_2 } {
889 Color::Bt709Limited
890 } else if name == unsafe { kCVImageBufferYCbCrMatrix_ITU_R_601_4 } {
891 Color::Bt601Limited
892 } else {
893 inferred
896 }
897 }
898
899 pub(crate) fn color(&self) -> Option<Color> {
903 let format = CVPixelBufferGetPixelFormatType(&self.buffer);
904 let limited = if format == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange {
905 true
906 } else if format == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange {
907 false
908 } else {
909 return None;
910 };
911 Some(self.matrix().with_range(limited))
912 }
913
914 pub(crate) fn download_i420(&self) -> Result<I420, Error> {
921 let format = CVPixelBufferGetPixelFormatType(&self.buffer);
922 if format != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
923 && format != kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
924 {
925 return Err(Error::Codec(anyhow::anyhow!(
926 "cannot download pixel format {format:#x}; expected NV12"
927 )));
928 }
929
930 let color = self.color();
931
932 let (w, h) = (self.width as usize, self.height as usize);
933 let (cw, ch) = (w / 2, h / 2);
934
935 let status = unsafe { CVPixelBufferLockBaseAddress(&self.buffer, LOCK_READ_ONLY) };
936 if status != 0 {
937 return Err(Error::Codec(anyhow::anyhow!(
938 "CVPixelBufferLockBaseAddress failed: {status}"
939 )));
940 }
941 let _guard = UnlockGuard(&self.buffer);
942
943 let mut data = vec![0u8; I420::len(self.width, self.height)];
944 let (luma, chroma) = data.split_at_mut(w * h);
945 let (u_plane, v_plane) = chroma.split_at_mut(cw * ch);
946
947 let y_base = CVPixelBufferGetBaseAddressOfPlane(&self.buffer, 0) as *const u8;
949 let y_stride = CVPixelBufferGetBytesPerRowOfPlane(&self.buffer, 0);
950 for row in 0..h {
951 unsafe {
952 ptr::copy_nonoverlapping(y_base.add(row * y_stride), luma[row * w..].as_mut_ptr(), w);
953 }
954 }
955
956 let uv_base = CVPixelBufferGetBaseAddressOfPlane(&self.buffer, 1) as *const u8;
958 let uv_stride = CVPixelBufferGetBytesPerRowOfPlane(&self.buffer, 1);
959 for row in 0..ch {
960 let src = unsafe { uv_base.add(row * uv_stride) };
961 for col in 0..cw {
962 unsafe {
963 u_plane[row * cw + col] = *src.add(col * 2);
964 v_plane[row * cw + col] = *src.add(col * 2 + 1);
965 }
966 }
967 }
968
969 Ok(I420 {
970 width: self.width,
971 height: self.height,
972 data,
973 color,
974 })
975 }
976 }
977
978 struct Scaler {
980 session: CFRetained<VTPixelTransferSession>,
981 pool: CFRetained<CVPixelBufferPool>,
982 width: u32,
983 height: u32,
984 }
985
986 unsafe impl Send for Scaler {}
990
991 impl Scaler {
992 fn new(width: u32, height: u32) -> Result<Self, Error> {
993 let mut session_ptr: *mut VTPixelTransferSession = std::ptr::null_mut();
994 let status = unsafe {
995 VTPixelTransferSession::create(None, NonNull::new(&mut session_ptr).expect("stack pointer is non-null"))
996 };
997 let session = NonNull::new(session_ptr)
998 .filter(|_| status == 0)
999 .map(|ptr| unsafe { CFRetained::from_raw(ptr) })
1000 .ok_or_else(|| Error::Codec(anyhow::anyhow!("VTPixelTransferSessionCreate failed: {status}")))?;
1001
1002 let attributes = pool_attributes(width, height)?;
1003 let mut pool_ptr: *mut CVPixelBufferPool = std::ptr::null_mut();
1004 let status = unsafe {
1005 CVPixelBufferPool::create(
1006 None,
1007 None,
1008 Some(&attributes),
1009 NonNull::new(&mut pool_ptr).expect("stack pointer is non-null"),
1010 )
1011 };
1012 let pool = NonNull::new(pool_ptr)
1013 .filter(|_| status == 0)
1014 .map(|ptr| unsafe { CFRetained::from_raw(ptr) })
1015 .ok_or_else(|| Error::Codec(anyhow::anyhow!("CVPixelBufferPoolCreate failed: {status}")))?;
1016
1017 Ok(Self {
1018 session,
1019 pool,
1020 width,
1021 height,
1022 })
1023 }
1024
1025 fn resize(&mut self, source: &PixelBuffer) -> Result<PixelBuffer, Error> {
1026 let mut output_ptr: *mut CVPixelBuffer = std::ptr::null_mut();
1027 let status = unsafe {
1028 CVPixelBufferPool::create_pixel_buffer(
1029 None,
1030 &self.pool,
1031 NonNull::new(&mut output_ptr).expect("stack pointer is non-null"),
1032 )
1033 };
1034 let output = NonNull::new(output_ptr)
1035 .filter(|_| status == 0)
1036 .map(|ptr| unsafe { CFRetained::from_raw(ptr) })
1037 .ok_or_else(|| Error::Codec(anyhow::anyhow!("CVPixelBufferPoolCreatePixelBuffer failed: {status}")))?;
1038
1039 let status = unsafe { self.session.transfer_image(&source.buffer, &output) };
1040 if status != 0 {
1041 return Err(Error::Codec(anyhow::anyhow!(
1042 "VTPixelTransferSessionTransferImage failed: {status}"
1043 )));
1044 }
1045
1046 Ok(PixelBuffer::new(output, self.width, self.height))
1047 }
1048 }
1049
1050 fn pool_attributes(width: u32, height: u32) -> Result<CFRetained<CFDictionary>, Error> {
1052 let width =
1053 i32::try_from(width).map_err(|_| Error::Codec(anyhow::anyhow!("pixel-buffer width is too large")))?;
1054 let height =
1055 i32::try_from(height).map_err(|_| Error::Codec(anyhow::anyhow!("pixel-buffer height is too large")))?;
1056 let format = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange as i32;
1057
1058 let width = cf_number(width)?;
1059 let height = cf_number(height)?;
1060 let format = cf_number(format)?;
1061 let iosurface = unsafe {
1062 CFDictionary::new(
1063 None,
1064 std::ptr::null_mut(),
1065 std::ptr::null_mut(),
1066 0,
1067 &objc2_core_foundation::kCFTypeDictionaryKeyCallBacks,
1068 &objc2_core_foundation::kCFTypeDictionaryValueCallBacks,
1069 )
1070 }
1071 .ok_or_else(|| Error::Codec(anyhow::anyhow!("failed to build IOSurface attributes dictionary")))?;
1072
1073 let mut keys = [
1074 (unsafe { kCVPixelBufferPixelFormatTypeKey } as *const CFString).cast::<c_void>(),
1075 (unsafe { kCVPixelBufferWidthKey } as *const CFString).cast::<c_void>(),
1076 (unsafe { kCVPixelBufferHeightKey } as *const CFString).cast::<c_void>(),
1077 (unsafe { kCVPixelBufferIOSurfacePropertiesKey } as *const CFString).cast::<c_void>(),
1078 ];
1079 let mut values = [
1080 (format.as_ref() as *const CFNumber).cast::<c_void>(),
1081 (width.as_ref() as *const CFNumber).cast::<c_void>(),
1082 (height.as_ref() as *const CFNumber).cast::<c_void>(),
1083 (iosurface.as_ref() as *const CFDictionary).cast::<c_void>(),
1084 ];
1085 unsafe {
1086 CFDictionary::new(
1087 None,
1088 keys.as_mut_ptr(),
1089 values.as_mut_ptr(),
1090 4,
1091 &objc2_core_foundation::kCFTypeDictionaryKeyCallBacks,
1092 &objc2_core_foundation::kCFTypeDictionaryValueCallBacks,
1093 )
1094 }
1095 .ok_or_else(|| {
1096 Error::Codec(anyhow::anyhow!(
1097 "failed to build pixel-buffer pool attributes dictionary"
1098 ))
1099 })
1100 }
1101
1102 fn cf_number(value: i32) -> Result<CFRetained<CFNumber>, Error> {
1103 unsafe { CFNumber::new(None, CFNumberType::SInt32Type, (&value as *const i32).cast::<c_void>()) }
1104 .ok_or_else(|| Error::Codec(anyhow::anyhow!("failed to build CFNumber")))
1105 }
1106
1107 struct UnlockGuard<'a>(&'a CVPixelBuffer);
1108
1109 impl Drop for UnlockGuard<'_> {
1110 fn drop(&mut self) {
1111 unsafe { CVPixelBufferUnlockBaseAddress(self.0, LOCK_READ_ONLY) };
1112 }
1113 }
1114
1115 pub(crate) fn upload_i420(frame: &I420) -> Result<CFRetained<CVPixelBuffer>, Error> {
1121 let (w, h) = (frame.width as usize, frame.height as usize);
1122 let (cw, ch) = (w / 2, h / 2);
1123
1124 let mut ptr: *mut CVPixelBuffer = std::ptr::null_mut();
1125 let status = unsafe {
1126 CVPixelBufferCreate(
1127 None,
1128 w,
1129 h,
1130 kCVPixelFormatType_420YpCbCr8Planar,
1131 None,
1132 NonNull::new(&mut ptr).unwrap(),
1133 )
1134 };
1135 let buffer = NonNull::new(ptr)
1136 .filter(|_| status == 0)
1137 .map(|p| unsafe { CFRetained::from_raw(p) })
1138 .ok_or_else(|| Error::Codec(anyhow::anyhow!("CVPixelBufferCreate failed: {status}")))?;
1139
1140 let flags = CVPixelBufferLockFlags(0);
1141 let status = unsafe { CVPixelBufferLockBaseAddress(&buffer, flags) };
1142 if status != 0 {
1143 return Err(Error::Codec(anyhow::anyhow!(
1144 "CVPixelBufferLockBaseAddress failed: {status}"
1145 )));
1146 }
1147
1148 copy_plane(&buffer, 0, frame.y(), w, h);
1149 copy_plane(&buffer, 1, frame.u(), cw, ch);
1150 copy_plane(&buffer, 2, frame.v(), cw, ch);
1151
1152 unsafe { CVPixelBufferUnlockBaseAddress(&buffer, flags) };
1153 Ok(buffer)
1154 }
1155
1156 fn copy_plane(buffer: &CVPixelBuffer, plane: usize, src: &[u8], row_bytes: usize, rows: usize) {
1159 let base = CVPixelBufferGetBaseAddressOfPlane(buffer, plane) as *mut u8;
1160 let stride = CVPixelBufferGetBytesPerRowOfPlane(buffer, plane);
1161 for y in 0..rows {
1162 unsafe {
1163 let dst = base.add(y * stride);
1164 std::ptr::copy_nonoverlapping(src[y * row_bytes..].as_ptr(), dst, row_bytes);
1165 }
1166 }
1167 }
1168}
1169
1170#[cfg(all(target_os = "linux", feature = "nvdec"))]
1171pub mod cuda {
1172 use std::sync::{Arc, OnceLock};
1176
1177 use cudarc::driver::{CudaContext, CudaFunction, LaunchConfig, PushKernelArg, result};
1178
1179 use super::I420;
1180 use crate::Error;
1181
1182 const RESIZE_PTX: &str = include_str!("frame/nv12_resize.ptx");
1185
1186 struct Kernels {
1189 luma: CudaFunction,
1190 chroma: CudaFunction,
1191 }
1192
1193 fn kernels(ctx: &Arc<CudaContext>) -> Result<&'static Kernels, Error> {
1194 static KERNELS: OnceLock<Result<Kernels, String>> = OnceLock::new();
1195 KERNELS
1196 .get_or_init(|| {
1197 let module = ctx
1198 .load_module(cudarc::nvrtc::Ptx::from_src(RESIZE_PTX))
1199 .map_err(|e| format!("load nv12_resize PTX: {e:?}"))?;
1200 Ok(Kernels {
1201 luma: module
1202 .load_function("resize_luma")
1203 .map_err(|e| format!("load resize_luma: {e:?}"))?,
1204 chroma: module
1205 .load_function("resize_chroma")
1206 .map_err(|e| format!("load resize_chroma: {e:?}"))?,
1207 })
1208 })
1209 .as_ref()
1210 .map_err(|e| Error::Codec(anyhow::anyhow!("CUDA resize unavailable: {e}")))
1211 }
1212
1213 struct Buffer {
1218 ctx: Arc<CudaContext>,
1219 ptr: cudarc::driver::sys::CUdeviceptr,
1220 len: usize,
1221 }
1222
1223 impl Drop for Buffer {
1224 fn drop(&mut self) {
1225 if self.ctx.bind_to_thread().is_ok() {
1227 let _ = unsafe { result::free_sync(self.ptr) };
1229 }
1230 }
1231 }
1232
1233 #[derive(Clone)]
1241 pub struct Frame {
1242 buf: Arc<Buffer>,
1243 pub(crate) width: u32,
1244 pub(crate) height: u32,
1245 pub(crate) pitch: u32,
1247 }
1248
1249 impl Frame {
1250 pub(crate) fn alloc(ctx: &Arc<CudaContext>, width: u32, height: u32, pitch: u32) -> Result<Self, Error> {
1253 debug_assert!(pitch >= width && width.is_multiple_of(2) && height.is_multiple_of(2));
1254 let len = pitch as usize * height as usize * 3 / 2;
1255 ctx.bind_to_thread()
1256 .map_err(|e| Error::Codec(anyhow::anyhow!("CUDA bind: {e:?}")))?;
1257 let ptr = unsafe { result::malloc_sync(len) }
1260 .map_err(|e| Error::Codec(anyhow::anyhow!("CUDA alloc of {len} bytes: {e:?}")))?;
1261 Ok(Self {
1262 buf: Arc::new(Buffer {
1263 ctx: ctx.clone(),
1264 ptr,
1265 len,
1266 }),
1267 width,
1268 height,
1269 pitch,
1270 })
1271 }
1272
1273 pub(crate) fn device_ptr(&self) -> u64 {
1276 self.buf.ptr
1277 }
1278
1279 pub(crate) fn download_i420(&self) -> Result<I420, Error> {
1282 self.buf
1283 .ctx
1284 .bind_to_thread()
1285 .map_err(|e| Error::Codec(anyhow::anyhow!("CUDA bind: {e:?}")))?;
1286 let mut host = vec![0u8; self.buf.len];
1287 unsafe { result::memcpy_dtoh_sync(&mut host, self.buf.ptr) }
1290 .map_err(|e| Error::Codec(anyhow::anyhow!("CUDA download: {e:?}")))?;
1291
1292 let (w, h) = (self.width as usize, self.height as usize);
1293 let (cw, ch) = (w / 2, h / 2);
1294 let pitch = self.pitch as usize;
1295
1296 let mut data = vec![0u8; I420::len(self.width, self.height)];
1297 let (luma, chroma) = data.split_at_mut(w * h);
1298 let (u_dst, v_dst) = chroma.split_at_mut(cw * ch);
1299
1300 for row in 0..h {
1301 luma[row * w..row * w + w].copy_from_slice(&host[row * pitch..row * pitch + w]);
1302 }
1303 let uv_base = pitch * h;
1304 for row in 0..ch {
1305 let src = &host[uv_base + row * pitch..uv_base + row * pitch + w];
1306 for col in 0..cw {
1307 u_dst[row * cw + col] = src[col * 2];
1308 v_dst[row * cw + col] = src[col * 2 + 1];
1309 }
1310 }
1311
1312 Ok(I420 {
1313 width: self.width,
1314 height: self.height,
1315 data,
1316 color: None,
1319 })
1320 }
1321
1322 pub(crate) fn resize(&self, width: u32, height: u32) -> Result<Self, Error> {
1326 let ctx = &self.buf.ctx;
1327 let kernels = kernels(ctx)?;
1328
1329 let pitch = width.next_multiple_of(256);
1332 let dst = Self::alloc(ctx, width, height, pitch)?;
1333
1334 let stream = ctx.default_stream();
1335 let block = (16u32, 16, 1);
1336 let grid = |w: u32, h: u32| (w.div_ceil(16), h.div_ceil(16), 1);
1337 let launch_err = |plane: &str, e| Error::Codec(anyhow::anyhow!("CUDA resize {plane}: {e:?}"));
1338
1339 unsafe {
1345 stream
1346 .launch_builder(&kernels.luma)
1347 .arg(&self.buf.ptr)
1348 .arg(&self.pitch)
1349 .arg(&self.width)
1350 .arg(&self.height)
1351 .arg(&dst.buf.ptr)
1352 .arg(&pitch)
1353 .arg(&width)
1354 .arg(&height)
1355 .launch(LaunchConfig {
1356 grid_dim: grid(width, height),
1357 block_dim: block,
1358 shared_mem_bytes: 0,
1359 })
1360 }
1361 .map_err(|e| launch_err("luma", e))?;
1362
1363 let src_uv = self.buf.ptr + u64::from(self.pitch) * u64::from(self.height);
1366 let dst_uv = dst.buf.ptr + u64::from(pitch) * u64::from(height);
1367 let (src_pw, src_ph) = (self.width / 2, self.height / 2);
1368 let (dst_pw, dst_ph) = (width / 2, height / 2);
1369 unsafe {
1371 stream
1372 .launch_builder(&kernels.chroma)
1373 .arg(&src_uv)
1374 .arg(&self.pitch)
1375 .arg(&src_pw)
1376 .arg(&src_ph)
1377 .arg(&dst_uv)
1378 .arg(&pitch)
1379 .arg(&dst_pw)
1380 .arg(&dst_ph)
1381 .launch(LaunchConfig {
1382 grid_dim: grid(dst_pw, dst_ph),
1383 block_dim: block,
1384 shared_mem_bytes: 0,
1385 })
1386 }
1387 .map_err(|e| launch_err("chroma", e))?;
1388
1389 stream
1392 .synchronize()
1393 .map_err(|e| Error::Codec(anyhow::anyhow!("CUDA resize sync: {e:?}")))?;
1394 Ok(dst)
1395 }
1396 }
1397}
1398
1399#[cfg(target_os = "windows")]
1400pub mod d3d11 {
1401 use std::ffi::c_void;
1405 use std::ptr;
1406 use std::sync::{LazyLock, Mutex};
1407
1408 use windows::Win32::Foundation::{HMODULE, RECT};
1409 use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_HARDWARE;
1410 use windows::Win32::Graphics::Direct3D10::ID3D10Multithread;
1411 use windows::Win32::Graphics::Direct3D11::{
1412 D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE, D3D11_BIND_VIDEO_ENCODER, D3D11_BOX,
1413 D3D11_CPU_ACCESS_READ, D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_CREATE_DEVICE_VIDEO_SUPPORT,
1414 D3D11_FORMAT_SUPPORT, D3D11_FORMAT_SUPPORT_RENDER_TARGET, D3D11_FORMAT_SUPPORT_SHADER_SAMPLE,
1415 D3D11_FORMAT_SUPPORT_VIDEO_ENCODER, D3D11_MAP_READ, D3D11_MAPPED_SUBRESOURCE, D3D11_SDK_VERSION,
1416 D3D11_TEX2D_VPIV, D3D11_TEX2D_VPOV, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, D3D11_USAGE_STAGING,
1417 D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE, D3D11_VIDEO_PROCESSOR_COLOR_SPACE, D3D11_VIDEO_PROCESSOR_CONTENT_DESC,
1418 D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC, D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC_0,
1419 D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC, D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC_0, D3D11_VIDEO_PROCESSOR_STREAM,
1420 D3D11_VIDEO_USAGE_PLAYBACK_NORMAL, D3D11_VPIV_DIMENSION_TEXTURE2D, D3D11_VPOV_DIMENSION_TEXTURE2D,
1421 D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D, ID3D11VideoContext, ID3D11VideoDevice,
1422 ID3D11VideoProcessor, ID3D11VideoProcessorEnumerator, ID3D11VideoProcessorInputView,
1423 ID3D11VideoProcessorOutputView,
1424 };
1425 #[cfg(test)]
1426 use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_NV12;
1427 use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT, DXGI_RATIONAL, DXGI_SAMPLE_DESC};
1428 use windows::Win32::Media::MediaFoundation::{IMFDXGIBuffer, IMFSample};
1429 use windows::core::Interface;
1430
1431 use super::{Cache, I420};
1432 use crate::{Error, Size};
1433
1434 fn err(ctx: &str, e: windows::core::Error) -> Error {
1435 Error::Codec(anyhow::anyhow!("{ctx}: {e}"))
1436 }
1437
1438 pub(crate) fn create_device() -> Result<ID3D11Device, Error> {
1443 let mut device: Option<ID3D11Device> = None;
1444 unsafe {
1445 D3D11CreateDevice(
1446 None,
1447 D3D_DRIVER_TYPE_HARDWARE,
1448 HMODULE::default(),
1449 D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_VIDEO_SUPPORT,
1450 None,
1451 D3D11_SDK_VERSION,
1452 Some(&mut device),
1453 None,
1454 None,
1455 )
1456 .map_err(|e| err("D3D11CreateDevice", e))?;
1457 }
1458 let device = device.ok_or_else(|| Error::Codec(anyhow::anyhow!("D3D11CreateDevice returned null")))?;
1459
1460 let multithread = device
1461 .cast::<ID3D10Multithread>()
1462 .map_err(|e| err("query ID3D10Multithread", e))?;
1463 unsafe {
1464 let _ = multithread.SetMultithreadProtected(true);
1465 }
1466 Ok(device)
1467 }
1468
1469 pub struct Texture {
1475 pub(crate) device: ID3D11Device,
1476 pub(crate) texture: ID3D11Texture2D,
1477 pub(crate) width: u32,
1478 pub(crate) height: u32,
1479 }
1480
1481 impl Texture {
1482 pub(crate) fn copy_from_sample(
1505 device: &ID3D11Device,
1506 sample: &IMFSample,
1507 width: u32,
1508 height: u32,
1509 ) -> Result<Self, Error> {
1510 let (source, subresource) = resolve(sample)?;
1511
1512 let mut desc = D3D11_TEXTURE2D_DESC::default();
1514 unsafe { source.GetDesc(&mut desc) };
1515 let texture = alloc(device, width, height, desc.Format)?;
1516
1517 let region = D3D11_BOX {
1520 left: 0,
1521 top: 0,
1522 front: 0,
1523 right: width,
1524 bottom: height,
1525 back: 1,
1526 };
1527 let context = unsafe { device.GetImmediateContext() }.map_err(|e| err("GetImmediateContext", e))?;
1528 unsafe {
1529 context.CopySubresourceRegion(&texture, 0, 0, 0, 0, &source, subresource, Some(®ion));
1530 }
1531
1532 Ok(Self {
1533 device: device.clone(),
1534 texture,
1535 width,
1536 height,
1537 })
1538 }
1539
1540 pub fn texture(&self) -> &ID3D11Texture2D {
1549 &self.texture
1550 }
1551
1552 pub fn device(&self) -> &ID3D11Device {
1555 &self.device
1556 }
1557
1558 pub fn width(&self) -> u32 {
1560 self.width
1561 }
1562
1563 pub fn height(&self) -> u32 {
1565 self.height
1566 }
1567
1568 pub(crate) fn download_i420(&self) -> Result<I420, Error> {
1572 let context = unsafe { self.device.GetImmediateContext() }.map_err(|e| err("GetImmediateContext", e))?;
1573
1574 let mut desc = D3D11_TEXTURE2D_DESC::default();
1576 unsafe { self.texture.GetDesc(&mut desc) };
1577 desc.ArraySize = 1;
1578 desc.MipLevels = 1;
1579 desc.Usage = D3D11_USAGE_STAGING;
1580 desc.BindFlags = 0;
1581 desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ.0 as u32;
1582 desc.MiscFlags = 0;
1583
1584 let mut staging: Option<ID3D11Texture2D> = None;
1585 unsafe {
1586 self.device
1587 .CreateTexture2D(&desc, None, Some(&mut staging))
1588 .map_err(|e| err("CreateTexture2D (staging)", e))?;
1589 }
1590 let staging = staging.ok_or_else(|| Error::Codec(anyhow::anyhow!("CreateTexture2D returned null")))?;
1591
1592 unsafe {
1593 context.CopySubresourceRegion(&staging, 0, 0, 0, 0, &self.texture, 0, None);
1594 }
1595
1596 let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
1597 unsafe {
1598 context
1599 .Map(&staging, 0, D3D11_MAP_READ, 0, Some(&mut mapped))
1600 .map_err(|e| err("Map (staging)", e))?;
1601 }
1602 let _guard = UnmapGuard {
1603 context: &context,
1604 resource: &staging,
1605 };
1606
1607 let (w, h) = (self.width as usize, self.height as usize);
1608 let (cw, ch) = (w / 2, h / 2);
1609 let pitch = mapped.RowPitch as usize;
1610 let base = mapped.pData as *const u8;
1611 let tex_height = desc.Height as usize;
1617
1618 let mut data = vec![0u8; I420::len(self.width, self.height)];
1619 let (luma, chroma) = data.split_at_mut(w * h);
1620 let (u_plane, v_plane) = chroma.split_at_mut(cw * ch);
1621
1622 for row in 0..h {
1624 unsafe {
1625 ptr::copy_nonoverlapping(base.add(row * pitch), luma[row * w..].as_mut_ptr(), w);
1626 }
1627 }
1628 let uv_base = unsafe { base.add(pitch * tex_height) };
1630 for row in 0..ch {
1631 let src = unsafe { uv_base.add(row * pitch) };
1632 for col in 0..cw {
1633 unsafe {
1634 u_plane[row * cw + col] = *src.add(col * 2);
1635 v_plane[row * cw + col] = *src.add(col * 2 + 1);
1636 }
1637 }
1638 }
1639
1640 Ok(I420 {
1641 width: self.width,
1642 height: self.height,
1643 data,
1644 color: None,
1647 })
1648 }
1649
1650 pub(crate) fn resize(&self, width: u32, height: u32) -> Result<Self, Error> {
1660 let source = Size::new(self.width, self.height);
1661 let target = Size::new(width, height);
1662 let key = ScalerKey::new(&self.device, source, target);
1663
1664 let scaler = {
1665 let mut scalers = SCALERS
1666 .lock()
1667 .map_err(|_| Error::Codec(anyhow::anyhow!("video-processor cache lock poisoned")))?;
1668 scalers
1669 .get_or_insert_with(key, || {
1670 Ok::<_, std::convert::Infallible>(ScalerState::discover(&self.device, source, target))
1671 })
1672 .expect("scaler discovery is infallible")
1673 };
1674 let mut state = scaler
1675 .lock()
1676 .map_err(|_| Error::Codec(anyhow::anyhow!("video processor lock poisoned")))?;
1677 let result = match &*state {
1678 ScalerState::Ready(scaler) => scaler.scale(&self.texture),
1679 ScalerState::Unsupported { reason, .. } => {
1680 return Err(Error::Codec(anyhow::anyhow!("GPU resize is unsupported: {reason}")));
1681 }
1682 };
1683 let texture = match result {
1684 Ok(texture) => texture,
1685 Err(ScaleError::Unsupported(err)) => {
1686 *state = ScalerState::Unsupported {
1687 _device: self.device.clone(),
1688 reason: err.to_string(),
1689 };
1690 return Err(err);
1691 }
1692 Err(ScaleError::Transient(err)) => return Err(err),
1693 };
1694 drop(state);
1695 drop(scaler);
1696 if let Ok(mut scalers) = SCALERS.lock() {
1697 scalers.prune();
1698 }
1699
1700 Ok(Self {
1701 device: self.device.clone(),
1702 texture,
1703 width,
1704 height,
1705 })
1706 }
1707 }
1708
1709 const SCALER_CACHE_CAPACITY: usize = 16;
1712
1713 static SCALERS: LazyLock<Mutex<Cache<ScalerKey, ScalerState>>> =
1718 LazyLock::new(|| Mutex::new(Cache::new(SCALER_CACHE_CAPACITY)));
1719
1720 enum ScalerState {
1722 Ready(Scaler),
1723 Unsupported {
1724 _device: ID3D11Device,
1726 reason: String,
1727 },
1728 }
1729
1730 impl ScalerState {
1731 fn discover(device: &ID3D11Device, source: Size, target: Size) -> Self {
1732 match Scaler::new(device, source, target) {
1733 Ok(scaler) => Self::Ready(scaler),
1734 Err(err) => Self::Unsupported {
1735 _device: device.clone(),
1736 reason: err.to_string(),
1737 },
1738 }
1739 }
1740 }
1741
1742 #[derive(Clone, PartialEq, Eq, Hash)]
1749 struct ScalerKey {
1750 device: usize,
1751 source: Size,
1752 target: Size,
1753 }
1754
1755 impl ScalerKey {
1756 fn new(device: &ID3D11Device, source: Size, target: Size) -> Self {
1757 Self {
1758 device: device.as_raw() as usize,
1759 source,
1760 target,
1761 }
1762 }
1763 }
1764
1765 struct Scaler {
1768 device: ID3D11Device,
1770 video: ID3D11VideoDevice,
1771 context: ID3D11VideoContext,
1772 enumerator: ID3D11VideoProcessorEnumerator,
1773 processor: ID3D11VideoProcessor,
1774 target: Size,
1775 }
1776
1777 enum ScaleError {
1779 Unsupported(Error),
1780 Transient(Error),
1781 }
1782
1783 impl Scaler {
1784 fn new(device: &ID3D11Device, source: Size, target: Size) -> Result<Self, Error> {
1785 let video = device
1786 .cast::<ID3D11VideoDevice>()
1787 .map_err(|e| err("query ID3D11VideoDevice", e))?;
1788 let immediate = unsafe { device.GetImmediateContext() }.map_err(|e| err("GetImmediateContext", e))?;
1789 let context = immediate
1790 .cast::<ID3D11VideoContext>()
1791 .map_err(|e| err("query ID3D11VideoContext", e))?;
1792
1793 let rate = DXGI_RATIONAL {
1796 Numerator: 30,
1797 Denominator: 1,
1798 };
1799 let desc = D3D11_VIDEO_PROCESSOR_CONTENT_DESC {
1800 InputFrameFormat: D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE,
1801 InputFrameRate: rate,
1802 InputWidth: source.width,
1803 InputHeight: source.height,
1804 OutputFrameRate: rate,
1805 OutputWidth: target.width,
1806 OutputHeight: target.height,
1807 Usage: D3D11_VIDEO_USAGE_PLAYBACK_NORMAL,
1808 };
1809
1810 let enumerator = unsafe { video.CreateVideoProcessorEnumerator(&desc) }
1811 .map_err(|e| err("CreateVideoProcessorEnumerator", e))?;
1812 let processor =
1813 unsafe { video.CreateVideoProcessor(&enumerator, 0) }.map_err(|e| err("CreateVideoProcessor", e))?;
1814
1815 let full = RECT {
1816 left: 0,
1817 top: 0,
1818 right: source.width as i32,
1819 bottom: source.height as i32,
1820 };
1821 let scaled = RECT {
1822 left: 0,
1823 top: 0,
1824 right: target.width as i32,
1825 bottom: target.height as i32,
1826 };
1827 unsafe {
1828 context.VideoProcessorSetStreamFrameFormat(&processor, 0, D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE);
1829 context.VideoProcessorSetStreamSourceRect(&processor, 0, true, Some(&full));
1831 context.VideoProcessorSetStreamDestRect(&processor, 0, true, Some(&scaled));
1832 context.VideoProcessorSetStreamAutoProcessingMode(&processor, 0, false);
1836 let space = D3D11_VIDEO_PROCESSOR_COLOR_SPACE::default();
1840 context.VideoProcessorSetStreamColorSpace(&processor, 0, &space);
1841 context.VideoProcessorSetOutputColorSpace(&processor, &space);
1842 }
1843
1844 Ok(Self {
1845 device: device.clone(),
1846 video,
1847 context,
1848 enumerator,
1849 processor,
1850 target,
1851 })
1852 }
1853
1854 fn scale(&self, source: &ID3D11Texture2D) -> Result<ID3D11Texture2D, ScaleError> {
1856 let mut desc = D3D11_TEXTURE2D_DESC::default();
1857 unsafe { source.GetDesc(&mut desc) };
1858 let output = alloc(&self.device, self.target.width, self.target.height, desc.Format)
1859 .map_err(ScaleError::Transient)?;
1860
1861 let input_desc = D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC {
1862 FourCC: 0,
1863 ViewDimension: D3D11_VPIV_DIMENSION_TEXTURE2D,
1864 Anonymous: D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC_0 {
1865 Texture2D: D3D11_TEX2D_VPIV {
1866 MipSlice: 0,
1867 ArraySlice: 0,
1868 },
1869 },
1870 };
1871 let mut input: Option<ID3D11VideoProcessorInputView> = None;
1872 unsafe {
1873 self.video
1874 .CreateVideoProcessorInputView(source, &self.enumerator, &input_desc, Some(&mut input))
1875 .map_err(|e| ScaleError::Unsupported(err("CreateVideoProcessorInputView", e)))?;
1876 }
1877 let input =
1878 input.ok_or_else(|| ScaleError::Unsupported(Error::Codec(anyhow::anyhow!("input view is null"))))?;
1879
1880 let output_desc = D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC {
1881 ViewDimension: D3D11_VPOV_DIMENSION_TEXTURE2D,
1882 Anonymous: D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC_0 {
1883 Texture2D: D3D11_TEX2D_VPOV { MipSlice: 0 },
1884 },
1885 };
1886 let mut view: Option<ID3D11VideoProcessorOutputView> = None;
1887 unsafe {
1888 self.video
1889 .CreateVideoProcessorOutputView(&output, &self.enumerator, &output_desc, Some(&mut view))
1890 .map_err(|e| ScaleError::Unsupported(err("CreateVideoProcessorOutputView", e)))?;
1891 }
1892 let view =
1893 view.ok_or_else(|| ScaleError::Unsupported(Error::Codec(anyhow::anyhow!("output view is null"))))?;
1894
1895 let streams = [D3D11_VIDEO_PROCESSOR_STREAM {
1896 Enable: true.into(),
1897 OutputIndex: 0,
1898 InputFrameOrField: 0,
1899 PastFrames: 0,
1900 FutureFrames: 0,
1901 ppPastSurfaces: ptr::null_mut(),
1902 pInputSurface: std::mem::ManuallyDrop::new(Some(input)),
1903 ppFutureSurfaces: ptr::null_mut(),
1904 ppPastSurfacesRight: ptr::null_mut(),
1905 pInputSurfaceRight: std::mem::ManuallyDrop::new(None),
1906 ppFutureSurfacesRight: ptr::null_mut(),
1907 }];
1908 let result = unsafe { self.context.VideoProcessorBlt(&self.processor, &view, 0, &streams) };
1909 drop(std::mem::ManuallyDrop::into_inner(unsafe {
1913 ptr::read(&streams[0].pInputSurface)
1914 }));
1915 result.map_err(|e| ScaleError::Transient(err("VideoProcessorBlt", e)))?;
1916
1917 Ok(output)
1918 }
1919 }
1920
1921 fn alloc(device: &ID3D11Device, width: u32, height: u32, format: DXGI_FORMAT) -> Result<ID3D11Texture2D, Error> {
1924 let desc = D3D11_TEXTURE2D_DESC {
1925 Width: width,
1926 Height: height,
1927 MipLevels: 1,
1928 ArraySize: 1,
1929 Format: format,
1930 SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 },
1931 Usage: D3D11_USAGE_DEFAULT,
1932 BindFlags: bind_flags(device, format),
1933 CPUAccessFlags: 0,
1934 MiscFlags: 0,
1935 };
1936
1937 let mut texture: Option<ID3D11Texture2D> = None;
1938 unsafe {
1939 device
1940 .CreateTexture2D(&desc, None, Some(&mut texture))
1941 .map_err(|e| err("CreateTexture2D", e))?;
1942 }
1943 texture.ok_or_else(|| Error::Codec(anyhow::anyhow!("CreateTexture2D returned null")))
1944 }
1945
1946 #[cfg(test)]
1950 pub(crate) fn upload_i420(device: &ID3D11Device, frame: &I420) -> Result<Texture, Error> {
1951 let (width, height) = (frame.width, frame.height);
1952 let texture = alloc(device, width, height, DXGI_FORMAT_NV12)?;
1953
1954 let (w, h) = (width as usize, height as usize);
1955 let mut nv12 = vec![0u8; w * h * 3 / 2];
1956 let (luma, chroma) = nv12.split_at_mut(w * h);
1957 luma.copy_from_slice(frame.y());
1958 super::interleave_uv(frame.u(), frame.v(), chroma);
1959
1960 let context = unsafe { device.GetImmediateContext() }.map_err(|e| err("GetImmediateContext", e))?;
1961 unsafe {
1964 context.UpdateSubresource(
1965 &texture,
1966 0,
1967 None,
1968 nv12.as_ptr().cast::<c_void>(),
1969 width,
1970 nv12.len() as u32,
1971 );
1972 }
1973
1974 Ok(Texture {
1975 device: device.clone(),
1976 texture,
1977 width,
1978 height,
1979 })
1980 }
1981
1982 fn resolve(sample: &IMFSample) -> Result<(ID3D11Texture2D, u32), Error> {
1985 let buffer = unsafe { sample.GetBufferByIndex(0) }.map_err(|e| err("get sample buffer", e))?;
1986 let dxgi = buffer
1987 .cast::<IMFDXGIBuffer>()
1988 .map_err(|e| err("sample buffer is not a DXGI surface", e))?;
1989
1990 let mut raw: *mut c_void = ptr::null_mut();
1992 unsafe {
1993 dxgi.GetResource(&ID3D11Texture2D::IID, &mut raw)
1994 .map_err(|e| err("get DXGI resource", e))?;
1995 }
1996 let texture = unsafe { ID3D11Texture2D::from_raw(raw) };
1997 let subresource = unsafe { dxgi.GetSubresourceIndex() }.map_err(|e| err("get subresource index", e))?;
1998 Ok((texture, subresource))
1999 }
2000
2001 fn bind_flags(device: &ID3D11Device, format: DXGI_FORMAT) -> u32 {
2011 let support = unsafe { device.CheckFormatSupport(format) }.unwrap_or_default();
2012 let supports = |flag: D3D11_FORMAT_SUPPORT| support & flag.0 as u32 != 0;
2013
2014 let mut flags = 0;
2015 if supports(D3D11_FORMAT_SUPPORT_SHADER_SAMPLE) {
2016 flags |= D3D11_BIND_SHADER_RESOURCE.0 as u32;
2017 }
2018 if supports(D3D11_FORMAT_SUPPORT_RENDER_TARGET) {
2019 flags |= D3D11_BIND_RENDER_TARGET.0 as u32;
2020 }
2021 if supports(D3D11_FORMAT_SUPPORT_VIDEO_ENCODER) {
2022 flags |= D3D11_BIND_VIDEO_ENCODER.0 as u32;
2023 }
2024 flags
2025 }
2026
2027 #[cfg(test)]
2029 pub(crate) fn supports_nv12_render_target(device: &ID3D11Device) -> bool {
2030 let support = unsafe { device.CheckFormatSupport(DXGI_FORMAT_NV12) }.unwrap_or_default();
2031 support & D3D11_FORMAT_SUPPORT_RENDER_TARGET.0 as u32 != 0
2032 }
2033
2034 struct UnmapGuard<'a> {
2035 context: &'a ID3D11DeviceContext,
2036 resource: &'a ID3D11Texture2D,
2037 }
2038
2039 impl Drop for UnmapGuard<'_> {
2040 fn drop(&mut self) {
2041 unsafe { self.context.Unmap(self.resource, 0) };
2042 }
2043 }
2044}
2045
2046#[cfg(test)]
2047mod tests {
2048 #[test]
2057 fn only_a_real_color_conversion_labels_its_output() {
2058 use super::I420;
2059 use crate::{Color, Size};
2060
2061 let size = Size::new(64, 64);
2062 let rgba = vec![0u8; size.pixels() as usize * 4];
2063 let converted = I420::from_rgba(&rgba, size.width * 4, size.width, size.height).expect("rgba to i420");
2064 assert_eq!(
2065 converted.color(),
2066 Some(Color::Bt601Limited),
2067 "an RGB conversion knows the matrix it used"
2068 );
2069
2070 let resized = converted.resize(32, 32).expect("resize");
2072 assert_eq!(resized.color(), Some(Color::Bt601Limited), "resize preserves the space");
2073
2074 let raw = I420::new(64, 64, vec![0; I420::len(64, 64)]).expect("i420");
2076 assert_eq!(raw.color(), None);
2077 assert_eq!(raw.with_color(Color::Bt709Full).color(), Some(Color::Bt709Full));
2078 }
2079
2080 #[cfg(target_os = "linux")]
2085 #[test]
2086 fn yuyv_capture_keeps_its_color_space_open() {
2087 let (width, height) = (1280, 720);
2088 let yuyv = vec![0u8; width as usize * height as usize * 2];
2090 let frame = super::I420::from_yuyv(&yuyv, width * 2, width, height).expect("yuyv to i420");
2091 assert_eq!(frame.color(), None, "a chroma resample names no color space");
2092 }
2093
2094 #[test]
2098 fn i420_new_rejects_a_short_buffer() {
2099 use super::I420;
2100
2101 assert!(I420::new(64, 32, vec![0; I420::len(64, 32)]).is_ok());
2102 assert!(I420::new(64, 32, vec![0; I420::len(64, 32) - 1]).is_err());
2103 assert!(I420::new(64, 32, Vec::new()).is_err());
2104 assert!(I420::new(63, 32, vec![0; I420::len(63, 32)]).is_err());
2106 assert!(I420::new(0, 32, Vec::new()).is_err());
2107 }
2108
2109 use super::{Frame, I420, Surface};
2110 use crate::Size;
2111
2112 #[test]
2115 fn surface_rgba_rejects_a_mismatched_buffer() {
2116 let ok = vec![0x80u8; 64 * 32 * 4];
2117 assert!(Surface::rgba(&ok, Size::new(64, 32)).is_ok());
2118 assert!(Surface::rgba(&ok[..ok.len() - 4], Size::new(64, 32)).is_err());
2119 assert!(Surface::rgba(&ok, Size::new(32, 32)).is_err());
2120 assert!(Surface::rgba(&ok, Size::new(0, 32)).is_err());
2121 }
2122
2123 #[test]
2131 fn rgb_conversion_follows_the_size_heuristic() {
2132 use yuv::{YuvPlanarImage, yuv420_to_rgba};
2133
2134 use crate::Color;
2135
2136 let red = |size: Size| {
2137 let rgba = [255u8, 0, 0, 255].repeat(size.pixels() as usize);
2138 I420::from_rgba(&rgba, size.width * 4, size.width, size.height).unwrap()
2139 };
2140
2141 let decode = |i420: &I420| {
2144 let (w, h) = (i420.width, i420.height);
2145 let (range, matrix) = Color::infer(Size::new(w, h)).yuv();
2146 let planar = YuvPlanarImage {
2147 y_plane: i420.y(),
2148 y_stride: w,
2149 u_plane: i420.u(),
2150 u_stride: w / 2,
2151 v_plane: i420.v(),
2152 v_stride: w / 2,
2153 width: w,
2154 height: h,
2155 };
2156 let mut rgba = vec![0u8; (w * h * 4) as usize];
2157 yuv420_to_rgba(&planar, &mut rgba, w * 4, range, matrix).unwrap();
2158 let px = ((h / 2 * w + w / 2) * 4) as usize;
2159 [rgba[px], rgba[px + 1], rgba[px + 2]]
2160 };
2161
2162 for (size, expected) in [
2163 (Size::new(720, 480), Color::Bt601Limited),
2164 (Size::new(720, 576), Color::Bt601Limited),
2165 (Size::new(1280, 720), Color::Bt709Limited),
2166 (Size::new(1920, 1080), Color::Bt709Limited),
2167 ] {
2168 let i420 = red(size);
2169 assert_eq!(i420.color(), Some(expected), "{size} reported color");
2170
2171 let rgb = decode(&i420);
2174 assert!(
2175 rgb[1] <= 2 && rgb[2] <= 2,
2176 "{size} red came back as {rgb:?}, so the matrix and the label disagree"
2177 );
2178 }
2179 }
2180
2181 #[test]
2184 fn frame_size_follows_the_surface() {
2185 let rgba = vec![0x80u8; 64 * 32 * 4];
2186 let surface = Surface::rgba(&rgba, Size::new(64, 32)).unwrap();
2187
2188 let frame = Frame::new(surface, moq_net::Timestamp::from_micros(1234).unwrap());
2189 assert_eq!(frame.size(), Size::new(64, 32));
2190
2191 let scaled = frame.resize(Size::new(32, 16)).unwrap();
2192 assert_eq!(scaled.size(), Size::new(32, 16));
2193 assert_eq!(scaled.timestamp, frame.timestamp);
2194 }
2195
2196 #[cfg(target_os = "macos")]
2200 #[test]
2201 fn into_pixel_buffer_uploads_a_cpu_frame() {
2202 use objc2_core_video::{CVPixelBufferGetHeight, CVPixelBufferGetWidth};
2203
2204 let i420 = I420::new(64, 32, vec![0x80; I420::len(64, 32)]).unwrap();
2205 let frame = Frame::new(Surface::I420(i420), moq_net::Timestamp::from_micros(0).unwrap());
2206
2207 let buffer = frame.surface.into_pixel_buffer().expect("upload a CPU frame");
2208 assert_eq!(CVPixelBufferGetWidth(&buffer), 64);
2209 assert_eq!(CVPixelBufferGetHeight(&buffer), 32);
2210 }
2211
2212 fn gradient_i420(width: u32, height: u32) -> I420 {
2215 let (w, h) = (width as usize, height as usize);
2216 let (cw, ch) = (w / 2, h / 2);
2217 let mut data = vec![0u8; I420::len(width, height)];
2218 let (y, chroma) = data.split_at_mut(w * h);
2219 let (u, v) = chroma.split_at_mut(cw * ch);
2220 for row in 0..h {
2221 for col in 0..w {
2222 y[row * w + col] = ((col * 255) / w) as u8;
2223 }
2224 }
2225 for row in 0..ch {
2226 for col in 0..cw {
2227 u[row * cw + col] = ((row * 255) / ch) as u8;
2228 v[row * cw + col] = (((row + col) * 255) / (ch + cw)) as u8;
2229 }
2230 }
2231 I420 {
2232 width,
2233 height,
2234 data,
2235 color: None,
2236 }
2237 }
2238
2239 fn mae(a: &[u8], b: &[u8]) -> u64 {
2241 assert_eq!(a.len(), b.len());
2242 a.iter().zip(b).map(|(x, y)| x.abs_diff(*y) as u64).sum::<u64>() / a.len() as u64
2243 }
2244
2245 #[test]
2248 fn i420_resize_follows_gradients() {
2249 let src = gradient_i420(320, 240);
2250 let dst = src.resize(128, 96).unwrap();
2251 assert_eq!((dst.width, dst.height), (128, 96));
2252
2253 let expected = gradient_i420(128, 96);
2255 assert!(mae(dst.y(), expected.y()) < 4, "luma ramp drifted");
2256 assert!(mae(dst.u(), expected.u()) < 4, "u ramp drifted");
2257 assert!(mae(dst.v(), expected.v()) < 4, "v ramp drifted");
2258 }
2259
2260 #[cfg(target_os = "macos")]
2263 #[test]
2264 fn pixel_buffer_resize_matches_cpu() {
2265 let src_i420 = gradient_i420(320, 240);
2266 let src = Surface::PixelBuffer(nv12_surface(&src_i420));
2267 let scaled = src.resize(Size::new(160, 120)).unwrap();
2268 let Surface::PixelBuffer(scaled) = scaled else {
2269 panic!("VideoToolbox resize downloaded to the CPU");
2270 };
2271
2272 let gpu = scaled.download_i420().unwrap();
2273 let cpu = src_i420.resize(160, 120).unwrap();
2274
2275 assert_eq!((gpu.width, gpu.height), (160, 120));
2276 assert!(mae(gpu.y(), cpu.y()) < 4, "GPU and CPU luma disagree");
2277 assert!(mae(gpu.u(), cpu.u()) < 4, "GPU and CPU u disagree");
2278 assert!(mae(gpu.v(), cpu.v()) < 4, "GPU and CPU v disagree");
2279 }
2280
2281 #[cfg(target_os = "macos")]
2283 #[test]
2284 fn pixel_buffer_resize_can_force_the_cpu() {
2285 let config = crate::resize::Config {
2286 acceleration: crate::resize::Acceleration::Cpu,
2287 ..Default::default()
2288 };
2289 let source = Surface::PixelBuffer(nv12_surface(&gradient_i420(320, 240)));
2290 let scaled = source.resize_with(Size::new(160, 120), &config).unwrap();
2291
2292 assert!(matches!(scaled, Surface::I420(_)), "CPU resize stayed on the GPU");
2293 }
2294
2295 #[cfg(target_os = "macos")]
2298 fn nv12_surface(frame: &I420) -> super::macos::PixelBuffer {
2299 use std::ptr::{self, NonNull};
2300
2301 use objc2_core_foundation::CFRetained;
2302 use objc2_core_video::{
2303 CVPixelBuffer, CVPixelBufferCreate, CVPixelBufferGetBaseAddressOfPlane, CVPixelBufferGetBytesPerRowOfPlane,
2304 CVPixelBufferLockBaseAddress, CVPixelBufferLockFlags, CVPixelBufferUnlockBaseAddress,
2305 kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
2306 };
2307
2308 let mut raw: *mut CVPixelBuffer = ptr::null_mut();
2309 let status = unsafe {
2310 CVPixelBufferCreate(
2311 None,
2312 frame.width as usize,
2313 frame.height as usize,
2314 kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
2315 None,
2316 NonNull::new(&mut raw).expect("stack pointer is non-null"),
2317 )
2318 };
2319 assert_eq!(status, 0, "CVPixelBufferCreate failed");
2320 let buffer = unsafe { CFRetained::from_raw(NonNull::new(raw).expect("CoreVideo returned a buffer")) };
2321
2322 let flags = CVPixelBufferLockFlags(0);
2323 assert_eq!(unsafe { CVPixelBufferLockBaseAddress(&buffer, flags) }, 0);
2324 let width = frame.width as usize;
2325 let height = frame.height as usize;
2326 let y_base = CVPixelBufferGetBaseAddressOfPlane(&buffer, 0) as *mut u8;
2327 let y_stride = CVPixelBufferGetBytesPerRowOfPlane(&buffer, 0);
2328 for row in 0..height {
2329 unsafe {
2330 ptr::copy_nonoverlapping(frame.y()[row * width..].as_ptr(), y_base.add(row * y_stride), width);
2331 }
2332 }
2333
2334 let (chroma_width, chroma_height) = (width / 2, height / 2);
2335 let uv_base = CVPixelBufferGetBaseAddressOfPlane(&buffer, 1) as *mut u8;
2336 let uv_stride = CVPixelBufferGetBytesPerRowOfPlane(&buffer, 1);
2337 for row in 0..chroma_height {
2338 let output = unsafe { uv_base.add(row * uv_stride) };
2339 for col in 0..chroma_width {
2340 unsafe {
2341 *output.add(col * 2) = frame.u()[row * chroma_width + col];
2342 *output.add(col * 2 + 1) = frame.v()[row * chroma_width + col];
2343 }
2344 }
2345 }
2346 unsafe { CVPixelBufferUnlockBaseAddress(&buffer, flags) };
2347
2348 super::macos::PixelBuffer::new(buffer, frame.width, frame.height)
2349 }
2350
2351 #[cfg(target_os = "windows")]
2353 #[test]
2354 #[ignore = "D3D11 GPU reproducer; VideoProcessorBlt can hang on affected drivers"]
2355 fn d3d11_resize_defaults_to_the_gpu() {
2356 let Ok(device) = super::d3d11::create_device() else {
2357 eprintln!("skipping: no Direct3D11 hardware device");
2358 return;
2359 };
2360 let Ok(texture) = super::d3d11::upload_i420(&device, &gradient_i420(320, 240)) else {
2361 eprintln!("skipping: driver will not allocate a usable NV12 texture");
2362 return;
2363 };
2364 if !super::d3d11::supports_nv12_render_target(&device) {
2365 eprintln!("skipping: driver cannot render to NV12");
2366 return;
2367 }
2368
2369 let scaled = Surface::Texture(texture).resize(crate::Size::new(160, 120)).unwrap();
2370 assert!(
2371 matches!(scaled, Surface::Texture(_)),
2372 "Direct3D11 resize downloaded to the CPU"
2373 );
2374 assert_eq!((scaled.width(), scaled.height()), (160, 120));
2375 }
2376
2377 #[cfg(target_os = "windows")]
2379 #[test]
2380 fn d3d11_resize_can_force_the_cpu() {
2381 let Ok(device) = super::d3d11::create_device() else {
2382 eprintln!("skipping: no Direct3D11 hardware device");
2383 return;
2384 };
2385 let Ok(texture) = super::d3d11::upload_i420(&device, &gradient_i420(320, 240)) else {
2386 eprintln!("skipping: driver will not allocate a usable NV12 texture");
2387 return;
2388 };
2389
2390 let config = crate::resize::Config {
2391 acceleration: crate::resize::Acceleration::Cpu,
2392 ..Default::default()
2393 };
2394 let scaled = Surface::Texture(texture)
2395 .resize_with(crate::Size::new(160, 120), &config)
2396 .unwrap();
2397 assert!(matches!(scaled, Surface::I420(_)), "Direct3D11 resize ignored CPU mode");
2398 }
2399
2400 #[cfg(target_os = "windows")]
2404 #[test]
2405 #[ignore = "explicit D3D11 GPU probe; VideoProcessorBlt can hang on affected drivers"]
2406 fn d3d11_resize_matches_cpu() {
2407 let Ok(device) = super::d3d11::create_device() else {
2408 eprintln!("skipping: no Direct3D11 hardware device");
2409 return;
2410 };
2411 let source = gradient_i420(320, 240);
2412 let Ok(texture) = super::d3d11::upload_i420(&device, &source) else {
2413 eprintln!("skipping: driver will not allocate a usable NV12 texture");
2414 return;
2415 };
2416 if !super::d3d11::supports_nv12_render_target(&device) {
2417 eprintln!("skipping: driver cannot render to NV12");
2418 return;
2419 }
2420
2421 let gpu = texture.resize(160, 120).unwrap().download_i420().unwrap();
2422 let cpu = source.resize(160, 120).unwrap();
2423
2424 assert_eq!((gpu.width, gpu.height), (160, 120));
2425 assert!(mae(gpu.y(), cpu.y()) < 4, "GPU and CPU luma disagree");
2426 assert!(mae(gpu.u(), cpu.u()) < 4, "GPU and CPU u disagree");
2427 assert!(mae(gpu.v(), cpu.v()) < 4, "GPU and CPU v disagree");
2428 }
2429
2430 #[cfg(all(target_os = "linux", feature = "nvdec"))]
2433 #[test]
2434 fn cuda_resize_matches_cpu() {
2435 use std::sync::Arc;
2436
2437 use cudarc::driver::{CudaContext, result};
2438
2439 use super::cuda;
2440
2441 if unsafe { libloading::Library::new("libcuda.so.1") }.is_err() {
2443 return;
2444 }
2445 let Ok(ctx): Result<Arc<CudaContext>, _> = CudaContext::new(0) else {
2446 return;
2447 };
2448
2449 let (w, h) = (322u32, 242u32); let src_i420 = gradient_i420(w, h);
2451
2452 let pitch = 512u32;
2454 let frame = cuda::Frame::alloc(&ctx, w, h, pitch).unwrap();
2455 let mut host = vec![0u8; pitch as usize * h as usize * 3 / 2];
2456 for row in 0..h as usize {
2457 let dst = row * pitch as usize;
2458 host[dst..dst + w as usize].copy_from_slice(&src_i420.y()[row * w as usize..(row + 1) * w as usize]);
2459 }
2460 let (cw, ch) = (w as usize / 2, h as usize / 2);
2461 for row in 0..ch {
2462 let dst = (h as usize + row) * pitch as usize;
2463 for col in 0..cw {
2464 host[dst + 2 * col] = src_i420.u()[row * cw + col];
2465 host[dst + 2 * col + 1] = src_i420.v()[row * cw + col];
2466 }
2467 }
2468 unsafe { result::memcpy_htod_sync(frame.device_ptr(), &host) }.unwrap();
2470
2471 let scaled = frame.resize(160, 120).unwrap();
2472 let gpu = scaled.download_i420().unwrap();
2473 let cpu = src_i420.resize(160, 120).unwrap();
2474
2475 assert_eq!((gpu.width, gpu.height), (160, 120));
2476 assert!(mae(gpu.y(), cpu.y()) < 4, "GPU and CPU luma disagree");
2477 assert!(mae(gpu.u(), cpu.u()) < 4, "GPU and CPU u disagree");
2478 assert!(mae(gpu.v(), cpu.v()) < 4, "GPU and CPU v disagree");
2479 }
2480}