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#[cfg(any(
328 test,
329 all(any(target_os = "linux", target_os = "windows"), feature = "opengl")
330))]
331use edgefirst_tensor::Tensor;
332#[doc(inline)]
333pub use edgefirst_tensor::{
334 fit_code, interpolation_code, letterbox_placement_code, normalization_code, ContentRegion,
335 ContentRegionSource, Fit, FitMode, Interpolation, LetterboxPlacement, Normalization,
336 NormalizationPreset, PadSpace, PreprocessSpec, Region,
337};
338use edgefirst_tensor::{
339 DType, PixelFormat, PixelLayout, TensorDyn, TensorMemory, TensorTrait as _,
340};
341use edgefirst_tensor::{DetectBox, ProtoData, Segmentation};
342use enum_dispatch::enum_dispatch;
343pub use error::{Error, Result};
344#[cfg(target_os = "linux")]
345pub use g2d::G2DProcessor;
346#[cfg(unix)]
349pub type CompletionFence = std::os::fd::OwnedFd;
350#[cfg(windows)]
353pub type CompletionFence = std::os::windows::io::OwnedHandle;
354#[cfg(all(
355 any(
356 target_os = "linux",
357 target_os = "macos",
358 target_os = "ios",
359 target_os = "android",
360 target_os = "windows"
361 ),
362 feature = "opengl"
363))]
364pub use opengl_headless::EglDisplayKind;
365#[cfg(all(
366 any(
367 target_os = "linux",
368 target_os = "macos",
369 target_os = "ios",
370 target_os = "android",
371 target_os = "windows"
372 ),
373 feature = "opengl"
374))]
375pub use opengl_headless::GLProcessorThreaded;
376#[cfg(all(
377 any(
378 target_os = "linux",
379 target_os = "macos",
380 target_os = "ios",
381 target_os = "android",
382 target_os = "windows"
383 ),
384 feature = "opengl"
385))]
386pub use opengl_headless::Int8InterpolationMode;
387#[cfg(target_os = "linux")]
388#[cfg(feature = "opengl")]
389pub use opengl_headless::{probe_egl_displays, EglDisplayInfo};
390#[cfg(all(
394 any(
395 target_os = "linux",
396 target_os = "macos",
397 target_os = "ios",
398 target_os = "android",
399 target_os = "windows"
400 ),
401 feature = "opengl"
402))]
403pub use opengl_headless::{CacheStats, ConvertStats, GlCacheStats};
404use std::{fmt::Display, time::Instant};
405
406mod colorimetry;
407mod cpu;
408mod error;
409mod g2d;
410#[path = "gl/mod.rs"]
411mod opengl_headless;
412mod options;
413pub use options::ConvertOptions;
414#[cfg(test)]
415mod test_support;
416mod tiling;
417pub use tiling::{tile_grid, TilePlacement, TileSpec, TilingConfig};
418
419edgefirst_tensor::ef_vocabulary! {
428 #[derive(Default)]
430 pub enum Rotation {
431 #[default]
432 None = 0, "none", NONE,
433 Clockwise90 = 1, "clockwise90", CLOCKWISE90,
434 Rotate180 = 2, "rotate180", ROTATE180,
435 CounterClockwise90 = 3, "counter_clockwise90", COUNTER_CLOCKWISE90,
436 }
437 #[doc(hidden)]
438 pub mod rotation_code;
439}
440
441impl Rotation {
442 pub fn from_degrees_clockwise(angle: usize) -> Rotation {
455 match angle.rem_euclid(360) {
456 0 => Rotation::None,
457 90 => Rotation::Clockwise90,
458 180 => Rotation::Rotate180,
459 270 => Rotation::CounterClockwise90,
460 _ => panic!("rotation angle is not a multiple of 90"),
461 }
462 }
463}
464
465edgefirst_tensor::ef_vocabulary! {
466 #[derive(Default)]
468 pub enum Flip {
469 #[default]
470 None = 0, "none", NONE,
471 Vertical = 1, "vertical", VERTICAL,
473 Horizontal = 2, "horizontal", HORIZONTAL,
475 }
476 #[doc(hidden)]
477 pub mod flip_code;
478}
479
480edgefirst_tensor::ef_vocabulary! {
481 #[derive(Default)]
483 pub enum ColorMode {
484 #[default]
489 Class = 0, "class", CLASS,
490 Instance = 1, "instance", INSTANCE,
495 Track = 2, "track", TRACK,
498 }
499 #[doc(hidden)]
500 pub mod color_mode_code;
501}
502
503impl ColorMode {
504 #[inline]
506 pub fn index(self, idx: usize, label: usize) -> usize {
507 match self {
508 ColorMode::Class => label,
509 ColorMode::Instance | ColorMode::Track => idx,
510 }
511 }
512}
513
514#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
535pub enum MaskResolution {
536 #[default]
538 Proto,
539 Scaled {
543 width: u32,
545 height: u32,
547 },
548}
549
550#[derive(Debug, Clone, Copy)]
566pub struct MaskOverlay<'a> {
567 pub background: Option<&'a TensorDyn>,
571 pub opacity: f32,
572 pub content_region: Option<[f32; 4]>,
583 pub color_mode: ColorMode,
584}
585
586impl Default for MaskOverlay<'_> {
587 fn default() -> Self {
588 Self {
589 background: None,
590 opacity: 1.0,
591 content_region: None,
592 color_mode: ColorMode::Class,
593 }
594 }
595}
596
597impl<'a> MaskOverlay<'a> {
598 pub fn new() -> Self {
599 Self::default()
600 }
601
602 pub fn with_background(mut self, bg: &'a TensorDyn) -> Self {
610 self.background = Some(bg);
611 self
612 }
613
614 pub fn with_opacity(mut self, opacity: f32) -> Self {
615 self.opacity = opacity.clamp(0.0, 1.0);
616 self
617 }
618
619 pub fn with_color_mode(mut self, mode: ColorMode) -> Self {
620 self.color_mode = mode;
621 self
622 }
623
624 pub fn with_convert_options(
633 mut self,
634 options: &ConvertOptions,
635 src_w: usize,
636 src_h: usize,
637 model_w: usize,
638 model_h: usize,
639 ) -> Self {
640 if let Ok(region) = options.content_region(src_w, src_h, model_w, model_h) {
641 self.content_region = region.letterboxed();
642 }
643 self
644 }
645}
646
647fn copy_segmentation(s: &Segmentation, bbox: [f32; 4]) -> Result<Segmentation> {
653 use edgefirst_tensor::TensorMapTrait as _;
654 let src = s
655 .segmentation
656 .as_typed::<u8>()
657 .ok_or_else(|| Error::InvalidShape("segmentation must be a U8 tensor".into()))?;
658 let map = src.map_read()?;
659 let dst = edgefirst_tensor::Tensor::<u8>::new(
666 s.segmentation.shape(),
667 Some(edgefirst_tensor::TensorMemory::Mem),
668 None,
669 )
670 .map_err(|e| Error::InvalidShape(format!("segmentation copy: {e}")))?;
671 {
672 let mut dmap = dst.map_mut()?;
673 dmap.as_mut_slice().copy_from_slice(map.as_slice());
674 }
675 Ok(Segmentation {
676 xmin: bbox[0],
677 ymin: bbox[1],
678 xmax: bbox[2],
679 ymax: bbox[3],
680 segmentation: dst.into(),
681 })
682}
683
684#[inline]
696fn unletter_bbox(bbox: DetectBox, lb: [f32; 4]) -> DetectBox {
697 DetectBox {
698 bbox: edgefirst_tensor::unletter_norm(bbox.bbox, lb),
699 ..bbox
700 }
701}
702
703#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
708pub struct Crop {
709 pub source: Option<Region>,
711 pub fit: Fit,
713}
714
715impl Crop {
716 pub fn new() -> Self {
718 Self::default()
719 }
720
721 pub fn no_crop() -> Self {
723 Self::default()
724 }
725
726 pub fn letterbox(pad: [u8; 4]) -> Self {
729 Self {
730 source: None,
731 fit: Fit::Letterbox { pad },
732 }
733 }
734
735 pub fn with_source(mut self, source: Option<Region>) -> Self {
737 self.source = source;
738 self
739 }
740
741 pub fn with_fit(mut self, fit: Fit) -> Self {
743 self.fit = fit;
744 self
745 }
746
747 pub(crate) fn resolve(
753 &self,
754 src_w: usize,
755 src_h: usize,
756 dst_w: usize,
757 dst_h: usize,
758 placement: LetterboxPlacement,
759 ) -> Result<ResolvedCrop, Error> {
760 let src_rect = self.source.map(region_to_rect);
761 let (sw, sh) = match self.source {
764 Some(r) => (r.width, r.height),
765 None => (src_w, src_h),
766 };
767 let resolved = match self.fit {
768 Fit::Stretch => ResolvedCrop {
769 src_rect,
770 dst_rect: None,
771 dst_color: None,
772 },
773 Fit::Letterbox { pad } => ResolvedCrop {
774 src_rect,
775 dst_rect: Some(letterbox_rect(sw, sh, dst_w, dst_h, placement)),
776 dst_color: Some(pad),
777 },
778 };
779 resolved.check_crop_dims(src_w, src_h, dst_w, dst_h)?;
780 Ok(resolved)
781 }
782
783 pub fn check_crop_dyn(
785 &self,
786 src: &edgefirst_tensor::TensorDyn,
787 dst: &edgefirst_tensor::TensorDyn,
788 ) -> Result<(), Error> {
789 self.resolve(
790 src.width().unwrap_or(0),
791 src.height().unwrap_or(0),
792 dst.width().unwrap_or(0),
793 dst.height().unwrap_or(0),
794 LetterboxPlacement::Center,
795 )
796 .map(|_| ())
797 }
798}
799
800#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
804pub(crate) struct ResolvedCrop {
805 pub(crate) src_rect: Option<Rect>,
806 pub(crate) dst_rect: Option<Rect>,
807 pub(crate) dst_color: Option<[u8; 4]>,
808}
809
810impl ResolvedCrop {
811 #[allow(dead_code)] pub(crate) fn no_crop() -> Self {
814 Self::default()
815 }
816
817 #[allow(dead_code)] pub(crate) fn resizes(
822 &self,
823 (src_w, src_h): (usize, usize),
824 (dst_w, dst_h): (usize, usize),
825 rotation: Rotation,
826 ) -> bool {
827 let (sw, sh) = self
828 .src_rect
829 .map_or((src_w, src_h), |r| (r.width, r.height));
830 let (dw, dh) = self
831 .dst_rect
832 .map_or((dst_w, dst_h), |r| (r.width, r.height));
833 let (sw, sh) = match rotation {
834 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (sh, sw),
835 Rotation::None | Rotation::Rotate180 => (sw, sh),
836 };
837 (sw, sh) != (dw, dh)
838 }
839
840 pub(crate) fn check_crop_dims(
842 &self,
843 src_w: usize,
844 src_h: usize,
845 dst_w: usize,
846 dst_h: usize,
847 ) -> Result<(), Error> {
848 let src_ok = self
849 .src_rect
850 .is_none_or(|r| r.left + r.width <= src_w && r.top + r.height <= src_h);
851 let dst_ok = self
852 .dst_rect
853 .is_none_or(|r| r.left + r.width <= dst_w && r.top + r.height <= dst_h);
854 match (src_ok, dst_ok) {
855 (true, true) => Ok(()),
856 (true, false) => Err(Error::CropInvalid(format!(
857 "Dest crop invalid: {:?}",
858 self.dst_rect
859 ))),
860 (false, true) => Err(Error::CropInvalid(format!(
861 "Src crop invalid: {:?}",
862 self.src_rect
863 ))),
864 (false, false) => Err(Error::CropInvalid(format!(
865 "Dest and Src crop invalid: {:?} {:?}",
866 self.dst_rect, self.src_rect
867 ))),
868 }
869 }
870}
871
872fn region_to_rect(r: Region) -> Rect {
874 Rect {
875 left: r.x,
876 top: r.y,
877 width: r.width,
878 height: r.height,
879 }
880}
881
882fn letterbox_rect(
884 sw: usize,
885 sh: usize,
886 dw: usize,
887 dh: usize,
888 placement: LetterboxPlacement,
889) -> Rect {
890 let r = edgefirst_tensor::letterbox_rect(sw, sh, dw, dh, placement);
891 Rect::new(r.x, r.y, r.width, r.height)
892}
893
894#[derive(Debug, Clone, Copy, PartialEq, Eq)]
899pub(crate) struct Rect {
900 pub left: usize,
901 pub top: usize,
902 pub width: usize,
903 pub height: usize,
904}
905
906impl Rect {
907 pub fn new(left: usize, top: usize, width: usize, height: usize) -> Self {
909 Self {
910 left,
911 top,
912 width,
913 height,
914 }
915 }
916}
917
918#[enum_dispatch(ImageProcessor)]
919pub trait ImageProcessorTrait {
920 fn convert(
962 &mut self,
963 src: &TensorDyn,
964 dst: &mut TensorDyn,
965 options: Option<ConvertOptions>,
966 ) -> Result<()>;
967
968 fn draw_decoded_masks(
1034 &mut self,
1035 dst: &mut TensorDyn,
1036 detect: &[DetectBox],
1037 segmentation: &[Segmentation],
1038 overlay: MaskOverlay<'_>,
1039 ) -> Result<()>;
1040
1041 fn draw_proto_masks(
1061 &mut self,
1062 dst: &mut TensorDyn,
1063 detect: &[DetectBox],
1064 proto_data: &ProtoData,
1065 overlay: MaskOverlay<'_>,
1066 ) -> Result<()>;
1067
1068 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()>;
1083
1084 fn set_box_line_width(&mut self, width: u32) -> Result<()>;
1100
1101 fn convert_deferred(
1118 &mut self,
1119 src: &TensorDyn,
1120 dst: &mut TensorDyn,
1121 options: Option<ConvertOptions>,
1122 ) -> Result<()> {
1123 self.convert(src, dst, options)
1124 }
1125
1126 fn flush(&mut self) -> Result<()> {
1133 Ok(())
1134 }
1135}
1136
1137#[derive(Debug, Clone, Default)]
1143pub struct ImageProcessorConfig {
1144 #[cfg(all(
1155 any(
1156 target_os = "linux",
1157 target_os = "macos",
1158 target_os = "ios",
1159 target_os = "android",
1160 target_os = "windows"
1161 ),
1162 feature = "opengl"
1163 ))]
1164 pub egl_display: Option<EglDisplayKind>,
1165
1166 pub backend: ComputeBackend,
1178
1179 pub colorimetry: ColorimetryMode,
1184
1185 pub interpolation: Interpolation,
1192
1193 pub egl_cache_capacity: Option<usize>,
1252}
1253
1254#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1269pub enum ColorimetryMode {
1270 #[default]
1274 Fast,
1275 Exact,
1278}
1279
1280pub(crate) fn runnable_interpolation(mode: Interpolation) -> Interpolation {
1284 let run = mode.supported();
1285 if run != mode && warn_first_fallback(mode) {
1286 log::warn!(
1287 "{} interpolation is not implemented; using {}",
1288 mode.as_str(),
1289 run.as_str()
1290 );
1291 }
1292 run
1293}
1294
1295static INTERPOLATION_FALLBACKS_WARNED: std::sync::atomic::AtomicU32 =
1297 std::sync::atomic::AtomicU32::new(0);
1298
1299fn warn_first_fallback(mode: Interpolation) -> bool {
1301 let bit = 1u32 << (mode.code() % 32);
1302 INTERPOLATION_FALLBACKS_WARNED.fetch_or(bit, std::sync::atomic::Ordering::Relaxed) & bit == 0
1303}
1304
1305pub(crate) fn interpolation_from_env() -> Option<Interpolation> {
1308 let value = std::env::var("EDGEFIRST_INTERPOLATION").ok()?;
1309 let value = value.trim().to_ascii_lowercase();
1310 if value.is_empty() {
1311 return None;
1312 }
1313 let mode = Interpolation::from_str_code(&value);
1314 if mode.is_none() {
1315 log::warn!(
1316 "EDGEFIRST_INTERPOLATION={value:?} is not recognised \
1317 (expected nearest or bilinear); ignoring it"
1318 );
1319 }
1320 mode
1321}
1322
1323edgefirst_tensor::ef_vocabulary! {
1324 #[derive(Default)]
1333 pub enum ComputeBackend {
1334 #[default]
1336 Auto = 0, "auto", AUTO,
1337 Cpu = 1, "cpu", CPU,
1339 G2d = 2, "g2d", G2D,
1341 OpenGl = 3, "opengl", OPENGL,
1343 }
1344 #[doc(hidden)]
1345 pub mod compute_backend_code;
1346}
1347
1348#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1354pub(crate) enum ForcedBackend {
1355 Cpu,
1356 G2d,
1357 OpenGl,
1358}
1359
1360fn with_gpu_declines(e: Error, declined: Vec<(&'static str, Error)>) -> Error {
1365 match e {
1366 Error::Tensor(denied @ edgefirst_tensor::Error::CpuAccessDenied { .. })
1367 if !declined.is_empty() =>
1368 {
1369 let declined: Vec<String> = declined
1370 .iter()
1371 .map(|(backend, e)| format!("{backend}: {e}"))
1372 .collect();
1373 Error::CpuFallbackDenied {
1374 declined: declined.join("; "),
1375 denied,
1376 }
1377 }
1378 other => other,
1379 }
1380}
1381
1382fn predicted_cpu_access(
1390 forced: Option<ForcedBackend>,
1391 gl: Option<Option<edgefirst_tensor::CpuAccess>>,
1392 g2d: Option<Option<edgefirst_tensor::CpuAccess>>,
1393 cpu: Option<Option<edgefirst_tensor::CpuAccess>>,
1394) -> edgefirst_tensor::CpuAccess {
1395 use edgefirst_tensor::CpuAccess;
1396 let only = |b: Option<Option<CpuAccess>>| b.flatten().unwrap_or(CpuAccess::None);
1397 match forced {
1398 Some(ForcedBackend::Cpu) => return only(cpu),
1399 Some(ForcedBackend::G2d) => return only(g2d),
1400 Some(ForcedBackend::OpenGl) => return only(gl),
1401 None => {}
1402 }
1403 let gl = gl.flatten();
1404 if gl == Some(CpuAccess::None) {
1405 return CpuAccess::None;
1406 }
1407 [gl, g2d.flatten(), cpu.flatten()]
1408 .into_iter()
1409 .flatten()
1410 .fold(CpuAccess::None, CpuAccess::union)
1411}
1412
1413#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1438pub struct RenderDtypeSupport {
1439 pub f32: bool,
1446 pub f16: bool,
1454}
1455
1456#[cfg(all(
1471 any(
1472 target_os = "linux",
1473 target_os = "windows",
1474 target_os = "macos",
1475 target_os = "ios",
1476 target_os = "android"
1477 ),
1478 feature = "opengl"
1479))]
1480pub(crate) fn float_pbo_eligible(dtype: DType, support: RenderDtypeSupport) -> bool {
1481 match dtype {
1482 DType::F16 => support.f16,
1483 DType::F32 => support.f32,
1484 _ => false,
1485 }
1486}
1487
1488#[derive(Debug)]
1491pub struct ImageProcessor {
1492 pub cpu: Option<CPUProcessor>,
1495
1496 #[cfg(target_os = "linux")]
1497 pub g2d: Option<G2DProcessor>,
1501 #[cfg(target_os = "linux")]
1502 #[cfg(feature = "opengl")]
1503 pub opengl: Option<GLProcessorThreaded>,
1507 #[cfg(any(
1508 target_os = "macos",
1509 target_os = "ios",
1510 target_os = "android",
1511 target_os = "windows"
1512 ))]
1513 #[cfg(feature = "opengl")]
1514 pub opengl: Option<GLProcessorThreaded>,
1523
1524 pub(crate) forced_backend: Option<ForcedBackend>,
1526
1527 pub(crate) convert_fallbacks: std::sync::atomic::AtomicU64,
1533
1534 pub(crate) interpolation: Interpolation,
1536
1537 pub(crate) env_interpolation: Option<Interpolation>,
1539}
1540
1541unsafe impl Send for ImageProcessor {}
1542unsafe impl Sync for ImageProcessor {}
1543
1544impl ImageProcessor {
1545 pub fn new() -> Result<Self> {
1571 Self::with_config(ImageProcessorConfig::default())
1572 }
1573
1574 pub fn convert_fallback_count(&self) -> u64 {
1581 self.convert_fallbacks
1582 .load(std::sync::atomic::Ordering::Relaxed)
1583 }
1584
1585 pub fn compression_fallback_count(&self) -> u64 {
1594 edgefirst_tensor::compression_fallback_count()
1595 }
1596
1597 pub fn convert_with_fence(
1620 &mut self,
1621 src: &TensorDyn,
1622 dst: &mut TensorDyn,
1623 options: Option<ConvertOptions>,
1624 ) -> Result<Option<CompletionFence>> {
1625 let options = self.resolve_interpolation(options.unwrap_or_default());
1626 #[cfg(any(
1627 target_os = "linux",
1628 target_os = "macos",
1629 target_os = "ios",
1630 target_os = "android",
1631 target_os = "windows"
1632 ))]
1633 #[cfg(feature = "opengl")]
1634 {
1635 let gl_forced = matches!(self.forced_backend, Some(ForcedBackend::OpenGl));
1636 if self.forced_backend.is_none() || gl_forced {
1637 if let Some(opengl) = self.opengl.as_mut() {
1638 match opengl.convert_with_fence(src, dst, Some(options)) {
1639 Ok(fd) => return Ok(fd),
1640 Err(e) if gl_forced => return Err(e),
1641 Err(e) => {
1642 log::debug!(
1646 "convert_with_fence: opengl declined, \
1647 falling back to the blocking chain: {e}"
1648 );
1649 }
1650 }
1651 } else if gl_forced {
1652 return Err(Error::ForcedBackendUnavailable("opengl".into()));
1653 }
1654 }
1655 }
1656 self.convert(src, dst, Some(options))?;
1659 Ok(None)
1660 }
1661
1662 pub fn supported_render_dtypes(&self) -> RenderDtypeSupport {
1675 #[cfg(all(
1676 any(
1677 target_os = "macos",
1678 target_os = "ios",
1679 target_os = "android",
1680 target_os = "windows"
1681 ),
1682 feature = "opengl"
1683 ))]
1684 if let Some(gl) = self.opengl.as_ref() {
1685 return gl.supported_render_dtypes();
1686 }
1687 #[cfg(all(target_os = "linux", feature = "opengl"))]
1688 if let Some(gl) = self.opengl.as_ref() {
1689 return gl.supported_render_dtypes();
1690 }
1691 RenderDtypeSupport {
1692 f32: false,
1693 f16: false,
1694 }
1695 }
1696
1697 pub fn with_config(config: ImageProcessorConfig) -> Result<Self> {
1706 let interpolation = config.interpolation;
1707 let mut processor = Self::with_config_backends(config)?;
1708 processor.env_interpolation = interpolation_from_env().map(runnable_interpolation);
1709 processor.set_interpolation(interpolation)?;
1710 Ok(processor)
1711 }
1712
1713 #[allow(unused_variables)]
1715 fn with_config_backends(config: ImageProcessorConfig) -> Result<Self> {
1716 match config.backend {
1720 ComputeBackend::Cpu => {
1721 log::info!("ComputeBackend::Cpu — CPU only");
1722 return Ok(Self {
1723 cpu: Some(CPUProcessor::new()),
1724 #[cfg(target_os = "linux")]
1725 g2d: None,
1726 #[cfg(target_os = "linux")]
1727 #[cfg(feature = "opengl")]
1728 opengl: None,
1729 #[cfg(any(
1730 target_os = "macos",
1731 target_os = "ios",
1732 target_os = "android",
1733 target_os = "windows"
1734 ))]
1735 #[cfg(feature = "opengl")]
1736 opengl: None,
1737 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1738 interpolation: Interpolation::Bilinear,
1739 env_interpolation: None,
1740 forced_backend: None,
1741 });
1742 }
1743 ComputeBackend::G2d => {
1744 log::info!("ComputeBackend::G2d — G2D + CPU fallback");
1745 #[cfg(target_os = "linux")]
1746 {
1747 let g2d = match G2DProcessor::new() {
1748 Ok(g) => Some(g),
1749 Err(e) => {
1750 log::warn!("G2D requested but failed to initialize: {e:?}");
1751 None
1752 }
1753 };
1754 return Ok(Self {
1755 cpu: Some(CPUProcessor::new()),
1756 g2d,
1757 #[cfg(feature = "opengl")]
1758 opengl: None,
1759 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1760 interpolation: Interpolation::Bilinear,
1761 env_interpolation: None,
1762 forced_backend: None,
1763 });
1764 }
1765 #[cfg(not(target_os = "linux"))]
1766 {
1767 log::warn!("G2D requested but not available on this platform, using CPU");
1768 return Ok(Self {
1769 cpu: Some(CPUProcessor::new()),
1770 #[cfg(any(
1771 target_os = "macos",
1772 target_os = "ios",
1773 target_os = "android",
1774 target_os = "windows"
1775 ))]
1776 #[cfg(feature = "opengl")]
1777 opengl: None,
1778 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1779 interpolation: Interpolation::Bilinear,
1780 env_interpolation: None,
1781 forced_backend: None,
1782 });
1783 }
1784 }
1785 ComputeBackend::OpenGl => {
1786 log::info!("ComputeBackend::OpenGl — OpenGL + CPU fallback");
1787 #[cfg(target_os = "linux")]
1788 {
1789 #[cfg(feature = "opengl")]
1790 let opengl = match GLProcessorThreaded::with_cache_capacity(
1791 config.egl_display,
1792 config.egl_cache_capacity,
1793 ) {
1794 Ok(gl) => Some(gl),
1795 Err(e) => {
1796 log::warn!("OpenGL requested but failed to initialize: {e:?}");
1797 None
1798 }
1799 };
1800 return Ok(Self {
1801 cpu: Some(CPUProcessor::new()),
1802 g2d: None,
1803 #[cfg(feature = "opengl")]
1804 opengl,
1805 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1806 interpolation: Interpolation::Bilinear,
1807 env_interpolation: None,
1808 forced_backend: None,
1809 }
1810 .apply_colorimetry_mode(config.colorimetry));
1811 }
1812 #[cfg(any(target_os = "macos", target_os = "ios"))]
1813 {
1814 #[cfg(feature = "opengl")]
1815 let opengl = match GLProcessorThreaded::with_cache_capacity(
1816 config.egl_display,
1817 config.egl_cache_capacity,
1818 ) {
1819 Ok(gl) => Some(gl),
1820 Err(e) => {
1821 log::warn!(
1822 "OpenGL requested on macOS but ANGLE init failed: {e:?} \
1823 (install ANGLE via `brew install startergo/angle/angle` \
1824 and re-sign the dylibs — see README.md § macOS GPU \
1825 Acceleration). Falling back to CPU."
1826 );
1827 None
1828 }
1829 };
1830 return Ok(Self {
1831 cpu: Some(CPUProcessor::new()),
1832 #[cfg(feature = "opengl")]
1833 opengl,
1834 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1835 interpolation: Interpolation::Bilinear,
1836 env_interpolation: None,
1837 forced_backend: None,
1838 }
1839 .apply_colorimetry_mode(config.colorimetry));
1840 }
1841 #[cfg(target_os = "android")]
1842 {
1843 #[cfg(feature = "opengl")]
1844 let opengl = match GLProcessorThreaded::with_cache_capacity(
1845 config.egl_display,
1846 config.egl_cache_capacity,
1847 ) {
1848 Ok(gl) => Some(gl),
1849 Err(e) => {
1850 log::warn!(
1851 "OpenGL requested but native EGL init failed: {e:?}. \
1852 Falling back to CPU."
1853 );
1854 None
1855 }
1856 };
1857 return Ok(Self {
1858 cpu: Some(CPUProcessor::new()),
1859 #[cfg(feature = "opengl")]
1860 opengl,
1861 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1862 interpolation: Interpolation::Bilinear,
1863 env_interpolation: None,
1864 forced_backend: None,
1865 }
1866 .apply_colorimetry_mode(config.colorimetry));
1867 }
1868 #[cfg(target_os = "windows")]
1869 {
1870 #[cfg(feature = "opengl")]
1871 let opengl = match GLProcessorThreaded::with_cache_capacity(
1872 config.egl_display,
1873 config.egl_cache_capacity,
1874 ) {
1875 Ok(gl) => Some(gl),
1876 Err(e) => {
1877 log::warn!(
1878 "OpenGL requested on Windows but ANGLE/D3D11 init failed: {e:?} \
1879 (fetch ANGLE with `scripts/fetch-angle.sh` and set \
1880 EDGEFIRST_ANGLE_PATH to the directory containing libEGL.dll + \
1881 libGLESv2.dll, or place the two DLLs next to the executable — \
1882 see README.md § Windows GPU Acceleration). Falling back to CPU."
1883 );
1884 None
1885 }
1886 };
1887 return Ok(Self {
1888 cpu: Some(CPUProcessor::new()),
1889 #[cfg(feature = "opengl")]
1890 opengl,
1891 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1892 interpolation: Interpolation::Bilinear,
1893 env_interpolation: None,
1894 forced_backend: None,
1895 }
1896 .apply_colorimetry_mode(config.colorimetry));
1897 }
1898 #[cfg(not(any(
1899 target_os = "linux",
1900 target_os = "macos",
1901 target_os = "ios",
1902 target_os = "android",
1903 target_os = "windows"
1904 )))]
1905 {
1906 log::warn!("OpenGL requested but not available on this platform, using CPU");
1907 return Ok(Self {
1908 cpu: Some(CPUProcessor::new()),
1909 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1910 interpolation: Interpolation::Bilinear,
1911 env_interpolation: None,
1912 forced_backend: None,
1913 });
1914 }
1915 }
1916 ComputeBackend::Auto => { }
1917 }
1918
1919 if let Ok(val) = std::env::var("EDGEFIRST_FORCE_BACKEND") {
1924 let val_lower = val.to_lowercase();
1925 let forced = match val_lower.as_str() {
1926 "cpu" => ForcedBackend::Cpu,
1927 "g2d" => ForcedBackend::G2d,
1928 "opengl" => ForcedBackend::OpenGl,
1929 other => {
1930 return Err(Error::ForcedBackendUnavailable(format!(
1931 "unknown EDGEFIRST_FORCE_BACKEND value: {other:?} (expected cpu, g2d, or opengl)"
1932 )));
1933 }
1934 };
1935
1936 log::info!("EDGEFIRST_FORCE_BACKEND={val} — only initializing {val_lower} backend");
1937
1938 return match forced {
1939 ForcedBackend::Cpu => Ok(Self {
1940 cpu: Some(CPUProcessor::new()),
1941 #[cfg(target_os = "linux")]
1942 g2d: None,
1943 #[cfg(target_os = "linux")]
1944 #[cfg(feature = "opengl")]
1945 opengl: None,
1946 #[cfg(any(
1947 target_os = "macos",
1948 target_os = "ios",
1949 target_os = "android",
1950 target_os = "windows"
1951 ))]
1952 #[cfg(feature = "opengl")]
1953 opengl: None,
1954 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1955 interpolation: Interpolation::Bilinear,
1956 env_interpolation: None,
1957 forced_backend: Some(ForcedBackend::Cpu),
1958 }),
1959 ForcedBackend::G2d => {
1960 #[cfg(target_os = "linux")]
1961 {
1962 let g2d = G2DProcessor::new().map_err(|e| {
1963 Error::ForcedBackendUnavailable(format!(
1964 "g2d forced but failed to initialize: {e:?}"
1965 ))
1966 })?;
1967 Ok(Self {
1968 cpu: None,
1969 g2d: Some(g2d),
1970 #[cfg(feature = "opengl")]
1971 opengl: None,
1972 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1973 interpolation: Interpolation::Bilinear,
1974 env_interpolation: None,
1975 forced_backend: Some(ForcedBackend::G2d),
1976 })
1977 }
1978 #[cfg(not(target_os = "linux"))]
1979 {
1980 Err(Error::ForcedBackendUnavailable(
1981 "g2d backend is only available on Linux".into(),
1982 ))
1983 }
1984 }
1985 ForcedBackend::OpenGl => {
1986 #[cfg(target_os = "linux")]
1987 #[cfg(feature = "opengl")]
1988 {
1989 let opengl = GLProcessorThreaded::with_cache_capacity(
1990 config.egl_display,
1991 config.egl_cache_capacity,
1992 )
1993 .map_err(|e| {
1994 Error::ForcedBackendUnavailable(format!(
1995 "opengl forced but failed to initialize: {e:?}"
1996 ))
1997 })?;
1998 Ok(Self {
1999 cpu: None,
2000 g2d: None,
2001 opengl: Some(opengl),
2002 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
2003 interpolation: Interpolation::Bilinear,
2004 env_interpolation: None,
2005 forced_backend: Some(ForcedBackend::OpenGl),
2006 }
2007 .apply_colorimetry_mode(config.colorimetry))
2008 }
2009 #[cfg(any(target_os = "macos", target_os = "ios"))]
2010 #[cfg(feature = "opengl")]
2011 {
2012 let opengl = GLProcessorThreaded::with_cache_capacity(
2013 config.egl_display,
2014 config.egl_cache_capacity,
2015 )
2016 .map_err(|e| {
2017 Error::ForcedBackendUnavailable(format!(
2018 "opengl forced on macOS but ANGLE init failed: {e:?}"
2019 ))
2020 })?;
2021 Ok(Self {
2022 cpu: None,
2023 opengl: Some(opengl),
2024 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
2025 interpolation: Interpolation::Bilinear,
2026 env_interpolation: None,
2027 forced_backend: Some(ForcedBackend::OpenGl),
2028 }
2029 .apply_colorimetry_mode(config.colorimetry))
2030 }
2031 #[cfg(target_os = "android")]
2032 #[cfg(feature = "opengl")]
2033 {
2034 let opengl = GLProcessorThreaded::with_cache_capacity(
2035 config.egl_display,
2036 config.egl_cache_capacity,
2037 )
2038 .map_err(|e| {
2039 Error::ForcedBackendUnavailable(format!(
2040 "opengl forced but native EGL init failed: {e:?}"
2041 ))
2042 })?;
2043 Ok(Self {
2044 cpu: None,
2045 opengl: Some(opengl),
2046 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
2047 interpolation: Interpolation::Bilinear,
2048 env_interpolation: None,
2049 forced_backend: Some(ForcedBackend::OpenGl),
2050 }
2051 .apply_colorimetry_mode(config.colorimetry))
2052 }
2053 #[cfg(target_os = "windows")]
2054 #[cfg(feature = "opengl")]
2055 {
2056 let opengl = GLProcessorThreaded::with_cache_capacity(
2057 config.egl_display,
2058 config.egl_cache_capacity,
2059 )
2060 .map_err(|e| {
2061 Error::ForcedBackendUnavailable(format!(
2062 "opengl forced on Windows but ANGLE/D3D11 init failed: {e:?}"
2063 ))
2064 })?;
2065 Ok(Self {
2066 cpu: None,
2067 opengl: Some(opengl),
2068 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
2069 interpolation: Interpolation::Bilinear,
2070 env_interpolation: None,
2071 forced_backend: Some(ForcedBackend::OpenGl),
2072 }
2073 .apply_colorimetry_mode(config.colorimetry))
2074 }
2075 #[cfg(not(all(
2076 any(
2077 target_os = "linux",
2078 target_os = "macos",
2079 target_os = "ios",
2080 target_os = "android",
2081 target_os = "windows"
2082 ),
2083 feature = "opengl"
2084 )))]
2085 {
2086 Err(Error::ForcedBackendUnavailable(
2087 "opengl backend requires Linux, macOS, Android or Windows with the \
2088 'opengl' feature enabled"
2089 .into(),
2090 ))
2091 }
2092 }
2093 };
2094 }
2095
2096 #[cfg(target_os = "linux")]
2098 let g2d = if std::env::var("EDGEFIRST_DISABLE_G2D")
2099 .map(|x| x != "0" && x.to_lowercase() != "false")
2100 .unwrap_or(false)
2101 {
2102 log::debug!("EDGEFIRST_DISABLE_G2D is set");
2103 None
2104 } else {
2105 match G2DProcessor::new() {
2106 Ok(g2d_converter) => Some(g2d_converter),
2107 Err(err) => {
2108 log::warn!("Failed to initialize G2D converter: {err:?}");
2109 None
2110 }
2111 }
2112 };
2113
2114 #[cfg(target_os = "linux")]
2115 #[cfg(feature = "opengl")]
2116 let opengl = if std::env::var("EDGEFIRST_DISABLE_GL")
2117 .map(|x| x != "0" && x.to_lowercase() != "false")
2118 .unwrap_or(false)
2119 {
2120 log::debug!("EDGEFIRST_DISABLE_GL is set");
2121 None
2122 } else {
2123 match GLProcessorThreaded::with_cache_capacity(
2124 config.egl_display,
2125 config.egl_cache_capacity,
2126 ) {
2127 Ok(gl_converter) => Some(gl_converter),
2128 Err(err) => {
2129 log::warn!("Failed to initialize GL converter: {err:?}");
2130 None
2131 }
2132 }
2133 };
2134
2135 #[cfg(any(
2136 target_os = "macos",
2137 target_os = "ios",
2138 target_os = "android",
2139 target_os = "windows"
2140 ))]
2141 #[cfg(feature = "opengl")]
2142 let opengl = if std::env::var("EDGEFIRST_DISABLE_GL")
2143 .map(|x| x != "0" && x.to_lowercase() != "false")
2144 .unwrap_or(false)
2145 {
2146 log::debug!("EDGEFIRST_DISABLE_GL is set");
2147 None
2148 } else {
2149 match GLProcessorThreaded::with_cache_capacity(
2150 config.egl_display,
2151 config.egl_cache_capacity,
2152 ) {
2153 Ok(gl_converter) => Some(gl_converter),
2154 Err(err) => {
2155 log::debug!(
2156 "GL backend unavailable: {err:?} \
2157 (CPU fallback will be used)"
2158 );
2159 None
2160 }
2161 }
2162 };
2163
2164 let cpu = if std::env::var("EDGEFIRST_DISABLE_CPU")
2165 .map(|x| x != "0" && x.to_lowercase() != "false")
2166 .unwrap_or(false)
2167 {
2168 log::debug!("EDGEFIRST_DISABLE_CPU is set");
2169 None
2170 } else {
2171 Some(CPUProcessor::new())
2172 };
2173 Ok(Self {
2174 cpu,
2175 #[cfg(target_os = "linux")]
2176 g2d,
2177 #[cfg(target_os = "linux")]
2178 #[cfg(feature = "opengl")]
2179 opengl,
2180 #[cfg(any(
2181 target_os = "macos",
2182 target_os = "ios",
2183 target_os = "android",
2184 target_os = "windows"
2185 ))]
2186 #[cfg(feature = "opengl")]
2187 opengl,
2188 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
2189 interpolation: Interpolation::Bilinear,
2190 env_interpolation: None,
2191 forced_backend: None,
2192 }
2193 .apply_colorimetry_mode(config.colorimetry))
2194 }
2195
2196 fn apply_colorimetry_mode(self, _mode: ColorimetryMode) -> Self {
2200 #[cfg(all(
2201 any(
2202 target_os = "linux",
2203 target_os = "macos",
2204 target_os = "ios",
2205 target_os = "android",
2206 target_os = "windows"
2207 ),
2208 feature = "opengl"
2209 ))]
2210 {
2211 let mut me = self;
2212 if let Err(e) = me.set_colorimetry_mode(_mode) {
2213 log::warn!("Failed to apply ColorimetryMode::{_mode:?}: {e:?}");
2214 }
2215 me
2216 }
2217 #[cfg(not(all(
2218 any(
2219 target_os = "linux",
2220 target_os = "macos",
2221 target_os = "ios",
2222 target_os = "android",
2223 target_os = "windows"
2224 ),
2225 feature = "opengl"
2226 )))]
2227 {
2228 let _ = _mode;
2229 self
2230 }
2231 }
2232
2233 #[cfg(all(
2238 any(
2239 target_os = "linux",
2240 target_os = "macos",
2241 target_os = "ios",
2242 target_os = "android",
2243 target_os = "windows"
2244 ),
2245 feature = "opengl"
2246 ))]
2247 pub fn set_colorimetry_mode(&mut self, mode: ColorimetryMode) -> Result<()> {
2248 if let Some(ref mut gl) = self.opengl {
2249 gl.set_colorimetry_mode(mode)?;
2250 }
2251 Ok(())
2252 }
2253
2254 pub fn set_interpolation(&mut self, mode: Interpolation) -> Result<()> {
2262 let mode = match self.env_interpolation {
2263 Some(env) => {
2264 if env != mode.supported() {
2265 log::debug!(
2266 "EDGEFIRST_INTERPOLATION pins Interpolation::{env:?}; \
2267 ignoring Interpolation::{mode:?}"
2268 );
2269 }
2270 env
2271 }
2272 None => runnable_interpolation(mode),
2273 };
2274 if mode == self.interpolation {
2275 return Ok(());
2276 }
2277 if let Some(cpu) = self.cpu.as_mut() {
2278 cpu.set_interpolation(mode);
2279 }
2280 #[cfg(target_os = "linux")]
2281 if let Some(g2d) = self.g2d.as_mut() {
2282 g2d.set_interpolation(mode);
2283 }
2284 #[cfg(all(
2285 any(
2286 target_os = "linux",
2287 target_os = "macos",
2288 target_os = "ios",
2289 target_os = "android",
2290 target_os = "windows"
2291 ),
2292 feature = "opengl"
2293 ))]
2294 if let Some(gl) = self.opengl.as_mut() {
2295 gl.set_interpolation(mode)?;
2296 }
2297 self.interpolation = mode;
2298 Ok(())
2299 }
2300
2301 pub fn interpolation(&self) -> Interpolation {
2304 self.interpolation
2305 }
2306
2307 fn resolve_interpolation(&self, options: ConvertOptions) -> ConvertOptions {
2311 let mode = match (self.env_interpolation, options.get_interpolation()) {
2312 (Some(env), _) => env,
2313 (None, Some(mode)) => runnable_interpolation(mode),
2314 (None, None) => self.interpolation,
2315 };
2316 options.with_interpolation(Some(mode))
2317 }
2318
2319 #[cfg(all(
2322 any(
2323 target_os = "linux",
2324 target_os = "macos",
2325 target_os = "ios",
2326 target_os = "android",
2327 target_os = "windows"
2328 ),
2329 feature = "opengl"
2330 ))]
2331 pub fn set_int8_interpolation_mode(&mut self, mode: Int8InterpolationMode) -> Result<()> {
2332 if let Some(ref mut gl) = self.opengl {
2333 gl.set_int8_interpolation_mode(mode)?;
2334 }
2335 Ok(())
2336 }
2337
2338 fn producer_access(
2344 &self,
2345 access: edgefirst_tensor::CpuAccess,
2346 format: PixelFormat,
2347 dtype: DType,
2348 memory: TensorMemory,
2349 views: bool,
2350 ) -> edgefirst_tensor::CpuAccess {
2351 use edgefirst_tensor::CpuAccess;
2352 #[cfg(all(
2353 any(
2354 target_os = "linux",
2355 target_os = "macos",
2356 target_os = "ios",
2357 target_os = "android",
2358 target_os = "windows"
2359 ),
2360 feature = "opengl"
2361 ))]
2362 let gl = self
2363 .opengl
2364 .as_ref()
2365 .map(|gl| gl.dst_cpu_access(memory, format, dtype));
2366 #[cfg(not(all(
2367 any(
2368 target_os = "linux",
2369 target_os = "macos",
2370 target_os = "ios",
2371 target_os = "android",
2372 target_os = "windows"
2373 ),
2374 feature = "opengl"
2375 )))]
2376 let gl = None;
2377 #[cfg(target_os = "linux")]
2378 let g2d = self
2379 .g2d
2380 .as_ref()
2381 .map(|_| g2d::dst_cpu_access(memory, format, dtype));
2382 #[cfg(not(target_os = "linux"))]
2383 let g2d = None;
2384 let cpu = self
2385 .cpu
2386 .as_ref()
2387 .map(|_| Some(cpu::convert_dst_access(None, format, dtype)));
2388 let needed = predicted_cpu_access(self.forced_backend, gl, g2d, cpu);
2389 let needed = if views && needed.writes() {
2390 CpuAccess::ReadWrite
2391 } else {
2392 needed
2393 };
2394 access.union(needed)
2395 }
2396
2397 pub fn create_image_desc(&self, desc: &edgefirst_tensor::ImageDesc) -> Result<TensorDyn> {
2412 if desc.compression().is_some() {
2414 return Ok(TensorDyn::image_desc(desc)?);
2415 }
2416 if !desc.contiguous() {
2417 return self.create_image(
2418 desc.width(),
2419 desc.height(),
2420 desc.format(),
2421 desc.dtype(),
2422 desc.memory(),
2423 desc.access(),
2424 );
2425 }
2426 let access = self.producer_access(
2427 desc.access(),
2428 desc.format(),
2429 desc.dtype(),
2430 TensorMemory::DmaBuf,
2431 false,
2432 );
2433 Ok(TensorDyn::image_desc(&desc.clone().with_access(access))?)
2434 }
2435
2436 pub fn create_image(
2591 &self,
2592 width: usize,
2593 height: usize,
2594 format: PixelFormat,
2595 dtype: DType,
2596 memory: Option<TensorMemory>,
2597 access: edgefirst_tensor::CpuAccess,
2598 ) -> Result<TensorDyn> {
2599 self.create_image_with(width, height, format, dtype, memory, access, false)
2600 }
2601
2602 #[cfg(target_os = "linux")]
2610 fn create_dma_image(
2611 &self,
2612 width: usize,
2613 height: usize,
2614 format: PixelFormat,
2615 dtype: DType,
2616 access: edgefirst_tensor::CpuAccess,
2617 views: bool,
2618 ) -> Result<TensorDyn> {
2619 let dma_access = self.producer_access(access, format, dtype, TensorMemory::DmaBuf, views);
2620 let natural =
2621 primary_plane_bpp(format, dtype.size()).and_then(|bpp| width.checked_mul(bpp));
2622 let padded = natural
2623 .and_then(align_pitch_bytes_to_gpu_alignment)
2624 .filter(|&stride| {
2625 format.layout() == edgefirst_tensor::PixelLayout::Packed
2626 && natural.is_some_and(|n| stride > n)
2627 });
2628 let memory = Some(edgefirst_tensor::TensorMemory::DmaBuf);
2629 match padded {
2630 Some(stride) => {
2631 log::debug!(
2632 "create_image: padding row stride for {format:?} {width}x{height} \
2633 from natural pitch to {stride} bytes for GPU alignment"
2634 );
2635 Ok(TensorDyn::image_with_stride(
2636 width, height, format, dtype, stride, memory, dma_access,
2637 )?)
2638 }
2639 None => Ok(TensorDyn::image(
2640 width, height, format, dtype, memory, dma_access,
2641 )?),
2642 }
2643 }
2644
2645 #[cfg(any(target_os = "linux", target_os = "windows"))]
2649 #[cfg(feature = "opengl")]
2650 fn create_byte_pbo_image(
2651 &self,
2652 width: usize,
2653 height: usize,
2654 format: PixelFormat,
2655 dtype: DType,
2656 ) -> Option<TensorDyn> {
2657 if dtype.size() != 1 {
2658 return None;
2659 }
2660 let gl = self.opengl.as_ref()?;
2661 let t = gl
2662 .create_pbo_image(width, height, format)
2663 .map_err(|e| log::debug!("PBO image creation failed, falling back to Mem: {e:?}"))
2664 .ok()?;
2665 if dtype != DType::I8 {
2666 return Some(TensorDyn::from(t));
2667 }
2668 debug_assert!(
2671 t.chroma().is_none(),
2672 "PBO i8 transmute requires chroma == None"
2673 );
2674 let t_i8: Tensor<i8> = unsafe { std::mem::transmute(t) };
2678 Some(TensorDyn::from(t_i8))
2679 }
2680
2681 #[allow(clippy::too_many_arguments)]
2684 pub(crate) fn create_image_with(
2685 &self,
2686 width: usize,
2687 height: usize,
2688 format: PixelFormat,
2689 dtype: DType,
2690 memory: Option<TensorMemory>,
2691 access: edgefirst_tensor::CpuAccess,
2692 views: bool,
2693 ) -> Result<TensorDyn> {
2694 #[cfg(target_os = "linux")]
2695 let try_dma = || self.create_dma_image(width, height, format, dtype, access, views);
2696
2697 match memory {
2704 #[cfg(target_os = "linux")]
2705 Some(TensorMemory::DmaBuf) => {
2706 if dtype == DType::F32 {
2708 return Err(Error::NotSupported(
2709 "F32 has no 32-bit-float DRM format for DMA-BUF; \
2710 use TensorMemory::Pbo for F32"
2711 .to_string(),
2712 ));
2713 }
2714 return try_dma();
2715 }
2716 Some(mem) => {
2717 return Ok(TensorDyn::image(
2718 width,
2719 height,
2720 format,
2721 dtype,
2722 Some(mem),
2723 self.producer_access(access, format, dtype, mem, views),
2724 )?);
2725 }
2726 None => {}
2727 }
2728
2729 #[cfg(any(
2736 target_os = "macos",
2737 target_os = "ios",
2738 target_os = "android",
2739 target_os = "windows"
2740 ))]
2741 #[cfg(feature = "opengl")]
2742 if let Some(gl) = self.opengl.as_ref() {
2743 let _ = gl;
2744 let support = self.supported_render_dtypes();
2749 let dtype_ok =
2750 !matches!(dtype, DType::F16 | DType::F32) || float_pbo_eligible(dtype, support);
2751 if dtype_ok {
2752 match TensorDyn::image(
2753 width,
2754 height,
2755 format,
2756 dtype,
2757 Some(edgefirst_tensor::TensorMemory::DmaBuf),
2758 self.producer_access(access, format, dtype, TensorMemory::DmaBuf, views),
2759 ) {
2760 Ok(img) => return Ok(img),
2761 Err(e) => {
2762 log::debug!(
2766 "create_image: zero-copy Dma allocation declined \
2767 ({format:?}/{dtype:?} {width}x{height}): {e:?}; using fallback storage"
2768 );
2769 }
2770 }
2771 }
2772 }
2773
2774 #[cfg(target_os = "linux")]
2777 {
2778 #[cfg(feature = "opengl")]
2779 let gl_uses_pbo = self
2780 .opengl
2781 .as_ref()
2782 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
2783 #[cfg(not(feature = "opengl"))]
2784 let gl_uses_pbo = false;
2785
2786 if !gl_uses_pbo {
2787 if let Ok(img) = try_dma() {
2788 return Ok(img);
2789 }
2790 }
2791 }
2792
2793 #[cfg(any(target_os = "linux", target_os = "windows"))]
2794 #[cfg(feature = "opengl")]
2795 if let Some(img) = self.create_byte_pbo_image(width, height, format, dtype) {
2796 return Ok(img);
2797 }
2798
2799 #[cfg(any(target_os = "linux", target_os = "windows"))]
2802 #[cfg(feature = "opengl")]
2803 if float_pbo_eligible(dtype, self.supported_render_dtypes()) {
2804 if let Some(gl) = &self.opengl {
2805 match gl.create_pbo_image_dtype(width, height, format, dtype) {
2806 Ok(t) => return Ok(t),
2807 Err(e) => {
2808 log::debug!(
2809 "Float PBO image creation failed for {dtype:?}, \
2810 falling back to Mem: {e:?}"
2811 );
2812 }
2813 }
2814 }
2815 }
2816
2817 Ok(TensorDyn::image(
2819 width,
2820 height,
2821 format,
2822 dtype,
2823 Some(edgefirst_tensor::TensorMemory::Mem),
2824 self.producer_access(access, format, dtype, TensorMemory::Mem, views),
2825 )?)
2826 }
2827
2828 #[allow(clippy::too_many_arguments)]
2882 #[cfg(target_os = "linux")]
2883 pub fn import_image(
2884 &self,
2885 image: edgefirst_tensor::PlaneDescriptor,
2886 chroma: Option<edgefirst_tensor::PlaneDescriptor>,
2887 width: usize,
2888 height: usize,
2889 format: PixelFormat,
2890 dtype: DType,
2891 colorimetry: Option<edgefirst_tensor::Colorimetry>,
2892 ) -> Result<TensorDyn> {
2893 use edgefirst_tensor::{Tensor, TensorMemory};
2894
2895 let image_stride = image.stride();
2897 let image_offset = image.offset();
2898 let chroma_stride = chroma.as_ref().and_then(|c| c.stride());
2899 let chroma_offset = chroma.as_ref().and_then(|c| c.offset());
2900
2901 if let Some(chroma_pd) = chroma {
2902 if dtype != DType::U8 && dtype != DType::I8 {
2907 return Err(Error::NotSupported(format!(
2908 "multiplane import only supports U8/I8, got {dtype:?}"
2909 )));
2910 }
2911 if format.layout() != PixelLayout::SemiPlanar {
2912 return Err(Error::NotSupported(format!(
2913 "import_image with chroma requires a semi-planar format, got {format:?}"
2914 )));
2915 }
2916
2917 let chroma_h = match format {
2918 PixelFormat::Nv12 => {
2919 height.div_ceil(2)
2921 }
2922 PixelFormat::Nv16 => {
2925 return Err(Error::NotSupported(
2926 "multiplane NV16 is not yet supported; use contiguous NV16 instead".into(),
2927 ))
2928 }
2929 _ => {
2930 return Err(Error::NotSupported(format!(
2931 "unsupported semi-planar format: {format:?}"
2932 )))
2933 }
2934 };
2935
2936 let luma = Tensor::<u8>::from_fd(image.into_fd(), &[height, width], Some("luma"))?;
2937 if luma.memory() != TensorMemory::DmaBuf {
2938 return Err(Error::NotSupported(format!(
2939 "luma fd must be DMA-backed, got {:?}",
2940 luma.memory()
2941 )));
2942 }
2943
2944 let mut chroma_tensor =
2945 Tensor::<u8>::from_fd(chroma_pd.into_fd(), &[chroma_h, width], Some("chroma"))?;
2946 if chroma_tensor.memory() != TensorMemory::DmaBuf {
2947 return Err(Error::NotSupported(format!(
2948 "chroma fd must be DMA-backed, got {:?}",
2949 chroma_tensor.memory()
2950 )));
2951 }
2952
2953 if let Some(s) = chroma_stride {
2957 if s < width {
2958 return Err(Error::InvalidShape(format!(
2959 "chroma stride {s} < minimum {width} for {format:?}"
2960 )));
2961 }
2962 chroma_tensor.set_row_stride_unchecked(s);
2963 }
2964 if let Some(o) = chroma_offset {
2965 chroma_tensor.set_plane_offset(o);
2966 }
2967
2968 let mut tensor = Tensor::<u8>::from_planes(luma, chroma_tensor, format)?;
2971
2972 if let Some(s) = image_stride {
2974 tensor.set_row_stride(s)?;
2975 }
2976 if let Some(o) = image_offset {
2977 tensor.set_plane_offset(o);
2978 }
2979
2980 if dtype == DType::I8 {
2981 const {
2985 assert!(std::mem::size_of::<Tensor<u8>>() == std::mem::size_of::<Tensor<i8>>());
2986 assert!(
2987 std::mem::align_of::<Tensor<u8>>() == std::mem::align_of::<Tensor<i8>>()
2988 );
2989 }
2990 let tensor_i8: Tensor<i8> = unsafe { std::mem::transmute(tensor) };
2991 let mut dyn_tensor = TensorDyn::from(tensor_i8);
2992 dyn_tensor.set_colorimetry(colorimetry);
2993 return Ok(dyn_tensor);
2994 }
2995 let mut dyn_tensor = TensorDyn::from(tensor);
2996 dyn_tensor.set_colorimetry(colorimetry);
2997 Ok(dyn_tensor)
2998 } else {
2999 let shape = format.allocation_shape(width, height).ok_or_else(|| {
3004 Error::NotSupported(format!(
3005 "unsupported pixel format for import_image: {format:?}"
3006 ))
3007 })?;
3008 let tensor = TensorDyn::from_fd(image.into_fd(), &shape, dtype, None)?;
3009 if tensor.memory() != TensorMemory::DmaBuf {
3010 return Err(Error::NotSupported(format!(
3011 "import_image requires DMA-backed fd, got {:?}",
3012 tensor.memory()
3013 )));
3014 }
3015 let mut tensor = tensor.with_format(format)?;
3016 if let Some(s) = image_stride {
3017 tensor.set_row_stride(s)?;
3018 }
3019 if let Some(o) = image_offset {
3020 tensor.set_plane_offset(o);
3021 }
3022 tensor.set_colorimetry(colorimetry);
3023 Ok(tensor)
3024 }
3025 }
3026
3027 #[cfg(feature = "decode")]
3052 pub fn draw_masks(
3053 &mut self,
3054 decoder: &edgefirst_decoder::Decoder,
3055 outputs: &[&TensorDyn],
3056 dst: &mut TensorDyn,
3057 overlay: MaskOverlay<'_>,
3058 ) -> Result<Vec<DetectBox>> {
3059 let mut output_boxes = Vec::with_capacity(100);
3060
3061 let proto_result = decoder
3063 .decode_proto(outputs, &mut output_boxes)
3064 .map_err(|e| Error::Internal(format!("decode_proto: {e:#?}")))?;
3065
3066 if let Some(proto_data) = proto_result {
3067 self.draw_proto_masks(dst, &output_boxes, &proto_data, overlay)?;
3068 } else {
3069 let mut output_masks = Vec::with_capacity(100);
3071 decoder
3072 .decode(outputs, &mut output_boxes, &mut output_masks)
3073 .map_err(|e| Error::Internal(format!("decode: {e:#?}")))?;
3074 self.draw_decoded_masks(dst, &output_boxes, &output_masks, overlay)?;
3075 }
3076 Ok(output_boxes)
3077 }
3078
3079 #[cfg(feature = "tracker")]
3087 pub fn draw_masks_tracked<TR: edgefirst_tracker::Tracker<DetectBox>>(
3088 &mut self,
3089 decoder: &edgefirst_decoder::Decoder,
3090 tracker: &mut TR,
3091 timestamp: u64,
3092 outputs: &[&TensorDyn],
3093 dst: &mut TensorDyn,
3094 overlay: MaskOverlay<'_>,
3095 ) -> Result<(Vec<DetectBox>, Vec<edgefirst_tracker::TrackInfo>)> {
3096 let mut output_boxes = Vec::with_capacity(100);
3097 let mut output_tracks = Vec::new();
3098
3099 let proto_result = decoder
3100 .decode_proto_tracked(
3101 tracker,
3102 timestamp,
3103 outputs,
3104 &mut output_boxes,
3105 &mut output_tracks,
3106 )
3107 .map_err(|e| Error::Internal(format!("decode_proto_tracked: {e:#?}")))?;
3108
3109 if let Some(proto_data) = proto_result {
3110 self.draw_proto_masks(dst, &output_boxes, &proto_data, overlay)?;
3111 } else {
3112 let mut output_masks = Vec::with_capacity(100);
3116 decoder
3117 .decode_tracked(
3118 tracker,
3119 timestamp,
3120 outputs,
3121 &mut output_boxes,
3122 &mut output_masks,
3123 &mut output_tracks,
3124 )
3125 .map_err(|e| Error::Internal(format!("decode_tracked: {e:#?}")))?;
3126 self.draw_decoded_masks(dst, &output_boxes, &output_masks, overlay)?;
3127 }
3128 Ok((output_boxes, output_tracks))
3129 }
3130
3131 pub fn materialize_masks(
3155 &mut self,
3156 detect: &[DetectBox],
3157 proto_data: &ProtoData,
3158 content_region: Option<[f32; 4]>,
3159 resolution: MaskResolution,
3160 ) -> Result<Vec<Segmentation>> {
3161 let cpu = self.cpu.as_mut().ok_or(Error::NoConverter)?;
3162 match resolution {
3163 MaskResolution::Proto => {
3164 cpu.materialize_segmentations(detect, proto_data, content_region)
3165 }
3166 MaskResolution::Scaled { width, height } => cpu.materialize_scaled_segmentations(
3167 detect,
3168 proto_data,
3169 content_region,
3170 width,
3171 height,
3172 ),
3173 }
3174 }
3175}
3176
3177impl ImageProcessorTrait for ImageProcessor {
3178 fn convert(
3184 &mut self,
3185 src: &TensorDyn,
3186 dst: &mut TensorDyn,
3187 options: Option<ConvertOptions>,
3188 ) -> Result<()> {
3189 let options = self.resolve_interpolation(options.unwrap_or_default());
3190 let (rotation, flip) = (options.get_rotation(), options.get_flip());
3191 let start = Instant::now();
3192 let src_fmt = src.format();
3193 let dst_fmt = dst.format();
3194 let _span = tracing::trace_span!(
3195 "image.convert",
3196 ?src_fmt,
3197 ?dst_fmt,
3198 src_memory = ?src.memory(),
3199 dst_memory = ?dst.memory(),
3200 ?rotation,
3201 ?flip,
3202 )
3203 .entered();
3204 log::trace!(
3205 "convert: {src_fmt:?}({:?}/{:?}) → {dst_fmt:?}({:?}/{:?}), \
3206 rotation={rotation:?}, flip={flip:?}, backend={:?}",
3207 src.dtype(),
3208 src.memory(),
3209 dst.dtype(),
3210 dst.memory(),
3211 self.forced_backend,
3212 );
3213
3214 if let Some(forced) = self.forced_backend {
3216 return match forced {
3217 ForcedBackend::Cpu => {
3218 if let Some(cpu) = self.cpu.as_mut() {
3219 let r = cpu.convert(src, dst, Some(options));
3220 log::trace!(
3221 "convert: forced=cpu result={} ({:?})",
3222 if r.is_ok() { "ok" } else { "err" },
3223 start.elapsed()
3224 );
3225 return r;
3226 }
3227 Err(Error::ForcedBackendUnavailable("cpu".into()))
3228 }
3229 ForcedBackend::G2d => {
3230 #[cfg(target_os = "linux")]
3231 if let Some(g2d) = self.g2d.as_mut() {
3232 let r = g2d.convert(src, dst, Some(options));
3233 log::trace!(
3234 "convert: forced=g2d result={} ({:?})",
3235 if r.is_ok() { "ok" } else { "err" },
3236 start.elapsed()
3237 );
3238 return r;
3239 }
3240 Err(Error::ForcedBackendUnavailable("g2d".into()))
3241 }
3242 ForcedBackend::OpenGl => {
3243 #[cfg(any(
3244 target_os = "linux",
3245 target_os = "macos",
3246 target_os = "ios",
3247 target_os = "android",
3248 target_os = "windows"
3249 ))]
3250 #[cfg(feature = "opengl")]
3251 if let Some(opengl) = self.opengl.as_mut() {
3252 let r = opengl.convert(src, dst, Some(options));
3253 log::trace!(
3254 "convert: forced=opengl result={} ({:?})",
3255 if r.is_ok() { "ok" } else { "err" },
3256 start.elapsed()
3257 );
3258 return r;
3259 }
3260 Err(Error::ForcedBackendUnavailable("opengl".into()))
3261 }
3262 };
3263 }
3264
3265 #[allow(unused_mut)]
3267 let mut declined: Vec<(&'static str, Error)> = Vec::new();
3268 #[cfg(any(
3269 target_os = "linux",
3270 target_os = "macos",
3271 target_os = "ios",
3272 target_os = "android",
3273 target_os = "windows"
3274 ))]
3275 #[cfg(feature = "opengl")]
3276 if let Some(opengl) = self.opengl.as_mut() {
3277 match opengl.convert(src, dst, Some(options)) {
3278 Ok(_) => {
3279 log::trace!(
3280 "convert: auto selected=opengl for {src_fmt:?}→{dst_fmt:?} ({:?})",
3281 start.elapsed()
3282 );
3283 return Ok(());
3284 }
3285 Err(e) => {
3286 self.convert_fallbacks
3287 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3288 log::debug!(
3289 "convert: auto opengl declined {src_fmt:?}@{:?}→{dst_fmt:?}@{:?}, \
3290 falling back toward G2D/CPU: {e}",
3291 src.memory(),
3292 dst.memory(),
3293 );
3294 declined.push(("opengl", e));
3295 }
3296 }
3297 }
3298
3299 #[cfg(target_os = "linux")]
3300 if let Some(g2d) = self.g2d.as_mut() {
3301 let src_is_yuv = src.format().is_some_and(|f| f.is_yuv());
3308 let dst_is_yuv = dst.format().is_some_and(|f| f.is_yuv());
3309 let g2d_eligible = if src_is_yuv || dst_is_yuv {
3310 let cm = if src_is_yuv {
3311 crate::colorimetry::effective_colorimetry(src)
3312 } else {
3313 crate::colorimetry::effective_colorimetry(dst)
3314 };
3315 crate::g2d::g2d_can_handle(&cm, true)
3316 } else {
3317 true
3318 };
3319 if !g2d_eligible {
3320 log::trace!(
3321 "convert: auto g2d skipped {src_fmt:?}→{dst_fmt:?} \
3322 (colorimetry not expressible: full-range/BT.2020)"
3323 );
3324 } else {
3325 match g2d.convert(src, dst, Some(options)) {
3326 Ok(_) => {
3327 log::trace!(
3328 "convert: auto selected=g2d for {src_fmt:?}→{dst_fmt:?} ({:?})",
3329 start.elapsed()
3330 );
3331 return Ok(());
3332 }
3333 Err(e) => {
3334 log::trace!("convert: auto g2d declined {src_fmt:?}→{dst_fmt:?}: {e}");
3335 declined.push(("g2d", e));
3336 }
3337 }
3338 }
3339 }
3340
3341 if let Some(cpu) = self.cpu.as_mut() {
3342 match cpu.convert(src, dst, Some(options)) {
3343 Ok(_) => {
3344 log::trace!(
3345 "convert: auto selected=cpu for {src_fmt:?}→{dst_fmt:?} ({:?})",
3346 start.elapsed()
3347 );
3348 return Ok(());
3349 }
3350 Err(e) => {
3351 log::trace!("convert: auto cpu failed {src_fmt:?}→{dst_fmt:?}: {e}");
3352 return Err(with_gpu_declines(e, declined));
3353 }
3354 }
3355 }
3356 Err(Error::NoConverter)
3357 }
3358
3359 fn convert_deferred(
3360 &mut self,
3361 src: &TensorDyn,
3362 dst: &mut TensorDyn,
3363 options: Option<ConvertOptions>,
3364 ) -> Result<()> {
3365 let options = self.resolve_interpolation(options.unwrap_or_default());
3366 #[cfg(any(
3372 target_os = "linux",
3373 target_os = "macos",
3374 target_os = "ios",
3375 target_os = "android",
3376 target_os = "windows"
3377 ))]
3378 #[cfg(feature = "opengl")]
3379 {
3380 let gl_forced = matches!(self.forced_backend, Some(ForcedBackend::OpenGl));
3381 if gl_forced || self.forced_backend.is_none() {
3382 if let Some(opengl) = self.opengl.as_mut() {
3383 match opengl.convert_deferred(src, dst, Some(options)) {
3384 Ok(()) => return Ok(()),
3385 Err(e) => {
3386 log::trace!("convert_deferred: gl declined: {e}; eager fallback");
3387 if gl_forced {
3390 return Err(e);
3391 }
3392 }
3393 }
3394 }
3395 }
3396 }
3397 self.convert(src, dst, Some(options))
3398 }
3399
3400 fn flush(&mut self) -> Result<()> {
3401 let _span = tracing::trace_span!("image.flush").entered();
3402 #[cfg(any(
3405 target_os = "linux",
3406 target_os = "macos",
3407 target_os = "ios",
3408 target_os = "android",
3409 target_os = "windows"
3410 ))]
3411 #[cfg(feature = "opengl")]
3412 if let Some(opengl) = self.opengl.as_mut() {
3413 return opengl.flush();
3414 }
3415 Ok(())
3416 }
3417
3418 fn draw_decoded_masks(
3419 &mut self,
3420 dst: &mut TensorDyn,
3421 detect: &[DetectBox],
3422 segmentation: &[Segmentation],
3423 overlay: MaskOverlay<'_>,
3424 ) -> Result<()> {
3425 let _span = tracing::trace_span!(
3426 "image.draw_decoded_masks",
3427 n_detections = detect.len(),
3428 n_segmentations = segmentation.len(),
3429 )
3430 .entered();
3431 let start = Instant::now();
3432
3433 if let Some(bg) = overlay.background {
3434 if bg.aliases(dst) {
3435 return Err(Error::AliasedBuffers(
3436 "background must not reference the same buffer as dst".to_string(),
3437 ));
3438 }
3439 }
3440
3441 let lb_boxes: Vec<DetectBox>;
3444 let lb_segs: Vec<Segmentation>;
3445 let (detect, segmentation) = if let Some(lb) = overlay.content_region {
3446 lb_boxes = detect.iter().map(|&d| unletter_bbox(d, lb)).collect();
3447 if segmentation.len() == lb_boxes.len() {
3450 lb_segs = segmentation
3457 .iter()
3458 .zip(lb_boxes.iter())
3459 .map(|(s, d)| copy_segmentation(s, d.bbox.into()))
3460 .collect::<Result<Vec<_>>>()?;
3461 (lb_boxes.as_slice(), lb_segs.as_slice())
3462 } else {
3463 (lb_boxes.as_slice(), segmentation)
3467 }
3468 } else {
3469 (detect, segmentation)
3470 };
3471 #[cfg(target_os = "linux")]
3472 let is_empty_frame = detect.is_empty() && segmentation.is_empty();
3473
3474 if let Some(forced) = self.forced_backend {
3476 return match forced {
3477 ForcedBackend::Cpu => {
3478 if let Some(cpu) = self.cpu.as_mut() {
3479 return cpu.draw_decoded_masks(dst, detect, segmentation, overlay);
3480 }
3481 Err(Error::ForcedBackendUnavailable("cpu".into()))
3482 }
3483 ForcedBackend::G2d => {
3484 #[cfg(target_os = "linux")]
3487 if let Some(g2d) = self.g2d.as_mut() {
3488 return g2d.draw_decoded_masks(dst, detect, segmentation, overlay);
3489 }
3490 Err(Error::ForcedBackendUnavailable("g2d".into()))
3491 }
3492 ForcedBackend::OpenGl => {
3493 #[cfg(any(
3496 target_os = "linux",
3497 target_os = "macos",
3498 target_os = "ios",
3499 target_os = "android",
3500 target_os = "windows"
3501 ))]
3502 #[cfg(feature = "opengl")]
3503 if let Some(opengl) = self.opengl.as_mut() {
3504 return opengl.draw_decoded_masks(dst, detect, segmentation, overlay);
3505 }
3506 Err(Error::ForcedBackendUnavailable("opengl".into()))
3507 }
3508 };
3509 }
3510
3511 #[cfg(target_os = "linux")]
3517 if is_empty_frame {
3518 if let Some(g2d) = self.g2d.as_mut() {
3519 match g2d.draw_decoded_masks(dst, detect, segmentation, overlay) {
3520 Ok(_) => {
3521 log::trace!(
3522 "draw_decoded_masks empty frame via g2d in {:?}",
3523 start.elapsed()
3524 );
3525 return Ok(());
3526 }
3527 Err(e) => log::trace!("g2d empty-frame path unavailable: {e:?}"),
3528 }
3529 }
3530 }
3531
3532 #[cfg(any(
3536 target_os = "linux",
3537 target_os = "macos",
3538 target_os = "ios",
3539 target_os = "android",
3540 target_os = "windows"
3541 ))]
3542 #[cfg(feature = "opengl")]
3543 if let Some(opengl) = self.opengl.as_mut() {
3544 log::trace!(
3545 "draw_decoded_masks started with opengl in {:?}",
3546 start.elapsed()
3547 );
3548 match opengl.draw_decoded_masks(dst, detect, segmentation, overlay) {
3549 Ok(_) => {
3550 log::trace!("draw_decoded_masks with opengl in {:?}", start.elapsed());
3551 return Ok(());
3552 }
3553 Err(e) => {
3554 log::trace!("draw_decoded_masks didn't work with opengl: {e:?}")
3555 }
3556 }
3557 }
3558
3559 log::trace!(
3560 "draw_decoded_masks started with cpu in {:?}",
3561 start.elapsed()
3562 );
3563 if let Some(cpu) = self.cpu.as_mut() {
3564 match cpu.draw_decoded_masks(dst, detect, segmentation, overlay) {
3565 Ok(_) => {
3566 log::trace!("draw_decoded_masks with cpu in {:?}", start.elapsed());
3567 return Ok(());
3568 }
3569 Err(e) => {
3570 log::trace!("draw_decoded_masks didn't work with cpu: {e:?}");
3571 return Err(e);
3572 }
3573 }
3574 }
3575 Err(Error::NoConverter)
3576 }
3577
3578 fn draw_proto_masks(
3579 &mut self,
3580 dst: &mut TensorDyn,
3581 detect: &[DetectBox],
3582 proto_data: &ProtoData,
3583 overlay: MaskOverlay<'_>,
3584 ) -> Result<()> {
3585 let start = Instant::now();
3586
3587 if let Some(bg) = overlay.background {
3588 if bg.aliases(dst) {
3589 return Err(Error::AliasedBuffers(
3590 "background must not reference the same buffer as dst".to_string(),
3591 ));
3592 }
3593 }
3594
3595 let lb_boxes: Vec<DetectBox>;
3601 let render_detect = if let Some(lb) = overlay.content_region {
3602 lb_boxes = detect.iter().map(|&d| unletter_bbox(d, lb)).collect();
3603 lb_boxes.as_slice()
3604 } else {
3605 detect
3606 };
3607 #[cfg(target_os = "linux")]
3608 let is_empty_frame = detect.is_empty();
3609
3610 if let Some(forced) = self.forced_backend {
3612 return match forced {
3613 ForcedBackend::Cpu => {
3614 if let Some(cpu) = self.cpu.as_mut() {
3615 return cpu.draw_proto_masks(dst, render_detect, proto_data, overlay);
3616 }
3617 Err(Error::ForcedBackendUnavailable("cpu".into()))
3618 }
3619 ForcedBackend::G2d => {
3620 #[cfg(target_os = "linux")]
3621 if let Some(g2d) = self.g2d.as_mut() {
3622 return g2d.draw_proto_masks(dst, render_detect, proto_data, overlay);
3623 }
3624 Err(Error::ForcedBackendUnavailable("g2d".into()))
3625 }
3626 ForcedBackend::OpenGl => {
3627 #[cfg(any(
3628 target_os = "linux",
3629 target_os = "macos",
3630 target_os = "ios",
3631 target_os = "android",
3632 target_os = "windows"
3633 ))]
3634 #[cfg(feature = "opengl")]
3635 if let Some(opengl) = self.opengl.as_mut() {
3636 return opengl.draw_proto_masks(dst, render_detect, proto_data, overlay);
3637 }
3638 Err(Error::ForcedBackendUnavailable("opengl".into()))
3639 }
3640 };
3641 }
3642
3643 #[cfg(target_os = "linux")]
3646 if is_empty_frame {
3647 if let Some(g2d) = self.g2d.as_mut() {
3648 match g2d.draw_proto_masks(dst, render_detect, proto_data, overlay) {
3649 Ok(_) => {
3650 log::trace!(
3651 "draw_proto_masks empty frame via g2d in {:?}",
3652 start.elapsed()
3653 );
3654 return Ok(());
3655 }
3656 Err(e) => log::trace!("g2d empty-frame path unavailable: {e:?}"),
3657 }
3658 }
3659 }
3660
3661 #[cfg(any(
3670 target_os = "linux",
3671 target_os = "macos",
3672 target_os = "ios",
3673 target_os = "android",
3674 target_os = "windows"
3675 ))]
3676 #[cfg(feature = "opengl")]
3677 if let (Some(_), Some(_)) = (self.cpu.as_ref(), self.opengl.as_ref()) {
3678 let segmentation = match self.cpu.as_mut() {
3679 Some(cpu) => {
3680 log::trace!(
3681 "draw_proto_masks started with hybrid (cpu+opengl) in {:?}",
3682 start.elapsed()
3683 );
3684 cpu.materialize_segmentations(detect, proto_data, overlay.content_region)?
3685 }
3686 None => unreachable!("cpu presence checked above"),
3687 };
3688 if let Some(opengl) = self.opengl.as_mut() {
3689 match opengl.draw_decoded_masks(dst, render_detect, &segmentation, overlay) {
3690 Ok(_) => {
3691 log::trace!(
3692 "draw_proto_masks with hybrid (cpu+opengl) in {:?}",
3693 start.elapsed()
3694 );
3695 return Ok(());
3696 }
3697 Err(e) => {
3698 log::trace!(
3699 "draw_proto_masks hybrid path failed, falling back to cpu: {e:?}"
3700 );
3701 }
3702 }
3703 }
3704 }
3705
3706 let Some(cpu) = self.cpu.as_mut() else {
3707 return Err(Error::Internal(
3708 "draw_proto_masks requires CPU backend for fallback path".into(),
3709 ));
3710 };
3711 log::trace!("draw_proto_masks started with cpu in {:?}", start.elapsed());
3712 cpu.draw_proto_masks(dst, render_detect, proto_data, overlay)
3713 }
3714
3715 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()> {
3716 let start = Instant::now();
3717
3718 if let Some(forced) = self.forced_backend {
3720 return match forced {
3721 ForcedBackend::Cpu => {
3722 if let Some(cpu) = self.cpu.as_mut() {
3723 return cpu.set_class_colors(colors);
3724 }
3725 Err(Error::ForcedBackendUnavailable("cpu".into()))
3726 }
3727 ForcedBackend::G2d => Err(Error::NotSupported(
3728 "g2d does not support set_class_colors".into(),
3729 )),
3730 ForcedBackend::OpenGl => {
3731 #[cfg(any(
3732 target_os = "linux",
3733 target_os = "macos",
3734 target_os = "ios",
3735 target_os = "android",
3736 target_os = "windows"
3737 ))]
3738 #[cfg(feature = "opengl")]
3739 if let Some(opengl) = self.opengl.as_mut() {
3740 return opengl.set_class_colors(colors);
3741 }
3742 Err(Error::ForcedBackendUnavailable("opengl".into()))
3743 }
3744 };
3745 }
3746
3747 let mut backends = 0usize;
3756 let mut first_err = None;
3757 #[cfg(any(
3758 target_os = "linux",
3759 target_os = "macos",
3760 target_os = "ios",
3761 target_os = "android",
3762 target_os = "windows"
3763 ))]
3764 #[cfg(feature = "opengl")]
3765 if let Some(opengl) = self.opengl.as_mut() {
3766 backends += 1;
3767 match opengl.set_class_colors(colors) {
3768 Ok(()) => log::trace!("colors set with opengl in {:?}", start.elapsed()),
3769 Err(e) => {
3770 log::trace!("colors didn't set with opengl: {e:?}");
3771 first_err.get_or_insert(e);
3772 }
3773 }
3774 }
3775 if let Some(cpu) = self.cpu.as_mut() {
3776 backends += 1;
3777 match cpu.set_class_colors(colors) {
3778 Ok(()) => log::trace!("colors set with cpu in {:?}", start.elapsed()),
3779 Err(e) => {
3780 log::trace!("colors didn't set with cpu: {e:?}");
3781 first_err.get_or_insert(e);
3782 }
3783 }
3784 }
3785 match first_err {
3789 Some(e) => Err(e),
3790 None if backends > 0 => Ok(()),
3791 None => Err(Error::NoConverter),
3792 }
3793 }
3794
3795 fn set_box_line_width(&mut self, width: u32) -> Result<()> {
3796 check_box_line_width(width)?;
3797
3798 if let Some(forced) = self.forced_backend {
3799 return match forced {
3800 ForcedBackend::Cpu => {
3801 if let Some(cpu) = self.cpu.as_mut() {
3802 return cpu.set_box_line_width(width);
3803 }
3804 Err(Error::ForcedBackendUnavailable("cpu".into()))
3805 }
3806 ForcedBackend::G2d => Err(Error::NotSupported(
3807 "g2d does not support set_box_line_width".into(),
3808 )),
3809 ForcedBackend::OpenGl => {
3810 #[cfg(any(
3811 target_os = "linux",
3812 target_os = "macos",
3813 target_os = "ios",
3814 target_os = "android",
3815 target_os = "windows"
3816 ))]
3817 #[cfg(feature = "opengl")]
3818 if let Some(opengl) = self.opengl.as_mut() {
3819 return opengl.set_box_line_width(width);
3820 }
3821 Err(Error::ForcedBackendUnavailable("opengl".into()))
3822 }
3823 };
3824 }
3825
3826 let mut backends = 0usize;
3828 let mut first_err = None;
3829 #[cfg(any(
3830 target_os = "linux",
3831 target_os = "macos",
3832 target_os = "ios",
3833 target_os = "android",
3834 target_os = "windows"
3835 ))]
3836 #[cfg(feature = "opengl")]
3837 if let Some(opengl) = self.opengl.as_mut() {
3838 backends += 1;
3839 if let Err(e) = opengl.set_box_line_width(width) {
3840 log::trace!("box line width didn't set with opengl: {e:?}");
3841 first_err.get_or_insert(e);
3842 }
3843 }
3844 if let Some(cpu) = self.cpu.as_mut() {
3845 backends += 1;
3846 if let Err(e) = cpu.set_box_line_width(width) {
3847 log::trace!("box line width didn't set with cpu: {e:?}");
3848 first_err.get_or_insert(e);
3849 }
3850 }
3851 match first_err {
3852 Some(e) => Err(e),
3853 None if backends > 0 => Ok(()),
3854 None => Err(Error::NoConverter),
3855 }
3856 }
3857}
3858
3859#[cfg(all(test, feature = "codec"))]
3869pub(crate) fn load_image_test_helper(
3870 image: &[u8],
3871 format: Option<PixelFormat>,
3872 memory: Option<TensorMemory>,
3873) -> Result<TensorDyn> {
3874 use edgefirst_codec::{peek_info, ImageDecoder, ImageLoad};
3875
3876 let info = peek_info(image)?;
3880 let native_fmt = info.format;
3881 let w = info.width;
3882 let h = info.height;
3883
3884 let mut decoder = ImageDecoder::new();
3885
3886 #[cfg(target_os = "linux")]
3889 let native_src = {
3890 if let Some(aligned_pitch) = padded_dma_pitch_for(native_fmt, w, &memory) {
3891 let mut dma = Tensor::<u8>::image_with_stride(
3892 w,
3893 h,
3894 native_fmt,
3895 aligned_pitch,
3896 Some(TensorMemory::DmaBuf),
3897 edgefirst_tensor::CpuAccess::ReadWrite,
3898 )?;
3899 dma.load_image(&mut decoder, image)?;
3900 TensorDyn::from(dma)
3901 } else {
3902 let mut img = Tensor::<u8>::image(
3903 w,
3904 h,
3905 native_fmt,
3906 memory,
3907 edgefirst_tensor::CpuAccess::ReadWrite,
3908 )?;
3909 img.load_image(&mut decoder, image)?;
3910 TensorDyn::from(img)
3911 }
3912 };
3913 #[cfg(not(target_os = "linux"))]
3914 let native_src = {
3915 let mut img = Tensor::<u8>::image(
3916 w,
3917 h,
3918 native_fmt,
3919 memory,
3920 edgefirst_tensor::CpuAccess::ReadWrite,
3921 )?;
3922 img.load_image(&mut decoder, image)?;
3923 TensorDyn::from(img)
3924 };
3925
3926 match format {
3930 Some(f) if f != native_fmt => {
3931 let mut dst = TensorDyn::image(
3932 w,
3933 h,
3934 f,
3935 DType::U8,
3936 memory,
3937 edgefirst_tensor::CpuAccess::ReadWrite,
3938 )?;
3939 #[allow(clippy::needless_update)]
3946 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
3947 backend: ComputeBackend::Cpu,
3948 ..Default::default()
3949 })?;
3950 proc.convert(&native_src, &mut dst, None)?;
3951 Ok(dst)
3952 }
3953 _ => Ok(native_src),
3954 }
3955}
3956
3957pub fn save_jpeg(tensor: &TensorDyn, path: impl AsRef<std::path::Path>, quality: u8) -> Result<()> {
3961 let t = tensor.as_typed::<u8>().ok_or(Error::UnsupportedFormat(
3962 "save_jpeg requires u8 tensor".to_string(),
3963 ))?;
3964 let fmt = t.format().ok_or(Error::NotAnImage)?;
3965 if fmt.layout() != PixelLayout::Packed {
3966 return Err(Error::NotImplemented(
3967 "Saving planar images is not supported".to_string(),
3968 ));
3969 }
3970
3971 let colour = match fmt {
3972 PixelFormat::Rgb => jpeg_encoder::ColorType::Rgb,
3973 PixelFormat::Rgba => jpeg_encoder::ColorType::Rgba,
3974 _ => {
3975 return Err(Error::NotImplemented(
3976 "Unsupported image format for saving".to_string(),
3977 ));
3978 }
3979 };
3980
3981 let w = t.width().ok_or(Error::NotAnImage)?;
3982 let h = t.height().ok_or(Error::NotAnImage)?;
3983 let encoder = jpeg_encoder::Encoder::new_file(path, quality)?;
3984 let tensor_map = t.map_read()?;
3985
3986 encoder.encode(&tensor_map, w as u16, h as u16, colour)?;
3987
3988 Ok(())
3989}
3990
3991pub(crate) struct FunctionTimer<T: Display> {
3992 name: T,
3993 start: std::time::Instant,
3994}
3995
3996impl<T: Display> FunctionTimer<T> {
3997 pub fn new(name: T) -> Self {
3998 Self {
3999 name,
4000 start: std::time::Instant::now(),
4001 }
4002 }
4003}
4004
4005impl<T: Display> Drop for FunctionTimer<T> {
4006 fn drop(&mut self) {
4007 log::trace!("{} elapsed: {:?}", self.name, self.start.elapsed())
4008 }
4009}
4010
4011const DEFAULT_COLORS: [[f32; 4]; 20] = [
4012 [0., 1., 0., 0.7],
4013 [1., 0.5568628, 0., 0.7],
4014 [0.25882353, 0.15294118, 0.13333333, 0.7],
4015 [0.8, 0.7647059, 0.78039216, 0.7],
4016 [0.3137255, 0.3137255, 0.3137255, 0.7],
4017 [0.1411765, 0.3098039, 0.1215686, 0.7],
4018 [1., 0.95686275, 0.5137255, 0.7],
4019 [0.3529412, 0.32156863, 0., 0.7],
4020 [0.4235294, 0.6235294, 0.6509804, 0.7],
4021 [0.5098039, 0.5098039, 0.7294118, 0.7],
4022 [0.00784314, 0.18823529, 0.29411765, 0.7],
4023 [0.0, 0.2706, 1.0, 0.7],
4024 [0.0, 0.0, 0.0, 0.7],
4025 [0.0, 0.5, 0.0, 0.7],
4026 [1.0, 0.0, 0.0, 0.7],
4027 [0.0, 0.0, 1.0, 0.7],
4028 [1.0, 0.5, 0.5, 0.7],
4029 [0.1333, 0.5451, 0.1333, 0.7],
4030 [0.1176, 0.4118, 0.8235, 0.7],
4031 [1., 1., 1., 0.7],
4032];
4033
4034const fn denorm<const M: usize, const N: usize>(a: [[f32; M]; N]) -> [[u8; M]; N] {
4035 let mut result = [[0; M]; N];
4036 let mut i = 0;
4037 while i < N {
4038 let mut j = 0;
4039 while j < M {
4040 result[i][j] = (a[i][j] * 255.0).round() as u8;
4041 j += 1;
4042 }
4043 i += 1;
4044 }
4045 result
4046}
4047
4048const DEFAULT_COLORS_U8: [[u8; 4]; 20] = denorm(DEFAULT_COLORS);
4049
4050pub const DEFAULT_BOX_LINE_WIDTH: u32 = 3;
4053
4054pub(crate) fn check_box_line_width(width: u32) -> Result<()> {
4056 if width == 0 {
4057 return Err(Error::InvalidOptions(
4058 "box line width must be at least 1 pixel".into(),
4059 ));
4060 }
4061 Ok(())
4062}
4063
4064#[cfg(test)]
4065#[cfg_attr(coverage_nightly, coverage(off))]
4066mod alignment_tests {
4067 use super::*;
4068
4069 #[test]
4070 fn align_width_rgba8_common_widths() {
4071 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); }
4082
4083 #[test]
4084 fn align_width_rgb888_packed() {
4085 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] {
4092 let padded = align_width_for_gpu_pitch(w, 3);
4093 assert!(padded >= w);
4094 assert_eq!((padded * 3) % 64, 0);
4095 assert_eq!((padded * 3) % 3, 0);
4096 }
4097 }
4098
4099 #[test]
4100 fn align_width_grey_u8() {
4101 assert_eq!(align_width_for_gpu_pitch(64, 1), 64);
4103 assert_eq!(align_width_for_gpu_pitch(640, 1), 640);
4104 assert_eq!(align_width_for_gpu_pitch(1, 1), 64);
4105 assert_eq!(align_width_for_gpu_pitch(65, 1), 128);
4106 }
4107
4108 #[test]
4109 fn align_width_zero_inputs() {
4110 assert_eq!(align_width_for_gpu_pitch(0, 4), 0);
4111 assert_eq!(align_width_for_gpu_pitch(640, 0), 640);
4112 }
4113
4114 #[test]
4115 fn align_width_never_returns_smaller_than_input() {
4116 for &bpp in &[1usize, 2, 3, 4, 8] {
4120 for &w in &[
4121 1usize,
4122 17,
4123 64,
4124 65,
4125 100,
4126 1280,
4127 1281,
4128 1920,
4129 3004,
4130 3072,
4131 3840,
4132 usize::MAX / 8,
4133 usize::MAX / 4,
4134 usize::MAX / 2,
4135 usize::MAX - 1,
4136 usize::MAX,
4137 ] {
4138 let aligned = align_width_for_gpu_pitch(w, bpp);
4139 assert!(
4140 aligned >= w,
4141 "align_width_for_gpu_pitch({w}, {bpp}) = {aligned} < {w}"
4142 );
4143 }
4144 }
4145 }
4146
4147 #[test]
4148 fn align_width_overflow_returns_unaligned_not_smaller() {
4149 let aligned_extreme = usize::MAX - 15; assert_eq!(
4155 align_width_for_gpu_pitch(aligned_extreme, 4),
4156 aligned_extreme
4157 );
4158 let misaligned_extreme = usize::MAX - 1;
4161 let result = align_width_for_gpu_pitch(misaligned_extreme, 4);
4162 assert!(
4163 result == misaligned_extreme || result >= misaligned_extreme,
4164 "extreme misaligned width must not be rounded down to {result}"
4165 );
4166 }
4167
4168 #[test]
4169 fn checked_lcm_basic_and_overflow() {
4170 assert_eq!(checked_num_integer_lcm(64, 4), Some(64));
4171 assert_eq!(checked_num_integer_lcm(64, 3), Some(192));
4172 assert_eq!(checked_num_integer_lcm(64, 1), Some(64));
4173 assert_eq!(checked_num_integer_lcm(0, 4), Some(0));
4174 assert_eq!(checked_num_integer_lcm(64, 0), Some(0));
4175 assert_eq!(
4177 checked_num_integer_lcm(usize::MAX, usize::MAX - 1),
4178 None,
4179 "coprime extreme values must overflow detect, not panic"
4180 );
4181 }
4182
4183 #[test]
4184 fn primary_plane_bpp_known_formats() {
4185 assert_eq!(primary_plane_bpp(PixelFormat::Rgba, 1), Some(4));
4187 assert_eq!(primary_plane_bpp(PixelFormat::Bgra, 1), Some(4));
4188 assert_eq!(primary_plane_bpp(PixelFormat::Rgb, 1), Some(3));
4189 assert_eq!(primary_plane_bpp(PixelFormat::Grey, 1), Some(1));
4190 assert_eq!(primary_plane_bpp(PixelFormat::Nv12, 1), Some(1));
4192 }
4193}
4194
4195#[cfg(test)]
4196#[cfg_attr(coverage_nightly, coverage(off))]
4197#[allow(deprecated)]
4198mod image_tests {
4199 use super::*;
4200 #[cfg(target_os = "linux")]
4201 use crate::test_support::is_dma_available;
4202 use crate::{CPUProcessor, Rotation};
4203 use edgefirst_tensor::{TensorMapTrait, TensorMemory, TensorTrait};
4204 use image::buffer::ConvertBuffer;
4205
4206 fn convert_img(
4212 proc: &mut dyn ImageProcessorTrait,
4213 src: TensorDyn,
4214 dst: TensorDyn,
4215 rotation: Rotation,
4216 flip: Flip,
4217 crop: Crop,
4218 ) -> (Result<()>, TensorDyn, TensorDyn) {
4219 let src_fourcc = src.format().unwrap();
4220 let dst_fourcc = dst.format().unwrap();
4221 let src_dyn = src;
4222 let mut dst_dyn = dst;
4223 let result = proc.convert(
4224 &src_dyn,
4225 &mut dst_dyn,
4226 Some(
4227 ConvertOptions::new()
4228 .rotation(rotation)
4229 .flip(flip)
4230 .crop(crop),
4231 ),
4232 );
4233 let src_back = {
4234 let mut __t = src_dyn.into_u8().unwrap();
4235 __t.set_format(src_fourcc).unwrap();
4236 TensorDyn::from(__t)
4237 };
4238 let dst_back = {
4239 let mut __t = dst_dyn.into_u8().unwrap();
4240 __t.set_format(dst_fourcc).unwrap();
4241 TensorDyn::from(__t)
4242 };
4243 (result, src_back, dst_back)
4244 }
4245
4246 #[ctor::ctor(unsafe)]
4247 fn init() {
4248 env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
4249 }
4250
4251 macro_rules! function {
4252 () => {{
4253 fn f() {}
4254 fn type_name_of<T>(_: T) -> &'static str {
4255 std::any::type_name::<T>()
4256 }
4257 let name = type_name_of(f);
4258
4259 match &name[..name.len() - 3].rfind(':') {
4261 Some(pos) => &name[pos + 1..name.len() - 3],
4262 None => &name[..name.len() - 3],
4263 }
4264 }};
4265 }
4266
4267 #[test]
4280 fn batch_view_dst_tiles_match_standalone() {
4281 let mut proc = match ImageProcessor::new() {
4282 Ok(p) => p,
4283 Err(e) => {
4284 crate::test_support::report_skip(&format!(
4285 "{} — ImageProcessor init failed ({e:?})",
4286 function!()
4287 ));
4288 return;
4289 }
4290 };
4291 let n = 3usize;
4292 let (w, h) = (32usize, 24usize);
4293 let colors: [[u8; 4]; 3] = [[210, 40, 40, 255], [40, 210, 40, 255], [40, 40, 210, 255]];
4294 let make_src = |c: [u8; 4]| -> TensorDyn {
4295 let bytes: Vec<u8> = c.iter().copied().cycle().take(w * h * 4).collect();
4296 load_bytes_to_tensor(w, h, PixelFormat::Rgba, Some(TensorMemory::Mem), &bytes).unwrap()
4297 };
4298 let Some(parent) = crate::test_support::dma_allocated(
4301 TensorDyn::image(
4302 w,
4303 n * h,
4304 PixelFormat::Rgba,
4305 DType::U8,
4306 Some(TensorMemory::DmaBuf),
4307 edgefirst_tensor::CpuAccess::ReadWrite,
4308 ),
4309 "tall DMA destination",
4310 ) else {
4311 return;
4312 };
4313
4314 for (i, &c) in colors.iter().enumerate().take(n) {
4316 let mut tile = parent.view(Region::new(0, i * h, w, h)).unwrap();
4317 proc.convert_deferred(&make_src(c), &mut tile, None)
4318 .unwrap_or_else(|e| panic!("convert_deferred tile {i}: {e:?}"));
4319 }
4320 proc.flush().unwrap();
4321
4322 for (i, &c) in colors.iter().enumerate().take(n) {
4323 let mut solo = TensorDyn::image(
4325 w,
4326 h,
4327 PixelFormat::Rgba,
4328 DType::U8,
4329 Some(TensorMemory::DmaBuf),
4330 edgefirst_tensor::CpuAccess::ReadWrite,
4331 )
4332 .unwrap();
4333 proc.convert(&make_src(c), &mut solo, None).unwrap();
4334
4335 let band = parent.view(Region::new(0, i * h, w, h)).unwrap();
4336 let band_bytes = band
4337 .as_typed::<u8>()
4338 .unwrap()
4339 .map()
4340 .unwrap()
4341 .as_slice()
4342 .to_vec();
4343 let solo_bytes = solo
4344 .as_typed::<u8>()
4345 .unwrap()
4346 .map()
4347 .unwrap()
4348 .as_slice()
4349 .to_vec();
4350 assert_eq!(
4351 band_bytes, solo_bytes,
4352 "tile {i}: band differs from standalone convert (placement or sibling wipe)"
4353 );
4354 assert!(
4355 band_bytes.as_chunks::<4>().0.iter().all(|&p| p == c),
4356 "tile {i}: band is not the expected solid color {c:?} (sibling wipe?)"
4357 );
4358 }
4359 }
4360
4361 #[cfg(test)]
4365 fn gradient_frame(w: usize, h: usize) -> TensorDyn {
4366 let mut bytes = vec![0u8; w * h * 4];
4367 for y in 0..h {
4368 for x in 0..w {
4369 let i = (y * w + x) * 4;
4370 bytes[i] = x as u8;
4371 bytes[i + 1] = y as u8;
4372 bytes[i + 2] = (x ^ y) as u8;
4373 bytes[i + 3] = 255;
4374 }
4375 }
4376 load_bytes_to_tensor(w, h, PixelFormat::Rgba, Some(TensorMemory::Mem), &bytes).unwrap()
4377 }
4378
4379 #[test]
4383 fn set_box_line_width_reaches_the_drawing_backend() {
4384 use crate::test_support::{box_drawn_columns, box_expected_span, BOX_DIM};
4385 let configs = [
4386 ImageProcessorConfig::default(),
4387 ImageProcessorConfig {
4388 backend: ComputeBackend::Cpu,
4389 ..Default::default()
4390 },
4391 ];
4392 for config in configs {
4393 let backend = config.backend;
4394 let mut proc = ImageProcessor::with_config(config).unwrap();
4395 let err = proc.set_box_line_width(0).unwrap_err();
4396 assert!(
4397 matches!(err, Error::InvalidOptions(_)),
4398 "{backend:?}: {err:?}"
4399 );
4400 let flat =
4401 crate::test_support::draw_box_fixture(&mut proc, [200, 100, 50, 255], Some(4), 1.0);
4402 assert_eq!(
4403 box_drawn_columns(&flat, BOX_DIM / 2),
4404 box_expected_span(4),
4405 "{backend:?}"
4406 );
4407 }
4408 }
4409
4410 #[test]
4414 fn tile_into_cpu_distinct_content_parity() {
4415 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
4416 backend: ComputeBackend::Cpu,
4417 ..Default::default()
4418 })
4419 .expect("the CPU backend is always available");
4420 let (fw, fh) = (96usize, 64usize);
4421 let src = gradient_frame(fw, fh);
4422 let cfg = TilingConfig::new(32, 32).with_overlap(0.0); let n = tile_grid(fh, fw, 32, 32, 0.0).len();
4424 assert_eq!(n, 6);
4425
4426 let mut parent = proc
4427 .alloc_tile_batch(
4428 n,
4429 &cfg,
4430 PixelFormat::Rgba,
4431 DType::U8,
4432 Some(TensorMemory::Mem),
4433 edgefirst_tensor::CpuAccess::ReadWrite,
4434 )
4435 .unwrap();
4436 let placements = proc.tile_into(&src, &mut parent, &cfg).unwrap();
4437 assert_eq!(placements.len(), n);
4438
4439 for p in &placements {
4440 let source = Region::new(
4441 p.origin.0 as usize,
4442 p.origin.1 as usize,
4443 p.crop_size.0 as usize,
4444 p.crop_size.1 as usize,
4445 );
4446 let mut solo = TensorDyn::image(
4447 32,
4448 32,
4449 PixelFormat::Rgba,
4450 DType::U8,
4451 Some(TensorMemory::Mem),
4452 edgefirst_tensor::CpuAccess::ReadWrite,
4453 )
4454 .unwrap();
4455 proc.convert(
4456 &src,
4457 &mut solo,
4458 Some(
4459 crate::ConvertOptions::new().crop(
4460 Crop::default()
4461 .with_source(Some(source))
4462 .with_fit(Fit::Stretch),
4463 ),
4464 ),
4465 )
4466 .unwrap();
4467 let band = parent.view(Region::new(0, p.index * 32, 32, 32)).unwrap();
4468 let band_bytes = band
4469 .as_typed::<u8>()
4470 .unwrap()
4471 .map()
4472 .unwrap()
4473 .as_slice()
4474 .to_vec();
4475 let solo_bytes = solo
4476 .as_typed::<u8>()
4477 .unwrap()
4478 .map()
4479 .unwrap()
4480 .as_slice()
4481 .to_vec();
4482 assert_eq!(
4483 band_bytes, solo_bytes,
4484 "tile {} band differs from standalone crop-convert",
4485 p.index
4486 );
4487 }
4488 }
4489
4490 #[test]
4493 fn tile_one_matches_tile_into_band() {
4494 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
4495 backend: ComputeBackend::Cpu,
4496 ..Default::default()
4497 })
4498 .expect("the CPU backend is always available");
4499 let (fw, fh) = (96usize, 64usize);
4500 let src = gradient_frame(fw, fh);
4501 let cfg = TilingConfig::new(32, 32).with_overlap(0.0);
4502 let n = tile_grid(fh, fw, 32, 32, 0.0).len();
4503
4504 let mut parent = proc
4505 .alloc_tile_batch(
4506 n,
4507 &cfg,
4508 PixelFormat::Rgba,
4509 DType::U8,
4510 Some(TensorMemory::Mem),
4511 edgefirst_tensor::CpuAccess::ReadWrite,
4512 )
4513 .unwrap();
4514 proc.tile_into(&src, &mut parent, &cfg).unwrap();
4515
4516 let plan = proc.plan_tiles(fw, fh, &cfg).unwrap();
4517 for p in &plan {
4518 let mut slot = TensorDyn::image(
4519 32,
4520 32,
4521 PixelFormat::Rgba,
4522 DType::U8,
4523 Some(TensorMemory::Mem),
4524 edgefirst_tensor::CpuAccess::ReadWrite,
4525 )
4526 .unwrap();
4527 proc.tile_one(&src, &mut slot, p, &cfg).unwrap();
4528 proc.flush().unwrap();
4529 let band = parent.view(Region::new(0, p.index * 32, 32, 32)).unwrap();
4530 let slot_bytes = slot
4531 .as_typed::<u8>()
4532 .unwrap()
4533 .map()
4534 .unwrap()
4535 .as_slice()
4536 .to_vec();
4537 let band_bytes = band
4538 .as_typed::<u8>()
4539 .unwrap()
4540 .map()
4541 .unwrap()
4542 .as_slice()
4543 .to_vec();
4544 assert_eq!(
4545 slot_bytes, band_bytes,
4546 "tile {} stream != batch band",
4547 p.index
4548 );
4549 }
4550 }
4551
4552 #[test]
4559 fn tile_into_auto_dma_parity() {
4560 let mut proc = match ImageProcessor::new() {
4561 Ok(p) => p,
4562 Err(e) => {
4563 crate::test_support::report_skip(&format!(
4564 "{} — ImageProcessor init failed ({e:?})",
4565 function!()
4566 ));
4567 return;
4568 }
4569 };
4570 let (fw, fh) = (96usize, 64usize);
4571 let src = gradient_frame(fw, fh);
4572 let cfg = TilingConfig::new(32, 32).with_overlap(0.0);
4573 let n = tile_grid(fh, fw, 32, 32, 0.0).len();
4574
4575 let Some(mut parent) = crate::test_support::dma_allocated(
4576 proc.alloc_tile_batch(
4577 n,
4578 &cfg,
4579 PixelFormat::Rgba,
4580 DType::U8,
4581 Some(TensorMemory::DmaBuf),
4582 edgefirst_tensor::CpuAccess::ReadWrite,
4583 ),
4584 "tall DMA parent",
4585 ) else {
4586 return;
4587 };
4588 let placements = proc.tile_into(&src, &mut parent, &cfg).unwrap();
4589
4590 for p in &placements {
4591 let source = Region::new(
4592 p.origin.0 as usize,
4593 p.origin.1 as usize,
4594 p.crop_size.0 as usize,
4595 p.crop_size.1 as usize,
4596 );
4597 let mut solo = TensorDyn::image(
4598 32,
4599 32,
4600 PixelFormat::Rgba,
4601 DType::U8,
4602 Some(TensorMemory::DmaBuf),
4603 edgefirst_tensor::CpuAccess::ReadWrite,
4604 )
4605 .unwrap();
4606 proc.convert(
4607 &src,
4608 &mut solo,
4609 Some(
4610 crate::ConvertOptions::new().crop(
4611 Crop::default()
4612 .with_source(Some(source))
4613 .with_fit(Fit::Stretch),
4614 ),
4615 ),
4616 )
4617 .unwrap();
4618 let band = parent.view(Region::new(0, p.index * 32, 32, 32)).unwrap();
4619 compare_images(
4625 &band,
4626 &solo,
4627 0.98,
4628 &format!("{}_tile{}", function!(), p.index),
4629 );
4630 }
4631 }
4632
4633 #[test]
4635 fn tile_into_undersized_dst_errors() {
4636 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
4637 backend: ComputeBackend::Cpu,
4638 ..Default::default()
4639 })
4640 .expect("the CPU backend is always available");
4641 let (fw, fh) = (96usize, 64usize);
4642 let src = gradient_frame(fw, fh);
4643 let cfg = TilingConfig::new(32, 32).with_overlap(0.0); let mut small = TensorDyn::image(
4646 32,
4647 2 * 32,
4648 PixelFormat::Rgba,
4649 DType::U8,
4650 Some(TensorMemory::Mem),
4651 edgefirst_tensor::CpuAccess::ReadWrite,
4652 )
4653 .unwrap();
4654 let r = proc.tile_into(&src, &mut small, &cfg);
4655 assert!(
4656 matches!(r, Err(Error::InvalidShape(_))),
4657 "expected InvalidShape, got {r:?}"
4658 );
4659 }
4660
4661 #[test]
4663 fn tiling_alloc_rejects_invalid_config() {
4664 let proc = ImageProcessor::with_config(ImageProcessorConfig {
4665 backend: ComputeBackend::Cpu,
4666 ..Default::default()
4667 })
4668 .expect("the CPU backend is always available");
4669 let bad = TilingConfig::new(0, 640);
4670 assert!(proc.plan_tiles(1920, 1080, &bad).is_err());
4671 assert!(proc
4672 .alloc_tile_batch(
4673 4,
4674 &bad,
4675 PixelFormat::Rgba,
4676 DType::U8,
4677 Some(TensorMemory::Mem),
4678 edgefirst_tensor::CpuAccess::ReadWrite,
4679 )
4680 .is_err());
4681 }
4682
4683 #[test]
4684 fn plan_tiles_metadata_4k() {
4685 let proc = ImageProcessor::with_config(ImageProcessorConfig {
4686 backend: ComputeBackend::Cpu,
4687 ..Default::default()
4688 })
4689 .expect("the CPU backend is always available");
4690 let cfg = TilingConfig::new(640, 640).with_overlap(0.2);
4691 let plan = proc.plan_tiles(3840, 2160, &cfg).unwrap();
4692 assert_eq!(plan.len(), 32);
4693 assert!(plan.iter().all(|p| p.count == 32));
4694 assert!(plan.iter().all(|p| p.crop_size == (640.0, 640.0)));
4695 assert!(plan.iter().all(|p| p.content_region.is_none())); assert!(plan.iter().all(|p| p.frame_dims == (3840.0, 2160.0)));
4697 assert_eq!(plan[0].origin, (0.0, 0.0));
4698 }
4699
4700 #[test]
4701 fn test_invalid_crop() {
4702 let src = TensorDyn::image(
4703 100,
4704 100,
4705 PixelFormat::Rgb,
4706 DType::U8,
4707 None,
4708 edgefirst_tensor::CpuAccess::ReadWrite,
4709 )
4710 .unwrap();
4711 let dst = TensorDyn::image(
4712 100,
4713 100,
4714 PixelFormat::Rgb,
4715 DType::U8,
4716 None,
4717 edgefirst_tensor::CpuAccess::ReadWrite,
4718 )
4719 .unwrap();
4720
4721 let crop = Crop::new().with_source(Some(Region::new(50, 50, 60, 60)));
4723 assert!(matches!(
4724 crop.check_crop_dyn(&src, &dst),
4725 Err(Error::CropInvalid(_))
4726 ));
4727
4728 let crop = Crop::new().with_source(Some(Region::new(0, 0, 10, 10)));
4730 assert!(crop.check_crop_dyn(&src, &dst).is_ok());
4731
4732 assert!(Crop::letterbox([0, 0, 0, 255])
4734 .check_crop_dyn(&src, &dst)
4735 .is_ok());
4736 }
4737
4738 #[test]
4739 fn test_invalid_tensor_format() -> Result<(), Error> {
4740 let mut tensor = Tensor::<u8>::new(&[720, 1280, 4, 1], None, None)?;
4742 let result = tensor.set_format(PixelFormat::Rgb);
4743 assert!(result.is_err(), "4D tensor should reject set_format");
4744
4745 let mut tensor = Tensor::<u8>::new(&[720, 1280, 4], None, None)?;
4747 let result = tensor.set_format(PixelFormat::Rgb);
4748 assert!(result.is_err(), "4-channel tensor should reject RGB format");
4749
4750 Ok(())
4751 }
4752
4753 #[test]
4754 fn test_invalid_image_file() -> Result<(), Error> {
4755 let result = crate::load_image_test_helper(&[123; 5000], None, None);
4756 assert!(
4757 matches!(result, Err(Error::Codec(_))),
4758 "unrecognised bytes should surface as Error::Codec, got {result:?}"
4759 );
4760 Ok(())
4761 }
4762
4763 #[test]
4764 fn test_invalid_jpeg_format() -> Result<(), Error> {
4765 let result = crate::load_image_test_helper(&[123; 5000], Some(PixelFormat::Yuyv), None);
4766 assert!(
4769 matches!(result, Err(Error::Codec(_))),
4770 "Yuyv target with garbage bytes should surface as Error::Codec, got {result:?}"
4771 );
4772 Ok(())
4773 }
4774
4775 #[test]
4776 fn test_load_resize_save() {
4777 let file = edgefirst_bench::testdata::read("zidane.jpg");
4778 let img = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4779 assert_eq!(img.width(), Some(1280));
4780 assert_eq!(img.height(), Some(720));
4781
4782 let dst = TensorDyn::image(
4783 640,
4784 360,
4785 PixelFormat::Rgba,
4786 DType::U8,
4787 None,
4788 edgefirst_tensor::CpuAccess::ReadWrite,
4789 )
4790 .unwrap();
4791 let mut converter = CPUProcessor::new();
4792 let (result, _img, dst) = convert_img(
4793 &mut converter,
4794 img,
4795 dst,
4796 Rotation::None,
4797 Flip::None,
4798 Crop::no_crop(),
4799 );
4800 result.unwrap();
4801 assert_eq!(dst.width(), Some(640));
4802 assert_eq!(dst.height(), Some(360));
4803
4804 crate::save_jpeg(&dst, "zidane_resized.jpg", 80).unwrap();
4805
4806 let file = std::fs::read("zidane_resized.jpg").unwrap();
4807 let img = crate::load_image_test_helper(&file, None, None).unwrap();
4810 assert_eq!(img.width(), Some(640));
4811 assert_eq!(img.height(), Some(360));
4812 assert_eq!(img.format().unwrap(), PixelFormat::Nv12);
4813 }
4814
4815 #[test]
4816 fn test_from_tensor_planar() -> Result<(), Error> {
4817 let mut tensor = Tensor::new(&[3, 720, 1280], None, None)?;
4818 tensor
4819 .map()?
4820 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.8bps"));
4821 let planar = {
4822 tensor
4823 .set_format(PixelFormat::PlanarRgb)
4824 .map_err(|e| crate::Error::Internal(e.to_string()))?;
4825 TensorDyn::from(tensor)
4826 };
4827
4828 let rbga = load_bytes_to_tensor(
4829 1280,
4830 720,
4831 PixelFormat::Rgba,
4832 None,
4833 &edgefirst_bench::testdata::read("camera720p.rgba"),
4834 )?;
4835 compare_images_convert_to_rgb(&planar, &rbga, 0.98, function!());
4836
4837 Ok(())
4838 }
4839
4840 #[test]
4841 fn test_from_tensor_invalid_format() {
4842 assert!(PixelFormat::from_fourcc(u32::from_le_bytes(*b"TEST")).is_none());
4845 }
4846
4847 #[test]
4848 #[should_panic(expected = "Failed to save planar RGB image")]
4849 fn test_save_planar() {
4850 let planar_img = load_bytes_to_tensor(
4851 1280,
4852 720,
4853 PixelFormat::PlanarRgb,
4854 None,
4855 &edgefirst_bench::testdata::read("camera720p.8bps"),
4856 )
4857 .unwrap();
4858
4859 let save_path = "/tmp/planar_rgb.jpg";
4860 crate::save_jpeg(&planar_img, save_path, 90).expect("Failed to save planar RGB image");
4861 }
4862
4863 #[test]
4864 #[should_panic(expected = "Failed to save YUYV image")]
4865 fn test_save_yuyv() {
4866 let planar_img = load_bytes_to_tensor(
4867 1280,
4868 720,
4869 PixelFormat::Yuyv,
4870 None,
4871 &edgefirst_bench::testdata::read("camera720p.yuyv"),
4872 )
4873 .unwrap();
4874
4875 let save_path = "/tmp/yuyv.jpg";
4876 crate::save_jpeg(&planar_img, save_path, 90).expect("Failed to save YUYV image");
4877 }
4878
4879 #[test]
4880 fn test_rotation_angle() {
4881 assert_eq!(Rotation::from_degrees_clockwise(0), Rotation::None);
4882 assert_eq!(Rotation::from_degrees_clockwise(90), Rotation::Clockwise90);
4883 assert_eq!(Rotation::from_degrees_clockwise(180), Rotation::Rotate180);
4884 assert_eq!(
4885 Rotation::from_degrees_clockwise(270),
4886 Rotation::CounterClockwise90
4887 );
4888 assert_eq!(Rotation::from_degrees_clockwise(360), Rotation::None);
4889 assert_eq!(Rotation::from_degrees_clockwise(450), Rotation::Clockwise90);
4890 assert_eq!(Rotation::from_degrees_clockwise(540), Rotation::Rotate180);
4891 assert_eq!(
4892 Rotation::from_degrees_clockwise(630),
4893 Rotation::CounterClockwise90
4894 );
4895 }
4896
4897 #[test]
4898 #[should_panic(expected = "rotation angle is not a multiple of 90")]
4899 fn test_rotation_angle_panic() {
4900 Rotation::from_degrees_clockwise(361);
4901 }
4902
4903 #[test]
4904 fn test_disable_env_var() -> Result<(), Error> {
4905 let _lock = acquire_env_lock();
4908
4909 let _guard = EnvGuard::snapshot(&[
4912 "EDGEFIRST_FORCE_BACKEND",
4913 "EDGEFIRST_DISABLE_GL",
4914 "EDGEFIRST_DISABLE_G2D",
4915 "EDGEFIRST_DISABLE_CPU",
4916 ]);
4917
4918 unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") };
4921
4922 #[cfg(target_os = "linux")]
4923 {
4924 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
4925 let converter = ImageProcessor::new()?;
4926 assert!(converter.g2d.is_none());
4927 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_G2D") };
4928 }
4929
4930 #[cfg(any(target_os = "linux", target_os = "windows"))]
4931 #[cfg(feature = "opengl")]
4932 {
4933 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
4934 let converter = ImageProcessor::new()?;
4935 assert!(converter.opengl.is_none());
4936 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_GL") };
4937 }
4938
4939 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
4940 let converter = ImageProcessor::new()?;
4941 assert!(converter.cpu.is_none());
4942 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_CPU") };
4943
4944 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
4946 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
4947 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
4948 let mut converter = ImageProcessor::new()?;
4949
4950 let src = TensorDyn::image(
4951 1280,
4952 720,
4953 PixelFormat::Rgba,
4954 DType::U8,
4955 None,
4956 edgefirst_tensor::CpuAccess::ReadWrite,
4957 )?;
4958 let dst = TensorDyn::image(
4959 640,
4960 360,
4961 PixelFormat::Rgba,
4962 DType::U8,
4963 None,
4964 edgefirst_tensor::CpuAccess::ReadWrite,
4965 )?;
4966 let (result, _src, _dst) = convert_img(
4967 &mut converter,
4968 src,
4969 dst,
4970 Rotation::None,
4971 Flip::None,
4972 Crop::no_crop(),
4973 );
4974 assert!(matches!(result, Err(Error::NoConverter)));
4975 Ok(())
4977 }
4978
4979 #[test]
4980 fn test_unsupported_conversion() {
4981 let src = TensorDyn::image(
4982 1280,
4983 720,
4984 PixelFormat::Nv12,
4985 DType::U8,
4986 None,
4987 edgefirst_tensor::CpuAccess::ReadWrite,
4988 )
4989 .unwrap();
4990 let dst = TensorDyn::image(
4991 640,
4992 360,
4993 PixelFormat::Nv12,
4994 DType::U8,
4995 None,
4996 edgefirst_tensor::CpuAccess::ReadWrite,
4997 )
4998 .unwrap();
4999 let mut converter = ImageProcessor::new().unwrap();
5000 let (result, _src, _dst) = convert_img(
5001 &mut converter,
5002 src,
5003 dst,
5004 Rotation::None,
5005 Flip::None,
5006 Crop::no_crop(),
5007 );
5008 log::debug!("result: {:?}", result);
5009 assert!(matches!(
5010 result,
5011 Err(Error::NotSupported(e)) if e.starts_with("Conversion from NV12 to NV12")
5012 ));
5013 }
5014
5015 #[test]
5016 fn test_load_grey() {
5017 let grey_img = crate::load_image_test_helper(
5021 &edgefirst_bench::testdata::read("grey.jpg"),
5022 Some(PixelFormat::Rgba),
5023 None,
5024 )
5025 .unwrap();
5026 assert_eq!(grey_img.width(), Some(1024));
5027 assert_eq!(grey_img.height(), Some(681));
5028
5029 let grey_but_rgb = crate::load_image_test_helper(
5035 &edgefirst_bench::testdata::read("grey-rgb.jpg"),
5036 Some(PixelFormat::Rgba),
5037 None,
5038 )
5039 .expect("odd-height colour JPEG should decode to NV12 and convert to RGBA");
5040 assert_eq!(grey_but_rgb.width(), Some(1024));
5041 assert_eq!(grey_but_rgb.height(), Some(681));
5042 }
5043
5044 #[test]
5045 fn test_new_nv12() {
5046 let nv12 = TensorDyn::image(
5047 1280,
5048 720,
5049 PixelFormat::Nv12,
5050 DType::U8,
5051 None,
5052 edgefirst_tensor::CpuAccess::ReadWrite,
5053 )
5054 .unwrap();
5055 assert_eq!(nv12.height(), Some(720));
5056 assert_eq!(nv12.width(), Some(1280));
5057 assert_eq!(nv12.format().unwrap(), PixelFormat::Nv12);
5058 assert_eq!(nv12.format().unwrap().channels(), 1);
5060 assert!(nv12.format().is_some_and(
5061 |f| f.layout() == PixelLayout::Planar || f.layout() == PixelLayout::SemiPlanar
5062 ))
5063 }
5064
5065 #[test]
5066 #[cfg(any(target_os = "linux", target_os = "windows"))]
5067 fn test_new_image_converter() {
5068 let dst_width = 640;
5069 let dst_height = 360;
5070 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5071 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5072
5073 let mut converter = ImageProcessor::new().unwrap();
5074 let converter_dst = converter
5075 .create_image(
5076 dst_width,
5077 dst_height,
5078 PixelFormat::Rgba,
5079 DType::U8,
5080 None,
5081 edgefirst_tensor::CpuAccess::ReadWrite,
5082 )
5083 .unwrap();
5084 let (result, src, converter_dst) = convert_img(
5085 &mut converter,
5086 src,
5087 converter_dst,
5088 Rotation::None,
5089 Flip::None,
5090 Crop::no_crop(),
5091 );
5092 result.unwrap();
5093
5094 let cpu_dst = TensorDyn::image(
5095 dst_width,
5096 dst_height,
5097 PixelFormat::Rgba,
5098 DType::U8,
5099 None,
5100 edgefirst_tensor::CpuAccess::ReadWrite,
5101 )
5102 .unwrap();
5103 let mut cpu_converter = CPUProcessor::new();
5104 let (result, _src, cpu_dst) = convert_img(
5105 &mut cpu_converter,
5106 src,
5107 cpu_dst,
5108 Rotation::None,
5109 Flip::None,
5110 Crop::no_crop(),
5111 );
5112 result.unwrap();
5113
5114 compare_images(&converter_dst, &cpu_dst, 0.98, function!());
5115 }
5116
5117 #[test]
5118 #[cfg(any(target_os = "linux", target_os = "windows"))]
5119 fn test_create_image_dtype_i8() {
5120 let mut converter = ImageProcessor::new().unwrap();
5121
5122 let dst = converter
5124 .create_image(
5125 320,
5126 240,
5127 PixelFormat::Rgb,
5128 DType::I8,
5129 None,
5130 edgefirst_tensor::CpuAccess::ReadWrite,
5131 )
5132 .unwrap();
5133 assert_eq!(dst.dtype(), DType::I8);
5134 assert!(dst.width() == Some(320));
5135 assert!(dst.height() == Some(240));
5136 assert_eq!(dst.format(), Some(PixelFormat::Rgb));
5137
5138 let dst_u8 = converter
5140 .create_image(
5141 320,
5142 240,
5143 PixelFormat::Rgb,
5144 DType::U8,
5145 None,
5146 edgefirst_tensor::CpuAccess::ReadWrite,
5147 )
5148 .unwrap();
5149 assert_eq!(dst_u8.dtype(), DType::U8);
5150
5151 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5153 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5154 let mut dst_i8 = converter
5155 .create_image(
5156 320,
5157 240,
5158 PixelFormat::Rgb,
5159 DType::I8,
5160 None,
5161 edgefirst_tensor::CpuAccess::ReadWrite,
5162 )
5163 .unwrap();
5164 converter.convert(&src, &mut dst_i8, None).unwrap();
5165 }
5166
5167 #[test]
5168 #[cfg(target_os = "linux")]
5169 fn test_create_image_nv12_dma_non_aligned_width() {
5170 let converter = ImageProcessor::new().unwrap();
5176
5177 let Some(img) = crate::test_support::dma_allocated(
5179 converter.create_image(
5180 100,
5181 64,
5182 PixelFormat::Nv12,
5183 DType::U8,
5184 Some(TensorMemory::DmaBuf),
5185 edgefirst_tensor::CpuAccess::ReadWrite,
5186 ),
5187 "create_image NV12 DMA non-aligned width",
5188 ) else {
5189 return;
5190 };
5191 assert_eq!(img.width(), Some(100));
5192 assert_eq!(img.height(), Some(64));
5193 assert_eq!(img.format(), Some(PixelFormat::Nv12));
5194 if let Some(stride) = img.row_stride() {
5195 assert!(
5196 stride >= 100,
5197 "NV12 row_stride {stride} must be >= the logical width (100)",
5198 );
5199 }
5200 }
5201
5202 #[test]
5203 #[ignore] fn test_crop_skip() {
5207 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5208 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5209
5210 let mut converter = ImageProcessor::new().unwrap();
5211 let converter_dst = converter
5212 .create_image(
5213 1280,
5214 720,
5215 PixelFormat::Rgba,
5216 DType::U8,
5217 None,
5218 edgefirst_tensor::CpuAccess::ReadWrite,
5219 )
5220 .unwrap();
5221 let crop = Crop::new().with_source(Some(Region::new(0, 0, 640, 640)));
5222 let (result, src, converter_dst) = convert_img(
5223 &mut converter,
5224 src,
5225 converter_dst,
5226 Rotation::None,
5227 Flip::None,
5228 crop,
5229 );
5230 result.unwrap();
5231
5232 let cpu_dst = TensorDyn::image(
5233 1280,
5234 720,
5235 PixelFormat::Rgba,
5236 DType::U8,
5237 None,
5238 edgefirst_tensor::CpuAccess::ReadWrite,
5239 )
5240 .unwrap();
5241 let mut cpu_converter = CPUProcessor::new();
5242 let (result, _src, cpu_dst) = convert_img(
5243 &mut cpu_converter,
5244 src,
5245 cpu_dst,
5246 Rotation::None,
5247 Flip::None,
5248 crop,
5249 );
5250 result.unwrap();
5251
5252 compare_images(&converter_dst, &cpu_dst, 0.99999, function!());
5253 }
5254
5255 #[test]
5256 fn test_invalid_pixel_format() {
5257 assert!(PixelFormat::from_fourcc(u32::from_le_bytes(*b"TEST")).is_none());
5260 }
5261
5262 #[cfg(target_os = "linux")]
5264 static G2D_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
5265
5266 #[cfg(target_os = "linux")]
5267 fn is_g2d_available() -> bool {
5268 crate::test_support::g2d_required_or(
5269 *G2D_AVAILABLE.get_or_init(|| G2DProcessor::new().is_ok()),
5270 )
5271 }
5272
5273 #[cfg(any(
5279 target_os = "linux",
5280 target_os = "macos",
5281 target_os = "ios",
5282 target_os = "android",
5283 target_os = "windows"
5284 ))]
5285 #[cfg(feature = "opengl")]
5286 static GL_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
5287
5288 #[cfg(any(
5289 target_os = "linux",
5290 target_os = "macos",
5291 target_os = "ios",
5292 target_os = "android",
5293 target_os = "windows"
5294 ))]
5295 #[cfg(feature = "opengl")]
5296 fn is_opengl_available() -> bool {
5298 *GL_AVAILABLE.get_or_init(|| GLProcessorThreaded::new(None).is_ok())
5299 }
5300
5301 #[test]
5313 #[cfg(feature = "opengl")]
5314 fn gl_backend_available_canary() {
5315 let require_gl = std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1");
5316 if !require_gl {
5317 crate::test_support::report_skip(&format!(
5318 "{} — HAL_TEST_REQUIRE_GL is not set to 1",
5319 function!()
5320 ));
5321 return;
5322 }
5323 #[cfg(target_os = "macos")]
5324 if std::env::var_os("HAL_TEST_ALLOW_DLOPEN_ANGLE").is_none() {
5325 crate::test_support::report_skip(&format!(
5326 "{} — ANGLE dlopen gate closed (coverage pass 1)",
5327 function!()
5328 ));
5329 return;
5330 }
5331 GLProcessorThreaded::new(None).expect(
5332 "HAL_TEST_REQUIRE_GL=1 but the GL backend failed to initialize — \
5333 check the ANGLE install/re-sign step and binary entitlements \
5334 (macOS) or the EGL stack (Linux)",
5335 );
5336 }
5337
5338 #[test]
5339 fn test_load_jpeg_with_exif() {
5340 use edgefirst_codec::peek_info;
5341
5342 let file = edgefirst_bench::testdata::read("zidane_rotated_exif.jpg").to_vec();
5347 let info = peek_info(&file).unwrap();
5348 assert_eq!(info.rotation_degrees, 90);
5349 assert!(!info.flip_horizontal);
5350
5351 let loaded = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5352 assert_eq!(loaded.width(), Some(1280));
5354 assert_eq!(loaded.height(), Some(720));
5355
5356 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5359 let cpu_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5360
5361 let rotation = Rotation::from_degrees_clockwise(info.rotation_degrees as usize);
5362 let (dst_width, dst_height) = (cpu_src.height().unwrap(), cpu_src.width().unwrap());
5363
5364 let cpu_dst = TensorDyn::image(
5365 dst_width,
5366 dst_height,
5367 PixelFormat::Rgba,
5368 DType::U8,
5369 None,
5370 edgefirst_tensor::CpuAccess::ReadWrite,
5371 )
5372 .unwrap();
5373 let mut cpu_converter = CPUProcessor::new();
5374
5375 let loaded_rotated = TensorDyn::image(
5378 dst_width,
5379 dst_height,
5380 PixelFormat::Rgba,
5381 DType::U8,
5382 None,
5383 edgefirst_tensor::CpuAccess::ReadWrite,
5384 )
5385 .unwrap();
5386 let (r0, _loaded, loaded_rotated) = convert_img(
5387 &mut cpu_converter,
5388 loaded,
5389 loaded_rotated,
5390 rotation,
5391 Flip::None,
5392 Crop::no_crop(),
5393 );
5394 r0.unwrap();
5395
5396 let (result, _cpu_src, cpu_dst) = convert_img(
5397 &mut cpu_converter,
5398 cpu_src,
5399 cpu_dst,
5400 rotation,
5401 Flip::None,
5402 Crop::no_crop(),
5403 );
5404 result.unwrap();
5405
5406 compare_images(&loaded_rotated, &cpu_dst, 0.98, function!());
5407 }
5408
5409 #[test]
5410 fn test_load_png_with_exif() {
5411 use edgefirst_codec::peek_info;
5412
5413 let file = edgefirst_bench::testdata::read("zidane_rotated_exif_180.png").to_vec();
5416 let info = peek_info(&file).unwrap();
5417 assert_eq!(info.rotation_degrees, 180);
5418 assert!(!info.flip_horizontal);
5419
5420 let loaded = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5421 assert_eq!(loaded.height(), Some(720));
5423 assert_eq!(loaded.width(), Some(1280));
5424
5425 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5430 let cpu_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5431
5432 let rotation = Rotation::from_degrees_clockwise(info.rotation_degrees as usize);
5433 let cpu_dst = TensorDyn::image(
5434 1280,
5435 720,
5436 PixelFormat::Rgba,
5437 DType::U8,
5438 None,
5439 edgefirst_tensor::CpuAccess::ReadWrite,
5440 )
5441 .unwrap();
5442 let mut cpu_converter = CPUProcessor::new();
5443
5444 let (result, _cpu_src, cpu_dst) = convert_img(
5445 &mut cpu_converter,
5446 cpu_src,
5447 cpu_dst,
5448 rotation,
5449 Flip::None,
5450 Crop::no_crop(),
5451 );
5452 result.unwrap();
5453
5454 let loaded_rotated = TensorDyn::image(
5457 1280,
5458 720,
5459 PixelFormat::Rgba,
5460 DType::U8,
5461 None,
5462 edgefirst_tensor::CpuAccess::ReadWrite,
5463 )
5464 .unwrap();
5465 let (r0, _loaded, loaded_rotated) = convert_img(
5466 &mut cpu_converter,
5467 loaded,
5468 loaded_rotated,
5469 rotation,
5470 Flip::None,
5471 Crop::no_crop(),
5472 );
5473 r0.unwrap();
5474
5475 compare_images(&loaded_rotated, &cpu_dst, 0.95, function!());
5480 }
5481
5482 #[cfg(target_os = "linux")]
5488 fn make_rgb_jpeg(width: u32, height: u32) -> Vec<u8> {
5489 let mut bytes = Vec::with_capacity((width * height * 3) as usize);
5490 for y in 0..height {
5491 for x in 0..width {
5492 bytes.push(((x + y) & 0xFF) as u8);
5493 bytes.push(((x.wrapping_mul(3)) & 0xFF) as u8);
5494 bytes.push(((y.wrapping_mul(5)) & 0xFF) as u8);
5495 }
5496 }
5497 let mut out = Vec::new();
5498 let encoder = jpeg_encoder::Encoder::new(&mut out, 85);
5499 encoder
5500 .encode(
5501 &bytes,
5502 width as u16,
5503 height as u16,
5504 jpeg_encoder::ColorType::Rgb,
5505 )
5506 .expect("jpeg-encoder must succeed on trivial input");
5507 out
5508 }
5509
5510 #[test]
5519 #[cfg(target_os = "linux")]
5520 #[cfg(feature = "opengl")]
5521 fn test_convert_rgba_non_4_aligned_width_end_to_end() {
5522 use crate::test_support::is_dma_available;
5523 if !is_dma_available() {
5524 crate::test_support::report_skip(
5525 "test_convert_rgba_non_4_aligned_width_end_to_end — DMA not available",
5526 );
5527 return;
5528 }
5529 let jpeg = make_rgb_jpeg(375, 333);
5533 let src_gl = crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), None).unwrap();
5534 assert_eq!(src_gl.width(), Some(375));
5535 let stride = src_gl.row_stride().unwrap();
5537 assert_eq!(stride, 1536, "expected padded pitch 1536, got {stride}");
5538
5539 let mut gl_proc = ImageProcessor::new().unwrap();
5541 let gl_dst = gl_proc
5542 .create_image(
5543 640,
5544 640,
5545 PixelFormat::Rgba,
5546 DType::U8,
5547 None,
5548 edgefirst_tensor::CpuAccess::ReadWrite,
5549 )
5550 .unwrap();
5551 let (r_gl, _src_gl, gl_dst) = convert_img(
5552 &mut gl_proc,
5553 src_gl,
5554 gl_dst,
5555 Rotation::None,
5556 Flip::None,
5557 Crop::no_crop(),
5558 );
5559 r_gl.expect("GL-backed convert must succeed for 375x333 Rgba src");
5560
5561 let src_cpu =
5566 crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), Some(TensorMemory::Mem))
5567 .unwrap();
5568 let mut cpu_proc = ImageProcessor::with_config(ImageProcessorConfig {
5569 backend: ComputeBackend::Cpu,
5570 ..Default::default()
5571 })
5572 .unwrap();
5573 let cpu_dst = TensorDyn::image(
5574 640,
5575 640,
5576 PixelFormat::Rgba,
5577 DType::U8,
5578 Some(TensorMemory::Mem),
5579 edgefirst_tensor::CpuAccess::ReadWrite,
5580 )
5581 .unwrap();
5582 let (r_cpu, _src_cpu, cpu_dst) = convert_img(
5583 &mut cpu_proc,
5584 src_cpu,
5585 cpu_dst,
5586 Rotation::None,
5587 Flip::None,
5588 Crop::no_crop(),
5589 );
5590 r_cpu.unwrap();
5591
5592 compare_images(&gl_dst, &cpu_dst, 0.95, function!());
5596 }
5597
5598 #[test]
5605 #[cfg(target_os = "linux")]
5606 fn test_load_jpeg_rgba_non_aligned_pitch_padded_dma() {
5607 use crate::test_support::is_dma_available;
5608 if !is_dma_available() {
5609 crate::test_support::report_skip(
5610 "test_load_jpeg_rgba_non_aligned_pitch_padded_dma — DMA not available",
5611 );
5612 return;
5613 }
5614 for &w in &[500u32, 612, 428] {
5618 let jpeg = make_rgb_jpeg(w, 333);
5619 let loaded =
5620 crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), None).unwrap();
5621 let natural = (w as usize) * 4;
5622 let aligned = crate::align_pitch_bytes_to_gpu_alignment(natural).unwrap();
5623 assert!(
5624 aligned > natural,
5625 "test sanity: width {w} should be unaligned"
5626 );
5627 let stride = loaded
5628 .row_stride()
5629 .expect("padded DMA path must set an explicit row_stride — regression if None");
5630 assert_eq!(
5631 stride, aligned,
5632 "width {w}: expected padded stride {aligned}, got {stride} \
5633 (regression: pitch-padding branch skipped?)"
5634 );
5635 let eff = loaded.effective_row_stride().unwrap();
5636 assert_eq!(
5637 eff, aligned,
5638 "effective_row_stride must match stored stride"
5639 );
5640 assert_eq!(loaded.width(), Some(w as usize));
5641 assert_eq!(loaded.height(), Some(333));
5642 }
5643 }
5644
5645 #[test]
5654 #[cfg(target_os = "linux")]
5655 fn test_padded_dma_pitch_for_respects_memory_choice() {
5656 use crate::test_support::is_dma_available;
5657 use edgefirst_tensor::TensorMemory;
5658
5659 let unaligned_w = 500;
5662
5663 assert_eq!(
5665 crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &Some(TensorMemory::Mem),),
5666 None,
5667 "Mem must never trigger DMA padding"
5668 );
5669 assert_eq!(
5670 crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &Some(TensorMemory::Shm),),
5671 None,
5672 "Shm must never trigger DMA padding"
5673 );
5674
5675 assert_eq!(
5680 crate::padded_dma_pitch_for(
5681 PixelFormat::Rgba,
5682 unaligned_w,
5683 &Some(TensorMemory::DmaBuf),
5684 ),
5685 Some(2048),
5686 "explicit Dma must pad regardless of runtime DMA availability"
5687 );
5688
5689 let none_result = crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &None);
5693 if is_dma_available() {
5694 assert_eq!(
5695 none_result,
5696 Some(2048),
5697 "memory=None + DMA available → pad (will route through DMA)"
5698 );
5699 } else {
5700 assert_eq!(
5701 none_result, None,
5702 "memory=None + DMA unavailable → must NOT pad (would force \
5703 image_with_stride into a DMA-only allocation that fails). \
5704 Regression: padded_dma_pitch_for ignored is_dma_available()."
5705 );
5706 }
5707 }
5708
5709 fn make_grey_png(width: u32, height: u32) -> Vec<u8> {
5713 let mut bytes = Vec::with_capacity((width * height) as usize);
5714 for y in 0..height {
5715 for x in 0..width {
5716 bytes.push(((x + y) & 0xFF) as u8);
5717 }
5718 }
5719 let img = image::GrayImage::from_vec(width, height, bytes).unwrap();
5720 let mut buf = Vec::new();
5721 img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)
5722 .unwrap();
5723 buf
5724 }
5725
5726 #[test]
5731 #[cfg(target_os = "linux")]
5732 fn test_load_png_grey_misaligned_width_dma() {
5733 use crate::test_support::is_dma_available;
5734 if !is_dma_available() {
5735 crate::test_support::report_skip(
5736 "test_load_png_grey_misaligned_width_dma — DMA not available",
5737 );
5738 return;
5739 }
5740 let png = make_grey_png(612, 388);
5741 let loaded = crate::load_image_test_helper(&png, Some(PixelFormat::Grey), None).unwrap();
5742 assert_eq!(loaded.width(), Some(612));
5743 assert_eq!(loaded.height(), Some(388));
5744 assert_eq!(loaded.format(), Some(PixelFormat::Grey));
5745
5746 let map = loaded.as_typed::<u8>().unwrap().map().unwrap();
5749 let stride = loaded.row_stride().unwrap_or(612);
5750 assert!(stride >= 612);
5751 let bytes: &[u8] = ↦
5752 for y in 0..388usize {
5753 for x in 0..612usize {
5754 let expected = ((x + y) & 0xFF) as u8;
5755 let got = bytes[y * stride + x];
5756 assert_eq!(
5757 got, expected,
5758 "grey png mismatch at ({x},{y}): got {got} expected {expected}"
5759 );
5760 }
5761 }
5762 }
5763
5764 #[test]
5768 fn test_load_png_grey_mem() {
5769 use edgefirst_tensor::TensorMemory;
5770 let png = make_grey_png(612, 100);
5771 let loaded =
5772 crate::load_image_test_helper(&png, Some(PixelFormat::Grey), Some(TensorMemory::Mem))
5773 .unwrap();
5774 assert_eq!(loaded.width(), Some(612));
5775 assert_eq!(loaded.height(), Some(100));
5776 assert_eq!(loaded.format(), Some(PixelFormat::Grey));
5777 let map = loaded.as_typed::<u8>().unwrap().map().unwrap();
5778 let bytes: &[u8] = ↦
5779 assert_eq!(bytes.len(), 612 * 100);
5781 for y in 0..100 {
5782 for x in 0..612 {
5783 assert_eq!(bytes[y * 612 + x], ((x + y) & 0xFF) as u8);
5784 }
5785 }
5786 }
5787
5788 #[test]
5792 fn test_load_png_grey_to_rgb_mem() {
5793 use edgefirst_tensor::TensorMemory;
5794 let png = make_grey_png(620, 240);
5795 let loaded =
5796 crate::load_image_test_helper(&png, Some(PixelFormat::Rgb), Some(TensorMemory::Mem))
5797 .unwrap();
5798 assert_eq!(loaded.width(), Some(620));
5799 assert_eq!(loaded.height(), Some(240));
5800 assert_eq!(loaded.format(), Some(PixelFormat::Rgb));
5801
5802 let map = loaded.as_typed::<u8>().unwrap().map().unwrap();
5804 let bytes: &[u8] = ↦
5805 for (x, y) in [(0usize, 0usize), (100, 50), (619, 239)] {
5806 let expected = ((x + y) & 0xFF) as u8;
5807 let off = (y * 620 + x) * 3;
5808 assert_eq!(bytes[off], expected, "R@{x},{y}");
5809 assert_eq!(bytes[off + 1], expected, "G@{x},{y}");
5810 assert_eq!(bytes[off + 2], expected, "B@{x},{y}");
5811 }
5812 }
5813
5814 #[test]
5815 #[cfg(target_os = "linux")]
5816 fn test_g2d_resize() {
5817 if !is_g2d_available() {
5818 crate::test_support::report_skip(
5819 "test_g2d_resize - G2D library (libg2d.so.2) not available",
5820 );
5821 return;
5822 }
5823 if !is_dma_available() {
5824 crate::test_support::report_skip("test_g2d_resize - DMA memory allocation not available (permission denied or no DMA-BUF support)");
5825 return;
5826 }
5827
5828 let dst_width = 640;
5829 let dst_height = 360;
5830 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5831 let src = crate::load_image_test_helper(
5832 &file,
5833 Some(PixelFormat::Rgba),
5834 Some(TensorMemory::DmaBuf),
5835 )
5836 .unwrap();
5837
5838 let g2d_dst = TensorDyn::image(
5839 dst_width,
5840 dst_height,
5841 PixelFormat::Rgba,
5842 DType::U8,
5843 Some(TensorMemory::DmaBuf),
5844 edgefirst_tensor::CpuAccess::ReadWrite,
5845 )
5846 .unwrap();
5847 let mut g2d_converter = G2DProcessor::new().unwrap();
5848 let (result, src, g2d_dst) = convert_img(
5849 &mut g2d_converter,
5850 src,
5851 g2d_dst,
5852 Rotation::None,
5853 Flip::None,
5854 Crop::no_crop(),
5855 );
5856 result.unwrap();
5857
5858 let cpu_dst = TensorDyn::image(
5859 dst_width,
5860 dst_height,
5861 PixelFormat::Rgba,
5862 DType::U8,
5863 None,
5864 edgefirst_tensor::CpuAccess::ReadWrite,
5865 )
5866 .unwrap();
5867 let mut cpu_converter = CPUProcessor::new();
5868 let (result, _src, cpu_dst) = convert_img(
5869 &mut cpu_converter,
5870 src,
5871 cpu_dst,
5872 Rotation::None,
5873 Flip::None,
5874 Crop::no_crop(),
5875 );
5876 result.unwrap();
5877
5878 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
5884 }
5885
5886 #[test]
5887 #[cfg(any(
5888 target_os = "linux",
5889 target_os = "macos",
5890 target_os = "ios",
5891 target_os = "android",
5892 target_os = "windows"
5893 ))]
5894 #[cfg(feature = "opengl")]
5895 fn test_opengl_resize() {
5896 if !is_opengl_available() {
5897 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
5898 return;
5899 }
5900
5901 let dst_width = 640;
5902 let dst_height = 360;
5903 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5904 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5905
5906 let cpu_dst = TensorDyn::image(
5907 dst_width,
5908 dst_height,
5909 PixelFormat::Rgba,
5910 DType::U8,
5911 None,
5912 edgefirst_tensor::CpuAccess::ReadWrite,
5913 )
5914 .unwrap();
5915 let mut cpu_converter = CPUProcessor::new();
5916 let (result, src, cpu_dst) = convert_img(
5917 &mut cpu_converter,
5918 src,
5919 cpu_dst,
5920 Rotation::None,
5921 Flip::None,
5922 Crop::no_crop(),
5923 );
5924 result.unwrap();
5925
5926 let mut src = src;
5927 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5928
5929 for _ in 0..5 {
5930 let gl_dst = TensorDyn::image(
5931 dst_width,
5932 dst_height,
5933 PixelFormat::Rgba,
5934 DType::U8,
5935 None,
5936 edgefirst_tensor::CpuAccess::ReadWrite,
5937 )
5938 .unwrap();
5939 let (result, src_back, gl_dst) = convert_img(
5940 &mut gl_converter,
5941 src,
5942 gl_dst,
5943 Rotation::None,
5944 Flip::None,
5945 Crop::no_crop(),
5946 );
5947 result.unwrap();
5948 src = src_back;
5949
5950 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5951 }
5952 }
5953
5954 #[test]
5955 #[cfg(any(
5956 target_os = "linux",
5957 target_os = "macos",
5958 target_os = "ios",
5959 target_os = "android",
5960 target_os = "windows"
5961 ))]
5962 #[cfg(feature = "opengl")]
5963 fn test_opengl_10_threads() {
5964 if !is_opengl_available() {
5965 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
5966 return;
5967 }
5968
5969 let handles: Vec<_> = (0..10)
5970 .map(|i| {
5971 std::thread::Builder::new()
5972 .name(format!("Thread {i}"))
5973 .spawn(test_opengl_resize)
5974 .unwrap()
5975 })
5976 .collect();
5977 handles.into_iter().for_each(|h| {
5978 if let Err(e) = h.join() {
5979 std::panic::resume_unwind(e)
5980 }
5981 });
5982 }
5983
5984 #[test]
5985 #[cfg(any(
5986 target_os = "linux",
5987 target_os = "macos",
5988 target_os = "ios",
5989 target_os = "android",
5990 target_os = "windows"
5991 ))]
5992 #[cfg(feature = "opengl")]
5993 fn test_opengl_grey() {
5994 if !is_opengl_available() {
5995 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
5996 return;
5997 }
5998
5999 let img = crate::load_image_test_helper(
6000 &edgefirst_bench::testdata::read("grey.jpg"),
6001 Some(PixelFormat::Grey),
6002 None,
6003 )
6004 .unwrap();
6005
6006 let gl_dst = TensorDyn::image(
6007 640,
6008 640,
6009 PixelFormat::Grey,
6010 DType::U8,
6011 None,
6012 edgefirst_tensor::CpuAccess::ReadWrite,
6013 )
6014 .unwrap();
6015 let cpu_dst = TensorDyn::image(
6016 640,
6017 640,
6018 PixelFormat::Grey,
6019 DType::U8,
6020 None,
6021 edgefirst_tensor::CpuAccess::ReadWrite,
6022 )
6023 .unwrap();
6024
6025 let mut converter = CPUProcessor::new();
6026
6027 let (result, img, cpu_dst) = convert_img(
6028 &mut converter,
6029 img,
6030 cpu_dst,
6031 Rotation::None,
6032 Flip::None,
6033 Crop::no_crop(),
6034 );
6035 result.unwrap();
6036
6037 let mut gl = GLProcessorThreaded::new(None).unwrap();
6038 let (result, _img, gl_dst) = convert_img(
6039 &mut gl,
6040 img,
6041 gl_dst,
6042 Rotation::None,
6043 Flip::None,
6044 Crop::no_crop(),
6045 );
6046 result.unwrap();
6047
6048 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
6049 }
6050
6051 #[test]
6052 #[cfg(target_os = "linux")]
6053 fn test_g2d_src_crop() {
6054 if !is_g2d_available() {
6055 crate::test_support::report_skip(
6056 "test_g2d_src_crop - G2D library (libg2d.so.2) not available",
6057 );
6058 return;
6059 }
6060 if !is_dma_available() {
6061 crate::test_support::report_skip("test_g2d_src_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)");
6062 return;
6063 }
6064
6065 let dst_width = 640;
6066 let dst_height = 640;
6067 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
6068 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
6069
6070 let cpu_dst = TensorDyn::image(
6071 dst_width,
6072 dst_height,
6073 PixelFormat::Rgba,
6074 DType::U8,
6075 None,
6076 edgefirst_tensor::CpuAccess::ReadWrite,
6077 )
6078 .unwrap();
6079 let mut cpu_converter = CPUProcessor::new();
6080 let crop = Crop::new().with_source(Some(Region::new(0, 0, 640, 360)));
6081 let (result, src, cpu_dst) = convert_img(
6082 &mut cpu_converter,
6083 src,
6084 cpu_dst,
6085 Rotation::None,
6086 Flip::None,
6087 crop,
6088 );
6089 result.unwrap();
6090
6091 let g2d_dst = TensorDyn::image(
6092 dst_width,
6093 dst_height,
6094 PixelFormat::Rgba,
6095 DType::U8,
6096 None,
6097 edgefirst_tensor::CpuAccess::ReadWrite,
6098 )
6099 .unwrap();
6100 let mut g2d_converter = G2DProcessor::new().unwrap();
6101 let (result, _src, g2d_dst) = convert_img(
6102 &mut g2d_converter,
6103 src,
6104 g2d_dst,
6105 Rotation::None,
6106 Flip::None,
6107 crop,
6108 );
6109 result.unwrap();
6110
6111 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
6117 }
6118
6119 #[test]
6120 #[cfg(target_os = "linux")]
6121 fn test_g2d_dst_crop() {
6122 if !is_g2d_available() {
6123 crate::test_support::report_skip(
6124 "test_g2d_dst_crop - G2D library (libg2d.so.2) not available",
6125 );
6126 return;
6127 }
6128 if !is_dma_available() {
6129 crate::test_support::report_skip("test_g2d_dst_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)");
6130 return;
6131 }
6132
6133 let dst_width = 640;
6134 let dst_height = 640;
6135 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
6136 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
6137
6138 let cpu_dst = TensorDyn::image(
6139 dst_width,
6140 dst_height,
6141 PixelFormat::Rgba,
6142 DType::U8,
6143 None,
6144 edgefirst_tensor::CpuAccess::ReadWrite,
6145 )
6146 .unwrap();
6147 let mut cpu_converter = CPUProcessor::new();
6148 let crop = Crop::new();
6149 let (result, src, cpu_dst) = convert_img(
6150 &mut cpu_converter,
6151 src,
6152 cpu_dst,
6153 Rotation::None,
6154 Flip::None,
6155 crop,
6156 );
6157 result.unwrap();
6158
6159 let g2d_dst = TensorDyn::image(
6160 dst_width,
6161 dst_height,
6162 PixelFormat::Rgba,
6163 DType::U8,
6164 None,
6165 edgefirst_tensor::CpuAccess::ReadWrite,
6166 )
6167 .unwrap();
6168 let mut g2d_converter = G2DProcessor::new().unwrap();
6169 let (result, _src, g2d_dst) = convert_img(
6170 &mut g2d_converter,
6171 src,
6172 g2d_dst,
6173 Rotation::None,
6174 Flip::None,
6175 crop,
6176 );
6177 result.unwrap();
6178
6179 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
6185 }
6186
6187 #[test]
6188 #[cfg(target_os = "linux")]
6189 fn test_g2d_all_rgba() {
6190 if !is_g2d_available() {
6191 crate::test_support::report_skip(
6192 "test_g2d_all_rgba - G2D library (libg2d.so.2) not available",
6193 );
6194 return;
6195 }
6196 if !is_dma_available() {
6197 crate::test_support::report_skip("test_g2d_all_rgba - DMA memory allocation not available (permission denied or no DMA-BUF support)");
6198 return;
6199 }
6200
6201 let dst_width = 640;
6202 let dst_height = 640;
6203 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
6204 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
6205 let src_dyn = src;
6206
6207 let mut cpu_dst = TensorDyn::image(
6208 dst_width,
6209 dst_height,
6210 PixelFormat::Rgba,
6211 DType::U8,
6212 None,
6213 edgefirst_tensor::CpuAccess::ReadWrite,
6214 )
6215 .unwrap();
6216 let mut cpu_converter = CPUProcessor::new();
6217 let mut g2d_dst = TensorDyn::image(
6218 dst_width,
6219 dst_height,
6220 PixelFormat::Rgba,
6221 DType::U8,
6222 None,
6223 edgefirst_tensor::CpuAccess::ReadWrite,
6224 )
6225 .unwrap();
6226 let mut g2d_converter = G2DProcessor::new().unwrap();
6227
6228 let crop = Crop::new().with_source(Some(Region::new(50, 120, 1024, 576)));
6229
6230 for rot in [
6231 Rotation::None,
6232 Rotation::Clockwise90,
6233 Rotation::Rotate180,
6234 Rotation::CounterClockwise90,
6235 ] {
6236 cpu_dst
6237 .as_typed::<u8>()
6238 .unwrap()
6239 .map()
6240 .unwrap()
6241 .as_mut_slice()
6242 .fill(114);
6243 g2d_dst
6244 .as_typed::<u8>()
6245 .unwrap()
6246 .map()
6247 .unwrap()
6248 .as_mut_slice()
6249 .fill(114);
6250 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
6251 let mut cpu_dst_dyn = cpu_dst;
6252 cpu_converter
6253 .convert(
6254 &src_dyn,
6255 &mut cpu_dst_dyn,
6256 Some(crate::ConvertOptions::new().crop(crop)),
6257 )
6258 .unwrap();
6259 cpu_dst = {
6260 let mut __t = cpu_dst_dyn.into_u8().unwrap();
6261 __t.set_format(PixelFormat::Rgba).unwrap();
6262 TensorDyn::from(__t)
6263 };
6264
6265 let mut g2d_dst_dyn = g2d_dst;
6266 g2d_converter
6267 .convert(
6268 &src_dyn,
6269 &mut g2d_dst_dyn,
6270 Some(crate::ConvertOptions::new().crop(crop)),
6271 )
6272 .unwrap();
6273 g2d_dst = {
6274 let mut __t = g2d_dst_dyn.into_u8().unwrap();
6275 __t.set_format(PixelFormat::Rgba).unwrap();
6276 TensorDyn::from(__t)
6277 };
6278
6279 compare_images(
6280 &g2d_dst,
6281 &cpu_dst,
6282 0.98,
6283 &format!("{} {:?} {:?}", function!(), rot, flip),
6284 );
6285 }
6286 }
6287 }
6288
6289 #[test]
6290 #[cfg(any(
6291 target_os = "linux",
6292 target_os = "macos",
6293 target_os = "ios",
6294 target_os = "android",
6295 target_os = "windows"
6296 ))]
6297 #[cfg(feature = "opengl")]
6298 fn test_opengl_src_crop() {
6299 if !is_opengl_available() {
6300 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
6301 return;
6302 }
6303
6304 let dst_width = 640;
6305 let dst_height = 360;
6306 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
6307 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
6308 let crop = Crop::new().with_source(Some(Region::new(320, 180, 1280 - 320, 720 - 180)));
6309
6310 let cpu_dst = TensorDyn::image(
6311 dst_width,
6312 dst_height,
6313 PixelFormat::Rgba,
6314 DType::U8,
6315 None,
6316 edgefirst_tensor::CpuAccess::ReadWrite,
6317 )
6318 .unwrap();
6319 let mut cpu_converter = CPUProcessor::new();
6320 let (result, src, cpu_dst) = convert_img(
6321 &mut cpu_converter,
6322 src,
6323 cpu_dst,
6324 Rotation::None,
6325 Flip::None,
6326 crop,
6327 );
6328 result.unwrap();
6329
6330 let gl_dst = TensorDyn::image(
6331 dst_width,
6332 dst_height,
6333 PixelFormat::Rgba,
6334 DType::U8,
6335 None,
6336 edgefirst_tensor::CpuAccess::ReadWrite,
6337 )
6338 .unwrap();
6339 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
6340 let (result, _src, gl_dst) = convert_img(
6341 &mut gl_converter,
6342 src,
6343 gl_dst,
6344 Rotation::None,
6345 Flip::None,
6346 crop,
6347 );
6348 result.unwrap();
6349
6350 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
6351 }
6352
6353 #[test]
6354 #[cfg(any(
6355 target_os = "linux",
6356 target_os = "macos",
6357 target_os = "ios",
6358 target_os = "android",
6359 target_os = "windows"
6360 ))]
6361 #[cfg(feature = "opengl")]
6362 fn test_opengl_dst_crop() {
6363 if !is_opengl_available() {
6364 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
6365 return;
6366 }
6367
6368 let dst_width = 640;
6369 let dst_height = 640;
6370 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
6371 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
6372
6373 let cpu_dst = TensorDyn::image(
6374 dst_width,
6375 dst_height,
6376 PixelFormat::Rgba,
6377 DType::U8,
6378 None,
6379 edgefirst_tensor::CpuAccess::ReadWrite,
6380 )
6381 .unwrap();
6382 let mut cpu_converter = CPUProcessor::new();
6383 let crop = Crop::new();
6384 let (result, src, cpu_dst) = convert_img(
6385 &mut cpu_converter,
6386 src,
6387 cpu_dst,
6388 Rotation::None,
6389 Flip::None,
6390 crop,
6391 );
6392 result.unwrap();
6393
6394 let gl_dst = TensorDyn::image(
6395 dst_width,
6396 dst_height,
6397 PixelFormat::Rgba,
6398 DType::U8,
6399 None,
6400 edgefirst_tensor::CpuAccess::ReadWrite,
6401 )
6402 .unwrap();
6403 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
6404 let (result, _src, gl_dst) = convert_img(
6405 &mut gl_converter,
6406 src,
6407 gl_dst,
6408 Rotation::None,
6409 Flip::None,
6410 crop,
6411 );
6412 result.unwrap();
6413
6414 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
6415 }
6416 #[cfg(all(target_os = "windows", feature = "opengl"))]
6420 fn synthetic_rgba(w: usize, h: usize) -> TensorDyn {
6421 let t = TensorDyn::image(
6422 w,
6423 h,
6424 PixelFormat::Rgba,
6425 DType::U8,
6426 Some(TensorMemory::Mem),
6427 edgefirst_tensor::CpuAccess::ReadWrite,
6428 )
6429 .unwrap();
6430 fill_synthetic_rgba(&t);
6431 t
6432 }
6433
6434 #[cfg(all(target_os = "windows", feature = "opengl"))]
6439 fn assert_pixels_identical(actual: &TensorDyn, expected: &TensorDyn, name: &str) {
6440 assert_eq!(actual.shape(), expected.shape(), "{name}: shapes differ");
6441 let flat = |t: &TensorDyn| {
6442 let typed = t.as_typed::<u8>().expect("u8 image");
6443 let mut out = vec![0u8; t.shape().iter().product::<usize>()];
6444 typed.copy_to_flat(&mut out).expect("compact the rows");
6445 out
6446 };
6447 let (a, e) = (flat(actual), flat(expected));
6448 let mismatches = a.iter().zip(&e).filter(|(x, y)| x != y).count();
6449 assert_eq!(
6450 mismatches,
6451 0,
6452 "{name}: {mismatches} of {} bytes differ (first rows: {:?} vs {:?})",
6453 a.len(),
6454 &a[..16.min(a.len())],
6455 &e[..16.min(e.len())]
6456 );
6457 }
6458
6459 #[cfg(all(target_os = "windows", feature = "opengl"))]
6462 fn fill_synthetic_rgba(t: &TensorDyn) {
6463 let w = t.width().unwrap();
6464 let h = t.height().unwrap();
6465 let bound = t.as_typed::<u8>().unwrap();
6466 let stride = bound.effective_row_stride().unwrap_or(w * 4);
6467 let mut m = bound.map().unwrap();
6468 let px = m.as_mut_slice();
6469 for y in 0..h {
6470 for x in 0..w {
6471 let o = y * stride + x * 4;
6472 px[o] = x as u8;
6473 px[o + 1] = y as u8;
6474 px[o + 2] = (x + y) as u8;
6475 px[o + 3] = 255;
6476 }
6477 }
6478 }
6479
6480 #[cfg(all(target_os = "windows", feature = "opengl"))]
6489 fn compare_float_images(a: &TensorDyn, b: &TensorDyn, tol: f64, what: &str) {
6490 assert_eq!(a.format(), b.format(), "{what}: pixel formats differ");
6491 assert_eq!(a.dtype(), b.dtype(), "{what}: dtypes differ");
6492 assert_eq!(a.shape(), b.shape(), "{what}: shapes differ");
6493 let values = |t: &TensorDyn| -> Vec<f64> {
6494 let elems: usize = t.shape().iter().product();
6495 match t.dtype() {
6496 DType::F32 => {
6497 let typed = t.as_typed::<f32>().expect("F32 image");
6498 let mut bytes = vec![0u8; elems * 4];
6499 typed
6500 .copy_to_flat(&mut bytes)
6501 .expect("compact the padded rows");
6502 bytes
6503 .as_chunks::<4>()
6504 .0
6505 .iter()
6506 .map(|c| f32::from_le_bytes(*c) as f64)
6507 .collect()
6508 }
6509 DType::F16 => {
6510 let typed = t.as_typed::<half::f16>().expect("F16 image");
6511 let mut bytes = vec![0u8; elems * 2];
6512 typed
6513 .copy_to_flat(&mut bytes)
6514 .expect("compact the padded rows");
6515 bytes
6516 .as_chunks::<2>()
6517 .0
6518 .iter()
6519 .map(|c| half::f16::from_le_bytes(*c).to_f64())
6520 .collect()
6521 }
6522 other => panic!("{what}: {other:?} is not a float dtype"),
6523 }
6524 };
6525 let (av, bv) = (values(a), values(b));
6526 assert_eq!(av.len(), bv.len(), "{what}: element counts differ");
6527 for (i, (x, y)) in av.iter().zip(bv.iter()).enumerate() {
6528 assert!(
6529 (x - y).abs() <= tol,
6530 "{what}: element {i} differs by {} (gpu {x}, cpu {y}, tol {tol})",
6531 (x - y).abs()
6532 );
6533 }
6534 }
6535
6536 #[test]
6559 #[cfg(all(target_os = "windows", feature = "opengl"))]
6560 fn windows_padded_float_pbo_rows_land_at_the_declared_stride() {
6561 if !is_opengl_available() {
6562 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
6563 return;
6564 }
6565 const W: usize = 64;
6566 const H: usize = 32;
6567 const POOL_W: usize = 96;
6568
6569 let mut gpu = ImageProcessor::new().unwrap();
6570 let support = gpu.supported_render_dtypes();
6571 let src = synthetic_rgba(W, H);
6572
6573 let cases: [(PixelFormat, DType, bool, &[usize]); 2] = [
6575 (
6576 PixelFormat::PlanarRgb,
6577 DType::F16,
6578 support.f16,
6579 &[W * 2, POOL_W * 2, W * 2 + 4],
6582 ),
6583 (
6584 PixelFormat::Rgb,
6585 DType::F32,
6586 support.f32,
6587 &[W * 3 * 4, POOL_W * 3 * 4],
6588 ),
6589 ];
6590
6591 for (fmt, dtype, supported, strides) in cases {
6592 if !supported {
6593 crate::test_support::report_skip(&format!(
6594 "{} - {fmt:?}/{dtype:?} not reported",
6595 function!()
6596 ));
6597 continue;
6598 }
6599 let Some(gl) = gpu.opengl.as_ref() else {
6600 crate::test_support::report_skip(&format!("{} - no GL processor", function!()));
6601 return;
6602 };
6603 let mut reference = gl
6605 .create_pbo_image_dtype(W, H, fmt, dtype)
6606 .unwrap_or_else(|e| panic!("{fmt:?}/{dtype:?}: tight reference PBO: {e:?}"));
6607 assert_eq!(
6608 reference.memory(),
6609 TensorMemory::Pbo,
6610 "{fmt:?}/{dtype:?}: the destination must be a PBO to reach this readback"
6611 );
6612 gpu.convert(&src, &mut reference, None)
6613 .unwrap_or_else(|e| panic!("{fmt:?}/{dtype:?}: tight convert failed: {e}"));
6614
6615 for &stride in strides {
6616 let label = format!("{fmt:?}/{dtype:?} at a {stride} B pitch");
6617 let mut padded = gpu
6620 .opengl
6621 .as_ref()
6622 .expect("GL processor")
6623 .create_pbo_image_dtype(POOL_W, H, fmt, dtype)
6624 .unwrap_or_else(|e| panic!("{label}: pool PBO: {e:?}"));
6625 padded
6626 .configure_image(W, H, fmt)
6627 .unwrap_or_else(|e| panic!("{label}: narrow the pool tensor: {e}"));
6628 padded
6629 .set_row_stride(stride)
6630 .unwrap_or_else(|e| panic!("{label}: declare the pitch: {e}"));
6631 assert_eq!(padded.effective_row_stride(), Some(stride), "{label}");
6632
6633 let before = gpu.convert_fallback_count();
6634 gpu.convert(&src, &mut padded, None)
6635 .unwrap_or_else(|e| panic!("{label}: padded convert failed: {e}"));
6636 assert_eq!(
6639 gpu.convert_fallback_count(),
6640 before,
6641 "{label}: fell back to the CPU backend, so the float PBO readback under test never ran"
6642 );
6643
6644 let tol = if dtype == DType::F16 {
6645 1.0 / 1024.0
6646 } else {
6647 1e-6
6648 };
6649 compare_float_images(&padded, &reference, tol, &label);
6650 }
6651 }
6652 }
6653
6654 #[test]
6664 #[cfg(all(target_os = "windows", feature = "opengl"))]
6665 fn windows_float_destinations_match_the_cpu_converter_for_every_layout() {
6666 if !is_opengl_available() {
6667 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
6668 return;
6669 }
6670 if !crate::test_support::is_gpu_buffer_available() {
6671 crate::test_support::report_skip(&format!(
6672 "{} - no zero-copy GPU buffers",
6673 function!()
6674 ));
6675 return;
6676 }
6677 let mut gpu = ImageProcessor::new().unwrap();
6678 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
6679 backend: ComputeBackend::Cpu,
6680 ..Default::default()
6681 })
6682 .unwrap();
6683 let support = gpu.supported_render_dtypes();
6684 eprintln!("{}: supported_render_dtypes = {support:?}", function!());
6685 let src = synthetic_rgba(640, 480);
6686 for (fmt, dtype, need) in [
6687 (PixelFormat::PlanarRgb, DType::F16, support.f16),
6688 (PixelFormat::PlanarRgb, DType::F32, support.f32),
6689 (PixelFormat::PlanarRgba, DType::F16, support.f16),
6690 (PixelFormat::PlanarRgba, DType::F32, support.f32),
6691 (PixelFormat::Rgb, DType::F16, support.f16),
6692 (PixelFormat::Rgb, DType::F32, support.f32),
6693 (PixelFormat::Rgba, DType::F16, support.f16),
6694 (PixelFormat::Rgba, DType::F32, support.f32),
6695 ] {
6696 if !need {
6697 crate::test_support::report_skip(&format!(
6698 "{} - {fmt:?}/{dtype:?} not reported",
6699 function!()
6700 ));
6701 continue;
6702 }
6703 let mut dst_gpu = gpu
6704 .create_image(
6705 640,
6706 480,
6707 fmt,
6708 dtype,
6709 Some(TensorMemory::DmaBuf),
6710 edgefirst_tensor::CpuAccess::ReadWrite,
6711 )
6712 .unwrap();
6713 assert_eq!(
6714 dst_gpu.memory(),
6715 TensorMemory::DmaBuf,
6716 "{fmt:?}/{dtype:?} must be a texture"
6717 );
6718 let mut dst_cpu = TensorDyn::image(
6719 640,
6720 480,
6721 fmt,
6722 dtype,
6723 Some(TensorMemory::Mem),
6724 edgefirst_tensor::CpuAccess::ReadWrite,
6725 )
6726 .unwrap();
6727 let before = gpu.convert_fallback_count();
6728 gpu.convert(&src, &mut dst_gpu, None).unwrap();
6729 cpu.convert(&src, &mut dst_cpu, None).unwrap();
6730 assert_eq!(
6731 gpu.convert_fallback_count(),
6732 before,
6733 "{fmt:?}/{dtype:?} fell back to CPU"
6734 );
6735 let tol = if dtype == DType::F16 {
6736 1.0 / 1024.0
6737 } else {
6738 1e-6
6739 };
6740 compare_float_images(&dst_gpu, &dst_cpu, tol, &format!("{fmt:?}/{dtype:?}"));
6741 }
6742 }
6743
6744 #[cfg(target_os = "windows")]
6747 #[link(name = "kernel32")]
6748 extern "system" {
6749 fn WaitForSingleObject(handle: *mut std::ffi::c_void, milliseconds: u32) -> u32;
6750 }
6751
6752 #[cfg(all(target_os = "windows", feature = "opengl"))]
6761 #[test]
6762 fn windows_deferred_batch_records_one_value_per_tile_and_submits_once() {
6763 use edgefirst_tensor::CpuAccess;
6764
6765 if !crate::test_support::is_gpu_buffer_available() {
6766 crate::test_support::report_skip(&format!(
6767 "{} - no zero-copy GPU buffers",
6768 function!()
6769 ));
6770 return;
6771 }
6772 let mut p = match ImageProcessor::new() {
6773 Ok(p) => p,
6774 Err(e) => {
6775 crate::test_support::report_skip(&format!(
6776 "{} - ImageProcessor init failed ({e:?})",
6777 function!()
6778 ));
6779 return;
6780 }
6781 };
6782 let (w, h, tiles) = (640usize, 480usize, 4usize);
6783 let src = synthetic_rgba(w, h);
6784 let mut dsts: Vec<TensorDyn> = (0..tiles)
6785 .map(|_| {
6786 p.create_image(w, h, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6787 .unwrap()
6788 })
6789 .collect();
6790 if dsts[0].memory() != TensorMemory::DmaBuf {
6791 crate::test_support::report_skip(&format!(
6792 "{} - destinations are not textures",
6793 function!()
6794 ));
6795 return;
6796 }
6797 for dst in &mut dsts {
6798 p.convert_deferred(&src, dst, None).unwrap();
6799 }
6800 let values: Vec<u64> = dsts
6801 .iter()
6802 .map(|d| {
6803 d.gpu_completion()
6804 .unwrap()
6805 .expect("each deferred convert records its own completion")
6806 .value
6807 })
6808 .collect();
6809 for pair in values.windows(2) {
6810 assert!(
6811 pair[1] > pair[0],
6812 "the batch allocated {values:?}, which is not one increasing value per tile"
6813 );
6814 }
6815 p.flush().unwrap();
6816 let dev = edgefirst_tensor::d3d11::device().unwrap();
6819 let last = *values.last().unwrap();
6820 let ev = dev.event_for(last).unwrap();
6821 assert_eq!(
6824 unsafe {
6825 WaitForSingleObject(
6826 std::os::windows::io::AsRawHandle::as_raw_handle(&ev) as _,
6827 5000,
6828 )
6829 },
6830 0,
6831 "the batch's newest value {last} never arrived after flush"
6832 );
6833
6834 let mut solo = p
6835 .create_image(w, h, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6836 .unwrap();
6837 p.convert(&src, &mut solo, None).unwrap();
6838 let flat = |img: &TensorDyn| -> Vec<u8> {
6842 let t = img.as_typed::<u8>().expect("u8 image");
6843 let mut out = vec![0u8; t.shape().iter().product::<usize>()];
6844 t.copy_to_flat(&mut out).expect("compact the padded rows");
6845 out
6846 };
6847 let want = flat(&solo);
6848 for (i, dst) in dsts.iter().enumerate() {
6849 assert_eq!(
6850 flat(dst),
6851 want,
6852 "tile {i} does not match a standalone convert"
6853 );
6854 }
6855 }
6856
6857 #[cfg(all(target_os = "windows", feature = "opengl"))]
6866 #[test]
6867 fn windows_dropping_a_processor_submits_an_open_batch() {
6868 use edgefirst_tensor::CpuAccess;
6869
6870 if !crate::test_support::is_gpu_buffer_available() {
6871 crate::test_support::report_skip(&format!(
6872 "{} - no zero-copy GPU buffers",
6873 function!()
6874 ));
6875 return;
6876 }
6877 let src = synthetic_rgba(320, 240);
6878 let recorded = {
6879 let mut p = match ImageProcessor::new() {
6880 Ok(p) => p,
6881 Err(e) => {
6882 crate::test_support::report_skip(&format!(
6883 "{} - init failed ({e:?})",
6884 function!()
6885 ));
6886 return;
6887 }
6888 };
6889 let mut dst = p
6890 .create_image(
6891 320,
6892 240,
6893 PixelFormat::Rgba,
6894 DType::U8,
6895 None,
6896 CpuAccess::Read,
6897 )
6898 .unwrap();
6899 if dst.memory() != TensorMemory::DmaBuf {
6900 crate::test_support::report_skip(&format!(
6901 "{} - destination is not a texture",
6902 function!()
6903 ));
6904 return;
6905 }
6906 p.convert_deferred(&src, &mut dst, None).unwrap();
6907 dst.gpu_completion()
6909 .unwrap()
6910 .expect("the deferred convert recorded a completion")
6911 .value
6912 };
6913
6914 let dev = edgefirst_tensor::d3d11::device().unwrap();
6915 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
6916 while dev.completed_value() < recorded {
6917 assert!(
6918 std::time::Instant::now() < deadline,
6919 "the dropped processor never submitted its batch: fence at {}, recorded {recorded}",
6920 dev.completed_value()
6921 );
6922 std::thread::yield_now();
6923 }
6924 }
6925
6926 #[cfg(all(target_os = "windows", feature = "opengl"))]
6931 #[test]
6932 fn windows_create_image_prefers_textures_and_convert_with_fence_returns_a_set_event() {
6933 use edgefirst_tensor::CpuAccess;
6934 use std::os::windows::io::AsRawHandle;
6935
6936 if !crate::test_support::is_gpu_buffer_available() {
6937 crate::test_support::report_skip(&format!(
6938 "{} - no zero-copy GPU buffers",
6939 function!()
6940 ));
6941 return;
6942 }
6943 let mut p = ImageProcessor::new().unwrap();
6944 let src = synthetic_rgba(320, 240);
6945 let mut dst = p
6946 .create_image(
6947 320,
6948 240,
6949 PixelFormat::Rgba,
6950 DType::U8,
6951 None,
6952 CpuAccess::Read,
6953 )
6954 .unwrap();
6955 if dst.memory() != TensorMemory::DmaBuf
6959 && !std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1")
6960 {
6961 crate::test_support::report_skip(&format!(
6962 "{} - create_image gave {:?}, not a texture (no GL backend)",
6963 function!(),
6964 dst.memory()
6965 ));
6966 return;
6967 }
6968 assert_eq!(
6969 dst.memory(),
6970 TensorMemory::DmaBuf,
6971 "create_image(None) must prefer a texture on Windows"
6972 );
6973 let before = dst.gpu_completion().unwrap().map(|c| c.value);
6976 let fence = p
6977 .convert_with_fence(&src, &mut dst, None)
6978 .unwrap()
6979 .expect("Windows exports a fence");
6980 let recorded = dst
6981 .gpu_completion()
6982 .unwrap()
6983 .expect("the convert recorded a completion")
6984 .value;
6985 assert!(
6986 before.is_none_or(|b| recorded > b),
6987 "the fenced convert recorded a newer value than {before:?}"
6988 );
6989 assert_eq!(
6994 edgefirst_tensor::d3d11::device().unwrap().last_signalled(),
6995 recorded,
6996 "a fenced convert must signal exactly once"
6997 );
6998 assert_eq!(
7003 unsafe { WaitForSingleObject(fence.as_raw_handle() as _, 5000) },
7004 0,
7005 "fence was not set within 5 seconds"
7006 );
7007 assert!(
7011 edgefirst_tensor::d3d11::device().unwrap().completed_value() >= recorded,
7012 "the event fired, so the recorded value {recorded} is complete"
7013 );
7014 assert_eq!(
7015 dst.gpu_completion().unwrap().map(|c| c.value),
7016 Some(recorded),
7017 "the destination still reports the value the event waited on"
7018 );
7019 let mut expected = TensorDyn::image(
7020 320,
7021 240,
7022 PixelFormat::Rgba,
7023 DType::U8,
7024 Some(TensorMemory::Mem),
7025 CpuAccess::ReadWrite,
7026 )
7027 .unwrap();
7028 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
7029 backend: ComputeBackend::Cpu,
7030 ..Default::default()
7031 })
7032 .unwrap();
7033 cpu.convert(&src, &mut expected, None).unwrap();
7034 compare_images(&dst, &expected, 0.0, "fenced convert");
7035 }
7036
7037 #[cfg(all(target_os = "windows", feature = "opengl"))]
7042 fn texture_or_skip(t: &TensorDyn, test: &str) -> bool {
7043 if t.memory() == TensorMemory::DmaBuf {
7044 return true;
7045 }
7046 if std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1") {
7047 panic!(
7048 "{test}: create_image(None) must prefer a texture on Windows, got {:?}",
7049 t.memory()
7050 );
7051 }
7052 crate::test_support::report_skip(&format!(
7053 "{test} - create_image gave {:?}, not a texture (no GL backend)",
7054 t.memory()
7055 ));
7056 false
7057 }
7058
7059 #[cfg(all(target_os = "windows", feature = "opengl"))]
7065 #[test]
7066 fn windows_draw_decoded_masks_into_a_texture_records_a_completion() {
7067 use edgefirst_tensor::{CpuAccess, Segmentation};
7068 use ndarray::Array3;
7069
7070 if !crate::test_support::is_gpu_buffer_available() {
7071 crate::test_support::report_skip(&format!(
7072 "{} - no zero-copy GPU buffers",
7073 function!()
7074 ));
7075 return;
7076 }
7077 let mut p = ImageProcessor::new().unwrap();
7078 let mut dst = p
7079 .create_image(64, 64, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
7080 .unwrap();
7081 if !texture_or_skip(&dst, function!()) {
7082 return;
7083 }
7084 assert!(
7085 dst.gpu_completion().unwrap().is_none(),
7086 "nothing has written this texture yet"
7087 );
7088
7089 let detect = DetectBox {
7090 bbox: [0.25, 0.25, 0.75, 0.75].into(),
7091 score: 0.99,
7092 label: 0,
7093 };
7094 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
7095 let seg = Segmentation {
7096 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
7097 .unwrap()
7098 .into(),
7099 xmin: 0.25,
7100 ymin: 0.25,
7101 xmax: 0.75,
7102 ymax: 0.75,
7103 };
7104 p.draw_decoded_masks(&mut dst, &[detect], &[seg], MaskOverlay::default())
7105 .expect("draw into a read-only texture");
7106
7107 let completion = dst
7108 .gpu_completion()
7109 .unwrap()
7110 .expect("the draw recorded no completion on dst");
7111 assert!(completion.value > 0, "completion value must be non-zero");
7112
7113 let mut flat = vec![0u8; 64 * 64 * 4];
7118 dst.as_typed::<u8>()
7119 .unwrap()
7120 .copy_to_flat(&mut flat)
7121 .expect("read the drawn texture");
7122 let px = |x: usize, y: usize| -> [u8; 4] {
7123 let o = (y * 64 + x) * 4;
7124 [flat[o], flat[o + 1], flat[o + 2], flat[o + 3]]
7125 };
7126 assert_eq!(px(2, 2), [0, 0, 0, 0], "corner (2,2) was not cleared");
7127 assert_ne!(px(32, 32), [0, 0, 0, 0], "centre (32,32) was not coloured");
7128 }
7129
7130 #[cfg(all(target_os = "windows", feature = "opengl"))]
7133 #[test]
7134 fn windows_draw_proto_masks_into_a_texture_records_a_completion() {
7135 use edgefirst_tensor::{CpuAccess, Tensor};
7136
7137 if !crate::test_support::is_gpu_buffer_available() {
7138 crate::test_support::report_skip(&format!(
7139 "{} - no zero-copy GPU buffers",
7140 function!()
7141 ));
7142 return;
7143 }
7144 let mut p = match with_force_backend(Some("opengl"), ImageProcessor::new) {
7148 Ok(p) => p,
7149 Err(e) if !std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1") => {
7150 crate::test_support::report_skip(&format!(
7151 "{} - no OpenGL backend: {e}",
7152 function!()
7153 ));
7154 return;
7155 }
7156 Err(e) => panic!("forced OpenGL backend unavailable: {e}"),
7157 };
7158 let mut dst = p
7159 .create_image(64, 64, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
7160 .unwrap();
7161 if !texture_or_skip(&dst, function!()) {
7162 return;
7163 }
7164
7165 let detect = DetectBox {
7166 bbox: [0.25, 0.25, 0.75, 0.75].into(),
7167 score: 0.99,
7168 label: 0,
7169 };
7170 let proto = ProtoData {
7173 mask_coefficients: TensorDyn::F32(
7174 Tensor::<f32>::from_slice(&[1.0_f32], &[1, 1]).unwrap(),
7175 ),
7176 protos: TensorDyn::F32(
7177 Tensor::<f32>::from_slice(&vec![1.0_f32; 8 * 8], &[8, 8, 1]).unwrap(),
7178 ),
7179 layout: ProtoLayout::Nhwc,
7180 };
7181 p.draw_proto_masks(&mut dst, &[detect], &proto, MaskOverlay::default())
7182 .expect("forced-GL proto draw into a read-only texture");
7183
7184 let completion = dst
7185 .gpu_completion()
7186 .unwrap()
7187 .expect("the proto draw recorded no completion on dst");
7188 assert!(completion.value > 0, "completion value must be non-zero");
7189 }
7190
7191 #[cfg(all(target_os = "windows", feature = "opengl"))]
7196 #[test]
7197 fn windows_draw_decoded_masks_composites_a_host_background_onto_a_texture() {
7198 use edgefirst_tensor::CpuAccess;
7199
7200 if !crate::test_support::is_gpu_buffer_available() {
7201 crate::test_support::report_skip(&format!(
7202 "{} - no zero-copy GPU buffers",
7203 function!()
7204 ));
7205 return;
7206 }
7207 let mut p = ImageProcessor::new().unwrap();
7208 let bg = synthetic_rgba(64, 48);
7209 let mut dst = p
7210 .create_image(64, 48, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
7211 .unwrap();
7212 if !texture_or_skip(&dst, function!()) {
7213 return;
7214 }
7215 p.draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::new().with_background(&bg))
7216 .expect("empty draw with a host background onto a texture");
7217 assert_pixels_identical(&dst, &bg, "host background onto a texture");
7218 assert!(dst.gpu_completion().unwrap().is_some());
7219 }
7220
7221 #[cfg(all(target_os = "windows", feature = "opengl"))]
7224 #[test]
7225 fn windows_draw_decoded_masks_composites_a_texture_background_onto_a_texture() {
7226 use edgefirst_tensor::CpuAccess;
7227
7228 if !crate::test_support::is_gpu_buffer_available() {
7229 crate::test_support::report_skip(&format!(
7230 "{} - no zero-copy GPU buffers",
7231 function!()
7232 ));
7233 return;
7234 }
7235 let mut p = ImageProcessor::new().unwrap();
7236 let expected = synthetic_rgba(64, 48);
7237 let bg = p
7238 .create_image(
7239 64,
7240 48,
7241 PixelFormat::Rgba,
7242 DType::U8,
7243 None,
7244 CpuAccess::ReadWrite,
7245 )
7246 .unwrap();
7247 if !texture_or_skip(&bg, function!()) {
7248 return;
7249 }
7250 fill_synthetic_rgba(&bg);
7251 let mut dst = p
7252 .create_image(64, 48, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
7253 .unwrap();
7254 assert_eq!(dst.memory(), TensorMemory::DmaBuf);
7255 p.draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::new().with_background(&bg))
7256 .expect("empty draw with a texture background onto a texture");
7257 assert_pixels_identical(&dst, &expected, "texture background onto a texture");
7258 assert!(dst.gpu_completion().unwrap().is_some());
7259 }
7260
7261 #[cfg(all(target_os = "windows", feature = "opengl"))]
7267 #[test]
7268 fn windows_draw_decoded_masks_into_a_texture_view_fills_only_its_band() {
7269 use edgefirst_tensor::CpuAccess;
7270
7271 if !crate::test_support::is_gpu_buffer_available() {
7272 crate::test_support::report_skip(&format!(
7273 "{} - no zero-copy GPU buffers",
7274 function!()
7275 ));
7276 return;
7277 }
7278 let (w, h) = (32usize, 64usize);
7279 let mut p = ImageProcessor::new().unwrap();
7280 let parent = p
7281 .create_image(
7282 w,
7283 h,
7284 PixelFormat::Rgba,
7285 DType::U8,
7286 None,
7287 CpuAccess::ReadWrite,
7288 )
7289 .unwrap();
7290 if !texture_or_skip(&parent, function!()) {
7291 return;
7292 }
7293 fill_synthetic_rgba(&parent);
7294
7295 let mut band = parent
7299 .view(edgefirst_tensor::Region::new(0, h / 2, w, h / 2))
7300 .unwrap();
7301 p.draw_decoded_masks(&mut band, &[], &[], MaskOverlay::default())
7302 .expect("empty draw into a texture view");
7303 drop(band);
7304
7305 let mut flat = vec![0u8; w * h * 4];
7306 parent
7307 .as_typed::<u8>()
7308 .unwrap()
7309 .copy_to_flat(&mut flat)
7310 .expect("read the parent texture");
7311 for y in 0..h {
7312 for x in 0..w {
7313 let o = (y * w + x) * 4;
7314 let got = [flat[o], flat[o + 1], flat[o + 2], flat[o + 3]];
7315 let want = if y < h / 2 {
7316 [x as u8, y as u8, (x + y) as u8, 255]
7317 } else {
7318 [0, 0, 0, 0]
7319 };
7320 assert_eq!(got, want, "pixel ({x},{y})");
7321 }
7322 }
7323
7324 let mut forced = with_force_backend(Some("opengl"), ImageProcessor::new).unwrap();
7327 let mut band = parent
7328 .view(edgefirst_tensor::Region::new(0, h / 2, w, h / 2))
7329 .unwrap();
7330 let err = forced
7331 .draw_decoded_masks(&mut band, &[], &[], MaskOverlay::default())
7332 .expect_err("forced GL must decline a view destination");
7333 assert!(
7334 matches!(err, Error::NotSupported(_)),
7335 "expected NotSupported, got {err:?}"
7336 );
7337 }
7338
7339 #[cfg(all(target_os = "windows", feature = "opengl"))]
7344 #[test]
7345 fn windows_class_colors_reach_the_cpu_backend_behind_a_declined_gl_draw() {
7346 use edgefirst_tensor::{CpuAccess, Segmentation};
7347 use ndarray::Array3;
7348
7349 if !crate::test_support::is_gpu_buffer_available() {
7350 crate::test_support::report_skip(&format!(
7351 "{} - no zero-copy GPU buffers",
7352 function!()
7353 ));
7354 return;
7355 }
7356 const CUSTOM: [u8; 4] = [7, 200, 13, 255];
7357 assert_ne!(
7358 CUSTOM[..3],
7359 DEFAULT_COLORS_U8[0][..3],
7360 "the custom colour must differ from the default palette's, or \
7361 this test cannot tell them apart"
7362 );
7363
7364 let (w, h) = (32usize, 64usize);
7365 let mut p = ImageProcessor::new().unwrap();
7366 let parent = p
7367 .create_image(
7368 w,
7369 h,
7370 PixelFormat::Rgba,
7371 DType::U8,
7372 None,
7373 CpuAccess::ReadWrite,
7374 )
7375 .unwrap();
7376 if parent.memory() != TensorMemory::DmaBuf
7381 && !std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1")
7382 {
7383 crate::test_support::report_skip(&format!(
7384 "{} - create_image gave {:?}, not a texture (no GL backend)",
7385 function!(),
7386 parent.memory()
7387 ));
7388 return;
7389 }
7390 p.set_class_colors(&[CUSTOM]).unwrap();
7391
7392 let detect = DetectBox {
7393 bbox: [0.25, 0.25, 0.75, 0.75].into(),
7394 score: 0.99,
7395 label: 0,
7396 };
7397 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
7398 let seg = Segmentation {
7399 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
7400 .unwrap()
7401 .into(),
7402 xmin: 0.25,
7403 ymin: 0.25,
7404 xmax: 0.75,
7405 ymax: 0.75,
7406 };
7407 let mut band = parent
7408 .view(edgefirst_tensor::Region::new(0, h / 2, w, h / 2))
7409 .unwrap();
7410 p.draw_decoded_masks(&mut band, &[detect], &[seg], MaskOverlay::default())
7411 .expect("draw into a texture view");
7412 drop(band);
7413
7414 let mut flat = vec![0u8; w * h * 4];
7415 parent
7416 .as_typed::<u8>()
7417 .unwrap()
7418 .copy_to_flat(&mut flat)
7419 .expect("read the parent texture");
7420 let o = ((h / 2 + h / 4) * w + w / 2) * 4;
7423 assert_eq!(
7424 &flat[o..o + 3],
7425 &CUSTOM[..3],
7426 "the CPU backend drew the default palette, so it never received \
7427 set_class_colors"
7428 );
7429 }
7430
7431 #[cfg(all(target_os = "windows", feature = "opengl"))]
7436 #[test]
7437 fn two_processors_alternate_on_texture_destinations_without_state_bleed() {
7438 use edgefirst_tensor::CpuAccess;
7439
7440 if !crate::test_support::is_gpu_buffer_available() {
7441 crate::test_support::report_skip(&format!(
7442 "{} - no zero-copy GPU buffers",
7443 function!()
7444 ));
7445 return;
7446 }
7447 let mut a = ImageProcessor::new().unwrap();
7448 let mut b = ImageProcessor::new().unwrap();
7449 let src_a = synthetic_rgba(512, 512);
7450 let src_b = synthetic_rgba(256, 256);
7451 let mut dst_a = a
7452 .create_image(
7453 512,
7454 512,
7455 PixelFormat::Rgba,
7456 DType::U8,
7457 None,
7458 CpuAccess::Read,
7459 )
7460 .unwrap();
7461 let mut dst_b = b
7462 .create_image(
7463 256,
7464 256,
7465 PixelFormat::Rgba,
7466 DType::U8,
7467 None,
7468 CpuAccess::Read,
7469 )
7470 .unwrap();
7471 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
7472 backend: ComputeBackend::Cpu,
7473 ..Default::default()
7474 })
7475 .unwrap();
7476 let mut ref_a = TensorDyn::image(
7477 512,
7478 512,
7479 PixelFormat::Rgba,
7480 DType::U8,
7481 Some(TensorMemory::Mem),
7482 CpuAccess::ReadWrite,
7483 )
7484 .unwrap();
7485 let mut ref_b = TensorDyn::image(
7486 256,
7487 256,
7488 PixelFormat::Rgba,
7489 DType::U8,
7490 Some(TensorMemory::Mem),
7491 CpuAccess::ReadWrite,
7492 )
7493 .unwrap();
7494 cpu.convert(&src_a, &mut ref_a, None).unwrap();
7495 cpu.convert(&src_b, &mut ref_b, None).unwrap();
7496 for _ in 0..200 {
7497 a.convert(&src_a, &mut dst_a, None).unwrap();
7498 b.convert(&src_b, &mut dst_b, None).unwrap();
7499 compare_images(&dst_a, &ref_a, 0.0, "processor A");
7500 compare_images(&dst_b, &ref_b, 0.0, "processor B");
7501 }
7502 }
7503
7504 #[test]
7505 #[cfg(any(
7506 target_os = "linux",
7507 target_os = "macos",
7508 target_os = "ios",
7509 target_os = "android",
7510 target_os = "windows"
7511 ))]
7512 #[cfg(feature = "opengl")]
7513 fn test_opengl_all_rgba() {
7514 if !is_opengl_available() {
7515 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
7516 return;
7517 }
7518
7519 let dst_width = 640;
7520 let dst_height = 640;
7521 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7522
7523 let mut cpu_converter = CPUProcessor::new();
7524
7525 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
7526
7527 let mut mem = vec![None, Some(TensorMemory::Mem)];
7531 if edgefirst_tensor::is_shm_available() {
7532 mem.push(Some(TensorMemory::Shm));
7533 }
7534 if crate::test_support::is_gpu_buffer_available() {
7535 mem.push(Some(TensorMemory::DmaBuf));
7536 } else {
7537 crate::test_support::report_skip(&format!(
7538 "{} - DmaBuf backing not exercised: no zero-copy GPU buffers",
7539 function!()
7540 ));
7541 }
7542 let crop = Crop::new().with_source(Some(Region::new(50, 120, 1024, 576)));
7543 for m in mem {
7544 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), m).unwrap();
7545 let src_dyn = src;
7546
7547 for rot in [
7548 Rotation::None,
7549 Rotation::Clockwise90,
7550 Rotation::Rotate180,
7551 Rotation::CounterClockwise90,
7552 ] {
7553 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
7554 let cpu_dst = TensorDyn::image(
7555 dst_width,
7556 dst_height,
7557 PixelFormat::Rgba,
7558 DType::U8,
7559 m,
7560 edgefirst_tensor::CpuAccess::ReadWrite,
7561 )
7562 .unwrap();
7563 let gl_dst = TensorDyn::image(
7564 dst_width,
7565 dst_height,
7566 PixelFormat::Rgba,
7567 DType::U8,
7568 m,
7569 edgefirst_tensor::CpuAccess::ReadWrite,
7570 )
7571 .unwrap();
7572 cpu_dst
7573 .as_typed::<u8>()
7574 .unwrap()
7575 .map()
7576 .unwrap()
7577 .as_mut_slice()
7578 .fill(114);
7579 gl_dst
7580 .as_typed::<u8>()
7581 .unwrap()
7582 .map()
7583 .unwrap()
7584 .as_mut_slice()
7585 .fill(114);
7586
7587 let mut cpu_dst_dyn = cpu_dst;
7588 cpu_converter
7589 .convert(
7590 &src_dyn,
7591 &mut cpu_dst_dyn,
7592 Some(crate::ConvertOptions::new().crop(crop)),
7593 )
7594 .unwrap();
7595 let cpu_dst = {
7596 let mut __t = cpu_dst_dyn.into_u8().unwrap();
7597 __t.set_format(PixelFormat::Rgba).unwrap();
7598 TensorDyn::from(__t)
7599 };
7600
7601 let mut gl_dst_dyn = gl_dst;
7602 gl_converter
7603 .convert(
7604 &src_dyn,
7605 &mut gl_dst_dyn,
7606 Some(crate::ConvertOptions::new().crop(crop)),
7607 )
7608 .map_err(|e| {
7609 log::error!("error mem {m:?} rot {rot:?} error: {e:?}");
7610 e
7611 })
7612 .unwrap();
7613 let gl_dst = {
7614 let mut __t = gl_dst_dyn.into_u8().unwrap();
7615 __t.set_format(PixelFormat::Rgba).unwrap();
7616 TensorDyn::from(__t)
7617 };
7618
7619 compare_images(
7620 &gl_dst,
7621 &cpu_dst,
7622 0.98,
7623 &format!("{} {:?} {:?}", function!(), rot, flip),
7624 );
7625 }
7626 }
7627 }
7628 }
7629
7630 #[test]
7631 #[cfg(target_os = "linux")]
7632 fn test_cpu_rotate() {
7633 for rot in [
7634 Rotation::Clockwise90,
7635 Rotation::Rotate180,
7636 Rotation::CounterClockwise90,
7637 ] {
7638 test_cpu_rotate_(rot);
7639 }
7640 }
7641
7642 #[cfg(target_os = "linux")]
7643 fn test_cpu_rotate_(rot: Rotation) {
7644 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7648
7649 let unchanged_src =
7650 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
7651 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
7652
7653 let (dst_width, dst_height) = match rot {
7654 Rotation::None | Rotation::Rotate180 => (src.width().unwrap(), src.height().unwrap()),
7655 Rotation::Clockwise90 | Rotation::CounterClockwise90 => {
7656 (src.height().unwrap(), src.width().unwrap())
7657 }
7658 };
7659
7660 let cpu_dst = TensorDyn::image(
7661 dst_width,
7662 dst_height,
7663 PixelFormat::Rgba,
7664 DType::U8,
7665 None,
7666 edgefirst_tensor::CpuAccess::ReadWrite,
7667 )
7668 .unwrap();
7669 let mut cpu_converter = CPUProcessor::new();
7670
7671 let (result, src, cpu_dst) = convert_img(
7674 &mut cpu_converter,
7675 src,
7676 cpu_dst,
7677 rot,
7678 Flip::None,
7679 Crop::no_crop(),
7680 );
7681 result.unwrap();
7682
7683 let (result, cpu_dst, src) = convert_img(
7684 &mut cpu_converter,
7685 cpu_dst,
7686 src,
7687 rot,
7688 Flip::None,
7689 Crop::no_crop(),
7690 );
7691 result.unwrap();
7692
7693 let (result, src, cpu_dst) = convert_img(
7694 &mut cpu_converter,
7695 src,
7696 cpu_dst,
7697 rot,
7698 Flip::None,
7699 Crop::no_crop(),
7700 );
7701 result.unwrap();
7702
7703 let (result, _cpu_dst, src) = convert_img(
7704 &mut cpu_converter,
7705 cpu_dst,
7706 src,
7707 rot,
7708 Flip::None,
7709 Crop::no_crop(),
7710 );
7711 result.unwrap();
7712
7713 compare_images(&src, &unchanged_src, 0.98, function!());
7714 }
7715
7716 #[test]
7717 #[cfg(any(
7718 target_os = "linux",
7719 target_os = "macos",
7720 target_os = "ios",
7721 target_os = "android",
7722 target_os = "windows"
7723 ))]
7724 #[cfg(feature = "opengl")]
7725 fn test_opengl_rotate() {
7726 if !is_opengl_available() {
7727 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
7728 return;
7729 }
7730
7731 let size = (1280, 720);
7732 let mut mem = vec![None, Some(TensorMemory::Mem)];
7734 if edgefirst_tensor::is_shm_available() {
7735 mem.push(Some(TensorMemory::Shm));
7736 }
7737 if crate::test_support::is_gpu_buffer_available() {
7738 mem.push(Some(TensorMemory::DmaBuf));
7739 } else {
7740 crate::test_support::report_skip(&format!(
7741 "{} - DmaBuf backing not exercised: no zero-copy GPU buffers",
7742 function!()
7743 ));
7744 }
7745 for m in mem {
7746 for rot in [
7747 Rotation::Clockwise90,
7748 Rotation::Rotate180,
7749 Rotation::CounterClockwise90,
7750 ] {
7751 test_opengl_rotate_(size, rot, m);
7752 }
7753 }
7754 }
7755
7756 #[cfg(any(
7757 target_os = "linux",
7758 target_os = "macos",
7759 target_os = "ios",
7760 target_os = "android",
7761 target_os = "windows"
7762 ))]
7763 #[cfg(feature = "opengl")]
7764 fn test_opengl_rotate_(
7765 size: (usize, usize),
7766 rot: Rotation,
7767 tensor_memory: Option<TensorMemory>,
7768 ) {
7769 let (dst_width, dst_height) = match rot {
7770 Rotation::None | Rotation::Rotate180 => size,
7771 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
7772 };
7773
7774 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7775 let src =
7776 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), tensor_memory).unwrap();
7777
7778 let cpu_dst = TensorDyn::image(
7779 dst_width,
7780 dst_height,
7781 PixelFormat::Rgba,
7782 DType::U8,
7783 None,
7784 edgefirst_tensor::CpuAccess::ReadWrite,
7785 )
7786 .unwrap();
7787 let mut cpu_converter = CPUProcessor::new();
7788
7789 let (result, mut src, cpu_dst) = convert_img(
7790 &mut cpu_converter,
7791 src,
7792 cpu_dst,
7793 rot,
7794 Flip::None,
7795 Crop::no_crop(),
7796 );
7797 result.unwrap();
7798
7799 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
7800
7801 for _ in 0..5 {
7802 let gl_dst = TensorDyn::image(
7803 dst_width,
7804 dst_height,
7805 PixelFormat::Rgba,
7806 DType::U8,
7807 tensor_memory,
7808 edgefirst_tensor::CpuAccess::ReadWrite,
7809 )
7810 .unwrap();
7811 let (result, src_back, gl_dst) = convert_img(
7812 &mut gl_converter,
7813 src,
7814 gl_dst,
7815 rot,
7816 Flip::None,
7817 Crop::no_crop(),
7818 );
7819 result.unwrap();
7820 src = src_back;
7821 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
7822 }
7823 }
7824
7825 #[test]
7826 #[cfg(target_os = "linux")]
7827 fn test_g2d_rotate() {
7828 if !is_g2d_available() {
7829 crate::test_support::report_skip(
7830 "test_g2d_rotate - G2D library (libg2d.so.2) not available",
7831 );
7832 return;
7833 }
7834 if !is_dma_available() {
7835 crate::test_support::report_skip("test_g2d_rotate - DMA memory allocation not available (permission denied or no DMA-BUF support)");
7836 return;
7837 }
7838
7839 let size = (1280, 720);
7840 for rot in [
7841 Rotation::Clockwise90,
7842 Rotation::Rotate180,
7843 Rotation::CounterClockwise90,
7844 ] {
7845 test_g2d_rotate_(size, rot);
7846 }
7847 }
7848
7849 #[cfg(target_os = "linux")]
7850 fn test_g2d_rotate_(size: (usize, usize), rot: Rotation) {
7851 let (dst_width, dst_height) = match rot {
7852 Rotation::None | Rotation::Rotate180 => size,
7853 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
7854 };
7855
7856 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7857 let src = crate::load_image_test_helper(
7858 &file,
7859 Some(PixelFormat::Rgba),
7860 Some(TensorMemory::DmaBuf),
7861 )
7862 .unwrap();
7863
7864 let cpu_dst = TensorDyn::image(
7865 dst_width,
7866 dst_height,
7867 PixelFormat::Rgba,
7868 DType::U8,
7869 None,
7870 edgefirst_tensor::CpuAccess::ReadWrite,
7871 )
7872 .unwrap();
7873 let mut cpu_converter = CPUProcessor::new();
7874
7875 let (result, src, cpu_dst) = convert_img(
7876 &mut cpu_converter,
7877 src,
7878 cpu_dst,
7879 rot,
7880 Flip::None,
7881 Crop::no_crop(),
7882 );
7883 result.unwrap();
7884
7885 let g2d_dst = TensorDyn::image(
7886 dst_width,
7887 dst_height,
7888 PixelFormat::Rgba,
7889 DType::U8,
7890 Some(TensorMemory::DmaBuf),
7891 edgefirst_tensor::CpuAccess::ReadWrite,
7892 )
7893 .unwrap();
7894 let mut g2d_converter = G2DProcessor::new().unwrap();
7895
7896 let (result, _src, g2d_dst) = convert_img(
7897 &mut g2d_converter,
7898 src,
7899 g2d_dst,
7900 rot,
7901 Flip::None,
7902 Crop::no_crop(),
7903 );
7904 result.unwrap();
7905
7906 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
7912 }
7913
7914 #[test]
7915 fn test_rgba_to_yuyv_resize_cpu() {
7916 let src = load_bytes_to_tensor(
7917 1280,
7918 720,
7919 PixelFormat::Rgba,
7920 None,
7921 &edgefirst_bench::testdata::read("camera720p.rgba"),
7922 )
7923 .unwrap();
7924
7925 let (dst_width, dst_height) = (640, 360);
7926
7927 let dst = TensorDyn::image(
7928 dst_width,
7929 dst_height,
7930 PixelFormat::Yuyv,
7931 DType::U8,
7932 None,
7933 edgefirst_tensor::CpuAccess::ReadWrite,
7934 )
7935 .unwrap();
7936
7937 let dst_through_yuyv = TensorDyn::image(
7938 dst_width,
7939 dst_height,
7940 PixelFormat::Rgba,
7941 DType::U8,
7942 None,
7943 edgefirst_tensor::CpuAccess::ReadWrite,
7944 )
7945 .unwrap();
7946 let dst_direct = TensorDyn::image(
7947 dst_width,
7948 dst_height,
7949 PixelFormat::Rgba,
7950 DType::U8,
7951 None,
7952 edgefirst_tensor::CpuAccess::ReadWrite,
7953 )
7954 .unwrap();
7955
7956 let mut cpu_converter = CPUProcessor::new();
7957
7958 let (result, src, dst) = convert_img(
7959 &mut cpu_converter,
7960 src,
7961 dst,
7962 Rotation::None,
7963 Flip::None,
7964 Crop::no_crop(),
7965 );
7966 result.unwrap();
7967
7968 let (result, _dst, dst_through_yuyv) = convert_img(
7969 &mut cpu_converter,
7970 dst,
7971 dst_through_yuyv,
7972 Rotation::None,
7973 Flip::None,
7974 Crop::no_crop(),
7975 );
7976 result.unwrap();
7977
7978 let (result, _src, dst_direct) = convert_img(
7979 &mut cpu_converter,
7980 src,
7981 dst_direct,
7982 Rotation::None,
7983 Flip::None,
7984 Crop::no_crop(),
7985 );
7986 result.unwrap();
7987
7988 compare_images(&dst_through_yuyv, &dst_direct, 0.98, function!());
7989 }
7990
7991 #[test]
7992 #[cfg(any(
7993 target_os = "linux",
7994 target_os = "macos",
7995 target_os = "ios",
7996 target_os = "android",
7997 target_os = "windows"
7998 ))]
7999 #[cfg(feature = "opengl")]
8000 #[ignore = "opengl doesn't support rendering to PixelFormat::Yuyv texture"]
8001 fn test_rgba_to_yuyv_resize_opengl() {
8002 if !is_opengl_available() {
8003 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
8004 return;
8005 }
8006
8007 if !crate::test_support::is_gpu_buffer_available() {
8008 crate::test_support::report_skip(&format!(
8009 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
8010 function!()
8011 ));
8012 return;
8013 }
8014
8015 let src = load_bytes_to_tensor(
8016 1280,
8017 720,
8018 PixelFormat::Rgba,
8019 None,
8020 &edgefirst_bench::testdata::read("camera720p.rgba"),
8021 )
8022 .unwrap();
8023
8024 let (dst_width, dst_height) = (640, 360);
8025
8026 let dst = TensorDyn::image(
8027 dst_width,
8028 dst_height,
8029 PixelFormat::Yuyv,
8030 DType::U8,
8031 Some(TensorMemory::DmaBuf),
8032 edgefirst_tensor::CpuAccess::ReadWrite,
8033 )
8034 .unwrap();
8035
8036 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
8037
8038 let (result, src, dst) = convert_img(
8039 &mut gl_converter,
8040 src,
8041 dst,
8042 Rotation::None,
8043 Flip::None,
8044 Crop::letterbox([255, 255, 255, 255]),
8045 );
8046 result.unwrap();
8047
8048 std::fs::write(
8049 "rgba_to_yuyv_opengl.yuyv",
8050 dst.as_typed::<u8>().unwrap().map().unwrap().as_slice(),
8051 )
8052 .unwrap();
8053 let cpu_dst = TensorDyn::image(
8054 dst_width,
8055 dst_height,
8056 PixelFormat::Yuyv,
8057 DType::U8,
8058 Some(TensorMemory::DmaBuf),
8059 edgefirst_tensor::CpuAccess::ReadWrite,
8060 )
8061 .unwrap();
8062 let (result, _src, cpu_dst) = convert_img(
8063 &mut CPUProcessor::new(),
8064 src,
8065 cpu_dst,
8066 Rotation::None,
8067 Flip::None,
8068 Crop::no_crop(),
8069 );
8070 result.unwrap();
8071
8072 compare_images_convert_to_rgb(&dst, &cpu_dst, 0.98, function!());
8073 }
8074
8075 #[test]
8076 #[cfg(target_os = "linux")]
8077 fn test_rgba_to_yuyv_resize_g2d() {
8078 if !is_g2d_available() {
8079 crate::test_support::report_skip(
8080 "test_rgba_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available",
8081 );
8082 return;
8083 }
8084 if !is_dma_available() {
8085 crate::test_support::report_skip("test_rgba_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
8086 return;
8087 }
8088
8089 let src = load_bytes_to_tensor(
8090 1280,
8091 720,
8092 PixelFormat::Rgba,
8093 Some(TensorMemory::DmaBuf),
8094 &edgefirst_bench::testdata::read("camera720p.rgba"),
8095 )
8096 .unwrap();
8097
8098 let (dst_width, dst_height) = (1280, 720);
8099
8100 let cpu_dst = TensorDyn::image(
8101 dst_width,
8102 dst_height,
8103 PixelFormat::Yuyv,
8104 DType::U8,
8105 Some(TensorMemory::DmaBuf),
8106 edgefirst_tensor::CpuAccess::ReadWrite,
8107 )
8108 .unwrap();
8109
8110 let g2d_dst = TensorDyn::image(
8111 dst_width,
8112 dst_height,
8113 PixelFormat::Yuyv,
8114 DType::U8,
8115 Some(TensorMemory::DmaBuf),
8116 edgefirst_tensor::CpuAccess::ReadWrite,
8117 )
8118 .unwrap();
8119
8120 let mut g2d_converter = G2DProcessor::new().unwrap();
8121 let crop = Crop::new();
8122
8123 g2d_dst
8124 .as_typed::<u8>()
8125 .unwrap()
8126 .map()
8127 .unwrap()
8128 .as_mut_slice()
8129 .fill(128);
8130 let (result, src, g2d_dst) = convert_img(
8131 &mut g2d_converter,
8132 src,
8133 g2d_dst,
8134 Rotation::None,
8135 Flip::None,
8136 crop,
8137 );
8138 result.unwrap();
8139
8140 let cpu_dst_img = cpu_dst;
8141 cpu_dst_img
8142 .as_typed::<u8>()
8143 .unwrap()
8144 .map()
8145 .unwrap()
8146 .as_mut_slice()
8147 .fill(128);
8148 let (result, _src, cpu_dst) = convert_img(
8149 &mut CPUProcessor::new(),
8150 src,
8151 cpu_dst_img,
8152 Rotation::None,
8153 Flip::None,
8154 crop,
8155 );
8156 result.unwrap();
8157
8158 compare_images_convert_to_rgb(&cpu_dst, &g2d_dst, 0.98, function!());
8159 }
8160
8161 #[test]
8162 fn test_yuyv_to_rgba_cpu() {
8163 let file = edgefirst_bench::testdata::read("camera720p.yuyv").to_vec();
8164 let src = TensorDyn::image(
8165 1280,
8166 720,
8167 PixelFormat::Yuyv,
8168 DType::U8,
8169 None,
8170 edgefirst_tensor::CpuAccess::ReadWrite,
8171 )
8172 .unwrap();
8173 src.as_typed::<u8>()
8174 .unwrap()
8175 .map()
8176 .unwrap()
8177 .as_mut_slice()
8178 .copy_from_slice(&file);
8179
8180 let dst = TensorDyn::image(
8181 1280,
8182 720,
8183 PixelFormat::Rgba,
8184 DType::U8,
8185 None,
8186 edgefirst_tensor::CpuAccess::ReadWrite,
8187 )
8188 .unwrap();
8189 let mut cpu_converter = CPUProcessor::new();
8190
8191 let (result, _src, dst) = convert_img(
8192 &mut cpu_converter,
8193 src,
8194 dst,
8195 Rotation::None,
8196 Flip::None,
8197 Crop::no_crop(),
8198 );
8199 result.unwrap();
8200
8201 let target_image = TensorDyn::image(
8202 1280,
8203 720,
8204 PixelFormat::Rgba,
8205 DType::U8,
8206 None,
8207 edgefirst_tensor::CpuAccess::ReadWrite,
8208 )
8209 .unwrap();
8210 target_image
8211 .as_typed::<u8>()
8212 .unwrap()
8213 .map()
8214 .unwrap()
8215 .as_mut_slice()
8216 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
8217
8218 compare_images(&dst, &target_image, 0.98, function!());
8221 }
8222
8223 #[test]
8224 fn test_yuyv_to_rgb_cpu() {
8225 let file = edgefirst_bench::testdata::read("camera720p.yuyv").to_vec();
8226 let src = TensorDyn::image(
8227 1280,
8228 720,
8229 PixelFormat::Yuyv,
8230 DType::U8,
8231 None,
8232 edgefirst_tensor::CpuAccess::ReadWrite,
8233 )
8234 .unwrap();
8235 src.as_typed::<u8>()
8236 .unwrap()
8237 .map()
8238 .unwrap()
8239 .as_mut_slice()
8240 .copy_from_slice(&file);
8241
8242 let dst = TensorDyn::image(
8243 1280,
8244 720,
8245 PixelFormat::Rgb,
8246 DType::U8,
8247 None,
8248 edgefirst_tensor::CpuAccess::ReadWrite,
8249 )
8250 .unwrap();
8251 let mut cpu_converter = CPUProcessor::new();
8252
8253 let (result, _src, dst) = convert_img(
8254 &mut cpu_converter,
8255 src,
8256 dst,
8257 Rotation::None,
8258 Flip::None,
8259 Crop::no_crop(),
8260 );
8261 result.unwrap();
8262
8263 let target_image = TensorDyn::image(
8264 1280,
8265 720,
8266 PixelFormat::Rgb,
8267 DType::U8,
8268 None,
8269 edgefirst_tensor::CpuAccess::ReadWrite,
8270 )
8271 .unwrap();
8272 target_image
8273 .as_typed::<u8>()
8274 .unwrap()
8275 .map()
8276 .unwrap()
8277 .as_mut_slice()
8278 .as_chunks_mut::<3>()
8279 .0
8280 .iter_mut()
8281 .zip(
8282 edgefirst_bench::testdata::read("camera720p.rgba")
8283 .as_chunks::<4>()
8284 .0,
8285 )
8286 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
8287
8288 compare_images(&dst, &target_image, 0.98, function!());
8291 }
8292
8293 #[test]
8294 #[cfg(target_os = "linux")]
8295 fn test_yuyv_to_rgba_g2d() {
8296 if !is_g2d_available() {
8297 crate::test_support::report_skip(
8298 "test_yuyv_to_rgba_g2d - G2D library (libg2d.so.2) not available",
8299 );
8300 return;
8301 }
8302 if !is_dma_available() {
8303 crate::test_support::report_skip("test_yuyv_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
8304 return;
8305 }
8306
8307 let src = load_bytes_to_tensor(
8308 1280,
8309 720,
8310 PixelFormat::Yuyv,
8311 None,
8312 &edgefirst_bench::testdata::read("camera720p.yuyv"),
8313 )
8314 .unwrap();
8315
8316 let dst = TensorDyn::image(
8317 1280,
8318 720,
8319 PixelFormat::Rgba,
8320 DType::U8,
8321 Some(TensorMemory::DmaBuf),
8322 edgefirst_tensor::CpuAccess::ReadWrite,
8323 )
8324 .unwrap();
8325 let mut g2d_converter = G2DProcessor::new().unwrap();
8326
8327 let (result, _src, dst) = convert_img(
8328 &mut g2d_converter,
8329 src,
8330 dst,
8331 Rotation::None,
8332 Flip::None,
8333 Crop::no_crop(),
8334 );
8335 result.unwrap();
8336
8337 let target_image = TensorDyn::image(
8338 1280,
8339 720,
8340 PixelFormat::Rgba,
8341 DType::U8,
8342 None,
8343 edgefirst_tensor::CpuAccess::ReadWrite,
8344 )
8345 .unwrap();
8346 target_image
8347 .as_typed::<u8>()
8348 .unwrap()
8349 .map()
8350 .unwrap()
8351 .as_mut_slice()
8352 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
8353
8354 compare_images(&dst, &target_image, 0.98, function!());
8358 }
8359
8360 #[test]
8361 #[cfg(any(
8362 target_os = "linux",
8363 target_os = "macos",
8364 target_os = "ios",
8365 target_os = "android",
8366 target_os = "windows"
8367 ))]
8368 #[cfg(feature = "opengl")]
8369 fn test_yuyv_to_rgba_opengl() {
8370 if !is_opengl_available() {
8371 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
8372 return;
8373 }
8374 if !crate::test_support::is_gpu_buffer_available() {
8377 crate::test_support::report_skip(&format!(
8378 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
8379 function!()
8380 ));
8381 return;
8382 }
8383
8384 let src = load_bytes_to_tensor(
8385 1280,
8386 720,
8387 PixelFormat::Yuyv,
8388 Some(TensorMemory::DmaBuf),
8389 &edgefirst_bench::testdata::read("camera720p.yuyv"),
8390 )
8391 .unwrap();
8392
8393 let dst = TensorDyn::image(
8394 1280,
8395 720,
8396 PixelFormat::Rgba,
8397 DType::U8,
8398 Some(TensorMemory::DmaBuf),
8399 edgefirst_tensor::CpuAccess::ReadWrite,
8400 )
8401 .unwrap();
8402 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
8403
8404 let (result, _src, dst) = convert_img(
8405 &mut gl_converter,
8406 src,
8407 dst,
8408 Rotation::None,
8409 Flip::None,
8410 Crop::no_crop(),
8411 );
8412 if let Err(crate::Error::NotSupported(msg)) = &result {
8415 crate::test_support::report_skip(&format!("{} — {msg}", function!()));
8416 return;
8417 }
8418 result.unwrap();
8419
8420 let target_image = TensorDyn::image(
8421 1280,
8422 720,
8423 PixelFormat::Rgba,
8424 DType::U8,
8425 None,
8426 edgefirst_tensor::CpuAccess::ReadWrite,
8427 )
8428 .unwrap();
8429 target_image
8430 .as_typed::<u8>()
8431 .unwrap()
8432 .map()
8433 .unwrap()
8434 .as_mut_slice()
8435 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
8436
8437 compare_images(&dst, &target_image, 0.98, function!());
8441 }
8442
8443 #[test]
8453 #[cfg(target_os = "macos")]
8454 #[cfg(feature = "opengl")]
8455 fn test_grey_r8_iosurface_to_rgba_opengl_macos() {
8456 let mut proc = match GLProcessorThreaded::new(None) {
8457 Ok(p) => p,
8458 Err(e) => {
8459 crate::test_support::report_skip(&format!(
8460 "{} — GL engine init failed ({e:?})",
8461 function!()
8462 ));
8463 return;
8464 }
8465 };
8466
8467 let (w, h) = (16usize, 16usize);
8468 let src = TensorDyn::image(
8469 w,
8470 h,
8471 PixelFormat::Grey,
8472 DType::U8,
8473 Some(TensorMemory::DmaBuf),
8474 edgefirst_tensor::CpuAccess::ReadWrite,
8475 )
8476 .expect("GREY IOSurface (R8/L008) should allocate — proves the FourCC mapping");
8477 {
8479 let su8 = src.as_typed::<u8>().unwrap();
8480 let stride = src
8481 .as_typed::<u8>()
8482 .unwrap()
8483 .effective_row_stride()
8484 .unwrap();
8485 let mut m = su8.map().unwrap();
8486 let buf = m.as_mut_slice();
8487 for y in 0..h {
8488 for x in 0..w {
8489 buf[y * stride + x] = ((x * 13 + y * 7) & 0xff) as u8;
8490 }
8491 }
8492 }
8493
8494 let dst = TensorDyn::image(
8495 w,
8496 h,
8497 PixelFormat::Rgba,
8498 DType::U8,
8499 Some(TensorMemory::DmaBuf),
8500 edgefirst_tensor::CpuAccess::ReadWrite,
8501 )
8502 .unwrap();
8503 let (result, src_back, dst) = convert_img(
8504 &mut proc,
8505 src,
8506 dst,
8507 Rotation::None,
8508 Flip::None,
8509 Crop::no_crop(),
8510 );
8511 result.expect("GREY(R8 IOSurface) → RGBA must convert on ANGLE (R8 binding works)");
8512
8513 let src_stride = src_back
8514 .as_typed::<u8>()
8515 .unwrap()
8516 .effective_row_stride()
8517 .unwrap();
8518 let src_map = src_back.as_typed::<u8>().unwrap().map().unwrap();
8519 let sbytes = src_map.as_slice();
8520 let dst_stride = dst
8521 .as_typed::<u8>()
8522 .unwrap()
8523 .effective_row_stride()
8524 .unwrap();
8525 let dst_map = dst.as_typed::<u8>().unwrap().map().unwrap();
8526 let dbytes = dst_map.as_slice();
8527 for y in 0..h {
8528 for x in 0..w {
8529 let yv = sbytes[y * src_stride + x] as i16;
8530 let p = y * dst_stride + x * 4;
8531 for c in 0..3 {
8532 assert!(
8533 (dbytes[p + c] as i16 - yv).abs() <= 2,
8534 "pixel ({x},{y}) ch{c} = {} expected ~{yv} (GREY→RGB identity)",
8535 dbytes[p + c]
8536 );
8537 }
8538 }
8539 }
8540 }
8541
8542 #[test]
8548 #[cfg(target_os = "macos")]
8549 #[cfg(feature = "opengl")]
8550 fn test_nv12_to_planar_f16_two_pass_opengl_macos() {
8551 let mut gpu = match GLProcessorThreaded::new(None) {
8552 Ok(p) => p,
8553 Err(e) => {
8554 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8555 return;
8556 }
8557 };
8558 let (w, h) = (64usize, 64usize);
8559 let Some(src) = crate::test_support::gpu_buffer_allocated(
8560 TensorDyn::image(
8561 w,
8562 h,
8563 PixelFormat::Nv12,
8564 DType::U8,
8565 Some(TensorMemory::DmaBuf),
8566 edgefirst_tensor::CpuAccess::ReadWrite,
8567 ),
8568 "NV12 IOSurface",
8569 ) else {
8570 return;
8571 };
8572 src.as_typed::<u8>()
8573 .unwrap()
8574 .map()
8575 .unwrap()
8576 .as_mut_slice()
8577 .fill(128); let Some(dst) = crate::test_support::gpu_buffer_allocated(
8580 TensorDyn::image(
8581 w,
8582 h,
8583 PixelFormat::PlanarRgb,
8584 DType::F16,
8585 Some(TensorMemory::DmaBuf),
8586 edgefirst_tensor::CpuAccess::ReadWrite,
8587 ),
8588 "F16 PlanarRgb IOSurface",
8589 ) else {
8590 return;
8591 };
8592 let mut dst = dst;
8593 if let Err(e) = ImageProcessorTrait::convert(&mut gpu, &src, &mut dst, None) {
8595 crate::test_support::report_skip(&format!(
8599 "{} — NV12→PlanarRgb F16 not available ({e:?})",
8600 function!()
8601 ));
8602 return;
8603 }
8604 let dt = dst.as_typed::<half::f16>().expect("dst is F16 PlanarRgb");
8605 let map = dt.map().unwrap();
8606 let vals = map.as_slice();
8607 let mut checked = 0usize;
8610 for &v in vals.iter() {
8611 let f = f32::from(v);
8612 assert!(
8613 (0.40..=0.60).contains(&f),
8614 "planar F16 value {f} not ~0.5 for neutral-grey NV12"
8615 );
8616 checked += 1;
8617 }
8618 assert!(
8619 checked >= w * h * 3,
8620 "expected >= 3 planes of samples, got {checked}"
8621 );
8622 }
8623
8624 #[test]
8632 #[cfg(target_os = "macos")]
8633 #[cfg(feature = "opengl")]
8634 fn test_nv12_to_planar_f16_two_pass_pool_opengl_macos() {
8635 let mut gpu = match GLProcessorThreaded::new(None) {
8636 Ok(p) => p,
8637 Err(e) => {
8638 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8639 return;
8640 }
8641 };
8642 let (fw, fh) = (96usize, 64usize);
8644 let (pool_w, pool_h) = (256usize, 768usize);
8645 let (model_w, model_h) = (128usize, 128usize);
8646
8647 let Some(mut src) = crate::test_support::gpu_buffer_allocated(
8648 TensorDyn::image(
8649 pool_w,
8650 pool_h,
8651 PixelFormat::Grey,
8652 DType::U8,
8653 Some(TensorMemory::DmaBuf),
8654 edgefirst_tensor::CpuAccess::ReadWrite,
8655 ),
8656 "R8 pool",
8657 ) else {
8658 return;
8659 };
8660 src.configure_image(fw, fh, PixelFormat::Nv12)
8661 .unwrap_or_else(|e| panic!("configure_image NV12 on pool: {e}"));
8662 let stride = src
8663 .as_typed::<u8>()
8664 .unwrap()
8665 .effective_row_stride()
8666 .unwrap();
8667 src.as_typed::<u8>()
8668 .unwrap()
8669 .map()
8670 .unwrap()
8671 .as_mut_slice()
8672 .fill(128); let Some(mut dst) = crate::test_support::gpu_buffer_allocated(
8675 TensorDyn::image(
8676 model_w,
8677 model_h,
8678 PixelFormat::PlanarRgb,
8679 DType::F16,
8680 Some(TensorMemory::DmaBuf),
8681 edgefirst_tensor::CpuAccess::ReadWrite,
8682 ),
8683 "F16 PlanarRgb dst",
8684 ) else {
8685 return;
8686 };
8687
8688 let _ = model_w;
8690 let crop = Crop::new()
8691 .with_source(Some(Region::new(0, 0, fw, fh)))
8692 .with_fit(Fit::Letterbox {
8693 pad: [0, 0, 0, 255],
8694 });
8695 if let Err(e) = ImageProcessorTrait::convert(
8696 &mut gpu,
8697 &src,
8698 &mut dst,
8699 Some(ConvertOptions::new().crop(crop)),
8700 ) {
8701 crate::test_support::report_skip(&format!(
8702 "{} — NV12→PlanarRgb F16 unavailable ({e:?})",
8703 function!()
8704 ));
8705 return;
8706 }
8707 let _ = stride;
8708 let dt = dst.as_typed::<half::f16>().expect("dst F16");
8711 let map = dt.map().unwrap();
8712 let any_half = map.as_slice().iter().any(|&v| {
8713 let f = f32::from(v);
8714 (0.40..=0.60).contains(&f)
8715 });
8716 assert!(any_half, "expected ~0.5 grey samples in the letterbox band");
8717 }
8718
8719 #[test]
8725 #[cfg(target_os = "macos")]
8726 #[cfg(feature = "opengl")]
8727 fn test_nv12_to_planar_f16_cross_thread_opengl_macos() {
8728 use std::sync::mpsc;
8729 let mut proc = match ImageProcessor::new() {
8732 Ok(p) => p,
8733 Err(e) => {
8734 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8735 return;
8736 }
8737 };
8738 let (fw, fh) = (96usize, 64usize);
8739 let Some(mut src) = crate::test_support::gpu_buffer_allocated(
8740 TensorDyn::image(
8741 256,
8742 768,
8743 PixelFormat::Grey,
8744 DType::U8,
8745 Some(TensorMemory::DmaBuf),
8746 edgefirst_tensor::CpuAccess::ReadWrite,
8747 ),
8748 "NV12 R8 pool",
8749 ) else {
8750 return;
8751 };
8752 src.configure_image(fw, fh, PixelFormat::Nv12).unwrap();
8753 src.as_typed::<u8>()
8754 .unwrap()
8755 .map()
8756 .unwrap()
8757 .as_mut_slice()
8758 .fill(128);
8759 let Some(mut dst) = crate::test_support::gpu_buffer_allocated(
8760 TensorDyn::image(
8761 128,
8762 128,
8763 PixelFormat::PlanarRgb,
8764 DType::F16,
8765 Some(TensorMemory::DmaBuf),
8766 edgefirst_tensor::CpuAccess::ReadWrite,
8767 ),
8768 "F16 PlanarRgb dst",
8769 ) else {
8770 return;
8771 };
8772 let crop = Crop::new().with_source(Some(Region::new(0, 0, fw, fh)));
8773
8774 let (tx, rx) = mpsc::channel::<bool>();
8777 let worker = std::thread::spawn(move || {
8778 let _ = ImageProcessorTrait::convert(
8779 &mut proc,
8780 &src,
8781 &mut dst,
8782 Some(ConvertOptions::new().crop(crop)),
8783 );
8784 let _ = tx.send(true);
8785 });
8786 match rx.recv_timeout(std::time::Duration::from_secs(20)) {
8787 Ok(_) => { let _ = worker.join(); }
8788 Err(_) => panic!(
8789 "cross-thread NV12→PlanarRgb convert HUNG (>20s) — reproduces the orchestrator deadlock"
8790 ),
8791 }
8792 }
8793
8794 #[test]
8801 #[cfg(target_os = "macos")]
8802 #[cfg(feature = "opengl")]
8803 fn test_nv_to_planar_f16_varying_sizes_no_leak_opengl_macos() {
8804 let mut gpu = match GLProcessorThreaded::new(None) {
8805 Ok(p) => p,
8806 Err(e) => {
8807 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8808 return;
8809 }
8810 };
8811 let (max_w, max_h) = (640usize, 640usize);
8814 let depth = 4usize;
8815 let mut srcs = Vec::new();
8816 let mut dsts = Vec::new();
8817 for _ in 0..depth {
8818 let Some(src) = crate::test_support::gpu_buffer_allocated(
8819 TensorDyn::image(
8820 max_w,
8821 max_h * 3,
8822 PixelFormat::Grey,
8823 DType::U8,
8824 Some(TensorMemory::DmaBuf),
8825 edgefirst_tensor::CpuAccess::ReadWrite,
8826 ),
8827 "R8 pool",
8828 ) else {
8829 return;
8830 };
8831 srcs.push(src);
8832 let Some(dst) = crate::test_support::gpu_buffer_allocated(
8833 TensorDyn::image(
8834 640,
8835 640,
8836 PixelFormat::PlanarRgb,
8837 DType::F16,
8838 Some(TensorMemory::DmaBuf),
8839 edgefirst_tensor::CpuAccess::ReadWrite,
8840 ),
8841 "F16 PlanarRgb dst",
8842 ) else {
8843 return;
8844 };
8845 dsts.push(dst);
8846 }
8847 let sizes = [
8849 (640, 480),
8850 (500, 375),
8851 (640, 427),
8852 (333, 500),
8853 (480, 640),
8854 (612, 612),
8855 (428, 640),
8856 (576, 432),
8857 ];
8858 let mut first_ms = 0f64;
8859 let mut last_ms = 0f64;
8860 let iters = 40usize;
8861 for i in 0..iters {
8862 let (fw, fh) = sizes[i % sizes.len()];
8863 let src = &mut srcs[i % depth];
8864 let dst = &mut dsts[i % depth];
8865 src.configure_image(fw, fh, PixelFormat::Nv24).unwrap();
8866 src.as_typed::<u8>()
8867 .unwrap()
8868 .map()
8869 .unwrap()
8870 .as_mut_slice()
8871 .fill(128);
8872 let crop = Crop::new().with_source(Some(Region::new(0, 0, fw, fh)));
8873 let t0 = std::time::Instant::now();
8874 ImageProcessorTrait::convert(
8875 &mut gpu,
8876 src,
8877 dst,
8878 Some(ConvertOptions::new().crop(crop)),
8879 )
8880 .unwrap_or_else(|e| panic!("convert iter {i} ({fw}×{fh}): {e}"));
8881 let ms = t0.elapsed().as_secs_f64() * 1e3;
8882 if i == 2 {
8883 first_ms = ms;
8884 }
8885 if i == iters - 1 {
8886 last_ms = ms;
8887 }
8888 }
8889 eprintln!("first={first_ms:.2}ms last={last_ms:.2}ms");
8890 assert!(
8891 last_ms < first_ms * 5.0 + 5.0,
8892 "convert latency ran away: first {first_ms:.2}ms → last {last_ms:.2}ms (intermediate/pbuffer leak)"
8893 );
8894 }
8895
8896 #[test]
8903 #[cfg(target_os = "macos")]
8904 #[cfg(feature = "opengl")]
8905 fn test_nv12_nv16_nv24_to_rgba_opengl_macos() {
8906 let mut gpu = match GLProcessorThreaded::new(None) {
8907 Ok(p) => p,
8908 Err(e) => {
8909 crate::test_support::report_skip(&format!(
8910 "{} — GL engine init failed ({e:?})",
8911 function!()
8912 ));
8913 return;
8914 }
8915 };
8916 let mut cpu = CPUProcessor::new();
8917
8918 let fill = |buf: &mut [u8], stride: usize, fmt: PixelFormat, w: usize, h: usize| {
8930 for y in 0..h {
8931 for x in 0..w {
8932 buf[y * stride + x] = ((x * 9 + y * 5) & 0xff) as u8;
8933 }
8934 }
8935 let (cw, ch, uv_grid_rows) = match fmt {
8936 PixelFormat::Nv12 => (w / 2, h / 2, 1usize),
8937 PixelFormat::Nv16 => (w / 2, h, 1usize),
8938 _ => (w, h, 2usize), };
8940 let uv_plane = h * stride;
8941 for cy in 0..ch {
8942 for cx in 0..cw {
8943 let off = uv_plane + cy * uv_grid_rows * stride + cx * 2;
8944 buf[off] = ((cx * 11 + 30) & 0xff) as u8;
8945 buf[off + 1] = ((cy * 7 + 200) & 0xff) as u8;
8946 }
8947 }
8948 };
8949
8950 for fmt in [PixelFormat::Nv12, PixelFormat::Nv16, PixelFormat::Nv24] {
8951 for (w, h) in [
8952 (16usize, 16usize), (15, 16), (16, 15), ] {
8956 let mem = TensorDyn::image(
8957 w,
8958 h,
8959 fmt,
8960 DType::U8,
8961 None,
8962 edgefirst_tensor::CpuAccess::ReadWrite,
8963 )
8964 .unwrap();
8965 let mem_stride = mem
8966 .as_typed::<u8>()
8967 .unwrap()
8968 .effective_row_stride()
8969 .unwrap();
8970 fill(
8971 mem.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
8972 mem_stride,
8973 fmt,
8974 w,
8975 h,
8976 );
8977 let cpu_dst = TensorDyn::image(
8978 w,
8979 h,
8980 PixelFormat::Rgba,
8981 DType::U8,
8982 None,
8983 edgefirst_tensor::CpuAccess::ReadWrite,
8984 )
8985 .unwrap();
8986 let (r, _s, cpu_dst) = convert_img(
8987 &mut cpu,
8988 mem,
8989 cpu_dst,
8990 Rotation::None,
8991 Flip::None,
8992 Crop::no_crop(),
8993 );
8994 r.unwrap_or_else(|e| panic!("CPU {fmt:?}->{w}x{h}->RGBA: {e}"));
8995
8996 let ios = TensorDyn::image(
8997 w,
8998 h,
8999 fmt,
9000 DType::U8,
9001 Some(TensorMemory::DmaBuf),
9002 edgefirst_tensor::CpuAccess::ReadWrite,
9003 )
9004 .unwrap_or_else(|e| panic!("{fmt:?} {w}x{h} IOSurface alloc: {e}"));
9005 let ios_stride = ios
9006 .as_typed::<u8>()
9007 .unwrap()
9008 .effective_row_stride()
9009 .unwrap();
9010 fill(
9011 ios.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
9012 ios_stride,
9013 fmt,
9014 w,
9015 h,
9016 );
9017 let gpu_dst = TensorDyn::image(
9018 w,
9019 h,
9020 PixelFormat::Rgba,
9021 DType::U8,
9022 Some(TensorMemory::DmaBuf),
9023 edgefirst_tensor::CpuAccess::ReadWrite,
9024 )
9025 .unwrap();
9026 let (r, _s, gpu_dst) = convert_img(
9027 &mut gpu,
9028 ios,
9029 gpu_dst,
9030 Rotation::None,
9031 Flip::None,
9032 Crop::no_crop(),
9033 );
9034 r.unwrap_or_else(|e| panic!("GPU {fmt:?}->{w}x{h}->RGBA on ANGLE: {e}"));
9035
9036 let cs = cpu_dst
9037 .as_typed::<u8>()
9038 .unwrap()
9039 .effective_row_stride()
9040 .unwrap();
9041 let cmap = cpu_dst.as_typed::<u8>().unwrap().map().unwrap();
9042 let cb = cmap.as_slice();
9043 let gs = gpu_dst
9044 .as_typed::<u8>()
9045 .unwrap()
9046 .effective_row_stride()
9047 .unwrap();
9048 let gmap = gpu_dst.as_typed::<u8>().unwrap().map().unwrap();
9049 let gb = gmap.as_slice();
9050 let mut max_d = 0i16;
9051 for y in 0..h {
9052 for x in 0..w {
9053 for c in 0..3 {
9054 let cv = cb[y * cs + x * 4 + c] as i16;
9055 let gv = gb[y * gs + x * 4 + c] as i16;
9056 max_d = max_d.max((cv - gv).abs());
9057 }
9058 }
9059 }
9060 assert!(
9061 max_d <= 3,
9062 "{fmt:?} {w}x{h}: GPU vs CPU RGBA max channel diff {max_d} > 3"
9063 );
9064 }
9065 }
9066 }
9067
9068 #[test]
9076 #[cfg(target_os = "macos")]
9077 #[cfg(feature = "opengl")]
9078 fn test_nv_to_rgba_larger_pool_surface_opengl_macos() {
9079 let mut gpu = match GLProcessorThreaded::new(None) {
9080 Ok(p) => p,
9081 Err(e) => {
9082 crate::test_support::report_skip(&format!(
9083 "{} — GL engine init failed ({e:?})",
9084 function!()
9085 ));
9086 return;
9087 }
9088 };
9089 let mut cpu = CPUProcessor::new();
9090 let (pool_w, pool_h) = (256usize, 256usize);
9093
9094 let fill = |buf: &mut [u8], stride: usize, fmt: PixelFormat, w: usize, h: usize| {
9098 for y in 0..h {
9099 for x in 0..w {
9100 buf[y * stride + x] = ((x * 9 + y * 5) & 0xff) as u8;
9101 }
9102 }
9103 let (cw, ch, uv_grid_rows) = match fmt {
9104 PixelFormat::Nv12 => (w / 2, h / 2, 1usize),
9105 PixelFormat::Nv16 => (w / 2, h, 1usize),
9106 _ => (w, h, 2usize), };
9108 let uv_plane = h * stride;
9109 for cy in 0..ch {
9110 for cx in 0..cw {
9111 let off = uv_plane + cy * uv_grid_rows * stride + cx * 2;
9112 buf[off] = ((cx * 11 + 30) & 0xff) as u8;
9113 buf[off + 1] = ((cy * 7 + 200) & 0xff) as u8;
9114 }
9115 }
9116 };
9117
9118 for fmt in [PixelFormat::Nv12, PixelFormat::Nv16, PixelFormat::Nv24] {
9119 for (w, h) in [
9120 (40usize, 24usize), (15, 16), (16, 15), ] {
9124 let ew = w.next_multiple_of(2);
9127
9128 let mem = TensorDyn::image(
9130 w,
9131 h,
9132 fmt,
9133 DType::U8,
9134 None,
9135 edgefirst_tensor::CpuAccess::ReadWrite,
9136 )
9137 .unwrap();
9138 let mem_stride = mem
9139 .as_typed::<u8>()
9140 .unwrap()
9141 .effective_row_stride()
9142 .unwrap();
9143 fill(
9144 mem.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
9145 mem_stride,
9146 fmt,
9147 w,
9148 h,
9149 );
9150 let cpu_dst = TensorDyn::image(
9151 w,
9152 h,
9153 PixelFormat::Rgba,
9154 DType::U8,
9155 None,
9156 edgefirst_tensor::CpuAccess::ReadWrite,
9157 )
9158 .unwrap();
9159 let (r, _s, cpu_dst) = convert_img(
9160 &mut cpu,
9161 mem,
9162 cpu_dst,
9163 Rotation::None,
9164 Flip::None,
9165 Crop::no_crop(),
9166 );
9167 r.unwrap_or_else(|e| panic!("CPU {fmt:?}->{w}x{h}->RGBA: {e}"));
9168
9169 let Some(mut ios) = crate::test_support::gpu_buffer_allocated(
9173 TensorDyn::image(
9174 pool_w,
9175 pool_h,
9176 PixelFormat::Grey,
9177 DType::U8,
9178 Some(TensorMemory::DmaBuf),
9179 edgefirst_tensor::CpuAccess::ReadWrite,
9180 ),
9181 "R8 pool IOSurface",
9182 ) else {
9183 return;
9184 };
9185 ios.configure_image(w, h, fmt)
9186 .unwrap_or_else(|e| panic!("configure_image {fmt:?} {w}x{h} on pool: {e}"));
9187 let ios_stride = ios
9188 .as_typed::<u8>()
9189 .unwrap()
9190 .effective_row_stride()
9191 .unwrap();
9192 assert!(
9193 ios_stride > ew,
9194 "{fmt:?} {w}x{h}: pool stride {ios_stride} should exceed even width {ew} \
9195 (test must exercise padding)"
9196 );
9197 fill(
9198 ios.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
9199 ios_stride,
9200 fmt,
9201 w,
9202 h,
9203 );
9204
9205 let gpu_dst = TensorDyn::image(
9206 w,
9207 h,
9208 PixelFormat::Rgba,
9209 DType::U8,
9210 Some(TensorMemory::DmaBuf),
9211 edgefirst_tensor::CpuAccess::ReadWrite,
9212 )
9213 .unwrap();
9214 let (r, _s, gpu_dst) = convert_img(
9215 &mut gpu,
9216 ios,
9217 gpu_dst,
9218 Rotation::None,
9219 Flip::None,
9220 Crop::no_crop(),
9221 );
9222 r.unwrap_or_else(|e| {
9223 panic!("GPU {fmt:?}->{w}x{h}->RGBA (pool surface) on ANGLE: {e}")
9224 });
9225
9226 let cs = cpu_dst
9227 .as_typed::<u8>()
9228 .unwrap()
9229 .effective_row_stride()
9230 .unwrap();
9231 let cmap = cpu_dst.as_typed::<u8>().unwrap().map().unwrap();
9232 let cb = cmap.as_slice();
9233 let gs = gpu_dst
9234 .as_typed::<u8>()
9235 .unwrap()
9236 .effective_row_stride()
9237 .unwrap();
9238 let gmap = gpu_dst.as_typed::<u8>().unwrap().map().unwrap();
9239 let gb = gmap.as_slice();
9240 let mut max_d = 0i16;
9241 for y in 0..h {
9242 for x in 0..w {
9243 for c in 0..3 {
9244 let cv = cb[y * cs + x * 4 + c] as i16;
9245 let gv = gb[y * gs + x * 4 + c] as i16;
9246 max_d = max_d.max((cv - gv).abs());
9247 }
9248 }
9249 }
9250 assert!(
9251 max_d <= 3,
9252 "{fmt:?} {w}x{h}: GPU(pool surface) vs CPU RGBA max channel diff {max_d} > 3"
9253 );
9254 }
9255 }
9256 }
9257
9258 #[test]
9259 #[cfg(target_os = "macos")]
9260 #[cfg(feature = "opengl")]
9261 fn test_yuyv_to_rgba_opengl_macos() {
9262 let mut proc = match GLProcessorThreaded::new(None) {
9263 Ok(p) => p,
9264 Err(e) => {
9265 crate::test_support::report_skip(&format!(
9266 "{} — GL engine init failed ({e:?}). \
9267 Install ANGLE via `brew install startergo/angle/angle` \
9268 and re-sign per README.md § macOS GPU Acceleration to \
9269 run this test.",
9270 function!()
9271 ));
9272 return;
9273 }
9274 };
9275
9276 let src = load_bytes_to_tensor(
9277 1280,
9278 720,
9279 PixelFormat::Yuyv,
9280 Some(TensorMemory::DmaBuf),
9281 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9282 )
9283 .unwrap();
9284
9285 let dst = TensorDyn::image(
9286 1280,
9287 720,
9288 PixelFormat::Rgba,
9289 DType::U8,
9290 Some(TensorMemory::DmaBuf),
9291 edgefirst_tensor::CpuAccess::ReadWrite,
9292 )
9293 .unwrap();
9294
9295 let (result, _src, dst) = convert_img(
9296 &mut proc,
9297 src,
9298 dst,
9299 Rotation::None,
9300 Flip::None,
9301 Crop::no_crop(),
9302 );
9303 result.unwrap();
9304
9305 let target_image = TensorDyn::image(
9306 1280,
9307 720,
9308 PixelFormat::Rgba,
9309 DType::U8,
9310 None,
9311 edgefirst_tensor::CpuAccess::ReadWrite,
9312 )
9313 .unwrap();
9314 target_image
9315 .as_typed::<u8>()
9316 .unwrap()
9317 .map()
9318 .unwrap()
9319 .as_mut_slice()
9320 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
9321
9322 compare_images(&dst, &target_image, 0.98, function!());
9327 }
9328
9329 #[test]
9348 #[cfg(target_os = "macos")]
9349 #[cfg(feature = "opengl")]
9350 fn test_yuyv_to_rgba_opengl_macos_multi_resolution() {
9351 let mut proc = match GLProcessorThreaded::new(None) {
9352 Ok(p) => p,
9353 Err(e) => {
9354 crate::test_support::report_skip(&format!(
9355 "{} — GL engine init failed ({e:?})",
9356 function!()
9357 ));
9358 return;
9359 }
9360 };
9361
9362 for (w, h) in [(64usize, 32usize), (3840, 2160)] {
9363 let bytes_per_row = w * 2;
9366 let mut yuyv = vec![0u8; bytes_per_row * h];
9367 for chunk in yuyv.as_chunks_mut::<4>().0 {
9368 chunk[0] = 128; chunk[1] = 128; chunk[2] = 128; chunk[3] = 128; }
9373
9374 let src =
9375 load_bytes_to_tensor(w, h, PixelFormat::Yuyv, Some(TensorMemory::DmaBuf), &yuyv)
9376 .unwrap();
9377
9378 let dst = TensorDyn::image(
9379 w,
9380 h,
9381 PixelFormat::Rgba,
9382 DType::U8,
9383 Some(TensorMemory::DmaBuf),
9384 edgefirst_tensor::CpuAccess::ReadWrite,
9385 )
9386 .unwrap();
9387
9388 let (result, _src, dst) = convert_img(
9389 &mut proc,
9390 src,
9391 dst,
9392 Rotation::None,
9393 Flip::None,
9394 Crop::no_crop(),
9395 );
9396 result.expect("GL convert should succeed at this resolution");
9397
9398 let dst_u8 = dst.as_typed::<u8>().unwrap();
9403 let dst_map = dst_u8.map().unwrap();
9404 let dst_bytes = dst_map.as_slice();
9405 assert_eq!(dst_bytes.len(), w * h * 4, "RGBA byte count");
9406 for px in dst_bytes.as_chunks::<4>().0 {
9407 for (i, &c) in px[..3].iter().enumerate() {
9408 assert!(
9409 (120..=140).contains(&c),
9410 "{}: channel {i} = {c} (expected ~128 ±12) at {w}×{h}",
9411 function!(),
9412 );
9413 }
9414 assert_eq!(px[3], 255, "alpha must be 1.0");
9415 }
9416 }
9417 }
9418
9419 #[test]
9429 #[cfg(target_os = "macos")]
9430 #[cfg(feature = "opengl")]
9431 fn test_macos_gl_pbuffer_cache_reuses_surfaces() {
9432 let mut proc = match GLProcessorThreaded::new(None) {
9433 Ok(p) => p,
9434 Err(e) => {
9435 crate::test_support::report_skip(&format!(
9436 "{} — GL engine init failed ({e:?})",
9437 function!()
9438 ));
9439 return;
9440 }
9441 };
9442
9443 let mut yuyv = vec![0u8; 64 * 32 * 2];
9445 for chunk in yuyv.as_chunks_mut::<4>().0 {
9446 chunk[0] = 200;
9447 chunk[1] = 100;
9448 chunk[2] = 200;
9449 chunk[3] = 156;
9450 }
9451 let src =
9452 load_bytes_to_tensor(64, 32, PixelFormat::Yuyv, Some(TensorMemory::DmaBuf), &yuyv)
9453 .unwrap();
9454 let dst = TensorDyn::image(
9455 64,
9456 32,
9457 PixelFormat::Rgba,
9458 DType::U8,
9459 Some(TensorMemory::DmaBuf),
9460 edgefirst_tensor::CpuAccess::ReadWrite,
9461 )
9462 .unwrap();
9463
9464 let (r1, src, dst) = convert_img(
9465 &mut proc,
9466 src,
9467 dst,
9468 Rotation::None,
9469 Flip::None,
9470 Crop::no_crop(),
9471 );
9472 r1.unwrap();
9473 let first: Vec<u8> = dst
9474 .as_typed::<u8>()
9475 .unwrap()
9476 .map()
9477 .unwrap()
9478 .as_slice()
9479 .to_vec();
9480
9481 let (r2, _src, dst) = convert_img(
9482 &mut proc,
9483 src,
9484 dst,
9485 Rotation::None,
9486 Flip::None,
9487 Crop::no_crop(),
9488 );
9489 r2.unwrap();
9490 let second: Vec<u8> = dst
9491 .as_typed::<u8>()
9492 .unwrap()
9493 .map()
9494 .unwrap()
9495 .as_slice()
9496 .to_vec();
9497
9498 assert_eq!(first, second, "cache-hit conversion must be deterministic");
9499 }
9500
9501 #[test]
9509 #[cfg(target_os = "macos")]
9510 #[cfg(feature = "opengl")]
9511 fn test_macos_gl_pbuffer_cache_steady_state() {
9512 let mut proc = match GLProcessorThreaded::new(None) {
9513 Ok(p) => p,
9514 Err(e) => {
9515 crate::test_support::report_skip(&format!(
9516 "{} — GL engine init failed ({e:?})",
9517 function!()
9518 ));
9519 return;
9520 }
9521 };
9522
9523 let (w, h) = (64usize, 32usize);
9524 const POOL: usize = 3;
9525 const FRAMES: usize = 100;
9526
9527 let yuyv = vec![128u8; w * h * 2];
9528 let pool: Vec<TensorDyn> = (0..POOL)
9529 .map(|_| {
9530 load_bytes_to_tensor(w, h, PixelFormat::Yuyv, Some(TensorMemory::DmaBuf), &yuyv)
9531 .unwrap()
9532 })
9533 .collect();
9534 let mut dst = TensorDyn::image(
9535 w,
9536 h,
9537 PixelFormat::Rgba,
9538 DType::U8,
9539 Some(TensorMemory::DmaBuf),
9540 edgefirst_tensor::CpuAccess::ReadWrite,
9541 )
9542 .unwrap();
9543
9544 for src in pool.iter().cycle().take(POOL * 2) {
9546 proc.convert(src, &mut dst, None).unwrap();
9547 }
9548 let warm = proc.egl_cache_stats().unwrap();
9549
9550 for src in pool.iter().cycle().take(FRAMES) {
9551 proc.convert(src, &mut dst, None).unwrap();
9552 }
9553 let steady = proc.egl_cache_stats().unwrap();
9554
9555 assert_eq!(
9556 warm.total_misses(),
9557 steady.total_misses(),
9558 "steady-state loop created new imports (warm {warm:?}, steady {steady:?})"
9559 );
9560 let hits = |s: &GlCacheStats| s.src.hits + s.dst.hits + s.nv_r8.hits + s.nv_uv.hits;
9561 assert!(
9562 hits(&steady) - hits(&warm) >= FRAMES as u64,
9563 "expected at least {FRAMES} import-cache hits over the loop, got {}",
9564 hits(&steady) - hits(&warm)
9565 );
9566 }
9567
9568 #[test]
9580 #[cfg(target_os = "macos")]
9581 #[cfg(feature = "opengl")]
9582 fn test_macos_gl_f16_planar_is_gl_backed() {
9583 let mut proc = ImageProcessor::new().expect("ImageProcessor");
9584 let Some(ref gl) = proc.opengl else {
9585 crate::test_support::report_skip(&format!("{} — GL backend unavailable", function!()));
9586 return;
9587 };
9588 if !gl.supported_render_dtypes().f16 {
9589 crate::test_support::report_skip(&format!(
9590 "{} — configuration lacks F16 color-buffer support",
9591 function!()
9592 ));
9593 return;
9594 }
9595 let stats_before = gl.egl_cache_stats().expect("cache stats");
9596
9597 let src = TensorDyn::image(
9598 1280,
9599 720,
9600 PixelFormat::Nv12,
9601 DType::U8,
9602 Some(TensorMemory::DmaBuf),
9603 edgefirst_tensor::CpuAccess::ReadWrite,
9604 )
9605 .unwrap();
9606 {
9607 let t = src.as_typed::<u8>().unwrap();
9608 let mut m = t.map().unwrap();
9609 for (i, b) in m.as_mut_slice().iter_mut().enumerate() {
9610 *b = ((i * 31) % 211) as u8;
9611 }
9612 }
9613 let mut dst = TensorDyn::image(
9614 640,
9615 640,
9616 PixelFormat::PlanarRgb,
9617 DType::F16,
9618 Some(TensorMemory::DmaBuf),
9619 edgefirst_tensor::CpuAccess::ReadWrite,
9620 )
9621 .unwrap();
9622
9623 proc.convert(
9624 &src,
9625 &mut dst,
9626 Some(crate::ConvertOptions::new().crop(Crop::letterbox([114, 114, 114, 255]))),
9627 )
9628 .expect("F16 capability reported but the NV12→PlanarF16 convert failed");
9629 let stats_after = proc
9630 .opengl
9631 .as_ref()
9632 .expect("GL backend present")
9633 .egl_cache_stats()
9634 .expect("cache stats");
9635 assert!(
9640 stats_after.total_misses() >= stats_before.total_misses() + 2,
9641 "convert succeeded but the GL engine did not import both the \
9642 source and the F16 destination — the work did not (fully) run \
9643 on the GL backend (silent CPU fallback); misses before={} after={}",
9644 stats_before.total_misses(),
9645 stats_after.total_misses()
9646 );
9647 }
9648
9649 #[test]
9655 #[cfg(feature = "opengl")]
9656 fn test_nv12_to_planar_f16_fused_engine_vs_cpu() {
9657 let mut gl = match ImageProcessor::with_config(ImageProcessorConfig {
9658 backend: ComputeBackend::OpenGl,
9659 ..Default::default()
9660 }) {
9661 Ok(p) if p.opengl.is_some() => p,
9662 _ => {
9663 crate::test_support::report_skip(&format!(
9664 "{} — GL backend unavailable",
9665 function!()
9666 ));
9667 return;
9668 }
9669 };
9670 if !gl
9671 .opengl
9672 .as_ref()
9673 .map(|g| g.supported_render_dtypes().f16)
9674 .unwrap_or(false)
9675 {
9676 crate::test_support::report_skip(&format!("{} — no F16 render support", function!()));
9677 return;
9678 }
9679 let mem = if crate::test_support::is_gpu_buffer_available() {
9680 TensorMemory::DmaBuf
9681 } else {
9682 crate::test_support::report_skip(&format!("{} — no zero-copy buffers", function!()));
9683 return;
9684 };
9685
9686 let src = TensorDyn::image(
9687 1280,
9688 720,
9689 PixelFormat::Nv12,
9690 DType::U8,
9691 Some(mem),
9692 edgefirst_tensor::CpuAccess::ReadWrite,
9693 )
9694 .unwrap();
9695 {
9696 let t = src.as_typed::<u8>().unwrap();
9702 let mut m = t.map().unwrap();
9703 let buf = m.as_mut_slice();
9704 let (w, h) = (1280usize, 720usize);
9705 for y in 0..h {
9706 for x in 0..w {
9707 buf[y * w + x] = ((x * 255) / w) as u8; }
9709 }
9710 for y in 0..(h / 2) {
9711 for x in 0..(w / 2) {
9712 let o = h * w + y * w + 2 * x;
9713 buf[o] = ((y * 255) / (h / 2)) as u8; buf[o + 1] = (((x + y) * 255) / (w / 2 + h / 2)) as u8; }
9716 }
9717 }
9718 let crop = Crop::letterbox([114, 114, 114, 255]);
9719 let mut gl_dst = TensorDyn::image(
9720 640,
9721 640,
9722 PixelFormat::PlanarRgb,
9723 DType::F16,
9724 Some(mem),
9725 edgefirst_tensor::CpuAccess::ReadWrite,
9726 )
9727 .unwrap();
9728 match gl.opengl.as_mut().expect("GL backend present").convert(
9735 &src,
9736 &mut gl_dst,
9737 Some(crate::ConvertOptions::new().crop(crop)),
9738 ) {
9739 Ok(()) => {}
9740 Err(crate::Error::NotSupported(msg)) => {
9741 crate::test_support::report_skip(&format!("{} — {msg}", function!()));
9742 return;
9743 }
9744 Err(e) => panic!("fused NV12→PlanarF16 GL convert: {e}"),
9745 }
9746
9747 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
9748 backend: ComputeBackend::Cpu,
9749 ..Default::default()
9750 })
9751 .unwrap();
9752 let mut cpu_dst = TensorDyn::image(
9753 640,
9754 640,
9755 PixelFormat::PlanarRgb,
9756 DType::F16,
9757 Some(TensorMemory::Mem),
9758 edgefirst_tensor::CpuAccess::ReadWrite,
9759 )
9760 .unwrap();
9761 cpu.convert(
9762 &src,
9763 &mut cpu_dst,
9764 Some(crate::ConvertOptions::new().crop(crop)),
9765 )
9766 .expect("CPU reference convert");
9767
9768 let g = gl_dst
9769 .as_typed::<half::f16>()
9770 .unwrap()
9771 .map()
9772 .unwrap()
9773 .as_slice()
9774 .to_vec();
9775 let c = cpu_dst
9776 .as_typed::<half::f16>()
9777 .unwrap()
9778 .map()
9779 .unwrap()
9780 .as_slice()
9781 .to_vec();
9782 assert_eq!(g.len(), c.len());
9783 let mut max_diff = 0.0f32;
9784 let mut max_at = 0usize;
9785 for (i, (a, b)) in g.iter().zip(c.iter()).enumerate() {
9786 let d = (a.to_f32() - b.to_f32()).abs();
9787 if d > max_diff {
9788 max_diff = d;
9789 max_at = i;
9790 }
9791 }
9792 let (plane, rem) = (max_at / (640 * 640), max_at % (640 * 640));
9794 let (row, col) = (rem / 640, rem % 640);
9795 eprintln!(
9796 "fused-vs-cpu: max_diff={max_diff} at plane={plane} row={row} col={col} \
9797 gl={} cpu={}",
9798 g[max_at].to_f32(),
9799 c[max_at].to_f32()
9800 );
9801 assert!(
9804 max_diff <= 4.0 / 255.0 + 1e-3,
9805 "fused NV12→PlanarF16 diverges from CPU reference: max_diff={max_diff}"
9806 );
9807 }
9808
9809 #[test]
9819 #[cfg(feature = "opengl")]
9820 fn test_zero_copy_src_to_mem_dst_gl_direct() {
9821 let mut proc = match ImageProcessor::new() {
9822 Ok(p) if p.opengl.is_some() => p,
9823 _ => {
9824 crate::test_support::report_skip(&format!(
9825 "{} — GL backend unavailable",
9826 function!()
9827 ));
9828 return;
9829 }
9830 };
9831 if !crate::test_support::is_gpu_buffer_available() {
9832 crate::test_support::report_skip(&format!("{} — no zero-copy buffers", function!()));
9833 return;
9834 }
9835
9836 let src = TensorDyn::image(
9837 1280,
9838 720,
9839 PixelFormat::Rgba,
9840 DType::U8,
9841 Some(TensorMemory::DmaBuf),
9842 edgefirst_tensor::CpuAccess::ReadWrite,
9843 )
9844 .unwrap();
9845 {
9846 let t = src.as_typed::<u8>().unwrap();
9847 let mut m = t.map().unwrap();
9848 for (i, b) in m.as_mut_slice().iter_mut().enumerate() {
9849 *b = ((i * 31) % 211) as u8;
9850 }
9851 }
9852 let mut gl_dst = TensorDyn::image(
9853 1280,
9854 720,
9855 PixelFormat::Bgra,
9856 DType::U8,
9857 Some(TensorMemory::Mem),
9858 edgefirst_tensor::CpuAccess::ReadWrite,
9859 )
9860 .unwrap();
9861 proc.opengl
9862 .as_mut()
9863 .expect("GL backend present")
9864 .convert(&src, &mut gl_dst, None)
9865 .expect("zero-copy src → heap dst GL convert");
9866
9867 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
9868 backend: ComputeBackend::Cpu,
9869 ..Default::default()
9870 })
9871 .unwrap();
9872 let mut cpu_dst = TensorDyn::image(
9873 1280,
9874 720,
9875 PixelFormat::Bgra,
9876 DType::U8,
9877 Some(TensorMemory::Mem),
9878 edgefirst_tensor::CpuAccess::ReadWrite,
9879 )
9880 .unwrap();
9881 cpu.convert(&src, &mut cpu_dst, None)
9882 .expect("CPU reference convert");
9883
9884 let g = gl_dst
9885 .as_typed::<u8>()
9886 .unwrap()
9887 .map()
9888 .unwrap()
9889 .as_slice()
9890 .to_vec();
9891 let c = cpu_dst
9892 .as_typed::<u8>()
9893 .unwrap()
9894 .map()
9895 .unwrap()
9896 .as_slice()
9897 .to_vec();
9898 assert_eq!(g.len(), c.len());
9899 let max_diff = g
9900 .iter()
9901 .zip(c.iter())
9902 .map(|(a, b)| a.abs_diff(*b))
9903 .max()
9904 .unwrap();
9905 assert!(
9908 max_diff <= 2,
9909 "zero-copy src → heap dst diverges from CPU reference: max_diff={max_diff}"
9910 );
9911 }
9912
9913 #[test]
9914 #[cfg(target_os = "linux")]
9915 fn test_yuyv_to_rgb_g2d() {
9916 if !is_g2d_available() {
9917 crate::test_support::report_skip(
9918 "test_yuyv_to_rgb_g2d - G2D library (libg2d.so.2) not available",
9919 );
9920 return;
9921 }
9922 if !is_dma_available() {
9923 crate::test_support::report_skip("test_yuyv_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
9924 return;
9925 }
9926
9927 let src = load_bytes_to_tensor(
9928 1280,
9929 720,
9930 PixelFormat::Yuyv,
9931 None,
9932 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9933 )
9934 .unwrap();
9935
9936 let g2d_dst = TensorDyn::image(
9937 1280,
9938 720,
9939 PixelFormat::Rgb,
9940 DType::U8,
9941 Some(TensorMemory::DmaBuf),
9942 edgefirst_tensor::CpuAccess::ReadWrite,
9943 )
9944 .unwrap();
9945 let mut g2d_converter = G2DProcessor::new().unwrap();
9946
9947 let (result, src, g2d_dst) = convert_img(
9948 &mut g2d_converter,
9949 src,
9950 g2d_dst,
9951 Rotation::None,
9952 Flip::None,
9953 Crop::no_crop(),
9954 );
9955 result.unwrap();
9956
9957 let cpu_dst = TensorDyn::image(
9958 1280,
9959 720,
9960 PixelFormat::Rgb,
9961 DType::U8,
9962 None,
9963 edgefirst_tensor::CpuAccess::ReadWrite,
9964 )
9965 .unwrap();
9966 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
9967
9968 let (result, _src, cpu_dst) = convert_img(
9969 &mut cpu_converter,
9970 src,
9971 cpu_dst,
9972 Rotation::None,
9973 Flip::None,
9974 Crop::no_crop(),
9975 );
9976 result.unwrap();
9977
9978 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
9984 }
9985
9986 #[test]
9987 #[cfg(target_os = "linux")]
9988 fn test_yuyv_to_yuyv_resize_g2d() {
9989 if !is_g2d_available() {
9990 crate::test_support::report_skip(
9991 "test_yuyv_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available",
9992 );
9993 return;
9994 }
9995 if !is_dma_available() {
9996 crate::test_support::report_skip("test_yuyv_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
9997 return;
9998 }
9999
10000 let src = load_bytes_to_tensor(
10001 1280,
10002 720,
10003 PixelFormat::Yuyv,
10004 None,
10005 &edgefirst_bench::testdata::read("camera720p.yuyv"),
10006 )
10007 .unwrap();
10008
10009 let g2d_dst = TensorDyn::image(
10010 600,
10011 400,
10012 PixelFormat::Yuyv,
10013 DType::U8,
10014 Some(TensorMemory::DmaBuf),
10015 edgefirst_tensor::CpuAccess::ReadWrite,
10016 )
10017 .unwrap();
10018 let mut g2d_converter = G2DProcessor::new().unwrap();
10019
10020 let (result, src, g2d_dst) = convert_img(
10021 &mut g2d_converter,
10022 src,
10023 g2d_dst,
10024 Rotation::None,
10025 Flip::None,
10026 Crop::no_crop(),
10027 );
10028 result.unwrap();
10029
10030 let cpu_dst = TensorDyn::image(
10031 600,
10032 400,
10033 PixelFormat::Yuyv,
10034 DType::U8,
10035 None,
10036 edgefirst_tensor::CpuAccess::ReadWrite,
10037 )
10038 .unwrap();
10039 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
10040
10041 let (result, _src, cpu_dst) = convert_img(
10042 &mut cpu_converter,
10043 src,
10044 cpu_dst,
10045 Rotation::None,
10046 Flip::None,
10047 Crop::no_crop(),
10048 );
10049 result.unwrap();
10050
10051 eprintln!(
10058 "WARNING: G2D has poor colorimetry support — YUYV resize diverges from the \
10059 CPU reference (~0.85 similarity); threshold held at 0.85, not 0.95."
10060 );
10061 compare_images_convert_to_rgb(&g2d_dst, &cpu_dst, 0.85, function!());
10062 }
10063
10064 #[test]
10065 fn test_yuyv_to_rgba_resize_cpu() {
10066 let src = load_bytes_to_tensor(
10067 1280,
10068 720,
10069 PixelFormat::Yuyv,
10070 None,
10071 &edgefirst_bench::testdata::read("camera720p.yuyv"),
10072 )
10073 .unwrap();
10074
10075 let (dst_width, dst_height) = (960, 540);
10076
10077 let dst = TensorDyn::image(
10078 dst_width,
10079 dst_height,
10080 PixelFormat::Rgba,
10081 DType::U8,
10082 None,
10083 edgefirst_tensor::CpuAccess::ReadWrite,
10084 )
10085 .unwrap();
10086 let mut cpu_converter = CPUProcessor::new();
10087
10088 let (result, _src, dst) = convert_img(
10089 &mut cpu_converter,
10090 src,
10091 dst,
10092 Rotation::None,
10093 Flip::None,
10094 Crop::no_crop(),
10095 );
10096 result.unwrap();
10097
10098 let dst_target = TensorDyn::image(
10099 dst_width,
10100 dst_height,
10101 PixelFormat::Rgba,
10102 DType::U8,
10103 None,
10104 edgefirst_tensor::CpuAccess::ReadWrite,
10105 )
10106 .unwrap();
10107 let src_target = load_bytes_to_tensor(
10108 1280,
10109 720,
10110 PixelFormat::Rgba,
10111 None,
10112 &edgefirst_bench::testdata::read("camera720p.rgba"),
10113 )
10114 .unwrap();
10115 let (result, _src_target, dst_target) = convert_img(
10116 &mut cpu_converter,
10117 src_target,
10118 dst_target,
10119 Rotation::None,
10120 Flip::None,
10121 Crop::no_crop(),
10122 );
10123 result.unwrap();
10124
10125 compare_images(&dst, &dst_target, 0.98, function!());
10128 }
10129
10130 #[test]
10131 #[cfg(target_os = "linux")]
10132 fn test_yuyv_to_rgba_crop_flip_g2d() {
10133 if !is_g2d_available() {
10134 crate::test_support::report_skip(
10135 "test_yuyv_to_rgba_crop_flip_g2d - G2D library (libg2d.so.2) not available",
10136 );
10137 return;
10138 }
10139 if !is_dma_available() {
10140 crate::test_support::report_skip("test_yuyv_to_rgba_crop_flip_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
10141 return;
10142 }
10143
10144 let src = load_bytes_to_tensor(
10145 1280,
10146 720,
10147 PixelFormat::Yuyv,
10148 Some(TensorMemory::DmaBuf),
10149 &edgefirst_bench::testdata::read("camera720p.yuyv"),
10150 )
10151 .unwrap();
10152
10153 let (dst_width, dst_height) = (640, 640);
10154
10155 let dst_g2d = TensorDyn::image(
10156 dst_width,
10157 dst_height,
10158 PixelFormat::Rgba,
10159 DType::U8,
10160 Some(TensorMemory::DmaBuf),
10161 edgefirst_tensor::CpuAccess::ReadWrite,
10162 )
10163 .unwrap();
10164 let mut g2d_converter = G2DProcessor::new().unwrap();
10165 let crop = Crop::new().with_source(Some(Region::new(20, 15, 400, 300)));
10166
10167 let (result, src, dst_g2d) = convert_img(
10168 &mut g2d_converter,
10169 src,
10170 dst_g2d,
10171 Rotation::None,
10172 Flip::Horizontal,
10173 crop,
10174 );
10175 result.unwrap();
10176
10177 let dst_cpu = TensorDyn::image(
10178 dst_width,
10179 dst_height,
10180 PixelFormat::Rgba,
10181 DType::U8,
10182 Some(TensorMemory::DmaBuf),
10183 edgefirst_tensor::CpuAccess::ReadWrite,
10184 )
10185 .unwrap();
10186 let mut cpu_converter = CPUProcessor::new();
10187
10188 let (result, _src, dst_cpu) = convert_img(
10189 &mut cpu_converter,
10190 src,
10191 dst_cpu,
10192 Rotation::None,
10193 Flip::Horizontal,
10194 crop,
10195 );
10196 result.unwrap();
10197 compare_images(&dst_g2d, &dst_cpu, 0.98, function!());
10203 }
10204
10205 #[test]
10206 #[cfg(any(
10207 target_os = "linux",
10208 target_os = "macos",
10209 target_os = "ios",
10210 target_os = "android",
10211 target_os = "windows"
10212 ))]
10213 #[cfg(feature = "opengl")]
10214 fn test_yuyv_to_rgba_crop_flip_opengl() {
10215 if !is_opengl_available() {
10216 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
10217 return;
10218 }
10219
10220 if !crate::test_support::is_gpu_buffer_available() {
10221 crate::test_support::report_skip(&format!(
10222 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
10223 function!()
10224 ));
10225 return;
10226 }
10227
10228 let src = load_bytes_to_tensor(
10229 1280,
10230 720,
10231 PixelFormat::Yuyv,
10232 Some(TensorMemory::DmaBuf),
10233 &edgefirst_bench::testdata::read("camera720p.yuyv"),
10234 )
10235 .unwrap();
10236
10237 let (dst_width, dst_height) = (640, 640);
10238
10239 let dst_gl = TensorDyn::image(
10240 dst_width,
10241 dst_height,
10242 PixelFormat::Rgba,
10243 DType::U8,
10244 Some(TensorMemory::DmaBuf),
10245 edgefirst_tensor::CpuAccess::ReadWrite,
10246 )
10247 .unwrap();
10248 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
10249 let crop = Crop::new().with_source(Some(Region::new(20, 15, 400, 300)));
10250
10251 let (result, src, dst_gl) = convert_img(
10252 &mut gl_converter,
10253 src,
10254 dst_gl,
10255 Rotation::None,
10256 Flip::Horizontal,
10257 crop,
10258 );
10259 if let Err(crate::Error::NotSupported(msg)) = &result {
10261 crate::test_support::report_skip(&format!("{} — {msg}", function!()));
10262 return;
10263 }
10264 result.unwrap();
10265
10266 let dst_cpu = TensorDyn::image(
10267 dst_width,
10268 dst_height,
10269 PixelFormat::Rgba,
10270 DType::U8,
10271 Some(TensorMemory::DmaBuf),
10272 edgefirst_tensor::CpuAccess::ReadWrite,
10273 )
10274 .unwrap();
10275 let mut cpu_converter = CPUProcessor::new();
10276
10277 let (result, _src, dst_cpu) = convert_img(
10278 &mut cpu_converter,
10279 src,
10280 dst_cpu,
10281 Rotation::None,
10282 Flip::Horizontal,
10283 crop,
10284 );
10285 result.unwrap();
10286 compare_images(&dst_gl, &dst_cpu, 0.98, function!());
10291 }
10292
10293 #[test]
10294 fn test_vyuy_to_rgba_cpu() {
10295 let file = edgefirst_bench::testdata::read("camera720p.vyuy").to_vec();
10296 let src = TensorDyn::image(
10297 1280,
10298 720,
10299 PixelFormat::Vyuy,
10300 DType::U8,
10301 None,
10302 edgefirst_tensor::CpuAccess::ReadWrite,
10303 )
10304 .unwrap();
10305 src.as_typed::<u8>()
10306 .unwrap()
10307 .map()
10308 .unwrap()
10309 .as_mut_slice()
10310 .copy_from_slice(&file);
10311
10312 let dst = TensorDyn::image(
10313 1280,
10314 720,
10315 PixelFormat::Rgba,
10316 DType::U8,
10317 None,
10318 edgefirst_tensor::CpuAccess::ReadWrite,
10319 )
10320 .unwrap();
10321 let mut cpu_converter = CPUProcessor::new();
10322
10323 let (result, _src, dst) = convert_img(
10324 &mut cpu_converter,
10325 src,
10326 dst,
10327 Rotation::None,
10328 Flip::None,
10329 Crop::no_crop(),
10330 );
10331 result.unwrap();
10332
10333 let target_image = TensorDyn::image(
10334 1280,
10335 720,
10336 PixelFormat::Rgba,
10337 DType::U8,
10338 None,
10339 edgefirst_tensor::CpuAccess::ReadWrite,
10340 )
10341 .unwrap();
10342 target_image
10343 .as_typed::<u8>()
10344 .unwrap()
10345 .map()
10346 .unwrap()
10347 .as_mut_slice()
10348 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
10349
10350 compare_images(&dst, &target_image, 0.98, function!());
10353 }
10354
10355 #[test]
10356 fn test_vyuy_to_rgb_cpu() {
10357 let file = edgefirst_bench::testdata::read("camera720p.vyuy").to_vec();
10358 let src = TensorDyn::image(
10359 1280,
10360 720,
10361 PixelFormat::Vyuy,
10362 DType::U8,
10363 None,
10364 edgefirst_tensor::CpuAccess::ReadWrite,
10365 )
10366 .unwrap();
10367 src.as_typed::<u8>()
10368 .unwrap()
10369 .map()
10370 .unwrap()
10371 .as_mut_slice()
10372 .copy_from_slice(&file);
10373
10374 let dst = TensorDyn::image(
10375 1280,
10376 720,
10377 PixelFormat::Rgb,
10378 DType::U8,
10379 None,
10380 edgefirst_tensor::CpuAccess::ReadWrite,
10381 )
10382 .unwrap();
10383 let mut cpu_converter = CPUProcessor::new();
10384
10385 let (result, _src, dst) = convert_img(
10386 &mut cpu_converter,
10387 src,
10388 dst,
10389 Rotation::None,
10390 Flip::None,
10391 Crop::no_crop(),
10392 );
10393 result.unwrap();
10394
10395 let target_image = TensorDyn::image(
10396 1280,
10397 720,
10398 PixelFormat::Rgb,
10399 DType::U8,
10400 None,
10401 edgefirst_tensor::CpuAccess::ReadWrite,
10402 )
10403 .unwrap();
10404 target_image
10405 .as_typed::<u8>()
10406 .unwrap()
10407 .map()
10408 .unwrap()
10409 .as_mut_slice()
10410 .as_chunks_mut::<3>()
10411 .0
10412 .iter_mut()
10413 .zip(
10414 edgefirst_bench::testdata::read("camera720p.rgba")
10415 .as_chunks::<4>()
10416 .0,
10417 )
10418 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
10419
10420 compare_images(&dst, &target_image, 0.98, function!());
10423 }
10424
10425 #[test]
10426 #[cfg(target_os = "linux")]
10427 #[ignore = "G2D does not support VYUY; re-enable when hardware support is added"]
10428 fn test_vyuy_to_rgba_g2d() {
10429 if !is_g2d_available() {
10430 crate::test_support::report_skip(
10431 "test_vyuy_to_rgba_g2d - G2D library (libg2d.so.2) not available",
10432 );
10433 return;
10434 }
10435 if !is_dma_available() {
10436 crate::test_support::report_skip("test_vyuy_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
10437 return;
10438 }
10439
10440 let src = load_bytes_to_tensor(
10441 1280,
10442 720,
10443 PixelFormat::Vyuy,
10444 None,
10445 &edgefirst_bench::testdata::read("camera720p.vyuy"),
10446 )
10447 .unwrap();
10448
10449 let dst = TensorDyn::image(
10450 1280,
10451 720,
10452 PixelFormat::Rgba,
10453 DType::U8,
10454 Some(TensorMemory::DmaBuf),
10455 edgefirst_tensor::CpuAccess::ReadWrite,
10456 )
10457 .unwrap();
10458 let mut g2d_converter = G2DProcessor::new().unwrap();
10459
10460 let (result, _src, dst) = convert_img(
10461 &mut g2d_converter,
10462 src,
10463 dst,
10464 Rotation::None,
10465 Flip::None,
10466 Crop::no_crop(),
10467 );
10468 match result {
10469 Err(Error::G2D(_)) => {
10470 crate::test_support::report_skip(
10471 "test_vyuy_to_rgba_g2d - G2D does not support PixelFormat::Vyuy format",
10472 );
10473 return;
10474 }
10475 r => r.unwrap(),
10476 }
10477
10478 let target_image = TensorDyn::image(
10479 1280,
10480 720,
10481 PixelFormat::Rgba,
10482 DType::U8,
10483 None,
10484 edgefirst_tensor::CpuAccess::ReadWrite,
10485 )
10486 .unwrap();
10487 target_image
10488 .as_typed::<u8>()
10489 .unwrap()
10490 .map()
10491 .unwrap()
10492 .as_mut_slice()
10493 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
10494
10495 compare_images(&dst, &target_image, 0.98, function!());
10499 }
10500
10501 #[test]
10502 #[cfg(target_os = "linux")]
10503 #[ignore = "G2D does not support VYUY; re-enable when hardware support is added"]
10504 fn test_vyuy_to_rgb_g2d() {
10505 if !is_g2d_available() {
10506 crate::test_support::report_skip(
10507 "test_vyuy_to_rgb_g2d - G2D library (libg2d.so.2) not available",
10508 );
10509 return;
10510 }
10511 if !is_dma_available() {
10512 crate::test_support::report_skip("test_vyuy_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
10513 return;
10514 }
10515
10516 let src = load_bytes_to_tensor(
10517 1280,
10518 720,
10519 PixelFormat::Vyuy,
10520 None,
10521 &edgefirst_bench::testdata::read("camera720p.vyuy"),
10522 )
10523 .unwrap();
10524
10525 let g2d_dst = TensorDyn::image(
10526 1280,
10527 720,
10528 PixelFormat::Rgb,
10529 DType::U8,
10530 Some(TensorMemory::DmaBuf),
10531 edgefirst_tensor::CpuAccess::ReadWrite,
10532 )
10533 .unwrap();
10534 let mut g2d_converter = G2DProcessor::new().unwrap();
10535
10536 let (result, src, g2d_dst) = convert_img(
10537 &mut g2d_converter,
10538 src,
10539 g2d_dst,
10540 Rotation::None,
10541 Flip::None,
10542 Crop::no_crop(),
10543 );
10544 match result {
10545 Err(Error::G2D(_)) => {
10546 crate::test_support::report_skip(
10547 "test_vyuy_to_rgb_g2d - G2D does not support PixelFormat::Vyuy format",
10548 );
10549 return;
10550 }
10551 r => r.unwrap(),
10552 }
10553
10554 let cpu_dst = TensorDyn::image(
10555 1280,
10556 720,
10557 PixelFormat::Rgb,
10558 DType::U8,
10559 None,
10560 edgefirst_tensor::CpuAccess::ReadWrite,
10561 )
10562 .unwrap();
10563 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
10564
10565 let (result, _src, cpu_dst) = convert_img(
10566 &mut cpu_converter,
10567 src,
10568 cpu_dst,
10569 Rotation::None,
10570 Flip::None,
10571 Crop::no_crop(),
10572 );
10573 result.unwrap();
10574
10575 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
10581 }
10582
10583 #[test]
10584 #[cfg(any(
10585 target_os = "linux",
10586 target_os = "macos",
10587 target_os = "ios",
10588 target_os = "android",
10589 target_os = "windows"
10590 ))]
10591 #[cfg(feature = "opengl")]
10592 fn test_vyuy_to_rgba_opengl() {
10593 if !is_opengl_available() {
10594 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
10595 return;
10596 }
10597 if !crate::test_support::is_gpu_buffer_available() {
10598 crate::test_support::report_skip(&format!(
10599 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
10600 function!()
10601 ));
10602 return;
10603 }
10604
10605 let expect = if cfg!(target_os = "linux") {
10607 crate::test_support::Expect::Succeeds
10608 } else {
10609 crate::test_support::Expect::MayFail("no zero-copy VYUY mapping on this platform")
10610 };
10611 let Some(src) = crate::test_support::allocated(
10612 load_bytes_to_tensor(
10613 1280,
10614 720,
10615 PixelFormat::Vyuy,
10616 Some(TensorMemory::DmaBuf),
10617 &edgefirst_bench::testdata::read("camera720p.vyuy"),
10618 ),
10619 "zero-copy VYUY source",
10620 expect,
10621 ) else {
10622 return;
10623 };
10624
10625 let dst = TensorDyn::image(
10626 1280,
10627 720,
10628 PixelFormat::Rgba,
10629 DType::U8,
10630 Some(TensorMemory::DmaBuf),
10631 edgefirst_tensor::CpuAccess::ReadWrite,
10632 )
10633 .unwrap();
10634 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
10635
10636 let (result, _src, dst) = convert_img(
10637 &mut gl_converter,
10638 src,
10639 dst,
10640 Rotation::None,
10641 Flip::None,
10642 Crop::no_crop(),
10643 );
10644 match result {
10645 Err(Error::NotSupported(_)) => {
10646 crate::test_support::report_skip(&format!(
10647 "{} - OpenGL does not support PixelFormat::Vyuy DMA format",
10648 function!()
10649 ));
10650 return;
10651 }
10652 r => r.unwrap(),
10653 }
10654
10655 let target_image = TensorDyn::image(
10656 1280,
10657 720,
10658 PixelFormat::Rgba,
10659 DType::U8,
10660 None,
10661 edgefirst_tensor::CpuAccess::ReadWrite,
10662 )
10663 .unwrap();
10664 target_image
10665 .as_typed::<u8>()
10666 .unwrap()
10667 .map()
10668 .unwrap()
10669 .as_mut_slice()
10670 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
10671
10672 compare_images(&dst, &target_image, 0.98, function!());
10676 }
10677
10678 #[test]
10679 fn test_nv12_to_rgba_cpu() {
10680 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
10681 let src = TensorDyn::image(
10682 1280,
10683 720,
10684 PixelFormat::Nv12,
10685 DType::U8,
10686 None,
10687 edgefirst_tensor::CpuAccess::ReadWrite,
10688 )
10689 .unwrap();
10690 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
10691 .copy_from_slice(&file);
10692
10693 let dst = TensorDyn::image(
10694 1280,
10695 720,
10696 PixelFormat::Rgba,
10697 DType::U8,
10698 None,
10699 edgefirst_tensor::CpuAccess::ReadWrite,
10700 )
10701 .unwrap();
10702 let mut cpu_converter = CPUProcessor::new();
10703
10704 let (result, _src, dst) = convert_img(
10705 &mut cpu_converter,
10706 src,
10707 dst,
10708 Rotation::None,
10709 Flip::None,
10710 Crop::no_crop(),
10711 );
10712 result.unwrap();
10713
10714 let target_image = crate::load_image_test_helper(
10715 &edgefirst_bench::testdata::read("zidane.jpg"),
10716 Some(PixelFormat::Rgba),
10717 None,
10718 )
10719 .unwrap();
10720
10721 compare_images(&dst, &target_image, 0.95, function!());
10726 }
10727
10728 #[test]
10729 fn test_nv12_odd_height_to_rgb_cpu() {
10730 let mut src = TensorDyn::image(
10741 8,
10742 5,
10743 PixelFormat::Nv12,
10744 DType::U8,
10745 Some(TensorMemory::Mem),
10746 edgefirst_tensor::CpuAccess::ReadWrite,
10747 )
10748 .unwrap();
10749 assert_eq!(src.shape(), &[8, 8]);
10750 assert_eq!((src.width(), src.height()), (Some(8), Some(5)));
10751 src.as_typed::<u8>()
10752 .unwrap()
10753 .map()
10754 .unwrap()
10755 .as_mut_slice()
10756 .fill(128);
10757 src.set_colorimetry(Some(
10761 edgefirst_tensor::Colorimetry::default()
10762 .with_encoding(edgefirst_tensor::ColorEncoding::Bt601)
10763 .with_range(edgefirst_tensor::ColorRange::Full),
10764 ));
10765
10766 let dst = TensorDyn::image(
10767 8,
10768 5,
10769 PixelFormat::Rgb,
10770 DType::U8,
10771 Some(TensorMemory::Mem),
10772 edgefirst_tensor::CpuAccess::ReadWrite,
10773 )
10774 .unwrap();
10775 let mut cpu_converter = CPUProcessor::new();
10776 let (result, _src, dst) = convert_img(
10777 &mut cpu_converter,
10778 src,
10779 dst,
10780 Rotation::None,
10781 Flip::None,
10782 Crop::no_crop(),
10783 );
10784 result.unwrap();
10785
10786 assert_eq!((dst.width(), dst.height()), (Some(8), Some(5)));
10787 let map = dst.as_typed::<u8>().unwrap().map().unwrap();
10788 for (i, &b) in map.as_slice().iter().enumerate() {
10789 assert!(
10790 (b as i16 - 128).abs() <= 2,
10791 "pixel byte {i} = {b}, expected ~128 for neutral-grey NV12"
10792 );
10793 }
10794 }
10795
10796 #[test]
10797 fn test_nv24_to_rgb_cpu() {
10798 let mut src = TensorDyn::image(
10804 8,
10805 4,
10806 PixelFormat::Nv24,
10807 DType::U8,
10808 Some(TensorMemory::Mem),
10809 edgefirst_tensor::CpuAccess::ReadWrite,
10810 )
10811 .unwrap();
10812 assert_eq!(src.shape(), &[12, 8]);
10813 assert_eq!((src.width(), src.height()), (Some(8), Some(4)));
10814 src.as_typed::<u8>()
10815 .unwrap()
10816 .map()
10817 .unwrap()
10818 .as_mut_slice()
10819 .fill(128);
10820 src.set_colorimetry(Some(
10823 edgefirst_tensor::Colorimetry::default()
10824 .with_encoding(edgefirst_tensor::ColorEncoding::Bt601)
10825 .with_range(edgefirst_tensor::ColorRange::Full),
10826 ));
10827
10828 let dst = TensorDyn::image(
10829 8,
10830 4,
10831 PixelFormat::Rgb,
10832 DType::U8,
10833 Some(TensorMemory::Mem),
10834 edgefirst_tensor::CpuAccess::ReadWrite,
10835 )
10836 .unwrap();
10837 let mut cpu_converter = CPUProcessor::new();
10838 let (result, _src, dst) = convert_img(
10839 &mut cpu_converter,
10840 src,
10841 dst,
10842 Rotation::None,
10843 Flip::None,
10844 Crop::no_crop(),
10845 );
10846 result.unwrap();
10847
10848 assert_eq!((dst.width(), dst.height()), (Some(8), Some(4)));
10849 let map = dst.as_typed::<u8>().unwrap().map().unwrap();
10850 for (i, &b) in map.as_slice().iter().enumerate() {
10851 assert!(
10852 (b as i16 - 128).abs() <= 2,
10853 "pixel byte {i} = {b}, expected ~128 for neutral-grey NV24"
10854 );
10855 }
10856 }
10857
10858 #[test]
10859 fn cpu_nv12_to_rgb_respects_tagged_bt2020() {
10860 fn decode_tagged(enc: edgefirst_tensor::ColorEncoding) -> [u8; 3] {
10868 let mut src = TensorDyn::image(
10869 8,
10870 4,
10871 PixelFormat::Nv12,
10872 DType::U8,
10873 Some(TensorMemory::Mem),
10874 edgefirst_tensor::CpuAccess::ReadWrite,
10875 )
10876 .unwrap();
10877 assert_eq!(src.shape(), &[6, 8]);
10879 {
10880 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
10881 let buf = map.as_mut_slice();
10882 buf[..32].fill(120); for px in buf[32..].as_chunks_mut::<2>().0 {
10884 px[0] = 180; px[1] = 64; }
10887 }
10888 src.set_colorimetry(Some(
10891 edgefirst_tensor::Colorimetry::default()
10892 .with_encoding(enc)
10893 .with_range(edgefirst_tensor::ColorRange::Limited),
10894 ));
10895 let dst = TensorDyn::image(
10896 8,
10897 4,
10898 PixelFormat::Rgb,
10899 DType::U8,
10900 Some(TensorMemory::Mem),
10901 edgefirst_tensor::CpuAccess::ReadWrite,
10902 )
10903 .unwrap();
10904 let mut cpu = CPUProcessor::new();
10905 let (result, _src, dst) = convert_img(
10906 &mut cpu,
10907 src,
10908 dst,
10909 Rotation::None,
10910 Flip::None,
10911 Crop::no_crop(),
10912 );
10913 result.unwrap();
10914 let map = dst.as_typed::<u8>().unwrap().map().unwrap();
10915 let s = map.as_slice();
10916 [s[0], s[1], s[2]]
10917 }
10918
10919 let bt601 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt601);
10920 let bt709 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt709);
10921 let bt2020 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt2020);
10922
10923 assert_ne!(
10924 bt2020, bt601,
10925 "BT.2020 must decode differently from BT.601 ({bt2020:?} vs {bt601:?})"
10926 );
10927 assert_ne!(
10928 bt2020, bt709,
10929 "BT.2020 must decode differently from BT.709 ({bt2020:?} vs {bt709:?})"
10930 );
10931 assert_ne!(
10932 bt601, bt709,
10933 "BT.601 must decode differently from BT.709 ({bt601:?} vs {bt709:?})"
10934 );
10935 }
10936
10937 #[test]
10938 fn test_nv12_to_rgb_cpu() {
10939 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
10940 let src = TensorDyn::image(
10941 1280,
10942 720,
10943 PixelFormat::Nv12,
10944 DType::U8,
10945 None,
10946 edgefirst_tensor::CpuAccess::ReadWrite,
10947 )
10948 .unwrap();
10949 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
10950 .copy_from_slice(&file);
10951
10952 let dst = TensorDyn::image(
10953 1280,
10954 720,
10955 PixelFormat::Rgb,
10956 DType::U8,
10957 None,
10958 edgefirst_tensor::CpuAccess::ReadWrite,
10959 )
10960 .unwrap();
10961 let mut cpu_converter = CPUProcessor::new();
10962
10963 let (result, _src, dst) = convert_img(
10964 &mut cpu_converter,
10965 src,
10966 dst,
10967 Rotation::None,
10968 Flip::None,
10969 Crop::no_crop(),
10970 );
10971 result.unwrap();
10972
10973 let target_image = crate::load_image_test_helper(
10974 &edgefirst_bench::testdata::read("zidane.jpg"),
10975 Some(PixelFormat::Rgb),
10976 None,
10977 )
10978 .unwrap();
10979
10980 compare_images(&dst, &target_image, 0.95, function!());
10985 }
10986
10987 #[test]
10988 fn test_nv12_to_grey_cpu() {
10989 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
10990 let src = TensorDyn::image(
10991 1280,
10992 720,
10993 PixelFormat::Nv12,
10994 DType::U8,
10995 None,
10996 edgefirst_tensor::CpuAccess::ReadWrite,
10997 )
10998 .unwrap();
10999 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
11000 .copy_from_slice(&file);
11001
11002 let dst = TensorDyn::image(
11003 1280,
11004 720,
11005 PixelFormat::Grey,
11006 DType::U8,
11007 None,
11008 edgefirst_tensor::CpuAccess::ReadWrite,
11009 )
11010 .unwrap();
11011 let mut cpu_converter = CPUProcessor::new();
11012
11013 let (result, _src, dst) = convert_img(
11014 &mut cpu_converter,
11015 src,
11016 dst,
11017 Rotation::None,
11018 Flip::None,
11019 Crop::no_crop(),
11020 );
11021 result.unwrap();
11022
11023 let target_image = crate::load_image_test_helper(
11024 &edgefirst_bench::testdata::read("zidane.jpg"),
11025 Some(PixelFormat::Grey),
11026 None,
11027 )
11028 .unwrap();
11029
11030 compare_images(&dst, &target_image, 0.95, function!());
11035 }
11036
11037 #[test]
11038 fn test_nv12_to_yuyv_cpu() {
11039 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
11040 let src = TensorDyn::image(
11041 1280,
11042 720,
11043 PixelFormat::Nv12,
11044 DType::U8,
11045 None,
11046 edgefirst_tensor::CpuAccess::ReadWrite,
11047 )
11048 .unwrap();
11049 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
11050 .copy_from_slice(&file);
11051
11052 let dst = TensorDyn::image(
11053 1280,
11054 720,
11055 PixelFormat::Yuyv,
11056 DType::U8,
11057 None,
11058 edgefirst_tensor::CpuAccess::ReadWrite,
11059 )
11060 .unwrap();
11061 let mut cpu_converter = CPUProcessor::new();
11062
11063 let (result, _src, dst) = convert_img(
11064 &mut cpu_converter,
11065 src,
11066 dst,
11067 Rotation::None,
11068 Flip::None,
11069 Crop::no_crop(),
11070 );
11071 result.unwrap();
11072
11073 let target_image = crate::load_image_test_helper(
11074 &edgefirst_bench::testdata::read("zidane.jpg"),
11075 Some(PixelFormat::Rgb),
11076 None,
11077 )
11078 .unwrap();
11079
11080 compare_images_convert_to_rgb(&dst, &target_image, 0.95, function!());
11085 }
11086
11087 #[test]
11088 fn test_cpu_resize_nv16() {
11089 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
11090 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
11091
11092 let cpu_nv16_dst = TensorDyn::image(
11093 640,
11094 640,
11095 PixelFormat::Nv16,
11096 DType::U8,
11097 None,
11098 edgefirst_tensor::CpuAccess::ReadWrite,
11099 )
11100 .unwrap();
11101 let cpu_rgb_dst = TensorDyn::image(
11102 640,
11103 640,
11104 PixelFormat::Rgb,
11105 DType::U8,
11106 None,
11107 edgefirst_tensor::CpuAccess::ReadWrite,
11108 )
11109 .unwrap();
11110 let mut cpu_converter = CPUProcessor::new();
11111 let crop = Crop::letterbox([255, 128, 0, 255]);
11112
11113 let (result, src, cpu_nv16_dst) = convert_img(
11114 &mut cpu_converter,
11115 src,
11116 cpu_nv16_dst,
11117 Rotation::None,
11118 Flip::None,
11119 crop,
11120 );
11121 result.unwrap();
11122
11123 let (result, _src, cpu_rgb_dst) = convert_img(
11124 &mut cpu_converter,
11125 src,
11126 cpu_rgb_dst,
11127 Rotation::None,
11128 Flip::None,
11129 crop,
11130 );
11131 result.unwrap();
11132 compare_images_convert_to_rgb(
11133 &cpu_nv16_dst,
11134 &cpu_rgb_dst,
11135 yuv_threshold(0.99),
11136 function!(),
11137 );
11138 }
11139
11140 fn yuv_threshold(balanced: f64) -> f64 {
11157 #[cfg(target_arch = "aarch64")]
11158 {
11159 if !std::arch::is_aarch64_feature_detected!("rdm") {
11160 return balanced - 0.01;
11161 }
11162 }
11163 balanced
11164 }
11165
11166 fn load_bytes_to_tensor(
11167 width: usize,
11168 height: usize,
11169 format: PixelFormat,
11170 memory: Option<TensorMemory>,
11171 bytes: &[u8],
11172 ) -> Result<TensorDyn, Error> {
11173 let src = TensorDyn::image(
11174 width,
11175 height,
11176 format,
11177 DType::U8,
11178 memory,
11179 edgefirst_tensor::CpuAccess::ReadWrite,
11180 )?;
11181 src.as_typed::<u8>()
11182 .unwrap()
11183 .map()?
11184 .as_mut_slice()
11185 .copy_from_slice(bytes);
11186 Ok(src)
11187 }
11188
11189 fn compare_images(img1: &TensorDyn, img2: &TensorDyn, threshold: f64, name: &str) {
11197 assert_eq!(img1.height(), img2.height(), "Heights differ");
11198 assert_eq!(img1.width(), img2.width(), "Widths differ");
11199 assert_eq!(
11200 img1.format().unwrap(),
11201 img2.format().unwrap(),
11202 "PixelFormat differ"
11203 );
11204 assert!(
11205 matches!(
11206 img1.format().unwrap(),
11207 PixelFormat::Rgb
11208 | PixelFormat::Rgba
11209 | PixelFormat::Grey
11210 | PixelFormat::PlanarRgb
11211 | PixelFormat::PlanarRgba
11212 ),
11213 "format must be Rgb or Rgba for comparison"
11214 );
11215
11216 fn flat(img: &TensorDyn) -> Vec<u8> {
11223 let t = img.as_typed::<u8>().expect("u8 image");
11224 let mut out = vec![0u8; t.shape().iter().product::<usize>()];
11225 t.copy_to_flat(&mut out).expect("compact the padded rows");
11226 out
11227 }
11228
11229 let image1 = match img1.format().unwrap() {
11230 PixelFormat::Rgb => image::RgbImage::from_vec(
11231 img1.width().unwrap() as u32,
11232 img1.height().unwrap() as u32,
11233 flat(img1),
11234 )
11235 .unwrap(),
11236 PixelFormat::Rgba => image::RgbaImage::from_vec(
11237 img1.width().unwrap() as u32,
11238 img1.height().unwrap() as u32,
11239 flat(img1),
11240 )
11241 .unwrap()
11242 .convert(),
11243 PixelFormat::Grey => image::GrayImage::from_vec(
11244 img1.width().unwrap() as u32,
11245 img1.height().unwrap() as u32,
11246 flat(img1),
11247 )
11248 .unwrap()
11249 .convert(),
11250 PixelFormat::PlanarRgb | PixelFormat::PlanarRgba => image::GrayImage::from_vec(
11251 img1.width().unwrap() as u32,
11252 (img1.height().unwrap() * img1.format().unwrap().channels()) as u32,
11253 flat(img1),
11254 )
11255 .unwrap()
11256 .convert(),
11257 _ => return,
11258 };
11259
11260 let image2 = match img2.format().unwrap() {
11261 PixelFormat::Rgb => image::RgbImage::from_vec(
11262 img2.width().unwrap() as u32,
11263 img2.height().unwrap() as u32,
11264 flat(img2),
11265 )
11266 .unwrap(),
11267 PixelFormat::Rgba => image::RgbaImage::from_vec(
11268 img2.width().unwrap() as u32,
11269 img2.height().unwrap() as u32,
11270 flat(img2),
11271 )
11272 .unwrap()
11273 .convert(),
11274 PixelFormat::Grey => image::GrayImage::from_vec(
11275 img2.width().unwrap() as u32,
11276 img2.height().unwrap() as u32,
11277 flat(img2),
11278 )
11279 .unwrap()
11280 .convert(),
11281 PixelFormat::PlanarRgb | PixelFormat::PlanarRgba => image::GrayImage::from_vec(
11282 img2.width().unwrap() as u32,
11283 (img2.height().unwrap() * img2.format().unwrap().channels()) as u32,
11284 flat(img2),
11285 )
11286 .unwrap()
11287 .convert(),
11288 _ => return,
11289 };
11290
11291 let similarity = image_compare::rgb_similarity_structure(
11292 &image_compare::Algorithm::RootMeanSquared,
11293 &image1,
11294 &image2,
11295 )
11296 .expect("Image Comparison failed");
11297 if similarity.score < threshold {
11298 similarity
11301 .image
11302 .to_color_map()
11303 .save(format!("{name}.png"))
11304 .unwrap();
11305 panic!(
11306 "{name}: converted image and target image have similarity score too low: {} < {}",
11307 similarity.score, threshold
11308 )
11309 }
11310 }
11311
11312 fn compare_images_convert_to_rgb(
11313 img1: &TensorDyn,
11314 img2: &TensorDyn,
11315 threshold: f64,
11316 name: &str,
11317 ) {
11318 assert_eq!(img1.height(), img2.height(), "Heights differ");
11319 assert_eq!(img1.width(), img2.width(), "Widths differ");
11320
11321 let mut img_rgb1 = TensorDyn::image(
11322 img1.width().unwrap(),
11323 img1.height().unwrap(),
11324 PixelFormat::Rgb,
11325 DType::U8,
11326 Some(TensorMemory::Mem),
11327 edgefirst_tensor::CpuAccess::ReadWrite,
11328 )
11329 .unwrap();
11330 let mut img_rgb2 = TensorDyn::image(
11331 img1.width().unwrap(),
11332 img1.height().unwrap(),
11333 PixelFormat::Rgb,
11334 DType::U8,
11335 Some(TensorMemory::Mem),
11336 edgefirst_tensor::CpuAccess::ReadWrite,
11337 )
11338 .unwrap();
11339 let mut __cv = CPUProcessor::default();
11340 let r1 = __cv.convert(img1, &mut img_rgb1, None);
11341 let r2 = __cv.convert(img2, &mut img_rgb2, None);
11342 if r1.is_err() || r2.is_err() {
11343 let w = img1.width().unwrap() as u32;
11345 let data1 = img1.as_typed::<u8>().unwrap().map().unwrap().to_vec();
11346 let data2 = img2.as_typed::<u8>().unwrap().map().unwrap().to_vec();
11347 let h1 = (data1.len() as u32) / w;
11348 let h2 = (data2.len() as u32) / w;
11349 let g1 = image::GrayImage::from_vec(w, h1, data1).unwrap();
11350 let g2 = image::GrayImage::from_vec(w, h2, data2).unwrap();
11351 let similarity = image_compare::gray_similarity_structure(
11352 &image_compare::Algorithm::RootMeanSquared,
11353 &g1,
11354 &g2,
11355 )
11356 .expect("Image Comparison failed");
11357 if similarity.score < threshold {
11358 panic!(
11359 "{name}: converted image and target image have similarity score too low: {} < {}",
11360 similarity.score, threshold
11361 )
11362 }
11363 return;
11364 }
11365
11366 let image1 = image::RgbImage::from_vec(
11367 img_rgb1.width().unwrap() as u32,
11368 img_rgb1.height().unwrap() as u32,
11369 img_rgb1.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
11370 )
11371 .unwrap();
11372
11373 let image2 = image::RgbImage::from_vec(
11374 img_rgb2.width().unwrap() as u32,
11375 img_rgb2.height().unwrap() as u32,
11376 img_rgb2.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
11377 )
11378 .unwrap();
11379
11380 let similarity = image_compare::rgb_similarity_structure(
11381 &image_compare::Algorithm::RootMeanSquared,
11382 &image1,
11383 &image2,
11384 )
11385 .expect("Image Comparison failed");
11386 if similarity.score < threshold {
11387 similarity
11390 .image
11391 .to_color_map()
11392 .save(format!("{name}.png"))
11393 .unwrap();
11394 panic!(
11395 "{name}: converted image and target image have similarity score too low: {} < {}",
11396 similarity.score, threshold
11397 )
11398 }
11399 }
11400
11401 #[test]
11406 fn test_nv12_image_creation() {
11407 let width = 640;
11408 let height = 480;
11409 let img = TensorDyn::image(
11410 width,
11411 height,
11412 PixelFormat::Nv12,
11413 DType::U8,
11414 None,
11415 edgefirst_tensor::CpuAccess::ReadWrite,
11416 )
11417 .unwrap();
11418
11419 assert_eq!(img.width(), Some(width));
11420 assert_eq!(img.height(), Some(height));
11421 assert_eq!(img.format().unwrap(), PixelFormat::Nv12);
11422 assert_eq!(
11424 img.as_typed::<u8>().unwrap().shape(),
11425 &[height * 3 / 2, width]
11426 );
11427 }
11428
11429 #[test]
11430 fn test_nv12_channels() {
11431 let img = TensorDyn::image(
11432 640,
11433 480,
11434 PixelFormat::Nv12,
11435 DType::U8,
11436 None,
11437 edgefirst_tensor::CpuAccess::ReadWrite,
11438 )
11439 .unwrap();
11440 assert_eq!(img.format().unwrap().channels(), 1);
11442 }
11443
11444 #[test]
11449 fn test_tensor_set_format_planar() {
11450 let mut tensor = Tensor::<u8>::new(&[3, 480, 640], None, None).unwrap();
11451 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
11452 assert_eq!(tensor.format(), Some(PixelFormat::PlanarRgb));
11453 assert_eq!(tensor.width(), Some(640));
11454 assert_eq!(tensor.height(), Some(480));
11455 }
11456
11457 #[test]
11458 fn test_tensor_set_format_interleaved() {
11459 let mut tensor = Tensor::<u8>::new(&[480, 640, 4], None, None).unwrap();
11460 tensor.set_format(PixelFormat::Rgba).unwrap();
11461 assert_eq!(tensor.format(), Some(PixelFormat::Rgba));
11462 assert_eq!(tensor.width(), Some(640));
11463 assert_eq!(tensor.height(), Some(480));
11464 }
11465
11466 #[test]
11467 fn test_tensordyn_image_rgb() {
11468 let img = TensorDyn::image(
11469 640,
11470 480,
11471 PixelFormat::Rgb,
11472 DType::U8,
11473 None,
11474 edgefirst_tensor::CpuAccess::ReadWrite,
11475 )
11476 .unwrap();
11477 assert_eq!(img.width(), Some(640));
11478 assert_eq!(img.height(), Some(480));
11479 assert_eq!(img.format(), Some(PixelFormat::Rgb));
11480 }
11481
11482 #[test]
11483 fn test_tensordyn_image_planar_rgb() {
11484 let img = TensorDyn::image(
11485 640,
11486 480,
11487 PixelFormat::PlanarRgb,
11488 DType::U8,
11489 None,
11490 edgefirst_tensor::CpuAccess::ReadWrite,
11491 )
11492 .unwrap();
11493 assert_eq!(img.width(), Some(640));
11494 assert_eq!(img.height(), Some(480));
11495 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
11496 }
11497
11498 #[test]
11499 fn test_rgb_int8_format() {
11500 let img = TensorDyn::image(
11502 1280,
11503 720,
11504 PixelFormat::Rgb,
11505 DType::I8,
11506 Some(TensorMemory::Mem),
11507 edgefirst_tensor::CpuAccess::ReadWrite,
11508 )
11509 .unwrap();
11510 assert_eq!(img.width(), Some(1280));
11511 assert_eq!(img.height(), Some(720));
11512 assert_eq!(img.format(), Some(PixelFormat::Rgb));
11513 assert_eq!(img.dtype(), DType::I8);
11514 }
11515
11516 #[test]
11517 fn test_planar_rgb_int8_format() {
11518 let img = TensorDyn::image(
11519 1280,
11520 720,
11521 PixelFormat::PlanarRgb,
11522 DType::I8,
11523 Some(TensorMemory::Mem),
11524 edgefirst_tensor::CpuAccess::ReadWrite,
11525 )
11526 .unwrap();
11527 assert_eq!(img.width(), Some(1280));
11528 assert_eq!(img.height(), Some(720));
11529 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
11530 assert_eq!(img.dtype(), DType::I8);
11531 }
11532
11533 #[test]
11534 fn test_rgb_from_tensor() {
11535 let mut tensor = Tensor::<u8>::new(&[720, 1280, 3], None, None).unwrap();
11536 tensor.set_format(PixelFormat::Rgb).unwrap();
11537 let img = TensorDyn::from(tensor);
11538 assert_eq!(img.width(), Some(1280));
11539 assert_eq!(img.height(), Some(720));
11540 assert_eq!(img.format(), Some(PixelFormat::Rgb));
11541 }
11542
11543 #[test]
11544 fn test_planar_rgb_from_tensor() {
11545 let mut tensor = Tensor::<u8>::new(&[3, 720, 1280], None, None).unwrap();
11546 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
11547 let img = TensorDyn::from(tensor);
11548 assert_eq!(img.width(), Some(1280));
11549 assert_eq!(img.height(), Some(720));
11550 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
11551 }
11552
11553 #[test]
11554 fn test_dtype_determines_int8() {
11555 let u8_img = TensorDyn::image(
11557 64,
11558 64,
11559 PixelFormat::Rgb,
11560 DType::U8,
11561 None,
11562 edgefirst_tensor::CpuAccess::ReadWrite,
11563 )
11564 .unwrap();
11565 let i8_img = TensorDyn::image(
11566 64,
11567 64,
11568 PixelFormat::Rgb,
11569 DType::I8,
11570 None,
11571 edgefirst_tensor::CpuAccess::ReadWrite,
11572 )
11573 .unwrap();
11574 assert_eq!(u8_img.dtype(), DType::U8);
11575 assert_eq!(i8_img.dtype(), DType::I8);
11576 }
11577
11578 #[test]
11579 fn test_pixel_layout_packed_vs_planar() {
11580 assert_eq!(PixelFormat::Rgb.layout(), PixelLayout::Packed);
11582 assert_eq!(PixelFormat::Rgba.layout(), PixelLayout::Packed);
11583 assert_eq!(PixelFormat::PlanarRgb.layout(), PixelLayout::Planar);
11584 assert_eq!(PixelFormat::Nv12.layout(), PixelLayout::SemiPlanar);
11585 }
11586
11587 #[cfg(target_os = "linux")]
11598 #[cfg(feature = "opengl")]
11599 #[test]
11600 fn pbo_reports_why_it_cannot_pin() {
11601 use edgefirst_tensor::{Error, TensorTrait};
11602
11603 let converter = ImageProcessor::new().unwrap();
11604 let is_pbo = converter
11605 .opengl
11606 .as_ref()
11607 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
11608 if !is_pbo {
11609 crate::test_support::report_skip(
11610 "pbo_reports_why_it_cannot_pin - transfer backend is not PBO",
11611 );
11612 return;
11613 }
11614
11615 let t = converter
11616 .create_image(
11617 64,
11618 64,
11619 PixelFormat::Rgba,
11620 DType::U8,
11621 None,
11622 edgefirst_tensor::CpuAccess::ReadWrite,
11623 )
11624 .unwrap();
11625 assert_eq!(t.as_typed::<u8>().unwrap().memory(), TensorMemory::Pbo);
11626 let t = t.as_typed::<u8>().unwrap();
11627
11628 match t.pin_host(edgefirst_tensor::CpuAccess::ReadWrite) {
11629 Err(Error::NotImplemented(m)) => {
11630 assert!(
11631 m.contains("glMapBufferRange"),
11632 "must name the constraint: {m}"
11633 );
11634 assert!(m.contains("map()"), "must point at the supported API: {m}");
11635 }
11636 other => panic!("expected an explained NotImplemented, got {other:?}"),
11637 }
11638 assert!(matches!(
11640 t.sync_for_cpu(edgefirst_tensor::CpuAccess::Read),
11641 Err(Error::NotImplemented(_))
11642 ));
11643
11644 let g = t.map().expect("map must work where pin cannot");
11646 assert!(!g.as_slice().is_empty());
11647 }
11648
11649 #[cfg(target_os = "linux")]
11653 #[cfg(feature = "opengl")]
11654 #[test]
11655 fn test_convert_pbo_to_pbo() {
11656 let mut converter = ImageProcessor::new().unwrap();
11657
11658 let is_pbo = converter
11660 .opengl
11661 .as_ref()
11662 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
11663 if !is_pbo {
11664 crate::test_support::report_skip(
11665 "test_convert_pbo_to_pbo - transfer backend is not PBO",
11666 );
11667 return;
11668 }
11669
11670 let src_w = 640;
11671 let src_h = 480;
11672 let dst_w = 320;
11673 let dst_h = 240;
11674
11675 let pbo_src = converter
11677 .create_image(
11678 src_w,
11679 src_h,
11680 PixelFormat::Rgba,
11681 DType::U8,
11682 None,
11683 edgefirst_tensor::CpuAccess::ReadWrite,
11684 )
11685 .unwrap();
11686 assert_eq!(
11687 pbo_src.as_typed::<u8>().unwrap().memory(),
11688 TensorMemory::Pbo,
11689 "create_image should produce a PBO tensor"
11690 );
11691
11692 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
11694 let jpeg_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
11695
11696 let mem_src = TensorDyn::image(
11698 src_w,
11699 src_h,
11700 PixelFormat::Rgba,
11701 DType::U8,
11702 Some(TensorMemory::Mem),
11703 edgefirst_tensor::CpuAccess::ReadWrite,
11704 )
11705 .unwrap();
11706 let (result, _jpeg_src, mem_src) = convert_img(
11707 &mut CPUProcessor::new(),
11708 jpeg_src,
11709 mem_src,
11710 Rotation::None,
11711 Flip::None,
11712 Crop::no_crop(),
11713 );
11714 result.unwrap();
11715
11716 {
11718 let src_data = mem_src.as_typed::<u8>().unwrap().map().unwrap();
11719 let mut pbo_map = pbo_src.as_typed::<u8>().unwrap().map().unwrap();
11720 pbo_map.copy_from_slice(&src_data);
11721 }
11722
11723 let pbo_dst = converter
11725 .create_image(
11726 dst_w,
11727 dst_h,
11728 PixelFormat::Rgba,
11729 DType::U8,
11730 None,
11731 edgefirst_tensor::CpuAccess::ReadWrite,
11732 )
11733 .unwrap();
11734 assert_eq!(
11735 pbo_dst.as_typed::<u8>().unwrap().memory(),
11736 TensorMemory::Pbo
11737 );
11738
11739 let mut pbo_dst = pbo_dst;
11741 let result = converter.convert(&pbo_src, &mut pbo_dst, None);
11742 result.unwrap();
11743
11744 let cpu_dst = TensorDyn::image(
11746 dst_w,
11747 dst_h,
11748 PixelFormat::Rgba,
11749 DType::U8,
11750 Some(TensorMemory::Mem),
11751 edgefirst_tensor::CpuAccess::ReadWrite,
11752 )
11753 .unwrap();
11754 let (result, _mem_src, cpu_dst) = convert_img(
11755 &mut CPUProcessor::new(),
11756 mem_src,
11757 cpu_dst,
11758 Rotation::None,
11759 Flip::None,
11760 Crop::no_crop(),
11761 );
11762 result.unwrap();
11763
11764 let pbo_dst_img = {
11765 let mut __t = pbo_dst.into_u8().unwrap();
11766 __t.set_format(PixelFormat::Rgba).unwrap();
11767 TensorDyn::from(__t)
11768 };
11769 compare_images(&pbo_dst_img, &cpu_dst, 0.95, function!());
11770 log::info!("test_convert_pbo_to_pbo: PASS — PBO-to-PBO convert matches CPU reference");
11771 }
11772
11773 #[test]
11774 fn test_image_bgra() {
11775 let img = TensorDyn::image(
11776 640,
11777 480,
11778 PixelFormat::Bgra,
11779 DType::U8,
11780 Some(edgefirst_tensor::TensorMemory::Mem),
11781 edgefirst_tensor::CpuAccess::ReadWrite,
11782 )
11783 .unwrap();
11784 assert_eq!(img.width(), Some(640));
11785 assert_eq!(img.height(), Some(480));
11786 assert_eq!(img.format().unwrap().channels(), 4);
11787 assert_eq!(img.format().unwrap(), PixelFormat::Bgra);
11788 }
11789
11790 #[test]
11795 fn test_force_backend_cpu() {
11796 let _lock = acquire_env_lock();
11797 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11798 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
11799 let converter = ImageProcessor::new().unwrap();
11800 assert!(converter.cpu.is_some());
11801 assert_eq!(converter.forced_backend, Some(ForcedBackend::Cpu));
11802 }
11803
11804 #[test]
11805 fn test_force_backend_invalid() {
11806 let _lock = acquire_env_lock();
11807 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11808 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "invalid") };
11809 let result = ImageProcessor::new();
11810 assert!(
11811 matches!(&result, Err(Error::ForcedBackendUnavailable(s)) if s.contains("unknown")),
11812 "invalid backend value should return ForcedBackendUnavailable error: {result:?}"
11813 );
11814 }
11815
11816 #[test]
11817 fn test_force_backend_unset() {
11818 let _lock = acquire_env_lock();
11819 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11820 unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") };
11821 let converter = ImageProcessor::new().unwrap();
11822 assert!(converter.forced_backend.is_none());
11823 }
11824
11825 #[test]
11830 fn test_draw_proto_masks_no_cpu_returns_error() {
11831 let _lock = acquire_env_lock();
11833 let _guard = EnvGuard::snapshot(&[
11834 "EDGEFIRST_FORCE_BACKEND",
11835 "EDGEFIRST_DISABLE_GL",
11836 "EDGEFIRST_DISABLE_G2D",
11837 "EDGEFIRST_DISABLE_CPU",
11838 ]);
11839
11840 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
11842 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
11843 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
11844
11845 let mut converter = ImageProcessor::new().unwrap();
11846 assert!(converter.cpu.is_none(), "CPU should be disabled");
11847
11848 let dst = TensorDyn::image(
11849 640,
11850 480,
11851 PixelFormat::Rgba,
11852 DType::U8,
11853 Some(TensorMemory::Mem),
11854 edgefirst_tensor::CpuAccess::ReadWrite,
11855 )
11856 .unwrap();
11857 let mut dst_dyn = dst;
11858 let det = [DetectBox {
11859 bbox: edgefirst_tensor::BoundingBox {
11860 xmin: 0.1,
11861 ymin: 0.1,
11862 xmax: 0.5,
11863 ymax: 0.5,
11864 },
11865 score: 0.9,
11866 label: 0,
11867 }];
11868 let proto_data = {
11869 use edgefirst_tensor::{Tensor, TensorDyn};
11870 let coeff_t = Tensor::<f32>::from_slice(&[0.5_f32; 4], &[1, 4]).unwrap();
11871 let protos_t =
11872 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
11873 ProtoData {
11874 mask_coefficients: TensorDyn::F32(coeff_t),
11875 protos: TensorDyn::F32(protos_t),
11876 layout: ProtoLayout::Nhwc,
11877 }
11878 };
11879 let result =
11880 converter.draw_proto_masks(&mut dst_dyn, &det, &proto_data, Default::default());
11881 assert!(
11882 matches!(&result, Err(Error::Internal(s)) if s.contains("CPU backend")),
11883 "draw_proto_masks without CPU should return Internal error: {result:?}"
11884 );
11885 }
11886
11887 #[test]
11888 fn test_draw_proto_masks_cpu_fallback_works() {
11889 let _lock = acquire_env_lock();
11892 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11893 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
11894 let mut converter = ImageProcessor::new().unwrap();
11895 assert!(converter.cpu.is_some());
11896
11897 let dst = TensorDyn::image(
11898 64,
11899 64,
11900 PixelFormat::Rgba,
11901 DType::U8,
11902 Some(TensorMemory::Mem),
11903 edgefirst_tensor::CpuAccess::ReadWrite,
11904 )
11905 .unwrap();
11906 let mut dst_dyn = dst;
11907 let det = [DetectBox {
11908 bbox: edgefirst_tensor::BoundingBox {
11909 xmin: 0.1,
11910 ymin: 0.1,
11911 xmax: 0.5,
11912 ymax: 0.5,
11913 },
11914 score: 0.9,
11915 label: 0,
11916 }];
11917 let proto_data = {
11918 use edgefirst_tensor::{Tensor, TensorDyn};
11919 let coeff_t = Tensor::<f32>::from_slice(&[0.5_f32; 4], &[1, 4]).unwrap();
11920 let protos_t =
11921 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
11922 ProtoData {
11923 mask_coefficients: TensorDyn::F32(coeff_t),
11924 protos: TensorDyn::F32(protos_t),
11925 layout: ProtoLayout::Nhwc,
11926 }
11927 };
11928 let result =
11929 converter.draw_proto_masks(&mut dst_dyn, &det, &proto_data, Default::default());
11930 assert!(result.is_ok(), "CPU fallback path should work: {result:?}");
11931 }
11932
11933 fn acquire_env_lock() -> std::sync::MutexGuard<'static, ()> {
11965 use std::sync::{Mutex, OnceLock};
11966 static ENV_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
11967 ENV_MUTEX
11968 .get_or_init(|| Mutex::new(()))
11969 .lock()
11970 .unwrap_or_else(|e| e.into_inner())
11971 }
11972
11973 struct EnvGuard {
11976 vars: Vec<(&'static str, Option<String>)>,
11977 }
11978
11979 impl EnvGuard {
11980 fn snapshot(names: &[&'static str]) -> Self {
11984 Self {
11985 vars: names.iter().map(|&k| (k, std::env::var(k).ok())).collect(),
11986 }
11987 }
11988 }
11989
11990 impl Drop for EnvGuard {
11991 fn drop(&mut self) {
11992 for (k, v) in &self.vars {
11993 match v {
11994 Some(s) => unsafe { std::env::set_var(k, s) },
11995 None => unsafe { std::env::remove_var(k) },
11996 }
11997 }
11998 }
11999 }
12000
12001 #[test]
12004 fn test_interpolation_env_parse() {
12005 let _lock = acquire_env_lock();
12006 let _guard = EnvGuard::snapshot(&["EDGEFIRST_INTERPOLATION"]);
12007 for (value, want) in [
12008 (Some("nearest"), Some(Interpolation::Nearest)),
12009 (Some(" BILINEAR "), Some(Interpolation::Bilinear)),
12010 (Some("lanczos"), None),
12011 (Some(""), None),
12012 (None, None),
12013 ] {
12014 match value {
12015 Some(v) => unsafe { std::env::set_var("EDGEFIRST_INTERPOLATION", v) },
12016 None => unsafe { std::env::remove_var("EDGEFIRST_INTERPOLATION") },
12017 }
12018 assert_eq!(
12019 interpolation_from_env(),
12020 want,
12021 "EDGEFIRST_INTERPOLATION={value:?}"
12022 );
12023 }
12024 }
12025
12026 #[test]
12029 fn test_interpolation_config_and_env_reach_cpu() {
12030 let _lock = acquire_env_lock();
12031 let _guard = EnvGuard::snapshot(&["EDGEFIRST_INTERPOLATION"]);
12032 let mut checker = Vec::with_capacity(8 * 8 * 4);
12033 for y in 0..8 {
12034 for x in 0..8 {
12035 let v = if (x + y) % 2 == 0 { 0u8 } else { 255 };
12036 checker.extend_from_slice(&[v, v, v, 255]);
12037 }
12038 }
12039 let resize = |processor: &mut dyn ImageProcessorTrait| {
12040 let mut src = TensorDyn::image(
12041 8,
12042 8,
12043 PixelFormat::Rgba,
12044 DType::U8,
12045 Some(TensorMemory::Mem),
12046 edgefirst_tensor::CpuAccess::ReadWrite,
12047 )
12048 .unwrap();
12049 src.as_u8_mut()
12050 .unwrap()
12051 .map_mut()
12052 .unwrap()
12053 .as_mut_slice()
12054 .copy_from_slice(&checker);
12055 let mut dst = TensorDyn::image(
12056 12,
12057 12,
12058 PixelFormat::Rgba,
12059 DType::U8,
12060 Some(TensorMemory::Mem),
12061 edgefirst_tensor::CpuAccess::ReadWrite,
12062 )
12063 .unwrap();
12064 processor.convert(&src, &mut dst, None).unwrap();
12065 dst.as_u8().unwrap().map().unwrap().as_slice().to_vec()
12066 };
12067 let nearest = resize(&mut CPUProcessor::new_nearest());
12068 let bilinear = resize(&mut CPUProcessor::new());
12069 assert_ne!(nearest, bilinear, "the checker must tell the filters apart");
12070 let config = |interpolation| ImageProcessorConfig {
12071 backend: ComputeBackend::Cpu,
12072 interpolation,
12073 ..Default::default()
12074 };
12075
12076 unsafe { std::env::remove_var("EDGEFIRST_INTERPOLATION") };
12077 let mut p = ImageProcessor::with_config(config(Interpolation::Nearest)).unwrap();
12078 assert_eq!(resize(&mut p), nearest, "config Nearest");
12079 p.set_interpolation(Interpolation::Bilinear).unwrap();
12080 assert_eq!(resize(&mut p), bilinear, "set_interpolation(Bilinear)");
12081
12082 unsafe { std::env::set_var("EDGEFIRST_INTERPOLATION", "nearest") };
12083 let mut p = ImageProcessor::with_config(config(Interpolation::Bilinear)).unwrap();
12084 assert_eq!(
12085 resize(&mut p),
12086 nearest,
12087 "EDGEFIRST_INTERPOLATION pins Nearest"
12088 );
12089 p.set_interpolation(Interpolation::Bilinear).unwrap();
12090 assert_eq!(
12091 resize(&mut p),
12092 nearest,
12093 "the env pin outlives set_interpolation"
12094 );
12095 }
12096
12097 #[test]
12101 fn test_per_convert_interpolation_overrides_processor() {
12102 let _lock = acquire_env_lock();
12103 let _guard = EnvGuard::snapshot(&["EDGEFIRST_INTERPOLATION"]);
12104 let mut checker = Vec::with_capacity(8 * 8 * 4);
12105 for y in 0..8 {
12106 for x in 0..8 {
12107 let v = if (x + y) % 2 == 0 { 0u8 } else { 255 };
12108 checker.extend_from_slice(&[v, v, v, 255]);
12109 }
12110 }
12111 let resize = |processor: &mut dyn ImageProcessorTrait, options: Option<ConvertOptions>| {
12112 let mut src = TensorDyn::image(
12113 8,
12114 8,
12115 PixelFormat::Rgba,
12116 DType::U8,
12117 Some(TensorMemory::Mem),
12118 edgefirst_tensor::CpuAccess::ReadWrite,
12119 )
12120 .unwrap();
12121 src.as_u8_mut()
12122 .unwrap()
12123 .map_mut()
12124 .unwrap()
12125 .as_mut_slice()
12126 .copy_from_slice(&checker);
12127 let mut dst = TensorDyn::image(
12128 12,
12129 12,
12130 PixelFormat::Rgba,
12131 DType::U8,
12132 Some(TensorMemory::Mem),
12133 edgefirst_tensor::CpuAccess::ReadWrite,
12134 )
12135 .unwrap();
12136 processor.convert(&src, &mut dst, options).unwrap();
12137 dst.as_u8().unwrap().map().unwrap().as_slice().to_vec()
12138 };
12139 let nearest = resize(&mut CPUProcessor::new_nearest(), None);
12140 let bilinear = resize(&mut CPUProcessor::new(), None);
12141 assert_ne!(nearest, bilinear, "the checker must tell the filters apart");
12142 let cpu_only = || {
12143 ImageProcessor::with_config(ImageProcessorConfig {
12144 backend: ComputeBackend::Cpu,
12145 ..Default::default()
12146 })
12147 .unwrap()
12148 };
12149 let model_a = ConvertOptions::new().interpolation(Interpolation::Nearest);
12150 let model_b = ConvertOptions::new().interpolation(Interpolation::Bicubic);
12151
12152 unsafe { std::env::remove_var("EDGEFIRST_INTERPOLATION") };
12153 let mut p = cpu_only();
12154 assert_eq!(p.interpolation(), Interpolation::Bilinear);
12155 for _ in 0..2 {
12156 assert_eq!(resize(&mut p, Some(model_a)), nearest, "model A: nearest");
12157 assert_eq!(resize(&mut p, Some(model_b)), bilinear, "model B: bicubic");
12158 assert_eq!(resize(&mut p, None), bilinear, "processor setting kept");
12159 }
12160 p.set_interpolation(Interpolation::Nearest).unwrap();
12161 assert_eq!(resize(&mut p, Some(model_b)), bilinear, "options win");
12162 assert_eq!(resize(&mut p, None), nearest, "processor fallback");
12163
12164 let mut spec = PreprocessSpec::new();
12165 spec.interpolation = Interpolation::Nearest;
12166 let from_spec = ConvertOptions::from_spec(&spec).unwrap();
12167 assert_eq!(from_spec.get_interpolation(), Some(Interpolation::Nearest));
12168 let mut p = cpu_only();
12169 let stretch = from_spec.crop(Crop::default());
12170 assert_eq!(
12171 resize(&mut p, Some(stretch)),
12172 nearest,
12173 "from_spec carries it"
12174 );
12175
12176 let cpu = p.cpu.as_mut().unwrap();
12177 cpu.set_interpolation(Interpolation::Nearest);
12178 p.set_interpolation(Interpolation::Bilinear).unwrap();
12179 assert_eq!(
12180 resize(p.cpu.as_mut().unwrap(), None),
12181 nearest,
12182 "an unchanged mode does not reach the backends"
12183 );
12184
12185 unsafe { std::env::set_var("EDGEFIRST_INTERPOLATION", "bilinear") };
12186 let mut p = cpu_only();
12187 assert_eq!(resize(&mut p, Some(model_a)), bilinear, "the env pin wins");
12188 }
12189
12190 #[test]
12193 #[cfg(feature = "opengl")]
12194 fn test_per_convert_interpolation_reaches_gl_worker() {
12195 let _lock = acquire_env_lock();
12196 let _guard = EnvGuard::snapshot(&["EDGEFIRST_INTERPOLATION"]);
12197 unsafe { std::env::remove_var("EDGEFIRST_INTERPOLATION") };
12198 let mut p = match ImageProcessor::with_config(ImageProcessorConfig {
12199 backend: ComputeBackend::OpenGl,
12200 ..Default::default()
12201 }) {
12202 Ok(p) if p.opengl.is_some() => p,
12203 _ => {
12204 crate::test_support::report_skip(&format!(
12205 "{} — GL backend unavailable",
12206 function!()
12207 ));
12208 return;
12209 }
12210 };
12211 let (sw, sh, dw, dh) = (40usize, 30usize, 56usize, 42usize);
12212 let mut rgba = Vec::with_capacity(sw * sh * 4);
12213 for y in 0..sh {
12214 for x in 0..sw {
12215 let v = if (x + y) % 2 == 0 { 16u8 } else { 235 };
12216 rgba.extend_from_slice(&[v, 128, 255 - v, 255]);
12217 }
12218 }
12219 let mut src = TensorDyn::image(
12220 sw,
12221 sh,
12222 PixelFormat::Rgba,
12223 DType::U8,
12224 Some(TensorMemory::Mem),
12225 edgefirst_tensor::CpuAccess::ReadWrite,
12226 )
12227 .unwrap();
12228 src.as_u8_mut()
12229 .unwrap()
12230 .map_mut()
12231 .unwrap()
12232 .as_mut_slice()
12233 .copy_from_slice(&rgba);
12234 let gl_convert = |p: &mut ImageProcessor, options: Option<ConvertOptions>| {
12235 let mut dst = TensorDyn::image(
12236 dw,
12237 dh,
12238 PixelFormat::Rgba,
12239 DType::U8,
12240 Some(TensorMemory::Mem),
12241 edgefirst_tensor::CpuAccess::ReadWrite,
12242 )
12243 .unwrap();
12244 let options = p.resolve_interpolation(options.unwrap_or_default());
12245 p.opengl
12246 .as_mut()
12247 .unwrap()
12248 .convert(&src, &mut dst, Some(options))
12249 .unwrap();
12250 dst.as_u8().unwrap().map().unwrap().as_slice().to_vec()
12251 };
12252 let nearest = ConvertOptions::new().interpolation(Interpolation::Nearest);
12253 let per_convert = gl_convert(&mut p, Some(nearest));
12254 let processor_bilinear = gl_convert(&mut p, None);
12255 p.set_interpolation(Interpolation::Nearest).unwrap();
12256 let processor_nearest = gl_convert(&mut p, None);
12257 assert_ne!(processor_bilinear, processor_nearest);
12258 assert_eq!(per_convert, processor_nearest, "per-convert Nearest");
12259 let bilinear = ConvertOptions::new().interpolation(Interpolation::Bilinear);
12260 assert_eq!(gl_convert(&mut p, Some(bilinear)), processor_bilinear);
12261 }
12262
12263 #[test]
12265 fn test_interpolation_fallback_warns_once() {
12266 runnable_interpolation(Interpolation::Area);
12267 assert!(!warn_first_fallback(Interpolation::Area));
12268 assert_eq!(
12269 runnable_interpolation(Interpolation::Area),
12270 Interpolation::Bilinear
12271 );
12272 assert!(!warn_first_fallback(Interpolation::Area));
12273 }
12274
12275 fn with_force_backend<R>(value: Option<&str>, body: impl FnOnce() -> R) -> R {
12279 let _lock = acquire_env_lock();
12280 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
12281 match value {
12282 Some(v) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", v) },
12283 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
12284 }
12285 body()
12286 }
12287
12288 fn make_dirty_dst(w: usize, h: usize, mem: Option<TensorMemory>) -> TensorDyn {
12293 let dst = TensorDyn::image(
12294 w,
12295 h,
12296 PixelFormat::Rgba,
12297 DType::U8,
12298 mem,
12299 edgefirst_tensor::CpuAccess::ReadWrite,
12300 )
12301 .unwrap();
12302 {
12303 use edgefirst_tensor::TensorMapTrait;
12304 let u8t = dst.as_typed::<u8>().unwrap();
12305 let mut map = u8t.map().unwrap();
12306 for (i, b) in map.as_mut_slice().iter_mut().enumerate() {
12307 *b = 0xA0u8.wrapping_add((i as u8) & 0x3F);
12308 }
12309 }
12310 dst
12311 }
12312
12313 fn make_bg(w: usize, h: usize, mem: Option<TensorMemory>, rgba: [u8; 4]) -> TensorDyn {
12315 let bg = TensorDyn::image(
12316 w,
12317 h,
12318 PixelFormat::Rgba,
12319 DType::U8,
12320 mem,
12321 edgefirst_tensor::CpuAccess::ReadWrite,
12322 )
12323 .unwrap();
12324 {
12325 use edgefirst_tensor::TensorMapTrait;
12326 let u8t = bg.as_typed::<u8>().unwrap();
12327 let mut map = u8t.map().unwrap();
12328 for chunk in map.as_mut_slice().as_chunks_mut::<4>().0 {
12329 chunk.copy_from_slice(&rgba);
12330 }
12331 }
12332 bg
12333 }
12334
12335 fn pixel_at(dst: &TensorDyn, x: usize, y: usize) -> [u8; 4] {
12336 use edgefirst_tensor::TensorMapTrait;
12337 let w = dst.width().unwrap();
12338 let off = (y * w + x) * 4;
12339 let u8t = dst.as_typed::<u8>().unwrap();
12340 let map = u8t.map().unwrap();
12341 let s = map.as_slice();
12342 [s[off], s[off + 1], s[off + 2], s[off + 3]]
12343 }
12344
12345 fn assert_every_pixel_eq(dst: &TensorDyn, expected: [u8; 4], case: &str) {
12346 use edgefirst_tensor::TensorMapTrait;
12347 let u8t = dst.as_typed::<u8>().unwrap();
12348 let map = u8t.map().unwrap();
12349 for (i, chunk) in map.as_slice().as_chunks::<4>().0.iter().enumerate() {
12350 assert_eq!(
12351 chunk, &expected,
12352 "{case}: pixel idx {i} = {chunk:?}, expected {expected:?}"
12353 );
12354 }
12355 }
12356
12357 fn scenario_empty_no_bg(processor: &mut ImageProcessor, case: &str) {
12360 let mut dst = make_dirty_dst(64, 64, None);
12361 processor
12362 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::default())
12363 .unwrap_or_else(|e| panic!("{case}/decoded_masks empty+no-bg failed: {e:?}"));
12364 assert_every_pixel_eq(&dst, [0, 0, 0, 0], &format!("{case}/decoded"));
12365
12366 let mut dst = make_dirty_dst(64, 64, None);
12367 let proto = {
12368 use edgefirst_tensor::{Tensor, TensorDyn};
12369 let coeff_t = Tensor::<f32>::from_slice(&[0.0_f32; 4], &[1, 4]).unwrap();
12371 let protos_t =
12372 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
12373 ProtoData {
12374 mask_coefficients: TensorDyn::F32(coeff_t),
12375 protos: TensorDyn::F32(protos_t),
12376 layout: ProtoLayout::Nhwc,
12377 }
12378 };
12379 processor
12380 .draw_proto_masks(&mut dst, &[], &proto, MaskOverlay::default())
12381 .unwrap_or_else(|e| panic!("{case}/proto_masks empty+no-bg failed: {e:?}"));
12382 assert_every_pixel_eq(&dst, [0, 0, 0, 0], &format!("{case}/proto"));
12383 }
12384
12385 fn scenario_empty_with_bg(processor: &mut ImageProcessor, case: &str) {
12388 let bg_color = [42, 99, 200, 255];
12389 let bg = make_bg(64, 64, None, bg_color);
12390 let overlay = MaskOverlay::new().with_background(&bg);
12391
12392 let mut dst = make_dirty_dst(64, 64, None);
12393 processor
12394 .draw_decoded_masks(&mut dst, &[], &[], overlay)
12395 .unwrap_or_else(|e| panic!("{case}/decoded_masks empty+bg failed: {e:?}"));
12396 assert_every_pixel_eq(&dst, bg_color, &format!("{case}/decoded bg blit"));
12397
12398 let mut dst = make_dirty_dst(64, 64, None);
12399 let proto = {
12400 use edgefirst_tensor::{Tensor, TensorDyn};
12401 let coeff_t = Tensor::<f32>::from_slice(&[0.0_f32; 4], &[1, 4]).unwrap();
12403 let protos_t =
12404 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
12405 ProtoData {
12406 mask_coefficients: TensorDyn::F32(coeff_t),
12407 protos: TensorDyn::F32(protos_t),
12408 layout: ProtoLayout::Nhwc,
12409 }
12410 };
12411 processor
12412 .draw_proto_masks(&mut dst, &[], &proto, overlay)
12413 .unwrap_or_else(|e| panic!("{case}/proto_masks empty+bg failed: {e:?}"));
12414 assert_every_pixel_eq(&dst, bg_color, &format!("{case}/proto bg blit"));
12415 }
12416
12417 fn scenario_detect_no_bg(processor: &mut ImageProcessor, case: &str) {
12421 use edgefirst_tensor::Segmentation;
12422 use ndarray::Array3;
12423 processor
12424 .set_class_colors(&[[200, 80, 40, 255]])
12425 .expect("set_class_colors");
12426
12427 let detect = DetectBox {
12428 bbox: [0.25, 0.25, 0.75, 0.75].into(),
12429 score: 0.99,
12430 label: 0,
12431 };
12432 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
12433 let seg = Segmentation {
12434 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
12435 .unwrap()
12436 .into(),
12437 xmin: 0.25,
12438 ymin: 0.25,
12439 xmax: 0.75,
12440 ymax: 0.75,
12441 };
12442
12443 let mut dst = make_dirty_dst(64, 64, None);
12444 processor
12445 .draw_decoded_masks(&mut dst, &[detect], &[seg], MaskOverlay::default())
12446 .unwrap_or_else(|e| panic!("{case}/decoded_masks detect+no-bg failed: {e:?}"));
12447
12448 let corner = pixel_at(&dst, 2, 2);
12450 assert_eq!(
12451 corner,
12452 [0, 0, 0, 0],
12453 "{case}/decoded: corner (2,2) leaked dirty pattern: {corner:?}"
12454 );
12455 let center = pixel_at(&dst, 32, 32);
12459 assert!(
12460 center != [0, 0, 0, 0],
12461 "{case}/decoded: center (32,32) was not coloured: {center:?}"
12462 );
12463 }
12464
12465 fn scenario_detect_with_bg(processor: &mut ImageProcessor, case: &str) {
12468 use edgefirst_tensor::Segmentation;
12469 use ndarray::Array3;
12470 processor
12471 .set_class_colors(&[[200, 80, 40, 255]])
12472 .expect("set_class_colors");
12473 let bg_color = [10, 20, 30, 255];
12474 let bg = make_bg(64, 64, None, bg_color);
12475
12476 let detect = DetectBox {
12477 bbox: [0.25, 0.25, 0.75, 0.75].into(),
12478 score: 0.99,
12479 label: 0,
12480 };
12481 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
12482 let seg = Segmentation {
12483 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
12484 .unwrap()
12485 .into(),
12486 xmin: 0.25,
12487 ymin: 0.25,
12488 xmax: 0.75,
12489 ymax: 0.75,
12490 };
12491
12492 let overlay = MaskOverlay::new().with_background(&bg);
12493 let mut dst = make_dirty_dst(64, 64, None);
12494 processor
12495 .draw_decoded_masks(&mut dst, &[detect], &[seg], overlay)
12496 .unwrap_or_else(|e| panic!("{case}/decoded_masks detect+bg failed: {e:?}"));
12497
12498 let corner = pixel_at(&dst, 2, 2);
12500 assert_eq!(
12501 corner, bg_color,
12502 "{case}/decoded: corner (2,2) should show bg {bg_color:?} got {corner:?}"
12503 );
12504 let center = pixel_at(&dst, 32, 32);
12507 assert!(
12508 center != bg_color,
12509 "{case}/decoded: center (32,32) should differ from bg {bg_color:?}, got {center:?}"
12510 );
12511 }
12512
12513 fn run_all_scenarios(
12516 force_backend: Option<&'static str>,
12517 case: &'static str,
12518 require_dma_for_bg: bool,
12519 ) {
12520 if require_dma_for_bg && !crate::test_support::is_dma_available() {
12521 crate::test_support::report_skip(&format!("{case} — DMA not available on this host"));
12522 return;
12523 }
12524 let processor_result = with_force_backend(force_backend, ImageProcessor::new);
12525 let mut processor = match processor_result {
12526 Ok(p) => p,
12527 Err(e) => {
12528 crate::test_support::report_skip(&format!("{case} — backend init failed: {e:?}"));
12529 return;
12530 }
12531 };
12532 scenario_empty_no_bg(&mut processor, case);
12533 scenario_empty_with_bg(&mut processor, case);
12534 scenario_detect_no_bg(&mut processor, case);
12535 scenario_detect_with_bg(&mut processor, case);
12536 }
12537
12538 #[test]
12539 fn test_draw_masks_4_scenarios_cpu() {
12540 run_all_scenarios(Some("cpu"), "cpu", false);
12541 }
12542
12543 #[test]
12544 fn test_draw_masks_4_scenarios_auto() {
12545 run_all_scenarios(None, "auto", false);
12546 }
12547
12548 #[cfg(any(
12549 target_os = "linux",
12550 target_os = "macos",
12551 target_os = "ios",
12552 target_os = "android",
12553 target_os = "windows"
12554 ))]
12555 #[cfg(feature = "opengl")]
12556 #[test]
12557 fn test_draw_masks_4_scenarios_opengl() {
12558 run_all_scenarios(Some("opengl"), "opengl", false);
12559 }
12560
12561 #[cfg(target_os = "linux")]
12566 #[test]
12567 fn test_draw_masks_zero_detection_g2d_forced() {
12568 if !crate::test_support::is_dma_available() {
12569 crate::test_support::report_skip("g2d forced — DMA not available on this host");
12570 return;
12571 }
12572 let processor_result = with_force_backend(Some("g2d"), ImageProcessor::new);
12573 let mut processor = match processor_result {
12574 Ok(p) => p,
12575 Err(e) => {
12576 crate::test_support::report_skip(&format!("g2d forced — init failed: {e:?}"));
12577 return;
12578 }
12579 };
12580
12581 let mut dst = TensorDyn::image(
12583 64,
12584 64,
12585 PixelFormat::Rgba,
12586 DType::U8,
12587 Some(TensorMemory::DmaBuf),
12588 edgefirst_tensor::CpuAccess::ReadWrite,
12589 )
12590 .unwrap();
12591 {
12592 use edgefirst_tensor::TensorMapTrait;
12593 let u8t = dst.as_typed_mut::<u8>().unwrap();
12594 let mut map = u8t.map().unwrap();
12595 map.as_mut_slice().fill(0xBB);
12596 }
12597 processor
12598 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::default())
12599 .expect("g2d empty+no-bg");
12600 assert_every_pixel_eq(&dst, [0, 0, 0, 0], "g2d/case1 cleared");
12601
12602 let bg_color = [7, 11, 13, 255];
12604 let bg = {
12605 let t = TensorDyn::image(
12606 64,
12607 64,
12608 PixelFormat::Rgba,
12609 DType::U8,
12610 Some(TensorMemory::DmaBuf),
12611 edgefirst_tensor::CpuAccess::ReadWrite,
12612 )
12613 .unwrap();
12614 {
12615 use edgefirst_tensor::TensorMapTrait;
12616 let u8t = t.as_typed::<u8>().unwrap();
12617 let mut map = u8t.map().unwrap();
12618 for chunk in map.as_mut_slice().as_chunks_mut::<4>().0 {
12619 chunk.copy_from_slice(&bg_color);
12620 }
12621 }
12622 t
12623 };
12624 let mut dst = TensorDyn::image(
12625 64,
12626 64,
12627 PixelFormat::Rgba,
12628 DType::U8,
12629 Some(TensorMemory::DmaBuf),
12630 edgefirst_tensor::CpuAccess::ReadWrite,
12631 )
12632 .unwrap();
12633 {
12634 use edgefirst_tensor::TensorMapTrait;
12635 let u8t = dst.as_typed_mut::<u8>().unwrap();
12636 let mut map = u8t.map().unwrap();
12637 map.as_mut_slice().fill(0x55);
12638 }
12639 processor
12640 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::new().with_background(&bg))
12641 .expect("g2d empty+bg");
12642 assert_every_pixel_eq(&dst, bg_color, "g2d/case2 bg blit");
12643
12644 let detect = DetectBox {
12646 bbox: [0.25, 0.25, 0.75, 0.75].into(),
12647 score: 0.9,
12648 label: 0,
12649 };
12650 let mut dst = TensorDyn::image(
12651 64,
12652 64,
12653 PixelFormat::Rgba,
12654 DType::U8,
12655 Some(TensorMemory::DmaBuf),
12656 edgefirst_tensor::CpuAccess::ReadWrite,
12657 )
12658 .unwrap();
12659 let err = processor
12660 .draw_decoded_masks(&mut dst, &[detect], &[], MaskOverlay::default())
12661 .expect_err("g2d must reject detect-present draw_decoded_masks");
12662 assert!(
12663 matches!(err, Error::NotImplemented(_)),
12664 "g2d case3 wrong error: {err:?}"
12665 );
12666 }
12667
12668 #[test]
12669 fn test_set_format_then_cpu_convert() {
12670 let _lock = acquire_env_lock();
12673 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
12674 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
12675 let mut processor = ImageProcessor::new().unwrap();
12676
12677 let image = edgefirst_bench::testdata::read("zidane.jpg");
12679 let src = load_image_test_helper(&image, Some(PixelFormat::Rgba), None).unwrap();
12680
12681 let mut dst =
12683 TensorDyn::new(&[640, 640, 3], DType::U8, Some(TensorMemory::Mem), None).unwrap();
12684 dst.set_format(PixelFormat::Rgb).unwrap();
12685
12686 processor.convert(&src, &mut dst, None).unwrap();
12688
12689 assert_eq!(dst.format(), Some(PixelFormat::Rgb));
12691 assert_eq!(dst.width(), Some(640));
12692 assert_eq!(dst.height(), Some(640));
12693 }
12694
12695 #[test]
12701 fn test_multiple_image_processors_same_thread() {
12702 let _lock = acquire_env_lock();
12705 let mut processors: Vec<ImageProcessor> = (0..4)
12706 .map(|_| ImageProcessor::new().expect("ImageProcessor::new() failed"))
12707 .collect();
12708
12709 for proc in &mut processors {
12710 let src = proc
12711 .create_image(
12712 128,
12713 128,
12714 PixelFormat::Rgb,
12715 DType::U8,
12716 None,
12717 edgefirst_tensor::CpuAccess::ReadWrite,
12718 )
12719 .expect("create src failed");
12720 let mut dst = proc
12721 .create_image(
12722 64,
12723 64,
12724 PixelFormat::Rgb,
12725 DType::U8,
12726 None,
12727 edgefirst_tensor::CpuAccess::ReadWrite,
12728 )
12729 .expect("create dst failed");
12730 proc.convert(&src, &mut dst, None).expect("convert failed");
12731 assert_eq!(dst.width(), Some(64));
12732 assert_eq!(dst.height(), Some(64));
12733 }
12734 }
12735
12736 #[test]
12743 fn test_multiple_image_processors_separate_threads() {
12744 use std::sync::mpsc;
12745 use std::time::Duration;
12746
12747 if std::env::var_os("EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS").is_some() {
12758 crate::test_support::report_skip(
12759 "test_multiple_image_processors_separate_threads — known Vivante \
12760 GC7000UL concurrent-EGL-teardown double-free \
12761 (EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS set)",
12762 );
12763 return;
12764 }
12765
12766 const TIMEOUT: Duration = Duration::from_secs(60);
12767
12768 let _lock = acquire_env_lock();
12771
12772 let (tx, rx) = mpsc::channel::<()>();
12773
12774 std::thread::spawn(move || {
12775 let handles: Vec<_> = (0..4)
12776 .map(|i| {
12777 std::thread::spawn(move || {
12778 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
12779 panic!("ImageProcessor::new() failed on thread {i}: {e}")
12780 });
12781 let src = proc
12782 .create_image(
12783 128,
12784 128,
12785 PixelFormat::Rgb,
12786 DType::U8,
12787 None,
12788 edgefirst_tensor::CpuAccess::ReadWrite,
12789 )
12790 .unwrap_or_else(|e| panic!("create src failed on thread {i}: {e}"));
12791 let mut dst = proc
12792 .create_image(
12793 64,
12794 64,
12795 PixelFormat::Rgb,
12796 DType::U8,
12797 None,
12798 edgefirst_tensor::CpuAccess::ReadWrite,
12799 )
12800 .unwrap_or_else(|e| panic!("create dst failed on thread {i}: {e}"));
12801 proc.convert(&src, &mut dst, None)
12802 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
12803 assert_eq!(dst.width(), Some(64));
12804 assert_eq!(dst.height(), Some(64));
12805 })
12806 })
12807 .collect();
12808
12809 for (i, h) in handles.into_iter().enumerate() {
12810 h.join()
12811 .unwrap_or_else(|e| panic!("thread {i} panicked: {e:?}"));
12812 }
12813
12814 let _ = tx.send(());
12815 });
12816
12817 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
12818 panic!("test_multiple_image_processors_separate_threads timed out after {TIMEOUT:?}")
12819 });
12820 }
12821
12822 #[test]
12829 fn test_image_processors_concurrent_operations() {
12830 use std::sync::{mpsc, Arc, Barrier};
12831 use std::time::Duration;
12832
12833 const N: usize = 4;
12834 const ROUNDS: usize = 10;
12835 const TIMEOUT: Duration = Duration::from_secs(60);
12836
12837 let _lock = acquire_env_lock();
12840
12841 let (tx, rx) = mpsc::channel::<()>();
12842
12843 std::thread::spawn(move || {
12844 let barrier = Arc::new(Barrier::new(N));
12845
12846 let handles: Vec<_> = (0..N)
12847 .map(|i| {
12848 let barrier = Arc::clone(&barrier);
12849 std::thread::spawn(move || {
12850 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
12851 panic!("ImageProcessor::new() failed on thread {i}: {e}")
12852 });
12853
12854 barrier.wait();
12856
12857 for round in 0..ROUNDS {
12859 let src = proc
12860 .create_image(
12861 128,
12862 128,
12863 PixelFormat::Rgb,
12864 DType::U8,
12865 None,
12866 edgefirst_tensor::CpuAccess::ReadWrite,
12867 )
12868 .unwrap_or_else(|e| {
12869 panic!("create src failed on thread {i} round {round}: {e}")
12870 });
12871 let mut dst = proc
12872 .create_image(
12873 64,
12874 64,
12875 PixelFormat::Rgb,
12876 DType::U8,
12877 None,
12878 edgefirst_tensor::CpuAccess::ReadWrite,
12879 )
12880 .unwrap_or_else(|e| {
12881 panic!("create dst failed on thread {i} round {round}: {e}")
12882 });
12883 proc.convert(&src, &mut dst, None).unwrap_or_else(|e| {
12884 panic!("convert failed on thread {i} round {round}: {e}")
12885 });
12886 assert_eq!(dst.width(), Some(64));
12887 assert_eq!(dst.height(), Some(64));
12888 }
12889 })
12890 })
12891 .collect();
12892
12893 for (i, h) in handles.into_iter().enumerate() {
12894 h.join()
12895 .unwrap_or_else(|e| panic!("thread {i} panicked: {e:?}"));
12896 }
12897
12898 let _ = tx.send(());
12899 });
12900
12901 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
12902 panic!("test_image_processors_concurrent_operations timed out after {TIMEOUT:?}")
12903 });
12904 }
12905
12906 #[test]
12924 fn test_parallel_processors_unique_outputs() {
12925 use std::sync::{mpsc, Arc, Barrier};
12926 use std::time::Duration;
12927
12928 const N: usize = 4;
12929 const ROUNDS: usize = 25;
12930 const TIMEOUT: Duration = Duration::from_secs(60);
12931
12932 if std::env::var_os("EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS").is_some() {
12933 crate::test_support::report_skip(
12934 "test_parallel_processors_unique_outputs — known Vivante \
12935 GC7000UL concurrent-multi-processor driver abort \
12936 (EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS set)",
12937 );
12938 return;
12939 }
12940
12941 let _lock = acquire_env_lock();
12942 let use_dma = crate::test_support::is_dma_available();
12944 if !use_dma {
12945 crate::test_support::report_skip(&format!(
12946 "{} - DmaBuf sources not exercised, running on Mem: no usable DMA heap",
12947 function!()
12948 ));
12949 }
12950 let (tx, rx) = mpsc::channel::<()>();
12951
12952 std::thread::spawn(move || {
12953 let barrier = Arc::new(Barrier::new(N));
12954 let handles: Vec<_> = (0..N)
12955 .map(|i| {
12956 let barrier = Arc::clone(&barrier);
12957 std::thread::spawn(move || {
12958 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
12959 panic!("ImageProcessor::new() failed on thread {i}: {e}")
12960 });
12961 let (w, h) = (640usize, 480usize);
12963 let mem = if use_dma {
12964 Some(TensorMemory::DmaBuf)
12965 } else {
12966 Some(TensorMemory::Mem)
12967 };
12968 let src = proc
12969 .create_image(
12970 w,
12971 h,
12972 PixelFormat::Nv12,
12973 DType::U8,
12974 mem,
12975 edgefirst_tensor::CpuAccess::ReadWrite,
12976 )
12977 .unwrap();
12978 {
12979 let t = src.as_typed::<u8>().unwrap();
12980 let mut m = t.map().unwrap();
12981 let s = m.as_mut_slice();
12982 for (j, b) in s[..w * h].iter_mut().enumerate() {
12983 *b = ((i * 53 + j) % 200 + 16) as u8;
12984 }
12985 for b in &mut s[w * h..] {
12986 *b = (80 + i * 24) as u8;
12987 }
12988 }
12989 let lb = Crop::letterbox([114, 114, 114, 255]);
12990 let convert_once = |proc: &mut ImageProcessor| -> Vec<u8> {
12991 let mut dst = proc
12992 .create_image(
12993 320,
12994 320,
12995 PixelFormat::Rgba,
12996 DType::U8,
12997 mem,
12998 edgefirst_tensor::CpuAccess::ReadWrite,
12999 )
13000 .unwrap();
13001 proc.convert(
13002 &src,
13003 &mut dst,
13004 Some(crate::ConvertOptions::new().crop(lb)),
13005 )
13006 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
13007 let t = dst.as_typed::<u8>().unwrap();
13008 let m = t.map().unwrap();
13009 m.as_slice().to_vec()
13010 };
13011
13012 let oracle = convert_once(&mut proc);
13013 barrier.wait();
13014 for round in 0..ROUNDS {
13015 let out = convert_once(&mut proc);
13016 let diffs = oracle.iter().zip(&out).filter(|(a, b)| a != b).count();
13017 assert!(
13018 diffs == 0,
13019 "thread {i} round {round}: {diffs}/{} bytes diverged \
13020 from this processor's own oracle — cross-processor \
13021 GL state leakage under parallel execution",
13022 oracle.len()
13023 );
13024 }
13025 })
13026 })
13027 .collect();
13028
13029 for (i, h) in handles.into_iter().enumerate() {
13030 h.join()
13031 .unwrap_or_else(|e| panic!("parallel thread {i} panicked: {e:?}"));
13032 }
13033 let _ = tx.send(());
13034 });
13035
13036 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
13037 panic!("test_parallel_processors_unique_outputs timed out after {TIMEOUT:?}")
13038 });
13039 }
13040
13041 #[test]
13050 #[ignore = "heavy on-demand GL-parallelism stressor; run explicitly on boards"]
13051 fn stress_parallel_processors_oracle() {
13052 use std::sync::{mpsc, Arc, Barrier};
13053 use std::time::Duration;
13054
13055 const N: usize = 4;
13056 const ROUNDS: usize = 200;
13057 const TIMEOUT: Duration = Duration::from_secs(600);
13058
13059 let _lock = acquire_env_lock();
13060 let use_dma = crate::test_support::is_dma_available();
13062 if !use_dma {
13063 crate::test_support::report_skip(&format!(
13064 "{} - DmaBuf sources not exercised, running on Mem: no usable DMA heap",
13065 function!()
13066 ));
13067 }
13068 let (tx, rx) = mpsc::channel::<()>();
13069
13070 std::thread::spawn(move || {
13071 let barrier = Arc::new(Barrier::new(N));
13072 let handles: Vec<_> = (0..N)
13073 .map(|i| {
13074 let barrier = Arc::clone(&barrier);
13075 std::thread::spawn(move || {
13076 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
13077 panic!("ImageProcessor::new() failed on thread {i}: {e}")
13078 });
13079 let (w, h) = (1280usize, 720usize);
13080 let mem = if use_dma {
13081 Some(TensorMemory::DmaBuf)
13082 } else {
13083 Some(TensorMemory::Mem)
13084 };
13085
13086 let src = proc
13089 .create_image(
13090 w,
13091 h,
13092 PixelFormat::Nv12,
13093 DType::U8,
13094 mem,
13095 edgefirst_tensor::CpuAccess::ReadWrite,
13096 )
13097 .unwrap();
13098 {
13099 let t = src.as_typed::<u8>().unwrap();
13100 let mut m = t.map().unwrap();
13101 let s = m.as_mut_slice();
13102 for (j, b) in s[..w * h].iter_mut().enumerate() {
13103 *b = ((i * 37 + j) % 200 + 16) as u8;
13104 }
13105 for b in &mut s[w * h..] {
13106 *b = (96 + i * 16) as u8;
13107 }
13108 }
13109 let lb = Crop::letterbox([114, 114, 114, 255]);
13110
13111 let convert_once = |proc: &mut ImageProcessor| -> Vec<u8> {
13112 let mut dst = proc
13113 .create_image(
13114 640,
13115 640,
13116 PixelFormat::Rgb,
13117 DType::U8,
13118 mem,
13119 edgefirst_tensor::CpuAccess::ReadWrite,
13120 )
13121 .unwrap();
13122 proc.convert(
13123 &src,
13124 &mut dst,
13125 Some(crate::ConvertOptions::new().crop(lb)),
13126 )
13127 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
13128 let t = dst.as_typed::<u8>().unwrap();
13129 let m = t.map().unwrap();
13130 m.as_slice().to_vec()
13131 };
13132
13133 let oracle = convert_once(&mut proc);
13134 barrier.wait();
13135 for round in 0..ROUNDS {
13136 let out = convert_once(&mut proc);
13137 let diffs = oracle.iter().zip(&out).filter(|(a, b)| a != b).count();
13138 assert!(
13139 diffs == 0,
13140 "thread {i} round {round}: {diffs}/{} bytes diverged \
13141 from the pre-barrier oracle",
13142 oracle.len()
13143 );
13144 }
13145 })
13146 })
13147 .collect();
13148
13149 for (i, h) in handles.into_iter().enumerate() {
13150 h.join()
13151 .unwrap_or_else(|e| panic!("stressor thread {i} panicked: {e:?}"));
13152 }
13153 let _ = tx.send(());
13154 });
13155
13156 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
13157 panic!("stress_parallel_processors_oracle timed out after {TIMEOUT:?}")
13158 });
13159 }
13160
13161 #[test]
13174 fn convert_f32_auto_never_errors_non_gl_combo() {
13175 const W: usize = 64;
13176 const H: usize = 64;
13177
13178 let src = TensorDyn::image(
13181 W,
13182 H,
13183 PixelFormat::Yuyv,
13184 DType::U8,
13185 Some(TensorMemory::Mem),
13186 edgefirst_tensor::CpuAccess::ReadWrite,
13187 )
13188 .unwrap();
13189 {
13190 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13191 let data = map.as_mut_slice();
13192 for chunk in data.as_chunks_mut::<4>().0 {
13193 chunk[0] = 128; chunk[1] = 128; chunk[2] = 160; chunk[3] = 128; }
13198 }
13199
13200 let mut dst = TensorDyn::image(
13201 W,
13202 H,
13203 PixelFormat::Rgb,
13204 DType::F32,
13205 Some(TensorMemory::Mem),
13206 edgefirst_tensor::CpuAccess::ReadWrite,
13207 )
13208 .unwrap();
13209
13210 let mut proc = ImageProcessor::new().unwrap();
13211 let result = proc.convert(&src, &mut dst, None);
13212 assert!(
13213 result.is_ok(),
13214 "auto-chain Yuyv→Rgb F32 must not error: {:?}",
13215 result.err()
13216 );
13217
13218 let map = dst.as_typed::<f32>().unwrap().map().unwrap();
13220 let floats = map.as_slice();
13221 assert_eq!(floats.len(), W * H * 3, "unexpected output element count");
13222 for (i, &v) in floats.iter().enumerate() {
13223 assert!(
13224 v.is_finite() && (0.0..=1.0).contains(&v),
13225 "output[{i}]={v} is not finite or not in [0,1]"
13226 );
13227 }
13228
13229 let first_non_zero = floats.iter().find(|&&v| v > 0.01);
13233 assert!(
13234 first_non_zero.is_some(),
13235 "all-zero output detected — CPU path likely did not write to the destination buffer"
13236 );
13237 let r0 = floats[0];
13240 assert!(
13241 (r0 - 0.502_f32).abs() < 0.05,
13242 "first pixel R={r0} expected ≈0.502 (Y=128 neutral grey from YUYV source)"
13243 );
13244 }
13245
13246 #[test]
13252 #[allow(clippy::needless_update)]
13258 fn convert_f16_forced_cpu_correct() {
13259 const W: usize = 16;
13260 const H: usize = 16;
13261 const TOL: f32 = 1.0 / 512.0; let src = TensorDyn::image(
13265 W,
13266 H,
13267 PixelFormat::Rgba,
13268 DType::U8,
13269 Some(TensorMemory::Mem),
13270 edgefirst_tensor::CpuAccess::ReadWrite,
13271 )
13272 .unwrap();
13273 {
13274 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13275 let data = map.as_mut_slice();
13276 for y in 0..H {
13277 for x in 0..W {
13278 let i = y * W + x;
13279 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;
13283 }
13284 }
13285 }
13286
13287 let mut dst = TensorDyn::image(
13288 W,
13289 H,
13290 PixelFormat::PlanarRgb,
13291 DType::F16,
13292 Some(TensorMemory::Mem),
13293 edgefirst_tensor::CpuAccess::ReadWrite,
13294 )
13295 .unwrap();
13296
13297 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
13298 backend: ComputeBackend::Cpu,
13299 ..Default::default()
13300 })
13301 .unwrap();
13302 proc.convert(&src, &mut dst, None)
13303 .expect("forced-CPU Rgba→PlanarRgb F16 must not error");
13304
13305 let src_map = src.as_typed::<u8>().unwrap().map().unwrap();
13306 let src_bytes = src_map.as_slice();
13307 let dst_map = dst.as_typed::<half::f16>().unwrap().map().unwrap();
13308 let dst_halfs = dst_map.as_slice();
13309
13310 let plane = W * H;
13311 assert_eq!(dst_halfs.len(), plane * 3, "wrong output element count");
13312
13313 for y in 0..H {
13314 for x in 0..W {
13315 let i = y * W + x;
13316 let r_exp = src_bytes[i * 4] as f32 / 255.0;
13317 let g_exp = src_bytes[i * 4 + 1] as f32 / 255.0;
13318 let b_exp = src_bytes[i * 4 + 2] as f32 / 255.0;
13319
13320 let r_got = dst_halfs[i].to_f32();
13321 let g_got = dst_halfs[plane + i].to_f32();
13322 let b_got = dst_halfs[2 * plane + i].to_f32();
13323
13324 assert!(
13325 (r_got - r_exp).abs() <= TOL,
13326 "R plane ({x},{y}): got {r_got}, expected {r_exp}"
13327 );
13328 assert!(
13329 (g_got - g_exp).abs() <= TOL,
13330 "G plane ({x},{y}): got {g_got}, expected {g_exp}"
13331 );
13332 assert!(
13333 (b_got - b_exp).abs() <= TOL,
13334 "B plane ({x},{y}): got {b_got}, expected {b_exp}"
13335 );
13336
13337 if src_bytes[i * 4] != src_bytes[i * 4 + 1] {
13339 assert_ne!(r_got, g_got, "R and G planes must differ at ({x},{y})");
13340 }
13341 }
13342 }
13343 }
13344
13345 #[test]
13353 fn convert_f32_with_rotation_falls_back() {
13354 const W: usize = 16;
13355 const H: usize = 16;
13356
13357 let src = TensorDyn::image(
13359 W,
13360 H,
13361 PixelFormat::Rgba,
13362 DType::U8,
13363 Some(TensorMemory::Mem),
13364 edgefirst_tensor::CpuAccess::ReadWrite,
13365 )
13366 .unwrap();
13367 {
13368 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13369 let data = map.as_mut_slice();
13370 for y in 0..H {
13371 for x in 0..W {
13372 let i = y * W + x;
13373 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;
13377 }
13378 }
13379 }
13380
13381 let mut dst = TensorDyn::image(
13383 H, W, PixelFormat::Rgb,
13386 DType::F32,
13387 Some(TensorMemory::Mem),
13388 edgefirst_tensor::CpuAccess::ReadWrite,
13389 )
13390 .unwrap();
13391
13392 let mut proc = ImageProcessor::new().unwrap();
13393 let result = proc.convert(
13394 &src,
13395 &mut dst,
13396 Some(crate::ConvertOptions::new().rotation(Rotation::Clockwise90)),
13397 );
13398 assert!(
13399 result.is_ok(),
13400 "auto-chain Rgba→Rgb F32 with Rot90 must not error: {:?}",
13401 result.err()
13402 );
13403
13404 let map = dst.as_typed::<f32>().unwrap().map().unwrap();
13405 let floats = map.as_slice();
13406 assert_eq!(floats.len(), H * W * 3, "unexpected output element count");
13407 for (i, &v) in floats.iter().enumerate() {
13408 assert!(
13409 v.is_finite() && (0.0..=1.0).contains(&v),
13410 "output[{i}]={v} is not finite or not in [0,1]"
13411 );
13412 }
13413 }
13414
13415 #[test]
13441 #[cfg(all(
13442 any(
13443 target_os = "linux",
13444 target_os = "macos",
13445 target_os = "ios",
13446 target_os = "android",
13447 target_os = "windows"
13448 ),
13449 feature = "opengl"
13450 ))]
13451 fn convert_f16_gl_cpu_parity_identity() {
13452 #[cfg(target_os = "macos")]
13458 if std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1")
13459 && std::env::var_os("HAL_TEST_ALLOW_DLOPEN_ANGLE").is_none()
13460 {
13461 crate::test_support::report_skip(
13462 "convert_f16_gl_cpu_parity_identity - ANGLE dlopen gate closed (coverage pass 1)",
13463 );
13464 return;
13465 }
13466 if !is_opengl_available() {
13467 crate::test_support::report_skip(
13468 "convert_f16_gl_cpu_parity_identity - OpenGL not available",
13469 );
13470 return;
13471 }
13472
13473 const W: usize = 32;
13475 const H: usize = 16;
13476 const TOL: f32 = 1.0 / 256.0; let src = TensorDyn::image(
13480 W,
13481 H,
13482 PixelFormat::Rgba,
13483 DType::U8,
13484 Some(TensorMemory::Mem),
13485 edgefirst_tensor::CpuAccess::ReadWrite,
13486 )
13487 .unwrap();
13488 {
13489 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13490 let data = map.as_mut_slice();
13491 for y in 0..H {
13492 for x in 0..W {
13493 let i = y * W + x;
13494 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;
13498 }
13499 }
13500 }
13501
13502 let gl_result = {
13504 let mut gl_proc = match ImageProcessor::with_config(ImageProcessorConfig {
13505 backend: ComputeBackend::OpenGl,
13506 ..Default::default()
13507 }) {
13508 Ok(p) => p,
13509 Err(e) => {
13510 crate::test_support::report_skip(&format!(
13511 "convert_f16_gl_cpu_parity_identity - GL backend unavailable: {e}"
13512 ));
13513 return;
13514 }
13515 };
13516
13517 if !gl_proc.supported_render_dtypes().f16 {
13518 crate::test_support::report_skip(
13519 "convert_f16_gl_cpu_parity_identity - F16 render not supported",
13520 );
13521 return;
13522 }
13523
13524 let Some(mut dst) = crate::test_support::gpu_buffer_allocated(
13545 TensorDyn::image(
13546 W,
13547 H,
13548 PixelFormat::PlanarRgb,
13549 DType::F16,
13550 Some(TensorMemory::DmaBuf),
13551 edgefirst_tensor::CpuAccess::ReadWrite,
13552 ),
13553 "zero-copy F16 PlanarRgb destination",
13554 ) else {
13555 return;
13556 };
13557 if dst.memory() != TensorMemory::DmaBuf {
13558 crate::test_support::report_skip(&format!(
13559 "convert_f16_gl_cpu_parity_identity - the zero-copy F16 \
13560 destination fell back to {:?}",
13561 dst.memory()
13562 ));
13563 return;
13564 }
13565 let require_gl = std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1");
13566 let fallbacks_before = gl_proc.convert_fallback_count();
13567 match gl_proc.convert(&src, &mut dst, None) {
13568 Ok(()) => {
13569 if gl_proc.convert_fallback_count() != fallbacks_before {
13570 assert!(
13571 !require_gl,
13572 "HAL_TEST_REQUIRE_GL=1 but \
13573 convert_f16_gl_cpu_parity_identity would have skipped \
13574 while reporting success: the GL F16 path declined and \
13575 the CPU served the convert"
13576 );
13577 crate::test_support::report_skip(
13578 "convert_f16_gl_cpu_parity_identity - the GL F16 path \
13579 declined and the CPU served the convert; comparing that \
13580 against the forced-CPU result would prove nothing",
13581 );
13582 return;
13583 }
13584 dst
13585 }
13586 Err(e) => {
13587 crate::test_support::report_skip(&format!(
13588 "convert_f16_gl_cpu_parity_identity - GL convert failed: {e}"
13589 ));
13590 return;
13591 }
13592 }
13593 };
13594
13595 let cpu_result = {
13597 let mut cpu_proc = ImageProcessor::with_config(ImageProcessorConfig {
13598 backend: ComputeBackend::Cpu,
13599 ..Default::default()
13600 })
13601 .unwrap();
13602 let mut dst = TensorDyn::image(
13603 W,
13604 H,
13605 PixelFormat::PlanarRgb,
13606 DType::F16,
13607 Some(TensorMemory::Mem),
13608 edgefirst_tensor::CpuAccess::ReadWrite,
13609 )
13610 .unwrap();
13611 cpu_proc
13612 .convert(&src, &mut dst, None)
13613 .expect("forced-CPU Rgba→PlanarRgb F16 must not error");
13614 dst
13615 };
13616
13617 let flat_f16 = |t: &TensorDyn| -> Vec<half::f16> {
13622 let typed = t.as_typed::<half::f16>().expect("F16 image");
13623 let mut bytes = vec![0u8; typed.shape().iter().product::<usize>() * 2];
13624 typed
13625 .copy_to_flat(&mut bytes)
13626 .expect("compact any padded rows");
13627 bytes
13628 .as_chunks::<2>()
13629 .0
13630 .iter()
13631 .map(|&c| half::f16::from_le_bytes(c))
13632 .collect()
13633 };
13634 let gl_halfs = flat_f16(&gl_result);
13635 let cpu_halfs = flat_f16(&cpu_result);
13636
13637 assert_eq!(
13638 gl_halfs.len(),
13639 cpu_halfs.len(),
13640 "GL and CPU output sizes differ"
13641 );
13642
13643 let plane = W * H;
13644 let channel_names = ["R", "G", "B"];
13645 for (idx, (gl_h, cpu_h)) in gl_halfs.iter().zip(cpu_halfs.iter()).enumerate() {
13646 let gl_v = gl_h.to_f32();
13647 let cpu_v = cpu_h.to_f32();
13648 let err = (gl_v - cpu_v).abs();
13649 let ch = channel_names[idx / plane];
13650 let pixel = idx % plane;
13651 assert!(
13652 err <= TOL,
13653 "GL vs CPU mismatch at {ch}[{pixel}]: GL={gl_v}, CPU={cpu_v}, err={err} > tol={TOL}"
13654 );
13655 }
13656 }
13657
13658 #[test]
13666 #[cfg(all(any(target_os = "linux", target_os = "windows"), feature = "opengl"))]
13667 fn supported_render_dtypes_linux_smoke() {
13668 let proc = match ImageProcessor::new() {
13669 Ok(p) => p,
13670 Err(e) => {
13671 crate::test_support::report_skip(&format!(
13672 "supported_render_dtypes_linux_smoke — ImageProcessor::new() failed: {e}"
13673 ));
13674 return;
13675 }
13676 };
13677 if proc.opengl.is_none() {
13678 crate::test_support::report_skip(
13679 "supported_render_dtypes_linux_smoke — no GL backend on this host",
13680 );
13681 return;
13682 }
13683 let support = proc.supported_render_dtypes();
13685 eprintln!(
13686 "supported_render_dtypes_linux_smoke: f16={} f32={}",
13687 support.f16, support.f32
13688 );
13689 }
13691
13692 #[test]
13701 fn convert_f16_pbo_non_4_aligned_width_falls_back() {
13702 const W: usize = 18; const H: usize = 16;
13704
13705 let src = TensorDyn::image(
13707 W,
13708 H,
13709 PixelFormat::Rgba,
13710 DType::U8,
13711 Some(TensorMemory::Mem),
13712 edgefirst_tensor::CpuAccess::ReadWrite,
13713 )
13714 .unwrap();
13715 {
13716 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13717 let data = map.as_mut_slice();
13718 for chunk in data.as_chunks_mut::<4>().0 {
13719 chunk[0] = 128;
13720 chunk[1] = 64;
13721 chunk[2] = 200;
13722 chunk[3] = 255;
13723 }
13724 }
13725
13726 let mut dst = TensorDyn::image(
13729 W,
13730 H,
13731 PixelFormat::PlanarRgb,
13732 DType::F16,
13733 Some(TensorMemory::Mem),
13734 edgefirst_tensor::CpuAccess::ReadWrite,
13735 )
13736 .unwrap();
13737
13738 let mut proc = ImageProcessor::new().unwrap();
13741 let result = proc.convert(&src, &mut dst, None);
13742 assert!(
13743 result.is_ok(),
13744 "auto-chain PlanarRgb F16 W%4!=0 must not error (CPU fallback): {:?}",
13745 result.err()
13746 );
13747
13748 let map = dst.as_typed::<half::f16>().unwrap().map().unwrap();
13750 let halfs = map.as_slice();
13751 assert_eq!(halfs.len(), W * H * 3, "unexpected element count");
13752 for (i, h) in halfs.iter().enumerate() {
13753 let v = h.to_f32();
13754 assert!(
13755 v.is_finite() && (0.0..=1.0).contains(&v),
13756 "output[{i}]={v} is not finite or not in [0,1]"
13757 );
13758 }
13759 }
13760
13761 #[test]
13771 #[allow(clippy::needless_update)]
13774 fn convert_nv12_to_rgb_f32_cpu() {
13775 const W: usize = 16;
13776 const H: usize = 16; let src = TensorDyn::image(
13780 W,
13781 H,
13782 PixelFormat::Nv12,
13783 DType::U8,
13784 Some(TensorMemory::Mem),
13785 edgefirst_tensor::CpuAccess::ReadWrite,
13786 )
13787 .unwrap();
13788 {
13789 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13790 map.as_mut_slice().fill(128); }
13792
13793 let mut dst = TensorDyn::image(
13794 W,
13795 H,
13796 PixelFormat::Rgb,
13797 DType::F32,
13798 Some(TensorMemory::Mem),
13799 edgefirst_tensor::CpuAccess::ReadWrite,
13800 )
13801 .unwrap();
13802
13803 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
13804 backend: ComputeBackend::Cpu,
13805 ..Default::default()
13806 })
13807 .unwrap();
13808
13809 let result = proc.convert(&src, &mut dst, None);
13810 assert!(
13811 result.is_ok(),
13812 "forced-CPU NV12→Rgb F32 must not error: {:?}",
13813 result.err()
13814 );
13815
13816 let map = dst.as_typed::<f32>().unwrap().map().unwrap();
13817 let floats = map.as_slice();
13818 assert_eq!(floats.len(), W * H * 3, "unexpected element count");
13819 for (i, &v) in floats.iter().enumerate() {
13820 assert!(
13821 v.is_finite() && (0.0..=1.0).contains(&v),
13822 "output[{i}]={v} is not finite or not in [0,1]"
13823 );
13824 }
13825 let non_zero = floats.iter().any(|&v| v > 0.01);
13827 assert!(non_zero, "all-zero output from NV12→Rgb F32 CPU path");
13828 }
13829
13830 #[test]
13834 #[allow(clippy::needless_update)]
13837 fn convert_nv12_to_planar_rgb_f16_cpu() {
13838 const W: usize = 16;
13839 const H: usize = 16;
13840
13841 let src = TensorDyn::image(
13842 W,
13843 H,
13844 PixelFormat::Nv12,
13845 DType::U8,
13846 Some(TensorMemory::Mem),
13847 edgefirst_tensor::CpuAccess::ReadWrite,
13848 )
13849 .unwrap();
13850 {
13851 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13852 map.as_mut_slice().fill(128);
13853 }
13854
13855 let mut dst = TensorDyn::image(
13856 W,
13857 H,
13858 PixelFormat::PlanarRgb,
13859 DType::F16,
13860 Some(TensorMemory::Mem),
13861 edgefirst_tensor::CpuAccess::ReadWrite,
13862 )
13863 .unwrap();
13864
13865 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
13866 backend: ComputeBackend::Cpu,
13867 ..Default::default()
13868 })
13869 .unwrap();
13870
13871 let result = proc.convert(&src, &mut dst, None);
13872 assert!(
13873 result.is_ok(),
13874 "forced-CPU NV12→PlanarRgb F16 must not error: {:?}",
13875 result.err()
13876 );
13877
13878 let map = dst.as_typed::<half::f16>().unwrap().map().unwrap();
13879 let halfs = map.as_slice();
13880 assert_eq!(halfs.len(), W * H * 3, "unexpected element count");
13881 for (i, h) in halfs.iter().enumerate() {
13882 let v = h.to_f32();
13883 assert!(
13884 v.is_finite() && (0.0..=1.0).contains(&v),
13885 "output[{i}]={v} is not finite or not in [0,1]"
13886 );
13887 }
13888 let non_zero = halfs.iter().any(|h| h.to_f32() > 0.01);
13889 assert!(non_zero, "all-zero output from NV12→PlanarRgb F16 CPU path");
13890 }
13891
13892 fn access_test_src(w: usize, h: usize) -> TensorDyn {
13897 use edgefirst_tensor::TensorMapTrait;
13898 let t = TensorDyn::image(
13899 w,
13900 h,
13901 PixelFormat::Rgba,
13902 DType::U8,
13903 Some(TensorMemory::Mem),
13904 edgefirst_tensor::CpuAccess::ReadWrite,
13905 )
13906 .unwrap();
13907 for (i, b) in t
13908 .as_u8()
13909 .unwrap()
13910 .map_mut()
13911 .unwrap()
13912 .as_mut_slice()
13913 .iter_mut()
13914 .enumerate()
13915 {
13916 *b = (i % 251) as u8;
13917 }
13918 t
13919 }
13920
13921 #[test]
13925 fn create_image_adds_readwrite_for_read_modify_write_passes() {
13926 use edgefirst_tensor::CpuAccess;
13927 let _lock = acquire_env_lock();
13928 let _guard = EnvGuard::snapshot(&[
13929 "EDGEFIRST_FORCE_BACKEND",
13930 "EDGEFIRST_DISABLE_GL",
13931 "EDGEFIRST_DISABLE_G2D",
13932 "EDGEFIRST_DISABLE_CPU",
13933 ]);
13934 unsafe {
13935 std::env::remove_var("EDGEFIRST_FORCE_BACKEND");
13936 std::env::remove_var("EDGEFIRST_DISABLE_CPU");
13937 std::env::set_var("EDGEFIRST_DISABLE_GL", "1");
13938 std::env::set_var("EDGEFIRST_DISABLE_G2D", "1");
13939 }
13940 let mut proc = ImageProcessor::new().unwrap();
13941 let src = access_test_src(128, 96);
13942 for (format, dtype) in [
13943 (PixelFormat::Rgb, DType::I8),
13944 (PixelFormat::Rgba, DType::I8),
13945 (PixelFormat::Bgra, DType::U8),
13946 ] {
13947 for requested in [CpuAccess::None, CpuAccess::Write] {
13948 let mut dst = proc
13949 .create_image(64, 48, format, dtype, None, requested)
13950 .unwrap();
13951 assert_eq!(
13952 dst.cpu_access(),
13953 CpuAccess::ReadWrite,
13954 "{format:?}/{dtype:?} requested {requested:?}"
13955 );
13956 proc.convert(&src, &mut dst, None).unwrap_or_else(|e| {
13957 panic!("CPU convert into {format:?}/{dtype:?} {requested:?}: {e}")
13958 });
13959 }
13960 }
13961 }
13962
13963 #[test]
13967 fn cpu_convert_refuses_a_write_only_destination_it_reads_back() {
13968 use edgefirst_tensor::{CpuAccess, Error as TensorError};
13969 let mut cpu = CPUProcessor::new();
13970 let src = access_test_src(32, 24);
13971 for (format, dtype) in [
13972 (PixelFormat::Rgb, DType::I8),
13973 (PixelFormat::Bgra, DType::U8),
13974 ] {
13975 let mut dst = TensorDyn::image(
13976 32,
13977 24,
13978 format,
13979 dtype,
13980 Some(TensorMemory::Mem),
13981 CpuAccess::Write,
13982 )
13983 .unwrap();
13984 match cpu.convert(&src, &mut dst, None) {
13985 Err(Error::Tensor(TensorError::CpuAccessDenied {
13986 declared: CpuAccess::Write,
13987 requested: CpuAccess::ReadWrite,
13988 })) => {}
13989 other => panic!("{format:?}/{dtype:?}: {other:?}"),
13990 }
13991 }
13992 let bgra_src = TensorDyn::image(
13993 32,
13994 24,
13995 PixelFormat::Bgra,
13996 DType::U8,
13997 Some(TensorMemory::Mem),
13998 CpuAccess::ReadWrite,
13999 )
14000 .unwrap();
14001 let mut dst = TensorDyn::image(
14002 32,
14003 24,
14004 PixelFormat::Bgra,
14005 DType::U8,
14006 Some(TensorMemory::Mem),
14007 CpuAccess::Write,
14008 )
14009 .unwrap();
14010 cpu.convert(&bgra_src, &mut dst, None).unwrap();
14011 }
14012
14013 #[test]
14017 fn predicted_cpu_access_unions_the_backends_that_may_run() {
14018 use edgefirst_tensor::CpuAccess::{self, None as No, Read, ReadWrite, Write};
14019 let p = predicted_cpu_access;
14020 let takes = |a: CpuAccess| Some(Some(a));
14021 let declines = Some(None);
14022 assert_eq!(
14024 p(
14025 Some(ForcedBackend::G2d),
14026 None,
14027 takes(ReadWrite),
14028 takes(Write)
14029 ),
14030 ReadWrite
14031 );
14032 assert_eq!(
14033 p(Some(ForcedBackend::G2d), None, declines, takes(Write)),
14034 No
14035 );
14036 assert_eq!(
14037 p(
14038 Some(ForcedBackend::OpenGl),
14039 takes(Write),
14040 None,
14041 takes(ReadWrite)
14042 ),
14043 Write
14044 );
14045 assert_eq!(
14046 p(Some(ForcedBackend::Cpu), takes(No), None, takes(Write)),
14047 Write
14048 );
14049 assert_eq!(p(None, takes(No), takes(ReadWrite), takes(ReadWrite)), No);
14051 assert_eq!(p(None, takes(Write), None, takes(ReadWrite)), ReadWrite);
14053 assert_eq!(p(None, declines, takes(No), takes(Write)), Write);
14055 assert_eq!(p(None, declines, takes(ReadWrite), None), ReadWrite);
14056 assert_eq!(p(None, None, None, takes(Read)), Read);
14057 assert_eq!(p(None, None, None, None), No);
14058 }
14059
14060 #[cfg(all(target_os = "linux", feature = "opengl"))]
14064 #[test]
14065 fn float_destinations_gl_declines_are_predicted_for_the_cpu() {
14066 use edgefirst_tensor::CpuAccess;
14067 let Ok(processor) = ImageProcessor::new() else {
14068 eprintln!("WARNING: SKIPPED: float prediction - no image processor");
14069 return;
14070 };
14071 let Some(gl) = processor.opengl.as_ref() else {
14072 eprintln!("WARNING: SKIPPED: float prediction - no OpenGL backend");
14073 return;
14074 };
14075 assert_eq!(
14076 gl.dst_cpu_access(TensorMemory::Mem, PixelFormat::PlanarRgb, DType::F16),
14077 None
14078 );
14079 assert_eq!(
14080 gl.dst_cpu_access(TensorMemory::DmaBuf, PixelFormat::Grey, DType::F32),
14081 None
14082 );
14083 let image = processor
14084 .create_image(
14085 64,
14086 64,
14087 PixelFormat::Rgb,
14088 DType::F32,
14089 Some(TensorMemory::Mem),
14090 CpuAccess::None,
14091 )
14092 .unwrap();
14093 assert!(image.cpu_access().writes());
14094 }
14095
14096 #[test]
14099 fn a_refused_cpu_fallback_reports_the_gpu_decline() {
14100 use edgefirst_tensor::{CpuAccess, Error as TensorError};
14101 let denied = || {
14102 Error::Tensor(TensorError::CpuAccessDenied {
14103 declared: CpuAccess::None,
14104 requested: CpuAccess::Write,
14105 })
14106 };
14107 let declined = || {
14108 vec![(
14109 "opengl",
14110 Error::NotSupported("unsupported source format".into()),
14111 )]
14112 };
14113 let e = with_gpu_declines(denied(), declined());
14114 assert!(
14115 matches!(
14116 &e,
14117 Error::CpuFallbackDenied {
14118 denied: TensorError::CpuAccessDenied { .. },
14119 ..
14120 }
14121 ),
14122 "{e:?}"
14123 );
14124 let msg = e.to_string();
14125 assert!(msg.contains("unsupported source format"), "{msg}");
14126 assert!(msg.contains("CPU access denied"), "{msg}");
14127 assert!(e.cpu_access_denied().is_some());
14128 assert!(matches!(
14129 with_gpu_declines(denied(), Vec::new()),
14130 Error::Tensor(TensorError::CpuAccessDenied { .. })
14131 ));
14132 assert!(matches!(
14133 with_gpu_declines(Error::NoConverter, declined()),
14134 Error::NoConverter
14135 ));
14136 }
14137
14138 #[test]
14142 fn create_image_adds_write_when_the_cpu_backend_fills_it() {
14143 use edgefirst_tensor::CpuAccess;
14144 let _lock = acquire_env_lock();
14145 let _guard = EnvGuard::snapshot(&[
14146 "EDGEFIRST_FORCE_BACKEND",
14147 "EDGEFIRST_DISABLE_GL",
14148 "EDGEFIRST_DISABLE_G2D",
14149 "EDGEFIRST_DISABLE_CPU",
14150 ]);
14151 unsafe {
14152 std::env::remove_var("EDGEFIRST_FORCE_BACKEND");
14153 std::env::remove_var("EDGEFIRST_DISABLE_CPU");
14154 std::env::set_var("EDGEFIRST_DISABLE_GL", "1");
14155 std::env::set_var("EDGEFIRST_DISABLE_G2D", "1");
14156 }
14157 let mut proc = ImageProcessor::new().unwrap();
14158 let src = access_test_src(128, 96);
14159 for (requested, declared) in [
14160 (CpuAccess::Read, CpuAccess::ReadWrite),
14161 (CpuAccess::None, CpuAccess::Write),
14162 (CpuAccess::Write, CpuAccess::Write),
14163 (CpuAccess::ReadWrite, CpuAccess::ReadWrite),
14164 ] {
14165 let mut dst = proc
14166 .create_image(64, 48, PixelFormat::Rgb, DType::U8, None, requested)
14167 .unwrap();
14168 assert_eq!(dst.cpu_access(), declared, "requested {requested:?}");
14169 proc.convert(&src, &mut dst, None)
14170 .unwrap_or_else(|e| panic!("CPU convert into a {requested:?} request: {e}"));
14171 }
14172 }
14173
14174 #[test]
14177 fn cpu_convert_refuses_tensors_outside_their_declaration() {
14178 use edgefirst_tensor::{CpuAccess, Error as TensorError};
14179 let mut cpu = CPUProcessor::new();
14180 let src = access_test_src(32, 24);
14181 for declared in [CpuAccess::Read, CpuAccess::None] {
14182 let mut dst = TensorDyn::image(
14183 32,
14184 24,
14185 PixelFormat::Rgb,
14186 DType::U8,
14187 Some(TensorMemory::Mem),
14188 declared,
14189 )
14190 .unwrap();
14191 match cpu.convert(&src, &mut dst, None) {
14192 Err(Error::Tensor(TensorError::CpuAccessDenied {
14193 declared: d,
14194 requested: CpuAccess::Write,
14195 })) => assert_eq!(d, declared),
14196 other => panic!("convert into a {declared:?} dst: {other:?}"),
14197 }
14198 }
14199 let write_only = TensorDyn::image(
14200 32,
14201 24,
14202 PixelFormat::Rgba,
14203 DType::U8,
14204 Some(TensorMemory::Mem),
14205 CpuAccess::Write,
14206 )
14207 .unwrap();
14208 let mut dst = TensorDyn::image(
14209 32,
14210 24,
14211 PixelFormat::Rgb,
14212 DType::U8,
14213 Some(TensorMemory::Mem),
14214 CpuAccess::ReadWrite,
14215 )
14216 .unwrap();
14217 let err = cpu.convert(&write_only, &mut dst, None).unwrap_err();
14218 assert!(
14219 matches!(
14220 err,
14221 Error::Tensor(TensorError::CpuAccessDenied {
14222 declared: CpuAccess::Write,
14223 requested: CpuAccess::Read,
14224 })
14225 ),
14226 "{err:?}"
14227 );
14228 assert!(err
14229 .to_string()
14230 .contains("declare CpuAccess::ReadWrite (the default)"));
14231 }
14232
14233 #[test]
14236 fn cpu_draw_requires_readwrite_on_the_destination() {
14237 use edgefirst_tensor::{CpuAccess, Error as TensorError};
14238 let mut cpu = CPUProcessor::new();
14239 let mut dst = TensorDyn::image(
14240 32,
14241 24,
14242 PixelFormat::Rgba,
14243 DType::U8,
14244 Some(TensorMemory::Mem),
14245 CpuAccess::Write,
14246 )
14247 .unwrap();
14248 let err = cpu
14249 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::default())
14250 .unwrap_err();
14251 assert!(
14252 matches!(
14253 err,
14254 Error::Tensor(TensorError::CpuAccessDenied {
14255 declared: CpuAccess::Write,
14256 requested: CpuAccess::ReadWrite,
14257 })
14258 ),
14259 "{err:?}"
14260 );
14261 }
14262
14263 #[test]
14268 fn create_image_desc_negotiates_and_counts_fallbacks() {
14269 use edgefirst_tensor::{CpuAccess, ImageDesc};
14270 let proc = ImageProcessor::new().unwrap();
14271
14272 let desc =
14273 ImageDesc::new(64, 64, PixelFormat::Rgba, DType::U8).with_access(CpuAccess::ReadWrite);
14274 let plain = proc.create_image_desc(&desc).unwrap();
14275 let classic = proc
14276 .create_image(
14277 64,
14278 64,
14279 PixelFormat::Rgba,
14280 DType::U8,
14281 None,
14282 CpuAccess::ReadWrite,
14283 )
14284 .unwrap();
14285 assert_eq!(plain.memory(), classic.memory(), "same negotiation path");
14286 assert_eq!(plain.compression(), None);
14287
14288 #[cfg(not(target_os = "android"))]
14289 {
14290 use edgefirst_tensor::Compression;
14291 let before = proc.compression_fallback_count();
14292 let desc = ImageDesc::new(64, 64, PixelFormat::Rgba, DType::U8)
14293 .with_access(CpuAccess::None)
14294 .with_compression(Compression::Any);
14295 let t = proc.create_image_desc(&desc).unwrap();
14296 assert_eq!(t.compression(), None, "no vendor tile scheme off-Android");
14297 assert!(
14298 proc.compression_fallback_count() > before,
14299 "Any resolving linear must count"
14300 );
14301 }
14302 }
14303
14304 #[test]
14307 #[cfg(target_os = "linux")]
14308 fn create_image_desc_contiguous_is_cma_dma_or_an_error() {
14309 use edgefirst_tensor::{Contiguity, ImageDesc, TensorMemory};
14310 let proc = ImageProcessor::new().unwrap();
14311 let desc = ImageDesc::new(64, 64, PixelFormat::Rgba, DType::U8).with_contiguous(true);
14312 match proc.create_image_desc(&desc) {
14313 Ok(t) => {
14314 assert_eq!(t.memory(), TensorMemory::DmaBuf);
14315 assert_eq!(t.contiguity(), Contiguity::Contiguous);
14316 }
14317 Err(e) => {
14318 assert!(e.to_string().contains("linux,cma"), "{e}");
14319 crate::test_support::report_skip(&format!(
14320 "{} - no CMA heap, the contiguous DMA-BUF arm did not run: {e}",
14321 function!()
14322 ));
14323 }
14324 }
14325 }
14326
14327 #[test]
14330 #[cfg(target_os = "linux")]
14331 fn create_image_f32_dma_rejected() {
14332 let proc = ImageProcessor::new().unwrap();
14333 let result = proc.create_image(
14334 64,
14335 64,
14336 PixelFormat::Rgb,
14337 DType::F32,
14338 Some(TensorMemory::DmaBuf),
14339 edgefirst_tensor::CpuAccess::ReadWrite,
14340 );
14341 assert!(
14342 result.is_err(),
14343 "create_image(F32, Dma) must fail — no DRM fourcc for f32"
14344 );
14345 }
14346
14347 #[test]
14356 #[cfg(target_os = "linux")]
14357 fn import_image_carries_colorimetry() {
14358 use edgefirst_tensor::{ColorEncoding, ColorRange, Colorimetry, TensorMemory};
14359
14360 let expected = Colorimetry::default()
14361 .with_encoding(ColorEncoding::Bt709)
14362 .with_range(ColorRange::Limited);
14363
14364 if !is_dma_available() {
14365 let mut t = TensorDyn::image(
14368 8,
14369 8,
14370 PixelFormat::Rgba,
14371 DType::U8,
14372 Some(TensorMemory::Mem),
14373 edgefirst_tensor::CpuAccess::ReadWrite,
14374 )
14375 .expect("alloc");
14376 assert_eq!(t.colorimetry(), None, "colorimetry must start as None");
14377 t.set_colorimetry(Some(expected));
14378 assert_eq!(
14379 t.colorimetry(),
14380 Some(expected),
14381 "set_colorimetry must round-trip"
14382 );
14383 crate::test_support::report_skip("import_image_carries_colorimetry (DMA unavailable); storage contract verified via TensorDyn");
14384 return;
14385 }
14386
14387 use edgefirst_tensor::{PlaneDescriptor, Tensor};
14390
14391 let rgba_bytes = 64 * 64 * 4; let dma_tensor = Tensor::<u8>::new(
14393 &[rgba_bytes],
14394 Some(TensorMemory::DmaBuf),
14395 Some("import_test"),
14396 )
14397 .expect("dma alloc");
14398 let pd =
14399 PlaneDescriptor::new(dma_tensor.dmabuf().expect("dma fd")).expect("PlaneDescriptor");
14400
14401 let proc = ImageProcessor::new().expect("ImageProcessor");
14402 let result = proc.import_image(
14403 pd,
14404 None,
14405 64,
14406 64,
14407 PixelFormat::Rgba,
14408 DType::U8,
14409 Some(expected),
14410 );
14411 let tensor = result.expect("import_image must succeed on DMA fd");
14412 assert_eq!(
14413 tensor.colorimetry(),
14414 Some(expected),
14415 "import_image must store the supplied colorimetry on the returned TensorDyn"
14416 );
14417 }
14418}