1#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
76
77#[cfg(all(coverage, target_os = "linux"))]
81#[used]
82#[link_section = ".init_array"]
83static __EDGEFIRST_COV_INSTALL: extern "C" fn() = {
84 extern "C" fn ctor() {
85 edgefirst_tensor::covguard::install();
86 }
87 ctor
88};
89
90pub const GPU_DMA_BUF_PITCH_ALIGNMENT_BYTES: usize = 64;
103
104pub fn align_width_for_gpu_pitch(width: usize, bpp: usize) -> usize {
142 if bpp == 0 || width == 0 {
143 return width;
144 }
145
146 let Some(lcm_alignment) = checked_num_integer_lcm(GPU_DMA_BUF_PITCH_ALIGNMENT_BYTES, bpp)
155 else {
156 log::warn!(
157 "align_width_for_gpu_pitch: lcm({GPU_DMA_BUF_PITCH_ALIGNMENT_BYTES}, {bpp}) \
158 overflows usize, returning unaligned width {width}"
159 );
160 return width;
161 };
162 if lcm_alignment == 0 {
163 return width;
164 }
165
166 debug_assert_eq!(lcm_alignment % bpp, 0);
167 let width_alignment = lcm_alignment / bpp;
168 if width_alignment == 0 {
169 return width;
170 }
171
172 let remainder = width % width_alignment;
173 if remainder == 0 {
174 return width;
175 }
176
177 let pad = width_alignment - remainder;
178 match width.checked_add(pad) {
179 Some(aligned) => aligned,
180 None => {
181 log::warn!(
182 "align_width_for_gpu_pitch: width {width} + pad {pad} overflows usize, \
183 returning unaligned (caller should use a smaller width or pre-aligned size)"
184 );
185 width
186 }
187 }
188}
189
190#[cfg(target_os = "linux")]
199pub(crate) fn align_pitch_bytes_to_gpu_alignment(min_pitch_bytes: usize) -> Option<usize> {
200 let alignment = GPU_DMA_BUF_PITCH_ALIGNMENT_BYTES;
201 if min_pitch_bytes == 0 {
202 return Some(0);
203 }
204 let remainder = min_pitch_bytes % alignment;
205 if remainder == 0 {
206 return Some(min_pitch_bytes);
207 }
208 min_pitch_bytes.checked_add(alignment - remainder)
209}
210
211fn checked_num_integer_lcm(a: usize, b: usize) -> Option<usize> {
214 if a == 0 || b == 0 {
215 return Some(0);
216 }
217 let g = num_integer_gcd(a, b);
218 (a / g).checked_mul(b)
221}
222
223fn num_integer_gcd(a: usize, b: usize) -> usize {
224 if b == 0 {
225 a
226 } else {
227 num_integer_gcd(b, a % b)
228 }
229}
230
231pub fn primary_plane_bpp(format: PixelFormat, elem: usize) -> Option<usize> {
247 use edgefirst_tensor::PixelLayout;
248 match format.layout() {
249 PixelLayout::Packed => Some(format.channels() * elem),
250 PixelLayout::Planar => Some(elem),
251 PixelLayout::SemiPlanar => Some(elem),
255 _ => None,
258 }
259}
260
261#[cfg(all(target_os = "linux", test))]
274pub(crate) fn padded_dma_pitch_for(
275 fmt: PixelFormat,
276 width: usize,
277 memory: &Option<TensorMemory>,
278) -> Option<usize> {
279 match memory {
289 Some(TensorMemory::Dma) => {}
290 None if edgefirst_tensor::is_dma_available() => {}
291 _ => return None,
292 }
293 if fmt.layout() != PixelLayout::Packed {
297 return None;
298 }
299 let bpp = primary_plane_bpp(fmt, 1)?;
300 let natural = width.checked_mul(bpp)?;
301 let aligned = align_pitch_bytes_to_gpu_alignment(natural)?;
302 if aligned > natural {
303 Some(aligned)
304 } else {
305 None
306 }
307}
308
309pub use cpu::CPUProcessor;
310pub use edgefirst_codec as codec;
311
312#[cfg(test)]
313use edgefirst_decoder::ProtoLayout;
314use edgefirst_decoder::{DetectBox, ProtoData, Segmentation};
315#[doc(inline)]
316pub use edgefirst_tensor::Region;
317#[cfg(any(test, all(target_os = "linux", feature = "opengl")))]
318use edgefirst_tensor::Tensor;
319use edgefirst_tensor::{
320 DType, PixelFormat, PixelLayout, TensorDyn, TensorMemory, TensorTrait as _,
321};
322use enum_dispatch::enum_dispatch;
323pub use error::{Error, Result};
324#[cfg(target_os = "linux")]
325pub use g2d::G2DProcessor;
326#[cfg(all(
327 any(
328 target_os = "linux",
329 target_os = "macos",
330 target_os = "ios",
331 target_os = "android"
332 ),
333 feature = "opengl"
334))]
335pub use opengl_headless::EglDisplayKind;
336#[cfg(all(
337 any(
338 target_os = "linux",
339 target_os = "macos",
340 target_os = "ios",
341 target_os = "android"
342 ),
343 feature = "opengl"
344))]
345pub use opengl_headless::GLProcessorThreaded;
346#[cfg(all(
347 any(
348 target_os = "linux",
349 target_os = "macos",
350 target_os = "ios",
351 target_os = "android"
352 ),
353 feature = "opengl"
354))]
355pub use opengl_headless::Int8InterpolationMode;
356#[cfg(target_os = "linux")]
357#[cfg(feature = "opengl")]
358pub use opengl_headless::{probe_egl_displays, EglDisplayInfo};
359#[cfg(all(
363 any(
364 target_os = "linux",
365 target_os = "macos",
366 target_os = "ios",
367 target_os = "android"
368 ),
369 feature = "opengl"
370))]
371pub use opengl_headless::{CacheStats, ConvertStats, GlCacheStats};
372use std::{fmt::Display, time::Instant};
373
374mod colorimetry;
375mod cpu;
376mod error;
377mod g2d;
378#[path = "gl/mod.rs"]
379mod opengl_headless;
380
381#[derive(Debug, Clone, Copy, PartialEq, Eq)]
385pub enum Rotation {
386 None = 0,
387 Clockwise90 = 1,
388 Rotate180 = 2,
389 CounterClockwise90 = 3,
390}
391impl Rotation {
392 pub fn from_degrees_clockwise(angle: usize) -> Rotation {
405 match angle.rem_euclid(360) {
406 0 => Rotation::None,
407 90 => Rotation::Clockwise90,
408 180 => Rotation::Rotate180,
409 270 => Rotation::CounterClockwise90,
410 _ => panic!("rotation angle is not a multiple of 90"),
411 }
412 }
413}
414
415#[derive(Debug, Clone, Copy, PartialEq, Eq)]
416pub enum Flip {
417 None = 0,
418 Vertical = 1,
419 Horizontal = 2,
420}
421
422#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
424pub enum ColorMode {
425 #[default]
430 Class,
431 Instance,
436 Track,
439}
440
441impl ColorMode {
442 #[inline]
444 pub fn index(self, idx: usize, label: usize) -> usize {
445 match self {
446 ColorMode::Class => label,
447 ColorMode::Instance | ColorMode::Track => idx,
448 }
449 }
450}
451
452#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
473pub enum MaskResolution {
474 #[default]
476 Proto,
477 Scaled {
481 width: u32,
483 height: u32,
485 },
486}
487
488#[derive(Debug, Clone, Copy)]
504pub struct MaskOverlay<'a> {
505 pub background: Option<&'a TensorDyn>,
509 pub opacity: f32,
510 pub letterbox: Option<[f32; 4]>,
520 pub color_mode: ColorMode,
521}
522
523impl Default for MaskOverlay<'_> {
524 fn default() -> Self {
525 Self {
526 background: None,
527 opacity: 1.0,
528 letterbox: None,
529 color_mode: ColorMode::Class,
530 }
531 }
532}
533
534impl<'a> MaskOverlay<'a> {
535 pub fn new() -> Self {
536 Self::default()
537 }
538
539 pub fn with_background(mut self, bg: &'a TensorDyn) -> Self {
547 self.background = Some(bg);
548 self
549 }
550
551 pub fn with_opacity(mut self, opacity: f32) -> Self {
552 self.opacity = opacity.clamp(0.0, 1.0);
553 self
554 }
555
556 pub fn with_color_mode(mut self, mode: ColorMode) -> Self {
557 self.color_mode = mode;
558 self
559 }
560
561 pub fn with_letterbox_crop(
571 mut self,
572 crop: &Crop,
573 src_w: usize,
574 src_h: usize,
575 model_w: usize,
576 model_h: usize,
577 ) -> Self {
578 if let Ok(resolved) = crop.resolve(src_w, src_h, model_w, model_h) {
581 if let Some(r) = resolved.dst_rect {
582 self.letterbox = Some([
583 r.left as f32 / model_w as f32,
584 r.top as f32 / model_h as f32,
585 (r.left + r.width) as f32 / model_w as f32,
586 (r.top + r.height) as f32 / model_h as f32,
587 ]);
588 }
589 }
590 self
591 }
592}
593
594#[inline]
603fn unletter_bbox(bbox: DetectBox, lb: [f32; 4]) -> DetectBox {
604 let b = bbox.bbox.to_canonical();
605 let [lx0, ly0, lx1, ly1] = lb;
606 let inv_w = if lx1 > lx0 { 1.0 / (lx1 - lx0) } else { 1.0 };
607 let inv_h = if ly1 > ly0 { 1.0 / (ly1 - ly0) } else { 1.0 };
608 DetectBox {
609 bbox: edgefirst_decoder::BoundingBox {
610 xmin: ((b.xmin - lx0) * inv_w).clamp(0.0, 1.0),
611 ymin: ((b.ymin - ly0) * inv_h).clamp(0.0, 1.0),
612 xmax: ((b.xmax - lx0) * inv_w).clamp(0.0, 1.0),
613 ymax: ((b.ymax - ly0) * inv_h).clamp(0.0, 1.0),
614 },
615 ..bbox
616 }
617}
618
619#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
621pub enum Fit {
622 #[default]
624 Stretch,
625 Letterbox { pad: [u8; 4] },
629}
630
631#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
636pub struct Crop {
637 pub source: Option<Region>,
639 pub fit: Fit,
641}
642
643impl Crop {
644 pub fn new() -> Self {
646 Self::default()
647 }
648
649 pub fn no_crop() -> Self {
651 Self::default()
652 }
653
654 pub fn letterbox(pad: [u8; 4]) -> Self {
657 Self {
658 source: None,
659 fit: Fit::Letterbox { pad },
660 }
661 }
662
663 pub fn with_source(mut self, source: Option<Region>) -> Self {
665 self.source = source;
666 self
667 }
668
669 pub fn with_fit(mut self, fit: Fit) -> Self {
671 self.fit = fit;
672 self
673 }
674
675 pub(crate) fn resolve(
681 &self,
682 src_w: usize,
683 src_h: usize,
684 dst_w: usize,
685 dst_h: usize,
686 ) -> Result<ResolvedCrop, Error> {
687 let src_rect = self.source.map(region_to_rect);
688 let (sw, sh) = match self.source {
691 Some(r) => (r.width, r.height),
692 None => (src_w, src_h),
693 };
694 let resolved = match self.fit {
695 Fit::Stretch => ResolvedCrop {
696 src_rect,
697 dst_rect: None,
698 dst_color: None,
699 },
700 Fit::Letterbox { pad } => ResolvedCrop {
701 src_rect,
702 dst_rect: Some(letterbox_rect(sw, sh, dst_w, dst_h)),
703 dst_color: Some(pad),
704 },
705 };
706 resolved.check_crop_dims(src_w, src_h, dst_w, dst_h)?;
707 Ok(resolved)
708 }
709
710 pub fn check_crop_dyn(
712 &self,
713 src: &edgefirst_tensor::TensorDyn,
714 dst: &edgefirst_tensor::TensorDyn,
715 ) -> Result<(), Error> {
716 self.resolve(
717 src.width().unwrap_or(0),
718 src.height().unwrap_or(0),
719 dst.width().unwrap_or(0),
720 dst.height().unwrap_or(0),
721 )
722 .map(|_| ())
723 }
724}
725
726#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
730pub(crate) struct ResolvedCrop {
731 pub(crate) src_rect: Option<Rect>,
732 pub(crate) dst_rect: Option<Rect>,
733 pub(crate) dst_color: Option<[u8; 4]>,
734}
735
736impl ResolvedCrop {
737 #[allow(dead_code)] pub(crate) fn no_crop() -> Self {
740 Self::default()
741 }
742
743 pub(crate) fn check_crop_dims(
745 &self,
746 src_w: usize,
747 src_h: usize,
748 dst_w: usize,
749 dst_h: usize,
750 ) -> Result<(), Error> {
751 let src_ok = self
752 .src_rect
753 .is_none_or(|r| r.left + r.width <= src_w && r.top + r.height <= src_h);
754 let dst_ok = self
755 .dst_rect
756 .is_none_or(|r| r.left + r.width <= dst_w && r.top + r.height <= dst_h);
757 match (src_ok, dst_ok) {
758 (true, true) => Ok(()),
759 (true, false) => Err(Error::CropInvalid(format!(
760 "Dest crop invalid: {:?}",
761 self.dst_rect
762 ))),
763 (false, true) => Err(Error::CropInvalid(format!(
764 "Src crop invalid: {:?}",
765 self.src_rect
766 ))),
767 (false, false) => Err(Error::CropInvalid(format!(
768 "Dest and Src crop invalid: {:?} {:?}",
769 self.dst_rect, self.src_rect
770 ))),
771 }
772 }
773}
774
775fn region_to_rect(r: Region) -> Rect {
777 Rect {
778 left: r.x,
779 top: r.y,
780 width: r.width,
781 height: r.height,
782 }
783}
784
785fn letterbox_rect(sw: usize, sh: usize, dw: usize, dh: usize) -> Rect {
789 if sw == 0 || sh == 0 {
790 return Rect::new(0, 0, dw, dh);
791 }
792 let src_aspect = sw as f64 / sh as f64;
793 let dst_aspect = dw as f64 / dh as f64;
794 let (new_w, new_h) = if src_aspect > dst_aspect {
795 (dw, ((dw as f64 / src_aspect).round() as usize).max(1))
796 } else {
797 (((dh as f64 * src_aspect).round() as usize).max(1), dh)
798 };
799 let left = dw.saturating_sub(new_w) / 2;
800 let top = dh.saturating_sub(new_h) / 2;
801 Rect::new(left, top, new_w, new_h)
802}
803
804#[derive(Debug, Clone, Copy, PartialEq, Eq)]
809pub(crate) struct Rect {
810 pub left: usize,
811 pub top: usize,
812 pub width: usize,
813 pub height: usize,
814}
815
816impl Rect {
817 pub fn new(left: usize, top: usize, width: usize, height: usize) -> Self {
819 Self {
820 left,
821 top,
822 width,
823 height,
824 }
825 }
826}
827
828#[enum_dispatch(ImageProcessor)]
829pub trait ImageProcessorTrait {
830 fn convert(
846 &mut self,
847 src: &TensorDyn,
848 dst: &mut TensorDyn,
849 rotation: Rotation,
850 flip: Flip,
851 crop: Crop,
852 ) -> Result<()>;
853
854 fn draw_decoded_masks(
911 &mut self,
912 dst: &mut TensorDyn,
913 detect: &[DetectBox],
914 segmentation: &[Segmentation],
915 overlay: MaskOverlay<'_>,
916 ) -> Result<()>;
917
918 fn draw_proto_masks(
938 &mut self,
939 dst: &mut TensorDyn,
940 detect: &[DetectBox],
941 proto_data: &ProtoData,
942 overlay: MaskOverlay<'_>,
943 ) -> Result<()>;
944
945 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()>;
948
949 fn convert_deferred(
966 &mut self,
967 src: &TensorDyn,
968 dst: &mut TensorDyn,
969 rotation: Rotation,
970 flip: Flip,
971 crop: Crop,
972 ) -> Result<()> {
973 self.convert(src, dst, rotation, flip, crop)
974 }
975
976 fn flush(&mut self) -> Result<()> {
983 Ok(())
984 }
985}
986
987#[derive(Debug, Clone, Default)]
993pub struct ImageProcessorConfig {
994 #[cfg(all(
1004 any(
1005 target_os = "linux",
1006 target_os = "macos",
1007 target_os = "ios",
1008 target_os = "android"
1009 ),
1010 feature = "opengl"
1011 ))]
1012 pub egl_display: Option<EglDisplayKind>,
1013
1014 pub backend: ComputeBackend,
1026
1027 pub colorimetry: ColorimetryMode,
1032}
1033
1034#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1049pub enum ColorimetryMode {
1050 #[default]
1054 Fast,
1055 Exact,
1058}
1059
1060#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1067pub enum ComputeBackend {
1068 #[default]
1070 Auto,
1071 Cpu,
1073 G2d,
1075 OpenGl,
1077}
1078
1079#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1085pub(crate) enum ForcedBackend {
1086 Cpu,
1087 G2d,
1088 OpenGl,
1089}
1090
1091#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1112pub struct RenderDtypeSupport {
1113 pub f32: bool,
1118 pub f16: bool,
1124}
1125
1126#[cfg(all(target_os = "linux", feature = "opengl"))]
1140pub(crate) fn float_pbo_eligible(dtype: DType, support: RenderDtypeSupport) -> bool {
1141 match dtype {
1142 DType::F16 => support.f16,
1143 DType::F32 => support.f32,
1144 _ => false,
1145 }
1146}
1147
1148#[derive(Debug)]
1151pub struct ImageProcessor {
1152 pub cpu: Option<CPUProcessor>,
1155
1156 #[cfg(target_os = "linux")]
1157 pub g2d: Option<G2DProcessor>,
1161 #[cfg(target_os = "linux")]
1162 #[cfg(feature = "opengl")]
1163 pub opengl: Option<GLProcessorThreaded>,
1167 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
1168 #[cfg(feature = "opengl")]
1169 pub opengl: Option<GLProcessorThreaded>,
1176
1177 pub(crate) forced_backend: Option<ForcedBackend>,
1179
1180 pub(crate) convert_fallbacks: std::sync::atomic::AtomicU64,
1186}
1187
1188unsafe impl Send for ImageProcessor {}
1189unsafe impl Sync for ImageProcessor {}
1190
1191impl ImageProcessor {
1192 pub fn new() -> Result<Self> {
1218 Self::with_config(ImageProcessorConfig::default())
1219 }
1220
1221 pub fn convert_fallback_count(&self) -> u64 {
1228 self.convert_fallbacks
1229 .load(std::sync::atomic::Ordering::Relaxed)
1230 }
1231
1232 pub fn compression_fallback_count(&self) -> u64 {
1241 edgefirst_tensor::compression_fallback_count()
1242 }
1243
1244 #[cfg(unix)]
1258 pub fn convert_with_fence(
1259 &mut self,
1260 src: &TensorDyn,
1261 dst: &mut TensorDyn,
1262 rotation: Rotation,
1263 flip: Flip,
1264 crop: Crop,
1265 ) -> Result<Option<std::os::fd::OwnedFd>> {
1266 #[cfg(any(
1267 target_os = "linux",
1268 target_os = "macos",
1269 target_os = "ios",
1270 target_os = "android"
1271 ))]
1272 #[cfg(feature = "opengl")]
1273 {
1274 let gl_forced = matches!(self.forced_backend, Some(ForcedBackend::OpenGl));
1275 if self.forced_backend.is_none() || gl_forced {
1276 if let Some(opengl) = self.opengl.as_mut() {
1277 match opengl.convert_with_fence(src, dst, rotation, flip, crop) {
1278 Ok(fd) => return Ok(fd),
1279 Err(e) if gl_forced => return Err(e),
1280 Err(e) => {
1281 log::debug!(
1285 "convert_with_fence: opengl declined, \
1286 falling back to the blocking chain: {e}"
1287 );
1288 }
1289 }
1290 } else if gl_forced {
1291 return Err(Error::ForcedBackendUnavailable("opengl".into()));
1292 }
1293 }
1294 }
1295 self.convert(src, dst, rotation, flip, crop)?;
1298 Ok(None)
1299 }
1300
1301 pub fn supported_render_dtypes(&self) -> RenderDtypeSupport {
1314 #[cfg(all(
1315 any(target_os = "macos", target_os = "ios", target_os = "android"),
1316 feature = "opengl"
1317 ))]
1318 if let Some(gl) = self.opengl.as_ref() {
1319 return gl.supported_render_dtypes();
1320 }
1321 #[cfg(all(target_os = "linux", feature = "opengl"))]
1322 if let Some(gl) = self.opengl.as_ref() {
1323 return gl.supported_render_dtypes();
1324 }
1325 RenderDtypeSupport {
1326 f32: false,
1327 f16: false,
1328 }
1329 }
1330
1331 #[allow(unused_variables)]
1340 pub fn with_config(config: ImageProcessorConfig) -> Result<Self> {
1341 match config.backend {
1345 ComputeBackend::Cpu => {
1346 log::info!("ComputeBackend::Cpu — CPU only");
1347 return Ok(Self {
1348 cpu: Some(CPUProcessor::new()),
1349 #[cfg(target_os = "linux")]
1350 g2d: None,
1351 #[cfg(target_os = "linux")]
1352 #[cfg(feature = "opengl")]
1353 opengl: None,
1354 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
1355 #[cfg(feature = "opengl")]
1356 opengl: None,
1357 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1358 forced_backend: None,
1359 });
1360 }
1361 ComputeBackend::G2d => {
1362 log::info!("ComputeBackend::G2d — G2D + CPU fallback");
1363 #[cfg(target_os = "linux")]
1364 {
1365 let g2d = match G2DProcessor::new() {
1366 Ok(g) => Some(g),
1367 Err(e) => {
1368 log::warn!("G2D requested but failed to initialize: {e:?}");
1369 None
1370 }
1371 };
1372 return Ok(Self {
1373 cpu: Some(CPUProcessor::new()),
1374 g2d,
1375 #[cfg(feature = "opengl")]
1376 opengl: None,
1377 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1378 forced_backend: None,
1379 });
1380 }
1381 #[cfg(not(target_os = "linux"))]
1382 {
1383 log::warn!("G2D requested but not available on this platform, using CPU");
1384 return Ok(Self {
1385 cpu: Some(CPUProcessor::new()),
1386 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
1387 #[cfg(feature = "opengl")]
1388 opengl: None,
1389 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1390 forced_backend: None,
1391 });
1392 }
1393 }
1394 ComputeBackend::OpenGl => {
1395 log::info!("ComputeBackend::OpenGl — OpenGL + CPU fallback");
1396 #[cfg(target_os = "linux")]
1397 {
1398 #[cfg(feature = "opengl")]
1399 let opengl = match GLProcessorThreaded::new(config.egl_display) {
1400 Ok(gl) => Some(gl),
1401 Err(e) => {
1402 log::warn!("OpenGL requested but failed to initialize: {e:?}");
1403 None
1404 }
1405 };
1406 return Ok(Self {
1407 cpu: Some(CPUProcessor::new()),
1408 g2d: None,
1409 #[cfg(feature = "opengl")]
1410 opengl,
1411 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1412 forced_backend: None,
1413 }
1414 .apply_colorimetry_mode(config.colorimetry));
1415 }
1416 #[cfg(any(target_os = "macos", target_os = "ios"))]
1417 {
1418 #[cfg(feature = "opengl")]
1419 let opengl = match GLProcessorThreaded::new(config.egl_display) {
1420 Ok(gl) => Some(gl),
1421 Err(e) => {
1422 log::warn!(
1423 "OpenGL requested on macOS but ANGLE init failed: {e:?} \
1424 (install ANGLE via `brew install startergo/angle/angle` \
1425 and re-sign the dylibs — see README.md § macOS GPU \
1426 Acceleration). Falling back to CPU."
1427 );
1428 None
1429 }
1430 };
1431 return Ok(Self {
1432 cpu: Some(CPUProcessor::new()),
1433 #[cfg(feature = "opengl")]
1434 opengl,
1435 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1436 forced_backend: None,
1437 }
1438 .apply_colorimetry_mode(config.colorimetry));
1439 }
1440 #[cfg(target_os = "android")]
1441 {
1442 #[cfg(feature = "opengl")]
1443 let opengl = match GLProcessorThreaded::new(config.egl_display) {
1444 Ok(gl) => Some(gl),
1445 Err(e) => {
1446 log::warn!(
1447 "OpenGL requested but native EGL init failed: {e:?}. \
1448 Falling back to CPU."
1449 );
1450 None
1451 }
1452 };
1453 return Ok(Self {
1454 cpu: Some(CPUProcessor::new()),
1455 #[cfg(feature = "opengl")]
1456 opengl,
1457 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1458 forced_backend: None,
1459 }
1460 .apply_colorimetry_mode(config.colorimetry));
1461 }
1462 #[cfg(not(any(
1463 target_os = "linux",
1464 target_os = "macos",
1465 target_os = "ios",
1466 target_os = "android"
1467 )))]
1468 {
1469 log::warn!("OpenGL requested but not available on this platform, using CPU");
1470 return Ok(Self {
1471 cpu: Some(CPUProcessor::new()),
1472 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1473 forced_backend: None,
1474 });
1475 }
1476 }
1477 ComputeBackend::Auto => { }
1478 }
1479
1480 if let Ok(val) = std::env::var("EDGEFIRST_FORCE_BACKEND") {
1485 let val_lower = val.to_lowercase();
1486 let forced = match val_lower.as_str() {
1487 "cpu" => ForcedBackend::Cpu,
1488 "g2d" => ForcedBackend::G2d,
1489 "opengl" => ForcedBackend::OpenGl,
1490 other => {
1491 return Err(Error::ForcedBackendUnavailable(format!(
1492 "unknown EDGEFIRST_FORCE_BACKEND value: {other:?} (expected cpu, g2d, or opengl)"
1493 )));
1494 }
1495 };
1496
1497 log::info!("EDGEFIRST_FORCE_BACKEND={val} — only initializing {val_lower} backend");
1498
1499 return match forced {
1500 ForcedBackend::Cpu => Ok(Self {
1501 cpu: Some(CPUProcessor::new()),
1502 #[cfg(target_os = "linux")]
1503 g2d: None,
1504 #[cfg(target_os = "linux")]
1505 #[cfg(feature = "opengl")]
1506 opengl: None,
1507 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
1508 #[cfg(feature = "opengl")]
1509 opengl: None,
1510 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1511 forced_backend: Some(ForcedBackend::Cpu),
1512 }),
1513 ForcedBackend::G2d => {
1514 #[cfg(target_os = "linux")]
1515 {
1516 let g2d = G2DProcessor::new().map_err(|e| {
1517 Error::ForcedBackendUnavailable(format!(
1518 "g2d forced but failed to initialize: {e:?}"
1519 ))
1520 })?;
1521 Ok(Self {
1522 cpu: None,
1523 g2d: Some(g2d),
1524 #[cfg(feature = "opengl")]
1525 opengl: None,
1526 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1527 forced_backend: Some(ForcedBackend::G2d),
1528 })
1529 }
1530 #[cfg(not(target_os = "linux"))]
1531 {
1532 Err(Error::ForcedBackendUnavailable(
1533 "g2d backend is only available on Linux".into(),
1534 ))
1535 }
1536 }
1537 ForcedBackend::OpenGl => {
1538 #[cfg(target_os = "linux")]
1539 #[cfg(feature = "opengl")]
1540 {
1541 let opengl = GLProcessorThreaded::new(config.egl_display).map_err(|e| {
1542 Error::ForcedBackendUnavailable(format!(
1543 "opengl forced but failed to initialize: {e:?}"
1544 ))
1545 })?;
1546 Ok(Self {
1547 cpu: None,
1548 g2d: None,
1549 opengl: Some(opengl),
1550 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1551 forced_backend: Some(ForcedBackend::OpenGl),
1552 }
1553 .apply_colorimetry_mode(config.colorimetry))
1554 }
1555 #[cfg(any(target_os = "macos", target_os = "ios"))]
1556 #[cfg(feature = "opengl")]
1557 {
1558 let opengl = GLProcessorThreaded::new(config.egl_display).map_err(|e| {
1559 Error::ForcedBackendUnavailable(format!(
1560 "opengl forced on macOS but ANGLE init failed: {e:?}"
1561 ))
1562 })?;
1563 Ok(Self {
1564 cpu: None,
1565 opengl: Some(opengl),
1566 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1567 forced_backend: Some(ForcedBackend::OpenGl),
1568 }
1569 .apply_colorimetry_mode(config.colorimetry))
1570 }
1571 #[cfg(target_os = "android")]
1572 #[cfg(feature = "opengl")]
1573 {
1574 let opengl = GLProcessorThreaded::new(config.egl_display).map_err(|e| {
1575 Error::ForcedBackendUnavailable(format!(
1576 "opengl forced but native EGL init failed: {e:?}"
1577 ))
1578 })?;
1579 Ok(Self {
1580 cpu: None,
1581 opengl: Some(opengl),
1582 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1583 forced_backend: Some(ForcedBackend::OpenGl),
1584 }
1585 .apply_colorimetry_mode(config.colorimetry))
1586 }
1587 #[cfg(not(all(
1588 any(
1589 target_os = "linux",
1590 target_os = "macos",
1591 target_os = "ios",
1592 target_os = "android"
1593 ),
1594 feature = "opengl"
1595 )))]
1596 {
1597 Err(Error::ForcedBackendUnavailable(
1598 "opengl backend requires Linux or macOS with the 'opengl' feature \
1599 enabled"
1600 .into(),
1601 ))
1602 }
1603 }
1604 };
1605 }
1606
1607 #[cfg(target_os = "linux")]
1609 let g2d = if std::env::var("EDGEFIRST_DISABLE_G2D")
1610 .map(|x| x != "0" && x.to_lowercase() != "false")
1611 .unwrap_or(false)
1612 {
1613 log::debug!("EDGEFIRST_DISABLE_G2D is set");
1614 None
1615 } else {
1616 match G2DProcessor::new() {
1617 Ok(g2d_converter) => Some(g2d_converter),
1618 Err(err) => {
1619 log::warn!("Failed to initialize G2D converter: {err:?}");
1620 None
1621 }
1622 }
1623 };
1624
1625 #[cfg(target_os = "linux")]
1626 #[cfg(feature = "opengl")]
1627 let opengl = if std::env::var("EDGEFIRST_DISABLE_GL")
1628 .map(|x| x != "0" && x.to_lowercase() != "false")
1629 .unwrap_or(false)
1630 {
1631 log::debug!("EDGEFIRST_DISABLE_GL is set");
1632 None
1633 } else {
1634 match GLProcessorThreaded::new(config.egl_display) {
1635 Ok(gl_converter) => Some(gl_converter),
1636 Err(err) => {
1637 log::warn!("Failed to initialize GL converter: {err:?}");
1638 None
1639 }
1640 }
1641 };
1642
1643 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
1644 #[cfg(feature = "opengl")]
1645 let opengl = if std::env::var("EDGEFIRST_DISABLE_GL")
1646 .map(|x| x != "0" && x.to_lowercase() != "false")
1647 .unwrap_or(false)
1648 {
1649 log::debug!("EDGEFIRST_DISABLE_GL is set");
1650 None
1651 } else {
1652 match GLProcessorThreaded::new(config.egl_display) {
1653 Ok(gl_converter) => Some(gl_converter),
1654 Err(err) => {
1655 log::debug!(
1656 "GL backend unavailable: {err:?} \
1657 (CPU fallback will be used)"
1658 );
1659 None
1660 }
1661 }
1662 };
1663
1664 let cpu = if std::env::var("EDGEFIRST_DISABLE_CPU")
1665 .map(|x| x != "0" && x.to_lowercase() != "false")
1666 .unwrap_or(false)
1667 {
1668 log::debug!("EDGEFIRST_DISABLE_CPU is set");
1669 None
1670 } else {
1671 Some(CPUProcessor::new())
1672 };
1673 Ok(Self {
1674 cpu,
1675 #[cfg(target_os = "linux")]
1676 g2d,
1677 #[cfg(target_os = "linux")]
1678 #[cfg(feature = "opengl")]
1679 opengl,
1680 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
1681 #[cfg(feature = "opengl")]
1682 opengl,
1683 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1684 forced_backend: None,
1685 }
1686 .apply_colorimetry_mode(config.colorimetry))
1687 }
1688
1689 fn apply_colorimetry_mode(self, _mode: ColorimetryMode) -> Self {
1693 #[cfg(all(
1694 any(
1695 target_os = "linux",
1696 target_os = "macos",
1697 target_os = "ios",
1698 target_os = "android"
1699 ),
1700 feature = "opengl"
1701 ))]
1702 {
1703 let mut me = self;
1704 if let Err(e) = me.set_colorimetry_mode(_mode) {
1705 log::warn!("Failed to apply ColorimetryMode::{_mode:?}: {e:?}");
1706 }
1707 me
1708 }
1709 #[cfg(not(all(
1710 any(
1711 target_os = "linux",
1712 target_os = "macos",
1713 target_os = "ios",
1714 target_os = "android"
1715 ),
1716 feature = "opengl"
1717 )))]
1718 {
1719 let _ = _mode;
1720 self
1721 }
1722 }
1723
1724 #[cfg(all(
1729 any(
1730 target_os = "linux",
1731 target_os = "macos",
1732 target_os = "ios",
1733 target_os = "android"
1734 ),
1735 feature = "opengl"
1736 ))]
1737 pub fn set_colorimetry_mode(&mut self, mode: ColorimetryMode) -> Result<()> {
1738 if let Some(ref mut gl) = self.opengl {
1739 gl.set_colorimetry_mode(mode)?;
1740 }
1741 Ok(())
1742 }
1743
1744 #[cfg(all(
1747 any(
1748 target_os = "linux",
1749 target_os = "macos",
1750 target_os = "ios",
1751 target_os = "android"
1752 ),
1753 feature = "opengl"
1754 ))]
1755 pub fn set_int8_interpolation_mode(&mut self, mode: Int8InterpolationMode) -> Result<()> {
1756 if let Some(ref mut gl) = self.opengl {
1757 gl.set_int8_interpolation_mode(mode)?;
1758 }
1759 Ok(())
1760 }
1761
1762 pub fn create_image_desc(&self, desc: &edgefirst_tensor::ImageDesc) -> Result<TensorDyn> {
1840 if desc.compression().is_none() {
1841 return self.create_image(
1842 desc.width(),
1843 desc.height(),
1844 desc.format(),
1845 desc.dtype(),
1846 desc.memory(),
1847 desc.access(),
1848 );
1849 }
1850 Ok(TensorDyn::image_desc(desc)?)
1851 }
1852
1853 pub fn create_image(
1854 &self,
1855 width: usize,
1856 height: usize,
1857 format: PixelFormat,
1858 dtype: DType,
1859 memory: Option<TensorMemory>,
1860 access: edgefirst_tensor::CpuAccess,
1861 ) -> Result<TensorDyn> {
1862 #[cfg(target_os = "linux")]
1873 let dma_stride_bytes: Option<usize> = primary_plane_bpp(format, dtype.size())
1874 .and_then(|bpp| width.checked_mul(bpp))
1875 .and_then(align_pitch_bytes_to_gpu_alignment);
1876
1877 #[cfg(target_os = "linux")]
1881 let try_dma = || -> Result<TensorDyn> {
1882 let packed = format.layout() == edgefirst_tensor::PixelLayout::Packed;
1890 match dma_stride_bytes {
1891 Some(stride)
1892 if packed
1893 && primary_plane_bpp(format, dtype.size())
1894 .and_then(|bpp| width.checked_mul(bpp))
1895 .is_some_and(|natural| stride > natural) =>
1896 {
1897 log::debug!(
1898 "create_image: padding row stride for {format:?} {width}x{height} \
1899 from natural pitch to {stride} bytes for GPU alignment"
1900 );
1901 Ok(TensorDyn::image_with_stride(
1902 width,
1903 height,
1904 format,
1905 dtype,
1906 stride,
1907 Some(edgefirst_tensor::TensorMemory::Dma),
1908 access,
1909 )?)
1910 }
1911 _ => Ok(TensorDyn::image(
1912 width,
1913 height,
1914 format,
1915 dtype,
1916 Some(edgefirst_tensor::TensorMemory::Dma),
1917 access,
1918 )?),
1919 }
1920 };
1921
1922 match memory {
1929 #[cfg(target_os = "linux")]
1930 Some(TensorMemory::Dma) => {
1931 if dtype == DType::F32 {
1933 return Err(Error::NotSupported(
1934 "F32 has no 32-bit-float DRM format for DMA-BUF; \
1935 use TensorMemory::Pbo for F32"
1936 .to_string(),
1937 ));
1938 }
1939 return try_dma();
1940 }
1941 Some(mem) => {
1942 return Ok(TensorDyn::image(
1943 width,
1944 height,
1945 format,
1946 dtype,
1947 Some(mem),
1948 access,
1949 )?);
1950 }
1951 None => {}
1952 }
1953
1954 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
1961 #[cfg(feature = "opengl")]
1962 if let Some(gl) = self.opengl.as_ref() {
1963 let _ = gl; match TensorDyn::image(
1965 width,
1966 height,
1967 format,
1968 dtype,
1969 Some(edgefirst_tensor::TensorMemory::Dma),
1970 access,
1971 ) {
1972 Ok(img) => return Ok(img),
1973 Err(e) => {
1974 log::debug!(
1978 "create_image: zero-copy Dma allocation declined \
1979 ({format:?}/{dtype:?} {width}x{height}): {e:?}; using fallback storage"
1980 );
1981 }
1982 }
1983 }
1984
1985 #[cfg(target_os = "linux")]
1988 {
1989 #[cfg(feature = "opengl")]
1990 let gl_uses_pbo = self
1991 .opengl
1992 .as_ref()
1993 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
1994 #[cfg(not(feature = "opengl"))]
1995 let gl_uses_pbo = false;
1996
1997 if !gl_uses_pbo {
1998 if let Ok(img) = try_dma() {
1999 return Ok(img);
2000 }
2001 }
2002 }
2003
2004 #[cfg(target_os = "linux")]
2008 #[cfg(feature = "opengl")]
2009 if dtype.size() == 1 {
2010 if let Some(gl) = &self.opengl {
2011 match gl.create_pbo_image(width, height, format) {
2012 Ok(t) => {
2013 if dtype == DType::I8 {
2014 debug_assert!(
2022 t.chroma().is_none(),
2023 "PBO i8 transmute requires chroma == None"
2024 );
2025 let t_i8: Tensor<i8> = unsafe { std::mem::transmute(t) };
2026 return Ok(TensorDyn::from(t_i8));
2027 }
2028 return Ok(TensorDyn::from(t));
2029 }
2030 Err(e) => log::debug!("PBO image creation failed, falling back to Mem: {e:?}"),
2031 }
2032 }
2033 }
2034
2035 #[cfg(target_os = "linux")]
2038 #[cfg(feature = "opengl")]
2039 if float_pbo_eligible(dtype, self.supported_render_dtypes()) {
2040 if let Some(gl) = &self.opengl {
2041 match gl.create_pbo_image_dtype(width, height, format, dtype) {
2042 Ok(t) => return Ok(t),
2043 Err(e) => {
2044 log::debug!(
2045 "Float PBO image creation failed for {dtype:?}, \
2046 falling back to Mem: {e:?}"
2047 );
2048 }
2049 }
2050 }
2051 }
2052
2053 Ok(TensorDyn::image(
2055 width,
2056 height,
2057 format,
2058 dtype,
2059 Some(edgefirst_tensor::TensorMemory::Mem),
2060 access,
2061 )?)
2062 }
2063
2064 #[allow(clippy::too_many_arguments)]
2118 #[cfg(target_os = "linux")]
2119 pub fn import_image(
2120 &self,
2121 image: edgefirst_tensor::PlaneDescriptor,
2122 chroma: Option<edgefirst_tensor::PlaneDescriptor>,
2123 width: usize,
2124 height: usize,
2125 format: PixelFormat,
2126 dtype: DType,
2127 colorimetry: Option<edgefirst_tensor::Colorimetry>,
2128 ) -> Result<TensorDyn> {
2129 use edgefirst_tensor::{Tensor, TensorMemory};
2130
2131 let image_stride = image.stride();
2133 let image_offset = image.offset();
2134 let chroma_stride = chroma.as_ref().and_then(|c| c.stride());
2135 let chroma_offset = chroma.as_ref().and_then(|c| c.offset());
2136
2137 if let Some(chroma_pd) = chroma {
2138 if dtype != DType::U8 && dtype != DType::I8 {
2143 return Err(Error::NotSupported(format!(
2144 "multiplane import only supports U8/I8, got {dtype:?}"
2145 )));
2146 }
2147 if format.layout() != PixelLayout::SemiPlanar {
2148 return Err(Error::NotSupported(format!(
2149 "import_image with chroma requires a semi-planar format, got {format:?}"
2150 )));
2151 }
2152
2153 let chroma_h = match format {
2154 PixelFormat::Nv12 => {
2155 height.div_ceil(2)
2157 }
2158 PixelFormat::Nv16 => {
2161 return Err(Error::NotSupported(
2162 "multiplane NV16 is not yet supported; use contiguous NV16 instead".into(),
2163 ))
2164 }
2165 _ => {
2166 return Err(Error::NotSupported(format!(
2167 "unsupported semi-planar format: {format:?}"
2168 )))
2169 }
2170 };
2171
2172 let luma = Tensor::<u8>::from_fd(image.into_fd(), &[height, width], Some("luma"))?;
2173 if luma.memory() != TensorMemory::Dma {
2174 return Err(Error::NotSupported(format!(
2175 "luma fd must be DMA-backed, got {:?}",
2176 luma.memory()
2177 )));
2178 }
2179
2180 let chroma_tensor =
2181 Tensor::<u8>::from_fd(chroma_pd.into_fd(), &[chroma_h, width], Some("chroma"))?;
2182 if chroma_tensor.memory() != TensorMemory::Dma {
2183 return Err(Error::NotSupported(format!(
2184 "chroma fd must be DMA-backed, got {:?}",
2185 chroma_tensor.memory()
2186 )));
2187 }
2188
2189 let mut tensor = Tensor::<u8>::from_planes(luma, chroma_tensor, format)?;
2192
2193 if let Some(s) = image_stride {
2195 tensor.set_row_stride(s)?;
2196 }
2197 if let Some(o) = image_offset {
2198 tensor.set_plane_offset(o);
2199 }
2200
2201 if let Some(chroma_ref) = tensor.chroma_mut() {
2206 if let Some(s) = chroma_stride {
2207 if s < width {
2208 return Err(Error::InvalidShape(format!(
2209 "chroma stride {s} < minimum {width} for {format:?}"
2210 )));
2211 }
2212 chroma_ref.set_row_stride_unchecked(s);
2213 }
2214 if let Some(o) = chroma_offset {
2215 chroma_ref.set_plane_offset(o);
2216 }
2217 }
2218
2219 if dtype == DType::I8 {
2220 const {
2224 assert!(std::mem::size_of::<Tensor<u8>>() == std::mem::size_of::<Tensor<i8>>());
2225 assert!(
2226 std::mem::align_of::<Tensor<u8>>() == std::mem::align_of::<Tensor<i8>>()
2227 );
2228 }
2229 let tensor_i8: Tensor<i8> = unsafe { std::mem::transmute(tensor) };
2230 let mut dyn_tensor = TensorDyn::from(tensor_i8);
2231 dyn_tensor.set_colorimetry(colorimetry);
2232 return Ok(dyn_tensor);
2233 }
2234 let mut dyn_tensor = TensorDyn::from(tensor);
2235 dyn_tensor.set_colorimetry(colorimetry);
2236 Ok(dyn_tensor)
2237 } else {
2238 let shape = format.image_shape(width, height).ok_or_else(|| {
2243 Error::NotSupported(format!(
2244 "unsupported pixel format for import_image: {format:?}"
2245 ))
2246 })?;
2247 let tensor = TensorDyn::from_fd(image.into_fd(), &shape, dtype, None)?;
2248 if tensor.memory() != TensorMemory::Dma {
2249 return Err(Error::NotSupported(format!(
2250 "import_image requires DMA-backed fd, got {:?}",
2251 tensor.memory()
2252 )));
2253 }
2254 let mut tensor = tensor.with_format(format)?;
2255 if let Some(s) = image_stride {
2256 tensor.set_row_stride(s)?;
2257 }
2258 if let Some(o) = image_offset {
2259 tensor.set_plane_offset(o);
2260 }
2261 tensor.set_colorimetry(colorimetry);
2262 Ok(tensor)
2263 }
2264 }
2265
2266 pub fn draw_masks(
2274 &mut self,
2275 decoder: &edgefirst_decoder::Decoder,
2276 outputs: &[&TensorDyn],
2277 dst: &mut TensorDyn,
2278 overlay: MaskOverlay<'_>,
2279 ) -> Result<Vec<DetectBox>> {
2280 let mut output_boxes = Vec::with_capacity(100);
2281
2282 let proto_result = decoder
2284 .decode_proto(outputs, &mut output_boxes)
2285 .map_err(|e| Error::Internal(format!("decode_proto: {e:#?}")))?;
2286
2287 if let Some(proto_data) = proto_result {
2288 self.draw_proto_masks(dst, &output_boxes, &proto_data, overlay)?;
2289 } else {
2290 let mut output_masks = Vec::with_capacity(100);
2292 decoder
2293 .decode(outputs, &mut output_boxes, &mut output_masks)
2294 .map_err(|e| Error::Internal(format!("decode: {e:#?}")))?;
2295 self.draw_decoded_masks(dst, &output_boxes, &output_masks, overlay)?;
2296 }
2297 Ok(output_boxes)
2298 }
2299
2300 #[cfg(feature = "tracker")]
2308 pub fn draw_masks_tracked<TR: edgefirst_tracker::Tracker<DetectBox>>(
2309 &mut self,
2310 decoder: &edgefirst_decoder::Decoder,
2311 tracker: &mut TR,
2312 timestamp: u64,
2313 outputs: &[&TensorDyn],
2314 dst: &mut TensorDyn,
2315 overlay: MaskOverlay<'_>,
2316 ) -> Result<(Vec<DetectBox>, Vec<edgefirst_tracker::TrackInfo>)> {
2317 let mut output_boxes = Vec::with_capacity(100);
2318 let mut output_tracks = Vec::new();
2319
2320 let proto_result = decoder
2321 .decode_proto_tracked(
2322 tracker,
2323 timestamp,
2324 outputs,
2325 &mut output_boxes,
2326 &mut output_tracks,
2327 )
2328 .map_err(|e| Error::Internal(format!("decode_proto_tracked: {e:#?}")))?;
2329
2330 if let Some(proto_data) = proto_result {
2331 self.draw_proto_masks(dst, &output_boxes, &proto_data, overlay)?;
2332 } else {
2333 let mut output_masks = Vec::with_capacity(100);
2337 decoder
2338 .decode_tracked(
2339 tracker,
2340 timestamp,
2341 outputs,
2342 &mut output_boxes,
2343 &mut output_masks,
2344 &mut output_tracks,
2345 )
2346 .map_err(|e| Error::Internal(format!("decode_tracked: {e:#?}")))?;
2347 self.draw_decoded_masks(dst, &output_boxes, &output_masks, overlay)?;
2348 }
2349 Ok((output_boxes, output_tracks))
2350 }
2351
2352 pub fn materialize_masks(
2376 &mut self,
2377 detect: &[DetectBox],
2378 proto_data: &ProtoData,
2379 letterbox: Option<[f32; 4]>,
2380 resolution: MaskResolution,
2381 ) -> Result<Vec<Segmentation>> {
2382 let cpu = self.cpu.as_mut().ok_or(Error::NoConverter)?;
2383 match resolution {
2384 MaskResolution::Proto => cpu.materialize_segmentations(detect, proto_data, letterbox),
2385 MaskResolution::Scaled { width, height } => {
2386 cpu.materialize_scaled_segmentations(detect, proto_data, letterbox, width, height)
2387 }
2388 }
2389 }
2390}
2391
2392impl ImageProcessorTrait for ImageProcessor {
2393 fn convert(
2399 &mut self,
2400 src: &TensorDyn,
2401 dst: &mut TensorDyn,
2402 rotation: Rotation,
2403 flip: Flip,
2404 crop: Crop,
2405 ) -> Result<()> {
2406 let start = Instant::now();
2407 let src_fmt = src.format();
2408 let dst_fmt = dst.format();
2409 let _span = tracing::trace_span!(
2410 "image.convert",
2411 ?src_fmt,
2412 ?dst_fmt,
2413 src_memory = ?src.memory(),
2414 dst_memory = ?dst.memory(),
2415 ?rotation,
2416 ?flip,
2417 )
2418 .entered();
2419 log::trace!(
2420 "convert: {src_fmt:?}({:?}/{:?}) → {dst_fmt:?}({:?}/{:?}), \
2421 rotation={rotation:?}, flip={flip:?}, backend={:?}",
2422 src.dtype(),
2423 src.memory(),
2424 dst.dtype(),
2425 dst.memory(),
2426 self.forced_backend,
2427 );
2428
2429 if let Some(forced) = self.forced_backend {
2431 return match forced {
2432 ForcedBackend::Cpu => {
2433 if let Some(cpu) = self.cpu.as_mut() {
2434 let r = cpu.convert(src, dst, rotation, flip, crop);
2435 log::trace!(
2436 "convert: forced=cpu result={} ({:?})",
2437 if r.is_ok() { "ok" } else { "err" },
2438 start.elapsed()
2439 );
2440 return r;
2441 }
2442 Err(Error::ForcedBackendUnavailable("cpu".into()))
2443 }
2444 ForcedBackend::G2d => {
2445 #[cfg(target_os = "linux")]
2446 if let Some(g2d) = self.g2d.as_mut() {
2447 let r = g2d.convert(src, dst, rotation, flip, crop);
2448 log::trace!(
2449 "convert: forced=g2d result={} ({:?})",
2450 if r.is_ok() { "ok" } else { "err" },
2451 start.elapsed()
2452 );
2453 return r;
2454 }
2455 Err(Error::ForcedBackendUnavailable("g2d".into()))
2456 }
2457 ForcedBackend::OpenGl => {
2458 #[cfg(any(
2459 target_os = "linux",
2460 target_os = "macos",
2461 target_os = "ios",
2462 target_os = "android"
2463 ))]
2464 #[cfg(feature = "opengl")]
2465 if let Some(opengl) = self.opengl.as_mut() {
2466 let r = opengl.convert(src, dst, rotation, flip, crop);
2467 log::trace!(
2468 "convert: forced=opengl result={} ({:?})",
2469 if r.is_ok() { "ok" } else { "err" },
2470 start.elapsed()
2471 );
2472 return r;
2473 }
2474 Err(Error::ForcedBackendUnavailable("opengl".into()))
2475 }
2476 };
2477 }
2478
2479 #[cfg(any(
2481 target_os = "linux",
2482 target_os = "macos",
2483 target_os = "ios",
2484 target_os = "android"
2485 ))]
2486 #[cfg(feature = "opengl")]
2487 if let Some(opengl) = self.opengl.as_mut() {
2488 match opengl.convert(src, dst, rotation, flip, crop) {
2489 Ok(_) => {
2490 log::trace!(
2491 "convert: auto selected=opengl for {src_fmt:?}→{dst_fmt:?} ({:?})",
2492 start.elapsed()
2493 );
2494 return Ok(());
2495 }
2496 Err(e) => {
2497 self.convert_fallbacks
2498 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2499 log::debug!(
2500 "convert: auto opengl declined {src_fmt:?}@{:?}→{dst_fmt:?}@{:?}, \
2501 falling back toward G2D/CPU: {e}",
2502 src.memory(),
2503 dst.memory(),
2504 );
2505 }
2506 }
2507 }
2508
2509 #[cfg(target_os = "linux")]
2510 if let Some(g2d) = self.g2d.as_mut() {
2511 let src_is_yuv = src.format().is_some_and(|f| f.is_yuv());
2518 let dst_is_yuv = dst.format().is_some_and(|f| f.is_yuv());
2519 let g2d_eligible = if src_is_yuv || dst_is_yuv {
2520 let cm = if src_is_yuv {
2521 crate::colorimetry::effective_colorimetry(src)
2522 } else {
2523 crate::colorimetry::effective_colorimetry(dst)
2524 };
2525 crate::g2d::g2d_can_handle(&cm, true)
2526 } else {
2527 true
2528 };
2529 if !g2d_eligible {
2530 log::trace!(
2531 "convert: auto g2d skipped {src_fmt:?}→{dst_fmt:?} \
2532 (colorimetry not expressible: full-range/BT.2020)"
2533 );
2534 } else {
2535 match g2d.convert(src, dst, rotation, flip, crop) {
2536 Ok(_) => {
2537 log::trace!(
2538 "convert: auto selected=g2d for {src_fmt:?}→{dst_fmt:?} ({:?})",
2539 start.elapsed()
2540 );
2541 return Ok(());
2542 }
2543 Err(e) => {
2544 log::trace!("convert: auto g2d declined {src_fmt:?}→{dst_fmt:?}: {e}");
2545 }
2546 }
2547 }
2548 }
2549
2550 if let Some(cpu) = self.cpu.as_mut() {
2551 match cpu.convert(src, dst, rotation, flip, crop) {
2552 Ok(_) => {
2553 log::trace!(
2554 "convert: auto selected=cpu for {src_fmt:?}→{dst_fmt:?} ({:?})",
2555 start.elapsed()
2556 );
2557 return Ok(());
2558 }
2559 Err(e) => {
2560 log::trace!("convert: auto cpu failed {src_fmt:?}→{dst_fmt:?}: {e}");
2561 return Err(e);
2562 }
2563 }
2564 }
2565 Err(Error::NoConverter)
2566 }
2567
2568 fn convert_deferred(
2569 &mut self,
2570 src: &TensorDyn,
2571 dst: &mut TensorDyn,
2572 rotation: Rotation,
2573 flip: Flip,
2574 crop: Crop,
2575 ) -> Result<()> {
2576 #[cfg(any(
2582 target_os = "linux",
2583 target_os = "macos",
2584 target_os = "ios",
2585 target_os = "android"
2586 ))]
2587 #[cfg(feature = "opengl")]
2588 {
2589 let gl_forced = matches!(self.forced_backend, Some(ForcedBackend::OpenGl));
2590 if gl_forced || self.forced_backend.is_none() {
2591 if let Some(opengl) = self.opengl.as_mut() {
2592 match opengl.convert_deferred(src, dst, rotation, flip, crop) {
2593 Ok(()) => return Ok(()),
2594 Err(e) => {
2595 log::trace!("convert_deferred: gl declined: {e}; eager fallback");
2596 if gl_forced {
2599 return Err(e);
2600 }
2601 }
2602 }
2603 }
2604 }
2605 }
2606 self.convert(src, dst, rotation, flip, crop)
2607 }
2608
2609 fn flush(&mut self) -> Result<()> {
2610 let _span = tracing::trace_span!("image.flush").entered();
2611 #[cfg(any(
2614 target_os = "linux",
2615 target_os = "macos",
2616 target_os = "ios",
2617 target_os = "android"
2618 ))]
2619 #[cfg(feature = "opengl")]
2620 if let Some(opengl) = self.opengl.as_mut() {
2621 return opengl.flush();
2622 }
2623 Ok(())
2624 }
2625
2626 fn draw_decoded_masks(
2627 &mut self,
2628 dst: &mut TensorDyn,
2629 detect: &[DetectBox],
2630 segmentation: &[Segmentation],
2631 overlay: MaskOverlay<'_>,
2632 ) -> Result<()> {
2633 let _span = tracing::trace_span!(
2634 "image.draw_decoded_masks",
2635 n_detections = detect.len(),
2636 n_segmentations = segmentation.len(),
2637 )
2638 .entered();
2639 let start = Instant::now();
2640
2641 if let Some(bg) = overlay.background {
2642 if bg.aliases(dst) {
2643 return Err(Error::AliasedBuffers(
2644 "background must not reference the same buffer as dst".to_string(),
2645 ));
2646 }
2647 }
2648
2649 let lb_boxes: Vec<DetectBox>;
2652 let lb_segs: Vec<Segmentation>;
2653 let (detect, segmentation) = if let Some(lb) = overlay.letterbox {
2654 lb_boxes = detect.iter().map(|&d| unletter_bbox(d, lb)).collect();
2655 lb_segs = if segmentation.len() == lb_boxes.len() {
2658 segmentation
2659 .iter()
2660 .zip(lb_boxes.iter())
2661 .map(|(s, d)| Segmentation {
2662 xmin: d.bbox.xmin,
2663 ymin: d.bbox.ymin,
2664 xmax: d.bbox.xmax,
2665 ymax: d.bbox.ymax,
2666 segmentation: s.segmentation.clone(),
2667 })
2668 .collect()
2669 } else {
2670 segmentation.to_vec()
2671 };
2672 (lb_boxes.as_slice(), lb_segs.as_slice())
2673 } else {
2674 (detect, segmentation)
2675 };
2676 #[cfg(target_os = "linux")]
2677 let is_empty_frame = detect.is_empty() && segmentation.is_empty();
2678
2679 if let Some(forced) = self.forced_backend {
2681 return match forced {
2682 ForcedBackend::Cpu => {
2683 if let Some(cpu) = self.cpu.as_mut() {
2684 return cpu.draw_decoded_masks(dst, detect, segmentation, overlay);
2685 }
2686 Err(Error::ForcedBackendUnavailable("cpu".into()))
2687 }
2688 ForcedBackend::G2d => {
2689 #[cfg(target_os = "linux")]
2692 if let Some(g2d) = self.g2d.as_mut() {
2693 return g2d.draw_decoded_masks(dst, detect, segmentation, overlay);
2694 }
2695 Err(Error::ForcedBackendUnavailable("g2d".into()))
2696 }
2697 ForcedBackend::OpenGl => {
2698 #[cfg(target_os = "linux")]
2701 #[cfg(feature = "opengl")]
2702 if let Some(opengl) = self.opengl.as_mut() {
2703 return opengl.draw_decoded_masks(dst, detect, segmentation, overlay);
2704 }
2705 Err(Error::ForcedBackendUnavailable("opengl".into()))
2706 }
2707 };
2708 }
2709
2710 #[cfg(target_os = "linux")]
2716 if is_empty_frame {
2717 if let Some(g2d) = self.g2d.as_mut() {
2718 match g2d.draw_decoded_masks(dst, detect, segmentation, overlay) {
2719 Ok(_) => {
2720 log::trace!(
2721 "draw_decoded_masks empty frame via g2d in {:?}",
2722 start.elapsed()
2723 );
2724 return Ok(());
2725 }
2726 Err(e) => log::trace!("g2d empty-frame path unavailable: {e:?}"),
2727 }
2728 }
2729 }
2730
2731 #[cfg(target_os = "linux")]
2735 #[cfg(feature = "opengl")]
2736 if let Some(opengl) = self.opengl.as_mut() {
2737 log::trace!(
2738 "draw_decoded_masks started with opengl in {:?}",
2739 start.elapsed()
2740 );
2741 match opengl.draw_decoded_masks(dst, detect, segmentation, overlay) {
2742 Ok(_) => {
2743 log::trace!("draw_decoded_masks with opengl in {:?}", start.elapsed());
2744 return Ok(());
2745 }
2746 Err(e) => {
2747 log::trace!("draw_decoded_masks didn't work with opengl: {e:?}")
2748 }
2749 }
2750 }
2751
2752 log::trace!(
2753 "draw_decoded_masks started with cpu in {:?}",
2754 start.elapsed()
2755 );
2756 if let Some(cpu) = self.cpu.as_mut() {
2757 match cpu.draw_decoded_masks(dst, detect, segmentation, overlay) {
2758 Ok(_) => {
2759 log::trace!("draw_decoded_masks with cpu in {:?}", start.elapsed());
2760 return Ok(());
2761 }
2762 Err(e) => {
2763 log::trace!("draw_decoded_masks didn't work with cpu: {e:?}");
2764 return Err(e);
2765 }
2766 }
2767 }
2768 Err(Error::NoConverter)
2769 }
2770
2771 fn draw_proto_masks(
2772 &mut self,
2773 dst: &mut TensorDyn,
2774 detect: &[DetectBox],
2775 proto_data: &ProtoData,
2776 overlay: MaskOverlay<'_>,
2777 ) -> Result<()> {
2778 let start = Instant::now();
2779
2780 if let Some(bg) = overlay.background {
2781 if bg.aliases(dst) {
2782 return Err(Error::AliasedBuffers(
2783 "background must not reference the same buffer as dst".to_string(),
2784 ));
2785 }
2786 }
2787
2788 let lb_boxes: Vec<DetectBox>;
2794 let render_detect = if let Some(lb) = overlay.letterbox {
2795 lb_boxes = detect.iter().map(|&d| unletter_bbox(d, lb)).collect();
2796 lb_boxes.as_slice()
2797 } else {
2798 detect
2799 };
2800 #[cfg(target_os = "linux")]
2801 let is_empty_frame = detect.is_empty();
2802
2803 if let Some(forced) = self.forced_backend {
2805 return match forced {
2806 ForcedBackend::Cpu => {
2807 if let Some(cpu) = self.cpu.as_mut() {
2808 return cpu.draw_proto_masks(dst, render_detect, proto_data, overlay);
2809 }
2810 Err(Error::ForcedBackendUnavailable("cpu".into()))
2811 }
2812 ForcedBackend::G2d => {
2813 #[cfg(target_os = "linux")]
2814 if let Some(g2d) = self.g2d.as_mut() {
2815 return g2d.draw_proto_masks(dst, render_detect, proto_data, overlay);
2816 }
2817 Err(Error::ForcedBackendUnavailable("g2d".into()))
2818 }
2819 ForcedBackend::OpenGl => {
2820 #[cfg(target_os = "linux")]
2821 #[cfg(feature = "opengl")]
2822 if let Some(opengl) = self.opengl.as_mut() {
2823 return opengl.draw_proto_masks(dst, render_detect, proto_data, overlay);
2824 }
2825 Err(Error::ForcedBackendUnavailable("opengl".into()))
2826 }
2827 };
2828 }
2829
2830 #[cfg(target_os = "linux")]
2833 if is_empty_frame {
2834 if let Some(g2d) = self.g2d.as_mut() {
2835 match g2d.draw_proto_masks(dst, render_detect, proto_data, overlay) {
2836 Ok(_) => {
2837 log::trace!(
2838 "draw_proto_masks empty frame via g2d in {:?}",
2839 start.elapsed()
2840 );
2841 return Ok(());
2842 }
2843 Err(e) => log::trace!("g2d empty-frame path unavailable: {e:?}"),
2844 }
2845 }
2846 }
2847
2848 #[cfg(target_os = "linux")]
2857 #[cfg(feature = "opengl")]
2858 if let (Some(_), Some(_)) = (self.cpu.as_ref(), self.opengl.as_ref()) {
2859 let segmentation = match self.cpu.as_mut() {
2860 Some(cpu) => {
2861 log::trace!(
2862 "draw_proto_masks started with hybrid (cpu+opengl) in {:?}",
2863 start.elapsed()
2864 );
2865 cpu.materialize_segmentations(detect, proto_data, overlay.letterbox)?
2866 }
2867 None => unreachable!("cpu presence checked above"),
2868 };
2869 if let Some(opengl) = self.opengl.as_mut() {
2870 match opengl.draw_decoded_masks(dst, render_detect, &segmentation, overlay) {
2871 Ok(_) => {
2872 log::trace!(
2873 "draw_proto_masks with hybrid (cpu+opengl) in {:?}",
2874 start.elapsed()
2875 );
2876 return Ok(());
2877 }
2878 Err(e) => {
2879 log::trace!(
2880 "draw_proto_masks hybrid path failed, falling back to cpu: {e:?}"
2881 );
2882 }
2883 }
2884 }
2885 }
2886
2887 let Some(cpu) = self.cpu.as_mut() else {
2888 return Err(Error::Internal(
2889 "draw_proto_masks requires CPU backend for fallback path".into(),
2890 ));
2891 };
2892 log::trace!("draw_proto_masks started with cpu in {:?}", start.elapsed());
2893 cpu.draw_proto_masks(dst, render_detect, proto_data, overlay)
2894 }
2895
2896 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()> {
2897 let start = Instant::now();
2898
2899 if let Some(forced) = self.forced_backend {
2901 return match forced {
2902 ForcedBackend::Cpu => {
2903 if let Some(cpu) = self.cpu.as_mut() {
2904 return cpu.set_class_colors(colors);
2905 }
2906 Err(Error::ForcedBackendUnavailable("cpu".into()))
2907 }
2908 ForcedBackend::G2d => Err(Error::NotSupported(
2909 "g2d does not support set_class_colors".into(),
2910 )),
2911 ForcedBackend::OpenGl => {
2912 #[cfg(target_os = "linux")]
2913 #[cfg(feature = "opengl")]
2914 if let Some(opengl) = self.opengl.as_mut() {
2915 return opengl.set_class_colors(colors);
2916 }
2917 Err(Error::ForcedBackendUnavailable("opengl".into()))
2918 }
2919 };
2920 }
2921
2922 #[cfg(target_os = "linux")]
2925 #[cfg(feature = "opengl")]
2926 if let Some(opengl) = self.opengl.as_mut() {
2927 log::trace!("image started with opengl in {:?}", start.elapsed());
2928 match opengl.set_class_colors(colors) {
2929 Ok(_) => {
2930 log::trace!("colors set with opengl in {:?}", start.elapsed());
2931 return Ok(());
2932 }
2933 Err(e) => {
2934 log::trace!("colors didn't set with opengl: {e:?}")
2935 }
2936 }
2937 }
2938 log::trace!("image started with cpu in {:?}", start.elapsed());
2939 if let Some(cpu) = self.cpu.as_mut() {
2940 match cpu.set_class_colors(colors) {
2941 Ok(_) => {
2942 log::trace!("colors set with cpu in {:?}", start.elapsed());
2943 return Ok(());
2944 }
2945 Err(e) => {
2946 log::trace!("colors didn't set with cpu: {e:?}");
2947 return Err(e);
2948 }
2949 }
2950 }
2951 Err(Error::NoConverter)
2952 }
2953}
2954
2955#[cfg(test)]
2965pub(crate) fn load_image_test_helper(
2966 image: &[u8],
2967 format: Option<PixelFormat>,
2968 memory: Option<TensorMemory>,
2969) -> Result<TensorDyn> {
2970 use edgefirst_codec::{peek_info, ImageDecoder, ImageLoad};
2971
2972 let info = peek_info(image)?;
2976 let native_fmt = info.format;
2977 let w = info.width;
2978 let h = info.height;
2979
2980 let mut decoder = ImageDecoder::new();
2981
2982 #[cfg(target_os = "linux")]
2985 let native_src = {
2986 if let Some(aligned_pitch) = padded_dma_pitch_for(native_fmt, w, &memory) {
2987 let mut dma = Tensor::<u8>::image_with_stride(
2988 w,
2989 h,
2990 native_fmt,
2991 aligned_pitch,
2992 Some(TensorMemory::Dma),
2993 edgefirst_tensor::CpuAccess::ReadWrite,
2994 )?;
2995 dma.load_image(&mut decoder, image)?;
2996 TensorDyn::from(dma)
2997 } else {
2998 let mut img = Tensor::<u8>::image(
2999 w,
3000 h,
3001 native_fmt,
3002 memory,
3003 edgefirst_tensor::CpuAccess::ReadWrite,
3004 )?;
3005 img.load_image(&mut decoder, image)?;
3006 TensorDyn::from(img)
3007 }
3008 };
3009 #[cfg(not(target_os = "linux"))]
3010 let native_src = {
3011 let mut img = Tensor::<u8>::image(
3012 w,
3013 h,
3014 native_fmt,
3015 memory,
3016 edgefirst_tensor::CpuAccess::ReadWrite,
3017 )?;
3018 img.load_image(&mut decoder, image)?;
3019 TensorDyn::from(img)
3020 };
3021
3022 match format {
3026 Some(f) if f != native_fmt => {
3027 let mut dst = TensorDyn::image(
3028 w,
3029 h,
3030 f,
3031 DType::U8,
3032 memory,
3033 edgefirst_tensor::CpuAccess::ReadWrite,
3034 )?;
3035 #[allow(clippy::needless_update)]
3042 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
3043 backend: ComputeBackend::Cpu,
3044 ..Default::default()
3045 })?;
3046 proc.convert(
3047 &native_src,
3048 &mut dst,
3049 Rotation::None,
3050 Flip::None,
3051 Crop::default(),
3052 )?;
3053 Ok(dst)
3054 }
3055 _ => Ok(native_src),
3056 }
3057}
3058
3059pub fn save_jpeg(tensor: &TensorDyn, path: impl AsRef<std::path::Path>, quality: u8) -> Result<()> {
3063 let t = tensor.as_u8().ok_or(Error::UnsupportedFormat(
3064 "save_jpeg requires u8 tensor".to_string(),
3065 ))?;
3066 let fmt = t.format().ok_or(Error::NotAnImage)?;
3067 if fmt.layout() != PixelLayout::Packed {
3068 return Err(Error::NotImplemented(
3069 "Saving planar images is not supported".to_string(),
3070 ));
3071 }
3072
3073 let colour = match fmt {
3074 PixelFormat::Rgb => jpeg_encoder::ColorType::Rgb,
3075 PixelFormat::Rgba => jpeg_encoder::ColorType::Rgba,
3076 _ => {
3077 return Err(Error::NotImplemented(
3078 "Unsupported image format for saving".to_string(),
3079 ));
3080 }
3081 };
3082
3083 let w = t.width().ok_or(Error::NotAnImage)?;
3084 let h = t.height().ok_or(Error::NotAnImage)?;
3085 let encoder = jpeg_encoder::Encoder::new_file(path, quality)?;
3086 let tensor_map = t.map_read()?;
3087
3088 encoder.encode(&tensor_map, w as u16, h as u16, colour)?;
3089
3090 Ok(())
3091}
3092
3093pub(crate) struct FunctionTimer<T: Display> {
3094 name: T,
3095 start: std::time::Instant,
3096}
3097
3098impl<T: Display> FunctionTimer<T> {
3099 pub fn new(name: T) -> Self {
3100 Self {
3101 name,
3102 start: std::time::Instant::now(),
3103 }
3104 }
3105}
3106
3107impl<T: Display> Drop for FunctionTimer<T> {
3108 fn drop(&mut self) {
3109 log::trace!("{} elapsed: {:?}", self.name, self.start.elapsed())
3110 }
3111}
3112
3113const DEFAULT_COLORS: [[f32; 4]; 20] = [
3114 [0., 1., 0., 0.7],
3115 [1., 0.5568628, 0., 0.7],
3116 [0.25882353, 0.15294118, 0.13333333, 0.7],
3117 [0.8, 0.7647059, 0.78039216, 0.7],
3118 [0.3137255, 0.3137255, 0.3137255, 0.7],
3119 [0.1411765, 0.3098039, 0.1215686, 0.7],
3120 [1., 0.95686275, 0.5137255, 0.7],
3121 [0.3529412, 0.32156863, 0., 0.7],
3122 [0.4235294, 0.6235294, 0.6509804, 0.7],
3123 [0.5098039, 0.5098039, 0.7294118, 0.7],
3124 [0.00784314, 0.18823529, 0.29411765, 0.7],
3125 [0.0, 0.2706, 1.0, 0.7],
3126 [0.0, 0.0, 0.0, 0.7],
3127 [0.0, 0.5, 0.0, 0.7],
3128 [1.0, 0.0, 0.0, 0.7],
3129 [0.0, 0.0, 1.0, 0.7],
3130 [1.0, 0.5, 0.5, 0.7],
3131 [0.1333, 0.5451, 0.1333, 0.7],
3132 [0.1176, 0.4118, 0.8235, 0.7],
3133 [1., 1., 1., 0.7],
3134];
3135
3136const fn denorm<const M: usize, const N: usize>(a: [[f32; M]; N]) -> [[u8; M]; N] {
3137 let mut result = [[0; M]; N];
3138 let mut i = 0;
3139 while i < N {
3140 let mut j = 0;
3141 while j < M {
3142 result[i][j] = (a[i][j] * 255.0).round() as u8;
3143 j += 1;
3144 }
3145 i += 1;
3146 }
3147 result
3148}
3149
3150const DEFAULT_COLORS_U8: [[u8; 4]; 20] = denorm(DEFAULT_COLORS);
3151
3152#[cfg(test)]
3153#[cfg_attr(coverage_nightly, coverage(off))]
3154mod alignment_tests {
3155 use super::*;
3156
3157 #[test]
3158 fn align_width_rgba8_common_widths() {
3159 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); }
3170
3171 #[test]
3172 fn align_width_rgb888_packed() {
3173 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] {
3180 let padded = align_width_for_gpu_pitch(w, 3);
3181 assert!(padded >= w);
3182 assert_eq!((padded * 3) % 64, 0);
3183 assert_eq!((padded * 3) % 3, 0);
3184 }
3185 }
3186
3187 #[test]
3188 fn align_width_grey_u8() {
3189 assert_eq!(align_width_for_gpu_pitch(64, 1), 64);
3191 assert_eq!(align_width_for_gpu_pitch(640, 1), 640);
3192 assert_eq!(align_width_for_gpu_pitch(1, 1), 64);
3193 assert_eq!(align_width_for_gpu_pitch(65, 1), 128);
3194 }
3195
3196 #[test]
3197 fn align_width_zero_inputs() {
3198 assert_eq!(align_width_for_gpu_pitch(0, 4), 0);
3199 assert_eq!(align_width_for_gpu_pitch(640, 0), 640);
3200 }
3201
3202 #[test]
3203 fn align_width_never_returns_smaller_than_input() {
3204 for &bpp in &[1usize, 2, 3, 4, 8] {
3208 for &w in &[
3209 1usize,
3210 17,
3211 64,
3212 65,
3213 100,
3214 1280,
3215 1281,
3216 1920,
3217 3004,
3218 3072,
3219 3840,
3220 usize::MAX / 8,
3221 usize::MAX / 4,
3222 usize::MAX / 2,
3223 usize::MAX - 1,
3224 usize::MAX,
3225 ] {
3226 let aligned = align_width_for_gpu_pitch(w, bpp);
3227 assert!(
3228 aligned >= w,
3229 "align_width_for_gpu_pitch({w}, {bpp}) = {aligned} < {w}"
3230 );
3231 }
3232 }
3233 }
3234
3235 #[test]
3236 fn align_width_overflow_returns_unaligned_not_smaller() {
3237 let aligned_extreme = usize::MAX - 15; assert_eq!(
3243 align_width_for_gpu_pitch(aligned_extreme, 4),
3244 aligned_extreme
3245 );
3246 let misaligned_extreme = usize::MAX - 1;
3249 let result = align_width_for_gpu_pitch(misaligned_extreme, 4);
3250 assert!(
3251 result == misaligned_extreme || result >= misaligned_extreme,
3252 "extreme misaligned width must not be rounded down to {result}"
3253 );
3254 }
3255
3256 #[test]
3257 fn checked_lcm_basic_and_overflow() {
3258 assert_eq!(checked_num_integer_lcm(64, 4), Some(64));
3259 assert_eq!(checked_num_integer_lcm(64, 3), Some(192));
3260 assert_eq!(checked_num_integer_lcm(64, 1), Some(64));
3261 assert_eq!(checked_num_integer_lcm(0, 4), Some(0));
3262 assert_eq!(checked_num_integer_lcm(64, 0), Some(0));
3263 assert_eq!(
3265 checked_num_integer_lcm(usize::MAX, usize::MAX - 1),
3266 None,
3267 "coprime extreme values must overflow detect, not panic"
3268 );
3269 }
3270
3271 #[test]
3272 fn primary_plane_bpp_known_formats() {
3273 assert_eq!(primary_plane_bpp(PixelFormat::Rgba, 1), Some(4));
3275 assert_eq!(primary_plane_bpp(PixelFormat::Bgra, 1), Some(4));
3276 assert_eq!(primary_plane_bpp(PixelFormat::Rgb, 1), Some(3));
3277 assert_eq!(primary_plane_bpp(PixelFormat::Grey, 1), Some(1));
3278 assert_eq!(primary_plane_bpp(PixelFormat::Nv12, 1), Some(1));
3280 }
3281}
3282
3283#[cfg(test)]
3284#[cfg_attr(coverage_nightly, coverage(off))]
3285#[allow(deprecated)]
3286mod image_tests {
3287 use super::*;
3288 use crate::{CPUProcessor, Rotation};
3289 #[cfg(target_os = "linux")]
3290 use edgefirst_tensor::is_dma_available;
3291 use edgefirst_tensor::{TensorMapTrait, TensorMemory, TensorTrait};
3292 use image::buffer::ConvertBuffer;
3293
3294 fn convert_img(
3300 proc: &mut dyn ImageProcessorTrait,
3301 src: TensorDyn,
3302 dst: TensorDyn,
3303 rotation: Rotation,
3304 flip: Flip,
3305 crop: Crop,
3306 ) -> (Result<()>, TensorDyn, TensorDyn) {
3307 let src_fourcc = src.format().unwrap();
3308 let dst_fourcc = dst.format().unwrap();
3309 let src_dyn = src;
3310 let mut dst_dyn = dst;
3311 let result = proc.convert(&src_dyn, &mut dst_dyn, rotation, flip, crop);
3312 let src_back = {
3313 let mut __t = src_dyn.into_u8().unwrap();
3314 __t.set_format(src_fourcc).unwrap();
3315 TensorDyn::from(__t)
3316 };
3317 let dst_back = {
3318 let mut __t = dst_dyn.into_u8().unwrap();
3319 __t.set_format(dst_fourcc).unwrap();
3320 TensorDyn::from(__t)
3321 };
3322 (result, src_back, dst_back)
3323 }
3324
3325 #[ctor::ctor(unsafe)]
3326 fn init() {
3327 env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
3328 }
3329
3330 macro_rules! function {
3331 () => {{
3332 fn f() {}
3333 fn type_name_of<T>(_: T) -> &'static str {
3334 std::any::type_name::<T>()
3335 }
3336 let name = type_name_of(f);
3337
3338 match &name[..name.len() - 3].rfind(':') {
3340 Some(pos) => &name[pos + 1..name.len() - 3],
3341 None => &name[..name.len() - 3],
3342 }
3343 }};
3344 }
3345
3346 #[test]
3359 fn batch_view_dst_tiles_match_standalone() {
3360 let mut proc = match ImageProcessor::new() {
3361 Ok(p) => p,
3362 Err(e) => {
3363 eprintln!(
3364 "SKIPPED: {} — ImageProcessor init failed ({e:?})",
3365 function!()
3366 );
3367 return;
3368 }
3369 };
3370 let n = 3usize;
3371 let (w, h) = (32usize, 24usize);
3372 let colors: [[u8; 4]; 3] = [[210, 40, 40, 255], [40, 210, 40, 255], [40, 40, 210, 255]];
3373 let make_src = |c: [u8; 4]| -> TensorDyn {
3374 let bytes: Vec<u8> = c.iter().copied().cycle().take(w * h * 4).collect();
3375 load_bytes_to_tensor(w, h, PixelFormat::Rgba, Some(TensorMemory::Mem), &bytes).unwrap()
3376 };
3377 let parent = match TensorDyn::image(
3380 w,
3381 n * h,
3382 PixelFormat::Rgba,
3383 DType::U8,
3384 Some(TensorMemory::Dma),
3385 edgefirst_tensor::CpuAccess::ReadWrite,
3386 ) {
3387 Ok(d) => d,
3388 Err(e) => {
3389 eprintln!(
3390 "SKIPPED: {} — tall DMA destination alloc failed ({e:?})",
3391 function!()
3392 );
3393 return;
3394 }
3395 };
3396
3397 for (i, &c) in colors.iter().enumerate().take(n) {
3399 let mut tile = parent.view(Region::new(0, i * h, w, h)).unwrap();
3400 proc.convert_deferred(
3401 &make_src(c),
3402 &mut tile,
3403 Rotation::None,
3404 Flip::None,
3405 Crop::no_crop(),
3406 )
3407 .unwrap_or_else(|e| panic!("convert_deferred tile {i}: {e:?}"));
3408 }
3409 proc.flush().unwrap();
3410
3411 for (i, &c) in colors.iter().enumerate().take(n) {
3412 let mut solo = TensorDyn::image(
3414 w,
3415 h,
3416 PixelFormat::Rgba,
3417 DType::U8,
3418 Some(TensorMemory::Dma),
3419 edgefirst_tensor::CpuAccess::ReadWrite,
3420 )
3421 .unwrap();
3422 proc.convert(
3423 &make_src(c),
3424 &mut solo,
3425 Rotation::None,
3426 Flip::None,
3427 Crop::no_crop(),
3428 )
3429 .unwrap();
3430
3431 let band = parent.view(Region::new(0, i * h, w, h)).unwrap();
3432 let band_bytes = band.as_u8().unwrap().map().unwrap().as_slice().to_vec();
3433 let solo_bytes = solo.as_u8().unwrap().map().unwrap().as_slice().to_vec();
3434 assert_eq!(
3435 band_bytes, solo_bytes,
3436 "tile {i}: band differs from standalone convert (placement or sibling wipe)"
3437 );
3438 assert!(
3439 band_bytes.chunks_exact(4).all(|p| p == c),
3440 "tile {i}: band is not the expected solid color {c:?} (sibling wipe?)"
3441 );
3442 }
3443 }
3444
3445 #[test]
3446 fn test_invalid_crop() {
3447 let src = TensorDyn::image(
3448 100,
3449 100,
3450 PixelFormat::Rgb,
3451 DType::U8,
3452 None,
3453 edgefirst_tensor::CpuAccess::ReadWrite,
3454 )
3455 .unwrap();
3456 let dst = TensorDyn::image(
3457 100,
3458 100,
3459 PixelFormat::Rgb,
3460 DType::U8,
3461 None,
3462 edgefirst_tensor::CpuAccess::ReadWrite,
3463 )
3464 .unwrap();
3465
3466 let crop = Crop::new().with_source(Some(Region::new(50, 50, 60, 60)));
3468 assert!(matches!(
3469 crop.check_crop_dyn(&src, &dst),
3470 Err(Error::CropInvalid(_))
3471 ));
3472
3473 let crop = Crop::new().with_source(Some(Region::new(0, 0, 10, 10)));
3475 assert!(crop.check_crop_dyn(&src, &dst).is_ok());
3476
3477 assert!(Crop::letterbox([0, 0, 0, 255])
3479 .check_crop_dyn(&src, &dst)
3480 .is_ok());
3481 }
3482
3483 #[test]
3484 fn test_invalid_tensor_format() -> Result<(), Error> {
3485 let mut tensor = Tensor::<u8>::new(&[720, 1280, 4, 1], None, None)?;
3487 let result = tensor.set_format(PixelFormat::Rgb);
3488 assert!(result.is_err(), "4D tensor should reject set_format");
3489
3490 let mut tensor = Tensor::<u8>::new(&[720, 1280, 4], None, None)?;
3492 let result = tensor.set_format(PixelFormat::Rgb);
3493 assert!(result.is_err(), "4-channel tensor should reject RGB format");
3494
3495 Ok(())
3496 }
3497
3498 #[test]
3499 fn test_invalid_image_file() -> Result<(), Error> {
3500 let result = crate::load_image_test_helper(&[123; 5000], None, None);
3501 assert!(
3502 matches!(result, Err(Error::Codec(_))),
3503 "unrecognised bytes should surface as Error::Codec, got {result:?}"
3504 );
3505 Ok(())
3506 }
3507
3508 #[test]
3509 fn test_invalid_jpeg_format() -> Result<(), Error> {
3510 let result = crate::load_image_test_helper(&[123; 5000], Some(PixelFormat::Yuyv), None);
3511 assert!(
3514 matches!(result, Err(Error::Codec(_))),
3515 "Yuyv target with garbage bytes should surface as Error::Codec, got {result:?}"
3516 );
3517 Ok(())
3518 }
3519
3520 #[test]
3521 fn test_load_resize_save() {
3522 let file = edgefirst_bench::testdata::read("zidane.jpg");
3523 let img = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
3524 assert_eq!(img.width(), Some(1280));
3525 assert_eq!(img.height(), Some(720));
3526
3527 let dst = TensorDyn::image(
3528 640,
3529 360,
3530 PixelFormat::Rgba,
3531 DType::U8,
3532 None,
3533 edgefirst_tensor::CpuAccess::ReadWrite,
3534 )
3535 .unwrap();
3536 let mut converter = CPUProcessor::new();
3537 let (result, _img, dst) = convert_img(
3538 &mut converter,
3539 img,
3540 dst,
3541 Rotation::None,
3542 Flip::None,
3543 Crop::no_crop(),
3544 );
3545 result.unwrap();
3546 assert_eq!(dst.width(), Some(640));
3547 assert_eq!(dst.height(), Some(360));
3548
3549 crate::save_jpeg(&dst, "zidane_resized.jpg", 80).unwrap();
3550
3551 let file = std::fs::read("zidane_resized.jpg").unwrap();
3552 let img = crate::load_image_test_helper(&file, None, None).unwrap();
3555 assert_eq!(img.width(), Some(640));
3556 assert_eq!(img.height(), Some(360));
3557 assert_eq!(img.format().unwrap(), PixelFormat::Nv12);
3558 }
3559
3560 #[test]
3561 fn test_from_tensor_planar() -> Result<(), Error> {
3562 let mut tensor = Tensor::new(&[3, 720, 1280], None, None)?;
3563 tensor
3564 .map()?
3565 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.8bps"));
3566 let planar = {
3567 tensor
3568 .set_format(PixelFormat::PlanarRgb)
3569 .map_err(|e| crate::Error::Internal(e.to_string()))?;
3570 TensorDyn::from(tensor)
3571 };
3572
3573 let rbga = load_bytes_to_tensor(
3574 1280,
3575 720,
3576 PixelFormat::Rgba,
3577 None,
3578 &edgefirst_bench::testdata::read("camera720p.rgba"),
3579 )?;
3580 compare_images_convert_to_rgb(&planar, &rbga, 0.98, function!());
3581
3582 Ok(())
3583 }
3584
3585 #[test]
3586 fn test_from_tensor_invalid_format() {
3587 assert!(PixelFormat::from_fourcc(u32::from_le_bytes(*b"TEST")).is_none());
3590 }
3591
3592 #[test]
3593 #[should_panic(expected = "Failed to save planar RGB image")]
3594 fn test_save_planar() {
3595 let planar_img = load_bytes_to_tensor(
3596 1280,
3597 720,
3598 PixelFormat::PlanarRgb,
3599 None,
3600 &edgefirst_bench::testdata::read("camera720p.8bps"),
3601 )
3602 .unwrap();
3603
3604 let save_path = "/tmp/planar_rgb.jpg";
3605 crate::save_jpeg(&planar_img, save_path, 90).expect("Failed to save planar RGB image");
3606 }
3607
3608 #[test]
3609 #[should_panic(expected = "Failed to save YUYV image")]
3610 fn test_save_yuyv() {
3611 let planar_img = load_bytes_to_tensor(
3612 1280,
3613 720,
3614 PixelFormat::Yuyv,
3615 None,
3616 &edgefirst_bench::testdata::read("camera720p.yuyv"),
3617 )
3618 .unwrap();
3619
3620 let save_path = "/tmp/yuyv.jpg";
3621 crate::save_jpeg(&planar_img, save_path, 90).expect("Failed to save YUYV image");
3622 }
3623
3624 #[test]
3625 fn test_rotation_angle() {
3626 assert_eq!(Rotation::from_degrees_clockwise(0), Rotation::None);
3627 assert_eq!(Rotation::from_degrees_clockwise(90), Rotation::Clockwise90);
3628 assert_eq!(Rotation::from_degrees_clockwise(180), Rotation::Rotate180);
3629 assert_eq!(
3630 Rotation::from_degrees_clockwise(270),
3631 Rotation::CounterClockwise90
3632 );
3633 assert_eq!(Rotation::from_degrees_clockwise(360), Rotation::None);
3634 assert_eq!(Rotation::from_degrees_clockwise(450), Rotation::Clockwise90);
3635 assert_eq!(Rotation::from_degrees_clockwise(540), Rotation::Rotate180);
3636 assert_eq!(
3637 Rotation::from_degrees_clockwise(630),
3638 Rotation::CounterClockwise90
3639 );
3640 }
3641
3642 #[test]
3643 #[should_panic(expected = "rotation angle is not a multiple of 90")]
3644 fn test_rotation_angle_panic() {
3645 Rotation::from_degrees_clockwise(361);
3646 }
3647
3648 #[test]
3649 fn test_disable_env_var() -> Result<(), Error> {
3650 let _lock = acquire_env_lock();
3653
3654 let _guard = EnvGuard::snapshot(&[
3657 "EDGEFIRST_FORCE_BACKEND",
3658 "EDGEFIRST_DISABLE_GL",
3659 "EDGEFIRST_DISABLE_G2D",
3660 "EDGEFIRST_DISABLE_CPU",
3661 ]);
3662
3663 unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") };
3666
3667 #[cfg(target_os = "linux")]
3668 {
3669 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
3670 let converter = ImageProcessor::new()?;
3671 assert!(converter.g2d.is_none());
3672 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_G2D") };
3673 }
3674
3675 #[cfg(target_os = "linux")]
3676 #[cfg(feature = "opengl")]
3677 {
3678 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
3679 let converter = ImageProcessor::new()?;
3680 assert!(converter.opengl.is_none());
3681 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_GL") };
3682 }
3683
3684 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
3685 let converter = ImageProcessor::new()?;
3686 assert!(converter.cpu.is_none());
3687 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_CPU") };
3688
3689 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
3691 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
3692 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
3693 let mut converter = ImageProcessor::new()?;
3694
3695 let src = TensorDyn::image(
3696 1280,
3697 720,
3698 PixelFormat::Rgba,
3699 DType::U8,
3700 None,
3701 edgefirst_tensor::CpuAccess::ReadWrite,
3702 )?;
3703 let dst = TensorDyn::image(
3704 640,
3705 360,
3706 PixelFormat::Rgba,
3707 DType::U8,
3708 None,
3709 edgefirst_tensor::CpuAccess::ReadWrite,
3710 )?;
3711 let (result, _src, _dst) = convert_img(
3712 &mut converter,
3713 src,
3714 dst,
3715 Rotation::None,
3716 Flip::None,
3717 Crop::no_crop(),
3718 );
3719 assert!(matches!(result, Err(Error::NoConverter)));
3720 Ok(())
3722 }
3723
3724 #[test]
3725 fn test_unsupported_conversion() {
3726 let src = TensorDyn::image(
3727 1280,
3728 720,
3729 PixelFormat::Nv12,
3730 DType::U8,
3731 None,
3732 edgefirst_tensor::CpuAccess::ReadWrite,
3733 )
3734 .unwrap();
3735 let dst = TensorDyn::image(
3736 640,
3737 360,
3738 PixelFormat::Nv12,
3739 DType::U8,
3740 None,
3741 edgefirst_tensor::CpuAccess::ReadWrite,
3742 )
3743 .unwrap();
3744 let mut converter = ImageProcessor::new().unwrap();
3745 let (result, _src, _dst) = convert_img(
3746 &mut converter,
3747 src,
3748 dst,
3749 Rotation::None,
3750 Flip::None,
3751 Crop::no_crop(),
3752 );
3753 log::debug!("result: {:?}", result);
3754 assert!(matches!(
3755 result,
3756 Err(Error::NotSupported(e)) if e.starts_with("Conversion from NV12 to NV12")
3757 ));
3758 }
3759
3760 #[test]
3761 fn test_load_grey() {
3762 let grey_img = crate::load_image_test_helper(
3766 &edgefirst_bench::testdata::read("grey.jpg"),
3767 Some(PixelFormat::Rgba),
3768 None,
3769 )
3770 .unwrap();
3771 assert_eq!(grey_img.width(), Some(1024));
3772 assert_eq!(grey_img.height(), Some(681));
3773
3774 let grey_but_rgb = crate::load_image_test_helper(
3780 &edgefirst_bench::testdata::read("grey-rgb.jpg"),
3781 Some(PixelFormat::Rgba),
3782 None,
3783 )
3784 .expect("odd-height colour JPEG should decode to NV12 and convert to RGBA");
3785 assert_eq!(grey_but_rgb.width(), Some(1024));
3786 assert_eq!(grey_but_rgb.height(), Some(681));
3787 }
3788
3789 #[test]
3790 fn test_new_nv12() {
3791 let nv12 = TensorDyn::image(
3792 1280,
3793 720,
3794 PixelFormat::Nv12,
3795 DType::U8,
3796 None,
3797 edgefirst_tensor::CpuAccess::ReadWrite,
3798 )
3799 .unwrap();
3800 assert_eq!(nv12.height(), Some(720));
3801 assert_eq!(nv12.width(), Some(1280));
3802 assert_eq!(nv12.format().unwrap(), PixelFormat::Nv12);
3803 assert_eq!(nv12.format().unwrap().channels(), 1);
3805 assert!(nv12.format().is_some_and(
3806 |f| f.layout() == PixelLayout::Planar || f.layout() == PixelLayout::SemiPlanar
3807 ))
3808 }
3809
3810 #[test]
3811 #[cfg(target_os = "linux")]
3812 fn test_new_image_converter() {
3813 let dst_width = 640;
3814 let dst_height = 360;
3815 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
3816 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
3817
3818 let mut converter = ImageProcessor::new().unwrap();
3819 let converter_dst = converter
3820 .create_image(
3821 dst_width,
3822 dst_height,
3823 PixelFormat::Rgba,
3824 DType::U8,
3825 None,
3826 edgefirst_tensor::CpuAccess::ReadWrite,
3827 )
3828 .unwrap();
3829 let (result, src, converter_dst) = convert_img(
3830 &mut converter,
3831 src,
3832 converter_dst,
3833 Rotation::None,
3834 Flip::None,
3835 Crop::no_crop(),
3836 );
3837 result.unwrap();
3838
3839 let cpu_dst = TensorDyn::image(
3840 dst_width,
3841 dst_height,
3842 PixelFormat::Rgba,
3843 DType::U8,
3844 None,
3845 edgefirst_tensor::CpuAccess::ReadWrite,
3846 )
3847 .unwrap();
3848 let mut cpu_converter = CPUProcessor::new();
3849 let (result, _src, cpu_dst) = convert_img(
3850 &mut cpu_converter,
3851 src,
3852 cpu_dst,
3853 Rotation::None,
3854 Flip::None,
3855 Crop::no_crop(),
3856 );
3857 result.unwrap();
3858
3859 compare_images(&converter_dst, &cpu_dst, 0.98, function!());
3860 }
3861
3862 #[test]
3863 #[cfg(target_os = "linux")]
3864 fn test_create_image_dtype_i8() {
3865 let mut converter = ImageProcessor::new().unwrap();
3866
3867 let dst = converter
3869 .create_image(
3870 320,
3871 240,
3872 PixelFormat::Rgb,
3873 DType::I8,
3874 None,
3875 edgefirst_tensor::CpuAccess::ReadWrite,
3876 )
3877 .unwrap();
3878 assert_eq!(dst.dtype(), DType::I8);
3879 assert!(dst.width() == Some(320));
3880 assert!(dst.height() == Some(240));
3881 assert_eq!(dst.format(), Some(PixelFormat::Rgb));
3882
3883 let dst_u8 = converter
3885 .create_image(
3886 320,
3887 240,
3888 PixelFormat::Rgb,
3889 DType::U8,
3890 None,
3891 edgefirst_tensor::CpuAccess::ReadWrite,
3892 )
3893 .unwrap();
3894 assert_eq!(dst_u8.dtype(), DType::U8);
3895
3896 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
3898 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
3899 let mut dst_i8 = converter
3900 .create_image(
3901 320,
3902 240,
3903 PixelFormat::Rgb,
3904 DType::I8,
3905 None,
3906 edgefirst_tensor::CpuAccess::ReadWrite,
3907 )
3908 .unwrap();
3909 converter
3910 .convert(
3911 &src,
3912 &mut dst_i8,
3913 Rotation::None,
3914 Flip::None,
3915 Crop::no_crop(),
3916 )
3917 .unwrap();
3918 }
3919
3920 #[test]
3921 #[cfg(target_os = "linux")]
3922 fn test_create_image_nv12_dma_non_aligned_width() {
3923 let converter = ImageProcessor::new().unwrap();
3929
3930 let result = converter.create_image(
3932 100,
3933 64,
3934 PixelFormat::Nv12,
3935 DType::U8,
3936 Some(TensorMemory::Dma),
3937 edgefirst_tensor::CpuAccess::ReadWrite,
3938 );
3939
3940 match result {
3941 Ok(img) => {
3942 assert_eq!(img.width(), Some(100));
3943 assert_eq!(img.height(), Some(64));
3944 assert_eq!(img.format(), Some(PixelFormat::Nv12));
3945 if let Some(stride) = img.row_stride() {
3946 assert!(
3947 stride >= 100,
3948 "NV12 row_stride {stride} must be >= the logical width (100)",
3949 );
3950 }
3951 }
3952 Err(e) => {
3953 eprintln!("SKIPPED: create_image NV12 DMA non-aligned width: {e}");
3955 }
3956 }
3957 }
3958
3959 #[test]
3960 #[ignore] fn test_crop_skip() {
3964 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
3965 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
3966
3967 let mut converter = ImageProcessor::new().unwrap();
3968 let converter_dst = converter
3969 .create_image(
3970 1280,
3971 720,
3972 PixelFormat::Rgba,
3973 DType::U8,
3974 None,
3975 edgefirst_tensor::CpuAccess::ReadWrite,
3976 )
3977 .unwrap();
3978 let crop = Crop::new().with_source(Some(Region::new(0, 0, 640, 640)));
3979 let (result, src, converter_dst) = convert_img(
3980 &mut converter,
3981 src,
3982 converter_dst,
3983 Rotation::None,
3984 Flip::None,
3985 crop,
3986 );
3987 result.unwrap();
3988
3989 let cpu_dst = TensorDyn::image(
3990 1280,
3991 720,
3992 PixelFormat::Rgba,
3993 DType::U8,
3994 None,
3995 edgefirst_tensor::CpuAccess::ReadWrite,
3996 )
3997 .unwrap();
3998 let mut cpu_converter = CPUProcessor::new();
3999 let (result, _src, cpu_dst) = convert_img(
4000 &mut cpu_converter,
4001 src,
4002 cpu_dst,
4003 Rotation::None,
4004 Flip::None,
4005 crop,
4006 );
4007 result.unwrap();
4008
4009 compare_images(&converter_dst, &cpu_dst, 0.99999, function!());
4010 }
4011
4012 #[test]
4013 fn test_invalid_pixel_format() {
4014 assert!(PixelFormat::from_fourcc(u32::from_le_bytes(*b"TEST")).is_none());
4017 }
4018
4019 #[cfg(target_os = "linux")]
4021 static G2D_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4022
4023 #[cfg(target_os = "linux")]
4024 fn is_g2d_available() -> bool {
4025 *G2D_AVAILABLE.get_or_init(|| G2DProcessor::new().is_ok())
4026 }
4027
4028 #[cfg(target_os = "linux")]
4029 #[cfg(feature = "opengl")]
4030 static GL_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4031
4032 #[cfg(target_os = "linux")]
4033 #[cfg(feature = "opengl")]
4034 fn is_opengl_available() -> bool {
4036 #[cfg(all(target_os = "linux", feature = "opengl"))]
4037 {
4038 *GL_AVAILABLE.get_or_init(|| GLProcessorThreaded::new(None).is_ok())
4039 }
4040
4041 #[cfg(not(all(target_os = "linux", feature = "opengl")))]
4042 {
4043 false
4044 }
4045 }
4046
4047 #[test]
4059 #[cfg(feature = "opengl")]
4060 fn gl_backend_available_canary() {
4061 let require_gl = std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1");
4062 if !require_gl {
4063 eprintln!(
4064 "SKIPPED: {} — HAL_TEST_REQUIRE_GL is not set to 1",
4065 function!()
4066 );
4067 return;
4068 }
4069 #[cfg(target_os = "macos")]
4070 if std::env::var_os("HAL_TEST_ALLOW_DLOPEN_ANGLE").is_none() {
4071 eprintln!(
4072 "SKIPPED: {} — ANGLE dlopen gate closed (coverage pass 1)",
4073 function!()
4074 );
4075 return;
4076 }
4077 GLProcessorThreaded::new(None).expect(
4078 "HAL_TEST_REQUIRE_GL=1 but the GL backend failed to initialize — \
4079 check the ANGLE install/re-sign step and binary entitlements \
4080 (macOS) or the EGL stack (Linux)",
4081 );
4082 }
4083
4084 #[test]
4085 fn test_load_jpeg_with_exif() {
4086 use edgefirst_codec::peek_info;
4087
4088 let file = edgefirst_bench::testdata::read("zidane_rotated_exif.jpg").to_vec();
4093 let info = peek_info(&file).unwrap();
4094 assert_eq!(info.rotation_degrees, 90);
4095 assert!(!info.flip_horizontal);
4096
4097 let loaded = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4098 assert_eq!(loaded.width(), Some(1280));
4100 assert_eq!(loaded.height(), Some(720));
4101
4102 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4105 let cpu_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4106
4107 let rotation = Rotation::from_degrees_clockwise(info.rotation_degrees as usize);
4108 let (dst_width, dst_height) = (cpu_src.height().unwrap(), cpu_src.width().unwrap());
4109
4110 let cpu_dst = TensorDyn::image(
4111 dst_width,
4112 dst_height,
4113 PixelFormat::Rgba,
4114 DType::U8,
4115 None,
4116 edgefirst_tensor::CpuAccess::ReadWrite,
4117 )
4118 .unwrap();
4119 let mut cpu_converter = CPUProcessor::new();
4120
4121 let loaded_rotated = TensorDyn::image(
4124 dst_width,
4125 dst_height,
4126 PixelFormat::Rgba,
4127 DType::U8,
4128 None,
4129 edgefirst_tensor::CpuAccess::ReadWrite,
4130 )
4131 .unwrap();
4132 let (r0, _loaded, loaded_rotated) = convert_img(
4133 &mut cpu_converter,
4134 loaded,
4135 loaded_rotated,
4136 rotation,
4137 Flip::None,
4138 Crop::no_crop(),
4139 );
4140 r0.unwrap();
4141
4142 let (result, _cpu_src, cpu_dst) = convert_img(
4143 &mut cpu_converter,
4144 cpu_src,
4145 cpu_dst,
4146 rotation,
4147 Flip::None,
4148 Crop::no_crop(),
4149 );
4150 result.unwrap();
4151
4152 compare_images(&loaded_rotated, &cpu_dst, 0.98, function!());
4153 }
4154
4155 #[test]
4156 fn test_load_png_with_exif() {
4157 use edgefirst_codec::peek_info;
4158
4159 let file = edgefirst_bench::testdata::read("zidane_rotated_exif_180.png").to_vec();
4162 let info = peek_info(&file).unwrap();
4163 assert_eq!(info.rotation_degrees, 180);
4164 assert!(!info.flip_horizontal);
4165
4166 let loaded = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4167 assert_eq!(loaded.height(), Some(720));
4169 assert_eq!(loaded.width(), Some(1280));
4170
4171 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4176 let cpu_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4177
4178 let rotation = Rotation::from_degrees_clockwise(info.rotation_degrees as usize);
4179 let cpu_dst = TensorDyn::image(
4180 1280,
4181 720,
4182 PixelFormat::Rgba,
4183 DType::U8,
4184 None,
4185 edgefirst_tensor::CpuAccess::ReadWrite,
4186 )
4187 .unwrap();
4188 let mut cpu_converter = CPUProcessor::new();
4189
4190 let (result, _cpu_src, cpu_dst) = convert_img(
4191 &mut cpu_converter,
4192 cpu_src,
4193 cpu_dst,
4194 rotation,
4195 Flip::None,
4196 Crop::no_crop(),
4197 );
4198 result.unwrap();
4199
4200 let loaded_rotated = TensorDyn::image(
4203 1280,
4204 720,
4205 PixelFormat::Rgba,
4206 DType::U8,
4207 None,
4208 edgefirst_tensor::CpuAccess::ReadWrite,
4209 )
4210 .unwrap();
4211 let (r0, _loaded, loaded_rotated) = convert_img(
4212 &mut cpu_converter,
4213 loaded,
4214 loaded_rotated,
4215 rotation,
4216 Flip::None,
4217 Crop::no_crop(),
4218 );
4219 r0.unwrap();
4220
4221 compare_images(&loaded_rotated, &cpu_dst, 0.95, function!());
4226 }
4227
4228 #[cfg(target_os = "linux")]
4234 fn make_rgb_jpeg(width: u32, height: u32) -> Vec<u8> {
4235 let mut bytes = Vec::with_capacity((width * height * 3) as usize);
4236 for y in 0..height {
4237 for x in 0..width {
4238 bytes.push(((x + y) & 0xFF) as u8);
4239 bytes.push(((x.wrapping_mul(3)) & 0xFF) as u8);
4240 bytes.push(((y.wrapping_mul(5)) & 0xFF) as u8);
4241 }
4242 }
4243 let mut out = Vec::new();
4244 let encoder = jpeg_encoder::Encoder::new(&mut out, 85);
4245 encoder
4246 .encode(
4247 &bytes,
4248 width as u16,
4249 height as u16,
4250 jpeg_encoder::ColorType::Rgb,
4251 )
4252 .expect("jpeg-encoder must succeed on trivial input");
4253 out
4254 }
4255
4256 #[test]
4265 #[cfg(target_os = "linux")]
4266 #[cfg(feature = "opengl")]
4267 fn test_convert_rgba_non_4_aligned_width_end_to_end() {
4268 use edgefirst_tensor::is_dma_available;
4269 if !is_dma_available() {
4270 eprintln!(
4271 "SKIPPED: test_convert_rgba_non_4_aligned_width_end_to_end — DMA not available"
4272 );
4273 return;
4274 }
4275 let jpeg = make_rgb_jpeg(375, 333);
4279 let src_gl = crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), None).unwrap();
4280 assert_eq!(src_gl.width(), Some(375));
4281 let stride = src_gl.row_stride().unwrap();
4283 assert_eq!(stride, 1536, "expected padded pitch 1536, got {stride}");
4284
4285 let mut gl_proc = ImageProcessor::new().unwrap();
4287 let gl_dst = gl_proc
4288 .create_image(
4289 640,
4290 640,
4291 PixelFormat::Rgba,
4292 DType::U8,
4293 None,
4294 edgefirst_tensor::CpuAccess::ReadWrite,
4295 )
4296 .unwrap();
4297 let (r_gl, _src_gl, gl_dst) = convert_img(
4298 &mut gl_proc,
4299 src_gl,
4300 gl_dst,
4301 Rotation::None,
4302 Flip::None,
4303 Crop::no_crop(),
4304 );
4305 r_gl.expect("GL-backed convert must succeed for 375x333 Rgba src");
4306
4307 let src_cpu =
4312 crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), Some(TensorMemory::Mem))
4313 .unwrap();
4314 let mut cpu_proc = ImageProcessor::with_config(ImageProcessorConfig {
4315 backend: ComputeBackend::Cpu,
4316 ..Default::default()
4317 })
4318 .unwrap();
4319 let cpu_dst = TensorDyn::image(
4320 640,
4321 640,
4322 PixelFormat::Rgba,
4323 DType::U8,
4324 Some(TensorMemory::Mem),
4325 edgefirst_tensor::CpuAccess::ReadWrite,
4326 )
4327 .unwrap();
4328 let (r_cpu, _src_cpu, cpu_dst) = convert_img(
4329 &mut cpu_proc,
4330 src_cpu,
4331 cpu_dst,
4332 Rotation::None,
4333 Flip::None,
4334 Crop::no_crop(),
4335 );
4336 r_cpu.unwrap();
4337
4338 compare_images(&gl_dst, &cpu_dst, 0.95, function!());
4342 }
4343
4344 #[test]
4351 #[cfg(target_os = "linux")]
4352 fn test_load_jpeg_rgba_non_aligned_pitch_padded_dma() {
4353 use edgefirst_tensor::is_dma_available;
4354 if !is_dma_available() {
4355 eprintln!(
4356 "SKIPPED: test_load_jpeg_rgba_non_aligned_pitch_padded_dma — DMA not available"
4357 );
4358 return;
4359 }
4360 for &w in &[500u32, 612, 428] {
4364 let jpeg = make_rgb_jpeg(w, 333);
4365 let loaded =
4366 crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), None).unwrap();
4367 let natural = (w as usize) * 4;
4368 let aligned = crate::align_pitch_bytes_to_gpu_alignment(natural).unwrap();
4369 assert!(
4370 aligned > natural,
4371 "test sanity: width {w} should be unaligned"
4372 );
4373 let stride = loaded
4374 .row_stride()
4375 .expect("padded DMA path must set an explicit row_stride — regression if None");
4376 assert_eq!(
4377 stride, aligned,
4378 "width {w}: expected padded stride {aligned}, got {stride} \
4379 (regression: pitch-padding branch skipped?)"
4380 );
4381 let eff = loaded.effective_row_stride().unwrap();
4382 assert_eq!(
4383 eff, aligned,
4384 "effective_row_stride must match stored stride"
4385 );
4386 assert_eq!(loaded.width(), Some(w as usize));
4387 assert_eq!(loaded.height(), Some(333));
4388 }
4389 }
4390
4391 #[test]
4400 #[cfg(target_os = "linux")]
4401 fn test_padded_dma_pitch_for_respects_memory_choice() {
4402 use edgefirst_tensor::{is_dma_available, TensorMemory};
4403
4404 let unaligned_w = 500;
4407
4408 assert_eq!(
4410 crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &Some(TensorMemory::Mem),),
4411 None,
4412 "Mem must never trigger DMA padding"
4413 );
4414 assert_eq!(
4415 crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &Some(TensorMemory::Shm),),
4416 None,
4417 "Shm must never trigger DMA padding"
4418 );
4419
4420 assert_eq!(
4425 crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &Some(TensorMemory::Dma),),
4426 Some(2048),
4427 "explicit Dma must pad regardless of runtime DMA availability"
4428 );
4429
4430 let none_result = crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &None);
4434 if is_dma_available() {
4435 assert_eq!(
4436 none_result,
4437 Some(2048),
4438 "memory=None + DMA available → pad (will route through DMA)"
4439 );
4440 } else {
4441 assert_eq!(
4442 none_result, None,
4443 "memory=None + DMA unavailable → must NOT pad (would force \
4444 image_with_stride into a DMA-only allocation that fails). \
4445 Regression: padded_dma_pitch_for ignored is_dma_available()."
4446 );
4447 }
4448 }
4449
4450 fn make_grey_png(width: u32, height: u32) -> Vec<u8> {
4454 let mut bytes = Vec::with_capacity((width * height) as usize);
4455 for y in 0..height {
4456 for x in 0..width {
4457 bytes.push(((x + y) & 0xFF) as u8);
4458 }
4459 }
4460 let img = image::GrayImage::from_vec(width, height, bytes).unwrap();
4461 let mut buf = Vec::new();
4462 img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)
4463 .unwrap();
4464 buf
4465 }
4466
4467 #[test]
4472 #[cfg(target_os = "linux")]
4473 fn test_load_png_grey_misaligned_width_dma() {
4474 use edgefirst_tensor::is_dma_available;
4475 if !is_dma_available() {
4476 eprintln!("SKIPPED: test_load_png_grey_misaligned_width_dma — DMA not available");
4477 return;
4478 }
4479 let png = make_grey_png(612, 388);
4480 let loaded = crate::load_image_test_helper(&png, Some(PixelFormat::Grey), None).unwrap();
4481 assert_eq!(loaded.width(), Some(612));
4482 assert_eq!(loaded.height(), Some(388));
4483 assert_eq!(loaded.format(), Some(PixelFormat::Grey));
4484
4485 let map = loaded.as_u8().unwrap().map().unwrap();
4488 let stride = loaded.row_stride().unwrap_or(612);
4489 assert!(stride >= 612);
4490 let bytes: &[u8] = ↦
4491 for y in 0..388usize {
4492 for x in 0..612usize {
4493 let expected = ((x + y) & 0xFF) as u8;
4494 let got = bytes[y * stride + x];
4495 assert_eq!(
4496 got, expected,
4497 "grey png mismatch at ({x},{y}): got {got} expected {expected}"
4498 );
4499 }
4500 }
4501 }
4502
4503 #[test]
4507 fn test_load_png_grey_mem() {
4508 use edgefirst_tensor::TensorMemory;
4509 let png = make_grey_png(612, 100);
4510 let loaded =
4511 crate::load_image_test_helper(&png, Some(PixelFormat::Grey), Some(TensorMemory::Mem))
4512 .unwrap();
4513 assert_eq!(loaded.width(), Some(612));
4514 assert_eq!(loaded.height(), Some(100));
4515 assert_eq!(loaded.format(), Some(PixelFormat::Grey));
4516 let map = loaded.as_u8().unwrap().map().unwrap();
4517 let bytes: &[u8] = ↦
4518 assert_eq!(bytes.len(), 612 * 100);
4520 for y in 0..100 {
4521 for x in 0..612 {
4522 assert_eq!(bytes[y * 612 + x], ((x + y) & 0xFF) as u8);
4523 }
4524 }
4525 }
4526
4527 #[test]
4531 fn test_load_png_grey_to_rgb_mem() {
4532 use edgefirst_tensor::TensorMemory;
4533 let png = make_grey_png(620, 240);
4534 let loaded =
4535 crate::load_image_test_helper(&png, Some(PixelFormat::Rgb), Some(TensorMemory::Mem))
4536 .unwrap();
4537 assert_eq!(loaded.width(), Some(620));
4538 assert_eq!(loaded.height(), Some(240));
4539 assert_eq!(loaded.format(), Some(PixelFormat::Rgb));
4540
4541 let map = loaded.as_u8().unwrap().map().unwrap();
4543 let bytes: &[u8] = ↦
4544 for (x, y) in [(0usize, 0usize), (100, 50), (619, 239)] {
4545 let expected = ((x + y) & 0xFF) as u8;
4546 let off = (y * 620 + x) * 3;
4547 assert_eq!(bytes[off], expected, "R@{x},{y}");
4548 assert_eq!(bytes[off + 1], expected, "G@{x},{y}");
4549 assert_eq!(bytes[off + 2], expected, "B@{x},{y}");
4550 }
4551 }
4552
4553 #[test]
4554 #[cfg(target_os = "linux")]
4555 fn test_g2d_resize() {
4556 if !is_g2d_available() {
4557 eprintln!("SKIPPED: test_g2d_resize - G2D library (libg2d.so.2) not available");
4558 return;
4559 }
4560 if !is_dma_available() {
4561 eprintln!(
4562 "SKIPPED: test_g2d_resize - DMA memory allocation not available (permission denied or no DMA-BUF support)"
4563 );
4564 return;
4565 }
4566
4567 let dst_width = 640;
4568 let dst_height = 360;
4569 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4570 let src =
4571 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), Some(TensorMemory::Dma))
4572 .unwrap();
4573
4574 let g2d_dst = TensorDyn::image(
4575 dst_width,
4576 dst_height,
4577 PixelFormat::Rgba,
4578 DType::U8,
4579 Some(TensorMemory::Dma),
4580 edgefirst_tensor::CpuAccess::ReadWrite,
4581 )
4582 .unwrap();
4583 let mut g2d_converter = G2DProcessor::new().unwrap();
4584 let (result, src, g2d_dst) = convert_img(
4585 &mut g2d_converter,
4586 src,
4587 g2d_dst,
4588 Rotation::None,
4589 Flip::None,
4590 Crop::no_crop(),
4591 );
4592 result.unwrap();
4593
4594 let cpu_dst = TensorDyn::image(
4595 dst_width,
4596 dst_height,
4597 PixelFormat::Rgba,
4598 DType::U8,
4599 None,
4600 edgefirst_tensor::CpuAccess::ReadWrite,
4601 )
4602 .unwrap();
4603 let mut cpu_converter = CPUProcessor::new();
4604 let (result, _src, cpu_dst) = convert_img(
4605 &mut cpu_converter,
4606 src,
4607 cpu_dst,
4608 Rotation::None,
4609 Flip::None,
4610 Crop::no_crop(),
4611 );
4612 result.unwrap();
4613
4614 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
4620 }
4621
4622 #[test]
4623 #[cfg(target_os = "linux")]
4624 #[cfg(feature = "opengl")]
4625 fn test_opengl_resize() {
4626 if !is_opengl_available() {
4627 eprintln!("SKIPPED: {} - OpenGL not available", function!());
4628 return;
4629 }
4630
4631 let dst_width = 640;
4632 let dst_height = 360;
4633 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4634 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4635
4636 let cpu_dst = TensorDyn::image(
4637 dst_width,
4638 dst_height,
4639 PixelFormat::Rgba,
4640 DType::U8,
4641 None,
4642 edgefirst_tensor::CpuAccess::ReadWrite,
4643 )
4644 .unwrap();
4645 let mut cpu_converter = CPUProcessor::new();
4646 let (result, src, cpu_dst) = convert_img(
4647 &mut cpu_converter,
4648 src,
4649 cpu_dst,
4650 Rotation::None,
4651 Flip::None,
4652 Crop::no_crop(),
4653 );
4654 result.unwrap();
4655
4656 let mut src = src;
4657 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
4658
4659 for _ in 0..5 {
4660 let gl_dst = TensorDyn::image(
4661 dst_width,
4662 dst_height,
4663 PixelFormat::Rgba,
4664 DType::U8,
4665 None,
4666 edgefirst_tensor::CpuAccess::ReadWrite,
4667 )
4668 .unwrap();
4669 let (result, src_back, gl_dst) = convert_img(
4670 &mut gl_converter,
4671 src,
4672 gl_dst,
4673 Rotation::None,
4674 Flip::None,
4675 Crop::no_crop(),
4676 );
4677 result.unwrap();
4678 src = src_back;
4679
4680 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
4681 }
4682 }
4683
4684 #[test]
4685 #[cfg(target_os = "linux")]
4686 #[cfg(feature = "opengl")]
4687 fn test_opengl_10_threads() {
4688 if !is_opengl_available() {
4689 eprintln!("SKIPPED: {} - OpenGL not available", function!());
4690 return;
4691 }
4692
4693 let handles: Vec<_> = (0..10)
4694 .map(|i| {
4695 std::thread::Builder::new()
4696 .name(format!("Thread {i}"))
4697 .spawn(test_opengl_resize)
4698 .unwrap()
4699 })
4700 .collect();
4701 handles.into_iter().for_each(|h| {
4702 if let Err(e) = h.join() {
4703 std::panic::resume_unwind(e)
4704 }
4705 });
4706 }
4707
4708 #[test]
4709 #[cfg(target_os = "linux")]
4710 #[cfg(feature = "opengl")]
4711 fn test_opengl_grey() {
4712 if !is_opengl_available() {
4713 eprintln!("SKIPPED: {} - OpenGL not available", function!());
4714 return;
4715 }
4716
4717 let img = crate::load_image_test_helper(
4718 &edgefirst_bench::testdata::read("grey.jpg"),
4719 Some(PixelFormat::Grey),
4720 None,
4721 )
4722 .unwrap();
4723
4724 let gl_dst = TensorDyn::image(
4725 640,
4726 640,
4727 PixelFormat::Grey,
4728 DType::U8,
4729 None,
4730 edgefirst_tensor::CpuAccess::ReadWrite,
4731 )
4732 .unwrap();
4733 let cpu_dst = TensorDyn::image(
4734 640,
4735 640,
4736 PixelFormat::Grey,
4737 DType::U8,
4738 None,
4739 edgefirst_tensor::CpuAccess::ReadWrite,
4740 )
4741 .unwrap();
4742
4743 let mut converter = CPUProcessor::new();
4744
4745 let (result, img, cpu_dst) = convert_img(
4746 &mut converter,
4747 img,
4748 cpu_dst,
4749 Rotation::None,
4750 Flip::None,
4751 Crop::no_crop(),
4752 );
4753 result.unwrap();
4754
4755 let mut gl = GLProcessorThreaded::new(None).unwrap();
4756 let (result, _img, gl_dst) = convert_img(
4757 &mut gl,
4758 img,
4759 gl_dst,
4760 Rotation::None,
4761 Flip::None,
4762 Crop::no_crop(),
4763 );
4764 result.unwrap();
4765
4766 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
4767 }
4768
4769 #[test]
4770 #[cfg(target_os = "linux")]
4771 fn test_g2d_src_crop() {
4772 if !is_g2d_available() {
4773 eprintln!("SKIPPED: test_g2d_src_crop - G2D library (libg2d.so.2) not available");
4774 return;
4775 }
4776 if !is_dma_available() {
4777 eprintln!(
4778 "SKIPPED: test_g2d_src_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)"
4779 );
4780 return;
4781 }
4782
4783 let dst_width = 640;
4784 let dst_height = 640;
4785 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4786 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4787
4788 let cpu_dst = TensorDyn::image(
4789 dst_width,
4790 dst_height,
4791 PixelFormat::Rgba,
4792 DType::U8,
4793 None,
4794 edgefirst_tensor::CpuAccess::ReadWrite,
4795 )
4796 .unwrap();
4797 let mut cpu_converter = CPUProcessor::new();
4798 let crop = Crop::new().with_source(Some(Region::new(0, 0, 640, 360)));
4799 let (result, src, cpu_dst) = convert_img(
4800 &mut cpu_converter,
4801 src,
4802 cpu_dst,
4803 Rotation::None,
4804 Flip::None,
4805 crop,
4806 );
4807 result.unwrap();
4808
4809 let g2d_dst = TensorDyn::image(
4810 dst_width,
4811 dst_height,
4812 PixelFormat::Rgba,
4813 DType::U8,
4814 None,
4815 edgefirst_tensor::CpuAccess::ReadWrite,
4816 )
4817 .unwrap();
4818 let mut g2d_converter = G2DProcessor::new().unwrap();
4819 let (result, _src, g2d_dst) = convert_img(
4820 &mut g2d_converter,
4821 src,
4822 g2d_dst,
4823 Rotation::None,
4824 Flip::None,
4825 crop,
4826 );
4827 result.unwrap();
4828
4829 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
4835 }
4836
4837 #[test]
4838 #[cfg(target_os = "linux")]
4839 fn test_g2d_dst_crop() {
4840 if !is_g2d_available() {
4841 eprintln!("SKIPPED: test_g2d_dst_crop - G2D library (libg2d.so.2) not available");
4842 return;
4843 }
4844 if !is_dma_available() {
4845 eprintln!(
4846 "SKIPPED: test_g2d_dst_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)"
4847 );
4848 return;
4849 }
4850
4851 let dst_width = 640;
4852 let dst_height = 640;
4853 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4854 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4855
4856 let cpu_dst = TensorDyn::image(
4857 dst_width,
4858 dst_height,
4859 PixelFormat::Rgba,
4860 DType::U8,
4861 None,
4862 edgefirst_tensor::CpuAccess::ReadWrite,
4863 )
4864 .unwrap();
4865 let mut cpu_converter = CPUProcessor::new();
4866 let crop = Crop::new();
4867 let (result, src, cpu_dst) = convert_img(
4868 &mut cpu_converter,
4869 src,
4870 cpu_dst,
4871 Rotation::None,
4872 Flip::None,
4873 crop,
4874 );
4875 result.unwrap();
4876
4877 let g2d_dst = TensorDyn::image(
4878 dst_width,
4879 dst_height,
4880 PixelFormat::Rgba,
4881 DType::U8,
4882 None,
4883 edgefirst_tensor::CpuAccess::ReadWrite,
4884 )
4885 .unwrap();
4886 let mut g2d_converter = G2DProcessor::new().unwrap();
4887 let (result, _src, g2d_dst) = convert_img(
4888 &mut g2d_converter,
4889 src,
4890 g2d_dst,
4891 Rotation::None,
4892 Flip::None,
4893 crop,
4894 );
4895 result.unwrap();
4896
4897 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
4903 }
4904
4905 #[test]
4906 #[cfg(target_os = "linux")]
4907 fn test_g2d_all_rgba() {
4908 if !is_g2d_available() {
4909 eprintln!("SKIPPED: test_g2d_all_rgba - G2D library (libg2d.so.2) not available");
4910 return;
4911 }
4912 if !is_dma_available() {
4913 eprintln!(
4914 "SKIPPED: test_g2d_all_rgba - DMA memory allocation not available (permission denied or no DMA-BUF support)"
4915 );
4916 return;
4917 }
4918
4919 let dst_width = 640;
4920 let dst_height = 640;
4921 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4922 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4923 let src_dyn = src;
4924
4925 let mut cpu_dst = TensorDyn::image(
4926 dst_width,
4927 dst_height,
4928 PixelFormat::Rgba,
4929 DType::U8,
4930 None,
4931 edgefirst_tensor::CpuAccess::ReadWrite,
4932 )
4933 .unwrap();
4934 let mut cpu_converter = CPUProcessor::new();
4935 let mut g2d_dst = TensorDyn::image(
4936 dst_width,
4937 dst_height,
4938 PixelFormat::Rgba,
4939 DType::U8,
4940 None,
4941 edgefirst_tensor::CpuAccess::ReadWrite,
4942 )
4943 .unwrap();
4944 let mut g2d_converter = G2DProcessor::new().unwrap();
4945
4946 let crop = Crop::new().with_source(Some(Region::new(50, 120, 1024, 576)));
4947
4948 for rot in [
4949 Rotation::None,
4950 Rotation::Clockwise90,
4951 Rotation::Rotate180,
4952 Rotation::CounterClockwise90,
4953 ] {
4954 cpu_dst
4955 .as_u8()
4956 .unwrap()
4957 .map()
4958 .unwrap()
4959 .as_mut_slice()
4960 .fill(114);
4961 g2d_dst
4962 .as_u8()
4963 .unwrap()
4964 .map()
4965 .unwrap()
4966 .as_mut_slice()
4967 .fill(114);
4968 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
4969 let mut cpu_dst_dyn = cpu_dst;
4970 cpu_converter
4971 .convert(&src_dyn, &mut cpu_dst_dyn, Rotation::None, Flip::None, crop)
4972 .unwrap();
4973 cpu_dst = {
4974 let mut __t = cpu_dst_dyn.into_u8().unwrap();
4975 __t.set_format(PixelFormat::Rgba).unwrap();
4976 TensorDyn::from(__t)
4977 };
4978
4979 let mut g2d_dst_dyn = g2d_dst;
4980 g2d_converter
4981 .convert(&src_dyn, &mut g2d_dst_dyn, Rotation::None, Flip::None, crop)
4982 .unwrap();
4983 g2d_dst = {
4984 let mut __t = g2d_dst_dyn.into_u8().unwrap();
4985 __t.set_format(PixelFormat::Rgba).unwrap();
4986 TensorDyn::from(__t)
4987 };
4988
4989 compare_images(
4990 &g2d_dst,
4991 &cpu_dst,
4992 0.98,
4993 &format!("{} {:?} {:?}", function!(), rot, flip),
4994 );
4995 }
4996 }
4997 }
4998
4999 #[test]
5000 #[cfg(target_os = "linux")]
5001 #[cfg(feature = "opengl")]
5002 fn test_opengl_src_crop() {
5003 if !is_opengl_available() {
5004 eprintln!("SKIPPED: {} - OpenGL not available", function!());
5005 return;
5006 }
5007
5008 let dst_width = 640;
5009 let dst_height = 360;
5010 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5011 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5012 let crop = Crop::new().with_source(Some(Region::new(320, 180, 1280 - 320, 720 - 180)));
5013
5014 let cpu_dst = TensorDyn::image(
5015 dst_width,
5016 dst_height,
5017 PixelFormat::Rgba,
5018 DType::U8,
5019 None,
5020 edgefirst_tensor::CpuAccess::ReadWrite,
5021 )
5022 .unwrap();
5023 let mut cpu_converter = CPUProcessor::new();
5024 let (result, src, cpu_dst) = convert_img(
5025 &mut cpu_converter,
5026 src,
5027 cpu_dst,
5028 Rotation::None,
5029 Flip::None,
5030 crop,
5031 );
5032 result.unwrap();
5033
5034 let gl_dst = TensorDyn::image(
5035 dst_width,
5036 dst_height,
5037 PixelFormat::Rgba,
5038 DType::U8,
5039 None,
5040 edgefirst_tensor::CpuAccess::ReadWrite,
5041 )
5042 .unwrap();
5043 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5044 let (result, _src, gl_dst) = convert_img(
5045 &mut gl_converter,
5046 src,
5047 gl_dst,
5048 Rotation::None,
5049 Flip::None,
5050 crop,
5051 );
5052 result.unwrap();
5053
5054 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5055 }
5056
5057 #[test]
5058 #[cfg(target_os = "linux")]
5059 #[cfg(feature = "opengl")]
5060 fn test_opengl_dst_crop() {
5061 if !is_opengl_available() {
5062 eprintln!("SKIPPED: {} - OpenGL not available", function!());
5063 return;
5064 }
5065
5066 let dst_width = 640;
5067 let dst_height = 640;
5068 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5069 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5070
5071 let cpu_dst = TensorDyn::image(
5072 dst_width,
5073 dst_height,
5074 PixelFormat::Rgba,
5075 DType::U8,
5076 None,
5077 edgefirst_tensor::CpuAccess::ReadWrite,
5078 )
5079 .unwrap();
5080 let mut cpu_converter = CPUProcessor::new();
5081 let crop = Crop::new();
5082 let (result, src, cpu_dst) = convert_img(
5083 &mut cpu_converter,
5084 src,
5085 cpu_dst,
5086 Rotation::None,
5087 Flip::None,
5088 crop,
5089 );
5090 result.unwrap();
5091
5092 let gl_dst = TensorDyn::image(
5093 dst_width,
5094 dst_height,
5095 PixelFormat::Rgba,
5096 DType::U8,
5097 None,
5098 edgefirst_tensor::CpuAccess::ReadWrite,
5099 )
5100 .unwrap();
5101 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5102 let (result, _src, gl_dst) = convert_img(
5103 &mut gl_converter,
5104 src,
5105 gl_dst,
5106 Rotation::None,
5107 Flip::None,
5108 crop,
5109 );
5110 result.unwrap();
5111
5112 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5113 }
5114
5115 #[test]
5116 #[cfg(target_os = "linux")]
5117 #[cfg(feature = "opengl")]
5118 fn test_opengl_all_rgba() {
5119 if !is_opengl_available() {
5120 eprintln!("SKIPPED: {} - OpenGL not available", function!());
5121 return;
5122 }
5123
5124 let dst_width = 640;
5125 let dst_height = 640;
5126 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5127
5128 let mut cpu_converter = CPUProcessor::new();
5129
5130 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5131
5132 let mut mem = vec![None, Some(TensorMemory::Mem), Some(TensorMemory::Shm)];
5133 if is_dma_available() {
5134 mem.push(Some(TensorMemory::Dma));
5135 }
5136 let crop = Crop::new().with_source(Some(Region::new(50, 120, 1024, 576)));
5137 for m in mem {
5138 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), m).unwrap();
5139 let src_dyn = src;
5140
5141 for rot in [
5142 Rotation::None,
5143 Rotation::Clockwise90,
5144 Rotation::Rotate180,
5145 Rotation::CounterClockwise90,
5146 ] {
5147 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
5148 let cpu_dst = TensorDyn::image(
5149 dst_width,
5150 dst_height,
5151 PixelFormat::Rgba,
5152 DType::U8,
5153 m,
5154 edgefirst_tensor::CpuAccess::ReadWrite,
5155 )
5156 .unwrap();
5157 let gl_dst = TensorDyn::image(
5158 dst_width,
5159 dst_height,
5160 PixelFormat::Rgba,
5161 DType::U8,
5162 m,
5163 edgefirst_tensor::CpuAccess::ReadWrite,
5164 )
5165 .unwrap();
5166 cpu_dst
5167 .as_u8()
5168 .unwrap()
5169 .map()
5170 .unwrap()
5171 .as_mut_slice()
5172 .fill(114);
5173 gl_dst
5174 .as_u8()
5175 .unwrap()
5176 .map()
5177 .unwrap()
5178 .as_mut_slice()
5179 .fill(114);
5180
5181 let mut cpu_dst_dyn = cpu_dst;
5182 cpu_converter
5183 .convert(&src_dyn, &mut cpu_dst_dyn, Rotation::None, Flip::None, crop)
5184 .unwrap();
5185 let cpu_dst = {
5186 let mut __t = cpu_dst_dyn.into_u8().unwrap();
5187 __t.set_format(PixelFormat::Rgba).unwrap();
5188 TensorDyn::from(__t)
5189 };
5190
5191 let mut gl_dst_dyn = gl_dst;
5192 gl_converter
5193 .convert(&src_dyn, &mut gl_dst_dyn, Rotation::None, Flip::None, crop)
5194 .map_err(|e| {
5195 log::error!("error mem {m:?} rot {rot:?} error: {e:?}");
5196 e
5197 })
5198 .unwrap();
5199 let gl_dst = {
5200 let mut __t = gl_dst_dyn.into_u8().unwrap();
5201 __t.set_format(PixelFormat::Rgba).unwrap();
5202 TensorDyn::from(__t)
5203 };
5204
5205 compare_images(
5206 &gl_dst,
5207 &cpu_dst,
5208 0.98,
5209 &format!("{} {:?} {:?}", function!(), rot, flip),
5210 );
5211 }
5212 }
5213 }
5214 }
5215
5216 #[test]
5217 #[cfg(target_os = "linux")]
5218 fn test_cpu_rotate() {
5219 for rot in [
5220 Rotation::Clockwise90,
5221 Rotation::Rotate180,
5222 Rotation::CounterClockwise90,
5223 ] {
5224 test_cpu_rotate_(rot);
5225 }
5226 }
5227
5228 #[cfg(target_os = "linux")]
5229 fn test_cpu_rotate_(rot: Rotation) {
5230 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5234
5235 let unchanged_src =
5236 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5237 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5238
5239 let (dst_width, dst_height) = match rot {
5240 Rotation::None | Rotation::Rotate180 => (src.width().unwrap(), src.height().unwrap()),
5241 Rotation::Clockwise90 | Rotation::CounterClockwise90 => {
5242 (src.height().unwrap(), src.width().unwrap())
5243 }
5244 };
5245
5246 let cpu_dst = TensorDyn::image(
5247 dst_width,
5248 dst_height,
5249 PixelFormat::Rgba,
5250 DType::U8,
5251 None,
5252 edgefirst_tensor::CpuAccess::ReadWrite,
5253 )
5254 .unwrap();
5255 let mut cpu_converter = CPUProcessor::new();
5256
5257 let (result, src, cpu_dst) = convert_img(
5260 &mut cpu_converter,
5261 src,
5262 cpu_dst,
5263 rot,
5264 Flip::None,
5265 Crop::no_crop(),
5266 );
5267 result.unwrap();
5268
5269 let (result, cpu_dst, src) = convert_img(
5270 &mut cpu_converter,
5271 cpu_dst,
5272 src,
5273 rot,
5274 Flip::None,
5275 Crop::no_crop(),
5276 );
5277 result.unwrap();
5278
5279 let (result, src, cpu_dst) = convert_img(
5280 &mut cpu_converter,
5281 src,
5282 cpu_dst,
5283 rot,
5284 Flip::None,
5285 Crop::no_crop(),
5286 );
5287 result.unwrap();
5288
5289 let (result, _cpu_dst, src) = convert_img(
5290 &mut cpu_converter,
5291 cpu_dst,
5292 src,
5293 rot,
5294 Flip::None,
5295 Crop::no_crop(),
5296 );
5297 result.unwrap();
5298
5299 compare_images(&src, &unchanged_src, 0.98, function!());
5300 }
5301
5302 #[test]
5303 #[cfg(target_os = "linux")]
5304 #[cfg(feature = "opengl")]
5305 fn test_opengl_rotate() {
5306 if !is_opengl_available() {
5307 eprintln!("SKIPPED: {} - OpenGL not available", function!());
5308 return;
5309 }
5310
5311 let size = (1280, 720);
5312 let mut mem = vec![None, Some(TensorMemory::Shm), Some(TensorMemory::Mem)];
5313
5314 if is_dma_available() {
5315 mem.push(Some(TensorMemory::Dma));
5316 }
5317 for m in mem {
5318 for rot in [
5319 Rotation::Clockwise90,
5320 Rotation::Rotate180,
5321 Rotation::CounterClockwise90,
5322 ] {
5323 test_opengl_rotate_(size, rot, m);
5324 }
5325 }
5326 }
5327
5328 #[cfg(target_os = "linux")]
5329 #[cfg(feature = "opengl")]
5330 fn test_opengl_rotate_(
5331 size: (usize, usize),
5332 rot: Rotation,
5333 tensor_memory: Option<TensorMemory>,
5334 ) {
5335 let (dst_width, dst_height) = match rot {
5336 Rotation::None | Rotation::Rotate180 => size,
5337 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
5338 };
5339
5340 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5341 let src =
5342 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), tensor_memory).unwrap();
5343
5344 let cpu_dst = TensorDyn::image(
5345 dst_width,
5346 dst_height,
5347 PixelFormat::Rgba,
5348 DType::U8,
5349 None,
5350 edgefirst_tensor::CpuAccess::ReadWrite,
5351 )
5352 .unwrap();
5353 let mut cpu_converter = CPUProcessor::new();
5354
5355 let (result, mut src, cpu_dst) = convert_img(
5356 &mut cpu_converter,
5357 src,
5358 cpu_dst,
5359 rot,
5360 Flip::None,
5361 Crop::no_crop(),
5362 );
5363 result.unwrap();
5364
5365 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5366
5367 for _ in 0..5 {
5368 let gl_dst = TensorDyn::image(
5369 dst_width,
5370 dst_height,
5371 PixelFormat::Rgba,
5372 DType::U8,
5373 tensor_memory,
5374 edgefirst_tensor::CpuAccess::ReadWrite,
5375 )
5376 .unwrap();
5377 let (result, src_back, gl_dst) = convert_img(
5378 &mut gl_converter,
5379 src,
5380 gl_dst,
5381 rot,
5382 Flip::None,
5383 Crop::no_crop(),
5384 );
5385 result.unwrap();
5386 src = src_back;
5387 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5388 }
5389 }
5390
5391 #[test]
5392 #[cfg(target_os = "linux")]
5393 fn test_g2d_rotate() {
5394 if !is_g2d_available() {
5395 eprintln!("SKIPPED: test_g2d_rotate - G2D library (libg2d.so.2) not available");
5396 return;
5397 }
5398 if !is_dma_available() {
5399 eprintln!(
5400 "SKIPPED: test_g2d_rotate - DMA memory allocation not available (permission denied or no DMA-BUF support)"
5401 );
5402 return;
5403 }
5404
5405 let size = (1280, 720);
5406 for rot in [
5407 Rotation::Clockwise90,
5408 Rotation::Rotate180,
5409 Rotation::CounterClockwise90,
5410 ] {
5411 test_g2d_rotate_(size, rot);
5412 }
5413 }
5414
5415 #[cfg(target_os = "linux")]
5416 fn test_g2d_rotate_(size: (usize, usize), rot: Rotation) {
5417 let (dst_width, dst_height) = match rot {
5418 Rotation::None | Rotation::Rotate180 => size,
5419 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
5420 };
5421
5422 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5423 let src =
5424 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), Some(TensorMemory::Dma))
5425 .unwrap();
5426
5427 let cpu_dst = TensorDyn::image(
5428 dst_width,
5429 dst_height,
5430 PixelFormat::Rgba,
5431 DType::U8,
5432 None,
5433 edgefirst_tensor::CpuAccess::ReadWrite,
5434 )
5435 .unwrap();
5436 let mut cpu_converter = CPUProcessor::new();
5437
5438 let (result, src, cpu_dst) = convert_img(
5439 &mut cpu_converter,
5440 src,
5441 cpu_dst,
5442 rot,
5443 Flip::None,
5444 Crop::no_crop(),
5445 );
5446 result.unwrap();
5447
5448 let g2d_dst = TensorDyn::image(
5449 dst_width,
5450 dst_height,
5451 PixelFormat::Rgba,
5452 DType::U8,
5453 Some(TensorMemory::Dma),
5454 edgefirst_tensor::CpuAccess::ReadWrite,
5455 )
5456 .unwrap();
5457 let mut g2d_converter = G2DProcessor::new().unwrap();
5458
5459 let (result, _src, g2d_dst) = convert_img(
5460 &mut g2d_converter,
5461 src,
5462 g2d_dst,
5463 rot,
5464 Flip::None,
5465 Crop::no_crop(),
5466 );
5467 result.unwrap();
5468
5469 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
5475 }
5476
5477 #[test]
5478 fn test_rgba_to_yuyv_resize_cpu() {
5479 let src = load_bytes_to_tensor(
5480 1280,
5481 720,
5482 PixelFormat::Rgba,
5483 None,
5484 &edgefirst_bench::testdata::read("camera720p.rgba"),
5485 )
5486 .unwrap();
5487
5488 let (dst_width, dst_height) = (640, 360);
5489
5490 let dst = TensorDyn::image(
5491 dst_width,
5492 dst_height,
5493 PixelFormat::Yuyv,
5494 DType::U8,
5495 None,
5496 edgefirst_tensor::CpuAccess::ReadWrite,
5497 )
5498 .unwrap();
5499
5500 let dst_through_yuyv = TensorDyn::image(
5501 dst_width,
5502 dst_height,
5503 PixelFormat::Rgba,
5504 DType::U8,
5505 None,
5506 edgefirst_tensor::CpuAccess::ReadWrite,
5507 )
5508 .unwrap();
5509 let dst_direct = TensorDyn::image(
5510 dst_width,
5511 dst_height,
5512 PixelFormat::Rgba,
5513 DType::U8,
5514 None,
5515 edgefirst_tensor::CpuAccess::ReadWrite,
5516 )
5517 .unwrap();
5518
5519 let mut cpu_converter = CPUProcessor::new();
5520
5521 let (result, src, dst) = convert_img(
5522 &mut cpu_converter,
5523 src,
5524 dst,
5525 Rotation::None,
5526 Flip::None,
5527 Crop::no_crop(),
5528 );
5529 result.unwrap();
5530
5531 let (result, _dst, dst_through_yuyv) = convert_img(
5532 &mut cpu_converter,
5533 dst,
5534 dst_through_yuyv,
5535 Rotation::None,
5536 Flip::None,
5537 Crop::no_crop(),
5538 );
5539 result.unwrap();
5540
5541 let (result, _src, dst_direct) = convert_img(
5542 &mut cpu_converter,
5543 src,
5544 dst_direct,
5545 Rotation::None,
5546 Flip::None,
5547 Crop::no_crop(),
5548 );
5549 result.unwrap();
5550
5551 compare_images(&dst_through_yuyv, &dst_direct, 0.98, function!());
5552 }
5553
5554 #[test]
5555 #[cfg(target_os = "linux")]
5556 #[cfg(feature = "opengl")]
5557 #[ignore = "opengl doesn't support rendering to PixelFormat::Yuyv texture"]
5558 fn test_rgba_to_yuyv_resize_opengl() {
5559 if !is_opengl_available() {
5560 eprintln!("SKIPPED: {} - OpenGL not available", function!());
5561 return;
5562 }
5563
5564 if !is_dma_available() {
5565 eprintln!(
5566 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
5567 function!()
5568 );
5569 return;
5570 }
5571
5572 let src = load_bytes_to_tensor(
5573 1280,
5574 720,
5575 PixelFormat::Rgba,
5576 None,
5577 &edgefirst_bench::testdata::read("camera720p.rgba"),
5578 )
5579 .unwrap();
5580
5581 let (dst_width, dst_height) = (640, 360);
5582
5583 let dst = TensorDyn::image(
5584 dst_width,
5585 dst_height,
5586 PixelFormat::Yuyv,
5587 DType::U8,
5588 Some(TensorMemory::Dma),
5589 edgefirst_tensor::CpuAccess::ReadWrite,
5590 )
5591 .unwrap();
5592
5593 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5594
5595 let (result, src, dst) = convert_img(
5596 &mut gl_converter,
5597 src,
5598 dst,
5599 Rotation::None,
5600 Flip::None,
5601 Crop::letterbox([255, 255, 255, 255]),
5602 );
5603 result.unwrap();
5604
5605 std::fs::write(
5606 "rgba_to_yuyv_opengl.yuyv",
5607 dst.as_u8().unwrap().map().unwrap().as_slice(),
5608 )
5609 .unwrap();
5610 let cpu_dst = TensorDyn::image(
5611 dst_width,
5612 dst_height,
5613 PixelFormat::Yuyv,
5614 DType::U8,
5615 Some(TensorMemory::Dma),
5616 edgefirst_tensor::CpuAccess::ReadWrite,
5617 )
5618 .unwrap();
5619 let (result, _src, cpu_dst) = convert_img(
5620 &mut CPUProcessor::new(),
5621 src,
5622 cpu_dst,
5623 Rotation::None,
5624 Flip::None,
5625 Crop::no_crop(),
5626 );
5627 result.unwrap();
5628
5629 compare_images_convert_to_rgb(&dst, &cpu_dst, 0.98, function!());
5630 }
5631
5632 #[test]
5633 #[cfg(target_os = "linux")]
5634 fn test_rgba_to_yuyv_resize_g2d() {
5635 if !is_g2d_available() {
5636 eprintln!(
5637 "SKIPPED: test_rgba_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available"
5638 );
5639 return;
5640 }
5641 if !is_dma_available() {
5642 eprintln!(
5643 "SKIPPED: test_rgba_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
5644 );
5645 return;
5646 }
5647
5648 let src = load_bytes_to_tensor(
5649 1280,
5650 720,
5651 PixelFormat::Rgba,
5652 Some(TensorMemory::Dma),
5653 &edgefirst_bench::testdata::read("camera720p.rgba"),
5654 )
5655 .unwrap();
5656
5657 let (dst_width, dst_height) = (1280, 720);
5658
5659 let cpu_dst = TensorDyn::image(
5660 dst_width,
5661 dst_height,
5662 PixelFormat::Yuyv,
5663 DType::U8,
5664 Some(TensorMemory::Dma),
5665 edgefirst_tensor::CpuAccess::ReadWrite,
5666 )
5667 .unwrap();
5668
5669 let g2d_dst = TensorDyn::image(
5670 dst_width,
5671 dst_height,
5672 PixelFormat::Yuyv,
5673 DType::U8,
5674 Some(TensorMemory::Dma),
5675 edgefirst_tensor::CpuAccess::ReadWrite,
5676 )
5677 .unwrap();
5678
5679 let mut g2d_converter = G2DProcessor::new().unwrap();
5680 let crop = Crop::new();
5681
5682 g2d_dst
5683 .as_u8()
5684 .unwrap()
5685 .map()
5686 .unwrap()
5687 .as_mut_slice()
5688 .fill(128);
5689 let (result, src, g2d_dst) = convert_img(
5690 &mut g2d_converter,
5691 src,
5692 g2d_dst,
5693 Rotation::None,
5694 Flip::None,
5695 crop,
5696 );
5697 result.unwrap();
5698
5699 let cpu_dst_img = cpu_dst;
5700 cpu_dst_img
5701 .as_u8()
5702 .unwrap()
5703 .map()
5704 .unwrap()
5705 .as_mut_slice()
5706 .fill(128);
5707 let (result, _src, cpu_dst) = convert_img(
5708 &mut CPUProcessor::new(),
5709 src,
5710 cpu_dst_img,
5711 Rotation::None,
5712 Flip::None,
5713 crop,
5714 );
5715 result.unwrap();
5716
5717 compare_images_convert_to_rgb(&cpu_dst, &g2d_dst, 0.98, function!());
5718 }
5719
5720 #[test]
5721 fn test_yuyv_to_rgba_cpu() {
5722 let file = edgefirst_bench::testdata::read("camera720p.yuyv").to_vec();
5723 let src = TensorDyn::image(
5724 1280,
5725 720,
5726 PixelFormat::Yuyv,
5727 DType::U8,
5728 None,
5729 edgefirst_tensor::CpuAccess::ReadWrite,
5730 )
5731 .unwrap();
5732 src.as_u8()
5733 .unwrap()
5734 .map()
5735 .unwrap()
5736 .as_mut_slice()
5737 .copy_from_slice(&file);
5738
5739 let dst = TensorDyn::image(
5740 1280,
5741 720,
5742 PixelFormat::Rgba,
5743 DType::U8,
5744 None,
5745 edgefirst_tensor::CpuAccess::ReadWrite,
5746 )
5747 .unwrap();
5748 let mut cpu_converter = CPUProcessor::new();
5749
5750 let (result, _src, dst) = convert_img(
5751 &mut cpu_converter,
5752 src,
5753 dst,
5754 Rotation::None,
5755 Flip::None,
5756 Crop::no_crop(),
5757 );
5758 result.unwrap();
5759
5760 let target_image = TensorDyn::image(
5761 1280,
5762 720,
5763 PixelFormat::Rgba,
5764 DType::U8,
5765 None,
5766 edgefirst_tensor::CpuAccess::ReadWrite,
5767 )
5768 .unwrap();
5769 target_image
5770 .as_u8()
5771 .unwrap()
5772 .map()
5773 .unwrap()
5774 .as_mut_slice()
5775 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
5776
5777 compare_images(&dst, &target_image, 0.98, function!());
5780 }
5781
5782 #[test]
5783 fn test_yuyv_to_rgb_cpu() {
5784 let file = edgefirst_bench::testdata::read("camera720p.yuyv").to_vec();
5785 let src = TensorDyn::image(
5786 1280,
5787 720,
5788 PixelFormat::Yuyv,
5789 DType::U8,
5790 None,
5791 edgefirst_tensor::CpuAccess::ReadWrite,
5792 )
5793 .unwrap();
5794 src.as_u8()
5795 .unwrap()
5796 .map()
5797 .unwrap()
5798 .as_mut_slice()
5799 .copy_from_slice(&file);
5800
5801 let dst = TensorDyn::image(
5802 1280,
5803 720,
5804 PixelFormat::Rgb,
5805 DType::U8,
5806 None,
5807 edgefirst_tensor::CpuAccess::ReadWrite,
5808 )
5809 .unwrap();
5810 let mut cpu_converter = CPUProcessor::new();
5811
5812 let (result, _src, dst) = convert_img(
5813 &mut cpu_converter,
5814 src,
5815 dst,
5816 Rotation::None,
5817 Flip::None,
5818 Crop::no_crop(),
5819 );
5820 result.unwrap();
5821
5822 let target_image = TensorDyn::image(
5823 1280,
5824 720,
5825 PixelFormat::Rgb,
5826 DType::U8,
5827 None,
5828 edgefirst_tensor::CpuAccess::ReadWrite,
5829 )
5830 .unwrap();
5831 target_image
5832 .as_u8()
5833 .unwrap()
5834 .map()
5835 .unwrap()
5836 .as_mut_slice()
5837 .as_chunks_mut::<3>()
5838 .0
5839 .iter_mut()
5840 .zip(
5841 edgefirst_bench::testdata::read("camera720p.rgba")
5842 .as_chunks::<4>()
5843 .0,
5844 )
5845 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
5846
5847 compare_images(&dst, &target_image, 0.98, function!());
5850 }
5851
5852 #[test]
5853 #[cfg(target_os = "linux")]
5854 fn test_yuyv_to_rgba_g2d() {
5855 if !is_g2d_available() {
5856 eprintln!("SKIPPED: test_yuyv_to_rgba_g2d - G2D library (libg2d.so.2) not available");
5857 return;
5858 }
5859 if !is_dma_available() {
5860 eprintln!(
5861 "SKIPPED: test_yuyv_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
5862 );
5863 return;
5864 }
5865
5866 let src = load_bytes_to_tensor(
5867 1280,
5868 720,
5869 PixelFormat::Yuyv,
5870 None,
5871 &edgefirst_bench::testdata::read("camera720p.yuyv"),
5872 )
5873 .unwrap();
5874
5875 let dst = TensorDyn::image(
5876 1280,
5877 720,
5878 PixelFormat::Rgba,
5879 DType::U8,
5880 Some(TensorMemory::Dma),
5881 edgefirst_tensor::CpuAccess::ReadWrite,
5882 )
5883 .unwrap();
5884 let mut g2d_converter = G2DProcessor::new().unwrap();
5885
5886 let (result, _src, dst) = convert_img(
5887 &mut g2d_converter,
5888 src,
5889 dst,
5890 Rotation::None,
5891 Flip::None,
5892 Crop::no_crop(),
5893 );
5894 result.unwrap();
5895
5896 let target_image = TensorDyn::image(
5897 1280,
5898 720,
5899 PixelFormat::Rgba,
5900 DType::U8,
5901 None,
5902 edgefirst_tensor::CpuAccess::ReadWrite,
5903 )
5904 .unwrap();
5905 target_image
5906 .as_u8()
5907 .unwrap()
5908 .map()
5909 .unwrap()
5910 .as_mut_slice()
5911 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
5912
5913 compare_images(&dst, &target_image, 0.98, function!());
5917 }
5918
5919 #[test]
5920 #[cfg(target_os = "linux")]
5921 #[cfg(feature = "opengl")]
5922 fn test_yuyv_to_rgba_opengl() {
5923 if !is_opengl_available() {
5924 eprintln!("SKIPPED: {} - OpenGL not available", function!());
5925 return;
5926 }
5927 if !is_dma_available() {
5928 eprintln!(
5929 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
5930 function!()
5931 );
5932 return;
5933 }
5934
5935 let src = load_bytes_to_tensor(
5936 1280,
5937 720,
5938 PixelFormat::Yuyv,
5939 Some(TensorMemory::Dma),
5940 &edgefirst_bench::testdata::read("camera720p.yuyv"),
5941 )
5942 .unwrap();
5943
5944 let dst = TensorDyn::image(
5945 1280,
5946 720,
5947 PixelFormat::Rgba,
5948 DType::U8,
5949 Some(TensorMemory::Dma),
5950 edgefirst_tensor::CpuAccess::ReadWrite,
5951 )
5952 .unwrap();
5953 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5954
5955 let (result, _src, dst) = convert_img(
5956 &mut gl_converter,
5957 src,
5958 dst,
5959 Rotation::None,
5960 Flip::None,
5961 Crop::no_crop(),
5962 );
5963 result.unwrap();
5964
5965 let target_image = TensorDyn::image(
5966 1280,
5967 720,
5968 PixelFormat::Rgba,
5969 DType::U8,
5970 None,
5971 edgefirst_tensor::CpuAccess::ReadWrite,
5972 )
5973 .unwrap();
5974 target_image
5975 .as_u8()
5976 .unwrap()
5977 .map()
5978 .unwrap()
5979 .as_mut_slice()
5980 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
5981
5982 compare_images(&dst, &target_image, 0.98, function!());
5986 }
5987
5988 #[test]
5998 #[cfg(target_os = "macos")]
5999 #[cfg(feature = "opengl")]
6000 fn test_grey_r8_iosurface_to_rgba_opengl_macos() {
6001 let mut proc = match GLProcessorThreaded::new(None) {
6002 Ok(p) => p,
6003 Err(e) => {
6004 eprintln!("SKIPPED: {} — GL engine init failed ({e:?})", function!());
6005 return;
6006 }
6007 };
6008
6009 let (w, h) = (16usize, 16usize);
6010 let src = TensorDyn::image(
6011 w,
6012 h,
6013 PixelFormat::Grey,
6014 DType::U8,
6015 Some(TensorMemory::Dma),
6016 edgefirst_tensor::CpuAccess::ReadWrite,
6017 )
6018 .expect("GREY IOSurface (R8/L008) should allocate — proves the FourCC mapping");
6019 {
6021 let su8 = src.as_u8().unwrap();
6022 let stride = src.as_u8().unwrap().effective_row_stride().unwrap();
6023 let mut m = su8.map().unwrap();
6024 let buf = m.as_mut_slice();
6025 for y in 0..h {
6026 for x in 0..w {
6027 buf[y * stride + x] = ((x * 13 + y * 7) & 0xff) as u8;
6028 }
6029 }
6030 }
6031
6032 let dst = TensorDyn::image(
6033 w,
6034 h,
6035 PixelFormat::Rgba,
6036 DType::U8,
6037 Some(TensorMemory::Dma),
6038 edgefirst_tensor::CpuAccess::ReadWrite,
6039 )
6040 .unwrap();
6041 let (result, src_back, dst) = convert_img(
6042 &mut proc,
6043 src,
6044 dst,
6045 Rotation::None,
6046 Flip::None,
6047 Crop::no_crop(),
6048 );
6049 result.expect("GREY(R8 IOSurface) → RGBA must convert on ANGLE (R8 binding works)");
6050
6051 let src_stride = src_back.as_u8().unwrap().effective_row_stride().unwrap();
6052 let src_map = src_back.as_u8().unwrap().map().unwrap();
6053 let sbytes = src_map.as_slice();
6054 let dst_stride = dst.as_u8().unwrap().effective_row_stride().unwrap();
6055 let dst_map = dst.as_u8().unwrap().map().unwrap();
6056 let dbytes = dst_map.as_slice();
6057 for y in 0..h {
6058 for x in 0..w {
6059 let yv = sbytes[y * src_stride + x] as i16;
6060 let p = y * dst_stride + x * 4;
6061 for c in 0..3 {
6062 assert!(
6063 (dbytes[p + c] as i16 - yv).abs() <= 2,
6064 "pixel ({x},{y}) ch{c} = {} expected ~{yv} (GREY→RGB identity)",
6065 dbytes[p + c]
6066 );
6067 }
6068 }
6069 }
6070 }
6071
6072 #[test]
6078 #[cfg(target_os = "macos")]
6079 #[cfg(feature = "opengl")]
6080 fn test_nv12_to_planar_f16_two_pass_opengl_macos() {
6081 let mut gpu = match GLProcessorThreaded::new(None) {
6082 Ok(p) => p,
6083 Err(e) => {
6084 eprintln!("SKIPPED: {} — init failed ({e:?})", function!());
6085 return;
6086 }
6087 };
6088 let (w, h) = (64usize, 64usize);
6089 let src = match TensorDyn::image(
6090 w,
6091 h,
6092 PixelFormat::Nv12,
6093 DType::U8,
6094 Some(TensorMemory::Dma),
6095 edgefirst_tensor::CpuAccess::ReadWrite,
6096 ) {
6097 Ok(t) => t,
6098 Err(e) => {
6099 eprintln!("SKIPPED: {} — NV12 IOSurface alloc: {e:?}", function!());
6100 return;
6101 }
6102 };
6103 src.as_u8().unwrap().map().unwrap().as_mut_slice().fill(128); let dst = match TensorDyn::image(
6106 w,
6107 h,
6108 PixelFormat::PlanarRgb,
6109 DType::F16,
6110 Some(TensorMemory::Dma),
6111 edgefirst_tensor::CpuAccess::ReadWrite,
6112 ) {
6113 Ok(t) => t,
6114 Err(e) => {
6115 eprintln!("SKIPPED: {} — F16 PlanarRgb IOSurface: {e:?}", function!());
6116 return;
6117 }
6118 };
6119 let mut dst = dst;
6120 if let Err(e) = ImageProcessorTrait::convert(
6122 &mut gpu,
6123 &src,
6124 &mut dst,
6125 Rotation::None,
6126 Flip::None,
6127 Crop::no_crop(),
6128 ) {
6129 eprintln!(
6133 "SKIPPED: {} — NV12→PlanarRgb F16 not available ({e:?})",
6134 function!()
6135 );
6136 return;
6137 }
6138 let dt = dst.as_f16().expect("dst is F16 PlanarRgb");
6139 let map = dt.map().unwrap();
6140 let vals = map.as_slice();
6141 let mut checked = 0usize;
6144 for &v in vals.iter() {
6145 let f = f32::from(v);
6146 assert!(
6147 (0.40..=0.60).contains(&f),
6148 "planar F16 value {f} not ~0.5 for neutral-grey NV12"
6149 );
6150 checked += 1;
6151 }
6152 assert!(
6153 checked >= w * h * 3,
6154 "expected >= 3 planes of samples, got {checked}"
6155 );
6156 }
6157
6158 #[test]
6166 #[cfg(target_os = "macos")]
6167 #[cfg(feature = "opengl")]
6168 fn test_nv12_to_planar_f16_two_pass_pool_opengl_macos() {
6169 let mut gpu = match GLProcessorThreaded::new(None) {
6170 Ok(p) => p,
6171 Err(e) => {
6172 eprintln!("SKIPPED: {} — init failed ({e:?})", function!());
6173 return;
6174 }
6175 };
6176 let (fw, fh) = (96usize, 64usize);
6178 let (pool_w, pool_h) = (256usize, 768usize);
6179 let (model_w, model_h) = (128usize, 128usize);
6180
6181 let mut src = match TensorDyn::image(
6182 pool_w,
6183 pool_h,
6184 PixelFormat::Grey,
6185 DType::U8,
6186 Some(TensorMemory::Dma),
6187 edgefirst_tensor::CpuAccess::ReadWrite,
6188 ) {
6189 Ok(t) => t,
6190 Err(e) => {
6191 eprintln!("SKIPPED: {} — R8 pool alloc: {e:?}", function!());
6192 return;
6193 }
6194 };
6195 src.configure_image(fw, fh, PixelFormat::Nv12)
6196 .unwrap_or_else(|e| panic!("configure_image NV12 on pool: {e}"));
6197 let stride = src.as_u8().unwrap().effective_row_stride().unwrap();
6198 src.as_u8().unwrap().map().unwrap().as_mut_slice().fill(128); let mut dst = match TensorDyn::image(
6201 model_w,
6202 model_h,
6203 PixelFormat::PlanarRgb,
6204 DType::F16,
6205 Some(TensorMemory::Dma),
6206 edgefirst_tensor::CpuAccess::ReadWrite,
6207 ) {
6208 Ok(t) => t,
6209 Err(e) => {
6210 eprintln!("SKIPPED: {} — F16 PlanarRgb dst: {e:?}", function!());
6211 return;
6212 }
6213 };
6214
6215 let _ = model_w;
6217 let crop = Crop::new()
6218 .with_source(Some(Region::new(0, 0, fw, fh)))
6219 .with_fit(Fit::Letterbox {
6220 pad: [0, 0, 0, 255],
6221 });
6222 if let Err(e) =
6223 ImageProcessorTrait::convert(&mut gpu, &src, &mut dst, Rotation::None, Flip::None, crop)
6224 {
6225 eprintln!(
6226 "SKIPPED: {} — NV12→PlanarRgb F16 unavailable ({e:?})",
6227 function!()
6228 );
6229 return;
6230 }
6231 let _ = stride;
6232 let dt = dst.as_f16().expect("dst F16");
6235 let map = dt.map().unwrap();
6236 let any_half = map.as_slice().iter().any(|&v| {
6237 let f = f32::from(v);
6238 (0.40..=0.60).contains(&f)
6239 });
6240 assert!(any_half, "expected ~0.5 grey samples in the letterbox band");
6241 }
6242
6243 #[test]
6249 #[cfg(target_os = "macos")]
6250 #[cfg(feature = "opengl")]
6251 fn test_nv12_to_planar_f16_cross_thread_opengl_macos() {
6252 use std::sync::mpsc;
6253 let mut proc = match ImageProcessor::new() {
6256 Ok(p) => p,
6257 Err(e) => {
6258 eprintln!("SKIPPED: {} — init failed ({e:?})", function!());
6259 return;
6260 }
6261 };
6262 let (fw, fh) = (96usize, 64usize);
6263 let mut src = match TensorDyn::image(
6264 256,
6265 768,
6266 PixelFormat::Grey,
6267 DType::U8,
6268 Some(TensorMemory::Dma),
6269 edgefirst_tensor::CpuAccess::ReadWrite,
6270 ) {
6271 Ok(t) => t,
6272 Err(e) => {
6273 eprintln!("SKIPPED: {} — pool: {e:?}", function!());
6274 return;
6275 }
6276 };
6277 src.configure_image(fw, fh, PixelFormat::Nv12).unwrap();
6278 src.as_u8().unwrap().map().unwrap().as_mut_slice().fill(128);
6279 let mut dst = match TensorDyn::image(
6280 128,
6281 128,
6282 PixelFormat::PlanarRgb,
6283 DType::F16,
6284 Some(TensorMemory::Dma),
6285 edgefirst_tensor::CpuAccess::ReadWrite,
6286 ) {
6287 Ok(t) => t,
6288 Err(e) => {
6289 eprintln!("SKIPPED: {} — dst: {e:?}", function!());
6290 return;
6291 }
6292 };
6293 let crop = Crop::new().with_source(Some(Region::new(0, 0, fw, fh)));
6294
6295 let (tx, rx) = mpsc::channel::<bool>();
6298 let worker = std::thread::spawn(move || {
6299 let _ = ImageProcessorTrait::convert(
6300 &mut proc,
6301 &src,
6302 &mut dst,
6303 Rotation::None,
6304 Flip::None,
6305 crop,
6306 );
6307 let _ = tx.send(true);
6308 });
6309 match rx.recv_timeout(std::time::Duration::from_secs(20)) {
6310 Ok(_) => { let _ = worker.join(); }
6311 Err(_) => panic!(
6312 "cross-thread NV12→PlanarRgb convert HUNG (>20s) — reproduces the orchestrator deadlock"
6313 ),
6314 }
6315 }
6316
6317 #[test]
6324 #[cfg(target_os = "macos")]
6325 #[cfg(feature = "opengl")]
6326 fn test_nv_to_planar_f16_varying_sizes_no_leak_opengl_macos() {
6327 let mut gpu = match GLProcessorThreaded::new(None) {
6328 Ok(p) => p,
6329 Err(e) => {
6330 eprintln!("SKIPPED: {} — init failed ({e:?})", function!());
6331 return;
6332 }
6333 };
6334 let (max_w, max_h) = (640usize, 640usize);
6337 let depth = 4usize;
6338 let mut srcs = Vec::new();
6339 let mut dsts = Vec::new();
6340 for _ in 0..depth {
6341 srcs.push(
6342 match TensorDyn::image(
6343 max_w,
6344 max_h * 3,
6345 PixelFormat::Grey,
6346 DType::U8,
6347 Some(TensorMemory::Dma),
6348 edgefirst_tensor::CpuAccess::ReadWrite,
6349 ) {
6350 Ok(t) => t,
6351 Err(e) => {
6352 eprintln!("SKIPPED: {} — pool: {e:?}", function!());
6353 return;
6354 }
6355 },
6356 );
6357 dsts.push(
6358 match TensorDyn::image(
6359 640,
6360 640,
6361 PixelFormat::PlanarRgb,
6362 DType::F16,
6363 Some(TensorMemory::Dma),
6364 edgefirst_tensor::CpuAccess::ReadWrite,
6365 ) {
6366 Ok(t) => t,
6367 Err(e) => {
6368 eprintln!("SKIPPED: {} — dst: {e:?}", function!());
6369 return;
6370 }
6371 },
6372 );
6373 }
6374 let sizes = [
6376 (640, 480),
6377 (500, 375),
6378 (640, 427),
6379 (333, 500),
6380 (480, 640),
6381 (612, 612),
6382 (428, 640),
6383 (576, 432),
6384 ];
6385 let mut first_ms = 0f64;
6386 let mut last_ms = 0f64;
6387 let iters = 40usize;
6388 for i in 0..iters {
6389 let (fw, fh) = sizes[i % sizes.len()];
6390 let src = &mut srcs[i % depth];
6391 let dst = &mut dsts[i % depth];
6392 src.configure_image(fw, fh, PixelFormat::Nv24).unwrap();
6393 src.as_u8().unwrap().map().unwrap().as_mut_slice().fill(128);
6394 let crop = Crop::new().with_source(Some(Region::new(0, 0, fw, fh)));
6395 let t0 = std::time::Instant::now();
6396 ImageProcessorTrait::convert(&mut gpu, src, dst, Rotation::None, Flip::None, crop)
6397 .unwrap_or_else(|e| panic!("convert iter {i} ({fw}×{fh}): {e}"));
6398 let ms = t0.elapsed().as_secs_f64() * 1e3;
6399 if i == 2 {
6400 first_ms = ms;
6401 }
6402 if i == iters - 1 {
6403 last_ms = ms;
6404 }
6405 }
6406 eprintln!("first={first_ms:.2}ms last={last_ms:.2}ms");
6407 assert!(
6408 last_ms < first_ms * 5.0 + 5.0,
6409 "convert latency ran away: first {first_ms:.2}ms → last {last_ms:.2}ms (intermediate/pbuffer leak)"
6410 );
6411 }
6412
6413 #[test]
6420 #[cfg(target_os = "macos")]
6421 #[cfg(feature = "opengl")]
6422 fn test_nv12_nv16_nv24_to_rgba_opengl_macos() {
6423 let mut gpu = match GLProcessorThreaded::new(None) {
6424 Ok(p) => p,
6425 Err(e) => {
6426 eprintln!("SKIPPED: {} — GL engine init failed ({e:?})", function!());
6427 return;
6428 }
6429 };
6430 let mut cpu = CPUProcessor::new();
6431
6432 let fill = |buf: &mut [u8], stride: usize, fmt: PixelFormat, w: usize, h: usize| {
6444 for y in 0..h {
6445 for x in 0..w {
6446 buf[y * stride + x] = ((x * 9 + y * 5) & 0xff) as u8;
6447 }
6448 }
6449 let (cw, ch, uv_grid_rows) = match fmt {
6450 PixelFormat::Nv12 => (w / 2, h / 2, 1usize),
6451 PixelFormat::Nv16 => (w / 2, h, 1usize),
6452 _ => (w, h, 2usize), };
6454 let uv_plane = h * stride;
6455 for cy in 0..ch {
6456 for cx in 0..cw {
6457 let off = uv_plane + cy * uv_grid_rows * stride + cx * 2;
6458 buf[off] = ((cx * 11 + 30) & 0xff) as u8;
6459 buf[off + 1] = ((cy * 7 + 200) & 0xff) as u8;
6460 }
6461 }
6462 };
6463
6464 for fmt in [PixelFormat::Nv12, PixelFormat::Nv16, PixelFormat::Nv24] {
6465 for (w, h) in [
6466 (16usize, 16usize), (15, 16), (16, 15), ] {
6470 let mem = TensorDyn::image(
6471 w,
6472 h,
6473 fmt,
6474 DType::U8,
6475 None,
6476 edgefirst_tensor::CpuAccess::ReadWrite,
6477 )
6478 .unwrap();
6479 let mem_stride = mem.as_u8().unwrap().effective_row_stride().unwrap();
6480 fill(
6481 mem.as_u8().unwrap().map().unwrap().as_mut_slice(),
6482 mem_stride,
6483 fmt,
6484 w,
6485 h,
6486 );
6487 let cpu_dst = TensorDyn::image(
6488 w,
6489 h,
6490 PixelFormat::Rgba,
6491 DType::U8,
6492 None,
6493 edgefirst_tensor::CpuAccess::ReadWrite,
6494 )
6495 .unwrap();
6496 let (r, _s, cpu_dst) = convert_img(
6497 &mut cpu,
6498 mem,
6499 cpu_dst,
6500 Rotation::None,
6501 Flip::None,
6502 Crop::no_crop(),
6503 );
6504 r.unwrap_or_else(|e| panic!("CPU {fmt:?}->{w}x{h}->RGBA: {e}"));
6505
6506 let ios = TensorDyn::image(
6507 w,
6508 h,
6509 fmt,
6510 DType::U8,
6511 Some(TensorMemory::Dma),
6512 edgefirst_tensor::CpuAccess::ReadWrite,
6513 )
6514 .unwrap_or_else(|e| panic!("{fmt:?} {w}x{h} IOSurface alloc: {e}"));
6515 let ios_stride = ios.as_u8().unwrap().effective_row_stride().unwrap();
6516 fill(
6517 ios.as_u8().unwrap().map().unwrap().as_mut_slice(),
6518 ios_stride,
6519 fmt,
6520 w,
6521 h,
6522 );
6523 let gpu_dst = TensorDyn::image(
6524 w,
6525 h,
6526 PixelFormat::Rgba,
6527 DType::U8,
6528 Some(TensorMemory::Dma),
6529 edgefirst_tensor::CpuAccess::ReadWrite,
6530 )
6531 .unwrap();
6532 let (r, _s, gpu_dst) = convert_img(
6533 &mut gpu,
6534 ios,
6535 gpu_dst,
6536 Rotation::None,
6537 Flip::None,
6538 Crop::no_crop(),
6539 );
6540 r.unwrap_or_else(|e| panic!("GPU {fmt:?}->{w}x{h}->RGBA on ANGLE: {e}"));
6541
6542 let cs = cpu_dst.as_u8().unwrap().effective_row_stride().unwrap();
6543 let cmap = cpu_dst.as_u8().unwrap().map().unwrap();
6544 let cb = cmap.as_slice();
6545 let gs = gpu_dst.as_u8().unwrap().effective_row_stride().unwrap();
6546 let gmap = gpu_dst.as_u8().unwrap().map().unwrap();
6547 let gb = gmap.as_slice();
6548 let mut max_d = 0i16;
6549 for y in 0..h {
6550 for x in 0..w {
6551 for c in 0..3 {
6552 let cv = cb[y * cs + x * 4 + c] as i16;
6553 let gv = gb[y * gs + x * 4 + c] as i16;
6554 max_d = max_d.max((cv - gv).abs());
6555 }
6556 }
6557 }
6558 assert!(
6559 max_d <= 3,
6560 "{fmt:?} {w}x{h}: GPU vs CPU RGBA max channel diff {max_d} > 3"
6561 );
6562 }
6563 }
6564 }
6565
6566 #[test]
6574 #[cfg(target_os = "macos")]
6575 #[cfg(feature = "opengl")]
6576 fn test_nv_to_rgba_larger_pool_surface_opengl_macos() {
6577 let mut gpu = match GLProcessorThreaded::new(None) {
6578 Ok(p) => p,
6579 Err(e) => {
6580 eprintln!("SKIPPED: {} — GL engine init failed ({e:?})", function!());
6581 return;
6582 }
6583 };
6584 let mut cpu = CPUProcessor::new();
6585 let (pool_w, pool_h) = (256usize, 256usize);
6588
6589 let fill = |buf: &mut [u8], stride: usize, fmt: PixelFormat, w: usize, h: usize| {
6593 for y in 0..h {
6594 for x in 0..w {
6595 buf[y * stride + x] = ((x * 9 + y * 5) & 0xff) as u8;
6596 }
6597 }
6598 let (cw, ch, uv_grid_rows) = match fmt {
6599 PixelFormat::Nv12 => (w / 2, h / 2, 1usize),
6600 PixelFormat::Nv16 => (w / 2, h, 1usize),
6601 _ => (w, h, 2usize), };
6603 let uv_plane = h * stride;
6604 for cy in 0..ch {
6605 for cx in 0..cw {
6606 let off = uv_plane + cy * uv_grid_rows * stride + cx * 2;
6607 buf[off] = ((cx * 11 + 30) & 0xff) as u8;
6608 buf[off + 1] = ((cy * 7 + 200) & 0xff) as u8;
6609 }
6610 }
6611 };
6612
6613 for fmt in [PixelFormat::Nv12, PixelFormat::Nv16, PixelFormat::Nv24] {
6614 for (w, h) in [
6615 (40usize, 24usize), (15, 16), (16, 15), ] {
6619 let ew = w.next_multiple_of(2);
6622
6623 let mem = TensorDyn::image(
6625 w,
6626 h,
6627 fmt,
6628 DType::U8,
6629 None,
6630 edgefirst_tensor::CpuAccess::ReadWrite,
6631 )
6632 .unwrap();
6633 let mem_stride = mem.as_u8().unwrap().effective_row_stride().unwrap();
6634 fill(
6635 mem.as_u8().unwrap().map().unwrap().as_mut_slice(),
6636 mem_stride,
6637 fmt,
6638 w,
6639 h,
6640 );
6641 let cpu_dst = TensorDyn::image(
6642 w,
6643 h,
6644 PixelFormat::Rgba,
6645 DType::U8,
6646 None,
6647 edgefirst_tensor::CpuAccess::ReadWrite,
6648 )
6649 .unwrap();
6650 let (r, _s, cpu_dst) = convert_img(
6651 &mut cpu,
6652 mem,
6653 cpu_dst,
6654 Rotation::None,
6655 Flip::None,
6656 Crop::no_crop(),
6657 );
6658 r.unwrap_or_else(|e| panic!("CPU {fmt:?}->{w}x{h}->RGBA: {e}"));
6659
6660 let mut ios = match TensorDyn::image(
6664 pool_w,
6665 pool_h,
6666 PixelFormat::Grey,
6667 DType::U8,
6668 Some(TensorMemory::Dma),
6669 edgefirst_tensor::CpuAccess::ReadWrite,
6670 ) {
6671 Ok(t) => t,
6672 Err(e) => {
6673 eprintln!("SKIPPED: {} — R8 pool IOSurface alloc: {e:?}", function!());
6674 return;
6675 }
6676 };
6677 ios.configure_image(w, h, fmt)
6678 .unwrap_or_else(|e| panic!("configure_image {fmt:?} {w}x{h} on pool: {e}"));
6679 let ios_stride = ios.as_u8().unwrap().effective_row_stride().unwrap();
6680 assert!(
6681 ios_stride > ew,
6682 "{fmt:?} {w}x{h}: pool stride {ios_stride} should exceed even width {ew} \
6683 (test must exercise padding)"
6684 );
6685 fill(
6686 ios.as_u8().unwrap().map().unwrap().as_mut_slice(),
6687 ios_stride,
6688 fmt,
6689 w,
6690 h,
6691 );
6692
6693 let gpu_dst = TensorDyn::image(
6694 w,
6695 h,
6696 PixelFormat::Rgba,
6697 DType::U8,
6698 Some(TensorMemory::Dma),
6699 edgefirst_tensor::CpuAccess::ReadWrite,
6700 )
6701 .unwrap();
6702 let (r, _s, gpu_dst) = convert_img(
6703 &mut gpu,
6704 ios,
6705 gpu_dst,
6706 Rotation::None,
6707 Flip::None,
6708 Crop::no_crop(),
6709 );
6710 r.unwrap_or_else(|e| {
6711 panic!("GPU {fmt:?}->{w}x{h}->RGBA (pool surface) on ANGLE: {e}")
6712 });
6713
6714 let cs = cpu_dst.as_u8().unwrap().effective_row_stride().unwrap();
6715 let cmap = cpu_dst.as_u8().unwrap().map().unwrap();
6716 let cb = cmap.as_slice();
6717 let gs = gpu_dst.as_u8().unwrap().effective_row_stride().unwrap();
6718 let gmap = gpu_dst.as_u8().unwrap().map().unwrap();
6719 let gb = gmap.as_slice();
6720 let mut max_d = 0i16;
6721 for y in 0..h {
6722 for x in 0..w {
6723 for c in 0..3 {
6724 let cv = cb[y * cs + x * 4 + c] as i16;
6725 let gv = gb[y * gs + x * 4 + c] as i16;
6726 max_d = max_d.max((cv - gv).abs());
6727 }
6728 }
6729 }
6730 assert!(
6731 max_d <= 3,
6732 "{fmt:?} {w}x{h}: GPU(pool surface) vs CPU RGBA max channel diff {max_d} > 3"
6733 );
6734 }
6735 }
6736 }
6737
6738 #[test]
6739 #[cfg(target_os = "macos")]
6740 #[cfg(feature = "opengl")]
6741 fn test_yuyv_to_rgba_opengl_macos() {
6742 let mut proc = match GLProcessorThreaded::new(None) {
6743 Ok(p) => p,
6744 Err(e) => {
6745 eprintln!(
6746 "SKIPPED: {} — GL engine init failed ({e:?}). \
6747 Install ANGLE via `brew install startergo/angle/angle` \
6748 and re-sign per README.md § macOS GPU Acceleration to \
6749 run this test.",
6750 function!()
6751 );
6752 return;
6753 }
6754 };
6755
6756 let src = load_bytes_to_tensor(
6757 1280,
6758 720,
6759 PixelFormat::Yuyv,
6760 Some(TensorMemory::Dma),
6761 &edgefirst_bench::testdata::read("camera720p.yuyv"),
6762 )
6763 .unwrap();
6764
6765 let dst = TensorDyn::image(
6766 1280,
6767 720,
6768 PixelFormat::Rgba,
6769 DType::U8,
6770 Some(TensorMemory::Dma),
6771 edgefirst_tensor::CpuAccess::ReadWrite,
6772 )
6773 .unwrap();
6774
6775 let (result, _src, dst) = convert_img(
6776 &mut proc,
6777 src,
6778 dst,
6779 Rotation::None,
6780 Flip::None,
6781 Crop::no_crop(),
6782 );
6783 result.unwrap();
6784
6785 let target_image = TensorDyn::image(
6786 1280,
6787 720,
6788 PixelFormat::Rgba,
6789 DType::U8,
6790 None,
6791 edgefirst_tensor::CpuAccess::ReadWrite,
6792 )
6793 .unwrap();
6794 target_image
6795 .as_u8()
6796 .unwrap()
6797 .map()
6798 .unwrap()
6799 .as_mut_slice()
6800 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
6801
6802 compare_images(&dst, &target_image, 0.98, function!());
6807 }
6808
6809 #[test]
6828 #[cfg(target_os = "macos")]
6829 #[cfg(feature = "opengl")]
6830 fn test_yuyv_to_rgba_opengl_macos_multi_resolution() {
6831 let mut proc = match GLProcessorThreaded::new(None) {
6832 Ok(p) => p,
6833 Err(e) => {
6834 eprintln!("SKIPPED: {} — GL engine init failed ({e:?})", function!());
6835 return;
6836 }
6837 };
6838
6839 for (w, h) in [(64usize, 32usize), (3840, 2160)] {
6840 let bytes_per_row = w * 2;
6843 let mut yuyv = vec![0u8; bytes_per_row * h];
6844 for chunk in yuyv.chunks_exact_mut(4) {
6845 chunk[0] = 128; chunk[1] = 128; chunk[2] = 128; chunk[3] = 128; }
6850
6851 let src = load_bytes_to_tensor(w, h, PixelFormat::Yuyv, Some(TensorMemory::Dma), &yuyv)
6852 .unwrap();
6853
6854 let dst = TensorDyn::image(
6855 w,
6856 h,
6857 PixelFormat::Rgba,
6858 DType::U8,
6859 Some(TensorMemory::Dma),
6860 edgefirst_tensor::CpuAccess::ReadWrite,
6861 )
6862 .unwrap();
6863
6864 let (result, _src, dst) = convert_img(
6865 &mut proc,
6866 src,
6867 dst,
6868 Rotation::None,
6869 Flip::None,
6870 Crop::no_crop(),
6871 );
6872 result.expect("GL convert should succeed at this resolution");
6873
6874 let dst_u8 = dst.as_u8().unwrap();
6879 let dst_map = dst_u8.map().unwrap();
6880 let dst_bytes = dst_map.as_slice();
6881 assert_eq!(dst_bytes.len(), w * h * 4, "RGBA byte count");
6882 for px in dst_bytes.chunks_exact(4) {
6883 for (i, &c) in px[..3].iter().enumerate() {
6884 assert!(
6885 (120..=140).contains(&c),
6886 "{}: channel {i} = {c} (expected ~128 ±12) at {w}×{h}",
6887 function!(),
6888 );
6889 }
6890 assert_eq!(px[3], 255, "alpha must be 1.0");
6891 }
6892 }
6893 }
6894
6895 #[test]
6905 #[cfg(target_os = "macos")]
6906 #[cfg(feature = "opengl")]
6907 fn test_macos_gl_pbuffer_cache_reuses_surfaces() {
6908 let mut proc = match GLProcessorThreaded::new(None) {
6909 Ok(p) => p,
6910 Err(e) => {
6911 eprintln!("SKIPPED: {} — GL engine init failed ({e:?})", function!());
6912 return;
6913 }
6914 };
6915
6916 let mut yuyv = vec![0u8; 64 * 32 * 2];
6918 for chunk in yuyv.chunks_exact_mut(4) {
6919 chunk[0] = 200;
6920 chunk[1] = 100;
6921 chunk[2] = 200;
6922 chunk[3] = 156;
6923 }
6924 let src = load_bytes_to_tensor(64, 32, PixelFormat::Yuyv, Some(TensorMemory::Dma), &yuyv)
6925 .unwrap();
6926 let dst = TensorDyn::image(
6927 64,
6928 32,
6929 PixelFormat::Rgba,
6930 DType::U8,
6931 Some(TensorMemory::Dma),
6932 edgefirst_tensor::CpuAccess::ReadWrite,
6933 )
6934 .unwrap();
6935
6936 let (r1, src, dst) = convert_img(
6937 &mut proc,
6938 src,
6939 dst,
6940 Rotation::None,
6941 Flip::None,
6942 Crop::no_crop(),
6943 );
6944 r1.unwrap();
6945 let first: Vec<u8> = dst.as_u8().unwrap().map().unwrap().as_slice().to_vec();
6946
6947 let (r2, _src, dst) = convert_img(
6948 &mut proc,
6949 src,
6950 dst,
6951 Rotation::None,
6952 Flip::None,
6953 Crop::no_crop(),
6954 );
6955 r2.unwrap();
6956 let second: Vec<u8> = dst.as_u8().unwrap().map().unwrap().as_slice().to_vec();
6957
6958 assert_eq!(first, second, "cache-hit conversion must be deterministic");
6959 }
6960
6961 #[test]
6969 #[cfg(target_os = "macos")]
6970 #[cfg(feature = "opengl")]
6971 fn test_macos_gl_pbuffer_cache_steady_state() {
6972 let mut proc = match GLProcessorThreaded::new(None) {
6973 Ok(p) => p,
6974 Err(e) => {
6975 eprintln!("SKIPPED: {} — GL engine init failed ({e:?})", function!());
6976 return;
6977 }
6978 };
6979
6980 let (w, h) = (64usize, 32usize);
6981 const POOL: usize = 3;
6982 const FRAMES: usize = 100;
6983
6984 let yuyv = vec![128u8; w * h * 2];
6985 let pool: Vec<TensorDyn> = (0..POOL)
6986 .map(|_| {
6987 load_bytes_to_tensor(w, h, PixelFormat::Yuyv, Some(TensorMemory::Dma), &yuyv)
6988 .unwrap()
6989 })
6990 .collect();
6991 let mut dst = TensorDyn::image(
6992 w,
6993 h,
6994 PixelFormat::Rgba,
6995 DType::U8,
6996 Some(TensorMemory::Dma),
6997 edgefirst_tensor::CpuAccess::ReadWrite,
6998 )
6999 .unwrap();
7000
7001 for src in pool.iter().cycle().take(POOL * 2) {
7003 proc.convert(src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
7004 .unwrap();
7005 }
7006 let warm = proc.egl_cache_stats().unwrap();
7007
7008 for src in pool.iter().cycle().take(FRAMES) {
7009 proc.convert(src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
7010 .unwrap();
7011 }
7012 let steady = proc.egl_cache_stats().unwrap();
7013
7014 assert_eq!(
7015 warm.total_misses(),
7016 steady.total_misses(),
7017 "steady-state loop created new imports (warm {warm:?}, steady {steady:?})"
7018 );
7019 let hits = |s: &GlCacheStats| s.src.hits + s.dst.hits + s.nv_r8.hits;
7020 assert!(
7021 hits(&steady) - hits(&warm) >= FRAMES as u64,
7022 "expected at least {FRAMES} import-cache hits over the loop, got {}",
7023 hits(&steady) - hits(&warm)
7024 );
7025 }
7026
7027 #[test]
7039 #[cfg(target_os = "macos")]
7040 #[cfg(feature = "opengl")]
7041 fn test_macos_gl_f16_planar_is_gl_backed() {
7042 let mut proc = ImageProcessor::new().expect("ImageProcessor");
7043 let Some(ref gl) = proc.opengl else {
7044 eprintln!("SKIPPED: {} — GL backend unavailable", function!());
7045 return;
7046 };
7047 if !gl.supported_render_dtypes().f16 {
7048 eprintln!(
7049 "SKIPPED: {} — configuration lacks F16 color-buffer support",
7050 function!()
7051 );
7052 return;
7053 }
7054 let stats_before = gl.egl_cache_stats().expect("cache stats");
7055
7056 let src = TensorDyn::image(
7057 1280,
7058 720,
7059 PixelFormat::Nv12,
7060 DType::U8,
7061 Some(TensorMemory::Dma),
7062 edgefirst_tensor::CpuAccess::ReadWrite,
7063 )
7064 .unwrap();
7065 {
7066 let t = src.as_u8().unwrap();
7067 let mut m = t.map().unwrap();
7068 for (i, b) in m.as_mut_slice().iter_mut().enumerate() {
7069 *b = ((i * 31) % 211) as u8;
7070 }
7071 }
7072 let mut dst = TensorDyn::image(
7073 640,
7074 640,
7075 PixelFormat::PlanarRgb,
7076 DType::F16,
7077 Some(TensorMemory::Dma),
7078 edgefirst_tensor::CpuAccess::ReadWrite,
7079 )
7080 .unwrap();
7081
7082 proc.convert(
7083 &src,
7084 &mut dst,
7085 Rotation::None,
7086 Flip::None,
7087 Crop::letterbox([114, 114, 114, 255]),
7088 )
7089 .expect("F16 capability reported but the NV12→PlanarF16 convert failed");
7090 let stats_after = proc
7091 .opengl
7092 .as_ref()
7093 .expect("GL backend present")
7094 .egl_cache_stats()
7095 .expect("cache stats");
7096 assert!(
7101 stats_after.total_misses() >= stats_before.total_misses() + 2,
7102 "convert succeeded but the GL engine did not import both the \
7103 source and the F16 destination — the work did not (fully) run \
7104 on the GL backend (silent CPU fallback); misses before={} after={}",
7105 stats_before.total_misses(),
7106 stats_after.total_misses()
7107 );
7108 }
7109
7110 #[test]
7116 #[cfg(feature = "opengl")]
7117 fn test_nv12_to_planar_f16_fused_engine_vs_cpu() {
7118 let mut gl = match ImageProcessor::with_config(ImageProcessorConfig {
7119 backend: ComputeBackend::OpenGl,
7120 ..Default::default()
7121 }) {
7122 Ok(p) if p.opengl.is_some() => p,
7123 _ => {
7124 eprintln!("SKIPPED: {} — GL backend unavailable", function!());
7125 return;
7126 }
7127 };
7128 if !gl
7129 .opengl
7130 .as_ref()
7131 .map(|g| g.supported_render_dtypes().f16)
7132 .unwrap_or(false)
7133 {
7134 eprintln!("SKIPPED: {} — no F16 render support", function!());
7135 return;
7136 }
7137 let mem = if edgefirst_tensor::is_gpu_buffer_available() {
7138 TensorMemory::Dma
7139 } else {
7140 eprintln!("SKIPPED: {} — no zero-copy buffers", function!());
7141 return;
7142 };
7143
7144 let src = TensorDyn::image(
7145 1280,
7146 720,
7147 PixelFormat::Nv12,
7148 DType::U8,
7149 Some(mem),
7150 edgefirst_tensor::CpuAccess::ReadWrite,
7151 )
7152 .unwrap();
7153 {
7154 let t = src.as_u8().unwrap();
7160 let mut m = t.map().unwrap();
7161 let buf = m.as_mut_slice();
7162 let (w, h) = (1280usize, 720usize);
7163 for y in 0..h {
7164 for x in 0..w {
7165 buf[y * w + x] = ((x * 255) / w) as u8; }
7167 }
7168 for y in 0..(h / 2) {
7169 for x in 0..(w / 2) {
7170 let o = h * w + y * w + 2 * x;
7171 buf[o] = ((y * 255) / (h / 2)) as u8; buf[o + 1] = (((x + y) * 255) / (w / 2 + h / 2)) as u8; }
7174 }
7175 }
7176 let crop = Crop::letterbox([114, 114, 114, 255]);
7177 let mut gl_dst = TensorDyn::image(
7178 640,
7179 640,
7180 PixelFormat::PlanarRgb,
7181 DType::F16,
7182 Some(mem),
7183 edgefirst_tensor::CpuAccess::ReadWrite,
7184 )
7185 .unwrap();
7186 gl.opengl
7190 .as_mut()
7191 .expect("GL backend present")
7192 .convert(&src, &mut gl_dst, Rotation::None, Flip::None, crop)
7193 .expect("fused NV12→PlanarF16 GL convert");
7194
7195 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
7196 backend: ComputeBackend::Cpu,
7197 ..Default::default()
7198 })
7199 .unwrap();
7200 let mut cpu_dst = TensorDyn::image(
7201 640,
7202 640,
7203 PixelFormat::PlanarRgb,
7204 DType::F16,
7205 Some(TensorMemory::Mem),
7206 edgefirst_tensor::CpuAccess::ReadWrite,
7207 )
7208 .unwrap();
7209 cpu.convert(&src, &mut cpu_dst, Rotation::None, Flip::None, crop)
7210 .expect("CPU reference convert");
7211
7212 let g = gl_dst.as_f16().unwrap().map().unwrap().as_slice().to_vec();
7213 let c = cpu_dst.as_f16().unwrap().map().unwrap().as_slice().to_vec();
7214 assert_eq!(g.len(), c.len());
7215 let mut max_diff = 0.0f32;
7216 let mut max_at = 0usize;
7217 for (i, (a, b)) in g.iter().zip(c.iter()).enumerate() {
7218 let d = (a.to_f32() - b.to_f32()).abs();
7219 if d > max_diff {
7220 max_diff = d;
7221 max_at = i;
7222 }
7223 }
7224 let (plane, rem) = (max_at / (640 * 640), max_at % (640 * 640));
7226 let (row, col) = (rem / 640, rem % 640);
7227 eprintln!(
7228 "fused-vs-cpu: max_diff={max_diff} at plane={plane} row={row} col={col} \
7229 gl={} cpu={}",
7230 g[max_at].to_f32(),
7231 c[max_at].to_f32()
7232 );
7233 assert!(
7236 max_diff <= 4.0 / 255.0 + 1e-3,
7237 "fused NV12→PlanarF16 diverges from CPU reference: max_diff={max_diff}"
7238 );
7239 }
7240
7241 #[test]
7251 #[cfg(feature = "opengl")]
7252 fn test_zero_copy_src_to_mem_dst_gl_direct() {
7253 let mut proc = match ImageProcessor::new() {
7254 Ok(p) if p.opengl.is_some() => p,
7255 _ => {
7256 eprintln!("SKIPPED: {} — GL backend unavailable", function!());
7257 return;
7258 }
7259 };
7260 if !edgefirst_tensor::is_gpu_buffer_available() {
7261 eprintln!("SKIPPED: {} — no zero-copy buffers", function!());
7262 return;
7263 }
7264
7265 let src = TensorDyn::image(
7266 1280,
7267 720,
7268 PixelFormat::Rgba,
7269 DType::U8,
7270 Some(TensorMemory::Dma),
7271 edgefirst_tensor::CpuAccess::ReadWrite,
7272 )
7273 .unwrap();
7274 {
7275 let t = src.as_u8().unwrap();
7276 let mut m = t.map().unwrap();
7277 for (i, b) in m.as_mut_slice().iter_mut().enumerate() {
7278 *b = ((i * 31) % 211) as u8;
7279 }
7280 }
7281 let mut gl_dst = TensorDyn::image(
7282 1280,
7283 720,
7284 PixelFormat::Bgra,
7285 DType::U8,
7286 Some(TensorMemory::Mem),
7287 edgefirst_tensor::CpuAccess::ReadWrite,
7288 )
7289 .unwrap();
7290 proc.opengl
7291 .as_mut()
7292 .expect("GL backend present")
7293 .convert(
7294 &src,
7295 &mut gl_dst,
7296 Rotation::None,
7297 Flip::None,
7298 Crop::no_crop(),
7299 )
7300 .expect("zero-copy src → heap dst GL convert");
7301
7302 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
7303 backend: ComputeBackend::Cpu,
7304 ..Default::default()
7305 })
7306 .unwrap();
7307 let mut cpu_dst = TensorDyn::image(
7308 1280,
7309 720,
7310 PixelFormat::Bgra,
7311 DType::U8,
7312 Some(TensorMemory::Mem),
7313 edgefirst_tensor::CpuAccess::ReadWrite,
7314 )
7315 .unwrap();
7316 cpu.convert(
7317 &src,
7318 &mut cpu_dst,
7319 Rotation::None,
7320 Flip::None,
7321 Crop::no_crop(),
7322 )
7323 .expect("CPU reference convert");
7324
7325 let g = gl_dst.as_u8().unwrap().map().unwrap().as_slice().to_vec();
7326 let c = cpu_dst.as_u8().unwrap().map().unwrap().as_slice().to_vec();
7327 assert_eq!(g.len(), c.len());
7328 let max_diff = g
7329 .iter()
7330 .zip(c.iter())
7331 .map(|(a, b)| a.abs_diff(*b))
7332 .max()
7333 .unwrap();
7334 assert!(
7337 max_diff <= 2,
7338 "zero-copy src → heap dst diverges from CPU reference: max_diff={max_diff}"
7339 );
7340 }
7341
7342 #[test]
7343 #[cfg(target_os = "linux")]
7344 fn test_yuyv_to_rgb_g2d() {
7345 if !is_g2d_available() {
7346 eprintln!("SKIPPED: test_yuyv_to_rgb_g2d - G2D library (libg2d.so.2) not available");
7347 return;
7348 }
7349 if !is_dma_available() {
7350 eprintln!(
7351 "SKIPPED: test_yuyv_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
7352 );
7353 return;
7354 }
7355
7356 let src = load_bytes_to_tensor(
7357 1280,
7358 720,
7359 PixelFormat::Yuyv,
7360 None,
7361 &edgefirst_bench::testdata::read("camera720p.yuyv"),
7362 )
7363 .unwrap();
7364
7365 let g2d_dst = TensorDyn::image(
7366 1280,
7367 720,
7368 PixelFormat::Rgb,
7369 DType::U8,
7370 Some(TensorMemory::Dma),
7371 edgefirst_tensor::CpuAccess::ReadWrite,
7372 )
7373 .unwrap();
7374 let mut g2d_converter = G2DProcessor::new().unwrap();
7375
7376 let (result, src, g2d_dst) = convert_img(
7377 &mut g2d_converter,
7378 src,
7379 g2d_dst,
7380 Rotation::None,
7381 Flip::None,
7382 Crop::no_crop(),
7383 );
7384 result.unwrap();
7385
7386 let cpu_dst = TensorDyn::image(
7387 1280,
7388 720,
7389 PixelFormat::Rgb,
7390 DType::U8,
7391 None,
7392 edgefirst_tensor::CpuAccess::ReadWrite,
7393 )
7394 .unwrap();
7395 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
7396
7397 let (result, _src, cpu_dst) = convert_img(
7398 &mut cpu_converter,
7399 src,
7400 cpu_dst,
7401 Rotation::None,
7402 Flip::None,
7403 Crop::no_crop(),
7404 );
7405 result.unwrap();
7406
7407 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
7413 }
7414
7415 #[test]
7416 #[cfg(target_os = "linux")]
7417 fn test_yuyv_to_yuyv_resize_g2d() {
7418 if !is_g2d_available() {
7419 eprintln!(
7420 "SKIPPED: test_yuyv_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available"
7421 );
7422 return;
7423 }
7424 if !is_dma_available() {
7425 eprintln!(
7426 "SKIPPED: test_yuyv_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
7427 );
7428 return;
7429 }
7430
7431 let src = load_bytes_to_tensor(
7432 1280,
7433 720,
7434 PixelFormat::Yuyv,
7435 None,
7436 &edgefirst_bench::testdata::read("camera720p.yuyv"),
7437 )
7438 .unwrap();
7439
7440 let g2d_dst = TensorDyn::image(
7441 600,
7442 400,
7443 PixelFormat::Yuyv,
7444 DType::U8,
7445 Some(TensorMemory::Dma),
7446 edgefirst_tensor::CpuAccess::ReadWrite,
7447 )
7448 .unwrap();
7449 let mut g2d_converter = G2DProcessor::new().unwrap();
7450
7451 let (result, src, g2d_dst) = convert_img(
7452 &mut g2d_converter,
7453 src,
7454 g2d_dst,
7455 Rotation::None,
7456 Flip::None,
7457 Crop::no_crop(),
7458 );
7459 result.unwrap();
7460
7461 let cpu_dst = TensorDyn::image(
7462 600,
7463 400,
7464 PixelFormat::Yuyv,
7465 DType::U8,
7466 None,
7467 edgefirst_tensor::CpuAccess::ReadWrite,
7468 )
7469 .unwrap();
7470 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
7471
7472 let (result, _src, cpu_dst) = convert_img(
7473 &mut cpu_converter,
7474 src,
7475 cpu_dst,
7476 Rotation::None,
7477 Flip::None,
7478 Crop::no_crop(),
7479 );
7480 result.unwrap();
7481
7482 eprintln!(
7489 "WARNING: G2D has poor colorimetry support — YUYV resize diverges from the \
7490 CPU reference (~0.85 similarity); threshold held at 0.85, not 0.95."
7491 );
7492 compare_images_convert_to_rgb(&g2d_dst, &cpu_dst, 0.85, function!());
7493 }
7494
7495 #[test]
7496 fn test_yuyv_to_rgba_resize_cpu() {
7497 let src = load_bytes_to_tensor(
7498 1280,
7499 720,
7500 PixelFormat::Yuyv,
7501 None,
7502 &edgefirst_bench::testdata::read("camera720p.yuyv"),
7503 )
7504 .unwrap();
7505
7506 let (dst_width, dst_height) = (960, 540);
7507
7508 let dst = TensorDyn::image(
7509 dst_width,
7510 dst_height,
7511 PixelFormat::Rgba,
7512 DType::U8,
7513 None,
7514 edgefirst_tensor::CpuAccess::ReadWrite,
7515 )
7516 .unwrap();
7517 let mut cpu_converter = CPUProcessor::new();
7518
7519 let (result, _src, dst) = convert_img(
7520 &mut cpu_converter,
7521 src,
7522 dst,
7523 Rotation::None,
7524 Flip::None,
7525 Crop::no_crop(),
7526 );
7527 result.unwrap();
7528
7529 let dst_target = TensorDyn::image(
7530 dst_width,
7531 dst_height,
7532 PixelFormat::Rgba,
7533 DType::U8,
7534 None,
7535 edgefirst_tensor::CpuAccess::ReadWrite,
7536 )
7537 .unwrap();
7538 let src_target = load_bytes_to_tensor(
7539 1280,
7540 720,
7541 PixelFormat::Rgba,
7542 None,
7543 &edgefirst_bench::testdata::read("camera720p.rgba"),
7544 )
7545 .unwrap();
7546 let (result, _src_target, dst_target) = convert_img(
7547 &mut cpu_converter,
7548 src_target,
7549 dst_target,
7550 Rotation::None,
7551 Flip::None,
7552 Crop::no_crop(),
7553 );
7554 result.unwrap();
7555
7556 compare_images(&dst, &dst_target, 0.98, function!());
7559 }
7560
7561 #[test]
7562 #[cfg(target_os = "linux")]
7563 fn test_yuyv_to_rgba_crop_flip_g2d() {
7564 if !is_g2d_available() {
7565 eprintln!(
7566 "SKIPPED: test_yuyv_to_rgba_crop_flip_g2d - G2D library (libg2d.so.2) not available"
7567 );
7568 return;
7569 }
7570 if !is_dma_available() {
7571 eprintln!(
7572 "SKIPPED: test_yuyv_to_rgba_crop_flip_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
7573 );
7574 return;
7575 }
7576
7577 let src = load_bytes_to_tensor(
7578 1280,
7579 720,
7580 PixelFormat::Yuyv,
7581 Some(TensorMemory::Dma),
7582 &edgefirst_bench::testdata::read("camera720p.yuyv"),
7583 )
7584 .unwrap();
7585
7586 let (dst_width, dst_height) = (640, 640);
7587
7588 let dst_g2d = TensorDyn::image(
7589 dst_width,
7590 dst_height,
7591 PixelFormat::Rgba,
7592 DType::U8,
7593 Some(TensorMemory::Dma),
7594 edgefirst_tensor::CpuAccess::ReadWrite,
7595 )
7596 .unwrap();
7597 let mut g2d_converter = G2DProcessor::new().unwrap();
7598 let crop = Crop::new().with_source(Some(Region::new(20, 15, 400, 300)));
7599
7600 let (result, src, dst_g2d) = convert_img(
7601 &mut g2d_converter,
7602 src,
7603 dst_g2d,
7604 Rotation::None,
7605 Flip::Horizontal,
7606 crop,
7607 );
7608 result.unwrap();
7609
7610 let dst_cpu = TensorDyn::image(
7611 dst_width,
7612 dst_height,
7613 PixelFormat::Rgba,
7614 DType::U8,
7615 Some(TensorMemory::Dma),
7616 edgefirst_tensor::CpuAccess::ReadWrite,
7617 )
7618 .unwrap();
7619 let mut cpu_converter = CPUProcessor::new();
7620
7621 let (result, _src, dst_cpu) = convert_img(
7622 &mut cpu_converter,
7623 src,
7624 dst_cpu,
7625 Rotation::None,
7626 Flip::Horizontal,
7627 crop,
7628 );
7629 result.unwrap();
7630 compare_images(&dst_g2d, &dst_cpu, 0.98, function!());
7636 }
7637
7638 #[test]
7639 #[cfg(target_os = "linux")]
7640 #[cfg(feature = "opengl")]
7641 fn test_yuyv_to_rgba_crop_flip_opengl() {
7642 if !is_opengl_available() {
7643 eprintln!("SKIPPED: {} - OpenGL not available", function!());
7644 return;
7645 }
7646
7647 if !is_dma_available() {
7648 eprintln!(
7649 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
7650 function!()
7651 );
7652 return;
7653 }
7654
7655 let src = load_bytes_to_tensor(
7656 1280,
7657 720,
7658 PixelFormat::Yuyv,
7659 Some(TensorMemory::Dma),
7660 &edgefirst_bench::testdata::read("camera720p.yuyv"),
7661 )
7662 .unwrap();
7663
7664 let (dst_width, dst_height) = (640, 640);
7665
7666 let dst_gl = TensorDyn::image(
7667 dst_width,
7668 dst_height,
7669 PixelFormat::Rgba,
7670 DType::U8,
7671 Some(TensorMemory::Dma),
7672 edgefirst_tensor::CpuAccess::ReadWrite,
7673 )
7674 .unwrap();
7675 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
7676 let crop = Crop::new().with_source(Some(Region::new(20, 15, 400, 300)));
7677
7678 let (result, src, dst_gl) = convert_img(
7679 &mut gl_converter,
7680 src,
7681 dst_gl,
7682 Rotation::None,
7683 Flip::Horizontal,
7684 crop,
7685 );
7686 result.unwrap();
7687
7688 let dst_cpu = TensorDyn::image(
7689 dst_width,
7690 dst_height,
7691 PixelFormat::Rgba,
7692 DType::U8,
7693 Some(TensorMemory::Dma),
7694 edgefirst_tensor::CpuAccess::ReadWrite,
7695 )
7696 .unwrap();
7697 let mut cpu_converter = CPUProcessor::new();
7698
7699 let (result, _src, dst_cpu) = convert_img(
7700 &mut cpu_converter,
7701 src,
7702 dst_cpu,
7703 Rotation::None,
7704 Flip::Horizontal,
7705 crop,
7706 );
7707 result.unwrap();
7708 compare_images(&dst_gl, &dst_cpu, 0.98, function!());
7713 }
7714
7715 #[test]
7716 fn test_vyuy_to_rgba_cpu() {
7717 let file = edgefirst_bench::testdata::read("camera720p.vyuy").to_vec();
7718 let src = TensorDyn::image(
7719 1280,
7720 720,
7721 PixelFormat::Vyuy,
7722 DType::U8,
7723 None,
7724 edgefirst_tensor::CpuAccess::ReadWrite,
7725 )
7726 .unwrap();
7727 src.as_u8()
7728 .unwrap()
7729 .map()
7730 .unwrap()
7731 .as_mut_slice()
7732 .copy_from_slice(&file);
7733
7734 let dst = TensorDyn::image(
7735 1280,
7736 720,
7737 PixelFormat::Rgba,
7738 DType::U8,
7739 None,
7740 edgefirst_tensor::CpuAccess::ReadWrite,
7741 )
7742 .unwrap();
7743 let mut cpu_converter = CPUProcessor::new();
7744
7745 let (result, _src, dst) = convert_img(
7746 &mut cpu_converter,
7747 src,
7748 dst,
7749 Rotation::None,
7750 Flip::None,
7751 Crop::no_crop(),
7752 );
7753 result.unwrap();
7754
7755 let target_image = TensorDyn::image(
7756 1280,
7757 720,
7758 PixelFormat::Rgba,
7759 DType::U8,
7760 None,
7761 edgefirst_tensor::CpuAccess::ReadWrite,
7762 )
7763 .unwrap();
7764 target_image
7765 .as_u8()
7766 .unwrap()
7767 .map()
7768 .unwrap()
7769 .as_mut_slice()
7770 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
7771
7772 compare_images(&dst, &target_image, 0.98, function!());
7775 }
7776
7777 #[test]
7778 fn test_vyuy_to_rgb_cpu() {
7779 let file = edgefirst_bench::testdata::read("camera720p.vyuy").to_vec();
7780 let src = TensorDyn::image(
7781 1280,
7782 720,
7783 PixelFormat::Vyuy,
7784 DType::U8,
7785 None,
7786 edgefirst_tensor::CpuAccess::ReadWrite,
7787 )
7788 .unwrap();
7789 src.as_u8()
7790 .unwrap()
7791 .map()
7792 .unwrap()
7793 .as_mut_slice()
7794 .copy_from_slice(&file);
7795
7796 let dst = TensorDyn::image(
7797 1280,
7798 720,
7799 PixelFormat::Rgb,
7800 DType::U8,
7801 None,
7802 edgefirst_tensor::CpuAccess::ReadWrite,
7803 )
7804 .unwrap();
7805 let mut cpu_converter = CPUProcessor::new();
7806
7807 let (result, _src, dst) = convert_img(
7808 &mut cpu_converter,
7809 src,
7810 dst,
7811 Rotation::None,
7812 Flip::None,
7813 Crop::no_crop(),
7814 );
7815 result.unwrap();
7816
7817 let target_image = TensorDyn::image(
7818 1280,
7819 720,
7820 PixelFormat::Rgb,
7821 DType::U8,
7822 None,
7823 edgefirst_tensor::CpuAccess::ReadWrite,
7824 )
7825 .unwrap();
7826 target_image
7827 .as_u8()
7828 .unwrap()
7829 .map()
7830 .unwrap()
7831 .as_mut_slice()
7832 .as_chunks_mut::<3>()
7833 .0
7834 .iter_mut()
7835 .zip(
7836 edgefirst_bench::testdata::read("camera720p.rgba")
7837 .as_chunks::<4>()
7838 .0,
7839 )
7840 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
7841
7842 compare_images(&dst, &target_image, 0.98, function!());
7845 }
7846
7847 #[test]
7848 #[cfg(target_os = "linux")]
7849 #[ignore = "G2D does not support VYUY; re-enable when hardware support is added"]
7850 fn test_vyuy_to_rgba_g2d() {
7851 if !is_g2d_available() {
7852 eprintln!("SKIPPED: test_vyuy_to_rgba_g2d - G2D library (libg2d.so.2) not available");
7853 return;
7854 }
7855 if !is_dma_available() {
7856 eprintln!(
7857 "SKIPPED: test_vyuy_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
7858 );
7859 return;
7860 }
7861
7862 let src = load_bytes_to_tensor(
7863 1280,
7864 720,
7865 PixelFormat::Vyuy,
7866 None,
7867 &edgefirst_bench::testdata::read("camera720p.vyuy"),
7868 )
7869 .unwrap();
7870
7871 let dst = TensorDyn::image(
7872 1280,
7873 720,
7874 PixelFormat::Rgba,
7875 DType::U8,
7876 Some(TensorMemory::Dma),
7877 edgefirst_tensor::CpuAccess::ReadWrite,
7878 )
7879 .unwrap();
7880 let mut g2d_converter = G2DProcessor::new().unwrap();
7881
7882 let (result, _src, dst) = convert_img(
7883 &mut g2d_converter,
7884 src,
7885 dst,
7886 Rotation::None,
7887 Flip::None,
7888 Crop::no_crop(),
7889 );
7890 match result {
7891 Err(Error::G2D(_)) => {
7892 eprintln!("SKIPPED: test_vyuy_to_rgba_g2d - G2D does not support PixelFormat::Vyuy format");
7893 return;
7894 }
7895 r => r.unwrap(),
7896 }
7897
7898 let target_image = TensorDyn::image(
7899 1280,
7900 720,
7901 PixelFormat::Rgba,
7902 DType::U8,
7903 None,
7904 edgefirst_tensor::CpuAccess::ReadWrite,
7905 )
7906 .unwrap();
7907 target_image
7908 .as_u8()
7909 .unwrap()
7910 .map()
7911 .unwrap()
7912 .as_mut_slice()
7913 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
7914
7915 compare_images(&dst, &target_image, 0.98, function!());
7919 }
7920
7921 #[test]
7922 #[cfg(target_os = "linux")]
7923 #[ignore = "G2D does not support VYUY; re-enable when hardware support is added"]
7924 fn test_vyuy_to_rgb_g2d() {
7925 if !is_g2d_available() {
7926 eprintln!("SKIPPED: test_vyuy_to_rgb_g2d - G2D library (libg2d.so.2) not available");
7927 return;
7928 }
7929 if !is_dma_available() {
7930 eprintln!(
7931 "SKIPPED: test_vyuy_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
7932 );
7933 return;
7934 }
7935
7936 let src = load_bytes_to_tensor(
7937 1280,
7938 720,
7939 PixelFormat::Vyuy,
7940 None,
7941 &edgefirst_bench::testdata::read("camera720p.vyuy"),
7942 )
7943 .unwrap();
7944
7945 let g2d_dst = TensorDyn::image(
7946 1280,
7947 720,
7948 PixelFormat::Rgb,
7949 DType::U8,
7950 Some(TensorMemory::Dma),
7951 edgefirst_tensor::CpuAccess::ReadWrite,
7952 )
7953 .unwrap();
7954 let mut g2d_converter = G2DProcessor::new().unwrap();
7955
7956 let (result, src, g2d_dst) = convert_img(
7957 &mut g2d_converter,
7958 src,
7959 g2d_dst,
7960 Rotation::None,
7961 Flip::None,
7962 Crop::no_crop(),
7963 );
7964 match result {
7965 Err(Error::G2D(_)) => {
7966 eprintln!(
7967 "SKIPPED: test_vyuy_to_rgb_g2d - G2D does not support PixelFormat::Vyuy format"
7968 );
7969 return;
7970 }
7971 r => r.unwrap(),
7972 }
7973
7974 let cpu_dst = TensorDyn::image(
7975 1280,
7976 720,
7977 PixelFormat::Rgb,
7978 DType::U8,
7979 None,
7980 edgefirst_tensor::CpuAccess::ReadWrite,
7981 )
7982 .unwrap();
7983 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
7984
7985 let (result, _src, cpu_dst) = convert_img(
7986 &mut cpu_converter,
7987 src,
7988 cpu_dst,
7989 Rotation::None,
7990 Flip::None,
7991 Crop::no_crop(),
7992 );
7993 result.unwrap();
7994
7995 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
8001 }
8002
8003 #[test]
8004 #[cfg(target_os = "linux")]
8005 #[cfg(feature = "opengl")]
8006 fn test_vyuy_to_rgba_opengl() {
8007 if !is_opengl_available() {
8008 eprintln!("SKIPPED: {} - OpenGL not available", function!());
8009 return;
8010 }
8011 if !is_dma_available() {
8012 eprintln!(
8013 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
8014 function!()
8015 );
8016 return;
8017 }
8018
8019 let src = load_bytes_to_tensor(
8020 1280,
8021 720,
8022 PixelFormat::Vyuy,
8023 Some(TensorMemory::Dma),
8024 &edgefirst_bench::testdata::read("camera720p.vyuy"),
8025 )
8026 .unwrap();
8027
8028 let dst = TensorDyn::image(
8029 1280,
8030 720,
8031 PixelFormat::Rgba,
8032 DType::U8,
8033 Some(TensorMemory::Dma),
8034 edgefirst_tensor::CpuAccess::ReadWrite,
8035 )
8036 .unwrap();
8037 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
8038
8039 let (result, _src, dst) = convert_img(
8040 &mut gl_converter,
8041 src,
8042 dst,
8043 Rotation::None,
8044 Flip::None,
8045 Crop::no_crop(),
8046 );
8047 match result {
8048 Err(Error::NotSupported(_)) => {
8049 eprintln!(
8050 "SKIPPED: {} - OpenGL does not support PixelFormat::Vyuy DMA format",
8051 function!()
8052 );
8053 return;
8054 }
8055 r => r.unwrap(),
8056 }
8057
8058 let target_image = TensorDyn::image(
8059 1280,
8060 720,
8061 PixelFormat::Rgba,
8062 DType::U8,
8063 None,
8064 edgefirst_tensor::CpuAccess::ReadWrite,
8065 )
8066 .unwrap();
8067 target_image
8068 .as_u8()
8069 .unwrap()
8070 .map()
8071 .unwrap()
8072 .as_mut_slice()
8073 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
8074
8075 compare_images(&dst, &target_image, 0.98, function!());
8079 }
8080
8081 #[test]
8082 fn test_nv12_to_rgba_cpu() {
8083 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
8084 let src = TensorDyn::image(
8085 1280,
8086 720,
8087 PixelFormat::Nv12,
8088 DType::U8,
8089 None,
8090 edgefirst_tensor::CpuAccess::ReadWrite,
8091 )
8092 .unwrap();
8093 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
8094 .copy_from_slice(&file);
8095
8096 let dst = TensorDyn::image(
8097 1280,
8098 720,
8099 PixelFormat::Rgba,
8100 DType::U8,
8101 None,
8102 edgefirst_tensor::CpuAccess::ReadWrite,
8103 )
8104 .unwrap();
8105 let mut cpu_converter = CPUProcessor::new();
8106
8107 let (result, _src, dst) = convert_img(
8108 &mut cpu_converter,
8109 src,
8110 dst,
8111 Rotation::None,
8112 Flip::None,
8113 Crop::no_crop(),
8114 );
8115 result.unwrap();
8116
8117 let target_image = crate::load_image_test_helper(
8118 &edgefirst_bench::testdata::read("zidane.jpg"),
8119 Some(PixelFormat::Rgba),
8120 None,
8121 )
8122 .unwrap();
8123
8124 compare_images(&dst, &target_image, 0.95, function!());
8129 }
8130
8131 #[test]
8132 fn test_nv12_odd_height_to_rgb_cpu() {
8133 let mut src = TensorDyn::image(
8144 8,
8145 5,
8146 PixelFormat::Nv12,
8147 DType::U8,
8148 Some(TensorMemory::Mem),
8149 edgefirst_tensor::CpuAccess::ReadWrite,
8150 )
8151 .unwrap();
8152 assert_eq!(src.shape(), &[8, 8]);
8153 assert_eq!((src.width(), src.height()), (Some(8), Some(5)));
8154 src.as_u8().unwrap().map().unwrap().as_mut_slice().fill(128);
8155 src.set_colorimetry(Some(
8159 edgefirst_tensor::Colorimetry::default()
8160 .with_encoding(edgefirst_tensor::ColorEncoding::Bt601)
8161 .with_range(edgefirst_tensor::ColorRange::Full),
8162 ));
8163
8164 let dst = TensorDyn::image(
8165 8,
8166 5,
8167 PixelFormat::Rgb,
8168 DType::U8,
8169 Some(TensorMemory::Mem),
8170 edgefirst_tensor::CpuAccess::ReadWrite,
8171 )
8172 .unwrap();
8173 let mut cpu_converter = CPUProcessor::new();
8174 let (result, _src, dst) = convert_img(
8175 &mut cpu_converter,
8176 src,
8177 dst,
8178 Rotation::None,
8179 Flip::None,
8180 Crop::no_crop(),
8181 );
8182 result.unwrap();
8183
8184 assert_eq!((dst.width(), dst.height()), (Some(8), Some(5)));
8185 let map = dst.as_u8().unwrap().map().unwrap();
8186 for (i, &b) in map.as_slice().iter().enumerate() {
8187 assert!(
8188 (b as i16 - 128).abs() <= 2,
8189 "pixel byte {i} = {b}, expected ~128 for neutral-grey NV12"
8190 );
8191 }
8192 }
8193
8194 #[test]
8195 fn test_nv24_to_rgb_cpu() {
8196 let mut src = TensorDyn::image(
8202 8,
8203 4,
8204 PixelFormat::Nv24,
8205 DType::U8,
8206 Some(TensorMemory::Mem),
8207 edgefirst_tensor::CpuAccess::ReadWrite,
8208 )
8209 .unwrap();
8210 assert_eq!(src.shape(), &[12, 8]);
8211 assert_eq!((src.width(), src.height()), (Some(8), Some(4)));
8212 src.as_u8().unwrap().map().unwrap().as_mut_slice().fill(128);
8213 src.set_colorimetry(Some(
8216 edgefirst_tensor::Colorimetry::default()
8217 .with_encoding(edgefirst_tensor::ColorEncoding::Bt601)
8218 .with_range(edgefirst_tensor::ColorRange::Full),
8219 ));
8220
8221 let dst = TensorDyn::image(
8222 8,
8223 4,
8224 PixelFormat::Rgb,
8225 DType::U8,
8226 Some(TensorMemory::Mem),
8227 edgefirst_tensor::CpuAccess::ReadWrite,
8228 )
8229 .unwrap();
8230 let mut cpu_converter = CPUProcessor::new();
8231 let (result, _src, dst) = convert_img(
8232 &mut cpu_converter,
8233 src,
8234 dst,
8235 Rotation::None,
8236 Flip::None,
8237 Crop::no_crop(),
8238 );
8239 result.unwrap();
8240
8241 assert_eq!((dst.width(), dst.height()), (Some(8), Some(4)));
8242 let map = dst.as_u8().unwrap().map().unwrap();
8243 for (i, &b) in map.as_slice().iter().enumerate() {
8244 assert!(
8245 (b as i16 - 128).abs() <= 2,
8246 "pixel byte {i} = {b}, expected ~128 for neutral-grey NV24"
8247 );
8248 }
8249 }
8250
8251 #[test]
8252 fn cpu_nv12_to_rgb_respects_tagged_bt2020() {
8253 fn decode_tagged(enc: edgefirst_tensor::ColorEncoding) -> [u8; 3] {
8261 let mut src = TensorDyn::image(
8262 8,
8263 4,
8264 PixelFormat::Nv12,
8265 DType::U8,
8266 Some(TensorMemory::Mem),
8267 edgefirst_tensor::CpuAccess::ReadWrite,
8268 )
8269 .unwrap();
8270 assert_eq!(src.shape(), &[6, 8]);
8272 {
8273 let mut map = src.as_u8().unwrap().map().unwrap();
8274 let buf = map.as_mut_slice();
8275 buf[..32].fill(120); for px in buf[32..].chunks_exact_mut(2) {
8277 px[0] = 180; px[1] = 64; }
8280 }
8281 src.set_colorimetry(Some(
8284 edgefirst_tensor::Colorimetry::default()
8285 .with_encoding(enc)
8286 .with_range(edgefirst_tensor::ColorRange::Limited),
8287 ));
8288 let dst = TensorDyn::image(
8289 8,
8290 4,
8291 PixelFormat::Rgb,
8292 DType::U8,
8293 Some(TensorMemory::Mem),
8294 edgefirst_tensor::CpuAccess::ReadWrite,
8295 )
8296 .unwrap();
8297 let mut cpu = CPUProcessor::new();
8298 let (result, _src, dst) = convert_img(
8299 &mut cpu,
8300 src,
8301 dst,
8302 Rotation::None,
8303 Flip::None,
8304 Crop::no_crop(),
8305 );
8306 result.unwrap();
8307 let map = dst.as_u8().unwrap().map().unwrap();
8308 let s = map.as_slice();
8309 [s[0], s[1], s[2]]
8310 }
8311
8312 let bt601 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt601);
8313 let bt709 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt709);
8314 let bt2020 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt2020);
8315
8316 assert_ne!(
8317 bt2020, bt601,
8318 "BT.2020 must decode differently from BT.601 ({bt2020:?} vs {bt601:?})"
8319 );
8320 assert_ne!(
8321 bt2020, bt709,
8322 "BT.2020 must decode differently from BT.709 ({bt2020:?} vs {bt709:?})"
8323 );
8324 assert_ne!(
8325 bt601, bt709,
8326 "BT.601 must decode differently from BT.709 ({bt601:?} vs {bt709:?})"
8327 );
8328 }
8329
8330 #[test]
8331 fn test_nv12_to_rgb_cpu() {
8332 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
8333 let src = TensorDyn::image(
8334 1280,
8335 720,
8336 PixelFormat::Nv12,
8337 DType::U8,
8338 None,
8339 edgefirst_tensor::CpuAccess::ReadWrite,
8340 )
8341 .unwrap();
8342 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
8343 .copy_from_slice(&file);
8344
8345 let dst = TensorDyn::image(
8346 1280,
8347 720,
8348 PixelFormat::Rgb,
8349 DType::U8,
8350 None,
8351 edgefirst_tensor::CpuAccess::ReadWrite,
8352 )
8353 .unwrap();
8354 let mut cpu_converter = CPUProcessor::new();
8355
8356 let (result, _src, dst) = convert_img(
8357 &mut cpu_converter,
8358 src,
8359 dst,
8360 Rotation::None,
8361 Flip::None,
8362 Crop::no_crop(),
8363 );
8364 result.unwrap();
8365
8366 let target_image = crate::load_image_test_helper(
8367 &edgefirst_bench::testdata::read("zidane.jpg"),
8368 Some(PixelFormat::Rgb),
8369 None,
8370 )
8371 .unwrap();
8372
8373 compare_images(&dst, &target_image, 0.95, function!());
8378 }
8379
8380 #[test]
8381 fn test_nv12_to_grey_cpu() {
8382 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
8383 let src = TensorDyn::image(
8384 1280,
8385 720,
8386 PixelFormat::Nv12,
8387 DType::U8,
8388 None,
8389 edgefirst_tensor::CpuAccess::ReadWrite,
8390 )
8391 .unwrap();
8392 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
8393 .copy_from_slice(&file);
8394
8395 let dst = TensorDyn::image(
8396 1280,
8397 720,
8398 PixelFormat::Grey,
8399 DType::U8,
8400 None,
8401 edgefirst_tensor::CpuAccess::ReadWrite,
8402 )
8403 .unwrap();
8404 let mut cpu_converter = CPUProcessor::new();
8405
8406 let (result, _src, dst) = convert_img(
8407 &mut cpu_converter,
8408 src,
8409 dst,
8410 Rotation::None,
8411 Flip::None,
8412 Crop::no_crop(),
8413 );
8414 result.unwrap();
8415
8416 let target_image = crate::load_image_test_helper(
8417 &edgefirst_bench::testdata::read("zidane.jpg"),
8418 Some(PixelFormat::Grey),
8419 None,
8420 )
8421 .unwrap();
8422
8423 compare_images(&dst, &target_image, 0.95, function!());
8428 }
8429
8430 #[test]
8431 fn test_nv12_to_yuyv_cpu() {
8432 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
8433 let src = TensorDyn::image(
8434 1280,
8435 720,
8436 PixelFormat::Nv12,
8437 DType::U8,
8438 None,
8439 edgefirst_tensor::CpuAccess::ReadWrite,
8440 )
8441 .unwrap();
8442 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
8443 .copy_from_slice(&file);
8444
8445 let dst = TensorDyn::image(
8446 1280,
8447 720,
8448 PixelFormat::Yuyv,
8449 DType::U8,
8450 None,
8451 edgefirst_tensor::CpuAccess::ReadWrite,
8452 )
8453 .unwrap();
8454 let mut cpu_converter = CPUProcessor::new();
8455
8456 let (result, _src, dst) = convert_img(
8457 &mut cpu_converter,
8458 src,
8459 dst,
8460 Rotation::None,
8461 Flip::None,
8462 Crop::no_crop(),
8463 );
8464 result.unwrap();
8465
8466 let target_image = crate::load_image_test_helper(
8467 &edgefirst_bench::testdata::read("zidane.jpg"),
8468 Some(PixelFormat::Rgb),
8469 None,
8470 )
8471 .unwrap();
8472
8473 compare_images_convert_to_rgb(&dst, &target_image, 0.95, function!());
8478 }
8479
8480 #[test]
8481 fn test_cpu_resize_nv16() {
8482 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
8483 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
8484
8485 let cpu_nv16_dst = TensorDyn::image(
8486 640,
8487 640,
8488 PixelFormat::Nv16,
8489 DType::U8,
8490 None,
8491 edgefirst_tensor::CpuAccess::ReadWrite,
8492 )
8493 .unwrap();
8494 let cpu_rgb_dst = TensorDyn::image(
8495 640,
8496 640,
8497 PixelFormat::Rgb,
8498 DType::U8,
8499 None,
8500 edgefirst_tensor::CpuAccess::ReadWrite,
8501 )
8502 .unwrap();
8503 let mut cpu_converter = CPUProcessor::new();
8504 let crop = Crop::letterbox([255, 128, 0, 255]);
8505
8506 let (result, src, cpu_nv16_dst) = convert_img(
8507 &mut cpu_converter,
8508 src,
8509 cpu_nv16_dst,
8510 Rotation::None,
8511 Flip::None,
8512 crop,
8513 );
8514 result.unwrap();
8515
8516 let (result, _src, cpu_rgb_dst) = convert_img(
8517 &mut cpu_converter,
8518 src,
8519 cpu_rgb_dst,
8520 Rotation::None,
8521 Flip::None,
8522 crop,
8523 );
8524 result.unwrap();
8525 compare_images_convert_to_rgb(&cpu_nv16_dst, &cpu_rgb_dst, 0.99, function!());
8526 }
8527
8528 fn load_bytes_to_tensor(
8529 width: usize,
8530 height: usize,
8531 format: PixelFormat,
8532 memory: Option<TensorMemory>,
8533 bytes: &[u8],
8534 ) -> Result<TensorDyn, Error> {
8535 let src = TensorDyn::image(
8536 width,
8537 height,
8538 format,
8539 DType::U8,
8540 memory,
8541 edgefirst_tensor::CpuAccess::ReadWrite,
8542 )?;
8543 src.as_u8()
8544 .unwrap()
8545 .map()?
8546 .as_mut_slice()
8547 .copy_from_slice(bytes);
8548 Ok(src)
8549 }
8550
8551 fn compare_images(img1: &TensorDyn, img2: &TensorDyn, threshold: f64, name: &str) {
8559 assert_eq!(img1.height(), img2.height(), "Heights differ");
8560 assert_eq!(img1.width(), img2.width(), "Widths differ");
8561 assert_eq!(
8562 img1.format().unwrap(),
8563 img2.format().unwrap(),
8564 "PixelFormat differ"
8565 );
8566 assert!(
8567 matches!(
8568 img1.format().unwrap(),
8569 PixelFormat::Rgb | PixelFormat::Rgba | PixelFormat::Grey | PixelFormat::PlanarRgb
8570 ),
8571 "format must be Rgb or Rgba for comparison"
8572 );
8573
8574 let image1 = match img1.format().unwrap() {
8575 PixelFormat::Rgb => image::RgbImage::from_vec(
8576 img1.width().unwrap() as u32,
8577 img1.height().unwrap() as u32,
8578 img1.as_u8().unwrap().map().unwrap().to_vec(),
8579 )
8580 .unwrap(),
8581 PixelFormat::Rgba => image::RgbaImage::from_vec(
8582 img1.width().unwrap() as u32,
8583 img1.height().unwrap() as u32,
8584 img1.as_u8().unwrap().map().unwrap().to_vec(),
8585 )
8586 .unwrap()
8587 .convert(),
8588 PixelFormat::Grey => image::GrayImage::from_vec(
8589 img1.width().unwrap() as u32,
8590 img1.height().unwrap() as u32,
8591 img1.as_u8().unwrap().map().unwrap().to_vec(),
8592 )
8593 .unwrap()
8594 .convert(),
8595 PixelFormat::PlanarRgb => image::GrayImage::from_vec(
8596 img1.width().unwrap() as u32,
8597 (img1.height().unwrap() * 3) as u32,
8598 img1.as_u8().unwrap().map().unwrap().to_vec(),
8599 )
8600 .unwrap()
8601 .convert(),
8602 _ => return,
8603 };
8604
8605 let image2 = match img2.format().unwrap() {
8606 PixelFormat::Rgb => image::RgbImage::from_vec(
8607 img2.width().unwrap() as u32,
8608 img2.height().unwrap() as u32,
8609 img2.as_u8().unwrap().map().unwrap().to_vec(),
8610 )
8611 .unwrap(),
8612 PixelFormat::Rgba => image::RgbaImage::from_vec(
8613 img2.width().unwrap() as u32,
8614 img2.height().unwrap() as u32,
8615 img2.as_u8().unwrap().map().unwrap().to_vec(),
8616 )
8617 .unwrap()
8618 .convert(),
8619 PixelFormat::Grey => image::GrayImage::from_vec(
8620 img2.width().unwrap() as u32,
8621 img2.height().unwrap() as u32,
8622 img2.as_u8().unwrap().map().unwrap().to_vec(),
8623 )
8624 .unwrap()
8625 .convert(),
8626 PixelFormat::PlanarRgb => image::GrayImage::from_vec(
8627 img2.width().unwrap() as u32,
8628 (img2.height().unwrap() * 3) as u32,
8629 img2.as_u8().unwrap().map().unwrap().to_vec(),
8630 )
8631 .unwrap()
8632 .convert(),
8633 _ => return,
8634 };
8635
8636 let similarity = image_compare::rgb_similarity_structure(
8637 &image_compare::Algorithm::RootMeanSquared,
8638 &image1,
8639 &image2,
8640 )
8641 .expect("Image Comparison failed");
8642 if similarity.score < threshold {
8643 similarity
8646 .image
8647 .to_color_map()
8648 .save(format!("{name}.png"))
8649 .unwrap();
8650 panic!(
8651 "{name}: converted image and target image have similarity score too low: {} < {}",
8652 similarity.score, threshold
8653 )
8654 }
8655 }
8656
8657 fn compare_images_convert_to_rgb(
8658 img1: &TensorDyn,
8659 img2: &TensorDyn,
8660 threshold: f64,
8661 name: &str,
8662 ) {
8663 assert_eq!(img1.height(), img2.height(), "Heights differ");
8664 assert_eq!(img1.width(), img2.width(), "Widths differ");
8665
8666 let mut img_rgb1 = TensorDyn::image(
8667 img1.width().unwrap(),
8668 img1.height().unwrap(),
8669 PixelFormat::Rgb,
8670 DType::U8,
8671 Some(TensorMemory::Mem),
8672 edgefirst_tensor::CpuAccess::ReadWrite,
8673 )
8674 .unwrap();
8675 let mut img_rgb2 = TensorDyn::image(
8676 img1.width().unwrap(),
8677 img1.height().unwrap(),
8678 PixelFormat::Rgb,
8679 DType::U8,
8680 Some(TensorMemory::Mem),
8681 edgefirst_tensor::CpuAccess::ReadWrite,
8682 )
8683 .unwrap();
8684 let mut __cv = CPUProcessor::default();
8685 let r1 = __cv.convert(
8686 img1,
8687 &mut img_rgb1,
8688 crate::Rotation::None,
8689 crate::Flip::None,
8690 crate::Crop::default(),
8691 );
8692 let r2 = __cv.convert(
8693 img2,
8694 &mut img_rgb2,
8695 crate::Rotation::None,
8696 crate::Flip::None,
8697 crate::Crop::default(),
8698 );
8699 if r1.is_err() || r2.is_err() {
8700 let w = img1.width().unwrap() as u32;
8702 let data1 = img1.as_u8().unwrap().map().unwrap().to_vec();
8703 let data2 = img2.as_u8().unwrap().map().unwrap().to_vec();
8704 let h1 = (data1.len() as u32) / w;
8705 let h2 = (data2.len() as u32) / w;
8706 let g1 = image::GrayImage::from_vec(w, h1, data1).unwrap();
8707 let g2 = image::GrayImage::from_vec(w, h2, data2).unwrap();
8708 let similarity = image_compare::gray_similarity_structure(
8709 &image_compare::Algorithm::RootMeanSquared,
8710 &g1,
8711 &g2,
8712 )
8713 .expect("Image Comparison failed");
8714 if similarity.score < threshold {
8715 panic!(
8716 "{name}: converted image and target image have similarity score too low: {} < {}",
8717 similarity.score, threshold
8718 )
8719 }
8720 return;
8721 }
8722
8723 let image1 = image::RgbImage::from_vec(
8724 img_rgb1.width().unwrap() as u32,
8725 img_rgb1.height().unwrap() as u32,
8726 img_rgb1.as_u8().unwrap().map().unwrap().to_vec(),
8727 )
8728 .unwrap();
8729
8730 let image2 = image::RgbImage::from_vec(
8731 img_rgb2.width().unwrap() as u32,
8732 img_rgb2.height().unwrap() as u32,
8733 img_rgb2.as_u8().unwrap().map().unwrap().to_vec(),
8734 )
8735 .unwrap();
8736
8737 let similarity = image_compare::rgb_similarity_structure(
8738 &image_compare::Algorithm::RootMeanSquared,
8739 &image1,
8740 &image2,
8741 )
8742 .expect("Image Comparison failed");
8743 if similarity.score < threshold {
8744 similarity
8747 .image
8748 .to_color_map()
8749 .save(format!("{name}.png"))
8750 .unwrap();
8751 panic!(
8752 "{name}: converted image and target image have similarity score too low: {} < {}",
8753 similarity.score, threshold
8754 )
8755 }
8756 }
8757
8758 #[test]
8763 fn test_nv12_image_creation() {
8764 let width = 640;
8765 let height = 480;
8766 let img = TensorDyn::image(
8767 width,
8768 height,
8769 PixelFormat::Nv12,
8770 DType::U8,
8771 None,
8772 edgefirst_tensor::CpuAccess::ReadWrite,
8773 )
8774 .unwrap();
8775
8776 assert_eq!(img.width(), Some(width));
8777 assert_eq!(img.height(), Some(height));
8778 assert_eq!(img.format().unwrap(), PixelFormat::Nv12);
8779 assert_eq!(img.as_u8().unwrap().shape(), &[height * 3 / 2, width]);
8781 }
8782
8783 #[test]
8784 fn test_nv12_channels() {
8785 let img = TensorDyn::image(
8786 640,
8787 480,
8788 PixelFormat::Nv12,
8789 DType::U8,
8790 None,
8791 edgefirst_tensor::CpuAccess::ReadWrite,
8792 )
8793 .unwrap();
8794 assert_eq!(img.format().unwrap().channels(), 1);
8796 }
8797
8798 #[test]
8803 fn test_tensor_set_format_planar() {
8804 let mut tensor = Tensor::<u8>::new(&[3, 480, 640], None, None).unwrap();
8805 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
8806 assert_eq!(tensor.format(), Some(PixelFormat::PlanarRgb));
8807 assert_eq!(tensor.width(), Some(640));
8808 assert_eq!(tensor.height(), Some(480));
8809 }
8810
8811 #[test]
8812 fn test_tensor_set_format_interleaved() {
8813 let mut tensor = Tensor::<u8>::new(&[480, 640, 4], None, None).unwrap();
8814 tensor.set_format(PixelFormat::Rgba).unwrap();
8815 assert_eq!(tensor.format(), Some(PixelFormat::Rgba));
8816 assert_eq!(tensor.width(), Some(640));
8817 assert_eq!(tensor.height(), Some(480));
8818 }
8819
8820 #[test]
8821 fn test_tensordyn_image_rgb() {
8822 let img = TensorDyn::image(
8823 640,
8824 480,
8825 PixelFormat::Rgb,
8826 DType::U8,
8827 None,
8828 edgefirst_tensor::CpuAccess::ReadWrite,
8829 )
8830 .unwrap();
8831 assert_eq!(img.width(), Some(640));
8832 assert_eq!(img.height(), Some(480));
8833 assert_eq!(img.format(), Some(PixelFormat::Rgb));
8834 }
8835
8836 #[test]
8837 fn test_tensordyn_image_planar_rgb() {
8838 let img = TensorDyn::image(
8839 640,
8840 480,
8841 PixelFormat::PlanarRgb,
8842 DType::U8,
8843 None,
8844 edgefirst_tensor::CpuAccess::ReadWrite,
8845 )
8846 .unwrap();
8847 assert_eq!(img.width(), Some(640));
8848 assert_eq!(img.height(), Some(480));
8849 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
8850 }
8851
8852 #[test]
8853 fn test_rgb_int8_format() {
8854 let img = TensorDyn::image(
8856 1280,
8857 720,
8858 PixelFormat::Rgb,
8859 DType::I8,
8860 Some(TensorMemory::Mem),
8861 edgefirst_tensor::CpuAccess::ReadWrite,
8862 )
8863 .unwrap();
8864 assert_eq!(img.width(), Some(1280));
8865 assert_eq!(img.height(), Some(720));
8866 assert_eq!(img.format(), Some(PixelFormat::Rgb));
8867 assert_eq!(img.dtype(), DType::I8);
8868 }
8869
8870 #[test]
8871 fn test_planar_rgb_int8_format() {
8872 let img = TensorDyn::image(
8873 1280,
8874 720,
8875 PixelFormat::PlanarRgb,
8876 DType::I8,
8877 Some(TensorMemory::Mem),
8878 edgefirst_tensor::CpuAccess::ReadWrite,
8879 )
8880 .unwrap();
8881 assert_eq!(img.width(), Some(1280));
8882 assert_eq!(img.height(), Some(720));
8883 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
8884 assert_eq!(img.dtype(), DType::I8);
8885 }
8886
8887 #[test]
8888 fn test_rgb_from_tensor() {
8889 let mut tensor = Tensor::<u8>::new(&[720, 1280, 3], None, None).unwrap();
8890 tensor.set_format(PixelFormat::Rgb).unwrap();
8891 let img = TensorDyn::from(tensor);
8892 assert_eq!(img.width(), Some(1280));
8893 assert_eq!(img.height(), Some(720));
8894 assert_eq!(img.format(), Some(PixelFormat::Rgb));
8895 }
8896
8897 #[test]
8898 fn test_planar_rgb_from_tensor() {
8899 let mut tensor = Tensor::<u8>::new(&[3, 720, 1280], None, None).unwrap();
8900 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
8901 let img = TensorDyn::from(tensor);
8902 assert_eq!(img.width(), Some(1280));
8903 assert_eq!(img.height(), Some(720));
8904 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
8905 }
8906
8907 #[test]
8908 fn test_dtype_determines_int8() {
8909 let u8_img = TensorDyn::image(
8911 64,
8912 64,
8913 PixelFormat::Rgb,
8914 DType::U8,
8915 None,
8916 edgefirst_tensor::CpuAccess::ReadWrite,
8917 )
8918 .unwrap();
8919 let i8_img = TensorDyn::image(
8920 64,
8921 64,
8922 PixelFormat::Rgb,
8923 DType::I8,
8924 None,
8925 edgefirst_tensor::CpuAccess::ReadWrite,
8926 )
8927 .unwrap();
8928 assert_eq!(u8_img.dtype(), DType::U8);
8929 assert_eq!(i8_img.dtype(), DType::I8);
8930 }
8931
8932 #[test]
8933 fn test_pixel_layout_packed_vs_planar() {
8934 assert_eq!(PixelFormat::Rgb.layout(), PixelLayout::Packed);
8936 assert_eq!(PixelFormat::Rgba.layout(), PixelLayout::Packed);
8937 assert_eq!(PixelFormat::PlanarRgb.layout(), PixelLayout::Planar);
8938 assert_eq!(PixelFormat::Nv12.layout(), PixelLayout::SemiPlanar);
8939 }
8940
8941 #[cfg(target_os = "linux")]
8946 #[cfg(feature = "opengl")]
8947 #[test]
8948 fn test_convert_pbo_to_pbo() {
8949 let mut converter = ImageProcessor::new().unwrap();
8950
8951 let is_pbo = converter
8953 .opengl
8954 .as_ref()
8955 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
8956 if !is_pbo {
8957 eprintln!("Skipping test_convert_pbo_to_pbo: backend is not PBO");
8958 return;
8959 }
8960
8961 let src_w = 640;
8962 let src_h = 480;
8963 let dst_w = 320;
8964 let dst_h = 240;
8965
8966 let pbo_src = converter
8968 .create_image(
8969 src_w,
8970 src_h,
8971 PixelFormat::Rgba,
8972 DType::U8,
8973 None,
8974 edgefirst_tensor::CpuAccess::ReadWrite,
8975 )
8976 .unwrap();
8977 assert_eq!(
8978 pbo_src.as_u8().unwrap().memory(),
8979 TensorMemory::Pbo,
8980 "create_image should produce a PBO tensor"
8981 );
8982
8983 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
8985 let jpeg_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
8986
8987 let mem_src = TensorDyn::image(
8989 src_w,
8990 src_h,
8991 PixelFormat::Rgba,
8992 DType::U8,
8993 Some(TensorMemory::Mem),
8994 edgefirst_tensor::CpuAccess::ReadWrite,
8995 )
8996 .unwrap();
8997 let (result, _jpeg_src, mem_src) = convert_img(
8998 &mut CPUProcessor::new(),
8999 jpeg_src,
9000 mem_src,
9001 Rotation::None,
9002 Flip::None,
9003 Crop::no_crop(),
9004 );
9005 result.unwrap();
9006
9007 {
9009 let src_data = mem_src.as_u8().unwrap().map().unwrap();
9010 let mut pbo_map = pbo_src.as_u8().unwrap().map().unwrap();
9011 pbo_map.copy_from_slice(&src_data);
9012 }
9013
9014 let pbo_dst = converter
9016 .create_image(
9017 dst_w,
9018 dst_h,
9019 PixelFormat::Rgba,
9020 DType::U8,
9021 None,
9022 edgefirst_tensor::CpuAccess::ReadWrite,
9023 )
9024 .unwrap();
9025 assert_eq!(pbo_dst.as_u8().unwrap().memory(), TensorMemory::Pbo);
9026
9027 let mut pbo_dst = pbo_dst;
9029 let result = converter.convert(
9030 &pbo_src,
9031 &mut pbo_dst,
9032 Rotation::None,
9033 Flip::None,
9034 Crop::no_crop(),
9035 );
9036 result.unwrap();
9037
9038 let cpu_dst = TensorDyn::image(
9040 dst_w,
9041 dst_h,
9042 PixelFormat::Rgba,
9043 DType::U8,
9044 Some(TensorMemory::Mem),
9045 edgefirst_tensor::CpuAccess::ReadWrite,
9046 )
9047 .unwrap();
9048 let (result, _mem_src, cpu_dst) = convert_img(
9049 &mut CPUProcessor::new(),
9050 mem_src,
9051 cpu_dst,
9052 Rotation::None,
9053 Flip::None,
9054 Crop::no_crop(),
9055 );
9056 result.unwrap();
9057
9058 let pbo_dst_img = {
9059 let mut __t = pbo_dst.into_u8().unwrap();
9060 __t.set_format(PixelFormat::Rgba).unwrap();
9061 TensorDyn::from(__t)
9062 };
9063 compare_images(&pbo_dst_img, &cpu_dst, 0.95, function!());
9064 log::info!("test_convert_pbo_to_pbo: PASS — PBO-to-PBO convert matches CPU reference");
9065 }
9066
9067 #[test]
9068 fn test_image_bgra() {
9069 let img = TensorDyn::image(
9070 640,
9071 480,
9072 PixelFormat::Bgra,
9073 DType::U8,
9074 Some(edgefirst_tensor::TensorMemory::Mem),
9075 edgefirst_tensor::CpuAccess::ReadWrite,
9076 )
9077 .unwrap();
9078 assert_eq!(img.width(), Some(640));
9079 assert_eq!(img.height(), Some(480));
9080 assert_eq!(img.format().unwrap().channels(), 4);
9081 assert_eq!(img.format().unwrap(), PixelFormat::Bgra);
9082 }
9083
9084 #[test]
9089 fn test_force_backend_cpu() {
9090 let _lock = acquire_env_lock();
9091 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
9092 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
9093 let converter = ImageProcessor::new().unwrap();
9094 assert!(converter.cpu.is_some());
9095 assert_eq!(converter.forced_backend, Some(ForcedBackend::Cpu));
9096 }
9097
9098 #[test]
9099 fn test_force_backend_invalid() {
9100 let _lock = acquire_env_lock();
9101 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
9102 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "invalid") };
9103 let result = ImageProcessor::new();
9104 assert!(
9105 matches!(&result, Err(Error::ForcedBackendUnavailable(s)) if s.contains("unknown")),
9106 "invalid backend value should return ForcedBackendUnavailable error: {result:?}"
9107 );
9108 }
9109
9110 #[test]
9111 fn test_force_backend_unset() {
9112 let _lock = acquire_env_lock();
9113 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
9114 unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") };
9115 let converter = ImageProcessor::new().unwrap();
9116 assert!(converter.forced_backend.is_none());
9117 }
9118
9119 #[test]
9124 fn test_draw_proto_masks_no_cpu_returns_error() {
9125 let _lock = acquire_env_lock();
9127 let _guard = EnvGuard::snapshot(&[
9128 "EDGEFIRST_FORCE_BACKEND",
9129 "EDGEFIRST_DISABLE_GL",
9130 "EDGEFIRST_DISABLE_G2D",
9131 "EDGEFIRST_DISABLE_CPU",
9132 ]);
9133
9134 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
9136 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
9137 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
9138
9139 let mut converter = ImageProcessor::new().unwrap();
9140 assert!(converter.cpu.is_none(), "CPU should be disabled");
9141
9142 let dst = TensorDyn::image(
9143 640,
9144 480,
9145 PixelFormat::Rgba,
9146 DType::U8,
9147 Some(TensorMemory::Mem),
9148 edgefirst_tensor::CpuAccess::ReadWrite,
9149 )
9150 .unwrap();
9151 let mut dst_dyn = dst;
9152 let det = [DetectBox {
9153 bbox: edgefirst_decoder::BoundingBox {
9154 xmin: 0.1,
9155 ymin: 0.1,
9156 xmax: 0.5,
9157 ymax: 0.5,
9158 },
9159 score: 0.9,
9160 label: 0,
9161 }];
9162 let proto_data = {
9163 use edgefirst_tensor::{Tensor, TensorDyn};
9164 let coeff_t = Tensor::<f32>::from_slice(&[0.5_f32; 4], &[1, 4]).unwrap();
9165 let protos_t =
9166 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
9167 ProtoData {
9168 mask_coefficients: TensorDyn::F32(coeff_t),
9169 protos: TensorDyn::F32(protos_t),
9170 layout: ProtoLayout::Nhwc,
9171 }
9172 };
9173 let result =
9174 converter.draw_proto_masks(&mut dst_dyn, &det, &proto_data, Default::default());
9175 assert!(
9176 matches!(&result, Err(Error::Internal(s)) if s.contains("CPU backend")),
9177 "draw_proto_masks without CPU should return Internal error: {result:?}"
9178 );
9179 }
9180
9181 #[test]
9182 fn test_draw_proto_masks_cpu_fallback_works() {
9183 let _lock = acquire_env_lock();
9186 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
9187 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
9188 let mut converter = ImageProcessor::new().unwrap();
9189 assert!(converter.cpu.is_some());
9190
9191 let dst = TensorDyn::image(
9192 64,
9193 64,
9194 PixelFormat::Rgba,
9195 DType::U8,
9196 Some(TensorMemory::Mem),
9197 edgefirst_tensor::CpuAccess::ReadWrite,
9198 )
9199 .unwrap();
9200 let mut dst_dyn = dst;
9201 let det = [DetectBox {
9202 bbox: edgefirst_decoder::BoundingBox {
9203 xmin: 0.1,
9204 ymin: 0.1,
9205 xmax: 0.5,
9206 ymax: 0.5,
9207 },
9208 score: 0.9,
9209 label: 0,
9210 }];
9211 let proto_data = {
9212 use edgefirst_tensor::{Tensor, TensorDyn};
9213 let coeff_t = Tensor::<f32>::from_slice(&[0.5_f32; 4], &[1, 4]).unwrap();
9214 let protos_t =
9215 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
9216 ProtoData {
9217 mask_coefficients: TensorDyn::F32(coeff_t),
9218 protos: TensorDyn::F32(protos_t),
9219 layout: ProtoLayout::Nhwc,
9220 }
9221 };
9222 let result =
9223 converter.draw_proto_masks(&mut dst_dyn, &det, &proto_data, Default::default());
9224 assert!(result.is_ok(), "CPU fallback path should work: {result:?}");
9225 }
9226
9227 fn acquire_env_lock() -> std::sync::MutexGuard<'static, ()> {
9259 use std::sync::{Mutex, OnceLock};
9260 static ENV_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
9261 ENV_MUTEX
9262 .get_or_init(|| Mutex::new(()))
9263 .lock()
9264 .unwrap_or_else(|e| e.into_inner())
9265 }
9266
9267 struct EnvGuard {
9270 vars: Vec<(&'static str, Option<String>)>,
9271 }
9272
9273 impl EnvGuard {
9274 fn snapshot(names: &[&'static str]) -> Self {
9278 Self {
9279 vars: names.iter().map(|&k| (k, std::env::var(k).ok())).collect(),
9280 }
9281 }
9282 }
9283
9284 impl Drop for EnvGuard {
9285 fn drop(&mut self) {
9286 for (k, v) in &self.vars {
9287 match v {
9288 Some(s) => unsafe { std::env::set_var(k, s) },
9289 None => unsafe { std::env::remove_var(k) },
9290 }
9291 }
9292 }
9293 }
9294
9295 fn with_force_backend<R>(value: Option<&str>, body: impl FnOnce() -> R) -> R {
9299 let _lock = acquire_env_lock();
9300 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
9301 match value {
9302 Some(v) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", v) },
9303 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
9304 }
9305 body()
9306 }
9307
9308 fn make_dirty_dst(w: usize, h: usize, mem: Option<TensorMemory>) -> TensorDyn {
9313 let dst = TensorDyn::image(
9314 w,
9315 h,
9316 PixelFormat::Rgba,
9317 DType::U8,
9318 mem,
9319 edgefirst_tensor::CpuAccess::ReadWrite,
9320 )
9321 .unwrap();
9322 {
9323 use edgefirst_tensor::TensorMapTrait;
9324 let u8t = dst.as_u8().unwrap();
9325 let mut map = u8t.map().unwrap();
9326 for (i, b) in map.as_mut_slice().iter_mut().enumerate() {
9327 *b = 0xA0u8.wrapping_add((i as u8) & 0x3F);
9328 }
9329 }
9330 dst
9331 }
9332
9333 fn make_bg(w: usize, h: usize, mem: Option<TensorMemory>, rgba: [u8; 4]) -> TensorDyn {
9335 let bg = TensorDyn::image(
9336 w,
9337 h,
9338 PixelFormat::Rgba,
9339 DType::U8,
9340 mem,
9341 edgefirst_tensor::CpuAccess::ReadWrite,
9342 )
9343 .unwrap();
9344 {
9345 use edgefirst_tensor::TensorMapTrait;
9346 let u8t = bg.as_u8().unwrap();
9347 let mut map = u8t.map().unwrap();
9348 for chunk in map.as_mut_slice().chunks_exact_mut(4) {
9349 chunk.copy_from_slice(&rgba);
9350 }
9351 }
9352 bg
9353 }
9354
9355 fn pixel_at(dst: &TensorDyn, x: usize, y: usize) -> [u8; 4] {
9356 use edgefirst_tensor::TensorMapTrait;
9357 let w = dst.width().unwrap();
9358 let off = (y * w + x) * 4;
9359 let u8t = dst.as_u8().unwrap();
9360 let map = u8t.map().unwrap();
9361 let s = map.as_slice();
9362 [s[off], s[off + 1], s[off + 2], s[off + 3]]
9363 }
9364
9365 fn assert_every_pixel_eq(dst: &TensorDyn, expected: [u8; 4], case: &str) {
9366 use edgefirst_tensor::TensorMapTrait;
9367 let u8t = dst.as_u8().unwrap();
9368 let map = u8t.map().unwrap();
9369 for (i, chunk) in map.as_slice().chunks_exact(4).enumerate() {
9370 assert_eq!(
9371 chunk, &expected,
9372 "{case}: pixel idx {i} = {chunk:?}, expected {expected:?}"
9373 );
9374 }
9375 }
9376
9377 fn scenario_empty_no_bg(processor: &mut ImageProcessor, case: &str) {
9380 let mut dst = make_dirty_dst(64, 64, None);
9381 processor
9382 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::default())
9383 .unwrap_or_else(|e| panic!("{case}/decoded_masks empty+no-bg failed: {e:?}"));
9384 assert_every_pixel_eq(&dst, [0, 0, 0, 0], &format!("{case}/decoded"));
9385
9386 let mut dst = make_dirty_dst(64, 64, None);
9387 let proto = {
9388 use edgefirst_tensor::{Tensor, TensorDyn};
9389 let coeff_t = Tensor::<f32>::from_slice(&[0.0_f32; 4], &[1, 4]).unwrap();
9391 let protos_t =
9392 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
9393 ProtoData {
9394 mask_coefficients: TensorDyn::F32(coeff_t),
9395 protos: TensorDyn::F32(protos_t),
9396 layout: ProtoLayout::Nhwc,
9397 }
9398 };
9399 processor
9400 .draw_proto_masks(&mut dst, &[], &proto, MaskOverlay::default())
9401 .unwrap_or_else(|e| panic!("{case}/proto_masks empty+no-bg failed: {e:?}"));
9402 assert_every_pixel_eq(&dst, [0, 0, 0, 0], &format!("{case}/proto"));
9403 }
9404
9405 fn scenario_empty_with_bg(processor: &mut ImageProcessor, case: &str) {
9408 let bg_color = [42, 99, 200, 255];
9409 let bg = make_bg(64, 64, None, bg_color);
9410 let overlay = MaskOverlay::new().with_background(&bg);
9411
9412 let mut dst = make_dirty_dst(64, 64, None);
9413 processor
9414 .draw_decoded_masks(&mut dst, &[], &[], overlay)
9415 .unwrap_or_else(|e| panic!("{case}/decoded_masks empty+bg failed: {e:?}"));
9416 assert_every_pixel_eq(&dst, bg_color, &format!("{case}/decoded bg blit"));
9417
9418 let mut dst = make_dirty_dst(64, 64, None);
9419 let proto = {
9420 use edgefirst_tensor::{Tensor, TensorDyn};
9421 let coeff_t = Tensor::<f32>::from_slice(&[0.0_f32; 4], &[1, 4]).unwrap();
9423 let protos_t =
9424 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
9425 ProtoData {
9426 mask_coefficients: TensorDyn::F32(coeff_t),
9427 protos: TensorDyn::F32(protos_t),
9428 layout: ProtoLayout::Nhwc,
9429 }
9430 };
9431 processor
9432 .draw_proto_masks(&mut dst, &[], &proto, overlay)
9433 .unwrap_or_else(|e| panic!("{case}/proto_masks empty+bg failed: {e:?}"));
9434 assert_every_pixel_eq(&dst, bg_color, &format!("{case}/proto bg blit"));
9435 }
9436
9437 fn scenario_detect_no_bg(processor: &mut ImageProcessor, case: &str) {
9441 use edgefirst_decoder::Segmentation;
9442 use ndarray::Array3;
9443 processor
9444 .set_class_colors(&[[200, 80, 40, 255]])
9445 .expect("set_class_colors");
9446
9447 let detect = DetectBox {
9448 bbox: [0.25, 0.25, 0.75, 0.75].into(),
9449 score: 0.99,
9450 label: 0,
9451 };
9452 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
9453 let seg = Segmentation {
9454 segmentation: seg_arr,
9455 xmin: 0.25,
9456 ymin: 0.25,
9457 xmax: 0.75,
9458 ymax: 0.75,
9459 };
9460
9461 let mut dst = make_dirty_dst(64, 64, None);
9462 processor
9463 .draw_decoded_masks(&mut dst, &[detect], &[seg], MaskOverlay::default())
9464 .unwrap_or_else(|e| panic!("{case}/decoded_masks detect+no-bg failed: {e:?}"));
9465
9466 let corner = pixel_at(&dst, 2, 2);
9468 assert_eq!(
9469 corner,
9470 [0, 0, 0, 0],
9471 "{case}/decoded: corner (2,2) leaked dirty pattern: {corner:?}"
9472 );
9473 let center = pixel_at(&dst, 32, 32);
9477 assert!(
9478 center != [0, 0, 0, 0],
9479 "{case}/decoded: center (32,32) was not coloured: {center:?}"
9480 );
9481 }
9482
9483 fn scenario_detect_with_bg(processor: &mut ImageProcessor, case: &str) {
9486 use edgefirst_decoder::Segmentation;
9487 use ndarray::Array3;
9488 processor
9489 .set_class_colors(&[[200, 80, 40, 255]])
9490 .expect("set_class_colors");
9491 let bg_color = [10, 20, 30, 255];
9492 let bg = make_bg(64, 64, None, bg_color);
9493
9494 let detect = DetectBox {
9495 bbox: [0.25, 0.25, 0.75, 0.75].into(),
9496 score: 0.99,
9497 label: 0,
9498 };
9499 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
9500 let seg = Segmentation {
9501 segmentation: seg_arr,
9502 xmin: 0.25,
9503 ymin: 0.25,
9504 xmax: 0.75,
9505 ymax: 0.75,
9506 };
9507
9508 let overlay = MaskOverlay::new().with_background(&bg);
9509 let mut dst = make_dirty_dst(64, 64, None);
9510 processor
9511 .draw_decoded_masks(&mut dst, &[detect], &[seg], overlay)
9512 .unwrap_or_else(|e| panic!("{case}/decoded_masks detect+bg failed: {e:?}"));
9513
9514 let corner = pixel_at(&dst, 2, 2);
9516 assert_eq!(
9517 corner, bg_color,
9518 "{case}/decoded: corner (2,2) should show bg {bg_color:?} got {corner:?}"
9519 );
9520 let center = pixel_at(&dst, 32, 32);
9523 assert!(
9524 center != bg_color,
9525 "{case}/decoded: center (32,32) should differ from bg {bg_color:?}, got {center:?}"
9526 );
9527 }
9528
9529 fn run_all_scenarios(
9532 force_backend: Option<&'static str>,
9533 case: &'static str,
9534 require_dma_for_bg: bool,
9535 ) {
9536 if require_dma_for_bg && !edgefirst_tensor::is_dma_available() {
9537 eprintln!("SKIPPED: {case} — DMA not available on this host");
9538 return;
9539 }
9540 let processor_result = with_force_backend(force_backend, ImageProcessor::new);
9541 let mut processor = match processor_result {
9542 Ok(p) => p,
9543 Err(e) => {
9544 eprintln!("SKIPPED: {case} — backend init failed: {e:?}");
9545 return;
9546 }
9547 };
9548 scenario_empty_no_bg(&mut processor, case);
9549 scenario_empty_with_bg(&mut processor, case);
9550 scenario_detect_no_bg(&mut processor, case);
9551 scenario_detect_with_bg(&mut processor, case);
9552 }
9553
9554 #[test]
9555 fn test_draw_masks_4_scenarios_cpu() {
9556 run_all_scenarios(Some("cpu"), "cpu", false);
9557 }
9558
9559 #[test]
9560 fn test_draw_masks_4_scenarios_auto() {
9561 run_all_scenarios(None, "auto", false);
9562 }
9563
9564 #[cfg(target_os = "linux")]
9565 #[cfg(feature = "opengl")]
9566 #[test]
9567 fn test_draw_masks_4_scenarios_opengl() {
9568 run_all_scenarios(Some("opengl"), "opengl", false);
9569 }
9570
9571 #[cfg(target_os = "linux")]
9576 #[test]
9577 fn test_draw_masks_zero_detection_g2d_forced() {
9578 if !edgefirst_tensor::is_dma_available() {
9579 eprintln!("SKIPPED: g2d forced — DMA not available on this host");
9580 return;
9581 }
9582 let processor_result = with_force_backend(Some("g2d"), ImageProcessor::new);
9583 let mut processor = match processor_result {
9584 Ok(p) => p,
9585 Err(e) => {
9586 eprintln!("SKIPPED: g2d forced — init failed: {e:?}");
9587 return;
9588 }
9589 };
9590
9591 let mut dst = TensorDyn::image(
9593 64,
9594 64,
9595 PixelFormat::Rgba,
9596 DType::U8,
9597 Some(TensorMemory::Dma),
9598 edgefirst_tensor::CpuAccess::ReadWrite,
9599 )
9600 .unwrap();
9601 {
9602 use edgefirst_tensor::TensorMapTrait;
9603 let u8t = dst.as_u8_mut().unwrap();
9604 let mut map = u8t.map().unwrap();
9605 map.as_mut_slice().fill(0xBB);
9606 }
9607 processor
9608 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::default())
9609 .expect("g2d empty+no-bg");
9610 assert_every_pixel_eq(&dst, [0, 0, 0, 0], "g2d/case1 cleared");
9611
9612 let bg_color = [7, 11, 13, 255];
9614 let bg = {
9615 let t = TensorDyn::image(
9616 64,
9617 64,
9618 PixelFormat::Rgba,
9619 DType::U8,
9620 Some(TensorMemory::Dma),
9621 edgefirst_tensor::CpuAccess::ReadWrite,
9622 )
9623 .unwrap();
9624 {
9625 use edgefirst_tensor::TensorMapTrait;
9626 let u8t = t.as_u8().unwrap();
9627 let mut map = u8t.map().unwrap();
9628 for chunk in map.as_mut_slice().chunks_exact_mut(4) {
9629 chunk.copy_from_slice(&bg_color);
9630 }
9631 }
9632 t
9633 };
9634 let mut dst = TensorDyn::image(
9635 64,
9636 64,
9637 PixelFormat::Rgba,
9638 DType::U8,
9639 Some(TensorMemory::Dma),
9640 edgefirst_tensor::CpuAccess::ReadWrite,
9641 )
9642 .unwrap();
9643 {
9644 use edgefirst_tensor::TensorMapTrait;
9645 let u8t = dst.as_u8_mut().unwrap();
9646 let mut map = u8t.map().unwrap();
9647 map.as_mut_slice().fill(0x55);
9648 }
9649 processor
9650 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::new().with_background(&bg))
9651 .expect("g2d empty+bg");
9652 assert_every_pixel_eq(&dst, bg_color, "g2d/case2 bg blit");
9653
9654 let detect = DetectBox {
9656 bbox: [0.25, 0.25, 0.75, 0.75].into(),
9657 score: 0.9,
9658 label: 0,
9659 };
9660 let mut dst = TensorDyn::image(
9661 64,
9662 64,
9663 PixelFormat::Rgba,
9664 DType::U8,
9665 Some(TensorMemory::Dma),
9666 edgefirst_tensor::CpuAccess::ReadWrite,
9667 )
9668 .unwrap();
9669 let err = processor
9670 .draw_decoded_masks(&mut dst, &[detect], &[], MaskOverlay::default())
9671 .expect_err("g2d must reject detect-present draw_decoded_masks");
9672 assert!(
9673 matches!(err, Error::NotImplemented(_)),
9674 "g2d case3 wrong error: {err:?}"
9675 );
9676 }
9677
9678 #[test]
9679 fn test_set_format_then_cpu_convert() {
9680 let _lock = acquire_env_lock();
9683 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
9684 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
9685 let mut processor = ImageProcessor::new().unwrap();
9686
9687 let image = edgefirst_bench::testdata::read("zidane.jpg");
9689 let src = load_image_test_helper(&image, Some(PixelFormat::Rgba), None).unwrap();
9690
9691 let mut dst =
9693 TensorDyn::new(&[640, 640, 3], DType::U8, Some(TensorMemory::Mem), None).unwrap();
9694 dst.set_format(PixelFormat::Rgb).unwrap();
9695
9696 processor
9698 .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
9699 .unwrap();
9700
9701 assert_eq!(dst.format(), Some(PixelFormat::Rgb));
9703 assert_eq!(dst.width(), Some(640));
9704 assert_eq!(dst.height(), Some(640));
9705 }
9706
9707 #[test]
9713 fn test_multiple_image_processors_same_thread() {
9714 let _lock = acquire_env_lock();
9717 let mut processors: Vec<ImageProcessor> = (0..4)
9718 .map(|_| ImageProcessor::new().expect("ImageProcessor::new() failed"))
9719 .collect();
9720
9721 for proc in &mut processors {
9722 let src = proc
9723 .create_image(
9724 128,
9725 128,
9726 PixelFormat::Rgb,
9727 DType::U8,
9728 None,
9729 edgefirst_tensor::CpuAccess::ReadWrite,
9730 )
9731 .expect("create src failed");
9732 let mut dst = proc
9733 .create_image(
9734 64,
9735 64,
9736 PixelFormat::Rgb,
9737 DType::U8,
9738 None,
9739 edgefirst_tensor::CpuAccess::ReadWrite,
9740 )
9741 .expect("create dst failed");
9742 proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
9743 .expect("convert failed");
9744 assert_eq!(dst.width(), Some(64));
9745 assert_eq!(dst.height(), Some(64));
9746 }
9747 }
9748
9749 #[test]
9756 fn test_multiple_image_processors_separate_threads() {
9757 use std::sync::mpsc;
9758 use std::time::Duration;
9759
9760 if std::env::var_os("EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS").is_some() {
9770 eprintln!(
9771 "SKIPPED: test_multiple_image_processors_separate_threads — known Vivante \
9772 GC7000UL concurrent-EGL-teardown double-free \
9773 (EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS set)"
9774 );
9775 return;
9776 }
9777
9778 const TIMEOUT: Duration = Duration::from_secs(60);
9779
9780 let _lock = acquire_env_lock();
9783
9784 let (tx, rx) = mpsc::channel::<()>();
9785
9786 std::thread::spawn(move || {
9787 let handles: Vec<_> = (0..4)
9788 .map(|i| {
9789 std::thread::spawn(move || {
9790 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
9791 panic!("ImageProcessor::new() failed on thread {i}: {e}")
9792 });
9793 let src = proc
9794 .create_image(
9795 128,
9796 128,
9797 PixelFormat::Rgb,
9798 DType::U8,
9799 None,
9800 edgefirst_tensor::CpuAccess::ReadWrite,
9801 )
9802 .unwrap_or_else(|e| panic!("create src failed on thread {i}: {e}"));
9803 let mut dst = proc
9804 .create_image(
9805 64,
9806 64,
9807 PixelFormat::Rgb,
9808 DType::U8,
9809 None,
9810 edgefirst_tensor::CpuAccess::ReadWrite,
9811 )
9812 .unwrap_or_else(|e| panic!("create dst failed on thread {i}: {e}"));
9813 proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
9814 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
9815 assert_eq!(dst.width(), Some(64));
9816 assert_eq!(dst.height(), Some(64));
9817 })
9818 })
9819 .collect();
9820
9821 for (i, h) in handles.into_iter().enumerate() {
9822 h.join()
9823 .unwrap_or_else(|e| panic!("thread {i} panicked: {e:?}"));
9824 }
9825
9826 let _ = tx.send(());
9827 });
9828
9829 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
9830 panic!("test_multiple_image_processors_separate_threads timed out after {TIMEOUT:?}")
9831 });
9832 }
9833
9834 #[test]
9841 fn test_image_processors_concurrent_operations() {
9842 use std::sync::{mpsc, Arc, Barrier};
9843 use std::time::Duration;
9844
9845 const N: usize = 4;
9846 const ROUNDS: usize = 10;
9847 const TIMEOUT: Duration = Duration::from_secs(60);
9848
9849 let _lock = acquire_env_lock();
9852
9853 let (tx, rx) = mpsc::channel::<()>();
9854
9855 std::thread::spawn(move || {
9856 let barrier = Arc::new(Barrier::new(N));
9857
9858 let handles: Vec<_> = (0..N)
9859 .map(|i| {
9860 let barrier = Arc::clone(&barrier);
9861 std::thread::spawn(move || {
9862 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
9863 panic!("ImageProcessor::new() failed on thread {i}: {e}")
9864 });
9865
9866 barrier.wait();
9868
9869 for round in 0..ROUNDS {
9871 let src = proc
9872 .create_image(
9873 128,
9874 128,
9875 PixelFormat::Rgb,
9876 DType::U8,
9877 None,
9878 edgefirst_tensor::CpuAccess::ReadWrite,
9879 )
9880 .unwrap_or_else(|e| {
9881 panic!("create src failed on thread {i} round {round}: {e}")
9882 });
9883 let mut dst = proc
9884 .create_image(
9885 64,
9886 64,
9887 PixelFormat::Rgb,
9888 DType::U8,
9889 None,
9890 edgefirst_tensor::CpuAccess::ReadWrite,
9891 )
9892 .unwrap_or_else(|e| {
9893 panic!("create dst failed on thread {i} round {round}: {e}")
9894 });
9895 proc.convert(
9896 &src,
9897 &mut dst,
9898 Rotation::None,
9899 Flip::None,
9900 Crop::default(),
9901 )
9902 .unwrap_or_else(|e| {
9903 panic!("convert failed on thread {i} round {round}: {e}")
9904 });
9905 assert_eq!(dst.width(), Some(64));
9906 assert_eq!(dst.height(), Some(64));
9907 }
9908 })
9909 })
9910 .collect();
9911
9912 for (i, h) in handles.into_iter().enumerate() {
9913 h.join()
9914 .unwrap_or_else(|e| panic!("thread {i} panicked: {e:?}"));
9915 }
9916
9917 let _ = tx.send(());
9918 });
9919
9920 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
9921 panic!("test_image_processors_concurrent_operations timed out after {TIMEOUT:?}")
9922 });
9923 }
9924
9925 #[test]
9943 fn test_parallel_processors_unique_outputs() {
9944 use std::sync::{mpsc, Arc, Barrier};
9945 use std::time::Duration;
9946
9947 const N: usize = 4;
9948 const ROUNDS: usize = 25;
9949 const TIMEOUT: Duration = Duration::from_secs(60);
9950
9951 if std::env::var_os("EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS").is_some() {
9952 eprintln!(
9953 "SKIPPED: test_parallel_processors_unique_outputs — known Vivante \
9954 GC7000UL concurrent-multi-processor driver abort \
9955 (EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS set)"
9956 );
9957 return;
9958 }
9959
9960 let _lock = acquire_env_lock();
9961 let (tx, rx) = mpsc::channel::<()>();
9962
9963 std::thread::spawn(move || {
9964 let barrier = Arc::new(Barrier::new(N));
9965 let handles: Vec<_> = (0..N)
9966 .map(|i| {
9967 let barrier = Arc::clone(&barrier);
9968 std::thread::spawn(move || {
9969 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
9970 panic!("ImageProcessor::new() failed on thread {i}: {e}")
9971 });
9972 let (w, h) = (640usize, 480usize);
9974 let mem = if edgefirst_tensor::is_dma_available() {
9975 Some(TensorMemory::Dma)
9976 } else {
9977 Some(TensorMemory::Mem)
9978 };
9979 let src = proc
9980 .create_image(
9981 w,
9982 h,
9983 PixelFormat::Nv12,
9984 DType::U8,
9985 mem,
9986 edgefirst_tensor::CpuAccess::ReadWrite,
9987 )
9988 .unwrap();
9989 {
9990 let t = src.as_u8().unwrap();
9991 let mut m = t.map().unwrap();
9992 let s = m.as_mut_slice();
9993 for (j, b) in s[..w * h].iter_mut().enumerate() {
9994 *b = ((i * 53 + j) % 200 + 16) as u8;
9995 }
9996 for b in &mut s[w * h..] {
9997 *b = (80 + i * 24) as u8;
9998 }
9999 }
10000 let lb = Crop::letterbox([114, 114, 114, 255]);
10001 let convert_once = |proc: &mut ImageProcessor| -> Vec<u8> {
10002 let mut dst = proc
10003 .create_image(
10004 320,
10005 320,
10006 PixelFormat::Rgba,
10007 DType::U8,
10008 mem,
10009 edgefirst_tensor::CpuAccess::ReadWrite,
10010 )
10011 .unwrap();
10012 proc.convert(&src, &mut dst, Rotation::None, Flip::None, lb)
10013 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
10014 let t = dst.as_u8().unwrap();
10015 let m = t.map().unwrap();
10016 m.as_slice().to_vec()
10017 };
10018
10019 let oracle = convert_once(&mut proc);
10020 barrier.wait();
10021 for round in 0..ROUNDS {
10022 let out = convert_once(&mut proc);
10023 let diffs = oracle.iter().zip(&out).filter(|(a, b)| a != b).count();
10024 assert!(
10025 diffs == 0,
10026 "thread {i} round {round}: {diffs}/{} bytes diverged \
10027 from this processor's own oracle — cross-processor \
10028 GL state leakage under parallel execution",
10029 oracle.len()
10030 );
10031 }
10032 })
10033 })
10034 .collect();
10035
10036 for (i, h) in handles.into_iter().enumerate() {
10037 h.join()
10038 .unwrap_or_else(|e| panic!("parallel thread {i} panicked: {e:?}"));
10039 }
10040 let _ = tx.send(());
10041 });
10042
10043 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
10044 panic!("test_parallel_processors_unique_outputs timed out after {TIMEOUT:?}")
10045 });
10046 }
10047
10048 #[test]
10057 #[ignore = "heavy on-demand GL-parallelism stressor; run explicitly on boards"]
10058 fn stress_parallel_processors_oracle() {
10059 use std::sync::{mpsc, Arc, Barrier};
10060 use std::time::Duration;
10061
10062 const N: usize = 4;
10063 const ROUNDS: usize = 200;
10064 const TIMEOUT: Duration = Duration::from_secs(600);
10065
10066 let _lock = acquire_env_lock();
10067 let (tx, rx) = mpsc::channel::<()>();
10068
10069 std::thread::spawn(move || {
10070 let barrier = Arc::new(Barrier::new(N));
10071 let handles: Vec<_> = (0..N)
10072 .map(|i| {
10073 let barrier = Arc::clone(&barrier);
10074 std::thread::spawn(move || {
10075 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
10076 panic!("ImageProcessor::new() failed on thread {i}: {e}")
10077 });
10078 let (w, h) = (1280usize, 720usize);
10079 let mem = if edgefirst_tensor::is_dma_available() {
10080 Some(TensorMemory::Dma)
10081 } else {
10082 Some(TensorMemory::Mem)
10083 };
10084
10085 let src = proc
10088 .create_image(
10089 w,
10090 h,
10091 PixelFormat::Nv12,
10092 DType::U8,
10093 mem,
10094 edgefirst_tensor::CpuAccess::ReadWrite,
10095 )
10096 .unwrap();
10097 {
10098 let t = src.as_u8().unwrap();
10099 let mut m = t.map().unwrap();
10100 let s = m.as_mut_slice();
10101 for (j, b) in s[..w * h].iter_mut().enumerate() {
10102 *b = ((i * 37 + j) % 200 + 16) as u8;
10103 }
10104 for b in &mut s[w * h..] {
10105 *b = (96 + i * 16) as u8;
10106 }
10107 }
10108 let lb = Crop::letterbox([114, 114, 114, 255]);
10109
10110 let convert_once = |proc: &mut ImageProcessor| -> Vec<u8> {
10111 let mut dst = proc
10112 .create_image(
10113 640,
10114 640,
10115 PixelFormat::Rgb,
10116 DType::U8,
10117 mem,
10118 edgefirst_tensor::CpuAccess::ReadWrite,
10119 )
10120 .unwrap();
10121 proc.convert(&src, &mut dst, Rotation::None, Flip::None, lb)
10122 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
10123 let t = dst.as_u8().unwrap();
10124 let m = t.map().unwrap();
10125 m.as_slice().to_vec()
10126 };
10127
10128 let oracle = convert_once(&mut proc);
10129 barrier.wait();
10130 for round in 0..ROUNDS {
10131 let out = convert_once(&mut proc);
10132 let diffs = oracle.iter().zip(&out).filter(|(a, b)| a != b).count();
10133 assert!(
10134 diffs == 0,
10135 "thread {i} round {round}: {diffs}/{} bytes diverged \
10136 from the pre-barrier oracle",
10137 oracle.len()
10138 );
10139 }
10140 })
10141 })
10142 .collect();
10143
10144 for (i, h) in handles.into_iter().enumerate() {
10145 h.join()
10146 .unwrap_or_else(|e| panic!("stressor thread {i} panicked: {e:?}"));
10147 }
10148 let _ = tx.send(());
10149 });
10150
10151 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
10152 panic!("stress_parallel_processors_oracle timed out after {TIMEOUT:?}")
10153 });
10154 }
10155
10156 #[test]
10169 fn convert_f32_auto_never_errors_non_gl_combo() {
10170 const W: usize = 64;
10171 const H: usize = 64;
10172
10173 let src = TensorDyn::image(
10176 W,
10177 H,
10178 PixelFormat::Yuyv,
10179 DType::U8,
10180 Some(TensorMemory::Mem),
10181 edgefirst_tensor::CpuAccess::ReadWrite,
10182 )
10183 .unwrap();
10184 {
10185 let mut map = src.as_u8().unwrap().map().unwrap();
10186 let data = map.as_mut_slice();
10187 for chunk in data.chunks_exact_mut(4) {
10188 chunk[0] = 128; chunk[1] = 128; chunk[2] = 160; chunk[3] = 128; }
10193 }
10194
10195 let mut dst = TensorDyn::image(
10196 W,
10197 H,
10198 PixelFormat::Rgb,
10199 DType::F32,
10200 Some(TensorMemory::Mem),
10201 edgefirst_tensor::CpuAccess::ReadWrite,
10202 )
10203 .unwrap();
10204
10205 let mut proc = ImageProcessor::new().unwrap();
10206 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
10207 assert!(
10208 result.is_ok(),
10209 "auto-chain Yuyv→Rgb F32 must not error: {:?}",
10210 result.err()
10211 );
10212
10213 let map = dst.as_f32().unwrap().map().unwrap();
10215 let floats = map.as_slice();
10216 assert_eq!(floats.len(), W * H * 3, "unexpected output element count");
10217 for (i, &v) in floats.iter().enumerate() {
10218 assert!(
10219 v.is_finite() && (0.0..=1.0).contains(&v),
10220 "output[{i}]={v} is not finite or not in [0,1]"
10221 );
10222 }
10223
10224 let first_non_zero = floats.iter().find(|&&v| v > 0.01);
10228 assert!(
10229 first_non_zero.is_some(),
10230 "all-zero output detected — CPU path likely did not write to the destination buffer"
10231 );
10232 let r0 = floats[0];
10235 assert!(
10236 (r0 - 0.502_f32).abs() < 0.05,
10237 "first pixel R={r0} expected ≈0.502 (Y=128 neutral grey from YUYV source)"
10238 );
10239 }
10240
10241 #[test]
10247 #[allow(clippy::needless_update)]
10253 fn convert_f16_forced_cpu_correct() {
10254 const W: usize = 16;
10255 const H: usize = 16;
10256 const TOL: f32 = 1.0 / 512.0; let src = TensorDyn::image(
10260 W,
10261 H,
10262 PixelFormat::Rgba,
10263 DType::U8,
10264 Some(TensorMemory::Mem),
10265 edgefirst_tensor::CpuAccess::ReadWrite,
10266 )
10267 .unwrap();
10268 {
10269 let mut map = src.as_u8().unwrap().map().unwrap();
10270 let data = map.as_mut_slice();
10271 for y in 0..H {
10272 for x in 0..W {
10273 let i = y * W + x;
10274 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;
10278 }
10279 }
10280 }
10281
10282 let mut dst = TensorDyn::image(
10283 W,
10284 H,
10285 PixelFormat::PlanarRgb,
10286 DType::F16,
10287 Some(TensorMemory::Mem),
10288 edgefirst_tensor::CpuAccess::ReadWrite,
10289 )
10290 .unwrap();
10291
10292 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
10293 backend: ComputeBackend::Cpu,
10294 ..Default::default()
10295 })
10296 .unwrap();
10297 proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
10298 .expect("forced-CPU Rgba→PlanarRgb F16 must not error");
10299
10300 let src_map = src.as_u8().unwrap().map().unwrap();
10301 let src_bytes = src_map.as_slice();
10302 let dst_map = dst.as_f16().unwrap().map().unwrap();
10303 let dst_halfs = dst_map.as_slice();
10304
10305 let plane = W * H;
10306 assert_eq!(dst_halfs.len(), plane * 3, "wrong output element count");
10307
10308 for y in 0..H {
10309 for x in 0..W {
10310 let i = y * W + x;
10311 let r_exp = src_bytes[i * 4] as f32 / 255.0;
10312 let g_exp = src_bytes[i * 4 + 1] as f32 / 255.0;
10313 let b_exp = src_bytes[i * 4 + 2] as f32 / 255.0;
10314
10315 let r_got = dst_halfs[i].to_f32();
10316 let g_got = dst_halfs[plane + i].to_f32();
10317 let b_got = dst_halfs[2 * plane + i].to_f32();
10318
10319 assert!(
10320 (r_got - r_exp).abs() <= TOL,
10321 "R plane ({x},{y}): got {r_got}, expected {r_exp}"
10322 );
10323 assert!(
10324 (g_got - g_exp).abs() <= TOL,
10325 "G plane ({x},{y}): got {g_got}, expected {g_exp}"
10326 );
10327 assert!(
10328 (b_got - b_exp).abs() <= TOL,
10329 "B plane ({x},{y}): got {b_got}, expected {b_exp}"
10330 );
10331
10332 if src_bytes[i * 4] != src_bytes[i * 4 + 1] {
10334 assert_ne!(r_got, g_got, "R and G planes must differ at ({x},{y})");
10335 }
10336 }
10337 }
10338 }
10339
10340 #[test]
10348 fn convert_f32_with_rotation_falls_back() {
10349 const W: usize = 16;
10350 const H: usize = 16;
10351
10352 let src = TensorDyn::image(
10354 W,
10355 H,
10356 PixelFormat::Rgba,
10357 DType::U8,
10358 Some(TensorMemory::Mem),
10359 edgefirst_tensor::CpuAccess::ReadWrite,
10360 )
10361 .unwrap();
10362 {
10363 let mut map = src.as_u8().unwrap().map().unwrap();
10364 let data = map.as_mut_slice();
10365 for y in 0..H {
10366 for x in 0..W {
10367 let i = y * W + x;
10368 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;
10372 }
10373 }
10374 }
10375
10376 let mut dst = TensorDyn::image(
10378 H, W, PixelFormat::Rgb,
10381 DType::F32,
10382 Some(TensorMemory::Mem),
10383 edgefirst_tensor::CpuAccess::ReadWrite,
10384 )
10385 .unwrap();
10386
10387 let mut proc = ImageProcessor::new().unwrap();
10388 let result = proc.convert(
10389 &src,
10390 &mut dst,
10391 Rotation::Clockwise90,
10392 Flip::None,
10393 Crop::default(),
10394 );
10395 assert!(
10396 result.is_ok(),
10397 "auto-chain Rgba→Rgb F32 with Rot90 must not error: {:?}",
10398 result.err()
10399 );
10400
10401 let map = dst.as_f32().unwrap().map().unwrap();
10402 let floats = map.as_slice();
10403 assert_eq!(floats.len(), H * W * 3, "unexpected output element count");
10404 for (i, &v) in floats.iter().enumerate() {
10405 assert!(
10406 v.is_finite() && (0.0..=1.0).contains(&v),
10407 "output[{i}]={v} is not finite or not in [0,1]"
10408 );
10409 }
10410 }
10411
10412 #[test]
10419 #[cfg(all(target_os = "linux", feature = "opengl"))]
10420 fn convert_f16_gl_cpu_parity_identity() {
10421 if !is_opengl_available() {
10422 eprintln!("SKIPPED: convert_f16_gl_cpu_parity_identity - OpenGL not available");
10423 return;
10424 }
10425
10426 const W: usize = 16;
10427 const H: usize = 16;
10428 const TOL: f32 = 1.0 / 256.0; let src = TensorDyn::image(
10432 W,
10433 H,
10434 PixelFormat::Rgba,
10435 DType::U8,
10436 Some(TensorMemory::Mem),
10437 edgefirst_tensor::CpuAccess::ReadWrite,
10438 )
10439 .unwrap();
10440 {
10441 let mut map = src.as_u8().unwrap().map().unwrap();
10442 let data = map.as_mut_slice();
10443 for y in 0..H {
10444 for x in 0..W {
10445 let i = y * W + x;
10446 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;
10450 }
10451 }
10452 }
10453
10454 let gl_result = {
10456 let mut gl_proc = match ImageProcessor::with_config(ImageProcessorConfig {
10457 backend: ComputeBackend::OpenGl,
10458 ..Default::default()
10459 }) {
10460 Ok(p) => p,
10461 Err(e) => {
10462 eprintln!(
10463 "SKIPPED: convert_f16_gl_cpu_parity_identity - GL backend unavailable: {e}"
10464 );
10465 return;
10466 }
10467 };
10468
10469 if !gl_proc.supported_render_dtypes().f16 {
10470 eprintln!("SKIPPED: convert_f16_gl_cpu_parity_identity - F16 render not supported");
10471 return;
10472 }
10473
10474 let mut dst = TensorDyn::image(
10475 W,
10476 H,
10477 PixelFormat::PlanarRgb,
10478 DType::F16,
10479 Some(TensorMemory::Mem),
10480 edgefirst_tensor::CpuAccess::ReadWrite,
10481 )
10482 .unwrap();
10483 match gl_proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default()) {
10484 Ok(()) => dst,
10485 Err(e) => {
10486 eprintln!(
10487 "SKIPPED: convert_f16_gl_cpu_parity_identity - GL convert failed: {e}"
10488 );
10489 return;
10490 }
10491 }
10492 };
10493
10494 let cpu_result = {
10496 let mut cpu_proc = ImageProcessor::with_config(ImageProcessorConfig {
10497 backend: ComputeBackend::Cpu,
10498 ..Default::default()
10499 })
10500 .unwrap();
10501 let mut dst = TensorDyn::image(
10502 W,
10503 H,
10504 PixelFormat::PlanarRgb,
10505 DType::F16,
10506 Some(TensorMemory::Mem),
10507 edgefirst_tensor::CpuAccess::ReadWrite,
10508 )
10509 .unwrap();
10510 cpu_proc
10511 .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
10512 .expect("forced-CPU Rgba→PlanarRgb F16 must not error");
10513 dst
10514 };
10515
10516 let gl_map = gl_result.as_f16().unwrap().map().unwrap();
10518 let cpu_map = cpu_result.as_f16().unwrap().map().unwrap();
10519 let gl_halfs = gl_map.as_slice();
10520 let cpu_halfs = cpu_map.as_slice();
10521
10522 assert_eq!(
10523 gl_halfs.len(),
10524 cpu_halfs.len(),
10525 "GL and CPU output sizes differ"
10526 );
10527
10528 let plane = W * H;
10529 let channel_names = ["R", "G", "B"];
10530 for (idx, (gl_h, cpu_h)) in gl_halfs.iter().zip(cpu_halfs.iter()).enumerate() {
10531 let gl_v = gl_h.to_f32();
10532 let cpu_v = cpu_h.to_f32();
10533 let err = (gl_v - cpu_v).abs();
10534 let ch = channel_names[idx / plane];
10535 let pixel = idx % plane;
10536 assert!(
10537 err <= TOL,
10538 "GL vs CPU mismatch at {ch}[{pixel}]: GL={gl_v}, CPU={cpu_v}, err={err} > tol={TOL}"
10539 );
10540 }
10541 }
10542
10543 #[test]
10550 #[cfg(all(target_os = "linux", feature = "opengl"))]
10551 fn supported_render_dtypes_linux_smoke() {
10552 let proc = match ImageProcessor::new() {
10553 Ok(p) => p,
10554 Err(e) => {
10555 eprintln!("SKIPPED: supported_render_dtypes_linux_smoke — ImageProcessor::new() failed: {e}");
10556 return;
10557 }
10558 };
10559 if proc.opengl.is_none() {
10560 eprintln!("SKIPPED: supported_render_dtypes_linux_smoke — no GL backend on this host");
10561 return;
10562 }
10563 let support = proc.supported_render_dtypes();
10565 eprintln!(
10566 "supported_render_dtypes_linux_smoke: f16={} f32={}",
10567 support.f16, support.f32
10568 );
10569 }
10571
10572 #[test]
10581 fn convert_f16_pbo_non_4_aligned_width_falls_back() {
10582 const W: usize = 18; const H: usize = 16;
10584
10585 let src = TensorDyn::image(
10587 W,
10588 H,
10589 PixelFormat::Rgba,
10590 DType::U8,
10591 Some(TensorMemory::Mem),
10592 edgefirst_tensor::CpuAccess::ReadWrite,
10593 )
10594 .unwrap();
10595 {
10596 let mut map = src.as_u8().unwrap().map().unwrap();
10597 let data = map.as_mut_slice();
10598 for chunk in data.chunks_exact_mut(4) {
10599 chunk[0] = 128;
10600 chunk[1] = 64;
10601 chunk[2] = 200;
10602 chunk[3] = 255;
10603 }
10604 }
10605
10606 let mut dst = TensorDyn::image(
10609 W,
10610 H,
10611 PixelFormat::PlanarRgb,
10612 DType::F16,
10613 Some(TensorMemory::Mem),
10614 edgefirst_tensor::CpuAccess::ReadWrite,
10615 )
10616 .unwrap();
10617
10618 let mut proc = ImageProcessor::new().unwrap();
10621 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
10622 assert!(
10623 result.is_ok(),
10624 "auto-chain PlanarRgb F16 W%4!=0 must not error (CPU fallback): {:?}",
10625 result.err()
10626 );
10627
10628 let map = dst.as_f16().unwrap().map().unwrap();
10630 let halfs = map.as_slice();
10631 assert_eq!(halfs.len(), W * H * 3, "unexpected element count");
10632 for (i, h) in halfs.iter().enumerate() {
10633 let v = h.to_f32();
10634 assert!(
10635 v.is_finite() && (0.0..=1.0).contains(&v),
10636 "output[{i}]={v} is not finite or not in [0,1]"
10637 );
10638 }
10639 }
10640
10641 #[test]
10651 #[allow(clippy::needless_update)]
10654 fn convert_nv12_to_rgb_f32_cpu() {
10655 const W: usize = 16;
10656 const H: usize = 16; let src = TensorDyn::image(
10660 W,
10661 H,
10662 PixelFormat::Nv12,
10663 DType::U8,
10664 Some(TensorMemory::Mem),
10665 edgefirst_tensor::CpuAccess::ReadWrite,
10666 )
10667 .unwrap();
10668 {
10669 let mut map = src.as_u8().unwrap().map().unwrap();
10670 map.as_mut_slice().fill(128); }
10672
10673 let mut dst = TensorDyn::image(
10674 W,
10675 H,
10676 PixelFormat::Rgb,
10677 DType::F32,
10678 Some(TensorMemory::Mem),
10679 edgefirst_tensor::CpuAccess::ReadWrite,
10680 )
10681 .unwrap();
10682
10683 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
10684 backend: ComputeBackend::Cpu,
10685 ..Default::default()
10686 })
10687 .unwrap();
10688
10689 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
10690 assert!(
10691 result.is_ok(),
10692 "forced-CPU NV12→Rgb F32 must not error: {:?}",
10693 result.err()
10694 );
10695
10696 let map = dst.as_f32().unwrap().map().unwrap();
10697 let floats = map.as_slice();
10698 assert_eq!(floats.len(), W * H * 3, "unexpected element count");
10699 for (i, &v) in floats.iter().enumerate() {
10700 assert!(
10701 v.is_finite() && (0.0..=1.0).contains(&v),
10702 "output[{i}]={v} is not finite or not in [0,1]"
10703 );
10704 }
10705 let non_zero = floats.iter().any(|&v| v > 0.01);
10707 assert!(non_zero, "all-zero output from NV12→Rgb F32 CPU path");
10708 }
10709
10710 #[test]
10714 #[allow(clippy::needless_update)]
10717 fn convert_nv12_to_planar_rgb_f16_cpu() {
10718 const W: usize = 16;
10719 const H: usize = 16;
10720
10721 let src = TensorDyn::image(
10722 W,
10723 H,
10724 PixelFormat::Nv12,
10725 DType::U8,
10726 Some(TensorMemory::Mem),
10727 edgefirst_tensor::CpuAccess::ReadWrite,
10728 )
10729 .unwrap();
10730 {
10731 let mut map = src.as_u8().unwrap().map().unwrap();
10732 map.as_mut_slice().fill(128);
10733 }
10734
10735 let mut dst = TensorDyn::image(
10736 W,
10737 H,
10738 PixelFormat::PlanarRgb,
10739 DType::F16,
10740 Some(TensorMemory::Mem),
10741 edgefirst_tensor::CpuAccess::ReadWrite,
10742 )
10743 .unwrap();
10744
10745 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
10746 backend: ComputeBackend::Cpu,
10747 ..Default::default()
10748 })
10749 .unwrap();
10750
10751 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
10752 assert!(
10753 result.is_ok(),
10754 "forced-CPU NV12→PlanarRgb F16 must not error: {:?}",
10755 result.err()
10756 );
10757
10758 let map = dst.as_f16().unwrap().map().unwrap();
10759 let halfs = map.as_slice();
10760 assert_eq!(halfs.len(), W * H * 3, "unexpected element count");
10761 for (i, h) in halfs.iter().enumerate() {
10762 let v = h.to_f32();
10763 assert!(
10764 v.is_finite() && (0.0..=1.0).contains(&v),
10765 "output[{i}]={v} is not finite or not in [0,1]"
10766 );
10767 }
10768 let non_zero = halfs.iter().any(|h| h.to_f32() > 0.01);
10769 assert!(non_zero, "all-zero output from NV12→PlanarRgb F16 CPU path");
10770 }
10771
10772 #[test]
10781 fn create_image_desc_negotiates_and_counts_fallbacks() {
10782 use edgefirst_tensor::{Compression, CpuAccess, ImageDesc};
10783 let proc = ImageProcessor::new().unwrap();
10784
10785 let desc =
10786 ImageDesc::new(64, 64, PixelFormat::Rgba, DType::U8).with_access(CpuAccess::ReadWrite);
10787 let plain = proc.create_image_desc(&desc).unwrap();
10788 let classic = proc
10789 .create_image(
10790 64,
10791 64,
10792 PixelFormat::Rgba,
10793 DType::U8,
10794 None,
10795 CpuAccess::ReadWrite,
10796 )
10797 .unwrap();
10798 assert_eq!(plain.memory(), classic.memory(), "same negotiation path");
10799 assert_eq!(plain.compression(), None);
10800
10801 #[cfg(not(target_os = "android"))]
10802 {
10803 let before = proc.compression_fallback_count();
10804 let desc = ImageDesc::new(64, 64, PixelFormat::Rgba, DType::U8)
10805 .with_compression(Compression::Any);
10806 let t = proc.create_image_desc(&desc).unwrap();
10807 assert_eq!(t.compression(), None, "no vendor tile scheme off-Android");
10808 assert!(
10809 proc.compression_fallback_count() > before,
10810 "Any resolving linear must count"
10811 );
10812 }
10813 }
10814
10815 #[test]
10818 #[cfg(target_os = "linux")]
10819 fn create_image_f32_dma_rejected() {
10820 let proc = ImageProcessor::new().unwrap();
10821 let result = proc.create_image(
10822 64,
10823 64,
10824 PixelFormat::Rgb,
10825 DType::F32,
10826 Some(TensorMemory::Dma),
10827 edgefirst_tensor::CpuAccess::ReadWrite,
10828 );
10829 assert!(
10830 result.is_err(),
10831 "create_image(F32, Dma) must fail — no DRM fourcc for f32"
10832 );
10833 }
10834
10835 #[test]
10844 #[cfg(target_os = "linux")]
10845 fn import_image_carries_colorimetry() {
10846 use edgefirst_tensor::{ColorEncoding, ColorRange, Colorimetry, TensorMemory};
10847
10848 let expected = Colorimetry::default()
10849 .with_encoding(ColorEncoding::Bt709)
10850 .with_range(ColorRange::Limited);
10851
10852 if !is_dma_available() {
10853 let mut t = TensorDyn::image(
10856 8,
10857 8,
10858 PixelFormat::Rgba,
10859 DType::U8,
10860 Some(TensorMemory::Mem),
10861 edgefirst_tensor::CpuAccess::ReadWrite,
10862 )
10863 .expect("alloc");
10864 assert_eq!(t.colorimetry(), None, "colorimetry must start as None");
10865 t.set_colorimetry(Some(expected));
10866 assert_eq!(
10867 t.colorimetry(),
10868 Some(expected),
10869 "set_colorimetry must round-trip"
10870 );
10871 eprintln!("SKIPPED import_image_carries_colorimetry (DMA unavailable); storage contract verified via TensorDyn");
10872 return;
10873 }
10874
10875 use edgefirst_tensor::{PlaneDescriptor, Tensor};
10878
10879 let rgba_bytes = 64 * 64 * 4; let dma_tensor =
10881 Tensor::<u8>::new(&[rgba_bytes], Some(TensorMemory::Dma), Some("import_test"))
10882 .expect("dma alloc");
10883 let pd =
10884 PlaneDescriptor::new(dma_tensor.dmabuf().expect("dma fd")).expect("PlaneDescriptor");
10885
10886 let proc = ImageProcessor::new().expect("ImageProcessor");
10887 let result = proc.import_image(
10888 pd,
10889 None,
10890 64,
10891 64,
10892 PixelFormat::Rgba,
10893 DType::U8,
10894 Some(expected),
10895 );
10896 let tensor = result.expect("import_image must succeed on DMA fd");
10897 assert_eq!(
10898 tensor.colorimetry(),
10899 Some(expected),
10900 "import_image must store the supplied colorimetry on the returned TensorDyn"
10901 );
10902 }
10903}