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