1#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
88
89#[cfg(all(coverage, target_os = "linux"))]
93#[used]
94#[link_section = ".init_array"]
95static __EDGEFIRST_COV_INSTALL: extern "C" fn() = {
96 extern "C" fn ctor() {
97 edgefirst_tensor::covguard::install();
98 }
99 ctor
100};
101
102pub const GPU_DMA_BUF_PITCH_ALIGNMENT_BYTES: usize = 64;
115
116pub fn align_width_for_gpu_pitch(width: usize, bpp: usize) -> usize {
154 if bpp == 0 || width == 0 {
155 return width;
156 }
157
158 let Some(lcm_alignment) = checked_num_integer_lcm(GPU_DMA_BUF_PITCH_ALIGNMENT_BYTES, bpp)
167 else {
168 log::warn!(
169 "align_width_for_gpu_pitch: lcm({GPU_DMA_BUF_PITCH_ALIGNMENT_BYTES}, {bpp}) \
170 overflows usize, returning unaligned width {width}"
171 );
172 return width;
173 };
174 if lcm_alignment == 0 {
175 return width;
176 }
177
178 debug_assert_eq!(lcm_alignment % bpp, 0);
179 let width_alignment = lcm_alignment / bpp;
180 if width_alignment == 0 {
181 return width;
182 }
183
184 let remainder = width % width_alignment;
185 if remainder == 0 {
186 return width;
187 }
188
189 let pad = width_alignment - remainder;
190 match width.checked_add(pad) {
191 Some(aligned) => aligned,
192 None => {
193 log::warn!(
194 "align_width_for_gpu_pitch: width {width} + pad {pad} overflows usize, \
195 returning unaligned (caller should use a smaller width or pre-aligned size)"
196 );
197 width
198 }
199 }
200}
201
202#[cfg(target_os = "linux")]
211pub(crate) fn align_pitch_bytes_to_gpu_alignment(min_pitch_bytes: usize) -> Option<usize> {
212 let alignment = GPU_DMA_BUF_PITCH_ALIGNMENT_BYTES;
213 if min_pitch_bytes == 0 {
214 return Some(0);
215 }
216 let remainder = min_pitch_bytes % alignment;
217 if remainder == 0 {
218 return Some(min_pitch_bytes);
219 }
220 min_pitch_bytes.checked_add(alignment - remainder)
221}
222
223fn checked_num_integer_lcm(a: usize, b: usize) -> Option<usize> {
226 if a == 0 || b == 0 {
227 return Some(0);
228 }
229 let g = num_integer_gcd(a, b);
230 (a / g).checked_mul(b)
233}
234
235fn num_integer_gcd(a: usize, b: usize) -> usize {
236 if b == 0 {
237 a
238 } else {
239 num_integer_gcd(b, a % b)
240 }
241}
242
243pub fn primary_plane_bpp(format: PixelFormat, elem: usize) -> Option<usize> {
259 use edgefirst_tensor::PixelLayout;
260 match format.layout() {
261 PixelLayout::Packed => Some(format.channels() * elem),
262 PixelLayout::Planar => Some(elem),
263 PixelLayout::SemiPlanar => Some(elem),
267 _ => None,
270 }
271}
272
273#[cfg(all(target_os = "linux", test))]
286pub(crate) fn padded_dma_pitch_for(
287 fmt: PixelFormat,
288 width: usize,
289 memory: &Option<TensorMemory>,
290) -> Option<usize> {
291 match memory {
301 Some(TensorMemory::DmaBuf) => {}
302 None if edgefirst_tensor::is_dma_available() => {}
303 _ => return None,
304 }
305 if fmt.layout() != PixelLayout::Packed {
309 return None;
310 }
311 let bpp = primary_plane_bpp(fmt, 1)?;
312 let natural = width.checked_mul(bpp)?;
313 let aligned = align_pitch_bytes_to_gpu_alignment(natural)?;
314 if aligned > natural {
315 Some(aligned)
316 } else {
317 None
318 }
319}
320
321pub use cpu::CPUProcessor;
322#[cfg(feature = "codec")]
323pub use edgefirst_codec as codec;
324
325#[cfg(test)]
326use edgefirst_tensor::ProtoLayout;
327#[doc(inline)]
328pub use edgefirst_tensor::Region;
329#[cfg(any(
330 test,
331 all(any(target_os = "linux", target_os = "windows"), feature = "opengl")
332))]
333use edgefirst_tensor::Tensor;
334use edgefirst_tensor::{
335 DType, PixelFormat, PixelLayout, TensorDyn, TensorMemory, TensorTrait as _,
336};
337use edgefirst_tensor::{DetectBox, ProtoData, Segmentation};
338use enum_dispatch::enum_dispatch;
339pub use error::{Error, Result};
340#[cfg(target_os = "linux")]
341pub use g2d::G2DProcessor;
342#[cfg(all(
345 any(
346 target_os = "linux",
347 target_os = "macos",
348 target_os = "ios",
349 target_os = "android",
350 target_os = "windows"
351 ),
352 feature = "opengl"
353))]
354pub use opengl_headless::CompletionFence;
355#[cfg(all(
356 any(
357 target_os = "linux",
358 target_os = "macos",
359 target_os = "ios",
360 target_os = "android",
361 target_os = "windows"
362 ),
363 feature = "opengl"
364))]
365pub use opengl_headless::EglDisplayKind;
366#[cfg(all(
367 any(
368 target_os = "linux",
369 target_os = "macos",
370 target_os = "ios",
371 target_os = "android",
372 target_os = "windows"
373 ),
374 feature = "opengl"
375))]
376pub use opengl_headless::GLProcessorThreaded;
377#[cfg(all(
378 any(
379 target_os = "linux",
380 target_os = "macos",
381 target_os = "ios",
382 target_os = "android",
383 target_os = "windows"
384 ),
385 feature = "opengl"
386))]
387pub use opengl_headless::Int8InterpolationMode;
388#[cfg(target_os = "linux")]
389#[cfg(feature = "opengl")]
390pub use opengl_headless::{probe_egl_displays, EglDisplayInfo};
391#[cfg(all(
395 any(
396 target_os = "linux",
397 target_os = "macos",
398 target_os = "ios",
399 target_os = "android",
400 target_os = "windows"
401 ),
402 feature = "opengl"
403))]
404pub use opengl_headless::{CacheStats, ConvertStats, GlCacheStats};
405use std::{fmt::Display, time::Instant};
406
407mod colorimetry;
408mod cpu;
409mod error;
410mod g2d;
411#[path = "gl/mod.rs"]
412mod opengl_headless;
413#[cfg(test)]
414mod test_support;
415mod tiling;
416pub use tiling::{tile_grid, TilePlacement, TileSpec, TilingConfig};
417
418#[derive(Debug, Clone, Copy, PartialEq, Eq)]
422pub enum Rotation {
423 None = 0,
424 Clockwise90 = 1,
425 Rotate180 = 2,
426 CounterClockwise90 = 3,
427}
428impl Rotation {
429 pub fn from_degrees_clockwise(angle: usize) -> Rotation {
442 match angle.rem_euclid(360) {
443 0 => Rotation::None,
444 90 => Rotation::Clockwise90,
445 180 => Rotation::Rotate180,
446 270 => Rotation::CounterClockwise90,
447 _ => panic!("rotation angle is not a multiple of 90"),
448 }
449 }
450}
451
452#[derive(Debug, Clone, Copy, PartialEq, Eq)]
453pub enum Flip {
454 None = 0,
455 Vertical = 1,
456 Horizontal = 2,
457}
458
459#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
461pub enum ColorMode {
462 #[default]
467 Class,
468 Instance,
473 Track,
476}
477
478impl ColorMode {
479 #[inline]
481 pub fn index(self, idx: usize, label: usize) -> usize {
482 match self {
483 ColorMode::Class => label,
484 ColorMode::Instance | ColorMode::Track => idx,
485 }
486 }
487}
488
489#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
510pub enum MaskResolution {
511 #[default]
513 Proto,
514 Scaled {
518 width: u32,
520 height: u32,
522 },
523}
524
525#[derive(Debug, Clone, Copy)]
541pub struct MaskOverlay<'a> {
542 pub background: Option<&'a TensorDyn>,
546 pub opacity: f32,
547 pub letterbox: Option<[f32; 4]>,
557 pub color_mode: ColorMode,
558}
559
560impl Default for MaskOverlay<'_> {
561 fn default() -> Self {
562 Self {
563 background: None,
564 opacity: 1.0,
565 letterbox: None,
566 color_mode: ColorMode::Class,
567 }
568 }
569}
570
571impl<'a> MaskOverlay<'a> {
572 pub fn new() -> Self {
573 Self::default()
574 }
575
576 pub fn with_background(mut self, bg: &'a TensorDyn) -> Self {
584 self.background = Some(bg);
585 self
586 }
587
588 pub fn with_opacity(mut self, opacity: f32) -> Self {
589 self.opacity = opacity.clamp(0.0, 1.0);
590 self
591 }
592
593 pub fn with_color_mode(mut self, mode: ColorMode) -> Self {
594 self.color_mode = mode;
595 self
596 }
597
598 pub fn with_letterbox_crop(
608 mut self,
609 crop: &Crop,
610 src_w: usize,
611 src_h: usize,
612 model_w: usize,
613 model_h: usize,
614 ) -> Self {
615 if let Ok(resolved) = crop.resolve(src_w, src_h, model_w, model_h) {
618 if let Some(r) = resolved.dst_rect {
619 self.letterbox = Some([
620 r.left as f32 / model_w as f32,
621 r.top as f32 / model_h as f32,
622 (r.left + r.width) as f32 / model_w as f32,
623 (r.top + r.height) as f32 / model_h as f32,
624 ]);
625 }
626 }
627 self
628 }
629}
630
631fn copy_segmentation(s: &Segmentation, bbox: [f32; 4]) -> Result<Segmentation> {
637 use edgefirst_tensor::TensorMapTrait as _;
638 let src = s
639 .segmentation
640 .as_typed::<u8>()
641 .ok_or_else(|| Error::InvalidShape("segmentation must be a U8 tensor".into()))?;
642 let map = src.map_read()?;
643 let dst = edgefirst_tensor::Tensor::<u8>::new(
650 s.segmentation.shape(),
651 Some(edgefirst_tensor::TensorMemory::Mem),
652 None,
653 )
654 .map_err(|e| Error::InvalidShape(format!("segmentation copy: {e}")))?;
655 {
656 let mut dmap = dst.map_mut()?;
657 dmap.as_mut_slice().copy_from_slice(map.as_slice());
658 }
659 Ok(Segmentation {
660 xmin: bbox[0],
661 ymin: bbox[1],
662 xmax: bbox[2],
663 ymax: bbox[3],
664 segmentation: dst.into(),
665 })
666}
667
668#[inline]
681fn unletter_bbox(bbox: DetectBox, lb: [f32; 4]) -> DetectBox {
682 DetectBox {
683 bbox: edgefirst_tensor::unletter_norm(bbox.bbox, lb),
684 ..bbox
685 }
686}
687
688#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
690pub enum Fit {
691 #[default]
693 Stretch,
694 Letterbox { pad: [u8; 4] },
698}
699
700#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
705pub struct Crop {
706 pub source: Option<Region>,
708 pub fit: Fit,
710}
711
712impl Crop {
713 pub fn new() -> Self {
715 Self::default()
716 }
717
718 pub fn no_crop() -> Self {
720 Self::default()
721 }
722
723 pub fn letterbox(pad: [u8; 4]) -> Self {
726 Self {
727 source: None,
728 fit: Fit::Letterbox { pad },
729 }
730 }
731
732 pub fn with_source(mut self, source: Option<Region>) -> Self {
734 self.source = source;
735 self
736 }
737
738 pub fn with_fit(mut self, fit: Fit) -> Self {
740 self.fit = fit;
741 self
742 }
743
744 pub(crate) fn resolve(
750 &self,
751 src_w: usize,
752 src_h: usize,
753 dst_w: usize,
754 dst_h: usize,
755 ) -> Result<ResolvedCrop, Error> {
756 let src_rect = self.source.map(region_to_rect);
757 let (sw, sh) = match self.source {
760 Some(r) => (r.width, r.height),
761 None => (src_w, src_h),
762 };
763 let resolved = match self.fit {
764 Fit::Stretch => ResolvedCrop {
765 src_rect,
766 dst_rect: None,
767 dst_color: None,
768 },
769 Fit::Letterbox { pad } => ResolvedCrop {
770 src_rect,
771 dst_rect: Some(letterbox_rect(sw, sh, dst_w, dst_h)),
772 dst_color: Some(pad),
773 },
774 };
775 resolved.check_crop_dims(src_w, src_h, dst_w, dst_h)?;
776 Ok(resolved)
777 }
778
779 pub fn check_crop_dyn(
781 &self,
782 src: &edgefirst_tensor::TensorDyn,
783 dst: &edgefirst_tensor::TensorDyn,
784 ) -> Result<(), Error> {
785 self.resolve(
786 src.width().unwrap_or(0),
787 src.height().unwrap_or(0),
788 dst.width().unwrap_or(0),
789 dst.height().unwrap_or(0),
790 )
791 .map(|_| ())
792 }
793}
794
795#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
799pub(crate) struct ResolvedCrop {
800 pub(crate) src_rect: Option<Rect>,
801 pub(crate) dst_rect: Option<Rect>,
802 pub(crate) dst_color: Option<[u8; 4]>,
803}
804
805impl ResolvedCrop {
806 #[allow(dead_code)] pub(crate) fn no_crop() -> Self {
809 Self::default()
810 }
811
812 pub(crate) fn check_crop_dims(
814 &self,
815 src_w: usize,
816 src_h: usize,
817 dst_w: usize,
818 dst_h: usize,
819 ) -> Result<(), Error> {
820 let src_ok = self
821 .src_rect
822 .is_none_or(|r| r.left + r.width <= src_w && r.top + r.height <= src_h);
823 let dst_ok = self
824 .dst_rect
825 .is_none_or(|r| r.left + r.width <= dst_w && r.top + r.height <= dst_h);
826 match (src_ok, dst_ok) {
827 (true, true) => Ok(()),
828 (true, false) => Err(Error::CropInvalid(format!(
829 "Dest crop invalid: {:?}",
830 self.dst_rect
831 ))),
832 (false, true) => Err(Error::CropInvalid(format!(
833 "Src crop invalid: {:?}",
834 self.src_rect
835 ))),
836 (false, false) => Err(Error::CropInvalid(format!(
837 "Dest and Src crop invalid: {:?} {:?}",
838 self.dst_rect, self.src_rect
839 ))),
840 }
841 }
842}
843
844fn region_to_rect(r: Region) -> Rect {
846 Rect {
847 left: r.x,
848 top: r.y,
849 width: r.width,
850 height: r.height,
851 }
852}
853
854fn letterbox_rect(sw: usize, sh: usize, dw: usize, dh: usize) -> Rect {
858 if sw == 0 || sh == 0 {
859 return Rect::new(0, 0, dw, dh);
860 }
861 let src_aspect = sw as f64 / sh as f64;
862 let dst_aspect = dw as f64 / dh as f64;
863 let (new_w, new_h) = if src_aspect > dst_aspect {
864 (dw, ((dw as f64 / src_aspect).round() as usize).max(1))
865 } else {
866 (((dh as f64 * src_aspect).round() as usize).max(1), dh)
867 };
868 let left = dw.saturating_sub(new_w) / 2;
869 let top = dh.saturating_sub(new_h) / 2;
870 Rect::new(left, top, new_w, new_h)
871}
872
873#[derive(Debug, Clone, Copy, PartialEq, Eq)]
878pub(crate) struct Rect {
879 pub left: usize,
880 pub top: usize,
881 pub width: usize,
882 pub height: usize,
883}
884
885impl Rect {
886 pub fn new(left: usize, top: usize, width: usize, height: usize) -> Self {
888 Self {
889 left,
890 top,
891 width,
892 height,
893 }
894 }
895}
896
897#[enum_dispatch(ImageProcessor)]
898pub trait ImageProcessorTrait {
899 fn convert(
936 &mut self,
937 src: &TensorDyn,
938 dst: &mut TensorDyn,
939 rotation: Rotation,
940 flip: Flip,
941 crop: Crop,
942 ) -> Result<()>;
943
944 fn draw_decoded_masks(
1007 &mut self,
1008 dst: &mut TensorDyn,
1009 detect: &[DetectBox],
1010 segmentation: &[Segmentation],
1011 overlay: MaskOverlay<'_>,
1012 ) -> Result<()>;
1013
1014 fn draw_proto_masks(
1034 &mut self,
1035 dst: &mut TensorDyn,
1036 detect: &[DetectBox],
1037 proto_data: &ProtoData,
1038 overlay: MaskOverlay<'_>,
1039 ) -> Result<()>;
1040
1041 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()>;
1049
1050 fn convert_deferred(
1067 &mut self,
1068 src: &TensorDyn,
1069 dst: &mut TensorDyn,
1070 rotation: Rotation,
1071 flip: Flip,
1072 crop: Crop,
1073 ) -> Result<()> {
1074 self.convert(src, dst, rotation, flip, crop)
1075 }
1076
1077 fn flush(&mut self) -> Result<()> {
1084 Ok(())
1085 }
1086}
1087
1088#[derive(Debug, Clone, Default)]
1094pub struct ImageProcessorConfig {
1095 #[cfg(all(
1106 any(
1107 target_os = "linux",
1108 target_os = "macos",
1109 target_os = "ios",
1110 target_os = "android",
1111 target_os = "windows"
1112 ),
1113 feature = "opengl"
1114 ))]
1115 pub egl_display: Option<EglDisplayKind>,
1116
1117 pub backend: ComputeBackend,
1129
1130 pub colorimetry: ColorimetryMode,
1135
1136 pub egl_cache_capacity: Option<usize>,
1192}
1193
1194#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1209pub enum ColorimetryMode {
1210 #[default]
1214 Fast,
1215 Exact,
1218}
1219
1220#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1227pub enum ComputeBackend {
1228 #[default]
1230 Auto,
1231 Cpu,
1233 G2d,
1235 OpenGl,
1237}
1238
1239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1245pub(crate) enum ForcedBackend {
1246 Cpu,
1247 G2d,
1248 OpenGl,
1249}
1250
1251#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1276pub struct RenderDtypeSupport {
1277 pub f32: bool,
1284 pub f16: bool,
1292}
1293
1294#[cfg(all(
1309 any(
1310 target_os = "linux",
1311 target_os = "windows",
1312 target_os = "macos",
1313 target_os = "ios",
1314 target_os = "android"
1315 ),
1316 feature = "opengl"
1317))]
1318pub(crate) fn float_pbo_eligible(dtype: DType, support: RenderDtypeSupport) -> bool {
1319 match dtype {
1320 DType::F16 => support.f16,
1321 DType::F32 => support.f32,
1322 _ => false,
1323 }
1324}
1325
1326#[derive(Debug)]
1329pub struct ImageProcessor {
1330 pub cpu: Option<CPUProcessor>,
1333
1334 #[cfg(target_os = "linux")]
1335 pub g2d: Option<G2DProcessor>,
1339 #[cfg(target_os = "linux")]
1340 #[cfg(feature = "opengl")]
1341 pub opengl: Option<GLProcessorThreaded>,
1345 #[cfg(any(
1346 target_os = "macos",
1347 target_os = "ios",
1348 target_os = "android",
1349 target_os = "windows"
1350 ))]
1351 #[cfg(feature = "opengl")]
1352 pub opengl: Option<GLProcessorThreaded>,
1361
1362 pub(crate) forced_backend: Option<ForcedBackend>,
1364
1365 pub(crate) convert_fallbacks: std::sync::atomic::AtomicU64,
1371}
1372
1373unsafe impl Send for ImageProcessor {}
1374unsafe impl Sync for ImageProcessor {}
1375
1376impl ImageProcessor {
1377 pub fn new() -> Result<Self> {
1403 Self::with_config(ImageProcessorConfig::default())
1404 }
1405
1406 pub fn convert_fallback_count(&self) -> u64 {
1413 self.convert_fallbacks
1414 .load(std::sync::atomic::Ordering::Relaxed)
1415 }
1416
1417 pub fn compression_fallback_count(&self) -> u64 {
1426 edgefirst_tensor::compression_fallback_count()
1427 }
1428
1429 pub fn convert_with_fence(
1452 &mut self,
1453 src: &TensorDyn,
1454 dst: &mut TensorDyn,
1455 rotation: Rotation,
1456 flip: Flip,
1457 crop: Crop,
1458 ) -> Result<Option<CompletionFence>> {
1459 #[cfg(any(
1460 target_os = "linux",
1461 target_os = "macos",
1462 target_os = "ios",
1463 target_os = "android",
1464 target_os = "windows"
1465 ))]
1466 #[cfg(feature = "opengl")]
1467 {
1468 let gl_forced = matches!(self.forced_backend, Some(ForcedBackend::OpenGl));
1469 if self.forced_backend.is_none() || gl_forced {
1470 if let Some(opengl) = self.opengl.as_mut() {
1471 match opengl.convert_with_fence(src, dst, rotation, flip, crop) {
1472 Ok(fd) => return Ok(fd),
1473 Err(e) if gl_forced => return Err(e),
1474 Err(e) => {
1475 log::debug!(
1479 "convert_with_fence: opengl declined, \
1480 falling back to the blocking chain: {e}"
1481 );
1482 }
1483 }
1484 } else if gl_forced {
1485 return Err(Error::ForcedBackendUnavailable("opengl".into()));
1486 }
1487 }
1488 }
1489 self.convert(src, dst, rotation, flip, crop)?;
1492 Ok(None)
1493 }
1494
1495 pub fn supported_render_dtypes(&self) -> RenderDtypeSupport {
1508 #[cfg(all(
1509 any(
1510 target_os = "macos",
1511 target_os = "ios",
1512 target_os = "android",
1513 target_os = "windows"
1514 ),
1515 feature = "opengl"
1516 ))]
1517 if let Some(gl) = self.opengl.as_ref() {
1518 return gl.supported_render_dtypes();
1519 }
1520 #[cfg(all(target_os = "linux", feature = "opengl"))]
1521 if let Some(gl) = self.opengl.as_ref() {
1522 return gl.supported_render_dtypes();
1523 }
1524 RenderDtypeSupport {
1525 f32: false,
1526 f16: false,
1527 }
1528 }
1529
1530 #[allow(unused_variables)]
1539 pub fn with_config(config: ImageProcessorConfig) -> Result<Self> {
1540 match config.backend {
1544 ComputeBackend::Cpu => {
1545 log::info!("ComputeBackend::Cpu — CPU only");
1546 return Ok(Self {
1547 cpu: Some(CPUProcessor::new()),
1548 #[cfg(target_os = "linux")]
1549 g2d: None,
1550 #[cfg(target_os = "linux")]
1551 #[cfg(feature = "opengl")]
1552 opengl: None,
1553 #[cfg(any(
1554 target_os = "macos",
1555 target_os = "ios",
1556 target_os = "android",
1557 target_os = "windows"
1558 ))]
1559 #[cfg(feature = "opengl")]
1560 opengl: None,
1561 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1562 forced_backend: None,
1563 });
1564 }
1565 ComputeBackend::G2d => {
1566 log::info!("ComputeBackend::G2d — G2D + CPU fallback");
1567 #[cfg(target_os = "linux")]
1568 {
1569 let g2d = match G2DProcessor::new() {
1570 Ok(g) => Some(g),
1571 Err(e) => {
1572 log::warn!("G2D requested but failed to initialize: {e:?}");
1573 None
1574 }
1575 };
1576 return Ok(Self {
1577 cpu: Some(CPUProcessor::new()),
1578 g2d,
1579 #[cfg(feature = "opengl")]
1580 opengl: None,
1581 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1582 forced_backend: None,
1583 });
1584 }
1585 #[cfg(not(target_os = "linux"))]
1586 {
1587 log::warn!("G2D requested but not available on this platform, using CPU");
1588 return Ok(Self {
1589 cpu: Some(CPUProcessor::new()),
1590 #[cfg(any(
1591 target_os = "macos",
1592 target_os = "ios",
1593 target_os = "android",
1594 target_os = "windows"
1595 ))]
1596 #[cfg(feature = "opengl")]
1597 opengl: None,
1598 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1599 forced_backend: None,
1600 });
1601 }
1602 }
1603 ComputeBackend::OpenGl => {
1604 log::info!("ComputeBackend::OpenGl — OpenGL + CPU fallback");
1605 #[cfg(target_os = "linux")]
1606 {
1607 #[cfg(feature = "opengl")]
1608 let opengl = match GLProcessorThreaded::with_cache_capacity(
1609 config.egl_display,
1610 config.egl_cache_capacity,
1611 ) {
1612 Ok(gl) => Some(gl),
1613 Err(e) => {
1614 log::warn!("OpenGL requested but failed to initialize: {e:?}");
1615 None
1616 }
1617 };
1618 return Ok(Self {
1619 cpu: Some(CPUProcessor::new()),
1620 g2d: None,
1621 #[cfg(feature = "opengl")]
1622 opengl,
1623 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1624 forced_backend: None,
1625 }
1626 .apply_colorimetry_mode(config.colorimetry));
1627 }
1628 #[cfg(any(target_os = "macos", target_os = "ios"))]
1629 {
1630 #[cfg(feature = "opengl")]
1631 let opengl = match GLProcessorThreaded::with_cache_capacity(
1632 config.egl_display,
1633 config.egl_cache_capacity,
1634 ) {
1635 Ok(gl) => Some(gl),
1636 Err(e) => {
1637 log::warn!(
1638 "OpenGL requested on macOS but ANGLE init failed: {e:?} \
1639 (install ANGLE via `brew install startergo/angle/angle` \
1640 and re-sign the dylibs — see README.md § macOS GPU \
1641 Acceleration). Falling back to CPU."
1642 );
1643 None
1644 }
1645 };
1646 return Ok(Self {
1647 cpu: Some(CPUProcessor::new()),
1648 #[cfg(feature = "opengl")]
1649 opengl,
1650 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1651 forced_backend: None,
1652 }
1653 .apply_colorimetry_mode(config.colorimetry));
1654 }
1655 #[cfg(target_os = "android")]
1656 {
1657 #[cfg(feature = "opengl")]
1658 let opengl = match GLProcessorThreaded::with_cache_capacity(
1659 config.egl_display,
1660 config.egl_cache_capacity,
1661 ) {
1662 Ok(gl) => Some(gl),
1663 Err(e) => {
1664 log::warn!(
1665 "OpenGL requested but native EGL init failed: {e:?}. \
1666 Falling back to CPU."
1667 );
1668 None
1669 }
1670 };
1671 return Ok(Self {
1672 cpu: Some(CPUProcessor::new()),
1673 #[cfg(feature = "opengl")]
1674 opengl,
1675 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1676 forced_backend: None,
1677 }
1678 .apply_colorimetry_mode(config.colorimetry));
1679 }
1680 #[cfg(target_os = "windows")]
1681 {
1682 #[cfg(feature = "opengl")]
1683 let opengl = match GLProcessorThreaded::with_cache_capacity(
1684 config.egl_display,
1685 config.egl_cache_capacity,
1686 ) {
1687 Ok(gl) => Some(gl),
1688 Err(e) => {
1689 log::warn!(
1690 "OpenGL requested on Windows but ANGLE/D3D11 init failed: {e:?} \
1691 (fetch ANGLE with `scripts/fetch-angle.sh` and set \
1692 EDGEFIRST_ANGLE_PATH to the directory containing libEGL.dll + \
1693 libGLESv2.dll, or place the two DLLs next to the executable — \
1694 see README.md § Windows GPU Acceleration). Falling back to CPU."
1695 );
1696 None
1697 }
1698 };
1699 return Ok(Self {
1700 cpu: Some(CPUProcessor::new()),
1701 #[cfg(feature = "opengl")]
1702 opengl,
1703 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1704 forced_backend: None,
1705 }
1706 .apply_colorimetry_mode(config.colorimetry));
1707 }
1708 #[cfg(not(any(
1709 target_os = "linux",
1710 target_os = "macos",
1711 target_os = "ios",
1712 target_os = "android",
1713 target_os = "windows"
1714 )))]
1715 {
1716 log::warn!("OpenGL requested but not available on this platform, using CPU");
1717 return Ok(Self {
1718 cpu: Some(CPUProcessor::new()),
1719 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1720 forced_backend: None,
1721 });
1722 }
1723 }
1724 ComputeBackend::Auto => { }
1725 }
1726
1727 if let Ok(val) = std::env::var("EDGEFIRST_FORCE_BACKEND") {
1732 let val_lower = val.to_lowercase();
1733 let forced = match val_lower.as_str() {
1734 "cpu" => ForcedBackend::Cpu,
1735 "g2d" => ForcedBackend::G2d,
1736 "opengl" => ForcedBackend::OpenGl,
1737 other => {
1738 return Err(Error::ForcedBackendUnavailable(format!(
1739 "unknown EDGEFIRST_FORCE_BACKEND value: {other:?} (expected cpu, g2d, or opengl)"
1740 )));
1741 }
1742 };
1743
1744 log::info!("EDGEFIRST_FORCE_BACKEND={val} — only initializing {val_lower} backend");
1745
1746 return match forced {
1747 ForcedBackend::Cpu => Ok(Self {
1748 cpu: Some(CPUProcessor::new()),
1749 #[cfg(target_os = "linux")]
1750 g2d: None,
1751 #[cfg(target_os = "linux")]
1752 #[cfg(feature = "opengl")]
1753 opengl: None,
1754 #[cfg(any(
1755 target_os = "macos",
1756 target_os = "ios",
1757 target_os = "android",
1758 target_os = "windows"
1759 ))]
1760 #[cfg(feature = "opengl")]
1761 opengl: None,
1762 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1763 forced_backend: Some(ForcedBackend::Cpu),
1764 }),
1765 ForcedBackend::G2d => {
1766 #[cfg(target_os = "linux")]
1767 {
1768 let g2d = G2DProcessor::new().map_err(|e| {
1769 Error::ForcedBackendUnavailable(format!(
1770 "g2d forced but failed to initialize: {e:?}"
1771 ))
1772 })?;
1773 Ok(Self {
1774 cpu: None,
1775 g2d: Some(g2d),
1776 #[cfg(feature = "opengl")]
1777 opengl: None,
1778 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1779 forced_backend: Some(ForcedBackend::G2d),
1780 })
1781 }
1782 #[cfg(not(target_os = "linux"))]
1783 {
1784 Err(Error::ForcedBackendUnavailable(
1785 "g2d backend is only available on Linux".into(),
1786 ))
1787 }
1788 }
1789 ForcedBackend::OpenGl => {
1790 #[cfg(target_os = "linux")]
1791 #[cfg(feature = "opengl")]
1792 {
1793 let opengl = GLProcessorThreaded::with_cache_capacity(
1794 config.egl_display,
1795 config.egl_cache_capacity,
1796 )
1797 .map_err(|e| {
1798 Error::ForcedBackendUnavailable(format!(
1799 "opengl forced but failed to initialize: {e:?}"
1800 ))
1801 })?;
1802 Ok(Self {
1803 cpu: None,
1804 g2d: None,
1805 opengl: Some(opengl),
1806 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1807 forced_backend: Some(ForcedBackend::OpenGl),
1808 }
1809 .apply_colorimetry_mode(config.colorimetry))
1810 }
1811 #[cfg(any(target_os = "macos", target_os = "ios"))]
1812 #[cfg(feature = "opengl")]
1813 {
1814 let opengl = GLProcessorThreaded::with_cache_capacity(
1815 config.egl_display,
1816 config.egl_cache_capacity,
1817 )
1818 .map_err(|e| {
1819 Error::ForcedBackendUnavailable(format!(
1820 "opengl forced on macOS but ANGLE init failed: {e:?}"
1821 ))
1822 })?;
1823 Ok(Self {
1824 cpu: None,
1825 opengl: Some(opengl),
1826 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1827 forced_backend: Some(ForcedBackend::OpenGl),
1828 }
1829 .apply_colorimetry_mode(config.colorimetry))
1830 }
1831 #[cfg(target_os = "android")]
1832 #[cfg(feature = "opengl")]
1833 {
1834 let opengl = GLProcessorThreaded::with_cache_capacity(
1835 config.egl_display,
1836 config.egl_cache_capacity,
1837 )
1838 .map_err(|e| {
1839 Error::ForcedBackendUnavailable(format!(
1840 "opengl forced but native EGL init failed: {e:?}"
1841 ))
1842 })?;
1843 Ok(Self {
1844 cpu: None,
1845 opengl: Some(opengl),
1846 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1847 forced_backend: Some(ForcedBackend::OpenGl),
1848 }
1849 .apply_colorimetry_mode(config.colorimetry))
1850 }
1851 #[cfg(target_os = "windows")]
1852 #[cfg(feature = "opengl")]
1853 {
1854 let opengl = GLProcessorThreaded::with_cache_capacity(
1855 config.egl_display,
1856 config.egl_cache_capacity,
1857 )
1858 .map_err(|e| {
1859 Error::ForcedBackendUnavailable(format!(
1860 "opengl forced on Windows but ANGLE/D3D11 init failed: {e:?}"
1861 ))
1862 })?;
1863 Ok(Self {
1864 cpu: None,
1865 opengl: Some(opengl),
1866 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1867 forced_backend: Some(ForcedBackend::OpenGl),
1868 }
1869 .apply_colorimetry_mode(config.colorimetry))
1870 }
1871 #[cfg(not(all(
1872 any(
1873 target_os = "linux",
1874 target_os = "macos",
1875 target_os = "ios",
1876 target_os = "android",
1877 target_os = "windows"
1878 ),
1879 feature = "opengl"
1880 )))]
1881 {
1882 Err(Error::ForcedBackendUnavailable(
1883 "opengl backend requires Linux, macOS, Android or Windows with the \
1884 'opengl' feature enabled"
1885 .into(),
1886 ))
1887 }
1888 }
1889 };
1890 }
1891
1892 #[cfg(target_os = "linux")]
1894 let g2d = if std::env::var("EDGEFIRST_DISABLE_G2D")
1895 .map(|x| x != "0" && x.to_lowercase() != "false")
1896 .unwrap_or(false)
1897 {
1898 log::debug!("EDGEFIRST_DISABLE_G2D is set");
1899 None
1900 } else {
1901 match G2DProcessor::new() {
1902 Ok(g2d_converter) => Some(g2d_converter),
1903 Err(err) => {
1904 log::warn!("Failed to initialize G2D converter: {err:?}");
1905 None
1906 }
1907 }
1908 };
1909
1910 #[cfg(target_os = "linux")]
1911 #[cfg(feature = "opengl")]
1912 let opengl = if std::env::var("EDGEFIRST_DISABLE_GL")
1913 .map(|x| x != "0" && x.to_lowercase() != "false")
1914 .unwrap_or(false)
1915 {
1916 log::debug!("EDGEFIRST_DISABLE_GL is set");
1917 None
1918 } else {
1919 match GLProcessorThreaded::with_cache_capacity(
1920 config.egl_display,
1921 config.egl_cache_capacity,
1922 ) {
1923 Ok(gl_converter) => Some(gl_converter),
1924 Err(err) => {
1925 log::warn!("Failed to initialize GL converter: {err:?}");
1926 None
1927 }
1928 }
1929 };
1930
1931 #[cfg(any(
1932 target_os = "macos",
1933 target_os = "ios",
1934 target_os = "android",
1935 target_os = "windows"
1936 ))]
1937 #[cfg(feature = "opengl")]
1938 let opengl = if std::env::var("EDGEFIRST_DISABLE_GL")
1939 .map(|x| x != "0" && x.to_lowercase() != "false")
1940 .unwrap_or(false)
1941 {
1942 log::debug!("EDGEFIRST_DISABLE_GL is set");
1943 None
1944 } else {
1945 match GLProcessorThreaded::with_cache_capacity(
1946 config.egl_display,
1947 config.egl_cache_capacity,
1948 ) {
1949 Ok(gl_converter) => Some(gl_converter),
1950 Err(err) => {
1951 log::debug!(
1952 "GL backend unavailable: {err:?} \
1953 (CPU fallback will be used)"
1954 );
1955 None
1956 }
1957 }
1958 };
1959
1960 let cpu = if std::env::var("EDGEFIRST_DISABLE_CPU")
1961 .map(|x| x != "0" && x.to_lowercase() != "false")
1962 .unwrap_or(false)
1963 {
1964 log::debug!("EDGEFIRST_DISABLE_CPU is set");
1965 None
1966 } else {
1967 Some(CPUProcessor::new())
1968 };
1969 Ok(Self {
1970 cpu,
1971 #[cfg(target_os = "linux")]
1972 g2d,
1973 #[cfg(target_os = "linux")]
1974 #[cfg(feature = "opengl")]
1975 opengl,
1976 #[cfg(any(
1977 target_os = "macos",
1978 target_os = "ios",
1979 target_os = "android",
1980 target_os = "windows"
1981 ))]
1982 #[cfg(feature = "opengl")]
1983 opengl,
1984 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1985 forced_backend: None,
1986 }
1987 .apply_colorimetry_mode(config.colorimetry))
1988 }
1989
1990 fn apply_colorimetry_mode(self, _mode: ColorimetryMode) -> Self {
1994 #[cfg(all(
1995 any(
1996 target_os = "linux",
1997 target_os = "macos",
1998 target_os = "ios",
1999 target_os = "android",
2000 target_os = "windows"
2001 ),
2002 feature = "opengl"
2003 ))]
2004 {
2005 let mut me = self;
2006 if let Err(e) = me.set_colorimetry_mode(_mode) {
2007 log::warn!("Failed to apply ColorimetryMode::{_mode:?}: {e:?}");
2008 }
2009 me
2010 }
2011 #[cfg(not(all(
2012 any(
2013 target_os = "linux",
2014 target_os = "macos",
2015 target_os = "ios",
2016 target_os = "android",
2017 target_os = "windows"
2018 ),
2019 feature = "opengl"
2020 )))]
2021 {
2022 let _ = _mode;
2023 self
2024 }
2025 }
2026
2027 #[cfg(all(
2032 any(
2033 target_os = "linux",
2034 target_os = "macos",
2035 target_os = "ios",
2036 target_os = "android",
2037 target_os = "windows"
2038 ),
2039 feature = "opengl"
2040 ))]
2041 pub fn set_colorimetry_mode(&mut self, mode: ColorimetryMode) -> Result<()> {
2042 if let Some(ref mut gl) = self.opengl {
2043 gl.set_colorimetry_mode(mode)?;
2044 }
2045 Ok(())
2046 }
2047
2048 #[cfg(all(
2051 any(
2052 target_os = "linux",
2053 target_os = "macos",
2054 target_os = "ios",
2055 target_os = "android",
2056 target_os = "windows"
2057 ),
2058 feature = "opengl"
2059 ))]
2060 pub fn set_int8_interpolation_mode(&mut self, mode: Int8InterpolationMode) -> Result<()> {
2061 if let Some(ref mut gl) = self.opengl {
2062 gl.set_int8_interpolation_mode(mode)?;
2063 }
2064 Ok(())
2065 }
2066
2067 pub fn create_image_desc(&self, desc: &edgefirst_tensor::ImageDesc) -> Result<TensorDyn> {
2078 if desc.compression().is_none() {
2079 return self.create_image(
2080 desc.width(),
2081 desc.height(),
2082 desc.format(),
2083 desc.dtype(),
2084 desc.memory(),
2085 desc.access(),
2086 );
2087 }
2088 Ok(TensorDyn::image_desc(desc)?)
2089 }
2090
2091 pub fn create_image(
2182 &self,
2183 width: usize,
2184 height: usize,
2185 format: PixelFormat,
2186 dtype: DType,
2187 memory: Option<TensorMemory>,
2188 access: edgefirst_tensor::CpuAccess,
2189 ) -> Result<TensorDyn> {
2190 #[cfg(target_os = "linux")]
2201 let dma_stride_bytes: Option<usize> = primary_plane_bpp(format, dtype.size())
2202 .and_then(|bpp| width.checked_mul(bpp))
2203 .and_then(align_pitch_bytes_to_gpu_alignment);
2204
2205 #[cfg(target_os = "linux")]
2209 let try_dma = || -> Result<TensorDyn> {
2210 let packed = format.layout() == edgefirst_tensor::PixelLayout::Packed;
2218 match dma_stride_bytes {
2219 Some(stride)
2220 if packed
2221 && primary_plane_bpp(format, dtype.size())
2222 .and_then(|bpp| width.checked_mul(bpp))
2223 .is_some_and(|natural| stride > natural) =>
2224 {
2225 log::debug!(
2226 "create_image: padding row stride for {format:?} {width}x{height} \
2227 from natural pitch to {stride} bytes for GPU alignment"
2228 );
2229 Ok(TensorDyn::image_with_stride(
2230 width,
2231 height,
2232 format,
2233 dtype,
2234 stride,
2235 Some(edgefirst_tensor::TensorMemory::DmaBuf),
2236 access,
2237 )?)
2238 }
2239 _ => Ok(TensorDyn::image(
2240 width,
2241 height,
2242 format,
2243 dtype,
2244 Some(edgefirst_tensor::TensorMemory::DmaBuf),
2245 access,
2246 )?),
2247 }
2248 };
2249
2250 match memory {
2257 #[cfg(target_os = "linux")]
2258 Some(TensorMemory::DmaBuf) => {
2259 if dtype == DType::F32 {
2261 return Err(Error::NotSupported(
2262 "F32 has no 32-bit-float DRM format for DMA-BUF; \
2263 use TensorMemory::Pbo for F32"
2264 .to_string(),
2265 ));
2266 }
2267 return try_dma();
2268 }
2269 Some(mem) => {
2270 return Ok(TensorDyn::image(
2271 width,
2272 height,
2273 format,
2274 dtype,
2275 Some(mem),
2276 access,
2277 )?);
2278 }
2279 None => {}
2280 }
2281
2282 #[cfg(any(
2289 target_os = "macos",
2290 target_os = "ios",
2291 target_os = "android",
2292 target_os = "windows"
2293 ))]
2294 #[cfg(feature = "opengl")]
2295 if let Some(gl) = self.opengl.as_ref() {
2296 let _ = gl;
2297 let support = self.supported_render_dtypes();
2302 let dtype_ok =
2303 !matches!(dtype, DType::F16 | DType::F32) || float_pbo_eligible(dtype, support);
2304 if dtype_ok {
2305 match TensorDyn::image(
2306 width,
2307 height,
2308 format,
2309 dtype,
2310 Some(edgefirst_tensor::TensorMemory::DmaBuf),
2311 access,
2312 ) {
2313 Ok(img) => return Ok(img),
2314 Err(e) => {
2315 log::debug!(
2319 "create_image: zero-copy Dma allocation declined \
2320 ({format:?}/{dtype:?} {width}x{height}): {e:?}; using fallback storage"
2321 );
2322 }
2323 }
2324 }
2325 }
2326
2327 #[cfg(target_os = "linux")]
2330 {
2331 #[cfg(feature = "opengl")]
2332 let gl_uses_pbo = self
2333 .opengl
2334 .as_ref()
2335 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
2336 #[cfg(not(feature = "opengl"))]
2337 let gl_uses_pbo = false;
2338
2339 if !gl_uses_pbo {
2340 if let Ok(img) = try_dma() {
2341 return Ok(img);
2342 }
2343 }
2344 }
2345
2346 #[cfg(any(target_os = "linux", target_os = "windows"))]
2350 #[cfg(feature = "opengl")]
2351 if dtype.size() == 1 {
2352 if let Some(gl) = &self.opengl {
2353 match gl.create_pbo_image(width, height, format) {
2354 Ok(t) => {
2355 if dtype == DType::I8 {
2356 debug_assert!(
2364 t.chroma().is_none(),
2365 "PBO i8 transmute requires chroma == None"
2366 );
2367 let t_i8: Tensor<i8> = unsafe { std::mem::transmute(t) };
2368 return Ok(TensorDyn::from(t_i8));
2369 }
2370 return Ok(TensorDyn::from(t));
2371 }
2372 Err(e) => log::debug!("PBO image creation failed, falling back to Mem: {e:?}"),
2373 }
2374 }
2375 }
2376
2377 #[cfg(any(target_os = "linux", target_os = "windows"))]
2380 #[cfg(feature = "opengl")]
2381 if float_pbo_eligible(dtype, self.supported_render_dtypes()) {
2382 if let Some(gl) = &self.opengl {
2383 match gl.create_pbo_image_dtype(width, height, format, dtype) {
2384 Ok(t) => return Ok(t),
2385 Err(e) => {
2386 log::debug!(
2387 "Float PBO image creation failed for {dtype:?}, \
2388 falling back to Mem: {e:?}"
2389 );
2390 }
2391 }
2392 }
2393 }
2394
2395 Ok(TensorDyn::image(
2397 width,
2398 height,
2399 format,
2400 dtype,
2401 Some(edgefirst_tensor::TensorMemory::Mem),
2402 access,
2403 )?)
2404 }
2405
2406 #[allow(clippy::too_many_arguments)]
2460 #[cfg(target_os = "linux")]
2461 pub fn import_image(
2462 &self,
2463 image: edgefirst_tensor::PlaneDescriptor,
2464 chroma: Option<edgefirst_tensor::PlaneDescriptor>,
2465 width: usize,
2466 height: usize,
2467 format: PixelFormat,
2468 dtype: DType,
2469 colorimetry: Option<edgefirst_tensor::Colorimetry>,
2470 ) -> Result<TensorDyn> {
2471 use edgefirst_tensor::{Tensor, TensorMemory};
2472
2473 let image_stride = image.stride();
2475 let image_offset = image.offset();
2476 let chroma_stride = chroma.as_ref().and_then(|c| c.stride());
2477 let chroma_offset = chroma.as_ref().and_then(|c| c.offset());
2478
2479 if let Some(chroma_pd) = chroma {
2480 if dtype != DType::U8 && dtype != DType::I8 {
2485 return Err(Error::NotSupported(format!(
2486 "multiplane import only supports U8/I8, got {dtype:?}"
2487 )));
2488 }
2489 if format.layout() != PixelLayout::SemiPlanar {
2490 return Err(Error::NotSupported(format!(
2491 "import_image with chroma requires a semi-planar format, got {format:?}"
2492 )));
2493 }
2494
2495 let chroma_h = match format {
2496 PixelFormat::Nv12 => {
2497 height.div_ceil(2)
2499 }
2500 PixelFormat::Nv16 => {
2503 return Err(Error::NotSupported(
2504 "multiplane NV16 is not yet supported; use contiguous NV16 instead".into(),
2505 ))
2506 }
2507 _ => {
2508 return Err(Error::NotSupported(format!(
2509 "unsupported semi-planar format: {format:?}"
2510 )))
2511 }
2512 };
2513
2514 let luma = Tensor::<u8>::from_fd(image.into_fd(), &[height, width], Some("luma"))?;
2515 if luma.memory() != TensorMemory::DmaBuf {
2516 return Err(Error::NotSupported(format!(
2517 "luma fd must be DMA-backed, got {:?}",
2518 luma.memory()
2519 )));
2520 }
2521
2522 let chroma_tensor =
2523 Tensor::<u8>::from_fd(chroma_pd.into_fd(), &[chroma_h, width], Some("chroma"))?;
2524 if chroma_tensor.memory() != TensorMemory::DmaBuf {
2525 return Err(Error::NotSupported(format!(
2526 "chroma fd must be DMA-backed, got {:?}",
2527 chroma_tensor.memory()
2528 )));
2529 }
2530
2531 let mut tensor = Tensor::<u8>::from_planes(luma, chroma_tensor, format)?;
2534
2535 if let Some(s) = image_stride {
2537 tensor.set_row_stride(s)?;
2538 }
2539 if let Some(o) = image_offset {
2540 tensor.set_plane_offset(o);
2541 }
2542
2543 if let Some(chroma_ref) = tensor.chroma_mut() {
2548 if let Some(s) = chroma_stride {
2549 if s < width {
2550 return Err(Error::InvalidShape(format!(
2551 "chroma stride {s} < minimum {width} for {format:?}"
2552 )));
2553 }
2554 chroma_ref.set_row_stride_unchecked(s);
2555 }
2556 if let Some(o) = chroma_offset {
2557 chroma_ref.set_plane_offset(o);
2558 }
2559 }
2560
2561 if dtype == DType::I8 {
2562 const {
2566 assert!(std::mem::size_of::<Tensor<u8>>() == std::mem::size_of::<Tensor<i8>>());
2567 assert!(
2568 std::mem::align_of::<Tensor<u8>>() == std::mem::align_of::<Tensor<i8>>()
2569 );
2570 }
2571 let tensor_i8: Tensor<i8> = unsafe { std::mem::transmute(tensor) };
2572 let mut dyn_tensor = TensorDyn::from(tensor_i8);
2573 dyn_tensor.set_colorimetry(colorimetry);
2574 return Ok(dyn_tensor);
2575 }
2576 let mut dyn_tensor = TensorDyn::from(tensor);
2577 dyn_tensor.set_colorimetry(colorimetry);
2578 Ok(dyn_tensor)
2579 } else {
2580 let shape = format.allocation_shape(width, height).ok_or_else(|| {
2585 Error::NotSupported(format!(
2586 "unsupported pixel format for import_image: {format:?}"
2587 ))
2588 })?;
2589 let tensor = TensorDyn::from_fd(image.into_fd(), &shape, dtype, None)?;
2590 if tensor.memory() != TensorMemory::DmaBuf {
2591 return Err(Error::NotSupported(format!(
2592 "import_image requires DMA-backed fd, got {:?}",
2593 tensor.memory()
2594 )));
2595 }
2596 let mut tensor = tensor.with_format(format)?;
2597 if let Some(s) = image_stride {
2598 tensor.set_row_stride(s)?;
2599 }
2600 if let Some(o) = image_offset {
2601 tensor.set_plane_offset(o);
2602 }
2603 tensor.set_colorimetry(colorimetry);
2604 Ok(tensor)
2605 }
2606 }
2607
2608 #[cfg(feature = "decode")]
2633 pub fn draw_masks(
2634 &mut self,
2635 decoder: &edgefirst_decoder::Decoder,
2636 outputs: &[&TensorDyn],
2637 dst: &mut TensorDyn,
2638 overlay: MaskOverlay<'_>,
2639 ) -> Result<Vec<DetectBox>> {
2640 let mut output_boxes = Vec::with_capacity(100);
2641
2642 let proto_result = decoder
2644 .decode_proto(outputs, &mut output_boxes)
2645 .map_err(|e| Error::Internal(format!("decode_proto: {e:#?}")))?;
2646
2647 if let Some(proto_data) = proto_result {
2648 self.draw_proto_masks(dst, &output_boxes, &proto_data, overlay)?;
2649 } else {
2650 let mut output_masks = Vec::with_capacity(100);
2652 decoder
2653 .decode(outputs, &mut output_boxes, &mut output_masks)
2654 .map_err(|e| Error::Internal(format!("decode: {e:#?}")))?;
2655 self.draw_decoded_masks(dst, &output_boxes, &output_masks, overlay)?;
2656 }
2657 Ok(output_boxes)
2658 }
2659
2660 #[cfg(feature = "tracker")]
2668 pub fn draw_masks_tracked<TR: edgefirst_tracker::Tracker<DetectBox>>(
2669 &mut self,
2670 decoder: &edgefirst_decoder::Decoder,
2671 tracker: &mut TR,
2672 timestamp: u64,
2673 outputs: &[&TensorDyn],
2674 dst: &mut TensorDyn,
2675 overlay: MaskOverlay<'_>,
2676 ) -> Result<(Vec<DetectBox>, Vec<edgefirst_tracker::TrackInfo>)> {
2677 let mut output_boxes = Vec::with_capacity(100);
2678 let mut output_tracks = Vec::new();
2679
2680 let proto_result = decoder
2681 .decode_proto_tracked(
2682 tracker,
2683 timestamp,
2684 outputs,
2685 &mut output_boxes,
2686 &mut output_tracks,
2687 )
2688 .map_err(|e| Error::Internal(format!("decode_proto_tracked: {e:#?}")))?;
2689
2690 if let Some(proto_data) = proto_result {
2691 self.draw_proto_masks(dst, &output_boxes, &proto_data, overlay)?;
2692 } else {
2693 let mut output_masks = Vec::with_capacity(100);
2697 decoder
2698 .decode_tracked(
2699 tracker,
2700 timestamp,
2701 outputs,
2702 &mut output_boxes,
2703 &mut output_masks,
2704 &mut output_tracks,
2705 )
2706 .map_err(|e| Error::Internal(format!("decode_tracked: {e:#?}")))?;
2707 self.draw_decoded_masks(dst, &output_boxes, &output_masks, overlay)?;
2708 }
2709 Ok((output_boxes, output_tracks))
2710 }
2711
2712 pub fn materialize_masks(
2736 &mut self,
2737 detect: &[DetectBox],
2738 proto_data: &ProtoData,
2739 letterbox: Option<[f32; 4]>,
2740 resolution: MaskResolution,
2741 ) -> Result<Vec<Segmentation>> {
2742 let cpu = self.cpu.as_mut().ok_or(Error::NoConverter)?;
2743 match resolution {
2744 MaskResolution::Proto => cpu.materialize_segmentations(detect, proto_data, letterbox),
2745 MaskResolution::Scaled { width, height } => {
2746 cpu.materialize_scaled_segmentations(detect, proto_data, letterbox, width, height)
2747 }
2748 }
2749 }
2750}
2751
2752impl ImageProcessorTrait for ImageProcessor {
2753 fn convert(
2759 &mut self,
2760 src: &TensorDyn,
2761 dst: &mut TensorDyn,
2762 rotation: Rotation,
2763 flip: Flip,
2764 crop: Crop,
2765 ) -> Result<()> {
2766 let start = Instant::now();
2767 let src_fmt = src.format();
2768 let dst_fmt = dst.format();
2769 let _span = tracing::trace_span!(
2770 "image.convert",
2771 ?src_fmt,
2772 ?dst_fmt,
2773 src_memory = ?src.memory(),
2774 dst_memory = ?dst.memory(),
2775 ?rotation,
2776 ?flip,
2777 )
2778 .entered();
2779 log::trace!(
2780 "convert: {src_fmt:?}({:?}/{:?}) → {dst_fmt:?}({:?}/{:?}), \
2781 rotation={rotation:?}, flip={flip:?}, backend={:?}",
2782 src.dtype(),
2783 src.memory(),
2784 dst.dtype(),
2785 dst.memory(),
2786 self.forced_backend,
2787 );
2788
2789 if let Some(forced) = self.forced_backend {
2791 return match forced {
2792 ForcedBackend::Cpu => {
2793 if let Some(cpu) = self.cpu.as_mut() {
2794 let r = cpu.convert(src, dst, rotation, flip, crop);
2795 log::trace!(
2796 "convert: forced=cpu result={} ({:?})",
2797 if r.is_ok() { "ok" } else { "err" },
2798 start.elapsed()
2799 );
2800 return r;
2801 }
2802 Err(Error::ForcedBackendUnavailable("cpu".into()))
2803 }
2804 ForcedBackend::G2d => {
2805 #[cfg(target_os = "linux")]
2806 if let Some(g2d) = self.g2d.as_mut() {
2807 let r = g2d.convert(src, dst, rotation, flip, crop);
2808 log::trace!(
2809 "convert: forced=g2d result={} ({:?})",
2810 if r.is_ok() { "ok" } else { "err" },
2811 start.elapsed()
2812 );
2813 return r;
2814 }
2815 Err(Error::ForcedBackendUnavailable("g2d".into()))
2816 }
2817 ForcedBackend::OpenGl => {
2818 #[cfg(any(
2819 target_os = "linux",
2820 target_os = "macos",
2821 target_os = "ios",
2822 target_os = "android",
2823 target_os = "windows"
2824 ))]
2825 #[cfg(feature = "opengl")]
2826 if let Some(opengl) = self.opengl.as_mut() {
2827 let r = opengl.convert(src, dst, rotation, flip, crop);
2828 log::trace!(
2829 "convert: forced=opengl result={} ({:?})",
2830 if r.is_ok() { "ok" } else { "err" },
2831 start.elapsed()
2832 );
2833 return r;
2834 }
2835 Err(Error::ForcedBackendUnavailable("opengl".into()))
2836 }
2837 };
2838 }
2839
2840 #[cfg(any(
2842 target_os = "linux",
2843 target_os = "macos",
2844 target_os = "ios",
2845 target_os = "android",
2846 target_os = "windows"
2847 ))]
2848 #[cfg(feature = "opengl")]
2849 if let Some(opengl) = self.opengl.as_mut() {
2850 match opengl.convert(src, dst, rotation, flip, crop) {
2851 Ok(_) => {
2852 log::trace!(
2853 "convert: auto selected=opengl for {src_fmt:?}→{dst_fmt:?} ({:?})",
2854 start.elapsed()
2855 );
2856 return Ok(());
2857 }
2858 Err(e) => {
2859 self.convert_fallbacks
2860 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2861 log::debug!(
2862 "convert: auto opengl declined {src_fmt:?}@{:?}→{dst_fmt:?}@{:?}, \
2863 falling back toward G2D/CPU: {e}",
2864 src.memory(),
2865 dst.memory(),
2866 );
2867 }
2868 }
2869 }
2870
2871 #[cfg(target_os = "linux")]
2872 if let Some(g2d) = self.g2d.as_mut() {
2873 let src_is_yuv = src.format().is_some_and(|f| f.is_yuv());
2880 let dst_is_yuv = dst.format().is_some_and(|f| f.is_yuv());
2881 let g2d_eligible = if src_is_yuv || dst_is_yuv {
2882 let cm = if src_is_yuv {
2883 crate::colorimetry::effective_colorimetry(src)
2884 } else {
2885 crate::colorimetry::effective_colorimetry(dst)
2886 };
2887 crate::g2d::g2d_can_handle(&cm, true)
2888 } else {
2889 true
2890 };
2891 if !g2d_eligible {
2892 log::trace!(
2893 "convert: auto g2d skipped {src_fmt:?}→{dst_fmt:?} \
2894 (colorimetry not expressible: full-range/BT.2020)"
2895 );
2896 } else {
2897 match g2d.convert(src, dst, rotation, flip, crop) {
2898 Ok(_) => {
2899 log::trace!(
2900 "convert: auto selected=g2d for {src_fmt:?}→{dst_fmt:?} ({:?})",
2901 start.elapsed()
2902 );
2903 return Ok(());
2904 }
2905 Err(e) => {
2906 log::trace!("convert: auto g2d declined {src_fmt:?}→{dst_fmt:?}: {e}");
2907 }
2908 }
2909 }
2910 }
2911
2912 if let Some(cpu) = self.cpu.as_mut() {
2913 match cpu.convert(src, dst, rotation, flip, crop) {
2914 Ok(_) => {
2915 log::trace!(
2916 "convert: auto selected=cpu for {src_fmt:?}→{dst_fmt:?} ({:?})",
2917 start.elapsed()
2918 );
2919 return Ok(());
2920 }
2921 Err(e) => {
2922 log::trace!("convert: auto cpu failed {src_fmt:?}→{dst_fmt:?}: {e}");
2923 return Err(e);
2924 }
2925 }
2926 }
2927 Err(Error::NoConverter)
2928 }
2929
2930 fn convert_deferred(
2931 &mut self,
2932 src: &TensorDyn,
2933 dst: &mut TensorDyn,
2934 rotation: Rotation,
2935 flip: Flip,
2936 crop: Crop,
2937 ) -> Result<()> {
2938 #[cfg(any(
2944 target_os = "linux",
2945 target_os = "macos",
2946 target_os = "ios",
2947 target_os = "android",
2948 target_os = "windows"
2949 ))]
2950 #[cfg(feature = "opengl")]
2951 {
2952 let gl_forced = matches!(self.forced_backend, Some(ForcedBackend::OpenGl));
2953 if gl_forced || self.forced_backend.is_none() {
2954 if let Some(opengl) = self.opengl.as_mut() {
2955 match opengl.convert_deferred(src, dst, rotation, flip, crop) {
2956 Ok(()) => return Ok(()),
2957 Err(e) => {
2958 log::trace!("convert_deferred: gl declined: {e}; eager fallback");
2959 if gl_forced {
2962 return Err(e);
2963 }
2964 }
2965 }
2966 }
2967 }
2968 }
2969 self.convert(src, dst, rotation, flip, crop)
2970 }
2971
2972 fn flush(&mut self) -> Result<()> {
2973 let _span = tracing::trace_span!("image.flush").entered();
2974 #[cfg(any(
2977 target_os = "linux",
2978 target_os = "macos",
2979 target_os = "ios",
2980 target_os = "android",
2981 target_os = "windows"
2982 ))]
2983 #[cfg(feature = "opengl")]
2984 if let Some(opengl) = self.opengl.as_mut() {
2985 return opengl.flush();
2986 }
2987 Ok(())
2988 }
2989
2990 fn draw_decoded_masks(
2991 &mut self,
2992 dst: &mut TensorDyn,
2993 detect: &[DetectBox],
2994 segmentation: &[Segmentation],
2995 overlay: MaskOverlay<'_>,
2996 ) -> Result<()> {
2997 let _span = tracing::trace_span!(
2998 "image.draw_decoded_masks",
2999 n_detections = detect.len(),
3000 n_segmentations = segmentation.len(),
3001 )
3002 .entered();
3003 let start = Instant::now();
3004
3005 if let Some(bg) = overlay.background {
3006 if bg.aliases(dst) {
3007 return Err(Error::AliasedBuffers(
3008 "background must not reference the same buffer as dst".to_string(),
3009 ));
3010 }
3011 }
3012
3013 let lb_boxes: Vec<DetectBox>;
3016 let lb_segs: Vec<Segmentation>;
3017 let (detect, segmentation) = if let Some(lb) = overlay.letterbox {
3018 lb_boxes = detect.iter().map(|&d| unletter_bbox(d, lb)).collect();
3019 if segmentation.len() == lb_boxes.len() {
3022 lb_segs = segmentation
3029 .iter()
3030 .zip(lb_boxes.iter())
3031 .map(|(s, d)| copy_segmentation(s, d.bbox.into()))
3032 .collect::<Result<Vec<_>>>()?;
3033 (lb_boxes.as_slice(), lb_segs.as_slice())
3034 } else {
3035 (lb_boxes.as_slice(), segmentation)
3039 }
3040 } else {
3041 (detect, segmentation)
3042 };
3043 #[cfg(target_os = "linux")]
3044 let is_empty_frame = detect.is_empty() && segmentation.is_empty();
3045
3046 if let Some(forced) = self.forced_backend {
3048 return match forced {
3049 ForcedBackend::Cpu => {
3050 if let Some(cpu) = self.cpu.as_mut() {
3051 return cpu.draw_decoded_masks(dst, detect, segmentation, overlay);
3052 }
3053 Err(Error::ForcedBackendUnavailable("cpu".into()))
3054 }
3055 ForcedBackend::G2d => {
3056 #[cfg(target_os = "linux")]
3059 if let Some(g2d) = self.g2d.as_mut() {
3060 return g2d.draw_decoded_masks(dst, detect, segmentation, overlay);
3061 }
3062 Err(Error::ForcedBackendUnavailable("g2d".into()))
3063 }
3064 ForcedBackend::OpenGl => {
3065 #[cfg(any(
3068 target_os = "linux",
3069 target_os = "macos",
3070 target_os = "ios",
3071 target_os = "android",
3072 target_os = "windows"
3073 ))]
3074 #[cfg(feature = "opengl")]
3075 if let Some(opengl) = self.opengl.as_mut() {
3076 return opengl.draw_decoded_masks(dst, detect, segmentation, overlay);
3077 }
3078 Err(Error::ForcedBackendUnavailable("opengl".into()))
3079 }
3080 };
3081 }
3082
3083 #[cfg(target_os = "linux")]
3089 if is_empty_frame {
3090 if let Some(g2d) = self.g2d.as_mut() {
3091 match g2d.draw_decoded_masks(dst, detect, segmentation, overlay) {
3092 Ok(_) => {
3093 log::trace!(
3094 "draw_decoded_masks empty frame via g2d in {:?}",
3095 start.elapsed()
3096 );
3097 return Ok(());
3098 }
3099 Err(e) => log::trace!("g2d empty-frame path unavailable: {e:?}"),
3100 }
3101 }
3102 }
3103
3104 #[cfg(any(
3108 target_os = "linux",
3109 target_os = "macos",
3110 target_os = "ios",
3111 target_os = "android",
3112 target_os = "windows"
3113 ))]
3114 #[cfg(feature = "opengl")]
3115 if let Some(opengl) = self.opengl.as_mut() {
3116 log::trace!(
3117 "draw_decoded_masks started with opengl in {:?}",
3118 start.elapsed()
3119 );
3120 match opengl.draw_decoded_masks(dst, detect, segmentation, overlay) {
3121 Ok(_) => {
3122 log::trace!("draw_decoded_masks with opengl in {:?}", start.elapsed());
3123 return Ok(());
3124 }
3125 Err(e) => {
3126 log::trace!("draw_decoded_masks didn't work with opengl: {e:?}")
3127 }
3128 }
3129 }
3130
3131 log::trace!(
3132 "draw_decoded_masks started with cpu in {:?}",
3133 start.elapsed()
3134 );
3135 if let Some(cpu) = self.cpu.as_mut() {
3136 match cpu.draw_decoded_masks(dst, detect, segmentation, overlay) {
3137 Ok(_) => {
3138 log::trace!("draw_decoded_masks with cpu in {:?}", start.elapsed());
3139 return Ok(());
3140 }
3141 Err(e) => {
3142 log::trace!("draw_decoded_masks didn't work with cpu: {e:?}");
3143 return Err(e);
3144 }
3145 }
3146 }
3147 Err(Error::NoConverter)
3148 }
3149
3150 fn draw_proto_masks(
3151 &mut self,
3152 dst: &mut TensorDyn,
3153 detect: &[DetectBox],
3154 proto_data: &ProtoData,
3155 overlay: MaskOverlay<'_>,
3156 ) -> Result<()> {
3157 let start = Instant::now();
3158
3159 if let Some(bg) = overlay.background {
3160 if bg.aliases(dst) {
3161 return Err(Error::AliasedBuffers(
3162 "background must not reference the same buffer as dst".to_string(),
3163 ));
3164 }
3165 }
3166
3167 let lb_boxes: Vec<DetectBox>;
3173 let render_detect = if let Some(lb) = overlay.letterbox {
3174 lb_boxes = detect.iter().map(|&d| unletter_bbox(d, lb)).collect();
3175 lb_boxes.as_slice()
3176 } else {
3177 detect
3178 };
3179 #[cfg(target_os = "linux")]
3180 let is_empty_frame = detect.is_empty();
3181
3182 if let Some(forced) = self.forced_backend {
3184 return match forced {
3185 ForcedBackend::Cpu => {
3186 if let Some(cpu) = self.cpu.as_mut() {
3187 return cpu.draw_proto_masks(dst, render_detect, proto_data, overlay);
3188 }
3189 Err(Error::ForcedBackendUnavailable("cpu".into()))
3190 }
3191 ForcedBackend::G2d => {
3192 #[cfg(target_os = "linux")]
3193 if let Some(g2d) = self.g2d.as_mut() {
3194 return g2d.draw_proto_masks(dst, render_detect, proto_data, overlay);
3195 }
3196 Err(Error::ForcedBackendUnavailable("g2d".into()))
3197 }
3198 ForcedBackend::OpenGl => {
3199 #[cfg(any(
3200 target_os = "linux",
3201 target_os = "macos",
3202 target_os = "ios",
3203 target_os = "android",
3204 target_os = "windows"
3205 ))]
3206 #[cfg(feature = "opengl")]
3207 if let Some(opengl) = self.opengl.as_mut() {
3208 return opengl.draw_proto_masks(dst, render_detect, proto_data, overlay);
3209 }
3210 Err(Error::ForcedBackendUnavailable("opengl".into()))
3211 }
3212 };
3213 }
3214
3215 #[cfg(target_os = "linux")]
3218 if is_empty_frame {
3219 if let Some(g2d) = self.g2d.as_mut() {
3220 match g2d.draw_proto_masks(dst, render_detect, proto_data, overlay) {
3221 Ok(_) => {
3222 log::trace!(
3223 "draw_proto_masks empty frame via g2d in {:?}",
3224 start.elapsed()
3225 );
3226 return Ok(());
3227 }
3228 Err(e) => log::trace!("g2d empty-frame path unavailable: {e:?}"),
3229 }
3230 }
3231 }
3232
3233 #[cfg(any(
3242 target_os = "linux",
3243 target_os = "macos",
3244 target_os = "ios",
3245 target_os = "android",
3246 target_os = "windows"
3247 ))]
3248 #[cfg(feature = "opengl")]
3249 if let (Some(_), Some(_)) = (self.cpu.as_ref(), self.opengl.as_ref()) {
3250 let segmentation = match self.cpu.as_mut() {
3251 Some(cpu) => {
3252 log::trace!(
3253 "draw_proto_masks started with hybrid (cpu+opengl) in {:?}",
3254 start.elapsed()
3255 );
3256 cpu.materialize_segmentations(detect, proto_data, overlay.letterbox)?
3257 }
3258 None => unreachable!("cpu presence checked above"),
3259 };
3260 if let Some(opengl) = self.opengl.as_mut() {
3261 match opengl.draw_decoded_masks(dst, render_detect, &segmentation, overlay) {
3262 Ok(_) => {
3263 log::trace!(
3264 "draw_proto_masks with hybrid (cpu+opengl) in {:?}",
3265 start.elapsed()
3266 );
3267 return Ok(());
3268 }
3269 Err(e) => {
3270 log::trace!(
3271 "draw_proto_masks hybrid path failed, falling back to cpu: {e:?}"
3272 );
3273 }
3274 }
3275 }
3276 }
3277
3278 let Some(cpu) = self.cpu.as_mut() else {
3279 return Err(Error::Internal(
3280 "draw_proto_masks requires CPU backend for fallback path".into(),
3281 ));
3282 };
3283 log::trace!("draw_proto_masks started with cpu in {:?}", start.elapsed());
3284 cpu.draw_proto_masks(dst, render_detect, proto_data, overlay)
3285 }
3286
3287 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()> {
3288 let start = Instant::now();
3289
3290 if let Some(forced) = self.forced_backend {
3292 return match forced {
3293 ForcedBackend::Cpu => {
3294 if let Some(cpu) = self.cpu.as_mut() {
3295 return cpu.set_class_colors(colors);
3296 }
3297 Err(Error::ForcedBackendUnavailable("cpu".into()))
3298 }
3299 ForcedBackend::G2d => Err(Error::NotSupported(
3300 "g2d does not support set_class_colors".into(),
3301 )),
3302 ForcedBackend::OpenGl => {
3303 #[cfg(any(
3304 target_os = "linux",
3305 target_os = "macos",
3306 target_os = "ios",
3307 target_os = "android",
3308 target_os = "windows"
3309 ))]
3310 #[cfg(feature = "opengl")]
3311 if let Some(opengl) = self.opengl.as_mut() {
3312 return opengl.set_class_colors(colors);
3313 }
3314 Err(Error::ForcedBackendUnavailable("opengl".into()))
3315 }
3316 };
3317 }
3318
3319 let mut backends = 0usize;
3328 let mut first_err = None;
3329 #[cfg(any(
3330 target_os = "linux",
3331 target_os = "macos",
3332 target_os = "ios",
3333 target_os = "android",
3334 target_os = "windows"
3335 ))]
3336 #[cfg(feature = "opengl")]
3337 if let Some(opengl) = self.opengl.as_mut() {
3338 backends += 1;
3339 match opengl.set_class_colors(colors) {
3340 Ok(()) => log::trace!("colors set with opengl in {:?}", start.elapsed()),
3341 Err(e) => {
3342 log::trace!("colors didn't set with opengl: {e:?}");
3343 first_err.get_or_insert(e);
3344 }
3345 }
3346 }
3347 if let Some(cpu) = self.cpu.as_mut() {
3348 backends += 1;
3349 match cpu.set_class_colors(colors) {
3350 Ok(()) => log::trace!("colors set with cpu in {:?}", start.elapsed()),
3351 Err(e) => {
3352 log::trace!("colors didn't set with cpu: {e:?}");
3353 first_err.get_or_insert(e);
3354 }
3355 }
3356 }
3357 match first_err {
3361 Some(e) => Err(e),
3362 None if backends > 0 => Ok(()),
3363 None => Err(Error::NoConverter),
3364 }
3365 }
3366}
3367
3368#[cfg(all(test, feature = "codec"))]
3378pub(crate) fn load_image_test_helper(
3379 image: &[u8],
3380 format: Option<PixelFormat>,
3381 memory: Option<TensorMemory>,
3382) -> Result<TensorDyn> {
3383 use edgefirst_codec::{peek_info, ImageDecoder, ImageLoad};
3384
3385 let info = peek_info(image)?;
3389 let native_fmt = info.format;
3390 let w = info.width;
3391 let h = info.height;
3392
3393 let mut decoder = ImageDecoder::new();
3394
3395 #[cfg(target_os = "linux")]
3398 let native_src = {
3399 if let Some(aligned_pitch) = padded_dma_pitch_for(native_fmt, w, &memory) {
3400 let mut dma = Tensor::<u8>::image_with_stride(
3401 w,
3402 h,
3403 native_fmt,
3404 aligned_pitch,
3405 Some(TensorMemory::DmaBuf),
3406 edgefirst_tensor::CpuAccess::ReadWrite,
3407 )?;
3408 dma.load_image(&mut decoder, image)?;
3409 TensorDyn::from(dma)
3410 } else {
3411 let mut img = Tensor::<u8>::image(
3412 w,
3413 h,
3414 native_fmt,
3415 memory,
3416 edgefirst_tensor::CpuAccess::ReadWrite,
3417 )?;
3418 img.load_image(&mut decoder, image)?;
3419 TensorDyn::from(img)
3420 }
3421 };
3422 #[cfg(not(target_os = "linux"))]
3423 let native_src = {
3424 let mut img = Tensor::<u8>::image(
3425 w,
3426 h,
3427 native_fmt,
3428 memory,
3429 edgefirst_tensor::CpuAccess::ReadWrite,
3430 )?;
3431 img.load_image(&mut decoder, image)?;
3432 TensorDyn::from(img)
3433 };
3434
3435 match format {
3439 Some(f) if f != native_fmt => {
3440 let mut dst = TensorDyn::image(
3441 w,
3442 h,
3443 f,
3444 DType::U8,
3445 memory,
3446 edgefirst_tensor::CpuAccess::ReadWrite,
3447 )?;
3448 #[allow(clippy::needless_update)]
3455 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
3456 backend: ComputeBackend::Cpu,
3457 ..Default::default()
3458 })?;
3459 proc.convert(
3460 &native_src,
3461 &mut dst,
3462 Rotation::None,
3463 Flip::None,
3464 Crop::default(),
3465 )?;
3466 Ok(dst)
3467 }
3468 _ => Ok(native_src),
3469 }
3470}
3471
3472pub fn save_jpeg(tensor: &TensorDyn, path: impl AsRef<std::path::Path>, quality: u8) -> Result<()> {
3476 let t = tensor.as_typed::<u8>().ok_or(Error::UnsupportedFormat(
3477 "save_jpeg requires u8 tensor".to_string(),
3478 ))?;
3479 let fmt = t.format().ok_or(Error::NotAnImage)?;
3480 if fmt.layout() != PixelLayout::Packed {
3481 return Err(Error::NotImplemented(
3482 "Saving planar images is not supported".to_string(),
3483 ));
3484 }
3485
3486 let colour = match fmt {
3487 PixelFormat::Rgb => jpeg_encoder::ColorType::Rgb,
3488 PixelFormat::Rgba => jpeg_encoder::ColorType::Rgba,
3489 _ => {
3490 return Err(Error::NotImplemented(
3491 "Unsupported image format for saving".to_string(),
3492 ));
3493 }
3494 };
3495
3496 let w = t.width().ok_or(Error::NotAnImage)?;
3497 let h = t.height().ok_or(Error::NotAnImage)?;
3498 let encoder = jpeg_encoder::Encoder::new_file(path, quality)?;
3499 let tensor_map = t.map_read()?;
3500
3501 encoder.encode(&tensor_map, w as u16, h as u16, colour)?;
3502
3503 Ok(())
3504}
3505
3506pub(crate) struct FunctionTimer<T: Display> {
3507 name: T,
3508 start: std::time::Instant,
3509}
3510
3511impl<T: Display> FunctionTimer<T> {
3512 pub fn new(name: T) -> Self {
3513 Self {
3514 name,
3515 start: std::time::Instant::now(),
3516 }
3517 }
3518}
3519
3520impl<T: Display> Drop for FunctionTimer<T> {
3521 fn drop(&mut self) {
3522 log::trace!("{} elapsed: {:?}", self.name, self.start.elapsed())
3523 }
3524}
3525
3526const DEFAULT_COLORS: [[f32; 4]; 20] = [
3527 [0., 1., 0., 0.7],
3528 [1., 0.5568628, 0., 0.7],
3529 [0.25882353, 0.15294118, 0.13333333, 0.7],
3530 [0.8, 0.7647059, 0.78039216, 0.7],
3531 [0.3137255, 0.3137255, 0.3137255, 0.7],
3532 [0.1411765, 0.3098039, 0.1215686, 0.7],
3533 [1., 0.95686275, 0.5137255, 0.7],
3534 [0.3529412, 0.32156863, 0., 0.7],
3535 [0.4235294, 0.6235294, 0.6509804, 0.7],
3536 [0.5098039, 0.5098039, 0.7294118, 0.7],
3537 [0.00784314, 0.18823529, 0.29411765, 0.7],
3538 [0.0, 0.2706, 1.0, 0.7],
3539 [0.0, 0.0, 0.0, 0.7],
3540 [0.0, 0.5, 0.0, 0.7],
3541 [1.0, 0.0, 0.0, 0.7],
3542 [0.0, 0.0, 1.0, 0.7],
3543 [1.0, 0.5, 0.5, 0.7],
3544 [0.1333, 0.5451, 0.1333, 0.7],
3545 [0.1176, 0.4118, 0.8235, 0.7],
3546 [1., 1., 1., 0.7],
3547];
3548
3549const fn denorm<const M: usize, const N: usize>(a: [[f32; M]; N]) -> [[u8; M]; N] {
3550 let mut result = [[0; M]; N];
3551 let mut i = 0;
3552 while i < N {
3553 let mut j = 0;
3554 while j < M {
3555 result[i][j] = (a[i][j] * 255.0).round() as u8;
3556 j += 1;
3557 }
3558 i += 1;
3559 }
3560 result
3561}
3562
3563const DEFAULT_COLORS_U8: [[u8; 4]; 20] = denorm(DEFAULT_COLORS);
3564
3565#[cfg(test)]
3566#[cfg_attr(coverage_nightly, coverage(off))]
3567mod alignment_tests {
3568 use super::*;
3569
3570 #[test]
3571 fn align_width_rgba8_common_widths() {
3572 assert_eq!(align_width_for_gpu_pitch(640, 4), 640); assert_eq!(align_width_for_gpu_pitch(1280, 4), 1280); assert_eq!(align_width_for_gpu_pitch(1920, 4), 1920); assert_eq!(align_width_for_gpu_pitch(3840, 4), 3840); assert_eq!(align_width_for_gpu_pitch(3004, 4), 3008); assert_eq!(align_width_for_gpu_pitch(3000, 4), 3008); assert_eq!(align_width_for_gpu_pitch(17, 4), 32); assert_eq!(align_width_for_gpu_pitch(1, 4), 16); }
3583
3584 #[test]
3585 fn align_width_rgb888_packed() {
3586 assert_eq!(align_width_for_gpu_pitch(64, 3), 64); assert_eq!(align_width_for_gpu_pitch(640, 3), 640); assert_eq!(align_width_for_gpu_pitch(1, 3), 64); assert_eq!(align_width_for_gpu_pitch(65, 3), 128); for w in [3004usize, 1281, 100, 17] {
3593 let padded = align_width_for_gpu_pitch(w, 3);
3594 assert!(padded >= w);
3595 assert_eq!((padded * 3) % 64, 0);
3596 assert_eq!((padded * 3) % 3, 0);
3597 }
3598 }
3599
3600 #[test]
3601 fn align_width_grey_u8() {
3602 assert_eq!(align_width_for_gpu_pitch(64, 1), 64);
3604 assert_eq!(align_width_for_gpu_pitch(640, 1), 640);
3605 assert_eq!(align_width_for_gpu_pitch(1, 1), 64);
3606 assert_eq!(align_width_for_gpu_pitch(65, 1), 128);
3607 }
3608
3609 #[test]
3610 fn align_width_zero_inputs() {
3611 assert_eq!(align_width_for_gpu_pitch(0, 4), 0);
3612 assert_eq!(align_width_for_gpu_pitch(640, 0), 640);
3613 }
3614
3615 #[test]
3616 fn align_width_never_returns_smaller_than_input() {
3617 for &bpp in &[1usize, 2, 3, 4, 8] {
3621 for &w in &[
3622 1usize,
3623 17,
3624 64,
3625 65,
3626 100,
3627 1280,
3628 1281,
3629 1920,
3630 3004,
3631 3072,
3632 3840,
3633 usize::MAX / 8,
3634 usize::MAX / 4,
3635 usize::MAX / 2,
3636 usize::MAX - 1,
3637 usize::MAX,
3638 ] {
3639 let aligned = align_width_for_gpu_pitch(w, bpp);
3640 assert!(
3641 aligned >= w,
3642 "align_width_for_gpu_pitch({w}, {bpp}) = {aligned} < {w}"
3643 );
3644 }
3645 }
3646 }
3647
3648 #[test]
3649 fn align_width_overflow_returns_unaligned_not_smaller() {
3650 let aligned_extreme = usize::MAX - 15; assert_eq!(
3656 align_width_for_gpu_pitch(aligned_extreme, 4),
3657 aligned_extreme
3658 );
3659 let misaligned_extreme = usize::MAX - 1;
3662 let result = align_width_for_gpu_pitch(misaligned_extreme, 4);
3663 assert!(
3664 result == misaligned_extreme || result >= misaligned_extreme,
3665 "extreme misaligned width must not be rounded down to {result}"
3666 );
3667 }
3668
3669 #[test]
3670 fn checked_lcm_basic_and_overflow() {
3671 assert_eq!(checked_num_integer_lcm(64, 4), Some(64));
3672 assert_eq!(checked_num_integer_lcm(64, 3), Some(192));
3673 assert_eq!(checked_num_integer_lcm(64, 1), Some(64));
3674 assert_eq!(checked_num_integer_lcm(0, 4), Some(0));
3675 assert_eq!(checked_num_integer_lcm(64, 0), Some(0));
3676 assert_eq!(
3678 checked_num_integer_lcm(usize::MAX, usize::MAX - 1),
3679 None,
3680 "coprime extreme values must overflow detect, not panic"
3681 );
3682 }
3683
3684 #[test]
3685 fn primary_plane_bpp_known_formats() {
3686 assert_eq!(primary_plane_bpp(PixelFormat::Rgba, 1), Some(4));
3688 assert_eq!(primary_plane_bpp(PixelFormat::Bgra, 1), Some(4));
3689 assert_eq!(primary_plane_bpp(PixelFormat::Rgb, 1), Some(3));
3690 assert_eq!(primary_plane_bpp(PixelFormat::Grey, 1), Some(1));
3691 assert_eq!(primary_plane_bpp(PixelFormat::Nv12, 1), Some(1));
3693 }
3694}
3695
3696#[cfg(test)]
3697#[cfg_attr(coverage_nightly, coverage(off))]
3698#[allow(deprecated)]
3699mod image_tests {
3700 use super::*;
3701 use crate::{CPUProcessor, Rotation};
3702 #[cfg(target_os = "linux")]
3703 use edgefirst_tensor::is_dma_available;
3704 use edgefirst_tensor::{TensorMapTrait, TensorMemory, TensorTrait};
3705 use image::buffer::ConvertBuffer;
3706
3707 fn convert_img(
3713 proc: &mut dyn ImageProcessorTrait,
3714 src: TensorDyn,
3715 dst: TensorDyn,
3716 rotation: Rotation,
3717 flip: Flip,
3718 crop: Crop,
3719 ) -> (Result<()>, TensorDyn, TensorDyn) {
3720 let src_fourcc = src.format().unwrap();
3721 let dst_fourcc = dst.format().unwrap();
3722 let src_dyn = src;
3723 let mut dst_dyn = dst;
3724 let result = proc.convert(&src_dyn, &mut dst_dyn, rotation, flip, crop);
3725 let src_back = {
3726 let mut __t = src_dyn.into_u8().unwrap();
3727 __t.set_format(src_fourcc).unwrap();
3728 TensorDyn::from(__t)
3729 };
3730 let dst_back = {
3731 let mut __t = dst_dyn.into_u8().unwrap();
3732 __t.set_format(dst_fourcc).unwrap();
3733 TensorDyn::from(__t)
3734 };
3735 (result, src_back, dst_back)
3736 }
3737
3738 #[ctor::ctor(unsafe)]
3739 fn init() {
3740 env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
3741 }
3742
3743 macro_rules! function {
3744 () => {{
3745 fn f() {}
3746 fn type_name_of<T>(_: T) -> &'static str {
3747 std::any::type_name::<T>()
3748 }
3749 let name = type_name_of(f);
3750
3751 match &name[..name.len() - 3].rfind(':') {
3753 Some(pos) => &name[pos + 1..name.len() - 3],
3754 None => &name[..name.len() - 3],
3755 }
3756 }};
3757 }
3758
3759 #[test]
3772 fn batch_view_dst_tiles_match_standalone() {
3773 let mut proc = match ImageProcessor::new() {
3774 Ok(p) => p,
3775 Err(e) => {
3776 crate::test_support::report_skip(&format!(
3777 "{} — ImageProcessor init failed ({e:?})",
3778 function!()
3779 ));
3780 return;
3781 }
3782 };
3783 let n = 3usize;
3784 let (w, h) = (32usize, 24usize);
3785 let colors: [[u8; 4]; 3] = [[210, 40, 40, 255], [40, 210, 40, 255], [40, 40, 210, 255]];
3786 let make_src = |c: [u8; 4]| -> TensorDyn {
3787 let bytes: Vec<u8> = c.iter().copied().cycle().take(w * h * 4).collect();
3788 load_bytes_to_tensor(w, h, PixelFormat::Rgba, Some(TensorMemory::Mem), &bytes).unwrap()
3789 };
3790 let parent = match TensorDyn::image(
3793 w,
3794 n * h,
3795 PixelFormat::Rgba,
3796 DType::U8,
3797 Some(TensorMemory::DmaBuf),
3798 edgefirst_tensor::CpuAccess::ReadWrite,
3799 ) {
3800 Ok(d) => d,
3801 Err(e) => {
3802 crate::test_support::report_skip(&format!(
3803 "{} — tall DMA destination alloc failed ({e:?})",
3804 function!()
3805 ));
3806 return;
3807 }
3808 };
3809
3810 for (i, &c) in colors.iter().enumerate().take(n) {
3812 let mut tile = parent.view(Region::new(0, i * h, w, h)).unwrap();
3813 proc.convert_deferred(
3814 &make_src(c),
3815 &mut tile,
3816 Rotation::None,
3817 Flip::None,
3818 Crop::no_crop(),
3819 )
3820 .unwrap_or_else(|e| panic!("convert_deferred tile {i}: {e:?}"));
3821 }
3822 proc.flush().unwrap();
3823
3824 for (i, &c) in colors.iter().enumerate().take(n) {
3825 let mut solo = TensorDyn::image(
3827 w,
3828 h,
3829 PixelFormat::Rgba,
3830 DType::U8,
3831 Some(TensorMemory::DmaBuf),
3832 edgefirst_tensor::CpuAccess::ReadWrite,
3833 )
3834 .unwrap();
3835 proc.convert(
3836 &make_src(c),
3837 &mut solo,
3838 Rotation::None,
3839 Flip::None,
3840 Crop::no_crop(),
3841 )
3842 .unwrap();
3843
3844 let band = parent.view(Region::new(0, i * h, w, h)).unwrap();
3845 let band_bytes = band
3846 .as_typed::<u8>()
3847 .unwrap()
3848 .map()
3849 .unwrap()
3850 .as_slice()
3851 .to_vec();
3852 let solo_bytes = solo
3853 .as_typed::<u8>()
3854 .unwrap()
3855 .map()
3856 .unwrap()
3857 .as_slice()
3858 .to_vec();
3859 assert_eq!(
3860 band_bytes, solo_bytes,
3861 "tile {i}: band differs from standalone convert (placement or sibling wipe)"
3862 );
3863 assert!(
3864 band_bytes.as_chunks::<4>().0.iter().all(|&p| p == c),
3865 "tile {i}: band is not the expected solid color {c:?} (sibling wipe?)"
3866 );
3867 }
3868 }
3869
3870 #[cfg(test)]
3874 fn gradient_frame(w: usize, h: usize) -> TensorDyn {
3875 let mut bytes = vec![0u8; w * h * 4];
3876 for y in 0..h {
3877 for x in 0..w {
3878 let i = (y * w + x) * 4;
3879 bytes[i] = x as u8;
3880 bytes[i + 1] = y as u8;
3881 bytes[i + 2] = (x ^ y) as u8;
3882 bytes[i + 3] = 255;
3883 }
3884 }
3885 load_bytes_to_tensor(w, h, PixelFormat::Rgba, Some(TensorMemory::Mem), &bytes).unwrap()
3886 }
3887
3888 #[test]
3892 fn tile_into_cpu_distinct_content_parity() {
3893 let mut proc = match ImageProcessor::with_config(ImageProcessorConfig {
3894 backend: ComputeBackend::Cpu,
3895 ..Default::default()
3896 }) {
3897 Ok(p) => p,
3898 Err(e) => {
3899 crate::test_support::report_skip(&format!(
3900 "{} — CPU init failed ({e:?})",
3901 function!()
3902 ));
3903 return;
3904 }
3905 };
3906 let (fw, fh) = (96usize, 64usize);
3907 let src = gradient_frame(fw, fh);
3908 let cfg = TilingConfig::new(32, 32).with_overlap(0.0); let n = tile_grid(fh, fw, 32, 32, 0.0).len();
3910 assert_eq!(n, 6);
3911
3912 let mut parent = proc
3913 .alloc_tile_batch(
3914 n,
3915 &cfg,
3916 PixelFormat::Rgba,
3917 DType::U8,
3918 Some(TensorMemory::Mem),
3919 edgefirst_tensor::CpuAccess::ReadWrite,
3920 )
3921 .unwrap();
3922 let placements = proc.tile_into(&src, &mut parent, &cfg).unwrap();
3923 assert_eq!(placements.len(), n);
3924
3925 for p in &placements {
3926 let source = Region::new(
3927 p.origin.0 as usize,
3928 p.origin.1 as usize,
3929 p.crop_size.0 as usize,
3930 p.crop_size.1 as usize,
3931 );
3932 let mut solo = TensorDyn::image(
3933 32,
3934 32,
3935 PixelFormat::Rgba,
3936 DType::U8,
3937 Some(TensorMemory::Mem),
3938 edgefirst_tensor::CpuAccess::ReadWrite,
3939 )
3940 .unwrap();
3941 proc.convert(
3942 &src,
3943 &mut solo,
3944 Rotation::None,
3945 Flip::None,
3946 Crop::default()
3947 .with_source(Some(source))
3948 .with_fit(Fit::Stretch),
3949 )
3950 .unwrap();
3951 let band = parent.view(Region::new(0, p.index * 32, 32, 32)).unwrap();
3952 let band_bytes = band
3953 .as_typed::<u8>()
3954 .unwrap()
3955 .map()
3956 .unwrap()
3957 .as_slice()
3958 .to_vec();
3959 let solo_bytes = solo
3960 .as_typed::<u8>()
3961 .unwrap()
3962 .map()
3963 .unwrap()
3964 .as_slice()
3965 .to_vec();
3966 assert_eq!(
3967 band_bytes, solo_bytes,
3968 "tile {} band differs from standalone crop-convert",
3969 p.index
3970 );
3971 }
3972 }
3973
3974 #[test]
3977 fn tile_one_matches_tile_into_band() {
3978 let mut proc = match ImageProcessor::with_config(ImageProcessorConfig {
3979 backend: ComputeBackend::Cpu,
3980 ..Default::default()
3981 }) {
3982 Ok(p) => p,
3983 Err(e) => {
3984 crate::test_support::report_skip(&format!(
3985 "{} — CPU init failed ({e:?})",
3986 function!()
3987 ));
3988 return;
3989 }
3990 };
3991 let (fw, fh) = (96usize, 64usize);
3992 let src = gradient_frame(fw, fh);
3993 let cfg = TilingConfig::new(32, 32).with_overlap(0.0);
3994 let n = tile_grid(fh, fw, 32, 32, 0.0).len();
3995
3996 let mut parent = proc
3997 .alloc_tile_batch(
3998 n,
3999 &cfg,
4000 PixelFormat::Rgba,
4001 DType::U8,
4002 Some(TensorMemory::Mem),
4003 edgefirst_tensor::CpuAccess::ReadWrite,
4004 )
4005 .unwrap();
4006 proc.tile_into(&src, &mut parent, &cfg).unwrap();
4007
4008 let plan = proc.plan_tiles(fw, fh, &cfg).unwrap();
4009 for p in &plan {
4010 let mut slot = TensorDyn::image(
4011 32,
4012 32,
4013 PixelFormat::Rgba,
4014 DType::U8,
4015 Some(TensorMemory::Mem),
4016 edgefirst_tensor::CpuAccess::ReadWrite,
4017 )
4018 .unwrap();
4019 proc.tile_one(&src, &mut slot, p, &cfg).unwrap();
4020 proc.flush().unwrap();
4021 let band = parent.view(Region::new(0, p.index * 32, 32, 32)).unwrap();
4022 let slot_bytes = slot
4023 .as_typed::<u8>()
4024 .unwrap()
4025 .map()
4026 .unwrap()
4027 .as_slice()
4028 .to_vec();
4029 let band_bytes = band
4030 .as_typed::<u8>()
4031 .unwrap()
4032 .map()
4033 .unwrap()
4034 .as_slice()
4035 .to_vec();
4036 assert_eq!(
4037 slot_bytes, band_bytes,
4038 "tile {} stream != batch band",
4039 p.index
4040 );
4041 }
4042 }
4043
4044 #[test]
4051 fn tile_into_auto_dma_parity() {
4052 let mut proc = match ImageProcessor::new() {
4053 Ok(p) => p,
4054 Err(e) => {
4055 crate::test_support::report_skip(&format!(
4056 "{} — ImageProcessor init failed ({e:?})",
4057 function!()
4058 ));
4059 return;
4060 }
4061 };
4062 let (fw, fh) = (96usize, 64usize);
4063 let src = gradient_frame(fw, fh);
4064 let cfg = TilingConfig::new(32, 32).with_overlap(0.0);
4065 let n = tile_grid(fh, fw, 32, 32, 0.0).len();
4066
4067 let mut parent = match proc.alloc_tile_batch(
4068 n,
4069 &cfg,
4070 PixelFormat::Rgba,
4071 DType::U8,
4072 Some(TensorMemory::DmaBuf),
4073 edgefirst_tensor::CpuAccess::ReadWrite,
4074 ) {
4075 Ok(p) => p,
4076 Err(e) => {
4077 crate::test_support::report_skip(&format!(
4078 "{} — tall DMA parent alloc failed ({e:?})",
4079 function!()
4080 ));
4081 return;
4082 }
4083 };
4084 let placements = proc.tile_into(&src, &mut parent, &cfg).unwrap();
4085
4086 for p in &placements {
4087 let source = Region::new(
4088 p.origin.0 as usize,
4089 p.origin.1 as usize,
4090 p.crop_size.0 as usize,
4091 p.crop_size.1 as usize,
4092 );
4093 let mut solo = TensorDyn::image(
4094 32,
4095 32,
4096 PixelFormat::Rgba,
4097 DType::U8,
4098 Some(TensorMemory::DmaBuf),
4099 edgefirst_tensor::CpuAccess::ReadWrite,
4100 )
4101 .unwrap();
4102 proc.convert(
4103 &src,
4104 &mut solo,
4105 Rotation::None,
4106 Flip::None,
4107 Crop::default()
4108 .with_source(Some(source))
4109 .with_fit(Fit::Stretch),
4110 )
4111 .unwrap();
4112 let band = parent.view(Region::new(0, p.index * 32, 32, 32)).unwrap();
4113 compare_images(
4119 &band,
4120 &solo,
4121 0.98,
4122 &format!("{}_tile{}", function!(), p.index),
4123 );
4124 }
4125 }
4126
4127 #[test]
4129 fn tile_into_undersized_dst_errors() {
4130 let mut proc = match ImageProcessor::with_config(ImageProcessorConfig {
4131 backend: ComputeBackend::Cpu,
4132 ..Default::default()
4133 }) {
4134 Ok(p) => p,
4135 Err(e) => {
4136 crate::test_support::report_skip(&format!(
4137 "{} — CPU init failed ({e:?})",
4138 function!()
4139 ));
4140 return;
4141 }
4142 };
4143 let (fw, fh) = (96usize, 64usize);
4144 let src = gradient_frame(fw, fh);
4145 let cfg = TilingConfig::new(32, 32).with_overlap(0.0); let mut small = TensorDyn::image(
4148 32,
4149 2 * 32,
4150 PixelFormat::Rgba,
4151 DType::U8,
4152 Some(TensorMemory::Mem),
4153 edgefirst_tensor::CpuAccess::ReadWrite,
4154 )
4155 .unwrap();
4156 let r = proc.tile_into(&src, &mut small, &cfg);
4157 assert!(
4158 matches!(r, Err(Error::InvalidShape(_))),
4159 "expected InvalidShape, got {r:?}"
4160 );
4161 }
4162
4163 #[test]
4165 fn tiling_alloc_rejects_invalid_config() {
4166 let proc = match ImageProcessor::with_config(ImageProcessorConfig {
4167 backend: ComputeBackend::Cpu,
4168 ..Default::default()
4169 }) {
4170 Ok(p) => p,
4171 Err(e) => {
4172 crate::test_support::report_skip(&format!(
4173 "{} — CPU init failed ({e:?})",
4174 function!()
4175 ));
4176 return;
4177 }
4178 };
4179 let bad = TilingConfig::new(0, 640);
4180 assert!(proc.plan_tiles(1920, 1080, &bad).is_err());
4181 assert!(proc
4182 .alloc_tile_batch(
4183 4,
4184 &bad,
4185 PixelFormat::Rgba,
4186 DType::U8,
4187 Some(TensorMemory::Mem),
4188 edgefirst_tensor::CpuAccess::ReadWrite,
4189 )
4190 .is_err());
4191 }
4192
4193 #[test]
4194 fn plan_tiles_metadata_4k() {
4195 let proc = match ImageProcessor::with_config(ImageProcessorConfig {
4196 backend: ComputeBackend::Cpu,
4197 ..Default::default()
4198 }) {
4199 Ok(p) => p,
4200 Err(e) => {
4201 crate::test_support::report_skip(&format!(
4202 "{} — CPU init failed ({e:?})",
4203 function!()
4204 ));
4205 return;
4206 }
4207 };
4208 let cfg = TilingConfig::new(640, 640).with_overlap(0.2);
4209 let plan = proc.plan_tiles(3840, 2160, &cfg).unwrap();
4210 assert_eq!(plan.len(), 32);
4211 assert!(plan.iter().all(|p| p.count == 32));
4212 assert!(plan.iter().all(|p| p.crop_size == (640.0, 640.0)));
4213 assert!(plan.iter().all(|p| p.letterbox.is_none())); assert!(plan.iter().all(|p| p.frame_dims == (3840.0, 2160.0)));
4215 assert_eq!(plan[0].origin, (0.0, 0.0));
4216 }
4217
4218 #[test]
4219 fn test_invalid_crop() {
4220 let src = TensorDyn::image(
4221 100,
4222 100,
4223 PixelFormat::Rgb,
4224 DType::U8,
4225 None,
4226 edgefirst_tensor::CpuAccess::ReadWrite,
4227 )
4228 .unwrap();
4229 let dst = TensorDyn::image(
4230 100,
4231 100,
4232 PixelFormat::Rgb,
4233 DType::U8,
4234 None,
4235 edgefirst_tensor::CpuAccess::ReadWrite,
4236 )
4237 .unwrap();
4238
4239 let crop = Crop::new().with_source(Some(Region::new(50, 50, 60, 60)));
4241 assert!(matches!(
4242 crop.check_crop_dyn(&src, &dst),
4243 Err(Error::CropInvalid(_))
4244 ));
4245
4246 let crop = Crop::new().with_source(Some(Region::new(0, 0, 10, 10)));
4248 assert!(crop.check_crop_dyn(&src, &dst).is_ok());
4249
4250 assert!(Crop::letterbox([0, 0, 0, 255])
4252 .check_crop_dyn(&src, &dst)
4253 .is_ok());
4254 }
4255
4256 #[test]
4257 fn test_invalid_tensor_format() -> Result<(), Error> {
4258 let mut tensor = Tensor::<u8>::new(&[720, 1280, 4, 1], None, None)?;
4260 let result = tensor.set_format(PixelFormat::Rgb);
4261 assert!(result.is_err(), "4D tensor should reject set_format");
4262
4263 let mut tensor = Tensor::<u8>::new(&[720, 1280, 4], None, None)?;
4265 let result = tensor.set_format(PixelFormat::Rgb);
4266 assert!(result.is_err(), "4-channel tensor should reject RGB format");
4267
4268 Ok(())
4269 }
4270
4271 #[test]
4272 fn test_invalid_image_file() -> Result<(), Error> {
4273 let result = crate::load_image_test_helper(&[123; 5000], None, None);
4274 assert!(
4275 matches!(result, Err(Error::Codec(_))),
4276 "unrecognised bytes should surface as Error::Codec, got {result:?}"
4277 );
4278 Ok(())
4279 }
4280
4281 #[test]
4282 fn test_invalid_jpeg_format() -> Result<(), Error> {
4283 let result = crate::load_image_test_helper(&[123; 5000], Some(PixelFormat::Yuyv), None);
4284 assert!(
4287 matches!(result, Err(Error::Codec(_))),
4288 "Yuyv target with garbage bytes should surface as Error::Codec, got {result:?}"
4289 );
4290 Ok(())
4291 }
4292
4293 #[test]
4294 fn test_load_resize_save() {
4295 let file = edgefirst_bench::testdata::read("zidane.jpg");
4296 let img = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4297 assert_eq!(img.width(), Some(1280));
4298 assert_eq!(img.height(), Some(720));
4299
4300 let dst = TensorDyn::image(
4301 640,
4302 360,
4303 PixelFormat::Rgba,
4304 DType::U8,
4305 None,
4306 edgefirst_tensor::CpuAccess::ReadWrite,
4307 )
4308 .unwrap();
4309 let mut converter = CPUProcessor::new();
4310 let (result, _img, dst) = convert_img(
4311 &mut converter,
4312 img,
4313 dst,
4314 Rotation::None,
4315 Flip::None,
4316 Crop::no_crop(),
4317 );
4318 result.unwrap();
4319 assert_eq!(dst.width(), Some(640));
4320 assert_eq!(dst.height(), Some(360));
4321
4322 crate::save_jpeg(&dst, "zidane_resized.jpg", 80).unwrap();
4323
4324 let file = std::fs::read("zidane_resized.jpg").unwrap();
4325 let img = crate::load_image_test_helper(&file, None, None).unwrap();
4328 assert_eq!(img.width(), Some(640));
4329 assert_eq!(img.height(), Some(360));
4330 assert_eq!(img.format().unwrap(), PixelFormat::Nv12);
4331 }
4332
4333 #[test]
4334 fn test_from_tensor_planar() -> Result<(), Error> {
4335 let mut tensor = Tensor::new(&[3, 720, 1280], None, None)?;
4336 tensor
4337 .map()?
4338 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.8bps"));
4339 let planar = {
4340 tensor
4341 .set_format(PixelFormat::PlanarRgb)
4342 .map_err(|e| crate::Error::Internal(e.to_string()))?;
4343 TensorDyn::from(tensor)
4344 };
4345
4346 let rbga = load_bytes_to_tensor(
4347 1280,
4348 720,
4349 PixelFormat::Rgba,
4350 None,
4351 &edgefirst_bench::testdata::read("camera720p.rgba"),
4352 )?;
4353 compare_images_convert_to_rgb(&planar, &rbga, 0.98, function!());
4354
4355 Ok(())
4356 }
4357
4358 #[test]
4359 fn test_from_tensor_invalid_format() {
4360 assert!(PixelFormat::from_fourcc(u32::from_le_bytes(*b"TEST")).is_none());
4363 }
4364
4365 #[test]
4366 #[should_panic(expected = "Failed to save planar RGB image")]
4367 fn test_save_planar() {
4368 let planar_img = load_bytes_to_tensor(
4369 1280,
4370 720,
4371 PixelFormat::PlanarRgb,
4372 None,
4373 &edgefirst_bench::testdata::read("camera720p.8bps"),
4374 )
4375 .unwrap();
4376
4377 let save_path = "/tmp/planar_rgb.jpg";
4378 crate::save_jpeg(&planar_img, save_path, 90).expect("Failed to save planar RGB image");
4379 }
4380
4381 #[test]
4382 #[should_panic(expected = "Failed to save YUYV image")]
4383 fn test_save_yuyv() {
4384 let planar_img = load_bytes_to_tensor(
4385 1280,
4386 720,
4387 PixelFormat::Yuyv,
4388 None,
4389 &edgefirst_bench::testdata::read("camera720p.yuyv"),
4390 )
4391 .unwrap();
4392
4393 let save_path = "/tmp/yuyv.jpg";
4394 crate::save_jpeg(&planar_img, save_path, 90).expect("Failed to save YUYV image");
4395 }
4396
4397 #[test]
4398 fn test_rotation_angle() {
4399 assert_eq!(Rotation::from_degrees_clockwise(0), Rotation::None);
4400 assert_eq!(Rotation::from_degrees_clockwise(90), Rotation::Clockwise90);
4401 assert_eq!(Rotation::from_degrees_clockwise(180), Rotation::Rotate180);
4402 assert_eq!(
4403 Rotation::from_degrees_clockwise(270),
4404 Rotation::CounterClockwise90
4405 );
4406 assert_eq!(Rotation::from_degrees_clockwise(360), Rotation::None);
4407 assert_eq!(Rotation::from_degrees_clockwise(450), Rotation::Clockwise90);
4408 assert_eq!(Rotation::from_degrees_clockwise(540), Rotation::Rotate180);
4409 assert_eq!(
4410 Rotation::from_degrees_clockwise(630),
4411 Rotation::CounterClockwise90
4412 );
4413 }
4414
4415 #[test]
4416 #[should_panic(expected = "rotation angle is not a multiple of 90")]
4417 fn test_rotation_angle_panic() {
4418 Rotation::from_degrees_clockwise(361);
4419 }
4420
4421 #[test]
4422 fn test_disable_env_var() -> Result<(), Error> {
4423 let _lock = acquire_env_lock();
4426
4427 let _guard = EnvGuard::snapshot(&[
4430 "EDGEFIRST_FORCE_BACKEND",
4431 "EDGEFIRST_DISABLE_GL",
4432 "EDGEFIRST_DISABLE_G2D",
4433 "EDGEFIRST_DISABLE_CPU",
4434 ]);
4435
4436 unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") };
4439
4440 #[cfg(target_os = "linux")]
4441 {
4442 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
4443 let converter = ImageProcessor::new()?;
4444 assert!(converter.g2d.is_none());
4445 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_G2D") };
4446 }
4447
4448 #[cfg(any(target_os = "linux", target_os = "windows"))]
4449 #[cfg(feature = "opengl")]
4450 {
4451 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
4452 let converter = ImageProcessor::new()?;
4453 assert!(converter.opengl.is_none());
4454 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_GL") };
4455 }
4456
4457 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
4458 let converter = ImageProcessor::new()?;
4459 assert!(converter.cpu.is_none());
4460 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_CPU") };
4461
4462 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
4464 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
4465 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
4466 let mut converter = ImageProcessor::new()?;
4467
4468 let src = TensorDyn::image(
4469 1280,
4470 720,
4471 PixelFormat::Rgba,
4472 DType::U8,
4473 None,
4474 edgefirst_tensor::CpuAccess::ReadWrite,
4475 )?;
4476 let dst = TensorDyn::image(
4477 640,
4478 360,
4479 PixelFormat::Rgba,
4480 DType::U8,
4481 None,
4482 edgefirst_tensor::CpuAccess::ReadWrite,
4483 )?;
4484 let (result, _src, _dst) = convert_img(
4485 &mut converter,
4486 src,
4487 dst,
4488 Rotation::None,
4489 Flip::None,
4490 Crop::no_crop(),
4491 );
4492 assert!(matches!(result, Err(Error::NoConverter)));
4493 Ok(())
4495 }
4496
4497 #[test]
4498 fn test_unsupported_conversion() {
4499 let src = TensorDyn::image(
4500 1280,
4501 720,
4502 PixelFormat::Nv12,
4503 DType::U8,
4504 None,
4505 edgefirst_tensor::CpuAccess::ReadWrite,
4506 )
4507 .unwrap();
4508 let dst = TensorDyn::image(
4509 640,
4510 360,
4511 PixelFormat::Nv12,
4512 DType::U8,
4513 None,
4514 edgefirst_tensor::CpuAccess::ReadWrite,
4515 )
4516 .unwrap();
4517 let mut converter = ImageProcessor::new().unwrap();
4518 let (result, _src, _dst) = convert_img(
4519 &mut converter,
4520 src,
4521 dst,
4522 Rotation::None,
4523 Flip::None,
4524 Crop::no_crop(),
4525 );
4526 log::debug!("result: {:?}", result);
4527 assert!(matches!(
4528 result,
4529 Err(Error::NotSupported(e)) if e.starts_with("Conversion from NV12 to NV12")
4530 ));
4531 }
4532
4533 #[test]
4534 fn test_load_grey() {
4535 let grey_img = crate::load_image_test_helper(
4539 &edgefirst_bench::testdata::read("grey.jpg"),
4540 Some(PixelFormat::Rgba),
4541 None,
4542 )
4543 .unwrap();
4544 assert_eq!(grey_img.width(), Some(1024));
4545 assert_eq!(grey_img.height(), Some(681));
4546
4547 let grey_but_rgb = crate::load_image_test_helper(
4553 &edgefirst_bench::testdata::read("grey-rgb.jpg"),
4554 Some(PixelFormat::Rgba),
4555 None,
4556 )
4557 .expect("odd-height colour JPEG should decode to NV12 and convert to RGBA");
4558 assert_eq!(grey_but_rgb.width(), Some(1024));
4559 assert_eq!(grey_but_rgb.height(), Some(681));
4560 }
4561
4562 #[test]
4563 fn test_new_nv12() {
4564 let nv12 = TensorDyn::image(
4565 1280,
4566 720,
4567 PixelFormat::Nv12,
4568 DType::U8,
4569 None,
4570 edgefirst_tensor::CpuAccess::ReadWrite,
4571 )
4572 .unwrap();
4573 assert_eq!(nv12.height(), Some(720));
4574 assert_eq!(nv12.width(), Some(1280));
4575 assert_eq!(nv12.format().unwrap(), PixelFormat::Nv12);
4576 assert_eq!(nv12.format().unwrap().channels(), 1);
4578 assert!(nv12.format().is_some_and(
4579 |f| f.layout() == PixelLayout::Planar || f.layout() == PixelLayout::SemiPlanar
4580 ))
4581 }
4582
4583 #[test]
4584 #[cfg(any(target_os = "linux", target_os = "windows"))]
4585 fn test_new_image_converter() {
4586 let dst_width = 640;
4587 let dst_height = 360;
4588 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4589 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4590
4591 let mut converter = ImageProcessor::new().unwrap();
4592 let converter_dst = converter
4593 .create_image(
4594 dst_width,
4595 dst_height,
4596 PixelFormat::Rgba,
4597 DType::U8,
4598 None,
4599 edgefirst_tensor::CpuAccess::ReadWrite,
4600 )
4601 .unwrap();
4602 let (result, src, converter_dst) = convert_img(
4603 &mut converter,
4604 src,
4605 converter_dst,
4606 Rotation::None,
4607 Flip::None,
4608 Crop::no_crop(),
4609 );
4610 result.unwrap();
4611
4612 let cpu_dst = TensorDyn::image(
4613 dst_width,
4614 dst_height,
4615 PixelFormat::Rgba,
4616 DType::U8,
4617 None,
4618 edgefirst_tensor::CpuAccess::ReadWrite,
4619 )
4620 .unwrap();
4621 let mut cpu_converter = CPUProcessor::new();
4622 let (result, _src, cpu_dst) = convert_img(
4623 &mut cpu_converter,
4624 src,
4625 cpu_dst,
4626 Rotation::None,
4627 Flip::None,
4628 Crop::no_crop(),
4629 );
4630 result.unwrap();
4631
4632 compare_images(&converter_dst, &cpu_dst, 0.98, function!());
4633 }
4634
4635 #[test]
4636 #[cfg(any(target_os = "linux", target_os = "windows"))]
4637 fn test_create_image_dtype_i8() {
4638 let mut converter = ImageProcessor::new().unwrap();
4639
4640 let dst = converter
4642 .create_image(
4643 320,
4644 240,
4645 PixelFormat::Rgb,
4646 DType::I8,
4647 None,
4648 edgefirst_tensor::CpuAccess::ReadWrite,
4649 )
4650 .unwrap();
4651 assert_eq!(dst.dtype(), DType::I8);
4652 assert!(dst.width() == Some(320));
4653 assert!(dst.height() == Some(240));
4654 assert_eq!(dst.format(), Some(PixelFormat::Rgb));
4655
4656 let dst_u8 = converter
4658 .create_image(
4659 320,
4660 240,
4661 PixelFormat::Rgb,
4662 DType::U8,
4663 None,
4664 edgefirst_tensor::CpuAccess::ReadWrite,
4665 )
4666 .unwrap();
4667 assert_eq!(dst_u8.dtype(), DType::U8);
4668
4669 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4671 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4672 let mut dst_i8 = converter
4673 .create_image(
4674 320,
4675 240,
4676 PixelFormat::Rgb,
4677 DType::I8,
4678 None,
4679 edgefirst_tensor::CpuAccess::ReadWrite,
4680 )
4681 .unwrap();
4682 converter
4683 .convert(
4684 &src,
4685 &mut dst_i8,
4686 Rotation::None,
4687 Flip::None,
4688 Crop::no_crop(),
4689 )
4690 .unwrap();
4691 }
4692
4693 #[test]
4694 #[cfg(target_os = "linux")]
4695 fn test_create_image_nv12_dma_non_aligned_width() {
4696 let converter = ImageProcessor::new().unwrap();
4702
4703 let result = converter.create_image(
4705 100,
4706 64,
4707 PixelFormat::Nv12,
4708 DType::U8,
4709 Some(TensorMemory::DmaBuf),
4710 edgefirst_tensor::CpuAccess::ReadWrite,
4711 );
4712
4713 match result {
4714 Ok(img) => {
4715 assert_eq!(img.width(), Some(100));
4716 assert_eq!(img.height(), Some(64));
4717 assert_eq!(img.format(), Some(PixelFormat::Nv12));
4718 if let Some(stride) = img.row_stride() {
4719 assert!(
4720 stride >= 100,
4721 "NV12 row_stride {stride} must be >= the logical width (100)",
4722 );
4723 }
4724 }
4725 Err(e) => {
4726 crate::test_support::report_skip(&format!(
4728 "create_image NV12 DMA non-aligned width: {e}"
4729 ));
4730 }
4731 }
4732 }
4733
4734 #[test]
4735 #[ignore] fn test_crop_skip() {
4739 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4740 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4741
4742 let mut converter = ImageProcessor::new().unwrap();
4743 let converter_dst = converter
4744 .create_image(
4745 1280,
4746 720,
4747 PixelFormat::Rgba,
4748 DType::U8,
4749 None,
4750 edgefirst_tensor::CpuAccess::ReadWrite,
4751 )
4752 .unwrap();
4753 let crop = Crop::new().with_source(Some(Region::new(0, 0, 640, 640)));
4754 let (result, src, converter_dst) = convert_img(
4755 &mut converter,
4756 src,
4757 converter_dst,
4758 Rotation::None,
4759 Flip::None,
4760 crop,
4761 );
4762 result.unwrap();
4763
4764 let cpu_dst = TensorDyn::image(
4765 1280,
4766 720,
4767 PixelFormat::Rgba,
4768 DType::U8,
4769 None,
4770 edgefirst_tensor::CpuAccess::ReadWrite,
4771 )
4772 .unwrap();
4773 let mut cpu_converter = CPUProcessor::new();
4774 let (result, _src, cpu_dst) = convert_img(
4775 &mut cpu_converter,
4776 src,
4777 cpu_dst,
4778 Rotation::None,
4779 Flip::None,
4780 crop,
4781 );
4782 result.unwrap();
4783
4784 compare_images(&converter_dst, &cpu_dst, 0.99999, function!());
4785 }
4786
4787 #[test]
4788 fn test_invalid_pixel_format() {
4789 assert!(PixelFormat::from_fourcc(u32::from_le_bytes(*b"TEST")).is_none());
4792 }
4793
4794 #[cfg(target_os = "linux")]
4796 static G2D_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4797
4798 #[cfg(target_os = "linux")]
4799 fn is_g2d_available() -> bool {
4800 *G2D_AVAILABLE.get_or_init(|| G2DProcessor::new().is_ok())
4801 }
4802
4803 #[cfg(any(
4809 target_os = "linux",
4810 target_os = "macos",
4811 target_os = "ios",
4812 target_os = "android",
4813 target_os = "windows"
4814 ))]
4815 #[cfg(feature = "opengl")]
4816 static GL_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4817
4818 #[cfg(any(
4819 target_os = "linux",
4820 target_os = "macos",
4821 target_os = "ios",
4822 target_os = "android",
4823 target_os = "windows"
4824 ))]
4825 #[cfg(feature = "opengl")]
4826 fn is_opengl_available() -> bool {
4828 *GL_AVAILABLE.get_or_init(|| GLProcessorThreaded::new(None).is_ok())
4829 }
4830
4831 #[test]
4843 #[cfg(feature = "opengl")]
4844 fn gl_backend_available_canary() {
4845 let require_gl = std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1");
4846 if !require_gl {
4847 crate::test_support::report_skip(&format!(
4848 "{} — HAL_TEST_REQUIRE_GL is not set to 1",
4849 function!()
4850 ));
4851 return;
4852 }
4853 #[cfg(target_os = "macos")]
4854 if std::env::var_os("HAL_TEST_ALLOW_DLOPEN_ANGLE").is_none() {
4855 crate::test_support::report_skip(&format!(
4856 "{} — ANGLE dlopen gate closed (coverage pass 1)",
4857 function!()
4858 ));
4859 return;
4860 }
4861 GLProcessorThreaded::new(None).expect(
4862 "HAL_TEST_REQUIRE_GL=1 but the GL backend failed to initialize — \
4863 check the ANGLE install/re-sign step and binary entitlements \
4864 (macOS) or the EGL stack (Linux)",
4865 );
4866 }
4867
4868 #[test]
4869 fn test_load_jpeg_with_exif() {
4870 use edgefirst_codec::peek_info;
4871
4872 let file = edgefirst_bench::testdata::read("zidane_rotated_exif.jpg").to_vec();
4877 let info = peek_info(&file).unwrap();
4878 assert_eq!(info.rotation_degrees, 90);
4879 assert!(!info.flip_horizontal);
4880
4881 let loaded = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4882 assert_eq!(loaded.width(), Some(1280));
4884 assert_eq!(loaded.height(), Some(720));
4885
4886 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4889 let cpu_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4890
4891 let rotation = Rotation::from_degrees_clockwise(info.rotation_degrees as usize);
4892 let (dst_width, dst_height) = (cpu_src.height().unwrap(), cpu_src.width().unwrap());
4893
4894 let cpu_dst = TensorDyn::image(
4895 dst_width,
4896 dst_height,
4897 PixelFormat::Rgba,
4898 DType::U8,
4899 None,
4900 edgefirst_tensor::CpuAccess::ReadWrite,
4901 )
4902 .unwrap();
4903 let mut cpu_converter = CPUProcessor::new();
4904
4905 let loaded_rotated = TensorDyn::image(
4908 dst_width,
4909 dst_height,
4910 PixelFormat::Rgba,
4911 DType::U8,
4912 None,
4913 edgefirst_tensor::CpuAccess::ReadWrite,
4914 )
4915 .unwrap();
4916 let (r0, _loaded, loaded_rotated) = convert_img(
4917 &mut cpu_converter,
4918 loaded,
4919 loaded_rotated,
4920 rotation,
4921 Flip::None,
4922 Crop::no_crop(),
4923 );
4924 r0.unwrap();
4925
4926 let (result, _cpu_src, cpu_dst) = convert_img(
4927 &mut cpu_converter,
4928 cpu_src,
4929 cpu_dst,
4930 rotation,
4931 Flip::None,
4932 Crop::no_crop(),
4933 );
4934 result.unwrap();
4935
4936 compare_images(&loaded_rotated, &cpu_dst, 0.98, function!());
4937 }
4938
4939 #[test]
4940 fn test_load_png_with_exif() {
4941 use edgefirst_codec::peek_info;
4942
4943 let file = edgefirst_bench::testdata::read("zidane_rotated_exif_180.png").to_vec();
4946 let info = peek_info(&file).unwrap();
4947 assert_eq!(info.rotation_degrees, 180);
4948 assert!(!info.flip_horizontal);
4949
4950 let loaded = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4951 assert_eq!(loaded.height(), Some(720));
4953 assert_eq!(loaded.width(), Some(1280));
4954
4955 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4960 let cpu_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4961
4962 let rotation = Rotation::from_degrees_clockwise(info.rotation_degrees as usize);
4963 let cpu_dst = TensorDyn::image(
4964 1280,
4965 720,
4966 PixelFormat::Rgba,
4967 DType::U8,
4968 None,
4969 edgefirst_tensor::CpuAccess::ReadWrite,
4970 )
4971 .unwrap();
4972 let mut cpu_converter = CPUProcessor::new();
4973
4974 let (result, _cpu_src, cpu_dst) = convert_img(
4975 &mut cpu_converter,
4976 cpu_src,
4977 cpu_dst,
4978 rotation,
4979 Flip::None,
4980 Crop::no_crop(),
4981 );
4982 result.unwrap();
4983
4984 let loaded_rotated = TensorDyn::image(
4987 1280,
4988 720,
4989 PixelFormat::Rgba,
4990 DType::U8,
4991 None,
4992 edgefirst_tensor::CpuAccess::ReadWrite,
4993 )
4994 .unwrap();
4995 let (r0, _loaded, loaded_rotated) = convert_img(
4996 &mut cpu_converter,
4997 loaded,
4998 loaded_rotated,
4999 rotation,
5000 Flip::None,
5001 Crop::no_crop(),
5002 );
5003 r0.unwrap();
5004
5005 compare_images(&loaded_rotated, &cpu_dst, 0.95, function!());
5010 }
5011
5012 #[cfg(target_os = "linux")]
5018 fn make_rgb_jpeg(width: u32, height: u32) -> Vec<u8> {
5019 let mut bytes = Vec::with_capacity((width * height * 3) as usize);
5020 for y in 0..height {
5021 for x in 0..width {
5022 bytes.push(((x + y) & 0xFF) as u8);
5023 bytes.push(((x.wrapping_mul(3)) & 0xFF) as u8);
5024 bytes.push(((y.wrapping_mul(5)) & 0xFF) as u8);
5025 }
5026 }
5027 let mut out = Vec::new();
5028 let encoder = jpeg_encoder::Encoder::new(&mut out, 85);
5029 encoder
5030 .encode(
5031 &bytes,
5032 width as u16,
5033 height as u16,
5034 jpeg_encoder::ColorType::Rgb,
5035 )
5036 .expect("jpeg-encoder must succeed on trivial input");
5037 out
5038 }
5039
5040 #[test]
5049 #[cfg(target_os = "linux")]
5050 #[cfg(feature = "opengl")]
5051 fn test_convert_rgba_non_4_aligned_width_end_to_end() {
5052 use edgefirst_tensor::is_dma_available;
5053 if !is_dma_available() {
5054 crate::test_support::report_skip(
5055 "test_convert_rgba_non_4_aligned_width_end_to_end — DMA not available",
5056 );
5057 return;
5058 }
5059 let jpeg = make_rgb_jpeg(375, 333);
5063 let src_gl = crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), None).unwrap();
5064 assert_eq!(src_gl.width(), Some(375));
5065 let stride = src_gl.row_stride().unwrap();
5067 assert_eq!(stride, 1536, "expected padded pitch 1536, got {stride}");
5068
5069 let mut gl_proc = ImageProcessor::new().unwrap();
5071 let gl_dst = gl_proc
5072 .create_image(
5073 640,
5074 640,
5075 PixelFormat::Rgba,
5076 DType::U8,
5077 None,
5078 edgefirst_tensor::CpuAccess::ReadWrite,
5079 )
5080 .unwrap();
5081 let (r_gl, _src_gl, gl_dst) = convert_img(
5082 &mut gl_proc,
5083 src_gl,
5084 gl_dst,
5085 Rotation::None,
5086 Flip::None,
5087 Crop::no_crop(),
5088 );
5089 r_gl.expect("GL-backed convert must succeed for 375x333 Rgba src");
5090
5091 let src_cpu =
5096 crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), Some(TensorMemory::Mem))
5097 .unwrap();
5098 let mut cpu_proc = ImageProcessor::with_config(ImageProcessorConfig {
5099 backend: ComputeBackend::Cpu,
5100 ..Default::default()
5101 })
5102 .unwrap();
5103 let cpu_dst = TensorDyn::image(
5104 640,
5105 640,
5106 PixelFormat::Rgba,
5107 DType::U8,
5108 Some(TensorMemory::Mem),
5109 edgefirst_tensor::CpuAccess::ReadWrite,
5110 )
5111 .unwrap();
5112 let (r_cpu, _src_cpu, cpu_dst) = convert_img(
5113 &mut cpu_proc,
5114 src_cpu,
5115 cpu_dst,
5116 Rotation::None,
5117 Flip::None,
5118 Crop::no_crop(),
5119 );
5120 r_cpu.unwrap();
5121
5122 compare_images(&gl_dst, &cpu_dst, 0.95, function!());
5126 }
5127
5128 #[test]
5135 #[cfg(target_os = "linux")]
5136 fn test_load_jpeg_rgba_non_aligned_pitch_padded_dma() {
5137 use edgefirst_tensor::is_dma_available;
5138 if !is_dma_available() {
5139 crate::test_support::report_skip(
5140 "test_load_jpeg_rgba_non_aligned_pitch_padded_dma — DMA not available",
5141 );
5142 return;
5143 }
5144 for &w in &[500u32, 612, 428] {
5148 let jpeg = make_rgb_jpeg(w, 333);
5149 let loaded =
5150 crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), None).unwrap();
5151 let natural = (w as usize) * 4;
5152 let aligned = crate::align_pitch_bytes_to_gpu_alignment(natural).unwrap();
5153 assert!(
5154 aligned > natural,
5155 "test sanity: width {w} should be unaligned"
5156 );
5157 let stride = loaded
5158 .row_stride()
5159 .expect("padded DMA path must set an explicit row_stride — regression if None");
5160 assert_eq!(
5161 stride, aligned,
5162 "width {w}: expected padded stride {aligned}, got {stride} \
5163 (regression: pitch-padding branch skipped?)"
5164 );
5165 let eff = loaded.effective_row_stride().unwrap();
5166 assert_eq!(
5167 eff, aligned,
5168 "effective_row_stride must match stored stride"
5169 );
5170 assert_eq!(loaded.width(), Some(w as usize));
5171 assert_eq!(loaded.height(), Some(333));
5172 }
5173 }
5174
5175 #[test]
5184 #[cfg(target_os = "linux")]
5185 fn test_padded_dma_pitch_for_respects_memory_choice() {
5186 use edgefirst_tensor::{is_dma_available, TensorMemory};
5187
5188 let unaligned_w = 500;
5191
5192 assert_eq!(
5194 crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &Some(TensorMemory::Mem),),
5195 None,
5196 "Mem must never trigger DMA padding"
5197 );
5198 assert_eq!(
5199 crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &Some(TensorMemory::Shm),),
5200 None,
5201 "Shm must never trigger DMA padding"
5202 );
5203
5204 assert_eq!(
5209 crate::padded_dma_pitch_for(
5210 PixelFormat::Rgba,
5211 unaligned_w,
5212 &Some(TensorMemory::DmaBuf),
5213 ),
5214 Some(2048),
5215 "explicit Dma must pad regardless of runtime DMA availability"
5216 );
5217
5218 let none_result = crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &None);
5222 if is_dma_available() {
5223 assert_eq!(
5224 none_result,
5225 Some(2048),
5226 "memory=None + DMA available → pad (will route through DMA)"
5227 );
5228 } else {
5229 assert_eq!(
5230 none_result, None,
5231 "memory=None + DMA unavailable → must NOT pad (would force \
5232 image_with_stride into a DMA-only allocation that fails). \
5233 Regression: padded_dma_pitch_for ignored is_dma_available()."
5234 );
5235 }
5236 }
5237
5238 fn make_grey_png(width: u32, height: u32) -> Vec<u8> {
5242 let mut bytes = Vec::with_capacity((width * height) as usize);
5243 for y in 0..height {
5244 for x in 0..width {
5245 bytes.push(((x + y) & 0xFF) as u8);
5246 }
5247 }
5248 let img = image::GrayImage::from_vec(width, height, bytes).unwrap();
5249 let mut buf = Vec::new();
5250 img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)
5251 .unwrap();
5252 buf
5253 }
5254
5255 #[test]
5260 #[cfg(target_os = "linux")]
5261 fn test_load_png_grey_misaligned_width_dma() {
5262 use edgefirst_tensor::is_dma_available;
5263 if !is_dma_available() {
5264 crate::test_support::report_skip(
5265 "test_load_png_grey_misaligned_width_dma — DMA not available",
5266 );
5267 return;
5268 }
5269 let png = make_grey_png(612, 388);
5270 let loaded = crate::load_image_test_helper(&png, Some(PixelFormat::Grey), None).unwrap();
5271 assert_eq!(loaded.width(), Some(612));
5272 assert_eq!(loaded.height(), Some(388));
5273 assert_eq!(loaded.format(), Some(PixelFormat::Grey));
5274
5275 let map = loaded.as_typed::<u8>().unwrap().map().unwrap();
5278 let stride = loaded.row_stride().unwrap_or(612);
5279 assert!(stride >= 612);
5280 let bytes: &[u8] = ↦
5281 for y in 0..388usize {
5282 for x in 0..612usize {
5283 let expected = ((x + y) & 0xFF) as u8;
5284 let got = bytes[y * stride + x];
5285 assert_eq!(
5286 got, expected,
5287 "grey png mismatch at ({x},{y}): got {got} expected {expected}"
5288 );
5289 }
5290 }
5291 }
5292
5293 #[test]
5297 fn test_load_png_grey_mem() {
5298 use edgefirst_tensor::TensorMemory;
5299 let png = make_grey_png(612, 100);
5300 let loaded =
5301 crate::load_image_test_helper(&png, Some(PixelFormat::Grey), Some(TensorMemory::Mem))
5302 .unwrap();
5303 assert_eq!(loaded.width(), Some(612));
5304 assert_eq!(loaded.height(), Some(100));
5305 assert_eq!(loaded.format(), Some(PixelFormat::Grey));
5306 let map = loaded.as_typed::<u8>().unwrap().map().unwrap();
5307 let bytes: &[u8] = ↦
5308 assert_eq!(bytes.len(), 612 * 100);
5310 for y in 0..100 {
5311 for x in 0..612 {
5312 assert_eq!(bytes[y * 612 + x], ((x + y) & 0xFF) as u8);
5313 }
5314 }
5315 }
5316
5317 #[test]
5321 fn test_load_png_grey_to_rgb_mem() {
5322 use edgefirst_tensor::TensorMemory;
5323 let png = make_grey_png(620, 240);
5324 let loaded =
5325 crate::load_image_test_helper(&png, Some(PixelFormat::Rgb), Some(TensorMemory::Mem))
5326 .unwrap();
5327 assert_eq!(loaded.width(), Some(620));
5328 assert_eq!(loaded.height(), Some(240));
5329 assert_eq!(loaded.format(), Some(PixelFormat::Rgb));
5330
5331 let map = loaded.as_typed::<u8>().unwrap().map().unwrap();
5333 let bytes: &[u8] = ↦
5334 for (x, y) in [(0usize, 0usize), (100, 50), (619, 239)] {
5335 let expected = ((x + y) & 0xFF) as u8;
5336 let off = (y * 620 + x) * 3;
5337 assert_eq!(bytes[off], expected, "R@{x},{y}");
5338 assert_eq!(bytes[off + 1], expected, "G@{x},{y}");
5339 assert_eq!(bytes[off + 2], expected, "B@{x},{y}");
5340 }
5341 }
5342
5343 #[test]
5344 #[cfg(target_os = "linux")]
5345 fn test_g2d_resize() {
5346 if !is_g2d_available() {
5347 crate::test_support::report_skip(
5348 "test_g2d_resize - G2D library (libg2d.so.2) not available",
5349 );
5350 return;
5351 }
5352 if !is_dma_available() {
5353 crate::test_support::report_skip("test_g2d_resize - DMA memory allocation not available (permission denied or no DMA-BUF support)");
5354 return;
5355 }
5356
5357 let dst_width = 640;
5358 let dst_height = 360;
5359 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5360 let src = crate::load_image_test_helper(
5361 &file,
5362 Some(PixelFormat::Rgba),
5363 Some(TensorMemory::DmaBuf),
5364 )
5365 .unwrap();
5366
5367 let g2d_dst = TensorDyn::image(
5368 dst_width,
5369 dst_height,
5370 PixelFormat::Rgba,
5371 DType::U8,
5372 Some(TensorMemory::DmaBuf),
5373 edgefirst_tensor::CpuAccess::ReadWrite,
5374 )
5375 .unwrap();
5376 let mut g2d_converter = G2DProcessor::new().unwrap();
5377 let (result, src, g2d_dst) = convert_img(
5378 &mut g2d_converter,
5379 src,
5380 g2d_dst,
5381 Rotation::None,
5382 Flip::None,
5383 Crop::no_crop(),
5384 );
5385 result.unwrap();
5386
5387 let cpu_dst = TensorDyn::image(
5388 dst_width,
5389 dst_height,
5390 PixelFormat::Rgba,
5391 DType::U8,
5392 None,
5393 edgefirst_tensor::CpuAccess::ReadWrite,
5394 )
5395 .unwrap();
5396 let mut cpu_converter = CPUProcessor::new();
5397 let (result, _src, cpu_dst) = convert_img(
5398 &mut cpu_converter,
5399 src,
5400 cpu_dst,
5401 Rotation::None,
5402 Flip::None,
5403 Crop::no_crop(),
5404 );
5405 result.unwrap();
5406
5407 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
5413 }
5414
5415 #[test]
5416 #[cfg(any(
5417 target_os = "linux",
5418 target_os = "macos",
5419 target_os = "ios",
5420 target_os = "android",
5421 target_os = "windows"
5422 ))]
5423 #[cfg(feature = "opengl")]
5424 fn test_opengl_resize() {
5425 if !is_opengl_available() {
5426 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
5427 return;
5428 }
5429
5430 let dst_width = 640;
5431 let dst_height = 360;
5432 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5433 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5434
5435 let cpu_dst = TensorDyn::image(
5436 dst_width,
5437 dst_height,
5438 PixelFormat::Rgba,
5439 DType::U8,
5440 None,
5441 edgefirst_tensor::CpuAccess::ReadWrite,
5442 )
5443 .unwrap();
5444 let mut cpu_converter = CPUProcessor::new();
5445 let (result, src, cpu_dst) = convert_img(
5446 &mut cpu_converter,
5447 src,
5448 cpu_dst,
5449 Rotation::None,
5450 Flip::None,
5451 Crop::no_crop(),
5452 );
5453 result.unwrap();
5454
5455 let mut src = src;
5456 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5457
5458 for _ in 0..5 {
5459 let gl_dst = TensorDyn::image(
5460 dst_width,
5461 dst_height,
5462 PixelFormat::Rgba,
5463 DType::U8,
5464 None,
5465 edgefirst_tensor::CpuAccess::ReadWrite,
5466 )
5467 .unwrap();
5468 let (result, src_back, gl_dst) = convert_img(
5469 &mut gl_converter,
5470 src,
5471 gl_dst,
5472 Rotation::None,
5473 Flip::None,
5474 Crop::no_crop(),
5475 );
5476 result.unwrap();
5477 src = src_back;
5478
5479 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5480 }
5481 }
5482
5483 #[test]
5484 #[cfg(any(
5485 target_os = "linux",
5486 target_os = "macos",
5487 target_os = "ios",
5488 target_os = "android",
5489 target_os = "windows"
5490 ))]
5491 #[cfg(feature = "opengl")]
5492 fn test_opengl_10_threads() {
5493 if !is_opengl_available() {
5494 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
5495 return;
5496 }
5497
5498 let handles: Vec<_> = (0..10)
5499 .map(|i| {
5500 std::thread::Builder::new()
5501 .name(format!("Thread {i}"))
5502 .spawn(test_opengl_resize)
5503 .unwrap()
5504 })
5505 .collect();
5506 handles.into_iter().for_each(|h| {
5507 if let Err(e) = h.join() {
5508 std::panic::resume_unwind(e)
5509 }
5510 });
5511 }
5512
5513 #[test]
5514 #[cfg(any(
5515 target_os = "linux",
5516 target_os = "macos",
5517 target_os = "ios",
5518 target_os = "android",
5519 target_os = "windows"
5520 ))]
5521 #[cfg(feature = "opengl")]
5522 fn test_opengl_grey() {
5523 if !is_opengl_available() {
5524 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
5525 return;
5526 }
5527
5528 let img = crate::load_image_test_helper(
5529 &edgefirst_bench::testdata::read("grey.jpg"),
5530 Some(PixelFormat::Grey),
5531 None,
5532 )
5533 .unwrap();
5534
5535 let gl_dst = TensorDyn::image(
5536 640,
5537 640,
5538 PixelFormat::Grey,
5539 DType::U8,
5540 None,
5541 edgefirst_tensor::CpuAccess::ReadWrite,
5542 )
5543 .unwrap();
5544 let cpu_dst = TensorDyn::image(
5545 640,
5546 640,
5547 PixelFormat::Grey,
5548 DType::U8,
5549 None,
5550 edgefirst_tensor::CpuAccess::ReadWrite,
5551 )
5552 .unwrap();
5553
5554 let mut converter = CPUProcessor::new();
5555
5556 let (result, img, cpu_dst) = convert_img(
5557 &mut converter,
5558 img,
5559 cpu_dst,
5560 Rotation::None,
5561 Flip::None,
5562 Crop::no_crop(),
5563 );
5564 result.unwrap();
5565
5566 let mut gl = GLProcessorThreaded::new(None).unwrap();
5567 let (result, _img, gl_dst) = convert_img(
5568 &mut gl,
5569 img,
5570 gl_dst,
5571 Rotation::None,
5572 Flip::None,
5573 Crop::no_crop(),
5574 );
5575 result.unwrap();
5576
5577 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5578 }
5579
5580 #[test]
5581 #[cfg(target_os = "linux")]
5582 fn test_g2d_src_crop() {
5583 if !is_g2d_available() {
5584 crate::test_support::report_skip(
5585 "test_g2d_src_crop - G2D library (libg2d.so.2) not available",
5586 );
5587 return;
5588 }
5589 if !is_dma_available() {
5590 crate::test_support::report_skip("test_g2d_src_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)");
5591 return;
5592 }
5593
5594 let dst_width = 640;
5595 let dst_height = 640;
5596 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5597 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5598
5599 let cpu_dst = TensorDyn::image(
5600 dst_width,
5601 dst_height,
5602 PixelFormat::Rgba,
5603 DType::U8,
5604 None,
5605 edgefirst_tensor::CpuAccess::ReadWrite,
5606 )
5607 .unwrap();
5608 let mut cpu_converter = CPUProcessor::new();
5609 let crop = Crop::new().with_source(Some(Region::new(0, 0, 640, 360)));
5610 let (result, src, cpu_dst) = convert_img(
5611 &mut cpu_converter,
5612 src,
5613 cpu_dst,
5614 Rotation::None,
5615 Flip::None,
5616 crop,
5617 );
5618 result.unwrap();
5619
5620 let g2d_dst = TensorDyn::image(
5621 dst_width,
5622 dst_height,
5623 PixelFormat::Rgba,
5624 DType::U8,
5625 None,
5626 edgefirst_tensor::CpuAccess::ReadWrite,
5627 )
5628 .unwrap();
5629 let mut g2d_converter = G2DProcessor::new().unwrap();
5630 let (result, _src, g2d_dst) = convert_img(
5631 &mut g2d_converter,
5632 src,
5633 g2d_dst,
5634 Rotation::None,
5635 Flip::None,
5636 crop,
5637 );
5638 result.unwrap();
5639
5640 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
5646 }
5647
5648 #[test]
5649 #[cfg(target_os = "linux")]
5650 fn test_g2d_dst_crop() {
5651 if !is_g2d_available() {
5652 crate::test_support::report_skip(
5653 "test_g2d_dst_crop - G2D library (libg2d.so.2) not available",
5654 );
5655 return;
5656 }
5657 if !is_dma_available() {
5658 crate::test_support::report_skip("test_g2d_dst_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)");
5659 return;
5660 }
5661
5662 let dst_width = 640;
5663 let dst_height = 640;
5664 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5665 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5666
5667 let cpu_dst = TensorDyn::image(
5668 dst_width,
5669 dst_height,
5670 PixelFormat::Rgba,
5671 DType::U8,
5672 None,
5673 edgefirst_tensor::CpuAccess::ReadWrite,
5674 )
5675 .unwrap();
5676 let mut cpu_converter = CPUProcessor::new();
5677 let crop = Crop::new();
5678 let (result, src, cpu_dst) = convert_img(
5679 &mut cpu_converter,
5680 src,
5681 cpu_dst,
5682 Rotation::None,
5683 Flip::None,
5684 crop,
5685 );
5686 result.unwrap();
5687
5688 let g2d_dst = TensorDyn::image(
5689 dst_width,
5690 dst_height,
5691 PixelFormat::Rgba,
5692 DType::U8,
5693 None,
5694 edgefirst_tensor::CpuAccess::ReadWrite,
5695 )
5696 .unwrap();
5697 let mut g2d_converter = G2DProcessor::new().unwrap();
5698 let (result, _src, g2d_dst) = convert_img(
5699 &mut g2d_converter,
5700 src,
5701 g2d_dst,
5702 Rotation::None,
5703 Flip::None,
5704 crop,
5705 );
5706 result.unwrap();
5707
5708 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
5714 }
5715
5716 #[test]
5717 #[cfg(target_os = "linux")]
5718 fn test_g2d_all_rgba() {
5719 if !is_g2d_available() {
5720 crate::test_support::report_skip(
5721 "test_g2d_all_rgba - G2D library (libg2d.so.2) not available",
5722 );
5723 return;
5724 }
5725 if !is_dma_available() {
5726 crate::test_support::report_skip("test_g2d_all_rgba - DMA memory allocation not available (permission denied or no DMA-BUF support)");
5727 return;
5728 }
5729
5730 let dst_width = 640;
5731 let dst_height = 640;
5732 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5733 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5734 let src_dyn = src;
5735
5736 let mut cpu_dst = TensorDyn::image(
5737 dst_width,
5738 dst_height,
5739 PixelFormat::Rgba,
5740 DType::U8,
5741 None,
5742 edgefirst_tensor::CpuAccess::ReadWrite,
5743 )
5744 .unwrap();
5745 let mut cpu_converter = CPUProcessor::new();
5746 let mut g2d_dst = TensorDyn::image(
5747 dst_width,
5748 dst_height,
5749 PixelFormat::Rgba,
5750 DType::U8,
5751 None,
5752 edgefirst_tensor::CpuAccess::ReadWrite,
5753 )
5754 .unwrap();
5755 let mut g2d_converter = G2DProcessor::new().unwrap();
5756
5757 let crop = Crop::new().with_source(Some(Region::new(50, 120, 1024, 576)));
5758
5759 for rot in [
5760 Rotation::None,
5761 Rotation::Clockwise90,
5762 Rotation::Rotate180,
5763 Rotation::CounterClockwise90,
5764 ] {
5765 cpu_dst
5766 .as_typed::<u8>()
5767 .unwrap()
5768 .map()
5769 .unwrap()
5770 .as_mut_slice()
5771 .fill(114);
5772 g2d_dst
5773 .as_typed::<u8>()
5774 .unwrap()
5775 .map()
5776 .unwrap()
5777 .as_mut_slice()
5778 .fill(114);
5779 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
5780 let mut cpu_dst_dyn = cpu_dst;
5781 cpu_converter
5782 .convert(&src_dyn, &mut cpu_dst_dyn, Rotation::None, Flip::None, crop)
5783 .unwrap();
5784 cpu_dst = {
5785 let mut __t = cpu_dst_dyn.into_u8().unwrap();
5786 __t.set_format(PixelFormat::Rgba).unwrap();
5787 TensorDyn::from(__t)
5788 };
5789
5790 let mut g2d_dst_dyn = g2d_dst;
5791 g2d_converter
5792 .convert(&src_dyn, &mut g2d_dst_dyn, Rotation::None, Flip::None, crop)
5793 .unwrap();
5794 g2d_dst = {
5795 let mut __t = g2d_dst_dyn.into_u8().unwrap();
5796 __t.set_format(PixelFormat::Rgba).unwrap();
5797 TensorDyn::from(__t)
5798 };
5799
5800 compare_images(
5801 &g2d_dst,
5802 &cpu_dst,
5803 0.98,
5804 &format!("{} {:?} {:?}", function!(), rot, flip),
5805 );
5806 }
5807 }
5808 }
5809
5810 #[test]
5811 #[cfg(any(
5812 target_os = "linux",
5813 target_os = "macos",
5814 target_os = "ios",
5815 target_os = "android",
5816 target_os = "windows"
5817 ))]
5818 #[cfg(feature = "opengl")]
5819 fn test_opengl_src_crop() {
5820 if !is_opengl_available() {
5821 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
5822 return;
5823 }
5824
5825 let dst_width = 640;
5826 let dst_height = 360;
5827 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5828 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5829 let crop = Crop::new().with_source(Some(Region::new(320, 180, 1280 - 320, 720 - 180)));
5830
5831 let cpu_dst = TensorDyn::image(
5832 dst_width,
5833 dst_height,
5834 PixelFormat::Rgba,
5835 DType::U8,
5836 None,
5837 edgefirst_tensor::CpuAccess::ReadWrite,
5838 )
5839 .unwrap();
5840 let mut cpu_converter = CPUProcessor::new();
5841 let (result, src, cpu_dst) = convert_img(
5842 &mut cpu_converter,
5843 src,
5844 cpu_dst,
5845 Rotation::None,
5846 Flip::None,
5847 crop,
5848 );
5849 result.unwrap();
5850
5851 let gl_dst = TensorDyn::image(
5852 dst_width,
5853 dst_height,
5854 PixelFormat::Rgba,
5855 DType::U8,
5856 None,
5857 edgefirst_tensor::CpuAccess::ReadWrite,
5858 )
5859 .unwrap();
5860 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5861 let (result, _src, gl_dst) = convert_img(
5862 &mut gl_converter,
5863 src,
5864 gl_dst,
5865 Rotation::None,
5866 Flip::None,
5867 crop,
5868 );
5869 result.unwrap();
5870
5871 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5872 }
5873
5874 #[test]
5875 #[cfg(any(
5876 target_os = "linux",
5877 target_os = "macos",
5878 target_os = "ios",
5879 target_os = "android",
5880 target_os = "windows"
5881 ))]
5882 #[cfg(feature = "opengl")]
5883 fn test_opengl_dst_crop() {
5884 if !is_opengl_available() {
5885 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
5886 return;
5887 }
5888
5889 let dst_width = 640;
5890 let dst_height = 640;
5891 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5892 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5893
5894 let cpu_dst = TensorDyn::image(
5895 dst_width,
5896 dst_height,
5897 PixelFormat::Rgba,
5898 DType::U8,
5899 None,
5900 edgefirst_tensor::CpuAccess::ReadWrite,
5901 )
5902 .unwrap();
5903 let mut cpu_converter = CPUProcessor::new();
5904 let crop = Crop::new();
5905 let (result, src, cpu_dst) = convert_img(
5906 &mut cpu_converter,
5907 src,
5908 cpu_dst,
5909 Rotation::None,
5910 Flip::None,
5911 crop,
5912 );
5913 result.unwrap();
5914
5915 let gl_dst = TensorDyn::image(
5916 dst_width,
5917 dst_height,
5918 PixelFormat::Rgba,
5919 DType::U8,
5920 None,
5921 edgefirst_tensor::CpuAccess::ReadWrite,
5922 )
5923 .unwrap();
5924 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5925 let (result, _src, gl_dst) = convert_img(
5926 &mut gl_converter,
5927 src,
5928 gl_dst,
5929 Rotation::None,
5930 Flip::None,
5931 crop,
5932 );
5933 result.unwrap();
5934
5935 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5936 }
5937 #[cfg(all(target_os = "windows", feature = "opengl"))]
5941 fn synthetic_rgba(w: usize, h: usize) -> TensorDyn {
5942 let t = TensorDyn::image(
5943 w,
5944 h,
5945 PixelFormat::Rgba,
5946 DType::U8,
5947 Some(TensorMemory::Mem),
5948 edgefirst_tensor::CpuAccess::ReadWrite,
5949 )
5950 .unwrap();
5951 fill_synthetic_rgba(&t);
5952 t
5953 }
5954
5955 #[cfg(all(target_os = "windows", feature = "opengl"))]
5960 fn assert_pixels_identical(actual: &TensorDyn, expected: &TensorDyn, name: &str) {
5961 assert_eq!(actual.shape(), expected.shape(), "{name}: shapes differ");
5962 let flat = |t: &TensorDyn| {
5963 let typed = t.as_typed::<u8>().expect("u8 image");
5964 let mut out = vec![0u8; t.shape().iter().product::<usize>()];
5965 typed.copy_to_flat(&mut out).expect("compact the rows");
5966 out
5967 };
5968 let (a, e) = (flat(actual), flat(expected));
5969 let mismatches = a.iter().zip(&e).filter(|(x, y)| x != y).count();
5970 assert_eq!(
5971 mismatches,
5972 0,
5973 "{name}: {mismatches} of {} bytes differ (first rows: {:?} vs {:?})",
5974 a.len(),
5975 &a[..16.min(a.len())],
5976 &e[..16.min(e.len())]
5977 );
5978 }
5979
5980 #[cfg(all(target_os = "windows", feature = "opengl"))]
5983 fn fill_synthetic_rgba(t: &TensorDyn) {
5984 let w = t.width().unwrap();
5985 let h = t.height().unwrap();
5986 let bound = t.as_typed::<u8>().unwrap();
5987 let stride = bound.effective_row_stride().unwrap_or(w * 4);
5988 let mut m = bound.map().unwrap();
5989 let px = m.as_mut_slice();
5990 for y in 0..h {
5991 for x in 0..w {
5992 let o = y * stride + x * 4;
5993 px[o] = x as u8;
5994 px[o + 1] = y as u8;
5995 px[o + 2] = (x + y) as u8;
5996 px[o + 3] = 255;
5997 }
5998 }
5999 }
6000
6001 #[cfg(all(target_os = "windows", feature = "opengl"))]
6010 fn compare_float_images(a: &TensorDyn, b: &TensorDyn, tol: f64, what: &str) {
6011 assert_eq!(a.format(), b.format(), "{what}: pixel formats differ");
6012 assert_eq!(a.dtype(), b.dtype(), "{what}: dtypes differ");
6013 assert_eq!(a.shape(), b.shape(), "{what}: shapes differ");
6014 let values = |t: &TensorDyn| -> Vec<f64> {
6015 let elems: usize = t.shape().iter().product();
6016 match t.dtype() {
6017 DType::F32 => {
6018 let typed = t.as_typed::<f32>().expect("F32 image");
6019 let mut bytes = vec![0u8; elems * 4];
6020 typed
6021 .copy_to_flat(&mut bytes)
6022 .expect("compact the padded rows");
6023 bytes
6024 .as_chunks::<4>()
6025 .0
6026 .iter()
6027 .map(|c| f32::from_le_bytes(*c) as f64)
6028 .collect()
6029 }
6030 DType::F16 => {
6031 let typed = t.as_typed::<half::f16>().expect("F16 image");
6032 let mut bytes = vec![0u8; elems * 2];
6033 typed
6034 .copy_to_flat(&mut bytes)
6035 .expect("compact the padded rows");
6036 bytes
6037 .as_chunks::<2>()
6038 .0
6039 .iter()
6040 .map(|c| half::f16::from_le_bytes(*c).to_f64())
6041 .collect()
6042 }
6043 other => panic!("{what}: {other:?} is not a float dtype"),
6044 }
6045 };
6046 let (av, bv) = (values(a), values(b));
6047 assert_eq!(av.len(), bv.len(), "{what}: element counts differ");
6048 for (i, (x, y)) in av.iter().zip(bv.iter()).enumerate() {
6049 assert!(
6050 (x - y).abs() <= tol,
6051 "{what}: element {i} differs by {} (gpu {x}, cpu {y}, tol {tol})",
6052 (x - y).abs()
6053 );
6054 }
6055 }
6056
6057 #[test]
6080 #[cfg(all(target_os = "windows", feature = "opengl"))]
6081 fn windows_padded_float_pbo_rows_land_at_the_declared_stride() {
6082 if !is_opengl_available() {
6083 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
6084 return;
6085 }
6086 const W: usize = 64;
6087 const H: usize = 32;
6088 const POOL_W: usize = 96;
6089
6090 let mut gpu = ImageProcessor::new().unwrap();
6091 let support = gpu.supported_render_dtypes();
6092 let src = synthetic_rgba(W, H);
6093
6094 let cases: [(PixelFormat, DType, bool, &[usize]); 2] = [
6096 (
6097 PixelFormat::PlanarRgb,
6098 DType::F16,
6099 support.f16,
6100 &[W * 2, POOL_W * 2, W * 2 + 4],
6103 ),
6104 (
6105 PixelFormat::Rgb,
6106 DType::F32,
6107 support.f32,
6108 &[W * 3 * 4, POOL_W * 3 * 4],
6109 ),
6110 ];
6111
6112 for (fmt, dtype, supported, strides) in cases {
6113 if !supported {
6114 crate::test_support::report_skip(&format!(
6115 "{} - {fmt:?}/{dtype:?} not reported",
6116 function!()
6117 ));
6118 continue;
6119 }
6120 let Some(gl) = gpu.opengl.as_ref() else {
6121 crate::test_support::report_skip(&format!("{} - no GL processor", function!()));
6122 return;
6123 };
6124 let mut reference = match gl.create_pbo_image_dtype(W, H, fmt, dtype) {
6126 Ok(t) => t,
6127 Err(e) => {
6128 crate::test_support::report_skip(&format!(
6129 "{} - no float PBO images: {e:?}",
6130 function!()
6131 ));
6132 return;
6133 }
6134 };
6135 assert_eq!(
6136 reference.memory(),
6137 TensorMemory::Pbo,
6138 "{fmt:?}/{dtype:?}: the destination must be a PBO to reach this readback"
6139 );
6140 gpu.convert(
6141 &src,
6142 &mut reference,
6143 Rotation::None,
6144 Flip::None,
6145 Crop::default(),
6146 )
6147 .unwrap_or_else(|e| panic!("{fmt:?}/{dtype:?}: tight convert failed: {e}"));
6148
6149 for &stride in strides {
6150 let label = format!("{fmt:?}/{dtype:?} at a {stride} B pitch");
6151 let mut padded = gpu
6154 .opengl
6155 .as_ref()
6156 .expect("GL processor")
6157 .create_pbo_image_dtype(POOL_W, H, fmt, dtype)
6158 .unwrap_or_else(|e| panic!("{label}: pool PBO: {e:?}"));
6159 padded
6160 .configure_image(W, H, fmt)
6161 .unwrap_or_else(|e| panic!("{label}: narrow the pool tensor: {e}"));
6162 padded
6163 .set_row_stride(stride)
6164 .unwrap_or_else(|e| panic!("{label}: declare the pitch: {e}"));
6165 assert_eq!(padded.effective_row_stride(), Some(stride), "{label}");
6166
6167 let before = gpu.convert_fallback_count();
6168 gpu.convert(
6169 &src,
6170 &mut padded,
6171 Rotation::None,
6172 Flip::None,
6173 Crop::default(),
6174 )
6175 .unwrap_or_else(|e| panic!("{label}: padded convert failed: {e}"));
6176 assert_eq!(
6179 gpu.convert_fallback_count(),
6180 before,
6181 "{label}: fell back to the CPU backend, so the float PBO readback under test never ran"
6182 );
6183
6184 let tol = if dtype == DType::F16 {
6185 1.0 / 1024.0
6186 } else {
6187 1e-6
6188 };
6189 compare_float_images(&padded, &reference, tol, &label);
6190 }
6191 }
6192 }
6193
6194 #[test]
6204 #[cfg(all(target_os = "windows", feature = "opengl"))]
6205 fn windows_float_destinations_match_the_cpu_converter_for_every_layout() {
6206 if !is_opengl_available() {
6207 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
6208 return;
6209 }
6210 if !edgefirst_tensor::is_gpu_buffer_available() {
6211 crate::test_support::report_skip(&format!(
6212 "{} - no zero-copy GPU buffers",
6213 function!()
6214 ));
6215 return;
6216 }
6217 let mut gpu = ImageProcessor::new().unwrap();
6218 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
6219 backend: ComputeBackend::Cpu,
6220 ..Default::default()
6221 })
6222 .unwrap();
6223 let support = gpu.supported_render_dtypes();
6224 eprintln!("{}: supported_render_dtypes = {support:?}", function!());
6225 let src = synthetic_rgba(640, 480);
6226 for (fmt, dtype, need) in [
6227 (PixelFormat::PlanarRgb, DType::F16, support.f16),
6228 (PixelFormat::PlanarRgb, DType::F32, support.f32),
6229 (PixelFormat::PlanarRgba, DType::F16, support.f16),
6230 (PixelFormat::PlanarRgba, DType::F32, support.f32),
6231 (PixelFormat::Rgb, DType::F16, support.f16),
6232 (PixelFormat::Rgb, DType::F32, support.f32),
6233 (PixelFormat::Rgba, DType::F16, support.f16),
6234 (PixelFormat::Rgba, DType::F32, support.f32),
6235 ] {
6236 if !need {
6237 eprintln!("{}: {fmt:?}/{dtype:?} not reported", function!());
6238 continue;
6239 }
6240 let mut dst_gpu = gpu
6241 .create_image(
6242 640,
6243 480,
6244 fmt,
6245 dtype,
6246 Some(TensorMemory::DmaBuf),
6247 edgefirst_tensor::CpuAccess::ReadWrite,
6248 )
6249 .unwrap();
6250 assert_eq!(
6251 dst_gpu.memory(),
6252 TensorMemory::DmaBuf,
6253 "{fmt:?}/{dtype:?} must be a texture"
6254 );
6255 let mut dst_cpu = TensorDyn::image(
6256 640,
6257 480,
6258 fmt,
6259 dtype,
6260 Some(TensorMemory::Mem),
6261 edgefirst_tensor::CpuAccess::ReadWrite,
6262 )
6263 .unwrap();
6264 let before = gpu.convert_fallback_count();
6265 gpu.convert(
6266 &src,
6267 &mut dst_gpu,
6268 Rotation::None,
6269 Flip::None,
6270 Crop::default(),
6271 )
6272 .unwrap();
6273 cpu.convert(
6274 &src,
6275 &mut dst_cpu,
6276 Rotation::None,
6277 Flip::None,
6278 Crop::default(),
6279 )
6280 .unwrap();
6281 assert_eq!(
6282 gpu.convert_fallback_count(),
6283 before,
6284 "{fmt:?}/{dtype:?} fell back to CPU"
6285 );
6286 let tol = if dtype == DType::F16 {
6287 1.0 / 1024.0
6288 } else {
6289 1e-6
6290 };
6291 compare_float_images(&dst_gpu, &dst_cpu, tol, &format!("{fmt:?}/{dtype:?}"));
6292 }
6293 }
6294
6295 #[cfg(target_os = "windows")]
6298 #[link(name = "kernel32")]
6299 extern "system" {
6300 fn WaitForSingleObject(handle: *mut std::ffi::c_void, milliseconds: u32) -> u32;
6301 }
6302
6303 #[cfg(all(target_os = "windows", feature = "opengl"))]
6312 #[test]
6313 fn windows_deferred_batch_records_one_value_per_tile_and_submits_once() {
6314 use edgefirst_tensor::CpuAccess;
6315
6316 if !edgefirst_tensor::is_gpu_buffer_available() {
6317 crate::test_support::report_skip(&format!(
6318 "{} - no zero-copy GPU buffers",
6319 function!()
6320 ));
6321 return;
6322 }
6323 let mut p = match ImageProcessor::new() {
6324 Ok(p) => p,
6325 Err(e) => {
6326 crate::test_support::report_skip(&format!(
6327 "{} - ImageProcessor init failed ({e:?})",
6328 function!()
6329 ));
6330 return;
6331 }
6332 };
6333 let (w, h, tiles) = (640usize, 480usize, 4usize);
6334 let src = synthetic_rgba(w, h);
6335 let mut dsts: Vec<TensorDyn> = (0..tiles)
6336 .map(|_| {
6337 p.create_image(w, h, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6338 .unwrap()
6339 })
6340 .collect();
6341 if dsts[0].memory() != TensorMemory::DmaBuf {
6342 crate::test_support::report_skip(&format!(
6343 "{} - destinations are not textures",
6344 function!()
6345 ));
6346 return;
6347 }
6348 for dst in &mut dsts {
6349 p.convert_deferred(&src, dst, Rotation::None, Flip::None, Crop::default())
6350 .unwrap();
6351 }
6352 let values: Vec<u64> = dsts
6353 .iter()
6354 .map(|d| {
6355 d.gpu_completion()
6356 .unwrap()
6357 .expect("each deferred convert records its own completion")
6358 .value
6359 })
6360 .collect();
6361 for pair in values.windows(2) {
6362 assert!(
6363 pair[1] > pair[0],
6364 "the batch allocated {values:?}, which is not one increasing value per tile"
6365 );
6366 }
6367 p.flush().unwrap();
6368 let dev = edgefirst_tensor::d3d11::device().unwrap();
6371 let last = *values.last().unwrap();
6372 let ev = dev.event_for(last).unwrap();
6373 assert_eq!(
6376 unsafe {
6377 WaitForSingleObject(
6378 std::os::windows::io::AsRawHandle::as_raw_handle(&ev) as _,
6379 5000,
6380 )
6381 },
6382 0,
6383 "the batch's newest value {last} never arrived after flush"
6384 );
6385
6386 let mut solo = p
6387 .create_image(w, h, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6388 .unwrap();
6389 p.convert(&src, &mut solo, Rotation::None, Flip::None, Crop::default())
6390 .unwrap();
6391 let flat = |img: &TensorDyn| -> Vec<u8> {
6395 let t = img.as_typed::<u8>().expect("u8 image");
6396 let mut out = vec![0u8; t.shape().iter().product::<usize>()];
6397 t.copy_to_flat(&mut out).expect("compact the padded rows");
6398 out
6399 };
6400 let want = flat(&solo);
6401 for (i, dst) in dsts.iter().enumerate() {
6402 assert_eq!(
6403 flat(dst),
6404 want,
6405 "tile {i} does not match a standalone convert"
6406 );
6407 }
6408 }
6409
6410 #[cfg(all(target_os = "windows", feature = "opengl"))]
6419 #[test]
6420 fn windows_dropping_a_processor_submits_an_open_batch() {
6421 use edgefirst_tensor::CpuAccess;
6422
6423 if !edgefirst_tensor::is_gpu_buffer_available() {
6424 crate::test_support::report_skip(&format!(
6425 "{} - no zero-copy GPU buffers",
6426 function!()
6427 ));
6428 return;
6429 }
6430 let src = synthetic_rgba(320, 240);
6431 let recorded = {
6432 let mut p = match ImageProcessor::new() {
6433 Ok(p) => p,
6434 Err(e) => {
6435 crate::test_support::report_skip(&format!(
6436 "{} - init failed ({e:?})",
6437 function!()
6438 ));
6439 return;
6440 }
6441 };
6442 let mut dst = p
6443 .create_image(
6444 320,
6445 240,
6446 PixelFormat::Rgba,
6447 DType::U8,
6448 None,
6449 CpuAccess::Read,
6450 )
6451 .unwrap();
6452 if dst.memory() != TensorMemory::DmaBuf {
6453 crate::test_support::report_skip(&format!(
6454 "{} - destination is not a texture",
6455 function!()
6456 ));
6457 return;
6458 }
6459 p.convert_deferred(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
6460 .unwrap();
6461 dst.gpu_completion()
6463 .unwrap()
6464 .expect("the deferred convert recorded a completion")
6465 .value
6466 };
6467
6468 let dev = edgefirst_tensor::d3d11::device().unwrap();
6469 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
6470 while dev.completed_value() < recorded {
6471 assert!(
6472 std::time::Instant::now() < deadline,
6473 "the dropped processor never submitted its batch: fence at {}, recorded {recorded}",
6474 dev.completed_value()
6475 );
6476 std::thread::yield_now();
6477 }
6478 }
6479
6480 #[cfg(all(target_os = "windows", feature = "opengl"))]
6485 #[test]
6486 fn windows_create_image_prefers_textures_and_convert_with_fence_returns_a_set_event() {
6487 use edgefirst_tensor::CpuAccess;
6488 use std::os::windows::io::AsRawHandle;
6489
6490 if !edgefirst_tensor::is_gpu_buffer_available() {
6491 crate::test_support::report_skip(&format!(
6492 "{} - no zero-copy GPU buffers",
6493 function!()
6494 ));
6495 return;
6496 }
6497 let mut p = ImageProcessor::new().unwrap();
6498 let src = synthetic_rgba(320, 240);
6499 let mut dst = p
6500 .create_image(
6501 320,
6502 240,
6503 PixelFormat::Rgba,
6504 DType::U8,
6505 None,
6506 CpuAccess::Read,
6507 )
6508 .unwrap();
6509 if dst.memory() != TensorMemory::DmaBuf
6513 && !std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1")
6514 {
6515 crate::test_support::report_skip(&format!(
6516 "{} - create_image gave {:?}, not a texture (no GL backend)",
6517 function!(),
6518 dst.memory()
6519 ));
6520 return;
6521 }
6522 assert_eq!(
6523 dst.memory(),
6524 TensorMemory::DmaBuf,
6525 "create_image(None) must prefer a texture on Windows"
6526 );
6527 let before = dst.gpu_completion().unwrap().map(|c| c.value);
6530 let fence = p
6531 .convert_with_fence(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
6532 .unwrap()
6533 .expect("Windows exports a fence");
6534 let recorded = dst
6535 .gpu_completion()
6536 .unwrap()
6537 .expect("the convert recorded a completion")
6538 .value;
6539 assert!(
6540 before.is_none_or(|b| recorded > b),
6541 "the fenced convert recorded a newer value than {before:?}"
6542 );
6543 assert_eq!(
6548 edgefirst_tensor::d3d11::device().unwrap().last_signalled(),
6549 recorded,
6550 "a fenced convert must signal exactly once"
6551 );
6552 assert_eq!(
6557 unsafe { WaitForSingleObject(fence.as_raw_handle() as _, 5000) },
6558 0,
6559 "fence was not set within 5 seconds"
6560 );
6561 assert!(
6565 edgefirst_tensor::d3d11::device().unwrap().completed_value() >= recorded,
6566 "the event fired, so the recorded value {recorded} is complete"
6567 );
6568 assert_eq!(
6569 dst.gpu_completion().unwrap().map(|c| c.value),
6570 Some(recorded),
6571 "the destination still reports the value the event waited on"
6572 );
6573 let mut expected = TensorDyn::image(
6574 320,
6575 240,
6576 PixelFormat::Rgba,
6577 DType::U8,
6578 Some(TensorMemory::Mem),
6579 CpuAccess::ReadWrite,
6580 )
6581 .unwrap();
6582 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
6583 backend: ComputeBackend::Cpu,
6584 ..Default::default()
6585 })
6586 .unwrap();
6587 cpu.convert(
6588 &src,
6589 &mut expected,
6590 Rotation::None,
6591 Flip::None,
6592 Crop::default(),
6593 )
6594 .unwrap();
6595 compare_images(&dst, &expected, 0.0, "fenced convert");
6596 }
6597
6598 #[cfg(all(target_os = "windows", feature = "opengl"))]
6603 fn texture_or_skip(t: &TensorDyn, test: &str) -> bool {
6604 if t.memory() == TensorMemory::DmaBuf {
6605 return true;
6606 }
6607 if std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1") {
6608 panic!(
6609 "{test}: create_image(None) must prefer a texture on Windows, got {:?}",
6610 t.memory()
6611 );
6612 }
6613 crate::test_support::report_skip(&format!(
6614 "{test} - create_image gave {:?}, not a texture (no GL backend)",
6615 t.memory()
6616 ));
6617 false
6618 }
6619
6620 #[cfg(all(target_os = "windows", feature = "opengl"))]
6626 #[test]
6627 fn windows_draw_decoded_masks_into_a_texture_records_a_completion() {
6628 use edgefirst_tensor::{CpuAccess, Segmentation};
6629 use ndarray::Array3;
6630
6631 if !edgefirst_tensor::is_gpu_buffer_available() {
6632 crate::test_support::report_skip(&format!(
6633 "{} - no zero-copy GPU buffers",
6634 function!()
6635 ));
6636 return;
6637 }
6638 let mut p = ImageProcessor::new().unwrap();
6639 let mut dst = p
6640 .create_image(64, 64, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6641 .unwrap();
6642 if !texture_or_skip(&dst, function!()) {
6643 return;
6644 }
6645 assert!(
6646 dst.gpu_completion().unwrap().is_none(),
6647 "nothing has written this texture yet"
6648 );
6649
6650 let detect = DetectBox {
6651 bbox: [0.25, 0.25, 0.75, 0.75].into(),
6652 score: 0.99,
6653 label: 0,
6654 };
6655 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
6656 let seg = Segmentation {
6657 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
6658 .unwrap()
6659 .into(),
6660 xmin: 0.25,
6661 ymin: 0.25,
6662 xmax: 0.75,
6663 ymax: 0.75,
6664 };
6665 p.draw_decoded_masks(&mut dst, &[detect], &[seg], MaskOverlay::default())
6666 .expect("draw into a read-only texture");
6667
6668 let completion = dst
6669 .gpu_completion()
6670 .unwrap()
6671 .expect("the draw recorded no completion on dst");
6672 assert!(completion.value > 0, "completion value must be non-zero");
6673
6674 let mut flat = vec![0u8; 64 * 64 * 4];
6679 dst.as_typed::<u8>()
6680 .unwrap()
6681 .copy_to_flat(&mut flat)
6682 .expect("read the drawn texture");
6683 let px = |x: usize, y: usize| -> [u8; 4] {
6684 let o = (y * 64 + x) * 4;
6685 [flat[o], flat[o + 1], flat[o + 2], flat[o + 3]]
6686 };
6687 assert_eq!(px(2, 2), [0, 0, 0, 0], "corner (2,2) was not cleared");
6688 assert_ne!(px(32, 32), [0, 0, 0, 0], "centre (32,32) was not coloured");
6689 }
6690
6691 #[cfg(all(target_os = "windows", feature = "opengl"))]
6694 #[test]
6695 fn windows_draw_proto_masks_into_a_texture_records_a_completion() {
6696 use edgefirst_tensor::{CpuAccess, Tensor};
6697
6698 if !edgefirst_tensor::is_gpu_buffer_available() {
6699 crate::test_support::report_skip(&format!(
6700 "{} - no zero-copy GPU buffers",
6701 function!()
6702 ));
6703 return;
6704 }
6705 let mut p = match with_force_backend(Some("opengl"), ImageProcessor::new) {
6709 Ok(p) => p,
6710 Err(e) if !std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1") => {
6711 crate::test_support::report_skip(&format!(
6712 "{} - no OpenGL backend: {e}",
6713 function!()
6714 ));
6715 return;
6716 }
6717 Err(e) => panic!("forced OpenGL backend unavailable: {e}"),
6718 };
6719 let mut dst = p
6720 .create_image(64, 64, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6721 .unwrap();
6722 if !texture_or_skip(&dst, function!()) {
6723 return;
6724 }
6725
6726 let detect = DetectBox {
6727 bbox: [0.25, 0.25, 0.75, 0.75].into(),
6728 score: 0.99,
6729 label: 0,
6730 };
6731 let proto = ProtoData {
6734 mask_coefficients: TensorDyn::F32(
6735 Tensor::<f32>::from_slice(&[1.0_f32], &[1, 1]).unwrap(),
6736 ),
6737 protos: TensorDyn::F32(
6738 Tensor::<f32>::from_slice(&vec![1.0_f32; 8 * 8], &[8, 8, 1]).unwrap(),
6739 ),
6740 layout: ProtoLayout::Nhwc,
6741 };
6742 p.draw_proto_masks(&mut dst, &[detect], &proto, MaskOverlay::default())
6743 .expect("forced-GL proto draw into a read-only texture");
6744
6745 let completion = dst
6746 .gpu_completion()
6747 .unwrap()
6748 .expect("the proto draw recorded no completion on dst");
6749 assert!(completion.value > 0, "completion value must be non-zero");
6750 }
6751
6752 #[cfg(all(target_os = "windows", feature = "opengl"))]
6757 #[test]
6758 fn windows_draw_decoded_masks_composites_a_host_background_onto_a_texture() {
6759 use edgefirst_tensor::CpuAccess;
6760
6761 if !edgefirst_tensor::is_gpu_buffer_available() {
6762 crate::test_support::report_skip(&format!(
6763 "{} - no zero-copy GPU buffers",
6764 function!()
6765 ));
6766 return;
6767 }
6768 let mut p = ImageProcessor::new().unwrap();
6769 let bg = synthetic_rgba(64, 48);
6770 let mut dst = p
6771 .create_image(64, 48, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6772 .unwrap();
6773 if !texture_or_skip(&dst, function!()) {
6774 return;
6775 }
6776 p.draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::new().with_background(&bg))
6777 .expect("empty draw with a host background onto a texture");
6778 assert_pixels_identical(&dst, &bg, "host background onto a texture");
6779 assert!(dst.gpu_completion().unwrap().is_some());
6780 }
6781
6782 #[cfg(all(target_os = "windows", feature = "opengl"))]
6785 #[test]
6786 fn windows_draw_decoded_masks_composites_a_texture_background_onto_a_texture() {
6787 use edgefirst_tensor::CpuAccess;
6788
6789 if !edgefirst_tensor::is_gpu_buffer_available() {
6790 crate::test_support::report_skip(&format!(
6791 "{} - no zero-copy GPU buffers",
6792 function!()
6793 ));
6794 return;
6795 }
6796 let mut p = ImageProcessor::new().unwrap();
6797 let expected = synthetic_rgba(64, 48);
6798 let bg = p
6799 .create_image(
6800 64,
6801 48,
6802 PixelFormat::Rgba,
6803 DType::U8,
6804 None,
6805 CpuAccess::ReadWrite,
6806 )
6807 .unwrap();
6808 if !texture_or_skip(&bg, function!()) {
6809 return;
6810 }
6811 fill_synthetic_rgba(&bg);
6812 let mut dst = p
6813 .create_image(64, 48, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6814 .unwrap();
6815 assert_eq!(dst.memory(), TensorMemory::DmaBuf);
6816 p.draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::new().with_background(&bg))
6817 .expect("empty draw with a texture background onto a texture");
6818 assert_pixels_identical(&dst, &expected, "texture background onto a texture");
6819 assert!(dst.gpu_completion().unwrap().is_some());
6820 }
6821
6822 #[cfg(all(target_os = "windows", feature = "opengl"))]
6828 #[test]
6829 fn windows_draw_decoded_masks_into_a_texture_view_fills_only_its_band() {
6830 use edgefirst_tensor::CpuAccess;
6831
6832 if !edgefirst_tensor::is_gpu_buffer_available() {
6833 crate::test_support::report_skip(&format!(
6834 "{} - no zero-copy GPU buffers",
6835 function!()
6836 ));
6837 return;
6838 }
6839 let (w, h) = (32usize, 64usize);
6840 let mut p = ImageProcessor::new().unwrap();
6841 let parent = p
6842 .create_image(
6843 w,
6844 h,
6845 PixelFormat::Rgba,
6846 DType::U8,
6847 None,
6848 CpuAccess::ReadWrite,
6849 )
6850 .unwrap();
6851 if !texture_or_skip(&parent, function!()) {
6852 return;
6853 }
6854 fill_synthetic_rgba(&parent);
6855
6856 let mut band = parent
6860 .view(edgefirst_tensor::Region::new(0, h / 2, w, h / 2))
6861 .unwrap();
6862 p.draw_decoded_masks(&mut band, &[], &[], MaskOverlay::default())
6863 .expect("empty draw into a texture view");
6864 drop(band);
6865
6866 let mut flat = vec![0u8; w * h * 4];
6867 parent
6868 .as_typed::<u8>()
6869 .unwrap()
6870 .copy_to_flat(&mut flat)
6871 .expect("read the parent texture");
6872 for y in 0..h {
6873 for x in 0..w {
6874 let o = (y * w + x) * 4;
6875 let got = [flat[o], flat[o + 1], flat[o + 2], flat[o + 3]];
6876 let want = if y < h / 2 {
6877 [x as u8, y as u8, (x + y) as u8, 255]
6878 } else {
6879 [0, 0, 0, 0]
6880 };
6881 assert_eq!(got, want, "pixel ({x},{y})");
6882 }
6883 }
6884
6885 let mut forced = with_force_backend(Some("opengl"), ImageProcessor::new).unwrap();
6888 let mut band = parent
6889 .view(edgefirst_tensor::Region::new(0, h / 2, w, h / 2))
6890 .unwrap();
6891 let err = forced
6892 .draw_decoded_masks(&mut band, &[], &[], MaskOverlay::default())
6893 .expect_err("forced GL must decline a view destination");
6894 assert!(
6895 matches!(err, Error::NotSupported(_)),
6896 "expected NotSupported, got {err:?}"
6897 );
6898 }
6899
6900 #[cfg(all(target_os = "windows", feature = "opengl"))]
6905 #[test]
6906 fn windows_class_colors_reach_the_cpu_backend_behind_a_declined_gl_draw() {
6907 use edgefirst_tensor::{CpuAccess, Segmentation};
6908 use ndarray::Array3;
6909
6910 if !edgefirst_tensor::is_gpu_buffer_available() {
6911 crate::test_support::report_skip(&format!(
6912 "{} - no zero-copy GPU buffers",
6913 function!()
6914 ));
6915 return;
6916 }
6917 const CUSTOM: [u8; 4] = [7, 200, 13, 255];
6918 assert_ne!(
6919 CUSTOM[..3],
6920 DEFAULT_COLORS_U8[0][..3],
6921 "the custom colour must differ from the default palette's, or \
6922 this test cannot tell them apart"
6923 );
6924
6925 let (w, h) = (32usize, 64usize);
6926 let mut p = ImageProcessor::new().unwrap();
6927 let parent = p
6928 .create_image(
6929 w,
6930 h,
6931 PixelFormat::Rgba,
6932 DType::U8,
6933 None,
6934 CpuAccess::ReadWrite,
6935 )
6936 .unwrap();
6937 if parent.memory() != TensorMemory::DmaBuf
6942 && !std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1")
6943 {
6944 crate::test_support::report_skip(&format!(
6945 "{} - create_image gave {:?}, not a texture (no GL backend)",
6946 function!(),
6947 parent.memory()
6948 ));
6949 return;
6950 }
6951 p.set_class_colors(&[CUSTOM]).unwrap();
6952
6953 let detect = DetectBox {
6954 bbox: [0.25, 0.25, 0.75, 0.75].into(),
6955 score: 0.99,
6956 label: 0,
6957 };
6958 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
6959 let seg = Segmentation {
6960 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
6961 .unwrap()
6962 .into(),
6963 xmin: 0.25,
6964 ymin: 0.25,
6965 xmax: 0.75,
6966 ymax: 0.75,
6967 };
6968 let mut band = parent
6969 .view(edgefirst_tensor::Region::new(0, h / 2, w, h / 2))
6970 .unwrap();
6971 p.draw_decoded_masks(&mut band, &[detect], &[seg], MaskOverlay::default())
6972 .expect("draw into a texture view");
6973 drop(band);
6974
6975 let mut flat = vec![0u8; w * h * 4];
6976 parent
6977 .as_typed::<u8>()
6978 .unwrap()
6979 .copy_to_flat(&mut flat)
6980 .expect("read the parent texture");
6981 let o = ((h / 2 + h / 4) * w + w / 2) * 4;
6984 assert_eq!(
6985 &flat[o..o + 3],
6986 &CUSTOM[..3],
6987 "the CPU backend drew the default palette, so it never received \
6988 set_class_colors"
6989 );
6990 }
6991
6992 #[cfg(all(target_os = "windows", feature = "opengl"))]
6997 #[test]
6998 fn two_processors_alternate_on_texture_destinations_without_state_bleed() {
6999 use edgefirst_tensor::CpuAccess;
7000
7001 if !edgefirst_tensor::is_gpu_buffer_available() {
7002 crate::test_support::report_skip(&format!(
7003 "{} - no zero-copy GPU buffers",
7004 function!()
7005 ));
7006 return;
7007 }
7008 let mut a = ImageProcessor::new().unwrap();
7009 let mut b = ImageProcessor::new().unwrap();
7010 let src_a = synthetic_rgba(512, 512);
7011 let src_b = synthetic_rgba(256, 256);
7012 let mut dst_a = a
7013 .create_image(
7014 512,
7015 512,
7016 PixelFormat::Rgba,
7017 DType::U8,
7018 None,
7019 CpuAccess::Read,
7020 )
7021 .unwrap();
7022 let mut dst_b = b
7023 .create_image(
7024 256,
7025 256,
7026 PixelFormat::Rgba,
7027 DType::U8,
7028 None,
7029 CpuAccess::Read,
7030 )
7031 .unwrap();
7032 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
7033 backend: ComputeBackend::Cpu,
7034 ..Default::default()
7035 })
7036 .unwrap();
7037 let mut ref_a = TensorDyn::image(
7038 512,
7039 512,
7040 PixelFormat::Rgba,
7041 DType::U8,
7042 Some(TensorMemory::Mem),
7043 CpuAccess::ReadWrite,
7044 )
7045 .unwrap();
7046 let mut ref_b = TensorDyn::image(
7047 256,
7048 256,
7049 PixelFormat::Rgba,
7050 DType::U8,
7051 Some(TensorMemory::Mem),
7052 CpuAccess::ReadWrite,
7053 )
7054 .unwrap();
7055 cpu.convert(
7056 &src_a,
7057 &mut ref_a,
7058 Rotation::None,
7059 Flip::None,
7060 Crop::default(),
7061 )
7062 .unwrap();
7063 cpu.convert(
7064 &src_b,
7065 &mut ref_b,
7066 Rotation::None,
7067 Flip::None,
7068 Crop::default(),
7069 )
7070 .unwrap();
7071 for _ in 0..200 {
7072 a.convert(
7073 &src_a,
7074 &mut dst_a,
7075 Rotation::None,
7076 Flip::None,
7077 Crop::default(),
7078 )
7079 .unwrap();
7080 b.convert(
7081 &src_b,
7082 &mut dst_b,
7083 Rotation::None,
7084 Flip::None,
7085 Crop::default(),
7086 )
7087 .unwrap();
7088 compare_images(&dst_a, &ref_a, 0.0, "processor A");
7089 compare_images(&dst_b, &ref_b, 0.0, "processor B");
7090 }
7091 }
7092
7093 #[test]
7094 #[cfg(any(
7095 target_os = "linux",
7096 target_os = "macos",
7097 target_os = "ios",
7098 target_os = "android",
7099 target_os = "windows"
7100 ))]
7101 #[cfg(feature = "opengl")]
7102 fn test_opengl_all_rgba() {
7103 if !is_opengl_available() {
7104 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
7105 return;
7106 }
7107
7108 let dst_width = 640;
7109 let dst_height = 640;
7110 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7111
7112 let mut cpu_converter = CPUProcessor::new();
7113
7114 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
7115
7116 let mut mem = vec![None, Some(TensorMemory::Mem)];
7120 if edgefirst_tensor::is_shm_available() {
7121 mem.push(Some(TensorMemory::Shm));
7122 }
7123 if edgefirst_tensor::is_gpu_buffer_available() {
7124 mem.push(Some(TensorMemory::DmaBuf));
7125 }
7126 let crop = Crop::new().with_source(Some(Region::new(50, 120, 1024, 576)));
7127 for m in mem {
7128 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), m).unwrap();
7129 let src_dyn = src;
7130
7131 for rot in [
7132 Rotation::None,
7133 Rotation::Clockwise90,
7134 Rotation::Rotate180,
7135 Rotation::CounterClockwise90,
7136 ] {
7137 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
7138 let cpu_dst = TensorDyn::image(
7139 dst_width,
7140 dst_height,
7141 PixelFormat::Rgba,
7142 DType::U8,
7143 m,
7144 edgefirst_tensor::CpuAccess::ReadWrite,
7145 )
7146 .unwrap();
7147 let gl_dst = TensorDyn::image(
7148 dst_width,
7149 dst_height,
7150 PixelFormat::Rgba,
7151 DType::U8,
7152 m,
7153 edgefirst_tensor::CpuAccess::ReadWrite,
7154 )
7155 .unwrap();
7156 cpu_dst
7157 .as_typed::<u8>()
7158 .unwrap()
7159 .map()
7160 .unwrap()
7161 .as_mut_slice()
7162 .fill(114);
7163 gl_dst
7164 .as_typed::<u8>()
7165 .unwrap()
7166 .map()
7167 .unwrap()
7168 .as_mut_slice()
7169 .fill(114);
7170
7171 let mut cpu_dst_dyn = cpu_dst;
7172 cpu_converter
7173 .convert(&src_dyn, &mut cpu_dst_dyn, Rotation::None, Flip::None, crop)
7174 .unwrap();
7175 let cpu_dst = {
7176 let mut __t = cpu_dst_dyn.into_u8().unwrap();
7177 __t.set_format(PixelFormat::Rgba).unwrap();
7178 TensorDyn::from(__t)
7179 };
7180
7181 let mut gl_dst_dyn = gl_dst;
7182 gl_converter
7183 .convert(&src_dyn, &mut gl_dst_dyn, Rotation::None, Flip::None, crop)
7184 .map_err(|e| {
7185 log::error!("error mem {m:?} rot {rot:?} error: {e:?}");
7186 e
7187 })
7188 .unwrap();
7189 let gl_dst = {
7190 let mut __t = gl_dst_dyn.into_u8().unwrap();
7191 __t.set_format(PixelFormat::Rgba).unwrap();
7192 TensorDyn::from(__t)
7193 };
7194
7195 compare_images(
7196 &gl_dst,
7197 &cpu_dst,
7198 0.98,
7199 &format!("{} {:?} {:?}", function!(), rot, flip),
7200 );
7201 }
7202 }
7203 }
7204 }
7205
7206 #[test]
7207 #[cfg(target_os = "linux")]
7208 fn test_cpu_rotate() {
7209 for rot in [
7210 Rotation::Clockwise90,
7211 Rotation::Rotate180,
7212 Rotation::CounterClockwise90,
7213 ] {
7214 test_cpu_rotate_(rot);
7215 }
7216 }
7217
7218 #[cfg(target_os = "linux")]
7219 fn test_cpu_rotate_(rot: Rotation) {
7220 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7224
7225 let unchanged_src =
7226 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
7227 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
7228
7229 let (dst_width, dst_height) = match rot {
7230 Rotation::None | Rotation::Rotate180 => (src.width().unwrap(), src.height().unwrap()),
7231 Rotation::Clockwise90 | Rotation::CounterClockwise90 => {
7232 (src.height().unwrap(), src.width().unwrap())
7233 }
7234 };
7235
7236 let cpu_dst = TensorDyn::image(
7237 dst_width,
7238 dst_height,
7239 PixelFormat::Rgba,
7240 DType::U8,
7241 None,
7242 edgefirst_tensor::CpuAccess::ReadWrite,
7243 )
7244 .unwrap();
7245 let mut cpu_converter = CPUProcessor::new();
7246
7247 let (result, src, cpu_dst) = convert_img(
7250 &mut cpu_converter,
7251 src,
7252 cpu_dst,
7253 rot,
7254 Flip::None,
7255 Crop::no_crop(),
7256 );
7257 result.unwrap();
7258
7259 let (result, cpu_dst, src) = convert_img(
7260 &mut cpu_converter,
7261 cpu_dst,
7262 src,
7263 rot,
7264 Flip::None,
7265 Crop::no_crop(),
7266 );
7267 result.unwrap();
7268
7269 let (result, src, cpu_dst) = convert_img(
7270 &mut cpu_converter,
7271 src,
7272 cpu_dst,
7273 rot,
7274 Flip::None,
7275 Crop::no_crop(),
7276 );
7277 result.unwrap();
7278
7279 let (result, _cpu_dst, src) = convert_img(
7280 &mut cpu_converter,
7281 cpu_dst,
7282 src,
7283 rot,
7284 Flip::None,
7285 Crop::no_crop(),
7286 );
7287 result.unwrap();
7288
7289 compare_images(&src, &unchanged_src, 0.98, function!());
7290 }
7291
7292 #[test]
7293 #[cfg(any(
7294 target_os = "linux",
7295 target_os = "macos",
7296 target_os = "ios",
7297 target_os = "android",
7298 target_os = "windows"
7299 ))]
7300 #[cfg(feature = "opengl")]
7301 fn test_opengl_rotate() {
7302 if !is_opengl_available() {
7303 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
7304 return;
7305 }
7306
7307 let size = (1280, 720);
7308 let mut mem = vec![None, Some(TensorMemory::Mem)];
7310 if edgefirst_tensor::is_shm_available() {
7311 mem.push(Some(TensorMemory::Shm));
7312 }
7313 if edgefirst_tensor::is_gpu_buffer_available() {
7314 mem.push(Some(TensorMemory::DmaBuf));
7315 }
7316 for m in mem {
7317 for rot in [
7318 Rotation::Clockwise90,
7319 Rotation::Rotate180,
7320 Rotation::CounterClockwise90,
7321 ] {
7322 test_opengl_rotate_(size, rot, m);
7323 }
7324 }
7325 }
7326
7327 #[cfg(any(
7328 target_os = "linux",
7329 target_os = "macos",
7330 target_os = "ios",
7331 target_os = "android",
7332 target_os = "windows"
7333 ))]
7334 #[cfg(feature = "opengl")]
7335 fn test_opengl_rotate_(
7336 size: (usize, usize),
7337 rot: Rotation,
7338 tensor_memory: Option<TensorMemory>,
7339 ) {
7340 let (dst_width, dst_height) = match rot {
7341 Rotation::None | Rotation::Rotate180 => size,
7342 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
7343 };
7344
7345 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7346 let src =
7347 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), tensor_memory).unwrap();
7348
7349 let cpu_dst = TensorDyn::image(
7350 dst_width,
7351 dst_height,
7352 PixelFormat::Rgba,
7353 DType::U8,
7354 None,
7355 edgefirst_tensor::CpuAccess::ReadWrite,
7356 )
7357 .unwrap();
7358 let mut cpu_converter = CPUProcessor::new();
7359
7360 let (result, mut src, cpu_dst) = convert_img(
7361 &mut cpu_converter,
7362 src,
7363 cpu_dst,
7364 rot,
7365 Flip::None,
7366 Crop::no_crop(),
7367 );
7368 result.unwrap();
7369
7370 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
7371
7372 for _ in 0..5 {
7373 let gl_dst = TensorDyn::image(
7374 dst_width,
7375 dst_height,
7376 PixelFormat::Rgba,
7377 DType::U8,
7378 tensor_memory,
7379 edgefirst_tensor::CpuAccess::ReadWrite,
7380 )
7381 .unwrap();
7382 let (result, src_back, gl_dst) = convert_img(
7383 &mut gl_converter,
7384 src,
7385 gl_dst,
7386 rot,
7387 Flip::None,
7388 Crop::no_crop(),
7389 );
7390 result.unwrap();
7391 src = src_back;
7392 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
7393 }
7394 }
7395
7396 #[test]
7397 #[cfg(target_os = "linux")]
7398 fn test_g2d_rotate() {
7399 if !is_g2d_available() {
7400 crate::test_support::report_skip(
7401 "test_g2d_rotate - G2D library (libg2d.so.2) not available",
7402 );
7403 return;
7404 }
7405 if !is_dma_available() {
7406 crate::test_support::report_skip("test_g2d_rotate - DMA memory allocation not available (permission denied or no DMA-BUF support)");
7407 return;
7408 }
7409
7410 let size = (1280, 720);
7411 for rot in [
7412 Rotation::Clockwise90,
7413 Rotation::Rotate180,
7414 Rotation::CounterClockwise90,
7415 ] {
7416 test_g2d_rotate_(size, rot);
7417 }
7418 }
7419
7420 #[cfg(target_os = "linux")]
7421 fn test_g2d_rotate_(size: (usize, usize), rot: Rotation) {
7422 let (dst_width, dst_height) = match rot {
7423 Rotation::None | Rotation::Rotate180 => size,
7424 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
7425 };
7426
7427 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7428 let src = crate::load_image_test_helper(
7429 &file,
7430 Some(PixelFormat::Rgba),
7431 Some(TensorMemory::DmaBuf),
7432 )
7433 .unwrap();
7434
7435 let cpu_dst = TensorDyn::image(
7436 dst_width,
7437 dst_height,
7438 PixelFormat::Rgba,
7439 DType::U8,
7440 None,
7441 edgefirst_tensor::CpuAccess::ReadWrite,
7442 )
7443 .unwrap();
7444 let mut cpu_converter = CPUProcessor::new();
7445
7446 let (result, src, cpu_dst) = convert_img(
7447 &mut cpu_converter,
7448 src,
7449 cpu_dst,
7450 rot,
7451 Flip::None,
7452 Crop::no_crop(),
7453 );
7454 result.unwrap();
7455
7456 let g2d_dst = TensorDyn::image(
7457 dst_width,
7458 dst_height,
7459 PixelFormat::Rgba,
7460 DType::U8,
7461 Some(TensorMemory::DmaBuf),
7462 edgefirst_tensor::CpuAccess::ReadWrite,
7463 )
7464 .unwrap();
7465 let mut g2d_converter = G2DProcessor::new().unwrap();
7466
7467 let (result, _src, g2d_dst) = convert_img(
7468 &mut g2d_converter,
7469 src,
7470 g2d_dst,
7471 rot,
7472 Flip::None,
7473 Crop::no_crop(),
7474 );
7475 result.unwrap();
7476
7477 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
7483 }
7484
7485 #[test]
7486 fn test_rgba_to_yuyv_resize_cpu() {
7487 let src = load_bytes_to_tensor(
7488 1280,
7489 720,
7490 PixelFormat::Rgba,
7491 None,
7492 &edgefirst_bench::testdata::read("camera720p.rgba"),
7493 )
7494 .unwrap();
7495
7496 let (dst_width, dst_height) = (640, 360);
7497
7498 let dst = TensorDyn::image(
7499 dst_width,
7500 dst_height,
7501 PixelFormat::Yuyv,
7502 DType::U8,
7503 None,
7504 edgefirst_tensor::CpuAccess::ReadWrite,
7505 )
7506 .unwrap();
7507
7508 let dst_through_yuyv = TensorDyn::image(
7509 dst_width,
7510 dst_height,
7511 PixelFormat::Rgba,
7512 DType::U8,
7513 None,
7514 edgefirst_tensor::CpuAccess::ReadWrite,
7515 )
7516 .unwrap();
7517 let dst_direct = TensorDyn::image(
7518 dst_width,
7519 dst_height,
7520 PixelFormat::Rgba,
7521 DType::U8,
7522 None,
7523 edgefirst_tensor::CpuAccess::ReadWrite,
7524 )
7525 .unwrap();
7526
7527 let mut cpu_converter = CPUProcessor::new();
7528
7529 let (result, src, dst) = convert_img(
7530 &mut cpu_converter,
7531 src,
7532 dst,
7533 Rotation::None,
7534 Flip::None,
7535 Crop::no_crop(),
7536 );
7537 result.unwrap();
7538
7539 let (result, _dst, dst_through_yuyv) = convert_img(
7540 &mut cpu_converter,
7541 dst,
7542 dst_through_yuyv,
7543 Rotation::None,
7544 Flip::None,
7545 Crop::no_crop(),
7546 );
7547 result.unwrap();
7548
7549 let (result, _src, dst_direct) = convert_img(
7550 &mut cpu_converter,
7551 src,
7552 dst_direct,
7553 Rotation::None,
7554 Flip::None,
7555 Crop::no_crop(),
7556 );
7557 result.unwrap();
7558
7559 compare_images(&dst_through_yuyv, &dst_direct, 0.98, function!());
7560 }
7561
7562 #[test]
7563 #[cfg(any(
7564 target_os = "linux",
7565 target_os = "macos",
7566 target_os = "ios",
7567 target_os = "android",
7568 target_os = "windows"
7569 ))]
7570 #[cfg(feature = "opengl")]
7571 #[ignore = "opengl doesn't support rendering to PixelFormat::Yuyv texture"]
7572 fn test_rgba_to_yuyv_resize_opengl() {
7573 if !is_opengl_available() {
7574 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
7575 return;
7576 }
7577
7578 if !edgefirst_tensor::is_gpu_buffer_available() {
7579 crate::test_support::report_skip(&format!(
7580 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
7581 function!()
7582 ));
7583 return;
7584 }
7585
7586 let src = load_bytes_to_tensor(
7587 1280,
7588 720,
7589 PixelFormat::Rgba,
7590 None,
7591 &edgefirst_bench::testdata::read("camera720p.rgba"),
7592 )
7593 .unwrap();
7594
7595 let (dst_width, dst_height) = (640, 360);
7596
7597 let dst = TensorDyn::image(
7598 dst_width,
7599 dst_height,
7600 PixelFormat::Yuyv,
7601 DType::U8,
7602 Some(TensorMemory::DmaBuf),
7603 edgefirst_tensor::CpuAccess::ReadWrite,
7604 )
7605 .unwrap();
7606
7607 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
7608
7609 let (result, src, dst) = convert_img(
7610 &mut gl_converter,
7611 src,
7612 dst,
7613 Rotation::None,
7614 Flip::None,
7615 Crop::letterbox([255, 255, 255, 255]),
7616 );
7617 result.unwrap();
7618
7619 std::fs::write(
7620 "rgba_to_yuyv_opengl.yuyv",
7621 dst.as_typed::<u8>().unwrap().map().unwrap().as_slice(),
7622 )
7623 .unwrap();
7624 let cpu_dst = TensorDyn::image(
7625 dst_width,
7626 dst_height,
7627 PixelFormat::Yuyv,
7628 DType::U8,
7629 Some(TensorMemory::DmaBuf),
7630 edgefirst_tensor::CpuAccess::ReadWrite,
7631 )
7632 .unwrap();
7633 let (result, _src, cpu_dst) = convert_img(
7634 &mut CPUProcessor::new(),
7635 src,
7636 cpu_dst,
7637 Rotation::None,
7638 Flip::None,
7639 Crop::no_crop(),
7640 );
7641 result.unwrap();
7642
7643 compare_images_convert_to_rgb(&dst, &cpu_dst, 0.98, function!());
7644 }
7645
7646 #[test]
7647 #[cfg(target_os = "linux")]
7648 fn test_rgba_to_yuyv_resize_g2d() {
7649 if !is_g2d_available() {
7650 crate::test_support::report_skip(
7651 "test_rgba_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available",
7652 );
7653 return;
7654 }
7655 if !is_dma_available() {
7656 crate::test_support::report_skip("test_rgba_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
7657 return;
7658 }
7659
7660 let src = load_bytes_to_tensor(
7661 1280,
7662 720,
7663 PixelFormat::Rgba,
7664 Some(TensorMemory::DmaBuf),
7665 &edgefirst_bench::testdata::read("camera720p.rgba"),
7666 )
7667 .unwrap();
7668
7669 let (dst_width, dst_height) = (1280, 720);
7670
7671 let cpu_dst = TensorDyn::image(
7672 dst_width,
7673 dst_height,
7674 PixelFormat::Yuyv,
7675 DType::U8,
7676 Some(TensorMemory::DmaBuf),
7677 edgefirst_tensor::CpuAccess::ReadWrite,
7678 )
7679 .unwrap();
7680
7681 let g2d_dst = TensorDyn::image(
7682 dst_width,
7683 dst_height,
7684 PixelFormat::Yuyv,
7685 DType::U8,
7686 Some(TensorMemory::DmaBuf),
7687 edgefirst_tensor::CpuAccess::ReadWrite,
7688 )
7689 .unwrap();
7690
7691 let mut g2d_converter = G2DProcessor::new().unwrap();
7692 let crop = Crop::new();
7693
7694 g2d_dst
7695 .as_typed::<u8>()
7696 .unwrap()
7697 .map()
7698 .unwrap()
7699 .as_mut_slice()
7700 .fill(128);
7701 let (result, src, g2d_dst) = convert_img(
7702 &mut g2d_converter,
7703 src,
7704 g2d_dst,
7705 Rotation::None,
7706 Flip::None,
7707 crop,
7708 );
7709 result.unwrap();
7710
7711 let cpu_dst_img = cpu_dst;
7712 cpu_dst_img
7713 .as_typed::<u8>()
7714 .unwrap()
7715 .map()
7716 .unwrap()
7717 .as_mut_slice()
7718 .fill(128);
7719 let (result, _src, cpu_dst) = convert_img(
7720 &mut CPUProcessor::new(),
7721 src,
7722 cpu_dst_img,
7723 Rotation::None,
7724 Flip::None,
7725 crop,
7726 );
7727 result.unwrap();
7728
7729 compare_images_convert_to_rgb(&cpu_dst, &g2d_dst, 0.98, function!());
7730 }
7731
7732 #[test]
7733 fn test_yuyv_to_rgba_cpu() {
7734 let file = edgefirst_bench::testdata::read("camera720p.yuyv").to_vec();
7735 let src = TensorDyn::image(
7736 1280,
7737 720,
7738 PixelFormat::Yuyv,
7739 DType::U8,
7740 None,
7741 edgefirst_tensor::CpuAccess::ReadWrite,
7742 )
7743 .unwrap();
7744 src.as_typed::<u8>()
7745 .unwrap()
7746 .map()
7747 .unwrap()
7748 .as_mut_slice()
7749 .copy_from_slice(&file);
7750
7751 let dst = TensorDyn::image(
7752 1280,
7753 720,
7754 PixelFormat::Rgba,
7755 DType::U8,
7756 None,
7757 edgefirst_tensor::CpuAccess::ReadWrite,
7758 )
7759 .unwrap();
7760 let mut cpu_converter = CPUProcessor::new();
7761
7762 let (result, _src, dst) = convert_img(
7763 &mut cpu_converter,
7764 src,
7765 dst,
7766 Rotation::None,
7767 Flip::None,
7768 Crop::no_crop(),
7769 );
7770 result.unwrap();
7771
7772 let target_image = TensorDyn::image(
7773 1280,
7774 720,
7775 PixelFormat::Rgba,
7776 DType::U8,
7777 None,
7778 edgefirst_tensor::CpuAccess::ReadWrite,
7779 )
7780 .unwrap();
7781 target_image
7782 .as_typed::<u8>()
7783 .unwrap()
7784 .map()
7785 .unwrap()
7786 .as_mut_slice()
7787 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
7788
7789 compare_images(&dst, &target_image, 0.98, function!());
7792 }
7793
7794 #[test]
7795 fn test_yuyv_to_rgb_cpu() {
7796 let file = edgefirst_bench::testdata::read("camera720p.yuyv").to_vec();
7797 let src = TensorDyn::image(
7798 1280,
7799 720,
7800 PixelFormat::Yuyv,
7801 DType::U8,
7802 None,
7803 edgefirst_tensor::CpuAccess::ReadWrite,
7804 )
7805 .unwrap();
7806 src.as_typed::<u8>()
7807 .unwrap()
7808 .map()
7809 .unwrap()
7810 .as_mut_slice()
7811 .copy_from_slice(&file);
7812
7813 let dst = TensorDyn::image(
7814 1280,
7815 720,
7816 PixelFormat::Rgb,
7817 DType::U8,
7818 None,
7819 edgefirst_tensor::CpuAccess::ReadWrite,
7820 )
7821 .unwrap();
7822 let mut cpu_converter = CPUProcessor::new();
7823
7824 let (result, _src, dst) = convert_img(
7825 &mut cpu_converter,
7826 src,
7827 dst,
7828 Rotation::None,
7829 Flip::None,
7830 Crop::no_crop(),
7831 );
7832 result.unwrap();
7833
7834 let target_image = TensorDyn::image(
7835 1280,
7836 720,
7837 PixelFormat::Rgb,
7838 DType::U8,
7839 None,
7840 edgefirst_tensor::CpuAccess::ReadWrite,
7841 )
7842 .unwrap();
7843 target_image
7844 .as_typed::<u8>()
7845 .unwrap()
7846 .map()
7847 .unwrap()
7848 .as_mut_slice()
7849 .as_chunks_mut::<3>()
7850 .0
7851 .iter_mut()
7852 .zip(
7853 edgefirst_bench::testdata::read("camera720p.rgba")
7854 .as_chunks::<4>()
7855 .0,
7856 )
7857 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
7858
7859 compare_images(&dst, &target_image, 0.98, function!());
7862 }
7863
7864 #[test]
7865 #[cfg(target_os = "linux")]
7866 fn test_yuyv_to_rgba_g2d() {
7867 if !is_g2d_available() {
7868 crate::test_support::report_skip(
7869 "test_yuyv_to_rgba_g2d - G2D library (libg2d.so.2) not available",
7870 );
7871 return;
7872 }
7873 if !is_dma_available() {
7874 crate::test_support::report_skip("test_yuyv_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
7875 return;
7876 }
7877
7878 let src = load_bytes_to_tensor(
7879 1280,
7880 720,
7881 PixelFormat::Yuyv,
7882 None,
7883 &edgefirst_bench::testdata::read("camera720p.yuyv"),
7884 )
7885 .unwrap();
7886
7887 let dst = TensorDyn::image(
7888 1280,
7889 720,
7890 PixelFormat::Rgba,
7891 DType::U8,
7892 Some(TensorMemory::DmaBuf),
7893 edgefirst_tensor::CpuAccess::ReadWrite,
7894 )
7895 .unwrap();
7896 let mut g2d_converter = G2DProcessor::new().unwrap();
7897
7898 let (result, _src, dst) = convert_img(
7899 &mut g2d_converter,
7900 src,
7901 dst,
7902 Rotation::None,
7903 Flip::None,
7904 Crop::no_crop(),
7905 );
7906 result.unwrap();
7907
7908 let target_image = TensorDyn::image(
7909 1280,
7910 720,
7911 PixelFormat::Rgba,
7912 DType::U8,
7913 None,
7914 edgefirst_tensor::CpuAccess::ReadWrite,
7915 )
7916 .unwrap();
7917 target_image
7918 .as_typed::<u8>()
7919 .unwrap()
7920 .map()
7921 .unwrap()
7922 .as_mut_slice()
7923 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
7924
7925 compare_images(&dst, &target_image, 0.98, function!());
7929 }
7930
7931 #[test]
7932 #[cfg(any(
7933 target_os = "linux",
7934 target_os = "macos",
7935 target_os = "ios",
7936 target_os = "android",
7937 target_os = "windows"
7938 ))]
7939 #[cfg(feature = "opengl")]
7940 fn test_yuyv_to_rgba_opengl() {
7941 if !is_opengl_available() {
7942 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
7943 return;
7944 }
7945 if !edgefirst_tensor::is_gpu_buffer_available() {
7948 crate::test_support::report_skip(&format!(
7949 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
7950 function!()
7951 ));
7952 return;
7953 }
7954
7955 let src = load_bytes_to_tensor(
7956 1280,
7957 720,
7958 PixelFormat::Yuyv,
7959 Some(TensorMemory::DmaBuf),
7960 &edgefirst_bench::testdata::read("camera720p.yuyv"),
7961 )
7962 .unwrap();
7963
7964 let dst = TensorDyn::image(
7965 1280,
7966 720,
7967 PixelFormat::Rgba,
7968 DType::U8,
7969 Some(TensorMemory::DmaBuf),
7970 edgefirst_tensor::CpuAccess::ReadWrite,
7971 )
7972 .unwrap();
7973 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
7974
7975 let (result, _src, dst) = convert_img(
7976 &mut gl_converter,
7977 src,
7978 dst,
7979 Rotation::None,
7980 Flip::None,
7981 Crop::no_crop(),
7982 );
7983 if let Err(crate::Error::NotSupported(msg)) = &result {
7986 crate::test_support::report_skip(&format!("{} — {msg}", function!()));
7987 return;
7988 }
7989 result.unwrap();
7990
7991 let target_image = TensorDyn::image(
7992 1280,
7993 720,
7994 PixelFormat::Rgba,
7995 DType::U8,
7996 None,
7997 edgefirst_tensor::CpuAccess::ReadWrite,
7998 )
7999 .unwrap();
8000 target_image
8001 .as_typed::<u8>()
8002 .unwrap()
8003 .map()
8004 .unwrap()
8005 .as_mut_slice()
8006 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
8007
8008 compare_images(&dst, &target_image, 0.98, function!());
8012 }
8013
8014 #[test]
8024 #[cfg(target_os = "macos")]
8025 #[cfg(feature = "opengl")]
8026 fn test_grey_r8_iosurface_to_rgba_opengl_macos() {
8027 let mut proc = match GLProcessorThreaded::new(None) {
8028 Ok(p) => p,
8029 Err(e) => {
8030 crate::test_support::report_skip(&format!(
8031 "{} — GL engine init failed ({e:?})",
8032 function!()
8033 ));
8034 return;
8035 }
8036 };
8037
8038 let (w, h) = (16usize, 16usize);
8039 let src = TensorDyn::image(
8040 w,
8041 h,
8042 PixelFormat::Grey,
8043 DType::U8,
8044 Some(TensorMemory::DmaBuf),
8045 edgefirst_tensor::CpuAccess::ReadWrite,
8046 )
8047 .expect("GREY IOSurface (R8/L008) should allocate — proves the FourCC mapping");
8048 {
8050 let su8 = src.as_typed::<u8>().unwrap();
8051 let stride = src
8052 .as_typed::<u8>()
8053 .unwrap()
8054 .effective_row_stride()
8055 .unwrap();
8056 let mut m = su8.map().unwrap();
8057 let buf = m.as_mut_slice();
8058 for y in 0..h {
8059 for x in 0..w {
8060 buf[y * stride + x] = ((x * 13 + y * 7) & 0xff) as u8;
8061 }
8062 }
8063 }
8064
8065 let dst = TensorDyn::image(
8066 w,
8067 h,
8068 PixelFormat::Rgba,
8069 DType::U8,
8070 Some(TensorMemory::DmaBuf),
8071 edgefirst_tensor::CpuAccess::ReadWrite,
8072 )
8073 .unwrap();
8074 let (result, src_back, dst) = convert_img(
8075 &mut proc,
8076 src,
8077 dst,
8078 Rotation::None,
8079 Flip::None,
8080 Crop::no_crop(),
8081 );
8082 result.expect("GREY(R8 IOSurface) → RGBA must convert on ANGLE (R8 binding works)");
8083
8084 let src_stride = src_back
8085 .as_typed::<u8>()
8086 .unwrap()
8087 .effective_row_stride()
8088 .unwrap();
8089 let src_map = src_back.as_typed::<u8>().unwrap().map().unwrap();
8090 let sbytes = src_map.as_slice();
8091 let dst_stride = dst
8092 .as_typed::<u8>()
8093 .unwrap()
8094 .effective_row_stride()
8095 .unwrap();
8096 let dst_map = dst.as_typed::<u8>().unwrap().map().unwrap();
8097 let dbytes = dst_map.as_slice();
8098 for y in 0..h {
8099 for x in 0..w {
8100 let yv = sbytes[y * src_stride + x] as i16;
8101 let p = y * dst_stride + x * 4;
8102 for c in 0..3 {
8103 assert!(
8104 (dbytes[p + c] as i16 - yv).abs() <= 2,
8105 "pixel ({x},{y}) ch{c} = {} expected ~{yv} (GREY→RGB identity)",
8106 dbytes[p + c]
8107 );
8108 }
8109 }
8110 }
8111 }
8112
8113 #[test]
8119 #[cfg(target_os = "macos")]
8120 #[cfg(feature = "opengl")]
8121 fn test_nv12_to_planar_f16_two_pass_opengl_macos() {
8122 let mut gpu = match GLProcessorThreaded::new(None) {
8123 Ok(p) => p,
8124 Err(e) => {
8125 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8126 return;
8127 }
8128 };
8129 let (w, h) = (64usize, 64usize);
8130 let src = match TensorDyn::image(
8131 w,
8132 h,
8133 PixelFormat::Nv12,
8134 DType::U8,
8135 Some(TensorMemory::DmaBuf),
8136 edgefirst_tensor::CpuAccess::ReadWrite,
8137 ) {
8138 Ok(t) => t,
8139 Err(e) => {
8140 crate::test_support::report_skip(&format!(
8141 "{} — NV12 IOSurface alloc: {e:?}",
8142 function!()
8143 ));
8144 return;
8145 }
8146 };
8147 src.as_typed::<u8>()
8148 .unwrap()
8149 .map()
8150 .unwrap()
8151 .as_mut_slice()
8152 .fill(128); let dst = match TensorDyn::image(
8155 w,
8156 h,
8157 PixelFormat::PlanarRgb,
8158 DType::F16,
8159 Some(TensorMemory::DmaBuf),
8160 edgefirst_tensor::CpuAccess::ReadWrite,
8161 ) {
8162 Ok(t) => t,
8163 Err(e) => {
8164 crate::test_support::report_skip(&format!(
8165 "{} — F16 PlanarRgb IOSurface: {e:?}",
8166 function!()
8167 ));
8168 return;
8169 }
8170 };
8171 let mut dst = dst;
8172 if let Err(e) = ImageProcessorTrait::convert(
8174 &mut gpu,
8175 &src,
8176 &mut dst,
8177 Rotation::None,
8178 Flip::None,
8179 Crop::no_crop(),
8180 ) {
8181 crate::test_support::report_skip(&format!(
8185 "{} — NV12→PlanarRgb F16 not available ({e:?})",
8186 function!()
8187 ));
8188 return;
8189 }
8190 let dt = dst.as_typed::<half::f16>().expect("dst is F16 PlanarRgb");
8191 let map = dt.map().unwrap();
8192 let vals = map.as_slice();
8193 let mut checked = 0usize;
8196 for &v in vals.iter() {
8197 let f = f32::from(v);
8198 assert!(
8199 (0.40..=0.60).contains(&f),
8200 "planar F16 value {f} not ~0.5 for neutral-grey NV12"
8201 );
8202 checked += 1;
8203 }
8204 assert!(
8205 checked >= w * h * 3,
8206 "expected >= 3 planes of samples, got {checked}"
8207 );
8208 }
8209
8210 #[test]
8218 #[cfg(target_os = "macos")]
8219 #[cfg(feature = "opengl")]
8220 fn test_nv12_to_planar_f16_two_pass_pool_opengl_macos() {
8221 let mut gpu = match GLProcessorThreaded::new(None) {
8222 Ok(p) => p,
8223 Err(e) => {
8224 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8225 return;
8226 }
8227 };
8228 let (fw, fh) = (96usize, 64usize);
8230 let (pool_w, pool_h) = (256usize, 768usize);
8231 let (model_w, model_h) = (128usize, 128usize);
8232
8233 let mut src = match TensorDyn::image(
8234 pool_w,
8235 pool_h,
8236 PixelFormat::Grey,
8237 DType::U8,
8238 Some(TensorMemory::DmaBuf),
8239 edgefirst_tensor::CpuAccess::ReadWrite,
8240 ) {
8241 Ok(t) => t,
8242 Err(e) => {
8243 crate::test_support::report_skip(&format!(
8244 "{} — R8 pool alloc: {e:?}",
8245 function!()
8246 ));
8247 return;
8248 }
8249 };
8250 src.configure_image(fw, fh, PixelFormat::Nv12)
8251 .unwrap_or_else(|e| panic!("configure_image NV12 on pool: {e}"));
8252 let stride = src
8253 .as_typed::<u8>()
8254 .unwrap()
8255 .effective_row_stride()
8256 .unwrap();
8257 src.as_typed::<u8>()
8258 .unwrap()
8259 .map()
8260 .unwrap()
8261 .as_mut_slice()
8262 .fill(128); let mut dst = match TensorDyn::image(
8265 model_w,
8266 model_h,
8267 PixelFormat::PlanarRgb,
8268 DType::F16,
8269 Some(TensorMemory::DmaBuf),
8270 edgefirst_tensor::CpuAccess::ReadWrite,
8271 ) {
8272 Ok(t) => t,
8273 Err(e) => {
8274 crate::test_support::report_skip(&format!(
8275 "{} — F16 PlanarRgb dst: {e:?}",
8276 function!()
8277 ));
8278 return;
8279 }
8280 };
8281
8282 let _ = model_w;
8284 let crop = Crop::new()
8285 .with_source(Some(Region::new(0, 0, fw, fh)))
8286 .with_fit(Fit::Letterbox {
8287 pad: [0, 0, 0, 255],
8288 });
8289 if let Err(e) =
8290 ImageProcessorTrait::convert(&mut gpu, &src, &mut dst, Rotation::None, Flip::None, crop)
8291 {
8292 crate::test_support::report_skip(&format!(
8293 "{} — NV12→PlanarRgb F16 unavailable ({e:?})",
8294 function!()
8295 ));
8296 return;
8297 }
8298 let _ = stride;
8299 let dt = dst.as_typed::<half::f16>().expect("dst F16");
8302 let map = dt.map().unwrap();
8303 let any_half = map.as_slice().iter().any(|&v| {
8304 let f = f32::from(v);
8305 (0.40..=0.60).contains(&f)
8306 });
8307 assert!(any_half, "expected ~0.5 grey samples in the letterbox band");
8308 }
8309
8310 #[test]
8316 #[cfg(target_os = "macos")]
8317 #[cfg(feature = "opengl")]
8318 fn test_nv12_to_planar_f16_cross_thread_opengl_macos() {
8319 use std::sync::mpsc;
8320 let mut proc = match ImageProcessor::new() {
8323 Ok(p) => p,
8324 Err(e) => {
8325 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8326 return;
8327 }
8328 };
8329 let (fw, fh) = (96usize, 64usize);
8330 let mut src = match TensorDyn::image(
8331 256,
8332 768,
8333 PixelFormat::Grey,
8334 DType::U8,
8335 Some(TensorMemory::DmaBuf),
8336 edgefirst_tensor::CpuAccess::ReadWrite,
8337 ) {
8338 Ok(t) => t,
8339 Err(e) => {
8340 crate::test_support::report_skip(&format!("{} — pool: {e:?}", function!()));
8341 return;
8342 }
8343 };
8344 src.configure_image(fw, fh, PixelFormat::Nv12).unwrap();
8345 src.as_typed::<u8>()
8346 .unwrap()
8347 .map()
8348 .unwrap()
8349 .as_mut_slice()
8350 .fill(128);
8351 let mut dst = match TensorDyn::image(
8352 128,
8353 128,
8354 PixelFormat::PlanarRgb,
8355 DType::F16,
8356 Some(TensorMemory::DmaBuf),
8357 edgefirst_tensor::CpuAccess::ReadWrite,
8358 ) {
8359 Ok(t) => t,
8360 Err(e) => {
8361 crate::test_support::report_skip(&format!("{} — dst: {e:?}", function!()));
8362 return;
8363 }
8364 };
8365 let crop = Crop::new().with_source(Some(Region::new(0, 0, fw, fh)));
8366
8367 let (tx, rx) = mpsc::channel::<bool>();
8370 let worker = std::thread::spawn(move || {
8371 let _ = ImageProcessorTrait::convert(
8372 &mut proc,
8373 &src,
8374 &mut dst,
8375 Rotation::None,
8376 Flip::None,
8377 crop,
8378 );
8379 let _ = tx.send(true);
8380 });
8381 match rx.recv_timeout(std::time::Duration::from_secs(20)) {
8382 Ok(_) => { let _ = worker.join(); }
8383 Err(_) => panic!(
8384 "cross-thread NV12→PlanarRgb convert HUNG (>20s) — reproduces the orchestrator deadlock"
8385 ),
8386 }
8387 }
8388
8389 #[test]
8396 #[cfg(target_os = "macos")]
8397 #[cfg(feature = "opengl")]
8398 fn test_nv_to_planar_f16_varying_sizes_no_leak_opengl_macos() {
8399 let mut gpu = match GLProcessorThreaded::new(None) {
8400 Ok(p) => p,
8401 Err(e) => {
8402 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8403 return;
8404 }
8405 };
8406 let (max_w, max_h) = (640usize, 640usize);
8409 let depth = 4usize;
8410 let mut srcs = Vec::new();
8411 let mut dsts = Vec::new();
8412 for _ in 0..depth {
8413 srcs.push(
8414 match TensorDyn::image(
8415 max_w,
8416 max_h * 3,
8417 PixelFormat::Grey,
8418 DType::U8,
8419 Some(TensorMemory::DmaBuf),
8420 edgefirst_tensor::CpuAccess::ReadWrite,
8421 ) {
8422 Ok(t) => t,
8423 Err(e) => {
8424 crate::test_support::report_skip(&format!("{} — pool: {e:?}", function!()));
8425 return;
8426 }
8427 },
8428 );
8429 dsts.push(
8430 match TensorDyn::image(
8431 640,
8432 640,
8433 PixelFormat::PlanarRgb,
8434 DType::F16,
8435 Some(TensorMemory::DmaBuf),
8436 edgefirst_tensor::CpuAccess::ReadWrite,
8437 ) {
8438 Ok(t) => t,
8439 Err(e) => {
8440 crate::test_support::report_skip(&format!("{} — dst: {e:?}", function!()));
8441 return;
8442 }
8443 },
8444 );
8445 }
8446 let sizes = [
8448 (640, 480),
8449 (500, 375),
8450 (640, 427),
8451 (333, 500),
8452 (480, 640),
8453 (612, 612),
8454 (428, 640),
8455 (576, 432),
8456 ];
8457 let mut first_ms = 0f64;
8458 let mut last_ms = 0f64;
8459 let iters = 40usize;
8460 for i in 0..iters {
8461 let (fw, fh) = sizes[i % sizes.len()];
8462 let src = &mut srcs[i % depth];
8463 let dst = &mut dsts[i % depth];
8464 src.configure_image(fw, fh, PixelFormat::Nv24).unwrap();
8465 src.as_typed::<u8>()
8466 .unwrap()
8467 .map()
8468 .unwrap()
8469 .as_mut_slice()
8470 .fill(128);
8471 let crop = Crop::new().with_source(Some(Region::new(0, 0, fw, fh)));
8472 let t0 = std::time::Instant::now();
8473 ImageProcessorTrait::convert(&mut gpu, src, dst, Rotation::None, Flip::None, crop)
8474 .unwrap_or_else(|e| panic!("convert iter {i} ({fw}×{fh}): {e}"));
8475 let ms = t0.elapsed().as_secs_f64() * 1e3;
8476 if i == 2 {
8477 first_ms = ms;
8478 }
8479 if i == iters - 1 {
8480 last_ms = ms;
8481 }
8482 }
8483 eprintln!("first={first_ms:.2}ms last={last_ms:.2}ms");
8484 assert!(
8485 last_ms < first_ms * 5.0 + 5.0,
8486 "convert latency ran away: first {first_ms:.2}ms → last {last_ms:.2}ms (intermediate/pbuffer leak)"
8487 );
8488 }
8489
8490 #[test]
8497 #[cfg(target_os = "macos")]
8498 #[cfg(feature = "opengl")]
8499 fn test_nv12_nv16_nv24_to_rgba_opengl_macos() {
8500 let mut gpu = match GLProcessorThreaded::new(None) {
8501 Ok(p) => p,
8502 Err(e) => {
8503 crate::test_support::report_skip(&format!(
8504 "{} — GL engine init failed ({e:?})",
8505 function!()
8506 ));
8507 return;
8508 }
8509 };
8510 let mut cpu = CPUProcessor::new();
8511
8512 let fill = |buf: &mut [u8], stride: usize, fmt: PixelFormat, w: usize, h: usize| {
8524 for y in 0..h {
8525 for x in 0..w {
8526 buf[y * stride + x] = ((x * 9 + y * 5) & 0xff) as u8;
8527 }
8528 }
8529 let (cw, ch, uv_grid_rows) = match fmt {
8530 PixelFormat::Nv12 => (w / 2, h / 2, 1usize),
8531 PixelFormat::Nv16 => (w / 2, h, 1usize),
8532 _ => (w, h, 2usize), };
8534 let uv_plane = h * stride;
8535 for cy in 0..ch {
8536 for cx in 0..cw {
8537 let off = uv_plane + cy * uv_grid_rows * stride + cx * 2;
8538 buf[off] = ((cx * 11 + 30) & 0xff) as u8;
8539 buf[off + 1] = ((cy * 7 + 200) & 0xff) as u8;
8540 }
8541 }
8542 };
8543
8544 for fmt in [PixelFormat::Nv12, PixelFormat::Nv16, PixelFormat::Nv24] {
8545 for (w, h) in [
8546 (16usize, 16usize), (15, 16), (16, 15), ] {
8550 let mem = TensorDyn::image(
8551 w,
8552 h,
8553 fmt,
8554 DType::U8,
8555 None,
8556 edgefirst_tensor::CpuAccess::ReadWrite,
8557 )
8558 .unwrap();
8559 let mem_stride = mem
8560 .as_typed::<u8>()
8561 .unwrap()
8562 .effective_row_stride()
8563 .unwrap();
8564 fill(
8565 mem.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
8566 mem_stride,
8567 fmt,
8568 w,
8569 h,
8570 );
8571 let cpu_dst = TensorDyn::image(
8572 w,
8573 h,
8574 PixelFormat::Rgba,
8575 DType::U8,
8576 None,
8577 edgefirst_tensor::CpuAccess::ReadWrite,
8578 )
8579 .unwrap();
8580 let (r, _s, cpu_dst) = convert_img(
8581 &mut cpu,
8582 mem,
8583 cpu_dst,
8584 Rotation::None,
8585 Flip::None,
8586 Crop::no_crop(),
8587 );
8588 r.unwrap_or_else(|e| panic!("CPU {fmt:?}->{w}x{h}->RGBA: {e}"));
8589
8590 let ios = TensorDyn::image(
8591 w,
8592 h,
8593 fmt,
8594 DType::U8,
8595 Some(TensorMemory::DmaBuf),
8596 edgefirst_tensor::CpuAccess::ReadWrite,
8597 )
8598 .unwrap_or_else(|e| panic!("{fmt:?} {w}x{h} IOSurface alloc: {e}"));
8599 let ios_stride = ios
8600 .as_typed::<u8>()
8601 .unwrap()
8602 .effective_row_stride()
8603 .unwrap();
8604 fill(
8605 ios.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
8606 ios_stride,
8607 fmt,
8608 w,
8609 h,
8610 );
8611 let gpu_dst = TensorDyn::image(
8612 w,
8613 h,
8614 PixelFormat::Rgba,
8615 DType::U8,
8616 Some(TensorMemory::DmaBuf),
8617 edgefirst_tensor::CpuAccess::ReadWrite,
8618 )
8619 .unwrap();
8620 let (r, _s, gpu_dst) = convert_img(
8621 &mut gpu,
8622 ios,
8623 gpu_dst,
8624 Rotation::None,
8625 Flip::None,
8626 Crop::no_crop(),
8627 );
8628 r.unwrap_or_else(|e| panic!("GPU {fmt:?}->{w}x{h}->RGBA on ANGLE: {e}"));
8629
8630 let cs = cpu_dst
8631 .as_typed::<u8>()
8632 .unwrap()
8633 .effective_row_stride()
8634 .unwrap();
8635 let cmap = cpu_dst.as_typed::<u8>().unwrap().map().unwrap();
8636 let cb = cmap.as_slice();
8637 let gs = gpu_dst
8638 .as_typed::<u8>()
8639 .unwrap()
8640 .effective_row_stride()
8641 .unwrap();
8642 let gmap = gpu_dst.as_typed::<u8>().unwrap().map().unwrap();
8643 let gb = gmap.as_slice();
8644 let mut max_d = 0i16;
8645 for y in 0..h {
8646 for x in 0..w {
8647 for c in 0..3 {
8648 let cv = cb[y * cs + x * 4 + c] as i16;
8649 let gv = gb[y * gs + x * 4 + c] as i16;
8650 max_d = max_d.max((cv - gv).abs());
8651 }
8652 }
8653 }
8654 assert!(
8655 max_d <= 3,
8656 "{fmt:?} {w}x{h}: GPU vs CPU RGBA max channel diff {max_d} > 3"
8657 );
8658 }
8659 }
8660 }
8661
8662 #[test]
8670 #[cfg(target_os = "macos")]
8671 #[cfg(feature = "opengl")]
8672 fn test_nv_to_rgba_larger_pool_surface_opengl_macos() {
8673 let mut gpu = match GLProcessorThreaded::new(None) {
8674 Ok(p) => p,
8675 Err(e) => {
8676 crate::test_support::report_skip(&format!(
8677 "{} — GL engine init failed ({e:?})",
8678 function!()
8679 ));
8680 return;
8681 }
8682 };
8683 let mut cpu = CPUProcessor::new();
8684 let (pool_w, pool_h) = (256usize, 256usize);
8687
8688 let fill = |buf: &mut [u8], stride: usize, fmt: PixelFormat, w: usize, h: usize| {
8692 for y in 0..h {
8693 for x in 0..w {
8694 buf[y * stride + x] = ((x * 9 + y * 5) & 0xff) as u8;
8695 }
8696 }
8697 let (cw, ch, uv_grid_rows) = match fmt {
8698 PixelFormat::Nv12 => (w / 2, h / 2, 1usize),
8699 PixelFormat::Nv16 => (w / 2, h, 1usize),
8700 _ => (w, h, 2usize), };
8702 let uv_plane = h * stride;
8703 for cy in 0..ch {
8704 for cx in 0..cw {
8705 let off = uv_plane + cy * uv_grid_rows * stride + cx * 2;
8706 buf[off] = ((cx * 11 + 30) & 0xff) as u8;
8707 buf[off + 1] = ((cy * 7 + 200) & 0xff) as u8;
8708 }
8709 }
8710 };
8711
8712 for fmt in [PixelFormat::Nv12, PixelFormat::Nv16, PixelFormat::Nv24] {
8713 for (w, h) in [
8714 (40usize, 24usize), (15, 16), (16, 15), ] {
8718 let ew = w.next_multiple_of(2);
8721
8722 let mem = TensorDyn::image(
8724 w,
8725 h,
8726 fmt,
8727 DType::U8,
8728 None,
8729 edgefirst_tensor::CpuAccess::ReadWrite,
8730 )
8731 .unwrap();
8732 let mem_stride = mem
8733 .as_typed::<u8>()
8734 .unwrap()
8735 .effective_row_stride()
8736 .unwrap();
8737 fill(
8738 mem.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
8739 mem_stride,
8740 fmt,
8741 w,
8742 h,
8743 );
8744 let cpu_dst = TensorDyn::image(
8745 w,
8746 h,
8747 PixelFormat::Rgba,
8748 DType::U8,
8749 None,
8750 edgefirst_tensor::CpuAccess::ReadWrite,
8751 )
8752 .unwrap();
8753 let (r, _s, cpu_dst) = convert_img(
8754 &mut cpu,
8755 mem,
8756 cpu_dst,
8757 Rotation::None,
8758 Flip::None,
8759 Crop::no_crop(),
8760 );
8761 r.unwrap_or_else(|e| panic!("CPU {fmt:?}->{w}x{h}->RGBA: {e}"));
8762
8763 let mut ios = match TensorDyn::image(
8767 pool_w,
8768 pool_h,
8769 PixelFormat::Grey,
8770 DType::U8,
8771 Some(TensorMemory::DmaBuf),
8772 edgefirst_tensor::CpuAccess::ReadWrite,
8773 ) {
8774 Ok(t) => t,
8775 Err(e) => {
8776 crate::test_support::report_skip(&format!(
8777 "{} — R8 pool IOSurface alloc: {e:?}",
8778 function!()
8779 ));
8780 return;
8781 }
8782 };
8783 ios.configure_image(w, h, fmt)
8784 .unwrap_or_else(|e| panic!("configure_image {fmt:?} {w}x{h} on pool: {e}"));
8785 let ios_stride = ios
8786 .as_typed::<u8>()
8787 .unwrap()
8788 .effective_row_stride()
8789 .unwrap();
8790 assert!(
8791 ios_stride > ew,
8792 "{fmt:?} {w}x{h}: pool stride {ios_stride} should exceed even width {ew} \
8793 (test must exercise padding)"
8794 );
8795 fill(
8796 ios.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
8797 ios_stride,
8798 fmt,
8799 w,
8800 h,
8801 );
8802
8803 let gpu_dst = TensorDyn::image(
8804 w,
8805 h,
8806 PixelFormat::Rgba,
8807 DType::U8,
8808 Some(TensorMemory::DmaBuf),
8809 edgefirst_tensor::CpuAccess::ReadWrite,
8810 )
8811 .unwrap();
8812 let (r, _s, gpu_dst) = convert_img(
8813 &mut gpu,
8814 ios,
8815 gpu_dst,
8816 Rotation::None,
8817 Flip::None,
8818 Crop::no_crop(),
8819 );
8820 r.unwrap_or_else(|e| {
8821 panic!("GPU {fmt:?}->{w}x{h}->RGBA (pool surface) on ANGLE: {e}")
8822 });
8823
8824 let cs = cpu_dst
8825 .as_typed::<u8>()
8826 .unwrap()
8827 .effective_row_stride()
8828 .unwrap();
8829 let cmap = cpu_dst.as_typed::<u8>().unwrap().map().unwrap();
8830 let cb = cmap.as_slice();
8831 let gs = gpu_dst
8832 .as_typed::<u8>()
8833 .unwrap()
8834 .effective_row_stride()
8835 .unwrap();
8836 let gmap = gpu_dst.as_typed::<u8>().unwrap().map().unwrap();
8837 let gb = gmap.as_slice();
8838 let mut max_d = 0i16;
8839 for y in 0..h {
8840 for x in 0..w {
8841 for c in 0..3 {
8842 let cv = cb[y * cs + x * 4 + c] as i16;
8843 let gv = gb[y * gs + x * 4 + c] as i16;
8844 max_d = max_d.max((cv - gv).abs());
8845 }
8846 }
8847 }
8848 assert!(
8849 max_d <= 3,
8850 "{fmt:?} {w}x{h}: GPU(pool surface) vs CPU RGBA max channel diff {max_d} > 3"
8851 );
8852 }
8853 }
8854 }
8855
8856 #[test]
8857 #[cfg(target_os = "macos")]
8858 #[cfg(feature = "opengl")]
8859 fn test_yuyv_to_rgba_opengl_macos() {
8860 let mut proc = match GLProcessorThreaded::new(None) {
8861 Ok(p) => p,
8862 Err(e) => {
8863 crate::test_support::report_skip(&format!(
8864 "{} — GL engine init failed ({e:?}). \
8865 Install ANGLE via `brew install startergo/angle/angle` \
8866 and re-sign per README.md § macOS GPU Acceleration to \
8867 run this test.",
8868 function!()
8869 ));
8870 return;
8871 }
8872 };
8873
8874 let src = load_bytes_to_tensor(
8875 1280,
8876 720,
8877 PixelFormat::Yuyv,
8878 Some(TensorMemory::DmaBuf),
8879 &edgefirst_bench::testdata::read("camera720p.yuyv"),
8880 )
8881 .unwrap();
8882
8883 let dst = TensorDyn::image(
8884 1280,
8885 720,
8886 PixelFormat::Rgba,
8887 DType::U8,
8888 Some(TensorMemory::DmaBuf),
8889 edgefirst_tensor::CpuAccess::ReadWrite,
8890 )
8891 .unwrap();
8892
8893 let (result, _src, dst) = convert_img(
8894 &mut proc,
8895 src,
8896 dst,
8897 Rotation::None,
8898 Flip::None,
8899 Crop::no_crop(),
8900 );
8901 result.unwrap();
8902
8903 let target_image = TensorDyn::image(
8904 1280,
8905 720,
8906 PixelFormat::Rgba,
8907 DType::U8,
8908 None,
8909 edgefirst_tensor::CpuAccess::ReadWrite,
8910 )
8911 .unwrap();
8912 target_image
8913 .as_typed::<u8>()
8914 .unwrap()
8915 .map()
8916 .unwrap()
8917 .as_mut_slice()
8918 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
8919
8920 compare_images(&dst, &target_image, 0.98, function!());
8925 }
8926
8927 #[test]
8946 #[cfg(target_os = "macos")]
8947 #[cfg(feature = "opengl")]
8948 fn test_yuyv_to_rgba_opengl_macos_multi_resolution() {
8949 let mut proc = match GLProcessorThreaded::new(None) {
8950 Ok(p) => p,
8951 Err(e) => {
8952 crate::test_support::report_skip(&format!(
8953 "{} — GL engine init failed ({e:?})",
8954 function!()
8955 ));
8956 return;
8957 }
8958 };
8959
8960 for (w, h) in [(64usize, 32usize), (3840, 2160)] {
8961 let bytes_per_row = w * 2;
8964 let mut yuyv = vec![0u8; bytes_per_row * h];
8965 for chunk in yuyv.as_chunks_mut::<4>().0 {
8966 chunk[0] = 128; chunk[1] = 128; chunk[2] = 128; chunk[3] = 128; }
8971
8972 let src =
8973 load_bytes_to_tensor(w, h, PixelFormat::Yuyv, Some(TensorMemory::DmaBuf), &yuyv)
8974 .unwrap();
8975
8976 let dst = TensorDyn::image(
8977 w,
8978 h,
8979 PixelFormat::Rgba,
8980 DType::U8,
8981 Some(TensorMemory::DmaBuf),
8982 edgefirst_tensor::CpuAccess::ReadWrite,
8983 )
8984 .unwrap();
8985
8986 let (result, _src, dst) = convert_img(
8987 &mut proc,
8988 src,
8989 dst,
8990 Rotation::None,
8991 Flip::None,
8992 Crop::no_crop(),
8993 );
8994 result.expect("GL convert should succeed at this resolution");
8995
8996 let dst_u8 = dst.as_typed::<u8>().unwrap();
9001 let dst_map = dst_u8.map().unwrap();
9002 let dst_bytes = dst_map.as_slice();
9003 assert_eq!(dst_bytes.len(), w * h * 4, "RGBA byte count");
9004 for px in dst_bytes.as_chunks::<4>().0 {
9005 for (i, &c) in px[..3].iter().enumerate() {
9006 assert!(
9007 (120..=140).contains(&c),
9008 "{}: channel {i} = {c} (expected ~128 ±12) at {w}×{h}",
9009 function!(),
9010 );
9011 }
9012 assert_eq!(px[3], 255, "alpha must be 1.0");
9013 }
9014 }
9015 }
9016
9017 #[test]
9027 #[cfg(target_os = "macos")]
9028 #[cfg(feature = "opengl")]
9029 fn test_macos_gl_pbuffer_cache_reuses_surfaces() {
9030 let mut proc = match GLProcessorThreaded::new(None) {
9031 Ok(p) => p,
9032 Err(e) => {
9033 crate::test_support::report_skip(&format!(
9034 "{} — GL engine init failed ({e:?})",
9035 function!()
9036 ));
9037 return;
9038 }
9039 };
9040
9041 let mut yuyv = vec![0u8; 64 * 32 * 2];
9043 for chunk in yuyv.as_chunks_mut::<4>().0 {
9044 chunk[0] = 200;
9045 chunk[1] = 100;
9046 chunk[2] = 200;
9047 chunk[3] = 156;
9048 }
9049 let src =
9050 load_bytes_to_tensor(64, 32, PixelFormat::Yuyv, Some(TensorMemory::DmaBuf), &yuyv)
9051 .unwrap();
9052 let dst = TensorDyn::image(
9053 64,
9054 32,
9055 PixelFormat::Rgba,
9056 DType::U8,
9057 Some(TensorMemory::DmaBuf),
9058 edgefirst_tensor::CpuAccess::ReadWrite,
9059 )
9060 .unwrap();
9061
9062 let (r1, src, dst) = convert_img(
9063 &mut proc,
9064 src,
9065 dst,
9066 Rotation::None,
9067 Flip::None,
9068 Crop::no_crop(),
9069 );
9070 r1.unwrap();
9071 let first: Vec<u8> = dst
9072 .as_typed::<u8>()
9073 .unwrap()
9074 .map()
9075 .unwrap()
9076 .as_slice()
9077 .to_vec();
9078
9079 let (r2, _src, dst) = convert_img(
9080 &mut proc,
9081 src,
9082 dst,
9083 Rotation::None,
9084 Flip::None,
9085 Crop::no_crop(),
9086 );
9087 r2.unwrap();
9088 let second: Vec<u8> = dst
9089 .as_typed::<u8>()
9090 .unwrap()
9091 .map()
9092 .unwrap()
9093 .as_slice()
9094 .to_vec();
9095
9096 assert_eq!(first, second, "cache-hit conversion must be deterministic");
9097 }
9098
9099 #[test]
9107 #[cfg(target_os = "macos")]
9108 #[cfg(feature = "opengl")]
9109 fn test_macos_gl_pbuffer_cache_steady_state() {
9110 let mut proc = match GLProcessorThreaded::new(None) {
9111 Ok(p) => p,
9112 Err(e) => {
9113 crate::test_support::report_skip(&format!(
9114 "{} — GL engine init failed ({e:?})",
9115 function!()
9116 ));
9117 return;
9118 }
9119 };
9120
9121 let (w, h) = (64usize, 32usize);
9122 const POOL: usize = 3;
9123 const FRAMES: usize = 100;
9124
9125 let yuyv = vec![128u8; w * h * 2];
9126 let pool: Vec<TensorDyn> = (0..POOL)
9127 .map(|_| {
9128 load_bytes_to_tensor(w, h, PixelFormat::Yuyv, Some(TensorMemory::DmaBuf), &yuyv)
9129 .unwrap()
9130 })
9131 .collect();
9132 let mut dst = TensorDyn::image(
9133 w,
9134 h,
9135 PixelFormat::Rgba,
9136 DType::U8,
9137 Some(TensorMemory::DmaBuf),
9138 edgefirst_tensor::CpuAccess::ReadWrite,
9139 )
9140 .unwrap();
9141
9142 for src in pool.iter().cycle().take(POOL * 2) {
9144 proc.convert(src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
9145 .unwrap();
9146 }
9147 let warm = proc.egl_cache_stats().unwrap();
9148
9149 for src in pool.iter().cycle().take(FRAMES) {
9150 proc.convert(src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
9151 .unwrap();
9152 }
9153 let steady = proc.egl_cache_stats().unwrap();
9154
9155 assert_eq!(
9156 warm.total_misses(),
9157 steady.total_misses(),
9158 "steady-state loop created new imports (warm {warm:?}, steady {steady:?})"
9159 );
9160 let hits = |s: &GlCacheStats| s.src.hits + s.dst.hits + s.nv_r8.hits;
9161 assert!(
9162 hits(&steady) - hits(&warm) >= FRAMES as u64,
9163 "expected at least {FRAMES} import-cache hits over the loop, got {}",
9164 hits(&steady) - hits(&warm)
9165 );
9166 }
9167
9168 #[test]
9180 #[cfg(target_os = "macos")]
9181 #[cfg(feature = "opengl")]
9182 fn test_macos_gl_f16_planar_is_gl_backed() {
9183 let mut proc = ImageProcessor::new().expect("ImageProcessor");
9184 let Some(ref gl) = proc.opengl else {
9185 crate::test_support::report_skip(&format!("{} — GL backend unavailable", function!()));
9186 return;
9187 };
9188 if !gl.supported_render_dtypes().f16 {
9189 crate::test_support::report_skip(&format!(
9190 "{} — configuration lacks F16 color-buffer support",
9191 function!()
9192 ));
9193 return;
9194 }
9195 let stats_before = gl.egl_cache_stats().expect("cache stats");
9196
9197 let src = TensorDyn::image(
9198 1280,
9199 720,
9200 PixelFormat::Nv12,
9201 DType::U8,
9202 Some(TensorMemory::DmaBuf),
9203 edgefirst_tensor::CpuAccess::ReadWrite,
9204 )
9205 .unwrap();
9206 {
9207 let t = src.as_typed::<u8>().unwrap();
9208 let mut m = t.map().unwrap();
9209 for (i, b) in m.as_mut_slice().iter_mut().enumerate() {
9210 *b = ((i * 31) % 211) as u8;
9211 }
9212 }
9213 let mut dst = TensorDyn::image(
9214 640,
9215 640,
9216 PixelFormat::PlanarRgb,
9217 DType::F16,
9218 Some(TensorMemory::DmaBuf),
9219 edgefirst_tensor::CpuAccess::ReadWrite,
9220 )
9221 .unwrap();
9222
9223 proc.convert(
9224 &src,
9225 &mut dst,
9226 Rotation::None,
9227 Flip::None,
9228 Crop::letterbox([114, 114, 114, 255]),
9229 )
9230 .expect("F16 capability reported but the NV12→PlanarF16 convert failed");
9231 let stats_after = proc
9232 .opengl
9233 .as_ref()
9234 .expect("GL backend present")
9235 .egl_cache_stats()
9236 .expect("cache stats");
9237 assert!(
9242 stats_after.total_misses() >= stats_before.total_misses() + 2,
9243 "convert succeeded but the GL engine did not import both the \
9244 source and the F16 destination — the work did not (fully) run \
9245 on the GL backend (silent CPU fallback); misses before={} after={}",
9246 stats_before.total_misses(),
9247 stats_after.total_misses()
9248 );
9249 }
9250
9251 #[test]
9257 #[cfg(feature = "opengl")]
9258 fn test_nv12_to_planar_f16_fused_engine_vs_cpu() {
9259 let mut gl = match ImageProcessor::with_config(ImageProcessorConfig {
9260 backend: ComputeBackend::OpenGl,
9261 ..Default::default()
9262 }) {
9263 Ok(p) if p.opengl.is_some() => p,
9264 _ => {
9265 crate::test_support::report_skip(&format!(
9266 "{} — GL backend unavailable",
9267 function!()
9268 ));
9269 return;
9270 }
9271 };
9272 if !gl
9273 .opengl
9274 .as_ref()
9275 .map(|g| g.supported_render_dtypes().f16)
9276 .unwrap_or(false)
9277 {
9278 crate::test_support::report_skip(&format!("{} — no F16 render support", function!()));
9279 return;
9280 }
9281 let mem = if edgefirst_tensor::is_gpu_buffer_available() {
9282 TensorMemory::DmaBuf
9283 } else {
9284 crate::test_support::report_skip(&format!("{} — no zero-copy buffers", function!()));
9285 return;
9286 };
9287
9288 let src = TensorDyn::image(
9289 1280,
9290 720,
9291 PixelFormat::Nv12,
9292 DType::U8,
9293 Some(mem),
9294 edgefirst_tensor::CpuAccess::ReadWrite,
9295 )
9296 .unwrap();
9297 {
9298 let t = src.as_typed::<u8>().unwrap();
9304 let mut m = t.map().unwrap();
9305 let buf = m.as_mut_slice();
9306 let (w, h) = (1280usize, 720usize);
9307 for y in 0..h {
9308 for x in 0..w {
9309 buf[y * w + x] = ((x * 255) / w) as u8; }
9311 }
9312 for y in 0..(h / 2) {
9313 for x in 0..(w / 2) {
9314 let o = h * w + y * w + 2 * x;
9315 buf[o] = ((y * 255) / (h / 2)) as u8; buf[o + 1] = (((x + y) * 255) / (w / 2 + h / 2)) as u8; }
9318 }
9319 }
9320 let crop = Crop::letterbox([114, 114, 114, 255]);
9321 let mut gl_dst = TensorDyn::image(
9322 640,
9323 640,
9324 PixelFormat::PlanarRgb,
9325 DType::F16,
9326 Some(mem),
9327 edgefirst_tensor::CpuAccess::ReadWrite,
9328 )
9329 .unwrap();
9330 match gl.opengl.as_mut().expect("GL backend present").convert(
9337 &src,
9338 &mut gl_dst,
9339 Rotation::None,
9340 Flip::None,
9341 crop,
9342 ) {
9343 Ok(()) => {}
9344 Err(crate::Error::NotSupported(msg)) => {
9345 crate::test_support::report_skip(&format!("{} — {msg}", function!()));
9346 return;
9347 }
9348 Err(e) => panic!("fused NV12→PlanarF16 GL convert: {e}"),
9349 }
9350
9351 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
9352 backend: ComputeBackend::Cpu,
9353 ..Default::default()
9354 })
9355 .unwrap();
9356 let mut cpu_dst = TensorDyn::image(
9357 640,
9358 640,
9359 PixelFormat::PlanarRgb,
9360 DType::F16,
9361 Some(TensorMemory::Mem),
9362 edgefirst_tensor::CpuAccess::ReadWrite,
9363 )
9364 .unwrap();
9365 cpu.convert(&src, &mut cpu_dst, Rotation::None, Flip::None, crop)
9366 .expect("CPU reference convert");
9367
9368 let g = gl_dst
9369 .as_typed::<half::f16>()
9370 .unwrap()
9371 .map()
9372 .unwrap()
9373 .as_slice()
9374 .to_vec();
9375 let c = cpu_dst
9376 .as_typed::<half::f16>()
9377 .unwrap()
9378 .map()
9379 .unwrap()
9380 .as_slice()
9381 .to_vec();
9382 assert_eq!(g.len(), c.len());
9383 let mut max_diff = 0.0f32;
9384 let mut max_at = 0usize;
9385 for (i, (a, b)) in g.iter().zip(c.iter()).enumerate() {
9386 let d = (a.to_f32() - b.to_f32()).abs();
9387 if d > max_diff {
9388 max_diff = d;
9389 max_at = i;
9390 }
9391 }
9392 let (plane, rem) = (max_at / (640 * 640), max_at % (640 * 640));
9394 let (row, col) = (rem / 640, rem % 640);
9395 eprintln!(
9396 "fused-vs-cpu: max_diff={max_diff} at plane={plane} row={row} col={col} \
9397 gl={} cpu={}",
9398 g[max_at].to_f32(),
9399 c[max_at].to_f32()
9400 );
9401 assert!(
9404 max_diff <= 4.0 / 255.0 + 1e-3,
9405 "fused NV12→PlanarF16 diverges from CPU reference: max_diff={max_diff}"
9406 );
9407 }
9408
9409 #[test]
9419 #[cfg(feature = "opengl")]
9420 fn test_zero_copy_src_to_mem_dst_gl_direct() {
9421 let mut proc = match ImageProcessor::new() {
9422 Ok(p) if p.opengl.is_some() => p,
9423 _ => {
9424 crate::test_support::report_skip(&format!(
9425 "{} — GL backend unavailable",
9426 function!()
9427 ));
9428 return;
9429 }
9430 };
9431 if !edgefirst_tensor::is_gpu_buffer_available() {
9432 crate::test_support::report_skip(&format!("{} — no zero-copy buffers", function!()));
9433 return;
9434 }
9435
9436 let src = TensorDyn::image(
9437 1280,
9438 720,
9439 PixelFormat::Rgba,
9440 DType::U8,
9441 Some(TensorMemory::DmaBuf),
9442 edgefirst_tensor::CpuAccess::ReadWrite,
9443 )
9444 .unwrap();
9445 {
9446 let t = src.as_typed::<u8>().unwrap();
9447 let mut m = t.map().unwrap();
9448 for (i, b) in m.as_mut_slice().iter_mut().enumerate() {
9449 *b = ((i * 31) % 211) as u8;
9450 }
9451 }
9452 let mut gl_dst = TensorDyn::image(
9453 1280,
9454 720,
9455 PixelFormat::Bgra,
9456 DType::U8,
9457 Some(TensorMemory::Mem),
9458 edgefirst_tensor::CpuAccess::ReadWrite,
9459 )
9460 .unwrap();
9461 proc.opengl
9462 .as_mut()
9463 .expect("GL backend present")
9464 .convert(
9465 &src,
9466 &mut gl_dst,
9467 Rotation::None,
9468 Flip::None,
9469 Crop::no_crop(),
9470 )
9471 .expect("zero-copy src → heap dst GL convert");
9472
9473 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
9474 backend: ComputeBackend::Cpu,
9475 ..Default::default()
9476 })
9477 .unwrap();
9478 let mut cpu_dst = TensorDyn::image(
9479 1280,
9480 720,
9481 PixelFormat::Bgra,
9482 DType::U8,
9483 Some(TensorMemory::Mem),
9484 edgefirst_tensor::CpuAccess::ReadWrite,
9485 )
9486 .unwrap();
9487 cpu.convert(
9488 &src,
9489 &mut cpu_dst,
9490 Rotation::None,
9491 Flip::None,
9492 Crop::no_crop(),
9493 )
9494 .expect("CPU reference convert");
9495
9496 let g = gl_dst
9497 .as_typed::<u8>()
9498 .unwrap()
9499 .map()
9500 .unwrap()
9501 .as_slice()
9502 .to_vec();
9503 let c = cpu_dst
9504 .as_typed::<u8>()
9505 .unwrap()
9506 .map()
9507 .unwrap()
9508 .as_slice()
9509 .to_vec();
9510 assert_eq!(g.len(), c.len());
9511 let max_diff = g
9512 .iter()
9513 .zip(c.iter())
9514 .map(|(a, b)| a.abs_diff(*b))
9515 .max()
9516 .unwrap();
9517 assert!(
9520 max_diff <= 2,
9521 "zero-copy src → heap dst diverges from CPU reference: max_diff={max_diff}"
9522 );
9523 }
9524
9525 #[test]
9526 #[cfg(target_os = "linux")]
9527 fn test_yuyv_to_rgb_g2d() {
9528 if !is_g2d_available() {
9529 crate::test_support::report_skip(
9530 "test_yuyv_to_rgb_g2d - G2D library (libg2d.so.2) not available",
9531 );
9532 return;
9533 }
9534 if !is_dma_available() {
9535 crate::test_support::report_skip("test_yuyv_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
9536 return;
9537 }
9538
9539 let src = load_bytes_to_tensor(
9540 1280,
9541 720,
9542 PixelFormat::Yuyv,
9543 None,
9544 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9545 )
9546 .unwrap();
9547
9548 let g2d_dst = TensorDyn::image(
9549 1280,
9550 720,
9551 PixelFormat::Rgb,
9552 DType::U8,
9553 Some(TensorMemory::DmaBuf),
9554 edgefirst_tensor::CpuAccess::ReadWrite,
9555 )
9556 .unwrap();
9557 let mut g2d_converter = G2DProcessor::new().unwrap();
9558
9559 let (result, src, g2d_dst) = convert_img(
9560 &mut g2d_converter,
9561 src,
9562 g2d_dst,
9563 Rotation::None,
9564 Flip::None,
9565 Crop::no_crop(),
9566 );
9567 result.unwrap();
9568
9569 let cpu_dst = TensorDyn::image(
9570 1280,
9571 720,
9572 PixelFormat::Rgb,
9573 DType::U8,
9574 None,
9575 edgefirst_tensor::CpuAccess::ReadWrite,
9576 )
9577 .unwrap();
9578 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
9579
9580 let (result, _src, cpu_dst) = convert_img(
9581 &mut cpu_converter,
9582 src,
9583 cpu_dst,
9584 Rotation::None,
9585 Flip::None,
9586 Crop::no_crop(),
9587 );
9588 result.unwrap();
9589
9590 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
9596 }
9597
9598 #[test]
9599 #[cfg(target_os = "linux")]
9600 fn test_yuyv_to_yuyv_resize_g2d() {
9601 if !is_g2d_available() {
9602 crate::test_support::report_skip(
9603 "test_yuyv_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available",
9604 );
9605 return;
9606 }
9607 if !is_dma_available() {
9608 crate::test_support::report_skip("test_yuyv_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
9609 return;
9610 }
9611
9612 let src = load_bytes_to_tensor(
9613 1280,
9614 720,
9615 PixelFormat::Yuyv,
9616 None,
9617 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9618 )
9619 .unwrap();
9620
9621 let g2d_dst = TensorDyn::image(
9622 600,
9623 400,
9624 PixelFormat::Yuyv,
9625 DType::U8,
9626 Some(TensorMemory::DmaBuf),
9627 edgefirst_tensor::CpuAccess::ReadWrite,
9628 )
9629 .unwrap();
9630 let mut g2d_converter = G2DProcessor::new().unwrap();
9631
9632 let (result, src, g2d_dst) = convert_img(
9633 &mut g2d_converter,
9634 src,
9635 g2d_dst,
9636 Rotation::None,
9637 Flip::None,
9638 Crop::no_crop(),
9639 );
9640 result.unwrap();
9641
9642 let cpu_dst = TensorDyn::image(
9643 600,
9644 400,
9645 PixelFormat::Yuyv,
9646 DType::U8,
9647 None,
9648 edgefirst_tensor::CpuAccess::ReadWrite,
9649 )
9650 .unwrap();
9651 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
9652
9653 let (result, _src, cpu_dst) = convert_img(
9654 &mut cpu_converter,
9655 src,
9656 cpu_dst,
9657 Rotation::None,
9658 Flip::None,
9659 Crop::no_crop(),
9660 );
9661 result.unwrap();
9662
9663 eprintln!(
9670 "WARNING: G2D has poor colorimetry support — YUYV resize diverges from the \
9671 CPU reference (~0.85 similarity); threshold held at 0.85, not 0.95."
9672 );
9673 compare_images_convert_to_rgb(&g2d_dst, &cpu_dst, 0.85, function!());
9674 }
9675
9676 #[test]
9677 fn test_yuyv_to_rgba_resize_cpu() {
9678 let src = load_bytes_to_tensor(
9679 1280,
9680 720,
9681 PixelFormat::Yuyv,
9682 None,
9683 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9684 )
9685 .unwrap();
9686
9687 let (dst_width, dst_height) = (960, 540);
9688
9689 let dst = TensorDyn::image(
9690 dst_width,
9691 dst_height,
9692 PixelFormat::Rgba,
9693 DType::U8,
9694 None,
9695 edgefirst_tensor::CpuAccess::ReadWrite,
9696 )
9697 .unwrap();
9698 let mut cpu_converter = CPUProcessor::new();
9699
9700 let (result, _src, dst) = convert_img(
9701 &mut cpu_converter,
9702 src,
9703 dst,
9704 Rotation::None,
9705 Flip::None,
9706 Crop::no_crop(),
9707 );
9708 result.unwrap();
9709
9710 let dst_target = TensorDyn::image(
9711 dst_width,
9712 dst_height,
9713 PixelFormat::Rgba,
9714 DType::U8,
9715 None,
9716 edgefirst_tensor::CpuAccess::ReadWrite,
9717 )
9718 .unwrap();
9719 let src_target = load_bytes_to_tensor(
9720 1280,
9721 720,
9722 PixelFormat::Rgba,
9723 None,
9724 &edgefirst_bench::testdata::read("camera720p.rgba"),
9725 )
9726 .unwrap();
9727 let (result, _src_target, dst_target) = convert_img(
9728 &mut cpu_converter,
9729 src_target,
9730 dst_target,
9731 Rotation::None,
9732 Flip::None,
9733 Crop::no_crop(),
9734 );
9735 result.unwrap();
9736
9737 compare_images(&dst, &dst_target, 0.98, function!());
9740 }
9741
9742 #[test]
9743 #[cfg(target_os = "linux")]
9744 fn test_yuyv_to_rgba_crop_flip_g2d() {
9745 if !is_g2d_available() {
9746 crate::test_support::report_skip(
9747 "test_yuyv_to_rgba_crop_flip_g2d - G2D library (libg2d.so.2) not available",
9748 );
9749 return;
9750 }
9751 if !is_dma_available() {
9752 crate::test_support::report_skip("test_yuyv_to_rgba_crop_flip_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
9753 return;
9754 }
9755
9756 let src = load_bytes_to_tensor(
9757 1280,
9758 720,
9759 PixelFormat::Yuyv,
9760 Some(TensorMemory::DmaBuf),
9761 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9762 )
9763 .unwrap();
9764
9765 let (dst_width, dst_height) = (640, 640);
9766
9767 let dst_g2d = TensorDyn::image(
9768 dst_width,
9769 dst_height,
9770 PixelFormat::Rgba,
9771 DType::U8,
9772 Some(TensorMemory::DmaBuf),
9773 edgefirst_tensor::CpuAccess::ReadWrite,
9774 )
9775 .unwrap();
9776 let mut g2d_converter = G2DProcessor::new().unwrap();
9777 let crop = Crop::new().with_source(Some(Region::new(20, 15, 400, 300)));
9778
9779 let (result, src, dst_g2d) = convert_img(
9780 &mut g2d_converter,
9781 src,
9782 dst_g2d,
9783 Rotation::None,
9784 Flip::Horizontal,
9785 crop,
9786 );
9787 result.unwrap();
9788
9789 let dst_cpu = TensorDyn::image(
9790 dst_width,
9791 dst_height,
9792 PixelFormat::Rgba,
9793 DType::U8,
9794 Some(TensorMemory::DmaBuf),
9795 edgefirst_tensor::CpuAccess::ReadWrite,
9796 )
9797 .unwrap();
9798 let mut cpu_converter = CPUProcessor::new();
9799
9800 let (result, _src, dst_cpu) = convert_img(
9801 &mut cpu_converter,
9802 src,
9803 dst_cpu,
9804 Rotation::None,
9805 Flip::Horizontal,
9806 crop,
9807 );
9808 result.unwrap();
9809 compare_images(&dst_g2d, &dst_cpu, 0.98, function!());
9815 }
9816
9817 #[test]
9818 #[cfg(any(
9819 target_os = "linux",
9820 target_os = "macos",
9821 target_os = "ios",
9822 target_os = "android",
9823 target_os = "windows"
9824 ))]
9825 #[cfg(feature = "opengl")]
9826 fn test_yuyv_to_rgba_crop_flip_opengl() {
9827 if !is_opengl_available() {
9828 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
9829 return;
9830 }
9831
9832 if !edgefirst_tensor::is_gpu_buffer_available() {
9833 crate::test_support::report_skip(&format!(
9834 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
9835 function!()
9836 ));
9837 return;
9838 }
9839
9840 let src = load_bytes_to_tensor(
9841 1280,
9842 720,
9843 PixelFormat::Yuyv,
9844 Some(TensorMemory::DmaBuf),
9845 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9846 )
9847 .unwrap();
9848
9849 let (dst_width, dst_height) = (640, 640);
9850
9851 let dst_gl = TensorDyn::image(
9852 dst_width,
9853 dst_height,
9854 PixelFormat::Rgba,
9855 DType::U8,
9856 Some(TensorMemory::DmaBuf),
9857 edgefirst_tensor::CpuAccess::ReadWrite,
9858 )
9859 .unwrap();
9860 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
9861 let crop = Crop::new().with_source(Some(Region::new(20, 15, 400, 300)));
9862
9863 let (result, src, dst_gl) = convert_img(
9864 &mut gl_converter,
9865 src,
9866 dst_gl,
9867 Rotation::None,
9868 Flip::Horizontal,
9869 crop,
9870 );
9871 if let Err(crate::Error::NotSupported(msg)) = &result {
9873 crate::test_support::report_skip(&format!("{} — {msg}", function!()));
9874 return;
9875 }
9876 result.unwrap();
9877
9878 let dst_cpu = TensorDyn::image(
9879 dst_width,
9880 dst_height,
9881 PixelFormat::Rgba,
9882 DType::U8,
9883 Some(TensorMemory::DmaBuf),
9884 edgefirst_tensor::CpuAccess::ReadWrite,
9885 )
9886 .unwrap();
9887 let mut cpu_converter = CPUProcessor::new();
9888
9889 let (result, _src, dst_cpu) = convert_img(
9890 &mut cpu_converter,
9891 src,
9892 dst_cpu,
9893 Rotation::None,
9894 Flip::Horizontal,
9895 crop,
9896 );
9897 result.unwrap();
9898 compare_images(&dst_gl, &dst_cpu, 0.98, function!());
9903 }
9904
9905 #[test]
9906 fn test_vyuy_to_rgba_cpu() {
9907 let file = edgefirst_bench::testdata::read("camera720p.vyuy").to_vec();
9908 let src = TensorDyn::image(
9909 1280,
9910 720,
9911 PixelFormat::Vyuy,
9912 DType::U8,
9913 None,
9914 edgefirst_tensor::CpuAccess::ReadWrite,
9915 )
9916 .unwrap();
9917 src.as_typed::<u8>()
9918 .unwrap()
9919 .map()
9920 .unwrap()
9921 .as_mut_slice()
9922 .copy_from_slice(&file);
9923
9924 let dst = TensorDyn::image(
9925 1280,
9926 720,
9927 PixelFormat::Rgba,
9928 DType::U8,
9929 None,
9930 edgefirst_tensor::CpuAccess::ReadWrite,
9931 )
9932 .unwrap();
9933 let mut cpu_converter = CPUProcessor::new();
9934
9935 let (result, _src, dst) = convert_img(
9936 &mut cpu_converter,
9937 src,
9938 dst,
9939 Rotation::None,
9940 Flip::None,
9941 Crop::no_crop(),
9942 );
9943 result.unwrap();
9944
9945 let target_image = TensorDyn::image(
9946 1280,
9947 720,
9948 PixelFormat::Rgba,
9949 DType::U8,
9950 None,
9951 edgefirst_tensor::CpuAccess::ReadWrite,
9952 )
9953 .unwrap();
9954 target_image
9955 .as_typed::<u8>()
9956 .unwrap()
9957 .map()
9958 .unwrap()
9959 .as_mut_slice()
9960 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
9961
9962 compare_images(&dst, &target_image, 0.98, function!());
9965 }
9966
9967 #[test]
9968 fn test_vyuy_to_rgb_cpu() {
9969 let file = edgefirst_bench::testdata::read("camera720p.vyuy").to_vec();
9970 let src = TensorDyn::image(
9971 1280,
9972 720,
9973 PixelFormat::Vyuy,
9974 DType::U8,
9975 None,
9976 edgefirst_tensor::CpuAccess::ReadWrite,
9977 )
9978 .unwrap();
9979 src.as_typed::<u8>()
9980 .unwrap()
9981 .map()
9982 .unwrap()
9983 .as_mut_slice()
9984 .copy_from_slice(&file);
9985
9986 let dst = TensorDyn::image(
9987 1280,
9988 720,
9989 PixelFormat::Rgb,
9990 DType::U8,
9991 None,
9992 edgefirst_tensor::CpuAccess::ReadWrite,
9993 )
9994 .unwrap();
9995 let mut cpu_converter = CPUProcessor::new();
9996
9997 let (result, _src, dst) = convert_img(
9998 &mut cpu_converter,
9999 src,
10000 dst,
10001 Rotation::None,
10002 Flip::None,
10003 Crop::no_crop(),
10004 );
10005 result.unwrap();
10006
10007 let target_image = TensorDyn::image(
10008 1280,
10009 720,
10010 PixelFormat::Rgb,
10011 DType::U8,
10012 None,
10013 edgefirst_tensor::CpuAccess::ReadWrite,
10014 )
10015 .unwrap();
10016 target_image
10017 .as_typed::<u8>()
10018 .unwrap()
10019 .map()
10020 .unwrap()
10021 .as_mut_slice()
10022 .as_chunks_mut::<3>()
10023 .0
10024 .iter_mut()
10025 .zip(
10026 edgefirst_bench::testdata::read("camera720p.rgba")
10027 .as_chunks::<4>()
10028 .0,
10029 )
10030 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
10031
10032 compare_images(&dst, &target_image, 0.98, function!());
10035 }
10036
10037 #[test]
10038 #[cfg(target_os = "linux")]
10039 #[ignore = "G2D does not support VYUY; re-enable when hardware support is added"]
10040 fn test_vyuy_to_rgba_g2d() {
10041 if !is_g2d_available() {
10042 crate::test_support::report_skip(
10043 "test_vyuy_to_rgba_g2d - G2D library (libg2d.so.2) not available",
10044 );
10045 return;
10046 }
10047 if !is_dma_available() {
10048 crate::test_support::report_skip("test_vyuy_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
10049 return;
10050 }
10051
10052 let src = load_bytes_to_tensor(
10053 1280,
10054 720,
10055 PixelFormat::Vyuy,
10056 None,
10057 &edgefirst_bench::testdata::read("camera720p.vyuy"),
10058 )
10059 .unwrap();
10060
10061 let dst = TensorDyn::image(
10062 1280,
10063 720,
10064 PixelFormat::Rgba,
10065 DType::U8,
10066 Some(TensorMemory::DmaBuf),
10067 edgefirst_tensor::CpuAccess::ReadWrite,
10068 )
10069 .unwrap();
10070 let mut g2d_converter = G2DProcessor::new().unwrap();
10071
10072 let (result, _src, dst) = convert_img(
10073 &mut g2d_converter,
10074 src,
10075 dst,
10076 Rotation::None,
10077 Flip::None,
10078 Crop::no_crop(),
10079 );
10080 match result {
10081 Err(Error::G2D(_)) => {
10082 crate::test_support::report_skip(
10083 "test_vyuy_to_rgba_g2d - G2D does not support PixelFormat::Vyuy format",
10084 );
10085 return;
10086 }
10087 r => r.unwrap(),
10088 }
10089
10090 let target_image = TensorDyn::image(
10091 1280,
10092 720,
10093 PixelFormat::Rgba,
10094 DType::U8,
10095 None,
10096 edgefirst_tensor::CpuAccess::ReadWrite,
10097 )
10098 .unwrap();
10099 target_image
10100 .as_typed::<u8>()
10101 .unwrap()
10102 .map()
10103 .unwrap()
10104 .as_mut_slice()
10105 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
10106
10107 compare_images(&dst, &target_image, 0.98, function!());
10111 }
10112
10113 #[test]
10114 #[cfg(target_os = "linux")]
10115 #[ignore = "G2D does not support VYUY; re-enable when hardware support is added"]
10116 fn test_vyuy_to_rgb_g2d() {
10117 if !is_g2d_available() {
10118 crate::test_support::report_skip(
10119 "test_vyuy_to_rgb_g2d - G2D library (libg2d.so.2) not available",
10120 );
10121 return;
10122 }
10123 if !is_dma_available() {
10124 crate::test_support::report_skip("test_vyuy_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
10125 return;
10126 }
10127
10128 let src = load_bytes_to_tensor(
10129 1280,
10130 720,
10131 PixelFormat::Vyuy,
10132 None,
10133 &edgefirst_bench::testdata::read("camera720p.vyuy"),
10134 )
10135 .unwrap();
10136
10137 let g2d_dst = TensorDyn::image(
10138 1280,
10139 720,
10140 PixelFormat::Rgb,
10141 DType::U8,
10142 Some(TensorMemory::DmaBuf),
10143 edgefirst_tensor::CpuAccess::ReadWrite,
10144 )
10145 .unwrap();
10146 let mut g2d_converter = G2DProcessor::new().unwrap();
10147
10148 let (result, src, g2d_dst) = convert_img(
10149 &mut g2d_converter,
10150 src,
10151 g2d_dst,
10152 Rotation::None,
10153 Flip::None,
10154 Crop::no_crop(),
10155 );
10156 match result {
10157 Err(Error::G2D(_)) => {
10158 crate::test_support::report_skip(
10159 "test_vyuy_to_rgb_g2d - G2D does not support PixelFormat::Vyuy format",
10160 );
10161 return;
10162 }
10163 r => r.unwrap(),
10164 }
10165
10166 let cpu_dst = TensorDyn::image(
10167 1280,
10168 720,
10169 PixelFormat::Rgb,
10170 DType::U8,
10171 None,
10172 edgefirst_tensor::CpuAccess::ReadWrite,
10173 )
10174 .unwrap();
10175 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
10176
10177 let (result, _src, cpu_dst) = convert_img(
10178 &mut cpu_converter,
10179 src,
10180 cpu_dst,
10181 Rotation::None,
10182 Flip::None,
10183 Crop::no_crop(),
10184 );
10185 result.unwrap();
10186
10187 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
10193 }
10194
10195 #[test]
10196 #[cfg(any(
10197 target_os = "linux",
10198 target_os = "macos",
10199 target_os = "ios",
10200 target_os = "android",
10201 target_os = "windows"
10202 ))]
10203 #[cfg(feature = "opengl")]
10204 fn test_vyuy_to_rgba_opengl() {
10205 if !is_opengl_available() {
10206 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
10207 return;
10208 }
10209 if !edgefirst_tensor::is_gpu_buffer_available() {
10210 crate::test_support::report_skip(&format!(
10211 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
10212 function!()
10213 ));
10214 return;
10215 }
10216
10217 let src = match load_bytes_to_tensor(
10221 1280,
10222 720,
10223 PixelFormat::Vyuy,
10224 Some(TensorMemory::DmaBuf),
10225 &edgefirst_bench::testdata::read("camera720p.vyuy"),
10226 ) {
10227 Ok(src) => src,
10228 Err(e) => {
10229 crate::test_support::report_skip(&format!(
10230 "{} - zero-copy VYUY source not allocatable on this platform ({e})",
10231 function!()
10232 ));
10233 return;
10234 }
10235 };
10236
10237 let dst = TensorDyn::image(
10238 1280,
10239 720,
10240 PixelFormat::Rgba,
10241 DType::U8,
10242 Some(TensorMemory::DmaBuf),
10243 edgefirst_tensor::CpuAccess::ReadWrite,
10244 )
10245 .unwrap();
10246 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
10247
10248 let (result, _src, dst) = convert_img(
10249 &mut gl_converter,
10250 src,
10251 dst,
10252 Rotation::None,
10253 Flip::None,
10254 Crop::no_crop(),
10255 );
10256 match result {
10257 Err(Error::NotSupported(_)) => {
10258 crate::test_support::report_skip(&format!(
10259 "{} - OpenGL does not support PixelFormat::Vyuy DMA format",
10260 function!()
10261 ));
10262 return;
10263 }
10264 r => r.unwrap(),
10265 }
10266
10267 let target_image = TensorDyn::image(
10268 1280,
10269 720,
10270 PixelFormat::Rgba,
10271 DType::U8,
10272 None,
10273 edgefirst_tensor::CpuAccess::ReadWrite,
10274 )
10275 .unwrap();
10276 target_image
10277 .as_typed::<u8>()
10278 .unwrap()
10279 .map()
10280 .unwrap()
10281 .as_mut_slice()
10282 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
10283
10284 compare_images(&dst, &target_image, 0.98, function!());
10288 }
10289
10290 #[test]
10291 fn test_nv12_to_rgba_cpu() {
10292 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
10293 let src = TensorDyn::image(
10294 1280,
10295 720,
10296 PixelFormat::Nv12,
10297 DType::U8,
10298 None,
10299 edgefirst_tensor::CpuAccess::ReadWrite,
10300 )
10301 .unwrap();
10302 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
10303 .copy_from_slice(&file);
10304
10305 let dst = TensorDyn::image(
10306 1280,
10307 720,
10308 PixelFormat::Rgba,
10309 DType::U8,
10310 None,
10311 edgefirst_tensor::CpuAccess::ReadWrite,
10312 )
10313 .unwrap();
10314 let mut cpu_converter = CPUProcessor::new();
10315
10316 let (result, _src, dst) = convert_img(
10317 &mut cpu_converter,
10318 src,
10319 dst,
10320 Rotation::None,
10321 Flip::None,
10322 Crop::no_crop(),
10323 );
10324 result.unwrap();
10325
10326 let target_image = crate::load_image_test_helper(
10327 &edgefirst_bench::testdata::read("zidane.jpg"),
10328 Some(PixelFormat::Rgba),
10329 None,
10330 )
10331 .unwrap();
10332
10333 compare_images(&dst, &target_image, 0.95, function!());
10338 }
10339
10340 #[test]
10341 fn test_nv12_odd_height_to_rgb_cpu() {
10342 let mut src = TensorDyn::image(
10353 8,
10354 5,
10355 PixelFormat::Nv12,
10356 DType::U8,
10357 Some(TensorMemory::Mem),
10358 edgefirst_tensor::CpuAccess::ReadWrite,
10359 )
10360 .unwrap();
10361 assert_eq!(src.shape(), &[8, 8]);
10362 assert_eq!((src.width(), src.height()), (Some(8), Some(5)));
10363 src.as_typed::<u8>()
10364 .unwrap()
10365 .map()
10366 .unwrap()
10367 .as_mut_slice()
10368 .fill(128);
10369 src.set_colorimetry(Some(
10373 edgefirst_tensor::Colorimetry::default()
10374 .with_encoding(edgefirst_tensor::ColorEncoding::Bt601)
10375 .with_range(edgefirst_tensor::ColorRange::Full),
10376 ));
10377
10378 let dst = TensorDyn::image(
10379 8,
10380 5,
10381 PixelFormat::Rgb,
10382 DType::U8,
10383 Some(TensorMemory::Mem),
10384 edgefirst_tensor::CpuAccess::ReadWrite,
10385 )
10386 .unwrap();
10387 let mut cpu_converter = CPUProcessor::new();
10388 let (result, _src, dst) = convert_img(
10389 &mut cpu_converter,
10390 src,
10391 dst,
10392 Rotation::None,
10393 Flip::None,
10394 Crop::no_crop(),
10395 );
10396 result.unwrap();
10397
10398 assert_eq!((dst.width(), dst.height()), (Some(8), Some(5)));
10399 let map = dst.as_typed::<u8>().unwrap().map().unwrap();
10400 for (i, &b) in map.as_slice().iter().enumerate() {
10401 assert!(
10402 (b as i16 - 128).abs() <= 2,
10403 "pixel byte {i} = {b}, expected ~128 for neutral-grey NV12"
10404 );
10405 }
10406 }
10407
10408 #[test]
10409 fn test_nv24_to_rgb_cpu() {
10410 let mut src = TensorDyn::image(
10416 8,
10417 4,
10418 PixelFormat::Nv24,
10419 DType::U8,
10420 Some(TensorMemory::Mem),
10421 edgefirst_tensor::CpuAccess::ReadWrite,
10422 )
10423 .unwrap();
10424 assert_eq!(src.shape(), &[12, 8]);
10425 assert_eq!((src.width(), src.height()), (Some(8), Some(4)));
10426 src.as_typed::<u8>()
10427 .unwrap()
10428 .map()
10429 .unwrap()
10430 .as_mut_slice()
10431 .fill(128);
10432 src.set_colorimetry(Some(
10435 edgefirst_tensor::Colorimetry::default()
10436 .with_encoding(edgefirst_tensor::ColorEncoding::Bt601)
10437 .with_range(edgefirst_tensor::ColorRange::Full),
10438 ));
10439
10440 let dst = TensorDyn::image(
10441 8,
10442 4,
10443 PixelFormat::Rgb,
10444 DType::U8,
10445 Some(TensorMemory::Mem),
10446 edgefirst_tensor::CpuAccess::ReadWrite,
10447 )
10448 .unwrap();
10449 let mut cpu_converter = CPUProcessor::new();
10450 let (result, _src, dst) = convert_img(
10451 &mut cpu_converter,
10452 src,
10453 dst,
10454 Rotation::None,
10455 Flip::None,
10456 Crop::no_crop(),
10457 );
10458 result.unwrap();
10459
10460 assert_eq!((dst.width(), dst.height()), (Some(8), Some(4)));
10461 let map = dst.as_typed::<u8>().unwrap().map().unwrap();
10462 for (i, &b) in map.as_slice().iter().enumerate() {
10463 assert!(
10464 (b as i16 - 128).abs() <= 2,
10465 "pixel byte {i} = {b}, expected ~128 for neutral-grey NV24"
10466 );
10467 }
10468 }
10469
10470 #[test]
10471 fn cpu_nv12_to_rgb_respects_tagged_bt2020() {
10472 fn decode_tagged(enc: edgefirst_tensor::ColorEncoding) -> [u8; 3] {
10480 let mut src = TensorDyn::image(
10481 8,
10482 4,
10483 PixelFormat::Nv12,
10484 DType::U8,
10485 Some(TensorMemory::Mem),
10486 edgefirst_tensor::CpuAccess::ReadWrite,
10487 )
10488 .unwrap();
10489 assert_eq!(src.shape(), &[6, 8]);
10491 {
10492 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
10493 let buf = map.as_mut_slice();
10494 buf[..32].fill(120); for px in buf[32..].as_chunks_mut::<2>().0 {
10496 px[0] = 180; px[1] = 64; }
10499 }
10500 src.set_colorimetry(Some(
10503 edgefirst_tensor::Colorimetry::default()
10504 .with_encoding(enc)
10505 .with_range(edgefirst_tensor::ColorRange::Limited),
10506 ));
10507 let dst = TensorDyn::image(
10508 8,
10509 4,
10510 PixelFormat::Rgb,
10511 DType::U8,
10512 Some(TensorMemory::Mem),
10513 edgefirst_tensor::CpuAccess::ReadWrite,
10514 )
10515 .unwrap();
10516 let mut cpu = CPUProcessor::new();
10517 let (result, _src, dst) = convert_img(
10518 &mut cpu,
10519 src,
10520 dst,
10521 Rotation::None,
10522 Flip::None,
10523 Crop::no_crop(),
10524 );
10525 result.unwrap();
10526 let map = dst.as_typed::<u8>().unwrap().map().unwrap();
10527 let s = map.as_slice();
10528 [s[0], s[1], s[2]]
10529 }
10530
10531 let bt601 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt601);
10532 let bt709 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt709);
10533 let bt2020 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt2020);
10534
10535 assert_ne!(
10536 bt2020, bt601,
10537 "BT.2020 must decode differently from BT.601 ({bt2020:?} vs {bt601:?})"
10538 );
10539 assert_ne!(
10540 bt2020, bt709,
10541 "BT.2020 must decode differently from BT.709 ({bt2020:?} vs {bt709:?})"
10542 );
10543 assert_ne!(
10544 bt601, bt709,
10545 "BT.601 must decode differently from BT.709 ({bt601:?} vs {bt709:?})"
10546 );
10547 }
10548
10549 #[test]
10550 fn test_nv12_to_rgb_cpu() {
10551 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
10552 let src = TensorDyn::image(
10553 1280,
10554 720,
10555 PixelFormat::Nv12,
10556 DType::U8,
10557 None,
10558 edgefirst_tensor::CpuAccess::ReadWrite,
10559 )
10560 .unwrap();
10561 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
10562 .copy_from_slice(&file);
10563
10564 let dst = TensorDyn::image(
10565 1280,
10566 720,
10567 PixelFormat::Rgb,
10568 DType::U8,
10569 None,
10570 edgefirst_tensor::CpuAccess::ReadWrite,
10571 )
10572 .unwrap();
10573 let mut cpu_converter = CPUProcessor::new();
10574
10575 let (result, _src, dst) = convert_img(
10576 &mut cpu_converter,
10577 src,
10578 dst,
10579 Rotation::None,
10580 Flip::None,
10581 Crop::no_crop(),
10582 );
10583 result.unwrap();
10584
10585 let target_image = crate::load_image_test_helper(
10586 &edgefirst_bench::testdata::read("zidane.jpg"),
10587 Some(PixelFormat::Rgb),
10588 None,
10589 )
10590 .unwrap();
10591
10592 compare_images(&dst, &target_image, 0.95, function!());
10597 }
10598
10599 #[test]
10600 fn test_nv12_to_grey_cpu() {
10601 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
10602 let src = TensorDyn::image(
10603 1280,
10604 720,
10605 PixelFormat::Nv12,
10606 DType::U8,
10607 None,
10608 edgefirst_tensor::CpuAccess::ReadWrite,
10609 )
10610 .unwrap();
10611 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
10612 .copy_from_slice(&file);
10613
10614 let dst = TensorDyn::image(
10615 1280,
10616 720,
10617 PixelFormat::Grey,
10618 DType::U8,
10619 None,
10620 edgefirst_tensor::CpuAccess::ReadWrite,
10621 )
10622 .unwrap();
10623 let mut cpu_converter = CPUProcessor::new();
10624
10625 let (result, _src, dst) = convert_img(
10626 &mut cpu_converter,
10627 src,
10628 dst,
10629 Rotation::None,
10630 Flip::None,
10631 Crop::no_crop(),
10632 );
10633 result.unwrap();
10634
10635 let target_image = crate::load_image_test_helper(
10636 &edgefirst_bench::testdata::read("zidane.jpg"),
10637 Some(PixelFormat::Grey),
10638 None,
10639 )
10640 .unwrap();
10641
10642 compare_images(&dst, &target_image, 0.95, function!());
10647 }
10648
10649 #[test]
10650 fn test_nv12_to_yuyv_cpu() {
10651 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
10652 let src = TensorDyn::image(
10653 1280,
10654 720,
10655 PixelFormat::Nv12,
10656 DType::U8,
10657 None,
10658 edgefirst_tensor::CpuAccess::ReadWrite,
10659 )
10660 .unwrap();
10661 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
10662 .copy_from_slice(&file);
10663
10664 let dst = TensorDyn::image(
10665 1280,
10666 720,
10667 PixelFormat::Yuyv,
10668 DType::U8,
10669 None,
10670 edgefirst_tensor::CpuAccess::ReadWrite,
10671 )
10672 .unwrap();
10673 let mut cpu_converter = CPUProcessor::new();
10674
10675 let (result, _src, dst) = convert_img(
10676 &mut cpu_converter,
10677 src,
10678 dst,
10679 Rotation::None,
10680 Flip::None,
10681 Crop::no_crop(),
10682 );
10683 result.unwrap();
10684
10685 let target_image = crate::load_image_test_helper(
10686 &edgefirst_bench::testdata::read("zidane.jpg"),
10687 Some(PixelFormat::Rgb),
10688 None,
10689 )
10690 .unwrap();
10691
10692 compare_images_convert_to_rgb(&dst, &target_image, 0.95, function!());
10697 }
10698
10699 #[test]
10700 fn test_cpu_resize_nv16() {
10701 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
10702 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
10703
10704 let cpu_nv16_dst = TensorDyn::image(
10705 640,
10706 640,
10707 PixelFormat::Nv16,
10708 DType::U8,
10709 None,
10710 edgefirst_tensor::CpuAccess::ReadWrite,
10711 )
10712 .unwrap();
10713 let cpu_rgb_dst = TensorDyn::image(
10714 640,
10715 640,
10716 PixelFormat::Rgb,
10717 DType::U8,
10718 None,
10719 edgefirst_tensor::CpuAccess::ReadWrite,
10720 )
10721 .unwrap();
10722 let mut cpu_converter = CPUProcessor::new();
10723 let crop = Crop::letterbox([255, 128, 0, 255]);
10724
10725 let (result, src, cpu_nv16_dst) = convert_img(
10726 &mut cpu_converter,
10727 src,
10728 cpu_nv16_dst,
10729 Rotation::None,
10730 Flip::None,
10731 crop,
10732 );
10733 result.unwrap();
10734
10735 let (result, _src, cpu_rgb_dst) = convert_img(
10736 &mut cpu_converter,
10737 src,
10738 cpu_rgb_dst,
10739 Rotation::None,
10740 Flip::None,
10741 crop,
10742 );
10743 result.unwrap();
10744 compare_images_convert_to_rgb(
10745 &cpu_nv16_dst,
10746 &cpu_rgb_dst,
10747 yuv_threshold(0.99),
10748 function!(),
10749 );
10750 }
10751
10752 fn yuv_threshold(balanced: f64) -> f64 {
10769 #[cfg(target_arch = "aarch64")]
10770 {
10771 if !std::arch::is_aarch64_feature_detected!("rdm") {
10772 return balanced - 0.01;
10773 }
10774 }
10775 balanced
10776 }
10777
10778 fn load_bytes_to_tensor(
10779 width: usize,
10780 height: usize,
10781 format: PixelFormat,
10782 memory: Option<TensorMemory>,
10783 bytes: &[u8],
10784 ) -> Result<TensorDyn, Error> {
10785 let src = TensorDyn::image(
10786 width,
10787 height,
10788 format,
10789 DType::U8,
10790 memory,
10791 edgefirst_tensor::CpuAccess::ReadWrite,
10792 )?;
10793 src.as_typed::<u8>()
10794 .unwrap()
10795 .map()?
10796 .as_mut_slice()
10797 .copy_from_slice(bytes);
10798 Ok(src)
10799 }
10800
10801 fn compare_images(img1: &TensorDyn, img2: &TensorDyn, threshold: f64, name: &str) {
10809 assert_eq!(img1.height(), img2.height(), "Heights differ");
10810 assert_eq!(img1.width(), img2.width(), "Widths differ");
10811 assert_eq!(
10812 img1.format().unwrap(),
10813 img2.format().unwrap(),
10814 "PixelFormat differ"
10815 );
10816 assert!(
10817 matches!(
10818 img1.format().unwrap(),
10819 PixelFormat::Rgb | PixelFormat::Rgba | PixelFormat::Grey | PixelFormat::PlanarRgb
10820 ),
10821 "format must be Rgb or Rgba for comparison"
10822 );
10823
10824 fn flat(img: &TensorDyn) -> Vec<u8> {
10831 let t = img.as_typed::<u8>().expect("u8 image");
10832 let mut out = vec![0u8; t.shape().iter().product::<usize>()];
10833 t.copy_to_flat(&mut out).expect("compact the padded rows");
10834 out
10835 }
10836
10837 let image1 = match img1.format().unwrap() {
10838 PixelFormat::Rgb => image::RgbImage::from_vec(
10839 img1.width().unwrap() as u32,
10840 img1.height().unwrap() as u32,
10841 flat(img1),
10842 )
10843 .unwrap(),
10844 PixelFormat::Rgba => image::RgbaImage::from_vec(
10845 img1.width().unwrap() as u32,
10846 img1.height().unwrap() as u32,
10847 flat(img1),
10848 )
10849 .unwrap()
10850 .convert(),
10851 PixelFormat::Grey => image::GrayImage::from_vec(
10852 img1.width().unwrap() as u32,
10853 img1.height().unwrap() as u32,
10854 flat(img1),
10855 )
10856 .unwrap()
10857 .convert(),
10858 PixelFormat::PlanarRgb => image::GrayImage::from_vec(
10859 img1.width().unwrap() as u32,
10860 (img1.height().unwrap() * 3) as u32,
10861 flat(img1),
10862 )
10863 .unwrap()
10864 .convert(),
10865 _ => return,
10866 };
10867
10868 let image2 = match img2.format().unwrap() {
10869 PixelFormat::Rgb => image::RgbImage::from_vec(
10870 img2.width().unwrap() as u32,
10871 img2.height().unwrap() as u32,
10872 flat(img2),
10873 )
10874 .unwrap(),
10875 PixelFormat::Rgba => image::RgbaImage::from_vec(
10876 img2.width().unwrap() as u32,
10877 img2.height().unwrap() as u32,
10878 flat(img2),
10879 )
10880 .unwrap()
10881 .convert(),
10882 PixelFormat::Grey => image::GrayImage::from_vec(
10883 img2.width().unwrap() as u32,
10884 img2.height().unwrap() as u32,
10885 flat(img2),
10886 )
10887 .unwrap()
10888 .convert(),
10889 PixelFormat::PlanarRgb => image::GrayImage::from_vec(
10890 img2.width().unwrap() as u32,
10891 (img2.height().unwrap() * 3) as u32,
10892 flat(img2),
10893 )
10894 .unwrap()
10895 .convert(),
10896 _ => return,
10897 };
10898
10899 let similarity = image_compare::rgb_similarity_structure(
10900 &image_compare::Algorithm::RootMeanSquared,
10901 &image1,
10902 &image2,
10903 )
10904 .expect("Image Comparison failed");
10905 if similarity.score < threshold {
10906 similarity
10909 .image
10910 .to_color_map()
10911 .save(format!("{name}.png"))
10912 .unwrap();
10913 panic!(
10914 "{name}: converted image and target image have similarity score too low: {} < {}",
10915 similarity.score, threshold
10916 )
10917 }
10918 }
10919
10920 fn compare_images_convert_to_rgb(
10921 img1: &TensorDyn,
10922 img2: &TensorDyn,
10923 threshold: f64,
10924 name: &str,
10925 ) {
10926 assert_eq!(img1.height(), img2.height(), "Heights differ");
10927 assert_eq!(img1.width(), img2.width(), "Widths differ");
10928
10929 let mut img_rgb1 = TensorDyn::image(
10930 img1.width().unwrap(),
10931 img1.height().unwrap(),
10932 PixelFormat::Rgb,
10933 DType::U8,
10934 Some(TensorMemory::Mem),
10935 edgefirst_tensor::CpuAccess::ReadWrite,
10936 )
10937 .unwrap();
10938 let mut img_rgb2 = TensorDyn::image(
10939 img1.width().unwrap(),
10940 img1.height().unwrap(),
10941 PixelFormat::Rgb,
10942 DType::U8,
10943 Some(TensorMemory::Mem),
10944 edgefirst_tensor::CpuAccess::ReadWrite,
10945 )
10946 .unwrap();
10947 let mut __cv = CPUProcessor::default();
10948 let r1 = __cv.convert(
10949 img1,
10950 &mut img_rgb1,
10951 crate::Rotation::None,
10952 crate::Flip::None,
10953 crate::Crop::default(),
10954 );
10955 let r2 = __cv.convert(
10956 img2,
10957 &mut img_rgb2,
10958 crate::Rotation::None,
10959 crate::Flip::None,
10960 crate::Crop::default(),
10961 );
10962 if r1.is_err() || r2.is_err() {
10963 let w = img1.width().unwrap() as u32;
10965 let data1 = img1.as_typed::<u8>().unwrap().map().unwrap().to_vec();
10966 let data2 = img2.as_typed::<u8>().unwrap().map().unwrap().to_vec();
10967 let h1 = (data1.len() as u32) / w;
10968 let h2 = (data2.len() as u32) / w;
10969 let g1 = image::GrayImage::from_vec(w, h1, data1).unwrap();
10970 let g2 = image::GrayImage::from_vec(w, h2, data2).unwrap();
10971 let similarity = image_compare::gray_similarity_structure(
10972 &image_compare::Algorithm::RootMeanSquared,
10973 &g1,
10974 &g2,
10975 )
10976 .expect("Image Comparison failed");
10977 if similarity.score < threshold {
10978 panic!(
10979 "{name}: converted image and target image have similarity score too low: {} < {}",
10980 similarity.score, threshold
10981 )
10982 }
10983 return;
10984 }
10985
10986 let image1 = image::RgbImage::from_vec(
10987 img_rgb1.width().unwrap() as u32,
10988 img_rgb1.height().unwrap() as u32,
10989 img_rgb1.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
10990 )
10991 .unwrap();
10992
10993 let image2 = image::RgbImage::from_vec(
10994 img_rgb2.width().unwrap() as u32,
10995 img_rgb2.height().unwrap() as u32,
10996 img_rgb2.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
10997 )
10998 .unwrap();
10999
11000 let similarity = image_compare::rgb_similarity_structure(
11001 &image_compare::Algorithm::RootMeanSquared,
11002 &image1,
11003 &image2,
11004 )
11005 .expect("Image Comparison failed");
11006 if similarity.score < threshold {
11007 similarity
11010 .image
11011 .to_color_map()
11012 .save(format!("{name}.png"))
11013 .unwrap();
11014 panic!(
11015 "{name}: converted image and target image have similarity score too low: {} < {}",
11016 similarity.score, threshold
11017 )
11018 }
11019 }
11020
11021 #[test]
11026 fn test_nv12_image_creation() {
11027 let width = 640;
11028 let height = 480;
11029 let img = TensorDyn::image(
11030 width,
11031 height,
11032 PixelFormat::Nv12,
11033 DType::U8,
11034 None,
11035 edgefirst_tensor::CpuAccess::ReadWrite,
11036 )
11037 .unwrap();
11038
11039 assert_eq!(img.width(), Some(width));
11040 assert_eq!(img.height(), Some(height));
11041 assert_eq!(img.format().unwrap(), PixelFormat::Nv12);
11042 assert_eq!(
11044 img.as_typed::<u8>().unwrap().shape(),
11045 &[height * 3 / 2, width]
11046 );
11047 }
11048
11049 #[test]
11050 fn test_nv12_channels() {
11051 let img = TensorDyn::image(
11052 640,
11053 480,
11054 PixelFormat::Nv12,
11055 DType::U8,
11056 None,
11057 edgefirst_tensor::CpuAccess::ReadWrite,
11058 )
11059 .unwrap();
11060 assert_eq!(img.format().unwrap().channels(), 1);
11062 }
11063
11064 #[test]
11069 fn test_tensor_set_format_planar() {
11070 let mut tensor = Tensor::<u8>::new(&[3, 480, 640], None, None).unwrap();
11071 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
11072 assert_eq!(tensor.format(), Some(PixelFormat::PlanarRgb));
11073 assert_eq!(tensor.width(), Some(640));
11074 assert_eq!(tensor.height(), Some(480));
11075 }
11076
11077 #[test]
11078 fn test_tensor_set_format_interleaved() {
11079 let mut tensor = Tensor::<u8>::new(&[480, 640, 4], None, None).unwrap();
11080 tensor.set_format(PixelFormat::Rgba).unwrap();
11081 assert_eq!(tensor.format(), Some(PixelFormat::Rgba));
11082 assert_eq!(tensor.width(), Some(640));
11083 assert_eq!(tensor.height(), Some(480));
11084 }
11085
11086 #[test]
11087 fn test_tensordyn_image_rgb() {
11088 let img = TensorDyn::image(
11089 640,
11090 480,
11091 PixelFormat::Rgb,
11092 DType::U8,
11093 None,
11094 edgefirst_tensor::CpuAccess::ReadWrite,
11095 )
11096 .unwrap();
11097 assert_eq!(img.width(), Some(640));
11098 assert_eq!(img.height(), Some(480));
11099 assert_eq!(img.format(), Some(PixelFormat::Rgb));
11100 }
11101
11102 #[test]
11103 fn test_tensordyn_image_planar_rgb() {
11104 let img = TensorDyn::image(
11105 640,
11106 480,
11107 PixelFormat::PlanarRgb,
11108 DType::U8,
11109 None,
11110 edgefirst_tensor::CpuAccess::ReadWrite,
11111 )
11112 .unwrap();
11113 assert_eq!(img.width(), Some(640));
11114 assert_eq!(img.height(), Some(480));
11115 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
11116 }
11117
11118 #[test]
11119 fn test_rgb_int8_format() {
11120 let img = TensorDyn::image(
11122 1280,
11123 720,
11124 PixelFormat::Rgb,
11125 DType::I8,
11126 Some(TensorMemory::Mem),
11127 edgefirst_tensor::CpuAccess::ReadWrite,
11128 )
11129 .unwrap();
11130 assert_eq!(img.width(), Some(1280));
11131 assert_eq!(img.height(), Some(720));
11132 assert_eq!(img.format(), Some(PixelFormat::Rgb));
11133 assert_eq!(img.dtype(), DType::I8);
11134 }
11135
11136 #[test]
11137 fn test_planar_rgb_int8_format() {
11138 let img = TensorDyn::image(
11139 1280,
11140 720,
11141 PixelFormat::PlanarRgb,
11142 DType::I8,
11143 Some(TensorMemory::Mem),
11144 edgefirst_tensor::CpuAccess::ReadWrite,
11145 )
11146 .unwrap();
11147 assert_eq!(img.width(), Some(1280));
11148 assert_eq!(img.height(), Some(720));
11149 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
11150 assert_eq!(img.dtype(), DType::I8);
11151 }
11152
11153 #[test]
11154 fn test_rgb_from_tensor() {
11155 let mut tensor = Tensor::<u8>::new(&[720, 1280, 3], None, None).unwrap();
11156 tensor.set_format(PixelFormat::Rgb).unwrap();
11157 let img = TensorDyn::from(tensor);
11158 assert_eq!(img.width(), Some(1280));
11159 assert_eq!(img.height(), Some(720));
11160 assert_eq!(img.format(), Some(PixelFormat::Rgb));
11161 }
11162
11163 #[test]
11164 fn test_planar_rgb_from_tensor() {
11165 let mut tensor = Tensor::<u8>::new(&[3, 720, 1280], None, None).unwrap();
11166 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
11167 let img = TensorDyn::from(tensor);
11168 assert_eq!(img.width(), Some(1280));
11169 assert_eq!(img.height(), Some(720));
11170 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
11171 }
11172
11173 #[test]
11174 fn test_dtype_determines_int8() {
11175 let u8_img = TensorDyn::image(
11177 64,
11178 64,
11179 PixelFormat::Rgb,
11180 DType::U8,
11181 None,
11182 edgefirst_tensor::CpuAccess::ReadWrite,
11183 )
11184 .unwrap();
11185 let i8_img = TensorDyn::image(
11186 64,
11187 64,
11188 PixelFormat::Rgb,
11189 DType::I8,
11190 None,
11191 edgefirst_tensor::CpuAccess::ReadWrite,
11192 )
11193 .unwrap();
11194 assert_eq!(u8_img.dtype(), DType::U8);
11195 assert_eq!(i8_img.dtype(), DType::I8);
11196 }
11197
11198 #[test]
11199 fn test_pixel_layout_packed_vs_planar() {
11200 assert_eq!(PixelFormat::Rgb.layout(), PixelLayout::Packed);
11202 assert_eq!(PixelFormat::Rgba.layout(), PixelLayout::Packed);
11203 assert_eq!(PixelFormat::PlanarRgb.layout(), PixelLayout::Planar);
11204 assert_eq!(PixelFormat::Nv12.layout(), PixelLayout::SemiPlanar);
11205 }
11206
11207 #[cfg(target_os = "linux")]
11218 #[cfg(feature = "opengl")]
11219 #[test]
11220 fn pbo_reports_why_it_cannot_pin() {
11221 use edgefirst_tensor::{Error, TensorTrait};
11222
11223 let converter = ImageProcessor::new().unwrap();
11224 let is_pbo = converter
11225 .opengl
11226 .as_ref()
11227 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
11228 if !is_pbo {
11229 use std::io::Write;
11234 let _ = writeln!(
11235 &mut std::io::stderr(),
11236 "SKIPPED: pbo_reports_why_it_cannot_pin - transfer backend is not PBO"
11237 );
11238 return;
11239 }
11240
11241 let t = converter
11242 .create_image(
11243 64,
11244 64,
11245 PixelFormat::Rgba,
11246 DType::U8,
11247 None,
11248 edgefirst_tensor::CpuAccess::ReadWrite,
11249 )
11250 .unwrap();
11251 assert_eq!(t.as_typed::<u8>().unwrap().memory(), TensorMemory::Pbo);
11252 let t = t.as_typed::<u8>().unwrap();
11253
11254 match t.pin_host(edgefirst_tensor::CpuAccess::ReadWrite) {
11255 Err(Error::NotImplemented(m)) => {
11256 assert!(
11257 m.contains("glMapBufferRange"),
11258 "must name the constraint: {m}"
11259 );
11260 assert!(m.contains("map()"), "must point at the supported API: {m}");
11261 }
11262 other => panic!("expected an explained NotImplemented, got {other:?}"),
11263 }
11264 assert!(matches!(
11266 t.sync_for_cpu(edgefirst_tensor::CpuAccess::Read),
11267 Err(Error::NotImplemented(_))
11268 ));
11269
11270 let g = t.map().expect("map must work where pin cannot");
11272 assert!(!g.as_slice().is_empty());
11273 }
11274
11275 #[cfg(target_os = "linux")]
11279 #[cfg(feature = "opengl")]
11280 #[test]
11281 fn test_convert_pbo_to_pbo() {
11282 let mut converter = ImageProcessor::new().unwrap();
11283
11284 let is_pbo = converter
11286 .opengl
11287 .as_ref()
11288 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
11289 if !is_pbo {
11290 log::warn!("Skipping test_convert_pbo_to_pbo: backend is not PBO");
11291 return;
11292 }
11293
11294 let src_w = 640;
11295 let src_h = 480;
11296 let dst_w = 320;
11297 let dst_h = 240;
11298
11299 let pbo_src = converter
11301 .create_image(
11302 src_w,
11303 src_h,
11304 PixelFormat::Rgba,
11305 DType::U8,
11306 None,
11307 edgefirst_tensor::CpuAccess::ReadWrite,
11308 )
11309 .unwrap();
11310 assert_eq!(
11311 pbo_src.as_typed::<u8>().unwrap().memory(),
11312 TensorMemory::Pbo,
11313 "create_image should produce a PBO tensor"
11314 );
11315
11316 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
11318 let jpeg_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
11319
11320 let mem_src = TensorDyn::image(
11322 src_w,
11323 src_h,
11324 PixelFormat::Rgba,
11325 DType::U8,
11326 Some(TensorMemory::Mem),
11327 edgefirst_tensor::CpuAccess::ReadWrite,
11328 )
11329 .unwrap();
11330 let (result, _jpeg_src, mem_src) = convert_img(
11331 &mut CPUProcessor::new(),
11332 jpeg_src,
11333 mem_src,
11334 Rotation::None,
11335 Flip::None,
11336 Crop::no_crop(),
11337 );
11338 result.unwrap();
11339
11340 {
11342 let src_data = mem_src.as_typed::<u8>().unwrap().map().unwrap();
11343 let mut pbo_map = pbo_src.as_typed::<u8>().unwrap().map().unwrap();
11344 pbo_map.copy_from_slice(&src_data);
11345 }
11346
11347 let pbo_dst = converter
11349 .create_image(
11350 dst_w,
11351 dst_h,
11352 PixelFormat::Rgba,
11353 DType::U8,
11354 None,
11355 edgefirst_tensor::CpuAccess::ReadWrite,
11356 )
11357 .unwrap();
11358 assert_eq!(
11359 pbo_dst.as_typed::<u8>().unwrap().memory(),
11360 TensorMemory::Pbo
11361 );
11362
11363 let mut pbo_dst = pbo_dst;
11365 let result = converter.convert(
11366 &pbo_src,
11367 &mut pbo_dst,
11368 Rotation::None,
11369 Flip::None,
11370 Crop::no_crop(),
11371 );
11372 result.unwrap();
11373
11374 let cpu_dst = TensorDyn::image(
11376 dst_w,
11377 dst_h,
11378 PixelFormat::Rgba,
11379 DType::U8,
11380 Some(TensorMemory::Mem),
11381 edgefirst_tensor::CpuAccess::ReadWrite,
11382 )
11383 .unwrap();
11384 let (result, _mem_src, cpu_dst) = convert_img(
11385 &mut CPUProcessor::new(),
11386 mem_src,
11387 cpu_dst,
11388 Rotation::None,
11389 Flip::None,
11390 Crop::no_crop(),
11391 );
11392 result.unwrap();
11393
11394 let pbo_dst_img = {
11395 let mut __t = pbo_dst.into_u8().unwrap();
11396 __t.set_format(PixelFormat::Rgba).unwrap();
11397 TensorDyn::from(__t)
11398 };
11399 compare_images(&pbo_dst_img, &cpu_dst, 0.95, function!());
11400 log::info!("test_convert_pbo_to_pbo: PASS — PBO-to-PBO convert matches CPU reference");
11401 }
11402
11403 #[test]
11404 fn test_image_bgra() {
11405 let img = TensorDyn::image(
11406 640,
11407 480,
11408 PixelFormat::Bgra,
11409 DType::U8,
11410 Some(edgefirst_tensor::TensorMemory::Mem),
11411 edgefirst_tensor::CpuAccess::ReadWrite,
11412 )
11413 .unwrap();
11414 assert_eq!(img.width(), Some(640));
11415 assert_eq!(img.height(), Some(480));
11416 assert_eq!(img.format().unwrap().channels(), 4);
11417 assert_eq!(img.format().unwrap(), PixelFormat::Bgra);
11418 }
11419
11420 #[test]
11425 fn test_force_backend_cpu() {
11426 let _lock = acquire_env_lock();
11427 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11428 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
11429 let converter = ImageProcessor::new().unwrap();
11430 assert!(converter.cpu.is_some());
11431 assert_eq!(converter.forced_backend, Some(ForcedBackend::Cpu));
11432 }
11433
11434 #[test]
11435 fn test_force_backend_invalid() {
11436 let _lock = acquire_env_lock();
11437 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11438 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "invalid") };
11439 let result = ImageProcessor::new();
11440 assert!(
11441 matches!(&result, Err(Error::ForcedBackendUnavailable(s)) if s.contains("unknown")),
11442 "invalid backend value should return ForcedBackendUnavailable error: {result:?}"
11443 );
11444 }
11445
11446 #[test]
11447 fn test_force_backend_unset() {
11448 let _lock = acquire_env_lock();
11449 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11450 unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") };
11451 let converter = ImageProcessor::new().unwrap();
11452 assert!(converter.forced_backend.is_none());
11453 }
11454
11455 #[test]
11460 fn test_draw_proto_masks_no_cpu_returns_error() {
11461 let _lock = acquire_env_lock();
11463 let _guard = EnvGuard::snapshot(&[
11464 "EDGEFIRST_FORCE_BACKEND",
11465 "EDGEFIRST_DISABLE_GL",
11466 "EDGEFIRST_DISABLE_G2D",
11467 "EDGEFIRST_DISABLE_CPU",
11468 ]);
11469
11470 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
11472 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
11473 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
11474
11475 let mut converter = ImageProcessor::new().unwrap();
11476 assert!(converter.cpu.is_none(), "CPU should be disabled");
11477
11478 let dst = TensorDyn::image(
11479 640,
11480 480,
11481 PixelFormat::Rgba,
11482 DType::U8,
11483 Some(TensorMemory::Mem),
11484 edgefirst_tensor::CpuAccess::ReadWrite,
11485 )
11486 .unwrap();
11487 let mut dst_dyn = dst;
11488 let det = [DetectBox {
11489 bbox: edgefirst_tensor::BoundingBox {
11490 xmin: 0.1,
11491 ymin: 0.1,
11492 xmax: 0.5,
11493 ymax: 0.5,
11494 },
11495 score: 0.9,
11496 label: 0,
11497 }];
11498 let proto_data = {
11499 use edgefirst_tensor::{Tensor, TensorDyn};
11500 let coeff_t = Tensor::<f32>::from_slice(&[0.5_f32; 4], &[1, 4]).unwrap();
11501 let protos_t =
11502 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
11503 ProtoData {
11504 mask_coefficients: TensorDyn::F32(coeff_t),
11505 protos: TensorDyn::F32(protos_t),
11506 layout: ProtoLayout::Nhwc,
11507 }
11508 };
11509 let result =
11510 converter.draw_proto_masks(&mut dst_dyn, &det, &proto_data, Default::default());
11511 assert!(
11512 matches!(&result, Err(Error::Internal(s)) if s.contains("CPU backend")),
11513 "draw_proto_masks without CPU should return Internal error: {result:?}"
11514 );
11515 }
11516
11517 #[test]
11518 fn test_draw_proto_masks_cpu_fallback_works() {
11519 let _lock = acquire_env_lock();
11522 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11523 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
11524 let mut converter = ImageProcessor::new().unwrap();
11525 assert!(converter.cpu.is_some());
11526
11527 let dst = TensorDyn::image(
11528 64,
11529 64,
11530 PixelFormat::Rgba,
11531 DType::U8,
11532 Some(TensorMemory::Mem),
11533 edgefirst_tensor::CpuAccess::ReadWrite,
11534 )
11535 .unwrap();
11536 let mut dst_dyn = dst;
11537 let det = [DetectBox {
11538 bbox: edgefirst_tensor::BoundingBox {
11539 xmin: 0.1,
11540 ymin: 0.1,
11541 xmax: 0.5,
11542 ymax: 0.5,
11543 },
11544 score: 0.9,
11545 label: 0,
11546 }];
11547 let proto_data = {
11548 use edgefirst_tensor::{Tensor, TensorDyn};
11549 let coeff_t = Tensor::<f32>::from_slice(&[0.5_f32; 4], &[1, 4]).unwrap();
11550 let protos_t =
11551 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
11552 ProtoData {
11553 mask_coefficients: TensorDyn::F32(coeff_t),
11554 protos: TensorDyn::F32(protos_t),
11555 layout: ProtoLayout::Nhwc,
11556 }
11557 };
11558 let result =
11559 converter.draw_proto_masks(&mut dst_dyn, &det, &proto_data, Default::default());
11560 assert!(result.is_ok(), "CPU fallback path should work: {result:?}");
11561 }
11562
11563 fn acquire_env_lock() -> std::sync::MutexGuard<'static, ()> {
11595 use std::sync::{Mutex, OnceLock};
11596 static ENV_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
11597 ENV_MUTEX
11598 .get_or_init(|| Mutex::new(()))
11599 .lock()
11600 .unwrap_or_else(|e| e.into_inner())
11601 }
11602
11603 struct EnvGuard {
11606 vars: Vec<(&'static str, Option<String>)>,
11607 }
11608
11609 impl EnvGuard {
11610 fn snapshot(names: &[&'static str]) -> Self {
11614 Self {
11615 vars: names.iter().map(|&k| (k, std::env::var(k).ok())).collect(),
11616 }
11617 }
11618 }
11619
11620 impl Drop for EnvGuard {
11621 fn drop(&mut self) {
11622 for (k, v) in &self.vars {
11623 match v {
11624 Some(s) => unsafe { std::env::set_var(k, s) },
11625 None => unsafe { std::env::remove_var(k) },
11626 }
11627 }
11628 }
11629 }
11630
11631 fn with_force_backend<R>(value: Option<&str>, body: impl FnOnce() -> R) -> R {
11635 let _lock = acquire_env_lock();
11636 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11637 match value {
11638 Some(v) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", v) },
11639 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
11640 }
11641 body()
11642 }
11643
11644 fn make_dirty_dst(w: usize, h: usize, mem: Option<TensorMemory>) -> TensorDyn {
11649 let dst = TensorDyn::image(
11650 w,
11651 h,
11652 PixelFormat::Rgba,
11653 DType::U8,
11654 mem,
11655 edgefirst_tensor::CpuAccess::ReadWrite,
11656 )
11657 .unwrap();
11658 {
11659 use edgefirst_tensor::TensorMapTrait;
11660 let u8t = dst.as_typed::<u8>().unwrap();
11661 let mut map = u8t.map().unwrap();
11662 for (i, b) in map.as_mut_slice().iter_mut().enumerate() {
11663 *b = 0xA0u8.wrapping_add((i as u8) & 0x3F);
11664 }
11665 }
11666 dst
11667 }
11668
11669 fn make_bg(w: usize, h: usize, mem: Option<TensorMemory>, rgba: [u8; 4]) -> TensorDyn {
11671 let bg = TensorDyn::image(
11672 w,
11673 h,
11674 PixelFormat::Rgba,
11675 DType::U8,
11676 mem,
11677 edgefirst_tensor::CpuAccess::ReadWrite,
11678 )
11679 .unwrap();
11680 {
11681 use edgefirst_tensor::TensorMapTrait;
11682 let u8t = bg.as_typed::<u8>().unwrap();
11683 let mut map = u8t.map().unwrap();
11684 for chunk in map.as_mut_slice().as_chunks_mut::<4>().0 {
11685 chunk.copy_from_slice(&rgba);
11686 }
11687 }
11688 bg
11689 }
11690
11691 fn pixel_at(dst: &TensorDyn, x: usize, y: usize) -> [u8; 4] {
11692 use edgefirst_tensor::TensorMapTrait;
11693 let w = dst.width().unwrap();
11694 let off = (y * w + x) * 4;
11695 let u8t = dst.as_typed::<u8>().unwrap();
11696 let map = u8t.map().unwrap();
11697 let s = map.as_slice();
11698 [s[off], s[off + 1], s[off + 2], s[off + 3]]
11699 }
11700
11701 fn assert_every_pixel_eq(dst: &TensorDyn, expected: [u8; 4], case: &str) {
11702 use edgefirst_tensor::TensorMapTrait;
11703 let u8t = dst.as_typed::<u8>().unwrap();
11704 let map = u8t.map().unwrap();
11705 for (i, chunk) in map.as_slice().as_chunks::<4>().0.iter().enumerate() {
11706 assert_eq!(
11707 chunk, &expected,
11708 "{case}: pixel idx {i} = {chunk:?}, expected {expected:?}"
11709 );
11710 }
11711 }
11712
11713 fn scenario_empty_no_bg(processor: &mut ImageProcessor, case: &str) {
11716 let mut dst = make_dirty_dst(64, 64, None);
11717 processor
11718 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::default())
11719 .unwrap_or_else(|e| panic!("{case}/decoded_masks empty+no-bg failed: {e:?}"));
11720 assert_every_pixel_eq(&dst, [0, 0, 0, 0], &format!("{case}/decoded"));
11721
11722 let mut dst = make_dirty_dst(64, 64, None);
11723 let proto = {
11724 use edgefirst_tensor::{Tensor, TensorDyn};
11725 let coeff_t = Tensor::<f32>::from_slice(&[0.0_f32; 4], &[1, 4]).unwrap();
11727 let protos_t =
11728 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
11729 ProtoData {
11730 mask_coefficients: TensorDyn::F32(coeff_t),
11731 protos: TensorDyn::F32(protos_t),
11732 layout: ProtoLayout::Nhwc,
11733 }
11734 };
11735 processor
11736 .draw_proto_masks(&mut dst, &[], &proto, MaskOverlay::default())
11737 .unwrap_or_else(|e| panic!("{case}/proto_masks empty+no-bg failed: {e:?}"));
11738 assert_every_pixel_eq(&dst, [0, 0, 0, 0], &format!("{case}/proto"));
11739 }
11740
11741 fn scenario_empty_with_bg(processor: &mut ImageProcessor, case: &str) {
11744 let bg_color = [42, 99, 200, 255];
11745 let bg = make_bg(64, 64, None, bg_color);
11746 let overlay = MaskOverlay::new().with_background(&bg);
11747
11748 let mut dst = make_dirty_dst(64, 64, None);
11749 processor
11750 .draw_decoded_masks(&mut dst, &[], &[], overlay)
11751 .unwrap_or_else(|e| panic!("{case}/decoded_masks empty+bg failed: {e:?}"));
11752 assert_every_pixel_eq(&dst, bg_color, &format!("{case}/decoded bg blit"));
11753
11754 let mut dst = make_dirty_dst(64, 64, None);
11755 let proto = {
11756 use edgefirst_tensor::{Tensor, TensorDyn};
11757 let coeff_t = Tensor::<f32>::from_slice(&[0.0_f32; 4], &[1, 4]).unwrap();
11759 let protos_t =
11760 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
11761 ProtoData {
11762 mask_coefficients: TensorDyn::F32(coeff_t),
11763 protos: TensorDyn::F32(protos_t),
11764 layout: ProtoLayout::Nhwc,
11765 }
11766 };
11767 processor
11768 .draw_proto_masks(&mut dst, &[], &proto, overlay)
11769 .unwrap_or_else(|e| panic!("{case}/proto_masks empty+bg failed: {e:?}"));
11770 assert_every_pixel_eq(&dst, bg_color, &format!("{case}/proto bg blit"));
11771 }
11772
11773 fn scenario_detect_no_bg(processor: &mut ImageProcessor, case: &str) {
11777 use edgefirst_tensor::Segmentation;
11778 use ndarray::Array3;
11779 processor
11780 .set_class_colors(&[[200, 80, 40, 255]])
11781 .expect("set_class_colors");
11782
11783 let detect = DetectBox {
11784 bbox: [0.25, 0.25, 0.75, 0.75].into(),
11785 score: 0.99,
11786 label: 0,
11787 };
11788 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
11789 let seg = Segmentation {
11790 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
11791 .unwrap()
11792 .into(),
11793 xmin: 0.25,
11794 ymin: 0.25,
11795 xmax: 0.75,
11796 ymax: 0.75,
11797 };
11798
11799 let mut dst = make_dirty_dst(64, 64, None);
11800 processor
11801 .draw_decoded_masks(&mut dst, &[detect], &[seg], MaskOverlay::default())
11802 .unwrap_or_else(|e| panic!("{case}/decoded_masks detect+no-bg failed: {e:?}"));
11803
11804 let corner = pixel_at(&dst, 2, 2);
11806 assert_eq!(
11807 corner,
11808 [0, 0, 0, 0],
11809 "{case}/decoded: corner (2,2) leaked dirty pattern: {corner:?}"
11810 );
11811 let center = pixel_at(&dst, 32, 32);
11815 assert!(
11816 center != [0, 0, 0, 0],
11817 "{case}/decoded: center (32,32) was not coloured: {center:?}"
11818 );
11819 }
11820
11821 fn scenario_detect_with_bg(processor: &mut ImageProcessor, case: &str) {
11824 use edgefirst_tensor::Segmentation;
11825 use ndarray::Array3;
11826 processor
11827 .set_class_colors(&[[200, 80, 40, 255]])
11828 .expect("set_class_colors");
11829 let bg_color = [10, 20, 30, 255];
11830 let bg = make_bg(64, 64, None, bg_color);
11831
11832 let detect = DetectBox {
11833 bbox: [0.25, 0.25, 0.75, 0.75].into(),
11834 score: 0.99,
11835 label: 0,
11836 };
11837 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
11838 let seg = Segmentation {
11839 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
11840 .unwrap()
11841 .into(),
11842 xmin: 0.25,
11843 ymin: 0.25,
11844 xmax: 0.75,
11845 ymax: 0.75,
11846 };
11847
11848 let overlay = MaskOverlay::new().with_background(&bg);
11849 let mut dst = make_dirty_dst(64, 64, None);
11850 processor
11851 .draw_decoded_masks(&mut dst, &[detect], &[seg], overlay)
11852 .unwrap_or_else(|e| panic!("{case}/decoded_masks detect+bg failed: {e:?}"));
11853
11854 let corner = pixel_at(&dst, 2, 2);
11856 assert_eq!(
11857 corner, bg_color,
11858 "{case}/decoded: corner (2,2) should show bg {bg_color:?} got {corner:?}"
11859 );
11860 let center = pixel_at(&dst, 32, 32);
11863 assert!(
11864 center != bg_color,
11865 "{case}/decoded: center (32,32) should differ from bg {bg_color:?}, got {center:?}"
11866 );
11867 }
11868
11869 fn run_all_scenarios(
11872 force_backend: Option<&'static str>,
11873 case: &'static str,
11874 require_dma_for_bg: bool,
11875 ) {
11876 if require_dma_for_bg && !edgefirst_tensor::is_dma_available() {
11877 crate::test_support::report_skip(&format!("{case} — DMA not available on this host"));
11878 return;
11879 }
11880 let processor_result = with_force_backend(force_backend, ImageProcessor::new);
11881 let mut processor = match processor_result {
11882 Ok(p) => p,
11883 Err(e) => {
11884 crate::test_support::report_skip(&format!("{case} — backend init failed: {e:?}"));
11885 return;
11886 }
11887 };
11888 scenario_empty_no_bg(&mut processor, case);
11889 scenario_empty_with_bg(&mut processor, case);
11890 scenario_detect_no_bg(&mut processor, case);
11891 scenario_detect_with_bg(&mut processor, case);
11892 }
11893
11894 #[test]
11895 fn test_draw_masks_4_scenarios_cpu() {
11896 run_all_scenarios(Some("cpu"), "cpu", false);
11897 }
11898
11899 #[test]
11900 fn test_draw_masks_4_scenarios_auto() {
11901 run_all_scenarios(None, "auto", false);
11902 }
11903
11904 #[cfg(any(
11905 target_os = "linux",
11906 target_os = "macos",
11907 target_os = "ios",
11908 target_os = "android",
11909 target_os = "windows"
11910 ))]
11911 #[cfg(feature = "opengl")]
11912 #[test]
11913 fn test_draw_masks_4_scenarios_opengl() {
11914 run_all_scenarios(Some("opengl"), "opengl", false);
11915 }
11916
11917 #[cfg(target_os = "linux")]
11922 #[test]
11923 fn test_draw_masks_zero_detection_g2d_forced() {
11924 if !edgefirst_tensor::is_dma_available() {
11925 crate::test_support::report_skip("g2d forced — DMA not available on this host");
11926 return;
11927 }
11928 let processor_result = with_force_backend(Some("g2d"), ImageProcessor::new);
11929 let mut processor = match processor_result {
11930 Ok(p) => p,
11931 Err(e) => {
11932 crate::test_support::report_skip(&format!("g2d forced — init failed: {e:?}"));
11933 return;
11934 }
11935 };
11936
11937 let mut dst = TensorDyn::image(
11939 64,
11940 64,
11941 PixelFormat::Rgba,
11942 DType::U8,
11943 Some(TensorMemory::DmaBuf),
11944 edgefirst_tensor::CpuAccess::ReadWrite,
11945 )
11946 .unwrap();
11947 {
11948 use edgefirst_tensor::TensorMapTrait;
11949 let u8t = dst.as_typed_mut::<u8>().unwrap();
11950 let mut map = u8t.map().unwrap();
11951 map.as_mut_slice().fill(0xBB);
11952 }
11953 processor
11954 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::default())
11955 .expect("g2d empty+no-bg");
11956 assert_every_pixel_eq(&dst, [0, 0, 0, 0], "g2d/case1 cleared");
11957
11958 let bg_color = [7, 11, 13, 255];
11960 let bg = {
11961 let t = TensorDyn::image(
11962 64,
11963 64,
11964 PixelFormat::Rgba,
11965 DType::U8,
11966 Some(TensorMemory::DmaBuf),
11967 edgefirst_tensor::CpuAccess::ReadWrite,
11968 )
11969 .unwrap();
11970 {
11971 use edgefirst_tensor::TensorMapTrait;
11972 let u8t = t.as_typed::<u8>().unwrap();
11973 let mut map = u8t.map().unwrap();
11974 for chunk in map.as_mut_slice().as_chunks_mut::<4>().0 {
11975 chunk.copy_from_slice(&bg_color);
11976 }
11977 }
11978 t
11979 };
11980 let mut dst = TensorDyn::image(
11981 64,
11982 64,
11983 PixelFormat::Rgba,
11984 DType::U8,
11985 Some(TensorMemory::DmaBuf),
11986 edgefirst_tensor::CpuAccess::ReadWrite,
11987 )
11988 .unwrap();
11989 {
11990 use edgefirst_tensor::TensorMapTrait;
11991 let u8t = dst.as_typed_mut::<u8>().unwrap();
11992 let mut map = u8t.map().unwrap();
11993 map.as_mut_slice().fill(0x55);
11994 }
11995 processor
11996 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::new().with_background(&bg))
11997 .expect("g2d empty+bg");
11998 assert_every_pixel_eq(&dst, bg_color, "g2d/case2 bg blit");
11999
12000 let detect = DetectBox {
12002 bbox: [0.25, 0.25, 0.75, 0.75].into(),
12003 score: 0.9,
12004 label: 0,
12005 };
12006 let mut dst = TensorDyn::image(
12007 64,
12008 64,
12009 PixelFormat::Rgba,
12010 DType::U8,
12011 Some(TensorMemory::DmaBuf),
12012 edgefirst_tensor::CpuAccess::ReadWrite,
12013 )
12014 .unwrap();
12015 let err = processor
12016 .draw_decoded_masks(&mut dst, &[detect], &[], MaskOverlay::default())
12017 .expect_err("g2d must reject detect-present draw_decoded_masks");
12018 assert!(
12019 matches!(err, Error::NotImplemented(_)),
12020 "g2d case3 wrong error: {err:?}"
12021 );
12022 }
12023
12024 #[test]
12025 fn test_set_format_then_cpu_convert() {
12026 let _lock = acquire_env_lock();
12029 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
12030 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
12031 let mut processor = ImageProcessor::new().unwrap();
12032
12033 let image = edgefirst_bench::testdata::read("zidane.jpg");
12035 let src = load_image_test_helper(&image, Some(PixelFormat::Rgba), None).unwrap();
12036
12037 let mut dst =
12039 TensorDyn::new(&[640, 640, 3], DType::U8, Some(TensorMemory::Mem), None).unwrap();
12040 dst.set_format(PixelFormat::Rgb).unwrap();
12041
12042 processor
12044 .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
12045 .unwrap();
12046
12047 assert_eq!(dst.format(), Some(PixelFormat::Rgb));
12049 assert_eq!(dst.width(), Some(640));
12050 assert_eq!(dst.height(), Some(640));
12051 }
12052
12053 #[test]
12059 fn test_multiple_image_processors_same_thread() {
12060 let _lock = acquire_env_lock();
12063 let mut processors: Vec<ImageProcessor> = (0..4)
12064 .map(|_| ImageProcessor::new().expect("ImageProcessor::new() failed"))
12065 .collect();
12066
12067 for proc in &mut processors {
12068 let src = proc
12069 .create_image(
12070 128,
12071 128,
12072 PixelFormat::Rgb,
12073 DType::U8,
12074 None,
12075 edgefirst_tensor::CpuAccess::ReadWrite,
12076 )
12077 .expect("create src failed");
12078 let mut dst = proc
12079 .create_image(
12080 64,
12081 64,
12082 PixelFormat::Rgb,
12083 DType::U8,
12084 None,
12085 edgefirst_tensor::CpuAccess::ReadWrite,
12086 )
12087 .expect("create dst failed");
12088 proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
12089 .expect("convert failed");
12090 assert_eq!(dst.width(), Some(64));
12091 assert_eq!(dst.height(), Some(64));
12092 }
12093 }
12094
12095 #[test]
12102 fn test_multiple_image_processors_separate_threads() {
12103 use std::sync::mpsc;
12104 use std::time::Duration;
12105
12106 if std::env::var_os("EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS").is_some() {
12116 crate::test_support::report_skip(
12117 "test_multiple_image_processors_separate_threads — known Vivante \
12118 GC7000UL concurrent-EGL-teardown double-free \
12119 (EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS set)",
12120 );
12121 return;
12122 }
12123
12124 const TIMEOUT: Duration = Duration::from_secs(60);
12125
12126 let _lock = acquire_env_lock();
12129
12130 let (tx, rx) = mpsc::channel::<()>();
12131
12132 std::thread::spawn(move || {
12133 let handles: Vec<_> = (0..4)
12134 .map(|i| {
12135 std::thread::spawn(move || {
12136 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
12137 panic!("ImageProcessor::new() failed on thread {i}: {e}")
12138 });
12139 let src = proc
12140 .create_image(
12141 128,
12142 128,
12143 PixelFormat::Rgb,
12144 DType::U8,
12145 None,
12146 edgefirst_tensor::CpuAccess::ReadWrite,
12147 )
12148 .unwrap_or_else(|e| panic!("create src failed on thread {i}: {e}"));
12149 let mut dst = proc
12150 .create_image(
12151 64,
12152 64,
12153 PixelFormat::Rgb,
12154 DType::U8,
12155 None,
12156 edgefirst_tensor::CpuAccess::ReadWrite,
12157 )
12158 .unwrap_or_else(|e| panic!("create dst failed on thread {i}: {e}"));
12159 proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
12160 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
12161 assert_eq!(dst.width(), Some(64));
12162 assert_eq!(dst.height(), Some(64));
12163 })
12164 })
12165 .collect();
12166
12167 for (i, h) in handles.into_iter().enumerate() {
12168 h.join()
12169 .unwrap_or_else(|e| panic!("thread {i} panicked: {e:?}"));
12170 }
12171
12172 let _ = tx.send(());
12173 });
12174
12175 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
12176 panic!("test_multiple_image_processors_separate_threads timed out after {TIMEOUT:?}")
12177 });
12178 }
12179
12180 #[test]
12187 fn test_image_processors_concurrent_operations() {
12188 use std::sync::{mpsc, Arc, Barrier};
12189 use std::time::Duration;
12190
12191 const N: usize = 4;
12192 const ROUNDS: usize = 10;
12193 const TIMEOUT: Duration = Duration::from_secs(60);
12194
12195 let _lock = acquire_env_lock();
12198
12199 let (tx, rx) = mpsc::channel::<()>();
12200
12201 std::thread::spawn(move || {
12202 let barrier = Arc::new(Barrier::new(N));
12203
12204 let handles: Vec<_> = (0..N)
12205 .map(|i| {
12206 let barrier = Arc::clone(&barrier);
12207 std::thread::spawn(move || {
12208 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
12209 panic!("ImageProcessor::new() failed on thread {i}: {e}")
12210 });
12211
12212 barrier.wait();
12214
12215 for round in 0..ROUNDS {
12217 let src = proc
12218 .create_image(
12219 128,
12220 128,
12221 PixelFormat::Rgb,
12222 DType::U8,
12223 None,
12224 edgefirst_tensor::CpuAccess::ReadWrite,
12225 )
12226 .unwrap_or_else(|e| {
12227 panic!("create src failed on thread {i} round {round}: {e}")
12228 });
12229 let mut dst = proc
12230 .create_image(
12231 64,
12232 64,
12233 PixelFormat::Rgb,
12234 DType::U8,
12235 None,
12236 edgefirst_tensor::CpuAccess::ReadWrite,
12237 )
12238 .unwrap_or_else(|e| {
12239 panic!("create dst failed on thread {i} round {round}: {e}")
12240 });
12241 proc.convert(
12242 &src,
12243 &mut dst,
12244 Rotation::None,
12245 Flip::None,
12246 Crop::default(),
12247 )
12248 .unwrap_or_else(|e| {
12249 panic!("convert failed on thread {i} round {round}: {e}")
12250 });
12251 assert_eq!(dst.width(), Some(64));
12252 assert_eq!(dst.height(), Some(64));
12253 }
12254 })
12255 })
12256 .collect();
12257
12258 for (i, h) in handles.into_iter().enumerate() {
12259 h.join()
12260 .unwrap_or_else(|e| panic!("thread {i} panicked: {e:?}"));
12261 }
12262
12263 let _ = tx.send(());
12264 });
12265
12266 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
12267 panic!("test_image_processors_concurrent_operations timed out after {TIMEOUT:?}")
12268 });
12269 }
12270
12271 #[test]
12289 fn test_parallel_processors_unique_outputs() {
12290 use std::sync::{mpsc, Arc, Barrier};
12291 use std::time::Duration;
12292
12293 const N: usize = 4;
12294 const ROUNDS: usize = 25;
12295 const TIMEOUT: Duration = Duration::from_secs(60);
12296
12297 if std::env::var_os("EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS").is_some() {
12298 crate::test_support::report_skip(
12299 "test_parallel_processors_unique_outputs — known Vivante \
12300 GC7000UL concurrent-multi-processor driver abort \
12301 (EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS set)",
12302 );
12303 return;
12304 }
12305
12306 let _lock = acquire_env_lock();
12307 let (tx, rx) = mpsc::channel::<()>();
12308
12309 std::thread::spawn(move || {
12310 let barrier = Arc::new(Barrier::new(N));
12311 let handles: Vec<_> = (0..N)
12312 .map(|i| {
12313 let barrier = Arc::clone(&barrier);
12314 std::thread::spawn(move || {
12315 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
12316 panic!("ImageProcessor::new() failed on thread {i}: {e}")
12317 });
12318 let (w, h) = (640usize, 480usize);
12320 let mem = if edgefirst_tensor::is_dma_available() {
12321 Some(TensorMemory::DmaBuf)
12322 } else {
12323 Some(TensorMemory::Mem)
12324 };
12325 let src = proc
12326 .create_image(
12327 w,
12328 h,
12329 PixelFormat::Nv12,
12330 DType::U8,
12331 mem,
12332 edgefirst_tensor::CpuAccess::ReadWrite,
12333 )
12334 .unwrap();
12335 {
12336 let t = src.as_typed::<u8>().unwrap();
12337 let mut m = t.map().unwrap();
12338 let s = m.as_mut_slice();
12339 for (j, b) in s[..w * h].iter_mut().enumerate() {
12340 *b = ((i * 53 + j) % 200 + 16) as u8;
12341 }
12342 for b in &mut s[w * h..] {
12343 *b = (80 + i * 24) as u8;
12344 }
12345 }
12346 let lb = Crop::letterbox([114, 114, 114, 255]);
12347 let convert_once = |proc: &mut ImageProcessor| -> Vec<u8> {
12348 let mut dst = proc
12349 .create_image(
12350 320,
12351 320,
12352 PixelFormat::Rgba,
12353 DType::U8,
12354 mem,
12355 edgefirst_tensor::CpuAccess::ReadWrite,
12356 )
12357 .unwrap();
12358 proc.convert(&src, &mut dst, Rotation::None, Flip::None, lb)
12359 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
12360 let t = dst.as_typed::<u8>().unwrap();
12361 let m = t.map().unwrap();
12362 m.as_slice().to_vec()
12363 };
12364
12365 let oracle = convert_once(&mut proc);
12366 barrier.wait();
12367 for round in 0..ROUNDS {
12368 let out = convert_once(&mut proc);
12369 let diffs = oracle.iter().zip(&out).filter(|(a, b)| a != b).count();
12370 assert!(
12371 diffs == 0,
12372 "thread {i} round {round}: {diffs}/{} bytes diverged \
12373 from this processor's own oracle — cross-processor \
12374 GL state leakage under parallel execution",
12375 oracle.len()
12376 );
12377 }
12378 })
12379 })
12380 .collect();
12381
12382 for (i, h) in handles.into_iter().enumerate() {
12383 h.join()
12384 .unwrap_or_else(|e| panic!("parallel thread {i} panicked: {e:?}"));
12385 }
12386 let _ = tx.send(());
12387 });
12388
12389 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
12390 panic!("test_parallel_processors_unique_outputs timed out after {TIMEOUT:?}")
12391 });
12392 }
12393
12394 #[test]
12403 #[ignore = "heavy on-demand GL-parallelism stressor; run explicitly on boards"]
12404 fn stress_parallel_processors_oracle() {
12405 use std::sync::{mpsc, Arc, Barrier};
12406 use std::time::Duration;
12407
12408 const N: usize = 4;
12409 const ROUNDS: usize = 200;
12410 const TIMEOUT: Duration = Duration::from_secs(600);
12411
12412 let _lock = acquire_env_lock();
12413 let (tx, rx) = mpsc::channel::<()>();
12414
12415 std::thread::spawn(move || {
12416 let barrier = Arc::new(Barrier::new(N));
12417 let handles: Vec<_> = (0..N)
12418 .map(|i| {
12419 let barrier = Arc::clone(&barrier);
12420 std::thread::spawn(move || {
12421 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
12422 panic!("ImageProcessor::new() failed on thread {i}: {e}")
12423 });
12424 let (w, h) = (1280usize, 720usize);
12425 let mem = if edgefirst_tensor::is_dma_available() {
12426 Some(TensorMemory::DmaBuf)
12427 } else {
12428 Some(TensorMemory::Mem)
12429 };
12430
12431 let src = proc
12434 .create_image(
12435 w,
12436 h,
12437 PixelFormat::Nv12,
12438 DType::U8,
12439 mem,
12440 edgefirst_tensor::CpuAccess::ReadWrite,
12441 )
12442 .unwrap();
12443 {
12444 let t = src.as_typed::<u8>().unwrap();
12445 let mut m = t.map().unwrap();
12446 let s = m.as_mut_slice();
12447 for (j, b) in s[..w * h].iter_mut().enumerate() {
12448 *b = ((i * 37 + j) % 200 + 16) as u8;
12449 }
12450 for b in &mut s[w * h..] {
12451 *b = (96 + i * 16) as u8;
12452 }
12453 }
12454 let lb = Crop::letterbox([114, 114, 114, 255]);
12455
12456 let convert_once = |proc: &mut ImageProcessor| -> Vec<u8> {
12457 let mut dst = proc
12458 .create_image(
12459 640,
12460 640,
12461 PixelFormat::Rgb,
12462 DType::U8,
12463 mem,
12464 edgefirst_tensor::CpuAccess::ReadWrite,
12465 )
12466 .unwrap();
12467 proc.convert(&src, &mut dst, Rotation::None, Flip::None, lb)
12468 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
12469 let t = dst.as_typed::<u8>().unwrap();
12470 let m = t.map().unwrap();
12471 m.as_slice().to_vec()
12472 };
12473
12474 let oracle = convert_once(&mut proc);
12475 barrier.wait();
12476 for round in 0..ROUNDS {
12477 let out = convert_once(&mut proc);
12478 let diffs = oracle.iter().zip(&out).filter(|(a, b)| a != b).count();
12479 assert!(
12480 diffs == 0,
12481 "thread {i} round {round}: {diffs}/{} bytes diverged \
12482 from the pre-barrier oracle",
12483 oracle.len()
12484 );
12485 }
12486 })
12487 })
12488 .collect();
12489
12490 for (i, h) in handles.into_iter().enumerate() {
12491 h.join()
12492 .unwrap_or_else(|e| panic!("stressor thread {i} panicked: {e:?}"));
12493 }
12494 let _ = tx.send(());
12495 });
12496
12497 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
12498 panic!("stress_parallel_processors_oracle timed out after {TIMEOUT:?}")
12499 });
12500 }
12501
12502 #[test]
12515 fn convert_f32_auto_never_errors_non_gl_combo() {
12516 const W: usize = 64;
12517 const H: usize = 64;
12518
12519 let src = TensorDyn::image(
12522 W,
12523 H,
12524 PixelFormat::Yuyv,
12525 DType::U8,
12526 Some(TensorMemory::Mem),
12527 edgefirst_tensor::CpuAccess::ReadWrite,
12528 )
12529 .unwrap();
12530 {
12531 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
12532 let data = map.as_mut_slice();
12533 for chunk in data.as_chunks_mut::<4>().0 {
12534 chunk[0] = 128; chunk[1] = 128; chunk[2] = 160; chunk[3] = 128; }
12539 }
12540
12541 let mut dst = TensorDyn::image(
12542 W,
12543 H,
12544 PixelFormat::Rgb,
12545 DType::F32,
12546 Some(TensorMemory::Mem),
12547 edgefirst_tensor::CpuAccess::ReadWrite,
12548 )
12549 .unwrap();
12550
12551 let mut proc = ImageProcessor::new().unwrap();
12552 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
12553 assert!(
12554 result.is_ok(),
12555 "auto-chain Yuyv→Rgb F32 must not error: {:?}",
12556 result.err()
12557 );
12558
12559 let map = dst.as_typed::<f32>().unwrap().map().unwrap();
12561 let floats = map.as_slice();
12562 assert_eq!(floats.len(), W * H * 3, "unexpected output element count");
12563 for (i, &v) in floats.iter().enumerate() {
12564 assert!(
12565 v.is_finite() && (0.0..=1.0).contains(&v),
12566 "output[{i}]={v} is not finite or not in [0,1]"
12567 );
12568 }
12569
12570 let first_non_zero = floats.iter().find(|&&v| v > 0.01);
12574 assert!(
12575 first_non_zero.is_some(),
12576 "all-zero output detected — CPU path likely did not write to the destination buffer"
12577 );
12578 let r0 = floats[0];
12581 assert!(
12582 (r0 - 0.502_f32).abs() < 0.05,
12583 "first pixel R={r0} expected ≈0.502 (Y=128 neutral grey from YUYV source)"
12584 );
12585 }
12586
12587 #[test]
12593 #[allow(clippy::needless_update)]
12599 fn convert_f16_forced_cpu_correct() {
12600 const W: usize = 16;
12601 const H: usize = 16;
12602 const TOL: f32 = 1.0 / 512.0; let src = TensorDyn::image(
12606 W,
12607 H,
12608 PixelFormat::Rgba,
12609 DType::U8,
12610 Some(TensorMemory::Mem),
12611 edgefirst_tensor::CpuAccess::ReadWrite,
12612 )
12613 .unwrap();
12614 {
12615 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
12616 let data = map.as_mut_slice();
12617 for y in 0..H {
12618 for x in 0..W {
12619 let i = y * W + x;
12620 data[i * 4] = (50 + x) as u8; data[i * 4 + 1] = (100 + y * 8) as u8; data[i * 4 + 2] = 200; data[i * 4 + 3] = 255;
12624 }
12625 }
12626 }
12627
12628 let mut dst = TensorDyn::image(
12629 W,
12630 H,
12631 PixelFormat::PlanarRgb,
12632 DType::F16,
12633 Some(TensorMemory::Mem),
12634 edgefirst_tensor::CpuAccess::ReadWrite,
12635 )
12636 .unwrap();
12637
12638 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
12639 backend: ComputeBackend::Cpu,
12640 ..Default::default()
12641 })
12642 .unwrap();
12643 proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
12644 .expect("forced-CPU Rgba→PlanarRgb F16 must not error");
12645
12646 let src_map = src.as_typed::<u8>().unwrap().map().unwrap();
12647 let src_bytes = src_map.as_slice();
12648 let dst_map = dst.as_typed::<half::f16>().unwrap().map().unwrap();
12649 let dst_halfs = dst_map.as_slice();
12650
12651 let plane = W * H;
12652 assert_eq!(dst_halfs.len(), plane * 3, "wrong output element count");
12653
12654 for y in 0..H {
12655 for x in 0..W {
12656 let i = y * W + x;
12657 let r_exp = src_bytes[i * 4] as f32 / 255.0;
12658 let g_exp = src_bytes[i * 4 + 1] as f32 / 255.0;
12659 let b_exp = src_bytes[i * 4 + 2] as f32 / 255.0;
12660
12661 let r_got = dst_halfs[i].to_f32();
12662 let g_got = dst_halfs[plane + i].to_f32();
12663 let b_got = dst_halfs[2 * plane + i].to_f32();
12664
12665 assert!(
12666 (r_got - r_exp).abs() <= TOL,
12667 "R plane ({x},{y}): got {r_got}, expected {r_exp}"
12668 );
12669 assert!(
12670 (g_got - g_exp).abs() <= TOL,
12671 "G plane ({x},{y}): got {g_got}, expected {g_exp}"
12672 );
12673 assert!(
12674 (b_got - b_exp).abs() <= TOL,
12675 "B plane ({x},{y}): got {b_got}, expected {b_exp}"
12676 );
12677
12678 if src_bytes[i * 4] != src_bytes[i * 4 + 1] {
12680 assert_ne!(r_got, g_got, "R and G planes must differ at ({x},{y})");
12681 }
12682 }
12683 }
12684 }
12685
12686 #[test]
12694 fn convert_f32_with_rotation_falls_back() {
12695 const W: usize = 16;
12696 const H: usize = 16;
12697
12698 let src = TensorDyn::image(
12700 W,
12701 H,
12702 PixelFormat::Rgba,
12703 DType::U8,
12704 Some(TensorMemory::Mem),
12705 edgefirst_tensor::CpuAccess::ReadWrite,
12706 )
12707 .unwrap();
12708 {
12709 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
12710 let data = map.as_mut_slice();
12711 for y in 0..H {
12712 for x in 0..W {
12713 let i = y * W + x;
12714 data[i * 4] = (x * 16) as u8; data[i * 4 + 1] = (y * 16) as u8; data[i * 4 + 2] = 128; data[i * 4 + 3] = 255;
12718 }
12719 }
12720 }
12721
12722 let mut dst = TensorDyn::image(
12724 H, W, PixelFormat::Rgb,
12727 DType::F32,
12728 Some(TensorMemory::Mem),
12729 edgefirst_tensor::CpuAccess::ReadWrite,
12730 )
12731 .unwrap();
12732
12733 let mut proc = ImageProcessor::new().unwrap();
12734 let result = proc.convert(
12735 &src,
12736 &mut dst,
12737 Rotation::Clockwise90,
12738 Flip::None,
12739 Crop::default(),
12740 );
12741 assert!(
12742 result.is_ok(),
12743 "auto-chain Rgba→Rgb F32 with Rot90 must not error: {:?}",
12744 result.err()
12745 );
12746
12747 let map = dst.as_typed::<f32>().unwrap().map().unwrap();
12748 let floats = map.as_slice();
12749 assert_eq!(floats.len(), H * W * 3, "unexpected output element count");
12750 for (i, &v) in floats.iter().enumerate() {
12751 assert!(
12752 v.is_finite() && (0.0..=1.0).contains(&v),
12753 "output[{i}]={v} is not finite or not in [0,1]"
12754 );
12755 }
12756 }
12757
12758 #[test]
12784 #[cfg(all(
12785 any(
12786 target_os = "linux",
12787 target_os = "macos",
12788 target_os = "ios",
12789 target_os = "android",
12790 target_os = "windows"
12791 ),
12792 feature = "opengl"
12793 ))]
12794 fn convert_f16_gl_cpu_parity_identity() {
12795 #[cfg(target_os = "macos")]
12801 if std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1")
12802 && std::env::var_os("HAL_TEST_ALLOW_DLOPEN_ANGLE").is_none()
12803 {
12804 crate::test_support::report_skip(
12805 "convert_f16_gl_cpu_parity_identity - ANGLE dlopen gate closed (coverage pass 1)",
12806 );
12807 return;
12808 }
12809 if !is_opengl_available() {
12810 crate::test_support::report_skip(
12811 "convert_f16_gl_cpu_parity_identity - OpenGL not available",
12812 );
12813 return;
12814 }
12815
12816 const W: usize = 16;
12817 const H: usize = 16;
12818 const TOL: f32 = 1.0 / 256.0; let src = TensorDyn::image(
12822 W,
12823 H,
12824 PixelFormat::Rgba,
12825 DType::U8,
12826 Some(TensorMemory::Mem),
12827 edgefirst_tensor::CpuAccess::ReadWrite,
12828 )
12829 .unwrap();
12830 {
12831 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
12832 let data = map.as_mut_slice();
12833 for y in 0..H {
12834 for x in 0..W {
12835 let i = y * W + x;
12836 data[i * 4] = (40 + x) as u8; data[i * 4 + 1] = (80 + y * 10) as u8; data[i * 4 + 2] = 180; data[i * 4 + 3] = 255;
12840 }
12841 }
12842 }
12843
12844 let gl_result = {
12846 let mut gl_proc = match ImageProcessor::with_config(ImageProcessorConfig {
12847 backend: ComputeBackend::OpenGl,
12848 ..Default::default()
12849 }) {
12850 Ok(p) => p,
12851 Err(e) => {
12852 crate::test_support::report_skip(&format!(
12853 "convert_f16_gl_cpu_parity_identity - GL backend unavailable: {e}"
12854 ));
12855 return;
12856 }
12857 };
12858
12859 if !gl_proc.supported_render_dtypes().f16 {
12860 crate::test_support::report_skip(
12861 "convert_f16_gl_cpu_parity_identity - F16 render not supported",
12862 );
12863 return;
12864 }
12865
12866 let mut dst = match TensorDyn::image(
12886 W,
12887 H,
12888 PixelFormat::PlanarRgb,
12889 DType::F16,
12890 Some(TensorMemory::DmaBuf),
12891 edgefirst_tensor::CpuAccess::ReadWrite,
12892 ) {
12893 Ok(t) if t.memory() == TensorMemory::DmaBuf => t,
12894 Ok(t) => {
12899 crate::test_support::report_skip(&format!(
12900 "convert_f16_gl_cpu_parity_identity - the zero-copy F16 \
12901 destination fell back to {:?}",
12902 t.memory()
12903 ));
12904 return;
12905 }
12906 Err(e) => {
12907 crate::test_support::report_skip(&format!(
12908 "convert_f16_gl_cpu_parity_identity - no zero-copy F16 \
12909 destination on this host: {e}"
12910 ));
12911 return;
12912 }
12913 };
12914 let require_gl = std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1");
12915 let fallbacks_before = gl_proc.convert_fallback_count();
12916 match gl_proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default()) {
12917 Ok(()) => {
12918 if gl_proc.convert_fallback_count() != fallbacks_before {
12919 assert!(
12920 !require_gl,
12921 "HAL_TEST_REQUIRE_GL=1 but \
12922 convert_f16_gl_cpu_parity_identity would have skipped \
12923 while reporting success: the GL F16 path declined and \
12924 the CPU served the convert"
12925 );
12926 crate::test_support::report_skip(
12927 "convert_f16_gl_cpu_parity_identity - the GL F16 path \
12928 declined and the CPU served the convert; comparing that \
12929 against the forced-CPU result would prove nothing",
12930 );
12931 return;
12932 }
12933 dst
12934 }
12935 Err(e) => {
12936 crate::test_support::report_skip(&format!(
12937 "convert_f16_gl_cpu_parity_identity - GL convert failed: {e}"
12938 ));
12939 return;
12940 }
12941 }
12942 };
12943
12944 let cpu_result = {
12946 let mut cpu_proc = ImageProcessor::with_config(ImageProcessorConfig {
12947 backend: ComputeBackend::Cpu,
12948 ..Default::default()
12949 })
12950 .unwrap();
12951 let mut dst = TensorDyn::image(
12952 W,
12953 H,
12954 PixelFormat::PlanarRgb,
12955 DType::F16,
12956 Some(TensorMemory::Mem),
12957 edgefirst_tensor::CpuAccess::ReadWrite,
12958 )
12959 .unwrap();
12960 cpu_proc
12961 .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
12962 .expect("forced-CPU Rgba→PlanarRgb F16 must not error");
12963 dst
12964 };
12965
12966 let flat_f16 = |t: &TensorDyn| -> Vec<half::f16> {
12971 let typed = t.as_typed::<half::f16>().expect("F16 image");
12972 let mut bytes = vec![0u8; typed.shape().iter().product::<usize>() * 2];
12973 typed
12974 .copy_to_flat(&mut bytes)
12975 .expect("compact any padded rows");
12976 bytes
12977 .as_chunks::<2>()
12978 .0
12979 .iter()
12980 .map(|&c| half::f16::from_le_bytes(c))
12981 .collect()
12982 };
12983 let gl_halfs = flat_f16(&gl_result);
12984 let cpu_halfs = flat_f16(&cpu_result);
12985
12986 assert_eq!(
12987 gl_halfs.len(),
12988 cpu_halfs.len(),
12989 "GL and CPU output sizes differ"
12990 );
12991
12992 let plane = W * H;
12993 let channel_names = ["R", "G", "B"];
12994 for (idx, (gl_h, cpu_h)) in gl_halfs.iter().zip(cpu_halfs.iter()).enumerate() {
12995 let gl_v = gl_h.to_f32();
12996 let cpu_v = cpu_h.to_f32();
12997 let err = (gl_v - cpu_v).abs();
12998 let ch = channel_names[idx / plane];
12999 let pixel = idx % plane;
13000 assert!(
13001 err <= TOL,
13002 "GL vs CPU mismatch at {ch}[{pixel}]: GL={gl_v}, CPU={cpu_v}, err={err} > tol={TOL}"
13003 );
13004 }
13005 }
13006
13007 #[test]
13015 #[cfg(all(any(target_os = "linux", target_os = "windows"), feature = "opengl"))]
13016 fn supported_render_dtypes_linux_smoke() {
13017 let proc = match ImageProcessor::new() {
13018 Ok(p) => p,
13019 Err(e) => {
13020 crate::test_support::report_skip(&format!(
13021 "supported_render_dtypes_linux_smoke — ImageProcessor::new() failed: {e}"
13022 ));
13023 return;
13024 }
13025 };
13026 if proc.opengl.is_none() {
13027 crate::test_support::report_skip(
13028 "supported_render_dtypes_linux_smoke — no GL backend on this host",
13029 );
13030 return;
13031 }
13032 let support = proc.supported_render_dtypes();
13034 eprintln!(
13035 "supported_render_dtypes_linux_smoke: f16={} f32={}",
13036 support.f16, support.f32
13037 );
13038 }
13040
13041 #[test]
13050 fn convert_f16_pbo_non_4_aligned_width_falls_back() {
13051 const W: usize = 18; const H: usize = 16;
13053
13054 let src = TensorDyn::image(
13056 W,
13057 H,
13058 PixelFormat::Rgba,
13059 DType::U8,
13060 Some(TensorMemory::Mem),
13061 edgefirst_tensor::CpuAccess::ReadWrite,
13062 )
13063 .unwrap();
13064 {
13065 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13066 let data = map.as_mut_slice();
13067 for chunk in data.as_chunks_mut::<4>().0 {
13068 chunk[0] = 128;
13069 chunk[1] = 64;
13070 chunk[2] = 200;
13071 chunk[3] = 255;
13072 }
13073 }
13074
13075 let mut dst = TensorDyn::image(
13078 W,
13079 H,
13080 PixelFormat::PlanarRgb,
13081 DType::F16,
13082 Some(TensorMemory::Mem),
13083 edgefirst_tensor::CpuAccess::ReadWrite,
13084 )
13085 .unwrap();
13086
13087 let mut proc = ImageProcessor::new().unwrap();
13090 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
13091 assert!(
13092 result.is_ok(),
13093 "auto-chain PlanarRgb F16 W%4!=0 must not error (CPU fallback): {:?}",
13094 result.err()
13095 );
13096
13097 let map = dst.as_typed::<half::f16>().unwrap().map().unwrap();
13099 let halfs = map.as_slice();
13100 assert_eq!(halfs.len(), W * H * 3, "unexpected element count");
13101 for (i, h) in halfs.iter().enumerate() {
13102 let v = h.to_f32();
13103 assert!(
13104 v.is_finite() && (0.0..=1.0).contains(&v),
13105 "output[{i}]={v} is not finite or not in [0,1]"
13106 );
13107 }
13108 }
13109
13110 #[test]
13120 #[allow(clippy::needless_update)]
13123 fn convert_nv12_to_rgb_f32_cpu() {
13124 const W: usize = 16;
13125 const H: usize = 16; let src = TensorDyn::image(
13129 W,
13130 H,
13131 PixelFormat::Nv12,
13132 DType::U8,
13133 Some(TensorMemory::Mem),
13134 edgefirst_tensor::CpuAccess::ReadWrite,
13135 )
13136 .unwrap();
13137 {
13138 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13139 map.as_mut_slice().fill(128); }
13141
13142 let mut dst = TensorDyn::image(
13143 W,
13144 H,
13145 PixelFormat::Rgb,
13146 DType::F32,
13147 Some(TensorMemory::Mem),
13148 edgefirst_tensor::CpuAccess::ReadWrite,
13149 )
13150 .unwrap();
13151
13152 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
13153 backend: ComputeBackend::Cpu,
13154 ..Default::default()
13155 })
13156 .unwrap();
13157
13158 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
13159 assert!(
13160 result.is_ok(),
13161 "forced-CPU NV12→Rgb F32 must not error: {:?}",
13162 result.err()
13163 );
13164
13165 let map = dst.as_typed::<f32>().unwrap().map().unwrap();
13166 let floats = map.as_slice();
13167 assert_eq!(floats.len(), W * H * 3, "unexpected element count");
13168 for (i, &v) in floats.iter().enumerate() {
13169 assert!(
13170 v.is_finite() && (0.0..=1.0).contains(&v),
13171 "output[{i}]={v} is not finite or not in [0,1]"
13172 );
13173 }
13174 let non_zero = floats.iter().any(|&v| v > 0.01);
13176 assert!(non_zero, "all-zero output from NV12→Rgb F32 CPU path");
13177 }
13178
13179 #[test]
13183 #[allow(clippy::needless_update)]
13186 fn convert_nv12_to_planar_rgb_f16_cpu() {
13187 const W: usize = 16;
13188 const H: usize = 16;
13189
13190 let src = TensorDyn::image(
13191 W,
13192 H,
13193 PixelFormat::Nv12,
13194 DType::U8,
13195 Some(TensorMemory::Mem),
13196 edgefirst_tensor::CpuAccess::ReadWrite,
13197 )
13198 .unwrap();
13199 {
13200 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13201 map.as_mut_slice().fill(128);
13202 }
13203
13204 let mut dst = TensorDyn::image(
13205 W,
13206 H,
13207 PixelFormat::PlanarRgb,
13208 DType::F16,
13209 Some(TensorMemory::Mem),
13210 edgefirst_tensor::CpuAccess::ReadWrite,
13211 )
13212 .unwrap();
13213
13214 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
13215 backend: ComputeBackend::Cpu,
13216 ..Default::default()
13217 })
13218 .unwrap();
13219
13220 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
13221 assert!(
13222 result.is_ok(),
13223 "forced-CPU NV12→PlanarRgb F16 must not error: {:?}",
13224 result.err()
13225 );
13226
13227 let map = dst.as_typed::<half::f16>().unwrap().map().unwrap();
13228 let halfs = map.as_slice();
13229 assert_eq!(halfs.len(), W * H * 3, "unexpected element count");
13230 for (i, h) in halfs.iter().enumerate() {
13231 let v = h.to_f32();
13232 assert!(
13233 v.is_finite() && (0.0..=1.0).contains(&v),
13234 "output[{i}]={v} is not finite or not in [0,1]"
13235 );
13236 }
13237 let non_zero = halfs.iter().any(|h| h.to_f32() > 0.01);
13238 assert!(non_zero, "all-zero output from NV12→PlanarRgb F16 CPU path");
13239 }
13240
13241 #[test]
13250 fn create_image_desc_negotiates_and_counts_fallbacks() {
13251 use edgefirst_tensor::{CpuAccess, ImageDesc};
13252 let proc = ImageProcessor::new().unwrap();
13253
13254 let desc =
13255 ImageDesc::new(64, 64, PixelFormat::Rgba, DType::U8).with_access(CpuAccess::ReadWrite);
13256 let plain = proc.create_image_desc(&desc).unwrap();
13257 let classic = proc
13258 .create_image(
13259 64,
13260 64,
13261 PixelFormat::Rgba,
13262 DType::U8,
13263 None,
13264 CpuAccess::ReadWrite,
13265 )
13266 .unwrap();
13267 assert_eq!(plain.memory(), classic.memory(), "same negotiation path");
13268 assert_eq!(plain.compression(), None);
13269
13270 #[cfg(not(target_os = "android"))]
13271 {
13272 use edgefirst_tensor::Compression;
13273 let before = proc.compression_fallback_count();
13274 let desc = ImageDesc::new(64, 64, PixelFormat::Rgba, DType::U8)
13275 .with_compression(Compression::Any);
13276 let t = proc.create_image_desc(&desc).unwrap();
13277 assert_eq!(t.compression(), None, "no vendor tile scheme off-Android");
13278 assert!(
13279 proc.compression_fallback_count() > before,
13280 "Any resolving linear must count"
13281 );
13282 }
13283 }
13284
13285 #[test]
13288 #[cfg(target_os = "linux")]
13289 fn create_image_f32_dma_rejected() {
13290 let proc = ImageProcessor::new().unwrap();
13291 let result = proc.create_image(
13292 64,
13293 64,
13294 PixelFormat::Rgb,
13295 DType::F32,
13296 Some(TensorMemory::DmaBuf),
13297 edgefirst_tensor::CpuAccess::ReadWrite,
13298 );
13299 assert!(
13300 result.is_err(),
13301 "create_image(F32, Dma) must fail — no DRM fourcc for f32"
13302 );
13303 }
13304
13305 #[test]
13314 #[cfg(target_os = "linux")]
13315 fn import_image_carries_colorimetry() {
13316 use edgefirst_tensor::{ColorEncoding, ColorRange, Colorimetry, TensorMemory};
13317
13318 let expected = Colorimetry::default()
13319 .with_encoding(ColorEncoding::Bt709)
13320 .with_range(ColorRange::Limited);
13321
13322 if !is_dma_available() {
13323 let mut t = TensorDyn::image(
13326 8,
13327 8,
13328 PixelFormat::Rgba,
13329 DType::U8,
13330 Some(TensorMemory::Mem),
13331 edgefirst_tensor::CpuAccess::ReadWrite,
13332 )
13333 .expect("alloc");
13334 assert_eq!(t.colorimetry(), None, "colorimetry must start as None");
13335 t.set_colorimetry(Some(expected));
13336 assert_eq!(
13337 t.colorimetry(),
13338 Some(expected),
13339 "set_colorimetry must round-trip"
13340 );
13341 crate::test_support::report_skip("import_image_carries_colorimetry (DMA unavailable); storage contract verified via TensorDyn");
13342 return;
13343 }
13344
13345 use edgefirst_tensor::{PlaneDescriptor, Tensor};
13348
13349 let rgba_bytes = 64 * 64 * 4; let dma_tensor = Tensor::<u8>::new(
13351 &[rgba_bytes],
13352 Some(TensorMemory::DmaBuf),
13353 Some("import_test"),
13354 )
13355 .expect("dma alloc");
13356 let pd =
13357 PlaneDescriptor::new(dma_tensor.dmabuf().expect("dma fd")).expect("PlaneDescriptor");
13358
13359 let proc = ImageProcessor::new().expect("ImageProcessor");
13360 let result = proc.import_image(
13361 pd,
13362 None,
13363 64,
13364 64,
13365 PixelFormat::Rgba,
13366 DType::U8,
13367 Some(expected),
13368 );
13369 let tensor = result.expect("import_image must succeed on DMA fd");
13370 assert_eq!(
13371 tensor.colorimetry(),
13372 Some(expected),
13373 "import_image must store the supplied colorimetry on the returned TensorDyn"
13374 );
13375 }
13376}