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;
380mod tiling;
381pub use tiling::{tile_grid, TilePlacement, TileSpec, TilingConfig};
382
383#[derive(Debug, Clone, Copy, PartialEq, Eq)]
387pub enum Rotation {
388 None = 0,
389 Clockwise90 = 1,
390 Rotate180 = 2,
391 CounterClockwise90 = 3,
392}
393impl Rotation {
394 pub fn from_degrees_clockwise(angle: usize) -> Rotation {
407 match angle.rem_euclid(360) {
408 0 => Rotation::None,
409 90 => Rotation::Clockwise90,
410 180 => Rotation::Rotate180,
411 270 => Rotation::CounterClockwise90,
412 _ => panic!("rotation angle is not a multiple of 90"),
413 }
414 }
415}
416
417#[derive(Debug, Clone, Copy, PartialEq, Eq)]
418pub enum Flip {
419 None = 0,
420 Vertical = 1,
421 Horizontal = 2,
422}
423
424#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
426pub enum ColorMode {
427 #[default]
432 Class,
433 Instance,
438 Track,
441}
442
443impl ColorMode {
444 #[inline]
446 pub fn index(self, idx: usize, label: usize) -> usize {
447 match self {
448 ColorMode::Class => label,
449 ColorMode::Instance | ColorMode::Track => idx,
450 }
451 }
452}
453
454#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
475pub enum MaskResolution {
476 #[default]
478 Proto,
479 Scaled {
483 width: u32,
485 height: u32,
487 },
488}
489
490#[derive(Debug, Clone, Copy)]
506pub struct MaskOverlay<'a> {
507 pub background: Option<&'a TensorDyn>,
511 pub opacity: f32,
512 pub letterbox: Option<[f32; 4]>,
522 pub color_mode: ColorMode,
523}
524
525impl Default for MaskOverlay<'_> {
526 fn default() -> Self {
527 Self {
528 background: None,
529 opacity: 1.0,
530 letterbox: None,
531 color_mode: ColorMode::Class,
532 }
533 }
534}
535
536impl<'a> MaskOverlay<'a> {
537 pub fn new() -> Self {
538 Self::default()
539 }
540
541 pub fn with_background(mut self, bg: &'a TensorDyn) -> Self {
549 self.background = Some(bg);
550 self
551 }
552
553 pub fn with_opacity(mut self, opacity: f32) -> Self {
554 self.opacity = opacity.clamp(0.0, 1.0);
555 self
556 }
557
558 pub fn with_color_mode(mut self, mode: ColorMode) -> Self {
559 self.color_mode = mode;
560 self
561 }
562
563 pub fn with_letterbox_crop(
573 mut self,
574 crop: &Crop,
575 src_w: usize,
576 src_h: usize,
577 model_w: usize,
578 model_h: usize,
579 ) -> Self {
580 if let Ok(resolved) = crop.resolve(src_w, src_h, model_w, model_h) {
583 if let Some(r) = resolved.dst_rect {
584 self.letterbox = Some([
585 r.left as f32 / model_w as f32,
586 r.top as f32 / model_h as f32,
587 (r.left + r.width) as f32 / model_w as f32,
588 (r.top + r.height) as f32 / model_h as f32,
589 ]);
590 }
591 }
592 self
593 }
594}
595
596#[inline]
609fn unletter_bbox(bbox: DetectBox, lb: [f32; 4]) -> DetectBox {
610 DetectBox {
611 bbox: edgefirst_decoder::tiling::unletter_norm(bbox.bbox, lb),
612 ..bbox
613 }
614}
615
616#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
618pub enum Fit {
619 #[default]
621 Stretch,
622 Letterbox { pad: [u8; 4] },
626}
627
628#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
633pub struct Crop {
634 pub source: Option<Region>,
636 pub fit: Fit,
638}
639
640impl Crop {
641 pub fn new() -> Self {
643 Self::default()
644 }
645
646 pub fn no_crop() -> Self {
648 Self::default()
649 }
650
651 pub fn letterbox(pad: [u8; 4]) -> Self {
654 Self {
655 source: None,
656 fit: Fit::Letterbox { pad },
657 }
658 }
659
660 pub fn with_source(mut self, source: Option<Region>) -> Self {
662 self.source = source;
663 self
664 }
665
666 pub fn with_fit(mut self, fit: Fit) -> Self {
668 self.fit = fit;
669 self
670 }
671
672 pub(crate) fn resolve(
678 &self,
679 src_w: usize,
680 src_h: usize,
681 dst_w: usize,
682 dst_h: usize,
683 ) -> Result<ResolvedCrop, Error> {
684 let src_rect = self.source.map(region_to_rect);
685 let (sw, sh) = match self.source {
688 Some(r) => (r.width, r.height),
689 None => (src_w, src_h),
690 };
691 let resolved = match self.fit {
692 Fit::Stretch => ResolvedCrop {
693 src_rect,
694 dst_rect: None,
695 dst_color: None,
696 },
697 Fit::Letterbox { pad } => ResolvedCrop {
698 src_rect,
699 dst_rect: Some(letterbox_rect(sw, sh, dst_w, dst_h)),
700 dst_color: Some(pad),
701 },
702 };
703 resolved.check_crop_dims(src_w, src_h, dst_w, dst_h)?;
704 Ok(resolved)
705 }
706
707 pub fn check_crop_dyn(
709 &self,
710 src: &edgefirst_tensor::TensorDyn,
711 dst: &edgefirst_tensor::TensorDyn,
712 ) -> Result<(), Error> {
713 self.resolve(
714 src.width().unwrap_or(0),
715 src.height().unwrap_or(0),
716 dst.width().unwrap_or(0),
717 dst.height().unwrap_or(0),
718 )
719 .map(|_| ())
720 }
721}
722
723#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
727pub(crate) struct ResolvedCrop {
728 pub(crate) src_rect: Option<Rect>,
729 pub(crate) dst_rect: Option<Rect>,
730 pub(crate) dst_color: Option<[u8; 4]>,
731}
732
733impl ResolvedCrop {
734 #[allow(dead_code)] pub(crate) fn no_crop() -> Self {
737 Self::default()
738 }
739
740 pub(crate) fn check_crop_dims(
742 &self,
743 src_w: usize,
744 src_h: usize,
745 dst_w: usize,
746 dst_h: usize,
747 ) -> Result<(), Error> {
748 let src_ok = self
749 .src_rect
750 .is_none_or(|r| r.left + r.width <= src_w && r.top + r.height <= src_h);
751 let dst_ok = self
752 .dst_rect
753 .is_none_or(|r| r.left + r.width <= dst_w && r.top + r.height <= dst_h);
754 match (src_ok, dst_ok) {
755 (true, true) => Ok(()),
756 (true, false) => Err(Error::CropInvalid(format!(
757 "Dest crop invalid: {:?}",
758 self.dst_rect
759 ))),
760 (false, true) => Err(Error::CropInvalid(format!(
761 "Src crop invalid: {:?}",
762 self.src_rect
763 ))),
764 (false, false) => Err(Error::CropInvalid(format!(
765 "Dest and Src crop invalid: {:?} {:?}",
766 self.dst_rect, self.src_rect
767 ))),
768 }
769 }
770}
771
772fn region_to_rect(r: Region) -> Rect {
774 Rect {
775 left: r.x,
776 top: r.y,
777 width: r.width,
778 height: r.height,
779 }
780}
781
782fn letterbox_rect(sw: usize, sh: usize, dw: usize, dh: usize) -> Rect {
786 if sw == 0 || sh == 0 {
787 return Rect::new(0, 0, dw, dh);
788 }
789 let src_aspect = sw as f64 / sh as f64;
790 let dst_aspect = dw as f64 / dh as f64;
791 let (new_w, new_h) = if src_aspect > dst_aspect {
792 (dw, ((dw as f64 / src_aspect).round() as usize).max(1))
793 } else {
794 (((dh as f64 * src_aspect).round() as usize).max(1), dh)
795 };
796 let left = dw.saturating_sub(new_w) / 2;
797 let top = dh.saturating_sub(new_h) / 2;
798 Rect::new(left, top, new_w, new_h)
799}
800
801#[derive(Debug, Clone, Copy, PartialEq, Eq)]
806pub(crate) struct Rect {
807 pub left: usize,
808 pub top: usize,
809 pub width: usize,
810 pub height: usize,
811}
812
813impl Rect {
814 pub fn new(left: usize, top: usize, width: usize, height: usize) -> Self {
816 Self {
817 left,
818 top,
819 width,
820 height,
821 }
822 }
823}
824
825#[enum_dispatch(ImageProcessor)]
826pub trait ImageProcessorTrait {
827 fn convert(
843 &mut self,
844 src: &TensorDyn,
845 dst: &mut TensorDyn,
846 rotation: Rotation,
847 flip: Flip,
848 crop: Crop,
849 ) -> Result<()>;
850
851 fn draw_decoded_masks(
908 &mut self,
909 dst: &mut TensorDyn,
910 detect: &[DetectBox],
911 segmentation: &[Segmentation],
912 overlay: MaskOverlay<'_>,
913 ) -> Result<()>;
914
915 fn draw_proto_masks(
935 &mut self,
936 dst: &mut TensorDyn,
937 detect: &[DetectBox],
938 proto_data: &ProtoData,
939 overlay: MaskOverlay<'_>,
940 ) -> Result<()>;
941
942 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()>;
945
946 fn convert_deferred(
963 &mut self,
964 src: &TensorDyn,
965 dst: &mut TensorDyn,
966 rotation: Rotation,
967 flip: Flip,
968 crop: Crop,
969 ) -> Result<()> {
970 self.convert(src, dst, rotation, flip, crop)
971 }
972
973 fn flush(&mut self) -> Result<()> {
980 Ok(())
981 }
982}
983
984#[derive(Debug, Clone, Default)]
990pub struct ImageProcessorConfig {
991 #[cfg(all(
1001 any(
1002 target_os = "linux",
1003 target_os = "macos",
1004 target_os = "ios",
1005 target_os = "android"
1006 ),
1007 feature = "opengl"
1008 ))]
1009 pub egl_display: Option<EglDisplayKind>,
1010
1011 pub backend: ComputeBackend,
1023
1024 pub colorimetry: ColorimetryMode,
1029}
1030
1031#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1046pub enum ColorimetryMode {
1047 #[default]
1051 Fast,
1052 Exact,
1055}
1056
1057#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1064pub enum ComputeBackend {
1065 #[default]
1067 Auto,
1068 Cpu,
1070 G2d,
1072 OpenGl,
1074}
1075
1076#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1082pub(crate) enum ForcedBackend {
1083 Cpu,
1084 G2d,
1085 OpenGl,
1086}
1087
1088#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1109pub struct RenderDtypeSupport {
1110 pub f32: bool,
1115 pub f16: bool,
1121}
1122
1123#[cfg(all(target_os = "linux", feature = "opengl"))]
1137pub(crate) fn float_pbo_eligible(dtype: DType, support: RenderDtypeSupport) -> bool {
1138 match dtype {
1139 DType::F16 => support.f16,
1140 DType::F32 => support.f32,
1141 _ => false,
1142 }
1143}
1144
1145#[derive(Debug)]
1148pub struct ImageProcessor {
1149 pub cpu: Option<CPUProcessor>,
1152
1153 #[cfg(target_os = "linux")]
1154 pub g2d: Option<G2DProcessor>,
1158 #[cfg(target_os = "linux")]
1159 #[cfg(feature = "opengl")]
1160 pub opengl: Option<GLProcessorThreaded>,
1164 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
1165 #[cfg(feature = "opengl")]
1166 pub opengl: Option<GLProcessorThreaded>,
1173
1174 pub(crate) forced_backend: Option<ForcedBackend>,
1176
1177 pub(crate) convert_fallbacks: std::sync::atomic::AtomicU64,
1183}
1184
1185unsafe impl Send for ImageProcessor {}
1186unsafe impl Sync for ImageProcessor {}
1187
1188impl ImageProcessor {
1189 pub fn new() -> Result<Self> {
1215 Self::with_config(ImageProcessorConfig::default())
1216 }
1217
1218 pub fn convert_fallback_count(&self) -> u64 {
1225 self.convert_fallbacks
1226 .load(std::sync::atomic::Ordering::Relaxed)
1227 }
1228
1229 pub fn compression_fallback_count(&self) -> u64 {
1238 edgefirst_tensor::compression_fallback_count()
1239 }
1240
1241 #[cfg(unix)]
1255 pub fn convert_with_fence(
1256 &mut self,
1257 src: &TensorDyn,
1258 dst: &mut TensorDyn,
1259 rotation: Rotation,
1260 flip: Flip,
1261 crop: Crop,
1262 ) -> Result<Option<std::os::fd::OwnedFd>> {
1263 #[cfg(any(
1264 target_os = "linux",
1265 target_os = "macos",
1266 target_os = "ios",
1267 target_os = "android"
1268 ))]
1269 #[cfg(feature = "opengl")]
1270 {
1271 let gl_forced = matches!(self.forced_backend, Some(ForcedBackend::OpenGl));
1272 if self.forced_backend.is_none() || gl_forced {
1273 if let Some(opengl) = self.opengl.as_mut() {
1274 match opengl.convert_with_fence(src, dst, rotation, flip, crop) {
1275 Ok(fd) => return Ok(fd),
1276 Err(e) if gl_forced => return Err(e),
1277 Err(e) => {
1278 log::debug!(
1282 "convert_with_fence: opengl declined, \
1283 falling back to the blocking chain: {e}"
1284 );
1285 }
1286 }
1287 } else if gl_forced {
1288 return Err(Error::ForcedBackendUnavailable("opengl".into()));
1289 }
1290 }
1291 }
1292 self.convert(src, dst, rotation, flip, crop)?;
1295 Ok(None)
1296 }
1297
1298 pub fn supported_render_dtypes(&self) -> RenderDtypeSupport {
1311 #[cfg(all(
1312 any(target_os = "macos", target_os = "ios", target_os = "android"),
1313 feature = "opengl"
1314 ))]
1315 if let Some(gl) = self.opengl.as_ref() {
1316 return gl.supported_render_dtypes();
1317 }
1318 #[cfg(all(target_os = "linux", feature = "opengl"))]
1319 if let Some(gl) = self.opengl.as_ref() {
1320 return gl.supported_render_dtypes();
1321 }
1322 RenderDtypeSupport {
1323 f32: false,
1324 f16: false,
1325 }
1326 }
1327
1328 #[allow(unused_variables)]
1337 pub fn with_config(config: ImageProcessorConfig) -> Result<Self> {
1338 match config.backend {
1342 ComputeBackend::Cpu => {
1343 log::info!("ComputeBackend::Cpu — CPU only");
1344 return Ok(Self {
1345 cpu: Some(CPUProcessor::new()),
1346 #[cfg(target_os = "linux")]
1347 g2d: None,
1348 #[cfg(target_os = "linux")]
1349 #[cfg(feature = "opengl")]
1350 opengl: None,
1351 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
1352 #[cfg(feature = "opengl")]
1353 opengl: None,
1354 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1355 forced_backend: None,
1356 });
1357 }
1358 ComputeBackend::G2d => {
1359 log::info!("ComputeBackend::G2d — G2D + CPU fallback");
1360 #[cfg(target_os = "linux")]
1361 {
1362 let g2d = match G2DProcessor::new() {
1363 Ok(g) => Some(g),
1364 Err(e) => {
1365 log::warn!("G2D requested but failed to initialize: {e:?}");
1366 None
1367 }
1368 };
1369 return Ok(Self {
1370 cpu: Some(CPUProcessor::new()),
1371 g2d,
1372 #[cfg(feature = "opengl")]
1373 opengl: None,
1374 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1375 forced_backend: None,
1376 });
1377 }
1378 #[cfg(not(target_os = "linux"))]
1379 {
1380 log::warn!("G2D requested but not available on this platform, using CPU");
1381 return Ok(Self {
1382 cpu: Some(CPUProcessor::new()),
1383 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
1384 #[cfg(feature = "opengl")]
1385 opengl: None,
1386 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1387 forced_backend: None,
1388 });
1389 }
1390 }
1391 ComputeBackend::OpenGl => {
1392 log::info!("ComputeBackend::OpenGl — OpenGL + CPU fallback");
1393 #[cfg(target_os = "linux")]
1394 {
1395 #[cfg(feature = "opengl")]
1396 let opengl = match GLProcessorThreaded::new(config.egl_display) {
1397 Ok(gl) => Some(gl),
1398 Err(e) => {
1399 log::warn!("OpenGL requested but failed to initialize: {e:?}");
1400 None
1401 }
1402 };
1403 return Ok(Self {
1404 cpu: Some(CPUProcessor::new()),
1405 g2d: None,
1406 #[cfg(feature = "opengl")]
1407 opengl,
1408 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1409 forced_backend: None,
1410 }
1411 .apply_colorimetry_mode(config.colorimetry));
1412 }
1413 #[cfg(any(target_os = "macos", target_os = "ios"))]
1414 {
1415 #[cfg(feature = "opengl")]
1416 let opengl = match GLProcessorThreaded::new(config.egl_display) {
1417 Ok(gl) => Some(gl),
1418 Err(e) => {
1419 log::warn!(
1420 "OpenGL requested on macOS but ANGLE init failed: {e:?} \
1421 (install ANGLE via `brew install startergo/angle/angle` \
1422 and re-sign the dylibs — see README.md § macOS GPU \
1423 Acceleration). Falling back to CPU."
1424 );
1425 None
1426 }
1427 };
1428 return Ok(Self {
1429 cpu: Some(CPUProcessor::new()),
1430 #[cfg(feature = "opengl")]
1431 opengl,
1432 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1433 forced_backend: None,
1434 }
1435 .apply_colorimetry_mode(config.colorimetry));
1436 }
1437 #[cfg(target_os = "android")]
1438 {
1439 #[cfg(feature = "opengl")]
1440 let opengl = match GLProcessorThreaded::new(config.egl_display) {
1441 Ok(gl) => Some(gl),
1442 Err(e) => {
1443 log::warn!(
1444 "OpenGL requested but native EGL init failed: {e:?}. \
1445 Falling back to CPU."
1446 );
1447 None
1448 }
1449 };
1450 return Ok(Self {
1451 cpu: Some(CPUProcessor::new()),
1452 #[cfg(feature = "opengl")]
1453 opengl,
1454 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1455 forced_backend: None,
1456 }
1457 .apply_colorimetry_mode(config.colorimetry));
1458 }
1459 #[cfg(not(any(
1460 target_os = "linux",
1461 target_os = "macos",
1462 target_os = "ios",
1463 target_os = "android"
1464 )))]
1465 {
1466 log::warn!("OpenGL requested but not available on this platform, using CPU");
1467 return Ok(Self {
1468 cpu: Some(CPUProcessor::new()),
1469 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1470 forced_backend: None,
1471 });
1472 }
1473 }
1474 ComputeBackend::Auto => { }
1475 }
1476
1477 if let Ok(val) = std::env::var("EDGEFIRST_FORCE_BACKEND") {
1482 let val_lower = val.to_lowercase();
1483 let forced = match val_lower.as_str() {
1484 "cpu" => ForcedBackend::Cpu,
1485 "g2d" => ForcedBackend::G2d,
1486 "opengl" => ForcedBackend::OpenGl,
1487 other => {
1488 return Err(Error::ForcedBackendUnavailable(format!(
1489 "unknown EDGEFIRST_FORCE_BACKEND value: {other:?} (expected cpu, g2d, or opengl)"
1490 )));
1491 }
1492 };
1493
1494 log::info!("EDGEFIRST_FORCE_BACKEND={val} — only initializing {val_lower} backend");
1495
1496 return match forced {
1497 ForcedBackend::Cpu => Ok(Self {
1498 cpu: Some(CPUProcessor::new()),
1499 #[cfg(target_os = "linux")]
1500 g2d: None,
1501 #[cfg(target_os = "linux")]
1502 #[cfg(feature = "opengl")]
1503 opengl: None,
1504 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
1505 #[cfg(feature = "opengl")]
1506 opengl: None,
1507 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1508 forced_backend: Some(ForcedBackend::Cpu),
1509 }),
1510 ForcedBackend::G2d => {
1511 #[cfg(target_os = "linux")]
1512 {
1513 let g2d = G2DProcessor::new().map_err(|e| {
1514 Error::ForcedBackendUnavailable(format!(
1515 "g2d forced but failed to initialize: {e:?}"
1516 ))
1517 })?;
1518 Ok(Self {
1519 cpu: None,
1520 g2d: Some(g2d),
1521 #[cfg(feature = "opengl")]
1522 opengl: None,
1523 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1524 forced_backend: Some(ForcedBackend::G2d),
1525 })
1526 }
1527 #[cfg(not(target_os = "linux"))]
1528 {
1529 Err(Error::ForcedBackendUnavailable(
1530 "g2d backend is only available on Linux".into(),
1531 ))
1532 }
1533 }
1534 ForcedBackend::OpenGl => {
1535 #[cfg(target_os = "linux")]
1536 #[cfg(feature = "opengl")]
1537 {
1538 let opengl = GLProcessorThreaded::new(config.egl_display).map_err(|e| {
1539 Error::ForcedBackendUnavailable(format!(
1540 "opengl forced but failed to initialize: {e:?}"
1541 ))
1542 })?;
1543 Ok(Self {
1544 cpu: None,
1545 g2d: None,
1546 opengl: Some(opengl),
1547 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1548 forced_backend: Some(ForcedBackend::OpenGl),
1549 }
1550 .apply_colorimetry_mode(config.colorimetry))
1551 }
1552 #[cfg(any(target_os = "macos", target_os = "ios"))]
1553 #[cfg(feature = "opengl")]
1554 {
1555 let opengl = GLProcessorThreaded::new(config.egl_display).map_err(|e| {
1556 Error::ForcedBackendUnavailable(format!(
1557 "opengl forced on macOS but ANGLE init failed: {e:?}"
1558 ))
1559 })?;
1560 Ok(Self {
1561 cpu: None,
1562 opengl: Some(opengl),
1563 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1564 forced_backend: Some(ForcedBackend::OpenGl),
1565 }
1566 .apply_colorimetry_mode(config.colorimetry))
1567 }
1568 #[cfg(target_os = "android")]
1569 #[cfg(feature = "opengl")]
1570 {
1571 let opengl = GLProcessorThreaded::new(config.egl_display).map_err(|e| {
1572 Error::ForcedBackendUnavailable(format!(
1573 "opengl forced but native EGL init failed: {e:?}"
1574 ))
1575 })?;
1576 Ok(Self {
1577 cpu: None,
1578 opengl: Some(opengl),
1579 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1580 forced_backend: Some(ForcedBackend::OpenGl),
1581 }
1582 .apply_colorimetry_mode(config.colorimetry))
1583 }
1584 #[cfg(not(all(
1585 any(
1586 target_os = "linux",
1587 target_os = "macos",
1588 target_os = "ios",
1589 target_os = "android"
1590 ),
1591 feature = "opengl"
1592 )))]
1593 {
1594 Err(Error::ForcedBackendUnavailable(
1595 "opengl backend requires Linux or macOS with the 'opengl' feature \
1596 enabled"
1597 .into(),
1598 ))
1599 }
1600 }
1601 };
1602 }
1603
1604 #[cfg(target_os = "linux")]
1606 let g2d = if std::env::var("EDGEFIRST_DISABLE_G2D")
1607 .map(|x| x != "0" && x.to_lowercase() != "false")
1608 .unwrap_or(false)
1609 {
1610 log::debug!("EDGEFIRST_DISABLE_G2D is set");
1611 None
1612 } else {
1613 match G2DProcessor::new() {
1614 Ok(g2d_converter) => Some(g2d_converter),
1615 Err(err) => {
1616 log::warn!("Failed to initialize G2D converter: {err:?}");
1617 None
1618 }
1619 }
1620 };
1621
1622 #[cfg(target_os = "linux")]
1623 #[cfg(feature = "opengl")]
1624 let opengl = if std::env::var("EDGEFIRST_DISABLE_GL")
1625 .map(|x| x != "0" && x.to_lowercase() != "false")
1626 .unwrap_or(false)
1627 {
1628 log::debug!("EDGEFIRST_DISABLE_GL is set");
1629 None
1630 } else {
1631 match GLProcessorThreaded::new(config.egl_display) {
1632 Ok(gl_converter) => Some(gl_converter),
1633 Err(err) => {
1634 log::warn!("Failed to initialize GL converter: {err:?}");
1635 None
1636 }
1637 }
1638 };
1639
1640 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
1641 #[cfg(feature = "opengl")]
1642 let opengl = if std::env::var("EDGEFIRST_DISABLE_GL")
1643 .map(|x| x != "0" && x.to_lowercase() != "false")
1644 .unwrap_or(false)
1645 {
1646 log::debug!("EDGEFIRST_DISABLE_GL is set");
1647 None
1648 } else {
1649 match GLProcessorThreaded::new(config.egl_display) {
1650 Ok(gl_converter) => Some(gl_converter),
1651 Err(err) => {
1652 log::debug!(
1653 "GL backend unavailable: {err:?} \
1654 (CPU fallback will be used)"
1655 );
1656 None
1657 }
1658 }
1659 };
1660
1661 let cpu = if std::env::var("EDGEFIRST_DISABLE_CPU")
1662 .map(|x| x != "0" && x.to_lowercase() != "false")
1663 .unwrap_or(false)
1664 {
1665 log::debug!("EDGEFIRST_DISABLE_CPU is set");
1666 None
1667 } else {
1668 Some(CPUProcessor::new())
1669 };
1670 Ok(Self {
1671 cpu,
1672 #[cfg(target_os = "linux")]
1673 g2d,
1674 #[cfg(target_os = "linux")]
1675 #[cfg(feature = "opengl")]
1676 opengl,
1677 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
1678 #[cfg(feature = "opengl")]
1679 opengl,
1680 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1681 forced_backend: None,
1682 }
1683 .apply_colorimetry_mode(config.colorimetry))
1684 }
1685
1686 fn apply_colorimetry_mode(self, _mode: ColorimetryMode) -> Self {
1690 #[cfg(all(
1691 any(
1692 target_os = "linux",
1693 target_os = "macos",
1694 target_os = "ios",
1695 target_os = "android"
1696 ),
1697 feature = "opengl"
1698 ))]
1699 {
1700 let mut me = self;
1701 if let Err(e) = me.set_colorimetry_mode(_mode) {
1702 log::warn!("Failed to apply ColorimetryMode::{_mode:?}: {e:?}");
1703 }
1704 me
1705 }
1706 #[cfg(not(all(
1707 any(
1708 target_os = "linux",
1709 target_os = "macos",
1710 target_os = "ios",
1711 target_os = "android"
1712 ),
1713 feature = "opengl"
1714 )))]
1715 {
1716 let _ = _mode;
1717 self
1718 }
1719 }
1720
1721 #[cfg(all(
1726 any(
1727 target_os = "linux",
1728 target_os = "macos",
1729 target_os = "ios",
1730 target_os = "android"
1731 ),
1732 feature = "opengl"
1733 ))]
1734 pub fn set_colorimetry_mode(&mut self, mode: ColorimetryMode) -> Result<()> {
1735 if let Some(ref mut gl) = self.opengl {
1736 gl.set_colorimetry_mode(mode)?;
1737 }
1738 Ok(())
1739 }
1740
1741 #[cfg(all(
1744 any(
1745 target_os = "linux",
1746 target_os = "macos",
1747 target_os = "ios",
1748 target_os = "android"
1749 ),
1750 feature = "opengl"
1751 ))]
1752 pub fn set_int8_interpolation_mode(&mut self, mode: Int8InterpolationMode) -> Result<()> {
1753 if let Some(ref mut gl) = self.opengl {
1754 gl.set_int8_interpolation_mode(mode)?;
1755 }
1756 Ok(())
1757 }
1758
1759 pub fn create_image_desc(&self, desc: &edgefirst_tensor::ImageDesc) -> Result<TensorDyn> {
1837 if desc.compression().is_none() {
1838 return self.create_image(
1839 desc.width(),
1840 desc.height(),
1841 desc.format(),
1842 desc.dtype(),
1843 desc.memory(),
1844 desc.access(),
1845 );
1846 }
1847 Ok(TensorDyn::image_desc(desc)?)
1848 }
1849
1850 pub fn create_image(
1851 &self,
1852 width: usize,
1853 height: usize,
1854 format: PixelFormat,
1855 dtype: DType,
1856 memory: Option<TensorMemory>,
1857 access: edgefirst_tensor::CpuAccess,
1858 ) -> Result<TensorDyn> {
1859 #[cfg(target_os = "linux")]
1870 let dma_stride_bytes: Option<usize> = primary_plane_bpp(format, dtype.size())
1871 .and_then(|bpp| width.checked_mul(bpp))
1872 .and_then(align_pitch_bytes_to_gpu_alignment);
1873
1874 #[cfg(target_os = "linux")]
1878 let try_dma = || -> Result<TensorDyn> {
1879 let packed = format.layout() == edgefirst_tensor::PixelLayout::Packed;
1887 match dma_stride_bytes {
1888 Some(stride)
1889 if packed
1890 && primary_plane_bpp(format, dtype.size())
1891 .and_then(|bpp| width.checked_mul(bpp))
1892 .is_some_and(|natural| stride > natural) =>
1893 {
1894 log::debug!(
1895 "create_image: padding row stride for {format:?} {width}x{height} \
1896 from natural pitch to {stride} bytes for GPU alignment"
1897 );
1898 Ok(TensorDyn::image_with_stride(
1899 width,
1900 height,
1901 format,
1902 dtype,
1903 stride,
1904 Some(edgefirst_tensor::TensorMemory::Dma),
1905 access,
1906 )?)
1907 }
1908 _ => Ok(TensorDyn::image(
1909 width,
1910 height,
1911 format,
1912 dtype,
1913 Some(edgefirst_tensor::TensorMemory::Dma),
1914 access,
1915 )?),
1916 }
1917 };
1918
1919 match memory {
1926 #[cfg(target_os = "linux")]
1927 Some(TensorMemory::Dma) => {
1928 if dtype == DType::F32 {
1930 return Err(Error::NotSupported(
1931 "F32 has no 32-bit-float DRM format for DMA-BUF; \
1932 use TensorMemory::Pbo for F32"
1933 .to_string(),
1934 ));
1935 }
1936 return try_dma();
1937 }
1938 Some(mem) => {
1939 return Ok(TensorDyn::image(
1940 width,
1941 height,
1942 format,
1943 dtype,
1944 Some(mem),
1945 access,
1946 )?);
1947 }
1948 None => {}
1949 }
1950
1951 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
1958 #[cfg(feature = "opengl")]
1959 if let Some(gl) = self.opengl.as_ref() {
1960 let _ = gl; match TensorDyn::image(
1962 width,
1963 height,
1964 format,
1965 dtype,
1966 Some(edgefirst_tensor::TensorMemory::Dma),
1967 access,
1968 ) {
1969 Ok(img) => return Ok(img),
1970 Err(e) => {
1971 log::debug!(
1975 "create_image: zero-copy Dma allocation declined \
1976 ({format:?}/{dtype:?} {width}x{height}): {e:?}; using fallback storage"
1977 );
1978 }
1979 }
1980 }
1981
1982 #[cfg(target_os = "linux")]
1985 {
1986 #[cfg(feature = "opengl")]
1987 let gl_uses_pbo = self
1988 .opengl
1989 .as_ref()
1990 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
1991 #[cfg(not(feature = "opengl"))]
1992 let gl_uses_pbo = false;
1993
1994 if !gl_uses_pbo {
1995 if let Ok(img) = try_dma() {
1996 return Ok(img);
1997 }
1998 }
1999 }
2000
2001 #[cfg(target_os = "linux")]
2005 #[cfg(feature = "opengl")]
2006 if dtype.size() == 1 {
2007 if let Some(gl) = &self.opengl {
2008 match gl.create_pbo_image(width, height, format) {
2009 Ok(t) => {
2010 if dtype == DType::I8 {
2011 debug_assert!(
2019 t.chroma().is_none(),
2020 "PBO i8 transmute requires chroma == None"
2021 );
2022 let t_i8: Tensor<i8> = unsafe { std::mem::transmute(t) };
2023 return Ok(TensorDyn::from(t_i8));
2024 }
2025 return Ok(TensorDyn::from(t));
2026 }
2027 Err(e) => log::debug!("PBO image creation failed, falling back to Mem: {e:?}"),
2028 }
2029 }
2030 }
2031
2032 #[cfg(target_os = "linux")]
2035 #[cfg(feature = "opengl")]
2036 if float_pbo_eligible(dtype, self.supported_render_dtypes()) {
2037 if let Some(gl) = &self.opengl {
2038 match gl.create_pbo_image_dtype(width, height, format, dtype) {
2039 Ok(t) => return Ok(t),
2040 Err(e) => {
2041 log::debug!(
2042 "Float PBO image creation failed for {dtype:?}, \
2043 falling back to Mem: {e:?}"
2044 );
2045 }
2046 }
2047 }
2048 }
2049
2050 Ok(TensorDyn::image(
2052 width,
2053 height,
2054 format,
2055 dtype,
2056 Some(edgefirst_tensor::TensorMemory::Mem),
2057 access,
2058 )?)
2059 }
2060
2061 #[allow(clippy::too_many_arguments)]
2115 #[cfg(target_os = "linux")]
2116 pub fn import_image(
2117 &self,
2118 image: edgefirst_tensor::PlaneDescriptor,
2119 chroma: Option<edgefirst_tensor::PlaneDescriptor>,
2120 width: usize,
2121 height: usize,
2122 format: PixelFormat,
2123 dtype: DType,
2124 colorimetry: Option<edgefirst_tensor::Colorimetry>,
2125 ) -> Result<TensorDyn> {
2126 use edgefirst_tensor::{Tensor, TensorMemory};
2127
2128 let image_stride = image.stride();
2130 let image_offset = image.offset();
2131 let chroma_stride = chroma.as_ref().and_then(|c| c.stride());
2132 let chroma_offset = chroma.as_ref().and_then(|c| c.offset());
2133
2134 if let Some(chroma_pd) = chroma {
2135 if dtype != DType::U8 && dtype != DType::I8 {
2140 return Err(Error::NotSupported(format!(
2141 "multiplane import only supports U8/I8, got {dtype:?}"
2142 )));
2143 }
2144 if format.layout() != PixelLayout::SemiPlanar {
2145 return Err(Error::NotSupported(format!(
2146 "import_image with chroma requires a semi-planar format, got {format:?}"
2147 )));
2148 }
2149
2150 let chroma_h = match format {
2151 PixelFormat::Nv12 => {
2152 height.div_ceil(2)
2154 }
2155 PixelFormat::Nv16 => {
2158 return Err(Error::NotSupported(
2159 "multiplane NV16 is not yet supported; use contiguous NV16 instead".into(),
2160 ))
2161 }
2162 _ => {
2163 return Err(Error::NotSupported(format!(
2164 "unsupported semi-planar format: {format:?}"
2165 )))
2166 }
2167 };
2168
2169 let luma = Tensor::<u8>::from_fd(image.into_fd(), &[height, width], Some("luma"))?;
2170 if luma.memory() != TensorMemory::Dma {
2171 return Err(Error::NotSupported(format!(
2172 "luma fd must be DMA-backed, got {:?}",
2173 luma.memory()
2174 )));
2175 }
2176
2177 let chroma_tensor =
2178 Tensor::<u8>::from_fd(chroma_pd.into_fd(), &[chroma_h, width], Some("chroma"))?;
2179 if chroma_tensor.memory() != TensorMemory::Dma {
2180 return Err(Error::NotSupported(format!(
2181 "chroma fd must be DMA-backed, got {:?}",
2182 chroma_tensor.memory()
2183 )));
2184 }
2185
2186 let mut tensor = Tensor::<u8>::from_planes(luma, chroma_tensor, format)?;
2189
2190 if let Some(s) = image_stride {
2192 tensor.set_row_stride(s)?;
2193 }
2194 if let Some(o) = image_offset {
2195 tensor.set_plane_offset(o);
2196 }
2197
2198 if let Some(chroma_ref) = tensor.chroma_mut() {
2203 if let Some(s) = chroma_stride {
2204 if s < width {
2205 return Err(Error::InvalidShape(format!(
2206 "chroma stride {s} < minimum {width} for {format:?}"
2207 )));
2208 }
2209 chroma_ref.set_row_stride_unchecked(s);
2210 }
2211 if let Some(o) = chroma_offset {
2212 chroma_ref.set_plane_offset(o);
2213 }
2214 }
2215
2216 if dtype == DType::I8 {
2217 const {
2221 assert!(std::mem::size_of::<Tensor<u8>>() == std::mem::size_of::<Tensor<i8>>());
2222 assert!(
2223 std::mem::align_of::<Tensor<u8>>() == std::mem::align_of::<Tensor<i8>>()
2224 );
2225 }
2226 let tensor_i8: Tensor<i8> = unsafe { std::mem::transmute(tensor) };
2227 let mut dyn_tensor = TensorDyn::from(tensor_i8);
2228 dyn_tensor.set_colorimetry(colorimetry);
2229 return Ok(dyn_tensor);
2230 }
2231 let mut dyn_tensor = TensorDyn::from(tensor);
2232 dyn_tensor.set_colorimetry(colorimetry);
2233 Ok(dyn_tensor)
2234 } else {
2235 let shape = format.image_shape(width, height).ok_or_else(|| {
2240 Error::NotSupported(format!(
2241 "unsupported pixel format for import_image: {format:?}"
2242 ))
2243 })?;
2244 let tensor = TensorDyn::from_fd(image.into_fd(), &shape, dtype, None)?;
2245 if tensor.memory() != TensorMemory::Dma {
2246 return Err(Error::NotSupported(format!(
2247 "import_image requires DMA-backed fd, got {:?}",
2248 tensor.memory()
2249 )));
2250 }
2251 let mut tensor = tensor.with_format(format)?;
2252 if let Some(s) = image_stride {
2253 tensor.set_row_stride(s)?;
2254 }
2255 if let Some(o) = image_offset {
2256 tensor.set_plane_offset(o);
2257 }
2258 tensor.set_colorimetry(colorimetry);
2259 Ok(tensor)
2260 }
2261 }
2262
2263 pub fn draw_masks(
2271 &mut self,
2272 decoder: &edgefirst_decoder::Decoder,
2273 outputs: &[&TensorDyn],
2274 dst: &mut TensorDyn,
2275 overlay: MaskOverlay<'_>,
2276 ) -> Result<Vec<DetectBox>> {
2277 let mut output_boxes = Vec::with_capacity(100);
2278
2279 let proto_result = decoder
2281 .decode_proto(outputs, &mut output_boxes)
2282 .map_err(|e| Error::Internal(format!("decode_proto: {e:#?}")))?;
2283
2284 if let Some(proto_data) = proto_result {
2285 self.draw_proto_masks(dst, &output_boxes, &proto_data, overlay)?;
2286 } else {
2287 let mut output_masks = Vec::with_capacity(100);
2289 decoder
2290 .decode(outputs, &mut output_boxes, &mut output_masks)
2291 .map_err(|e| Error::Internal(format!("decode: {e:#?}")))?;
2292 self.draw_decoded_masks(dst, &output_boxes, &output_masks, overlay)?;
2293 }
2294 Ok(output_boxes)
2295 }
2296
2297 #[cfg(feature = "tracker")]
2305 pub fn draw_masks_tracked<TR: edgefirst_tracker::Tracker<DetectBox>>(
2306 &mut self,
2307 decoder: &edgefirst_decoder::Decoder,
2308 tracker: &mut TR,
2309 timestamp: u64,
2310 outputs: &[&TensorDyn],
2311 dst: &mut TensorDyn,
2312 overlay: MaskOverlay<'_>,
2313 ) -> Result<(Vec<DetectBox>, Vec<edgefirst_tracker::TrackInfo>)> {
2314 let mut output_boxes = Vec::with_capacity(100);
2315 let mut output_tracks = Vec::new();
2316
2317 let proto_result = decoder
2318 .decode_proto_tracked(
2319 tracker,
2320 timestamp,
2321 outputs,
2322 &mut output_boxes,
2323 &mut output_tracks,
2324 )
2325 .map_err(|e| Error::Internal(format!("decode_proto_tracked: {e:#?}")))?;
2326
2327 if let Some(proto_data) = proto_result {
2328 self.draw_proto_masks(dst, &output_boxes, &proto_data, overlay)?;
2329 } else {
2330 let mut output_masks = Vec::with_capacity(100);
2334 decoder
2335 .decode_tracked(
2336 tracker,
2337 timestamp,
2338 outputs,
2339 &mut output_boxes,
2340 &mut output_masks,
2341 &mut output_tracks,
2342 )
2343 .map_err(|e| Error::Internal(format!("decode_tracked: {e:#?}")))?;
2344 self.draw_decoded_masks(dst, &output_boxes, &output_masks, overlay)?;
2345 }
2346 Ok((output_boxes, output_tracks))
2347 }
2348
2349 pub fn materialize_masks(
2373 &mut self,
2374 detect: &[DetectBox],
2375 proto_data: &ProtoData,
2376 letterbox: Option<[f32; 4]>,
2377 resolution: MaskResolution,
2378 ) -> Result<Vec<Segmentation>> {
2379 let cpu = self.cpu.as_mut().ok_or(Error::NoConverter)?;
2380 match resolution {
2381 MaskResolution::Proto => cpu.materialize_segmentations(detect, proto_data, letterbox),
2382 MaskResolution::Scaled { width, height } => {
2383 cpu.materialize_scaled_segmentations(detect, proto_data, letterbox, width, height)
2384 }
2385 }
2386 }
2387}
2388
2389impl ImageProcessorTrait for ImageProcessor {
2390 fn convert(
2396 &mut self,
2397 src: &TensorDyn,
2398 dst: &mut TensorDyn,
2399 rotation: Rotation,
2400 flip: Flip,
2401 crop: Crop,
2402 ) -> Result<()> {
2403 let start = Instant::now();
2404 let src_fmt = src.format();
2405 let dst_fmt = dst.format();
2406 let _span = tracing::trace_span!(
2407 "image.convert",
2408 ?src_fmt,
2409 ?dst_fmt,
2410 src_memory = ?src.memory(),
2411 dst_memory = ?dst.memory(),
2412 ?rotation,
2413 ?flip,
2414 )
2415 .entered();
2416 log::trace!(
2417 "convert: {src_fmt:?}({:?}/{:?}) → {dst_fmt:?}({:?}/{:?}), \
2418 rotation={rotation:?}, flip={flip:?}, backend={:?}",
2419 src.dtype(),
2420 src.memory(),
2421 dst.dtype(),
2422 dst.memory(),
2423 self.forced_backend,
2424 );
2425
2426 if let Some(forced) = self.forced_backend {
2428 return match forced {
2429 ForcedBackend::Cpu => {
2430 if let Some(cpu) = self.cpu.as_mut() {
2431 let r = cpu.convert(src, dst, rotation, flip, crop);
2432 log::trace!(
2433 "convert: forced=cpu result={} ({:?})",
2434 if r.is_ok() { "ok" } else { "err" },
2435 start.elapsed()
2436 );
2437 return r;
2438 }
2439 Err(Error::ForcedBackendUnavailable("cpu".into()))
2440 }
2441 ForcedBackend::G2d => {
2442 #[cfg(target_os = "linux")]
2443 if let Some(g2d) = self.g2d.as_mut() {
2444 let r = g2d.convert(src, dst, rotation, flip, crop);
2445 log::trace!(
2446 "convert: forced=g2d result={} ({:?})",
2447 if r.is_ok() { "ok" } else { "err" },
2448 start.elapsed()
2449 );
2450 return r;
2451 }
2452 Err(Error::ForcedBackendUnavailable("g2d".into()))
2453 }
2454 ForcedBackend::OpenGl => {
2455 #[cfg(any(
2456 target_os = "linux",
2457 target_os = "macos",
2458 target_os = "ios",
2459 target_os = "android"
2460 ))]
2461 #[cfg(feature = "opengl")]
2462 if let Some(opengl) = self.opengl.as_mut() {
2463 let r = opengl.convert(src, dst, rotation, flip, crop);
2464 log::trace!(
2465 "convert: forced=opengl result={} ({:?})",
2466 if r.is_ok() { "ok" } else { "err" },
2467 start.elapsed()
2468 );
2469 return r;
2470 }
2471 Err(Error::ForcedBackendUnavailable("opengl".into()))
2472 }
2473 };
2474 }
2475
2476 #[cfg(any(
2478 target_os = "linux",
2479 target_os = "macos",
2480 target_os = "ios",
2481 target_os = "android"
2482 ))]
2483 #[cfg(feature = "opengl")]
2484 if let Some(opengl) = self.opengl.as_mut() {
2485 match opengl.convert(src, dst, rotation, flip, crop) {
2486 Ok(_) => {
2487 log::trace!(
2488 "convert: auto selected=opengl for {src_fmt:?}→{dst_fmt:?} ({:?})",
2489 start.elapsed()
2490 );
2491 return Ok(());
2492 }
2493 Err(e) => {
2494 self.convert_fallbacks
2495 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2496 log::debug!(
2497 "convert: auto opengl declined {src_fmt:?}@{:?}→{dst_fmt:?}@{:?}, \
2498 falling back toward G2D/CPU: {e}",
2499 src.memory(),
2500 dst.memory(),
2501 );
2502 }
2503 }
2504 }
2505
2506 #[cfg(target_os = "linux")]
2507 if let Some(g2d) = self.g2d.as_mut() {
2508 let src_is_yuv = src.format().is_some_and(|f| f.is_yuv());
2515 let dst_is_yuv = dst.format().is_some_and(|f| f.is_yuv());
2516 let g2d_eligible = if src_is_yuv || dst_is_yuv {
2517 let cm = if src_is_yuv {
2518 crate::colorimetry::effective_colorimetry(src)
2519 } else {
2520 crate::colorimetry::effective_colorimetry(dst)
2521 };
2522 crate::g2d::g2d_can_handle(&cm, true)
2523 } else {
2524 true
2525 };
2526 if !g2d_eligible {
2527 log::trace!(
2528 "convert: auto g2d skipped {src_fmt:?}→{dst_fmt:?} \
2529 (colorimetry not expressible: full-range/BT.2020)"
2530 );
2531 } else {
2532 match g2d.convert(src, dst, rotation, flip, crop) {
2533 Ok(_) => {
2534 log::trace!(
2535 "convert: auto selected=g2d for {src_fmt:?}→{dst_fmt:?} ({:?})",
2536 start.elapsed()
2537 );
2538 return Ok(());
2539 }
2540 Err(e) => {
2541 log::trace!("convert: auto g2d declined {src_fmt:?}→{dst_fmt:?}: {e}");
2542 }
2543 }
2544 }
2545 }
2546
2547 if let Some(cpu) = self.cpu.as_mut() {
2548 match cpu.convert(src, dst, rotation, flip, crop) {
2549 Ok(_) => {
2550 log::trace!(
2551 "convert: auto selected=cpu for {src_fmt:?}→{dst_fmt:?} ({:?})",
2552 start.elapsed()
2553 );
2554 return Ok(());
2555 }
2556 Err(e) => {
2557 log::trace!("convert: auto cpu failed {src_fmt:?}→{dst_fmt:?}: {e}");
2558 return Err(e);
2559 }
2560 }
2561 }
2562 Err(Error::NoConverter)
2563 }
2564
2565 fn convert_deferred(
2566 &mut self,
2567 src: &TensorDyn,
2568 dst: &mut TensorDyn,
2569 rotation: Rotation,
2570 flip: Flip,
2571 crop: Crop,
2572 ) -> Result<()> {
2573 #[cfg(any(
2579 target_os = "linux",
2580 target_os = "macos",
2581 target_os = "ios",
2582 target_os = "android"
2583 ))]
2584 #[cfg(feature = "opengl")]
2585 {
2586 let gl_forced = matches!(self.forced_backend, Some(ForcedBackend::OpenGl));
2587 if gl_forced || self.forced_backend.is_none() {
2588 if let Some(opengl) = self.opengl.as_mut() {
2589 match opengl.convert_deferred(src, dst, rotation, flip, crop) {
2590 Ok(()) => return Ok(()),
2591 Err(e) => {
2592 log::trace!("convert_deferred: gl declined: {e}; eager fallback");
2593 if gl_forced {
2596 return Err(e);
2597 }
2598 }
2599 }
2600 }
2601 }
2602 }
2603 self.convert(src, dst, rotation, flip, crop)
2604 }
2605
2606 fn flush(&mut self) -> Result<()> {
2607 let _span = tracing::trace_span!("image.flush").entered();
2608 #[cfg(any(
2611 target_os = "linux",
2612 target_os = "macos",
2613 target_os = "ios",
2614 target_os = "android"
2615 ))]
2616 #[cfg(feature = "opengl")]
2617 if let Some(opengl) = self.opengl.as_mut() {
2618 return opengl.flush();
2619 }
2620 Ok(())
2621 }
2622
2623 fn draw_decoded_masks(
2624 &mut self,
2625 dst: &mut TensorDyn,
2626 detect: &[DetectBox],
2627 segmentation: &[Segmentation],
2628 overlay: MaskOverlay<'_>,
2629 ) -> Result<()> {
2630 let _span = tracing::trace_span!(
2631 "image.draw_decoded_masks",
2632 n_detections = detect.len(),
2633 n_segmentations = segmentation.len(),
2634 )
2635 .entered();
2636 let start = Instant::now();
2637
2638 if let Some(bg) = overlay.background {
2639 if bg.aliases(dst) {
2640 return Err(Error::AliasedBuffers(
2641 "background must not reference the same buffer as dst".to_string(),
2642 ));
2643 }
2644 }
2645
2646 let lb_boxes: Vec<DetectBox>;
2649 let lb_segs: Vec<Segmentation>;
2650 let (detect, segmentation) = if let Some(lb) = overlay.letterbox {
2651 lb_boxes = detect.iter().map(|&d| unletter_bbox(d, lb)).collect();
2652 lb_segs = if segmentation.len() == lb_boxes.len() {
2655 segmentation
2656 .iter()
2657 .zip(lb_boxes.iter())
2658 .map(|(s, d)| Segmentation {
2659 xmin: d.bbox.xmin,
2660 ymin: d.bbox.ymin,
2661 xmax: d.bbox.xmax,
2662 ymax: d.bbox.ymax,
2663 segmentation: s.segmentation.clone(),
2664 })
2665 .collect()
2666 } else {
2667 segmentation.to_vec()
2668 };
2669 (lb_boxes.as_slice(), lb_segs.as_slice())
2670 } else {
2671 (detect, segmentation)
2672 };
2673 #[cfg(target_os = "linux")]
2674 let is_empty_frame = detect.is_empty() && segmentation.is_empty();
2675
2676 if let Some(forced) = self.forced_backend {
2678 return match forced {
2679 ForcedBackend::Cpu => {
2680 if let Some(cpu) = self.cpu.as_mut() {
2681 return cpu.draw_decoded_masks(dst, detect, segmentation, overlay);
2682 }
2683 Err(Error::ForcedBackendUnavailable("cpu".into()))
2684 }
2685 ForcedBackend::G2d => {
2686 #[cfg(target_os = "linux")]
2689 if let Some(g2d) = self.g2d.as_mut() {
2690 return g2d.draw_decoded_masks(dst, detect, segmentation, overlay);
2691 }
2692 Err(Error::ForcedBackendUnavailable("g2d".into()))
2693 }
2694 ForcedBackend::OpenGl => {
2695 #[cfg(target_os = "linux")]
2698 #[cfg(feature = "opengl")]
2699 if let Some(opengl) = self.opengl.as_mut() {
2700 return opengl.draw_decoded_masks(dst, detect, segmentation, overlay);
2701 }
2702 Err(Error::ForcedBackendUnavailable("opengl".into()))
2703 }
2704 };
2705 }
2706
2707 #[cfg(target_os = "linux")]
2713 if is_empty_frame {
2714 if let Some(g2d) = self.g2d.as_mut() {
2715 match g2d.draw_decoded_masks(dst, detect, segmentation, overlay) {
2716 Ok(_) => {
2717 log::trace!(
2718 "draw_decoded_masks empty frame via g2d in {:?}",
2719 start.elapsed()
2720 );
2721 return Ok(());
2722 }
2723 Err(e) => log::trace!("g2d empty-frame path unavailable: {e:?}"),
2724 }
2725 }
2726 }
2727
2728 #[cfg(target_os = "linux")]
2732 #[cfg(feature = "opengl")]
2733 if let Some(opengl) = self.opengl.as_mut() {
2734 log::trace!(
2735 "draw_decoded_masks started with opengl in {:?}",
2736 start.elapsed()
2737 );
2738 match opengl.draw_decoded_masks(dst, detect, segmentation, overlay) {
2739 Ok(_) => {
2740 log::trace!("draw_decoded_masks with opengl in {:?}", start.elapsed());
2741 return Ok(());
2742 }
2743 Err(e) => {
2744 log::trace!("draw_decoded_masks didn't work with opengl: {e:?}")
2745 }
2746 }
2747 }
2748
2749 log::trace!(
2750 "draw_decoded_masks started with cpu in {:?}",
2751 start.elapsed()
2752 );
2753 if let Some(cpu) = self.cpu.as_mut() {
2754 match cpu.draw_decoded_masks(dst, detect, segmentation, overlay) {
2755 Ok(_) => {
2756 log::trace!("draw_decoded_masks with cpu in {:?}", start.elapsed());
2757 return Ok(());
2758 }
2759 Err(e) => {
2760 log::trace!("draw_decoded_masks didn't work with cpu: {e:?}");
2761 return Err(e);
2762 }
2763 }
2764 }
2765 Err(Error::NoConverter)
2766 }
2767
2768 fn draw_proto_masks(
2769 &mut self,
2770 dst: &mut TensorDyn,
2771 detect: &[DetectBox],
2772 proto_data: &ProtoData,
2773 overlay: MaskOverlay<'_>,
2774 ) -> Result<()> {
2775 let start = Instant::now();
2776
2777 if let Some(bg) = overlay.background {
2778 if bg.aliases(dst) {
2779 return Err(Error::AliasedBuffers(
2780 "background must not reference the same buffer as dst".to_string(),
2781 ));
2782 }
2783 }
2784
2785 let lb_boxes: Vec<DetectBox>;
2791 let render_detect = if let Some(lb) = overlay.letterbox {
2792 lb_boxes = detect.iter().map(|&d| unletter_bbox(d, lb)).collect();
2793 lb_boxes.as_slice()
2794 } else {
2795 detect
2796 };
2797 #[cfg(target_os = "linux")]
2798 let is_empty_frame = detect.is_empty();
2799
2800 if let Some(forced) = self.forced_backend {
2802 return match forced {
2803 ForcedBackend::Cpu => {
2804 if let Some(cpu) = self.cpu.as_mut() {
2805 return cpu.draw_proto_masks(dst, render_detect, proto_data, overlay);
2806 }
2807 Err(Error::ForcedBackendUnavailable("cpu".into()))
2808 }
2809 ForcedBackend::G2d => {
2810 #[cfg(target_os = "linux")]
2811 if let Some(g2d) = self.g2d.as_mut() {
2812 return g2d.draw_proto_masks(dst, render_detect, proto_data, overlay);
2813 }
2814 Err(Error::ForcedBackendUnavailable("g2d".into()))
2815 }
2816 ForcedBackend::OpenGl => {
2817 #[cfg(target_os = "linux")]
2818 #[cfg(feature = "opengl")]
2819 if let Some(opengl) = self.opengl.as_mut() {
2820 return opengl.draw_proto_masks(dst, render_detect, proto_data, overlay);
2821 }
2822 Err(Error::ForcedBackendUnavailable("opengl".into()))
2823 }
2824 };
2825 }
2826
2827 #[cfg(target_os = "linux")]
2830 if is_empty_frame {
2831 if let Some(g2d) = self.g2d.as_mut() {
2832 match g2d.draw_proto_masks(dst, render_detect, proto_data, overlay) {
2833 Ok(_) => {
2834 log::trace!(
2835 "draw_proto_masks empty frame via g2d in {:?}",
2836 start.elapsed()
2837 );
2838 return Ok(());
2839 }
2840 Err(e) => log::trace!("g2d empty-frame path unavailable: {e:?}"),
2841 }
2842 }
2843 }
2844
2845 #[cfg(target_os = "linux")]
2854 #[cfg(feature = "opengl")]
2855 if let (Some(_), Some(_)) = (self.cpu.as_ref(), self.opengl.as_ref()) {
2856 let segmentation = match self.cpu.as_mut() {
2857 Some(cpu) => {
2858 log::trace!(
2859 "draw_proto_masks started with hybrid (cpu+opengl) in {:?}",
2860 start.elapsed()
2861 );
2862 cpu.materialize_segmentations(detect, proto_data, overlay.letterbox)?
2863 }
2864 None => unreachable!("cpu presence checked above"),
2865 };
2866 if let Some(opengl) = self.opengl.as_mut() {
2867 match opengl.draw_decoded_masks(dst, render_detect, &segmentation, overlay) {
2868 Ok(_) => {
2869 log::trace!(
2870 "draw_proto_masks with hybrid (cpu+opengl) in {:?}",
2871 start.elapsed()
2872 );
2873 return Ok(());
2874 }
2875 Err(e) => {
2876 log::trace!(
2877 "draw_proto_masks hybrid path failed, falling back to cpu: {e:?}"
2878 );
2879 }
2880 }
2881 }
2882 }
2883
2884 let Some(cpu) = self.cpu.as_mut() else {
2885 return Err(Error::Internal(
2886 "draw_proto_masks requires CPU backend for fallback path".into(),
2887 ));
2888 };
2889 log::trace!("draw_proto_masks started with cpu in {:?}", start.elapsed());
2890 cpu.draw_proto_masks(dst, render_detect, proto_data, overlay)
2891 }
2892
2893 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()> {
2894 let start = Instant::now();
2895
2896 if let Some(forced) = self.forced_backend {
2898 return match forced {
2899 ForcedBackend::Cpu => {
2900 if let Some(cpu) = self.cpu.as_mut() {
2901 return cpu.set_class_colors(colors);
2902 }
2903 Err(Error::ForcedBackendUnavailable("cpu".into()))
2904 }
2905 ForcedBackend::G2d => Err(Error::NotSupported(
2906 "g2d does not support set_class_colors".into(),
2907 )),
2908 ForcedBackend::OpenGl => {
2909 #[cfg(target_os = "linux")]
2910 #[cfg(feature = "opengl")]
2911 if let Some(opengl) = self.opengl.as_mut() {
2912 return opengl.set_class_colors(colors);
2913 }
2914 Err(Error::ForcedBackendUnavailable("opengl".into()))
2915 }
2916 };
2917 }
2918
2919 #[cfg(target_os = "linux")]
2922 #[cfg(feature = "opengl")]
2923 if let Some(opengl) = self.opengl.as_mut() {
2924 log::trace!("image started with opengl in {:?}", start.elapsed());
2925 match opengl.set_class_colors(colors) {
2926 Ok(_) => {
2927 log::trace!("colors set with opengl in {:?}", start.elapsed());
2928 return Ok(());
2929 }
2930 Err(e) => {
2931 log::trace!("colors didn't set with opengl: {e:?}")
2932 }
2933 }
2934 }
2935 log::trace!("image started with cpu in {:?}", start.elapsed());
2936 if let Some(cpu) = self.cpu.as_mut() {
2937 match cpu.set_class_colors(colors) {
2938 Ok(_) => {
2939 log::trace!("colors set with cpu in {:?}", start.elapsed());
2940 return Ok(());
2941 }
2942 Err(e) => {
2943 log::trace!("colors didn't set with cpu: {e:?}");
2944 return Err(e);
2945 }
2946 }
2947 }
2948 Err(Error::NoConverter)
2949 }
2950}
2951
2952#[cfg(test)]
2962pub(crate) fn load_image_test_helper(
2963 image: &[u8],
2964 format: Option<PixelFormat>,
2965 memory: Option<TensorMemory>,
2966) -> Result<TensorDyn> {
2967 use edgefirst_codec::{peek_info, ImageDecoder, ImageLoad};
2968
2969 let info = peek_info(image)?;
2973 let native_fmt = info.format;
2974 let w = info.width;
2975 let h = info.height;
2976
2977 let mut decoder = ImageDecoder::new();
2978
2979 #[cfg(target_os = "linux")]
2982 let native_src = {
2983 if let Some(aligned_pitch) = padded_dma_pitch_for(native_fmt, w, &memory) {
2984 let mut dma = Tensor::<u8>::image_with_stride(
2985 w,
2986 h,
2987 native_fmt,
2988 aligned_pitch,
2989 Some(TensorMemory::Dma),
2990 edgefirst_tensor::CpuAccess::ReadWrite,
2991 )?;
2992 dma.load_image(&mut decoder, image)?;
2993 TensorDyn::from(dma)
2994 } else {
2995 let mut img = Tensor::<u8>::image(
2996 w,
2997 h,
2998 native_fmt,
2999 memory,
3000 edgefirst_tensor::CpuAccess::ReadWrite,
3001 )?;
3002 img.load_image(&mut decoder, image)?;
3003 TensorDyn::from(img)
3004 }
3005 };
3006 #[cfg(not(target_os = "linux"))]
3007 let native_src = {
3008 let mut img = Tensor::<u8>::image(
3009 w,
3010 h,
3011 native_fmt,
3012 memory,
3013 edgefirst_tensor::CpuAccess::ReadWrite,
3014 )?;
3015 img.load_image(&mut decoder, image)?;
3016 TensorDyn::from(img)
3017 };
3018
3019 match format {
3023 Some(f) if f != native_fmt => {
3024 let mut dst = TensorDyn::image(
3025 w,
3026 h,
3027 f,
3028 DType::U8,
3029 memory,
3030 edgefirst_tensor::CpuAccess::ReadWrite,
3031 )?;
3032 #[allow(clippy::needless_update)]
3039 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
3040 backend: ComputeBackend::Cpu,
3041 ..Default::default()
3042 })?;
3043 proc.convert(
3044 &native_src,
3045 &mut dst,
3046 Rotation::None,
3047 Flip::None,
3048 Crop::default(),
3049 )?;
3050 Ok(dst)
3051 }
3052 _ => Ok(native_src),
3053 }
3054}
3055
3056pub fn save_jpeg(tensor: &TensorDyn, path: impl AsRef<std::path::Path>, quality: u8) -> Result<()> {
3060 let t = tensor.as_u8().ok_or(Error::UnsupportedFormat(
3061 "save_jpeg requires u8 tensor".to_string(),
3062 ))?;
3063 let fmt = t.format().ok_or(Error::NotAnImage)?;
3064 if fmt.layout() != PixelLayout::Packed {
3065 return Err(Error::NotImplemented(
3066 "Saving planar images is not supported".to_string(),
3067 ));
3068 }
3069
3070 let colour = match fmt {
3071 PixelFormat::Rgb => jpeg_encoder::ColorType::Rgb,
3072 PixelFormat::Rgba => jpeg_encoder::ColorType::Rgba,
3073 _ => {
3074 return Err(Error::NotImplemented(
3075 "Unsupported image format for saving".to_string(),
3076 ));
3077 }
3078 };
3079
3080 let w = t.width().ok_or(Error::NotAnImage)?;
3081 let h = t.height().ok_or(Error::NotAnImage)?;
3082 let encoder = jpeg_encoder::Encoder::new_file(path, quality)?;
3083 let tensor_map = t.map_read()?;
3084
3085 encoder.encode(&tensor_map, w as u16, h as u16, colour)?;
3086
3087 Ok(())
3088}
3089
3090pub(crate) struct FunctionTimer<T: Display> {
3091 name: T,
3092 start: std::time::Instant,
3093}
3094
3095impl<T: Display> FunctionTimer<T> {
3096 pub fn new(name: T) -> Self {
3097 Self {
3098 name,
3099 start: std::time::Instant::now(),
3100 }
3101 }
3102}
3103
3104impl<T: Display> Drop for FunctionTimer<T> {
3105 fn drop(&mut self) {
3106 log::trace!("{} elapsed: {:?}", self.name, self.start.elapsed())
3107 }
3108}
3109
3110const DEFAULT_COLORS: [[f32; 4]; 20] = [
3111 [0., 1., 0., 0.7],
3112 [1., 0.5568628, 0., 0.7],
3113 [0.25882353, 0.15294118, 0.13333333, 0.7],
3114 [0.8, 0.7647059, 0.78039216, 0.7],
3115 [0.3137255, 0.3137255, 0.3137255, 0.7],
3116 [0.1411765, 0.3098039, 0.1215686, 0.7],
3117 [1., 0.95686275, 0.5137255, 0.7],
3118 [0.3529412, 0.32156863, 0., 0.7],
3119 [0.4235294, 0.6235294, 0.6509804, 0.7],
3120 [0.5098039, 0.5098039, 0.7294118, 0.7],
3121 [0.00784314, 0.18823529, 0.29411765, 0.7],
3122 [0.0, 0.2706, 1.0, 0.7],
3123 [0.0, 0.0, 0.0, 0.7],
3124 [0.0, 0.5, 0.0, 0.7],
3125 [1.0, 0.0, 0.0, 0.7],
3126 [0.0, 0.0, 1.0, 0.7],
3127 [1.0, 0.5, 0.5, 0.7],
3128 [0.1333, 0.5451, 0.1333, 0.7],
3129 [0.1176, 0.4118, 0.8235, 0.7],
3130 [1., 1., 1., 0.7],
3131];
3132
3133const fn denorm<const M: usize, const N: usize>(a: [[f32; M]; N]) -> [[u8; M]; N] {
3134 let mut result = [[0; M]; N];
3135 let mut i = 0;
3136 while i < N {
3137 let mut j = 0;
3138 while j < M {
3139 result[i][j] = (a[i][j] * 255.0).round() as u8;
3140 j += 1;
3141 }
3142 i += 1;
3143 }
3144 result
3145}
3146
3147const DEFAULT_COLORS_U8: [[u8; 4]; 20] = denorm(DEFAULT_COLORS);
3148
3149#[cfg(test)]
3150#[cfg_attr(coverage_nightly, coverage(off))]
3151mod alignment_tests {
3152 use super::*;
3153
3154 #[test]
3155 fn align_width_rgba8_common_widths() {
3156 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); }
3167
3168 #[test]
3169 fn align_width_rgb888_packed() {
3170 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] {
3177 let padded = align_width_for_gpu_pitch(w, 3);
3178 assert!(padded >= w);
3179 assert_eq!((padded * 3) % 64, 0);
3180 assert_eq!((padded * 3) % 3, 0);
3181 }
3182 }
3183
3184 #[test]
3185 fn align_width_grey_u8() {
3186 assert_eq!(align_width_for_gpu_pitch(64, 1), 64);
3188 assert_eq!(align_width_for_gpu_pitch(640, 1), 640);
3189 assert_eq!(align_width_for_gpu_pitch(1, 1), 64);
3190 assert_eq!(align_width_for_gpu_pitch(65, 1), 128);
3191 }
3192
3193 #[test]
3194 fn align_width_zero_inputs() {
3195 assert_eq!(align_width_for_gpu_pitch(0, 4), 0);
3196 assert_eq!(align_width_for_gpu_pitch(640, 0), 640);
3197 }
3198
3199 #[test]
3200 fn align_width_never_returns_smaller_than_input() {
3201 for &bpp in &[1usize, 2, 3, 4, 8] {
3205 for &w in &[
3206 1usize,
3207 17,
3208 64,
3209 65,
3210 100,
3211 1280,
3212 1281,
3213 1920,
3214 3004,
3215 3072,
3216 3840,
3217 usize::MAX / 8,
3218 usize::MAX / 4,
3219 usize::MAX / 2,
3220 usize::MAX - 1,
3221 usize::MAX,
3222 ] {
3223 let aligned = align_width_for_gpu_pitch(w, bpp);
3224 assert!(
3225 aligned >= w,
3226 "align_width_for_gpu_pitch({w}, {bpp}) = {aligned} < {w}"
3227 );
3228 }
3229 }
3230 }
3231
3232 #[test]
3233 fn align_width_overflow_returns_unaligned_not_smaller() {
3234 let aligned_extreme = usize::MAX - 15; assert_eq!(
3240 align_width_for_gpu_pitch(aligned_extreme, 4),
3241 aligned_extreme
3242 );
3243 let misaligned_extreme = usize::MAX - 1;
3246 let result = align_width_for_gpu_pitch(misaligned_extreme, 4);
3247 assert!(
3248 result == misaligned_extreme || result >= misaligned_extreme,
3249 "extreme misaligned width must not be rounded down to {result}"
3250 );
3251 }
3252
3253 #[test]
3254 fn checked_lcm_basic_and_overflow() {
3255 assert_eq!(checked_num_integer_lcm(64, 4), Some(64));
3256 assert_eq!(checked_num_integer_lcm(64, 3), Some(192));
3257 assert_eq!(checked_num_integer_lcm(64, 1), Some(64));
3258 assert_eq!(checked_num_integer_lcm(0, 4), Some(0));
3259 assert_eq!(checked_num_integer_lcm(64, 0), Some(0));
3260 assert_eq!(
3262 checked_num_integer_lcm(usize::MAX, usize::MAX - 1),
3263 None,
3264 "coprime extreme values must overflow detect, not panic"
3265 );
3266 }
3267
3268 #[test]
3269 fn primary_plane_bpp_known_formats() {
3270 assert_eq!(primary_plane_bpp(PixelFormat::Rgba, 1), Some(4));
3272 assert_eq!(primary_plane_bpp(PixelFormat::Bgra, 1), Some(4));
3273 assert_eq!(primary_plane_bpp(PixelFormat::Rgb, 1), Some(3));
3274 assert_eq!(primary_plane_bpp(PixelFormat::Grey, 1), Some(1));
3275 assert_eq!(primary_plane_bpp(PixelFormat::Nv12, 1), Some(1));
3277 }
3278}
3279
3280#[cfg(test)]
3281#[cfg_attr(coverage_nightly, coverage(off))]
3282#[allow(deprecated)]
3283mod image_tests {
3284 use super::*;
3285 use crate::{CPUProcessor, Rotation};
3286 #[cfg(target_os = "linux")]
3287 use edgefirst_tensor::is_dma_available;
3288 use edgefirst_tensor::{TensorMapTrait, TensorMemory, TensorTrait};
3289 use image::buffer::ConvertBuffer;
3290
3291 fn convert_img(
3297 proc: &mut dyn ImageProcessorTrait,
3298 src: TensorDyn,
3299 dst: TensorDyn,
3300 rotation: Rotation,
3301 flip: Flip,
3302 crop: Crop,
3303 ) -> (Result<()>, TensorDyn, TensorDyn) {
3304 let src_fourcc = src.format().unwrap();
3305 let dst_fourcc = dst.format().unwrap();
3306 let src_dyn = src;
3307 let mut dst_dyn = dst;
3308 let result = proc.convert(&src_dyn, &mut dst_dyn, rotation, flip, crop);
3309 let src_back = {
3310 let mut __t = src_dyn.into_u8().unwrap();
3311 __t.set_format(src_fourcc).unwrap();
3312 TensorDyn::from(__t)
3313 };
3314 let dst_back = {
3315 let mut __t = dst_dyn.into_u8().unwrap();
3316 __t.set_format(dst_fourcc).unwrap();
3317 TensorDyn::from(__t)
3318 };
3319 (result, src_back, dst_back)
3320 }
3321
3322 #[ctor::ctor(unsafe)]
3323 fn init() {
3324 env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
3325 }
3326
3327 macro_rules! function {
3328 () => {{
3329 fn f() {}
3330 fn type_name_of<T>(_: T) -> &'static str {
3331 std::any::type_name::<T>()
3332 }
3333 let name = type_name_of(f);
3334
3335 match &name[..name.len() - 3].rfind(':') {
3337 Some(pos) => &name[pos + 1..name.len() - 3],
3338 None => &name[..name.len() - 3],
3339 }
3340 }};
3341 }
3342
3343 #[test]
3356 fn batch_view_dst_tiles_match_standalone() {
3357 let mut proc = match ImageProcessor::new() {
3358 Ok(p) => p,
3359 Err(e) => {
3360 eprintln!(
3361 "SKIPPED: {} — ImageProcessor init failed ({e:?})",
3362 function!()
3363 );
3364 return;
3365 }
3366 };
3367 let n = 3usize;
3368 let (w, h) = (32usize, 24usize);
3369 let colors: [[u8; 4]; 3] = [[210, 40, 40, 255], [40, 210, 40, 255], [40, 40, 210, 255]];
3370 let make_src = |c: [u8; 4]| -> TensorDyn {
3371 let bytes: Vec<u8> = c.iter().copied().cycle().take(w * h * 4).collect();
3372 load_bytes_to_tensor(w, h, PixelFormat::Rgba, Some(TensorMemory::Mem), &bytes).unwrap()
3373 };
3374 let parent = match TensorDyn::image(
3377 w,
3378 n * h,
3379 PixelFormat::Rgba,
3380 DType::U8,
3381 Some(TensorMemory::Dma),
3382 edgefirst_tensor::CpuAccess::ReadWrite,
3383 ) {
3384 Ok(d) => d,
3385 Err(e) => {
3386 eprintln!(
3387 "SKIPPED: {} — tall DMA destination alloc failed ({e:?})",
3388 function!()
3389 );
3390 return;
3391 }
3392 };
3393
3394 for (i, &c) in colors.iter().enumerate().take(n) {
3396 let mut tile = parent.view(Region::new(0, i * h, w, h)).unwrap();
3397 proc.convert_deferred(
3398 &make_src(c),
3399 &mut tile,
3400 Rotation::None,
3401 Flip::None,
3402 Crop::no_crop(),
3403 )
3404 .unwrap_or_else(|e| panic!("convert_deferred tile {i}: {e:?}"));
3405 }
3406 proc.flush().unwrap();
3407
3408 for (i, &c) in colors.iter().enumerate().take(n) {
3409 let mut solo = TensorDyn::image(
3411 w,
3412 h,
3413 PixelFormat::Rgba,
3414 DType::U8,
3415 Some(TensorMemory::Dma),
3416 edgefirst_tensor::CpuAccess::ReadWrite,
3417 )
3418 .unwrap();
3419 proc.convert(
3420 &make_src(c),
3421 &mut solo,
3422 Rotation::None,
3423 Flip::None,
3424 Crop::no_crop(),
3425 )
3426 .unwrap();
3427
3428 let band = parent.view(Region::new(0, i * h, w, h)).unwrap();
3429 let band_bytes = band.as_u8().unwrap().map().unwrap().as_slice().to_vec();
3430 let solo_bytes = solo.as_u8().unwrap().map().unwrap().as_slice().to_vec();
3431 assert_eq!(
3432 band_bytes, solo_bytes,
3433 "tile {i}: band differs from standalone convert (placement or sibling wipe)"
3434 );
3435 assert!(
3436 band_bytes.chunks_exact(4).all(|p| p == c),
3437 "tile {i}: band is not the expected solid color {c:?} (sibling wipe?)"
3438 );
3439 }
3440 }
3441
3442 #[cfg(test)]
3446 fn gradient_frame(w: usize, h: usize) -> TensorDyn {
3447 let mut bytes = vec![0u8; w * h * 4];
3448 for y in 0..h {
3449 for x in 0..w {
3450 let i = (y * w + x) * 4;
3451 bytes[i] = x as u8;
3452 bytes[i + 1] = y as u8;
3453 bytes[i + 2] = (x ^ y) as u8;
3454 bytes[i + 3] = 255;
3455 }
3456 }
3457 load_bytes_to_tensor(w, h, PixelFormat::Rgba, Some(TensorMemory::Mem), &bytes).unwrap()
3458 }
3459
3460 #[test]
3464 fn tile_into_cpu_distinct_content_parity() {
3465 let mut proc = match ImageProcessor::with_config(ImageProcessorConfig {
3466 backend: ComputeBackend::Cpu,
3467 ..Default::default()
3468 }) {
3469 Ok(p) => p,
3470 Err(e) => {
3471 eprintln!("SKIPPED: {} — CPU init failed ({e:?})", function!());
3472 return;
3473 }
3474 };
3475 let (fw, fh) = (96usize, 64usize);
3476 let src = gradient_frame(fw, fh);
3477 let cfg = TilingConfig::new(32, 32).with_overlap(0.0); let n = tile_grid(fh, fw, 32, 32, 0.0).len();
3479 assert_eq!(n, 6);
3480
3481 let mut parent = proc
3482 .alloc_tile_batch(
3483 n,
3484 &cfg,
3485 PixelFormat::Rgba,
3486 DType::U8,
3487 Some(TensorMemory::Mem),
3488 edgefirst_tensor::CpuAccess::ReadWrite,
3489 )
3490 .unwrap();
3491 let placements = proc.tile_into(&src, &mut parent, &cfg).unwrap();
3492 assert_eq!(placements.len(), n);
3493
3494 for p in &placements {
3495 let source = Region::new(
3496 p.origin.0 as usize,
3497 p.origin.1 as usize,
3498 p.crop_size.0 as usize,
3499 p.crop_size.1 as usize,
3500 );
3501 let mut solo = TensorDyn::image(
3502 32,
3503 32,
3504 PixelFormat::Rgba,
3505 DType::U8,
3506 Some(TensorMemory::Mem),
3507 edgefirst_tensor::CpuAccess::ReadWrite,
3508 )
3509 .unwrap();
3510 proc.convert(
3511 &src,
3512 &mut solo,
3513 Rotation::None,
3514 Flip::None,
3515 Crop::default()
3516 .with_source(Some(source))
3517 .with_fit(Fit::Stretch),
3518 )
3519 .unwrap();
3520 let band = parent.view(Region::new(0, p.index * 32, 32, 32)).unwrap();
3521 let band_bytes = band.as_u8().unwrap().map().unwrap().as_slice().to_vec();
3522 let solo_bytes = solo.as_u8().unwrap().map().unwrap().as_slice().to_vec();
3523 assert_eq!(
3524 band_bytes, solo_bytes,
3525 "tile {} band differs from standalone crop-convert",
3526 p.index
3527 );
3528 }
3529 }
3530
3531 #[test]
3534 fn tile_one_matches_tile_into_band() {
3535 let mut proc = match ImageProcessor::with_config(ImageProcessorConfig {
3536 backend: ComputeBackend::Cpu,
3537 ..Default::default()
3538 }) {
3539 Ok(p) => p,
3540 Err(e) => {
3541 eprintln!("SKIPPED: {} — CPU init failed ({e:?})", function!());
3542 return;
3543 }
3544 };
3545 let (fw, fh) = (96usize, 64usize);
3546 let src = gradient_frame(fw, fh);
3547 let cfg = TilingConfig::new(32, 32).with_overlap(0.0);
3548 let n = tile_grid(fh, fw, 32, 32, 0.0).len();
3549
3550 let mut parent = proc
3551 .alloc_tile_batch(
3552 n,
3553 &cfg,
3554 PixelFormat::Rgba,
3555 DType::U8,
3556 Some(TensorMemory::Mem),
3557 edgefirst_tensor::CpuAccess::ReadWrite,
3558 )
3559 .unwrap();
3560 proc.tile_into(&src, &mut parent, &cfg).unwrap();
3561
3562 let plan = proc.plan_tiles(fw, fh, &cfg).unwrap();
3563 for p in &plan {
3564 let mut slot = TensorDyn::image(
3565 32,
3566 32,
3567 PixelFormat::Rgba,
3568 DType::U8,
3569 Some(TensorMemory::Mem),
3570 edgefirst_tensor::CpuAccess::ReadWrite,
3571 )
3572 .unwrap();
3573 proc.tile_one(&src, &mut slot, p, &cfg).unwrap();
3574 proc.flush().unwrap();
3575 let band = parent.view(Region::new(0, p.index * 32, 32, 32)).unwrap();
3576 let slot_bytes = slot.as_u8().unwrap().map().unwrap().as_slice().to_vec();
3577 let band_bytes = band.as_u8().unwrap().map().unwrap().as_slice().to_vec();
3578 assert_eq!(
3579 slot_bytes, band_bytes,
3580 "tile {} stream != batch band",
3581 p.index
3582 );
3583 }
3584 }
3585
3586 #[test]
3593 fn tile_into_auto_dma_parity() {
3594 let mut proc = match ImageProcessor::new() {
3595 Ok(p) => p,
3596 Err(e) => {
3597 eprintln!(
3598 "SKIPPED: {} — ImageProcessor init failed ({e:?})",
3599 function!()
3600 );
3601 return;
3602 }
3603 };
3604 let (fw, fh) = (96usize, 64usize);
3605 let src = gradient_frame(fw, fh);
3606 let cfg = TilingConfig::new(32, 32).with_overlap(0.0);
3607 let n = tile_grid(fh, fw, 32, 32, 0.0).len();
3608
3609 let mut parent = match proc.alloc_tile_batch(
3610 n,
3611 &cfg,
3612 PixelFormat::Rgba,
3613 DType::U8,
3614 Some(TensorMemory::Dma),
3615 edgefirst_tensor::CpuAccess::ReadWrite,
3616 ) {
3617 Ok(p) => p,
3618 Err(e) => {
3619 eprintln!(
3620 "SKIPPED: {} — tall DMA parent alloc failed ({e:?})",
3621 function!()
3622 );
3623 return;
3624 }
3625 };
3626 let placements = proc.tile_into(&src, &mut parent, &cfg).unwrap();
3627
3628 for p in &placements {
3629 let source = Region::new(
3630 p.origin.0 as usize,
3631 p.origin.1 as usize,
3632 p.crop_size.0 as usize,
3633 p.crop_size.1 as usize,
3634 );
3635 let mut solo = TensorDyn::image(
3636 32,
3637 32,
3638 PixelFormat::Rgba,
3639 DType::U8,
3640 Some(TensorMemory::Dma),
3641 edgefirst_tensor::CpuAccess::ReadWrite,
3642 )
3643 .unwrap();
3644 proc.convert(
3645 &src,
3646 &mut solo,
3647 Rotation::None,
3648 Flip::None,
3649 Crop::default()
3650 .with_source(Some(source))
3651 .with_fit(Fit::Stretch),
3652 )
3653 .unwrap();
3654 let band = parent.view(Region::new(0, p.index * 32, 32, 32)).unwrap();
3655 compare_images(
3661 &band,
3662 &solo,
3663 0.98,
3664 &format!("{}_tile{}", function!(), p.index),
3665 );
3666 }
3667 }
3668
3669 #[test]
3671 fn tile_into_undersized_dst_errors() {
3672 let mut proc = match ImageProcessor::with_config(ImageProcessorConfig {
3673 backend: ComputeBackend::Cpu,
3674 ..Default::default()
3675 }) {
3676 Ok(p) => p,
3677 Err(e) => {
3678 eprintln!("SKIPPED: {} — CPU init failed ({e:?})", function!());
3679 return;
3680 }
3681 };
3682 let (fw, fh) = (96usize, 64usize);
3683 let src = gradient_frame(fw, fh);
3684 let cfg = TilingConfig::new(32, 32).with_overlap(0.0); let mut small = TensorDyn::image(
3687 32,
3688 2 * 32,
3689 PixelFormat::Rgba,
3690 DType::U8,
3691 Some(TensorMemory::Mem),
3692 edgefirst_tensor::CpuAccess::ReadWrite,
3693 )
3694 .unwrap();
3695 let r = proc.tile_into(&src, &mut small, &cfg);
3696 assert!(
3697 matches!(r, Err(Error::InvalidShape(_))),
3698 "expected InvalidShape, got {r:?}"
3699 );
3700 }
3701
3702 #[test]
3704 fn tiling_alloc_rejects_invalid_config() {
3705 let proc = match ImageProcessor::with_config(ImageProcessorConfig {
3706 backend: ComputeBackend::Cpu,
3707 ..Default::default()
3708 }) {
3709 Ok(p) => p,
3710 Err(e) => {
3711 eprintln!("SKIPPED: {} — CPU init failed ({e:?})", function!());
3712 return;
3713 }
3714 };
3715 let bad = TilingConfig::new(0, 640);
3716 assert!(proc.plan_tiles(1920, 1080, &bad).is_err());
3717 assert!(proc
3718 .alloc_tile_batch(
3719 4,
3720 &bad,
3721 PixelFormat::Rgba,
3722 DType::U8,
3723 Some(TensorMemory::Mem),
3724 edgefirst_tensor::CpuAccess::ReadWrite,
3725 )
3726 .is_err());
3727 }
3728
3729 #[test]
3730 fn plan_tiles_metadata_4k() {
3731 let proc = match ImageProcessor::with_config(ImageProcessorConfig {
3732 backend: ComputeBackend::Cpu,
3733 ..Default::default()
3734 }) {
3735 Ok(p) => p,
3736 Err(e) => {
3737 eprintln!("SKIPPED: {} — CPU init failed ({e:?})", function!());
3738 return;
3739 }
3740 };
3741 let cfg = TilingConfig::new(640, 640).with_overlap(0.2);
3742 let plan = proc.plan_tiles(3840, 2160, &cfg).unwrap();
3743 assert_eq!(plan.len(), 32);
3744 assert!(plan.iter().all(|p| p.count == 32));
3745 assert!(plan.iter().all(|p| p.crop_size == (640.0, 640.0)));
3746 assert!(plan.iter().all(|p| p.letterbox.is_none())); assert!(plan.iter().all(|p| p.frame_dims == (3840.0, 2160.0)));
3748 assert_eq!(plan[0].origin, (0.0, 0.0));
3749 }
3750
3751 #[test]
3752 fn test_invalid_crop() {
3753 let src = TensorDyn::image(
3754 100,
3755 100,
3756 PixelFormat::Rgb,
3757 DType::U8,
3758 None,
3759 edgefirst_tensor::CpuAccess::ReadWrite,
3760 )
3761 .unwrap();
3762 let dst = TensorDyn::image(
3763 100,
3764 100,
3765 PixelFormat::Rgb,
3766 DType::U8,
3767 None,
3768 edgefirst_tensor::CpuAccess::ReadWrite,
3769 )
3770 .unwrap();
3771
3772 let crop = Crop::new().with_source(Some(Region::new(50, 50, 60, 60)));
3774 assert!(matches!(
3775 crop.check_crop_dyn(&src, &dst),
3776 Err(Error::CropInvalid(_))
3777 ));
3778
3779 let crop = Crop::new().with_source(Some(Region::new(0, 0, 10, 10)));
3781 assert!(crop.check_crop_dyn(&src, &dst).is_ok());
3782
3783 assert!(Crop::letterbox([0, 0, 0, 255])
3785 .check_crop_dyn(&src, &dst)
3786 .is_ok());
3787 }
3788
3789 #[test]
3790 fn test_invalid_tensor_format() -> Result<(), Error> {
3791 let mut tensor = Tensor::<u8>::new(&[720, 1280, 4, 1], None, None)?;
3793 let result = tensor.set_format(PixelFormat::Rgb);
3794 assert!(result.is_err(), "4D tensor should reject set_format");
3795
3796 let mut tensor = Tensor::<u8>::new(&[720, 1280, 4], None, None)?;
3798 let result = tensor.set_format(PixelFormat::Rgb);
3799 assert!(result.is_err(), "4-channel tensor should reject RGB format");
3800
3801 Ok(())
3802 }
3803
3804 #[test]
3805 fn test_invalid_image_file() -> Result<(), Error> {
3806 let result = crate::load_image_test_helper(&[123; 5000], None, None);
3807 assert!(
3808 matches!(result, Err(Error::Codec(_))),
3809 "unrecognised bytes should surface as Error::Codec, got {result:?}"
3810 );
3811 Ok(())
3812 }
3813
3814 #[test]
3815 fn test_invalid_jpeg_format() -> Result<(), Error> {
3816 let result = crate::load_image_test_helper(&[123; 5000], Some(PixelFormat::Yuyv), None);
3817 assert!(
3820 matches!(result, Err(Error::Codec(_))),
3821 "Yuyv target with garbage bytes should surface as Error::Codec, got {result:?}"
3822 );
3823 Ok(())
3824 }
3825
3826 #[test]
3827 fn test_load_resize_save() {
3828 let file = edgefirst_bench::testdata::read("zidane.jpg");
3829 let img = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
3830 assert_eq!(img.width(), Some(1280));
3831 assert_eq!(img.height(), Some(720));
3832
3833 let dst = TensorDyn::image(
3834 640,
3835 360,
3836 PixelFormat::Rgba,
3837 DType::U8,
3838 None,
3839 edgefirst_tensor::CpuAccess::ReadWrite,
3840 )
3841 .unwrap();
3842 let mut converter = CPUProcessor::new();
3843 let (result, _img, dst) = convert_img(
3844 &mut converter,
3845 img,
3846 dst,
3847 Rotation::None,
3848 Flip::None,
3849 Crop::no_crop(),
3850 );
3851 result.unwrap();
3852 assert_eq!(dst.width(), Some(640));
3853 assert_eq!(dst.height(), Some(360));
3854
3855 crate::save_jpeg(&dst, "zidane_resized.jpg", 80).unwrap();
3856
3857 let file = std::fs::read("zidane_resized.jpg").unwrap();
3858 let img = crate::load_image_test_helper(&file, None, None).unwrap();
3861 assert_eq!(img.width(), Some(640));
3862 assert_eq!(img.height(), Some(360));
3863 assert_eq!(img.format().unwrap(), PixelFormat::Nv12);
3864 }
3865
3866 #[test]
3867 fn test_from_tensor_planar() -> Result<(), Error> {
3868 let mut tensor = Tensor::new(&[3, 720, 1280], None, None)?;
3869 tensor
3870 .map()?
3871 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.8bps"));
3872 let planar = {
3873 tensor
3874 .set_format(PixelFormat::PlanarRgb)
3875 .map_err(|e| crate::Error::Internal(e.to_string()))?;
3876 TensorDyn::from(tensor)
3877 };
3878
3879 let rbga = load_bytes_to_tensor(
3880 1280,
3881 720,
3882 PixelFormat::Rgba,
3883 None,
3884 &edgefirst_bench::testdata::read("camera720p.rgba"),
3885 )?;
3886 compare_images_convert_to_rgb(&planar, &rbga, 0.98, function!());
3887
3888 Ok(())
3889 }
3890
3891 #[test]
3892 fn test_from_tensor_invalid_format() {
3893 assert!(PixelFormat::from_fourcc(u32::from_le_bytes(*b"TEST")).is_none());
3896 }
3897
3898 #[test]
3899 #[should_panic(expected = "Failed to save planar RGB image")]
3900 fn test_save_planar() {
3901 let planar_img = load_bytes_to_tensor(
3902 1280,
3903 720,
3904 PixelFormat::PlanarRgb,
3905 None,
3906 &edgefirst_bench::testdata::read("camera720p.8bps"),
3907 )
3908 .unwrap();
3909
3910 let save_path = "/tmp/planar_rgb.jpg";
3911 crate::save_jpeg(&planar_img, save_path, 90).expect("Failed to save planar RGB image");
3912 }
3913
3914 #[test]
3915 #[should_panic(expected = "Failed to save YUYV image")]
3916 fn test_save_yuyv() {
3917 let planar_img = load_bytes_to_tensor(
3918 1280,
3919 720,
3920 PixelFormat::Yuyv,
3921 None,
3922 &edgefirst_bench::testdata::read("camera720p.yuyv"),
3923 )
3924 .unwrap();
3925
3926 let save_path = "/tmp/yuyv.jpg";
3927 crate::save_jpeg(&planar_img, save_path, 90).expect("Failed to save YUYV image");
3928 }
3929
3930 #[test]
3931 fn test_rotation_angle() {
3932 assert_eq!(Rotation::from_degrees_clockwise(0), Rotation::None);
3933 assert_eq!(Rotation::from_degrees_clockwise(90), Rotation::Clockwise90);
3934 assert_eq!(Rotation::from_degrees_clockwise(180), Rotation::Rotate180);
3935 assert_eq!(
3936 Rotation::from_degrees_clockwise(270),
3937 Rotation::CounterClockwise90
3938 );
3939 assert_eq!(Rotation::from_degrees_clockwise(360), Rotation::None);
3940 assert_eq!(Rotation::from_degrees_clockwise(450), Rotation::Clockwise90);
3941 assert_eq!(Rotation::from_degrees_clockwise(540), Rotation::Rotate180);
3942 assert_eq!(
3943 Rotation::from_degrees_clockwise(630),
3944 Rotation::CounterClockwise90
3945 );
3946 }
3947
3948 #[test]
3949 #[should_panic(expected = "rotation angle is not a multiple of 90")]
3950 fn test_rotation_angle_panic() {
3951 Rotation::from_degrees_clockwise(361);
3952 }
3953
3954 #[test]
3955 fn test_disable_env_var() -> Result<(), Error> {
3956 let _lock = acquire_env_lock();
3959
3960 let _guard = EnvGuard::snapshot(&[
3963 "EDGEFIRST_FORCE_BACKEND",
3964 "EDGEFIRST_DISABLE_GL",
3965 "EDGEFIRST_DISABLE_G2D",
3966 "EDGEFIRST_DISABLE_CPU",
3967 ]);
3968
3969 unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") };
3972
3973 #[cfg(target_os = "linux")]
3974 {
3975 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
3976 let converter = ImageProcessor::new()?;
3977 assert!(converter.g2d.is_none());
3978 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_G2D") };
3979 }
3980
3981 #[cfg(target_os = "linux")]
3982 #[cfg(feature = "opengl")]
3983 {
3984 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
3985 let converter = ImageProcessor::new()?;
3986 assert!(converter.opengl.is_none());
3987 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_GL") };
3988 }
3989
3990 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
3991 let converter = ImageProcessor::new()?;
3992 assert!(converter.cpu.is_none());
3993 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_CPU") };
3994
3995 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
3997 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
3998 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
3999 let mut converter = ImageProcessor::new()?;
4000
4001 let src = TensorDyn::image(
4002 1280,
4003 720,
4004 PixelFormat::Rgba,
4005 DType::U8,
4006 None,
4007 edgefirst_tensor::CpuAccess::ReadWrite,
4008 )?;
4009 let dst = TensorDyn::image(
4010 640,
4011 360,
4012 PixelFormat::Rgba,
4013 DType::U8,
4014 None,
4015 edgefirst_tensor::CpuAccess::ReadWrite,
4016 )?;
4017 let (result, _src, _dst) = convert_img(
4018 &mut converter,
4019 src,
4020 dst,
4021 Rotation::None,
4022 Flip::None,
4023 Crop::no_crop(),
4024 );
4025 assert!(matches!(result, Err(Error::NoConverter)));
4026 Ok(())
4028 }
4029
4030 #[test]
4031 fn test_unsupported_conversion() {
4032 let src = TensorDyn::image(
4033 1280,
4034 720,
4035 PixelFormat::Nv12,
4036 DType::U8,
4037 None,
4038 edgefirst_tensor::CpuAccess::ReadWrite,
4039 )
4040 .unwrap();
4041 let dst = TensorDyn::image(
4042 640,
4043 360,
4044 PixelFormat::Nv12,
4045 DType::U8,
4046 None,
4047 edgefirst_tensor::CpuAccess::ReadWrite,
4048 )
4049 .unwrap();
4050 let mut converter = ImageProcessor::new().unwrap();
4051 let (result, _src, _dst) = convert_img(
4052 &mut converter,
4053 src,
4054 dst,
4055 Rotation::None,
4056 Flip::None,
4057 Crop::no_crop(),
4058 );
4059 log::debug!("result: {:?}", result);
4060 assert!(matches!(
4061 result,
4062 Err(Error::NotSupported(e)) if e.starts_with("Conversion from NV12 to NV12")
4063 ));
4064 }
4065
4066 #[test]
4067 fn test_load_grey() {
4068 let grey_img = crate::load_image_test_helper(
4072 &edgefirst_bench::testdata::read("grey.jpg"),
4073 Some(PixelFormat::Rgba),
4074 None,
4075 )
4076 .unwrap();
4077 assert_eq!(grey_img.width(), Some(1024));
4078 assert_eq!(grey_img.height(), Some(681));
4079
4080 let grey_but_rgb = crate::load_image_test_helper(
4086 &edgefirst_bench::testdata::read("grey-rgb.jpg"),
4087 Some(PixelFormat::Rgba),
4088 None,
4089 )
4090 .expect("odd-height colour JPEG should decode to NV12 and convert to RGBA");
4091 assert_eq!(grey_but_rgb.width(), Some(1024));
4092 assert_eq!(grey_but_rgb.height(), Some(681));
4093 }
4094
4095 #[test]
4096 fn test_new_nv12() {
4097 let nv12 = TensorDyn::image(
4098 1280,
4099 720,
4100 PixelFormat::Nv12,
4101 DType::U8,
4102 None,
4103 edgefirst_tensor::CpuAccess::ReadWrite,
4104 )
4105 .unwrap();
4106 assert_eq!(nv12.height(), Some(720));
4107 assert_eq!(nv12.width(), Some(1280));
4108 assert_eq!(nv12.format().unwrap(), PixelFormat::Nv12);
4109 assert_eq!(nv12.format().unwrap().channels(), 1);
4111 assert!(nv12.format().is_some_and(
4112 |f| f.layout() == PixelLayout::Planar || f.layout() == PixelLayout::SemiPlanar
4113 ))
4114 }
4115
4116 #[test]
4117 #[cfg(target_os = "linux")]
4118 fn test_new_image_converter() {
4119 let dst_width = 640;
4120 let dst_height = 360;
4121 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4122 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4123
4124 let mut converter = ImageProcessor::new().unwrap();
4125 let converter_dst = converter
4126 .create_image(
4127 dst_width,
4128 dst_height,
4129 PixelFormat::Rgba,
4130 DType::U8,
4131 None,
4132 edgefirst_tensor::CpuAccess::ReadWrite,
4133 )
4134 .unwrap();
4135 let (result, src, converter_dst) = convert_img(
4136 &mut converter,
4137 src,
4138 converter_dst,
4139 Rotation::None,
4140 Flip::None,
4141 Crop::no_crop(),
4142 );
4143 result.unwrap();
4144
4145 let cpu_dst = TensorDyn::image(
4146 dst_width,
4147 dst_height,
4148 PixelFormat::Rgba,
4149 DType::U8,
4150 None,
4151 edgefirst_tensor::CpuAccess::ReadWrite,
4152 )
4153 .unwrap();
4154 let mut cpu_converter = CPUProcessor::new();
4155 let (result, _src, cpu_dst) = convert_img(
4156 &mut cpu_converter,
4157 src,
4158 cpu_dst,
4159 Rotation::None,
4160 Flip::None,
4161 Crop::no_crop(),
4162 );
4163 result.unwrap();
4164
4165 compare_images(&converter_dst, &cpu_dst, 0.98, function!());
4166 }
4167
4168 #[test]
4169 #[cfg(target_os = "linux")]
4170 fn test_create_image_dtype_i8() {
4171 let mut converter = ImageProcessor::new().unwrap();
4172
4173 let dst = converter
4175 .create_image(
4176 320,
4177 240,
4178 PixelFormat::Rgb,
4179 DType::I8,
4180 None,
4181 edgefirst_tensor::CpuAccess::ReadWrite,
4182 )
4183 .unwrap();
4184 assert_eq!(dst.dtype(), DType::I8);
4185 assert!(dst.width() == Some(320));
4186 assert!(dst.height() == Some(240));
4187 assert_eq!(dst.format(), Some(PixelFormat::Rgb));
4188
4189 let dst_u8 = converter
4191 .create_image(
4192 320,
4193 240,
4194 PixelFormat::Rgb,
4195 DType::U8,
4196 None,
4197 edgefirst_tensor::CpuAccess::ReadWrite,
4198 )
4199 .unwrap();
4200 assert_eq!(dst_u8.dtype(), DType::U8);
4201
4202 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4204 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4205 let mut dst_i8 = converter
4206 .create_image(
4207 320,
4208 240,
4209 PixelFormat::Rgb,
4210 DType::I8,
4211 None,
4212 edgefirst_tensor::CpuAccess::ReadWrite,
4213 )
4214 .unwrap();
4215 converter
4216 .convert(
4217 &src,
4218 &mut dst_i8,
4219 Rotation::None,
4220 Flip::None,
4221 Crop::no_crop(),
4222 )
4223 .unwrap();
4224 }
4225
4226 #[test]
4227 #[cfg(target_os = "linux")]
4228 fn test_create_image_nv12_dma_non_aligned_width() {
4229 let converter = ImageProcessor::new().unwrap();
4235
4236 let result = converter.create_image(
4238 100,
4239 64,
4240 PixelFormat::Nv12,
4241 DType::U8,
4242 Some(TensorMemory::Dma),
4243 edgefirst_tensor::CpuAccess::ReadWrite,
4244 );
4245
4246 match result {
4247 Ok(img) => {
4248 assert_eq!(img.width(), Some(100));
4249 assert_eq!(img.height(), Some(64));
4250 assert_eq!(img.format(), Some(PixelFormat::Nv12));
4251 if let Some(stride) = img.row_stride() {
4252 assert!(
4253 stride >= 100,
4254 "NV12 row_stride {stride} must be >= the logical width (100)",
4255 );
4256 }
4257 }
4258 Err(e) => {
4259 eprintln!("SKIPPED: create_image NV12 DMA non-aligned width: {e}");
4261 }
4262 }
4263 }
4264
4265 #[test]
4266 #[ignore] fn test_crop_skip() {
4270 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4271 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4272
4273 let mut converter = ImageProcessor::new().unwrap();
4274 let converter_dst = converter
4275 .create_image(
4276 1280,
4277 720,
4278 PixelFormat::Rgba,
4279 DType::U8,
4280 None,
4281 edgefirst_tensor::CpuAccess::ReadWrite,
4282 )
4283 .unwrap();
4284 let crop = Crop::new().with_source(Some(Region::new(0, 0, 640, 640)));
4285 let (result, src, converter_dst) = convert_img(
4286 &mut converter,
4287 src,
4288 converter_dst,
4289 Rotation::None,
4290 Flip::None,
4291 crop,
4292 );
4293 result.unwrap();
4294
4295 let cpu_dst = TensorDyn::image(
4296 1280,
4297 720,
4298 PixelFormat::Rgba,
4299 DType::U8,
4300 None,
4301 edgefirst_tensor::CpuAccess::ReadWrite,
4302 )
4303 .unwrap();
4304 let mut cpu_converter = CPUProcessor::new();
4305 let (result, _src, cpu_dst) = convert_img(
4306 &mut cpu_converter,
4307 src,
4308 cpu_dst,
4309 Rotation::None,
4310 Flip::None,
4311 crop,
4312 );
4313 result.unwrap();
4314
4315 compare_images(&converter_dst, &cpu_dst, 0.99999, function!());
4316 }
4317
4318 #[test]
4319 fn test_invalid_pixel_format() {
4320 assert!(PixelFormat::from_fourcc(u32::from_le_bytes(*b"TEST")).is_none());
4323 }
4324
4325 #[cfg(target_os = "linux")]
4327 static G2D_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4328
4329 #[cfg(target_os = "linux")]
4330 fn is_g2d_available() -> bool {
4331 *G2D_AVAILABLE.get_or_init(|| G2DProcessor::new().is_ok())
4332 }
4333
4334 #[cfg(target_os = "linux")]
4335 #[cfg(feature = "opengl")]
4336 static GL_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4337
4338 #[cfg(target_os = "linux")]
4339 #[cfg(feature = "opengl")]
4340 fn is_opengl_available() -> bool {
4342 #[cfg(all(target_os = "linux", feature = "opengl"))]
4343 {
4344 *GL_AVAILABLE.get_or_init(|| GLProcessorThreaded::new(None).is_ok())
4345 }
4346
4347 #[cfg(not(all(target_os = "linux", feature = "opengl")))]
4348 {
4349 false
4350 }
4351 }
4352
4353 #[test]
4365 #[cfg(feature = "opengl")]
4366 fn gl_backend_available_canary() {
4367 let require_gl = std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1");
4368 if !require_gl {
4369 eprintln!(
4370 "SKIPPED: {} — HAL_TEST_REQUIRE_GL is not set to 1",
4371 function!()
4372 );
4373 return;
4374 }
4375 #[cfg(target_os = "macos")]
4376 if std::env::var_os("HAL_TEST_ALLOW_DLOPEN_ANGLE").is_none() {
4377 eprintln!(
4378 "SKIPPED: {} — ANGLE dlopen gate closed (coverage pass 1)",
4379 function!()
4380 );
4381 return;
4382 }
4383 GLProcessorThreaded::new(None).expect(
4384 "HAL_TEST_REQUIRE_GL=1 but the GL backend failed to initialize — \
4385 check the ANGLE install/re-sign step and binary entitlements \
4386 (macOS) or the EGL stack (Linux)",
4387 );
4388 }
4389
4390 #[test]
4391 fn test_load_jpeg_with_exif() {
4392 use edgefirst_codec::peek_info;
4393
4394 let file = edgefirst_bench::testdata::read("zidane_rotated_exif.jpg").to_vec();
4399 let info = peek_info(&file).unwrap();
4400 assert_eq!(info.rotation_degrees, 90);
4401 assert!(!info.flip_horizontal);
4402
4403 let loaded = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4404 assert_eq!(loaded.width(), Some(1280));
4406 assert_eq!(loaded.height(), Some(720));
4407
4408 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4411 let cpu_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4412
4413 let rotation = Rotation::from_degrees_clockwise(info.rotation_degrees as usize);
4414 let (dst_width, dst_height) = (cpu_src.height().unwrap(), cpu_src.width().unwrap());
4415
4416 let cpu_dst = TensorDyn::image(
4417 dst_width,
4418 dst_height,
4419 PixelFormat::Rgba,
4420 DType::U8,
4421 None,
4422 edgefirst_tensor::CpuAccess::ReadWrite,
4423 )
4424 .unwrap();
4425 let mut cpu_converter = CPUProcessor::new();
4426
4427 let loaded_rotated = TensorDyn::image(
4430 dst_width,
4431 dst_height,
4432 PixelFormat::Rgba,
4433 DType::U8,
4434 None,
4435 edgefirst_tensor::CpuAccess::ReadWrite,
4436 )
4437 .unwrap();
4438 let (r0, _loaded, loaded_rotated) = convert_img(
4439 &mut cpu_converter,
4440 loaded,
4441 loaded_rotated,
4442 rotation,
4443 Flip::None,
4444 Crop::no_crop(),
4445 );
4446 r0.unwrap();
4447
4448 let (result, _cpu_src, cpu_dst) = convert_img(
4449 &mut cpu_converter,
4450 cpu_src,
4451 cpu_dst,
4452 rotation,
4453 Flip::None,
4454 Crop::no_crop(),
4455 );
4456 result.unwrap();
4457
4458 compare_images(&loaded_rotated, &cpu_dst, 0.98, function!());
4459 }
4460
4461 #[test]
4462 fn test_load_png_with_exif() {
4463 use edgefirst_codec::peek_info;
4464
4465 let file = edgefirst_bench::testdata::read("zidane_rotated_exif_180.png").to_vec();
4468 let info = peek_info(&file).unwrap();
4469 assert_eq!(info.rotation_degrees, 180);
4470 assert!(!info.flip_horizontal);
4471
4472 let loaded = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4473 assert_eq!(loaded.height(), Some(720));
4475 assert_eq!(loaded.width(), Some(1280));
4476
4477 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4482 let cpu_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4483
4484 let rotation = Rotation::from_degrees_clockwise(info.rotation_degrees as usize);
4485 let cpu_dst = TensorDyn::image(
4486 1280,
4487 720,
4488 PixelFormat::Rgba,
4489 DType::U8,
4490 None,
4491 edgefirst_tensor::CpuAccess::ReadWrite,
4492 )
4493 .unwrap();
4494 let mut cpu_converter = CPUProcessor::new();
4495
4496 let (result, _cpu_src, cpu_dst) = convert_img(
4497 &mut cpu_converter,
4498 cpu_src,
4499 cpu_dst,
4500 rotation,
4501 Flip::None,
4502 Crop::no_crop(),
4503 );
4504 result.unwrap();
4505
4506 let loaded_rotated = TensorDyn::image(
4509 1280,
4510 720,
4511 PixelFormat::Rgba,
4512 DType::U8,
4513 None,
4514 edgefirst_tensor::CpuAccess::ReadWrite,
4515 )
4516 .unwrap();
4517 let (r0, _loaded, loaded_rotated) = convert_img(
4518 &mut cpu_converter,
4519 loaded,
4520 loaded_rotated,
4521 rotation,
4522 Flip::None,
4523 Crop::no_crop(),
4524 );
4525 r0.unwrap();
4526
4527 compare_images(&loaded_rotated, &cpu_dst, 0.95, function!());
4532 }
4533
4534 #[cfg(target_os = "linux")]
4540 fn make_rgb_jpeg(width: u32, height: u32) -> Vec<u8> {
4541 let mut bytes = Vec::with_capacity((width * height * 3) as usize);
4542 for y in 0..height {
4543 for x in 0..width {
4544 bytes.push(((x + y) & 0xFF) as u8);
4545 bytes.push(((x.wrapping_mul(3)) & 0xFF) as u8);
4546 bytes.push(((y.wrapping_mul(5)) & 0xFF) as u8);
4547 }
4548 }
4549 let mut out = Vec::new();
4550 let encoder = jpeg_encoder::Encoder::new(&mut out, 85);
4551 encoder
4552 .encode(
4553 &bytes,
4554 width as u16,
4555 height as u16,
4556 jpeg_encoder::ColorType::Rgb,
4557 )
4558 .expect("jpeg-encoder must succeed on trivial input");
4559 out
4560 }
4561
4562 #[test]
4571 #[cfg(target_os = "linux")]
4572 #[cfg(feature = "opengl")]
4573 fn test_convert_rgba_non_4_aligned_width_end_to_end() {
4574 use edgefirst_tensor::is_dma_available;
4575 if !is_dma_available() {
4576 eprintln!(
4577 "SKIPPED: test_convert_rgba_non_4_aligned_width_end_to_end — DMA not available"
4578 );
4579 return;
4580 }
4581 let jpeg = make_rgb_jpeg(375, 333);
4585 let src_gl = crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), None).unwrap();
4586 assert_eq!(src_gl.width(), Some(375));
4587 let stride = src_gl.row_stride().unwrap();
4589 assert_eq!(stride, 1536, "expected padded pitch 1536, got {stride}");
4590
4591 let mut gl_proc = ImageProcessor::new().unwrap();
4593 let gl_dst = gl_proc
4594 .create_image(
4595 640,
4596 640,
4597 PixelFormat::Rgba,
4598 DType::U8,
4599 None,
4600 edgefirst_tensor::CpuAccess::ReadWrite,
4601 )
4602 .unwrap();
4603 let (r_gl, _src_gl, gl_dst) = convert_img(
4604 &mut gl_proc,
4605 src_gl,
4606 gl_dst,
4607 Rotation::None,
4608 Flip::None,
4609 Crop::no_crop(),
4610 );
4611 r_gl.expect("GL-backed convert must succeed for 375x333 Rgba src");
4612
4613 let src_cpu =
4618 crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), Some(TensorMemory::Mem))
4619 .unwrap();
4620 let mut cpu_proc = ImageProcessor::with_config(ImageProcessorConfig {
4621 backend: ComputeBackend::Cpu,
4622 ..Default::default()
4623 })
4624 .unwrap();
4625 let cpu_dst = TensorDyn::image(
4626 640,
4627 640,
4628 PixelFormat::Rgba,
4629 DType::U8,
4630 Some(TensorMemory::Mem),
4631 edgefirst_tensor::CpuAccess::ReadWrite,
4632 )
4633 .unwrap();
4634 let (r_cpu, _src_cpu, cpu_dst) = convert_img(
4635 &mut cpu_proc,
4636 src_cpu,
4637 cpu_dst,
4638 Rotation::None,
4639 Flip::None,
4640 Crop::no_crop(),
4641 );
4642 r_cpu.unwrap();
4643
4644 compare_images(&gl_dst, &cpu_dst, 0.95, function!());
4648 }
4649
4650 #[test]
4657 #[cfg(target_os = "linux")]
4658 fn test_load_jpeg_rgba_non_aligned_pitch_padded_dma() {
4659 use edgefirst_tensor::is_dma_available;
4660 if !is_dma_available() {
4661 eprintln!(
4662 "SKIPPED: test_load_jpeg_rgba_non_aligned_pitch_padded_dma — DMA not available"
4663 );
4664 return;
4665 }
4666 for &w in &[500u32, 612, 428] {
4670 let jpeg = make_rgb_jpeg(w, 333);
4671 let loaded =
4672 crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), None).unwrap();
4673 let natural = (w as usize) * 4;
4674 let aligned = crate::align_pitch_bytes_to_gpu_alignment(natural).unwrap();
4675 assert!(
4676 aligned > natural,
4677 "test sanity: width {w} should be unaligned"
4678 );
4679 let stride = loaded
4680 .row_stride()
4681 .expect("padded DMA path must set an explicit row_stride — regression if None");
4682 assert_eq!(
4683 stride, aligned,
4684 "width {w}: expected padded stride {aligned}, got {stride} \
4685 (regression: pitch-padding branch skipped?)"
4686 );
4687 let eff = loaded.effective_row_stride().unwrap();
4688 assert_eq!(
4689 eff, aligned,
4690 "effective_row_stride must match stored stride"
4691 );
4692 assert_eq!(loaded.width(), Some(w as usize));
4693 assert_eq!(loaded.height(), Some(333));
4694 }
4695 }
4696
4697 #[test]
4706 #[cfg(target_os = "linux")]
4707 fn test_padded_dma_pitch_for_respects_memory_choice() {
4708 use edgefirst_tensor::{is_dma_available, TensorMemory};
4709
4710 let unaligned_w = 500;
4713
4714 assert_eq!(
4716 crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &Some(TensorMemory::Mem),),
4717 None,
4718 "Mem must never trigger DMA padding"
4719 );
4720 assert_eq!(
4721 crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &Some(TensorMemory::Shm),),
4722 None,
4723 "Shm must never trigger DMA padding"
4724 );
4725
4726 assert_eq!(
4731 crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &Some(TensorMemory::Dma),),
4732 Some(2048),
4733 "explicit Dma must pad regardless of runtime DMA availability"
4734 );
4735
4736 let none_result = crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &None);
4740 if is_dma_available() {
4741 assert_eq!(
4742 none_result,
4743 Some(2048),
4744 "memory=None + DMA available → pad (will route through DMA)"
4745 );
4746 } else {
4747 assert_eq!(
4748 none_result, None,
4749 "memory=None + DMA unavailable → must NOT pad (would force \
4750 image_with_stride into a DMA-only allocation that fails). \
4751 Regression: padded_dma_pitch_for ignored is_dma_available()."
4752 );
4753 }
4754 }
4755
4756 fn make_grey_png(width: u32, height: u32) -> Vec<u8> {
4760 let mut bytes = Vec::with_capacity((width * height) as usize);
4761 for y in 0..height {
4762 for x in 0..width {
4763 bytes.push(((x + y) & 0xFF) as u8);
4764 }
4765 }
4766 let img = image::GrayImage::from_vec(width, height, bytes).unwrap();
4767 let mut buf = Vec::new();
4768 img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)
4769 .unwrap();
4770 buf
4771 }
4772
4773 #[test]
4778 #[cfg(target_os = "linux")]
4779 fn test_load_png_grey_misaligned_width_dma() {
4780 use edgefirst_tensor::is_dma_available;
4781 if !is_dma_available() {
4782 eprintln!("SKIPPED: test_load_png_grey_misaligned_width_dma — DMA not available");
4783 return;
4784 }
4785 let png = make_grey_png(612, 388);
4786 let loaded = crate::load_image_test_helper(&png, Some(PixelFormat::Grey), None).unwrap();
4787 assert_eq!(loaded.width(), Some(612));
4788 assert_eq!(loaded.height(), Some(388));
4789 assert_eq!(loaded.format(), Some(PixelFormat::Grey));
4790
4791 let map = loaded.as_u8().unwrap().map().unwrap();
4794 let stride = loaded.row_stride().unwrap_or(612);
4795 assert!(stride >= 612);
4796 let bytes: &[u8] = ↦
4797 for y in 0..388usize {
4798 for x in 0..612usize {
4799 let expected = ((x + y) & 0xFF) as u8;
4800 let got = bytes[y * stride + x];
4801 assert_eq!(
4802 got, expected,
4803 "grey png mismatch at ({x},{y}): got {got} expected {expected}"
4804 );
4805 }
4806 }
4807 }
4808
4809 #[test]
4813 fn test_load_png_grey_mem() {
4814 use edgefirst_tensor::TensorMemory;
4815 let png = make_grey_png(612, 100);
4816 let loaded =
4817 crate::load_image_test_helper(&png, Some(PixelFormat::Grey), Some(TensorMemory::Mem))
4818 .unwrap();
4819 assert_eq!(loaded.width(), Some(612));
4820 assert_eq!(loaded.height(), Some(100));
4821 assert_eq!(loaded.format(), Some(PixelFormat::Grey));
4822 let map = loaded.as_u8().unwrap().map().unwrap();
4823 let bytes: &[u8] = ↦
4824 assert_eq!(bytes.len(), 612 * 100);
4826 for y in 0..100 {
4827 for x in 0..612 {
4828 assert_eq!(bytes[y * 612 + x], ((x + y) & 0xFF) as u8);
4829 }
4830 }
4831 }
4832
4833 #[test]
4837 fn test_load_png_grey_to_rgb_mem() {
4838 use edgefirst_tensor::TensorMemory;
4839 let png = make_grey_png(620, 240);
4840 let loaded =
4841 crate::load_image_test_helper(&png, Some(PixelFormat::Rgb), Some(TensorMemory::Mem))
4842 .unwrap();
4843 assert_eq!(loaded.width(), Some(620));
4844 assert_eq!(loaded.height(), Some(240));
4845 assert_eq!(loaded.format(), Some(PixelFormat::Rgb));
4846
4847 let map = loaded.as_u8().unwrap().map().unwrap();
4849 let bytes: &[u8] = ↦
4850 for (x, y) in [(0usize, 0usize), (100, 50), (619, 239)] {
4851 let expected = ((x + y) & 0xFF) as u8;
4852 let off = (y * 620 + x) * 3;
4853 assert_eq!(bytes[off], expected, "R@{x},{y}");
4854 assert_eq!(bytes[off + 1], expected, "G@{x},{y}");
4855 assert_eq!(bytes[off + 2], expected, "B@{x},{y}");
4856 }
4857 }
4858
4859 #[test]
4860 #[cfg(target_os = "linux")]
4861 fn test_g2d_resize() {
4862 if !is_g2d_available() {
4863 eprintln!("SKIPPED: test_g2d_resize - G2D library (libg2d.so.2) not available");
4864 return;
4865 }
4866 if !is_dma_available() {
4867 eprintln!(
4868 "SKIPPED: test_g2d_resize - DMA memory allocation not available (permission denied or no DMA-BUF support)"
4869 );
4870 return;
4871 }
4872
4873 let dst_width = 640;
4874 let dst_height = 360;
4875 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4876 let src =
4877 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), Some(TensorMemory::Dma))
4878 .unwrap();
4879
4880 let g2d_dst = TensorDyn::image(
4881 dst_width,
4882 dst_height,
4883 PixelFormat::Rgba,
4884 DType::U8,
4885 Some(TensorMemory::Dma),
4886 edgefirst_tensor::CpuAccess::ReadWrite,
4887 )
4888 .unwrap();
4889 let mut g2d_converter = G2DProcessor::new().unwrap();
4890 let (result, src, g2d_dst) = convert_img(
4891 &mut g2d_converter,
4892 src,
4893 g2d_dst,
4894 Rotation::None,
4895 Flip::None,
4896 Crop::no_crop(),
4897 );
4898 result.unwrap();
4899
4900 let cpu_dst = TensorDyn::image(
4901 dst_width,
4902 dst_height,
4903 PixelFormat::Rgba,
4904 DType::U8,
4905 None,
4906 edgefirst_tensor::CpuAccess::ReadWrite,
4907 )
4908 .unwrap();
4909 let mut cpu_converter = CPUProcessor::new();
4910 let (result, _src, cpu_dst) = convert_img(
4911 &mut cpu_converter,
4912 src,
4913 cpu_dst,
4914 Rotation::None,
4915 Flip::None,
4916 Crop::no_crop(),
4917 );
4918 result.unwrap();
4919
4920 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
4926 }
4927
4928 #[test]
4929 #[cfg(target_os = "linux")]
4930 #[cfg(feature = "opengl")]
4931 fn test_opengl_resize() {
4932 if !is_opengl_available() {
4933 eprintln!("SKIPPED: {} - OpenGL not available", function!());
4934 return;
4935 }
4936
4937 let dst_width = 640;
4938 let dst_height = 360;
4939 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4940 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4941
4942 let cpu_dst = TensorDyn::image(
4943 dst_width,
4944 dst_height,
4945 PixelFormat::Rgba,
4946 DType::U8,
4947 None,
4948 edgefirst_tensor::CpuAccess::ReadWrite,
4949 )
4950 .unwrap();
4951 let mut cpu_converter = CPUProcessor::new();
4952 let (result, src, cpu_dst) = convert_img(
4953 &mut cpu_converter,
4954 src,
4955 cpu_dst,
4956 Rotation::None,
4957 Flip::None,
4958 Crop::no_crop(),
4959 );
4960 result.unwrap();
4961
4962 let mut src = src;
4963 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
4964
4965 for _ in 0..5 {
4966 let gl_dst = TensorDyn::image(
4967 dst_width,
4968 dst_height,
4969 PixelFormat::Rgba,
4970 DType::U8,
4971 None,
4972 edgefirst_tensor::CpuAccess::ReadWrite,
4973 )
4974 .unwrap();
4975 let (result, src_back, gl_dst) = convert_img(
4976 &mut gl_converter,
4977 src,
4978 gl_dst,
4979 Rotation::None,
4980 Flip::None,
4981 Crop::no_crop(),
4982 );
4983 result.unwrap();
4984 src = src_back;
4985
4986 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
4987 }
4988 }
4989
4990 #[test]
4991 #[cfg(target_os = "linux")]
4992 #[cfg(feature = "opengl")]
4993 fn test_opengl_10_threads() {
4994 if !is_opengl_available() {
4995 eprintln!("SKIPPED: {} - OpenGL not available", function!());
4996 return;
4997 }
4998
4999 let handles: Vec<_> = (0..10)
5000 .map(|i| {
5001 std::thread::Builder::new()
5002 .name(format!("Thread {i}"))
5003 .spawn(test_opengl_resize)
5004 .unwrap()
5005 })
5006 .collect();
5007 handles.into_iter().for_each(|h| {
5008 if let Err(e) = h.join() {
5009 std::panic::resume_unwind(e)
5010 }
5011 });
5012 }
5013
5014 #[test]
5015 #[cfg(target_os = "linux")]
5016 #[cfg(feature = "opengl")]
5017 fn test_opengl_grey() {
5018 if !is_opengl_available() {
5019 eprintln!("SKIPPED: {} - OpenGL not available", function!());
5020 return;
5021 }
5022
5023 let img = crate::load_image_test_helper(
5024 &edgefirst_bench::testdata::read("grey.jpg"),
5025 Some(PixelFormat::Grey),
5026 None,
5027 )
5028 .unwrap();
5029
5030 let gl_dst = TensorDyn::image(
5031 640,
5032 640,
5033 PixelFormat::Grey,
5034 DType::U8,
5035 None,
5036 edgefirst_tensor::CpuAccess::ReadWrite,
5037 )
5038 .unwrap();
5039 let cpu_dst = TensorDyn::image(
5040 640,
5041 640,
5042 PixelFormat::Grey,
5043 DType::U8,
5044 None,
5045 edgefirst_tensor::CpuAccess::ReadWrite,
5046 )
5047 .unwrap();
5048
5049 let mut converter = CPUProcessor::new();
5050
5051 let (result, img, cpu_dst) = convert_img(
5052 &mut converter,
5053 img,
5054 cpu_dst,
5055 Rotation::None,
5056 Flip::None,
5057 Crop::no_crop(),
5058 );
5059 result.unwrap();
5060
5061 let mut gl = GLProcessorThreaded::new(None).unwrap();
5062 let (result, _img, gl_dst) = convert_img(
5063 &mut gl,
5064 img,
5065 gl_dst,
5066 Rotation::None,
5067 Flip::None,
5068 Crop::no_crop(),
5069 );
5070 result.unwrap();
5071
5072 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5073 }
5074
5075 #[test]
5076 #[cfg(target_os = "linux")]
5077 fn test_g2d_src_crop() {
5078 if !is_g2d_available() {
5079 eprintln!("SKIPPED: test_g2d_src_crop - G2D library (libg2d.so.2) not available");
5080 return;
5081 }
5082 if !is_dma_available() {
5083 eprintln!(
5084 "SKIPPED: test_g2d_src_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)"
5085 );
5086 return;
5087 }
5088
5089 let dst_width = 640;
5090 let dst_height = 640;
5091 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5092 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5093
5094 let cpu_dst = TensorDyn::image(
5095 dst_width,
5096 dst_height,
5097 PixelFormat::Rgba,
5098 DType::U8,
5099 None,
5100 edgefirst_tensor::CpuAccess::ReadWrite,
5101 )
5102 .unwrap();
5103 let mut cpu_converter = CPUProcessor::new();
5104 let crop = Crop::new().with_source(Some(Region::new(0, 0, 640, 360)));
5105 let (result, src, cpu_dst) = convert_img(
5106 &mut cpu_converter,
5107 src,
5108 cpu_dst,
5109 Rotation::None,
5110 Flip::None,
5111 crop,
5112 );
5113 result.unwrap();
5114
5115 let g2d_dst = TensorDyn::image(
5116 dst_width,
5117 dst_height,
5118 PixelFormat::Rgba,
5119 DType::U8,
5120 None,
5121 edgefirst_tensor::CpuAccess::ReadWrite,
5122 )
5123 .unwrap();
5124 let mut g2d_converter = G2DProcessor::new().unwrap();
5125 let (result, _src, g2d_dst) = convert_img(
5126 &mut g2d_converter,
5127 src,
5128 g2d_dst,
5129 Rotation::None,
5130 Flip::None,
5131 crop,
5132 );
5133 result.unwrap();
5134
5135 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
5141 }
5142
5143 #[test]
5144 #[cfg(target_os = "linux")]
5145 fn test_g2d_dst_crop() {
5146 if !is_g2d_available() {
5147 eprintln!("SKIPPED: test_g2d_dst_crop - G2D library (libg2d.so.2) not available");
5148 return;
5149 }
5150 if !is_dma_available() {
5151 eprintln!(
5152 "SKIPPED: test_g2d_dst_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)"
5153 );
5154 return;
5155 }
5156
5157 let dst_width = 640;
5158 let dst_height = 640;
5159 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5160 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5161
5162 let cpu_dst = TensorDyn::image(
5163 dst_width,
5164 dst_height,
5165 PixelFormat::Rgba,
5166 DType::U8,
5167 None,
5168 edgefirst_tensor::CpuAccess::ReadWrite,
5169 )
5170 .unwrap();
5171 let mut cpu_converter = CPUProcessor::new();
5172 let crop = Crop::new();
5173 let (result, src, cpu_dst) = convert_img(
5174 &mut cpu_converter,
5175 src,
5176 cpu_dst,
5177 Rotation::None,
5178 Flip::None,
5179 crop,
5180 );
5181 result.unwrap();
5182
5183 let g2d_dst = TensorDyn::image(
5184 dst_width,
5185 dst_height,
5186 PixelFormat::Rgba,
5187 DType::U8,
5188 None,
5189 edgefirst_tensor::CpuAccess::ReadWrite,
5190 )
5191 .unwrap();
5192 let mut g2d_converter = G2DProcessor::new().unwrap();
5193 let (result, _src, g2d_dst) = convert_img(
5194 &mut g2d_converter,
5195 src,
5196 g2d_dst,
5197 Rotation::None,
5198 Flip::None,
5199 crop,
5200 );
5201 result.unwrap();
5202
5203 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
5209 }
5210
5211 #[test]
5212 #[cfg(target_os = "linux")]
5213 fn test_g2d_all_rgba() {
5214 if !is_g2d_available() {
5215 eprintln!("SKIPPED: test_g2d_all_rgba - G2D library (libg2d.so.2) not available");
5216 return;
5217 }
5218 if !is_dma_available() {
5219 eprintln!(
5220 "SKIPPED: test_g2d_all_rgba - DMA memory allocation not available (permission denied or no DMA-BUF support)"
5221 );
5222 return;
5223 }
5224
5225 let dst_width = 640;
5226 let dst_height = 640;
5227 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5228 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5229 let src_dyn = src;
5230
5231 let mut cpu_dst = TensorDyn::image(
5232 dst_width,
5233 dst_height,
5234 PixelFormat::Rgba,
5235 DType::U8,
5236 None,
5237 edgefirst_tensor::CpuAccess::ReadWrite,
5238 )
5239 .unwrap();
5240 let mut cpu_converter = CPUProcessor::new();
5241 let mut g2d_dst = TensorDyn::image(
5242 dst_width,
5243 dst_height,
5244 PixelFormat::Rgba,
5245 DType::U8,
5246 None,
5247 edgefirst_tensor::CpuAccess::ReadWrite,
5248 )
5249 .unwrap();
5250 let mut g2d_converter = G2DProcessor::new().unwrap();
5251
5252 let crop = Crop::new().with_source(Some(Region::new(50, 120, 1024, 576)));
5253
5254 for rot in [
5255 Rotation::None,
5256 Rotation::Clockwise90,
5257 Rotation::Rotate180,
5258 Rotation::CounterClockwise90,
5259 ] {
5260 cpu_dst
5261 .as_u8()
5262 .unwrap()
5263 .map()
5264 .unwrap()
5265 .as_mut_slice()
5266 .fill(114);
5267 g2d_dst
5268 .as_u8()
5269 .unwrap()
5270 .map()
5271 .unwrap()
5272 .as_mut_slice()
5273 .fill(114);
5274 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
5275 let mut cpu_dst_dyn = cpu_dst;
5276 cpu_converter
5277 .convert(&src_dyn, &mut cpu_dst_dyn, Rotation::None, Flip::None, crop)
5278 .unwrap();
5279 cpu_dst = {
5280 let mut __t = cpu_dst_dyn.into_u8().unwrap();
5281 __t.set_format(PixelFormat::Rgba).unwrap();
5282 TensorDyn::from(__t)
5283 };
5284
5285 let mut g2d_dst_dyn = g2d_dst;
5286 g2d_converter
5287 .convert(&src_dyn, &mut g2d_dst_dyn, Rotation::None, Flip::None, crop)
5288 .unwrap();
5289 g2d_dst = {
5290 let mut __t = g2d_dst_dyn.into_u8().unwrap();
5291 __t.set_format(PixelFormat::Rgba).unwrap();
5292 TensorDyn::from(__t)
5293 };
5294
5295 compare_images(
5296 &g2d_dst,
5297 &cpu_dst,
5298 0.98,
5299 &format!("{} {:?} {:?}", function!(), rot, flip),
5300 );
5301 }
5302 }
5303 }
5304
5305 #[test]
5306 #[cfg(target_os = "linux")]
5307 #[cfg(feature = "opengl")]
5308 fn test_opengl_src_crop() {
5309 if !is_opengl_available() {
5310 eprintln!("SKIPPED: {} - OpenGL not available", function!());
5311 return;
5312 }
5313
5314 let dst_width = 640;
5315 let dst_height = 360;
5316 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5317 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5318 let crop = Crop::new().with_source(Some(Region::new(320, 180, 1280 - 320, 720 - 180)));
5319
5320 let cpu_dst = TensorDyn::image(
5321 dst_width,
5322 dst_height,
5323 PixelFormat::Rgba,
5324 DType::U8,
5325 None,
5326 edgefirst_tensor::CpuAccess::ReadWrite,
5327 )
5328 .unwrap();
5329 let mut cpu_converter = CPUProcessor::new();
5330 let (result, src, cpu_dst) = convert_img(
5331 &mut cpu_converter,
5332 src,
5333 cpu_dst,
5334 Rotation::None,
5335 Flip::None,
5336 crop,
5337 );
5338 result.unwrap();
5339
5340 let gl_dst = TensorDyn::image(
5341 dst_width,
5342 dst_height,
5343 PixelFormat::Rgba,
5344 DType::U8,
5345 None,
5346 edgefirst_tensor::CpuAccess::ReadWrite,
5347 )
5348 .unwrap();
5349 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5350 let (result, _src, gl_dst) = convert_img(
5351 &mut gl_converter,
5352 src,
5353 gl_dst,
5354 Rotation::None,
5355 Flip::None,
5356 crop,
5357 );
5358 result.unwrap();
5359
5360 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5361 }
5362
5363 #[test]
5364 #[cfg(target_os = "linux")]
5365 #[cfg(feature = "opengl")]
5366 fn test_opengl_dst_crop() {
5367 if !is_opengl_available() {
5368 eprintln!("SKIPPED: {} - OpenGL not available", function!());
5369 return;
5370 }
5371
5372 let dst_width = 640;
5373 let dst_height = 640;
5374 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5375 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5376
5377 let cpu_dst = TensorDyn::image(
5378 dst_width,
5379 dst_height,
5380 PixelFormat::Rgba,
5381 DType::U8,
5382 None,
5383 edgefirst_tensor::CpuAccess::ReadWrite,
5384 )
5385 .unwrap();
5386 let mut cpu_converter = CPUProcessor::new();
5387 let crop = Crop::new();
5388 let (result, src, cpu_dst) = convert_img(
5389 &mut cpu_converter,
5390 src,
5391 cpu_dst,
5392 Rotation::None,
5393 Flip::None,
5394 crop,
5395 );
5396 result.unwrap();
5397
5398 let gl_dst = TensorDyn::image(
5399 dst_width,
5400 dst_height,
5401 PixelFormat::Rgba,
5402 DType::U8,
5403 None,
5404 edgefirst_tensor::CpuAccess::ReadWrite,
5405 )
5406 .unwrap();
5407 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5408 let (result, _src, gl_dst) = convert_img(
5409 &mut gl_converter,
5410 src,
5411 gl_dst,
5412 Rotation::None,
5413 Flip::None,
5414 crop,
5415 );
5416 result.unwrap();
5417
5418 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5419 }
5420
5421 #[test]
5422 #[cfg(target_os = "linux")]
5423 #[cfg(feature = "opengl")]
5424 fn test_opengl_all_rgba() {
5425 if !is_opengl_available() {
5426 eprintln!("SKIPPED: {} - OpenGL not available", function!());
5427 return;
5428 }
5429
5430 let dst_width = 640;
5431 let dst_height = 640;
5432 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5433
5434 let mut cpu_converter = CPUProcessor::new();
5435
5436 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5437
5438 let mut mem = vec![None, Some(TensorMemory::Mem), Some(TensorMemory::Shm)];
5439 if is_dma_available() {
5440 mem.push(Some(TensorMemory::Dma));
5441 }
5442 let crop = Crop::new().with_source(Some(Region::new(50, 120, 1024, 576)));
5443 for m in mem {
5444 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), m).unwrap();
5445 let src_dyn = src;
5446
5447 for rot in [
5448 Rotation::None,
5449 Rotation::Clockwise90,
5450 Rotation::Rotate180,
5451 Rotation::CounterClockwise90,
5452 ] {
5453 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
5454 let cpu_dst = TensorDyn::image(
5455 dst_width,
5456 dst_height,
5457 PixelFormat::Rgba,
5458 DType::U8,
5459 m,
5460 edgefirst_tensor::CpuAccess::ReadWrite,
5461 )
5462 .unwrap();
5463 let gl_dst = TensorDyn::image(
5464 dst_width,
5465 dst_height,
5466 PixelFormat::Rgba,
5467 DType::U8,
5468 m,
5469 edgefirst_tensor::CpuAccess::ReadWrite,
5470 )
5471 .unwrap();
5472 cpu_dst
5473 .as_u8()
5474 .unwrap()
5475 .map()
5476 .unwrap()
5477 .as_mut_slice()
5478 .fill(114);
5479 gl_dst
5480 .as_u8()
5481 .unwrap()
5482 .map()
5483 .unwrap()
5484 .as_mut_slice()
5485 .fill(114);
5486
5487 let mut cpu_dst_dyn = cpu_dst;
5488 cpu_converter
5489 .convert(&src_dyn, &mut cpu_dst_dyn, Rotation::None, Flip::None, crop)
5490 .unwrap();
5491 let cpu_dst = {
5492 let mut __t = cpu_dst_dyn.into_u8().unwrap();
5493 __t.set_format(PixelFormat::Rgba).unwrap();
5494 TensorDyn::from(__t)
5495 };
5496
5497 let mut gl_dst_dyn = gl_dst;
5498 gl_converter
5499 .convert(&src_dyn, &mut gl_dst_dyn, Rotation::None, Flip::None, crop)
5500 .map_err(|e| {
5501 log::error!("error mem {m:?} rot {rot:?} error: {e:?}");
5502 e
5503 })
5504 .unwrap();
5505 let gl_dst = {
5506 let mut __t = gl_dst_dyn.into_u8().unwrap();
5507 __t.set_format(PixelFormat::Rgba).unwrap();
5508 TensorDyn::from(__t)
5509 };
5510
5511 compare_images(
5512 &gl_dst,
5513 &cpu_dst,
5514 0.98,
5515 &format!("{} {:?} {:?}", function!(), rot, flip),
5516 );
5517 }
5518 }
5519 }
5520 }
5521
5522 #[test]
5523 #[cfg(target_os = "linux")]
5524 fn test_cpu_rotate() {
5525 for rot in [
5526 Rotation::Clockwise90,
5527 Rotation::Rotate180,
5528 Rotation::CounterClockwise90,
5529 ] {
5530 test_cpu_rotate_(rot);
5531 }
5532 }
5533
5534 #[cfg(target_os = "linux")]
5535 fn test_cpu_rotate_(rot: Rotation) {
5536 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5540
5541 let unchanged_src =
5542 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5543 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5544
5545 let (dst_width, dst_height) = match rot {
5546 Rotation::None | Rotation::Rotate180 => (src.width().unwrap(), src.height().unwrap()),
5547 Rotation::Clockwise90 | Rotation::CounterClockwise90 => {
5548 (src.height().unwrap(), src.width().unwrap())
5549 }
5550 };
5551
5552 let cpu_dst = TensorDyn::image(
5553 dst_width,
5554 dst_height,
5555 PixelFormat::Rgba,
5556 DType::U8,
5557 None,
5558 edgefirst_tensor::CpuAccess::ReadWrite,
5559 )
5560 .unwrap();
5561 let mut cpu_converter = CPUProcessor::new();
5562
5563 let (result, src, cpu_dst) = convert_img(
5566 &mut cpu_converter,
5567 src,
5568 cpu_dst,
5569 rot,
5570 Flip::None,
5571 Crop::no_crop(),
5572 );
5573 result.unwrap();
5574
5575 let (result, cpu_dst, src) = convert_img(
5576 &mut cpu_converter,
5577 cpu_dst,
5578 src,
5579 rot,
5580 Flip::None,
5581 Crop::no_crop(),
5582 );
5583 result.unwrap();
5584
5585 let (result, src, cpu_dst) = convert_img(
5586 &mut cpu_converter,
5587 src,
5588 cpu_dst,
5589 rot,
5590 Flip::None,
5591 Crop::no_crop(),
5592 );
5593 result.unwrap();
5594
5595 let (result, _cpu_dst, src) = convert_img(
5596 &mut cpu_converter,
5597 cpu_dst,
5598 src,
5599 rot,
5600 Flip::None,
5601 Crop::no_crop(),
5602 );
5603 result.unwrap();
5604
5605 compare_images(&src, &unchanged_src, 0.98, function!());
5606 }
5607
5608 #[test]
5609 #[cfg(target_os = "linux")]
5610 #[cfg(feature = "opengl")]
5611 fn test_opengl_rotate() {
5612 if !is_opengl_available() {
5613 eprintln!("SKIPPED: {} - OpenGL not available", function!());
5614 return;
5615 }
5616
5617 let size = (1280, 720);
5618 let mut mem = vec![None, Some(TensorMemory::Shm), Some(TensorMemory::Mem)];
5619
5620 if is_dma_available() {
5621 mem.push(Some(TensorMemory::Dma));
5622 }
5623 for m in mem {
5624 for rot in [
5625 Rotation::Clockwise90,
5626 Rotation::Rotate180,
5627 Rotation::CounterClockwise90,
5628 ] {
5629 test_opengl_rotate_(size, rot, m);
5630 }
5631 }
5632 }
5633
5634 #[cfg(target_os = "linux")]
5635 #[cfg(feature = "opengl")]
5636 fn test_opengl_rotate_(
5637 size: (usize, usize),
5638 rot: Rotation,
5639 tensor_memory: Option<TensorMemory>,
5640 ) {
5641 let (dst_width, dst_height) = match rot {
5642 Rotation::None | Rotation::Rotate180 => size,
5643 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
5644 };
5645
5646 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5647 let src =
5648 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), tensor_memory).unwrap();
5649
5650 let cpu_dst = TensorDyn::image(
5651 dst_width,
5652 dst_height,
5653 PixelFormat::Rgba,
5654 DType::U8,
5655 None,
5656 edgefirst_tensor::CpuAccess::ReadWrite,
5657 )
5658 .unwrap();
5659 let mut cpu_converter = CPUProcessor::new();
5660
5661 let (result, mut src, cpu_dst) = convert_img(
5662 &mut cpu_converter,
5663 src,
5664 cpu_dst,
5665 rot,
5666 Flip::None,
5667 Crop::no_crop(),
5668 );
5669 result.unwrap();
5670
5671 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5672
5673 for _ in 0..5 {
5674 let gl_dst = TensorDyn::image(
5675 dst_width,
5676 dst_height,
5677 PixelFormat::Rgba,
5678 DType::U8,
5679 tensor_memory,
5680 edgefirst_tensor::CpuAccess::ReadWrite,
5681 )
5682 .unwrap();
5683 let (result, src_back, gl_dst) = convert_img(
5684 &mut gl_converter,
5685 src,
5686 gl_dst,
5687 rot,
5688 Flip::None,
5689 Crop::no_crop(),
5690 );
5691 result.unwrap();
5692 src = src_back;
5693 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5694 }
5695 }
5696
5697 #[test]
5698 #[cfg(target_os = "linux")]
5699 fn test_g2d_rotate() {
5700 if !is_g2d_available() {
5701 eprintln!("SKIPPED: test_g2d_rotate - G2D library (libg2d.so.2) not available");
5702 return;
5703 }
5704 if !is_dma_available() {
5705 eprintln!(
5706 "SKIPPED: test_g2d_rotate - DMA memory allocation not available (permission denied or no DMA-BUF support)"
5707 );
5708 return;
5709 }
5710
5711 let size = (1280, 720);
5712 for rot in [
5713 Rotation::Clockwise90,
5714 Rotation::Rotate180,
5715 Rotation::CounterClockwise90,
5716 ] {
5717 test_g2d_rotate_(size, rot);
5718 }
5719 }
5720
5721 #[cfg(target_os = "linux")]
5722 fn test_g2d_rotate_(size: (usize, usize), rot: Rotation) {
5723 let (dst_width, dst_height) = match rot {
5724 Rotation::None | Rotation::Rotate180 => size,
5725 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
5726 };
5727
5728 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5729 let src =
5730 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), Some(TensorMemory::Dma))
5731 .unwrap();
5732
5733 let cpu_dst = TensorDyn::image(
5734 dst_width,
5735 dst_height,
5736 PixelFormat::Rgba,
5737 DType::U8,
5738 None,
5739 edgefirst_tensor::CpuAccess::ReadWrite,
5740 )
5741 .unwrap();
5742 let mut cpu_converter = CPUProcessor::new();
5743
5744 let (result, src, cpu_dst) = convert_img(
5745 &mut cpu_converter,
5746 src,
5747 cpu_dst,
5748 rot,
5749 Flip::None,
5750 Crop::no_crop(),
5751 );
5752 result.unwrap();
5753
5754 let g2d_dst = TensorDyn::image(
5755 dst_width,
5756 dst_height,
5757 PixelFormat::Rgba,
5758 DType::U8,
5759 Some(TensorMemory::Dma),
5760 edgefirst_tensor::CpuAccess::ReadWrite,
5761 )
5762 .unwrap();
5763 let mut g2d_converter = G2DProcessor::new().unwrap();
5764
5765 let (result, _src, g2d_dst) = convert_img(
5766 &mut g2d_converter,
5767 src,
5768 g2d_dst,
5769 rot,
5770 Flip::None,
5771 Crop::no_crop(),
5772 );
5773 result.unwrap();
5774
5775 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
5781 }
5782
5783 #[test]
5784 fn test_rgba_to_yuyv_resize_cpu() {
5785 let src = load_bytes_to_tensor(
5786 1280,
5787 720,
5788 PixelFormat::Rgba,
5789 None,
5790 &edgefirst_bench::testdata::read("camera720p.rgba"),
5791 )
5792 .unwrap();
5793
5794 let (dst_width, dst_height) = (640, 360);
5795
5796 let dst = TensorDyn::image(
5797 dst_width,
5798 dst_height,
5799 PixelFormat::Yuyv,
5800 DType::U8,
5801 None,
5802 edgefirst_tensor::CpuAccess::ReadWrite,
5803 )
5804 .unwrap();
5805
5806 let dst_through_yuyv = TensorDyn::image(
5807 dst_width,
5808 dst_height,
5809 PixelFormat::Rgba,
5810 DType::U8,
5811 None,
5812 edgefirst_tensor::CpuAccess::ReadWrite,
5813 )
5814 .unwrap();
5815 let dst_direct = TensorDyn::image(
5816 dst_width,
5817 dst_height,
5818 PixelFormat::Rgba,
5819 DType::U8,
5820 None,
5821 edgefirst_tensor::CpuAccess::ReadWrite,
5822 )
5823 .unwrap();
5824
5825 let mut cpu_converter = CPUProcessor::new();
5826
5827 let (result, src, dst) = convert_img(
5828 &mut cpu_converter,
5829 src,
5830 dst,
5831 Rotation::None,
5832 Flip::None,
5833 Crop::no_crop(),
5834 );
5835 result.unwrap();
5836
5837 let (result, _dst, dst_through_yuyv) = convert_img(
5838 &mut cpu_converter,
5839 dst,
5840 dst_through_yuyv,
5841 Rotation::None,
5842 Flip::None,
5843 Crop::no_crop(),
5844 );
5845 result.unwrap();
5846
5847 let (result, _src, dst_direct) = convert_img(
5848 &mut cpu_converter,
5849 src,
5850 dst_direct,
5851 Rotation::None,
5852 Flip::None,
5853 Crop::no_crop(),
5854 );
5855 result.unwrap();
5856
5857 compare_images(&dst_through_yuyv, &dst_direct, 0.98, function!());
5858 }
5859
5860 #[test]
5861 #[cfg(target_os = "linux")]
5862 #[cfg(feature = "opengl")]
5863 #[ignore = "opengl doesn't support rendering to PixelFormat::Yuyv texture"]
5864 fn test_rgba_to_yuyv_resize_opengl() {
5865 if !is_opengl_available() {
5866 eprintln!("SKIPPED: {} - OpenGL not available", function!());
5867 return;
5868 }
5869
5870 if !is_dma_available() {
5871 eprintln!(
5872 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
5873 function!()
5874 );
5875 return;
5876 }
5877
5878 let src = load_bytes_to_tensor(
5879 1280,
5880 720,
5881 PixelFormat::Rgba,
5882 None,
5883 &edgefirst_bench::testdata::read("camera720p.rgba"),
5884 )
5885 .unwrap();
5886
5887 let (dst_width, dst_height) = (640, 360);
5888
5889 let dst = TensorDyn::image(
5890 dst_width,
5891 dst_height,
5892 PixelFormat::Yuyv,
5893 DType::U8,
5894 Some(TensorMemory::Dma),
5895 edgefirst_tensor::CpuAccess::ReadWrite,
5896 )
5897 .unwrap();
5898
5899 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5900
5901 let (result, src, dst) = convert_img(
5902 &mut gl_converter,
5903 src,
5904 dst,
5905 Rotation::None,
5906 Flip::None,
5907 Crop::letterbox([255, 255, 255, 255]),
5908 );
5909 result.unwrap();
5910
5911 std::fs::write(
5912 "rgba_to_yuyv_opengl.yuyv",
5913 dst.as_u8().unwrap().map().unwrap().as_slice(),
5914 )
5915 .unwrap();
5916 let cpu_dst = TensorDyn::image(
5917 dst_width,
5918 dst_height,
5919 PixelFormat::Yuyv,
5920 DType::U8,
5921 Some(TensorMemory::Dma),
5922 edgefirst_tensor::CpuAccess::ReadWrite,
5923 )
5924 .unwrap();
5925 let (result, _src, cpu_dst) = convert_img(
5926 &mut CPUProcessor::new(),
5927 src,
5928 cpu_dst,
5929 Rotation::None,
5930 Flip::None,
5931 Crop::no_crop(),
5932 );
5933 result.unwrap();
5934
5935 compare_images_convert_to_rgb(&dst, &cpu_dst, 0.98, function!());
5936 }
5937
5938 #[test]
5939 #[cfg(target_os = "linux")]
5940 fn test_rgba_to_yuyv_resize_g2d() {
5941 if !is_g2d_available() {
5942 eprintln!(
5943 "SKIPPED: test_rgba_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available"
5944 );
5945 return;
5946 }
5947 if !is_dma_available() {
5948 eprintln!(
5949 "SKIPPED: test_rgba_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
5950 );
5951 return;
5952 }
5953
5954 let src = load_bytes_to_tensor(
5955 1280,
5956 720,
5957 PixelFormat::Rgba,
5958 Some(TensorMemory::Dma),
5959 &edgefirst_bench::testdata::read("camera720p.rgba"),
5960 )
5961 .unwrap();
5962
5963 let (dst_width, dst_height) = (1280, 720);
5964
5965 let cpu_dst = TensorDyn::image(
5966 dst_width,
5967 dst_height,
5968 PixelFormat::Yuyv,
5969 DType::U8,
5970 Some(TensorMemory::Dma),
5971 edgefirst_tensor::CpuAccess::ReadWrite,
5972 )
5973 .unwrap();
5974
5975 let g2d_dst = TensorDyn::image(
5976 dst_width,
5977 dst_height,
5978 PixelFormat::Yuyv,
5979 DType::U8,
5980 Some(TensorMemory::Dma),
5981 edgefirst_tensor::CpuAccess::ReadWrite,
5982 )
5983 .unwrap();
5984
5985 let mut g2d_converter = G2DProcessor::new().unwrap();
5986 let crop = Crop::new();
5987
5988 g2d_dst
5989 .as_u8()
5990 .unwrap()
5991 .map()
5992 .unwrap()
5993 .as_mut_slice()
5994 .fill(128);
5995 let (result, src, g2d_dst) = convert_img(
5996 &mut g2d_converter,
5997 src,
5998 g2d_dst,
5999 Rotation::None,
6000 Flip::None,
6001 crop,
6002 );
6003 result.unwrap();
6004
6005 let cpu_dst_img = cpu_dst;
6006 cpu_dst_img
6007 .as_u8()
6008 .unwrap()
6009 .map()
6010 .unwrap()
6011 .as_mut_slice()
6012 .fill(128);
6013 let (result, _src, cpu_dst) = convert_img(
6014 &mut CPUProcessor::new(),
6015 src,
6016 cpu_dst_img,
6017 Rotation::None,
6018 Flip::None,
6019 crop,
6020 );
6021 result.unwrap();
6022
6023 compare_images_convert_to_rgb(&cpu_dst, &g2d_dst, 0.98, function!());
6024 }
6025
6026 #[test]
6027 fn test_yuyv_to_rgba_cpu() {
6028 let file = edgefirst_bench::testdata::read("camera720p.yuyv").to_vec();
6029 let src = TensorDyn::image(
6030 1280,
6031 720,
6032 PixelFormat::Yuyv,
6033 DType::U8,
6034 None,
6035 edgefirst_tensor::CpuAccess::ReadWrite,
6036 )
6037 .unwrap();
6038 src.as_u8()
6039 .unwrap()
6040 .map()
6041 .unwrap()
6042 .as_mut_slice()
6043 .copy_from_slice(&file);
6044
6045 let dst = TensorDyn::image(
6046 1280,
6047 720,
6048 PixelFormat::Rgba,
6049 DType::U8,
6050 None,
6051 edgefirst_tensor::CpuAccess::ReadWrite,
6052 )
6053 .unwrap();
6054 let mut cpu_converter = CPUProcessor::new();
6055
6056 let (result, _src, dst) = convert_img(
6057 &mut cpu_converter,
6058 src,
6059 dst,
6060 Rotation::None,
6061 Flip::None,
6062 Crop::no_crop(),
6063 );
6064 result.unwrap();
6065
6066 let target_image = TensorDyn::image(
6067 1280,
6068 720,
6069 PixelFormat::Rgba,
6070 DType::U8,
6071 None,
6072 edgefirst_tensor::CpuAccess::ReadWrite,
6073 )
6074 .unwrap();
6075 target_image
6076 .as_u8()
6077 .unwrap()
6078 .map()
6079 .unwrap()
6080 .as_mut_slice()
6081 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
6082
6083 compare_images(&dst, &target_image, 0.98, function!());
6086 }
6087
6088 #[test]
6089 fn test_yuyv_to_rgb_cpu() {
6090 let file = edgefirst_bench::testdata::read("camera720p.yuyv").to_vec();
6091 let src = TensorDyn::image(
6092 1280,
6093 720,
6094 PixelFormat::Yuyv,
6095 DType::U8,
6096 None,
6097 edgefirst_tensor::CpuAccess::ReadWrite,
6098 )
6099 .unwrap();
6100 src.as_u8()
6101 .unwrap()
6102 .map()
6103 .unwrap()
6104 .as_mut_slice()
6105 .copy_from_slice(&file);
6106
6107 let dst = TensorDyn::image(
6108 1280,
6109 720,
6110 PixelFormat::Rgb,
6111 DType::U8,
6112 None,
6113 edgefirst_tensor::CpuAccess::ReadWrite,
6114 )
6115 .unwrap();
6116 let mut cpu_converter = CPUProcessor::new();
6117
6118 let (result, _src, dst) = convert_img(
6119 &mut cpu_converter,
6120 src,
6121 dst,
6122 Rotation::None,
6123 Flip::None,
6124 Crop::no_crop(),
6125 );
6126 result.unwrap();
6127
6128 let target_image = TensorDyn::image(
6129 1280,
6130 720,
6131 PixelFormat::Rgb,
6132 DType::U8,
6133 None,
6134 edgefirst_tensor::CpuAccess::ReadWrite,
6135 )
6136 .unwrap();
6137 target_image
6138 .as_u8()
6139 .unwrap()
6140 .map()
6141 .unwrap()
6142 .as_mut_slice()
6143 .as_chunks_mut::<3>()
6144 .0
6145 .iter_mut()
6146 .zip(
6147 edgefirst_bench::testdata::read("camera720p.rgba")
6148 .as_chunks::<4>()
6149 .0,
6150 )
6151 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
6152
6153 compare_images(&dst, &target_image, 0.98, function!());
6156 }
6157
6158 #[test]
6159 #[cfg(target_os = "linux")]
6160 fn test_yuyv_to_rgba_g2d() {
6161 if !is_g2d_available() {
6162 eprintln!("SKIPPED: test_yuyv_to_rgba_g2d - G2D library (libg2d.so.2) not available");
6163 return;
6164 }
6165 if !is_dma_available() {
6166 eprintln!(
6167 "SKIPPED: test_yuyv_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
6168 );
6169 return;
6170 }
6171
6172 let src = load_bytes_to_tensor(
6173 1280,
6174 720,
6175 PixelFormat::Yuyv,
6176 None,
6177 &edgefirst_bench::testdata::read("camera720p.yuyv"),
6178 )
6179 .unwrap();
6180
6181 let dst = TensorDyn::image(
6182 1280,
6183 720,
6184 PixelFormat::Rgba,
6185 DType::U8,
6186 Some(TensorMemory::Dma),
6187 edgefirst_tensor::CpuAccess::ReadWrite,
6188 )
6189 .unwrap();
6190 let mut g2d_converter = G2DProcessor::new().unwrap();
6191
6192 let (result, _src, dst) = convert_img(
6193 &mut g2d_converter,
6194 src,
6195 dst,
6196 Rotation::None,
6197 Flip::None,
6198 Crop::no_crop(),
6199 );
6200 result.unwrap();
6201
6202 let target_image = TensorDyn::image(
6203 1280,
6204 720,
6205 PixelFormat::Rgba,
6206 DType::U8,
6207 None,
6208 edgefirst_tensor::CpuAccess::ReadWrite,
6209 )
6210 .unwrap();
6211 target_image
6212 .as_u8()
6213 .unwrap()
6214 .map()
6215 .unwrap()
6216 .as_mut_slice()
6217 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
6218
6219 compare_images(&dst, &target_image, 0.98, function!());
6223 }
6224
6225 #[test]
6226 #[cfg(target_os = "linux")]
6227 #[cfg(feature = "opengl")]
6228 fn test_yuyv_to_rgba_opengl() {
6229 if !is_opengl_available() {
6230 eprintln!("SKIPPED: {} - OpenGL not available", function!());
6231 return;
6232 }
6233 if !is_dma_available() {
6234 eprintln!(
6235 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
6236 function!()
6237 );
6238 return;
6239 }
6240
6241 let src = load_bytes_to_tensor(
6242 1280,
6243 720,
6244 PixelFormat::Yuyv,
6245 Some(TensorMemory::Dma),
6246 &edgefirst_bench::testdata::read("camera720p.yuyv"),
6247 )
6248 .unwrap();
6249
6250 let dst = TensorDyn::image(
6251 1280,
6252 720,
6253 PixelFormat::Rgba,
6254 DType::U8,
6255 Some(TensorMemory::Dma),
6256 edgefirst_tensor::CpuAccess::ReadWrite,
6257 )
6258 .unwrap();
6259 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
6260
6261 let (result, _src, dst) = convert_img(
6262 &mut gl_converter,
6263 src,
6264 dst,
6265 Rotation::None,
6266 Flip::None,
6267 Crop::no_crop(),
6268 );
6269 result.unwrap();
6270
6271 let target_image = TensorDyn::image(
6272 1280,
6273 720,
6274 PixelFormat::Rgba,
6275 DType::U8,
6276 None,
6277 edgefirst_tensor::CpuAccess::ReadWrite,
6278 )
6279 .unwrap();
6280 target_image
6281 .as_u8()
6282 .unwrap()
6283 .map()
6284 .unwrap()
6285 .as_mut_slice()
6286 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
6287
6288 compare_images(&dst, &target_image, 0.98, function!());
6292 }
6293
6294 #[test]
6304 #[cfg(target_os = "macos")]
6305 #[cfg(feature = "opengl")]
6306 fn test_grey_r8_iosurface_to_rgba_opengl_macos() {
6307 let mut proc = match GLProcessorThreaded::new(None) {
6308 Ok(p) => p,
6309 Err(e) => {
6310 eprintln!("SKIPPED: {} — GL engine init failed ({e:?})", function!());
6311 return;
6312 }
6313 };
6314
6315 let (w, h) = (16usize, 16usize);
6316 let src = TensorDyn::image(
6317 w,
6318 h,
6319 PixelFormat::Grey,
6320 DType::U8,
6321 Some(TensorMemory::Dma),
6322 edgefirst_tensor::CpuAccess::ReadWrite,
6323 )
6324 .expect("GREY IOSurface (R8/L008) should allocate — proves the FourCC mapping");
6325 {
6327 let su8 = src.as_u8().unwrap();
6328 let stride = src.as_u8().unwrap().effective_row_stride().unwrap();
6329 let mut m = su8.map().unwrap();
6330 let buf = m.as_mut_slice();
6331 for y in 0..h {
6332 for x in 0..w {
6333 buf[y * stride + x] = ((x * 13 + y * 7) & 0xff) as u8;
6334 }
6335 }
6336 }
6337
6338 let dst = TensorDyn::image(
6339 w,
6340 h,
6341 PixelFormat::Rgba,
6342 DType::U8,
6343 Some(TensorMemory::Dma),
6344 edgefirst_tensor::CpuAccess::ReadWrite,
6345 )
6346 .unwrap();
6347 let (result, src_back, dst) = convert_img(
6348 &mut proc,
6349 src,
6350 dst,
6351 Rotation::None,
6352 Flip::None,
6353 Crop::no_crop(),
6354 );
6355 result.expect("GREY(R8 IOSurface) → RGBA must convert on ANGLE (R8 binding works)");
6356
6357 let src_stride = src_back.as_u8().unwrap().effective_row_stride().unwrap();
6358 let src_map = src_back.as_u8().unwrap().map().unwrap();
6359 let sbytes = src_map.as_slice();
6360 let dst_stride = dst.as_u8().unwrap().effective_row_stride().unwrap();
6361 let dst_map = dst.as_u8().unwrap().map().unwrap();
6362 let dbytes = dst_map.as_slice();
6363 for y in 0..h {
6364 for x in 0..w {
6365 let yv = sbytes[y * src_stride + x] as i16;
6366 let p = y * dst_stride + x * 4;
6367 for c in 0..3 {
6368 assert!(
6369 (dbytes[p + c] as i16 - yv).abs() <= 2,
6370 "pixel ({x},{y}) ch{c} = {} expected ~{yv} (GREY→RGB identity)",
6371 dbytes[p + c]
6372 );
6373 }
6374 }
6375 }
6376 }
6377
6378 #[test]
6384 #[cfg(target_os = "macos")]
6385 #[cfg(feature = "opengl")]
6386 fn test_nv12_to_planar_f16_two_pass_opengl_macos() {
6387 let mut gpu = match GLProcessorThreaded::new(None) {
6388 Ok(p) => p,
6389 Err(e) => {
6390 eprintln!("SKIPPED: {} — init failed ({e:?})", function!());
6391 return;
6392 }
6393 };
6394 let (w, h) = (64usize, 64usize);
6395 let src = match TensorDyn::image(
6396 w,
6397 h,
6398 PixelFormat::Nv12,
6399 DType::U8,
6400 Some(TensorMemory::Dma),
6401 edgefirst_tensor::CpuAccess::ReadWrite,
6402 ) {
6403 Ok(t) => t,
6404 Err(e) => {
6405 eprintln!("SKIPPED: {} — NV12 IOSurface alloc: {e:?}", function!());
6406 return;
6407 }
6408 };
6409 src.as_u8().unwrap().map().unwrap().as_mut_slice().fill(128); let dst = match TensorDyn::image(
6412 w,
6413 h,
6414 PixelFormat::PlanarRgb,
6415 DType::F16,
6416 Some(TensorMemory::Dma),
6417 edgefirst_tensor::CpuAccess::ReadWrite,
6418 ) {
6419 Ok(t) => t,
6420 Err(e) => {
6421 eprintln!("SKIPPED: {} — F16 PlanarRgb IOSurface: {e:?}", function!());
6422 return;
6423 }
6424 };
6425 let mut dst = dst;
6426 if let Err(e) = ImageProcessorTrait::convert(
6428 &mut gpu,
6429 &src,
6430 &mut dst,
6431 Rotation::None,
6432 Flip::None,
6433 Crop::no_crop(),
6434 ) {
6435 eprintln!(
6439 "SKIPPED: {} — NV12→PlanarRgb F16 not available ({e:?})",
6440 function!()
6441 );
6442 return;
6443 }
6444 let dt = dst.as_f16().expect("dst is F16 PlanarRgb");
6445 let map = dt.map().unwrap();
6446 let vals = map.as_slice();
6447 let mut checked = 0usize;
6450 for &v in vals.iter() {
6451 let f = f32::from(v);
6452 assert!(
6453 (0.40..=0.60).contains(&f),
6454 "planar F16 value {f} not ~0.5 for neutral-grey NV12"
6455 );
6456 checked += 1;
6457 }
6458 assert!(
6459 checked >= w * h * 3,
6460 "expected >= 3 planes of samples, got {checked}"
6461 );
6462 }
6463
6464 #[test]
6472 #[cfg(target_os = "macos")]
6473 #[cfg(feature = "opengl")]
6474 fn test_nv12_to_planar_f16_two_pass_pool_opengl_macos() {
6475 let mut gpu = match GLProcessorThreaded::new(None) {
6476 Ok(p) => p,
6477 Err(e) => {
6478 eprintln!("SKIPPED: {} — init failed ({e:?})", function!());
6479 return;
6480 }
6481 };
6482 let (fw, fh) = (96usize, 64usize);
6484 let (pool_w, pool_h) = (256usize, 768usize);
6485 let (model_w, model_h) = (128usize, 128usize);
6486
6487 let mut src = match TensorDyn::image(
6488 pool_w,
6489 pool_h,
6490 PixelFormat::Grey,
6491 DType::U8,
6492 Some(TensorMemory::Dma),
6493 edgefirst_tensor::CpuAccess::ReadWrite,
6494 ) {
6495 Ok(t) => t,
6496 Err(e) => {
6497 eprintln!("SKIPPED: {} — R8 pool alloc: {e:?}", function!());
6498 return;
6499 }
6500 };
6501 src.configure_image(fw, fh, PixelFormat::Nv12)
6502 .unwrap_or_else(|e| panic!("configure_image NV12 on pool: {e}"));
6503 let stride = src.as_u8().unwrap().effective_row_stride().unwrap();
6504 src.as_u8().unwrap().map().unwrap().as_mut_slice().fill(128); let mut dst = match TensorDyn::image(
6507 model_w,
6508 model_h,
6509 PixelFormat::PlanarRgb,
6510 DType::F16,
6511 Some(TensorMemory::Dma),
6512 edgefirst_tensor::CpuAccess::ReadWrite,
6513 ) {
6514 Ok(t) => t,
6515 Err(e) => {
6516 eprintln!("SKIPPED: {} — F16 PlanarRgb dst: {e:?}", function!());
6517 return;
6518 }
6519 };
6520
6521 let _ = model_w;
6523 let crop = Crop::new()
6524 .with_source(Some(Region::new(0, 0, fw, fh)))
6525 .with_fit(Fit::Letterbox {
6526 pad: [0, 0, 0, 255],
6527 });
6528 if let Err(e) =
6529 ImageProcessorTrait::convert(&mut gpu, &src, &mut dst, Rotation::None, Flip::None, crop)
6530 {
6531 eprintln!(
6532 "SKIPPED: {} — NV12→PlanarRgb F16 unavailable ({e:?})",
6533 function!()
6534 );
6535 return;
6536 }
6537 let _ = stride;
6538 let dt = dst.as_f16().expect("dst F16");
6541 let map = dt.map().unwrap();
6542 let any_half = map.as_slice().iter().any(|&v| {
6543 let f = f32::from(v);
6544 (0.40..=0.60).contains(&f)
6545 });
6546 assert!(any_half, "expected ~0.5 grey samples in the letterbox band");
6547 }
6548
6549 #[test]
6555 #[cfg(target_os = "macos")]
6556 #[cfg(feature = "opengl")]
6557 fn test_nv12_to_planar_f16_cross_thread_opengl_macos() {
6558 use std::sync::mpsc;
6559 let mut proc = match ImageProcessor::new() {
6562 Ok(p) => p,
6563 Err(e) => {
6564 eprintln!("SKIPPED: {} — init failed ({e:?})", function!());
6565 return;
6566 }
6567 };
6568 let (fw, fh) = (96usize, 64usize);
6569 let mut src = match TensorDyn::image(
6570 256,
6571 768,
6572 PixelFormat::Grey,
6573 DType::U8,
6574 Some(TensorMemory::Dma),
6575 edgefirst_tensor::CpuAccess::ReadWrite,
6576 ) {
6577 Ok(t) => t,
6578 Err(e) => {
6579 eprintln!("SKIPPED: {} — pool: {e:?}", function!());
6580 return;
6581 }
6582 };
6583 src.configure_image(fw, fh, PixelFormat::Nv12).unwrap();
6584 src.as_u8().unwrap().map().unwrap().as_mut_slice().fill(128);
6585 let mut dst = match TensorDyn::image(
6586 128,
6587 128,
6588 PixelFormat::PlanarRgb,
6589 DType::F16,
6590 Some(TensorMemory::Dma),
6591 edgefirst_tensor::CpuAccess::ReadWrite,
6592 ) {
6593 Ok(t) => t,
6594 Err(e) => {
6595 eprintln!("SKIPPED: {} — dst: {e:?}", function!());
6596 return;
6597 }
6598 };
6599 let crop = Crop::new().with_source(Some(Region::new(0, 0, fw, fh)));
6600
6601 let (tx, rx) = mpsc::channel::<bool>();
6604 let worker = std::thread::spawn(move || {
6605 let _ = ImageProcessorTrait::convert(
6606 &mut proc,
6607 &src,
6608 &mut dst,
6609 Rotation::None,
6610 Flip::None,
6611 crop,
6612 );
6613 let _ = tx.send(true);
6614 });
6615 match rx.recv_timeout(std::time::Duration::from_secs(20)) {
6616 Ok(_) => { let _ = worker.join(); }
6617 Err(_) => panic!(
6618 "cross-thread NV12→PlanarRgb convert HUNG (>20s) — reproduces the orchestrator deadlock"
6619 ),
6620 }
6621 }
6622
6623 #[test]
6630 #[cfg(target_os = "macos")]
6631 #[cfg(feature = "opengl")]
6632 fn test_nv_to_planar_f16_varying_sizes_no_leak_opengl_macos() {
6633 let mut gpu = match GLProcessorThreaded::new(None) {
6634 Ok(p) => p,
6635 Err(e) => {
6636 eprintln!("SKIPPED: {} — init failed ({e:?})", function!());
6637 return;
6638 }
6639 };
6640 let (max_w, max_h) = (640usize, 640usize);
6643 let depth = 4usize;
6644 let mut srcs = Vec::new();
6645 let mut dsts = Vec::new();
6646 for _ in 0..depth {
6647 srcs.push(
6648 match TensorDyn::image(
6649 max_w,
6650 max_h * 3,
6651 PixelFormat::Grey,
6652 DType::U8,
6653 Some(TensorMemory::Dma),
6654 edgefirst_tensor::CpuAccess::ReadWrite,
6655 ) {
6656 Ok(t) => t,
6657 Err(e) => {
6658 eprintln!("SKIPPED: {} — pool: {e:?}", function!());
6659 return;
6660 }
6661 },
6662 );
6663 dsts.push(
6664 match TensorDyn::image(
6665 640,
6666 640,
6667 PixelFormat::PlanarRgb,
6668 DType::F16,
6669 Some(TensorMemory::Dma),
6670 edgefirst_tensor::CpuAccess::ReadWrite,
6671 ) {
6672 Ok(t) => t,
6673 Err(e) => {
6674 eprintln!("SKIPPED: {} — dst: {e:?}", function!());
6675 return;
6676 }
6677 },
6678 );
6679 }
6680 let sizes = [
6682 (640, 480),
6683 (500, 375),
6684 (640, 427),
6685 (333, 500),
6686 (480, 640),
6687 (612, 612),
6688 (428, 640),
6689 (576, 432),
6690 ];
6691 let mut first_ms = 0f64;
6692 let mut last_ms = 0f64;
6693 let iters = 40usize;
6694 for i in 0..iters {
6695 let (fw, fh) = sizes[i % sizes.len()];
6696 let src = &mut srcs[i % depth];
6697 let dst = &mut dsts[i % depth];
6698 src.configure_image(fw, fh, PixelFormat::Nv24).unwrap();
6699 src.as_u8().unwrap().map().unwrap().as_mut_slice().fill(128);
6700 let crop = Crop::new().with_source(Some(Region::new(0, 0, fw, fh)));
6701 let t0 = std::time::Instant::now();
6702 ImageProcessorTrait::convert(&mut gpu, src, dst, Rotation::None, Flip::None, crop)
6703 .unwrap_or_else(|e| panic!("convert iter {i} ({fw}×{fh}): {e}"));
6704 let ms = t0.elapsed().as_secs_f64() * 1e3;
6705 if i == 2 {
6706 first_ms = ms;
6707 }
6708 if i == iters - 1 {
6709 last_ms = ms;
6710 }
6711 }
6712 eprintln!("first={first_ms:.2}ms last={last_ms:.2}ms");
6713 assert!(
6714 last_ms < first_ms * 5.0 + 5.0,
6715 "convert latency ran away: first {first_ms:.2}ms → last {last_ms:.2}ms (intermediate/pbuffer leak)"
6716 );
6717 }
6718
6719 #[test]
6726 #[cfg(target_os = "macos")]
6727 #[cfg(feature = "opengl")]
6728 fn test_nv12_nv16_nv24_to_rgba_opengl_macos() {
6729 let mut gpu = match GLProcessorThreaded::new(None) {
6730 Ok(p) => p,
6731 Err(e) => {
6732 eprintln!("SKIPPED: {} — GL engine init failed ({e:?})", function!());
6733 return;
6734 }
6735 };
6736 let mut cpu = CPUProcessor::new();
6737
6738 let fill = |buf: &mut [u8], stride: usize, fmt: PixelFormat, w: usize, h: usize| {
6750 for y in 0..h {
6751 for x in 0..w {
6752 buf[y * stride + x] = ((x * 9 + y * 5) & 0xff) as u8;
6753 }
6754 }
6755 let (cw, ch, uv_grid_rows) = match fmt {
6756 PixelFormat::Nv12 => (w / 2, h / 2, 1usize),
6757 PixelFormat::Nv16 => (w / 2, h, 1usize),
6758 _ => (w, h, 2usize), };
6760 let uv_plane = h * stride;
6761 for cy in 0..ch {
6762 for cx in 0..cw {
6763 let off = uv_plane + cy * uv_grid_rows * stride + cx * 2;
6764 buf[off] = ((cx * 11 + 30) & 0xff) as u8;
6765 buf[off + 1] = ((cy * 7 + 200) & 0xff) as u8;
6766 }
6767 }
6768 };
6769
6770 for fmt in [PixelFormat::Nv12, PixelFormat::Nv16, PixelFormat::Nv24] {
6771 for (w, h) in [
6772 (16usize, 16usize), (15, 16), (16, 15), ] {
6776 let mem = TensorDyn::image(
6777 w,
6778 h,
6779 fmt,
6780 DType::U8,
6781 None,
6782 edgefirst_tensor::CpuAccess::ReadWrite,
6783 )
6784 .unwrap();
6785 let mem_stride = mem.as_u8().unwrap().effective_row_stride().unwrap();
6786 fill(
6787 mem.as_u8().unwrap().map().unwrap().as_mut_slice(),
6788 mem_stride,
6789 fmt,
6790 w,
6791 h,
6792 );
6793 let cpu_dst = TensorDyn::image(
6794 w,
6795 h,
6796 PixelFormat::Rgba,
6797 DType::U8,
6798 None,
6799 edgefirst_tensor::CpuAccess::ReadWrite,
6800 )
6801 .unwrap();
6802 let (r, _s, cpu_dst) = convert_img(
6803 &mut cpu,
6804 mem,
6805 cpu_dst,
6806 Rotation::None,
6807 Flip::None,
6808 Crop::no_crop(),
6809 );
6810 r.unwrap_or_else(|e| panic!("CPU {fmt:?}->{w}x{h}->RGBA: {e}"));
6811
6812 let ios = TensorDyn::image(
6813 w,
6814 h,
6815 fmt,
6816 DType::U8,
6817 Some(TensorMemory::Dma),
6818 edgefirst_tensor::CpuAccess::ReadWrite,
6819 )
6820 .unwrap_or_else(|e| panic!("{fmt:?} {w}x{h} IOSurface alloc: {e}"));
6821 let ios_stride = ios.as_u8().unwrap().effective_row_stride().unwrap();
6822 fill(
6823 ios.as_u8().unwrap().map().unwrap().as_mut_slice(),
6824 ios_stride,
6825 fmt,
6826 w,
6827 h,
6828 );
6829 let gpu_dst = TensorDyn::image(
6830 w,
6831 h,
6832 PixelFormat::Rgba,
6833 DType::U8,
6834 Some(TensorMemory::Dma),
6835 edgefirst_tensor::CpuAccess::ReadWrite,
6836 )
6837 .unwrap();
6838 let (r, _s, gpu_dst) = convert_img(
6839 &mut gpu,
6840 ios,
6841 gpu_dst,
6842 Rotation::None,
6843 Flip::None,
6844 Crop::no_crop(),
6845 );
6846 r.unwrap_or_else(|e| panic!("GPU {fmt:?}->{w}x{h}->RGBA on ANGLE: {e}"));
6847
6848 let cs = cpu_dst.as_u8().unwrap().effective_row_stride().unwrap();
6849 let cmap = cpu_dst.as_u8().unwrap().map().unwrap();
6850 let cb = cmap.as_slice();
6851 let gs = gpu_dst.as_u8().unwrap().effective_row_stride().unwrap();
6852 let gmap = gpu_dst.as_u8().unwrap().map().unwrap();
6853 let gb = gmap.as_slice();
6854 let mut max_d = 0i16;
6855 for y in 0..h {
6856 for x in 0..w {
6857 for c in 0..3 {
6858 let cv = cb[y * cs + x * 4 + c] as i16;
6859 let gv = gb[y * gs + x * 4 + c] as i16;
6860 max_d = max_d.max((cv - gv).abs());
6861 }
6862 }
6863 }
6864 assert!(
6865 max_d <= 3,
6866 "{fmt:?} {w}x{h}: GPU vs CPU RGBA max channel diff {max_d} > 3"
6867 );
6868 }
6869 }
6870 }
6871
6872 #[test]
6880 #[cfg(target_os = "macos")]
6881 #[cfg(feature = "opengl")]
6882 fn test_nv_to_rgba_larger_pool_surface_opengl_macos() {
6883 let mut gpu = match GLProcessorThreaded::new(None) {
6884 Ok(p) => p,
6885 Err(e) => {
6886 eprintln!("SKIPPED: {} — GL engine init failed ({e:?})", function!());
6887 return;
6888 }
6889 };
6890 let mut cpu = CPUProcessor::new();
6891 let (pool_w, pool_h) = (256usize, 256usize);
6894
6895 let fill = |buf: &mut [u8], stride: usize, fmt: PixelFormat, w: usize, h: usize| {
6899 for y in 0..h {
6900 for x in 0..w {
6901 buf[y * stride + x] = ((x * 9 + y * 5) & 0xff) as u8;
6902 }
6903 }
6904 let (cw, ch, uv_grid_rows) = match fmt {
6905 PixelFormat::Nv12 => (w / 2, h / 2, 1usize),
6906 PixelFormat::Nv16 => (w / 2, h, 1usize),
6907 _ => (w, h, 2usize), };
6909 let uv_plane = h * stride;
6910 for cy in 0..ch {
6911 for cx in 0..cw {
6912 let off = uv_plane + cy * uv_grid_rows * stride + cx * 2;
6913 buf[off] = ((cx * 11 + 30) & 0xff) as u8;
6914 buf[off + 1] = ((cy * 7 + 200) & 0xff) as u8;
6915 }
6916 }
6917 };
6918
6919 for fmt in [PixelFormat::Nv12, PixelFormat::Nv16, PixelFormat::Nv24] {
6920 for (w, h) in [
6921 (40usize, 24usize), (15, 16), (16, 15), ] {
6925 let ew = w.next_multiple_of(2);
6928
6929 let mem = TensorDyn::image(
6931 w,
6932 h,
6933 fmt,
6934 DType::U8,
6935 None,
6936 edgefirst_tensor::CpuAccess::ReadWrite,
6937 )
6938 .unwrap();
6939 let mem_stride = mem.as_u8().unwrap().effective_row_stride().unwrap();
6940 fill(
6941 mem.as_u8().unwrap().map().unwrap().as_mut_slice(),
6942 mem_stride,
6943 fmt,
6944 w,
6945 h,
6946 );
6947 let cpu_dst = TensorDyn::image(
6948 w,
6949 h,
6950 PixelFormat::Rgba,
6951 DType::U8,
6952 None,
6953 edgefirst_tensor::CpuAccess::ReadWrite,
6954 )
6955 .unwrap();
6956 let (r, _s, cpu_dst) = convert_img(
6957 &mut cpu,
6958 mem,
6959 cpu_dst,
6960 Rotation::None,
6961 Flip::None,
6962 Crop::no_crop(),
6963 );
6964 r.unwrap_or_else(|e| panic!("CPU {fmt:?}->{w}x{h}->RGBA: {e}"));
6965
6966 let mut ios = match TensorDyn::image(
6970 pool_w,
6971 pool_h,
6972 PixelFormat::Grey,
6973 DType::U8,
6974 Some(TensorMemory::Dma),
6975 edgefirst_tensor::CpuAccess::ReadWrite,
6976 ) {
6977 Ok(t) => t,
6978 Err(e) => {
6979 eprintln!("SKIPPED: {} — R8 pool IOSurface alloc: {e:?}", function!());
6980 return;
6981 }
6982 };
6983 ios.configure_image(w, h, fmt)
6984 .unwrap_or_else(|e| panic!("configure_image {fmt:?} {w}x{h} on pool: {e}"));
6985 let ios_stride = ios.as_u8().unwrap().effective_row_stride().unwrap();
6986 assert!(
6987 ios_stride > ew,
6988 "{fmt:?} {w}x{h}: pool stride {ios_stride} should exceed even width {ew} \
6989 (test must exercise padding)"
6990 );
6991 fill(
6992 ios.as_u8().unwrap().map().unwrap().as_mut_slice(),
6993 ios_stride,
6994 fmt,
6995 w,
6996 h,
6997 );
6998
6999 let gpu_dst = TensorDyn::image(
7000 w,
7001 h,
7002 PixelFormat::Rgba,
7003 DType::U8,
7004 Some(TensorMemory::Dma),
7005 edgefirst_tensor::CpuAccess::ReadWrite,
7006 )
7007 .unwrap();
7008 let (r, _s, gpu_dst) = convert_img(
7009 &mut gpu,
7010 ios,
7011 gpu_dst,
7012 Rotation::None,
7013 Flip::None,
7014 Crop::no_crop(),
7015 );
7016 r.unwrap_or_else(|e| {
7017 panic!("GPU {fmt:?}->{w}x{h}->RGBA (pool surface) on ANGLE: {e}")
7018 });
7019
7020 let cs = cpu_dst.as_u8().unwrap().effective_row_stride().unwrap();
7021 let cmap = cpu_dst.as_u8().unwrap().map().unwrap();
7022 let cb = cmap.as_slice();
7023 let gs = gpu_dst.as_u8().unwrap().effective_row_stride().unwrap();
7024 let gmap = gpu_dst.as_u8().unwrap().map().unwrap();
7025 let gb = gmap.as_slice();
7026 let mut max_d = 0i16;
7027 for y in 0..h {
7028 for x in 0..w {
7029 for c in 0..3 {
7030 let cv = cb[y * cs + x * 4 + c] as i16;
7031 let gv = gb[y * gs + x * 4 + c] as i16;
7032 max_d = max_d.max((cv - gv).abs());
7033 }
7034 }
7035 }
7036 assert!(
7037 max_d <= 3,
7038 "{fmt:?} {w}x{h}: GPU(pool surface) vs CPU RGBA max channel diff {max_d} > 3"
7039 );
7040 }
7041 }
7042 }
7043
7044 #[test]
7045 #[cfg(target_os = "macos")]
7046 #[cfg(feature = "opengl")]
7047 fn test_yuyv_to_rgba_opengl_macos() {
7048 let mut proc = match GLProcessorThreaded::new(None) {
7049 Ok(p) => p,
7050 Err(e) => {
7051 eprintln!(
7052 "SKIPPED: {} — GL engine init failed ({e:?}). \
7053 Install ANGLE via `brew install startergo/angle/angle` \
7054 and re-sign per README.md § macOS GPU Acceleration to \
7055 run this test.",
7056 function!()
7057 );
7058 return;
7059 }
7060 };
7061
7062 let src = load_bytes_to_tensor(
7063 1280,
7064 720,
7065 PixelFormat::Yuyv,
7066 Some(TensorMemory::Dma),
7067 &edgefirst_bench::testdata::read("camera720p.yuyv"),
7068 )
7069 .unwrap();
7070
7071 let dst = TensorDyn::image(
7072 1280,
7073 720,
7074 PixelFormat::Rgba,
7075 DType::U8,
7076 Some(TensorMemory::Dma),
7077 edgefirst_tensor::CpuAccess::ReadWrite,
7078 )
7079 .unwrap();
7080
7081 let (result, _src, dst) = convert_img(
7082 &mut proc,
7083 src,
7084 dst,
7085 Rotation::None,
7086 Flip::None,
7087 Crop::no_crop(),
7088 );
7089 result.unwrap();
7090
7091 let target_image = TensorDyn::image(
7092 1280,
7093 720,
7094 PixelFormat::Rgba,
7095 DType::U8,
7096 None,
7097 edgefirst_tensor::CpuAccess::ReadWrite,
7098 )
7099 .unwrap();
7100 target_image
7101 .as_u8()
7102 .unwrap()
7103 .map()
7104 .unwrap()
7105 .as_mut_slice()
7106 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
7107
7108 compare_images(&dst, &target_image, 0.98, function!());
7113 }
7114
7115 #[test]
7134 #[cfg(target_os = "macos")]
7135 #[cfg(feature = "opengl")]
7136 fn test_yuyv_to_rgba_opengl_macos_multi_resolution() {
7137 let mut proc = match GLProcessorThreaded::new(None) {
7138 Ok(p) => p,
7139 Err(e) => {
7140 eprintln!("SKIPPED: {} — GL engine init failed ({e:?})", function!());
7141 return;
7142 }
7143 };
7144
7145 for (w, h) in [(64usize, 32usize), (3840, 2160)] {
7146 let bytes_per_row = w * 2;
7149 let mut yuyv = vec![0u8; bytes_per_row * h];
7150 for chunk in yuyv.chunks_exact_mut(4) {
7151 chunk[0] = 128; chunk[1] = 128; chunk[2] = 128; chunk[3] = 128; }
7156
7157 let src = load_bytes_to_tensor(w, h, PixelFormat::Yuyv, Some(TensorMemory::Dma), &yuyv)
7158 .unwrap();
7159
7160 let dst = TensorDyn::image(
7161 w,
7162 h,
7163 PixelFormat::Rgba,
7164 DType::U8,
7165 Some(TensorMemory::Dma),
7166 edgefirst_tensor::CpuAccess::ReadWrite,
7167 )
7168 .unwrap();
7169
7170 let (result, _src, dst) = convert_img(
7171 &mut proc,
7172 src,
7173 dst,
7174 Rotation::None,
7175 Flip::None,
7176 Crop::no_crop(),
7177 );
7178 result.expect("GL convert should succeed at this resolution");
7179
7180 let dst_u8 = dst.as_u8().unwrap();
7185 let dst_map = dst_u8.map().unwrap();
7186 let dst_bytes = dst_map.as_slice();
7187 assert_eq!(dst_bytes.len(), w * h * 4, "RGBA byte count");
7188 for px in dst_bytes.chunks_exact(4) {
7189 for (i, &c) in px[..3].iter().enumerate() {
7190 assert!(
7191 (120..=140).contains(&c),
7192 "{}: channel {i} = {c} (expected ~128 ±12) at {w}×{h}",
7193 function!(),
7194 );
7195 }
7196 assert_eq!(px[3], 255, "alpha must be 1.0");
7197 }
7198 }
7199 }
7200
7201 #[test]
7211 #[cfg(target_os = "macos")]
7212 #[cfg(feature = "opengl")]
7213 fn test_macos_gl_pbuffer_cache_reuses_surfaces() {
7214 let mut proc = match GLProcessorThreaded::new(None) {
7215 Ok(p) => p,
7216 Err(e) => {
7217 eprintln!("SKIPPED: {} — GL engine init failed ({e:?})", function!());
7218 return;
7219 }
7220 };
7221
7222 let mut yuyv = vec![0u8; 64 * 32 * 2];
7224 for chunk in yuyv.chunks_exact_mut(4) {
7225 chunk[0] = 200;
7226 chunk[1] = 100;
7227 chunk[2] = 200;
7228 chunk[3] = 156;
7229 }
7230 let src = load_bytes_to_tensor(64, 32, PixelFormat::Yuyv, Some(TensorMemory::Dma), &yuyv)
7231 .unwrap();
7232 let dst = TensorDyn::image(
7233 64,
7234 32,
7235 PixelFormat::Rgba,
7236 DType::U8,
7237 Some(TensorMemory::Dma),
7238 edgefirst_tensor::CpuAccess::ReadWrite,
7239 )
7240 .unwrap();
7241
7242 let (r1, src, dst) = convert_img(
7243 &mut proc,
7244 src,
7245 dst,
7246 Rotation::None,
7247 Flip::None,
7248 Crop::no_crop(),
7249 );
7250 r1.unwrap();
7251 let first: Vec<u8> = dst.as_u8().unwrap().map().unwrap().as_slice().to_vec();
7252
7253 let (r2, _src, dst) = convert_img(
7254 &mut proc,
7255 src,
7256 dst,
7257 Rotation::None,
7258 Flip::None,
7259 Crop::no_crop(),
7260 );
7261 r2.unwrap();
7262 let second: Vec<u8> = dst.as_u8().unwrap().map().unwrap().as_slice().to_vec();
7263
7264 assert_eq!(first, second, "cache-hit conversion must be deterministic");
7265 }
7266
7267 #[test]
7275 #[cfg(target_os = "macos")]
7276 #[cfg(feature = "opengl")]
7277 fn test_macos_gl_pbuffer_cache_steady_state() {
7278 let mut proc = match GLProcessorThreaded::new(None) {
7279 Ok(p) => p,
7280 Err(e) => {
7281 eprintln!("SKIPPED: {} — GL engine init failed ({e:?})", function!());
7282 return;
7283 }
7284 };
7285
7286 let (w, h) = (64usize, 32usize);
7287 const POOL: usize = 3;
7288 const FRAMES: usize = 100;
7289
7290 let yuyv = vec![128u8; w * h * 2];
7291 let pool: Vec<TensorDyn> = (0..POOL)
7292 .map(|_| {
7293 load_bytes_to_tensor(w, h, PixelFormat::Yuyv, Some(TensorMemory::Dma), &yuyv)
7294 .unwrap()
7295 })
7296 .collect();
7297 let mut dst = TensorDyn::image(
7298 w,
7299 h,
7300 PixelFormat::Rgba,
7301 DType::U8,
7302 Some(TensorMemory::Dma),
7303 edgefirst_tensor::CpuAccess::ReadWrite,
7304 )
7305 .unwrap();
7306
7307 for src in pool.iter().cycle().take(POOL * 2) {
7309 proc.convert(src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
7310 .unwrap();
7311 }
7312 let warm = proc.egl_cache_stats().unwrap();
7313
7314 for src in pool.iter().cycle().take(FRAMES) {
7315 proc.convert(src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
7316 .unwrap();
7317 }
7318 let steady = proc.egl_cache_stats().unwrap();
7319
7320 assert_eq!(
7321 warm.total_misses(),
7322 steady.total_misses(),
7323 "steady-state loop created new imports (warm {warm:?}, steady {steady:?})"
7324 );
7325 let hits = |s: &GlCacheStats| s.src.hits + s.dst.hits + s.nv_r8.hits;
7326 assert!(
7327 hits(&steady) - hits(&warm) >= FRAMES as u64,
7328 "expected at least {FRAMES} import-cache hits over the loop, got {}",
7329 hits(&steady) - hits(&warm)
7330 );
7331 }
7332
7333 #[test]
7345 #[cfg(target_os = "macos")]
7346 #[cfg(feature = "opengl")]
7347 fn test_macos_gl_f16_planar_is_gl_backed() {
7348 let mut proc = ImageProcessor::new().expect("ImageProcessor");
7349 let Some(ref gl) = proc.opengl else {
7350 eprintln!("SKIPPED: {} — GL backend unavailable", function!());
7351 return;
7352 };
7353 if !gl.supported_render_dtypes().f16 {
7354 eprintln!(
7355 "SKIPPED: {} — configuration lacks F16 color-buffer support",
7356 function!()
7357 );
7358 return;
7359 }
7360 let stats_before = gl.egl_cache_stats().expect("cache stats");
7361
7362 let src = TensorDyn::image(
7363 1280,
7364 720,
7365 PixelFormat::Nv12,
7366 DType::U8,
7367 Some(TensorMemory::Dma),
7368 edgefirst_tensor::CpuAccess::ReadWrite,
7369 )
7370 .unwrap();
7371 {
7372 let t = src.as_u8().unwrap();
7373 let mut m = t.map().unwrap();
7374 for (i, b) in m.as_mut_slice().iter_mut().enumerate() {
7375 *b = ((i * 31) % 211) as u8;
7376 }
7377 }
7378 let mut dst = TensorDyn::image(
7379 640,
7380 640,
7381 PixelFormat::PlanarRgb,
7382 DType::F16,
7383 Some(TensorMemory::Dma),
7384 edgefirst_tensor::CpuAccess::ReadWrite,
7385 )
7386 .unwrap();
7387
7388 proc.convert(
7389 &src,
7390 &mut dst,
7391 Rotation::None,
7392 Flip::None,
7393 Crop::letterbox([114, 114, 114, 255]),
7394 )
7395 .expect("F16 capability reported but the NV12→PlanarF16 convert failed");
7396 let stats_after = proc
7397 .opengl
7398 .as_ref()
7399 .expect("GL backend present")
7400 .egl_cache_stats()
7401 .expect("cache stats");
7402 assert!(
7407 stats_after.total_misses() >= stats_before.total_misses() + 2,
7408 "convert succeeded but the GL engine did not import both the \
7409 source and the F16 destination — the work did not (fully) run \
7410 on the GL backend (silent CPU fallback); misses before={} after={}",
7411 stats_before.total_misses(),
7412 stats_after.total_misses()
7413 );
7414 }
7415
7416 #[test]
7422 #[cfg(feature = "opengl")]
7423 fn test_nv12_to_planar_f16_fused_engine_vs_cpu() {
7424 let mut gl = match ImageProcessor::with_config(ImageProcessorConfig {
7425 backend: ComputeBackend::OpenGl,
7426 ..Default::default()
7427 }) {
7428 Ok(p) if p.opengl.is_some() => p,
7429 _ => {
7430 eprintln!("SKIPPED: {} — GL backend unavailable", function!());
7431 return;
7432 }
7433 };
7434 if !gl
7435 .opengl
7436 .as_ref()
7437 .map(|g| g.supported_render_dtypes().f16)
7438 .unwrap_or(false)
7439 {
7440 eprintln!("SKIPPED: {} — no F16 render support", function!());
7441 return;
7442 }
7443 let mem = if edgefirst_tensor::is_gpu_buffer_available() {
7444 TensorMemory::Dma
7445 } else {
7446 eprintln!("SKIPPED: {} — no zero-copy buffers", function!());
7447 return;
7448 };
7449
7450 let src = TensorDyn::image(
7451 1280,
7452 720,
7453 PixelFormat::Nv12,
7454 DType::U8,
7455 Some(mem),
7456 edgefirst_tensor::CpuAccess::ReadWrite,
7457 )
7458 .unwrap();
7459 {
7460 let t = src.as_u8().unwrap();
7466 let mut m = t.map().unwrap();
7467 let buf = m.as_mut_slice();
7468 let (w, h) = (1280usize, 720usize);
7469 for y in 0..h {
7470 for x in 0..w {
7471 buf[y * w + x] = ((x * 255) / w) as u8; }
7473 }
7474 for y in 0..(h / 2) {
7475 for x in 0..(w / 2) {
7476 let o = h * w + y * w + 2 * x;
7477 buf[o] = ((y * 255) / (h / 2)) as u8; buf[o + 1] = (((x + y) * 255) / (w / 2 + h / 2)) as u8; }
7480 }
7481 }
7482 let crop = Crop::letterbox([114, 114, 114, 255]);
7483 let mut gl_dst = TensorDyn::image(
7484 640,
7485 640,
7486 PixelFormat::PlanarRgb,
7487 DType::F16,
7488 Some(mem),
7489 edgefirst_tensor::CpuAccess::ReadWrite,
7490 )
7491 .unwrap();
7492 gl.opengl
7496 .as_mut()
7497 .expect("GL backend present")
7498 .convert(&src, &mut gl_dst, Rotation::None, Flip::None, crop)
7499 .expect("fused NV12→PlanarF16 GL convert");
7500
7501 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
7502 backend: ComputeBackend::Cpu,
7503 ..Default::default()
7504 })
7505 .unwrap();
7506 let mut cpu_dst = TensorDyn::image(
7507 640,
7508 640,
7509 PixelFormat::PlanarRgb,
7510 DType::F16,
7511 Some(TensorMemory::Mem),
7512 edgefirst_tensor::CpuAccess::ReadWrite,
7513 )
7514 .unwrap();
7515 cpu.convert(&src, &mut cpu_dst, Rotation::None, Flip::None, crop)
7516 .expect("CPU reference convert");
7517
7518 let g = gl_dst.as_f16().unwrap().map().unwrap().as_slice().to_vec();
7519 let c = cpu_dst.as_f16().unwrap().map().unwrap().as_slice().to_vec();
7520 assert_eq!(g.len(), c.len());
7521 let mut max_diff = 0.0f32;
7522 let mut max_at = 0usize;
7523 for (i, (a, b)) in g.iter().zip(c.iter()).enumerate() {
7524 let d = (a.to_f32() - b.to_f32()).abs();
7525 if d > max_diff {
7526 max_diff = d;
7527 max_at = i;
7528 }
7529 }
7530 let (plane, rem) = (max_at / (640 * 640), max_at % (640 * 640));
7532 let (row, col) = (rem / 640, rem % 640);
7533 eprintln!(
7534 "fused-vs-cpu: max_diff={max_diff} at plane={plane} row={row} col={col} \
7535 gl={} cpu={}",
7536 g[max_at].to_f32(),
7537 c[max_at].to_f32()
7538 );
7539 assert!(
7542 max_diff <= 4.0 / 255.0 + 1e-3,
7543 "fused NV12→PlanarF16 diverges from CPU reference: max_diff={max_diff}"
7544 );
7545 }
7546
7547 #[test]
7557 #[cfg(feature = "opengl")]
7558 fn test_zero_copy_src_to_mem_dst_gl_direct() {
7559 let mut proc = match ImageProcessor::new() {
7560 Ok(p) if p.opengl.is_some() => p,
7561 _ => {
7562 eprintln!("SKIPPED: {} — GL backend unavailable", function!());
7563 return;
7564 }
7565 };
7566 if !edgefirst_tensor::is_gpu_buffer_available() {
7567 eprintln!("SKIPPED: {} — no zero-copy buffers", function!());
7568 return;
7569 }
7570
7571 let src = TensorDyn::image(
7572 1280,
7573 720,
7574 PixelFormat::Rgba,
7575 DType::U8,
7576 Some(TensorMemory::Dma),
7577 edgefirst_tensor::CpuAccess::ReadWrite,
7578 )
7579 .unwrap();
7580 {
7581 let t = src.as_u8().unwrap();
7582 let mut m = t.map().unwrap();
7583 for (i, b) in m.as_mut_slice().iter_mut().enumerate() {
7584 *b = ((i * 31) % 211) as u8;
7585 }
7586 }
7587 let mut gl_dst = TensorDyn::image(
7588 1280,
7589 720,
7590 PixelFormat::Bgra,
7591 DType::U8,
7592 Some(TensorMemory::Mem),
7593 edgefirst_tensor::CpuAccess::ReadWrite,
7594 )
7595 .unwrap();
7596 proc.opengl
7597 .as_mut()
7598 .expect("GL backend present")
7599 .convert(
7600 &src,
7601 &mut gl_dst,
7602 Rotation::None,
7603 Flip::None,
7604 Crop::no_crop(),
7605 )
7606 .expect("zero-copy src → heap dst GL convert");
7607
7608 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
7609 backend: ComputeBackend::Cpu,
7610 ..Default::default()
7611 })
7612 .unwrap();
7613 let mut cpu_dst = TensorDyn::image(
7614 1280,
7615 720,
7616 PixelFormat::Bgra,
7617 DType::U8,
7618 Some(TensorMemory::Mem),
7619 edgefirst_tensor::CpuAccess::ReadWrite,
7620 )
7621 .unwrap();
7622 cpu.convert(
7623 &src,
7624 &mut cpu_dst,
7625 Rotation::None,
7626 Flip::None,
7627 Crop::no_crop(),
7628 )
7629 .expect("CPU reference convert");
7630
7631 let g = gl_dst.as_u8().unwrap().map().unwrap().as_slice().to_vec();
7632 let c = cpu_dst.as_u8().unwrap().map().unwrap().as_slice().to_vec();
7633 assert_eq!(g.len(), c.len());
7634 let max_diff = g
7635 .iter()
7636 .zip(c.iter())
7637 .map(|(a, b)| a.abs_diff(*b))
7638 .max()
7639 .unwrap();
7640 assert!(
7643 max_diff <= 2,
7644 "zero-copy src → heap dst diverges from CPU reference: max_diff={max_diff}"
7645 );
7646 }
7647
7648 #[test]
7649 #[cfg(target_os = "linux")]
7650 fn test_yuyv_to_rgb_g2d() {
7651 if !is_g2d_available() {
7652 eprintln!("SKIPPED: test_yuyv_to_rgb_g2d - G2D library (libg2d.so.2) not available");
7653 return;
7654 }
7655 if !is_dma_available() {
7656 eprintln!(
7657 "SKIPPED: test_yuyv_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
7658 );
7659 return;
7660 }
7661
7662 let src = load_bytes_to_tensor(
7663 1280,
7664 720,
7665 PixelFormat::Yuyv,
7666 None,
7667 &edgefirst_bench::testdata::read("camera720p.yuyv"),
7668 )
7669 .unwrap();
7670
7671 let g2d_dst = TensorDyn::image(
7672 1280,
7673 720,
7674 PixelFormat::Rgb,
7675 DType::U8,
7676 Some(TensorMemory::Dma),
7677 edgefirst_tensor::CpuAccess::ReadWrite,
7678 )
7679 .unwrap();
7680 let mut g2d_converter = G2DProcessor::new().unwrap();
7681
7682 let (result, src, g2d_dst) = convert_img(
7683 &mut g2d_converter,
7684 src,
7685 g2d_dst,
7686 Rotation::None,
7687 Flip::None,
7688 Crop::no_crop(),
7689 );
7690 result.unwrap();
7691
7692 let cpu_dst = TensorDyn::image(
7693 1280,
7694 720,
7695 PixelFormat::Rgb,
7696 DType::U8,
7697 None,
7698 edgefirst_tensor::CpuAccess::ReadWrite,
7699 )
7700 .unwrap();
7701 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
7702
7703 let (result, _src, cpu_dst) = convert_img(
7704 &mut cpu_converter,
7705 src,
7706 cpu_dst,
7707 Rotation::None,
7708 Flip::None,
7709 Crop::no_crop(),
7710 );
7711 result.unwrap();
7712
7713 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
7719 }
7720
7721 #[test]
7722 #[cfg(target_os = "linux")]
7723 fn test_yuyv_to_yuyv_resize_g2d() {
7724 if !is_g2d_available() {
7725 eprintln!(
7726 "SKIPPED: test_yuyv_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available"
7727 );
7728 return;
7729 }
7730 if !is_dma_available() {
7731 eprintln!(
7732 "SKIPPED: test_yuyv_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
7733 );
7734 return;
7735 }
7736
7737 let src = load_bytes_to_tensor(
7738 1280,
7739 720,
7740 PixelFormat::Yuyv,
7741 None,
7742 &edgefirst_bench::testdata::read("camera720p.yuyv"),
7743 )
7744 .unwrap();
7745
7746 let g2d_dst = TensorDyn::image(
7747 600,
7748 400,
7749 PixelFormat::Yuyv,
7750 DType::U8,
7751 Some(TensorMemory::Dma),
7752 edgefirst_tensor::CpuAccess::ReadWrite,
7753 )
7754 .unwrap();
7755 let mut g2d_converter = G2DProcessor::new().unwrap();
7756
7757 let (result, src, g2d_dst) = convert_img(
7758 &mut g2d_converter,
7759 src,
7760 g2d_dst,
7761 Rotation::None,
7762 Flip::None,
7763 Crop::no_crop(),
7764 );
7765 result.unwrap();
7766
7767 let cpu_dst = TensorDyn::image(
7768 600,
7769 400,
7770 PixelFormat::Yuyv,
7771 DType::U8,
7772 None,
7773 edgefirst_tensor::CpuAccess::ReadWrite,
7774 )
7775 .unwrap();
7776 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
7777
7778 let (result, _src, cpu_dst) = convert_img(
7779 &mut cpu_converter,
7780 src,
7781 cpu_dst,
7782 Rotation::None,
7783 Flip::None,
7784 Crop::no_crop(),
7785 );
7786 result.unwrap();
7787
7788 eprintln!(
7795 "WARNING: G2D has poor colorimetry support — YUYV resize diverges from the \
7796 CPU reference (~0.85 similarity); threshold held at 0.85, not 0.95."
7797 );
7798 compare_images_convert_to_rgb(&g2d_dst, &cpu_dst, 0.85, function!());
7799 }
7800
7801 #[test]
7802 fn test_yuyv_to_rgba_resize_cpu() {
7803 let src = load_bytes_to_tensor(
7804 1280,
7805 720,
7806 PixelFormat::Yuyv,
7807 None,
7808 &edgefirst_bench::testdata::read("camera720p.yuyv"),
7809 )
7810 .unwrap();
7811
7812 let (dst_width, dst_height) = (960, 540);
7813
7814 let dst = TensorDyn::image(
7815 dst_width,
7816 dst_height,
7817 PixelFormat::Rgba,
7818 DType::U8,
7819 None,
7820 edgefirst_tensor::CpuAccess::ReadWrite,
7821 )
7822 .unwrap();
7823 let mut cpu_converter = CPUProcessor::new();
7824
7825 let (result, _src, dst) = convert_img(
7826 &mut cpu_converter,
7827 src,
7828 dst,
7829 Rotation::None,
7830 Flip::None,
7831 Crop::no_crop(),
7832 );
7833 result.unwrap();
7834
7835 let dst_target = TensorDyn::image(
7836 dst_width,
7837 dst_height,
7838 PixelFormat::Rgba,
7839 DType::U8,
7840 None,
7841 edgefirst_tensor::CpuAccess::ReadWrite,
7842 )
7843 .unwrap();
7844 let src_target = load_bytes_to_tensor(
7845 1280,
7846 720,
7847 PixelFormat::Rgba,
7848 None,
7849 &edgefirst_bench::testdata::read("camera720p.rgba"),
7850 )
7851 .unwrap();
7852 let (result, _src_target, dst_target) = convert_img(
7853 &mut cpu_converter,
7854 src_target,
7855 dst_target,
7856 Rotation::None,
7857 Flip::None,
7858 Crop::no_crop(),
7859 );
7860 result.unwrap();
7861
7862 compare_images(&dst, &dst_target, 0.98, function!());
7865 }
7866
7867 #[test]
7868 #[cfg(target_os = "linux")]
7869 fn test_yuyv_to_rgba_crop_flip_g2d() {
7870 if !is_g2d_available() {
7871 eprintln!(
7872 "SKIPPED: test_yuyv_to_rgba_crop_flip_g2d - G2D library (libg2d.so.2) not available"
7873 );
7874 return;
7875 }
7876 if !is_dma_available() {
7877 eprintln!(
7878 "SKIPPED: test_yuyv_to_rgba_crop_flip_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
7879 );
7880 return;
7881 }
7882
7883 let src = load_bytes_to_tensor(
7884 1280,
7885 720,
7886 PixelFormat::Yuyv,
7887 Some(TensorMemory::Dma),
7888 &edgefirst_bench::testdata::read("camera720p.yuyv"),
7889 )
7890 .unwrap();
7891
7892 let (dst_width, dst_height) = (640, 640);
7893
7894 let dst_g2d = TensorDyn::image(
7895 dst_width,
7896 dst_height,
7897 PixelFormat::Rgba,
7898 DType::U8,
7899 Some(TensorMemory::Dma),
7900 edgefirst_tensor::CpuAccess::ReadWrite,
7901 )
7902 .unwrap();
7903 let mut g2d_converter = G2DProcessor::new().unwrap();
7904 let crop = Crop::new().with_source(Some(Region::new(20, 15, 400, 300)));
7905
7906 let (result, src, dst_g2d) = convert_img(
7907 &mut g2d_converter,
7908 src,
7909 dst_g2d,
7910 Rotation::None,
7911 Flip::Horizontal,
7912 crop,
7913 );
7914 result.unwrap();
7915
7916 let dst_cpu = TensorDyn::image(
7917 dst_width,
7918 dst_height,
7919 PixelFormat::Rgba,
7920 DType::U8,
7921 Some(TensorMemory::Dma),
7922 edgefirst_tensor::CpuAccess::ReadWrite,
7923 )
7924 .unwrap();
7925 let mut cpu_converter = CPUProcessor::new();
7926
7927 let (result, _src, dst_cpu) = convert_img(
7928 &mut cpu_converter,
7929 src,
7930 dst_cpu,
7931 Rotation::None,
7932 Flip::Horizontal,
7933 crop,
7934 );
7935 result.unwrap();
7936 compare_images(&dst_g2d, &dst_cpu, 0.98, function!());
7942 }
7943
7944 #[test]
7945 #[cfg(target_os = "linux")]
7946 #[cfg(feature = "opengl")]
7947 fn test_yuyv_to_rgba_crop_flip_opengl() {
7948 if !is_opengl_available() {
7949 eprintln!("SKIPPED: {} - OpenGL not available", function!());
7950 return;
7951 }
7952
7953 if !is_dma_available() {
7954 eprintln!(
7955 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
7956 function!()
7957 );
7958 return;
7959 }
7960
7961 let src = load_bytes_to_tensor(
7962 1280,
7963 720,
7964 PixelFormat::Yuyv,
7965 Some(TensorMemory::Dma),
7966 &edgefirst_bench::testdata::read("camera720p.yuyv"),
7967 )
7968 .unwrap();
7969
7970 let (dst_width, dst_height) = (640, 640);
7971
7972 let dst_gl = TensorDyn::image(
7973 dst_width,
7974 dst_height,
7975 PixelFormat::Rgba,
7976 DType::U8,
7977 Some(TensorMemory::Dma),
7978 edgefirst_tensor::CpuAccess::ReadWrite,
7979 )
7980 .unwrap();
7981 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
7982 let crop = Crop::new().with_source(Some(Region::new(20, 15, 400, 300)));
7983
7984 let (result, src, dst_gl) = convert_img(
7985 &mut gl_converter,
7986 src,
7987 dst_gl,
7988 Rotation::None,
7989 Flip::Horizontal,
7990 crop,
7991 );
7992 result.unwrap();
7993
7994 let dst_cpu = TensorDyn::image(
7995 dst_width,
7996 dst_height,
7997 PixelFormat::Rgba,
7998 DType::U8,
7999 Some(TensorMemory::Dma),
8000 edgefirst_tensor::CpuAccess::ReadWrite,
8001 )
8002 .unwrap();
8003 let mut cpu_converter = CPUProcessor::new();
8004
8005 let (result, _src, dst_cpu) = convert_img(
8006 &mut cpu_converter,
8007 src,
8008 dst_cpu,
8009 Rotation::None,
8010 Flip::Horizontal,
8011 crop,
8012 );
8013 result.unwrap();
8014 compare_images(&dst_gl, &dst_cpu, 0.98, function!());
8019 }
8020
8021 #[test]
8022 fn test_vyuy_to_rgba_cpu() {
8023 let file = edgefirst_bench::testdata::read("camera720p.vyuy").to_vec();
8024 let src = TensorDyn::image(
8025 1280,
8026 720,
8027 PixelFormat::Vyuy,
8028 DType::U8,
8029 None,
8030 edgefirst_tensor::CpuAccess::ReadWrite,
8031 )
8032 .unwrap();
8033 src.as_u8()
8034 .unwrap()
8035 .map()
8036 .unwrap()
8037 .as_mut_slice()
8038 .copy_from_slice(&file);
8039
8040 let dst = TensorDyn::image(
8041 1280,
8042 720,
8043 PixelFormat::Rgba,
8044 DType::U8,
8045 None,
8046 edgefirst_tensor::CpuAccess::ReadWrite,
8047 )
8048 .unwrap();
8049 let mut cpu_converter = CPUProcessor::new();
8050
8051 let (result, _src, dst) = convert_img(
8052 &mut cpu_converter,
8053 src,
8054 dst,
8055 Rotation::None,
8056 Flip::None,
8057 Crop::no_crop(),
8058 );
8059 result.unwrap();
8060
8061 let target_image = TensorDyn::image(
8062 1280,
8063 720,
8064 PixelFormat::Rgba,
8065 DType::U8,
8066 None,
8067 edgefirst_tensor::CpuAccess::ReadWrite,
8068 )
8069 .unwrap();
8070 target_image
8071 .as_u8()
8072 .unwrap()
8073 .map()
8074 .unwrap()
8075 .as_mut_slice()
8076 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
8077
8078 compare_images(&dst, &target_image, 0.98, function!());
8081 }
8082
8083 #[test]
8084 fn test_vyuy_to_rgb_cpu() {
8085 let file = edgefirst_bench::testdata::read("camera720p.vyuy").to_vec();
8086 let src = TensorDyn::image(
8087 1280,
8088 720,
8089 PixelFormat::Vyuy,
8090 DType::U8,
8091 None,
8092 edgefirst_tensor::CpuAccess::ReadWrite,
8093 )
8094 .unwrap();
8095 src.as_u8()
8096 .unwrap()
8097 .map()
8098 .unwrap()
8099 .as_mut_slice()
8100 .copy_from_slice(&file);
8101
8102 let dst = TensorDyn::image(
8103 1280,
8104 720,
8105 PixelFormat::Rgb,
8106 DType::U8,
8107 None,
8108 edgefirst_tensor::CpuAccess::ReadWrite,
8109 )
8110 .unwrap();
8111 let mut cpu_converter = CPUProcessor::new();
8112
8113 let (result, _src, dst) = convert_img(
8114 &mut cpu_converter,
8115 src,
8116 dst,
8117 Rotation::None,
8118 Flip::None,
8119 Crop::no_crop(),
8120 );
8121 result.unwrap();
8122
8123 let target_image = TensorDyn::image(
8124 1280,
8125 720,
8126 PixelFormat::Rgb,
8127 DType::U8,
8128 None,
8129 edgefirst_tensor::CpuAccess::ReadWrite,
8130 )
8131 .unwrap();
8132 target_image
8133 .as_u8()
8134 .unwrap()
8135 .map()
8136 .unwrap()
8137 .as_mut_slice()
8138 .as_chunks_mut::<3>()
8139 .0
8140 .iter_mut()
8141 .zip(
8142 edgefirst_bench::testdata::read("camera720p.rgba")
8143 .as_chunks::<4>()
8144 .0,
8145 )
8146 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
8147
8148 compare_images(&dst, &target_image, 0.98, function!());
8151 }
8152
8153 #[test]
8154 #[cfg(target_os = "linux")]
8155 #[ignore = "G2D does not support VYUY; re-enable when hardware support is added"]
8156 fn test_vyuy_to_rgba_g2d() {
8157 if !is_g2d_available() {
8158 eprintln!("SKIPPED: test_vyuy_to_rgba_g2d - G2D library (libg2d.so.2) not available");
8159 return;
8160 }
8161 if !is_dma_available() {
8162 eprintln!(
8163 "SKIPPED: test_vyuy_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
8164 );
8165 return;
8166 }
8167
8168 let src = load_bytes_to_tensor(
8169 1280,
8170 720,
8171 PixelFormat::Vyuy,
8172 None,
8173 &edgefirst_bench::testdata::read("camera720p.vyuy"),
8174 )
8175 .unwrap();
8176
8177 let dst = TensorDyn::image(
8178 1280,
8179 720,
8180 PixelFormat::Rgba,
8181 DType::U8,
8182 Some(TensorMemory::Dma),
8183 edgefirst_tensor::CpuAccess::ReadWrite,
8184 )
8185 .unwrap();
8186 let mut g2d_converter = G2DProcessor::new().unwrap();
8187
8188 let (result, _src, dst) = convert_img(
8189 &mut g2d_converter,
8190 src,
8191 dst,
8192 Rotation::None,
8193 Flip::None,
8194 Crop::no_crop(),
8195 );
8196 match result {
8197 Err(Error::G2D(_)) => {
8198 eprintln!("SKIPPED: test_vyuy_to_rgba_g2d - G2D does not support PixelFormat::Vyuy format");
8199 return;
8200 }
8201 r => r.unwrap(),
8202 }
8203
8204 let target_image = TensorDyn::image(
8205 1280,
8206 720,
8207 PixelFormat::Rgba,
8208 DType::U8,
8209 None,
8210 edgefirst_tensor::CpuAccess::ReadWrite,
8211 )
8212 .unwrap();
8213 target_image
8214 .as_u8()
8215 .unwrap()
8216 .map()
8217 .unwrap()
8218 .as_mut_slice()
8219 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
8220
8221 compare_images(&dst, &target_image, 0.98, function!());
8225 }
8226
8227 #[test]
8228 #[cfg(target_os = "linux")]
8229 #[ignore = "G2D does not support VYUY; re-enable when hardware support is added"]
8230 fn test_vyuy_to_rgb_g2d() {
8231 if !is_g2d_available() {
8232 eprintln!("SKIPPED: test_vyuy_to_rgb_g2d - G2D library (libg2d.so.2) not available");
8233 return;
8234 }
8235 if !is_dma_available() {
8236 eprintln!(
8237 "SKIPPED: test_vyuy_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
8238 );
8239 return;
8240 }
8241
8242 let src = load_bytes_to_tensor(
8243 1280,
8244 720,
8245 PixelFormat::Vyuy,
8246 None,
8247 &edgefirst_bench::testdata::read("camera720p.vyuy"),
8248 )
8249 .unwrap();
8250
8251 let g2d_dst = TensorDyn::image(
8252 1280,
8253 720,
8254 PixelFormat::Rgb,
8255 DType::U8,
8256 Some(TensorMemory::Dma),
8257 edgefirst_tensor::CpuAccess::ReadWrite,
8258 )
8259 .unwrap();
8260 let mut g2d_converter = G2DProcessor::new().unwrap();
8261
8262 let (result, src, g2d_dst) = convert_img(
8263 &mut g2d_converter,
8264 src,
8265 g2d_dst,
8266 Rotation::None,
8267 Flip::None,
8268 Crop::no_crop(),
8269 );
8270 match result {
8271 Err(Error::G2D(_)) => {
8272 eprintln!(
8273 "SKIPPED: test_vyuy_to_rgb_g2d - G2D does not support PixelFormat::Vyuy format"
8274 );
8275 return;
8276 }
8277 r => r.unwrap(),
8278 }
8279
8280 let cpu_dst = TensorDyn::image(
8281 1280,
8282 720,
8283 PixelFormat::Rgb,
8284 DType::U8,
8285 None,
8286 edgefirst_tensor::CpuAccess::ReadWrite,
8287 )
8288 .unwrap();
8289 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
8290
8291 let (result, _src, cpu_dst) = convert_img(
8292 &mut cpu_converter,
8293 src,
8294 cpu_dst,
8295 Rotation::None,
8296 Flip::None,
8297 Crop::no_crop(),
8298 );
8299 result.unwrap();
8300
8301 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
8307 }
8308
8309 #[test]
8310 #[cfg(target_os = "linux")]
8311 #[cfg(feature = "opengl")]
8312 fn test_vyuy_to_rgba_opengl() {
8313 if !is_opengl_available() {
8314 eprintln!("SKIPPED: {} - OpenGL not available", function!());
8315 return;
8316 }
8317 if !is_dma_available() {
8318 eprintln!(
8319 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
8320 function!()
8321 );
8322 return;
8323 }
8324
8325 let src = load_bytes_to_tensor(
8326 1280,
8327 720,
8328 PixelFormat::Vyuy,
8329 Some(TensorMemory::Dma),
8330 &edgefirst_bench::testdata::read("camera720p.vyuy"),
8331 )
8332 .unwrap();
8333
8334 let dst = TensorDyn::image(
8335 1280,
8336 720,
8337 PixelFormat::Rgba,
8338 DType::U8,
8339 Some(TensorMemory::Dma),
8340 edgefirst_tensor::CpuAccess::ReadWrite,
8341 )
8342 .unwrap();
8343 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
8344
8345 let (result, _src, dst) = convert_img(
8346 &mut gl_converter,
8347 src,
8348 dst,
8349 Rotation::None,
8350 Flip::None,
8351 Crop::no_crop(),
8352 );
8353 match result {
8354 Err(Error::NotSupported(_)) => {
8355 eprintln!(
8356 "SKIPPED: {} - OpenGL does not support PixelFormat::Vyuy DMA format",
8357 function!()
8358 );
8359 return;
8360 }
8361 r => r.unwrap(),
8362 }
8363
8364 let target_image = TensorDyn::image(
8365 1280,
8366 720,
8367 PixelFormat::Rgba,
8368 DType::U8,
8369 None,
8370 edgefirst_tensor::CpuAccess::ReadWrite,
8371 )
8372 .unwrap();
8373 target_image
8374 .as_u8()
8375 .unwrap()
8376 .map()
8377 .unwrap()
8378 .as_mut_slice()
8379 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
8380
8381 compare_images(&dst, &target_image, 0.98, function!());
8385 }
8386
8387 #[test]
8388 fn test_nv12_to_rgba_cpu() {
8389 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
8390 let src = TensorDyn::image(
8391 1280,
8392 720,
8393 PixelFormat::Nv12,
8394 DType::U8,
8395 None,
8396 edgefirst_tensor::CpuAccess::ReadWrite,
8397 )
8398 .unwrap();
8399 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
8400 .copy_from_slice(&file);
8401
8402 let dst = TensorDyn::image(
8403 1280,
8404 720,
8405 PixelFormat::Rgba,
8406 DType::U8,
8407 None,
8408 edgefirst_tensor::CpuAccess::ReadWrite,
8409 )
8410 .unwrap();
8411 let mut cpu_converter = CPUProcessor::new();
8412
8413 let (result, _src, dst) = convert_img(
8414 &mut cpu_converter,
8415 src,
8416 dst,
8417 Rotation::None,
8418 Flip::None,
8419 Crop::no_crop(),
8420 );
8421 result.unwrap();
8422
8423 let target_image = crate::load_image_test_helper(
8424 &edgefirst_bench::testdata::read("zidane.jpg"),
8425 Some(PixelFormat::Rgba),
8426 None,
8427 )
8428 .unwrap();
8429
8430 compare_images(&dst, &target_image, 0.95, function!());
8435 }
8436
8437 #[test]
8438 fn test_nv12_odd_height_to_rgb_cpu() {
8439 let mut src = TensorDyn::image(
8450 8,
8451 5,
8452 PixelFormat::Nv12,
8453 DType::U8,
8454 Some(TensorMemory::Mem),
8455 edgefirst_tensor::CpuAccess::ReadWrite,
8456 )
8457 .unwrap();
8458 assert_eq!(src.shape(), &[8, 8]);
8459 assert_eq!((src.width(), src.height()), (Some(8), Some(5)));
8460 src.as_u8().unwrap().map().unwrap().as_mut_slice().fill(128);
8461 src.set_colorimetry(Some(
8465 edgefirst_tensor::Colorimetry::default()
8466 .with_encoding(edgefirst_tensor::ColorEncoding::Bt601)
8467 .with_range(edgefirst_tensor::ColorRange::Full),
8468 ));
8469
8470 let dst = TensorDyn::image(
8471 8,
8472 5,
8473 PixelFormat::Rgb,
8474 DType::U8,
8475 Some(TensorMemory::Mem),
8476 edgefirst_tensor::CpuAccess::ReadWrite,
8477 )
8478 .unwrap();
8479 let mut cpu_converter = CPUProcessor::new();
8480 let (result, _src, dst) = convert_img(
8481 &mut cpu_converter,
8482 src,
8483 dst,
8484 Rotation::None,
8485 Flip::None,
8486 Crop::no_crop(),
8487 );
8488 result.unwrap();
8489
8490 assert_eq!((dst.width(), dst.height()), (Some(8), Some(5)));
8491 let map = dst.as_u8().unwrap().map().unwrap();
8492 for (i, &b) in map.as_slice().iter().enumerate() {
8493 assert!(
8494 (b as i16 - 128).abs() <= 2,
8495 "pixel byte {i} = {b}, expected ~128 for neutral-grey NV12"
8496 );
8497 }
8498 }
8499
8500 #[test]
8501 fn test_nv24_to_rgb_cpu() {
8502 let mut src = TensorDyn::image(
8508 8,
8509 4,
8510 PixelFormat::Nv24,
8511 DType::U8,
8512 Some(TensorMemory::Mem),
8513 edgefirst_tensor::CpuAccess::ReadWrite,
8514 )
8515 .unwrap();
8516 assert_eq!(src.shape(), &[12, 8]);
8517 assert_eq!((src.width(), src.height()), (Some(8), Some(4)));
8518 src.as_u8().unwrap().map().unwrap().as_mut_slice().fill(128);
8519 src.set_colorimetry(Some(
8522 edgefirst_tensor::Colorimetry::default()
8523 .with_encoding(edgefirst_tensor::ColorEncoding::Bt601)
8524 .with_range(edgefirst_tensor::ColorRange::Full),
8525 ));
8526
8527 let dst = TensorDyn::image(
8528 8,
8529 4,
8530 PixelFormat::Rgb,
8531 DType::U8,
8532 Some(TensorMemory::Mem),
8533 edgefirst_tensor::CpuAccess::ReadWrite,
8534 )
8535 .unwrap();
8536 let mut cpu_converter = CPUProcessor::new();
8537 let (result, _src, dst) = convert_img(
8538 &mut cpu_converter,
8539 src,
8540 dst,
8541 Rotation::None,
8542 Flip::None,
8543 Crop::no_crop(),
8544 );
8545 result.unwrap();
8546
8547 assert_eq!((dst.width(), dst.height()), (Some(8), Some(4)));
8548 let map = dst.as_u8().unwrap().map().unwrap();
8549 for (i, &b) in map.as_slice().iter().enumerate() {
8550 assert!(
8551 (b as i16 - 128).abs() <= 2,
8552 "pixel byte {i} = {b}, expected ~128 for neutral-grey NV24"
8553 );
8554 }
8555 }
8556
8557 #[test]
8558 fn cpu_nv12_to_rgb_respects_tagged_bt2020() {
8559 fn decode_tagged(enc: edgefirst_tensor::ColorEncoding) -> [u8; 3] {
8567 let mut src = TensorDyn::image(
8568 8,
8569 4,
8570 PixelFormat::Nv12,
8571 DType::U8,
8572 Some(TensorMemory::Mem),
8573 edgefirst_tensor::CpuAccess::ReadWrite,
8574 )
8575 .unwrap();
8576 assert_eq!(src.shape(), &[6, 8]);
8578 {
8579 let mut map = src.as_u8().unwrap().map().unwrap();
8580 let buf = map.as_mut_slice();
8581 buf[..32].fill(120); for px in buf[32..].chunks_exact_mut(2) {
8583 px[0] = 180; px[1] = 64; }
8586 }
8587 src.set_colorimetry(Some(
8590 edgefirst_tensor::Colorimetry::default()
8591 .with_encoding(enc)
8592 .with_range(edgefirst_tensor::ColorRange::Limited),
8593 ));
8594 let dst = TensorDyn::image(
8595 8,
8596 4,
8597 PixelFormat::Rgb,
8598 DType::U8,
8599 Some(TensorMemory::Mem),
8600 edgefirst_tensor::CpuAccess::ReadWrite,
8601 )
8602 .unwrap();
8603 let mut cpu = CPUProcessor::new();
8604 let (result, _src, dst) = convert_img(
8605 &mut cpu,
8606 src,
8607 dst,
8608 Rotation::None,
8609 Flip::None,
8610 Crop::no_crop(),
8611 );
8612 result.unwrap();
8613 let map = dst.as_u8().unwrap().map().unwrap();
8614 let s = map.as_slice();
8615 [s[0], s[1], s[2]]
8616 }
8617
8618 let bt601 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt601);
8619 let bt709 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt709);
8620 let bt2020 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt2020);
8621
8622 assert_ne!(
8623 bt2020, bt601,
8624 "BT.2020 must decode differently from BT.601 ({bt2020:?} vs {bt601:?})"
8625 );
8626 assert_ne!(
8627 bt2020, bt709,
8628 "BT.2020 must decode differently from BT.709 ({bt2020:?} vs {bt709:?})"
8629 );
8630 assert_ne!(
8631 bt601, bt709,
8632 "BT.601 must decode differently from BT.709 ({bt601:?} vs {bt709:?})"
8633 );
8634 }
8635
8636 #[test]
8637 fn test_nv12_to_rgb_cpu() {
8638 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
8639 let src = TensorDyn::image(
8640 1280,
8641 720,
8642 PixelFormat::Nv12,
8643 DType::U8,
8644 None,
8645 edgefirst_tensor::CpuAccess::ReadWrite,
8646 )
8647 .unwrap();
8648 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
8649 .copy_from_slice(&file);
8650
8651 let dst = TensorDyn::image(
8652 1280,
8653 720,
8654 PixelFormat::Rgb,
8655 DType::U8,
8656 None,
8657 edgefirst_tensor::CpuAccess::ReadWrite,
8658 )
8659 .unwrap();
8660 let mut cpu_converter = CPUProcessor::new();
8661
8662 let (result, _src, dst) = convert_img(
8663 &mut cpu_converter,
8664 src,
8665 dst,
8666 Rotation::None,
8667 Flip::None,
8668 Crop::no_crop(),
8669 );
8670 result.unwrap();
8671
8672 let target_image = crate::load_image_test_helper(
8673 &edgefirst_bench::testdata::read("zidane.jpg"),
8674 Some(PixelFormat::Rgb),
8675 None,
8676 )
8677 .unwrap();
8678
8679 compare_images(&dst, &target_image, 0.95, function!());
8684 }
8685
8686 #[test]
8687 fn test_nv12_to_grey_cpu() {
8688 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
8689 let src = TensorDyn::image(
8690 1280,
8691 720,
8692 PixelFormat::Nv12,
8693 DType::U8,
8694 None,
8695 edgefirst_tensor::CpuAccess::ReadWrite,
8696 )
8697 .unwrap();
8698 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
8699 .copy_from_slice(&file);
8700
8701 let dst = TensorDyn::image(
8702 1280,
8703 720,
8704 PixelFormat::Grey,
8705 DType::U8,
8706 None,
8707 edgefirst_tensor::CpuAccess::ReadWrite,
8708 )
8709 .unwrap();
8710 let mut cpu_converter = CPUProcessor::new();
8711
8712 let (result, _src, dst) = convert_img(
8713 &mut cpu_converter,
8714 src,
8715 dst,
8716 Rotation::None,
8717 Flip::None,
8718 Crop::no_crop(),
8719 );
8720 result.unwrap();
8721
8722 let target_image = crate::load_image_test_helper(
8723 &edgefirst_bench::testdata::read("zidane.jpg"),
8724 Some(PixelFormat::Grey),
8725 None,
8726 )
8727 .unwrap();
8728
8729 compare_images(&dst, &target_image, 0.95, function!());
8734 }
8735
8736 #[test]
8737 fn test_nv12_to_yuyv_cpu() {
8738 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
8739 let src = TensorDyn::image(
8740 1280,
8741 720,
8742 PixelFormat::Nv12,
8743 DType::U8,
8744 None,
8745 edgefirst_tensor::CpuAccess::ReadWrite,
8746 )
8747 .unwrap();
8748 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
8749 .copy_from_slice(&file);
8750
8751 let dst = TensorDyn::image(
8752 1280,
8753 720,
8754 PixelFormat::Yuyv,
8755 DType::U8,
8756 None,
8757 edgefirst_tensor::CpuAccess::ReadWrite,
8758 )
8759 .unwrap();
8760 let mut cpu_converter = CPUProcessor::new();
8761
8762 let (result, _src, dst) = convert_img(
8763 &mut cpu_converter,
8764 src,
8765 dst,
8766 Rotation::None,
8767 Flip::None,
8768 Crop::no_crop(),
8769 );
8770 result.unwrap();
8771
8772 let target_image = crate::load_image_test_helper(
8773 &edgefirst_bench::testdata::read("zidane.jpg"),
8774 Some(PixelFormat::Rgb),
8775 None,
8776 )
8777 .unwrap();
8778
8779 compare_images_convert_to_rgb(&dst, &target_image, 0.95, function!());
8784 }
8785
8786 #[test]
8787 fn test_cpu_resize_nv16() {
8788 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
8789 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
8790
8791 let cpu_nv16_dst = TensorDyn::image(
8792 640,
8793 640,
8794 PixelFormat::Nv16,
8795 DType::U8,
8796 None,
8797 edgefirst_tensor::CpuAccess::ReadWrite,
8798 )
8799 .unwrap();
8800 let cpu_rgb_dst = TensorDyn::image(
8801 640,
8802 640,
8803 PixelFormat::Rgb,
8804 DType::U8,
8805 None,
8806 edgefirst_tensor::CpuAccess::ReadWrite,
8807 )
8808 .unwrap();
8809 let mut cpu_converter = CPUProcessor::new();
8810 let crop = Crop::letterbox([255, 128, 0, 255]);
8811
8812 let (result, src, cpu_nv16_dst) = convert_img(
8813 &mut cpu_converter,
8814 src,
8815 cpu_nv16_dst,
8816 Rotation::None,
8817 Flip::None,
8818 crop,
8819 );
8820 result.unwrap();
8821
8822 let (result, _src, cpu_rgb_dst) = convert_img(
8823 &mut cpu_converter,
8824 src,
8825 cpu_rgb_dst,
8826 Rotation::None,
8827 Flip::None,
8828 crop,
8829 );
8830 result.unwrap();
8831 compare_images_convert_to_rgb(&cpu_nv16_dst, &cpu_rgb_dst, 0.99, function!());
8832 }
8833
8834 fn load_bytes_to_tensor(
8835 width: usize,
8836 height: usize,
8837 format: PixelFormat,
8838 memory: Option<TensorMemory>,
8839 bytes: &[u8],
8840 ) -> Result<TensorDyn, Error> {
8841 let src = TensorDyn::image(
8842 width,
8843 height,
8844 format,
8845 DType::U8,
8846 memory,
8847 edgefirst_tensor::CpuAccess::ReadWrite,
8848 )?;
8849 src.as_u8()
8850 .unwrap()
8851 .map()?
8852 .as_mut_slice()
8853 .copy_from_slice(bytes);
8854 Ok(src)
8855 }
8856
8857 fn compare_images(img1: &TensorDyn, img2: &TensorDyn, threshold: f64, name: &str) {
8865 assert_eq!(img1.height(), img2.height(), "Heights differ");
8866 assert_eq!(img1.width(), img2.width(), "Widths differ");
8867 assert_eq!(
8868 img1.format().unwrap(),
8869 img2.format().unwrap(),
8870 "PixelFormat differ"
8871 );
8872 assert!(
8873 matches!(
8874 img1.format().unwrap(),
8875 PixelFormat::Rgb | PixelFormat::Rgba | PixelFormat::Grey | PixelFormat::PlanarRgb
8876 ),
8877 "format must be Rgb or Rgba for comparison"
8878 );
8879
8880 let image1 = match img1.format().unwrap() {
8881 PixelFormat::Rgb => image::RgbImage::from_vec(
8882 img1.width().unwrap() as u32,
8883 img1.height().unwrap() as u32,
8884 img1.as_u8().unwrap().map().unwrap().to_vec(),
8885 )
8886 .unwrap(),
8887 PixelFormat::Rgba => image::RgbaImage::from_vec(
8888 img1.width().unwrap() as u32,
8889 img1.height().unwrap() as u32,
8890 img1.as_u8().unwrap().map().unwrap().to_vec(),
8891 )
8892 .unwrap()
8893 .convert(),
8894 PixelFormat::Grey => image::GrayImage::from_vec(
8895 img1.width().unwrap() as u32,
8896 img1.height().unwrap() as u32,
8897 img1.as_u8().unwrap().map().unwrap().to_vec(),
8898 )
8899 .unwrap()
8900 .convert(),
8901 PixelFormat::PlanarRgb => image::GrayImage::from_vec(
8902 img1.width().unwrap() as u32,
8903 (img1.height().unwrap() * 3) as u32,
8904 img1.as_u8().unwrap().map().unwrap().to_vec(),
8905 )
8906 .unwrap()
8907 .convert(),
8908 _ => return,
8909 };
8910
8911 let image2 = match img2.format().unwrap() {
8912 PixelFormat::Rgb => image::RgbImage::from_vec(
8913 img2.width().unwrap() as u32,
8914 img2.height().unwrap() as u32,
8915 img2.as_u8().unwrap().map().unwrap().to_vec(),
8916 )
8917 .unwrap(),
8918 PixelFormat::Rgba => image::RgbaImage::from_vec(
8919 img2.width().unwrap() as u32,
8920 img2.height().unwrap() as u32,
8921 img2.as_u8().unwrap().map().unwrap().to_vec(),
8922 )
8923 .unwrap()
8924 .convert(),
8925 PixelFormat::Grey => image::GrayImage::from_vec(
8926 img2.width().unwrap() as u32,
8927 img2.height().unwrap() as u32,
8928 img2.as_u8().unwrap().map().unwrap().to_vec(),
8929 )
8930 .unwrap()
8931 .convert(),
8932 PixelFormat::PlanarRgb => image::GrayImage::from_vec(
8933 img2.width().unwrap() as u32,
8934 (img2.height().unwrap() * 3) as u32,
8935 img2.as_u8().unwrap().map().unwrap().to_vec(),
8936 )
8937 .unwrap()
8938 .convert(),
8939 _ => return,
8940 };
8941
8942 let similarity = image_compare::rgb_similarity_structure(
8943 &image_compare::Algorithm::RootMeanSquared,
8944 &image1,
8945 &image2,
8946 )
8947 .expect("Image Comparison failed");
8948 if similarity.score < threshold {
8949 similarity
8952 .image
8953 .to_color_map()
8954 .save(format!("{name}.png"))
8955 .unwrap();
8956 panic!(
8957 "{name}: converted image and target image have similarity score too low: {} < {}",
8958 similarity.score, threshold
8959 )
8960 }
8961 }
8962
8963 fn compare_images_convert_to_rgb(
8964 img1: &TensorDyn,
8965 img2: &TensorDyn,
8966 threshold: f64,
8967 name: &str,
8968 ) {
8969 assert_eq!(img1.height(), img2.height(), "Heights differ");
8970 assert_eq!(img1.width(), img2.width(), "Widths differ");
8971
8972 let mut img_rgb1 = TensorDyn::image(
8973 img1.width().unwrap(),
8974 img1.height().unwrap(),
8975 PixelFormat::Rgb,
8976 DType::U8,
8977 Some(TensorMemory::Mem),
8978 edgefirst_tensor::CpuAccess::ReadWrite,
8979 )
8980 .unwrap();
8981 let mut img_rgb2 = TensorDyn::image(
8982 img1.width().unwrap(),
8983 img1.height().unwrap(),
8984 PixelFormat::Rgb,
8985 DType::U8,
8986 Some(TensorMemory::Mem),
8987 edgefirst_tensor::CpuAccess::ReadWrite,
8988 )
8989 .unwrap();
8990 let mut __cv = CPUProcessor::default();
8991 let r1 = __cv.convert(
8992 img1,
8993 &mut img_rgb1,
8994 crate::Rotation::None,
8995 crate::Flip::None,
8996 crate::Crop::default(),
8997 );
8998 let r2 = __cv.convert(
8999 img2,
9000 &mut img_rgb2,
9001 crate::Rotation::None,
9002 crate::Flip::None,
9003 crate::Crop::default(),
9004 );
9005 if r1.is_err() || r2.is_err() {
9006 let w = img1.width().unwrap() as u32;
9008 let data1 = img1.as_u8().unwrap().map().unwrap().to_vec();
9009 let data2 = img2.as_u8().unwrap().map().unwrap().to_vec();
9010 let h1 = (data1.len() as u32) / w;
9011 let h2 = (data2.len() as u32) / w;
9012 let g1 = image::GrayImage::from_vec(w, h1, data1).unwrap();
9013 let g2 = image::GrayImage::from_vec(w, h2, data2).unwrap();
9014 let similarity = image_compare::gray_similarity_structure(
9015 &image_compare::Algorithm::RootMeanSquared,
9016 &g1,
9017 &g2,
9018 )
9019 .expect("Image Comparison failed");
9020 if similarity.score < threshold {
9021 panic!(
9022 "{name}: converted image and target image have similarity score too low: {} < {}",
9023 similarity.score, threshold
9024 )
9025 }
9026 return;
9027 }
9028
9029 let image1 = image::RgbImage::from_vec(
9030 img_rgb1.width().unwrap() as u32,
9031 img_rgb1.height().unwrap() as u32,
9032 img_rgb1.as_u8().unwrap().map().unwrap().to_vec(),
9033 )
9034 .unwrap();
9035
9036 let image2 = image::RgbImage::from_vec(
9037 img_rgb2.width().unwrap() as u32,
9038 img_rgb2.height().unwrap() as u32,
9039 img_rgb2.as_u8().unwrap().map().unwrap().to_vec(),
9040 )
9041 .unwrap();
9042
9043 let similarity = image_compare::rgb_similarity_structure(
9044 &image_compare::Algorithm::RootMeanSquared,
9045 &image1,
9046 &image2,
9047 )
9048 .expect("Image Comparison failed");
9049 if similarity.score < threshold {
9050 similarity
9053 .image
9054 .to_color_map()
9055 .save(format!("{name}.png"))
9056 .unwrap();
9057 panic!(
9058 "{name}: converted image and target image have similarity score too low: {} < {}",
9059 similarity.score, threshold
9060 )
9061 }
9062 }
9063
9064 #[test]
9069 fn test_nv12_image_creation() {
9070 let width = 640;
9071 let height = 480;
9072 let img = TensorDyn::image(
9073 width,
9074 height,
9075 PixelFormat::Nv12,
9076 DType::U8,
9077 None,
9078 edgefirst_tensor::CpuAccess::ReadWrite,
9079 )
9080 .unwrap();
9081
9082 assert_eq!(img.width(), Some(width));
9083 assert_eq!(img.height(), Some(height));
9084 assert_eq!(img.format().unwrap(), PixelFormat::Nv12);
9085 assert_eq!(img.as_u8().unwrap().shape(), &[height * 3 / 2, width]);
9087 }
9088
9089 #[test]
9090 fn test_nv12_channels() {
9091 let img = TensorDyn::image(
9092 640,
9093 480,
9094 PixelFormat::Nv12,
9095 DType::U8,
9096 None,
9097 edgefirst_tensor::CpuAccess::ReadWrite,
9098 )
9099 .unwrap();
9100 assert_eq!(img.format().unwrap().channels(), 1);
9102 }
9103
9104 #[test]
9109 fn test_tensor_set_format_planar() {
9110 let mut tensor = Tensor::<u8>::new(&[3, 480, 640], None, None).unwrap();
9111 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
9112 assert_eq!(tensor.format(), Some(PixelFormat::PlanarRgb));
9113 assert_eq!(tensor.width(), Some(640));
9114 assert_eq!(tensor.height(), Some(480));
9115 }
9116
9117 #[test]
9118 fn test_tensor_set_format_interleaved() {
9119 let mut tensor = Tensor::<u8>::new(&[480, 640, 4], None, None).unwrap();
9120 tensor.set_format(PixelFormat::Rgba).unwrap();
9121 assert_eq!(tensor.format(), Some(PixelFormat::Rgba));
9122 assert_eq!(tensor.width(), Some(640));
9123 assert_eq!(tensor.height(), Some(480));
9124 }
9125
9126 #[test]
9127 fn test_tensordyn_image_rgb() {
9128 let img = TensorDyn::image(
9129 640,
9130 480,
9131 PixelFormat::Rgb,
9132 DType::U8,
9133 None,
9134 edgefirst_tensor::CpuAccess::ReadWrite,
9135 )
9136 .unwrap();
9137 assert_eq!(img.width(), Some(640));
9138 assert_eq!(img.height(), Some(480));
9139 assert_eq!(img.format(), Some(PixelFormat::Rgb));
9140 }
9141
9142 #[test]
9143 fn test_tensordyn_image_planar_rgb() {
9144 let img = TensorDyn::image(
9145 640,
9146 480,
9147 PixelFormat::PlanarRgb,
9148 DType::U8,
9149 None,
9150 edgefirst_tensor::CpuAccess::ReadWrite,
9151 )
9152 .unwrap();
9153 assert_eq!(img.width(), Some(640));
9154 assert_eq!(img.height(), Some(480));
9155 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
9156 }
9157
9158 #[test]
9159 fn test_rgb_int8_format() {
9160 let img = TensorDyn::image(
9162 1280,
9163 720,
9164 PixelFormat::Rgb,
9165 DType::I8,
9166 Some(TensorMemory::Mem),
9167 edgefirst_tensor::CpuAccess::ReadWrite,
9168 )
9169 .unwrap();
9170 assert_eq!(img.width(), Some(1280));
9171 assert_eq!(img.height(), Some(720));
9172 assert_eq!(img.format(), Some(PixelFormat::Rgb));
9173 assert_eq!(img.dtype(), DType::I8);
9174 }
9175
9176 #[test]
9177 fn test_planar_rgb_int8_format() {
9178 let img = TensorDyn::image(
9179 1280,
9180 720,
9181 PixelFormat::PlanarRgb,
9182 DType::I8,
9183 Some(TensorMemory::Mem),
9184 edgefirst_tensor::CpuAccess::ReadWrite,
9185 )
9186 .unwrap();
9187 assert_eq!(img.width(), Some(1280));
9188 assert_eq!(img.height(), Some(720));
9189 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
9190 assert_eq!(img.dtype(), DType::I8);
9191 }
9192
9193 #[test]
9194 fn test_rgb_from_tensor() {
9195 let mut tensor = Tensor::<u8>::new(&[720, 1280, 3], None, None).unwrap();
9196 tensor.set_format(PixelFormat::Rgb).unwrap();
9197 let img = TensorDyn::from(tensor);
9198 assert_eq!(img.width(), Some(1280));
9199 assert_eq!(img.height(), Some(720));
9200 assert_eq!(img.format(), Some(PixelFormat::Rgb));
9201 }
9202
9203 #[test]
9204 fn test_planar_rgb_from_tensor() {
9205 let mut tensor = Tensor::<u8>::new(&[3, 720, 1280], None, None).unwrap();
9206 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
9207 let img = TensorDyn::from(tensor);
9208 assert_eq!(img.width(), Some(1280));
9209 assert_eq!(img.height(), Some(720));
9210 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
9211 }
9212
9213 #[test]
9214 fn test_dtype_determines_int8() {
9215 let u8_img = TensorDyn::image(
9217 64,
9218 64,
9219 PixelFormat::Rgb,
9220 DType::U8,
9221 None,
9222 edgefirst_tensor::CpuAccess::ReadWrite,
9223 )
9224 .unwrap();
9225 let i8_img = TensorDyn::image(
9226 64,
9227 64,
9228 PixelFormat::Rgb,
9229 DType::I8,
9230 None,
9231 edgefirst_tensor::CpuAccess::ReadWrite,
9232 )
9233 .unwrap();
9234 assert_eq!(u8_img.dtype(), DType::U8);
9235 assert_eq!(i8_img.dtype(), DType::I8);
9236 }
9237
9238 #[test]
9239 fn test_pixel_layout_packed_vs_planar() {
9240 assert_eq!(PixelFormat::Rgb.layout(), PixelLayout::Packed);
9242 assert_eq!(PixelFormat::Rgba.layout(), PixelLayout::Packed);
9243 assert_eq!(PixelFormat::PlanarRgb.layout(), PixelLayout::Planar);
9244 assert_eq!(PixelFormat::Nv12.layout(), PixelLayout::SemiPlanar);
9245 }
9246
9247 #[cfg(target_os = "linux")]
9252 #[cfg(feature = "opengl")]
9253 #[test]
9254 fn test_convert_pbo_to_pbo() {
9255 let mut converter = ImageProcessor::new().unwrap();
9256
9257 let is_pbo = converter
9259 .opengl
9260 .as_ref()
9261 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
9262 if !is_pbo {
9263 eprintln!("Skipping test_convert_pbo_to_pbo: backend is not PBO");
9264 return;
9265 }
9266
9267 let src_w = 640;
9268 let src_h = 480;
9269 let dst_w = 320;
9270 let dst_h = 240;
9271
9272 let pbo_src = converter
9274 .create_image(
9275 src_w,
9276 src_h,
9277 PixelFormat::Rgba,
9278 DType::U8,
9279 None,
9280 edgefirst_tensor::CpuAccess::ReadWrite,
9281 )
9282 .unwrap();
9283 assert_eq!(
9284 pbo_src.as_u8().unwrap().memory(),
9285 TensorMemory::Pbo,
9286 "create_image should produce a PBO tensor"
9287 );
9288
9289 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
9291 let jpeg_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
9292
9293 let mem_src = TensorDyn::image(
9295 src_w,
9296 src_h,
9297 PixelFormat::Rgba,
9298 DType::U8,
9299 Some(TensorMemory::Mem),
9300 edgefirst_tensor::CpuAccess::ReadWrite,
9301 )
9302 .unwrap();
9303 let (result, _jpeg_src, mem_src) = convert_img(
9304 &mut CPUProcessor::new(),
9305 jpeg_src,
9306 mem_src,
9307 Rotation::None,
9308 Flip::None,
9309 Crop::no_crop(),
9310 );
9311 result.unwrap();
9312
9313 {
9315 let src_data = mem_src.as_u8().unwrap().map().unwrap();
9316 let mut pbo_map = pbo_src.as_u8().unwrap().map().unwrap();
9317 pbo_map.copy_from_slice(&src_data);
9318 }
9319
9320 let pbo_dst = converter
9322 .create_image(
9323 dst_w,
9324 dst_h,
9325 PixelFormat::Rgba,
9326 DType::U8,
9327 None,
9328 edgefirst_tensor::CpuAccess::ReadWrite,
9329 )
9330 .unwrap();
9331 assert_eq!(pbo_dst.as_u8().unwrap().memory(), TensorMemory::Pbo);
9332
9333 let mut pbo_dst = pbo_dst;
9335 let result = converter.convert(
9336 &pbo_src,
9337 &mut pbo_dst,
9338 Rotation::None,
9339 Flip::None,
9340 Crop::no_crop(),
9341 );
9342 result.unwrap();
9343
9344 let cpu_dst = TensorDyn::image(
9346 dst_w,
9347 dst_h,
9348 PixelFormat::Rgba,
9349 DType::U8,
9350 Some(TensorMemory::Mem),
9351 edgefirst_tensor::CpuAccess::ReadWrite,
9352 )
9353 .unwrap();
9354 let (result, _mem_src, cpu_dst) = convert_img(
9355 &mut CPUProcessor::new(),
9356 mem_src,
9357 cpu_dst,
9358 Rotation::None,
9359 Flip::None,
9360 Crop::no_crop(),
9361 );
9362 result.unwrap();
9363
9364 let pbo_dst_img = {
9365 let mut __t = pbo_dst.into_u8().unwrap();
9366 __t.set_format(PixelFormat::Rgba).unwrap();
9367 TensorDyn::from(__t)
9368 };
9369 compare_images(&pbo_dst_img, &cpu_dst, 0.95, function!());
9370 log::info!("test_convert_pbo_to_pbo: PASS — PBO-to-PBO convert matches CPU reference");
9371 }
9372
9373 #[test]
9374 fn test_image_bgra() {
9375 let img = TensorDyn::image(
9376 640,
9377 480,
9378 PixelFormat::Bgra,
9379 DType::U8,
9380 Some(edgefirst_tensor::TensorMemory::Mem),
9381 edgefirst_tensor::CpuAccess::ReadWrite,
9382 )
9383 .unwrap();
9384 assert_eq!(img.width(), Some(640));
9385 assert_eq!(img.height(), Some(480));
9386 assert_eq!(img.format().unwrap().channels(), 4);
9387 assert_eq!(img.format().unwrap(), PixelFormat::Bgra);
9388 }
9389
9390 #[test]
9395 fn test_force_backend_cpu() {
9396 let _lock = acquire_env_lock();
9397 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
9398 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
9399 let converter = ImageProcessor::new().unwrap();
9400 assert!(converter.cpu.is_some());
9401 assert_eq!(converter.forced_backend, Some(ForcedBackend::Cpu));
9402 }
9403
9404 #[test]
9405 fn test_force_backend_invalid() {
9406 let _lock = acquire_env_lock();
9407 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
9408 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "invalid") };
9409 let result = ImageProcessor::new();
9410 assert!(
9411 matches!(&result, Err(Error::ForcedBackendUnavailable(s)) if s.contains("unknown")),
9412 "invalid backend value should return ForcedBackendUnavailable error: {result:?}"
9413 );
9414 }
9415
9416 #[test]
9417 fn test_force_backend_unset() {
9418 let _lock = acquire_env_lock();
9419 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
9420 unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") };
9421 let converter = ImageProcessor::new().unwrap();
9422 assert!(converter.forced_backend.is_none());
9423 }
9424
9425 #[test]
9430 fn test_draw_proto_masks_no_cpu_returns_error() {
9431 let _lock = acquire_env_lock();
9433 let _guard = EnvGuard::snapshot(&[
9434 "EDGEFIRST_FORCE_BACKEND",
9435 "EDGEFIRST_DISABLE_GL",
9436 "EDGEFIRST_DISABLE_G2D",
9437 "EDGEFIRST_DISABLE_CPU",
9438 ]);
9439
9440 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
9442 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
9443 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
9444
9445 let mut converter = ImageProcessor::new().unwrap();
9446 assert!(converter.cpu.is_none(), "CPU should be disabled");
9447
9448 let dst = TensorDyn::image(
9449 640,
9450 480,
9451 PixelFormat::Rgba,
9452 DType::U8,
9453 Some(TensorMemory::Mem),
9454 edgefirst_tensor::CpuAccess::ReadWrite,
9455 )
9456 .unwrap();
9457 let mut dst_dyn = dst;
9458 let det = [DetectBox {
9459 bbox: edgefirst_decoder::BoundingBox {
9460 xmin: 0.1,
9461 ymin: 0.1,
9462 xmax: 0.5,
9463 ymax: 0.5,
9464 },
9465 score: 0.9,
9466 label: 0,
9467 }];
9468 let proto_data = {
9469 use edgefirst_tensor::{Tensor, TensorDyn};
9470 let coeff_t = Tensor::<f32>::from_slice(&[0.5_f32; 4], &[1, 4]).unwrap();
9471 let protos_t =
9472 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
9473 ProtoData {
9474 mask_coefficients: TensorDyn::F32(coeff_t),
9475 protos: TensorDyn::F32(protos_t),
9476 layout: ProtoLayout::Nhwc,
9477 }
9478 };
9479 let result =
9480 converter.draw_proto_masks(&mut dst_dyn, &det, &proto_data, Default::default());
9481 assert!(
9482 matches!(&result, Err(Error::Internal(s)) if s.contains("CPU backend")),
9483 "draw_proto_masks without CPU should return Internal error: {result:?}"
9484 );
9485 }
9486
9487 #[test]
9488 fn test_draw_proto_masks_cpu_fallback_works() {
9489 let _lock = acquire_env_lock();
9492 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
9493 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
9494 let mut converter = ImageProcessor::new().unwrap();
9495 assert!(converter.cpu.is_some());
9496
9497 let dst = TensorDyn::image(
9498 64,
9499 64,
9500 PixelFormat::Rgba,
9501 DType::U8,
9502 Some(TensorMemory::Mem),
9503 edgefirst_tensor::CpuAccess::ReadWrite,
9504 )
9505 .unwrap();
9506 let mut dst_dyn = dst;
9507 let det = [DetectBox {
9508 bbox: edgefirst_decoder::BoundingBox {
9509 xmin: 0.1,
9510 ymin: 0.1,
9511 xmax: 0.5,
9512 ymax: 0.5,
9513 },
9514 score: 0.9,
9515 label: 0,
9516 }];
9517 let proto_data = {
9518 use edgefirst_tensor::{Tensor, TensorDyn};
9519 let coeff_t = Tensor::<f32>::from_slice(&[0.5_f32; 4], &[1, 4]).unwrap();
9520 let protos_t =
9521 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
9522 ProtoData {
9523 mask_coefficients: TensorDyn::F32(coeff_t),
9524 protos: TensorDyn::F32(protos_t),
9525 layout: ProtoLayout::Nhwc,
9526 }
9527 };
9528 let result =
9529 converter.draw_proto_masks(&mut dst_dyn, &det, &proto_data, Default::default());
9530 assert!(result.is_ok(), "CPU fallback path should work: {result:?}");
9531 }
9532
9533 fn acquire_env_lock() -> std::sync::MutexGuard<'static, ()> {
9565 use std::sync::{Mutex, OnceLock};
9566 static ENV_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
9567 ENV_MUTEX
9568 .get_or_init(|| Mutex::new(()))
9569 .lock()
9570 .unwrap_or_else(|e| e.into_inner())
9571 }
9572
9573 struct EnvGuard {
9576 vars: Vec<(&'static str, Option<String>)>,
9577 }
9578
9579 impl EnvGuard {
9580 fn snapshot(names: &[&'static str]) -> Self {
9584 Self {
9585 vars: names.iter().map(|&k| (k, std::env::var(k).ok())).collect(),
9586 }
9587 }
9588 }
9589
9590 impl Drop for EnvGuard {
9591 fn drop(&mut self) {
9592 for (k, v) in &self.vars {
9593 match v {
9594 Some(s) => unsafe { std::env::set_var(k, s) },
9595 None => unsafe { std::env::remove_var(k) },
9596 }
9597 }
9598 }
9599 }
9600
9601 fn with_force_backend<R>(value: Option<&str>, body: impl FnOnce() -> R) -> R {
9605 let _lock = acquire_env_lock();
9606 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
9607 match value {
9608 Some(v) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", v) },
9609 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
9610 }
9611 body()
9612 }
9613
9614 fn make_dirty_dst(w: usize, h: usize, mem: Option<TensorMemory>) -> TensorDyn {
9619 let dst = TensorDyn::image(
9620 w,
9621 h,
9622 PixelFormat::Rgba,
9623 DType::U8,
9624 mem,
9625 edgefirst_tensor::CpuAccess::ReadWrite,
9626 )
9627 .unwrap();
9628 {
9629 use edgefirst_tensor::TensorMapTrait;
9630 let u8t = dst.as_u8().unwrap();
9631 let mut map = u8t.map().unwrap();
9632 for (i, b) in map.as_mut_slice().iter_mut().enumerate() {
9633 *b = 0xA0u8.wrapping_add((i as u8) & 0x3F);
9634 }
9635 }
9636 dst
9637 }
9638
9639 fn make_bg(w: usize, h: usize, mem: Option<TensorMemory>, rgba: [u8; 4]) -> TensorDyn {
9641 let bg = TensorDyn::image(
9642 w,
9643 h,
9644 PixelFormat::Rgba,
9645 DType::U8,
9646 mem,
9647 edgefirst_tensor::CpuAccess::ReadWrite,
9648 )
9649 .unwrap();
9650 {
9651 use edgefirst_tensor::TensorMapTrait;
9652 let u8t = bg.as_u8().unwrap();
9653 let mut map = u8t.map().unwrap();
9654 for chunk in map.as_mut_slice().chunks_exact_mut(4) {
9655 chunk.copy_from_slice(&rgba);
9656 }
9657 }
9658 bg
9659 }
9660
9661 fn pixel_at(dst: &TensorDyn, x: usize, y: usize) -> [u8; 4] {
9662 use edgefirst_tensor::TensorMapTrait;
9663 let w = dst.width().unwrap();
9664 let off = (y * w + x) * 4;
9665 let u8t = dst.as_u8().unwrap();
9666 let map = u8t.map().unwrap();
9667 let s = map.as_slice();
9668 [s[off], s[off + 1], s[off + 2], s[off + 3]]
9669 }
9670
9671 fn assert_every_pixel_eq(dst: &TensorDyn, expected: [u8; 4], case: &str) {
9672 use edgefirst_tensor::TensorMapTrait;
9673 let u8t = dst.as_u8().unwrap();
9674 let map = u8t.map().unwrap();
9675 for (i, chunk) in map.as_slice().chunks_exact(4).enumerate() {
9676 assert_eq!(
9677 chunk, &expected,
9678 "{case}: pixel idx {i} = {chunk:?}, expected {expected:?}"
9679 );
9680 }
9681 }
9682
9683 fn scenario_empty_no_bg(processor: &mut ImageProcessor, case: &str) {
9686 let mut dst = make_dirty_dst(64, 64, None);
9687 processor
9688 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::default())
9689 .unwrap_or_else(|e| panic!("{case}/decoded_masks empty+no-bg failed: {e:?}"));
9690 assert_every_pixel_eq(&dst, [0, 0, 0, 0], &format!("{case}/decoded"));
9691
9692 let mut dst = make_dirty_dst(64, 64, None);
9693 let proto = {
9694 use edgefirst_tensor::{Tensor, TensorDyn};
9695 let coeff_t = Tensor::<f32>::from_slice(&[0.0_f32; 4], &[1, 4]).unwrap();
9697 let protos_t =
9698 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
9699 ProtoData {
9700 mask_coefficients: TensorDyn::F32(coeff_t),
9701 protos: TensorDyn::F32(protos_t),
9702 layout: ProtoLayout::Nhwc,
9703 }
9704 };
9705 processor
9706 .draw_proto_masks(&mut dst, &[], &proto, MaskOverlay::default())
9707 .unwrap_or_else(|e| panic!("{case}/proto_masks empty+no-bg failed: {e:?}"));
9708 assert_every_pixel_eq(&dst, [0, 0, 0, 0], &format!("{case}/proto"));
9709 }
9710
9711 fn scenario_empty_with_bg(processor: &mut ImageProcessor, case: &str) {
9714 let bg_color = [42, 99, 200, 255];
9715 let bg = make_bg(64, 64, None, bg_color);
9716 let overlay = MaskOverlay::new().with_background(&bg);
9717
9718 let mut dst = make_dirty_dst(64, 64, None);
9719 processor
9720 .draw_decoded_masks(&mut dst, &[], &[], overlay)
9721 .unwrap_or_else(|e| panic!("{case}/decoded_masks empty+bg failed: {e:?}"));
9722 assert_every_pixel_eq(&dst, bg_color, &format!("{case}/decoded bg blit"));
9723
9724 let mut dst = make_dirty_dst(64, 64, None);
9725 let proto = {
9726 use edgefirst_tensor::{Tensor, TensorDyn};
9727 let coeff_t = Tensor::<f32>::from_slice(&[0.0_f32; 4], &[1, 4]).unwrap();
9729 let protos_t =
9730 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
9731 ProtoData {
9732 mask_coefficients: TensorDyn::F32(coeff_t),
9733 protos: TensorDyn::F32(protos_t),
9734 layout: ProtoLayout::Nhwc,
9735 }
9736 };
9737 processor
9738 .draw_proto_masks(&mut dst, &[], &proto, overlay)
9739 .unwrap_or_else(|e| panic!("{case}/proto_masks empty+bg failed: {e:?}"));
9740 assert_every_pixel_eq(&dst, bg_color, &format!("{case}/proto bg blit"));
9741 }
9742
9743 fn scenario_detect_no_bg(processor: &mut ImageProcessor, case: &str) {
9747 use edgefirst_decoder::Segmentation;
9748 use ndarray::Array3;
9749 processor
9750 .set_class_colors(&[[200, 80, 40, 255]])
9751 .expect("set_class_colors");
9752
9753 let detect = DetectBox {
9754 bbox: [0.25, 0.25, 0.75, 0.75].into(),
9755 score: 0.99,
9756 label: 0,
9757 };
9758 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
9759 let seg = Segmentation {
9760 segmentation: seg_arr,
9761 xmin: 0.25,
9762 ymin: 0.25,
9763 xmax: 0.75,
9764 ymax: 0.75,
9765 };
9766
9767 let mut dst = make_dirty_dst(64, 64, None);
9768 processor
9769 .draw_decoded_masks(&mut dst, &[detect], &[seg], MaskOverlay::default())
9770 .unwrap_or_else(|e| panic!("{case}/decoded_masks detect+no-bg failed: {e:?}"));
9771
9772 let corner = pixel_at(&dst, 2, 2);
9774 assert_eq!(
9775 corner,
9776 [0, 0, 0, 0],
9777 "{case}/decoded: corner (2,2) leaked dirty pattern: {corner:?}"
9778 );
9779 let center = pixel_at(&dst, 32, 32);
9783 assert!(
9784 center != [0, 0, 0, 0],
9785 "{case}/decoded: center (32,32) was not coloured: {center:?}"
9786 );
9787 }
9788
9789 fn scenario_detect_with_bg(processor: &mut ImageProcessor, case: &str) {
9792 use edgefirst_decoder::Segmentation;
9793 use ndarray::Array3;
9794 processor
9795 .set_class_colors(&[[200, 80, 40, 255]])
9796 .expect("set_class_colors");
9797 let bg_color = [10, 20, 30, 255];
9798 let bg = make_bg(64, 64, None, bg_color);
9799
9800 let detect = DetectBox {
9801 bbox: [0.25, 0.25, 0.75, 0.75].into(),
9802 score: 0.99,
9803 label: 0,
9804 };
9805 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
9806 let seg = Segmentation {
9807 segmentation: seg_arr,
9808 xmin: 0.25,
9809 ymin: 0.25,
9810 xmax: 0.75,
9811 ymax: 0.75,
9812 };
9813
9814 let overlay = MaskOverlay::new().with_background(&bg);
9815 let mut dst = make_dirty_dst(64, 64, None);
9816 processor
9817 .draw_decoded_masks(&mut dst, &[detect], &[seg], overlay)
9818 .unwrap_or_else(|e| panic!("{case}/decoded_masks detect+bg failed: {e:?}"));
9819
9820 let corner = pixel_at(&dst, 2, 2);
9822 assert_eq!(
9823 corner, bg_color,
9824 "{case}/decoded: corner (2,2) should show bg {bg_color:?} got {corner:?}"
9825 );
9826 let center = pixel_at(&dst, 32, 32);
9829 assert!(
9830 center != bg_color,
9831 "{case}/decoded: center (32,32) should differ from bg {bg_color:?}, got {center:?}"
9832 );
9833 }
9834
9835 fn run_all_scenarios(
9838 force_backend: Option<&'static str>,
9839 case: &'static str,
9840 require_dma_for_bg: bool,
9841 ) {
9842 if require_dma_for_bg && !edgefirst_tensor::is_dma_available() {
9843 eprintln!("SKIPPED: {case} — DMA not available on this host");
9844 return;
9845 }
9846 let processor_result = with_force_backend(force_backend, ImageProcessor::new);
9847 let mut processor = match processor_result {
9848 Ok(p) => p,
9849 Err(e) => {
9850 eprintln!("SKIPPED: {case} — backend init failed: {e:?}");
9851 return;
9852 }
9853 };
9854 scenario_empty_no_bg(&mut processor, case);
9855 scenario_empty_with_bg(&mut processor, case);
9856 scenario_detect_no_bg(&mut processor, case);
9857 scenario_detect_with_bg(&mut processor, case);
9858 }
9859
9860 #[test]
9861 fn test_draw_masks_4_scenarios_cpu() {
9862 run_all_scenarios(Some("cpu"), "cpu", false);
9863 }
9864
9865 #[test]
9866 fn test_draw_masks_4_scenarios_auto() {
9867 run_all_scenarios(None, "auto", false);
9868 }
9869
9870 #[cfg(target_os = "linux")]
9871 #[cfg(feature = "opengl")]
9872 #[test]
9873 fn test_draw_masks_4_scenarios_opengl() {
9874 run_all_scenarios(Some("opengl"), "opengl", false);
9875 }
9876
9877 #[cfg(target_os = "linux")]
9882 #[test]
9883 fn test_draw_masks_zero_detection_g2d_forced() {
9884 if !edgefirst_tensor::is_dma_available() {
9885 eprintln!("SKIPPED: g2d forced — DMA not available on this host");
9886 return;
9887 }
9888 let processor_result = with_force_backend(Some("g2d"), ImageProcessor::new);
9889 let mut processor = match processor_result {
9890 Ok(p) => p,
9891 Err(e) => {
9892 eprintln!("SKIPPED: g2d forced — init failed: {e:?}");
9893 return;
9894 }
9895 };
9896
9897 let mut dst = TensorDyn::image(
9899 64,
9900 64,
9901 PixelFormat::Rgba,
9902 DType::U8,
9903 Some(TensorMemory::Dma),
9904 edgefirst_tensor::CpuAccess::ReadWrite,
9905 )
9906 .unwrap();
9907 {
9908 use edgefirst_tensor::TensorMapTrait;
9909 let u8t = dst.as_u8_mut().unwrap();
9910 let mut map = u8t.map().unwrap();
9911 map.as_mut_slice().fill(0xBB);
9912 }
9913 processor
9914 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::default())
9915 .expect("g2d empty+no-bg");
9916 assert_every_pixel_eq(&dst, [0, 0, 0, 0], "g2d/case1 cleared");
9917
9918 let bg_color = [7, 11, 13, 255];
9920 let bg = {
9921 let t = TensorDyn::image(
9922 64,
9923 64,
9924 PixelFormat::Rgba,
9925 DType::U8,
9926 Some(TensorMemory::Dma),
9927 edgefirst_tensor::CpuAccess::ReadWrite,
9928 )
9929 .unwrap();
9930 {
9931 use edgefirst_tensor::TensorMapTrait;
9932 let u8t = t.as_u8().unwrap();
9933 let mut map = u8t.map().unwrap();
9934 for chunk in map.as_mut_slice().chunks_exact_mut(4) {
9935 chunk.copy_from_slice(&bg_color);
9936 }
9937 }
9938 t
9939 };
9940 let mut dst = TensorDyn::image(
9941 64,
9942 64,
9943 PixelFormat::Rgba,
9944 DType::U8,
9945 Some(TensorMemory::Dma),
9946 edgefirst_tensor::CpuAccess::ReadWrite,
9947 )
9948 .unwrap();
9949 {
9950 use edgefirst_tensor::TensorMapTrait;
9951 let u8t = dst.as_u8_mut().unwrap();
9952 let mut map = u8t.map().unwrap();
9953 map.as_mut_slice().fill(0x55);
9954 }
9955 processor
9956 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::new().with_background(&bg))
9957 .expect("g2d empty+bg");
9958 assert_every_pixel_eq(&dst, bg_color, "g2d/case2 bg blit");
9959
9960 let detect = DetectBox {
9962 bbox: [0.25, 0.25, 0.75, 0.75].into(),
9963 score: 0.9,
9964 label: 0,
9965 };
9966 let mut dst = TensorDyn::image(
9967 64,
9968 64,
9969 PixelFormat::Rgba,
9970 DType::U8,
9971 Some(TensorMemory::Dma),
9972 edgefirst_tensor::CpuAccess::ReadWrite,
9973 )
9974 .unwrap();
9975 let err = processor
9976 .draw_decoded_masks(&mut dst, &[detect], &[], MaskOverlay::default())
9977 .expect_err("g2d must reject detect-present draw_decoded_masks");
9978 assert!(
9979 matches!(err, Error::NotImplemented(_)),
9980 "g2d case3 wrong error: {err:?}"
9981 );
9982 }
9983
9984 #[test]
9985 fn test_set_format_then_cpu_convert() {
9986 let _lock = acquire_env_lock();
9989 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
9990 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
9991 let mut processor = ImageProcessor::new().unwrap();
9992
9993 let image = edgefirst_bench::testdata::read("zidane.jpg");
9995 let src = load_image_test_helper(&image, Some(PixelFormat::Rgba), None).unwrap();
9996
9997 let mut dst =
9999 TensorDyn::new(&[640, 640, 3], DType::U8, Some(TensorMemory::Mem), None).unwrap();
10000 dst.set_format(PixelFormat::Rgb).unwrap();
10001
10002 processor
10004 .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
10005 .unwrap();
10006
10007 assert_eq!(dst.format(), Some(PixelFormat::Rgb));
10009 assert_eq!(dst.width(), Some(640));
10010 assert_eq!(dst.height(), Some(640));
10011 }
10012
10013 #[test]
10019 fn test_multiple_image_processors_same_thread() {
10020 let _lock = acquire_env_lock();
10023 let mut processors: Vec<ImageProcessor> = (0..4)
10024 .map(|_| ImageProcessor::new().expect("ImageProcessor::new() failed"))
10025 .collect();
10026
10027 for proc in &mut processors {
10028 let src = proc
10029 .create_image(
10030 128,
10031 128,
10032 PixelFormat::Rgb,
10033 DType::U8,
10034 None,
10035 edgefirst_tensor::CpuAccess::ReadWrite,
10036 )
10037 .expect("create src failed");
10038 let mut dst = proc
10039 .create_image(
10040 64,
10041 64,
10042 PixelFormat::Rgb,
10043 DType::U8,
10044 None,
10045 edgefirst_tensor::CpuAccess::ReadWrite,
10046 )
10047 .expect("create dst failed");
10048 proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
10049 .expect("convert failed");
10050 assert_eq!(dst.width(), Some(64));
10051 assert_eq!(dst.height(), Some(64));
10052 }
10053 }
10054
10055 #[test]
10062 fn test_multiple_image_processors_separate_threads() {
10063 use std::sync::mpsc;
10064 use std::time::Duration;
10065
10066 if std::env::var_os("EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS").is_some() {
10076 eprintln!(
10077 "SKIPPED: test_multiple_image_processors_separate_threads — known Vivante \
10078 GC7000UL concurrent-EGL-teardown double-free \
10079 (EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS set)"
10080 );
10081 return;
10082 }
10083
10084 const TIMEOUT: Duration = Duration::from_secs(60);
10085
10086 let _lock = acquire_env_lock();
10089
10090 let (tx, rx) = mpsc::channel::<()>();
10091
10092 std::thread::spawn(move || {
10093 let handles: Vec<_> = (0..4)
10094 .map(|i| {
10095 std::thread::spawn(move || {
10096 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
10097 panic!("ImageProcessor::new() failed on thread {i}: {e}")
10098 });
10099 let src = proc
10100 .create_image(
10101 128,
10102 128,
10103 PixelFormat::Rgb,
10104 DType::U8,
10105 None,
10106 edgefirst_tensor::CpuAccess::ReadWrite,
10107 )
10108 .unwrap_or_else(|e| panic!("create src failed on thread {i}: {e}"));
10109 let mut dst = proc
10110 .create_image(
10111 64,
10112 64,
10113 PixelFormat::Rgb,
10114 DType::U8,
10115 None,
10116 edgefirst_tensor::CpuAccess::ReadWrite,
10117 )
10118 .unwrap_or_else(|e| panic!("create dst failed on thread {i}: {e}"));
10119 proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
10120 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
10121 assert_eq!(dst.width(), Some(64));
10122 assert_eq!(dst.height(), Some(64));
10123 })
10124 })
10125 .collect();
10126
10127 for (i, h) in handles.into_iter().enumerate() {
10128 h.join()
10129 .unwrap_or_else(|e| panic!("thread {i} panicked: {e:?}"));
10130 }
10131
10132 let _ = tx.send(());
10133 });
10134
10135 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
10136 panic!("test_multiple_image_processors_separate_threads timed out after {TIMEOUT:?}")
10137 });
10138 }
10139
10140 #[test]
10147 fn test_image_processors_concurrent_operations() {
10148 use std::sync::{mpsc, Arc, Barrier};
10149 use std::time::Duration;
10150
10151 const N: usize = 4;
10152 const ROUNDS: usize = 10;
10153 const TIMEOUT: Duration = Duration::from_secs(60);
10154
10155 let _lock = acquire_env_lock();
10158
10159 let (tx, rx) = mpsc::channel::<()>();
10160
10161 std::thread::spawn(move || {
10162 let barrier = Arc::new(Barrier::new(N));
10163
10164 let handles: Vec<_> = (0..N)
10165 .map(|i| {
10166 let barrier = Arc::clone(&barrier);
10167 std::thread::spawn(move || {
10168 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
10169 panic!("ImageProcessor::new() failed on thread {i}: {e}")
10170 });
10171
10172 barrier.wait();
10174
10175 for round in 0..ROUNDS {
10177 let src = proc
10178 .create_image(
10179 128,
10180 128,
10181 PixelFormat::Rgb,
10182 DType::U8,
10183 None,
10184 edgefirst_tensor::CpuAccess::ReadWrite,
10185 )
10186 .unwrap_or_else(|e| {
10187 panic!("create src failed on thread {i} round {round}: {e}")
10188 });
10189 let mut dst = proc
10190 .create_image(
10191 64,
10192 64,
10193 PixelFormat::Rgb,
10194 DType::U8,
10195 None,
10196 edgefirst_tensor::CpuAccess::ReadWrite,
10197 )
10198 .unwrap_or_else(|e| {
10199 panic!("create dst failed on thread {i} round {round}: {e}")
10200 });
10201 proc.convert(
10202 &src,
10203 &mut dst,
10204 Rotation::None,
10205 Flip::None,
10206 Crop::default(),
10207 )
10208 .unwrap_or_else(|e| {
10209 panic!("convert failed on thread {i} round {round}: {e}")
10210 });
10211 assert_eq!(dst.width(), Some(64));
10212 assert_eq!(dst.height(), Some(64));
10213 }
10214 })
10215 })
10216 .collect();
10217
10218 for (i, h) in handles.into_iter().enumerate() {
10219 h.join()
10220 .unwrap_or_else(|e| panic!("thread {i} panicked: {e:?}"));
10221 }
10222
10223 let _ = tx.send(());
10224 });
10225
10226 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
10227 panic!("test_image_processors_concurrent_operations timed out after {TIMEOUT:?}")
10228 });
10229 }
10230
10231 #[test]
10249 fn test_parallel_processors_unique_outputs() {
10250 use std::sync::{mpsc, Arc, Barrier};
10251 use std::time::Duration;
10252
10253 const N: usize = 4;
10254 const ROUNDS: usize = 25;
10255 const TIMEOUT: Duration = Duration::from_secs(60);
10256
10257 if std::env::var_os("EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS").is_some() {
10258 eprintln!(
10259 "SKIPPED: test_parallel_processors_unique_outputs — known Vivante \
10260 GC7000UL concurrent-multi-processor driver abort \
10261 (EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS set)"
10262 );
10263 return;
10264 }
10265
10266 let _lock = acquire_env_lock();
10267 let (tx, rx) = mpsc::channel::<()>();
10268
10269 std::thread::spawn(move || {
10270 let barrier = Arc::new(Barrier::new(N));
10271 let handles: Vec<_> = (0..N)
10272 .map(|i| {
10273 let barrier = Arc::clone(&barrier);
10274 std::thread::spawn(move || {
10275 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
10276 panic!("ImageProcessor::new() failed on thread {i}: {e}")
10277 });
10278 let (w, h) = (640usize, 480usize);
10280 let mem = if edgefirst_tensor::is_dma_available() {
10281 Some(TensorMemory::Dma)
10282 } else {
10283 Some(TensorMemory::Mem)
10284 };
10285 let src = proc
10286 .create_image(
10287 w,
10288 h,
10289 PixelFormat::Nv12,
10290 DType::U8,
10291 mem,
10292 edgefirst_tensor::CpuAccess::ReadWrite,
10293 )
10294 .unwrap();
10295 {
10296 let t = src.as_u8().unwrap();
10297 let mut m = t.map().unwrap();
10298 let s = m.as_mut_slice();
10299 for (j, b) in s[..w * h].iter_mut().enumerate() {
10300 *b = ((i * 53 + j) % 200 + 16) as u8;
10301 }
10302 for b in &mut s[w * h..] {
10303 *b = (80 + i * 24) as u8;
10304 }
10305 }
10306 let lb = Crop::letterbox([114, 114, 114, 255]);
10307 let convert_once = |proc: &mut ImageProcessor| -> Vec<u8> {
10308 let mut dst = proc
10309 .create_image(
10310 320,
10311 320,
10312 PixelFormat::Rgba,
10313 DType::U8,
10314 mem,
10315 edgefirst_tensor::CpuAccess::ReadWrite,
10316 )
10317 .unwrap();
10318 proc.convert(&src, &mut dst, Rotation::None, Flip::None, lb)
10319 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
10320 let t = dst.as_u8().unwrap();
10321 let m = t.map().unwrap();
10322 m.as_slice().to_vec()
10323 };
10324
10325 let oracle = convert_once(&mut proc);
10326 barrier.wait();
10327 for round in 0..ROUNDS {
10328 let out = convert_once(&mut proc);
10329 let diffs = oracle.iter().zip(&out).filter(|(a, b)| a != b).count();
10330 assert!(
10331 diffs == 0,
10332 "thread {i} round {round}: {diffs}/{} bytes diverged \
10333 from this processor's own oracle — cross-processor \
10334 GL state leakage under parallel execution",
10335 oracle.len()
10336 );
10337 }
10338 })
10339 })
10340 .collect();
10341
10342 for (i, h) in handles.into_iter().enumerate() {
10343 h.join()
10344 .unwrap_or_else(|e| panic!("parallel thread {i} panicked: {e:?}"));
10345 }
10346 let _ = tx.send(());
10347 });
10348
10349 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
10350 panic!("test_parallel_processors_unique_outputs timed out after {TIMEOUT:?}")
10351 });
10352 }
10353
10354 #[test]
10363 #[ignore = "heavy on-demand GL-parallelism stressor; run explicitly on boards"]
10364 fn stress_parallel_processors_oracle() {
10365 use std::sync::{mpsc, Arc, Barrier};
10366 use std::time::Duration;
10367
10368 const N: usize = 4;
10369 const ROUNDS: usize = 200;
10370 const TIMEOUT: Duration = Duration::from_secs(600);
10371
10372 let _lock = acquire_env_lock();
10373 let (tx, rx) = mpsc::channel::<()>();
10374
10375 std::thread::spawn(move || {
10376 let barrier = Arc::new(Barrier::new(N));
10377 let handles: Vec<_> = (0..N)
10378 .map(|i| {
10379 let barrier = Arc::clone(&barrier);
10380 std::thread::spawn(move || {
10381 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
10382 panic!("ImageProcessor::new() failed on thread {i}: {e}")
10383 });
10384 let (w, h) = (1280usize, 720usize);
10385 let mem = if edgefirst_tensor::is_dma_available() {
10386 Some(TensorMemory::Dma)
10387 } else {
10388 Some(TensorMemory::Mem)
10389 };
10390
10391 let src = proc
10394 .create_image(
10395 w,
10396 h,
10397 PixelFormat::Nv12,
10398 DType::U8,
10399 mem,
10400 edgefirst_tensor::CpuAccess::ReadWrite,
10401 )
10402 .unwrap();
10403 {
10404 let t = src.as_u8().unwrap();
10405 let mut m = t.map().unwrap();
10406 let s = m.as_mut_slice();
10407 for (j, b) in s[..w * h].iter_mut().enumerate() {
10408 *b = ((i * 37 + j) % 200 + 16) as u8;
10409 }
10410 for b in &mut s[w * h..] {
10411 *b = (96 + i * 16) as u8;
10412 }
10413 }
10414 let lb = Crop::letterbox([114, 114, 114, 255]);
10415
10416 let convert_once = |proc: &mut ImageProcessor| -> Vec<u8> {
10417 let mut dst = proc
10418 .create_image(
10419 640,
10420 640,
10421 PixelFormat::Rgb,
10422 DType::U8,
10423 mem,
10424 edgefirst_tensor::CpuAccess::ReadWrite,
10425 )
10426 .unwrap();
10427 proc.convert(&src, &mut dst, Rotation::None, Flip::None, lb)
10428 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
10429 let t = dst.as_u8().unwrap();
10430 let m = t.map().unwrap();
10431 m.as_slice().to_vec()
10432 };
10433
10434 let oracle = convert_once(&mut proc);
10435 barrier.wait();
10436 for round in 0..ROUNDS {
10437 let out = convert_once(&mut proc);
10438 let diffs = oracle.iter().zip(&out).filter(|(a, b)| a != b).count();
10439 assert!(
10440 diffs == 0,
10441 "thread {i} round {round}: {diffs}/{} bytes diverged \
10442 from the pre-barrier oracle",
10443 oracle.len()
10444 );
10445 }
10446 })
10447 })
10448 .collect();
10449
10450 for (i, h) in handles.into_iter().enumerate() {
10451 h.join()
10452 .unwrap_or_else(|e| panic!("stressor thread {i} panicked: {e:?}"));
10453 }
10454 let _ = tx.send(());
10455 });
10456
10457 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
10458 panic!("stress_parallel_processors_oracle timed out after {TIMEOUT:?}")
10459 });
10460 }
10461
10462 #[test]
10475 fn convert_f32_auto_never_errors_non_gl_combo() {
10476 const W: usize = 64;
10477 const H: usize = 64;
10478
10479 let src = TensorDyn::image(
10482 W,
10483 H,
10484 PixelFormat::Yuyv,
10485 DType::U8,
10486 Some(TensorMemory::Mem),
10487 edgefirst_tensor::CpuAccess::ReadWrite,
10488 )
10489 .unwrap();
10490 {
10491 let mut map = src.as_u8().unwrap().map().unwrap();
10492 let data = map.as_mut_slice();
10493 for chunk in data.chunks_exact_mut(4) {
10494 chunk[0] = 128; chunk[1] = 128; chunk[2] = 160; chunk[3] = 128; }
10499 }
10500
10501 let mut dst = TensorDyn::image(
10502 W,
10503 H,
10504 PixelFormat::Rgb,
10505 DType::F32,
10506 Some(TensorMemory::Mem),
10507 edgefirst_tensor::CpuAccess::ReadWrite,
10508 )
10509 .unwrap();
10510
10511 let mut proc = ImageProcessor::new().unwrap();
10512 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
10513 assert!(
10514 result.is_ok(),
10515 "auto-chain Yuyv→Rgb F32 must not error: {:?}",
10516 result.err()
10517 );
10518
10519 let map = dst.as_f32().unwrap().map().unwrap();
10521 let floats = map.as_slice();
10522 assert_eq!(floats.len(), W * H * 3, "unexpected output element count");
10523 for (i, &v) in floats.iter().enumerate() {
10524 assert!(
10525 v.is_finite() && (0.0..=1.0).contains(&v),
10526 "output[{i}]={v} is not finite or not in [0,1]"
10527 );
10528 }
10529
10530 let first_non_zero = floats.iter().find(|&&v| v > 0.01);
10534 assert!(
10535 first_non_zero.is_some(),
10536 "all-zero output detected — CPU path likely did not write to the destination buffer"
10537 );
10538 let r0 = floats[0];
10541 assert!(
10542 (r0 - 0.502_f32).abs() < 0.05,
10543 "first pixel R={r0} expected ≈0.502 (Y=128 neutral grey from YUYV source)"
10544 );
10545 }
10546
10547 #[test]
10553 #[allow(clippy::needless_update)]
10559 fn convert_f16_forced_cpu_correct() {
10560 const W: usize = 16;
10561 const H: usize = 16;
10562 const TOL: f32 = 1.0 / 512.0; let src = TensorDyn::image(
10566 W,
10567 H,
10568 PixelFormat::Rgba,
10569 DType::U8,
10570 Some(TensorMemory::Mem),
10571 edgefirst_tensor::CpuAccess::ReadWrite,
10572 )
10573 .unwrap();
10574 {
10575 let mut map = src.as_u8().unwrap().map().unwrap();
10576 let data = map.as_mut_slice();
10577 for y in 0..H {
10578 for x in 0..W {
10579 let i = y * W + x;
10580 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;
10584 }
10585 }
10586 }
10587
10588 let mut dst = TensorDyn::image(
10589 W,
10590 H,
10591 PixelFormat::PlanarRgb,
10592 DType::F16,
10593 Some(TensorMemory::Mem),
10594 edgefirst_tensor::CpuAccess::ReadWrite,
10595 )
10596 .unwrap();
10597
10598 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
10599 backend: ComputeBackend::Cpu,
10600 ..Default::default()
10601 })
10602 .unwrap();
10603 proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
10604 .expect("forced-CPU Rgba→PlanarRgb F16 must not error");
10605
10606 let src_map = src.as_u8().unwrap().map().unwrap();
10607 let src_bytes = src_map.as_slice();
10608 let dst_map = dst.as_f16().unwrap().map().unwrap();
10609 let dst_halfs = dst_map.as_slice();
10610
10611 let plane = W * H;
10612 assert_eq!(dst_halfs.len(), plane * 3, "wrong output element count");
10613
10614 for y in 0..H {
10615 for x in 0..W {
10616 let i = y * W + x;
10617 let r_exp = src_bytes[i * 4] as f32 / 255.0;
10618 let g_exp = src_bytes[i * 4 + 1] as f32 / 255.0;
10619 let b_exp = src_bytes[i * 4 + 2] as f32 / 255.0;
10620
10621 let r_got = dst_halfs[i].to_f32();
10622 let g_got = dst_halfs[plane + i].to_f32();
10623 let b_got = dst_halfs[2 * plane + i].to_f32();
10624
10625 assert!(
10626 (r_got - r_exp).abs() <= TOL,
10627 "R plane ({x},{y}): got {r_got}, expected {r_exp}"
10628 );
10629 assert!(
10630 (g_got - g_exp).abs() <= TOL,
10631 "G plane ({x},{y}): got {g_got}, expected {g_exp}"
10632 );
10633 assert!(
10634 (b_got - b_exp).abs() <= TOL,
10635 "B plane ({x},{y}): got {b_got}, expected {b_exp}"
10636 );
10637
10638 if src_bytes[i * 4] != src_bytes[i * 4 + 1] {
10640 assert_ne!(r_got, g_got, "R and G planes must differ at ({x},{y})");
10641 }
10642 }
10643 }
10644 }
10645
10646 #[test]
10654 fn convert_f32_with_rotation_falls_back() {
10655 const W: usize = 16;
10656 const H: usize = 16;
10657
10658 let src = TensorDyn::image(
10660 W,
10661 H,
10662 PixelFormat::Rgba,
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 let data = map.as_mut_slice();
10671 for y in 0..H {
10672 for x in 0..W {
10673 let i = y * W + x;
10674 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;
10678 }
10679 }
10680 }
10681
10682 let mut dst = TensorDyn::image(
10684 H, W, PixelFormat::Rgb,
10687 DType::F32,
10688 Some(TensorMemory::Mem),
10689 edgefirst_tensor::CpuAccess::ReadWrite,
10690 )
10691 .unwrap();
10692
10693 let mut proc = ImageProcessor::new().unwrap();
10694 let result = proc.convert(
10695 &src,
10696 &mut dst,
10697 Rotation::Clockwise90,
10698 Flip::None,
10699 Crop::default(),
10700 );
10701 assert!(
10702 result.is_ok(),
10703 "auto-chain Rgba→Rgb F32 with Rot90 must not error: {:?}",
10704 result.err()
10705 );
10706
10707 let map = dst.as_f32().unwrap().map().unwrap();
10708 let floats = map.as_slice();
10709 assert_eq!(floats.len(), H * W * 3, "unexpected output element count");
10710 for (i, &v) in floats.iter().enumerate() {
10711 assert!(
10712 v.is_finite() && (0.0..=1.0).contains(&v),
10713 "output[{i}]={v} is not finite or not in [0,1]"
10714 );
10715 }
10716 }
10717
10718 #[test]
10725 #[cfg(all(target_os = "linux", feature = "opengl"))]
10726 fn convert_f16_gl_cpu_parity_identity() {
10727 if !is_opengl_available() {
10728 eprintln!("SKIPPED: convert_f16_gl_cpu_parity_identity - OpenGL not available");
10729 return;
10730 }
10731
10732 const W: usize = 16;
10733 const H: usize = 16;
10734 const TOL: f32 = 1.0 / 256.0; let src = TensorDyn::image(
10738 W,
10739 H,
10740 PixelFormat::Rgba,
10741 DType::U8,
10742 Some(TensorMemory::Mem),
10743 edgefirst_tensor::CpuAccess::ReadWrite,
10744 )
10745 .unwrap();
10746 {
10747 let mut map = src.as_u8().unwrap().map().unwrap();
10748 let data = map.as_mut_slice();
10749 for y in 0..H {
10750 for x in 0..W {
10751 let i = y * W + x;
10752 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;
10756 }
10757 }
10758 }
10759
10760 let gl_result = {
10762 let mut gl_proc = match ImageProcessor::with_config(ImageProcessorConfig {
10763 backend: ComputeBackend::OpenGl,
10764 ..Default::default()
10765 }) {
10766 Ok(p) => p,
10767 Err(e) => {
10768 eprintln!(
10769 "SKIPPED: convert_f16_gl_cpu_parity_identity - GL backend unavailable: {e}"
10770 );
10771 return;
10772 }
10773 };
10774
10775 if !gl_proc.supported_render_dtypes().f16 {
10776 eprintln!("SKIPPED: convert_f16_gl_cpu_parity_identity - F16 render not supported");
10777 return;
10778 }
10779
10780 let mut dst = TensorDyn::image(
10781 W,
10782 H,
10783 PixelFormat::PlanarRgb,
10784 DType::F16,
10785 Some(TensorMemory::Mem),
10786 edgefirst_tensor::CpuAccess::ReadWrite,
10787 )
10788 .unwrap();
10789 match gl_proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default()) {
10790 Ok(()) => dst,
10791 Err(e) => {
10792 eprintln!(
10793 "SKIPPED: convert_f16_gl_cpu_parity_identity - GL convert failed: {e}"
10794 );
10795 return;
10796 }
10797 }
10798 };
10799
10800 let cpu_result = {
10802 let mut cpu_proc = ImageProcessor::with_config(ImageProcessorConfig {
10803 backend: ComputeBackend::Cpu,
10804 ..Default::default()
10805 })
10806 .unwrap();
10807 let mut dst = TensorDyn::image(
10808 W,
10809 H,
10810 PixelFormat::PlanarRgb,
10811 DType::F16,
10812 Some(TensorMemory::Mem),
10813 edgefirst_tensor::CpuAccess::ReadWrite,
10814 )
10815 .unwrap();
10816 cpu_proc
10817 .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
10818 .expect("forced-CPU Rgba→PlanarRgb F16 must not error");
10819 dst
10820 };
10821
10822 let gl_map = gl_result.as_f16().unwrap().map().unwrap();
10824 let cpu_map = cpu_result.as_f16().unwrap().map().unwrap();
10825 let gl_halfs = gl_map.as_slice();
10826 let cpu_halfs = cpu_map.as_slice();
10827
10828 assert_eq!(
10829 gl_halfs.len(),
10830 cpu_halfs.len(),
10831 "GL and CPU output sizes differ"
10832 );
10833
10834 let plane = W * H;
10835 let channel_names = ["R", "G", "B"];
10836 for (idx, (gl_h, cpu_h)) in gl_halfs.iter().zip(cpu_halfs.iter()).enumerate() {
10837 let gl_v = gl_h.to_f32();
10838 let cpu_v = cpu_h.to_f32();
10839 let err = (gl_v - cpu_v).abs();
10840 let ch = channel_names[idx / plane];
10841 let pixel = idx % plane;
10842 assert!(
10843 err <= TOL,
10844 "GL vs CPU mismatch at {ch}[{pixel}]: GL={gl_v}, CPU={cpu_v}, err={err} > tol={TOL}"
10845 );
10846 }
10847 }
10848
10849 #[test]
10856 #[cfg(all(target_os = "linux", feature = "opengl"))]
10857 fn supported_render_dtypes_linux_smoke() {
10858 let proc = match ImageProcessor::new() {
10859 Ok(p) => p,
10860 Err(e) => {
10861 eprintln!("SKIPPED: supported_render_dtypes_linux_smoke — ImageProcessor::new() failed: {e}");
10862 return;
10863 }
10864 };
10865 if proc.opengl.is_none() {
10866 eprintln!("SKIPPED: supported_render_dtypes_linux_smoke — no GL backend on this host");
10867 return;
10868 }
10869 let support = proc.supported_render_dtypes();
10871 eprintln!(
10872 "supported_render_dtypes_linux_smoke: f16={} f32={}",
10873 support.f16, support.f32
10874 );
10875 }
10877
10878 #[test]
10887 fn convert_f16_pbo_non_4_aligned_width_falls_back() {
10888 const W: usize = 18; const H: usize = 16;
10890
10891 let src = TensorDyn::image(
10893 W,
10894 H,
10895 PixelFormat::Rgba,
10896 DType::U8,
10897 Some(TensorMemory::Mem),
10898 edgefirst_tensor::CpuAccess::ReadWrite,
10899 )
10900 .unwrap();
10901 {
10902 let mut map = src.as_u8().unwrap().map().unwrap();
10903 let data = map.as_mut_slice();
10904 for chunk in data.chunks_exact_mut(4) {
10905 chunk[0] = 128;
10906 chunk[1] = 64;
10907 chunk[2] = 200;
10908 chunk[3] = 255;
10909 }
10910 }
10911
10912 let mut dst = TensorDyn::image(
10915 W,
10916 H,
10917 PixelFormat::PlanarRgb,
10918 DType::F16,
10919 Some(TensorMemory::Mem),
10920 edgefirst_tensor::CpuAccess::ReadWrite,
10921 )
10922 .unwrap();
10923
10924 let mut proc = ImageProcessor::new().unwrap();
10927 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
10928 assert!(
10929 result.is_ok(),
10930 "auto-chain PlanarRgb F16 W%4!=0 must not error (CPU fallback): {:?}",
10931 result.err()
10932 );
10933
10934 let map = dst.as_f16().unwrap().map().unwrap();
10936 let halfs = map.as_slice();
10937 assert_eq!(halfs.len(), W * H * 3, "unexpected element count");
10938 for (i, h) in halfs.iter().enumerate() {
10939 let v = h.to_f32();
10940 assert!(
10941 v.is_finite() && (0.0..=1.0).contains(&v),
10942 "output[{i}]={v} is not finite or not in [0,1]"
10943 );
10944 }
10945 }
10946
10947 #[test]
10957 #[allow(clippy::needless_update)]
10960 fn convert_nv12_to_rgb_f32_cpu() {
10961 const W: usize = 16;
10962 const H: usize = 16; let src = TensorDyn::image(
10966 W,
10967 H,
10968 PixelFormat::Nv12,
10969 DType::U8,
10970 Some(TensorMemory::Mem),
10971 edgefirst_tensor::CpuAccess::ReadWrite,
10972 )
10973 .unwrap();
10974 {
10975 let mut map = src.as_u8().unwrap().map().unwrap();
10976 map.as_mut_slice().fill(128); }
10978
10979 let mut dst = TensorDyn::image(
10980 W,
10981 H,
10982 PixelFormat::Rgb,
10983 DType::F32,
10984 Some(TensorMemory::Mem),
10985 edgefirst_tensor::CpuAccess::ReadWrite,
10986 )
10987 .unwrap();
10988
10989 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
10990 backend: ComputeBackend::Cpu,
10991 ..Default::default()
10992 })
10993 .unwrap();
10994
10995 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
10996 assert!(
10997 result.is_ok(),
10998 "forced-CPU NV12→Rgb F32 must not error: {:?}",
10999 result.err()
11000 );
11001
11002 let map = dst.as_f32().unwrap().map().unwrap();
11003 let floats = map.as_slice();
11004 assert_eq!(floats.len(), W * H * 3, "unexpected element count");
11005 for (i, &v) in floats.iter().enumerate() {
11006 assert!(
11007 v.is_finite() && (0.0..=1.0).contains(&v),
11008 "output[{i}]={v} is not finite or not in [0,1]"
11009 );
11010 }
11011 let non_zero = floats.iter().any(|&v| v > 0.01);
11013 assert!(non_zero, "all-zero output from NV12→Rgb F32 CPU path");
11014 }
11015
11016 #[test]
11020 #[allow(clippy::needless_update)]
11023 fn convert_nv12_to_planar_rgb_f16_cpu() {
11024 const W: usize = 16;
11025 const H: usize = 16;
11026
11027 let src = TensorDyn::image(
11028 W,
11029 H,
11030 PixelFormat::Nv12,
11031 DType::U8,
11032 Some(TensorMemory::Mem),
11033 edgefirst_tensor::CpuAccess::ReadWrite,
11034 )
11035 .unwrap();
11036 {
11037 let mut map = src.as_u8().unwrap().map().unwrap();
11038 map.as_mut_slice().fill(128);
11039 }
11040
11041 let mut dst = TensorDyn::image(
11042 W,
11043 H,
11044 PixelFormat::PlanarRgb,
11045 DType::F16,
11046 Some(TensorMemory::Mem),
11047 edgefirst_tensor::CpuAccess::ReadWrite,
11048 )
11049 .unwrap();
11050
11051 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
11052 backend: ComputeBackend::Cpu,
11053 ..Default::default()
11054 })
11055 .unwrap();
11056
11057 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
11058 assert!(
11059 result.is_ok(),
11060 "forced-CPU NV12→PlanarRgb F16 must not error: {:?}",
11061 result.err()
11062 );
11063
11064 let map = dst.as_f16().unwrap().map().unwrap();
11065 let halfs = map.as_slice();
11066 assert_eq!(halfs.len(), W * H * 3, "unexpected element count");
11067 for (i, h) in halfs.iter().enumerate() {
11068 let v = h.to_f32();
11069 assert!(
11070 v.is_finite() && (0.0..=1.0).contains(&v),
11071 "output[{i}]={v} is not finite or not in [0,1]"
11072 );
11073 }
11074 let non_zero = halfs.iter().any(|h| h.to_f32() > 0.01);
11075 assert!(non_zero, "all-zero output from NV12→PlanarRgb F16 CPU path");
11076 }
11077
11078 #[test]
11087 fn create_image_desc_negotiates_and_counts_fallbacks() {
11088 use edgefirst_tensor::{Compression, CpuAccess, ImageDesc};
11089 let proc = ImageProcessor::new().unwrap();
11090
11091 let desc =
11092 ImageDesc::new(64, 64, PixelFormat::Rgba, DType::U8).with_access(CpuAccess::ReadWrite);
11093 let plain = proc.create_image_desc(&desc).unwrap();
11094 let classic = proc
11095 .create_image(
11096 64,
11097 64,
11098 PixelFormat::Rgba,
11099 DType::U8,
11100 None,
11101 CpuAccess::ReadWrite,
11102 )
11103 .unwrap();
11104 assert_eq!(plain.memory(), classic.memory(), "same negotiation path");
11105 assert_eq!(plain.compression(), None);
11106
11107 #[cfg(not(target_os = "android"))]
11108 {
11109 let before = proc.compression_fallback_count();
11110 let desc = ImageDesc::new(64, 64, PixelFormat::Rgba, DType::U8)
11111 .with_compression(Compression::Any);
11112 let t = proc.create_image_desc(&desc).unwrap();
11113 assert_eq!(t.compression(), None, "no vendor tile scheme off-Android");
11114 assert!(
11115 proc.compression_fallback_count() > before,
11116 "Any resolving linear must count"
11117 );
11118 }
11119 }
11120
11121 #[test]
11124 #[cfg(target_os = "linux")]
11125 fn create_image_f32_dma_rejected() {
11126 let proc = ImageProcessor::new().unwrap();
11127 let result = proc.create_image(
11128 64,
11129 64,
11130 PixelFormat::Rgb,
11131 DType::F32,
11132 Some(TensorMemory::Dma),
11133 edgefirst_tensor::CpuAccess::ReadWrite,
11134 );
11135 assert!(
11136 result.is_err(),
11137 "create_image(F32, Dma) must fail — no DRM fourcc for f32"
11138 );
11139 }
11140
11141 #[test]
11150 #[cfg(target_os = "linux")]
11151 fn import_image_carries_colorimetry() {
11152 use edgefirst_tensor::{ColorEncoding, ColorRange, Colorimetry, TensorMemory};
11153
11154 let expected = Colorimetry::default()
11155 .with_encoding(ColorEncoding::Bt709)
11156 .with_range(ColorRange::Limited);
11157
11158 if !is_dma_available() {
11159 let mut t = TensorDyn::image(
11162 8,
11163 8,
11164 PixelFormat::Rgba,
11165 DType::U8,
11166 Some(TensorMemory::Mem),
11167 edgefirst_tensor::CpuAccess::ReadWrite,
11168 )
11169 .expect("alloc");
11170 assert_eq!(t.colorimetry(), None, "colorimetry must start as None");
11171 t.set_colorimetry(Some(expected));
11172 assert_eq!(
11173 t.colorimetry(),
11174 Some(expected),
11175 "set_colorimetry must round-trip"
11176 );
11177 eprintln!("SKIPPED import_image_carries_colorimetry (DMA unavailable); storage contract verified via TensorDyn");
11178 return;
11179 }
11180
11181 use edgefirst_tensor::{PlaneDescriptor, Tensor};
11184
11185 let rgba_bytes = 64 * 64 * 4; let dma_tensor =
11187 Tensor::<u8>::new(&[rgba_bytes], Some(TensorMemory::Dma), Some("import_test"))
11188 .expect("dma alloc");
11189 let pd =
11190 PlaneDescriptor::new(dma_tensor.dmabuf().expect("dma fd")).expect("PlaneDescriptor");
11191
11192 let proc = ImageProcessor::new().expect("ImageProcessor");
11193 let result = proc.import_image(
11194 pd,
11195 None,
11196 64,
11197 64,
11198 PixelFormat::Rgba,
11199 DType::U8,
11200 Some(expected),
11201 );
11202 let tensor = result.expect("import_image must succeed on DMA fd");
11203 assert_eq!(
11204 tensor.colorimetry(),
11205 Some(expected),
11206 "import_image must store the supplied colorimetry on the returned TensorDyn"
11207 );
11208 }
11209}