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> {
67 Ok(Frame {
68 timestamp: self.timestamp,
69 surface: self.surface.resize(size)?,
70 })
71 }
72}
73
74#[non_exhaustive]
93pub enum Surface {
94 #[cfg(target_os = "macos")]
97 PixelBuffer(macos::PixelBuffer),
98 #[cfg(target_os = "windows")]
100 Texture(d3d11::Texture),
101 #[cfg(all(target_os = "linux", feature = "nvdec"))]
104 Cuda(cuda::Frame),
105 I420(I420),
107}
108
109impl Surface {
110 pub fn width(&self) -> u32 {
112 match self {
113 #[cfg(target_os = "macos")]
114 Surface::PixelBuffer(s) => s.width,
115 #[cfg(target_os = "windows")]
116 Surface::Texture(t) => t.width,
117 #[cfg(all(target_os = "linux", feature = "nvdec"))]
118 Surface::Cuda(c) => c.width,
119 Surface::I420(i) => i.width,
120 }
121 }
122
123 pub fn height(&self) -> u32 {
125 match self {
126 #[cfg(target_os = "macos")]
127 Surface::PixelBuffer(s) => s.height,
128 #[cfg(target_os = "windows")]
129 Surface::Texture(t) => t.height,
130 #[cfg(all(target_os = "linux", feature = "nvdec"))]
131 Surface::Cuda(c) => c.height,
132 Surface::I420(i) => i.height,
133 }
134 }
135
136 pub fn rgba(rgba: &[u8], size: Size) -> Result<Self, Error> {
145 size.validate("RGBA frame")?;
146 let expected = size.pixels() as usize * 4;
147 if rgba.len() != expected {
148 return Err(Error::Codec(anyhow::anyhow!(
149 "RGBA buffer is {} bytes, expected {expected} for {size}",
150 rgba.len()
151 )));
152 }
153 Ok(Surface::I420(I420::from_rgba(
154 rgba,
155 size.width * 4,
156 size.width,
157 size.height,
158 )?))
159 }
160
161 pub fn resize(&self, size: Size) -> Result<Surface, Error> {
165 size.validate("resize to")?;
166 let Size { width, height } = size;
167
168 Ok(match self {
169 Surface::I420(i420) => Surface::I420(i420.resize(width, height)?),
170 #[cfg(target_os = "macos")]
171 Surface::PixelBuffer(pixels) => match pixels.resize(width, height) {
172 Ok(scaled) => Surface::PixelBuffer(scaled),
173 Err(err) => {
176 static WARN_ONCE: std::sync::Once = std::sync::Once::new();
177 WARN_ONCE.call_once(|| tracing::warn!(%err, "GPU resize failed; falling back to the CPU"));
178 Surface::I420(pixels.download_i420()?.resize(width, height)?)
179 }
180 },
181 #[cfg(all(target_os = "linux", feature = "nvdec"))]
182 Surface::Cuda(cuda) => match cuda.resize(width, height) {
183 Ok(scaled) => Surface::Cuda(scaled),
184 Err(err) => {
187 static WARN_ONCE: std::sync::Once = std::sync::Once::new();
188 WARN_ONCE.call_once(|| tracing::warn!(%err, "GPU resize failed; falling back to the CPU"));
189 Surface::I420(cuda.download_i420()?.resize(width, height)?)
190 }
191 },
192 #[allow(unreachable_patterns)]
195 other => Surface::I420(other.to_i420()?.into_owned().resize(width, height)?),
196 })
197 }
198
199 pub fn into_i420(self) -> Result<Bytes, Error> {
209 match self {
210 Surface::I420(i420) => Ok(Bytes::from(i420.data)),
211 #[allow(unreachable_patterns)]
212 other => Ok(Bytes::from(other.to_i420()?.into_owned().data)),
213 }
214 }
215
216 #[cfg(target_os = "macos")]
230 pub fn into_pixel_buffer(
231 self,
232 ) -> Result<objc2_core_foundation::CFRetained<objc2_core_video::CVPixelBuffer>, Error> {
233 match self {
234 Surface::PixelBuffer(pixels) => Ok(pixels.buffer),
235 Surface::I420(i420) => macos::upload_i420(&i420),
236 }
237 }
238
239 pub fn color(&self) -> Option<Color> {
249 match self {
250 #[cfg(target_os = "macos")]
251 Surface::PixelBuffer(s) => s.color(),
252 #[cfg(target_os = "windows")]
253 Surface::Texture(_) => None,
254 #[cfg(all(target_os = "linux", feature = "nvdec"))]
255 Surface::Cuda(_) => None,
256 Surface::I420(i) => i.color(),
257 }
258 }
259
260 pub(crate) fn to_i420(&self) -> Result<Cow<'_, I420>, Error> {
262 match self {
263 #[cfg(target_os = "macos")]
264 Surface::PixelBuffer(s) => Ok(Cow::Owned(s.download_i420()?)),
265 #[cfg(target_os = "windows")]
266 Surface::Texture(t) => Ok(Cow::Owned(t.download_i420()?)),
267 #[cfg(all(target_os = "linux", feature = "nvdec"))]
268 Surface::Cuda(c) => Ok(Cow::Owned(c.download_i420()?)),
269 Surface::I420(i) => Ok(Cow::Borrowed(i)),
270 }
271 }
272}
273
274#[derive(Clone)]
277pub struct I420 {
278 pub(crate) width: u32,
279 pub(crate) height: u32,
280 pub(crate) data: Vec<u8>,
282 pub(crate) color: Option<Color>,
287}
288
289impl I420 {
290 pub fn new(width: u32, height: u32, data: Vec<u8>) -> Result<Self, Error> {
297 crate::Size::new(width, height).validate("I420")?;
298 let expected = Self::len(width, height);
299 if data.len() != expected {
300 return Err(Error::Codec(anyhow::anyhow!(
301 "I420 {width}x{height} needs {expected} bytes, got {}",
302 data.len()
303 )));
304 }
305 Ok(Self {
306 width,
307 height,
308 data,
309 color: None,
310 })
311 }
312
313 pub fn width(&self) -> u32 {
315 self.width
316 }
317
318 pub fn height(&self) -> u32 {
320 self.height
321 }
322
323 pub fn data(&self) -> &[u8] {
325 &self.data
326 }
327
328 pub fn color(&self) -> Option<Color> {
336 self.color
337 }
338
339 pub fn with_color(mut self, color: Color) -> Self {
342 self.color = Some(color);
343 self
344 }
345
346 pub fn len(width: u32, height: u32) -> usize {
348 let luma = width as usize * height as usize;
349 luma + luma / 2
350 }
351
352 pub(crate) fn from_rgba(rgba: &[u8], stride: u32, width: u32, height: u32) -> Result<Self, Error> {
357 let color = Color::infer(Size::new(width, height));
358 let (range, matrix) = color.yuv();
359 let mut planar = YuvPlanarImageMut::alloc(width, height, YuvChromaSubsampling::Yuv420);
360 rgba_to_yuv420(&mut planar, rgba, stride, range, matrix, YuvConversionMode::Balanced)
361 .map_err(|e| Error::Codec(anyhow::anyhow!("rgba_to_yuv420 failed for {width}x{height}: {e}")))?;
362 Ok(Self::pack(&planar, width, height, Some(color)))
363 }
364
365 #[cfg(any(target_os = "windows", all(target_os = "linux", feature = "pipewire")))]
371 pub(crate) fn from_bgra(bgra: &[u8], stride: u32, width: u32, height: u32) -> Result<Self, Error> {
372 use yuv::bgra_to_yuv420;
373
374 let color = Color::infer(Size::new(width, height));
375 let (range, matrix) = color.yuv();
376 let mut planar = YuvPlanarImageMut::alloc(width, height, YuvChromaSubsampling::Yuv420);
377 bgra_to_yuv420(&mut planar, bgra, stride, range, matrix, YuvConversionMode::Balanced)
378 .map_err(|e| Error::Codec(anyhow::anyhow!("bgra_to_yuv420 failed for {width}x{height}: {e}")))?;
379 Ok(Self::pack(&planar, width, height, Some(color)))
380 }
381
382 pub(crate) fn from_planes(
388 y: &[u8],
389 u: &[u8],
390 v: &[u8],
391 y_stride: usize,
392 uv_stride: usize,
393 width: u32,
394 height: u32,
395 ) -> Self {
396 let (w, h) = (width as usize, height as usize);
397 let (cw, ch) = (w / 2, h / 2);
398
399 let mut data = vec![0u8; Self::len(width, height)];
400 let (luma, chroma) = data.split_at_mut(w * h);
401 let (u_dst, v_dst) = chroma.split_at_mut(cw * ch);
402
403 for row in 0..h {
404 luma[row * w..row * w + w].copy_from_slice(&y[row * y_stride..row * y_stride + w]);
405 }
406 for row in 0..ch {
407 u_dst[row * cw..row * cw + cw].copy_from_slice(&u[row * uv_stride..row * uv_stride + cw]);
408 v_dst[row * cw..row * cw + cw].copy_from_slice(&v[row * uv_stride..row * uv_stride + cw]);
409 }
410
411 Self {
412 width,
413 height,
414 data,
415 color: None,
416 }
417 }
418
419 #[cfg(target_os = "linux")]
423 pub(crate) fn from_rgb(rgb: &[u8], width: u32, height: u32) -> Result<Self, Error> {
424 use yuv::rgb_to_yuv420;
425
426 let color = Color::infer(Size::new(width, height));
427 let (range, matrix) = color.yuv();
428 let mut planar = YuvPlanarImageMut::alloc(width, height, YuvChromaSubsampling::Yuv420);
429 rgb_to_yuv420(&mut planar, rgb, width * 3, range, matrix, YuvConversionMode::Balanced)
430 .map_err(|e| Error::Codec(anyhow::anyhow!("rgb_to_yuv420 failed for {width}x{height}: {e}")))?;
431 Ok(Self::pack(&planar, width, height, Some(color)))
432 }
433
434 #[cfg(target_os = "linux")]
438 pub(crate) fn from_yuyv(yuyv: &[u8], stride: u32, width: u32, height: u32) -> Result<Self, Error> {
439 use yuv::{YuvPackedImage, yuyv422_to_yuv420};
440
441 let mut planar = YuvPlanarImageMut::alloc(width, height, YuvChromaSubsampling::Yuv420);
442 let packed = YuvPackedImage {
443 yuy: yuyv,
444 yuy_stride: stride,
445 width,
446 height,
447 };
448 yuyv422_to_yuv420(&mut planar, &packed)
449 .map_err(|e| Error::Codec(anyhow::anyhow!("yuyv422_to_yuv420 failed for {width}x{height}: {e}")))?;
450 Ok(Self::pack(&planar, width, height, None))
453 }
454
455 #[cfg(target_os = "windows")]
460 pub(crate) fn from_nv12(nv12: &[u8], width: u32, height: u32) -> Result<Self, Error> {
461 let (w, h) = (width as usize, height as usize);
462 let luma = w * h;
463 let chroma = luma / 4;
464 let need = luma + 2 * chroma;
465 if nv12.len() < need {
466 return Err(Error::Codec(anyhow::anyhow!(
467 "NV12 buffer too small: {} < {need} for {width}x{height}",
468 nv12.len()
469 )));
470 }
471
472 let mut data = vec![0u8; Self::len(width, height)];
473 data[..luma].copy_from_slice(&nv12[..luma]);
474 let (u_dst, v_dst) = data[luma..].split_at_mut(chroma);
475 deinterleave_uv(&nv12[luma..need], u_dst, v_dst);
476 Ok(Self {
477 width,
478 height,
479 data,
480 color: None,
481 })
482 }
483
484 pub(crate) fn resize(&self, width: u32, height: u32) -> Result<Self, Error> {
488 use std::cell::RefCell;
489
490 use fast_image_resize::images::{Image, ImageRef};
491 use fast_image_resize::{FilterType, PixelType, ResizeAlg, ResizeOptions, Resizer};
492
493 thread_local! {
497 static RESIZER: RefCell<Resizer> = RefCell::new(Resizer::new());
498 }
499
500 let options = ResizeOptions::new().resize_alg(ResizeAlg::Convolution(FilterType::Bilinear));
503
504 let plane = |resizer: &mut Resizer,
505 src: &[u8],
506 sw: u32,
507 sh: u32,
508 dst: &mut [u8],
509 dw: u32,
510 dh: u32|
511 -> Result<(), Error> {
512 let src = ImageRef::new(sw, sh, src, PixelType::U8)
513 .map_err(|e| Error::Codec(anyhow::anyhow!("resize source: {e}")))?;
514 let mut dst = Image::from_slice_u8(dw, dh, dst, PixelType::U8)
515 .map_err(|e| Error::Codec(anyhow::anyhow!("resize destination: {e}")))?;
516 resizer
517 .resize(&src, &mut dst, &options)
518 .map_err(|e| Error::Codec(anyhow::anyhow!("resize: {e}")))
519 };
520
521 let luma = width as usize * height as usize;
522 let mut data = vec![0u8; Self::len(width, height)];
523 let (y_dst, chroma) = data.split_at_mut(luma);
524 let (u_dst, v_dst) = chroma.split_at_mut(luma / 4);
525
526 RESIZER.with_borrow_mut(|resizer| {
527 plane(resizer, self.y(), self.width, self.height, y_dst, width, height)?;
528 let (sw2, sh2) = (self.width / 2, self.height / 2);
529 let (dw2, dh2) = (width / 2, height / 2);
530 plane(resizer, self.u(), sw2, sh2, u_dst, dw2, dh2)?;
531 plane(resizer, self.v(), sw2, sh2, v_dst, dw2, dh2)
532 })?;
533
534 Ok(Self {
536 width,
537 height,
538 data,
539 color: self.color,
540 })
541 }
542
543 fn pack(planar: &YuvPlanarImageMut<u8>, width: u32, height: u32, color: Option<Color>) -> Self {
549 let mut data = Vec::with_capacity(Self::len(width, height));
550 data.extend_from_slice(planar.y_plane.borrow());
551 data.extend_from_slice(planar.u_plane.borrow());
552 data.extend_from_slice(planar.v_plane.borrow());
553 Self {
554 width,
555 height,
556 data,
557 color,
558 }
559 }
560
561 fn luma_len(&self) -> usize {
562 self.width as usize * self.height as usize
563 }
564
565 fn chroma_len(&self) -> usize {
566 self.luma_len() / 4
567 }
568
569 pub fn y(&self) -> &[u8] {
571 &self.data[..self.luma_len()]
572 }
573
574 pub fn u(&self) -> &[u8] {
576 let start = self.luma_len();
577 &self.data[start..start + self.chroma_len()]
578 }
579
580 pub fn v(&self) -> &[u8] {
582 let start = self.luma_len() + self.chroma_len();
583 &self.data[start..start + self.chroma_len()]
584 }
585}
586
587#[cfg(any(target_os = "windows", all(target_os = "linux", feature = "nvenc")))]
590pub(crate) fn interleave_uv(u: &[u8], v: &[u8], uv: &mut [u8]) {
591 for (pair, (u, v)) in uv.chunks_exact_mut(2).zip(u.iter().zip(v)) {
592 pair[0] = *u;
593 pair[1] = *v;
594 }
595}
596
597#[cfg(target_os = "windows")]
600pub(crate) fn deinterleave_uv(uv: &[u8], u: &mut [u8], v: &mut [u8]) {
601 for (pair, (u, v)) in uv.chunks_exact(2).zip(u.iter_mut().zip(v)) {
602 *u = pair[0];
603 *v = pair[1];
604 }
605}
606
607#[cfg(target_os = "macos")]
608pub mod macos {
609 use std::collections::{HashMap, VecDeque};
614 use std::ffi::c_void;
615 use std::ptr;
616 use std::ptr::NonNull;
617 use std::sync::{Arc, LazyLock, Mutex};
618
619 use objc2_core_foundation::{CFDictionary, CFNumber, CFNumberType, CFRetained, CFString};
620 use objc2_core_video::{
621 CVPixelBuffer, CVPixelBufferCreate, CVPixelBufferGetBaseAddressOfPlane, CVPixelBufferGetBytesPerRowOfPlane,
622 CVPixelBufferGetPixelFormatType, CVPixelBufferLockBaseAddress, CVPixelBufferLockFlags, CVPixelBufferPool,
623 CVPixelBufferUnlockBaseAddress, kCVImageBufferYCbCrMatrix_ITU_R_601_4, kCVImageBufferYCbCrMatrix_ITU_R_709_2,
624 kCVImageBufferYCbCrMatrixKey, kCVPixelBufferHeightKey, kCVPixelBufferIOSurfacePropertiesKey,
625 kCVPixelBufferPixelFormatTypeKey, kCVPixelBufferWidthKey, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
626 kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, kCVPixelFormatType_420YpCbCr8Planar,
627 };
628 use objc2_video_toolbox::VTPixelTransferSession;
629
630 use super::I420;
631 use crate::{Color, Error};
632
633 const LOCK_READ_ONLY: CVPixelBufferLockFlags = CVPixelBufferLockFlags(1);
635
636 const SCALER_CACHE_CAPACITY: usize = 16;
639
640 type ScalerCache = Mutex<Cache<Scaler>>;
644 static SCALERS: LazyLock<ScalerCache> = LazyLock::new(|| Mutex::new(Cache::new(SCALER_CACHE_CAPACITY)));
645
646 struct Cache<T> {
648 values: HashMap<(u32, u32), Arc<Mutex<T>>>,
649 order: VecDeque<(u32, u32)>,
650 capacity: usize,
651 }
652
653 impl<T> Cache<T> {
654 fn new(capacity: usize) -> Self {
655 Self {
656 values: HashMap::new(),
657 order: VecDeque::new(),
658 capacity,
659 }
660 }
661
662 fn get_or_insert_with<E>(
663 &mut self,
664 key: (u32, u32),
665 create: impl FnOnce() -> Result<T, E>,
666 ) -> Result<Arc<Mutex<T>>, E> {
667 if let Some(value) = self.values.get(&key).cloned() {
668 self.touch(key);
669 return Ok(value);
670 }
671
672 let value = Arc::new(Mutex::new(create()?));
673 self.values.insert(key, Arc::clone(&value));
674 self.touch(key);
675 self.prune();
676 Ok(value)
677 }
678
679 fn touch(&mut self, key: (u32, u32)) {
680 self.order.retain(|entry| *entry != key);
681 self.order.push_back(key);
682 }
683
684 fn prune(&mut self) {
685 let mut remaining = self.order.len();
686 while self.values.len() > self.capacity && remaining > 0 {
687 let key = self.order.pop_front().expect("remaining entries");
688 let idle = self.values.get(&key).is_some_and(|value| Arc::strong_count(value) == 1);
689 if idle {
690 self.values.remove(&key);
691 } else {
692 self.order.push_back(key);
693 }
694 remaining -= 1;
695 }
696 }
697 }
698
699 pub struct PixelBuffer {
702 pub(crate) buffer: CFRetained<CVPixelBuffer>,
703 pub(crate) width: u32,
704 pub(crate) height: u32,
705 }
706
707 unsafe impl Send for PixelBuffer {}
717 unsafe impl Sync for PixelBuffer {}
718
719 impl PixelBuffer {
720 pub fn buffer(&self) -> &CVPixelBuffer {
723 &self.buffer
724 }
725
726 pub fn width(&self) -> u32 {
728 self.width
729 }
730
731 pub fn height(&self) -> u32 {
733 self.height
734 }
735
736 pub(crate) fn new(buffer: CFRetained<CVPixelBuffer>, width: u32, height: u32) -> Self {
737 Self { buffer, width, height }
738 }
739
740 pub(crate) fn resize(&self, width: u32, height: u32) -> Result<Self, Error> {
742 let scaler = {
743 let mut scalers = SCALERS
744 .lock()
745 .map_err(|_| Error::Codec(anyhow::anyhow!("pixel-transfer scaler cache lock poisoned")))?;
746 scalers.get_or_insert_with((width, height), || Scaler::new(width, height))?
747 };
748
749 let result = scaler
750 .lock()
751 .map_err(|_| Error::Codec(anyhow::anyhow!("pixel-transfer scaler lock poisoned")))?
752 .resize(self);
753 drop(scaler);
754 if let Ok(mut scalers) = SCALERS.lock() {
755 scalers.prune();
756 }
757 result
758 }
759
760 fn matrix(&self) -> Color {
768 let inferred = Color::infer(crate::Size::new(self.width, self.height));
769 let Some(value) = (unsafe { self.buffer.attachment(kCVImageBufferYCbCrMatrixKey, ptr::null_mut()) }) else {
771 return inferred;
772 };
773 let Some(name) = value.downcast_ref::<CFString>() else {
774 return inferred;
775 };
776
777 if name == unsafe { kCVImageBufferYCbCrMatrix_ITU_R_709_2 } {
780 Color::Bt709Limited
781 } else if name == unsafe { kCVImageBufferYCbCrMatrix_ITU_R_601_4 } {
782 Color::Bt601Limited
783 } else {
784 inferred
787 }
788 }
789
790 pub(crate) fn color(&self) -> Option<Color> {
794 let format = CVPixelBufferGetPixelFormatType(&self.buffer);
795 let limited = if format == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange {
796 true
797 } else if format == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange {
798 false
799 } else {
800 return None;
801 };
802 Some(self.matrix().with_range(limited))
803 }
804
805 pub(crate) fn download_i420(&self) -> Result<I420, Error> {
812 let format = CVPixelBufferGetPixelFormatType(&self.buffer);
813 if format != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
814 && format != kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
815 {
816 return Err(Error::Codec(anyhow::anyhow!(
817 "cannot download pixel format {format:#x}; expected NV12"
818 )));
819 }
820
821 let color = self.color();
822
823 let (w, h) = (self.width as usize, self.height as usize);
824 let (cw, ch) = (w / 2, h / 2);
825
826 let status = unsafe { CVPixelBufferLockBaseAddress(&self.buffer, LOCK_READ_ONLY) };
827 if status != 0 {
828 return Err(Error::Codec(anyhow::anyhow!(
829 "CVPixelBufferLockBaseAddress failed: {status}"
830 )));
831 }
832 let _guard = UnlockGuard(&self.buffer);
833
834 let mut data = vec![0u8; I420::len(self.width, self.height)];
835 let (luma, chroma) = data.split_at_mut(w * h);
836 let (u_plane, v_plane) = chroma.split_at_mut(cw * ch);
837
838 let y_base = CVPixelBufferGetBaseAddressOfPlane(&self.buffer, 0) as *const u8;
840 let y_stride = CVPixelBufferGetBytesPerRowOfPlane(&self.buffer, 0);
841 for row in 0..h {
842 unsafe {
843 ptr::copy_nonoverlapping(y_base.add(row * y_stride), luma[row * w..].as_mut_ptr(), w);
844 }
845 }
846
847 let uv_base = CVPixelBufferGetBaseAddressOfPlane(&self.buffer, 1) as *const u8;
849 let uv_stride = CVPixelBufferGetBytesPerRowOfPlane(&self.buffer, 1);
850 for row in 0..ch {
851 let src = unsafe { uv_base.add(row * uv_stride) };
852 for col in 0..cw {
853 unsafe {
854 u_plane[row * cw + col] = *src.add(col * 2);
855 v_plane[row * cw + col] = *src.add(col * 2 + 1);
856 }
857 }
858 }
859
860 Ok(I420 {
861 width: self.width,
862 height: self.height,
863 data,
864 color,
865 })
866 }
867 }
868
869 struct Scaler {
871 session: CFRetained<VTPixelTransferSession>,
872 pool: CFRetained<CVPixelBufferPool>,
873 width: u32,
874 height: u32,
875 }
876
877 unsafe impl Send for Scaler {}
881
882 impl Scaler {
883 fn new(width: u32, height: u32) -> Result<Self, Error> {
884 let mut session_ptr: *mut VTPixelTransferSession = std::ptr::null_mut();
885 let status = unsafe {
886 VTPixelTransferSession::create(None, NonNull::new(&mut session_ptr).expect("stack pointer is non-null"))
887 };
888 let session = NonNull::new(session_ptr)
889 .filter(|_| status == 0)
890 .map(|ptr| unsafe { CFRetained::from_raw(ptr) })
891 .ok_or_else(|| Error::Codec(anyhow::anyhow!("VTPixelTransferSessionCreate failed: {status}")))?;
892
893 let attributes = pool_attributes(width, height)?;
894 let mut pool_ptr: *mut CVPixelBufferPool = std::ptr::null_mut();
895 let status = unsafe {
896 CVPixelBufferPool::create(
897 None,
898 None,
899 Some(&attributes),
900 NonNull::new(&mut pool_ptr).expect("stack pointer is non-null"),
901 )
902 };
903 let pool = NonNull::new(pool_ptr)
904 .filter(|_| status == 0)
905 .map(|ptr| unsafe { CFRetained::from_raw(ptr) })
906 .ok_or_else(|| Error::Codec(anyhow::anyhow!("CVPixelBufferPoolCreate failed: {status}")))?;
907
908 Ok(Self {
909 session,
910 pool,
911 width,
912 height,
913 })
914 }
915
916 fn resize(&mut self, source: &PixelBuffer) -> Result<PixelBuffer, Error> {
917 let mut output_ptr: *mut CVPixelBuffer = std::ptr::null_mut();
918 let status = unsafe {
919 CVPixelBufferPool::create_pixel_buffer(
920 None,
921 &self.pool,
922 NonNull::new(&mut output_ptr).expect("stack pointer is non-null"),
923 )
924 };
925 let output = NonNull::new(output_ptr)
926 .filter(|_| status == 0)
927 .map(|ptr| unsafe { CFRetained::from_raw(ptr) })
928 .ok_or_else(|| Error::Codec(anyhow::anyhow!("CVPixelBufferPoolCreatePixelBuffer failed: {status}")))?;
929
930 let status = unsafe { self.session.transfer_image(&source.buffer, &output) };
931 if status != 0 {
932 return Err(Error::Codec(anyhow::anyhow!(
933 "VTPixelTransferSessionTransferImage failed: {status}"
934 )));
935 }
936
937 Ok(PixelBuffer::new(output, self.width, self.height))
938 }
939 }
940
941 fn pool_attributes(width: u32, height: u32) -> Result<CFRetained<CFDictionary>, Error> {
943 let width =
944 i32::try_from(width).map_err(|_| Error::Codec(anyhow::anyhow!("pixel-buffer width is too large")))?;
945 let height =
946 i32::try_from(height).map_err(|_| Error::Codec(anyhow::anyhow!("pixel-buffer height is too large")))?;
947 let format = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange as i32;
948
949 let width = cf_number(width)?;
950 let height = cf_number(height)?;
951 let format = cf_number(format)?;
952 let iosurface = unsafe {
953 CFDictionary::new(
954 None,
955 std::ptr::null_mut(),
956 std::ptr::null_mut(),
957 0,
958 &objc2_core_foundation::kCFTypeDictionaryKeyCallBacks,
959 &objc2_core_foundation::kCFTypeDictionaryValueCallBacks,
960 )
961 }
962 .ok_or_else(|| Error::Codec(anyhow::anyhow!("failed to build IOSurface attributes dictionary")))?;
963
964 let mut keys = [
965 (unsafe { kCVPixelBufferPixelFormatTypeKey } as *const CFString).cast::<c_void>(),
966 (unsafe { kCVPixelBufferWidthKey } as *const CFString).cast::<c_void>(),
967 (unsafe { kCVPixelBufferHeightKey } as *const CFString).cast::<c_void>(),
968 (unsafe { kCVPixelBufferIOSurfacePropertiesKey } as *const CFString).cast::<c_void>(),
969 ];
970 let mut values = [
971 (format.as_ref() as *const CFNumber).cast::<c_void>(),
972 (width.as_ref() as *const CFNumber).cast::<c_void>(),
973 (height.as_ref() as *const CFNumber).cast::<c_void>(),
974 (iosurface.as_ref() as *const CFDictionary).cast::<c_void>(),
975 ];
976 unsafe {
977 CFDictionary::new(
978 None,
979 keys.as_mut_ptr(),
980 values.as_mut_ptr(),
981 4,
982 &objc2_core_foundation::kCFTypeDictionaryKeyCallBacks,
983 &objc2_core_foundation::kCFTypeDictionaryValueCallBacks,
984 )
985 }
986 .ok_or_else(|| {
987 Error::Codec(anyhow::anyhow!(
988 "failed to build pixel-buffer pool attributes dictionary"
989 ))
990 })
991 }
992
993 fn cf_number(value: i32) -> Result<CFRetained<CFNumber>, Error> {
994 unsafe { CFNumber::new(None, CFNumberType::SInt32Type, (&value as *const i32).cast::<c_void>()) }
995 .ok_or_else(|| Error::Codec(anyhow::anyhow!("failed to build CFNumber")))
996 }
997
998 struct UnlockGuard<'a>(&'a CVPixelBuffer);
999
1000 impl Drop for UnlockGuard<'_> {
1001 fn drop(&mut self) {
1002 unsafe { CVPixelBufferUnlockBaseAddress(self.0, LOCK_READ_ONLY) };
1003 }
1004 }
1005
1006 pub(crate) fn upload_i420(frame: &I420) -> Result<CFRetained<CVPixelBuffer>, Error> {
1012 let (w, h) = (frame.width as usize, frame.height as usize);
1013 let (cw, ch) = (w / 2, h / 2);
1014
1015 let mut ptr: *mut CVPixelBuffer = std::ptr::null_mut();
1016 let status = unsafe {
1017 CVPixelBufferCreate(
1018 None,
1019 w,
1020 h,
1021 kCVPixelFormatType_420YpCbCr8Planar,
1022 None,
1023 NonNull::new(&mut ptr).unwrap(),
1024 )
1025 };
1026 let buffer = NonNull::new(ptr)
1027 .filter(|_| status == 0)
1028 .map(|p| unsafe { CFRetained::from_raw(p) })
1029 .ok_or_else(|| Error::Codec(anyhow::anyhow!("CVPixelBufferCreate failed: {status}")))?;
1030
1031 let flags = CVPixelBufferLockFlags(0);
1032 let status = unsafe { CVPixelBufferLockBaseAddress(&buffer, flags) };
1033 if status != 0 {
1034 return Err(Error::Codec(anyhow::anyhow!(
1035 "CVPixelBufferLockBaseAddress failed: {status}"
1036 )));
1037 }
1038
1039 copy_plane(&buffer, 0, frame.y(), w, h);
1040 copy_plane(&buffer, 1, frame.u(), cw, ch);
1041 copy_plane(&buffer, 2, frame.v(), cw, ch);
1042
1043 unsafe { CVPixelBufferUnlockBaseAddress(&buffer, flags) };
1044 Ok(buffer)
1045 }
1046
1047 fn copy_plane(buffer: &CVPixelBuffer, plane: usize, src: &[u8], row_bytes: usize, rows: usize) {
1050 let base = CVPixelBufferGetBaseAddressOfPlane(buffer, plane) as *mut u8;
1051 let stride = CVPixelBufferGetBytesPerRowOfPlane(buffer, plane);
1052 for y in 0..rows {
1053 unsafe {
1054 let dst = base.add(y * stride);
1055 std::ptr::copy_nonoverlapping(src[y * row_bytes..].as_ptr(), dst, row_bytes);
1056 }
1057 }
1058 }
1059
1060 #[cfg(test)]
1061 mod cache_tests {
1062 use super::Cache;
1063
1064 #[test]
1065 fn evicts_the_least_recently_used_idle_value() {
1066 let mut cache = Cache::new(2);
1067
1068 let first = cache.get_or_insert_with((1, 1), || Ok::<_, ()>(())).unwrap();
1069 drop(first);
1070 let second = cache.get_or_insert_with((2, 2), || Ok::<_, ()>(())).unwrap();
1071 drop(second);
1072
1073 let first = cache
1074 .get_or_insert_with((1, 1), || Err::<(), _>("cached value was recreated"))
1075 .unwrap();
1076 drop(first);
1077 let third = cache.get_or_insert_with((3, 3), || Ok::<_, ()>(())).unwrap();
1078 drop(third);
1079
1080 assert!(cache.values.contains_key(&(1, 1)));
1081 assert!(!cache.values.contains_key(&(2, 2)));
1082 assert!(cache.values.contains_key(&(3, 3)));
1083 assert_eq!(cache.values.len(), 2);
1084 }
1085
1086 #[test]
1087 fn defers_eviction_until_an_active_value_is_released() {
1088 let mut cache = Cache::new(1);
1089 let first = cache.get_or_insert_with((1, 1), || Ok::<_, ()>(())).unwrap();
1090 let second = cache.get_or_insert_with((2, 2), || Ok::<_, ()>(())).unwrap();
1091 assert_eq!(cache.values.len(), 2);
1092
1093 drop(first);
1094 cache.prune();
1095 assert!(!cache.values.contains_key(&(1, 1)));
1096 assert!(cache.values.contains_key(&(2, 2)));
1097 assert_eq!(cache.values.len(), 1);
1098 drop(second);
1099 }
1100 }
1101}
1102
1103#[cfg(all(target_os = "linux", feature = "nvdec"))]
1104pub mod cuda {
1105 use std::sync::{Arc, OnceLock};
1109
1110 use cudarc::driver::{CudaContext, CudaFunction, LaunchConfig, PushKernelArg, result};
1111
1112 use super::I420;
1113 use crate::Error;
1114
1115 const RESIZE_PTX: &str = include_str!("frame/nv12_resize.ptx");
1118
1119 struct Kernels {
1122 luma: CudaFunction,
1123 chroma: CudaFunction,
1124 }
1125
1126 fn kernels(ctx: &Arc<CudaContext>) -> Result<&'static Kernels, Error> {
1127 static KERNELS: OnceLock<Result<Kernels, String>> = OnceLock::new();
1128 KERNELS
1129 .get_or_init(|| {
1130 let module = ctx
1131 .load_module(cudarc::nvrtc::Ptx::from_src(RESIZE_PTX))
1132 .map_err(|e| format!("load nv12_resize PTX: {e:?}"))?;
1133 Ok(Kernels {
1134 luma: module
1135 .load_function("resize_luma")
1136 .map_err(|e| format!("load resize_luma: {e:?}"))?,
1137 chroma: module
1138 .load_function("resize_chroma")
1139 .map_err(|e| format!("load resize_chroma: {e:?}"))?,
1140 })
1141 })
1142 .as_ref()
1143 .map_err(|e| Error::Codec(anyhow::anyhow!("CUDA resize unavailable: {e}")))
1144 }
1145
1146 struct Buffer {
1151 ctx: Arc<CudaContext>,
1152 ptr: cudarc::driver::sys::CUdeviceptr,
1153 len: usize,
1154 }
1155
1156 impl Drop for Buffer {
1157 fn drop(&mut self) {
1158 if self.ctx.bind_to_thread().is_ok() {
1160 let _ = unsafe { result::free_sync(self.ptr) };
1162 }
1163 }
1164 }
1165
1166 #[derive(Clone)]
1174 pub struct Frame {
1175 buf: Arc<Buffer>,
1176 pub(crate) width: u32,
1177 pub(crate) height: u32,
1178 pub(crate) pitch: u32,
1180 }
1181
1182 impl Frame {
1183 pub(crate) fn alloc(ctx: &Arc<CudaContext>, width: u32, height: u32, pitch: u32) -> Result<Self, Error> {
1186 debug_assert!(pitch >= width && width.is_multiple_of(2) && height.is_multiple_of(2));
1187 let len = pitch as usize * height as usize * 3 / 2;
1188 ctx.bind_to_thread()
1189 .map_err(|e| Error::Codec(anyhow::anyhow!("CUDA bind: {e:?}")))?;
1190 let ptr = unsafe { result::malloc_sync(len) }
1193 .map_err(|e| Error::Codec(anyhow::anyhow!("CUDA alloc of {len} bytes: {e:?}")))?;
1194 Ok(Self {
1195 buf: Arc::new(Buffer {
1196 ctx: ctx.clone(),
1197 ptr,
1198 len,
1199 }),
1200 width,
1201 height,
1202 pitch,
1203 })
1204 }
1205
1206 pub(crate) fn device_ptr(&self) -> u64 {
1209 self.buf.ptr
1210 }
1211
1212 pub(crate) fn download_i420(&self) -> Result<I420, Error> {
1215 self.buf
1216 .ctx
1217 .bind_to_thread()
1218 .map_err(|e| Error::Codec(anyhow::anyhow!("CUDA bind: {e:?}")))?;
1219 let mut host = vec![0u8; self.buf.len];
1220 unsafe { result::memcpy_dtoh_sync(&mut host, self.buf.ptr) }
1223 .map_err(|e| Error::Codec(anyhow::anyhow!("CUDA download: {e:?}")))?;
1224
1225 let (w, h) = (self.width as usize, self.height as usize);
1226 let (cw, ch) = (w / 2, h / 2);
1227 let pitch = self.pitch as usize;
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_dst, v_dst) = chroma.split_at_mut(cw * ch);
1232
1233 for row in 0..h {
1234 luma[row * w..row * w + w].copy_from_slice(&host[row * pitch..row * pitch + w]);
1235 }
1236 let uv_base = pitch * h;
1237 for row in 0..ch {
1238 let src = &host[uv_base + row * pitch..uv_base + row * pitch + w];
1239 for col in 0..cw {
1240 u_dst[row * cw + col] = src[col * 2];
1241 v_dst[row * cw + col] = src[col * 2 + 1];
1242 }
1243 }
1244
1245 Ok(I420 {
1246 width: self.width,
1247 height: self.height,
1248 data,
1249 color: None,
1252 })
1253 }
1254
1255 pub(crate) fn resize(&self, width: u32, height: u32) -> Result<Self, Error> {
1259 let ctx = &self.buf.ctx;
1260 let kernels = kernels(ctx)?;
1261
1262 let pitch = width.next_multiple_of(256);
1265 let dst = Self::alloc(ctx, width, height, pitch)?;
1266
1267 let stream = ctx.default_stream();
1268 let block = (16u32, 16, 1);
1269 let grid = |w: u32, h: u32| (w.div_ceil(16), h.div_ceil(16), 1);
1270 let launch_err = |plane: &str, e| Error::Codec(anyhow::anyhow!("CUDA resize {plane}: {e:?}"));
1271
1272 unsafe {
1278 stream
1279 .launch_builder(&kernels.luma)
1280 .arg(&self.buf.ptr)
1281 .arg(&self.pitch)
1282 .arg(&self.width)
1283 .arg(&self.height)
1284 .arg(&dst.buf.ptr)
1285 .arg(&pitch)
1286 .arg(&width)
1287 .arg(&height)
1288 .launch(LaunchConfig {
1289 grid_dim: grid(width, height),
1290 block_dim: block,
1291 shared_mem_bytes: 0,
1292 })
1293 }
1294 .map_err(|e| launch_err("luma", e))?;
1295
1296 let src_uv = self.buf.ptr + u64::from(self.pitch) * u64::from(self.height);
1299 let dst_uv = dst.buf.ptr + u64::from(pitch) * u64::from(height);
1300 let (src_pw, src_ph) = (self.width / 2, self.height / 2);
1301 let (dst_pw, dst_ph) = (width / 2, height / 2);
1302 unsafe {
1304 stream
1305 .launch_builder(&kernels.chroma)
1306 .arg(&src_uv)
1307 .arg(&self.pitch)
1308 .arg(&src_pw)
1309 .arg(&src_ph)
1310 .arg(&dst_uv)
1311 .arg(&pitch)
1312 .arg(&dst_pw)
1313 .arg(&dst_ph)
1314 .launch(LaunchConfig {
1315 grid_dim: grid(dst_pw, dst_ph),
1316 block_dim: block,
1317 shared_mem_bytes: 0,
1318 })
1319 }
1320 .map_err(|e| launch_err("chroma", e))?;
1321
1322 stream
1325 .synchronize()
1326 .map_err(|e| Error::Codec(anyhow::anyhow!("CUDA resize sync: {e:?}")))?;
1327 Ok(dst)
1328 }
1329 }
1330}
1331
1332#[cfg(target_os = "windows")]
1333pub mod d3d11 {
1334 use std::ffi::c_void;
1338 use std::ptr;
1339
1340 use windows::Win32::Foundation::HMODULE;
1341 use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_HARDWARE;
1342 use windows::Win32::Graphics::Direct3D10::ID3D10Multithread;
1343 use windows::Win32::Graphics::Direct3D11::{
1344 D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE, D3D11_BIND_VIDEO_ENCODER, D3D11_BOX,
1345 D3D11_CPU_ACCESS_READ, D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_CREATE_DEVICE_VIDEO_SUPPORT,
1346 D3D11_FORMAT_SUPPORT, D3D11_FORMAT_SUPPORT_RENDER_TARGET, D3D11_FORMAT_SUPPORT_SHADER_SAMPLE,
1347 D3D11_FORMAT_SUPPORT_VIDEO_ENCODER, D3D11_MAP_READ, D3D11_MAPPED_SUBRESOURCE, D3D11_SDK_VERSION,
1348 D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, D3D11_USAGE_STAGING, D3D11CreateDevice, ID3D11Device,
1349 ID3D11DeviceContext, ID3D11Texture2D,
1350 };
1351 use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT, DXGI_SAMPLE_DESC};
1352 use windows::Win32::Media::MediaFoundation::{IMFDXGIBuffer, IMFSample};
1353 use windows::core::Interface;
1354
1355 use super::I420;
1356 use crate::Error;
1357
1358 fn err(ctx: &str, e: windows::core::Error) -> Error {
1359 Error::Codec(anyhow::anyhow!("{ctx}: {e}"))
1360 }
1361
1362 pub(crate) fn create_device() -> Result<ID3D11Device, Error> {
1367 let mut device: Option<ID3D11Device> = None;
1368 unsafe {
1369 D3D11CreateDevice(
1370 None,
1371 D3D_DRIVER_TYPE_HARDWARE,
1372 HMODULE::default(),
1373 D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_VIDEO_SUPPORT,
1374 None,
1375 D3D11_SDK_VERSION,
1376 Some(&mut device),
1377 None,
1378 None,
1379 )
1380 .map_err(|e| err("D3D11CreateDevice", e))?;
1381 }
1382 let device = device.ok_or_else(|| Error::Codec(anyhow::anyhow!("D3D11CreateDevice returned null")))?;
1383
1384 let multithread = device
1385 .cast::<ID3D10Multithread>()
1386 .map_err(|e| err("query ID3D10Multithread", e))?;
1387 unsafe {
1388 let _ = multithread.SetMultithreadProtected(true);
1389 }
1390 Ok(device)
1391 }
1392
1393 pub struct Texture {
1399 pub(crate) device: ID3D11Device,
1400 pub(crate) texture: ID3D11Texture2D,
1401 pub(crate) width: u32,
1402 pub(crate) height: u32,
1403 }
1404
1405 impl Texture {
1406 pub(crate) fn copy_from_sample(
1429 device: &ID3D11Device,
1430 sample: &IMFSample,
1431 width: u32,
1432 height: u32,
1433 ) -> Result<Self, Error> {
1434 let (source, subresource) = resolve(sample)?;
1435
1436 let mut desc = D3D11_TEXTURE2D_DESC::default();
1438 unsafe { source.GetDesc(&mut desc) };
1439 let texture = alloc(device, width, height, desc.Format)?;
1440
1441 let region = D3D11_BOX {
1444 left: 0,
1445 top: 0,
1446 front: 0,
1447 right: width,
1448 bottom: height,
1449 back: 1,
1450 };
1451 let context = unsafe { device.GetImmediateContext() }.map_err(|e| err("GetImmediateContext", e))?;
1452 unsafe {
1453 context.CopySubresourceRegion(&texture, 0, 0, 0, 0, &source, subresource, Some(®ion));
1454 }
1455
1456 Ok(Self {
1457 device: device.clone(),
1458 texture,
1459 width,
1460 height,
1461 })
1462 }
1463
1464 pub fn texture(&self) -> &ID3D11Texture2D {
1473 &self.texture
1474 }
1475
1476 pub fn device(&self) -> &ID3D11Device {
1479 &self.device
1480 }
1481
1482 pub fn width(&self) -> u32 {
1484 self.width
1485 }
1486
1487 pub fn height(&self) -> u32 {
1489 self.height
1490 }
1491
1492 pub(crate) fn download_i420(&self) -> Result<I420, Error> {
1496 let context = unsafe { self.device.GetImmediateContext() }.map_err(|e| err("GetImmediateContext", e))?;
1497
1498 let mut desc = D3D11_TEXTURE2D_DESC::default();
1500 unsafe { self.texture.GetDesc(&mut desc) };
1501 desc.ArraySize = 1;
1502 desc.MipLevels = 1;
1503 desc.Usage = D3D11_USAGE_STAGING;
1504 desc.BindFlags = 0;
1505 desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ.0 as u32;
1506 desc.MiscFlags = 0;
1507
1508 let mut staging: Option<ID3D11Texture2D> = None;
1509 unsafe {
1510 self.device
1511 .CreateTexture2D(&desc, None, Some(&mut staging))
1512 .map_err(|e| err("CreateTexture2D (staging)", e))?;
1513 }
1514 let staging = staging.ok_or_else(|| Error::Codec(anyhow::anyhow!("CreateTexture2D returned null")))?;
1515
1516 unsafe {
1517 context.CopySubresourceRegion(&staging, 0, 0, 0, 0, &self.texture, 0, None);
1518 }
1519
1520 let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
1521 unsafe {
1522 context
1523 .Map(&staging, 0, D3D11_MAP_READ, 0, Some(&mut mapped))
1524 .map_err(|e| err("Map (staging)", e))?;
1525 }
1526 let _guard = UnmapGuard {
1527 context: &context,
1528 resource: &staging,
1529 };
1530
1531 let (w, h) = (self.width as usize, self.height as usize);
1532 let (cw, ch) = (w / 2, h / 2);
1533 let pitch = mapped.RowPitch as usize;
1534 let base = mapped.pData as *const u8;
1535 let tex_height = desc.Height as usize;
1541
1542 let mut data = vec![0u8; I420::len(self.width, self.height)];
1543 let (luma, chroma) = data.split_at_mut(w * h);
1544 let (u_plane, v_plane) = chroma.split_at_mut(cw * ch);
1545
1546 for row in 0..h {
1548 unsafe {
1549 ptr::copy_nonoverlapping(base.add(row * pitch), luma[row * w..].as_mut_ptr(), w);
1550 }
1551 }
1552 let uv_base = unsafe { base.add(pitch * tex_height) };
1554 for row in 0..ch {
1555 let src = unsafe { uv_base.add(row * pitch) };
1556 for col in 0..cw {
1557 unsafe {
1558 u_plane[row * cw + col] = *src.add(col * 2);
1559 v_plane[row * cw + col] = *src.add(col * 2 + 1);
1560 }
1561 }
1562 }
1563
1564 Ok(I420 {
1565 width: self.width,
1566 height: self.height,
1567 data,
1568 color: None,
1571 })
1572 }
1573 }
1574
1575 fn alloc(device: &ID3D11Device, width: u32, height: u32, format: DXGI_FORMAT) -> Result<ID3D11Texture2D, Error> {
1578 let desc = D3D11_TEXTURE2D_DESC {
1579 Width: width,
1580 Height: height,
1581 MipLevels: 1,
1582 ArraySize: 1,
1583 Format: format,
1584 SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 },
1585 Usage: D3D11_USAGE_DEFAULT,
1586 BindFlags: bind_flags(device, format),
1587 CPUAccessFlags: 0,
1588 MiscFlags: 0,
1589 };
1590
1591 let mut texture: Option<ID3D11Texture2D> = None;
1592 unsafe {
1593 device
1594 .CreateTexture2D(&desc, None, Some(&mut texture))
1595 .map_err(|e| err("CreateTexture2D", e))?;
1596 }
1597 texture.ok_or_else(|| Error::Codec(anyhow::anyhow!("CreateTexture2D returned null")))
1598 }
1599
1600 fn resolve(sample: &IMFSample) -> Result<(ID3D11Texture2D, u32), Error> {
1603 let buffer = unsafe { sample.GetBufferByIndex(0) }.map_err(|e| err("get sample buffer", e))?;
1604 let dxgi = buffer
1605 .cast::<IMFDXGIBuffer>()
1606 .map_err(|e| err("sample buffer is not a DXGI surface", e))?;
1607
1608 let mut raw: *mut c_void = ptr::null_mut();
1610 unsafe {
1611 dxgi.GetResource(&ID3D11Texture2D::IID, &mut raw)
1612 .map_err(|e| err("get DXGI resource", e))?;
1613 }
1614 let texture = unsafe { ID3D11Texture2D::from_raw(raw) };
1615 let subresource = unsafe { dxgi.GetSubresourceIndex() }.map_err(|e| err("get subresource index", e))?;
1616 Ok((texture, subresource))
1617 }
1618
1619 fn bind_flags(device: &ID3D11Device, format: DXGI_FORMAT) -> u32 {
1629 let support = unsafe { device.CheckFormatSupport(format) }.unwrap_or_default();
1630 let supports = |flag: D3D11_FORMAT_SUPPORT| support & flag.0 as u32 != 0;
1631
1632 let mut flags = 0;
1633 if supports(D3D11_FORMAT_SUPPORT_SHADER_SAMPLE) {
1634 flags |= D3D11_BIND_SHADER_RESOURCE.0 as u32;
1635 }
1636 if supports(D3D11_FORMAT_SUPPORT_RENDER_TARGET) {
1637 flags |= D3D11_BIND_RENDER_TARGET.0 as u32;
1638 }
1639 if supports(D3D11_FORMAT_SUPPORT_VIDEO_ENCODER) {
1640 flags |= D3D11_BIND_VIDEO_ENCODER.0 as u32;
1641 }
1642 flags
1643 }
1644
1645 struct UnmapGuard<'a> {
1646 context: &'a ID3D11DeviceContext,
1647 resource: &'a ID3D11Texture2D,
1648 }
1649
1650 impl Drop for UnmapGuard<'_> {
1651 fn drop(&mut self) {
1652 unsafe { self.context.Unmap(self.resource, 0) };
1653 }
1654 }
1655}
1656
1657#[cfg(test)]
1658mod tests {
1659 #[test]
1668 fn only_a_real_color_conversion_labels_its_output() {
1669 use super::I420;
1670 use crate::{Color, Size};
1671
1672 let size = Size::new(64, 64);
1673 let rgba = vec![0u8; size.pixels() as usize * 4];
1674 let converted = I420::from_rgba(&rgba, size.width * 4, size.width, size.height).expect("rgba to i420");
1675 assert_eq!(
1676 converted.color(),
1677 Some(Color::Bt601Limited),
1678 "an RGB conversion knows the matrix it used"
1679 );
1680
1681 let resized = converted.resize(32, 32).expect("resize");
1683 assert_eq!(resized.color(), Some(Color::Bt601Limited), "resize preserves the space");
1684
1685 let raw = I420::new(64, 64, vec![0; I420::len(64, 64)]).expect("i420");
1687 assert_eq!(raw.color(), None);
1688 assert_eq!(raw.with_color(Color::Bt709Full).color(), Some(Color::Bt709Full));
1689 }
1690
1691 #[cfg(target_os = "linux")]
1696 #[test]
1697 fn yuyv_capture_keeps_its_color_space_open() {
1698 let (width, height) = (1280, 720);
1699 let yuyv = vec![0u8; width as usize * height as usize * 2];
1701 let frame = super::I420::from_yuyv(&yuyv, width * 2, width, height).expect("yuyv to i420");
1702 assert_eq!(frame.color(), None, "a chroma resample names no color space");
1703 }
1704
1705 #[test]
1709 fn i420_new_rejects_a_short_buffer() {
1710 use super::I420;
1711
1712 assert!(I420::new(64, 32, vec![0; I420::len(64, 32)]).is_ok());
1713 assert!(I420::new(64, 32, vec![0; I420::len(64, 32) - 1]).is_err());
1714 assert!(I420::new(64, 32, Vec::new()).is_err());
1715 assert!(I420::new(63, 32, vec![0; I420::len(63, 32)]).is_err());
1717 assert!(I420::new(0, 32, Vec::new()).is_err());
1718 }
1719
1720 use super::{Frame, I420, Surface};
1721 use crate::Size;
1722
1723 #[test]
1726 fn surface_rgba_rejects_a_mismatched_buffer() {
1727 let ok = vec![0x80u8; 64 * 32 * 4];
1728 assert!(Surface::rgba(&ok, Size::new(64, 32)).is_ok());
1729 assert!(Surface::rgba(&ok[..ok.len() - 4], Size::new(64, 32)).is_err());
1730 assert!(Surface::rgba(&ok, Size::new(32, 32)).is_err());
1731 assert!(Surface::rgba(&ok, Size::new(0, 32)).is_err());
1732 }
1733
1734 #[test]
1742 fn rgb_conversion_follows_the_size_heuristic() {
1743 use yuv::{YuvPlanarImage, yuv420_to_rgba};
1744
1745 use crate::Color;
1746
1747 let red = |size: Size| {
1748 let rgba = [255u8, 0, 0, 255].repeat(size.pixels() as usize);
1749 I420::from_rgba(&rgba, size.width * 4, size.width, size.height).unwrap()
1750 };
1751
1752 let decode = |i420: &I420| {
1755 let (w, h) = (i420.width, i420.height);
1756 let (range, matrix) = Color::infer(Size::new(w, h)).yuv();
1757 let planar = YuvPlanarImage {
1758 y_plane: i420.y(),
1759 y_stride: w,
1760 u_plane: i420.u(),
1761 u_stride: w / 2,
1762 v_plane: i420.v(),
1763 v_stride: w / 2,
1764 width: w,
1765 height: h,
1766 };
1767 let mut rgba = vec![0u8; (w * h * 4) as usize];
1768 yuv420_to_rgba(&planar, &mut rgba, w * 4, range, matrix).unwrap();
1769 let px = ((h / 2 * w + w / 2) * 4) as usize;
1770 [rgba[px], rgba[px + 1], rgba[px + 2]]
1771 };
1772
1773 for (size, expected) in [
1774 (Size::new(720, 480), Color::Bt601Limited),
1775 (Size::new(720, 576), Color::Bt601Limited),
1776 (Size::new(1280, 720), Color::Bt709Limited),
1777 (Size::new(1920, 1080), Color::Bt709Limited),
1778 ] {
1779 let i420 = red(size);
1780 assert_eq!(i420.color(), Some(expected), "{size} reported color");
1781
1782 let rgb = decode(&i420);
1785 assert!(
1786 rgb[1] <= 2 && rgb[2] <= 2,
1787 "{size} red came back as {rgb:?}, so the matrix and the label disagree"
1788 );
1789 }
1790 }
1791
1792 #[test]
1795 fn frame_size_follows_the_surface() {
1796 let rgba = vec![0x80u8; 64 * 32 * 4];
1797 let surface = Surface::rgba(&rgba, Size::new(64, 32)).unwrap();
1798
1799 let frame = Frame::new(surface, moq_net::Timestamp::from_micros(1234).unwrap());
1800 assert_eq!(frame.size(), Size::new(64, 32));
1801
1802 let scaled = frame.resize(Size::new(32, 16)).unwrap();
1803 assert_eq!(scaled.size(), Size::new(32, 16));
1804 assert_eq!(scaled.timestamp, frame.timestamp);
1805 }
1806
1807 #[cfg(target_os = "macos")]
1811 #[test]
1812 fn into_pixel_buffer_uploads_a_cpu_frame() {
1813 use objc2_core_video::{CVPixelBufferGetHeight, CVPixelBufferGetWidth};
1814
1815 let i420 = I420::new(64, 32, vec![0x80; I420::len(64, 32)]).unwrap();
1816 let frame = Frame::new(Surface::I420(i420), moq_net::Timestamp::from_micros(0).unwrap());
1817
1818 let buffer = frame.surface.into_pixel_buffer().expect("upload a CPU frame");
1819 assert_eq!(CVPixelBufferGetWidth(&buffer), 64);
1820 assert_eq!(CVPixelBufferGetHeight(&buffer), 32);
1821 }
1822
1823 fn gradient_i420(width: u32, height: u32) -> I420 {
1826 let (w, h) = (width as usize, height as usize);
1827 let (cw, ch) = (w / 2, h / 2);
1828 let mut data = vec![0u8; I420::len(width, height)];
1829 let (y, chroma) = data.split_at_mut(w * h);
1830 let (u, v) = chroma.split_at_mut(cw * ch);
1831 for row in 0..h {
1832 for col in 0..w {
1833 y[row * w + col] = ((col * 255) / w) as u8;
1834 }
1835 }
1836 for row in 0..ch {
1837 for col in 0..cw {
1838 u[row * cw + col] = ((row * 255) / ch) as u8;
1839 v[row * cw + col] = (((row + col) * 255) / (ch + cw)) as u8;
1840 }
1841 }
1842 I420 {
1843 width,
1844 height,
1845 data,
1846 color: None,
1847 }
1848 }
1849
1850 fn mae(a: &[u8], b: &[u8]) -> u64 {
1852 assert_eq!(a.len(), b.len());
1853 a.iter().zip(b).map(|(x, y)| x.abs_diff(*y) as u64).sum::<u64>() / a.len() as u64
1854 }
1855
1856 #[test]
1859 fn i420_resize_follows_gradients() {
1860 let src = gradient_i420(320, 240);
1861 let dst = src.resize(128, 96).unwrap();
1862 assert_eq!((dst.width, dst.height), (128, 96));
1863
1864 let expected = gradient_i420(128, 96);
1866 assert!(mae(dst.y(), expected.y()) < 4, "luma ramp drifted");
1867 assert!(mae(dst.u(), expected.u()) < 4, "u ramp drifted");
1868 assert!(mae(dst.v(), expected.v()) < 4, "v ramp drifted");
1869 }
1870
1871 #[cfg(target_os = "macos")]
1874 #[test]
1875 fn pixel_buffer_resize_matches_cpu() {
1876 let src_i420 = gradient_i420(320, 240);
1877 let src = Surface::PixelBuffer(nv12_surface(&src_i420));
1878 let scaled = src.resize(Size::new(160, 120)).unwrap();
1879 let Surface::PixelBuffer(scaled) = scaled else {
1880 panic!("VideoToolbox resize downloaded to the CPU");
1881 };
1882
1883 let gpu = scaled.download_i420().unwrap();
1884 let cpu = src_i420.resize(160, 120).unwrap();
1885
1886 assert_eq!((gpu.width, gpu.height), (160, 120));
1887 assert!(mae(gpu.y(), cpu.y()) < 4, "GPU and CPU luma disagree");
1888 assert!(mae(gpu.u(), cpu.u()) < 4, "GPU and CPU u disagree");
1889 assert!(mae(gpu.v(), cpu.v()) < 4, "GPU and CPU v disagree");
1890 }
1891
1892 #[cfg(target_os = "macos")]
1895 fn nv12_surface(frame: &I420) -> super::macos::PixelBuffer {
1896 use std::ptr::{self, NonNull};
1897
1898 use objc2_core_foundation::CFRetained;
1899 use objc2_core_video::{
1900 CVPixelBuffer, CVPixelBufferCreate, CVPixelBufferGetBaseAddressOfPlane, CVPixelBufferGetBytesPerRowOfPlane,
1901 CVPixelBufferLockBaseAddress, CVPixelBufferLockFlags, CVPixelBufferUnlockBaseAddress,
1902 kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
1903 };
1904
1905 let mut raw: *mut CVPixelBuffer = ptr::null_mut();
1906 let status = unsafe {
1907 CVPixelBufferCreate(
1908 None,
1909 frame.width as usize,
1910 frame.height as usize,
1911 kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
1912 None,
1913 NonNull::new(&mut raw).expect("stack pointer is non-null"),
1914 )
1915 };
1916 assert_eq!(status, 0, "CVPixelBufferCreate failed");
1917 let buffer = unsafe { CFRetained::from_raw(NonNull::new(raw).expect("CoreVideo returned a buffer")) };
1918
1919 let flags = CVPixelBufferLockFlags(0);
1920 assert_eq!(unsafe { CVPixelBufferLockBaseAddress(&buffer, flags) }, 0);
1921 let width = frame.width as usize;
1922 let height = frame.height as usize;
1923 let y_base = CVPixelBufferGetBaseAddressOfPlane(&buffer, 0) as *mut u8;
1924 let y_stride = CVPixelBufferGetBytesPerRowOfPlane(&buffer, 0);
1925 for row in 0..height {
1926 unsafe {
1927 ptr::copy_nonoverlapping(frame.y()[row * width..].as_ptr(), y_base.add(row * y_stride), width);
1928 }
1929 }
1930
1931 let (chroma_width, chroma_height) = (width / 2, height / 2);
1932 let uv_base = CVPixelBufferGetBaseAddressOfPlane(&buffer, 1) as *mut u8;
1933 let uv_stride = CVPixelBufferGetBytesPerRowOfPlane(&buffer, 1);
1934 for row in 0..chroma_height {
1935 let output = unsafe { uv_base.add(row * uv_stride) };
1936 for col in 0..chroma_width {
1937 unsafe {
1938 *output.add(col * 2) = frame.u()[row * chroma_width + col];
1939 *output.add(col * 2 + 1) = frame.v()[row * chroma_width + col];
1940 }
1941 }
1942 }
1943 unsafe { CVPixelBufferUnlockBaseAddress(&buffer, flags) };
1944
1945 super::macos::PixelBuffer::new(buffer, frame.width, frame.height)
1946 }
1947
1948 #[cfg(all(target_os = "linux", feature = "nvdec"))]
1951 #[test]
1952 fn cuda_resize_matches_cpu() {
1953 use std::sync::Arc;
1954
1955 use cudarc::driver::{CudaContext, result};
1956
1957 use super::cuda;
1958
1959 if unsafe { libloading::Library::new("libcuda.so.1") }.is_err() {
1961 return;
1962 }
1963 let Ok(ctx): Result<Arc<CudaContext>, _> = CudaContext::new(0) else {
1964 return;
1965 };
1966
1967 let (w, h) = (322u32, 242u32); let src_i420 = gradient_i420(w, h);
1969
1970 let pitch = 512u32;
1972 let frame = cuda::Frame::alloc(&ctx, w, h, pitch).unwrap();
1973 let mut host = vec![0u8; pitch as usize * h as usize * 3 / 2];
1974 for row in 0..h as usize {
1975 let dst = row * pitch as usize;
1976 host[dst..dst + w as usize].copy_from_slice(&src_i420.y()[row * w as usize..(row + 1) * w as usize]);
1977 }
1978 let (cw, ch) = (w as usize / 2, h as usize / 2);
1979 for row in 0..ch {
1980 let dst = (h as usize + row) * pitch as usize;
1981 for col in 0..cw {
1982 host[dst + 2 * col] = src_i420.u()[row * cw + col];
1983 host[dst + 2 * col + 1] = src_i420.v()[row * cw + col];
1984 }
1985 }
1986 unsafe { result::memcpy_htod_sync(frame.device_ptr(), &host) }.unwrap();
1988
1989 let scaled = frame.resize(160, 120).unwrap();
1990 let gpu = scaled.download_i420().unwrap();
1991 let cpu = src_i420.resize(160, 120).unwrap();
1992
1993 assert_eq!((gpu.width, gpu.height), (160, 120));
1994 assert!(mae(gpu.y(), cpu.y()) < 4, "GPU and CPU luma disagree");
1995 assert!(mae(gpu.u(), cpu.u()) < 4, "GPU and CPU u disagree");
1996 assert!(mae(gpu.v(), cpu.v()) < 4, "GPU and CPU v disagree");
1997 }
1998}