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