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 #[allow(dead_code)] pub(crate) fn resizes(
817 &self,
818 (src_w, src_h): (usize, usize),
819 (dst_w, dst_h): (usize, usize),
820 rotation: Rotation,
821 ) -> bool {
822 let (sw, sh) = self
823 .src_rect
824 .map_or((src_w, src_h), |r| (r.width, r.height));
825 let (dw, dh) = self
826 .dst_rect
827 .map_or((dst_w, dst_h), |r| (r.width, r.height));
828 let (sw, sh) = match rotation {
829 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (sh, sw),
830 Rotation::None | Rotation::Rotate180 => (sw, sh),
831 };
832 (sw, sh) != (dw, dh)
833 }
834
835 pub(crate) fn check_crop_dims(
837 &self,
838 src_w: usize,
839 src_h: usize,
840 dst_w: usize,
841 dst_h: usize,
842 ) -> Result<(), Error> {
843 let src_ok = self
844 .src_rect
845 .is_none_or(|r| r.left + r.width <= src_w && r.top + r.height <= src_h);
846 let dst_ok = self
847 .dst_rect
848 .is_none_or(|r| r.left + r.width <= dst_w && r.top + r.height <= dst_h);
849 match (src_ok, dst_ok) {
850 (true, true) => Ok(()),
851 (true, false) => Err(Error::CropInvalid(format!(
852 "Dest crop invalid: {:?}",
853 self.dst_rect
854 ))),
855 (false, true) => Err(Error::CropInvalid(format!(
856 "Src crop invalid: {:?}",
857 self.src_rect
858 ))),
859 (false, false) => Err(Error::CropInvalid(format!(
860 "Dest and Src crop invalid: {:?} {:?}",
861 self.dst_rect, self.src_rect
862 ))),
863 }
864 }
865}
866
867fn region_to_rect(r: Region) -> Rect {
869 Rect {
870 left: r.x,
871 top: r.y,
872 width: r.width,
873 height: r.height,
874 }
875}
876
877fn letterbox_rect(sw: usize, sh: usize, dw: usize, dh: usize) -> Rect {
881 if sw == 0 || sh == 0 {
882 return Rect::new(0, 0, dw, dh);
883 }
884 let src_aspect = sw as f64 / sh as f64;
885 let dst_aspect = dw as f64 / dh as f64;
886 let (new_w, new_h) = if src_aspect > dst_aspect {
887 (dw, ((dw as f64 / src_aspect).round() as usize).max(1))
888 } else {
889 (((dh as f64 * src_aspect).round() as usize).max(1), dh)
890 };
891 let left = dw.saturating_sub(new_w) / 2;
892 let top = dh.saturating_sub(new_h) / 2;
893 Rect::new(left, top, new_w, new_h)
894}
895
896#[derive(Debug, Clone, Copy, PartialEq, Eq)]
901pub(crate) struct Rect {
902 pub left: usize,
903 pub top: usize,
904 pub width: usize,
905 pub height: usize,
906}
907
908impl Rect {
909 pub fn new(left: usize, top: usize, width: usize, height: usize) -> Self {
911 Self {
912 left,
913 top,
914 width,
915 height,
916 }
917 }
918}
919
920#[enum_dispatch(ImageProcessor)]
921pub trait ImageProcessorTrait {
922 fn convert(
959 &mut self,
960 src: &TensorDyn,
961 dst: &mut TensorDyn,
962 rotation: Rotation,
963 flip: Flip,
964 crop: Crop,
965 ) -> Result<()>;
966
967 fn draw_decoded_masks(
1030 &mut self,
1031 dst: &mut TensorDyn,
1032 detect: &[DetectBox],
1033 segmentation: &[Segmentation],
1034 overlay: MaskOverlay<'_>,
1035 ) -> Result<()>;
1036
1037 fn draw_proto_masks(
1057 &mut self,
1058 dst: &mut TensorDyn,
1059 detect: &[DetectBox],
1060 proto_data: &ProtoData,
1061 overlay: MaskOverlay<'_>,
1062 ) -> Result<()>;
1063
1064 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()>;
1072
1073 fn convert_deferred(
1090 &mut self,
1091 src: &TensorDyn,
1092 dst: &mut TensorDyn,
1093 rotation: Rotation,
1094 flip: Flip,
1095 crop: Crop,
1096 ) -> Result<()> {
1097 self.convert(src, dst, rotation, flip, crop)
1098 }
1099
1100 fn flush(&mut self) -> Result<()> {
1107 Ok(())
1108 }
1109}
1110
1111#[derive(Debug, Clone, Default)]
1117pub struct ImageProcessorConfig {
1118 #[cfg(all(
1129 any(
1130 target_os = "linux",
1131 target_os = "macos",
1132 target_os = "ios",
1133 target_os = "android",
1134 target_os = "windows"
1135 ),
1136 feature = "opengl"
1137 ))]
1138 pub egl_display: Option<EglDisplayKind>,
1139
1140 pub backend: ComputeBackend,
1152
1153 pub colorimetry: ColorimetryMode,
1158
1159 pub interpolation: Interpolation,
1164
1165 pub egl_cache_capacity: Option<usize>,
1224}
1225
1226#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1241pub enum ColorimetryMode {
1242 #[default]
1246 Fast,
1247 Exact,
1250}
1251
1252#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1264#[non_exhaustive]
1265pub enum Interpolation {
1266 Nearest,
1270 #[default]
1275 Bilinear,
1276}
1277
1278impl Interpolation {
1279 pub(crate) fn from_env() -> Option<Self> {
1282 let value = std::env::var("EDGEFIRST_INTERPOLATION").ok()?;
1283 match value.trim().to_ascii_lowercase().as_str() {
1284 "nearest" => Some(Self::Nearest),
1285 "bilinear" => Some(Self::Bilinear),
1286 "" => None,
1287 other => {
1288 log::warn!(
1289 "EDGEFIRST_INTERPOLATION={other:?} is not recognised \
1290 (expected nearest or bilinear); ignoring it"
1291 );
1292 None
1293 }
1294 }
1295 }
1296}
1297
1298#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1305pub enum ComputeBackend {
1306 #[default]
1308 Auto,
1309 Cpu,
1311 G2d,
1313 OpenGl,
1315}
1316
1317#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1323pub(crate) enum ForcedBackend {
1324 Cpu,
1325 G2d,
1326 OpenGl,
1327}
1328
1329#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1354pub struct RenderDtypeSupport {
1355 pub f32: bool,
1362 pub f16: bool,
1370}
1371
1372#[cfg(all(
1387 any(
1388 target_os = "linux",
1389 target_os = "windows",
1390 target_os = "macos",
1391 target_os = "ios",
1392 target_os = "android"
1393 ),
1394 feature = "opengl"
1395))]
1396pub(crate) fn float_pbo_eligible(dtype: DType, support: RenderDtypeSupport) -> bool {
1397 match dtype {
1398 DType::F16 => support.f16,
1399 DType::F32 => support.f32,
1400 _ => false,
1401 }
1402}
1403
1404#[derive(Debug)]
1407pub struct ImageProcessor {
1408 pub cpu: Option<CPUProcessor>,
1411
1412 #[cfg(target_os = "linux")]
1413 pub g2d: Option<G2DProcessor>,
1417 #[cfg(target_os = "linux")]
1418 #[cfg(feature = "opengl")]
1419 pub opengl: Option<GLProcessorThreaded>,
1423 #[cfg(any(
1424 target_os = "macos",
1425 target_os = "ios",
1426 target_os = "android",
1427 target_os = "windows"
1428 ))]
1429 #[cfg(feature = "opengl")]
1430 pub opengl: Option<GLProcessorThreaded>,
1439
1440 pub(crate) forced_backend: Option<ForcedBackend>,
1442
1443 pub(crate) convert_fallbacks: std::sync::atomic::AtomicU64,
1449}
1450
1451unsafe impl Send for ImageProcessor {}
1452unsafe impl Sync for ImageProcessor {}
1453
1454impl ImageProcessor {
1455 pub fn new() -> Result<Self> {
1481 Self::with_config(ImageProcessorConfig::default())
1482 }
1483
1484 pub fn convert_fallback_count(&self) -> u64 {
1491 self.convert_fallbacks
1492 .load(std::sync::atomic::Ordering::Relaxed)
1493 }
1494
1495 pub fn compression_fallback_count(&self) -> u64 {
1504 edgefirst_tensor::compression_fallback_count()
1505 }
1506
1507 pub fn convert_with_fence(
1530 &mut self,
1531 src: &TensorDyn,
1532 dst: &mut TensorDyn,
1533 rotation: Rotation,
1534 flip: Flip,
1535 crop: Crop,
1536 ) -> Result<Option<CompletionFence>> {
1537 #[cfg(any(
1538 target_os = "linux",
1539 target_os = "macos",
1540 target_os = "ios",
1541 target_os = "android",
1542 target_os = "windows"
1543 ))]
1544 #[cfg(feature = "opengl")]
1545 {
1546 let gl_forced = matches!(self.forced_backend, Some(ForcedBackend::OpenGl));
1547 if self.forced_backend.is_none() || gl_forced {
1548 if let Some(opengl) = self.opengl.as_mut() {
1549 match opengl.convert_with_fence(src, dst, rotation, flip, crop) {
1550 Ok(fd) => return Ok(fd),
1551 Err(e) if gl_forced => return Err(e),
1552 Err(e) => {
1553 log::debug!(
1557 "convert_with_fence: opengl declined, \
1558 falling back to the blocking chain: {e}"
1559 );
1560 }
1561 }
1562 } else if gl_forced {
1563 return Err(Error::ForcedBackendUnavailable("opengl".into()));
1564 }
1565 }
1566 }
1567 self.convert(src, dst, rotation, flip, crop)?;
1570 Ok(None)
1571 }
1572
1573 pub fn supported_render_dtypes(&self) -> RenderDtypeSupport {
1586 #[cfg(all(
1587 any(
1588 target_os = "macos",
1589 target_os = "ios",
1590 target_os = "android",
1591 target_os = "windows"
1592 ),
1593 feature = "opengl"
1594 ))]
1595 if let Some(gl) = self.opengl.as_ref() {
1596 return gl.supported_render_dtypes();
1597 }
1598 #[cfg(all(target_os = "linux", feature = "opengl"))]
1599 if let Some(gl) = self.opengl.as_ref() {
1600 return gl.supported_render_dtypes();
1601 }
1602 RenderDtypeSupport {
1603 f32: false,
1604 f16: false,
1605 }
1606 }
1607
1608 pub fn with_config(config: ImageProcessorConfig) -> Result<Self> {
1617 let interpolation = config.interpolation;
1618 let mut processor = Self::with_config_backends(config)?;
1619 processor.set_interpolation(interpolation)?;
1620 Ok(processor)
1621 }
1622
1623 #[allow(unused_variables)]
1625 fn with_config_backends(config: ImageProcessorConfig) -> Result<Self> {
1626 match config.backend {
1630 ComputeBackend::Cpu => {
1631 log::info!("ComputeBackend::Cpu — CPU only");
1632 return Ok(Self {
1633 cpu: Some(CPUProcessor::new()),
1634 #[cfg(target_os = "linux")]
1635 g2d: None,
1636 #[cfg(target_os = "linux")]
1637 #[cfg(feature = "opengl")]
1638 opengl: None,
1639 #[cfg(any(
1640 target_os = "macos",
1641 target_os = "ios",
1642 target_os = "android",
1643 target_os = "windows"
1644 ))]
1645 #[cfg(feature = "opengl")]
1646 opengl: None,
1647 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1648 forced_backend: None,
1649 });
1650 }
1651 ComputeBackend::G2d => {
1652 log::info!("ComputeBackend::G2d — G2D + CPU fallback");
1653 #[cfg(target_os = "linux")]
1654 {
1655 let g2d = match G2DProcessor::new() {
1656 Ok(g) => Some(g),
1657 Err(e) => {
1658 log::warn!("G2D requested but failed to initialize: {e:?}");
1659 None
1660 }
1661 };
1662 return Ok(Self {
1663 cpu: Some(CPUProcessor::new()),
1664 g2d,
1665 #[cfg(feature = "opengl")]
1666 opengl: None,
1667 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1668 forced_backend: None,
1669 });
1670 }
1671 #[cfg(not(target_os = "linux"))]
1672 {
1673 log::warn!("G2D requested but not available on this platform, using CPU");
1674 return Ok(Self {
1675 cpu: Some(CPUProcessor::new()),
1676 #[cfg(any(
1677 target_os = "macos",
1678 target_os = "ios",
1679 target_os = "android",
1680 target_os = "windows"
1681 ))]
1682 #[cfg(feature = "opengl")]
1683 opengl: None,
1684 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1685 forced_backend: None,
1686 });
1687 }
1688 }
1689 ComputeBackend::OpenGl => {
1690 log::info!("ComputeBackend::OpenGl — OpenGL + CPU fallback");
1691 #[cfg(target_os = "linux")]
1692 {
1693 #[cfg(feature = "opengl")]
1694 let opengl = match GLProcessorThreaded::with_cache_capacity(
1695 config.egl_display,
1696 config.egl_cache_capacity,
1697 ) {
1698 Ok(gl) => Some(gl),
1699 Err(e) => {
1700 log::warn!("OpenGL requested but failed to initialize: {e:?}");
1701 None
1702 }
1703 };
1704 return Ok(Self {
1705 cpu: Some(CPUProcessor::new()),
1706 g2d: None,
1707 #[cfg(feature = "opengl")]
1708 opengl,
1709 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1710 forced_backend: None,
1711 }
1712 .apply_colorimetry_mode(config.colorimetry));
1713 }
1714 #[cfg(any(target_os = "macos", target_os = "ios"))]
1715 {
1716 #[cfg(feature = "opengl")]
1717 let opengl = match GLProcessorThreaded::with_cache_capacity(
1718 config.egl_display,
1719 config.egl_cache_capacity,
1720 ) {
1721 Ok(gl) => Some(gl),
1722 Err(e) => {
1723 log::warn!(
1724 "OpenGL requested on macOS but ANGLE init failed: {e:?} \
1725 (install ANGLE via `brew install startergo/angle/angle` \
1726 and re-sign the dylibs — see README.md § macOS GPU \
1727 Acceleration). Falling back to CPU."
1728 );
1729 None
1730 }
1731 };
1732 return Ok(Self {
1733 cpu: Some(CPUProcessor::new()),
1734 #[cfg(feature = "opengl")]
1735 opengl,
1736 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1737 forced_backend: None,
1738 }
1739 .apply_colorimetry_mode(config.colorimetry));
1740 }
1741 #[cfg(target_os = "android")]
1742 {
1743 #[cfg(feature = "opengl")]
1744 let opengl = match GLProcessorThreaded::with_cache_capacity(
1745 config.egl_display,
1746 config.egl_cache_capacity,
1747 ) {
1748 Ok(gl) => Some(gl),
1749 Err(e) => {
1750 log::warn!(
1751 "OpenGL requested but native EGL init failed: {e:?}. \
1752 Falling back to CPU."
1753 );
1754 None
1755 }
1756 };
1757 return Ok(Self {
1758 cpu: Some(CPUProcessor::new()),
1759 #[cfg(feature = "opengl")]
1760 opengl,
1761 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1762 forced_backend: None,
1763 }
1764 .apply_colorimetry_mode(config.colorimetry));
1765 }
1766 #[cfg(target_os = "windows")]
1767 {
1768 #[cfg(feature = "opengl")]
1769 let opengl = match GLProcessorThreaded::with_cache_capacity(
1770 config.egl_display,
1771 config.egl_cache_capacity,
1772 ) {
1773 Ok(gl) => Some(gl),
1774 Err(e) => {
1775 log::warn!(
1776 "OpenGL requested on Windows but ANGLE/D3D11 init failed: {e:?} \
1777 (fetch ANGLE with `scripts/fetch-angle.sh` and set \
1778 EDGEFIRST_ANGLE_PATH to the directory containing libEGL.dll + \
1779 libGLESv2.dll, or place the two DLLs next to the executable — \
1780 see README.md § Windows GPU Acceleration). Falling back to CPU."
1781 );
1782 None
1783 }
1784 };
1785 return Ok(Self {
1786 cpu: Some(CPUProcessor::new()),
1787 #[cfg(feature = "opengl")]
1788 opengl,
1789 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1790 forced_backend: None,
1791 }
1792 .apply_colorimetry_mode(config.colorimetry));
1793 }
1794 #[cfg(not(any(
1795 target_os = "linux",
1796 target_os = "macos",
1797 target_os = "ios",
1798 target_os = "android",
1799 target_os = "windows"
1800 )))]
1801 {
1802 log::warn!("OpenGL requested but not available on this platform, using CPU");
1803 return Ok(Self {
1804 cpu: Some(CPUProcessor::new()),
1805 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1806 forced_backend: None,
1807 });
1808 }
1809 }
1810 ComputeBackend::Auto => { }
1811 }
1812
1813 if let Ok(val) = std::env::var("EDGEFIRST_FORCE_BACKEND") {
1818 let val_lower = val.to_lowercase();
1819 let forced = match val_lower.as_str() {
1820 "cpu" => ForcedBackend::Cpu,
1821 "g2d" => ForcedBackend::G2d,
1822 "opengl" => ForcedBackend::OpenGl,
1823 other => {
1824 return Err(Error::ForcedBackendUnavailable(format!(
1825 "unknown EDGEFIRST_FORCE_BACKEND value: {other:?} (expected cpu, g2d, or opengl)"
1826 )));
1827 }
1828 };
1829
1830 log::info!("EDGEFIRST_FORCE_BACKEND={val} — only initializing {val_lower} backend");
1831
1832 return match forced {
1833 ForcedBackend::Cpu => Ok(Self {
1834 cpu: Some(CPUProcessor::new()),
1835 #[cfg(target_os = "linux")]
1836 g2d: None,
1837 #[cfg(target_os = "linux")]
1838 #[cfg(feature = "opengl")]
1839 opengl: None,
1840 #[cfg(any(
1841 target_os = "macos",
1842 target_os = "ios",
1843 target_os = "android",
1844 target_os = "windows"
1845 ))]
1846 #[cfg(feature = "opengl")]
1847 opengl: None,
1848 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1849 forced_backend: Some(ForcedBackend::Cpu),
1850 }),
1851 ForcedBackend::G2d => {
1852 #[cfg(target_os = "linux")]
1853 {
1854 let g2d = G2DProcessor::new().map_err(|e| {
1855 Error::ForcedBackendUnavailable(format!(
1856 "g2d forced but failed to initialize: {e:?}"
1857 ))
1858 })?;
1859 Ok(Self {
1860 cpu: None,
1861 g2d: Some(g2d),
1862 #[cfg(feature = "opengl")]
1863 opengl: None,
1864 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1865 forced_backend: Some(ForcedBackend::G2d),
1866 })
1867 }
1868 #[cfg(not(target_os = "linux"))]
1869 {
1870 Err(Error::ForcedBackendUnavailable(
1871 "g2d backend is only available on Linux".into(),
1872 ))
1873 }
1874 }
1875 ForcedBackend::OpenGl => {
1876 #[cfg(target_os = "linux")]
1877 #[cfg(feature = "opengl")]
1878 {
1879 let opengl = GLProcessorThreaded::with_cache_capacity(
1880 config.egl_display,
1881 config.egl_cache_capacity,
1882 )
1883 .map_err(|e| {
1884 Error::ForcedBackendUnavailable(format!(
1885 "opengl forced but failed to initialize: {e:?}"
1886 ))
1887 })?;
1888 Ok(Self {
1889 cpu: None,
1890 g2d: None,
1891 opengl: Some(opengl),
1892 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1893 forced_backend: Some(ForcedBackend::OpenGl),
1894 }
1895 .apply_colorimetry_mode(config.colorimetry))
1896 }
1897 #[cfg(any(target_os = "macos", target_os = "ios"))]
1898 #[cfg(feature = "opengl")]
1899 {
1900 let opengl = GLProcessorThreaded::with_cache_capacity(
1901 config.egl_display,
1902 config.egl_cache_capacity,
1903 )
1904 .map_err(|e| {
1905 Error::ForcedBackendUnavailable(format!(
1906 "opengl forced on macOS but ANGLE init failed: {e:?}"
1907 ))
1908 })?;
1909 Ok(Self {
1910 cpu: None,
1911 opengl: Some(opengl),
1912 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1913 forced_backend: Some(ForcedBackend::OpenGl),
1914 }
1915 .apply_colorimetry_mode(config.colorimetry))
1916 }
1917 #[cfg(target_os = "android")]
1918 #[cfg(feature = "opengl")]
1919 {
1920 let opengl = GLProcessorThreaded::with_cache_capacity(
1921 config.egl_display,
1922 config.egl_cache_capacity,
1923 )
1924 .map_err(|e| {
1925 Error::ForcedBackendUnavailable(format!(
1926 "opengl forced but native EGL init failed: {e:?}"
1927 ))
1928 })?;
1929 Ok(Self {
1930 cpu: None,
1931 opengl: Some(opengl),
1932 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1933 forced_backend: Some(ForcedBackend::OpenGl),
1934 }
1935 .apply_colorimetry_mode(config.colorimetry))
1936 }
1937 #[cfg(target_os = "windows")]
1938 #[cfg(feature = "opengl")]
1939 {
1940 let opengl = GLProcessorThreaded::with_cache_capacity(
1941 config.egl_display,
1942 config.egl_cache_capacity,
1943 )
1944 .map_err(|e| {
1945 Error::ForcedBackendUnavailable(format!(
1946 "opengl forced on Windows but ANGLE/D3D11 init failed: {e:?}"
1947 ))
1948 })?;
1949 Ok(Self {
1950 cpu: None,
1951 opengl: Some(opengl),
1952 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1953 forced_backend: Some(ForcedBackend::OpenGl),
1954 }
1955 .apply_colorimetry_mode(config.colorimetry))
1956 }
1957 #[cfg(not(all(
1958 any(
1959 target_os = "linux",
1960 target_os = "macos",
1961 target_os = "ios",
1962 target_os = "android",
1963 target_os = "windows"
1964 ),
1965 feature = "opengl"
1966 )))]
1967 {
1968 Err(Error::ForcedBackendUnavailable(
1969 "opengl backend requires Linux, macOS, Android or Windows with the \
1970 'opengl' feature enabled"
1971 .into(),
1972 ))
1973 }
1974 }
1975 };
1976 }
1977
1978 #[cfg(target_os = "linux")]
1980 let g2d = if std::env::var("EDGEFIRST_DISABLE_G2D")
1981 .map(|x| x != "0" && x.to_lowercase() != "false")
1982 .unwrap_or(false)
1983 {
1984 log::debug!("EDGEFIRST_DISABLE_G2D is set");
1985 None
1986 } else {
1987 match G2DProcessor::new() {
1988 Ok(g2d_converter) => Some(g2d_converter),
1989 Err(err) => {
1990 log::warn!("Failed to initialize G2D converter: {err:?}");
1991 None
1992 }
1993 }
1994 };
1995
1996 #[cfg(target_os = "linux")]
1997 #[cfg(feature = "opengl")]
1998 let opengl = if std::env::var("EDGEFIRST_DISABLE_GL")
1999 .map(|x| x != "0" && x.to_lowercase() != "false")
2000 .unwrap_or(false)
2001 {
2002 log::debug!("EDGEFIRST_DISABLE_GL is set");
2003 None
2004 } else {
2005 match GLProcessorThreaded::with_cache_capacity(
2006 config.egl_display,
2007 config.egl_cache_capacity,
2008 ) {
2009 Ok(gl_converter) => Some(gl_converter),
2010 Err(err) => {
2011 log::warn!("Failed to initialize GL converter: {err:?}");
2012 None
2013 }
2014 }
2015 };
2016
2017 #[cfg(any(
2018 target_os = "macos",
2019 target_os = "ios",
2020 target_os = "android",
2021 target_os = "windows"
2022 ))]
2023 #[cfg(feature = "opengl")]
2024 let opengl = if std::env::var("EDGEFIRST_DISABLE_GL")
2025 .map(|x| x != "0" && x.to_lowercase() != "false")
2026 .unwrap_or(false)
2027 {
2028 log::debug!("EDGEFIRST_DISABLE_GL is set");
2029 None
2030 } else {
2031 match GLProcessorThreaded::with_cache_capacity(
2032 config.egl_display,
2033 config.egl_cache_capacity,
2034 ) {
2035 Ok(gl_converter) => Some(gl_converter),
2036 Err(err) => {
2037 log::debug!(
2038 "GL backend unavailable: {err:?} \
2039 (CPU fallback will be used)"
2040 );
2041 None
2042 }
2043 }
2044 };
2045
2046 let cpu = if std::env::var("EDGEFIRST_DISABLE_CPU")
2047 .map(|x| x != "0" && x.to_lowercase() != "false")
2048 .unwrap_or(false)
2049 {
2050 log::debug!("EDGEFIRST_DISABLE_CPU is set");
2051 None
2052 } else {
2053 Some(CPUProcessor::new())
2054 };
2055 Ok(Self {
2056 cpu,
2057 #[cfg(target_os = "linux")]
2058 g2d,
2059 #[cfg(target_os = "linux")]
2060 #[cfg(feature = "opengl")]
2061 opengl,
2062 #[cfg(any(
2063 target_os = "macos",
2064 target_os = "ios",
2065 target_os = "android",
2066 target_os = "windows"
2067 ))]
2068 #[cfg(feature = "opengl")]
2069 opengl,
2070 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
2071 forced_backend: None,
2072 }
2073 .apply_colorimetry_mode(config.colorimetry))
2074 }
2075
2076 fn apply_colorimetry_mode(self, _mode: ColorimetryMode) -> Self {
2080 #[cfg(all(
2081 any(
2082 target_os = "linux",
2083 target_os = "macos",
2084 target_os = "ios",
2085 target_os = "android",
2086 target_os = "windows"
2087 ),
2088 feature = "opengl"
2089 ))]
2090 {
2091 let mut me = self;
2092 if let Err(e) = me.set_colorimetry_mode(_mode) {
2093 log::warn!("Failed to apply ColorimetryMode::{_mode:?}: {e:?}");
2094 }
2095 me
2096 }
2097 #[cfg(not(all(
2098 any(
2099 target_os = "linux",
2100 target_os = "macos",
2101 target_os = "ios",
2102 target_os = "android",
2103 target_os = "windows"
2104 ),
2105 feature = "opengl"
2106 )))]
2107 {
2108 let _ = _mode;
2109 self
2110 }
2111 }
2112
2113 #[cfg(all(
2118 any(
2119 target_os = "linux",
2120 target_os = "macos",
2121 target_os = "ios",
2122 target_os = "android",
2123 target_os = "windows"
2124 ),
2125 feature = "opengl"
2126 ))]
2127 pub fn set_colorimetry_mode(&mut self, mode: ColorimetryMode) -> Result<()> {
2128 if let Some(ref mut gl) = self.opengl {
2129 gl.set_colorimetry_mode(mode)?;
2130 }
2131 Ok(())
2132 }
2133
2134 pub fn set_interpolation(&mut self, mode: Interpolation) -> Result<()> {
2139 let mode = match Interpolation::from_env() {
2140 Some(env) => {
2141 if env != mode {
2142 log::info!(
2143 "EDGEFIRST_INTERPOLATION pins Interpolation::{env:?}; \
2144 ignoring Interpolation::{mode:?}"
2145 );
2146 }
2147 env
2148 }
2149 None => mode,
2150 };
2151 if let Some(cpu) = self.cpu.as_mut() {
2152 cpu.set_interpolation(mode);
2153 }
2154 #[cfg(target_os = "linux")]
2155 if let Some(g2d) = self.g2d.as_mut() {
2156 g2d.set_interpolation(mode);
2157 }
2158 #[cfg(all(
2159 any(
2160 target_os = "linux",
2161 target_os = "macos",
2162 target_os = "ios",
2163 target_os = "android",
2164 target_os = "windows"
2165 ),
2166 feature = "opengl"
2167 ))]
2168 if let Some(gl) = self.opengl.as_mut() {
2169 gl.set_interpolation(mode)?;
2170 }
2171 Ok(())
2172 }
2173
2174 #[cfg(all(
2177 any(
2178 target_os = "linux",
2179 target_os = "macos",
2180 target_os = "ios",
2181 target_os = "android",
2182 target_os = "windows"
2183 ),
2184 feature = "opengl"
2185 ))]
2186 pub fn set_int8_interpolation_mode(&mut self, mode: Int8InterpolationMode) -> Result<()> {
2187 if let Some(ref mut gl) = self.opengl {
2188 gl.set_int8_interpolation_mode(mode)?;
2189 }
2190 Ok(())
2191 }
2192
2193 pub fn create_image_desc(&self, desc: &edgefirst_tensor::ImageDesc) -> Result<TensorDyn> {
2204 if desc.compression().is_none() {
2205 return self.create_image(
2206 desc.width(),
2207 desc.height(),
2208 desc.format(),
2209 desc.dtype(),
2210 desc.memory(),
2211 desc.access(),
2212 );
2213 }
2214 Ok(TensorDyn::image_desc(desc)?)
2215 }
2216
2217 pub fn create_image(
2308 &self,
2309 width: usize,
2310 height: usize,
2311 format: PixelFormat,
2312 dtype: DType,
2313 memory: Option<TensorMemory>,
2314 access: edgefirst_tensor::CpuAccess,
2315 ) -> Result<TensorDyn> {
2316 #[cfg(target_os = "linux")]
2327 let dma_stride_bytes: Option<usize> = primary_plane_bpp(format, dtype.size())
2328 .and_then(|bpp| width.checked_mul(bpp))
2329 .and_then(align_pitch_bytes_to_gpu_alignment);
2330
2331 #[cfg(target_os = "linux")]
2335 let try_dma = || -> Result<TensorDyn> {
2336 let packed = format.layout() == edgefirst_tensor::PixelLayout::Packed;
2344 match dma_stride_bytes {
2345 Some(stride)
2346 if packed
2347 && primary_plane_bpp(format, dtype.size())
2348 .and_then(|bpp| width.checked_mul(bpp))
2349 .is_some_and(|natural| stride > natural) =>
2350 {
2351 log::debug!(
2352 "create_image: padding row stride for {format:?} {width}x{height} \
2353 from natural pitch to {stride} bytes for GPU alignment"
2354 );
2355 Ok(TensorDyn::image_with_stride(
2356 width,
2357 height,
2358 format,
2359 dtype,
2360 stride,
2361 Some(edgefirst_tensor::TensorMemory::DmaBuf),
2362 access,
2363 )?)
2364 }
2365 _ => Ok(TensorDyn::image(
2366 width,
2367 height,
2368 format,
2369 dtype,
2370 Some(edgefirst_tensor::TensorMemory::DmaBuf),
2371 access,
2372 )?),
2373 }
2374 };
2375
2376 match memory {
2383 #[cfg(target_os = "linux")]
2384 Some(TensorMemory::DmaBuf) => {
2385 if dtype == DType::F32 {
2387 return Err(Error::NotSupported(
2388 "F32 has no 32-bit-float DRM format for DMA-BUF; \
2389 use TensorMemory::Pbo for F32"
2390 .to_string(),
2391 ));
2392 }
2393 return try_dma();
2394 }
2395 Some(mem) => {
2396 return Ok(TensorDyn::image(
2397 width,
2398 height,
2399 format,
2400 dtype,
2401 Some(mem),
2402 access,
2403 )?);
2404 }
2405 None => {}
2406 }
2407
2408 #[cfg(any(
2415 target_os = "macos",
2416 target_os = "ios",
2417 target_os = "android",
2418 target_os = "windows"
2419 ))]
2420 #[cfg(feature = "opengl")]
2421 if let Some(gl) = self.opengl.as_ref() {
2422 let _ = gl;
2423 let support = self.supported_render_dtypes();
2428 let dtype_ok =
2429 !matches!(dtype, DType::F16 | DType::F32) || float_pbo_eligible(dtype, support);
2430 if dtype_ok {
2431 match TensorDyn::image(
2432 width,
2433 height,
2434 format,
2435 dtype,
2436 Some(edgefirst_tensor::TensorMemory::DmaBuf),
2437 access,
2438 ) {
2439 Ok(img) => return Ok(img),
2440 Err(e) => {
2441 log::debug!(
2445 "create_image: zero-copy Dma allocation declined \
2446 ({format:?}/{dtype:?} {width}x{height}): {e:?}; using fallback storage"
2447 );
2448 }
2449 }
2450 }
2451 }
2452
2453 #[cfg(target_os = "linux")]
2456 {
2457 #[cfg(feature = "opengl")]
2458 let gl_uses_pbo = self
2459 .opengl
2460 .as_ref()
2461 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
2462 #[cfg(not(feature = "opengl"))]
2463 let gl_uses_pbo = false;
2464
2465 if !gl_uses_pbo {
2466 if let Ok(img) = try_dma() {
2467 return Ok(img);
2468 }
2469 }
2470 }
2471
2472 #[cfg(any(target_os = "linux", target_os = "windows"))]
2476 #[cfg(feature = "opengl")]
2477 if dtype.size() == 1 {
2478 if let Some(gl) = &self.opengl {
2479 match gl.create_pbo_image(width, height, format) {
2480 Ok(t) => {
2481 if dtype == DType::I8 {
2482 debug_assert!(
2490 t.chroma().is_none(),
2491 "PBO i8 transmute requires chroma == None"
2492 );
2493 let t_i8: Tensor<i8> = unsafe { std::mem::transmute(t) };
2494 return Ok(TensorDyn::from(t_i8));
2495 }
2496 return Ok(TensorDyn::from(t));
2497 }
2498 Err(e) => log::debug!("PBO image creation failed, falling back to Mem: {e:?}"),
2499 }
2500 }
2501 }
2502
2503 #[cfg(any(target_os = "linux", target_os = "windows"))]
2506 #[cfg(feature = "opengl")]
2507 if float_pbo_eligible(dtype, self.supported_render_dtypes()) {
2508 if let Some(gl) = &self.opengl {
2509 match gl.create_pbo_image_dtype(width, height, format, dtype) {
2510 Ok(t) => return Ok(t),
2511 Err(e) => {
2512 log::debug!(
2513 "Float PBO image creation failed for {dtype:?}, \
2514 falling back to Mem: {e:?}"
2515 );
2516 }
2517 }
2518 }
2519 }
2520
2521 Ok(TensorDyn::image(
2523 width,
2524 height,
2525 format,
2526 dtype,
2527 Some(edgefirst_tensor::TensorMemory::Mem),
2528 access,
2529 )?)
2530 }
2531
2532 #[allow(clippy::too_many_arguments)]
2586 #[cfg(target_os = "linux")]
2587 pub fn import_image(
2588 &self,
2589 image: edgefirst_tensor::PlaneDescriptor,
2590 chroma: Option<edgefirst_tensor::PlaneDescriptor>,
2591 width: usize,
2592 height: usize,
2593 format: PixelFormat,
2594 dtype: DType,
2595 colorimetry: Option<edgefirst_tensor::Colorimetry>,
2596 ) -> Result<TensorDyn> {
2597 use edgefirst_tensor::{Tensor, TensorMemory};
2598
2599 let image_stride = image.stride();
2601 let image_offset = image.offset();
2602 let chroma_stride = chroma.as_ref().and_then(|c| c.stride());
2603 let chroma_offset = chroma.as_ref().and_then(|c| c.offset());
2604
2605 if let Some(chroma_pd) = chroma {
2606 if dtype != DType::U8 && dtype != DType::I8 {
2611 return Err(Error::NotSupported(format!(
2612 "multiplane import only supports U8/I8, got {dtype:?}"
2613 )));
2614 }
2615 if format.layout() != PixelLayout::SemiPlanar {
2616 return Err(Error::NotSupported(format!(
2617 "import_image with chroma requires a semi-planar format, got {format:?}"
2618 )));
2619 }
2620
2621 let chroma_h = match format {
2622 PixelFormat::Nv12 => {
2623 height.div_ceil(2)
2625 }
2626 PixelFormat::Nv16 => {
2629 return Err(Error::NotSupported(
2630 "multiplane NV16 is not yet supported; use contiguous NV16 instead".into(),
2631 ))
2632 }
2633 _ => {
2634 return Err(Error::NotSupported(format!(
2635 "unsupported semi-planar format: {format:?}"
2636 )))
2637 }
2638 };
2639
2640 let luma = Tensor::<u8>::from_fd(image.into_fd(), &[height, width], Some("luma"))?;
2641 if luma.memory() != TensorMemory::DmaBuf {
2642 return Err(Error::NotSupported(format!(
2643 "luma fd must be DMA-backed, got {:?}",
2644 luma.memory()
2645 )));
2646 }
2647
2648 let mut chroma_tensor =
2649 Tensor::<u8>::from_fd(chroma_pd.into_fd(), &[chroma_h, width], Some("chroma"))?;
2650 if chroma_tensor.memory() != TensorMemory::DmaBuf {
2651 return Err(Error::NotSupported(format!(
2652 "chroma fd must be DMA-backed, got {:?}",
2653 chroma_tensor.memory()
2654 )));
2655 }
2656
2657 if let Some(s) = chroma_stride {
2661 if s < width {
2662 return Err(Error::InvalidShape(format!(
2663 "chroma stride {s} < minimum {width} for {format:?}"
2664 )));
2665 }
2666 chroma_tensor.set_row_stride_unchecked(s);
2667 }
2668 if let Some(o) = chroma_offset {
2669 chroma_tensor.set_plane_offset(o);
2670 }
2671
2672 let mut tensor = Tensor::<u8>::from_planes(luma, chroma_tensor, format)?;
2675
2676 if let Some(s) = image_stride {
2678 tensor.set_row_stride(s)?;
2679 }
2680 if let Some(o) = image_offset {
2681 tensor.set_plane_offset(o);
2682 }
2683
2684 if dtype == DType::I8 {
2685 const {
2689 assert!(std::mem::size_of::<Tensor<u8>>() == std::mem::size_of::<Tensor<i8>>());
2690 assert!(
2691 std::mem::align_of::<Tensor<u8>>() == std::mem::align_of::<Tensor<i8>>()
2692 );
2693 }
2694 let tensor_i8: Tensor<i8> = unsafe { std::mem::transmute(tensor) };
2695 let mut dyn_tensor = TensorDyn::from(tensor_i8);
2696 dyn_tensor.set_colorimetry(colorimetry);
2697 return Ok(dyn_tensor);
2698 }
2699 let mut dyn_tensor = TensorDyn::from(tensor);
2700 dyn_tensor.set_colorimetry(colorimetry);
2701 Ok(dyn_tensor)
2702 } else {
2703 let shape = format.allocation_shape(width, height).ok_or_else(|| {
2708 Error::NotSupported(format!(
2709 "unsupported pixel format for import_image: {format:?}"
2710 ))
2711 })?;
2712 let tensor = TensorDyn::from_fd(image.into_fd(), &shape, dtype, None)?;
2713 if tensor.memory() != TensorMemory::DmaBuf {
2714 return Err(Error::NotSupported(format!(
2715 "import_image requires DMA-backed fd, got {:?}",
2716 tensor.memory()
2717 )));
2718 }
2719 let mut tensor = tensor.with_format(format)?;
2720 if let Some(s) = image_stride {
2721 tensor.set_row_stride(s)?;
2722 }
2723 if let Some(o) = image_offset {
2724 tensor.set_plane_offset(o);
2725 }
2726 tensor.set_colorimetry(colorimetry);
2727 Ok(tensor)
2728 }
2729 }
2730
2731 #[cfg(feature = "decode")]
2756 pub fn draw_masks(
2757 &mut self,
2758 decoder: &edgefirst_decoder::Decoder,
2759 outputs: &[&TensorDyn],
2760 dst: &mut TensorDyn,
2761 overlay: MaskOverlay<'_>,
2762 ) -> Result<Vec<DetectBox>> {
2763 let mut output_boxes = Vec::with_capacity(100);
2764
2765 let proto_result = decoder
2767 .decode_proto(outputs, &mut output_boxes)
2768 .map_err(|e| Error::Internal(format!("decode_proto: {e:#?}")))?;
2769
2770 if let Some(proto_data) = proto_result {
2771 self.draw_proto_masks(dst, &output_boxes, &proto_data, overlay)?;
2772 } else {
2773 let mut output_masks = Vec::with_capacity(100);
2775 decoder
2776 .decode(outputs, &mut output_boxes, &mut output_masks)
2777 .map_err(|e| Error::Internal(format!("decode: {e:#?}")))?;
2778 self.draw_decoded_masks(dst, &output_boxes, &output_masks, overlay)?;
2779 }
2780 Ok(output_boxes)
2781 }
2782
2783 #[cfg(feature = "tracker")]
2791 pub fn draw_masks_tracked<TR: edgefirst_tracker::Tracker<DetectBox>>(
2792 &mut self,
2793 decoder: &edgefirst_decoder::Decoder,
2794 tracker: &mut TR,
2795 timestamp: u64,
2796 outputs: &[&TensorDyn],
2797 dst: &mut TensorDyn,
2798 overlay: MaskOverlay<'_>,
2799 ) -> Result<(Vec<DetectBox>, Vec<edgefirst_tracker::TrackInfo>)> {
2800 let mut output_boxes = Vec::with_capacity(100);
2801 let mut output_tracks = Vec::new();
2802
2803 let proto_result = decoder
2804 .decode_proto_tracked(
2805 tracker,
2806 timestamp,
2807 outputs,
2808 &mut output_boxes,
2809 &mut output_tracks,
2810 )
2811 .map_err(|e| Error::Internal(format!("decode_proto_tracked: {e:#?}")))?;
2812
2813 if let Some(proto_data) = proto_result {
2814 self.draw_proto_masks(dst, &output_boxes, &proto_data, overlay)?;
2815 } else {
2816 let mut output_masks = Vec::with_capacity(100);
2820 decoder
2821 .decode_tracked(
2822 tracker,
2823 timestamp,
2824 outputs,
2825 &mut output_boxes,
2826 &mut output_masks,
2827 &mut output_tracks,
2828 )
2829 .map_err(|e| Error::Internal(format!("decode_tracked: {e:#?}")))?;
2830 self.draw_decoded_masks(dst, &output_boxes, &output_masks, overlay)?;
2831 }
2832 Ok((output_boxes, output_tracks))
2833 }
2834
2835 pub fn materialize_masks(
2859 &mut self,
2860 detect: &[DetectBox],
2861 proto_data: &ProtoData,
2862 letterbox: Option<[f32; 4]>,
2863 resolution: MaskResolution,
2864 ) -> Result<Vec<Segmentation>> {
2865 let cpu = self.cpu.as_mut().ok_or(Error::NoConverter)?;
2866 match resolution {
2867 MaskResolution::Proto => cpu.materialize_segmentations(detect, proto_data, letterbox),
2868 MaskResolution::Scaled { width, height } => {
2869 cpu.materialize_scaled_segmentations(detect, proto_data, letterbox, width, height)
2870 }
2871 }
2872 }
2873}
2874
2875impl ImageProcessorTrait for ImageProcessor {
2876 fn convert(
2882 &mut self,
2883 src: &TensorDyn,
2884 dst: &mut TensorDyn,
2885 rotation: Rotation,
2886 flip: Flip,
2887 crop: Crop,
2888 ) -> Result<()> {
2889 let start = Instant::now();
2890 let src_fmt = src.format();
2891 let dst_fmt = dst.format();
2892 let _span = tracing::trace_span!(
2893 "image.convert",
2894 ?src_fmt,
2895 ?dst_fmt,
2896 src_memory = ?src.memory(),
2897 dst_memory = ?dst.memory(),
2898 ?rotation,
2899 ?flip,
2900 )
2901 .entered();
2902 log::trace!(
2903 "convert: {src_fmt:?}({:?}/{:?}) → {dst_fmt:?}({:?}/{:?}), \
2904 rotation={rotation:?}, flip={flip:?}, backend={:?}",
2905 src.dtype(),
2906 src.memory(),
2907 dst.dtype(),
2908 dst.memory(),
2909 self.forced_backend,
2910 );
2911
2912 if let Some(forced) = self.forced_backend {
2914 return match forced {
2915 ForcedBackend::Cpu => {
2916 if let Some(cpu) = self.cpu.as_mut() {
2917 let r = cpu.convert(src, dst, rotation, flip, crop);
2918 log::trace!(
2919 "convert: forced=cpu result={} ({:?})",
2920 if r.is_ok() { "ok" } else { "err" },
2921 start.elapsed()
2922 );
2923 return r;
2924 }
2925 Err(Error::ForcedBackendUnavailable("cpu".into()))
2926 }
2927 ForcedBackend::G2d => {
2928 #[cfg(target_os = "linux")]
2929 if let Some(g2d) = self.g2d.as_mut() {
2930 let r = g2d.convert(src, dst, rotation, flip, crop);
2931 log::trace!(
2932 "convert: forced=g2d result={} ({:?})",
2933 if r.is_ok() { "ok" } else { "err" },
2934 start.elapsed()
2935 );
2936 return r;
2937 }
2938 Err(Error::ForcedBackendUnavailable("g2d".into()))
2939 }
2940 ForcedBackend::OpenGl => {
2941 #[cfg(any(
2942 target_os = "linux",
2943 target_os = "macos",
2944 target_os = "ios",
2945 target_os = "android",
2946 target_os = "windows"
2947 ))]
2948 #[cfg(feature = "opengl")]
2949 if let Some(opengl) = self.opengl.as_mut() {
2950 let r = opengl.convert(src, dst, rotation, flip, crop);
2951 log::trace!(
2952 "convert: forced=opengl result={} ({:?})",
2953 if r.is_ok() { "ok" } else { "err" },
2954 start.elapsed()
2955 );
2956 return r;
2957 }
2958 Err(Error::ForcedBackendUnavailable("opengl".into()))
2959 }
2960 };
2961 }
2962
2963 #[cfg(any(
2965 target_os = "linux",
2966 target_os = "macos",
2967 target_os = "ios",
2968 target_os = "android",
2969 target_os = "windows"
2970 ))]
2971 #[cfg(feature = "opengl")]
2972 if let Some(opengl) = self.opengl.as_mut() {
2973 match opengl.convert(src, dst, rotation, flip, crop) {
2974 Ok(_) => {
2975 log::trace!(
2976 "convert: auto selected=opengl for {src_fmt:?}→{dst_fmt:?} ({:?})",
2977 start.elapsed()
2978 );
2979 return Ok(());
2980 }
2981 Err(e) => {
2982 self.convert_fallbacks
2983 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2984 log::debug!(
2985 "convert: auto opengl declined {src_fmt:?}@{:?}→{dst_fmt:?}@{:?}, \
2986 falling back toward G2D/CPU: {e}",
2987 src.memory(),
2988 dst.memory(),
2989 );
2990 }
2991 }
2992 }
2993
2994 #[cfg(target_os = "linux")]
2995 if let Some(g2d) = self.g2d.as_mut() {
2996 let src_is_yuv = src.format().is_some_and(|f| f.is_yuv());
3003 let dst_is_yuv = dst.format().is_some_and(|f| f.is_yuv());
3004 let g2d_eligible = if src_is_yuv || dst_is_yuv {
3005 let cm = if src_is_yuv {
3006 crate::colorimetry::effective_colorimetry(src)
3007 } else {
3008 crate::colorimetry::effective_colorimetry(dst)
3009 };
3010 crate::g2d::g2d_can_handle(&cm, true)
3011 } else {
3012 true
3013 };
3014 if !g2d_eligible {
3015 log::trace!(
3016 "convert: auto g2d skipped {src_fmt:?}→{dst_fmt:?} \
3017 (colorimetry not expressible: full-range/BT.2020)"
3018 );
3019 } else {
3020 match g2d.convert(src, dst, rotation, flip, crop) {
3021 Ok(_) => {
3022 log::trace!(
3023 "convert: auto selected=g2d for {src_fmt:?}→{dst_fmt:?} ({:?})",
3024 start.elapsed()
3025 );
3026 return Ok(());
3027 }
3028 Err(e) => {
3029 log::trace!("convert: auto g2d declined {src_fmt:?}→{dst_fmt:?}: {e}");
3030 }
3031 }
3032 }
3033 }
3034
3035 if let Some(cpu) = self.cpu.as_mut() {
3036 match cpu.convert(src, dst, rotation, flip, crop) {
3037 Ok(_) => {
3038 log::trace!(
3039 "convert: auto selected=cpu for {src_fmt:?}→{dst_fmt:?} ({:?})",
3040 start.elapsed()
3041 );
3042 return Ok(());
3043 }
3044 Err(e) => {
3045 log::trace!("convert: auto cpu failed {src_fmt:?}→{dst_fmt:?}: {e}");
3046 return Err(e);
3047 }
3048 }
3049 }
3050 Err(Error::NoConverter)
3051 }
3052
3053 fn convert_deferred(
3054 &mut self,
3055 src: &TensorDyn,
3056 dst: &mut TensorDyn,
3057 rotation: Rotation,
3058 flip: Flip,
3059 crop: Crop,
3060 ) -> Result<()> {
3061 #[cfg(any(
3067 target_os = "linux",
3068 target_os = "macos",
3069 target_os = "ios",
3070 target_os = "android",
3071 target_os = "windows"
3072 ))]
3073 #[cfg(feature = "opengl")]
3074 {
3075 let gl_forced = matches!(self.forced_backend, Some(ForcedBackend::OpenGl));
3076 if gl_forced || self.forced_backend.is_none() {
3077 if let Some(opengl) = self.opengl.as_mut() {
3078 match opengl.convert_deferred(src, dst, rotation, flip, crop) {
3079 Ok(()) => return Ok(()),
3080 Err(e) => {
3081 log::trace!("convert_deferred: gl declined: {e}; eager fallback");
3082 if gl_forced {
3085 return Err(e);
3086 }
3087 }
3088 }
3089 }
3090 }
3091 }
3092 self.convert(src, dst, rotation, flip, crop)
3093 }
3094
3095 fn flush(&mut self) -> Result<()> {
3096 let _span = tracing::trace_span!("image.flush").entered();
3097 #[cfg(any(
3100 target_os = "linux",
3101 target_os = "macos",
3102 target_os = "ios",
3103 target_os = "android",
3104 target_os = "windows"
3105 ))]
3106 #[cfg(feature = "opengl")]
3107 if let Some(opengl) = self.opengl.as_mut() {
3108 return opengl.flush();
3109 }
3110 Ok(())
3111 }
3112
3113 fn draw_decoded_masks(
3114 &mut self,
3115 dst: &mut TensorDyn,
3116 detect: &[DetectBox],
3117 segmentation: &[Segmentation],
3118 overlay: MaskOverlay<'_>,
3119 ) -> Result<()> {
3120 let _span = tracing::trace_span!(
3121 "image.draw_decoded_masks",
3122 n_detections = detect.len(),
3123 n_segmentations = segmentation.len(),
3124 )
3125 .entered();
3126 let start = Instant::now();
3127
3128 if let Some(bg) = overlay.background {
3129 if bg.aliases(dst) {
3130 return Err(Error::AliasedBuffers(
3131 "background must not reference the same buffer as dst".to_string(),
3132 ));
3133 }
3134 }
3135
3136 let lb_boxes: Vec<DetectBox>;
3139 let lb_segs: Vec<Segmentation>;
3140 let (detect, segmentation) = if let Some(lb) = overlay.letterbox {
3141 lb_boxes = detect.iter().map(|&d| unletter_bbox(d, lb)).collect();
3142 if segmentation.len() == lb_boxes.len() {
3145 lb_segs = segmentation
3152 .iter()
3153 .zip(lb_boxes.iter())
3154 .map(|(s, d)| copy_segmentation(s, d.bbox.into()))
3155 .collect::<Result<Vec<_>>>()?;
3156 (lb_boxes.as_slice(), lb_segs.as_slice())
3157 } else {
3158 (lb_boxes.as_slice(), segmentation)
3162 }
3163 } else {
3164 (detect, segmentation)
3165 };
3166 #[cfg(target_os = "linux")]
3167 let is_empty_frame = detect.is_empty() && segmentation.is_empty();
3168
3169 if let Some(forced) = self.forced_backend {
3171 return match forced {
3172 ForcedBackend::Cpu => {
3173 if let Some(cpu) = self.cpu.as_mut() {
3174 return cpu.draw_decoded_masks(dst, detect, segmentation, overlay);
3175 }
3176 Err(Error::ForcedBackendUnavailable("cpu".into()))
3177 }
3178 ForcedBackend::G2d => {
3179 #[cfg(target_os = "linux")]
3182 if let Some(g2d) = self.g2d.as_mut() {
3183 return g2d.draw_decoded_masks(dst, detect, segmentation, overlay);
3184 }
3185 Err(Error::ForcedBackendUnavailable("g2d".into()))
3186 }
3187 ForcedBackend::OpenGl => {
3188 #[cfg(any(
3191 target_os = "linux",
3192 target_os = "macos",
3193 target_os = "ios",
3194 target_os = "android",
3195 target_os = "windows"
3196 ))]
3197 #[cfg(feature = "opengl")]
3198 if let Some(opengl) = self.opengl.as_mut() {
3199 return opengl.draw_decoded_masks(dst, detect, segmentation, overlay);
3200 }
3201 Err(Error::ForcedBackendUnavailable("opengl".into()))
3202 }
3203 };
3204 }
3205
3206 #[cfg(target_os = "linux")]
3212 if is_empty_frame {
3213 if let Some(g2d) = self.g2d.as_mut() {
3214 match g2d.draw_decoded_masks(dst, detect, segmentation, overlay) {
3215 Ok(_) => {
3216 log::trace!(
3217 "draw_decoded_masks empty frame via g2d in {:?}",
3218 start.elapsed()
3219 );
3220 return Ok(());
3221 }
3222 Err(e) => log::trace!("g2d empty-frame path unavailable: {e:?}"),
3223 }
3224 }
3225 }
3226
3227 #[cfg(any(
3231 target_os = "linux",
3232 target_os = "macos",
3233 target_os = "ios",
3234 target_os = "android",
3235 target_os = "windows"
3236 ))]
3237 #[cfg(feature = "opengl")]
3238 if let Some(opengl) = self.opengl.as_mut() {
3239 log::trace!(
3240 "draw_decoded_masks started with opengl in {:?}",
3241 start.elapsed()
3242 );
3243 match opengl.draw_decoded_masks(dst, detect, segmentation, overlay) {
3244 Ok(_) => {
3245 log::trace!("draw_decoded_masks with opengl in {:?}", start.elapsed());
3246 return Ok(());
3247 }
3248 Err(e) => {
3249 log::trace!("draw_decoded_masks didn't work with opengl: {e:?}")
3250 }
3251 }
3252 }
3253
3254 log::trace!(
3255 "draw_decoded_masks started with cpu in {:?}",
3256 start.elapsed()
3257 );
3258 if let Some(cpu) = self.cpu.as_mut() {
3259 match cpu.draw_decoded_masks(dst, detect, segmentation, overlay) {
3260 Ok(_) => {
3261 log::trace!("draw_decoded_masks with cpu in {:?}", start.elapsed());
3262 return Ok(());
3263 }
3264 Err(e) => {
3265 log::trace!("draw_decoded_masks didn't work with cpu: {e:?}");
3266 return Err(e);
3267 }
3268 }
3269 }
3270 Err(Error::NoConverter)
3271 }
3272
3273 fn draw_proto_masks(
3274 &mut self,
3275 dst: &mut TensorDyn,
3276 detect: &[DetectBox],
3277 proto_data: &ProtoData,
3278 overlay: MaskOverlay<'_>,
3279 ) -> Result<()> {
3280 let start = Instant::now();
3281
3282 if let Some(bg) = overlay.background {
3283 if bg.aliases(dst) {
3284 return Err(Error::AliasedBuffers(
3285 "background must not reference the same buffer as dst".to_string(),
3286 ));
3287 }
3288 }
3289
3290 let lb_boxes: Vec<DetectBox>;
3296 let render_detect = if let Some(lb) = overlay.letterbox {
3297 lb_boxes = detect.iter().map(|&d| unletter_bbox(d, lb)).collect();
3298 lb_boxes.as_slice()
3299 } else {
3300 detect
3301 };
3302 #[cfg(target_os = "linux")]
3303 let is_empty_frame = detect.is_empty();
3304
3305 if let Some(forced) = self.forced_backend {
3307 return match forced {
3308 ForcedBackend::Cpu => {
3309 if let Some(cpu) = self.cpu.as_mut() {
3310 return cpu.draw_proto_masks(dst, render_detect, proto_data, overlay);
3311 }
3312 Err(Error::ForcedBackendUnavailable("cpu".into()))
3313 }
3314 ForcedBackend::G2d => {
3315 #[cfg(target_os = "linux")]
3316 if let Some(g2d) = self.g2d.as_mut() {
3317 return g2d.draw_proto_masks(dst, render_detect, proto_data, overlay);
3318 }
3319 Err(Error::ForcedBackendUnavailable("g2d".into()))
3320 }
3321 ForcedBackend::OpenGl => {
3322 #[cfg(any(
3323 target_os = "linux",
3324 target_os = "macos",
3325 target_os = "ios",
3326 target_os = "android",
3327 target_os = "windows"
3328 ))]
3329 #[cfg(feature = "opengl")]
3330 if let Some(opengl) = self.opengl.as_mut() {
3331 return opengl.draw_proto_masks(dst, render_detect, proto_data, overlay);
3332 }
3333 Err(Error::ForcedBackendUnavailable("opengl".into()))
3334 }
3335 };
3336 }
3337
3338 #[cfg(target_os = "linux")]
3341 if is_empty_frame {
3342 if let Some(g2d) = self.g2d.as_mut() {
3343 match g2d.draw_proto_masks(dst, render_detect, proto_data, overlay) {
3344 Ok(_) => {
3345 log::trace!(
3346 "draw_proto_masks empty frame via g2d in {:?}",
3347 start.elapsed()
3348 );
3349 return Ok(());
3350 }
3351 Err(e) => log::trace!("g2d empty-frame path unavailable: {e:?}"),
3352 }
3353 }
3354 }
3355
3356 #[cfg(any(
3365 target_os = "linux",
3366 target_os = "macos",
3367 target_os = "ios",
3368 target_os = "android",
3369 target_os = "windows"
3370 ))]
3371 #[cfg(feature = "opengl")]
3372 if let (Some(_), Some(_)) = (self.cpu.as_ref(), self.opengl.as_ref()) {
3373 let segmentation = match self.cpu.as_mut() {
3374 Some(cpu) => {
3375 log::trace!(
3376 "draw_proto_masks started with hybrid (cpu+opengl) in {:?}",
3377 start.elapsed()
3378 );
3379 cpu.materialize_segmentations(detect, proto_data, overlay.letterbox)?
3380 }
3381 None => unreachable!("cpu presence checked above"),
3382 };
3383 if let Some(opengl) = self.opengl.as_mut() {
3384 match opengl.draw_decoded_masks(dst, render_detect, &segmentation, overlay) {
3385 Ok(_) => {
3386 log::trace!(
3387 "draw_proto_masks with hybrid (cpu+opengl) in {:?}",
3388 start.elapsed()
3389 );
3390 return Ok(());
3391 }
3392 Err(e) => {
3393 log::trace!(
3394 "draw_proto_masks hybrid path failed, falling back to cpu: {e:?}"
3395 );
3396 }
3397 }
3398 }
3399 }
3400
3401 let Some(cpu) = self.cpu.as_mut() else {
3402 return Err(Error::Internal(
3403 "draw_proto_masks requires CPU backend for fallback path".into(),
3404 ));
3405 };
3406 log::trace!("draw_proto_masks started with cpu in {:?}", start.elapsed());
3407 cpu.draw_proto_masks(dst, render_detect, proto_data, overlay)
3408 }
3409
3410 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()> {
3411 let start = Instant::now();
3412
3413 if let Some(forced) = self.forced_backend {
3415 return match forced {
3416 ForcedBackend::Cpu => {
3417 if let Some(cpu) = self.cpu.as_mut() {
3418 return cpu.set_class_colors(colors);
3419 }
3420 Err(Error::ForcedBackendUnavailable("cpu".into()))
3421 }
3422 ForcedBackend::G2d => Err(Error::NotSupported(
3423 "g2d does not support set_class_colors".into(),
3424 )),
3425 ForcedBackend::OpenGl => {
3426 #[cfg(any(
3427 target_os = "linux",
3428 target_os = "macos",
3429 target_os = "ios",
3430 target_os = "android",
3431 target_os = "windows"
3432 ))]
3433 #[cfg(feature = "opengl")]
3434 if let Some(opengl) = self.opengl.as_mut() {
3435 return opengl.set_class_colors(colors);
3436 }
3437 Err(Error::ForcedBackendUnavailable("opengl".into()))
3438 }
3439 };
3440 }
3441
3442 let mut backends = 0usize;
3451 let mut first_err = None;
3452 #[cfg(any(
3453 target_os = "linux",
3454 target_os = "macos",
3455 target_os = "ios",
3456 target_os = "android",
3457 target_os = "windows"
3458 ))]
3459 #[cfg(feature = "opengl")]
3460 if let Some(opengl) = self.opengl.as_mut() {
3461 backends += 1;
3462 match opengl.set_class_colors(colors) {
3463 Ok(()) => log::trace!("colors set with opengl in {:?}", start.elapsed()),
3464 Err(e) => {
3465 log::trace!("colors didn't set with opengl: {e:?}");
3466 first_err.get_or_insert(e);
3467 }
3468 }
3469 }
3470 if let Some(cpu) = self.cpu.as_mut() {
3471 backends += 1;
3472 match cpu.set_class_colors(colors) {
3473 Ok(()) => log::trace!("colors set with cpu in {:?}", start.elapsed()),
3474 Err(e) => {
3475 log::trace!("colors didn't set with cpu: {e:?}");
3476 first_err.get_or_insert(e);
3477 }
3478 }
3479 }
3480 match first_err {
3484 Some(e) => Err(e),
3485 None if backends > 0 => Ok(()),
3486 None => Err(Error::NoConverter),
3487 }
3488 }
3489}
3490
3491#[cfg(all(test, feature = "codec"))]
3501pub(crate) fn load_image_test_helper(
3502 image: &[u8],
3503 format: Option<PixelFormat>,
3504 memory: Option<TensorMemory>,
3505) -> Result<TensorDyn> {
3506 use edgefirst_codec::{peek_info, ImageDecoder, ImageLoad};
3507
3508 let info = peek_info(image)?;
3512 let native_fmt = info.format;
3513 let w = info.width;
3514 let h = info.height;
3515
3516 let mut decoder = ImageDecoder::new();
3517
3518 #[cfg(target_os = "linux")]
3521 let native_src = {
3522 if let Some(aligned_pitch) = padded_dma_pitch_for(native_fmt, w, &memory) {
3523 let mut dma = Tensor::<u8>::image_with_stride(
3524 w,
3525 h,
3526 native_fmt,
3527 aligned_pitch,
3528 Some(TensorMemory::DmaBuf),
3529 edgefirst_tensor::CpuAccess::ReadWrite,
3530 )?;
3531 dma.load_image(&mut decoder, image)?;
3532 TensorDyn::from(dma)
3533 } else {
3534 let mut img = Tensor::<u8>::image(
3535 w,
3536 h,
3537 native_fmt,
3538 memory,
3539 edgefirst_tensor::CpuAccess::ReadWrite,
3540 )?;
3541 img.load_image(&mut decoder, image)?;
3542 TensorDyn::from(img)
3543 }
3544 };
3545 #[cfg(not(target_os = "linux"))]
3546 let native_src = {
3547 let mut img = Tensor::<u8>::image(
3548 w,
3549 h,
3550 native_fmt,
3551 memory,
3552 edgefirst_tensor::CpuAccess::ReadWrite,
3553 )?;
3554 img.load_image(&mut decoder, image)?;
3555 TensorDyn::from(img)
3556 };
3557
3558 match format {
3562 Some(f) if f != native_fmt => {
3563 let mut dst = TensorDyn::image(
3564 w,
3565 h,
3566 f,
3567 DType::U8,
3568 memory,
3569 edgefirst_tensor::CpuAccess::ReadWrite,
3570 )?;
3571 #[allow(clippy::needless_update)]
3578 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
3579 backend: ComputeBackend::Cpu,
3580 ..Default::default()
3581 })?;
3582 proc.convert(
3583 &native_src,
3584 &mut dst,
3585 Rotation::None,
3586 Flip::None,
3587 Crop::default(),
3588 )?;
3589 Ok(dst)
3590 }
3591 _ => Ok(native_src),
3592 }
3593}
3594
3595pub fn save_jpeg(tensor: &TensorDyn, path: impl AsRef<std::path::Path>, quality: u8) -> Result<()> {
3599 let t = tensor.as_typed::<u8>().ok_or(Error::UnsupportedFormat(
3600 "save_jpeg requires u8 tensor".to_string(),
3601 ))?;
3602 let fmt = t.format().ok_or(Error::NotAnImage)?;
3603 if fmt.layout() != PixelLayout::Packed {
3604 return Err(Error::NotImplemented(
3605 "Saving planar images is not supported".to_string(),
3606 ));
3607 }
3608
3609 let colour = match fmt {
3610 PixelFormat::Rgb => jpeg_encoder::ColorType::Rgb,
3611 PixelFormat::Rgba => jpeg_encoder::ColorType::Rgba,
3612 _ => {
3613 return Err(Error::NotImplemented(
3614 "Unsupported image format for saving".to_string(),
3615 ));
3616 }
3617 };
3618
3619 let w = t.width().ok_or(Error::NotAnImage)?;
3620 let h = t.height().ok_or(Error::NotAnImage)?;
3621 let encoder = jpeg_encoder::Encoder::new_file(path, quality)?;
3622 let tensor_map = t.map_read()?;
3623
3624 encoder.encode(&tensor_map, w as u16, h as u16, colour)?;
3625
3626 Ok(())
3627}
3628
3629pub(crate) struct FunctionTimer<T: Display> {
3630 name: T,
3631 start: std::time::Instant,
3632}
3633
3634impl<T: Display> FunctionTimer<T> {
3635 pub fn new(name: T) -> Self {
3636 Self {
3637 name,
3638 start: std::time::Instant::now(),
3639 }
3640 }
3641}
3642
3643impl<T: Display> Drop for FunctionTimer<T> {
3644 fn drop(&mut self) {
3645 log::trace!("{} elapsed: {:?}", self.name, self.start.elapsed())
3646 }
3647}
3648
3649const DEFAULT_COLORS: [[f32; 4]; 20] = [
3650 [0., 1., 0., 0.7],
3651 [1., 0.5568628, 0., 0.7],
3652 [0.25882353, 0.15294118, 0.13333333, 0.7],
3653 [0.8, 0.7647059, 0.78039216, 0.7],
3654 [0.3137255, 0.3137255, 0.3137255, 0.7],
3655 [0.1411765, 0.3098039, 0.1215686, 0.7],
3656 [1., 0.95686275, 0.5137255, 0.7],
3657 [0.3529412, 0.32156863, 0., 0.7],
3658 [0.4235294, 0.6235294, 0.6509804, 0.7],
3659 [0.5098039, 0.5098039, 0.7294118, 0.7],
3660 [0.00784314, 0.18823529, 0.29411765, 0.7],
3661 [0.0, 0.2706, 1.0, 0.7],
3662 [0.0, 0.0, 0.0, 0.7],
3663 [0.0, 0.5, 0.0, 0.7],
3664 [1.0, 0.0, 0.0, 0.7],
3665 [0.0, 0.0, 1.0, 0.7],
3666 [1.0, 0.5, 0.5, 0.7],
3667 [0.1333, 0.5451, 0.1333, 0.7],
3668 [0.1176, 0.4118, 0.8235, 0.7],
3669 [1., 1., 1., 0.7],
3670];
3671
3672const fn denorm<const M: usize, const N: usize>(a: [[f32; M]; N]) -> [[u8; M]; N] {
3673 let mut result = [[0; M]; N];
3674 let mut i = 0;
3675 while i < N {
3676 let mut j = 0;
3677 while j < M {
3678 result[i][j] = (a[i][j] * 255.0).round() as u8;
3679 j += 1;
3680 }
3681 i += 1;
3682 }
3683 result
3684}
3685
3686const DEFAULT_COLORS_U8: [[u8; 4]; 20] = denorm(DEFAULT_COLORS);
3687
3688#[cfg(test)]
3689#[cfg_attr(coverage_nightly, coverage(off))]
3690mod alignment_tests {
3691 use super::*;
3692
3693 #[test]
3694 fn align_width_rgba8_common_widths() {
3695 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); }
3706
3707 #[test]
3708 fn align_width_rgb888_packed() {
3709 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] {
3716 let padded = align_width_for_gpu_pitch(w, 3);
3717 assert!(padded >= w);
3718 assert_eq!((padded * 3) % 64, 0);
3719 assert_eq!((padded * 3) % 3, 0);
3720 }
3721 }
3722
3723 #[test]
3724 fn align_width_grey_u8() {
3725 assert_eq!(align_width_for_gpu_pitch(64, 1), 64);
3727 assert_eq!(align_width_for_gpu_pitch(640, 1), 640);
3728 assert_eq!(align_width_for_gpu_pitch(1, 1), 64);
3729 assert_eq!(align_width_for_gpu_pitch(65, 1), 128);
3730 }
3731
3732 #[test]
3733 fn align_width_zero_inputs() {
3734 assert_eq!(align_width_for_gpu_pitch(0, 4), 0);
3735 assert_eq!(align_width_for_gpu_pitch(640, 0), 640);
3736 }
3737
3738 #[test]
3739 fn align_width_never_returns_smaller_than_input() {
3740 for &bpp in &[1usize, 2, 3, 4, 8] {
3744 for &w in &[
3745 1usize,
3746 17,
3747 64,
3748 65,
3749 100,
3750 1280,
3751 1281,
3752 1920,
3753 3004,
3754 3072,
3755 3840,
3756 usize::MAX / 8,
3757 usize::MAX / 4,
3758 usize::MAX / 2,
3759 usize::MAX - 1,
3760 usize::MAX,
3761 ] {
3762 let aligned = align_width_for_gpu_pitch(w, bpp);
3763 assert!(
3764 aligned >= w,
3765 "align_width_for_gpu_pitch({w}, {bpp}) = {aligned} < {w}"
3766 );
3767 }
3768 }
3769 }
3770
3771 #[test]
3772 fn align_width_overflow_returns_unaligned_not_smaller() {
3773 let aligned_extreme = usize::MAX - 15; assert_eq!(
3779 align_width_for_gpu_pitch(aligned_extreme, 4),
3780 aligned_extreme
3781 );
3782 let misaligned_extreme = usize::MAX - 1;
3785 let result = align_width_for_gpu_pitch(misaligned_extreme, 4);
3786 assert!(
3787 result == misaligned_extreme || result >= misaligned_extreme,
3788 "extreme misaligned width must not be rounded down to {result}"
3789 );
3790 }
3791
3792 #[test]
3793 fn checked_lcm_basic_and_overflow() {
3794 assert_eq!(checked_num_integer_lcm(64, 4), Some(64));
3795 assert_eq!(checked_num_integer_lcm(64, 3), Some(192));
3796 assert_eq!(checked_num_integer_lcm(64, 1), Some(64));
3797 assert_eq!(checked_num_integer_lcm(0, 4), Some(0));
3798 assert_eq!(checked_num_integer_lcm(64, 0), Some(0));
3799 assert_eq!(
3801 checked_num_integer_lcm(usize::MAX, usize::MAX - 1),
3802 None,
3803 "coprime extreme values must overflow detect, not panic"
3804 );
3805 }
3806
3807 #[test]
3808 fn primary_plane_bpp_known_formats() {
3809 assert_eq!(primary_plane_bpp(PixelFormat::Rgba, 1), Some(4));
3811 assert_eq!(primary_plane_bpp(PixelFormat::Bgra, 1), Some(4));
3812 assert_eq!(primary_plane_bpp(PixelFormat::Rgb, 1), Some(3));
3813 assert_eq!(primary_plane_bpp(PixelFormat::Grey, 1), Some(1));
3814 assert_eq!(primary_plane_bpp(PixelFormat::Nv12, 1), Some(1));
3816 }
3817}
3818
3819#[cfg(test)]
3820#[cfg_attr(coverage_nightly, coverage(off))]
3821#[allow(deprecated)]
3822mod image_tests {
3823 use super::*;
3824 use crate::{CPUProcessor, Rotation};
3825 #[cfg(target_os = "linux")]
3826 use edgefirst_tensor::is_dma_available;
3827 use edgefirst_tensor::{TensorMapTrait, TensorMemory, TensorTrait};
3828 use image::buffer::ConvertBuffer;
3829
3830 fn convert_img(
3836 proc: &mut dyn ImageProcessorTrait,
3837 src: TensorDyn,
3838 dst: TensorDyn,
3839 rotation: Rotation,
3840 flip: Flip,
3841 crop: Crop,
3842 ) -> (Result<()>, TensorDyn, TensorDyn) {
3843 let src_fourcc = src.format().unwrap();
3844 let dst_fourcc = dst.format().unwrap();
3845 let src_dyn = src;
3846 let mut dst_dyn = dst;
3847 let result = proc.convert(&src_dyn, &mut dst_dyn, rotation, flip, crop);
3848 let src_back = {
3849 let mut __t = src_dyn.into_u8().unwrap();
3850 __t.set_format(src_fourcc).unwrap();
3851 TensorDyn::from(__t)
3852 };
3853 let dst_back = {
3854 let mut __t = dst_dyn.into_u8().unwrap();
3855 __t.set_format(dst_fourcc).unwrap();
3856 TensorDyn::from(__t)
3857 };
3858 (result, src_back, dst_back)
3859 }
3860
3861 #[ctor::ctor(unsafe)]
3862 fn init() {
3863 env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
3864 }
3865
3866 macro_rules! function {
3867 () => {{
3868 fn f() {}
3869 fn type_name_of<T>(_: T) -> &'static str {
3870 std::any::type_name::<T>()
3871 }
3872 let name = type_name_of(f);
3873
3874 match &name[..name.len() - 3].rfind(':') {
3876 Some(pos) => &name[pos + 1..name.len() - 3],
3877 None => &name[..name.len() - 3],
3878 }
3879 }};
3880 }
3881
3882 #[test]
3895 fn batch_view_dst_tiles_match_standalone() {
3896 let mut proc = match ImageProcessor::new() {
3897 Ok(p) => p,
3898 Err(e) => {
3899 crate::test_support::report_skip(&format!(
3900 "{} — ImageProcessor init failed ({e:?})",
3901 function!()
3902 ));
3903 return;
3904 }
3905 };
3906 let n = 3usize;
3907 let (w, h) = (32usize, 24usize);
3908 let colors: [[u8; 4]; 3] = [[210, 40, 40, 255], [40, 210, 40, 255], [40, 40, 210, 255]];
3909 let make_src = |c: [u8; 4]| -> TensorDyn {
3910 let bytes: Vec<u8> = c.iter().copied().cycle().take(w * h * 4).collect();
3911 load_bytes_to_tensor(w, h, PixelFormat::Rgba, Some(TensorMemory::Mem), &bytes).unwrap()
3912 };
3913 let parent = match TensorDyn::image(
3916 w,
3917 n * h,
3918 PixelFormat::Rgba,
3919 DType::U8,
3920 Some(TensorMemory::DmaBuf),
3921 edgefirst_tensor::CpuAccess::ReadWrite,
3922 ) {
3923 Ok(d) => d,
3924 Err(e) => {
3925 crate::test_support::report_skip(&format!(
3926 "{} — tall DMA destination alloc failed ({e:?})",
3927 function!()
3928 ));
3929 return;
3930 }
3931 };
3932
3933 for (i, &c) in colors.iter().enumerate().take(n) {
3935 let mut tile = parent.view(Region::new(0, i * h, w, h)).unwrap();
3936 proc.convert_deferred(
3937 &make_src(c),
3938 &mut tile,
3939 Rotation::None,
3940 Flip::None,
3941 Crop::no_crop(),
3942 )
3943 .unwrap_or_else(|e| panic!("convert_deferred tile {i}: {e:?}"));
3944 }
3945 proc.flush().unwrap();
3946
3947 for (i, &c) in colors.iter().enumerate().take(n) {
3948 let mut solo = TensorDyn::image(
3950 w,
3951 h,
3952 PixelFormat::Rgba,
3953 DType::U8,
3954 Some(TensorMemory::DmaBuf),
3955 edgefirst_tensor::CpuAccess::ReadWrite,
3956 )
3957 .unwrap();
3958 proc.convert(
3959 &make_src(c),
3960 &mut solo,
3961 Rotation::None,
3962 Flip::None,
3963 Crop::no_crop(),
3964 )
3965 .unwrap();
3966
3967 let band = parent.view(Region::new(0, i * h, w, h)).unwrap();
3968 let band_bytes = band
3969 .as_typed::<u8>()
3970 .unwrap()
3971 .map()
3972 .unwrap()
3973 .as_slice()
3974 .to_vec();
3975 let solo_bytes = solo
3976 .as_typed::<u8>()
3977 .unwrap()
3978 .map()
3979 .unwrap()
3980 .as_slice()
3981 .to_vec();
3982 assert_eq!(
3983 band_bytes, solo_bytes,
3984 "tile {i}: band differs from standalone convert (placement or sibling wipe)"
3985 );
3986 assert!(
3987 band_bytes.as_chunks::<4>().0.iter().all(|&p| p == c),
3988 "tile {i}: band is not the expected solid color {c:?} (sibling wipe?)"
3989 );
3990 }
3991 }
3992
3993 #[cfg(test)]
3997 fn gradient_frame(w: usize, h: usize) -> TensorDyn {
3998 let mut bytes = vec![0u8; w * h * 4];
3999 for y in 0..h {
4000 for x in 0..w {
4001 let i = (y * w + x) * 4;
4002 bytes[i] = x as u8;
4003 bytes[i + 1] = y as u8;
4004 bytes[i + 2] = (x ^ y) as u8;
4005 bytes[i + 3] = 255;
4006 }
4007 }
4008 load_bytes_to_tensor(w, h, PixelFormat::Rgba, Some(TensorMemory::Mem), &bytes).unwrap()
4009 }
4010
4011 #[test]
4015 fn tile_into_cpu_distinct_content_parity() {
4016 let mut proc = match ImageProcessor::with_config(ImageProcessorConfig {
4017 backend: ComputeBackend::Cpu,
4018 ..Default::default()
4019 }) {
4020 Ok(p) => p,
4021 Err(e) => {
4022 crate::test_support::report_skip(&format!(
4023 "{} — CPU init failed ({e:?})",
4024 function!()
4025 ));
4026 return;
4027 }
4028 };
4029 let (fw, fh) = (96usize, 64usize);
4030 let src = gradient_frame(fw, fh);
4031 let cfg = TilingConfig::new(32, 32).with_overlap(0.0); let n = tile_grid(fh, fw, 32, 32, 0.0).len();
4033 assert_eq!(n, 6);
4034
4035 let mut parent = proc
4036 .alloc_tile_batch(
4037 n,
4038 &cfg,
4039 PixelFormat::Rgba,
4040 DType::U8,
4041 Some(TensorMemory::Mem),
4042 edgefirst_tensor::CpuAccess::ReadWrite,
4043 )
4044 .unwrap();
4045 let placements = proc.tile_into(&src, &mut parent, &cfg).unwrap();
4046 assert_eq!(placements.len(), n);
4047
4048 for p in &placements {
4049 let source = Region::new(
4050 p.origin.0 as usize,
4051 p.origin.1 as usize,
4052 p.crop_size.0 as usize,
4053 p.crop_size.1 as usize,
4054 );
4055 let mut solo = TensorDyn::image(
4056 32,
4057 32,
4058 PixelFormat::Rgba,
4059 DType::U8,
4060 Some(TensorMemory::Mem),
4061 edgefirst_tensor::CpuAccess::ReadWrite,
4062 )
4063 .unwrap();
4064 proc.convert(
4065 &src,
4066 &mut solo,
4067 Rotation::None,
4068 Flip::None,
4069 Crop::default()
4070 .with_source(Some(source))
4071 .with_fit(Fit::Stretch),
4072 )
4073 .unwrap();
4074 let band = parent.view(Region::new(0, p.index * 32, 32, 32)).unwrap();
4075 let band_bytes = band
4076 .as_typed::<u8>()
4077 .unwrap()
4078 .map()
4079 .unwrap()
4080 .as_slice()
4081 .to_vec();
4082 let solo_bytes = solo
4083 .as_typed::<u8>()
4084 .unwrap()
4085 .map()
4086 .unwrap()
4087 .as_slice()
4088 .to_vec();
4089 assert_eq!(
4090 band_bytes, solo_bytes,
4091 "tile {} band differs from standalone crop-convert",
4092 p.index
4093 );
4094 }
4095 }
4096
4097 #[test]
4100 fn tile_one_matches_tile_into_band() {
4101 let mut proc = match ImageProcessor::with_config(ImageProcessorConfig {
4102 backend: ComputeBackend::Cpu,
4103 ..Default::default()
4104 }) {
4105 Ok(p) => p,
4106 Err(e) => {
4107 crate::test_support::report_skip(&format!(
4108 "{} — CPU init failed ({e:?})",
4109 function!()
4110 ));
4111 return;
4112 }
4113 };
4114 let (fw, fh) = (96usize, 64usize);
4115 let src = gradient_frame(fw, fh);
4116 let cfg = TilingConfig::new(32, 32).with_overlap(0.0);
4117 let n = tile_grid(fh, fw, 32, 32, 0.0).len();
4118
4119 let mut parent = proc
4120 .alloc_tile_batch(
4121 n,
4122 &cfg,
4123 PixelFormat::Rgba,
4124 DType::U8,
4125 Some(TensorMemory::Mem),
4126 edgefirst_tensor::CpuAccess::ReadWrite,
4127 )
4128 .unwrap();
4129 proc.tile_into(&src, &mut parent, &cfg).unwrap();
4130
4131 let plan = proc.plan_tiles(fw, fh, &cfg).unwrap();
4132 for p in &plan {
4133 let mut slot = TensorDyn::image(
4134 32,
4135 32,
4136 PixelFormat::Rgba,
4137 DType::U8,
4138 Some(TensorMemory::Mem),
4139 edgefirst_tensor::CpuAccess::ReadWrite,
4140 )
4141 .unwrap();
4142 proc.tile_one(&src, &mut slot, p, &cfg).unwrap();
4143 proc.flush().unwrap();
4144 let band = parent.view(Region::new(0, p.index * 32, 32, 32)).unwrap();
4145 let slot_bytes = slot
4146 .as_typed::<u8>()
4147 .unwrap()
4148 .map()
4149 .unwrap()
4150 .as_slice()
4151 .to_vec();
4152 let band_bytes = band
4153 .as_typed::<u8>()
4154 .unwrap()
4155 .map()
4156 .unwrap()
4157 .as_slice()
4158 .to_vec();
4159 assert_eq!(
4160 slot_bytes, band_bytes,
4161 "tile {} stream != batch band",
4162 p.index
4163 );
4164 }
4165 }
4166
4167 #[test]
4174 fn tile_into_auto_dma_parity() {
4175 let mut proc = match ImageProcessor::new() {
4176 Ok(p) => p,
4177 Err(e) => {
4178 crate::test_support::report_skip(&format!(
4179 "{} — ImageProcessor init failed ({e:?})",
4180 function!()
4181 ));
4182 return;
4183 }
4184 };
4185 let (fw, fh) = (96usize, 64usize);
4186 let src = gradient_frame(fw, fh);
4187 let cfg = TilingConfig::new(32, 32).with_overlap(0.0);
4188 let n = tile_grid(fh, fw, 32, 32, 0.0).len();
4189
4190 let mut parent = match proc.alloc_tile_batch(
4191 n,
4192 &cfg,
4193 PixelFormat::Rgba,
4194 DType::U8,
4195 Some(TensorMemory::DmaBuf),
4196 edgefirst_tensor::CpuAccess::ReadWrite,
4197 ) {
4198 Ok(p) => p,
4199 Err(e) => {
4200 crate::test_support::report_skip(&format!(
4201 "{} — tall DMA parent alloc failed ({e:?})",
4202 function!()
4203 ));
4204 return;
4205 }
4206 };
4207 let placements = proc.tile_into(&src, &mut parent, &cfg).unwrap();
4208
4209 for p in &placements {
4210 let source = Region::new(
4211 p.origin.0 as usize,
4212 p.origin.1 as usize,
4213 p.crop_size.0 as usize,
4214 p.crop_size.1 as usize,
4215 );
4216 let mut solo = TensorDyn::image(
4217 32,
4218 32,
4219 PixelFormat::Rgba,
4220 DType::U8,
4221 Some(TensorMemory::DmaBuf),
4222 edgefirst_tensor::CpuAccess::ReadWrite,
4223 )
4224 .unwrap();
4225 proc.convert(
4226 &src,
4227 &mut solo,
4228 Rotation::None,
4229 Flip::None,
4230 Crop::default()
4231 .with_source(Some(source))
4232 .with_fit(Fit::Stretch),
4233 )
4234 .unwrap();
4235 let band = parent.view(Region::new(0, p.index * 32, 32, 32)).unwrap();
4236 compare_images(
4242 &band,
4243 &solo,
4244 0.98,
4245 &format!("{}_tile{}", function!(), p.index),
4246 );
4247 }
4248 }
4249
4250 #[test]
4252 fn tile_into_undersized_dst_errors() {
4253 let mut proc = match ImageProcessor::with_config(ImageProcessorConfig {
4254 backend: ComputeBackend::Cpu,
4255 ..Default::default()
4256 }) {
4257 Ok(p) => p,
4258 Err(e) => {
4259 crate::test_support::report_skip(&format!(
4260 "{} — CPU init failed ({e:?})",
4261 function!()
4262 ));
4263 return;
4264 }
4265 };
4266 let (fw, fh) = (96usize, 64usize);
4267 let src = gradient_frame(fw, fh);
4268 let cfg = TilingConfig::new(32, 32).with_overlap(0.0); let mut small = TensorDyn::image(
4271 32,
4272 2 * 32,
4273 PixelFormat::Rgba,
4274 DType::U8,
4275 Some(TensorMemory::Mem),
4276 edgefirst_tensor::CpuAccess::ReadWrite,
4277 )
4278 .unwrap();
4279 let r = proc.tile_into(&src, &mut small, &cfg);
4280 assert!(
4281 matches!(r, Err(Error::InvalidShape(_))),
4282 "expected InvalidShape, got {r:?}"
4283 );
4284 }
4285
4286 #[test]
4288 fn tiling_alloc_rejects_invalid_config() {
4289 let proc = match ImageProcessor::with_config(ImageProcessorConfig {
4290 backend: ComputeBackend::Cpu,
4291 ..Default::default()
4292 }) {
4293 Ok(p) => p,
4294 Err(e) => {
4295 crate::test_support::report_skip(&format!(
4296 "{} — CPU init failed ({e:?})",
4297 function!()
4298 ));
4299 return;
4300 }
4301 };
4302 let bad = TilingConfig::new(0, 640);
4303 assert!(proc.plan_tiles(1920, 1080, &bad).is_err());
4304 assert!(proc
4305 .alloc_tile_batch(
4306 4,
4307 &bad,
4308 PixelFormat::Rgba,
4309 DType::U8,
4310 Some(TensorMemory::Mem),
4311 edgefirst_tensor::CpuAccess::ReadWrite,
4312 )
4313 .is_err());
4314 }
4315
4316 #[test]
4317 fn plan_tiles_metadata_4k() {
4318 let proc = match ImageProcessor::with_config(ImageProcessorConfig {
4319 backend: ComputeBackend::Cpu,
4320 ..Default::default()
4321 }) {
4322 Ok(p) => p,
4323 Err(e) => {
4324 crate::test_support::report_skip(&format!(
4325 "{} — CPU init failed ({e:?})",
4326 function!()
4327 ));
4328 return;
4329 }
4330 };
4331 let cfg = TilingConfig::new(640, 640).with_overlap(0.2);
4332 let plan = proc.plan_tiles(3840, 2160, &cfg).unwrap();
4333 assert_eq!(plan.len(), 32);
4334 assert!(plan.iter().all(|p| p.count == 32));
4335 assert!(plan.iter().all(|p| p.crop_size == (640.0, 640.0)));
4336 assert!(plan.iter().all(|p| p.letterbox.is_none())); assert!(plan.iter().all(|p| p.frame_dims == (3840.0, 2160.0)));
4338 assert_eq!(plan[0].origin, (0.0, 0.0));
4339 }
4340
4341 #[test]
4342 fn test_invalid_crop() {
4343 let src = TensorDyn::image(
4344 100,
4345 100,
4346 PixelFormat::Rgb,
4347 DType::U8,
4348 None,
4349 edgefirst_tensor::CpuAccess::ReadWrite,
4350 )
4351 .unwrap();
4352 let dst = TensorDyn::image(
4353 100,
4354 100,
4355 PixelFormat::Rgb,
4356 DType::U8,
4357 None,
4358 edgefirst_tensor::CpuAccess::ReadWrite,
4359 )
4360 .unwrap();
4361
4362 let crop = Crop::new().with_source(Some(Region::new(50, 50, 60, 60)));
4364 assert!(matches!(
4365 crop.check_crop_dyn(&src, &dst),
4366 Err(Error::CropInvalid(_))
4367 ));
4368
4369 let crop = Crop::new().with_source(Some(Region::new(0, 0, 10, 10)));
4371 assert!(crop.check_crop_dyn(&src, &dst).is_ok());
4372
4373 assert!(Crop::letterbox([0, 0, 0, 255])
4375 .check_crop_dyn(&src, &dst)
4376 .is_ok());
4377 }
4378
4379 #[test]
4380 fn test_invalid_tensor_format() -> Result<(), Error> {
4381 let mut tensor = Tensor::<u8>::new(&[720, 1280, 4, 1], None, None)?;
4383 let result = tensor.set_format(PixelFormat::Rgb);
4384 assert!(result.is_err(), "4D tensor should reject set_format");
4385
4386 let mut tensor = Tensor::<u8>::new(&[720, 1280, 4], None, None)?;
4388 let result = tensor.set_format(PixelFormat::Rgb);
4389 assert!(result.is_err(), "4-channel tensor should reject RGB format");
4390
4391 Ok(())
4392 }
4393
4394 #[test]
4395 fn test_invalid_image_file() -> Result<(), Error> {
4396 let result = crate::load_image_test_helper(&[123; 5000], None, None);
4397 assert!(
4398 matches!(result, Err(Error::Codec(_))),
4399 "unrecognised bytes should surface as Error::Codec, got {result:?}"
4400 );
4401 Ok(())
4402 }
4403
4404 #[test]
4405 fn test_invalid_jpeg_format() -> Result<(), Error> {
4406 let result = crate::load_image_test_helper(&[123; 5000], Some(PixelFormat::Yuyv), None);
4407 assert!(
4410 matches!(result, Err(Error::Codec(_))),
4411 "Yuyv target with garbage bytes should surface as Error::Codec, got {result:?}"
4412 );
4413 Ok(())
4414 }
4415
4416 #[test]
4417 fn test_load_resize_save() {
4418 let file = edgefirst_bench::testdata::read("zidane.jpg");
4419 let img = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4420 assert_eq!(img.width(), Some(1280));
4421 assert_eq!(img.height(), Some(720));
4422
4423 let dst = TensorDyn::image(
4424 640,
4425 360,
4426 PixelFormat::Rgba,
4427 DType::U8,
4428 None,
4429 edgefirst_tensor::CpuAccess::ReadWrite,
4430 )
4431 .unwrap();
4432 let mut converter = CPUProcessor::new();
4433 let (result, _img, dst) = convert_img(
4434 &mut converter,
4435 img,
4436 dst,
4437 Rotation::None,
4438 Flip::None,
4439 Crop::no_crop(),
4440 );
4441 result.unwrap();
4442 assert_eq!(dst.width(), Some(640));
4443 assert_eq!(dst.height(), Some(360));
4444
4445 crate::save_jpeg(&dst, "zidane_resized.jpg", 80).unwrap();
4446
4447 let file = std::fs::read("zidane_resized.jpg").unwrap();
4448 let img = crate::load_image_test_helper(&file, None, None).unwrap();
4451 assert_eq!(img.width(), Some(640));
4452 assert_eq!(img.height(), Some(360));
4453 assert_eq!(img.format().unwrap(), PixelFormat::Nv12);
4454 }
4455
4456 #[test]
4457 fn test_from_tensor_planar() -> Result<(), Error> {
4458 let mut tensor = Tensor::new(&[3, 720, 1280], None, None)?;
4459 tensor
4460 .map()?
4461 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.8bps"));
4462 let planar = {
4463 tensor
4464 .set_format(PixelFormat::PlanarRgb)
4465 .map_err(|e| crate::Error::Internal(e.to_string()))?;
4466 TensorDyn::from(tensor)
4467 };
4468
4469 let rbga = load_bytes_to_tensor(
4470 1280,
4471 720,
4472 PixelFormat::Rgba,
4473 None,
4474 &edgefirst_bench::testdata::read("camera720p.rgba"),
4475 )?;
4476 compare_images_convert_to_rgb(&planar, &rbga, 0.98, function!());
4477
4478 Ok(())
4479 }
4480
4481 #[test]
4482 fn test_from_tensor_invalid_format() {
4483 assert!(PixelFormat::from_fourcc(u32::from_le_bytes(*b"TEST")).is_none());
4486 }
4487
4488 #[test]
4489 #[should_panic(expected = "Failed to save planar RGB image")]
4490 fn test_save_planar() {
4491 let planar_img = load_bytes_to_tensor(
4492 1280,
4493 720,
4494 PixelFormat::PlanarRgb,
4495 None,
4496 &edgefirst_bench::testdata::read("camera720p.8bps"),
4497 )
4498 .unwrap();
4499
4500 let save_path = "/tmp/planar_rgb.jpg";
4501 crate::save_jpeg(&planar_img, save_path, 90).expect("Failed to save planar RGB image");
4502 }
4503
4504 #[test]
4505 #[should_panic(expected = "Failed to save YUYV image")]
4506 fn test_save_yuyv() {
4507 let planar_img = load_bytes_to_tensor(
4508 1280,
4509 720,
4510 PixelFormat::Yuyv,
4511 None,
4512 &edgefirst_bench::testdata::read("camera720p.yuyv"),
4513 )
4514 .unwrap();
4515
4516 let save_path = "/tmp/yuyv.jpg";
4517 crate::save_jpeg(&planar_img, save_path, 90).expect("Failed to save YUYV image");
4518 }
4519
4520 #[test]
4521 fn test_rotation_angle() {
4522 assert_eq!(Rotation::from_degrees_clockwise(0), Rotation::None);
4523 assert_eq!(Rotation::from_degrees_clockwise(90), Rotation::Clockwise90);
4524 assert_eq!(Rotation::from_degrees_clockwise(180), Rotation::Rotate180);
4525 assert_eq!(
4526 Rotation::from_degrees_clockwise(270),
4527 Rotation::CounterClockwise90
4528 );
4529 assert_eq!(Rotation::from_degrees_clockwise(360), Rotation::None);
4530 assert_eq!(Rotation::from_degrees_clockwise(450), Rotation::Clockwise90);
4531 assert_eq!(Rotation::from_degrees_clockwise(540), Rotation::Rotate180);
4532 assert_eq!(
4533 Rotation::from_degrees_clockwise(630),
4534 Rotation::CounterClockwise90
4535 );
4536 }
4537
4538 #[test]
4539 #[should_panic(expected = "rotation angle is not a multiple of 90")]
4540 fn test_rotation_angle_panic() {
4541 Rotation::from_degrees_clockwise(361);
4542 }
4543
4544 #[test]
4545 fn test_disable_env_var() -> Result<(), Error> {
4546 let _lock = acquire_env_lock();
4549
4550 let _guard = EnvGuard::snapshot(&[
4553 "EDGEFIRST_FORCE_BACKEND",
4554 "EDGEFIRST_DISABLE_GL",
4555 "EDGEFIRST_DISABLE_G2D",
4556 "EDGEFIRST_DISABLE_CPU",
4557 ]);
4558
4559 unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") };
4562
4563 #[cfg(target_os = "linux")]
4564 {
4565 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
4566 let converter = ImageProcessor::new()?;
4567 assert!(converter.g2d.is_none());
4568 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_G2D") };
4569 }
4570
4571 #[cfg(any(target_os = "linux", target_os = "windows"))]
4572 #[cfg(feature = "opengl")]
4573 {
4574 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
4575 let converter = ImageProcessor::new()?;
4576 assert!(converter.opengl.is_none());
4577 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_GL") };
4578 }
4579
4580 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
4581 let converter = ImageProcessor::new()?;
4582 assert!(converter.cpu.is_none());
4583 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_CPU") };
4584
4585 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
4587 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
4588 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
4589 let mut converter = ImageProcessor::new()?;
4590
4591 let src = TensorDyn::image(
4592 1280,
4593 720,
4594 PixelFormat::Rgba,
4595 DType::U8,
4596 None,
4597 edgefirst_tensor::CpuAccess::ReadWrite,
4598 )?;
4599 let dst = TensorDyn::image(
4600 640,
4601 360,
4602 PixelFormat::Rgba,
4603 DType::U8,
4604 None,
4605 edgefirst_tensor::CpuAccess::ReadWrite,
4606 )?;
4607 let (result, _src, _dst) = convert_img(
4608 &mut converter,
4609 src,
4610 dst,
4611 Rotation::None,
4612 Flip::None,
4613 Crop::no_crop(),
4614 );
4615 assert!(matches!(result, Err(Error::NoConverter)));
4616 Ok(())
4618 }
4619
4620 #[test]
4621 fn test_unsupported_conversion() {
4622 let src = TensorDyn::image(
4623 1280,
4624 720,
4625 PixelFormat::Nv12,
4626 DType::U8,
4627 None,
4628 edgefirst_tensor::CpuAccess::ReadWrite,
4629 )
4630 .unwrap();
4631 let dst = TensorDyn::image(
4632 640,
4633 360,
4634 PixelFormat::Nv12,
4635 DType::U8,
4636 None,
4637 edgefirst_tensor::CpuAccess::ReadWrite,
4638 )
4639 .unwrap();
4640 let mut converter = ImageProcessor::new().unwrap();
4641 let (result, _src, _dst) = convert_img(
4642 &mut converter,
4643 src,
4644 dst,
4645 Rotation::None,
4646 Flip::None,
4647 Crop::no_crop(),
4648 );
4649 log::debug!("result: {:?}", result);
4650 assert!(matches!(
4651 result,
4652 Err(Error::NotSupported(e)) if e.starts_with("Conversion from NV12 to NV12")
4653 ));
4654 }
4655
4656 #[test]
4657 fn test_load_grey() {
4658 let grey_img = crate::load_image_test_helper(
4662 &edgefirst_bench::testdata::read("grey.jpg"),
4663 Some(PixelFormat::Rgba),
4664 None,
4665 )
4666 .unwrap();
4667 assert_eq!(grey_img.width(), Some(1024));
4668 assert_eq!(grey_img.height(), Some(681));
4669
4670 let grey_but_rgb = crate::load_image_test_helper(
4676 &edgefirst_bench::testdata::read("grey-rgb.jpg"),
4677 Some(PixelFormat::Rgba),
4678 None,
4679 )
4680 .expect("odd-height colour JPEG should decode to NV12 and convert to RGBA");
4681 assert_eq!(grey_but_rgb.width(), Some(1024));
4682 assert_eq!(grey_but_rgb.height(), Some(681));
4683 }
4684
4685 #[test]
4686 fn test_new_nv12() {
4687 let nv12 = TensorDyn::image(
4688 1280,
4689 720,
4690 PixelFormat::Nv12,
4691 DType::U8,
4692 None,
4693 edgefirst_tensor::CpuAccess::ReadWrite,
4694 )
4695 .unwrap();
4696 assert_eq!(nv12.height(), Some(720));
4697 assert_eq!(nv12.width(), Some(1280));
4698 assert_eq!(nv12.format().unwrap(), PixelFormat::Nv12);
4699 assert_eq!(nv12.format().unwrap().channels(), 1);
4701 assert!(nv12.format().is_some_and(
4702 |f| f.layout() == PixelLayout::Planar || f.layout() == PixelLayout::SemiPlanar
4703 ))
4704 }
4705
4706 #[test]
4707 #[cfg(any(target_os = "linux", target_os = "windows"))]
4708 fn test_new_image_converter() {
4709 let dst_width = 640;
4710 let dst_height = 360;
4711 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4712 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4713
4714 let mut converter = ImageProcessor::new().unwrap();
4715 let converter_dst = converter
4716 .create_image(
4717 dst_width,
4718 dst_height,
4719 PixelFormat::Rgba,
4720 DType::U8,
4721 None,
4722 edgefirst_tensor::CpuAccess::ReadWrite,
4723 )
4724 .unwrap();
4725 let (result, src, converter_dst) = convert_img(
4726 &mut converter,
4727 src,
4728 converter_dst,
4729 Rotation::None,
4730 Flip::None,
4731 Crop::no_crop(),
4732 );
4733 result.unwrap();
4734
4735 let cpu_dst = TensorDyn::image(
4736 dst_width,
4737 dst_height,
4738 PixelFormat::Rgba,
4739 DType::U8,
4740 None,
4741 edgefirst_tensor::CpuAccess::ReadWrite,
4742 )
4743 .unwrap();
4744 let mut cpu_converter = CPUProcessor::new();
4745 let (result, _src, cpu_dst) = convert_img(
4746 &mut cpu_converter,
4747 src,
4748 cpu_dst,
4749 Rotation::None,
4750 Flip::None,
4751 Crop::no_crop(),
4752 );
4753 result.unwrap();
4754
4755 compare_images(&converter_dst, &cpu_dst, 0.98, function!());
4756 }
4757
4758 #[test]
4759 #[cfg(any(target_os = "linux", target_os = "windows"))]
4760 fn test_create_image_dtype_i8() {
4761 let mut converter = ImageProcessor::new().unwrap();
4762
4763 let dst = converter
4765 .create_image(
4766 320,
4767 240,
4768 PixelFormat::Rgb,
4769 DType::I8,
4770 None,
4771 edgefirst_tensor::CpuAccess::ReadWrite,
4772 )
4773 .unwrap();
4774 assert_eq!(dst.dtype(), DType::I8);
4775 assert!(dst.width() == Some(320));
4776 assert!(dst.height() == Some(240));
4777 assert_eq!(dst.format(), Some(PixelFormat::Rgb));
4778
4779 let dst_u8 = converter
4781 .create_image(
4782 320,
4783 240,
4784 PixelFormat::Rgb,
4785 DType::U8,
4786 None,
4787 edgefirst_tensor::CpuAccess::ReadWrite,
4788 )
4789 .unwrap();
4790 assert_eq!(dst_u8.dtype(), DType::U8);
4791
4792 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4794 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4795 let mut dst_i8 = converter
4796 .create_image(
4797 320,
4798 240,
4799 PixelFormat::Rgb,
4800 DType::I8,
4801 None,
4802 edgefirst_tensor::CpuAccess::ReadWrite,
4803 )
4804 .unwrap();
4805 converter
4806 .convert(
4807 &src,
4808 &mut dst_i8,
4809 Rotation::None,
4810 Flip::None,
4811 Crop::no_crop(),
4812 )
4813 .unwrap();
4814 }
4815
4816 #[test]
4817 #[cfg(target_os = "linux")]
4818 fn test_create_image_nv12_dma_non_aligned_width() {
4819 let converter = ImageProcessor::new().unwrap();
4825
4826 let result = converter.create_image(
4828 100,
4829 64,
4830 PixelFormat::Nv12,
4831 DType::U8,
4832 Some(TensorMemory::DmaBuf),
4833 edgefirst_tensor::CpuAccess::ReadWrite,
4834 );
4835
4836 match result {
4837 Ok(img) => {
4838 assert_eq!(img.width(), Some(100));
4839 assert_eq!(img.height(), Some(64));
4840 assert_eq!(img.format(), Some(PixelFormat::Nv12));
4841 if let Some(stride) = img.row_stride() {
4842 assert!(
4843 stride >= 100,
4844 "NV12 row_stride {stride} must be >= the logical width (100)",
4845 );
4846 }
4847 }
4848 Err(e) => {
4849 crate::test_support::report_skip(&format!(
4851 "create_image NV12 DMA non-aligned width: {e}"
4852 ));
4853 }
4854 }
4855 }
4856
4857 #[test]
4858 #[ignore] fn test_crop_skip() {
4862 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4863 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4864
4865 let mut converter = ImageProcessor::new().unwrap();
4866 let converter_dst = converter
4867 .create_image(
4868 1280,
4869 720,
4870 PixelFormat::Rgba,
4871 DType::U8,
4872 None,
4873 edgefirst_tensor::CpuAccess::ReadWrite,
4874 )
4875 .unwrap();
4876 let crop = Crop::new().with_source(Some(Region::new(0, 0, 640, 640)));
4877 let (result, src, converter_dst) = convert_img(
4878 &mut converter,
4879 src,
4880 converter_dst,
4881 Rotation::None,
4882 Flip::None,
4883 crop,
4884 );
4885 result.unwrap();
4886
4887 let cpu_dst = TensorDyn::image(
4888 1280,
4889 720,
4890 PixelFormat::Rgba,
4891 DType::U8,
4892 None,
4893 edgefirst_tensor::CpuAccess::ReadWrite,
4894 )
4895 .unwrap();
4896 let mut cpu_converter = CPUProcessor::new();
4897 let (result, _src, cpu_dst) = convert_img(
4898 &mut cpu_converter,
4899 src,
4900 cpu_dst,
4901 Rotation::None,
4902 Flip::None,
4903 crop,
4904 );
4905 result.unwrap();
4906
4907 compare_images(&converter_dst, &cpu_dst, 0.99999, function!());
4908 }
4909
4910 #[test]
4911 fn test_invalid_pixel_format() {
4912 assert!(PixelFormat::from_fourcc(u32::from_le_bytes(*b"TEST")).is_none());
4915 }
4916
4917 #[cfg(target_os = "linux")]
4919 static G2D_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4920
4921 #[cfg(target_os = "linux")]
4922 fn is_g2d_available() -> bool {
4923 *G2D_AVAILABLE.get_or_init(|| G2DProcessor::new().is_ok())
4924 }
4925
4926 #[cfg(any(
4932 target_os = "linux",
4933 target_os = "macos",
4934 target_os = "ios",
4935 target_os = "android",
4936 target_os = "windows"
4937 ))]
4938 #[cfg(feature = "opengl")]
4939 static GL_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4940
4941 #[cfg(any(
4942 target_os = "linux",
4943 target_os = "macos",
4944 target_os = "ios",
4945 target_os = "android",
4946 target_os = "windows"
4947 ))]
4948 #[cfg(feature = "opengl")]
4949 fn is_opengl_available() -> bool {
4951 *GL_AVAILABLE.get_or_init(|| GLProcessorThreaded::new(None).is_ok())
4952 }
4953
4954 #[test]
4966 #[cfg(feature = "opengl")]
4967 fn gl_backend_available_canary() {
4968 let require_gl = std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1");
4969 if !require_gl {
4970 crate::test_support::report_skip(&format!(
4971 "{} — HAL_TEST_REQUIRE_GL is not set to 1",
4972 function!()
4973 ));
4974 return;
4975 }
4976 #[cfg(target_os = "macos")]
4977 if std::env::var_os("HAL_TEST_ALLOW_DLOPEN_ANGLE").is_none() {
4978 crate::test_support::report_skip(&format!(
4979 "{} — ANGLE dlopen gate closed (coverage pass 1)",
4980 function!()
4981 ));
4982 return;
4983 }
4984 GLProcessorThreaded::new(None).expect(
4985 "HAL_TEST_REQUIRE_GL=1 but the GL backend failed to initialize — \
4986 check the ANGLE install/re-sign step and binary entitlements \
4987 (macOS) or the EGL stack (Linux)",
4988 );
4989 }
4990
4991 #[test]
4992 fn test_load_jpeg_with_exif() {
4993 use edgefirst_codec::peek_info;
4994
4995 let file = edgefirst_bench::testdata::read("zidane_rotated_exif.jpg").to_vec();
5000 let info = peek_info(&file).unwrap();
5001 assert_eq!(info.rotation_degrees, 90);
5002 assert!(!info.flip_horizontal);
5003
5004 let loaded = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5005 assert_eq!(loaded.width(), Some(1280));
5007 assert_eq!(loaded.height(), Some(720));
5008
5009 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5012 let cpu_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5013
5014 let rotation = Rotation::from_degrees_clockwise(info.rotation_degrees as usize);
5015 let (dst_width, dst_height) = (cpu_src.height().unwrap(), cpu_src.width().unwrap());
5016
5017 let cpu_dst = TensorDyn::image(
5018 dst_width,
5019 dst_height,
5020 PixelFormat::Rgba,
5021 DType::U8,
5022 None,
5023 edgefirst_tensor::CpuAccess::ReadWrite,
5024 )
5025 .unwrap();
5026 let mut cpu_converter = CPUProcessor::new();
5027
5028 let loaded_rotated = TensorDyn::image(
5031 dst_width,
5032 dst_height,
5033 PixelFormat::Rgba,
5034 DType::U8,
5035 None,
5036 edgefirst_tensor::CpuAccess::ReadWrite,
5037 )
5038 .unwrap();
5039 let (r0, _loaded, loaded_rotated) = convert_img(
5040 &mut cpu_converter,
5041 loaded,
5042 loaded_rotated,
5043 rotation,
5044 Flip::None,
5045 Crop::no_crop(),
5046 );
5047 r0.unwrap();
5048
5049 let (result, _cpu_src, cpu_dst) = convert_img(
5050 &mut cpu_converter,
5051 cpu_src,
5052 cpu_dst,
5053 rotation,
5054 Flip::None,
5055 Crop::no_crop(),
5056 );
5057 result.unwrap();
5058
5059 compare_images(&loaded_rotated, &cpu_dst, 0.98, function!());
5060 }
5061
5062 #[test]
5063 fn test_load_png_with_exif() {
5064 use edgefirst_codec::peek_info;
5065
5066 let file = edgefirst_bench::testdata::read("zidane_rotated_exif_180.png").to_vec();
5069 let info = peek_info(&file).unwrap();
5070 assert_eq!(info.rotation_degrees, 180);
5071 assert!(!info.flip_horizontal);
5072
5073 let loaded = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5074 assert_eq!(loaded.height(), Some(720));
5076 assert_eq!(loaded.width(), Some(1280));
5077
5078 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5083 let cpu_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5084
5085 let rotation = Rotation::from_degrees_clockwise(info.rotation_degrees as usize);
5086 let cpu_dst = TensorDyn::image(
5087 1280,
5088 720,
5089 PixelFormat::Rgba,
5090 DType::U8,
5091 None,
5092 edgefirst_tensor::CpuAccess::ReadWrite,
5093 )
5094 .unwrap();
5095 let mut cpu_converter = CPUProcessor::new();
5096
5097 let (result, _cpu_src, cpu_dst) = convert_img(
5098 &mut cpu_converter,
5099 cpu_src,
5100 cpu_dst,
5101 rotation,
5102 Flip::None,
5103 Crop::no_crop(),
5104 );
5105 result.unwrap();
5106
5107 let loaded_rotated = TensorDyn::image(
5110 1280,
5111 720,
5112 PixelFormat::Rgba,
5113 DType::U8,
5114 None,
5115 edgefirst_tensor::CpuAccess::ReadWrite,
5116 )
5117 .unwrap();
5118 let (r0, _loaded, loaded_rotated) = convert_img(
5119 &mut cpu_converter,
5120 loaded,
5121 loaded_rotated,
5122 rotation,
5123 Flip::None,
5124 Crop::no_crop(),
5125 );
5126 r0.unwrap();
5127
5128 compare_images(&loaded_rotated, &cpu_dst, 0.95, function!());
5133 }
5134
5135 #[cfg(target_os = "linux")]
5141 fn make_rgb_jpeg(width: u32, height: u32) -> Vec<u8> {
5142 let mut bytes = Vec::with_capacity((width * height * 3) as usize);
5143 for y in 0..height {
5144 for x in 0..width {
5145 bytes.push(((x + y) & 0xFF) as u8);
5146 bytes.push(((x.wrapping_mul(3)) & 0xFF) as u8);
5147 bytes.push(((y.wrapping_mul(5)) & 0xFF) as u8);
5148 }
5149 }
5150 let mut out = Vec::new();
5151 let encoder = jpeg_encoder::Encoder::new(&mut out, 85);
5152 encoder
5153 .encode(
5154 &bytes,
5155 width as u16,
5156 height as u16,
5157 jpeg_encoder::ColorType::Rgb,
5158 )
5159 .expect("jpeg-encoder must succeed on trivial input");
5160 out
5161 }
5162
5163 #[test]
5172 #[cfg(target_os = "linux")]
5173 #[cfg(feature = "opengl")]
5174 fn test_convert_rgba_non_4_aligned_width_end_to_end() {
5175 use edgefirst_tensor::is_dma_available;
5176 if !is_dma_available() {
5177 crate::test_support::report_skip(
5178 "test_convert_rgba_non_4_aligned_width_end_to_end — DMA not available",
5179 );
5180 return;
5181 }
5182 let jpeg = make_rgb_jpeg(375, 333);
5186 let src_gl = crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), None).unwrap();
5187 assert_eq!(src_gl.width(), Some(375));
5188 let stride = src_gl.row_stride().unwrap();
5190 assert_eq!(stride, 1536, "expected padded pitch 1536, got {stride}");
5191
5192 let mut gl_proc = ImageProcessor::new().unwrap();
5194 let gl_dst = gl_proc
5195 .create_image(
5196 640,
5197 640,
5198 PixelFormat::Rgba,
5199 DType::U8,
5200 None,
5201 edgefirst_tensor::CpuAccess::ReadWrite,
5202 )
5203 .unwrap();
5204 let (r_gl, _src_gl, gl_dst) = convert_img(
5205 &mut gl_proc,
5206 src_gl,
5207 gl_dst,
5208 Rotation::None,
5209 Flip::None,
5210 Crop::no_crop(),
5211 );
5212 r_gl.expect("GL-backed convert must succeed for 375x333 Rgba src");
5213
5214 let src_cpu =
5219 crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), Some(TensorMemory::Mem))
5220 .unwrap();
5221 let mut cpu_proc = ImageProcessor::with_config(ImageProcessorConfig {
5222 backend: ComputeBackend::Cpu,
5223 ..Default::default()
5224 })
5225 .unwrap();
5226 let cpu_dst = TensorDyn::image(
5227 640,
5228 640,
5229 PixelFormat::Rgba,
5230 DType::U8,
5231 Some(TensorMemory::Mem),
5232 edgefirst_tensor::CpuAccess::ReadWrite,
5233 )
5234 .unwrap();
5235 let (r_cpu, _src_cpu, cpu_dst) = convert_img(
5236 &mut cpu_proc,
5237 src_cpu,
5238 cpu_dst,
5239 Rotation::None,
5240 Flip::None,
5241 Crop::no_crop(),
5242 );
5243 r_cpu.unwrap();
5244
5245 compare_images(&gl_dst, &cpu_dst, 0.95, function!());
5249 }
5250
5251 #[test]
5258 #[cfg(target_os = "linux")]
5259 fn test_load_jpeg_rgba_non_aligned_pitch_padded_dma() {
5260 use edgefirst_tensor::is_dma_available;
5261 if !is_dma_available() {
5262 crate::test_support::report_skip(
5263 "test_load_jpeg_rgba_non_aligned_pitch_padded_dma — DMA not available",
5264 );
5265 return;
5266 }
5267 for &w in &[500u32, 612, 428] {
5271 let jpeg = make_rgb_jpeg(w, 333);
5272 let loaded =
5273 crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), None).unwrap();
5274 let natural = (w as usize) * 4;
5275 let aligned = crate::align_pitch_bytes_to_gpu_alignment(natural).unwrap();
5276 assert!(
5277 aligned > natural,
5278 "test sanity: width {w} should be unaligned"
5279 );
5280 let stride = loaded
5281 .row_stride()
5282 .expect("padded DMA path must set an explicit row_stride — regression if None");
5283 assert_eq!(
5284 stride, aligned,
5285 "width {w}: expected padded stride {aligned}, got {stride} \
5286 (regression: pitch-padding branch skipped?)"
5287 );
5288 let eff = loaded.effective_row_stride().unwrap();
5289 assert_eq!(
5290 eff, aligned,
5291 "effective_row_stride must match stored stride"
5292 );
5293 assert_eq!(loaded.width(), Some(w as usize));
5294 assert_eq!(loaded.height(), Some(333));
5295 }
5296 }
5297
5298 #[test]
5307 #[cfg(target_os = "linux")]
5308 fn test_padded_dma_pitch_for_respects_memory_choice() {
5309 use edgefirst_tensor::{is_dma_available, TensorMemory};
5310
5311 let unaligned_w = 500;
5314
5315 assert_eq!(
5317 crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &Some(TensorMemory::Mem),),
5318 None,
5319 "Mem must never trigger DMA padding"
5320 );
5321 assert_eq!(
5322 crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &Some(TensorMemory::Shm),),
5323 None,
5324 "Shm must never trigger DMA padding"
5325 );
5326
5327 assert_eq!(
5332 crate::padded_dma_pitch_for(
5333 PixelFormat::Rgba,
5334 unaligned_w,
5335 &Some(TensorMemory::DmaBuf),
5336 ),
5337 Some(2048),
5338 "explicit Dma must pad regardless of runtime DMA availability"
5339 );
5340
5341 let none_result = crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &None);
5345 if is_dma_available() {
5346 assert_eq!(
5347 none_result,
5348 Some(2048),
5349 "memory=None + DMA available → pad (will route through DMA)"
5350 );
5351 } else {
5352 assert_eq!(
5353 none_result, None,
5354 "memory=None + DMA unavailable → must NOT pad (would force \
5355 image_with_stride into a DMA-only allocation that fails). \
5356 Regression: padded_dma_pitch_for ignored is_dma_available()."
5357 );
5358 }
5359 }
5360
5361 fn make_grey_png(width: u32, height: u32) -> Vec<u8> {
5365 let mut bytes = Vec::with_capacity((width * height) as usize);
5366 for y in 0..height {
5367 for x in 0..width {
5368 bytes.push(((x + y) & 0xFF) as u8);
5369 }
5370 }
5371 let img = image::GrayImage::from_vec(width, height, bytes).unwrap();
5372 let mut buf = Vec::new();
5373 img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)
5374 .unwrap();
5375 buf
5376 }
5377
5378 #[test]
5383 #[cfg(target_os = "linux")]
5384 fn test_load_png_grey_misaligned_width_dma() {
5385 use edgefirst_tensor::is_dma_available;
5386 if !is_dma_available() {
5387 crate::test_support::report_skip(
5388 "test_load_png_grey_misaligned_width_dma — DMA not available",
5389 );
5390 return;
5391 }
5392 let png = make_grey_png(612, 388);
5393 let loaded = crate::load_image_test_helper(&png, Some(PixelFormat::Grey), None).unwrap();
5394 assert_eq!(loaded.width(), Some(612));
5395 assert_eq!(loaded.height(), Some(388));
5396 assert_eq!(loaded.format(), Some(PixelFormat::Grey));
5397
5398 let map = loaded.as_typed::<u8>().unwrap().map().unwrap();
5401 let stride = loaded.row_stride().unwrap_or(612);
5402 assert!(stride >= 612);
5403 let bytes: &[u8] = ↦
5404 for y in 0..388usize {
5405 for x in 0..612usize {
5406 let expected = ((x + y) & 0xFF) as u8;
5407 let got = bytes[y * stride + x];
5408 assert_eq!(
5409 got, expected,
5410 "grey png mismatch at ({x},{y}): got {got} expected {expected}"
5411 );
5412 }
5413 }
5414 }
5415
5416 #[test]
5420 fn test_load_png_grey_mem() {
5421 use edgefirst_tensor::TensorMemory;
5422 let png = make_grey_png(612, 100);
5423 let loaded =
5424 crate::load_image_test_helper(&png, Some(PixelFormat::Grey), Some(TensorMemory::Mem))
5425 .unwrap();
5426 assert_eq!(loaded.width(), Some(612));
5427 assert_eq!(loaded.height(), Some(100));
5428 assert_eq!(loaded.format(), Some(PixelFormat::Grey));
5429 let map = loaded.as_typed::<u8>().unwrap().map().unwrap();
5430 let bytes: &[u8] = ↦
5431 assert_eq!(bytes.len(), 612 * 100);
5433 for y in 0..100 {
5434 for x in 0..612 {
5435 assert_eq!(bytes[y * 612 + x], ((x + y) & 0xFF) as u8);
5436 }
5437 }
5438 }
5439
5440 #[test]
5444 fn test_load_png_grey_to_rgb_mem() {
5445 use edgefirst_tensor::TensorMemory;
5446 let png = make_grey_png(620, 240);
5447 let loaded =
5448 crate::load_image_test_helper(&png, Some(PixelFormat::Rgb), Some(TensorMemory::Mem))
5449 .unwrap();
5450 assert_eq!(loaded.width(), Some(620));
5451 assert_eq!(loaded.height(), Some(240));
5452 assert_eq!(loaded.format(), Some(PixelFormat::Rgb));
5453
5454 let map = loaded.as_typed::<u8>().unwrap().map().unwrap();
5456 let bytes: &[u8] = ↦
5457 for (x, y) in [(0usize, 0usize), (100, 50), (619, 239)] {
5458 let expected = ((x + y) & 0xFF) as u8;
5459 let off = (y * 620 + x) * 3;
5460 assert_eq!(bytes[off], expected, "R@{x},{y}");
5461 assert_eq!(bytes[off + 1], expected, "G@{x},{y}");
5462 assert_eq!(bytes[off + 2], expected, "B@{x},{y}");
5463 }
5464 }
5465
5466 #[test]
5467 #[cfg(target_os = "linux")]
5468 fn test_g2d_resize() {
5469 if !is_g2d_available() {
5470 crate::test_support::report_skip(
5471 "test_g2d_resize - G2D library (libg2d.so.2) not available",
5472 );
5473 return;
5474 }
5475 if !is_dma_available() {
5476 crate::test_support::report_skip("test_g2d_resize - DMA memory allocation not available (permission denied or no DMA-BUF support)");
5477 return;
5478 }
5479
5480 let dst_width = 640;
5481 let dst_height = 360;
5482 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5483 let src = crate::load_image_test_helper(
5484 &file,
5485 Some(PixelFormat::Rgba),
5486 Some(TensorMemory::DmaBuf),
5487 )
5488 .unwrap();
5489
5490 let g2d_dst = TensorDyn::image(
5491 dst_width,
5492 dst_height,
5493 PixelFormat::Rgba,
5494 DType::U8,
5495 Some(TensorMemory::DmaBuf),
5496 edgefirst_tensor::CpuAccess::ReadWrite,
5497 )
5498 .unwrap();
5499 let mut g2d_converter = G2DProcessor::new().unwrap();
5500 let (result, src, g2d_dst) = convert_img(
5501 &mut g2d_converter,
5502 src,
5503 g2d_dst,
5504 Rotation::None,
5505 Flip::None,
5506 Crop::no_crop(),
5507 );
5508 result.unwrap();
5509
5510 let cpu_dst = TensorDyn::image(
5511 dst_width,
5512 dst_height,
5513 PixelFormat::Rgba,
5514 DType::U8,
5515 None,
5516 edgefirst_tensor::CpuAccess::ReadWrite,
5517 )
5518 .unwrap();
5519 let mut cpu_converter = CPUProcessor::new();
5520 let (result, _src, cpu_dst) = convert_img(
5521 &mut cpu_converter,
5522 src,
5523 cpu_dst,
5524 Rotation::None,
5525 Flip::None,
5526 Crop::no_crop(),
5527 );
5528 result.unwrap();
5529
5530 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
5536 }
5537
5538 #[test]
5539 #[cfg(any(
5540 target_os = "linux",
5541 target_os = "macos",
5542 target_os = "ios",
5543 target_os = "android",
5544 target_os = "windows"
5545 ))]
5546 #[cfg(feature = "opengl")]
5547 fn test_opengl_resize() {
5548 if !is_opengl_available() {
5549 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
5550 return;
5551 }
5552
5553 let dst_width = 640;
5554 let dst_height = 360;
5555 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5556 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5557
5558 let cpu_dst = TensorDyn::image(
5559 dst_width,
5560 dst_height,
5561 PixelFormat::Rgba,
5562 DType::U8,
5563 None,
5564 edgefirst_tensor::CpuAccess::ReadWrite,
5565 )
5566 .unwrap();
5567 let mut cpu_converter = CPUProcessor::new();
5568 let (result, src, cpu_dst) = convert_img(
5569 &mut cpu_converter,
5570 src,
5571 cpu_dst,
5572 Rotation::None,
5573 Flip::None,
5574 Crop::no_crop(),
5575 );
5576 result.unwrap();
5577
5578 let mut src = src;
5579 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5580
5581 for _ in 0..5 {
5582 let gl_dst = TensorDyn::image(
5583 dst_width,
5584 dst_height,
5585 PixelFormat::Rgba,
5586 DType::U8,
5587 None,
5588 edgefirst_tensor::CpuAccess::ReadWrite,
5589 )
5590 .unwrap();
5591 let (result, src_back, gl_dst) = convert_img(
5592 &mut gl_converter,
5593 src,
5594 gl_dst,
5595 Rotation::None,
5596 Flip::None,
5597 Crop::no_crop(),
5598 );
5599 result.unwrap();
5600 src = src_back;
5601
5602 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5603 }
5604 }
5605
5606 #[test]
5607 #[cfg(any(
5608 target_os = "linux",
5609 target_os = "macos",
5610 target_os = "ios",
5611 target_os = "android",
5612 target_os = "windows"
5613 ))]
5614 #[cfg(feature = "opengl")]
5615 fn test_opengl_10_threads() {
5616 if !is_opengl_available() {
5617 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
5618 return;
5619 }
5620
5621 let handles: Vec<_> = (0..10)
5622 .map(|i| {
5623 std::thread::Builder::new()
5624 .name(format!("Thread {i}"))
5625 .spawn(test_opengl_resize)
5626 .unwrap()
5627 })
5628 .collect();
5629 handles.into_iter().for_each(|h| {
5630 if let Err(e) = h.join() {
5631 std::panic::resume_unwind(e)
5632 }
5633 });
5634 }
5635
5636 #[test]
5637 #[cfg(any(
5638 target_os = "linux",
5639 target_os = "macos",
5640 target_os = "ios",
5641 target_os = "android",
5642 target_os = "windows"
5643 ))]
5644 #[cfg(feature = "opengl")]
5645 fn test_opengl_grey() {
5646 if !is_opengl_available() {
5647 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
5648 return;
5649 }
5650
5651 let img = crate::load_image_test_helper(
5652 &edgefirst_bench::testdata::read("grey.jpg"),
5653 Some(PixelFormat::Grey),
5654 None,
5655 )
5656 .unwrap();
5657
5658 let gl_dst = TensorDyn::image(
5659 640,
5660 640,
5661 PixelFormat::Grey,
5662 DType::U8,
5663 None,
5664 edgefirst_tensor::CpuAccess::ReadWrite,
5665 )
5666 .unwrap();
5667 let cpu_dst = TensorDyn::image(
5668 640,
5669 640,
5670 PixelFormat::Grey,
5671 DType::U8,
5672 None,
5673 edgefirst_tensor::CpuAccess::ReadWrite,
5674 )
5675 .unwrap();
5676
5677 let mut converter = CPUProcessor::new();
5678
5679 let (result, img, cpu_dst) = convert_img(
5680 &mut converter,
5681 img,
5682 cpu_dst,
5683 Rotation::None,
5684 Flip::None,
5685 Crop::no_crop(),
5686 );
5687 result.unwrap();
5688
5689 let mut gl = GLProcessorThreaded::new(None).unwrap();
5690 let (result, _img, gl_dst) = convert_img(
5691 &mut gl,
5692 img,
5693 gl_dst,
5694 Rotation::None,
5695 Flip::None,
5696 Crop::no_crop(),
5697 );
5698 result.unwrap();
5699
5700 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5701 }
5702
5703 #[test]
5704 #[cfg(target_os = "linux")]
5705 fn test_g2d_src_crop() {
5706 if !is_g2d_available() {
5707 crate::test_support::report_skip(
5708 "test_g2d_src_crop - G2D library (libg2d.so.2) not available",
5709 );
5710 return;
5711 }
5712 if !is_dma_available() {
5713 crate::test_support::report_skip("test_g2d_src_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)");
5714 return;
5715 }
5716
5717 let dst_width = 640;
5718 let dst_height = 640;
5719 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5720 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5721
5722 let cpu_dst = TensorDyn::image(
5723 dst_width,
5724 dst_height,
5725 PixelFormat::Rgba,
5726 DType::U8,
5727 None,
5728 edgefirst_tensor::CpuAccess::ReadWrite,
5729 )
5730 .unwrap();
5731 let mut cpu_converter = CPUProcessor::new();
5732 let crop = Crop::new().with_source(Some(Region::new(0, 0, 640, 360)));
5733 let (result, src, cpu_dst) = convert_img(
5734 &mut cpu_converter,
5735 src,
5736 cpu_dst,
5737 Rotation::None,
5738 Flip::None,
5739 crop,
5740 );
5741 result.unwrap();
5742
5743 let 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 let (result, _src, g2d_dst) = convert_img(
5754 &mut g2d_converter,
5755 src,
5756 g2d_dst,
5757 Rotation::None,
5758 Flip::None,
5759 crop,
5760 );
5761 result.unwrap();
5762
5763 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
5769 }
5770
5771 #[test]
5772 #[cfg(target_os = "linux")]
5773 fn test_g2d_dst_crop() {
5774 if !is_g2d_available() {
5775 crate::test_support::report_skip(
5776 "test_g2d_dst_crop - G2D library (libg2d.so.2) not available",
5777 );
5778 return;
5779 }
5780 if !is_dma_available() {
5781 crate::test_support::report_skip("test_g2d_dst_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)");
5782 return;
5783 }
5784
5785 let dst_width = 640;
5786 let dst_height = 640;
5787 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5788 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5789
5790 let cpu_dst = TensorDyn::image(
5791 dst_width,
5792 dst_height,
5793 PixelFormat::Rgba,
5794 DType::U8,
5795 None,
5796 edgefirst_tensor::CpuAccess::ReadWrite,
5797 )
5798 .unwrap();
5799 let mut cpu_converter = CPUProcessor::new();
5800 let crop = Crop::new();
5801 let (result, src, cpu_dst) = convert_img(
5802 &mut cpu_converter,
5803 src,
5804 cpu_dst,
5805 Rotation::None,
5806 Flip::None,
5807 crop,
5808 );
5809 result.unwrap();
5810
5811 let g2d_dst = TensorDyn::image(
5812 dst_width,
5813 dst_height,
5814 PixelFormat::Rgba,
5815 DType::U8,
5816 None,
5817 edgefirst_tensor::CpuAccess::ReadWrite,
5818 )
5819 .unwrap();
5820 let mut g2d_converter = G2DProcessor::new().unwrap();
5821 let (result, _src, g2d_dst) = convert_img(
5822 &mut g2d_converter,
5823 src,
5824 g2d_dst,
5825 Rotation::None,
5826 Flip::None,
5827 crop,
5828 );
5829 result.unwrap();
5830
5831 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
5837 }
5838
5839 #[test]
5840 #[cfg(target_os = "linux")]
5841 fn test_g2d_all_rgba() {
5842 if !is_g2d_available() {
5843 crate::test_support::report_skip(
5844 "test_g2d_all_rgba - G2D library (libg2d.so.2) not available",
5845 );
5846 return;
5847 }
5848 if !is_dma_available() {
5849 crate::test_support::report_skip("test_g2d_all_rgba - DMA memory allocation not available (permission denied or no DMA-BUF support)");
5850 return;
5851 }
5852
5853 let dst_width = 640;
5854 let dst_height = 640;
5855 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5856 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5857 let src_dyn = src;
5858
5859 let mut cpu_dst = TensorDyn::image(
5860 dst_width,
5861 dst_height,
5862 PixelFormat::Rgba,
5863 DType::U8,
5864 None,
5865 edgefirst_tensor::CpuAccess::ReadWrite,
5866 )
5867 .unwrap();
5868 let mut cpu_converter = CPUProcessor::new();
5869 let mut g2d_dst = TensorDyn::image(
5870 dst_width,
5871 dst_height,
5872 PixelFormat::Rgba,
5873 DType::U8,
5874 None,
5875 edgefirst_tensor::CpuAccess::ReadWrite,
5876 )
5877 .unwrap();
5878 let mut g2d_converter = G2DProcessor::new().unwrap();
5879
5880 let crop = Crop::new().with_source(Some(Region::new(50, 120, 1024, 576)));
5881
5882 for rot in [
5883 Rotation::None,
5884 Rotation::Clockwise90,
5885 Rotation::Rotate180,
5886 Rotation::CounterClockwise90,
5887 ] {
5888 cpu_dst
5889 .as_typed::<u8>()
5890 .unwrap()
5891 .map()
5892 .unwrap()
5893 .as_mut_slice()
5894 .fill(114);
5895 g2d_dst
5896 .as_typed::<u8>()
5897 .unwrap()
5898 .map()
5899 .unwrap()
5900 .as_mut_slice()
5901 .fill(114);
5902 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
5903 let mut cpu_dst_dyn = cpu_dst;
5904 cpu_converter
5905 .convert(&src_dyn, &mut cpu_dst_dyn, Rotation::None, Flip::None, crop)
5906 .unwrap();
5907 cpu_dst = {
5908 let mut __t = cpu_dst_dyn.into_u8().unwrap();
5909 __t.set_format(PixelFormat::Rgba).unwrap();
5910 TensorDyn::from(__t)
5911 };
5912
5913 let mut g2d_dst_dyn = g2d_dst;
5914 g2d_converter
5915 .convert(&src_dyn, &mut g2d_dst_dyn, Rotation::None, Flip::None, crop)
5916 .unwrap();
5917 g2d_dst = {
5918 let mut __t = g2d_dst_dyn.into_u8().unwrap();
5919 __t.set_format(PixelFormat::Rgba).unwrap();
5920 TensorDyn::from(__t)
5921 };
5922
5923 compare_images(
5924 &g2d_dst,
5925 &cpu_dst,
5926 0.98,
5927 &format!("{} {:?} {:?}", function!(), rot, flip),
5928 );
5929 }
5930 }
5931 }
5932
5933 #[test]
5934 #[cfg(any(
5935 target_os = "linux",
5936 target_os = "macos",
5937 target_os = "ios",
5938 target_os = "android",
5939 target_os = "windows"
5940 ))]
5941 #[cfg(feature = "opengl")]
5942 fn test_opengl_src_crop() {
5943 if !is_opengl_available() {
5944 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
5945 return;
5946 }
5947
5948 let dst_width = 640;
5949 let dst_height = 360;
5950 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5951 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5952 let crop = Crop::new().with_source(Some(Region::new(320, 180, 1280 - 320, 720 - 180)));
5953
5954 let cpu_dst = TensorDyn::image(
5955 dst_width,
5956 dst_height,
5957 PixelFormat::Rgba,
5958 DType::U8,
5959 None,
5960 edgefirst_tensor::CpuAccess::ReadWrite,
5961 )
5962 .unwrap();
5963 let mut cpu_converter = CPUProcessor::new();
5964 let (result, src, cpu_dst) = convert_img(
5965 &mut cpu_converter,
5966 src,
5967 cpu_dst,
5968 Rotation::None,
5969 Flip::None,
5970 crop,
5971 );
5972 result.unwrap();
5973
5974 let gl_dst = TensorDyn::image(
5975 dst_width,
5976 dst_height,
5977 PixelFormat::Rgba,
5978 DType::U8,
5979 None,
5980 edgefirst_tensor::CpuAccess::ReadWrite,
5981 )
5982 .unwrap();
5983 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5984 let (result, _src, gl_dst) = convert_img(
5985 &mut gl_converter,
5986 src,
5987 gl_dst,
5988 Rotation::None,
5989 Flip::None,
5990 crop,
5991 );
5992 result.unwrap();
5993
5994 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5995 }
5996
5997 #[test]
5998 #[cfg(any(
5999 target_os = "linux",
6000 target_os = "macos",
6001 target_os = "ios",
6002 target_os = "android",
6003 target_os = "windows"
6004 ))]
6005 #[cfg(feature = "opengl")]
6006 fn test_opengl_dst_crop() {
6007 if !is_opengl_available() {
6008 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
6009 return;
6010 }
6011
6012 let dst_width = 640;
6013 let dst_height = 640;
6014 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
6015 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
6016
6017 let cpu_dst = TensorDyn::image(
6018 dst_width,
6019 dst_height,
6020 PixelFormat::Rgba,
6021 DType::U8,
6022 None,
6023 edgefirst_tensor::CpuAccess::ReadWrite,
6024 )
6025 .unwrap();
6026 let mut cpu_converter = CPUProcessor::new();
6027 let crop = Crop::new();
6028 let (result, src, cpu_dst) = convert_img(
6029 &mut cpu_converter,
6030 src,
6031 cpu_dst,
6032 Rotation::None,
6033 Flip::None,
6034 crop,
6035 );
6036 result.unwrap();
6037
6038 let gl_dst = TensorDyn::image(
6039 dst_width,
6040 dst_height,
6041 PixelFormat::Rgba,
6042 DType::U8,
6043 None,
6044 edgefirst_tensor::CpuAccess::ReadWrite,
6045 )
6046 .unwrap();
6047 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
6048 let (result, _src, gl_dst) = convert_img(
6049 &mut gl_converter,
6050 src,
6051 gl_dst,
6052 Rotation::None,
6053 Flip::None,
6054 crop,
6055 );
6056 result.unwrap();
6057
6058 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
6059 }
6060 #[cfg(all(target_os = "windows", feature = "opengl"))]
6064 fn synthetic_rgba(w: usize, h: usize) -> TensorDyn {
6065 let t = TensorDyn::image(
6066 w,
6067 h,
6068 PixelFormat::Rgba,
6069 DType::U8,
6070 Some(TensorMemory::Mem),
6071 edgefirst_tensor::CpuAccess::ReadWrite,
6072 )
6073 .unwrap();
6074 fill_synthetic_rgba(&t);
6075 t
6076 }
6077
6078 #[cfg(all(target_os = "windows", feature = "opengl"))]
6083 fn assert_pixels_identical(actual: &TensorDyn, expected: &TensorDyn, name: &str) {
6084 assert_eq!(actual.shape(), expected.shape(), "{name}: shapes differ");
6085 let flat = |t: &TensorDyn| {
6086 let typed = t.as_typed::<u8>().expect("u8 image");
6087 let mut out = vec![0u8; t.shape().iter().product::<usize>()];
6088 typed.copy_to_flat(&mut out).expect("compact the rows");
6089 out
6090 };
6091 let (a, e) = (flat(actual), flat(expected));
6092 let mismatches = a.iter().zip(&e).filter(|(x, y)| x != y).count();
6093 assert_eq!(
6094 mismatches,
6095 0,
6096 "{name}: {mismatches} of {} bytes differ (first rows: {:?} vs {:?})",
6097 a.len(),
6098 &a[..16.min(a.len())],
6099 &e[..16.min(e.len())]
6100 );
6101 }
6102
6103 #[cfg(all(target_os = "windows", feature = "opengl"))]
6106 fn fill_synthetic_rgba(t: &TensorDyn) {
6107 let w = t.width().unwrap();
6108 let h = t.height().unwrap();
6109 let bound = t.as_typed::<u8>().unwrap();
6110 let stride = bound.effective_row_stride().unwrap_or(w * 4);
6111 let mut m = bound.map().unwrap();
6112 let px = m.as_mut_slice();
6113 for y in 0..h {
6114 for x in 0..w {
6115 let o = y * stride + x * 4;
6116 px[o] = x as u8;
6117 px[o + 1] = y as u8;
6118 px[o + 2] = (x + y) as u8;
6119 px[o + 3] = 255;
6120 }
6121 }
6122 }
6123
6124 #[cfg(all(target_os = "windows", feature = "opengl"))]
6133 fn compare_float_images(a: &TensorDyn, b: &TensorDyn, tol: f64, what: &str) {
6134 assert_eq!(a.format(), b.format(), "{what}: pixel formats differ");
6135 assert_eq!(a.dtype(), b.dtype(), "{what}: dtypes differ");
6136 assert_eq!(a.shape(), b.shape(), "{what}: shapes differ");
6137 let values = |t: &TensorDyn| -> Vec<f64> {
6138 let elems: usize = t.shape().iter().product();
6139 match t.dtype() {
6140 DType::F32 => {
6141 let typed = t.as_typed::<f32>().expect("F32 image");
6142 let mut bytes = vec![0u8; elems * 4];
6143 typed
6144 .copy_to_flat(&mut bytes)
6145 .expect("compact the padded rows");
6146 bytes
6147 .as_chunks::<4>()
6148 .0
6149 .iter()
6150 .map(|c| f32::from_le_bytes(*c) as f64)
6151 .collect()
6152 }
6153 DType::F16 => {
6154 let typed = t.as_typed::<half::f16>().expect("F16 image");
6155 let mut bytes = vec![0u8; elems * 2];
6156 typed
6157 .copy_to_flat(&mut bytes)
6158 .expect("compact the padded rows");
6159 bytes
6160 .as_chunks::<2>()
6161 .0
6162 .iter()
6163 .map(|c| half::f16::from_le_bytes(*c).to_f64())
6164 .collect()
6165 }
6166 other => panic!("{what}: {other:?} is not a float dtype"),
6167 }
6168 };
6169 let (av, bv) = (values(a), values(b));
6170 assert_eq!(av.len(), bv.len(), "{what}: element counts differ");
6171 for (i, (x, y)) in av.iter().zip(bv.iter()).enumerate() {
6172 assert!(
6173 (x - y).abs() <= tol,
6174 "{what}: element {i} differs by {} (gpu {x}, cpu {y}, tol {tol})",
6175 (x - y).abs()
6176 );
6177 }
6178 }
6179
6180 #[test]
6203 #[cfg(all(target_os = "windows", feature = "opengl"))]
6204 fn windows_padded_float_pbo_rows_land_at_the_declared_stride() {
6205 if !is_opengl_available() {
6206 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
6207 return;
6208 }
6209 const W: usize = 64;
6210 const H: usize = 32;
6211 const POOL_W: usize = 96;
6212
6213 let mut gpu = ImageProcessor::new().unwrap();
6214 let support = gpu.supported_render_dtypes();
6215 let src = synthetic_rgba(W, H);
6216
6217 let cases: [(PixelFormat, DType, bool, &[usize]); 2] = [
6219 (
6220 PixelFormat::PlanarRgb,
6221 DType::F16,
6222 support.f16,
6223 &[W * 2, POOL_W * 2, W * 2 + 4],
6226 ),
6227 (
6228 PixelFormat::Rgb,
6229 DType::F32,
6230 support.f32,
6231 &[W * 3 * 4, POOL_W * 3 * 4],
6232 ),
6233 ];
6234
6235 for (fmt, dtype, supported, strides) in cases {
6236 if !supported {
6237 crate::test_support::report_skip(&format!(
6238 "{} - {fmt:?}/{dtype:?} not reported",
6239 function!()
6240 ));
6241 continue;
6242 }
6243 let Some(gl) = gpu.opengl.as_ref() else {
6244 crate::test_support::report_skip(&format!("{} - no GL processor", function!()));
6245 return;
6246 };
6247 let mut reference = match gl.create_pbo_image_dtype(W, H, fmt, dtype) {
6249 Ok(t) => t,
6250 Err(e) => {
6251 crate::test_support::report_skip(&format!(
6252 "{} - no float PBO images: {e:?}",
6253 function!()
6254 ));
6255 return;
6256 }
6257 };
6258 assert_eq!(
6259 reference.memory(),
6260 TensorMemory::Pbo,
6261 "{fmt:?}/{dtype:?}: the destination must be a PBO to reach this readback"
6262 );
6263 gpu.convert(
6264 &src,
6265 &mut reference,
6266 Rotation::None,
6267 Flip::None,
6268 Crop::default(),
6269 )
6270 .unwrap_or_else(|e| panic!("{fmt:?}/{dtype:?}: tight convert failed: {e}"));
6271
6272 for &stride in strides {
6273 let label = format!("{fmt:?}/{dtype:?} at a {stride} B pitch");
6274 let mut padded = gpu
6277 .opengl
6278 .as_ref()
6279 .expect("GL processor")
6280 .create_pbo_image_dtype(POOL_W, H, fmt, dtype)
6281 .unwrap_or_else(|e| panic!("{label}: pool PBO: {e:?}"));
6282 padded
6283 .configure_image(W, H, fmt)
6284 .unwrap_or_else(|e| panic!("{label}: narrow the pool tensor: {e}"));
6285 padded
6286 .set_row_stride(stride)
6287 .unwrap_or_else(|e| panic!("{label}: declare the pitch: {e}"));
6288 assert_eq!(padded.effective_row_stride(), Some(stride), "{label}");
6289
6290 let before = gpu.convert_fallback_count();
6291 gpu.convert(
6292 &src,
6293 &mut padded,
6294 Rotation::None,
6295 Flip::None,
6296 Crop::default(),
6297 )
6298 .unwrap_or_else(|e| panic!("{label}: padded convert failed: {e}"));
6299 assert_eq!(
6302 gpu.convert_fallback_count(),
6303 before,
6304 "{label}: fell back to the CPU backend, so the float PBO readback under test never ran"
6305 );
6306
6307 let tol = if dtype == DType::F16 {
6308 1.0 / 1024.0
6309 } else {
6310 1e-6
6311 };
6312 compare_float_images(&padded, &reference, tol, &label);
6313 }
6314 }
6315 }
6316
6317 #[test]
6327 #[cfg(all(target_os = "windows", feature = "opengl"))]
6328 fn windows_float_destinations_match_the_cpu_converter_for_every_layout() {
6329 if !is_opengl_available() {
6330 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
6331 return;
6332 }
6333 if !edgefirst_tensor::is_gpu_buffer_available() {
6334 crate::test_support::report_skip(&format!(
6335 "{} - no zero-copy GPU buffers",
6336 function!()
6337 ));
6338 return;
6339 }
6340 let mut gpu = ImageProcessor::new().unwrap();
6341 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
6342 backend: ComputeBackend::Cpu,
6343 ..Default::default()
6344 })
6345 .unwrap();
6346 let support = gpu.supported_render_dtypes();
6347 eprintln!("{}: supported_render_dtypes = {support:?}", function!());
6348 let src = synthetic_rgba(640, 480);
6349 for (fmt, dtype, need) in [
6350 (PixelFormat::PlanarRgb, DType::F16, support.f16),
6351 (PixelFormat::PlanarRgb, DType::F32, support.f32),
6352 (PixelFormat::PlanarRgba, DType::F16, support.f16),
6353 (PixelFormat::PlanarRgba, DType::F32, support.f32),
6354 (PixelFormat::Rgb, DType::F16, support.f16),
6355 (PixelFormat::Rgb, DType::F32, support.f32),
6356 (PixelFormat::Rgba, DType::F16, support.f16),
6357 (PixelFormat::Rgba, DType::F32, support.f32),
6358 ] {
6359 if !need {
6360 eprintln!("{}: {fmt:?}/{dtype:?} not reported", function!());
6361 continue;
6362 }
6363 let mut dst_gpu = gpu
6364 .create_image(
6365 640,
6366 480,
6367 fmt,
6368 dtype,
6369 Some(TensorMemory::DmaBuf),
6370 edgefirst_tensor::CpuAccess::ReadWrite,
6371 )
6372 .unwrap();
6373 assert_eq!(
6374 dst_gpu.memory(),
6375 TensorMemory::DmaBuf,
6376 "{fmt:?}/{dtype:?} must be a texture"
6377 );
6378 let mut dst_cpu = TensorDyn::image(
6379 640,
6380 480,
6381 fmt,
6382 dtype,
6383 Some(TensorMemory::Mem),
6384 edgefirst_tensor::CpuAccess::ReadWrite,
6385 )
6386 .unwrap();
6387 let before = gpu.convert_fallback_count();
6388 gpu.convert(
6389 &src,
6390 &mut dst_gpu,
6391 Rotation::None,
6392 Flip::None,
6393 Crop::default(),
6394 )
6395 .unwrap();
6396 cpu.convert(
6397 &src,
6398 &mut dst_cpu,
6399 Rotation::None,
6400 Flip::None,
6401 Crop::default(),
6402 )
6403 .unwrap();
6404 assert_eq!(
6405 gpu.convert_fallback_count(),
6406 before,
6407 "{fmt:?}/{dtype:?} fell back to CPU"
6408 );
6409 let tol = if dtype == DType::F16 {
6410 1.0 / 1024.0
6411 } else {
6412 1e-6
6413 };
6414 compare_float_images(&dst_gpu, &dst_cpu, tol, &format!("{fmt:?}/{dtype:?}"));
6415 }
6416 }
6417
6418 #[cfg(target_os = "windows")]
6421 #[link(name = "kernel32")]
6422 extern "system" {
6423 fn WaitForSingleObject(handle: *mut std::ffi::c_void, milliseconds: u32) -> u32;
6424 }
6425
6426 #[cfg(all(target_os = "windows", feature = "opengl"))]
6435 #[test]
6436 fn windows_deferred_batch_records_one_value_per_tile_and_submits_once() {
6437 use edgefirst_tensor::CpuAccess;
6438
6439 if !edgefirst_tensor::is_gpu_buffer_available() {
6440 crate::test_support::report_skip(&format!(
6441 "{} - no zero-copy GPU buffers",
6442 function!()
6443 ));
6444 return;
6445 }
6446 let mut p = match ImageProcessor::new() {
6447 Ok(p) => p,
6448 Err(e) => {
6449 crate::test_support::report_skip(&format!(
6450 "{} - ImageProcessor init failed ({e:?})",
6451 function!()
6452 ));
6453 return;
6454 }
6455 };
6456 let (w, h, tiles) = (640usize, 480usize, 4usize);
6457 let src = synthetic_rgba(w, h);
6458 let mut dsts: Vec<TensorDyn> = (0..tiles)
6459 .map(|_| {
6460 p.create_image(w, h, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6461 .unwrap()
6462 })
6463 .collect();
6464 if dsts[0].memory() != TensorMemory::DmaBuf {
6465 crate::test_support::report_skip(&format!(
6466 "{} - destinations are not textures",
6467 function!()
6468 ));
6469 return;
6470 }
6471 for dst in &mut dsts {
6472 p.convert_deferred(&src, dst, Rotation::None, Flip::None, Crop::default())
6473 .unwrap();
6474 }
6475 let values: Vec<u64> = dsts
6476 .iter()
6477 .map(|d| {
6478 d.gpu_completion()
6479 .unwrap()
6480 .expect("each deferred convert records its own completion")
6481 .value
6482 })
6483 .collect();
6484 for pair in values.windows(2) {
6485 assert!(
6486 pair[1] > pair[0],
6487 "the batch allocated {values:?}, which is not one increasing value per tile"
6488 );
6489 }
6490 p.flush().unwrap();
6491 let dev = edgefirst_tensor::d3d11::device().unwrap();
6494 let last = *values.last().unwrap();
6495 let ev = dev.event_for(last).unwrap();
6496 assert_eq!(
6499 unsafe {
6500 WaitForSingleObject(
6501 std::os::windows::io::AsRawHandle::as_raw_handle(&ev) as _,
6502 5000,
6503 )
6504 },
6505 0,
6506 "the batch's newest value {last} never arrived after flush"
6507 );
6508
6509 let mut solo = p
6510 .create_image(w, h, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6511 .unwrap();
6512 p.convert(&src, &mut solo, Rotation::None, Flip::None, Crop::default())
6513 .unwrap();
6514 let flat = |img: &TensorDyn| -> Vec<u8> {
6518 let t = img.as_typed::<u8>().expect("u8 image");
6519 let mut out = vec![0u8; t.shape().iter().product::<usize>()];
6520 t.copy_to_flat(&mut out).expect("compact the padded rows");
6521 out
6522 };
6523 let want = flat(&solo);
6524 for (i, dst) in dsts.iter().enumerate() {
6525 assert_eq!(
6526 flat(dst),
6527 want,
6528 "tile {i} does not match a standalone convert"
6529 );
6530 }
6531 }
6532
6533 #[cfg(all(target_os = "windows", feature = "opengl"))]
6542 #[test]
6543 fn windows_dropping_a_processor_submits_an_open_batch() {
6544 use edgefirst_tensor::CpuAccess;
6545
6546 if !edgefirst_tensor::is_gpu_buffer_available() {
6547 crate::test_support::report_skip(&format!(
6548 "{} - no zero-copy GPU buffers",
6549 function!()
6550 ));
6551 return;
6552 }
6553 let src = synthetic_rgba(320, 240);
6554 let recorded = {
6555 let mut p = match ImageProcessor::new() {
6556 Ok(p) => p,
6557 Err(e) => {
6558 crate::test_support::report_skip(&format!(
6559 "{} - init failed ({e:?})",
6560 function!()
6561 ));
6562 return;
6563 }
6564 };
6565 let mut dst = p
6566 .create_image(
6567 320,
6568 240,
6569 PixelFormat::Rgba,
6570 DType::U8,
6571 None,
6572 CpuAccess::Read,
6573 )
6574 .unwrap();
6575 if dst.memory() != TensorMemory::DmaBuf {
6576 crate::test_support::report_skip(&format!(
6577 "{} - destination is not a texture",
6578 function!()
6579 ));
6580 return;
6581 }
6582 p.convert_deferred(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
6583 .unwrap();
6584 dst.gpu_completion()
6586 .unwrap()
6587 .expect("the deferred convert recorded a completion")
6588 .value
6589 };
6590
6591 let dev = edgefirst_tensor::d3d11::device().unwrap();
6592 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
6593 while dev.completed_value() < recorded {
6594 assert!(
6595 std::time::Instant::now() < deadline,
6596 "the dropped processor never submitted its batch: fence at {}, recorded {recorded}",
6597 dev.completed_value()
6598 );
6599 std::thread::yield_now();
6600 }
6601 }
6602
6603 #[cfg(all(target_os = "windows", feature = "opengl"))]
6608 #[test]
6609 fn windows_create_image_prefers_textures_and_convert_with_fence_returns_a_set_event() {
6610 use edgefirst_tensor::CpuAccess;
6611 use std::os::windows::io::AsRawHandle;
6612
6613 if !edgefirst_tensor::is_gpu_buffer_available() {
6614 crate::test_support::report_skip(&format!(
6615 "{} - no zero-copy GPU buffers",
6616 function!()
6617 ));
6618 return;
6619 }
6620 let mut p = ImageProcessor::new().unwrap();
6621 let src = synthetic_rgba(320, 240);
6622 let mut dst = p
6623 .create_image(
6624 320,
6625 240,
6626 PixelFormat::Rgba,
6627 DType::U8,
6628 None,
6629 CpuAccess::Read,
6630 )
6631 .unwrap();
6632 if dst.memory() != TensorMemory::DmaBuf
6636 && !std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1")
6637 {
6638 crate::test_support::report_skip(&format!(
6639 "{} - create_image gave {:?}, not a texture (no GL backend)",
6640 function!(),
6641 dst.memory()
6642 ));
6643 return;
6644 }
6645 assert_eq!(
6646 dst.memory(),
6647 TensorMemory::DmaBuf,
6648 "create_image(None) must prefer a texture on Windows"
6649 );
6650 let before = dst.gpu_completion().unwrap().map(|c| c.value);
6653 let fence = p
6654 .convert_with_fence(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
6655 .unwrap()
6656 .expect("Windows exports a fence");
6657 let recorded = dst
6658 .gpu_completion()
6659 .unwrap()
6660 .expect("the convert recorded a completion")
6661 .value;
6662 assert!(
6663 before.is_none_or(|b| recorded > b),
6664 "the fenced convert recorded a newer value than {before:?}"
6665 );
6666 assert_eq!(
6671 edgefirst_tensor::d3d11::device().unwrap().last_signalled(),
6672 recorded,
6673 "a fenced convert must signal exactly once"
6674 );
6675 assert_eq!(
6680 unsafe { WaitForSingleObject(fence.as_raw_handle() as _, 5000) },
6681 0,
6682 "fence was not set within 5 seconds"
6683 );
6684 assert!(
6688 edgefirst_tensor::d3d11::device().unwrap().completed_value() >= recorded,
6689 "the event fired, so the recorded value {recorded} is complete"
6690 );
6691 assert_eq!(
6692 dst.gpu_completion().unwrap().map(|c| c.value),
6693 Some(recorded),
6694 "the destination still reports the value the event waited on"
6695 );
6696 let mut expected = TensorDyn::image(
6697 320,
6698 240,
6699 PixelFormat::Rgba,
6700 DType::U8,
6701 Some(TensorMemory::Mem),
6702 CpuAccess::ReadWrite,
6703 )
6704 .unwrap();
6705 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
6706 backend: ComputeBackend::Cpu,
6707 ..Default::default()
6708 })
6709 .unwrap();
6710 cpu.convert(
6711 &src,
6712 &mut expected,
6713 Rotation::None,
6714 Flip::None,
6715 Crop::default(),
6716 )
6717 .unwrap();
6718 compare_images(&dst, &expected, 0.0, "fenced convert");
6719 }
6720
6721 #[cfg(all(target_os = "windows", feature = "opengl"))]
6726 fn texture_or_skip(t: &TensorDyn, test: &str) -> bool {
6727 if t.memory() == TensorMemory::DmaBuf {
6728 return true;
6729 }
6730 if std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1") {
6731 panic!(
6732 "{test}: create_image(None) must prefer a texture on Windows, got {:?}",
6733 t.memory()
6734 );
6735 }
6736 crate::test_support::report_skip(&format!(
6737 "{test} - create_image gave {:?}, not a texture (no GL backend)",
6738 t.memory()
6739 ));
6740 false
6741 }
6742
6743 #[cfg(all(target_os = "windows", feature = "opengl"))]
6749 #[test]
6750 fn windows_draw_decoded_masks_into_a_texture_records_a_completion() {
6751 use edgefirst_tensor::{CpuAccess, Segmentation};
6752 use ndarray::Array3;
6753
6754 if !edgefirst_tensor::is_gpu_buffer_available() {
6755 crate::test_support::report_skip(&format!(
6756 "{} - no zero-copy GPU buffers",
6757 function!()
6758 ));
6759 return;
6760 }
6761 let mut p = ImageProcessor::new().unwrap();
6762 let mut dst = p
6763 .create_image(64, 64, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6764 .unwrap();
6765 if !texture_or_skip(&dst, function!()) {
6766 return;
6767 }
6768 assert!(
6769 dst.gpu_completion().unwrap().is_none(),
6770 "nothing has written this texture yet"
6771 );
6772
6773 let detect = DetectBox {
6774 bbox: [0.25, 0.25, 0.75, 0.75].into(),
6775 score: 0.99,
6776 label: 0,
6777 };
6778 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
6779 let seg = Segmentation {
6780 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
6781 .unwrap()
6782 .into(),
6783 xmin: 0.25,
6784 ymin: 0.25,
6785 xmax: 0.75,
6786 ymax: 0.75,
6787 };
6788 p.draw_decoded_masks(&mut dst, &[detect], &[seg], MaskOverlay::default())
6789 .expect("draw into a read-only texture");
6790
6791 let completion = dst
6792 .gpu_completion()
6793 .unwrap()
6794 .expect("the draw recorded no completion on dst");
6795 assert!(completion.value > 0, "completion value must be non-zero");
6796
6797 let mut flat = vec![0u8; 64 * 64 * 4];
6802 dst.as_typed::<u8>()
6803 .unwrap()
6804 .copy_to_flat(&mut flat)
6805 .expect("read the drawn texture");
6806 let px = |x: usize, y: usize| -> [u8; 4] {
6807 let o = (y * 64 + x) * 4;
6808 [flat[o], flat[o + 1], flat[o + 2], flat[o + 3]]
6809 };
6810 assert_eq!(px(2, 2), [0, 0, 0, 0], "corner (2,2) was not cleared");
6811 assert_ne!(px(32, 32), [0, 0, 0, 0], "centre (32,32) was not coloured");
6812 }
6813
6814 #[cfg(all(target_os = "windows", feature = "opengl"))]
6817 #[test]
6818 fn windows_draw_proto_masks_into_a_texture_records_a_completion() {
6819 use edgefirst_tensor::{CpuAccess, Tensor};
6820
6821 if !edgefirst_tensor::is_gpu_buffer_available() {
6822 crate::test_support::report_skip(&format!(
6823 "{} - no zero-copy GPU buffers",
6824 function!()
6825 ));
6826 return;
6827 }
6828 let mut p = match with_force_backend(Some("opengl"), ImageProcessor::new) {
6832 Ok(p) => p,
6833 Err(e) if !std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1") => {
6834 crate::test_support::report_skip(&format!(
6835 "{} - no OpenGL backend: {e}",
6836 function!()
6837 ));
6838 return;
6839 }
6840 Err(e) => panic!("forced OpenGL backend unavailable: {e}"),
6841 };
6842 let mut dst = p
6843 .create_image(64, 64, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6844 .unwrap();
6845 if !texture_or_skip(&dst, function!()) {
6846 return;
6847 }
6848
6849 let detect = DetectBox {
6850 bbox: [0.25, 0.25, 0.75, 0.75].into(),
6851 score: 0.99,
6852 label: 0,
6853 };
6854 let proto = ProtoData {
6857 mask_coefficients: TensorDyn::F32(
6858 Tensor::<f32>::from_slice(&[1.0_f32], &[1, 1]).unwrap(),
6859 ),
6860 protos: TensorDyn::F32(
6861 Tensor::<f32>::from_slice(&vec![1.0_f32; 8 * 8], &[8, 8, 1]).unwrap(),
6862 ),
6863 layout: ProtoLayout::Nhwc,
6864 };
6865 p.draw_proto_masks(&mut dst, &[detect], &proto, MaskOverlay::default())
6866 .expect("forced-GL proto draw into a read-only texture");
6867
6868 let completion = dst
6869 .gpu_completion()
6870 .unwrap()
6871 .expect("the proto draw recorded no completion on dst");
6872 assert!(completion.value > 0, "completion value must be non-zero");
6873 }
6874
6875 #[cfg(all(target_os = "windows", feature = "opengl"))]
6880 #[test]
6881 fn windows_draw_decoded_masks_composites_a_host_background_onto_a_texture() {
6882 use edgefirst_tensor::CpuAccess;
6883
6884 if !edgefirst_tensor::is_gpu_buffer_available() {
6885 crate::test_support::report_skip(&format!(
6886 "{} - no zero-copy GPU buffers",
6887 function!()
6888 ));
6889 return;
6890 }
6891 let mut p = ImageProcessor::new().unwrap();
6892 let bg = synthetic_rgba(64, 48);
6893 let mut dst = p
6894 .create_image(64, 48, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6895 .unwrap();
6896 if !texture_or_skip(&dst, function!()) {
6897 return;
6898 }
6899 p.draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::new().with_background(&bg))
6900 .expect("empty draw with a host background onto a texture");
6901 assert_pixels_identical(&dst, &bg, "host background onto a texture");
6902 assert!(dst.gpu_completion().unwrap().is_some());
6903 }
6904
6905 #[cfg(all(target_os = "windows", feature = "opengl"))]
6908 #[test]
6909 fn windows_draw_decoded_masks_composites_a_texture_background_onto_a_texture() {
6910 use edgefirst_tensor::CpuAccess;
6911
6912 if !edgefirst_tensor::is_gpu_buffer_available() {
6913 crate::test_support::report_skip(&format!(
6914 "{} - no zero-copy GPU buffers",
6915 function!()
6916 ));
6917 return;
6918 }
6919 let mut p = ImageProcessor::new().unwrap();
6920 let expected = synthetic_rgba(64, 48);
6921 let bg = p
6922 .create_image(
6923 64,
6924 48,
6925 PixelFormat::Rgba,
6926 DType::U8,
6927 None,
6928 CpuAccess::ReadWrite,
6929 )
6930 .unwrap();
6931 if !texture_or_skip(&bg, function!()) {
6932 return;
6933 }
6934 fill_synthetic_rgba(&bg);
6935 let mut dst = p
6936 .create_image(64, 48, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6937 .unwrap();
6938 assert_eq!(dst.memory(), TensorMemory::DmaBuf);
6939 p.draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::new().with_background(&bg))
6940 .expect("empty draw with a texture background onto a texture");
6941 assert_pixels_identical(&dst, &expected, "texture background onto a texture");
6942 assert!(dst.gpu_completion().unwrap().is_some());
6943 }
6944
6945 #[cfg(all(target_os = "windows", feature = "opengl"))]
6951 #[test]
6952 fn windows_draw_decoded_masks_into_a_texture_view_fills_only_its_band() {
6953 use edgefirst_tensor::CpuAccess;
6954
6955 if !edgefirst_tensor::is_gpu_buffer_available() {
6956 crate::test_support::report_skip(&format!(
6957 "{} - no zero-copy GPU buffers",
6958 function!()
6959 ));
6960 return;
6961 }
6962 let (w, h) = (32usize, 64usize);
6963 let mut p = ImageProcessor::new().unwrap();
6964 let parent = p
6965 .create_image(
6966 w,
6967 h,
6968 PixelFormat::Rgba,
6969 DType::U8,
6970 None,
6971 CpuAccess::ReadWrite,
6972 )
6973 .unwrap();
6974 if !texture_or_skip(&parent, function!()) {
6975 return;
6976 }
6977 fill_synthetic_rgba(&parent);
6978
6979 let mut band = parent
6983 .view(edgefirst_tensor::Region::new(0, h / 2, w, h / 2))
6984 .unwrap();
6985 p.draw_decoded_masks(&mut band, &[], &[], MaskOverlay::default())
6986 .expect("empty draw into a texture view");
6987 drop(band);
6988
6989 let mut flat = vec![0u8; w * h * 4];
6990 parent
6991 .as_typed::<u8>()
6992 .unwrap()
6993 .copy_to_flat(&mut flat)
6994 .expect("read the parent texture");
6995 for y in 0..h {
6996 for x in 0..w {
6997 let o = (y * w + x) * 4;
6998 let got = [flat[o], flat[o + 1], flat[o + 2], flat[o + 3]];
6999 let want = if y < h / 2 {
7000 [x as u8, y as u8, (x + y) as u8, 255]
7001 } else {
7002 [0, 0, 0, 0]
7003 };
7004 assert_eq!(got, want, "pixel ({x},{y})");
7005 }
7006 }
7007
7008 let mut forced = with_force_backend(Some("opengl"), ImageProcessor::new).unwrap();
7011 let mut band = parent
7012 .view(edgefirst_tensor::Region::new(0, h / 2, w, h / 2))
7013 .unwrap();
7014 let err = forced
7015 .draw_decoded_masks(&mut band, &[], &[], MaskOverlay::default())
7016 .expect_err("forced GL must decline a view destination");
7017 assert!(
7018 matches!(err, Error::NotSupported(_)),
7019 "expected NotSupported, got {err:?}"
7020 );
7021 }
7022
7023 #[cfg(all(target_os = "windows", feature = "opengl"))]
7028 #[test]
7029 fn windows_class_colors_reach_the_cpu_backend_behind_a_declined_gl_draw() {
7030 use edgefirst_tensor::{CpuAccess, Segmentation};
7031 use ndarray::Array3;
7032
7033 if !edgefirst_tensor::is_gpu_buffer_available() {
7034 crate::test_support::report_skip(&format!(
7035 "{} - no zero-copy GPU buffers",
7036 function!()
7037 ));
7038 return;
7039 }
7040 const CUSTOM: [u8; 4] = [7, 200, 13, 255];
7041 assert_ne!(
7042 CUSTOM[..3],
7043 DEFAULT_COLORS_U8[0][..3],
7044 "the custom colour must differ from the default palette's, or \
7045 this test cannot tell them apart"
7046 );
7047
7048 let (w, h) = (32usize, 64usize);
7049 let mut p = ImageProcessor::new().unwrap();
7050 let parent = p
7051 .create_image(
7052 w,
7053 h,
7054 PixelFormat::Rgba,
7055 DType::U8,
7056 None,
7057 CpuAccess::ReadWrite,
7058 )
7059 .unwrap();
7060 if parent.memory() != TensorMemory::DmaBuf
7065 && !std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1")
7066 {
7067 crate::test_support::report_skip(&format!(
7068 "{} - create_image gave {:?}, not a texture (no GL backend)",
7069 function!(),
7070 parent.memory()
7071 ));
7072 return;
7073 }
7074 p.set_class_colors(&[CUSTOM]).unwrap();
7075
7076 let detect = DetectBox {
7077 bbox: [0.25, 0.25, 0.75, 0.75].into(),
7078 score: 0.99,
7079 label: 0,
7080 };
7081 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
7082 let seg = Segmentation {
7083 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
7084 .unwrap()
7085 .into(),
7086 xmin: 0.25,
7087 ymin: 0.25,
7088 xmax: 0.75,
7089 ymax: 0.75,
7090 };
7091 let mut band = parent
7092 .view(edgefirst_tensor::Region::new(0, h / 2, w, h / 2))
7093 .unwrap();
7094 p.draw_decoded_masks(&mut band, &[detect], &[seg], MaskOverlay::default())
7095 .expect("draw into a texture view");
7096 drop(band);
7097
7098 let mut flat = vec![0u8; w * h * 4];
7099 parent
7100 .as_typed::<u8>()
7101 .unwrap()
7102 .copy_to_flat(&mut flat)
7103 .expect("read the parent texture");
7104 let o = ((h / 2 + h / 4) * w + w / 2) * 4;
7107 assert_eq!(
7108 &flat[o..o + 3],
7109 &CUSTOM[..3],
7110 "the CPU backend drew the default palette, so it never received \
7111 set_class_colors"
7112 );
7113 }
7114
7115 #[cfg(all(target_os = "windows", feature = "opengl"))]
7120 #[test]
7121 fn two_processors_alternate_on_texture_destinations_without_state_bleed() {
7122 use edgefirst_tensor::CpuAccess;
7123
7124 if !edgefirst_tensor::is_gpu_buffer_available() {
7125 crate::test_support::report_skip(&format!(
7126 "{} - no zero-copy GPU buffers",
7127 function!()
7128 ));
7129 return;
7130 }
7131 let mut a = ImageProcessor::new().unwrap();
7132 let mut b = ImageProcessor::new().unwrap();
7133 let src_a = synthetic_rgba(512, 512);
7134 let src_b = synthetic_rgba(256, 256);
7135 let mut dst_a = a
7136 .create_image(
7137 512,
7138 512,
7139 PixelFormat::Rgba,
7140 DType::U8,
7141 None,
7142 CpuAccess::Read,
7143 )
7144 .unwrap();
7145 let mut dst_b = b
7146 .create_image(
7147 256,
7148 256,
7149 PixelFormat::Rgba,
7150 DType::U8,
7151 None,
7152 CpuAccess::Read,
7153 )
7154 .unwrap();
7155 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
7156 backend: ComputeBackend::Cpu,
7157 ..Default::default()
7158 })
7159 .unwrap();
7160 let mut ref_a = TensorDyn::image(
7161 512,
7162 512,
7163 PixelFormat::Rgba,
7164 DType::U8,
7165 Some(TensorMemory::Mem),
7166 CpuAccess::ReadWrite,
7167 )
7168 .unwrap();
7169 let mut ref_b = TensorDyn::image(
7170 256,
7171 256,
7172 PixelFormat::Rgba,
7173 DType::U8,
7174 Some(TensorMemory::Mem),
7175 CpuAccess::ReadWrite,
7176 )
7177 .unwrap();
7178 cpu.convert(
7179 &src_a,
7180 &mut ref_a,
7181 Rotation::None,
7182 Flip::None,
7183 Crop::default(),
7184 )
7185 .unwrap();
7186 cpu.convert(
7187 &src_b,
7188 &mut ref_b,
7189 Rotation::None,
7190 Flip::None,
7191 Crop::default(),
7192 )
7193 .unwrap();
7194 for _ in 0..200 {
7195 a.convert(
7196 &src_a,
7197 &mut dst_a,
7198 Rotation::None,
7199 Flip::None,
7200 Crop::default(),
7201 )
7202 .unwrap();
7203 b.convert(
7204 &src_b,
7205 &mut dst_b,
7206 Rotation::None,
7207 Flip::None,
7208 Crop::default(),
7209 )
7210 .unwrap();
7211 compare_images(&dst_a, &ref_a, 0.0, "processor A");
7212 compare_images(&dst_b, &ref_b, 0.0, "processor B");
7213 }
7214 }
7215
7216 #[test]
7217 #[cfg(any(
7218 target_os = "linux",
7219 target_os = "macos",
7220 target_os = "ios",
7221 target_os = "android",
7222 target_os = "windows"
7223 ))]
7224 #[cfg(feature = "opengl")]
7225 fn test_opengl_all_rgba() {
7226 if !is_opengl_available() {
7227 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
7228 return;
7229 }
7230
7231 let dst_width = 640;
7232 let dst_height = 640;
7233 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7234
7235 let mut cpu_converter = CPUProcessor::new();
7236
7237 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
7238
7239 let mut mem = vec![None, Some(TensorMemory::Mem)];
7243 if edgefirst_tensor::is_shm_available() {
7244 mem.push(Some(TensorMemory::Shm));
7245 }
7246 if edgefirst_tensor::is_gpu_buffer_available() {
7247 mem.push(Some(TensorMemory::DmaBuf));
7248 }
7249 let crop = Crop::new().with_source(Some(Region::new(50, 120, 1024, 576)));
7250 for m in mem {
7251 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), m).unwrap();
7252 let src_dyn = src;
7253
7254 for rot in [
7255 Rotation::None,
7256 Rotation::Clockwise90,
7257 Rotation::Rotate180,
7258 Rotation::CounterClockwise90,
7259 ] {
7260 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
7261 let cpu_dst = TensorDyn::image(
7262 dst_width,
7263 dst_height,
7264 PixelFormat::Rgba,
7265 DType::U8,
7266 m,
7267 edgefirst_tensor::CpuAccess::ReadWrite,
7268 )
7269 .unwrap();
7270 let gl_dst = TensorDyn::image(
7271 dst_width,
7272 dst_height,
7273 PixelFormat::Rgba,
7274 DType::U8,
7275 m,
7276 edgefirst_tensor::CpuAccess::ReadWrite,
7277 )
7278 .unwrap();
7279 cpu_dst
7280 .as_typed::<u8>()
7281 .unwrap()
7282 .map()
7283 .unwrap()
7284 .as_mut_slice()
7285 .fill(114);
7286 gl_dst
7287 .as_typed::<u8>()
7288 .unwrap()
7289 .map()
7290 .unwrap()
7291 .as_mut_slice()
7292 .fill(114);
7293
7294 let mut cpu_dst_dyn = cpu_dst;
7295 cpu_converter
7296 .convert(&src_dyn, &mut cpu_dst_dyn, Rotation::None, Flip::None, crop)
7297 .unwrap();
7298 let cpu_dst = {
7299 let mut __t = cpu_dst_dyn.into_u8().unwrap();
7300 __t.set_format(PixelFormat::Rgba).unwrap();
7301 TensorDyn::from(__t)
7302 };
7303
7304 let mut gl_dst_dyn = gl_dst;
7305 gl_converter
7306 .convert(&src_dyn, &mut gl_dst_dyn, Rotation::None, Flip::None, crop)
7307 .map_err(|e| {
7308 log::error!("error mem {m:?} rot {rot:?} error: {e:?}");
7309 e
7310 })
7311 .unwrap();
7312 let gl_dst = {
7313 let mut __t = gl_dst_dyn.into_u8().unwrap();
7314 __t.set_format(PixelFormat::Rgba).unwrap();
7315 TensorDyn::from(__t)
7316 };
7317
7318 compare_images(
7319 &gl_dst,
7320 &cpu_dst,
7321 0.98,
7322 &format!("{} {:?} {:?}", function!(), rot, flip),
7323 );
7324 }
7325 }
7326 }
7327 }
7328
7329 #[test]
7330 #[cfg(target_os = "linux")]
7331 fn test_cpu_rotate() {
7332 for rot in [
7333 Rotation::Clockwise90,
7334 Rotation::Rotate180,
7335 Rotation::CounterClockwise90,
7336 ] {
7337 test_cpu_rotate_(rot);
7338 }
7339 }
7340
7341 #[cfg(target_os = "linux")]
7342 fn test_cpu_rotate_(rot: Rotation) {
7343 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7347
7348 let unchanged_src =
7349 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
7350 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
7351
7352 let (dst_width, dst_height) = match rot {
7353 Rotation::None | Rotation::Rotate180 => (src.width().unwrap(), src.height().unwrap()),
7354 Rotation::Clockwise90 | Rotation::CounterClockwise90 => {
7355 (src.height().unwrap(), src.width().unwrap())
7356 }
7357 };
7358
7359 let cpu_dst = TensorDyn::image(
7360 dst_width,
7361 dst_height,
7362 PixelFormat::Rgba,
7363 DType::U8,
7364 None,
7365 edgefirst_tensor::CpuAccess::ReadWrite,
7366 )
7367 .unwrap();
7368 let mut cpu_converter = CPUProcessor::new();
7369
7370 let (result, src, cpu_dst) = convert_img(
7373 &mut cpu_converter,
7374 src,
7375 cpu_dst,
7376 rot,
7377 Flip::None,
7378 Crop::no_crop(),
7379 );
7380 result.unwrap();
7381
7382 let (result, cpu_dst, src) = convert_img(
7383 &mut cpu_converter,
7384 cpu_dst,
7385 src,
7386 rot,
7387 Flip::None,
7388 Crop::no_crop(),
7389 );
7390 result.unwrap();
7391
7392 let (result, src, cpu_dst) = convert_img(
7393 &mut cpu_converter,
7394 src,
7395 cpu_dst,
7396 rot,
7397 Flip::None,
7398 Crop::no_crop(),
7399 );
7400 result.unwrap();
7401
7402 let (result, _cpu_dst, src) = convert_img(
7403 &mut cpu_converter,
7404 cpu_dst,
7405 src,
7406 rot,
7407 Flip::None,
7408 Crop::no_crop(),
7409 );
7410 result.unwrap();
7411
7412 compare_images(&src, &unchanged_src, 0.98, function!());
7413 }
7414
7415 #[test]
7416 #[cfg(any(
7417 target_os = "linux",
7418 target_os = "macos",
7419 target_os = "ios",
7420 target_os = "android",
7421 target_os = "windows"
7422 ))]
7423 #[cfg(feature = "opengl")]
7424 fn test_opengl_rotate() {
7425 if !is_opengl_available() {
7426 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
7427 return;
7428 }
7429
7430 let size = (1280, 720);
7431 let mut mem = vec![None, Some(TensorMemory::Mem)];
7433 if edgefirst_tensor::is_shm_available() {
7434 mem.push(Some(TensorMemory::Shm));
7435 }
7436 if edgefirst_tensor::is_gpu_buffer_available() {
7437 mem.push(Some(TensorMemory::DmaBuf));
7438 }
7439 for m in mem {
7440 for rot in [
7441 Rotation::Clockwise90,
7442 Rotation::Rotate180,
7443 Rotation::CounterClockwise90,
7444 ] {
7445 test_opengl_rotate_(size, rot, m);
7446 }
7447 }
7448 }
7449
7450 #[cfg(any(
7451 target_os = "linux",
7452 target_os = "macos",
7453 target_os = "ios",
7454 target_os = "android",
7455 target_os = "windows"
7456 ))]
7457 #[cfg(feature = "opengl")]
7458 fn test_opengl_rotate_(
7459 size: (usize, usize),
7460 rot: Rotation,
7461 tensor_memory: Option<TensorMemory>,
7462 ) {
7463 let (dst_width, dst_height) = match rot {
7464 Rotation::None | Rotation::Rotate180 => size,
7465 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
7466 };
7467
7468 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7469 let src =
7470 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), tensor_memory).unwrap();
7471
7472 let cpu_dst = TensorDyn::image(
7473 dst_width,
7474 dst_height,
7475 PixelFormat::Rgba,
7476 DType::U8,
7477 None,
7478 edgefirst_tensor::CpuAccess::ReadWrite,
7479 )
7480 .unwrap();
7481 let mut cpu_converter = CPUProcessor::new();
7482
7483 let (result, mut src, cpu_dst) = convert_img(
7484 &mut cpu_converter,
7485 src,
7486 cpu_dst,
7487 rot,
7488 Flip::None,
7489 Crop::no_crop(),
7490 );
7491 result.unwrap();
7492
7493 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
7494
7495 for _ in 0..5 {
7496 let gl_dst = TensorDyn::image(
7497 dst_width,
7498 dst_height,
7499 PixelFormat::Rgba,
7500 DType::U8,
7501 tensor_memory,
7502 edgefirst_tensor::CpuAccess::ReadWrite,
7503 )
7504 .unwrap();
7505 let (result, src_back, gl_dst) = convert_img(
7506 &mut gl_converter,
7507 src,
7508 gl_dst,
7509 rot,
7510 Flip::None,
7511 Crop::no_crop(),
7512 );
7513 result.unwrap();
7514 src = src_back;
7515 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
7516 }
7517 }
7518
7519 #[test]
7520 #[cfg(target_os = "linux")]
7521 fn test_g2d_rotate() {
7522 if !is_g2d_available() {
7523 crate::test_support::report_skip(
7524 "test_g2d_rotate - G2D library (libg2d.so.2) not available",
7525 );
7526 return;
7527 }
7528 if !is_dma_available() {
7529 crate::test_support::report_skip("test_g2d_rotate - DMA memory allocation not available (permission denied or no DMA-BUF support)");
7530 return;
7531 }
7532
7533 let size = (1280, 720);
7534 for rot in [
7535 Rotation::Clockwise90,
7536 Rotation::Rotate180,
7537 Rotation::CounterClockwise90,
7538 ] {
7539 test_g2d_rotate_(size, rot);
7540 }
7541 }
7542
7543 #[cfg(target_os = "linux")]
7544 fn test_g2d_rotate_(size: (usize, usize), rot: Rotation) {
7545 let (dst_width, dst_height) = match rot {
7546 Rotation::None | Rotation::Rotate180 => size,
7547 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
7548 };
7549
7550 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7551 let src = crate::load_image_test_helper(
7552 &file,
7553 Some(PixelFormat::Rgba),
7554 Some(TensorMemory::DmaBuf),
7555 )
7556 .unwrap();
7557
7558 let cpu_dst = TensorDyn::image(
7559 dst_width,
7560 dst_height,
7561 PixelFormat::Rgba,
7562 DType::U8,
7563 None,
7564 edgefirst_tensor::CpuAccess::ReadWrite,
7565 )
7566 .unwrap();
7567 let mut cpu_converter = CPUProcessor::new();
7568
7569 let (result, src, cpu_dst) = convert_img(
7570 &mut cpu_converter,
7571 src,
7572 cpu_dst,
7573 rot,
7574 Flip::None,
7575 Crop::no_crop(),
7576 );
7577 result.unwrap();
7578
7579 let g2d_dst = TensorDyn::image(
7580 dst_width,
7581 dst_height,
7582 PixelFormat::Rgba,
7583 DType::U8,
7584 Some(TensorMemory::DmaBuf),
7585 edgefirst_tensor::CpuAccess::ReadWrite,
7586 )
7587 .unwrap();
7588 let mut g2d_converter = G2DProcessor::new().unwrap();
7589
7590 let (result, _src, g2d_dst) = convert_img(
7591 &mut g2d_converter,
7592 src,
7593 g2d_dst,
7594 rot,
7595 Flip::None,
7596 Crop::no_crop(),
7597 );
7598 result.unwrap();
7599
7600 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
7606 }
7607
7608 #[test]
7609 fn test_rgba_to_yuyv_resize_cpu() {
7610 let src = load_bytes_to_tensor(
7611 1280,
7612 720,
7613 PixelFormat::Rgba,
7614 None,
7615 &edgefirst_bench::testdata::read("camera720p.rgba"),
7616 )
7617 .unwrap();
7618
7619 let (dst_width, dst_height) = (640, 360);
7620
7621 let dst = TensorDyn::image(
7622 dst_width,
7623 dst_height,
7624 PixelFormat::Yuyv,
7625 DType::U8,
7626 None,
7627 edgefirst_tensor::CpuAccess::ReadWrite,
7628 )
7629 .unwrap();
7630
7631 let dst_through_yuyv = TensorDyn::image(
7632 dst_width,
7633 dst_height,
7634 PixelFormat::Rgba,
7635 DType::U8,
7636 None,
7637 edgefirst_tensor::CpuAccess::ReadWrite,
7638 )
7639 .unwrap();
7640 let dst_direct = TensorDyn::image(
7641 dst_width,
7642 dst_height,
7643 PixelFormat::Rgba,
7644 DType::U8,
7645 None,
7646 edgefirst_tensor::CpuAccess::ReadWrite,
7647 )
7648 .unwrap();
7649
7650 let mut cpu_converter = CPUProcessor::new();
7651
7652 let (result, src, dst) = convert_img(
7653 &mut cpu_converter,
7654 src,
7655 dst,
7656 Rotation::None,
7657 Flip::None,
7658 Crop::no_crop(),
7659 );
7660 result.unwrap();
7661
7662 let (result, _dst, dst_through_yuyv) = convert_img(
7663 &mut cpu_converter,
7664 dst,
7665 dst_through_yuyv,
7666 Rotation::None,
7667 Flip::None,
7668 Crop::no_crop(),
7669 );
7670 result.unwrap();
7671
7672 let (result, _src, dst_direct) = convert_img(
7673 &mut cpu_converter,
7674 src,
7675 dst_direct,
7676 Rotation::None,
7677 Flip::None,
7678 Crop::no_crop(),
7679 );
7680 result.unwrap();
7681
7682 compare_images(&dst_through_yuyv, &dst_direct, 0.98, function!());
7683 }
7684
7685 #[test]
7686 #[cfg(any(
7687 target_os = "linux",
7688 target_os = "macos",
7689 target_os = "ios",
7690 target_os = "android",
7691 target_os = "windows"
7692 ))]
7693 #[cfg(feature = "opengl")]
7694 #[ignore = "opengl doesn't support rendering to PixelFormat::Yuyv texture"]
7695 fn test_rgba_to_yuyv_resize_opengl() {
7696 if !is_opengl_available() {
7697 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
7698 return;
7699 }
7700
7701 if !edgefirst_tensor::is_gpu_buffer_available() {
7702 crate::test_support::report_skip(&format!(
7703 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
7704 function!()
7705 ));
7706 return;
7707 }
7708
7709 let src = load_bytes_to_tensor(
7710 1280,
7711 720,
7712 PixelFormat::Rgba,
7713 None,
7714 &edgefirst_bench::testdata::read("camera720p.rgba"),
7715 )
7716 .unwrap();
7717
7718 let (dst_width, dst_height) = (640, 360);
7719
7720 let dst = TensorDyn::image(
7721 dst_width,
7722 dst_height,
7723 PixelFormat::Yuyv,
7724 DType::U8,
7725 Some(TensorMemory::DmaBuf),
7726 edgefirst_tensor::CpuAccess::ReadWrite,
7727 )
7728 .unwrap();
7729
7730 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
7731
7732 let (result, src, dst) = convert_img(
7733 &mut gl_converter,
7734 src,
7735 dst,
7736 Rotation::None,
7737 Flip::None,
7738 Crop::letterbox([255, 255, 255, 255]),
7739 );
7740 result.unwrap();
7741
7742 std::fs::write(
7743 "rgba_to_yuyv_opengl.yuyv",
7744 dst.as_typed::<u8>().unwrap().map().unwrap().as_slice(),
7745 )
7746 .unwrap();
7747 let cpu_dst = TensorDyn::image(
7748 dst_width,
7749 dst_height,
7750 PixelFormat::Yuyv,
7751 DType::U8,
7752 Some(TensorMemory::DmaBuf),
7753 edgefirst_tensor::CpuAccess::ReadWrite,
7754 )
7755 .unwrap();
7756 let (result, _src, cpu_dst) = convert_img(
7757 &mut CPUProcessor::new(),
7758 src,
7759 cpu_dst,
7760 Rotation::None,
7761 Flip::None,
7762 Crop::no_crop(),
7763 );
7764 result.unwrap();
7765
7766 compare_images_convert_to_rgb(&dst, &cpu_dst, 0.98, function!());
7767 }
7768
7769 #[test]
7770 #[cfg(target_os = "linux")]
7771 fn test_rgba_to_yuyv_resize_g2d() {
7772 if !is_g2d_available() {
7773 crate::test_support::report_skip(
7774 "test_rgba_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available",
7775 );
7776 return;
7777 }
7778 if !is_dma_available() {
7779 crate::test_support::report_skip("test_rgba_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
7780 return;
7781 }
7782
7783 let src = load_bytes_to_tensor(
7784 1280,
7785 720,
7786 PixelFormat::Rgba,
7787 Some(TensorMemory::DmaBuf),
7788 &edgefirst_bench::testdata::read("camera720p.rgba"),
7789 )
7790 .unwrap();
7791
7792 let (dst_width, dst_height) = (1280, 720);
7793
7794 let cpu_dst = TensorDyn::image(
7795 dst_width,
7796 dst_height,
7797 PixelFormat::Yuyv,
7798 DType::U8,
7799 Some(TensorMemory::DmaBuf),
7800 edgefirst_tensor::CpuAccess::ReadWrite,
7801 )
7802 .unwrap();
7803
7804 let g2d_dst = TensorDyn::image(
7805 dst_width,
7806 dst_height,
7807 PixelFormat::Yuyv,
7808 DType::U8,
7809 Some(TensorMemory::DmaBuf),
7810 edgefirst_tensor::CpuAccess::ReadWrite,
7811 )
7812 .unwrap();
7813
7814 let mut g2d_converter = G2DProcessor::new().unwrap();
7815 let crop = Crop::new();
7816
7817 g2d_dst
7818 .as_typed::<u8>()
7819 .unwrap()
7820 .map()
7821 .unwrap()
7822 .as_mut_slice()
7823 .fill(128);
7824 let (result, src, g2d_dst) = convert_img(
7825 &mut g2d_converter,
7826 src,
7827 g2d_dst,
7828 Rotation::None,
7829 Flip::None,
7830 crop,
7831 );
7832 result.unwrap();
7833
7834 let cpu_dst_img = cpu_dst;
7835 cpu_dst_img
7836 .as_typed::<u8>()
7837 .unwrap()
7838 .map()
7839 .unwrap()
7840 .as_mut_slice()
7841 .fill(128);
7842 let (result, _src, cpu_dst) = convert_img(
7843 &mut CPUProcessor::new(),
7844 src,
7845 cpu_dst_img,
7846 Rotation::None,
7847 Flip::None,
7848 crop,
7849 );
7850 result.unwrap();
7851
7852 compare_images_convert_to_rgb(&cpu_dst, &g2d_dst, 0.98, function!());
7853 }
7854
7855 #[test]
7856 fn test_yuyv_to_rgba_cpu() {
7857 let file = edgefirst_bench::testdata::read("camera720p.yuyv").to_vec();
7858 let src = TensorDyn::image(
7859 1280,
7860 720,
7861 PixelFormat::Yuyv,
7862 DType::U8,
7863 None,
7864 edgefirst_tensor::CpuAccess::ReadWrite,
7865 )
7866 .unwrap();
7867 src.as_typed::<u8>()
7868 .unwrap()
7869 .map()
7870 .unwrap()
7871 .as_mut_slice()
7872 .copy_from_slice(&file);
7873
7874 let dst = TensorDyn::image(
7875 1280,
7876 720,
7877 PixelFormat::Rgba,
7878 DType::U8,
7879 None,
7880 edgefirst_tensor::CpuAccess::ReadWrite,
7881 )
7882 .unwrap();
7883 let mut cpu_converter = CPUProcessor::new();
7884
7885 let (result, _src, dst) = convert_img(
7886 &mut cpu_converter,
7887 src,
7888 dst,
7889 Rotation::None,
7890 Flip::None,
7891 Crop::no_crop(),
7892 );
7893 result.unwrap();
7894
7895 let target_image = TensorDyn::image(
7896 1280,
7897 720,
7898 PixelFormat::Rgba,
7899 DType::U8,
7900 None,
7901 edgefirst_tensor::CpuAccess::ReadWrite,
7902 )
7903 .unwrap();
7904 target_image
7905 .as_typed::<u8>()
7906 .unwrap()
7907 .map()
7908 .unwrap()
7909 .as_mut_slice()
7910 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
7911
7912 compare_images(&dst, &target_image, 0.98, function!());
7915 }
7916
7917 #[test]
7918 fn test_yuyv_to_rgb_cpu() {
7919 let file = edgefirst_bench::testdata::read("camera720p.yuyv").to_vec();
7920 let src = TensorDyn::image(
7921 1280,
7922 720,
7923 PixelFormat::Yuyv,
7924 DType::U8,
7925 None,
7926 edgefirst_tensor::CpuAccess::ReadWrite,
7927 )
7928 .unwrap();
7929 src.as_typed::<u8>()
7930 .unwrap()
7931 .map()
7932 .unwrap()
7933 .as_mut_slice()
7934 .copy_from_slice(&file);
7935
7936 let dst = TensorDyn::image(
7937 1280,
7938 720,
7939 PixelFormat::Rgb,
7940 DType::U8,
7941 None,
7942 edgefirst_tensor::CpuAccess::ReadWrite,
7943 )
7944 .unwrap();
7945 let mut cpu_converter = CPUProcessor::new();
7946
7947 let (result, _src, dst) = convert_img(
7948 &mut cpu_converter,
7949 src,
7950 dst,
7951 Rotation::None,
7952 Flip::None,
7953 Crop::no_crop(),
7954 );
7955 result.unwrap();
7956
7957 let target_image = TensorDyn::image(
7958 1280,
7959 720,
7960 PixelFormat::Rgb,
7961 DType::U8,
7962 None,
7963 edgefirst_tensor::CpuAccess::ReadWrite,
7964 )
7965 .unwrap();
7966 target_image
7967 .as_typed::<u8>()
7968 .unwrap()
7969 .map()
7970 .unwrap()
7971 .as_mut_slice()
7972 .as_chunks_mut::<3>()
7973 .0
7974 .iter_mut()
7975 .zip(
7976 edgefirst_bench::testdata::read("camera720p.rgba")
7977 .as_chunks::<4>()
7978 .0,
7979 )
7980 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
7981
7982 compare_images(&dst, &target_image, 0.98, function!());
7985 }
7986
7987 #[test]
7988 #[cfg(target_os = "linux")]
7989 fn test_yuyv_to_rgba_g2d() {
7990 if !is_g2d_available() {
7991 crate::test_support::report_skip(
7992 "test_yuyv_to_rgba_g2d - G2D library (libg2d.so.2) not available",
7993 );
7994 return;
7995 }
7996 if !is_dma_available() {
7997 crate::test_support::report_skip("test_yuyv_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
7998 return;
7999 }
8000
8001 let src = load_bytes_to_tensor(
8002 1280,
8003 720,
8004 PixelFormat::Yuyv,
8005 None,
8006 &edgefirst_bench::testdata::read("camera720p.yuyv"),
8007 )
8008 .unwrap();
8009
8010 let dst = TensorDyn::image(
8011 1280,
8012 720,
8013 PixelFormat::Rgba,
8014 DType::U8,
8015 Some(TensorMemory::DmaBuf),
8016 edgefirst_tensor::CpuAccess::ReadWrite,
8017 )
8018 .unwrap();
8019 let mut g2d_converter = G2DProcessor::new().unwrap();
8020
8021 let (result, _src, dst) = convert_img(
8022 &mut g2d_converter,
8023 src,
8024 dst,
8025 Rotation::None,
8026 Flip::None,
8027 Crop::no_crop(),
8028 );
8029 result.unwrap();
8030
8031 let target_image = TensorDyn::image(
8032 1280,
8033 720,
8034 PixelFormat::Rgba,
8035 DType::U8,
8036 None,
8037 edgefirst_tensor::CpuAccess::ReadWrite,
8038 )
8039 .unwrap();
8040 target_image
8041 .as_typed::<u8>()
8042 .unwrap()
8043 .map()
8044 .unwrap()
8045 .as_mut_slice()
8046 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
8047
8048 compare_images(&dst, &target_image, 0.98, function!());
8052 }
8053
8054 #[test]
8055 #[cfg(any(
8056 target_os = "linux",
8057 target_os = "macos",
8058 target_os = "ios",
8059 target_os = "android",
8060 target_os = "windows"
8061 ))]
8062 #[cfg(feature = "opengl")]
8063 fn test_yuyv_to_rgba_opengl() {
8064 if !is_opengl_available() {
8065 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
8066 return;
8067 }
8068 if !edgefirst_tensor::is_gpu_buffer_available() {
8071 crate::test_support::report_skip(&format!(
8072 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
8073 function!()
8074 ));
8075 return;
8076 }
8077
8078 let src = load_bytes_to_tensor(
8079 1280,
8080 720,
8081 PixelFormat::Yuyv,
8082 Some(TensorMemory::DmaBuf),
8083 &edgefirst_bench::testdata::read("camera720p.yuyv"),
8084 )
8085 .unwrap();
8086
8087 let dst = TensorDyn::image(
8088 1280,
8089 720,
8090 PixelFormat::Rgba,
8091 DType::U8,
8092 Some(TensorMemory::DmaBuf),
8093 edgefirst_tensor::CpuAccess::ReadWrite,
8094 )
8095 .unwrap();
8096 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
8097
8098 let (result, _src, dst) = convert_img(
8099 &mut gl_converter,
8100 src,
8101 dst,
8102 Rotation::None,
8103 Flip::None,
8104 Crop::no_crop(),
8105 );
8106 if let Err(crate::Error::NotSupported(msg)) = &result {
8109 crate::test_support::report_skip(&format!("{} — {msg}", function!()));
8110 return;
8111 }
8112 result.unwrap();
8113
8114 let target_image = TensorDyn::image(
8115 1280,
8116 720,
8117 PixelFormat::Rgba,
8118 DType::U8,
8119 None,
8120 edgefirst_tensor::CpuAccess::ReadWrite,
8121 )
8122 .unwrap();
8123 target_image
8124 .as_typed::<u8>()
8125 .unwrap()
8126 .map()
8127 .unwrap()
8128 .as_mut_slice()
8129 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
8130
8131 compare_images(&dst, &target_image, 0.98, function!());
8135 }
8136
8137 #[test]
8147 #[cfg(target_os = "macos")]
8148 #[cfg(feature = "opengl")]
8149 fn test_grey_r8_iosurface_to_rgba_opengl_macos() {
8150 let mut proc = match GLProcessorThreaded::new(None) {
8151 Ok(p) => p,
8152 Err(e) => {
8153 crate::test_support::report_skip(&format!(
8154 "{} — GL engine init failed ({e:?})",
8155 function!()
8156 ));
8157 return;
8158 }
8159 };
8160
8161 let (w, h) = (16usize, 16usize);
8162 let src = TensorDyn::image(
8163 w,
8164 h,
8165 PixelFormat::Grey,
8166 DType::U8,
8167 Some(TensorMemory::DmaBuf),
8168 edgefirst_tensor::CpuAccess::ReadWrite,
8169 )
8170 .expect("GREY IOSurface (R8/L008) should allocate — proves the FourCC mapping");
8171 {
8173 let su8 = src.as_typed::<u8>().unwrap();
8174 let stride = src
8175 .as_typed::<u8>()
8176 .unwrap()
8177 .effective_row_stride()
8178 .unwrap();
8179 let mut m = su8.map().unwrap();
8180 let buf = m.as_mut_slice();
8181 for y in 0..h {
8182 for x in 0..w {
8183 buf[y * stride + x] = ((x * 13 + y * 7) & 0xff) as u8;
8184 }
8185 }
8186 }
8187
8188 let dst = TensorDyn::image(
8189 w,
8190 h,
8191 PixelFormat::Rgba,
8192 DType::U8,
8193 Some(TensorMemory::DmaBuf),
8194 edgefirst_tensor::CpuAccess::ReadWrite,
8195 )
8196 .unwrap();
8197 let (result, src_back, dst) = convert_img(
8198 &mut proc,
8199 src,
8200 dst,
8201 Rotation::None,
8202 Flip::None,
8203 Crop::no_crop(),
8204 );
8205 result.expect("GREY(R8 IOSurface) → RGBA must convert on ANGLE (R8 binding works)");
8206
8207 let src_stride = src_back
8208 .as_typed::<u8>()
8209 .unwrap()
8210 .effective_row_stride()
8211 .unwrap();
8212 let src_map = src_back.as_typed::<u8>().unwrap().map().unwrap();
8213 let sbytes = src_map.as_slice();
8214 let dst_stride = dst
8215 .as_typed::<u8>()
8216 .unwrap()
8217 .effective_row_stride()
8218 .unwrap();
8219 let dst_map = dst.as_typed::<u8>().unwrap().map().unwrap();
8220 let dbytes = dst_map.as_slice();
8221 for y in 0..h {
8222 for x in 0..w {
8223 let yv = sbytes[y * src_stride + x] as i16;
8224 let p = y * dst_stride + x * 4;
8225 for c in 0..3 {
8226 assert!(
8227 (dbytes[p + c] as i16 - yv).abs() <= 2,
8228 "pixel ({x},{y}) ch{c} = {} expected ~{yv} (GREY→RGB identity)",
8229 dbytes[p + c]
8230 );
8231 }
8232 }
8233 }
8234 }
8235
8236 #[test]
8242 #[cfg(target_os = "macos")]
8243 #[cfg(feature = "opengl")]
8244 fn test_nv12_to_planar_f16_two_pass_opengl_macos() {
8245 let mut gpu = match GLProcessorThreaded::new(None) {
8246 Ok(p) => p,
8247 Err(e) => {
8248 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8249 return;
8250 }
8251 };
8252 let (w, h) = (64usize, 64usize);
8253 let src = match TensorDyn::image(
8254 w,
8255 h,
8256 PixelFormat::Nv12,
8257 DType::U8,
8258 Some(TensorMemory::DmaBuf),
8259 edgefirst_tensor::CpuAccess::ReadWrite,
8260 ) {
8261 Ok(t) => t,
8262 Err(e) => {
8263 crate::test_support::report_skip(&format!(
8264 "{} — NV12 IOSurface alloc: {e:?}",
8265 function!()
8266 ));
8267 return;
8268 }
8269 };
8270 src.as_typed::<u8>()
8271 .unwrap()
8272 .map()
8273 .unwrap()
8274 .as_mut_slice()
8275 .fill(128); let dst = match TensorDyn::image(
8278 w,
8279 h,
8280 PixelFormat::PlanarRgb,
8281 DType::F16,
8282 Some(TensorMemory::DmaBuf),
8283 edgefirst_tensor::CpuAccess::ReadWrite,
8284 ) {
8285 Ok(t) => t,
8286 Err(e) => {
8287 crate::test_support::report_skip(&format!(
8288 "{} — F16 PlanarRgb IOSurface: {e:?}",
8289 function!()
8290 ));
8291 return;
8292 }
8293 };
8294 let mut dst = dst;
8295 if let Err(e) = ImageProcessorTrait::convert(
8297 &mut gpu,
8298 &src,
8299 &mut dst,
8300 Rotation::None,
8301 Flip::None,
8302 Crop::no_crop(),
8303 ) {
8304 crate::test_support::report_skip(&format!(
8308 "{} — NV12→PlanarRgb F16 not available ({e:?})",
8309 function!()
8310 ));
8311 return;
8312 }
8313 let dt = dst.as_typed::<half::f16>().expect("dst is F16 PlanarRgb");
8314 let map = dt.map().unwrap();
8315 let vals = map.as_slice();
8316 let mut checked = 0usize;
8319 for &v in vals.iter() {
8320 let f = f32::from(v);
8321 assert!(
8322 (0.40..=0.60).contains(&f),
8323 "planar F16 value {f} not ~0.5 for neutral-grey NV12"
8324 );
8325 checked += 1;
8326 }
8327 assert!(
8328 checked >= w * h * 3,
8329 "expected >= 3 planes of samples, got {checked}"
8330 );
8331 }
8332
8333 #[test]
8341 #[cfg(target_os = "macos")]
8342 #[cfg(feature = "opengl")]
8343 fn test_nv12_to_planar_f16_two_pass_pool_opengl_macos() {
8344 let mut gpu = match GLProcessorThreaded::new(None) {
8345 Ok(p) => p,
8346 Err(e) => {
8347 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8348 return;
8349 }
8350 };
8351 let (fw, fh) = (96usize, 64usize);
8353 let (pool_w, pool_h) = (256usize, 768usize);
8354 let (model_w, model_h) = (128usize, 128usize);
8355
8356 let mut src = match TensorDyn::image(
8357 pool_w,
8358 pool_h,
8359 PixelFormat::Grey,
8360 DType::U8,
8361 Some(TensorMemory::DmaBuf),
8362 edgefirst_tensor::CpuAccess::ReadWrite,
8363 ) {
8364 Ok(t) => t,
8365 Err(e) => {
8366 crate::test_support::report_skip(&format!(
8367 "{} — R8 pool alloc: {e:?}",
8368 function!()
8369 ));
8370 return;
8371 }
8372 };
8373 src.configure_image(fw, fh, PixelFormat::Nv12)
8374 .unwrap_or_else(|e| panic!("configure_image NV12 on pool: {e}"));
8375 let stride = src
8376 .as_typed::<u8>()
8377 .unwrap()
8378 .effective_row_stride()
8379 .unwrap();
8380 src.as_typed::<u8>()
8381 .unwrap()
8382 .map()
8383 .unwrap()
8384 .as_mut_slice()
8385 .fill(128); let mut dst = match TensorDyn::image(
8388 model_w,
8389 model_h,
8390 PixelFormat::PlanarRgb,
8391 DType::F16,
8392 Some(TensorMemory::DmaBuf),
8393 edgefirst_tensor::CpuAccess::ReadWrite,
8394 ) {
8395 Ok(t) => t,
8396 Err(e) => {
8397 crate::test_support::report_skip(&format!(
8398 "{} — F16 PlanarRgb dst: {e:?}",
8399 function!()
8400 ));
8401 return;
8402 }
8403 };
8404
8405 let _ = model_w;
8407 let crop = Crop::new()
8408 .with_source(Some(Region::new(0, 0, fw, fh)))
8409 .with_fit(Fit::Letterbox {
8410 pad: [0, 0, 0, 255],
8411 });
8412 if let Err(e) =
8413 ImageProcessorTrait::convert(&mut gpu, &src, &mut dst, Rotation::None, Flip::None, crop)
8414 {
8415 crate::test_support::report_skip(&format!(
8416 "{} — NV12→PlanarRgb F16 unavailable ({e:?})",
8417 function!()
8418 ));
8419 return;
8420 }
8421 let _ = stride;
8422 let dt = dst.as_typed::<half::f16>().expect("dst F16");
8425 let map = dt.map().unwrap();
8426 let any_half = map.as_slice().iter().any(|&v| {
8427 let f = f32::from(v);
8428 (0.40..=0.60).contains(&f)
8429 });
8430 assert!(any_half, "expected ~0.5 grey samples in the letterbox band");
8431 }
8432
8433 #[test]
8439 #[cfg(target_os = "macos")]
8440 #[cfg(feature = "opengl")]
8441 fn test_nv12_to_planar_f16_cross_thread_opengl_macos() {
8442 use std::sync::mpsc;
8443 let mut proc = match ImageProcessor::new() {
8446 Ok(p) => p,
8447 Err(e) => {
8448 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8449 return;
8450 }
8451 };
8452 let (fw, fh) = (96usize, 64usize);
8453 let mut src = match TensorDyn::image(
8454 256,
8455 768,
8456 PixelFormat::Grey,
8457 DType::U8,
8458 Some(TensorMemory::DmaBuf),
8459 edgefirst_tensor::CpuAccess::ReadWrite,
8460 ) {
8461 Ok(t) => t,
8462 Err(e) => {
8463 crate::test_support::report_skip(&format!("{} — pool: {e:?}", function!()));
8464 return;
8465 }
8466 };
8467 src.configure_image(fw, fh, PixelFormat::Nv12).unwrap();
8468 src.as_typed::<u8>()
8469 .unwrap()
8470 .map()
8471 .unwrap()
8472 .as_mut_slice()
8473 .fill(128);
8474 let mut dst = match TensorDyn::image(
8475 128,
8476 128,
8477 PixelFormat::PlanarRgb,
8478 DType::F16,
8479 Some(TensorMemory::DmaBuf),
8480 edgefirst_tensor::CpuAccess::ReadWrite,
8481 ) {
8482 Ok(t) => t,
8483 Err(e) => {
8484 crate::test_support::report_skip(&format!("{} — dst: {e:?}", function!()));
8485 return;
8486 }
8487 };
8488 let crop = Crop::new().with_source(Some(Region::new(0, 0, fw, fh)));
8489
8490 let (tx, rx) = mpsc::channel::<bool>();
8493 let worker = std::thread::spawn(move || {
8494 let _ = ImageProcessorTrait::convert(
8495 &mut proc,
8496 &src,
8497 &mut dst,
8498 Rotation::None,
8499 Flip::None,
8500 crop,
8501 );
8502 let _ = tx.send(true);
8503 });
8504 match rx.recv_timeout(std::time::Duration::from_secs(20)) {
8505 Ok(_) => { let _ = worker.join(); }
8506 Err(_) => panic!(
8507 "cross-thread NV12→PlanarRgb convert HUNG (>20s) — reproduces the orchestrator deadlock"
8508 ),
8509 }
8510 }
8511
8512 #[test]
8519 #[cfg(target_os = "macos")]
8520 #[cfg(feature = "opengl")]
8521 fn test_nv_to_planar_f16_varying_sizes_no_leak_opengl_macos() {
8522 let mut gpu = match GLProcessorThreaded::new(None) {
8523 Ok(p) => p,
8524 Err(e) => {
8525 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8526 return;
8527 }
8528 };
8529 let (max_w, max_h) = (640usize, 640usize);
8532 let depth = 4usize;
8533 let mut srcs = Vec::new();
8534 let mut dsts = Vec::new();
8535 for _ in 0..depth {
8536 srcs.push(
8537 match TensorDyn::image(
8538 max_w,
8539 max_h * 3,
8540 PixelFormat::Grey,
8541 DType::U8,
8542 Some(TensorMemory::DmaBuf),
8543 edgefirst_tensor::CpuAccess::ReadWrite,
8544 ) {
8545 Ok(t) => t,
8546 Err(e) => {
8547 crate::test_support::report_skip(&format!("{} — pool: {e:?}", function!()));
8548 return;
8549 }
8550 },
8551 );
8552 dsts.push(
8553 match TensorDyn::image(
8554 640,
8555 640,
8556 PixelFormat::PlanarRgb,
8557 DType::F16,
8558 Some(TensorMemory::DmaBuf),
8559 edgefirst_tensor::CpuAccess::ReadWrite,
8560 ) {
8561 Ok(t) => t,
8562 Err(e) => {
8563 crate::test_support::report_skip(&format!("{} — dst: {e:?}", function!()));
8564 return;
8565 }
8566 },
8567 );
8568 }
8569 let sizes = [
8571 (640, 480),
8572 (500, 375),
8573 (640, 427),
8574 (333, 500),
8575 (480, 640),
8576 (612, 612),
8577 (428, 640),
8578 (576, 432),
8579 ];
8580 let mut first_ms = 0f64;
8581 let mut last_ms = 0f64;
8582 let iters = 40usize;
8583 for i in 0..iters {
8584 let (fw, fh) = sizes[i % sizes.len()];
8585 let src = &mut srcs[i % depth];
8586 let dst = &mut dsts[i % depth];
8587 src.configure_image(fw, fh, PixelFormat::Nv24).unwrap();
8588 src.as_typed::<u8>()
8589 .unwrap()
8590 .map()
8591 .unwrap()
8592 .as_mut_slice()
8593 .fill(128);
8594 let crop = Crop::new().with_source(Some(Region::new(0, 0, fw, fh)));
8595 let t0 = std::time::Instant::now();
8596 ImageProcessorTrait::convert(&mut gpu, src, dst, Rotation::None, Flip::None, crop)
8597 .unwrap_or_else(|e| panic!("convert iter {i} ({fw}×{fh}): {e}"));
8598 let ms = t0.elapsed().as_secs_f64() * 1e3;
8599 if i == 2 {
8600 first_ms = ms;
8601 }
8602 if i == iters - 1 {
8603 last_ms = ms;
8604 }
8605 }
8606 eprintln!("first={first_ms:.2}ms last={last_ms:.2}ms");
8607 assert!(
8608 last_ms < first_ms * 5.0 + 5.0,
8609 "convert latency ran away: first {first_ms:.2}ms → last {last_ms:.2}ms (intermediate/pbuffer leak)"
8610 );
8611 }
8612
8613 #[test]
8620 #[cfg(target_os = "macos")]
8621 #[cfg(feature = "opengl")]
8622 fn test_nv12_nv16_nv24_to_rgba_opengl_macos() {
8623 let mut gpu = match GLProcessorThreaded::new(None) {
8624 Ok(p) => p,
8625 Err(e) => {
8626 crate::test_support::report_skip(&format!(
8627 "{} — GL engine init failed ({e:?})",
8628 function!()
8629 ));
8630 return;
8631 }
8632 };
8633 let mut cpu = CPUProcessor::new();
8634
8635 let fill = |buf: &mut [u8], stride: usize, fmt: PixelFormat, w: usize, h: usize| {
8647 for y in 0..h {
8648 for x in 0..w {
8649 buf[y * stride + x] = ((x * 9 + y * 5) & 0xff) as u8;
8650 }
8651 }
8652 let (cw, ch, uv_grid_rows) = match fmt {
8653 PixelFormat::Nv12 => (w / 2, h / 2, 1usize),
8654 PixelFormat::Nv16 => (w / 2, h, 1usize),
8655 _ => (w, h, 2usize), };
8657 let uv_plane = h * stride;
8658 for cy in 0..ch {
8659 for cx in 0..cw {
8660 let off = uv_plane + cy * uv_grid_rows * stride + cx * 2;
8661 buf[off] = ((cx * 11 + 30) & 0xff) as u8;
8662 buf[off + 1] = ((cy * 7 + 200) & 0xff) as u8;
8663 }
8664 }
8665 };
8666
8667 for fmt in [PixelFormat::Nv12, PixelFormat::Nv16, PixelFormat::Nv24] {
8668 for (w, h) in [
8669 (16usize, 16usize), (15, 16), (16, 15), ] {
8673 let mem = TensorDyn::image(
8674 w,
8675 h,
8676 fmt,
8677 DType::U8,
8678 None,
8679 edgefirst_tensor::CpuAccess::ReadWrite,
8680 )
8681 .unwrap();
8682 let mem_stride = mem
8683 .as_typed::<u8>()
8684 .unwrap()
8685 .effective_row_stride()
8686 .unwrap();
8687 fill(
8688 mem.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
8689 mem_stride,
8690 fmt,
8691 w,
8692 h,
8693 );
8694 let cpu_dst = TensorDyn::image(
8695 w,
8696 h,
8697 PixelFormat::Rgba,
8698 DType::U8,
8699 None,
8700 edgefirst_tensor::CpuAccess::ReadWrite,
8701 )
8702 .unwrap();
8703 let (r, _s, cpu_dst) = convert_img(
8704 &mut cpu,
8705 mem,
8706 cpu_dst,
8707 Rotation::None,
8708 Flip::None,
8709 Crop::no_crop(),
8710 );
8711 r.unwrap_or_else(|e| panic!("CPU {fmt:?}->{w}x{h}->RGBA: {e}"));
8712
8713 let ios = TensorDyn::image(
8714 w,
8715 h,
8716 fmt,
8717 DType::U8,
8718 Some(TensorMemory::DmaBuf),
8719 edgefirst_tensor::CpuAccess::ReadWrite,
8720 )
8721 .unwrap_or_else(|e| panic!("{fmt:?} {w}x{h} IOSurface alloc: {e}"));
8722 let ios_stride = ios
8723 .as_typed::<u8>()
8724 .unwrap()
8725 .effective_row_stride()
8726 .unwrap();
8727 fill(
8728 ios.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
8729 ios_stride,
8730 fmt,
8731 w,
8732 h,
8733 );
8734 let gpu_dst = TensorDyn::image(
8735 w,
8736 h,
8737 PixelFormat::Rgba,
8738 DType::U8,
8739 Some(TensorMemory::DmaBuf),
8740 edgefirst_tensor::CpuAccess::ReadWrite,
8741 )
8742 .unwrap();
8743 let (r, _s, gpu_dst) = convert_img(
8744 &mut gpu,
8745 ios,
8746 gpu_dst,
8747 Rotation::None,
8748 Flip::None,
8749 Crop::no_crop(),
8750 );
8751 r.unwrap_or_else(|e| panic!("GPU {fmt:?}->{w}x{h}->RGBA on ANGLE: {e}"));
8752
8753 let cs = cpu_dst
8754 .as_typed::<u8>()
8755 .unwrap()
8756 .effective_row_stride()
8757 .unwrap();
8758 let cmap = cpu_dst.as_typed::<u8>().unwrap().map().unwrap();
8759 let cb = cmap.as_slice();
8760 let gs = gpu_dst
8761 .as_typed::<u8>()
8762 .unwrap()
8763 .effective_row_stride()
8764 .unwrap();
8765 let gmap = gpu_dst.as_typed::<u8>().unwrap().map().unwrap();
8766 let gb = gmap.as_slice();
8767 let mut max_d = 0i16;
8768 for y in 0..h {
8769 for x in 0..w {
8770 for c in 0..3 {
8771 let cv = cb[y * cs + x * 4 + c] as i16;
8772 let gv = gb[y * gs + x * 4 + c] as i16;
8773 max_d = max_d.max((cv - gv).abs());
8774 }
8775 }
8776 }
8777 assert!(
8778 max_d <= 3,
8779 "{fmt:?} {w}x{h}: GPU vs CPU RGBA max channel diff {max_d} > 3"
8780 );
8781 }
8782 }
8783 }
8784
8785 #[test]
8793 #[cfg(target_os = "macos")]
8794 #[cfg(feature = "opengl")]
8795 fn test_nv_to_rgba_larger_pool_surface_opengl_macos() {
8796 let mut gpu = match GLProcessorThreaded::new(None) {
8797 Ok(p) => p,
8798 Err(e) => {
8799 crate::test_support::report_skip(&format!(
8800 "{} — GL engine init failed ({e:?})",
8801 function!()
8802 ));
8803 return;
8804 }
8805 };
8806 let mut cpu = CPUProcessor::new();
8807 let (pool_w, pool_h) = (256usize, 256usize);
8810
8811 let fill = |buf: &mut [u8], stride: usize, fmt: PixelFormat, w: usize, h: usize| {
8815 for y in 0..h {
8816 for x in 0..w {
8817 buf[y * stride + x] = ((x * 9 + y * 5) & 0xff) as u8;
8818 }
8819 }
8820 let (cw, ch, uv_grid_rows) = match fmt {
8821 PixelFormat::Nv12 => (w / 2, h / 2, 1usize),
8822 PixelFormat::Nv16 => (w / 2, h, 1usize),
8823 _ => (w, h, 2usize), };
8825 let uv_plane = h * stride;
8826 for cy in 0..ch {
8827 for cx in 0..cw {
8828 let off = uv_plane + cy * uv_grid_rows * stride + cx * 2;
8829 buf[off] = ((cx * 11 + 30) & 0xff) as u8;
8830 buf[off + 1] = ((cy * 7 + 200) & 0xff) as u8;
8831 }
8832 }
8833 };
8834
8835 for fmt in [PixelFormat::Nv12, PixelFormat::Nv16, PixelFormat::Nv24] {
8836 for (w, h) in [
8837 (40usize, 24usize), (15, 16), (16, 15), ] {
8841 let ew = w.next_multiple_of(2);
8844
8845 let mem = TensorDyn::image(
8847 w,
8848 h,
8849 fmt,
8850 DType::U8,
8851 None,
8852 edgefirst_tensor::CpuAccess::ReadWrite,
8853 )
8854 .unwrap();
8855 let mem_stride = mem
8856 .as_typed::<u8>()
8857 .unwrap()
8858 .effective_row_stride()
8859 .unwrap();
8860 fill(
8861 mem.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
8862 mem_stride,
8863 fmt,
8864 w,
8865 h,
8866 );
8867 let cpu_dst = TensorDyn::image(
8868 w,
8869 h,
8870 PixelFormat::Rgba,
8871 DType::U8,
8872 None,
8873 edgefirst_tensor::CpuAccess::ReadWrite,
8874 )
8875 .unwrap();
8876 let (r, _s, cpu_dst) = convert_img(
8877 &mut cpu,
8878 mem,
8879 cpu_dst,
8880 Rotation::None,
8881 Flip::None,
8882 Crop::no_crop(),
8883 );
8884 r.unwrap_or_else(|e| panic!("CPU {fmt:?}->{w}x{h}->RGBA: {e}"));
8885
8886 let mut ios = match TensorDyn::image(
8890 pool_w,
8891 pool_h,
8892 PixelFormat::Grey,
8893 DType::U8,
8894 Some(TensorMemory::DmaBuf),
8895 edgefirst_tensor::CpuAccess::ReadWrite,
8896 ) {
8897 Ok(t) => t,
8898 Err(e) => {
8899 crate::test_support::report_skip(&format!(
8900 "{} — R8 pool IOSurface alloc: {e:?}",
8901 function!()
8902 ));
8903 return;
8904 }
8905 };
8906 ios.configure_image(w, h, fmt)
8907 .unwrap_or_else(|e| panic!("configure_image {fmt:?} {w}x{h} on pool: {e}"));
8908 let ios_stride = ios
8909 .as_typed::<u8>()
8910 .unwrap()
8911 .effective_row_stride()
8912 .unwrap();
8913 assert!(
8914 ios_stride > ew,
8915 "{fmt:?} {w}x{h}: pool stride {ios_stride} should exceed even width {ew} \
8916 (test must exercise padding)"
8917 );
8918 fill(
8919 ios.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
8920 ios_stride,
8921 fmt,
8922 w,
8923 h,
8924 );
8925
8926 let gpu_dst = TensorDyn::image(
8927 w,
8928 h,
8929 PixelFormat::Rgba,
8930 DType::U8,
8931 Some(TensorMemory::DmaBuf),
8932 edgefirst_tensor::CpuAccess::ReadWrite,
8933 )
8934 .unwrap();
8935 let (r, _s, gpu_dst) = convert_img(
8936 &mut gpu,
8937 ios,
8938 gpu_dst,
8939 Rotation::None,
8940 Flip::None,
8941 Crop::no_crop(),
8942 );
8943 r.unwrap_or_else(|e| {
8944 panic!("GPU {fmt:?}->{w}x{h}->RGBA (pool surface) on ANGLE: {e}")
8945 });
8946
8947 let cs = cpu_dst
8948 .as_typed::<u8>()
8949 .unwrap()
8950 .effective_row_stride()
8951 .unwrap();
8952 let cmap = cpu_dst.as_typed::<u8>().unwrap().map().unwrap();
8953 let cb = cmap.as_slice();
8954 let gs = gpu_dst
8955 .as_typed::<u8>()
8956 .unwrap()
8957 .effective_row_stride()
8958 .unwrap();
8959 let gmap = gpu_dst.as_typed::<u8>().unwrap().map().unwrap();
8960 let gb = gmap.as_slice();
8961 let mut max_d = 0i16;
8962 for y in 0..h {
8963 for x in 0..w {
8964 for c in 0..3 {
8965 let cv = cb[y * cs + x * 4 + c] as i16;
8966 let gv = gb[y * gs + x * 4 + c] as i16;
8967 max_d = max_d.max((cv - gv).abs());
8968 }
8969 }
8970 }
8971 assert!(
8972 max_d <= 3,
8973 "{fmt:?} {w}x{h}: GPU(pool surface) vs CPU RGBA max channel diff {max_d} > 3"
8974 );
8975 }
8976 }
8977 }
8978
8979 #[test]
8980 #[cfg(target_os = "macos")]
8981 #[cfg(feature = "opengl")]
8982 fn test_yuyv_to_rgba_opengl_macos() {
8983 let mut proc = match GLProcessorThreaded::new(None) {
8984 Ok(p) => p,
8985 Err(e) => {
8986 crate::test_support::report_skip(&format!(
8987 "{} — GL engine init failed ({e:?}). \
8988 Install ANGLE via `brew install startergo/angle/angle` \
8989 and re-sign per README.md § macOS GPU Acceleration to \
8990 run this test.",
8991 function!()
8992 ));
8993 return;
8994 }
8995 };
8996
8997 let src = load_bytes_to_tensor(
8998 1280,
8999 720,
9000 PixelFormat::Yuyv,
9001 Some(TensorMemory::DmaBuf),
9002 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9003 )
9004 .unwrap();
9005
9006 let dst = TensorDyn::image(
9007 1280,
9008 720,
9009 PixelFormat::Rgba,
9010 DType::U8,
9011 Some(TensorMemory::DmaBuf),
9012 edgefirst_tensor::CpuAccess::ReadWrite,
9013 )
9014 .unwrap();
9015
9016 let (result, _src, dst) = convert_img(
9017 &mut proc,
9018 src,
9019 dst,
9020 Rotation::None,
9021 Flip::None,
9022 Crop::no_crop(),
9023 );
9024 result.unwrap();
9025
9026 let target_image = TensorDyn::image(
9027 1280,
9028 720,
9029 PixelFormat::Rgba,
9030 DType::U8,
9031 None,
9032 edgefirst_tensor::CpuAccess::ReadWrite,
9033 )
9034 .unwrap();
9035 target_image
9036 .as_typed::<u8>()
9037 .unwrap()
9038 .map()
9039 .unwrap()
9040 .as_mut_slice()
9041 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
9042
9043 compare_images(&dst, &target_image, 0.98, function!());
9048 }
9049
9050 #[test]
9069 #[cfg(target_os = "macos")]
9070 #[cfg(feature = "opengl")]
9071 fn test_yuyv_to_rgba_opengl_macos_multi_resolution() {
9072 let mut proc = match GLProcessorThreaded::new(None) {
9073 Ok(p) => p,
9074 Err(e) => {
9075 crate::test_support::report_skip(&format!(
9076 "{} — GL engine init failed ({e:?})",
9077 function!()
9078 ));
9079 return;
9080 }
9081 };
9082
9083 for (w, h) in [(64usize, 32usize), (3840, 2160)] {
9084 let bytes_per_row = w * 2;
9087 let mut yuyv = vec![0u8; bytes_per_row * h];
9088 for chunk in yuyv.as_chunks_mut::<4>().0 {
9089 chunk[0] = 128; chunk[1] = 128; chunk[2] = 128; chunk[3] = 128; }
9094
9095 let src =
9096 load_bytes_to_tensor(w, h, PixelFormat::Yuyv, Some(TensorMemory::DmaBuf), &yuyv)
9097 .unwrap();
9098
9099 let dst = TensorDyn::image(
9100 w,
9101 h,
9102 PixelFormat::Rgba,
9103 DType::U8,
9104 Some(TensorMemory::DmaBuf),
9105 edgefirst_tensor::CpuAccess::ReadWrite,
9106 )
9107 .unwrap();
9108
9109 let (result, _src, dst) = convert_img(
9110 &mut proc,
9111 src,
9112 dst,
9113 Rotation::None,
9114 Flip::None,
9115 Crop::no_crop(),
9116 );
9117 result.expect("GL convert should succeed at this resolution");
9118
9119 let dst_u8 = dst.as_typed::<u8>().unwrap();
9124 let dst_map = dst_u8.map().unwrap();
9125 let dst_bytes = dst_map.as_slice();
9126 assert_eq!(dst_bytes.len(), w * h * 4, "RGBA byte count");
9127 for px in dst_bytes.as_chunks::<4>().0 {
9128 for (i, &c) in px[..3].iter().enumerate() {
9129 assert!(
9130 (120..=140).contains(&c),
9131 "{}: channel {i} = {c} (expected ~128 ±12) at {w}×{h}",
9132 function!(),
9133 );
9134 }
9135 assert_eq!(px[3], 255, "alpha must be 1.0");
9136 }
9137 }
9138 }
9139
9140 #[test]
9150 #[cfg(target_os = "macos")]
9151 #[cfg(feature = "opengl")]
9152 fn test_macos_gl_pbuffer_cache_reuses_surfaces() {
9153 let mut proc = match GLProcessorThreaded::new(None) {
9154 Ok(p) => p,
9155 Err(e) => {
9156 crate::test_support::report_skip(&format!(
9157 "{} — GL engine init failed ({e:?})",
9158 function!()
9159 ));
9160 return;
9161 }
9162 };
9163
9164 let mut yuyv = vec![0u8; 64 * 32 * 2];
9166 for chunk in yuyv.as_chunks_mut::<4>().0 {
9167 chunk[0] = 200;
9168 chunk[1] = 100;
9169 chunk[2] = 200;
9170 chunk[3] = 156;
9171 }
9172 let src =
9173 load_bytes_to_tensor(64, 32, PixelFormat::Yuyv, Some(TensorMemory::DmaBuf), &yuyv)
9174 .unwrap();
9175 let dst = TensorDyn::image(
9176 64,
9177 32,
9178 PixelFormat::Rgba,
9179 DType::U8,
9180 Some(TensorMemory::DmaBuf),
9181 edgefirst_tensor::CpuAccess::ReadWrite,
9182 )
9183 .unwrap();
9184
9185 let (r1, src, dst) = convert_img(
9186 &mut proc,
9187 src,
9188 dst,
9189 Rotation::None,
9190 Flip::None,
9191 Crop::no_crop(),
9192 );
9193 r1.unwrap();
9194 let first: Vec<u8> = dst
9195 .as_typed::<u8>()
9196 .unwrap()
9197 .map()
9198 .unwrap()
9199 .as_slice()
9200 .to_vec();
9201
9202 let (r2, _src, dst) = convert_img(
9203 &mut proc,
9204 src,
9205 dst,
9206 Rotation::None,
9207 Flip::None,
9208 Crop::no_crop(),
9209 );
9210 r2.unwrap();
9211 let second: Vec<u8> = dst
9212 .as_typed::<u8>()
9213 .unwrap()
9214 .map()
9215 .unwrap()
9216 .as_slice()
9217 .to_vec();
9218
9219 assert_eq!(first, second, "cache-hit conversion must be deterministic");
9220 }
9221
9222 #[test]
9230 #[cfg(target_os = "macos")]
9231 #[cfg(feature = "opengl")]
9232 fn test_macos_gl_pbuffer_cache_steady_state() {
9233 let mut proc = match GLProcessorThreaded::new(None) {
9234 Ok(p) => p,
9235 Err(e) => {
9236 crate::test_support::report_skip(&format!(
9237 "{} — GL engine init failed ({e:?})",
9238 function!()
9239 ));
9240 return;
9241 }
9242 };
9243
9244 let (w, h) = (64usize, 32usize);
9245 const POOL: usize = 3;
9246 const FRAMES: usize = 100;
9247
9248 let yuyv = vec![128u8; w * h * 2];
9249 let pool: Vec<TensorDyn> = (0..POOL)
9250 .map(|_| {
9251 load_bytes_to_tensor(w, h, PixelFormat::Yuyv, Some(TensorMemory::DmaBuf), &yuyv)
9252 .unwrap()
9253 })
9254 .collect();
9255 let mut dst = TensorDyn::image(
9256 w,
9257 h,
9258 PixelFormat::Rgba,
9259 DType::U8,
9260 Some(TensorMemory::DmaBuf),
9261 edgefirst_tensor::CpuAccess::ReadWrite,
9262 )
9263 .unwrap();
9264
9265 for src in pool.iter().cycle().take(POOL * 2) {
9267 proc.convert(src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
9268 .unwrap();
9269 }
9270 let warm = proc.egl_cache_stats().unwrap();
9271
9272 for src in pool.iter().cycle().take(FRAMES) {
9273 proc.convert(src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
9274 .unwrap();
9275 }
9276 let steady = proc.egl_cache_stats().unwrap();
9277
9278 assert_eq!(
9279 warm.total_misses(),
9280 steady.total_misses(),
9281 "steady-state loop created new imports (warm {warm:?}, steady {steady:?})"
9282 );
9283 let hits = |s: &GlCacheStats| s.src.hits + s.dst.hits + s.nv_r8.hits + s.nv_uv.hits;
9284 assert!(
9285 hits(&steady) - hits(&warm) >= FRAMES as u64,
9286 "expected at least {FRAMES} import-cache hits over the loop, got {}",
9287 hits(&steady) - hits(&warm)
9288 );
9289 }
9290
9291 #[test]
9303 #[cfg(target_os = "macos")]
9304 #[cfg(feature = "opengl")]
9305 fn test_macos_gl_f16_planar_is_gl_backed() {
9306 let mut proc = ImageProcessor::new().expect("ImageProcessor");
9307 let Some(ref gl) = proc.opengl else {
9308 crate::test_support::report_skip(&format!("{} — GL backend unavailable", function!()));
9309 return;
9310 };
9311 if !gl.supported_render_dtypes().f16 {
9312 crate::test_support::report_skip(&format!(
9313 "{} — configuration lacks F16 color-buffer support",
9314 function!()
9315 ));
9316 return;
9317 }
9318 let stats_before = gl.egl_cache_stats().expect("cache stats");
9319
9320 let src = TensorDyn::image(
9321 1280,
9322 720,
9323 PixelFormat::Nv12,
9324 DType::U8,
9325 Some(TensorMemory::DmaBuf),
9326 edgefirst_tensor::CpuAccess::ReadWrite,
9327 )
9328 .unwrap();
9329 {
9330 let t = src.as_typed::<u8>().unwrap();
9331 let mut m = t.map().unwrap();
9332 for (i, b) in m.as_mut_slice().iter_mut().enumerate() {
9333 *b = ((i * 31) % 211) as u8;
9334 }
9335 }
9336 let mut dst = TensorDyn::image(
9337 640,
9338 640,
9339 PixelFormat::PlanarRgb,
9340 DType::F16,
9341 Some(TensorMemory::DmaBuf),
9342 edgefirst_tensor::CpuAccess::ReadWrite,
9343 )
9344 .unwrap();
9345
9346 proc.convert(
9347 &src,
9348 &mut dst,
9349 Rotation::None,
9350 Flip::None,
9351 Crop::letterbox([114, 114, 114, 255]),
9352 )
9353 .expect("F16 capability reported but the NV12→PlanarF16 convert failed");
9354 let stats_after = proc
9355 .opengl
9356 .as_ref()
9357 .expect("GL backend present")
9358 .egl_cache_stats()
9359 .expect("cache stats");
9360 assert!(
9365 stats_after.total_misses() >= stats_before.total_misses() + 2,
9366 "convert succeeded but the GL engine did not import both the \
9367 source and the F16 destination — the work did not (fully) run \
9368 on the GL backend (silent CPU fallback); misses before={} after={}",
9369 stats_before.total_misses(),
9370 stats_after.total_misses()
9371 );
9372 }
9373
9374 #[test]
9380 #[cfg(feature = "opengl")]
9381 fn test_nv12_to_planar_f16_fused_engine_vs_cpu() {
9382 let mut gl = match ImageProcessor::with_config(ImageProcessorConfig {
9383 backend: ComputeBackend::OpenGl,
9384 ..Default::default()
9385 }) {
9386 Ok(p) if p.opengl.is_some() => p,
9387 _ => {
9388 crate::test_support::report_skip(&format!(
9389 "{} — GL backend unavailable",
9390 function!()
9391 ));
9392 return;
9393 }
9394 };
9395 if !gl
9396 .opengl
9397 .as_ref()
9398 .map(|g| g.supported_render_dtypes().f16)
9399 .unwrap_or(false)
9400 {
9401 crate::test_support::report_skip(&format!("{} — no F16 render support", function!()));
9402 return;
9403 }
9404 let mem = if edgefirst_tensor::is_gpu_buffer_available() {
9405 TensorMemory::DmaBuf
9406 } else {
9407 crate::test_support::report_skip(&format!("{} — no zero-copy buffers", function!()));
9408 return;
9409 };
9410
9411 let src = TensorDyn::image(
9412 1280,
9413 720,
9414 PixelFormat::Nv12,
9415 DType::U8,
9416 Some(mem),
9417 edgefirst_tensor::CpuAccess::ReadWrite,
9418 )
9419 .unwrap();
9420 {
9421 let t = src.as_typed::<u8>().unwrap();
9427 let mut m = t.map().unwrap();
9428 let buf = m.as_mut_slice();
9429 let (w, h) = (1280usize, 720usize);
9430 for y in 0..h {
9431 for x in 0..w {
9432 buf[y * w + x] = ((x * 255) / w) as u8; }
9434 }
9435 for y in 0..(h / 2) {
9436 for x in 0..(w / 2) {
9437 let o = h * w + y * w + 2 * x;
9438 buf[o] = ((y * 255) / (h / 2)) as u8; buf[o + 1] = (((x + y) * 255) / (w / 2 + h / 2)) as u8; }
9441 }
9442 }
9443 let crop = Crop::letterbox([114, 114, 114, 255]);
9444 let mut gl_dst = TensorDyn::image(
9445 640,
9446 640,
9447 PixelFormat::PlanarRgb,
9448 DType::F16,
9449 Some(mem),
9450 edgefirst_tensor::CpuAccess::ReadWrite,
9451 )
9452 .unwrap();
9453 match gl.opengl.as_mut().expect("GL backend present").convert(
9460 &src,
9461 &mut gl_dst,
9462 Rotation::None,
9463 Flip::None,
9464 crop,
9465 ) {
9466 Ok(()) => {}
9467 Err(crate::Error::NotSupported(msg)) => {
9468 crate::test_support::report_skip(&format!("{} — {msg}", function!()));
9469 return;
9470 }
9471 Err(e) => panic!("fused NV12→PlanarF16 GL convert: {e}"),
9472 }
9473
9474 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
9475 backend: ComputeBackend::Cpu,
9476 ..Default::default()
9477 })
9478 .unwrap();
9479 let mut cpu_dst = TensorDyn::image(
9480 640,
9481 640,
9482 PixelFormat::PlanarRgb,
9483 DType::F16,
9484 Some(TensorMemory::Mem),
9485 edgefirst_tensor::CpuAccess::ReadWrite,
9486 )
9487 .unwrap();
9488 cpu.convert(&src, &mut cpu_dst, Rotation::None, Flip::None, crop)
9489 .expect("CPU reference convert");
9490
9491 let g = gl_dst
9492 .as_typed::<half::f16>()
9493 .unwrap()
9494 .map()
9495 .unwrap()
9496 .as_slice()
9497 .to_vec();
9498 let c = cpu_dst
9499 .as_typed::<half::f16>()
9500 .unwrap()
9501 .map()
9502 .unwrap()
9503 .as_slice()
9504 .to_vec();
9505 assert_eq!(g.len(), c.len());
9506 let mut max_diff = 0.0f32;
9507 let mut max_at = 0usize;
9508 for (i, (a, b)) in g.iter().zip(c.iter()).enumerate() {
9509 let d = (a.to_f32() - b.to_f32()).abs();
9510 if d > max_diff {
9511 max_diff = d;
9512 max_at = i;
9513 }
9514 }
9515 let (plane, rem) = (max_at / (640 * 640), max_at % (640 * 640));
9517 let (row, col) = (rem / 640, rem % 640);
9518 eprintln!(
9519 "fused-vs-cpu: max_diff={max_diff} at plane={plane} row={row} col={col} \
9520 gl={} cpu={}",
9521 g[max_at].to_f32(),
9522 c[max_at].to_f32()
9523 );
9524 assert!(
9527 max_diff <= 4.0 / 255.0 + 1e-3,
9528 "fused NV12→PlanarF16 diverges from CPU reference: max_diff={max_diff}"
9529 );
9530 }
9531
9532 #[test]
9542 #[cfg(feature = "opengl")]
9543 fn test_zero_copy_src_to_mem_dst_gl_direct() {
9544 let mut proc = match ImageProcessor::new() {
9545 Ok(p) if p.opengl.is_some() => p,
9546 _ => {
9547 crate::test_support::report_skip(&format!(
9548 "{} — GL backend unavailable",
9549 function!()
9550 ));
9551 return;
9552 }
9553 };
9554 if !edgefirst_tensor::is_gpu_buffer_available() {
9555 crate::test_support::report_skip(&format!("{} — no zero-copy buffers", function!()));
9556 return;
9557 }
9558
9559 let src = TensorDyn::image(
9560 1280,
9561 720,
9562 PixelFormat::Rgba,
9563 DType::U8,
9564 Some(TensorMemory::DmaBuf),
9565 edgefirst_tensor::CpuAccess::ReadWrite,
9566 )
9567 .unwrap();
9568 {
9569 let t = src.as_typed::<u8>().unwrap();
9570 let mut m = t.map().unwrap();
9571 for (i, b) in m.as_mut_slice().iter_mut().enumerate() {
9572 *b = ((i * 31) % 211) as u8;
9573 }
9574 }
9575 let mut gl_dst = TensorDyn::image(
9576 1280,
9577 720,
9578 PixelFormat::Bgra,
9579 DType::U8,
9580 Some(TensorMemory::Mem),
9581 edgefirst_tensor::CpuAccess::ReadWrite,
9582 )
9583 .unwrap();
9584 proc.opengl
9585 .as_mut()
9586 .expect("GL backend present")
9587 .convert(
9588 &src,
9589 &mut gl_dst,
9590 Rotation::None,
9591 Flip::None,
9592 Crop::no_crop(),
9593 )
9594 .expect("zero-copy src → heap dst GL convert");
9595
9596 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
9597 backend: ComputeBackend::Cpu,
9598 ..Default::default()
9599 })
9600 .unwrap();
9601 let mut cpu_dst = TensorDyn::image(
9602 1280,
9603 720,
9604 PixelFormat::Bgra,
9605 DType::U8,
9606 Some(TensorMemory::Mem),
9607 edgefirst_tensor::CpuAccess::ReadWrite,
9608 )
9609 .unwrap();
9610 cpu.convert(
9611 &src,
9612 &mut cpu_dst,
9613 Rotation::None,
9614 Flip::None,
9615 Crop::no_crop(),
9616 )
9617 .expect("CPU reference convert");
9618
9619 let g = gl_dst
9620 .as_typed::<u8>()
9621 .unwrap()
9622 .map()
9623 .unwrap()
9624 .as_slice()
9625 .to_vec();
9626 let c = cpu_dst
9627 .as_typed::<u8>()
9628 .unwrap()
9629 .map()
9630 .unwrap()
9631 .as_slice()
9632 .to_vec();
9633 assert_eq!(g.len(), c.len());
9634 let max_diff = g
9635 .iter()
9636 .zip(c.iter())
9637 .map(|(a, b)| a.abs_diff(*b))
9638 .max()
9639 .unwrap();
9640 assert!(
9643 max_diff <= 2,
9644 "zero-copy src → heap dst diverges from CPU reference: max_diff={max_diff}"
9645 );
9646 }
9647
9648 #[test]
9649 #[cfg(target_os = "linux")]
9650 fn test_yuyv_to_rgb_g2d() {
9651 if !is_g2d_available() {
9652 crate::test_support::report_skip(
9653 "test_yuyv_to_rgb_g2d - G2D library (libg2d.so.2) not available",
9654 );
9655 return;
9656 }
9657 if !is_dma_available() {
9658 crate::test_support::report_skip("test_yuyv_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
9659 return;
9660 }
9661
9662 let src = load_bytes_to_tensor(
9663 1280,
9664 720,
9665 PixelFormat::Yuyv,
9666 None,
9667 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9668 )
9669 .unwrap();
9670
9671 let g2d_dst = TensorDyn::image(
9672 1280,
9673 720,
9674 PixelFormat::Rgb,
9675 DType::U8,
9676 Some(TensorMemory::DmaBuf),
9677 edgefirst_tensor::CpuAccess::ReadWrite,
9678 )
9679 .unwrap();
9680 let mut g2d_converter = G2DProcessor::new().unwrap();
9681
9682 let (result, src, g2d_dst) = convert_img(
9683 &mut g2d_converter,
9684 src,
9685 g2d_dst,
9686 Rotation::None,
9687 Flip::None,
9688 Crop::no_crop(),
9689 );
9690 result.unwrap();
9691
9692 let cpu_dst = TensorDyn::image(
9693 1280,
9694 720,
9695 PixelFormat::Rgb,
9696 DType::U8,
9697 None,
9698 edgefirst_tensor::CpuAccess::ReadWrite,
9699 )
9700 .unwrap();
9701 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
9702
9703 let (result, _src, cpu_dst) = convert_img(
9704 &mut cpu_converter,
9705 src,
9706 cpu_dst,
9707 Rotation::None,
9708 Flip::None,
9709 Crop::no_crop(),
9710 );
9711 result.unwrap();
9712
9713 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
9719 }
9720
9721 #[test]
9722 #[cfg(target_os = "linux")]
9723 fn test_yuyv_to_yuyv_resize_g2d() {
9724 if !is_g2d_available() {
9725 crate::test_support::report_skip(
9726 "test_yuyv_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available",
9727 );
9728 return;
9729 }
9730 if !is_dma_available() {
9731 crate::test_support::report_skip("test_yuyv_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
9732 return;
9733 }
9734
9735 let src = load_bytes_to_tensor(
9736 1280,
9737 720,
9738 PixelFormat::Yuyv,
9739 None,
9740 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9741 )
9742 .unwrap();
9743
9744 let g2d_dst = TensorDyn::image(
9745 600,
9746 400,
9747 PixelFormat::Yuyv,
9748 DType::U8,
9749 Some(TensorMemory::DmaBuf),
9750 edgefirst_tensor::CpuAccess::ReadWrite,
9751 )
9752 .unwrap();
9753 let mut g2d_converter = G2DProcessor::new().unwrap();
9754
9755 let (result, src, g2d_dst) = convert_img(
9756 &mut g2d_converter,
9757 src,
9758 g2d_dst,
9759 Rotation::None,
9760 Flip::None,
9761 Crop::no_crop(),
9762 );
9763 result.unwrap();
9764
9765 let cpu_dst = TensorDyn::image(
9766 600,
9767 400,
9768 PixelFormat::Yuyv,
9769 DType::U8,
9770 None,
9771 edgefirst_tensor::CpuAccess::ReadWrite,
9772 )
9773 .unwrap();
9774 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
9775
9776 let (result, _src, cpu_dst) = convert_img(
9777 &mut cpu_converter,
9778 src,
9779 cpu_dst,
9780 Rotation::None,
9781 Flip::None,
9782 Crop::no_crop(),
9783 );
9784 result.unwrap();
9785
9786 eprintln!(
9793 "WARNING: G2D has poor colorimetry support — YUYV resize diverges from the \
9794 CPU reference (~0.85 similarity); threshold held at 0.85, not 0.95."
9795 );
9796 compare_images_convert_to_rgb(&g2d_dst, &cpu_dst, 0.85, function!());
9797 }
9798
9799 #[test]
9800 fn test_yuyv_to_rgba_resize_cpu() {
9801 let src = load_bytes_to_tensor(
9802 1280,
9803 720,
9804 PixelFormat::Yuyv,
9805 None,
9806 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9807 )
9808 .unwrap();
9809
9810 let (dst_width, dst_height) = (960, 540);
9811
9812 let dst = TensorDyn::image(
9813 dst_width,
9814 dst_height,
9815 PixelFormat::Rgba,
9816 DType::U8,
9817 None,
9818 edgefirst_tensor::CpuAccess::ReadWrite,
9819 )
9820 .unwrap();
9821 let mut cpu_converter = CPUProcessor::new();
9822
9823 let (result, _src, dst) = convert_img(
9824 &mut cpu_converter,
9825 src,
9826 dst,
9827 Rotation::None,
9828 Flip::None,
9829 Crop::no_crop(),
9830 );
9831 result.unwrap();
9832
9833 let dst_target = TensorDyn::image(
9834 dst_width,
9835 dst_height,
9836 PixelFormat::Rgba,
9837 DType::U8,
9838 None,
9839 edgefirst_tensor::CpuAccess::ReadWrite,
9840 )
9841 .unwrap();
9842 let src_target = load_bytes_to_tensor(
9843 1280,
9844 720,
9845 PixelFormat::Rgba,
9846 None,
9847 &edgefirst_bench::testdata::read("camera720p.rgba"),
9848 )
9849 .unwrap();
9850 let (result, _src_target, dst_target) = convert_img(
9851 &mut cpu_converter,
9852 src_target,
9853 dst_target,
9854 Rotation::None,
9855 Flip::None,
9856 Crop::no_crop(),
9857 );
9858 result.unwrap();
9859
9860 compare_images(&dst, &dst_target, 0.98, function!());
9863 }
9864
9865 #[test]
9866 #[cfg(target_os = "linux")]
9867 fn test_yuyv_to_rgba_crop_flip_g2d() {
9868 if !is_g2d_available() {
9869 crate::test_support::report_skip(
9870 "test_yuyv_to_rgba_crop_flip_g2d - G2D library (libg2d.so.2) not available",
9871 );
9872 return;
9873 }
9874 if !is_dma_available() {
9875 crate::test_support::report_skip("test_yuyv_to_rgba_crop_flip_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
9876 return;
9877 }
9878
9879 let src = load_bytes_to_tensor(
9880 1280,
9881 720,
9882 PixelFormat::Yuyv,
9883 Some(TensorMemory::DmaBuf),
9884 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9885 )
9886 .unwrap();
9887
9888 let (dst_width, dst_height) = (640, 640);
9889
9890 let dst_g2d = TensorDyn::image(
9891 dst_width,
9892 dst_height,
9893 PixelFormat::Rgba,
9894 DType::U8,
9895 Some(TensorMemory::DmaBuf),
9896 edgefirst_tensor::CpuAccess::ReadWrite,
9897 )
9898 .unwrap();
9899 let mut g2d_converter = G2DProcessor::new().unwrap();
9900 let crop = Crop::new().with_source(Some(Region::new(20, 15, 400, 300)));
9901
9902 let (result, src, dst_g2d) = convert_img(
9903 &mut g2d_converter,
9904 src,
9905 dst_g2d,
9906 Rotation::None,
9907 Flip::Horizontal,
9908 crop,
9909 );
9910 result.unwrap();
9911
9912 let dst_cpu = TensorDyn::image(
9913 dst_width,
9914 dst_height,
9915 PixelFormat::Rgba,
9916 DType::U8,
9917 Some(TensorMemory::DmaBuf),
9918 edgefirst_tensor::CpuAccess::ReadWrite,
9919 )
9920 .unwrap();
9921 let mut cpu_converter = CPUProcessor::new();
9922
9923 let (result, _src, dst_cpu) = convert_img(
9924 &mut cpu_converter,
9925 src,
9926 dst_cpu,
9927 Rotation::None,
9928 Flip::Horizontal,
9929 crop,
9930 );
9931 result.unwrap();
9932 compare_images(&dst_g2d, &dst_cpu, 0.98, function!());
9938 }
9939
9940 #[test]
9941 #[cfg(any(
9942 target_os = "linux",
9943 target_os = "macos",
9944 target_os = "ios",
9945 target_os = "android",
9946 target_os = "windows"
9947 ))]
9948 #[cfg(feature = "opengl")]
9949 fn test_yuyv_to_rgba_crop_flip_opengl() {
9950 if !is_opengl_available() {
9951 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
9952 return;
9953 }
9954
9955 if !edgefirst_tensor::is_gpu_buffer_available() {
9956 crate::test_support::report_skip(&format!(
9957 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
9958 function!()
9959 ));
9960 return;
9961 }
9962
9963 let src = load_bytes_to_tensor(
9964 1280,
9965 720,
9966 PixelFormat::Yuyv,
9967 Some(TensorMemory::DmaBuf),
9968 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9969 )
9970 .unwrap();
9971
9972 let (dst_width, dst_height) = (640, 640);
9973
9974 let dst_gl = TensorDyn::image(
9975 dst_width,
9976 dst_height,
9977 PixelFormat::Rgba,
9978 DType::U8,
9979 Some(TensorMemory::DmaBuf),
9980 edgefirst_tensor::CpuAccess::ReadWrite,
9981 )
9982 .unwrap();
9983 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
9984 let crop = Crop::new().with_source(Some(Region::new(20, 15, 400, 300)));
9985
9986 let (result, src, dst_gl) = convert_img(
9987 &mut gl_converter,
9988 src,
9989 dst_gl,
9990 Rotation::None,
9991 Flip::Horizontal,
9992 crop,
9993 );
9994 if let Err(crate::Error::NotSupported(msg)) = &result {
9996 crate::test_support::report_skip(&format!("{} — {msg}", function!()));
9997 return;
9998 }
9999 result.unwrap();
10000
10001 let dst_cpu = TensorDyn::image(
10002 dst_width,
10003 dst_height,
10004 PixelFormat::Rgba,
10005 DType::U8,
10006 Some(TensorMemory::DmaBuf),
10007 edgefirst_tensor::CpuAccess::ReadWrite,
10008 )
10009 .unwrap();
10010 let mut cpu_converter = CPUProcessor::new();
10011
10012 let (result, _src, dst_cpu) = convert_img(
10013 &mut cpu_converter,
10014 src,
10015 dst_cpu,
10016 Rotation::None,
10017 Flip::Horizontal,
10018 crop,
10019 );
10020 result.unwrap();
10021 compare_images(&dst_gl, &dst_cpu, 0.98, function!());
10026 }
10027
10028 #[test]
10029 fn test_vyuy_to_rgba_cpu() {
10030 let file = edgefirst_bench::testdata::read("camera720p.vyuy").to_vec();
10031 let src = TensorDyn::image(
10032 1280,
10033 720,
10034 PixelFormat::Vyuy,
10035 DType::U8,
10036 None,
10037 edgefirst_tensor::CpuAccess::ReadWrite,
10038 )
10039 .unwrap();
10040 src.as_typed::<u8>()
10041 .unwrap()
10042 .map()
10043 .unwrap()
10044 .as_mut_slice()
10045 .copy_from_slice(&file);
10046
10047 let dst = TensorDyn::image(
10048 1280,
10049 720,
10050 PixelFormat::Rgba,
10051 DType::U8,
10052 None,
10053 edgefirst_tensor::CpuAccess::ReadWrite,
10054 )
10055 .unwrap();
10056 let mut cpu_converter = CPUProcessor::new();
10057
10058 let (result, _src, dst) = convert_img(
10059 &mut cpu_converter,
10060 src,
10061 dst,
10062 Rotation::None,
10063 Flip::None,
10064 Crop::no_crop(),
10065 );
10066 result.unwrap();
10067
10068 let target_image = TensorDyn::image(
10069 1280,
10070 720,
10071 PixelFormat::Rgba,
10072 DType::U8,
10073 None,
10074 edgefirst_tensor::CpuAccess::ReadWrite,
10075 )
10076 .unwrap();
10077 target_image
10078 .as_typed::<u8>()
10079 .unwrap()
10080 .map()
10081 .unwrap()
10082 .as_mut_slice()
10083 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
10084
10085 compare_images(&dst, &target_image, 0.98, function!());
10088 }
10089
10090 #[test]
10091 fn test_vyuy_to_rgb_cpu() {
10092 let file = edgefirst_bench::testdata::read("camera720p.vyuy").to_vec();
10093 let src = TensorDyn::image(
10094 1280,
10095 720,
10096 PixelFormat::Vyuy,
10097 DType::U8,
10098 None,
10099 edgefirst_tensor::CpuAccess::ReadWrite,
10100 )
10101 .unwrap();
10102 src.as_typed::<u8>()
10103 .unwrap()
10104 .map()
10105 .unwrap()
10106 .as_mut_slice()
10107 .copy_from_slice(&file);
10108
10109 let dst = TensorDyn::image(
10110 1280,
10111 720,
10112 PixelFormat::Rgb,
10113 DType::U8,
10114 None,
10115 edgefirst_tensor::CpuAccess::ReadWrite,
10116 )
10117 .unwrap();
10118 let mut cpu_converter = CPUProcessor::new();
10119
10120 let (result, _src, dst) = convert_img(
10121 &mut cpu_converter,
10122 src,
10123 dst,
10124 Rotation::None,
10125 Flip::None,
10126 Crop::no_crop(),
10127 );
10128 result.unwrap();
10129
10130 let target_image = TensorDyn::image(
10131 1280,
10132 720,
10133 PixelFormat::Rgb,
10134 DType::U8,
10135 None,
10136 edgefirst_tensor::CpuAccess::ReadWrite,
10137 )
10138 .unwrap();
10139 target_image
10140 .as_typed::<u8>()
10141 .unwrap()
10142 .map()
10143 .unwrap()
10144 .as_mut_slice()
10145 .as_chunks_mut::<3>()
10146 .0
10147 .iter_mut()
10148 .zip(
10149 edgefirst_bench::testdata::read("camera720p.rgba")
10150 .as_chunks::<4>()
10151 .0,
10152 )
10153 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
10154
10155 compare_images(&dst, &target_image, 0.98, function!());
10158 }
10159
10160 #[test]
10161 #[cfg(target_os = "linux")]
10162 #[ignore = "G2D does not support VYUY; re-enable when hardware support is added"]
10163 fn test_vyuy_to_rgba_g2d() {
10164 if !is_g2d_available() {
10165 crate::test_support::report_skip(
10166 "test_vyuy_to_rgba_g2d - G2D library (libg2d.so.2) not available",
10167 );
10168 return;
10169 }
10170 if !is_dma_available() {
10171 crate::test_support::report_skip("test_vyuy_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
10172 return;
10173 }
10174
10175 let src = load_bytes_to_tensor(
10176 1280,
10177 720,
10178 PixelFormat::Vyuy,
10179 None,
10180 &edgefirst_bench::testdata::read("camera720p.vyuy"),
10181 )
10182 .unwrap();
10183
10184 let dst = TensorDyn::image(
10185 1280,
10186 720,
10187 PixelFormat::Rgba,
10188 DType::U8,
10189 Some(TensorMemory::DmaBuf),
10190 edgefirst_tensor::CpuAccess::ReadWrite,
10191 )
10192 .unwrap();
10193 let mut g2d_converter = G2DProcessor::new().unwrap();
10194
10195 let (result, _src, dst) = convert_img(
10196 &mut g2d_converter,
10197 src,
10198 dst,
10199 Rotation::None,
10200 Flip::None,
10201 Crop::no_crop(),
10202 );
10203 match result {
10204 Err(Error::G2D(_)) => {
10205 crate::test_support::report_skip(
10206 "test_vyuy_to_rgba_g2d - G2D does not support PixelFormat::Vyuy format",
10207 );
10208 return;
10209 }
10210 r => r.unwrap(),
10211 }
10212
10213 let target_image = TensorDyn::image(
10214 1280,
10215 720,
10216 PixelFormat::Rgba,
10217 DType::U8,
10218 None,
10219 edgefirst_tensor::CpuAccess::ReadWrite,
10220 )
10221 .unwrap();
10222 target_image
10223 .as_typed::<u8>()
10224 .unwrap()
10225 .map()
10226 .unwrap()
10227 .as_mut_slice()
10228 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
10229
10230 compare_images(&dst, &target_image, 0.98, function!());
10234 }
10235
10236 #[test]
10237 #[cfg(target_os = "linux")]
10238 #[ignore = "G2D does not support VYUY; re-enable when hardware support is added"]
10239 fn test_vyuy_to_rgb_g2d() {
10240 if !is_g2d_available() {
10241 crate::test_support::report_skip(
10242 "test_vyuy_to_rgb_g2d - G2D library (libg2d.so.2) not available",
10243 );
10244 return;
10245 }
10246 if !is_dma_available() {
10247 crate::test_support::report_skip("test_vyuy_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
10248 return;
10249 }
10250
10251 let src = load_bytes_to_tensor(
10252 1280,
10253 720,
10254 PixelFormat::Vyuy,
10255 None,
10256 &edgefirst_bench::testdata::read("camera720p.vyuy"),
10257 )
10258 .unwrap();
10259
10260 let g2d_dst = TensorDyn::image(
10261 1280,
10262 720,
10263 PixelFormat::Rgb,
10264 DType::U8,
10265 Some(TensorMemory::DmaBuf),
10266 edgefirst_tensor::CpuAccess::ReadWrite,
10267 )
10268 .unwrap();
10269 let mut g2d_converter = G2DProcessor::new().unwrap();
10270
10271 let (result, src, g2d_dst) = convert_img(
10272 &mut g2d_converter,
10273 src,
10274 g2d_dst,
10275 Rotation::None,
10276 Flip::None,
10277 Crop::no_crop(),
10278 );
10279 match result {
10280 Err(Error::G2D(_)) => {
10281 crate::test_support::report_skip(
10282 "test_vyuy_to_rgb_g2d - G2D does not support PixelFormat::Vyuy format",
10283 );
10284 return;
10285 }
10286 r => r.unwrap(),
10287 }
10288
10289 let cpu_dst = TensorDyn::image(
10290 1280,
10291 720,
10292 PixelFormat::Rgb,
10293 DType::U8,
10294 None,
10295 edgefirst_tensor::CpuAccess::ReadWrite,
10296 )
10297 .unwrap();
10298 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
10299
10300 let (result, _src, cpu_dst) = convert_img(
10301 &mut cpu_converter,
10302 src,
10303 cpu_dst,
10304 Rotation::None,
10305 Flip::None,
10306 Crop::no_crop(),
10307 );
10308 result.unwrap();
10309
10310 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
10316 }
10317
10318 #[test]
10319 #[cfg(any(
10320 target_os = "linux",
10321 target_os = "macos",
10322 target_os = "ios",
10323 target_os = "android",
10324 target_os = "windows"
10325 ))]
10326 #[cfg(feature = "opengl")]
10327 fn test_vyuy_to_rgba_opengl() {
10328 if !is_opengl_available() {
10329 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
10330 return;
10331 }
10332 if !edgefirst_tensor::is_gpu_buffer_available() {
10333 crate::test_support::report_skip(&format!(
10334 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
10335 function!()
10336 ));
10337 return;
10338 }
10339
10340 let src = match load_bytes_to_tensor(
10344 1280,
10345 720,
10346 PixelFormat::Vyuy,
10347 Some(TensorMemory::DmaBuf),
10348 &edgefirst_bench::testdata::read("camera720p.vyuy"),
10349 ) {
10350 Ok(src) => src,
10351 Err(e) => {
10352 crate::test_support::report_skip(&format!(
10353 "{} - zero-copy VYUY source not allocatable on this platform ({e})",
10354 function!()
10355 ));
10356 return;
10357 }
10358 };
10359
10360 let dst = TensorDyn::image(
10361 1280,
10362 720,
10363 PixelFormat::Rgba,
10364 DType::U8,
10365 Some(TensorMemory::DmaBuf),
10366 edgefirst_tensor::CpuAccess::ReadWrite,
10367 )
10368 .unwrap();
10369 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
10370
10371 let (result, _src, dst) = convert_img(
10372 &mut gl_converter,
10373 src,
10374 dst,
10375 Rotation::None,
10376 Flip::None,
10377 Crop::no_crop(),
10378 );
10379 match result {
10380 Err(Error::NotSupported(_)) => {
10381 crate::test_support::report_skip(&format!(
10382 "{} - OpenGL does not support PixelFormat::Vyuy DMA format",
10383 function!()
10384 ));
10385 return;
10386 }
10387 r => r.unwrap(),
10388 }
10389
10390 let target_image = TensorDyn::image(
10391 1280,
10392 720,
10393 PixelFormat::Rgba,
10394 DType::U8,
10395 None,
10396 edgefirst_tensor::CpuAccess::ReadWrite,
10397 )
10398 .unwrap();
10399 target_image
10400 .as_typed::<u8>()
10401 .unwrap()
10402 .map()
10403 .unwrap()
10404 .as_mut_slice()
10405 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
10406
10407 compare_images(&dst, &target_image, 0.98, function!());
10411 }
10412
10413 #[test]
10414 fn test_nv12_to_rgba_cpu() {
10415 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
10416 let src = TensorDyn::image(
10417 1280,
10418 720,
10419 PixelFormat::Nv12,
10420 DType::U8,
10421 None,
10422 edgefirst_tensor::CpuAccess::ReadWrite,
10423 )
10424 .unwrap();
10425 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
10426 .copy_from_slice(&file);
10427
10428 let dst = TensorDyn::image(
10429 1280,
10430 720,
10431 PixelFormat::Rgba,
10432 DType::U8,
10433 None,
10434 edgefirst_tensor::CpuAccess::ReadWrite,
10435 )
10436 .unwrap();
10437 let mut cpu_converter = CPUProcessor::new();
10438
10439 let (result, _src, dst) = convert_img(
10440 &mut cpu_converter,
10441 src,
10442 dst,
10443 Rotation::None,
10444 Flip::None,
10445 Crop::no_crop(),
10446 );
10447 result.unwrap();
10448
10449 let target_image = crate::load_image_test_helper(
10450 &edgefirst_bench::testdata::read("zidane.jpg"),
10451 Some(PixelFormat::Rgba),
10452 None,
10453 )
10454 .unwrap();
10455
10456 compare_images(&dst, &target_image, 0.95, function!());
10461 }
10462
10463 #[test]
10464 fn test_nv12_odd_height_to_rgb_cpu() {
10465 let mut src = TensorDyn::image(
10476 8,
10477 5,
10478 PixelFormat::Nv12,
10479 DType::U8,
10480 Some(TensorMemory::Mem),
10481 edgefirst_tensor::CpuAccess::ReadWrite,
10482 )
10483 .unwrap();
10484 assert_eq!(src.shape(), &[8, 8]);
10485 assert_eq!((src.width(), src.height()), (Some(8), Some(5)));
10486 src.as_typed::<u8>()
10487 .unwrap()
10488 .map()
10489 .unwrap()
10490 .as_mut_slice()
10491 .fill(128);
10492 src.set_colorimetry(Some(
10496 edgefirst_tensor::Colorimetry::default()
10497 .with_encoding(edgefirst_tensor::ColorEncoding::Bt601)
10498 .with_range(edgefirst_tensor::ColorRange::Full),
10499 ));
10500
10501 let dst = TensorDyn::image(
10502 8,
10503 5,
10504 PixelFormat::Rgb,
10505 DType::U8,
10506 Some(TensorMemory::Mem),
10507 edgefirst_tensor::CpuAccess::ReadWrite,
10508 )
10509 .unwrap();
10510 let mut cpu_converter = CPUProcessor::new();
10511 let (result, _src, dst) = convert_img(
10512 &mut cpu_converter,
10513 src,
10514 dst,
10515 Rotation::None,
10516 Flip::None,
10517 Crop::no_crop(),
10518 );
10519 result.unwrap();
10520
10521 assert_eq!((dst.width(), dst.height()), (Some(8), Some(5)));
10522 let map = dst.as_typed::<u8>().unwrap().map().unwrap();
10523 for (i, &b) in map.as_slice().iter().enumerate() {
10524 assert!(
10525 (b as i16 - 128).abs() <= 2,
10526 "pixel byte {i} = {b}, expected ~128 for neutral-grey NV12"
10527 );
10528 }
10529 }
10530
10531 #[test]
10532 fn test_nv24_to_rgb_cpu() {
10533 let mut src = TensorDyn::image(
10539 8,
10540 4,
10541 PixelFormat::Nv24,
10542 DType::U8,
10543 Some(TensorMemory::Mem),
10544 edgefirst_tensor::CpuAccess::ReadWrite,
10545 )
10546 .unwrap();
10547 assert_eq!(src.shape(), &[12, 8]);
10548 assert_eq!((src.width(), src.height()), (Some(8), Some(4)));
10549 src.as_typed::<u8>()
10550 .unwrap()
10551 .map()
10552 .unwrap()
10553 .as_mut_slice()
10554 .fill(128);
10555 src.set_colorimetry(Some(
10558 edgefirst_tensor::Colorimetry::default()
10559 .with_encoding(edgefirst_tensor::ColorEncoding::Bt601)
10560 .with_range(edgefirst_tensor::ColorRange::Full),
10561 ));
10562
10563 let dst = TensorDyn::image(
10564 8,
10565 4,
10566 PixelFormat::Rgb,
10567 DType::U8,
10568 Some(TensorMemory::Mem),
10569 edgefirst_tensor::CpuAccess::ReadWrite,
10570 )
10571 .unwrap();
10572 let mut cpu_converter = CPUProcessor::new();
10573 let (result, _src, dst) = convert_img(
10574 &mut cpu_converter,
10575 src,
10576 dst,
10577 Rotation::None,
10578 Flip::None,
10579 Crop::no_crop(),
10580 );
10581 result.unwrap();
10582
10583 assert_eq!((dst.width(), dst.height()), (Some(8), Some(4)));
10584 let map = dst.as_typed::<u8>().unwrap().map().unwrap();
10585 for (i, &b) in map.as_slice().iter().enumerate() {
10586 assert!(
10587 (b as i16 - 128).abs() <= 2,
10588 "pixel byte {i} = {b}, expected ~128 for neutral-grey NV24"
10589 );
10590 }
10591 }
10592
10593 #[test]
10594 fn cpu_nv12_to_rgb_respects_tagged_bt2020() {
10595 fn decode_tagged(enc: edgefirst_tensor::ColorEncoding) -> [u8; 3] {
10603 let mut src = TensorDyn::image(
10604 8,
10605 4,
10606 PixelFormat::Nv12,
10607 DType::U8,
10608 Some(TensorMemory::Mem),
10609 edgefirst_tensor::CpuAccess::ReadWrite,
10610 )
10611 .unwrap();
10612 assert_eq!(src.shape(), &[6, 8]);
10614 {
10615 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
10616 let buf = map.as_mut_slice();
10617 buf[..32].fill(120); for px in buf[32..].as_chunks_mut::<2>().0 {
10619 px[0] = 180; px[1] = 64; }
10622 }
10623 src.set_colorimetry(Some(
10626 edgefirst_tensor::Colorimetry::default()
10627 .with_encoding(enc)
10628 .with_range(edgefirst_tensor::ColorRange::Limited),
10629 ));
10630 let dst = TensorDyn::image(
10631 8,
10632 4,
10633 PixelFormat::Rgb,
10634 DType::U8,
10635 Some(TensorMemory::Mem),
10636 edgefirst_tensor::CpuAccess::ReadWrite,
10637 )
10638 .unwrap();
10639 let mut cpu = CPUProcessor::new();
10640 let (result, _src, dst) = convert_img(
10641 &mut cpu,
10642 src,
10643 dst,
10644 Rotation::None,
10645 Flip::None,
10646 Crop::no_crop(),
10647 );
10648 result.unwrap();
10649 let map = dst.as_typed::<u8>().unwrap().map().unwrap();
10650 let s = map.as_slice();
10651 [s[0], s[1], s[2]]
10652 }
10653
10654 let bt601 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt601);
10655 let bt709 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt709);
10656 let bt2020 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt2020);
10657
10658 assert_ne!(
10659 bt2020, bt601,
10660 "BT.2020 must decode differently from BT.601 ({bt2020:?} vs {bt601:?})"
10661 );
10662 assert_ne!(
10663 bt2020, bt709,
10664 "BT.2020 must decode differently from BT.709 ({bt2020:?} vs {bt709:?})"
10665 );
10666 assert_ne!(
10667 bt601, bt709,
10668 "BT.601 must decode differently from BT.709 ({bt601:?} vs {bt709:?})"
10669 );
10670 }
10671
10672 #[test]
10673 fn test_nv12_to_rgb_cpu() {
10674 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
10675 let src = TensorDyn::image(
10676 1280,
10677 720,
10678 PixelFormat::Nv12,
10679 DType::U8,
10680 None,
10681 edgefirst_tensor::CpuAccess::ReadWrite,
10682 )
10683 .unwrap();
10684 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
10685 .copy_from_slice(&file);
10686
10687 let dst = TensorDyn::image(
10688 1280,
10689 720,
10690 PixelFormat::Rgb,
10691 DType::U8,
10692 None,
10693 edgefirst_tensor::CpuAccess::ReadWrite,
10694 )
10695 .unwrap();
10696 let mut cpu_converter = CPUProcessor::new();
10697
10698 let (result, _src, dst) = convert_img(
10699 &mut cpu_converter,
10700 src,
10701 dst,
10702 Rotation::None,
10703 Flip::None,
10704 Crop::no_crop(),
10705 );
10706 result.unwrap();
10707
10708 let target_image = crate::load_image_test_helper(
10709 &edgefirst_bench::testdata::read("zidane.jpg"),
10710 Some(PixelFormat::Rgb),
10711 None,
10712 )
10713 .unwrap();
10714
10715 compare_images(&dst, &target_image, 0.95, function!());
10720 }
10721
10722 #[test]
10723 fn test_nv12_to_grey_cpu() {
10724 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
10725 let src = TensorDyn::image(
10726 1280,
10727 720,
10728 PixelFormat::Nv12,
10729 DType::U8,
10730 None,
10731 edgefirst_tensor::CpuAccess::ReadWrite,
10732 )
10733 .unwrap();
10734 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
10735 .copy_from_slice(&file);
10736
10737 let dst = TensorDyn::image(
10738 1280,
10739 720,
10740 PixelFormat::Grey,
10741 DType::U8,
10742 None,
10743 edgefirst_tensor::CpuAccess::ReadWrite,
10744 )
10745 .unwrap();
10746 let mut cpu_converter = CPUProcessor::new();
10747
10748 let (result, _src, dst) = convert_img(
10749 &mut cpu_converter,
10750 src,
10751 dst,
10752 Rotation::None,
10753 Flip::None,
10754 Crop::no_crop(),
10755 );
10756 result.unwrap();
10757
10758 let target_image = crate::load_image_test_helper(
10759 &edgefirst_bench::testdata::read("zidane.jpg"),
10760 Some(PixelFormat::Grey),
10761 None,
10762 )
10763 .unwrap();
10764
10765 compare_images(&dst, &target_image, 0.95, function!());
10770 }
10771
10772 #[test]
10773 fn test_nv12_to_yuyv_cpu() {
10774 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
10775 let src = TensorDyn::image(
10776 1280,
10777 720,
10778 PixelFormat::Nv12,
10779 DType::U8,
10780 None,
10781 edgefirst_tensor::CpuAccess::ReadWrite,
10782 )
10783 .unwrap();
10784 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
10785 .copy_from_slice(&file);
10786
10787 let dst = TensorDyn::image(
10788 1280,
10789 720,
10790 PixelFormat::Yuyv,
10791 DType::U8,
10792 None,
10793 edgefirst_tensor::CpuAccess::ReadWrite,
10794 )
10795 .unwrap();
10796 let mut cpu_converter = CPUProcessor::new();
10797
10798 let (result, _src, dst) = convert_img(
10799 &mut cpu_converter,
10800 src,
10801 dst,
10802 Rotation::None,
10803 Flip::None,
10804 Crop::no_crop(),
10805 );
10806 result.unwrap();
10807
10808 let target_image = crate::load_image_test_helper(
10809 &edgefirst_bench::testdata::read("zidane.jpg"),
10810 Some(PixelFormat::Rgb),
10811 None,
10812 )
10813 .unwrap();
10814
10815 compare_images_convert_to_rgb(&dst, &target_image, 0.95, function!());
10820 }
10821
10822 #[test]
10823 fn test_cpu_resize_nv16() {
10824 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
10825 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
10826
10827 let cpu_nv16_dst = TensorDyn::image(
10828 640,
10829 640,
10830 PixelFormat::Nv16,
10831 DType::U8,
10832 None,
10833 edgefirst_tensor::CpuAccess::ReadWrite,
10834 )
10835 .unwrap();
10836 let cpu_rgb_dst = TensorDyn::image(
10837 640,
10838 640,
10839 PixelFormat::Rgb,
10840 DType::U8,
10841 None,
10842 edgefirst_tensor::CpuAccess::ReadWrite,
10843 )
10844 .unwrap();
10845 let mut cpu_converter = CPUProcessor::new();
10846 let crop = Crop::letterbox([255, 128, 0, 255]);
10847
10848 let (result, src, cpu_nv16_dst) = convert_img(
10849 &mut cpu_converter,
10850 src,
10851 cpu_nv16_dst,
10852 Rotation::None,
10853 Flip::None,
10854 crop,
10855 );
10856 result.unwrap();
10857
10858 let (result, _src, cpu_rgb_dst) = convert_img(
10859 &mut cpu_converter,
10860 src,
10861 cpu_rgb_dst,
10862 Rotation::None,
10863 Flip::None,
10864 crop,
10865 );
10866 result.unwrap();
10867 compare_images_convert_to_rgb(
10868 &cpu_nv16_dst,
10869 &cpu_rgb_dst,
10870 yuv_threshold(0.99),
10871 function!(),
10872 );
10873 }
10874
10875 fn yuv_threshold(balanced: f64) -> f64 {
10892 #[cfg(target_arch = "aarch64")]
10893 {
10894 if !std::arch::is_aarch64_feature_detected!("rdm") {
10895 return balanced - 0.01;
10896 }
10897 }
10898 balanced
10899 }
10900
10901 fn load_bytes_to_tensor(
10902 width: usize,
10903 height: usize,
10904 format: PixelFormat,
10905 memory: Option<TensorMemory>,
10906 bytes: &[u8],
10907 ) -> Result<TensorDyn, Error> {
10908 let src = TensorDyn::image(
10909 width,
10910 height,
10911 format,
10912 DType::U8,
10913 memory,
10914 edgefirst_tensor::CpuAccess::ReadWrite,
10915 )?;
10916 src.as_typed::<u8>()
10917 .unwrap()
10918 .map()?
10919 .as_mut_slice()
10920 .copy_from_slice(bytes);
10921 Ok(src)
10922 }
10923
10924 fn compare_images(img1: &TensorDyn, img2: &TensorDyn, threshold: f64, name: &str) {
10932 assert_eq!(img1.height(), img2.height(), "Heights differ");
10933 assert_eq!(img1.width(), img2.width(), "Widths differ");
10934 assert_eq!(
10935 img1.format().unwrap(),
10936 img2.format().unwrap(),
10937 "PixelFormat differ"
10938 );
10939 assert!(
10940 matches!(
10941 img1.format().unwrap(),
10942 PixelFormat::Rgb | PixelFormat::Rgba | PixelFormat::Grey | PixelFormat::PlanarRgb
10943 ),
10944 "format must be Rgb or Rgba for comparison"
10945 );
10946
10947 fn flat(img: &TensorDyn) -> Vec<u8> {
10954 let t = img.as_typed::<u8>().expect("u8 image");
10955 let mut out = vec![0u8; t.shape().iter().product::<usize>()];
10956 t.copy_to_flat(&mut out).expect("compact the padded rows");
10957 out
10958 }
10959
10960 let image1 = match img1.format().unwrap() {
10961 PixelFormat::Rgb => image::RgbImage::from_vec(
10962 img1.width().unwrap() as u32,
10963 img1.height().unwrap() as u32,
10964 flat(img1),
10965 )
10966 .unwrap(),
10967 PixelFormat::Rgba => image::RgbaImage::from_vec(
10968 img1.width().unwrap() as u32,
10969 img1.height().unwrap() as u32,
10970 flat(img1),
10971 )
10972 .unwrap()
10973 .convert(),
10974 PixelFormat::Grey => image::GrayImage::from_vec(
10975 img1.width().unwrap() as u32,
10976 img1.height().unwrap() as u32,
10977 flat(img1),
10978 )
10979 .unwrap()
10980 .convert(),
10981 PixelFormat::PlanarRgb => image::GrayImage::from_vec(
10982 img1.width().unwrap() as u32,
10983 (img1.height().unwrap() * 3) as u32,
10984 flat(img1),
10985 )
10986 .unwrap()
10987 .convert(),
10988 _ => return,
10989 };
10990
10991 let image2 = match img2.format().unwrap() {
10992 PixelFormat::Rgb => image::RgbImage::from_vec(
10993 img2.width().unwrap() as u32,
10994 img2.height().unwrap() as u32,
10995 flat(img2),
10996 )
10997 .unwrap(),
10998 PixelFormat::Rgba => image::RgbaImage::from_vec(
10999 img2.width().unwrap() as u32,
11000 img2.height().unwrap() as u32,
11001 flat(img2),
11002 )
11003 .unwrap()
11004 .convert(),
11005 PixelFormat::Grey => image::GrayImage::from_vec(
11006 img2.width().unwrap() as u32,
11007 img2.height().unwrap() as u32,
11008 flat(img2),
11009 )
11010 .unwrap()
11011 .convert(),
11012 PixelFormat::PlanarRgb => image::GrayImage::from_vec(
11013 img2.width().unwrap() as u32,
11014 (img2.height().unwrap() * 3) as u32,
11015 flat(img2),
11016 )
11017 .unwrap()
11018 .convert(),
11019 _ => return,
11020 };
11021
11022 let similarity = image_compare::rgb_similarity_structure(
11023 &image_compare::Algorithm::RootMeanSquared,
11024 &image1,
11025 &image2,
11026 )
11027 .expect("Image Comparison failed");
11028 if similarity.score < threshold {
11029 similarity
11032 .image
11033 .to_color_map()
11034 .save(format!("{name}.png"))
11035 .unwrap();
11036 panic!(
11037 "{name}: converted image and target image have similarity score too low: {} < {}",
11038 similarity.score, threshold
11039 )
11040 }
11041 }
11042
11043 fn compare_images_convert_to_rgb(
11044 img1: &TensorDyn,
11045 img2: &TensorDyn,
11046 threshold: f64,
11047 name: &str,
11048 ) {
11049 assert_eq!(img1.height(), img2.height(), "Heights differ");
11050 assert_eq!(img1.width(), img2.width(), "Widths differ");
11051
11052 let mut img_rgb1 = TensorDyn::image(
11053 img1.width().unwrap(),
11054 img1.height().unwrap(),
11055 PixelFormat::Rgb,
11056 DType::U8,
11057 Some(TensorMemory::Mem),
11058 edgefirst_tensor::CpuAccess::ReadWrite,
11059 )
11060 .unwrap();
11061 let mut img_rgb2 = TensorDyn::image(
11062 img1.width().unwrap(),
11063 img1.height().unwrap(),
11064 PixelFormat::Rgb,
11065 DType::U8,
11066 Some(TensorMemory::Mem),
11067 edgefirst_tensor::CpuAccess::ReadWrite,
11068 )
11069 .unwrap();
11070 let mut __cv = CPUProcessor::default();
11071 let r1 = __cv.convert(
11072 img1,
11073 &mut img_rgb1,
11074 crate::Rotation::None,
11075 crate::Flip::None,
11076 crate::Crop::default(),
11077 );
11078 let r2 = __cv.convert(
11079 img2,
11080 &mut img_rgb2,
11081 crate::Rotation::None,
11082 crate::Flip::None,
11083 crate::Crop::default(),
11084 );
11085 if r1.is_err() || r2.is_err() {
11086 let w = img1.width().unwrap() as u32;
11088 let data1 = img1.as_typed::<u8>().unwrap().map().unwrap().to_vec();
11089 let data2 = img2.as_typed::<u8>().unwrap().map().unwrap().to_vec();
11090 let h1 = (data1.len() as u32) / w;
11091 let h2 = (data2.len() as u32) / w;
11092 let g1 = image::GrayImage::from_vec(w, h1, data1).unwrap();
11093 let g2 = image::GrayImage::from_vec(w, h2, data2).unwrap();
11094 let similarity = image_compare::gray_similarity_structure(
11095 &image_compare::Algorithm::RootMeanSquared,
11096 &g1,
11097 &g2,
11098 )
11099 .expect("Image Comparison failed");
11100 if similarity.score < threshold {
11101 panic!(
11102 "{name}: converted image and target image have similarity score too low: {} < {}",
11103 similarity.score, threshold
11104 )
11105 }
11106 return;
11107 }
11108
11109 let image1 = image::RgbImage::from_vec(
11110 img_rgb1.width().unwrap() as u32,
11111 img_rgb1.height().unwrap() as u32,
11112 img_rgb1.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
11113 )
11114 .unwrap();
11115
11116 let image2 = image::RgbImage::from_vec(
11117 img_rgb2.width().unwrap() as u32,
11118 img_rgb2.height().unwrap() as u32,
11119 img_rgb2.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
11120 )
11121 .unwrap();
11122
11123 let similarity = image_compare::rgb_similarity_structure(
11124 &image_compare::Algorithm::RootMeanSquared,
11125 &image1,
11126 &image2,
11127 )
11128 .expect("Image Comparison failed");
11129 if similarity.score < threshold {
11130 similarity
11133 .image
11134 .to_color_map()
11135 .save(format!("{name}.png"))
11136 .unwrap();
11137 panic!(
11138 "{name}: converted image and target image have similarity score too low: {} < {}",
11139 similarity.score, threshold
11140 )
11141 }
11142 }
11143
11144 #[test]
11149 fn test_nv12_image_creation() {
11150 let width = 640;
11151 let height = 480;
11152 let img = TensorDyn::image(
11153 width,
11154 height,
11155 PixelFormat::Nv12,
11156 DType::U8,
11157 None,
11158 edgefirst_tensor::CpuAccess::ReadWrite,
11159 )
11160 .unwrap();
11161
11162 assert_eq!(img.width(), Some(width));
11163 assert_eq!(img.height(), Some(height));
11164 assert_eq!(img.format().unwrap(), PixelFormat::Nv12);
11165 assert_eq!(
11167 img.as_typed::<u8>().unwrap().shape(),
11168 &[height * 3 / 2, width]
11169 );
11170 }
11171
11172 #[test]
11173 fn test_nv12_channels() {
11174 let img = TensorDyn::image(
11175 640,
11176 480,
11177 PixelFormat::Nv12,
11178 DType::U8,
11179 None,
11180 edgefirst_tensor::CpuAccess::ReadWrite,
11181 )
11182 .unwrap();
11183 assert_eq!(img.format().unwrap().channels(), 1);
11185 }
11186
11187 #[test]
11192 fn test_tensor_set_format_planar() {
11193 let mut tensor = Tensor::<u8>::new(&[3, 480, 640], None, None).unwrap();
11194 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
11195 assert_eq!(tensor.format(), Some(PixelFormat::PlanarRgb));
11196 assert_eq!(tensor.width(), Some(640));
11197 assert_eq!(tensor.height(), Some(480));
11198 }
11199
11200 #[test]
11201 fn test_tensor_set_format_interleaved() {
11202 let mut tensor = Tensor::<u8>::new(&[480, 640, 4], None, None).unwrap();
11203 tensor.set_format(PixelFormat::Rgba).unwrap();
11204 assert_eq!(tensor.format(), Some(PixelFormat::Rgba));
11205 assert_eq!(tensor.width(), Some(640));
11206 assert_eq!(tensor.height(), Some(480));
11207 }
11208
11209 #[test]
11210 fn test_tensordyn_image_rgb() {
11211 let img = TensorDyn::image(
11212 640,
11213 480,
11214 PixelFormat::Rgb,
11215 DType::U8,
11216 None,
11217 edgefirst_tensor::CpuAccess::ReadWrite,
11218 )
11219 .unwrap();
11220 assert_eq!(img.width(), Some(640));
11221 assert_eq!(img.height(), Some(480));
11222 assert_eq!(img.format(), Some(PixelFormat::Rgb));
11223 }
11224
11225 #[test]
11226 fn test_tensordyn_image_planar_rgb() {
11227 let img = TensorDyn::image(
11228 640,
11229 480,
11230 PixelFormat::PlanarRgb,
11231 DType::U8,
11232 None,
11233 edgefirst_tensor::CpuAccess::ReadWrite,
11234 )
11235 .unwrap();
11236 assert_eq!(img.width(), Some(640));
11237 assert_eq!(img.height(), Some(480));
11238 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
11239 }
11240
11241 #[test]
11242 fn test_rgb_int8_format() {
11243 let img = TensorDyn::image(
11245 1280,
11246 720,
11247 PixelFormat::Rgb,
11248 DType::I8,
11249 Some(TensorMemory::Mem),
11250 edgefirst_tensor::CpuAccess::ReadWrite,
11251 )
11252 .unwrap();
11253 assert_eq!(img.width(), Some(1280));
11254 assert_eq!(img.height(), Some(720));
11255 assert_eq!(img.format(), Some(PixelFormat::Rgb));
11256 assert_eq!(img.dtype(), DType::I8);
11257 }
11258
11259 #[test]
11260 fn test_planar_rgb_int8_format() {
11261 let img = TensorDyn::image(
11262 1280,
11263 720,
11264 PixelFormat::PlanarRgb,
11265 DType::I8,
11266 Some(TensorMemory::Mem),
11267 edgefirst_tensor::CpuAccess::ReadWrite,
11268 )
11269 .unwrap();
11270 assert_eq!(img.width(), Some(1280));
11271 assert_eq!(img.height(), Some(720));
11272 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
11273 assert_eq!(img.dtype(), DType::I8);
11274 }
11275
11276 #[test]
11277 fn test_rgb_from_tensor() {
11278 let mut tensor = Tensor::<u8>::new(&[720, 1280, 3], None, None).unwrap();
11279 tensor.set_format(PixelFormat::Rgb).unwrap();
11280 let img = TensorDyn::from(tensor);
11281 assert_eq!(img.width(), Some(1280));
11282 assert_eq!(img.height(), Some(720));
11283 assert_eq!(img.format(), Some(PixelFormat::Rgb));
11284 }
11285
11286 #[test]
11287 fn test_planar_rgb_from_tensor() {
11288 let mut tensor = Tensor::<u8>::new(&[3, 720, 1280], None, None).unwrap();
11289 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
11290 let img = TensorDyn::from(tensor);
11291 assert_eq!(img.width(), Some(1280));
11292 assert_eq!(img.height(), Some(720));
11293 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
11294 }
11295
11296 #[test]
11297 fn test_dtype_determines_int8() {
11298 let u8_img = TensorDyn::image(
11300 64,
11301 64,
11302 PixelFormat::Rgb,
11303 DType::U8,
11304 None,
11305 edgefirst_tensor::CpuAccess::ReadWrite,
11306 )
11307 .unwrap();
11308 let i8_img = TensorDyn::image(
11309 64,
11310 64,
11311 PixelFormat::Rgb,
11312 DType::I8,
11313 None,
11314 edgefirst_tensor::CpuAccess::ReadWrite,
11315 )
11316 .unwrap();
11317 assert_eq!(u8_img.dtype(), DType::U8);
11318 assert_eq!(i8_img.dtype(), DType::I8);
11319 }
11320
11321 #[test]
11322 fn test_pixel_layout_packed_vs_planar() {
11323 assert_eq!(PixelFormat::Rgb.layout(), PixelLayout::Packed);
11325 assert_eq!(PixelFormat::Rgba.layout(), PixelLayout::Packed);
11326 assert_eq!(PixelFormat::PlanarRgb.layout(), PixelLayout::Planar);
11327 assert_eq!(PixelFormat::Nv12.layout(), PixelLayout::SemiPlanar);
11328 }
11329
11330 #[cfg(target_os = "linux")]
11341 #[cfg(feature = "opengl")]
11342 #[test]
11343 fn pbo_reports_why_it_cannot_pin() {
11344 use edgefirst_tensor::{Error, TensorTrait};
11345
11346 let converter = ImageProcessor::new().unwrap();
11347 let is_pbo = converter
11348 .opengl
11349 .as_ref()
11350 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
11351 if !is_pbo {
11352 use std::io::Write;
11357 let _ = writeln!(
11358 &mut std::io::stderr(),
11359 "SKIPPED: pbo_reports_why_it_cannot_pin - transfer backend is not PBO"
11360 );
11361 return;
11362 }
11363
11364 let t = converter
11365 .create_image(
11366 64,
11367 64,
11368 PixelFormat::Rgba,
11369 DType::U8,
11370 None,
11371 edgefirst_tensor::CpuAccess::ReadWrite,
11372 )
11373 .unwrap();
11374 assert_eq!(t.as_typed::<u8>().unwrap().memory(), TensorMemory::Pbo);
11375 let t = t.as_typed::<u8>().unwrap();
11376
11377 match t.pin_host(edgefirst_tensor::CpuAccess::ReadWrite) {
11378 Err(Error::NotImplemented(m)) => {
11379 assert!(
11380 m.contains("glMapBufferRange"),
11381 "must name the constraint: {m}"
11382 );
11383 assert!(m.contains("map()"), "must point at the supported API: {m}");
11384 }
11385 other => panic!("expected an explained NotImplemented, got {other:?}"),
11386 }
11387 assert!(matches!(
11389 t.sync_for_cpu(edgefirst_tensor::CpuAccess::Read),
11390 Err(Error::NotImplemented(_))
11391 ));
11392
11393 let g = t.map().expect("map must work where pin cannot");
11395 assert!(!g.as_slice().is_empty());
11396 }
11397
11398 #[cfg(target_os = "linux")]
11402 #[cfg(feature = "opengl")]
11403 #[test]
11404 fn test_convert_pbo_to_pbo() {
11405 let mut converter = ImageProcessor::new().unwrap();
11406
11407 let is_pbo = converter
11409 .opengl
11410 .as_ref()
11411 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
11412 if !is_pbo {
11413 log::warn!("Skipping test_convert_pbo_to_pbo: backend is not PBO");
11414 return;
11415 }
11416
11417 let src_w = 640;
11418 let src_h = 480;
11419 let dst_w = 320;
11420 let dst_h = 240;
11421
11422 let pbo_src = converter
11424 .create_image(
11425 src_w,
11426 src_h,
11427 PixelFormat::Rgba,
11428 DType::U8,
11429 None,
11430 edgefirst_tensor::CpuAccess::ReadWrite,
11431 )
11432 .unwrap();
11433 assert_eq!(
11434 pbo_src.as_typed::<u8>().unwrap().memory(),
11435 TensorMemory::Pbo,
11436 "create_image should produce a PBO tensor"
11437 );
11438
11439 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
11441 let jpeg_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
11442
11443 let mem_src = TensorDyn::image(
11445 src_w,
11446 src_h,
11447 PixelFormat::Rgba,
11448 DType::U8,
11449 Some(TensorMemory::Mem),
11450 edgefirst_tensor::CpuAccess::ReadWrite,
11451 )
11452 .unwrap();
11453 let (result, _jpeg_src, mem_src) = convert_img(
11454 &mut CPUProcessor::new(),
11455 jpeg_src,
11456 mem_src,
11457 Rotation::None,
11458 Flip::None,
11459 Crop::no_crop(),
11460 );
11461 result.unwrap();
11462
11463 {
11465 let src_data = mem_src.as_typed::<u8>().unwrap().map().unwrap();
11466 let mut pbo_map = pbo_src.as_typed::<u8>().unwrap().map().unwrap();
11467 pbo_map.copy_from_slice(&src_data);
11468 }
11469
11470 let pbo_dst = converter
11472 .create_image(
11473 dst_w,
11474 dst_h,
11475 PixelFormat::Rgba,
11476 DType::U8,
11477 None,
11478 edgefirst_tensor::CpuAccess::ReadWrite,
11479 )
11480 .unwrap();
11481 assert_eq!(
11482 pbo_dst.as_typed::<u8>().unwrap().memory(),
11483 TensorMemory::Pbo
11484 );
11485
11486 let mut pbo_dst = pbo_dst;
11488 let result = converter.convert(
11489 &pbo_src,
11490 &mut pbo_dst,
11491 Rotation::None,
11492 Flip::None,
11493 Crop::no_crop(),
11494 );
11495 result.unwrap();
11496
11497 let cpu_dst = TensorDyn::image(
11499 dst_w,
11500 dst_h,
11501 PixelFormat::Rgba,
11502 DType::U8,
11503 Some(TensorMemory::Mem),
11504 edgefirst_tensor::CpuAccess::ReadWrite,
11505 )
11506 .unwrap();
11507 let (result, _mem_src, cpu_dst) = convert_img(
11508 &mut CPUProcessor::new(),
11509 mem_src,
11510 cpu_dst,
11511 Rotation::None,
11512 Flip::None,
11513 Crop::no_crop(),
11514 );
11515 result.unwrap();
11516
11517 let pbo_dst_img = {
11518 let mut __t = pbo_dst.into_u8().unwrap();
11519 __t.set_format(PixelFormat::Rgba).unwrap();
11520 TensorDyn::from(__t)
11521 };
11522 compare_images(&pbo_dst_img, &cpu_dst, 0.95, function!());
11523 log::info!("test_convert_pbo_to_pbo: PASS — PBO-to-PBO convert matches CPU reference");
11524 }
11525
11526 #[test]
11527 fn test_image_bgra() {
11528 let img = TensorDyn::image(
11529 640,
11530 480,
11531 PixelFormat::Bgra,
11532 DType::U8,
11533 Some(edgefirst_tensor::TensorMemory::Mem),
11534 edgefirst_tensor::CpuAccess::ReadWrite,
11535 )
11536 .unwrap();
11537 assert_eq!(img.width(), Some(640));
11538 assert_eq!(img.height(), Some(480));
11539 assert_eq!(img.format().unwrap().channels(), 4);
11540 assert_eq!(img.format().unwrap(), PixelFormat::Bgra);
11541 }
11542
11543 #[test]
11548 fn test_force_backend_cpu() {
11549 let _lock = acquire_env_lock();
11550 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11551 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
11552 let converter = ImageProcessor::new().unwrap();
11553 assert!(converter.cpu.is_some());
11554 assert_eq!(converter.forced_backend, Some(ForcedBackend::Cpu));
11555 }
11556
11557 #[test]
11558 fn test_force_backend_invalid() {
11559 let _lock = acquire_env_lock();
11560 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11561 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "invalid") };
11562 let result = ImageProcessor::new();
11563 assert!(
11564 matches!(&result, Err(Error::ForcedBackendUnavailable(s)) if s.contains("unknown")),
11565 "invalid backend value should return ForcedBackendUnavailable error: {result:?}"
11566 );
11567 }
11568
11569 #[test]
11570 fn test_force_backend_unset() {
11571 let _lock = acquire_env_lock();
11572 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11573 unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") };
11574 let converter = ImageProcessor::new().unwrap();
11575 assert!(converter.forced_backend.is_none());
11576 }
11577
11578 #[test]
11583 fn test_draw_proto_masks_no_cpu_returns_error() {
11584 let _lock = acquire_env_lock();
11586 let _guard = EnvGuard::snapshot(&[
11587 "EDGEFIRST_FORCE_BACKEND",
11588 "EDGEFIRST_DISABLE_GL",
11589 "EDGEFIRST_DISABLE_G2D",
11590 "EDGEFIRST_DISABLE_CPU",
11591 ]);
11592
11593 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
11595 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
11596 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
11597
11598 let mut converter = ImageProcessor::new().unwrap();
11599 assert!(converter.cpu.is_none(), "CPU should be disabled");
11600
11601 let dst = TensorDyn::image(
11602 640,
11603 480,
11604 PixelFormat::Rgba,
11605 DType::U8,
11606 Some(TensorMemory::Mem),
11607 edgefirst_tensor::CpuAccess::ReadWrite,
11608 )
11609 .unwrap();
11610 let mut dst_dyn = dst;
11611 let det = [DetectBox {
11612 bbox: edgefirst_tensor::BoundingBox {
11613 xmin: 0.1,
11614 ymin: 0.1,
11615 xmax: 0.5,
11616 ymax: 0.5,
11617 },
11618 score: 0.9,
11619 label: 0,
11620 }];
11621 let proto_data = {
11622 use edgefirst_tensor::{Tensor, TensorDyn};
11623 let coeff_t = Tensor::<f32>::from_slice(&[0.5_f32; 4], &[1, 4]).unwrap();
11624 let protos_t =
11625 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
11626 ProtoData {
11627 mask_coefficients: TensorDyn::F32(coeff_t),
11628 protos: TensorDyn::F32(protos_t),
11629 layout: ProtoLayout::Nhwc,
11630 }
11631 };
11632 let result =
11633 converter.draw_proto_masks(&mut dst_dyn, &det, &proto_data, Default::default());
11634 assert!(
11635 matches!(&result, Err(Error::Internal(s)) if s.contains("CPU backend")),
11636 "draw_proto_masks without CPU should return Internal error: {result:?}"
11637 );
11638 }
11639
11640 #[test]
11641 fn test_draw_proto_masks_cpu_fallback_works() {
11642 let _lock = acquire_env_lock();
11645 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11646 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
11647 let mut converter = ImageProcessor::new().unwrap();
11648 assert!(converter.cpu.is_some());
11649
11650 let dst = TensorDyn::image(
11651 64,
11652 64,
11653 PixelFormat::Rgba,
11654 DType::U8,
11655 Some(TensorMemory::Mem),
11656 edgefirst_tensor::CpuAccess::ReadWrite,
11657 )
11658 .unwrap();
11659 let mut dst_dyn = dst;
11660 let det = [DetectBox {
11661 bbox: edgefirst_tensor::BoundingBox {
11662 xmin: 0.1,
11663 ymin: 0.1,
11664 xmax: 0.5,
11665 ymax: 0.5,
11666 },
11667 score: 0.9,
11668 label: 0,
11669 }];
11670 let proto_data = {
11671 use edgefirst_tensor::{Tensor, TensorDyn};
11672 let coeff_t = Tensor::<f32>::from_slice(&[0.5_f32; 4], &[1, 4]).unwrap();
11673 let protos_t =
11674 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
11675 ProtoData {
11676 mask_coefficients: TensorDyn::F32(coeff_t),
11677 protos: TensorDyn::F32(protos_t),
11678 layout: ProtoLayout::Nhwc,
11679 }
11680 };
11681 let result =
11682 converter.draw_proto_masks(&mut dst_dyn, &det, &proto_data, Default::default());
11683 assert!(result.is_ok(), "CPU fallback path should work: {result:?}");
11684 }
11685
11686 fn acquire_env_lock() -> std::sync::MutexGuard<'static, ()> {
11718 use std::sync::{Mutex, OnceLock};
11719 static ENV_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
11720 ENV_MUTEX
11721 .get_or_init(|| Mutex::new(()))
11722 .lock()
11723 .unwrap_or_else(|e| e.into_inner())
11724 }
11725
11726 struct EnvGuard {
11729 vars: Vec<(&'static str, Option<String>)>,
11730 }
11731
11732 impl EnvGuard {
11733 fn snapshot(names: &[&'static str]) -> Self {
11737 Self {
11738 vars: names.iter().map(|&k| (k, std::env::var(k).ok())).collect(),
11739 }
11740 }
11741 }
11742
11743 impl Drop for EnvGuard {
11744 fn drop(&mut self) {
11745 for (k, v) in &self.vars {
11746 match v {
11747 Some(s) => unsafe { std::env::set_var(k, s) },
11748 None => unsafe { std::env::remove_var(k) },
11749 }
11750 }
11751 }
11752 }
11753
11754 #[test]
11757 fn test_interpolation_env_parse() {
11758 let _lock = acquire_env_lock();
11759 let _guard = EnvGuard::snapshot(&["EDGEFIRST_INTERPOLATION"]);
11760 for (value, want) in [
11761 (Some("nearest"), Some(Interpolation::Nearest)),
11762 (Some(" BILINEAR "), Some(Interpolation::Bilinear)),
11763 (Some("lanczos"), None),
11764 (Some(""), None),
11765 (None, None),
11766 ] {
11767 match value {
11768 Some(v) => unsafe { std::env::set_var("EDGEFIRST_INTERPOLATION", v) },
11769 None => unsafe { std::env::remove_var("EDGEFIRST_INTERPOLATION") },
11770 }
11771 assert_eq!(
11772 Interpolation::from_env(),
11773 want,
11774 "EDGEFIRST_INTERPOLATION={value:?}"
11775 );
11776 }
11777 }
11778
11779 #[test]
11782 fn test_interpolation_config_and_env_reach_cpu() {
11783 let _lock = acquire_env_lock();
11784 let _guard = EnvGuard::snapshot(&["EDGEFIRST_INTERPOLATION"]);
11785 let mut checker = Vec::with_capacity(8 * 8 * 4);
11786 for y in 0..8 {
11787 for x in 0..8 {
11788 let v = if (x + y) % 2 == 0 { 0u8 } else { 255 };
11789 checker.extend_from_slice(&[v, v, v, 255]);
11790 }
11791 }
11792 let resize = |processor: &mut dyn ImageProcessorTrait| {
11793 let mut src = TensorDyn::image(
11794 8,
11795 8,
11796 PixelFormat::Rgba,
11797 DType::U8,
11798 Some(TensorMemory::Mem),
11799 edgefirst_tensor::CpuAccess::ReadWrite,
11800 )
11801 .unwrap();
11802 src.as_u8_mut()
11803 .unwrap()
11804 .map_mut()
11805 .unwrap()
11806 .as_mut_slice()
11807 .copy_from_slice(&checker);
11808 let mut dst = TensorDyn::image(
11809 12,
11810 12,
11811 PixelFormat::Rgba,
11812 DType::U8,
11813 Some(TensorMemory::Mem),
11814 edgefirst_tensor::CpuAccess::ReadWrite,
11815 )
11816 .unwrap();
11817 processor
11818 .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
11819 .unwrap();
11820 dst.as_u8().unwrap().map().unwrap().as_slice().to_vec()
11821 };
11822 let nearest = resize(&mut CPUProcessor::new_nearest());
11823 let bilinear = resize(&mut CPUProcessor::new());
11824 assert_ne!(nearest, bilinear, "the checker must tell the filters apart");
11825 let config = |interpolation| ImageProcessorConfig {
11826 backend: ComputeBackend::Cpu,
11827 interpolation,
11828 ..Default::default()
11829 };
11830
11831 unsafe { std::env::remove_var("EDGEFIRST_INTERPOLATION") };
11832 let mut p = ImageProcessor::with_config(config(Interpolation::Nearest)).unwrap();
11833 assert_eq!(resize(&mut p), nearest, "config Nearest");
11834 p.set_interpolation(Interpolation::Bilinear).unwrap();
11835 assert_eq!(resize(&mut p), bilinear, "set_interpolation(Bilinear)");
11836
11837 unsafe { std::env::set_var("EDGEFIRST_INTERPOLATION", "nearest") };
11838 let mut p = ImageProcessor::with_config(config(Interpolation::Bilinear)).unwrap();
11839 assert_eq!(
11840 resize(&mut p),
11841 nearest,
11842 "EDGEFIRST_INTERPOLATION pins Nearest"
11843 );
11844 p.set_interpolation(Interpolation::Bilinear).unwrap();
11845 assert_eq!(
11846 resize(&mut p),
11847 nearest,
11848 "the env pin outlives set_interpolation"
11849 );
11850 }
11851
11852 fn with_force_backend<R>(value: Option<&str>, body: impl FnOnce() -> R) -> R {
11856 let _lock = acquire_env_lock();
11857 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11858 match value {
11859 Some(v) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", v) },
11860 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
11861 }
11862 body()
11863 }
11864
11865 fn make_dirty_dst(w: usize, h: usize, mem: Option<TensorMemory>) -> TensorDyn {
11870 let dst = TensorDyn::image(
11871 w,
11872 h,
11873 PixelFormat::Rgba,
11874 DType::U8,
11875 mem,
11876 edgefirst_tensor::CpuAccess::ReadWrite,
11877 )
11878 .unwrap();
11879 {
11880 use edgefirst_tensor::TensorMapTrait;
11881 let u8t = dst.as_typed::<u8>().unwrap();
11882 let mut map = u8t.map().unwrap();
11883 for (i, b) in map.as_mut_slice().iter_mut().enumerate() {
11884 *b = 0xA0u8.wrapping_add((i as u8) & 0x3F);
11885 }
11886 }
11887 dst
11888 }
11889
11890 fn make_bg(w: usize, h: usize, mem: Option<TensorMemory>, rgba: [u8; 4]) -> TensorDyn {
11892 let bg = TensorDyn::image(
11893 w,
11894 h,
11895 PixelFormat::Rgba,
11896 DType::U8,
11897 mem,
11898 edgefirst_tensor::CpuAccess::ReadWrite,
11899 )
11900 .unwrap();
11901 {
11902 use edgefirst_tensor::TensorMapTrait;
11903 let u8t = bg.as_typed::<u8>().unwrap();
11904 let mut map = u8t.map().unwrap();
11905 for chunk in map.as_mut_slice().as_chunks_mut::<4>().0 {
11906 chunk.copy_from_slice(&rgba);
11907 }
11908 }
11909 bg
11910 }
11911
11912 fn pixel_at(dst: &TensorDyn, x: usize, y: usize) -> [u8; 4] {
11913 use edgefirst_tensor::TensorMapTrait;
11914 let w = dst.width().unwrap();
11915 let off = (y * w + x) * 4;
11916 let u8t = dst.as_typed::<u8>().unwrap();
11917 let map = u8t.map().unwrap();
11918 let s = map.as_slice();
11919 [s[off], s[off + 1], s[off + 2], s[off + 3]]
11920 }
11921
11922 fn assert_every_pixel_eq(dst: &TensorDyn, expected: [u8; 4], case: &str) {
11923 use edgefirst_tensor::TensorMapTrait;
11924 let u8t = dst.as_typed::<u8>().unwrap();
11925 let map = u8t.map().unwrap();
11926 for (i, chunk) in map.as_slice().as_chunks::<4>().0.iter().enumerate() {
11927 assert_eq!(
11928 chunk, &expected,
11929 "{case}: pixel idx {i} = {chunk:?}, expected {expected:?}"
11930 );
11931 }
11932 }
11933
11934 fn scenario_empty_no_bg(processor: &mut ImageProcessor, case: &str) {
11937 let mut dst = make_dirty_dst(64, 64, None);
11938 processor
11939 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::default())
11940 .unwrap_or_else(|e| panic!("{case}/decoded_masks empty+no-bg failed: {e:?}"));
11941 assert_every_pixel_eq(&dst, [0, 0, 0, 0], &format!("{case}/decoded"));
11942
11943 let mut dst = make_dirty_dst(64, 64, None);
11944 let proto = {
11945 use edgefirst_tensor::{Tensor, TensorDyn};
11946 let coeff_t = Tensor::<f32>::from_slice(&[0.0_f32; 4], &[1, 4]).unwrap();
11948 let protos_t =
11949 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
11950 ProtoData {
11951 mask_coefficients: TensorDyn::F32(coeff_t),
11952 protos: TensorDyn::F32(protos_t),
11953 layout: ProtoLayout::Nhwc,
11954 }
11955 };
11956 processor
11957 .draw_proto_masks(&mut dst, &[], &proto, MaskOverlay::default())
11958 .unwrap_or_else(|e| panic!("{case}/proto_masks empty+no-bg failed: {e:?}"));
11959 assert_every_pixel_eq(&dst, [0, 0, 0, 0], &format!("{case}/proto"));
11960 }
11961
11962 fn scenario_empty_with_bg(processor: &mut ImageProcessor, case: &str) {
11965 let bg_color = [42, 99, 200, 255];
11966 let bg = make_bg(64, 64, None, bg_color);
11967 let overlay = MaskOverlay::new().with_background(&bg);
11968
11969 let mut dst = make_dirty_dst(64, 64, None);
11970 processor
11971 .draw_decoded_masks(&mut dst, &[], &[], overlay)
11972 .unwrap_or_else(|e| panic!("{case}/decoded_masks empty+bg failed: {e:?}"));
11973 assert_every_pixel_eq(&dst, bg_color, &format!("{case}/decoded bg blit"));
11974
11975 let mut dst = make_dirty_dst(64, 64, None);
11976 let proto = {
11977 use edgefirst_tensor::{Tensor, TensorDyn};
11978 let coeff_t = Tensor::<f32>::from_slice(&[0.0_f32; 4], &[1, 4]).unwrap();
11980 let protos_t =
11981 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
11982 ProtoData {
11983 mask_coefficients: TensorDyn::F32(coeff_t),
11984 protos: TensorDyn::F32(protos_t),
11985 layout: ProtoLayout::Nhwc,
11986 }
11987 };
11988 processor
11989 .draw_proto_masks(&mut dst, &[], &proto, overlay)
11990 .unwrap_or_else(|e| panic!("{case}/proto_masks empty+bg failed: {e:?}"));
11991 assert_every_pixel_eq(&dst, bg_color, &format!("{case}/proto bg blit"));
11992 }
11993
11994 fn scenario_detect_no_bg(processor: &mut ImageProcessor, case: &str) {
11998 use edgefirst_tensor::Segmentation;
11999 use ndarray::Array3;
12000 processor
12001 .set_class_colors(&[[200, 80, 40, 255]])
12002 .expect("set_class_colors");
12003
12004 let detect = DetectBox {
12005 bbox: [0.25, 0.25, 0.75, 0.75].into(),
12006 score: 0.99,
12007 label: 0,
12008 };
12009 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
12010 let seg = Segmentation {
12011 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
12012 .unwrap()
12013 .into(),
12014 xmin: 0.25,
12015 ymin: 0.25,
12016 xmax: 0.75,
12017 ymax: 0.75,
12018 };
12019
12020 let mut dst = make_dirty_dst(64, 64, None);
12021 processor
12022 .draw_decoded_masks(&mut dst, &[detect], &[seg], MaskOverlay::default())
12023 .unwrap_or_else(|e| panic!("{case}/decoded_masks detect+no-bg failed: {e:?}"));
12024
12025 let corner = pixel_at(&dst, 2, 2);
12027 assert_eq!(
12028 corner,
12029 [0, 0, 0, 0],
12030 "{case}/decoded: corner (2,2) leaked dirty pattern: {corner:?}"
12031 );
12032 let center = pixel_at(&dst, 32, 32);
12036 assert!(
12037 center != [0, 0, 0, 0],
12038 "{case}/decoded: center (32,32) was not coloured: {center:?}"
12039 );
12040 }
12041
12042 fn scenario_detect_with_bg(processor: &mut ImageProcessor, case: &str) {
12045 use edgefirst_tensor::Segmentation;
12046 use ndarray::Array3;
12047 processor
12048 .set_class_colors(&[[200, 80, 40, 255]])
12049 .expect("set_class_colors");
12050 let bg_color = [10, 20, 30, 255];
12051 let bg = make_bg(64, 64, None, bg_color);
12052
12053 let detect = DetectBox {
12054 bbox: [0.25, 0.25, 0.75, 0.75].into(),
12055 score: 0.99,
12056 label: 0,
12057 };
12058 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
12059 let seg = Segmentation {
12060 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
12061 .unwrap()
12062 .into(),
12063 xmin: 0.25,
12064 ymin: 0.25,
12065 xmax: 0.75,
12066 ymax: 0.75,
12067 };
12068
12069 let overlay = MaskOverlay::new().with_background(&bg);
12070 let mut dst = make_dirty_dst(64, 64, None);
12071 processor
12072 .draw_decoded_masks(&mut dst, &[detect], &[seg], overlay)
12073 .unwrap_or_else(|e| panic!("{case}/decoded_masks detect+bg failed: {e:?}"));
12074
12075 let corner = pixel_at(&dst, 2, 2);
12077 assert_eq!(
12078 corner, bg_color,
12079 "{case}/decoded: corner (2,2) should show bg {bg_color:?} got {corner:?}"
12080 );
12081 let center = pixel_at(&dst, 32, 32);
12084 assert!(
12085 center != bg_color,
12086 "{case}/decoded: center (32,32) should differ from bg {bg_color:?}, got {center:?}"
12087 );
12088 }
12089
12090 fn run_all_scenarios(
12093 force_backend: Option<&'static str>,
12094 case: &'static str,
12095 require_dma_for_bg: bool,
12096 ) {
12097 if require_dma_for_bg && !edgefirst_tensor::is_dma_available() {
12098 crate::test_support::report_skip(&format!("{case} — DMA not available on this host"));
12099 return;
12100 }
12101 let processor_result = with_force_backend(force_backend, ImageProcessor::new);
12102 let mut processor = match processor_result {
12103 Ok(p) => p,
12104 Err(e) => {
12105 crate::test_support::report_skip(&format!("{case} — backend init failed: {e:?}"));
12106 return;
12107 }
12108 };
12109 scenario_empty_no_bg(&mut processor, case);
12110 scenario_empty_with_bg(&mut processor, case);
12111 scenario_detect_no_bg(&mut processor, case);
12112 scenario_detect_with_bg(&mut processor, case);
12113 }
12114
12115 #[test]
12116 fn test_draw_masks_4_scenarios_cpu() {
12117 run_all_scenarios(Some("cpu"), "cpu", false);
12118 }
12119
12120 #[test]
12121 fn test_draw_masks_4_scenarios_auto() {
12122 run_all_scenarios(None, "auto", false);
12123 }
12124
12125 #[cfg(any(
12126 target_os = "linux",
12127 target_os = "macos",
12128 target_os = "ios",
12129 target_os = "android",
12130 target_os = "windows"
12131 ))]
12132 #[cfg(feature = "opengl")]
12133 #[test]
12134 fn test_draw_masks_4_scenarios_opengl() {
12135 run_all_scenarios(Some("opengl"), "opengl", false);
12136 }
12137
12138 #[cfg(target_os = "linux")]
12143 #[test]
12144 fn test_draw_masks_zero_detection_g2d_forced() {
12145 if !edgefirst_tensor::is_dma_available() {
12146 crate::test_support::report_skip("g2d forced — DMA not available on this host");
12147 return;
12148 }
12149 let processor_result = with_force_backend(Some("g2d"), ImageProcessor::new);
12150 let mut processor = match processor_result {
12151 Ok(p) => p,
12152 Err(e) => {
12153 crate::test_support::report_skip(&format!("g2d forced — init failed: {e:?}"));
12154 return;
12155 }
12156 };
12157
12158 let mut dst = TensorDyn::image(
12160 64,
12161 64,
12162 PixelFormat::Rgba,
12163 DType::U8,
12164 Some(TensorMemory::DmaBuf),
12165 edgefirst_tensor::CpuAccess::ReadWrite,
12166 )
12167 .unwrap();
12168 {
12169 use edgefirst_tensor::TensorMapTrait;
12170 let u8t = dst.as_typed_mut::<u8>().unwrap();
12171 let mut map = u8t.map().unwrap();
12172 map.as_mut_slice().fill(0xBB);
12173 }
12174 processor
12175 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::default())
12176 .expect("g2d empty+no-bg");
12177 assert_every_pixel_eq(&dst, [0, 0, 0, 0], "g2d/case1 cleared");
12178
12179 let bg_color = [7, 11, 13, 255];
12181 let bg = {
12182 let t = TensorDyn::image(
12183 64,
12184 64,
12185 PixelFormat::Rgba,
12186 DType::U8,
12187 Some(TensorMemory::DmaBuf),
12188 edgefirst_tensor::CpuAccess::ReadWrite,
12189 )
12190 .unwrap();
12191 {
12192 use edgefirst_tensor::TensorMapTrait;
12193 let u8t = t.as_typed::<u8>().unwrap();
12194 let mut map = u8t.map().unwrap();
12195 for chunk in map.as_mut_slice().as_chunks_mut::<4>().0 {
12196 chunk.copy_from_slice(&bg_color);
12197 }
12198 }
12199 t
12200 };
12201 let mut dst = TensorDyn::image(
12202 64,
12203 64,
12204 PixelFormat::Rgba,
12205 DType::U8,
12206 Some(TensorMemory::DmaBuf),
12207 edgefirst_tensor::CpuAccess::ReadWrite,
12208 )
12209 .unwrap();
12210 {
12211 use edgefirst_tensor::TensorMapTrait;
12212 let u8t = dst.as_typed_mut::<u8>().unwrap();
12213 let mut map = u8t.map().unwrap();
12214 map.as_mut_slice().fill(0x55);
12215 }
12216 processor
12217 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::new().with_background(&bg))
12218 .expect("g2d empty+bg");
12219 assert_every_pixel_eq(&dst, bg_color, "g2d/case2 bg blit");
12220
12221 let detect = DetectBox {
12223 bbox: [0.25, 0.25, 0.75, 0.75].into(),
12224 score: 0.9,
12225 label: 0,
12226 };
12227 let mut dst = TensorDyn::image(
12228 64,
12229 64,
12230 PixelFormat::Rgba,
12231 DType::U8,
12232 Some(TensorMemory::DmaBuf),
12233 edgefirst_tensor::CpuAccess::ReadWrite,
12234 )
12235 .unwrap();
12236 let err = processor
12237 .draw_decoded_masks(&mut dst, &[detect], &[], MaskOverlay::default())
12238 .expect_err("g2d must reject detect-present draw_decoded_masks");
12239 assert!(
12240 matches!(err, Error::NotImplemented(_)),
12241 "g2d case3 wrong error: {err:?}"
12242 );
12243 }
12244
12245 #[test]
12246 fn test_set_format_then_cpu_convert() {
12247 let _lock = acquire_env_lock();
12250 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
12251 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
12252 let mut processor = ImageProcessor::new().unwrap();
12253
12254 let image = edgefirst_bench::testdata::read("zidane.jpg");
12256 let src = load_image_test_helper(&image, Some(PixelFormat::Rgba), None).unwrap();
12257
12258 let mut dst =
12260 TensorDyn::new(&[640, 640, 3], DType::U8, Some(TensorMemory::Mem), None).unwrap();
12261 dst.set_format(PixelFormat::Rgb).unwrap();
12262
12263 processor
12265 .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
12266 .unwrap();
12267
12268 assert_eq!(dst.format(), Some(PixelFormat::Rgb));
12270 assert_eq!(dst.width(), Some(640));
12271 assert_eq!(dst.height(), Some(640));
12272 }
12273
12274 #[test]
12280 fn test_multiple_image_processors_same_thread() {
12281 let _lock = acquire_env_lock();
12284 let mut processors: Vec<ImageProcessor> = (0..4)
12285 .map(|_| ImageProcessor::new().expect("ImageProcessor::new() failed"))
12286 .collect();
12287
12288 for proc in &mut processors {
12289 let src = proc
12290 .create_image(
12291 128,
12292 128,
12293 PixelFormat::Rgb,
12294 DType::U8,
12295 None,
12296 edgefirst_tensor::CpuAccess::ReadWrite,
12297 )
12298 .expect("create src failed");
12299 let mut dst = proc
12300 .create_image(
12301 64,
12302 64,
12303 PixelFormat::Rgb,
12304 DType::U8,
12305 None,
12306 edgefirst_tensor::CpuAccess::ReadWrite,
12307 )
12308 .expect("create dst failed");
12309 proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
12310 .expect("convert failed");
12311 assert_eq!(dst.width(), Some(64));
12312 assert_eq!(dst.height(), Some(64));
12313 }
12314 }
12315
12316 #[test]
12323 fn test_multiple_image_processors_separate_threads() {
12324 use std::sync::mpsc;
12325 use std::time::Duration;
12326
12327 if std::env::var_os("EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS").is_some() {
12338 crate::test_support::report_skip(
12339 "test_multiple_image_processors_separate_threads — known Vivante \
12340 GC7000UL concurrent-EGL-teardown double-free \
12341 (EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS set)",
12342 );
12343 return;
12344 }
12345
12346 const TIMEOUT: Duration = Duration::from_secs(60);
12347
12348 let _lock = acquire_env_lock();
12351
12352 let (tx, rx) = mpsc::channel::<()>();
12353
12354 std::thread::spawn(move || {
12355 let handles: Vec<_> = (0..4)
12356 .map(|i| {
12357 std::thread::spawn(move || {
12358 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
12359 panic!("ImageProcessor::new() failed on thread {i}: {e}")
12360 });
12361 let src = proc
12362 .create_image(
12363 128,
12364 128,
12365 PixelFormat::Rgb,
12366 DType::U8,
12367 None,
12368 edgefirst_tensor::CpuAccess::ReadWrite,
12369 )
12370 .unwrap_or_else(|e| panic!("create src failed on thread {i}: {e}"));
12371 let mut dst = proc
12372 .create_image(
12373 64,
12374 64,
12375 PixelFormat::Rgb,
12376 DType::U8,
12377 None,
12378 edgefirst_tensor::CpuAccess::ReadWrite,
12379 )
12380 .unwrap_or_else(|e| panic!("create dst failed on thread {i}: {e}"));
12381 proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
12382 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
12383 assert_eq!(dst.width(), Some(64));
12384 assert_eq!(dst.height(), Some(64));
12385 })
12386 })
12387 .collect();
12388
12389 for (i, h) in handles.into_iter().enumerate() {
12390 h.join()
12391 .unwrap_or_else(|e| panic!("thread {i} panicked: {e:?}"));
12392 }
12393
12394 let _ = tx.send(());
12395 });
12396
12397 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
12398 panic!("test_multiple_image_processors_separate_threads timed out after {TIMEOUT:?}")
12399 });
12400 }
12401
12402 #[test]
12409 fn test_image_processors_concurrent_operations() {
12410 use std::sync::{mpsc, Arc, Barrier};
12411 use std::time::Duration;
12412
12413 const N: usize = 4;
12414 const ROUNDS: usize = 10;
12415 const TIMEOUT: Duration = Duration::from_secs(60);
12416
12417 let _lock = acquire_env_lock();
12420
12421 let (tx, rx) = mpsc::channel::<()>();
12422
12423 std::thread::spawn(move || {
12424 let barrier = Arc::new(Barrier::new(N));
12425
12426 let handles: Vec<_> = (0..N)
12427 .map(|i| {
12428 let barrier = Arc::clone(&barrier);
12429 std::thread::spawn(move || {
12430 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
12431 panic!("ImageProcessor::new() failed on thread {i}: {e}")
12432 });
12433
12434 barrier.wait();
12436
12437 for round in 0..ROUNDS {
12439 let src = proc
12440 .create_image(
12441 128,
12442 128,
12443 PixelFormat::Rgb,
12444 DType::U8,
12445 None,
12446 edgefirst_tensor::CpuAccess::ReadWrite,
12447 )
12448 .unwrap_or_else(|e| {
12449 panic!("create src failed on thread {i} round {round}: {e}")
12450 });
12451 let mut dst = proc
12452 .create_image(
12453 64,
12454 64,
12455 PixelFormat::Rgb,
12456 DType::U8,
12457 None,
12458 edgefirst_tensor::CpuAccess::ReadWrite,
12459 )
12460 .unwrap_or_else(|e| {
12461 panic!("create dst failed on thread {i} round {round}: {e}")
12462 });
12463 proc.convert(
12464 &src,
12465 &mut dst,
12466 Rotation::None,
12467 Flip::None,
12468 Crop::default(),
12469 )
12470 .unwrap_or_else(|e| {
12471 panic!("convert failed on thread {i} round {round}: {e}")
12472 });
12473 assert_eq!(dst.width(), Some(64));
12474 assert_eq!(dst.height(), Some(64));
12475 }
12476 })
12477 })
12478 .collect();
12479
12480 for (i, h) in handles.into_iter().enumerate() {
12481 h.join()
12482 .unwrap_or_else(|e| panic!("thread {i} panicked: {e:?}"));
12483 }
12484
12485 let _ = tx.send(());
12486 });
12487
12488 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
12489 panic!("test_image_processors_concurrent_operations timed out after {TIMEOUT:?}")
12490 });
12491 }
12492
12493 #[test]
12511 fn test_parallel_processors_unique_outputs() {
12512 use std::sync::{mpsc, Arc, Barrier};
12513 use std::time::Duration;
12514
12515 const N: usize = 4;
12516 const ROUNDS: usize = 25;
12517 const TIMEOUT: Duration = Duration::from_secs(60);
12518
12519 if std::env::var_os("EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS").is_some() {
12520 crate::test_support::report_skip(
12521 "test_parallel_processors_unique_outputs — known Vivante \
12522 GC7000UL concurrent-multi-processor driver abort \
12523 (EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS set)",
12524 );
12525 return;
12526 }
12527
12528 let _lock = acquire_env_lock();
12529 let (tx, rx) = mpsc::channel::<()>();
12530
12531 std::thread::spawn(move || {
12532 let barrier = Arc::new(Barrier::new(N));
12533 let handles: Vec<_> = (0..N)
12534 .map(|i| {
12535 let barrier = Arc::clone(&barrier);
12536 std::thread::spawn(move || {
12537 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
12538 panic!("ImageProcessor::new() failed on thread {i}: {e}")
12539 });
12540 let (w, h) = (640usize, 480usize);
12542 let mem = if edgefirst_tensor::is_dma_available() {
12543 Some(TensorMemory::DmaBuf)
12544 } else {
12545 Some(TensorMemory::Mem)
12546 };
12547 let src = proc
12548 .create_image(
12549 w,
12550 h,
12551 PixelFormat::Nv12,
12552 DType::U8,
12553 mem,
12554 edgefirst_tensor::CpuAccess::ReadWrite,
12555 )
12556 .unwrap();
12557 {
12558 let t = src.as_typed::<u8>().unwrap();
12559 let mut m = t.map().unwrap();
12560 let s = m.as_mut_slice();
12561 for (j, b) in s[..w * h].iter_mut().enumerate() {
12562 *b = ((i * 53 + j) % 200 + 16) as u8;
12563 }
12564 for b in &mut s[w * h..] {
12565 *b = (80 + i * 24) as u8;
12566 }
12567 }
12568 let lb = Crop::letterbox([114, 114, 114, 255]);
12569 let convert_once = |proc: &mut ImageProcessor| -> Vec<u8> {
12570 let mut dst = proc
12571 .create_image(
12572 320,
12573 320,
12574 PixelFormat::Rgba,
12575 DType::U8,
12576 mem,
12577 edgefirst_tensor::CpuAccess::ReadWrite,
12578 )
12579 .unwrap();
12580 proc.convert(&src, &mut dst, Rotation::None, Flip::None, lb)
12581 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
12582 let t = dst.as_typed::<u8>().unwrap();
12583 let m = t.map().unwrap();
12584 m.as_slice().to_vec()
12585 };
12586
12587 let oracle = convert_once(&mut proc);
12588 barrier.wait();
12589 for round in 0..ROUNDS {
12590 let out = convert_once(&mut proc);
12591 let diffs = oracle.iter().zip(&out).filter(|(a, b)| a != b).count();
12592 assert!(
12593 diffs == 0,
12594 "thread {i} round {round}: {diffs}/{} bytes diverged \
12595 from this processor's own oracle — cross-processor \
12596 GL state leakage under parallel execution",
12597 oracle.len()
12598 );
12599 }
12600 })
12601 })
12602 .collect();
12603
12604 for (i, h) in handles.into_iter().enumerate() {
12605 h.join()
12606 .unwrap_or_else(|e| panic!("parallel thread {i} panicked: {e:?}"));
12607 }
12608 let _ = tx.send(());
12609 });
12610
12611 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
12612 panic!("test_parallel_processors_unique_outputs timed out after {TIMEOUT:?}")
12613 });
12614 }
12615
12616 #[test]
12625 #[ignore = "heavy on-demand GL-parallelism stressor; run explicitly on boards"]
12626 fn stress_parallel_processors_oracle() {
12627 use std::sync::{mpsc, Arc, Barrier};
12628 use std::time::Duration;
12629
12630 const N: usize = 4;
12631 const ROUNDS: usize = 200;
12632 const TIMEOUT: Duration = Duration::from_secs(600);
12633
12634 let _lock = acquire_env_lock();
12635 let (tx, rx) = mpsc::channel::<()>();
12636
12637 std::thread::spawn(move || {
12638 let barrier = Arc::new(Barrier::new(N));
12639 let handles: Vec<_> = (0..N)
12640 .map(|i| {
12641 let barrier = Arc::clone(&barrier);
12642 std::thread::spawn(move || {
12643 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
12644 panic!("ImageProcessor::new() failed on thread {i}: {e}")
12645 });
12646 let (w, h) = (1280usize, 720usize);
12647 let mem = if edgefirst_tensor::is_dma_available() {
12648 Some(TensorMemory::DmaBuf)
12649 } else {
12650 Some(TensorMemory::Mem)
12651 };
12652
12653 let src = proc
12656 .create_image(
12657 w,
12658 h,
12659 PixelFormat::Nv12,
12660 DType::U8,
12661 mem,
12662 edgefirst_tensor::CpuAccess::ReadWrite,
12663 )
12664 .unwrap();
12665 {
12666 let t = src.as_typed::<u8>().unwrap();
12667 let mut m = t.map().unwrap();
12668 let s = m.as_mut_slice();
12669 for (j, b) in s[..w * h].iter_mut().enumerate() {
12670 *b = ((i * 37 + j) % 200 + 16) as u8;
12671 }
12672 for b in &mut s[w * h..] {
12673 *b = (96 + i * 16) as u8;
12674 }
12675 }
12676 let lb = Crop::letterbox([114, 114, 114, 255]);
12677
12678 let convert_once = |proc: &mut ImageProcessor| -> Vec<u8> {
12679 let mut dst = proc
12680 .create_image(
12681 640,
12682 640,
12683 PixelFormat::Rgb,
12684 DType::U8,
12685 mem,
12686 edgefirst_tensor::CpuAccess::ReadWrite,
12687 )
12688 .unwrap();
12689 proc.convert(&src, &mut dst, Rotation::None, Flip::None, lb)
12690 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
12691 let t = dst.as_typed::<u8>().unwrap();
12692 let m = t.map().unwrap();
12693 m.as_slice().to_vec()
12694 };
12695
12696 let oracle = convert_once(&mut proc);
12697 barrier.wait();
12698 for round in 0..ROUNDS {
12699 let out = convert_once(&mut proc);
12700 let diffs = oracle.iter().zip(&out).filter(|(a, b)| a != b).count();
12701 assert!(
12702 diffs == 0,
12703 "thread {i} round {round}: {diffs}/{} bytes diverged \
12704 from the pre-barrier oracle",
12705 oracle.len()
12706 );
12707 }
12708 })
12709 })
12710 .collect();
12711
12712 for (i, h) in handles.into_iter().enumerate() {
12713 h.join()
12714 .unwrap_or_else(|e| panic!("stressor thread {i} panicked: {e:?}"));
12715 }
12716 let _ = tx.send(());
12717 });
12718
12719 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
12720 panic!("stress_parallel_processors_oracle timed out after {TIMEOUT:?}")
12721 });
12722 }
12723
12724 #[test]
12737 fn convert_f32_auto_never_errors_non_gl_combo() {
12738 const W: usize = 64;
12739 const H: usize = 64;
12740
12741 let src = TensorDyn::image(
12744 W,
12745 H,
12746 PixelFormat::Yuyv,
12747 DType::U8,
12748 Some(TensorMemory::Mem),
12749 edgefirst_tensor::CpuAccess::ReadWrite,
12750 )
12751 .unwrap();
12752 {
12753 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
12754 let data = map.as_mut_slice();
12755 for chunk in data.as_chunks_mut::<4>().0 {
12756 chunk[0] = 128; chunk[1] = 128; chunk[2] = 160; chunk[3] = 128; }
12761 }
12762
12763 let mut dst = TensorDyn::image(
12764 W,
12765 H,
12766 PixelFormat::Rgb,
12767 DType::F32,
12768 Some(TensorMemory::Mem),
12769 edgefirst_tensor::CpuAccess::ReadWrite,
12770 )
12771 .unwrap();
12772
12773 let mut proc = ImageProcessor::new().unwrap();
12774 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
12775 assert!(
12776 result.is_ok(),
12777 "auto-chain Yuyv→Rgb F32 must not error: {:?}",
12778 result.err()
12779 );
12780
12781 let map = dst.as_typed::<f32>().unwrap().map().unwrap();
12783 let floats = map.as_slice();
12784 assert_eq!(floats.len(), W * H * 3, "unexpected output element count");
12785 for (i, &v) in floats.iter().enumerate() {
12786 assert!(
12787 v.is_finite() && (0.0..=1.0).contains(&v),
12788 "output[{i}]={v} is not finite or not in [0,1]"
12789 );
12790 }
12791
12792 let first_non_zero = floats.iter().find(|&&v| v > 0.01);
12796 assert!(
12797 first_non_zero.is_some(),
12798 "all-zero output detected — CPU path likely did not write to the destination buffer"
12799 );
12800 let r0 = floats[0];
12803 assert!(
12804 (r0 - 0.502_f32).abs() < 0.05,
12805 "first pixel R={r0} expected ≈0.502 (Y=128 neutral grey from YUYV source)"
12806 );
12807 }
12808
12809 #[test]
12815 #[allow(clippy::needless_update)]
12821 fn convert_f16_forced_cpu_correct() {
12822 const W: usize = 16;
12823 const H: usize = 16;
12824 const TOL: f32 = 1.0 / 512.0; let src = TensorDyn::image(
12828 W,
12829 H,
12830 PixelFormat::Rgba,
12831 DType::U8,
12832 Some(TensorMemory::Mem),
12833 edgefirst_tensor::CpuAccess::ReadWrite,
12834 )
12835 .unwrap();
12836 {
12837 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
12838 let data = map.as_mut_slice();
12839 for y in 0..H {
12840 for x in 0..W {
12841 let i = y * W + x;
12842 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;
12846 }
12847 }
12848 }
12849
12850 let mut dst = TensorDyn::image(
12851 W,
12852 H,
12853 PixelFormat::PlanarRgb,
12854 DType::F16,
12855 Some(TensorMemory::Mem),
12856 edgefirst_tensor::CpuAccess::ReadWrite,
12857 )
12858 .unwrap();
12859
12860 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
12861 backend: ComputeBackend::Cpu,
12862 ..Default::default()
12863 })
12864 .unwrap();
12865 proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
12866 .expect("forced-CPU Rgba→PlanarRgb F16 must not error");
12867
12868 let src_map = src.as_typed::<u8>().unwrap().map().unwrap();
12869 let src_bytes = src_map.as_slice();
12870 let dst_map = dst.as_typed::<half::f16>().unwrap().map().unwrap();
12871 let dst_halfs = dst_map.as_slice();
12872
12873 let plane = W * H;
12874 assert_eq!(dst_halfs.len(), plane * 3, "wrong output element count");
12875
12876 for y in 0..H {
12877 for x in 0..W {
12878 let i = y * W + x;
12879 let r_exp = src_bytes[i * 4] as f32 / 255.0;
12880 let g_exp = src_bytes[i * 4 + 1] as f32 / 255.0;
12881 let b_exp = src_bytes[i * 4 + 2] as f32 / 255.0;
12882
12883 let r_got = dst_halfs[i].to_f32();
12884 let g_got = dst_halfs[plane + i].to_f32();
12885 let b_got = dst_halfs[2 * plane + i].to_f32();
12886
12887 assert!(
12888 (r_got - r_exp).abs() <= TOL,
12889 "R plane ({x},{y}): got {r_got}, expected {r_exp}"
12890 );
12891 assert!(
12892 (g_got - g_exp).abs() <= TOL,
12893 "G plane ({x},{y}): got {g_got}, expected {g_exp}"
12894 );
12895 assert!(
12896 (b_got - b_exp).abs() <= TOL,
12897 "B plane ({x},{y}): got {b_got}, expected {b_exp}"
12898 );
12899
12900 if src_bytes[i * 4] != src_bytes[i * 4 + 1] {
12902 assert_ne!(r_got, g_got, "R and G planes must differ at ({x},{y})");
12903 }
12904 }
12905 }
12906 }
12907
12908 #[test]
12916 fn convert_f32_with_rotation_falls_back() {
12917 const W: usize = 16;
12918 const H: usize = 16;
12919
12920 let src = TensorDyn::image(
12922 W,
12923 H,
12924 PixelFormat::Rgba,
12925 DType::U8,
12926 Some(TensorMemory::Mem),
12927 edgefirst_tensor::CpuAccess::ReadWrite,
12928 )
12929 .unwrap();
12930 {
12931 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
12932 let data = map.as_mut_slice();
12933 for y in 0..H {
12934 for x in 0..W {
12935 let i = y * W + x;
12936 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;
12940 }
12941 }
12942 }
12943
12944 let mut dst = TensorDyn::image(
12946 H, W, PixelFormat::Rgb,
12949 DType::F32,
12950 Some(TensorMemory::Mem),
12951 edgefirst_tensor::CpuAccess::ReadWrite,
12952 )
12953 .unwrap();
12954
12955 let mut proc = ImageProcessor::new().unwrap();
12956 let result = proc.convert(
12957 &src,
12958 &mut dst,
12959 Rotation::Clockwise90,
12960 Flip::None,
12961 Crop::default(),
12962 );
12963 assert!(
12964 result.is_ok(),
12965 "auto-chain Rgba→Rgb F32 with Rot90 must not error: {:?}",
12966 result.err()
12967 );
12968
12969 let map = dst.as_typed::<f32>().unwrap().map().unwrap();
12970 let floats = map.as_slice();
12971 assert_eq!(floats.len(), H * W * 3, "unexpected output element count");
12972 for (i, &v) in floats.iter().enumerate() {
12973 assert!(
12974 v.is_finite() && (0.0..=1.0).contains(&v),
12975 "output[{i}]={v} is not finite or not in [0,1]"
12976 );
12977 }
12978 }
12979
12980 #[test]
13006 #[cfg(all(
13007 any(
13008 target_os = "linux",
13009 target_os = "macos",
13010 target_os = "ios",
13011 target_os = "android",
13012 target_os = "windows"
13013 ),
13014 feature = "opengl"
13015 ))]
13016 fn convert_f16_gl_cpu_parity_identity() {
13017 #[cfg(target_os = "macos")]
13023 if std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1")
13024 && std::env::var_os("HAL_TEST_ALLOW_DLOPEN_ANGLE").is_none()
13025 {
13026 crate::test_support::report_skip(
13027 "convert_f16_gl_cpu_parity_identity - ANGLE dlopen gate closed (coverage pass 1)",
13028 );
13029 return;
13030 }
13031 if !is_opengl_available() {
13032 crate::test_support::report_skip(
13033 "convert_f16_gl_cpu_parity_identity - OpenGL not available",
13034 );
13035 return;
13036 }
13037
13038 const W: usize = 16;
13039 const H: usize = 16;
13040 const TOL: f32 = 1.0 / 256.0; let src = TensorDyn::image(
13044 W,
13045 H,
13046 PixelFormat::Rgba,
13047 DType::U8,
13048 Some(TensorMemory::Mem),
13049 edgefirst_tensor::CpuAccess::ReadWrite,
13050 )
13051 .unwrap();
13052 {
13053 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13054 let data = map.as_mut_slice();
13055 for y in 0..H {
13056 for x in 0..W {
13057 let i = y * W + x;
13058 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;
13062 }
13063 }
13064 }
13065
13066 let gl_result = {
13068 let mut gl_proc = match ImageProcessor::with_config(ImageProcessorConfig {
13069 backend: ComputeBackend::OpenGl,
13070 ..Default::default()
13071 }) {
13072 Ok(p) => p,
13073 Err(e) => {
13074 crate::test_support::report_skip(&format!(
13075 "convert_f16_gl_cpu_parity_identity - GL backend unavailable: {e}"
13076 ));
13077 return;
13078 }
13079 };
13080
13081 if !gl_proc.supported_render_dtypes().f16 {
13082 crate::test_support::report_skip(
13083 "convert_f16_gl_cpu_parity_identity - F16 render not supported",
13084 );
13085 return;
13086 }
13087
13088 let mut dst = match TensorDyn::image(
13108 W,
13109 H,
13110 PixelFormat::PlanarRgb,
13111 DType::F16,
13112 Some(TensorMemory::DmaBuf),
13113 edgefirst_tensor::CpuAccess::ReadWrite,
13114 ) {
13115 Ok(t) if t.memory() == TensorMemory::DmaBuf => t,
13116 Ok(t) => {
13121 crate::test_support::report_skip(&format!(
13122 "convert_f16_gl_cpu_parity_identity - the zero-copy F16 \
13123 destination fell back to {:?}",
13124 t.memory()
13125 ));
13126 return;
13127 }
13128 Err(e) => {
13129 crate::test_support::report_skip(&format!(
13130 "convert_f16_gl_cpu_parity_identity - no zero-copy F16 \
13131 destination on this host: {e}"
13132 ));
13133 return;
13134 }
13135 };
13136 let require_gl = std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1");
13137 let fallbacks_before = gl_proc.convert_fallback_count();
13138 match gl_proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default()) {
13139 Ok(()) => {
13140 if gl_proc.convert_fallback_count() != fallbacks_before {
13141 assert!(
13142 !require_gl,
13143 "HAL_TEST_REQUIRE_GL=1 but \
13144 convert_f16_gl_cpu_parity_identity would have skipped \
13145 while reporting success: the GL F16 path declined and \
13146 the CPU served the convert"
13147 );
13148 crate::test_support::report_skip(
13149 "convert_f16_gl_cpu_parity_identity - the GL F16 path \
13150 declined and the CPU served the convert; comparing that \
13151 against the forced-CPU result would prove nothing",
13152 );
13153 return;
13154 }
13155 dst
13156 }
13157 Err(e) => {
13158 crate::test_support::report_skip(&format!(
13159 "convert_f16_gl_cpu_parity_identity - GL convert failed: {e}"
13160 ));
13161 return;
13162 }
13163 }
13164 };
13165
13166 let cpu_result = {
13168 let mut cpu_proc = ImageProcessor::with_config(ImageProcessorConfig {
13169 backend: ComputeBackend::Cpu,
13170 ..Default::default()
13171 })
13172 .unwrap();
13173 let mut dst = TensorDyn::image(
13174 W,
13175 H,
13176 PixelFormat::PlanarRgb,
13177 DType::F16,
13178 Some(TensorMemory::Mem),
13179 edgefirst_tensor::CpuAccess::ReadWrite,
13180 )
13181 .unwrap();
13182 cpu_proc
13183 .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
13184 .expect("forced-CPU Rgba→PlanarRgb F16 must not error");
13185 dst
13186 };
13187
13188 let flat_f16 = |t: &TensorDyn| -> Vec<half::f16> {
13193 let typed = t.as_typed::<half::f16>().expect("F16 image");
13194 let mut bytes = vec![0u8; typed.shape().iter().product::<usize>() * 2];
13195 typed
13196 .copy_to_flat(&mut bytes)
13197 .expect("compact any padded rows");
13198 bytes
13199 .as_chunks::<2>()
13200 .0
13201 .iter()
13202 .map(|&c| half::f16::from_le_bytes(c))
13203 .collect()
13204 };
13205 let gl_halfs = flat_f16(&gl_result);
13206 let cpu_halfs = flat_f16(&cpu_result);
13207
13208 assert_eq!(
13209 gl_halfs.len(),
13210 cpu_halfs.len(),
13211 "GL and CPU output sizes differ"
13212 );
13213
13214 let plane = W * H;
13215 let channel_names = ["R", "G", "B"];
13216 for (idx, (gl_h, cpu_h)) in gl_halfs.iter().zip(cpu_halfs.iter()).enumerate() {
13217 let gl_v = gl_h.to_f32();
13218 let cpu_v = cpu_h.to_f32();
13219 let err = (gl_v - cpu_v).abs();
13220 let ch = channel_names[idx / plane];
13221 let pixel = idx % plane;
13222 assert!(
13223 err <= TOL,
13224 "GL vs CPU mismatch at {ch}[{pixel}]: GL={gl_v}, CPU={cpu_v}, err={err} > tol={TOL}"
13225 );
13226 }
13227 }
13228
13229 #[test]
13237 #[cfg(all(any(target_os = "linux", target_os = "windows"), feature = "opengl"))]
13238 fn supported_render_dtypes_linux_smoke() {
13239 let proc = match ImageProcessor::new() {
13240 Ok(p) => p,
13241 Err(e) => {
13242 crate::test_support::report_skip(&format!(
13243 "supported_render_dtypes_linux_smoke — ImageProcessor::new() failed: {e}"
13244 ));
13245 return;
13246 }
13247 };
13248 if proc.opengl.is_none() {
13249 crate::test_support::report_skip(
13250 "supported_render_dtypes_linux_smoke — no GL backend on this host",
13251 );
13252 return;
13253 }
13254 let support = proc.supported_render_dtypes();
13256 eprintln!(
13257 "supported_render_dtypes_linux_smoke: f16={} f32={}",
13258 support.f16, support.f32
13259 );
13260 }
13262
13263 #[test]
13272 fn convert_f16_pbo_non_4_aligned_width_falls_back() {
13273 const W: usize = 18; const H: usize = 16;
13275
13276 let src = TensorDyn::image(
13278 W,
13279 H,
13280 PixelFormat::Rgba,
13281 DType::U8,
13282 Some(TensorMemory::Mem),
13283 edgefirst_tensor::CpuAccess::ReadWrite,
13284 )
13285 .unwrap();
13286 {
13287 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13288 let data = map.as_mut_slice();
13289 for chunk in data.as_chunks_mut::<4>().0 {
13290 chunk[0] = 128;
13291 chunk[1] = 64;
13292 chunk[2] = 200;
13293 chunk[3] = 255;
13294 }
13295 }
13296
13297 let mut dst = TensorDyn::image(
13300 W,
13301 H,
13302 PixelFormat::PlanarRgb,
13303 DType::F16,
13304 Some(TensorMemory::Mem),
13305 edgefirst_tensor::CpuAccess::ReadWrite,
13306 )
13307 .unwrap();
13308
13309 let mut proc = ImageProcessor::new().unwrap();
13312 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
13313 assert!(
13314 result.is_ok(),
13315 "auto-chain PlanarRgb F16 W%4!=0 must not error (CPU fallback): {:?}",
13316 result.err()
13317 );
13318
13319 let map = dst.as_typed::<half::f16>().unwrap().map().unwrap();
13321 let halfs = map.as_slice();
13322 assert_eq!(halfs.len(), W * H * 3, "unexpected element count");
13323 for (i, h) in halfs.iter().enumerate() {
13324 let v = h.to_f32();
13325 assert!(
13326 v.is_finite() && (0.0..=1.0).contains(&v),
13327 "output[{i}]={v} is not finite or not in [0,1]"
13328 );
13329 }
13330 }
13331
13332 #[test]
13342 #[allow(clippy::needless_update)]
13345 fn convert_nv12_to_rgb_f32_cpu() {
13346 const W: usize = 16;
13347 const H: usize = 16; let src = TensorDyn::image(
13351 W,
13352 H,
13353 PixelFormat::Nv12,
13354 DType::U8,
13355 Some(TensorMemory::Mem),
13356 edgefirst_tensor::CpuAccess::ReadWrite,
13357 )
13358 .unwrap();
13359 {
13360 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13361 map.as_mut_slice().fill(128); }
13363
13364 let mut dst = TensorDyn::image(
13365 W,
13366 H,
13367 PixelFormat::Rgb,
13368 DType::F32,
13369 Some(TensorMemory::Mem),
13370 edgefirst_tensor::CpuAccess::ReadWrite,
13371 )
13372 .unwrap();
13373
13374 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
13375 backend: ComputeBackend::Cpu,
13376 ..Default::default()
13377 })
13378 .unwrap();
13379
13380 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
13381 assert!(
13382 result.is_ok(),
13383 "forced-CPU NV12→Rgb F32 must not error: {:?}",
13384 result.err()
13385 );
13386
13387 let map = dst.as_typed::<f32>().unwrap().map().unwrap();
13388 let floats = map.as_slice();
13389 assert_eq!(floats.len(), W * H * 3, "unexpected element count");
13390 for (i, &v) in floats.iter().enumerate() {
13391 assert!(
13392 v.is_finite() && (0.0..=1.0).contains(&v),
13393 "output[{i}]={v} is not finite or not in [0,1]"
13394 );
13395 }
13396 let non_zero = floats.iter().any(|&v| v > 0.01);
13398 assert!(non_zero, "all-zero output from NV12→Rgb F32 CPU path");
13399 }
13400
13401 #[test]
13405 #[allow(clippy::needless_update)]
13408 fn convert_nv12_to_planar_rgb_f16_cpu() {
13409 const W: usize = 16;
13410 const H: usize = 16;
13411
13412 let src = TensorDyn::image(
13413 W,
13414 H,
13415 PixelFormat::Nv12,
13416 DType::U8,
13417 Some(TensorMemory::Mem),
13418 edgefirst_tensor::CpuAccess::ReadWrite,
13419 )
13420 .unwrap();
13421 {
13422 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13423 map.as_mut_slice().fill(128);
13424 }
13425
13426 let mut dst = TensorDyn::image(
13427 W,
13428 H,
13429 PixelFormat::PlanarRgb,
13430 DType::F16,
13431 Some(TensorMemory::Mem),
13432 edgefirst_tensor::CpuAccess::ReadWrite,
13433 )
13434 .unwrap();
13435
13436 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
13437 backend: ComputeBackend::Cpu,
13438 ..Default::default()
13439 })
13440 .unwrap();
13441
13442 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
13443 assert!(
13444 result.is_ok(),
13445 "forced-CPU NV12→PlanarRgb F16 must not error: {:?}",
13446 result.err()
13447 );
13448
13449 let map = dst.as_typed::<half::f16>().unwrap().map().unwrap();
13450 let halfs = map.as_slice();
13451 assert_eq!(halfs.len(), W * H * 3, "unexpected element count");
13452 for (i, h) in halfs.iter().enumerate() {
13453 let v = h.to_f32();
13454 assert!(
13455 v.is_finite() && (0.0..=1.0).contains(&v),
13456 "output[{i}]={v} is not finite or not in [0,1]"
13457 );
13458 }
13459 let non_zero = halfs.iter().any(|h| h.to_f32() > 0.01);
13460 assert!(non_zero, "all-zero output from NV12→PlanarRgb F16 CPU path");
13461 }
13462
13463 #[test]
13472 fn create_image_desc_negotiates_and_counts_fallbacks() {
13473 use edgefirst_tensor::{CpuAccess, ImageDesc};
13474 let proc = ImageProcessor::new().unwrap();
13475
13476 let desc =
13477 ImageDesc::new(64, 64, PixelFormat::Rgba, DType::U8).with_access(CpuAccess::ReadWrite);
13478 let plain = proc.create_image_desc(&desc).unwrap();
13479 let classic = proc
13480 .create_image(
13481 64,
13482 64,
13483 PixelFormat::Rgba,
13484 DType::U8,
13485 None,
13486 CpuAccess::ReadWrite,
13487 )
13488 .unwrap();
13489 assert_eq!(plain.memory(), classic.memory(), "same negotiation path");
13490 assert_eq!(plain.compression(), None);
13491
13492 #[cfg(not(target_os = "android"))]
13493 {
13494 use edgefirst_tensor::Compression;
13495 let before = proc.compression_fallback_count();
13496 let desc = ImageDesc::new(64, 64, PixelFormat::Rgba, DType::U8)
13497 .with_compression(Compression::Any);
13498 let t = proc.create_image_desc(&desc).unwrap();
13499 assert_eq!(t.compression(), None, "no vendor tile scheme off-Android");
13500 assert!(
13501 proc.compression_fallback_count() > before,
13502 "Any resolving linear must count"
13503 );
13504 }
13505 }
13506
13507 #[test]
13510 #[cfg(target_os = "linux")]
13511 fn create_image_f32_dma_rejected() {
13512 let proc = ImageProcessor::new().unwrap();
13513 let result = proc.create_image(
13514 64,
13515 64,
13516 PixelFormat::Rgb,
13517 DType::F32,
13518 Some(TensorMemory::DmaBuf),
13519 edgefirst_tensor::CpuAccess::ReadWrite,
13520 );
13521 assert!(
13522 result.is_err(),
13523 "create_image(F32, Dma) must fail — no DRM fourcc for f32"
13524 );
13525 }
13526
13527 #[test]
13536 #[cfg(target_os = "linux")]
13537 fn import_image_carries_colorimetry() {
13538 use edgefirst_tensor::{ColorEncoding, ColorRange, Colorimetry, TensorMemory};
13539
13540 let expected = Colorimetry::default()
13541 .with_encoding(ColorEncoding::Bt709)
13542 .with_range(ColorRange::Limited);
13543
13544 if !is_dma_available() {
13545 let mut t = TensorDyn::image(
13548 8,
13549 8,
13550 PixelFormat::Rgba,
13551 DType::U8,
13552 Some(TensorMemory::Mem),
13553 edgefirst_tensor::CpuAccess::ReadWrite,
13554 )
13555 .expect("alloc");
13556 assert_eq!(t.colorimetry(), None, "colorimetry must start as None");
13557 t.set_colorimetry(Some(expected));
13558 assert_eq!(
13559 t.colorimetry(),
13560 Some(expected),
13561 "set_colorimetry must round-trip"
13562 );
13563 crate::test_support::report_skip("import_image_carries_colorimetry (DMA unavailable); storage contract verified via TensorDyn");
13564 return;
13565 }
13566
13567 use edgefirst_tensor::{PlaneDescriptor, Tensor};
13570
13571 let rgba_bytes = 64 * 64 * 4; let dma_tensor = Tensor::<u8>::new(
13573 &[rgba_bytes],
13574 Some(TensorMemory::DmaBuf),
13575 Some("import_test"),
13576 )
13577 .expect("dma alloc");
13578 let pd =
13579 PlaneDescriptor::new(dma_tensor.dmabuf().expect("dma fd")).expect("PlaneDescriptor");
13580
13581 let proc = ImageProcessor::new().expect("ImageProcessor");
13582 let result = proc.import_image(
13583 pd,
13584 None,
13585 64,
13586 64,
13587 PixelFormat::Rgba,
13588 DType::U8,
13589 Some(expected),
13590 );
13591 let tensor = result.expect("import_image must succeed on DMA fd");
13592 assert_eq!(
13593 tensor.colorimetry(),
13594 Some(expected),
13595 "import_image must store the supplied colorimetry on the returned TensorDyn"
13596 );
13597 }
13598}