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]
6058 #[cfg(any(
6059 target_os = "linux",
6060 target_os = "macos",
6061 target_os = "ios",
6062 target_os = "android",
6063 target_os = "windows"
6064 ))]
6065 #[cfg(feature = "opengl")]
6066 fn test_opengl_grey_zero_copy_source_to_rgb() {
6067 if !is_opengl_available() {
6068 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
6069 return;
6070 }
6071 if !crate::test_support::is_gpu_buffer_available() {
6072 crate::test_support::report_skip(&format!(
6073 "{} - zero-copy GPU buffer not available on this host",
6074 function!()
6075 ));
6076 return;
6077 }
6078 let mut gl = GLProcessorThreaded::new(None).unwrap();
6079 let h = 40usize;
6080 for w in [64usize, 62] {
6081 let grey = |memory| -> Option<TensorDyn> {
6082 let t = TensorDyn::image(
6083 w,
6084 h,
6085 PixelFormat::Grey,
6086 DType::U8,
6087 Some(memory),
6088 edgefirst_tensor::CpuAccess::ReadWrite,
6089 )
6090 .ok()?;
6091 let stride = t.effective_row_stride().unwrap_or(w);
6092 {
6093 let mut map = t.as_u8().unwrap().map().unwrap();
6094 let buf = map.as_mut_slice();
6095 for r in 0..h {
6096 for c in 0..w {
6097 buf[r * stride + c] = ((r * 13 + c * 7) as u8).wrapping_add(3);
6098 }
6099 }
6100 }
6101 Some(t)
6102 };
6103 let Some(src_gpu) = grey(TensorMemory::DmaBuf) else {
6104 crate::test_support::report_skip(&format!(
6105 "{} - no zero-copy Grey {w}x{h} buffer on this host",
6106 function!()
6107 ));
6108 return;
6109 };
6110 let src_mem = grey(TensorMemory::Mem).unwrap();
6111 for fmt in [PixelFormat::Rgba, PixelFormat::Rgb, PixelFormat::PlanarRgb] {
6112 if fmt == PixelFormat::Rgb && (w * 3) % 4 != 0 {
6113 continue;
6114 }
6115 let dst = || {
6116 TensorDyn::image(
6117 w,
6118 h,
6119 fmt,
6120 DType::U8,
6121 None,
6122 edgefirst_tensor::CpuAccess::ReadWrite,
6123 )
6124 .unwrap()
6125 };
6126 let mut cpu_dst = dst();
6127 CPUProcessor::new()
6128 .convert(&src_mem, &mut cpu_dst, None)
6129 .unwrap();
6130 let mut gl_dst = dst();
6131 let before = gl.convert_stats().unwrap();
6132 gl.convert(&src_gpu, &mut gl_dst, None).unwrap();
6133 let after = gl.convert_stats().unwrap();
6134 let imports = after.src_imports - before.src_imports;
6135 let uploads = after.src_uploads - before.src_uploads;
6136 log::info!(
6137 "{}: {w}x{h} Grey -> {fmt:?} imports={imports} uploads={uploads}",
6138 function!()
6139 );
6140 #[cfg(target_os = "linux")]
6145 if gl.transfer_backend().is_zero_copy() {
6146 assert_eq!(
6147 imports, 1,
6148 "{w}x{h} Grey -> {fmt:?}: the DMA-BUF source was not imported"
6149 );
6150 }
6151 assert_grey_rgb_rows_match(&gl_dst, &cpu_dst, fmt, w, h);
6152 }
6153 }
6154 }
6155
6156 #[cfg(feature = "opengl")]
6160 fn assert_grey_rgb_rows_match(
6161 gl_dst: &TensorDyn,
6162 cpu_dst: &TensorDyn,
6163 fmt: PixelFormat,
6164 w: usize,
6165 h: usize,
6166 ) {
6167 let (rows, row_bytes) = match fmt {
6168 PixelFormat::PlanarRgb => (3 * h, w),
6169 _ => (h, w * fmt.channels()),
6170 };
6171 let gl_t = gl_dst.as_u8().unwrap();
6172 let cpu_t = cpu_dst.as_u8().unwrap();
6173 let gl_stride = gl_t.effective_row_stride().unwrap_or(row_bytes);
6174 let cpu_stride = cpu_t.effective_row_stride().unwrap_or(row_bytes);
6175 let gl_map = gl_t.map().unwrap();
6176 let cpu_map = cpu_t.map().unwrap();
6177 for r in 0..rows {
6178 let g = &gl_map.as_slice()[r * gl_stride..][..row_bytes];
6179 let c = &cpu_map.as_slice()[r * cpu_stride..][..row_bytes];
6180 if let Some(i) = (0..row_bytes).find(|&i| g[i].abs_diff(c[i]) > 4) {
6181 panic!(
6182 "{w}x{h} Grey -> {fmt:?}: row {r} byte {i} is {} on GL, {} on CPU",
6183 g[i], c[i]
6184 );
6185 }
6186 }
6187 }
6188
6189 #[test]
6190 #[cfg(target_os = "linux")]
6191 fn test_g2d_src_crop() {
6192 if !is_g2d_available() {
6193 crate::test_support::report_skip(
6194 "test_g2d_src_crop - G2D library (libg2d.so.2) not available",
6195 );
6196 return;
6197 }
6198 if !is_dma_available() {
6199 crate::test_support::report_skip("test_g2d_src_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)");
6200 return;
6201 }
6202
6203 let dst_width = 640;
6204 let dst_height = 640;
6205 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
6206 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
6207
6208 let cpu_dst = TensorDyn::image(
6209 dst_width,
6210 dst_height,
6211 PixelFormat::Rgba,
6212 DType::U8,
6213 None,
6214 edgefirst_tensor::CpuAccess::ReadWrite,
6215 )
6216 .unwrap();
6217 let mut cpu_converter = CPUProcessor::new();
6218 let crop = Crop::new().with_source(Some(Region::new(0, 0, 640, 360)));
6219 let (result, src, cpu_dst) = convert_img(
6220 &mut cpu_converter,
6221 src,
6222 cpu_dst,
6223 Rotation::None,
6224 Flip::None,
6225 crop,
6226 );
6227 result.unwrap();
6228
6229 let g2d_dst = TensorDyn::image(
6230 dst_width,
6231 dst_height,
6232 PixelFormat::Rgba,
6233 DType::U8,
6234 None,
6235 edgefirst_tensor::CpuAccess::ReadWrite,
6236 )
6237 .unwrap();
6238 let mut g2d_converter = G2DProcessor::new().unwrap();
6239 let (result, _src, g2d_dst) = convert_img(
6240 &mut g2d_converter,
6241 src,
6242 g2d_dst,
6243 Rotation::None,
6244 Flip::None,
6245 crop,
6246 );
6247 result.unwrap();
6248
6249 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
6255 }
6256
6257 #[test]
6258 #[cfg(target_os = "linux")]
6259 fn test_g2d_dst_crop() {
6260 if !is_g2d_available() {
6261 crate::test_support::report_skip(
6262 "test_g2d_dst_crop - G2D library (libg2d.so.2) not available",
6263 );
6264 return;
6265 }
6266 if !is_dma_available() {
6267 crate::test_support::report_skip("test_g2d_dst_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)");
6268 return;
6269 }
6270
6271 let dst_width = 640;
6272 let dst_height = 640;
6273 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
6274 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
6275
6276 let cpu_dst = TensorDyn::image(
6277 dst_width,
6278 dst_height,
6279 PixelFormat::Rgba,
6280 DType::U8,
6281 None,
6282 edgefirst_tensor::CpuAccess::ReadWrite,
6283 )
6284 .unwrap();
6285 let mut cpu_converter = CPUProcessor::new();
6286 let crop = Crop::new();
6287 let (result, src, cpu_dst) = convert_img(
6288 &mut cpu_converter,
6289 src,
6290 cpu_dst,
6291 Rotation::None,
6292 Flip::None,
6293 crop,
6294 );
6295 result.unwrap();
6296
6297 let g2d_dst = TensorDyn::image(
6298 dst_width,
6299 dst_height,
6300 PixelFormat::Rgba,
6301 DType::U8,
6302 None,
6303 edgefirst_tensor::CpuAccess::ReadWrite,
6304 )
6305 .unwrap();
6306 let mut g2d_converter = G2DProcessor::new().unwrap();
6307 let (result, _src, g2d_dst) = convert_img(
6308 &mut g2d_converter,
6309 src,
6310 g2d_dst,
6311 Rotation::None,
6312 Flip::None,
6313 crop,
6314 );
6315 result.unwrap();
6316
6317 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
6323 }
6324
6325 #[test]
6326 #[cfg(target_os = "linux")]
6327 fn test_g2d_all_rgba() {
6328 if !is_g2d_available() {
6329 crate::test_support::report_skip(
6330 "test_g2d_all_rgba - G2D library (libg2d.so.2) not available",
6331 );
6332 return;
6333 }
6334 if !is_dma_available() {
6335 crate::test_support::report_skip("test_g2d_all_rgba - DMA memory allocation not available (permission denied or no DMA-BUF support)");
6336 return;
6337 }
6338
6339 let dst_width = 640;
6340 let dst_height = 640;
6341 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
6342 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
6343 let src_dyn = src;
6344
6345 let mut cpu_dst = TensorDyn::image(
6346 dst_width,
6347 dst_height,
6348 PixelFormat::Rgba,
6349 DType::U8,
6350 None,
6351 edgefirst_tensor::CpuAccess::ReadWrite,
6352 )
6353 .unwrap();
6354 let mut cpu_converter = CPUProcessor::new();
6355 let mut g2d_dst = TensorDyn::image(
6356 dst_width,
6357 dst_height,
6358 PixelFormat::Rgba,
6359 DType::U8,
6360 None,
6361 edgefirst_tensor::CpuAccess::ReadWrite,
6362 )
6363 .unwrap();
6364 let mut g2d_converter = G2DProcessor::new().unwrap();
6365
6366 let crop = Crop::new().with_source(Some(Region::new(50, 120, 1024, 576)));
6367
6368 for rot in [
6369 Rotation::None,
6370 Rotation::Clockwise90,
6371 Rotation::Rotate180,
6372 Rotation::CounterClockwise90,
6373 ] {
6374 cpu_dst
6375 .as_typed::<u8>()
6376 .unwrap()
6377 .map()
6378 .unwrap()
6379 .as_mut_slice()
6380 .fill(114);
6381 g2d_dst
6382 .as_typed::<u8>()
6383 .unwrap()
6384 .map()
6385 .unwrap()
6386 .as_mut_slice()
6387 .fill(114);
6388 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
6389 let mut cpu_dst_dyn = cpu_dst;
6390 cpu_converter
6391 .convert(
6392 &src_dyn,
6393 &mut cpu_dst_dyn,
6394 Some(crate::ConvertOptions::new().crop(crop)),
6395 )
6396 .unwrap();
6397 cpu_dst = {
6398 let mut __t = cpu_dst_dyn.into_u8().unwrap();
6399 __t.set_format(PixelFormat::Rgba).unwrap();
6400 TensorDyn::from(__t)
6401 };
6402
6403 let mut g2d_dst_dyn = g2d_dst;
6404 g2d_converter
6405 .convert(
6406 &src_dyn,
6407 &mut g2d_dst_dyn,
6408 Some(crate::ConvertOptions::new().crop(crop)),
6409 )
6410 .unwrap();
6411 g2d_dst = {
6412 let mut __t = g2d_dst_dyn.into_u8().unwrap();
6413 __t.set_format(PixelFormat::Rgba).unwrap();
6414 TensorDyn::from(__t)
6415 };
6416
6417 compare_images(
6418 &g2d_dst,
6419 &cpu_dst,
6420 0.98,
6421 &format!("{} {:?} {:?}", function!(), rot, flip),
6422 );
6423 }
6424 }
6425 }
6426
6427 #[test]
6428 #[cfg(any(
6429 target_os = "linux",
6430 target_os = "macos",
6431 target_os = "ios",
6432 target_os = "android",
6433 target_os = "windows"
6434 ))]
6435 #[cfg(feature = "opengl")]
6436 fn test_opengl_src_crop() {
6437 if !is_opengl_available() {
6438 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
6439 return;
6440 }
6441
6442 let dst_width = 640;
6443 let dst_height = 360;
6444 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
6445 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
6446 let crop = Crop::new().with_source(Some(Region::new(320, 180, 1280 - 320, 720 - 180)));
6447
6448 let cpu_dst = TensorDyn::image(
6449 dst_width,
6450 dst_height,
6451 PixelFormat::Rgba,
6452 DType::U8,
6453 None,
6454 edgefirst_tensor::CpuAccess::ReadWrite,
6455 )
6456 .unwrap();
6457 let mut cpu_converter = CPUProcessor::new();
6458 let (result, src, cpu_dst) = convert_img(
6459 &mut cpu_converter,
6460 src,
6461 cpu_dst,
6462 Rotation::None,
6463 Flip::None,
6464 crop,
6465 );
6466 result.unwrap();
6467
6468 let gl_dst = TensorDyn::image(
6469 dst_width,
6470 dst_height,
6471 PixelFormat::Rgba,
6472 DType::U8,
6473 None,
6474 edgefirst_tensor::CpuAccess::ReadWrite,
6475 )
6476 .unwrap();
6477 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
6478 let (result, _src, gl_dst) = convert_img(
6479 &mut gl_converter,
6480 src,
6481 gl_dst,
6482 Rotation::None,
6483 Flip::None,
6484 crop,
6485 );
6486 result.unwrap();
6487
6488 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
6489 }
6490
6491 #[test]
6492 #[cfg(any(
6493 target_os = "linux",
6494 target_os = "macos",
6495 target_os = "ios",
6496 target_os = "android",
6497 target_os = "windows"
6498 ))]
6499 #[cfg(feature = "opengl")]
6500 fn test_opengl_dst_crop() {
6501 if !is_opengl_available() {
6502 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
6503 return;
6504 }
6505
6506 let dst_width = 640;
6507 let dst_height = 640;
6508 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
6509 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
6510
6511 let cpu_dst = TensorDyn::image(
6512 dst_width,
6513 dst_height,
6514 PixelFormat::Rgba,
6515 DType::U8,
6516 None,
6517 edgefirst_tensor::CpuAccess::ReadWrite,
6518 )
6519 .unwrap();
6520 let mut cpu_converter = CPUProcessor::new();
6521 let crop = Crop::new();
6522 let (result, src, cpu_dst) = convert_img(
6523 &mut cpu_converter,
6524 src,
6525 cpu_dst,
6526 Rotation::None,
6527 Flip::None,
6528 crop,
6529 );
6530 result.unwrap();
6531
6532 let gl_dst = TensorDyn::image(
6533 dst_width,
6534 dst_height,
6535 PixelFormat::Rgba,
6536 DType::U8,
6537 None,
6538 edgefirst_tensor::CpuAccess::ReadWrite,
6539 )
6540 .unwrap();
6541 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
6542 let (result, _src, gl_dst) = convert_img(
6543 &mut gl_converter,
6544 src,
6545 gl_dst,
6546 Rotation::None,
6547 Flip::None,
6548 crop,
6549 );
6550 result.unwrap();
6551
6552 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
6553 }
6554 #[cfg(all(target_os = "windows", feature = "opengl"))]
6558 fn synthetic_rgba(w: usize, h: usize) -> TensorDyn {
6559 let t = TensorDyn::image(
6560 w,
6561 h,
6562 PixelFormat::Rgba,
6563 DType::U8,
6564 Some(TensorMemory::Mem),
6565 edgefirst_tensor::CpuAccess::ReadWrite,
6566 )
6567 .unwrap();
6568 fill_synthetic_rgba(&t);
6569 t
6570 }
6571
6572 #[cfg(all(target_os = "windows", feature = "opengl"))]
6577 fn assert_pixels_identical(actual: &TensorDyn, expected: &TensorDyn, name: &str) {
6578 assert_eq!(actual.shape(), expected.shape(), "{name}: shapes differ");
6579 let flat = |t: &TensorDyn| {
6580 let typed = t.as_typed::<u8>().expect("u8 image");
6581 let mut out = vec![0u8; t.shape().iter().product::<usize>()];
6582 typed.copy_to_flat(&mut out).expect("compact the rows");
6583 out
6584 };
6585 let (a, e) = (flat(actual), flat(expected));
6586 let mismatches = a.iter().zip(&e).filter(|(x, y)| x != y).count();
6587 assert_eq!(
6588 mismatches,
6589 0,
6590 "{name}: {mismatches} of {} bytes differ (first rows: {:?} vs {:?})",
6591 a.len(),
6592 &a[..16.min(a.len())],
6593 &e[..16.min(e.len())]
6594 );
6595 }
6596
6597 #[cfg(all(target_os = "windows", feature = "opengl"))]
6600 fn fill_synthetic_rgba(t: &TensorDyn) {
6601 let w = t.width().unwrap();
6602 let h = t.height().unwrap();
6603 let bound = t.as_typed::<u8>().unwrap();
6604 let stride = bound.effective_row_stride().unwrap_or(w * 4);
6605 let mut m = bound.map().unwrap();
6606 let px = m.as_mut_slice();
6607 for y in 0..h {
6608 for x in 0..w {
6609 let o = y * stride + x * 4;
6610 px[o] = x as u8;
6611 px[o + 1] = y as u8;
6612 px[o + 2] = (x + y) as u8;
6613 px[o + 3] = 255;
6614 }
6615 }
6616 }
6617
6618 #[cfg(all(target_os = "windows", feature = "opengl"))]
6627 fn compare_float_images(a: &TensorDyn, b: &TensorDyn, tol: f64, what: &str) {
6628 assert_eq!(a.format(), b.format(), "{what}: pixel formats differ");
6629 assert_eq!(a.dtype(), b.dtype(), "{what}: dtypes differ");
6630 assert_eq!(a.shape(), b.shape(), "{what}: shapes differ");
6631 let values = |t: &TensorDyn| -> Vec<f64> {
6632 let elems: usize = t.shape().iter().product();
6633 match t.dtype() {
6634 DType::F32 => {
6635 let typed = t.as_typed::<f32>().expect("F32 image");
6636 let mut bytes = vec![0u8; elems * 4];
6637 typed
6638 .copy_to_flat(&mut bytes)
6639 .expect("compact the padded rows");
6640 bytes
6641 .as_chunks::<4>()
6642 .0
6643 .iter()
6644 .map(|c| f32::from_le_bytes(*c) as f64)
6645 .collect()
6646 }
6647 DType::F16 => {
6648 let typed = t.as_typed::<half::f16>().expect("F16 image");
6649 let mut bytes = vec![0u8; elems * 2];
6650 typed
6651 .copy_to_flat(&mut bytes)
6652 .expect("compact the padded rows");
6653 bytes
6654 .as_chunks::<2>()
6655 .0
6656 .iter()
6657 .map(|c| half::f16::from_le_bytes(*c).to_f64())
6658 .collect()
6659 }
6660 other => panic!("{what}: {other:?} is not a float dtype"),
6661 }
6662 };
6663 let (av, bv) = (values(a), values(b));
6664 assert_eq!(av.len(), bv.len(), "{what}: element counts differ");
6665 for (i, (x, y)) in av.iter().zip(bv.iter()).enumerate() {
6666 assert!(
6667 (x - y).abs() <= tol,
6668 "{what}: element {i} differs by {} (gpu {x}, cpu {y}, tol {tol})",
6669 (x - y).abs()
6670 );
6671 }
6672 }
6673
6674 #[test]
6697 #[cfg(all(target_os = "windows", feature = "opengl"))]
6698 fn windows_padded_float_pbo_rows_land_at_the_declared_stride() {
6699 if !is_opengl_available() {
6700 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
6701 return;
6702 }
6703 const W: usize = 64;
6704 const H: usize = 32;
6705 const POOL_W: usize = 96;
6706
6707 let mut gpu = ImageProcessor::new().unwrap();
6708 let support = gpu.supported_render_dtypes();
6709 let src = synthetic_rgba(W, H);
6710
6711 let cases: [(PixelFormat, DType, bool, &[usize]); 2] = [
6713 (
6714 PixelFormat::PlanarRgb,
6715 DType::F16,
6716 support.f16,
6717 &[W * 2, POOL_W * 2, W * 2 + 4],
6720 ),
6721 (
6722 PixelFormat::Rgb,
6723 DType::F32,
6724 support.f32,
6725 &[W * 3 * 4, POOL_W * 3 * 4],
6726 ),
6727 ];
6728
6729 for (fmt, dtype, supported, strides) in cases {
6730 if !supported {
6731 crate::test_support::report_skip(&format!(
6732 "{} - {fmt:?}/{dtype:?} not reported",
6733 function!()
6734 ));
6735 continue;
6736 }
6737 let Some(gl) = gpu.opengl.as_ref() else {
6738 crate::test_support::report_skip(&format!("{} - no GL processor", function!()));
6739 return;
6740 };
6741 let mut reference = gl
6743 .create_pbo_image_dtype(W, H, fmt, dtype)
6744 .unwrap_or_else(|e| panic!("{fmt:?}/{dtype:?}: tight reference PBO: {e:?}"));
6745 assert_eq!(
6746 reference.memory(),
6747 TensorMemory::Pbo,
6748 "{fmt:?}/{dtype:?}: the destination must be a PBO to reach this readback"
6749 );
6750 gpu.convert(&src, &mut reference, None)
6751 .unwrap_or_else(|e| panic!("{fmt:?}/{dtype:?}: tight convert failed: {e}"));
6752
6753 for &stride in strides {
6754 let label = format!("{fmt:?}/{dtype:?} at a {stride} B pitch");
6755 let mut padded = gpu
6758 .opengl
6759 .as_ref()
6760 .expect("GL processor")
6761 .create_pbo_image_dtype(POOL_W, H, fmt, dtype)
6762 .unwrap_or_else(|e| panic!("{label}: pool PBO: {e:?}"));
6763 padded
6764 .configure_image(W, H, fmt)
6765 .unwrap_or_else(|e| panic!("{label}: narrow the pool tensor: {e}"));
6766 padded
6767 .set_row_stride(stride)
6768 .unwrap_or_else(|e| panic!("{label}: declare the pitch: {e}"));
6769 assert_eq!(padded.effective_row_stride(), Some(stride), "{label}");
6770
6771 let before = gpu.convert_fallback_count();
6772 gpu.convert(&src, &mut padded, None)
6773 .unwrap_or_else(|e| panic!("{label}: padded convert failed: {e}"));
6774 assert_eq!(
6777 gpu.convert_fallback_count(),
6778 before,
6779 "{label}: fell back to the CPU backend, so the float PBO readback under test never ran"
6780 );
6781
6782 let tol = if dtype == DType::F16 {
6783 1.0 / 1024.0
6784 } else {
6785 1e-6
6786 };
6787 compare_float_images(&padded, &reference, tol, &label);
6788 }
6789 }
6790 }
6791
6792 #[test]
6802 #[cfg(all(target_os = "windows", feature = "opengl"))]
6803 fn windows_float_destinations_match_the_cpu_converter_for_every_layout() {
6804 if !is_opengl_available() {
6805 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
6806 return;
6807 }
6808 if !crate::test_support::is_gpu_buffer_available() {
6809 crate::test_support::report_skip(&format!(
6810 "{} - no zero-copy GPU buffers",
6811 function!()
6812 ));
6813 return;
6814 }
6815 let mut gpu = ImageProcessor::new().unwrap();
6816 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
6817 backend: ComputeBackend::Cpu,
6818 ..Default::default()
6819 })
6820 .unwrap();
6821 let support = gpu.supported_render_dtypes();
6822 eprintln!("{}: supported_render_dtypes = {support:?}", function!());
6823 let src = synthetic_rgba(640, 480);
6824 for (fmt, dtype, need) in [
6825 (PixelFormat::PlanarRgb, DType::F16, support.f16),
6826 (PixelFormat::PlanarRgb, DType::F32, support.f32),
6827 (PixelFormat::PlanarRgba, DType::F16, support.f16),
6828 (PixelFormat::PlanarRgba, DType::F32, support.f32),
6829 (PixelFormat::Rgb, DType::F16, support.f16),
6830 (PixelFormat::Rgb, DType::F32, support.f32),
6831 (PixelFormat::Rgba, DType::F16, support.f16),
6832 (PixelFormat::Rgba, DType::F32, support.f32),
6833 ] {
6834 if !need {
6835 crate::test_support::report_skip(&format!(
6836 "{} - {fmt:?}/{dtype:?} not reported",
6837 function!()
6838 ));
6839 continue;
6840 }
6841 let mut dst_gpu = gpu
6842 .create_image(
6843 640,
6844 480,
6845 fmt,
6846 dtype,
6847 Some(TensorMemory::DmaBuf),
6848 edgefirst_tensor::CpuAccess::ReadWrite,
6849 )
6850 .unwrap();
6851 assert_eq!(
6852 dst_gpu.memory(),
6853 TensorMemory::DmaBuf,
6854 "{fmt:?}/{dtype:?} must be a texture"
6855 );
6856 let mut dst_cpu = TensorDyn::image(
6857 640,
6858 480,
6859 fmt,
6860 dtype,
6861 Some(TensorMemory::Mem),
6862 edgefirst_tensor::CpuAccess::ReadWrite,
6863 )
6864 .unwrap();
6865 let before = gpu.convert_fallback_count();
6866 gpu.convert(&src, &mut dst_gpu, None).unwrap();
6867 cpu.convert(&src, &mut dst_cpu, None).unwrap();
6868 assert_eq!(
6869 gpu.convert_fallback_count(),
6870 before,
6871 "{fmt:?}/{dtype:?} fell back to CPU"
6872 );
6873 let tol = if dtype == DType::F16 {
6874 1.0 / 1024.0
6875 } else {
6876 1e-6
6877 };
6878 compare_float_images(&dst_gpu, &dst_cpu, tol, &format!("{fmt:?}/{dtype:?}"));
6879 }
6880 }
6881
6882 #[cfg(target_os = "windows")]
6885 #[link(name = "kernel32")]
6886 extern "system" {
6887 fn WaitForSingleObject(handle: *mut std::ffi::c_void, milliseconds: u32) -> u32;
6888 }
6889
6890 #[cfg(all(target_os = "windows", feature = "opengl"))]
6899 #[test]
6900 fn windows_deferred_batch_records_one_value_per_tile_and_submits_once() {
6901 use edgefirst_tensor::CpuAccess;
6902
6903 if !crate::test_support::is_gpu_buffer_available() {
6904 crate::test_support::report_skip(&format!(
6905 "{} - no zero-copy GPU buffers",
6906 function!()
6907 ));
6908 return;
6909 }
6910 let mut p = match ImageProcessor::new() {
6911 Ok(p) => p,
6912 Err(e) => {
6913 crate::test_support::report_skip(&format!(
6914 "{} - ImageProcessor init failed ({e:?})",
6915 function!()
6916 ));
6917 return;
6918 }
6919 };
6920 let (w, h, tiles) = (640usize, 480usize, 4usize);
6921 let src = synthetic_rgba(w, h);
6922 let mut dsts: Vec<TensorDyn> = (0..tiles)
6923 .map(|_| {
6924 p.create_image(w, h, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6925 .unwrap()
6926 })
6927 .collect();
6928 if dsts[0].memory() != TensorMemory::DmaBuf {
6929 crate::test_support::report_skip(&format!(
6930 "{} - destinations are not textures",
6931 function!()
6932 ));
6933 return;
6934 }
6935 for dst in &mut dsts {
6936 p.convert_deferred(&src, dst, None).unwrap();
6937 }
6938 let values: Vec<u64> = dsts
6939 .iter()
6940 .map(|d| {
6941 d.gpu_completion()
6942 .unwrap()
6943 .expect("each deferred convert records its own completion")
6944 .value
6945 })
6946 .collect();
6947 for pair in values.windows(2) {
6948 assert!(
6949 pair[1] > pair[0],
6950 "the batch allocated {values:?}, which is not one increasing value per tile"
6951 );
6952 }
6953 p.flush().unwrap();
6954 let dev = edgefirst_tensor::d3d11::device().unwrap();
6957 let last = *values.last().unwrap();
6958 let ev = dev.event_for(last).unwrap();
6959 assert_eq!(
6962 unsafe {
6963 WaitForSingleObject(
6964 std::os::windows::io::AsRawHandle::as_raw_handle(&ev) as _,
6965 5000,
6966 )
6967 },
6968 0,
6969 "the batch's newest value {last} never arrived after flush"
6970 );
6971
6972 let mut solo = p
6973 .create_image(w, h, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6974 .unwrap();
6975 p.convert(&src, &mut solo, None).unwrap();
6976 let flat = |img: &TensorDyn| -> Vec<u8> {
6980 let t = img.as_typed::<u8>().expect("u8 image");
6981 let mut out = vec![0u8; t.shape().iter().product::<usize>()];
6982 t.copy_to_flat(&mut out).expect("compact the padded rows");
6983 out
6984 };
6985 let want = flat(&solo);
6986 for (i, dst) in dsts.iter().enumerate() {
6987 assert_eq!(
6988 flat(dst),
6989 want,
6990 "tile {i} does not match a standalone convert"
6991 );
6992 }
6993 }
6994
6995 #[cfg(all(target_os = "windows", feature = "opengl"))]
7004 #[test]
7005 fn windows_dropping_a_processor_submits_an_open_batch() {
7006 use edgefirst_tensor::CpuAccess;
7007
7008 if !crate::test_support::is_gpu_buffer_available() {
7009 crate::test_support::report_skip(&format!(
7010 "{} - no zero-copy GPU buffers",
7011 function!()
7012 ));
7013 return;
7014 }
7015 let src = synthetic_rgba(320, 240);
7016 let recorded = {
7017 let mut p = match ImageProcessor::new() {
7018 Ok(p) => p,
7019 Err(e) => {
7020 crate::test_support::report_skip(&format!(
7021 "{} - init failed ({e:?})",
7022 function!()
7023 ));
7024 return;
7025 }
7026 };
7027 let mut dst = p
7028 .create_image(
7029 320,
7030 240,
7031 PixelFormat::Rgba,
7032 DType::U8,
7033 None,
7034 CpuAccess::Read,
7035 )
7036 .unwrap();
7037 if dst.memory() != TensorMemory::DmaBuf {
7038 crate::test_support::report_skip(&format!(
7039 "{} - destination is not a texture",
7040 function!()
7041 ));
7042 return;
7043 }
7044 p.convert_deferred(&src, &mut dst, None).unwrap();
7045 dst.gpu_completion()
7047 .unwrap()
7048 .expect("the deferred convert recorded a completion")
7049 .value
7050 };
7051
7052 let dev = edgefirst_tensor::d3d11::device().unwrap();
7053 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
7054 while dev.completed_value() < recorded {
7055 assert!(
7056 std::time::Instant::now() < deadline,
7057 "the dropped processor never submitted its batch: fence at {}, recorded {recorded}",
7058 dev.completed_value()
7059 );
7060 std::thread::yield_now();
7061 }
7062 }
7063
7064 #[cfg(all(target_os = "windows", feature = "opengl"))]
7069 #[test]
7070 fn windows_create_image_prefers_textures_and_convert_with_fence_returns_a_set_event() {
7071 use edgefirst_tensor::CpuAccess;
7072 use std::os::windows::io::AsRawHandle;
7073
7074 if !crate::test_support::is_gpu_buffer_available() {
7075 crate::test_support::report_skip(&format!(
7076 "{} - no zero-copy GPU buffers",
7077 function!()
7078 ));
7079 return;
7080 }
7081 let mut p = ImageProcessor::new().unwrap();
7082 let src = synthetic_rgba(320, 240);
7083 let mut dst = p
7084 .create_image(
7085 320,
7086 240,
7087 PixelFormat::Rgba,
7088 DType::U8,
7089 None,
7090 CpuAccess::Read,
7091 )
7092 .unwrap();
7093 if dst.memory() != TensorMemory::DmaBuf
7097 && !std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1")
7098 {
7099 crate::test_support::report_skip(&format!(
7100 "{} - create_image gave {:?}, not a texture (no GL backend)",
7101 function!(),
7102 dst.memory()
7103 ));
7104 return;
7105 }
7106 assert_eq!(
7107 dst.memory(),
7108 TensorMemory::DmaBuf,
7109 "create_image(None) must prefer a texture on Windows"
7110 );
7111 let before = dst.gpu_completion().unwrap().map(|c| c.value);
7114 let fence = p
7115 .convert_with_fence(&src, &mut dst, None)
7116 .unwrap()
7117 .expect("Windows exports a fence");
7118 let recorded = dst
7119 .gpu_completion()
7120 .unwrap()
7121 .expect("the convert recorded a completion")
7122 .value;
7123 assert!(
7124 before.is_none_or(|b| recorded > b),
7125 "the fenced convert recorded a newer value than {before:?}"
7126 );
7127 assert_eq!(
7132 edgefirst_tensor::d3d11::device().unwrap().last_signalled(),
7133 recorded,
7134 "a fenced convert must signal exactly once"
7135 );
7136 assert_eq!(
7141 unsafe { WaitForSingleObject(fence.as_raw_handle() as _, 5000) },
7142 0,
7143 "fence was not set within 5 seconds"
7144 );
7145 assert!(
7149 edgefirst_tensor::d3d11::device().unwrap().completed_value() >= recorded,
7150 "the event fired, so the recorded value {recorded} is complete"
7151 );
7152 assert_eq!(
7153 dst.gpu_completion().unwrap().map(|c| c.value),
7154 Some(recorded),
7155 "the destination still reports the value the event waited on"
7156 );
7157 let mut expected = TensorDyn::image(
7158 320,
7159 240,
7160 PixelFormat::Rgba,
7161 DType::U8,
7162 Some(TensorMemory::Mem),
7163 CpuAccess::ReadWrite,
7164 )
7165 .unwrap();
7166 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
7167 backend: ComputeBackend::Cpu,
7168 ..Default::default()
7169 })
7170 .unwrap();
7171 cpu.convert(&src, &mut expected, None).unwrap();
7172 compare_images(&dst, &expected, 0.0, "fenced convert");
7173 }
7174
7175 #[cfg(all(target_os = "windows", feature = "opengl"))]
7180 fn texture_or_skip(t: &TensorDyn, test: &str) -> bool {
7181 if t.memory() == TensorMemory::DmaBuf {
7182 return true;
7183 }
7184 if std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1") {
7185 panic!(
7186 "{test}: create_image(None) must prefer a texture on Windows, got {:?}",
7187 t.memory()
7188 );
7189 }
7190 crate::test_support::report_skip(&format!(
7191 "{test} - create_image gave {:?}, not a texture (no GL backend)",
7192 t.memory()
7193 ));
7194 false
7195 }
7196
7197 #[cfg(all(target_os = "windows", feature = "opengl"))]
7203 #[test]
7204 fn windows_draw_decoded_masks_into_a_texture_records_a_completion() {
7205 use edgefirst_tensor::{CpuAccess, Segmentation};
7206 use ndarray::Array3;
7207
7208 if !crate::test_support::is_gpu_buffer_available() {
7209 crate::test_support::report_skip(&format!(
7210 "{} - no zero-copy GPU buffers",
7211 function!()
7212 ));
7213 return;
7214 }
7215 let mut p = ImageProcessor::new().unwrap();
7216 let mut dst = p
7217 .create_image(64, 64, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
7218 .unwrap();
7219 if !texture_or_skip(&dst, function!()) {
7220 return;
7221 }
7222 assert!(
7223 dst.gpu_completion().unwrap().is_none(),
7224 "nothing has written this texture yet"
7225 );
7226
7227 let detect = DetectBox {
7228 bbox: [0.25, 0.25, 0.75, 0.75].into(),
7229 score: 0.99,
7230 label: 0,
7231 };
7232 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
7233 let seg = Segmentation {
7234 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
7235 .unwrap()
7236 .into(),
7237 xmin: 0.25,
7238 ymin: 0.25,
7239 xmax: 0.75,
7240 ymax: 0.75,
7241 };
7242 p.draw_decoded_masks(&mut dst, &[detect], &[seg], MaskOverlay::default())
7243 .expect("draw into a read-only texture");
7244
7245 let completion = dst
7246 .gpu_completion()
7247 .unwrap()
7248 .expect("the draw recorded no completion on dst");
7249 assert!(completion.value > 0, "completion value must be non-zero");
7250
7251 let mut flat = vec![0u8; 64 * 64 * 4];
7256 dst.as_typed::<u8>()
7257 .unwrap()
7258 .copy_to_flat(&mut flat)
7259 .expect("read the drawn texture");
7260 let px = |x: usize, y: usize| -> [u8; 4] {
7261 let o = (y * 64 + x) * 4;
7262 [flat[o], flat[o + 1], flat[o + 2], flat[o + 3]]
7263 };
7264 assert_eq!(px(2, 2), [0, 0, 0, 0], "corner (2,2) was not cleared");
7265 assert_ne!(px(32, 32), [0, 0, 0, 0], "centre (32,32) was not coloured");
7266 }
7267
7268 #[cfg(all(target_os = "windows", feature = "opengl"))]
7271 #[test]
7272 fn windows_draw_proto_masks_into_a_texture_records_a_completion() {
7273 use edgefirst_tensor::{CpuAccess, Tensor};
7274
7275 if !crate::test_support::is_gpu_buffer_available() {
7276 crate::test_support::report_skip(&format!(
7277 "{} - no zero-copy GPU buffers",
7278 function!()
7279 ));
7280 return;
7281 }
7282 let mut p = match with_force_backend(Some("opengl"), ImageProcessor::new) {
7286 Ok(p) => p,
7287 Err(e) if !std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1") => {
7288 crate::test_support::report_skip(&format!(
7289 "{} - no OpenGL backend: {e}",
7290 function!()
7291 ));
7292 return;
7293 }
7294 Err(e) => panic!("forced OpenGL backend unavailable: {e}"),
7295 };
7296 let mut dst = p
7297 .create_image(64, 64, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
7298 .unwrap();
7299 if !texture_or_skip(&dst, function!()) {
7300 return;
7301 }
7302
7303 let detect = DetectBox {
7304 bbox: [0.25, 0.25, 0.75, 0.75].into(),
7305 score: 0.99,
7306 label: 0,
7307 };
7308 let proto = ProtoData {
7311 mask_coefficients: TensorDyn::F32(
7312 Tensor::<f32>::from_slice(&[1.0_f32], &[1, 1]).unwrap(),
7313 ),
7314 protos: TensorDyn::F32(
7315 Tensor::<f32>::from_slice(&vec![1.0_f32; 8 * 8], &[8, 8, 1]).unwrap(),
7316 ),
7317 layout: ProtoLayout::Nhwc,
7318 };
7319 p.draw_proto_masks(&mut dst, &[detect], &proto, MaskOverlay::default())
7320 .expect("forced-GL proto draw into a read-only texture");
7321
7322 let completion = dst
7323 .gpu_completion()
7324 .unwrap()
7325 .expect("the proto draw recorded no completion on dst");
7326 assert!(completion.value > 0, "completion value must be non-zero");
7327 }
7328
7329 #[cfg(all(target_os = "windows", feature = "opengl"))]
7334 #[test]
7335 fn windows_draw_decoded_masks_composites_a_host_background_onto_a_texture() {
7336 use edgefirst_tensor::CpuAccess;
7337
7338 if !crate::test_support::is_gpu_buffer_available() {
7339 crate::test_support::report_skip(&format!(
7340 "{} - no zero-copy GPU buffers",
7341 function!()
7342 ));
7343 return;
7344 }
7345 let mut p = ImageProcessor::new().unwrap();
7346 let bg = synthetic_rgba(64, 48);
7347 let mut dst = p
7348 .create_image(64, 48, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
7349 .unwrap();
7350 if !texture_or_skip(&dst, function!()) {
7351 return;
7352 }
7353 p.draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::new().with_background(&bg))
7354 .expect("empty draw with a host background onto a texture");
7355 assert_pixels_identical(&dst, &bg, "host background onto a texture");
7356 assert!(dst.gpu_completion().unwrap().is_some());
7357 }
7358
7359 #[cfg(all(target_os = "windows", feature = "opengl"))]
7362 #[test]
7363 fn windows_draw_decoded_masks_composites_a_texture_background_onto_a_texture() {
7364 use edgefirst_tensor::CpuAccess;
7365
7366 if !crate::test_support::is_gpu_buffer_available() {
7367 crate::test_support::report_skip(&format!(
7368 "{} - no zero-copy GPU buffers",
7369 function!()
7370 ));
7371 return;
7372 }
7373 let mut p = ImageProcessor::new().unwrap();
7374 let expected = synthetic_rgba(64, 48);
7375 let bg = p
7376 .create_image(
7377 64,
7378 48,
7379 PixelFormat::Rgba,
7380 DType::U8,
7381 None,
7382 CpuAccess::ReadWrite,
7383 )
7384 .unwrap();
7385 if !texture_or_skip(&bg, function!()) {
7386 return;
7387 }
7388 fill_synthetic_rgba(&bg);
7389 let mut dst = p
7390 .create_image(64, 48, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
7391 .unwrap();
7392 assert_eq!(dst.memory(), TensorMemory::DmaBuf);
7393 p.draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::new().with_background(&bg))
7394 .expect("empty draw with a texture background onto a texture");
7395 assert_pixels_identical(&dst, &expected, "texture background onto a texture");
7396 assert!(dst.gpu_completion().unwrap().is_some());
7397 }
7398
7399 #[cfg(all(target_os = "windows", feature = "opengl"))]
7405 #[test]
7406 fn windows_draw_decoded_masks_into_a_texture_view_fills_only_its_band() {
7407 use edgefirst_tensor::CpuAccess;
7408
7409 if !crate::test_support::is_gpu_buffer_available() {
7410 crate::test_support::report_skip(&format!(
7411 "{} - no zero-copy GPU buffers",
7412 function!()
7413 ));
7414 return;
7415 }
7416 let (w, h) = (32usize, 64usize);
7417 let mut p = ImageProcessor::new().unwrap();
7418 let parent = p
7419 .create_image(
7420 w,
7421 h,
7422 PixelFormat::Rgba,
7423 DType::U8,
7424 None,
7425 CpuAccess::ReadWrite,
7426 )
7427 .unwrap();
7428 if !texture_or_skip(&parent, function!()) {
7429 return;
7430 }
7431 fill_synthetic_rgba(&parent);
7432
7433 let mut band = parent
7437 .view(edgefirst_tensor::Region::new(0, h / 2, w, h / 2))
7438 .unwrap();
7439 p.draw_decoded_masks(&mut band, &[], &[], MaskOverlay::default())
7440 .expect("empty draw into a texture view");
7441 drop(band);
7442
7443 let mut flat = vec![0u8; w * h * 4];
7444 parent
7445 .as_typed::<u8>()
7446 .unwrap()
7447 .copy_to_flat(&mut flat)
7448 .expect("read the parent texture");
7449 for y in 0..h {
7450 for x in 0..w {
7451 let o = (y * w + x) * 4;
7452 let got = [flat[o], flat[o + 1], flat[o + 2], flat[o + 3]];
7453 let want = if y < h / 2 {
7454 [x as u8, y as u8, (x + y) as u8, 255]
7455 } else {
7456 [0, 0, 0, 0]
7457 };
7458 assert_eq!(got, want, "pixel ({x},{y})");
7459 }
7460 }
7461
7462 let mut forced = with_force_backend(Some("opengl"), ImageProcessor::new).unwrap();
7465 let mut band = parent
7466 .view(edgefirst_tensor::Region::new(0, h / 2, w, h / 2))
7467 .unwrap();
7468 let err = forced
7469 .draw_decoded_masks(&mut band, &[], &[], MaskOverlay::default())
7470 .expect_err("forced GL must decline a view destination");
7471 assert!(
7472 matches!(err, Error::NotSupported(_)),
7473 "expected NotSupported, got {err:?}"
7474 );
7475 }
7476
7477 #[cfg(all(target_os = "windows", feature = "opengl"))]
7482 #[test]
7483 fn windows_class_colors_reach_the_cpu_backend_behind_a_declined_gl_draw() {
7484 use edgefirst_tensor::{CpuAccess, Segmentation};
7485 use ndarray::Array3;
7486
7487 if !crate::test_support::is_gpu_buffer_available() {
7488 crate::test_support::report_skip(&format!(
7489 "{} - no zero-copy GPU buffers",
7490 function!()
7491 ));
7492 return;
7493 }
7494 const CUSTOM: [u8; 4] = [7, 200, 13, 255];
7495 assert_ne!(
7496 CUSTOM[..3],
7497 DEFAULT_COLORS_U8[0][..3],
7498 "the custom colour must differ from the default palette's, or \
7499 this test cannot tell them apart"
7500 );
7501
7502 let (w, h) = (32usize, 64usize);
7503 let mut p = ImageProcessor::new().unwrap();
7504 let parent = p
7505 .create_image(
7506 w,
7507 h,
7508 PixelFormat::Rgba,
7509 DType::U8,
7510 None,
7511 CpuAccess::ReadWrite,
7512 )
7513 .unwrap();
7514 if parent.memory() != TensorMemory::DmaBuf
7519 && !std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1")
7520 {
7521 crate::test_support::report_skip(&format!(
7522 "{} - create_image gave {:?}, not a texture (no GL backend)",
7523 function!(),
7524 parent.memory()
7525 ));
7526 return;
7527 }
7528 p.set_class_colors(&[CUSTOM]).unwrap();
7529
7530 let detect = DetectBox {
7531 bbox: [0.25, 0.25, 0.75, 0.75].into(),
7532 score: 0.99,
7533 label: 0,
7534 };
7535 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
7536 let seg = Segmentation {
7537 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
7538 .unwrap()
7539 .into(),
7540 xmin: 0.25,
7541 ymin: 0.25,
7542 xmax: 0.75,
7543 ymax: 0.75,
7544 };
7545 let mut band = parent
7546 .view(edgefirst_tensor::Region::new(0, h / 2, w, h / 2))
7547 .unwrap();
7548 p.draw_decoded_masks(&mut band, &[detect], &[seg], MaskOverlay::default())
7549 .expect("draw into a texture view");
7550 drop(band);
7551
7552 let mut flat = vec![0u8; w * h * 4];
7553 parent
7554 .as_typed::<u8>()
7555 .unwrap()
7556 .copy_to_flat(&mut flat)
7557 .expect("read the parent texture");
7558 let o = ((h / 2 + h / 4) * w + w / 2) * 4;
7561 assert_eq!(
7562 &flat[o..o + 3],
7563 &CUSTOM[..3],
7564 "the CPU backend drew the default palette, so it never received \
7565 set_class_colors"
7566 );
7567 }
7568
7569 #[cfg(all(target_os = "windows", feature = "opengl"))]
7574 #[test]
7575 fn two_processors_alternate_on_texture_destinations_without_state_bleed() {
7576 use edgefirst_tensor::CpuAccess;
7577
7578 if !crate::test_support::is_gpu_buffer_available() {
7579 crate::test_support::report_skip(&format!(
7580 "{} - no zero-copy GPU buffers",
7581 function!()
7582 ));
7583 return;
7584 }
7585 let mut a = ImageProcessor::new().unwrap();
7586 let mut b = ImageProcessor::new().unwrap();
7587 let src_a = synthetic_rgba(512, 512);
7588 let src_b = synthetic_rgba(256, 256);
7589 let mut dst_a = a
7590 .create_image(
7591 512,
7592 512,
7593 PixelFormat::Rgba,
7594 DType::U8,
7595 None,
7596 CpuAccess::Read,
7597 )
7598 .unwrap();
7599 let mut dst_b = b
7600 .create_image(
7601 256,
7602 256,
7603 PixelFormat::Rgba,
7604 DType::U8,
7605 None,
7606 CpuAccess::Read,
7607 )
7608 .unwrap();
7609 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
7610 backend: ComputeBackend::Cpu,
7611 ..Default::default()
7612 })
7613 .unwrap();
7614 let mut ref_a = TensorDyn::image(
7615 512,
7616 512,
7617 PixelFormat::Rgba,
7618 DType::U8,
7619 Some(TensorMemory::Mem),
7620 CpuAccess::ReadWrite,
7621 )
7622 .unwrap();
7623 let mut ref_b = TensorDyn::image(
7624 256,
7625 256,
7626 PixelFormat::Rgba,
7627 DType::U8,
7628 Some(TensorMemory::Mem),
7629 CpuAccess::ReadWrite,
7630 )
7631 .unwrap();
7632 cpu.convert(&src_a, &mut ref_a, None).unwrap();
7633 cpu.convert(&src_b, &mut ref_b, None).unwrap();
7634 for _ in 0..200 {
7635 a.convert(&src_a, &mut dst_a, None).unwrap();
7636 b.convert(&src_b, &mut dst_b, None).unwrap();
7637 compare_images(&dst_a, &ref_a, 0.0, "processor A");
7638 compare_images(&dst_b, &ref_b, 0.0, "processor B");
7639 }
7640 }
7641
7642 #[test]
7643 #[cfg(any(
7644 target_os = "linux",
7645 target_os = "macos",
7646 target_os = "ios",
7647 target_os = "android",
7648 target_os = "windows"
7649 ))]
7650 #[cfg(feature = "opengl")]
7651 fn test_opengl_all_rgba() {
7652 if !is_opengl_available() {
7653 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
7654 return;
7655 }
7656
7657 let dst_width = 640;
7658 let dst_height = 640;
7659 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7660
7661 let mut cpu_converter = CPUProcessor::new();
7662
7663 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
7664
7665 let mut mem = vec![None, Some(TensorMemory::Mem)];
7669 if edgefirst_tensor::is_shm_available() {
7670 mem.push(Some(TensorMemory::Shm));
7671 }
7672 if crate::test_support::is_gpu_buffer_available() {
7673 mem.push(Some(TensorMemory::DmaBuf));
7674 } else {
7675 crate::test_support::report_skip(&format!(
7676 "{} - DmaBuf backing not exercised: no zero-copy GPU buffers",
7677 function!()
7678 ));
7679 }
7680 let crop = Crop::new().with_source(Some(Region::new(50, 120, 1024, 576)));
7681 for m in mem {
7682 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), m).unwrap();
7683 let src_dyn = src;
7684
7685 for rot in [
7686 Rotation::None,
7687 Rotation::Clockwise90,
7688 Rotation::Rotate180,
7689 Rotation::CounterClockwise90,
7690 ] {
7691 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
7692 let cpu_dst = TensorDyn::image(
7693 dst_width,
7694 dst_height,
7695 PixelFormat::Rgba,
7696 DType::U8,
7697 m,
7698 edgefirst_tensor::CpuAccess::ReadWrite,
7699 )
7700 .unwrap();
7701 let gl_dst = TensorDyn::image(
7702 dst_width,
7703 dst_height,
7704 PixelFormat::Rgba,
7705 DType::U8,
7706 m,
7707 edgefirst_tensor::CpuAccess::ReadWrite,
7708 )
7709 .unwrap();
7710 cpu_dst
7711 .as_typed::<u8>()
7712 .unwrap()
7713 .map()
7714 .unwrap()
7715 .as_mut_slice()
7716 .fill(114);
7717 gl_dst
7718 .as_typed::<u8>()
7719 .unwrap()
7720 .map()
7721 .unwrap()
7722 .as_mut_slice()
7723 .fill(114);
7724
7725 let mut cpu_dst_dyn = cpu_dst;
7726 cpu_converter
7727 .convert(
7728 &src_dyn,
7729 &mut cpu_dst_dyn,
7730 Some(crate::ConvertOptions::new().crop(crop)),
7731 )
7732 .unwrap();
7733 let cpu_dst = {
7734 let mut __t = cpu_dst_dyn.into_u8().unwrap();
7735 __t.set_format(PixelFormat::Rgba).unwrap();
7736 TensorDyn::from(__t)
7737 };
7738
7739 let mut gl_dst_dyn = gl_dst;
7740 gl_converter
7741 .convert(
7742 &src_dyn,
7743 &mut gl_dst_dyn,
7744 Some(crate::ConvertOptions::new().crop(crop)),
7745 )
7746 .map_err(|e| {
7747 log::error!("error mem {m:?} rot {rot:?} error: {e:?}");
7748 e
7749 })
7750 .unwrap();
7751 let gl_dst = {
7752 let mut __t = gl_dst_dyn.into_u8().unwrap();
7753 __t.set_format(PixelFormat::Rgba).unwrap();
7754 TensorDyn::from(__t)
7755 };
7756
7757 compare_images(
7758 &gl_dst,
7759 &cpu_dst,
7760 0.98,
7761 &format!("{} {:?} {:?}", function!(), rot, flip),
7762 );
7763 }
7764 }
7765 }
7766 }
7767
7768 #[test]
7769 #[cfg(target_os = "linux")]
7770 fn test_cpu_rotate() {
7771 for rot in [
7772 Rotation::Clockwise90,
7773 Rotation::Rotate180,
7774 Rotation::CounterClockwise90,
7775 ] {
7776 test_cpu_rotate_(rot);
7777 }
7778 }
7779
7780 #[cfg(target_os = "linux")]
7781 fn test_cpu_rotate_(rot: Rotation) {
7782 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7786
7787 let unchanged_src =
7788 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
7789 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
7790
7791 let (dst_width, dst_height) = match rot {
7792 Rotation::None | Rotation::Rotate180 => (src.width().unwrap(), src.height().unwrap()),
7793 Rotation::Clockwise90 | Rotation::CounterClockwise90 => {
7794 (src.height().unwrap(), src.width().unwrap())
7795 }
7796 };
7797
7798 let cpu_dst = TensorDyn::image(
7799 dst_width,
7800 dst_height,
7801 PixelFormat::Rgba,
7802 DType::U8,
7803 None,
7804 edgefirst_tensor::CpuAccess::ReadWrite,
7805 )
7806 .unwrap();
7807 let mut cpu_converter = CPUProcessor::new();
7808
7809 let (result, src, cpu_dst) = convert_img(
7812 &mut cpu_converter,
7813 src,
7814 cpu_dst,
7815 rot,
7816 Flip::None,
7817 Crop::no_crop(),
7818 );
7819 result.unwrap();
7820
7821 let (result, cpu_dst, src) = convert_img(
7822 &mut cpu_converter,
7823 cpu_dst,
7824 src,
7825 rot,
7826 Flip::None,
7827 Crop::no_crop(),
7828 );
7829 result.unwrap();
7830
7831 let (result, src, cpu_dst) = convert_img(
7832 &mut cpu_converter,
7833 src,
7834 cpu_dst,
7835 rot,
7836 Flip::None,
7837 Crop::no_crop(),
7838 );
7839 result.unwrap();
7840
7841 let (result, _cpu_dst, src) = convert_img(
7842 &mut cpu_converter,
7843 cpu_dst,
7844 src,
7845 rot,
7846 Flip::None,
7847 Crop::no_crop(),
7848 );
7849 result.unwrap();
7850
7851 compare_images(&src, &unchanged_src, 0.98, function!());
7852 }
7853
7854 #[test]
7855 #[cfg(any(
7856 target_os = "linux",
7857 target_os = "macos",
7858 target_os = "ios",
7859 target_os = "android",
7860 target_os = "windows"
7861 ))]
7862 #[cfg(feature = "opengl")]
7863 fn test_opengl_rotate() {
7864 if !is_opengl_available() {
7865 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
7866 return;
7867 }
7868
7869 let size = (1280, 720);
7870 let mut mem = vec![None, Some(TensorMemory::Mem)];
7872 if edgefirst_tensor::is_shm_available() {
7873 mem.push(Some(TensorMemory::Shm));
7874 }
7875 if crate::test_support::is_gpu_buffer_available() {
7876 mem.push(Some(TensorMemory::DmaBuf));
7877 } else {
7878 crate::test_support::report_skip(&format!(
7879 "{} - DmaBuf backing not exercised: no zero-copy GPU buffers",
7880 function!()
7881 ));
7882 }
7883 for m in mem {
7884 for rot in [
7885 Rotation::Clockwise90,
7886 Rotation::Rotate180,
7887 Rotation::CounterClockwise90,
7888 ] {
7889 test_opengl_rotate_(size, rot, m);
7890 }
7891 }
7892 }
7893
7894 #[cfg(any(
7895 target_os = "linux",
7896 target_os = "macos",
7897 target_os = "ios",
7898 target_os = "android",
7899 target_os = "windows"
7900 ))]
7901 #[cfg(feature = "opengl")]
7902 fn test_opengl_rotate_(
7903 size: (usize, usize),
7904 rot: Rotation,
7905 tensor_memory: Option<TensorMemory>,
7906 ) {
7907 let (dst_width, dst_height) = match rot {
7908 Rotation::None | Rotation::Rotate180 => size,
7909 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
7910 };
7911
7912 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7913 let src =
7914 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), tensor_memory).unwrap();
7915
7916 let cpu_dst = TensorDyn::image(
7917 dst_width,
7918 dst_height,
7919 PixelFormat::Rgba,
7920 DType::U8,
7921 None,
7922 edgefirst_tensor::CpuAccess::ReadWrite,
7923 )
7924 .unwrap();
7925 let mut cpu_converter = CPUProcessor::new();
7926
7927 let (result, mut src, cpu_dst) = convert_img(
7928 &mut cpu_converter,
7929 src,
7930 cpu_dst,
7931 rot,
7932 Flip::None,
7933 Crop::no_crop(),
7934 );
7935 result.unwrap();
7936
7937 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
7938
7939 for _ in 0..5 {
7940 let gl_dst = TensorDyn::image(
7941 dst_width,
7942 dst_height,
7943 PixelFormat::Rgba,
7944 DType::U8,
7945 tensor_memory,
7946 edgefirst_tensor::CpuAccess::ReadWrite,
7947 )
7948 .unwrap();
7949 let (result, src_back, gl_dst) = convert_img(
7950 &mut gl_converter,
7951 src,
7952 gl_dst,
7953 rot,
7954 Flip::None,
7955 Crop::no_crop(),
7956 );
7957 result.unwrap();
7958 src = src_back;
7959 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
7960 }
7961 }
7962
7963 #[test]
7964 #[cfg(target_os = "linux")]
7965 fn test_g2d_rotate() {
7966 if !is_g2d_available() {
7967 crate::test_support::report_skip(
7968 "test_g2d_rotate - G2D library (libg2d.so.2) not available",
7969 );
7970 return;
7971 }
7972 if !is_dma_available() {
7973 crate::test_support::report_skip("test_g2d_rotate - DMA memory allocation not available (permission denied or no DMA-BUF support)");
7974 return;
7975 }
7976
7977 let size = (1280, 720);
7978 for rot in [
7979 Rotation::Clockwise90,
7980 Rotation::Rotate180,
7981 Rotation::CounterClockwise90,
7982 ] {
7983 test_g2d_rotate_(size, rot);
7984 }
7985 }
7986
7987 #[cfg(target_os = "linux")]
7988 fn test_g2d_rotate_(size: (usize, usize), rot: Rotation) {
7989 let (dst_width, dst_height) = match rot {
7990 Rotation::None | Rotation::Rotate180 => size,
7991 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
7992 };
7993
7994 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7995 let src = crate::load_image_test_helper(
7996 &file,
7997 Some(PixelFormat::Rgba),
7998 Some(TensorMemory::DmaBuf),
7999 )
8000 .unwrap();
8001
8002 let cpu_dst = TensorDyn::image(
8003 dst_width,
8004 dst_height,
8005 PixelFormat::Rgba,
8006 DType::U8,
8007 None,
8008 edgefirst_tensor::CpuAccess::ReadWrite,
8009 )
8010 .unwrap();
8011 let mut cpu_converter = CPUProcessor::new();
8012
8013 let (result, src, cpu_dst) = convert_img(
8014 &mut cpu_converter,
8015 src,
8016 cpu_dst,
8017 rot,
8018 Flip::None,
8019 Crop::no_crop(),
8020 );
8021 result.unwrap();
8022
8023 let g2d_dst = TensorDyn::image(
8024 dst_width,
8025 dst_height,
8026 PixelFormat::Rgba,
8027 DType::U8,
8028 Some(TensorMemory::DmaBuf),
8029 edgefirst_tensor::CpuAccess::ReadWrite,
8030 )
8031 .unwrap();
8032 let mut g2d_converter = G2DProcessor::new().unwrap();
8033
8034 let (result, _src, g2d_dst) = convert_img(
8035 &mut g2d_converter,
8036 src,
8037 g2d_dst,
8038 rot,
8039 Flip::None,
8040 Crop::no_crop(),
8041 );
8042 result.unwrap();
8043
8044 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
8050 }
8051
8052 #[test]
8053 fn test_rgba_to_yuyv_resize_cpu() {
8054 let src = load_bytes_to_tensor(
8055 1280,
8056 720,
8057 PixelFormat::Rgba,
8058 None,
8059 &edgefirst_bench::testdata::read("camera720p.rgba"),
8060 )
8061 .unwrap();
8062
8063 let (dst_width, dst_height) = (640, 360);
8064
8065 let dst = TensorDyn::image(
8066 dst_width,
8067 dst_height,
8068 PixelFormat::Yuyv,
8069 DType::U8,
8070 None,
8071 edgefirst_tensor::CpuAccess::ReadWrite,
8072 )
8073 .unwrap();
8074
8075 let dst_through_yuyv = TensorDyn::image(
8076 dst_width,
8077 dst_height,
8078 PixelFormat::Rgba,
8079 DType::U8,
8080 None,
8081 edgefirst_tensor::CpuAccess::ReadWrite,
8082 )
8083 .unwrap();
8084 let dst_direct = TensorDyn::image(
8085 dst_width,
8086 dst_height,
8087 PixelFormat::Rgba,
8088 DType::U8,
8089 None,
8090 edgefirst_tensor::CpuAccess::ReadWrite,
8091 )
8092 .unwrap();
8093
8094 let mut cpu_converter = CPUProcessor::new();
8095
8096 let (result, src, dst) = convert_img(
8097 &mut cpu_converter,
8098 src,
8099 dst,
8100 Rotation::None,
8101 Flip::None,
8102 Crop::no_crop(),
8103 );
8104 result.unwrap();
8105
8106 let (result, _dst, dst_through_yuyv) = convert_img(
8107 &mut cpu_converter,
8108 dst,
8109 dst_through_yuyv,
8110 Rotation::None,
8111 Flip::None,
8112 Crop::no_crop(),
8113 );
8114 result.unwrap();
8115
8116 let (result, _src, dst_direct) = convert_img(
8117 &mut cpu_converter,
8118 src,
8119 dst_direct,
8120 Rotation::None,
8121 Flip::None,
8122 Crop::no_crop(),
8123 );
8124 result.unwrap();
8125
8126 compare_images(&dst_through_yuyv, &dst_direct, 0.98, function!());
8127 }
8128
8129 #[test]
8130 #[cfg(any(
8131 target_os = "linux",
8132 target_os = "macos",
8133 target_os = "ios",
8134 target_os = "android",
8135 target_os = "windows"
8136 ))]
8137 #[cfg(feature = "opengl")]
8138 #[ignore = "opengl doesn't support rendering to PixelFormat::Yuyv texture"]
8139 fn test_rgba_to_yuyv_resize_opengl() {
8140 if !is_opengl_available() {
8141 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
8142 return;
8143 }
8144
8145 if !crate::test_support::is_gpu_buffer_available() {
8146 crate::test_support::report_skip(&format!(
8147 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
8148 function!()
8149 ));
8150 return;
8151 }
8152
8153 let src = load_bytes_to_tensor(
8154 1280,
8155 720,
8156 PixelFormat::Rgba,
8157 None,
8158 &edgefirst_bench::testdata::read("camera720p.rgba"),
8159 )
8160 .unwrap();
8161
8162 let (dst_width, dst_height) = (640, 360);
8163
8164 let dst = TensorDyn::image(
8165 dst_width,
8166 dst_height,
8167 PixelFormat::Yuyv,
8168 DType::U8,
8169 Some(TensorMemory::DmaBuf),
8170 edgefirst_tensor::CpuAccess::ReadWrite,
8171 )
8172 .unwrap();
8173
8174 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
8175
8176 let (result, src, dst) = convert_img(
8177 &mut gl_converter,
8178 src,
8179 dst,
8180 Rotation::None,
8181 Flip::None,
8182 Crop::letterbox([255, 255, 255, 255]),
8183 );
8184 result.unwrap();
8185
8186 std::fs::write(
8187 "rgba_to_yuyv_opengl.yuyv",
8188 dst.as_typed::<u8>().unwrap().map().unwrap().as_slice(),
8189 )
8190 .unwrap();
8191 let cpu_dst = TensorDyn::image(
8192 dst_width,
8193 dst_height,
8194 PixelFormat::Yuyv,
8195 DType::U8,
8196 Some(TensorMemory::DmaBuf),
8197 edgefirst_tensor::CpuAccess::ReadWrite,
8198 )
8199 .unwrap();
8200 let (result, _src, cpu_dst) = convert_img(
8201 &mut CPUProcessor::new(),
8202 src,
8203 cpu_dst,
8204 Rotation::None,
8205 Flip::None,
8206 Crop::no_crop(),
8207 );
8208 result.unwrap();
8209
8210 compare_images_convert_to_rgb(&dst, &cpu_dst, 0.98, function!());
8211 }
8212
8213 #[test]
8214 #[cfg(target_os = "linux")]
8215 fn test_rgba_to_yuyv_resize_g2d() {
8216 if !is_g2d_available() {
8217 crate::test_support::report_skip(
8218 "test_rgba_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available",
8219 );
8220 return;
8221 }
8222 if !is_dma_available() {
8223 crate::test_support::report_skip("test_rgba_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
8224 return;
8225 }
8226
8227 let src = load_bytes_to_tensor(
8228 1280,
8229 720,
8230 PixelFormat::Rgba,
8231 Some(TensorMemory::DmaBuf),
8232 &edgefirst_bench::testdata::read("camera720p.rgba"),
8233 )
8234 .unwrap();
8235
8236 let (dst_width, dst_height) = (1280, 720);
8237
8238 let cpu_dst = TensorDyn::image(
8239 dst_width,
8240 dst_height,
8241 PixelFormat::Yuyv,
8242 DType::U8,
8243 Some(TensorMemory::DmaBuf),
8244 edgefirst_tensor::CpuAccess::ReadWrite,
8245 )
8246 .unwrap();
8247
8248 let g2d_dst = TensorDyn::image(
8249 dst_width,
8250 dst_height,
8251 PixelFormat::Yuyv,
8252 DType::U8,
8253 Some(TensorMemory::DmaBuf),
8254 edgefirst_tensor::CpuAccess::ReadWrite,
8255 )
8256 .unwrap();
8257
8258 let mut g2d_converter = G2DProcessor::new().unwrap();
8259 let crop = Crop::new();
8260
8261 g2d_dst
8262 .as_typed::<u8>()
8263 .unwrap()
8264 .map()
8265 .unwrap()
8266 .as_mut_slice()
8267 .fill(128);
8268 let (result, src, g2d_dst) = convert_img(
8269 &mut g2d_converter,
8270 src,
8271 g2d_dst,
8272 Rotation::None,
8273 Flip::None,
8274 crop,
8275 );
8276 result.unwrap();
8277
8278 let cpu_dst_img = cpu_dst;
8279 cpu_dst_img
8280 .as_typed::<u8>()
8281 .unwrap()
8282 .map()
8283 .unwrap()
8284 .as_mut_slice()
8285 .fill(128);
8286 let (result, _src, cpu_dst) = convert_img(
8287 &mut CPUProcessor::new(),
8288 src,
8289 cpu_dst_img,
8290 Rotation::None,
8291 Flip::None,
8292 crop,
8293 );
8294 result.unwrap();
8295
8296 compare_images_convert_to_rgb(&cpu_dst, &g2d_dst, 0.98, function!());
8297 }
8298
8299 #[test]
8300 fn test_yuyv_to_rgba_cpu() {
8301 let file = edgefirst_bench::testdata::read("camera720p.yuyv").to_vec();
8302 let src = TensorDyn::image(
8303 1280,
8304 720,
8305 PixelFormat::Yuyv,
8306 DType::U8,
8307 None,
8308 edgefirst_tensor::CpuAccess::ReadWrite,
8309 )
8310 .unwrap();
8311 src.as_typed::<u8>()
8312 .unwrap()
8313 .map()
8314 .unwrap()
8315 .as_mut_slice()
8316 .copy_from_slice(&file);
8317
8318 let dst = TensorDyn::image(
8319 1280,
8320 720,
8321 PixelFormat::Rgba,
8322 DType::U8,
8323 None,
8324 edgefirst_tensor::CpuAccess::ReadWrite,
8325 )
8326 .unwrap();
8327 let mut cpu_converter = CPUProcessor::new();
8328
8329 let (result, _src, dst) = convert_img(
8330 &mut cpu_converter,
8331 src,
8332 dst,
8333 Rotation::None,
8334 Flip::None,
8335 Crop::no_crop(),
8336 );
8337 result.unwrap();
8338
8339 let target_image = TensorDyn::image(
8340 1280,
8341 720,
8342 PixelFormat::Rgba,
8343 DType::U8,
8344 None,
8345 edgefirst_tensor::CpuAccess::ReadWrite,
8346 )
8347 .unwrap();
8348 target_image
8349 .as_typed::<u8>()
8350 .unwrap()
8351 .map()
8352 .unwrap()
8353 .as_mut_slice()
8354 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
8355
8356 compare_images(&dst, &target_image, 0.98, function!());
8359 }
8360
8361 #[test]
8362 fn test_yuyv_to_rgb_cpu() {
8363 let file = edgefirst_bench::testdata::read("camera720p.yuyv").to_vec();
8364 let src = TensorDyn::image(
8365 1280,
8366 720,
8367 PixelFormat::Yuyv,
8368 DType::U8,
8369 None,
8370 edgefirst_tensor::CpuAccess::ReadWrite,
8371 )
8372 .unwrap();
8373 src.as_typed::<u8>()
8374 .unwrap()
8375 .map()
8376 .unwrap()
8377 .as_mut_slice()
8378 .copy_from_slice(&file);
8379
8380 let dst = TensorDyn::image(
8381 1280,
8382 720,
8383 PixelFormat::Rgb,
8384 DType::U8,
8385 None,
8386 edgefirst_tensor::CpuAccess::ReadWrite,
8387 )
8388 .unwrap();
8389 let mut cpu_converter = CPUProcessor::new();
8390
8391 let (result, _src, dst) = convert_img(
8392 &mut cpu_converter,
8393 src,
8394 dst,
8395 Rotation::None,
8396 Flip::None,
8397 Crop::no_crop(),
8398 );
8399 result.unwrap();
8400
8401 let target_image = TensorDyn::image(
8402 1280,
8403 720,
8404 PixelFormat::Rgb,
8405 DType::U8,
8406 None,
8407 edgefirst_tensor::CpuAccess::ReadWrite,
8408 )
8409 .unwrap();
8410 target_image
8411 .as_typed::<u8>()
8412 .unwrap()
8413 .map()
8414 .unwrap()
8415 .as_mut_slice()
8416 .as_chunks_mut::<3>()
8417 .0
8418 .iter_mut()
8419 .zip(
8420 edgefirst_bench::testdata::read("camera720p.rgba")
8421 .as_chunks::<4>()
8422 .0,
8423 )
8424 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
8425
8426 compare_images(&dst, &target_image, 0.98, function!());
8429 }
8430
8431 #[test]
8432 #[cfg(target_os = "linux")]
8433 fn test_yuyv_to_rgba_g2d() {
8434 if !is_g2d_available() {
8435 crate::test_support::report_skip(
8436 "test_yuyv_to_rgba_g2d - G2D library (libg2d.so.2) not available",
8437 );
8438 return;
8439 }
8440 if !is_dma_available() {
8441 crate::test_support::report_skip("test_yuyv_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
8442 return;
8443 }
8444
8445 let src = load_bytes_to_tensor(
8446 1280,
8447 720,
8448 PixelFormat::Yuyv,
8449 None,
8450 &edgefirst_bench::testdata::read("camera720p.yuyv"),
8451 )
8452 .unwrap();
8453
8454 let dst = TensorDyn::image(
8455 1280,
8456 720,
8457 PixelFormat::Rgba,
8458 DType::U8,
8459 Some(TensorMemory::DmaBuf),
8460 edgefirst_tensor::CpuAccess::ReadWrite,
8461 )
8462 .unwrap();
8463 let mut g2d_converter = G2DProcessor::new().unwrap();
8464
8465 let (result, _src, dst) = convert_img(
8466 &mut g2d_converter,
8467 src,
8468 dst,
8469 Rotation::None,
8470 Flip::None,
8471 Crop::no_crop(),
8472 );
8473 result.unwrap();
8474
8475 let target_image = TensorDyn::image(
8476 1280,
8477 720,
8478 PixelFormat::Rgba,
8479 DType::U8,
8480 None,
8481 edgefirst_tensor::CpuAccess::ReadWrite,
8482 )
8483 .unwrap();
8484 target_image
8485 .as_typed::<u8>()
8486 .unwrap()
8487 .map()
8488 .unwrap()
8489 .as_mut_slice()
8490 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
8491
8492 compare_images(&dst, &target_image, 0.98, function!());
8496 }
8497
8498 #[test]
8499 #[cfg(any(
8500 target_os = "linux",
8501 target_os = "macos",
8502 target_os = "ios",
8503 target_os = "android",
8504 target_os = "windows"
8505 ))]
8506 #[cfg(feature = "opengl")]
8507 fn test_yuyv_to_rgba_opengl() {
8508 if !is_opengl_available() {
8509 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
8510 return;
8511 }
8512 if !crate::test_support::is_gpu_buffer_available() {
8515 crate::test_support::report_skip(&format!(
8516 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
8517 function!()
8518 ));
8519 return;
8520 }
8521
8522 let src = load_bytes_to_tensor(
8523 1280,
8524 720,
8525 PixelFormat::Yuyv,
8526 Some(TensorMemory::DmaBuf),
8527 &edgefirst_bench::testdata::read("camera720p.yuyv"),
8528 )
8529 .unwrap();
8530
8531 let dst = TensorDyn::image(
8532 1280,
8533 720,
8534 PixelFormat::Rgba,
8535 DType::U8,
8536 Some(TensorMemory::DmaBuf),
8537 edgefirst_tensor::CpuAccess::ReadWrite,
8538 )
8539 .unwrap();
8540 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
8541
8542 let (result, _src, dst) = convert_img(
8543 &mut gl_converter,
8544 src,
8545 dst,
8546 Rotation::None,
8547 Flip::None,
8548 Crop::no_crop(),
8549 );
8550 if let Err(crate::Error::NotSupported(msg)) = &result {
8553 crate::test_support::report_skip(&format!("{} — {msg}", function!()));
8554 return;
8555 }
8556 result.unwrap();
8557
8558 let target_image = TensorDyn::image(
8559 1280,
8560 720,
8561 PixelFormat::Rgba,
8562 DType::U8,
8563 None,
8564 edgefirst_tensor::CpuAccess::ReadWrite,
8565 )
8566 .unwrap();
8567 target_image
8568 .as_typed::<u8>()
8569 .unwrap()
8570 .map()
8571 .unwrap()
8572 .as_mut_slice()
8573 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
8574
8575 compare_images(&dst, &target_image, 0.98, function!());
8579 }
8580
8581 #[test]
8591 #[cfg(target_os = "macos")]
8592 #[cfg(feature = "opengl")]
8593 fn test_grey_r8_iosurface_to_rgba_opengl_macos() {
8594 let mut proc = match GLProcessorThreaded::new(None) {
8595 Ok(p) => p,
8596 Err(e) => {
8597 crate::test_support::report_skip(&format!(
8598 "{} — GL engine init failed ({e:?})",
8599 function!()
8600 ));
8601 return;
8602 }
8603 };
8604
8605 let (w, h) = (16usize, 16usize);
8606 let src = TensorDyn::image(
8607 w,
8608 h,
8609 PixelFormat::Grey,
8610 DType::U8,
8611 Some(TensorMemory::DmaBuf),
8612 edgefirst_tensor::CpuAccess::ReadWrite,
8613 )
8614 .expect("GREY IOSurface (R8/L008) should allocate — proves the FourCC mapping");
8615 {
8617 let su8 = src.as_typed::<u8>().unwrap();
8618 let stride = src
8619 .as_typed::<u8>()
8620 .unwrap()
8621 .effective_row_stride()
8622 .unwrap();
8623 let mut m = su8.map().unwrap();
8624 let buf = m.as_mut_slice();
8625 for y in 0..h {
8626 for x in 0..w {
8627 buf[y * stride + x] = ((x * 13 + y * 7) & 0xff) as u8;
8628 }
8629 }
8630 }
8631
8632 let dst = TensorDyn::image(
8633 w,
8634 h,
8635 PixelFormat::Rgba,
8636 DType::U8,
8637 Some(TensorMemory::DmaBuf),
8638 edgefirst_tensor::CpuAccess::ReadWrite,
8639 )
8640 .unwrap();
8641 let (result, src_back, dst) = convert_img(
8642 &mut proc,
8643 src,
8644 dst,
8645 Rotation::None,
8646 Flip::None,
8647 Crop::no_crop(),
8648 );
8649 result.expect("GREY(R8 IOSurface) → RGBA must convert on ANGLE (R8 binding works)");
8650
8651 let src_stride = src_back
8652 .as_typed::<u8>()
8653 .unwrap()
8654 .effective_row_stride()
8655 .unwrap();
8656 let src_map = src_back.as_typed::<u8>().unwrap().map().unwrap();
8657 let sbytes = src_map.as_slice();
8658 let dst_stride = dst
8659 .as_typed::<u8>()
8660 .unwrap()
8661 .effective_row_stride()
8662 .unwrap();
8663 let dst_map = dst.as_typed::<u8>().unwrap().map().unwrap();
8664 let dbytes = dst_map.as_slice();
8665 for y in 0..h {
8666 for x in 0..w {
8667 let yv = sbytes[y * src_stride + x] as i16;
8668 let p = y * dst_stride + x * 4;
8669 for c in 0..3 {
8670 assert!(
8671 (dbytes[p + c] as i16 - yv).abs() <= 2,
8672 "pixel ({x},{y}) ch{c} = {} expected ~{yv} (GREY→RGB identity)",
8673 dbytes[p + c]
8674 );
8675 }
8676 }
8677 }
8678 }
8679
8680 #[test]
8686 #[cfg(target_os = "macos")]
8687 #[cfg(feature = "opengl")]
8688 fn test_nv12_to_planar_f16_two_pass_opengl_macos() {
8689 let mut gpu = match GLProcessorThreaded::new(None) {
8690 Ok(p) => p,
8691 Err(e) => {
8692 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8693 return;
8694 }
8695 };
8696 let (w, h) = (64usize, 64usize);
8697 let Some(src) = crate::test_support::gpu_buffer_allocated(
8698 TensorDyn::image(
8699 w,
8700 h,
8701 PixelFormat::Nv12,
8702 DType::U8,
8703 Some(TensorMemory::DmaBuf),
8704 edgefirst_tensor::CpuAccess::ReadWrite,
8705 ),
8706 "NV12 IOSurface",
8707 ) else {
8708 return;
8709 };
8710 src.as_typed::<u8>()
8711 .unwrap()
8712 .map()
8713 .unwrap()
8714 .as_mut_slice()
8715 .fill(128); let Some(dst) = crate::test_support::gpu_buffer_allocated(
8718 TensorDyn::image(
8719 w,
8720 h,
8721 PixelFormat::PlanarRgb,
8722 DType::F16,
8723 Some(TensorMemory::DmaBuf),
8724 edgefirst_tensor::CpuAccess::ReadWrite,
8725 ),
8726 "F16 PlanarRgb IOSurface",
8727 ) else {
8728 return;
8729 };
8730 let mut dst = dst;
8731 if let Err(e) = ImageProcessorTrait::convert(&mut gpu, &src, &mut dst, None) {
8733 crate::test_support::report_skip(&format!(
8737 "{} — NV12→PlanarRgb F16 not available ({e:?})",
8738 function!()
8739 ));
8740 return;
8741 }
8742 let dt = dst.as_typed::<half::f16>().expect("dst is F16 PlanarRgb");
8743 let map = dt.map().unwrap();
8744 let vals = map.as_slice();
8745 let mut checked = 0usize;
8748 for &v in vals.iter() {
8749 let f = f32::from(v);
8750 assert!(
8751 (0.40..=0.60).contains(&f),
8752 "planar F16 value {f} not ~0.5 for neutral-grey NV12"
8753 );
8754 checked += 1;
8755 }
8756 assert!(
8757 checked >= w * h * 3,
8758 "expected >= 3 planes of samples, got {checked}"
8759 );
8760 }
8761
8762 #[test]
8770 #[cfg(target_os = "macos")]
8771 #[cfg(feature = "opengl")]
8772 fn test_nv12_to_planar_f16_two_pass_pool_opengl_macos() {
8773 let mut gpu = match GLProcessorThreaded::new(None) {
8774 Ok(p) => p,
8775 Err(e) => {
8776 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8777 return;
8778 }
8779 };
8780 let (fw, fh) = (96usize, 64usize);
8782 let (pool_w, pool_h) = (256usize, 768usize);
8783 let (model_w, model_h) = (128usize, 128usize);
8784
8785 let Some(mut src) = crate::test_support::gpu_buffer_allocated(
8786 TensorDyn::image(
8787 pool_w,
8788 pool_h,
8789 PixelFormat::Grey,
8790 DType::U8,
8791 Some(TensorMemory::DmaBuf),
8792 edgefirst_tensor::CpuAccess::ReadWrite,
8793 ),
8794 "R8 pool",
8795 ) else {
8796 return;
8797 };
8798 src.configure_image(fw, fh, PixelFormat::Nv12)
8799 .unwrap_or_else(|e| panic!("configure_image NV12 on pool: {e}"));
8800 let stride = src
8801 .as_typed::<u8>()
8802 .unwrap()
8803 .effective_row_stride()
8804 .unwrap();
8805 src.as_typed::<u8>()
8806 .unwrap()
8807 .map()
8808 .unwrap()
8809 .as_mut_slice()
8810 .fill(128); let Some(mut dst) = crate::test_support::gpu_buffer_allocated(
8813 TensorDyn::image(
8814 model_w,
8815 model_h,
8816 PixelFormat::PlanarRgb,
8817 DType::F16,
8818 Some(TensorMemory::DmaBuf),
8819 edgefirst_tensor::CpuAccess::ReadWrite,
8820 ),
8821 "F16 PlanarRgb dst",
8822 ) else {
8823 return;
8824 };
8825
8826 let _ = model_w;
8828 let crop = Crop::new()
8829 .with_source(Some(Region::new(0, 0, fw, fh)))
8830 .with_fit(Fit::Letterbox {
8831 pad: [0, 0, 0, 255],
8832 });
8833 if let Err(e) = ImageProcessorTrait::convert(
8834 &mut gpu,
8835 &src,
8836 &mut dst,
8837 Some(ConvertOptions::new().crop(crop)),
8838 ) {
8839 crate::test_support::report_skip(&format!(
8840 "{} — NV12→PlanarRgb F16 unavailable ({e:?})",
8841 function!()
8842 ));
8843 return;
8844 }
8845 let _ = stride;
8846 let dt = dst.as_typed::<half::f16>().expect("dst F16");
8849 let map = dt.map().unwrap();
8850 let any_half = map.as_slice().iter().any(|&v| {
8851 let f = f32::from(v);
8852 (0.40..=0.60).contains(&f)
8853 });
8854 assert!(any_half, "expected ~0.5 grey samples in the letterbox band");
8855 }
8856
8857 #[test]
8863 #[cfg(target_os = "macos")]
8864 #[cfg(feature = "opengl")]
8865 fn test_nv12_to_planar_f16_cross_thread_opengl_macos() {
8866 use std::sync::mpsc;
8867 let mut proc = match ImageProcessor::new() {
8870 Ok(p) => p,
8871 Err(e) => {
8872 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8873 return;
8874 }
8875 };
8876 let (fw, fh) = (96usize, 64usize);
8877 let Some(mut src) = crate::test_support::gpu_buffer_allocated(
8878 TensorDyn::image(
8879 256,
8880 768,
8881 PixelFormat::Grey,
8882 DType::U8,
8883 Some(TensorMemory::DmaBuf),
8884 edgefirst_tensor::CpuAccess::ReadWrite,
8885 ),
8886 "NV12 R8 pool",
8887 ) else {
8888 return;
8889 };
8890 src.configure_image(fw, fh, PixelFormat::Nv12).unwrap();
8891 src.as_typed::<u8>()
8892 .unwrap()
8893 .map()
8894 .unwrap()
8895 .as_mut_slice()
8896 .fill(128);
8897 let Some(mut dst) = crate::test_support::gpu_buffer_allocated(
8898 TensorDyn::image(
8899 128,
8900 128,
8901 PixelFormat::PlanarRgb,
8902 DType::F16,
8903 Some(TensorMemory::DmaBuf),
8904 edgefirst_tensor::CpuAccess::ReadWrite,
8905 ),
8906 "F16 PlanarRgb dst",
8907 ) else {
8908 return;
8909 };
8910 let crop = Crop::new().with_source(Some(Region::new(0, 0, fw, fh)));
8911
8912 let (tx, rx) = mpsc::channel::<bool>();
8915 let worker = std::thread::spawn(move || {
8916 let _ = ImageProcessorTrait::convert(
8917 &mut proc,
8918 &src,
8919 &mut dst,
8920 Some(ConvertOptions::new().crop(crop)),
8921 );
8922 let _ = tx.send(true);
8923 });
8924 match rx.recv_timeout(std::time::Duration::from_secs(20)) {
8925 Ok(_) => { let _ = worker.join(); }
8926 Err(_) => panic!(
8927 "cross-thread NV12→PlanarRgb convert HUNG (>20s) — reproduces the orchestrator deadlock"
8928 ),
8929 }
8930 }
8931
8932 #[test]
8939 #[cfg(target_os = "macos")]
8940 #[cfg(feature = "opengl")]
8941 fn test_nv_to_planar_f16_varying_sizes_no_leak_opengl_macos() {
8942 let mut gpu = match GLProcessorThreaded::new(None) {
8943 Ok(p) => p,
8944 Err(e) => {
8945 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8946 return;
8947 }
8948 };
8949 let (max_w, max_h) = (640usize, 640usize);
8952 let depth = 4usize;
8953 let mut srcs = Vec::new();
8954 let mut dsts = Vec::new();
8955 for _ in 0..depth {
8956 let Some(src) = crate::test_support::gpu_buffer_allocated(
8957 TensorDyn::image(
8958 max_w,
8959 max_h * 3,
8960 PixelFormat::Grey,
8961 DType::U8,
8962 Some(TensorMemory::DmaBuf),
8963 edgefirst_tensor::CpuAccess::ReadWrite,
8964 ),
8965 "R8 pool",
8966 ) else {
8967 return;
8968 };
8969 srcs.push(src);
8970 let Some(dst) = crate::test_support::gpu_buffer_allocated(
8971 TensorDyn::image(
8972 640,
8973 640,
8974 PixelFormat::PlanarRgb,
8975 DType::F16,
8976 Some(TensorMemory::DmaBuf),
8977 edgefirst_tensor::CpuAccess::ReadWrite,
8978 ),
8979 "F16 PlanarRgb dst",
8980 ) else {
8981 return;
8982 };
8983 dsts.push(dst);
8984 }
8985 let sizes = [
8987 (640, 480),
8988 (500, 375),
8989 (640, 427),
8990 (333, 500),
8991 (480, 640),
8992 (612, 612),
8993 (428, 640),
8994 (576, 432),
8995 ];
8996 let mut first_ms = 0f64;
8997 let mut last_ms = 0f64;
8998 let iters = 40usize;
8999 for i in 0..iters {
9000 let (fw, fh) = sizes[i % sizes.len()];
9001 let src = &mut srcs[i % depth];
9002 let dst = &mut dsts[i % depth];
9003 src.configure_image(fw, fh, PixelFormat::Nv24).unwrap();
9004 src.as_typed::<u8>()
9005 .unwrap()
9006 .map()
9007 .unwrap()
9008 .as_mut_slice()
9009 .fill(128);
9010 let crop = Crop::new().with_source(Some(Region::new(0, 0, fw, fh)));
9011 let t0 = std::time::Instant::now();
9012 ImageProcessorTrait::convert(
9013 &mut gpu,
9014 src,
9015 dst,
9016 Some(ConvertOptions::new().crop(crop)),
9017 )
9018 .unwrap_or_else(|e| panic!("convert iter {i} ({fw}×{fh}): {e}"));
9019 let ms = t0.elapsed().as_secs_f64() * 1e3;
9020 if i == 2 {
9021 first_ms = ms;
9022 }
9023 if i == iters - 1 {
9024 last_ms = ms;
9025 }
9026 }
9027 eprintln!("first={first_ms:.2}ms last={last_ms:.2}ms");
9028 assert!(
9029 last_ms < first_ms * 5.0 + 5.0,
9030 "convert latency ran away: first {first_ms:.2}ms → last {last_ms:.2}ms (intermediate/pbuffer leak)"
9031 );
9032 }
9033
9034 #[test]
9041 #[cfg(target_os = "macos")]
9042 #[cfg(feature = "opengl")]
9043 fn test_nv12_nv16_nv24_to_rgba_opengl_macos() {
9044 let mut gpu = match GLProcessorThreaded::new(None) {
9045 Ok(p) => p,
9046 Err(e) => {
9047 crate::test_support::report_skip(&format!(
9048 "{} — GL engine init failed ({e:?})",
9049 function!()
9050 ));
9051 return;
9052 }
9053 };
9054 let mut cpu = CPUProcessor::new();
9055
9056 let fill = |buf: &mut [u8], stride: usize, fmt: PixelFormat, w: usize, h: usize| {
9068 for y in 0..h {
9069 for x in 0..w {
9070 buf[y * stride + x] = ((x * 9 + y * 5) & 0xff) as u8;
9071 }
9072 }
9073 let (cw, ch, uv_grid_rows) = match fmt {
9074 PixelFormat::Nv12 => (w / 2, h / 2, 1usize),
9075 PixelFormat::Nv16 => (w / 2, h, 1usize),
9076 _ => (w, h, 2usize), };
9078 let uv_plane = h * stride;
9079 for cy in 0..ch {
9080 for cx in 0..cw {
9081 let off = uv_plane + cy * uv_grid_rows * stride + cx * 2;
9082 buf[off] = ((cx * 11 + 30) & 0xff) as u8;
9083 buf[off + 1] = ((cy * 7 + 200) & 0xff) as u8;
9084 }
9085 }
9086 };
9087
9088 for fmt in [PixelFormat::Nv12, PixelFormat::Nv16, PixelFormat::Nv24] {
9089 for (w, h) in [
9090 (16usize, 16usize), (15, 16), (16, 15), ] {
9094 let mem = TensorDyn::image(
9095 w,
9096 h,
9097 fmt,
9098 DType::U8,
9099 None,
9100 edgefirst_tensor::CpuAccess::ReadWrite,
9101 )
9102 .unwrap();
9103 let mem_stride = mem
9104 .as_typed::<u8>()
9105 .unwrap()
9106 .effective_row_stride()
9107 .unwrap();
9108 fill(
9109 mem.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
9110 mem_stride,
9111 fmt,
9112 w,
9113 h,
9114 );
9115 let cpu_dst = TensorDyn::image(
9116 w,
9117 h,
9118 PixelFormat::Rgba,
9119 DType::U8,
9120 None,
9121 edgefirst_tensor::CpuAccess::ReadWrite,
9122 )
9123 .unwrap();
9124 let (r, _s, cpu_dst) = convert_img(
9125 &mut cpu,
9126 mem,
9127 cpu_dst,
9128 Rotation::None,
9129 Flip::None,
9130 Crop::no_crop(),
9131 );
9132 r.unwrap_or_else(|e| panic!("CPU {fmt:?}->{w}x{h}->RGBA: {e}"));
9133
9134 let ios = TensorDyn::image(
9135 w,
9136 h,
9137 fmt,
9138 DType::U8,
9139 Some(TensorMemory::DmaBuf),
9140 edgefirst_tensor::CpuAccess::ReadWrite,
9141 )
9142 .unwrap_or_else(|e| panic!("{fmt:?} {w}x{h} IOSurface alloc: {e}"));
9143 let ios_stride = ios
9144 .as_typed::<u8>()
9145 .unwrap()
9146 .effective_row_stride()
9147 .unwrap();
9148 fill(
9149 ios.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
9150 ios_stride,
9151 fmt,
9152 w,
9153 h,
9154 );
9155 let gpu_dst = TensorDyn::image(
9156 w,
9157 h,
9158 PixelFormat::Rgba,
9159 DType::U8,
9160 Some(TensorMemory::DmaBuf),
9161 edgefirst_tensor::CpuAccess::ReadWrite,
9162 )
9163 .unwrap();
9164 let (r, _s, gpu_dst) = convert_img(
9165 &mut gpu,
9166 ios,
9167 gpu_dst,
9168 Rotation::None,
9169 Flip::None,
9170 Crop::no_crop(),
9171 );
9172 r.unwrap_or_else(|e| panic!("GPU {fmt:?}->{w}x{h}->RGBA on ANGLE: {e}"));
9173
9174 let cs = cpu_dst
9175 .as_typed::<u8>()
9176 .unwrap()
9177 .effective_row_stride()
9178 .unwrap();
9179 let cmap = cpu_dst.as_typed::<u8>().unwrap().map().unwrap();
9180 let cb = cmap.as_slice();
9181 let gs = gpu_dst
9182 .as_typed::<u8>()
9183 .unwrap()
9184 .effective_row_stride()
9185 .unwrap();
9186 let gmap = gpu_dst.as_typed::<u8>().unwrap().map().unwrap();
9187 let gb = gmap.as_slice();
9188 let mut max_d = 0i16;
9189 for y in 0..h {
9190 for x in 0..w {
9191 for c in 0..3 {
9192 let cv = cb[y * cs + x * 4 + c] as i16;
9193 let gv = gb[y * gs + x * 4 + c] as i16;
9194 max_d = max_d.max((cv - gv).abs());
9195 }
9196 }
9197 }
9198 assert!(
9199 max_d <= 3,
9200 "{fmt:?} {w}x{h}: GPU vs CPU RGBA max channel diff {max_d} > 3"
9201 );
9202 }
9203 }
9204 }
9205
9206 #[test]
9214 #[cfg(target_os = "macos")]
9215 #[cfg(feature = "opengl")]
9216 fn test_nv_to_rgba_larger_pool_surface_opengl_macos() {
9217 let mut gpu = match GLProcessorThreaded::new(None) {
9218 Ok(p) => p,
9219 Err(e) => {
9220 crate::test_support::report_skip(&format!(
9221 "{} — GL engine init failed ({e:?})",
9222 function!()
9223 ));
9224 return;
9225 }
9226 };
9227 let mut cpu = CPUProcessor::new();
9228 let (pool_w, pool_h) = (256usize, 256usize);
9231
9232 let fill = |buf: &mut [u8], stride: usize, fmt: PixelFormat, w: usize, h: usize| {
9236 for y in 0..h {
9237 for x in 0..w {
9238 buf[y * stride + x] = ((x * 9 + y * 5) & 0xff) as u8;
9239 }
9240 }
9241 let (cw, ch, uv_grid_rows) = match fmt {
9242 PixelFormat::Nv12 => (w / 2, h / 2, 1usize),
9243 PixelFormat::Nv16 => (w / 2, h, 1usize),
9244 _ => (w, h, 2usize), };
9246 let uv_plane = h * stride;
9247 for cy in 0..ch {
9248 for cx in 0..cw {
9249 let off = uv_plane + cy * uv_grid_rows * stride + cx * 2;
9250 buf[off] = ((cx * 11 + 30) & 0xff) as u8;
9251 buf[off + 1] = ((cy * 7 + 200) & 0xff) as u8;
9252 }
9253 }
9254 };
9255
9256 for fmt in [PixelFormat::Nv12, PixelFormat::Nv16, PixelFormat::Nv24] {
9257 for (w, h) in [
9258 (40usize, 24usize), (15, 16), (16, 15), ] {
9262 let ew = w.next_multiple_of(2);
9265
9266 let mem = TensorDyn::image(
9268 w,
9269 h,
9270 fmt,
9271 DType::U8,
9272 None,
9273 edgefirst_tensor::CpuAccess::ReadWrite,
9274 )
9275 .unwrap();
9276 let mem_stride = mem
9277 .as_typed::<u8>()
9278 .unwrap()
9279 .effective_row_stride()
9280 .unwrap();
9281 fill(
9282 mem.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
9283 mem_stride,
9284 fmt,
9285 w,
9286 h,
9287 );
9288 let cpu_dst = TensorDyn::image(
9289 w,
9290 h,
9291 PixelFormat::Rgba,
9292 DType::U8,
9293 None,
9294 edgefirst_tensor::CpuAccess::ReadWrite,
9295 )
9296 .unwrap();
9297 let (r, _s, cpu_dst) = convert_img(
9298 &mut cpu,
9299 mem,
9300 cpu_dst,
9301 Rotation::None,
9302 Flip::None,
9303 Crop::no_crop(),
9304 );
9305 r.unwrap_or_else(|e| panic!("CPU {fmt:?}->{w}x{h}->RGBA: {e}"));
9306
9307 let Some(mut ios) = crate::test_support::gpu_buffer_allocated(
9311 TensorDyn::image(
9312 pool_w,
9313 pool_h,
9314 PixelFormat::Grey,
9315 DType::U8,
9316 Some(TensorMemory::DmaBuf),
9317 edgefirst_tensor::CpuAccess::ReadWrite,
9318 ),
9319 "R8 pool IOSurface",
9320 ) else {
9321 return;
9322 };
9323 ios.configure_image(w, h, fmt)
9324 .unwrap_or_else(|e| panic!("configure_image {fmt:?} {w}x{h} on pool: {e}"));
9325 let ios_stride = ios
9326 .as_typed::<u8>()
9327 .unwrap()
9328 .effective_row_stride()
9329 .unwrap();
9330 assert!(
9331 ios_stride > ew,
9332 "{fmt:?} {w}x{h}: pool stride {ios_stride} should exceed even width {ew} \
9333 (test must exercise padding)"
9334 );
9335 fill(
9336 ios.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
9337 ios_stride,
9338 fmt,
9339 w,
9340 h,
9341 );
9342
9343 let gpu_dst = TensorDyn::image(
9344 w,
9345 h,
9346 PixelFormat::Rgba,
9347 DType::U8,
9348 Some(TensorMemory::DmaBuf),
9349 edgefirst_tensor::CpuAccess::ReadWrite,
9350 )
9351 .unwrap();
9352 let (r, _s, gpu_dst) = convert_img(
9353 &mut gpu,
9354 ios,
9355 gpu_dst,
9356 Rotation::None,
9357 Flip::None,
9358 Crop::no_crop(),
9359 );
9360 r.unwrap_or_else(|e| {
9361 panic!("GPU {fmt:?}->{w}x{h}->RGBA (pool surface) on ANGLE: {e}")
9362 });
9363
9364 let cs = cpu_dst
9365 .as_typed::<u8>()
9366 .unwrap()
9367 .effective_row_stride()
9368 .unwrap();
9369 let cmap = cpu_dst.as_typed::<u8>().unwrap().map().unwrap();
9370 let cb = cmap.as_slice();
9371 let gs = gpu_dst
9372 .as_typed::<u8>()
9373 .unwrap()
9374 .effective_row_stride()
9375 .unwrap();
9376 let gmap = gpu_dst.as_typed::<u8>().unwrap().map().unwrap();
9377 let gb = gmap.as_slice();
9378 let mut max_d = 0i16;
9379 for y in 0..h {
9380 for x in 0..w {
9381 for c in 0..3 {
9382 let cv = cb[y * cs + x * 4 + c] as i16;
9383 let gv = gb[y * gs + x * 4 + c] as i16;
9384 max_d = max_d.max((cv - gv).abs());
9385 }
9386 }
9387 }
9388 assert!(
9389 max_d <= 3,
9390 "{fmt:?} {w}x{h}: GPU(pool surface) vs CPU RGBA max channel diff {max_d} > 3"
9391 );
9392 }
9393 }
9394 }
9395
9396 #[test]
9397 #[cfg(target_os = "macos")]
9398 #[cfg(feature = "opengl")]
9399 fn test_yuyv_to_rgba_opengl_macos() {
9400 let mut proc = match GLProcessorThreaded::new(None) {
9401 Ok(p) => p,
9402 Err(e) => {
9403 crate::test_support::report_skip(&format!(
9404 "{} — GL engine init failed ({e:?}). \
9405 Install ANGLE via `brew install startergo/angle/angle` \
9406 and re-sign per README.md § macOS GPU Acceleration to \
9407 run this test.",
9408 function!()
9409 ));
9410 return;
9411 }
9412 };
9413
9414 let src = load_bytes_to_tensor(
9415 1280,
9416 720,
9417 PixelFormat::Yuyv,
9418 Some(TensorMemory::DmaBuf),
9419 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9420 )
9421 .unwrap();
9422
9423 let dst = TensorDyn::image(
9424 1280,
9425 720,
9426 PixelFormat::Rgba,
9427 DType::U8,
9428 Some(TensorMemory::DmaBuf),
9429 edgefirst_tensor::CpuAccess::ReadWrite,
9430 )
9431 .unwrap();
9432
9433 let (result, _src, dst) = convert_img(
9434 &mut proc,
9435 src,
9436 dst,
9437 Rotation::None,
9438 Flip::None,
9439 Crop::no_crop(),
9440 );
9441 result.unwrap();
9442
9443 let target_image = TensorDyn::image(
9444 1280,
9445 720,
9446 PixelFormat::Rgba,
9447 DType::U8,
9448 None,
9449 edgefirst_tensor::CpuAccess::ReadWrite,
9450 )
9451 .unwrap();
9452 target_image
9453 .as_typed::<u8>()
9454 .unwrap()
9455 .map()
9456 .unwrap()
9457 .as_mut_slice()
9458 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
9459
9460 compare_images(&dst, &target_image, 0.98, function!());
9465 }
9466
9467 #[test]
9486 #[cfg(target_os = "macos")]
9487 #[cfg(feature = "opengl")]
9488 fn test_yuyv_to_rgba_opengl_macos_multi_resolution() {
9489 let mut proc = match GLProcessorThreaded::new(None) {
9490 Ok(p) => p,
9491 Err(e) => {
9492 crate::test_support::report_skip(&format!(
9493 "{} — GL engine init failed ({e:?})",
9494 function!()
9495 ));
9496 return;
9497 }
9498 };
9499
9500 for (w, h) in [(64usize, 32usize), (3840, 2160)] {
9501 let bytes_per_row = w * 2;
9504 let mut yuyv = vec![0u8; bytes_per_row * h];
9505 for chunk in yuyv.as_chunks_mut::<4>().0 {
9506 chunk[0] = 128; chunk[1] = 128; chunk[2] = 128; chunk[3] = 128; }
9511
9512 let src =
9513 load_bytes_to_tensor(w, h, PixelFormat::Yuyv, Some(TensorMemory::DmaBuf), &yuyv)
9514 .unwrap();
9515
9516 let dst = TensorDyn::image(
9517 w,
9518 h,
9519 PixelFormat::Rgba,
9520 DType::U8,
9521 Some(TensorMemory::DmaBuf),
9522 edgefirst_tensor::CpuAccess::ReadWrite,
9523 )
9524 .unwrap();
9525
9526 let (result, _src, dst) = convert_img(
9527 &mut proc,
9528 src,
9529 dst,
9530 Rotation::None,
9531 Flip::None,
9532 Crop::no_crop(),
9533 );
9534 result.expect("GL convert should succeed at this resolution");
9535
9536 let dst_u8 = dst.as_typed::<u8>().unwrap();
9541 let dst_map = dst_u8.map().unwrap();
9542 let dst_bytes = dst_map.as_slice();
9543 assert_eq!(dst_bytes.len(), w * h * 4, "RGBA byte count");
9544 for px in dst_bytes.as_chunks::<4>().0 {
9545 for (i, &c) in px[..3].iter().enumerate() {
9546 assert!(
9547 (120..=140).contains(&c),
9548 "{}: channel {i} = {c} (expected ~128 ±12) at {w}×{h}",
9549 function!(),
9550 );
9551 }
9552 assert_eq!(px[3], 255, "alpha must be 1.0");
9553 }
9554 }
9555 }
9556
9557 #[test]
9567 #[cfg(target_os = "macos")]
9568 #[cfg(feature = "opengl")]
9569 fn test_macos_gl_pbuffer_cache_reuses_surfaces() {
9570 let mut proc = match GLProcessorThreaded::new(None) {
9571 Ok(p) => p,
9572 Err(e) => {
9573 crate::test_support::report_skip(&format!(
9574 "{} — GL engine init failed ({e:?})",
9575 function!()
9576 ));
9577 return;
9578 }
9579 };
9580
9581 let mut yuyv = vec![0u8; 64 * 32 * 2];
9583 for chunk in yuyv.as_chunks_mut::<4>().0 {
9584 chunk[0] = 200;
9585 chunk[1] = 100;
9586 chunk[2] = 200;
9587 chunk[3] = 156;
9588 }
9589 let src =
9590 load_bytes_to_tensor(64, 32, PixelFormat::Yuyv, Some(TensorMemory::DmaBuf), &yuyv)
9591 .unwrap();
9592 let dst = TensorDyn::image(
9593 64,
9594 32,
9595 PixelFormat::Rgba,
9596 DType::U8,
9597 Some(TensorMemory::DmaBuf),
9598 edgefirst_tensor::CpuAccess::ReadWrite,
9599 )
9600 .unwrap();
9601
9602 let (r1, src, dst) = convert_img(
9603 &mut proc,
9604 src,
9605 dst,
9606 Rotation::None,
9607 Flip::None,
9608 Crop::no_crop(),
9609 );
9610 r1.unwrap();
9611 let first: Vec<u8> = dst
9612 .as_typed::<u8>()
9613 .unwrap()
9614 .map()
9615 .unwrap()
9616 .as_slice()
9617 .to_vec();
9618
9619 let (r2, _src, dst) = convert_img(
9620 &mut proc,
9621 src,
9622 dst,
9623 Rotation::None,
9624 Flip::None,
9625 Crop::no_crop(),
9626 );
9627 r2.unwrap();
9628 let second: Vec<u8> = dst
9629 .as_typed::<u8>()
9630 .unwrap()
9631 .map()
9632 .unwrap()
9633 .as_slice()
9634 .to_vec();
9635
9636 assert_eq!(first, second, "cache-hit conversion must be deterministic");
9637 }
9638
9639 #[test]
9647 #[cfg(target_os = "macos")]
9648 #[cfg(feature = "opengl")]
9649 fn test_macos_gl_pbuffer_cache_steady_state() {
9650 let mut proc = match GLProcessorThreaded::new(None) {
9651 Ok(p) => p,
9652 Err(e) => {
9653 crate::test_support::report_skip(&format!(
9654 "{} — GL engine init failed ({e:?})",
9655 function!()
9656 ));
9657 return;
9658 }
9659 };
9660
9661 let (w, h) = (64usize, 32usize);
9662 const POOL: usize = 3;
9663 const FRAMES: usize = 100;
9664
9665 let yuyv = vec![128u8; w * h * 2];
9666 let pool: Vec<TensorDyn> = (0..POOL)
9667 .map(|_| {
9668 load_bytes_to_tensor(w, h, PixelFormat::Yuyv, Some(TensorMemory::DmaBuf), &yuyv)
9669 .unwrap()
9670 })
9671 .collect();
9672 let mut dst = TensorDyn::image(
9673 w,
9674 h,
9675 PixelFormat::Rgba,
9676 DType::U8,
9677 Some(TensorMemory::DmaBuf),
9678 edgefirst_tensor::CpuAccess::ReadWrite,
9679 )
9680 .unwrap();
9681
9682 for src in pool.iter().cycle().take(POOL * 2) {
9684 proc.convert(src, &mut dst, None).unwrap();
9685 }
9686 let warm = proc.egl_cache_stats().unwrap();
9687
9688 for src in pool.iter().cycle().take(FRAMES) {
9689 proc.convert(src, &mut dst, None).unwrap();
9690 }
9691 let steady = proc.egl_cache_stats().unwrap();
9692
9693 assert_eq!(
9694 warm.total_misses(),
9695 steady.total_misses(),
9696 "steady-state loop created new imports (warm {warm:?}, steady {steady:?})"
9697 );
9698 let hits = |s: &GlCacheStats| s.src.hits + s.dst.hits + s.nv_r8.hits + s.nv_uv.hits;
9699 assert!(
9700 hits(&steady) - hits(&warm) >= FRAMES as u64,
9701 "expected at least {FRAMES} import-cache hits over the loop, got {}",
9702 hits(&steady) - hits(&warm)
9703 );
9704 }
9705
9706 #[test]
9718 #[cfg(target_os = "macos")]
9719 #[cfg(feature = "opengl")]
9720 fn test_macos_gl_f16_planar_is_gl_backed() {
9721 let mut proc = ImageProcessor::new().expect("ImageProcessor");
9722 let Some(ref gl) = proc.opengl else {
9723 crate::test_support::report_skip(&format!("{} — GL backend unavailable", function!()));
9724 return;
9725 };
9726 if !gl.supported_render_dtypes().f16 {
9727 crate::test_support::report_skip(&format!(
9728 "{} — configuration lacks F16 color-buffer support",
9729 function!()
9730 ));
9731 return;
9732 }
9733 let stats_before = gl.egl_cache_stats().expect("cache stats");
9734
9735 let src = TensorDyn::image(
9736 1280,
9737 720,
9738 PixelFormat::Nv12,
9739 DType::U8,
9740 Some(TensorMemory::DmaBuf),
9741 edgefirst_tensor::CpuAccess::ReadWrite,
9742 )
9743 .unwrap();
9744 {
9745 let t = src.as_typed::<u8>().unwrap();
9746 let mut m = t.map().unwrap();
9747 for (i, b) in m.as_mut_slice().iter_mut().enumerate() {
9748 *b = ((i * 31) % 211) as u8;
9749 }
9750 }
9751 let mut dst = TensorDyn::image(
9752 640,
9753 640,
9754 PixelFormat::PlanarRgb,
9755 DType::F16,
9756 Some(TensorMemory::DmaBuf),
9757 edgefirst_tensor::CpuAccess::ReadWrite,
9758 )
9759 .unwrap();
9760
9761 proc.convert(
9762 &src,
9763 &mut dst,
9764 Some(crate::ConvertOptions::new().crop(Crop::letterbox([114, 114, 114, 255]))),
9765 )
9766 .expect("F16 capability reported but the NV12→PlanarF16 convert failed");
9767 let stats_after = proc
9768 .opengl
9769 .as_ref()
9770 .expect("GL backend present")
9771 .egl_cache_stats()
9772 .expect("cache stats");
9773 assert!(
9778 stats_after.total_misses() >= stats_before.total_misses() + 2,
9779 "convert succeeded but the GL engine did not import both the \
9780 source and the F16 destination — the work did not (fully) run \
9781 on the GL backend (silent CPU fallback); misses before={} after={}",
9782 stats_before.total_misses(),
9783 stats_after.total_misses()
9784 );
9785 }
9786
9787 #[test]
9793 #[cfg(feature = "opengl")]
9794 fn test_nv12_to_planar_f16_fused_engine_vs_cpu() {
9795 let mut gl = match ImageProcessor::with_config(ImageProcessorConfig {
9796 backend: ComputeBackend::OpenGl,
9797 ..Default::default()
9798 }) {
9799 Ok(p) if p.opengl.is_some() => p,
9800 _ => {
9801 crate::test_support::report_skip(&format!(
9802 "{} — GL backend unavailable",
9803 function!()
9804 ));
9805 return;
9806 }
9807 };
9808 if !gl
9809 .opengl
9810 .as_ref()
9811 .map(|g| g.supported_render_dtypes().f16)
9812 .unwrap_or(false)
9813 {
9814 crate::test_support::report_skip(&format!("{} — no F16 render support", function!()));
9815 return;
9816 }
9817 let mem = if crate::test_support::is_gpu_buffer_available() {
9818 TensorMemory::DmaBuf
9819 } else {
9820 crate::test_support::report_skip(&format!("{} — no zero-copy buffers", function!()));
9821 return;
9822 };
9823
9824 let src = TensorDyn::image(
9825 1280,
9826 720,
9827 PixelFormat::Nv12,
9828 DType::U8,
9829 Some(mem),
9830 edgefirst_tensor::CpuAccess::ReadWrite,
9831 )
9832 .unwrap();
9833 {
9834 let t = src.as_typed::<u8>().unwrap();
9840 let mut m = t.map().unwrap();
9841 let buf = m.as_mut_slice();
9842 let (w, h) = (1280usize, 720usize);
9843 for y in 0..h {
9844 for x in 0..w {
9845 buf[y * w + x] = ((x * 255) / w) as u8; }
9847 }
9848 for y in 0..(h / 2) {
9849 for x in 0..(w / 2) {
9850 let o = h * w + y * w + 2 * x;
9851 buf[o] = ((y * 255) / (h / 2)) as u8; buf[o + 1] = (((x + y) * 255) / (w / 2 + h / 2)) as u8; }
9854 }
9855 }
9856 let crop = Crop::letterbox([114, 114, 114, 255]);
9857 let mut gl_dst = TensorDyn::image(
9858 640,
9859 640,
9860 PixelFormat::PlanarRgb,
9861 DType::F16,
9862 Some(mem),
9863 edgefirst_tensor::CpuAccess::ReadWrite,
9864 )
9865 .unwrap();
9866 match gl.opengl.as_mut().expect("GL backend present").convert(
9873 &src,
9874 &mut gl_dst,
9875 Some(crate::ConvertOptions::new().crop(crop)),
9876 ) {
9877 Ok(()) => {}
9878 Err(crate::Error::NotSupported(msg)) => {
9879 crate::test_support::report_skip(&format!("{} — {msg}", function!()));
9880 return;
9881 }
9882 Err(e) => panic!("fused NV12→PlanarF16 GL convert: {e}"),
9883 }
9884
9885 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
9886 backend: ComputeBackend::Cpu,
9887 ..Default::default()
9888 })
9889 .unwrap();
9890 let mut cpu_dst = TensorDyn::image(
9891 640,
9892 640,
9893 PixelFormat::PlanarRgb,
9894 DType::F16,
9895 Some(TensorMemory::Mem),
9896 edgefirst_tensor::CpuAccess::ReadWrite,
9897 )
9898 .unwrap();
9899 cpu.convert(
9900 &src,
9901 &mut cpu_dst,
9902 Some(crate::ConvertOptions::new().crop(crop)),
9903 )
9904 .expect("CPU reference convert");
9905
9906 let g = gl_dst
9907 .as_typed::<half::f16>()
9908 .unwrap()
9909 .map()
9910 .unwrap()
9911 .as_slice()
9912 .to_vec();
9913 let c = cpu_dst
9914 .as_typed::<half::f16>()
9915 .unwrap()
9916 .map()
9917 .unwrap()
9918 .as_slice()
9919 .to_vec();
9920 assert_eq!(g.len(), c.len());
9921 let mut max_diff = 0.0f32;
9922 let mut max_at = 0usize;
9923 for (i, (a, b)) in g.iter().zip(c.iter()).enumerate() {
9924 let d = (a.to_f32() - b.to_f32()).abs();
9925 if d > max_diff {
9926 max_diff = d;
9927 max_at = i;
9928 }
9929 }
9930 let (plane, rem) = (max_at / (640 * 640), max_at % (640 * 640));
9932 let (row, col) = (rem / 640, rem % 640);
9933 eprintln!(
9934 "fused-vs-cpu: max_diff={max_diff} at plane={plane} row={row} col={col} \
9935 gl={} cpu={}",
9936 g[max_at].to_f32(),
9937 c[max_at].to_f32()
9938 );
9939 assert!(
9942 max_diff <= 4.0 / 255.0 + 1e-3,
9943 "fused NV12→PlanarF16 diverges from CPU reference: max_diff={max_diff}"
9944 );
9945 }
9946
9947 #[test]
9957 #[cfg(feature = "opengl")]
9958 fn test_zero_copy_src_to_mem_dst_gl_direct() {
9959 let mut proc = match ImageProcessor::new() {
9960 Ok(p) if p.opengl.is_some() => p,
9961 _ => {
9962 crate::test_support::report_skip(&format!(
9963 "{} — GL backend unavailable",
9964 function!()
9965 ));
9966 return;
9967 }
9968 };
9969 if !crate::test_support::is_gpu_buffer_available() {
9970 crate::test_support::report_skip(&format!("{} — no zero-copy buffers", function!()));
9971 return;
9972 }
9973
9974 let src = TensorDyn::image(
9975 1280,
9976 720,
9977 PixelFormat::Rgba,
9978 DType::U8,
9979 Some(TensorMemory::DmaBuf),
9980 edgefirst_tensor::CpuAccess::ReadWrite,
9981 )
9982 .unwrap();
9983 {
9984 let t = src.as_typed::<u8>().unwrap();
9985 let mut m = t.map().unwrap();
9986 for (i, b) in m.as_mut_slice().iter_mut().enumerate() {
9987 *b = ((i * 31) % 211) as u8;
9988 }
9989 }
9990 let mut gl_dst = TensorDyn::image(
9991 1280,
9992 720,
9993 PixelFormat::Bgra,
9994 DType::U8,
9995 Some(TensorMemory::Mem),
9996 edgefirst_tensor::CpuAccess::ReadWrite,
9997 )
9998 .unwrap();
9999 proc.opengl
10000 .as_mut()
10001 .expect("GL backend present")
10002 .convert(&src, &mut gl_dst, None)
10003 .expect("zero-copy src → heap dst GL convert");
10004
10005 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
10006 backend: ComputeBackend::Cpu,
10007 ..Default::default()
10008 })
10009 .unwrap();
10010 let mut cpu_dst = TensorDyn::image(
10011 1280,
10012 720,
10013 PixelFormat::Bgra,
10014 DType::U8,
10015 Some(TensorMemory::Mem),
10016 edgefirst_tensor::CpuAccess::ReadWrite,
10017 )
10018 .unwrap();
10019 cpu.convert(&src, &mut cpu_dst, None)
10020 .expect("CPU reference convert");
10021
10022 let g = gl_dst
10023 .as_typed::<u8>()
10024 .unwrap()
10025 .map()
10026 .unwrap()
10027 .as_slice()
10028 .to_vec();
10029 let c = cpu_dst
10030 .as_typed::<u8>()
10031 .unwrap()
10032 .map()
10033 .unwrap()
10034 .as_slice()
10035 .to_vec();
10036 assert_eq!(g.len(), c.len());
10037 let max_diff = g
10038 .iter()
10039 .zip(c.iter())
10040 .map(|(a, b)| a.abs_diff(*b))
10041 .max()
10042 .unwrap();
10043 assert!(
10046 max_diff <= 2,
10047 "zero-copy src → heap dst diverges from CPU reference: max_diff={max_diff}"
10048 );
10049 }
10050
10051 #[test]
10052 #[cfg(target_os = "linux")]
10053 fn test_yuyv_to_rgb_g2d() {
10054 if !is_g2d_available() {
10055 crate::test_support::report_skip(
10056 "test_yuyv_to_rgb_g2d - G2D library (libg2d.so.2) not available",
10057 );
10058 return;
10059 }
10060 if !is_dma_available() {
10061 crate::test_support::report_skip("test_yuyv_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
10062 return;
10063 }
10064
10065 let src = load_bytes_to_tensor(
10066 1280,
10067 720,
10068 PixelFormat::Yuyv,
10069 None,
10070 &edgefirst_bench::testdata::read("camera720p.yuyv"),
10071 )
10072 .unwrap();
10073
10074 let g2d_dst = TensorDyn::image(
10075 1280,
10076 720,
10077 PixelFormat::Rgb,
10078 DType::U8,
10079 Some(TensorMemory::DmaBuf),
10080 edgefirst_tensor::CpuAccess::ReadWrite,
10081 )
10082 .unwrap();
10083 let mut g2d_converter = G2DProcessor::new().unwrap();
10084
10085 let (result, src, g2d_dst) = convert_img(
10086 &mut g2d_converter,
10087 src,
10088 g2d_dst,
10089 Rotation::None,
10090 Flip::None,
10091 Crop::no_crop(),
10092 );
10093 result.unwrap();
10094
10095 let cpu_dst = TensorDyn::image(
10096 1280,
10097 720,
10098 PixelFormat::Rgb,
10099 DType::U8,
10100 None,
10101 edgefirst_tensor::CpuAccess::ReadWrite,
10102 )
10103 .unwrap();
10104 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
10105
10106 let (result, _src, cpu_dst) = convert_img(
10107 &mut cpu_converter,
10108 src,
10109 cpu_dst,
10110 Rotation::None,
10111 Flip::None,
10112 Crop::no_crop(),
10113 );
10114 result.unwrap();
10115
10116 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
10122 }
10123
10124 #[test]
10125 #[cfg(target_os = "linux")]
10126 fn test_yuyv_to_yuyv_resize_g2d() {
10127 if !is_g2d_available() {
10128 crate::test_support::report_skip(
10129 "test_yuyv_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available",
10130 );
10131 return;
10132 }
10133 if !is_dma_available() {
10134 crate::test_support::report_skip("test_yuyv_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
10135 return;
10136 }
10137
10138 let src = load_bytes_to_tensor(
10139 1280,
10140 720,
10141 PixelFormat::Yuyv,
10142 None,
10143 &edgefirst_bench::testdata::read("camera720p.yuyv"),
10144 )
10145 .unwrap();
10146
10147 let g2d_dst = TensorDyn::image(
10148 600,
10149 400,
10150 PixelFormat::Yuyv,
10151 DType::U8,
10152 Some(TensorMemory::DmaBuf),
10153 edgefirst_tensor::CpuAccess::ReadWrite,
10154 )
10155 .unwrap();
10156 let mut g2d_converter = G2DProcessor::new().unwrap();
10157
10158 let (result, src, g2d_dst) = convert_img(
10159 &mut g2d_converter,
10160 src,
10161 g2d_dst,
10162 Rotation::None,
10163 Flip::None,
10164 Crop::no_crop(),
10165 );
10166 result.unwrap();
10167
10168 let cpu_dst = TensorDyn::image(
10169 600,
10170 400,
10171 PixelFormat::Yuyv,
10172 DType::U8,
10173 None,
10174 edgefirst_tensor::CpuAccess::ReadWrite,
10175 )
10176 .unwrap();
10177 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
10178
10179 let (result, _src, cpu_dst) = convert_img(
10180 &mut cpu_converter,
10181 src,
10182 cpu_dst,
10183 Rotation::None,
10184 Flip::None,
10185 Crop::no_crop(),
10186 );
10187 result.unwrap();
10188
10189 eprintln!(
10196 "WARNING: G2D has poor colorimetry support — YUYV resize diverges from the \
10197 CPU reference (~0.85 similarity); threshold held at 0.85, not 0.95."
10198 );
10199 compare_images_convert_to_rgb(&g2d_dst, &cpu_dst, 0.85, function!());
10200 }
10201
10202 #[test]
10203 fn test_yuyv_to_rgba_resize_cpu() {
10204 let src = load_bytes_to_tensor(
10205 1280,
10206 720,
10207 PixelFormat::Yuyv,
10208 None,
10209 &edgefirst_bench::testdata::read("camera720p.yuyv"),
10210 )
10211 .unwrap();
10212
10213 let (dst_width, dst_height) = (960, 540);
10214
10215 let dst = TensorDyn::image(
10216 dst_width,
10217 dst_height,
10218 PixelFormat::Rgba,
10219 DType::U8,
10220 None,
10221 edgefirst_tensor::CpuAccess::ReadWrite,
10222 )
10223 .unwrap();
10224 let mut cpu_converter = CPUProcessor::new();
10225
10226 let (result, _src, dst) = convert_img(
10227 &mut cpu_converter,
10228 src,
10229 dst,
10230 Rotation::None,
10231 Flip::None,
10232 Crop::no_crop(),
10233 );
10234 result.unwrap();
10235
10236 let dst_target = TensorDyn::image(
10237 dst_width,
10238 dst_height,
10239 PixelFormat::Rgba,
10240 DType::U8,
10241 None,
10242 edgefirst_tensor::CpuAccess::ReadWrite,
10243 )
10244 .unwrap();
10245 let src_target = load_bytes_to_tensor(
10246 1280,
10247 720,
10248 PixelFormat::Rgba,
10249 None,
10250 &edgefirst_bench::testdata::read("camera720p.rgba"),
10251 )
10252 .unwrap();
10253 let (result, _src_target, dst_target) = convert_img(
10254 &mut cpu_converter,
10255 src_target,
10256 dst_target,
10257 Rotation::None,
10258 Flip::None,
10259 Crop::no_crop(),
10260 );
10261 result.unwrap();
10262
10263 compare_images(&dst, &dst_target, 0.98, function!());
10266 }
10267
10268 #[test]
10269 #[cfg(target_os = "linux")]
10270 fn test_yuyv_to_rgba_crop_flip_g2d() {
10271 if !is_g2d_available() {
10272 crate::test_support::report_skip(
10273 "test_yuyv_to_rgba_crop_flip_g2d - G2D library (libg2d.so.2) not available",
10274 );
10275 return;
10276 }
10277 if !is_dma_available() {
10278 crate::test_support::report_skip("test_yuyv_to_rgba_crop_flip_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
10279 return;
10280 }
10281
10282 let src = load_bytes_to_tensor(
10283 1280,
10284 720,
10285 PixelFormat::Yuyv,
10286 Some(TensorMemory::DmaBuf),
10287 &edgefirst_bench::testdata::read("camera720p.yuyv"),
10288 )
10289 .unwrap();
10290
10291 let (dst_width, dst_height) = (640, 640);
10292
10293 let dst_g2d = TensorDyn::image(
10294 dst_width,
10295 dst_height,
10296 PixelFormat::Rgba,
10297 DType::U8,
10298 Some(TensorMemory::DmaBuf),
10299 edgefirst_tensor::CpuAccess::ReadWrite,
10300 )
10301 .unwrap();
10302 let mut g2d_converter = G2DProcessor::new().unwrap();
10303 let crop = Crop::new().with_source(Some(Region::new(20, 15, 400, 300)));
10304
10305 let (result, src, dst_g2d) = convert_img(
10306 &mut g2d_converter,
10307 src,
10308 dst_g2d,
10309 Rotation::None,
10310 Flip::Horizontal,
10311 crop,
10312 );
10313 result.unwrap();
10314
10315 let dst_cpu = TensorDyn::image(
10316 dst_width,
10317 dst_height,
10318 PixelFormat::Rgba,
10319 DType::U8,
10320 Some(TensorMemory::DmaBuf),
10321 edgefirst_tensor::CpuAccess::ReadWrite,
10322 )
10323 .unwrap();
10324 let mut cpu_converter = CPUProcessor::new();
10325
10326 let (result, _src, dst_cpu) = convert_img(
10327 &mut cpu_converter,
10328 src,
10329 dst_cpu,
10330 Rotation::None,
10331 Flip::Horizontal,
10332 crop,
10333 );
10334 result.unwrap();
10335 compare_images(&dst_g2d, &dst_cpu, 0.98, function!());
10341 }
10342
10343 #[test]
10344 #[cfg(any(
10345 target_os = "linux",
10346 target_os = "macos",
10347 target_os = "ios",
10348 target_os = "android",
10349 target_os = "windows"
10350 ))]
10351 #[cfg(feature = "opengl")]
10352 fn test_yuyv_to_rgba_crop_flip_opengl() {
10353 if !is_opengl_available() {
10354 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
10355 return;
10356 }
10357
10358 if !crate::test_support::is_gpu_buffer_available() {
10359 crate::test_support::report_skip(&format!(
10360 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
10361 function!()
10362 ));
10363 return;
10364 }
10365
10366 let src = load_bytes_to_tensor(
10367 1280,
10368 720,
10369 PixelFormat::Yuyv,
10370 Some(TensorMemory::DmaBuf),
10371 &edgefirst_bench::testdata::read("camera720p.yuyv"),
10372 )
10373 .unwrap();
10374
10375 let (dst_width, dst_height) = (640, 640);
10376
10377 let dst_gl = TensorDyn::image(
10378 dst_width,
10379 dst_height,
10380 PixelFormat::Rgba,
10381 DType::U8,
10382 Some(TensorMemory::DmaBuf),
10383 edgefirst_tensor::CpuAccess::ReadWrite,
10384 )
10385 .unwrap();
10386 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
10387 let crop = Crop::new().with_source(Some(Region::new(20, 15, 400, 300)));
10388
10389 let (result, src, dst_gl) = convert_img(
10390 &mut gl_converter,
10391 src,
10392 dst_gl,
10393 Rotation::None,
10394 Flip::Horizontal,
10395 crop,
10396 );
10397 if let Err(crate::Error::NotSupported(msg)) = &result {
10399 crate::test_support::report_skip(&format!("{} — {msg}", function!()));
10400 return;
10401 }
10402 result.unwrap();
10403
10404 let dst_cpu = TensorDyn::image(
10405 dst_width,
10406 dst_height,
10407 PixelFormat::Rgba,
10408 DType::U8,
10409 Some(TensorMemory::DmaBuf),
10410 edgefirst_tensor::CpuAccess::ReadWrite,
10411 )
10412 .unwrap();
10413 let mut cpu_converter = CPUProcessor::new();
10414
10415 let (result, _src, dst_cpu) = convert_img(
10416 &mut cpu_converter,
10417 src,
10418 dst_cpu,
10419 Rotation::None,
10420 Flip::Horizontal,
10421 crop,
10422 );
10423 result.unwrap();
10424 compare_images(&dst_gl, &dst_cpu, 0.98, function!());
10429 }
10430
10431 #[test]
10432 fn test_vyuy_to_rgba_cpu() {
10433 let file = edgefirst_bench::testdata::read("camera720p.vyuy").to_vec();
10434 let src = TensorDyn::image(
10435 1280,
10436 720,
10437 PixelFormat::Vyuy,
10438 DType::U8,
10439 None,
10440 edgefirst_tensor::CpuAccess::ReadWrite,
10441 )
10442 .unwrap();
10443 src.as_typed::<u8>()
10444 .unwrap()
10445 .map()
10446 .unwrap()
10447 .as_mut_slice()
10448 .copy_from_slice(&file);
10449
10450 let dst = TensorDyn::image(
10451 1280,
10452 720,
10453 PixelFormat::Rgba,
10454 DType::U8,
10455 None,
10456 edgefirst_tensor::CpuAccess::ReadWrite,
10457 )
10458 .unwrap();
10459 let mut cpu_converter = CPUProcessor::new();
10460
10461 let (result, _src, dst) = convert_img(
10462 &mut cpu_converter,
10463 src,
10464 dst,
10465 Rotation::None,
10466 Flip::None,
10467 Crop::no_crop(),
10468 );
10469 result.unwrap();
10470
10471 let target_image = TensorDyn::image(
10472 1280,
10473 720,
10474 PixelFormat::Rgba,
10475 DType::U8,
10476 None,
10477 edgefirst_tensor::CpuAccess::ReadWrite,
10478 )
10479 .unwrap();
10480 target_image
10481 .as_typed::<u8>()
10482 .unwrap()
10483 .map()
10484 .unwrap()
10485 .as_mut_slice()
10486 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
10487
10488 compare_images(&dst, &target_image, 0.98, function!());
10491 }
10492
10493 #[test]
10494 fn test_vyuy_to_rgb_cpu() {
10495 let file = edgefirst_bench::testdata::read("camera720p.vyuy").to_vec();
10496 let src = TensorDyn::image(
10497 1280,
10498 720,
10499 PixelFormat::Vyuy,
10500 DType::U8,
10501 None,
10502 edgefirst_tensor::CpuAccess::ReadWrite,
10503 )
10504 .unwrap();
10505 src.as_typed::<u8>()
10506 .unwrap()
10507 .map()
10508 .unwrap()
10509 .as_mut_slice()
10510 .copy_from_slice(&file);
10511
10512 let dst = TensorDyn::image(
10513 1280,
10514 720,
10515 PixelFormat::Rgb,
10516 DType::U8,
10517 None,
10518 edgefirst_tensor::CpuAccess::ReadWrite,
10519 )
10520 .unwrap();
10521 let mut cpu_converter = CPUProcessor::new();
10522
10523 let (result, _src, dst) = convert_img(
10524 &mut cpu_converter,
10525 src,
10526 dst,
10527 Rotation::None,
10528 Flip::None,
10529 Crop::no_crop(),
10530 );
10531 result.unwrap();
10532
10533 let target_image = TensorDyn::image(
10534 1280,
10535 720,
10536 PixelFormat::Rgb,
10537 DType::U8,
10538 None,
10539 edgefirst_tensor::CpuAccess::ReadWrite,
10540 )
10541 .unwrap();
10542 target_image
10543 .as_typed::<u8>()
10544 .unwrap()
10545 .map()
10546 .unwrap()
10547 .as_mut_slice()
10548 .as_chunks_mut::<3>()
10549 .0
10550 .iter_mut()
10551 .zip(
10552 edgefirst_bench::testdata::read("camera720p.rgba")
10553 .as_chunks::<4>()
10554 .0,
10555 )
10556 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
10557
10558 compare_images(&dst, &target_image, 0.98, function!());
10561 }
10562
10563 #[test]
10564 #[cfg(target_os = "linux")]
10565 #[ignore = "G2D does not support VYUY; re-enable when hardware support is added"]
10566 fn test_vyuy_to_rgba_g2d() {
10567 if !is_g2d_available() {
10568 crate::test_support::report_skip(
10569 "test_vyuy_to_rgba_g2d - G2D library (libg2d.so.2) not available",
10570 );
10571 return;
10572 }
10573 if !is_dma_available() {
10574 crate::test_support::report_skip("test_vyuy_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
10575 return;
10576 }
10577
10578 let src = load_bytes_to_tensor(
10579 1280,
10580 720,
10581 PixelFormat::Vyuy,
10582 None,
10583 &edgefirst_bench::testdata::read("camera720p.vyuy"),
10584 )
10585 .unwrap();
10586
10587 let dst = TensorDyn::image(
10588 1280,
10589 720,
10590 PixelFormat::Rgba,
10591 DType::U8,
10592 Some(TensorMemory::DmaBuf),
10593 edgefirst_tensor::CpuAccess::ReadWrite,
10594 )
10595 .unwrap();
10596 let mut g2d_converter = G2DProcessor::new().unwrap();
10597
10598 let (result, _src, dst) = convert_img(
10599 &mut g2d_converter,
10600 src,
10601 dst,
10602 Rotation::None,
10603 Flip::None,
10604 Crop::no_crop(),
10605 );
10606 match result {
10607 Err(Error::G2D(_)) => {
10608 crate::test_support::report_skip(
10609 "test_vyuy_to_rgba_g2d - G2D does not support PixelFormat::Vyuy format",
10610 );
10611 return;
10612 }
10613 r => r.unwrap(),
10614 }
10615
10616 let target_image = TensorDyn::image(
10617 1280,
10618 720,
10619 PixelFormat::Rgba,
10620 DType::U8,
10621 None,
10622 edgefirst_tensor::CpuAccess::ReadWrite,
10623 )
10624 .unwrap();
10625 target_image
10626 .as_typed::<u8>()
10627 .unwrap()
10628 .map()
10629 .unwrap()
10630 .as_mut_slice()
10631 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
10632
10633 compare_images(&dst, &target_image, 0.98, function!());
10637 }
10638
10639 #[test]
10640 #[cfg(target_os = "linux")]
10641 #[ignore = "G2D does not support VYUY; re-enable when hardware support is added"]
10642 fn test_vyuy_to_rgb_g2d() {
10643 if !is_g2d_available() {
10644 crate::test_support::report_skip(
10645 "test_vyuy_to_rgb_g2d - G2D library (libg2d.so.2) not available",
10646 );
10647 return;
10648 }
10649 if !is_dma_available() {
10650 crate::test_support::report_skip("test_vyuy_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
10651 return;
10652 }
10653
10654 let src = load_bytes_to_tensor(
10655 1280,
10656 720,
10657 PixelFormat::Vyuy,
10658 None,
10659 &edgefirst_bench::testdata::read("camera720p.vyuy"),
10660 )
10661 .unwrap();
10662
10663 let g2d_dst = TensorDyn::image(
10664 1280,
10665 720,
10666 PixelFormat::Rgb,
10667 DType::U8,
10668 Some(TensorMemory::DmaBuf),
10669 edgefirst_tensor::CpuAccess::ReadWrite,
10670 )
10671 .unwrap();
10672 let mut g2d_converter = G2DProcessor::new().unwrap();
10673
10674 let (result, src, g2d_dst) = convert_img(
10675 &mut g2d_converter,
10676 src,
10677 g2d_dst,
10678 Rotation::None,
10679 Flip::None,
10680 Crop::no_crop(),
10681 );
10682 match result {
10683 Err(Error::G2D(_)) => {
10684 crate::test_support::report_skip(
10685 "test_vyuy_to_rgb_g2d - G2D does not support PixelFormat::Vyuy format",
10686 );
10687 return;
10688 }
10689 r => r.unwrap(),
10690 }
10691
10692 let cpu_dst = TensorDyn::image(
10693 1280,
10694 720,
10695 PixelFormat::Rgb,
10696 DType::U8,
10697 None,
10698 edgefirst_tensor::CpuAccess::ReadWrite,
10699 )
10700 .unwrap();
10701 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
10702
10703 let (result, _src, cpu_dst) = convert_img(
10704 &mut cpu_converter,
10705 src,
10706 cpu_dst,
10707 Rotation::None,
10708 Flip::None,
10709 Crop::no_crop(),
10710 );
10711 result.unwrap();
10712
10713 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
10719 }
10720
10721 #[test]
10722 #[cfg(any(
10723 target_os = "linux",
10724 target_os = "macos",
10725 target_os = "ios",
10726 target_os = "android",
10727 target_os = "windows"
10728 ))]
10729 #[cfg(feature = "opengl")]
10730 fn test_vyuy_to_rgba_opengl() {
10731 if !is_opengl_available() {
10732 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
10733 return;
10734 }
10735 if !crate::test_support::is_gpu_buffer_available() {
10736 crate::test_support::report_skip(&format!(
10737 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
10738 function!()
10739 ));
10740 return;
10741 }
10742
10743 let expect = if cfg!(target_os = "linux") {
10745 crate::test_support::Expect::Succeeds
10746 } else {
10747 crate::test_support::Expect::MayFail("no zero-copy VYUY mapping on this platform")
10748 };
10749 let Some(src) = crate::test_support::allocated(
10750 load_bytes_to_tensor(
10751 1280,
10752 720,
10753 PixelFormat::Vyuy,
10754 Some(TensorMemory::DmaBuf),
10755 &edgefirst_bench::testdata::read("camera720p.vyuy"),
10756 ),
10757 "zero-copy VYUY source",
10758 expect,
10759 ) else {
10760 return;
10761 };
10762
10763 let dst = TensorDyn::image(
10764 1280,
10765 720,
10766 PixelFormat::Rgba,
10767 DType::U8,
10768 Some(TensorMemory::DmaBuf),
10769 edgefirst_tensor::CpuAccess::ReadWrite,
10770 )
10771 .unwrap();
10772 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
10773
10774 let (result, _src, dst) = convert_img(
10775 &mut gl_converter,
10776 src,
10777 dst,
10778 Rotation::None,
10779 Flip::None,
10780 Crop::no_crop(),
10781 );
10782 match result {
10783 Err(Error::NotSupported(_)) => {
10784 crate::test_support::report_skip(&format!(
10785 "{} - OpenGL does not support PixelFormat::Vyuy DMA format",
10786 function!()
10787 ));
10788 return;
10789 }
10790 r => r.unwrap(),
10791 }
10792
10793 let target_image = TensorDyn::image(
10794 1280,
10795 720,
10796 PixelFormat::Rgba,
10797 DType::U8,
10798 None,
10799 edgefirst_tensor::CpuAccess::ReadWrite,
10800 )
10801 .unwrap();
10802 target_image
10803 .as_typed::<u8>()
10804 .unwrap()
10805 .map()
10806 .unwrap()
10807 .as_mut_slice()
10808 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
10809
10810 compare_images(&dst, &target_image, 0.98, function!());
10814 }
10815
10816 #[test]
10817 fn test_nv12_to_rgba_cpu() {
10818 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
10819 let src = TensorDyn::image(
10820 1280,
10821 720,
10822 PixelFormat::Nv12,
10823 DType::U8,
10824 None,
10825 edgefirst_tensor::CpuAccess::ReadWrite,
10826 )
10827 .unwrap();
10828 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
10829 .copy_from_slice(&file);
10830
10831 let dst = TensorDyn::image(
10832 1280,
10833 720,
10834 PixelFormat::Rgba,
10835 DType::U8,
10836 None,
10837 edgefirst_tensor::CpuAccess::ReadWrite,
10838 )
10839 .unwrap();
10840 let mut cpu_converter = CPUProcessor::new();
10841
10842 let (result, _src, dst) = convert_img(
10843 &mut cpu_converter,
10844 src,
10845 dst,
10846 Rotation::None,
10847 Flip::None,
10848 Crop::no_crop(),
10849 );
10850 result.unwrap();
10851
10852 let target_image = crate::load_image_test_helper(
10853 &edgefirst_bench::testdata::read("zidane.jpg"),
10854 Some(PixelFormat::Rgba),
10855 None,
10856 )
10857 .unwrap();
10858
10859 compare_images(&dst, &target_image, 0.95, function!());
10864 }
10865
10866 #[test]
10867 fn test_nv12_odd_height_to_rgb_cpu() {
10868 let mut src = TensorDyn::image(
10879 8,
10880 5,
10881 PixelFormat::Nv12,
10882 DType::U8,
10883 Some(TensorMemory::Mem),
10884 edgefirst_tensor::CpuAccess::ReadWrite,
10885 )
10886 .unwrap();
10887 assert_eq!(src.shape(), &[8, 8]);
10888 assert_eq!((src.width(), src.height()), (Some(8), Some(5)));
10889 src.as_typed::<u8>()
10890 .unwrap()
10891 .map()
10892 .unwrap()
10893 .as_mut_slice()
10894 .fill(128);
10895 src.set_colorimetry(Some(
10899 edgefirst_tensor::Colorimetry::default()
10900 .with_encoding(edgefirst_tensor::ColorEncoding::Bt601)
10901 .with_range(edgefirst_tensor::ColorRange::Full),
10902 ));
10903
10904 let dst = TensorDyn::image(
10905 8,
10906 5,
10907 PixelFormat::Rgb,
10908 DType::U8,
10909 Some(TensorMemory::Mem),
10910 edgefirst_tensor::CpuAccess::ReadWrite,
10911 )
10912 .unwrap();
10913 let mut cpu_converter = CPUProcessor::new();
10914 let (result, _src, dst) = convert_img(
10915 &mut cpu_converter,
10916 src,
10917 dst,
10918 Rotation::None,
10919 Flip::None,
10920 Crop::no_crop(),
10921 );
10922 result.unwrap();
10923
10924 assert_eq!((dst.width(), dst.height()), (Some(8), Some(5)));
10925 let map = dst.as_typed::<u8>().unwrap().map().unwrap();
10926 for (i, &b) in map.as_slice().iter().enumerate() {
10927 assert!(
10928 (b as i16 - 128).abs() <= 2,
10929 "pixel byte {i} = {b}, expected ~128 for neutral-grey NV12"
10930 );
10931 }
10932 }
10933
10934 #[test]
10935 fn test_nv24_to_rgb_cpu() {
10936 let mut src = TensorDyn::image(
10942 8,
10943 4,
10944 PixelFormat::Nv24,
10945 DType::U8,
10946 Some(TensorMemory::Mem),
10947 edgefirst_tensor::CpuAccess::ReadWrite,
10948 )
10949 .unwrap();
10950 assert_eq!(src.shape(), &[12, 8]);
10951 assert_eq!((src.width(), src.height()), (Some(8), Some(4)));
10952 src.as_typed::<u8>()
10953 .unwrap()
10954 .map()
10955 .unwrap()
10956 .as_mut_slice()
10957 .fill(128);
10958 src.set_colorimetry(Some(
10961 edgefirst_tensor::Colorimetry::default()
10962 .with_encoding(edgefirst_tensor::ColorEncoding::Bt601)
10963 .with_range(edgefirst_tensor::ColorRange::Full),
10964 ));
10965
10966 let dst = TensorDyn::image(
10967 8,
10968 4,
10969 PixelFormat::Rgb,
10970 DType::U8,
10971 Some(TensorMemory::Mem),
10972 edgefirst_tensor::CpuAccess::ReadWrite,
10973 )
10974 .unwrap();
10975 let mut cpu_converter = CPUProcessor::new();
10976 let (result, _src, dst) = convert_img(
10977 &mut cpu_converter,
10978 src,
10979 dst,
10980 Rotation::None,
10981 Flip::None,
10982 Crop::no_crop(),
10983 );
10984 result.unwrap();
10985
10986 assert_eq!((dst.width(), dst.height()), (Some(8), Some(4)));
10987 let map = dst.as_typed::<u8>().unwrap().map().unwrap();
10988 for (i, &b) in map.as_slice().iter().enumerate() {
10989 assert!(
10990 (b as i16 - 128).abs() <= 2,
10991 "pixel byte {i} = {b}, expected ~128 for neutral-grey NV24"
10992 );
10993 }
10994 }
10995
10996 #[test]
10997 fn cpu_nv12_to_rgb_respects_tagged_bt2020() {
10998 fn decode_tagged(enc: edgefirst_tensor::ColorEncoding) -> [u8; 3] {
11006 let mut src = TensorDyn::image(
11007 8,
11008 4,
11009 PixelFormat::Nv12,
11010 DType::U8,
11011 Some(TensorMemory::Mem),
11012 edgefirst_tensor::CpuAccess::ReadWrite,
11013 )
11014 .unwrap();
11015 assert_eq!(src.shape(), &[6, 8]);
11017 {
11018 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
11019 let buf = map.as_mut_slice();
11020 buf[..32].fill(120); for px in buf[32..].as_chunks_mut::<2>().0 {
11022 px[0] = 180; px[1] = 64; }
11025 }
11026 src.set_colorimetry(Some(
11029 edgefirst_tensor::Colorimetry::default()
11030 .with_encoding(enc)
11031 .with_range(edgefirst_tensor::ColorRange::Limited),
11032 ));
11033 let dst = TensorDyn::image(
11034 8,
11035 4,
11036 PixelFormat::Rgb,
11037 DType::U8,
11038 Some(TensorMemory::Mem),
11039 edgefirst_tensor::CpuAccess::ReadWrite,
11040 )
11041 .unwrap();
11042 let mut cpu = CPUProcessor::new();
11043 let (result, _src, dst) = convert_img(
11044 &mut cpu,
11045 src,
11046 dst,
11047 Rotation::None,
11048 Flip::None,
11049 Crop::no_crop(),
11050 );
11051 result.unwrap();
11052 let map = dst.as_typed::<u8>().unwrap().map().unwrap();
11053 let s = map.as_slice();
11054 [s[0], s[1], s[2]]
11055 }
11056
11057 let bt601 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt601);
11058 let bt709 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt709);
11059 let bt2020 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt2020);
11060
11061 assert_ne!(
11062 bt2020, bt601,
11063 "BT.2020 must decode differently from BT.601 ({bt2020:?} vs {bt601:?})"
11064 );
11065 assert_ne!(
11066 bt2020, bt709,
11067 "BT.2020 must decode differently from BT.709 ({bt2020:?} vs {bt709:?})"
11068 );
11069 assert_ne!(
11070 bt601, bt709,
11071 "BT.601 must decode differently from BT.709 ({bt601:?} vs {bt709:?})"
11072 );
11073 }
11074
11075 #[test]
11076 fn test_nv12_to_rgb_cpu() {
11077 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
11078 let src = TensorDyn::image(
11079 1280,
11080 720,
11081 PixelFormat::Nv12,
11082 DType::U8,
11083 None,
11084 edgefirst_tensor::CpuAccess::ReadWrite,
11085 )
11086 .unwrap();
11087 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
11088 .copy_from_slice(&file);
11089
11090 let dst = TensorDyn::image(
11091 1280,
11092 720,
11093 PixelFormat::Rgb,
11094 DType::U8,
11095 None,
11096 edgefirst_tensor::CpuAccess::ReadWrite,
11097 )
11098 .unwrap();
11099 let mut cpu_converter = CPUProcessor::new();
11100
11101 let (result, _src, dst) = convert_img(
11102 &mut cpu_converter,
11103 src,
11104 dst,
11105 Rotation::None,
11106 Flip::None,
11107 Crop::no_crop(),
11108 );
11109 result.unwrap();
11110
11111 let target_image = crate::load_image_test_helper(
11112 &edgefirst_bench::testdata::read("zidane.jpg"),
11113 Some(PixelFormat::Rgb),
11114 None,
11115 )
11116 .unwrap();
11117
11118 compare_images(&dst, &target_image, 0.95, function!());
11123 }
11124
11125 #[test]
11126 fn test_nv12_to_grey_cpu() {
11127 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
11128 let src = TensorDyn::image(
11129 1280,
11130 720,
11131 PixelFormat::Nv12,
11132 DType::U8,
11133 None,
11134 edgefirst_tensor::CpuAccess::ReadWrite,
11135 )
11136 .unwrap();
11137 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
11138 .copy_from_slice(&file);
11139
11140 let dst = TensorDyn::image(
11141 1280,
11142 720,
11143 PixelFormat::Grey,
11144 DType::U8,
11145 None,
11146 edgefirst_tensor::CpuAccess::ReadWrite,
11147 )
11148 .unwrap();
11149 let mut cpu_converter = CPUProcessor::new();
11150
11151 let (result, _src, dst) = convert_img(
11152 &mut cpu_converter,
11153 src,
11154 dst,
11155 Rotation::None,
11156 Flip::None,
11157 Crop::no_crop(),
11158 );
11159 result.unwrap();
11160
11161 let target_image = crate::load_image_test_helper(
11162 &edgefirst_bench::testdata::read("zidane.jpg"),
11163 Some(PixelFormat::Grey),
11164 None,
11165 )
11166 .unwrap();
11167
11168 compare_images(&dst, &target_image, 0.95, function!());
11173 }
11174
11175 #[test]
11176 fn test_nv12_to_yuyv_cpu() {
11177 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
11178 let src = TensorDyn::image(
11179 1280,
11180 720,
11181 PixelFormat::Nv12,
11182 DType::U8,
11183 None,
11184 edgefirst_tensor::CpuAccess::ReadWrite,
11185 )
11186 .unwrap();
11187 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
11188 .copy_from_slice(&file);
11189
11190 let dst = TensorDyn::image(
11191 1280,
11192 720,
11193 PixelFormat::Yuyv,
11194 DType::U8,
11195 None,
11196 edgefirst_tensor::CpuAccess::ReadWrite,
11197 )
11198 .unwrap();
11199 let mut cpu_converter = CPUProcessor::new();
11200
11201 let (result, _src, dst) = convert_img(
11202 &mut cpu_converter,
11203 src,
11204 dst,
11205 Rotation::None,
11206 Flip::None,
11207 Crop::no_crop(),
11208 );
11209 result.unwrap();
11210
11211 let target_image = crate::load_image_test_helper(
11212 &edgefirst_bench::testdata::read("zidane.jpg"),
11213 Some(PixelFormat::Rgb),
11214 None,
11215 )
11216 .unwrap();
11217
11218 compare_images_convert_to_rgb(&dst, &target_image, 0.95, function!());
11223 }
11224
11225 #[test]
11226 fn test_cpu_resize_nv16() {
11227 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
11228 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
11229
11230 let cpu_nv16_dst = TensorDyn::image(
11231 640,
11232 640,
11233 PixelFormat::Nv16,
11234 DType::U8,
11235 None,
11236 edgefirst_tensor::CpuAccess::ReadWrite,
11237 )
11238 .unwrap();
11239 let cpu_rgb_dst = TensorDyn::image(
11240 640,
11241 640,
11242 PixelFormat::Rgb,
11243 DType::U8,
11244 None,
11245 edgefirst_tensor::CpuAccess::ReadWrite,
11246 )
11247 .unwrap();
11248 let mut cpu_converter = CPUProcessor::new();
11249 let crop = Crop::letterbox([255, 128, 0, 255]);
11250
11251 let (result, src, cpu_nv16_dst) = convert_img(
11252 &mut cpu_converter,
11253 src,
11254 cpu_nv16_dst,
11255 Rotation::None,
11256 Flip::None,
11257 crop,
11258 );
11259 result.unwrap();
11260
11261 let (result, _src, cpu_rgb_dst) = convert_img(
11262 &mut cpu_converter,
11263 src,
11264 cpu_rgb_dst,
11265 Rotation::None,
11266 Flip::None,
11267 crop,
11268 );
11269 result.unwrap();
11270 compare_images_convert_to_rgb(&cpu_nv16_dst, &cpu_rgb_dst, 0.99, function!());
11271 }
11272
11273 fn load_bytes_to_tensor(
11274 width: usize,
11275 height: usize,
11276 format: PixelFormat,
11277 memory: Option<TensorMemory>,
11278 bytes: &[u8],
11279 ) -> Result<TensorDyn, Error> {
11280 let src = TensorDyn::image(
11281 width,
11282 height,
11283 format,
11284 DType::U8,
11285 memory,
11286 edgefirst_tensor::CpuAccess::ReadWrite,
11287 )?;
11288 src.as_typed::<u8>()
11289 .unwrap()
11290 .map()?
11291 .as_mut_slice()
11292 .copy_from_slice(bytes);
11293 Ok(src)
11294 }
11295
11296 fn compare_images(img1: &TensorDyn, img2: &TensorDyn, threshold: f64, name: &str) {
11304 assert_eq!(img1.height(), img2.height(), "Heights differ");
11305 assert_eq!(img1.width(), img2.width(), "Widths differ");
11306 assert_eq!(
11307 img1.format().unwrap(),
11308 img2.format().unwrap(),
11309 "PixelFormat differ"
11310 );
11311 assert!(
11312 matches!(
11313 img1.format().unwrap(),
11314 PixelFormat::Rgb
11315 | PixelFormat::Rgba
11316 | PixelFormat::Grey
11317 | PixelFormat::PlanarRgb
11318 | PixelFormat::PlanarRgba
11319 ),
11320 "format must be Rgb or Rgba for comparison"
11321 );
11322
11323 fn flat(img: &TensorDyn) -> Vec<u8> {
11330 let t = img.as_typed::<u8>().expect("u8 image");
11331 let mut out = vec![0u8; t.shape().iter().product::<usize>()];
11332 t.copy_to_flat(&mut out).expect("compact the padded rows");
11333 out
11334 }
11335
11336 let image1 = match img1.format().unwrap() {
11337 PixelFormat::Rgb => image::RgbImage::from_vec(
11338 img1.width().unwrap() as u32,
11339 img1.height().unwrap() as u32,
11340 flat(img1),
11341 )
11342 .unwrap(),
11343 PixelFormat::Rgba => image::RgbaImage::from_vec(
11344 img1.width().unwrap() as u32,
11345 img1.height().unwrap() as u32,
11346 flat(img1),
11347 )
11348 .unwrap()
11349 .convert(),
11350 PixelFormat::Grey => image::GrayImage::from_vec(
11351 img1.width().unwrap() as u32,
11352 img1.height().unwrap() as u32,
11353 flat(img1),
11354 )
11355 .unwrap()
11356 .convert(),
11357 PixelFormat::PlanarRgb | PixelFormat::PlanarRgba => image::GrayImage::from_vec(
11358 img1.width().unwrap() as u32,
11359 (img1.height().unwrap() * img1.format().unwrap().channels()) as u32,
11360 flat(img1),
11361 )
11362 .unwrap()
11363 .convert(),
11364 _ => return,
11365 };
11366
11367 let image2 = match img2.format().unwrap() {
11368 PixelFormat::Rgb => image::RgbImage::from_vec(
11369 img2.width().unwrap() as u32,
11370 img2.height().unwrap() as u32,
11371 flat(img2),
11372 )
11373 .unwrap(),
11374 PixelFormat::Rgba => image::RgbaImage::from_vec(
11375 img2.width().unwrap() as u32,
11376 img2.height().unwrap() as u32,
11377 flat(img2),
11378 )
11379 .unwrap()
11380 .convert(),
11381 PixelFormat::Grey => image::GrayImage::from_vec(
11382 img2.width().unwrap() as u32,
11383 img2.height().unwrap() as u32,
11384 flat(img2),
11385 )
11386 .unwrap()
11387 .convert(),
11388 PixelFormat::PlanarRgb | PixelFormat::PlanarRgba => image::GrayImage::from_vec(
11389 img2.width().unwrap() as u32,
11390 (img2.height().unwrap() * img2.format().unwrap().channels()) as u32,
11391 flat(img2),
11392 )
11393 .unwrap()
11394 .convert(),
11395 _ => return,
11396 };
11397
11398 let similarity = image_compare::rgb_similarity_structure(
11399 &image_compare::Algorithm::RootMeanSquared,
11400 &image1,
11401 &image2,
11402 )
11403 .expect("Image Comparison failed");
11404 if similarity.score < threshold {
11405 similarity
11408 .image
11409 .to_color_map()
11410 .save(format!("{name}.png"))
11411 .unwrap();
11412 panic!(
11413 "{name}: converted image and target image have similarity score too low: {} < {}",
11414 similarity.score, threshold
11415 )
11416 }
11417 }
11418
11419 fn compare_images_convert_to_rgb(
11420 img1: &TensorDyn,
11421 img2: &TensorDyn,
11422 threshold: f64,
11423 name: &str,
11424 ) {
11425 assert_eq!(img1.height(), img2.height(), "Heights differ");
11426 assert_eq!(img1.width(), img2.width(), "Widths differ");
11427
11428 let mut img_rgb1 = TensorDyn::image(
11429 img1.width().unwrap(),
11430 img1.height().unwrap(),
11431 PixelFormat::Rgb,
11432 DType::U8,
11433 Some(TensorMemory::Mem),
11434 edgefirst_tensor::CpuAccess::ReadWrite,
11435 )
11436 .unwrap();
11437 let mut img_rgb2 = TensorDyn::image(
11438 img1.width().unwrap(),
11439 img1.height().unwrap(),
11440 PixelFormat::Rgb,
11441 DType::U8,
11442 Some(TensorMemory::Mem),
11443 edgefirst_tensor::CpuAccess::ReadWrite,
11444 )
11445 .unwrap();
11446 let mut __cv = CPUProcessor::default();
11447 let r1 = __cv.convert(img1, &mut img_rgb1, None);
11448 let r2 = __cv.convert(img2, &mut img_rgb2, None);
11449 if r1.is_err() || r2.is_err() {
11450 let w = img1.width().unwrap() as u32;
11452 let data1 = img1.as_typed::<u8>().unwrap().map().unwrap().to_vec();
11453 let data2 = img2.as_typed::<u8>().unwrap().map().unwrap().to_vec();
11454 let h1 = (data1.len() as u32) / w;
11455 let h2 = (data2.len() as u32) / w;
11456 let g1 = image::GrayImage::from_vec(w, h1, data1).unwrap();
11457 let g2 = image::GrayImage::from_vec(w, h2, data2).unwrap();
11458 let similarity = image_compare::gray_similarity_structure(
11459 &image_compare::Algorithm::RootMeanSquared,
11460 &g1,
11461 &g2,
11462 )
11463 .expect("Image Comparison failed");
11464 if similarity.score < threshold {
11465 panic!(
11466 "{name}: converted image and target image have similarity score too low: {} < {}",
11467 similarity.score, threshold
11468 )
11469 }
11470 return;
11471 }
11472
11473 let image1 = image::RgbImage::from_vec(
11474 img_rgb1.width().unwrap() as u32,
11475 img_rgb1.height().unwrap() as u32,
11476 img_rgb1.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
11477 )
11478 .unwrap();
11479
11480 let image2 = image::RgbImage::from_vec(
11481 img_rgb2.width().unwrap() as u32,
11482 img_rgb2.height().unwrap() as u32,
11483 img_rgb2.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
11484 )
11485 .unwrap();
11486
11487 let similarity = image_compare::rgb_similarity_structure(
11488 &image_compare::Algorithm::RootMeanSquared,
11489 &image1,
11490 &image2,
11491 )
11492 .expect("Image Comparison failed");
11493 if similarity.score < threshold {
11494 similarity
11497 .image
11498 .to_color_map()
11499 .save(format!("{name}.png"))
11500 .unwrap();
11501 panic!(
11502 "{name}: converted image and target image have similarity score too low: {} < {}",
11503 similarity.score, threshold
11504 )
11505 }
11506 }
11507
11508 #[test]
11513 fn test_nv12_image_creation() {
11514 let width = 640;
11515 let height = 480;
11516 let img = TensorDyn::image(
11517 width,
11518 height,
11519 PixelFormat::Nv12,
11520 DType::U8,
11521 None,
11522 edgefirst_tensor::CpuAccess::ReadWrite,
11523 )
11524 .unwrap();
11525
11526 assert_eq!(img.width(), Some(width));
11527 assert_eq!(img.height(), Some(height));
11528 assert_eq!(img.format().unwrap(), PixelFormat::Nv12);
11529 assert_eq!(
11531 img.as_typed::<u8>().unwrap().shape(),
11532 &[height * 3 / 2, width]
11533 );
11534 }
11535
11536 #[test]
11537 fn test_nv12_channels() {
11538 let img = TensorDyn::image(
11539 640,
11540 480,
11541 PixelFormat::Nv12,
11542 DType::U8,
11543 None,
11544 edgefirst_tensor::CpuAccess::ReadWrite,
11545 )
11546 .unwrap();
11547 assert_eq!(img.format().unwrap().channels(), 1);
11549 }
11550
11551 #[test]
11556 fn test_tensor_set_format_planar() {
11557 let mut tensor = Tensor::<u8>::new(&[3, 480, 640], None, None).unwrap();
11558 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
11559 assert_eq!(tensor.format(), Some(PixelFormat::PlanarRgb));
11560 assert_eq!(tensor.width(), Some(640));
11561 assert_eq!(tensor.height(), Some(480));
11562 }
11563
11564 #[test]
11565 fn test_tensor_set_format_interleaved() {
11566 let mut tensor = Tensor::<u8>::new(&[480, 640, 4], None, None).unwrap();
11567 tensor.set_format(PixelFormat::Rgba).unwrap();
11568 assert_eq!(tensor.format(), Some(PixelFormat::Rgba));
11569 assert_eq!(tensor.width(), Some(640));
11570 assert_eq!(tensor.height(), Some(480));
11571 }
11572
11573 #[test]
11574 fn test_tensordyn_image_rgb() {
11575 let img = TensorDyn::image(
11576 640,
11577 480,
11578 PixelFormat::Rgb,
11579 DType::U8,
11580 None,
11581 edgefirst_tensor::CpuAccess::ReadWrite,
11582 )
11583 .unwrap();
11584 assert_eq!(img.width(), Some(640));
11585 assert_eq!(img.height(), Some(480));
11586 assert_eq!(img.format(), Some(PixelFormat::Rgb));
11587 }
11588
11589 #[test]
11590 fn test_tensordyn_image_planar_rgb() {
11591 let img = TensorDyn::image(
11592 640,
11593 480,
11594 PixelFormat::PlanarRgb,
11595 DType::U8,
11596 None,
11597 edgefirst_tensor::CpuAccess::ReadWrite,
11598 )
11599 .unwrap();
11600 assert_eq!(img.width(), Some(640));
11601 assert_eq!(img.height(), Some(480));
11602 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
11603 }
11604
11605 #[test]
11606 fn test_rgb_int8_format() {
11607 let img = TensorDyn::image(
11609 1280,
11610 720,
11611 PixelFormat::Rgb,
11612 DType::I8,
11613 Some(TensorMemory::Mem),
11614 edgefirst_tensor::CpuAccess::ReadWrite,
11615 )
11616 .unwrap();
11617 assert_eq!(img.width(), Some(1280));
11618 assert_eq!(img.height(), Some(720));
11619 assert_eq!(img.format(), Some(PixelFormat::Rgb));
11620 assert_eq!(img.dtype(), DType::I8);
11621 }
11622
11623 #[test]
11624 fn test_planar_rgb_int8_format() {
11625 let img = TensorDyn::image(
11626 1280,
11627 720,
11628 PixelFormat::PlanarRgb,
11629 DType::I8,
11630 Some(TensorMemory::Mem),
11631 edgefirst_tensor::CpuAccess::ReadWrite,
11632 )
11633 .unwrap();
11634 assert_eq!(img.width(), Some(1280));
11635 assert_eq!(img.height(), Some(720));
11636 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
11637 assert_eq!(img.dtype(), DType::I8);
11638 }
11639
11640 #[test]
11641 fn test_rgb_from_tensor() {
11642 let mut tensor = Tensor::<u8>::new(&[720, 1280, 3], None, None).unwrap();
11643 tensor.set_format(PixelFormat::Rgb).unwrap();
11644 let img = TensorDyn::from(tensor);
11645 assert_eq!(img.width(), Some(1280));
11646 assert_eq!(img.height(), Some(720));
11647 assert_eq!(img.format(), Some(PixelFormat::Rgb));
11648 }
11649
11650 #[test]
11651 fn test_planar_rgb_from_tensor() {
11652 let mut tensor = Tensor::<u8>::new(&[3, 720, 1280], None, None).unwrap();
11653 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
11654 let img = TensorDyn::from(tensor);
11655 assert_eq!(img.width(), Some(1280));
11656 assert_eq!(img.height(), Some(720));
11657 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
11658 }
11659
11660 #[test]
11661 fn test_dtype_determines_int8() {
11662 let u8_img = TensorDyn::image(
11664 64,
11665 64,
11666 PixelFormat::Rgb,
11667 DType::U8,
11668 None,
11669 edgefirst_tensor::CpuAccess::ReadWrite,
11670 )
11671 .unwrap();
11672 let i8_img = TensorDyn::image(
11673 64,
11674 64,
11675 PixelFormat::Rgb,
11676 DType::I8,
11677 None,
11678 edgefirst_tensor::CpuAccess::ReadWrite,
11679 )
11680 .unwrap();
11681 assert_eq!(u8_img.dtype(), DType::U8);
11682 assert_eq!(i8_img.dtype(), DType::I8);
11683 }
11684
11685 #[test]
11686 fn test_pixel_layout_packed_vs_planar() {
11687 assert_eq!(PixelFormat::Rgb.layout(), PixelLayout::Packed);
11689 assert_eq!(PixelFormat::Rgba.layout(), PixelLayout::Packed);
11690 assert_eq!(PixelFormat::PlanarRgb.layout(), PixelLayout::Planar);
11691 assert_eq!(PixelFormat::Nv12.layout(), PixelLayout::SemiPlanar);
11692 }
11693
11694 #[cfg(target_os = "linux")]
11705 #[cfg(feature = "opengl")]
11706 #[test]
11707 fn pbo_reports_why_it_cannot_pin() {
11708 use edgefirst_tensor::{Error, TensorTrait};
11709
11710 let converter = ImageProcessor::new().unwrap();
11711 let is_pbo = converter
11712 .opengl
11713 .as_ref()
11714 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
11715 if !is_pbo {
11716 crate::test_support::report_skip(
11717 "pbo_reports_why_it_cannot_pin - transfer backend is not PBO",
11718 );
11719 return;
11720 }
11721
11722 let t = converter
11723 .create_image(
11724 64,
11725 64,
11726 PixelFormat::Rgba,
11727 DType::U8,
11728 None,
11729 edgefirst_tensor::CpuAccess::ReadWrite,
11730 )
11731 .unwrap();
11732 assert_eq!(t.as_typed::<u8>().unwrap().memory(), TensorMemory::Pbo);
11733 let t = t.as_typed::<u8>().unwrap();
11734
11735 match t.pin_host(edgefirst_tensor::CpuAccess::ReadWrite) {
11736 Err(Error::NotImplemented(m)) => {
11737 assert!(
11738 m.contains("glMapBufferRange"),
11739 "must name the constraint: {m}"
11740 );
11741 assert!(m.contains("map()"), "must point at the supported API: {m}");
11742 }
11743 other => panic!("expected an explained NotImplemented, got {other:?}"),
11744 }
11745 assert!(matches!(
11747 t.sync_for_cpu(edgefirst_tensor::CpuAccess::Read),
11748 Err(Error::NotImplemented(_))
11749 ));
11750
11751 let g = t.map().expect("map must work where pin cannot");
11753 assert!(!g.as_slice().is_empty());
11754 }
11755
11756 #[cfg(target_os = "linux")]
11760 #[cfg(feature = "opengl")]
11761 #[test]
11762 fn test_convert_pbo_to_pbo() {
11763 let mut converter = ImageProcessor::new().unwrap();
11764
11765 let is_pbo = converter
11767 .opengl
11768 .as_ref()
11769 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
11770 if !is_pbo {
11771 crate::test_support::report_skip(
11772 "test_convert_pbo_to_pbo - transfer backend is not PBO",
11773 );
11774 return;
11775 }
11776
11777 let src_w = 640;
11778 let src_h = 480;
11779 let dst_w = 320;
11780 let dst_h = 240;
11781
11782 let pbo_src = converter
11784 .create_image(
11785 src_w,
11786 src_h,
11787 PixelFormat::Rgba,
11788 DType::U8,
11789 None,
11790 edgefirst_tensor::CpuAccess::ReadWrite,
11791 )
11792 .unwrap();
11793 assert_eq!(
11794 pbo_src.as_typed::<u8>().unwrap().memory(),
11795 TensorMemory::Pbo,
11796 "create_image should produce a PBO tensor"
11797 );
11798
11799 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
11801 let jpeg_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
11802
11803 let mem_src = TensorDyn::image(
11805 src_w,
11806 src_h,
11807 PixelFormat::Rgba,
11808 DType::U8,
11809 Some(TensorMemory::Mem),
11810 edgefirst_tensor::CpuAccess::ReadWrite,
11811 )
11812 .unwrap();
11813 let (result, _jpeg_src, mem_src) = convert_img(
11814 &mut CPUProcessor::new(),
11815 jpeg_src,
11816 mem_src,
11817 Rotation::None,
11818 Flip::None,
11819 Crop::no_crop(),
11820 );
11821 result.unwrap();
11822
11823 {
11825 let src_data = mem_src.as_typed::<u8>().unwrap().map().unwrap();
11826 let mut pbo_map = pbo_src.as_typed::<u8>().unwrap().map().unwrap();
11827 pbo_map.copy_from_slice(&src_data);
11828 }
11829
11830 let pbo_dst = converter
11832 .create_image(
11833 dst_w,
11834 dst_h,
11835 PixelFormat::Rgba,
11836 DType::U8,
11837 None,
11838 edgefirst_tensor::CpuAccess::ReadWrite,
11839 )
11840 .unwrap();
11841 assert_eq!(
11842 pbo_dst.as_typed::<u8>().unwrap().memory(),
11843 TensorMemory::Pbo
11844 );
11845
11846 let mut pbo_dst = pbo_dst;
11848 let result = converter.convert(&pbo_src, &mut pbo_dst, None);
11849 result.unwrap();
11850
11851 let cpu_dst = TensorDyn::image(
11853 dst_w,
11854 dst_h,
11855 PixelFormat::Rgba,
11856 DType::U8,
11857 Some(TensorMemory::Mem),
11858 edgefirst_tensor::CpuAccess::ReadWrite,
11859 )
11860 .unwrap();
11861 let (result, _mem_src, cpu_dst) = convert_img(
11862 &mut CPUProcessor::new(),
11863 mem_src,
11864 cpu_dst,
11865 Rotation::None,
11866 Flip::None,
11867 Crop::no_crop(),
11868 );
11869 result.unwrap();
11870
11871 let pbo_dst_img = {
11872 let mut __t = pbo_dst.into_u8().unwrap();
11873 __t.set_format(PixelFormat::Rgba).unwrap();
11874 TensorDyn::from(__t)
11875 };
11876 compare_images(&pbo_dst_img, &cpu_dst, 0.95, function!());
11877 log::info!("test_convert_pbo_to_pbo: PASS — PBO-to-PBO convert matches CPU reference");
11878 }
11879
11880 #[test]
11881 fn test_image_bgra() {
11882 let img = TensorDyn::image(
11883 640,
11884 480,
11885 PixelFormat::Bgra,
11886 DType::U8,
11887 Some(edgefirst_tensor::TensorMemory::Mem),
11888 edgefirst_tensor::CpuAccess::ReadWrite,
11889 )
11890 .unwrap();
11891 assert_eq!(img.width(), Some(640));
11892 assert_eq!(img.height(), Some(480));
11893 assert_eq!(img.format().unwrap().channels(), 4);
11894 assert_eq!(img.format().unwrap(), PixelFormat::Bgra);
11895 }
11896
11897 #[test]
11902 fn test_force_backend_cpu() {
11903 let _lock = acquire_env_lock();
11904 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11905 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
11906 let converter = ImageProcessor::new().unwrap();
11907 assert!(converter.cpu.is_some());
11908 assert_eq!(converter.forced_backend, Some(ForcedBackend::Cpu));
11909 }
11910
11911 #[test]
11912 fn test_force_backend_invalid() {
11913 let _lock = acquire_env_lock();
11914 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11915 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "invalid") };
11916 let result = ImageProcessor::new();
11917 assert!(
11918 matches!(&result, Err(Error::ForcedBackendUnavailable(s)) if s.contains("unknown")),
11919 "invalid backend value should return ForcedBackendUnavailable error: {result:?}"
11920 );
11921 }
11922
11923 #[test]
11924 fn test_force_backend_unset() {
11925 let _lock = acquire_env_lock();
11926 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11927 unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") };
11928 let converter = ImageProcessor::new().unwrap();
11929 assert!(converter.forced_backend.is_none());
11930 }
11931
11932 #[test]
11937 fn test_draw_proto_masks_no_cpu_returns_error() {
11938 let _lock = acquire_env_lock();
11940 let _guard = EnvGuard::snapshot(&[
11941 "EDGEFIRST_FORCE_BACKEND",
11942 "EDGEFIRST_DISABLE_GL",
11943 "EDGEFIRST_DISABLE_G2D",
11944 "EDGEFIRST_DISABLE_CPU",
11945 ]);
11946
11947 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
11949 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
11950 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
11951
11952 let mut converter = ImageProcessor::new().unwrap();
11953 assert!(converter.cpu.is_none(), "CPU should be disabled");
11954
11955 let dst = TensorDyn::image(
11956 640,
11957 480,
11958 PixelFormat::Rgba,
11959 DType::U8,
11960 Some(TensorMemory::Mem),
11961 edgefirst_tensor::CpuAccess::ReadWrite,
11962 )
11963 .unwrap();
11964 let mut dst_dyn = dst;
11965 let det = [DetectBox {
11966 bbox: edgefirst_tensor::BoundingBox {
11967 xmin: 0.1,
11968 ymin: 0.1,
11969 xmax: 0.5,
11970 ymax: 0.5,
11971 },
11972 score: 0.9,
11973 label: 0,
11974 }];
11975 let proto_data = {
11976 use edgefirst_tensor::{Tensor, TensorDyn};
11977 let coeff_t = Tensor::<f32>::from_slice(&[0.5_f32; 4], &[1, 4]).unwrap();
11978 let protos_t =
11979 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
11980 ProtoData {
11981 mask_coefficients: TensorDyn::F32(coeff_t),
11982 protos: TensorDyn::F32(protos_t),
11983 layout: ProtoLayout::Nhwc,
11984 }
11985 };
11986 let result =
11987 converter.draw_proto_masks(&mut dst_dyn, &det, &proto_data, Default::default());
11988 assert!(
11989 matches!(&result, Err(Error::Internal(s)) if s.contains("CPU backend")),
11990 "draw_proto_masks without CPU should return Internal error: {result:?}"
11991 );
11992 }
11993
11994 #[test]
11995 fn test_draw_proto_masks_cpu_fallback_works() {
11996 let _lock = acquire_env_lock();
11999 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
12000 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
12001 let mut converter = ImageProcessor::new().unwrap();
12002 assert!(converter.cpu.is_some());
12003
12004 let dst = TensorDyn::image(
12005 64,
12006 64,
12007 PixelFormat::Rgba,
12008 DType::U8,
12009 Some(TensorMemory::Mem),
12010 edgefirst_tensor::CpuAccess::ReadWrite,
12011 )
12012 .unwrap();
12013 let mut dst_dyn = dst;
12014 let det = [DetectBox {
12015 bbox: edgefirst_tensor::BoundingBox {
12016 xmin: 0.1,
12017 ymin: 0.1,
12018 xmax: 0.5,
12019 ymax: 0.5,
12020 },
12021 score: 0.9,
12022 label: 0,
12023 }];
12024 let proto_data = {
12025 use edgefirst_tensor::{Tensor, TensorDyn};
12026 let coeff_t = Tensor::<f32>::from_slice(&[0.5_f32; 4], &[1, 4]).unwrap();
12027 let protos_t =
12028 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
12029 ProtoData {
12030 mask_coefficients: TensorDyn::F32(coeff_t),
12031 protos: TensorDyn::F32(protos_t),
12032 layout: ProtoLayout::Nhwc,
12033 }
12034 };
12035 let result =
12036 converter.draw_proto_masks(&mut dst_dyn, &det, &proto_data, Default::default());
12037 assert!(result.is_ok(), "CPU fallback path should work: {result:?}");
12038 }
12039
12040 fn acquire_env_lock() -> std::sync::MutexGuard<'static, ()> {
12072 use std::sync::{Mutex, OnceLock};
12073 static ENV_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
12074 ENV_MUTEX
12075 .get_or_init(|| Mutex::new(()))
12076 .lock()
12077 .unwrap_or_else(|e| e.into_inner())
12078 }
12079
12080 struct EnvGuard {
12083 vars: Vec<(&'static str, Option<String>)>,
12084 }
12085
12086 impl EnvGuard {
12087 fn snapshot(names: &[&'static str]) -> Self {
12091 Self {
12092 vars: names.iter().map(|&k| (k, std::env::var(k).ok())).collect(),
12093 }
12094 }
12095 }
12096
12097 impl Drop for EnvGuard {
12098 fn drop(&mut self) {
12099 for (k, v) in &self.vars {
12100 match v {
12101 Some(s) => unsafe { std::env::set_var(k, s) },
12102 None => unsafe { std::env::remove_var(k) },
12103 }
12104 }
12105 }
12106 }
12107
12108 #[test]
12111 fn test_interpolation_env_parse() {
12112 let _lock = acquire_env_lock();
12113 let _guard = EnvGuard::snapshot(&["EDGEFIRST_INTERPOLATION"]);
12114 for (value, want) in [
12115 (Some("nearest"), Some(Interpolation::Nearest)),
12116 (Some(" BILINEAR "), Some(Interpolation::Bilinear)),
12117 (Some("lanczos"), None),
12118 (Some(""), None),
12119 (None, None),
12120 ] {
12121 match value {
12122 Some(v) => unsafe { std::env::set_var("EDGEFIRST_INTERPOLATION", v) },
12123 None => unsafe { std::env::remove_var("EDGEFIRST_INTERPOLATION") },
12124 }
12125 assert_eq!(
12126 interpolation_from_env(),
12127 want,
12128 "EDGEFIRST_INTERPOLATION={value:?}"
12129 );
12130 }
12131 }
12132
12133 #[test]
12136 fn test_interpolation_config_and_env_reach_cpu() {
12137 let _lock = acquire_env_lock();
12138 let _guard = EnvGuard::snapshot(&["EDGEFIRST_INTERPOLATION"]);
12139 let mut checker = Vec::with_capacity(8 * 8 * 4);
12140 for y in 0..8 {
12141 for x in 0..8 {
12142 let v = if (x + y) % 2 == 0 { 0u8 } else { 255 };
12143 checker.extend_from_slice(&[v, v, v, 255]);
12144 }
12145 }
12146 let resize = |processor: &mut dyn ImageProcessorTrait| {
12147 let mut src = TensorDyn::image(
12148 8,
12149 8,
12150 PixelFormat::Rgba,
12151 DType::U8,
12152 Some(TensorMemory::Mem),
12153 edgefirst_tensor::CpuAccess::ReadWrite,
12154 )
12155 .unwrap();
12156 src.as_u8_mut()
12157 .unwrap()
12158 .map_mut()
12159 .unwrap()
12160 .as_mut_slice()
12161 .copy_from_slice(&checker);
12162 let mut dst = TensorDyn::image(
12163 12,
12164 12,
12165 PixelFormat::Rgba,
12166 DType::U8,
12167 Some(TensorMemory::Mem),
12168 edgefirst_tensor::CpuAccess::ReadWrite,
12169 )
12170 .unwrap();
12171 processor.convert(&src, &mut dst, None).unwrap();
12172 dst.as_u8().unwrap().map().unwrap().as_slice().to_vec()
12173 };
12174 let nearest = resize(&mut CPUProcessor::new_nearest());
12175 let bilinear = resize(&mut CPUProcessor::new());
12176 assert_ne!(nearest, bilinear, "the checker must tell the filters apart");
12177 let config = |interpolation| ImageProcessorConfig {
12178 backend: ComputeBackend::Cpu,
12179 interpolation,
12180 ..Default::default()
12181 };
12182
12183 unsafe { std::env::remove_var("EDGEFIRST_INTERPOLATION") };
12184 let mut p = ImageProcessor::with_config(config(Interpolation::Nearest)).unwrap();
12185 assert_eq!(resize(&mut p), nearest, "config Nearest");
12186 p.set_interpolation(Interpolation::Bilinear).unwrap();
12187 assert_eq!(resize(&mut p), bilinear, "set_interpolation(Bilinear)");
12188
12189 unsafe { std::env::set_var("EDGEFIRST_INTERPOLATION", "nearest") };
12190 let mut p = ImageProcessor::with_config(config(Interpolation::Bilinear)).unwrap();
12191 assert_eq!(
12192 resize(&mut p),
12193 nearest,
12194 "EDGEFIRST_INTERPOLATION pins Nearest"
12195 );
12196 p.set_interpolation(Interpolation::Bilinear).unwrap();
12197 assert_eq!(
12198 resize(&mut p),
12199 nearest,
12200 "the env pin outlives set_interpolation"
12201 );
12202 }
12203
12204 #[test]
12208 fn test_per_convert_interpolation_overrides_processor() {
12209 let _lock = acquire_env_lock();
12210 let _guard = EnvGuard::snapshot(&["EDGEFIRST_INTERPOLATION"]);
12211 let mut checker = Vec::with_capacity(8 * 8 * 4);
12212 for y in 0..8 {
12213 for x in 0..8 {
12214 let v = if (x + y) % 2 == 0 { 0u8 } else { 255 };
12215 checker.extend_from_slice(&[v, v, v, 255]);
12216 }
12217 }
12218 let resize = |processor: &mut dyn ImageProcessorTrait, options: Option<ConvertOptions>| {
12219 let mut src = TensorDyn::image(
12220 8,
12221 8,
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(&checker);
12234 let mut dst = TensorDyn::image(
12235 12,
12236 12,
12237 PixelFormat::Rgba,
12238 DType::U8,
12239 Some(TensorMemory::Mem),
12240 edgefirst_tensor::CpuAccess::ReadWrite,
12241 )
12242 .unwrap();
12243 processor.convert(&src, &mut dst, options).unwrap();
12244 dst.as_u8().unwrap().map().unwrap().as_slice().to_vec()
12245 };
12246 let nearest = resize(&mut CPUProcessor::new_nearest(), None);
12247 let bilinear = resize(&mut CPUProcessor::new(), None);
12248 assert_ne!(nearest, bilinear, "the checker must tell the filters apart");
12249 let cpu_only = || {
12250 ImageProcessor::with_config(ImageProcessorConfig {
12251 backend: ComputeBackend::Cpu,
12252 ..Default::default()
12253 })
12254 .unwrap()
12255 };
12256 let model_a = ConvertOptions::new().interpolation(Interpolation::Nearest);
12257 let model_b = ConvertOptions::new().interpolation(Interpolation::Bicubic);
12258
12259 unsafe { std::env::remove_var("EDGEFIRST_INTERPOLATION") };
12260 let mut p = cpu_only();
12261 assert_eq!(p.interpolation(), Interpolation::Bilinear);
12262 for _ in 0..2 {
12263 assert_eq!(resize(&mut p, Some(model_a)), nearest, "model A: nearest");
12264 assert_eq!(resize(&mut p, Some(model_b)), bilinear, "model B: bicubic");
12265 assert_eq!(resize(&mut p, None), bilinear, "processor setting kept");
12266 }
12267 p.set_interpolation(Interpolation::Nearest).unwrap();
12268 assert_eq!(resize(&mut p, Some(model_b)), bilinear, "options win");
12269 assert_eq!(resize(&mut p, None), nearest, "processor fallback");
12270
12271 let mut spec = PreprocessSpec::new();
12272 spec.interpolation = Interpolation::Nearest;
12273 let from_spec = ConvertOptions::from_spec(&spec).unwrap();
12274 assert_eq!(from_spec.get_interpolation(), Some(Interpolation::Nearest));
12275 let mut p = cpu_only();
12276 let stretch = from_spec.crop(Crop::default());
12277 assert_eq!(
12278 resize(&mut p, Some(stretch)),
12279 nearest,
12280 "from_spec carries it"
12281 );
12282
12283 let cpu = p.cpu.as_mut().unwrap();
12284 cpu.set_interpolation(Interpolation::Nearest);
12285 p.set_interpolation(Interpolation::Bilinear).unwrap();
12286 assert_eq!(
12287 resize(p.cpu.as_mut().unwrap(), None),
12288 nearest,
12289 "an unchanged mode does not reach the backends"
12290 );
12291
12292 unsafe { std::env::set_var("EDGEFIRST_INTERPOLATION", "bilinear") };
12293 let mut p = cpu_only();
12294 assert_eq!(resize(&mut p, Some(model_a)), bilinear, "the env pin wins");
12295 }
12296
12297 #[test]
12300 #[cfg(feature = "opengl")]
12301 fn test_per_convert_interpolation_reaches_gl_worker() {
12302 let _lock = acquire_env_lock();
12303 let _guard = EnvGuard::snapshot(&["EDGEFIRST_INTERPOLATION"]);
12304 unsafe { std::env::remove_var("EDGEFIRST_INTERPOLATION") };
12305 let mut p = match ImageProcessor::with_config(ImageProcessorConfig {
12306 backend: ComputeBackend::OpenGl,
12307 ..Default::default()
12308 }) {
12309 Ok(p) if p.opengl.is_some() => p,
12310 _ => {
12311 crate::test_support::report_skip(&format!(
12312 "{} — GL backend unavailable",
12313 function!()
12314 ));
12315 return;
12316 }
12317 };
12318 let (sw, sh, dw, dh) = (40usize, 30usize, 56usize, 42usize);
12319 let mut rgba = Vec::with_capacity(sw * sh * 4);
12320 for y in 0..sh {
12321 for x in 0..sw {
12322 let v = if (x + y) % 2 == 0 { 16u8 } else { 235 };
12323 rgba.extend_from_slice(&[v, 128, 255 - v, 255]);
12324 }
12325 }
12326 let mut src = TensorDyn::image(
12327 sw,
12328 sh,
12329 PixelFormat::Rgba,
12330 DType::U8,
12331 Some(TensorMemory::Mem),
12332 edgefirst_tensor::CpuAccess::ReadWrite,
12333 )
12334 .unwrap();
12335 src.as_u8_mut()
12336 .unwrap()
12337 .map_mut()
12338 .unwrap()
12339 .as_mut_slice()
12340 .copy_from_slice(&rgba);
12341 let gl_convert = |p: &mut ImageProcessor, options: Option<ConvertOptions>| {
12342 let mut dst = TensorDyn::image(
12343 dw,
12344 dh,
12345 PixelFormat::Rgba,
12346 DType::U8,
12347 Some(TensorMemory::Mem),
12348 edgefirst_tensor::CpuAccess::ReadWrite,
12349 )
12350 .unwrap();
12351 let options = p.resolve_interpolation(options.unwrap_or_default());
12352 p.opengl
12353 .as_mut()
12354 .unwrap()
12355 .convert(&src, &mut dst, Some(options))
12356 .unwrap();
12357 dst.as_u8().unwrap().map().unwrap().as_slice().to_vec()
12358 };
12359 let nearest = ConvertOptions::new().interpolation(Interpolation::Nearest);
12360 let per_convert = gl_convert(&mut p, Some(nearest));
12361 let processor_bilinear = gl_convert(&mut p, None);
12362 p.set_interpolation(Interpolation::Nearest).unwrap();
12363 let processor_nearest = gl_convert(&mut p, None);
12364 assert_ne!(processor_bilinear, processor_nearest);
12365 assert_eq!(per_convert, processor_nearest, "per-convert Nearest");
12366 let bilinear = ConvertOptions::new().interpolation(Interpolation::Bilinear);
12367 assert_eq!(gl_convert(&mut p, Some(bilinear)), processor_bilinear);
12368 }
12369
12370 #[test]
12372 fn test_interpolation_fallback_warns_once() {
12373 runnable_interpolation(Interpolation::Area);
12374 assert!(!warn_first_fallback(Interpolation::Area));
12375 assert_eq!(
12376 runnable_interpolation(Interpolation::Area),
12377 Interpolation::Bilinear
12378 );
12379 assert!(!warn_first_fallback(Interpolation::Area));
12380 }
12381
12382 fn with_force_backend<R>(value: Option<&str>, body: impl FnOnce() -> R) -> R {
12386 let _lock = acquire_env_lock();
12387 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
12388 match value {
12389 Some(v) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", v) },
12390 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
12391 }
12392 body()
12393 }
12394
12395 fn make_dirty_dst(w: usize, h: usize, mem: Option<TensorMemory>) -> TensorDyn {
12400 let dst = TensorDyn::image(
12401 w,
12402 h,
12403 PixelFormat::Rgba,
12404 DType::U8,
12405 mem,
12406 edgefirst_tensor::CpuAccess::ReadWrite,
12407 )
12408 .unwrap();
12409 {
12410 use edgefirst_tensor::TensorMapTrait;
12411 let u8t = dst.as_typed::<u8>().unwrap();
12412 let mut map = u8t.map().unwrap();
12413 for (i, b) in map.as_mut_slice().iter_mut().enumerate() {
12414 *b = 0xA0u8.wrapping_add((i as u8) & 0x3F);
12415 }
12416 }
12417 dst
12418 }
12419
12420 fn make_bg(w: usize, h: usize, mem: Option<TensorMemory>, rgba: [u8; 4]) -> TensorDyn {
12422 let bg = TensorDyn::image(
12423 w,
12424 h,
12425 PixelFormat::Rgba,
12426 DType::U8,
12427 mem,
12428 edgefirst_tensor::CpuAccess::ReadWrite,
12429 )
12430 .unwrap();
12431 {
12432 use edgefirst_tensor::TensorMapTrait;
12433 let u8t = bg.as_typed::<u8>().unwrap();
12434 let mut map = u8t.map().unwrap();
12435 for chunk in map.as_mut_slice().as_chunks_mut::<4>().0 {
12436 chunk.copy_from_slice(&rgba);
12437 }
12438 }
12439 bg
12440 }
12441
12442 fn pixel_at(dst: &TensorDyn, x: usize, y: usize) -> [u8; 4] {
12443 use edgefirst_tensor::TensorMapTrait;
12444 let w = dst.width().unwrap();
12445 let off = (y * w + x) * 4;
12446 let u8t = dst.as_typed::<u8>().unwrap();
12447 let map = u8t.map().unwrap();
12448 let s = map.as_slice();
12449 [s[off], s[off + 1], s[off + 2], s[off + 3]]
12450 }
12451
12452 fn assert_every_pixel_eq(dst: &TensorDyn, expected: [u8; 4], case: &str) {
12453 use edgefirst_tensor::TensorMapTrait;
12454 let u8t = dst.as_typed::<u8>().unwrap();
12455 let map = u8t.map().unwrap();
12456 for (i, chunk) in map.as_slice().as_chunks::<4>().0.iter().enumerate() {
12457 assert_eq!(
12458 chunk, &expected,
12459 "{case}: pixel idx {i} = {chunk:?}, expected {expected:?}"
12460 );
12461 }
12462 }
12463
12464 fn scenario_empty_no_bg(processor: &mut ImageProcessor, case: &str) {
12467 let mut dst = make_dirty_dst(64, 64, None);
12468 processor
12469 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::default())
12470 .unwrap_or_else(|e| panic!("{case}/decoded_masks empty+no-bg failed: {e:?}"));
12471 assert_every_pixel_eq(&dst, [0, 0, 0, 0], &format!("{case}/decoded"));
12472
12473 let mut dst = make_dirty_dst(64, 64, None);
12474 let proto = {
12475 use edgefirst_tensor::{Tensor, TensorDyn};
12476 let coeff_t = Tensor::<f32>::from_slice(&[0.0_f32; 4], &[1, 4]).unwrap();
12478 let protos_t =
12479 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
12480 ProtoData {
12481 mask_coefficients: TensorDyn::F32(coeff_t),
12482 protos: TensorDyn::F32(protos_t),
12483 layout: ProtoLayout::Nhwc,
12484 }
12485 };
12486 processor
12487 .draw_proto_masks(&mut dst, &[], &proto, MaskOverlay::default())
12488 .unwrap_or_else(|e| panic!("{case}/proto_masks empty+no-bg failed: {e:?}"));
12489 assert_every_pixel_eq(&dst, [0, 0, 0, 0], &format!("{case}/proto"));
12490 }
12491
12492 fn scenario_empty_with_bg(processor: &mut ImageProcessor, case: &str) {
12495 let bg_color = [42, 99, 200, 255];
12496 let bg = make_bg(64, 64, None, bg_color);
12497 let overlay = MaskOverlay::new().with_background(&bg);
12498
12499 let mut dst = make_dirty_dst(64, 64, None);
12500 processor
12501 .draw_decoded_masks(&mut dst, &[], &[], overlay)
12502 .unwrap_or_else(|e| panic!("{case}/decoded_masks empty+bg failed: {e:?}"));
12503 assert_every_pixel_eq(&dst, bg_color, &format!("{case}/decoded bg blit"));
12504
12505 let mut dst = make_dirty_dst(64, 64, None);
12506 let proto = {
12507 use edgefirst_tensor::{Tensor, TensorDyn};
12508 let coeff_t = Tensor::<f32>::from_slice(&[0.0_f32; 4], &[1, 4]).unwrap();
12510 let protos_t =
12511 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
12512 ProtoData {
12513 mask_coefficients: TensorDyn::F32(coeff_t),
12514 protos: TensorDyn::F32(protos_t),
12515 layout: ProtoLayout::Nhwc,
12516 }
12517 };
12518 processor
12519 .draw_proto_masks(&mut dst, &[], &proto, overlay)
12520 .unwrap_or_else(|e| panic!("{case}/proto_masks empty+bg failed: {e:?}"));
12521 assert_every_pixel_eq(&dst, bg_color, &format!("{case}/proto bg blit"));
12522 }
12523
12524 fn scenario_detect_no_bg(processor: &mut ImageProcessor, case: &str) {
12528 use edgefirst_tensor::Segmentation;
12529 use ndarray::Array3;
12530 processor
12531 .set_class_colors(&[[200, 80, 40, 255]])
12532 .expect("set_class_colors");
12533
12534 let detect = DetectBox {
12535 bbox: [0.25, 0.25, 0.75, 0.75].into(),
12536 score: 0.99,
12537 label: 0,
12538 };
12539 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
12540 let seg = Segmentation {
12541 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
12542 .unwrap()
12543 .into(),
12544 xmin: 0.25,
12545 ymin: 0.25,
12546 xmax: 0.75,
12547 ymax: 0.75,
12548 };
12549
12550 let mut dst = make_dirty_dst(64, 64, None);
12551 processor
12552 .draw_decoded_masks(&mut dst, &[detect], &[seg], MaskOverlay::default())
12553 .unwrap_or_else(|e| panic!("{case}/decoded_masks detect+no-bg failed: {e:?}"));
12554
12555 let corner = pixel_at(&dst, 2, 2);
12557 assert_eq!(
12558 corner,
12559 [0, 0, 0, 0],
12560 "{case}/decoded: corner (2,2) leaked dirty pattern: {corner:?}"
12561 );
12562 let center = pixel_at(&dst, 32, 32);
12566 assert!(
12567 center != [0, 0, 0, 0],
12568 "{case}/decoded: center (32,32) was not coloured: {center:?}"
12569 );
12570 }
12571
12572 fn scenario_detect_with_bg(processor: &mut ImageProcessor, case: &str) {
12575 use edgefirst_tensor::Segmentation;
12576 use ndarray::Array3;
12577 processor
12578 .set_class_colors(&[[200, 80, 40, 255]])
12579 .expect("set_class_colors");
12580 let bg_color = [10, 20, 30, 255];
12581 let bg = make_bg(64, 64, None, bg_color);
12582
12583 let detect = DetectBox {
12584 bbox: [0.25, 0.25, 0.75, 0.75].into(),
12585 score: 0.99,
12586 label: 0,
12587 };
12588 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
12589 let seg = Segmentation {
12590 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
12591 .unwrap()
12592 .into(),
12593 xmin: 0.25,
12594 ymin: 0.25,
12595 xmax: 0.75,
12596 ymax: 0.75,
12597 };
12598
12599 let overlay = MaskOverlay::new().with_background(&bg);
12600 let mut dst = make_dirty_dst(64, 64, None);
12601 processor
12602 .draw_decoded_masks(&mut dst, &[detect], &[seg], overlay)
12603 .unwrap_or_else(|e| panic!("{case}/decoded_masks detect+bg failed: {e:?}"));
12604
12605 let corner = pixel_at(&dst, 2, 2);
12607 assert_eq!(
12608 corner, bg_color,
12609 "{case}/decoded: corner (2,2) should show bg {bg_color:?} got {corner:?}"
12610 );
12611 let center = pixel_at(&dst, 32, 32);
12614 assert!(
12615 center != bg_color,
12616 "{case}/decoded: center (32,32) should differ from bg {bg_color:?}, got {center:?}"
12617 );
12618 }
12619
12620 fn run_all_scenarios(
12623 force_backend: Option<&'static str>,
12624 case: &'static str,
12625 require_dma_for_bg: bool,
12626 ) {
12627 if require_dma_for_bg && !crate::test_support::is_dma_available() {
12628 crate::test_support::report_skip(&format!("{case} — DMA not available on this host"));
12629 return;
12630 }
12631 let processor_result = with_force_backend(force_backend, ImageProcessor::new);
12632 let mut processor = match processor_result {
12633 Ok(p) => p,
12634 Err(e) => {
12635 crate::test_support::report_skip(&format!("{case} — backend init failed: {e:?}"));
12636 return;
12637 }
12638 };
12639 scenario_empty_no_bg(&mut processor, case);
12640 scenario_empty_with_bg(&mut processor, case);
12641 scenario_detect_no_bg(&mut processor, case);
12642 scenario_detect_with_bg(&mut processor, case);
12643 }
12644
12645 #[test]
12646 fn test_draw_masks_4_scenarios_cpu() {
12647 run_all_scenarios(Some("cpu"), "cpu", false);
12648 }
12649
12650 #[test]
12651 fn test_draw_masks_4_scenarios_auto() {
12652 run_all_scenarios(None, "auto", false);
12653 }
12654
12655 #[cfg(any(
12656 target_os = "linux",
12657 target_os = "macos",
12658 target_os = "ios",
12659 target_os = "android",
12660 target_os = "windows"
12661 ))]
12662 #[cfg(feature = "opengl")]
12663 #[test]
12664 fn test_draw_masks_4_scenarios_opengl() {
12665 run_all_scenarios(Some("opengl"), "opengl", false);
12666 }
12667
12668 #[cfg(target_os = "linux")]
12673 #[test]
12674 fn test_draw_masks_zero_detection_g2d_forced() {
12675 if !crate::test_support::is_dma_available() {
12676 crate::test_support::report_skip("g2d forced — DMA not available on this host");
12677 return;
12678 }
12679 let processor_result = with_force_backend(Some("g2d"), ImageProcessor::new);
12680 let mut processor = match processor_result {
12681 Ok(p) => p,
12682 Err(e) => {
12683 crate::test_support::report_skip(&format!("g2d forced — init failed: {e:?}"));
12684 return;
12685 }
12686 };
12687
12688 let mut dst = TensorDyn::image(
12690 64,
12691 64,
12692 PixelFormat::Rgba,
12693 DType::U8,
12694 Some(TensorMemory::DmaBuf),
12695 edgefirst_tensor::CpuAccess::ReadWrite,
12696 )
12697 .unwrap();
12698 {
12699 use edgefirst_tensor::TensorMapTrait;
12700 let u8t = dst.as_typed_mut::<u8>().unwrap();
12701 let mut map = u8t.map().unwrap();
12702 map.as_mut_slice().fill(0xBB);
12703 }
12704 processor
12705 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::default())
12706 .expect("g2d empty+no-bg");
12707 assert_every_pixel_eq(&dst, [0, 0, 0, 0], "g2d/case1 cleared");
12708
12709 let bg_color = [7, 11, 13, 255];
12711 let bg = {
12712 let t = TensorDyn::image(
12713 64,
12714 64,
12715 PixelFormat::Rgba,
12716 DType::U8,
12717 Some(TensorMemory::DmaBuf),
12718 edgefirst_tensor::CpuAccess::ReadWrite,
12719 )
12720 .unwrap();
12721 {
12722 use edgefirst_tensor::TensorMapTrait;
12723 let u8t = t.as_typed::<u8>().unwrap();
12724 let mut map = u8t.map().unwrap();
12725 for chunk in map.as_mut_slice().as_chunks_mut::<4>().0 {
12726 chunk.copy_from_slice(&bg_color);
12727 }
12728 }
12729 t
12730 };
12731 let mut dst = TensorDyn::image(
12732 64,
12733 64,
12734 PixelFormat::Rgba,
12735 DType::U8,
12736 Some(TensorMemory::DmaBuf),
12737 edgefirst_tensor::CpuAccess::ReadWrite,
12738 )
12739 .unwrap();
12740 {
12741 use edgefirst_tensor::TensorMapTrait;
12742 let u8t = dst.as_typed_mut::<u8>().unwrap();
12743 let mut map = u8t.map().unwrap();
12744 map.as_mut_slice().fill(0x55);
12745 }
12746 processor
12747 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::new().with_background(&bg))
12748 .expect("g2d empty+bg");
12749 assert_every_pixel_eq(&dst, bg_color, "g2d/case2 bg blit");
12750
12751 let detect = DetectBox {
12753 bbox: [0.25, 0.25, 0.75, 0.75].into(),
12754 score: 0.9,
12755 label: 0,
12756 };
12757 let mut dst = TensorDyn::image(
12758 64,
12759 64,
12760 PixelFormat::Rgba,
12761 DType::U8,
12762 Some(TensorMemory::DmaBuf),
12763 edgefirst_tensor::CpuAccess::ReadWrite,
12764 )
12765 .unwrap();
12766 let err = processor
12767 .draw_decoded_masks(&mut dst, &[detect], &[], MaskOverlay::default())
12768 .expect_err("g2d must reject detect-present draw_decoded_masks");
12769 assert!(
12770 matches!(err, Error::NotImplemented(_)),
12771 "g2d case3 wrong error: {err:?}"
12772 );
12773 }
12774
12775 #[test]
12776 fn test_set_format_then_cpu_convert() {
12777 let _lock = acquire_env_lock();
12780 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
12781 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
12782 let mut processor = ImageProcessor::new().unwrap();
12783
12784 let image = edgefirst_bench::testdata::read("zidane.jpg");
12786 let src = load_image_test_helper(&image, Some(PixelFormat::Rgba), None).unwrap();
12787
12788 let mut dst =
12790 TensorDyn::new(&[640, 640, 3], DType::U8, Some(TensorMemory::Mem), None).unwrap();
12791 dst.set_format(PixelFormat::Rgb).unwrap();
12792
12793 processor.convert(&src, &mut dst, None).unwrap();
12795
12796 assert_eq!(dst.format(), Some(PixelFormat::Rgb));
12798 assert_eq!(dst.width(), Some(640));
12799 assert_eq!(dst.height(), Some(640));
12800 }
12801
12802 #[test]
12808 fn test_multiple_image_processors_same_thread() {
12809 let _lock = acquire_env_lock();
12812 let mut processors: Vec<ImageProcessor> = (0..4)
12813 .map(|_| ImageProcessor::new().expect("ImageProcessor::new() failed"))
12814 .collect();
12815
12816 for proc in &mut processors {
12817 let src = proc
12818 .create_image(
12819 128,
12820 128,
12821 PixelFormat::Rgb,
12822 DType::U8,
12823 None,
12824 edgefirst_tensor::CpuAccess::ReadWrite,
12825 )
12826 .expect("create src failed");
12827 let mut dst = proc
12828 .create_image(
12829 64,
12830 64,
12831 PixelFormat::Rgb,
12832 DType::U8,
12833 None,
12834 edgefirst_tensor::CpuAccess::ReadWrite,
12835 )
12836 .expect("create dst failed");
12837 proc.convert(&src, &mut dst, None).expect("convert failed");
12838 assert_eq!(dst.width(), Some(64));
12839 assert_eq!(dst.height(), Some(64));
12840 }
12841 }
12842
12843 #[test]
12850 fn test_multiple_image_processors_separate_threads() {
12851 use std::sync::mpsc;
12852 use std::time::Duration;
12853
12854 if std::env::var_os("EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS").is_some() {
12865 crate::test_support::report_skip(
12866 "test_multiple_image_processors_separate_threads — known Vivante \
12867 GC7000UL concurrent-EGL-teardown double-free \
12868 (EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS set)",
12869 );
12870 return;
12871 }
12872
12873 const TIMEOUT: Duration = Duration::from_secs(60);
12874
12875 let _lock = acquire_env_lock();
12878
12879 let (tx, rx) = mpsc::channel::<()>();
12880
12881 std::thread::spawn(move || {
12882 let handles: Vec<_> = (0..4)
12883 .map(|i| {
12884 std::thread::spawn(move || {
12885 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
12886 panic!("ImageProcessor::new() failed on thread {i}: {e}")
12887 });
12888 let src = proc
12889 .create_image(
12890 128,
12891 128,
12892 PixelFormat::Rgb,
12893 DType::U8,
12894 None,
12895 edgefirst_tensor::CpuAccess::ReadWrite,
12896 )
12897 .unwrap_or_else(|e| panic!("create src failed on thread {i}: {e}"));
12898 let mut dst = proc
12899 .create_image(
12900 64,
12901 64,
12902 PixelFormat::Rgb,
12903 DType::U8,
12904 None,
12905 edgefirst_tensor::CpuAccess::ReadWrite,
12906 )
12907 .unwrap_or_else(|e| panic!("create dst failed on thread {i}: {e}"));
12908 proc.convert(&src, &mut dst, None)
12909 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
12910 assert_eq!(dst.width(), Some(64));
12911 assert_eq!(dst.height(), Some(64));
12912 })
12913 })
12914 .collect();
12915
12916 for (i, h) in handles.into_iter().enumerate() {
12917 h.join()
12918 .unwrap_or_else(|e| panic!("thread {i} panicked: {e:?}"));
12919 }
12920
12921 let _ = tx.send(());
12922 });
12923
12924 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
12925 panic!("test_multiple_image_processors_separate_threads timed out after {TIMEOUT:?}")
12926 });
12927 }
12928
12929 #[test]
12936 fn test_image_processors_concurrent_operations() {
12937 use std::sync::{mpsc, Arc, Barrier};
12938 use std::time::Duration;
12939
12940 const N: usize = 4;
12941 const ROUNDS: usize = 10;
12942 const TIMEOUT: Duration = Duration::from_secs(60);
12943
12944 let _lock = acquire_env_lock();
12947
12948 let (tx, rx) = mpsc::channel::<()>();
12949
12950 std::thread::spawn(move || {
12951 let barrier = Arc::new(Barrier::new(N));
12952
12953 let handles: Vec<_> = (0..N)
12954 .map(|i| {
12955 let barrier = Arc::clone(&barrier);
12956 std::thread::spawn(move || {
12957 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
12958 panic!("ImageProcessor::new() failed on thread {i}: {e}")
12959 });
12960
12961 barrier.wait();
12963
12964 for round in 0..ROUNDS {
12966 let src = proc
12967 .create_image(
12968 128,
12969 128,
12970 PixelFormat::Rgb,
12971 DType::U8,
12972 None,
12973 edgefirst_tensor::CpuAccess::ReadWrite,
12974 )
12975 .unwrap_or_else(|e| {
12976 panic!("create src failed on thread {i} round {round}: {e}")
12977 });
12978 let mut dst = proc
12979 .create_image(
12980 64,
12981 64,
12982 PixelFormat::Rgb,
12983 DType::U8,
12984 None,
12985 edgefirst_tensor::CpuAccess::ReadWrite,
12986 )
12987 .unwrap_or_else(|e| {
12988 panic!("create dst failed on thread {i} round {round}: {e}")
12989 });
12990 proc.convert(&src, &mut dst, None).unwrap_or_else(|e| {
12991 panic!("convert failed on thread {i} round {round}: {e}")
12992 });
12993 assert_eq!(dst.width(), Some(64));
12994 assert_eq!(dst.height(), Some(64));
12995 }
12996 })
12997 })
12998 .collect();
12999
13000 for (i, h) in handles.into_iter().enumerate() {
13001 h.join()
13002 .unwrap_or_else(|e| panic!("thread {i} panicked: {e:?}"));
13003 }
13004
13005 let _ = tx.send(());
13006 });
13007
13008 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
13009 panic!("test_image_processors_concurrent_operations timed out after {TIMEOUT:?}")
13010 });
13011 }
13012
13013 #[test]
13031 fn test_parallel_processors_unique_outputs() {
13032 use std::sync::{mpsc, Arc, Barrier};
13033 use std::time::Duration;
13034
13035 const N: usize = 4;
13036 const ROUNDS: usize = 25;
13037 const TIMEOUT: Duration = Duration::from_secs(60);
13038
13039 if std::env::var_os("EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS").is_some() {
13040 crate::test_support::report_skip(
13041 "test_parallel_processors_unique_outputs — known Vivante \
13042 GC7000UL concurrent-multi-processor driver abort \
13043 (EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS set)",
13044 );
13045 return;
13046 }
13047
13048 let _lock = acquire_env_lock();
13049 let use_dma = crate::test_support::is_dma_available();
13051 if !use_dma {
13052 crate::test_support::report_skip(&format!(
13053 "{} - DmaBuf sources not exercised, running on Mem: no usable DMA heap",
13054 function!()
13055 ));
13056 }
13057 let (tx, rx) = mpsc::channel::<()>();
13058
13059 std::thread::spawn(move || {
13060 let barrier = Arc::new(Barrier::new(N));
13061 let handles: Vec<_> = (0..N)
13062 .map(|i| {
13063 let barrier = Arc::clone(&barrier);
13064 std::thread::spawn(move || {
13065 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
13066 panic!("ImageProcessor::new() failed on thread {i}: {e}")
13067 });
13068 let (w, h) = (640usize, 480usize);
13070 let mem = if use_dma {
13071 Some(TensorMemory::DmaBuf)
13072 } else {
13073 Some(TensorMemory::Mem)
13074 };
13075 let src = proc
13076 .create_image(
13077 w,
13078 h,
13079 PixelFormat::Nv12,
13080 DType::U8,
13081 mem,
13082 edgefirst_tensor::CpuAccess::ReadWrite,
13083 )
13084 .unwrap();
13085 {
13086 let t = src.as_typed::<u8>().unwrap();
13087 let mut m = t.map().unwrap();
13088 let s = m.as_mut_slice();
13089 for (j, b) in s[..w * h].iter_mut().enumerate() {
13090 *b = ((i * 53 + j) % 200 + 16) as u8;
13091 }
13092 for b in &mut s[w * h..] {
13093 *b = (80 + i * 24) as u8;
13094 }
13095 }
13096 let lb = Crop::letterbox([114, 114, 114, 255]);
13097 let convert_once = |proc: &mut ImageProcessor| -> Vec<u8> {
13098 let mut dst = proc
13099 .create_image(
13100 320,
13101 320,
13102 PixelFormat::Rgba,
13103 DType::U8,
13104 mem,
13105 edgefirst_tensor::CpuAccess::ReadWrite,
13106 )
13107 .unwrap();
13108 proc.convert(
13109 &src,
13110 &mut dst,
13111 Some(crate::ConvertOptions::new().crop(lb)),
13112 )
13113 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
13114 let t = dst.as_typed::<u8>().unwrap();
13115 let m = t.map().unwrap();
13116 m.as_slice().to_vec()
13117 };
13118
13119 let oracle = convert_once(&mut proc);
13120 barrier.wait();
13121 for round in 0..ROUNDS {
13122 let out = convert_once(&mut proc);
13123 let diffs = oracle.iter().zip(&out).filter(|(a, b)| a != b).count();
13124 assert!(
13125 diffs == 0,
13126 "thread {i} round {round}: {diffs}/{} bytes diverged \
13127 from this processor's own oracle — cross-processor \
13128 GL state leakage under parallel execution",
13129 oracle.len()
13130 );
13131 }
13132 })
13133 })
13134 .collect();
13135
13136 for (i, h) in handles.into_iter().enumerate() {
13137 h.join()
13138 .unwrap_or_else(|e| panic!("parallel thread {i} panicked: {e:?}"));
13139 }
13140 let _ = tx.send(());
13141 });
13142
13143 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
13144 panic!("test_parallel_processors_unique_outputs timed out after {TIMEOUT:?}")
13145 });
13146 }
13147
13148 #[test]
13157 #[ignore = "heavy on-demand GL-parallelism stressor; run explicitly on boards"]
13158 fn stress_parallel_processors_oracle() {
13159 use std::sync::{mpsc, Arc, Barrier};
13160 use std::time::Duration;
13161
13162 const N: usize = 4;
13163 const ROUNDS: usize = 200;
13164 const TIMEOUT: Duration = Duration::from_secs(600);
13165
13166 let _lock = acquire_env_lock();
13167 let use_dma = crate::test_support::is_dma_available();
13169 if !use_dma {
13170 crate::test_support::report_skip(&format!(
13171 "{} - DmaBuf sources not exercised, running on Mem: no usable DMA heap",
13172 function!()
13173 ));
13174 }
13175 let (tx, rx) = mpsc::channel::<()>();
13176
13177 std::thread::spawn(move || {
13178 let barrier = Arc::new(Barrier::new(N));
13179 let handles: Vec<_> = (0..N)
13180 .map(|i| {
13181 let barrier = Arc::clone(&barrier);
13182 std::thread::spawn(move || {
13183 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
13184 panic!("ImageProcessor::new() failed on thread {i}: {e}")
13185 });
13186 let (w, h) = (1280usize, 720usize);
13187 let mem = if use_dma {
13188 Some(TensorMemory::DmaBuf)
13189 } else {
13190 Some(TensorMemory::Mem)
13191 };
13192
13193 let src = proc
13196 .create_image(
13197 w,
13198 h,
13199 PixelFormat::Nv12,
13200 DType::U8,
13201 mem,
13202 edgefirst_tensor::CpuAccess::ReadWrite,
13203 )
13204 .unwrap();
13205 {
13206 let t = src.as_typed::<u8>().unwrap();
13207 let mut m = t.map().unwrap();
13208 let s = m.as_mut_slice();
13209 for (j, b) in s[..w * h].iter_mut().enumerate() {
13210 *b = ((i * 37 + j) % 200 + 16) as u8;
13211 }
13212 for b in &mut s[w * h..] {
13213 *b = (96 + i * 16) as u8;
13214 }
13215 }
13216 let lb = Crop::letterbox([114, 114, 114, 255]);
13217
13218 let convert_once = |proc: &mut ImageProcessor| -> Vec<u8> {
13219 let mut dst = proc
13220 .create_image(
13221 640,
13222 640,
13223 PixelFormat::Rgb,
13224 DType::U8,
13225 mem,
13226 edgefirst_tensor::CpuAccess::ReadWrite,
13227 )
13228 .unwrap();
13229 proc.convert(
13230 &src,
13231 &mut dst,
13232 Some(crate::ConvertOptions::new().crop(lb)),
13233 )
13234 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
13235 let t = dst.as_typed::<u8>().unwrap();
13236 let m = t.map().unwrap();
13237 m.as_slice().to_vec()
13238 };
13239
13240 let oracle = convert_once(&mut proc);
13241 barrier.wait();
13242 for round in 0..ROUNDS {
13243 let out = convert_once(&mut proc);
13244 let diffs = oracle.iter().zip(&out).filter(|(a, b)| a != b).count();
13245 assert!(
13246 diffs == 0,
13247 "thread {i} round {round}: {diffs}/{} bytes diverged \
13248 from the pre-barrier oracle",
13249 oracle.len()
13250 );
13251 }
13252 })
13253 })
13254 .collect();
13255
13256 for (i, h) in handles.into_iter().enumerate() {
13257 h.join()
13258 .unwrap_or_else(|e| panic!("stressor thread {i} panicked: {e:?}"));
13259 }
13260 let _ = tx.send(());
13261 });
13262
13263 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
13264 panic!("stress_parallel_processors_oracle timed out after {TIMEOUT:?}")
13265 });
13266 }
13267
13268 #[test]
13281 fn convert_f32_auto_never_errors_non_gl_combo() {
13282 const W: usize = 64;
13283 const H: usize = 64;
13284
13285 let src = TensorDyn::image(
13288 W,
13289 H,
13290 PixelFormat::Yuyv,
13291 DType::U8,
13292 Some(TensorMemory::Mem),
13293 edgefirst_tensor::CpuAccess::ReadWrite,
13294 )
13295 .unwrap();
13296 {
13297 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13298 let data = map.as_mut_slice();
13299 for chunk in data.as_chunks_mut::<4>().0 {
13300 chunk[0] = 128; chunk[1] = 128; chunk[2] = 160; chunk[3] = 128; }
13305 }
13306
13307 let mut dst = TensorDyn::image(
13308 W,
13309 H,
13310 PixelFormat::Rgb,
13311 DType::F32,
13312 Some(TensorMemory::Mem),
13313 edgefirst_tensor::CpuAccess::ReadWrite,
13314 )
13315 .unwrap();
13316
13317 let mut proc = ImageProcessor::new().unwrap();
13318 let result = proc.convert(&src, &mut dst, None);
13319 assert!(
13320 result.is_ok(),
13321 "auto-chain Yuyv→Rgb F32 must not error: {:?}",
13322 result.err()
13323 );
13324
13325 let map = dst.as_typed::<f32>().unwrap().map().unwrap();
13327 let floats = map.as_slice();
13328 assert_eq!(floats.len(), W * H * 3, "unexpected output element count");
13329 for (i, &v) in floats.iter().enumerate() {
13330 assert!(
13331 v.is_finite() && (0.0..=1.0).contains(&v),
13332 "output[{i}]={v} is not finite or not in [0,1]"
13333 );
13334 }
13335
13336 let first_non_zero = floats.iter().find(|&&v| v > 0.01);
13340 assert!(
13341 first_non_zero.is_some(),
13342 "all-zero output detected — CPU path likely did not write to the destination buffer"
13343 );
13344 let r0 = floats[0];
13347 assert!(
13348 (r0 - 0.502_f32).abs() < 0.05,
13349 "first pixel R={r0} expected ≈0.502 (Y=128 neutral grey from YUYV source)"
13350 );
13351 }
13352
13353 #[test]
13359 #[allow(clippy::needless_update)]
13365 fn convert_f16_forced_cpu_correct() {
13366 const W: usize = 16;
13367 const H: usize = 16;
13368 const TOL: f32 = 1.0 / 512.0; let src = TensorDyn::image(
13372 W,
13373 H,
13374 PixelFormat::Rgba,
13375 DType::U8,
13376 Some(TensorMemory::Mem),
13377 edgefirst_tensor::CpuAccess::ReadWrite,
13378 )
13379 .unwrap();
13380 {
13381 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13382 let data = map.as_mut_slice();
13383 for y in 0..H {
13384 for x in 0..W {
13385 let i = y * W + x;
13386 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;
13390 }
13391 }
13392 }
13393
13394 let mut dst = TensorDyn::image(
13395 W,
13396 H,
13397 PixelFormat::PlanarRgb,
13398 DType::F16,
13399 Some(TensorMemory::Mem),
13400 edgefirst_tensor::CpuAccess::ReadWrite,
13401 )
13402 .unwrap();
13403
13404 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
13405 backend: ComputeBackend::Cpu,
13406 ..Default::default()
13407 })
13408 .unwrap();
13409 proc.convert(&src, &mut dst, None)
13410 .expect("forced-CPU Rgba→PlanarRgb F16 must not error");
13411
13412 let src_map = src.as_typed::<u8>().unwrap().map().unwrap();
13413 let src_bytes = src_map.as_slice();
13414 let dst_map = dst.as_typed::<half::f16>().unwrap().map().unwrap();
13415 let dst_halfs = dst_map.as_slice();
13416
13417 let plane = W * H;
13418 assert_eq!(dst_halfs.len(), plane * 3, "wrong output element count");
13419
13420 for y in 0..H {
13421 for x in 0..W {
13422 let i = y * W + x;
13423 let r_exp = src_bytes[i * 4] as f32 / 255.0;
13424 let g_exp = src_bytes[i * 4 + 1] as f32 / 255.0;
13425 let b_exp = src_bytes[i * 4 + 2] as f32 / 255.0;
13426
13427 let r_got = dst_halfs[i].to_f32();
13428 let g_got = dst_halfs[plane + i].to_f32();
13429 let b_got = dst_halfs[2 * plane + i].to_f32();
13430
13431 assert!(
13432 (r_got - r_exp).abs() <= TOL,
13433 "R plane ({x},{y}): got {r_got}, expected {r_exp}"
13434 );
13435 assert!(
13436 (g_got - g_exp).abs() <= TOL,
13437 "G plane ({x},{y}): got {g_got}, expected {g_exp}"
13438 );
13439 assert!(
13440 (b_got - b_exp).abs() <= TOL,
13441 "B plane ({x},{y}): got {b_got}, expected {b_exp}"
13442 );
13443
13444 if src_bytes[i * 4] != src_bytes[i * 4 + 1] {
13446 assert_ne!(r_got, g_got, "R and G planes must differ at ({x},{y})");
13447 }
13448 }
13449 }
13450 }
13451
13452 #[test]
13460 fn convert_f32_with_rotation_falls_back() {
13461 const W: usize = 16;
13462 const H: usize = 16;
13463
13464 let src = TensorDyn::image(
13466 W,
13467 H,
13468 PixelFormat::Rgba,
13469 DType::U8,
13470 Some(TensorMemory::Mem),
13471 edgefirst_tensor::CpuAccess::ReadWrite,
13472 )
13473 .unwrap();
13474 {
13475 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13476 let data = map.as_mut_slice();
13477 for y in 0..H {
13478 for x in 0..W {
13479 let i = y * W + x;
13480 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;
13484 }
13485 }
13486 }
13487
13488 let mut dst = TensorDyn::image(
13490 H, W, PixelFormat::Rgb,
13493 DType::F32,
13494 Some(TensorMemory::Mem),
13495 edgefirst_tensor::CpuAccess::ReadWrite,
13496 )
13497 .unwrap();
13498
13499 let mut proc = ImageProcessor::new().unwrap();
13500 let result = proc.convert(
13501 &src,
13502 &mut dst,
13503 Some(crate::ConvertOptions::new().rotation(Rotation::Clockwise90)),
13504 );
13505 assert!(
13506 result.is_ok(),
13507 "auto-chain Rgba→Rgb F32 with Rot90 must not error: {:?}",
13508 result.err()
13509 );
13510
13511 let map = dst.as_typed::<f32>().unwrap().map().unwrap();
13512 let floats = map.as_slice();
13513 assert_eq!(floats.len(), H * W * 3, "unexpected output element count");
13514 for (i, &v) in floats.iter().enumerate() {
13515 assert!(
13516 v.is_finite() && (0.0..=1.0).contains(&v),
13517 "output[{i}]={v} is not finite or not in [0,1]"
13518 );
13519 }
13520 }
13521
13522 #[test]
13548 #[cfg(all(
13549 any(
13550 target_os = "linux",
13551 target_os = "macos",
13552 target_os = "ios",
13553 target_os = "android",
13554 target_os = "windows"
13555 ),
13556 feature = "opengl"
13557 ))]
13558 fn convert_f16_gl_cpu_parity_identity() {
13559 #[cfg(target_os = "macos")]
13565 if std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1")
13566 && std::env::var_os("HAL_TEST_ALLOW_DLOPEN_ANGLE").is_none()
13567 {
13568 crate::test_support::report_skip(
13569 "convert_f16_gl_cpu_parity_identity - ANGLE dlopen gate closed (coverage pass 1)",
13570 );
13571 return;
13572 }
13573 if !is_opengl_available() {
13574 crate::test_support::report_skip(
13575 "convert_f16_gl_cpu_parity_identity - OpenGL not available",
13576 );
13577 return;
13578 }
13579
13580 const W: usize = 32;
13582 const H: usize = 16;
13583 const TOL: f32 = 1.0 / 256.0; let src = TensorDyn::image(
13587 W,
13588 H,
13589 PixelFormat::Rgba,
13590 DType::U8,
13591 Some(TensorMemory::Mem),
13592 edgefirst_tensor::CpuAccess::ReadWrite,
13593 )
13594 .unwrap();
13595 {
13596 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13597 let data = map.as_mut_slice();
13598 for y in 0..H {
13599 for x in 0..W {
13600 let i = y * W + x;
13601 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;
13605 }
13606 }
13607 }
13608
13609 let gl_result = {
13611 let mut gl_proc = match ImageProcessor::with_config(ImageProcessorConfig {
13612 backend: ComputeBackend::OpenGl,
13613 ..Default::default()
13614 }) {
13615 Ok(p) => p,
13616 Err(e) => {
13617 crate::test_support::report_skip(&format!(
13618 "convert_f16_gl_cpu_parity_identity - GL backend unavailable: {e}"
13619 ));
13620 return;
13621 }
13622 };
13623
13624 if !gl_proc.supported_render_dtypes().f16 {
13625 crate::test_support::report_skip(
13626 "convert_f16_gl_cpu_parity_identity - F16 render not supported",
13627 );
13628 return;
13629 }
13630
13631 let Some(mut dst) = crate::test_support::gpu_buffer_allocated(
13652 TensorDyn::image(
13653 W,
13654 H,
13655 PixelFormat::PlanarRgb,
13656 DType::F16,
13657 Some(TensorMemory::DmaBuf),
13658 edgefirst_tensor::CpuAccess::ReadWrite,
13659 ),
13660 "zero-copy F16 PlanarRgb destination",
13661 ) else {
13662 return;
13663 };
13664 if dst.memory() != TensorMemory::DmaBuf {
13665 crate::test_support::report_skip(&format!(
13666 "convert_f16_gl_cpu_parity_identity - the zero-copy F16 \
13667 destination fell back to {:?}",
13668 dst.memory()
13669 ));
13670 return;
13671 }
13672 let require_gl = std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1");
13673 let fallbacks_before = gl_proc.convert_fallback_count();
13674 match gl_proc.convert(&src, &mut dst, None) {
13675 Ok(()) => {
13676 if gl_proc.convert_fallback_count() != fallbacks_before {
13677 assert!(
13678 !require_gl,
13679 "HAL_TEST_REQUIRE_GL=1 but \
13680 convert_f16_gl_cpu_parity_identity would have skipped \
13681 while reporting success: the GL F16 path declined and \
13682 the CPU served the convert"
13683 );
13684 crate::test_support::report_skip(
13685 "convert_f16_gl_cpu_parity_identity - the GL F16 path \
13686 declined and the CPU served the convert; comparing that \
13687 against the forced-CPU result would prove nothing",
13688 );
13689 return;
13690 }
13691 dst
13692 }
13693 Err(e) => {
13694 crate::test_support::report_skip(&format!(
13695 "convert_f16_gl_cpu_parity_identity - GL convert failed: {e}"
13696 ));
13697 return;
13698 }
13699 }
13700 };
13701
13702 let cpu_result = {
13704 let mut cpu_proc = ImageProcessor::with_config(ImageProcessorConfig {
13705 backend: ComputeBackend::Cpu,
13706 ..Default::default()
13707 })
13708 .unwrap();
13709 let mut dst = TensorDyn::image(
13710 W,
13711 H,
13712 PixelFormat::PlanarRgb,
13713 DType::F16,
13714 Some(TensorMemory::Mem),
13715 edgefirst_tensor::CpuAccess::ReadWrite,
13716 )
13717 .unwrap();
13718 cpu_proc
13719 .convert(&src, &mut dst, None)
13720 .expect("forced-CPU Rgba→PlanarRgb F16 must not error");
13721 dst
13722 };
13723
13724 let flat_f16 = |t: &TensorDyn| -> Vec<half::f16> {
13729 let typed = t.as_typed::<half::f16>().expect("F16 image");
13730 let mut bytes = vec![0u8; typed.shape().iter().product::<usize>() * 2];
13731 typed
13732 .copy_to_flat(&mut bytes)
13733 .expect("compact any padded rows");
13734 bytes
13735 .as_chunks::<2>()
13736 .0
13737 .iter()
13738 .map(|&c| half::f16::from_le_bytes(c))
13739 .collect()
13740 };
13741 let gl_halfs = flat_f16(&gl_result);
13742 let cpu_halfs = flat_f16(&cpu_result);
13743
13744 assert_eq!(
13745 gl_halfs.len(),
13746 cpu_halfs.len(),
13747 "GL and CPU output sizes differ"
13748 );
13749
13750 let plane = W * H;
13751 let channel_names = ["R", "G", "B"];
13752 for (idx, (gl_h, cpu_h)) in gl_halfs.iter().zip(cpu_halfs.iter()).enumerate() {
13753 let gl_v = gl_h.to_f32();
13754 let cpu_v = cpu_h.to_f32();
13755 let err = (gl_v - cpu_v).abs();
13756 let ch = channel_names[idx / plane];
13757 let pixel = idx % plane;
13758 assert!(
13759 err <= TOL,
13760 "GL vs CPU mismatch at {ch}[{pixel}]: GL={gl_v}, CPU={cpu_v}, err={err} > tol={TOL}"
13761 );
13762 }
13763 }
13764
13765 #[test]
13773 #[cfg(all(any(target_os = "linux", target_os = "windows"), feature = "opengl"))]
13774 fn supported_render_dtypes_linux_smoke() {
13775 let proc = match ImageProcessor::new() {
13776 Ok(p) => p,
13777 Err(e) => {
13778 crate::test_support::report_skip(&format!(
13779 "supported_render_dtypes_linux_smoke — ImageProcessor::new() failed: {e}"
13780 ));
13781 return;
13782 }
13783 };
13784 if proc.opengl.is_none() {
13785 crate::test_support::report_skip(
13786 "supported_render_dtypes_linux_smoke — no GL backend on this host",
13787 );
13788 return;
13789 }
13790 let support = proc.supported_render_dtypes();
13792 eprintln!(
13793 "supported_render_dtypes_linux_smoke: f16={} f32={}",
13794 support.f16, support.f32
13795 );
13796 }
13798
13799 #[test]
13808 fn convert_f16_pbo_non_4_aligned_width_falls_back() {
13809 const W: usize = 18; const H: usize = 16;
13811
13812 let src = TensorDyn::image(
13814 W,
13815 H,
13816 PixelFormat::Rgba,
13817 DType::U8,
13818 Some(TensorMemory::Mem),
13819 edgefirst_tensor::CpuAccess::ReadWrite,
13820 )
13821 .unwrap();
13822 {
13823 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13824 let data = map.as_mut_slice();
13825 for chunk in data.as_chunks_mut::<4>().0 {
13826 chunk[0] = 128;
13827 chunk[1] = 64;
13828 chunk[2] = 200;
13829 chunk[3] = 255;
13830 }
13831 }
13832
13833 let mut dst = TensorDyn::image(
13836 W,
13837 H,
13838 PixelFormat::PlanarRgb,
13839 DType::F16,
13840 Some(TensorMemory::Mem),
13841 edgefirst_tensor::CpuAccess::ReadWrite,
13842 )
13843 .unwrap();
13844
13845 let mut proc = ImageProcessor::new().unwrap();
13848 let result = proc.convert(&src, &mut dst, None);
13849 assert!(
13850 result.is_ok(),
13851 "auto-chain PlanarRgb F16 W%4!=0 must not error (CPU fallback): {:?}",
13852 result.err()
13853 );
13854
13855 let map = dst.as_typed::<half::f16>().unwrap().map().unwrap();
13857 let halfs = map.as_slice();
13858 assert_eq!(halfs.len(), W * H * 3, "unexpected element count");
13859 for (i, h) in halfs.iter().enumerate() {
13860 let v = h.to_f32();
13861 assert!(
13862 v.is_finite() && (0.0..=1.0).contains(&v),
13863 "output[{i}]={v} is not finite or not in [0,1]"
13864 );
13865 }
13866 }
13867
13868 #[test]
13878 #[allow(clippy::needless_update)]
13881 fn convert_nv12_to_rgb_f32_cpu() {
13882 const W: usize = 16;
13883 const H: usize = 16; let src = TensorDyn::image(
13887 W,
13888 H,
13889 PixelFormat::Nv12,
13890 DType::U8,
13891 Some(TensorMemory::Mem),
13892 edgefirst_tensor::CpuAccess::ReadWrite,
13893 )
13894 .unwrap();
13895 {
13896 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13897 map.as_mut_slice().fill(128); }
13899
13900 let mut dst = TensorDyn::image(
13901 W,
13902 H,
13903 PixelFormat::Rgb,
13904 DType::F32,
13905 Some(TensorMemory::Mem),
13906 edgefirst_tensor::CpuAccess::ReadWrite,
13907 )
13908 .unwrap();
13909
13910 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
13911 backend: ComputeBackend::Cpu,
13912 ..Default::default()
13913 })
13914 .unwrap();
13915
13916 let result = proc.convert(&src, &mut dst, None);
13917 assert!(
13918 result.is_ok(),
13919 "forced-CPU NV12→Rgb F32 must not error: {:?}",
13920 result.err()
13921 );
13922
13923 let map = dst.as_typed::<f32>().unwrap().map().unwrap();
13924 let floats = map.as_slice();
13925 assert_eq!(floats.len(), W * H * 3, "unexpected element count");
13926 for (i, &v) in floats.iter().enumerate() {
13927 assert!(
13928 v.is_finite() && (0.0..=1.0).contains(&v),
13929 "output[{i}]={v} is not finite or not in [0,1]"
13930 );
13931 }
13932 let non_zero = floats.iter().any(|&v| v > 0.01);
13934 assert!(non_zero, "all-zero output from NV12→Rgb F32 CPU path");
13935 }
13936
13937 #[test]
13941 #[allow(clippy::needless_update)]
13944 fn convert_nv12_to_planar_rgb_f16_cpu() {
13945 const W: usize = 16;
13946 const H: usize = 16;
13947
13948 let src = TensorDyn::image(
13949 W,
13950 H,
13951 PixelFormat::Nv12,
13952 DType::U8,
13953 Some(TensorMemory::Mem),
13954 edgefirst_tensor::CpuAccess::ReadWrite,
13955 )
13956 .unwrap();
13957 {
13958 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13959 map.as_mut_slice().fill(128);
13960 }
13961
13962 let mut dst = TensorDyn::image(
13963 W,
13964 H,
13965 PixelFormat::PlanarRgb,
13966 DType::F16,
13967 Some(TensorMemory::Mem),
13968 edgefirst_tensor::CpuAccess::ReadWrite,
13969 )
13970 .unwrap();
13971
13972 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
13973 backend: ComputeBackend::Cpu,
13974 ..Default::default()
13975 })
13976 .unwrap();
13977
13978 let result = proc.convert(&src, &mut dst, None);
13979 assert!(
13980 result.is_ok(),
13981 "forced-CPU NV12→PlanarRgb F16 must not error: {:?}",
13982 result.err()
13983 );
13984
13985 let map = dst.as_typed::<half::f16>().unwrap().map().unwrap();
13986 let halfs = map.as_slice();
13987 assert_eq!(halfs.len(), W * H * 3, "unexpected element count");
13988 for (i, h) in halfs.iter().enumerate() {
13989 let v = h.to_f32();
13990 assert!(
13991 v.is_finite() && (0.0..=1.0).contains(&v),
13992 "output[{i}]={v} is not finite or not in [0,1]"
13993 );
13994 }
13995 let non_zero = halfs.iter().any(|h| h.to_f32() > 0.01);
13996 assert!(non_zero, "all-zero output from NV12→PlanarRgb F16 CPU path");
13997 }
13998
13999 fn access_test_src(w: usize, h: usize) -> TensorDyn {
14004 use edgefirst_tensor::TensorMapTrait;
14005 let t = TensorDyn::image(
14006 w,
14007 h,
14008 PixelFormat::Rgba,
14009 DType::U8,
14010 Some(TensorMemory::Mem),
14011 edgefirst_tensor::CpuAccess::ReadWrite,
14012 )
14013 .unwrap();
14014 for (i, b) in t
14015 .as_u8()
14016 .unwrap()
14017 .map_mut()
14018 .unwrap()
14019 .as_mut_slice()
14020 .iter_mut()
14021 .enumerate()
14022 {
14023 *b = (i % 251) as u8;
14024 }
14025 t
14026 }
14027
14028 #[test]
14032 fn create_image_adds_readwrite_for_read_modify_write_passes() {
14033 use edgefirst_tensor::CpuAccess;
14034 let _lock = acquire_env_lock();
14035 let _guard = EnvGuard::snapshot(&[
14036 "EDGEFIRST_FORCE_BACKEND",
14037 "EDGEFIRST_DISABLE_GL",
14038 "EDGEFIRST_DISABLE_G2D",
14039 "EDGEFIRST_DISABLE_CPU",
14040 ]);
14041 unsafe {
14042 std::env::remove_var("EDGEFIRST_FORCE_BACKEND");
14043 std::env::remove_var("EDGEFIRST_DISABLE_CPU");
14044 std::env::set_var("EDGEFIRST_DISABLE_GL", "1");
14045 std::env::set_var("EDGEFIRST_DISABLE_G2D", "1");
14046 }
14047 let mut proc = ImageProcessor::new().unwrap();
14048 let src = access_test_src(128, 96);
14049 for (format, dtype) in [
14050 (PixelFormat::Rgb, DType::I8),
14051 (PixelFormat::Rgba, DType::I8),
14052 (PixelFormat::Bgra, DType::U8),
14053 ] {
14054 for requested in [CpuAccess::None, CpuAccess::Write] {
14055 let mut dst = proc
14056 .create_image(64, 48, format, dtype, None, requested)
14057 .unwrap();
14058 assert_eq!(
14059 dst.cpu_access(),
14060 CpuAccess::ReadWrite,
14061 "{format:?}/{dtype:?} requested {requested:?}"
14062 );
14063 proc.convert(&src, &mut dst, None).unwrap_or_else(|e| {
14064 panic!("CPU convert into {format:?}/{dtype:?} {requested:?}: {e}")
14065 });
14066 }
14067 }
14068 }
14069
14070 #[test]
14074 fn cpu_convert_refuses_a_write_only_destination_it_reads_back() {
14075 use edgefirst_tensor::{CpuAccess, Error as TensorError};
14076 let mut cpu = CPUProcessor::new();
14077 let src = access_test_src(32, 24);
14078 for (format, dtype) in [
14079 (PixelFormat::Rgb, DType::I8),
14080 (PixelFormat::Bgra, DType::U8),
14081 ] {
14082 let mut dst = TensorDyn::image(
14083 32,
14084 24,
14085 format,
14086 dtype,
14087 Some(TensorMemory::Mem),
14088 CpuAccess::Write,
14089 )
14090 .unwrap();
14091 match cpu.convert(&src, &mut dst, None) {
14092 Err(Error::Tensor(TensorError::CpuAccessDenied {
14093 declared: CpuAccess::Write,
14094 requested: CpuAccess::ReadWrite,
14095 })) => {}
14096 other => panic!("{format:?}/{dtype:?}: {other:?}"),
14097 }
14098 }
14099 let bgra_src = TensorDyn::image(
14100 32,
14101 24,
14102 PixelFormat::Bgra,
14103 DType::U8,
14104 Some(TensorMemory::Mem),
14105 CpuAccess::ReadWrite,
14106 )
14107 .unwrap();
14108 let mut dst = TensorDyn::image(
14109 32,
14110 24,
14111 PixelFormat::Bgra,
14112 DType::U8,
14113 Some(TensorMemory::Mem),
14114 CpuAccess::Write,
14115 )
14116 .unwrap();
14117 cpu.convert(&bgra_src, &mut dst, None).unwrap();
14118 }
14119
14120 #[test]
14124 fn predicted_cpu_access_unions_the_backends_that_may_run() {
14125 use edgefirst_tensor::CpuAccess::{self, None as No, Read, ReadWrite, Write};
14126 let p = predicted_cpu_access;
14127 let takes = |a: CpuAccess| Some(Some(a));
14128 let declines = Some(None);
14129 assert_eq!(
14131 p(
14132 Some(ForcedBackend::G2d),
14133 None,
14134 takes(ReadWrite),
14135 takes(Write)
14136 ),
14137 ReadWrite
14138 );
14139 assert_eq!(
14140 p(Some(ForcedBackend::G2d), None, declines, takes(Write)),
14141 No
14142 );
14143 assert_eq!(
14144 p(
14145 Some(ForcedBackend::OpenGl),
14146 takes(Write),
14147 None,
14148 takes(ReadWrite)
14149 ),
14150 Write
14151 );
14152 assert_eq!(
14153 p(Some(ForcedBackend::Cpu), takes(No), None, takes(Write)),
14154 Write
14155 );
14156 assert_eq!(p(None, takes(No), takes(ReadWrite), takes(ReadWrite)), No);
14158 assert_eq!(p(None, takes(Write), None, takes(ReadWrite)), ReadWrite);
14160 assert_eq!(p(None, declines, takes(No), takes(Write)), Write);
14162 assert_eq!(p(None, declines, takes(ReadWrite), None), ReadWrite);
14163 assert_eq!(p(None, None, None, takes(Read)), Read);
14164 assert_eq!(p(None, None, None, None), No);
14165 }
14166
14167 #[cfg(all(target_os = "linux", feature = "opengl"))]
14171 #[test]
14172 fn float_destinations_gl_declines_are_predicted_for_the_cpu() {
14173 use edgefirst_tensor::CpuAccess;
14174 let Ok(processor) = ImageProcessor::new() else {
14175 eprintln!("WARNING: SKIPPED: float prediction - no image processor");
14176 return;
14177 };
14178 let Some(gl) = processor.opengl.as_ref() else {
14179 eprintln!("WARNING: SKIPPED: float prediction - no OpenGL backend");
14180 return;
14181 };
14182 assert_eq!(
14183 gl.dst_cpu_access(TensorMemory::Mem, PixelFormat::PlanarRgb, DType::F16),
14184 None
14185 );
14186 assert_eq!(
14187 gl.dst_cpu_access(TensorMemory::DmaBuf, PixelFormat::Grey, DType::F32),
14188 None
14189 );
14190 let image = processor
14191 .create_image(
14192 64,
14193 64,
14194 PixelFormat::Rgb,
14195 DType::F32,
14196 Some(TensorMemory::Mem),
14197 CpuAccess::None,
14198 )
14199 .unwrap();
14200 assert!(image.cpu_access().writes());
14201 }
14202
14203 #[test]
14206 fn a_refused_cpu_fallback_reports_the_gpu_decline() {
14207 use edgefirst_tensor::{CpuAccess, Error as TensorError};
14208 let denied = || {
14209 Error::Tensor(TensorError::CpuAccessDenied {
14210 declared: CpuAccess::None,
14211 requested: CpuAccess::Write,
14212 })
14213 };
14214 let declined = || {
14215 vec![(
14216 "opengl",
14217 Error::NotSupported("unsupported source format".into()),
14218 )]
14219 };
14220 let e = with_gpu_declines(denied(), declined());
14221 assert!(
14222 matches!(
14223 &e,
14224 Error::CpuFallbackDenied {
14225 denied: TensorError::CpuAccessDenied { .. },
14226 ..
14227 }
14228 ),
14229 "{e:?}"
14230 );
14231 let msg = e.to_string();
14232 assert!(msg.contains("unsupported source format"), "{msg}");
14233 assert!(msg.contains("CPU access denied"), "{msg}");
14234 assert!(e.cpu_access_denied().is_some());
14235 assert!(matches!(
14236 with_gpu_declines(denied(), Vec::new()),
14237 Error::Tensor(TensorError::CpuAccessDenied { .. })
14238 ));
14239 assert!(matches!(
14240 with_gpu_declines(Error::NoConverter, declined()),
14241 Error::NoConverter
14242 ));
14243 }
14244
14245 #[test]
14249 fn create_image_adds_write_when_the_cpu_backend_fills_it() {
14250 use edgefirst_tensor::CpuAccess;
14251 let _lock = acquire_env_lock();
14252 let _guard = EnvGuard::snapshot(&[
14253 "EDGEFIRST_FORCE_BACKEND",
14254 "EDGEFIRST_DISABLE_GL",
14255 "EDGEFIRST_DISABLE_G2D",
14256 "EDGEFIRST_DISABLE_CPU",
14257 ]);
14258 unsafe {
14259 std::env::remove_var("EDGEFIRST_FORCE_BACKEND");
14260 std::env::remove_var("EDGEFIRST_DISABLE_CPU");
14261 std::env::set_var("EDGEFIRST_DISABLE_GL", "1");
14262 std::env::set_var("EDGEFIRST_DISABLE_G2D", "1");
14263 }
14264 let mut proc = ImageProcessor::new().unwrap();
14265 let src = access_test_src(128, 96);
14266 for (requested, declared) in [
14267 (CpuAccess::Read, CpuAccess::ReadWrite),
14268 (CpuAccess::None, CpuAccess::Write),
14269 (CpuAccess::Write, CpuAccess::Write),
14270 (CpuAccess::ReadWrite, CpuAccess::ReadWrite),
14271 ] {
14272 let mut dst = proc
14273 .create_image(64, 48, PixelFormat::Rgb, DType::U8, None, requested)
14274 .unwrap();
14275 assert_eq!(dst.cpu_access(), declared, "requested {requested:?}");
14276 proc.convert(&src, &mut dst, None)
14277 .unwrap_or_else(|e| panic!("CPU convert into a {requested:?} request: {e}"));
14278 }
14279 }
14280
14281 #[test]
14284 fn cpu_convert_refuses_tensors_outside_their_declaration() {
14285 use edgefirst_tensor::{CpuAccess, Error as TensorError};
14286 let mut cpu = CPUProcessor::new();
14287 let src = access_test_src(32, 24);
14288 for declared in [CpuAccess::Read, CpuAccess::None] {
14289 let mut dst = TensorDyn::image(
14290 32,
14291 24,
14292 PixelFormat::Rgb,
14293 DType::U8,
14294 Some(TensorMemory::Mem),
14295 declared,
14296 )
14297 .unwrap();
14298 match cpu.convert(&src, &mut dst, None) {
14299 Err(Error::Tensor(TensorError::CpuAccessDenied {
14300 declared: d,
14301 requested: CpuAccess::Write,
14302 })) => assert_eq!(d, declared),
14303 other => panic!("convert into a {declared:?} dst: {other:?}"),
14304 }
14305 }
14306 let write_only = TensorDyn::image(
14307 32,
14308 24,
14309 PixelFormat::Rgba,
14310 DType::U8,
14311 Some(TensorMemory::Mem),
14312 CpuAccess::Write,
14313 )
14314 .unwrap();
14315 let mut dst = TensorDyn::image(
14316 32,
14317 24,
14318 PixelFormat::Rgb,
14319 DType::U8,
14320 Some(TensorMemory::Mem),
14321 CpuAccess::ReadWrite,
14322 )
14323 .unwrap();
14324 let err = cpu.convert(&write_only, &mut dst, None).unwrap_err();
14325 assert!(
14326 matches!(
14327 err,
14328 Error::Tensor(TensorError::CpuAccessDenied {
14329 declared: CpuAccess::Write,
14330 requested: CpuAccess::Read,
14331 })
14332 ),
14333 "{err:?}"
14334 );
14335 assert!(err
14336 .to_string()
14337 .contains("declare CpuAccess::ReadWrite (the default)"));
14338 }
14339
14340 #[test]
14343 fn cpu_draw_requires_readwrite_on_the_destination() {
14344 use edgefirst_tensor::{CpuAccess, Error as TensorError};
14345 let mut cpu = CPUProcessor::new();
14346 let mut dst = TensorDyn::image(
14347 32,
14348 24,
14349 PixelFormat::Rgba,
14350 DType::U8,
14351 Some(TensorMemory::Mem),
14352 CpuAccess::Write,
14353 )
14354 .unwrap();
14355 let err = cpu
14356 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::default())
14357 .unwrap_err();
14358 assert!(
14359 matches!(
14360 err,
14361 Error::Tensor(TensorError::CpuAccessDenied {
14362 declared: CpuAccess::Write,
14363 requested: CpuAccess::ReadWrite,
14364 })
14365 ),
14366 "{err:?}"
14367 );
14368 }
14369
14370 #[test]
14375 fn create_image_desc_negotiates_and_counts_fallbacks() {
14376 use edgefirst_tensor::{CpuAccess, ImageDesc};
14377 let proc = ImageProcessor::new().unwrap();
14378
14379 let desc =
14380 ImageDesc::new(64, 64, PixelFormat::Rgba, DType::U8).with_access(CpuAccess::ReadWrite);
14381 let plain = proc.create_image_desc(&desc).unwrap();
14382 let classic = proc
14383 .create_image(
14384 64,
14385 64,
14386 PixelFormat::Rgba,
14387 DType::U8,
14388 None,
14389 CpuAccess::ReadWrite,
14390 )
14391 .unwrap();
14392 assert_eq!(plain.memory(), classic.memory(), "same negotiation path");
14393 assert_eq!(plain.compression(), None);
14394
14395 #[cfg(not(target_os = "android"))]
14396 {
14397 use edgefirst_tensor::Compression;
14398 let before = proc.compression_fallback_count();
14399 let desc = ImageDesc::new(64, 64, PixelFormat::Rgba, DType::U8)
14400 .with_access(CpuAccess::None)
14401 .with_compression(Compression::Any);
14402 let t = proc.create_image_desc(&desc).unwrap();
14403 assert_eq!(t.compression(), None, "no vendor tile scheme off-Android");
14404 assert!(
14405 proc.compression_fallback_count() > before,
14406 "Any resolving linear must count"
14407 );
14408 }
14409 }
14410
14411 #[test]
14414 #[cfg(target_os = "linux")]
14415 fn create_image_desc_contiguous_is_cma_dma_or_an_error() {
14416 use edgefirst_tensor::{Contiguity, ImageDesc, TensorMemory};
14417 let proc = ImageProcessor::new().unwrap();
14418 let desc = ImageDesc::new(64, 64, PixelFormat::Rgba, DType::U8).with_contiguous(true);
14419 match proc.create_image_desc(&desc) {
14420 Ok(t) => {
14421 assert_eq!(t.memory(), TensorMemory::DmaBuf);
14422 assert_eq!(t.contiguity(), Contiguity::Contiguous);
14423 }
14424 Err(e) => {
14425 assert!(e.to_string().contains("linux,cma"), "{e}");
14426 crate::test_support::report_skip(&format!(
14427 "{} - no CMA heap, the contiguous DMA-BUF arm did not run: {e}",
14428 function!()
14429 ));
14430 }
14431 }
14432 }
14433
14434 #[test]
14437 #[cfg(target_os = "linux")]
14438 fn create_image_f32_dma_rejected() {
14439 let proc = ImageProcessor::new().unwrap();
14440 let result = proc.create_image(
14441 64,
14442 64,
14443 PixelFormat::Rgb,
14444 DType::F32,
14445 Some(TensorMemory::DmaBuf),
14446 edgefirst_tensor::CpuAccess::ReadWrite,
14447 );
14448 assert!(
14449 result.is_err(),
14450 "create_image(F32, Dma) must fail — no DRM fourcc for f32"
14451 );
14452 }
14453
14454 #[test]
14463 #[cfg(target_os = "linux")]
14464 fn import_image_carries_colorimetry() {
14465 use edgefirst_tensor::{ColorEncoding, ColorRange, Colorimetry, TensorMemory};
14466
14467 let expected = Colorimetry::default()
14468 .with_encoding(ColorEncoding::Bt709)
14469 .with_range(ColorRange::Limited);
14470
14471 if !is_dma_available() {
14472 let mut t = TensorDyn::image(
14475 8,
14476 8,
14477 PixelFormat::Rgba,
14478 DType::U8,
14479 Some(TensorMemory::Mem),
14480 edgefirst_tensor::CpuAccess::ReadWrite,
14481 )
14482 .expect("alloc");
14483 assert_eq!(t.colorimetry(), None, "colorimetry must start as None");
14484 t.set_colorimetry(Some(expected));
14485 assert_eq!(
14486 t.colorimetry(),
14487 Some(expected),
14488 "set_colorimetry must round-trip"
14489 );
14490 crate::test_support::report_skip("import_image_carries_colorimetry (DMA unavailable); storage contract verified via TensorDyn");
14491 return;
14492 }
14493
14494 use edgefirst_tensor::{PlaneDescriptor, Tensor};
14497
14498 let rgba_bytes = 64 * 64 * 4; let dma_tensor = Tensor::<u8>::new(
14500 &[rgba_bytes],
14501 Some(TensorMemory::DmaBuf),
14502 Some("import_test"),
14503 )
14504 .expect("dma alloc");
14505 let pd =
14506 PlaneDescriptor::new(dma_tensor.dmabuf().expect("dma fd")).expect("PlaneDescriptor");
14507
14508 let proc = ImageProcessor::new().expect("ImageProcessor");
14509 let result = proc.import_image(
14510 pd,
14511 None,
14512 64,
14513 64,
14514 PixelFormat::Rgba,
14515 DType::U8,
14516 Some(expected),
14517 );
14518 let tensor = result.expect("import_image must succeed on DMA fd");
14519 assert_eq!(
14520 tensor.colorimetry(),
14521 Some(expected),
14522 "import_image must store the supplied colorimetry on the returned TensorDyn"
14523 );
14524 }
14525}