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