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 mut 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 if let Some(s) = chroma_stride {
2535 if s < width {
2536 return Err(Error::InvalidShape(format!(
2537 "chroma stride {s} < minimum {width} for {format:?}"
2538 )));
2539 }
2540 chroma_tensor.set_row_stride_unchecked(s);
2541 }
2542 if let Some(o) = chroma_offset {
2543 chroma_tensor.set_plane_offset(o);
2544 }
2545
2546 let mut tensor = Tensor::<u8>::from_planes(luma, chroma_tensor, format)?;
2549
2550 if let Some(s) = image_stride {
2552 tensor.set_row_stride(s)?;
2553 }
2554 if let Some(o) = image_offset {
2555 tensor.set_plane_offset(o);
2556 }
2557
2558 if dtype == DType::I8 {
2559 const {
2563 assert!(std::mem::size_of::<Tensor<u8>>() == std::mem::size_of::<Tensor<i8>>());
2564 assert!(
2565 std::mem::align_of::<Tensor<u8>>() == std::mem::align_of::<Tensor<i8>>()
2566 );
2567 }
2568 let tensor_i8: Tensor<i8> = unsafe { std::mem::transmute(tensor) };
2569 let mut dyn_tensor = TensorDyn::from(tensor_i8);
2570 dyn_tensor.set_colorimetry(colorimetry);
2571 return Ok(dyn_tensor);
2572 }
2573 let mut dyn_tensor = TensorDyn::from(tensor);
2574 dyn_tensor.set_colorimetry(colorimetry);
2575 Ok(dyn_tensor)
2576 } else {
2577 let shape = format.allocation_shape(width, height).ok_or_else(|| {
2582 Error::NotSupported(format!(
2583 "unsupported pixel format for import_image: {format:?}"
2584 ))
2585 })?;
2586 let tensor = TensorDyn::from_fd(image.into_fd(), &shape, dtype, None)?;
2587 if tensor.memory() != TensorMemory::DmaBuf {
2588 return Err(Error::NotSupported(format!(
2589 "import_image requires DMA-backed fd, got {:?}",
2590 tensor.memory()
2591 )));
2592 }
2593 let mut tensor = tensor.with_format(format)?;
2594 if let Some(s) = image_stride {
2595 tensor.set_row_stride(s)?;
2596 }
2597 if let Some(o) = image_offset {
2598 tensor.set_plane_offset(o);
2599 }
2600 tensor.set_colorimetry(colorimetry);
2601 Ok(tensor)
2602 }
2603 }
2604
2605 #[cfg(feature = "decode")]
2630 pub fn draw_masks(
2631 &mut self,
2632 decoder: &edgefirst_decoder::Decoder,
2633 outputs: &[&TensorDyn],
2634 dst: &mut TensorDyn,
2635 overlay: MaskOverlay<'_>,
2636 ) -> Result<Vec<DetectBox>> {
2637 let mut output_boxes = Vec::with_capacity(100);
2638
2639 let proto_result = decoder
2641 .decode_proto(outputs, &mut output_boxes)
2642 .map_err(|e| Error::Internal(format!("decode_proto: {e:#?}")))?;
2643
2644 if let Some(proto_data) = proto_result {
2645 self.draw_proto_masks(dst, &output_boxes, &proto_data, overlay)?;
2646 } else {
2647 let mut output_masks = Vec::with_capacity(100);
2649 decoder
2650 .decode(outputs, &mut output_boxes, &mut output_masks)
2651 .map_err(|e| Error::Internal(format!("decode: {e:#?}")))?;
2652 self.draw_decoded_masks(dst, &output_boxes, &output_masks, overlay)?;
2653 }
2654 Ok(output_boxes)
2655 }
2656
2657 #[cfg(feature = "tracker")]
2665 pub fn draw_masks_tracked<TR: edgefirst_tracker::Tracker<DetectBox>>(
2666 &mut self,
2667 decoder: &edgefirst_decoder::Decoder,
2668 tracker: &mut TR,
2669 timestamp: u64,
2670 outputs: &[&TensorDyn],
2671 dst: &mut TensorDyn,
2672 overlay: MaskOverlay<'_>,
2673 ) -> Result<(Vec<DetectBox>, Vec<edgefirst_tracker::TrackInfo>)> {
2674 let mut output_boxes = Vec::with_capacity(100);
2675 let mut output_tracks = Vec::new();
2676
2677 let proto_result = decoder
2678 .decode_proto_tracked(
2679 tracker,
2680 timestamp,
2681 outputs,
2682 &mut output_boxes,
2683 &mut output_tracks,
2684 )
2685 .map_err(|e| Error::Internal(format!("decode_proto_tracked: {e:#?}")))?;
2686
2687 if let Some(proto_data) = proto_result {
2688 self.draw_proto_masks(dst, &output_boxes, &proto_data, overlay)?;
2689 } else {
2690 let mut output_masks = Vec::with_capacity(100);
2694 decoder
2695 .decode_tracked(
2696 tracker,
2697 timestamp,
2698 outputs,
2699 &mut output_boxes,
2700 &mut output_masks,
2701 &mut output_tracks,
2702 )
2703 .map_err(|e| Error::Internal(format!("decode_tracked: {e:#?}")))?;
2704 self.draw_decoded_masks(dst, &output_boxes, &output_masks, overlay)?;
2705 }
2706 Ok((output_boxes, output_tracks))
2707 }
2708
2709 pub fn materialize_masks(
2733 &mut self,
2734 detect: &[DetectBox],
2735 proto_data: &ProtoData,
2736 letterbox: Option<[f32; 4]>,
2737 resolution: MaskResolution,
2738 ) -> Result<Vec<Segmentation>> {
2739 let cpu = self.cpu.as_mut().ok_or(Error::NoConverter)?;
2740 match resolution {
2741 MaskResolution::Proto => cpu.materialize_segmentations(detect, proto_data, letterbox),
2742 MaskResolution::Scaled { width, height } => {
2743 cpu.materialize_scaled_segmentations(detect, proto_data, letterbox, width, height)
2744 }
2745 }
2746 }
2747}
2748
2749impl ImageProcessorTrait for ImageProcessor {
2750 fn convert(
2756 &mut self,
2757 src: &TensorDyn,
2758 dst: &mut TensorDyn,
2759 rotation: Rotation,
2760 flip: Flip,
2761 crop: Crop,
2762 ) -> Result<()> {
2763 let start = Instant::now();
2764 let src_fmt = src.format();
2765 let dst_fmt = dst.format();
2766 let _span = tracing::trace_span!(
2767 "image.convert",
2768 ?src_fmt,
2769 ?dst_fmt,
2770 src_memory = ?src.memory(),
2771 dst_memory = ?dst.memory(),
2772 ?rotation,
2773 ?flip,
2774 )
2775 .entered();
2776 log::trace!(
2777 "convert: {src_fmt:?}({:?}/{:?}) → {dst_fmt:?}({:?}/{:?}), \
2778 rotation={rotation:?}, flip={flip:?}, backend={:?}",
2779 src.dtype(),
2780 src.memory(),
2781 dst.dtype(),
2782 dst.memory(),
2783 self.forced_backend,
2784 );
2785
2786 if let Some(forced) = self.forced_backend {
2788 return match forced {
2789 ForcedBackend::Cpu => {
2790 if let Some(cpu) = self.cpu.as_mut() {
2791 let r = cpu.convert(src, dst, rotation, flip, crop);
2792 log::trace!(
2793 "convert: forced=cpu result={} ({:?})",
2794 if r.is_ok() { "ok" } else { "err" },
2795 start.elapsed()
2796 );
2797 return r;
2798 }
2799 Err(Error::ForcedBackendUnavailable("cpu".into()))
2800 }
2801 ForcedBackend::G2d => {
2802 #[cfg(target_os = "linux")]
2803 if let Some(g2d) = self.g2d.as_mut() {
2804 let r = g2d.convert(src, dst, rotation, flip, crop);
2805 log::trace!(
2806 "convert: forced=g2d result={} ({:?})",
2807 if r.is_ok() { "ok" } else { "err" },
2808 start.elapsed()
2809 );
2810 return r;
2811 }
2812 Err(Error::ForcedBackendUnavailable("g2d".into()))
2813 }
2814 ForcedBackend::OpenGl => {
2815 #[cfg(any(
2816 target_os = "linux",
2817 target_os = "macos",
2818 target_os = "ios",
2819 target_os = "android",
2820 target_os = "windows"
2821 ))]
2822 #[cfg(feature = "opengl")]
2823 if let Some(opengl) = self.opengl.as_mut() {
2824 let r = opengl.convert(src, dst, rotation, flip, crop);
2825 log::trace!(
2826 "convert: forced=opengl result={} ({:?})",
2827 if r.is_ok() { "ok" } else { "err" },
2828 start.elapsed()
2829 );
2830 return r;
2831 }
2832 Err(Error::ForcedBackendUnavailable("opengl".into()))
2833 }
2834 };
2835 }
2836
2837 #[cfg(any(
2839 target_os = "linux",
2840 target_os = "macos",
2841 target_os = "ios",
2842 target_os = "android",
2843 target_os = "windows"
2844 ))]
2845 #[cfg(feature = "opengl")]
2846 if let Some(opengl) = self.opengl.as_mut() {
2847 match opengl.convert(src, dst, rotation, flip, crop) {
2848 Ok(_) => {
2849 log::trace!(
2850 "convert: auto selected=opengl for {src_fmt:?}→{dst_fmt:?} ({:?})",
2851 start.elapsed()
2852 );
2853 return Ok(());
2854 }
2855 Err(e) => {
2856 self.convert_fallbacks
2857 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2858 log::debug!(
2859 "convert: auto opengl declined {src_fmt:?}@{:?}→{dst_fmt:?}@{:?}, \
2860 falling back toward G2D/CPU: {e}",
2861 src.memory(),
2862 dst.memory(),
2863 );
2864 }
2865 }
2866 }
2867
2868 #[cfg(target_os = "linux")]
2869 if let Some(g2d) = self.g2d.as_mut() {
2870 let src_is_yuv = src.format().is_some_and(|f| f.is_yuv());
2877 let dst_is_yuv = dst.format().is_some_and(|f| f.is_yuv());
2878 let g2d_eligible = if src_is_yuv || dst_is_yuv {
2879 let cm = if src_is_yuv {
2880 crate::colorimetry::effective_colorimetry(src)
2881 } else {
2882 crate::colorimetry::effective_colorimetry(dst)
2883 };
2884 crate::g2d::g2d_can_handle(&cm, true)
2885 } else {
2886 true
2887 };
2888 if !g2d_eligible {
2889 log::trace!(
2890 "convert: auto g2d skipped {src_fmt:?}→{dst_fmt:?} \
2891 (colorimetry not expressible: full-range/BT.2020)"
2892 );
2893 } else {
2894 match g2d.convert(src, dst, rotation, flip, crop) {
2895 Ok(_) => {
2896 log::trace!(
2897 "convert: auto selected=g2d for {src_fmt:?}→{dst_fmt:?} ({:?})",
2898 start.elapsed()
2899 );
2900 return Ok(());
2901 }
2902 Err(e) => {
2903 log::trace!("convert: auto g2d declined {src_fmt:?}→{dst_fmt:?}: {e}");
2904 }
2905 }
2906 }
2907 }
2908
2909 if let Some(cpu) = self.cpu.as_mut() {
2910 match cpu.convert(src, dst, rotation, flip, crop) {
2911 Ok(_) => {
2912 log::trace!(
2913 "convert: auto selected=cpu for {src_fmt:?}→{dst_fmt:?} ({:?})",
2914 start.elapsed()
2915 );
2916 return Ok(());
2917 }
2918 Err(e) => {
2919 log::trace!("convert: auto cpu failed {src_fmt:?}→{dst_fmt:?}: {e}");
2920 return Err(e);
2921 }
2922 }
2923 }
2924 Err(Error::NoConverter)
2925 }
2926
2927 fn convert_deferred(
2928 &mut self,
2929 src: &TensorDyn,
2930 dst: &mut TensorDyn,
2931 rotation: Rotation,
2932 flip: Flip,
2933 crop: Crop,
2934 ) -> Result<()> {
2935 #[cfg(any(
2941 target_os = "linux",
2942 target_os = "macos",
2943 target_os = "ios",
2944 target_os = "android",
2945 target_os = "windows"
2946 ))]
2947 #[cfg(feature = "opengl")]
2948 {
2949 let gl_forced = matches!(self.forced_backend, Some(ForcedBackend::OpenGl));
2950 if gl_forced || self.forced_backend.is_none() {
2951 if let Some(opengl) = self.opengl.as_mut() {
2952 match opengl.convert_deferred(src, dst, rotation, flip, crop) {
2953 Ok(()) => return Ok(()),
2954 Err(e) => {
2955 log::trace!("convert_deferred: gl declined: {e}; eager fallback");
2956 if gl_forced {
2959 return Err(e);
2960 }
2961 }
2962 }
2963 }
2964 }
2965 }
2966 self.convert(src, dst, rotation, flip, crop)
2967 }
2968
2969 fn flush(&mut self) -> Result<()> {
2970 let _span = tracing::trace_span!("image.flush").entered();
2971 #[cfg(any(
2974 target_os = "linux",
2975 target_os = "macos",
2976 target_os = "ios",
2977 target_os = "android",
2978 target_os = "windows"
2979 ))]
2980 #[cfg(feature = "opengl")]
2981 if let Some(opengl) = self.opengl.as_mut() {
2982 return opengl.flush();
2983 }
2984 Ok(())
2985 }
2986
2987 fn draw_decoded_masks(
2988 &mut self,
2989 dst: &mut TensorDyn,
2990 detect: &[DetectBox],
2991 segmentation: &[Segmentation],
2992 overlay: MaskOverlay<'_>,
2993 ) -> Result<()> {
2994 let _span = tracing::trace_span!(
2995 "image.draw_decoded_masks",
2996 n_detections = detect.len(),
2997 n_segmentations = segmentation.len(),
2998 )
2999 .entered();
3000 let start = Instant::now();
3001
3002 if let Some(bg) = overlay.background {
3003 if bg.aliases(dst) {
3004 return Err(Error::AliasedBuffers(
3005 "background must not reference the same buffer as dst".to_string(),
3006 ));
3007 }
3008 }
3009
3010 let lb_boxes: Vec<DetectBox>;
3013 let lb_segs: Vec<Segmentation>;
3014 let (detect, segmentation) = if let Some(lb) = overlay.letterbox {
3015 lb_boxes = detect.iter().map(|&d| unletter_bbox(d, lb)).collect();
3016 if segmentation.len() == lb_boxes.len() {
3019 lb_segs = segmentation
3026 .iter()
3027 .zip(lb_boxes.iter())
3028 .map(|(s, d)| copy_segmentation(s, d.bbox.into()))
3029 .collect::<Result<Vec<_>>>()?;
3030 (lb_boxes.as_slice(), lb_segs.as_slice())
3031 } else {
3032 (lb_boxes.as_slice(), segmentation)
3036 }
3037 } else {
3038 (detect, segmentation)
3039 };
3040 #[cfg(target_os = "linux")]
3041 let is_empty_frame = detect.is_empty() && segmentation.is_empty();
3042
3043 if let Some(forced) = self.forced_backend {
3045 return match forced {
3046 ForcedBackend::Cpu => {
3047 if let Some(cpu) = self.cpu.as_mut() {
3048 return cpu.draw_decoded_masks(dst, detect, segmentation, overlay);
3049 }
3050 Err(Error::ForcedBackendUnavailable("cpu".into()))
3051 }
3052 ForcedBackend::G2d => {
3053 #[cfg(target_os = "linux")]
3056 if let Some(g2d) = self.g2d.as_mut() {
3057 return g2d.draw_decoded_masks(dst, detect, segmentation, overlay);
3058 }
3059 Err(Error::ForcedBackendUnavailable("g2d".into()))
3060 }
3061 ForcedBackend::OpenGl => {
3062 #[cfg(any(
3065 target_os = "linux",
3066 target_os = "macos",
3067 target_os = "ios",
3068 target_os = "android",
3069 target_os = "windows"
3070 ))]
3071 #[cfg(feature = "opengl")]
3072 if let Some(opengl) = self.opengl.as_mut() {
3073 return opengl.draw_decoded_masks(dst, detect, segmentation, overlay);
3074 }
3075 Err(Error::ForcedBackendUnavailable("opengl".into()))
3076 }
3077 };
3078 }
3079
3080 #[cfg(target_os = "linux")]
3086 if is_empty_frame {
3087 if let Some(g2d) = self.g2d.as_mut() {
3088 match g2d.draw_decoded_masks(dst, detect, segmentation, overlay) {
3089 Ok(_) => {
3090 log::trace!(
3091 "draw_decoded_masks empty frame via g2d in {:?}",
3092 start.elapsed()
3093 );
3094 return Ok(());
3095 }
3096 Err(e) => log::trace!("g2d empty-frame path unavailable: {e:?}"),
3097 }
3098 }
3099 }
3100
3101 #[cfg(any(
3105 target_os = "linux",
3106 target_os = "macos",
3107 target_os = "ios",
3108 target_os = "android",
3109 target_os = "windows"
3110 ))]
3111 #[cfg(feature = "opengl")]
3112 if let Some(opengl) = self.opengl.as_mut() {
3113 log::trace!(
3114 "draw_decoded_masks started with opengl in {:?}",
3115 start.elapsed()
3116 );
3117 match opengl.draw_decoded_masks(dst, detect, segmentation, overlay) {
3118 Ok(_) => {
3119 log::trace!("draw_decoded_masks with opengl in {:?}", start.elapsed());
3120 return Ok(());
3121 }
3122 Err(e) => {
3123 log::trace!("draw_decoded_masks didn't work with opengl: {e:?}")
3124 }
3125 }
3126 }
3127
3128 log::trace!(
3129 "draw_decoded_masks started with cpu in {:?}",
3130 start.elapsed()
3131 );
3132 if let Some(cpu) = self.cpu.as_mut() {
3133 match cpu.draw_decoded_masks(dst, detect, segmentation, overlay) {
3134 Ok(_) => {
3135 log::trace!("draw_decoded_masks with cpu in {:?}", start.elapsed());
3136 return Ok(());
3137 }
3138 Err(e) => {
3139 log::trace!("draw_decoded_masks didn't work with cpu: {e:?}");
3140 return Err(e);
3141 }
3142 }
3143 }
3144 Err(Error::NoConverter)
3145 }
3146
3147 fn draw_proto_masks(
3148 &mut self,
3149 dst: &mut TensorDyn,
3150 detect: &[DetectBox],
3151 proto_data: &ProtoData,
3152 overlay: MaskOverlay<'_>,
3153 ) -> Result<()> {
3154 let start = Instant::now();
3155
3156 if let Some(bg) = overlay.background {
3157 if bg.aliases(dst) {
3158 return Err(Error::AliasedBuffers(
3159 "background must not reference the same buffer as dst".to_string(),
3160 ));
3161 }
3162 }
3163
3164 let lb_boxes: Vec<DetectBox>;
3170 let render_detect = if let Some(lb) = overlay.letterbox {
3171 lb_boxes = detect.iter().map(|&d| unletter_bbox(d, lb)).collect();
3172 lb_boxes.as_slice()
3173 } else {
3174 detect
3175 };
3176 #[cfg(target_os = "linux")]
3177 let is_empty_frame = detect.is_empty();
3178
3179 if let Some(forced) = self.forced_backend {
3181 return match forced {
3182 ForcedBackend::Cpu => {
3183 if let Some(cpu) = self.cpu.as_mut() {
3184 return cpu.draw_proto_masks(dst, render_detect, proto_data, overlay);
3185 }
3186 Err(Error::ForcedBackendUnavailable("cpu".into()))
3187 }
3188 ForcedBackend::G2d => {
3189 #[cfg(target_os = "linux")]
3190 if let Some(g2d) = self.g2d.as_mut() {
3191 return g2d.draw_proto_masks(dst, render_detect, proto_data, overlay);
3192 }
3193 Err(Error::ForcedBackendUnavailable("g2d".into()))
3194 }
3195 ForcedBackend::OpenGl => {
3196 #[cfg(any(
3197 target_os = "linux",
3198 target_os = "macos",
3199 target_os = "ios",
3200 target_os = "android",
3201 target_os = "windows"
3202 ))]
3203 #[cfg(feature = "opengl")]
3204 if let Some(opengl) = self.opengl.as_mut() {
3205 return opengl.draw_proto_masks(dst, render_detect, proto_data, overlay);
3206 }
3207 Err(Error::ForcedBackendUnavailable("opengl".into()))
3208 }
3209 };
3210 }
3211
3212 #[cfg(target_os = "linux")]
3215 if is_empty_frame {
3216 if let Some(g2d) = self.g2d.as_mut() {
3217 match g2d.draw_proto_masks(dst, render_detect, proto_data, overlay) {
3218 Ok(_) => {
3219 log::trace!(
3220 "draw_proto_masks empty frame via g2d in {:?}",
3221 start.elapsed()
3222 );
3223 return Ok(());
3224 }
3225 Err(e) => log::trace!("g2d empty-frame path unavailable: {e:?}"),
3226 }
3227 }
3228 }
3229
3230 #[cfg(any(
3239 target_os = "linux",
3240 target_os = "macos",
3241 target_os = "ios",
3242 target_os = "android",
3243 target_os = "windows"
3244 ))]
3245 #[cfg(feature = "opengl")]
3246 if let (Some(_), Some(_)) = (self.cpu.as_ref(), self.opengl.as_ref()) {
3247 let segmentation = match self.cpu.as_mut() {
3248 Some(cpu) => {
3249 log::trace!(
3250 "draw_proto_masks started with hybrid (cpu+opengl) in {:?}",
3251 start.elapsed()
3252 );
3253 cpu.materialize_segmentations(detect, proto_data, overlay.letterbox)?
3254 }
3255 None => unreachable!("cpu presence checked above"),
3256 };
3257 if let Some(opengl) = self.opengl.as_mut() {
3258 match opengl.draw_decoded_masks(dst, render_detect, &segmentation, overlay) {
3259 Ok(_) => {
3260 log::trace!(
3261 "draw_proto_masks with hybrid (cpu+opengl) in {:?}",
3262 start.elapsed()
3263 );
3264 return Ok(());
3265 }
3266 Err(e) => {
3267 log::trace!(
3268 "draw_proto_masks hybrid path failed, falling back to cpu: {e:?}"
3269 );
3270 }
3271 }
3272 }
3273 }
3274
3275 let Some(cpu) = self.cpu.as_mut() else {
3276 return Err(Error::Internal(
3277 "draw_proto_masks requires CPU backend for fallback path".into(),
3278 ));
3279 };
3280 log::trace!("draw_proto_masks started with cpu in {:?}", start.elapsed());
3281 cpu.draw_proto_masks(dst, render_detect, proto_data, overlay)
3282 }
3283
3284 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()> {
3285 let start = Instant::now();
3286
3287 if let Some(forced) = self.forced_backend {
3289 return match forced {
3290 ForcedBackend::Cpu => {
3291 if let Some(cpu) = self.cpu.as_mut() {
3292 return cpu.set_class_colors(colors);
3293 }
3294 Err(Error::ForcedBackendUnavailable("cpu".into()))
3295 }
3296 ForcedBackend::G2d => Err(Error::NotSupported(
3297 "g2d does not support set_class_colors".into(),
3298 )),
3299 ForcedBackend::OpenGl => {
3300 #[cfg(any(
3301 target_os = "linux",
3302 target_os = "macos",
3303 target_os = "ios",
3304 target_os = "android",
3305 target_os = "windows"
3306 ))]
3307 #[cfg(feature = "opengl")]
3308 if let Some(opengl) = self.opengl.as_mut() {
3309 return opengl.set_class_colors(colors);
3310 }
3311 Err(Error::ForcedBackendUnavailable("opengl".into()))
3312 }
3313 };
3314 }
3315
3316 let mut backends = 0usize;
3325 let mut first_err = None;
3326 #[cfg(any(
3327 target_os = "linux",
3328 target_os = "macos",
3329 target_os = "ios",
3330 target_os = "android",
3331 target_os = "windows"
3332 ))]
3333 #[cfg(feature = "opengl")]
3334 if let Some(opengl) = self.opengl.as_mut() {
3335 backends += 1;
3336 match opengl.set_class_colors(colors) {
3337 Ok(()) => log::trace!("colors set with opengl in {:?}", start.elapsed()),
3338 Err(e) => {
3339 log::trace!("colors didn't set with opengl: {e:?}");
3340 first_err.get_or_insert(e);
3341 }
3342 }
3343 }
3344 if let Some(cpu) = self.cpu.as_mut() {
3345 backends += 1;
3346 match cpu.set_class_colors(colors) {
3347 Ok(()) => log::trace!("colors set with cpu in {:?}", start.elapsed()),
3348 Err(e) => {
3349 log::trace!("colors didn't set with cpu: {e:?}");
3350 first_err.get_or_insert(e);
3351 }
3352 }
3353 }
3354 match first_err {
3358 Some(e) => Err(e),
3359 None if backends > 0 => Ok(()),
3360 None => Err(Error::NoConverter),
3361 }
3362 }
3363}
3364
3365#[cfg(all(test, feature = "codec"))]
3375pub(crate) fn load_image_test_helper(
3376 image: &[u8],
3377 format: Option<PixelFormat>,
3378 memory: Option<TensorMemory>,
3379) -> Result<TensorDyn> {
3380 use edgefirst_codec::{peek_info, ImageDecoder, ImageLoad};
3381
3382 let info = peek_info(image)?;
3386 let native_fmt = info.format;
3387 let w = info.width;
3388 let h = info.height;
3389
3390 let mut decoder = ImageDecoder::new();
3391
3392 #[cfg(target_os = "linux")]
3395 let native_src = {
3396 if let Some(aligned_pitch) = padded_dma_pitch_for(native_fmt, w, &memory) {
3397 let mut dma = Tensor::<u8>::image_with_stride(
3398 w,
3399 h,
3400 native_fmt,
3401 aligned_pitch,
3402 Some(TensorMemory::DmaBuf),
3403 edgefirst_tensor::CpuAccess::ReadWrite,
3404 )?;
3405 dma.load_image(&mut decoder, image)?;
3406 TensorDyn::from(dma)
3407 } else {
3408 let mut img = Tensor::<u8>::image(
3409 w,
3410 h,
3411 native_fmt,
3412 memory,
3413 edgefirst_tensor::CpuAccess::ReadWrite,
3414 )?;
3415 img.load_image(&mut decoder, image)?;
3416 TensorDyn::from(img)
3417 }
3418 };
3419 #[cfg(not(target_os = "linux"))]
3420 let native_src = {
3421 let mut img = Tensor::<u8>::image(
3422 w,
3423 h,
3424 native_fmt,
3425 memory,
3426 edgefirst_tensor::CpuAccess::ReadWrite,
3427 )?;
3428 img.load_image(&mut decoder, image)?;
3429 TensorDyn::from(img)
3430 };
3431
3432 match format {
3436 Some(f) if f != native_fmt => {
3437 let mut dst = TensorDyn::image(
3438 w,
3439 h,
3440 f,
3441 DType::U8,
3442 memory,
3443 edgefirst_tensor::CpuAccess::ReadWrite,
3444 )?;
3445 #[allow(clippy::needless_update)]
3452 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
3453 backend: ComputeBackend::Cpu,
3454 ..Default::default()
3455 })?;
3456 proc.convert(
3457 &native_src,
3458 &mut dst,
3459 Rotation::None,
3460 Flip::None,
3461 Crop::default(),
3462 )?;
3463 Ok(dst)
3464 }
3465 _ => Ok(native_src),
3466 }
3467}
3468
3469pub fn save_jpeg(tensor: &TensorDyn, path: impl AsRef<std::path::Path>, quality: u8) -> Result<()> {
3473 let t = tensor.as_typed::<u8>().ok_or(Error::UnsupportedFormat(
3474 "save_jpeg requires u8 tensor".to_string(),
3475 ))?;
3476 let fmt = t.format().ok_or(Error::NotAnImage)?;
3477 if fmt.layout() != PixelLayout::Packed {
3478 return Err(Error::NotImplemented(
3479 "Saving planar images is not supported".to_string(),
3480 ));
3481 }
3482
3483 let colour = match fmt {
3484 PixelFormat::Rgb => jpeg_encoder::ColorType::Rgb,
3485 PixelFormat::Rgba => jpeg_encoder::ColorType::Rgba,
3486 _ => {
3487 return Err(Error::NotImplemented(
3488 "Unsupported image format for saving".to_string(),
3489 ));
3490 }
3491 };
3492
3493 let w = t.width().ok_or(Error::NotAnImage)?;
3494 let h = t.height().ok_or(Error::NotAnImage)?;
3495 let encoder = jpeg_encoder::Encoder::new_file(path, quality)?;
3496 let tensor_map = t.map_read()?;
3497
3498 encoder.encode(&tensor_map, w as u16, h as u16, colour)?;
3499
3500 Ok(())
3501}
3502
3503pub(crate) struct FunctionTimer<T: Display> {
3504 name: T,
3505 start: std::time::Instant,
3506}
3507
3508impl<T: Display> FunctionTimer<T> {
3509 pub fn new(name: T) -> Self {
3510 Self {
3511 name,
3512 start: std::time::Instant::now(),
3513 }
3514 }
3515}
3516
3517impl<T: Display> Drop for FunctionTimer<T> {
3518 fn drop(&mut self) {
3519 log::trace!("{} elapsed: {:?}", self.name, self.start.elapsed())
3520 }
3521}
3522
3523const DEFAULT_COLORS: [[f32; 4]; 20] = [
3524 [0., 1., 0., 0.7],
3525 [1., 0.5568628, 0., 0.7],
3526 [0.25882353, 0.15294118, 0.13333333, 0.7],
3527 [0.8, 0.7647059, 0.78039216, 0.7],
3528 [0.3137255, 0.3137255, 0.3137255, 0.7],
3529 [0.1411765, 0.3098039, 0.1215686, 0.7],
3530 [1., 0.95686275, 0.5137255, 0.7],
3531 [0.3529412, 0.32156863, 0., 0.7],
3532 [0.4235294, 0.6235294, 0.6509804, 0.7],
3533 [0.5098039, 0.5098039, 0.7294118, 0.7],
3534 [0.00784314, 0.18823529, 0.29411765, 0.7],
3535 [0.0, 0.2706, 1.0, 0.7],
3536 [0.0, 0.0, 0.0, 0.7],
3537 [0.0, 0.5, 0.0, 0.7],
3538 [1.0, 0.0, 0.0, 0.7],
3539 [0.0, 0.0, 1.0, 0.7],
3540 [1.0, 0.5, 0.5, 0.7],
3541 [0.1333, 0.5451, 0.1333, 0.7],
3542 [0.1176, 0.4118, 0.8235, 0.7],
3543 [1., 1., 1., 0.7],
3544];
3545
3546const fn denorm<const M: usize, const N: usize>(a: [[f32; M]; N]) -> [[u8; M]; N] {
3547 let mut result = [[0; M]; N];
3548 let mut i = 0;
3549 while i < N {
3550 let mut j = 0;
3551 while j < M {
3552 result[i][j] = (a[i][j] * 255.0).round() as u8;
3553 j += 1;
3554 }
3555 i += 1;
3556 }
3557 result
3558}
3559
3560const DEFAULT_COLORS_U8: [[u8; 4]; 20] = denorm(DEFAULT_COLORS);
3561
3562#[cfg(test)]
3563#[cfg_attr(coverage_nightly, coverage(off))]
3564mod alignment_tests {
3565 use super::*;
3566
3567 #[test]
3568 fn align_width_rgba8_common_widths() {
3569 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); }
3580
3581 #[test]
3582 fn align_width_rgb888_packed() {
3583 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] {
3590 let padded = align_width_for_gpu_pitch(w, 3);
3591 assert!(padded >= w);
3592 assert_eq!((padded * 3) % 64, 0);
3593 assert_eq!((padded * 3) % 3, 0);
3594 }
3595 }
3596
3597 #[test]
3598 fn align_width_grey_u8() {
3599 assert_eq!(align_width_for_gpu_pitch(64, 1), 64);
3601 assert_eq!(align_width_for_gpu_pitch(640, 1), 640);
3602 assert_eq!(align_width_for_gpu_pitch(1, 1), 64);
3603 assert_eq!(align_width_for_gpu_pitch(65, 1), 128);
3604 }
3605
3606 #[test]
3607 fn align_width_zero_inputs() {
3608 assert_eq!(align_width_for_gpu_pitch(0, 4), 0);
3609 assert_eq!(align_width_for_gpu_pitch(640, 0), 640);
3610 }
3611
3612 #[test]
3613 fn align_width_never_returns_smaller_than_input() {
3614 for &bpp in &[1usize, 2, 3, 4, 8] {
3618 for &w in &[
3619 1usize,
3620 17,
3621 64,
3622 65,
3623 100,
3624 1280,
3625 1281,
3626 1920,
3627 3004,
3628 3072,
3629 3840,
3630 usize::MAX / 8,
3631 usize::MAX / 4,
3632 usize::MAX / 2,
3633 usize::MAX - 1,
3634 usize::MAX,
3635 ] {
3636 let aligned = align_width_for_gpu_pitch(w, bpp);
3637 assert!(
3638 aligned >= w,
3639 "align_width_for_gpu_pitch({w}, {bpp}) = {aligned} < {w}"
3640 );
3641 }
3642 }
3643 }
3644
3645 #[test]
3646 fn align_width_overflow_returns_unaligned_not_smaller() {
3647 let aligned_extreme = usize::MAX - 15; assert_eq!(
3653 align_width_for_gpu_pitch(aligned_extreme, 4),
3654 aligned_extreme
3655 );
3656 let misaligned_extreme = usize::MAX - 1;
3659 let result = align_width_for_gpu_pitch(misaligned_extreme, 4);
3660 assert!(
3661 result == misaligned_extreme || result >= misaligned_extreme,
3662 "extreme misaligned width must not be rounded down to {result}"
3663 );
3664 }
3665
3666 #[test]
3667 fn checked_lcm_basic_and_overflow() {
3668 assert_eq!(checked_num_integer_lcm(64, 4), Some(64));
3669 assert_eq!(checked_num_integer_lcm(64, 3), Some(192));
3670 assert_eq!(checked_num_integer_lcm(64, 1), Some(64));
3671 assert_eq!(checked_num_integer_lcm(0, 4), Some(0));
3672 assert_eq!(checked_num_integer_lcm(64, 0), Some(0));
3673 assert_eq!(
3675 checked_num_integer_lcm(usize::MAX, usize::MAX - 1),
3676 None,
3677 "coprime extreme values must overflow detect, not panic"
3678 );
3679 }
3680
3681 #[test]
3682 fn primary_plane_bpp_known_formats() {
3683 assert_eq!(primary_plane_bpp(PixelFormat::Rgba, 1), Some(4));
3685 assert_eq!(primary_plane_bpp(PixelFormat::Bgra, 1), Some(4));
3686 assert_eq!(primary_plane_bpp(PixelFormat::Rgb, 1), Some(3));
3687 assert_eq!(primary_plane_bpp(PixelFormat::Grey, 1), Some(1));
3688 assert_eq!(primary_plane_bpp(PixelFormat::Nv12, 1), Some(1));
3690 }
3691}
3692
3693#[cfg(test)]
3694#[cfg_attr(coverage_nightly, coverage(off))]
3695#[allow(deprecated)]
3696mod image_tests {
3697 use super::*;
3698 use crate::{CPUProcessor, Rotation};
3699 #[cfg(target_os = "linux")]
3700 use edgefirst_tensor::is_dma_available;
3701 use edgefirst_tensor::{TensorMapTrait, TensorMemory, TensorTrait};
3702 use image::buffer::ConvertBuffer;
3703
3704 fn convert_img(
3710 proc: &mut dyn ImageProcessorTrait,
3711 src: TensorDyn,
3712 dst: TensorDyn,
3713 rotation: Rotation,
3714 flip: Flip,
3715 crop: Crop,
3716 ) -> (Result<()>, TensorDyn, TensorDyn) {
3717 let src_fourcc = src.format().unwrap();
3718 let dst_fourcc = dst.format().unwrap();
3719 let src_dyn = src;
3720 let mut dst_dyn = dst;
3721 let result = proc.convert(&src_dyn, &mut dst_dyn, rotation, flip, crop);
3722 let src_back = {
3723 let mut __t = src_dyn.into_u8().unwrap();
3724 __t.set_format(src_fourcc).unwrap();
3725 TensorDyn::from(__t)
3726 };
3727 let dst_back = {
3728 let mut __t = dst_dyn.into_u8().unwrap();
3729 __t.set_format(dst_fourcc).unwrap();
3730 TensorDyn::from(__t)
3731 };
3732 (result, src_back, dst_back)
3733 }
3734
3735 #[ctor::ctor(unsafe)]
3736 fn init() {
3737 env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
3738 }
3739
3740 macro_rules! function {
3741 () => {{
3742 fn f() {}
3743 fn type_name_of<T>(_: T) -> &'static str {
3744 std::any::type_name::<T>()
3745 }
3746 let name = type_name_of(f);
3747
3748 match &name[..name.len() - 3].rfind(':') {
3750 Some(pos) => &name[pos + 1..name.len() - 3],
3751 None => &name[..name.len() - 3],
3752 }
3753 }};
3754 }
3755
3756 #[test]
3769 fn batch_view_dst_tiles_match_standalone() {
3770 let mut proc = match ImageProcessor::new() {
3771 Ok(p) => p,
3772 Err(e) => {
3773 crate::test_support::report_skip(&format!(
3774 "{} — ImageProcessor init failed ({e:?})",
3775 function!()
3776 ));
3777 return;
3778 }
3779 };
3780 let n = 3usize;
3781 let (w, h) = (32usize, 24usize);
3782 let colors: [[u8; 4]; 3] = [[210, 40, 40, 255], [40, 210, 40, 255], [40, 40, 210, 255]];
3783 let make_src = |c: [u8; 4]| -> TensorDyn {
3784 let bytes: Vec<u8> = c.iter().copied().cycle().take(w * h * 4).collect();
3785 load_bytes_to_tensor(w, h, PixelFormat::Rgba, Some(TensorMemory::Mem), &bytes).unwrap()
3786 };
3787 let parent = match TensorDyn::image(
3790 w,
3791 n * h,
3792 PixelFormat::Rgba,
3793 DType::U8,
3794 Some(TensorMemory::DmaBuf),
3795 edgefirst_tensor::CpuAccess::ReadWrite,
3796 ) {
3797 Ok(d) => d,
3798 Err(e) => {
3799 crate::test_support::report_skip(&format!(
3800 "{} — tall DMA destination alloc failed ({e:?})",
3801 function!()
3802 ));
3803 return;
3804 }
3805 };
3806
3807 for (i, &c) in colors.iter().enumerate().take(n) {
3809 let mut tile = parent.view(Region::new(0, i * h, w, h)).unwrap();
3810 proc.convert_deferred(
3811 &make_src(c),
3812 &mut tile,
3813 Rotation::None,
3814 Flip::None,
3815 Crop::no_crop(),
3816 )
3817 .unwrap_or_else(|e| panic!("convert_deferred tile {i}: {e:?}"));
3818 }
3819 proc.flush().unwrap();
3820
3821 for (i, &c) in colors.iter().enumerate().take(n) {
3822 let mut solo = TensorDyn::image(
3824 w,
3825 h,
3826 PixelFormat::Rgba,
3827 DType::U8,
3828 Some(TensorMemory::DmaBuf),
3829 edgefirst_tensor::CpuAccess::ReadWrite,
3830 )
3831 .unwrap();
3832 proc.convert(
3833 &make_src(c),
3834 &mut solo,
3835 Rotation::None,
3836 Flip::None,
3837 Crop::no_crop(),
3838 )
3839 .unwrap();
3840
3841 let band = parent.view(Region::new(0, i * h, w, h)).unwrap();
3842 let band_bytes = band
3843 .as_typed::<u8>()
3844 .unwrap()
3845 .map()
3846 .unwrap()
3847 .as_slice()
3848 .to_vec();
3849 let solo_bytes = solo
3850 .as_typed::<u8>()
3851 .unwrap()
3852 .map()
3853 .unwrap()
3854 .as_slice()
3855 .to_vec();
3856 assert_eq!(
3857 band_bytes, solo_bytes,
3858 "tile {i}: band differs from standalone convert (placement or sibling wipe)"
3859 );
3860 assert!(
3861 band_bytes.as_chunks::<4>().0.iter().all(|&p| p == c),
3862 "tile {i}: band is not the expected solid color {c:?} (sibling wipe?)"
3863 );
3864 }
3865 }
3866
3867 #[cfg(test)]
3871 fn gradient_frame(w: usize, h: usize) -> TensorDyn {
3872 let mut bytes = vec![0u8; w * h * 4];
3873 for y in 0..h {
3874 for x in 0..w {
3875 let i = (y * w + x) * 4;
3876 bytes[i] = x as u8;
3877 bytes[i + 1] = y as u8;
3878 bytes[i + 2] = (x ^ y) as u8;
3879 bytes[i + 3] = 255;
3880 }
3881 }
3882 load_bytes_to_tensor(w, h, PixelFormat::Rgba, Some(TensorMemory::Mem), &bytes).unwrap()
3883 }
3884
3885 #[test]
3889 fn tile_into_cpu_distinct_content_parity() {
3890 let mut proc = match ImageProcessor::with_config(ImageProcessorConfig {
3891 backend: ComputeBackend::Cpu,
3892 ..Default::default()
3893 }) {
3894 Ok(p) => p,
3895 Err(e) => {
3896 crate::test_support::report_skip(&format!(
3897 "{} — CPU init failed ({e:?})",
3898 function!()
3899 ));
3900 return;
3901 }
3902 };
3903 let (fw, fh) = (96usize, 64usize);
3904 let src = gradient_frame(fw, fh);
3905 let cfg = TilingConfig::new(32, 32).with_overlap(0.0); let n = tile_grid(fh, fw, 32, 32, 0.0).len();
3907 assert_eq!(n, 6);
3908
3909 let mut parent = proc
3910 .alloc_tile_batch(
3911 n,
3912 &cfg,
3913 PixelFormat::Rgba,
3914 DType::U8,
3915 Some(TensorMemory::Mem),
3916 edgefirst_tensor::CpuAccess::ReadWrite,
3917 )
3918 .unwrap();
3919 let placements = proc.tile_into(&src, &mut parent, &cfg).unwrap();
3920 assert_eq!(placements.len(), n);
3921
3922 for p in &placements {
3923 let source = Region::new(
3924 p.origin.0 as usize,
3925 p.origin.1 as usize,
3926 p.crop_size.0 as usize,
3927 p.crop_size.1 as usize,
3928 );
3929 let mut solo = TensorDyn::image(
3930 32,
3931 32,
3932 PixelFormat::Rgba,
3933 DType::U8,
3934 Some(TensorMemory::Mem),
3935 edgefirst_tensor::CpuAccess::ReadWrite,
3936 )
3937 .unwrap();
3938 proc.convert(
3939 &src,
3940 &mut solo,
3941 Rotation::None,
3942 Flip::None,
3943 Crop::default()
3944 .with_source(Some(source))
3945 .with_fit(Fit::Stretch),
3946 )
3947 .unwrap();
3948 let band = parent.view(Region::new(0, p.index * 32, 32, 32)).unwrap();
3949 let band_bytes = band
3950 .as_typed::<u8>()
3951 .unwrap()
3952 .map()
3953 .unwrap()
3954 .as_slice()
3955 .to_vec();
3956 let solo_bytes = solo
3957 .as_typed::<u8>()
3958 .unwrap()
3959 .map()
3960 .unwrap()
3961 .as_slice()
3962 .to_vec();
3963 assert_eq!(
3964 band_bytes, solo_bytes,
3965 "tile {} band differs from standalone crop-convert",
3966 p.index
3967 );
3968 }
3969 }
3970
3971 #[test]
3974 fn tile_one_matches_tile_into_band() {
3975 let mut proc = match ImageProcessor::with_config(ImageProcessorConfig {
3976 backend: ComputeBackend::Cpu,
3977 ..Default::default()
3978 }) {
3979 Ok(p) => p,
3980 Err(e) => {
3981 crate::test_support::report_skip(&format!(
3982 "{} — CPU init failed ({e:?})",
3983 function!()
3984 ));
3985 return;
3986 }
3987 };
3988 let (fw, fh) = (96usize, 64usize);
3989 let src = gradient_frame(fw, fh);
3990 let cfg = TilingConfig::new(32, 32).with_overlap(0.0);
3991 let n = tile_grid(fh, fw, 32, 32, 0.0).len();
3992
3993 let mut parent = proc
3994 .alloc_tile_batch(
3995 n,
3996 &cfg,
3997 PixelFormat::Rgba,
3998 DType::U8,
3999 Some(TensorMemory::Mem),
4000 edgefirst_tensor::CpuAccess::ReadWrite,
4001 )
4002 .unwrap();
4003 proc.tile_into(&src, &mut parent, &cfg).unwrap();
4004
4005 let plan = proc.plan_tiles(fw, fh, &cfg).unwrap();
4006 for p in &plan {
4007 let mut slot = TensorDyn::image(
4008 32,
4009 32,
4010 PixelFormat::Rgba,
4011 DType::U8,
4012 Some(TensorMemory::Mem),
4013 edgefirst_tensor::CpuAccess::ReadWrite,
4014 )
4015 .unwrap();
4016 proc.tile_one(&src, &mut slot, p, &cfg).unwrap();
4017 proc.flush().unwrap();
4018 let band = parent.view(Region::new(0, p.index * 32, 32, 32)).unwrap();
4019 let slot_bytes = slot
4020 .as_typed::<u8>()
4021 .unwrap()
4022 .map()
4023 .unwrap()
4024 .as_slice()
4025 .to_vec();
4026 let band_bytes = band
4027 .as_typed::<u8>()
4028 .unwrap()
4029 .map()
4030 .unwrap()
4031 .as_slice()
4032 .to_vec();
4033 assert_eq!(
4034 slot_bytes, band_bytes,
4035 "tile {} stream != batch band",
4036 p.index
4037 );
4038 }
4039 }
4040
4041 #[test]
4048 fn tile_into_auto_dma_parity() {
4049 let mut proc = match ImageProcessor::new() {
4050 Ok(p) => p,
4051 Err(e) => {
4052 crate::test_support::report_skip(&format!(
4053 "{} — ImageProcessor init failed ({e:?})",
4054 function!()
4055 ));
4056 return;
4057 }
4058 };
4059 let (fw, fh) = (96usize, 64usize);
4060 let src = gradient_frame(fw, fh);
4061 let cfg = TilingConfig::new(32, 32).with_overlap(0.0);
4062 let n = tile_grid(fh, fw, 32, 32, 0.0).len();
4063
4064 let mut parent = match proc.alloc_tile_batch(
4065 n,
4066 &cfg,
4067 PixelFormat::Rgba,
4068 DType::U8,
4069 Some(TensorMemory::DmaBuf),
4070 edgefirst_tensor::CpuAccess::ReadWrite,
4071 ) {
4072 Ok(p) => p,
4073 Err(e) => {
4074 crate::test_support::report_skip(&format!(
4075 "{} — tall DMA parent alloc failed ({e:?})",
4076 function!()
4077 ));
4078 return;
4079 }
4080 };
4081 let placements = proc.tile_into(&src, &mut parent, &cfg).unwrap();
4082
4083 for p in &placements {
4084 let source = Region::new(
4085 p.origin.0 as usize,
4086 p.origin.1 as usize,
4087 p.crop_size.0 as usize,
4088 p.crop_size.1 as usize,
4089 );
4090 let mut solo = TensorDyn::image(
4091 32,
4092 32,
4093 PixelFormat::Rgba,
4094 DType::U8,
4095 Some(TensorMemory::DmaBuf),
4096 edgefirst_tensor::CpuAccess::ReadWrite,
4097 )
4098 .unwrap();
4099 proc.convert(
4100 &src,
4101 &mut solo,
4102 Rotation::None,
4103 Flip::None,
4104 Crop::default()
4105 .with_source(Some(source))
4106 .with_fit(Fit::Stretch),
4107 )
4108 .unwrap();
4109 let band = parent.view(Region::new(0, p.index * 32, 32, 32)).unwrap();
4110 compare_images(
4116 &band,
4117 &solo,
4118 0.98,
4119 &format!("{}_tile{}", function!(), p.index),
4120 );
4121 }
4122 }
4123
4124 #[test]
4126 fn tile_into_undersized_dst_errors() {
4127 let mut proc = match ImageProcessor::with_config(ImageProcessorConfig {
4128 backend: ComputeBackend::Cpu,
4129 ..Default::default()
4130 }) {
4131 Ok(p) => p,
4132 Err(e) => {
4133 crate::test_support::report_skip(&format!(
4134 "{} — CPU init failed ({e:?})",
4135 function!()
4136 ));
4137 return;
4138 }
4139 };
4140 let (fw, fh) = (96usize, 64usize);
4141 let src = gradient_frame(fw, fh);
4142 let cfg = TilingConfig::new(32, 32).with_overlap(0.0); let mut small = TensorDyn::image(
4145 32,
4146 2 * 32,
4147 PixelFormat::Rgba,
4148 DType::U8,
4149 Some(TensorMemory::Mem),
4150 edgefirst_tensor::CpuAccess::ReadWrite,
4151 )
4152 .unwrap();
4153 let r = proc.tile_into(&src, &mut small, &cfg);
4154 assert!(
4155 matches!(r, Err(Error::InvalidShape(_))),
4156 "expected InvalidShape, got {r:?}"
4157 );
4158 }
4159
4160 #[test]
4162 fn tiling_alloc_rejects_invalid_config() {
4163 let proc = match ImageProcessor::with_config(ImageProcessorConfig {
4164 backend: ComputeBackend::Cpu,
4165 ..Default::default()
4166 }) {
4167 Ok(p) => p,
4168 Err(e) => {
4169 crate::test_support::report_skip(&format!(
4170 "{} — CPU init failed ({e:?})",
4171 function!()
4172 ));
4173 return;
4174 }
4175 };
4176 let bad = TilingConfig::new(0, 640);
4177 assert!(proc.plan_tiles(1920, 1080, &bad).is_err());
4178 assert!(proc
4179 .alloc_tile_batch(
4180 4,
4181 &bad,
4182 PixelFormat::Rgba,
4183 DType::U8,
4184 Some(TensorMemory::Mem),
4185 edgefirst_tensor::CpuAccess::ReadWrite,
4186 )
4187 .is_err());
4188 }
4189
4190 #[test]
4191 fn plan_tiles_metadata_4k() {
4192 let proc = match ImageProcessor::with_config(ImageProcessorConfig {
4193 backend: ComputeBackend::Cpu,
4194 ..Default::default()
4195 }) {
4196 Ok(p) => p,
4197 Err(e) => {
4198 crate::test_support::report_skip(&format!(
4199 "{} — CPU init failed ({e:?})",
4200 function!()
4201 ));
4202 return;
4203 }
4204 };
4205 let cfg = TilingConfig::new(640, 640).with_overlap(0.2);
4206 let plan = proc.plan_tiles(3840, 2160, &cfg).unwrap();
4207 assert_eq!(plan.len(), 32);
4208 assert!(plan.iter().all(|p| p.count == 32));
4209 assert!(plan.iter().all(|p| p.crop_size == (640.0, 640.0)));
4210 assert!(plan.iter().all(|p| p.letterbox.is_none())); assert!(plan.iter().all(|p| p.frame_dims == (3840.0, 2160.0)));
4212 assert_eq!(plan[0].origin, (0.0, 0.0));
4213 }
4214
4215 #[test]
4216 fn test_invalid_crop() {
4217 let src = TensorDyn::image(
4218 100,
4219 100,
4220 PixelFormat::Rgb,
4221 DType::U8,
4222 None,
4223 edgefirst_tensor::CpuAccess::ReadWrite,
4224 )
4225 .unwrap();
4226 let dst = TensorDyn::image(
4227 100,
4228 100,
4229 PixelFormat::Rgb,
4230 DType::U8,
4231 None,
4232 edgefirst_tensor::CpuAccess::ReadWrite,
4233 )
4234 .unwrap();
4235
4236 let crop = Crop::new().with_source(Some(Region::new(50, 50, 60, 60)));
4238 assert!(matches!(
4239 crop.check_crop_dyn(&src, &dst),
4240 Err(Error::CropInvalid(_))
4241 ));
4242
4243 let crop = Crop::new().with_source(Some(Region::new(0, 0, 10, 10)));
4245 assert!(crop.check_crop_dyn(&src, &dst).is_ok());
4246
4247 assert!(Crop::letterbox([0, 0, 0, 255])
4249 .check_crop_dyn(&src, &dst)
4250 .is_ok());
4251 }
4252
4253 #[test]
4254 fn test_invalid_tensor_format() -> Result<(), Error> {
4255 let mut tensor = Tensor::<u8>::new(&[720, 1280, 4, 1], None, None)?;
4257 let result = tensor.set_format(PixelFormat::Rgb);
4258 assert!(result.is_err(), "4D tensor should reject set_format");
4259
4260 let mut tensor = Tensor::<u8>::new(&[720, 1280, 4], None, None)?;
4262 let result = tensor.set_format(PixelFormat::Rgb);
4263 assert!(result.is_err(), "4-channel tensor should reject RGB format");
4264
4265 Ok(())
4266 }
4267
4268 #[test]
4269 fn test_invalid_image_file() -> Result<(), Error> {
4270 let result = crate::load_image_test_helper(&[123; 5000], None, None);
4271 assert!(
4272 matches!(result, Err(Error::Codec(_))),
4273 "unrecognised bytes should surface as Error::Codec, got {result:?}"
4274 );
4275 Ok(())
4276 }
4277
4278 #[test]
4279 fn test_invalid_jpeg_format() -> Result<(), Error> {
4280 let result = crate::load_image_test_helper(&[123; 5000], Some(PixelFormat::Yuyv), None);
4281 assert!(
4284 matches!(result, Err(Error::Codec(_))),
4285 "Yuyv target with garbage bytes should surface as Error::Codec, got {result:?}"
4286 );
4287 Ok(())
4288 }
4289
4290 #[test]
4291 fn test_load_resize_save() {
4292 let file = edgefirst_bench::testdata::read("zidane.jpg");
4293 let img = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4294 assert_eq!(img.width(), Some(1280));
4295 assert_eq!(img.height(), Some(720));
4296
4297 let dst = TensorDyn::image(
4298 640,
4299 360,
4300 PixelFormat::Rgba,
4301 DType::U8,
4302 None,
4303 edgefirst_tensor::CpuAccess::ReadWrite,
4304 )
4305 .unwrap();
4306 let mut converter = CPUProcessor::new();
4307 let (result, _img, dst) = convert_img(
4308 &mut converter,
4309 img,
4310 dst,
4311 Rotation::None,
4312 Flip::None,
4313 Crop::no_crop(),
4314 );
4315 result.unwrap();
4316 assert_eq!(dst.width(), Some(640));
4317 assert_eq!(dst.height(), Some(360));
4318
4319 crate::save_jpeg(&dst, "zidane_resized.jpg", 80).unwrap();
4320
4321 let file = std::fs::read("zidane_resized.jpg").unwrap();
4322 let img = crate::load_image_test_helper(&file, None, None).unwrap();
4325 assert_eq!(img.width(), Some(640));
4326 assert_eq!(img.height(), Some(360));
4327 assert_eq!(img.format().unwrap(), PixelFormat::Nv12);
4328 }
4329
4330 #[test]
4331 fn test_from_tensor_planar() -> Result<(), Error> {
4332 let mut tensor = Tensor::new(&[3, 720, 1280], None, None)?;
4333 tensor
4334 .map()?
4335 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.8bps"));
4336 let planar = {
4337 tensor
4338 .set_format(PixelFormat::PlanarRgb)
4339 .map_err(|e| crate::Error::Internal(e.to_string()))?;
4340 TensorDyn::from(tensor)
4341 };
4342
4343 let rbga = load_bytes_to_tensor(
4344 1280,
4345 720,
4346 PixelFormat::Rgba,
4347 None,
4348 &edgefirst_bench::testdata::read("camera720p.rgba"),
4349 )?;
4350 compare_images_convert_to_rgb(&planar, &rbga, 0.98, function!());
4351
4352 Ok(())
4353 }
4354
4355 #[test]
4356 fn test_from_tensor_invalid_format() {
4357 assert!(PixelFormat::from_fourcc(u32::from_le_bytes(*b"TEST")).is_none());
4360 }
4361
4362 #[test]
4363 #[should_panic(expected = "Failed to save planar RGB image")]
4364 fn test_save_planar() {
4365 let planar_img = load_bytes_to_tensor(
4366 1280,
4367 720,
4368 PixelFormat::PlanarRgb,
4369 None,
4370 &edgefirst_bench::testdata::read("camera720p.8bps"),
4371 )
4372 .unwrap();
4373
4374 let save_path = "/tmp/planar_rgb.jpg";
4375 crate::save_jpeg(&planar_img, save_path, 90).expect("Failed to save planar RGB image");
4376 }
4377
4378 #[test]
4379 #[should_panic(expected = "Failed to save YUYV image")]
4380 fn test_save_yuyv() {
4381 let planar_img = load_bytes_to_tensor(
4382 1280,
4383 720,
4384 PixelFormat::Yuyv,
4385 None,
4386 &edgefirst_bench::testdata::read("camera720p.yuyv"),
4387 )
4388 .unwrap();
4389
4390 let save_path = "/tmp/yuyv.jpg";
4391 crate::save_jpeg(&planar_img, save_path, 90).expect("Failed to save YUYV image");
4392 }
4393
4394 #[test]
4395 fn test_rotation_angle() {
4396 assert_eq!(Rotation::from_degrees_clockwise(0), Rotation::None);
4397 assert_eq!(Rotation::from_degrees_clockwise(90), Rotation::Clockwise90);
4398 assert_eq!(Rotation::from_degrees_clockwise(180), Rotation::Rotate180);
4399 assert_eq!(
4400 Rotation::from_degrees_clockwise(270),
4401 Rotation::CounterClockwise90
4402 );
4403 assert_eq!(Rotation::from_degrees_clockwise(360), Rotation::None);
4404 assert_eq!(Rotation::from_degrees_clockwise(450), Rotation::Clockwise90);
4405 assert_eq!(Rotation::from_degrees_clockwise(540), Rotation::Rotate180);
4406 assert_eq!(
4407 Rotation::from_degrees_clockwise(630),
4408 Rotation::CounterClockwise90
4409 );
4410 }
4411
4412 #[test]
4413 #[should_panic(expected = "rotation angle is not a multiple of 90")]
4414 fn test_rotation_angle_panic() {
4415 Rotation::from_degrees_clockwise(361);
4416 }
4417
4418 #[test]
4419 fn test_disable_env_var() -> Result<(), Error> {
4420 let _lock = acquire_env_lock();
4423
4424 let _guard = EnvGuard::snapshot(&[
4427 "EDGEFIRST_FORCE_BACKEND",
4428 "EDGEFIRST_DISABLE_GL",
4429 "EDGEFIRST_DISABLE_G2D",
4430 "EDGEFIRST_DISABLE_CPU",
4431 ]);
4432
4433 unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") };
4436
4437 #[cfg(target_os = "linux")]
4438 {
4439 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
4440 let converter = ImageProcessor::new()?;
4441 assert!(converter.g2d.is_none());
4442 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_G2D") };
4443 }
4444
4445 #[cfg(any(target_os = "linux", target_os = "windows"))]
4446 #[cfg(feature = "opengl")]
4447 {
4448 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
4449 let converter = ImageProcessor::new()?;
4450 assert!(converter.opengl.is_none());
4451 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_GL") };
4452 }
4453
4454 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
4455 let converter = ImageProcessor::new()?;
4456 assert!(converter.cpu.is_none());
4457 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_CPU") };
4458
4459 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
4461 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
4462 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
4463 let mut converter = ImageProcessor::new()?;
4464
4465 let src = TensorDyn::image(
4466 1280,
4467 720,
4468 PixelFormat::Rgba,
4469 DType::U8,
4470 None,
4471 edgefirst_tensor::CpuAccess::ReadWrite,
4472 )?;
4473 let dst = TensorDyn::image(
4474 640,
4475 360,
4476 PixelFormat::Rgba,
4477 DType::U8,
4478 None,
4479 edgefirst_tensor::CpuAccess::ReadWrite,
4480 )?;
4481 let (result, _src, _dst) = convert_img(
4482 &mut converter,
4483 src,
4484 dst,
4485 Rotation::None,
4486 Flip::None,
4487 Crop::no_crop(),
4488 );
4489 assert!(matches!(result, Err(Error::NoConverter)));
4490 Ok(())
4492 }
4493
4494 #[test]
4495 fn test_unsupported_conversion() {
4496 let src = TensorDyn::image(
4497 1280,
4498 720,
4499 PixelFormat::Nv12,
4500 DType::U8,
4501 None,
4502 edgefirst_tensor::CpuAccess::ReadWrite,
4503 )
4504 .unwrap();
4505 let dst = TensorDyn::image(
4506 640,
4507 360,
4508 PixelFormat::Nv12,
4509 DType::U8,
4510 None,
4511 edgefirst_tensor::CpuAccess::ReadWrite,
4512 )
4513 .unwrap();
4514 let mut converter = ImageProcessor::new().unwrap();
4515 let (result, _src, _dst) = convert_img(
4516 &mut converter,
4517 src,
4518 dst,
4519 Rotation::None,
4520 Flip::None,
4521 Crop::no_crop(),
4522 );
4523 log::debug!("result: {:?}", result);
4524 assert!(matches!(
4525 result,
4526 Err(Error::NotSupported(e)) if e.starts_with("Conversion from NV12 to NV12")
4527 ));
4528 }
4529
4530 #[test]
4531 fn test_load_grey() {
4532 let grey_img = crate::load_image_test_helper(
4536 &edgefirst_bench::testdata::read("grey.jpg"),
4537 Some(PixelFormat::Rgba),
4538 None,
4539 )
4540 .unwrap();
4541 assert_eq!(grey_img.width(), Some(1024));
4542 assert_eq!(grey_img.height(), Some(681));
4543
4544 let grey_but_rgb = crate::load_image_test_helper(
4550 &edgefirst_bench::testdata::read("grey-rgb.jpg"),
4551 Some(PixelFormat::Rgba),
4552 None,
4553 )
4554 .expect("odd-height colour JPEG should decode to NV12 and convert to RGBA");
4555 assert_eq!(grey_but_rgb.width(), Some(1024));
4556 assert_eq!(grey_but_rgb.height(), Some(681));
4557 }
4558
4559 #[test]
4560 fn test_new_nv12() {
4561 let nv12 = TensorDyn::image(
4562 1280,
4563 720,
4564 PixelFormat::Nv12,
4565 DType::U8,
4566 None,
4567 edgefirst_tensor::CpuAccess::ReadWrite,
4568 )
4569 .unwrap();
4570 assert_eq!(nv12.height(), Some(720));
4571 assert_eq!(nv12.width(), Some(1280));
4572 assert_eq!(nv12.format().unwrap(), PixelFormat::Nv12);
4573 assert_eq!(nv12.format().unwrap().channels(), 1);
4575 assert!(nv12.format().is_some_and(
4576 |f| f.layout() == PixelLayout::Planar || f.layout() == PixelLayout::SemiPlanar
4577 ))
4578 }
4579
4580 #[test]
4581 #[cfg(any(target_os = "linux", target_os = "windows"))]
4582 fn test_new_image_converter() {
4583 let dst_width = 640;
4584 let dst_height = 360;
4585 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4586 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4587
4588 let mut converter = ImageProcessor::new().unwrap();
4589 let converter_dst = converter
4590 .create_image(
4591 dst_width,
4592 dst_height,
4593 PixelFormat::Rgba,
4594 DType::U8,
4595 None,
4596 edgefirst_tensor::CpuAccess::ReadWrite,
4597 )
4598 .unwrap();
4599 let (result, src, converter_dst) = convert_img(
4600 &mut converter,
4601 src,
4602 converter_dst,
4603 Rotation::None,
4604 Flip::None,
4605 Crop::no_crop(),
4606 );
4607 result.unwrap();
4608
4609 let cpu_dst = TensorDyn::image(
4610 dst_width,
4611 dst_height,
4612 PixelFormat::Rgba,
4613 DType::U8,
4614 None,
4615 edgefirst_tensor::CpuAccess::ReadWrite,
4616 )
4617 .unwrap();
4618 let mut cpu_converter = CPUProcessor::new();
4619 let (result, _src, cpu_dst) = convert_img(
4620 &mut cpu_converter,
4621 src,
4622 cpu_dst,
4623 Rotation::None,
4624 Flip::None,
4625 Crop::no_crop(),
4626 );
4627 result.unwrap();
4628
4629 compare_images(&converter_dst, &cpu_dst, 0.98, function!());
4630 }
4631
4632 #[test]
4633 #[cfg(any(target_os = "linux", target_os = "windows"))]
4634 fn test_create_image_dtype_i8() {
4635 let mut converter = ImageProcessor::new().unwrap();
4636
4637 let dst = converter
4639 .create_image(
4640 320,
4641 240,
4642 PixelFormat::Rgb,
4643 DType::I8,
4644 None,
4645 edgefirst_tensor::CpuAccess::ReadWrite,
4646 )
4647 .unwrap();
4648 assert_eq!(dst.dtype(), DType::I8);
4649 assert!(dst.width() == Some(320));
4650 assert!(dst.height() == Some(240));
4651 assert_eq!(dst.format(), Some(PixelFormat::Rgb));
4652
4653 let dst_u8 = converter
4655 .create_image(
4656 320,
4657 240,
4658 PixelFormat::Rgb,
4659 DType::U8,
4660 None,
4661 edgefirst_tensor::CpuAccess::ReadWrite,
4662 )
4663 .unwrap();
4664 assert_eq!(dst_u8.dtype(), DType::U8);
4665
4666 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4668 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4669 let mut dst_i8 = converter
4670 .create_image(
4671 320,
4672 240,
4673 PixelFormat::Rgb,
4674 DType::I8,
4675 None,
4676 edgefirst_tensor::CpuAccess::ReadWrite,
4677 )
4678 .unwrap();
4679 converter
4680 .convert(
4681 &src,
4682 &mut dst_i8,
4683 Rotation::None,
4684 Flip::None,
4685 Crop::no_crop(),
4686 )
4687 .unwrap();
4688 }
4689
4690 #[test]
4691 #[cfg(target_os = "linux")]
4692 fn test_create_image_nv12_dma_non_aligned_width() {
4693 let converter = ImageProcessor::new().unwrap();
4699
4700 let result = converter.create_image(
4702 100,
4703 64,
4704 PixelFormat::Nv12,
4705 DType::U8,
4706 Some(TensorMemory::DmaBuf),
4707 edgefirst_tensor::CpuAccess::ReadWrite,
4708 );
4709
4710 match result {
4711 Ok(img) => {
4712 assert_eq!(img.width(), Some(100));
4713 assert_eq!(img.height(), Some(64));
4714 assert_eq!(img.format(), Some(PixelFormat::Nv12));
4715 if let Some(stride) = img.row_stride() {
4716 assert!(
4717 stride >= 100,
4718 "NV12 row_stride {stride} must be >= the logical width (100)",
4719 );
4720 }
4721 }
4722 Err(e) => {
4723 crate::test_support::report_skip(&format!(
4725 "create_image NV12 DMA non-aligned width: {e}"
4726 ));
4727 }
4728 }
4729 }
4730
4731 #[test]
4732 #[ignore] fn test_crop_skip() {
4736 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4737 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4738
4739 let mut converter = ImageProcessor::new().unwrap();
4740 let converter_dst = converter
4741 .create_image(
4742 1280,
4743 720,
4744 PixelFormat::Rgba,
4745 DType::U8,
4746 None,
4747 edgefirst_tensor::CpuAccess::ReadWrite,
4748 )
4749 .unwrap();
4750 let crop = Crop::new().with_source(Some(Region::new(0, 0, 640, 640)));
4751 let (result, src, converter_dst) = convert_img(
4752 &mut converter,
4753 src,
4754 converter_dst,
4755 Rotation::None,
4756 Flip::None,
4757 crop,
4758 );
4759 result.unwrap();
4760
4761 let cpu_dst = TensorDyn::image(
4762 1280,
4763 720,
4764 PixelFormat::Rgba,
4765 DType::U8,
4766 None,
4767 edgefirst_tensor::CpuAccess::ReadWrite,
4768 )
4769 .unwrap();
4770 let mut cpu_converter = CPUProcessor::new();
4771 let (result, _src, cpu_dst) = convert_img(
4772 &mut cpu_converter,
4773 src,
4774 cpu_dst,
4775 Rotation::None,
4776 Flip::None,
4777 crop,
4778 );
4779 result.unwrap();
4780
4781 compare_images(&converter_dst, &cpu_dst, 0.99999, function!());
4782 }
4783
4784 #[test]
4785 fn test_invalid_pixel_format() {
4786 assert!(PixelFormat::from_fourcc(u32::from_le_bytes(*b"TEST")).is_none());
4789 }
4790
4791 #[cfg(target_os = "linux")]
4793 static G2D_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4794
4795 #[cfg(target_os = "linux")]
4796 fn is_g2d_available() -> bool {
4797 *G2D_AVAILABLE.get_or_init(|| G2DProcessor::new().is_ok())
4798 }
4799
4800 #[cfg(any(
4806 target_os = "linux",
4807 target_os = "macos",
4808 target_os = "ios",
4809 target_os = "android",
4810 target_os = "windows"
4811 ))]
4812 #[cfg(feature = "opengl")]
4813 static GL_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4814
4815 #[cfg(any(
4816 target_os = "linux",
4817 target_os = "macos",
4818 target_os = "ios",
4819 target_os = "android",
4820 target_os = "windows"
4821 ))]
4822 #[cfg(feature = "opengl")]
4823 fn is_opengl_available() -> bool {
4825 *GL_AVAILABLE.get_or_init(|| GLProcessorThreaded::new(None).is_ok())
4826 }
4827
4828 #[test]
4840 #[cfg(feature = "opengl")]
4841 fn gl_backend_available_canary() {
4842 let require_gl = std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1");
4843 if !require_gl {
4844 crate::test_support::report_skip(&format!(
4845 "{} — HAL_TEST_REQUIRE_GL is not set to 1",
4846 function!()
4847 ));
4848 return;
4849 }
4850 #[cfg(target_os = "macos")]
4851 if std::env::var_os("HAL_TEST_ALLOW_DLOPEN_ANGLE").is_none() {
4852 crate::test_support::report_skip(&format!(
4853 "{} — ANGLE dlopen gate closed (coverage pass 1)",
4854 function!()
4855 ));
4856 return;
4857 }
4858 GLProcessorThreaded::new(None).expect(
4859 "HAL_TEST_REQUIRE_GL=1 but the GL backend failed to initialize — \
4860 check the ANGLE install/re-sign step and binary entitlements \
4861 (macOS) or the EGL stack (Linux)",
4862 );
4863 }
4864
4865 #[test]
4866 fn test_load_jpeg_with_exif() {
4867 use edgefirst_codec::peek_info;
4868
4869 let file = edgefirst_bench::testdata::read("zidane_rotated_exif.jpg").to_vec();
4874 let info = peek_info(&file).unwrap();
4875 assert_eq!(info.rotation_degrees, 90);
4876 assert!(!info.flip_horizontal);
4877
4878 let loaded = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4879 assert_eq!(loaded.width(), Some(1280));
4881 assert_eq!(loaded.height(), Some(720));
4882
4883 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4886 let cpu_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4887
4888 let rotation = Rotation::from_degrees_clockwise(info.rotation_degrees as usize);
4889 let (dst_width, dst_height) = (cpu_src.height().unwrap(), cpu_src.width().unwrap());
4890
4891 let cpu_dst = TensorDyn::image(
4892 dst_width,
4893 dst_height,
4894 PixelFormat::Rgba,
4895 DType::U8,
4896 None,
4897 edgefirst_tensor::CpuAccess::ReadWrite,
4898 )
4899 .unwrap();
4900 let mut cpu_converter = CPUProcessor::new();
4901
4902 let loaded_rotated = TensorDyn::image(
4905 dst_width,
4906 dst_height,
4907 PixelFormat::Rgba,
4908 DType::U8,
4909 None,
4910 edgefirst_tensor::CpuAccess::ReadWrite,
4911 )
4912 .unwrap();
4913 let (r0, _loaded, loaded_rotated) = convert_img(
4914 &mut cpu_converter,
4915 loaded,
4916 loaded_rotated,
4917 rotation,
4918 Flip::None,
4919 Crop::no_crop(),
4920 );
4921 r0.unwrap();
4922
4923 let (result, _cpu_src, cpu_dst) = convert_img(
4924 &mut cpu_converter,
4925 cpu_src,
4926 cpu_dst,
4927 rotation,
4928 Flip::None,
4929 Crop::no_crop(),
4930 );
4931 result.unwrap();
4932
4933 compare_images(&loaded_rotated, &cpu_dst, 0.98, function!());
4934 }
4935
4936 #[test]
4937 fn test_load_png_with_exif() {
4938 use edgefirst_codec::peek_info;
4939
4940 let file = edgefirst_bench::testdata::read("zidane_rotated_exif_180.png").to_vec();
4943 let info = peek_info(&file).unwrap();
4944 assert_eq!(info.rotation_degrees, 180);
4945 assert!(!info.flip_horizontal);
4946
4947 let loaded = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4948 assert_eq!(loaded.height(), Some(720));
4950 assert_eq!(loaded.width(), Some(1280));
4951
4952 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4957 let cpu_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4958
4959 let rotation = Rotation::from_degrees_clockwise(info.rotation_degrees as usize);
4960 let cpu_dst = TensorDyn::image(
4961 1280,
4962 720,
4963 PixelFormat::Rgba,
4964 DType::U8,
4965 None,
4966 edgefirst_tensor::CpuAccess::ReadWrite,
4967 )
4968 .unwrap();
4969 let mut cpu_converter = CPUProcessor::new();
4970
4971 let (result, _cpu_src, cpu_dst) = convert_img(
4972 &mut cpu_converter,
4973 cpu_src,
4974 cpu_dst,
4975 rotation,
4976 Flip::None,
4977 Crop::no_crop(),
4978 );
4979 result.unwrap();
4980
4981 let loaded_rotated = TensorDyn::image(
4984 1280,
4985 720,
4986 PixelFormat::Rgba,
4987 DType::U8,
4988 None,
4989 edgefirst_tensor::CpuAccess::ReadWrite,
4990 )
4991 .unwrap();
4992 let (r0, _loaded, loaded_rotated) = convert_img(
4993 &mut cpu_converter,
4994 loaded,
4995 loaded_rotated,
4996 rotation,
4997 Flip::None,
4998 Crop::no_crop(),
4999 );
5000 r0.unwrap();
5001
5002 compare_images(&loaded_rotated, &cpu_dst, 0.95, function!());
5007 }
5008
5009 #[cfg(target_os = "linux")]
5015 fn make_rgb_jpeg(width: u32, height: u32) -> Vec<u8> {
5016 let mut bytes = Vec::with_capacity((width * height * 3) as usize);
5017 for y in 0..height {
5018 for x in 0..width {
5019 bytes.push(((x + y) & 0xFF) as u8);
5020 bytes.push(((x.wrapping_mul(3)) & 0xFF) as u8);
5021 bytes.push(((y.wrapping_mul(5)) & 0xFF) as u8);
5022 }
5023 }
5024 let mut out = Vec::new();
5025 let encoder = jpeg_encoder::Encoder::new(&mut out, 85);
5026 encoder
5027 .encode(
5028 &bytes,
5029 width as u16,
5030 height as u16,
5031 jpeg_encoder::ColorType::Rgb,
5032 )
5033 .expect("jpeg-encoder must succeed on trivial input");
5034 out
5035 }
5036
5037 #[test]
5046 #[cfg(target_os = "linux")]
5047 #[cfg(feature = "opengl")]
5048 fn test_convert_rgba_non_4_aligned_width_end_to_end() {
5049 use edgefirst_tensor::is_dma_available;
5050 if !is_dma_available() {
5051 crate::test_support::report_skip(
5052 "test_convert_rgba_non_4_aligned_width_end_to_end — DMA not available",
5053 );
5054 return;
5055 }
5056 let jpeg = make_rgb_jpeg(375, 333);
5060 let src_gl = crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), None).unwrap();
5061 assert_eq!(src_gl.width(), Some(375));
5062 let stride = src_gl.row_stride().unwrap();
5064 assert_eq!(stride, 1536, "expected padded pitch 1536, got {stride}");
5065
5066 let mut gl_proc = ImageProcessor::new().unwrap();
5068 let gl_dst = gl_proc
5069 .create_image(
5070 640,
5071 640,
5072 PixelFormat::Rgba,
5073 DType::U8,
5074 None,
5075 edgefirst_tensor::CpuAccess::ReadWrite,
5076 )
5077 .unwrap();
5078 let (r_gl, _src_gl, gl_dst) = convert_img(
5079 &mut gl_proc,
5080 src_gl,
5081 gl_dst,
5082 Rotation::None,
5083 Flip::None,
5084 Crop::no_crop(),
5085 );
5086 r_gl.expect("GL-backed convert must succeed for 375x333 Rgba src");
5087
5088 let src_cpu =
5093 crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), Some(TensorMemory::Mem))
5094 .unwrap();
5095 let mut cpu_proc = ImageProcessor::with_config(ImageProcessorConfig {
5096 backend: ComputeBackend::Cpu,
5097 ..Default::default()
5098 })
5099 .unwrap();
5100 let cpu_dst = TensorDyn::image(
5101 640,
5102 640,
5103 PixelFormat::Rgba,
5104 DType::U8,
5105 Some(TensorMemory::Mem),
5106 edgefirst_tensor::CpuAccess::ReadWrite,
5107 )
5108 .unwrap();
5109 let (r_cpu, _src_cpu, cpu_dst) = convert_img(
5110 &mut cpu_proc,
5111 src_cpu,
5112 cpu_dst,
5113 Rotation::None,
5114 Flip::None,
5115 Crop::no_crop(),
5116 );
5117 r_cpu.unwrap();
5118
5119 compare_images(&gl_dst, &cpu_dst, 0.95, function!());
5123 }
5124
5125 #[test]
5132 #[cfg(target_os = "linux")]
5133 fn test_load_jpeg_rgba_non_aligned_pitch_padded_dma() {
5134 use edgefirst_tensor::is_dma_available;
5135 if !is_dma_available() {
5136 crate::test_support::report_skip(
5137 "test_load_jpeg_rgba_non_aligned_pitch_padded_dma — DMA not available",
5138 );
5139 return;
5140 }
5141 for &w in &[500u32, 612, 428] {
5145 let jpeg = make_rgb_jpeg(w, 333);
5146 let loaded =
5147 crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), None).unwrap();
5148 let natural = (w as usize) * 4;
5149 let aligned = crate::align_pitch_bytes_to_gpu_alignment(natural).unwrap();
5150 assert!(
5151 aligned > natural,
5152 "test sanity: width {w} should be unaligned"
5153 );
5154 let stride = loaded
5155 .row_stride()
5156 .expect("padded DMA path must set an explicit row_stride — regression if None");
5157 assert_eq!(
5158 stride, aligned,
5159 "width {w}: expected padded stride {aligned}, got {stride} \
5160 (regression: pitch-padding branch skipped?)"
5161 );
5162 let eff = loaded.effective_row_stride().unwrap();
5163 assert_eq!(
5164 eff, aligned,
5165 "effective_row_stride must match stored stride"
5166 );
5167 assert_eq!(loaded.width(), Some(w as usize));
5168 assert_eq!(loaded.height(), Some(333));
5169 }
5170 }
5171
5172 #[test]
5181 #[cfg(target_os = "linux")]
5182 fn test_padded_dma_pitch_for_respects_memory_choice() {
5183 use edgefirst_tensor::{is_dma_available, TensorMemory};
5184
5185 let unaligned_w = 500;
5188
5189 assert_eq!(
5191 crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &Some(TensorMemory::Mem),),
5192 None,
5193 "Mem must never trigger DMA padding"
5194 );
5195 assert_eq!(
5196 crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &Some(TensorMemory::Shm),),
5197 None,
5198 "Shm must never trigger DMA padding"
5199 );
5200
5201 assert_eq!(
5206 crate::padded_dma_pitch_for(
5207 PixelFormat::Rgba,
5208 unaligned_w,
5209 &Some(TensorMemory::DmaBuf),
5210 ),
5211 Some(2048),
5212 "explicit Dma must pad regardless of runtime DMA availability"
5213 );
5214
5215 let none_result = crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &None);
5219 if is_dma_available() {
5220 assert_eq!(
5221 none_result,
5222 Some(2048),
5223 "memory=None + DMA available → pad (will route through DMA)"
5224 );
5225 } else {
5226 assert_eq!(
5227 none_result, None,
5228 "memory=None + DMA unavailable → must NOT pad (would force \
5229 image_with_stride into a DMA-only allocation that fails). \
5230 Regression: padded_dma_pitch_for ignored is_dma_available()."
5231 );
5232 }
5233 }
5234
5235 fn make_grey_png(width: u32, height: u32) -> Vec<u8> {
5239 let mut bytes = Vec::with_capacity((width * height) as usize);
5240 for y in 0..height {
5241 for x in 0..width {
5242 bytes.push(((x + y) & 0xFF) as u8);
5243 }
5244 }
5245 let img = image::GrayImage::from_vec(width, height, bytes).unwrap();
5246 let mut buf = Vec::new();
5247 img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)
5248 .unwrap();
5249 buf
5250 }
5251
5252 #[test]
5257 #[cfg(target_os = "linux")]
5258 fn test_load_png_grey_misaligned_width_dma() {
5259 use edgefirst_tensor::is_dma_available;
5260 if !is_dma_available() {
5261 crate::test_support::report_skip(
5262 "test_load_png_grey_misaligned_width_dma — DMA not available",
5263 );
5264 return;
5265 }
5266 let png = make_grey_png(612, 388);
5267 let loaded = crate::load_image_test_helper(&png, Some(PixelFormat::Grey), None).unwrap();
5268 assert_eq!(loaded.width(), Some(612));
5269 assert_eq!(loaded.height(), Some(388));
5270 assert_eq!(loaded.format(), Some(PixelFormat::Grey));
5271
5272 let map = loaded.as_typed::<u8>().unwrap().map().unwrap();
5275 let stride = loaded.row_stride().unwrap_or(612);
5276 assert!(stride >= 612);
5277 let bytes: &[u8] = ↦
5278 for y in 0..388usize {
5279 for x in 0..612usize {
5280 let expected = ((x + y) & 0xFF) as u8;
5281 let got = bytes[y * stride + x];
5282 assert_eq!(
5283 got, expected,
5284 "grey png mismatch at ({x},{y}): got {got} expected {expected}"
5285 );
5286 }
5287 }
5288 }
5289
5290 #[test]
5294 fn test_load_png_grey_mem() {
5295 use edgefirst_tensor::TensorMemory;
5296 let png = make_grey_png(612, 100);
5297 let loaded =
5298 crate::load_image_test_helper(&png, Some(PixelFormat::Grey), Some(TensorMemory::Mem))
5299 .unwrap();
5300 assert_eq!(loaded.width(), Some(612));
5301 assert_eq!(loaded.height(), Some(100));
5302 assert_eq!(loaded.format(), Some(PixelFormat::Grey));
5303 let map = loaded.as_typed::<u8>().unwrap().map().unwrap();
5304 let bytes: &[u8] = ↦
5305 assert_eq!(bytes.len(), 612 * 100);
5307 for y in 0..100 {
5308 for x in 0..612 {
5309 assert_eq!(bytes[y * 612 + x], ((x + y) & 0xFF) as u8);
5310 }
5311 }
5312 }
5313
5314 #[test]
5318 fn test_load_png_grey_to_rgb_mem() {
5319 use edgefirst_tensor::TensorMemory;
5320 let png = make_grey_png(620, 240);
5321 let loaded =
5322 crate::load_image_test_helper(&png, Some(PixelFormat::Rgb), Some(TensorMemory::Mem))
5323 .unwrap();
5324 assert_eq!(loaded.width(), Some(620));
5325 assert_eq!(loaded.height(), Some(240));
5326 assert_eq!(loaded.format(), Some(PixelFormat::Rgb));
5327
5328 let map = loaded.as_typed::<u8>().unwrap().map().unwrap();
5330 let bytes: &[u8] = ↦
5331 for (x, y) in [(0usize, 0usize), (100, 50), (619, 239)] {
5332 let expected = ((x + y) & 0xFF) as u8;
5333 let off = (y * 620 + x) * 3;
5334 assert_eq!(bytes[off], expected, "R@{x},{y}");
5335 assert_eq!(bytes[off + 1], expected, "G@{x},{y}");
5336 assert_eq!(bytes[off + 2], expected, "B@{x},{y}");
5337 }
5338 }
5339
5340 #[test]
5341 #[cfg(target_os = "linux")]
5342 fn test_g2d_resize() {
5343 if !is_g2d_available() {
5344 crate::test_support::report_skip(
5345 "test_g2d_resize - G2D library (libg2d.so.2) not available",
5346 );
5347 return;
5348 }
5349 if !is_dma_available() {
5350 crate::test_support::report_skip("test_g2d_resize - DMA memory allocation not available (permission denied or no DMA-BUF support)");
5351 return;
5352 }
5353
5354 let dst_width = 640;
5355 let dst_height = 360;
5356 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5357 let src = crate::load_image_test_helper(
5358 &file,
5359 Some(PixelFormat::Rgba),
5360 Some(TensorMemory::DmaBuf),
5361 )
5362 .unwrap();
5363
5364 let g2d_dst = TensorDyn::image(
5365 dst_width,
5366 dst_height,
5367 PixelFormat::Rgba,
5368 DType::U8,
5369 Some(TensorMemory::DmaBuf),
5370 edgefirst_tensor::CpuAccess::ReadWrite,
5371 )
5372 .unwrap();
5373 let mut g2d_converter = G2DProcessor::new().unwrap();
5374 let (result, src, g2d_dst) = convert_img(
5375 &mut g2d_converter,
5376 src,
5377 g2d_dst,
5378 Rotation::None,
5379 Flip::None,
5380 Crop::no_crop(),
5381 );
5382 result.unwrap();
5383
5384 let cpu_dst = TensorDyn::image(
5385 dst_width,
5386 dst_height,
5387 PixelFormat::Rgba,
5388 DType::U8,
5389 None,
5390 edgefirst_tensor::CpuAccess::ReadWrite,
5391 )
5392 .unwrap();
5393 let mut cpu_converter = CPUProcessor::new();
5394 let (result, _src, cpu_dst) = convert_img(
5395 &mut cpu_converter,
5396 src,
5397 cpu_dst,
5398 Rotation::None,
5399 Flip::None,
5400 Crop::no_crop(),
5401 );
5402 result.unwrap();
5403
5404 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
5410 }
5411
5412 #[test]
5413 #[cfg(any(
5414 target_os = "linux",
5415 target_os = "macos",
5416 target_os = "ios",
5417 target_os = "android",
5418 target_os = "windows"
5419 ))]
5420 #[cfg(feature = "opengl")]
5421 fn test_opengl_resize() {
5422 if !is_opengl_available() {
5423 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
5424 return;
5425 }
5426
5427 let dst_width = 640;
5428 let dst_height = 360;
5429 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5430 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5431
5432 let cpu_dst = TensorDyn::image(
5433 dst_width,
5434 dst_height,
5435 PixelFormat::Rgba,
5436 DType::U8,
5437 None,
5438 edgefirst_tensor::CpuAccess::ReadWrite,
5439 )
5440 .unwrap();
5441 let mut cpu_converter = CPUProcessor::new();
5442 let (result, src, cpu_dst) = convert_img(
5443 &mut cpu_converter,
5444 src,
5445 cpu_dst,
5446 Rotation::None,
5447 Flip::None,
5448 Crop::no_crop(),
5449 );
5450 result.unwrap();
5451
5452 let mut src = src;
5453 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5454
5455 for _ in 0..5 {
5456 let gl_dst = TensorDyn::image(
5457 dst_width,
5458 dst_height,
5459 PixelFormat::Rgba,
5460 DType::U8,
5461 None,
5462 edgefirst_tensor::CpuAccess::ReadWrite,
5463 )
5464 .unwrap();
5465 let (result, src_back, gl_dst) = convert_img(
5466 &mut gl_converter,
5467 src,
5468 gl_dst,
5469 Rotation::None,
5470 Flip::None,
5471 Crop::no_crop(),
5472 );
5473 result.unwrap();
5474 src = src_back;
5475
5476 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5477 }
5478 }
5479
5480 #[test]
5481 #[cfg(any(
5482 target_os = "linux",
5483 target_os = "macos",
5484 target_os = "ios",
5485 target_os = "android",
5486 target_os = "windows"
5487 ))]
5488 #[cfg(feature = "opengl")]
5489 fn test_opengl_10_threads() {
5490 if !is_opengl_available() {
5491 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
5492 return;
5493 }
5494
5495 let handles: Vec<_> = (0..10)
5496 .map(|i| {
5497 std::thread::Builder::new()
5498 .name(format!("Thread {i}"))
5499 .spawn(test_opengl_resize)
5500 .unwrap()
5501 })
5502 .collect();
5503 handles.into_iter().for_each(|h| {
5504 if let Err(e) = h.join() {
5505 std::panic::resume_unwind(e)
5506 }
5507 });
5508 }
5509
5510 #[test]
5511 #[cfg(any(
5512 target_os = "linux",
5513 target_os = "macos",
5514 target_os = "ios",
5515 target_os = "android",
5516 target_os = "windows"
5517 ))]
5518 #[cfg(feature = "opengl")]
5519 fn test_opengl_grey() {
5520 if !is_opengl_available() {
5521 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
5522 return;
5523 }
5524
5525 let img = crate::load_image_test_helper(
5526 &edgefirst_bench::testdata::read("grey.jpg"),
5527 Some(PixelFormat::Grey),
5528 None,
5529 )
5530 .unwrap();
5531
5532 let gl_dst = TensorDyn::image(
5533 640,
5534 640,
5535 PixelFormat::Grey,
5536 DType::U8,
5537 None,
5538 edgefirst_tensor::CpuAccess::ReadWrite,
5539 )
5540 .unwrap();
5541 let cpu_dst = TensorDyn::image(
5542 640,
5543 640,
5544 PixelFormat::Grey,
5545 DType::U8,
5546 None,
5547 edgefirst_tensor::CpuAccess::ReadWrite,
5548 )
5549 .unwrap();
5550
5551 let mut converter = CPUProcessor::new();
5552
5553 let (result, img, cpu_dst) = convert_img(
5554 &mut converter,
5555 img,
5556 cpu_dst,
5557 Rotation::None,
5558 Flip::None,
5559 Crop::no_crop(),
5560 );
5561 result.unwrap();
5562
5563 let mut gl = GLProcessorThreaded::new(None).unwrap();
5564 let (result, _img, gl_dst) = convert_img(
5565 &mut gl,
5566 img,
5567 gl_dst,
5568 Rotation::None,
5569 Flip::None,
5570 Crop::no_crop(),
5571 );
5572 result.unwrap();
5573
5574 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5575 }
5576
5577 #[test]
5578 #[cfg(target_os = "linux")]
5579 fn test_g2d_src_crop() {
5580 if !is_g2d_available() {
5581 crate::test_support::report_skip(
5582 "test_g2d_src_crop - G2D library (libg2d.so.2) not available",
5583 );
5584 return;
5585 }
5586 if !is_dma_available() {
5587 crate::test_support::report_skip("test_g2d_src_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)");
5588 return;
5589 }
5590
5591 let dst_width = 640;
5592 let dst_height = 640;
5593 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5594 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5595
5596 let cpu_dst = TensorDyn::image(
5597 dst_width,
5598 dst_height,
5599 PixelFormat::Rgba,
5600 DType::U8,
5601 None,
5602 edgefirst_tensor::CpuAccess::ReadWrite,
5603 )
5604 .unwrap();
5605 let mut cpu_converter = CPUProcessor::new();
5606 let crop = Crop::new().with_source(Some(Region::new(0, 0, 640, 360)));
5607 let (result, src, cpu_dst) = convert_img(
5608 &mut cpu_converter,
5609 src,
5610 cpu_dst,
5611 Rotation::None,
5612 Flip::None,
5613 crop,
5614 );
5615 result.unwrap();
5616
5617 let g2d_dst = TensorDyn::image(
5618 dst_width,
5619 dst_height,
5620 PixelFormat::Rgba,
5621 DType::U8,
5622 None,
5623 edgefirst_tensor::CpuAccess::ReadWrite,
5624 )
5625 .unwrap();
5626 let mut g2d_converter = G2DProcessor::new().unwrap();
5627 let (result, _src, g2d_dst) = convert_img(
5628 &mut g2d_converter,
5629 src,
5630 g2d_dst,
5631 Rotation::None,
5632 Flip::None,
5633 crop,
5634 );
5635 result.unwrap();
5636
5637 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
5643 }
5644
5645 #[test]
5646 #[cfg(target_os = "linux")]
5647 fn test_g2d_dst_crop() {
5648 if !is_g2d_available() {
5649 crate::test_support::report_skip(
5650 "test_g2d_dst_crop - G2D library (libg2d.so.2) not available",
5651 );
5652 return;
5653 }
5654 if !is_dma_available() {
5655 crate::test_support::report_skip("test_g2d_dst_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)");
5656 return;
5657 }
5658
5659 let dst_width = 640;
5660 let dst_height = 640;
5661 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5662 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5663
5664 let cpu_dst = TensorDyn::image(
5665 dst_width,
5666 dst_height,
5667 PixelFormat::Rgba,
5668 DType::U8,
5669 None,
5670 edgefirst_tensor::CpuAccess::ReadWrite,
5671 )
5672 .unwrap();
5673 let mut cpu_converter = CPUProcessor::new();
5674 let crop = Crop::new();
5675 let (result, src, cpu_dst) = convert_img(
5676 &mut cpu_converter,
5677 src,
5678 cpu_dst,
5679 Rotation::None,
5680 Flip::None,
5681 crop,
5682 );
5683 result.unwrap();
5684
5685 let g2d_dst = TensorDyn::image(
5686 dst_width,
5687 dst_height,
5688 PixelFormat::Rgba,
5689 DType::U8,
5690 None,
5691 edgefirst_tensor::CpuAccess::ReadWrite,
5692 )
5693 .unwrap();
5694 let mut g2d_converter = G2DProcessor::new().unwrap();
5695 let (result, _src, g2d_dst) = convert_img(
5696 &mut g2d_converter,
5697 src,
5698 g2d_dst,
5699 Rotation::None,
5700 Flip::None,
5701 crop,
5702 );
5703 result.unwrap();
5704
5705 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
5711 }
5712
5713 #[test]
5714 #[cfg(target_os = "linux")]
5715 fn test_g2d_all_rgba() {
5716 if !is_g2d_available() {
5717 crate::test_support::report_skip(
5718 "test_g2d_all_rgba - G2D library (libg2d.so.2) not available",
5719 );
5720 return;
5721 }
5722 if !is_dma_available() {
5723 crate::test_support::report_skip("test_g2d_all_rgba - DMA memory allocation not available (permission denied or no DMA-BUF support)");
5724 return;
5725 }
5726
5727 let dst_width = 640;
5728 let dst_height = 640;
5729 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5730 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5731 let src_dyn = src;
5732
5733 let mut cpu_dst = TensorDyn::image(
5734 dst_width,
5735 dst_height,
5736 PixelFormat::Rgba,
5737 DType::U8,
5738 None,
5739 edgefirst_tensor::CpuAccess::ReadWrite,
5740 )
5741 .unwrap();
5742 let mut cpu_converter = CPUProcessor::new();
5743 let mut g2d_dst = TensorDyn::image(
5744 dst_width,
5745 dst_height,
5746 PixelFormat::Rgba,
5747 DType::U8,
5748 None,
5749 edgefirst_tensor::CpuAccess::ReadWrite,
5750 )
5751 .unwrap();
5752 let mut g2d_converter = G2DProcessor::new().unwrap();
5753
5754 let crop = Crop::new().with_source(Some(Region::new(50, 120, 1024, 576)));
5755
5756 for rot in [
5757 Rotation::None,
5758 Rotation::Clockwise90,
5759 Rotation::Rotate180,
5760 Rotation::CounterClockwise90,
5761 ] {
5762 cpu_dst
5763 .as_typed::<u8>()
5764 .unwrap()
5765 .map()
5766 .unwrap()
5767 .as_mut_slice()
5768 .fill(114);
5769 g2d_dst
5770 .as_typed::<u8>()
5771 .unwrap()
5772 .map()
5773 .unwrap()
5774 .as_mut_slice()
5775 .fill(114);
5776 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
5777 let mut cpu_dst_dyn = cpu_dst;
5778 cpu_converter
5779 .convert(&src_dyn, &mut cpu_dst_dyn, Rotation::None, Flip::None, crop)
5780 .unwrap();
5781 cpu_dst = {
5782 let mut __t = cpu_dst_dyn.into_u8().unwrap();
5783 __t.set_format(PixelFormat::Rgba).unwrap();
5784 TensorDyn::from(__t)
5785 };
5786
5787 let mut g2d_dst_dyn = g2d_dst;
5788 g2d_converter
5789 .convert(&src_dyn, &mut g2d_dst_dyn, Rotation::None, Flip::None, crop)
5790 .unwrap();
5791 g2d_dst = {
5792 let mut __t = g2d_dst_dyn.into_u8().unwrap();
5793 __t.set_format(PixelFormat::Rgba).unwrap();
5794 TensorDyn::from(__t)
5795 };
5796
5797 compare_images(
5798 &g2d_dst,
5799 &cpu_dst,
5800 0.98,
5801 &format!("{} {:?} {:?}", function!(), rot, flip),
5802 );
5803 }
5804 }
5805 }
5806
5807 #[test]
5808 #[cfg(any(
5809 target_os = "linux",
5810 target_os = "macos",
5811 target_os = "ios",
5812 target_os = "android",
5813 target_os = "windows"
5814 ))]
5815 #[cfg(feature = "opengl")]
5816 fn test_opengl_src_crop() {
5817 if !is_opengl_available() {
5818 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
5819 return;
5820 }
5821
5822 let dst_width = 640;
5823 let dst_height = 360;
5824 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5825 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5826 let crop = Crop::new().with_source(Some(Region::new(320, 180, 1280 - 320, 720 - 180)));
5827
5828 let cpu_dst = TensorDyn::image(
5829 dst_width,
5830 dst_height,
5831 PixelFormat::Rgba,
5832 DType::U8,
5833 None,
5834 edgefirst_tensor::CpuAccess::ReadWrite,
5835 )
5836 .unwrap();
5837 let mut cpu_converter = CPUProcessor::new();
5838 let (result, src, cpu_dst) = convert_img(
5839 &mut cpu_converter,
5840 src,
5841 cpu_dst,
5842 Rotation::None,
5843 Flip::None,
5844 crop,
5845 );
5846 result.unwrap();
5847
5848 let gl_dst = TensorDyn::image(
5849 dst_width,
5850 dst_height,
5851 PixelFormat::Rgba,
5852 DType::U8,
5853 None,
5854 edgefirst_tensor::CpuAccess::ReadWrite,
5855 )
5856 .unwrap();
5857 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5858 let (result, _src, gl_dst) = convert_img(
5859 &mut gl_converter,
5860 src,
5861 gl_dst,
5862 Rotation::None,
5863 Flip::None,
5864 crop,
5865 );
5866 result.unwrap();
5867
5868 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5869 }
5870
5871 #[test]
5872 #[cfg(any(
5873 target_os = "linux",
5874 target_os = "macos",
5875 target_os = "ios",
5876 target_os = "android",
5877 target_os = "windows"
5878 ))]
5879 #[cfg(feature = "opengl")]
5880 fn test_opengl_dst_crop() {
5881 if !is_opengl_available() {
5882 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
5883 return;
5884 }
5885
5886 let dst_width = 640;
5887 let dst_height = 640;
5888 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5889 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5890
5891 let cpu_dst = TensorDyn::image(
5892 dst_width,
5893 dst_height,
5894 PixelFormat::Rgba,
5895 DType::U8,
5896 None,
5897 edgefirst_tensor::CpuAccess::ReadWrite,
5898 )
5899 .unwrap();
5900 let mut cpu_converter = CPUProcessor::new();
5901 let crop = Crop::new();
5902 let (result, src, cpu_dst) = convert_img(
5903 &mut cpu_converter,
5904 src,
5905 cpu_dst,
5906 Rotation::None,
5907 Flip::None,
5908 crop,
5909 );
5910 result.unwrap();
5911
5912 let gl_dst = TensorDyn::image(
5913 dst_width,
5914 dst_height,
5915 PixelFormat::Rgba,
5916 DType::U8,
5917 None,
5918 edgefirst_tensor::CpuAccess::ReadWrite,
5919 )
5920 .unwrap();
5921 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5922 let (result, _src, gl_dst) = convert_img(
5923 &mut gl_converter,
5924 src,
5925 gl_dst,
5926 Rotation::None,
5927 Flip::None,
5928 crop,
5929 );
5930 result.unwrap();
5931
5932 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5933 }
5934 #[cfg(all(target_os = "windows", feature = "opengl"))]
5938 fn synthetic_rgba(w: usize, h: usize) -> TensorDyn {
5939 let t = TensorDyn::image(
5940 w,
5941 h,
5942 PixelFormat::Rgba,
5943 DType::U8,
5944 Some(TensorMemory::Mem),
5945 edgefirst_tensor::CpuAccess::ReadWrite,
5946 )
5947 .unwrap();
5948 fill_synthetic_rgba(&t);
5949 t
5950 }
5951
5952 #[cfg(all(target_os = "windows", feature = "opengl"))]
5957 fn assert_pixels_identical(actual: &TensorDyn, expected: &TensorDyn, name: &str) {
5958 assert_eq!(actual.shape(), expected.shape(), "{name}: shapes differ");
5959 let flat = |t: &TensorDyn| {
5960 let typed = t.as_typed::<u8>().expect("u8 image");
5961 let mut out = vec![0u8; t.shape().iter().product::<usize>()];
5962 typed.copy_to_flat(&mut out).expect("compact the rows");
5963 out
5964 };
5965 let (a, e) = (flat(actual), flat(expected));
5966 let mismatches = a.iter().zip(&e).filter(|(x, y)| x != y).count();
5967 assert_eq!(
5968 mismatches,
5969 0,
5970 "{name}: {mismatches} of {} bytes differ (first rows: {:?} vs {:?})",
5971 a.len(),
5972 &a[..16.min(a.len())],
5973 &e[..16.min(e.len())]
5974 );
5975 }
5976
5977 #[cfg(all(target_os = "windows", feature = "opengl"))]
5980 fn fill_synthetic_rgba(t: &TensorDyn) {
5981 let w = t.width().unwrap();
5982 let h = t.height().unwrap();
5983 let bound = t.as_typed::<u8>().unwrap();
5984 let stride = bound.effective_row_stride().unwrap_or(w * 4);
5985 let mut m = bound.map().unwrap();
5986 let px = m.as_mut_slice();
5987 for y in 0..h {
5988 for x in 0..w {
5989 let o = y * stride + x * 4;
5990 px[o] = x as u8;
5991 px[o + 1] = y as u8;
5992 px[o + 2] = (x + y) as u8;
5993 px[o + 3] = 255;
5994 }
5995 }
5996 }
5997
5998 #[cfg(all(target_os = "windows", feature = "opengl"))]
6007 fn compare_float_images(a: &TensorDyn, b: &TensorDyn, tol: f64, what: &str) {
6008 assert_eq!(a.format(), b.format(), "{what}: pixel formats differ");
6009 assert_eq!(a.dtype(), b.dtype(), "{what}: dtypes differ");
6010 assert_eq!(a.shape(), b.shape(), "{what}: shapes differ");
6011 let values = |t: &TensorDyn| -> Vec<f64> {
6012 let elems: usize = t.shape().iter().product();
6013 match t.dtype() {
6014 DType::F32 => {
6015 let typed = t.as_typed::<f32>().expect("F32 image");
6016 let mut bytes = vec![0u8; elems * 4];
6017 typed
6018 .copy_to_flat(&mut bytes)
6019 .expect("compact the padded rows");
6020 bytes
6021 .as_chunks::<4>()
6022 .0
6023 .iter()
6024 .map(|c| f32::from_le_bytes(*c) as f64)
6025 .collect()
6026 }
6027 DType::F16 => {
6028 let typed = t.as_typed::<half::f16>().expect("F16 image");
6029 let mut bytes = vec![0u8; elems * 2];
6030 typed
6031 .copy_to_flat(&mut bytes)
6032 .expect("compact the padded rows");
6033 bytes
6034 .as_chunks::<2>()
6035 .0
6036 .iter()
6037 .map(|c| half::f16::from_le_bytes(*c).to_f64())
6038 .collect()
6039 }
6040 other => panic!("{what}: {other:?} is not a float dtype"),
6041 }
6042 };
6043 let (av, bv) = (values(a), values(b));
6044 assert_eq!(av.len(), bv.len(), "{what}: element counts differ");
6045 for (i, (x, y)) in av.iter().zip(bv.iter()).enumerate() {
6046 assert!(
6047 (x - y).abs() <= tol,
6048 "{what}: element {i} differs by {} (gpu {x}, cpu {y}, tol {tol})",
6049 (x - y).abs()
6050 );
6051 }
6052 }
6053
6054 #[test]
6077 #[cfg(all(target_os = "windows", feature = "opengl"))]
6078 fn windows_padded_float_pbo_rows_land_at_the_declared_stride() {
6079 if !is_opengl_available() {
6080 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
6081 return;
6082 }
6083 const W: usize = 64;
6084 const H: usize = 32;
6085 const POOL_W: usize = 96;
6086
6087 let mut gpu = ImageProcessor::new().unwrap();
6088 let support = gpu.supported_render_dtypes();
6089 let src = synthetic_rgba(W, H);
6090
6091 let cases: [(PixelFormat, DType, bool, &[usize]); 2] = [
6093 (
6094 PixelFormat::PlanarRgb,
6095 DType::F16,
6096 support.f16,
6097 &[W * 2, POOL_W * 2, W * 2 + 4],
6100 ),
6101 (
6102 PixelFormat::Rgb,
6103 DType::F32,
6104 support.f32,
6105 &[W * 3 * 4, POOL_W * 3 * 4],
6106 ),
6107 ];
6108
6109 for (fmt, dtype, supported, strides) in cases {
6110 if !supported {
6111 crate::test_support::report_skip(&format!(
6112 "{} - {fmt:?}/{dtype:?} not reported",
6113 function!()
6114 ));
6115 continue;
6116 }
6117 let Some(gl) = gpu.opengl.as_ref() else {
6118 crate::test_support::report_skip(&format!("{} - no GL processor", function!()));
6119 return;
6120 };
6121 let mut reference = match gl.create_pbo_image_dtype(W, H, fmt, dtype) {
6123 Ok(t) => t,
6124 Err(e) => {
6125 crate::test_support::report_skip(&format!(
6126 "{} - no float PBO images: {e:?}",
6127 function!()
6128 ));
6129 return;
6130 }
6131 };
6132 assert_eq!(
6133 reference.memory(),
6134 TensorMemory::Pbo,
6135 "{fmt:?}/{dtype:?}: the destination must be a PBO to reach this readback"
6136 );
6137 gpu.convert(
6138 &src,
6139 &mut reference,
6140 Rotation::None,
6141 Flip::None,
6142 Crop::default(),
6143 )
6144 .unwrap_or_else(|e| panic!("{fmt:?}/{dtype:?}: tight convert failed: {e}"));
6145
6146 for &stride in strides {
6147 let label = format!("{fmt:?}/{dtype:?} at a {stride} B pitch");
6148 let mut padded = gpu
6151 .opengl
6152 .as_ref()
6153 .expect("GL processor")
6154 .create_pbo_image_dtype(POOL_W, H, fmt, dtype)
6155 .unwrap_or_else(|e| panic!("{label}: pool PBO: {e:?}"));
6156 padded
6157 .configure_image(W, H, fmt)
6158 .unwrap_or_else(|e| panic!("{label}: narrow the pool tensor: {e}"));
6159 padded
6160 .set_row_stride(stride)
6161 .unwrap_or_else(|e| panic!("{label}: declare the pitch: {e}"));
6162 assert_eq!(padded.effective_row_stride(), Some(stride), "{label}");
6163
6164 let before = gpu.convert_fallback_count();
6165 gpu.convert(
6166 &src,
6167 &mut padded,
6168 Rotation::None,
6169 Flip::None,
6170 Crop::default(),
6171 )
6172 .unwrap_or_else(|e| panic!("{label}: padded convert failed: {e}"));
6173 assert_eq!(
6176 gpu.convert_fallback_count(),
6177 before,
6178 "{label}: fell back to the CPU backend, so the float PBO readback under test never ran"
6179 );
6180
6181 let tol = if dtype == DType::F16 {
6182 1.0 / 1024.0
6183 } else {
6184 1e-6
6185 };
6186 compare_float_images(&padded, &reference, tol, &label);
6187 }
6188 }
6189 }
6190
6191 #[test]
6201 #[cfg(all(target_os = "windows", feature = "opengl"))]
6202 fn windows_float_destinations_match_the_cpu_converter_for_every_layout() {
6203 if !is_opengl_available() {
6204 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
6205 return;
6206 }
6207 if !edgefirst_tensor::is_gpu_buffer_available() {
6208 crate::test_support::report_skip(&format!(
6209 "{} - no zero-copy GPU buffers",
6210 function!()
6211 ));
6212 return;
6213 }
6214 let mut gpu = ImageProcessor::new().unwrap();
6215 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
6216 backend: ComputeBackend::Cpu,
6217 ..Default::default()
6218 })
6219 .unwrap();
6220 let support = gpu.supported_render_dtypes();
6221 eprintln!("{}: supported_render_dtypes = {support:?}", function!());
6222 let src = synthetic_rgba(640, 480);
6223 for (fmt, dtype, need) in [
6224 (PixelFormat::PlanarRgb, DType::F16, support.f16),
6225 (PixelFormat::PlanarRgb, DType::F32, support.f32),
6226 (PixelFormat::PlanarRgba, DType::F16, support.f16),
6227 (PixelFormat::PlanarRgba, DType::F32, support.f32),
6228 (PixelFormat::Rgb, DType::F16, support.f16),
6229 (PixelFormat::Rgb, DType::F32, support.f32),
6230 (PixelFormat::Rgba, DType::F16, support.f16),
6231 (PixelFormat::Rgba, DType::F32, support.f32),
6232 ] {
6233 if !need {
6234 eprintln!("{}: {fmt:?}/{dtype:?} not reported", function!());
6235 continue;
6236 }
6237 let mut dst_gpu = gpu
6238 .create_image(
6239 640,
6240 480,
6241 fmt,
6242 dtype,
6243 Some(TensorMemory::DmaBuf),
6244 edgefirst_tensor::CpuAccess::ReadWrite,
6245 )
6246 .unwrap();
6247 assert_eq!(
6248 dst_gpu.memory(),
6249 TensorMemory::DmaBuf,
6250 "{fmt:?}/{dtype:?} must be a texture"
6251 );
6252 let mut dst_cpu = TensorDyn::image(
6253 640,
6254 480,
6255 fmt,
6256 dtype,
6257 Some(TensorMemory::Mem),
6258 edgefirst_tensor::CpuAccess::ReadWrite,
6259 )
6260 .unwrap();
6261 let before = gpu.convert_fallback_count();
6262 gpu.convert(
6263 &src,
6264 &mut dst_gpu,
6265 Rotation::None,
6266 Flip::None,
6267 Crop::default(),
6268 )
6269 .unwrap();
6270 cpu.convert(
6271 &src,
6272 &mut dst_cpu,
6273 Rotation::None,
6274 Flip::None,
6275 Crop::default(),
6276 )
6277 .unwrap();
6278 assert_eq!(
6279 gpu.convert_fallback_count(),
6280 before,
6281 "{fmt:?}/{dtype:?} fell back to CPU"
6282 );
6283 let tol = if dtype == DType::F16 {
6284 1.0 / 1024.0
6285 } else {
6286 1e-6
6287 };
6288 compare_float_images(&dst_gpu, &dst_cpu, tol, &format!("{fmt:?}/{dtype:?}"));
6289 }
6290 }
6291
6292 #[cfg(target_os = "windows")]
6295 #[link(name = "kernel32")]
6296 extern "system" {
6297 fn WaitForSingleObject(handle: *mut std::ffi::c_void, milliseconds: u32) -> u32;
6298 }
6299
6300 #[cfg(all(target_os = "windows", feature = "opengl"))]
6309 #[test]
6310 fn windows_deferred_batch_records_one_value_per_tile_and_submits_once() {
6311 use edgefirst_tensor::CpuAccess;
6312
6313 if !edgefirst_tensor::is_gpu_buffer_available() {
6314 crate::test_support::report_skip(&format!(
6315 "{} - no zero-copy GPU buffers",
6316 function!()
6317 ));
6318 return;
6319 }
6320 let mut p = match ImageProcessor::new() {
6321 Ok(p) => p,
6322 Err(e) => {
6323 crate::test_support::report_skip(&format!(
6324 "{} - ImageProcessor init failed ({e:?})",
6325 function!()
6326 ));
6327 return;
6328 }
6329 };
6330 let (w, h, tiles) = (640usize, 480usize, 4usize);
6331 let src = synthetic_rgba(w, h);
6332 let mut dsts: Vec<TensorDyn> = (0..tiles)
6333 .map(|_| {
6334 p.create_image(w, h, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6335 .unwrap()
6336 })
6337 .collect();
6338 if dsts[0].memory() != TensorMemory::DmaBuf {
6339 crate::test_support::report_skip(&format!(
6340 "{} - destinations are not textures",
6341 function!()
6342 ));
6343 return;
6344 }
6345 for dst in &mut dsts {
6346 p.convert_deferred(&src, dst, Rotation::None, Flip::None, Crop::default())
6347 .unwrap();
6348 }
6349 let values: Vec<u64> = dsts
6350 .iter()
6351 .map(|d| {
6352 d.gpu_completion()
6353 .unwrap()
6354 .expect("each deferred convert records its own completion")
6355 .value
6356 })
6357 .collect();
6358 for pair in values.windows(2) {
6359 assert!(
6360 pair[1] > pair[0],
6361 "the batch allocated {values:?}, which is not one increasing value per tile"
6362 );
6363 }
6364 p.flush().unwrap();
6365 let dev = edgefirst_tensor::d3d11::device().unwrap();
6368 let last = *values.last().unwrap();
6369 let ev = dev.event_for(last).unwrap();
6370 assert_eq!(
6373 unsafe {
6374 WaitForSingleObject(
6375 std::os::windows::io::AsRawHandle::as_raw_handle(&ev) as _,
6376 5000,
6377 )
6378 },
6379 0,
6380 "the batch's newest value {last} never arrived after flush"
6381 );
6382
6383 let mut solo = p
6384 .create_image(w, h, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6385 .unwrap();
6386 p.convert(&src, &mut solo, Rotation::None, Flip::None, Crop::default())
6387 .unwrap();
6388 let flat = |img: &TensorDyn| -> Vec<u8> {
6392 let t = img.as_typed::<u8>().expect("u8 image");
6393 let mut out = vec![0u8; t.shape().iter().product::<usize>()];
6394 t.copy_to_flat(&mut out).expect("compact the padded rows");
6395 out
6396 };
6397 let want = flat(&solo);
6398 for (i, dst) in dsts.iter().enumerate() {
6399 assert_eq!(
6400 flat(dst),
6401 want,
6402 "tile {i} does not match a standalone convert"
6403 );
6404 }
6405 }
6406
6407 #[cfg(all(target_os = "windows", feature = "opengl"))]
6416 #[test]
6417 fn windows_dropping_a_processor_submits_an_open_batch() {
6418 use edgefirst_tensor::CpuAccess;
6419
6420 if !edgefirst_tensor::is_gpu_buffer_available() {
6421 crate::test_support::report_skip(&format!(
6422 "{} - no zero-copy GPU buffers",
6423 function!()
6424 ));
6425 return;
6426 }
6427 let src = synthetic_rgba(320, 240);
6428 let recorded = {
6429 let mut p = match ImageProcessor::new() {
6430 Ok(p) => p,
6431 Err(e) => {
6432 crate::test_support::report_skip(&format!(
6433 "{} - init failed ({e:?})",
6434 function!()
6435 ));
6436 return;
6437 }
6438 };
6439 let mut dst = p
6440 .create_image(
6441 320,
6442 240,
6443 PixelFormat::Rgba,
6444 DType::U8,
6445 None,
6446 CpuAccess::Read,
6447 )
6448 .unwrap();
6449 if dst.memory() != TensorMemory::DmaBuf {
6450 crate::test_support::report_skip(&format!(
6451 "{} - destination is not a texture",
6452 function!()
6453 ));
6454 return;
6455 }
6456 p.convert_deferred(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
6457 .unwrap();
6458 dst.gpu_completion()
6460 .unwrap()
6461 .expect("the deferred convert recorded a completion")
6462 .value
6463 };
6464
6465 let dev = edgefirst_tensor::d3d11::device().unwrap();
6466 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
6467 while dev.completed_value() < recorded {
6468 assert!(
6469 std::time::Instant::now() < deadline,
6470 "the dropped processor never submitted its batch: fence at {}, recorded {recorded}",
6471 dev.completed_value()
6472 );
6473 std::thread::yield_now();
6474 }
6475 }
6476
6477 #[cfg(all(target_os = "windows", feature = "opengl"))]
6482 #[test]
6483 fn windows_create_image_prefers_textures_and_convert_with_fence_returns_a_set_event() {
6484 use edgefirst_tensor::CpuAccess;
6485 use std::os::windows::io::AsRawHandle;
6486
6487 if !edgefirst_tensor::is_gpu_buffer_available() {
6488 crate::test_support::report_skip(&format!(
6489 "{} - no zero-copy GPU buffers",
6490 function!()
6491 ));
6492 return;
6493 }
6494 let mut p = ImageProcessor::new().unwrap();
6495 let src = synthetic_rgba(320, 240);
6496 let mut dst = p
6497 .create_image(
6498 320,
6499 240,
6500 PixelFormat::Rgba,
6501 DType::U8,
6502 None,
6503 CpuAccess::Read,
6504 )
6505 .unwrap();
6506 if dst.memory() != TensorMemory::DmaBuf
6510 && !std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1")
6511 {
6512 crate::test_support::report_skip(&format!(
6513 "{} - create_image gave {:?}, not a texture (no GL backend)",
6514 function!(),
6515 dst.memory()
6516 ));
6517 return;
6518 }
6519 assert_eq!(
6520 dst.memory(),
6521 TensorMemory::DmaBuf,
6522 "create_image(None) must prefer a texture on Windows"
6523 );
6524 let before = dst.gpu_completion().unwrap().map(|c| c.value);
6527 let fence = p
6528 .convert_with_fence(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
6529 .unwrap()
6530 .expect("Windows exports a fence");
6531 let recorded = dst
6532 .gpu_completion()
6533 .unwrap()
6534 .expect("the convert recorded a completion")
6535 .value;
6536 assert!(
6537 before.is_none_or(|b| recorded > b),
6538 "the fenced convert recorded a newer value than {before:?}"
6539 );
6540 assert_eq!(
6545 edgefirst_tensor::d3d11::device().unwrap().last_signalled(),
6546 recorded,
6547 "a fenced convert must signal exactly once"
6548 );
6549 assert_eq!(
6554 unsafe { WaitForSingleObject(fence.as_raw_handle() as _, 5000) },
6555 0,
6556 "fence was not set within 5 seconds"
6557 );
6558 assert!(
6562 edgefirst_tensor::d3d11::device().unwrap().completed_value() >= recorded,
6563 "the event fired, so the recorded value {recorded} is complete"
6564 );
6565 assert_eq!(
6566 dst.gpu_completion().unwrap().map(|c| c.value),
6567 Some(recorded),
6568 "the destination still reports the value the event waited on"
6569 );
6570 let mut expected = TensorDyn::image(
6571 320,
6572 240,
6573 PixelFormat::Rgba,
6574 DType::U8,
6575 Some(TensorMemory::Mem),
6576 CpuAccess::ReadWrite,
6577 )
6578 .unwrap();
6579 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
6580 backend: ComputeBackend::Cpu,
6581 ..Default::default()
6582 })
6583 .unwrap();
6584 cpu.convert(
6585 &src,
6586 &mut expected,
6587 Rotation::None,
6588 Flip::None,
6589 Crop::default(),
6590 )
6591 .unwrap();
6592 compare_images(&dst, &expected, 0.0, "fenced convert");
6593 }
6594
6595 #[cfg(all(target_os = "windows", feature = "opengl"))]
6600 fn texture_or_skip(t: &TensorDyn, test: &str) -> bool {
6601 if t.memory() == TensorMemory::DmaBuf {
6602 return true;
6603 }
6604 if std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1") {
6605 panic!(
6606 "{test}: create_image(None) must prefer a texture on Windows, got {:?}",
6607 t.memory()
6608 );
6609 }
6610 crate::test_support::report_skip(&format!(
6611 "{test} - create_image gave {:?}, not a texture (no GL backend)",
6612 t.memory()
6613 ));
6614 false
6615 }
6616
6617 #[cfg(all(target_os = "windows", feature = "opengl"))]
6623 #[test]
6624 fn windows_draw_decoded_masks_into_a_texture_records_a_completion() {
6625 use edgefirst_tensor::{CpuAccess, Segmentation};
6626 use ndarray::Array3;
6627
6628 if !edgefirst_tensor::is_gpu_buffer_available() {
6629 crate::test_support::report_skip(&format!(
6630 "{} - no zero-copy GPU buffers",
6631 function!()
6632 ));
6633 return;
6634 }
6635 let mut p = ImageProcessor::new().unwrap();
6636 let mut dst = p
6637 .create_image(64, 64, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6638 .unwrap();
6639 if !texture_or_skip(&dst, function!()) {
6640 return;
6641 }
6642 assert!(
6643 dst.gpu_completion().unwrap().is_none(),
6644 "nothing has written this texture yet"
6645 );
6646
6647 let detect = DetectBox {
6648 bbox: [0.25, 0.25, 0.75, 0.75].into(),
6649 score: 0.99,
6650 label: 0,
6651 };
6652 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
6653 let seg = Segmentation {
6654 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
6655 .unwrap()
6656 .into(),
6657 xmin: 0.25,
6658 ymin: 0.25,
6659 xmax: 0.75,
6660 ymax: 0.75,
6661 };
6662 p.draw_decoded_masks(&mut dst, &[detect], &[seg], MaskOverlay::default())
6663 .expect("draw into a read-only texture");
6664
6665 let completion = dst
6666 .gpu_completion()
6667 .unwrap()
6668 .expect("the draw recorded no completion on dst");
6669 assert!(completion.value > 0, "completion value must be non-zero");
6670
6671 let mut flat = vec![0u8; 64 * 64 * 4];
6676 dst.as_typed::<u8>()
6677 .unwrap()
6678 .copy_to_flat(&mut flat)
6679 .expect("read the drawn texture");
6680 let px = |x: usize, y: usize| -> [u8; 4] {
6681 let o = (y * 64 + x) * 4;
6682 [flat[o], flat[o + 1], flat[o + 2], flat[o + 3]]
6683 };
6684 assert_eq!(px(2, 2), [0, 0, 0, 0], "corner (2,2) was not cleared");
6685 assert_ne!(px(32, 32), [0, 0, 0, 0], "centre (32,32) was not coloured");
6686 }
6687
6688 #[cfg(all(target_os = "windows", feature = "opengl"))]
6691 #[test]
6692 fn windows_draw_proto_masks_into_a_texture_records_a_completion() {
6693 use edgefirst_tensor::{CpuAccess, Tensor};
6694
6695 if !edgefirst_tensor::is_gpu_buffer_available() {
6696 crate::test_support::report_skip(&format!(
6697 "{} - no zero-copy GPU buffers",
6698 function!()
6699 ));
6700 return;
6701 }
6702 let mut p = match with_force_backend(Some("opengl"), ImageProcessor::new) {
6706 Ok(p) => p,
6707 Err(e) if !std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1") => {
6708 crate::test_support::report_skip(&format!(
6709 "{} - no OpenGL backend: {e}",
6710 function!()
6711 ));
6712 return;
6713 }
6714 Err(e) => panic!("forced OpenGL backend unavailable: {e}"),
6715 };
6716 let mut dst = p
6717 .create_image(64, 64, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6718 .unwrap();
6719 if !texture_or_skip(&dst, function!()) {
6720 return;
6721 }
6722
6723 let detect = DetectBox {
6724 bbox: [0.25, 0.25, 0.75, 0.75].into(),
6725 score: 0.99,
6726 label: 0,
6727 };
6728 let proto = ProtoData {
6731 mask_coefficients: TensorDyn::F32(
6732 Tensor::<f32>::from_slice(&[1.0_f32], &[1, 1]).unwrap(),
6733 ),
6734 protos: TensorDyn::F32(
6735 Tensor::<f32>::from_slice(&vec![1.0_f32; 8 * 8], &[8, 8, 1]).unwrap(),
6736 ),
6737 layout: ProtoLayout::Nhwc,
6738 };
6739 p.draw_proto_masks(&mut dst, &[detect], &proto, MaskOverlay::default())
6740 .expect("forced-GL proto draw into a read-only texture");
6741
6742 let completion = dst
6743 .gpu_completion()
6744 .unwrap()
6745 .expect("the proto draw recorded no completion on dst");
6746 assert!(completion.value > 0, "completion value must be non-zero");
6747 }
6748
6749 #[cfg(all(target_os = "windows", feature = "opengl"))]
6754 #[test]
6755 fn windows_draw_decoded_masks_composites_a_host_background_onto_a_texture() {
6756 use edgefirst_tensor::CpuAccess;
6757
6758 if !edgefirst_tensor::is_gpu_buffer_available() {
6759 crate::test_support::report_skip(&format!(
6760 "{} - no zero-copy GPU buffers",
6761 function!()
6762 ));
6763 return;
6764 }
6765 let mut p = ImageProcessor::new().unwrap();
6766 let bg = synthetic_rgba(64, 48);
6767 let mut dst = p
6768 .create_image(64, 48, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6769 .unwrap();
6770 if !texture_or_skip(&dst, function!()) {
6771 return;
6772 }
6773 p.draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::new().with_background(&bg))
6774 .expect("empty draw with a host background onto a texture");
6775 assert_pixels_identical(&dst, &bg, "host background onto a texture");
6776 assert!(dst.gpu_completion().unwrap().is_some());
6777 }
6778
6779 #[cfg(all(target_os = "windows", feature = "opengl"))]
6782 #[test]
6783 fn windows_draw_decoded_masks_composites_a_texture_background_onto_a_texture() {
6784 use edgefirst_tensor::CpuAccess;
6785
6786 if !edgefirst_tensor::is_gpu_buffer_available() {
6787 crate::test_support::report_skip(&format!(
6788 "{} - no zero-copy GPU buffers",
6789 function!()
6790 ));
6791 return;
6792 }
6793 let mut p = ImageProcessor::new().unwrap();
6794 let expected = synthetic_rgba(64, 48);
6795 let bg = p
6796 .create_image(
6797 64,
6798 48,
6799 PixelFormat::Rgba,
6800 DType::U8,
6801 None,
6802 CpuAccess::ReadWrite,
6803 )
6804 .unwrap();
6805 if !texture_or_skip(&bg, function!()) {
6806 return;
6807 }
6808 fill_synthetic_rgba(&bg);
6809 let mut dst = p
6810 .create_image(64, 48, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6811 .unwrap();
6812 assert_eq!(dst.memory(), TensorMemory::DmaBuf);
6813 p.draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::new().with_background(&bg))
6814 .expect("empty draw with a texture background onto a texture");
6815 assert_pixels_identical(&dst, &expected, "texture background onto a texture");
6816 assert!(dst.gpu_completion().unwrap().is_some());
6817 }
6818
6819 #[cfg(all(target_os = "windows", feature = "opengl"))]
6825 #[test]
6826 fn windows_draw_decoded_masks_into_a_texture_view_fills_only_its_band() {
6827 use edgefirst_tensor::CpuAccess;
6828
6829 if !edgefirst_tensor::is_gpu_buffer_available() {
6830 crate::test_support::report_skip(&format!(
6831 "{} - no zero-copy GPU buffers",
6832 function!()
6833 ));
6834 return;
6835 }
6836 let (w, h) = (32usize, 64usize);
6837 let mut p = ImageProcessor::new().unwrap();
6838 let parent = p
6839 .create_image(
6840 w,
6841 h,
6842 PixelFormat::Rgba,
6843 DType::U8,
6844 None,
6845 CpuAccess::ReadWrite,
6846 )
6847 .unwrap();
6848 if !texture_or_skip(&parent, function!()) {
6849 return;
6850 }
6851 fill_synthetic_rgba(&parent);
6852
6853 let mut band = parent
6857 .view(edgefirst_tensor::Region::new(0, h / 2, w, h / 2))
6858 .unwrap();
6859 p.draw_decoded_masks(&mut band, &[], &[], MaskOverlay::default())
6860 .expect("empty draw into a texture view");
6861 drop(band);
6862
6863 let mut flat = vec![0u8; w * h * 4];
6864 parent
6865 .as_typed::<u8>()
6866 .unwrap()
6867 .copy_to_flat(&mut flat)
6868 .expect("read the parent texture");
6869 for y in 0..h {
6870 for x in 0..w {
6871 let o = (y * w + x) * 4;
6872 let got = [flat[o], flat[o + 1], flat[o + 2], flat[o + 3]];
6873 let want = if y < h / 2 {
6874 [x as u8, y as u8, (x + y) as u8, 255]
6875 } else {
6876 [0, 0, 0, 0]
6877 };
6878 assert_eq!(got, want, "pixel ({x},{y})");
6879 }
6880 }
6881
6882 let mut forced = with_force_backend(Some("opengl"), ImageProcessor::new).unwrap();
6885 let mut band = parent
6886 .view(edgefirst_tensor::Region::new(0, h / 2, w, h / 2))
6887 .unwrap();
6888 let err = forced
6889 .draw_decoded_masks(&mut band, &[], &[], MaskOverlay::default())
6890 .expect_err("forced GL must decline a view destination");
6891 assert!(
6892 matches!(err, Error::NotSupported(_)),
6893 "expected NotSupported, got {err:?}"
6894 );
6895 }
6896
6897 #[cfg(all(target_os = "windows", feature = "opengl"))]
6902 #[test]
6903 fn windows_class_colors_reach_the_cpu_backend_behind_a_declined_gl_draw() {
6904 use edgefirst_tensor::{CpuAccess, Segmentation};
6905 use ndarray::Array3;
6906
6907 if !edgefirst_tensor::is_gpu_buffer_available() {
6908 crate::test_support::report_skip(&format!(
6909 "{} - no zero-copy GPU buffers",
6910 function!()
6911 ));
6912 return;
6913 }
6914 const CUSTOM: [u8; 4] = [7, 200, 13, 255];
6915 assert_ne!(
6916 CUSTOM[..3],
6917 DEFAULT_COLORS_U8[0][..3],
6918 "the custom colour must differ from the default palette's, or \
6919 this test cannot tell them apart"
6920 );
6921
6922 let (w, h) = (32usize, 64usize);
6923 let mut p = ImageProcessor::new().unwrap();
6924 let parent = p
6925 .create_image(
6926 w,
6927 h,
6928 PixelFormat::Rgba,
6929 DType::U8,
6930 None,
6931 CpuAccess::ReadWrite,
6932 )
6933 .unwrap();
6934 if parent.memory() != TensorMemory::DmaBuf
6939 && !std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1")
6940 {
6941 crate::test_support::report_skip(&format!(
6942 "{} - create_image gave {:?}, not a texture (no GL backend)",
6943 function!(),
6944 parent.memory()
6945 ));
6946 return;
6947 }
6948 p.set_class_colors(&[CUSTOM]).unwrap();
6949
6950 let detect = DetectBox {
6951 bbox: [0.25, 0.25, 0.75, 0.75].into(),
6952 score: 0.99,
6953 label: 0,
6954 };
6955 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
6956 let seg = Segmentation {
6957 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
6958 .unwrap()
6959 .into(),
6960 xmin: 0.25,
6961 ymin: 0.25,
6962 xmax: 0.75,
6963 ymax: 0.75,
6964 };
6965 let mut band = parent
6966 .view(edgefirst_tensor::Region::new(0, h / 2, w, h / 2))
6967 .unwrap();
6968 p.draw_decoded_masks(&mut band, &[detect], &[seg], MaskOverlay::default())
6969 .expect("draw into a texture view");
6970 drop(band);
6971
6972 let mut flat = vec![0u8; w * h * 4];
6973 parent
6974 .as_typed::<u8>()
6975 .unwrap()
6976 .copy_to_flat(&mut flat)
6977 .expect("read the parent texture");
6978 let o = ((h / 2 + h / 4) * w + w / 2) * 4;
6981 assert_eq!(
6982 &flat[o..o + 3],
6983 &CUSTOM[..3],
6984 "the CPU backend drew the default palette, so it never received \
6985 set_class_colors"
6986 );
6987 }
6988
6989 #[cfg(all(target_os = "windows", feature = "opengl"))]
6994 #[test]
6995 fn two_processors_alternate_on_texture_destinations_without_state_bleed() {
6996 use edgefirst_tensor::CpuAccess;
6997
6998 if !edgefirst_tensor::is_gpu_buffer_available() {
6999 crate::test_support::report_skip(&format!(
7000 "{} - no zero-copy GPU buffers",
7001 function!()
7002 ));
7003 return;
7004 }
7005 let mut a = ImageProcessor::new().unwrap();
7006 let mut b = ImageProcessor::new().unwrap();
7007 let src_a = synthetic_rgba(512, 512);
7008 let src_b = synthetic_rgba(256, 256);
7009 let mut dst_a = a
7010 .create_image(
7011 512,
7012 512,
7013 PixelFormat::Rgba,
7014 DType::U8,
7015 None,
7016 CpuAccess::Read,
7017 )
7018 .unwrap();
7019 let mut dst_b = b
7020 .create_image(
7021 256,
7022 256,
7023 PixelFormat::Rgba,
7024 DType::U8,
7025 None,
7026 CpuAccess::Read,
7027 )
7028 .unwrap();
7029 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
7030 backend: ComputeBackend::Cpu,
7031 ..Default::default()
7032 })
7033 .unwrap();
7034 let mut ref_a = TensorDyn::image(
7035 512,
7036 512,
7037 PixelFormat::Rgba,
7038 DType::U8,
7039 Some(TensorMemory::Mem),
7040 CpuAccess::ReadWrite,
7041 )
7042 .unwrap();
7043 let mut ref_b = TensorDyn::image(
7044 256,
7045 256,
7046 PixelFormat::Rgba,
7047 DType::U8,
7048 Some(TensorMemory::Mem),
7049 CpuAccess::ReadWrite,
7050 )
7051 .unwrap();
7052 cpu.convert(
7053 &src_a,
7054 &mut ref_a,
7055 Rotation::None,
7056 Flip::None,
7057 Crop::default(),
7058 )
7059 .unwrap();
7060 cpu.convert(
7061 &src_b,
7062 &mut ref_b,
7063 Rotation::None,
7064 Flip::None,
7065 Crop::default(),
7066 )
7067 .unwrap();
7068 for _ in 0..200 {
7069 a.convert(
7070 &src_a,
7071 &mut dst_a,
7072 Rotation::None,
7073 Flip::None,
7074 Crop::default(),
7075 )
7076 .unwrap();
7077 b.convert(
7078 &src_b,
7079 &mut dst_b,
7080 Rotation::None,
7081 Flip::None,
7082 Crop::default(),
7083 )
7084 .unwrap();
7085 compare_images(&dst_a, &ref_a, 0.0, "processor A");
7086 compare_images(&dst_b, &ref_b, 0.0, "processor B");
7087 }
7088 }
7089
7090 #[test]
7091 #[cfg(any(
7092 target_os = "linux",
7093 target_os = "macos",
7094 target_os = "ios",
7095 target_os = "android",
7096 target_os = "windows"
7097 ))]
7098 #[cfg(feature = "opengl")]
7099 fn test_opengl_all_rgba() {
7100 if !is_opengl_available() {
7101 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
7102 return;
7103 }
7104
7105 let dst_width = 640;
7106 let dst_height = 640;
7107 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7108
7109 let mut cpu_converter = CPUProcessor::new();
7110
7111 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
7112
7113 let mut mem = vec![None, Some(TensorMemory::Mem)];
7117 if edgefirst_tensor::is_shm_available() {
7118 mem.push(Some(TensorMemory::Shm));
7119 }
7120 if edgefirst_tensor::is_gpu_buffer_available() {
7121 mem.push(Some(TensorMemory::DmaBuf));
7122 }
7123 let crop = Crop::new().with_source(Some(Region::new(50, 120, 1024, 576)));
7124 for m in mem {
7125 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), m).unwrap();
7126 let src_dyn = src;
7127
7128 for rot in [
7129 Rotation::None,
7130 Rotation::Clockwise90,
7131 Rotation::Rotate180,
7132 Rotation::CounterClockwise90,
7133 ] {
7134 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
7135 let cpu_dst = TensorDyn::image(
7136 dst_width,
7137 dst_height,
7138 PixelFormat::Rgba,
7139 DType::U8,
7140 m,
7141 edgefirst_tensor::CpuAccess::ReadWrite,
7142 )
7143 .unwrap();
7144 let gl_dst = TensorDyn::image(
7145 dst_width,
7146 dst_height,
7147 PixelFormat::Rgba,
7148 DType::U8,
7149 m,
7150 edgefirst_tensor::CpuAccess::ReadWrite,
7151 )
7152 .unwrap();
7153 cpu_dst
7154 .as_typed::<u8>()
7155 .unwrap()
7156 .map()
7157 .unwrap()
7158 .as_mut_slice()
7159 .fill(114);
7160 gl_dst
7161 .as_typed::<u8>()
7162 .unwrap()
7163 .map()
7164 .unwrap()
7165 .as_mut_slice()
7166 .fill(114);
7167
7168 let mut cpu_dst_dyn = cpu_dst;
7169 cpu_converter
7170 .convert(&src_dyn, &mut cpu_dst_dyn, Rotation::None, Flip::None, crop)
7171 .unwrap();
7172 let cpu_dst = {
7173 let mut __t = cpu_dst_dyn.into_u8().unwrap();
7174 __t.set_format(PixelFormat::Rgba).unwrap();
7175 TensorDyn::from(__t)
7176 };
7177
7178 let mut gl_dst_dyn = gl_dst;
7179 gl_converter
7180 .convert(&src_dyn, &mut gl_dst_dyn, Rotation::None, Flip::None, crop)
7181 .map_err(|e| {
7182 log::error!("error mem {m:?} rot {rot:?} error: {e:?}");
7183 e
7184 })
7185 .unwrap();
7186 let gl_dst = {
7187 let mut __t = gl_dst_dyn.into_u8().unwrap();
7188 __t.set_format(PixelFormat::Rgba).unwrap();
7189 TensorDyn::from(__t)
7190 };
7191
7192 compare_images(
7193 &gl_dst,
7194 &cpu_dst,
7195 0.98,
7196 &format!("{} {:?} {:?}", function!(), rot, flip),
7197 );
7198 }
7199 }
7200 }
7201 }
7202
7203 #[test]
7204 #[cfg(target_os = "linux")]
7205 fn test_cpu_rotate() {
7206 for rot in [
7207 Rotation::Clockwise90,
7208 Rotation::Rotate180,
7209 Rotation::CounterClockwise90,
7210 ] {
7211 test_cpu_rotate_(rot);
7212 }
7213 }
7214
7215 #[cfg(target_os = "linux")]
7216 fn test_cpu_rotate_(rot: Rotation) {
7217 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7221
7222 let unchanged_src =
7223 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
7224 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
7225
7226 let (dst_width, dst_height) = match rot {
7227 Rotation::None | Rotation::Rotate180 => (src.width().unwrap(), src.height().unwrap()),
7228 Rotation::Clockwise90 | Rotation::CounterClockwise90 => {
7229 (src.height().unwrap(), src.width().unwrap())
7230 }
7231 };
7232
7233 let cpu_dst = TensorDyn::image(
7234 dst_width,
7235 dst_height,
7236 PixelFormat::Rgba,
7237 DType::U8,
7238 None,
7239 edgefirst_tensor::CpuAccess::ReadWrite,
7240 )
7241 .unwrap();
7242 let mut cpu_converter = CPUProcessor::new();
7243
7244 let (result, src, cpu_dst) = convert_img(
7247 &mut cpu_converter,
7248 src,
7249 cpu_dst,
7250 rot,
7251 Flip::None,
7252 Crop::no_crop(),
7253 );
7254 result.unwrap();
7255
7256 let (result, cpu_dst, src) = convert_img(
7257 &mut cpu_converter,
7258 cpu_dst,
7259 src,
7260 rot,
7261 Flip::None,
7262 Crop::no_crop(),
7263 );
7264 result.unwrap();
7265
7266 let (result, src, cpu_dst) = convert_img(
7267 &mut cpu_converter,
7268 src,
7269 cpu_dst,
7270 rot,
7271 Flip::None,
7272 Crop::no_crop(),
7273 );
7274 result.unwrap();
7275
7276 let (result, _cpu_dst, src) = convert_img(
7277 &mut cpu_converter,
7278 cpu_dst,
7279 src,
7280 rot,
7281 Flip::None,
7282 Crop::no_crop(),
7283 );
7284 result.unwrap();
7285
7286 compare_images(&src, &unchanged_src, 0.98, function!());
7287 }
7288
7289 #[test]
7290 #[cfg(any(
7291 target_os = "linux",
7292 target_os = "macos",
7293 target_os = "ios",
7294 target_os = "android",
7295 target_os = "windows"
7296 ))]
7297 #[cfg(feature = "opengl")]
7298 fn test_opengl_rotate() {
7299 if !is_opengl_available() {
7300 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
7301 return;
7302 }
7303
7304 let size = (1280, 720);
7305 let mut mem = vec![None, Some(TensorMemory::Mem)];
7307 if edgefirst_tensor::is_shm_available() {
7308 mem.push(Some(TensorMemory::Shm));
7309 }
7310 if edgefirst_tensor::is_gpu_buffer_available() {
7311 mem.push(Some(TensorMemory::DmaBuf));
7312 }
7313 for m in mem {
7314 for rot in [
7315 Rotation::Clockwise90,
7316 Rotation::Rotate180,
7317 Rotation::CounterClockwise90,
7318 ] {
7319 test_opengl_rotate_(size, rot, m);
7320 }
7321 }
7322 }
7323
7324 #[cfg(any(
7325 target_os = "linux",
7326 target_os = "macos",
7327 target_os = "ios",
7328 target_os = "android",
7329 target_os = "windows"
7330 ))]
7331 #[cfg(feature = "opengl")]
7332 fn test_opengl_rotate_(
7333 size: (usize, usize),
7334 rot: Rotation,
7335 tensor_memory: Option<TensorMemory>,
7336 ) {
7337 let (dst_width, dst_height) = match rot {
7338 Rotation::None | Rotation::Rotate180 => size,
7339 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
7340 };
7341
7342 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7343 let src =
7344 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), tensor_memory).unwrap();
7345
7346 let cpu_dst = TensorDyn::image(
7347 dst_width,
7348 dst_height,
7349 PixelFormat::Rgba,
7350 DType::U8,
7351 None,
7352 edgefirst_tensor::CpuAccess::ReadWrite,
7353 )
7354 .unwrap();
7355 let mut cpu_converter = CPUProcessor::new();
7356
7357 let (result, mut src, cpu_dst) = convert_img(
7358 &mut cpu_converter,
7359 src,
7360 cpu_dst,
7361 rot,
7362 Flip::None,
7363 Crop::no_crop(),
7364 );
7365 result.unwrap();
7366
7367 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
7368
7369 for _ in 0..5 {
7370 let gl_dst = TensorDyn::image(
7371 dst_width,
7372 dst_height,
7373 PixelFormat::Rgba,
7374 DType::U8,
7375 tensor_memory,
7376 edgefirst_tensor::CpuAccess::ReadWrite,
7377 )
7378 .unwrap();
7379 let (result, src_back, gl_dst) = convert_img(
7380 &mut gl_converter,
7381 src,
7382 gl_dst,
7383 rot,
7384 Flip::None,
7385 Crop::no_crop(),
7386 );
7387 result.unwrap();
7388 src = src_back;
7389 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
7390 }
7391 }
7392
7393 #[test]
7394 #[cfg(target_os = "linux")]
7395 fn test_g2d_rotate() {
7396 if !is_g2d_available() {
7397 crate::test_support::report_skip(
7398 "test_g2d_rotate - G2D library (libg2d.so.2) not available",
7399 );
7400 return;
7401 }
7402 if !is_dma_available() {
7403 crate::test_support::report_skip("test_g2d_rotate - DMA memory allocation not available (permission denied or no DMA-BUF support)");
7404 return;
7405 }
7406
7407 let size = (1280, 720);
7408 for rot in [
7409 Rotation::Clockwise90,
7410 Rotation::Rotate180,
7411 Rotation::CounterClockwise90,
7412 ] {
7413 test_g2d_rotate_(size, rot);
7414 }
7415 }
7416
7417 #[cfg(target_os = "linux")]
7418 fn test_g2d_rotate_(size: (usize, usize), rot: Rotation) {
7419 let (dst_width, dst_height) = match rot {
7420 Rotation::None | Rotation::Rotate180 => size,
7421 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
7422 };
7423
7424 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7425 let src = crate::load_image_test_helper(
7426 &file,
7427 Some(PixelFormat::Rgba),
7428 Some(TensorMemory::DmaBuf),
7429 )
7430 .unwrap();
7431
7432 let cpu_dst = TensorDyn::image(
7433 dst_width,
7434 dst_height,
7435 PixelFormat::Rgba,
7436 DType::U8,
7437 None,
7438 edgefirst_tensor::CpuAccess::ReadWrite,
7439 )
7440 .unwrap();
7441 let mut cpu_converter = CPUProcessor::new();
7442
7443 let (result, src, cpu_dst) = convert_img(
7444 &mut cpu_converter,
7445 src,
7446 cpu_dst,
7447 rot,
7448 Flip::None,
7449 Crop::no_crop(),
7450 );
7451 result.unwrap();
7452
7453 let g2d_dst = TensorDyn::image(
7454 dst_width,
7455 dst_height,
7456 PixelFormat::Rgba,
7457 DType::U8,
7458 Some(TensorMemory::DmaBuf),
7459 edgefirst_tensor::CpuAccess::ReadWrite,
7460 )
7461 .unwrap();
7462 let mut g2d_converter = G2DProcessor::new().unwrap();
7463
7464 let (result, _src, g2d_dst) = convert_img(
7465 &mut g2d_converter,
7466 src,
7467 g2d_dst,
7468 rot,
7469 Flip::None,
7470 Crop::no_crop(),
7471 );
7472 result.unwrap();
7473
7474 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
7480 }
7481
7482 #[test]
7483 fn test_rgba_to_yuyv_resize_cpu() {
7484 let src = load_bytes_to_tensor(
7485 1280,
7486 720,
7487 PixelFormat::Rgba,
7488 None,
7489 &edgefirst_bench::testdata::read("camera720p.rgba"),
7490 )
7491 .unwrap();
7492
7493 let (dst_width, dst_height) = (640, 360);
7494
7495 let dst = TensorDyn::image(
7496 dst_width,
7497 dst_height,
7498 PixelFormat::Yuyv,
7499 DType::U8,
7500 None,
7501 edgefirst_tensor::CpuAccess::ReadWrite,
7502 )
7503 .unwrap();
7504
7505 let dst_through_yuyv = TensorDyn::image(
7506 dst_width,
7507 dst_height,
7508 PixelFormat::Rgba,
7509 DType::U8,
7510 None,
7511 edgefirst_tensor::CpuAccess::ReadWrite,
7512 )
7513 .unwrap();
7514 let dst_direct = TensorDyn::image(
7515 dst_width,
7516 dst_height,
7517 PixelFormat::Rgba,
7518 DType::U8,
7519 None,
7520 edgefirst_tensor::CpuAccess::ReadWrite,
7521 )
7522 .unwrap();
7523
7524 let mut cpu_converter = CPUProcessor::new();
7525
7526 let (result, src, dst) = convert_img(
7527 &mut cpu_converter,
7528 src,
7529 dst,
7530 Rotation::None,
7531 Flip::None,
7532 Crop::no_crop(),
7533 );
7534 result.unwrap();
7535
7536 let (result, _dst, dst_through_yuyv) = convert_img(
7537 &mut cpu_converter,
7538 dst,
7539 dst_through_yuyv,
7540 Rotation::None,
7541 Flip::None,
7542 Crop::no_crop(),
7543 );
7544 result.unwrap();
7545
7546 let (result, _src, dst_direct) = convert_img(
7547 &mut cpu_converter,
7548 src,
7549 dst_direct,
7550 Rotation::None,
7551 Flip::None,
7552 Crop::no_crop(),
7553 );
7554 result.unwrap();
7555
7556 compare_images(&dst_through_yuyv, &dst_direct, 0.98, function!());
7557 }
7558
7559 #[test]
7560 #[cfg(any(
7561 target_os = "linux",
7562 target_os = "macos",
7563 target_os = "ios",
7564 target_os = "android",
7565 target_os = "windows"
7566 ))]
7567 #[cfg(feature = "opengl")]
7568 #[ignore = "opengl doesn't support rendering to PixelFormat::Yuyv texture"]
7569 fn test_rgba_to_yuyv_resize_opengl() {
7570 if !is_opengl_available() {
7571 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
7572 return;
7573 }
7574
7575 if !edgefirst_tensor::is_gpu_buffer_available() {
7576 crate::test_support::report_skip(&format!(
7577 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
7578 function!()
7579 ));
7580 return;
7581 }
7582
7583 let src = load_bytes_to_tensor(
7584 1280,
7585 720,
7586 PixelFormat::Rgba,
7587 None,
7588 &edgefirst_bench::testdata::read("camera720p.rgba"),
7589 )
7590 .unwrap();
7591
7592 let (dst_width, dst_height) = (640, 360);
7593
7594 let dst = TensorDyn::image(
7595 dst_width,
7596 dst_height,
7597 PixelFormat::Yuyv,
7598 DType::U8,
7599 Some(TensorMemory::DmaBuf),
7600 edgefirst_tensor::CpuAccess::ReadWrite,
7601 )
7602 .unwrap();
7603
7604 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
7605
7606 let (result, src, dst) = convert_img(
7607 &mut gl_converter,
7608 src,
7609 dst,
7610 Rotation::None,
7611 Flip::None,
7612 Crop::letterbox([255, 255, 255, 255]),
7613 );
7614 result.unwrap();
7615
7616 std::fs::write(
7617 "rgba_to_yuyv_opengl.yuyv",
7618 dst.as_typed::<u8>().unwrap().map().unwrap().as_slice(),
7619 )
7620 .unwrap();
7621 let cpu_dst = TensorDyn::image(
7622 dst_width,
7623 dst_height,
7624 PixelFormat::Yuyv,
7625 DType::U8,
7626 Some(TensorMemory::DmaBuf),
7627 edgefirst_tensor::CpuAccess::ReadWrite,
7628 )
7629 .unwrap();
7630 let (result, _src, cpu_dst) = convert_img(
7631 &mut CPUProcessor::new(),
7632 src,
7633 cpu_dst,
7634 Rotation::None,
7635 Flip::None,
7636 Crop::no_crop(),
7637 );
7638 result.unwrap();
7639
7640 compare_images_convert_to_rgb(&dst, &cpu_dst, 0.98, function!());
7641 }
7642
7643 #[test]
7644 #[cfg(target_os = "linux")]
7645 fn test_rgba_to_yuyv_resize_g2d() {
7646 if !is_g2d_available() {
7647 crate::test_support::report_skip(
7648 "test_rgba_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available",
7649 );
7650 return;
7651 }
7652 if !is_dma_available() {
7653 crate::test_support::report_skip("test_rgba_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
7654 return;
7655 }
7656
7657 let src = load_bytes_to_tensor(
7658 1280,
7659 720,
7660 PixelFormat::Rgba,
7661 Some(TensorMemory::DmaBuf),
7662 &edgefirst_bench::testdata::read("camera720p.rgba"),
7663 )
7664 .unwrap();
7665
7666 let (dst_width, dst_height) = (1280, 720);
7667
7668 let cpu_dst = TensorDyn::image(
7669 dst_width,
7670 dst_height,
7671 PixelFormat::Yuyv,
7672 DType::U8,
7673 Some(TensorMemory::DmaBuf),
7674 edgefirst_tensor::CpuAccess::ReadWrite,
7675 )
7676 .unwrap();
7677
7678 let g2d_dst = TensorDyn::image(
7679 dst_width,
7680 dst_height,
7681 PixelFormat::Yuyv,
7682 DType::U8,
7683 Some(TensorMemory::DmaBuf),
7684 edgefirst_tensor::CpuAccess::ReadWrite,
7685 )
7686 .unwrap();
7687
7688 let mut g2d_converter = G2DProcessor::new().unwrap();
7689 let crop = Crop::new();
7690
7691 g2d_dst
7692 .as_typed::<u8>()
7693 .unwrap()
7694 .map()
7695 .unwrap()
7696 .as_mut_slice()
7697 .fill(128);
7698 let (result, src, g2d_dst) = convert_img(
7699 &mut g2d_converter,
7700 src,
7701 g2d_dst,
7702 Rotation::None,
7703 Flip::None,
7704 crop,
7705 );
7706 result.unwrap();
7707
7708 let cpu_dst_img = cpu_dst;
7709 cpu_dst_img
7710 .as_typed::<u8>()
7711 .unwrap()
7712 .map()
7713 .unwrap()
7714 .as_mut_slice()
7715 .fill(128);
7716 let (result, _src, cpu_dst) = convert_img(
7717 &mut CPUProcessor::new(),
7718 src,
7719 cpu_dst_img,
7720 Rotation::None,
7721 Flip::None,
7722 crop,
7723 );
7724 result.unwrap();
7725
7726 compare_images_convert_to_rgb(&cpu_dst, &g2d_dst, 0.98, function!());
7727 }
7728
7729 #[test]
7730 fn test_yuyv_to_rgba_cpu() {
7731 let file = edgefirst_bench::testdata::read("camera720p.yuyv").to_vec();
7732 let src = TensorDyn::image(
7733 1280,
7734 720,
7735 PixelFormat::Yuyv,
7736 DType::U8,
7737 None,
7738 edgefirst_tensor::CpuAccess::ReadWrite,
7739 )
7740 .unwrap();
7741 src.as_typed::<u8>()
7742 .unwrap()
7743 .map()
7744 .unwrap()
7745 .as_mut_slice()
7746 .copy_from_slice(&file);
7747
7748 let dst = TensorDyn::image(
7749 1280,
7750 720,
7751 PixelFormat::Rgba,
7752 DType::U8,
7753 None,
7754 edgefirst_tensor::CpuAccess::ReadWrite,
7755 )
7756 .unwrap();
7757 let mut cpu_converter = CPUProcessor::new();
7758
7759 let (result, _src, dst) = convert_img(
7760 &mut cpu_converter,
7761 src,
7762 dst,
7763 Rotation::None,
7764 Flip::None,
7765 Crop::no_crop(),
7766 );
7767 result.unwrap();
7768
7769 let target_image = TensorDyn::image(
7770 1280,
7771 720,
7772 PixelFormat::Rgba,
7773 DType::U8,
7774 None,
7775 edgefirst_tensor::CpuAccess::ReadWrite,
7776 )
7777 .unwrap();
7778 target_image
7779 .as_typed::<u8>()
7780 .unwrap()
7781 .map()
7782 .unwrap()
7783 .as_mut_slice()
7784 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
7785
7786 compare_images(&dst, &target_image, 0.98, function!());
7789 }
7790
7791 #[test]
7792 fn test_yuyv_to_rgb_cpu() {
7793 let file = edgefirst_bench::testdata::read("camera720p.yuyv").to_vec();
7794 let src = TensorDyn::image(
7795 1280,
7796 720,
7797 PixelFormat::Yuyv,
7798 DType::U8,
7799 None,
7800 edgefirst_tensor::CpuAccess::ReadWrite,
7801 )
7802 .unwrap();
7803 src.as_typed::<u8>()
7804 .unwrap()
7805 .map()
7806 .unwrap()
7807 .as_mut_slice()
7808 .copy_from_slice(&file);
7809
7810 let dst = TensorDyn::image(
7811 1280,
7812 720,
7813 PixelFormat::Rgb,
7814 DType::U8,
7815 None,
7816 edgefirst_tensor::CpuAccess::ReadWrite,
7817 )
7818 .unwrap();
7819 let mut cpu_converter = CPUProcessor::new();
7820
7821 let (result, _src, dst) = convert_img(
7822 &mut cpu_converter,
7823 src,
7824 dst,
7825 Rotation::None,
7826 Flip::None,
7827 Crop::no_crop(),
7828 );
7829 result.unwrap();
7830
7831 let target_image = TensorDyn::image(
7832 1280,
7833 720,
7834 PixelFormat::Rgb,
7835 DType::U8,
7836 None,
7837 edgefirst_tensor::CpuAccess::ReadWrite,
7838 )
7839 .unwrap();
7840 target_image
7841 .as_typed::<u8>()
7842 .unwrap()
7843 .map()
7844 .unwrap()
7845 .as_mut_slice()
7846 .as_chunks_mut::<3>()
7847 .0
7848 .iter_mut()
7849 .zip(
7850 edgefirst_bench::testdata::read("camera720p.rgba")
7851 .as_chunks::<4>()
7852 .0,
7853 )
7854 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
7855
7856 compare_images(&dst, &target_image, 0.98, function!());
7859 }
7860
7861 #[test]
7862 #[cfg(target_os = "linux")]
7863 fn test_yuyv_to_rgba_g2d() {
7864 if !is_g2d_available() {
7865 crate::test_support::report_skip(
7866 "test_yuyv_to_rgba_g2d - G2D library (libg2d.so.2) not available",
7867 );
7868 return;
7869 }
7870 if !is_dma_available() {
7871 crate::test_support::report_skip("test_yuyv_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
7872 return;
7873 }
7874
7875 let src = load_bytes_to_tensor(
7876 1280,
7877 720,
7878 PixelFormat::Yuyv,
7879 None,
7880 &edgefirst_bench::testdata::read("camera720p.yuyv"),
7881 )
7882 .unwrap();
7883
7884 let dst = TensorDyn::image(
7885 1280,
7886 720,
7887 PixelFormat::Rgba,
7888 DType::U8,
7889 Some(TensorMemory::DmaBuf),
7890 edgefirst_tensor::CpuAccess::ReadWrite,
7891 )
7892 .unwrap();
7893 let mut g2d_converter = G2DProcessor::new().unwrap();
7894
7895 let (result, _src, dst) = convert_img(
7896 &mut g2d_converter,
7897 src,
7898 dst,
7899 Rotation::None,
7900 Flip::None,
7901 Crop::no_crop(),
7902 );
7903 result.unwrap();
7904
7905 let target_image = TensorDyn::image(
7906 1280,
7907 720,
7908 PixelFormat::Rgba,
7909 DType::U8,
7910 None,
7911 edgefirst_tensor::CpuAccess::ReadWrite,
7912 )
7913 .unwrap();
7914 target_image
7915 .as_typed::<u8>()
7916 .unwrap()
7917 .map()
7918 .unwrap()
7919 .as_mut_slice()
7920 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
7921
7922 compare_images(&dst, &target_image, 0.98, function!());
7926 }
7927
7928 #[test]
7929 #[cfg(any(
7930 target_os = "linux",
7931 target_os = "macos",
7932 target_os = "ios",
7933 target_os = "android",
7934 target_os = "windows"
7935 ))]
7936 #[cfg(feature = "opengl")]
7937 fn test_yuyv_to_rgba_opengl() {
7938 if !is_opengl_available() {
7939 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
7940 return;
7941 }
7942 if !edgefirst_tensor::is_gpu_buffer_available() {
7945 crate::test_support::report_skip(&format!(
7946 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
7947 function!()
7948 ));
7949 return;
7950 }
7951
7952 let src = load_bytes_to_tensor(
7953 1280,
7954 720,
7955 PixelFormat::Yuyv,
7956 Some(TensorMemory::DmaBuf),
7957 &edgefirst_bench::testdata::read("camera720p.yuyv"),
7958 )
7959 .unwrap();
7960
7961 let dst = TensorDyn::image(
7962 1280,
7963 720,
7964 PixelFormat::Rgba,
7965 DType::U8,
7966 Some(TensorMemory::DmaBuf),
7967 edgefirst_tensor::CpuAccess::ReadWrite,
7968 )
7969 .unwrap();
7970 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
7971
7972 let (result, _src, dst) = convert_img(
7973 &mut gl_converter,
7974 src,
7975 dst,
7976 Rotation::None,
7977 Flip::None,
7978 Crop::no_crop(),
7979 );
7980 if let Err(crate::Error::NotSupported(msg)) = &result {
7983 crate::test_support::report_skip(&format!("{} — {msg}", function!()));
7984 return;
7985 }
7986 result.unwrap();
7987
7988 let target_image = TensorDyn::image(
7989 1280,
7990 720,
7991 PixelFormat::Rgba,
7992 DType::U8,
7993 None,
7994 edgefirst_tensor::CpuAccess::ReadWrite,
7995 )
7996 .unwrap();
7997 target_image
7998 .as_typed::<u8>()
7999 .unwrap()
8000 .map()
8001 .unwrap()
8002 .as_mut_slice()
8003 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
8004
8005 compare_images(&dst, &target_image, 0.98, function!());
8009 }
8010
8011 #[test]
8021 #[cfg(target_os = "macos")]
8022 #[cfg(feature = "opengl")]
8023 fn test_grey_r8_iosurface_to_rgba_opengl_macos() {
8024 let mut proc = match GLProcessorThreaded::new(None) {
8025 Ok(p) => p,
8026 Err(e) => {
8027 crate::test_support::report_skip(&format!(
8028 "{} — GL engine init failed ({e:?})",
8029 function!()
8030 ));
8031 return;
8032 }
8033 };
8034
8035 let (w, h) = (16usize, 16usize);
8036 let src = TensorDyn::image(
8037 w,
8038 h,
8039 PixelFormat::Grey,
8040 DType::U8,
8041 Some(TensorMemory::DmaBuf),
8042 edgefirst_tensor::CpuAccess::ReadWrite,
8043 )
8044 .expect("GREY IOSurface (R8/L008) should allocate — proves the FourCC mapping");
8045 {
8047 let su8 = src.as_typed::<u8>().unwrap();
8048 let stride = src
8049 .as_typed::<u8>()
8050 .unwrap()
8051 .effective_row_stride()
8052 .unwrap();
8053 let mut m = su8.map().unwrap();
8054 let buf = m.as_mut_slice();
8055 for y in 0..h {
8056 for x in 0..w {
8057 buf[y * stride + x] = ((x * 13 + y * 7) & 0xff) as u8;
8058 }
8059 }
8060 }
8061
8062 let dst = TensorDyn::image(
8063 w,
8064 h,
8065 PixelFormat::Rgba,
8066 DType::U8,
8067 Some(TensorMemory::DmaBuf),
8068 edgefirst_tensor::CpuAccess::ReadWrite,
8069 )
8070 .unwrap();
8071 let (result, src_back, dst) = convert_img(
8072 &mut proc,
8073 src,
8074 dst,
8075 Rotation::None,
8076 Flip::None,
8077 Crop::no_crop(),
8078 );
8079 result.expect("GREY(R8 IOSurface) → RGBA must convert on ANGLE (R8 binding works)");
8080
8081 let src_stride = src_back
8082 .as_typed::<u8>()
8083 .unwrap()
8084 .effective_row_stride()
8085 .unwrap();
8086 let src_map = src_back.as_typed::<u8>().unwrap().map().unwrap();
8087 let sbytes = src_map.as_slice();
8088 let dst_stride = dst
8089 .as_typed::<u8>()
8090 .unwrap()
8091 .effective_row_stride()
8092 .unwrap();
8093 let dst_map = dst.as_typed::<u8>().unwrap().map().unwrap();
8094 let dbytes = dst_map.as_slice();
8095 for y in 0..h {
8096 for x in 0..w {
8097 let yv = sbytes[y * src_stride + x] as i16;
8098 let p = y * dst_stride + x * 4;
8099 for c in 0..3 {
8100 assert!(
8101 (dbytes[p + c] as i16 - yv).abs() <= 2,
8102 "pixel ({x},{y}) ch{c} = {} expected ~{yv} (GREY→RGB identity)",
8103 dbytes[p + c]
8104 );
8105 }
8106 }
8107 }
8108 }
8109
8110 #[test]
8116 #[cfg(target_os = "macos")]
8117 #[cfg(feature = "opengl")]
8118 fn test_nv12_to_planar_f16_two_pass_opengl_macos() {
8119 let mut gpu = match GLProcessorThreaded::new(None) {
8120 Ok(p) => p,
8121 Err(e) => {
8122 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8123 return;
8124 }
8125 };
8126 let (w, h) = (64usize, 64usize);
8127 let src = match TensorDyn::image(
8128 w,
8129 h,
8130 PixelFormat::Nv12,
8131 DType::U8,
8132 Some(TensorMemory::DmaBuf),
8133 edgefirst_tensor::CpuAccess::ReadWrite,
8134 ) {
8135 Ok(t) => t,
8136 Err(e) => {
8137 crate::test_support::report_skip(&format!(
8138 "{} — NV12 IOSurface alloc: {e:?}",
8139 function!()
8140 ));
8141 return;
8142 }
8143 };
8144 src.as_typed::<u8>()
8145 .unwrap()
8146 .map()
8147 .unwrap()
8148 .as_mut_slice()
8149 .fill(128); let dst = match TensorDyn::image(
8152 w,
8153 h,
8154 PixelFormat::PlanarRgb,
8155 DType::F16,
8156 Some(TensorMemory::DmaBuf),
8157 edgefirst_tensor::CpuAccess::ReadWrite,
8158 ) {
8159 Ok(t) => t,
8160 Err(e) => {
8161 crate::test_support::report_skip(&format!(
8162 "{} — F16 PlanarRgb IOSurface: {e:?}",
8163 function!()
8164 ));
8165 return;
8166 }
8167 };
8168 let mut dst = dst;
8169 if let Err(e) = ImageProcessorTrait::convert(
8171 &mut gpu,
8172 &src,
8173 &mut dst,
8174 Rotation::None,
8175 Flip::None,
8176 Crop::no_crop(),
8177 ) {
8178 crate::test_support::report_skip(&format!(
8182 "{} — NV12→PlanarRgb F16 not available ({e:?})",
8183 function!()
8184 ));
8185 return;
8186 }
8187 let dt = dst.as_typed::<half::f16>().expect("dst is F16 PlanarRgb");
8188 let map = dt.map().unwrap();
8189 let vals = map.as_slice();
8190 let mut checked = 0usize;
8193 for &v in vals.iter() {
8194 let f = f32::from(v);
8195 assert!(
8196 (0.40..=0.60).contains(&f),
8197 "planar F16 value {f} not ~0.5 for neutral-grey NV12"
8198 );
8199 checked += 1;
8200 }
8201 assert!(
8202 checked >= w * h * 3,
8203 "expected >= 3 planes of samples, got {checked}"
8204 );
8205 }
8206
8207 #[test]
8215 #[cfg(target_os = "macos")]
8216 #[cfg(feature = "opengl")]
8217 fn test_nv12_to_planar_f16_two_pass_pool_opengl_macos() {
8218 let mut gpu = match GLProcessorThreaded::new(None) {
8219 Ok(p) => p,
8220 Err(e) => {
8221 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8222 return;
8223 }
8224 };
8225 let (fw, fh) = (96usize, 64usize);
8227 let (pool_w, pool_h) = (256usize, 768usize);
8228 let (model_w, model_h) = (128usize, 128usize);
8229
8230 let mut src = match TensorDyn::image(
8231 pool_w,
8232 pool_h,
8233 PixelFormat::Grey,
8234 DType::U8,
8235 Some(TensorMemory::DmaBuf),
8236 edgefirst_tensor::CpuAccess::ReadWrite,
8237 ) {
8238 Ok(t) => t,
8239 Err(e) => {
8240 crate::test_support::report_skip(&format!(
8241 "{} — R8 pool alloc: {e:?}",
8242 function!()
8243 ));
8244 return;
8245 }
8246 };
8247 src.configure_image(fw, fh, PixelFormat::Nv12)
8248 .unwrap_or_else(|e| panic!("configure_image NV12 on pool: {e}"));
8249 let stride = src
8250 .as_typed::<u8>()
8251 .unwrap()
8252 .effective_row_stride()
8253 .unwrap();
8254 src.as_typed::<u8>()
8255 .unwrap()
8256 .map()
8257 .unwrap()
8258 .as_mut_slice()
8259 .fill(128); let mut dst = match TensorDyn::image(
8262 model_w,
8263 model_h,
8264 PixelFormat::PlanarRgb,
8265 DType::F16,
8266 Some(TensorMemory::DmaBuf),
8267 edgefirst_tensor::CpuAccess::ReadWrite,
8268 ) {
8269 Ok(t) => t,
8270 Err(e) => {
8271 crate::test_support::report_skip(&format!(
8272 "{} — F16 PlanarRgb dst: {e:?}",
8273 function!()
8274 ));
8275 return;
8276 }
8277 };
8278
8279 let _ = model_w;
8281 let crop = Crop::new()
8282 .with_source(Some(Region::new(0, 0, fw, fh)))
8283 .with_fit(Fit::Letterbox {
8284 pad: [0, 0, 0, 255],
8285 });
8286 if let Err(e) =
8287 ImageProcessorTrait::convert(&mut gpu, &src, &mut dst, Rotation::None, Flip::None, crop)
8288 {
8289 crate::test_support::report_skip(&format!(
8290 "{} — NV12→PlanarRgb F16 unavailable ({e:?})",
8291 function!()
8292 ));
8293 return;
8294 }
8295 let _ = stride;
8296 let dt = dst.as_typed::<half::f16>().expect("dst F16");
8299 let map = dt.map().unwrap();
8300 let any_half = map.as_slice().iter().any(|&v| {
8301 let f = f32::from(v);
8302 (0.40..=0.60).contains(&f)
8303 });
8304 assert!(any_half, "expected ~0.5 grey samples in the letterbox band");
8305 }
8306
8307 #[test]
8313 #[cfg(target_os = "macos")]
8314 #[cfg(feature = "opengl")]
8315 fn test_nv12_to_planar_f16_cross_thread_opengl_macos() {
8316 use std::sync::mpsc;
8317 let mut proc = match ImageProcessor::new() {
8320 Ok(p) => p,
8321 Err(e) => {
8322 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8323 return;
8324 }
8325 };
8326 let (fw, fh) = (96usize, 64usize);
8327 let mut src = match TensorDyn::image(
8328 256,
8329 768,
8330 PixelFormat::Grey,
8331 DType::U8,
8332 Some(TensorMemory::DmaBuf),
8333 edgefirst_tensor::CpuAccess::ReadWrite,
8334 ) {
8335 Ok(t) => t,
8336 Err(e) => {
8337 crate::test_support::report_skip(&format!("{} — pool: {e:?}", function!()));
8338 return;
8339 }
8340 };
8341 src.configure_image(fw, fh, PixelFormat::Nv12).unwrap();
8342 src.as_typed::<u8>()
8343 .unwrap()
8344 .map()
8345 .unwrap()
8346 .as_mut_slice()
8347 .fill(128);
8348 let mut dst = match TensorDyn::image(
8349 128,
8350 128,
8351 PixelFormat::PlanarRgb,
8352 DType::F16,
8353 Some(TensorMemory::DmaBuf),
8354 edgefirst_tensor::CpuAccess::ReadWrite,
8355 ) {
8356 Ok(t) => t,
8357 Err(e) => {
8358 crate::test_support::report_skip(&format!("{} — dst: {e:?}", function!()));
8359 return;
8360 }
8361 };
8362 let crop = Crop::new().with_source(Some(Region::new(0, 0, fw, fh)));
8363
8364 let (tx, rx) = mpsc::channel::<bool>();
8367 let worker = std::thread::spawn(move || {
8368 let _ = ImageProcessorTrait::convert(
8369 &mut proc,
8370 &src,
8371 &mut dst,
8372 Rotation::None,
8373 Flip::None,
8374 crop,
8375 );
8376 let _ = tx.send(true);
8377 });
8378 match rx.recv_timeout(std::time::Duration::from_secs(20)) {
8379 Ok(_) => { let _ = worker.join(); }
8380 Err(_) => panic!(
8381 "cross-thread NV12→PlanarRgb convert HUNG (>20s) — reproduces the orchestrator deadlock"
8382 ),
8383 }
8384 }
8385
8386 #[test]
8393 #[cfg(target_os = "macos")]
8394 #[cfg(feature = "opengl")]
8395 fn test_nv_to_planar_f16_varying_sizes_no_leak_opengl_macos() {
8396 let mut gpu = match GLProcessorThreaded::new(None) {
8397 Ok(p) => p,
8398 Err(e) => {
8399 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8400 return;
8401 }
8402 };
8403 let (max_w, max_h) = (640usize, 640usize);
8406 let depth = 4usize;
8407 let mut srcs = Vec::new();
8408 let mut dsts = Vec::new();
8409 for _ in 0..depth {
8410 srcs.push(
8411 match TensorDyn::image(
8412 max_w,
8413 max_h * 3,
8414 PixelFormat::Grey,
8415 DType::U8,
8416 Some(TensorMemory::DmaBuf),
8417 edgefirst_tensor::CpuAccess::ReadWrite,
8418 ) {
8419 Ok(t) => t,
8420 Err(e) => {
8421 crate::test_support::report_skip(&format!("{} — pool: {e:?}", function!()));
8422 return;
8423 }
8424 },
8425 );
8426 dsts.push(
8427 match TensorDyn::image(
8428 640,
8429 640,
8430 PixelFormat::PlanarRgb,
8431 DType::F16,
8432 Some(TensorMemory::DmaBuf),
8433 edgefirst_tensor::CpuAccess::ReadWrite,
8434 ) {
8435 Ok(t) => t,
8436 Err(e) => {
8437 crate::test_support::report_skip(&format!("{} — dst: {e:?}", function!()));
8438 return;
8439 }
8440 },
8441 );
8442 }
8443 let sizes = [
8445 (640, 480),
8446 (500, 375),
8447 (640, 427),
8448 (333, 500),
8449 (480, 640),
8450 (612, 612),
8451 (428, 640),
8452 (576, 432),
8453 ];
8454 let mut first_ms = 0f64;
8455 let mut last_ms = 0f64;
8456 let iters = 40usize;
8457 for i in 0..iters {
8458 let (fw, fh) = sizes[i % sizes.len()];
8459 let src = &mut srcs[i % depth];
8460 let dst = &mut dsts[i % depth];
8461 src.configure_image(fw, fh, PixelFormat::Nv24).unwrap();
8462 src.as_typed::<u8>()
8463 .unwrap()
8464 .map()
8465 .unwrap()
8466 .as_mut_slice()
8467 .fill(128);
8468 let crop = Crop::new().with_source(Some(Region::new(0, 0, fw, fh)));
8469 let t0 = std::time::Instant::now();
8470 ImageProcessorTrait::convert(&mut gpu, src, dst, Rotation::None, Flip::None, crop)
8471 .unwrap_or_else(|e| panic!("convert iter {i} ({fw}×{fh}): {e}"));
8472 let ms = t0.elapsed().as_secs_f64() * 1e3;
8473 if i == 2 {
8474 first_ms = ms;
8475 }
8476 if i == iters - 1 {
8477 last_ms = ms;
8478 }
8479 }
8480 eprintln!("first={first_ms:.2}ms last={last_ms:.2}ms");
8481 assert!(
8482 last_ms < first_ms * 5.0 + 5.0,
8483 "convert latency ran away: first {first_ms:.2}ms → last {last_ms:.2}ms (intermediate/pbuffer leak)"
8484 );
8485 }
8486
8487 #[test]
8494 #[cfg(target_os = "macos")]
8495 #[cfg(feature = "opengl")]
8496 fn test_nv12_nv16_nv24_to_rgba_opengl_macos() {
8497 let mut gpu = match GLProcessorThreaded::new(None) {
8498 Ok(p) => p,
8499 Err(e) => {
8500 crate::test_support::report_skip(&format!(
8501 "{} — GL engine init failed ({e:?})",
8502 function!()
8503 ));
8504 return;
8505 }
8506 };
8507 let mut cpu = CPUProcessor::new();
8508
8509 let fill = |buf: &mut [u8], stride: usize, fmt: PixelFormat, w: usize, h: usize| {
8521 for y in 0..h {
8522 for x in 0..w {
8523 buf[y * stride + x] = ((x * 9 + y * 5) & 0xff) as u8;
8524 }
8525 }
8526 let (cw, ch, uv_grid_rows) = match fmt {
8527 PixelFormat::Nv12 => (w / 2, h / 2, 1usize),
8528 PixelFormat::Nv16 => (w / 2, h, 1usize),
8529 _ => (w, h, 2usize), };
8531 let uv_plane = h * stride;
8532 for cy in 0..ch {
8533 for cx in 0..cw {
8534 let off = uv_plane + cy * uv_grid_rows * stride + cx * 2;
8535 buf[off] = ((cx * 11 + 30) & 0xff) as u8;
8536 buf[off + 1] = ((cy * 7 + 200) & 0xff) as u8;
8537 }
8538 }
8539 };
8540
8541 for fmt in [PixelFormat::Nv12, PixelFormat::Nv16, PixelFormat::Nv24] {
8542 for (w, h) in [
8543 (16usize, 16usize), (15, 16), (16, 15), ] {
8547 let mem = TensorDyn::image(
8548 w,
8549 h,
8550 fmt,
8551 DType::U8,
8552 None,
8553 edgefirst_tensor::CpuAccess::ReadWrite,
8554 )
8555 .unwrap();
8556 let mem_stride = mem
8557 .as_typed::<u8>()
8558 .unwrap()
8559 .effective_row_stride()
8560 .unwrap();
8561 fill(
8562 mem.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
8563 mem_stride,
8564 fmt,
8565 w,
8566 h,
8567 );
8568 let cpu_dst = TensorDyn::image(
8569 w,
8570 h,
8571 PixelFormat::Rgba,
8572 DType::U8,
8573 None,
8574 edgefirst_tensor::CpuAccess::ReadWrite,
8575 )
8576 .unwrap();
8577 let (r, _s, cpu_dst) = convert_img(
8578 &mut cpu,
8579 mem,
8580 cpu_dst,
8581 Rotation::None,
8582 Flip::None,
8583 Crop::no_crop(),
8584 );
8585 r.unwrap_or_else(|e| panic!("CPU {fmt:?}->{w}x{h}->RGBA: {e}"));
8586
8587 let ios = TensorDyn::image(
8588 w,
8589 h,
8590 fmt,
8591 DType::U8,
8592 Some(TensorMemory::DmaBuf),
8593 edgefirst_tensor::CpuAccess::ReadWrite,
8594 )
8595 .unwrap_or_else(|e| panic!("{fmt:?} {w}x{h} IOSurface alloc: {e}"));
8596 let ios_stride = ios
8597 .as_typed::<u8>()
8598 .unwrap()
8599 .effective_row_stride()
8600 .unwrap();
8601 fill(
8602 ios.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
8603 ios_stride,
8604 fmt,
8605 w,
8606 h,
8607 );
8608 let gpu_dst = TensorDyn::image(
8609 w,
8610 h,
8611 PixelFormat::Rgba,
8612 DType::U8,
8613 Some(TensorMemory::DmaBuf),
8614 edgefirst_tensor::CpuAccess::ReadWrite,
8615 )
8616 .unwrap();
8617 let (r, _s, gpu_dst) = convert_img(
8618 &mut gpu,
8619 ios,
8620 gpu_dst,
8621 Rotation::None,
8622 Flip::None,
8623 Crop::no_crop(),
8624 );
8625 r.unwrap_or_else(|e| panic!("GPU {fmt:?}->{w}x{h}->RGBA on ANGLE: {e}"));
8626
8627 let cs = cpu_dst
8628 .as_typed::<u8>()
8629 .unwrap()
8630 .effective_row_stride()
8631 .unwrap();
8632 let cmap = cpu_dst.as_typed::<u8>().unwrap().map().unwrap();
8633 let cb = cmap.as_slice();
8634 let gs = gpu_dst
8635 .as_typed::<u8>()
8636 .unwrap()
8637 .effective_row_stride()
8638 .unwrap();
8639 let gmap = gpu_dst.as_typed::<u8>().unwrap().map().unwrap();
8640 let gb = gmap.as_slice();
8641 let mut max_d = 0i16;
8642 for y in 0..h {
8643 for x in 0..w {
8644 for c in 0..3 {
8645 let cv = cb[y * cs + x * 4 + c] as i16;
8646 let gv = gb[y * gs + x * 4 + c] as i16;
8647 max_d = max_d.max((cv - gv).abs());
8648 }
8649 }
8650 }
8651 assert!(
8652 max_d <= 3,
8653 "{fmt:?} {w}x{h}: GPU vs CPU RGBA max channel diff {max_d} > 3"
8654 );
8655 }
8656 }
8657 }
8658
8659 #[test]
8667 #[cfg(target_os = "macos")]
8668 #[cfg(feature = "opengl")]
8669 fn test_nv_to_rgba_larger_pool_surface_opengl_macos() {
8670 let mut gpu = match GLProcessorThreaded::new(None) {
8671 Ok(p) => p,
8672 Err(e) => {
8673 crate::test_support::report_skip(&format!(
8674 "{} — GL engine init failed ({e:?})",
8675 function!()
8676 ));
8677 return;
8678 }
8679 };
8680 let mut cpu = CPUProcessor::new();
8681 let (pool_w, pool_h) = (256usize, 256usize);
8684
8685 let fill = |buf: &mut [u8], stride: usize, fmt: PixelFormat, w: usize, h: usize| {
8689 for y in 0..h {
8690 for x in 0..w {
8691 buf[y * stride + x] = ((x * 9 + y * 5) & 0xff) as u8;
8692 }
8693 }
8694 let (cw, ch, uv_grid_rows) = match fmt {
8695 PixelFormat::Nv12 => (w / 2, h / 2, 1usize),
8696 PixelFormat::Nv16 => (w / 2, h, 1usize),
8697 _ => (w, h, 2usize), };
8699 let uv_plane = h * stride;
8700 for cy in 0..ch {
8701 for cx in 0..cw {
8702 let off = uv_plane + cy * uv_grid_rows * stride + cx * 2;
8703 buf[off] = ((cx * 11 + 30) & 0xff) as u8;
8704 buf[off + 1] = ((cy * 7 + 200) & 0xff) as u8;
8705 }
8706 }
8707 };
8708
8709 for fmt in [PixelFormat::Nv12, PixelFormat::Nv16, PixelFormat::Nv24] {
8710 for (w, h) in [
8711 (40usize, 24usize), (15, 16), (16, 15), ] {
8715 let ew = w.next_multiple_of(2);
8718
8719 let mem = TensorDyn::image(
8721 w,
8722 h,
8723 fmt,
8724 DType::U8,
8725 None,
8726 edgefirst_tensor::CpuAccess::ReadWrite,
8727 )
8728 .unwrap();
8729 let mem_stride = mem
8730 .as_typed::<u8>()
8731 .unwrap()
8732 .effective_row_stride()
8733 .unwrap();
8734 fill(
8735 mem.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
8736 mem_stride,
8737 fmt,
8738 w,
8739 h,
8740 );
8741 let cpu_dst = TensorDyn::image(
8742 w,
8743 h,
8744 PixelFormat::Rgba,
8745 DType::U8,
8746 None,
8747 edgefirst_tensor::CpuAccess::ReadWrite,
8748 )
8749 .unwrap();
8750 let (r, _s, cpu_dst) = convert_img(
8751 &mut cpu,
8752 mem,
8753 cpu_dst,
8754 Rotation::None,
8755 Flip::None,
8756 Crop::no_crop(),
8757 );
8758 r.unwrap_or_else(|e| panic!("CPU {fmt:?}->{w}x{h}->RGBA: {e}"));
8759
8760 let mut ios = match TensorDyn::image(
8764 pool_w,
8765 pool_h,
8766 PixelFormat::Grey,
8767 DType::U8,
8768 Some(TensorMemory::DmaBuf),
8769 edgefirst_tensor::CpuAccess::ReadWrite,
8770 ) {
8771 Ok(t) => t,
8772 Err(e) => {
8773 crate::test_support::report_skip(&format!(
8774 "{} — R8 pool IOSurface alloc: {e:?}",
8775 function!()
8776 ));
8777 return;
8778 }
8779 };
8780 ios.configure_image(w, h, fmt)
8781 .unwrap_or_else(|e| panic!("configure_image {fmt:?} {w}x{h} on pool: {e}"));
8782 let ios_stride = ios
8783 .as_typed::<u8>()
8784 .unwrap()
8785 .effective_row_stride()
8786 .unwrap();
8787 assert!(
8788 ios_stride > ew,
8789 "{fmt:?} {w}x{h}: pool stride {ios_stride} should exceed even width {ew} \
8790 (test must exercise padding)"
8791 );
8792 fill(
8793 ios.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
8794 ios_stride,
8795 fmt,
8796 w,
8797 h,
8798 );
8799
8800 let gpu_dst = TensorDyn::image(
8801 w,
8802 h,
8803 PixelFormat::Rgba,
8804 DType::U8,
8805 Some(TensorMemory::DmaBuf),
8806 edgefirst_tensor::CpuAccess::ReadWrite,
8807 )
8808 .unwrap();
8809 let (r, _s, gpu_dst) = convert_img(
8810 &mut gpu,
8811 ios,
8812 gpu_dst,
8813 Rotation::None,
8814 Flip::None,
8815 Crop::no_crop(),
8816 );
8817 r.unwrap_or_else(|e| {
8818 panic!("GPU {fmt:?}->{w}x{h}->RGBA (pool surface) on ANGLE: {e}")
8819 });
8820
8821 let cs = cpu_dst
8822 .as_typed::<u8>()
8823 .unwrap()
8824 .effective_row_stride()
8825 .unwrap();
8826 let cmap = cpu_dst.as_typed::<u8>().unwrap().map().unwrap();
8827 let cb = cmap.as_slice();
8828 let gs = gpu_dst
8829 .as_typed::<u8>()
8830 .unwrap()
8831 .effective_row_stride()
8832 .unwrap();
8833 let gmap = gpu_dst.as_typed::<u8>().unwrap().map().unwrap();
8834 let gb = gmap.as_slice();
8835 let mut max_d = 0i16;
8836 for y in 0..h {
8837 for x in 0..w {
8838 for c in 0..3 {
8839 let cv = cb[y * cs + x * 4 + c] as i16;
8840 let gv = gb[y * gs + x * 4 + c] as i16;
8841 max_d = max_d.max((cv - gv).abs());
8842 }
8843 }
8844 }
8845 assert!(
8846 max_d <= 3,
8847 "{fmt:?} {w}x{h}: GPU(pool surface) vs CPU RGBA max channel diff {max_d} > 3"
8848 );
8849 }
8850 }
8851 }
8852
8853 #[test]
8854 #[cfg(target_os = "macos")]
8855 #[cfg(feature = "opengl")]
8856 fn test_yuyv_to_rgba_opengl_macos() {
8857 let mut proc = match GLProcessorThreaded::new(None) {
8858 Ok(p) => p,
8859 Err(e) => {
8860 crate::test_support::report_skip(&format!(
8861 "{} — GL engine init failed ({e:?}). \
8862 Install ANGLE via `brew install startergo/angle/angle` \
8863 and re-sign per README.md § macOS GPU Acceleration to \
8864 run this test.",
8865 function!()
8866 ));
8867 return;
8868 }
8869 };
8870
8871 let src = load_bytes_to_tensor(
8872 1280,
8873 720,
8874 PixelFormat::Yuyv,
8875 Some(TensorMemory::DmaBuf),
8876 &edgefirst_bench::testdata::read("camera720p.yuyv"),
8877 )
8878 .unwrap();
8879
8880 let dst = TensorDyn::image(
8881 1280,
8882 720,
8883 PixelFormat::Rgba,
8884 DType::U8,
8885 Some(TensorMemory::DmaBuf),
8886 edgefirst_tensor::CpuAccess::ReadWrite,
8887 )
8888 .unwrap();
8889
8890 let (result, _src, dst) = convert_img(
8891 &mut proc,
8892 src,
8893 dst,
8894 Rotation::None,
8895 Flip::None,
8896 Crop::no_crop(),
8897 );
8898 result.unwrap();
8899
8900 let target_image = TensorDyn::image(
8901 1280,
8902 720,
8903 PixelFormat::Rgba,
8904 DType::U8,
8905 None,
8906 edgefirst_tensor::CpuAccess::ReadWrite,
8907 )
8908 .unwrap();
8909 target_image
8910 .as_typed::<u8>()
8911 .unwrap()
8912 .map()
8913 .unwrap()
8914 .as_mut_slice()
8915 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
8916
8917 compare_images(&dst, &target_image, 0.98, function!());
8922 }
8923
8924 #[test]
8943 #[cfg(target_os = "macos")]
8944 #[cfg(feature = "opengl")]
8945 fn test_yuyv_to_rgba_opengl_macos_multi_resolution() {
8946 let mut proc = match GLProcessorThreaded::new(None) {
8947 Ok(p) => p,
8948 Err(e) => {
8949 crate::test_support::report_skip(&format!(
8950 "{} — GL engine init failed ({e:?})",
8951 function!()
8952 ));
8953 return;
8954 }
8955 };
8956
8957 for (w, h) in [(64usize, 32usize), (3840, 2160)] {
8958 let bytes_per_row = w * 2;
8961 let mut yuyv = vec![0u8; bytes_per_row * h];
8962 for chunk in yuyv.as_chunks_mut::<4>().0 {
8963 chunk[0] = 128; chunk[1] = 128; chunk[2] = 128; chunk[3] = 128; }
8968
8969 let src =
8970 load_bytes_to_tensor(w, h, PixelFormat::Yuyv, Some(TensorMemory::DmaBuf), &yuyv)
8971 .unwrap();
8972
8973 let dst = TensorDyn::image(
8974 w,
8975 h,
8976 PixelFormat::Rgba,
8977 DType::U8,
8978 Some(TensorMemory::DmaBuf),
8979 edgefirst_tensor::CpuAccess::ReadWrite,
8980 )
8981 .unwrap();
8982
8983 let (result, _src, dst) = convert_img(
8984 &mut proc,
8985 src,
8986 dst,
8987 Rotation::None,
8988 Flip::None,
8989 Crop::no_crop(),
8990 );
8991 result.expect("GL convert should succeed at this resolution");
8992
8993 let dst_u8 = dst.as_typed::<u8>().unwrap();
8998 let dst_map = dst_u8.map().unwrap();
8999 let dst_bytes = dst_map.as_slice();
9000 assert_eq!(dst_bytes.len(), w * h * 4, "RGBA byte count");
9001 for px in dst_bytes.as_chunks::<4>().0 {
9002 for (i, &c) in px[..3].iter().enumerate() {
9003 assert!(
9004 (120..=140).contains(&c),
9005 "{}: channel {i} = {c} (expected ~128 ±12) at {w}×{h}",
9006 function!(),
9007 );
9008 }
9009 assert_eq!(px[3], 255, "alpha must be 1.0");
9010 }
9011 }
9012 }
9013
9014 #[test]
9024 #[cfg(target_os = "macos")]
9025 #[cfg(feature = "opengl")]
9026 fn test_macos_gl_pbuffer_cache_reuses_surfaces() {
9027 let mut proc = match GLProcessorThreaded::new(None) {
9028 Ok(p) => p,
9029 Err(e) => {
9030 crate::test_support::report_skip(&format!(
9031 "{} — GL engine init failed ({e:?})",
9032 function!()
9033 ));
9034 return;
9035 }
9036 };
9037
9038 let mut yuyv = vec![0u8; 64 * 32 * 2];
9040 for chunk in yuyv.as_chunks_mut::<4>().0 {
9041 chunk[0] = 200;
9042 chunk[1] = 100;
9043 chunk[2] = 200;
9044 chunk[3] = 156;
9045 }
9046 let src =
9047 load_bytes_to_tensor(64, 32, PixelFormat::Yuyv, Some(TensorMemory::DmaBuf), &yuyv)
9048 .unwrap();
9049 let dst = TensorDyn::image(
9050 64,
9051 32,
9052 PixelFormat::Rgba,
9053 DType::U8,
9054 Some(TensorMemory::DmaBuf),
9055 edgefirst_tensor::CpuAccess::ReadWrite,
9056 )
9057 .unwrap();
9058
9059 let (r1, src, dst) = convert_img(
9060 &mut proc,
9061 src,
9062 dst,
9063 Rotation::None,
9064 Flip::None,
9065 Crop::no_crop(),
9066 );
9067 r1.unwrap();
9068 let first: Vec<u8> = dst
9069 .as_typed::<u8>()
9070 .unwrap()
9071 .map()
9072 .unwrap()
9073 .as_slice()
9074 .to_vec();
9075
9076 let (r2, _src, dst) = convert_img(
9077 &mut proc,
9078 src,
9079 dst,
9080 Rotation::None,
9081 Flip::None,
9082 Crop::no_crop(),
9083 );
9084 r2.unwrap();
9085 let second: Vec<u8> = dst
9086 .as_typed::<u8>()
9087 .unwrap()
9088 .map()
9089 .unwrap()
9090 .as_slice()
9091 .to_vec();
9092
9093 assert_eq!(first, second, "cache-hit conversion must be deterministic");
9094 }
9095
9096 #[test]
9104 #[cfg(target_os = "macos")]
9105 #[cfg(feature = "opengl")]
9106 fn test_macos_gl_pbuffer_cache_steady_state() {
9107 let mut proc = match GLProcessorThreaded::new(None) {
9108 Ok(p) => p,
9109 Err(e) => {
9110 crate::test_support::report_skip(&format!(
9111 "{} — GL engine init failed ({e:?})",
9112 function!()
9113 ));
9114 return;
9115 }
9116 };
9117
9118 let (w, h) = (64usize, 32usize);
9119 const POOL: usize = 3;
9120 const FRAMES: usize = 100;
9121
9122 let yuyv = vec![128u8; w * h * 2];
9123 let pool: Vec<TensorDyn> = (0..POOL)
9124 .map(|_| {
9125 load_bytes_to_tensor(w, h, PixelFormat::Yuyv, Some(TensorMemory::DmaBuf), &yuyv)
9126 .unwrap()
9127 })
9128 .collect();
9129 let mut dst = TensorDyn::image(
9130 w,
9131 h,
9132 PixelFormat::Rgba,
9133 DType::U8,
9134 Some(TensorMemory::DmaBuf),
9135 edgefirst_tensor::CpuAccess::ReadWrite,
9136 )
9137 .unwrap();
9138
9139 for src in pool.iter().cycle().take(POOL * 2) {
9141 proc.convert(src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
9142 .unwrap();
9143 }
9144 let warm = proc.egl_cache_stats().unwrap();
9145
9146 for src in pool.iter().cycle().take(FRAMES) {
9147 proc.convert(src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
9148 .unwrap();
9149 }
9150 let steady = proc.egl_cache_stats().unwrap();
9151
9152 assert_eq!(
9153 warm.total_misses(),
9154 steady.total_misses(),
9155 "steady-state loop created new imports (warm {warm:?}, steady {steady:?})"
9156 );
9157 let hits = |s: &GlCacheStats| s.src.hits + s.dst.hits + s.nv_r8.hits;
9158 assert!(
9159 hits(&steady) - hits(&warm) >= FRAMES as u64,
9160 "expected at least {FRAMES} import-cache hits over the loop, got {}",
9161 hits(&steady) - hits(&warm)
9162 );
9163 }
9164
9165 #[test]
9177 #[cfg(target_os = "macos")]
9178 #[cfg(feature = "opengl")]
9179 fn test_macos_gl_f16_planar_is_gl_backed() {
9180 let mut proc = ImageProcessor::new().expect("ImageProcessor");
9181 let Some(ref gl) = proc.opengl else {
9182 crate::test_support::report_skip(&format!("{} — GL backend unavailable", function!()));
9183 return;
9184 };
9185 if !gl.supported_render_dtypes().f16 {
9186 crate::test_support::report_skip(&format!(
9187 "{} — configuration lacks F16 color-buffer support",
9188 function!()
9189 ));
9190 return;
9191 }
9192 let stats_before = gl.egl_cache_stats().expect("cache stats");
9193
9194 let src = TensorDyn::image(
9195 1280,
9196 720,
9197 PixelFormat::Nv12,
9198 DType::U8,
9199 Some(TensorMemory::DmaBuf),
9200 edgefirst_tensor::CpuAccess::ReadWrite,
9201 )
9202 .unwrap();
9203 {
9204 let t = src.as_typed::<u8>().unwrap();
9205 let mut m = t.map().unwrap();
9206 for (i, b) in m.as_mut_slice().iter_mut().enumerate() {
9207 *b = ((i * 31) % 211) as u8;
9208 }
9209 }
9210 let mut dst = TensorDyn::image(
9211 640,
9212 640,
9213 PixelFormat::PlanarRgb,
9214 DType::F16,
9215 Some(TensorMemory::DmaBuf),
9216 edgefirst_tensor::CpuAccess::ReadWrite,
9217 )
9218 .unwrap();
9219
9220 proc.convert(
9221 &src,
9222 &mut dst,
9223 Rotation::None,
9224 Flip::None,
9225 Crop::letterbox([114, 114, 114, 255]),
9226 )
9227 .expect("F16 capability reported but the NV12→PlanarF16 convert failed");
9228 let stats_after = proc
9229 .opengl
9230 .as_ref()
9231 .expect("GL backend present")
9232 .egl_cache_stats()
9233 .expect("cache stats");
9234 assert!(
9239 stats_after.total_misses() >= stats_before.total_misses() + 2,
9240 "convert succeeded but the GL engine did not import both the \
9241 source and the F16 destination — the work did not (fully) run \
9242 on the GL backend (silent CPU fallback); misses before={} after={}",
9243 stats_before.total_misses(),
9244 stats_after.total_misses()
9245 );
9246 }
9247
9248 #[test]
9254 #[cfg(feature = "opengl")]
9255 fn test_nv12_to_planar_f16_fused_engine_vs_cpu() {
9256 let mut gl = match ImageProcessor::with_config(ImageProcessorConfig {
9257 backend: ComputeBackend::OpenGl,
9258 ..Default::default()
9259 }) {
9260 Ok(p) if p.opengl.is_some() => p,
9261 _ => {
9262 crate::test_support::report_skip(&format!(
9263 "{} — GL backend unavailable",
9264 function!()
9265 ));
9266 return;
9267 }
9268 };
9269 if !gl
9270 .opengl
9271 .as_ref()
9272 .map(|g| g.supported_render_dtypes().f16)
9273 .unwrap_or(false)
9274 {
9275 crate::test_support::report_skip(&format!("{} — no F16 render support", function!()));
9276 return;
9277 }
9278 let mem = if edgefirst_tensor::is_gpu_buffer_available() {
9279 TensorMemory::DmaBuf
9280 } else {
9281 crate::test_support::report_skip(&format!("{} — no zero-copy buffers", function!()));
9282 return;
9283 };
9284
9285 let src = TensorDyn::image(
9286 1280,
9287 720,
9288 PixelFormat::Nv12,
9289 DType::U8,
9290 Some(mem),
9291 edgefirst_tensor::CpuAccess::ReadWrite,
9292 )
9293 .unwrap();
9294 {
9295 let t = src.as_typed::<u8>().unwrap();
9301 let mut m = t.map().unwrap();
9302 let buf = m.as_mut_slice();
9303 let (w, h) = (1280usize, 720usize);
9304 for y in 0..h {
9305 for x in 0..w {
9306 buf[y * w + x] = ((x * 255) / w) as u8; }
9308 }
9309 for y in 0..(h / 2) {
9310 for x in 0..(w / 2) {
9311 let o = h * w + y * w + 2 * x;
9312 buf[o] = ((y * 255) / (h / 2)) as u8; buf[o + 1] = (((x + y) * 255) / (w / 2 + h / 2)) as u8; }
9315 }
9316 }
9317 let crop = Crop::letterbox([114, 114, 114, 255]);
9318 let mut gl_dst = TensorDyn::image(
9319 640,
9320 640,
9321 PixelFormat::PlanarRgb,
9322 DType::F16,
9323 Some(mem),
9324 edgefirst_tensor::CpuAccess::ReadWrite,
9325 )
9326 .unwrap();
9327 match gl.opengl.as_mut().expect("GL backend present").convert(
9334 &src,
9335 &mut gl_dst,
9336 Rotation::None,
9337 Flip::None,
9338 crop,
9339 ) {
9340 Ok(()) => {}
9341 Err(crate::Error::NotSupported(msg)) => {
9342 crate::test_support::report_skip(&format!("{} — {msg}", function!()));
9343 return;
9344 }
9345 Err(e) => panic!("fused NV12→PlanarF16 GL convert: {e}"),
9346 }
9347
9348 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
9349 backend: ComputeBackend::Cpu,
9350 ..Default::default()
9351 })
9352 .unwrap();
9353 let mut cpu_dst = TensorDyn::image(
9354 640,
9355 640,
9356 PixelFormat::PlanarRgb,
9357 DType::F16,
9358 Some(TensorMemory::Mem),
9359 edgefirst_tensor::CpuAccess::ReadWrite,
9360 )
9361 .unwrap();
9362 cpu.convert(&src, &mut cpu_dst, Rotation::None, Flip::None, crop)
9363 .expect("CPU reference convert");
9364
9365 let g = gl_dst
9366 .as_typed::<half::f16>()
9367 .unwrap()
9368 .map()
9369 .unwrap()
9370 .as_slice()
9371 .to_vec();
9372 let c = cpu_dst
9373 .as_typed::<half::f16>()
9374 .unwrap()
9375 .map()
9376 .unwrap()
9377 .as_slice()
9378 .to_vec();
9379 assert_eq!(g.len(), c.len());
9380 let mut max_diff = 0.0f32;
9381 let mut max_at = 0usize;
9382 for (i, (a, b)) in g.iter().zip(c.iter()).enumerate() {
9383 let d = (a.to_f32() - b.to_f32()).abs();
9384 if d > max_diff {
9385 max_diff = d;
9386 max_at = i;
9387 }
9388 }
9389 let (plane, rem) = (max_at / (640 * 640), max_at % (640 * 640));
9391 let (row, col) = (rem / 640, rem % 640);
9392 eprintln!(
9393 "fused-vs-cpu: max_diff={max_diff} at plane={plane} row={row} col={col} \
9394 gl={} cpu={}",
9395 g[max_at].to_f32(),
9396 c[max_at].to_f32()
9397 );
9398 assert!(
9401 max_diff <= 4.0 / 255.0 + 1e-3,
9402 "fused NV12→PlanarF16 diverges from CPU reference: max_diff={max_diff}"
9403 );
9404 }
9405
9406 #[test]
9416 #[cfg(feature = "opengl")]
9417 fn test_zero_copy_src_to_mem_dst_gl_direct() {
9418 let mut proc = match ImageProcessor::new() {
9419 Ok(p) if p.opengl.is_some() => p,
9420 _ => {
9421 crate::test_support::report_skip(&format!(
9422 "{} — GL backend unavailable",
9423 function!()
9424 ));
9425 return;
9426 }
9427 };
9428 if !edgefirst_tensor::is_gpu_buffer_available() {
9429 crate::test_support::report_skip(&format!("{} — no zero-copy buffers", function!()));
9430 return;
9431 }
9432
9433 let src = TensorDyn::image(
9434 1280,
9435 720,
9436 PixelFormat::Rgba,
9437 DType::U8,
9438 Some(TensorMemory::DmaBuf),
9439 edgefirst_tensor::CpuAccess::ReadWrite,
9440 )
9441 .unwrap();
9442 {
9443 let t = src.as_typed::<u8>().unwrap();
9444 let mut m = t.map().unwrap();
9445 for (i, b) in m.as_mut_slice().iter_mut().enumerate() {
9446 *b = ((i * 31) % 211) as u8;
9447 }
9448 }
9449 let mut gl_dst = TensorDyn::image(
9450 1280,
9451 720,
9452 PixelFormat::Bgra,
9453 DType::U8,
9454 Some(TensorMemory::Mem),
9455 edgefirst_tensor::CpuAccess::ReadWrite,
9456 )
9457 .unwrap();
9458 proc.opengl
9459 .as_mut()
9460 .expect("GL backend present")
9461 .convert(
9462 &src,
9463 &mut gl_dst,
9464 Rotation::None,
9465 Flip::None,
9466 Crop::no_crop(),
9467 )
9468 .expect("zero-copy src → heap dst GL convert");
9469
9470 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
9471 backend: ComputeBackend::Cpu,
9472 ..Default::default()
9473 })
9474 .unwrap();
9475 let mut cpu_dst = TensorDyn::image(
9476 1280,
9477 720,
9478 PixelFormat::Bgra,
9479 DType::U8,
9480 Some(TensorMemory::Mem),
9481 edgefirst_tensor::CpuAccess::ReadWrite,
9482 )
9483 .unwrap();
9484 cpu.convert(
9485 &src,
9486 &mut cpu_dst,
9487 Rotation::None,
9488 Flip::None,
9489 Crop::no_crop(),
9490 )
9491 .expect("CPU reference convert");
9492
9493 let g = gl_dst
9494 .as_typed::<u8>()
9495 .unwrap()
9496 .map()
9497 .unwrap()
9498 .as_slice()
9499 .to_vec();
9500 let c = cpu_dst
9501 .as_typed::<u8>()
9502 .unwrap()
9503 .map()
9504 .unwrap()
9505 .as_slice()
9506 .to_vec();
9507 assert_eq!(g.len(), c.len());
9508 let max_diff = g
9509 .iter()
9510 .zip(c.iter())
9511 .map(|(a, b)| a.abs_diff(*b))
9512 .max()
9513 .unwrap();
9514 assert!(
9517 max_diff <= 2,
9518 "zero-copy src → heap dst diverges from CPU reference: max_diff={max_diff}"
9519 );
9520 }
9521
9522 #[test]
9523 #[cfg(target_os = "linux")]
9524 fn test_yuyv_to_rgb_g2d() {
9525 if !is_g2d_available() {
9526 crate::test_support::report_skip(
9527 "test_yuyv_to_rgb_g2d - G2D library (libg2d.so.2) not available",
9528 );
9529 return;
9530 }
9531 if !is_dma_available() {
9532 crate::test_support::report_skip("test_yuyv_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
9533 return;
9534 }
9535
9536 let src = load_bytes_to_tensor(
9537 1280,
9538 720,
9539 PixelFormat::Yuyv,
9540 None,
9541 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9542 )
9543 .unwrap();
9544
9545 let g2d_dst = TensorDyn::image(
9546 1280,
9547 720,
9548 PixelFormat::Rgb,
9549 DType::U8,
9550 Some(TensorMemory::DmaBuf),
9551 edgefirst_tensor::CpuAccess::ReadWrite,
9552 )
9553 .unwrap();
9554 let mut g2d_converter = G2DProcessor::new().unwrap();
9555
9556 let (result, src, g2d_dst) = convert_img(
9557 &mut g2d_converter,
9558 src,
9559 g2d_dst,
9560 Rotation::None,
9561 Flip::None,
9562 Crop::no_crop(),
9563 );
9564 result.unwrap();
9565
9566 let cpu_dst = TensorDyn::image(
9567 1280,
9568 720,
9569 PixelFormat::Rgb,
9570 DType::U8,
9571 None,
9572 edgefirst_tensor::CpuAccess::ReadWrite,
9573 )
9574 .unwrap();
9575 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
9576
9577 let (result, _src, cpu_dst) = convert_img(
9578 &mut cpu_converter,
9579 src,
9580 cpu_dst,
9581 Rotation::None,
9582 Flip::None,
9583 Crop::no_crop(),
9584 );
9585 result.unwrap();
9586
9587 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
9593 }
9594
9595 #[test]
9596 #[cfg(target_os = "linux")]
9597 fn test_yuyv_to_yuyv_resize_g2d() {
9598 if !is_g2d_available() {
9599 crate::test_support::report_skip(
9600 "test_yuyv_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available",
9601 );
9602 return;
9603 }
9604 if !is_dma_available() {
9605 crate::test_support::report_skip("test_yuyv_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
9606 return;
9607 }
9608
9609 let src = load_bytes_to_tensor(
9610 1280,
9611 720,
9612 PixelFormat::Yuyv,
9613 None,
9614 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9615 )
9616 .unwrap();
9617
9618 let g2d_dst = TensorDyn::image(
9619 600,
9620 400,
9621 PixelFormat::Yuyv,
9622 DType::U8,
9623 Some(TensorMemory::DmaBuf),
9624 edgefirst_tensor::CpuAccess::ReadWrite,
9625 )
9626 .unwrap();
9627 let mut g2d_converter = G2DProcessor::new().unwrap();
9628
9629 let (result, src, g2d_dst) = convert_img(
9630 &mut g2d_converter,
9631 src,
9632 g2d_dst,
9633 Rotation::None,
9634 Flip::None,
9635 Crop::no_crop(),
9636 );
9637 result.unwrap();
9638
9639 let cpu_dst = TensorDyn::image(
9640 600,
9641 400,
9642 PixelFormat::Yuyv,
9643 DType::U8,
9644 None,
9645 edgefirst_tensor::CpuAccess::ReadWrite,
9646 )
9647 .unwrap();
9648 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
9649
9650 let (result, _src, cpu_dst) = convert_img(
9651 &mut cpu_converter,
9652 src,
9653 cpu_dst,
9654 Rotation::None,
9655 Flip::None,
9656 Crop::no_crop(),
9657 );
9658 result.unwrap();
9659
9660 eprintln!(
9667 "WARNING: G2D has poor colorimetry support — YUYV resize diverges from the \
9668 CPU reference (~0.85 similarity); threshold held at 0.85, not 0.95."
9669 );
9670 compare_images_convert_to_rgb(&g2d_dst, &cpu_dst, 0.85, function!());
9671 }
9672
9673 #[test]
9674 fn test_yuyv_to_rgba_resize_cpu() {
9675 let src = load_bytes_to_tensor(
9676 1280,
9677 720,
9678 PixelFormat::Yuyv,
9679 None,
9680 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9681 )
9682 .unwrap();
9683
9684 let (dst_width, dst_height) = (960, 540);
9685
9686 let dst = TensorDyn::image(
9687 dst_width,
9688 dst_height,
9689 PixelFormat::Rgba,
9690 DType::U8,
9691 None,
9692 edgefirst_tensor::CpuAccess::ReadWrite,
9693 )
9694 .unwrap();
9695 let mut cpu_converter = CPUProcessor::new();
9696
9697 let (result, _src, dst) = convert_img(
9698 &mut cpu_converter,
9699 src,
9700 dst,
9701 Rotation::None,
9702 Flip::None,
9703 Crop::no_crop(),
9704 );
9705 result.unwrap();
9706
9707 let dst_target = TensorDyn::image(
9708 dst_width,
9709 dst_height,
9710 PixelFormat::Rgba,
9711 DType::U8,
9712 None,
9713 edgefirst_tensor::CpuAccess::ReadWrite,
9714 )
9715 .unwrap();
9716 let src_target = load_bytes_to_tensor(
9717 1280,
9718 720,
9719 PixelFormat::Rgba,
9720 None,
9721 &edgefirst_bench::testdata::read("camera720p.rgba"),
9722 )
9723 .unwrap();
9724 let (result, _src_target, dst_target) = convert_img(
9725 &mut cpu_converter,
9726 src_target,
9727 dst_target,
9728 Rotation::None,
9729 Flip::None,
9730 Crop::no_crop(),
9731 );
9732 result.unwrap();
9733
9734 compare_images(&dst, &dst_target, 0.98, function!());
9737 }
9738
9739 #[test]
9740 #[cfg(target_os = "linux")]
9741 fn test_yuyv_to_rgba_crop_flip_g2d() {
9742 if !is_g2d_available() {
9743 crate::test_support::report_skip(
9744 "test_yuyv_to_rgba_crop_flip_g2d - G2D library (libg2d.so.2) not available",
9745 );
9746 return;
9747 }
9748 if !is_dma_available() {
9749 crate::test_support::report_skip("test_yuyv_to_rgba_crop_flip_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
9750 return;
9751 }
9752
9753 let src = load_bytes_to_tensor(
9754 1280,
9755 720,
9756 PixelFormat::Yuyv,
9757 Some(TensorMemory::DmaBuf),
9758 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9759 )
9760 .unwrap();
9761
9762 let (dst_width, dst_height) = (640, 640);
9763
9764 let dst_g2d = TensorDyn::image(
9765 dst_width,
9766 dst_height,
9767 PixelFormat::Rgba,
9768 DType::U8,
9769 Some(TensorMemory::DmaBuf),
9770 edgefirst_tensor::CpuAccess::ReadWrite,
9771 )
9772 .unwrap();
9773 let mut g2d_converter = G2DProcessor::new().unwrap();
9774 let crop = Crop::new().with_source(Some(Region::new(20, 15, 400, 300)));
9775
9776 let (result, src, dst_g2d) = convert_img(
9777 &mut g2d_converter,
9778 src,
9779 dst_g2d,
9780 Rotation::None,
9781 Flip::Horizontal,
9782 crop,
9783 );
9784 result.unwrap();
9785
9786 let dst_cpu = TensorDyn::image(
9787 dst_width,
9788 dst_height,
9789 PixelFormat::Rgba,
9790 DType::U8,
9791 Some(TensorMemory::DmaBuf),
9792 edgefirst_tensor::CpuAccess::ReadWrite,
9793 )
9794 .unwrap();
9795 let mut cpu_converter = CPUProcessor::new();
9796
9797 let (result, _src, dst_cpu) = convert_img(
9798 &mut cpu_converter,
9799 src,
9800 dst_cpu,
9801 Rotation::None,
9802 Flip::Horizontal,
9803 crop,
9804 );
9805 result.unwrap();
9806 compare_images(&dst_g2d, &dst_cpu, 0.98, function!());
9812 }
9813
9814 #[test]
9815 #[cfg(any(
9816 target_os = "linux",
9817 target_os = "macos",
9818 target_os = "ios",
9819 target_os = "android",
9820 target_os = "windows"
9821 ))]
9822 #[cfg(feature = "opengl")]
9823 fn test_yuyv_to_rgba_crop_flip_opengl() {
9824 if !is_opengl_available() {
9825 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
9826 return;
9827 }
9828
9829 if !edgefirst_tensor::is_gpu_buffer_available() {
9830 crate::test_support::report_skip(&format!(
9831 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
9832 function!()
9833 ));
9834 return;
9835 }
9836
9837 let src = load_bytes_to_tensor(
9838 1280,
9839 720,
9840 PixelFormat::Yuyv,
9841 Some(TensorMemory::DmaBuf),
9842 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9843 )
9844 .unwrap();
9845
9846 let (dst_width, dst_height) = (640, 640);
9847
9848 let dst_gl = TensorDyn::image(
9849 dst_width,
9850 dst_height,
9851 PixelFormat::Rgba,
9852 DType::U8,
9853 Some(TensorMemory::DmaBuf),
9854 edgefirst_tensor::CpuAccess::ReadWrite,
9855 )
9856 .unwrap();
9857 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
9858 let crop = Crop::new().with_source(Some(Region::new(20, 15, 400, 300)));
9859
9860 let (result, src, dst_gl) = convert_img(
9861 &mut gl_converter,
9862 src,
9863 dst_gl,
9864 Rotation::None,
9865 Flip::Horizontal,
9866 crop,
9867 );
9868 if let Err(crate::Error::NotSupported(msg)) = &result {
9870 crate::test_support::report_skip(&format!("{} — {msg}", function!()));
9871 return;
9872 }
9873 result.unwrap();
9874
9875 let dst_cpu = TensorDyn::image(
9876 dst_width,
9877 dst_height,
9878 PixelFormat::Rgba,
9879 DType::U8,
9880 Some(TensorMemory::DmaBuf),
9881 edgefirst_tensor::CpuAccess::ReadWrite,
9882 )
9883 .unwrap();
9884 let mut cpu_converter = CPUProcessor::new();
9885
9886 let (result, _src, dst_cpu) = convert_img(
9887 &mut cpu_converter,
9888 src,
9889 dst_cpu,
9890 Rotation::None,
9891 Flip::Horizontal,
9892 crop,
9893 );
9894 result.unwrap();
9895 compare_images(&dst_gl, &dst_cpu, 0.98, function!());
9900 }
9901
9902 #[test]
9903 fn test_vyuy_to_rgba_cpu() {
9904 let file = edgefirst_bench::testdata::read("camera720p.vyuy").to_vec();
9905 let src = TensorDyn::image(
9906 1280,
9907 720,
9908 PixelFormat::Vyuy,
9909 DType::U8,
9910 None,
9911 edgefirst_tensor::CpuAccess::ReadWrite,
9912 )
9913 .unwrap();
9914 src.as_typed::<u8>()
9915 .unwrap()
9916 .map()
9917 .unwrap()
9918 .as_mut_slice()
9919 .copy_from_slice(&file);
9920
9921 let dst = TensorDyn::image(
9922 1280,
9923 720,
9924 PixelFormat::Rgba,
9925 DType::U8,
9926 None,
9927 edgefirst_tensor::CpuAccess::ReadWrite,
9928 )
9929 .unwrap();
9930 let mut cpu_converter = CPUProcessor::new();
9931
9932 let (result, _src, dst) = convert_img(
9933 &mut cpu_converter,
9934 src,
9935 dst,
9936 Rotation::None,
9937 Flip::None,
9938 Crop::no_crop(),
9939 );
9940 result.unwrap();
9941
9942 let target_image = TensorDyn::image(
9943 1280,
9944 720,
9945 PixelFormat::Rgba,
9946 DType::U8,
9947 None,
9948 edgefirst_tensor::CpuAccess::ReadWrite,
9949 )
9950 .unwrap();
9951 target_image
9952 .as_typed::<u8>()
9953 .unwrap()
9954 .map()
9955 .unwrap()
9956 .as_mut_slice()
9957 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
9958
9959 compare_images(&dst, &target_image, 0.98, function!());
9962 }
9963
9964 #[test]
9965 fn test_vyuy_to_rgb_cpu() {
9966 let file = edgefirst_bench::testdata::read("camera720p.vyuy").to_vec();
9967 let src = TensorDyn::image(
9968 1280,
9969 720,
9970 PixelFormat::Vyuy,
9971 DType::U8,
9972 None,
9973 edgefirst_tensor::CpuAccess::ReadWrite,
9974 )
9975 .unwrap();
9976 src.as_typed::<u8>()
9977 .unwrap()
9978 .map()
9979 .unwrap()
9980 .as_mut_slice()
9981 .copy_from_slice(&file);
9982
9983 let dst = TensorDyn::image(
9984 1280,
9985 720,
9986 PixelFormat::Rgb,
9987 DType::U8,
9988 None,
9989 edgefirst_tensor::CpuAccess::ReadWrite,
9990 )
9991 .unwrap();
9992 let mut cpu_converter = CPUProcessor::new();
9993
9994 let (result, _src, dst) = convert_img(
9995 &mut cpu_converter,
9996 src,
9997 dst,
9998 Rotation::None,
9999 Flip::None,
10000 Crop::no_crop(),
10001 );
10002 result.unwrap();
10003
10004 let target_image = TensorDyn::image(
10005 1280,
10006 720,
10007 PixelFormat::Rgb,
10008 DType::U8,
10009 None,
10010 edgefirst_tensor::CpuAccess::ReadWrite,
10011 )
10012 .unwrap();
10013 target_image
10014 .as_typed::<u8>()
10015 .unwrap()
10016 .map()
10017 .unwrap()
10018 .as_mut_slice()
10019 .as_chunks_mut::<3>()
10020 .0
10021 .iter_mut()
10022 .zip(
10023 edgefirst_bench::testdata::read("camera720p.rgba")
10024 .as_chunks::<4>()
10025 .0,
10026 )
10027 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
10028
10029 compare_images(&dst, &target_image, 0.98, function!());
10032 }
10033
10034 #[test]
10035 #[cfg(target_os = "linux")]
10036 #[ignore = "G2D does not support VYUY; re-enable when hardware support is added"]
10037 fn test_vyuy_to_rgba_g2d() {
10038 if !is_g2d_available() {
10039 crate::test_support::report_skip(
10040 "test_vyuy_to_rgba_g2d - G2D library (libg2d.so.2) not available",
10041 );
10042 return;
10043 }
10044 if !is_dma_available() {
10045 crate::test_support::report_skip("test_vyuy_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
10046 return;
10047 }
10048
10049 let src = load_bytes_to_tensor(
10050 1280,
10051 720,
10052 PixelFormat::Vyuy,
10053 None,
10054 &edgefirst_bench::testdata::read("camera720p.vyuy"),
10055 )
10056 .unwrap();
10057
10058 let dst = TensorDyn::image(
10059 1280,
10060 720,
10061 PixelFormat::Rgba,
10062 DType::U8,
10063 Some(TensorMemory::DmaBuf),
10064 edgefirst_tensor::CpuAccess::ReadWrite,
10065 )
10066 .unwrap();
10067 let mut g2d_converter = G2DProcessor::new().unwrap();
10068
10069 let (result, _src, dst) = convert_img(
10070 &mut g2d_converter,
10071 src,
10072 dst,
10073 Rotation::None,
10074 Flip::None,
10075 Crop::no_crop(),
10076 );
10077 match result {
10078 Err(Error::G2D(_)) => {
10079 crate::test_support::report_skip(
10080 "test_vyuy_to_rgba_g2d - G2D does not support PixelFormat::Vyuy format",
10081 );
10082 return;
10083 }
10084 r => r.unwrap(),
10085 }
10086
10087 let target_image = TensorDyn::image(
10088 1280,
10089 720,
10090 PixelFormat::Rgba,
10091 DType::U8,
10092 None,
10093 edgefirst_tensor::CpuAccess::ReadWrite,
10094 )
10095 .unwrap();
10096 target_image
10097 .as_typed::<u8>()
10098 .unwrap()
10099 .map()
10100 .unwrap()
10101 .as_mut_slice()
10102 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
10103
10104 compare_images(&dst, &target_image, 0.98, function!());
10108 }
10109
10110 #[test]
10111 #[cfg(target_os = "linux")]
10112 #[ignore = "G2D does not support VYUY; re-enable when hardware support is added"]
10113 fn test_vyuy_to_rgb_g2d() {
10114 if !is_g2d_available() {
10115 crate::test_support::report_skip(
10116 "test_vyuy_to_rgb_g2d - G2D library (libg2d.so.2) not available",
10117 );
10118 return;
10119 }
10120 if !is_dma_available() {
10121 crate::test_support::report_skip("test_vyuy_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
10122 return;
10123 }
10124
10125 let src = load_bytes_to_tensor(
10126 1280,
10127 720,
10128 PixelFormat::Vyuy,
10129 None,
10130 &edgefirst_bench::testdata::read("camera720p.vyuy"),
10131 )
10132 .unwrap();
10133
10134 let g2d_dst = TensorDyn::image(
10135 1280,
10136 720,
10137 PixelFormat::Rgb,
10138 DType::U8,
10139 Some(TensorMemory::DmaBuf),
10140 edgefirst_tensor::CpuAccess::ReadWrite,
10141 )
10142 .unwrap();
10143 let mut g2d_converter = G2DProcessor::new().unwrap();
10144
10145 let (result, src, g2d_dst) = convert_img(
10146 &mut g2d_converter,
10147 src,
10148 g2d_dst,
10149 Rotation::None,
10150 Flip::None,
10151 Crop::no_crop(),
10152 );
10153 match result {
10154 Err(Error::G2D(_)) => {
10155 crate::test_support::report_skip(
10156 "test_vyuy_to_rgb_g2d - G2D does not support PixelFormat::Vyuy format",
10157 );
10158 return;
10159 }
10160 r => r.unwrap(),
10161 }
10162
10163 let cpu_dst = TensorDyn::image(
10164 1280,
10165 720,
10166 PixelFormat::Rgb,
10167 DType::U8,
10168 None,
10169 edgefirst_tensor::CpuAccess::ReadWrite,
10170 )
10171 .unwrap();
10172 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
10173
10174 let (result, _src, cpu_dst) = convert_img(
10175 &mut cpu_converter,
10176 src,
10177 cpu_dst,
10178 Rotation::None,
10179 Flip::None,
10180 Crop::no_crop(),
10181 );
10182 result.unwrap();
10183
10184 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
10190 }
10191
10192 #[test]
10193 #[cfg(any(
10194 target_os = "linux",
10195 target_os = "macos",
10196 target_os = "ios",
10197 target_os = "android",
10198 target_os = "windows"
10199 ))]
10200 #[cfg(feature = "opengl")]
10201 fn test_vyuy_to_rgba_opengl() {
10202 if !is_opengl_available() {
10203 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
10204 return;
10205 }
10206 if !edgefirst_tensor::is_gpu_buffer_available() {
10207 crate::test_support::report_skip(&format!(
10208 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
10209 function!()
10210 ));
10211 return;
10212 }
10213
10214 let src = match load_bytes_to_tensor(
10218 1280,
10219 720,
10220 PixelFormat::Vyuy,
10221 Some(TensorMemory::DmaBuf),
10222 &edgefirst_bench::testdata::read("camera720p.vyuy"),
10223 ) {
10224 Ok(src) => src,
10225 Err(e) => {
10226 crate::test_support::report_skip(&format!(
10227 "{} - zero-copy VYUY source not allocatable on this platform ({e})",
10228 function!()
10229 ));
10230 return;
10231 }
10232 };
10233
10234 let dst = TensorDyn::image(
10235 1280,
10236 720,
10237 PixelFormat::Rgba,
10238 DType::U8,
10239 Some(TensorMemory::DmaBuf),
10240 edgefirst_tensor::CpuAccess::ReadWrite,
10241 )
10242 .unwrap();
10243 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
10244
10245 let (result, _src, dst) = convert_img(
10246 &mut gl_converter,
10247 src,
10248 dst,
10249 Rotation::None,
10250 Flip::None,
10251 Crop::no_crop(),
10252 );
10253 match result {
10254 Err(Error::NotSupported(_)) => {
10255 crate::test_support::report_skip(&format!(
10256 "{} - OpenGL does not support PixelFormat::Vyuy DMA format",
10257 function!()
10258 ));
10259 return;
10260 }
10261 r => r.unwrap(),
10262 }
10263
10264 let target_image = TensorDyn::image(
10265 1280,
10266 720,
10267 PixelFormat::Rgba,
10268 DType::U8,
10269 None,
10270 edgefirst_tensor::CpuAccess::ReadWrite,
10271 )
10272 .unwrap();
10273 target_image
10274 .as_typed::<u8>()
10275 .unwrap()
10276 .map()
10277 .unwrap()
10278 .as_mut_slice()
10279 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
10280
10281 compare_images(&dst, &target_image, 0.98, function!());
10285 }
10286
10287 #[test]
10288 fn test_nv12_to_rgba_cpu() {
10289 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
10290 let src = TensorDyn::image(
10291 1280,
10292 720,
10293 PixelFormat::Nv12,
10294 DType::U8,
10295 None,
10296 edgefirst_tensor::CpuAccess::ReadWrite,
10297 )
10298 .unwrap();
10299 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
10300 .copy_from_slice(&file);
10301
10302 let dst = TensorDyn::image(
10303 1280,
10304 720,
10305 PixelFormat::Rgba,
10306 DType::U8,
10307 None,
10308 edgefirst_tensor::CpuAccess::ReadWrite,
10309 )
10310 .unwrap();
10311 let mut cpu_converter = CPUProcessor::new();
10312
10313 let (result, _src, dst) = convert_img(
10314 &mut cpu_converter,
10315 src,
10316 dst,
10317 Rotation::None,
10318 Flip::None,
10319 Crop::no_crop(),
10320 );
10321 result.unwrap();
10322
10323 let target_image = crate::load_image_test_helper(
10324 &edgefirst_bench::testdata::read("zidane.jpg"),
10325 Some(PixelFormat::Rgba),
10326 None,
10327 )
10328 .unwrap();
10329
10330 compare_images(&dst, &target_image, 0.95, function!());
10335 }
10336
10337 #[test]
10338 fn test_nv12_odd_height_to_rgb_cpu() {
10339 let mut src = TensorDyn::image(
10350 8,
10351 5,
10352 PixelFormat::Nv12,
10353 DType::U8,
10354 Some(TensorMemory::Mem),
10355 edgefirst_tensor::CpuAccess::ReadWrite,
10356 )
10357 .unwrap();
10358 assert_eq!(src.shape(), &[8, 8]);
10359 assert_eq!((src.width(), src.height()), (Some(8), Some(5)));
10360 src.as_typed::<u8>()
10361 .unwrap()
10362 .map()
10363 .unwrap()
10364 .as_mut_slice()
10365 .fill(128);
10366 src.set_colorimetry(Some(
10370 edgefirst_tensor::Colorimetry::default()
10371 .with_encoding(edgefirst_tensor::ColorEncoding::Bt601)
10372 .with_range(edgefirst_tensor::ColorRange::Full),
10373 ));
10374
10375 let dst = TensorDyn::image(
10376 8,
10377 5,
10378 PixelFormat::Rgb,
10379 DType::U8,
10380 Some(TensorMemory::Mem),
10381 edgefirst_tensor::CpuAccess::ReadWrite,
10382 )
10383 .unwrap();
10384 let mut cpu_converter = CPUProcessor::new();
10385 let (result, _src, dst) = convert_img(
10386 &mut cpu_converter,
10387 src,
10388 dst,
10389 Rotation::None,
10390 Flip::None,
10391 Crop::no_crop(),
10392 );
10393 result.unwrap();
10394
10395 assert_eq!((dst.width(), dst.height()), (Some(8), Some(5)));
10396 let map = dst.as_typed::<u8>().unwrap().map().unwrap();
10397 for (i, &b) in map.as_slice().iter().enumerate() {
10398 assert!(
10399 (b as i16 - 128).abs() <= 2,
10400 "pixel byte {i} = {b}, expected ~128 for neutral-grey NV12"
10401 );
10402 }
10403 }
10404
10405 #[test]
10406 fn test_nv24_to_rgb_cpu() {
10407 let mut src = TensorDyn::image(
10413 8,
10414 4,
10415 PixelFormat::Nv24,
10416 DType::U8,
10417 Some(TensorMemory::Mem),
10418 edgefirst_tensor::CpuAccess::ReadWrite,
10419 )
10420 .unwrap();
10421 assert_eq!(src.shape(), &[12, 8]);
10422 assert_eq!((src.width(), src.height()), (Some(8), Some(4)));
10423 src.as_typed::<u8>()
10424 .unwrap()
10425 .map()
10426 .unwrap()
10427 .as_mut_slice()
10428 .fill(128);
10429 src.set_colorimetry(Some(
10432 edgefirst_tensor::Colorimetry::default()
10433 .with_encoding(edgefirst_tensor::ColorEncoding::Bt601)
10434 .with_range(edgefirst_tensor::ColorRange::Full),
10435 ));
10436
10437 let dst = TensorDyn::image(
10438 8,
10439 4,
10440 PixelFormat::Rgb,
10441 DType::U8,
10442 Some(TensorMemory::Mem),
10443 edgefirst_tensor::CpuAccess::ReadWrite,
10444 )
10445 .unwrap();
10446 let mut cpu_converter = CPUProcessor::new();
10447 let (result, _src, dst) = convert_img(
10448 &mut cpu_converter,
10449 src,
10450 dst,
10451 Rotation::None,
10452 Flip::None,
10453 Crop::no_crop(),
10454 );
10455 result.unwrap();
10456
10457 assert_eq!((dst.width(), dst.height()), (Some(8), Some(4)));
10458 let map = dst.as_typed::<u8>().unwrap().map().unwrap();
10459 for (i, &b) in map.as_slice().iter().enumerate() {
10460 assert!(
10461 (b as i16 - 128).abs() <= 2,
10462 "pixel byte {i} = {b}, expected ~128 for neutral-grey NV24"
10463 );
10464 }
10465 }
10466
10467 #[test]
10468 fn cpu_nv12_to_rgb_respects_tagged_bt2020() {
10469 fn decode_tagged(enc: edgefirst_tensor::ColorEncoding) -> [u8; 3] {
10477 let mut src = TensorDyn::image(
10478 8,
10479 4,
10480 PixelFormat::Nv12,
10481 DType::U8,
10482 Some(TensorMemory::Mem),
10483 edgefirst_tensor::CpuAccess::ReadWrite,
10484 )
10485 .unwrap();
10486 assert_eq!(src.shape(), &[6, 8]);
10488 {
10489 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
10490 let buf = map.as_mut_slice();
10491 buf[..32].fill(120); for px in buf[32..].as_chunks_mut::<2>().0 {
10493 px[0] = 180; px[1] = 64; }
10496 }
10497 src.set_colorimetry(Some(
10500 edgefirst_tensor::Colorimetry::default()
10501 .with_encoding(enc)
10502 .with_range(edgefirst_tensor::ColorRange::Limited),
10503 ));
10504 let dst = TensorDyn::image(
10505 8,
10506 4,
10507 PixelFormat::Rgb,
10508 DType::U8,
10509 Some(TensorMemory::Mem),
10510 edgefirst_tensor::CpuAccess::ReadWrite,
10511 )
10512 .unwrap();
10513 let mut cpu = CPUProcessor::new();
10514 let (result, _src, dst) = convert_img(
10515 &mut cpu,
10516 src,
10517 dst,
10518 Rotation::None,
10519 Flip::None,
10520 Crop::no_crop(),
10521 );
10522 result.unwrap();
10523 let map = dst.as_typed::<u8>().unwrap().map().unwrap();
10524 let s = map.as_slice();
10525 [s[0], s[1], s[2]]
10526 }
10527
10528 let bt601 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt601);
10529 let bt709 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt709);
10530 let bt2020 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt2020);
10531
10532 assert_ne!(
10533 bt2020, bt601,
10534 "BT.2020 must decode differently from BT.601 ({bt2020:?} vs {bt601:?})"
10535 );
10536 assert_ne!(
10537 bt2020, bt709,
10538 "BT.2020 must decode differently from BT.709 ({bt2020:?} vs {bt709:?})"
10539 );
10540 assert_ne!(
10541 bt601, bt709,
10542 "BT.601 must decode differently from BT.709 ({bt601:?} vs {bt709:?})"
10543 );
10544 }
10545
10546 #[test]
10547 fn test_nv12_to_rgb_cpu() {
10548 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
10549 let src = TensorDyn::image(
10550 1280,
10551 720,
10552 PixelFormat::Nv12,
10553 DType::U8,
10554 None,
10555 edgefirst_tensor::CpuAccess::ReadWrite,
10556 )
10557 .unwrap();
10558 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
10559 .copy_from_slice(&file);
10560
10561 let dst = TensorDyn::image(
10562 1280,
10563 720,
10564 PixelFormat::Rgb,
10565 DType::U8,
10566 None,
10567 edgefirst_tensor::CpuAccess::ReadWrite,
10568 )
10569 .unwrap();
10570 let mut cpu_converter = CPUProcessor::new();
10571
10572 let (result, _src, dst) = convert_img(
10573 &mut cpu_converter,
10574 src,
10575 dst,
10576 Rotation::None,
10577 Flip::None,
10578 Crop::no_crop(),
10579 );
10580 result.unwrap();
10581
10582 let target_image = crate::load_image_test_helper(
10583 &edgefirst_bench::testdata::read("zidane.jpg"),
10584 Some(PixelFormat::Rgb),
10585 None,
10586 )
10587 .unwrap();
10588
10589 compare_images(&dst, &target_image, 0.95, function!());
10594 }
10595
10596 #[test]
10597 fn test_nv12_to_grey_cpu() {
10598 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
10599 let src = TensorDyn::image(
10600 1280,
10601 720,
10602 PixelFormat::Nv12,
10603 DType::U8,
10604 None,
10605 edgefirst_tensor::CpuAccess::ReadWrite,
10606 )
10607 .unwrap();
10608 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
10609 .copy_from_slice(&file);
10610
10611 let dst = TensorDyn::image(
10612 1280,
10613 720,
10614 PixelFormat::Grey,
10615 DType::U8,
10616 None,
10617 edgefirst_tensor::CpuAccess::ReadWrite,
10618 )
10619 .unwrap();
10620 let mut cpu_converter = CPUProcessor::new();
10621
10622 let (result, _src, dst) = convert_img(
10623 &mut cpu_converter,
10624 src,
10625 dst,
10626 Rotation::None,
10627 Flip::None,
10628 Crop::no_crop(),
10629 );
10630 result.unwrap();
10631
10632 let target_image = crate::load_image_test_helper(
10633 &edgefirst_bench::testdata::read("zidane.jpg"),
10634 Some(PixelFormat::Grey),
10635 None,
10636 )
10637 .unwrap();
10638
10639 compare_images(&dst, &target_image, 0.95, function!());
10644 }
10645
10646 #[test]
10647 fn test_nv12_to_yuyv_cpu() {
10648 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
10649 let src = TensorDyn::image(
10650 1280,
10651 720,
10652 PixelFormat::Nv12,
10653 DType::U8,
10654 None,
10655 edgefirst_tensor::CpuAccess::ReadWrite,
10656 )
10657 .unwrap();
10658 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
10659 .copy_from_slice(&file);
10660
10661 let dst = TensorDyn::image(
10662 1280,
10663 720,
10664 PixelFormat::Yuyv,
10665 DType::U8,
10666 None,
10667 edgefirst_tensor::CpuAccess::ReadWrite,
10668 )
10669 .unwrap();
10670 let mut cpu_converter = CPUProcessor::new();
10671
10672 let (result, _src, dst) = convert_img(
10673 &mut cpu_converter,
10674 src,
10675 dst,
10676 Rotation::None,
10677 Flip::None,
10678 Crop::no_crop(),
10679 );
10680 result.unwrap();
10681
10682 let target_image = crate::load_image_test_helper(
10683 &edgefirst_bench::testdata::read("zidane.jpg"),
10684 Some(PixelFormat::Rgb),
10685 None,
10686 )
10687 .unwrap();
10688
10689 compare_images_convert_to_rgb(&dst, &target_image, 0.95, function!());
10694 }
10695
10696 #[test]
10697 fn test_cpu_resize_nv16() {
10698 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
10699 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
10700
10701 let cpu_nv16_dst = TensorDyn::image(
10702 640,
10703 640,
10704 PixelFormat::Nv16,
10705 DType::U8,
10706 None,
10707 edgefirst_tensor::CpuAccess::ReadWrite,
10708 )
10709 .unwrap();
10710 let cpu_rgb_dst = TensorDyn::image(
10711 640,
10712 640,
10713 PixelFormat::Rgb,
10714 DType::U8,
10715 None,
10716 edgefirst_tensor::CpuAccess::ReadWrite,
10717 )
10718 .unwrap();
10719 let mut cpu_converter = CPUProcessor::new();
10720 let crop = Crop::letterbox([255, 128, 0, 255]);
10721
10722 let (result, src, cpu_nv16_dst) = convert_img(
10723 &mut cpu_converter,
10724 src,
10725 cpu_nv16_dst,
10726 Rotation::None,
10727 Flip::None,
10728 crop,
10729 );
10730 result.unwrap();
10731
10732 let (result, _src, cpu_rgb_dst) = convert_img(
10733 &mut cpu_converter,
10734 src,
10735 cpu_rgb_dst,
10736 Rotation::None,
10737 Flip::None,
10738 crop,
10739 );
10740 result.unwrap();
10741 compare_images_convert_to_rgb(
10742 &cpu_nv16_dst,
10743 &cpu_rgb_dst,
10744 yuv_threshold(0.99),
10745 function!(),
10746 );
10747 }
10748
10749 fn yuv_threshold(balanced: f64) -> f64 {
10766 #[cfg(target_arch = "aarch64")]
10767 {
10768 if !std::arch::is_aarch64_feature_detected!("rdm") {
10769 return balanced - 0.01;
10770 }
10771 }
10772 balanced
10773 }
10774
10775 fn load_bytes_to_tensor(
10776 width: usize,
10777 height: usize,
10778 format: PixelFormat,
10779 memory: Option<TensorMemory>,
10780 bytes: &[u8],
10781 ) -> Result<TensorDyn, Error> {
10782 let src = TensorDyn::image(
10783 width,
10784 height,
10785 format,
10786 DType::U8,
10787 memory,
10788 edgefirst_tensor::CpuAccess::ReadWrite,
10789 )?;
10790 src.as_typed::<u8>()
10791 .unwrap()
10792 .map()?
10793 .as_mut_slice()
10794 .copy_from_slice(bytes);
10795 Ok(src)
10796 }
10797
10798 fn compare_images(img1: &TensorDyn, img2: &TensorDyn, threshold: f64, name: &str) {
10806 assert_eq!(img1.height(), img2.height(), "Heights differ");
10807 assert_eq!(img1.width(), img2.width(), "Widths differ");
10808 assert_eq!(
10809 img1.format().unwrap(),
10810 img2.format().unwrap(),
10811 "PixelFormat differ"
10812 );
10813 assert!(
10814 matches!(
10815 img1.format().unwrap(),
10816 PixelFormat::Rgb | PixelFormat::Rgba | PixelFormat::Grey | PixelFormat::PlanarRgb
10817 ),
10818 "format must be Rgb or Rgba for comparison"
10819 );
10820
10821 fn flat(img: &TensorDyn) -> Vec<u8> {
10828 let t = img.as_typed::<u8>().expect("u8 image");
10829 let mut out = vec![0u8; t.shape().iter().product::<usize>()];
10830 t.copy_to_flat(&mut out).expect("compact the padded rows");
10831 out
10832 }
10833
10834 let image1 = match img1.format().unwrap() {
10835 PixelFormat::Rgb => image::RgbImage::from_vec(
10836 img1.width().unwrap() as u32,
10837 img1.height().unwrap() as u32,
10838 flat(img1),
10839 )
10840 .unwrap(),
10841 PixelFormat::Rgba => image::RgbaImage::from_vec(
10842 img1.width().unwrap() as u32,
10843 img1.height().unwrap() as u32,
10844 flat(img1),
10845 )
10846 .unwrap()
10847 .convert(),
10848 PixelFormat::Grey => image::GrayImage::from_vec(
10849 img1.width().unwrap() as u32,
10850 img1.height().unwrap() as u32,
10851 flat(img1),
10852 )
10853 .unwrap()
10854 .convert(),
10855 PixelFormat::PlanarRgb => image::GrayImage::from_vec(
10856 img1.width().unwrap() as u32,
10857 (img1.height().unwrap() * 3) as u32,
10858 flat(img1),
10859 )
10860 .unwrap()
10861 .convert(),
10862 _ => return,
10863 };
10864
10865 let image2 = match img2.format().unwrap() {
10866 PixelFormat::Rgb => image::RgbImage::from_vec(
10867 img2.width().unwrap() as u32,
10868 img2.height().unwrap() as u32,
10869 flat(img2),
10870 )
10871 .unwrap(),
10872 PixelFormat::Rgba => image::RgbaImage::from_vec(
10873 img2.width().unwrap() as u32,
10874 img2.height().unwrap() as u32,
10875 flat(img2),
10876 )
10877 .unwrap()
10878 .convert(),
10879 PixelFormat::Grey => image::GrayImage::from_vec(
10880 img2.width().unwrap() as u32,
10881 img2.height().unwrap() as u32,
10882 flat(img2),
10883 )
10884 .unwrap()
10885 .convert(),
10886 PixelFormat::PlanarRgb => image::GrayImage::from_vec(
10887 img2.width().unwrap() as u32,
10888 (img2.height().unwrap() * 3) as u32,
10889 flat(img2),
10890 )
10891 .unwrap()
10892 .convert(),
10893 _ => return,
10894 };
10895
10896 let similarity = image_compare::rgb_similarity_structure(
10897 &image_compare::Algorithm::RootMeanSquared,
10898 &image1,
10899 &image2,
10900 )
10901 .expect("Image Comparison failed");
10902 if similarity.score < threshold {
10903 similarity
10906 .image
10907 .to_color_map()
10908 .save(format!("{name}.png"))
10909 .unwrap();
10910 panic!(
10911 "{name}: converted image and target image have similarity score too low: {} < {}",
10912 similarity.score, threshold
10913 )
10914 }
10915 }
10916
10917 fn compare_images_convert_to_rgb(
10918 img1: &TensorDyn,
10919 img2: &TensorDyn,
10920 threshold: f64,
10921 name: &str,
10922 ) {
10923 assert_eq!(img1.height(), img2.height(), "Heights differ");
10924 assert_eq!(img1.width(), img2.width(), "Widths differ");
10925
10926 let mut img_rgb1 = TensorDyn::image(
10927 img1.width().unwrap(),
10928 img1.height().unwrap(),
10929 PixelFormat::Rgb,
10930 DType::U8,
10931 Some(TensorMemory::Mem),
10932 edgefirst_tensor::CpuAccess::ReadWrite,
10933 )
10934 .unwrap();
10935 let mut img_rgb2 = TensorDyn::image(
10936 img1.width().unwrap(),
10937 img1.height().unwrap(),
10938 PixelFormat::Rgb,
10939 DType::U8,
10940 Some(TensorMemory::Mem),
10941 edgefirst_tensor::CpuAccess::ReadWrite,
10942 )
10943 .unwrap();
10944 let mut __cv = CPUProcessor::default();
10945 let r1 = __cv.convert(
10946 img1,
10947 &mut img_rgb1,
10948 crate::Rotation::None,
10949 crate::Flip::None,
10950 crate::Crop::default(),
10951 );
10952 let r2 = __cv.convert(
10953 img2,
10954 &mut img_rgb2,
10955 crate::Rotation::None,
10956 crate::Flip::None,
10957 crate::Crop::default(),
10958 );
10959 if r1.is_err() || r2.is_err() {
10960 let w = img1.width().unwrap() as u32;
10962 let data1 = img1.as_typed::<u8>().unwrap().map().unwrap().to_vec();
10963 let data2 = img2.as_typed::<u8>().unwrap().map().unwrap().to_vec();
10964 let h1 = (data1.len() as u32) / w;
10965 let h2 = (data2.len() as u32) / w;
10966 let g1 = image::GrayImage::from_vec(w, h1, data1).unwrap();
10967 let g2 = image::GrayImage::from_vec(w, h2, data2).unwrap();
10968 let similarity = image_compare::gray_similarity_structure(
10969 &image_compare::Algorithm::RootMeanSquared,
10970 &g1,
10971 &g2,
10972 )
10973 .expect("Image Comparison failed");
10974 if similarity.score < threshold {
10975 panic!(
10976 "{name}: converted image and target image have similarity score too low: {} < {}",
10977 similarity.score, threshold
10978 )
10979 }
10980 return;
10981 }
10982
10983 let image1 = image::RgbImage::from_vec(
10984 img_rgb1.width().unwrap() as u32,
10985 img_rgb1.height().unwrap() as u32,
10986 img_rgb1.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
10987 )
10988 .unwrap();
10989
10990 let image2 = image::RgbImage::from_vec(
10991 img_rgb2.width().unwrap() as u32,
10992 img_rgb2.height().unwrap() as u32,
10993 img_rgb2.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
10994 )
10995 .unwrap();
10996
10997 let similarity = image_compare::rgb_similarity_structure(
10998 &image_compare::Algorithm::RootMeanSquared,
10999 &image1,
11000 &image2,
11001 )
11002 .expect("Image Comparison failed");
11003 if similarity.score < threshold {
11004 similarity
11007 .image
11008 .to_color_map()
11009 .save(format!("{name}.png"))
11010 .unwrap();
11011 panic!(
11012 "{name}: converted image and target image have similarity score too low: {} < {}",
11013 similarity.score, threshold
11014 )
11015 }
11016 }
11017
11018 #[test]
11023 fn test_nv12_image_creation() {
11024 let width = 640;
11025 let height = 480;
11026 let img = TensorDyn::image(
11027 width,
11028 height,
11029 PixelFormat::Nv12,
11030 DType::U8,
11031 None,
11032 edgefirst_tensor::CpuAccess::ReadWrite,
11033 )
11034 .unwrap();
11035
11036 assert_eq!(img.width(), Some(width));
11037 assert_eq!(img.height(), Some(height));
11038 assert_eq!(img.format().unwrap(), PixelFormat::Nv12);
11039 assert_eq!(
11041 img.as_typed::<u8>().unwrap().shape(),
11042 &[height * 3 / 2, width]
11043 );
11044 }
11045
11046 #[test]
11047 fn test_nv12_channels() {
11048 let img = TensorDyn::image(
11049 640,
11050 480,
11051 PixelFormat::Nv12,
11052 DType::U8,
11053 None,
11054 edgefirst_tensor::CpuAccess::ReadWrite,
11055 )
11056 .unwrap();
11057 assert_eq!(img.format().unwrap().channels(), 1);
11059 }
11060
11061 #[test]
11066 fn test_tensor_set_format_planar() {
11067 let mut tensor = Tensor::<u8>::new(&[3, 480, 640], None, None).unwrap();
11068 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
11069 assert_eq!(tensor.format(), Some(PixelFormat::PlanarRgb));
11070 assert_eq!(tensor.width(), Some(640));
11071 assert_eq!(tensor.height(), Some(480));
11072 }
11073
11074 #[test]
11075 fn test_tensor_set_format_interleaved() {
11076 let mut tensor = Tensor::<u8>::new(&[480, 640, 4], None, None).unwrap();
11077 tensor.set_format(PixelFormat::Rgba).unwrap();
11078 assert_eq!(tensor.format(), Some(PixelFormat::Rgba));
11079 assert_eq!(tensor.width(), Some(640));
11080 assert_eq!(tensor.height(), Some(480));
11081 }
11082
11083 #[test]
11084 fn test_tensordyn_image_rgb() {
11085 let img = TensorDyn::image(
11086 640,
11087 480,
11088 PixelFormat::Rgb,
11089 DType::U8,
11090 None,
11091 edgefirst_tensor::CpuAccess::ReadWrite,
11092 )
11093 .unwrap();
11094 assert_eq!(img.width(), Some(640));
11095 assert_eq!(img.height(), Some(480));
11096 assert_eq!(img.format(), Some(PixelFormat::Rgb));
11097 }
11098
11099 #[test]
11100 fn test_tensordyn_image_planar_rgb() {
11101 let img = TensorDyn::image(
11102 640,
11103 480,
11104 PixelFormat::PlanarRgb,
11105 DType::U8,
11106 None,
11107 edgefirst_tensor::CpuAccess::ReadWrite,
11108 )
11109 .unwrap();
11110 assert_eq!(img.width(), Some(640));
11111 assert_eq!(img.height(), Some(480));
11112 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
11113 }
11114
11115 #[test]
11116 fn test_rgb_int8_format() {
11117 let img = TensorDyn::image(
11119 1280,
11120 720,
11121 PixelFormat::Rgb,
11122 DType::I8,
11123 Some(TensorMemory::Mem),
11124 edgefirst_tensor::CpuAccess::ReadWrite,
11125 )
11126 .unwrap();
11127 assert_eq!(img.width(), Some(1280));
11128 assert_eq!(img.height(), Some(720));
11129 assert_eq!(img.format(), Some(PixelFormat::Rgb));
11130 assert_eq!(img.dtype(), DType::I8);
11131 }
11132
11133 #[test]
11134 fn test_planar_rgb_int8_format() {
11135 let img = TensorDyn::image(
11136 1280,
11137 720,
11138 PixelFormat::PlanarRgb,
11139 DType::I8,
11140 Some(TensorMemory::Mem),
11141 edgefirst_tensor::CpuAccess::ReadWrite,
11142 )
11143 .unwrap();
11144 assert_eq!(img.width(), Some(1280));
11145 assert_eq!(img.height(), Some(720));
11146 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
11147 assert_eq!(img.dtype(), DType::I8);
11148 }
11149
11150 #[test]
11151 fn test_rgb_from_tensor() {
11152 let mut tensor = Tensor::<u8>::new(&[720, 1280, 3], None, None).unwrap();
11153 tensor.set_format(PixelFormat::Rgb).unwrap();
11154 let img = TensorDyn::from(tensor);
11155 assert_eq!(img.width(), Some(1280));
11156 assert_eq!(img.height(), Some(720));
11157 assert_eq!(img.format(), Some(PixelFormat::Rgb));
11158 }
11159
11160 #[test]
11161 fn test_planar_rgb_from_tensor() {
11162 let mut tensor = Tensor::<u8>::new(&[3, 720, 1280], None, None).unwrap();
11163 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
11164 let img = TensorDyn::from(tensor);
11165 assert_eq!(img.width(), Some(1280));
11166 assert_eq!(img.height(), Some(720));
11167 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
11168 }
11169
11170 #[test]
11171 fn test_dtype_determines_int8() {
11172 let u8_img = TensorDyn::image(
11174 64,
11175 64,
11176 PixelFormat::Rgb,
11177 DType::U8,
11178 None,
11179 edgefirst_tensor::CpuAccess::ReadWrite,
11180 )
11181 .unwrap();
11182 let i8_img = TensorDyn::image(
11183 64,
11184 64,
11185 PixelFormat::Rgb,
11186 DType::I8,
11187 None,
11188 edgefirst_tensor::CpuAccess::ReadWrite,
11189 )
11190 .unwrap();
11191 assert_eq!(u8_img.dtype(), DType::U8);
11192 assert_eq!(i8_img.dtype(), DType::I8);
11193 }
11194
11195 #[test]
11196 fn test_pixel_layout_packed_vs_planar() {
11197 assert_eq!(PixelFormat::Rgb.layout(), PixelLayout::Packed);
11199 assert_eq!(PixelFormat::Rgba.layout(), PixelLayout::Packed);
11200 assert_eq!(PixelFormat::PlanarRgb.layout(), PixelLayout::Planar);
11201 assert_eq!(PixelFormat::Nv12.layout(), PixelLayout::SemiPlanar);
11202 }
11203
11204 #[cfg(target_os = "linux")]
11215 #[cfg(feature = "opengl")]
11216 #[test]
11217 fn pbo_reports_why_it_cannot_pin() {
11218 use edgefirst_tensor::{Error, TensorTrait};
11219
11220 let converter = ImageProcessor::new().unwrap();
11221 let is_pbo = converter
11222 .opengl
11223 .as_ref()
11224 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
11225 if !is_pbo {
11226 use std::io::Write;
11231 let _ = writeln!(
11232 &mut std::io::stderr(),
11233 "SKIPPED: pbo_reports_why_it_cannot_pin - transfer backend is not PBO"
11234 );
11235 return;
11236 }
11237
11238 let t = converter
11239 .create_image(
11240 64,
11241 64,
11242 PixelFormat::Rgba,
11243 DType::U8,
11244 None,
11245 edgefirst_tensor::CpuAccess::ReadWrite,
11246 )
11247 .unwrap();
11248 assert_eq!(t.as_typed::<u8>().unwrap().memory(), TensorMemory::Pbo);
11249 let t = t.as_typed::<u8>().unwrap();
11250
11251 match t.pin_host(edgefirst_tensor::CpuAccess::ReadWrite) {
11252 Err(Error::NotImplemented(m)) => {
11253 assert!(
11254 m.contains("glMapBufferRange"),
11255 "must name the constraint: {m}"
11256 );
11257 assert!(m.contains("map()"), "must point at the supported API: {m}");
11258 }
11259 other => panic!("expected an explained NotImplemented, got {other:?}"),
11260 }
11261 assert!(matches!(
11263 t.sync_for_cpu(edgefirst_tensor::CpuAccess::Read),
11264 Err(Error::NotImplemented(_))
11265 ));
11266
11267 let g = t.map().expect("map must work where pin cannot");
11269 assert!(!g.as_slice().is_empty());
11270 }
11271
11272 #[cfg(target_os = "linux")]
11276 #[cfg(feature = "opengl")]
11277 #[test]
11278 fn test_convert_pbo_to_pbo() {
11279 let mut converter = ImageProcessor::new().unwrap();
11280
11281 let is_pbo = converter
11283 .opengl
11284 .as_ref()
11285 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
11286 if !is_pbo {
11287 log::warn!("Skipping test_convert_pbo_to_pbo: backend is not PBO");
11288 return;
11289 }
11290
11291 let src_w = 640;
11292 let src_h = 480;
11293 let dst_w = 320;
11294 let dst_h = 240;
11295
11296 let pbo_src = converter
11298 .create_image(
11299 src_w,
11300 src_h,
11301 PixelFormat::Rgba,
11302 DType::U8,
11303 None,
11304 edgefirst_tensor::CpuAccess::ReadWrite,
11305 )
11306 .unwrap();
11307 assert_eq!(
11308 pbo_src.as_typed::<u8>().unwrap().memory(),
11309 TensorMemory::Pbo,
11310 "create_image should produce a PBO tensor"
11311 );
11312
11313 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
11315 let jpeg_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
11316
11317 let mem_src = TensorDyn::image(
11319 src_w,
11320 src_h,
11321 PixelFormat::Rgba,
11322 DType::U8,
11323 Some(TensorMemory::Mem),
11324 edgefirst_tensor::CpuAccess::ReadWrite,
11325 )
11326 .unwrap();
11327 let (result, _jpeg_src, mem_src) = convert_img(
11328 &mut CPUProcessor::new(),
11329 jpeg_src,
11330 mem_src,
11331 Rotation::None,
11332 Flip::None,
11333 Crop::no_crop(),
11334 );
11335 result.unwrap();
11336
11337 {
11339 let src_data = mem_src.as_typed::<u8>().unwrap().map().unwrap();
11340 let mut pbo_map = pbo_src.as_typed::<u8>().unwrap().map().unwrap();
11341 pbo_map.copy_from_slice(&src_data);
11342 }
11343
11344 let pbo_dst = converter
11346 .create_image(
11347 dst_w,
11348 dst_h,
11349 PixelFormat::Rgba,
11350 DType::U8,
11351 None,
11352 edgefirst_tensor::CpuAccess::ReadWrite,
11353 )
11354 .unwrap();
11355 assert_eq!(
11356 pbo_dst.as_typed::<u8>().unwrap().memory(),
11357 TensorMemory::Pbo
11358 );
11359
11360 let mut pbo_dst = pbo_dst;
11362 let result = converter.convert(
11363 &pbo_src,
11364 &mut pbo_dst,
11365 Rotation::None,
11366 Flip::None,
11367 Crop::no_crop(),
11368 );
11369 result.unwrap();
11370
11371 let cpu_dst = TensorDyn::image(
11373 dst_w,
11374 dst_h,
11375 PixelFormat::Rgba,
11376 DType::U8,
11377 Some(TensorMemory::Mem),
11378 edgefirst_tensor::CpuAccess::ReadWrite,
11379 )
11380 .unwrap();
11381 let (result, _mem_src, cpu_dst) = convert_img(
11382 &mut CPUProcessor::new(),
11383 mem_src,
11384 cpu_dst,
11385 Rotation::None,
11386 Flip::None,
11387 Crop::no_crop(),
11388 );
11389 result.unwrap();
11390
11391 let pbo_dst_img = {
11392 let mut __t = pbo_dst.into_u8().unwrap();
11393 __t.set_format(PixelFormat::Rgba).unwrap();
11394 TensorDyn::from(__t)
11395 };
11396 compare_images(&pbo_dst_img, &cpu_dst, 0.95, function!());
11397 log::info!("test_convert_pbo_to_pbo: PASS — PBO-to-PBO convert matches CPU reference");
11398 }
11399
11400 #[test]
11401 fn test_image_bgra() {
11402 let img = TensorDyn::image(
11403 640,
11404 480,
11405 PixelFormat::Bgra,
11406 DType::U8,
11407 Some(edgefirst_tensor::TensorMemory::Mem),
11408 edgefirst_tensor::CpuAccess::ReadWrite,
11409 )
11410 .unwrap();
11411 assert_eq!(img.width(), Some(640));
11412 assert_eq!(img.height(), Some(480));
11413 assert_eq!(img.format().unwrap().channels(), 4);
11414 assert_eq!(img.format().unwrap(), PixelFormat::Bgra);
11415 }
11416
11417 #[test]
11422 fn test_force_backend_cpu() {
11423 let _lock = acquire_env_lock();
11424 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11425 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
11426 let converter = ImageProcessor::new().unwrap();
11427 assert!(converter.cpu.is_some());
11428 assert_eq!(converter.forced_backend, Some(ForcedBackend::Cpu));
11429 }
11430
11431 #[test]
11432 fn test_force_backend_invalid() {
11433 let _lock = acquire_env_lock();
11434 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11435 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "invalid") };
11436 let result = ImageProcessor::new();
11437 assert!(
11438 matches!(&result, Err(Error::ForcedBackendUnavailable(s)) if s.contains("unknown")),
11439 "invalid backend value should return ForcedBackendUnavailable error: {result:?}"
11440 );
11441 }
11442
11443 #[test]
11444 fn test_force_backend_unset() {
11445 let _lock = acquire_env_lock();
11446 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11447 unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") };
11448 let converter = ImageProcessor::new().unwrap();
11449 assert!(converter.forced_backend.is_none());
11450 }
11451
11452 #[test]
11457 fn test_draw_proto_masks_no_cpu_returns_error() {
11458 let _lock = acquire_env_lock();
11460 let _guard = EnvGuard::snapshot(&[
11461 "EDGEFIRST_FORCE_BACKEND",
11462 "EDGEFIRST_DISABLE_GL",
11463 "EDGEFIRST_DISABLE_G2D",
11464 "EDGEFIRST_DISABLE_CPU",
11465 ]);
11466
11467 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
11469 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
11470 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
11471
11472 let mut converter = ImageProcessor::new().unwrap();
11473 assert!(converter.cpu.is_none(), "CPU should be disabled");
11474
11475 let dst = TensorDyn::image(
11476 640,
11477 480,
11478 PixelFormat::Rgba,
11479 DType::U8,
11480 Some(TensorMemory::Mem),
11481 edgefirst_tensor::CpuAccess::ReadWrite,
11482 )
11483 .unwrap();
11484 let mut dst_dyn = dst;
11485 let det = [DetectBox {
11486 bbox: edgefirst_tensor::BoundingBox {
11487 xmin: 0.1,
11488 ymin: 0.1,
11489 xmax: 0.5,
11490 ymax: 0.5,
11491 },
11492 score: 0.9,
11493 label: 0,
11494 }];
11495 let proto_data = {
11496 use edgefirst_tensor::{Tensor, TensorDyn};
11497 let coeff_t = Tensor::<f32>::from_slice(&[0.5_f32; 4], &[1, 4]).unwrap();
11498 let protos_t =
11499 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
11500 ProtoData {
11501 mask_coefficients: TensorDyn::F32(coeff_t),
11502 protos: TensorDyn::F32(protos_t),
11503 layout: ProtoLayout::Nhwc,
11504 }
11505 };
11506 let result =
11507 converter.draw_proto_masks(&mut dst_dyn, &det, &proto_data, Default::default());
11508 assert!(
11509 matches!(&result, Err(Error::Internal(s)) if s.contains("CPU backend")),
11510 "draw_proto_masks without CPU should return Internal error: {result:?}"
11511 );
11512 }
11513
11514 #[test]
11515 fn test_draw_proto_masks_cpu_fallback_works() {
11516 let _lock = acquire_env_lock();
11519 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11520 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
11521 let mut converter = ImageProcessor::new().unwrap();
11522 assert!(converter.cpu.is_some());
11523
11524 let dst = TensorDyn::image(
11525 64,
11526 64,
11527 PixelFormat::Rgba,
11528 DType::U8,
11529 Some(TensorMemory::Mem),
11530 edgefirst_tensor::CpuAccess::ReadWrite,
11531 )
11532 .unwrap();
11533 let mut dst_dyn = dst;
11534 let det = [DetectBox {
11535 bbox: edgefirst_tensor::BoundingBox {
11536 xmin: 0.1,
11537 ymin: 0.1,
11538 xmax: 0.5,
11539 ymax: 0.5,
11540 },
11541 score: 0.9,
11542 label: 0,
11543 }];
11544 let proto_data = {
11545 use edgefirst_tensor::{Tensor, TensorDyn};
11546 let coeff_t = Tensor::<f32>::from_slice(&[0.5_f32; 4], &[1, 4]).unwrap();
11547 let protos_t =
11548 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
11549 ProtoData {
11550 mask_coefficients: TensorDyn::F32(coeff_t),
11551 protos: TensorDyn::F32(protos_t),
11552 layout: ProtoLayout::Nhwc,
11553 }
11554 };
11555 let result =
11556 converter.draw_proto_masks(&mut dst_dyn, &det, &proto_data, Default::default());
11557 assert!(result.is_ok(), "CPU fallback path should work: {result:?}");
11558 }
11559
11560 fn acquire_env_lock() -> std::sync::MutexGuard<'static, ()> {
11592 use std::sync::{Mutex, OnceLock};
11593 static ENV_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
11594 ENV_MUTEX
11595 .get_or_init(|| Mutex::new(()))
11596 .lock()
11597 .unwrap_or_else(|e| e.into_inner())
11598 }
11599
11600 struct EnvGuard {
11603 vars: Vec<(&'static str, Option<String>)>,
11604 }
11605
11606 impl EnvGuard {
11607 fn snapshot(names: &[&'static str]) -> Self {
11611 Self {
11612 vars: names.iter().map(|&k| (k, std::env::var(k).ok())).collect(),
11613 }
11614 }
11615 }
11616
11617 impl Drop for EnvGuard {
11618 fn drop(&mut self) {
11619 for (k, v) in &self.vars {
11620 match v {
11621 Some(s) => unsafe { std::env::set_var(k, s) },
11622 None => unsafe { std::env::remove_var(k) },
11623 }
11624 }
11625 }
11626 }
11627
11628 fn with_force_backend<R>(value: Option<&str>, body: impl FnOnce() -> R) -> R {
11632 let _lock = acquire_env_lock();
11633 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11634 match value {
11635 Some(v) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", v) },
11636 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
11637 }
11638 body()
11639 }
11640
11641 fn make_dirty_dst(w: usize, h: usize, mem: Option<TensorMemory>) -> TensorDyn {
11646 let dst = TensorDyn::image(
11647 w,
11648 h,
11649 PixelFormat::Rgba,
11650 DType::U8,
11651 mem,
11652 edgefirst_tensor::CpuAccess::ReadWrite,
11653 )
11654 .unwrap();
11655 {
11656 use edgefirst_tensor::TensorMapTrait;
11657 let u8t = dst.as_typed::<u8>().unwrap();
11658 let mut map = u8t.map().unwrap();
11659 for (i, b) in map.as_mut_slice().iter_mut().enumerate() {
11660 *b = 0xA0u8.wrapping_add((i as u8) & 0x3F);
11661 }
11662 }
11663 dst
11664 }
11665
11666 fn make_bg(w: usize, h: usize, mem: Option<TensorMemory>, rgba: [u8; 4]) -> TensorDyn {
11668 let bg = TensorDyn::image(
11669 w,
11670 h,
11671 PixelFormat::Rgba,
11672 DType::U8,
11673 mem,
11674 edgefirst_tensor::CpuAccess::ReadWrite,
11675 )
11676 .unwrap();
11677 {
11678 use edgefirst_tensor::TensorMapTrait;
11679 let u8t = bg.as_typed::<u8>().unwrap();
11680 let mut map = u8t.map().unwrap();
11681 for chunk in map.as_mut_slice().as_chunks_mut::<4>().0 {
11682 chunk.copy_from_slice(&rgba);
11683 }
11684 }
11685 bg
11686 }
11687
11688 fn pixel_at(dst: &TensorDyn, x: usize, y: usize) -> [u8; 4] {
11689 use edgefirst_tensor::TensorMapTrait;
11690 let w = dst.width().unwrap();
11691 let off = (y * w + x) * 4;
11692 let u8t = dst.as_typed::<u8>().unwrap();
11693 let map = u8t.map().unwrap();
11694 let s = map.as_slice();
11695 [s[off], s[off + 1], s[off + 2], s[off + 3]]
11696 }
11697
11698 fn assert_every_pixel_eq(dst: &TensorDyn, expected: [u8; 4], case: &str) {
11699 use edgefirst_tensor::TensorMapTrait;
11700 let u8t = dst.as_typed::<u8>().unwrap();
11701 let map = u8t.map().unwrap();
11702 for (i, chunk) in map.as_slice().as_chunks::<4>().0.iter().enumerate() {
11703 assert_eq!(
11704 chunk, &expected,
11705 "{case}: pixel idx {i} = {chunk:?}, expected {expected:?}"
11706 );
11707 }
11708 }
11709
11710 fn scenario_empty_no_bg(processor: &mut ImageProcessor, case: &str) {
11713 let mut dst = make_dirty_dst(64, 64, None);
11714 processor
11715 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::default())
11716 .unwrap_or_else(|e| panic!("{case}/decoded_masks empty+no-bg failed: {e:?}"));
11717 assert_every_pixel_eq(&dst, [0, 0, 0, 0], &format!("{case}/decoded"));
11718
11719 let mut dst = make_dirty_dst(64, 64, None);
11720 let proto = {
11721 use edgefirst_tensor::{Tensor, TensorDyn};
11722 let coeff_t = Tensor::<f32>::from_slice(&[0.0_f32; 4], &[1, 4]).unwrap();
11724 let protos_t =
11725 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
11726 ProtoData {
11727 mask_coefficients: TensorDyn::F32(coeff_t),
11728 protos: TensorDyn::F32(protos_t),
11729 layout: ProtoLayout::Nhwc,
11730 }
11731 };
11732 processor
11733 .draw_proto_masks(&mut dst, &[], &proto, MaskOverlay::default())
11734 .unwrap_or_else(|e| panic!("{case}/proto_masks empty+no-bg failed: {e:?}"));
11735 assert_every_pixel_eq(&dst, [0, 0, 0, 0], &format!("{case}/proto"));
11736 }
11737
11738 fn scenario_empty_with_bg(processor: &mut ImageProcessor, case: &str) {
11741 let bg_color = [42, 99, 200, 255];
11742 let bg = make_bg(64, 64, None, bg_color);
11743 let overlay = MaskOverlay::new().with_background(&bg);
11744
11745 let mut dst = make_dirty_dst(64, 64, None);
11746 processor
11747 .draw_decoded_masks(&mut dst, &[], &[], overlay)
11748 .unwrap_or_else(|e| panic!("{case}/decoded_masks empty+bg failed: {e:?}"));
11749 assert_every_pixel_eq(&dst, bg_color, &format!("{case}/decoded bg blit"));
11750
11751 let mut dst = make_dirty_dst(64, 64, None);
11752 let proto = {
11753 use edgefirst_tensor::{Tensor, TensorDyn};
11754 let coeff_t = Tensor::<f32>::from_slice(&[0.0_f32; 4], &[1, 4]).unwrap();
11756 let protos_t =
11757 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
11758 ProtoData {
11759 mask_coefficients: TensorDyn::F32(coeff_t),
11760 protos: TensorDyn::F32(protos_t),
11761 layout: ProtoLayout::Nhwc,
11762 }
11763 };
11764 processor
11765 .draw_proto_masks(&mut dst, &[], &proto, overlay)
11766 .unwrap_or_else(|e| panic!("{case}/proto_masks empty+bg failed: {e:?}"));
11767 assert_every_pixel_eq(&dst, bg_color, &format!("{case}/proto bg blit"));
11768 }
11769
11770 fn scenario_detect_no_bg(processor: &mut ImageProcessor, case: &str) {
11774 use edgefirst_tensor::Segmentation;
11775 use ndarray::Array3;
11776 processor
11777 .set_class_colors(&[[200, 80, 40, 255]])
11778 .expect("set_class_colors");
11779
11780 let detect = DetectBox {
11781 bbox: [0.25, 0.25, 0.75, 0.75].into(),
11782 score: 0.99,
11783 label: 0,
11784 };
11785 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
11786 let seg = Segmentation {
11787 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
11788 .unwrap()
11789 .into(),
11790 xmin: 0.25,
11791 ymin: 0.25,
11792 xmax: 0.75,
11793 ymax: 0.75,
11794 };
11795
11796 let mut dst = make_dirty_dst(64, 64, None);
11797 processor
11798 .draw_decoded_masks(&mut dst, &[detect], &[seg], MaskOverlay::default())
11799 .unwrap_or_else(|e| panic!("{case}/decoded_masks detect+no-bg failed: {e:?}"));
11800
11801 let corner = pixel_at(&dst, 2, 2);
11803 assert_eq!(
11804 corner,
11805 [0, 0, 0, 0],
11806 "{case}/decoded: corner (2,2) leaked dirty pattern: {corner:?}"
11807 );
11808 let center = pixel_at(&dst, 32, 32);
11812 assert!(
11813 center != [0, 0, 0, 0],
11814 "{case}/decoded: center (32,32) was not coloured: {center:?}"
11815 );
11816 }
11817
11818 fn scenario_detect_with_bg(processor: &mut ImageProcessor, case: &str) {
11821 use edgefirst_tensor::Segmentation;
11822 use ndarray::Array3;
11823 processor
11824 .set_class_colors(&[[200, 80, 40, 255]])
11825 .expect("set_class_colors");
11826 let bg_color = [10, 20, 30, 255];
11827 let bg = make_bg(64, 64, None, bg_color);
11828
11829 let detect = DetectBox {
11830 bbox: [0.25, 0.25, 0.75, 0.75].into(),
11831 score: 0.99,
11832 label: 0,
11833 };
11834 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
11835 let seg = Segmentation {
11836 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
11837 .unwrap()
11838 .into(),
11839 xmin: 0.25,
11840 ymin: 0.25,
11841 xmax: 0.75,
11842 ymax: 0.75,
11843 };
11844
11845 let overlay = MaskOverlay::new().with_background(&bg);
11846 let mut dst = make_dirty_dst(64, 64, None);
11847 processor
11848 .draw_decoded_masks(&mut dst, &[detect], &[seg], overlay)
11849 .unwrap_or_else(|e| panic!("{case}/decoded_masks detect+bg failed: {e:?}"));
11850
11851 let corner = pixel_at(&dst, 2, 2);
11853 assert_eq!(
11854 corner, bg_color,
11855 "{case}/decoded: corner (2,2) should show bg {bg_color:?} got {corner:?}"
11856 );
11857 let center = pixel_at(&dst, 32, 32);
11860 assert!(
11861 center != bg_color,
11862 "{case}/decoded: center (32,32) should differ from bg {bg_color:?}, got {center:?}"
11863 );
11864 }
11865
11866 fn run_all_scenarios(
11869 force_backend: Option<&'static str>,
11870 case: &'static str,
11871 require_dma_for_bg: bool,
11872 ) {
11873 if require_dma_for_bg && !edgefirst_tensor::is_dma_available() {
11874 crate::test_support::report_skip(&format!("{case} — DMA not available on this host"));
11875 return;
11876 }
11877 let processor_result = with_force_backend(force_backend, ImageProcessor::new);
11878 let mut processor = match processor_result {
11879 Ok(p) => p,
11880 Err(e) => {
11881 crate::test_support::report_skip(&format!("{case} — backend init failed: {e:?}"));
11882 return;
11883 }
11884 };
11885 scenario_empty_no_bg(&mut processor, case);
11886 scenario_empty_with_bg(&mut processor, case);
11887 scenario_detect_no_bg(&mut processor, case);
11888 scenario_detect_with_bg(&mut processor, case);
11889 }
11890
11891 #[test]
11892 fn test_draw_masks_4_scenarios_cpu() {
11893 run_all_scenarios(Some("cpu"), "cpu", false);
11894 }
11895
11896 #[test]
11897 fn test_draw_masks_4_scenarios_auto() {
11898 run_all_scenarios(None, "auto", false);
11899 }
11900
11901 #[cfg(any(
11902 target_os = "linux",
11903 target_os = "macos",
11904 target_os = "ios",
11905 target_os = "android",
11906 target_os = "windows"
11907 ))]
11908 #[cfg(feature = "opengl")]
11909 #[test]
11910 fn test_draw_masks_4_scenarios_opengl() {
11911 run_all_scenarios(Some("opengl"), "opengl", false);
11912 }
11913
11914 #[cfg(target_os = "linux")]
11919 #[test]
11920 fn test_draw_masks_zero_detection_g2d_forced() {
11921 if !edgefirst_tensor::is_dma_available() {
11922 crate::test_support::report_skip("g2d forced — DMA not available on this host");
11923 return;
11924 }
11925 let processor_result = with_force_backend(Some("g2d"), ImageProcessor::new);
11926 let mut processor = match processor_result {
11927 Ok(p) => p,
11928 Err(e) => {
11929 crate::test_support::report_skip(&format!("g2d forced — init failed: {e:?}"));
11930 return;
11931 }
11932 };
11933
11934 let mut dst = TensorDyn::image(
11936 64,
11937 64,
11938 PixelFormat::Rgba,
11939 DType::U8,
11940 Some(TensorMemory::DmaBuf),
11941 edgefirst_tensor::CpuAccess::ReadWrite,
11942 )
11943 .unwrap();
11944 {
11945 use edgefirst_tensor::TensorMapTrait;
11946 let u8t = dst.as_typed_mut::<u8>().unwrap();
11947 let mut map = u8t.map().unwrap();
11948 map.as_mut_slice().fill(0xBB);
11949 }
11950 processor
11951 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::default())
11952 .expect("g2d empty+no-bg");
11953 assert_every_pixel_eq(&dst, [0, 0, 0, 0], "g2d/case1 cleared");
11954
11955 let bg_color = [7, 11, 13, 255];
11957 let bg = {
11958 let t = TensorDyn::image(
11959 64,
11960 64,
11961 PixelFormat::Rgba,
11962 DType::U8,
11963 Some(TensorMemory::DmaBuf),
11964 edgefirst_tensor::CpuAccess::ReadWrite,
11965 )
11966 .unwrap();
11967 {
11968 use edgefirst_tensor::TensorMapTrait;
11969 let u8t = t.as_typed::<u8>().unwrap();
11970 let mut map = u8t.map().unwrap();
11971 for chunk in map.as_mut_slice().as_chunks_mut::<4>().0 {
11972 chunk.copy_from_slice(&bg_color);
11973 }
11974 }
11975 t
11976 };
11977 let mut dst = TensorDyn::image(
11978 64,
11979 64,
11980 PixelFormat::Rgba,
11981 DType::U8,
11982 Some(TensorMemory::DmaBuf),
11983 edgefirst_tensor::CpuAccess::ReadWrite,
11984 )
11985 .unwrap();
11986 {
11987 use edgefirst_tensor::TensorMapTrait;
11988 let u8t = dst.as_typed_mut::<u8>().unwrap();
11989 let mut map = u8t.map().unwrap();
11990 map.as_mut_slice().fill(0x55);
11991 }
11992 processor
11993 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::new().with_background(&bg))
11994 .expect("g2d empty+bg");
11995 assert_every_pixel_eq(&dst, bg_color, "g2d/case2 bg blit");
11996
11997 let detect = DetectBox {
11999 bbox: [0.25, 0.25, 0.75, 0.75].into(),
12000 score: 0.9,
12001 label: 0,
12002 };
12003 let mut dst = TensorDyn::image(
12004 64,
12005 64,
12006 PixelFormat::Rgba,
12007 DType::U8,
12008 Some(TensorMemory::DmaBuf),
12009 edgefirst_tensor::CpuAccess::ReadWrite,
12010 )
12011 .unwrap();
12012 let err = processor
12013 .draw_decoded_masks(&mut dst, &[detect], &[], MaskOverlay::default())
12014 .expect_err("g2d must reject detect-present draw_decoded_masks");
12015 assert!(
12016 matches!(err, Error::NotImplemented(_)),
12017 "g2d case3 wrong error: {err:?}"
12018 );
12019 }
12020
12021 #[test]
12022 fn test_set_format_then_cpu_convert() {
12023 let _lock = acquire_env_lock();
12026 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
12027 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
12028 let mut processor = ImageProcessor::new().unwrap();
12029
12030 let image = edgefirst_bench::testdata::read("zidane.jpg");
12032 let src = load_image_test_helper(&image, Some(PixelFormat::Rgba), None).unwrap();
12033
12034 let mut dst =
12036 TensorDyn::new(&[640, 640, 3], DType::U8, Some(TensorMemory::Mem), None).unwrap();
12037 dst.set_format(PixelFormat::Rgb).unwrap();
12038
12039 processor
12041 .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
12042 .unwrap();
12043
12044 assert_eq!(dst.format(), Some(PixelFormat::Rgb));
12046 assert_eq!(dst.width(), Some(640));
12047 assert_eq!(dst.height(), Some(640));
12048 }
12049
12050 #[test]
12056 fn test_multiple_image_processors_same_thread() {
12057 let _lock = acquire_env_lock();
12060 let mut processors: Vec<ImageProcessor> = (0..4)
12061 .map(|_| ImageProcessor::new().expect("ImageProcessor::new() failed"))
12062 .collect();
12063
12064 for proc in &mut processors {
12065 let src = proc
12066 .create_image(
12067 128,
12068 128,
12069 PixelFormat::Rgb,
12070 DType::U8,
12071 None,
12072 edgefirst_tensor::CpuAccess::ReadWrite,
12073 )
12074 .expect("create src failed");
12075 let mut dst = proc
12076 .create_image(
12077 64,
12078 64,
12079 PixelFormat::Rgb,
12080 DType::U8,
12081 None,
12082 edgefirst_tensor::CpuAccess::ReadWrite,
12083 )
12084 .expect("create dst failed");
12085 proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
12086 .expect("convert failed");
12087 assert_eq!(dst.width(), Some(64));
12088 assert_eq!(dst.height(), Some(64));
12089 }
12090 }
12091
12092 #[test]
12099 fn test_multiple_image_processors_separate_threads() {
12100 use std::sync::mpsc;
12101 use std::time::Duration;
12102
12103 if std::env::var_os("EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS").is_some() {
12113 crate::test_support::report_skip(
12114 "test_multiple_image_processors_separate_threads — known Vivante \
12115 GC7000UL concurrent-EGL-teardown double-free \
12116 (EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS set)",
12117 );
12118 return;
12119 }
12120
12121 const TIMEOUT: Duration = Duration::from_secs(60);
12122
12123 let _lock = acquire_env_lock();
12126
12127 let (tx, rx) = mpsc::channel::<()>();
12128
12129 std::thread::spawn(move || {
12130 let handles: Vec<_> = (0..4)
12131 .map(|i| {
12132 std::thread::spawn(move || {
12133 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
12134 panic!("ImageProcessor::new() failed on thread {i}: {e}")
12135 });
12136 let src = proc
12137 .create_image(
12138 128,
12139 128,
12140 PixelFormat::Rgb,
12141 DType::U8,
12142 None,
12143 edgefirst_tensor::CpuAccess::ReadWrite,
12144 )
12145 .unwrap_or_else(|e| panic!("create src failed on thread {i}: {e}"));
12146 let mut dst = proc
12147 .create_image(
12148 64,
12149 64,
12150 PixelFormat::Rgb,
12151 DType::U8,
12152 None,
12153 edgefirst_tensor::CpuAccess::ReadWrite,
12154 )
12155 .unwrap_or_else(|e| panic!("create dst failed on thread {i}: {e}"));
12156 proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
12157 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
12158 assert_eq!(dst.width(), Some(64));
12159 assert_eq!(dst.height(), Some(64));
12160 })
12161 })
12162 .collect();
12163
12164 for (i, h) in handles.into_iter().enumerate() {
12165 h.join()
12166 .unwrap_or_else(|e| panic!("thread {i} panicked: {e:?}"));
12167 }
12168
12169 let _ = tx.send(());
12170 });
12171
12172 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
12173 panic!("test_multiple_image_processors_separate_threads timed out after {TIMEOUT:?}")
12174 });
12175 }
12176
12177 #[test]
12184 fn test_image_processors_concurrent_operations() {
12185 use std::sync::{mpsc, Arc, Barrier};
12186 use std::time::Duration;
12187
12188 const N: usize = 4;
12189 const ROUNDS: usize = 10;
12190 const TIMEOUT: Duration = Duration::from_secs(60);
12191
12192 let _lock = acquire_env_lock();
12195
12196 let (tx, rx) = mpsc::channel::<()>();
12197
12198 std::thread::spawn(move || {
12199 let barrier = Arc::new(Barrier::new(N));
12200
12201 let handles: Vec<_> = (0..N)
12202 .map(|i| {
12203 let barrier = Arc::clone(&barrier);
12204 std::thread::spawn(move || {
12205 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
12206 panic!("ImageProcessor::new() failed on thread {i}: {e}")
12207 });
12208
12209 barrier.wait();
12211
12212 for round in 0..ROUNDS {
12214 let src = proc
12215 .create_image(
12216 128,
12217 128,
12218 PixelFormat::Rgb,
12219 DType::U8,
12220 None,
12221 edgefirst_tensor::CpuAccess::ReadWrite,
12222 )
12223 .unwrap_or_else(|e| {
12224 panic!("create src failed on thread {i} round {round}: {e}")
12225 });
12226 let mut dst = proc
12227 .create_image(
12228 64,
12229 64,
12230 PixelFormat::Rgb,
12231 DType::U8,
12232 None,
12233 edgefirst_tensor::CpuAccess::ReadWrite,
12234 )
12235 .unwrap_or_else(|e| {
12236 panic!("create dst failed on thread {i} round {round}: {e}")
12237 });
12238 proc.convert(
12239 &src,
12240 &mut dst,
12241 Rotation::None,
12242 Flip::None,
12243 Crop::default(),
12244 )
12245 .unwrap_or_else(|e| {
12246 panic!("convert failed on thread {i} round {round}: {e}")
12247 });
12248 assert_eq!(dst.width(), Some(64));
12249 assert_eq!(dst.height(), Some(64));
12250 }
12251 })
12252 })
12253 .collect();
12254
12255 for (i, h) in handles.into_iter().enumerate() {
12256 h.join()
12257 .unwrap_or_else(|e| panic!("thread {i} panicked: {e:?}"));
12258 }
12259
12260 let _ = tx.send(());
12261 });
12262
12263 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
12264 panic!("test_image_processors_concurrent_operations timed out after {TIMEOUT:?}")
12265 });
12266 }
12267
12268 #[test]
12286 fn test_parallel_processors_unique_outputs() {
12287 use std::sync::{mpsc, Arc, Barrier};
12288 use std::time::Duration;
12289
12290 const N: usize = 4;
12291 const ROUNDS: usize = 25;
12292 const TIMEOUT: Duration = Duration::from_secs(60);
12293
12294 if std::env::var_os("EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS").is_some() {
12295 crate::test_support::report_skip(
12296 "test_parallel_processors_unique_outputs — known Vivante \
12297 GC7000UL concurrent-multi-processor driver abort \
12298 (EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS set)",
12299 );
12300 return;
12301 }
12302
12303 let _lock = acquire_env_lock();
12304 let (tx, rx) = mpsc::channel::<()>();
12305
12306 std::thread::spawn(move || {
12307 let barrier = Arc::new(Barrier::new(N));
12308 let handles: Vec<_> = (0..N)
12309 .map(|i| {
12310 let barrier = Arc::clone(&barrier);
12311 std::thread::spawn(move || {
12312 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
12313 panic!("ImageProcessor::new() failed on thread {i}: {e}")
12314 });
12315 let (w, h) = (640usize, 480usize);
12317 let mem = if edgefirst_tensor::is_dma_available() {
12318 Some(TensorMemory::DmaBuf)
12319 } else {
12320 Some(TensorMemory::Mem)
12321 };
12322 let src = proc
12323 .create_image(
12324 w,
12325 h,
12326 PixelFormat::Nv12,
12327 DType::U8,
12328 mem,
12329 edgefirst_tensor::CpuAccess::ReadWrite,
12330 )
12331 .unwrap();
12332 {
12333 let t = src.as_typed::<u8>().unwrap();
12334 let mut m = t.map().unwrap();
12335 let s = m.as_mut_slice();
12336 for (j, b) in s[..w * h].iter_mut().enumerate() {
12337 *b = ((i * 53 + j) % 200 + 16) as u8;
12338 }
12339 for b in &mut s[w * h..] {
12340 *b = (80 + i * 24) as u8;
12341 }
12342 }
12343 let lb = Crop::letterbox([114, 114, 114, 255]);
12344 let convert_once = |proc: &mut ImageProcessor| -> Vec<u8> {
12345 let mut dst = proc
12346 .create_image(
12347 320,
12348 320,
12349 PixelFormat::Rgba,
12350 DType::U8,
12351 mem,
12352 edgefirst_tensor::CpuAccess::ReadWrite,
12353 )
12354 .unwrap();
12355 proc.convert(&src, &mut dst, Rotation::None, Flip::None, lb)
12356 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
12357 let t = dst.as_typed::<u8>().unwrap();
12358 let m = t.map().unwrap();
12359 m.as_slice().to_vec()
12360 };
12361
12362 let oracle = convert_once(&mut proc);
12363 barrier.wait();
12364 for round in 0..ROUNDS {
12365 let out = convert_once(&mut proc);
12366 let diffs = oracle.iter().zip(&out).filter(|(a, b)| a != b).count();
12367 assert!(
12368 diffs == 0,
12369 "thread {i} round {round}: {diffs}/{} bytes diverged \
12370 from this processor's own oracle — cross-processor \
12371 GL state leakage under parallel execution",
12372 oracle.len()
12373 );
12374 }
12375 })
12376 })
12377 .collect();
12378
12379 for (i, h) in handles.into_iter().enumerate() {
12380 h.join()
12381 .unwrap_or_else(|e| panic!("parallel thread {i} panicked: {e:?}"));
12382 }
12383 let _ = tx.send(());
12384 });
12385
12386 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
12387 panic!("test_parallel_processors_unique_outputs timed out after {TIMEOUT:?}")
12388 });
12389 }
12390
12391 #[test]
12400 #[ignore = "heavy on-demand GL-parallelism stressor; run explicitly on boards"]
12401 fn stress_parallel_processors_oracle() {
12402 use std::sync::{mpsc, Arc, Barrier};
12403 use std::time::Duration;
12404
12405 const N: usize = 4;
12406 const ROUNDS: usize = 200;
12407 const TIMEOUT: Duration = Duration::from_secs(600);
12408
12409 let _lock = acquire_env_lock();
12410 let (tx, rx) = mpsc::channel::<()>();
12411
12412 std::thread::spawn(move || {
12413 let barrier = Arc::new(Barrier::new(N));
12414 let handles: Vec<_> = (0..N)
12415 .map(|i| {
12416 let barrier = Arc::clone(&barrier);
12417 std::thread::spawn(move || {
12418 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
12419 panic!("ImageProcessor::new() failed on thread {i}: {e}")
12420 });
12421 let (w, h) = (1280usize, 720usize);
12422 let mem = if edgefirst_tensor::is_dma_available() {
12423 Some(TensorMemory::DmaBuf)
12424 } else {
12425 Some(TensorMemory::Mem)
12426 };
12427
12428 let src = proc
12431 .create_image(
12432 w,
12433 h,
12434 PixelFormat::Nv12,
12435 DType::U8,
12436 mem,
12437 edgefirst_tensor::CpuAccess::ReadWrite,
12438 )
12439 .unwrap();
12440 {
12441 let t = src.as_typed::<u8>().unwrap();
12442 let mut m = t.map().unwrap();
12443 let s = m.as_mut_slice();
12444 for (j, b) in s[..w * h].iter_mut().enumerate() {
12445 *b = ((i * 37 + j) % 200 + 16) as u8;
12446 }
12447 for b in &mut s[w * h..] {
12448 *b = (96 + i * 16) as u8;
12449 }
12450 }
12451 let lb = Crop::letterbox([114, 114, 114, 255]);
12452
12453 let convert_once = |proc: &mut ImageProcessor| -> Vec<u8> {
12454 let mut dst = proc
12455 .create_image(
12456 640,
12457 640,
12458 PixelFormat::Rgb,
12459 DType::U8,
12460 mem,
12461 edgefirst_tensor::CpuAccess::ReadWrite,
12462 )
12463 .unwrap();
12464 proc.convert(&src, &mut dst, Rotation::None, Flip::None, lb)
12465 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
12466 let t = dst.as_typed::<u8>().unwrap();
12467 let m = t.map().unwrap();
12468 m.as_slice().to_vec()
12469 };
12470
12471 let oracle = convert_once(&mut proc);
12472 barrier.wait();
12473 for round in 0..ROUNDS {
12474 let out = convert_once(&mut proc);
12475 let diffs = oracle.iter().zip(&out).filter(|(a, b)| a != b).count();
12476 assert!(
12477 diffs == 0,
12478 "thread {i} round {round}: {diffs}/{} bytes diverged \
12479 from the pre-barrier oracle",
12480 oracle.len()
12481 );
12482 }
12483 })
12484 })
12485 .collect();
12486
12487 for (i, h) in handles.into_iter().enumerate() {
12488 h.join()
12489 .unwrap_or_else(|e| panic!("stressor thread {i} panicked: {e:?}"));
12490 }
12491 let _ = tx.send(());
12492 });
12493
12494 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
12495 panic!("stress_parallel_processors_oracle timed out after {TIMEOUT:?}")
12496 });
12497 }
12498
12499 #[test]
12512 fn convert_f32_auto_never_errors_non_gl_combo() {
12513 const W: usize = 64;
12514 const H: usize = 64;
12515
12516 let src = TensorDyn::image(
12519 W,
12520 H,
12521 PixelFormat::Yuyv,
12522 DType::U8,
12523 Some(TensorMemory::Mem),
12524 edgefirst_tensor::CpuAccess::ReadWrite,
12525 )
12526 .unwrap();
12527 {
12528 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
12529 let data = map.as_mut_slice();
12530 for chunk in data.as_chunks_mut::<4>().0 {
12531 chunk[0] = 128; chunk[1] = 128; chunk[2] = 160; chunk[3] = 128; }
12536 }
12537
12538 let mut dst = TensorDyn::image(
12539 W,
12540 H,
12541 PixelFormat::Rgb,
12542 DType::F32,
12543 Some(TensorMemory::Mem),
12544 edgefirst_tensor::CpuAccess::ReadWrite,
12545 )
12546 .unwrap();
12547
12548 let mut proc = ImageProcessor::new().unwrap();
12549 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
12550 assert!(
12551 result.is_ok(),
12552 "auto-chain Yuyv→Rgb F32 must not error: {:?}",
12553 result.err()
12554 );
12555
12556 let map = dst.as_typed::<f32>().unwrap().map().unwrap();
12558 let floats = map.as_slice();
12559 assert_eq!(floats.len(), W * H * 3, "unexpected output element count");
12560 for (i, &v) in floats.iter().enumerate() {
12561 assert!(
12562 v.is_finite() && (0.0..=1.0).contains(&v),
12563 "output[{i}]={v} is not finite or not in [0,1]"
12564 );
12565 }
12566
12567 let first_non_zero = floats.iter().find(|&&v| v > 0.01);
12571 assert!(
12572 first_non_zero.is_some(),
12573 "all-zero output detected — CPU path likely did not write to the destination buffer"
12574 );
12575 let r0 = floats[0];
12578 assert!(
12579 (r0 - 0.502_f32).abs() < 0.05,
12580 "first pixel R={r0} expected ≈0.502 (Y=128 neutral grey from YUYV source)"
12581 );
12582 }
12583
12584 #[test]
12590 #[allow(clippy::needless_update)]
12596 fn convert_f16_forced_cpu_correct() {
12597 const W: usize = 16;
12598 const H: usize = 16;
12599 const TOL: f32 = 1.0 / 512.0; let src = TensorDyn::image(
12603 W,
12604 H,
12605 PixelFormat::Rgba,
12606 DType::U8,
12607 Some(TensorMemory::Mem),
12608 edgefirst_tensor::CpuAccess::ReadWrite,
12609 )
12610 .unwrap();
12611 {
12612 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
12613 let data = map.as_mut_slice();
12614 for y in 0..H {
12615 for x in 0..W {
12616 let i = y * W + x;
12617 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;
12621 }
12622 }
12623 }
12624
12625 let mut dst = TensorDyn::image(
12626 W,
12627 H,
12628 PixelFormat::PlanarRgb,
12629 DType::F16,
12630 Some(TensorMemory::Mem),
12631 edgefirst_tensor::CpuAccess::ReadWrite,
12632 )
12633 .unwrap();
12634
12635 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
12636 backend: ComputeBackend::Cpu,
12637 ..Default::default()
12638 })
12639 .unwrap();
12640 proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
12641 .expect("forced-CPU Rgba→PlanarRgb F16 must not error");
12642
12643 let src_map = src.as_typed::<u8>().unwrap().map().unwrap();
12644 let src_bytes = src_map.as_slice();
12645 let dst_map = dst.as_typed::<half::f16>().unwrap().map().unwrap();
12646 let dst_halfs = dst_map.as_slice();
12647
12648 let plane = W * H;
12649 assert_eq!(dst_halfs.len(), plane * 3, "wrong output element count");
12650
12651 for y in 0..H {
12652 for x in 0..W {
12653 let i = y * W + x;
12654 let r_exp = src_bytes[i * 4] as f32 / 255.0;
12655 let g_exp = src_bytes[i * 4 + 1] as f32 / 255.0;
12656 let b_exp = src_bytes[i * 4 + 2] as f32 / 255.0;
12657
12658 let r_got = dst_halfs[i].to_f32();
12659 let g_got = dst_halfs[plane + i].to_f32();
12660 let b_got = dst_halfs[2 * plane + i].to_f32();
12661
12662 assert!(
12663 (r_got - r_exp).abs() <= TOL,
12664 "R plane ({x},{y}): got {r_got}, expected {r_exp}"
12665 );
12666 assert!(
12667 (g_got - g_exp).abs() <= TOL,
12668 "G plane ({x},{y}): got {g_got}, expected {g_exp}"
12669 );
12670 assert!(
12671 (b_got - b_exp).abs() <= TOL,
12672 "B plane ({x},{y}): got {b_got}, expected {b_exp}"
12673 );
12674
12675 if src_bytes[i * 4] != src_bytes[i * 4 + 1] {
12677 assert_ne!(r_got, g_got, "R and G planes must differ at ({x},{y})");
12678 }
12679 }
12680 }
12681 }
12682
12683 #[test]
12691 fn convert_f32_with_rotation_falls_back() {
12692 const W: usize = 16;
12693 const H: usize = 16;
12694
12695 let src = TensorDyn::image(
12697 W,
12698 H,
12699 PixelFormat::Rgba,
12700 DType::U8,
12701 Some(TensorMemory::Mem),
12702 edgefirst_tensor::CpuAccess::ReadWrite,
12703 )
12704 .unwrap();
12705 {
12706 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
12707 let data = map.as_mut_slice();
12708 for y in 0..H {
12709 for x in 0..W {
12710 let i = y * W + x;
12711 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;
12715 }
12716 }
12717 }
12718
12719 let mut dst = TensorDyn::image(
12721 H, W, PixelFormat::Rgb,
12724 DType::F32,
12725 Some(TensorMemory::Mem),
12726 edgefirst_tensor::CpuAccess::ReadWrite,
12727 )
12728 .unwrap();
12729
12730 let mut proc = ImageProcessor::new().unwrap();
12731 let result = proc.convert(
12732 &src,
12733 &mut dst,
12734 Rotation::Clockwise90,
12735 Flip::None,
12736 Crop::default(),
12737 );
12738 assert!(
12739 result.is_ok(),
12740 "auto-chain Rgba→Rgb F32 with Rot90 must not error: {:?}",
12741 result.err()
12742 );
12743
12744 let map = dst.as_typed::<f32>().unwrap().map().unwrap();
12745 let floats = map.as_slice();
12746 assert_eq!(floats.len(), H * W * 3, "unexpected output element count");
12747 for (i, &v) in floats.iter().enumerate() {
12748 assert!(
12749 v.is_finite() && (0.0..=1.0).contains(&v),
12750 "output[{i}]={v} is not finite or not in [0,1]"
12751 );
12752 }
12753 }
12754
12755 #[test]
12781 #[cfg(all(
12782 any(
12783 target_os = "linux",
12784 target_os = "macos",
12785 target_os = "ios",
12786 target_os = "android",
12787 target_os = "windows"
12788 ),
12789 feature = "opengl"
12790 ))]
12791 fn convert_f16_gl_cpu_parity_identity() {
12792 #[cfg(target_os = "macos")]
12798 if std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1")
12799 && std::env::var_os("HAL_TEST_ALLOW_DLOPEN_ANGLE").is_none()
12800 {
12801 crate::test_support::report_skip(
12802 "convert_f16_gl_cpu_parity_identity - ANGLE dlopen gate closed (coverage pass 1)",
12803 );
12804 return;
12805 }
12806 if !is_opengl_available() {
12807 crate::test_support::report_skip(
12808 "convert_f16_gl_cpu_parity_identity - OpenGL not available",
12809 );
12810 return;
12811 }
12812
12813 const W: usize = 16;
12814 const H: usize = 16;
12815 const TOL: f32 = 1.0 / 256.0; let src = TensorDyn::image(
12819 W,
12820 H,
12821 PixelFormat::Rgba,
12822 DType::U8,
12823 Some(TensorMemory::Mem),
12824 edgefirst_tensor::CpuAccess::ReadWrite,
12825 )
12826 .unwrap();
12827 {
12828 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
12829 let data = map.as_mut_slice();
12830 for y in 0..H {
12831 for x in 0..W {
12832 let i = y * W + x;
12833 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;
12837 }
12838 }
12839 }
12840
12841 let gl_result = {
12843 let mut gl_proc = match ImageProcessor::with_config(ImageProcessorConfig {
12844 backend: ComputeBackend::OpenGl,
12845 ..Default::default()
12846 }) {
12847 Ok(p) => p,
12848 Err(e) => {
12849 crate::test_support::report_skip(&format!(
12850 "convert_f16_gl_cpu_parity_identity - GL backend unavailable: {e}"
12851 ));
12852 return;
12853 }
12854 };
12855
12856 if !gl_proc.supported_render_dtypes().f16 {
12857 crate::test_support::report_skip(
12858 "convert_f16_gl_cpu_parity_identity - F16 render not supported",
12859 );
12860 return;
12861 }
12862
12863 let mut dst = match TensorDyn::image(
12883 W,
12884 H,
12885 PixelFormat::PlanarRgb,
12886 DType::F16,
12887 Some(TensorMemory::DmaBuf),
12888 edgefirst_tensor::CpuAccess::ReadWrite,
12889 ) {
12890 Ok(t) if t.memory() == TensorMemory::DmaBuf => t,
12891 Ok(t) => {
12896 crate::test_support::report_skip(&format!(
12897 "convert_f16_gl_cpu_parity_identity - the zero-copy F16 \
12898 destination fell back to {:?}",
12899 t.memory()
12900 ));
12901 return;
12902 }
12903 Err(e) => {
12904 crate::test_support::report_skip(&format!(
12905 "convert_f16_gl_cpu_parity_identity - no zero-copy F16 \
12906 destination on this host: {e}"
12907 ));
12908 return;
12909 }
12910 };
12911 let require_gl = std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1");
12912 let fallbacks_before = gl_proc.convert_fallback_count();
12913 match gl_proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default()) {
12914 Ok(()) => {
12915 if gl_proc.convert_fallback_count() != fallbacks_before {
12916 assert!(
12917 !require_gl,
12918 "HAL_TEST_REQUIRE_GL=1 but \
12919 convert_f16_gl_cpu_parity_identity would have skipped \
12920 while reporting success: the GL F16 path declined and \
12921 the CPU served the convert"
12922 );
12923 crate::test_support::report_skip(
12924 "convert_f16_gl_cpu_parity_identity - the GL F16 path \
12925 declined and the CPU served the convert; comparing that \
12926 against the forced-CPU result would prove nothing",
12927 );
12928 return;
12929 }
12930 dst
12931 }
12932 Err(e) => {
12933 crate::test_support::report_skip(&format!(
12934 "convert_f16_gl_cpu_parity_identity - GL convert failed: {e}"
12935 ));
12936 return;
12937 }
12938 }
12939 };
12940
12941 let cpu_result = {
12943 let mut cpu_proc = ImageProcessor::with_config(ImageProcessorConfig {
12944 backend: ComputeBackend::Cpu,
12945 ..Default::default()
12946 })
12947 .unwrap();
12948 let mut dst = TensorDyn::image(
12949 W,
12950 H,
12951 PixelFormat::PlanarRgb,
12952 DType::F16,
12953 Some(TensorMemory::Mem),
12954 edgefirst_tensor::CpuAccess::ReadWrite,
12955 )
12956 .unwrap();
12957 cpu_proc
12958 .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
12959 .expect("forced-CPU Rgba→PlanarRgb F16 must not error");
12960 dst
12961 };
12962
12963 let flat_f16 = |t: &TensorDyn| -> Vec<half::f16> {
12968 let typed = t.as_typed::<half::f16>().expect("F16 image");
12969 let mut bytes = vec![0u8; typed.shape().iter().product::<usize>() * 2];
12970 typed
12971 .copy_to_flat(&mut bytes)
12972 .expect("compact any padded rows");
12973 bytes
12974 .as_chunks::<2>()
12975 .0
12976 .iter()
12977 .map(|&c| half::f16::from_le_bytes(c))
12978 .collect()
12979 };
12980 let gl_halfs = flat_f16(&gl_result);
12981 let cpu_halfs = flat_f16(&cpu_result);
12982
12983 assert_eq!(
12984 gl_halfs.len(),
12985 cpu_halfs.len(),
12986 "GL and CPU output sizes differ"
12987 );
12988
12989 let plane = W * H;
12990 let channel_names = ["R", "G", "B"];
12991 for (idx, (gl_h, cpu_h)) in gl_halfs.iter().zip(cpu_halfs.iter()).enumerate() {
12992 let gl_v = gl_h.to_f32();
12993 let cpu_v = cpu_h.to_f32();
12994 let err = (gl_v - cpu_v).abs();
12995 let ch = channel_names[idx / plane];
12996 let pixel = idx % plane;
12997 assert!(
12998 err <= TOL,
12999 "GL vs CPU mismatch at {ch}[{pixel}]: GL={gl_v}, CPU={cpu_v}, err={err} > tol={TOL}"
13000 );
13001 }
13002 }
13003
13004 #[test]
13012 #[cfg(all(any(target_os = "linux", target_os = "windows"), feature = "opengl"))]
13013 fn supported_render_dtypes_linux_smoke() {
13014 let proc = match ImageProcessor::new() {
13015 Ok(p) => p,
13016 Err(e) => {
13017 crate::test_support::report_skip(&format!(
13018 "supported_render_dtypes_linux_smoke — ImageProcessor::new() failed: {e}"
13019 ));
13020 return;
13021 }
13022 };
13023 if proc.opengl.is_none() {
13024 crate::test_support::report_skip(
13025 "supported_render_dtypes_linux_smoke — no GL backend on this host",
13026 );
13027 return;
13028 }
13029 let support = proc.supported_render_dtypes();
13031 eprintln!(
13032 "supported_render_dtypes_linux_smoke: f16={} f32={}",
13033 support.f16, support.f32
13034 );
13035 }
13037
13038 #[test]
13047 fn convert_f16_pbo_non_4_aligned_width_falls_back() {
13048 const W: usize = 18; const H: usize = 16;
13050
13051 let src = TensorDyn::image(
13053 W,
13054 H,
13055 PixelFormat::Rgba,
13056 DType::U8,
13057 Some(TensorMemory::Mem),
13058 edgefirst_tensor::CpuAccess::ReadWrite,
13059 )
13060 .unwrap();
13061 {
13062 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13063 let data = map.as_mut_slice();
13064 for chunk in data.as_chunks_mut::<4>().0 {
13065 chunk[0] = 128;
13066 chunk[1] = 64;
13067 chunk[2] = 200;
13068 chunk[3] = 255;
13069 }
13070 }
13071
13072 let mut dst = TensorDyn::image(
13075 W,
13076 H,
13077 PixelFormat::PlanarRgb,
13078 DType::F16,
13079 Some(TensorMemory::Mem),
13080 edgefirst_tensor::CpuAccess::ReadWrite,
13081 )
13082 .unwrap();
13083
13084 let mut proc = ImageProcessor::new().unwrap();
13087 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
13088 assert!(
13089 result.is_ok(),
13090 "auto-chain PlanarRgb F16 W%4!=0 must not error (CPU fallback): {:?}",
13091 result.err()
13092 );
13093
13094 let map = dst.as_typed::<half::f16>().unwrap().map().unwrap();
13096 let halfs = map.as_slice();
13097 assert_eq!(halfs.len(), W * H * 3, "unexpected element count");
13098 for (i, h) in halfs.iter().enumerate() {
13099 let v = h.to_f32();
13100 assert!(
13101 v.is_finite() && (0.0..=1.0).contains(&v),
13102 "output[{i}]={v} is not finite or not in [0,1]"
13103 );
13104 }
13105 }
13106
13107 #[test]
13117 #[allow(clippy::needless_update)]
13120 fn convert_nv12_to_rgb_f32_cpu() {
13121 const W: usize = 16;
13122 const H: usize = 16; let src = TensorDyn::image(
13126 W,
13127 H,
13128 PixelFormat::Nv12,
13129 DType::U8,
13130 Some(TensorMemory::Mem),
13131 edgefirst_tensor::CpuAccess::ReadWrite,
13132 )
13133 .unwrap();
13134 {
13135 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13136 map.as_mut_slice().fill(128); }
13138
13139 let mut dst = TensorDyn::image(
13140 W,
13141 H,
13142 PixelFormat::Rgb,
13143 DType::F32,
13144 Some(TensorMemory::Mem),
13145 edgefirst_tensor::CpuAccess::ReadWrite,
13146 )
13147 .unwrap();
13148
13149 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
13150 backend: ComputeBackend::Cpu,
13151 ..Default::default()
13152 })
13153 .unwrap();
13154
13155 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
13156 assert!(
13157 result.is_ok(),
13158 "forced-CPU NV12→Rgb F32 must not error: {:?}",
13159 result.err()
13160 );
13161
13162 let map = dst.as_typed::<f32>().unwrap().map().unwrap();
13163 let floats = map.as_slice();
13164 assert_eq!(floats.len(), W * H * 3, "unexpected element count");
13165 for (i, &v) in floats.iter().enumerate() {
13166 assert!(
13167 v.is_finite() && (0.0..=1.0).contains(&v),
13168 "output[{i}]={v} is not finite or not in [0,1]"
13169 );
13170 }
13171 let non_zero = floats.iter().any(|&v| v > 0.01);
13173 assert!(non_zero, "all-zero output from NV12→Rgb F32 CPU path");
13174 }
13175
13176 #[test]
13180 #[allow(clippy::needless_update)]
13183 fn convert_nv12_to_planar_rgb_f16_cpu() {
13184 const W: usize = 16;
13185 const H: usize = 16;
13186
13187 let src = TensorDyn::image(
13188 W,
13189 H,
13190 PixelFormat::Nv12,
13191 DType::U8,
13192 Some(TensorMemory::Mem),
13193 edgefirst_tensor::CpuAccess::ReadWrite,
13194 )
13195 .unwrap();
13196 {
13197 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13198 map.as_mut_slice().fill(128);
13199 }
13200
13201 let mut dst = TensorDyn::image(
13202 W,
13203 H,
13204 PixelFormat::PlanarRgb,
13205 DType::F16,
13206 Some(TensorMemory::Mem),
13207 edgefirst_tensor::CpuAccess::ReadWrite,
13208 )
13209 .unwrap();
13210
13211 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
13212 backend: ComputeBackend::Cpu,
13213 ..Default::default()
13214 })
13215 .unwrap();
13216
13217 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
13218 assert!(
13219 result.is_ok(),
13220 "forced-CPU NV12→PlanarRgb F16 must not error: {:?}",
13221 result.err()
13222 );
13223
13224 let map = dst.as_typed::<half::f16>().unwrap().map().unwrap();
13225 let halfs = map.as_slice();
13226 assert_eq!(halfs.len(), W * H * 3, "unexpected element count");
13227 for (i, h) in halfs.iter().enumerate() {
13228 let v = h.to_f32();
13229 assert!(
13230 v.is_finite() && (0.0..=1.0).contains(&v),
13231 "output[{i}]={v} is not finite or not in [0,1]"
13232 );
13233 }
13234 let non_zero = halfs.iter().any(|h| h.to_f32() > 0.01);
13235 assert!(non_zero, "all-zero output from NV12→PlanarRgb F16 CPU path");
13236 }
13237
13238 #[test]
13247 fn create_image_desc_negotiates_and_counts_fallbacks() {
13248 use edgefirst_tensor::{CpuAccess, ImageDesc};
13249 let proc = ImageProcessor::new().unwrap();
13250
13251 let desc =
13252 ImageDesc::new(64, 64, PixelFormat::Rgba, DType::U8).with_access(CpuAccess::ReadWrite);
13253 let plain = proc.create_image_desc(&desc).unwrap();
13254 let classic = proc
13255 .create_image(
13256 64,
13257 64,
13258 PixelFormat::Rgba,
13259 DType::U8,
13260 None,
13261 CpuAccess::ReadWrite,
13262 )
13263 .unwrap();
13264 assert_eq!(plain.memory(), classic.memory(), "same negotiation path");
13265 assert_eq!(plain.compression(), None);
13266
13267 #[cfg(not(target_os = "android"))]
13268 {
13269 use edgefirst_tensor::Compression;
13270 let before = proc.compression_fallback_count();
13271 let desc = ImageDesc::new(64, 64, PixelFormat::Rgba, DType::U8)
13272 .with_compression(Compression::Any);
13273 let t = proc.create_image_desc(&desc).unwrap();
13274 assert_eq!(t.compression(), None, "no vendor tile scheme off-Android");
13275 assert!(
13276 proc.compression_fallback_count() > before,
13277 "Any resolving linear must count"
13278 );
13279 }
13280 }
13281
13282 #[test]
13285 #[cfg(target_os = "linux")]
13286 fn create_image_f32_dma_rejected() {
13287 let proc = ImageProcessor::new().unwrap();
13288 let result = proc.create_image(
13289 64,
13290 64,
13291 PixelFormat::Rgb,
13292 DType::F32,
13293 Some(TensorMemory::DmaBuf),
13294 edgefirst_tensor::CpuAccess::ReadWrite,
13295 );
13296 assert!(
13297 result.is_err(),
13298 "create_image(F32, Dma) must fail — no DRM fourcc for f32"
13299 );
13300 }
13301
13302 #[test]
13311 #[cfg(target_os = "linux")]
13312 fn import_image_carries_colorimetry() {
13313 use edgefirst_tensor::{ColorEncoding, ColorRange, Colorimetry, TensorMemory};
13314
13315 let expected = Colorimetry::default()
13316 .with_encoding(ColorEncoding::Bt709)
13317 .with_range(ColorRange::Limited);
13318
13319 if !is_dma_available() {
13320 let mut t = TensorDyn::image(
13323 8,
13324 8,
13325 PixelFormat::Rgba,
13326 DType::U8,
13327 Some(TensorMemory::Mem),
13328 edgefirst_tensor::CpuAccess::ReadWrite,
13329 )
13330 .expect("alloc");
13331 assert_eq!(t.colorimetry(), None, "colorimetry must start as None");
13332 t.set_colorimetry(Some(expected));
13333 assert_eq!(
13334 t.colorimetry(),
13335 Some(expected),
13336 "set_colorimetry must round-trip"
13337 );
13338 crate::test_support::report_skip("import_image_carries_colorimetry (DMA unavailable); storage contract verified via TensorDyn");
13339 return;
13340 }
13341
13342 use edgefirst_tensor::{PlaneDescriptor, Tensor};
13345
13346 let rgba_bytes = 64 * 64 * 4; let dma_tensor = Tensor::<u8>::new(
13348 &[rgba_bytes],
13349 Some(TensorMemory::DmaBuf),
13350 Some("import_test"),
13351 )
13352 .expect("dma alloc");
13353 let pd =
13354 PlaneDescriptor::new(dma_tensor.dmabuf().expect("dma fd")).expect("PlaneDescriptor");
13355
13356 let proc = ImageProcessor::new().expect("ImageProcessor");
13357 let result = proc.import_image(
13358 pd,
13359 None,
13360 64,
13361 64,
13362 PixelFormat::Rgba,
13363 DType::U8,
13364 Some(expected),
13365 );
13366 let tensor = result.expect("import_image must succeed on DMA fd");
13367 assert_eq!(
13368 tensor.colorimetry(),
13369 Some(expected),
13370 "import_image must store the supplied colorimetry on the returned TensorDyn"
13371 );
13372 }
13373}