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(
1031 &mut self,
1032 dst: &mut TensorDyn,
1033 detect: &[DetectBox],
1034 segmentation: &[Segmentation],
1035 overlay: MaskOverlay<'_>,
1036 ) -> Result<()>;
1037
1038 fn draw_proto_masks(
1058 &mut self,
1059 dst: &mut TensorDyn,
1060 detect: &[DetectBox],
1061 proto_data: &ProtoData,
1062 overlay: MaskOverlay<'_>,
1063 ) -> Result<()>;
1064
1065 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()>;
1073
1074 fn convert_deferred(
1091 &mut self,
1092 src: &TensorDyn,
1093 dst: &mut TensorDyn,
1094 options: Option<ConvertOptions>,
1095 ) -> Result<()> {
1096 self.convert(src, dst, options)
1097 }
1098
1099 fn flush(&mut self) -> Result<()> {
1106 Ok(())
1107 }
1108}
1109
1110#[derive(Debug, Clone, Default)]
1116pub struct ImageProcessorConfig {
1117 #[cfg(all(
1128 any(
1129 target_os = "linux",
1130 target_os = "macos",
1131 target_os = "ios",
1132 target_os = "android",
1133 target_os = "windows"
1134 ),
1135 feature = "opengl"
1136 ))]
1137 pub egl_display: Option<EglDisplayKind>,
1138
1139 pub backend: ComputeBackend,
1151
1152 pub colorimetry: ColorimetryMode,
1157
1158 pub interpolation: Interpolation,
1165
1166 pub egl_cache_capacity: Option<usize>,
1225}
1226
1227#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1242pub enum ColorimetryMode {
1243 #[default]
1247 Fast,
1248 Exact,
1251}
1252
1253pub(crate) fn runnable_interpolation(mode: Interpolation) -> Interpolation {
1257 let run = mode.supported();
1258 if run != mode && warn_first_fallback(mode) {
1259 log::warn!(
1260 "{} interpolation is not implemented; using {}",
1261 mode.as_str(),
1262 run.as_str()
1263 );
1264 }
1265 run
1266}
1267
1268static INTERPOLATION_FALLBACKS_WARNED: std::sync::atomic::AtomicU32 =
1270 std::sync::atomic::AtomicU32::new(0);
1271
1272fn warn_first_fallback(mode: Interpolation) -> bool {
1274 let bit = 1u32 << (mode.code() % 32);
1275 INTERPOLATION_FALLBACKS_WARNED.fetch_or(bit, std::sync::atomic::Ordering::Relaxed) & bit == 0
1276}
1277
1278pub(crate) fn interpolation_from_env() -> Option<Interpolation> {
1281 let value = std::env::var("EDGEFIRST_INTERPOLATION").ok()?;
1282 let value = value.trim().to_ascii_lowercase();
1283 if value.is_empty() {
1284 return None;
1285 }
1286 let mode = Interpolation::from_str_code(&value);
1287 if mode.is_none() {
1288 log::warn!(
1289 "EDGEFIRST_INTERPOLATION={value:?} is not recognised \
1290 (expected nearest or bilinear); ignoring it"
1291 );
1292 }
1293 mode
1294}
1295
1296edgefirst_tensor::ef_vocabulary! {
1297 #[derive(Default)]
1306 pub enum ComputeBackend {
1307 #[default]
1309 Auto = 0, "auto", AUTO,
1310 Cpu = 1, "cpu", CPU,
1312 G2d = 2, "g2d", G2D,
1314 OpenGl = 3, "opengl", OPENGL,
1316 }
1317 #[doc(hidden)]
1318 pub mod compute_backend_code;
1319}
1320
1321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1327pub(crate) enum ForcedBackend {
1328 Cpu,
1329 G2d,
1330 OpenGl,
1331}
1332
1333fn with_gpu_declines(e: Error, declined: Vec<(&'static str, Error)>) -> Error {
1338 match e {
1339 Error::Tensor(denied @ edgefirst_tensor::Error::CpuAccessDenied { .. })
1340 if !declined.is_empty() =>
1341 {
1342 let declined: Vec<String> = declined
1343 .iter()
1344 .map(|(backend, e)| format!("{backend}: {e}"))
1345 .collect();
1346 Error::CpuFallbackDenied {
1347 declined: declined.join("; "),
1348 denied,
1349 }
1350 }
1351 other => other,
1352 }
1353}
1354
1355fn predicted_cpu_access(
1363 forced: Option<ForcedBackend>,
1364 gl: Option<Option<edgefirst_tensor::CpuAccess>>,
1365 g2d: Option<Option<edgefirst_tensor::CpuAccess>>,
1366 cpu: Option<Option<edgefirst_tensor::CpuAccess>>,
1367) -> edgefirst_tensor::CpuAccess {
1368 use edgefirst_tensor::CpuAccess;
1369 let only = |b: Option<Option<CpuAccess>>| b.flatten().unwrap_or(CpuAccess::None);
1370 match forced {
1371 Some(ForcedBackend::Cpu) => return only(cpu),
1372 Some(ForcedBackend::G2d) => return only(g2d),
1373 Some(ForcedBackend::OpenGl) => return only(gl),
1374 None => {}
1375 }
1376 let gl = gl.flatten();
1377 if gl == Some(CpuAccess::None) {
1378 return CpuAccess::None;
1379 }
1380 [gl, g2d.flatten(), cpu.flatten()]
1381 .into_iter()
1382 .flatten()
1383 .fold(CpuAccess::None, CpuAccess::union)
1384}
1385
1386#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1411pub struct RenderDtypeSupport {
1412 pub f32: bool,
1419 pub f16: bool,
1427}
1428
1429#[cfg(all(
1444 any(
1445 target_os = "linux",
1446 target_os = "windows",
1447 target_os = "macos",
1448 target_os = "ios",
1449 target_os = "android"
1450 ),
1451 feature = "opengl"
1452))]
1453pub(crate) fn float_pbo_eligible(dtype: DType, support: RenderDtypeSupport) -> bool {
1454 match dtype {
1455 DType::F16 => support.f16,
1456 DType::F32 => support.f32,
1457 _ => false,
1458 }
1459}
1460
1461#[derive(Debug)]
1464pub struct ImageProcessor {
1465 pub cpu: Option<CPUProcessor>,
1468
1469 #[cfg(target_os = "linux")]
1470 pub g2d: Option<G2DProcessor>,
1474 #[cfg(target_os = "linux")]
1475 #[cfg(feature = "opengl")]
1476 pub opengl: Option<GLProcessorThreaded>,
1480 #[cfg(any(
1481 target_os = "macos",
1482 target_os = "ios",
1483 target_os = "android",
1484 target_os = "windows"
1485 ))]
1486 #[cfg(feature = "opengl")]
1487 pub opengl: Option<GLProcessorThreaded>,
1496
1497 pub(crate) forced_backend: Option<ForcedBackend>,
1499
1500 pub(crate) convert_fallbacks: std::sync::atomic::AtomicU64,
1506
1507 pub(crate) interpolation: Interpolation,
1509
1510 pub(crate) env_interpolation: Option<Interpolation>,
1512}
1513
1514unsafe impl Send for ImageProcessor {}
1515unsafe impl Sync for ImageProcessor {}
1516
1517impl ImageProcessor {
1518 pub fn new() -> Result<Self> {
1544 Self::with_config(ImageProcessorConfig::default())
1545 }
1546
1547 pub fn convert_fallback_count(&self) -> u64 {
1554 self.convert_fallbacks
1555 .load(std::sync::atomic::Ordering::Relaxed)
1556 }
1557
1558 pub fn compression_fallback_count(&self) -> u64 {
1567 edgefirst_tensor::compression_fallback_count()
1568 }
1569
1570 pub fn convert_with_fence(
1593 &mut self,
1594 src: &TensorDyn,
1595 dst: &mut TensorDyn,
1596 options: Option<ConvertOptions>,
1597 ) -> Result<Option<CompletionFence>> {
1598 let options = self.resolve_interpolation(options.unwrap_or_default());
1599 #[cfg(any(
1600 target_os = "linux",
1601 target_os = "macos",
1602 target_os = "ios",
1603 target_os = "android",
1604 target_os = "windows"
1605 ))]
1606 #[cfg(feature = "opengl")]
1607 {
1608 let gl_forced = matches!(self.forced_backend, Some(ForcedBackend::OpenGl));
1609 if self.forced_backend.is_none() || gl_forced {
1610 if let Some(opengl) = self.opengl.as_mut() {
1611 match opengl.convert_with_fence(src, dst, Some(options)) {
1612 Ok(fd) => return Ok(fd),
1613 Err(e) if gl_forced => return Err(e),
1614 Err(e) => {
1615 log::debug!(
1619 "convert_with_fence: opengl declined, \
1620 falling back to the blocking chain: {e}"
1621 );
1622 }
1623 }
1624 } else if gl_forced {
1625 return Err(Error::ForcedBackendUnavailable("opengl".into()));
1626 }
1627 }
1628 }
1629 self.convert(src, dst, Some(options))?;
1632 Ok(None)
1633 }
1634
1635 pub fn supported_render_dtypes(&self) -> RenderDtypeSupport {
1648 #[cfg(all(
1649 any(
1650 target_os = "macos",
1651 target_os = "ios",
1652 target_os = "android",
1653 target_os = "windows"
1654 ),
1655 feature = "opengl"
1656 ))]
1657 if let Some(gl) = self.opengl.as_ref() {
1658 return gl.supported_render_dtypes();
1659 }
1660 #[cfg(all(target_os = "linux", feature = "opengl"))]
1661 if let Some(gl) = self.opengl.as_ref() {
1662 return gl.supported_render_dtypes();
1663 }
1664 RenderDtypeSupport {
1665 f32: false,
1666 f16: false,
1667 }
1668 }
1669
1670 pub fn with_config(config: ImageProcessorConfig) -> Result<Self> {
1679 let interpolation = config.interpolation;
1680 let mut processor = Self::with_config_backends(config)?;
1681 processor.env_interpolation = interpolation_from_env().map(runnable_interpolation);
1682 processor.set_interpolation(interpolation)?;
1683 Ok(processor)
1684 }
1685
1686 #[allow(unused_variables)]
1688 fn with_config_backends(config: ImageProcessorConfig) -> Result<Self> {
1689 match config.backend {
1693 ComputeBackend::Cpu => {
1694 log::info!("ComputeBackend::Cpu — CPU only");
1695 return Ok(Self {
1696 cpu: Some(CPUProcessor::new()),
1697 #[cfg(target_os = "linux")]
1698 g2d: None,
1699 #[cfg(target_os = "linux")]
1700 #[cfg(feature = "opengl")]
1701 opengl: None,
1702 #[cfg(any(
1703 target_os = "macos",
1704 target_os = "ios",
1705 target_os = "android",
1706 target_os = "windows"
1707 ))]
1708 #[cfg(feature = "opengl")]
1709 opengl: None,
1710 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1711 interpolation: Interpolation::Bilinear,
1712 env_interpolation: None,
1713 forced_backend: None,
1714 });
1715 }
1716 ComputeBackend::G2d => {
1717 log::info!("ComputeBackend::G2d — G2D + CPU fallback");
1718 #[cfg(target_os = "linux")]
1719 {
1720 let g2d = match G2DProcessor::new() {
1721 Ok(g) => Some(g),
1722 Err(e) => {
1723 log::warn!("G2D requested but failed to initialize: {e:?}");
1724 None
1725 }
1726 };
1727 return Ok(Self {
1728 cpu: Some(CPUProcessor::new()),
1729 g2d,
1730 #[cfg(feature = "opengl")]
1731 opengl: None,
1732 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1733 interpolation: Interpolation::Bilinear,
1734 env_interpolation: None,
1735 forced_backend: None,
1736 });
1737 }
1738 #[cfg(not(target_os = "linux"))]
1739 {
1740 log::warn!("G2D requested but not available on this platform, using CPU");
1741 return Ok(Self {
1742 cpu: Some(CPUProcessor::new()),
1743 #[cfg(any(
1744 target_os = "macos",
1745 target_os = "ios",
1746 target_os = "android",
1747 target_os = "windows"
1748 ))]
1749 #[cfg(feature = "opengl")]
1750 opengl: None,
1751 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1752 interpolation: Interpolation::Bilinear,
1753 env_interpolation: None,
1754 forced_backend: None,
1755 });
1756 }
1757 }
1758 ComputeBackend::OpenGl => {
1759 log::info!("ComputeBackend::OpenGl — OpenGL + CPU fallback");
1760 #[cfg(target_os = "linux")]
1761 {
1762 #[cfg(feature = "opengl")]
1763 let opengl = match GLProcessorThreaded::with_cache_capacity(
1764 config.egl_display,
1765 config.egl_cache_capacity,
1766 ) {
1767 Ok(gl) => Some(gl),
1768 Err(e) => {
1769 log::warn!("OpenGL requested but failed to initialize: {e:?}");
1770 None
1771 }
1772 };
1773 return Ok(Self {
1774 cpu: Some(CPUProcessor::new()),
1775 g2d: None,
1776 #[cfg(feature = "opengl")]
1777 opengl,
1778 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1779 interpolation: Interpolation::Bilinear,
1780 env_interpolation: None,
1781 forced_backend: None,
1782 }
1783 .apply_colorimetry_mode(config.colorimetry));
1784 }
1785 #[cfg(any(target_os = "macos", target_os = "ios"))]
1786 {
1787 #[cfg(feature = "opengl")]
1788 let opengl = match GLProcessorThreaded::with_cache_capacity(
1789 config.egl_display,
1790 config.egl_cache_capacity,
1791 ) {
1792 Ok(gl) => Some(gl),
1793 Err(e) => {
1794 log::warn!(
1795 "OpenGL requested on macOS but ANGLE init failed: {e:?} \
1796 (install ANGLE via `brew install startergo/angle/angle` \
1797 and re-sign the dylibs — see README.md § macOS GPU \
1798 Acceleration). Falling back to CPU."
1799 );
1800 None
1801 }
1802 };
1803 return Ok(Self {
1804 cpu: Some(CPUProcessor::new()),
1805 #[cfg(feature = "opengl")]
1806 opengl,
1807 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1808 interpolation: Interpolation::Bilinear,
1809 env_interpolation: None,
1810 forced_backend: None,
1811 }
1812 .apply_colorimetry_mode(config.colorimetry));
1813 }
1814 #[cfg(target_os = "android")]
1815 {
1816 #[cfg(feature = "opengl")]
1817 let opengl = match GLProcessorThreaded::with_cache_capacity(
1818 config.egl_display,
1819 config.egl_cache_capacity,
1820 ) {
1821 Ok(gl) => Some(gl),
1822 Err(e) => {
1823 log::warn!(
1824 "OpenGL requested but native EGL init failed: {e:?}. \
1825 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 = "windows")]
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 on Windows but ANGLE/D3D11 init failed: {e:?} \
1852 (fetch ANGLE with `scripts/fetch-angle.sh` and set \
1853 EDGEFIRST_ANGLE_PATH to the directory containing libEGL.dll + \
1854 libGLESv2.dll, or place the two DLLs next to the executable — \
1855 see README.md § Windows GPU Acceleration). Falling back to CPU."
1856 );
1857 None
1858 }
1859 };
1860 return Ok(Self {
1861 cpu: Some(CPUProcessor::new()),
1862 #[cfg(feature = "opengl")]
1863 opengl,
1864 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1865 interpolation: Interpolation::Bilinear,
1866 env_interpolation: None,
1867 forced_backend: None,
1868 }
1869 .apply_colorimetry_mode(config.colorimetry));
1870 }
1871 #[cfg(not(any(
1872 target_os = "linux",
1873 target_os = "macos",
1874 target_os = "ios",
1875 target_os = "android",
1876 target_os = "windows"
1877 )))]
1878 {
1879 log::warn!("OpenGL requested but not available on this platform, using CPU");
1880 return Ok(Self {
1881 cpu: Some(CPUProcessor::new()),
1882 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1883 interpolation: Interpolation::Bilinear,
1884 env_interpolation: None,
1885 forced_backend: None,
1886 });
1887 }
1888 }
1889 ComputeBackend::Auto => { }
1890 }
1891
1892 if let Ok(val) = std::env::var("EDGEFIRST_FORCE_BACKEND") {
1897 let val_lower = val.to_lowercase();
1898 let forced = match val_lower.as_str() {
1899 "cpu" => ForcedBackend::Cpu,
1900 "g2d" => ForcedBackend::G2d,
1901 "opengl" => ForcedBackend::OpenGl,
1902 other => {
1903 return Err(Error::ForcedBackendUnavailable(format!(
1904 "unknown EDGEFIRST_FORCE_BACKEND value: {other:?} (expected cpu, g2d, or opengl)"
1905 )));
1906 }
1907 };
1908
1909 log::info!("EDGEFIRST_FORCE_BACKEND={val} — only initializing {val_lower} backend");
1910
1911 return match forced {
1912 ForcedBackend::Cpu => Ok(Self {
1913 cpu: Some(CPUProcessor::new()),
1914 #[cfg(target_os = "linux")]
1915 g2d: None,
1916 #[cfg(target_os = "linux")]
1917 #[cfg(feature = "opengl")]
1918 opengl: None,
1919 #[cfg(any(
1920 target_os = "macos",
1921 target_os = "ios",
1922 target_os = "android",
1923 target_os = "windows"
1924 ))]
1925 #[cfg(feature = "opengl")]
1926 opengl: None,
1927 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1928 interpolation: Interpolation::Bilinear,
1929 env_interpolation: None,
1930 forced_backend: Some(ForcedBackend::Cpu),
1931 }),
1932 ForcedBackend::G2d => {
1933 #[cfg(target_os = "linux")]
1934 {
1935 let g2d = G2DProcessor::new().map_err(|e| {
1936 Error::ForcedBackendUnavailable(format!(
1937 "g2d forced but failed to initialize: {e:?}"
1938 ))
1939 })?;
1940 Ok(Self {
1941 cpu: None,
1942 g2d: Some(g2d),
1943 #[cfg(feature = "opengl")]
1944 opengl: None,
1945 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1946 interpolation: Interpolation::Bilinear,
1947 env_interpolation: None,
1948 forced_backend: Some(ForcedBackend::G2d),
1949 })
1950 }
1951 #[cfg(not(target_os = "linux"))]
1952 {
1953 Err(Error::ForcedBackendUnavailable(
1954 "g2d backend is only available on Linux".into(),
1955 ))
1956 }
1957 }
1958 ForcedBackend::OpenGl => {
1959 #[cfg(target_os = "linux")]
1960 #[cfg(feature = "opengl")]
1961 {
1962 let opengl = GLProcessorThreaded::with_cache_capacity(
1963 config.egl_display,
1964 config.egl_cache_capacity,
1965 )
1966 .map_err(|e| {
1967 Error::ForcedBackendUnavailable(format!(
1968 "opengl forced but failed to initialize: {e:?}"
1969 ))
1970 })?;
1971 Ok(Self {
1972 cpu: None,
1973 g2d: None,
1974 opengl: Some(opengl),
1975 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1976 interpolation: Interpolation::Bilinear,
1977 env_interpolation: None,
1978 forced_backend: Some(ForcedBackend::OpenGl),
1979 }
1980 .apply_colorimetry_mode(config.colorimetry))
1981 }
1982 #[cfg(any(target_os = "macos", target_os = "ios"))]
1983 #[cfg(feature = "opengl")]
1984 {
1985 let opengl = GLProcessorThreaded::with_cache_capacity(
1986 config.egl_display,
1987 config.egl_cache_capacity,
1988 )
1989 .map_err(|e| {
1990 Error::ForcedBackendUnavailable(format!(
1991 "opengl forced on macOS but ANGLE init failed: {e:?}"
1992 ))
1993 })?;
1994 Ok(Self {
1995 cpu: None,
1996 opengl: Some(opengl),
1997 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1998 interpolation: Interpolation::Bilinear,
1999 env_interpolation: None,
2000 forced_backend: Some(ForcedBackend::OpenGl),
2001 }
2002 .apply_colorimetry_mode(config.colorimetry))
2003 }
2004 #[cfg(target_os = "android")]
2005 #[cfg(feature = "opengl")]
2006 {
2007 let opengl = GLProcessorThreaded::with_cache_capacity(
2008 config.egl_display,
2009 config.egl_cache_capacity,
2010 )
2011 .map_err(|e| {
2012 Error::ForcedBackendUnavailable(format!(
2013 "opengl forced but native EGL init failed: {e:?}"
2014 ))
2015 })?;
2016 Ok(Self {
2017 cpu: None,
2018 opengl: Some(opengl),
2019 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
2020 interpolation: Interpolation::Bilinear,
2021 env_interpolation: None,
2022 forced_backend: Some(ForcedBackend::OpenGl),
2023 }
2024 .apply_colorimetry_mode(config.colorimetry))
2025 }
2026 #[cfg(target_os = "windows")]
2027 #[cfg(feature = "opengl")]
2028 {
2029 let opengl = GLProcessorThreaded::with_cache_capacity(
2030 config.egl_display,
2031 config.egl_cache_capacity,
2032 )
2033 .map_err(|e| {
2034 Error::ForcedBackendUnavailable(format!(
2035 "opengl forced on Windows but ANGLE/D3D11 init failed: {e:?}"
2036 ))
2037 })?;
2038 Ok(Self {
2039 cpu: None,
2040 opengl: Some(opengl),
2041 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
2042 interpolation: Interpolation::Bilinear,
2043 env_interpolation: None,
2044 forced_backend: Some(ForcedBackend::OpenGl),
2045 }
2046 .apply_colorimetry_mode(config.colorimetry))
2047 }
2048 #[cfg(not(all(
2049 any(
2050 target_os = "linux",
2051 target_os = "macos",
2052 target_os = "ios",
2053 target_os = "android",
2054 target_os = "windows"
2055 ),
2056 feature = "opengl"
2057 )))]
2058 {
2059 Err(Error::ForcedBackendUnavailable(
2060 "opengl backend requires Linux, macOS, Android or Windows with the \
2061 'opengl' feature enabled"
2062 .into(),
2063 ))
2064 }
2065 }
2066 };
2067 }
2068
2069 #[cfg(target_os = "linux")]
2071 let g2d = if std::env::var("EDGEFIRST_DISABLE_G2D")
2072 .map(|x| x != "0" && x.to_lowercase() != "false")
2073 .unwrap_or(false)
2074 {
2075 log::debug!("EDGEFIRST_DISABLE_G2D is set");
2076 None
2077 } else {
2078 match G2DProcessor::new() {
2079 Ok(g2d_converter) => Some(g2d_converter),
2080 Err(err) => {
2081 log::warn!("Failed to initialize G2D converter: {err:?}");
2082 None
2083 }
2084 }
2085 };
2086
2087 #[cfg(target_os = "linux")]
2088 #[cfg(feature = "opengl")]
2089 let opengl = if std::env::var("EDGEFIRST_DISABLE_GL")
2090 .map(|x| x != "0" && x.to_lowercase() != "false")
2091 .unwrap_or(false)
2092 {
2093 log::debug!("EDGEFIRST_DISABLE_GL is set");
2094 None
2095 } else {
2096 match GLProcessorThreaded::with_cache_capacity(
2097 config.egl_display,
2098 config.egl_cache_capacity,
2099 ) {
2100 Ok(gl_converter) => Some(gl_converter),
2101 Err(err) => {
2102 log::warn!("Failed to initialize GL converter: {err:?}");
2103 None
2104 }
2105 }
2106 };
2107
2108 #[cfg(any(
2109 target_os = "macos",
2110 target_os = "ios",
2111 target_os = "android",
2112 target_os = "windows"
2113 ))]
2114 #[cfg(feature = "opengl")]
2115 let opengl = if std::env::var("EDGEFIRST_DISABLE_GL")
2116 .map(|x| x != "0" && x.to_lowercase() != "false")
2117 .unwrap_or(false)
2118 {
2119 log::debug!("EDGEFIRST_DISABLE_GL is set");
2120 None
2121 } else {
2122 match GLProcessorThreaded::with_cache_capacity(
2123 config.egl_display,
2124 config.egl_cache_capacity,
2125 ) {
2126 Ok(gl_converter) => Some(gl_converter),
2127 Err(err) => {
2128 log::debug!(
2129 "GL backend unavailable: {err:?} \
2130 (CPU fallback will be used)"
2131 );
2132 None
2133 }
2134 }
2135 };
2136
2137 let cpu = if std::env::var("EDGEFIRST_DISABLE_CPU")
2138 .map(|x| x != "0" && x.to_lowercase() != "false")
2139 .unwrap_or(false)
2140 {
2141 log::debug!("EDGEFIRST_DISABLE_CPU is set");
2142 None
2143 } else {
2144 Some(CPUProcessor::new())
2145 };
2146 Ok(Self {
2147 cpu,
2148 #[cfg(target_os = "linux")]
2149 g2d,
2150 #[cfg(target_os = "linux")]
2151 #[cfg(feature = "opengl")]
2152 opengl,
2153 #[cfg(any(
2154 target_os = "macos",
2155 target_os = "ios",
2156 target_os = "android",
2157 target_os = "windows"
2158 ))]
2159 #[cfg(feature = "opengl")]
2160 opengl,
2161 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
2162 interpolation: Interpolation::Bilinear,
2163 env_interpolation: None,
2164 forced_backend: None,
2165 }
2166 .apply_colorimetry_mode(config.colorimetry))
2167 }
2168
2169 fn apply_colorimetry_mode(self, _mode: ColorimetryMode) -> Self {
2173 #[cfg(all(
2174 any(
2175 target_os = "linux",
2176 target_os = "macos",
2177 target_os = "ios",
2178 target_os = "android",
2179 target_os = "windows"
2180 ),
2181 feature = "opengl"
2182 ))]
2183 {
2184 let mut me = self;
2185 if let Err(e) = me.set_colorimetry_mode(_mode) {
2186 log::warn!("Failed to apply ColorimetryMode::{_mode:?}: {e:?}");
2187 }
2188 me
2189 }
2190 #[cfg(not(all(
2191 any(
2192 target_os = "linux",
2193 target_os = "macos",
2194 target_os = "ios",
2195 target_os = "android",
2196 target_os = "windows"
2197 ),
2198 feature = "opengl"
2199 )))]
2200 {
2201 let _ = _mode;
2202 self
2203 }
2204 }
2205
2206 #[cfg(all(
2211 any(
2212 target_os = "linux",
2213 target_os = "macos",
2214 target_os = "ios",
2215 target_os = "android",
2216 target_os = "windows"
2217 ),
2218 feature = "opengl"
2219 ))]
2220 pub fn set_colorimetry_mode(&mut self, mode: ColorimetryMode) -> Result<()> {
2221 if let Some(ref mut gl) = self.opengl {
2222 gl.set_colorimetry_mode(mode)?;
2223 }
2224 Ok(())
2225 }
2226
2227 pub fn set_interpolation(&mut self, mode: Interpolation) -> Result<()> {
2235 let mode = match self.env_interpolation {
2236 Some(env) => {
2237 if env != mode.supported() {
2238 log::debug!(
2239 "EDGEFIRST_INTERPOLATION pins Interpolation::{env:?}; \
2240 ignoring Interpolation::{mode:?}"
2241 );
2242 }
2243 env
2244 }
2245 None => runnable_interpolation(mode),
2246 };
2247 if mode == self.interpolation {
2248 return Ok(());
2249 }
2250 if let Some(cpu) = self.cpu.as_mut() {
2251 cpu.set_interpolation(mode);
2252 }
2253 #[cfg(target_os = "linux")]
2254 if let Some(g2d) = self.g2d.as_mut() {
2255 g2d.set_interpolation(mode);
2256 }
2257 #[cfg(all(
2258 any(
2259 target_os = "linux",
2260 target_os = "macos",
2261 target_os = "ios",
2262 target_os = "android",
2263 target_os = "windows"
2264 ),
2265 feature = "opengl"
2266 ))]
2267 if let Some(gl) = self.opengl.as_mut() {
2268 gl.set_interpolation(mode)?;
2269 }
2270 self.interpolation = mode;
2271 Ok(())
2272 }
2273
2274 pub fn interpolation(&self) -> Interpolation {
2277 self.interpolation
2278 }
2279
2280 fn resolve_interpolation(&self, options: ConvertOptions) -> ConvertOptions {
2284 let mode = match (self.env_interpolation, options.get_interpolation()) {
2285 (Some(env), _) => env,
2286 (None, Some(mode)) => runnable_interpolation(mode),
2287 (None, None) => self.interpolation,
2288 };
2289 options.with_interpolation(Some(mode))
2290 }
2291
2292 #[cfg(all(
2295 any(
2296 target_os = "linux",
2297 target_os = "macos",
2298 target_os = "ios",
2299 target_os = "android",
2300 target_os = "windows"
2301 ),
2302 feature = "opengl"
2303 ))]
2304 pub fn set_int8_interpolation_mode(&mut self, mode: Int8InterpolationMode) -> Result<()> {
2305 if let Some(ref mut gl) = self.opengl {
2306 gl.set_int8_interpolation_mode(mode)?;
2307 }
2308 Ok(())
2309 }
2310
2311 fn producer_access(
2317 &self,
2318 access: edgefirst_tensor::CpuAccess,
2319 format: PixelFormat,
2320 dtype: DType,
2321 memory: TensorMemory,
2322 views: bool,
2323 ) -> edgefirst_tensor::CpuAccess {
2324 use edgefirst_tensor::CpuAccess;
2325 #[cfg(all(
2326 any(
2327 target_os = "linux",
2328 target_os = "macos",
2329 target_os = "ios",
2330 target_os = "android",
2331 target_os = "windows"
2332 ),
2333 feature = "opengl"
2334 ))]
2335 let gl = self
2336 .opengl
2337 .as_ref()
2338 .map(|gl| gl.dst_cpu_access(memory, format, dtype));
2339 #[cfg(not(all(
2340 any(
2341 target_os = "linux",
2342 target_os = "macos",
2343 target_os = "ios",
2344 target_os = "android",
2345 target_os = "windows"
2346 ),
2347 feature = "opengl"
2348 )))]
2349 let gl = None;
2350 #[cfg(target_os = "linux")]
2351 let g2d = self
2352 .g2d
2353 .as_ref()
2354 .map(|_| g2d::dst_cpu_access(memory, format, dtype));
2355 #[cfg(not(target_os = "linux"))]
2356 let g2d = None;
2357 let cpu = self
2358 .cpu
2359 .as_ref()
2360 .map(|_| Some(cpu::convert_dst_access(None, format, dtype)));
2361 let needed = predicted_cpu_access(self.forced_backend, gl, g2d, cpu);
2362 let needed = if views && needed.writes() {
2363 CpuAccess::ReadWrite
2364 } else {
2365 needed
2366 };
2367 access.union(needed)
2368 }
2369
2370 pub fn create_image_desc(&self, desc: &edgefirst_tensor::ImageDesc) -> Result<TensorDyn> {
2385 if desc.compression().is_some() {
2387 return Ok(TensorDyn::image_desc(desc)?);
2388 }
2389 if !desc.contiguous() {
2390 return self.create_image(
2391 desc.width(),
2392 desc.height(),
2393 desc.format(),
2394 desc.dtype(),
2395 desc.memory(),
2396 desc.access(),
2397 );
2398 }
2399 let access = self.producer_access(
2400 desc.access(),
2401 desc.format(),
2402 desc.dtype(),
2403 TensorMemory::DmaBuf,
2404 false,
2405 );
2406 Ok(TensorDyn::image_desc(&desc.clone().with_access(access))?)
2407 }
2408
2409 pub fn create_image(
2522 &self,
2523 width: usize,
2524 height: usize,
2525 format: PixelFormat,
2526 dtype: DType,
2527 memory: Option<TensorMemory>,
2528 access: edgefirst_tensor::CpuAccess,
2529 ) -> Result<TensorDyn> {
2530 self.create_image_with(width, height, format, dtype, memory, access, false)
2531 }
2532
2533 #[cfg(target_os = "linux")]
2541 fn create_dma_image(
2542 &self,
2543 width: usize,
2544 height: usize,
2545 format: PixelFormat,
2546 dtype: DType,
2547 access: edgefirst_tensor::CpuAccess,
2548 views: bool,
2549 ) -> Result<TensorDyn> {
2550 let dma_access = self.producer_access(access, format, dtype, TensorMemory::DmaBuf, views);
2551 let natural =
2552 primary_plane_bpp(format, dtype.size()).and_then(|bpp| width.checked_mul(bpp));
2553 let padded = natural
2554 .and_then(align_pitch_bytes_to_gpu_alignment)
2555 .filter(|&stride| {
2556 format.layout() == edgefirst_tensor::PixelLayout::Packed
2557 && natural.is_some_and(|n| stride > n)
2558 });
2559 let memory = Some(edgefirst_tensor::TensorMemory::DmaBuf);
2560 match padded {
2561 Some(stride) => {
2562 log::debug!(
2563 "create_image: padding row stride for {format:?} {width}x{height} \
2564 from natural pitch to {stride} bytes for GPU alignment"
2565 );
2566 Ok(TensorDyn::image_with_stride(
2567 width, height, format, dtype, stride, memory, dma_access,
2568 )?)
2569 }
2570 None => Ok(TensorDyn::image(
2571 width, height, format, dtype, memory, dma_access,
2572 )?),
2573 }
2574 }
2575
2576 #[cfg(any(target_os = "linux", target_os = "windows"))]
2580 #[cfg(feature = "opengl")]
2581 fn create_byte_pbo_image(
2582 &self,
2583 width: usize,
2584 height: usize,
2585 format: PixelFormat,
2586 dtype: DType,
2587 ) -> Option<TensorDyn> {
2588 if dtype.size() != 1 {
2589 return None;
2590 }
2591 let gl = self.opengl.as_ref()?;
2592 let t = gl
2593 .create_pbo_image(width, height, format)
2594 .map_err(|e| log::debug!("PBO image creation failed, falling back to Mem: {e:?}"))
2595 .ok()?;
2596 if dtype != DType::I8 {
2597 return Some(TensorDyn::from(t));
2598 }
2599 debug_assert!(
2602 t.chroma().is_none(),
2603 "PBO i8 transmute requires chroma == None"
2604 );
2605 let t_i8: Tensor<i8> = unsafe { std::mem::transmute(t) };
2609 Some(TensorDyn::from(t_i8))
2610 }
2611
2612 #[allow(clippy::too_many_arguments)]
2615 pub(crate) fn create_image_with(
2616 &self,
2617 width: usize,
2618 height: usize,
2619 format: PixelFormat,
2620 dtype: DType,
2621 memory: Option<TensorMemory>,
2622 access: edgefirst_tensor::CpuAccess,
2623 views: bool,
2624 ) -> Result<TensorDyn> {
2625 #[cfg(target_os = "linux")]
2626 let try_dma = || self.create_dma_image(width, height, format, dtype, access, views);
2627
2628 match memory {
2635 #[cfg(target_os = "linux")]
2636 Some(TensorMemory::DmaBuf) => {
2637 if dtype == DType::F32 {
2639 return Err(Error::NotSupported(
2640 "F32 has no 32-bit-float DRM format for DMA-BUF; \
2641 use TensorMemory::Pbo for F32"
2642 .to_string(),
2643 ));
2644 }
2645 return try_dma();
2646 }
2647 Some(mem) => {
2648 return Ok(TensorDyn::image(
2649 width,
2650 height,
2651 format,
2652 dtype,
2653 Some(mem),
2654 self.producer_access(access, format, dtype, mem, views),
2655 )?);
2656 }
2657 None => {}
2658 }
2659
2660 #[cfg(any(
2667 target_os = "macos",
2668 target_os = "ios",
2669 target_os = "android",
2670 target_os = "windows"
2671 ))]
2672 #[cfg(feature = "opengl")]
2673 if let Some(gl) = self.opengl.as_ref() {
2674 let _ = gl;
2675 let support = self.supported_render_dtypes();
2680 let dtype_ok =
2681 !matches!(dtype, DType::F16 | DType::F32) || float_pbo_eligible(dtype, support);
2682 if dtype_ok {
2683 match TensorDyn::image(
2684 width,
2685 height,
2686 format,
2687 dtype,
2688 Some(edgefirst_tensor::TensorMemory::DmaBuf),
2689 self.producer_access(access, format, dtype, TensorMemory::DmaBuf, views),
2690 ) {
2691 Ok(img) => return Ok(img),
2692 Err(e) => {
2693 log::debug!(
2697 "create_image: zero-copy Dma allocation declined \
2698 ({format:?}/{dtype:?} {width}x{height}): {e:?}; using fallback storage"
2699 );
2700 }
2701 }
2702 }
2703 }
2704
2705 #[cfg(target_os = "linux")]
2708 {
2709 #[cfg(feature = "opengl")]
2710 let gl_uses_pbo = self
2711 .opengl
2712 .as_ref()
2713 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
2714 #[cfg(not(feature = "opengl"))]
2715 let gl_uses_pbo = false;
2716
2717 if !gl_uses_pbo {
2718 if let Ok(img) = try_dma() {
2719 return Ok(img);
2720 }
2721 }
2722 }
2723
2724 #[cfg(any(target_os = "linux", target_os = "windows"))]
2725 #[cfg(feature = "opengl")]
2726 if let Some(img) = self.create_byte_pbo_image(width, height, format, dtype) {
2727 return Ok(img);
2728 }
2729
2730 #[cfg(any(target_os = "linux", target_os = "windows"))]
2733 #[cfg(feature = "opengl")]
2734 if float_pbo_eligible(dtype, self.supported_render_dtypes()) {
2735 if let Some(gl) = &self.opengl {
2736 match gl.create_pbo_image_dtype(width, height, format, dtype) {
2737 Ok(t) => return Ok(t),
2738 Err(e) => {
2739 log::debug!(
2740 "Float PBO image creation failed for {dtype:?}, \
2741 falling back to Mem: {e:?}"
2742 );
2743 }
2744 }
2745 }
2746 }
2747
2748 Ok(TensorDyn::image(
2750 width,
2751 height,
2752 format,
2753 dtype,
2754 Some(edgefirst_tensor::TensorMemory::Mem),
2755 self.producer_access(access, format, dtype, TensorMemory::Mem, views),
2756 )?)
2757 }
2758
2759 #[allow(clippy::too_many_arguments)]
2813 #[cfg(target_os = "linux")]
2814 pub fn import_image(
2815 &self,
2816 image: edgefirst_tensor::PlaneDescriptor,
2817 chroma: Option<edgefirst_tensor::PlaneDescriptor>,
2818 width: usize,
2819 height: usize,
2820 format: PixelFormat,
2821 dtype: DType,
2822 colorimetry: Option<edgefirst_tensor::Colorimetry>,
2823 ) -> Result<TensorDyn> {
2824 use edgefirst_tensor::{Tensor, TensorMemory};
2825
2826 let image_stride = image.stride();
2828 let image_offset = image.offset();
2829 let chroma_stride = chroma.as_ref().and_then(|c| c.stride());
2830 let chroma_offset = chroma.as_ref().and_then(|c| c.offset());
2831
2832 if let Some(chroma_pd) = chroma {
2833 if dtype != DType::U8 && dtype != DType::I8 {
2838 return Err(Error::NotSupported(format!(
2839 "multiplane import only supports U8/I8, got {dtype:?}"
2840 )));
2841 }
2842 if format.layout() != PixelLayout::SemiPlanar {
2843 return Err(Error::NotSupported(format!(
2844 "import_image with chroma requires a semi-planar format, got {format:?}"
2845 )));
2846 }
2847
2848 let chroma_h = match format {
2849 PixelFormat::Nv12 => {
2850 height.div_ceil(2)
2852 }
2853 PixelFormat::Nv16 => {
2856 return Err(Error::NotSupported(
2857 "multiplane NV16 is not yet supported; use contiguous NV16 instead".into(),
2858 ))
2859 }
2860 _ => {
2861 return Err(Error::NotSupported(format!(
2862 "unsupported semi-planar format: {format:?}"
2863 )))
2864 }
2865 };
2866
2867 let luma = Tensor::<u8>::from_fd(image.into_fd(), &[height, width], Some("luma"))?;
2868 if luma.memory() != TensorMemory::DmaBuf {
2869 return Err(Error::NotSupported(format!(
2870 "luma fd must be DMA-backed, got {:?}",
2871 luma.memory()
2872 )));
2873 }
2874
2875 let mut chroma_tensor =
2876 Tensor::<u8>::from_fd(chroma_pd.into_fd(), &[chroma_h, width], Some("chroma"))?;
2877 if chroma_tensor.memory() != TensorMemory::DmaBuf {
2878 return Err(Error::NotSupported(format!(
2879 "chroma fd must be DMA-backed, got {:?}",
2880 chroma_tensor.memory()
2881 )));
2882 }
2883
2884 if let Some(s) = chroma_stride {
2888 if s < width {
2889 return Err(Error::InvalidShape(format!(
2890 "chroma stride {s} < minimum {width} for {format:?}"
2891 )));
2892 }
2893 chroma_tensor.set_row_stride_unchecked(s);
2894 }
2895 if let Some(o) = chroma_offset {
2896 chroma_tensor.set_plane_offset(o);
2897 }
2898
2899 let mut tensor = Tensor::<u8>::from_planes(luma, chroma_tensor, format)?;
2902
2903 if let Some(s) = image_stride {
2905 tensor.set_row_stride(s)?;
2906 }
2907 if let Some(o) = image_offset {
2908 tensor.set_plane_offset(o);
2909 }
2910
2911 if dtype == DType::I8 {
2912 const {
2916 assert!(std::mem::size_of::<Tensor<u8>>() == std::mem::size_of::<Tensor<i8>>());
2917 assert!(
2918 std::mem::align_of::<Tensor<u8>>() == std::mem::align_of::<Tensor<i8>>()
2919 );
2920 }
2921 let tensor_i8: Tensor<i8> = unsafe { std::mem::transmute(tensor) };
2922 let mut dyn_tensor = TensorDyn::from(tensor_i8);
2923 dyn_tensor.set_colorimetry(colorimetry);
2924 return Ok(dyn_tensor);
2925 }
2926 let mut dyn_tensor = TensorDyn::from(tensor);
2927 dyn_tensor.set_colorimetry(colorimetry);
2928 Ok(dyn_tensor)
2929 } else {
2930 let shape = format.allocation_shape(width, height).ok_or_else(|| {
2935 Error::NotSupported(format!(
2936 "unsupported pixel format for import_image: {format:?}"
2937 ))
2938 })?;
2939 let tensor = TensorDyn::from_fd(image.into_fd(), &shape, dtype, None)?;
2940 if tensor.memory() != TensorMemory::DmaBuf {
2941 return Err(Error::NotSupported(format!(
2942 "import_image requires DMA-backed fd, got {:?}",
2943 tensor.memory()
2944 )));
2945 }
2946 let mut tensor = tensor.with_format(format)?;
2947 if let Some(s) = image_stride {
2948 tensor.set_row_stride(s)?;
2949 }
2950 if let Some(o) = image_offset {
2951 tensor.set_plane_offset(o);
2952 }
2953 tensor.set_colorimetry(colorimetry);
2954 Ok(tensor)
2955 }
2956 }
2957
2958 #[cfg(feature = "decode")]
2983 pub fn draw_masks(
2984 &mut self,
2985 decoder: &edgefirst_decoder::Decoder,
2986 outputs: &[&TensorDyn],
2987 dst: &mut TensorDyn,
2988 overlay: MaskOverlay<'_>,
2989 ) -> Result<Vec<DetectBox>> {
2990 let mut output_boxes = Vec::with_capacity(100);
2991
2992 let proto_result = decoder
2994 .decode_proto(outputs, &mut output_boxes)
2995 .map_err(|e| Error::Internal(format!("decode_proto: {e:#?}")))?;
2996
2997 if let Some(proto_data) = proto_result {
2998 self.draw_proto_masks(dst, &output_boxes, &proto_data, overlay)?;
2999 } else {
3000 let mut output_masks = Vec::with_capacity(100);
3002 decoder
3003 .decode(outputs, &mut output_boxes, &mut output_masks)
3004 .map_err(|e| Error::Internal(format!("decode: {e:#?}")))?;
3005 self.draw_decoded_masks(dst, &output_boxes, &output_masks, overlay)?;
3006 }
3007 Ok(output_boxes)
3008 }
3009
3010 #[cfg(feature = "tracker")]
3018 pub fn draw_masks_tracked<TR: edgefirst_tracker::Tracker<DetectBox>>(
3019 &mut self,
3020 decoder: &edgefirst_decoder::Decoder,
3021 tracker: &mut TR,
3022 timestamp: u64,
3023 outputs: &[&TensorDyn],
3024 dst: &mut TensorDyn,
3025 overlay: MaskOverlay<'_>,
3026 ) -> Result<(Vec<DetectBox>, Vec<edgefirst_tracker::TrackInfo>)> {
3027 let mut output_boxes = Vec::with_capacity(100);
3028 let mut output_tracks = Vec::new();
3029
3030 let proto_result = decoder
3031 .decode_proto_tracked(
3032 tracker,
3033 timestamp,
3034 outputs,
3035 &mut output_boxes,
3036 &mut output_tracks,
3037 )
3038 .map_err(|e| Error::Internal(format!("decode_proto_tracked: {e:#?}")))?;
3039
3040 if let Some(proto_data) = proto_result {
3041 self.draw_proto_masks(dst, &output_boxes, &proto_data, overlay)?;
3042 } else {
3043 let mut output_masks = Vec::with_capacity(100);
3047 decoder
3048 .decode_tracked(
3049 tracker,
3050 timestamp,
3051 outputs,
3052 &mut output_boxes,
3053 &mut output_masks,
3054 &mut output_tracks,
3055 )
3056 .map_err(|e| Error::Internal(format!("decode_tracked: {e:#?}")))?;
3057 self.draw_decoded_masks(dst, &output_boxes, &output_masks, overlay)?;
3058 }
3059 Ok((output_boxes, output_tracks))
3060 }
3061
3062 pub fn materialize_masks(
3086 &mut self,
3087 detect: &[DetectBox],
3088 proto_data: &ProtoData,
3089 content_region: Option<[f32; 4]>,
3090 resolution: MaskResolution,
3091 ) -> Result<Vec<Segmentation>> {
3092 let cpu = self.cpu.as_mut().ok_or(Error::NoConverter)?;
3093 match resolution {
3094 MaskResolution::Proto => {
3095 cpu.materialize_segmentations(detect, proto_data, content_region)
3096 }
3097 MaskResolution::Scaled { width, height } => cpu.materialize_scaled_segmentations(
3098 detect,
3099 proto_data,
3100 content_region,
3101 width,
3102 height,
3103 ),
3104 }
3105 }
3106}
3107
3108impl ImageProcessorTrait for ImageProcessor {
3109 fn convert(
3115 &mut self,
3116 src: &TensorDyn,
3117 dst: &mut TensorDyn,
3118 options: Option<ConvertOptions>,
3119 ) -> Result<()> {
3120 let options = self.resolve_interpolation(options.unwrap_or_default());
3121 let (rotation, flip) = (options.get_rotation(), options.get_flip());
3122 let start = Instant::now();
3123 let src_fmt = src.format();
3124 let dst_fmt = dst.format();
3125 let _span = tracing::trace_span!(
3126 "image.convert",
3127 ?src_fmt,
3128 ?dst_fmt,
3129 src_memory = ?src.memory(),
3130 dst_memory = ?dst.memory(),
3131 ?rotation,
3132 ?flip,
3133 )
3134 .entered();
3135 log::trace!(
3136 "convert: {src_fmt:?}({:?}/{:?}) → {dst_fmt:?}({:?}/{:?}), \
3137 rotation={rotation:?}, flip={flip:?}, backend={:?}",
3138 src.dtype(),
3139 src.memory(),
3140 dst.dtype(),
3141 dst.memory(),
3142 self.forced_backend,
3143 );
3144
3145 if let Some(forced) = self.forced_backend {
3147 return match forced {
3148 ForcedBackend::Cpu => {
3149 if let Some(cpu) = self.cpu.as_mut() {
3150 let r = cpu.convert(src, dst, Some(options));
3151 log::trace!(
3152 "convert: forced=cpu result={} ({:?})",
3153 if r.is_ok() { "ok" } else { "err" },
3154 start.elapsed()
3155 );
3156 return r;
3157 }
3158 Err(Error::ForcedBackendUnavailable("cpu".into()))
3159 }
3160 ForcedBackend::G2d => {
3161 #[cfg(target_os = "linux")]
3162 if let Some(g2d) = self.g2d.as_mut() {
3163 let r = g2d.convert(src, dst, Some(options));
3164 log::trace!(
3165 "convert: forced=g2d result={} ({:?})",
3166 if r.is_ok() { "ok" } else { "err" },
3167 start.elapsed()
3168 );
3169 return r;
3170 }
3171 Err(Error::ForcedBackendUnavailable("g2d".into()))
3172 }
3173 ForcedBackend::OpenGl => {
3174 #[cfg(any(
3175 target_os = "linux",
3176 target_os = "macos",
3177 target_os = "ios",
3178 target_os = "android",
3179 target_os = "windows"
3180 ))]
3181 #[cfg(feature = "opengl")]
3182 if let Some(opengl) = self.opengl.as_mut() {
3183 let r = opengl.convert(src, dst, Some(options));
3184 log::trace!(
3185 "convert: forced=opengl result={} ({:?})",
3186 if r.is_ok() { "ok" } else { "err" },
3187 start.elapsed()
3188 );
3189 return r;
3190 }
3191 Err(Error::ForcedBackendUnavailable("opengl".into()))
3192 }
3193 };
3194 }
3195
3196 #[allow(unused_mut)]
3198 let mut declined: Vec<(&'static str, Error)> = Vec::new();
3199 #[cfg(any(
3200 target_os = "linux",
3201 target_os = "macos",
3202 target_os = "ios",
3203 target_os = "android",
3204 target_os = "windows"
3205 ))]
3206 #[cfg(feature = "opengl")]
3207 if let Some(opengl) = self.opengl.as_mut() {
3208 match opengl.convert(src, dst, Some(options)) {
3209 Ok(_) => {
3210 log::trace!(
3211 "convert: auto selected=opengl for {src_fmt:?}→{dst_fmt:?} ({:?})",
3212 start.elapsed()
3213 );
3214 return Ok(());
3215 }
3216 Err(e) => {
3217 self.convert_fallbacks
3218 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3219 log::debug!(
3220 "convert: auto opengl declined {src_fmt:?}@{:?}→{dst_fmt:?}@{:?}, \
3221 falling back toward G2D/CPU: {e}",
3222 src.memory(),
3223 dst.memory(),
3224 );
3225 declined.push(("opengl", e));
3226 }
3227 }
3228 }
3229
3230 #[cfg(target_os = "linux")]
3231 if let Some(g2d) = self.g2d.as_mut() {
3232 let src_is_yuv = src.format().is_some_and(|f| f.is_yuv());
3239 let dst_is_yuv = dst.format().is_some_and(|f| f.is_yuv());
3240 let g2d_eligible = if src_is_yuv || dst_is_yuv {
3241 let cm = if src_is_yuv {
3242 crate::colorimetry::effective_colorimetry(src)
3243 } else {
3244 crate::colorimetry::effective_colorimetry(dst)
3245 };
3246 crate::g2d::g2d_can_handle(&cm, true)
3247 } else {
3248 true
3249 };
3250 if !g2d_eligible {
3251 log::trace!(
3252 "convert: auto g2d skipped {src_fmt:?}→{dst_fmt:?} \
3253 (colorimetry not expressible: full-range/BT.2020)"
3254 );
3255 } else {
3256 match g2d.convert(src, dst, Some(options)) {
3257 Ok(_) => {
3258 log::trace!(
3259 "convert: auto selected=g2d for {src_fmt:?}→{dst_fmt:?} ({:?})",
3260 start.elapsed()
3261 );
3262 return Ok(());
3263 }
3264 Err(e) => {
3265 log::trace!("convert: auto g2d declined {src_fmt:?}→{dst_fmt:?}: {e}");
3266 declined.push(("g2d", e));
3267 }
3268 }
3269 }
3270 }
3271
3272 if let Some(cpu) = self.cpu.as_mut() {
3273 match cpu.convert(src, dst, Some(options)) {
3274 Ok(_) => {
3275 log::trace!(
3276 "convert: auto selected=cpu for {src_fmt:?}→{dst_fmt:?} ({:?})",
3277 start.elapsed()
3278 );
3279 return Ok(());
3280 }
3281 Err(e) => {
3282 log::trace!("convert: auto cpu failed {src_fmt:?}→{dst_fmt:?}: {e}");
3283 return Err(with_gpu_declines(e, declined));
3284 }
3285 }
3286 }
3287 Err(Error::NoConverter)
3288 }
3289
3290 fn convert_deferred(
3291 &mut self,
3292 src: &TensorDyn,
3293 dst: &mut TensorDyn,
3294 options: Option<ConvertOptions>,
3295 ) -> Result<()> {
3296 let options = self.resolve_interpolation(options.unwrap_or_default());
3297 #[cfg(any(
3303 target_os = "linux",
3304 target_os = "macos",
3305 target_os = "ios",
3306 target_os = "android",
3307 target_os = "windows"
3308 ))]
3309 #[cfg(feature = "opengl")]
3310 {
3311 let gl_forced = matches!(self.forced_backend, Some(ForcedBackend::OpenGl));
3312 if gl_forced || self.forced_backend.is_none() {
3313 if let Some(opengl) = self.opengl.as_mut() {
3314 match opengl.convert_deferred(src, dst, Some(options)) {
3315 Ok(()) => return Ok(()),
3316 Err(e) => {
3317 log::trace!("convert_deferred: gl declined: {e}; eager fallback");
3318 if gl_forced {
3321 return Err(e);
3322 }
3323 }
3324 }
3325 }
3326 }
3327 }
3328 self.convert(src, dst, Some(options))
3329 }
3330
3331 fn flush(&mut self) -> Result<()> {
3332 let _span = tracing::trace_span!("image.flush").entered();
3333 #[cfg(any(
3336 target_os = "linux",
3337 target_os = "macos",
3338 target_os = "ios",
3339 target_os = "android",
3340 target_os = "windows"
3341 ))]
3342 #[cfg(feature = "opengl")]
3343 if let Some(opengl) = self.opengl.as_mut() {
3344 return opengl.flush();
3345 }
3346 Ok(())
3347 }
3348
3349 fn draw_decoded_masks(
3350 &mut self,
3351 dst: &mut TensorDyn,
3352 detect: &[DetectBox],
3353 segmentation: &[Segmentation],
3354 overlay: MaskOverlay<'_>,
3355 ) -> Result<()> {
3356 let _span = tracing::trace_span!(
3357 "image.draw_decoded_masks",
3358 n_detections = detect.len(),
3359 n_segmentations = segmentation.len(),
3360 )
3361 .entered();
3362 let start = Instant::now();
3363
3364 if let Some(bg) = overlay.background {
3365 if bg.aliases(dst) {
3366 return Err(Error::AliasedBuffers(
3367 "background must not reference the same buffer as dst".to_string(),
3368 ));
3369 }
3370 }
3371
3372 let lb_boxes: Vec<DetectBox>;
3375 let lb_segs: Vec<Segmentation>;
3376 let (detect, segmentation) = if let Some(lb) = overlay.content_region {
3377 lb_boxes = detect.iter().map(|&d| unletter_bbox(d, lb)).collect();
3378 if segmentation.len() == lb_boxes.len() {
3381 lb_segs = segmentation
3388 .iter()
3389 .zip(lb_boxes.iter())
3390 .map(|(s, d)| copy_segmentation(s, d.bbox.into()))
3391 .collect::<Result<Vec<_>>>()?;
3392 (lb_boxes.as_slice(), lb_segs.as_slice())
3393 } else {
3394 (lb_boxes.as_slice(), segmentation)
3398 }
3399 } else {
3400 (detect, segmentation)
3401 };
3402 #[cfg(target_os = "linux")]
3403 let is_empty_frame = detect.is_empty() && segmentation.is_empty();
3404
3405 if let Some(forced) = self.forced_backend {
3407 return match forced {
3408 ForcedBackend::Cpu => {
3409 if let Some(cpu) = self.cpu.as_mut() {
3410 return cpu.draw_decoded_masks(dst, detect, segmentation, overlay);
3411 }
3412 Err(Error::ForcedBackendUnavailable("cpu".into()))
3413 }
3414 ForcedBackend::G2d => {
3415 #[cfg(target_os = "linux")]
3418 if let Some(g2d) = self.g2d.as_mut() {
3419 return g2d.draw_decoded_masks(dst, detect, segmentation, overlay);
3420 }
3421 Err(Error::ForcedBackendUnavailable("g2d".into()))
3422 }
3423 ForcedBackend::OpenGl => {
3424 #[cfg(any(
3427 target_os = "linux",
3428 target_os = "macos",
3429 target_os = "ios",
3430 target_os = "android",
3431 target_os = "windows"
3432 ))]
3433 #[cfg(feature = "opengl")]
3434 if let Some(opengl) = self.opengl.as_mut() {
3435 return opengl.draw_decoded_masks(dst, detect, segmentation, overlay);
3436 }
3437 Err(Error::ForcedBackendUnavailable("opengl".into()))
3438 }
3439 };
3440 }
3441
3442 #[cfg(target_os = "linux")]
3448 if is_empty_frame {
3449 if let Some(g2d) = self.g2d.as_mut() {
3450 match g2d.draw_decoded_masks(dst, detect, segmentation, overlay) {
3451 Ok(_) => {
3452 log::trace!(
3453 "draw_decoded_masks empty frame via g2d in {:?}",
3454 start.elapsed()
3455 );
3456 return Ok(());
3457 }
3458 Err(e) => log::trace!("g2d empty-frame path unavailable: {e:?}"),
3459 }
3460 }
3461 }
3462
3463 #[cfg(any(
3467 target_os = "linux",
3468 target_os = "macos",
3469 target_os = "ios",
3470 target_os = "android",
3471 target_os = "windows"
3472 ))]
3473 #[cfg(feature = "opengl")]
3474 if let Some(opengl) = self.opengl.as_mut() {
3475 log::trace!(
3476 "draw_decoded_masks started with opengl in {:?}",
3477 start.elapsed()
3478 );
3479 match opengl.draw_decoded_masks(dst, detect, segmentation, overlay) {
3480 Ok(_) => {
3481 log::trace!("draw_decoded_masks with opengl in {:?}", start.elapsed());
3482 return Ok(());
3483 }
3484 Err(e) => {
3485 log::trace!("draw_decoded_masks didn't work with opengl: {e:?}")
3486 }
3487 }
3488 }
3489
3490 log::trace!(
3491 "draw_decoded_masks started with cpu in {:?}",
3492 start.elapsed()
3493 );
3494 if let Some(cpu) = self.cpu.as_mut() {
3495 match cpu.draw_decoded_masks(dst, detect, segmentation, overlay) {
3496 Ok(_) => {
3497 log::trace!("draw_decoded_masks with cpu in {:?}", start.elapsed());
3498 return Ok(());
3499 }
3500 Err(e) => {
3501 log::trace!("draw_decoded_masks didn't work with cpu: {e:?}");
3502 return Err(e);
3503 }
3504 }
3505 }
3506 Err(Error::NoConverter)
3507 }
3508
3509 fn draw_proto_masks(
3510 &mut self,
3511 dst: &mut TensorDyn,
3512 detect: &[DetectBox],
3513 proto_data: &ProtoData,
3514 overlay: MaskOverlay<'_>,
3515 ) -> Result<()> {
3516 let start = Instant::now();
3517
3518 if let Some(bg) = overlay.background {
3519 if bg.aliases(dst) {
3520 return Err(Error::AliasedBuffers(
3521 "background must not reference the same buffer as dst".to_string(),
3522 ));
3523 }
3524 }
3525
3526 let lb_boxes: Vec<DetectBox>;
3532 let render_detect = if let Some(lb) = overlay.content_region {
3533 lb_boxes = detect.iter().map(|&d| unletter_bbox(d, lb)).collect();
3534 lb_boxes.as_slice()
3535 } else {
3536 detect
3537 };
3538 #[cfg(target_os = "linux")]
3539 let is_empty_frame = detect.is_empty();
3540
3541 if let Some(forced) = self.forced_backend {
3543 return match forced {
3544 ForcedBackend::Cpu => {
3545 if let Some(cpu) = self.cpu.as_mut() {
3546 return cpu.draw_proto_masks(dst, render_detect, proto_data, overlay);
3547 }
3548 Err(Error::ForcedBackendUnavailable("cpu".into()))
3549 }
3550 ForcedBackend::G2d => {
3551 #[cfg(target_os = "linux")]
3552 if let Some(g2d) = self.g2d.as_mut() {
3553 return g2d.draw_proto_masks(dst, render_detect, proto_data, overlay);
3554 }
3555 Err(Error::ForcedBackendUnavailable("g2d".into()))
3556 }
3557 ForcedBackend::OpenGl => {
3558 #[cfg(any(
3559 target_os = "linux",
3560 target_os = "macos",
3561 target_os = "ios",
3562 target_os = "android",
3563 target_os = "windows"
3564 ))]
3565 #[cfg(feature = "opengl")]
3566 if let Some(opengl) = self.opengl.as_mut() {
3567 return opengl.draw_proto_masks(dst, render_detect, proto_data, overlay);
3568 }
3569 Err(Error::ForcedBackendUnavailable("opengl".into()))
3570 }
3571 };
3572 }
3573
3574 #[cfg(target_os = "linux")]
3577 if is_empty_frame {
3578 if let Some(g2d) = self.g2d.as_mut() {
3579 match g2d.draw_proto_masks(dst, render_detect, proto_data, overlay) {
3580 Ok(_) => {
3581 log::trace!(
3582 "draw_proto_masks empty frame via g2d in {:?}",
3583 start.elapsed()
3584 );
3585 return Ok(());
3586 }
3587 Err(e) => log::trace!("g2d empty-frame path unavailable: {e:?}"),
3588 }
3589 }
3590 }
3591
3592 #[cfg(any(
3601 target_os = "linux",
3602 target_os = "macos",
3603 target_os = "ios",
3604 target_os = "android",
3605 target_os = "windows"
3606 ))]
3607 #[cfg(feature = "opengl")]
3608 if let (Some(_), Some(_)) = (self.cpu.as_ref(), self.opengl.as_ref()) {
3609 let segmentation = match self.cpu.as_mut() {
3610 Some(cpu) => {
3611 log::trace!(
3612 "draw_proto_masks started with hybrid (cpu+opengl) in {:?}",
3613 start.elapsed()
3614 );
3615 cpu.materialize_segmentations(detect, proto_data, overlay.content_region)?
3616 }
3617 None => unreachable!("cpu presence checked above"),
3618 };
3619 if let Some(opengl) = self.opengl.as_mut() {
3620 match opengl.draw_decoded_masks(dst, render_detect, &segmentation, overlay) {
3621 Ok(_) => {
3622 log::trace!(
3623 "draw_proto_masks with hybrid (cpu+opengl) in {:?}",
3624 start.elapsed()
3625 );
3626 return Ok(());
3627 }
3628 Err(e) => {
3629 log::trace!(
3630 "draw_proto_masks hybrid path failed, falling back to cpu: {e:?}"
3631 );
3632 }
3633 }
3634 }
3635 }
3636
3637 let Some(cpu) = self.cpu.as_mut() else {
3638 return Err(Error::Internal(
3639 "draw_proto_masks requires CPU backend for fallback path".into(),
3640 ));
3641 };
3642 log::trace!("draw_proto_masks started with cpu in {:?}", start.elapsed());
3643 cpu.draw_proto_masks(dst, render_detect, proto_data, overlay)
3644 }
3645
3646 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()> {
3647 let start = Instant::now();
3648
3649 if let Some(forced) = self.forced_backend {
3651 return match forced {
3652 ForcedBackend::Cpu => {
3653 if let Some(cpu) = self.cpu.as_mut() {
3654 return cpu.set_class_colors(colors);
3655 }
3656 Err(Error::ForcedBackendUnavailable("cpu".into()))
3657 }
3658 ForcedBackend::G2d => Err(Error::NotSupported(
3659 "g2d does not support set_class_colors".into(),
3660 )),
3661 ForcedBackend::OpenGl => {
3662 #[cfg(any(
3663 target_os = "linux",
3664 target_os = "macos",
3665 target_os = "ios",
3666 target_os = "android",
3667 target_os = "windows"
3668 ))]
3669 #[cfg(feature = "opengl")]
3670 if let Some(opengl) = self.opengl.as_mut() {
3671 return opengl.set_class_colors(colors);
3672 }
3673 Err(Error::ForcedBackendUnavailable("opengl".into()))
3674 }
3675 };
3676 }
3677
3678 let mut backends = 0usize;
3687 let mut first_err = None;
3688 #[cfg(any(
3689 target_os = "linux",
3690 target_os = "macos",
3691 target_os = "ios",
3692 target_os = "android",
3693 target_os = "windows"
3694 ))]
3695 #[cfg(feature = "opengl")]
3696 if let Some(opengl) = self.opengl.as_mut() {
3697 backends += 1;
3698 match opengl.set_class_colors(colors) {
3699 Ok(()) => log::trace!("colors set with opengl in {:?}", start.elapsed()),
3700 Err(e) => {
3701 log::trace!("colors didn't set with opengl: {e:?}");
3702 first_err.get_or_insert(e);
3703 }
3704 }
3705 }
3706 if let Some(cpu) = self.cpu.as_mut() {
3707 backends += 1;
3708 match cpu.set_class_colors(colors) {
3709 Ok(()) => log::trace!("colors set with cpu in {:?}", start.elapsed()),
3710 Err(e) => {
3711 log::trace!("colors didn't set with cpu: {e:?}");
3712 first_err.get_or_insert(e);
3713 }
3714 }
3715 }
3716 match first_err {
3720 Some(e) => Err(e),
3721 None if backends > 0 => Ok(()),
3722 None => Err(Error::NoConverter),
3723 }
3724 }
3725}
3726
3727#[cfg(all(test, feature = "codec"))]
3737pub(crate) fn load_image_test_helper(
3738 image: &[u8],
3739 format: Option<PixelFormat>,
3740 memory: Option<TensorMemory>,
3741) -> Result<TensorDyn> {
3742 use edgefirst_codec::{peek_info, ImageDecoder, ImageLoad};
3743
3744 let info = peek_info(image)?;
3748 let native_fmt = info.format;
3749 let w = info.width;
3750 let h = info.height;
3751
3752 let mut decoder = ImageDecoder::new();
3753
3754 #[cfg(target_os = "linux")]
3757 let native_src = {
3758 if let Some(aligned_pitch) = padded_dma_pitch_for(native_fmt, w, &memory) {
3759 let mut dma = Tensor::<u8>::image_with_stride(
3760 w,
3761 h,
3762 native_fmt,
3763 aligned_pitch,
3764 Some(TensorMemory::DmaBuf),
3765 edgefirst_tensor::CpuAccess::ReadWrite,
3766 )?;
3767 dma.load_image(&mut decoder, image)?;
3768 TensorDyn::from(dma)
3769 } else {
3770 let mut img = Tensor::<u8>::image(
3771 w,
3772 h,
3773 native_fmt,
3774 memory,
3775 edgefirst_tensor::CpuAccess::ReadWrite,
3776 )?;
3777 img.load_image(&mut decoder, image)?;
3778 TensorDyn::from(img)
3779 }
3780 };
3781 #[cfg(not(target_os = "linux"))]
3782 let native_src = {
3783 let mut img = Tensor::<u8>::image(
3784 w,
3785 h,
3786 native_fmt,
3787 memory,
3788 edgefirst_tensor::CpuAccess::ReadWrite,
3789 )?;
3790 img.load_image(&mut decoder, image)?;
3791 TensorDyn::from(img)
3792 };
3793
3794 match format {
3798 Some(f) if f != native_fmt => {
3799 let mut dst = TensorDyn::image(
3800 w,
3801 h,
3802 f,
3803 DType::U8,
3804 memory,
3805 edgefirst_tensor::CpuAccess::ReadWrite,
3806 )?;
3807 #[allow(clippy::needless_update)]
3814 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
3815 backend: ComputeBackend::Cpu,
3816 ..Default::default()
3817 })?;
3818 proc.convert(&native_src, &mut dst, None)?;
3819 Ok(dst)
3820 }
3821 _ => Ok(native_src),
3822 }
3823}
3824
3825pub fn save_jpeg(tensor: &TensorDyn, path: impl AsRef<std::path::Path>, quality: u8) -> Result<()> {
3829 let t = tensor.as_typed::<u8>().ok_or(Error::UnsupportedFormat(
3830 "save_jpeg requires u8 tensor".to_string(),
3831 ))?;
3832 let fmt = t.format().ok_or(Error::NotAnImage)?;
3833 if fmt.layout() != PixelLayout::Packed {
3834 return Err(Error::NotImplemented(
3835 "Saving planar images is not supported".to_string(),
3836 ));
3837 }
3838
3839 let colour = match fmt {
3840 PixelFormat::Rgb => jpeg_encoder::ColorType::Rgb,
3841 PixelFormat::Rgba => jpeg_encoder::ColorType::Rgba,
3842 _ => {
3843 return Err(Error::NotImplemented(
3844 "Unsupported image format for saving".to_string(),
3845 ));
3846 }
3847 };
3848
3849 let w = t.width().ok_or(Error::NotAnImage)?;
3850 let h = t.height().ok_or(Error::NotAnImage)?;
3851 let encoder = jpeg_encoder::Encoder::new_file(path, quality)?;
3852 let tensor_map = t.map_read()?;
3853
3854 encoder.encode(&tensor_map, w as u16, h as u16, colour)?;
3855
3856 Ok(())
3857}
3858
3859pub(crate) struct FunctionTimer<T: Display> {
3860 name: T,
3861 start: std::time::Instant,
3862}
3863
3864impl<T: Display> FunctionTimer<T> {
3865 pub fn new(name: T) -> Self {
3866 Self {
3867 name,
3868 start: std::time::Instant::now(),
3869 }
3870 }
3871}
3872
3873impl<T: Display> Drop for FunctionTimer<T> {
3874 fn drop(&mut self) {
3875 log::trace!("{} elapsed: {:?}", self.name, self.start.elapsed())
3876 }
3877}
3878
3879const DEFAULT_COLORS: [[f32; 4]; 20] = [
3880 [0., 1., 0., 0.7],
3881 [1., 0.5568628, 0., 0.7],
3882 [0.25882353, 0.15294118, 0.13333333, 0.7],
3883 [0.8, 0.7647059, 0.78039216, 0.7],
3884 [0.3137255, 0.3137255, 0.3137255, 0.7],
3885 [0.1411765, 0.3098039, 0.1215686, 0.7],
3886 [1., 0.95686275, 0.5137255, 0.7],
3887 [0.3529412, 0.32156863, 0., 0.7],
3888 [0.4235294, 0.6235294, 0.6509804, 0.7],
3889 [0.5098039, 0.5098039, 0.7294118, 0.7],
3890 [0.00784314, 0.18823529, 0.29411765, 0.7],
3891 [0.0, 0.2706, 1.0, 0.7],
3892 [0.0, 0.0, 0.0, 0.7],
3893 [0.0, 0.5, 0.0, 0.7],
3894 [1.0, 0.0, 0.0, 0.7],
3895 [0.0, 0.0, 1.0, 0.7],
3896 [1.0, 0.5, 0.5, 0.7],
3897 [0.1333, 0.5451, 0.1333, 0.7],
3898 [0.1176, 0.4118, 0.8235, 0.7],
3899 [1., 1., 1., 0.7],
3900];
3901
3902const fn denorm<const M: usize, const N: usize>(a: [[f32; M]; N]) -> [[u8; M]; N] {
3903 let mut result = [[0; M]; N];
3904 let mut i = 0;
3905 while i < N {
3906 let mut j = 0;
3907 while j < M {
3908 result[i][j] = (a[i][j] * 255.0).round() as u8;
3909 j += 1;
3910 }
3911 i += 1;
3912 }
3913 result
3914}
3915
3916const DEFAULT_COLORS_U8: [[u8; 4]; 20] = denorm(DEFAULT_COLORS);
3917
3918#[cfg(test)]
3919#[cfg_attr(coverage_nightly, coverage(off))]
3920mod alignment_tests {
3921 use super::*;
3922
3923 #[test]
3924 fn align_width_rgba8_common_widths() {
3925 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); }
3936
3937 #[test]
3938 fn align_width_rgb888_packed() {
3939 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] {
3946 let padded = align_width_for_gpu_pitch(w, 3);
3947 assert!(padded >= w);
3948 assert_eq!((padded * 3) % 64, 0);
3949 assert_eq!((padded * 3) % 3, 0);
3950 }
3951 }
3952
3953 #[test]
3954 fn align_width_grey_u8() {
3955 assert_eq!(align_width_for_gpu_pitch(64, 1), 64);
3957 assert_eq!(align_width_for_gpu_pitch(640, 1), 640);
3958 assert_eq!(align_width_for_gpu_pitch(1, 1), 64);
3959 assert_eq!(align_width_for_gpu_pitch(65, 1), 128);
3960 }
3961
3962 #[test]
3963 fn align_width_zero_inputs() {
3964 assert_eq!(align_width_for_gpu_pitch(0, 4), 0);
3965 assert_eq!(align_width_for_gpu_pitch(640, 0), 640);
3966 }
3967
3968 #[test]
3969 fn align_width_never_returns_smaller_than_input() {
3970 for &bpp in &[1usize, 2, 3, 4, 8] {
3974 for &w in &[
3975 1usize,
3976 17,
3977 64,
3978 65,
3979 100,
3980 1280,
3981 1281,
3982 1920,
3983 3004,
3984 3072,
3985 3840,
3986 usize::MAX / 8,
3987 usize::MAX / 4,
3988 usize::MAX / 2,
3989 usize::MAX - 1,
3990 usize::MAX,
3991 ] {
3992 let aligned = align_width_for_gpu_pitch(w, bpp);
3993 assert!(
3994 aligned >= w,
3995 "align_width_for_gpu_pitch({w}, {bpp}) = {aligned} < {w}"
3996 );
3997 }
3998 }
3999 }
4000
4001 #[test]
4002 fn align_width_overflow_returns_unaligned_not_smaller() {
4003 let aligned_extreme = usize::MAX - 15; assert_eq!(
4009 align_width_for_gpu_pitch(aligned_extreme, 4),
4010 aligned_extreme
4011 );
4012 let misaligned_extreme = usize::MAX - 1;
4015 let result = align_width_for_gpu_pitch(misaligned_extreme, 4);
4016 assert!(
4017 result == misaligned_extreme || result >= misaligned_extreme,
4018 "extreme misaligned width must not be rounded down to {result}"
4019 );
4020 }
4021
4022 #[test]
4023 fn checked_lcm_basic_and_overflow() {
4024 assert_eq!(checked_num_integer_lcm(64, 4), Some(64));
4025 assert_eq!(checked_num_integer_lcm(64, 3), Some(192));
4026 assert_eq!(checked_num_integer_lcm(64, 1), Some(64));
4027 assert_eq!(checked_num_integer_lcm(0, 4), Some(0));
4028 assert_eq!(checked_num_integer_lcm(64, 0), Some(0));
4029 assert_eq!(
4031 checked_num_integer_lcm(usize::MAX, usize::MAX - 1),
4032 None,
4033 "coprime extreme values must overflow detect, not panic"
4034 );
4035 }
4036
4037 #[test]
4038 fn primary_plane_bpp_known_formats() {
4039 assert_eq!(primary_plane_bpp(PixelFormat::Rgba, 1), Some(4));
4041 assert_eq!(primary_plane_bpp(PixelFormat::Bgra, 1), Some(4));
4042 assert_eq!(primary_plane_bpp(PixelFormat::Rgb, 1), Some(3));
4043 assert_eq!(primary_plane_bpp(PixelFormat::Grey, 1), Some(1));
4044 assert_eq!(primary_plane_bpp(PixelFormat::Nv12, 1), Some(1));
4046 }
4047}
4048
4049#[cfg(test)]
4050#[cfg_attr(coverage_nightly, coverage(off))]
4051#[allow(deprecated)]
4052mod image_tests {
4053 use super::*;
4054 #[cfg(target_os = "linux")]
4055 use crate::test_support::is_dma_available;
4056 use crate::{CPUProcessor, Rotation};
4057 use edgefirst_tensor::{TensorMapTrait, TensorMemory, TensorTrait};
4058 use image::buffer::ConvertBuffer;
4059
4060 fn convert_img(
4066 proc: &mut dyn ImageProcessorTrait,
4067 src: TensorDyn,
4068 dst: TensorDyn,
4069 rotation: Rotation,
4070 flip: Flip,
4071 crop: Crop,
4072 ) -> (Result<()>, TensorDyn, TensorDyn) {
4073 let src_fourcc = src.format().unwrap();
4074 let dst_fourcc = dst.format().unwrap();
4075 let src_dyn = src;
4076 let mut dst_dyn = dst;
4077 let result = proc.convert(
4078 &src_dyn,
4079 &mut dst_dyn,
4080 Some(
4081 ConvertOptions::new()
4082 .rotation(rotation)
4083 .flip(flip)
4084 .crop(crop),
4085 ),
4086 );
4087 let src_back = {
4088 let mut __t = src_dyn.into_u8().unwrap();
4089 __t.set_format(src_fourcc).unwrap();
4090 TensorDyn::from(__t)
4091 };
4092 let dst_back = {
4093 let mut __t = dst_dyn.into_u8().unwrap();
4094 __t.set_format(dst_fourcc).unwrap();
4095 TensorDyn::from(__t)
4096 };
4097 (result, src_back, dst_back)
4098 }
4099
4100 #[ctor::ctor(unsafe)]
4101 fn init() {
4102 env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
4103 }
4104
4105 macro_rules! function {
4106 () => {{
4107 fn f() {}
4108 fn type_name_of<T>(_: T) -> &'static str {
4109 std::any::type_name::<T>()
4110 }
4111 let name = type_name_of(f);
4112
4113 match &name[..name.len() - 3].rfind(':') {
4115 Some(pos) => &name[pos + 1..name.len() - 3],
4116 None => &name[..name.len() - 3],
4117 }
4118 }};
4119 }
4120
4121 #[test]
4134 fn batch_view_dst_tiles_match_standalone() {
4135 let mut proc = match ImageProcessor::new() {
4136 Ok(p) => p,
4137 Err(e) => {
4138 crate::test_support::report_skip(&format!(
4139 "{} — ImageProcessor init failed ({e:?})",
4140 function!()
4141 ));
4142 return;
4143 }
4144 };
4145 let n = 3usize;
4146 let (w, h) = (32usize, 24usize);
4147 let colors: [[u8; 4]; 3] = [[210, 40, 40, 255], [40, 210, 40, 255], [40, 40, 210, 255]];
4148 let make_src = |c: [u8; 4]| -> TensorDyn {
4149 let bytes: Vec<u8> = c.iter().copied().cycle().take(w * h * 4).collect();
4150 load_bytes_to_tensor(w, h, PixelFormat::Rgba, Some(TensorMemory::Mem), &bytes).unwrap()
4151 };
4152 let Some(parent) = crate::test_support::dma_allocated(
4155 TensorDyn::image(
4156 w,
4157 n * h,
4158 PixelFormat::Rgba,
4159 DType::U8,
4160 Some(TensorMemory::DmaBuf),
4161 edgefirst_tensor::CpuAccess::ReadWrite,
4162 ),
4163 "tall DMA destination",
4164 ) else {
4165 return;
4166 };
4167
4168 for (i, &c) in colors.iter().enumerate().take(n) {
4170 let mut tile = parent.view(Region::new(0, i * h, w, h)).unwrap();
4171 proc.convert_deferred(&make_src(c), &mut tile, None)
4172 .unwrap_or_else(|e| panic!("convert_deferred tile {i}: {e:?}"));
4173 }
4174 proc.flush().unwrap();
4175
4176 for (i, &c) in colors.iter().enumerate().take(n) {
4177 let mut solo = TensorDyn::image(
4179 w,
4180 h,
4181 PixelFormat::Rgba,
4182 DType::U8,
4183 Some(TensorMemory::DmaBuf),
4184 edgefirst_tensor::CpuAccess::ReadWrite,
4185 )
4186 .unwrap();
4187 proc.convert(&make_src(c), &mut solo, None).unwrap();
4188
4189 let band = parent.view(Region::new(0, i * h, w, h)).unwrap();
4190 let band_bytes = band
4191 .as_typed::<u8>()
4192 .unwrap()
4193 .map()
4194 .unwrap()
4195 .as_slice()
4196 .to_vec();
4197 let solo_bytes = solo
4198 .as_typed::<u8>()
4199 .unwrap()
4200 .map()
4201 .unwrap()
4202 .as_slice()
4203 .to_vec();
4204 assert_eq!(
4205 band_bytes, solo_bytes,
4206 "tile {i}: band differs from standalone convert (placement or sibling wipe)"
4207 );
4208 assert!(
4209 band_bytes.as_chunks::<4>().0.iter().all(|&p| p == c),
4210 "tile {i}: band is not the expected solid color {c:?} (sibling wipe?)"
4211 );
4212 }
4213 }
4214
4215 #[cfg(test)]
4219 fn gradient_frame(w: usize, h: usize) -> TensorDyn {
4220 let mut bytes = vec![0u8; w * h * 4];
4221 for y in 0..h {
4222 for x in 0..w {
4223 let i = (y * w + x) * 4;
4224 bytes[i] = x as u8;
4225 bytes[i + 1] = y as u8;
4226 bytes[i + 2] = (x ^ y) as u8;
4227 bytes[i + 3] = 255;
4228 }
4229 }
4230 load_bytes_to_tensor(w, h, PixelFormat::Rgba, Some(TensorMemory::Mem), &bytes).unwrap()
4231 }
4232
4233 #[test]
4237 fn tile_into_cpu_distinct_content_parity() {
4238 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
4239 backend: ComputeBackend::Cpu,
4240 ..Default::default()
4241 })
4242 .expect("the CPU backend is always available");
4243 let (fw, fh) = (96usize, 64usize);
4244 let src = gradient_frame(fw, fh);
4245 let cfg = TilingConfig::new(32, 32).with_overlap(0.0); let n = tile_grid(fh, fw, 32, 32, 0.0).len();
4247 assert_eq!(n, 6);
4248
4249 let mut parent = proc
4250 .alloc_tile_batch(
4251 n,
4252 &cfg,
4253 PixelFormat::Rgba,
4254 DType::U8,
4255 Some(TensorMemory::Mem),
4256 edgefirst_tensor::CpuAccess::ReadWrite,
4257 )
4258 .unwrap();
4259 let placements = proc.tile_into(&src, &mut parent, &cfg).unwrap();
4260 assert_eq!(placements.len(), n);
4261
4262 for p in &placements {
4263 let source = Region::new(
4264 p.origin.0 as usize,
4265 p.origin.1 as usize,
4266 p.crop_size.0 as usize,
4267 p.crop_size.1 as usize,
4268 );
4269 let mut solo = TensorDyn::image(
4270 32,
4271 32,
4272 PixelFormat::Rgba,
4273 DType::U8,
4274 Some(TensorMemory::Mem),
4275 edgefirst_tensor::CpuAccess::ReadWrite,
4276 )
4277 .unwrap();
4278 proc.convert(
4279 &src,
4280 &mut solo,
4281 Some(
4282 crate::ConvertOptions::new().crop(
4283 Crop::default()
4284 .with_source(Some(source))
4285 .with_fit(Fit::Stretch),
4286 ),
4287 ),
4288 )
4289 .unwrap();
4290 let band = parent.view(Region::new(0, p.index * 32, 32, 32)).unwrap();
4291 let band_bytes = band
4292 .as_typed::<u8>()
4293 .unwrap()
4294 .map()
4295 .unwrap()
4296 .as_slice()
4297 .to_vec();
4298 let solo_bytes = solo
4299 .as_typed::<u8>()
4300 .unwrap()
4301 .map()
4302 .unwrap()
4303 .as_slice()
4304 .to_vec();
4305 assert_eq!(
4306 band_bytes, solo_bytes,
4307 "tile {} band differs from standalone crop-convert",
4308 p.index
4309 );
4310 }
4311 }
4312
4313 #[test]
4316 fn tile_one_matches_tile_into_band() {
4317 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
4318 backend: ComputeBackend::Cpu,
4319 ..Default::default()
4320 })
4321 .expect("the CPU backend is always available");
4322 let (fw, fh) = (96usize, 64usize);
4323 let src = gradient_frame(fw, fh);
4324 let cfg = TilingConfig::new(32, 32).with_overlap(0.0);
4325 let n = tile_grid(fh, fw, 32, 32, 0.0).len();
4326
4327 let mut parent = proc
4328 .alloc_tile_batch(
4329 n,
4330 &cfg,
4331 PixelFormat::Rgba,
4332 DType::U8,
4333 Some(TensorMemory::Mem),
4334 edgefirst_tensor::CpuAccess::ReadWrite,
4335 )
4336 .unwrap();
4337 proc.tile_into(&src, &mut parent, &cfg).unwrap();
4338
4339 let plan = proc.plan_tiles(fw, fh, &cfg).unwrap();
4340 for p in &plan {
4341 let mut slot = TensorDyn::image(
4342 32,
4343 32,
4344 PixelFormat::Rgba,
4345 DType::U8,
4346 Some(TensorMemory::Mem),
4347 edgefirst_tensor::CpuAccess::ReadWrite,
4348 )
4349 .unwrap();
4350 proc.tile_one(&src, &mut slot, p, &cfg).unwrap();
4351 proc.flush().unwrap();
4352 let band = parent.view(Region::new(0, p.index * 32, 32, 32)).unwrap();
4353 let slot_bytes = slot
4354 .as_typed::<u8>()
4355 .unwrap()
4356 .map()
4357 .unwrap()
4358 .as_slice()
4359 .to_vec();
4360 let band_bytes = band
4361 .as_typed::<u8>()
4362 .unwrap()
4363 .map()
4364 .unwrap()
4365 .as_slice()
4366 .to_vec();
4367 assert_eq!(
4368 slot_bytes, band_bytes,
4369 "tile {} stream != batch band",
4370 p.index
4371 );
4372 }
4373 }
4374
4375 #[test]
4382 fn tile_into_auto_dma_parity() {
4383 let mut proc = match ImageProcessor::new() {
4384 Ok(p) => p,
4385 Err(e) => {
4386 crate::test_support::report_skip(&format!(
4387 "{} — ImageProcessor init failed ({e:?})",
4388 function!()
4389 ));
4390 return;
4391 }
4392 };
4393 let (fw, fh) = (96usize, 64usize);
4394 let src = gradient_frame(fw, fh);
4395 let cfg = TilingConfig::new(32, 32).with_overlap(0.0);
4396 let n = tile_grid(fh, fw, 32, 32, 0.0).len();
4397
4398 let Some(mut parent) = crate::test_support::dma_allocated(
4399 proc.alloc_tile_batch(
4400 n,
4401 &cfg,
4402 PixelFormat::Rgba,
4403 DType::U8,
4404 Some(TensorMemory::DmaBuf),
4405 edgefirst_tensor::CpuAccess::ReadWrite,
4406 ),
4407 "tall DMA parent",
4408 ) else {
4409 return;
4410 };
4411 let placements = proc.tile_into(&src, &mut parent, &cfg).unwrap();
4412
4413 for p in &placements {
4414 let source = Region::new(
4415 p.origin.0 as usize,
4416 p.origin.1 as usize,
4417 p.crop_size.0 as usize,
4418 p.crop_size.1 as usize,
4419 );
4420 let mut solo = TensorDyn::image(
4421 32,
4422 32,
4423 PixelFormat::Rgba,
4424 DType::U8,
4425 Some(TensorMemory::DmaBuf),
4426 edgefirst_tensor::CpuAccess::ReadWrite,
4427 )
4428 .unwrap();
4429 proc.convert(
4430 &src,
4431 &mut solo,
4432 Some(
4433 crate::ConvertOptions::new().crop(
4434 Crop::default()
4435 .with_source(Some(source))
4436 .with_fit(Fit::Stretch),
4437 ),
4438 ),
4439 )
4440 .unwrap();
4441 let band = parent.view(Region::new(0, p.index * 32, 32, 32)).unwrap();
4442 compare_images(
4448 &band,
4449 &solo,
4450 0.98,
4451 &format!("{}_tile{}", function!(), p.index),
4452 );
4453 }
4454 }
4455
4456 #[test]
4458 fn tile_into_undersized_dst_errors() {
4459 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
4460 backend: ComputeBackend::Cpu,
4461 ..Default::default()
4462 })
4463 .expect("the CPU backend is always available");
4464 let (fw, fh) = (96usize, 64usize);
4465 let src = gradient_frame(fw, fh);
4466 let cfg = TilingConfig::new(32, 32).with_overlap(0.0); let mut small = TensorDyn::image(
4469 32,
4470 2 * 32,
4471 PixelFormat::Rgba,
4472 DType::U8,
4473 Some(TensorMemory::Mem),
4474 edgefirst_tensor::CpuAccess::ReadWrite,
4475 )
4476 .unwrap();
4477 let r = proc.tile_into(&src, &mut small, &cfg);
4478 assert!(
4479 matches!(r, Err(Error::InvalidShape(_))),
4480 "expected InvalidShape, got {r:?}"
4481 );
4482 }
4483
4484 #[test]
4486 fn tiling_alloc_rejects_invalid_config() {
4487 let proc = ImageProcessor::with_config(ImageProcessorConfig {
4488 backend: ComputeBackend::Cpu,
4489 ..Default::default()
4490 })
4491 .expect("the CPU backend is always available");
4492 let bad = TilingConfig::new(0, 640);
4493 assert!(proc.plan_tiles(1920, 1080, &bad).is_err());
4494 assert!(proc
4495 .alloc_tile_batch(
4496 4,
4497 &bad,
4498 PixelFormat::Rgba,
4499 DType::U8,
4500 Some(TensorMemory::Mem),
4501 edgefirst_tensor::CpuAccess::ReadWrite,
4502 )
4503 .is_err());
4504 }
4505
4506 #[test]
4507 fn plan_tiles_metadata_4k() {
4508 let proc = ImageProcessor::with_config(ImageProcessorConfig {
4509 backend: ComputeBackend::Cpu,
4510 ..Default::default()
4511 })
4512 .expect("the CPU backend is always available");
4513 let cfg = TilingConfig::new(640, 640).with_overlap(0.2);
4514 let plan = proc.plan_tiles(3840, 2160, &cfg).unwrap();
4515 assert_eq!(plan.len(), 32);
4516 assert!(plan.iter().all(|p| p.count == 32));
4517 assert!(plan.iter().all(|p| p.crop_size == (640.0, 640.0)));
4518 assert!(plan.iter().all(|p| p.content_region.is_none())); assert!(plan.iter().all(|p| p.frame_dims == (3840.0, 2160.0)));
4520 assert_eq!(plan[0].origin, (0.0, 0.0));
4521 }
4522
4523 #[test]
4524 fn test_invalid_crop() {
4525 let src = TensorDyn::image(
4526 100,
4527 100,
4528 PixelFormat::Rgb,
4529 DType::U8,
4530 None,
4531 edgefirst_tensor::CpuAccess::ReadWrite,
4532 )
4533 .unwrap();
4534 let dst = TensorDyn::image(
4535 100,
4536 100,
4537 PixelFormat::Rgb,
4538 DType::U8,
4539 None,
4540 edgefirst_tensor::CpuAccess::ReadWrite,
4541 )
4542 .unwrap();
4543
4544 let crop = Crop::new().with_source(Some(Region::new(50, 50, 60, 60)));
4546 assert!(matches!(
4547 crop.check_crop_dyn(&src, &dst),
4548 Err(Error::CropInvalid(_))
4549 ));
4550
4551 let crop = Crop::new().with_source(Some(Region::new(0, 0, 10, 10)));
4553 assert!(crop.check_crop_dyn(&src, &dst).is_ok());
4554
4555 assert!(Crop::letterbox([0, 0, 0, 255])
4557 .check_crop_dyn(&src, &dst)
4558 .is_ok());
4559 }
4560
4561 #[test]
4562 fn test_invalid_tensor_format() -> Result<(), Error> {
4563 let mut tensor = Tensor::<u8>::new(&[720, 1280, 4, 1], None, None)?;
4565 let result = tensor.set_format(PixelFormat::Rgb);
4566 assert!(result.is_err(), "4D tensor should reject set_format");
4567
4568 let mut tensor = Tensor::<u8>::new(&[720, 1280, 4], None, None)?;
4570 let result = tensor.set_format(PixelFormat::Rgb);
4571 assert!(result.is_err(), "4-channel tensor should reject RGB format");
4572
4573 Ok(())
4574 }
4575
4576 #[test]
4577 fn test_invalid_image_file() -> Result<(), Error> {
4578 let result = crate::load_image_test_helper(&[123; 5000], None, None);
4579 assert!(
4580 matches!(result, Err(Error::Codec(_))),
4581 "unrecognised bytes should surface as Error::Codec, got {result:?}"
4582 );
4583 Ok(())
4584 }
4585
4586 #[test]
4587 fn test_invalid_jpeg_format() -> Result<(), Error> {
4588 let result = crate::load_image_test_helper(&[123; 5000], Some(PixelFormat::Yuyv), None);
4589 assert!(
4592 matches!(result, Err(Error::Codec(_))),
4593 "Yuyv target with garbage bytes should surface as Error::Codec, got {result:?}"
4594 );
4595 Ok(())
4596 }
4597
4598 #[test]
4599 fn test_load_resize_save() {
4600 let file = edgefirst_bench::testdata::read("zidane.jpg");
4601 let img = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4602 assert_eq!(img.width(), Some(1280));
4603 assert_eq!(img.height(), Some(720));
4604
4605 let dst = TensorDyn::image(
4606 640,
4607 360,
4608 PixelFormat::Rgba,
4609 DType::U8,
4610 None,
4611 edgefirst_tensor::CpuAccess::ReadWrite,
4612 )
4613 .unwrap();
4614 let mut converter = CPUProcessor::new();
4615 let (result, _img, dst) = convert_img(
4616 &mut converter,
4617 img,
4618 dst,
4619 Rotation::None,
4620 Flip::None,
4621 Crop::no_crop(),
4622 );
4623 result.unwrap();
4624 assert_eq!(dst.width(), Some(640));
4625 assert_eq!(dst.height(), Some(360));
4626
4627 crate::save_jpeg(&dst, "zidane_resized.jpg", 80).unwrap();
4628
4629 let file = std::fs::read("zidane_resized.jpg").unwrap();
4630 let img = crate::load_image_test_helper(&file, None, None).unwrap();
4633 assert_eq!(img.width(), Some(640));
4634 assert_eq!(img.height(), Some(360));
4635 assert_eq!(img.format().unwrap(), PixelFormat::Nv12);
4636 }
4637
4638 #[test]
4639 fn test_from_tensor_planar() -> Result<(), Error> {
4640 let mut tensor = Tensor::new(&[3, 720, 1280], None, None)?;
4641 tensor
4642 .map()?
4643 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.8bps"));
4644 let planar = {
4645 tensor
4646 .set_format(PixelFormat::PlanarRgb)
4647 .map_err(|e| crate::Error::Internal(e.to_string()))?;
4648 TensorDyn::from(tensor)
4649 };
4650
4651 let rbga = load_bytes_to_tensor(
4652 1280,
4653 720,
4654 PixelFormat::Rgba,
4655 None,
4656 &edgefirst_bench::testdata::read("camera720p.rgba"),
4657 )?;
4658 compare_images_convert_to_rgb(&planar, &rbga, 0.98, function!());
4659
4660 Ok(())
4661 }
4662
4663 #[test]
4664 fn test_from_tensor_invalid_format() {
4665 assert!(PixelFormat::from_fourcc(u32::from_le_bytes(*b"TEST")).is_none());
4668 }
4669
4670 #[test]
4671 #[should_panic(expected = "Failed to save planar RGB image")]
4672 fn test_save_planar() {
4673 let planar_img = load_bytes_to_tensor(
4674 1280,
4675 720,
4676 PixelFormat::PlanarRgb,
4677 None,
4678 &edgefirst_bench::testdata::read("camera720p.8bps"),
4679 )
4680 .unwrap();
4681
4682 let save_path = "/tmp/planar_rgb.jpg";
4683 crate::save_jpeg(&planar_img, save_path, 90).expect("Failed to save planar RGB image");
4684 }
4685
4686 #[test]
4687 #[should_panic(expected = "Failed to save YUYV image")]
4688 fn test_save_yuyv() {
4689 let planar_img = load_bytes_to_tensor(
4690 1280,
4691 720,
4692 PixelFormat::Yuyv,
4693 None,
4694 &edgefirst_bench::testdata::read("camera720p.yuyv"),
4695 )
4696 .unwrap();
4697
4698 let save_path = "/tmp/yuyv.jpg";
4699 crate::save_jpeg(&planar_img, save_path, 90).expect("Failed to save YUYV image");
4700 }
4701
4702 #[test]
4703 fn test_rotation_angle() {
4704 assert_eq!(Rotation::from_degrees_clockwise(0), Rotation::None);
4705 assert_eq!(Rotation::from_degrees_clockwise(90), Rotation::Clockwise90);
4706 assert_eq!(Rotation::from_degrees_clockwise(180), Rotation::Rotate180);
4707 assert_eq!(
4708 Rotation::from_degrees_clockwise(270),
4709 Rotation::CounterClockwise90
4710 );
4711 assert_eq!(Rotation::from_degrees_clockwise(360), Rotation::None);
4712 assert_eq!(Rotation::from_degrees_clockwise(450), Rotation::Clockwise90);
4713 assert_eq!(Rotation::from_degrees_clockwise(540), Rotation::Rotate180);
4714 assert_eq!(
4715 Rotation::from_degrees_clockwise(630),
4716 Rotation::CounterClockwise90
4717 );
4718 }
4719
4720 #[test]
4721 #[should_panic(expected = "rotation angle is not a multiple of 90")]
4722 fn test_rotation_angle_panic() {
4723 Rotation::from_degrees_clockwise(361);
4724 }
4725
4726 #[test]
4727 fn test_disable_env_var() -> Result<(), Error> {
4728 let _lock = acquire_env_lock();
4731
4732 let _guard = EnvGuard::snapshot(&[
4735 "EDGEFIRST_FORCE_BACKEND",
4736 "EDGEFIRST_DISABLE_GL",
4737 "EDGEFIRST_DISABLE_G2D",
4738 "EDGEFIRST_DISABLE_CPU",
4739 ]);
4740
4741 unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") };
4744
4745 #[cfg(target_os = "linux")]
4746 {
4747 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
4748 let converter = ImageProcessor::new()?;
4749 assert!(converter.g2d.is_none());
4750 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_G2D") };
4751 }
4752
4753 #[cfg(any(target_os = "linux", target_os = "windows"))]
4754 #[cfg(feature = "opengl")]
4755 {
4756 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
4757 let converter = ImageProcessor::new()?;
4758 assert!(converter.opengl.is_none());
4759 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_GL") };
4760 }
4761
4762 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
4763 let converter = ImageProcessor::new()?;
4764 assert!(converter.cpu.is_none());
4765 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_CPU") };
4766
4767 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
4769 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
4770 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
4771 let mut converter = ImageProcessor::new()?;
4772
4773 let src = TensorDyn::image(
4774 1280,
4775 720,
4776 PixelFormat::Rgba,
4777 DType::U8,
4778 None,
4779 edgefirst_tensor::CpuAccess::ReadWrite,
4780 )?;
4781 let dst = TensorDyn::image(
4782 640,
4783 360,
4784 PixelFormat::Rgba,
4785 DType::U8,
4786 None,
4787 edgefirst_tensor::CpuAccess::ReadWrite,
4788 )?;
4789 let (result, _src, _dst) = convert_img(
4790 &mut converter,
4791 src,
4792 dst,
4793 Rotation::None,
4794 Flip::None,
4795 Crop::no_crop(),
4796 );
4797 assert!(matches!(result, Err(Error::NoConverter)));
4798 Ok(())
4800 }
4801
4802 #[test]
4803 fn test_unsupported_conversion() {
4804 let src = TensorDyn::image(
4805 1280,
4806 720,
4807 PixelFormat::Nv12,
4808 DType::U8,
4809 None,
4810 edgefirst_tensor::CpuAccess::ReadWrite,
4811 )
4812 .unwrap();
4813 let dst = TensorDyn::image(
4814 640,
4815 360,
4816 PixelFormat::Nv12,
4817 DType::U8,
4818 None,
4819 edgefirst_tensor::CpuAccess::ReadWrite,
4820 )
4821 .unwrap();
4822 let mut converter = ImageProcessor::new().unwrap();
4823 let (result, _src, _dst) = convert_img(
4824 &mut converter,
4825 src,
4826 dst,
4827 Rotation::None,
4828 Flip::None,
4829 Crop::no_crop(),
4830 );
4831 log::debug!("result: {:?}", result);
4832 assert!(matches!(
4833 result,
4834 Err(Error::NotSupported(e)) if e.starts_with("Conversion from NV12 to NV12")
4835 ));
4836 }
4837
4838 #[test]
4839 fn test_load_grey() {
4840 let grey_img = crate::load_image_test_helper(
4844 &edgefirst_bench::testdata::read("grey.jpg"),
4845 Some(PixelFormat::Rgba),
4846 None,
4847 )
4848 .unwrap();
4849 assert_eq!(grey_img.width(), Some(1024));
4850 assert_eq!(grey_img.height(), Some(681));
4851
4852 let grey_but_rgb = crate::load_image_test_helper(
4858 &edgefirst_bench::testdata::read("grey-rgb.jpg"),
4859 Some(PixelFormat::Rgba),
4860 None,
4861 )
4862 .expect("odd-height colour JPEG should decode to NV12 and convert to RGBA");
4863 assert_eq!(grey_but_rgb.width(), Some(1024));
4864 assert_eq!(grey_but_rgb.height(), Some(681));
4865 }
4866
4867 #[test]
4868 fn test_new_nv12() {
4869 let nv12 = TensorDyn::image(
4870 1280,
4871 720,
4872 PixelFormat::Nv12,
4873 DType::U8,
4874 None,
4875 edgefirst_tensor::CpuAccess::ReadWrite,
4876 )
4877 .unwrap();
4878 assert_eq!(nv12.height(), Some(720));
4879 assert_eq!(nv12.width(), Some(1280));
4880 assert_eq!(nv12.format().unwrap(), PixelFormat::Nv12);
4881 assert_eq!(nv12.format().unwrap().channels(), 1);
4883 assert!(nv12.format().is_some_and(
4884 |f| f.layout() == PixelLayout::Planar || f.layout() == PixelLayout::SemiPlanar
4885 ))
4886 }
4887
4888 #[test]
4889 #[cfg(any(target_os = "linux", target_os = "windows"))]
4890 fn test_new_image_converter() {
4891 let dst_width = 640;
4892 let dst_height = 360;
4893 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4894 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4895
4896 let mut converter = ImageProcessor::new().unwrap();
4897 let converter_dst = converter
4898 .create_image(
4899 dst_width,
4900 dst_height,
4901 PixelFormat::Rgba,
4902 DType::U8,
4903 None,
4904 edgefirst_tensor::CpuAccess::ReadWrite,
4905 )
4906 .unwrap();
4907 let (result, src, converter_dst) = convert_img(
4908 &mut converter,
4909 src,
4910 converter_dst,
4911 Rotation::None,
4912 Flip::None,
4913 Crop::no_crop(),
4914 );
4915 result.unwrap();
4916
4917 let cpu_dst = TensorDyn::image(
4918 dst_width,
4919 dst_height,
4920 PixelFormat::Rgba,
4921 DType::U8,
4922 None,
4923 edgefirst_tensor::CpuAccess::ReadWrite,
4924 )
4925 .unwrap();
4926 let mut cpu_converter = CPUProcessor::new();
4927 let (result, _src, cpu_dst) = convert_img(
4928 &mut cpu_converter,
4929 src,
4930 cpu_dst,
4931 Rotation::None,
4932 Flip::None,
4933 Crop::no_crop(),
4934 );
4935 result.unwrap();
4936
4937 compare_images(&converter_dst, &cpu_dst, 0.98, function!());
4938 }
4939
4940 #[test]
4941 #[cfg(any(target_os = "linux", target_os = "windows"))]
4942 fn test_create_image_dtype_i8() {
4943 let mut converter = ImageProcessor::new().unwrap();
4944
4945 let dst = converter
4947 .create_image(
4948 320,
4949 240,
4950 PixelFormat::Rgb,
4951 DType::I8,
4952 None,
4953 edgefirst_tensor::CpuAccess::ReadWrite,
4954 )
4955 .unwrap();
4956 assert_eq!(dst.dtype(), DType::I8);
4957 assert!(dst.width() == Some(320));
4958 assert!(dst.height() == Some(240));
4959 assert_eq!(dst.format(), Some(PixelFormat::Rgb));
4960
4961 let dst_u8 = converter
4963 .create_image(
4964 320,
4965 240,
4966 PixelFormat::Rgb,
4967 DType::U8,
4968 None,
4969 edgefirst_tensor::CpuAccess::ReadWrite,
4970 )
4971 .unwrap();
4972 assert_eq!(dst_u8.dtype(), DType::U8);
4973
4974 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4976 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4977 let mut dst_i8 = converter
4978 .create_image(
4979 320,
4980 240,
4981 PixelFormat::Rgb,
4982 DType::I8,
4983 None,
4984 edgefirst_tensor::CpuAccess::ReadWrite,
4985 )
4986 .unwrap();
4987 converter.convert(&src, &mut dst_i8, None).unwrap();
4988 }
4989
4990 #[test]
4991 #[cfg(target_os = "linux")]
4992 fn test_create_image_nv12_dma_non_aligned_width() {
4993 let converter = ImageProcessor::new().unwrap();
4999
5000 let Some(img) = crate::test_support::dma_allocated(
5002 converter.create_image(
5003 100,
5004 64,
5005 PixelFormat::Nv12,
5006 DType::U8,
5007 Some(TensorMemory::DmaBuf),
5008 edgefirst_tensor::CpuAccess::ReadWrite,
5009 ),
5010 "create_image NV12 DMA non-aligned width",
5011 ) else {
5012 return;
5013 };
5014 assert_eq!(img.width(), Some(100));
5015 assert_eq!(img.height(), Some(64));
5016 assert_eq!(img.format(), Some(PixelFormat::Nv12));
5017 if let Some(stride) = img.row_stride() {
5018 assert!(
5019 stride >= 100,
5020 "NV12 row_stride {stride} must be >= the logical width (100)",
5021 );
5022 }
5023 }
5024
5025 #[test]
5026 #[ignore] fn test_crop_skip() {
5030 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5031 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5032
5033 let mut converter = ImageProcessor::new().unwrap();
5034 let converter_dst = converter
5035 .create_image(
5036 1280,
5037 720,
5038 PixelFormat::Rgba,
5039 DType::U8,
5040 None,
5041 edgefirst_tensor::CpuAccess::ReadWrite,
5042 )
5043 .unwrap();
5044 let crop = Crop::new().with_source(Some(Region::new(0, 0, 640, 640)));
5045 let (result, src, converter_dst) = convert_img(
5046 &mut converter,
5047 src,
5048 converter_dst,
5049 Rotation::None,
5050 Flip::None,
5051 crop,
5052 );
5053 result.unwrap();
5054
5055 let cpu_dst = TensorDyn::image(
5056 1280,
5057 720,
5058 PixelFormat::Rgba,
5059 DType::U8,
5060 None,
5061 edgefirst_tensor::CpuAccess::ReadWrite,
5062 )
5063 .unwrap();
5064 let mut cpu_converter = CPUProcessor::new();
5065 let (result, _src, cpu_dst) = convert_img(
5066 &mut cpu_converter,
5067 src,
5068 cpu_dst,
5069 Rotation::None,
5070 Flip::None,
5071 crop,
5072 );
5073 result.unwrap();
5074
5075 compare_images(&converter_dst, &cpu_dst, 0.99999, function!());
5076 }
5077
5078 #[test]
5079 fn test_invalid_pixel_format() {
5080 assert!(PixelFormat::from_fourcc(u32::from_le_bytes(*b"TEST")).is_none());
5083 }
5084
5085 #[cfg(target_os = "linux")]
5087 static G2D_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
5088
5089 #[cfg(target_os = "linux")]
5090 fn is_g2d_available() -> bool {
5091 crate::test_support::g2d_required_or(
5092 *G2D_AVAILABLE.get_or_init(|| G2DProcessor::new().is_ok()),
5093 )
5094 }
5095
5096 #[cfg(any(
5102 target_os = "linux",
5103 target_os = "macos",
5104 target_os = "ios",
5105 target_os = "android",
5106 target_os = "windows"
5107 ))]
5108 #[cfg(feature = "opengl")]
5109 static GL_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
5110
5111 #[cfg(any(
5112 target_os = "linux",
5113 target_os = "macos",
5114 target_os = "ios",
5115 target_os = "android",
5116 target_os = "windows"
5117 ))]
5118 #[cfg(feature = "opengl")]
5119 fn is_opengl_available() -> bool {
5121 *GL_AVAILABLE.get_or_init(|| GLProcessorThreaded::new(None).is_ok())
5122 }
5123
5124 #[test]
5136 #[cfg(feature = "opengl")]
5137 fn gl_backend_available_canary() {
5138 let require_gl = std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1");
5139 if !require_gl {
5140 crate::test_support::report_skip(&format!(
5141 "{} — HAL_TEST_REQUIRE_GL is not set to 1",
5142 function!()
5143 ));
5144 return;
5145 }
5146 #[cfg(target_os = "macos")]
5147 if std::env::var_os("HAL_TEST_ALLOW_DLOPEN_ANGLE").is_none() {
5148 crate::test_support::report_skip(&format!(
5149 "{} — ANGLE dlopen gate closed (coverage pass 1)",
5150 function!()
5151 ));
5152 return;
5153 }
5154 GLProcessorThreaded::new(None).expect(
5155 "HAL_TEST_REQUIRE_GL=1 but the GL backend failed to initialize — \
5156 check the ANGLE install/re-sign step and binary entitlements \
5157 (macOS) or the EGL stack (Linux)",
5158 );
5159 }
5160
5161 #[test]
5162 fn test_load_jpeg_with_exif() {
5163 use edgefirst_codec::peek_info;
5164
5165 let file = edgefirst_bench::testdata::read("zidane_rotated_exif.jpg").to_vec();
5170 let info = peek_info(&file).unwrap();
5171 assert_eq!(info.rotation_degrees, 90);
5172 assert!(!info.flip_horizontal);
5173
5174 let loaded = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5175 assert_eq!(loaded.width(), Some(1280));
5177 assert_eq!(loaded.height(), Some(720));
5178
5179 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5182 let cpu_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5183
5184 let rotation = Rotation::from_degrees_clockwise(info.rotation_degrees as usize);
5185 let (dst_width, dst_height) = (cpu_src.height().unwrap(), cpu_src.width().unwrap());
5186
5187 let cpu_dst = TensorDyn::image(
5188 dst_width,
5189 dst_height,
5190 PixelFormat::Rgba,
5191 DType::U8,
5192 None,
5193 edgefirst_tensor::CpuAccess::ReadWrite,
5194 )
5195 .unwrap();
5196 let mut cpu_converter = CPUProcessor::new();
5197
5198 let loaded_rotated = TensorDyn::image(
5201 dst_width,
5202 dst_height,
5203 PixelFormat::Rgba,
5204 DType::U8,
5205 None,
5206 edgefirst_tensor::CpuAccess::ReadWrite,
5207 )
5208 .unwrap();
5209 let (r0, _loaded, loaded_rotated) = convert_img(
5210 &mut cpu_converter,
5211 loaded,
5212 loaded_rotated,
5213 rotation,
5214 Flip::None,
5215 Crop::no_crop(),
5216 );
5217 r0.unwrap();
5218
5219 let (result, _cpu_src, cpu_dst) = convert_img(
5220 &mut cpu_converter,
5221 cpu_src,
5222 cpu_dst,
5223 rotation,
5224 Flip::None,
5225 Crop::no_crop(),
5226 );
5227 result.unwrap();
5228
5229 compare_images(&loaded_rotated, &cpu_dst, 0.98, function!());
5230 }
5231
5232 #[test]
5233 fn test_load_png_with_exif() {
5234 use edgefirst_codec::peek_info;
5235
5236 let file = edgefirst_bench::testdata::read("zidane_rotated_exif_180.png").to_vec();
5239 let info = peek_info(&file).unwrap();
5240 assert_eq!(info.rotation_degrees, 180);
5241 assert!(!info.flip_horizontal);
5242
5243 let loaded = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5244 assert_eq!(loaded.height(), Some(720));
5246 assert_eq!(loaded.width(), Some(1280));
5247
5248 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5253 let cpu_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5254
5255 let rotation = Rotation::from_degrees_clockwise(info.rotation_degrees as usize);
5256 let cpu_dst = TensorDyn::image(
5257 1280,
5258 720,
5259 PixelFormat::Rgba,
5260 DType::U8,
5261 None,
5262 edgefirst_tensor::CpuAccess::ReadWrite,
5263 )
5264 .unwrap();
5265 let mut cpu_converter = CPUProcessor::new();
5266
5267 let (result, _cpu_src, cpu_dst) = convert_img(
5268 &mut cpu_converter,
5269 cpu_src,
5270 cpu_dst,
5271 rotation,
5272 Flip::None,
5273 Crop::no_crop(),
5274 );
5275 result.unwrap();
5276
5277 let loaded_rotated = TensorDyn::image(
5280 1280,
5281 720,
5282 PixelFormat::Rgba,
5283 DType::U8,
5284 None,
5285 edgefirst_tensor::CpuAccess::ReadWrite,
5286 )
5287 .unwrap();
5288 let (r0, _loaded, loaded_rotated) = convert_img(
5289 &mut cpu_converter,
5290 loaded,
5291 loaded_rotated,
5292 rotation,
5293 Flip::None,
5294 Crop::no_crop(),
5295 );
5296 r0.unwrap();
5297
5298 compare_images(&loaded_rotated, &cpu_dst, 0.95, function!());
5303 }
5304
5305 #[cfg(target_os = "linux")]
5311 fn make_rgb_jpeg(width: u32, height: u32) -> Vec<u8> {
5312 let mut bytes = Vec::with_capacity((width * height * 3) as usize);
5313 for y in 0..height {
5314 for x in 0..width {
5315 bytes.push(((x + y) & 0xFF) as u8);
5316 bytes.push(((x.wrapping_mul(3)) & 0xFF) as u8);
5317 bytes.push(((y.wrapping_mul(5)) & 0xFF) as u8);
5318 }
5319 }
5320 let mut out = Vec::new();
5321 let encoder = jpeg_encoder::Encoder::new(&mut out, 85);
5322 encoder
5323 .encode(
5324 &bytes,
5325 width as u16,
5326 height as u16,
5327 jpeg_encoder::ColorType::Rgb,
5328 )
5329 .expect("jpeg-encoder must succeed on trivial input");
5330 out
5331 }
5332
5333 #[test]
5342 #[cfg(target_os = "linux")]
5343 #[cfg(feature = "opengl")]
5344 fn test_convert_rgba_non_4_aligned_width_end_to_end() {
5345 use crate::test_support::is_dma_available;
5346 if !is_dma_available() {
5347 crate::test_support::report_skip(
5348 "test_convert_rgba_non_4_aligned_width_end_to_end — DMA not available",
5349 );
5350 return;
5351 }
5352 let jpeg = make_rgb_jpeg(375, 333);
5356 let src_gl = crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), None).unwrap();
5357 assert_eq!(src_gl.width(), Some(375));
5358 let stride = src_gl.row_stride().unwrap();
5360 assert_eq!(stride, 1536, "expected padded pitch 1536, got {stride}");
5361
5362 let mut gl_proc = ImageProcessor::new().unwrap();
5364 let gl_dst = gl_proc
5365 .create_image(
5366 640,
5367 640,
5368 PixelFormat::Rgba,
5369 DType::U8,
5370 None,
5371 edgefirst_tensor::CpuAccess::ReadWrite,
5372 )
5373 .unwrap();
5374 let (r_gl, _src_gl, gl_dst) = convert_img(
5375 &mut gl_proc,
5376 src_gl,
5377 gl_dst,
5378 Rotation::None,
5379 Flip::None,
5380 Crop::no_crop(),
5381 );
5382 r_gl.expect("GL-backed convert must succeed for 375x333 Rgba src");
5383
5384 let src_cpu =
5389 crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), Some(TensorMemory::Mem))
5390 .unwrap();
5391 let mut cpu_proc = ImageProcessor::with_config(ImageProcessorConfig {
5392 backend: ComputeBackend::Cpu,
5393 ..Default::default()
5394 })
5395 .unwrap();
5396 let cpu_dst = TensorDyn::image(
5397 640,
5398 640,
5399 PixelFormat::Rgba,
5400 DType::U8,
5401 Some(TensorMemory::Mem),
5402 edgefirst_tensor::CpuAccess::ReadWrite,
5403 )
5404 .unwrap();
5405 let (r_cpu, _src_cpu, cpu_dst) = convert_img(
5406 &mut cpu_proc,
5407 src_cpu,
5408 cpu_dst,
5409 Rotation::None,
5410 Flip::None,
5411 Crop::no_crop(),
5412 );
5413 r_cpu.unwrap();
5414
5415 compare_images(&gl_dst, &cpu_dst, 0.95, function!());
5419 }
5420
5421 #[test]
5428 #[cfg(target_os = "linux")]
5429 fn test_load_jpeg_rgba_non_aligned_pitch_padded_dma() {
5430 use crate::test_support::is_dma_available;
5431 if !is_dma_available() {
5432 crate::test_support::report_skip(
5433 "test_load_jpeg_rgba_non_aligned_pitch_padded_dma — DMA not available",
5434 );
5435 return;
5436 }
5437 for &w in &[500u32, 612, 428] {
5441 let jpeg = make_rgb_jpeg(w, 333);
5442 let loaded =
5443 crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), None).unwrap();
5444 let natural = (w as usize) * 4;
5445 let aligned = crate::align_pitch_bytes_to_gpu_alignment(natural).unwrap();
5446 assert!(
5447 aligned > natural,
5448 "test sanity: width {w} should be unaligned"
5449 );
5450 let stride = loaded
5451 .row_stride()
5452 .expect("padded DMA path must set an explicit row_stride — regression if None");
5453 assert_eq!(
5454 stride, aligned,
5455 "width {w}: expected padded stride {aligned}, got {stride} \
5456 (regression: pitch-padding branch skipped?)"
5457 );
5458 let eff = loaded.effective_row_stride().unwrap();
5459 assert_eq!(
5460 eff, aligned,
5461 "effective_row_stride must match stored stride"
5462 );
5463 assert_eq!(loaded.width(), Some(w as usize));
5464 assert_eq!(loaded.height(), Some(333));
5465 }
5466 }
5467
5468 #[test]
5477 #[cfg(target_os = "linux")]
5478 fn test_padded_dma_pitch_for_respects_memory_choice() {
5479 use crate::test_support::is_dma_available;
5480 use edgefirst_tensor::TensorMemory;
5481
5482 let unaligned_w = 500;
5485
5486 assert_eq!(
5488 crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &Some(TensorMemory::Mem),),
5489 None,
5490 "Mem must never trigger DMA padding"
5491 );
5492 assert_eq!(
5493 crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &Some(TensorMemory::Shm),),
5494 None,
5495 "Shm must never trigger DMA padding"
5496 );
5497
5498 assert_eq!(
5503 crate::padded_dma_pitch_for(
5504 PixelFormat::Rgba,
5505 unaligned_w,
5506 &Some(TensorMemory::DmaBuf),
5507 ),
5508 Some(2048),
5509 "explicit Dma must pad regardless of runtime DMA availability"
5510 );
5511
5512 let none_result = crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &None);
5516 if is_dma_available() {
5517 assert_eq!(
5518 none_result,
5519 Some(2048),
5520 "memory=None + DMA available → pad (will route through DMA)"
5521 );
5522 } else {
5523 assert_eq!(
5524 none_result, None,
5525 "memory=None + DMA unavailable → must NOT pad (would force \
5526 image_with_stride into a DMA-only allocation that fails). \
5527 Regression: padded_dma_pitch_for ignored is_dma_available()."
5528 );
5529 }
5530 }
5531
5532 fn make_grey_png(width: u32, height: u32) -> Vec<u8> {
5536 let mut bytes = Vec::with_capacity((width * height) as usize);
5537 for y in 0..height {
5538 for x in 0..width {
5539 bytes.push(((x + y) & 0xFF) as u8);
5540 }
5541 }
5542 let img = image::GrayImage::from_vec(width, height, bytes).unwrap();
5543 let mut buf = Vec::new();
5544 img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)
5545 .unwrap();
5546 buf
5547 }
5548
5549 #[test]
5554 #[cfg(target_os = "linux")]
5555 fn test_load_png_grey_misaligned_width_dma() {
5556 use crate::test_support::is_dma_available;
5557 if !is_dma_available() {
5558 crate::test_support::report_skip(
5559 "test_load_png_grey_misaligned_width_dma — DMA not available",
5560 );
5561 return;
5562 }
5563 let png = make_grey_png(612, 388);
5564 let loaded = crate::load_image_test_helper(&png, Some(PixelFormat::Grey), None).unwrap();
5565 assert_eq!(loaded.width(), Some(612));
5566 assert_eq!(loaded.height(), Some(388));
5567 assert_eq!(loaded.format(), Some(PixelFormat::Grey));
5568
5569 let map = loaded.as_typed::<u8>().unwrap().map().unwrap();
5572 let stride = loaded.row_stride().unwrap_or(612);
5573 assert!(stride >= 612);
5574 let bytes: &[u8] = ↦
5575 for y in 0..388usize {
5576 for x in 0..612usize {
5577 let expected = ((x + y) & 0xFF) as u8;
5578 let got = bytes[y * stride + x];
5579 assert_eq!(
5580 got, expected,
5581 "grey png mismatch at ({x},{y}): got {got} expected {expected}"
5582 );
5583 }
5584 }
5585 }
5586
5587 #[test]
5591 fn test_load_png_grey_mem() {
5592 use edgefirst_tensor::TensorMemory;
5593 let png = make_grey_png(612, 100);
5594 let loaded =
5595 crate::load_image_test_helper(&png, Some(PixelFormat::Grey), Some(TensorMemory::Mem))
5596 .unwrap();
5597 assert_eq!(loaded.width(), Some(612));
5598 assert_eq!(loaded.height(), Some(100));
5599 assert_eq!(loaded.format(), Some(PixelFormat::Grey));
5600 let map = loaded.as_typed::<u8>().unwrap().map().unwrap();
5601 let bytes: &[u8] = ↦
5602 assert_eq!(bytes.len(), 612 * 100);
5604 for y in 0..100 {
5605 for x in 0..612 {
5606 assert_eq!(bytes[y * 612 + x], ((x + y) & 0xFF) as u8);
5607 }
5608 }
5609 }
5610
5611 #[test]
5615 fn test_load_png_grey_to_rgb_mem() {
5616 use edgefirst_tensor::TensorMemory;
5617 let png = make_grey_png(620, 240);
5618 let loaded =
5619 crate::load_image_test_helper(&png, Some(PixelFormat::Rgb), Some(TensorMemory::Mem))
5620 .unwrap();
5621 assert_eq!(loaded.width(), Some(620));
5622 assert_eq!(loaded.height(), Some(240));
5623 assert_eq!(loaded.format(), Some(PixelFormat::Rgb));
5624
5625 let map = loaded.as_typed::<u8>().unwrap().map().unwrap();
5627 let bytes: &[u8] = ↦
5628 for (x, y) in [(0usize, 0usize), (100, 50), (619, 239)] {
5629 let expected = ((x + y) & 0xFF) as u8;
5630 let off = (y * 620 + x) * 3;
5631 assert_eq!(bytes[off], expected, "R@{x},{y}");
5632 assert_eq!(bytes[off + 1], expected, "G@{x},{y}");
5633 assert_eq!(bytes[off + 2], expected, "B@{x},{y}");
5634 }
5635 }
5636
5637 #[test]
5638 #[cfg(target_os = "linux")]
5639 fn test_g2d_resize() {
5640 if !is_g2d_available() {
5641 crate::test_support::report_skip(
5642 "test_g2d_resize - G2D library (libg2d.so.2) not available",
5643 );
5644 return;
5645 }
5646 if !is_dma_available() {
5647 crate::test_support::report_skip("test_g2d_resize - DMA memory allocation not available (permission denied or no DMA-BUF support)");
5648 return;
5649 }
5650
5651 let dst_width = 640;
5652 let dst_height = 360;
5653 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5654 let src = crate::load_image_test_helper(
5655 &file,
5656 Some(PixelFormat::Rgba),
5657 Some(TensorMemory::DmaBuf),
5658 )
5659 .unwrap();
5660
5661 let g2d_dst = TensorDyn::image(
5662 dst_width,
5663 dst_height,
5664 PixelFormat::Rgba,
5665 DType::U8,
5666 Some(TensorMemory::DmaBuf),
5667 edgefirst_tensor::CpuAccess::ReadWrite,
5668 )
5669 .unwrap();
5670 let mut g2d_converter = G2DProcessor::new().unwrap();
5671 let (result, src, g2d_dst) = convert_img(
5672 &mut g2d_converter,
5673 src,
5674 g2d_dst,
5675 Rotation::None,
5676 Flip::None,
5677 Crop::no_crop(),
5678 );
5679 result.unwrap();
5680
5681 let cpu_dst = TensorDyn::image(
5682 dst_width,
5683 dst_height,
5684 PixelFormat::Rgba,
5685 DType::U8,
5686 None,
5687 edgefirst_tensor::CpuAccess::ReadWrite,
5688 )
5689 .unwrap();
5690 let mut cpu_converter = CPUProcessor::new();
5691 let (result, _src, cpu_dst) = convert_img(
5692 &mut cpu_converter,
5693 src,
5694 cpu_dst,
5695 Rotation::None,
5696 Flip::None,
5697 Crop::no_crop(),
5698 );
5699 result.unwrap();
5700
5701 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
5707 }
5708
5709 #[test]
5710 #[cfg(any(
5711 target_os = "linux",
5712 target_os = "macos",
5713 target_os = "ios",
5714 target_os = "android",
5715 target_os = "windows"
5716 ))]
5717 #[cfg(feature = "opengl")]
5718 fn test_opengl_resize() {
5719 if !is_opengl_available() {
5720 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
5721 return;
5722 }
5723
5724 let dst_width = 640;
5725 let dst_height = 360;
5726 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5727 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5728
5729 let cpu_dst = TensorDyn::image(
5730 dst_width,
5731 dst_height,
5732 PixelFormat::Rgba,
5733 DType::U8,
5734 None,
5735 edgefirst_tensor::CpuAccess::ReadWrite,
5736 )
5737 .unwrap();
5738 let mut cpu_converter = CPUProcessor::new();
5739 let (result, src, cpu_dst) = convert_img(
5740 &mut cpu_converter,
5741 src,
5742 cpu_dst,
5743 Rotation::None,
5744 Flip::None,
5745 Crop::no_crop(),
5746 );
5747 result.unwrap();
5748
5749 let mut src = src;
5750 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5751
5752 for _ in 0..5 {
5753 let gl_dst = TensorDyn::image(
5754 dst_width,
5755 dst_height,
5756 PixelFormat::Rgba,
5757 DType::U8,
5758 None,
5759 edgefirst_tensor::CpuAccess::ReadWrite,
5760 )
5761 .unwrap();
5762 let (result, src_back, gl_dst) = convert_img(
5763 &mut gl_converter,
5764 src,
5765 gl_dst,
5766 Rotation::None,
5767 Flip::None,
5768 Crop::no_crop(),
5769 );
5770 result.unwrap();
5771 src = src_back;
5772
5773 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5774 }
5775 }
5776
5777 #[test]
5778 #[cfg(any(
5779 target_os = "linux",
5780 target_os = "macos",
5781 target_os = "ios",
5782 target_os = "android",
5783 target_os = "windows"
5784 ))]
5785 #[cfg(feature = "opengl")]
5786 fn test_opengl_10_threads() {
5787 if !is_opengl_available() {
5788 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
5789 return;
5790 }
5791
5792 let handles: Vec<_> = (0..10)
5793 .map(|i| {
5794 std::thread::Builder::new()
5795 .name(format!("Thread {i}"))
5796 .spawn(test_opengl_resize)
5797 .unwrap()
5798 })
5799 .collect();
5800 handles.into_iter().for_each(|h| {
5801 if let Err(e) = h.join() {
5802 std::panic::resume_unwind(e)
5803 }
5804 });
5805 }
5806
5807 #[test]
5808 #[cfg(any(
5809 target_os = "linux",
5810 target_os = "macos",
5811 target_os = "ios",
5812 target_os = "android",
5813 target_os = "windows"
5814 ))]
5815 #[cfg(feature = "opengl")]
5816 fn test_opengl_grey() {
5817 if !is_opengl_available() {
5818 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
5819 return;
5820 }
5821
5822 let img = crate::load_image_test_helper(
5823 &edgefirst_bench::testdata::read("grey.jpg"),
5824 Some(PixelFormat::Grey),
5825 None,
5826 )
5827 .unwrap();
5828
5829 let gl_dst = TensorDyn::image(
5830 640,
5831 640,
5832 PixelFormat::Grey,
5833 DType::U8,
5834 None,
5835 edgefirst_tensor::CpuAccess::ReadWrite,
5836 )
5837 .unwrap();
5838 let cpu_dst = TensorDyn::image(
5839 640,
5840 640,
5841 PixelFormat::Grey,
5842 DType::U8,
5843 None,
5844 edgefirst_tensor::CpuAccess::ReadWrite,
5845 )
5846 .unwrap();
5847
5848 let mut converter = CPUProcessor::new();
5849
5850 let (result, img, cpu_dst) = convert_img(
5851 &mut converter,
5852 img,
5853 cpu_dst,
5854 Rotation::None,
5855 Flip::None,
5856 Crop::no_crop(),
5857 );
5858 result.unwrap();
5859
5860 let mut gl = GLProcessorThreaded::new(None).unwrap();
5861 let (result, _img, gl_dst) = convert_img(
5862 &mut gl,
5863 img,
5864 gl_dst,
5865 Rotation::None,
5866 Flip::None,
5867 Crop::no_crop(),
5868 );
5869 result.unwrap();
5870
5871 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5872 }
5873
5874 #[test]
5875 #[cfg(target_os = "linux")]
5876 fn test_g2d_src_crop() {
5877 if !is_g2d_available() {
5878 crate::test_support::report_skip(
5879 "test_g2d_src_crop - G2D library (libg2d.so.2) not available",
5880 );
5881 return;
5882 }
5883 if !is_dma_available() {
5884 crate::test_support::report_skip("test_g2d_src_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)");
5885 return;
5886 }
5887
5888 let dst_width = 640;
5889 let dst_height = 640;
5890 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5891 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5892
5893 let cpu_dst = TensorDyn::image(
5894 dst_width,
5895 dst_height,
5896 PixelFormat::Rgba,
5897 DType::U8,
5898 None,
5899 edgefirst_tensor::CpuAccess::ReadWrite,
5900 )
5901 .unwrap();
5902 let mut cpu_converter = CPUProcessor::new();
5903 let crop = Crop::new().with_source(Some(Region::new(0, 0, 640, 360)));
5904 let (result, src, cpu_dst) = convert_img(
5905 &mut cpu_converter,
5906 src,
5907 cpu_dst,
5908 Rotation::None,
5909 Flip::None,
5910 crop,
5911 );
5912 result.unwrap();
5913
5914 let g2d_dst = TensorDyn::image(
5915 dst_width,
5916 dst_height,
5917 PixelFormat::Rgba,
5918 DType::U8,
5919 None,
5920 edgefirst_tensor::CpuAccess::ReadWrite,
5921 )
5922 .unwrap();
5923 let mut g2d_converter = G2DProcessor::new().unwrap();
5924 let (result, _src, g2d_dst) = convert_img(
5925 &mut g2d_converter,
5926 src,
5927 g2d_dst,
5928 Rotation::None,
5929 Flip::None,
5930 crop,
5931 );
5932 result.unwrap();
5933
5934 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
5940 }
5941
5942 #[test]
5943 #[cfg(target_os = "linux")]
5944 fn test_g2d_dst_crop() {
5945 if !is_g2d_available() {
5946 crate::test_support::report_skip(
5947 "test_g2d_dst_crop - G2D library (libg2d.so.2) not available",
5948 );
5949 return;
5950 }
5951 if !is_dma_available() {
5952 crate::test_support::report_skip("test_g2d_dst_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)");
5953 return;
5954 }
5955
5956 let dst_width = 640;
5957 let dst_height = 640;
5958 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5959 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5960
5961 let cpu_dst = TensorDyn::image(
5962 dst_width,
5963 dst_height,
5964 PixelFormat::Rgba,
5965 DType::U8,
5966 None,
5967 edgefirst_tensor::CpuAccess::ReadWrite,
5968 )
5969 .unwrap();
5970 let mut cpu_converter = CPUProcessor::new();
5971 let crop = Crop::new();
5972 let (result, src, cpu_dst) = convert_img(
5973 &mut cpu_converter,
5974 src,
5975 cpu_dst,
5976 Rotation::None,
5977 Flip::None,
5978 crop,
5979 );
5980 result.unwrap();
5981
5982 let g2d_dst = TensorDyn::image(
5983 dst_width,
5984 dst_height,
5985 PixelFormat::Rgba,
5986 DType::U8,
5987 None,
5988 edgefirst_tensor::CpuAccess::ReadWrite,
5989 )
5990 .unwrap();
5991 let mut g2d_converter = G2DProcessor::new().unwrap();
5992 let (result, _src, g2d_dst) = convert_img(
5993 &mut g2d_converter,
5994 src,
5995 g2d_dst,
5996 Rotation::None,
5997 Flip::None,
5998 crop,
5999 );
6000 result.unwrap();
6001
6002 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
6008 }
6009
6010 #[test]
6011 #[cfg(target_os = "linux")]
6012 fn test_g2d_all_rgba() {
6013 if !is_g2d_available() {
6014 crate::test_support::report_skip(
6015 "test_g2d_all_rgba - G2D library (libg2d.so.2) not available",
6016 );
6017 return;
6018 }
6019 if !is_dma_available() {
6020 crate::test_support::report_skip("test_g2d_all_rgba - DMA memory allocation not available (permission denied or no DMA-BUF support)");
6021 return;
6022 }
6023
6024 let dst_width = 640;
6025 let dst_height = 640;
6026 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
6027 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
6028 let src_dyn = src;
6029
6030 let mut cpu_dst = TensorDyn::image(
6031 dst_width,
6032 dst_height,
6033 PixelFormat::Rgba,
6034 DType::U8,
6035 None,
6036 edgefirst_tensor::CpuAccess::ReadWrite,
6037 )
6038 .unwrap();
6039 let mut cpu_converter = CPUProcessor::new();
6040 let mut g2d_dst = TensorDyn::image(
6041 dst_width,
6042 dst_height,
6043 PixelFormat::Rgba,
6044 DType::U8,
6045 None,
6046 edgefirst_tensor::CpuAccess::ReadWrite,
6047 )
6048 .unwrap();
6049 let mut g2d_converter = G2DProcessor::new().unwrap();
6050
6051 let crop = Crop::new().with_source(Some(Region::new(50, 120, 1024, 576)));
6052
6053 for rot in [
6054 Rotation::None,
6055 Rotation::Clockwise90,
6056 Rotation::Rotate180,
6057 Rotation::CounterClockwise90,
6058 ] {
6059 cpu_dst
6060 .as_typed::<u8>()
6061 .unwrap()
6062 .map()
6063 .unwrap()
6064 .as_mut_slice()
6065 .fill(114);
6066 g2d_dst
6067 .as_typed::<u8>()
6068 .unwrap()
6069 .map()
6070 .unwrap()
6071 .as_mut_slice()
6072 .fill(114);
6073 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
6074 let mut cpu_dst_dyn = cpu_dst;
6075 cpu_converter
6076 .convert(
6077 &src_dyn,
6078 &mut cpu_dst_dyn,
6079 Some(crate::ConvertOptions::new().crop(crop)),
6080 )
6081 .unwrap();
6082 cpu_dst = {
6083 let mut __t = cpu_dst_dyn.into_u8().unwrap();
6084 __t.set_format(PixelFormat::Rgba).unwrap();
6085 TensorDyn::from(__t)
6086 };
6087
6088 let mut g2d_dst_dyn = g2d_dst;
6089 g2d_converter
6090 .convert(
6091 &src_dyn,
6092 &mut g2d_dst_dyn,
6093 Some(crate::ConvertOptions::new().crop(crop)),
6094 )
6095 .unwrap();
6096 g2d_dst = {
6097 let mut __t = g2d_dst_dyn.into_u8().unwrap();
6098 __t.set_format(PixelFormat::Rgba).unwrap();
6099 TensorDyn::from(__t)
6100 };
6101
6102 compare_images(
6103 &g2d_dst,
6104 &cpu_dst,
6105 0.98,
6106 &format!("{} {:?} {:?}", function!(), rot, flip),
6107 );
6108 }
6109 }
6110 }
6111
6112 #[test]
6113 #[cfg(any(
6114 target_os = "linux",
6115 target_os = "macos",
6116 target_os = "ios",
6117 target_os = "android",
6118 target_os = "windows"
6119 ))]
6120 #[cfg(feature = "opengl")]
6121 fn test_opengl_src_crop() {
6122 if !is_opengl_available() {
6123 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
6124 return;
6125 }
6126
6127 let dst_width = 640;
6128 let dst_height = 360;
6129 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
6130 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
6131 let crop = Crop::new().with_source(Some(Region::new(320, 180, 1280 - 320, 720 - 180)));
6132
6133 let cpu_dst = TensorDyn::image(
6134 dst_width,
6135 dst_height,
6136 PixelFormat::Rgba,
6137 DType::U8,
6138 None,
6139 edgefirst_tensor::CpuAccess::ReadWrite,
6140 )
6141 .unwrap();
6142 let mut cpu_converter = CPUProcessor::new();
6143 let (result, src, cpu_dst) = convert_img(
6144 &mut cpu_converter,
6145 src,
6146 cpu_dst,
6147 Rotation::None,
6148 Flip::None,
6149 crop,
6150 );
6151 result.unwrap();
6152
6153 let gl_dst = TensorDyn::image(
6154 dst_width,
6155 dst_height,
6156 PixelFormat::Rgba,
6157 DType::U8,
6158 None,
6159 edgefirst_tensor::CpuAccess::ReadWrite,
6160 )
6161 .unwrap();
6162 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
6163 let (result, _src, gl_dst) = convert_img(
6164 &mut gl_converter,
6165 src,
6166 gl_dst,
6167 Rotation::None,
6168 Flip::None,
6169 crop,
6170 );
6171 result.unwrap();
6172
6173 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
6174 }
6175
6176 #[test]
6177 #[cfg(any(
6178 target_os = "linux",
6179 target_os = "macos",
6180 target_os = "ios",
6181 target_os = "android",
6182 target_os = "windows"
6183 ))]
6184 #[cfg(feature = "opengl")]
6185 fn test_opengl_dst_crop() {
6186 if !is_opengl_available() {
6187 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
6188 return;
6189 }
6190
6191 let dst_width = 640;
6192 let dst_height = 640;
6193 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
6194 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
6195
6196 let cpu_dst = TensorDyn::image(
6197 dst_width,
6198 dst_height,
6199 PixelFormat::Rgba,
6200 DType::U8,
6201 None,
6202 edgefirst_tensor::CpuAccess::ReadWrite,
6203 )
6204 .unwrap();
6205 let mut cpu_converter = CPUProcessor::new();
6206 let crop = Crop::new();
6207 let (result, src, cpu_dst) = convert_img(
6208 &mut cpu_converter,
6209 src,
6210 cpu_dst,
6211 Rotation::None,
6212 Flip::None,
6213 crop,
6214 );
6215 result.unwrap();
6216
6217 let gl_dst = TensorDyn::image(
6218 dst_width,
6219 dst_height,
6220 PixelFormat::Rgba,
6221 DType::U8,
6222 None,
6223 edgefirst_tensor::CpuAccess::ReadWrite,
6224 )
6225 .unwrap();
6226 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
6227 let (result, _src, gl_dst) = convert_img(
6228 &mut gl_converter,
6229 src,
6230 gl_dst,
6231 Rotation::None,
6232 Flip::None,
6233 crop,
6234 );
6235 result.unwrap();
6236
6237 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
6238 }
6239 #[cfg(all(target_os = "windows", feature = "opengl"))]
6243 fn synthetic_rgba(w: usize, h: usize) -> TensorDyn {
6244 let t = TensorDyn::image(
6245 w,
6246 h,
6247 PixelFormat::Rgba,
6248 DType::U8,
6249 Some(TensorMemory::Mem),
6250 edgefirst_tensor::CpuAccess::ReadWrite,
6251 )
6252 .unwrap();
6253 fill_synthetic_rgba(&t);
6254 t
6255 }
6256
6257 #[cfg(all(target_os = "windows", feature = "opengl"))]
6262 fn assert_pixels_identical(actual: &TensorDyn, expected: &TensorDyn, name: &str) {
6263 assert_eq!(actual.shape(), expected.shape(), "{name}: shapes differ");
6264 let flat = |t: &TensorDyn| {
6265 let typed = t.as_typed::<u8>().expect("u8 image");
6266 let mut out = vec![0u8; t.shape().iter().product::<usize>()];
6267 typed.copy_to_flat(&mut out).expect("compact the rows");
6268 out
6269 };
6270 let (a, e) = (flat(actual), flat(expected));
6271 let mismatches = a.iter().zip(&e).filter(|(x, y)| x != y).count();
6272 assert_eq!(
6273 mismatches,
6274 0,
6275 "{name}: {mismatches} of {} bytes differ (first rows: {:?} vs {:?})",
6276 a.len(),
6277 &a[..16.min(a.len())],
6278 &e[..16.min(e.len())]
6279 );
6280 }
6281
6282 #[cfg(all(target_os = "windows", feature = "opengl"))]
6285 fn fill_synthetic_rgba(t: &TensorDyn) {
6286 let w = t.width().unwrap();
6287 let h = t.height().unwrap();
6288 let bound = t.as_typed::<u8>().unwrap();
6289 let stride = bound.effective_row_stride().unwrap_or(w * 4);
6290 let mut m = bound.map().unwrap();
6291 let px = m.as_mut_slice();
6292 for y in 0..h {
6293 for x in 0..w {
6294 let o = y * stride + x * 4;
6295 px[o] = x as u8;
6296 px[o + 1] = y as u8;
6297 px[o + 2] = (x + y) as u8;
6298 px[o + 3] = 255;
6299 }
6300 }
6301 }
6302
6303 #[cfg(all(target_os = "windows", feature = "opengl"))]
6312 fn compare_float_images(a: &TensorDyn, b: &TensorDyn, tol: f64, what: &str) {
6313 assert_eq!(a.format(), b.format(), "{what}: pixel formats differ");
6314 assert_eq!(a.dtype(), b.dtype(), "{what}: dtypes differ");
6315 assert_eq!(a.shape(), b.shape(), "{what}: shapes differ");
6316 let values = |t: &TensorDyn| -> Vec<f64> {
6317 let elems: usize = t.shape().iter().product();
6318 match t.dtype() {
6319 DType::F32 => {
6320 let typed = t.as_typed::<f32>().expect("F32 image");
6321 let mut bytes = vec![0u8; elems * 4];
6322 typed
6323 .copy_to_flat(&mut bytes)
6324 .expect("compact the padded rows");
6325 bytes
6326 .as_chunks::<4>()
6327 .0
6328 .iter()
6329 .map(|c| f32::from_le_bytes(*c) as f64)
6330 .collect()
6331 }
6332 DType::F16 => {
6333 let typed = t.as_typed::<half::f16>().expect("F16 image");
6334 let mut bytes = vec![0u8; elems * 2];
6335 typed
6336 .copy_to_flat(&mut bytes)
6337 .expect("compact the padded rows");
6338 bytes
6339 .as_chunks::<2>()
6340 .0
6341 .iter()
6342 .map(|c| half::f16::from_le_bytes(*c).to_f64())
6343 .collect()
6344 }
6345 other => panic!("{what}: {other:?} is not a float dtype"),
6346 }
6347 };
6348 let (av, bv) = (values(a), values(b));
6349 assert_eq!(av.len(), bv.len(), "{what}: element counts differ");
6350 for (i, (x, y)) in av.iter().zip(bv.iter()).enumerate() {
6351 assert!(
6352 (x - y).abs() <= tol,
6353 "{what}: element {i} differs by {} (gpu {x}, cpu {y}, tol {tol})",
6354 (x - y).abs()
6355 );
6356 }
6357 }
6358
6359 #[test]
6382 #[cfg(all(target_os = "windows", feature = "opengl"))]
6383 fn windows_padded_float_pbo_rows_land_at_the_declared_stride() {
6384 if !is_opengl_available() {
6385 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
6386 return;
6387 }
6388 const W: usize = 64;
6389 const H: usize = 32;
6390 const POOL_W: usize = 96;
6391
6392 let mut gpu = ImageProcessor::new().unwrap();
6393 let support = gpu.supported_render_dtypes();
6394 let src = synthetic_rgba(W, H);
6395
6396 let cases: [(PixelFormat, DType, bool, &[usize]); 2] = [
6398 (
6399 PixelFormat::PlanarRgb,
6400 DType::F16,
6401 support.f16,
6402 &[W * 2, POOL_W * 2, W * 2 + 4],
6405 ),
6406 (
6407 PixelFormat::Rgb,
6408 DType::F32,
6409 support.f32,
6410 &[W * 3 * 4, POOL_W * 3 * 4],
6411 ),
6412 ];
6413
6414 for (fmt, dtype, supported, strides) in cases {
6415 if !supported {
6416 crate::test_support::report_skip(&format!(
6417 "{} - {fmt:?}/{dtype:?} not reported",
6418 function!()
6419 ));
6420 continue;
6421 }
6422 let Some(gl) = gpu.opengl.as_ref() else {
6423 crate::test_support::report_skip(&format!("{} - no GL processor", function!()));
6424 return;
6425 };
6426 let mut reference = gl
6428 .create_pbo_image_dtype(W, H, fmt, dtype)
6429 .unwrap_or_else(|e| panic!("{fmt:?}/{dtype:?}: tight reference PBO: {e:?}"));
6430 assert_eq!(
6431 reference.memory(),
6432 TensorMemory::Pbo,
6433 "{fmt:?}/{dtype:?}: the destination must be a PBO to reach this readback"
6434 );
6435 gpu.convert(&src, &mut reference, None)
6436 .unwrap_or_else(|e| panic!("{fmt:?}/{dtype:?}: tight convert failed: {e}"));
6437
6438 for &stride in strides {
6439 let label = format!("{fmt:?}/{dtype:?} at a {stride} B pitch");
6440 let mut padded = gpu
6443 .opengl
6444 .as_ref()
6445 .expect("GL processor")
6446 .create_pbo_image_dtype(POOL_W, H, fmt, dtype)
6447 .unwrap_or_else(|e| panic!("{label}: pool PBO: {e:?}"));
6448 padded
6449 .configure_image(W, H, fmt)
6450 .unwrap_or_else(|e| panic!("{label}: narrow the pool tensor: {e}"));
6451 padded
6452 .set_row_stride(stride)
6453 .unwrap_or_else(|e| panic!("{label}: declare the pitch: {e}"));
6454 assert_eq!(padded.effective_row_stride(), Some(stride), "{label}");
6455
6456 let before = gpu.convert_fallback_count();
6457 gpu.convert(&src, &mut padded, None)
6458 .unwrap_or_else(|e| panic!("{label}: padded convert failed: {e}"));
6459 assert_eq!(
6462 gpu.convert_fallback_count(),
6463 before,
6464 "{label}: fell back to the CPU backend, so the float PBO readback under test never ran"
6465 );
6466
6467 let tol = if dtype == DType::F16 {
6468 1.0 / 1024.0
6469 } else {
6470 1e-6
6471 };
6472 compare_float_images(&padded, &reference, tol, &label);
6473 }
6474 }
6475 }
6476
6477 #[test]
6487 #[cfg(all(target_os = "windows", feature = "opengl"))]
6488 fn windows_float_destinations_match_the_cpu_converter_for_every_layout() {
6489 if !is_opengl_available() {
6490 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
6491 return;
6492 }
6493 if !crate::test_support::is_gpu_buffer_available() {
6494 crate::test_support::report_skip(&format!(
6495 "{} - no zero-copy GPU buffers",
6496 function!()
6497 ));
6498 return;
6499 }
6500 let mut gpu = ImageProcessor::new().unwrap();
6501 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
6502 backend: ComputeBackend::Cpu,
6503 ..Default::default()
6504 })
6505 .unwrap();
6506 let support = gpu.supported_render_dtypes();
6507 eprintln!("{}: supported_render_dtypes = {support:?}", function!());
6508 let src = synthetic_rgba(640, 480);
6509 for (fmt, dtype, need) in [
6510 (PixelFormat::PlanarRgb, DType::F16, support.f16),
6511 (PixelFormat::PlanarRgb, DType::F32, support.f32),
6512 (PixelFormat::PlanarRgba, DType::F16, support.f16),
6513 (PixelFormat::PlanarRgba, DType::F32, support.f32),
6514 (PixelFormat::Rgb, DType::F16, support.f16),
6515 (PixelFormat::Rgb, DType::F32, support.f32),
6516 (PixelFormat::Rgba, DType::F16, support.f16),
6517 (PixelFormat::Rgba, DType::F32, support.f32),
6518 ] {
6519 if !need {
6520 crate::test_support::report_skip(&format!(
6521 "{} - {fmt:?}/{dtype:?} not reported",
6522 function!()
6523 ));
6524 continue;
6525 }
6526 let mut dst_gpu = gpu
6527 .create_image(
6528 640,
6529 480,
6530 fmt,
6531 dtype,
6532 Some(TensorMemory::DmaBuf),
6533 edgefirst_tensor::CpuAccess::ReadWrite,
6534 )
6535 .unwrap();
6536 assert_eq!(
6537 dst_gpu.memory(),
6538 TensorMemory::DmaBuf,
6539 "{fmt:?}/{dtype:?} must be a texture"
6540 );
6541 let mut dst_cpu = TensorDyn::image(
6542 640,
6543 480,
6544 fmt,
6545 dtype,
6546 Some(TensorMemory::Mem),
6547 edgefirst_tensor::CpuAccess::ReadWrite,
6548 )
6549 .unwrap();
6550 let before = gpu.convert_fallback_count();
6551 gpu.convert(&src, &mut dst_gpu, None).unwrap();
6552 cpu.convert(&src, &mut dst_cpu, None).unwrap();
6553 assert_eq!(
6554 gpu.convert_fallback_count(),
6555 before,
6556 "{fmt:?}/{dtype:?} fell back to CPU"
6557 );
6558 let tol = if dtype == DType::F16 {
6559 1.0 / 1024.0
6560 } else {
6561 1e-6
6562 };
6563 compare_float_images(&dst_gpu, &dst_cpu, tol, &format!("{fmt:?}/{dtype:?}"));
6564 }
6565 }
6566
6567 #[cfg(target_os = "windows")]
6570 #[link(name = "kernel32")]
6571 extern "system" {
6572 fn WaitForSingleObject(handle: *mut std::ffi::c_void, milliseconds: u32) -> u32;
6573 }
6574
6575 #[cfg(all(target_os = "windows", feature = "opengl"))]
6584 #[test]
6585 fn windows_deferred_batch_records_one_value_per_tile_and_submits_once() {
6586 use edgefirst_tensor::CpuAccess;
6587
6588 if !crate::test_support::is_gpu_buffer_available() {
6589 crate::test_support::report_skip(&format!(
6590 "{} - no zero-copy GPU buffers",
6591 function!()
6592 ));
6593 return;
6594 }
6595 let mut p = match ImageProcessor::new() {
6596 Ok(p) => p,
6597 Err(e) => {
6598 crate::test_support::report_skip(&format!(
6599 "{} - ImageProcessor init failed ({e:?})",
6600 function!()
6601 ));
6602 return;
6603 }
6604 };
6605 let (w, h, tiles) = (640usize, 480usize, 4usize);
6606 let src = synthetic_rgba(w, h);
6607 let mut dsts: Vec<TensorDyn> = (0..tiles)
6608 .map(|_| {
6609 p.create_image(w, h, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6610 .unwrap()
6611 })
6612 .collect();
6613 if dsts[0].memory() != TensorMemory::DmaBuf {
6614 crate::test_support::report_skip(&format!(
6615 "{} - destinations are not textures",
6616 function!()
6617 ));
6618 return;
6619 }
6620 for dst in &mut dsts {
6621 p.convert_deferred(&src, dst, None).unwrap();
6622 }
6623 let values: Vec<u64> = dsts
6624 .iter()
6625 .map(|d| {
6626 d.gpu_completion()
6627 .unwrap()
6628 .expect("each deferred convert records its own completion")
6629 .value
6630 })
6631 .collect();
6632 for pair in values.windows(2) {
6633 assert!(
6634 pair[1] > pair[0],
6635 "the batch allocated {values:?}, which is not one increasing value per tile"
6636 );
6637 }
6638 p.flush().unwrap();
6639 let dev = edgefirst_tensor::d3d11::device().unwrap();
6642 let last = *values.last().unwrap();
6643 let ev = dev.event_for(last).unwrap();
6644 assert_eq!(
6647 unsafe {
6648 WaitForSingleObject(
6649 std::os::windows::io::AsRawHandle::as_raw_handle(&ev) as _,
6650 5000,
6651 )
6652 },
6653 0,
6654 "the batch's newest value {last} never arrived after flush"
6655 );
6656
6657 let mut solo = p
6658 .create_image(w, h, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6659 .unwrap();
6660 p.convert(&src, &mut solo, None).unwrap();
6661 let flat = |img: &TensorDyn| -> Vec<u8> {
6665 let t = img.as_typed::<u8>().expect("u8 image");
6666 let mut out = vec![0u8; t.shape().iter().product::<usize>()];
6667 t.copy_to_flat(&mut out).expect("compact the padded rows");
6668 out
6669 };
6670 let want = flat(&solo);
6671 for (i, dst) in dsts.iter().enumerate() {
6672 assert_eq!(
6673 flat(dst),
6674 want,
6675 "tile {i} does not match a standalone convert"
6676 );
6677 }
6678 }
6679
6680 #[cfg(all(target_os = "windows", feature = "opengl"))]
6689 #[test]
6690 fn windows_dropping_a_processor_submits_an_open_batch() {
6691 use edgefirst_tensor::CpuAccess;
6692
6693 if !crate::test_support::is_gpu_buffer_available() {
6694 crate::test_support::report_skip(&format!(
6695 "{} - no zero-copy GPU buffers",
6696 function!()
6697 ));
6698 return;
6699 }
6700 let src = synthetic_rgba(320, 240);
6701 let recorded = {
6702 let mut p = match ImageProcessor::new() {
6703 Ok(p) => p,
6704 Err(e) => {
6705 crate::test_support::report_skip(&format!(
6706 "{} - init failed ({e:?})",
6707 function!()
6708 ));
6709 return;
6710 }
6711 };
6712 let mut dst = p
6713 .create_image(
6714 320,
6715 240,
6716 PixelFormat::Rgba,
6717 DType::U8,
6718 None,
6719 CpuAccess::Read,
6720 )
6721 .unwrap();
6722 if dst.memory() != TensorMemory::DmaBuf {
6723 crate::test_support::report_skip(&format!(
6724 "{} - destination is not a texture",
6725 function!()
6726 ));
6727 return;
6728 }
6729 p.convert_deferred(&src, &mut dst, None).unwrap();
6730 dst.gpu_completion()
6732 .unwrap()
6733 .expect("the deferred convert recorded a completion")
6734 .value
6735 };
6736
6737 let dev = edgefirst_tensor::d3d11::device().unwrap();
6738 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
6739 while dev.completed_value() < recorded {
6740 assert!(
6741 std::time::Instant::now() < deadline,
6742 "the dropped processor never submitted its batch: fence at {}, recorded {recorded}",
6743 dev.completed_value()
6744 );
6745 std::thread::yield_now();
6746 }
6747 }
6748
6749 #[cfg(all(target_os = "windows", feature = "opengl"))]
6754 #[test]
6755 fn windows_create_image_prefers_textures_and_convert_with_fence_returns_a_set_event() {
6756 use edgefirst_tensor::CpuAccess;
6757 use std::os::windows::io::AsRawHandle;
6758
6759 if !crate::test_support::is_gpu_buffer_available() {
6760 crate::test_support::report_skip(&format!(
6761 "{} - no zero-copy GPU buffers",
6762 function!()
6763 ));
6764 return;
6765 }
6766 let mut p = ImageProcessor::new().unwrap();
6767 let src = synthetic_rgba(320, 240);
6768 let mut dst = p
6769 .create_image(
6770 320,
6771 240,
6772 PixelFormat::Rgba,
6773 DType::U8,
6774 None,
6775 CpuAccess::Read,
6776 )
6777 .unwrap();
6778 if dst.memory() != TensorMemory::DmaBuf
6782 && !std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1")
6783 {
6784 crate::test_support::report_skip(&format!(
6785 "{} - create_image gave {:?}, not a texture (no GL backend)",
6786 function!(),
6787 dst.memory()
6788 ));
6789 return;
6790 }
6791 assert_eq!(
6792 dst.memory(),
6793 TensorMemory::DmaBuf,
6794 "create_image(None) must prefer a texture on Windows"
6795 );
6796 let before = dst.gpu_completion().unwrap().map(|c| c.value);
6799 let fence = p
6800 .convert_with_fence(&src, &mut dst, None)
6801 .unwrap()
6802 .expect("Windows exports a fence");
6803 let recorded = dst
6804 .gpu_completion()
6805 .unwrap()
6806 .expect("the convert recorded a completion")
6807 .value;
6808 assert!(
6809 before.is_none_or(|b| recorded > b),
6810 "the fenced convert recorded a newer value than {before:?}"
6811 );
6812 assert_eq!(
6817 edgefirst_tensor::d3d11::device().unwrap().last_signalled(),
6818 recorded,
6819 "a fenced convert must signal exactly once"
6820 );
6821 assert_eq!(
6826 unsafe { WaitForSingleObject(fence.as_raw_handle() as _, 5000) },
6827 0,
6828 "fence was not set within 5 seconds"
6829 );
6830 assert!(
6834 edgefirst_tensor::d3d11::device().unwrap().completed_value() >= recorded,
6835 "the event fired, so the recorded value {recorded} is complete"
6836 );
6837 assert_eq!(
6838 dst.gpu_completion().unwrap().map(|c| c.value),
6839 Some(recorded),
6840 "the destination still reports the value the event waited on"
6841 );
6842 let mut expected = TensorDyn::image(
6843 320,
6844 240,
6845 PixelFormat::Rgba,
6846 DType::U8,
6847 Some(TensorMemory::Mem),
6848 CpuAccess::ReadWrite,
6849 )
6850 .unwrap();
6851 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
6852 backend: ComputeBackend::Cpu,
6853 ..Default::default()
6854 })
6855 .unwrap();
6856 cpu.convert(&src, &mut expected, None).unwrap();
6857 compare_images(&dst, &expected, 0.0, "fenced convert");
6858 }
6859
6860 #[cfg(all(target_os = "windows", feature = "opengl"))]
6865 fn texture_or_skip(t: &TensorDyn, test: &str) -> bool {
6866 if t.memory() == TensorMemory::DmaBuf {
6867 return true;
6868 }
6869 if std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1") {
6870 panic!(
6871 "{test}: create_image(None) must prefer a texture on Windows, got {:?}",
6872 t.memory()
6873 );
6874 }
6875 crate::test_support::report_skip(&format!(
6876 "{test} - create_image gave {:?}, not a texture (no GL backend)",
6877 t.memory()
6878 ));
6879 false
6880 }
6881
6882 #[cfg(all(target_os = "windows", feature = "opengl"))]
6888 #[test]
6889 fn windows_draw_decoded_masks_into_a_texture_records_a_completion() {
6890 use edgefirst_tensor::{CpuAccess, Segmentation};
6891 use ndarray::Array3;
6892
6893 if !crate::test_support::is_gpu_buffer_available() {
6894 crate::test_support::report_skip(&format!(
6895 "{} - no zero-copy GPU buffers",
6896 function!()
6897 ));
6898 return;
6899 }
6900 let mut p = ImageProcessor::new().unwrap();
6901 let mut dst = p
6902 .create_image(64, 64, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6903 .unwrap();
6904 if !texture_or_skip(&dst, function!()) {
6905 return;
6906 }
6907 assert!(
6908 dst.gpu_completion().unwrap().is_none(),
6909 "nothing has written this texture yet"
6910 );
6911
6912 let detect = DetectBox {
6913 bbox: [0.25, 0.25, 0.75, 0.75].into(),
6914 score: 0.99,
6915 label: 0,
6916 };
6917 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
6918 let seg = Segmentation {
6919 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
6920 .unwrap()
6921 .into(),
6922 xmin: 0.25,
6923 ymin: 0.25,
6924 xmax: 0.75,
6925 ymax: 0.75,
6926 };
6927 p.draw_decoded_masks(&mut dst, &[detect], &[seg], MaskOverlay::default())
6928 .expect("draw into a read-only texture");
6929
6930 let completion = dst
6931 .gpu_completion()
6932 .unwrap()
6933 .expect("the draw recorded no completion on dst");
6934 assert!(completion.value > 0, "completion value must be non-zero");
6935
6936 let mut flat = vec![0u8; 64 * 64 * 4];
6941 dst.as_typed::<u8>()
6942 .unwrap()
6943 .copy_to_flat(&mut flat)
6944 .expect("read the drawn texture");
6945 let px = |x: usize, y: usize| -> [u8; 4] {
6946 let o = (y * 64 + x) * 4;
6947 [flat[o], flat[o + 1], flat[o + 2], flat[o + 3]]
6948 };
6949 assert_eq!(px(2, 2), [0, 0, 0, 0], "corner (2,2) was not cleared");
6950 assert_ne!(px(32, 32), [0, 0, 0, 0], "centre (32,32) was not coloured");
6951 }
6952
6953 #[cfg(all(target_os = "windows", feature = "opengl"))]
6956 #[test]
6957 fn windows_draw_proto_masks_into_a_texture_records_a_completion() {
6958 use edgefirst_tensor::{CpuAccess, Tensor};
6959
6960 if !crate::test_support::is_gpu_buffer_available() {
6961 crate::test_support::report_skip(&format!(
6962 "{} - no zero-copy GPU buffers",
6963 function!()
6964 ));
6965 return;
6966 }
6967 let mut p = match with_force_backend(Some("opengl"), ImageProcessor::new) {
6971 Ok(p) => p,
6972 Err(e) if !std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1") => {
6973 crate::test_support::report_skip(&format!(
6974 "{} - no OpenGL backend: {e}",
6975 function!()
6976 ));
6977 return;
6978 }
6979 Err(e) => panic!("forced OpenGL backend unavailable: {e}"),
6980 };
6981 let mut dst = p
6982 .create_image(64, 64, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
6983 .unwrap();
6984 if !texture_or_skip(&dst, function!()) {
6985 return;
6986 }
6987
6988 let detect = DetectBox {
6989 bbox: [0.25, 0.25, 0.75, 0.75].into(),
6990 score: 0.99,
6991 label: 0,
6992 };
6993 let proto = ProtoData {
6996 mask_coefficients: TensorDyn::F32(
6997 Tensor::<f32>::from_slice(&[1.0_f32], &[1, 1]).unwrap(),
6998 ),
6999 protos: TensorDyn::F32(
7000 Tensor::<f32>::from_slice(&vec![1.0_f32; 8 * 8], &[8, 8, 1]).unwrap(),
7001 ),
7002 layout: ProtoLayout::Nhwc,
7003 };
7004 p.draw_proto_masks(&mut dst, &[detect], &proto, MaskOverlay::default())
7005 .expect("forced-GL proto draw into a read-only texture");
7006
7007 let completion = dst
7008 .gpu_completion()
7009 .unwrap()
7010 .expect("the proto draw recorded no completion on dst");
7011 assert!(completion.value > 0, "completion value must be non-zero");
7012 }
7013
7014 #[cfg(all(target_os = "windows", feature = "opengl"))]
7019 #[test]
7020 fn windows_draw_decoded_masks_composites_a_host_background_onto_a_texture() {
7021 use edgefirst_tensor::CpuAccess;
7022
7023 if !crate::test_support::is_gpu_buffer_available() {
7024 crate::test_support::report_skip(&format!(
7025 "{} - no zero-copy GPU buffers",
7026 function!()
7027 ));
7028 return;
7029 }
7030 let mut p = ImageProcessor::new().unwrap();
7031 let bg = synthetic_rgba(64, 48);
7032 let mut dst = p
7033 .create_image(64, 48, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
7034 .unwrap();
7035 if !texture_or_skip(&dst, function!()) {
7036 return;
7037 }
7038 p.draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::new().with_background(&bg))
7039 .expect("empty draw with a host background onto a texture");
7040 assert_pixels_identical(&dst, &bg, "host background onto a texture");
7041 assert!(dst.gpu_completion().unwrap().is_some());
7042 }
7043
7044 #[cfg(all(target_os = "windows", feature = "opengl"))]
7047 #[test]
7048 fn windows_draw_decoded_masks_composites_a_texture_background_onto_a_texture() {
7049 use edgefirst_tensor::CpuAccess;
7050
7051 if !crate::test_support::is_gpu_buffer_available() {
7052 crate::test_support::report_skip(&format!(
7053 "{} - no zero-copy GPU buffers",
7054 function!()
7055 ));
7056 return;
7057 }
7058 let mut p = ImageProcessor::new().unwrap();
7059 let expected = synthetic_rgba(64, 48);
7060 let bg = p
7061 .create_image(
7062 64,
7063 48,
7064 PixelFormat::Rgba,
7065 DType::U8,
7066 None,
7067 CpuAccess::ReadWrite,
7068 )
7069 .unwrap();
7070 if !texture_or_skip(&bg, function!()) {
7071 return;
7072 }
7073 fill_synthetic_rgba(&bg);
7074 let mut dst = p
7075 .create_image(64, 48, PixelFormat::Rgba, DType::U8, None, CpuAccess::Read)
7076 .unwrap();
7077 assert_eq!(dst.memory(), TensorMemory::DmaBuf);
7078 p.draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::new().with_background(&bg))
7079 .expect("empty draw with a texture background onto a texture");
7080 assert_pixels_identical(&dst, &expected, "texture background onto a texture");
7081 assert!(dst.gpu_completion().unwrap().is_some());
7082 }
7083
7084 #[cfg(all(target_os = "windows", feature = "opengl"))]
7090 #[test]
7091 fn windows_draw_decoded_masks_into_a_texture_view_fills_only_its_band() {
7092 use edgefirst_tensor::CpuAccess;
7093
7094 if !crate::test_support::is_gpu_buffer_available() {
7095 crate::test_support::report_skip(&format!(
7096 "{} - no zero-copy GPU buffers",
7097 function!()
7098 ));
7099 return;
7100 }
7101 let (w, h) = (32usize, 64usize);
7102 let mut p = ImageProcessor::new().unwrap();
7103 let parent = p
7104 .create_image(
7105 w,
7106 h,
7107 PixelFormat::Rgba,
7108 DType::U8,
7109 None,
7110 CpuAccess::ReadWrite,
7111 )
7112 .unwrap();
7113 if !texture_or_skip(&parent, function!()) {
7114 return;
7115 }
7116 fill_synthetic_rgba(&parent);
7117
7118 let mut band = parent
7122 .view(edgefirst_tensor::Region::new(0, h / 2, w, h / 2))
7123 .unwrap();
7124 p.draw_decoded_masks(&mut band, &[], &[], MaskOverlay::default())
7125 .expect("empty draw into a texture view");
7126 drop(band);
7127
7128 let mut flat = vec![0u8; w * h * 4];
7129 parent
7130 .as_typed::<u8>()
7131 .unwrap()
7132 .copy_to_flat(&mut flat)
7133 .expect("read the parent texture");
7134 for y in 0..h {
7135 for x in 0..w {
7136 let o = (y * w + x) * 4;
7137 let got = [flat[o], flat[o + 1], flat[o + 2], flat[o + 3]];
7138 let want = if y < h / 2 {
7139 [x as u8, y as u8, (x + y) as u8, 255]
7140 } else {
7141 [0, 0, 0, 0]
7142 };
7143 assert_eq!(got, want, "pixel ({x},{y})");
7144 }
7145 }
7146
7147 let mut forced = with_force_backend(Some("opengl"), ImageProcessor::new).unwrap();
7150 let mut band = parent
7151 .view(edgefirst_tensor::Region::new(0, h / 2, w, h / 2))
7152 .unwrap();
7153 let err = forced
7154 .draw_decoded_masks(&mut band, &[], &[], MaskOverlay::default())
7155 .expect_err("forced GL must decline a view destination");
7156 assert!(
7157 matches!(err, Error::NotSupported(_)),
7158 "expected NotSupported, got {err:?}"
7159 );
7160 }
7161
7162 #[cfg(all(target_os = "windows", feature = "opengl"))]
7167 #[test]
7168 fn windows_class_colors_reach_the_cpu_backend_behind_a_declined_gl_draw() {
7169 use edgefirst_tensor::{CpuAccess, Segmentation};
7170 use ndarray::Array3;
7171
7172 if !crate::test_support::is_gpu_buffer_available() {
7173 crate::test_support::report_skip(&format!(
7174 "{} - no zero-copy GPU buffers",
7175 function!()
7176 ));
7177 return;
7178 }
7179 const CUSTOM: [u8; 4] = [7, 200, 13, 255];
7180 assert_ne!(
7181 CUSTOM[..3],
7182 DEFAULT_COLORS_U8[0][..3],
7183 "the custom colour must differ from the default palette's, or \
7184 this test cannot tell them apart"
7185 );
7186
7187 let (w, h) = (32usize, 64usize);
7188 let mut p = ImageProcessor::new().unwrap();
7189 let parent = p
7190 .create_image(
7191 w,
7192 h,
7193 PixelFormat::Rgba,
7194 DType::U8,
7195 None,
7196 CpuAccess::ReadWrite,
7197 )
7198 .unwrap();
7199 if parent.memory() != TensorMemory::DmaBuf
7204 && !std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1")
7205 {
7206 crate::test_support::report_skip(&format!(
7207 "{} - create_image gave {:?}, not a texture (no GL backend)",
7208 function!(),
7209 parent.memory()
7210 ));
7211 return;
7212 }
7213 p.set_class_colors(&[CUSTOM]).unwrap();
7214
7215 let detect = DetectBox {
7216 bbox: [0.25, 0.25, 0.75, 0.75].into(),
7217 score: 0.99,
7218 label: 0,
7219 };
7220 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
7221 let seg = Segmentation {
7222 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
7223 .unwrap()
7224 .into(),
7225 xmin: 0.25,
7226 ymin: 0.25,
7227 xmax: 0.75,
7228 ymax: 0.75,
7229 };
7230 let mut band = parent
7231 .view(edgefirst_tensor::Region::new(0, h / 2, w, h / 2))
7232 .unwrap();
7233 p.draw_decoded_masks(&mut band, &[detect], &[seg], MaskOverlay::default())
7234 .expect("draw into a texture view");
7235 drop(band);
7236
7237 let mut flat = vec![0u8; w * h * 4];
7238 parent
7239 .as_typed::<u8>()
7240 .unwrap()
7241 .copy_to_flat(&mut flat)
7242 .expect("read the parent texture");
7243 let o = ((h / 2 + h / 4) * w + w / 2) * 4;
7246 assert_eq!(
7247 &flat[o..o + 3],
7248 &CUSTOM[..3],
7249 "the CPU backend drew the default palette, so it never received \
7250 set_class_colors"
7251 );
7252 }
7253
7254 #[cfg(all(target_os = "windows", feature = "opengl"))]
7259 #[test]
7260 fn two_processors_alternate_on_texture_destinations_without_state_bleed() {
7261 use edgefirst_tensor::CpuAccess;
7262
7263 if !crate::test_support::is_gpu_buffer_available() {
7264 crate::test_support::report_skip(&format!(
7265 "{} - no zero-copy GPU buffers",
7266 function!()
7267 ));
7268 return;
7269 }
7270 let mut a = ImageProcessor::new().unwrap();
7271 let mut b = ImageProcessor::new().unwrap();
7272 let src_a = synthetic_rgba(512, 512);
7273 let src_b = synthetic_rgba(256, 256);
7274 let mut dst_a = a
7275 .create_image(
7276 512,
7277 512,
7278 PixelFormat::Rgba,
7279 DType::U8,
7280 None,
7281 CpuAccess::Read,
7282 )
7283 .unwrap();
7284 let mut dst_b = b
7285 .create_image(
7286 256,
7287 256,
7288 PixelFormat::Rgba,
7289 DType::U8,
7290 None,
7291 CpuAccess::Read,
7292 )
7293 .unwrap();
7294 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
7295 backend: ComputeBackend::Cpu,
7296 ..Default::default()
7297 })
7298 .unwrap();
7299 let mut ref_a = TensorDyn::image(
7300 512,
7301 512,
7302 PixelFormat::Rgba,
7303 DType::U8,
7304 Some(TensorMemory::Mem),
7305 CpuAccess::ReadWrite,
7306 )
7307 .unwrap();
7308 let mut ref_b = TensorDyn::image(
7309 256,
7310 256,
7311 PixelFormat::Rgba,
7312 DType::U8,
7313 Some(TensorMemory::Mem),
7314 CpuAccess::ReadWrite,
7315 )
7316 .unwrap();
7317 cpu.convert(&src_a, &mut ref_a, None).unwrap();
7318 cpu.convert(&src_b, &mut ref_b, None).unwrap();
7319 for _ in 0..200 {
7320 a.convert(&src_a, &mut dst_a, None).unwrap();
7321 b.convert(&src_b, &mut dst_b, None).unwrap();
7322 compare_images(&dst_a, &ref_a, 0.0, "processor A");
7323 compare_images(&dst_b, &ref_b, 0.0, "processor B");
7324 }
7325 }
7326
7327 #[test]
7328 #[cfg(any(
7329 target_os = "linux",
7330 target_os = "macos",
7331 target_os = "ios",
7332 target_os = "android",
7333 target_os = "windows"
7334 ))]
7335 #[cfg(feature = "opengl")]
7336 fn test_opengl_all_rgba() {
7337 if !is_opengl_available() {
7338 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
7339 return;
7340 }
7341
7342 let dst_width = 640;
7343 let dst_height = 640;
7344 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7345
7346 let mut cpu_converter = CPUProcessor::new();
7347
7348 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
7349
7350 let mut mem = vec![None, Some(TensorMemory::Mem)];
7354 if edgefirst_tensor::is_shm_available() {
7355 mem.push(Some(TensorMemory::Shm));
7356 }
7357 if crate::test_support::is_gpu_buffer_available() {
7358 mem.push(Some(TensorMemory::DmaBuf));
7359 } else {
7360 crate::test_support::report_skip(&format!(
7361 "{} - DmaBuf backing not exercised: no zero-copy GPU buffers",
7362 function!()
7363 ));
7364 }
7365 let crop = Crop::new().with_source(Some(Region::new(50, 120, 1024, 576)));
7366 for m in mem {
7367 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), m).unwrap();
7368 let src_dyn = src;
7369
7370 for rot in [
7371 Rotation::None,
7372 Rotation::Clockwise90,
7373 Rotation::Rotate180,
7374 Rotation::CounterClockwise90,
7375 ] {
7376 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
7377 let cpu_dst = TensorDyn::image(
7378 dst_width,
7379 dst_height,
7380 PixelFormat::Rgba,
7381 DType::U8,
7382 m,
7383 edgefirst_tensor::CpuAccess::ReadWrite,
7384 )
7385 .unwrap();
7386 let gl_dst = TensorDyn::image(
7387 dst_width,
7388 dst_height,
7389 PixelFormat::Rgba,
7390 DType::U8,
7391 m,
7392 edgefirst_tensor::CpuAccess::ReadWrite,
7393 )
7394 .unwrap();
7395 cpu_dst
7396 .as_typed::<u8>()
7397 .unwrap()
7398 .map()
7399 .unwrap()
7400 .as_mut_slice()
7401 .fill(114);
7402 gl_dst
7403 .as_typed::<u8>()
7404 .unwrap()
7405 .map()
7406 .unwrap()
7407 .as_mut_slice()
7408 .fill(114);
7409
7410 let mut cpu_dst_dyn = cpu_dst;
7411 cpu_converter
7412 .convert(
7413 &src_dyn,
7414 &mut cpu_dst_dyn,
7415 Some(crate::ConvertOptions::new().crop(crop)),
7416 )
7417 .unwrap();
7418 let cpu_dst = {
7419 let mut __t = cpu_dst_dyn.into_u8().unwrap();
7420 __t.set_format(PixelFormat::Rgba).unwrap();
7421 TensorDyn::from(__t)
7422 };
7423
7424 let mut gl_dst_dyn = gl_dst;
7425 gl_converter
7426 .convert(
7427 &src_dyn,
7428 &mut gl_dst_dyn,
7429 Some(crate::ConvertOptions::new().crop(crop)),
7430 )
7431 .map_err(|e| {
7432 log::error!("error mem {m:?} rot {rot:?} error: {e:?}");
7433 e
7434 })
7435 .unwrap();
7436 let gl_dst = {
7437 let mut __t = gl_dst_dyn.into_u8().unwrap();
7438 __t.set_format(PixelFormat::Rgba).unwrap();
7439 TensorDyn::from(__t)
7440 };
7441
7442 compare_images(
7443 &gl_dst,
7444 &cpu_dst,
7445 0.98,
7446 &format!("{} {:?} {:?}", function!(), rot, flip),
7447 );
7448 }
7449 }
7450 }
7451 }
7452
7453 #[test]
7454 #[cfg(target_os = "linux")]
7455 fn test_cpu_rotate() {
7456 for rot in [
7457 Rotation::Clockwise90,
7458 Rotation::Rotate180,
7459 Rotation::CounterClockwise90,
7460 ] {
7461 test_cpu_rotate_(rot);
7462 }
7463 }
7464
7465 #[cfg(target_os = "linux")]
7466 fn test_cpu_rotate_(rot: Rotation) {
7467 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7471
7472 let unchanged_src =
7473 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
7474 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
7475
7476 let (dst_width, dst_height) = match rot {
7477 Rotation::None | Rotation::Rotate180 => (src.width().unwrap(), src.height().unwrap()),
7478 Rotation::Clockwise90 | Rotation::CounterClockwise90 => {
7479 (src.height().unwrap(), src.width().unwrap())
7480 }
7481 };
7482
7483 let cpu_dst = TensorDyn::image(
7484 dst_width,
7485 dst_height,
7486 PixelFormat::Rgba,
7487 DType::U8,
7488 None,
7489 edgefirst_tensor::CpuAccess::ReadWrite,
7490 )
7491 .unwrap();
7492 let mut cpu_converter = CPUProcessor::new();
7493
7494 let (result, src, cpu_dst) = convert_img(
7497 &mut cpu_converter,
7498 src,
7499 cpu_dst,
7500 rot,
7501 Flip::None,
7502 Crop::no_crop(),
7503 );
7504 result.unwrap();
7505
7506 let (result, cpu_dst, src) = convert_img(
7507 &mut cpu_converter,
7508 cpu_dst,
7509 src,
7510 rot,
7511 Flip::None,
7512 Crop::no_crop(),
7513 );
7514 result.unwrap();
7515
7516 let (result, src, cpu_dst) = convert_img(
7517 &mut cpu_converter,
7518 src,
7519 cpu_dst,
7520 rot,
7521 Flip::None,
7522 Crop::no_crop(),
7523 );
7524 result.unwrap();
7525
7526 let (result, _cpu_dst, src) = convert_img(
7527 &mut cpu_converter,
7528 cpu_dst,
7529 src,
7530 rot,
7531 Flip::None,
7532 Crop::no_crop(),
7533 );
7534 result.unwrap();
7535
7536 compare_images(&src, &unchanged_src, 0.98, function!());
7537 }
7538
7539 #[test]
7540 #[cfg(any(
7541 target_os = "linux",
7542 target_os = "macos",
7543 target_os = "ios",
7544 target_os = "android",
7545 target_os = "windows"
7546 ))]
7547 #[cfg(feature = "opengl")]
7548 fn test_opengl_rotate() {
7549 if !is_opengl_available() {
7550 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
7551 return;
7552 }
7553
7554 let size = (1280, 720);
7555 let mut mem = vec![None, Some(TensorMemory::Mem)];
7557 if edgefirst_tensor::is_shm_available() {
7558 mem.push(Some(TensorMemory::Shm));
7559 }
7560 if crate::test_support::is_gpu_buffer_available() {
7561 mem.push(Some(TensorMemory::DmaBuf));
7562 } else {
7563 crate::test_support::report_skip(&format!(
7564 "{} - DmaBuf backing not exercised: no zero-copy GPU buffers",
7565 function!()
7566 ));
7567 }
7568 for m in mem {
7569 for rot in [
7570 Rotation::Clockwise90,
7571 Rotation::Rotate180,
7572 Rotation::CounterClockwise90,
7573 ] {
7574 test_opengl_rotate_(size, rot, m);
7575 }
7576 }
7577 }
7578
7579 #[cfg(any(
7580 target_os = "linux",
7581 target_os = "macos",
7582 target_os = "ios",
7583 target_os = "android",
7584 target_os = "windows"
7585 ))]
7586 #[cfg(feature = "opengl")]
7587 fn test_opengl_rotate_(
7588 size: (usize, usize),
7589 rot: Rotation,
7590 tensor_memory: Option<TensorMemory>,
7591 ) {
7592 let (dst_width, dst_height) = match rot {
7593 Rotation::None | Rotation::Rotate180 => size,
7594 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
7595 };
7596
7597 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7598 let src =
7599 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), tensor_memory).unwrap();
7600
7601 let cpu_dst = TensorDyn::image(
7602 dst_width,
7603 dst_height,
7604 PixelFormat::Rgba,
7605 DType::U8,
7606 None,
7607 edgefirst_tensor::CpuAccess::ReadWrite,
7608 )
7609 .unwrap();
7610 let mut cpu_converter = CPUProcessor::new();
7611
7612 let (result, mut src, cpu_dst) = convert_img(
7613 &mut cpu_converter,
7614 src,
7615 cpu_dst,
7616 rot,
7617 Flip::None,
7618 Crop::no_crop(),
7619 );
7620 result.unwrap();
7621
7622 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
7623
7624 for _ in 0..5 {
7625 let gl_dst = TensorDyn::image(
7626 dst_width,
7627 dst_height,
7628 PixelFormat::Rgba,
7629 DType::U8,
7630 tensor_memory,
7631 edgefirst_tensor::CpuAccess::ReadWrite,
7632 )
7633 .unwrap();
7634 let (result, src_back, gl_dst) = convert_img(
7635 &mut gl_converter,
7636 src,
7637 gl_dst,
7638 rot,
7639 Flip::None,
7640 Crop::no_crop(),
7641 );
7642 result.unwrap();
7643 src = src_back;
7644 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
7645 }
7646 }
7647
7648 #[test]
7649 #[cfg(target_os = "linux")]
7650 fn test_g2d_rotate() {
7651 if !is_g2d_available() {
7652 crate::test_support::report_skip(
7653 "test_g2d_rotate - G2D library (libg2d.so.2) not available",
7654 );
7655 return;
7656 }
7657 if !is_dma_available() {
7658 crate::test_support::report_skip("test_g2d_rotate - DMA memory allocation not available (permission denied or no DMA-BUF support)");
7659 return;
7660 }
7661
7662 let size = (1280, 720);
7663 for rot in [
7664 Rotation::Clockwise90,
7665 Rotation::Rotate180,
7666 Rotation::CounterClockwise90,
7667 ] {
7668 test_g2d_rotate_(size, rot);
7669 }
7670 }
7671
7672 #[cfg(target_os = "linux")]
7673 fn test_g2d_rotate_(size: (usize, usize), rot: Rotation) {
7674 let (dst_width, dst_height) = match rot {
7675 Rotation::None | Rotation::Rotate180 => size,
7676 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
7677 };
7678
7679 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
7680 let src = crate::load_image_test_helper(
7681 &file,
7682 Some(PixelFormat::Rgba),
7683 Some(TensorMemory::DmaBuf),
7684 )
7685 .unwrap();
7686
7687 let cpu_dst = TensorDyn::image(
7688 dst_width,
7689 dst_height,
7690 PixelFormat::Rgba,
7691 DType::U8,
7692 None,
7693 edgefirst_tensor::CpuAccess::ReadWrite,
7694 )
7695 .unwrap();
7696 let mut cpu_converter = CPUProcessor::new();
7697
7698 let (result, src, cpu_dst) = convert_img(
7699 &mut cpu_converter,
7700 src,
7701 cpu_dst,
7702 rot,
7703 Flip::None,
7704 Crop::no_crop(),
7705 );
7706 result.unwrap();
7707
7708 let g2d_dst = TensorDyn::image(
7709 dst_width,
7710 dst_height,
7711 PixelFormat::Rgba,
7712 DType::U8,
7713 Some(TensorMemory::DmaBuf),
7714 edgefirst_tensor::CpuAccess::ReadWrite,
7715 )
7716 .unwrap();
7717 let mut g2d_converter = G2DProcessor::new().unwrap();
7718
7719 let (result, _src, g2d_dst) = convert_img(
7720 &mut g2d_converter,
7721 src,
7722 g2d_dst,
7723 rot,
7724 Flip::None,
7725 Crop::no_crop(),
7726 );
7727 result.unwrap();
7728
7729 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
7735 }
7736
7737 #[test]
7738 fn test_rgba_to_yuyv_resize_cpu() {
7739 let src = load_bytes_to_tensor(
7740 1280,
7741 720,
7742 PixelFormat::Rgba,
7743 None,
7744 &edgefirst_bench::testdata::read("camera720p.rgba"),
7745 )
7746 .unwrap();
7747
7748 let (dst_width, dst_height) = (640, 360);
7749
7750 let dst = TensorDyn::image(
7751 dst_width,
7752 dst_height,
7753 PixelFormat::Yuyv,
7754 DType::U8,
7755 None,
7756 edgefirst_tensor::CpuAccess::ReadWrite,
7757 )
7758 .unwrap();
7759
7760 let dst_through_yuyv = TensorDyn::image(
7761 dst_width,
7762 dst_height,
7763 PixelFormat::Rgba,
7764 DType::U8,
7765 None,
7766 edgefirst_tensor::CpuAccess::ReadWrite,
7767 )
7768 .unwrap();
7769 let dst_direct = TensorDyn::image(
7770 dst_width,
7771 dst_height,
7772 PixelFormat::Rgba,
7773 DType::U8,
7774 None,
7775 edgefirst_tensor::CpuAccess::ReadWrite,
7776 )
7777 .unwrap();
7778
7779 let mut cpu_converter = CPUProcessor::new();
7780
7781 let (result, src, dst) = convert_img(
7782 &mut cpu_converter,
7783 src,
7784 dst,
7785 Rotation::None,
7786 Flip::None,
7787 Crop::no_crop(),
7788 );
7789 result.unwrap();
7790
7791 let (result, _dst, dst_through_yuyv) = convert_img(
7792 &mut cpu_converter,
7793 dst,
7794 dst_through_yuyv,
7795 Rotation::None,
7796 Flip::None,
7797 Crop::no_crop(),
7798 );
7799 result.unwrap();
7800
7801 let (result, _src, dst_direct) = convert_img(
7802 &mut cpu_converter,
7803 src,
7804 dst_direct,
7805 Rotation::None,
7806 Flip::None,
7807 Crop::no_crop(),
7808 );
7809 result.unwrap();
7810
7811 compare_images(&dst_through_yuyv, &dst_direct, 0.98, function!());
7812 }
7813
7814 #[test]
7815 #[cfg(any(
7816 target_os = "linux",
7817 target_os = "macos",
7818 target_os = "ios",
7819 target_os = "android",
7820 target_os = "windows"
7821 ))]
7822 #[cfg(feature = "opengl")]
7823 #[ignore = "opengl doesn't support rendering to PixelFormat::Yuyv texture"]
7824 fn test_rgba_to_yuyv_resize_opengl() {
7825 if !is_opengl_available() {
7826 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
7827 return;
7828 }
7829
7830 if !crate::test_support::is_gpu_buffer_available() {
7831 crate::test_support::report_skip(&format!(
7832 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
7833 function!()
7834 ));
7835 return;
7836 }
7837
7838 let src = load_bytes_to_tensor(
7839 1280,
7840 720,
7841 PixelFormat::Rgba,
7842 None,
7843 &edgefirst_bench::testdata::read("camera720p.rgba"),
7844 )
7845 .unwrap();
7846
7847 let (dst_width, dst_height) = (640, 360);
7848
7849 let dst = TensorDyn::image(
7850 dst_width,
7851 dst_height,
7852 PixelFormat::Yuyv,
7853 DType::U8,
7854 Some(TensorMemory::DmaBuf),
7855 edgefirst_tensor::CpuAccess::ReadWrite,
7856 )
7857 .unwrap();
7858
7859 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
7860
7861 let (result, src, dst) = convert_img(
7862 &mut gl_converter,
7863 src,
7864 dst,
7865 Rotation::None,
7866 Flip::None,
7867 Crop::letterbox([255, 255, 255, 255]),
7868 );
7869 result.unwrap();
7870
7871 std::fs::write(
7872 "rgba_to_yuyv_opengl.yuyv",
7873 dst.as_typed::<u8>().unwrap().map().unwrap().as_slice(),
7874 )
7875 .unwrap();
7876 let cpu_dst = TensorDyn::image(
7877 dst_width,
7878 dst_height,
7879 PixelFormat::Yuyv,
7880 DType::U8,
7881 Some(TensorMemory::DmaBuf),
7882 edgefirst_tensor::CpuAccess::ReadWrite,
7883 )
7884 .unwrap();
7885 let (result, _src, cpu_dst) = convert_img(
7886 &mut CPUProcessor::new(),
7887 src,
7888 cpu_dst,
7889 Rotation::None,
7890 Flip::None,
7891 Crop::no_crop(),
7892 );
7893 result.unwrap();
7894
7895 compare_images_convert_to_rgb(&dst, &cpu_dst, 0.98, function!());
7896 }
7897
7898 #[test]
7899 #[cfg(target_os = "linux")]
7900 fn test_rgba_to_yuyv_resize_g2d() {
7901 if !is_g2d_available() {
7902 crate::test_support::report_skip(
7903 "test_rgba_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available",
7904 );
7905 return;
7906 }
7907 if !is_dma_available() {
7908 crate::test_support::report_skip("test_rgba_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
7909 return;
7910 }
7911
7912 let src = load_bytes_to_tensor(
7913 1280,
7914 720,
7915 PixelFormat::Rgba,
7916 Some(TensorMemory::DmaBuf),
7917 &edgefirst_bench::testdata::read("camera720p.rgba"),
7918 )
7919 .unwrap();
7920
7921 let (dst_width, dst_height) = (1280, 720);
7922
7923 let cpu_dst = TensorDyn::image(
7924 dst_width,
7925 dst_height,
7926 PixelFormat::Yuyv,
7927 DType::U8,
7928 Some(TensorMemory::DmaBuf),
7929 edgefirst_tensor::CpuAccess::ReadWrite,
7930 )
7931 .unwrap();
7932
7933 let g2d_dst = TensorDyn::image(
7934 dst_width,
7935 dst_height,
7936 PixelFormat::Yuyv,
7937 DType::U8,
7938 Some(TensorMemory::DmaBuf),
7939 edgefirst_tensor::CpuAccess::ReadWrite,
7940 )
7941 .unwrap();
7942
7943 let mut g2d_converter = G2DProcessor::new().unwrap();
7944 let crop = Crop::new();
7945
7946 g2d_dst
7947 .as_typed::<u8>()
7948 .unwrap()
7949 .map()
7950 .unwrap()
7951 .as_mut_slice()
7952 .fill(128);
7953 let (result, src, g2d_dst) = convert_img(
7954 &mut g2d_converter,
7955 src,
7956 g2d_dst,
7957 Rotation::None,
7958 Flip::None,
7959 crop,
7960 );
7961 result.unwrap();
7962
7963 let cpu_dst_img = cpu_dst;
7964 cpu_dst_img
7965 .as_typed::<u8>()
7966 .unwrap()
7967 .map()
7968 .unwrap()
7969 .as_mut_slice()
7970 .fill(128);
7971 let (result, _src, cpu_dst) = convert_img(
7972 &mut CPUProcessor::new(),
7973 src,
7974 cpu_dst_img,
7975 Rotation::None,
7976 Flip::None,
7977 crop,
7978 );
7979 result.unwrap();
7980
7981 compare_images_convert_to_rgb(&cpu_dst, &g2d_dst, 0.98, function!());
7982 }
7983
7984 #[test]
7985 fn test_yuyv_to_rgba_cpu() {
7986 let file = edgefirst_bench::testdata::read("camera720p.yuyv").to_vec();
7987 let src = TensorDyn::image(
7988 1280,
7989 720,
7990 PixelFormat::Yuyv,
7991 DType::U8,
7992 None,
7993 edgefirst_tensor::CpuAccess::ReadWrite,
7994 )
7995 .unwrap();
7996 src.as_typed::<u8>()
7997 .unwrap()
7998 .map()
7999 .unwrap()
8000 .as_mut_slice()
8001 .copy_from_slice(&file);
8002
8003 let dst = TensorDyn::image(
8004 1280,
8005 720,
8006 PixelFormat::Rgba,
8007 DType::U8,
8008 None,
8009 edgefirst_tensor::CpuAccess::ReadWrite,
8010 )
8011 .unwrap();
8012 let mut cpu_converter = CPUProcessor::new();
8013
8014 let (result, _src, dst) = convert_img(
8015 &mut cpu_converter,
8016 src,
8017 dst,
8018 Rotation::None,
8019 Flip::None,
8020 Crop::no_crop(),
8021 );
8022 result.unwrap();
8023
8024 let target_image = TensorDyn::image(
8025 1280,
8026 720,
8027 PixelFormat::Rgba,
8028 DType::U8,
8029 None,
8030 edgefirst_tensor::CpuAccess::ReadWrite,
8031 )
8032 .unwrap();
8033 target_image
8034 .as_typed::<u8>()
8035 .unwrap()
8036 .map()
8037 .unwrap()
8038 .as_mut_slice()
8039 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
8040
8041 compare_images(&dst, &target_image, 0.98, function!());
8044 }
8045
8046 #[test]
8047 fn test_yuyv_to_rgb_cpu() {
8048 let file = edgefirst_bench::testdata::read("camera720p.yuyv").to_vec();
8049 let src = TensorDyn::image(
8050 1280,
8051 720,
8052 PixelFormat::Yuyv,
8053 DType::U8,
8054 None,
8055 edgefirst_tensor::CpuAccess::ReadWrite,
8056 )
8057 .unwrap();
8058 src.as_typed::<u8>()
8059 .unwrap()
8060 .map()
8061 .unwrap()
8062 .as_mut_slice()
8063 .copy_from_slice(&file);
8064
8065 let dst = TensorDyn::image(
8066 1280,
8067 720,
8068 PixelFormat::Rgb,
8069 DType::U8,
8070 None,
8071 edgefirst_tensor::CpuAccess::ReadWrite,
8072 )
8073 .unwrap();
8074 let mut cpu_converter = CPUProcessor::new();
8075
8076 let (result, _src, dst) = convert_img(
8077 &mut cpu_converter,
8078 src,
8079 dst,
8080 Rotation::None,
8081 Flip::None,
8082 Crop::no_crop(),
8083 );
8084 result.unwrap();
8085
8086 let target_image = TensorDyn::image(
8087 1280,
8088 720,
8089 PixelFormat::Rgb,
8090 DType::U8,
8091 None,
8092 edgefirst_tensor::CpuAccess::ReadWrite,
8093 )
8094 .unwrap();
8095 target_image
8096 .as_typed::<u8>()
8097 .unwrap()
8098 .map()
8099 .unwrap()
8100 .as_mut_slice()
8101 .as_chunks_mut::<3>()
8102 .0
8103 .iter_mut()
8104 .zip(
8105 edgefirst_bench::testdata::read("camera720p.rgba")
8106 .as_chunks::<4>()
8107 .0,
8108 )
8109 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
8110
8111 compare_images(&dst, &target_image, 0.98, function!());
8114 }
8115
8116 #[test]
8117 #[cfg(target_os = "linux")]
8118 fn test_yuyv_to_rgba_g2d() {
8119 if !is_g2d_available() {
8120 crate::test_support::report_skip(
8121 "test_yuyv_to_rgba_g2d - G2D library (libg2d.so.2) not available",
8122 );
8123 return;
8124 }
8125 if !is_dma_available() {
8126 crate::test_support::report_skip("test_yuyv_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
8127 return;
8128 }
8129
8130 let src = load_bytes_to_tensor(
8131 1280,
8132 720,
8133 PixelFormat::Yuyv,
8134 None,
8135 &edgefirst_bench::testdata::read("camera720p.yuyv"),
8136 )
8137 .unwrap();
8138
8139 let dst = TensorDyn::image(
8140 1280,
8141 720,
8142 PixelFormat::Rgba,
8143 DType::U8,
8144 Some(TensorMemory::DmaBuf),
8145 edgefirst_tensor::CpuAccess::ReadWrite,
8146 )
8147 .unwrap();
8148 let mut g2d_converter = G2DProcessor::new().unwrap();
8149
8150 let (result, _src, dst) = convert_img(
8151 &mut g2d_converter,
8152 src,
8153 dst,
8154 Rotation::None,
8155 Flip::None,
8156 Crop::no_crop(),
8157 );
8158 result.unwrap();
8159
8160 let target_image = TensorDyn::image(
8161 1280,
8162 720,
8163 PixelFormat::Rgba,
8164 DType::U8,
8165 None,
8166 edgefirst_tensor::CpuAccess::ReadWrite,
8167 )
8168 .unwrap();
8169 target_image
8170 .as_typed::<u8>()
8171 .unwrap()
8172 .map()
8173 .unwrap()
8174 .as_mut_slice()
8175 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
8176
8177 compare_images(&dst, &target_image, 0.98, function!());
8181 }
8182
8183 #[test]
8184 #[cfg(any(
8185 target_os = "linux",
8186 target_os = "macos",
8187 target_os = "ios",
8188 target_os = "android",
8189 target_os = "windows"
8190 ))]
8191 #[cfg(feature = "opengl")]
8192 fn test_yuyv_to_rgba_opengl() {
8193 if !is_opengl_available() {
8194 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
8195 return;
8196 }
8197 if !crate::test_support::is_gpu_buffer_available() {
8200 crate::test_support::report_skip(&format!(
8201 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
8202 function!()
8203 ));
8204 return;
8205 }
8206
8207 let src = load_bytes_to_tensor(
8208 1280,
8209 720,
8210 PixelFormat::Yuyv,
8211 Some(TensorMemory::DmaBuf),
8212 &edgefirst_bench::testdata::read("camera720p.yuyv"),
8213 )
8214 .unwrap();
8215
8216 let dst = TensorDyn::image(
8217 1280,
8218 720,
8219 PixelFormat::Rgba,
8220 DType::U8,
8221 Some(TensorMemory::DmaBuf),
8222 edgefirst_tensor::CpuAccess::ReadWrite,
8223 )
8224 .unwrap();
8225 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
8226
8227 let (result, _src, dst) = convert_img(
8228 &mut gl_converter,
8229 src,
8230 dst,
8231 Rotation::None,
8232 Flip::None,
8233 Crop::no_crop(),
8234 );
8235 if let Err(crate::Error::NotSupported(msg)) = &result {
8238 crate::test_support::report_skip(&format!("{} — {msg}", function!()));
8239 return;
8240 }
8241 result.unwrap();
8242
8243 let target_image = TensorDyn::image(
8244 1280,
8245 720,
8246 PixelFormat::Rgba,
8247 DType::U8,
8248 None,
8249 edgefirst_tensor::CpuAccess::ReadWrite,
8250 )
8251 .unwrap();
8252 target_image
8253 .as_typed::<u8>()
8254 .unwrap()
8255 .map()
8256 .unwrap()
8257 .as_mut_slice()
8258 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
8259
8260 compare_images(&dst, &target_image, 0.98, function!());
8264 }
8265
8266 #[test]
8276 #[cfg(target_os = "macos")]
8277 #[cfg(feature = "opengl")]
8278 fn test_grey_r8_iosurface_to_rgba_opengl_macos() {
8279 let mut proc = match GLProcessorThreaded::new(None) {
8280 Ok(p) => p,
8281 Err(e) => {
8282 crate::test_support::report_skip(&format!(
8283 "{} — GL engine init failed ({e:?})",
8284 function!()
8285 ));
8286 return;
8287 }
8288 };
8289
8290 let (w, h) = (16usize, 16usize);
8291 let src = TensorDyn::image(
8292 w,
8293 h,
8294 PixelFormat::Grey,
8295 DType::U8,
8296 Some(TensorMemory::DmaBuf),
8297 edgefirst_tensor::CpuAccess::ReadWrite,
8298 )
8299 .expect("GREY IOSurface (R8/L008) should allocate — proves the FourCC mapping");
8300 {
8302 let su8 = src.as_typed::<u8>().unwrap();
8303 let stride = src
8304 .as_typed::<u8>()
8305 .unwrap()
8306 .effective_row_stride()
8307 .unwrap();
8308 let mut m = su8.map().unwrap();
8309 let buf = m.as_mut_slice();
8310 for y in 0..h {
8311 for x in 0..w {
8312 buf[y * stride + x] = ((x * 13 + y * 7) & 0xff) as u8;
8313 }
8314 }
8315 }
8316
8317 let dst = TensorDyn::image(
8318 w,
8319 h,
8320 PixelFormat::Rgba,
8321 DType::U8,
8322 Some(TensorMemory::DmaBuf),
8323 edgefirst_tensor::CpuAccess::ReadWrite,
8324 )
8325 .unwrap();
8326 let (result, src_back, dst) = convert_img(
8327 &mut proc,
8328 src,
8329 dst,
8330 Rotation::None,
8331 Flip::None,
8332 Crop::no_crop(),
8333 );
8334 result.expect("GREY(R8 IOSurface) → RGBA must convert on ANGLE (R8 binding works)");
8335
8336 let src_stride = src_back
8337 .as_typed::<u8>()
8338 .unwrap()
8339 .effective_row_stride()
8340 .unwrap();
8341 let src_map = src_back.as_typed::<u8>().unwrap().map().unwrap();
8342 let sbytes = src_map.as_slice();
8343 let dst_stride = dst
8344 .as_typed::<u8>()
8345 .unwrap()
8346 .effective_row_stride()
8347 .unwrap();
8348 let dst_map = dst.as_typed::<u8>().unwrap().map().unwrap();
8349 let dbytes = dst_map.as_slice();
8350 for y in 0..h {
8351 for x in 0..w {
8352 let yv = sbytes[y * src_stride + x] as i16;
8353 let p = y * dst_stride + x * 4;
8354 for c in 0..3 {
8355 assert!(
8356 (dbytes[p + c] as i16 - yv).abs() <= 2,
8357 "pixel ({x},{y}) ch{c} = {} expected ~{yv} (GREY→RGB identity)",
8358 dbytes[p + c]
8359 );
8360 }
8361 }
8362 }
8363 }
8364
8365 #[test]
8371 #[cfg(target_os = "macos")]
8372 #[cfg(feature = "opengl")]
8373 fn test_nv12_to_planar_f16_two_pass_opengl_macos() {
8374 let mut gpu = match GLProcessorThreaded::new(None) {
8375 Ok(p) => p,
8376 Err(e) => {
8377 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8378 return;
8379 }
8380 };
8381 let (w, h) = (64usize, 64usize);
8382 let Some(src) = crate::test_support::gpu_buffer_allocated(
8383 TensorDyn::image(
8384 w,
8385 h,
8386 PixelFormat::Nv12,
8387 DType::U8,
8388 Some(TensorMemory::DmaBuf),
8389 edgefirst_tensor::CpuAccess::ReadWrite,
8390 ),
8391 "NV12 IOSurface",
8392 ) else {
8393 return;
8394 };
8395 src.as_typed::<u8>()
8396 .unwrap()
8397 .map()
8398 .unwrap()
8399 .as_mut_slice()
8400 .fill(128); let Some(dst) = crate::test_support::gpu_buffer_allocated(
8403 TensorDyn::image(
8404 w,
8405 h,
8406 PixelFormat::PlanarRgb,
8407 DType::F16,
8408 Some(TensorMemory::DmaBuf),
8409 edgefirst_tensor::CpuAccess::ReadWrite,
8410 ),
8411 "F16 PlanarRgb IOSurface",
8412 ) else {
8413 return;
8414 };
8415 let mut dst = dst;
8416 if let Err(e) = ImageProcessorTrait::convert(&mut gpu, &src, &mut dst, None) {
8418 crate::test_support::report_skip(&format!(
8422 "{} — NV12→PlanarRgb F16 not available ({e:?})",
8423 function!()
8424 ));
8425 return;
8426 }
8427 let dt = dst.as_typed::<half::f16>().expect("dst is F16 PlanarRgb");
8428 let map = dt.map().unwrap();
8429 let vals = map.as_slice();
8430 let mut checked = 0usize;
8433 for &v in vals.iter() {
8434 let f = f32::from(v);
8435 assert!(
8436 (0.40..=0.60).contains(&f),
8437 "planar F16 value {f} not ~0.5 for neutral-grey NV12"
8438 );
8439 checked += 1;
8440 }
8441 assert!(
8442 checked >= w * h * 3,
8443 "expected >= 3 planes of samples, got {checked}"
8444 );
8445 }
8446
8447 #[test]
8455 #[cfg(target_os = "macos")]
8456 #[cfg(feature = "opengl")]
8457 fn test_nv12_to_planar_f16_two_pass_pool_opengl_macos() {
8458 let mut gpu = match GLProcessorThreaded::new(None) {
8459 Ok(p) => p,
8460 Err(e) => {
8461 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8462 return;
8463 }
8464 };
8465 let (fw, fh) = (96usize, 64usize);
8467 let (pool_w, pool_h) = (256usize, 768usize);
8468 let (model_w, model_h) = (128usize, 128usize);
8469
8470 let Some(mut src) = crate::test_support::gpu_buffer_allocated(
8471 TensorDyn::image(
8472 pool_w,
8473 pool_h,
8474 PixelFormat::Grey,
8475 DType::U8,
8476 Some(TensorMemory::DmaBuf),
8477 edgefirst_tensor::CpuAccess::ReadWrite,
8478 ),
8479 "R8 pool",
8480 ) else {
8481 return;
8482 };
8483 src.configure_image(fw, fh, PixelFormat::Nv12)
8484 .unwrap_or_else(|e| panic!("configure_image NV12 on pool: {e}"));
8485 let stride = src
8486 .as_typed::<u8>()
8487 .unwrap()
8488 .effective_row_stride()
8489 .unwrap();
8490 src.as_typed::<u8>()
8491 .unwrap()
8492 .map()
8493 .unwrap()
8494 .as_mut_slice()
8495 .fill(128); let Some(mut dst) = crate::test_support::gpu_buffer_allocated(
8498 TensorDyn::image(
8499 model_w,
8500 model_h,
8501 PixelFormat::PlanarRgb,
8502 DType::F16,
8503 Some(TensorMemory::DmaBuf),
8504 edgefirst_tensor::CpuAccess::ReadWrite,
8505 ),
8506 "F16 PlanarRgb dst",
8507 ) else {
8508 return;
8509 };
8510
8511 let _ = model_w;
8513 let crop = Crop::new()
8514 .with_source(Some(Region::new(0, 0, fw, fh)))
8515 .with_fit(Fit::Letterbox {
8516 pad: [0, 0, 0, 255],
8517 });
8518 if let Err(e) = ImageProcessorTrait::convert(
8519 &mut gpu,
8520 &src,
8521 &mut dst,
8522 Some(ConvertOptions::new().crop(crop)),
8523 ) {
8524 crate::test_support::report_skip(&format!(
8525 "{} — NV12→PlanarRgb F16 unavailable ({e:?})",
8526 function!()
8527 ));
8528 return;
8529 }
8530 let _ = stride;
8531 let dt = dst.as_typed::<half::f16>().expect("dst F16");
8534 let map = dt.map().unwrap();
8535 let any_half = map.as_slice().iter().any(|&v| {
8536 let f = f32::from(v);
8537 (0.40..=0.60).contains(&f)
8538 });
8539 assert!(any_half, "expected ~0.5 grey samples in the letterbox band");
8540 }
8541
8542 #[test]
8548 #[cfg(target_os = "macos")]
8549 #[cfg(feature = "opengl")]
8550 fn test_nv12_to_planar_f16_cross_thread_opengl_macos() {
8551 use std::sync::mpsc;
8552 let mut proc = match ImageProcessor::new() {
8555 Ok(p) => p,
8556 Err(e) => {
8557 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8558 return;
8559 }
8560 };
8561 let (fw, fh) = (96usize, 64usize);
8562 let Some(mut src) = crate::test_support::gpu_buffer_allocated(
8563 TensorDyn::image(
8564 256,
8565 768,
8566 PixelFormat::Grey,
8567 DType::U8,
8568 Some(TensorMemory::DmaBuf),
8569 edgefirst_tensor::CpuAccess::ReadWrite,
8570 ),
8571 "NV12 R8 pool",
8572 ) else {
8573 return;
8574 };
8575 src.configure_image(fw, fh, PixelFormat::Nv12).unwrap();
8576 src.as_typed::<u8>()
8577 .unwrap()
8578 .map()
8579 .unwrap()
8580 .as_mut_slice()
8581 .fill(128);
8582 let Some(mut dst) = crate::test_support::gpu_buffer_allocated(
8583 TensorDyn::image(
8584 128,
8585 128,
8586 PixelFormat::PlanarRgb,
8587 DType::F16,
8588 Some(TensorMemory::DmaBuf),
8589 edgefirst_tensor::CpuAccess::ReadWrite,
8590 ),
8591 "F16 PlanarRgb dst",
8592 ) else {
8593 return;
8594 };
8595 let crop = Crop::new().with_source(Some(Region::new(0, 0, fw, fh)));
8596
8597 let (tx, rx) = mpsc::channel::<bool>();
8600 let worker = std::thread::spawn(move || {
8601 let _ = ImageProcessorTrait::convert(
8602 &mut proc,
8603 &src,
8604 &mut dst,
8605 Some(ConvertOptions::new().crop(crop)),
8606 );
8607 let _ = tx.send(true);
8608 });
8609 match rx.recv_timeout(std::time::Duration::from_secs(20)) {
8610 Ok(_) => { let _ = worker.join(); }
8611 Err(_) => panic!(
8612 "cross-thread NV12→PlanarRgb convert HUNG (>20s) — reproduces the orchestrator deadlock"
8613 ),
8614 }
8615 }
8616
8617 #[test]
8624 #[cfg(target_os = "macos")]
8625 #[cfg(feature = "opengl")]
8626 fn test_nv_to_planar_f16_varying_sizes_no_leak_opengl_macos() {
8627 let mut gpu = match GLProcessorThreaded::new(None) {
8628 Ok(p) => p,
8629 Err(e) => {
8630 crate::test_support::report_skip(&format!("{} — init failed ({e:?})", function!()));
8631 return;
8632 }
8633 };
8634 let (max_w, max_h) = (640usize, 640usize);
8637 let depth = 4usize;
8638 let mut srcs = Vec::new();
8639 let mut dsts = Vec::new();
8640 for _ in 0..depth {
8641 let Some(src) = crate::test_support::gpu_buffer_allocated(
8642 TensorDyn::image(
8643 max_w,
8644 max_h * 3,
8645 PixelFormat::Grey,
8646 DType::U8,
8647 Some(TensorMemory::DmaBuf),
8648 edgefirst_tensor::CpuAccess::ReadWrite,
8649 ),
8650 "R8 pool",
8651 ) else {
8652 return;
8653 };
8654 srcs.push(src);
8655 let Some(dst) = crate::test_support::gpu_buffer_allocated(
8656 TensorDyn::image(
8657 640,
8658 640,
8659 PixelFormat::PlanarRgb,
8660 DType::F16,
8661 Some(TensorMemory::DmaBuf),
8662 edgefirst_tensor::CpuAccess::ReadWrite,
8663 ),
8664 "F16 PlanarRgb dst",
8665 ) else {
8666 return;
8667 };
8668 dsts.push(dst);
8669 }
8670 let sizes = [
8672 (640, 480),
8673 (500, 375),
8674 (640, 427),
8675 (333, 500),
8676 (480, 640),
8677 (612, 612),
8678 (428, 640),
8679 (576, 432),
8680 ];
8681 let mut first_ms = 0f64;
8682 let mut last_ms = 0f64;
8683 let iters = 40usize;
8684 for i in 0..iters {
8685 let (fw, fh) = sizes[i % sizes.len()];
8686 let src = &mut srcs[i % depth];
8687 let dst = &mut dsts[i % depth];
8688 src.configure_image(fw, fh, PixelFormat::Nv24).unwrap();
8689 src.as_typed::<u8>()
8690 .unwrap()
8691 .map()
8692 .unwrap()
8693 .as_mut_slice()
8694 .fill(128);
8695 let crop = Crop::new().with_source(Some(Region::new(0, 0, fw, fh)));
8696 let t0 = std::time::Instant::now();
8697 ImageProcessorTrait::convert(
8698 &mut gpu,
8699 src,
8700 dst,
8701 Some(ConvertOptions::new().crop(crop)),
8702 )
8703 .unwrap_or_else(|e| panic!("convert iter {i} ({fw}×{fh}): {e}"));
8704 let ms = t0.elapsed().as_secs_f64() * 1e3;
8705 if i == 2 {
8706 first_ms = ms;
8707 }
8708 if i == iters - 1 {
8709 last_ms = ms;
8710 }
8711 }
8712 eprintln!("first={first_ms:.2}ms last={last_ms:.2}ms");
8713 assert!(
8714 last_ms < first_ms * 5.0 + 5.0,
8715 "convert latency ran away: first {first_ms:.2}ms → last {last_ms:.2}ms (intermediate/pbuffer leak)"
8716 );
8717 }
8718
8719 #[test]
8726 #[cfg(target_os = "macos")]
8727 #[cfg(feature = "opengl")]
8728 fn test_nv12_nv16_nv24_to_rgba_opengl_macos() {
8729 let mut gpu = match GLProcessorThreaded::new(None) {
8730 Ok(p) => p,
8731 Err(e) => {
8732 crate::test_support::report_skip(&format!(
8733 "{} — GL engine init failed ({e:?})",
8734 function!()
8735 ));
8736 return;
8737 }
8738 };
8739 let mut cpu = CPUProcessor::new();
8740
8741 let fill = |buf: &mut [u8], stride: usize, fmt: PixelFormat, w: usize, h: usize| {
8753 for y in 0..h {
8754 for x in 0..w {
8755 buf[y * stride + x] = ((x * 9 + y * 5) & 0xff) as u8;
8756 }
8757 }
8758 let (cw, ch, uv_grid_rows) = match fmt {
8759 PixelFormat::Nv12 => (w / 2, h / 2, 1usize),
8760 PixelFormat::Nv16 => (w / 2, h, 1usize),
8761 _ => (w, h, 2usize), };
8763 let uv_plane = h * stride;
8764 for cy in 0..ch {
8765 for cx in 0..cw {
8766 let off = uv_plane + cy * uv_grid_rows * stride + cx * 2;
8767 buf[off] = ((cx * 11 + 30) & 0xff) as u8;
8768 buf[off + 1] = ((cy * 7 + 200) & 0xff) as u8;
8769 }
8770 }
8771 };
8772
8773 for fmt in [PixelFormat::Nv12, PixelFormat::Nv16, PixelFormat::Nv24] {
8774 for (w, h) in [
8775 (16usize, 16usize), (15, 16), (16, 15), ] {
8779 let mem = TensorDyn::image(
8780 w,
8781 h,
8782 fmt,
8783 DType::U8,
8784 None,
8785 edgefirst_tensor::CpuAccess::ReadWrite,
8786 )
8787 .unwrap();
8788 let mem_stride = mem
8789 .as_typed::<u8>()
8790 .unwrap()
8791 .effective_row_stride()
8792 .unwrap();
8793 fill(
8794 mem.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
8795 mem_stride,
8796 fmt,
8797 w,
8798 h,
8799 );
8800 let cpu_dst = TensorDyn::image(
8801 w,
8802 h,
8803 PixelFormat::Rgba,
8804 DType::U8,
8805 None,
8806 edgefirst_tensor::CpuAccess::ReadWrite,
8807 )
8808 .unwrap();
8809 let (r, _s, cpu_dst) = convert_img(
8810 &mut cpu,
8811 mem,
8812 cpu_dst,
8813 Rotation::None,
8814 Flip::None,
8815 Crop::no_crop(),
8816 );
8817 r.unwrap_or_else(|e| panic!("CPU {fmt:?}->{w}x{h}->RGBA: {e}"));
8818
8819 let ios = TensorDyn::image(
8820 w,
8821 h,
8822 fmt,
8823 DType::U8,
8824 Some(TensorMemory::DmaBuf),
8825 edgefirst_tensor::CpuAccess::ReadWrite,
8826 )
8827 .unwrap_or_else(|e| panic!("{fmt:?} {w}x{h} IOSurface alloc: {e}"));
8828 let ios_stride = ios
8829 .as_typed::<u8>()
8830 .unwrap()
8831 .effective_row_stride()
8832 .unwrap();
8833 fill(
8834 ios.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
8835 ios_stride,
8836 fmt,
8837 w,
8838 h,
8839 );
8840 let gpu_dst = TensorDyn::image(
8841 w,
8842 h,
8843 PixelFormat::Rgba,
8844 DType::U8,
8845 Some(TensorMemory::DmaBuf),
8846 edgefirst_tensor::CpuAccess::ReadWrite,
8847 )
8848 .unwrap();
8849 let (r, _s, gpu_dst) = convert_img(
8850 &mut gpu,
8851 ios,
8852 gpu_dst,
8853 Rotation::None,
8854 Flip::None,
8855 Crop::no_crop(),
8856 );
8857 r.unwrap_or_else(|e| panic!("GPU {fmt:?}->{w}x{h}->RGBA on ANGLE: {e}"));
8858
8859 let cs = cpu_dst
8860 .as_typed::<u8>()
8861 .unwrap()
8862 .effective_row_stride()
8863 .unwrap();
8864 let cmap = cpu_dst.as_typed::<u8>().unwrap().map().unwrap();
8865 let cb = cmap.as_slice();
8866 let gs = gpu_dst
8867 .as_typed::<u8>()
8868 .unwrap()
8869 .effective_row_stride()
8870 .unwrap();
8871 let gmap = gpu_dst.as_typed::<u8>().unwrap().map().unwrap();
8872 let gb = gmap.as_slice();
8873 let mut max_d = 0i16;
8874 for y in 0..h {
8875 for x in 0..w {
8876 for c in 0..3 {
8877 let cv = cb[y * cs + x * 4 + c] as i16;
8878 let gv = gb[y * gs + x * 4 + c] as i16;
8879 max_d = max_d.max((cv - gv).abs());
8880 }
8881 }
8882 }
8883 assert!(
8884 max_d <= 3,
8885 "{fmt:?} {w}x{h}: GPU vs CPU RGBA max channel diff {max_d} > 3"
8886 );
8887 }
8888 }
8889 }
8890
8891 #[test]
8899 #[cfg(target_os = "macos")]
8900 #[cfg(feature = "opengl")]
8901 fn test_nv_to_rgba_larger_pool_surface_opengl_macos() {
8902 let mut gpu = match GLProcessorThreaded::new(None) {
8903 Ok(p) => p,
8904 Err(e) => {
8905 crate::test_support::report_skip(&format!(
8906 "{} — GL engine init failed ({e:?})",
8907 function!()
8908 ));
8909 return;
8910 }
8911 };
8912 let mut cpu = CPUProcessor::new();
8913 let (pool_w, pool_h) = (256usize, 256usize);
8916
8917 let fill = |buf: &mut [u8], stride: usize, fmt: PixelFormat, w: usize, h: usize| {
8921 for y in 0..h {
8922 for x in 0..w {
8923 buf[y * stride + x] = ((x * 9 + y * 5) & 0xff) as u8;
8924 }
8925 }
8926 let (cw, ch, uv_grid_rows) = match fmt {
8927 PixelFormat::Nv12 => (w / 2, h / 2, 1usize),
8928 PixelFormat::Nv16 => (w / 2, h, 1usize),
8929 _ => (w, h, 2usize), };
8931 let uv_plane = h * stride;
8932 for cy in 0..ch {
8933 for cx in 0..cw {
8934 let off = uv_plane + cy * uv_grid_rows * stride + cx * 2;
8935 buf[off] = ((cx * 11 + 30) & 0xff) as u8;
8936 buf[off + 1] = ((cy * 7 + 200) & 0xff) as u8;
8937 }
8938 }
8939 };
8940
8941 for fmt in [PixelFormat::Nv12, PixelFormat::Nv16, PixelFormat::Nv24] {
8942 for (w, h) in [
8943 (40usize, 24usize), (15, 16), (16, 15), ] {
8947 let ew = w.next_multiple_of(2);
8950
8951 let mem = TensorDyn::image(
8953 w,
8954 h,
8955 fmt,
8956 DType::U8,
8957 None,
8958 edgefirst_tensor::CpuAccess::ReadWrite,
8959 )
8960 .unwrap();
8961 let mem_stride = mem
8962 .as_typed::<u8>()
8963 .unwrap()
8964 .effective_row_stride()
8965 .unwrap();
8966 fill(
8967 mem.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
8968 mem_stride,
8969 fmt,
8970 w,
8971 h,
8972 );
8973 let cpu_dst = TensorDyn::image(
8974 w,
8975 h,
8976 PixelFormat::Rgba,
8977 DType::U8,
8978 None,
8979 edgefirst_tensor::CpuAccess::ReadWrite,
8980 )
8981 .unwrap();
8982 let (r, _s, cpu_dst) = convert_img(
8983 &mut cpu,
8984 mem,
8985 cpu_dst,
8986 Rotation::None,
8987 Flip::None,
8988 Crop::no_crop(),
8989 );
8990 r.unwrap_or_else(|e| panic!("CPU {fmt:?}->{w}x{h}->RGBA: {e}"));
8991
8992 let Some(mut ios) = crate::test_support::gpu_buffer_allocated(
8996 TensorDyn::image(
8997 pool_w,
8998 pool_h,
8999 PixelFormat::Grey,
9000 DType::U8,
9001 Some(TensorMemory::DmaBuf),
9002 edgefirst_tensor::CpuAccess::ReadWrite,
9003 ),
9004 "R8 pool IOSurface",
9005 ) else {
9006 return;
9007 };
9008 ios.configure_image(w, h, fmt)
9009 .unwrap_or_else(|e| panic!("configure_image {fmt:?} {w}x{h} on pool: {e}"));
9010 let ios_stride = ios
9011 .as_typed::<u8>()
9012 .unwrap()
9013 .effective_row_stride()
9014 .unwrap();
9015 assert!(
9016 ios_stride > ew,
9017 "{fmt:?} {w}x{h}: pool stride {ios_stride} should exceed even width {ew} \
9018 (test must exercise padding)"
9019 );
9020 fill(
9021 ios.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice(),
9022 ios_stride,
9023 fmt,
9024 w,
9025 h,
9026 );
9027
9028 let gpu_dst = TensorDyn::image(
9029 w,
9030 h,
9031 PixelFormat::Rgba,
9032 DType::U8,
9033 Some(TensorMemory::DmaBuf),
9034 edgefirst_tensor::CpuAccess::ReadWrite,
9035 )
9036 .unwrap();
9037 let (r, _s, gpu_dst) = convert_img(
9038 &mut gpu,
9039 ios,
9040 gpu_dst,
9041 Rotation::None,
9042 Flip::None,
9043 Crop::no_crop(),
9044 );
9045 r.unwrap_or_else(|e| {
9046 panic!("GPU {fmt:?}->{w}x{h}->RGBA (pool surface) on ANGLE: {e}")
9047 });
9048
9049 let cs = cpu_dst
9050 .as_typed::<u8>()
9051 .unwrap()
9052 .effective_row_stride()
9053 .unwrap();
9054 let cmap = cpu_dst.as_typed::<u8>().unwrap().map().unwrap();
9055 let cb = cmap.as_slice();
9056 let gs = gpu_dst
9057 .as_typed::<u8>()
9058 .unwrap()
9059 .effective_row_stride()
9060 .unwrap();
9061 let gmap = gpu_dst.as_typed::<u8>().unwrap().map().unwrap();
9062 let gb = gmap.as_slice();
9063 let mut max_d = 0i16;
9064 for y in 0..h {
9065 for x in 0..w {
9066 for c in 0..3 {
9067 let cv = cb[y * cs + x * 4 + c] as i16;
9068 let gv = gb[y * gs + x * 4 + c] as i16;
9069 max_d = max_d.max((cv - gv).abs());
9070 }
9071 }
9072 }
9073 assert!(
9074 max_d <= 3,
9075 "{fmt:?} {w}x{h}: GPU(pool surface) vs CPU RGBA max channel diff {max_d} > 3"
9076 );
9077 }
9078 }
9079 }
9080
9081 #[test]
9082 #[cfg(target_os = "macos")]
9083 #[cfg(feature = "opengl")]
9084 fn test_yuyv_to_rgba_opengl_macos() {
9085 let mut proc = match GLProcessorThreaded::new(None) {
9086 Ok(p) => p,
9087 Err(e) => {
9088 crate::test_support::report_skip(&format!(
9089 "{} — GL engine init failed ({e:?}). \
9090 Install ANGLE via `brew install startergo/angle/angle` \
9091 and re-sign per README.md § macOS GPU Acceleration to \
9092 run this test.",
9093 function!()
9094 ));
9095 return;
9096 }
9097 };
9098
9099 let src = load_bytes_to_tensor(
9100 1280,
9101 720,
9102 PixelFormat::Yuyv,
9103 Some(TensorMemory::DmaBuf),
9104 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9105 )
9106 .unwrap();
9107
9108 let dst = TensorDyn::image(
9109 1280,
9110 720,
9111 PixelFormat::Rgba,
9112 DType::U8,
9113 Some(TensorMemory::DmaBuf),
9114 edgefirst_tensor::CpuAccess::ReadWrite,
9115 )
9116 .unwrap();
9117
9118 let (result, _src, dst) = convert_img(
9119 &mut proc,
9120 src,
9121 dst,
9122 Rotation::None,
9123 Flip::None,
9124 Crop::no_crop(),
9125 );
9126 result.unwrap();
9127
9128 let target_image = TensorDyn::image(
9129 1280,
9130 720,
9131 PixelFormat::Rgba,
9132 DType::U8,
9133 None,
9134 edgefirst_tensor::CpuAccess::ReadWrite,
9135 )
9136 .unwrap();
9137 target_image
9138 .as_typed::<u8>()
9139 .unwrap()
9140 .map()
9141 .unwrap()
9142 .as_mut_slice()
9143 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
9144
9145 compare_images(&dst, &target_image, 0.98, function!());
9150 }
9151
9152 #[test]
9171 #[cfg(target_os = "macos")]
9172 #[cfg(feature = "opengl")]
9173 fn test_yuyv_to_rgba_opengl_macos_multi_resolution() {
9174 let mut proc = match GLProcessorThreaded::new(None) {
9175 Ok(p) => p,
9176 Err(e) => {
9177 crate::test_support::report_skip(&format!(
9178 "{} — GL engine init failed ({e:?})",
9179 function!()
9180 ));
9181 return;
9182 }
9183 };
9184
9185 for (w, h) in [(64usize, 32usize), (3840, 2160)] {
9186 let bytes_per_row = w * 2;
9189 let mut yuyv = vec![0u8; bytes_per_row * h];
9190 for chunk in yuyv.as_chunks_mut::<4>().0 {
9191 chunk[0] = 128; chunk[1] = 128; chunk[2] = 128; chunk[3] = 128; }
9196
9197 let src =
9198 load_bytes_to_tensor(w, h, PixelFormat::Yuyv, Some(TensorMemory::DmaBuf), &yuyv)
9199 .unwrap();
9200
9201 let dst = TensorDyn::image(
9202 w,
9203 h,
9204 PixelFormat::Rgba,
9205 DType::U8,
9206 Some(TensorMemory::DmaBuf),
9207 edgefirst_tensor::CpuAccess::ReadWrite,
9208 )
9209 .unwrap();
9210
9211 let (result, _src, dst) = convert_img(
9212 &mut proc,
9213 src,
9214 dst,
9215 Rotation::None,
9216 Flip::None,
9217 Crop::no_crop(),
9218 );
9219 result.expect("GL convert should succeed at this resolution");
9220
9221 let dst_u8 = dst.as_typed::<u8>().unwrap();
9226 let dst_map = dst_u8.map().unwrap();
9227 let dst_bytes = dst_map.as_slice();
9228 assert_eq!(dst_bytes.len(), w * h * 4, "RGBA byte count");
9229 for px in dst_bytes.as_chunks::<4>().0 {
9230 for (i, &c) in px[..3].iter().enumerate() {
9231 assert!(
9232 (120..=140).contains(&c),
9233 "{}: channel {i} = {c} (expected ~128 ±12) at {w}×{h}",
9234 function!(),
9235 );
9236 }
9237 assert_eq!(px[3], 255, "alpha must be 1.0");
9238 }
9239 }
9240 }
9241
9242 #[test]
9252 #[cfg(target_os = "macos")]
9253 #[cfg(feature = "opengl")]
9254 fn test_macos_gl_pbuffer_cache_reuses_surfaces() {
9255 let mut proc = match GLProcessorThreaded::new(None) {
9256 Ok(p) => p,
9257 Err(e) => {
9258 crate::test_support::report_skip(&format!(
9259 "{} — GL engine init failed ({e:?})",
9260 function!()
9261 ));
9262 return;
9263 }
9264 };
9265
9266 let mut yuyv = vec![0u8; 64 * 32 * 2];
9268 for chunk in yuyv.as_chunks_mut::<4>().0 {
9269 chunk[0] = 200;
9270 chunk[1] = 100;
9271 chunk[2] = 200;
9272 chunk[3] = 156;
9273 }
9274 let src =
9275 load_bytes_to_tensor(64, 32, PixelFormat::Yuyv, Some(TensorMemory::DmaBuf), &yuyv)
9276 .unwrap();
9277 let dst = TensorDyn::image(
9278 64,
9279 32,
9280 PixelFormat::Rgba,
9281 DType::U8,
9282 Some(TensorMemory::DmaBuf),
9283 edgefirst_tensor::CpuAccess::ReadWrite,
9284 )
9285 .unwrap();
9286
9287 let (r1, src, dst) = convert_img(
9288 &mut proc,
9289 src,
9290 dst,
9291 Rotation::None,
9292 Flip::None,
9293 Crop::no_crop(),
9294 );
9295 r1.unwrap();
9296 let first: Vec<u8> = dst
9297 .as_typed::<u8>()
9298 .unwrap()
9299 .map()
9300 .unwrap()
9301 .as_slice()
9302 .to_vec();
9303
9304 let (r2, _src, dst) = convert_img(
9305 &mut proc,
9306 src,
9307 dst,
9308 Rotation::None,
9309 Flip::None,
9310 Crop::no_crop(),
9311 );
9312 r2.unwrap();
9313 let second: Vec<u8> = dst
9314 .as_typed::<u8>()
9315 .unwrap()
9316 .map()
9317 .unwrap()
9318 .as_slice()
9319 .to_vec();
9320
9321 assert_eq!(first, second, "cache-hit conversion must be deterministic");
9322 }
9323
9324 #[test]
9332 #[cfg(target_os = "macos")]
9333 #[cfg(feature = "opengl")]
9334 fn test_macos_gl_pbuffer_cache_steady_state() {
9335 let mut proc = match GLProcessorThreaded::new(None) {
9336 Ok(p) => p,
9337 Err(e) => {
9338 crate::test_support::report_skip(&format!(
9339 "{} — GL engine init failed ({e:?})",
9340 function!()
9341 ));
9342 return;
9343 }
9344 };
9345
9346 let (w, h) = (64usize, 32usize);
9347 const POOL: usize = 3;
9348 const FRAMES: usize = 100;
9349
9350 let yuyv = vec![128u8; w * h * 2];
9351 let pool: Vec<TensorDyn> = (0..POOL)
9352 .map(|_| {
9353 load_bytes_to_tensor(w, h, PixelFormat::Yuyv, Some(TensorMemory::DmaBuf), &yuyv)
9354 .unwrap()
9355 })
9356 .collect();
9357 let mut dst = TensorDyn::image(
9358 w,
9359 h,
9360 PixelFormat::Rgba,
9361 DType::U8,
9362 Some(TensorMemory::DmaBuf),
9363 edgefirst_tensor::CpuAccess::ReadWrite,
9364 )
9365 .unwrap();
9366
9367 for src in pool.iter().cycle().take(POOL * 2) {
9369 proc.convert(src, &mut dst, None).unwrap();
9370 }
9371 let warm = proc.egl_cache_stats().unwrap();
9372
9373 for src in pool.iter().cycle().take(FRAMES) {
9374 proc.convert(src, &mut dst, None).unwrap();
9375 }
9376 let steady = proc.egl_cache_stats().unwrap();
9377
9378 assert_eq!(
9379 warm.total_misses(),
9380 steady.total_misses(),
9381 "steady-state loop created new imports (warm {warm:?}, steady {steady:?})"
9382 );
9383 let hits = |s: &GlCacheStats| s.src.hits + s.dst.hits + s.nv_r8.hits + s.nv_uv.hits;
9384 assert!(
9385 hits(&steady) - hits(&warm) >= FRAMES as u64,
9386 "expected at least {FRAMES} import-cache hits over the loop, got {}",
9387 hits(&steady) - hits(&warm)
9388 );
9389 }
9390
9391 #[test]
9403 #[cfg(target_os = "macos")]
9404 #[cfg(feature = "opengl")]
9405 fn test_macos_gl_f16_planar_is_gl_backed() {
9406 let mut proc = ImageProcessor::new().expect("ImageProcessor");
9407 let Some(ref gl) = proc.opengl else {
9408 crate::test_support::report_skip(&format!("{} — GL backend unavailable", function!()));
9409 return;
9410 };
9411 if !gl.supported_render_dtypes().f16 {
9412 crate::test_support::report_skip(&format!(
9413 "{} — configuration lacks F16 color-buffer support",
9414 function!()
9415 ));
9416 return;
9417 }
9418 let stats_before = gl.egl_cache_stats().expect("cache stats");
9419
9420 let src = TensorDyn::image(
9421 1280,
9422 720,
9423 PixelFormat::Nv12,
9424 DType::U8,
9425 Some(TensorMemory::DmaBuf),
9426 edgefirst_tensor::CpuAccess::ReadWrite,
9427 )
9428 .unwrap();
9429 {
9430 let t = src.as_typed::<u8>().unwrap();
9431 let mut m = t.map().unwrap();
9432 for (i, b) in m.as_mut_slice().iter_mut().enumerate() {
9433 *b = ((i * 31) % 211) as u8;
9434 }
9435 }
9436 let mut dst = TensorDyn::image(
9437 640,
9438 640,
9439 PixelFormat::PlanarRgb,
9440 DType::F16,
9441 Some(TensorMemory::DmaBuf),
9442 edgefirst_tensor::CpuAccess::ReadWrite,
9443 )
9444 .unwrap();
9445
9446 proc.convert(
9447 &src,
9448 &mut dst,
9449 Some(crate::ConvertOptions::new().crop(Crop::letterbox([114, 114, 114, 255]))),
9450 )
9451 .expect("F16 capability reported but the NV12→PlanarF16 convert failed");
9452 let stats_after = proc
9453 .opengl
9454 .as_ref()
9455 .expect("GL backend present")
9456 .egl_cache_stats()
9457 .expect("cache stats");
9458 assert!(
9463 stats_after.total_misses() >= stats_before.total_misses() + 2,
9464 "convert succeeded but the GL engine did not import both the \
9465 source and the F16 destination — the work did not (fully) run \
9466 on the GL backend (silent CPU fallback); misses before={} after={}",
9467 stats_before.total_misses(),
9468 stats_after.total_misses()
9469 );
9470 }
9471
9472 #[test]
9478 #[cfg(feature = "opengl")]
9479 fn test_nv12_to_planar_f16_fused_engine_vs_cpu() {
9480 let mut gl = match ImageProcessor::with_config(ImageProcessorConfig {
9481 backend: ComputeBackend::OpenGl,
9482 ..Default::default()
9483 }) {
9484 Ok(p) if p.opengl.is_some() => p,
9485 _ => {
9486 crate::test_support::report_skip(&format!(
9487 "{} — GL backend unavailable",
9488 function!()
9489 ));
9490 return;
9491 }
9492 };
9493 if !gl
9494 .opengl
9495 .as_ref()
9496 .map(|g| g.supported_render_dtypes().f16)
9497 .unwrap_or(false)
9498 {
9499 crate::test_support::report_skip(&format!("{} — no F16 render support", function!()));
9500 return;
9501 }
9502 let mem = if crate::test_support::is_gpu_buffer_available() {
9503 TensorMemory::DmaBuf
9504 } else {
9505 crate::test_support::report_skip(&format!("{} — no zero-copy buffers", function!()));
9506 return;
9507 };
9508
9509 let src = TensorDyn::image(
9510 1280,
9511 720,
9512 PixelFormat::Nv12,
9513 DType::U8,
9514 Some(mem),
9515 edgefirst_tensor::CpuAccess::ReadWrite,
9516 )
9517 .unwrap();
9518 {
9519 let t = src.as_typed::<u8>().unwrap();
9525 let mut m = t.map().unwrap();
9526 let buf = m.as_mut_slice();
9527 let (w, h) = (1280usize, 720usize);
9528 for y in 0..h {
9529 for x in 0..w {
9530 buf[y * w + x] = ((x * 255) / w) as u8; }
9532 }
9533 for y in 0..(h / 2) {
9534 for x in 0..(w / 2) {
9535 let o = h * w + y * w + 2 * x;
9536 buf[o] = ((y * 255) / (h / 2)) as u8; buf[o + 1] = (((x + y) * 255) / (w / 2 + h / 2)) as u8; }
9539 }
9540 }
9541 let crop = Crop::letterbox([114, 114, 114, 255]);
9542 let mut gl_dst = TensorDyn::image(
9543 640,
9544 640,
9545 PixelFormat::PlanarRgb,
9546 DType::F16,
9547 Some(mem),
9548 edgefirst_tensor::CpuAccess::ReadWrite,
9549 )
9550 .unwrap();
9551 match gl.opengl.as_mut().expect("GL backend present").convert(
9558 &src,
9559 &mut gl_dst,
9560 Some(crate::ConvertOptions::new().crop(crop)),
9561 ) {
9562 Ok(()) => {}
9563 Err(crate::Error::NotSupported(msg)) => {
9564 crate::test_support::report_skip(&format!("{} — {msg}", function!()));
9565 return;
9566 }
9567 Err(e) => panic!("fused NV12→PlanarF16 GL convert: {e}"),
9568 }
9569
9570 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
9571 backend: ComputeBackend::Cpu,
9572 ..Default::default()
9573 })
9574 .unwrap();
9575 let mut cpu_dst = TensorDyn::image(
9576 640,
9577 640,
9578 PixelFormat::PlanarRgb,
9579 DType::F16,
9580 Some(TensorMemory::Mem),
9581 edgefirst_tensor::CpuAccess::ReadWrite,
9582 )
9583 .unwrap();
9584 cpu.convert(
9585 &src,
9586 &mut cpu_dst,
9587 Some(crate::ConvertOptions::new().crop(crop)),
9588 )
9589 .expect("CPU reference convert");
9590
9591 let g = gl_dst
9592 .as_typed::<half::f16>()
9593 .unwrap()
9594 .map()
9595 .unwrap()
9596 .as_slice()
9597 .to_vec();
9598 let c = cpu_dst
9599 .as_typed::<half::f16>()
9600 .unwrap()
9601 .map()
9602 .unwrap()
9603 .as_slice()
9604 .to_vec();
9605 assert_eq!(g.len(), c.len());
9606 let mut max_diff = 0.0f32;
9607 let mut max_at = 0usize;
9608 for (i, (a, b)) in g.iter().zip(c.iter()).enumerate() {
9609 let d = (a.to_f32() - b.to_f32()).abs();
9610 if d > max_diff {
9611 max_diff = d;
9612 max_at = i;
9613 }
9614 }
9615 let (plane, rem) = (max_at / (640 * 640), max_at % (640 * 640));
9617 let (row, col) = (rem / 640, rem % 640);
9618 eprintln!(
9619 "fused-vs-cpu: max_diff={max_diff} at plane={plane} row={row} col={col} \
9620 gl={} cpu={}",
9621 g[max_at].to_f32(),
9622 c[max_at].to_f32()
9623 );
9624 assert!(
9627 max_diff <= 4.0 / 255.0 + 1e-3,
9628 "fused NV12→PlanarF16 diverges from CPU reference: max_diff={max_diff}"
9629 );
9630 }
9631
9632 #[test]
9642 #[cfg(feature = "opengl")]
9643 fn test_zero_copy_src_to_mem_dst_gl_direct() {
9644 let mut proc = match ImageProcessor::new() {
9645 Ok(p) if p.opengl.is_some() => p,
9646 _ => {
9647 crate::test_support::report_skip(&format!(
9648 "{} — GL backend unavailable",
9649 function!()
9650 ));
9651 return;
9652 }
9653 };
9654 if !crate::test_support::is_gpu_buffer_available() {
9655 crate::test_support::report_skip(&format!("{} — no zero-copy buffers", function!()));
9656 return;
9657 }
9658
9659 let src = TensorDyn::image(
9660 1280,
9661 720,
9662 PixelFormat::Rgba,
9663 DType::U8,
9664 Some(TensorMemory::DmaBuf),
9665 edgefirst_tensor::CpuAccess::ReadWrite,
9666 )
9667 .unwrap();
9668 {
9669 let t = src.as_typed::<u8>().unwrap();
9670 let mut m = t.map().unwrap();
9671 for (i, b) in m.as_mut_slice().iter_mut().enumerate() {
9672 *b = ((i * 31) % 211) as u8;
9673 }
9674 }
9675 let mut gl_dst = TensorDyn::image(
9676 1280,
9677 720,
9678 PixelFormat::Bgra,
9679 DType::U8,
9680 Some(TensorMemory::Mem),
9681 edgefirst_tensor::CpuAccess::ReadWrite,
9682 )
9683 .unwrap();
9684 proc.opengl
9685 .as_mut()
9686 .expect("GL backend present")
9687 .convert(&src, &mut gl_dst, None)
9688 .expect("zero-copy src → heap dst GL convert");
9689
9690 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
9691 backend: ComputeBackend::Cpu,
9692 ..Default::default()
9693 })
9694 .unwrap();
9695 let mut cpu_dst = TensorDyn::image(
9696 1280,
9697 720,
9698 PixelFormat::Bgra,
9699 DType::U8,
9700 Some(TensorMemory::Mem),
9701 edgefirst_tensor::CpuAccess::ReadWrite,
9702 )
9703 .unwrap();
9704 cpu.convert(&src, &mut cpu_dst, None)
9705 .expect("CPU reference convert");
9706
9707 let g = gl_dst
9708 .as_typed::<u8>()
9709 .unwrap()
9710 .map()
9711 .unwrap()
9712 .as_slice()
9713 .to_vec();
9714 let c = cpu_dst
9715 .as_typed::<u8>()
9716 .unwrap()
9717 .map()
9718 .unwrap()
9719 .as_slice()
9720 .to_vec();
9721 assert_eq!(g.len(), c.len());
9722 let max_diff = g
9723 .iter()
9724 .zip(c.iter())
9725 .map(|(a, b)| a.abs_diff(*b))
9726 .max()
9727 .unwrap();
9728 assert!(
9731 max_diff <= 2,
9732 "zero-copy src → heap dst diverges from CPU reference: max_diff={max_diff}"
9733 );
9734 }
9735
9736 #[test]
9737 #[cfg(target_os = "linux")]
9738 fn test_yuyv_to_rgb_g2d() {
9739 if !is_g2d_available() {
9740 crate::test_support::report_skip(
9741 "test_yuyv_to_rgb_g2d - G2D library (libg2d.so.2) not available",
9742 );
9743 return;
9744 }
9745 if !is_dma_available() {
9746 crate::test_support::report_skip("test_yuyv_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
9747 return;
9748 }
9749
9750 let src = load_bytes_to_tensor(
9751 1280,
9752 720,
9753 PixelFormat::Yuyv,
9754 None,
9755 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9756 )
9757 .unwrap();
9758
9759 let g2d_dst = TensorDyn::image(
9760 1280,
9761 720,
9762 PixelFormat::Rgb,
9763 DType::U8,
9764 Some(TensorMemory::DmaBuf),
9765 edgefirst_tensor::CpuAccess::ReadWrite,
9766 )
9767 .unwrap();
9768 let mut g2d_converter = G2DProcessor::new().unwrap();
9769
9770 let (result, src, g2d_dst) = convert_img(
9771 &mut g2d_converter,
9772 src,
9773 g2d_dst,
9774 Rotation::None,
9775 Flip::None,
9776 Crop::no_crop(),
9777 );
9778 result.unwrap();
9779
9780 let cpu_dst = TensorDyn::image(
9781 1280,
9782 720,
9783 PixelFormat::Rgb,
9784 DType::U8,
9785 None,
9786 edgefirst_tensor::CpuAccess::ReadWrite,
9787 )
9788 .unwrap();
9789 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
9790
9791 let (result, _src, cpu_dst) = convert_img(
9792 &mut cpu_converter,
9793 src,
9794 cpu_dst,
9795 Rotation::None,
9796 Flip::None,
9797 Crop::no_crop(),
9798 );
9799 result.unwrap();
9800
9801 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
9807 }
9808
9809 #[test]
9810 #[cfg(target_os = "linux")]
9811 fn test_yuyv_to_yuyv_resize_g2d() {
9812 if !is_g2d_available() {
9813 crate::test_support::report_skip(
9814 "test_yuyv_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available",
9815 );
9816 return;
9817 }
9818 if !is_dma_available() {
9819 crate::test_support::report_skip("test_yuyv_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
9820 return;
9821 }
9822
9823 let src = load_bytes_to_tensor(
9824 1280,
9825 720,
9826 PixelFormat::Yuyv,
9827 None,
9828 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9829 )
9830 .unwrap();
9831
9832 let g2d_dst = TensorDyn::image(
9833 600,
9834 400,
9835 PixelFormat::Yuyv,
9836 DType::U8,
9837 Some(TensorMemory::DmaBuf),
9838 edgefirst_tensor::CpuAccess::ReadWrite,
9839 )
9840 .unwrap();
9841 let mut g2d_converter = G2DProcessor::new().unwrap();
9842
9843 let (result, src, g2d_dst) = convert_img(
9844 &mut g2d_converter,
9845 src,
9846 g2d_dst,
9847 Rotation::None,
9848 Flip::None,
9849 Crop::no_crop(),
9850 );
9851 result.unwrap();
9852
9853 let cpu_dst = TensorDyn::image(
9854 600,
9855 400,
9856 PixelFormat::Yuyv,
9857 DType::U8,
9858 None,
9859 edgefirst_tensor::CpuAccess::ReadWrite,
9860 )
9861 .unwrap();
9862 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
9863
9864 let (result, _src, cpu_dst) = convert_img(
9865 &mut cpu_converter,
9866 src,
9867 cpu_dst,
9868 Rotation::None,
9869 Flip::None,
9870 Crop::no_crop(),
9871 );
9872 result.unwrap();
9873
9874 eprintln!(
9881 "WARNING: G2D has poor colorimetry support — YUYV resize diverges from the \
9882 CPU reference (~0.85 similarity); threshold held at 0.85, not 0.95."
9883 );
9884 compare_images_convert_to_rgb(&g2d_dst, &cpu_dst, 0.85, function!());
9885 }
9886
9887 #[test]
9888 fn test_yuyv_to_rgba_resize_cpu() {
9889 let src = load_bytes_to_tensor(
9890 1280,
9891 720,
9892 PixelFormat::Yuyv,
9893 None,
9894 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9895 )
9896 .unwrap();
9897
9898 let (dst_width, dst_height) = (960, 540);
9899
9900 let dst = TensorDyn::image(
9901 dst_width,
9902 dst_height,
9903 PixelFormat::Rgba,
9904 DType::U8,
9905 None,
9906 edgefirst_tensor::CpuAccess::ReadWrite,
9907 )
9908 .unwrap();
9909 let mut cpu_converter = CPUProcessor::new();
9910
9911 let (result, _src, dst) = convert_img(
9912 &mut cpu_converter,
9913 src,
9914 dst,
9915 Rotation::None,
9916 Flip::None,
9917 Crop::no_crop(),
9918 );
9919 result.unwrap();
9920
9921 let dst_target = TensorDyn::image(
9922 dst_width,
9923 dst_height,
9924 PixelFormat::Rgba,
9925 DType::U8,
9926 None,
9927 edgefirst_tensor::CpuAccess::ReadWrite,
9928 )
9929 .unwrap();
9930 let src_target = load_bytes_to_tensor(
9931 1280,
9932 720,
9933 PixelFormat::Rgba,
9934 None,
9935 &edgefirst_bench::testdata::read("camera720p.rgba"),
9936 )
9937 .unwrap();
9938 let (result, _src_target, dst_target) = convert_img(
9939 &mut cpu_converter,
9940 src_target,
9941 dst_target,
9942 Rotation::None,
9943 Flip::None,
9944 Crop::no_crop(),
9945 );
9946 result.unwrap();
9947
9948 compare_images(&dst, &dst_target, 0.98, function!());
9951 }
9952
9953 #[test]
9954 #[cfg(target_os = "linux")]
9955 fn test_yuyv_to_rgba_crop_flip_g2d() {
9956 if !is_g2d_available() {
9957 crate::test_support::report_skip(
9958 "test_yuyv_to_rgba_crop_flip_g2d - G2D library (libg2d.so.2) not available",
9959 );
9960 return;
9961 }
9962 if !is_dma_available() {
9963 crate::test_support::report_skip("test_yuyv_to_rgba_crop_flip_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
9964 return;
9965 }
9966
9967 let src = load_bytes_to_tensor(
9968 1280,
9969 720,
9970 PixelFormat::Yuyv,
9971 Some(TensorMemory::DmaBuf),
9972 &edgefirst_bench::testdata::read("camera720p.yuyv"),
9973 )
9974 .unwrap();
9975
9976 let (dst_width, dst_height) = (640, 640);
9977
9978 let dst_g2d = TensorDyn::image(
9979 dst_width,
9980 dst_height,
9981 PixelFormat::Rgba,
9982 DType::U8,
9983 Some(TensorMemory::DmaBuf),
9984 edgefirst_tensor::CpuAccess::ReadWrite,
9985 )
9986 .unwrap();
9987 let mut g2d_converter = G2DProcessor::new().unwrap();
9988 let crop = Crop::new().with_source(Some(Region::new(20, 15, 400, 300)));
9989
9990 let (result, src, dst_g2d) = convert_img(
9991 &mut g2d_converter,
9992 src,
9993 dst_g2d,
9994 Rotation::None,
9995 Flip::Horizontal,
9996 crop,
9997 );
9998 result.unwrap();
9999
10000 let dst_cpu = TensorDyn::image(
10001 dst_width,
10002 dst_height,
10003 PixelFormat::Rgba,
10004 DType::U8,
10005 Some(TensorMemory::DmaBuf),
10006 edgefirst_tensor::CpuAccess::ReadWrite,
10007 )
10008 .unwrap();
10009 let mut cpu_converter = CPUProcessor::new();
10010
10011 let (result, _src, dst_cpu) = convert_img(
10012 &mut cpu_converter,
10013 src,
10014 dst_cpu,
10015 Rotation::None,
10016 Flip::Horizontal,
10017 crop,
10018 );
10019 result.unwrap();
10020 compare_images(&dst_g2d, &dst_cpu, 0.98, function!());
10026 }
10027
10028 #[test]
10029 #[cfg(any(
10030 target_os = "linux",
10031 target_os = "macos",
10032 target_os = "ios",
10033 target_os = "android",
10034 target_os = "windows"
10035 ))]
10036 #[cfg(feature = "opengl")]
10037 fn test_yuyv_to_rgba_crop_flip_opengl() {
10038 if !is_opengl_available() {
10039 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
10040 return;
10041 }
10042
10043 if !crate::test_support::is_gpu_buffer_available() {
10044 crate::test_support::report_skip(&format!(
10045 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
10046 function!()
10047 ));
10048 return;
10049 }
10050
10051 let src = load_bytes_to_tensor(
10052 1280,
10053 720,
10054 PixelFormat::Yuyv,
10055 Some(TensorMemory::DmaBuf),
10056 &edgefirst_bench::testdata::read("camera720p.yuyv"),
10057 )
10058 .unwrap();
10059
10060 let (dst_width, dst_height) = (640, 640);
10061
10062 let dst_gl = TensorDyn::image(
10063 dst_width,
10064 dst_height,
10065 PixelFormat::Rgba,
10066 DType::U8,
10067 Some(TensorMemory::DmaBuf),
10068 edgefirst_tensor::CpuAccess::ReadWrite,
10069 )
10070 .unwrap();
10071 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
10072 let crop = Crop::new().with_source(Some(Region::new(20, 15, 400, 300)));
10073
10074 let (result, src, dst_gl) = convert_img(
10075 &mut gl_converter,
10076 src,
10077 dst_gl,
10078 Rotation::None,
10079 Flip::Horizontal,
10080 crop,
10081 );
10082 if let Err(crate::Error::NotSupported(msg)) = &result {
10084 crate::test_support::report_skip(&format!("{} — {msg}", function!()));
10085 return;
10086 }
10087 result.unwrap();
10088
10089 let dst_cpu = TensorDyn::image(
10090 dst_width,
10091 dst_height,
10092 PixelFormat::Rgba,
10093 DType::U8,
10094 Some(TensorMemory::DmaBuf),
10095 edgefirst_tensor::CpuAccess::ReadWrite,
10096 )
10097 .unwrap();
10098 let mut cpu_converter = CPUProcessor::new();
10099
10100 let (result, _src, dst_cpu) = convert_img(
10101 &mut cpu_converter,
10102 src,
10103 dst_cpu,
10104 Rotation::None,
10105 Flip::Horizontal,
10106 crop,
10107 );
10108 result.unwrap();
10109 compare_images(&dst_gl, &dst_cpu, 0.98, function!());
10114 }
10115
10116 #[test]
10117 fn test_vyuy_to_rgba_cpu() {
10118 let file = edgefirst_bench::testdata::read("camera720p.vyuy").to_vec();
10119 let src = TensorDyn::image(
10120 1280,
10121 720,
10122 PixelFormat::Vyuy,
10123 DType::U8,
10124 None,
10125 edgefirst_tensor::CpuAccess::ReadWrite,
10126 )
10127 .unwrap();
10128 src.as_typed::<u8>()
10129 .unwrap()
10130 .map()
10131 .unwrap()
10132 .as_mut_slice()
10133 .copy_from_slice(&file);
10134
10135 let dst = TensorDyn::image(
10136 1280,
10137 720,
10138 PixelFormat::Rgba,
10139 DType::U8,
10140 None,
10141 edgefirst_tensor::CpuAccess::ReadWrite,
10142 )
10143 .unwrap();
10144 let mut cpu_converter = CPUProcessor::new();
10145
10146 let (result, _src, dst) = convert_img(
10147 &mut cpu_converter,
10148 src,
10149 dst,
10150 Rotation::None,
10151 Flip::None,
10152 Crop::no_crop(),
10153 );
10154 result.unwrap();
10155
10156 let target_image = TensorDyn::image(
10157 1280,
10158 720,
10159 PixelFormat::Rgba,
10160 DType::U8,
10161 None,
10162 edgefirst_tensor::CpuAccess::ReadWrite,
10163 )
10164 .unwrap();
10165 target_image
10166 .as_typed::<u8>()
10167 .unwrap()
10168 .map()
10169 .unwrap()
10170 .as_mut_slice()
10171 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
10172
10173 compare_images(&dst, &target_image, 0.98, function!());
10176 }
10177
10178 #[test]
10179 fn test_vyuy_to_rgb_cpu() {
10180 let file = edgefirst_bench::testdata::read("camera720p.vyuy").to_vec();
10181 let src = TensorDyn::image(
10182 1280,
10183 720,
10184 PixelFormat::Vyuy,
10185 DType::U8,
10186 None,
10187 edgefirst_tensor::CpuAccess::ReadWrite,
10188 )
10189 .unwrap();
10190 src.as_typed::<u8>()
10191 .unwrap()
10192 .map()
10193 .unwrap()
10194 .as_mut_slice()
10195 .copy_from_slice(&file);
10196
10197 let dst = TensorDyn::image(
10198 1280,
10199 720,
10200 PixelFormat::Rgb,
10201 DType::U8,
10202 None,
10203 edgefirst_tensor::CpuAccess::ReadWrite,
10204 )
10205 .unwrap();
10206 let mut cpu_converter = CPUProcessor::new();
10207
10208 let (result, _src, dst) = convert_img(
10209 &mut cpu_converter,
10210 src,
10211 dst,
10212 Rotation::None,
10213 Flip::None,
10214 Crop::no_crop(),
10215 );
10216 result.unwrap();
10217
10218 let target_image = TensorDyn::image(
10219 1280,
10220 720,
10221 PixelFormat::Rgb,
10222 DType::U8,
10223 None,
10224 edgefirst_tensor::CpuAccess::ReadWrite,
10225 )
10226 .unwrap();
10227 target_image
10228 .as_typed::<u8>()
10229 .unwrap()
10230 .map()
10231 .unwrap()
10232 .as_mut_slice()
10233 .as_chunks_mut::<3>()
10234 .0
10235 .iter_mut()
10236 .zip(
10237 edgefirst_bench::testdata::read("camera720p.rgba")
10238 .as_chunks::<4>()
10239 .0,
10240 )
10241 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
10242
10243 compare_images(&dst, &target_image, 0.98, function!());
10246 }
10247
10248 #[test]
10249 #[cfg(target_os = "linux")]
10250 #[ignore = "G2D does not support VYUY; re-enable when hardware support is added"]
10251 fn test_vyuy_to_rgba_g2d() {
10252 if !is_g2d_available() {
10253 crate::test_support::report_skip(
10254 "test_vyuy_to_rgba_g2d - G2D library (libg2d.so.2) not available",
10255 );
10256 return;
10257 }
10258 if !is_dma_available() {
10259 crate::test_support::report_skip("test_vyuy_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
10260 return;
10261 }
10262
10263 let src = load_bytes_to_tensor(
10264 1280,
10265 720,
10266 PixelFormat::Vyuy,
10267 None,
10268 &edgefirst_bench::testdata::read("camera720p.vyuy"),
10269 )
10270 .unwrap();
10271
10272 let dst = TensorDyn::image(
10273 1280,
10274 720,
10275 PixelFormat::Rgba,
10276 DType::U8,
10277 Some(TensorMemory::DmaBuf),
10278 edgefirst_tensor::CpuAccess::ReadWrite,
10279 )
10280 .unwrap();
10281 let mut g2d_converter = G2DProcessor::new().unwrap();
10282
10283 let (result, _src, dst) = convert_img(
10284 &mut g2d_converter,
10285 src,
10286 dst,
10287 Rotation::None,
10288 Flip::None,
10289 Crop::no_crop(),
10290 );
10291 match result {
10292 Err(Error::G2D(_)) => {
10293 crate::test_support::report_skip(
10294 "test_vyuy_to_rgba_g2d - G2D does not support PixelFormat::Vyuy format",
10295 );
10296 return;
10297 }
10298 r => r.unwrap(),
10299 }
10300
10301 let target_image = TensorDyn::image(
10302 1280,
10303 720,
10304 PixelFormat::Rgba,
10305 DType::U8,
10306 None,
10307 edgefirst_tensor::CpuAccess::ReadWrite,
10308 )
10309 .unwrap();
10310 target_image
10311 .as_typed::<u8>()
10312 .unwrap()
10313 .map()
10314 .unwrap()
10315 .as_mut_slice()
10316 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
10317
10318 compare_images(&dst, &target_image, 0.98, function!());
10322 }
10323
10324 #[test]
10325 #[cfg(target_os = "linux")]
10326 #[ignore = "G2D does not support VYUY; re-enable when hardware support is added"]
10327 fn test_vyuy_to_rgb_g2d() {
10328 if !is_g2d_available() {
10329 crate::test_support::report_skip(
10330 "test_vyuy_to_rgb_g2d - G2D library (libg2d.so.2) not available",
10331 );
10332 return;
10333 }
10334 if !is_dma_available() {
10335 crate::test_support::report_skip("test_vyuy_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)");
10336 return;
10337 }
10338
10339 let src = load_bytes_to_tensor(
10340 1280,
10341 720,
10342 PixelFormat::Vyuy,
10343 None,
10344 &edgefirst_bench::testdata::read("camera720p.vyuy"),
10345 )
10346 .unwrap();
10347
10348 let g2d_dst = TensorDyn::image(
10349 1280,
10350 720,
10351 PixelFormat::Rgb,
10352 DType::U8,
10353 Some(TensorMemory::DmaBuf),
10354 edgefirst_tensor::CpuAccess::ReadWrite,
10355 )
10356 .unwrap();
10357 let mut g2d_converter = G2DProcessor::new().unwrap();
10358
10359 let (result, src, g2d_dst) = convert_img(
10360 &mut g2d_converter,
10361 src,
10362 g2d_dst,
10363 Rotation::None,
10364 Flip::None,
10365 Crop::no_crop(),
10366 );
10367 match result {
10368 Err(Error::G2D(_)) => {
10369 crate::test_support::report_skip(
10370 "test_vyuy_to_rgb_g2d - G2D does not support PixelFormat::Vyuy format",
10371 );
10372 return;
10373 }
10374 r => r.unwrap(),
10375 }
10376
10377 let cpu_dst = TensorDyn::image(
10378 1280,
10379 720,
10380 PixelFormat::Rgb,
10381 DType::U8,
10382 None,
10383 edgefirst_tensor::CpuAccess::ReadWrite,
10384 )
10385 .unwrap();
10386 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
10387
10388 let (result, _src, cpu_dst) = convert_img(
10389 &mut cpu_converter,
10390 src,
10391 cpu_dst,
10392 Rotation::None,
10393 Flip::None,
10394 Crop::no_crop(),
10395 );
10396 result.unwrap();
10397
10398 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
10404 }
10405
10406 #[test]
10407 #[cfg(any(
10408 target_os = "linux",
10409 target_os = "macos",
10410 target_os = "ios",
10411 target_os = "android",
10412 target_os = "windows"
10413 ))]
10414 #[cfg(feature = "opengl")]
10415 fn test_vyuy_to_rgba_opengl() {
10416 if !is_opengl_available() {
10417 crate::test_support::report_skip(&format!("{} - OpenGL not available", function!()));
10418 return;
10419 }
10420 if !crate::test_support::is_gpu_buffer_available() {
10421 crate::test_support::report_skip(&format!(
10422 "{} - zero-copy GPU buffer (DMA-BUF / IOSurface) not available on this host",
10423 function!()
10424 ));
10425 return;
10426 }
10427
10428 let expect = if cfg!(target_os = "linux") {
10430 crate::test_support::Expect::Succeeds
10431 } else {
10432 crate::test_support::Expect::MayFail("no zero-copy VYUY mapping on this platform")
10433 };
10434 let Some(src) = crate::test_support::allocated(
10435 load_bytes_to_tensor(
10436 1280,
10437 720,
10438 PixelFormat::Vyuy,
10439 Some(TensorMemory::DmaBuf),
10440 &edgefirst_bench::testdata::read("camera720p.vyuy"),
10441 ),
10442 "zero-copy VYUY source",
10443 expect,
10444 ) else {
10445 return;
10446 };
10447
10448 let dst = TensorDyn::image(
10449 1280,
10450 720,
10451 PixelFormat::Rgba,
10452 DType::U8,
10453 Some(TensorMemory::DmaBuf),
10454 edgefirst_tensor::CpuAccess::ReadWrite,
10455 )
10456 .unwrap();
10457 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
10458
10459 let (result, _src, dst) = convert_img(
10460 &mut gl_converter,
10461 src,
10462 dst,
10463 Rotation::None,
10464 Flip::None,
10465 Crop::no_crop(),
10466 );
10467 match result {
10468 Err(Error::NotSupported(_)) => {
10469 crate::test_support::report_skip(&format!(
10470 "{} - OpenGL does not support PixelFormat::Vyuy DMA format",
10471 function!()
10472 ));
10473 return;
10474 }
10475 r => r.unwrap(),
10476 }
10477
10478 let target_image = TensorDyn::image(
10479 1280,
10480 720,
10481 PixelFormat::Rgba,
10482 DType::U8,
10483 None,
10484 edgefirst_tensor::CpuAccess::ReadWrite,
10485 )
10486 .unwrap();
10487 target_image
10488 .as_typed::<u8>()
10489 .unwrap()
10490 .map()
10491 .unwrap()
10492 .as_mut_slice()
10493 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
10494
10495 compare_images(&dst, &target_image, 0.98, function!());
10499 }
10500
10501 #[test]
10502 fn test_nv12_to_rgba_cpu() {
10503 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
10504 let src = TensorDyn::image(
10505 1280,
10506 720,
10507 PixelFormat::Nv12,
10508 DType::U8,
10509 None,
10510 edgefirst_tensor::CpuAccess::ReadWrite,
10511 )
10512 .unwrap();
10513 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
10514 .copy_from_slice(&file);
10515
10516 let dst = TensorDyn::image(
10517 1280,
10518 720,
10519 PixelFormat::Rgba,
10520 DType::U8,
10521 None,
10522 edgefirst_tensor::CpuAccess::ReadWrite,
10523 )
10524 .unwrap();
10525 let mut cpu_converter = CPUProcessor::new();
10526
10527 let (result, _src, dst) = convert_img(
10528 &mut cpu_converter,
10529 src,
10530 dst,
10531 Rotation::None,
10532 Flip::None,
10533 Crop::no_crop(),
10534 );
10535 result.unwrap();
10536
10537 let target_image = crate::load_image_test_helper(
10538 &edgefirst_bench::testdata::read("zidane.jpg"),
10539 Some(PixelFormat::Rgba),
10540 None,
10541 )
10542 .unwrap();
10543
10544 compare_images(&dst, &target_image, 0.95, function!());
10549 }
10550
10551 #[test]
10552 fn test_nv12_odd_height_to_rgb_cpu() {
10553 let mut src = TensorDyn::image(
10564 8,
10565 5,
10566 PixelFormat::Nv12,
10567 DType::U8,
10568 Some(TensorMemory::Mem),
10569 edgefirst_tensor::CpuAccess::ReadWrite,
10570 )
10571 .unwrap();
10572 assert_eq!(src.shape(), &[8, 8]);
10573 assert_eq!((src.width(), src.height()), (Some(8), Some(5)));
10574 src.as_typed::<u8>()
10575 .unwrap()
10576 .map()
10577 .unwrap()
10578 .as_mut_slice()
10579 .fill(128);
10580 src.set_colorimetry(Some(
10584 edgefirst_tensor::Colorimetry::default()
10585 .with_encoding(edgefirst_tensor::ColorEncoding::Bt601)
10586 .with_range(edgefirst_tensor::ColorRange::Full),
10587 ));
10588
10589 let dst = TensorDyn::image(
10590 8,
10591 5,
10592 PixelFormat::Rgb,
10593 DType::U8,
10594 Some(TensorMemory::Mem),
10595 edgefirst_tensor::CpuAccess::ReadWrite,
10596 )
10597 .unwrap();
10598 let mut cpu_converter = CPUProcessor::new();
10599 let (result, _src, dst) = convert_img(
10600 &mut cpu_converter,
10601 src,
10602 dst,
10603 Rotation::None,
10604 Flip::None,
10605 Crop::no_crop(),
10606 );
10607 result.unwrap();
10608
10609 assert_eq!((dst.width(), dst.height()), (Some(8), Some(5)));
10610 let map = dst.as_typed::<u8>().unwrap().map().unwrap();
10611 for (i, &b) in map.as_slice().iter().enumerate() {
10612 assert!(
10613 (b as i16 - 128).abs() <= 2,
10614 "pixel byte {i} = {b}, expected ~128 for neutral-grey NV12"
10615 );
10616 }
10617 }
10618
10619 #[test]
10620 fn test_nv24_to_rgb_cpu() {
10621 let mut src = TensorDyn::image(
10627 8,
10628 4,
10629 PixelFormat::Nv24,
10630 DType::U8,
10631 Some(TensorMemory::Mem),
10632 edgefirst_tensor::CpuAccess::ReadWrite,
10633 )
10634 .unwrap();
10635 assert_eq!(src.shape(), &[12, 8]);
10636 assert_eq!((src.width(), src.height()), (Some(8), Some(4)));
10637 src.as_typed::<u8>()
10638 .unwrap()
10639 .map()
10640 .unwrap()
10641 .as_mut_slice()
10642 .fill(128);
10643 src.set_colorimetry(Some(
10646 edgefirst_tensor::Colorimetry::default()
10647 .with_encoding(edgefirst_tensor::ColorEncoding::Bt601)
10648 .with_range(edgefirst_tensor::ColorRange::Full),
10649 ));
10650
10651 let dst = TensorDyn::image(
10652 8,
10653 4,
10654 PixelFormat::Rgb,
10655 DType::U8,
10656 Some(TensorMemory::Mem),
10657 edgefirst_tensor::CpuAccess::ReadWrite,
10658 )
10659 .unwrap();
10660 let mut cpu_converter = CPUProcessor::new();
10661 let (result, _src, dst) = convert_img(
10662 &mut cpu_converter,
10663 src,
10664 dst,
10665 Rotation::None,
10666 Flip::None,
10667 Crop::no_crop(),
10668 );
10669 result.unwrap();
10670
10671 assert_eq!((dst.width(), dst.height()), (Some(8), Some(4)));
10672 let map = dst.as_typed::<u8>().unwrap().map().unwrap();
10673 for (i, &b) in map.as_slice().iter().enumerate() {
10674 assert!(
10675 (b as i16 - 128).abs() <= 2,
10676 "pixel byte {i} = {b}, expected ~128 for neutral-grey NV24"
10677 );
10678 }
10679 }
10680
10681 #[test]
10682 fn cpu_nv12_to_rgb_respects_tagged_bt2020() {
10683 fn decode_tagged(enc: edgefirst_tensor::ColorEncoding) -> [u8; 3] {
10691 let mut src = TensorDyn::image(
10692 8,
10693 4,
10694 PixelFormat::Nv12,
10695 DType::U8,
10696 Some(TensorMemory::Mem),
10697 edgefirst_tensor::CpuAccess::ReadWrite,
10698 )
10699 .unwrap();
10700 assert_eq!(src.shape(), &[6, 8]);
10702 {
10703 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
10704 let buf = map.as_mut_slice();
10705 buf[..32].fill(120); for px in buf[32..].as_chunks_mut::<2>().0 {
10707 px[0] = 180; px[1] = 64; }
10710 }
10711 src.set_colorimetry(Some(
10714 edgefirst_tensor::Colorimetry::default()
10715 .with_encoding(enc)
10716 .with_range(edgefirst_tensor::ColorRange::Limited),
10717 ));
10718 let dst = TensorDyn::image(
10719 8,
10720 4,
10721 PixelFormat::Rgb,
10722 DType::U8,
10723 Some(TensorMemory::Mem),
10724 edgefirst_tensor::CpuAccess::ReadWrite,
10725 )
10726 .unwrap();
10727 let mut cpu = CPUProcessor::new();
10728 let (result, _src, dst) = convert_img(
10729 &mut cpu,
10730 src,
10731 dst,
10732 Rotation::None,
10733 Flip::None,
10734 Crop::no_crop(),
10735 );
10736 result.unwrap();
10737 let map = dst.as_typed::<u8>().unwrap().map().unwrap();
10738 let s = map.as_slice();
10739 [s[0], s[1], s[2]]
10740 }
10741
10742 let bt601 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt601);
10743 let bt709 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt709);
10744 let bt2020 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt2020);
10745
10746 assert_ne!(
10747 bt2020, bt601,
10748 "BT.2020 must decode differently from BT.601 ({bt2020:?} vs {bt601:?})"
10749 );
10750 assert_ne!(
10751 bt2020, bt709,
10752 "BT.2020 must decode differently from BT.709 ({bt2020:?} vs {bt709:?})"
10753 );
10754 assert_ne!(
10755 bt601, bt709,
10756 "BT.601 must decode differently from BT.709 ({bt601:?} vs {bt709:?})"
10757 );
10758 }
10759
10760 #[test]
10761 fn test_nv12_to_rgb_cpu() {
10762 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
10763 let src = TensorDyn::image(
10764 1280,
10765 720,
10766 PixelFormat::Nv12,
10767 DType::U8,
10768 None,
10769 edgefirst_tensor::CpuAccess::ReadWrite,
10770 )
10771 .unwrap();
10772 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
10773 .copy_from_slice(&file);
10774
10775 let dst = TensorDyn::image(
10776 1280,
10777 720,
10778 PixelFormat::Rgb,
10779 DType::U8,
10780 None,
10781 edgefirst_tensor::CpuAccess::ReadWrite,
10782 )
10783 .unwrap();
10784 let mut cpu_converter = CPUProcessor::new();
10785
10786 let (result, _src, dst) = convert_img(
10787 &mut cpu_converter,
10788 src,
10789 dst,
10790 Rotation::None,
10791 Flip::None,
10792 Crop::no_crop(),
10793 );
10794 result.unwrap();
10795
10796 let target_image = crate::load_image_test_helper(
10797 &edgefirst_bench::testdata::read("zidane.jpg"),
10798 Some(PixelFormat::Rgb),
10799 None,
10800 )
10801 .unwrap();
10802
10803 compare_images(&dst, &target_image, 0.95, function!());
10808 }
10809
10810 #[test]
10811 fn test_nv12_to_grey_cpu() {
10812 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
10813 let src = TensorDyn::image(
10814 1280,
10815 720,
10816 PixelFormat::Nv12,
10817 DType::U8,
10818 None,
10819 edgefirst_tensor::CpuAccess::ReadWrite,
10820 )
10821 .unwrap();
10822 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
10823 .copy_from_slice(&file);
10824
10825 let dst = TensorDyn::image(
10826 1280,
10827 720,
10828 PixelFormat::Grey,
10829 DType::U8,
10830 None,
10831 edgefirst_tensor::CpuAccess::ReadWrite,
10832 )
10833 .unwrap();
10834 let mut cpu_converter = CPUProcessor::new();
10835
10836 let (result, _src, dst) = convert_img(
10837 &mut cpu_converter,
10838 src,
10839 dst,
10840 Rotation::None,
10841 Flip::None,
10842 Crop::no_crop(),
10843 );
10844 result.unwrap();
10845
10846 let target_image = crate::load_image_test_helper(
10847 &edgefirst_bench::testdata::read("zidane.jpg"),
10848 Some(PixelFormat::Grey),
10849 None,
10850 )
10851 .unwrap();
10852
10853 compare_images(&dst, &target_image, 0.95, function!());
10858 }
10859
10860 #[test]
10861 fn test_nv12_to_yuyv_cpu() {
10862 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
10863 let src = TensorDyn::image(
10864 1280,
10865 720,
10866 PixelFormat::Nv12,
10867 DType::U8,
10868 None,
10869 edgefirst_tensor::CpuAccess::ReadWrite,
10870 )
10871 .unwrap();
10872 src.as_typed::<u8>().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
10873 .copy_from_slice(&file);
10874
10875 let dst = TensorDyn::image(
10876 1280,
10877 720,
10878 PixelFormat::Yuyv,
10879 DType::U8,
10880 None,
10881 edgefirst_tensor::CpuAccess::ReadWrite,
10882 )
10883 .unwrap();
10884 let mut cpu_converter = CPUProcessor::new();
10885
10886 let (result, _src, dst) = convert_img(
10887 &mut cpu_converter,
10888 src,
10889 dst,
10890 Rotation::None,
10891 Flip::None,
10892 Crop::no_crop(),
10893 );
10894 result.unwrap();
10895
10896 let target_image = crate::load_image_test_helper(
10897 &edgefirst_bench::testdata::read("zidane.jpg"),
10898 Some(PixelFormat::Rgb),
10899 None,
10900 )
10901 .unwrap();
10902
10903 compare_images_convert_to_rgb(&dst, &target_image, 0.95, function!());
10908 }
10909
10910 #[test]
10911 fn test_cpu_resize_nv16() {
10912 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
10913 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
10914
10915 let cpu_nv16_dst = TensorDyn::image(
10916 640,
10917 640,
10918 PixelFormat::Nv16,
10919 DType::U8,
10920 None,
10921 edgefirst_tensor::CpuAccess::ReadWrite,
10922 )
10923 .unwrap();
10924 let cpu_rgb_dst = TensorDyn::image(
10925 640,
10926 640,
10927 PixelFormat::Rgb,
10928 DType::U8,
10929 None,
10930 edgefirst_tensor::CpuAccess::ReadWrite,
10931 )
10932 .unwrap();
10933 let mut cpu_converter = CPUProcessor::new();
10934 let crop = Crop::letterbox([255, 128, 0, 255]);
10935
10936 let (result, src, cpu_nv16_dst) = convert_img(
10937 &mut cpu_converter,
10938 src,
10939 cpu_nv16_dst,
10940 Rotation::None,
10941 Flip::None,
10942 crop,
10943 );
10944 result.unwrap();
10945
10946 let (result, _src, cpu_rgb_dst) = convert_img(
10947 &mut cpu_converter,
10948 src,
10949 cpu_rgb_dst,
10950 Rotation::None,
10951 Flip::None,
10952 crop,
10953 );
10954 result.unwrap();
10955 compare_images_convert_to_rgb(
10956 &cpu_nv16_dst,
10957 &cpu_rgb_dst,
10958 yuv_threshold(0.99),
10959 function!(),
10960 );
10961 }
10962
10963 fn yuv_threshold(balanced: f64) -> f64 {
10980 #[cfg(target_arch = "aarch64")]
10981 {
10982 if !std::arch::is_aarch64_feature_detected!("rdm") {
10983 return balanced - 0.01;
10984 }
10985 }
10986 balanced
10987 }
10988
10989 fn load_bytes_to_tensor(
10990 width: usize,
10991 height: usize,
10992 format: PixelFormat,
10993 memory: Option<TensorMemory>,
10994 bytes: &[u8],
10995 ) -> Result<TensorDyn, Error> {
10996 let src = TensorDyn::image(
10997 width,
10998 height,
10999 format,
11000 DType::U8,
11001 memory,
11002 edgefirst_tensor::CpuAccess::ReadWrite,
11003 )?;
11004 src.as_typed::<u8>()
11005 .unwrap()
11006 .map()?
11007 .as_mut_slice()
11008 .copy_from_slice(bytes);
11009 Ok(src)
11010 }
11011
11012 fn compare_images(img1: &TensorDyn, img2: &TensorDyn, threshold: f64, name: &str) {
11020 assert_eq!(img1.height(), img2.height(), "Heights differ");
11021 assert_eq!(img1.width(), img2.width(), "Widths differ");
11022 assert_eq!(
11023 img1.format().unwrap(),
11024 img2.format().unwrap(),
11025 "PixelFormat differ"
11026 );
11027 assert!(
11028 matches!(
11029 img1.format().unwrap(),
11030 PixelFormat::Rgb
11031 | PixelFormat::Rgba
11032 | PixelFormat::Grey
11033 | PixelFormat::PlanarRgb
11034 | PixelFormat::PlanarRgba
11035 ),
11036 "format must be Rgb or Rgba for comparison"
11037 );
11038
11039 fn flat(img: &TensorDyn) -> Vec<u8> {
11046 let t = img.as_typed::<u8>().expect("u8 image");
11047 let mut out = vec![0u8; t.shape().iter().product::<usize>()];
11048 t.copy_to_flat(&mut out).expect("compact the padded rows");
11049 out
11050 }
11051
11052 let image1 = match img1.format().unwrap() {
11053 PixelFormat::Rgb => image::RgbImage::from_vec(
11054 img1.width().unwrap() as u32,
11055 img1.height().unwrap() as u32,
11056 flat(img1),
11057 )
11058 .unwrap(),
11059 PixelFormat::Rgba => image::RgbaImage::from_vec(
11060 img1.width().unwrap() as u32,
11061 img1.height().unwrap() as u32,
11062 flat(img1),
11063 )
11064 .unwrap()
11065 .convert(),
11066 PixelFormat::Grey => image::GrayImage::from_vec(
11067 img1.width().unwrap() as u32,
11068 img1.height().unwrap() as u32,
11069 flat(img1),
11070 )
11071 .unwrap()
11072 .convert(),
11073 PixelFormat::PlanarRgb | PixelFormat::PlanarRgba => image::GrayImage::from_vec(
11074 img1.width().unwrap() as u32,
11075 (img1.height().unwrap() * img1.format().unwrap().channels()) as u32,
11076 flat(img1),
11077 )
11078 .unwrap()
11079 .convert(),
11080 _ => return,
11081 };
11082
11083 let image2 = match img2.format().unwrap() {
11084 PixelFormat::Rgb => image::RgbImage::from_vec(
11085 img2.width().unwrap() as u32,
11086 img2.height().unwrap() as u32,
11087 flat(img2),
11088 )
11089 .unwrap(),
11090 PixelFormat::Rgba => image::RgbaImage::from_vec(
11091 img2.width().unwrap() as u32,
11092 img2.height().unwrap() as u32,
11093 flat(img2),
11094 )
11095 .unwrap()
11096 .convert(),
11097 PixelFormat::Grey => image::GrayImage::from_vec(
11098 img2.width().unwrap() as u32,
11099 img2.height().unwrap() as u32,
11100 flat(img2),
11101 )
11102 .unwrap()
11103 .convert(),
11104 PixelFormat::PlanarRgb | PixelFormat::PlanarRgba => image::GrayImage::from_vec(
11105 img2.width().unwrap() as u32,
11106 (img2.height().unwrap() * img2.format().unwrap().channels()) as u32,
11107 flat(img2),
11108 )
11109 .unwrap()
11110 .convert(),
11111 _ => return,
11112 };
11113
11114 let similarity = image_compare::rgb_similarity_structure(
11115 &image_compare::Algorithm::RootMeanSquared,
11116 &image1,
11117 &image2,
11118 )
11119 .expect("Image Comparison failed");
11120 if similarity.score < threshold {
11121 similarity
11124 .image
11125 .to_color_map()
11126 .save(format!("{name}.png"))
11127 .unwrap();
11128 panic!(
11129 "{name}: converted image and target image have similarity score too low: {} < {}",
11130 similarity.score, threshold
11131 )
11132 }
11133 }
11134
11135 fn compare_images_convert_to_rgb(
11136 img1: &TensorDyn,
11137 img2: &TensorDyn,
11138 threshold: f64,
11139 name: &str,
11140 ) {
11141 assert_eq!(img1.height(), img2.height(), "Heights differ");
11142 assert_eq!(img1.width(), img2.width(), "Widths differ");
11143
11144 let mut img_rgb1 = TensorDyn::image(
11145 img1.width().unwrap(),
11146 img1.height().unwrap(),
11147 PixelFormat::Rgb,
11148 DType::U8,
11149 Some(TensorMemory::Mem),
11150 edgefirst_tensor::CpuAccess::ReadWrite,
11151 )
11152 .unwrap();
11153 let mut img_rgb2 = TensorDyn::image(
11154 img1.width().unwrap(),
11155 img1.height().unwrap(),
11156 PixelFormat::Rgb,
11157 DType::U8,
11158 Some(TensorMemory::Mem),
11159 edgefirst_tensor::CpuAccess::ReadWrite,
11160 )
11161 .unwrap();
11162 let mut __cv = CPUProcessor::default();
11163 let r1 = __cv.convert(img1, &mut img_rgb1, None);
11164 let r2 = __cv.convert(img2, &mut img_rgb2, None);
11165 if r1.is_err() || r2.is_err() {
11166 let w = img1.width().unwrap() as u32;
11168 let data1 = img1.as_typed::<u8>().unwrap().map().unwrap().to_vec();
11169 let data2 = img2.as_typed::<u8>().unwrap().map().unwrap().to_vec();
11170 let h1 = (data1.len() as u32) / w;
11171 let h2 = (data2.len() as u32) / w;
11172 let g1 = image::GrayImage::from_vec(w, h1, data1).unwrap();
11173 let g2 = image::GrayImage::from_vec(w, h2, data2).unwrap();
11174 let similarity = image_compare::gray_similarity_structure(
11175 &image_compare::Algorithm::RootMeanSquared,
11176 &g1,
11177 &g2,
11178 )
11179 .expect("Image Comparison failed");
11180 if similarity.score < threshold {
11181 panic!(
11182 "{name}: converted image and target image have similarity score too low: {} < {}",
11183 similarity.score, threshold
11184 )
11185 }
11186 return;
11187 }
11188
11189 let image1 = image::RgbImage::from_vec(
11190 img_rgb1.width().unwrap() as u32,
11191 img_rgb1.height().unwrap() as u32,
11192 img_rgb1.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
11193 )
11194 .unwrap();
11195
11196 let image2 = image::RgbImage::from_vec(
11197 img_rgb2.width().unwrap() as u32,
11198 img_rgb2.height().unwrap() as u32,
11199 img_rgb2.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
11200 )
11201 .unwrap();
11202
11203 let similarity = image_compare::rgb_similarity_structure(
11204 &image_compare::Algorithm::RootMeanSquared,
11205 &image1,
11206 &image2,
11207 )
11208 .expect("Image Comparison failed");
11209 if similarity.score < threshold {
11210 similarity
11213 .image
11214 .to_color_map()
11215 .save(format!("{name}.png"))
11216 .unwrap();
11217 panic!(
11218 "{name}: converted image and target image have similarity score too low: {} < {}",
11219 similarity.score, threshold
11220 )
11221 }
11222 }
11223
11224 #[test]
11229 fn test_nv12_image_creation() {
11230 let width = 640;
11231 let height = 480;
11232 let img = TensorDyn::image(
11233 width,
11234 height,
11235 PixelFormat::Nv12,
11236 DType::U8,
11237 None,
11238 edgefirst_tensor::CpuAccess::ReadWrite,
11239 )
11240 .unwrap();
11241
11242 assert_eq!(img.width(), Some(width));
11243 assert_eq!(img.height(), Some(height));
11244 assert_eq!(img.format().unwrap(), PixelFormat::Nv12);
11245 assert_eq!(
11247 img.as_typed::<u8>().unwrap().shape(),
11248 &[height * 3 / 2, width]
11249 );
11250 }
11251
11252 #[test]
11253 fn test_nv12_channels() {
11254 let img = TensorDyn::image(
11255 640,
11256 480,
11257 PixelFormat::Nv12,
11258 DType::U8,
11259 None,
11260 edgefirst_tensor::CpuAccess::ReadWrite,
11261 )
11262 .unwrap();
11263 assert_eq!(img.format().unwrap().channels(), 1);
11265 }
11266
11267 #[test]
11272 fn test_tensor_set_format_planar() {
11273 let mut tensor = Tensor::<u8>::new(&[3, 480, 640], None, None).unwrap();
11274 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
11275 assert_eq!(tensor.format(), Some(PixelFormat::PlanarRgb));
11276 assert_eq!(tensor.width(), Some(640));
11277 assert_eq!(tensor.height(), Some(480));
11278 }
11279
11280 #[test]
11281 fn test_tensor_set_format_interleaved() {
11282 let mut tensor = Tensor::<u8>::new(&[480, 640, 4], None, None).unwrap();
11283 tensor.set_format(PixelFormat::Rgba).unwrap();
11284 assert_eq!(tensor.format(), Some(PixelFormat::Rgba));
11285 assert_eq!(tensor.width(), Some(640));
11286 assert_eq!(tensor.height(), Some(480));
11287 }
11288
11289 #[test]
11290 fn test_tensordyn_image_rgb() {
11291 let img = TensorDyn::image(
11292 640,
11293 480,
11294 PixelFormat::Rgb,
11295 DType::U8,
11296 None,
11297 edgefirst_tensor::CpuAccess::ReadWrite,
11298 )
11299 .unwrap();
11300 assert_eq!(img.width(), Some(640));
11301 assert_eq!(img.height(), Some(480));
11302 assert_eq!(img.format(), Some(PixelFormat::Rgb));
11303 }
11304
11305 #[test]
11306 fn test_tensordyn_image_planar_rgb() {
11307 let img = TensorDyn::image(
11308 640,
11309 480,
11310 PixelFormat::PlanarRgb,
11311 DType::U8,
11312 None,
11313 edgefirst_tensor::CpuAccess::ReadWrite,
11314 )
11315 .unwrap();
11316 assert_eq!(img.width(), Some(640));
11317 assert_eq!(img.height(), Some(480));
11318 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
11319 }
11320
11321 #[test]
11322 fn test_rgb_int8_format() {
11323 let img = TensorDyn::image(
11325 1280,
11326 720,
11327 PixelFormat::Rgb,
11328 DType::I8,
11329 Some(TensorMemory::Mem),
11330 edgefirst_tensor::CpuAccess::ReadWrite,
11331 )
11332 .unwrap();
11333 assert_eq!(img.width(), Some(1280));
11334 assert_eq!(img.height(), Some(720));
11335 assert_eq!(img.format(), Some(PixelFormat::Rgb));
11336 assert_eq!(img.dtype(), DType::I8);
11337 }
11338
11339 #[test]
11340 fn test_planar_rgb_int8_format() {
11341 let img = TensorDyn::image(
11342 1280,
11343 720,
11344 PixelFormat::PlanarRgb,
11345 DType::I8,
11346 Some(TensorMemory::Mem),
11347 edgefirst_tensor::CpuAccess::ReadWrite,
11348 )
11349 .unwrap();
11350 assert_eq!(img.width(), Some(1280));
11351 assert_eq!(img.height(), Some(720));
11352 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
11353 assert_eq!(img.dtype(), DType::I8);
11354 }
11355
11356 #[test]
11357 fn test_rgb_from_tensor() {
11358 let mut tensor = Tensor::<u8>::new(&[720, 1280, 3], None, None).unwrap();
11359 tensor.set_format(PixelFormat::Rgb).unwrap();
11360 let img = TensorDyn::from(tensor);
11361 assert_eq!(img.width(), Some(1280));
11362 assert_eq!(img.height(), Some(720));
11363 assert_eq!(img.format(), Some(PixelFormat::Rgb));
11364 }
11365
11366 #[test]
11367 fn test_planar_rgb_from_tensor() {
11368 let mut tensor = Tensor::<u8>::new(&[3, 720, 1280], None, None).unwrap();
11369 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
11370 let img = TensorDyn::from(tensor);
11371 assert_eq!(img.width(), Some(1280));
11372 assert_eq!(img.height(), Some(720));
11373 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
11374 }
11375
11376 #[test]
11377 fn test_dtype_determines_int8() {
11378 let u8_img = TensorDyn::image(
11380 64,
11381 64,
11382 PixelFormat::Rgb,
11383 DType::U8,
11384 None,
11385 edgefirst_tensor::CpuAccess::ReadWrite,
11386 )
11387 .unwrap();
11388 let i8_img = TensorDyn::image(
11389 64,
11390 64,
11391 PixelFormat::Rgb,
11392 DType::I8,
11393 None,
11394 edgefirst_tensor::CpuAccess::ReadWrite,
11395 )
11396 .unwrap();
11397 assert_eq!(u8_img.dtype(), DType::U8);
11398 assert_eq!(i8_img.dtype(), DType::I8);
11399 }
11400
11401 #[test]
11402 fn test_pixel_layout_packed_vs_planar() {
11403 assert_eq!(PixelFormat::Rgb.layout(), PixelLayout::Packed);
11405 assert_eq!(PixelFormat::Rgba.layout(), PixelLayout::Packed);
11406 assert_eq!(PixelFormat::PlanarRgb.layout(), PixelLayout::Planar);
11407 assert_eq!(PixelFormat::Nv12.layout(), PixelLayout::SemiPlanar);
11408 }
11409
11410 #[cfg(target_os = "linux")]
11421 #[cfg(feature = "opengl")]
11422 #[test]
11423 fn pbo_reports_why_it_cannot_pin() {
11424 use edgefirst_tensor::{Error, TensorTrait};
11425
11426 let converter = ImageProcessor::new().unwrap();
11427 let is_pbo = converter
11428 .opengl
11429 .as_ref()
11430 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
11431 if !is_pbo {
11432 crate::test_support::report_skip(
11433 "pbo_reports_why_it_cannot_pin - transfer backend is not PBO",
11434 );
11435 return;
11436 }
11437
11438 let t = converter
11439 .create_image(
11440 64,
11441 64,
11442 PixelFormat::Rgba,
11443 DType::U8,
11444 None,
11445 edgefirst_tensor::CpuAccess::ReadWrite,
11446 )
11447 .unwrap();
11448 assert_eq!(t.as_typed::<u8>().unwrap().memory(), TensorMemory::Pbo);
11449 let t = t.as_typed::<u8>().unwrap();
11450
11451 match t.pin_host(edgefirst_tensor::CpuAccess::ReadWrite) {
11452 Err(Error::NotImplemented(m)) => {
11453 assert!(
11454 m.contains("glMapBufferRange"),
11455 "must name the constraint: {m}"
11456 );
11457 assert!(m.contains("map()"), "must point at the supported API: {m}");
11458 }
11459 other => panic!("expected an explained NotImplemented, got {other:?}"),
11460 }
11461 assert!(matches!(
11463 t.sync_for_cpu(edgefirst_tensor::CpuAccess::Read),
11464 Err(Error::NotImplemented(_))
11465 ));
11466
11467 let g = t.map().expect("map must work where pin cannot");
11469 assert!(!g.as_slice().is_empty());
11470 }
11471
11472 #[cfg(target_os = "linux")]
11476 #[cfg(feature = "opengl")]
11477 #[test]
11478 fn test_convert_pbo_to_pbo() {
11479 let mut converter = ImageProcessor::new().unwrap();
11480
11481 let is_pbo = converter
11483 .opengl
11484 .as_ref()
11485 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
11486 if !is_pbo {
11487 crate::test_support::report_skip(
11488 "test_convert_pbo_to_pbo - transfer backend is not PBO",
11489 );
11490 return;
11491 }
11492
11493 let src_w = 640;
11494 let src_h = 480;
11495 let dst_w = 320;
11496 let dst_h = 240;
11497
11498 let pbo_src = converter
11500 .create_image(
11501 src_w,
11502 src_h,
11503 PixelFormat::Rgba,
11504 DType::U8,
11505 None,
11506 edgefirst_tensor::CpuAccess::ReadWrite,
11507 )
11508 .unwrap();
11509 assert_eq!(
11510 pbo_src.as_typed::<u8>().unwrap().memory(),
11511 TensorMemory::Pbo,
11512 "create_image should produce a PBO tensor"
11513 );
11514
11515 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
11517 let jpeg_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
11518
11519 let mem_src = TensorDyn::image(
11521 src_w,
11522 src_h,
11523 PixelFormat::Rgba,
11524 DType::U8,
11525 Some(TensorMemory::Mem),
11526 edgefirst_tensor::CpuAccess::ReadWrite,
11527 )
11528 .unwrap();
11529 let (result, _jpeg_src, mem_src) = convert_img(
11530 &mut CPUProcessor::new(),
11531 jpeg_src,
11532 mem_src,
11533 Rotation::None,
11534 Flip::None,
11535 Crop::no_crop(),
11536 );
11537 result.unwrap();
11538
11539 {
11541 let src_data = mem_src.as_typed::<u8>().unwrap().map().unwrap();
11542 let mut pbo_map = pbo_src.as_typed::<u8>().unwrap().map().unwrap();
11543 pbo_map.copy_from_slice(&src_data);
11544 }
11545
11546 let pbo_dst = converter
11548 .create_image(
11549 dst_w,
11550 dst_h,
11551 PixelFormat::Rgba,
11552 DType::U8,
11553 None,
11554 edgefirst_tensor::CpuAccess::ReadWrite,
11555 )
11556 .unwrap();
11557 assert_eq!(
11558 pbo_dst.as_typed::<u8>().unwrap().memory(),
11559 TensorMemory::Pbo
11560 );
11561
11562 let mut pbo_dst = pbo_dst;
11564 let result = converter.convert(&pbo_src, &mut pbo_dst, None);
11565 result.unwrap();
11566
11567 let cpu_dst = TensorDyn::image(
11569 dst_w,
11570 dst_h,
11571 PixelFormat::Rgba,
11572 DType::U8,
11573 Some(TensorMemory::Mem),
11574 edgefirst_tensor::CpuAccess::ReadWrite,
11575 )
11576 .unwrap();
11577 let (result, _mem_src, cpu_dst) = convert_img(
11578 &mut CPUProcessor::new(),
11579 mem_src,
11580 cpu_dst,
11581 Rotation::None,
11582 Flip::None,
11583 Crop::no_crop(),
11584 );
11585 result.unwrap();
11586
11587 let pbo_dst_img = {
11588 let mut __t = pbo_dst.into_u8().unwrap();
11589 __t.set_format(PixelFormat::Rgba).unwrap();
11590 TensorDyn::from(__t)
11591 };
11592 compare_images(&pbo_dst_img, &cpu_dst, 0.95, function!());
11593 log::info!("test_convert_pbo_to_pbo: PASS — PBO-to-PBO convert matches CPU reference");
11594 }
11595
11596 #[test]
11597 fn test_image_bgra() {
11598 let img = TensorDyn::image(
11599 640,
11600 480,
11601 PixelFormat::Bgra,
11602 DType::U8,
11603 Some(edgefirst_tensor::TensorMemory::Mem),
11604 edgefirst_tensor::CpuAccess::ReadWrite,
11605 )
11606 .unwrap();
11607 assert_eq!(img.width(), Some(640));
11608 assert_eq!(img.height(), Some(480));
11609 assert_eq!(img.format().unwrap().channels(), 4);
11610 assert_eq!(img.format().unwrap(), PixelFormat::Bgra);
11611 }
11612
11613 #[test]
11618 fn test_force_backend_cpu() {
11619 let _lock = acquire_env_lock();
11620 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11621 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
11622 let converter = ImageProcessor::new().unwrap();
11623 assert!(converter.cpu.is_some());
11624 assert_eq!(converter.forced_backend, Some(ForcedBackend::Cpu));
11625 }
11626
11627 #[test]
11628 fn test_force_backend_invalid() {
11629 let _lock = acquire_env_lock();
11630 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11631 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "invalid") };
11632 let result = ImageProcessor::new();
11633 assert!(
11634 matches!(&result, Err(Error::ForcedBackendUnavailable(s)) if s.contains("unknown")),
11635 "invalid backend value should return ForcedBackendUnavailable error: {result:?}"
11636 );
11637 }
11638
11639 #[test]
11640 fn test_force_backend_unset() {
11641 let _lock = acquire_env_lock();
11642 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11643 unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") };
11644 let converter = ImageProcessor::new().unwrap();
11645 assert!(converter.forced_backend.is_none());
11646 }
11647
11648 #[test]
11653 fn test_draw_proto_masks_no_cpu_returns_error() {
11654 let _lock = acquire_env_lock();
11656 let _guard = EnvGuard::snapshot(&[
11657 "EDGEFIRST_FORCE_BACKEND",
11658 "EDGEFIRST_DISABLE_GL",
11659 "EDGEFIRST_DISABLE_G2D",
11660 "EDGEFIRST_DISABLE_CPU",
11661 ]);
11662
11663 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
11665 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
11666 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
11667
11668 let mut converter = ImageProcessor::new().unwrap();
11669 assert!(converter.cpu.is_none(), "CPU should be disabled");
11670
11671 let dst = TensorDyn::image(
11672 640,
11673 480,
11674 PixelFormat::Rgba,
11675 DType::U8,
11676 Some(TensorMemory::Mem),
11677 edgefirst_tensor::CpuAccess::ReadWrite,
11678 )
11679 .unwrap();
11680 let mut dst_dyn = dst;
11681 let det = [DetectBox {
11682 bbox: edgefirst_tensor::BoundingBox {
11683 xmin: 0.1,
11684 ymin: 0.1,
11685 xmax: 0.5,
11686 ymax: 0.5,
11687 },
11688 score: 0.9,
11689 label: 0,
11690 }];
11691 let proto_data = {
11692 use edgefirst_tensor::{Tensor, TensorDyn};
11693 let coeff_t = Tensor::<f32>::from_slice(&[0.5_f32; 4], &[1, 4]).unwrap();
11694 let protos_t =
11695 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
11696 ProtoData {
11697 mask_coefficients: TensorDyn::F32(coeff_t),
11698 protos: TensorDyn::F32(protos_t),
11699 layout: ProtoLayout::Nhwc,
11700 }
11701 };
11702 let result =
11703 converter.draw_proto_masks(&mut dst_dyn, &det, &proto_data, Default::default());
11704 assert!(
11705 matches!(&result, Err(Error::Internal(s)) if s.contains("CPU backend")),
11706 "draw_proto_masks without CPU should return Internal error: {result:?}"
11707 );
11708 }
11709
11710 #[test]
11711 fn test_draw_proto_masks_cpu_fallback_works() {
11712 let _lock = acquire_env_lock();
11715 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
11716 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
11717 let mut converter = ImageProcessor::new().unwrap();
11718 assert!(converter.cpu.is_some());
11719
11720 let dst = TensorDyn::image(
11721 64,
11722 64,
11723 PixelFormat::Rgba,
11724 DType::U8,
11725 Some(TensorMemory::Mem),
11726 edgefirst_tensor::CpuAccess::ReadWrite,
11727 )
11728 .unwrap();
11729 let mut dst_dyn = dst;
11730 let det = [DetectBox {
11731 bbox: edgefirst_tensor::BoundingBox {
11732 xmin: 0.1,
11733 ymin: 0.1,
11734 xmax: 0.5,
11735 ymax: 0.5,
11736 },
11737 score: 0.9,
11738 label: 0,
11739 }];
11740 let proto_data = {
11741 use edgefirst_tensor::{Tensor, TensorDyn};
11742 let coeff_t = Tensor::<f32>::from_slice(&[0.5_f32; 4], &[1, 4]).unwrap();
11743 let protos_t =
11744 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
11745 ProtoData {
11746 mask_coefficients: TensorDyn::F32(coeff_t),
11747 protos: TensorDyn::F32(protos_t),
11748 layout: ProtoLayout::Nhwc,
11749 }
11750 };
11751 let result =
11752 converter.draw_proto_masks(&mut dst_dyn, &det, &proto_data, Default::default());
11753 assert!(result.is_ok(), "CPU fallback path should work: {result:?}");
11754 }
11755
11756 fn acquire_env_lock() -> std::sync::MutexGuard<'static, ()> {
11788 use std::sync::{Mutex, OnceLock};
11789 static ENV_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
11790 ENV_MUTEX
11791 .get_or_init(|| Mutex::new(()))
11792 .lock()
11793 .unwrap_or_else(|e| e.into_inner())
11794 }
11795
11796 struct EnvGuard {
11799 vars: Vec<(&'static str, Option<String>)>,
11800 }
11801
11802 impl EnvGuard {
11803 fn snapshot(names: &[&'static str]) -> Self {
11807 Self {
11808 vars: names.iter().map(|&k| (k, std::env::var(k).ok())).collect(),
11809 }
11810 }
11811 }
11812
11813 impl Drop for EnvGuard {
11814 fn drop(&mut self) {
11815 for (k, v) in &self.vars {
11816 match v {
11817 Some(s) => unsafe { std::env::set_var(k, s) },
11818 None => unsafe { std::env::remove_var(k) },
11819 }
11820 }
11821 }
11822 }
11823
11824 #[test]
11827 fn test_interpolation_env_parse() {
11828 let _lock = acquire_env_lock();
11829 let _guard = EnvGuard::snapshot(&["EDGEFIRST_INTERPOLATION"]);
11830 for (value, want) in [
11831 (Some("nearest"), Some(Interpolation::Nearest)),
11832 (Some(" BILINEAR "), Some(Interpolation::Bilinear)),
11833 (Some("lanczos"), None),
11834 (Some(""), None),
11835 (None, None),
11836 ] {
11837 match value {
11838 Some(v) => unsafe { std::env::set_var("EDGEFIRST_INTERPOLATION", v) },
11839 None => unsafe { std::env::remove_var("EDGEFIRST_INTERPOLATION") },
11840 }
11841 assert_eq!(
11842 interpolation_from_env(),
11843 want,
11844 "EDGEFIRST_INTERPOLATION={value:?}"
11845 );
11846 }
11847 }
11848
11849 #[test]
11852 fn test_interpolation_config_and_env_reach_cpu() {
11853 let _lock = acquire_env_lock();
11854 let _guard = EnvGuard::snapshot(&["EDGEFIRST_INTERPOLATION"]);
11855 let mut checker = Vec::with_capacity(8 * 8 * 4);
11856 for y in 0..8 {
11857 for x in 0..8 {
11858 let v = if (x + y) % 2 == 0 { 0u8 } else { 255 };
11859 checker.extend_from_slice(&[v, v, v, 255]);
11860 }
11861 }
11862 let resize = |processor: &mut dyn ImageProcessorTrait| {
11863 let mut src = TensorDyn::image(
11864 8,
11865 8,
11866 PixelFormat::Rgba,
11867 DType::U8,
11868 Some(TensorMemory::Mem),
11869 edgefirst_tensor::CpuAccess::ReadWrite,
11870 )
11871 .unwrap();
11872 src.as_u8_mut()
11873 .unwrap()
11874 .map_mut()
11875 .unwrap()
11876 .as_mut_slice()
11877 .copy_from_slice(&checker);
11878 let mut dst = TensorDyn::image(
11879 12,
11880 12,
11881 PixelFormat::Rgba,
11882 DType::U8,
11883 Some(TensorMemory::Mem),
11884 edgefirst_tensor::CpuAccess::ReadWrite,
11885 )
11886 .unwrap();
11887 processor.convert(&src, &mut dst, None).unwrap();
11888 dst.as_u8().unwrap().map().unwrap().as_slice().to_vec()
11889 };
11890 let nearest = resize(&mut CPUProcessor::new_nearest());
11891 let bilinear = resize(&mut CPUProcessor::new());
11892 assert_ne!(nearest, bilinear, "the checker must tell the filters apart");
11893 let config = |interpolation| ImageProcessorConfig {
11894 backend: ComputeBackend::Cpu,
11895 interpolation,
11896 ..Default::default()
11897 };
11898
11899 unsafe { std::env::remove_var("EDGEFIRST_INTERPOLATION") };
11900 let mut p = ImageProcessor::with_config(config(Interpolation::Nearest)).unwrap();
11901 assert_eq!(resize(&mut p), nearest, "config Nearest");
11902 p.set_interpolation(Interpolation::Bilinear).unwrap();
11903 assert_eq!(resize(&mut p), bilinear, "set_interpolation(Bilinear)");
11904
11905 unsafe { std::env::set_var("EDGEFIRST_INTERPOLATION", "nearest") };
11906 let mut p = ImageProcessor::with_config(config(Interpolation::Bilinear)).unwrap();
11907 assert_eq!(
11908 resize(&mut p),
11909 nearest,
11910 "EDGEFIRST_INTERPOLATION pins Nearest"
11911 );
11912 p.set_interpolation(Interpolation::Bilinear).unwrap();
11913 assert_eq!(
11914 resize(&mut p),
11915 nearest,
11916 "the env pin outlives set_interpolation"
11917 );
11918 }
11919
11920 #[test]
11924 fn test_per_convert_interpolation_overrides_processor() {
11925 let _lock = acquire_env_lock();
11926 let _guard = EnvGuard::snapshot(&["EDGEFIRST_INTERPOLATION"]);
11927 let mut checker = Vec::with_capacity(8 * 8 * 4);
11928 for y in 0..8 {
11929 for x in 0..8 {
11930 let v = if (x + y) % 2 == 0 { 0u8 } else { 255 };
11931 checker.extend_from_slice(&[v, v, v, 255]);
11932 }
11933 }
11934 let resize = |processor: &mut dyn ImageProcessorTrait, options: Option<ConvertOptions>| {
11935 let mut src = TensorDyn::image(
11936 8,
11937 8,
11938 PixelFormat::Rgba,
11939 DType::U8,
11940 Some(TensorMemory::Mem),
11941 edgefirst_tensor::CpuAccess::ReadWrite,
11942 )
11943 .unwrap();
11944 src.as_u8_mut()
11945 .unwrap()
11946 .map_mut()
11947 .unwrap()
11948 .as_mut_slice()
11949 .copy_from_slice(&checker);
11950 let mut dst = TensorDyn::image(
11951 12,
11952 12,
11953 PixelFormat::Rgba,
11954 DType::U8,
11955 Some(TensorMemory::Mem),
11956 edgefirst_tensor::CpuAccess::ReadWrite,
11957 )
11958 .unwrap();
11959 processor.convert(&src, &mut dst, options).unwrap();
11960 dst.as_u8().unwrap().map().unwrap().as_slice().to_vec()
11961 };
11962 let nearest = resize(&mut CPUProcessor::new_nearest(), None);
11963 let bilinear = resize(&mut CPUProcessor::new(), None);
11964 assert_ne!(nearest, bilinear, "the checker must tell the filters apart");
11965 let cpu_only = || {
11966 ImageProcessor::with_config(ImageProcessorConfig {
11967 backend: ComputeBackend::Cpu,
11968 ..Default::default()
11969 })
11970 .unwrap()
11971 };
11972 let model_a = ConvertOptions::new().interpolation(Interpolation::Nearest);
11973 let model_b = ConvertOptions::new().interpolation(Interpolation::Bicubic);
11974
11975 unsafe { std::env::remove_var("EDGEFIRST_INTERPOLATION") };
11976 let mut p = cpu_only();
11977 assert_eq!(p.interpolation(), Interpolation::Bilinear);
11978 for _ in 0..2 {
11979 assert_eq!(resize(&mut p, Some(model_a)), nearest, "model A: nearest");
11980 assert_eq!(resize(&mut p, Some(model_b)), bilinear, "model B: bicubic");
11981 assert_eq!(resize(&mut p, None), bilinear, "processor setting kept");
11982 }
11983 p.set_interpolation(Interpolation::Nearest).unwrap();
11984 assert_eq!(resize(&mut p, Some(model_b)), bilinear, "options win");
11985 assert_eq!(resize(&mut p, None), nearest, "processor fallback");
11986
11987 let mut spec = PreprocessSpec::new();
11988 spec.interpolation = Interpolation::Nearest;
11989 let from_spec = ConvertOptions::from_spec(&spec).unwrap();
11990 assert_eq!(from_spec.get_interpolation(), Some(Interpolation::Nearest));
11991 let mut p = cpu_only();
11992 let stretch = from_spec.crop(Crop::default());
11993 assert_eq!(
11994 resize(&mut p, Some(stretch)),
11995 nearest,
11996 "from_spec carries it"
11997 );
11998
11999 let cpu = p.cpu.as_mut().unwrap();
12000 cpu.set_interpolation(Interpolation::Nearest);
12001 p.set_interpolation(Interpolation::Bilinear).unwrap();
12002 assert_eq!(
12003 resize(p.cpu.as_mut().unwrap(), None),
12004 nearest,
12005 "an unchanged mode does not reach the backends"
12006 );
12007
12008 unsafe { std::env::set_var("EDGEFIRST_INTERPOLATION", "bilinear") };
12009 let mut p = cpu_only();
12010 assert_eq!(resize(&mut p, Some(model_a)), bilinear, "the env pin wins");
12011 }
12012
12013 #[test]
12016 #[cfg(feature = "opengl")]
12017 fn test_per_convert_interpolation_reaches_gl_worker() {
12018 let _lock = acquire_env_lock();
12019 let _guard = EnvGuard::snapshot(&["EDGEFIRST_INTERPOLATION"]);
12020 unsafe { std::env::remove_var("EDGEFIRST_INTERPOLATION") };
12021 let mut p = match ImageProcessor::with_config(ImageProcessorConfig {
12022 backend: ComputeBackend::OpenGl,
12023 ..Default::default()
12024 }) {
12025 Ok(p) if p.opengl.is_some() => p,
12026 _ => {
12027 crate::test_support::report_skip(&format!(
12028 "{} — GL backend unavailable",
12029 function!()
12030 ));
12031 return;
12032 }
12033 };
12034 let (sw, sh, dw, dh) = (40usize, 30usize, 56usize, 42usize);
12035 let mut rgba = Vec::with_capacity(sw * sh * 4);
12036 for y in 0..sh {
12037 for x in 0..sw {
12038 let v = if (x + y) % 2 == 0 { 16u8 } else { 235 };
12039 rgba.extend_from_slice(&[v, 128, 255 - v, 255]);
12040 }
12041 }
12042 let mut src = TensorDyn::image(
12043 sw,
12044 sh,
12045 PixelFormat::Rgba,
12046 DType::U8,
12047 Some(TensorMemory::Mem),
12048 edgefirst_tensor::CpuAccess::ReadWrite,
12049 )
12050 .unwrap();
12051 src.as_u8_mut()
12052 .unwrap()
12053 .map_mut()
12054 .unwrap()
12055 .as_mut_slice()
12056 .copy_from_slice(&rgba);
12057 let gl_convert = |p: &mut ImageProcessor, options: Option<ConvertOptions>| {
12058 let mut dst = TensorDyn::image(
12059 dw,
12060 dh,
12061 PixelFormat::Rgba,
12062 DType::U8,
12063 Some(TensorMemory::Mem),
12064 edgefirst_tensor::CpuAccess::ReadWrite,
12065 )
12066 .unwrap();
12067 let options = p.resolve_interpolation(options.unwrap_or_default());
12068 p.opengl
12069 .as_mut()
12070 .unwrap()
12071 .convert(&src, &mut dst, Some(options))
12072 .unwrap();
12073 dst.as_u8().unwrap().map().unwrap().as_slice().to_vec()
12074 };
12075 let nearest = ConvertOptions::new().interpolation(Interpolation::Nearest);
12076 let per_convert = gl_convert(&mut p, Some(nearest));
12077 let processor_bilinear = gl_convert(&mut p, None);
12078 p.set_interpolation(Interpolation::Nearest).unwrap();
12079 let processor_nearest = gl_convert(&mut p, None);
12080 assert_ne!(processor_bilinear, processor_nearest);
12081 assert_eq!(per_convert, processor_nearest, "per-convert Nearest");
12082 let bilinear = ConvertOptions::new().interpolation(Interpolation::Bilinear);
12083 assert_eq!(gl_convert(&mut p, Some(bilinear)), processor_bilinear);
12084 }
12085
12086 #[test]
12088 fn test_interpolation_fallback_warns_once() {
12089 runnable_interpolation(Interpolation::Area);
12090 assert!(!warn_first_fallback(Interpolation::Area));
12091 assert_eq!(
12092 runnable_interpolation(Interpolation::Area),
12093 Interpolation::Bilinear
12094 );
12095 assert!(!warn_first_fallback(Interpolation::Area));
12096 }
12097
12098 fn with_force_backend<R>(value: Option<&str>, body: impl FnOnce() -> R) -> R {
12102 let _lock = acquire_env_lock();
12103 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
12104 match value {
12105 Some(v) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", v) },
12106 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
12107 }
12108 body()
12109 }
12110
12111 fn make_dirty_dst(w: usize, h: usize, mem: Option<TensorMemory>) -> TensorDyn {
12116 let dst = TensorDyn::image(
12117 w,
12118 h,
12119 PixelFormat::Rgba,
12120 DType::U8,
12121 mem,
12122 edgefirst_tensor::CpuAccess::ReadWrite,
12123 )
12124 .unwrap();
12125 {
12126 use edgefirst_tensor::TensorMapTrait;
12127 let u8t = dst.as_typed::<u8>().unwrap();
12128 let mut map = u8t.map().unwrap();
12129 for (i, b) in map.as_mut_slice().iter_mut().enumerate() {
12130 *b = 0xA0u8.wrapping_add((i as u8) & 0x3F);
12131 }
12132 }
12133 dst
12134 }
12135
12136 fn make_bg(w: usize, h: usize, mem: Option<TensorMemory>, rgba: [u8; 4]) -> TensorDyn {
12138 let bg = TensorDyn::image(
12139 w,
12140 h,
12141 PixelFormat::Rgba,
12142 DType::U8,
12143 mem,
12144 edgefirst_tensor::CpuAccess::ReadWrite,
12145 )
12146 .unwrap();
12147 {
12148 use edgefirst_tensor::TensorMapTrait;
12149 let u8t = bg.as_typed::<u8>().unwrap();
12150 let mut map = u8t.map().unwrap();
12151 for chunk in map.as_mut_slice().as_chunks_mut::<4>().0 {
12152 chunk.copy_from_slice(&rgba);
12153 }
12154 }
12155 bg
12156 }
12157
12158 fn pixel_at(dst: &TensorDyn, x: usize, y: usize) -> [u8; 4] {
12159 use edgefirst_tensor::TensorMapTrait;
12160 let w = dst.width().unwrap();
12161 let off = (y * w + x) * 4;
12162 let u8t = dst.as_typed::<u8>().unwrap();
12163 let map = u8t.map().unwrap();
12164 let s = map.as_slice();
12165 [s[off], s[off + 1], s[off + 2], s[off + 3]]
12166 }
12167
12168 fn assert_every_pixel_eq(dst: &TensorDyn, expected: [u8; 4], case: &str) {
12169 use edgefirst_tensor::TensorMapTrait;
12170 let u8t = dst.as_typed::<u8>().unwrap();
12171 let map = u8t.map().unwrap();
12172 for (i, chunk) in map.as_slice().as_chunks::<4>().0.iter().enumerate() {
12173 assert_eq!(
12174 chunk, &expected,
12175 "{case}: pixel idx {i} = {chunk:?}, expected {expected:?}"
12176 );
12177 }
12178 }
12179
12180 fn scenario_empty_no_bg(processor: &mut ImageProcessor, case: &str) {
12183 let mut dst = make_dirty_dst(64, 64, None);
12184 processor
12185 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::default())
12186 .unwrap_or_else(|e| panic!("{case}/decoded_masks empty+no-bg failed: {e:?}"));
12187 assert_every_pixel_eq(&dst, [0, 0, 0, 0], &format!("{case}/decoded"));
12188
12189 let mut dst = make_dirty_dst(64, 64, None);
12190 let proto = {
12191 use edgefirst_tensor::{Tensor, TensorDyn};
12192 let coeff_t = Tensor::<f32>::from_slice(&[0.0_f32; 4], &[1, 4]).unwrap();
12194 let protos_t =
12195 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
12196 ProtoData {
12197 mask_coefficients: TensorDyn::F32(coeff_t),
12198 protos: TensorDyn::F32(protos_t),
12199 layout: ProtoLayout::Nhwc,
12200 }
12201 };
12202 processor
12203 .draw_proto_masks(&mut dst, &[], &proto, MaskOverlay::default())
12204 .unwrap_or_else(|e| panic!("{case}/proto_masks empty+no-bg failed: {e:?}"));
12205 assert_every_pixel_eq(&dst, [0, 0, 0, 0], &format!("{case}/proto"));
12206 }
12207
12208 fn scenario_empty_with_bg(processor: &mut ImageProcessor, case: &str) {
12211 let bg_color = [42, 99, 200, 255];
12212 let bg = make_bg(64, 64, None, bg_color);
12213 let overlay = MaskOverlay::new().with_background(&bg);
12214
12215 let mut dst = make_dirty_dst(64, 64, None);
12216 processor
12217 .draw_decoded_masks(&mut dst, &[], &[], overlay)
12218 .unwrap_or_else(|e| panic!("{case}/decoded_masks empty+bg failed: {e:?}"));
12219 assert_every_pixel_eq(&dst, bg_color, &format!("{case}/decoded bg blit"));
12220
12221 let mut dst = make_dirty_dst(64, 64, None);
12222 let proto = {
12223 use edgefirst_tensor::{Tensor, TensorDyn};
12224 let coeff_t = Tensor::<f32>::from_slice(&[0.0_f32; 4], &[1, 4]).unwrap();
12226 let protos_t =
12227 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
12228 ProtoData {
12229 mask_coefficients: TensorDyn::F32(coeff_t),
12230 protos: TensorDyn::F32(protos_t),
12231 layout: ProtoLayout::Nhwc,
12232 }
12233 };
12234 processor
12235 .draw_proto_masks(&mut dst, &[], &proto, overlay)
12236 .unwrap_or_else(|e| panic!("{case}/proto_masks empty+bg failed: {e:?}"));
12237 assert_every_pixel_eq(&dst, bg_color, &format!("{case}/proto bg blit"));
12238 }
12239
12240 fn scenario_detect_no_bg(processor: &mut ImageProcessor, case: &str) {
12244 use edgefirst_tensor::Segmentation;
12245 use ndarray::Array3;
12246 processor
12247 .set_class_colors(&[[200, 80, 40, 255]])
12248 .expect("set_class_colors");
12249
12250 let detect = DetectBox {
12251 bbox: [0.25, 0.25, 0.75, 0.75].into(),
12252 score: 0.99,
12253 label: 0,
12254 };
12255 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
12256 let seg = Segmentation {
12257 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
12258 .unwrap()
12259 .into(),
12260 xmin: 0.25,
12261 ymin: 0.25,
12262 xmax: 0.75,
12263 ymax: 0.75,
12264 };
12265
12266 let mut dst = make_dirty_dst(64, 64, None);
12267 processor
12268 .draw_decoded_masks(&mut dst, &[detect], &[seg], MaskOverlay::default())
12269 .unwrap_or_else(|e| panic!("{case}/decoded_masks detect+no-bg failed: {e:?}"));
12270
12271 let corner = pixel_at(&dst, 2, 2);
12273 assert_eq!(
12274 corner,
12275 [0, 0, 0, 0],
12276 "{case}/decoded: corner (2,2) leaked dirty pattern: {corner:?}"
12277 );
12278 let center = pixel_at(&dst, 32, 32);
12282 assert!(
12283 center != [0, 0, 0, 0],
12284 "{case}/decoded: center (32,32) was not coloured: {center:?}"
12285 );
12286 }
12287
12288 fn scenario_detect_with_bg(processor: &mut ImageProcessor, case: &str) {
12291 use edgefirst_tensor::Segmentation;
12292 use ndarray::Array3;
12293 processor
12294 .set_class_colors(&[[200, 80, 40, 255]])
12295 .expect("set_class_colors");
12296 let bg_color = [10, 20, 30, 255];
12297 let bg = make_bg(64, 64, None, bg_color);
12298
12299 let detect = DetectBox {
12300 bbox: [0.25, 0.25, 0.75, 0.75].into(),
12301 score: 0.99,
12302 label: 0,
12303 };
12304 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
12305 let seg = Segmentation {
12306 segmentation: edgefirst_tensor::Tensor::from_arrayview3(seg_arr.view())
12307 .unwrap()
12308 .into(),
12309 xmin: 0.25,
12310 ymin: 0.25,
12311 xmax: 0.75,
12312 ymax: 0.75,
12313 };
12314
12315 let overlay = MaskOverlay::new().with_background(&bg);
12316 let mut dst = make_dirty_dst(64, 64, None);
12317 processor
12318 .draw_decoded_masks(&mut dst, &[detect], &[seg], overlay)
12319 .unwrap_or_else(|e| panic!("{case}/decoded_masks detect+bg failed: {e:?}"));
12320
12321 let corner = pixel_at(&dst, 2, 2);
12323 assert_eq!(
12324 corner, bg_color,
12325 "{case}/decoded: corner (2,2) should show bg {bg_color:?} got {corner:?}"
12326 );
12327 let center = pixel_at(&dst, 32, 32);
12330 assert!(
12331 center != bg_color,
12332 "{case}/decoded: center (32,32) should differ from bg {bg_color:?}, got {center:?}"
12333 );
12334 }
12335
12336 fn run_all_scenarios(
12339 force_backend: Option<&'static str>,
12340 case: &'static str,
12341 require_dma_for_bg: bool,
12342 ) {
12343 if require_dma_for_bg && !crate::test_support::is_dma_available() {
12344 crate::test_support::report_skip(&format!("{case} — DMA not available on this host"));
12345 return;
12346 }
12347 let processor_result = with_force_backend(force_backend, ImageProcessor::new);
12348 let mut processor = match processor_result {
12349 Ok(p) => p,
12350 Err(e) => {
12351 crate::test_support::report_skip(&format!("{case} — backend init failed: {e:?}"));
12352 return;
12353 }
12354 };
12355 scenario_empty_no_bg(&mut processor, case);
12356 scenario_empty_with_bg(&mut processor, case);
12357 scenario_detect_no_bg(&mut processor, case);
12358 scenario_detect_with_bg(&mut processor, case);
12359 }
12360
12361 #[test]
12362 fn test_draw_masks_4_scenarios_cpu() {
12363 run_all_scenarios(Some("cpu"), "cpu", false);
12364 }
12365
12366 #[test]
12367 fn test_draw_masks_4_scenarios_auto() {
12368 run_all_scenarios(None, "auto", false);
12369 }
12370
12371 #[cfg(any(
12372 target_os = "linux",
12373 target_os = "macos",
12374 target_os = "ios",
12375 target_os = "android",
12376 target_os = "windows"
12377 ))]
12378 #[cfg(feature = "opengl")]
12379 #[test]
12380 fn test_draw_masks_4_scenarios_opengl() {
12381 run_all_scenarios(Some("opengl"), "opengl", false);
12382 }
12383
12384 #[cfg(target_os = "linux")]
12389 #[test]
12390 fn test_draw_masks_zero_detection_g2d_forced() {
12391 if !crate::test_support::is_dma_available() {
12392 crate::test_support::report_skip("g2d forced — DMA not available on this host");
12393 return;
12394 }
12395 let processor_result = with_force_backend(Some("g2d"), ImageProcessor::new);
12396 let mut processor = match processor_result {
12397 Ok(p) => p,
12398 Err(e) => {
12399 crate::test_support::report_skip(&format!("g2d forced — init failed: {e:?}"));
12400 return;
12401 }
12402 };
12403
12404 let mut dst = TensorDyn::image(
12406 64,
12407 64,
12408 PixelFormat::Rgba,
12409 DType::U8,
12410 Some(TensorMemory::DmaBuf),
12411 edgefirst_tensor::CpuAccess::ReadWrite,
12412 )
12413 .unwrap();
12414 {
12415 use edgefirst_tensor::TensorMapTrait;
12416 let u8t = dst.as_typed_mut::<u8>().unwrap();
12417 let mut map = u8t.map().unwrap();
12418 map.as_mut_slice().fill(0xBB);
12419 }
12420 processor
12421 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::default())
12422 .expect("g2d empty+no-bg");
12423 assert_every_pixel_eq(&dst, [0, 0, 0, 0], "g2d/case1 cleared");
12424
12425 let bg_color = [7, 11, 13, 255];
12427 let bg = {
12428 let t = TensorDyn::image(
12429 64,
12430 64,
12431 PixelFormat::Rgba,
12432 DType::U8,
12433 Some(TensorMemory::DmaBuf),
12434 edgefirst_tensor::CpuAccess::ReadWrite,
12435 )
12436 .unwrap();
12437 {
12438 use edgefirst_tensor::TensorMapTrait;
12439 let u8t = t.as_typed::<u8>().unwrap();
12440 let mut map = u8t.map().unwrap();
12441 for chunk in map.as_mut_slice().as_chunks_mut::<4>().0 {
12442 chunk.copy_from_slice(&bg_color);
12443 }
12444 }
12445 t
12446 };
12447 let mut dst = TensorDyn::image(
12448 64,
12449 64,
12450 PixelFormat::Rgba,
12451 DType::U8,
12452 Some(TensorMemory::DmaBuf),
12453 edgefirst_tensor::CpuAccess::ReadWrite,
12454 )
12455 .unwrap();
12456 {
12457 use edgefirst_tensor::TensorMapTrait;
12458 let u8t = dst.as_typed_mut::<u8>().unwrap();
12459 let mut map = u8t.map().unwrap();
12460 map.as_mut_slice().fill(0x55);
12461 }
12462 processor
12463 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::new().with_background(&bg))
12464 .expect("g2d empty+bg");
12465 assert_every_pixel_eq(&dst, bg_color, "g2d/case2 bg blit");
12466
12467 let detect = DetectBox {
12469 bbox: [0.25, 0.25, 0.75, 0.75].into(),
12470 score: 0.9,
12471 label: 0,
12472 };
12473 let mut dst = TensorDyn::image(
12474 64,
12475 64,
12476 PixelFormat::Rgba,
12477 DType::U8,
12478 Some(TensorMemory::DmaBuf),
12479 edgefirst_tensor::CpuAccess::ReadWrite,
12480 )
12481 .unwrap();
12482 let err = processor
12483 .draw_decoded_masks(&mut dst, &[detect], &[], MaskOverlay::default())
12484 .expect_err("g2d must reject detect-present draw_decoded_masks");
12485 assert!(
12486 matches!(err, Error::NotImplemented(_)),
12487 "g2d case3 wrong error: {err:?}"
12488 );
12489 }
12490
12491 #[test]
12492 fn test_set_format_then_cpu_convert() {
12493 let _lock = acquire_env_lock();
12496 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
12497 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
12498 let mut processor = ImageProcessor::new().unwrap();
12499
12500 let image = edgefirst_bench::testdata::read("zidane.jpg");
12502 let src = load_image_test_helper(&image, Some(PixelFormat::Rgba), None).unwrap();
12503
12504 let mut dst =
12506 TensorDyn::new(&[640, 640, 3], DType::U8, Some(TensorMemory::Mem), None).unwrap();
12507 dst.set_format(PixelFormat::Rgb).unwrap();
12508
12509 processor.convert(&src, &mut dst, None).unwrap();
12511
12512 assert_eq!(dst.format(), Some(PixelFormat::Rgb));
12514 assert_eq!(dst.width(), Some(640));
12515 assert_eq!(dst.height(), Some(640));
12516 }
12517
12518 #[test]
12524 fn test_multiple_image_processors_same_thread() {
12525 let _lock = acquire_env_lock();
12528 let mut processors: Vec<ImageProcessor> = (0..4)
12529 .map(|_| ImageProcessor::new().expect("ImageProcessor::new() failed"))
12530 .collect();
12531
12532 for proc in &mut processors {
12533 let src = proc
12534 .create_image(
12535 128,
12536 128,
12537 PixelFormat::Rgb,
12538 DType::U8,
12539 None,
12540 edgefirst_tensor::CpuAccess::ReadWrite,
12541 )
12542 .expect("create src failed");
12543 let mut dst = proc
12544 .create_image(
12545 64,
12546 64,
12547 PixelFormat::Rgb,
12548 DType::U8,
12549 None,
12550 edgefirst_tensor::CpuAccess::ReadWrite,
12551 )
12552 .expect("create dst failed");
12553 proc.convert(&src, &mut dst, None).expect("convert failed");
12554 assert_eq!(dst.width(), Some(64));
12555 assert_eq!(dst.height(), Some(64));
12556 }
12557 }
12558
12559 #[test]
12566 fn test_multiple_image_processors_separate_threads() {
12567 use std::sync::mpsc;
12568 use std::time::Duration;
12569
12570 if std::env::var_os("EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS").is_some() {
12581 crate::test_support::report_skip(
12582 "test_multiple_image_processors_separate_threads — known Vivante \
12583 GC7000UL concurrent-EGL-teardown double-free \
12584 (EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS set)",
12585 );
12586 return;
12587 }
12588
12589 const TIMEOUT: Duration = Duration::from_secs(60);
12590
12591 let _lock = acquire_env_lock();
12594
12595 let (tx, rx) = mpsc::channel::<()>();
12596
12597 std::thread::spawn(move || {
12598 let handles: Vec<_> = (0..4)
12599 .map(|i| {
12600 std::thread::spawn(move || {
12601 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
12602 panic!("ImageProcessor::new() failed on thread {i}: {e}")
12603 });
12604 let src = proc
12605 .create_image(
12606 128,
12607 128,
12608 PixelFormat::Rgb,
12609 DType::U8,
12610 None,
12611 edgefirst_tensor::CpuAccess::ReadWrite,
12612 )
12613 .unwrap_or_else(|e| panic!("create src failed on thread {i}: {e}"));
12614 let mut dst = proc
12615 .create_image(
12616 64,
12617 64,
12618 PixelFormat::Rgb,
12619 DType::U8,
12620 None,
12621 edgefirst_tensor::CpuAccess::ReadWrite,
12622 )
12623 .unwrap_or_else(|e| panic!("create dst failed on thread {i}: {e}"));
12624 proc.convert(&src, &mut dst, None)
12625 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
12626 assert_eq!(dst.width(), Some(64));
12627 assert_eq!(dst.height(), Some(64));
12628 })
12629 })
12630 .collect();
12631
12632 for (i, h) in handles.into_iter().enumerate() {
12633 h.join()
12634 .unwrap_or_else(|e| panic!("thread {i} panicked: {e:?}"));
12635 }
12636
12637 let _ = tx.send(());
12638 });
12639
12640 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
12641 panic!("test_multiple_image_processors_separate_threads timed out after {TIMEOUT:?}")
12642 });
12643 }
12644
12645 #[test]
12652 fn test_image_processors_concurrent_operations() {
12653 use std::sync::{mpsc, Arc, Barrier};
12654 use std::time::Duration;
12655
12656 const N: usize = 4;
12657 const ROUNDS: usize = 10;
12658 const TIMEOUT: Duration = Duration::from_secs(60);
12659
12660 let _lock = acquire_env_lock();
12663
12664 let (tx, rx) = mpsc::channel::<()>();
12665
12666 std::thread::spawn(move || {
12667 let barrier = Arc::new(Barrier::new(N));
12668
12669 let handles: Vec<_> = (0..N)
12670 .map(|i| {
12671 let barrier = Arc::clone(&barrier);
12672 std::thread::spawn(move || {
12673 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
12674 panic!("ImageProcessor::new() failed on thread {i}: {e}")
12675 });
12676
12677 barrier.wait();
12679
12680 for round in 0..ROUNDS {
12682 let src = proc
12683 .create_image(
12684 128,
12685 128,
12686 PixelFormat::Rgb,
12687 DType::U8,
12688 None,
12689 edgefirst_tensor::CpuAccess::ReadWrite,
12690 )
12691 .unwrap_or_else(|e| {
12692 panic!("create src failed on thread {i} round {round}: {e}")
12693 });
12694 let mut dst = proc
12695 .create_image(
12696 64,
12697 64,
12698 PixelFormat::Rgb,
12699 DType::U8,
12700 None,
12701 edgefirst_tensor::CpuAccess::ReadWrite,
12702 )
12703 .unwrap_or_else(|e| {
12704 panic!("create dst failed on thread {i} round {round}: {e}")
12705 });
12706 proc.convert(&src, &mut dst, None).unwrap_or_else(|e| {
12707 panic!("convert failed on thread {i} round {round}: {e}")
12708 });
12709 assert_eq!(dst.width(), Some(64));
12710 assert_eq!(dst.height(), Some(64));
12711 }
12712 })
12713 })
12714 .collect();
12715
12716 for (i, h) in handles.into_iter().enumerate() {
12717 h.join()
12718 .unwrap_or_else(|e| panic!("thread {i} panicked: {e:?}"));
12719 }
12720
12721 let _ = tx.send(());
12722 });
12723
12724 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
12725 panic!("test_image_processors_concurrent_operations timed out after {TIMEOUT:?}")
12726 });
12727 }
12728
12729 #[test]
12747 fn test_parallel_processors_unique_outputs() {
12748 use std::sync::{mpsc, Arc, Barrier};
12749 use std::time::Duration;
12750
12751 const N: usize = 4;
12752 const ROUNDS: usize = 25;
12753 const TIMEOUT: Duration = Duration::from_secs(60);
12754
12755 if std::env::var_os("EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS").is_some() {
12756 crate::test_support::report_skip(
12757 "test_parallel_processors_unique_outputs — known Vivante \
12758 GC7000UL concurrent-multi-processor driver abort \
12759 (EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS set)",
12760 );
12761 return;
12762 }
12763
12764 let _lock = acquire_env_lock();
12765 let use_dma = crate::test_support::is_dma_available();
12767 if !use_dma {
12768 crate::test_support::report_skip(&format!(
12769 "{} - DmaBuf sources not exercised, running on Mem: no usable DMA heap",
12770 function!()
12771 ));
12772 }
12773 let (tx, rx) = mpsc::channel::<()>();
12774
12775 std::thread::spawn(move || {
12776 let barrier = Arc::new(Barrier::new(N));
12777 let handles: Vec<_> = (0..N)
12778 .map(|i| {
12779 let barrier = Arc::clone(&barrier);
12780 std::thread::spawn(move || {
12781 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
12782 panic!("ImageProcessor::new() failed on thread {i}: {e}")
12783 });
12784 let (w, h) = (640usize, 480usize);
12786 let mem = if use_dma {
12787 Some(TensorMemory::DmaBuf)
12788 } else {
12789 Some(TensorMemory::Mem)
12790 };
12791 let src = proc
12792 .create_image(
12793 w,
12794 h,
12795 PixelFormat::Nv12,
12796 DType::U8,
12797 mem,
12798 edgefirst_tensor::CpuAccess::ReadWrite,
12799 )
12800 .unwrap();
12801 {
12802 let t = src.as_typed::<u8>().unwrap();
12803 let mut m = t.map().unwrap();
12804 let s = m.as_mut_slice();
12805 for (j, b) in s[..w * h].iter_mut().enumerate() {
12806 *b = ((i * 53 + j) % 200 + 16) as u8;
12807 }
12808 for b in &mut s[w * h..] {
12809 *b = (80 + i * 24) as u8;
12810 }
12811 }
12812 let lb = Crop::letterbox([114, 114, 114, 255]);
12813 let convert_once = |proc: &mut ImageProcessor| -> Vec<u8> {
12814 let mut dst = proc
12815 .create_image(
12816 320,
12817 320,
12818 PixelFormat::Rgba,
12819 DType::U8,
12820 mem,
12821 edgefirst_tensor::CpuAccess::ReadWrite,
12822 )
12823 .unwrap();
12824 proc.convert(
12825 &src,
12826 &mut dst,
12827 Some(crate::ConvertOptions::new().crop(lb)),
12828 )
12829 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
12830 let t = dst.as_typed::<u8>().unwrap();
12831 let m = t.map().unwrap();
12832 m.as_slice().to_vec()
12833 };
12834
12835 let oracle = convert_once(&mut proc);
12836 barrier.wait();
12837 for round in 0..ROUNDS {
12838 let out = convert_once(&mut proc);
12839 let diffs = oracle.iter().zip(&out).filter(|(a, b)| a != b).count();
12840 assert!(
12841 diffs == 0,
12842 "thread {i} round {round}: {diffs}/{} bytes diverged \
12843 from this processor's own oracle — cross-processor \
12844 GL state leakage under parallel execution",
12845 oracle.len()
12846 );
12847 }
12848 })
12849 })
12850 .collect();
12851
12852 for (i, h) in handles.into_iter().enumerate() {
12853 h.join()
12854 .unwrap_or_else(|e| panic!("parallel thread {i} panicked: {e:?}"));
12855 }
12856 let _ = tx.send(());
12857 });
12858
12859 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
12860 panic!("test_parallel_processors_unique_outputs timed out after {TIMEOUT:?}")
12861 });
12862 }
12863
12864 #[test]
12873 #[ignore = "heavy on-demand GL-parallelism stressor; run explicitly on boards"]
12874 fn stress_parallel_processors_oracle() {
12875 use std::sync::{mpsc, Arc, Barrier};
12876 use std::time::Duration;
12877
12878 const N: usize = 4;
12879 const ROUNDS: usize = 200;
12880 const TIMEOUT: Duration = Duration::from_secs(600);
12881
12882 let _lock = acquire_env_lock();
12883 let use_dma = crate::test_support::is_dma_available();
12885 if !use_dma {
12886 crate::test_support::report_skip(&format!(
12887 "{} - DmaBuf sources not exercised, running on Mem: no usable DMA heap",
12888 function!()
12889 ));
12890 }
12891 let (tx, rx) = mpsc::channel::<()>();
12892
12893 std::thread::spawn(move || {
12894 let barrier = Arc::new(Barrier::new(N));
12895 let handles: Vec<_> = (0..N)
12896 .map(|i| {
12897 let barrier = Arc::clone(&barrier);
12898 std::thread::spawn(move || {
12899 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
12900 panic!("ImageProcessor::new() failed on thread {i}: {e}")
12901 });
12902 let (w, h) = (1280usize, 720usize);
12903 let mem = if use_dma {
12904 Some(TensorMemory::DmaBuf)
12905 } else {
12906 Some(TensorMemory::Mem)
12907 };
12908
12909 let src = proc
12912 .create_image(
12913 w,
12914 h,
12915 PixelFormat::Nv12,
12916 DType::U8,
12917 mem,
12918 edgefirst_tensor::CpuAccess::ReadWrite,
12919 )
12920 .unwrap();
12921 {
12922 let t = src.as_typed::<u8>().unwrap();
12923 let mut m = t.map().unwrap();
12924 let s = m.as_mut_slice();
12925 for (j, b) in s[..w * h].iter_mut().enumerate() {
12926 *b = ((i * 37 + j) % 200 + 16) as u8;
12927 }
12928 for b in &mut s[w * h..] {
12929 *b = (96 + i * 16) as u8;
12930 }
12931 }
12932 let lb = Crop::letterbox([114, 114, 114, 255]);
12933
12934 let convert_once = |proc: &mut ImageProcessor| -> Vec<u8> {
12935 let mut dst = proc
12936 .create_image(
12937 640,
12938 640,
12939 PixelFormat::Rgb,
12940 DType::U8,
12941 mem,
12942 edgefirst_tensor::CpuAccess::ReadWrite,
12943 )
12944 .unwrap();
12945 proc.convert(
12946 &src,
12947 &mut dst,
12948 Some(crate::ConvertOptions::new().crop(lb)),
12949 )
12950 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
12951 let t = dst.as_typed::<u8>().unwrap();
12952 let m = t.map().unwrap();
12953 m.as_slice().to_vec()
12954 };
12955
12956 let oracle = convert_once(&mut proc);
12957 barrier.wait();
12958 for round in 0..ROUNDS {
12959 let out = convert_once(&mut proc);
12960 let diffs = oracle.iter().zip(&out).filter(|(a, b)| a != b).count();
12961 assert!(
12962 diffs == 0,
12963 "thread {i} round {round}: {diffs}/{} bytes diverged \
12964 from the pre-barrier oracle",
12965 oracle.len()
12966 );
12967 }
12968 })
12969 })
12970 .collect();
12971
12972 for (i, h) in handles.into_iter().enumerate() {
12973 h.join()
12974 .unwrap_or_else(|e| panic!("stressor thread {i} panicked: {e:?}"));
12975 }
12976 let _ = tx.send(());
12977 });
12978
12979 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
12980 panic!("stress_parallel_processors_oracle timed out after {TIMEOUT:?}")
12981 });
12982 }
12983
12984 #[test]
12997 fn convert_f32_auto_never_errors_non_gl_combo() {
12998 const W: usize = 64;
12999 const H: usize = 64;
13000
13001 let src = TensorDyn::image(
13004 W,
13005 H,
13006 PixelFormat::Yuyv,
13007 DType::U8,
13008 Some(TensorMemory::Mem),
13009 edgefirst_tensor::CpuAccess::ReadWrite,
13010 )
13011 .unwrap();
13012 {
13013 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13014 let data = map.as_mut_slice();
13015 for chunk in data.as_chunks_mut::<4>().0 {
13016 chunk[0] = 128; chunk[1] = 128; chunk[2] = 160; chunk[3] = 128; }
13021 }
13022
13023 let mut dst = TensorDyn::image(
13024 W,
13025 H,
13026 PixelFormat::Rgb,
13027 DType::F32,
13028 Some(TensorMemory::Mem),
13029 edgefirst_tensor::CpuAccess::ReadWrite,
13030 )
13031 .unwrap();
13032
13033 let mut proc = ImageProcessor::new().unwrap();
13034 let result = proc.convert(&src, &mut dst, None);
13035 assert!(
13036 result.is_ok(),
13037 "auto-chain Yuyv→Rgb F32 must not error: {:?}",
13038 result.err()
13039 );
13040
13041 let map = dst.as_typed::<f32>().unwrap().map().unwrap();
13043 let floats = map.as_slice();
13044 assert_eq!(floats.len(), W * H * 3, "unexpected output element count");
13045 for (i, &v) in floats.iter().enumerate() {
13046 assert!(
13047 v.is_finite() && (0.0..=1.0).contains(&v),
13048 "output[{i}]={v} is not finite or not in [0,1]"
13049 );
13050 }
13051
13052 let first_non_zero = floats.iter().find(|&&v| v > 0.01);
13056 assert!(
13057 first_non_zero.is_some(),
13058 "all-zero output detected — CPU path likely did not write to the destination buffer"
13059 );
13060 let r0 = floats[0];
13063 assert!(
13064 (r0 - 0.502_f32).abs() < 0.05,
13065 "first pixel R={r0} expected ≈0.502 (Y=128 neutral grey from YUYV source)"
13066 );
13067 }
13068
13069 #[test]
13075 #[allow(clippy::needless_update)]
13081 fn convert_f16_forced_cpu_correct() {
13082 const W: usize = 16;
13083 const H: usize = 16;
13084 const TOL: f32 = 1.0 / 512.0; let src = TensorDyn::image(
13088 W,
13089 H,
13090 PixelFormat::Rgba,
13091 DType::U8,
13092 Some(TensorMemory::Mem),
13093 edgefirst_tensor::CpuAccess::ReadWrite,
13094 )
13095 .unwrap();
13096 {
13097 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13098 let data = map.as_mut_slice();
13099 for y in 0..H {
13100 for x in 0..W {
13101 let i = y * W + x;
13102 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;
13106 }
13107 }
13108 }
13109
13110 let mut dst = TensorDyn::image(
13111 W,
13112 H,
13113 PixelFormat::PlanarRgb,
13114 DType::F16,
13115 Some(TensorMemory::Mem),
13116 edgefirst_tensor::CpuAccess::ReadWrite,
13117 )
13118 .unwrap();
13119
13120 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
13121 backend: ComputeBackend::Cpu,
13122 ..Default::default()
13123 })
13124 .unwrap();
13125 proc.convert(&src, &mut dst, None)
13126 .expect("forced-CPU Rgba→PlanarRgb F16 must not error");
13127
13128 let src_map = src.as_typed::<u8>().unwrap().map().unwrap();
13129 let src_bytes = src_map.as_slice();
13130 let dst_map = dst.as_typed::<half::f16>().unwrap().map().unwrap();
13131 let dst_halfs = dst_map.as_slice();
13132
13133 let plane = W * H;
13134 assert_eq!(dst_halfs.len(), plane * 3, "wrong output element count");
13135
13136 for y in 0..H {
13137 for x in 0..W {
13138 let i = y * W + x;
13139 let r_exp = src_bytes[i * 4] as f32 / 255.0;
13140 let g_exp = src_bytes[i * 4 + 1] as f32 / 255.0;
13141 let b_exp = src_bytes[i * 4 + 2] as f32 / 255.0;
13142
13143 let r_got = dst_halfs[i].to_f32();
13144 let g_got = dst_halfs[plane + i].to_f32();
13145 let b_got = dst_halfs[2 * plane + i].to_f32();
13146
13147 assert!(
13148 (r_got - r_exp).abs() <= TOL,
13149 "R plane ({x},{y}): got {r_got}, expected {r_exp}"
13150 );
13151 assert!(
13152 (g_got - g_exp).abs() <= TOL,
13153 "G plane ({x},{y}): got {g_got}, expected {g_exp}"
13154 );
13155 assert!(
13156 (b_got - b_exp).abs() <= TOL,
13157 "B plane ({x},{y}): got {b_got}, expected {b_exp}"
13158 );
13159
13160 if src_bytes[i * 4] != src_bytes[i * 4 + 1] {
13162 assert_ne!(r_got, g_got, "R and G planes must differ at ({x},{y})");
13163 }
13164 }
13165 }
13166 }
13167
13168 #[test]
13176 fn convert_f32_with_rotation_falls_back() {
13177 const W: usize = 16;
13178 const H: usize = 16;
13179
13180 let src = TensorDyn::image(
13182 W,
13183 H,
13184 PixelFormat::Rgba,
13185 DType::U8,
13186 Some(TensorMemory::Mem),
13187 edgefirst_tensor::CpuAccess::ReadWrite,
13188 )
13189 .unwrap();
13190 {
13191 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13192 let data = map.as_mut_slice();
13193 for y in 0..H {
13194 for x in 0..W {
13195 let i = y * W + x;
13196 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;
13200 }
13201 }
13202 }
13203
13204 let mut dst = TensorDyn::image(
13206 H, W, PixelFormat::Rgb,
13209 DType::F32,
13210 Some(TensorMemory::Mem),
13211 edgefirst_tensor::CpuAccess::ReadWrite,
13212 )
13213 .unwrap();
13214
13215 let mut proc = ImageProcessor::new().unwrap();
13216 let result = proc.convert(
13217 &src,
13218 &mut dst,
13219 Some(crate::ConvertOptions::new().rotation(Rotation::Clockwise90)),
13220 );
13221 assert!(
13222 result.is_ok(),
13223 "auto-chain Rgba→Rgb F32 with Rot90 must not error: {:?}",
13224 result.err()
13225 );
13226
13227 let map = dst.as_typed::<f32>().unwrap().map().unwrap();
13228 let floats = map.as_slice();
13229 assert_eq!(floats.len(), H * W * 3, "unexpected output element count");
13230 for (i, &v) in floats.iter().enumerate() {
13231 assert!(
13232 v.is_finite() && (0.0..=1.0).contains(&v),
13233 "output[{i}]={v} is not finite or not in [0,1]"
13234 );
13235 }
13236 }
13237
13238 #[test]
13264 #[cfg(all(
13265 any(
13266 target_os = "linux",
13267 target_os = "macos",
13268 target_os = "ios",
13269 target_os = "android",
13270 target_os = "windows"
13271 ),
13272 feature = "opengl"
13273 ))]
13274 fn convert_f16_gl_cpu_parity_identity() {
13275 #[cfg(target_os = "macos")]
13281 if std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1")
13282 && std::env::var_os("HAL_TEST_ALLOW_DLOPEN_ANGLE").is_none()
13283 {
13284 crate::test_support::report_skip(
13285 "convert_f16_gl_cpu_parity_identity - ANGLE dlopen gate closed (coverage pass 1)",
13286 );
13287 return;
13288 }
13289 if !is_opengl_available() {
13290 crate::test_support::report_skip(
13291 "convert_f16_gl_cpu_parity_identity - OpenGL not available",
13292 );
13293 return;
13294 }
13295
13296 const W: usize = 32;
13298 const H: usize = 16;
13299 const TOL: f32 = 1.0 / 256.0; let src = TensorDyn::image(
13303 W,
13304 H,
13305 PixelFormat::Rgba,
13306 DType::U8,
13307 Some(TensorMemory::Mem),
13308 edgefirst_tensor::CpuAccess::ReadWrite,
13309 )
13310 .unwrap();
13311 {
13312 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13313 let data = map.as_mut_slice();
13314 for y in 0..H {
13315 for x in 0..W {
13316 let i = y * W + x;
13317 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;
13321 }
13322 }
13323 }
13324
13325 let gl_result = {
13327 let mut gl_proc = match ImageProcessor::with_config(ImageProcessorConfig {
13328 backend: ComputeBackend::OpenGl,
13329 ..Default::default()
13330 }) {
13331 Ok(p) => p,
13332 Err(e) => {
13333 crate::test_support::report_skip(&format!(
13334 "convert_f16_gl_cpu_parity_identity - GL backend unavailable: {e}"
13335 ));
13336 return;
13337 }
13338 };
13339
13340 if !gl_proc.supported_render_dtypes().f16 {
13341 crate::test_support::report_skip(
13342 "convert_f16_gl_cpu_parity_identity - F16 render not supported",
13343 );
13344 return;
13345 }
13346
13347 let Some(mut dst) = crate::test_support::gpu_buffer_allocated(
13368 TensorDyn::image(
13369 W,
13370 H,
13371 PixelFormat::PlanarRgb,
13372 DType::F16,
13373 Some(TensorMemory::DmaBuf),
13374 edgefirst_tensor::CpuAccess::ReadWrite,
13375 ),
13376 "zero-copy F16 PlanarRgb destination",
13377 ) else {
13378 return;
13379 };
13380 if dst.memory() != TensorMemory::DmaBuf {
13381 crate::test_support::report_skip(&format!(
13382 "convert_f16_gl_cpu_parity_identity - the zero-copy F16 \
13383 destination fell back to {:?}",
13384 dst.memory()
13385 ));
13386 return;
13387 }
13388 let require_gl = std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1");
13389 let fallbacks_before = gl_proc.convert_fallback_count();
13390 match gl_proc.convert(&src, &mut dst, None) {
13391 Ok(()) => {
13392 if gl_proc.convert_fallback_count() != fallbacks_before {
13393 assert!(
13394 !require_gl,
13395 "HAL_TEST_REQUIRE_GL=1 but \
13396 convert_f16_gl_cpu_parity_identity would have skipped \
13397 while reporting success: the GL F16 path declined and \
13398 the CPU served the convert"
13399 );
13400 crate::test_support::report_skip(
13401 "convert_f16_gl_cpu_parity_identity - the GL F16 path \
13402 declined and the CPU served the convert; comparing that \
13403 against the forced-CPU result would prove nothing",
13404 );
13405 return;
13406 }
13407 dst
13408 }
13409 Err(e) => {
13410 crate::test_support::report_skip(&format!(
13411 "convert_f16_gl_cpu_parity_identity - GL convert failed: {e}"
13412 ));
13413 return;
13414 }
13415 }
13416 };
13417
13418 let cpu_result = {
13420 let mut cpu_proc = ImageProcessor::with_config(ImageProcessorConfig {
13421 backend: ComputeBackend::Cpu,
13422 ..Default::default()
13423 })
13424 .unwrap();
13425 let mut dst = TensorDyn::image(
13426 W,
13427 H,
13428 PixelFormat::PlanarRgb,
13429 DType::F16,
13430 Some(TensorMemory::Mem),
13431 edgefirst_tensor::CpuAccess::ReadWrite,
13432 )
13433 .unwrap();
13434 cpu_proc
13435 .convert(&src, &mut dst, None)
13436 .expect("forced-CPU Rgba→PlanarRgb F16 must not error");
13437 dst
13438 };
13439
13440 let flat_f16 = |t: &TensorDyn| -> Vec<half::f16> {
13445 let typed = t.as_typed::<half::f16>().expect("F16 image");
13446 let mut bytes = vec![0u8; typed.shape().iter().product::<usize>() * 2];
13447 typed
13448 .copy_to_flat(&mut bytes)
13449 .expect("compact any padded rows");
13450 bytes
13451 .as_chunks::<2>()
13452 .0
13453 .iter()
13454 .map(|&c| half::f16::from_le_bytes(c))
13455 .collect()
13456 };
13457 let gl_halfs = flat_f16(&gl_result);
13458 let cpu_halfs = flat_f16(&cpu_result);
13459
13460 assert_eq!(
13461 gl_halfs.len(),
13462 cpu_halfs.len(),
13463 "GL and CPU output sizes differ"
13464 );
13465
13466 let plane = W * H;
13467 let channel_names = ["R", "G", "B"];
13468 for (idx, (gl_h, cpu_h)) in gl_halfs.iter().zip(cpu_halfs.iter()).enumerate() {
13469 let gl_v = gl_h.to_f32();
13470 let cpu_v = cpu_h.to_f32();
13471 let err = (gl_v - cpu_v).abs();
13472 let ch = channel_names[idx / plane];
13473 let pixel = idx % plane;
13474 assert!(
13475 err <= TOL,
13476 "GL vs CPU mismatch at {ch}[{pixel}]: GL={gl_v}, CPU={cpu_v}, err={err} > tol={TOL}"
13477 );
13478 }
13479 }
13480
13481 #[test]
13489 #[cfg(all(any(target_os = "linux", target_os = "windows"), feature = "opengl"))]
13490 fn supported_render_dtypes_linux_smoke() {
13491 let proc = match ImageProcessor::new() {
13492 Ok(p) => p,
13493 Err(e) => {
13494 crate::test_support::report_skip(&format!(
13495 "supported_render_dtypes_linux_smoke — ImageProcessor::new() failed: {e}"
13496 ));
13497 return;
13498 }
13499 };
13500 if proc.opengl.is_none() {
13501 crate::test_support::report_skip(
13502 "supported_render_dtypes_linux_smoke — no GL backend on this host",
13503 );
13504 return;
13505 }
13506 let support = proc.supported_render_dtypes();
13508 eprintln!(
13509 "supported_render_dtypes_linux_smoke: f16={} f32={}",
13510 support.f16, support.f32
13511 );
13512 }
13514
13515 #[test]
13524 fn convert_f16_pbo_non_4_aligned_width_falls_back() {
13525 const W: usize = 18; const H: usize = 16;
13527
13528 let src = TensorDyn::image(
13530 W,
13531 H,
13532 PixelFormat::Rgba,
13533 DType::U8,
13534 Some(TensorMemory::Mem),
13535 edgefirst_tensor::CpuAccess::ReadWrite,
13536 )
13537 .unwrap();
13538 {
13539 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13540 let data = map.as_mut_slice();
13541 for chunk in data.as_chunks_mut::<4>().0 {
13542 chunk[0] = 128;
13543 chunk[1] = 64;
13544 chunk[2] = 200;
13545 chunk[3] = 255;
13546 }
13547 }
13548
13549 let mut dst = TensorDyn::image(
13552 W,
13553 H,
13554 PixelFormat::PlanarRgb,
13555 DType::F16,
13556 Some(TensorMemory::Mem),
13557 edgefirst_tensor::CpuAccess::ReadWrite,
13558 )
13559 .unwrap();
13560
13561 let mut proc = ImageProcessor::new().unwrap();
13564 let result = proc.convert(&src, &mut dst, None);
13565 assert!(
13566 result.is_ok(),
13567 "auto-chain PlanarRgb F16 W%4!=0 must not error (CPU fallback): {:?}",
13568 result.err()
13569 );
13570
13571 let map = dst.as_typed::<half::f16>().unwrap().map().unwrap();
13573 let halfs = map.as_slice();
13574 assert_eq!(halfs.len(), W * H * 3, "unexpected element count");
13575 for (i, h) in halfs.iter().enumerate() {
13576 let v = h.to_f32();
13577 assert!(
13578 v.is_finite() && (0.0..=1.0).contains(&v),
13579 "output[{i}]={v} is not finite or not in [0,1]"
13580 );
13581 }
13582 }
13583
13584 #[test]
13594 #[allow(clippy::needless_update)]
13597 fn convert_nv12_to_rgb_f32_cpu() {
13598 const W: usize = 16;
13599 const H: usize = 16; let src = TensorDyn::image(
13603 W,
13604 H,
13605 PixelFormat::Nv12,
13606 DType::U8,
13607 Some(TensorMemory::Mem),
13608 edgefirst_tensor::CpuAccess::ReadWrite,
13609 )
13610 .unwrap();
13611 {
13612 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13613 map.as_mut_slice().fill(128); }
13615
13616 let mut dst = TensorDyn::image(
13617 W,
13618 H,
13619 PixelFormat::Rgb,
13620 DType::F32,
13621 Some(TensorMemory::Mem),
13622 edgefirst_tensor::CpuAccess::ReadWrite,
13623 )
13624 .unwrap();
13625
13626 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
13627 backend: ComputeBackend::Cpu,
13628 ..Default::default()
13629 })
13630 .unwrap();
13631
13632 let result = proc.convert(&src, &mut dst, None);
13633 assert!(
13634 result.is_ok(),
13635 "forced-CPU NV12→Rgb F32 must not error: {:?}",
13636 result.err()
13637 );
13638
13639 let map = dst.as_typed::<f32>().unwrap().map().unwrap();
13640 let floats = map.as_slice();
13641 assert_eq!(floats.len(), W * H * 3, "unexpected element count");
13642 for (i, &v) in floats.iter().enumerate() {
13643 assert!(
13644 v.is_finite() && (0.0..=1.0).contains(&v),
13645 "output[{i}]={v} is not finite or not in [0,1]"
13646 );
13647 }
13648 let non_zero = floats.iter().any(|&v| v > 0.01);
13650 assert!(non_zero, "all-zero output from NV12→Rgb F32 CPU path");
13651 }
13652
13653 #[test]
13657 #[allow(clippy::needless_update)]
13660 fn convert_nv12_to_planar_rgb_f16_cpu() {
13661 const W: usize = 16;
13662 const H: usize = 16;
13663
13664 let src = TensorDyn::image(
13665 W,
13666 H,
13667 PixelFormat::Nv12,
13668 DType::U8,
13669 Some(TensorMemory::Mem),
13670 edgefirst_tensor::CpuAccess::ReadWrite,
13671 )
13672 .unwrap();
13673 {
13674 let mut map = src.as_typed::<u8>().unwrap().map().unwrap();
13675 map.as_mut_slice().fill(128);
13676 }
13677
13678 let mut dst = TensorDyn::image(
13679 W,
13680 H,
13681 PixelFormat::PlanarRgb,
13682 DType::F16,
13683 Some(TensorMemory::Mem),
13684 edgefirst_tensor::CpuAccess::ReadWrite,
13685 )
13686 .unwrap();
13687
13688 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
13689 backend: ComputeBackend::Cpu,
13690 ..Default::default()
13691 })
13692 .unwrap();
13693
13694 let result = proc.convert(&src, &mut dst, None);
13695 assert!(
13696 result.is_ok(),
13697 "forced-CPU NV12→PlanarRgb F16 must not error: {:?}",
13698 result.err()
13699 );
13700
13701 let map = dst.as_typed::<half::f16>().unwrap().map().unwrap();
13702 let halfs = map.as_slice();
13703 assert_eq!(halfs.len(), W * H * 3, "unexpected element count");
13704 for (i, h) in halfs.iter().enumerate() {
13705 let v = h.to_f32();
13706 assert!(
13707 v.is_finite() && (0.0..=1.0).contains(&v),
13708 "output[{i}]={v} is not finite or not in [0,1]"
13709 );
13710 }
13711 let non_zero = halfs.iter().any(|h| h.to_f32() > 0.01);
13712 assert!(non_zero, "all-zero output from NV12→PlanarRgb F16 CPU path");
13713 }
13714
13715 fn access_test_src(w: usize, h: usize) -> TensorDyn {
13720 use edgefirst_tensor::TensorMapTrait;
13721 let t = TensorDyn::image(
13722 w,
13723 h,
13724 PixelFormat::Rgba,
13725 DType::U8,
13726 Some(TensorMemory::Mem),
13727 edgefirst_tensor::CpuAccess::ReadWrite,
13728 )
13729 .unwrap();
13730 for (i, b) in t
13731 .as_u8()
13732 .unwrap()
13733 .map_mut()
13734 .unwrap()
13735 .as_mut_slice()
13736 .iter_mut()
13737 .enumerate()
13738 {
13739 *b = (i % 251) as u8;
13740 }
13741 t
13742 }
13743
13744 #[test]
13748 fn create_image_adds_readwrite_for_read_modify_write_passes() {
13749 use edgefirst_tensor::CpuAccess;
13750 let _lock = acquire_env_lock();
13751 let _guard = EnvGuard::snapshot(&[
13752 "EDGEFIRST_FORCE_BACKEND",
13753 "EDGEFIRST_DISABLE_GL",
13754 "EDGEFIRST_DISABLE_G2D",
13755 "EDGEFIRST_DISABLE_CPU",
13756 ]);
13757 unsafe {
13758 std::env::remove_var("EDGEFIRST_FORCE_BACKEND");
13759 std::env::remove_var("EDGEFIRST_DISABLE_CPU");
13760 std::env::set_var("EDGEFIRST_DISABLE_GL", "1");
13761 std::env::set_var("EDGEFIRST_DISABLE_G2D", "1");
13762 }
13763 let mut proc = ImageProcessor::new().unwrap();
13764 let src = access_test_src(128, 96);
13765 for (format, dtype) in [
13766 (PixelFormat::Rgb, DType::I8),
13767 (PixelFormat::Rgba, DType::I8),
13768 (PixelFormat::Bgra, DType::U8),
13769 ] {
13770 for requested in [CpuAccess::None, CpuAccess::Write] {
13771 let mut dst = proc
13772 .create_image(64, 48, format, dtype, None, requested)
13773 .unwrap();
13774 assert_eq!(
13775 dst.cpu_access(),
13776 CpuAccess::ReadWrite,
13777 "{format:?}/{dtype:?} requested {requested:?}"
13778 );
13779 proc.convert(&src, &mut dst, None).unwrap_or_else(|e| {
13780 panic!("CPU convert into {format:?}/{dtype:?} {requested:?}: {e}")
13781 });
13782 }
13783 }
13784 }
13785
13786 #[test]
13790 fn cpu_convert_refuses_a_write_only_destination_it_reads_back() {
13791 use edgefirst_tensor::{CpuAccess, Error as TensorError};
13792 let mut cpu = CPUProcessor::new();
13793 let src = access_test_src(32, 24);
13794 for (format, dtype) in [
13795 (PixelFormat::Rgb, DType::I8),
13796 (PixelFormat::Bgra, DType::U8),
13797 ] {
13798 let mut dst = TensorDyn::image(
13799 32,
13800 24,
13801 format,
13802 dtype,
13803 Some(TensorMemory::Mem),
13804 CpuAccess::Write,
13805 )
13806 .unwrap();
13807 match cpu.convert(&src, &mut dst, None) {
13808 Err(Error::Tensor(TensorError::CpuAccessDenied {
13809 declared: CpuAccess::Write,
13810 requested: CpuAccess::ReadWrite,
13811 })) => {}
13812 other => panic!("{format:?}/{dtype:?}: {other:?}"),
13813 }
13814 }
13815 let bgra_src = TensorDyn::image(
13816 32,
13817 24,
13818 PixelFormat::Bgra,
13819 DType::U8,
13820 Some(TensorMemory::Mem),
13821 CpuAccess::ReadWrite,
13822 )
13823 .unwrap();
13824 let mut dst = TensorDyn::image(
13825 32,
13826 24,
13827 PixelFormat::Bgra,
13828 DType::U8,
13829 Some(TensorMemory::Mem),
13830 CpuAccess::Write,
13831 )
13832 .unwrap();
13833 cpu.convert(&bgra_src, &mut dst, None).unwrap();
13834 }
13835
13836 #[test]
13840 fn predicted_cpu_access_unions_the_backends_that_may_run() {
13841 use edgefirst_tensor::CpuAccess::{self, None as No, Read, ReadWrite, Write};
13842 let p = predicted_cpu_access;
13843 let takes = |a: CpuAccess| Some(Some(a));
13844 let declines = Some(None);
13845 assert_eq!(
13847 p(
13848 Some(ForcedBackend::G2d),
13849 None,
13850 takes(ReadWrite),
13851 takes(Write)
13852 ),
13853 ReadWrite
13854 );
13855 assert_eq!(
13856 p(Some(ForcedBackend::G2d), None, declines, takes(Write)),
13857 No
13858 );
13859 assert_eq!(
13860 p(
13861 Some(ForcedBackend::OpenGl),
13862 takes(Write),
13863 None,
13864 takes(ReadWrite)
13865 ),
13866 Write
13867 );
13868 assert_eq!(
13869 p(Some(ForcedBackend::Cpu), takes(No), None, takes(Write)),
13870 Write
13871 );
13872 assert_eq!(p(None, takes(No), takes(ReadWrite), takes(ReadWrite)), No);
13874 assert_eq!(p(None, takes(Write), None, takes(ReadWrite)), ReadWrite);
13876 assert_eq!(p(None, declines, takes(No), takes(Write)), Write);
13878 assert_eq!(p(None, declines, takes(ReadWrite), None), ReadWrite);
13879 assert_eq!(p(None, None, None, takes(Read)), Read);
13880 assert_eq!(p(None, None, None, None), No);
13881 }
13882
13883 #[cfg(all(target_os = "linux", feature = "opengl"))]
13887 #[test]
13888 fn float_destinations_gl_declines_are_predicted_for_the_cpu() {
13889 use edgefirst_tensor::CpuAccess;
13890 let Ok(processor) = ImageProcessor::new() else {
13891 eprintln!("WARNING: SKIPPED: float prediction - no image processor");
13892 return;
13893 };
13894 let Some(gl) = processor.opengl.as_ref() else {
13895 eprintln!("WARNING: SKIPPED: float prediction - no OpenGL backend");
13896 return;
13897 };
13898 assert_eq!(
13899 gl.dst_cpu_access(TensorMemory::Mem, PixelFormat::PlanarRgb, DType::F16),
13900 None
13901 );
13902 assert_eq!(
13903 gl.dst_cpu_access(TensorMemory::DmaBuf, PixelFormat::Grey, DType::F32),
13904 None
13905 );
13906 let image = processor
13907 .create_image(
13908 64,
13909 64,
13910 PixelFormat::Rgb,
13911 DType::F32,
13912 Some(TensorMemory::Mem),
13913 CpuAccess::None,
13914 )
13915 .unwrap();
13916 assert!(image.cpu_access().writes());
13917 }
13918
13919 #[test]
13922 fn a_refused_cpu_fallback_reports_the_gpu_decline() {
13923 use edgefirst_tensor::{CpuAccess, Error as TensorError};
13924 let denied = || {
13925 Error::Tensor(TensorError::CpuAccessDenied {
13926 declared: CpuAccess::None,
13927 requested: CpuAccess::Write,
13928 })
13929 };
13930 let declined = || {
13931 vec![(
13932 "opengl",
13933 Error::NotSupported("unsupported source format".into()),
13934 )]
13935 };
13936 let e = with_gpu_declines(denied(), declined());
13937 assert!(
13938 matches!(
13939 &e,
13940 Error::CpuFallbackDenied {
13941 denied: TensorError::CpuAccessDenied { .. },
13942 ..
13943 }
13944 ),
13945 "{e:?}"
13946 );
13947 let msg = e.to_string();
13948 assert!(msg.contains("unsupported source format"), "{msg}");
13949 assert!(msg.contains("CPU access denied"), "{msg}");
13950 assert!(e.cpu_access_denied().is_some());
13951 assert!(matches!(
13952 with_gpu_declines(denied(), Vec::new()),
13953 Error::Tensor(TensorError::CpuAccessDenied { .. })
13954 ));
13955 assert!(matches!(
13956 with_gpu_declines(Error::NoConverter, declined()),
13957 Error::NoConverter
13958 ));
13959 }
13960
13961 #[test]
13965 fn create_image_adds_write_when_the_cpu_backend_fills_it() {
13966 use edgefirst_tensor::CpuAccess;
13967 let _lock = acquire_env_lock();
13968 let _guard = EnvGuard::snapshot(&[
13969 "EDGEFIRST_FORCE_BACKEND",
13970 "EDGEFIRST_DISABLE_GL",
13971 "EDGEFIRST_DISABLE_G2D",
13972 "EDGEFIRST_DISABLE_CPU",
13973 ]);
13974 unsafe {
13975 std::env::remove_var("EDGEFIRST_FORCE_BACKEND");
13976 std::env::remove_var("EDGEFIRST_DISABLE_CPU");
13977 std::env::set_var("EDGEFIRST_DISABLE_GL", "1");
13978 std::env::set_var("EDGEFIRST_DISABLE_G2D", "1");
13979 }
13980 let mut proc = ImageProcessor::new().unwrap();
13981 let src = access_test_src(128, 96);
13982 for (requested, declared) in [
13983 (CpuAccess::Read, CpuAccess::ReadWrite),
13984 (CpuAccess::None, CpuAccess::Write),
13985 (CpuAccess::Write, CpuAccess::Write),
13986 (CpuAccess::ReadWrite, CpuAccess::ReadWrite),
13987 ] {
13988 let mut dst = proc
13989 .create_image(64, 48, PixelFormat::Rgb, DType::U8, None, requested)
13990 .unwrap();
13991 assert_eq!(dst.cpu_access(), declared, "requested {requested:?}");
13992 proc.convert(&src, &mut dst, None)
13993 .unwrap_or_else(|e| panic!("CPU convert into a {requested:?} request: {e}"));
13994 }
13995 }
13996
13997 #[test]
14000 fn cpu_convert_refuses_tensors_outside_their_declaration() {
14001 use edgefirst_tensor::{CpuAccess, Error as TensorError};
14002 let mut cpu = CPUProcessor::new();
14003 let src = access_test_src(32, 24);
14004 for declared in [CpuAccess::Read, CpuAccess::None] {
14005 let mut dst = TensorDyn::image(
14006 32,
14007 24,
14008 PixelFormat::Rgb,
14009 DType::U8,
14010 Some(TensorMemory::Mem),
14011 declared,
14012 )
14013 .unwrap();
14014 match cpu.convert(&src, &mut dst, None) {
14015 Err(Error::Tensor(TensorError::CpuAccessDenied {
14016 declared: d,
14017 requested: CpuAccess::Write,
14018 })) => assert_eq!(d, declared),
14019 other => panic!("convert into a {declared:?} dst: {other:?}"),
14020 }
14021 }
14022 let write_only = TensorDyn::image(
14023 32,
14024 24,
14025 PixelFormat::Rgba,
14026 DType::U8,
14027 Some(TensorMemory::Mem),
14028 CpuAccess::Write,
14029 )
14030 .unwrap();
14031 let mut dst = TensorDyn::image(
14032 32,
14033 24,
14034 PixelFormat::Rgb,
14035 DType::U8,
14036 Some(TensorMemory::Mem),
14037 CpuAccess::ReadWrite,
14038 )
14039 .unwrap();
14040 let err = cpu.convert(&write_only, &mut dst, None).unwrap_err();
14041 assert!(
14042 matches!(
14043 err,
14044 Error::Tensor(TensorError::CpuAccessDenied {
14045 declared: CpuAccess::Write,
14046 requested: CpuAccess::Read,
14047 })
14048 ),
14049 "{err:?}"
14050 );
14051 assert!(err
14052 .to_string()
14053 .contains("declare CpuAccess::ReadWrite (the default)"));
14054 }
14055
14056 #[test]
14059 fn cpu_draw_requires_readwrite_on_the_destination() {
14060 use edgefirst_tensor::{CpuAccess, Error as TensorError};
14061 let mut cpu = CPUProcessor::new();
14062 let mut dst = TensorDyn::image(
14063 32,
14064 24,
14065 PixelFormat::Rgba,
14066 DType::U8,
14067 Some(TensorMemory::Mem),
14068 CpuAccess::Write,
14069 )
14070 .unwrap();
14071 let err = cpu
14072 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::default())
14073 .unwrap_err();
14074 assert!(
14075 matches!(
14076 err,
14077 Error::Tensor(TensorError::CpuAccessDenied {
14078 declared: CpuAccess::Write,
14079 requested: CpuAccess::ReadWrite,
14080 })
14081 ),
14082 "{err:?}"
14083 );
14084 }
14085
14086 #[test]
14091 fn create_image_desc_negotiates_and_counts_fallbacks() {
14092 use edgefirst_tensor::{CpuAccess, ImageDesc};
14093 let proc = ImageProcessor::new().unwrap();
14094
14095 let desc =
14096 ImageDesc::new(64, 64, PixelFormat::Rgba, DType::U8).with_access(CpuAccess::ReadWrite);
14097 let plain = proc.create_image_desc(&desc).unwrap();
14098 let classic = proc
14099 .create_image(
14100 64,
14101 64,
14102 PixelFormat::Rgba,
14103 DType::U8,
14104 None,
14105 CpuAccess::ReadWrite,
14106 )
14107 .unwrap();
14108 assert_eq!(plain.memory(), classic.memory(), "same negotiation path");
14109 assert_eq!(plain.compression(), None);
14110
14111 #[cfg(not(target_os = "android"))]
14112 {
14113 use edgefirst_tensor::Compression;
14114 let before = proc.compression_fallback_count();
14115 let desc = ImageDesc::new(64, 64, PixelFormat::Rgba, DType::U8)
14116 .with_access(CpuAccess::None)
14117 .with_compression(Compression::Any);
14118 let t = proc.create_image_desc(&desc).unwrap();
14119 assert_eq!(t.compression(), None, "no vendor tile scheme off-Android");
14120 assert!(
14121 proc.compression_fallback_count() > before,
14122 "Any resolving linear must count"
14123 );
14124 }
14125 }
14126
14127 #[test]
14130 #[cfg(target_os = "linux")]
14131 fn create_image_desc_contiguous_is_cma_dma_or_an_error() {
14132 use edgefirst_tensor::{Contiguity, ImageDesc, TensorMemory};
14133 let proc = ImageProcessor::new().unwrap();
14134 let desc = ImageDesc::new(64, 64, PixelFormat::Rgba, DType::U8).with_contiguous(true);
14135 match proc.create_image_desc(&desc) {
14136 Ok(t) => {
14137 assert_eq!(t.memory(), TensorMemory::DmaBuf);
14138 assert_eq!(t.contiguity(), Contiguity::Contiguous);
14139 }
14140 Err(e) => {
14141 assert!(e.to_string().contains("linux,cma"), "{e}");
14142 crate::test_support::report_skip(&format!(
14143 "{} - no CMA heap, the contiguous DMA-BUF arm did not run: {e}",
14144 function!()
14145 ));
14146 }
14147 }
14148 }
14149
14150 #[test]
14153 #[cfg(target_os = "linux")]
14154 fn create_image_f32_dma_rejected() {
14155 let proc = ImageProcessor::new().unwrap();
14156 let result = proc.create_image(
14157 64,
14158 64,
14159 PixelFormat::Rgb,
14160 DType::F32,
14161 Some(TensorMemory::DmaBuf),
14162 edgefirst_tensor::CpuAccess::ReadWrite,
14163 );
14164 assert!(
14165 result.is_err(),
14166 "create_image(F32, Dma) must fail — no DRM fourcc for f32"
14167 );
14168 }
14169
14170 #[test]
14179 #[cfg(target_os = "linux")]
14180 fn import_image_carries_colorimetry() {
14181 use edgefirst_tensor::{ColorEncoding, ColorRange, Colorimetry, TensorMemory};
14182
14183 let expected = Colorimetry::default()
14184 .with_encoding(ColorEncoding::Bt709)
14185 .with_range(ColorRange::Limited);
14186
14187 if !is_dma_available() {
14188 let mut t = TensorDyn::image(
14191 8,
14192 8,
14193 PixelFormat::Rgba,
14194 DType::U8,
14195 Some(TensorMemory::Mem),
14196 edgefirst_tensor::CpuAccess::ReadWrite,
14197 )
14198 .expect("alloc");
14199 assert_eq!(t.colorimetry(), None, "colorimetry must start as None");
14200 t.set_colorimetry(Some(expected));
14201 assert_eq!(
14202 t.colorimetry(),
14203 Some(expected),
14204 "set_colorimetry must round-trip"
14205 );
14206 crate::test_support::report_skip("import_image_carries_colorimetry (DMA unavailable); storage contract verified via TensorDyn");
14207 return;
14208 }
14209
14210 use edgefirst_tensor::{PlaneDescriptor, Tensor};
14213
14214 let rgba_bytes = 64 * 64 * 4; let dma_tensor = Tensor::<u8>::new(
14216 &[rgba_bytes],
14217 Some(TensorMemory::DmaBuf),
14218 Some("import_test"),
14219 )
14220 .expect("dma alloc");
14221 let pd =
14222 PlaneDescriptor::new(dma_tensor.dmabuf().expect("dma fd")).expect("PlaneDescriptor");
14223
14224 let proc = ImageProcessor::new().expect("ImageProcessor");
14225 let result = proc.import_image(
14226 pd,
14227 None,
14228 64,
14229 64,
14230 PixelFormat::Rgba,
14231 DType::U8,
14232 Some(expected),
14233 );
14234 let tensor = result.expect("import_image must succeed on DMA fd");
14235 assert_eq!(
14236 tensor.colorimetry(),
14237 Some(expected),
14238 "import_image must store the supplied colorimetry on the returned TensorDyn"
14239 );
14240 }
14241}