1#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
88
89#[cfg(all(coverage, target_os = "linux"))]
93#[used]
94#[link_section = ".init_array"]
95static __EDGEFIRST_COV_INSTALL: extern "C" fn() = {
96 extern "C" fn ctor() {
97 edgefirst_tensor::covguard::install();
98 }
99 ctor
100};
101
102pub const GPU_DMA_BUF_PITCH_ALIGNMENT_BYTES: usize = 64;
115
116pub fn align_width_for_gpu_pitch(width: usize, bpp: usize) -> usize {
154 if bpp == 0 || width == 0 {
155 return width;
156 }
157
158 let Some(lcm_alignment) = checked_num_integer_lcm(GPU_DMA_BUF_PITCH_ALIGNMENT_BYTES, bpp)
167 else {
168 log::warn!(
169 "align_width_for_gpu_pitch: lcm({GPU_DMA_BUF_PITCH_ALIGNMENT_BYTES}, {bpp}) \
170 overflows usize, returning unaligned width {width}"
171 );
172 return width;
173 };
174 if lcm_alignment == 0 {
175 return width;
176 }
177
178 debug_assert_eq!(lcm_alignment % bpp, 0);
179 let width_alignment = lcm_alignment / bpp;
180 if width_alignment == 0 {
181 return width;
182 }
183
184 let remainder = width % width_alignment;
185 if remainder == 0 {
186 return width;
187 }
188
189 let pad = width_alignment - remainder;
190 match width.checked_add(pad) {
191 Some(aligned) => aligned,
192 None => {
193 log::warn!(
194 "align_width_for_gpu_pitch: width {width} + pad {pad} overflows usize, \
195 returning unaligned (caller should use a smaller width or pre-aligned size)"
196 );
197 width
198 }
199 }
200}
201
202#[cfg(target_os = "linux")]
211pub(crate) fn align_pitch_bytes_to_gpu_alignment(min_pitch_bytes: usize) -> Option<usize> {
212 let alignment = GPU_DMA_BUF_PITCH_ALIGNMENT_BYTES;
213 if min_pitch_bytes == 0 {
214 return Some(0);
215 }
216 let remainder = min_pitch_bytes % alignment;
217 if remainder == 0 {
218 return Some(min_pitch_bytes);
219 }
220 min_pitch_bytes.checked_add(alignment - remainder)
221}
222
223fn checked_num_integer_lcm(a: usize, b: usize) -> Option<usize> {
226 if a == 0 || b == 0 {
227 return Some(0);
228 }
229 let g = num_integer_gcd(a, b);
230 (a / g).checked_mul(b)
233}
234
235fn num_integer_gcd(a: usize, b: usize) -> usize {
236 if b == 0 {
237 a
238 } else {
239 num_integer_gcd(b, a % b)
240 }
241}
242
243pub fn primary_plane_bpp(format: PixelFormat, elem: usize) -> Option<usize> {
259 use edgefirst_tensor::PixelLayout;
260 match format.layout() {
261 PixelLayout::Packed => Some(format.channels() * elem),
262 PixelLayout::Planar => Some(elem),
263 PixelLayout::SemiPlanar => Some(elem),
267 _ => None,
270 }
271}
272
273#[cfg(all(target_os = "linux", test))]
286pub(crate) fn padded_dma_pitch_for(
287 fmt: PixelFormat,
288 width: usize,
289 memory: &Option<TensorMemory>,
290) -> Option<usize> {
291 match memory {
301 Some(TensorMemory::Dma) => {}
302 None if edgefirst_tensor::is_dma_available() => {}
303 _ => return None,
304 }
305 if fmt.layout() != PixelLayout::Packed {
309 return None;
310 }
311 let bpp = primary_plane_bpp(fmt, 1)?;
312 let natural = width.checked_mul(bpp)?;
313 let aligned = align_pitch_bytes_to_gpu_alignment(natural)?;
314 if aligned > natural {
315 Some(aligned)
316 } else {
317 None
318 }
319}
320
321pub use cpu::CPUProcessor;
322pub use edgefirst_codec as codec;
323
324#[cfg(test)]
325use edgefirst_decoder::ProtoLayout;
326use edgefirst_decoder::{DetectBox, ProtoData, Segmentation};
327#[doc(inline)]
328pub use edgefirst_tensor::Region;
329#[cfg(any(test, all(target_os = "linux", feature = "opengl")))]
330use edgefirst_tensor::Tensor;
331use edgefirst_tensor::{
332 DType, PixelFormat, PixelLayout, TensorDyn, TensorMemory, TensorTrait as _,
333};
334use enum_dispatch::enum_dispatch;
335pub use error::{Error, Result};
336#[cfg(target_os = "linux")]
337pub use g2d::G2DProcessor;
338#[cfg(all(
339 any(
340 target_os = "linux",
341 target_os = "macos",
342 target_os = "ios",
343 target_os = "android"
344 ),
345 feature = "opengl"
346))]
347pub use opengl_headless::EglDisplayKind;
348#[cfg(all(
349 any(
350 target_os = "linux",
351 target_os = "macos",
352 target_os = "ios",
353 target_os = "android"
354 ),
355 feature = "opengl"
356))]
357pub use opengl_headless::GLProcessorThreaded;
358#[cfg(all(
359 any(
360 target_os = "linux",
361 target_os = "macos",
362 target_os = "ios",
363 target_os = "android"
364 ),
365 feature = "opengl"
366))]
367pub use opengl_headless::Int8InterpolationMode;
368#[cfg(target_os = "linux")]
369#[cfg(feature = "opengl")]
370pub use opengl_headless::{probe_egl_displays, EglDisplayInfo};
371#[cfg(all(
375 any(
376 target_os = "linux",
377 target_os = "macos",
378 target_os = "ios",
379 target_os = "android"
380 ),
381 feature = "opengl"
382))]
383pub use opengl_headless::{CacheStats, ConvertStats, GlCacheStats};
384use std::{fmt::Display, time::Instant};
385
386mod colorimetry;
387mod cpu;
388mod error;
389mod g2d;
390#[path = "gl/mod.rs"]
391mod opengl_headless;
392mod tiling;
393pub use tiling::{tile_grid, TilePlacement, TileSpec, TilingConfig};
394
395#[derive(Debug, Clone, Copy, PartialEq, Eq)]
399pub enum Rotation {
400 None = 0,
401 Clockwise90 = 1,
402 Rotate180 = 2,
403 CounterClockwise90 = 3,
404}
405impl Rotation {
406 pub fn from_degrees_clockwise(angle: usize) -> Rotation {
419 match angle.rem_euclid(360) {
420 0 => Rotation::None,
421 90 => Rotation::Clockwise90,
422 180 => Rotation::Rotate180,
423 270 => Rotation::CounterClockwise90,
424 _ => panic!("rotation angle is not a multiple of 90"),
425 }
426 }
427}
428
429#[derive(Debug, Clone, Copy, PartialEq, Eq)]
430pub enum Flip {
431 None = 0,
432 Vertical = 1,
433 Horizontal = 2,
434}
435
436#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
438pub enum ColorMode {
439 #[default]
444 Class,
445 Instance,
450 Track,
453}
454
455impl ColorMode {
456 #[inline]
458 pub fn index(self, idx: usize, label: usize) -> usize {
459 match self {
460 ColorMode::Class => label,
461 ColorMode::Instance | ColorMode::Track => idx,
462 }
463 }
464}
465
466#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
487pub enum MaskResolution {
488 #[default]
490 Proto,
491 Scaled {
495 width: u32,
497 height: u32,
499 },
500}
501
502#[derive(Debug, Clone, Copy)]
518pub struct MaskOverlay<'a> {
519 pub background: Option<&'a TensorDyn>,
523 pub opacity: f32,
524 pub letterbox: Option<[f32; 4]>,
534 pub color_mode: ColorMode,
535}
536
537impl Default for MaskOverlay<'_> {
538 fn default() -> Self {
539 Self {
540 background: None,
541 opacity: 1.0,
542 letterbox: None,
543 color_mode: ColorMode::Class,
544 }
545 }
546}
547
548impl<'a> MaskOverlay<'a> {
549 pub fn new() -> Self {
550 Self::default()
551 }
552
553 pub fn with_background(mut self, bg: &'a TensorDyn) -> Self {
561 self.background = Some(bg);
562 self
563 }
564
565 pub fn with_opacity(mut self, opacity: f32) -> Self {
566 self.opacity = opacity.clamp(0.0, 1.0);
567 self
568 }
569
570 pub fn with_color_mode(mut self, mode: ColorMode) -> Self {
571 self.color_mode = mode;
572 self
573 }
574
575 pub fn with_letterbox_crop(
585 mut self,
586 crop: &Crop,
587 src_w: usize,
588 src_h: usize,
589 model_w: usize,
590 model_h: usize,
591 ) -> Self {
592 if let Ok(resolved) = crop.resolve(src_w, src_h, model_w, model_h) {
595 if let Some(r) = resolved.dst_rect {
596 self.letterbox = Some([
597 r.left as f32 / model_w as f32,
598 r.top as f32 / model_h as f32,
599 (r.left + r.width) as f32 / model_w as f32,
600 (r.top + r.height) as f32 / model_h as f32,
601 ]);
602 }
603 }
604 self
605 }
606}
607
608#[inline]
621fn unletter_bbox(bbox: DetectBox, lb: [f32; 4]) -> DetectBox {
622 DetectBox {
623 bbox: edgefirst_decoder::tiling::unletter_norm(bbox.bbox, lb),
624 ..bbox
625 }
626}
627
628#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
630pub enum Fit {
631 #[default]
633 Stretch,
634 Letterbox { pad: [u8; 4] },
638}
639
640#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
645pub struct Crop {
646 pub source: Option<Region>,
648 pub fit: Fit,
650}
651
652impl Crop {
653 pub fn new() -> Self {
655 Self::default()
656 }
657
658 pub fn no_crop() -> Self {
660 Self::default()
661 }
662
663 pub fn letterbox(pad: [u8; 4]) -> Self {
666 Self {
667 source: None,
668 fit: Fit::Letterbox { pad },
669 }
670 }
671
672 pub fn with_source(mut self, source: Option<Region>) -> Self {
674 self.source = source;
675 self
676 }
677
678 pub fn with_fit(mut self, fit: Fit) -> Self {
680 self.fit = fit;
681 self
682 }
683
684 pub(crate) fn resolve(
690 &self,
691 src_w: usize,
692 src_h: usize,
693 dst_w: usize,
694 dst_h: usize,
695 ) -> Result<ResolvedCrop, Error> {
696 let src_rect = self.source.map(region_to_rect);
697 let (sw, sh) = match self.source {
700 Some(r) => (r.width, r.height),
701 None => (src_w, src_h),
702 };
703 let resolved = match self.fit {
704 Fit::Stretch => ResolvedCrop {
705 src_rect,
706 dst_rect: None,
707 dst_color: None,
708 },
709 Fit::Letterbox { pad } => ResolvedCrop {
710 src_rect,
711 dst_rect: Some(letterbox_rect(sw, sh, dst_w, dst_h)),
712 dst_color: Some(pad),
713 },
714 };
715 resolved.check_crop_dims(src_w, src_h, dst_w, dst_h)?;
716 Ok(resolved)
717 }
718
719 pub fn check_crop_dyn(
721 &self,
722 src: &edgefirst_tensor::TensorDyn,
723 dst: &edgefirst_tensor::TensorDyn,
724 ) -> Result<(), Error> {
725 self.resolve(
726 src.width().unwrap_or(0),
727 src.height().unwrap_or(0),
728 dst.width().unwrap_or(0),
729 dst.height().unwrap_or(0),
730 )
731 .map(|_| ())
732 }
733}
734
735#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
739pub(crate) struct ResolvedCrop {
740 pub(crate) src_rect: Option<Rect>,
741 pub(crate) dst_rect: Option<Rect>,
742 pub(crate) dst_color: Option<[u8; 4]>,
743}
744
745impl ResolvedCrop {
746 #[allow(dead_code)] pub(crate) fn no_crop() -> Self {
749 Self::default()
750 }
751
752 pub(crate) fn check_crop_dims(
754 &self,
755 src_w: usize,
756 src_h: usize,
757 dst_w: usize,
758 dst_h: usize,
759 ) -> Result<(), Error> {
760 let src_ok = self
761 .src_rect
762 .is_none_or(|r| r.left + r.width <= src_w && r.top + r.height <= src_h);
763 let dst_ok = self
764 .dst_rect
765 .is_none_or(|r| r.left + r.width <= dst_w && r.top + r.height <= dst_h);
766 match (src_ok, dst_ok) {
767 (true, true) => Ok(()),
768 (true, false) => Err(Error::CropInvalid(format!(
769 "Dest crop invalid: {:?}",
770 self.dst_rect
771 ))),
772 (false, true) => Err(Error::CropInvalid(format!(
773 "Src crop invalid: {:?}",
774 self.src_rect
775 ))),
776 (false, false) => Err(Error::CropInvalid(format!(
777 "Dest and Src crop invalid: {:?} {:?}",
778 self.dst_rect, self.src_rect
779 ))),
780 }
781 }
782}
783
784fn region_to_rect(r: Region) -> Rect {
786 Rect {
787 left: r.x,
788 top: r.y,
789 width: r.width,
790 height: r.height,
791 }
792}
793
794fn letterbox_rect(sw: usize, sh: usize, dw: usize, dh: usize) -> Rect {
798 if sw == 0 || sh == 0 {
799 return Rect::new(0, 0, dw, dh);
800 }
801 let src_aspect = sw as f64 / sh as f64;
802 let dst_aspect = dw as f64 / dh as f64;
803 let (new_w, new_h) = if src_aspect > dst_aspect {
804 (dw, ((dw as f64 / src_aspect).round() as usize).max(1))
805 } else {
806 (((dh as f64 * src_aspect).round() as usize).max(1), dh)
807 };
808 let left = dw.saturating_sub(new_w) / 2;
809 let top = dh.saturating_sub(new_h) / 2;
810 Rect::new(left, top, new_w, new_h)
811}
812
813#[derive(Debug, Clone, Copy, PartialEq, Eq)]
818pub(crate) struct Rect {
819 pub left: usize,
820 pub top: usize,
821 pub width: usize,
822 pub height: usize,
823}
824
825impl Rect {
826 pub fn new(left: usize, top: usize, width: usize, height: usize) -> Self {
828 Self {
829 left,
830 top,
831 width,
832 height,
833 }
834 }
835}
836
837#[enum_dispatch(ImageProcessor)]
838pub trait ImageProcessorTrait {
839 fn convert(
876 &mut self,
877 src: &TensorDyn,
878 dst: &mut TensorDyn,
879 rotation: Rotation,
880 flip: Flip,
881 crop: Crop,
882 ) -> Result<()>;
883
884 fn draw_decoded_masks(
941 &mut self,
942 dst: &mut TensorDyn,
943 detect: &[DetectBox],
944 segmentation: &[Segmentation],
945 overlay: MaskOverlay<'_>,
946 ) -> Result<()>;
947
948 fn draw_proto_masks(
968 &mut self,
969 dst: &mut TensorDyn,
970 detect: &[DetectBox],
971 proto_data: &ProtoData,
972 overlay: MaskOverlay<'_>,
973 ) -> Result<()>;
974
975 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()>;
978
979 fn convert_deferred(
996 &mut self,
997 src: &TensorDyn,
998 dst: &mut TensorDyn,
999 rotation: Rotation,
1000 flip: Flip,
1001 crop: Crop,
1002 ) -> Result<()> {
1003 self.convert(src, dst, rotation, flip, crop)
1004 }
1005
1006 fn flush(&mut self) -> Result<()> {
1013 Ok(())
1014 }
1015}
1016
1017#[derive(Debug, Clone, Default)]
1023pub struct ImageProcessorConfig {
1024 #[cfg(all(
1034 any(
1035 target_os = "linux",
1036 target_os = "macos",
1037 target_os = "ios",
1038 target_os = "android"
1039 ),
1040 feature = "opengl"
1041 ))]
1042 pub egl_display: Option<EglDisplayKind>,
1043
1044 pub backend: ComputeBackend,
1056
1057 pub colorimetry: ColorimetryMode,
1062}
1063
1064#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1079pub enum ColorimetryMode {
1080 #[default]
1084 Fast,
1085 Exact,
1088}
1089
1090#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1097pub enum ComputeBackend {
1098 #[default]
1100 Auto,
1101 Cpu,
1103 G2d,
1105 OpenGl,
1107}
1108
1109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1115pub(crate) enum ForcedBackend {
1116 Cpu,
1117 G2d,
1118 OpenGl,
1119}
1120
1121#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1142pub struct RenderDtypeSupport {
1143 pub f32: bool,
1148 pub f16: bool,
1154}
1155
1156#[cfg(all(target_os = "linux", feature = "opengl"))]
1170pub(crate) fn float_pbo_eligible(dtype: DType, support: RenderDtypeSupport) -> bool {
1171 match dtype {
1172 DType::F16 => support.f16,
1173 DType::F32 => support.f32,
1174 _ => false,
1175 }
1176}
1177
1178#[derive(Debug)]
1181pub struct ImageProcessor {
1182 pub cpu: Option<CPUProcessor>,
1185
1186 #[cfg(target_os = "linux")]
1187 pub g2d: Option<G2DProcessor>,
1191 #[cfg(target_os = "linux")]
1192 #[cfg(feature = "opengl")]
1193 pub opengl: Option<GLProcessorThreaded>,
1197 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
1198 #[cfg(feature = "opengl")]
1199 pub opengl: Option<GLProcessorThreaded>,
1206
1207 pub(crate) forced_backend: Option<ForcedBackend>,
1209
1210 pub(crate) convert_fallbacks: std::sync::atomic::AtomicU64,
1216}
1217
1218unsafe impl Send for ImageProcessor {}
1219unsafe impl Sync for ImageProcessor {}
1220
1221impl ImageProcessor {
1222 pub fn new() -> Result<Self> {
1248 Self::with_config(ImageProcessorConfig::default())
1249 }
1250
1251 pub fn convert_fallback_count(&self) -> u64 {
1258 self.convert_fallbacks
1259 .load(std::sync::atomic::Ordering::Relaxed)
1260 }
1261
1262 pub fn compression_fallback_count(&self) -> u64 {
1271 edgefirst_tensor::compression_fallback_count()
1272 }
1273
1274 #[cfg(unix)]
1288 pub fn convert_with_fence(
1289 &mut self,
1290 src: &TensorDyn,
1291 dst: &mut TensorDyn,
1292 rotation: Rotation,
1293 flip: Flip,
1294 crop: Crop,
1295 ) -> Result<Option<std::os::fd::OwnedFd>> {
1296 #[cfg(any(
1297 target_os = "linux",
1298 target_os = "macos",
1299 target_os = "ios",
1300 target_os = "android"
1301 ))]
1302 #[cfg(feature = "opengl")]
1303 {
1304 let gl_forced = matches!(self.forced_backend, Some(ForcedBackend::OpenGl));
1305 if self.forced_backend.is_none() || gl_forced {
1306 if let Some(opengl) = self.opengl.as_mut() {
1307 match opengl.convert_with_fence(src, dst, rotation, flip, crop) {
1308 Ok(fd) => return Ok(fd),
1309 Err(e) if gl_forced => return Err(e),
1310 Err(e) => {
1311 log::debug!(
1315 "convert_with_fence: opengl declined, \
1316 falling back to the blocking chain: {e}"
1317 );
1318 }
1319 }
1320 } else if gl_forced {
1321 return Err(Error::ForcedBackendUnavailable("opengl".into()));
1322 }
1323 }
1324 }
1325 self.convert(src, dst, rotation, flip, crop)?;
1328 Ok(None)
1329 }
1330
1331 pub fn supported_render_dtypes(&self) -> RenderDtypeSupport {
1344 #[cfg(all(
1345 any(target_os = "macos", target_os = "ios", target_os = "android"),
1346 feature = "opengl"
1347 ))]
1348 if let Some(gl) = self.opengl.as_ref() {
1349 return gl.supported_render_dtypes();
1350 }
1351 #[cfg(all(target_os = "linux", feature = "opengl"))]
1352 if let Some(gl) = self.opengl.as_ref() {
1353 return gl.supported_render_dtypes();
1354 }
1355 RenderDtypeSupport {
1356 f32: false,
1357 f16: false,
1358 }
1359 }
1360
1361 #[allow(unused_variables)]
1370 pub fn with_config(config: ImageProcessorConfig) -> Result<Self> {
1371 match config.backend {
1375 ComputeBackend::Cpu => {
1376 log::info!("ComputeBackend::Cpu — CPU only");
1377 return Ok(Self {
1378 cpu: Some(CPUProcessor::new()),
1379 #[cfg(target_os = "linux")]
1380 g2d: None,
1381 #[cfg(target_os = "linux")]
1382 #[cfg(feature = "opengl")]
1383 opengl: None,
1384 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
1385 #[cfg(feature = "opengl")]
1386 opengl: None,
1387 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1388 forced_backend: None,
1389 });
1390 }
1391 ComputeBackend::G2d => {
1392 log::info!("ComputeBackend::G2d — G2D + CPU fallback");
1393 #[cfg(target_os = "linux")]
1394 {
1395 let g2d = match G2DProcessor::new() {
1396 Ok(g) => Some(g),
1397 Err(e) => {
1398 log::warn!("G2D requested but failed to initialize: {e:?}");
1399 None
1400 }
1401 };
1402 return Ok(Self {
1403 cpu: Some(CPUProcessor::new()),
1404 g2d,
1405 #[cfg(feature = "opengl")]
1406 opengl: None,
1407 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1408 forced_backend: None,
1409 });
1410 }
1411 #[cfg(not(target_os = "linux"))]
1412 {
1413 log::warn!("G2D requested but not available on this platform, using CPU");
1414 return Ok(Self {
1415 cpu: Some(CPUProcessor::new()),
1416 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
1417 #[cfg(feature = "opengl")]
1418 opengl: None,
1419 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1420 forced_backend: None,
1421 });
1422 }
1423 }
1424 ComputeBackend::OpenGl => {
1425 log::info!("ComputeBackend::OpenGl — OpenGL + CPU fallback");
1426 #[cfg(target_os = "linux")]
1427 {
1428 #[cfg(feature = "opengl")]
1429 let opengl = match GLProcessorThreaded::new(config.egl_display) {
1430 Ok(gl) => Some(gl),
1431 Err(e) => {
1432 log::warn!("OpenGL requested but failed to initialize: {e:?}");
1433 None
1434 }
1435 };
1436 return Ok(Self {
1437 cpu: Some(CPUProcessor::new()),
1438 g2d: None,
1439 #[cfg(feature = "opengl")]
1440 opengl,
1441 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1442 forced_backend: None,
1443 }
1444 .apply_colorimetry_mode(config.colorimetry));
1445 }
1446 #[cfg(any(target_os = "macos", target_os = "ios"))]
1447 {
1448 #[cfg(feature = "opengl")]
1449 let opengl = match GLProcessorThreaded::new(config.egl_display) {
1450 Ok(gl) => Some(gl),
1451 Err(e) => {
1452 log::warn!(
1453 "OpenGL requested on macOS but ANGLE init failed: {e:?} \
1454 (install ANGLE via `brew install startergo/angle/angle` \
1455 and re-sign the dylibs — see README.md § macOS GPU \
1456 Acceleration). Falling back to CPU."
1457 );
1458 None
1459 }
1460 };
1461 return Ok(Self {
1462 cpu: Some(CPUProcessor::new()),
1463 #[cfg(feature = "opengl")]
1464 opengl,
1465 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1466 forced_backend: None,
1467 }
1468 .apply_colorimetry_mode(config.colorimetry));
1469 }
1470 #[cfg(target_os = "android")]
1471 {
1472 #[cfg(feature = "opengl")]
1473 let opengl = match GLProcessorThreaded::new(config.egl_display) {
1474 Ok(gl) => Some(gl),
1475 Err(e) => {
1476 log::warn!(
1477 "OpenGL requested but native EGL init failed: {e:?}. \
1478 Falling back to CPU."
1479 );
1480 None
1481 }
1482 };
1483 return Ok(Self {
1484 cpu: Some(CPUProcessor::new()),
1485 #[cfg(feature = "opengl")]
1486 opengl,
1487 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1488 forced_backend: None,
1489 }
1490 .apply_colorimetry_mode(config.colorimetry));
1491 }
1492 #[cfg(not(any(
1493 target_os = "linux",
1494 target_os = "macos",
1495 target_os = "ios",
1496 target_os = "android"
1497 )))]
1498 {
1499 log::warn!("OpenGL requested but not available on this platform, using CPU");
1500 return Ok(Self {
1501 cpu: Some(CPUProcessor::new()),
1502 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1503 forced_backend: None,
1504 });
1505 }
1506 }
1507 ComputeBackend::Auto => { }
1508 }
1509
1510 if let Ok(val) = std::env::var("EDGEFIRST_FORCE_BACKEND") {
1515 let val_lower = val.to_lowercase();
1516 let forced = match val_lower.as_str() {
1517 "cpu" => ForcedBackend::Cpu,
1518 "g2d" => ForcedBackend::G2d,
1519 "opengl" => ForcedBackend::OpenGl,
1520 other => {
1521 return Err(Error::ForcedBackendUnavailable(format!(
1522 "unknown EDGEFIRST_FORCE_BACKEND value: {other:?} (expected cpu, g2d, or opengl)"
1523 )));
1524 }
1525 };
1526
1527 log::info!("EDGEFIRST_FORCE_BACKEND={val} — only initializing {val_lower} backend");
1528
1529 return match forced {
1530 ForcedBackend::Cpu => Ok(Self {
1531 cpu: Some(CPUProcessor::new()),
1532 #[cfg(target_os = "linux")]
1533 g2d: None,
1534 #[cfg(target_os = "linux")]
1535 #[cfg(feature = "opengl")]
1536 opengl: None,
1537 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
1538 #[cfg(feature = "opengl")]
1539 opengl: None,
1540 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1541 forced_backend: Some(ForcedBackend::Cpu),
1542 }),
1543 ForcedBackend::G2d => {
1544 #[cfg(target_os = "linux")]
1545 {
1546 let g2d = G2DProcessor::new().map_err(|e| {
1547 Error::ForcedBackendUnavailable(format!(
1548 "g2d forced but failed to initialize: {e:?}"
1549 ))
1550 })?;
1551 Ok(Self {
1552 cpu: None,
1553 g2d: Some(g2d),
1554 #[cfg(feature = "opengl")]
1555 opengl: None,
1556 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1557 forced_backend: Some(ForcedBackend::G2d),
1558 })
1559 }
1560 #[cfg(not(target_os = "linux"))]
1561 {
1562 Err(Error::ForcedBackendUnavailable(
1563 "g2d backend is only available on Linux".into(),
1564 ))
1565 }
1566 }
1567 ForcedBackend::OpenGl => {
1568 #[cfg(target_os = "linux")]
1569 #[cfg(feature = "opengl")]
1570 {
1571 let opengl = GLProcessorThreaded::new(config.egl_display).map_err(|e| {
1572 Error::ForcedBackendUnavailable(format!(
1573 "opengl forced but failed to initialize: {e:?}"
1574 ))
1575 })?;
1576 Ok(Self {
1577 cpu: None,
1578 g2d: None,
1579 opengl: Some(opengl),
1580 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1581 forced_backend: Some(ForcedBackend::OpenGl),
1582 }
1583 .apply_colorimetry_mode(config.colorimetry))
1584 }
1585 #[cfg(any(target_os = "macos", target_os = "ios"))]
1586 #[cfg(feature = "opengl")]
1587 {
1588 let opengl = GLProcessorThreaded::new(config.egl_display).map_err(|e| {
1589 Error::ForcedBackendUnavailable(format!(
1590 "opengl forced on macOS but ANGLE init failed: {e:?}"
1591 ))
1592 })?;
1593 Ok(Self {
1594 cpu: None,
1595 opengl: Some(opengl),
1596 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1597 forced_backend: Some(ForcedBackend::OpenGl),
1598 }
1599 .apply_colorimetry_mode(config.colorimetry))
1600 }
1601 #[cfg(target_os = "android")]
1602 #[cfg(feature = "opengl")]
1603 {
1604 let opengl = GLProcessorThreaded::new(config.egl_display).map_err(|e| {
1605 Error::ForcedBackendUnavailable(format!(
1606 "opengl forced but native EGL init failed: {e:?}"
1607 ))
1608 })?;
1609 Ok(Self {
1610 cpu: None,
1611 opengl: Some(opengl),
1612 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1613 forced_backend: Some(ForcedBackend::OpenGl),
1614 }
1615 .apply_colorimetry_mode(config.colorimetry))
1616 }
1617 #[cfg(not(all(
1618 any(
1619 target_os = "linux",
1620 target_os = "macos",
1621 target_os = "ios",
1622 target_os = "android"
1623 ),
1624 feature = "opengl"
1625 )))]
1626 {
1627 Err(Error::ForcedBackendUnavailable(
1628 "opengl backend requires Linux or macOS with the 'opengl' feature \
1629 enabled"
1630 .into(),
1631 ))
1632 }
1633 }
1634 };
1635 }
1636
1637 #[cfg(target_os = "linux")]
1639 let g2d = if std::env::var("EDGEFIRST_DISABLE_G2D")
1640 .map(|x| x != "0" && x.to_lowercase() != "false")
1641 .unwrap_or(false)
1642 {
1643 log::debug!("EDGEFIRST_DISABLE_G2D is set");
1644 None
1645 } else {
1646 match G2DProcessor::new() {
1647 Ok(g2d_converter) => Some(g2d_converter),
1648 Err(err) => {
1649 log::warn!("Failed to initialize G2D converter: {err:?}");
1650 None
1651 }
1652 }
1653 };
1654
1655 #[cfg(target_os = "linux")]
1656 #[cfg(feature = "opengl")]
1657 let opengl = if std::env::var("EDGEFIRST_DISABLE_GL")
1658 .map(|x| x != "0" && x.to_lowercase() != "false")
1659 .unwrap_or(false)
1660 {
1661 log::debug!("EDGEFIRST_DISABLE_GL is set");
1662 None
1663 } else {
1664 match GLProcessorThreaded::new(config.egl_display) {
1665 Ok(gl_converter) => Some(gl_converter),
1666 Err(err) => {
1667 log::warn!("Failed to initialize GL converter: {err:?}");
1668 None
1669 }
1670 }
1671 };
1672
1673 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
1674 #[cfg(feature = "opengl")]
1675 let opengl = if std::env::var("EDGEFIRST_DISABLE_GL")
1676 .map(|x| x != "0" && x.to_lowercase() != "false")
1677 .unwrap_or(false)
1678 {
1679 log::debug!("EDGEFIRST_DISABLE_GL is set");
1680 None
1681 } else {
1682 match GLProcessorThreaded::new(config.egl_display) {
1683 Ok(gl_converter) => Some(gl_converter),
1684 Err(err) => {
1685 log::debug!(
1686 "GL backend unavailable: {err:?} \
1687 (CPU fallback will be used)"
1688 );
1689 None
1690 }
1691 }
1692 };
1693
1694 let cpu = if std::env::var("EDGEFIRST_DISABLE_CPU")
1695 .map(|x| x != "0" && x.to_lowercase() != "false")
1696 .unwrap_or(false)
1697 {
1698 log::debug!("EDGEFIRST_DISABLE_CPU is set");
1699 None
1700 } else {
1701 Some(CPUProcessor::new())
1702 };
1703 Ok(Self {
1704 cpu,
1705 #[cfg(target_os = "linux")]
1706 g2d,
1707 #[cfg(target_os = "linux")]
1708 #[cfg(feature = "opengl")]
1709 opengl,
1710 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
1711 #[cfg(feature = "opengl")]
1712 opengl,
1713 convert_fallbacks: std::sync::atomic::AtomicU64::new(0),
1714 forced_backend: None,
1715 }
1716 .apply_colorimetry_mode(config.colorimetry))
1717 }
1718
1719 fn apply_colorimetry_mode(self, _mode: ColorimetryMode) -> Self {
1723 #[cfg(all(
1724 any(
1725 target_os = "linux",
1726 target_os = "macos",
1727 target_os = "ios",
1728 target_os = "android"
1729 ),
1730 feature = "opengl"
1731 ))]
1732 {
1733 let mut me = self;
1734 if let Err(e) = me.set_colorimetry_mode(_mode) {
1735 log::warn!("Failed to apply ColorimetryMode::{_mode:?}: {e:?}");
1736 }
1737 me
1738 }
1739 #[cfg(not(all(
1740 any(
1741 target_os = "linux",
1742 target_os = "macos",
1743 target_os = "ios",
1744 target_os = "android"
1745 ),
1746 feature = "opengl"
1747 )))]
1748 {
1749 let _ = _mode;
1750 self
1751 }
1752 }
1753
1754 #[cfg(all(
1759 any(
1760 target_os = "linux",
1761 target_os = "macos",
1762 target_os = "ios",
1763 target_os = "android"
1764 ),
1765 feature = "opengl"
1766 ))]
1767 pub fn set_colorimetry_mode(&mut self, mode: ColorimetryMode) -> Result<()> {
1768 if let Some(ref mut gl) = self.opengl {
1769 gl.set_colorimetry_mode(mode)?;
1770 }
1771 Ok(())
1772 }
1773
1774 #[cfg(all(
1777 any(
1778 target_os = "linux",
1779 target_os = "macos",
1780 target_os = "ios",
1781 target_os = "android"
1782 ),
1783 feature = "opengl"
1784 ))]
1785 pub fn set_int8_interpolation_mode(&mut self, mode: Int8InterpolationMode) -> Result<()> {
1786 if let Some(ref mut gl) = self.opengl {
1787 gl.set_int8_interpolation_mode(mode)?;
1788 }
1789 Ok(())
1790 }
1791
1792 pub fn create_image_desc(&self, desc: &edgefirst_tensor::ImageDesc) -> Result<TensorDyn> {
1803 if desc.compression().is_none() {
1804 return self.create_image(
1805 desc.width(),
1806 desc.height(),
1807 desc.format(),
1808 desc.dtype(),
1809 desc.memory(),
1810 desc.access(),
1811 );
1812 }
1813 Ok(TensorDyn::image_desc(desc)?)
1814 }
1815
1816 pub fn create_image(
1907 &self,
1908 width: usize,
1909 height: usize,
1910 format: PixelFormat,
1911 dtype: DType,
1912 memory: Option<TensorMemory>,
1913 access: edgefirst_tensor::CpuAccess,
1914 ) -> Result<TensorDyn> {
1915 #[cfg(target_os = "linux")]
1926 let dma_stride_bytes: Option<usize> = primary_plane_bpp(format, dtype.size())
1927 .and_then(|bpp| width.checked_mul(bpp))
1928 .and_then(align_pitch_bytes_to_gpu_alignment);
1929
1930 #[cfg(target_os = "linux")]
1934 let try_dma = || -> Result<TensorDyn> {
1935 let packed = format.layout() == edgefirst_tensor::PixelLayout::Packed;
1943 match dma_stride_bytes {
1944 Some(stride)
1945 if packed
1946 && primary_plane_bpp(format, dtype.size())
1947 .and_then(|bpp| width.checked_mul(bpp))
1948 .is_some_and(|natural| stride > natural) =>
1949 {
1950 log::debug!(
1951 "create_image: padding row stride for {format:?} {width}x{height} \
1952 from natural pitch to {stride} bytes for GPU alignment"
1953 );
1954 Ok(TensorDyn::image_with_stride(
1955 width,
1956 height,
1957 format,
1958 dtype,
1959 stride,
1960 Some(edgefirst_tensor::TensorMemory::Dma),
1961 access,
1962 )?)
1963 }
1964 _ => Ok(TensorDyn::image(
1965 width,
1966 height,
1967 format,
1968 dtype,
1969 Some(edgefirst_tensor::TensorMemory::Dma),
1970 access,
1971 )?),
1972 }
1973 };
1974
1975 match memory {
1982 #[cfg(target_os = "linux")]
1983 Some(TensorMemory::Dma) => {
1984 if dtype == DType::F32 {
1986 return Err(Error::NotSupported(
1987 "F32 has no 32-bit-float DRM format for DMA-BUF; \
1988 use TensorMemory::Pbo for F32"
1989 .to_string(),
1990 ));
1991 }
1992 return try_dma();
1993 }
1994 Some(mem) => {
1995 return Ok(TensorDyn::image(
1996 width,
1997 height,
1998 format,
1999 dtype,
2000 Some(mem),
2001 access,
2002 )?);
2003 }
2004 None => {}
2005 }
2006
2007 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
2014 #[cfg(feature = "opengl")]
2015 if let Some(gl) = self.opengl.as_ref() {
2016 let _ = gl; match TensorDyn::image(
2018 width,
2019 height,
2020 format,
2021 dtype,
2022 Some(edgefirst_tensor::TensorMemory::Dma),
2023 access,
2024 ) {
2025 Ok(img) => return Ok(img),
2026 Err(e) => {
2027 log::debug!(
2031 "create_image: zero-copy Dma allocation declined \
2032 ({format:?}/{dtype:?} {width}x{height}): {e:?}; using fallback storage"
2033 );
2034 }
2035 }
2036 }
2037
2038 #[cfg(target_os = "linux")]
2041 {
2042 #[cfg(feature = "opengl")]
2043 let gl_uses_pbo = self
2044 .opengl
2045 .as_ref()
2046 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
2047 #[cfg(not(feature = "opengl"))]
2048 let gl_uses_pbo = false;
2049
2050 if !gl_uses_pbo {
2051 if let Ok(img) = try_dma() {
2052 return Ok(img);
2053 }
2054 }
2055 }
2056
2057 #[cfg(target_os = "linux")]
2061 #[cfg(feature = "opengl")]
2062 if dtype.size() == 1 {
2063 if let Some(gl) = &self.opengl {
2064 match gl.create_pbo_image(width, height, format) {
2065 Ok(t) => {
2066 if dtype == DType::I8 {
2067 debug_assert!(
2075 t.chroma().is_none(),
2076 "PBO i8 transmute requires chroma == None"
2077 );
2078 let t_i8: Tensor<i8> = unsafe { std::mem::transmute(t) };
2079 return Ok(TensorDyn::from(t_i8));
2080 }
2081 return Ok(TensorDyn::from(t));
2082 }
2083 Err(e) => log::debug!("PBO image creation failed, falling back to Mem: {e:?}"),
2084 }
2085 }
2086 }
2087
2088 #[cfg(target_os = "linux")]
2091 #[cfg(feature = "opengl")]
2092 if float_pbo_eligible(dtype, self.supported_render_dtypes()) {
2093 if let Some(gl) = &self.opengl {
2094 match gl.create_pbo_image_dtype(width, height, format, dtype) {
2095 Ok(t) => return Ok(t),
2096 Err(e) => {
2097 log::debug!(
2098 "Float PBO image creation failed for {dtype:?}, \
2099 falling back to Mem: {e:?}"
2100 );
2101 }
2102 }
2103 }
2104 }
2105
2106 Ok(TensorDyn::image(
2108 width,
2109 height,
2110 format,
2111 dtype,
2112 Some(edgefirst_tensor::TensorMemory::Mem),
2113 access,
2114 )?)
2115 }
2116
2117 #[allow(clippy::too_many_arguments)]
2171 #[cfg(target_os = "linux")]
2172 pub fn import_image(
2173 &self,
2174 image: edgefirst_tensor::PlaneDescriptor,
2175 chroma: Option<edgefirst_tensor::PlaneDescriptor>,
2176 width: usize,
2177 height: usize,
2178 format: PixelFormat,
2179 dtype: DType,
2180 colorimetry: Option<edgefirst_tensor::Colorimetry>,
2181 ) -> Result<TensorDyn> {
2182 use edgefirst_tensor::{Tensor, TensorMemory};
2183
2184 let image_stride = image.stride();
2186 let image_offset = image.offset();
2187 let chroma_stride = chroma.as_ref().and_then(|c| c.stride());
2188 let chroma_offset = chroma.as_ref().and_then(|c| c.offset());
2189
2190 if let Some(chroma_pd) = chroma {
2191 if dtype != DType::U8 && dtype != DType::I8 {
2196 return Err(Error::NotSupported(format!(
2197 "multiplane import only supports U8/I8, got {dtype:?}"
2198 )));
2199 }
2200 if format.layout() != PixelLayout::SemiPlanar {
2201 return Err(Error::NotSupported(format!(
2202 "import_image with chroma requires a semi-planar format, got {format:?}"
2203 )));
2204 }
2205
2206 let chroma_h = match format {
2207 PixelFormat::Nv12 => {
2208 height.div_ceil(2)
2210 }
2211 PixelFormat::Nv16 => {
2214 return Err(Error::NotSupported(
2215 "multiplane NV16 is not yet supported; use contiguous NV16 instead".into(),
2216 ))
2217 }
2218 _ => {
2219 return Err(Error::NotSupported(format!(
2220 "unsupported semi-planar format: {format:?}"
2221 )))
2222 }
2223 };
2224
2225 let luma = Tensor::<u8>::from_fd(image.into_fd(), &[height, width], Some("luma"))?;
2226 if luma.memory() != TensorMemory::Dma {
2227 return Err(Error::NotSupported(format!(
2228 "luma fd must be DMA-backed, got {:?}",
2229 luma.memory()
2230 )));
2231 }
2232
2233 let chroma_tensor =
2234 Tensor::<u8>::from_fd(chroma_pd.into_fd(), &[chroma_h, width], Some("chroma"))?;
2235 if chroma_tensor.memory() != TensorMemory::Dma {
2236 return Err(Error::NotSupported(format!(
2237 "chroma fd must be DMA-backed, got {:?}",
2238 chroma_tensor.memory()
2239 )));
2240 }
2241
2242 let mut tensor = Tensor::<u8>::from_planes(luma, chroma_tensor, format)?;
2245
2246 if let Some(s) = image_stride {
2248 tensor.set_row_stride(s)?;
2249 }
2250 if let Some(o) = image_offset {
2251 tensor.set_plane_offset(o);
2252 }
2253
2254 if let Some(chroma_ref) = tensor.chroma_mut() {
2259 if let Some(s) = chroma_stride {
2260 if s < width {
2261 return Err(Error::InvalidShape(format!(
2262 "chroma stride {s} < minimum {width} for {format:?}"
2263 )));
2264 }
2265 chroma_ref.set_row_stride_unchecked(s);
2266 }
2267 if let Some(o) = chroma_offset {
2268 chroma_ref.set_plane_offset(o);
2269 }
2270 }
2271
2272 if dtype == DType::I8 {
2273 const {
2277 assert!(std::mem::size_of::<Tensor<u8>>() == std::mem::size_of::<Tensor<i8>>());
2278 assert!(
2279 std::mem::align_of::<Tensor<u8>>() == std::mem::align_of::<Tensor<i8>>()
2280 );
2281 }
2282 let tensor_i8: Tensor<i8> = unsafe { std::mem::transmute(tensor) };
2283 let mut dyn_tensor = TensorDyn::from(tensor_i8);
2284 dyn_tensor.set_colorimetry(colorimetry);
2285 return Ok(dyn_tensor);
2286 }
2287 let mut dyn_tensor = TensorDyn::from(tensor);
2288 dyn_tensor.set_colorimetry(colorimetry);
2289 Ok(dyn_tensor)
2290 } else {
2291 let shape = format.image_shape(width, height).ok_or_else(|| {
2296 Error::NotSupported(format!(
2297 "unsupported pixel format for import_image: {format:?}"
2298 ))
2299 })?;
2300 let tensor = TensorDyn::from_fd(image.into_fd(), &shape, dtype, None)?;
2301 if tensor.memory() != TensorMemory::Dma {
2302 return Err(Error::NotSupported(format!(
2303 "import_image requires DMA-backed fd, got {:?}",
2304 tensor.memory()
2305 )));
2306 }
2307 let mut tensor = tensor.with_format(format)?;
2308 if let Some(s) = image_stride {
2309 tensor.set_row_stride(s)?;
2310 }
2311 if let Some(o) = image_offset {
2312 tensor.set_plane_offset(o);
2313 }
2314 tensor.set_colorimetry(colorimetry);
2315 Ok(tensor)
2316 }
2317 }
2318
2319 pub fn draw_masks(
2327 &mut self,
2328 decoder: &edgefirst_decoder::Decoder,
2329 outputs: &[&TensorDyn],
2330 dst: &mut TensorDyn,
2331 overlay: MaskOverlay<'_>,
2332 ) -> Result<Vec<DetectBox>> {
2333 let mut output_boxes = Vec::with_capacity(100);
2334
2335 let proto_result = decoder
2337 .decode_proto(outputs, &mut output_boxes)
2338 .map_err(|e| Error::Internal(format!("decode_proto: {e:#?}")))?;
2339
2340 if let Some(proto_data) = proto_result {
2341 self.draw_proto_masks(dst, &output_boxes, &proto_data, overlay)?;
2342 } else {
2343 let mut output_masks = Vec::with_capacity(100);
2345 decoder
2346 .decode(outputs, &mut output_boxes, &mut output_masks)
2347 .map_err(|e| Error::Internal(format!("decode: {e:#?}")))?;
2348 self.draw_decoded_masks(dst, &output_boxes, &output_masks, overlay)?;
2349 }
2350 Ok(output_boxes)
2351 }
2352
2353 #[cfg(feature = "tracker")]
2361 pub fn draw_masks_tracked<TR: edgefirst_tracker::Tracker<DetectBox>>(
2362 &mut self,
2363 decoder: &edgefirst_decoder::Decoder,
2364 tracker: &mut TR,
2365 timestamp: u64,
2366 outputs: &[&TensorDyn],
2367 dst: &mut TensorDyn,
2368 overlay: MaskOverlay<'_>,
2369 ) -> Result<(Vec<DetectBox>, Vec<edgefirst_tracker::TrackInfo>)> {
2370 let mut output_boxes = Vec::with_capacity(100);
2371 let mut output_tracks = Vec::new();
2372
2373 let proto_result = decoder
2374 .decode_proto_tracked(
2375 tracker,
2376 timestamp,
2377 outputs,
2378 &mut output_boxes,
2379 &mut output_tracks,
2380 )
2381 .map_err(|e| Error::Internal(format!("decode_proto_tracked: {e:#?}")))?;
2382
2383 if let Some(proto_data) = proto_result {
2384 self.draw_proto_masks(dst, &output_boxes, &proto_data, overlay)?;
2385 } else {
2386 let mut output_masks = Vec::with_capacity(100);
2390 decoder
2391 .decode_tracked(
2392 tracker,
2393 timestamp,
2394 outputs,
2395 &mut output_boxes,
2396 &mut output_masks,
2397 &mut output_tracks,
2398 )
2399 .map_err(|e| Error::Internal(format!("decode_tracked: {e:#?}")))?;
2400 self.draw_decoded_masks(dst, &output_boxes, &output_masks, overlay)?;
2401 }
2402 Ok((output_boxes, output_tracks))
2403 }
2404
2405 pub fn materialize_masks(
2429 &mut self,
2430 detect: &[DetectBox],
2431 proto_data: &ProtoData,
2432 letterbox: Option<[f32; 4]>,
2433 resolution: MaskResolution,
2434 ) -> Result<Vec<Segmentation>> {
2435 let cpu = self.cpu.as_mut().ok_or(Error::NoConverter)?;
2436 match resolution {
2437 MaskResolution::Proto => cpu.materialize_segmentations(detect, proto_data, letterbox),
2438 MaskResolution::Scaled { width, height } => {
2439 cpu.materialize_scaled_segmentations(detect, proto_data, letterbox, width, height)
2440 }
2441 }
2442 }
2443}
2444
2445impl ImageProcessorTrait for ImageProcessor {
2446 fn convert(
2452 &mut self,
2453 src: &TensorDyn,
2454 dst: &mut TensorDyn,
2455 rotation: Rotation,
2456 flip: Flip,
2457 crop: Crop,
2458 ) -> Result<()> {
2459 let start = Instant::now();
2460 let src_fmt = src.format();
2461 let dst_fmt = dst.format();
2462 let _span = tracing::trace_span!(
2463 "image.convert",
2464 ?src_fmt,
2465 ?dst_fmt,
2466 src_memory = ?src.memory(),
2467 dst_memory = ?dst.memory(),
2468 ?rotation,
2469 ?flip,
2470 )
2471 .entered();
2472 log::trace!(
2473 "convert: {src_fmt:?}({:?}/{:?}) → {dst_fmt:?}({:?}/{:?}), \
2474 rotation={rotation:?}, flip={flip:?}, backend={:?}",
2475 src.dtype(),
2476 src.memory(),
2477 dst.dtype(),
2478 dst.memory(),
2479 self.forced_backend,
2480 );
2481
2482 if let Some(forced) = self.forced_backend {
2484 return match forced {
2485 ForcedBackend::Cpu => {
2486 if let Some(cpu) = self.cpu.as_mut() {
2487 let r = cpu.convert(src, dst, rotation, flip, crop);
2488 log::trace!(
2489 "convert: forced=cpu result={} ({:?})",
2490 if r.is_ok() { "ok" } else { "err" },
2491 start.elapsed()
2492 );
2493 return r;
2494 }
2495 Err(Error::ForcedBackendUnavailable("cpu".into()))
2496 }
2497 ForcedBackend::G2d => {
2498 #[cfg(target_os = "linux")]
2499 if let Some(g2d) = self.g2d.as_mut() {
2500 let r = g2d.convert(src, dst, rotation, flip, crop);
2501 log::trace!(
2502 "convert: forced=g2d result={} ({:?})",
2503 if r.is_ok() { "ok" } else { "err" },
2504 start.elapsed()
2505 );
2506 return r;
2507 }
2508 Err(Error::ForcedBackendUnavailable("g2d".into()))
2509 }
2510 ForcedBackend::OpenGl => {
2511 #[cfg(any(
2512 target_os = "linux",
2513 target_os = "macos",
2514 target_os = "ios",
2515 target_os = "android"
2516 ))]
2517 #[cfg(feature = "opengl")]
2518 if let Some(opengl) = self.opengl.as_mut() {
2519 let r = opengl.convert(src, dst, rotation, flip, crop);
2520 log::trace!(
2521 "convert: forced=opengl result={} ({:?})",
2522 if r.is_ok() { "ok" } else { "err" },
2523 start.elapsed()
2524 );
2525 return r;
2526 }
2527 Err(Error::ForcedBackendUnavailable("opengl".into()))
2528 }
2529 };
2530 }
2531
2532 #[cfg(any(
2534 target_os = "linux",
2535 target_os = "macos",
2536 target_os = "ios",
2537 target_os = "android"
2538 ))]
2539 #[cfg(feature = "opengl")]
2540 if let Some(opengl) = self.opengl.as_mut() {
2541 match opengl.convert(src, dst, rotation, flip, crop) {
2542 Ok(_) => {
2543 log::trace!(
2544 "convert: auto selected=opengl for {src_fmt:?}→{dst_fmt:?} ({:?})",
2545 start.elapsed()
2546 );
2547 return Ok(());
2548 }
2549 Err(e) => {
2550 self.convert_fallbacks
2551 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2552 log::debug!(
2553 "convert: auto opengl declined {src_fmt:?}@{:?}→{dst_fmt:?}@{:?}, \
2554 falling back toward G2D/CPU: {e}",
2555 src.memory(),
2556 dst.memory(),
2557 );
2558 }
2559 }
2560 }
2561
2562 #[cfg(target_os = "linux")]
2563 if let Some(g2d) = self.g2d.as_mut() {
2564 let src_is_yuv = src.format().is_some_and(|f| f.is_yuv());
2571 let dst_is_yuv = dst.format().is_some_and(|f| f.is_yuv());
2572 let g2d_eligible = if src_is_yuv || dst_is_yuv {
2573 let cm = if src_is_yuv {
2574 crate::colorimetry::effective_colorimetry(src)
2575 } else {
2576 crate::colorimetry::effective_colorimetry(dst)
2577 };
2578 crate::g2d::g2d_can_handle(&cm, true)
2579 } else {
2580 true
2581 };
2582 if !g2d_eligible {
2583 log::trace!(
2584 "convert: auto g2d skipped {src_fmt:?}→{dst_fmt:?} \
2585 (colorimetry not expressible: full-range/BT.2020)"
2586 );
2587 } else {
2588 match g2d.convert(src, dst, rotation, flip, crop) {
2589 Ok(_) => {
2590 log::trace!(
2591 "convert: auto selected=g2d for {src_fmt:?}→{dst_fmt:?} ({:?})",
2592 start.elapsed()
2593 );
2594 return Ok(());
2595 }
2596 Err(e) => {
2597 log::trace!("convert: auto g2d declined {src_fmt:?}→{dst_fmt:?}: {e}");
2598 }
2599 }
2600 }
2601 }
2602
2603 if let Some(cpu) = self.cpu.as_mut() {
2604 match cpu.convert(src, dst, rotation, flip, crop) {
2605 Ok(_) => {
2606 log::trace!(
2607 "convert: auto selected=cpu for {src_fmt:?}→{dst_fmt:?} ({:?})",
2608 start.elapsed()
2609 );
2610 return Ok(());
2611 }
2612 Err(e) => {
2613 log::trace!("convert: auto cpu failed {src_fmt:?}→{dst_fmt:?}: {e}");
2614 return Err(e);
2615 }
2616 }
2617 }
2618 Err(Error::NoConverter)
2619 }
2620
2621 fn convert_deferred(
2622 &mut self,
2623 src: &TensorDyn,
2624 dst: &mut TensorDyn,
2625 rotation: Rotation,
2626 flip: Flip,
2627 crop: Crop,
2628 ) -> Result<()> {
2629 #[cfg(any(
2635 target_os = "linux",
2636 target_os = "macos",
2637 target_os = "ios",
2638 target_os = "android"
2639 ))]
2640 #[cfg(feature = "opengl")]
2641 {
2642 let gl_forced = matches!(self.forced_backend, Some(ForcedBackend::OpenGl));
2643 if gl_forced || self.forced_backend.is_none() {
2644 if let Some(opengl) = self.opengl.as_mut() {
2645 match opengl.convert_deferred(src, dst, rotation, flip, crop) {
2646 Ok(()) => return Ok(()),
2647 Err(e) => {
2648 log::trace!("convert_deferred: gl declined: {e}; eager fallback");
2649 if gl_forced {
2652 return Err(e);
2653 }
2654 }
2655 }
2656 }
2657 }
2658 }
2659 self.convert(src, dst, rotation, flip, crop)
2660 }
2661
2662 fn flush(&mut self) -> Result<()> {
2663 let _span = tracing::trace_span!("image.flush").entered();
2664 #[cfg(any(
2667 target_os = "linux",
2668 target_os = "macos",
2669 target_os = "ios",
2670 target_os = "android"
2671 ))]
2672 #[cfg(feature = "opengl")]
2673 if let Some(opengl) = self.opengl.as_mut() {
2674 return opengl.flush();
2675 }
2676 Ok(())
2677 }
2678
2679 fn draw_decoded_masks(
2680 &mut self,
2681 dst: &mut TensorDyn,
2682 detect: &[DetectBox],
2683 segmentation: &[Segmentation],
2684 overlay: MaskOverlay<'_>,
2685 ) -> Result<()> {
2686 let _span = tracing::trace_span!(
2687 "image.draw_decoded_masks",
2688 n_detections = detect.len(),
2689 n_segmentations = segmentation.len(),
2690 )
2691 .entered();
2692 let start = Instant::now();
2693
2694 if let Some(bg) = overlay.background {
2695 if bg.aliases(dst) {
2696 return Err(Error::AliasedBuffers(
2697 "background must not reference the same buffer as dst".to_string(),
2698 ));
2699 }
2700 }
2701
2702 let lb_boxes: Vec<DetectBox>;
2705 let lb_segs: Vec<Segmentation>;
2706 let (detect, segmentation) = if let Some(lb) = overlay.letterbox {
2707 lb_boxes = detect.iter().map(|&d| unletter_bbox(d, lb)).collect();
2708 lb_segs = if segmentation.len() == lb_boxes.len() {
2711 segmentation
2712 .iter()
2713 .zip(lb_boxes.iter())
2714 .map(|(s, d)| Segmentation {
2715 xmin: d.bbox.xmin,
2716 ymin: d.bbox.ymin,
2717 xmax: d.bbox.xmax,
2718 ymax: d.bbox.ymax,
2719 segmentation: s.segmentation.clone(),
2720 })
2721 .collect()
2722 } else {
2723 segmentation.to_vec()
2724 };
2725 (lb_boxes.as_slice(), lb_segs.as_slice())
2726 } else {
2727 (detect, segmentation)
2728 };
2729 #[cfg(target_os = "linux")]
2730 let is_empty_frame = detect.is_empty() && segmentation.is_empty();
2731
2732 if let Some(forced) = self.forced_backend {
2734 return match forced {
2735 ForcedBackend::Cpu => {
2736 if let Some(cpu) = self.cpu.as_mut() {
2737 return cpu.draw_decoded_masks(dst, detect, segmentation, overlay);
2738 }
2739 Err(Error::ForcedBackendUnavailable("cpu".into()))
2740 }
2741 ForcedBackend::G2d => {
2742 #[cfg(target_os = "linux")]
2745 if let Some(g2d) = self.g2d.as_mut() {
2746 return g2d.draw_decoded_masks(dst, detect, segmentation, overlay);
2747 }
2748 Err(Error::ForcedBackendUnavailable("g2d".into()))
2749 }
2750 ForcedBackend::OpenGl => {
2751 #[cfg(target_os = "linux")]
2754 #[cfg(feature = "opengl")]
2755 if let Some(opengl) = self.opengl.as_mut() {
2756 return opengl.draw_decoded_masks(dst, detect, segmentation, overlay);
2757 }
2758 Err(Error::ForcedBackendUnavailable("opengl".into()))
2759 }
2760 };
2761 }
2762
2763 #[cfg(target_os = "linux")]
2769 if is_empty_frame {
2770 if let Some(g2d) = self.g2d.as_mut() {
2771 match g2d.draw_decoded_masks(dst, detect, segmentation, overlay) {
2772 Ok(_) => {
2773 log::trace!(
2774 "draw_decoded_masks empty frame via g2d in {:?}",
2775 start.elapsed()
2776 );
2777 return Ok(());
2778 }
2779 Err(e) => log::trace!("g2d empty-frame path unavailable: {e:?}"),
2780 }
2781 }
2782 }
2783
2784 #[cfg(target_os = "linux")]
2788 #[cfg(feature = "opengl")]
2789 if let Some(opengl) = self.opengl.as_mut() {
2790 log::trace!(
2791 "draw_decoded_masks started with opengl in {:?}",
2792 start.elapsed()
2793 );
2794 match opengl.draw_decoded_masks(dst, detect, segmentation, overlay) {
2795 Ok(_) => {
2796 log::trace!("draw_decoded_masks with opengl in {:?}", start.elapsed());
2797 return Ok(());
2798 }
2799 Err(e) => {
2800 log::trace!("draw_decoded_masks didn't work with opengl: {e:?}")
2801 }
2802 }
2803 }
2804
2805 log::trace!(
2806 "draw_decoded_masks started with cpu in {:?}",
2807 start.elapsed()
2808 );
2809 if let Some(cpu) = self.cpu.as_mut() {
2810 match cpu.draw_decoded_masks(dst, detect, segmentation, overlay) {
2811 Ok(_) => {
2812 log::trace!("draw_decoded_masks with cpu in {:?}", start.elapsed());
2813 return Ok(());
2814 }
2815 Err(e) => {
2816 log::trace!("draw_decoded_masks didn't work with cpu: {e:?}");
2817 return Err(e);
2818 }
2819 }
2820 }
2821 Err(Error::NoConverter)
2822 }
2823
2824 fn draw_proto_masks(
2825 &mut self,
2826 dst: &mut TensorDyn,
2827 detect: &[DetectBox],
2828 proto_data: &ProtoData,
2829 overlay: MaskOverlay<'_>,
2830 ) -> Result<()> {
2831 let start = Instant::now();
2832
2833 if let Some(bg) = overlay.background {
2834 if bg.aliases(dst) {
2835 return Err(Error::AliasedBuffers(
2836 "background must not reference the same buffer as dst".to_string(),
2837 ));
2838 }
2839 }
2840
2841 let lb_boxes: Vec<DetectBox>;
2847 let render_detect = if let Some(lb) = overlay.letterbox {
2848 lb_boxes = detect.iter().map(|&d| unletter_bbox(d, lb)).collect();
2849 lb_boxes.as_slice()
2850 } else {
2851 detect
2852 };
2853 #[cfg(target_os = "linux")]
2854 let is_empty_frame = detect.is_empty();
2855
2856 if let Some(forced) = self.forced_backend {
2858 return match forced {
2859 ForcedBackend::Cpu => {
2860 if let Some(cpu) = self.cpu.as_mut() {
2861 return cpu.draw_proto_masks(dst, render_detect, proto_data, overlay);
2862 }
2863 Err(Error::ForcedBackendUnavailable("cpu".into()))
2864 }
2865 ForcedBackend::G2d => {
2866 #[cfg(target_os = "linux")]
2867 if let Some(g2d) = self.g2d.as_mut() {
2868 return g2d.draw_proto_masks(dst, render_detect, proto_data, overlay);
2869 }
2870 Err(Error::ForcedBackendUnavailable("g2d".into()))
2871 }
2872 ForcedBackend::OpenGl => {
2873 #[cfg(target_os = "linux")]
2874 #[cfg(feature = "opengl")]
2875 if let Some(opengl) = self.opengl.as_mut() {
2876 return opengl.draw_proto_masks(dst, render_detect, proto_data, overlay);
2877 }
2878 Err(Error::ForcedBackendUnavailable("opengl".into()))
2879 }
2880 };
2881 }
2882
2883 #[cfg(target_os = "linux")]
2886 if is_empty_frame {
2887 if let Some(g2d) = self.g2d.as_mut() {
2888 match g2d.draw_proto_masks(dst, render_detect, proto_data, overlay) {
2889 Ok(_) => {
2890 log::trace!(
2891 "draw_proto_masks empty frame via g2d in {:?}",
2892 start.elapsed()
2893 );
2894 return Ok(());
2895 }
2896 Err(e) => log::trace!("g2d empty-frame path unavailable: {e:?}"),
2897 }
2898 }
2899 }
2900
2901 #[cfg(target_os = "linux")]
2910 #[cfg(feature = "opengl")]
2911 if let (Some(_), Some(_)) = (self.cpu.as_ref(), self.opengl.as_ref()) {
2912 let segmentation = match self.cpu.as_mut() {
2913 Some(cpu) => {
2914 log::trace!(
2915 "draw_proto_masks started with hybrid (cpu+opengl) in {:?}",
2916 start.elapsed()
2917 );
2918 cpu.materialize_segmentations(detect, proto_data, overlay.letterbox)?
2919 }
2920 None => unreachable!("cpu presence checked above"),
2921 };
2922 if let Some(opengl) = self.opengl.as_mut() {
2923 match opengl.draw_decoded_masks(dst, render_detect, &segmentation, overlay) {
2924 Ok(_) => {
2925 log::trace!(
2926 "draw_proto_masks with hybrid (cpu+opengl) in {:?}",
2927 start.elapsed()
2928 );
2929 return Ok(());
2930 }
2931 Err(e) => {
2932 log::trace!(
2933 "draw_proto_masks hybrid path failed, falling back to cpu: {e:?}"
2934 );
2935 }
2936 }
2937 }
2938 }
2939
2940 let Some(cpu) = self.cpu.as_mut() else {
2941 return Err(Error::Internal(
2942 "draw_proto_masks requires CPU backend for fallback path".into(),
2943 ));
2944 };
2945 log::trace!("draw_proto_masks started with cpu in {:?}", start.elapsed());
2946 cpu.draw_proto_masks(dst, render_detect, proto_data, overlay)
2947 }
2948
2949 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()> {
2950 let start = Instant::now();
2951
2952 if let Some(forced) = self.forced_backend {
2954 return match forced {
2955 ForcedBackend::Cpu => {
2956 if let Some(cpu) = self.cpu.as_mut() {
2957 return cpu.set_class_colors(colors);
2958 }
2959 Err(Error::ForcedBackendUnavailable("cpu".into()))
2960 }
2961 ForcedBackend::G2d => Err(Error::NotSupported(
2962 "g2d does not support set_class_colors".into(),
2963 )),
2964 ForcedBackend::OpenGl => {
2965 #[cfg(target_os = "linux")]
2966 #[cfg(feature = "opengl")]
2967 if let Some(opengl) = self.opengl.as_mut() {
2968 return opengl.set_class_colors(colors);
2969 }
2970 Err(Error::ForcedBackendUnavailable("opengl".into()))
2971 }
2972 };
2973 }
2974
2975 #[cfg(target_os = "linux")]
2978 #[cfg(feature = "opengl")]
2979 if let Some(opengl) = self.opengl.as_mut() {
2980 log::trace!("image started with opengl in {:?}", start.elapsed());
2981 match opengl.set_class_colors(colors) {
2982 Ok(_) => {
2983 log::trace!("colors set with opengl in {:?}", start.elapsed());
2984 return Ok(());
2985 }
2986 Err(e) => {
2987 log::trace!("colors didn't set with opengl: {e:?}")
2988 }
2989 }
2990 }
2991 log::trace!("image started with cpu in {:?}", start.elapsed());
2992 if let Some(cpu) = self.cpu.as_mut() {
2993 match cpu.set_class_colors(colors) {
2994 Ok(_) => {
2995 log::trace!("colors set with cpu in {:?}", start.elapsed());
2996 return Ok(());
2997 }
2998 Err(e) => {
2999 log::trace!("colors didn't set with cpu: {e:?}");
3000 return Err(e);
3001 }
3002 }
3003 }
3004 Err(Error::NoConverter)
3005 }
3006}
3007
3008#[cfg(test)]
3018pub(crate) fn load_image_test_helper(
3019 image: &[u8],
3020 format: Option<PixelFormat>,
3021 memory: Option<TensorMemory>,
3022) -> Result<TensorDyn> {
3023 use edgefirst_codec::{peek_info, ImageDecoder, ImageLoad};
3024
3025 let info = peek_info(image)?;
3029 let native_fmt = info.format;
3030 let w = info.width;
3031 let h = info.height;
3032
3033 let mut decoder = ImageDecoder::new();
3034
3035 #[cfg(target_os = "linux")]
3038 let native_src = {
3039 if let Some(aligned_pitch) = padded_dma_pitch_for(native_fmt, w, &memory) {
3040 let mut dma = Tensor::<u8>::image_with_stride(
3041 w,
3042 h,
3043 native_fmt,
3044 aligned_pitch,
3045 Some(TensorMemory::Dma),
3046 edgefirst_tensor::CpuAccess::ReadWrite,
3047 )?;
3048 dma.load_image(&mut decoder, image)?;
3049 TensorDyn::from(dma)
3050 } else {
3051 let mut img = Tensor::<u8>::image(
3052 w,
3053 h,
3054 native_fmt,
3055 memory,
3056 edgefirst_tensor::CpuAccess::ReadWrite,
3057 )?;
3058 img.load_image(&mut decoder, image)?;
3059 TensorDyn::from(img)
3060 }
3061 };
3062 #[cfg(not(target_os = "linux"))]
3063 let native_src = {
3064 let mut img = Tensor::<u8>::image(
3065 w,
3066 h,
3067 native_fmt,
3068 memory,
3069 edgefirst_tensor::CpuAccess::ReadWrite,
3070 )?;
3071 img.load_image(&mut decoder, image)?;
3072 TensorDyn::from(img)
3073 };
3074
3075 match format {
3079 Some(f) if f != native_fmt => {
3080 let mut dst = TensorDyn::image(
3081 w,
3082 h,
3083 f,
3084 DType::U8,
3085 memory,
3086 edgefirst_tensor::CpuAccess::ReadWrite,
3087 )?;
3088 #[allow(clippy::needless_update)]
3095 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
3096 backend: ComputeBackend::Cpu,
3097 ..Default::default()
3098 })?;
3099 proc.convert(
3100 &native_src,
3101 &mut dst,
3102 Rotation::None,
3103 Flip::None,
3104 Crop::default(),
3105 )?;
3106 Ok(dst)
3107 }
3108 _ => Ok(native_src),
3109 }
3110}
3111
3112pub fn save_jpeg(tensor: &TensorDyn, path: impl AsRef<std::path::Path>, quality: u8) -> Result<()> {
3116 let t = tensor.as_u8().ok_or(Error::UnsupportedFormat(
3117 "save_jpeg requires u8 tensor".to_string(),
3118 ))?;
3119 let fmt = t.format().ok_or(Error::NotAnImage)?;
3120 if fmt.layout() != PixelLayout::Packed {
3121 return Err(Error::NotImplemented(
3122 "Saving planar images is not supported".to_string(),
3123 ));
3124 }
3125
3126 let colour = match fmt {
3127 PixelFormat::Rgb => jpeg_encoder::ColorType::Rgb,
3128 PixelFormat::Rgba => jpeg_encoder::ColorType::Rgba,
3129 _ => {
3130 return Err(Error::NotImplemented(
3131 "Unsupported image format for saving".to_string(),
3132 ));
3133 }
3134 };
3135
3136 let w = t.width().ok_or(Error::NotAnImage)?;
3137 let h = t.height().ok_or(Error::NotAnImage)?;
3138 let encoder = jpeg_encoder::Encoder::new_file(path, quality)?;
3139 let tensor_map = t.map_read()?;
3140
3141 encoder.encode(&tensor_map, w as u16, h as u16, colour)?;
3142
3143 Ok(())
3144}
3145
3146pub(crate) struct FunctionTimer<T: Display> {
3147 name: T,
3148 start: std::time::Instant,
3149}
3150
3151impl<T: Display> FunctionTimer<T> {
3152 pub fn new(name: T) -> Self {
3153 Self {
3154 name,
3155 start: std::time::Instant::now(),
3156 }
3157 }
3158}
3159
3160impl<T: Display> Drop for FunctionTimer<T> {
3161 fn drop(&mut self) {
3162 log::trace!("{} elapsed: {:?}", self.name, self.start.elapsed())
3163 }
3164}
3165
3166const DEFAULT_COLORS: [[f32; 4]; 20] = [
3167 [0., 1., 0., 0.7],
3168 [1., 0.5568628, 0., 0.7],
3169 [0.25882353, 0.15294118, 0.13333333, 0.7],
3170 [0.8, 0.7647059, 0.78039216, 0.7],
3171 [0.3137255, 0.3137255, 0.3137255, 0.7],
3172 [0.1411765, 0.3098039, 0.1215686, 0.7],
3173 [1., 0.95686275, 0.5137255, 0.7],
3174 [0.3529412, 0.32156863, 0., 0.7],
3175 [0.4235294, 0.6235294, 0.6509804, 0.7],
3176 [0.5098039, 0.5098039, 0.7294118, 0.7],
3177 [0.00784314, 0.18823529, 0.29411765, 0.7],
3178 [0.0, 0.2706, 1.0, 0.7],
3179 [0.0, 0.0, 0.0, 0.7],
3180 [0.0, 0.5, 0.0, 0.7],
3181 [1.0, 0.0, 0.0, 0.7],
3182 [0.0, 0.0, 1.0, 0.7],
3183 [1.0, 0.5, 0.5, 0.7],
3184 [0.1333, 0.5451, 0.1333, 0.7],
3185 [0.1176, 0.4118, 0.8235, 0.7],
3186 [1., 1., 1., 0.7],
3187];
3188
3189const fn denorm<const M: usize, const N: usize>(a: [[f32; M]; N]) -> [[u8; M]; N] {
3190 let mut result = [[0; M]; N];
3191 let mut i = 0;
3192 while i < N {
3193 let mut j = 0;
3194 while j < M {
3195 result[i][j] = (a[i][j] * 255.0).round() as u8;
3196 j += 1;
3197 }
3198 i += 1;
3199 }
3200 result
3201}
3202
3203const DEFAULT_COLORS_U8: [[u8; 4]; 20] = denorm(DEFAULT_COLORS);
3204
3205#[cfg(test)]
3206#[cfg_attr(coverage_nightly, coverage(off))]
3207mod alignment_tests {
3208 use super::*;
3209
3210 #[test]
3211 fn align_width_rgba8_common_widths() {
3212 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); }
3223
3224 #[test]
3225 fn align_width_rgb888_packed() {
3226 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] {
3233 let padded = align_width_for_gpu_pitch(w, 3);
3234 assert!(padded >= w);
3235 assert_eq!((padded * 3) % 64, 0);
3236 assert_eq!((padded * 3) % 3, 0);
3237 }
3238 }
3239
3240 #[test]
3241 fn align_width_grey_u8() {
3242 assert_eq!(align_width_for_gpu_pitch(64, 1), 64);
3244 assert_eq!(align_width_for_gpu_pitch(640, 1), 640);
3245 assert_eq!(align_width_for_gpu_pitch(1, 1), 64);
3246 assert_eq!(align_width_for_gpu_pitch(65, 1), 128);
3247 }
3248
3249 #[test]
3250 fn align_width_zero_inputs() {
3251 assert_eq!(align_width_for_gpu_pitch(0, 4), 0);
3252 assert_eq!(align_width_for_gpu_pitch(640, 0), 640);
3253 }
3254
3255 #[test]
3256 fn align_width_never_returns_smaller_than_input() {
3257 for &bpp in &[1usize, 2, 3, 4, 8] {
3261 for &w in &[
3262 1usize,
3263 17,
3264 64,
3265 65,
3266 100,
3267 1280,
3268 1281,
3269 1920,
3270 3004,
3271 3072,
3272 3840,
3273 usize::MAX / 8,
3274 usize::MAX / 4,
3275 usize::MAX / 2,
3276 usize::MAX - 1,
3277 usize::MAX,
3278 ] {
3279 let aligned = align_width_for_gpu_pitch(w, bpp);
3280 assert!(
3281 aligned >= w,
3282 "align_width_for_gpu_pitch({w}, {bpp}) = {aligned} < {w}"
3283 );
3284 }
3285 }
3286 }
3287
3288 #[test]
3289 fn align_width_overflow_returns_unaligned_not_smaller() {
3290 let aligned_extreme = usize::MAX - 15; assert_eq!(
3296 align_width_for_gpu_pitch(aligned_extreme, 4),
3297 aligned_extreme
3298 );
3299 let misaligned_extreme = usize::MAX - 1;
3302 let result = align_width_for_gpu_pitch(misaligned_extreme, 4);
3303 assert!(
3304 result == misaligned_extreme || result >= misaligned_extreme,
3305 "extreme misaligned width must not be rounded down to {result}"
3306 );
3307 }
3308
3309 #[test]
3310 fn checked_lcm_basic_and_overflow() {
3311 assert_eq!(checked_num_integer_lcm(64, 4), Some(64));
3312 assert_eq!(checked_num_integer_lcm(64, 3), Some(192));
3313 assert_eq!(checked_num_integer_lcm(64, 1), Some(64));
3314 assert_eq!(checked_num_integer_lcm(0, 4), Some(0));
3315 assert_eq!(checked_num_integer_lcm(64, 0), Some(0));
3316 assert_eq!(
3318 checked_num_integer_lcm(usize::MAX, usize::MAX - 1),
3319 None,
3320 "coprime extreme values must overflow detect, not panic"
3321 );
3322 }
3323
3324 #[test]
3325 fn primary_plane_bpp_known_formats() {
3326 assert_eq!(primary_plane_bpp(PixelFormat::Rgba, 1), Some(4));
3328 assert_eq!(primary_plane_bpp(PixelFormat::Bgra, 1), Some(4));
3329 assert_eq!(primary_plane_bpp(PixelFormat::Rgb, 1), Some(3));
3330 assert_eq!(primary_plane_bpp(PixelFormat::Grey, 1), Some(1));
3331 assert_eq!(primary_plane_bpp(PixelFormat::Nv12, 1), Some(1));
3333 }
3334}
3335
3336#[cfg(test)]
3337#[cfg_attr(coverage_nightly, coverage(off))]
3338#[allow(deprecated)]
3339mod image_tests {
3340 use super::*;
3341 use crate::{CPUProcessor, Rotation};
3342 #[cfg(target_os = "linux")]
3343 use edgefirst_tensor::is_dma_available;
3344 use edgefirst_tensor::{TensorMapTrait, TensorMemory, TensorTrait};
3345 use image::buffer::ConvertBuffer;
3346
3347 fn convert_img(
3353 proc: &mut dyn ImageProcessorTrait,
3354 src: TensorDyn,
3355 dst: TensorDyn,
3356 rotation: Rotation,
3357 flip: Flip,
3358 crop: Crop,
3359 ) -> (Result<()>, TensorDyn, TensorDyn) {
3360 let src_fourcc = src.format().unwrap();
3361 let dst_fourcc = dst.format().unwrap();
3362 let src_dyn = src;
3363 let mut dst_dyn = dst;
3364 let result = proc.convert(&src_dyn, &mut dst_dyn, rotation, flip, crop);
3365 let src_back = {
3366 let mut __t = src_dyn.into_u8().unwrap();
3367 __t.set_format(src_fourcc).unwrap();
3368 TensorDyn::from(__t)
3369 };
3370 let dst_back = {
3371 let mut __t = dst_dyn.into_u8().unwrap();
3372 __t.set_format(dst_fourcc).unwrap();
3373 TensorDyn::from(__t)
3374 };
3375 (result, src_back, dst_back)
3376 }
3377
3378 #[ctor::ctor(unsafe)]
3379 fn init() {
3380 env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
3381 }
3382
3383 macro_rules! function {
3384 () => {{
3385 fn f() {}
3386 fn type_name_of<T>(_: T) -> &'static str {
3387 std::any::type_name::<T>()
3388 }
3389 let name = type_name_of(f);
3390
3391 match &name[..name.len() - 3].rfind(':') {
3393 Some(pos) => &name[pos + 1..name.len() - 3],
3394 None => &name[..name.len() - 3],
3395 }
3396 }};
3397 }
3398
3399 #[test]
3412 fn batch_view_dst_tiles_match_standalone() {
3413 let mut proc = match ImageProcessor::new() {
3414 Ok(p) => p,
3415 Err(e) => {
3416 eprintln!(
3417 "SKIPPED: {} — ImageProcessor init failed ({e:?})",
3418 function!()
3419 );
3420 return;
3421 }
3422 };
3423 let n = 3usize;
3424 let (w, h) = (32usize, 24usize);
3425 let colors: [[u8; 4]; 3] = [[210, 40, 40, 255], [40, 210, 40, 255], [40, 40, 210, 255]];
3426 let make_src = |c: [u8; 4]| -> TensorDyn {
3427 let bytes: Vec<u8> = c.iter().copied().cycle().take(w * h * 4).collect();
3428 load_bytes_to_tensor(w, h, PixelFormat::Rgba, Some(TensorMemory::Mem), &bytes).unwrap()
3429 };
3430 let parent = match TensorDyn::image(
3433 w,
3434 n * h,
3435 PixelFormat::Rgba,
3436 DType::U8,
3437 Some(TensorMemory::Dma),
3438 edgefirst_tensor::CpuAccess::ReadWrite,
3439 ) {
3440 Ok(d) => d,
3441 Err(e) => {
3442 eprintln!(
3443 "SKIPPED: {} — tall DMA destination alloc failed ({e:?})",
3444 function!()
3445 );
3446 return;
3447 }
3448 };
3449
3450 for (i, &c) in colors.iter().enumerate().take(n) {
3452 let mut tile = parent.view(Region::new(0, i * h, w, h)).unwrap();
3453 proc.convert_deferred(
3454 &make_src(c),
3455 &mut tile,
3456 Rotation::None,
3457 Flip::None,
3458 Crop::no_crop(),
3459 )
3460 .unwrap_or_else(|e| panic!("convert_deferred tile {i}: {e:?}"));
3461 }
3462 proc.flush().unwrap();
3463
3464 for (i, &c) in colors.iter().enumerate().take(n) {
3465 let mut solo = TensorDyn::image(
3467 w,
3468 h,
3469 PixelFormat::Rgba,
3470 DType::U8,
3471 Some(TensorMemory::Dma),
3472 edgefirst_tensor::CpuAccess::ReadWrite,
3473 )
3474 .unwrap();
3475 proc.convert(
3476 &make_src(c),
3477 &mut solo,
3478 Rotation::None,
3479 Flip::None,
3480 Crop::no_crop(),
3481 )
3482 .unwrap();
3483
3484 let band = parent.view(Region::new(0, i * h, w, h)).unwrap();
3485 let band_bytes = band.as_u8().unwrap().map().unwrap().as_slice().to_vec();
3486 let solo_bytes = solo.as_u8().unwrap().map().unwrap().as_slice().to_vec();
3487 assert_eq!(
3488 band_bytes, solo_bytes,
3489 "tile {i}: band differs from standalone convert (placement or sibling wipe)"
3490 );
3491 assert!(
3492 band_bytes.chunks_exact(4).all(|p| p == c),
3493 "tile {i}: band is not the expected solid color {c:?} (sibling wipe?)"
3494 );
3495 }
3496 }
3497
3498 #[cfg(test)]
3502 fn gradient_frame(w: usize, h: usize) -> TensorDyn {
3503 let mut bytes = vec![0u8; w * h * 4];
3504 for y in 0..h {
3505 for x in 0..w {
3506 let i = (y * w + x) * 4;
3507 bytes[i] = x as u8;
3508 bytes[i + 1] = y as u8;
3509 bytes[i + 2] = (x ^ y) as u8;
3510 bytes[i + 3] = 255;
3511 }
3512 }
3513 load_bytes_to_tensor(w, h, PixelFormat::Rgba, Some(TensorMemory::Mem), &bytes).unwrap()
3514 }
3515
3516 #[test]
3520 fn tile_into_cpu_distinct_content_parity() {
3521 let mut proc = match ImageProcessor::with_config(ImageProcessorConfig {
3522 backend: ComputeBackend::Cpu,
3523 ..Default::default()
3524 }) {
3525 Ok(p) => p,
3526 Err(e) => {
3527 eprintln!("SKIPPED: {} — CPU init failed ({e:?})", function!());
3528 return;
3529 }
3530 };
3531 let (fw, fh) = (96usize, 64usize);
3532 let src = gradient_frame(fw, fh);
3533 let cfg = TilingConfig::new(32, 32).with_overlap(0.0); let n = tile_grid(fh, fw, 32, 32, 0.0).len();
3535 assert_eq!(n, 6);
3536
3537 let mut parent = proc
3538 .alloc_tile_batch(
3539 n,
3540 &cfg,
3541 PixelFormat::Rgba,
3542 DType::U8,
3543 Some(TensorMemory::Mem),
3544 edgefirst_tensor::CpuAccess::ReadWrite,
3545 )
3546 .unwrap();
3547 let placements = proc.tile_into(&src, &mut parent, &cfg).unwrap();
3548 assert_eq!(placements.len(), n);
3549
3550 for p in &placements {
3551 let source = Region::new(
3552 p.origin.0 as usize,
3553 p.origin.1 as usize,
3554 p.crop_size.0 as usize,
3555 p.crop_size.1 as usize,
3556 );
3557 let mut solo = TensorDyn::image(
3558 32,
3559 32,
3560 PixelFormat::Rgba,
3561 DType::U8,
3562 Some(TensorMemory::Mem),
3563 edgefirst_tensor::CpuAccess::ReadWrite,
3564 )
3565 .unwrap();
3566 proc.convert(
3567 &src,
3568 &mut solo,
3569 Rotation::None,
3570 Flip::None,
3571 Crop::default()
3572 .with_source(Some(source))
3573 .with_fit(Fit::Stretch),
3574 )
3575 .unwrap();
3576 let band = parent.view(Region::new(0, p.index * 32, 32, 32)).unwrap();
3577 let band_bytes = band.as_u8().unwrap().map().unwrap().as_slice().to_vec();
3578 let solo_bytes = solo.as_u8().unwrap().map().unwrap().as_slice().to_vec();
3579 assert_eq!(
3580 band_bytes, solo_bytes,
3581 "tile {} band differs from standalone crop-convert",
3582 p.index
3583 );
3584 }
3585 }
3586
3587 #[test]
3590 fn tile_one_matches_tile_into_band() {
3591 let mut proc = match ImageProcessor::with_config(ImageProcessorConfig {
3592 backend: ComputeBackend::Cpu,
3593 ..Default::default()
3594 }) {
3595 Ok(p) => p,
3596 Err(e) => {
3597 eprintln!("SKIPPED: {} — CPU init failed ({e:?})", function!());
3598 return;
3599 }
3600 };
3601 let (fw, fh) = (96usize, 64usize);
3602 let src = gradient_frame(fw, fh);
3603 let cfg = TilingConfig::new(32, 32).with_overlap(0.0);
3604 let n = tile_grid(fh, fw, 32, 32, 0.0).len();
3605
3606 let mut parent = proc
3607 .alloc_tile_batch(
3608 n,
3609 &cfg,
3610 PixelFormat::Rgba,
3611 DType::U8,
3612 Some(TensorMemory::Mem),
3613 edgefirst_tensor::CpuAccess::ReadWrite,
3614 )
3615 .unwrap();
3616 proc.tile_into(&src, &mut parent, &cfg).unwrap();
3617
3618 let plan = proc.plan_tiles(fw, fh, &cfg).unwrap();
3619 for p in &plan {
3620 let mut slot = TensorDyn::image(
3621 32,
3622 32,
3623 PixelFormat::Rgba,
3624 DType::U8,
3625 Some(TensorMemory::Mem),
3626 edgefirst_tensor::CpuAccess::ReadWrite,
3627 )
3628 .unwrap();
3629 proc.tile_one(&src, &mut slot, p, &cfg).unwrap();
3630 proc.flush().unwrap();
3631 let band = parent.view(Region::new(0, p.index * 32, 32, 32)).unwrap();
3632 let slot_bytes = slot.as_u8().unwrap().map().unwrap().as_slice().to_vec();
3633 let band_bytes = band.as_u8().unwrap().map().unwrap().as_slice().to_vec();
3634 assert_eq!(
3635 slot_bytes, band_bytes,
3636 "tile {} stream != batch band",
3637 p.index
3638 );
3639 }
3640 }
3641
3642 #[test]
3649 fn tile_into_auto_dma_parity() {
3650 let mut proc = match ImageProcessor::new() {
3651 Ok(p) => p,
3652 Err(e) => {
3653 eprintln!(
3654 "SKIPPED: {} — ImageProcessor init failed ({e:?})",
3655 function!()
3656 );
3657 return;
3658 }
3659 };
3660 let (fw, fh) = (96usize, 64usize);
3661 let src = gradient_frame(fw, fh);
3662 let cfg = TilingConfig::new(32, 32).with_overlap(0.0);
3663 let n = tile_grid(fh, fw, 32, 32, 0.0).len();
3664
3665 let mut parent = match proc.alloc_tile_batch(
3666 n,
3667 &cfg,
3668 PixelFormat::Rgba,
3669 DType::U8,
3670 Some(TensorMemory::Dma),
3671 edgefirst_tensor::CpuAccess::ReadWrite,
3672 ) {
3673 Ok(p) => p,
3674 Err(e) => {
3675 eprintln!(
3676 "SKIPPED: {} — tall DMA parent alloc failed ({e:?})",
3677 function!()
3678 );
3679 return;
3680 }
3681 };
3682 let placements = proc.tile_into(&src, &mut parent, &cfg).unwrap();
3683
3684 for p in &placements {
3685 let source = Region::new(
3686 p.origin.0 as usize,
3687 p.origin.1 as usize,
3688 p.crop_size.0 as usize,
3689 p.crop_size.1 as usize,
3690 );
3691 let mut solo = TensorDyn::image(
3692 32,
3693 32,
3694 PixelFormat::Rgba,
3695 DType::U8,
3696 Some(TensorMemory::Dma),
3697 edgefirst_tensor::CpuAccess::ReadWrite,
3698 )
3699 .unwrap();
3700 proc.convert(
3701 &src,
3702 &mut solo,
3703 Rotation::None,
3704 Flip::None,
3705 Crop::default()
3706 .with_source(Some(source))
3707 .with_fit(Fit::Stretch),
3708 )
3709 .unwrap();
3710 let band = parent.view(Region::new(0, p.index * 32, 32, 32)).unwrap();
3711 compare_images(
3717 &band,
3718 &solo,
3719 0.98,
3720 &format!("{}_tile{}", function!(), p.index),
3721 );
3722 }
3723 }
3724
3725 #[test]
3727 fn tile_into_undersized_dst_errors() {
3728 let mut proc = match ImageProcessor::with_config(ImageProcessorConfig {
3729 backend: ComputeBackend::Cpu,
3730 ..Default::default()
3731 }) {
3732 Ok(p) => p,
3733 Err(e) => {
3734 eprintln!("SKIPPED: {} — CPU init failed ({e:?})", function!());
3735 return;
3736 }
3737 };
3738 let (fw, fh) = (96usize, 64usize);
3739 let src = gradient_frame(fw, fh);
3740 let cfg = TilingConfig::new(32, 32).with_overlap(0.0); let mut small = TensorDyn::image(
3743 32,
3744 2 * 32,
3745 PixelFormat::Rgba,
3746 DType::U8,
3747 Some(TensorMemory::Mem),
3748 edgefirst_tensor::CpuAccess::ReadWrite,
3749 )
3750 .unwrap();
3751 let r = proc.tile_into(&src, &mut small, &cfg);
3752 assert!(
3753 matches!(r, Err(Error::InvalidShape(_))),
3754 "expected InvalidShape, got {r:?}"
3755 );
3756 }
3757
3758 #[test]
3760 fn tiling_alloc_rejects_invalid_config() {
3761 let proc = match ImageProcessor::with_config(ImageProcessorConfig {
3762 backend: ComputeBackend::Cpu,
3763 ..Default::default()
3764 }) {
3765 Ok(p) => p,
3766 Err(e) => {
3767 eprintln!("SKIPPED: {} — CPU init failed ({e:?})", function!());
3768 return;
3769 }
3770 };
3771 let bad = TilingConfig::new(0, 640);
3772 assert!(proc.plan_tiles(1920, 1080, &bad).is_err());
3773 assert!(proc
3774 .alloc_tile_batch(
3775 4,
3776 &bad,
3777 PixelFormat::Rgba,
3778 DType::U8,
3779 Some(TensorMemory::Mem),
3780 edgefirst_tensor::CpuAccess::ReadWrite,
3781 )
3782 .is_err());
3783 }
3784
3785 #[test]
3786 fn plan_tiles_metadata_4k() {
3787 let proc = match ImageProcessor::with_config(ImageProcessorConfig {
3788 backend: ComputeBackend::Cpu,
3789 ..Default::default()
3790 }) {
3791 Ok(p) => p,
3792 Err(e) => {
3793 eprintln!("SKIPPED: {} — CPU init failed ({e:?})", function!());
3794 return;
3795 }
3796 };
3797 let cfg = TilingConfig::new(640, 640).with_overlap(0.2);
3798 let plan = proc.plan_tiles(3840, 2160, &cfg).unwrap();
3799 assert_eq!(plan.len(), 32);
3800 assert!(plan.iter().all(|p| p.count == 32));
3801 assert!(plan.iter().all(|p| p.crop_size == (640.0, 640.0)));
3802 assert!(plan.iter().all(|p| p.letterbox.is_none())); assert!(plan.iter().all(|p| p.frame_dims == (3840.0, 2160.0)));
3804 assert_eq!(plan[0].origin, (0.0, 0.0));
3805 }
3806
3807 #[test]
3808 fn test_invalid_crop() {
3809 let src = TensorDyn::image(
3810 100,
3811 100,
3812 PixelFormat::Rgb,
3813 DType::U8,
3814 None,
3815 edgefirst_tensor::CpuAccess::ReadWrite,
3816 )
3817 .unwrap();
3818 let dst = TensorDyn::image(
3819 100,
3820 100,
3821 PixelFormat::Rgb,
3822 DType::U8,
3823 None,
3824 edgefirst_tensor::CpuAccess::ReadWrite,
3825 )
3826 .unwrap();
3827
3828 let crop = Crop::new().with_source(Some(Region::new(50, 50, 60, 60)));
3830 assert!(matches!(
3831 crop.check_crop_dyn(&src, &dst),
3832 Err(Error::CropInvalid(_))
3833 ));
3834
3835 let crop = Crop::new().with_source(Some(Region::new(0, 0, 10, 10)));
3837 assert!(crop.check_crop_dyn(&src, &dst).is_ok());
3838
3839 assert!(Crop::letterbox([0, 0, 0, 255])
3841 .check_crop_dyn(&src, &dst)
3842 .is_ok());
3843 }
3844
3845 #[test]
3846 fn test_invalid_tensor_format() -> Result<(), Error> {
3847 let mut tensor = Tensor::<u8>::new(&[720, 1280, 4, 1], None, None)?;
3849 let result = tensor.set_format(PixelFormat::Rgb);
3850 assert!(result.is_err(), "4D tensor should reject set_format");
3851
3852 let mut tensor = Tensor::<u8>::new(&[720, 1280, 4], None, None)?;
3854 let result = tensor.set_format(PixelFormat::Rgb);
3855 assert!(result.is_err(), "4-channel tensor should reject RGB format");
3856
3857 Ok(())
3858 }
3859
3860 #[test]
3861 fn test_invalid_image_file() -> Result<(), Error> {
3862 let result = crate::load_image_test_helper(&[123; 5000], None, None);
3863 assert!(
3864 matches!(result, Err(Error::Codec(_))),
3865 "unrecognised bytes should surface as Error::Codec, got {result:?}"
3866 );
3867 Ok(())
3868 }
3869
3870 #[test]
3871 fn test_invalid_jpeg_format() -> Result<(), Error> {
3872 let result = crate::load_image_test_helper(&[123; 5000], Some(PixelFormat::Yuyv), None);
3873 assert!(
3876 matches!(result, Err(Error::Codec(_))),
3877 "Yuyv target with garbage bytes should surface as Error::Codec, got {result:?}"
3878 );
3879 Ok(())
3880 }
3881
3882 #[test]
3883 fn test_load_resize_save() {
3884 let file = edgefirst_bench::testdata::read("zidane.jpg");
3885 let img = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
3886 assert_eq!(img.width(), Some(1280));
3887 assert_eq!(img.height(), Some(720));
3888
3889 let dst = TensorDyn::image(
3890 640,
3891 360,
3892 PixelFormat::Rgba,
3893 DType::U8,
3894 None,
3895 edgefirst_tensor::CpuAccess::ReadWrite,
3896 )
3897 .unwrap();
3898 let mut converter = CPUProcessor::new();
3899 let (result, _img, dst) = convert_img(
3900 &mut converter,
3901 img,
3902 dst,
3903 Rotation::None,
3904 Flip::None,
3905 Crop::no_crop(),
3906 );
3907 result.unwrap();
3908 assert_eq!(dst.width(), Some(640));
3909 assert_eq!(dst.height(), Some(360));
3910
3911 crate::save_jpeg(&dst, "zidane_resized.jpg", 80).unwrap();
3912
3913 let file = std::fs::read("zidane_resized.jpg").unwrap();
3914 let img = crate::load_image_test_helper(&file, None, None).unwrap();
3917 assert_eq!(img.width(), Some(640));
3918 assert_eq!(img.height(), Some(360));
3919 assert_eq!(img.format().unwrap(), PixelFormat::Nv12);
3920 }
3921
3922 #[test]
3923 fn test_from_tensor_planar() -> Result<(), Error> {
3924 let mut tensor = Tensor::new(&[3, 720, 1280], None, None)?;
3925 tensor
3926 .map()?
3927 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.8bps"));
3928 let planar = {
3929 tensor
3930 .set_format(PixelFormat::PlanarRgb)
3931 .map_err(|e| crate::Error::Internal(e.to_string()))?;
3932 TensorDyn::from(tensor)
3933 };
3934
3935 let rbga = load_bytes_to_tensor(
3936 1280,
3937 720,
3938 PixelFormat::Rgba,
3939 None,
3940 &edgefirst_bench::testdata::read("camera720p.rgba"),
3941 )?;
3942 compare_images_convert_to_rgb(&planar, &rbga, 0.98, function!());
3943
3944 Ok(())
3945 }
3946
3947 #[test]
3948 fn test_from_tensor_invalid_format() {
3949 assert!(PixelFormat::from_fourcc(u32::from_le_bytes(*b"TEST")).is_none());
3952 }
3953
3954 #[test]
3955 #[should_panic(expected = "Failed to save planar RGB image")]
3956 fn test_save_planar() {
3957 let planar_img = load_bytes_to_tensor(
3958 1280,
3959 720,
3960 PixelFormat::PlanarRgb,
3961 None,
3962 &edgefirst_bench::testdata::read("camera720p.8bps"),
3963 )
3964 .unwrap();
3965
3966 let save_path = "/tmp/planar_rgb.jpg";
3967 crate::save_jpeg(&planar_img, save_path, 90).expect("Failed to save planar RGB image");
3968 }
3969
3970 #[test]
3971 #[should_panic(expected = "Failed to save YUYV image")]
3972 fn test_save_yuyv() {
3973 let planar_img = load_bytes_to_tensor(
3974 1280,
3975 720,
3976 PixelFormat::Yuyv,
3977 None,
3978 &edgefirst_bench::testdata::read("camera720p.yuyv"),
3979 )
3980 .unwrap();
3981
3982 let save_path = "/tmp/yuyv.jpg";
3983 crate::save_jpeg(&planar_img, save_path, 90).expect("Failed to save YUYV image");
3984 }
3985
3986 #[test]
3987 fn test_rotation_angle() {
3988 assert_eq!(Rotation::from_degrees_clockwise(0), Rotation::None);
3989 assert_eq!(Rotation::from_degrees_clockwise(90), Rotation::Clockwise90);
3990 assert_eq!(Rotation::from_degrees_clockwise(180), Rotation::Rotate180);
3991 assert_eq!(
3992 Rotation::from_degrees_clockwise(270),
3993 Rotation::CounterClockwise90
3994 );
3995 assert_eq!(Rotation::from_degrees_clockwise(360), Rotation::None);
3996 assert_eq!(Rotation::from_degrees_clockwise(450), Rotation::Clockwise90);
3997 assert_eq!(Rotation::from_degrees_clockwise(540), Rotation::Rotate180);
3998 assert_eq!(
3999 Rotation::from_degrees_clockwise(630),
4000 Rotation::CounterClockwise90
4001 );
4002 }
4003
4004 #[test]
4005 #[should_panic(expected = "rotation angle is not a multiple of 90")]
4006 fn test_rotation_angle_panic() {
4007 Rotation::from_degrees_clockwise(361);
4008 }
4009
4010 #[test]
4011 fn test_disable_env_var() -> Result<(), Error> {
4012 let _lock = acquire_env_lock();
4015
4016 let _guard = EnvGuard::snapshot(&[
4019 "EDGEFIRST_FORCE_BACKEND",
4020 "EDGEFIRST_DISABLE_GL",
4021 "EDGEFIRST_DISABLE_G2D",
4022 "EDGEFIRST_DISABLE_CPU",
4023 ]);
4024
4025 unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") };
4028
4029 #[cfg(target_os = "linux")]
4030 {
4031 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
4032 let converter = ImageProcessor::new()?;
4033 assert!(converter.g2d.is_none());
4034 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_G2D") };
4035 }
4036
4037 #[cfg(target_os = "linux")]
4038 #[cfg(feature = "opengl")]
4039 {
4040 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
4041 let converter = ImageProcessor::new()?;
4042 assert!(converter.opengl.is_none());
4043 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_GL") };
4044 }
4045
4046 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
4047 let converter = ImageProcessor::new()?;
4048 assert!(converter.cpu.is_none());
4049 unsafe { std::env::remove_var("EDGEFIRST_DISABLE_CPU") };
4050
4051 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
4053 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
4054 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
4055 let mut converter = ImageProcessor::new()?;
4056
4057 let src = TensorDyn::image(
4058 1280,
4059 720,
4060 PixelFormat::Rgba,
4061 DType::U8,
4062 None,
4063 edgefirst_tensor::CpuAccess::ReadWrite,
4064 )?;
4065 let dst = TensorDyn::image(
4066 640,
4067 360,
4068 PixelFormat::Rgba,
4069 DType::U8,
4070 None,
4071 edgefirst_tensor::CpuAccess::ReadWrite,
4072 )?;
4073 let (result, _src, _dst) = convert_img(
4074 &mut converter,
4075 src,
4076 dst,
4077 Rotation::None,
4078 Flip::None,
4079 Crop::no_crop(),
4080 );
4081 assert!(matches!(result, Err(Error::NoConverter)));
4082 Ok(())
4084 }
4085
4086 #[test]
4087 fn test_unsupported_conversion() {
4088 let src = TensorDyn::image(
4089 1280,
4090 720,
4091 PixelFormat::Nv12,
4092 DType::U8,
4093 None,
4094 edgefirst_tensor::CpuAccess::ReadWrite,
4095 )
4096 .unwrap();
4097 let dst = TensorDyn::image(
4098 640,
4099 360,
4100 PixelFormat::Nv12,
4101 DType::U8,
4102 None,
4103 edgefirst_tensor::CpuAccess::ReadWrite,
4104 )
4105 .unwrap();
4106 let mut converter = ImageProcessor::new().unwrap();
4107 let (result, _src, _dst) = convert_img(
4108 &mut converter,
4109 src,
4110 dst,
4111 Rotation::None,
4112 Flip::None,
4113 Crop::no_crop(),
4114 );
4115 log::debug!("result: {:?}", result);
4116 assert!(matches!(
4117 result,
4118 Err(Error::NotSupported(e)) if e.starts_with("Conversion from NV12 to NV12")
4119 ));
4120 }
4121
4122 #[test]
4123 fn test_load_grey() {
4124 let grey_img = crate::load_image_test_helper(
4128 &edgefirst_bench::testdata::read("grey.jpg"),
4129 Some(PixelFormat::Rgba),
4130 None,
4131 )
4132 .unwrap();
4133 assert_eq!(grey_img.width(), Some(1024));
4134 assert_eq!(grey_img.height(), Some(681));
4135
4136 let grey_but_rgb = crate::load_image_test_helper(
4142 &edgefirst_bench::testdata::read("grey-rgb.jpg"),
4143 Some(PixelFormat::Rgba),
4144 None,
4145 )
4146 .expect("odd-height colour JPEG should decode to NV12 and convert to RGBA");
4147 assert_eq!(grey_but_rgb.width(), Some(1024));
4148 assert_eq!(grey_but_rgb.height(), Some(681));
4149 }
4150
4151 #[test]
4152 fn test_new_nv12() {
4153 let nv12 = TensorDyn::image(
4154 1280,
4155 720,
4156 PixelFormat::Nv12,
4157 DType::U8,
4158 None,
4159 edgefirst_tensor::CpuAccess::ReadWrite,
4160 )
4161 .unwrap();
4162 assert_eq!(nv12.height(), Some(720));
4163 assert_eq!(nv12.width(), Some(1280));
4164 assert_eq!(nv12.format().unwrap(), PixelFormat::Nv12);
4165 assert_eq!(nv12.format().unwrap().channels(), 1);
4167 assert!(nv12.format().is_some_and(
4168 |f| f.layout() == PixelLayout::Planar || f.layout() == PixelLayout::SemiPlanar
4169 ))
4170 }
4171
4172 #[test]
4173 #[cfg(target_os = "linux")]
4174 fn test_new_image_converter() {
4175 let dst_width = 640;
4176 let dst_height = 360;
4177 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4178 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4179
4180 let mut converter = ImageProcessor::new().unwrap();
4181 let converter_dst = converter
4182 .create_image(
4183 dst_width,
4184 dst_height,
4185 PixelFormat::Rgba,
4186 DType::U8,
4187 None,
4188 edgefirst_tensor::CpuAccess::ReadWrite,
4189 )
4190 .unwrap();
4191 let (result, src, converter_dst) = convert_img(
4192 &mut converter,
4193 src,
4194 converter_dst,
4195 Rotation::None,
4196 Flip::None,
4197 Crop::no_crop(),
4198 );
4199 result.unwrap();
4200
4201 let cpu_dst = TensorDyn::image(
4202 dst_width,
4203 dst_height,
4204 PixelFormat::Rgba,
4205 DType::U8,
4206 None,
4207 edgefirst_tensor::CpuAccess::ReadWrite,
4208 )
4209 .unwrap();
4210 let mut cpu_converter = CPUProcessor::new();
4211 let (result, _src, cpu_dst) = convert_img(
4212 &mut cpu_converter,
4213 src,
4214 cpu_dst,
4215 Rotation::None,
4216 Flip::None,
4217 Crop::no_crop(),
4218 );
4219 result.unwrap();
4220
4221 compare_images(&converter_dst, &cpu_dst, 0.98, function!());
4222 }
4223
4224 #[test]
4225 #[cfg(target_os = "linux")]
4226 fn test_create_image_dtype_i8() {
4227 let mut converter = ImageProcessor::new().unwrap();
4228
4229 let dst = converter
4231 .create_image(
4232 320,
4233 240,
4234 PixelFormat::Rgb,
4235 DType::I8,
4236 None,
4237 edgefirst_tensor::CpuAccess::ReadWrite,
4238 )
4239 .unwrap();
4240 assert_eq!(dst.dtype(), DType::I8);
4241 assert!(dst.width() == Some(320));
4242 assert!(dst.height() == Some(240));
4243 assert_eq!(dst.format(), Some(PixelFormat::Rgb));
4244
4245 let dst_u8 = converter
4247 .create_image(
4248 320,
4249 240,
4250 PixelFormat::Rgb,
4251 DType::U8,
4252 None,
4253 edgefirst_tensor::CpuAccess::ReadWrite,
4254 )
4255 .unwrap();
4256 assert_eq!(dst_u8.dtype(), DType::U8);
4257
4258 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4260 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4261 let mut dst_i8 = converter
4262 .create_image(
4263 320,
4264 240,
4265 PixelFormat::Rgb,
4266 DType::I8,
4267 None,
4268 edgefirst_tensor::CpuAccess::ReadWrite,
4269 )
4270 .unwrap();
4271 converter
4272 .convert(
4273 &src,
4274 &mut dst_i8,
4275 Rotation::None,
4276 Flip::None,
4277 Crop::no_crop(),
4278 )
4279 .unwrap();
4280 }
4281
4282 #[test]
4283 #[cfg(target_os = "linux")]
4284 fn test_create_image_nv12_dma_non_aligned_width() {
4285 let converter = ImageProcessor::new().unwrap();
4291
4292 let result = converter.create_image(
4294 100,
4295 64,
4296 PixelFormat::Nv12,
4297 DType::U8,
4298 Some(TensorMemory::Dma),
4299 edgefirst_tensor::CpuAccess::ReadWrite,
4300 );
4301
4302 match result {
4303 Ok(img) => {
4304 assert_eq!(img.width(), Some(100));
4305 assert_eq!(img.height(), Some(64));
4306 assert_eq!(img.format(), Some(PixelFormat::Nv12));
4307 if let Some(stride) = img.row_stride() {
4308 assert!(
4309 stride >= 100,
4310 "NV12 row_stride {stride} must be >= the logical width (100)",
4311 );
4312 }
4313 }
4314 Err(e) => {
4315 eprintln!("SKIPPED: create_image NV12 DMA non-aligned width: {e}");
4317 }
4318 }
4319 }
4320
4321 #[test]
4322 #[ignore] fn test_crop_skip() {
4326 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4327 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4328
4329 let mut converter = ImageProcessor::new().unwrap();
4330 let converter_dst = converter
4331 .create_image(
4332 1280,
4333 720,
4334 PixelFormat::Rgba,
4335 DType::U8,
4336 None,
4337 edgefirst_tensor::CpuAccess::ReadWrite,
4338 )
4339 .unwrap();
4340 let crop = Crop::new().with_source(Some(Region::new(0, 0, 640, 640)));
4341 let (result, src, converter_dst) = convert_img(
4342 &mut converter,
4343 src,
4344 converter_dst,
4345 Rotation::None,
4346 Flip::None,
4347 crop,
4348 );
4349 result.unwrap();
4350
4351 let cpu_dst = TensorDyn::image(
4352 1280,
4353 720,
4354 PixelFormat::Rgba,
4355 DType::U8,
4356 None,
4357 edgefirst_tensor::CpuAccess::ReadWrite,
4358 )
4359 .unwrap();
4360 let mut cpu_converter = CPUProcessor::new();
4361 let (result, _src, cpu_dst) = convert_img(
4362 &mut cpu_converter,
4363 src,
4364 cpu_dst,
4365 Rotation::None,
4366 Flip::None,
4367 crop,
4368 );
4369 result.unwrap();
4370
4371 compare_images(&converter_dst, &cpu_dst, 0.99999, function!());
4372 }
4373
4374 #[test]
4375 fn test_invalid_pixel_format() {
4376 assert!(PixelFormat::from_fourcc(u32::from_le_bytes(*b"TEST")).is_none());
4379 }
4380
4381 #[cfg(target_os = "linux")]
4383 static G2D_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4384
4385 #[cfg(target_os = "linux")]
4386 fn is_g2d_available() -> bool {
4387 *G2D_AVAILABLE.get_or_init(|| G2DProcessor::new().is_ok())
4388 }
4389
4390 #[cfg(target_os = "linux")]
4391 #[cfg(feature = "opengl")]
4392 static GL_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4393
4394 #[cfg(target_os = "linux")]
4395 #[cfg(feature = "opengl")]
4396 fn is_opengl_available() -> bool {
4398 #[cfg(all(target_os = "linux", feature = "opengl"))]
4399 {
4400 *GL_AVAILABLE.get_or_init(|| GLProcessorThreaded::new(None).is_ok())
4401 }
4402
4403 #[cfg(not(all(target_os = "linux", feature = "opengl")))]
4404 {
4405 false
4406 }
4407 }
4408
4409 #[test]
4421 #[cfg(feature = "opengl")]
4422 fn gl_backend_available_canary() {
4423 let require_gl = std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1");
4424 if !require_gl {
4425 eprintln!(
4426 "SKIPPED: {} — HAL_TEST_REQUIRE_GL is not set to 1",
4427 function!()
4428 );
4429 return;
4430 }
4431 #[cfg(target_os = "macos")]
4432 if std::env::var_os("HAL_TEST_ALLOW_DLOPEN_ANGLE").is_none() {
4433 eprintln!(
4434 "SKIPPED: {} — ANGLE dlopen gate closed (coverage pass 1)",
4435 function!()
4436 );
4437 return;
4438 }
4439 GLProcessorThreaded::new(None).expect(
4440 "HAL_TEST_REQUIRE_GL=1 but the GL backend failed to initialize — \
4441 check the ANGLE install/re-sign step and binary entitlements \
4442 (macOS) or the EGL stack (Linux)",
4443 );
4444 }
4445
4446 #[test]
4447 fn test_load_jpeg_with_exif() {
4448 use edgefirst_codec::peek_info;
4449
4450 let file = edgefirst_bench::testdata::read("zidane_rotated_exif.jpg").to_vec();
4455 let info = peek_info(&file).unwrap();
4456 assert_eq!(info.rotation_degrees, 90);
4457 assert!(!info.flip_horizontal);
4458
4459 let loaded = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4460 assert_eq!(loaded.width(), Some(1280));
4462 assert_eq!(loaded.height(), Some(720));
4463
4464 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4467 let cpu_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4468
4469 let rotation = Rotation::from_degrees_clockwise(info.rotation_degrees as usize);
4470 let (dst_width, dst_height) = (cpu_src.height().unwrap(), cpu_src.width().unwrap());
4471
4472 let cpu_dst = TensorDyn::image(
4473 dst_width,
4474 dst_height,
4475 PixelFormat::Rgba,
4476 DType::U8,
4477 None,
4478 edgefirst_tensor::CpuAccess::ReadWrite,
4479 )
4480 .unwrap();
4481 let mut cpu_converter = CPUProcessor::new();
4482
4483 let loaded_rotated = TensorDyn::image(
4486 dst_width,
4487 dst_height,
4488 PixelFormat::Rgba,
4489 DType::U8,
4490 None,
4491 edgefirst_tensor::CpuAccess::ReadWrite,
4492 )
4493 .unwrap();
4494 let (r0, _loaded, loaded_rotated) = convert_img(
4495 &mut cpu_converter,
4496 loaded,
4497 loaded_rotated,
4498 rotation,
4499 Flip::None,
4500 Crop::no_crop(),
4501 );
4502 r0.unwrap();
4503
4504 let (result, _cpu_src, cpu_dst) = convert_img(
4505 &mut cpu_converter,
4506 cpu_src,
4507 cpu_dst,
4508 rotation,
4509 Flip::None,
4510 Crop::no_crop(),
4511 );
4512 result.unwrap();
4513
4514 compare_images(&loaded_rotated, &cpu_dst, 0.98, function!());
4515 }
4516
4517 #[test]
4518 fn test_load_png_with_exif() {
4519 use edgefirst_codec::peek_info;
4520
4521 let file = edgefirst_bench::testdata::read("zidane_rotated_exif_180.png").to_vec();
4524 let info = peek_info(&file).unwrap();
4525 assert_eq!(info.rotation_degrees, 180);
4526 assert!(!info.flip_horizontal);
4527
4528 let loaded = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4529 assert_eq!(loaded.height(), Some(720));
4531 assert_eq!(loaded.width(), Some(1280));
4532
4533 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4538 let cpu_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4539
4540 let rotation = Rotation::from_degrees_clockwise(info.rotation_degrees as usize);
4541 let cpu_dst = TensorDyn::image(
4542 1280,
4543 720,
4544 PixelFormat::Rgba,
4545 DType::U8,
4546 None,
4547 edgefirst_tensor::CpuAccess::ReadWrite,
4548 )
4549 .unwrap();
4550 let mut cpu_converter = CPUProcessor::new();
4551
4552 let (result, _cpu_src, cpu_dst) = convert_img(
4553 &mut cpu_converter,
4554 cpu_src,
4555 cpu_dst,
4556 rotation,
4557 Flip::None,
4558 Crop::no_crop(),
4559 );
4560 result.unwrap();
4561
4562 let loaded_rotated = TensorDyn::image(
4565 1280,
4566 720,
4567 PixelFormat::Rgba,
4568 DType::U8,
4569 None,
4570 edgefirst_tensor::CpuAccess::ReadWrite,
4571 )
4572 .unwrap();
4573 let (r0, _loaded, loaded_rotated) = convert_img(
4574 &mut cpu_converter,
4575 loaded,
4576 loaded_rotated,
4577 rotation,
4578 Flip::None,
4579 Crop::no_crop(),
4580 );
4581 r0.unwrap();
4582
4583 compare_images(&loaded_rotated, &cpu_dst, 0.95, function!());
4588 }
4589
4590 #[cfg(target_os = "linux")]
4596 fn make_rgb_jpeg(width: u32, height: u32) -> Vec<u8> {
4597 let mut bytes = Vec::with_capacity((width * height * 3) as usize);
4598 for y in 0..height {
4599 for x in 0..width {
4600 bytes.push(((x + y) & 0xFF) as u8);
4601 bytes.push(((x.wrapping_mul(3)) & 0xFF) as u8);
4602 bytes.push(((y.wrapping_mul(5)) & 0xFF) as u8);
4603 }
4604 }
4605 let mut out = Vec::new();
4606 let encoder = jpeg_encoder::Encoder::new(&mut out, 85);
4607 encoder
4608 .encode(
4609 &bytes,
4610 width as u16,
4611 height as u16,
4612 jpeg_encoder::ColorType::Rgb,
4613 )
4614 .expect("jpeg-encoder must succeed on trivial input");
4615 out
4616 }
4617
4618 #[test]
4627 #[cfg(target_os = "linux")]
4628 #[cfg(feature = "opengl")]
4629 fn test_convert_rgba_non_4_aligned_width_end_to_end() {
4630 use edgefirst_tensor::is_dma_available;
4631 if !is_dma_available() {
4632 eprintln!(
4633 "SKIPPED: test_convert_rgba_non_4_aligned_width_end_to_end — DMA not available"
4634 );
4635 return;
4636 }
4637 let jpeg = make_rgb_jpeg(375, 333);
4641 let src_gl = crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), None).unwrap();
4642 assert_eq!(src_gl.width(), Some(375));
4643 let stride = src_gl.row_stride().unwrap();
4645 assert_eq!(stride, 1536, "expected padded pitch 1536, got {stride}");
4646
4647 let mut gl_proc = ImageProcessor::new().unwrap();
4649 let gl_dst = gl_proc
4650 .create_image(
4651 640,
4652 640,
4653 PixelFormat::Rgba,
4654 DType::U8,
4655 None,
4656 edgefirst_tensor::CpuAccess::ReadWrite,
4657 )
4658 .unwrap();
4659 let (r_gl, _src_gl, gl_dst) = convert_img(
4660 &mut gl_proc,
4661 src_gl,
4662 gl_dst,
4663 Rotation::None,
4664 Flip::None,
4665 Crop::no_crop(),
4666 );
4667 r_gl.expect("GL-backed convert must succeed for 375x333 Rgba src");
4668
4669 let src_cpu =
4674 crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), Some(TensorMemory::Mem))
4675 .unwrap();
4676 let mut cpu_proc = ImageProcessor::with_config(ImageProcessorConfig {
4677 backend: ComputeBackend::Cpu,
4678 ..Default::default()
4679 })
4680 .unwrap();
4681 let cpu_dst = TensorDyn::image(
4682 640,
4683 640,
4684 PixelFormat::Rgba,
4685 DType::U8,
4686 Some(TensorMemory::Mem),
4687 edgefirst_tensor::CpuAccess::ReadWrite,
4688 )
4689 .unwrap();
4690 let (r_cpu, _src_cpu, cpu_dst) = convert_img(
4691 &mut cpu_proc,
4692 src_cpu,
4693 cpu_dst,
4694 Rotation::None,
4695 Flip::None,
4696 Crop::no_crop(),
4697 );
4698 r_cpu.unwrap();
4699
4700 compare_images(&gl_dst, &cpu_dst, 0.95, function!());
4704 }
4705
4706 #[test]
4713 #[cfg(target_os = "linux")]
4714 fn test_load_jpeg_rgba_non_aligned_pitch_padded_dma() {
4715 use edgefirst_tensor::is_dma_available;
4716 if !is_dma_available() {
4717 eprintln!(
4718 "SKIPPED: test_load_jpeg_rgba_non_aligned_pitch_padded_dma — DMA not available"
4719 );
4720 return;
4721 }
4722 for &w in &[500u32, 612, 428] {
4726 let jpeg = make_rgb_jpeg(w, 333);
4727 let loaded =
4728 crate::load_image_test_helper(&jpeg, Some(PixelFormat::Rgba), None).unwrap();
4729 let natural = (w as usize) * 4;
4730 let aligned = crate::align_pitch_bytes_to_gpu_alignment(natural).unwrap();
4731 assert!(
4732 aligned > natural,
4733 "test sanity: width {w} should be unaligned"
4734 );
4735 let stride = loaded
4736 .row_stride()
4737 .expect("padded DMA path must set an explicit row_stride — regression if None");
4738 assert_eq!(
4739 stride, aligned,
4740 "width {w}: expected padded stride {aligned}, got {stride} \
4741 (regression: pitch-padding branch skipped?)"
4742 );
4743 let eff = loaded.effective_row_stride().unwrap();
4744 assert_eq!(
4745 eff, aligned,
4746 "effective_row_stride must match stored stride"
4747 );
4748 assert_eq!(loaded.width(), Some(w as usize));
4749 assert_eq!(loaded.height(), Some(333));
4750 }
4751 }
4752
4753 #[test]
4762 #[cfg(target_os = "linux")]
4763 fn test_padded_dma_pitch_for_respects_memory_choice() {
4764 use edgefirst_tensor::{is_dma_available, TensorMemory};
4765
4766 let unaligned_w = 500;
4769
4770 assert_eq!(
4772 crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &Some(TensorMemory::Mem),),
4773 None,
4774 "Mem must never trigger DMA padding"
4775 );
4776 assert_eq!(
4777 crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &Some(TensorMemory::Shm),),
4778 None,
4779 "Shm must never trigger DMA padding"
4780 );
4781
4782 assert_eq!(
4787 crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &Some(TensorMemory::Dma),),
4788 Some(2048),
4789 "explicit Dma must pad regardless of runtime DMA availability"
4790 );
4791
4792 let none_result = crate::padded_dma_pitch_for(PixelFormat::Rgba, unaligned_w, &None);
4796 if is_dma_available() {
4797 assert_eq!(
4798 none_result,
4799 Some(2048),
4800 "memory=None + DMA available → pad (will route through DMA)"
4801 );
4802 } else {
4803 assert_eq!(
4804 none_result, None,
4805 "memory=None + DMA unavailable → must NOT pad (would force \
4806 image_with_stride into a DMA-only allocation that fails). \
4807 Regression: padded_dma_pitch_for ignored is_dma_available()."
4808 );
4809 }
4810 }
4811
4812 fn make_grey_png(width: u32, height: u32) -> Vec<u8> {
4816 let mut bytes = Vec::with_capacity((width * height) as usize);
4817 for y in 0..height {
4818 for x in 0..width {
4819 bytes.push(((x + y) & 0xFF) as u8);
4820 }
4821 }
4822 let img = image::GrayImage::from_vec(width, height, bytes).unwrap();
4823 let mut buf = Vec::new();
4824 img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)
4825 .unwrap();
4826 buf
4827 }
4828
4829 #[test]
4834 #[cfg(target_os = "linux")]
4835 fn test_load_png_grey_misaligned_width_dma() {
4836 use edgefirst_tensor::is_dma_available;
4837 if !is_dma_available() {
4838 eprintln!("SKIPPED: test_load_png_grey_misaligned_width_dma — DMA not available");
4839 return;
4840 }
4841 let png = make_grey_png(612, 388);
4842 let loaded = crate::load_image_test_helper(&png, Some(PixelFormat::Grey), None).unwrap();
4843 assert_eq!(loaded.width(), Some(612));
4844 assert_eq!(loaded.height(), Some(388));
4845 assert_eq!(loaded.format(), Some(PixelFormat::Grey));
4846
4847 let map = loaded.as_u8().unwrap().map().unwrap();
4850 let stride = loaded.row_stride().unwrap_or(612);
4851 assert!(stride >= 612);
4852 let bytes: &[u8] = ↦
4853 for y in 0..388usize {
4854 for x in 0..612usize {
4855 let expected = ((x + y) & 0xFF) as u8;
4856 let got = bytes[y * stride + x];
4857 assert_eq!(
4858 got, expected,
4859 "grey png mismatch at ({x},{y}): got {got} expected {expected}"
4860 );
4861 }
4862 }
4863 }
4864
4865 #[test]
4869 fn test_load_png_grey_mem() {
4870 use edgefirst_tensor::TensorMemory;
4871 let png = make_grey_png(612, 100);
4872 let loaded =
4873 crate::load_image_test_helper(&png, Some(PixelFormat::Grey), Some(TensorMemory::Mem))
4874 .unwrap();
4875 assert_eq!(loaded.width(), Some(612));
4876 assert_eq!(loaded.height(), Some(100));
4877 assert_eq!(loaded.format(), Some(PixelFormat::Grey));
4878 let map = loaded.as_u8().unwrap().map().unwrap();
4879 let bytes: &[u8] = ↦
4880 assert_eq!(bytes.len(), 612 * 100);
4882 for y in 0..100 {
4883 for x in 0..612 {
4884 assert_eq!(bytes[y * 612 + x], ((x + y) & 0xFF) as u8);
4885 }
4886 }
4887 }
4888
4889 #[test]
4893 fn test_load_png_grey_to_rgb_mem() {
4894 use edgefirst_tensor::TensorMemory;
4895 let png = make_grey_png(620, 240);
4896 let loaded =
4897 crate::load_image_test_helper(&png, Some(PixelFormat::Rgb), Some(TensorMemory::Mem))
4898 .unwrap();
4899 assert_eq!(loaded.width(), Some(620));
4900 assert_eq!(loaded.height(), Some(240));
4901 assert_eq!(loaded.format(), Some(PixelFormat::Rgb));
4902
4903 let map = loaded.as_u8().unwrap().map().unwrap();
4905 let bytes: &[u8] = ↦
4906 for (x, y) in [(0usize, 0usize), (100, 50), (619, 239)] {
4907 let expected = ((x + y) & 0xFF) as u8;
4908 let off = (y * 620 + x) * 3;
4909 assert_eq!(bytes[off], expected, "R@{x},{y}");
4910 assert_eq!(bytes[off + 1], expected, "G@{x},{y}");
4911 assert_eq!(bytes[off + 2], expected, "B@{x},{y}");
4912 }
4913 }
4914
4915 #[test]
4916 #[cfg(target_os = "linux")]
4917 fn test_g2d_resize() {
4918 if !is_g2d_available() {
4919 eprintln!("SKIPPED: test_g2d_resize - G2D library (libg2d.so.2) not available");
4920 return;
4921 }
4922 if !is_dma_available() {
4923 eprintln!(
4924 "SKIPPED: test_g2d_resize - DMA memory allocation not available (permission denied or no DMA-BUF support)"
4925 );
4926 return;
4927 }
4928
4929 let dst_width = 640;
4930 let dst_height = 360;
4931 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4932 let src =
4933 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), Some(TensorMemory::Dma))
4934 .unwrap();
4935
4936 let g2d_dst = TensorDyn::image(
4937 dst_width,
4938 dst_height,
4939 PixelFormat::Rgba,
4940 DType::U8,
4941 Some(TensorMemory::Dma),
4942 edgefirst_tensor::CpuAccess::ReadWrite,
4943 )
4944 .unwrap();
4945 let mut g2d_converter = G2DProcessor::new().unwrap();
4946 let (result, src, g2d_dst) = convert_img(
4947 &mut g2d_converter,
4948 src,
4949 g2d_dst,
4950 Rotation::None,
4951 Flip::None,
4952 Crop::no_crop(),
4953 );
4954 result.unwrap();
4955
4956 let cpu_dst = TensorDyn::image(
4957 dst_width,
4958 dst_height,
4959 PixelFormat::Rgba,
4960 DType::U8,
4961 None,
4962 edgefirst_tensor::CpuAccess::ReadWrite,
4963 )
4964 .unwrap();
4965 let mut cpu_converter = CPUProcessor::new();
4966 let (result, _src, cpu_dst) = convert_img(
4967 &mut cpu_converter,
4968 src,
4969 cpu_dst,
4970 Rotation::None,
4971 Flip::None,
4972 Crop::no_crop(),
4973 );
4974 result.unwrap();
4975
4976 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
4982 }
4983
4984 #[test]
4985 #[cfg(target_os = "linux")]
4986 #[cfg(feature = "opengl")]
4987 fn test_opengl_resize() {
4988 if !is_opengl_available() {
4989 eprintln!("SKIPPED: {} - OpenGL not available", function!());
4990 return;
4991 }
4992
4993 let dst_width = 640;
4994 let dst_height = 360;
4995 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
4996 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
4997
4998 let cpu_dst = TensorDyn::image(
4999 dst_width,
5000 dst_height,
5001 PixelFormat::Rgba,
5002 DType::U8,
5003 None,
5004 edgefirst_tensor::CpuAccess::ReadWrite,
5005 )
5006 .unwrap();
5007 let mut cpu_converter = CPUProcessor::new();
5008 let (result, src, cpu_dst) = convert_img(
5009 &mut cpu_converter,
5010 src,
5011 cpu_dst,
5012 Rotation::None,
5013 Flip::None,
5014 Crop::no_crop(),
5015 );
5016 result.unwrap();
5017
5018 let mut src = src;
5019 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5020
5021 for _ in 0..5 {
5022 let gl_dst = TensorDyn::image(
5023 dst_width,
5024 dst_height,
5025 PixelFormat::Rgba,
5026 DType::U8,
5027 None,
5028 edgefirst_tensor::CpuAccess::ReadWrite,
5029 )
5030 .unwrap();
5031 let (result, src_back, gl_dst) = convert_img(
5032 &mut gl_converter,
5033 src,
5034 gl_dst,
5035 Rotation::None,
5036 Flip::None,
5037 Crop::no_crop(),
5038 );
5039 result.unwrap();
5040 src = src_back;
5041
5042 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5043 }
5044 }
5045
5046 #[test]
5047 #[cfg(target_os = "linux")]
5048 #[cfg(feature = "opengl")]
5049 fn test_opengl_10_threads() {
5050 if !is_opengl_available() {
5051 eprintln!("SKIPPED: {} - OpenGL not available", function!());
5052 return;
5053 }
5054
5055 let handles: Vec<_> = (0..10)
5056 .map(|i| {
5057 std::thread::Builder::new()
5058 .name(format!("Thread {i}"))
5059 .spawn(test_opengl_resize)
5060 .unwrap()
5061 })
5062 .collect();
5063 handles.into_iter().for_each(|h| {
5064 if let Err(e) = h.join() {
5065 std::panic::resume_unwind(e)
5066 }
5067 });
5068 }
5069
5070 #[test]
5071 #[cfg(target_os = "linux")]
5072 #[cfg(feature = "opengl")]
5073 fn test_opengl_grey() {
5074 if !is_opengl_available() {
5075 eprintln!("SKIPPED: {} - OpenGL not available", function!());
5076 return;
5077 }
5078
5079 let img = crate::load_image_test_helper(
5080 &edgefirst_bench::testdata::read("grey.jpg"),
5081 Some(PixelFormat::Grey),
5082 None,
5083 )
5084 .unwrap();
5085
5086 let gl_dst = TensorDyn::image(
5087 640,
5088 640,
5089 PixelFormat::Grey,
5090 DType::U8,
5091 None,
5092 edgefirst_tensor::CpuAccess::ReadWrite,
5093 )
5094 .unwrap();
5095 let cpu_dst = TensorDyn::image(
5096 640,
5097 640,
5098 PixelFormat::Grey,
5099 DType::U8,
5100 None,
5101 edgefirst_tensor::CpuAccess::ReadWrite,
5102 )
5103 .unwrap();
5104
5105 let mut converter = CPUProcessor::new();
5106
5107 let (result, img, cpu_dst) = convert_img(
5108 &mut converter,
5109 img,
5110 cpu_dst,
5111 Rotation::None,
5112 Flip::None,
5113 Crop::no_crop(),
5114 );
5115 result.unwrap();
5116
5117 let mut gl = GLProcessorThreaded::new(None).unwrap();
5118 let (result, _img, gl_dst) = convert_img(
5119 &mut gl,
5120 img,
5121 gl_dst,
5122 Rotation::None,
5123 Flip::None,
5124 Crop::no_crop(),
5125 );
5126 result.unwrap();
5127
5128 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5129 }
5130
5131 #[test]
5132 #[cfg(target_os = "linux")]
5133 fn test_g2d_src_crop() {
5134 if !is_g2d_available() {
5135 eprintln!("SKIPPED: test_g2d_src_crop - G2D library (libg2d.so.2) not available");
5136 return;
5137 }
5138 if !is_dma_available() {
5139 eprintln!(
5140 "SKIPPED: test_g2d_src_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)"
5141 );
5142 return;
5143 }
5144
5145 let dst_width = 640;
5146 let dst_height = 640;
5147 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5148 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5149
5150 let cpu_dst = TensorDyn::image(
5151 dst_width,
5152 dst_height,
5153 PixelFormat::Rgba,
5154 DType::U8,
5155 None,
5156 edgefirst_tensor::CpuAccess::ReadWrite,
5157 )
5158 .unwrap();
5159 let mut cpu_converter = CPUProcessor::new();
5160 let crop = Crop::new().with_source(Some(Region::new(0, 0, 640, 360)));
5161 let (result, src, cpu_dst) = convert_img(
5162 &mut cpu_converter,
5163 src,
5164 cpu_dst,
5165 Rotation::None,
5166 Flip::None,
5167 crop,
5168 );
5169 result.unwrap();
5170
5171 let g2d_dst = TensorDyn::image(
5172 dst_width,
5173 dst_height,
5174 PixelFormat::Rgba,
5175 DType::U8,
5176 None,
5177 edgefirst_tensor::CpuAccess::ReadWrite,
5178 )
5179 .unwrap();
5180 let mut g2d_converter = G2DProcessor::new().unwrap();
5181 let (result, _src, g2d_dst) = convert_img(
5182 &mut g2d_converter,
5183 src,
5184 g2d_dst,
5185 Rotation::None,
5186 Flip::None,
5187 crop,
5188 );
5189 result.unwrap();
5190
5191 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
5197 }
5198
5199 #[test]
5200 #[cfg(target_os = "linux")]
5201 fn test_g2d_dst_crop() {
5202 if !is_g2d_available() {
5203 eprintln!("SKIPPED: test_g2d_dst_crop - G2D library (libg2d.so.2) not available");
5204 return;
5205 }
5206 if !is_dma_available() {
5207 eprintln!(
5208 "SKIPPED: test_g2d_dst_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)"
5209 );
5210 return;
5211 }
5212
5213 let dst_width = 640;
5214 let dst_height = 640;
5215 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5216 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5217
5218 let cpu_dst = TensorDyn::image(
5219 dst_width,
5220 dst_height,
5221 PixelFormat::Rgba,
5222 DType::U8,
5223 None,
5224 edgefirst_tensor::CpuAccess::ReadWrite,
5225 )
5226 .unwrap();
5227 let mut cpu_converter = CPUProcessor::new();
5228 let crop = Crop::new();
5229 let (result, src, cpu_dst) = convert_img(
5230 &mut cpu_converter,
5231 src,
5232 cpu_dst,
5233 Rotation::None,
5234 Flip::None,
5235 crop,
5236 );
5237 result.unwrap();
5238
5239 let g2d_dst = TensorDyn::image(
5240 dst_width,
5241 dst_height,
5242 PixelFormat::Rgba,
5243 DType::U8,
5244 None,
5245 edgefirst_tensor::CpuAccess::ReadWrite,
5246 )
5247 .unwrap();
5248 let mut g2d_converter = G2DProcessor::new().unwrap();
5249 let (result, _src, g2d_dst) = convert_img(
5250 &mut g2d_converter,
5251 src,
5252 g2d_dst,
5253 Rotation::None,
5254 Flip::None,
5255 crop,
5256 );
5257 result.unwrap();
5258
5259 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
5265 }
5266
5267 #[test]
5268 #[cfg(target_os = "linux")]
5269 fn test_g2d_all_rgba() {
5270 if !is_g2d_available() {
5271 eprintln!("SKIPPED: test_g2d_all_rgba - G2D library (libg2d.so.2) not available");
5272 return;
5273 }
5274 if !is_dma_available() {
5275 eprintln!(
5276 "SKIPPED: test_g2d_all_rgba - DMA memory allocation not available (permission denied or no DMA-BUF support)"
5277 );
5278 return;
5279 }
5280
5281 let dst_width = 640;
5282 let dst_height = 640;
5283 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5284 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5285 let src_dyn = src;
5286
5287 let mut cpu_dst = TensorDyn::image(
5288 dst_width,
5289 dst_height,
5290 PixelFormat::Rgba,
5291 DType::U8,
5292 None,
5293 edgefirst_tensor::CpuAccess::ReadWrite,
5294 )
5295 .unwrap();
5296 let mut cpu_converter = CPUProcessor::new();
5297 let mut g2d_dst = TensorDyn::image(
5298 dst_width,
5299 dst_height,
5300 PixelFormat::Rgba,
5301 DType::U8,
5302 None,
5303 edgefirst_tensor::CpuAccess::ReadWrite,
5304 )
5305 .unwrap();
5306 let mut g2d_converter = G2DProcessor::new().unwrap();
5307
5308 let crop = Crop::new().with_source(Some(Region::new(50, 120, 1024, 576)));
5309
5310 for rot in [
5311 Rotation::None,
5312 Rotation::Clockwise90,
5313 Rotation::Rotate180,
5314 Rotation::CounterClockwise90,
5315 ] {
5316 cpu_dst
5317 .as_u8()
5318 .unwrap()
5319 .map()
5320 .unwrap()
5321 .as_mut_slice()
5322 .fill(114);
5323 g2d_dst
5324 .as_u8()
5325 .unwrap()
5326 .map()
5327 .unwrap()
5328 .as_mut_slice()
5329 .fill(114);
5330 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
5331 let mut cpu_dst_dyn = cpu_dst;
5332 cpu_converter
5333 .convert(&src_dyn, &mut cpu_dst_dyn, Rotation::None, Flip::None, crop)
5334 .unwrap();
5335 cpu_dst = {
5336 let mut __t = cpu_dst_dyn.into_u8().unwrap();
5337 __t.set_format(PixelFormat::Rgba).unwrap();
5338 TensorDyn::from(__t)
5339 };
5340
5341 let mut g2d_dst_dyn = g2d_dst;
5342 g2d_converter
5343 .convert(&src_dyn, &mut g2d_dst_dyn, Rotation::None, Flip::None, crop)
5344 .unwrap();
5345 g2d_dst = {
5346 let mut __t = g2d_dst_dyn.into_u8().unwrap();
5347 __t.set_format(PixelFormat::Rgba).unwrap();
5348 TensorDyn::from(__t)
5349 };
5350
5351 compare_images(
5352 &g2d_dst,
5353 &cpu_dst,
5354 0.98,
5355 &format!("{} {:?} {:?}", function!(), rot, flip),
5356 );
5357 }
5358 }
5359 }
5360
5361 #[test]
5362 #[cfg(target_os = "linux")]
5363 #[cfg(feature = "opengl")]
5364 fn test_opengl_src_crop() {
5365 if !is_opengl_available() {
5366 eprintln!("SKIPPED: {} - OpenGL not available", function!());
5367 return;
5368 }
5369
5370 let dst_width = 640;
5371 let dst_height = 360;
5372 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5373 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5374 let crop = Crop::new().with_source(Some(Region::new(320, 180, 1280 - 320, 720 - 180)));
5375
5376 let cpu_dst = TensorDyn::image(
5377 dst_width,
5378 dst_height,
5379 PixelFormat::Rgba,
5380 DType::U8,
5381 None,
5382 edgefirst_tensor::CpuAccess::ReadWrite,
5383 )
5384 .unwrap();
5385 let mut cpu_converter = CPUProcessor::new();
5386 let (result, src, cpu_dst) = convert_img(
5387 &mut cpu_converter,
5388 src,
5389 cpu_dst,
5390 Rotation::None,
5391 Flip::None,
5392 crop,
5393 );
5394 result.unwrap();
5395
5396 let gl_dst = TensorDyn::image(
5397 dst_width,
5398 dst_height,
5399 PixelFormat::Rgba,
5400 DType::U8,
5401 None,
5402 edgefirst_tensor::CpuAccess::ReadWrite,
5403 )
5404 .unwrap();
5405 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5406 let (result, _src, gl_dst) = convert_img(
5407 &mut gl_converter,
5408 src,
5409 gl_dst,
5410 Rotation::None,
5411 Flip::None,
5412 crop,
5413 );
5414 result.unwrap();
5415
5416 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5417 }
5418
5419 #[test]
5420 #[cfg(target_os = "linux")]
5421 #[cfg(feature = "opengl")]
5422 fn test_opengl_dst_crop() {
5423 if !is_opengl_available() {
5424 eprintln!("SKIPPED: {} - OpenGL not available", function!());
5425 return;
5426 }
5427
5428 let dst_width = 640;
5429 let dst_height = 640;
5430 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5431 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5432
5433 let cpu_dst = TensorDyn::image(
5434 dst_width,
5435 dst_height,
5436 PixelFormat::Rgba,
5437 DType::U8,
5438 None,
5439 edgefirst_tensor::CpuAccess::ReadWrite,
5440 )
5441 .unwrap();
5442 let mut cpu_converter = CPUProcessor::new();
5443 let crop = Crop::new();
5444 let (result, src, cpu_dst) = convert_img(
5445 &mut cpu_converter,
5446 src,
5447 cpu_dst,
5448 Rotation::None,
5449 Flip::None,
5450 crop,
5451 );
5452 result.unwrap();
5453
5454 let gl_dst = TensorDyn::image(
5455 dst_width,
5456 dst_height,
5457 PixelFormat::Rgba,
5458 DType::U8,
5459 None,
5460 edgefirst_tensor::CpuAccess::ReadWrite,
5461 )
5462 .unwrap();
5463 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5464 let (result, _src, gl_dst) = convert_img(
5465 &mut gl_converter,
5466 src,
5467 gl_dst,
5468 Rotation::None,
5469 Flip::None,
5470 crop,
5471 );
5472 result.unwrap();
5473
5474 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5475 }
5476
5477 #[test]
5478 #[cfg(target_os = "linux")]
5479 #[cfg(feature = "opengl")]
5480 fn test_opengl_all_rgba() {
5481 if !is_opengl_available() {
5482 eprintln!("SKIPPED: {} - OpenGL not available", function!());
5483 return;
5484 }
5485
5486 let dst_width = 640;
5487 let dst_height = 640;
5488 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5489
5490 let mut cpu_converter = CPUProcessor::new();
5491
5492 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5493
5494 let mut mem = vec![None, Some(TensorMemory::Mem), Some(TensorMemory::Shm)];
5495 if is_dma_available() {
5496 mem.push(Some(TensorMemory::Dma));
5497 }
5498 let crop = Crop::new().with_source(Some(Region::new(50, 120, 1024, 576)));
5499 for m in mem {
5500 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), m).unwrap();
5501 let src_dyn = src;
5502
5503 for rot in [
5504 Rotation::None,
5505 Rotation::Clockwise90,
5506 Rotation::Rotate180,
5507 Rotation::CounterClockwise90,
5508 ] {
5509 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
5510 let cpu_dst = TensorDyn::image(
5511 dst_width,
5512 dst_height,
5513 PixelFormat::Rgba,
5514 DType::U8,
5515 m,
5516 edgefirst_tensor::CpuAccess::ReadWrite,
5517 )
5518 .unwrap();
5519 let gl_dst = TensorDyn::image(
5520 dst_width,
5521 dst_height,
5522 PixelFormat::Rgba,
5523 DType::U8,
5524 m,
5525 edgefirst_tensor::CpuAccess::ReadWrite,
5526 )
5527 .unwrap();
5528 cpu_dst
5529 .as_u8()
5530 .unwrap()
5531 .map()
5532 .unwrap()
5533 .as_mut_slice()
5534 .fill(114);
5535 gl_dst
5536 .as_u8()
5537 .unwrap()
5538 .map()
5539 .unwrap()
5540 .as_mut_slice()
5541 .fill(114);
5542
5543 let mut cpu_dst_dyn = cpu_dst;
5544 cpu_converter
5545 .convert(&src_dyn, &mut cpu_dst_dyn, Rotation::None, Flip::None, crop)
5546 .unwrap();
5547 let cpu_dst = {
5548 let mut __t = cpu_dst_dyn.into_u8().unwrap();
5549 __t.set_format(PixelFormat::Rgba).unwrap();
5550 TensorDyn::from(__t)
5551 };
5552
5553 let mut gl_dst_dyn = gl_dst;
5554 gl_converter
5555 .convert(&src_dyn, &mut gl_dst_dyn, Rotation::None, Flip::None, crop)
5556 .map_err(|e| {
5557 log::error!("error mem {m:?} rot {rot:?} error: {e:?}");
5558 e
5559 })
5560 .unwrap();
5561 let gl_dst = {
5562 let mut __t = gl_dst_dyn.into_u8().unwrap();
5563 __t.set_format(PixelFormat::Rgba).unwrap();
5564 TensorDyn::from(__t)
5565 };
5566
5567 compare_images(
5568 &gl_dst,
5569 &cpu_dst,
5570 0.98,
5571 &format!("{} {:?} {:?}", function!(), rot, flip),
5572 );
5573 }
5574 }
5575 }
5576 }
5577
5578 #[test]
5579 #[cfg(target_os = "linux")]
5580 fn test_cpu_rotate() {
5581 for rot in [
5582 Rotation::Clockwise90,
5583 Rotation::Rotate180,
5584 Rotation::CounterClockwise90,
5585 ] {
5586 test_cpu_rotate_(rot);
5587 }
5588 }
5589
5590 #[cfg(target_os = "linux")]
5591 fn test_cpu_rotate_(rot: Rotation) {
5592 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5596
5597 let unchanged_src =
5598 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5599 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
5600
5601 let (dst_width, dst_height) = match rot {
5602 Rotation::None | Rotation::Rotate180 => (src.width().unwrap(), src.height().unwrap()),
5603 Rotation::Clockwise90 | Rotation::CounterClockwise90 => {
5604 (src.height().unwrap(), src.width().unwrap())
5605 }
5606 };
5607
5608 let cpu_dst = TensorDyn::image(
5609 dst_width,
5610 dst_height,
5611 PixelFormat::Rgba,
5612 DType::U8,
5613 None,
5614 edgefirst_tensor::CpuAccess::ReadWrite,
5615 )
5616 .unwrap();
5617 let mut cpu_converter = CPUProcessor::new();
5618
5619 let (result, src, cpu_dst) = convert_img(
5622 &mut cpu_converter,
5623 src,
5624 cpu_dst,
5625 rot,
5626 Flip::None,
5627 Crop::no_crop(),
5628 );
5629 result.unwrap();
5630
5631 let (result, cpu_dst, src) = convert_img(
5632 &mut cpu_converter,
5633 cpu_dst,
5634 src,
5635 rot,
5636 Flip::None,
5637 Crop::no_crop(),
5638 );
5639 result.unwrap();
5640
5641 let (result, src, cpu_dst) = convert_img(
5642 &mut cpu_converter,
5643 src,
5644 cpu_dst,
5645 rot,
5646 Flip::None,
5647 Crop::no_crop(),
5648 );
5649 result.unwrap();
5650
5651 let (result, _cpu_dst, src) = convert_img(
5652 &mut cpu_converter,
5653 cpu_dst,
5654 src,
5655 rot,
5656 Flip::None,
5657 Crop::no_crop(),
5658 );
5659 result.unwrap();
5660
5661 compare_images(&src, &unchanged_src, 0.98, function!());
5662 }
5663
5664 #[test]
5665 #[cfg(target_os = "linux")]
5666 #[cfg(feature = "opengl")]
5667 fn test_opengl_rotate() {
5668 if !is_opengl_available() {
5669 eprintln!("SKIPPED: {} - OpenGL not available", function!());
5670 return;
5671 }
5672
5673 let size = (1280, 720);
5674 let mut mem = vec![None, Some(TensorMemory::Shm), Some(TensorMemory::Mem)];
5675
5676 if is_dma_available() {
5677 mem.push(Some(TensorMemory::Dma));
5678 }
5679 for m in mem {
5680 for rot in [
5681 Rotation::Clockwise90,
5682 Rotation::Rotate180,
5683 Rotation::CounterClockwise90,
5684 ] {
5685 test_opengl_rotate_(size, rot, m);
5686 }
5687 }
5688 }
5689
5690 #[cfg(target_os = "linux")]
5691 #[cfg(feature = "opengl")]
5692 fn test_opengl_rotate_(
5693 size: (usize, usize),
5694 rot: Rotation,
5695 tensor_memory: Option<TensorMemory>,
5696 ) {
5697 let (dst_width, dst_height) = match rot {
5698 Rotation::None | Rotation::Rotate180 => size,
5699 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
5700 };
5701
5702 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5703 let src =
5704 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), tensor_memory).unwrap();
5705
5706 let cpu_dst = TensorDyn::image(
5707 dst_width,
5708 dst_height,
5709 PixelFormat::Rgba,
5710 DType::U8,
5711 None,
5712 edgefirst_tensor::CpuAccess::ReadWrite,
5713 )
5714 .unwrap();
5715 let mut cpu_converter = CPUProcessor::new();
5716
5717 let (result, mut src, cpu_dst) = convert_img(
5718 &mut cpu_converter,
5719 src,
5720 cpu_dst,
5721 rot,
5722 Flip::None,
5723 Crop::no_crop(),
5724 );
5725 result.unwrap();
5726
5727 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5728
5729 for _ in 0..5 {
5730 let gl_dst = TensorDyn::image(
5731 dst_width,
5732 dst_height,
5733 PixelFormat::Rgba,
5734 DType::U8,
5735 tensor_memory,
5736 edgefirst_tensor::CpuAccess::ReadWrite,
5737 )
5738 .unwrap();
5739 let (result, src_back, gl_dst) = convert_img(
5740 &mut gl_converter,
5741 src,
5742 gl_dst,
5743 rot,
5744 Flip::None,
5745 Crop::no_crop(),
5746 );
5747 result.unwrap();
5748 src = src_back;
5749 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5750 }
5751 }
5752
5753 #[test]
5754 #[cfg(target_os = "linux")]
5755 fn test_g2d_rotate() {
5756 if !is_g2d_available() {
5757 eprintln!("SKIPPED: test_g2d_rotate - G2D library (libg2d.so.2) not available");
5758 return;
5759 }
5760 if !is_dma_available() {
5761 eprintln!(
5762 "SKIPPED: test_g2d_rotate - DMA memory allocation not available (permission denied or no DMA-BUF support)"
5763 );
5764 return;
5765 }
5766
5767 let size = (1280, 720);
5768 for rot in [
5769 Rotation::Clockwise90,
5770 Rotation::Rotate180,
5771 Rotation::CounterClockwise90,
5772 ] {
5773 test_g2d_rotate_(size, rot);
5774 }
5775 }
5776
5777 #[cfg(target_os = "linux")]
5778 fn test_g2d_rotate_(size: (usize, usize), rot: Rotation) {
5779 let (dst_width, dst_height) = match rot {
5780 Rotation::None | Rotation::Rotate180 => size,
5781 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
5782 };
5783
5784 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
5785 let src =
5786 crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), Some(TensorMemory::Dma))
5787 .unwrap();
5788
5789 let cpu_dst = TensorDyn::image(
5790 dst_width,
5791 dst_height,
5792 PixelFormat::Rgba,
5793 DType::U8,
5794 None,
5795 edgefirst_tensor::CpuAccess::ReadWrite,
5796 )
5797 .unwrap();
5798 let mut cpu_converter = CPUProcessor::new();
5799
5800 let (result, src, cpu_dst) = convert_img(
5801 &mut cpu_converter,
5802 src,
5803 cpu_dst,
5804 rot,
5805 Flip::None,
5806 Crop::no_crop(),
5807 );
5808 result.unwrap();
5809
5810 let g2d_dst = TensorDyn::image(
5811 dst_width,
5812 dst_height,
5813 PixelFormat::Rgba,
5814 DType::U8,
5815 Some(TensorMemory::Dma),
5816 edgefirst_tensor::CpuAccess::ReadWrite,
5817 )
5818 .unwrap();
5819 let mut g2d_converter = G2DProcessor::new().unwrap();
5820
5821 let (result, _src, g2d_dst) = convert_img(
5822 &mut g2d_converter,
5823 src,
5824 g2d_dst,
5825 rot,
5826 Flip::None,
5827 Crop::no_crop(),
5828 );
5829 result.unwrap();
5830
5831 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
5837 }
5838
5839 #[test]
5840 fn test_rgba_to_yuyv_resize_cpu() {
5841 let src = load_bytes_to_tensor(
5842 1280,
5843 720,
5844 PixelFormat::Rgba,
5845 None,
5846 &edgefirst_bench::testdata::read("camera720p.rgba"),
5847 )
5848 .unwrap();
5849
5850 let (dst_width, dst_height) = (640, 360);
5851
5852 let dst = TensorDyn::image(
5853 dst_width,
5854 dst_height,
5855 PixelFormat::Yuyv,
5856 DType::U8,
5857 None,
5858 edgefirst_tensor::CpuAccess::ReadWrite,
5859 )
5860 .unwrap();
5861
5862 let dst_through_yuyv = TensorDyn::image(
5863 dst_width,
5864 dst_height,
5865 PixelFormat::Rgba,
5866 DType::U8,
5867 None,
5868 edgefirst_tensor::CpuAccess::ReadWrite,
5869 )
5870 .unwrap();
5871 let dst_direct = TensorDyn::image(
5872 dst_width,
5873 dst_height,
5874 PixelFormat::Rgba,
5875 DType::U8,
5876 None,
5877 edgefirst_tensor::CpuAccess::ReadWrite,
5878 )
5879 .unwrap();
5880
5881 let mut cpu_converter = CPUProcessor::new();
5882
5883 let (result, src, dst) = convert_img(
5884 &mut cpu_converter,
5885 src,
5886 dst,
5887 Rotation::None,
5888 Flip::None,
5889 Crop::no_crop(),
5890 );
5891 result.unwrap();
5892
5893 let (result, _dst, dst_through_yuyv) = convert_img(
5894 &mut cpu_converter,
5895 dst,
5896 dst_through_yuyv,
5897 Rotation::None,
5898 Flip::None,
5899 Crop::no_crop(),
5900 );
5901 result.unwrap();
5902
5903 let (result, _src, dst_direct) = convert_img(
5904 &mut cpu_converter,
5905 src,
5906 dst_direct,
5907 Rotation::None,
5908 Flip::None,
5909 Crop::no_crop(),
5910 );
5911 result.unwrap();
5912
5913 compare_images(&dst_through_yuyv, &dst_direct, 0.98, function!());
5914 }
5915
5916 #[test]
5917 #[cfg(target_os = "linux")]
5918 #[cfg(feature = "opengl")]
5919 #[ignore = "opengl doesn't support rendering to PixelFormat::Yuyv texture"]
5920 fn test_rgba_to_yuyv_resize_opengl() {
5921 if !is_opengl_available() {
5922 eprintln!("SKIPPED: {} - OpenGL not available", function!());
5923 return;
5924 }
5925
5926 if !is_dma_available() {
5927 eprintln!(
5928 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
5929 function!()
5930 );
5931 return;
5932 }
5933
5934 let src = load_bytes_to_tensor(
5935 1280,
5936 720,
5937 PixelFormat::Rgba,
5938 None,
5939 &edgefirst_bench::testdata::read("camera720p.rgba"),
5940 )
5941 .unwrap();
5942
5943 let (dst_width, dst_height) = (640, 360);
5944
5945 let dst = TensorDyn::image(
5946 dst_width,
5947 dst_height,
5948 PixelFormat::Yuyv,
5949 DType::U8,
5950 Some(TensorMemory::Dma),
5951 edgefirst_tensor::CpuAccess::ReadWrite,
5952 )
5953 .unwrap();
5954
5955 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5956
5957 let (result, src, dst) = convert_img(
5958 &mut gl_converter,
5959 src,
5960 dst,
5961 Rotation::None,
5962 Flip::None,
5963 Crop::letterbox([255, 255, 255, 255]),
5964 );
5965 result.unwrap();
5966
5967 std::fs::write(
5968 "rgba_to_yuyv_opengl.yuyv",
5969 dst.as_u8().unwrap().map().unwrap().as_slice(),
5970 )
5971 .unwrap();
5972 let cpu_dst = TensorDyn::image(
5973 dst_width,
5974 dst_height,
5975 PixelFormat::Yuyv,
5976 DType::U8,
5977 Some(TensorMemory::Dma),
5978 edgefirst_tensor::CpuAccess::ReadWrite,
5979 )
5980 .unwrap();
5981 let (result, _src, cpu_dst) = convert_img(
5982 &mut CPUProcessor::new(),
5983 src,
5984 cpu_dst,
5985 Rotation::None,
5986 Flip::None,
5987 Crop::no_crop(),
5988 );
5989 result.unwrap();
5990
5991 compare_images_convert_to_rgb(&dst, &cpu_dst, 0.98, function!());
5992 }
5993
5994 #[test]
5995 #[cfg(target_os = "linux")]
5996 fn test_rgba_to_yuyv_resize_g2d() {
5997 if !is_g2d_available() {
5998 eprintln!(
5999 "SKIPPED: test_rgba_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available"
6000 );
6001 return;
6002 }
6003 if !is_dma_available() {
6004 eprintln!(
6005 "SKIPPED: test_rgba_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
6006 );
6007 return;
6008 }
6009
6010 let src = load_bytes_to_tensor(
6011 1280,
6012 720,
6013 PixelFormat::Rgba,
6014 Some(TensorMemory::Dma),
6015 &edgefirst_bench::testdata::read("camera720p.rgba"),
6016 )
6017 .unwrap();
6018
6019 let (dst_width, dst_height) = (1280, 720);
6020
6021 let cpu_dst = TensorDyn::image(
6022 dst_width,
6023 dst_height,
6024 PixelFormat::Yuyv,
6025 DType::U8,
6026 Some(TensorMemory::Dma),
6027 edgefirst_tensor::CpuAccess::ReadWrite,
6028 )
6029 .unwrap();
6030
6031 let g2d_dst = TensorDyn::image(
6032 dst_width,
6033 dst_height,
6034 PixelFormat::Yuyv,
6035 DType::U8,
6036 Some(TensorMemory::Dma),
6037 edgefirst_tensor::CpuAccess::ReadWrite,
6038 )
6039 .unwrap();
6040
6041 let mut g2d_converter = G2DProcessor::new().unwrap();
6042 let crop = Crop::new();
6043
6044 g2d_dst
6045 .as_u8()
6046 .unwrap()
6047 .map()
6048 .unwrap()
6049 .as_mut_slice()
6050 .fill(128);
6051 let (result, src, g2d_dst) = convert_img(
6052 &mut g2d_converter,
6053 src,
6054 g2d_dst,
6055 Rotation::None,
6056 Flip::None,
6057 crop,
6058 );
6059 result.unwrap();
6060
6061 let cpu_dst_img = cpu_dst;
6062 cpu_dst_img
6063 .as_u8()
6064 .unwrap()
6065 .map()
6066 .unwrap()
6067 .as_mut_slice()
6068 .fill(128);
6069 let (result, _src, cpu_dst) = convert_img(
6070 &mut CPUProcessor::new(),
6071 src,
6072 cpu_dst_img,
6073 Rotation::None,
6074 Flip::None,
6075 crop,
6076 );
6077 result.unwrap();
6078
6079 compare_images_convert_to_rgb(&cpu_dst, &g2d_dst, 0.98, function!());
6080 }
6081
6082 #[test]
6083 fn test_yuyv_to_rgba_cpu() {
6084 let file = edgefirst_bench::testdata::read("camera720p.yuyv").to_vec();
6085 let src = TensorDyn::image(
6086 1280,
6087 720,
6088 PixelFormat::Yuyv,
6089 DType::U8,
6090 None,
6091 edgefirst_tensor::CpuAccess::ReadWrite,
6092 )
6093 .unwrap();
6094 src.as_u8()
6095 .unwrap()
6096 .map()
6097 .unwrap()
6098 .as_mut_slice()
6099 .copy_from_slice(&file);
6100
6101 let dst = TensorDyn::image(
6102 1280,
6103 720,
6104 PixelFormat::Rgba,
6105 DType::U8,
6106 None,
6107 edgefirst_tensor::CpuAccess::ReadWrite,
6108 )
6109 .unwrap();
6110 let mut cpu_converter = CPUProcessor::new();
6111
6112 let (result, _src, dst) = convert_img(
6113 &mut cpu_converter,
6114 src,
6115 dst,
6116 Rotation::None,
6117 Flip::None,
6118 Crop::no_crop(),
6119 );
6120 result.unwrap();
6121
6122 let target_image = TensorDyn::image(
6123 1280,
6124 720,
6125 PixelFormat::Rgba,
6126 DType::U8,
6127 None,
6128 edgefirst_tensor::CpuAccess::ReadWrite,
6129 )
6130 .unwrap();
6131 target_image
6132 .as_u8()
6133 .unwrap()
6134 .map()
6135 .unwrap()
6136 .as_mut_slice()
6137 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
6138
6139 compare_images(&dst, &target_image, 0.98, function!());
6142 }
6143
6144 #[test]
6145 fn test_yuyv_to_rgb_cpu() {
6146 let file = edgefirst_bench::testdata::read("camera720p.yuyv").to_vec();
6147 let src = TensorDyn::image(
6148 1280,
6149 720,
6150 PixelFormat::Yuyv,
6151 DType::U8,
6152 None,
6153 edgefirst_tensor::CpuAccess::ReadWrite,
6154 )
6155 .unwrap();
6156 src.as_u8()
6157 .unwrap()
6158 .map()
6159 .unwrap()
6160 .as_mut_slice()
6161 .copy_from_slice(&file);
6162
6163 let dst = TensorDyn::image(
6164 1280,
6165 720,
6166 PixelFormat::Rgb,
6167 DType::U8,
6168 None,
6169 edgefirst_tensor::CpuAccess::ReadWrite,
6170 )
6171 .unwrap();
6172 let mut cpu_converter = CPUProcessor::new();
6173
6174 let (result, _src, dst) = convert_img(
6175 &mut cpu_converter,
6176 src,
6177 dst,
6178 Rotation::None,
6179 Flip::None,
6180 Crop::no_crop(),
6181 );
6182 result.unwrap();
6183
6184 let target_image = TensorDyn::image(
6185 1280,
6186 720,
6187 PixelFormat::Rgb,
6188 DType::U8,
6189 None,
6190 edgefirst_tensor::CpuAccess::ReadWrite,
6191 )
6192 .unwrap();
6193 target_image
6194 .as_u8()
6195 .unwrap()
6196 .map()
6197 .unwrap()
6198 .as_mut_slice()
6199 .as_chunks_mut::<3>()
6200 .0
6201 .iter_mut()
6202 .zip(
6203 edgefirst_bench::testdata::read("camera720p.rgba")
6204 .as_chunks::<4>()
6205 .0,
6206 )
6207 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
6208
6209 compare_images(&dst, &target_image, 0.98, function!());
6212 }
6213
6214 #[test]
6215 #[cfg(target_os = "linux")]
6216 fn test_yuyv_to_rgba_g2d() {
6217 if !is_g2d_available() {
6218 eprintln!("SKIPPED: test_yuyv_to_rgba_g2d - G2D library (libg2d.so.2) not available");
6219 return;
6220 }
6221 if !is_dma_available() {
6222 eprintln!(
6223 "SKIPPED: test_yuyv_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
6224 );
6225 return;
6226 }
6227
6228 let src = load_bytes_to_tensor(
6229 1280,
6230 720,
6231 PixelFormat::Yuyv,
6232 None,
6233 &edgefirst_bench::testdata::read("camera720p.yuyv"),
6234 )
6235 .unwrap();
6236
6237 let dst = TensorDyn::image(
6238 1280,
6239 720,
6240 PixelFormat::Rgba,
6241 DType::U8,
6242 Some(TensorMemory::Dma),
6243 edgefirst_tensor::CpuAccess::ReadWrite,
6244 )
6245 .unwrap();
6246 let mut g2d_converter = G2DProcessor::new().unwrap();
6247
6248 let (result, _src, dst) = convert_img(
6249 &mut g2d_converter,
6250 src,
6251 dst,
6252 Rotation::None,
6253 Flip::None,
6254 Crop::no_crop(),
6255 );
6256 result.unwrap();
6257
6258 let target_image = TensorDyn::image(
6259 1280,
6260 720,
6261 PixelFormat::Rgba,
6262 DType::U8,
6263 None,
6264 edgefirst_tensor::CpuAccess::ReadWrite,
6265 )
6266 .unwrap();
6267 target_image
6268 .as_u8()
6269 .unwrap()
6270 .map()
6271 .unwrap()
6272 .as_mut_slice()
6273 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
6274
6275 compare_images(&dst, &target_image, 0.98, function!());
6279 }
6280
6281 #[test]
6282 #[cfg(target_os = "linux")]
6283 #[cfg(feature = "opengl")]
6284 fn test_yuyv_to_rgba_opengl() {
6285 if !is_opengl_available() {
6286 eprintln!("SKIPPED: {} - OpenGL not available", function!());
6287 return;
6288 }
6289 if !is_dma_available() {
6290 eprintln!(
6291 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
6292 function!()
6293 );
6294 return;
6295 }
6296
6297 let src = load_bytes_to_tensor(
6298 1280,
6299 720,
6300 PixelFormat::Yuyv,
6301 Some(TensorMemory::Dma),
6302 &edgefirst_bench::testdata::read("camera720p.yuyv"),
6303 )
6304 .unwrap();
6305
6306 let dst = TensorDyn::image(
6307 1280,
6308 720,
6309 PixelFormat::Rgba,
6310 DType::U8,
6311 Some(TensorMemory::Dma),
6312 edgefirst_tensor::CpuAccess::ReadWrite,
6313 )
6314 .unwrap();
6315 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
6316
6317 let (result, _src, dst) = convert_img(
6318 &mut gl_converter,
6319 src,
6320 dst,
6321 Rotation::None,
6322 Flip::None,
6323 Crop::no_crop(),
6324 );
6325 result.unwrap();
6326
6327 let target_image = TensorDyn::image(
6328 1280,
6329 720,
6330 PixelFormat::Rgba,
6331 DType::U8,
6332 None,
6333 edgefirst_tensor::CpuAccess::ReadWrite,
6334 )
6335 .unwrap();
6336 target_image
6337 .as_u8()
6338 .unwrap()
6339 .map()
6340 .unwrap()
6341 .as_mut_slice()
6342 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
6343
6344 compare_images(&dst, &target_image, 0.98, function!());
6348 }
6349
6350 #[test]
6360 #[cfg(target_os = "macos")]
6361 #[cfg(feature = "opengl")]
6362 fn test_grey_r8_iosurface_to_rgba_opengl_macos() {
6363 let mut proc = match GLProcessorThreaded::new(None) {
6364 Ok(p) => p,
6365 Err(e) => {
6366 eprintln!("SKIPPED: {} — GL engine init failed ({e:?})", function!());
6367 return;
6368 }
6369 };
6370
6371 let (w, h) = (16usize, 16usize);
6372 let src = TensorDyn::image(
6373 w,
6374 h,
6375 PixelFormat::Grey,
6376 DType::U8,
6377 Some(TensorMemory::Dma),
6378 edgefirst_tensor::CpuAccess::ReadWrite,
6379 )
6380 .expect("GREY IOSurface (R8/L008) should allocate — proves the FourCC mapping");
6381 {
6383 let su8 = src.as_u8().unwrap();
6384 let stride = src.as_u8().unwrap().effective_row_stride().unwrap();
6385 let mut m = su8.map().unwrap();
6386 let buf = m.as_mut_slice();
6387 for y in 0..h {
6388 for x in 0..w {
6389 buf[y * stride + x] = ((x * 13 + y * 7) & 0xff) as u8;
6390 }
6391 }
6392 }
6393
6394 let dst = TensorDyn::image(
6395 w,
6396 h,
6397 PixelFormat::Rgba,
6398 DType::U8,
6399 Some(TensorMemory::Dma),
6400 edgefirst_tensor::CpuAccess::ReadWrite,
6401 )
6402 .unwrap();
6403 let (result, src_back, dst) = convert_img(
6404 &mut proc,
6405 src,
6406 dst,
6407 Rotation::None,
6408 Flip::None,
6409 Crop::no_crop(),
6410 );
6411 result.expect("GREY(R8 IOSurface) → RGBA must convert on ANGLE (R8 binding works)");
6412
6413 let src_stride = src_back.as_u8().unwrap().effective_row_stride().unwrap();
6414 let src_map = src_back.as_u8().unwrap().map().unwrap();
6415 let sbytes = src_map.as_slice();
6416 let dst_stride = dst.as_u8().unwrap().effective_row_stride().unwrap();
6417 let dst_map = dst.as_u8().unwrap().map().unwrap();
6418 let dbytes = dst_map.as_slice();
6419 for y in 0..h {
6420 for x in 0..w {
6421 let yv = sbytes[y * src_stride + x] as i16;
6422 let p = y * dst_stride + x * 4;
6423 for c in 0..3 {
6424 assert!(
6425 (dbytes[p + c] as i16 - yv).abs() <= 2,
6426 "pixel ({x},{y}) ch{c} = {} expected ~{yv} (GREY→RGB identity)",
6427 dbytes[p + c]
6428 );
6429 }
6430 }
6431 }
6432 }
6433
6434 #[test]
6440 #[cfg(target_os = "macos")]
6441 #[cfg(feature = "opengl")]
6442 fn test_nv12_to_planar_f16_two_pass_opengl_macos() {
6443 let mut gpu = match GLProcessorThreaded::new(None) {
6444 Ok(p) => p,
6445 Err(e) => {
6446 eprintln!("SKIPPED: {} — init failed ({e:?})", function!());
6447 return;
6448 }
6449 };
6450 let (w, h) = (64usize, 64usize);
6451 let src = match TensorDyn::image(
6452 w,
6453 h,
6454 PixelFormat::Nv12,
6455 DType::U8,
6456 Some(TensorMemory::Dma),
6457 edgefirst_tensor::CpuAccess::ReadWrite,
6458 ) {
6459 Ok(t) => t,
6460 Err(e) => {
6461 eprintln!("SKIPPED: {} — NV12 IOSurface alloc: {e:?}", function!());
6462 return;
6463 }
6464 };
6465 src.as_u8().unwrap().map().unwrap().as_mut_slice().fill(128); let dst = match TensorDyn::image(
6468 w,
6469 h,
6470 PixelFormat::PlanarRgb,
6471 DType::F16,
6472 Some(TensorMemory::Dma),
6473 edgefirst_tensor::CpuAccess::ReadWrite,
6474 ) {
6475 Ok(t) => t,
6476 Err(e) => {
6477 eprintln!("SKIPPED: {} — F16 PlanarRgb IOSurface: {e:?}", function!());
6478 return;
6479 }
6480 };
6481 let mut dst = dst;
6482 if let Err(e) = ImageProcessorTrait::convert(
6484 &mut gpu,
6485 &src,
6486 &mut dst,
6487 Rotation::None,
6488 Flip::None,
6489 Crop::no_crop(),
6490 ) {
6491 eprintln!(
6495 "SKIPPED: {} — NV12→PlanarRgb F16 not available ({e:?})",
6496 function!()
6497 );
6498 return;
6499 }
6500 let dt = dst.as_f16().expect("dst is F16 PlanarRgb");
6501 let map = dt.map().unwrap();
6502 let vals = map.as_slice();
6503 let mut checked = 0usize;
6506 for &v in vals.iter() {
6507 let f = f32::from(v);
6508 assert!(
6509 (0.40..=0.60).contains(&f),
6510 "planar F16 value {f} not ~0.5 for neutral-grey NV12"
6511 );
6512 checked += 1;
6513 }
6514 assert!(
6515 checked >= w * h * 3,
6516 "expected >= 3 planes of samples, got {checked}"
6517 );
6518 }
6519
6520 #[test]
6528 #[cfg(target_os = "macos")]
6529 #[cfg(feature = "opengl")]
6530 fn test_nv12_to_planar_f16_two_pass_pool_opengl_macos() {
6531 let mut gpu = match GLProcessorThreaded::new(None) {
6532 Ok(p) => p,
6533 Err(e) => {
6534 eprintln!("SKIPPED: {} — init failed ({e:?})", function!());
6535 return;
6536 }
6537 };
6538 let (fw, fh) = (96usize, 64usize);
6540 let (pool_w, pool_h) = (256usize, 768usize);
6541 let (model_w, model_h) = (128usize, 128usize);
6542
6543 let mut src = match TensorDyn::image(
6544 pool_w,
6545 pool_h,
6546 PixelFormat::Grey,
6547 DType::U8,
6548 Some(TensorMemory::Dma),
6549 edgefirst_tensor::CpuAccess::ReadWrite,
6550 ) {
6551 Ok(t) => t,
6552 Err(e) => {
6553 eprintln!("SKIPPED: {} — R8 pool alloc: {e:?}", function!());
6554 return;
6555 }
6556 };
6557 src.configure_image(fw, fh, PixelFormat::Nv12)
6558 .unwrap_or_else(|e| panic!("configure_image NV12 on pool: {e}"));
6559 let stride = src.as_u8().unwrap().effective_row_stride().unwrap();
6560 src.as_u8().unwrap().map().unwrap().as_mut_slice().fill(128); let mut dst = match TensorDyn::image(
6563 model_w,
6564 model_h,
6565 PixelFormat::PlanarRgb,
6566 DType::F16,
6567 Some(TensorMemory::Dma),
6568 edgefirst_tensor::CpuAccess::ReadWrite,
6569 ) {
6570 Ok(t) => t,
6571 Err(e) => {
6572 eprintln!("SKIPPED: {} — F16 PlanarRgb dst: {e:?}", function!());
6573 return;
6574 }
6575 };
6576
6577 let _ = model_w;
6579 let crop = Crop::new()
6580 .with_source(Some(Region::new(0, 0, fw, fh)))
6581 .with_fit(Fit::Letterbox {
6582 pad: [0, 0, 0, 255],
6583 });
6584 if let Err(e) =
6585 ImageProcessorTrait::convert(&mut gpu, &src, &mut dst, Rotation::None, Flip::None, crop)
6586 {
6587 eprintln!(
6588 "SKIPPED: {} — NV12→PlanarRgb F16 unavailable ({e:?})",
6589 function!()
6590 );
6591 return;
6592 }
6593 let _ = stride;
6594 let dt = dst.as_f16().expect("dst F16");
6597 let map = dt.map().unwrap();
6598 let any_half = map.as_slice().iter().any(|&v| {
6599 let f = f32::from(v);
6600 (0.40..=0.60).contains(&f)
6601 });
6602 assert!(any_half, "expected ~0.5 grey samples in the letterbox band");
6603 }
6604
6605 #[test]
6611 #[cfg(target_os = "macos")]
6612 #[cfg(feature = "opengl")]
6613 fn test_nv12_to_planar_f16_cross_thread_opengl_macos() {
6614 use std::sync::mpsc;
6615 let mut proc = match ImageProcessor::new() {
6618 Ok(p) => p,
6619 Err(e) => {
6620 eprintln!("SKIPPED: {} — init failed ({e:?})", function!());
6621 return;
6622 }
6623 };
6624 let (fw, fh) = (96usize, 64usize);
6625 let mut src = match TensorDyn::image(
6626 256,
6627 768,
6628 PixelFormat::Grey,
6629 DType::U8,
6630 Some(TensorMemory::Dma),
6631 edgefirst_tensor::CpuAccess::ReadWrite,
6632 ) {
6633 Ok(t) => t,
6634 Err(e) => {
6635 eprintln!("SKIPPED: {} — pool: {e:?}", function!());
6636 return;
6637 }
6638 };
6639 src.configure_image(fw, fh, PixelFormat::Nv12).unwrap();
6640 src.as_u8().unwrap().map().unwrap().as_mut_slice().fill(128);
6641 let mut dst = match TensorDyn::image(
6642 128,
6643 128,
6644 PixelFormat::PlanarRgb,
6645 DType::F16,
6646 Some(TensorMemory::Dma),
6647 edgefirst_tensor::CpuAccess::ReadWrite,
6648 ) {
6649 Ok(t) => t,
6650 Err(e) => {
6651 eprintln!("SKIPPED: {} — dst: {e:?}", function!());
6652 return;
6653 }
6654 };
6655 let crop = Crop::new().with_source(Some(Region::new(0, 0, fw, fh)));
6656
6657 let (tx, rx) = mpsc::channel::<bool>();
6660 let worker = std::thread::spawn(move || {
6661 let _ = ImageProcessorTrait::convert(
6662 &mut proc,
6663 &src,
6664 &mut dst,
6665 Rotation::None,
6666 Flip::None,
6667 crop,
6668 );
6669 let _ = tx.send(true);
6670 });
6671 match rx.recv_timeout(std::time::Duration::from_secs(20)) {
6672 Ok(_) => { let _ = worker.join(); }
6673 Err(_) => panic!(
6674 "cross-thread NV12→PlanarRgb convert HUNG (>20s) — reproduces the orchestrator deadlock"
6675 ),
6676 }
6677 }
6678
6679 #[test]
6686 #[cfg(target_os = "macos")]
6687 #[cfg(feature = "opengl")]
6688 fn test_nv_to_planar_f16_varying_sizes_no_leak_opengl_macos() {
6689 let mut gpu = match GLProcessorThreaded::new(None) {
6690 Ok(p) => p,
6691 Err(e) => {
6692 eprintln!("SKIPPED: {} — init failed ({e:?})", function!());
6693 return;
6694 }
6695 };
6696 let (max_w, max_h) = (640usize, 640usize);
6699 let depth = 4usize;
6700 let mut srcs = Vec::new();
6701 let mut dsts = Vec::new();
6702 for _ in 0..depth {
6703 srcs.push(
6704 match TensorDyn::image(
6705 max_w,
6706 max_h * 3,
6707 PixelFormat::Grey,
6708 DType::U8,
6709 Some(TensorMemory::Dma),
6710 edgefirst_tensor::CpuAccess::ReadWrite,
6711 ) {
6712 Ok(t) => t,
6713 Err(e) => {
6714 eprintln!("SKIPPED: {} — pool: {e:?}", function!());
6715 return;
6716 }
6717 },
6718 );
6719 dsts.push(
6720 match TensorDyn::image(
6721 640,
6722 640,
6723 PixelFormat::PlanarRgb,
6724 DType::F16,
6725 Some(TensorMemory::Dma),
6726 edgefirst_tensor::CpuAccess::ReadWrite,
6727 ) {
6728 Ok(t) => t,
6729 Err(e) => {
6730 eprintln!("SKIPPED: {} — dst: {e:?}", function!());
6731 return;
6732 }
6733 },
6734 );
6735 }
6736 let sizes = [
6738 (640, 480),
6739 (500, 375),
6740 (640, 427),
6741 (333, 500),
6742 (480, 640),
6743 (612, 612),
6744 (428, 640),
6745 (576, 432),
6746 ];
6747 let mut first_ms = 0f64;
6748 let mut last_ms = 0f64;
6749 let iters = 40usize;
6750 for i in 0..iters {
6751 let (fw, fh) = sizes[i % sizes.len()];
6752 let src = &mut srcs[i % depth];
6753 let dst = &mut dsts[i % depth];
6754 src.configure_image(fw, fh, PixelFormat::Nv24).unwrap();
6755 src.as_u8().unwrap().map().unwrap().as_mut_slice().fill(128);
6756 let crop = Crop::new().with_source(Some(Region::new(0, 0, fw, fh)));
6757 let t0 = std::time::Instant::now();
6758 ImageProcessorTrait::convert(&mut gpu, src, dst, Rotation::None, Flip::None, crop)
6759 .unwrap_or_else(|e| panic!("convert iter {i} ({fw}×{fh}): {e}"));
6760 let ms = t0.elapsed().as_secs_f64() * 1e3;
6761 if i == 2 {
6762 first_ms = ms;
6763 }
6764 if i == iters - 1 {
6765 last_ms = ms;
6766 }
6767 }
6768 eprintln!("first={first_ms:.2}ms last={last_ms:.2}ms");
6769 assert!(
6770 last_ms < first_ms * 5.0 + 5.0,
6771 "convert latency ran away: first {first_ms:.2}ms → last {last_ms:.2}ms (intermediate/pbuffer leak)"
6772 );
6773 }
6774
6775 #[test]
6782 #[cfg(target_os = "macos")]
6783 #[cfg(feature = "opengl")]
6784 fn test_nv12_nv16_nv24_to_rgba_opengl_macos() {
6785 let mut gpu = match GLProcessorThreaded::new(None) {
6786 Ok(p) => p,
6787 Err(e) => {
6788 eprintln!("SKIPPED: {} — GL engine init failed ({e:?})", function!());
6789 return;
6790 }
6791 };
6792 let mut cpu = CPUProcessor::new();
6793
6794 let fill = |buf: &mut [u8], stride: usize, fmt: PixelFormat, w: usize, h: usize| {
6806 for y in 0..h {
6807 for x in 0..w {
6808 buf[y * stride + x] = ((x * 9 + y * 5) & 0xff) as u8;
6809 }
6810 }
6811 let (cw, ch, uv_grid_rows) = match fmt {
6812 PixelFormat::Nv12 => (w / 2, h / 2, 1usize),
6813 PixelFormat::Nv16 => (w / 2, h, 1usize),
6814 _ => (w, h, 2usize), };
6816 let uv_plane = h * stride;
6817 for cy in 0..ch {
6818 for cx in 0..cw {
6819 let off = uv_plane + cy * uv_grid_rows * stride + cx * 2;
6820 buf[off] = ((cx * 11 + 30) & 0xff) as u8;
6821 buf[off + 1] = ((cy * 7 + 200) & 0xff) as u8;
6822 }
6823 }
6824 };
6825
6826 for fmt in [PixelFormat::Nv12, PixelFormat::Nv16, PixelFormat::Nv24] {
6827 for (w, h) in [
6828 (16usize, 16usize), (15, 16), (16, 15), ] {
6832 let mem = TensorDyn::image(
6833 w,
6834 h,
6835 fmt,
6836 DType::U8,
6837 None,
6838 edgefirst_tensor::CpuAccess::ReadWrite,
6839 )
6840 .unwrap();
6841 let mem_stride = mem.as_u8().unwrap().effective_row_stride().unwrap();
6842 fill(
6843 mem.as_u8().unwrap().map().unwrap().as_mut_slice(),
6844 mem_stride,
6845 fmt,
6846 w,
6847 h,
6848 );
6849 let cpu_dst = TensorDyn::image(
6850 w,
6851 h,
6852 PixelFormat::Rgba,
6853 DType::U8,
6854 None,
6855 edgefirst_tensor::CpuAccess::ReadWrite,
6856 )
6857 .unwrap();
6858 let (r, _s, cpu_dst) = convert_img(
6859 &mut cpu,
6860 mem,
6861 cpu_dst,
6862 Rotation::None,
6863 Flip::None,
6864 Crop::no_crop(),
6865 );
6866 r.unwrap_or_else(|e| panic!("CPU {fmt:?}->{w}x{h}->RGBA: {e}"));
6867
6868 let ios = TensorDyn::image(
6869 w,
6870 h,
6871 fmt,
6872 DType::U8,
6873 Some(TensorMemory::Dma),
6874 edgefirst_tensor::CpuAccess::ReadWrite,
6875 )
6876 .unwrap_or_else(|e| panic!("{fmt:?} {w}x{h} IOSurface alloc: {e}"));
6877 let ios_stride = ios.as_u8().unwrap().effective_row_stride().unwrap();
6878 fill(
6879 ios.as_u8().unwrap().map().unwrap().as_mut_slice(),
6880 ios_stride,
6881 fmt,
6882 w,
6883 h,
6884 );
6885 let gpu_dst = TensorDyn::image(
6886 w,
6887 h,
6888 PixelFormat::Rgba,
6889 DType::U8,
6890 Some(TensorMemory::Dma),
6891 edgefirst_tensor::CpuAccess::ReadWrite,
6892 )
6893 .unwrap();
6894 let (r, _s, gpu_dst) = convert_img(
6895 &mut gpu,
6896 ios,
6897 gpu_dst,
6898 Rotation::None,
6899 Flip::None,
6900 Crop::no_crop(),
6901 );
6902 r.unwrap_or_else(|e| panic!("GPU {fmt:?}->{w}x{h}->RGBA on ANGLE: {e}"));
6903
6904 let cs = cpu_dst.as_u8().unwrap().effective_row_stride().unwrap();
6905 let cmap = cpu_dst.as_u8().unwrap().map().unwrap();
6906 let cb = cmap.as_slice();
6907 let gs = gpu_dst.as_u8().unwrap().effective_row_stride().unwrap();
6908 let gmap = gpu_dst.as_u8().unwrap().map().unwrap();
6909 let gb = gmap.as_slice();
6910 let mut max_d = 0i16;
6911 for y in 0..h {
6912 for x in 0..w {
6913 for c in 0..3 {
6914 let cv = cb[y * cs + x * 4 + c] as i16;
6915 let gv = gb[y * gs + x * 4 + c] as i16;
6916 max_d = max_d.max((cv - gv).abs());
6917 }
6918 }
6919 }
6920 assert!(
6921 max_d <= 3,
6922 "{fmt:?} {w}x{h}: GPU vs CPU RGBA max channel diff {max_d} > 3"
6923 );
6924 }
6925 }
6926 }
6927
6928 #[test]
6936 #[cfg(target_os = "macos")]
6937 #[cfg(feature = "opengl")]
6938 fn test_nv_to_rgba_larger_pool_surface_opengl_macos() {
6939 let mut gpu = match GLProcessorThreaded::new(None) {
6940 Ok(p) => p,
6941 Err(e) => {
6942 eprintln!("SKIPPED: {} — GL engine init failed ({e:?})", function!());
6943 return;
6944 }
6945 };
6946 let mut cpu = CPUProcessor::new();
6947 let (pool_w, pool_h) = (256usize, 256usize);
6950
6951 let fill = |buf: &mut [u8], stride: usize, fmt: PixelFormat, w: usize, h: usize| {
6955 for y in 0..h {
6956 for x in 0..w {
6957 buf[y * stride + x] = ((x * 9 + y * 5) & 0xff) as u8;
6958 }
6959 }
6960 let (cw, ch, uv_grid_rows) = match fmt {
6961 PixelFormat::Nv12 => (w / 2, h / 2, 1usize),
6962 PixelFormat::Nv16 => (w / 2, h, 1usize),
6963 _ => (w, h, 2usize), };
6965 let uv_plane = h * stride;
6966 for cy in 0..ch {
6967 for cx in 0..cw {
6968 let off = uv_plane + cy * uv_grid_rows * stride + cx * 2;
6969 buf[off] = ((cx * 11 + 30) & 0xff) as u8;
6970 buf[off + 1] = ((cy * 7 + 200) & 0xff) as u8;
6971 }
6972 }
6973 };
6974
6975 for fmt in [PixelFormat::Nv12, PixelFormat::Nv16, PixelFormat::Nv24] {
6976 for (w, h) in [
6977 (40usize, 24usize), (15, 16), (16, 15), ] {
6981 let ew = w.next_multiple_of(2);
6984
6985 let mem = TensorDyn::image(
6987 w,
6988 h,
6989 fmt,
6990 DType::U8,
6991 None,
6992 edgefirst_tensor::CpuAccess::ReadWrite,
6993 )
6994 .unwrap();
6995 let mem_stride = mem.as_u8().unwrap().effective_row_stride().unwrap();
6996 fill(
6997 mem.as_u8().unwrap().map().unwrap().as_mut_slice(),
6998 mem_stride,
6999 fmt,
7000 w,
7001 h,
7002 );
7003 let cpu_dst = TensorDyn::image(
7004 w,
7005 h,
7006 PixelFormat::Rgba,
7007 DType::U8,
7008 None,
7009 edgefirst_tensor::CpuAccess::ReadWrite,
7010 )
7011 .unwrap();
7012 let (r, _s, cpu_dst) = convert_img(
7013 &mut cpu,
7014 mem,
7015 cpu_dst,
7016 Rotation::None,
7017 Flip::None,
7018 Crop::no_crop(),
7019 );
7020 r.unwrap_or_else(|e| panic!("CPU {fmt:?}->{w}x{h}->RGBA: {e}"));
7021
7022 let mut ios = match TensorDyn::image(
7026 pool_w,
7027 pool_h,
7028 PixelFormat::Grey,
7029 DType::U8,
7030 Some(TensorMemory::Dma),
7031 edgefirst_tensor::CpuAccess::ReadWrite,
7032 ) {
7033 Ok(t) => t,
7034 Err(e) => {
7035 eprintln!("SKIPPED: {} — R8 pool IOSurface alloc: {e:?}", function!());
7036 return;
7037 }
7038 };
7039 ios.configure_image(w, h, fmt)
7040 .unwrap_or_else(|e| panic!("configure_image {fmt:?} {w}x{h} on pool: {e}"));
7041 let ios_stride = ios.as_u8().unwrap().effective_row_stride().unwrap();
7042 assert!(
7043 ios_stride > ew,
7044 "{fmt:?} {w}x{h}: pool stride {ios_stride} should exceed even width {ew} \
7045 (test must exercise padding)"
7046 );
7047 fill(
7048 ios.as_u8().unwrap().map().unwrap().as_mut_slice(),
7049 ios_stride,
7050 fmt,
7051 w,
7052 h,
7053 );
7054
7055 let gpu_dst = TensorDyn::image(
7056 w,
7057 h,
7058 PixelFormat::Rgba,
7059 DType::U8,
7060 Some(TensorMemory::Dma),
7061 edgefirst_tensor::CpuAccess::ReadWrite,
7062 )
7063 .unwrap();
7064 let (r, _s, gpu_dst) = convert_img(
7065 &mut gpu,
7066 ios,
7067 gpu_dst,
7068 Rotation::None,
7069 Flip::None,
7070 Crop::no_crop(),
7071 );
7072 r.unwrap_or_else(|e| {
7073 panic!("GPU {fmt:?}->{w}x{h}->RGBA (pool surface) on ANGLE: {e}")
7074 });
7075
7076 let cs = cpu_dst.as_u8().unwrap().effective_row_stride().unwrap();
7077 let cmap = cpu_dst.as_u8().unwrap().map().unwrap();
7078 let cb = cmap.as_slice();
7079 let gs = gpu_dst.as_u8().unwrap().effective_row_stride().unwrap();
7080 let gmap = gpu_dst.as_u8().unwrap().map().unwrap();
7081 let gb = gmap.as_slice();
7082 let mut max_d = 0i16;
7083 for y in 0..h {
7084 for x in 0..w {
7085 for c in 0..3 {
7086 let cv = cb[y * cs + x * 4 + c] as i16;
7087 let gv = gb[y * gs + x * 4 + c] as i16;
7088 max_d = max_d.max((cv - gv).abs());
7089 }
7090 }
7091 }
7092 assert!(
7093 max_d <= 3,
7094 "{fmt:?} {w}x{h}: GPU(pool surface) vs CPU RGBA max channel diff {max_d} > 3"
7095 );
7096 }
7097 }
7098 }
7099
7100 #[test]
7101 #[cfg(target_os = "macos")]
7102 #[cfg(feature = "opengl")]
7103 fn test_yuyv_to_rgba_opengl_macos() {
7104 let mut proc = match GLProcessorThreaded::new(None) {
7105 Ok(p) => p,
7106 Err(e) => {
7107 eprintln!(
7108 "SKIPPED: {} — GL engine init failed ({e:?}). \
7109 Install ANGLE via `brew install startergo/angle/angle` \
7110 and re-sign per README.md § macOS GPU Acceleration to \
7111 run this test.",
7112 function!()
7113 );
7114 return;
7115 }
7116 };
7117
7118 let src = load_bytes_to_tensor(
7119 1280,
7120 720,
7121 PixelFormat::Yuyv,
7122 Some(TensorMemory::Dma),
7123 &edgefirst_bench::testdata::read("camera720p.yuyv"),
7124 )
7125 .unwrap();
7126
7127 let dst = TensorDyn::image(
7128 1280,
7129 720,
7130 PixelFormat::Rgba,
7131 DType::U8,
7132 Some(TensorMemory::Dma),
7133 edgefirst_tensor::CpuAccess::ReadWrite,
7134 )
7135 .unwrap();
7136
7137 let (result, _src, dst) = convert_img(
7138 &mut proc,
7139 src,
7140 dst,
7141 Rotation::None,
7142 Flip::None,
7143 Crop::no_crop(),
7144 );
7145 result.unwrap();
7146
7147 let target_image = TensorDyn::image(
7148 1280,
7149 720,
7150 PixelFormat::Rgba,
7151 DType::U8,
7152 None,
7153 edgefirst_tensor::CpuAccess::ReadWrite,
7154 )
7155 .unwrap();
7156 target_image
7157 .as_u8()
7158 .unwrap()
7159 .map()
7160 .unwrap()
7161 .as_mut_slice()
7162 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
7163
7164 compare_images(&dst, &target_image, 0.98, function!());
7169 }
7170
7171 #[test]
7190 #[cfg(target_os = "macos")]
7191 #[cfg(feature = "opengl")]
7192 fn test_yuyv_to_rgba_opengl_macos_multi_resolution() {
7193 let mut proc = match GLProcessorThreaded::new(None) {
7194 Ok(p) => p,
7195 Err(e) => {
7196 eprintln!("SKIPPED: {} — GL engine init failed ({e:?})", function!());
7197 return;
7198 }
7199 };
7200
7201 for (w, h) in [(64usize, 32usize), (3840, 2160)] {
7202 let bytes_per_row = w * 2;
7205 let mut yuyv = vec![0u8; bytes_per_row * h];
7206 for chunk in yuyv.chunks_exact_mut(4) {
7207 chunk[0] = 128; chunk[1] = 128; chunk[2] = 128; chunk[3] = 128; }
7212
7213 let src = load_bytes_to_tensor(w, h, PixelFormat::Yuyv, Some(TensorMemory::Dma), &yuyv)
7214 .unwrap();
7215
7216 let dst = TensorDyn::image(
7217 w,
7218 h,
7219 PixelFormat::Rgba,
7220 DType::U8,
7221 Some(TensorMemory::Dma),
7222 edgefirst_tensor::CpuAccess::ReadWrite,
7223 )
7224 .unwrap();
7225
7226 let (result, _src, dst) = convert_img(
7227 &mut proc,
7228 src,
7229 dst,
7230 Rotation::None,
7231 Flip::None,
7232 Crop::no_crop(),
7233 );
7234 result.expect("GL convert should succeed at this resolution");
7235
7236 let dst_u8 = dst.as_u8().unwrap();
7241 let dst_map = dst_u8.map().unwrap();
7242 let dst_bytes = dst_map.as_slice();
7243 assert_eq!(dst_bytes.len(), w * h * 4, "RGBA byte count");
7244 for px in dst_bytes.chunks_exact(4) {
7245 for (i, &c) in px[..3].iter().enumerate() {
7246 assert!(
7247 (120..=140).contains(&c),
7248 "{}: channel {i} = {c} (expected ~128 ±12) at {w}×{h}",
7249 function!(),
7250 );
7251 }
7252 assert_eq!(px[3], 255, "alpha must be 1.0");
7253 }
7254 }
7255 }
7256
7257 #[test]
7267 #[cfg(target_os = "macos")]
7268 #[cfg(feature = "opengl")]
7269 fn test_macos_gl_pbuffer_cache_reuses_surfaces() {
7270 let mut proc = match GLProcessorThreaded::new(None) {
7271 Ok(p) => p,
7272 Err(e) => {
7273 eprintln!("SKIPPED: {} — GL engine init failed ({e:?})", function!());
7274 return;
7275 }
7276 };
7277
7278 let mut yuyv = vec![0u8; 64 * 32 * 2];
7280 for chunk in yuyv.chunks_exact_mut(4) {
7281 chunk[0] = 200;
7282 chunk[1] = 100;
7283 chunk[2] = 200;
7284 chunk[3] = 156;
7285 }
7286 let src = load_bytes_to_tensor(64, 32, PixelFormat::Yuyv, Some(TensorMemory::Dma), &yuyv)
7287 .unwrap();
7288 let dst = TensorDyn::image(
7289 64,
7290 32,
7291 PixelFormat::Rgba,
7292 DType::U8,
7293 Some(TensorMemory::Dma),
7294 edgefirst_tensor::CpuAccess::ReadWrite,
7295 )
7296 .unwrap();
7297
7298 let (r1, src, dst) = convert_img(
7299 &mut proc,
7300 src,
7301 dst,
7302 Rotation::None,
7303 Flip::None,
7304 Crop::no_crop(),
7305 );
7306 r1.unwrap();
7307 let first: Vec<u8> = dst.as_u8().unwrap().map().unwrap().as_slice().to_vec();
7308
7309 let (r2, _src, dst) = convert_img(
7310 &mut proc,
7311 src,
7312 dst,
7313 Rotation::None,
7314 Flip::None,
7315 Crop::no_crop(),
7316 );
7317 r2.unwrap();
7318 let second: Vec<u8> = dst.as_u8().unwrap().map().unwrap().as_slice().to_vec();
7319
7320 assert_eq!(first, second, "cache-hit conversion must be deterministic");
7321 }
7322
7323 #[test]
7331 #[cfg(target_os = "macos")]
7332 #[cfg(feature = "opengl")]
7333 fn test_macos_gl_pbuffer_cache_steady_state() {
7334 let mut proc = match GLProcessorThreaded::new(None) {
7335 Ok(p) => p,
7336 Err(e) => {
7337 eprintln!("SKIPPED: {} — GL engine init failed ({e:?})", function!());
7338 return;
7339 }
7340 };
7341
7342 let (w, h) = (64usize, 32usize);
7343 const POOL: usize = 3;
7344 const FRAMES: usize = 100;
7345
7346 let yuyv = vec![128u8; w * h * 2];
7347 let pool: Vec<TensorDyn> = (0..POOL)
7348 .map(|_| {
7349 load_bytes_to_tensor(w, h, PixelFormat::Yuyv, Some(TensorMemory::Dma), &yuyv)
7350 .unwrap()
7351 })
7352 .collect();
7353 let mut dst = TensorDyn::image(
7354 w,
7355 h,
7356 PixelFormat::Rgba,
7357 DType::U8,
7358 Some(TensorMemory::Dma),
7359 edgefirst_tensor::CpuAccess::ReadWrite,
7360 )
7361 .unwrap();
7362
7363 for src in pool.iter().cycle().take(POOL * 2) {
7365 proc.convert(src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
7366 .unwrap();
7367 }
7368 let warm = proc.egl_cache_stats().unwrap();
7369
7370 for src in pool.iter().cycle().take(FRAMES) {
7371 proc.convert(src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
7372 .unwrap();
7373 }
7374 let steady = proc.egl_cache_stats().unwrap();
7375
7376 assert_eq!(
7377 warm.total_misses(),
7378 steady.total_misses(),
7379 "steady-state loop created new imports (warm {warm:?}, steady {steady:?})"
7380 );
7381 let hits = |s: &GlCacheStats| s.src.hits + s.dst.hits + s.nv_r8.hits;
7382 assert!(
7383 hits(&steady) - hits(&warm) >= FRAMES as u64,
7384 "expected at least {FRAMES} import-cache hits over the loop, got {}",
7385 hits(&steady) - hits(&warm)
7386 );
7387 }
7388
7389 #[test]
7401 #[cfg(target_os = "macos")]
7402 #[cfg(feature = "opengl")]
7403 fn test_macos_gl_f16_planar_is_gl_backed() {
7404 let mut proc = ImageProcessor::new().expect("ImageProcessor");
7405 let Some(ref gl) = proc.opengl else {
7406 eprintln!("SKIPPED: {} — GL backend unavailable", function!());
7407 return;
7408 };
7409 if !gl.supported_render_dtypes().f16 {
7410 eprintln!(
7411 "SKIPPED: {} — configuration lacks F16 color-buffer support",
7412 function!()
7413 );
7414 return;
7415 }
7416 let stats_before = gl.egl_cache_stats().expect("cache stats");
7417
7418 let src = TensorDyn::image(
7419 1280,
7420 720,
7421 PixelFormat::Nv12,
7422 DType::U8,
7423 Some(TensorMemory::Dma),
7424 edgefirst_tensor::CpuAccess::ReadWrite,
7425 )
7426 .unwrap();
7427 {
7428 let t = src.as_u8().unwrap();
7429 let mut m = t.map().unwrap();
7430 for (i, b) in m.as_mut_slice().iter_mut().enumerate() {
7431 *b = ((i * 31) % 211) as u8;
7432 }
7433 }
7434 let mut dst = TensorDyn::image(
7435 640,
7436 640,
7437 PixelFormat::PlanarRgb,
7438 DType::F16,
7439 Some(TensorMemory::Dma),
7440 edgefirst_tensor::CpuAccess::ReadWrite,
7441 )
7442 .unwrap();
7443
7444 proc.convert(
7445 &src,
7446 &mut dst,
7447 Rotation::None,
7448 Flip::None,
7449 Crop::letterbox([114, 114, 114, 255]),
7450 )
7451 .expect("F16 capability reported but the NV12→PlanarF16 convert failed");
7452 let stats_after = proc
7453 .opengl
7454 .as_ref()
7455 .expect("GL backend present")
7456 .egl_cache_stats()
7457 .expect("cache stats");
7458 assert!(
7463 stats_after.total_misses() >= stats_before.total_misses() + 2,
7464 "convert succeeded but the GL engine did not import both the \
7465 source and the F16 destination — the work did not (fully) run \
7466 on the GL backend (silent CPU fallback); misses before={} after={}",
7467 stats_before.total_misses(),
7468 stats_after.total_misses()
7469 );
7470 }
7471
7472 #[test]
7478 #[cfg(feature = "opengl")]
7479 fn test_nv12_to_planar_f16_fused_engine_vs_cpu() {
7480 let mut gl = match ImageProcessor::with_config(ImageProcessorConfig {
7481 backend: ComputeBackend::OpenGl,
7482 ..Default::default()
7483 }) {
7484 Ok(p) if p.opengl.is_some() => p,
7485 _ => {
7486 eprintln!("SKIPPED: {} — GL backend unavailable", function!());
7487 return;
7488 }
7489 };
7490 if !gl
7491 .opengl
7492 .as_ref()
7493 .map(|g| g.supported_render_dtypes().f16)
7494 .unwrap_or(false)
7495 {
7496 eprintln!("SKIPPED: {} — no F16 render support", function!());
7497 return;
7498 }
7499 let mem = if edgefirst_tensor::is_gpu_buffer_available() {
7500 TensorMemory::Dma
7501 } else {
7502 eprintln!("SKIPPED: {} — no zero-copy buffers", function!());
7503 return;
7504 };
7505
7506 let src = TensorDyn::image(
7507 1280,
7508 720,
7509 PixelFormat::Nv12,
7510 DType::U8,
7511 Some(mem),
7512 edgefirst_tensor::CpuAccess::ReadWrite,
7513 )
7514 .unwrap();
7515 {
7516 let t = src.as_u8().unwrap();
7522 let mut m = t.map().unwrap();
7523 let buf = m.as_mut_slice();
7524 let (w, h) = (1280usize, 720usize);
7525 for y in 0..h {
7526 for x in 0..w {
7527 buf[y * w + x] = ((x * 255) / w) as u8; }
7529 }
7530 for y in 0..(h / 2) {
7531 for x in 0..(w / 2) {
7532 let o = h * w + y * w + 2 * x;
7533 buf[o] = ((y * 255) / (h / 2)) as u8; buf[o + 1] = (((x + y) * 255) / (w / 2 + h / 2)) as u8; }
7536 }
7537 }
7538 let crop = Crop::letterbox([114, 114, 114, 255]);
7539 let mut gl_dst = TensorDyn::image(
7540 640,
7541 640,
7542 PixelFormat::PlanarRgb,
7543 DType::F16,
7544 Some(mem),
7545 edgefirst_tensor::CpuAccess::ReadWrite,
7546 )
7547 .unwrap();
7548 gl.opengl
7552 .as_mut()
7553 .expect("GL backend present")
7554 .convert(&src, &mut gl_dst, Rotation::None, Flip::None, crop)
7555 .expect("fused NV12→PlanarF16 GL convert");
7556
7557 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
7558 backend: ComputeBackend::Cpu,
7559 ..Default::default()
7560 })
7561 .unwrap();
7562 let mut cpu_dst = TensorDyn::image(
7563 640,
7564 640,
7565 PixelFormat::PlanarRgb,
7566 DType::F16,
7567 Some(TensorMemory::Mem),
7568 edgefirst_tensor::CpuAccess::ReadWrite,
7569 )
7570 .unwrap();
7571 cpu.convert(&src, &mut cpu_dst, Rotation::None, Flip::None, crop)
7572 .expect("CPU reference convert");
7573
7574 let g = gl_dst.as_f16().unwrap().map().unwrap().as_slice().to_vec();
7575 let c = cpu_dst.as_f16().unwrap().map().unwrap().as_slice().to_vec();
7576 assert_eq!(g.len(), c.len());
7577 let mut max_diff = 0.0f32;
7578 let mut max_at = 0usize;
7579 for (i, (a, b)) in g.iter().zip(c.iter()).enumerate() {
7580 let d = (a.to_f32() - b.to_f32()).abs();
7581 if d > max_diff {
7582 max_diff = d;
7583 max_at = i;
7584 }
7585 }
7586 let (plane, rem) = (max_at / (640 * 640), max_at % (640 * 640));
7588 let (row, col) = (rem / 640, rem % 640);
7589 eprintln!(
7590 "fused-vs-cpu: max_diff={max_diff} at plane={plane} row={row} col={col} \
7591 gl={} cpu={}",
7592 g[max_at].to_f32(),
7593 c[max_at].to_f32()
7594 );
7595 assert!(
7598 max_diff <= 4.0 / 255.0 + 1e-3,
7599 "fused NV12→PlanarF16 diverges from CPU reference: max_diff={max_diff}"
7600 );
7601 }
7602
7603 #[test]
7613 #[cfg(feature = "opengl")]
7614 fn test_zero_copy_src_to_mem_dst_gl_direct() {
7615 let mut proc = match ImageProcessor::new() {
7616 Ok(p) if p.opengl.is_some() => p,
7617 _ => {
7618 eprintln!("SKIPPED: {} — GL backend unavailable", function!());
7619 return;
7620 }
7621 };
7622 if !edgefirst_tensor::is_gpu_buffer_available() {
7623 eprintln!("SKIPPED: {} — no zero-copy buffers", function!());
7624 return;
7625 }
7626
7627 let src = TensorDyn::image(
7628 1280,
7629 720,
7630 PixelFormat::Rgba,
7631 DType::U8,
7632 Some(TensorMemory::Dma),
7633 edgefirst_tensor::CpuAccess::ReadWrite,
7634 )
7635 .unwrap();
7636 {
7637 let t = src.as_u8().unwrap();
7638 let mut m = t.map().unwrap();
7639 for (i, b) in m.as_mut_slice().iter_mut().enumerate() {
7640 *b = ((i * 31) % 211) as u8;
7641 }
7642 }
7643 let mut gl_dst = TensorDyn::image(
7644 1280,
7645 720,
7646 PixelFormat::Bgra,
7647 DType::U8,
7648 Some(TensorMemory::Mem),
7649 edgefirst_tensor::CpuAccess::ReadWrite,
7650 )
7651 .unwrap();
7652 proc.opengl
7653 .as_mut()
7654 .expect("GL backend present")
7655 .convert(
7656 &src,
7657 &mut gl_dst,
7658 Rotation::None,
7659 Flip::None,
7660 Crop::no_crop(),
7661 )
7662 .expect("zero-copy src → heap dst GL convert");
7663
7664 let mut cpu = ImageProcessor::with_config(ImageProcessorConfig {
7665 backend: ComputeBackend::Cpu,
7666 ..Default::default()
7667 })
7668 .unwrap();
7669 let mut cpu_dst = TensorDyn::image(
7670 1280,
7671 720,
7672 PixelFormat::Bgra,
7673 DType::U8,
7674 Some(TensorMemory::Mem),
7675 edgefirst_tensor::CpuAccess::ReadWrite,
7676 )
7677 .unwrap();
7678 cpu.convert(
7679 &src,
7680 &mut cpu_dst,
7681 Rotation::None,
7682 Flip::None,
7683 Crop::no_crop(),
7684 )
7685 .expect("CPU reference convert");
7686
7687 let g = gl_dst.as_u8().unwrap().map().unwrap().as_slice().to_vec();
7688 let c = cpu_dst.as_u8().unwrap().map().unwrap().as_slice().to_vec();
7689 assert_eq!(g.len(), c.len());
7690 let max_diff = g
7691 .iter()
7692 .zip(c.iter())
7693 .map(|(a, b)| a.abs_diff(*b))
7694 .max()
7695 .unwrap();
7696 assert!(
7699 max_diff <= 2,
7700 "zero-copy src → heap dst diverges from CPU reference: max_diff={max_diff}"
7701 );
7702 }
7703
7704 #[test]
7705 #[cfg(target_os = "linux")]
7706 fn test_yuyv_to_rgb_g2d() {
7707 if !is_g2d_available() {
7708 eprintln!("SKIPPED: test_yuyv_to_rgb_g2d - G2D library (libg2d.so.2) not available");
7709 return;
7710 }
7711 if !is_dma_available() {
7712 eprintln!(
7713 "SKIPPED: test_yuyv_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
7714 );
7715 return;
7716 }
7717
7718 let src = load_bytes_to_tensor(
7719 1280,
7720 720,
7721 PixelFormat::Yuyv,
7722 None,
7723 &edgefirst_bench::testdata::read("camera720p.yuyv"),
7724 )
7725 .unwrap();
7726
7727 let g2d_dst = TensorDyn::image(
7728 1280,
7729 720,
7730 PixelFormat::Rgb,
7731 DType::U8,
7732 Some(TensorMemory::Dma),
7733 edgefirst_tensor::CpuAccess::ReadWrite,
7734 )
7735 .unwrap();
7736 let mut g2d_converter = G2DProcessor::new().unwrap();
7737
7738 let (result, src, g2d_dst) = convert_img(
7739 &mut g2d_converter,
7740 src,
7741 g2d_dst,
7742 Rotation::None,
7743 Flip::None,
7744 Crop::no_crop(),
7745 );
7746 result.unwrap();
7747
7748 let cpu_dst = TensorDyn::image(
7749 1280,
7750 720,
7751 PixelFormat::Rgb,
7752 DType::U8,
7753 None,
7754 edgefirst_tensor::CpuAccess::ReadWrite,
7755 )
7756 .unwrap();
7757 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
7758
7759 let (result, _src, cpu_dst) = convert_img(
7760 &mut cpu_converter,
7761 src,
7762 cpu_dst,
7763 Rotation::None,
7764 Flip::None,
7765 Crop::no_crop(),
7766 );
7767 result.unwrap();
7768
7769 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
7775 }
7776
7777 #[test]
7778 #[cfg(target_os = "linux")]
7779 fn test_yuyv_to_yuyv_resize_g2d() {
7780 if !is_g2d_available() {
7781 eprintln!(
7782 "SKIPPED: test_yuyv_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available"
7783 );
7784 return;
7785 }
7786 if !is_dma_available() {
7787 eprintln!(
7788 "SKIPPED: test_yuyv_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
7789 );
7790 return;
7791 }
7792
7793 let src = load_bytes_to_tensor(
7794 1280,
7795 720,
7796 PixelFormat::Yuyv,
7797 None,
7798 &edgefirst_bench::testdata::read("camera720p.yuyv"),
7799 )
7800 .unwrap();
7801
7802 let g2d_dst = TensorDyn::image(
7803 600,
7804 400,
7805 PixelFormat::Yuyv,
7806 DType::U8,
7807 Some(TensorMemory::Dma),
7808 edgefirst_tensor::CpuAccess::ReadWrite,
7809 )
7810 .unwrap();
7811 let mut g2d_converter = G2DProcessor::new().unwrap();
7812
7813 let (result, src, g2d_dst) = convert_img(
7814 &mut g2d_converter,
7815 src,
7816 g2d_dst,
7817 Rotation::None,
7818 Flip::None,
7819 Crop::no_crop(),
7820 );
7821 result.unwrap();
7822
7823 let cpu_dst = TensorDyn::image(
7824 600,
7825 400,
7826 PixelFormat::Yuyv,
7827 DType::U8,
7828 None,
7829 edgefirst_tensor::CpuAccess::ReadWrite,
7830 )
7831 .unwrap();
7832 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
7833
7834 let (result, _src, cpu_dst) = convert_img(
7835 &mut cpu_converter,
7836 src,
7837 cpu_dst,
7838 Rotation::None,
7839 Flip::None,
7840 Crop::no_crop(),
7841 );
7842 result.unwrap();
7843
7844 eprintln!(
7851 "WARNING: G2D has poor colorimetry support — YUYV resize diverges from the \
7852 CPU reference (~0.85 similarity); threshold held at 0.85, not 0.95."
7853 );
7854 compare_images_convert_to_rgb(&g2d_dst, &cpu_dst, 0.85, function!());
7855 }
7856
7857 #[test]
7858 fn test_yuyv_to_rgba_resize_cpu() {
7859 let src = load_bytes_to_tensor(
7860 1280,
7861 720,
7862 PixelFormat::Yuyv,
7863 None,
7864 &edgefirst_bench::testdata::read("camera720p.yuyv"),
7865 )
7866 .unwrap();
7867
7868 let (dst_width, dst_height) = (960, 540);
7869
7870 let dst = TensorDyn::image(
7871 dst_width,
7872 dst_height,
7873 PixelFormat::Rgba,
7874 DType::U8,
7875 None,
7876 edgefirst_tensor::CpuAccess::ReadWrite,
7877 )
7878 .unwrap();
7879 let mut cpu_converter = CPUProcessor::new();
7880
7881 let (result, _src, dst) = convert_img(
7882 &mut cpu_converter,
7883 src,
7884 dst,
7885 Rotation::None,
7886 Flip::None,
7887 Crop::no_crop(),
7888 );
7889 result.unwrap();
7890
7891 let dst_target = TensorDyn::image(
7892 dst_width,
7893 dst_height,
7894 PixelFormat::Rgba,
7895 DType::U8,
7896 None,
7897 edgefirst_tensor::CpuAccess::ReadWrite,
7898 )
7899 .unwrap();
7900 let src_target = load_bytes_to_tensor(
7901 1280,
7902 720,
7903 PixelFormat::Rgba,
7904 None,
7905 &edgefirst_bench::testdata::read("camera720p.rgba"),
7906 )
7907 .unwrap();
7908 let (result, _src_target, dst_target) = convert_img(
7909 &mut cpu_converter,
7910 src_target,
7911 dst_target,
7912 Rotation::None,
7913 Flip::None,
7914 Crop::no_crop(),
7915 );
7916 result.unwrap();
7917
7918 compare_images(&dst, &dst_target, 0.98, function!());
7921 }
7922
7923 #[test]
7924 #[cfg(target_os = "linux")]
7925 fn test_yuyv_to_rgba_crop_flip_g2d() {
7926 if !is_g2d_available() {
7927 eprintln!(
7928 "SKIPPED: test_yuyv_to_rgba_crop_flip_g2d - G2D library (libg2d.so.2) not available"
7929 );
7930 return;
7931 }
7932 if !is_dma_available() {
7933 eprintln!(
7934 "SKIPPED: test_yuyv_to_rgba_crop_flip_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
7935 );
7936 return;
7937 }
7938
7939 let src = load_bytes_to_tensor(
7940 1280,
7941 720,
7942 PixelFormat::Yuyv,
7943 Some(TensorMemory::Dma),
7944 &edgefirst_bench::testdata::read("camera720p.yuyv"),
7945 )
7946 .unwrap();
7947
7948 let (dst_width, dst_height) = (640, 640);
7949
7950 let dst_g2d = TensorDyn::image(
7951 dst_width,
7952 dst_height,
7953 PixelFormat::Rgba,
7954 DType::U8,
7955 Some(TensorMemory::Dma),
7956 edgefirst_tensor::CpuAccess::ReadWrite,
7957 )
7958 .unwrap();
7959 let mut g2d_converter = G2DProcessor::new().unwrap();
7960 let crop = Crop::new().with_source(Some(Region::new(20, 15, 400, 300)));
7961
7962 let (result, src, dst_g2d) = convert_img(
7963 &mut g2d_converter,
7964 src,
7965 dst_g2d,
7966 Rotation::None,
7967 Flip::Horizontal,
7968 crop,
7969 );
7970 result.unwrap();
7971
7972 let dst_cpu = 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 cpu_converter = CPUProcessor::new();
7982
7983 let (result, _src, dst_cpu) = convert_img(
7984 &mut cpu_converter,
7985 src,
7986 dst_cpu,
7987 Rotation::None,
7988 Flip::Horizontal,
7989 crop,
7990 );
7991 result.unwrap();
7992 compare_images(&dst_g2d, &dst_cpu, 0.98, function!());
7998 }
7999
8000 #[test]
8001 #[cfg(target_os = "linux")]
8002 #[cfg(feature = "opengl")]
8003 fn test_yuyv_to_rgba_crop_flip_opengl() {
8004 if !is_opengl_available() {
8005 eprintln!("SKIPPED: {} - OpenGL not available", function!());
8006 return;
8007 }
8008
8009 if !is_dma_available() {
8010 eprintln!(
8011 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
8012 function!()
8013 );
8014 return;
8015 }
8016
8017 let src = load_bytes_to_tensor(
8018 1280,
8019 720,
8020 PixelFormat::Yuyv,
8021 Some(TensorMemory::Dma),
8022 &edgefirst_bench::testdata::read("camera720p.yuyv"),
8023 )
8024 .unwrap();
8025
8026 let (dst_width, dst_height) = (640, 640);
8027
8028 let dst_gl = TensorDyn::image(
8029 dst_width,
8030 dst_height,
8031 PixelFormat::Rgba,
8032 DType::U8,
8033 Some(TensorMemory::Dma),
8034 edgefirst_tensor::CpuAccess::ReadWrite,
8035 )
8036 .unwrap();
8037 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
8038 let crop = Crop::new().with_source(Some(Region::new(20, 15, 400, 300)));
8039
8040 let (result, src, dst_gl) = convert_img(
8041 &mut gl_converter,
8042 src,
8043 dst_gl,
8044 Rotation::None,
8045 Flip::Horizontal,
8046 crop,
8047 );
8048 result.unwrap();
8049
8050 let dst_cpu = TensorDyn::image(
8051 dst_width,
8052 dst_height,
8053 PixelFormat::Rgba,
8054 DType::U8,
8055 Some(TensorMemory::Dma),
8056 edgefirst_tensor::CpuAccess::ReadWrite,
8057 )
8058 .unwrap();
8059 let mut cpu_converter = CPUProcessor::new();
8060
8061 let (result, _src, dst_cpu) = convert_img(
8062 &mut cpu_converter,
8063 src,
8064 dst_cpu,
8065 Rotation::None,
8066 Flip::Horizontal,
8067 crop,
8068 );
8069 result.unwrap();
8070 compare_images(&dst_gl, &dst_cpu, 0.98, function!());
8075 }
8076
8077 #[test]
8078 fn test_vyuy_to_rgba_cpu() {
8079 let file = edgefirst_bench::testdata::read("camera720p.vyuy").to_vec();
8080 let src = TensorDyn::image(
8081 1280,
8082 720,
8083 PixelFormat::Vyuy,
8084 DType::U8,
8085 None,
8086 edgefirst_tensor::CpuAccess::ReadWrite,
8087 )
8088 .unwrap();
8089 src.as_u8()
8090 .unwrap()
8091 .map()
8092 .unwrap()
8093 .as_mut_slice()
8094 .copy_from_slice(&file);
8095
8096 let dst = TensorDyn::image(
8097 1280,
8098 720,
8099 PixelFormat::Rgba,
8100 DType::U8,
8101 None,
8102 edgefirst_tensor::CpuAccess::ReadWrite,
8103 )
8104 .unwrap();
8105 let mut cpu_converter = CPUProcessor::new();
8106
8107 let (result, _src, dst) = convert_img(
8108 &mut cpu_converter,
8109 src,
8110 dst,
8111 Rotation::None,
8112 Flip::None,
8113 Crop::no_crop(),
8114 );
8115 result.unwrap();
8116
8117 let target_image = TensorDyn::image(
8118 1280,
8119 720,
8120 PixelFormat::Rgba,
8121 DType::U8,
8122 None,
8123 edgefirst_tensor::CpuAccess::ReadWrite,
8124 )
8125 .unwrap();
8126 target_image
8127 .as_u8()
8128 .unwrap()
8129 .map()
8130 .unwrap()
8131 .as_mut_slice()
8132 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
8133
8134 compare_images(&dst, &target_image, 0.98, function!());
8137 }
8138
8139 #[test]
8140 fn test_vyuy_to_rgb_cpu() {
8141 let file = edgefirst_bench::testdata::read("camera720p.vyuy").to_vec();
8142 let src = TensorDyn::image(
8143 1280,
8144 720,
8145 PixelFormat::Vyuy,
8146 DType::U8,
8147 None,
8148 edgefirst_tensor::CpuAccess::ReadWrite,
8149 )
8150 .unwrap();
8151 src.as_u8()
8152 .unwrap()
8153 .map()
8154 .unwrap()
8155 .as_mut_slice()
8156 .copy_from_slice(&file);
8157
8158 let dst = TensorDyn::image(
8159 1280,
8160 720,
8161 PixelFormat::Rgb,
8162 DType::U8,
8163 None,
8164 edgefirst_tensor::CpuAccess::ReadWrite,
8165 )
8166 .unwrap();
8167 let mut cpu_converter = CPUProcessor::new();
8168
8169 let (result, _src, dst) = convert_img(
8170 &mut cpu_converter,
8171 src,
8172 dst,
8173 Rotation::None,
8174 Flip::None,
8175 Crop::no_crop(),
8176 );
8177 result.unwrap();
8178
8179 let target_image = TensorDyn::image(
8180 1280,
8181 720,
8182 PixelFormat::Rgb,
8183 DType::U8,
8184 None,
8185 edgefirst_tensor::CpuAccess::ReadWrite,
8186 )
8187 .unwrap();
8188 target_image
8189 .as_u8()
8190 .unwrap()
8191 .map()
8192 .unwrap()
8193 .as_mut_slice()
8194 .as_chunks_mut::<3>()
8195 .0
8196 .iter_mut()
8197 .zip(
8198 edgefirst_bench::testdata::read("camera720p.rgba")
8199 .as_chunks::<4>()
8200 .0,
8201 )
8202 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
8203
8204 compare_images(&dst, &target_image, 0.98, function!());
8207 }
8208
8209 #[test]
8210 #[cfg(target_os = "linux")]
8211 #[ignore = "G2D does not support VYUY; re-enable when hardware support is added"]
8212 fn test_vyuy_to_rgba_g2d() {
8213 if !is_g2d_available() {
8214 eprintln!("SKIPPED: test_vyuy_to_rgba_g2d - G2D library (libg2d.so.2) not available");
8215 return;
8216 }
8217 if !is_dma_available() {
8218 eprintln!(
8219 "SKIPPED: test_vyuy_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
8220 );
8221 return;
8222 }
8223
8224 let src = load_bytes_to_tensor(
8225 1280,
8226 720,
8227 PixelFormat::Vyuy,
8228 None,
8229 &edgefirst_bench::testdata::read("camera720p.vyuy"),
8230 )
8231 .unwrap();
8232
8233 let dst = TensorDyn::image(
8234 1280,
8235 720,
8236 PixelFormat::Rgba,
8237 DType::U8,
8238 Some(TensorMemory::Dma),
8239 edgefirst_tensor::CpuAccess::ReadWrite,
8240 )
8241 .unwrap();
8242 let mut g2d_converter = G2DProcessor::new().unwrap();
8243
8244 let (result, _src, dst) = convert_img(
8245 &mut g2d_converter,
8246 src,
8247 dst,
8248 Rotation::None,
8249 Flip::None,
8250 Crop::no_crop(),
8251 );
8252 match result {
8253 Err(Error::G2D(_)) => {
8254 eprintln!("SKIPPED: test_vyuy_to_rgba_g2d - G2D does not support PixelFormat::Vyuy format");
8255 return;
8256 }
8257 r => r.unwrap(),
8258 }
8259
8260 let target_image = TensorDyn::image(
8261 1280,
8262 720,
8263 PixelFormat::Rgba,
8264 DType::U8,
8265 None,
8266 edgefirst_tensor::CpuAccess::ReadWrite,
8267 )
8268 .unwrap();
8269 target_image
8270 .as_u8()
8271 .unwrap()
8272 .map()
8273 .unwrap()
8274 .as_mut_slice()
8275 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
8276
8277 compare_images(&dst, &target_image, 0.98, function!());
8281 }
8282
8283 #[test]
8284 #[cfg(target_os = "linux")]
8285 #[ignore = "G2D does not support VYUY; re-enable when hardware support is added"]
8286 fn test_vyuy_to_rgb_g2d() {
8287 if !is_g2d_available() {
8288 eprintln!("SKIPPED: test_vyuy_to_rgb_g2d - G2D library (libg2d.so.2) not available");
8289 return;
8290 }
8291 if !is_dma_available() {
8292 eprintln!(
8293 "SKIPPED: test_vyuy_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
8294 );
8295 return;
8296 }
8297
8298 let src = load_bytes_to_tensor(
8299 1280,
8300 720,
8301 PixelFormat::Vyuy,
8302 None,
8303 &edgefirst_bench::testdata::read("camera720p.vyuy"),
8304 )
8305 .unwrap();
8306
8307 let g2d_dst = TensorDyn::image(
8308 1280,
8309 720,
8310 PixelFormat::Rgb,
8311 DType::U8,
8312 Some(TensorMemory::Dma),
8313 edgefirst_tensor::CpuAccess::ReadWrite,
8314 )
8315 .unwrap();
8316 let mut g2d_converter = G2DProcessor::new().unwrap();
8317
8318 let (result, src, g2d_dst) = convert_img(
8319 &mut g2d_converter,
8320 src,
8321 g2d_dst,
8322 Rotation::None,
8323 Flip::None,
8324 Crop::no_crop(),
8325 );
8326 match result {
8327 Err(Error::G2D(_)) => {
8328 eprintln!(
8329 "SKIPPED: test_vyuy_to_rgb_g2d - G2D does not support PixelFormat::Vyuy format"
8330 );
8331 return;
8332 }
8333 r => r.unwrap(),
8334 }
8335
8336 let cpu_dst = TensorDyn::image(
8337 1280,
8338 720,
8339 PixelFormat::Rgb,
8340 DType::U8,
8341 None,
8342 edgefirst_tensor::CpuAccess::ReadWrite,
8343 )
8344 .unwrap();
8345 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
8346
8347 let (result, _src, cpu_dst) = convert_img(
8348 &mut cpu_converter,
8349 src,
8350 cpu_dst,
8351 Rotation::None,
8352 Flip::None,
8353 Crop::no_crop(),
8354 );
8355 result.unwrap();
8356
8357 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
8363 }
8364
8365 #[test]
8366 #[cfg(target_os = "linux")]
8367 #[cfg(feature = "opengl")]
8368 fn test_vyuy_to_rgba_opengl() {
8369 if !is_opengl_available() {
8370 eprintln!("SKIPPED: {} - OpenGL not available", function!());
8371 return;
8372 }
8373 if !is_dma_available() {
8374 eprintln!(
8375 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
8376 function!()
8377 );
8378 return;
8379 }
8380
8381 let src = load_bytes_to_tensor(
8382 1280,
8383 720,
8384 PixelFormat::Vyuy,
8385 Some(TensorMemory::Dma),
8386 &edgefirst_bench::testdata::read("camera720p.vyuy"),
8387 )
8388 .unwrap();
8389
8390 let dst = TensorDyn::image(
8391 1280,
8392 720,
8393 PixelFormat::Rgba,
8394 DType::U8,
8395 Some(TensorMemory::Dma),
8396 edgefirst_tensor::CpuAccess::ReadWrite,
8397 )
8398 .unwrap();
8399 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
8400
8401 let (result, _src, dst) = convert_img(
8402 &mut gl_converter,
8403 src,
8404 dst,
8405 Rotation::None,
8406 Flip::None,
8407 Crop::no_crop(),
8408 );
8409 match result {
8410 Err(Error::NotSupported(_)) => {
8411 eprintln!(
8412 "SKIPPED: {} - OpenGL does not support PixelFormat::Vyuy DMA format",
8413 function!()
8414 );
8415 return;
8416 }
8417 r => r.unwrap(),
8418 }
8419
8420 let target_image = TensorDyn::image(
8421 1280,
8422 720,
8423 PixelFormat::Rgba,
8424 DType::U8,
8425 None,
8426 edgefirst_tensor::CpuAccess::ReadWrite,
8427 )
8428 .unwrap();
8429 target_image
8430 .as_u8()
8431 .unwrap()
8432 .map()
8433 .unwrap()
8434 .as_mut_slice()
8435 .copy_from_slice(&edgefirst_bench::testdata::read("camera720p.rgba"));
8436
8437 compare_images(&dst, &target_image, 0.98, function!());
8441 }
8442
8443 #[test]
8444 fn test_nv12_to_rgba_cpu() {
8445 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
8446 let src = TensorDyn::image(
8447 1280,
8448 720,
8449 PixelFormat::Nv12,
8450 DType::U8,
8451 None,
8452 edgefirst_tensor::CpuAccess::ReadWrite,
8453 )
8454 .unwrap();
8455 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
8456 .copy_from_slice(&file);
8457
8458 let dst = TensorDyn::image(
8459 1280,
8460 720,
8461 PixelFormat::Rgba,
8462 DType::U8,
8463 None,
8464 edgefirst_tensor::CpuAccess::ReadWrite,
8465 )
8466 .unwrap();
8467 let mut cpu_converter = CPUProcessor::new();
8468
8469 let (result, _src, dst) = convert_img(
8470 &mut cpu_converter,
8471 src,
8472 dst,
8473 Rotation::None,
8474 Flip::None,
8475 Crop::no_crop(),
8476 );
8477 result.unwrap();
8478
8479 let target_image = crate::load_image_test_helper(
8480 &edgefirst_bench::testdata::read("zidane.jpg"),
8481 Some(PixelFormat::Rgba),
8482 None,
8483 )
8484 .unwrap();
8485
8486 compare_images(&dst, &target_image, 0.95, function!());
8491 }
8492
8493 #[test]
8494 fn test_nv12_odd_height_to_rgb_cpu() {
8495 let mut src = TensorDyn::image(
8506 8,
8507 5,
8508 PixelFormat::Nv12,
8509 DType::U8,
8510 Some(TensorMemory::Mem),
8511 edgefirst_tensor::CpuAccess::ReadWrite,
8512 )
8513 .unwrap();
8514 assert_eq!(src.shape(), &[8, 8]);
8515 assert_eq!((src.width(), src.height()), (Some(8), Some(5)));
8516 src.as_u8().unwrap().map().unwrap().as_mut_slice().fill(128);
8517 src.set_colorimetry(Some(
8521 edgefirst_tensor::Colorimetry::default()
8522 .with_encoding(edgefirst_tensor::ColorEncoding::Bt601)
8523 .with_range(edgefirst_tensor::ColorRange::Full),
8524 ));
8525
8526 let dst = TensorDyn::image(
8527 8,
8528 5,
8529 PixelFormat::Rgb,
8530 DType::U8,
8531 Some(TensorMemory::Mem),
8532 edgefirst_tensor::CpuAccess::ReadWrite,
8533 )
8534 .unwrap();
8535 let mut cpu_converter = CPUProcessor::new();
8536 let (result, _src, dst) = convert_img(
8537 &mut cpu_converter,
8538 src,
8539 dst,
8540 Rotation::None,
8541 Flip::None,
8542 Crop::no_crop(),
8543 );
8544 result.unwrap();
8545
8546 assert_eq!((dst.width(), dst.height()), (Some(8), Some(5)));
8547 let map = dst.as_u8().unwrap().map().unwrap();
8548 for (i, &b) in map.as_slice().iter().enumerate() {
8549 assert!(
8550 (b as i16 - 128).abs() <= 2,
8551 "pixel byte {i} = {b}, expected ~128 for neutral-grey NV12"
8552 );
8553 }
8554 }
8555
8556 #[test]
8557 fn test_nv24_to_rgb_cpu() {
8558 let mut src = TensorDyn::image(
8564 8,
8565 4,
8566 PixelFormat::Nv24,
8567 DType::U8,
8568 Some(TensorMemory::Mem),
8569 edgefirst_tensor::CpuAccess::ReadWrite,
8570 )
8571 .unwrap();
8572 assert_eq!(src.shape(), &[12, 8]);
8573 assert_eq!((src.width(), src.height()), (Some(8), Some(4)));
8574 src.as_u8().unwrap().map().unwrap().as_mut_slice().fill(128);
8575 src.set_colorimetry(Some(
8578 edgefirst_tensor::Colorimetry::default()
8579 .with_encoding(edgefirst_tensor::ColorEncoding::Bt601)
8580 .with_range(edgefirst_tensor::ColorRange::Full),
8581 ));
8582
8583 let dst = TensorDyn::image(
8584 8,
8585 4,
8586 PixelFormat::Rgb,
8587 DType::U8,
8588 Some(TensorMemory::Mem),
8589 edgefirst_tensor::CpuAccess::ReadWrite,
8590 )
8591 .unwrap();
8592 let mut cpu_converter = CPUProcessor::new();
8593 let (result, _src, dst) = convert_img(
8594 &mut cpu_converter,
8595 src,
8596 dst,
8597 Rotation::None,
8598 Flip::None,
8599 Crop::no_crop(),
8600 );
8601 result.unwrap();
8602
8603 assert_eq!((dst.width(), dst.height()), (Some(8), Some(4)));
8604 let map = dst.as_u8().unwrap().map().unwrap();
8605 for (i, &b) in map.as_slice().iter().enumerate() {
8606 assert!(
8607 (b as i16 - 128).abs() <= 2,
8608 "pixel byte {i} = {b}, expected ~128 for neutral-grey NV24"
8609 );
8610 }
8611 }
8612
8613 #[test]
8614 fn cpu_nv12_to_rgb_respects_tagged_bt2020() {
8615 fn decode_tagged(enc: edgefirst_tensor::ColorEncoding) -> [u8; 3] {
8623 let mut src = TensorDyn::image(
8624 8,
8625 4,
8626 PixelFormat::Nv12,
8627 DType::U8,
8628 Some(TensorMemory::Mem),
8629 edgefirst_tensor::CpuAccess::ReadWrite,
8630 )
8631 .unwrap();
8632 assert_eq!(src.shape(), &[6, 8]);
8634 {
8635 let mut map = src.as_u8().unwrap().map().unwrap();
8636 let buf = map.as_mut_slice();
8637 buf[..32].fill(120); for px in buf[32..].chunks_exact_mut(2) {
8639 px[0] = 180; px[1] = 64; }
8642 }
8643 src.set_colorimetry(Some(
8646 edgefirst_tensor::Colorimetry::default()
8647 .with_encoding(enc)
8648 .with_range(edgefirst_tensor::ColorRange::Limited),
8649 ));
8650 let dst = TensorDyn::image(
8651 8,
8652 4,
8653 PixelFormat::Rgb,
8654 DType::U8,
8655 Some(TensorMemory::Mem),
8656 edgefirst_tensor::CpuAccess::ReadWrite,
8657 )
8658 .unwrap();
8659 let mut cpu = CPUProcessor::new();
8660 let (result, _src, dst) = convert_img(
8661 &mut cpu,
8662 src,
8663 dst,
8664 Rotation::None,
8665 Flip::None,
8666 Crop::no_crop(),
8667 );
8668 result.unwrap();
8669 let map = dst.as_u8().unwrap().map().unwrap();
8670 let s = map.as_slice();
8671 [s[0], s[1], s[2]]
8672 }
8673
8674 let bt601 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt601);
8675 let bt709 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt709);
8676 let bt2020 = decode_tagged(edgefirst_tensor::ColorEncoding::Bt2020);
8677
8678 assert_ne!(
8679 bt2020, bt601,
8680 "BT.2020 must decode differently from BT.601 ({bt2020:?} vs {bt601:?})"
8681 );
8682 assert_ne!(
8683 bt2020, bt709,
8684 "BT.2020 must decode differently from BT.709 ({bt2020:?} vs {bt709:?})"
8685 );
8686 assert_ne!(
8687 bt601, bt709,
8688 "BT.601 must decode differently from BT.709 ({bt601:?} vs {bt709:?})"
8689 );
8690 }
8691
8692 #[test]
8693 fn test_nv12_to_rgb_cpu() {
8694 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
8695 let src = TensorDyn::image(
8696 1280,
8697 720,
8698 PixelFormat::Nv12,
8699 DType::U8,
8700 None,
8701 edgefirst_tensor::CpuAccess::ReadWrite,
8702 )
8703 .unwrap();
8704 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
8705 .copy_from_slice(&file);
8706
8707 let dst = TensorDyn::image(
8708 1280,
8709 720,
8710 PixelFormat::Rgb,
8711 DType::U8,
8712 None,
8713 edgefirst_tensor::CpuAccess::ReadWrite,
8714 )
8715 .unwrap();
8716 let mut cpu_converter = CPUProcessor::new();
8717
8718 let (result, _src, dst) = convert_img(
8719 &mut cpu_converter,
8720 src,
8721 dst,
8722 Rotation::None,
8723 Flip::None,
8724 Crop::no_crop(),
8725 );
8726 result.unwrap();
8727
8728 let target_image = crate::load_image_test_helper(
8729 &edgefirst_bench::testdata::read("zidane.jpg"),
8730 Some(PixelFormat::Rgb),
8731 None,
8732 )
8733 .unwrap();
8734
8735 compare_images(&dst, &target_image, 0.95, function!());
8740 }
8741
8742 #[test]
8743 fn test_nv12_to_grey_cpu() {
8744 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
8745 let src = TensorDyn::image(
8746 1280,
8747 720,
8748 PixelFormat::Nv12,
8749 DType::U8,
8750 None,
8751 edgefirst_tensor::CpuAccess::ReadWrite,
8752 )
8753 .unwrap();
8754 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
8755 .copy_from_slice(&file);
8756
8757 let dst = TensorDyn::image(
8758 1280,
8759 720,
8760 PixelFormat::Grey,
8761 DType::U8,
8762 None,
8763 edgefirst_tensor::CpuAccess::ReadWrite,
8764 )
8765 .unwrap();
8766 let mut cpu_converter = CPUProcessor::new();
8767
8768 let (result, _src, dst) = convert_img(
8769 &mut cpu_converter,
8770 src,
8771 dst,
8772 Rotation::None,
8773 Flip::None,
8774 Crop::no_crop(),
8775 );
8776 result.unwrap();
8777
8778 let target_image = crate::load_image_test_helper(
8779 &edgefirst_bench::testdata::read("zidane.jpg"),
8780 Some(PixelFormat::Grey),
8781 None,
8782 )
8783 .unwrap();
8784
8785 compare_images(&dst, &target_image, 0.95, function!());
8790 }
8791
8792 #[test]
8793 fn test_nv12_to_yuyv_cpu() {
8794 let file = edgefirst_bench::testdata::read("zidane.nv12").to_vec();
8795 let src = TensorDyn::image(
8796 1280,
8797 720,
8798 PixelFormat::Nv12,
8799 DType::U8,
8800 None,
8801 edgefirst_tensor::CpuAccess::ReadWrite,
8802 )
8803 .unwrap();
8804 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
8805 .copy_from_slice(&file);
8806
8807 let dst = TensorDyn::image(
8808 1280,
8809 720,
8810 PixelFormat::Yuyv,
8811 DType::U8,
8812 None,
8813 edgefirst_tensor::CpuAccess::ReadWrite,
8814 )
8815 .unwrap();
8816 let mut cpu_converter = CPUProcessor::new();
8817
8818 let (result, _src, dst) = convert_img(
8819 &mut cpu_converter,
8820 src,
8821 dst,
8822 Rotation::None,
8823 Flip::None,
8824 Crop::no_crop(),
8825 );
8826 result.unwrap();
8827
8828 let target_image = crate::load_image_test_helper(
8829 &edgefirst_bench::testdata::read("zidane.jpg"),
8830 Some(PixelFormat::Rgb),
8831 None,
8832 )
8833 .unwrap();
8834
8835 compare_images_convert_to_rgb(&dst, &target_image, 0.95, function!());
8840 }
8841
8842 #[test]
8843 fn test_cpu_resize_nv16() {
8844 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
8845 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
8846
8847 let cpu_nv16_dst = TensorDyn::image(
8848 640,
8849 640,
8850 PixelFormat::Nv16,
8851 DType::U8,
8852 None,
8853 edgefirst_tensor::CpuAccess::ReadWrite,
8854 )
8855 .unwrap();
8856 let cpu_rgb_dst = TensorDyn::image(
8857 640,
8858 640,
8859 PixelFormat::Rgb,
8860 DType::U8,
8861 None,
8862 edgefirst_tensor::CpuAccess::ReadWrite,
8863 )
8864 .unwrap();
8865 let mut cpu_converter = CPUProcessor::new();
8866 let crop = Crop::letterbox([255, 128, 0, 255]);
8867
8868 let (result, src, cpu_nv16_dst) = convert_img(
8869 &mut cpu_converter,
8870 src,
8871 cpu_nv16_dst,
8872 Rotation::None,
8873 Flip::None,
8874 crop,
8875 );
8876 result.unwrap();
8877
8878 let (result, _src, cpu_rgb_dst) = convert_img(
8879 &mut cpu_converter,
8880 src,
8881 cpu_rgb_dst,
8882 Rotation::None,
8883 Flip::None,
8884 crop,
8885 );
8886 result.unwrap();
8887 compare_images_convert_to_rgb(&cpu_nv16_dst, &cpu_rgb_dst, 0.99, function!());
8888 }
8889
8890 fn load_bytes_to_tensor(
8891 width: usize,
8892 height: usize,
8893 format: PixelFormat,
8894 memory: Option<TensorMemory>,
8895 bytes: &[u8],
8896 ) -> Result<TensorDyn, Error> {
8897 let src = TensorDyn::image(
8898 width,
8899 height,
8900 format,
8901 DType::U8,
8902 memory,
8903 edgefirst_tensor::CpuAccess::ReadWrite,
8904 )?;
8905 src.as_u8()
8906 .unwrap()
8907 .map()?
8908 .as_mut_slice()
8909 .copy_from_slice(bytes);
8910 Ok(src)
8911 }
8912
8913 fn compare_images(img1: &TensorDyn, img2: &TensorDyn, threshold: f64, name: &str) {
8921 assert_eq!(img1.height(), img2.height(), "Heights differ");
8922 assert_eq!(img1.width(), img2.width(), "Widths differ");
8923 assert_eq!(
8924 img1.format().unwrap(),
8925 img2.format().unwrap(),
8926 "PixelFormat differ"
8927 );
8928 assert!(
8929 matches!(
8930 img1.format().unwrap(),
8931 PixelFormat::Rgb | PixelFormat::Rgba | PixelFormat::Grey | PixelFormat::PlanarRgb
8932 ),
8933 "format must be Rgb or Rgba for comparison"
8934 );
8935
8936 let image1 = match img1.format().unwrap() {
8937 PixelFormat::Rgb => image::RgbImage::from_vec(
8938 img1.width().unwrap() as u32,
8939 img1.height().unwrap() as u32,
8940 img1.as_u8().unwrap().map().unwrap().to_vec(),
8941 )
8942 .unwrap(),
8943 PixelFormat::Rgba => image::RgbaImage::from_vec(
8944 img1.width().unwrap() as u32,
8945 img1.height().unwrap() as u32,
8946 img1.as_u8().unwrap().map().unwrap().to_vec(),
8947 )
8948 .unwrap()
8949 .convert(),
8950 PixelFormat::Grey => image::GrayImage::from_vec(
8951 img1.width().unwrap() as u32,
8952 img1.height().unwrap() as u32,
8953 img1.as_u8().unwrap().map().unwrap().to_vec(),
8954 )
8955 .unwrap()
8956 .convert(),
8957 PixelFormat::PlanarRgb => image::GrayImage::from_vec(
8958 img1.width().unwrap() as u32,
8959 (img1.height().unwrap() * 3) as u32,
8960 img1.as_u8().unwrap().map().unwrap().to_vec(),
8961 )
8962 .unwrap()
8963 .convert(),
8964 _ => return,
8965 };
8966
8967 let image2 = match img2.format().unwrap() {
8968 PixelFormat::Rgb => image::RgbImage::from_vec(
8969 img2.width().unwrap() as u32,
8970 img2.height().unwrap() as u32,
8971 img2.as_u8().unwrap().map().unwrap().to_vec(),
8972 )
8973 .unwrap(),
8974 PixelFormat::Rgba => image::RgbaImage::from_vec(
8975 img2.width().unwrap() as u32,
8976 img2.height().unwrap() as u32,
8977 img2.as_u8().unwrap().map().unwrap().to_vec(),
8978 )
8979 .unwrap()
8980 .convert(),
8981 PixelFormat::Grey => image::GrayImage::from_vec(
8982 img2.width().unwrap() as u32,
8983 img2.height().unwrap() as u32,
8984 img2.as_u8().unwrap().map().unwrap().to_vec(),
8985 )
8986 .unwrap()
8987 .convert(),
8988 PixelFormat::PlanarRgb => image::GrayImage::from_vec(
8989 img2.width().unwrap() as u32,
8990 (img2.height().unwrap() * 3) as u32,
8991 img2.as_u8().unwrap().map().unwrap().to_vec(),
8992 )
8993 .unwrap()
8994 .convert(),
8995 _ => return,
8996 };
8997
8998 let similarity = image_compare::rgb_similarity_structure(
8999 &image_compare::Algorithm::RootMeanSquared,
9000 &image1,
9001 &image2,
9002 )
9003 .expect("Image Comparison failed");
9004 if similarity.score < threshold {
9005 similarity
9008 .image
9009 .to_color_map()
9010 .save(format!("{name}.png"))
9011 .unwrap();
9012 panic!(
9013 "{name}: converted image and target image have similarity score too low: {} < {}",
9014 similarity.score, threshold
9015 )
9016 }
9017 }
9018
9019 fn compare_images_convert_to_rgb(
9020 img1: &TensorDyn,
9021 img2: &TensorDyn,
9022 threshold: f64,
9023 name: &str,
9024 ) {
9025 assert_eq!(img1.height(), img2.height(), "Heights differ");
9026 assert_eq!(img1.width(), img2.width(), "Widths differ");
9027
9028 let mut img_rgb1 = TensorDyn::image(
9029 img1.width().unwrap(),
9030 img1.height().unwrap(),
9031 PixelFormat::Rgb,
9032 DType::U8,
9033 Some(TensorMemory::Mem),
9034 edgefirst_tensor::CpuAccess::ReadWrite,
9035 )
9036 .unwrap();
9037 let mut img_rgb2 = TensorDyn::image(
9038 img1.width().unwrap(),
9039 img1.height().unwrap(),
9040 PixelFormat::Rgb,
9041 DType::U8,
9042 Some(TensorMemory::Mem),
9043 edgefirst_tensor::CpuAccess::ReadWrite,
9044 )
9045 .unwrap();
9046 let mut __cv = CPUProcessor::default();
9047 let r1 = __cv.convert(
9048 img1,
9049 &mut img_rgb1,
9050 crate::Rotation::None,
9051 crate::Flip::None,
9052 crate::Crop::default(),
9053 );
9054 let r2 = __cv.convert(
9055 img2,
9056 &mut img_rgb2,
9057 crate::Rotation::None,
9058 crate::Flip::None,
9059 crate::Crop::default(),
9060 );
9061 if r1.is_err() || r2.is_err() {
9062 let w = img1.width().unwrap() as u32;
9064 let data1 = img1.as_u8().unwrap().map().unwrap().to_vec();
9065 let data2 = img2.as_u8().unwrap().map().unwrap().to_vec();
9066 let h1 = (data1.len() as u32) / w;
9067 let h2 = (data2.len() as u32) / w;
9068 let g1 = image::GrayImage::from_vec(w, h1, data1).unwrap();
9069 let g2 = image::GrayImage::from_vec(w, h2, data2).unwrap();
9070 let similarity = image_compare::gray_similarity_structure(
9071 &image_compare::Algorithm::RootMeanSquared,
9072 &g1,
9073 &g2,
9074 )
9075 .expect("Image Comparison failed");
9076 if similarity.score < threshold {
9077 panic!(
9078 "{name}: converted image and target image have similarity score too low: {} < {}",
9079 similarity.score, threshold
9080 )
9081 }
9082 return;
9083 }
9084
9085 let image1 = image::RgbImage::from_vec(
9086 img_rgb1.width().unwrap() as u32,
9087 img_rgb1.height().unwrap() as u32,
9088 img_rgb1.as_u8().unwrap().map().unwrap().to_vec(),
9089 )
9090 .unwrap();
9091
9092 let image2 = image::RgbImage::from_vec(
9093 img_rgb2.width().unwrap() as u32,
9094 img_rgb2.height().unwrap() as u32,
9095 img_rgb2.as_u8().unwrap().map().unwrap().to_vec(),
9096 )
9097 .unwrap();
9098
9099 let similarity = image_compare::rgb_similarity_structure(
9100 &image_compare::Algorithm::RootMeanSquared,
9101 &image1,
9102 &image2,
9103 )
9104 .expect("Image Comparison failed");
9105 if similarity.score < threshold {
9106 similarity
9109 .image
9110 .to_color_map()
9111 .save(format!("{name}.png"))
9112 .unwrap();
9113 panic!(
9114 "{name}: converted image and target image have similarity score too low: {} < {}",
9115 similarity.score, threshold
9116 )
9117 }
9118 }
9119
9120 #[test]
9125 fn test_nv12_image_creation() {
9126 let width = 640;
9127 let height = 480;
9128 let img = TensorDyn::image(
9129 width,
9130 height,
9131 PixelFormat::Nv12,
9132 DType::U8,
9133 None,
9134 edgefirst_tensor::CpuAccess::ReadWrite,
9135 )
9136 .unwrap();
9137
9138 assert_eq!(img.width(), Some(width));
9139 assert_eq!(img.height(), Some(height));
9140 assert_eq!(img.format().unwrap(), PixelFormat::Nv12);
9141 assert_eq!(img.as_u8().unwrap().shape(), &[height * 3 / 2, width]);
9143 }
9144
9145 #[test]
9146 fn test_nv12_channels() {
9147 let img = TensorDyn::image(
9148 640,
9149 480,
9150 PixelFormat::Nv12,
9151 DType::U8,
9152 None,
9153 edgefirst_tensor::CpuAccess::ReadWrite,
9154 )
9155 .unwrap();
9156 assert_eq!(img.format().unwrap().channels(), 1);
9158 }
9159
9160 #[test]
9165 fn test_tensor_set_format_planar() {
9166 let mut tensor = Tensor::<u8>::new(&[3, 480, 640], None, None).unwrap();
9167 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
9168 assert_eq!(tensor.format(), Some(PixelFormat::PlanarRgb));
9169 assert_eq!(tensor.width(), Some(640));
9170 assert_eq!(tensor.height(), Some(480));
9171 }
9172
9173 #[test]
9174 fn test_tensor_set_format_interleaved() {
9175 let mut tensor = Tensor::<u8>::new(&[480, 640, 4], None, None).unwrap();
9176 tensor.set_format(PixelFormat::Rgba).unwrap();
9177 assert_eq!(tensor.format(), Some(PixelFormat::Rgba));
9178 assert_eq!(tensor.width(), Some(640));
9179 assert_eq!(tensor.height(), Some(480));
9180 }
9181
9182 #[test]
9183 fn test_tensordyn_image_rgb() {
9184 let img = TensorDyn::image(
9185 640,
9186 480,
9187 PixelFormat::Rgb,
9188 DType::U8,
9189 None,
9190 edgefirst_tensor::CpuAccess::ReadWrite,
9191 )
9192 .unwrap();
9193 assert_eq!(img.width(), Some(640));
9194 assert_eq!(img.height(), Some(480));
9195 assert_eq!(img.format(), Some(PixelFormat::Rgb));
9196 }
9197
9198 #[test]
9199 fn test_tensordyn_image_planar_rgb() {
9200 let img = TensorDyn::image(
9201 640,
9202 480,
9203 PixelFormat::PlanarRgb,
9204 DType::U8,
9205 None,
9206 edgefirst_tensor::CpuAccess::ReadWrite,
9207 )
9208 .unwrap();
9209 assert_eq!(img.width(), Some(640));
9210 assert_eq!(img.height(), Some(480));
9211 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
9212 }
9213
9214 #[test]
9215 fn test_rgb_int8_format() {
9216 let img = TensorDyn::image(
9218 1280,
9219 720,
9220 PixelFormat::Rgb,
9221 DType::I8,
9222 Some(TensorMemory::Mem),
9223 edgefirst_tensor::CpuAccess::ReadWrite,
9224 )
9225 .unwrap();
9226 assert_eq!(img.width(), Some(1280));
9227 assert_eq!(img.height(), Some(720));
9228 assert_eq!(img.format(), Some(PixelFormat::Rgb));
9229 assert_eq!(img.dtype(), DType::I8);
9230 }
9231
9232 #[test]
9233 fn test_planar_rgb_int8_format() {
9234 let img = TensorDyn::image(
9235 1280,
9236 720,
9237 PixelFormat::PlanarRgb,
9238 DType::I8,
9239 Some(TensorMemory::Mem),
9240 edgefirst_tensor::CpuAccess::ReadWrite,
9241 )
9242 .unwrap();
9243 assert_eq!(img.width(), Some(1280));
9244 assert_eq!(img.height(), Some(720));
9245 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
9246 assert_eq!(img.dtype(), DType::I8);
9247 }
9248
9249 #[test]
9250 fn test_rgb_from_tensor() {
9251 let mut tensor = Tensor::<u8>::new(&[720, 1280, 3], None, None).unwrap();
9252 tensor.set_format(PixelFormat::Rgb).unwrap();
9253 let img = TensorDyn::from(tensor);
9254 assert_eq!(img.width(), Some(1280));
9255 assert_eq!(img.height(), Some(720));
9256 assert_eq!(img.format(), Some(PixelFormat::Rgb));
9257 }
9258
9259 #[test]
9260 fn test_planar_rgb_from_tensor() {
9261 let mut tensor = Tensor::<u8>::new(&[3, 720, 1280], None, None).unwrap();
9262 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
9263 let img = TensorDyn::from(tensor);
9264 assert_eq!(img.width(), Some(1280));
9265 assert_eq!(img.height(), Some(720));
9266 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
9267 }
9268
9269 #[test]
9270 fn test_dtype_determines_int8() {
9271 let u8_img = TensorDyn::image(
9273 64,
9274 64,
9275 PixelFormat::Rgb,
9276 DType::U8,
9277 None,
9278 edgefirst_tensor::CpuAccess::ReadWrite,
9279 )
9280 .unwrap();
9281 let i8_img = TensorDyn::image(
9282 64,
9283 64,
9284 PixelFormat::Rgb,
9285 DType::I8,
9286 None,
9287 edgefirst_tensor::CpuAccess::ReadWrite,
9288 )
9289 .unwrap();
9290 assert_eq!(u8_img.dtype(), DType::U8);
9291 assert_eq!(i8_img.dtype(), DType::I8);
9292 }
9293
9294 #[test]
9295 fn test_pixel_layout_packed_vs_planar() {
9296 assert_eq!(PixelFormat::Rgb.layout(), PixelLayout::Packed);
9298 assert_eq!(PixelFormat::Rgba.layout(), PixelLayout::Packed);
9299 assert_eq!(PixelFormat::PlanarRgb.layout(), PixelLayout::Planar);
9300 assert_eq!(PixelFormat::Nv12.layout(), PixelLayout::SemiPlanar);
9301 }
9302
9303 #[cfg(target_os = "linux")]
9308 #[cfg(feature = "opengl")]
9309 #[test]
9310 fn test_convert_pbo_to_pbo() {
9311 let mut converter = ImageProcessor::new().unwrap();
9312
9313 let is_pbo = converter
9315 .opengl
9316 .as_ref()
9317 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
9318 if !is_pbo {
9319 eprintln!("Skipping test_convert_pbo_to_pbo: backend is not PBO");
9320 return;
9321 }
9322
9323 let src_w = 640;
9324 let src_h = 480;
9325 let dst_w = 320;
9326 let dst_h = 240;
9327
9328 let pbo_src = converter
9330 .create_image(
9331 src_w,
9332 src_h,
9333 PixelFormat::Rgba,
9334 DType::U8,
9335 None,
9336 edgefirst_tensor::CpuAccess::ReadWrite,
9337 )
9338 .unwrap();
9339 assert_eq!(
9340 pbo_src.as_u8().unwrap().memory(),
9341 TensorMemory::Pbo,
9342 "create_image should produce a PBO tensor"
9343 );
9344
9345 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
9347 let jpeg_src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgba), None).unwrap();
9348
9349 let mem_src = TensorDyn::image(
9351 src_w,
9352 src_h,
9353 PixelFormat::Rgba,
9354 DType::U8,
9355 Some(TensorMemory::Mem),
9356 edgefirst_tensor::CpuAccess::ReadWrite,
9357 )
9358 .unwrap();
9359 let (result, _jpeg_src, mem_src) = convert_img(
9360 &mut CPUProcessor::new(),
9361 jpeg_src,
9362 mem_src,
9363 Rotation::None,
9364 Flip::None,
9365 Crop::no_crop(),
9366 );
9367 result.unwrap();
9368
9369 {
9371 let src_data = mem_src.as_u8().unwrap().map().unwrap();
9372 let mut pbo_map = pbo_src.as_u8().unwrap().map().unwrap();
9373 pbo_map.copy_from_slice(&src_data);
9374 }
9375
9376 let pbo_dst = converter
9378 .create_image(
9379 dst_w,
9380 dst_h,
9381 PixelFormat::Rgba,
9382 DType::U8,
9383 None,
9384 edgefirst_tensor::CpuAccess::ReadWrite,
9385 )
9386 .unwrap();
9387 assert_eq!(pbo_dst.as_u8().unwrap().memory(), TensorMemory::Pbo);
9388
9389 let mut pbo_dst = pbo_dst;
9391 let result = converter.convert(
9392 &pbo_src,
9393 &mut pbo_dst,
9394 Rotation::None,
9395 Flip::None,
9396 Crop::no_crop(),
9397 );
9398 result.unwrap();
9399
9400 let cpu_dst = TensorDyn::image(
9402 dst_w,
9403 dst_h,
9404 PixelFormat::Rgba,
9405 DType::U8,
9406 Some(TensorMemory::Mem),
9407 edgefirst_tensor::CpuAccess::ReadWrite,
9408 )
9409 .unwrap();
9410 let (result, _mem_src, cpu_dst) = convert_img(
9411 &mut CPUProcessor::new(),
9412 mem_src,
9413 cpu_dst,
9414 Rotation::None,
9415 Flip::None,
9416 Crop::no_crop(),
9417 );
9418 result.unwrap();
9419
9420 let pbo_dst_img = {
9421 let mut __t = pbo_dst.into_u8().unwrap();
9422 __t.set_format(PixelFormat::Rgba).unwrap();
9423 TensorDyn::from(__t)
9424 };
9425 compare_images(&pbo_dst_img, &cpu_dst, 0.95, function!());
9426 log::info!("test_convert_pbo_to_pbo: PASS — PBO-to-PBO convert matches CPU reference");
9427 }
9428
9429 #[test]
9430 fn test_image_bgra() {
9431 let img = TensorDyn::image(
9432 640,
9433 480,
9434 PixelFormat::Bgra,
9435 DType::U8,
9436 Some(edgefirst_tensor::TensorMemory::Mem),
9437 edgefirst_tensor::CpuAccess::ReadWrite,
9438 )
9439 .unwrap();
9440 assert_eq!(img.width(), Some(640));
9441 assert_eq!(img.height(), Some(480));
9442 assert_eq!(img.format().unwrap().channels(), 4);
9443 assert_eq!(img.format().unwrap(), PixelFormat::Bgra);
9444 }
9445
9446 #[test]
9451 fn test_force_backend_cpu() {
9452 let _lock = acquire_env_lock();
9453 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
9454 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
9455 let converter = ImageProcessor::new().unwrap();
9456 assert!(converter.cpu.is_some());
9457 assert_eq!(converter.forced_backend, Some(ForcedBackend::Cpu));
9458 }
9459
9460 #[test]
9461 fn test_force_backend_invalid() {
9462 let _lock = acquire_env_lock();
9463 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
9464 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "invalid") };
9465 let result = ImageProcessor::new();
9466 assert!(
9467 matches!(&result, Err(Error::ForcedBackendUnavailable(s)) if s.contains("unknown")),
9468 "invalid backend value should return ForcedBackendUnavailable error: {result:?}"
9469 );
9470 }
9471
9472 #[test]
9473 fn test_force_backend_unset() {
9474 let _lock = acquire_env_lock();
9475 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
9476 unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") };
9477 let converter = ImageProcessor::new().unwrap();
9478 assert!(converter.forced_backend.is_none());
9479 }
9480
9481 #[test]
9486 fn test_draw_proto_masks_no_cpu_returns_error() {
9487 let _lock = acquire_env_lock();
9489 let _guard = EnvGuard::snapshot(&[
9490 "EDGEFIRST_FORCE_BACKEND",
9491 "EDGEFIRST_DISABLE_GL",
9492 "EDGEFIRST_DISABLE_G2D",
9493 "EDGEFIRST_DISABLE_CPU",
9494 ]);
9495
9496 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
9498 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
9499 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
9500
9501 let mut converter = ImageProcessor::new().unwrap();
9502 assert!(converter.cpu.is_none(), "CPU should be disabled");
9503
9504 let dst = TensorDyn::image(
9505 640,
9506 480,
9507 PixelFormat::Rgba,
9508 DType::U8,
9509 Some(TensorMemory::Mem),
9510 edgefirst_tensor::CpuAccess::ReadWrite,
9511 )
9512 .unwrap();
9513 let mut dst_dyn = dst;
9514 let det = [DetectBox {
9515 bbox: edgefirst_decoder::BoundingBox {
9516 xmin: 0.1,
9517 ymin: 0.1,
9518 xmax: 0.5,
9519 ymax: 0.5,
9520 },
9521 score: 0.9,
9522 label: 0,
9523 }];
9524 let proto_data = {
9525 use edgefirst_tensor::{Tensor, TensorDyn};
9526 let coeff_t = Tensor::<f32>::from_slice(&[0.5_f32; 4], &[1, 4]).unwrap();
9527 let protos_t =
9528 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
9529 ProtoData {
9530 mask_coefficients: TensorDyn::F32(coeff_t),
9531 protos: TensorDyn::F32(protos_t),
9532 layout: ProtoLayout::Nhwc,
9533 }
9534 };
9535 let result =
9536 converter.draw_proto_masks(&mut dst_dyn, &det, &proto_data, Default::default());
9537 assert!(
9538 matches!(&result, Err(Error::Internal(s)) if s.contains("CPU backend")),
9539 "draw_proto_masks without CPU should return Internal error: {result:?}"
9540 );
9541 }
9542
9543 #[test]
9544 fn test_draw_proto_masks_cpu_fallback_works() {
9545 let _lock = acquire_env_lock();
9548 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
9549 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
9550 let mut converter = ImageProcessor::new().unwrap();
9551 assert!(converter.cpu.is_some());
9552
9553 let dst = TensorDyn::image(
9554 64,
9555 64,
9556 PixelFormat::Rgba,
9557 DType::U8,
9558 Some(TensorMemory::Mem),
9559 edgefirst_tensor::CpuAccess::ReadWrite,
9560 )
9561 .unwrap();
9562 let mut dst_dyn = dst;
9563 let det = [DetectBox {
9564 bbox: edgefirst_decoder::BoundingBox {
9565 xmin: 0.1,
9566 ymin: 0.1,
9567 xmax: 0.5,
9568 ymax: 0.5,
9569 },
9570 score: 0.9,
9571 label: 0,
9572 }];
9573 let proto_data = {
9574 use edgefirst_tensor::{Tensor, TensorDyn};
9575 let coeff_t = Tensor::<f32>::from_slice(&[0.5_f32; 4], &[1, 4]).unwrap();
9576 let protos_t =
9577 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
9578 ProtoData {
9579 mask_coefficients: TensorDyn::F32(coeff_t),
9580 protos: TensorDyn::F32(protos_t),
9581 layout: ProtoLayout::Nhwc,
9582 }
9583 };
9584 let result =
9585 converter.draw_proto_masks(&mut dst_dyn, &det, &proto_data, Default::default());
9586 assert!(result.is_ok(), "CPU fallback path should work: {result:?}");
9587 }
9588
9589 fn acquire_env_lock() -> std::sync::MutexGuard<'static, ()> {
9621 use std::sync::{Mutex, OnceLock};
9622 static ENV_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
9623 ENV_MUTEX
9624 .get_or_init(|| Mutex::new(()))
9625 .lock()
9626 .unwrap_or_else(|e| e.into_inner())
9627 }
9628
9629 struct EnvGuard {
9632 vars: Vec<(&'static str, Option<String>)>,
9633 }
9634
9635 impl EnvGuard {
9636 fn snapshot(names: &[&'static str]) -> Self {
9640 Self {
9641 vars: names.iter().map(|&k| (k, std::env::var(k).ok())).collect(),
9642 }
9643 }
9644 }
9645
9646 impl Drop for EnvGuard {
9647 fn drop(&mut self) {
9648 for (k, v) in &self.vars {
9649 match v {
9650 Some(s) => unsafe { std::env::set_var(k, s) },
9651 None => unsafe { std::env::remove_var(k) },
9652 }
9653 }
9654 }
9655 }
9656
9657 fn with_force_backend<R>(value: Option<&str>, body: impl FnOnce() -> R) -> R {
9661 let _lock = acquire_env_lock();
9662 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
9663 match value {
9664 Some(v) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", v) },
9665 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
9666 }
9667 body()
9668 }
9669
9670 fn make_dirty_dst(w: usize, h: usize, mem: Option<TensorMemory>) -> TensorDyn {
9675 let dst = TensorDyn::image(
9676 w,
9677 h,
9678 PixelFormat::Rgba,
9679 DType::U8,
9680 mem,
9681 edgefirst_tensor::CpuAccess::ReadWrite,
9682 )
9683 .unwrap();
9684 {
9685 use edgefirst_tensor::TensorMapTrait;
9686 let u8t = dst.as_u8().unwrap();
9687 let mut map = u8t.map().unwrap();
9688 for (i, b) in map.as_mut_slice().iter_mut().enumerate() {
9689 *b = 0xA0u8.wrapping_add((i as u8) & 0x3F);
9690 }
9691 }
9692 dst
9693 }
9694
9695 fn make_bg(w: usize, h: usize, mem: Option<TensorMemory>, rgba: [u8; 4]) -> TensorDyn {
9697 let bg = TensorDyn::image(
9698 w,
9699 h,
9700 PixelFormat::Rgba,
9701 DType::U8,
9702 mem,
9703 edgefirst_tensor::CpuAccess::ReadWrite,
9704 )
9705 .unwrap();
9706 {
9707 use edgefirst_tensor::TensorMapTrait;
9708 let u8t = bg.as_u8().unwrap();
9709 let mut map = u8t.map().unwrap();
9710 for chunk in map.as_mut_slice().chunks_exact_mut(4) {
9711 chunk.copy_from_slice(&rgba);
9712 }
9713 }
9714 bg
9715 }
9716
9717 fn pixel_at(dst: &TensorDyn, x: usize, y: usize) -> [u8; 4] {
9718 use edgefirst_tensor::TensorMapTrait;
9719 let w = dst.width().unwrap();
9720 let off = (y * w + x) * 4;
9721 let u8t = dst.as_u8().unwrap();
9722 let map = u8t.map().unwrap();
9723 let s = map.as_slice();
9724 [s[off], s[off + 1], s[off + 2], s[off + 3]]
9725 }
9726
9727 fn assert_every_pixel_eq(dst: &TensorDyn, expected: [u8; 4], case: &str) {
9728 use edgefirst_tensor::TensorMapTrait;
9729 let u8t = dst.as_u8().unwrap();
9730 let map = u8t.map().unwrap();
9731 for (i, chunk) in map.as_slice().chunks_exact(4).enumerate() {
9732 assert_eq!(
9733 chunk, &expected,
9734 "{case}: pixel idx {i} = {chunk:?}, expected {expected:?}"
9735 );
9736 }
9737 }
9738
9739 fn scenario_empty_no_bg(processor: &mut ImageProcessor, case: &str) {
9742 let mut dst = make_dirty_dst(64, 64, None);
9743 processor
9744 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::default())
9745 .unwrap_or_else(|e| panic!("{case}/decoded_masks empty+no-bg failed: {e:?}"));
9746 assert_every_pixel_eq(&dst, [0, 0, 0, 0], &format!("{case}/decoded"));
9747
9748 let mut dst = make_dirty_dst(64, 64, None);
9749 let proto = {
9750 use edgefirst_tensor::{Tensor, TensorDyn};
9751 let coeff_t = Tensor::<f32>::from_slice(&[0.0_f32; 4], &[1, 4]).unwrap();
9753 let protos_t =
9754 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
9755 ProtoData {
9756 mask_coefficients: TensorDyn::F32(coeff_t),
9757 protos: TensorDyn::F32(protos_t),
9758 layout: ProtoLayout::Nhwc,
9759 }
9760 };
9761 processor
9762 .draw_proto_masks(&mut dst, &[], &proto, MaskOverlay::default())
9763 .unwrap_or_else(|e| panic!("{case}/proto_masks empty+no-bg failed: {e:?}"));
9764 assert_every_pixel_eq(&dst, [0, 0, 0, 0], &format!("{case}/proto"));
9765 }
9766
9767 fn scenario_empty_with_bg(processor: &mut ImageProcessor, case: &str) {
9770 let bg_color = [42, 99, 200, 255];
9771 let bg = make_bg(64, 64, None, bg_color);
9772 let overlay = MaskOverlay::new().with_background(&bg);
9773
9774 let mut dst = make_dirty_dst(64, 64, None);
9775 processor
9776 .draw_decoded_masks(&mut dst, &[], &[], overlay)
9777 .unwrap_or_else(|e| panic!("{case}/decoded_masks empty+bg failed: {e:?}"));
9778 assert_every_pixel_eq(&dst, bg_color, &format!("{case}/decoded bg blit"));
9779
9780 let mut dst = make_dirty_dst(64, 64, None);
9781 let proto = {
9782 use edgefirst_tensor::{Tensor, TensorDyn};
9783 let coeff_t = Tensor::<f32>::from_slice(&[0.0_f32; 4], &[1, 4]).unwrap();
9785 let protos_t =
9786 Tensor::<f32>::from_slice(&vec![0.0_f32; 8 * 8 * 4], &[8, 8, 4]).unwrap();
9787 ProtoData {
9788 mask_coefficients: TensorDyn::F32(coeff_t),
9789 protos: TensorDyn::F32(protos_t),
9790 layout: ProtoLayout::Nhwc,
9791 }
9792 };
9793 processor
9794 .draw_proto_masks(&mut dst, &[], &proto, overlay)
9795 .unwrap_or_else(|e| panic!("{case}/proto_masks empty+bg failed: {e:?}"));
9796 assert_every_pixel_eq(&dst, bg_color, &format!("{case}/proto bg blit"));
9797 }
9798
9799 fn scenario_detect_no_bg(processor: &mut ImageProcessor, case: &str) {
9803 use edgefirst_decoder::Segmentation;
9804 use ndarray::Array3;
9805 processor
9806 .set_class_colors(&[[200, 80, 40, 255]])
9807 .expect("set_class_colors");
9808
9809 let detect = DetectBox {
9810 bbox: [0.25, 0.25, 0.75, 0.75].into(),
9811 score: 0.99,
9812 label: 0,
9813 };
9814 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
9815 let seg = Segmentation {
9816 segmentation: seg_arr,
9817 xmin: 0.25,
9818 ymin: 0.25,
9819 xmax: 0.75,
9820 ymax: 0.75,
9821 };
9822
9823 let mut dst = make_dirty_dst(64, 64, None);
9824 processor
9825 .draw_decoded_masks(&mut dst, &[detect], &[seg], MaskOverlay::default())
9826 .unwrap_or_else(|e| panic!("{case}/decoded_masks detect+no-bg failed: {e:?}"));
9827
9828 let corner = pixel_at(&dst, 2, 2);
9830 assert_eq!(
9831 corner,
9832 [0, 0, 0, 0],
9833 "{case}/decoded: corner (2,2) leaked dirty pattern: {corner:?}"
9834 );
9835 let center = pixel_at(&dst, 32, 32);
9839 assert!(
9840 center != [0, 0, 0, 0],
9841 "{case}/decoded: center (32,32) was not coloured: {center:?}"
9842 );
9843 }
9844
9845 fn scenario_detect_with_bg(processor: &mut ImageProcessor, case: &str) {
9848 use edgefirst_decoder::Segmentation;
9849 use ndarray::Array3;
9850 processor
9851 .set_class_colors(&[[200, 80, 40, 255]])
9852 .expect("set_class_colors");
9853 let bg_color = [10, 20, 30, 255];
9854 let bg = make_bg(64, 64, None, bg_color);
9855
9856 let detect = DetectBox {
9857 bbox: [0.25, 0.25, 0.75, 0.75].into(),
9858 score: 0.99,
9859 label: 0,
9860 };
9861 let seg_arr = Array3::from_shape_fn((4, 4, 1), |_| 255u8);
9862 let seg = Segmentation {
9863 segmentation: seg_arr,
9864 xmin: 0.25,
9865 ymin: 0.25,
9866 xmax: 0.75,
9867 ymax: 0.75,
9868 };
9869
9870 let overlay = MaskOverlay::new().with_background(&bg);
9871 let mut dst = make_dirty_dst(64, 64, None);
9872 processor
9873 .draw_decoded_masks(&mut dst, &[detect], &[seg], overlay)
9874 .unwrap_or_else(|e| panic!("{case}/decoded_masks detect+bg failed: {e:?}"));
9875
9876 let corner = pixel_at(&dst, 2, 2);
9878 assert_eq!(
9879 corner, bg_color,
9880 "{case}/decoded: corner (2,2) should show bg {bg_color:?} got {corner:?}"
9881 );
9882 let center = pixel_at(&dst, 32, 32);
9885 assert!(
9886 center != bg_color,
9887 "{case}/decoded: center (32,32) should differ from bg {bg_color:?}, got {center:?}"
9888 );
9889 }
9890
9891 fn run_all_scenarios(
9894 force_backend: Option<&'static str>,
9895 case: &'static str,
9896 require_dma_for_bg: bool,
9897 ) {
9898 if require_dma_for_bg && !edgefirst_tensor::is_dma_available() {
9899 eprintln!("SKIPPED: {case} — DMA not available on this host");
9900 return;
9901 }
9902 let processor_result = with_force_backend(force_backend, ImageProcessor::new);
9903 let mut processor = match processor_result {
9904 Ok(p) => p,
9905 Err(e) => {
9906 eprintln!("SKIPPED: {case} — backend init failed: {e:?}");
9907 return;
9908 }
9909 };
9910 scenario_empty_no_bg(&mut processor, case);
9911 scenario_empty_with_bg(&mut processor, case);
9912 scenario_detect_no_bg(&mut processor, case);
9913 scenario_detect_with_bg(&mut processor, case);
9914 }
9915
9916 #[test]
9917 fn test_draw_masks_4_scenarios_cpu() {
9918 run_all_scenarios(Some("cpu"), "cpu", false);
9919 }
9920
9921 #[test]
9922 fn test_draw_masks_4_scenarios_auto() {
9923 run_all_scenarios(None, "auto", false);
9924 }
9925
9926 #[cfg(target_os = "linux")]
9927 #[cfg(feature = "opengl")]
9928 #[test]
9929 fn test_draw_masks_4_scenarios_opengl() {
9930 run_all_scenarios(Some("opengl"), "opengl", false);
9931 }
9932
9933 #[cfg(target_os = "linux")]
9938 #[test]
9939 fn test_draw_masks_zero_detection_g2d_forced() {
9940 if !edgefirst_tensor::is_dma_available() {
9941 eprintln!("SKIPPED: g2d forced — DMA not available on this host");
9942 return;
9943 }
9944 let processor_result = with_force_backend(Some("g2d"), ImageProcessor::new);
9945 let mut processor = match processor_result {
9946 Ok(p) => p,
9947 Err(e) => {
9948 eprintln!("SKIPPED: g2d forced — init failed: {e:?}");
9949 return;
9950 }
9951 };
9952
9953 let mut dst = TensorDyn::image(
9955 64,
9956 64,
9957 PixelFormat::Rgba,
9958 DType::U8,
9959 Some(TensorMemory::Dma),
9960 edgefirst_tensor::CpuAccess::ReadWrite,
9961 )
9962 .unwrap();
9963 {
9964 use edgefirst_tensor::TensorMapTrait;
9965 let u8t = dst.as_u8_mut().unwrap();
9966 let mut map = u8t.map().unwrap();
9967 map.as_mut_slice().fill(0xBB);
9968 }
9969 processor
9970 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::default())
9971 .expect("g2d empty+no-bg");
9972 assert_every_pixel_eq(&dst, [0, 0, 0, 0], "g2d/case1 cleared");
9973
9974 let bg_color = [7, 11, 13, 255];
9976 let bg = {
9977 let t = TensorDyn::image(
9978 64,
9979 64,
9980 PixelFormat::Rgba,
9981 DType::U8,
9982 Some(TensorMemory::Dma),
9983 edgefirst_tensor::CpuAccess::ReadWrite,
9984 )
9985 .unwrap();
9986 {
9987 use edgefirst_tensor::TensorMapTrait;
9988 let u8t = t.as_u8().unwrap();
9989 let mut map = u8t.map().unwrap();
9990 for chunk in map.as_mut_slice().chunks_exact_mut(4) {
9991 chunk.copy_from_slice(&bg_color);
9992 }
9993 }
9994 t
9995 };
9996 let mut dst = TensorDyn::image(
9997 64,
9998 64,
9999 PixelFormat::Rgba,
10000 DType::U8,
10001 Some(TensorMemory::Dma),
10002 edgefirst_tensor::CpuAccess::ReadWrite,
10003 )
10004 .unwrap();
10005 {
10006 use edgefirst_tensor::TensorMapTrait;
10007 let u8t = dst.as_u8_mut().unwrap();
10008 let mut map = u8t.map().unwrap();
10009 map.as_mut_slice().fill(0x55);
10010 }
10011 processor
10012 .draw_decoded_masks(&mut dst, &[], &[], MaskOverlay::new().with_background(&bg))
10013 .expect("g2d empty+bg");
10014 assert_every_pixel_eq(&dst, bg_color, "g2d/case2 bg blit");
10015
10016 let detect = DetectBox {
10018 bbox: [0.25, 0.25, 0.75, 0.75].into(),
10019 score: 0.9,
10020 label: 0,
10021 };
10022 let mut dst = TensorDyn::image(
10023 64,
10024 64,
10025 PixelFormat::Rgba,
10026 DType::U8,
10027 Some(TensorMemory::Dma),
10028 edgefirst_tensor::CpuAccess::ReadWrite,
10029 )
10030 .unwrap();
10031 let err = processor
10032 .draw_decoded_masks(&mut dst, &[detect], &[], MaskOverlay::default())
10033 .expect_err("g2d must reject detect-present draw_decoded_masks");
10034 assert!(
10035 matches!(err, Error::NotImplemented(_)),
10036 "g2d case3 wrong error: {err:?}"
10037 );
10038 }
10039
10040 #[test]
10041 fn test_set_format_then_cpu_convert() {
10042 let _lock = acquire_env_lock();
10045 let _guard = EnvGuard::snapshot(&["EDGEFIRST_FORCE_BACKEND"]);
10046 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
10047 let mut processor = ImageProcessor::new().unwrap();
10048
10049 let image = edgefirst_bench::testdata::read("zidane.jpg");
10051 let src = load_image_test_helper(&image, Some(PixelFormat::Rgba), None).unwrap();
10052
10053 let mut dst =
10055 TensorDyn::new(&[640, 640, 3], DType::U8, Some(TensorMemory::Mem), None).unwrap();
10056 dst.set_format(PixelFormat::Rgb).unwrap();
10057
10058 processor
10060 .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
10061 .unwrap();
10062
10063 assert_eq!(dst.format(), Some(PixelFormat::Rgb));
10065 assert_eq!(dst.width(), Some(640));
10066 assert_eq!(dst.height(), Some(640));
10067 }
10068
10069 #[test]
10075 fn test_multiple_image_processors_same_thread() {
10076 let _lock = acquire_env_lock();
10079 let mut processors: Vec<ImageProcessor> = (0..4)
10080 .map(|_| ImageProcessor::new().expect("ImageProcessor::new() failed"))
10081 .collect();
10082
10083 for proc in &mut processors {
10084 let src = proc
10085 .create_image(
10086 128,
10087 128,
10088 PixelFormat::Rgb,
10089 DType::U8,
10090 None,
10091 edgefirst_tensor::CpuAccess::ReadWrite,
10092 )
10093 .expect("create src failed");
10094 let mut dst = proc
10095 .create_image(
10096 64,
10097 64,
10098 PixelFormat::Rgb,
10099 DType::U8,
10100 None,
10101 edgefirst_tensor::CpuAccess::ReadWrite,
10102 )
10103 .expect("create dst failed");
10104 proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
10105 .expect("convert failed");
10106 assert_eq!(dst.width(), Some(64));
10107 assert_eq!(dst.height(), Some(64));
10108 }
10109 }
10110
10111 #[test]
10118 fn test_multiple_image_processors_separate_threads() {
10119 use std::sync::mpsc;
10120 use std::time::Duration;
10121
10122 if std::env::var_os("EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS").is_some() {
10132 eprintln!(
10133 "SKIPPED: test_multiple_image_processors_separate_threads — known Vivante \
10134 GC7000UL concurrent-EGL-teardown double-free \
10135 (EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS set)"
10136 );
10137 return;
10138 }
10139
10140 const TIMEOUT: Duration = Duration::from_secs(60);
10141
10142 let _lock = acquire_env_lock();
10145
10146 let (tx, rx) = mpsc::channel::<()>();
10147
10148 std::thread::spawn(move || {
10149 let handles: Vec<_> = (0..4)
10150 .map(|i| {
10151 std::thread::spawn(move || {
10152 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
10153 panic!("ImageProcessor::new() failed on thread {i}: {e}")
10154 });
10155 let src = proc
10156 .create_image(
10157 128,
10158 128,
10159 PixelFormat::Rgb,
10160 DType::U8,
10161 None,
10162 edgefirst_tensor::CpuAccess::ReadWrite,
10163 )
10164 .unwrap_or_else(|e| panic!("create src failed on thread {i}: {e}"));
10165 let mut dst = proc
10166 .create_image(
10167 64,
10168 64,
10169 PixelFormat::Rgb,
10170 DType::U8,
10171 None,
10172 edgefirst_tensor::CpuAccess::ReadWrite,
10173 )
10174 .unwrap_or_else(|e| panic!("create dst failed on thread {i}: {e}"));
10175 proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
10176 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
10177 assert_eq!(dst.width(), Some(64));
10178 assert_eq!(dst.height(), Some(64));
10179 })
10180 })
10181 .collect();
10182
10183 for (i, h) in handles.into_iter().enumerate() {
10184 h.join()
10185 .unwrap_or_else(|e| panic!("thread {i} panicked: {e:?}"));
10186 }
10187
10188 let _ = tx.send(());
10189 });
10190
10191 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
10192 panic!("test_multiple_image_processors_separate_threads timed out after {TIMEOUT:?}")
10193 });
10194 }
10195
10196 #[test]
10203 fn test_image_processors_concurrent_operations() {
10204 use std::sync::{mpsc, Arc, Barrier};
10205 use std::time::Duration;
10206
10207 const N: usize = 4;
10208 const ROUNDS: usize = 10;
10209 const TIMEOUT: Duration = Duration::from_secs(60);
10210
10211 let _lock = acquire_env_lock();
10214
10215 let (tx, rx) = mpsc::channel::<()>();
10216
10217 std::thread::spawn(move || {
10218 let barrier = Arc::new(Barrier::new(N));
10219
10220 let handles: Vec<_> = (0..N)
10221 .map(|i| {
10222 let barrier = Arc::clone(&barrier);
10223 std::thread::spawn(move || {
10224 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
10225 panic!("ImageProcessor::new() failed on thread {i}: {e}")
10226 });
10227
10228 barrier.wait();
10230
10231 for round in 0..ROUNDS {
10233 let src = proc
10234 .create_image(
10235 128,
10236 128,
10237 PixelFormat::Rgb,
10238 DType::U8,
10239 None,
10240 edgefirst_tensor::CpuAccess::ReadWrite,
10241 )
10242 .unwrap_or_else(|e| {
10243 panic!("create src failed on thread {i} round {round}: {e}")
10244 });
10245 let mut dst = proc
10246 .create_image(
10247 64,
10248 64,
10249 PixelFormat::Rgb,
10250 DType::U8,
10251 None,
10252 edgefirst_tensor::CpuAccess::ReadWrite,
10253 )
10254 .unwrap_or_else(|e| {
10255 panic!("create dst failed on thread {i} round {round}: {e}")
10256 });
10257 proc.convert(
10258 &src,
10259 &mut dst,
10260 Rotation::None,
10261 Flip::None,
10262 Crop::default(),
10263 )
10264 .unwrap_or_else(|e| {
10265 panic!("convert failed on thread {i} round {round}: {e}")
10266 });
10267 assert_eq!(dst.width(), Some(64));
10268 assert_eq!(dst.height(), Some(64));
10269 }
10270 })
10271 })
10272 .collect();
10273
10274 for (i, h) in handles.into_iter().enumerate() {
10275 h.join()
10276 .unwrap_or_else(|e| panic!("thread {i} panicked: {e:?}"));
10277 }
10278
10279 let _ = tx.send(());
10280 });
10281
10282 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
10283 panic!("test_image_processors_concurrent_operations timed out after {TIMEOUT:?}")
10284 });
10285 }
10286
10287 #[test]
10305 fn test_parallel_processors_unique_outputs() {
10306 use std::sync::{mpsc, Arc, Barrier};
10307 use std::time::Duration;
10308
10309 const N: usize = 4;
10310 const ROUNDS: usize = 25;
10311 const TIMEOUT: Duration = Duration::from_secs(60);
10312
10313 if std::env::var_os("EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS").is_some() {
10314 eprintln!(
10315 "SKIPPED: test_parallel_processors_unique_outputs — known Vivante \
10316 GC7000UL concurrent-multi-processor driver abort \
10317 (EDGEFIRST_SKIP_VIVANTE_KNOWN_BUGS set)"
10318 );
10319 return;
10320 }
10321
10322 let _lock = acquire_env_lock();
10323 let (tx, rx) = mpsc::channel::<()>();
10324
10325 std::thread::spawn(move || {
10326 let barrier = Arc::new(Barrier::new(N));
10327 let handles: Vec<_> = (0..N)
10328 .map(|i| {
10329 let barrier = Arc::clone(&barrier);
10330 std::thread::spawn(move || {
10331 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
10332 panic!("ImageProcessor::new() failed on thread {i}: {e}")
10333 });
10334 let (w, h) = (640usize, 480usize);
10336 let mem = if edgefirst_tensor::is_dma_available() {
10337 Some(TensorMemory::Dma)
10338 } else {
10339 Some(TensorMemory::Mem)
10340 };
10341 let src = proc
10342 .create_image(
10343 w,
10344 h,
10345 PixelFormat::Nv12,
10346 DType::U8,
10347 mem,
10348 edgefirst_tensor::CpuAccess::ReadWrite,
10349 )
10350 .unwrap();
10351 {
10352 let t = src.as_u8().unwrap();
10353 let mut m = t.map().unwrap();
10354 let s = m.as_mut_slice();
10355 for (j, b) in s[..w * h].iter_mut().enumerate() {
10356 *b = ((i * 53 + j) % 200 + 16) as u8;
10357 }
10358 for b in &mut s[w * h..] {
10359 *b = (80 + i * 24) as u8;
10360 }
10361 }
10362 let lb = Crop::letterbox([114, 114, 114, 255]);
10363 let convert_once = |proc: &mut ImageProcessor| -> Vec<u8> {
10364 let mut dst = proc
10365 .create_image(
10366 320,
10367 320,
10368 PixelFormat::Rgba,
10369 DType::U8,
10370 mem,
10371 edgefirst_tensor::CpuAccess::ReadWrite,
10372 )
10373 .unwrap();
10374 proc.convert(&src, &mut dst, Rotation::None, Flip::None, lb)
10375 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
10376 let t = dst.as_u8().unwrap();
10377 let m = t.map().unwrap();
10378 m.as_slice().to_vec()
10379 };
10380
10381 let oracle = convert_once(&mut proc);
10382 barrier.wait();
10383 for round in 0..ROUNDS {
10384 let out = convert_once(&mut proc);
10385 let diffs = oracle.iter().zip(&out).filter(|(a, b)| a != b).count();
10386 assert!(
10387 diffs == 0,
10388 "thread {i} round {round}: {diffs}/{} bytes diverged \
10389 from this processor's own oracle — cross-processor \
10390 GL state leakage under parallel execution",
10391 oracle.len()
10392 );
10393 }
10394 })
10395 })
10396 .collect();
10397
10398 for (i, h) in handles.into_iter().enumerate() {
10399 h.join()
10400 .unwrap_or_else(|e| panic!("parallel thread {i} panicked: {e:?}"));
10401 }
10402 let _ = tx.send(());
10403 });
10404
10405 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
10406 panic!("test_parallel_processors_unique_outputs timed out after {TIMEOUT:?}")
10407 });
10408 }
10409
10410 #[test]
10419 #[ignore = "heavy on-demand GL-parallelism stressor; run explicitly on boards"]
10420 fn stress_parallel_processors_oracle() {
10421 use std::sync::{mpsc, Arc, Barrier};
10422 use std::time::Duration;
10423
10424 const N: usize = 4;
10425 const ROUNDS: usize = 200;
10426 const TIMEOUT: Duration = Duration::from_secs(600);
10427
10428 let _lock = acquire_env_lock();
10429 let (tx, rx) = mpsc::channel::<()>();
10430
10431 std::thread::spawn(move || {
10432 let barrier = Arc::new(Barrier::new(N));
10433 let handles: Vec<_> = (0..N)
10434 .map(|i| {
10435 let barrier = Arc::clone(&barrier);
10436 std::thread::spawn(move || {
10437 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
10438 panic!("ImageProcessor::new() failed on thread {i}: {e}")
10439 });
10440 let (w, h) = (1280usize, 720usize);
10441 let mem = if edgefirst_tensor::is_dma_available() {
10442 Some(TensorMemory::Dma)
10443 } else {
10444 Some(TensorMemory::Mem)
10445 };
10446
10447 let src = proc
10450 .create_image(
10451 w,
10452 h,
10453 PixelFormat::Nv12,
10454 DType::U8,
10455 mem,
10456 edgefirst_tensor::CpuAccess::ReadWrite,
10457 )
10458 .unwrap();
10459 {
10460 let t = src.as_u8().unwrap();
10461 let mut m = t.map().unwrap();
10462 let s = m.as_mut_slice();
10463 for (j, b) in s[..w * h].iter_mut().enumerate() {
10464 *b = ((i * 37 + j) % 200 + 16) as u8;
10465 }
10466 for b in &mut s[w * h..] {
10467 *b = (96 + i * 16) as u8;
10468 }
10469 }
10470 let lb = Crop::letterbox([114, 114, 114, 255]);
10471
10472 let convert_once = |proc: &mut ImageProcessor| -> Vec<u8> {
10473 let mut dst = proc
10474 .create_image(
10475 640,
10476 640,
10477 PixelFormat::Rgb,
10478 DType::U8,
10479 mem,
10480 edgefirst_tensor::CpuAccess::ReadWrite,
10481 )
10482 .unwrap();
10483 proc.convert(&src, &mut dst, Rotation::None, Flip::None, lb)
10484 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
10485 let t = dst.as_u8().unwrap();
10486 let m = t.map().unwrap();
10487 m.as_slice().to_vec()
10488 };
10489
10490 let oracle = convert_once(&mut proc);
10491 barrier.wait();
10492 for round in 0..ROUNDS {
10493 let out = convert_once(&mut proc);
10494 let diffs = oracle.iter().zip(&out).filter(|(a, b)| a != b).count();
10495 assert!(
10496 diffs == 0,
10497 "thread {i} round {round}: {diffs}/{} bytes diverged \
10498 from the pre-barrier oracle",
10499 oracle.len()
10500 );
10501 }
10502 })
10503 })
10504 .collect();
10505
10506 for (i, h) in handles.into_iter().enumerate() {
10507 h.join()
10508 .unwrap_or_else(|e| panic!("stressor thread {i} panicked: {e:?}"));
10509 }
10510 let _ = tx.send(());
10511 });
10512
10513 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
10514 panic!("stress_parallel_processors_oracle timed out after {TIMEOUT:?}")
10515 });
10516 }
10517
10518 #[test]
10531 fn convert_f32_auto_never_errors_non_gl_combo() {
10532 const W: usize = 64;
10533 const H: usize = 64;
10534
10535 let src = TensorDyn::image(
10538 W,
10539 H,
10540 PixelFormat::Yuyv,
10541 DType::U8,
10542 Some(TensorMemory::Mem),
10543 edgefirst_tensor::CpuAccess::ReadWrite,
10544 )
10545 .unwrap();
10546 {
10547 let mut map = src.as_u8().unwrap().map().unwrap();
10548 let data = map.as_mut_slice();
10549 for chunk in data.chunks_exact_mut(4) {
10550 chunk[0] = 128; chunk[1] = 128; chunk[2] = 160; chunk[3] = 128; }
10555 }
10556
10557 let mut dst = TensorDyn::image(
10558 W,
10559 H,
10560 PixelFormat::Rgb,
10561 DType::F32,
10562 Some(TensorMemory::Mem),
10563 edgefirst_tensor::CpuAccess::ReadWrite,
10564 )
10565 .unwrap();
10566
10567 let mut proc = ImageProcessor::new().unwrap();
10568 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
10569 assert!(
10570 result.is_ok(),
10571 "auto-chain Yuyv→Rgb F32 must not error: {:?}",
10572 result.err()
10573 );
10574
10575 let map = dst.as_f32().unwrap().map().unwrap();
10577 let floats = map.as_slice();
10578 assert_eq!(floats.len(), W * H * 3, "unexpected output element count");
10579 for (i, &v) in floats.iter().enumerate() {
10580 assert!(
10581 v.is_finite() && (0.0..=1.0).contains(&v),
10582 "output[{i}]={v} is not finite or not in [0,1]"
10583 );
10584 }
10585
10586 let first_non_zero = floats.iter().find(|&&v| v > 0.01);
10590 assert!(
10591 first_non_zero.is_some(),
10592 "all-zero output detected — CPU path likely did not write to the destination buffer"
10593 );
10594 let r0 = floats[0];
10597 assert!(
10598 (r0 - 0.502_f32).abs() < 0.05,
10599 "first pixel R={r0} expected ≈0.502 (Y=128 neutral grey from YUYV source)"
10600 );
10601 }
10602
10603 #[test]
10609 #[allow(clippy::needless_update)]
10615 fn convert_f16_forced_cpu_correct() {
10616 const W: usize = 16;
10617 const H: usize = 16;
10618 const TOL: f32 = 1.0 / 512.0; let src = TensorDyn::image(
10622 W,
10623 H,
10624 PixelFormat::Rgba,
10625 DType::U8,
10626 Some(TensorMemory::Mem),
10627 edgefirst_tensor::CpuAccess::ReadWrite,
10628 )
10629 .unwrap();
10630 {
10631 let mut map = src.as_u8().unwrap().map().unwrap();
10632 let data = map.as_mut_slice();
10633 for y in 0..H {
10634 for x in 0..W {
10635 let i = y * W + x;
10636 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;
10640 }
10641 }
10642 }
10643
10644 let mut dst = TensorDyn::image(
10645 W,
10646 H,
10647 PixelFormat::PlanarRgb,
10648 DType::F16,
10649 Some(TensorMemory::Mem),
10650 edgefirst_tensor::CpuAccess::ReadWrite,
10651 )
10652 .unwrap();
10653
10654 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
10655 backend: ComputeBackend::Cpu,
10656 ..Default::default()
10657 })
10658 .unwrap();
10659 proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
10660 .expect("forced-CPU Rgba→PlanarRgb F16 must not error");
10661
10662 let src_map = src.as_u8().unwrap().map().unwrap();
10663 let src_bytes = src_map.as_slice();
10664 let dst_map = dst.as_f16().unwrap().map().unwrap();
10665 let dst_halfs = dst_map.as_slice();
10666
10667 let plane = W * H;
10668 assert_eq!(dst_halfs.len(), plane * 3, "wrong output element count");
10669
10670 for y in 0..H {
10671 for x in 0..W {
10672 let i = y * W + x;
10673 let r_exp = src_bytes[i * 4] as f32 / 255.0;
10674 let g_exp = src_bytes[i * 4 + 1] as f32 / 255.0;
10675 let b_exp = src_bytes[i * 4 + 2] as f32 / 255.0;
10676
10677 let r_got = dst_halfs[i].to_f32();
10678 let g_got = dst_halfs[plane + i].to_f32();
10679 let b_got = dst_halfs[2 * plane + i].to_f32();
10680
10681 assert!(
10682 (r_got - r_exp).abs() <= TOL,
10683 "R plane ({x},{y}): got {r_got}, expected {r_exp}"
10684 );
10685 assert!(
10686 (g_got - g_exp).abs() <= TOL,
10687 "G plane ({x},{y}): got {g_got}, expected {g_exp}"
10688 );
10689 assert!(
10690 (b_got - b_exp).abs() <= TOL,
10691 "B plane ({x},{y}): got {b_got}, expected {b_exp}"
10692 );
10693
10694 if src_bytes[i * 4] != src_bytes[i * 4 + 1] {
10696 assert_ne!(r_got, g_got, "R and G planes must differ at ({x},{y})");
10697 }
10698 }
10699 }
10700 }
10701
10702 #[test]
10710 fn convert_f32_with_rotation_falls_back() {
10711 const W: usize = 16;
10712 const H: usize = 16;
10713
10714 let src = TensorDyn::image(
10716 W,
10717 H,
10718 PixelFormat::Rgba,
10719 DType::U8,
10720 Some(TensorMemory::Mem),
10721 edgefirst_tensor::CpuAccess::ReadWrite,
10722 )
10723 .unwrap();
10724 {
10725 let mut map = src.as_u8().unwrap().map().unwrap();
10726 let data = map.as_mut_slice();
10727 for y in 0..H {
10728 for x in 0..W {
10729 let i = y * W + x;
10730 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;
10734 }
10735 }
10736 }
10737
10738 let mut dst = TensorDyn::image(
10740 H, W, PixelFormat::Rgb,
10743 DType::F32,
10744 Some(TensorMemory::Mem),
10745 edgefirst_tensor::CpuAccess::ReadWrite,
10746 )
10747 .unwrap();
10748
10749 let mut proc = ImageProcessor::new().unwrap();
10750 let result = proc.convert(
10751 &src,
10752 &mut dst,
10753 Rotation::Clockwise90,
10754 Flip::None,
10755 Crop::default(),
10756 );
10757 assert!(
10758 result.is_ok(),
10759 "auto-chain Rgba→Rgb F32 with Rot90 must not error: {:?}",
10760 result.err()
10761 );
10762
10763 let map = dst.as_f32().unwrap().map().unwrap();
10764 let floats = map.as_slice();
10765 assert_eq!(floats.len(), H * W * 3, "unexpected output element count");
10766 for (i, &v) in floats.iter().enumerate() {
10767 assert!(
10768 v.is_finite() && (0.0..=1.0).contains(&v),
10769 "output[{i}]={v} is not finite or not in [0,1]"
10770 );
10771 }
10772 }
10773
10774 #[test]
10781 #[cfg(all(target_os = "linux", feature = "opengl"))]
10782 fn convert_f16_gl_cpu_parity_identity() {
10783 if !is_opengl_available() {
10784 eprintln!("SKIPPED: convert_f16_gl_cpu_parity_identity - OpenGL not available");
10785 return;
10786 }
10787
10788 const W: usize = 16;
10789 const H: usize = 16;
10790 const TOL: f32 = 1.0 / 256.0; let src = TensorDyn::image(
10794 W,
10795 H,
10796 PixelFormat::Rgba,
10797 DType::U8,
10798 Some(TensorMemory::Mem),
10799 edgefirst_tensor::CpuAccess::ReadWrite,
10800 )
10801 .unwrap();
10802 {
10803 let mut map = src.as_u8().unwrap().map().unwrap();
10804 let data = map.as_mut_slice();
10805 for y in 0..H {
10806 for x in 0..W {
10807 let i = y * W + x;
10808 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;
10812 }
10813 }
10814 }
10815
10816 let gl_result = {
10818 let mut gl_proc = match ImageProcessor::with_config(ImageProcessorConfig {
10819 backend: ComputeBackend::OpenGl,
10820 ..Default::default()
10821 }) {
10822 Ok(p) => p,
10823 Err(e) => {
10824 eprintln!(
10825 "SKIPPED: convert_f16_gl_cpu_parity_identity - GL backend unavailable: {e}"
10826 );
10827 return;
10828 }
10829 };
10830
10831 if !gl_proc.supported_render_dtypes().f16 {
10832 eprintln!("SKIPPED: convert_f16_gl_cpu_parity_identity - F16 render not supported");
10833 return;
10834 }
10835
10836 let mut dst = TensorDyn::image(
10837 W,
10838 H,
10839 PixelFormat::PlanarRgb,
10840 DType::F16,
10841 Some(TensorMemory::Mem),
10842 edgefirst_tensor::CpuAccess::ReadWrite,
10843 )
10844 .unwrap();
10845 match gl_proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default()) {
10846 Ok(()) => dst,
10847 Err(e) => {
10848 eprintln!(
10849 "SKIPPED: convert_f16_gl_cpu_parity_identity - GL convert failed: {e}"
10850 );
10851 return;
10852 }
10853 }
10854 };
10855
10856 let cpu_result = {
10858 let mut cpu_proc = ImageProcessor::with_config(ImageProcessorConfig {
10859 backend: ComputeBackend::Cpu,
10860 ..Default::default()
10861 })
10862 .unwrap();
10863 let mut dst = TensorDyn::image(
10864 W,
10865 H,
10866 PixelFormat::PlanarRgb,
10867 DType::F16,
10868 Some(TensorMemory::Mem),
10869 edgefirst_tensor::CpuAccess::ReadWrite,
10870 )
10871 .unwrap();
10872 cpu_proc
10873 .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
10874 .expect("forced-CPU Rgba→PlanarRgb F16 must not error");
10875 dst
10876 };
10877
10878 let gl_map = gl_result.as_f16().unwrap().map().unwrap();
10880 let cpu_map = cpu_result.as_f16().unwrap().map().unwrap();
10881 let gl_halfs = gl_map.as_slice();
10882 let cpu_halfs = cpu_map.as_slice();
10883
10884 assert_eq!(
10885 gl_halfs.len(),
10886 cpu_halfs.len(),
10887 "GL and CPU output sizes differ"
10888 );
10889
10890 let plane = W * H;
10891 let channel_names = ["R", "G", "B"];
10892 for (idx, (gl_h, cpu_h)) in gl_halfs.iter().zip(cpu_halfs.iter()).enumerate() {
10893 let gl_v = gl_h.to_f32();
10894 let cpu_v = cpu_h.to_f32();
10895 let err = (gl_v - cpu_v).abs();
10896 let ch = channel_names[idx / plane];
10897 let pixel = idx % plane;
10898 assert!(
10899 err <= TOL,
10900 "GL vs CPU mismatch at {ch}[{pixel}]: GL={gl_v}, CPU={cpu_v}, err={err} > tol={TOL}"
10901 );
10902 }
10903 }
10904
10905 #[test]
10912 #[cfg(all(target_os = "linux", feature = "opengl"))]
10913 fn supported_render_dtypes_linux_smoke() {
10914 let proc = match ImageProcessor::new() {
10915 Ok(p) => p,
10916 Err(e) => {
10917 eprintln!("SKIPPED: supported_render_dtypes_linux_smoke — ImageProcessor::new() failed: {e}");
10918 return;
10919 }
10920 };
10921 if proc.opengl.is_none() {
10922 eprintln!("SKIPPED: supported_render_dtypes_linux_smoke — no GL backend on this host");
10923 return;
10924 }
10925 let support = proc.supported_render_dtypes();
10927 eprintln!(
10928 "supported_render_dtypes_linux_smoke: f16={} f32={}",
10929 support.f16, support.f32
10930 );
10931 }
10933
10934 #[test]
10943 fn convert_f16_pbo_non_4_aligned_width_falls_back() {
10944 const W: usize = 18; const H: usize = 16;
10946
10947 let src = TensorDyn::image(
10949 W,
10950 H,
10951 PixelFormat::Rgba,
10952 DType::U8,
10953 Some(TensorMemory::Mem),
10954 edgefirst_tensor::CpuAccess::ReadWrite,
10955 )
10956 .unwrap();
10957 {
10958 let mut map = src.as_u8().unwrap().map().unwrap();
10959 let data = map.as_mut_slice();
10960 for chunk in data.chunks_exact_mut(4) {
10961 chunk[0] = 128;
10962 chunk[1] = 64;
10963 chunk[2] = 200;
10964 chunk[3] = 255;
10965 }
10966 }
10967
10968 let mut dst = TensorDyn::image(
10971 W,
10972 H,
10973 PixelFormat::PlanarRgb,
10974 DType::F16,
10975 Some(TensorMemory::Mem),
10976 edgefirst_tensor::CpuAccess::ReadWrite,
10977 )
10978 .unwrap();
10979
10980 let mut proc = ImageProcessor::new().unwrap();
10983 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
10984 assert!(
10985 result.is_ok(),
10986 "auto-chain PlanarRgb F16 W%4!=0 must not error (CPU fallback): {:?}",
10987 result.err()
10988 );
10989
10990 let map = dst.as_f16().unwrap().map().unwrap();
10992 let halfs = map.as_slice();
10993 assert_eq!(halfs.len(), W * H * 3, "unexpected element count");
10994 for (i, h) in halfs.iter().enumerate() {
10995 let v = h.to_f32();
10996 assert!(
10997 v.is_finite() && (0.0..=1.0).contains(&v),
10998 "output[{i}]={v} is not finite or not in [0,1]"
10999 );
11000 }
11001 }
11002
11003 #[test]
11013 #[allow(clippy::needless_update)]
11016 fn convert_nv12_to_rgb_f32_cpu() {
11017 const W: usize = 16;
11018 const H: usize = 16; let src = TensorDyn::image(
11022 W,
11023 H,
11024 PixelFormat::Nv12,
11025 DType::U8,
11026 Some(TensorMemory::Mem),
11027 edgefirst_tensor::CpuAccess::ReadWrite,
11028 )
11029 .unwrap();
11030 {
11031 let mut map = src.as_u8().unwrap().map().unwrap();
11032 map.as_mut_slice().fill(128); }
11034
11035 let mut dst = TensorDyn::image(
11036 W,
11037 H,
11038 PixelFormat::Rgb,
11039 DType::F32,
11040 Some(TensorMemory::Mem),
11041 edgefirst_tensor::CpuAccess::ReadWrite,
11042 )
11043 .unwrap();
11044
11045 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
11046 backend: ComputeBackend::Cpu,
11047 ..Default::default()
11048 })
11049 .unwrap();
11050
11051 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
11052 assert!(
11053 result.is_ok(),
11054 "forced-CPU NV12→Rgb F32 must not error: {:?}",
11055 result.err()
11056 );
11057
11058 let map = dst.as_f32().unwrap().map().unwrap();
11059 let floats = map.as_slice();
11060 assert_eq!(floats.len(), W * H * 3, "unexpected element count");
11061 for (i, &v) in floats.iter().enumerate() {
11062 assert!(
11063 v.is_finite() && (0.0..=1.0).contains(&v),
11064 "output[{i}]={v} is not finite or not in [0,1]"
11065 );
11066 }
11067 let non_zero = floats.iter().any(|&v| v > 0.01);
11069 assert!(non_zero, "all-zero output from NV12→Rgb F32 CPU path");
11070 }
11071
11072 #[test]
11076 #[allow(clippy::needless_update)]
11079 fn convert_nv12_to_planar_rgb_f16_cpu() {
11080 const W: usize = 16;
11081 const H: usize = 16;
11082
11083 let src = TensorDyn::image(
11084 W,
11085 H,
11086 PixelFormat::Nv12,
11087 DType::U8,
11088 Some(TensorMemory::Mem),
11089 edgefirst_tensor::CpuAccess::ReadWrite,
11090 )
11091 .unwrap();
11092 {
11093 let mut map = src.as_u8().unwrap().map().unwrap();
11094 map.as_mut_slice().fill(128);
11095 }
11096
11097 let mut dst = TensorDyn::image(
11098 W,
11099 H,
11100 PixelFormat::PlanarRgb,
11101 DType::F16,
11102 Some(TensorMemory::Mem),
11103 edgefirst_tensor::CpuAccess::ReadWrite,
11104 )
11105 .unwrap();
11106
11107 let mut proc = ImageProcessor::with_config(ImageProcessorConfig {
11108 backend: ComputeBackend::Cpu,
11109 ..Default::default()
11110 })
11111 .unwrap();
11112
11113 let result = proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default());
11114 assert!(
11115 result.is_ok(),
11116 "forced-CPU NV12→PlanarRgb F16 must not error: {:?}",
11117 result.err()
11118 );
11119
11120 let map = dst.as_f16().unwrap().map().unwrap();
11121 let halfs = map.as_slice();
11122 assert_eq!(halfs.len(), W * H * 3, "unexpected element count");
11123 for (i, h) in halfs.iter().enumerate() {
11124 let v = h.to_f32();
11125 assert!(
11126 v.is_finite() && (0.0..=1.0).contains(&v),
11127 "output[{i}]={v} is not finite or not in [0,1]"
11128 );
11129 }
11130 let non_zero = halfs.iter().any(|h| h.to_f32() > 0.01);
11131 assert!(non_zero, "all-zero output from NV12→PlanarRgb F16 CPU path");
11132 }
11133
11134 #[test]
11143 fn create_image_desc_negotiates_and_counts_fallbacks() {
11144 use edgefirst_tensor::{Compression, CpuAccess, ImageDesc};
11145 let proc = ImageProcessor::new().unwrap();
11146
11147 let desc =
11148 ImageDesc::new(64, 64, PixelFormat::Rgba, DType::U8).with_access(CpuAccess::ReadWrite);
11149 let plain = proc.create_image_desc(&desc).unwrap();
11150 let classic = proc
11151 .create_image(
11152 64,
11153 64,
11154 PixelFormat::Rgba,
11155 DType::U8,
11156 None,
11157 CpuAccess::ReadWrite,
11158 )
11159 .unwrap();
11160 assert_eq!(plain.memory(), classic.memory(), "same negotiation path");
11161 assert_eq!(plain.compression(), None);
11162
11163 #[cfg(not(target_os = "android"))]
11164 {
11165 let before = proc.compression_fallback_count();
11166 let desc = ImageDesc::new(64, 64, PixelFormat::Rgba, DType::U8)
11167 .with_compression(Compression::Any);
11168 let t = proc.create_image_desc(&desc).unwrap();
11169 assert_eq!(t.compression(), None, "no vendor tile scheme off-Android");
11170 assert!(
11171 proc.compression_fallback_count() > before,
11172 "Any resolving linear must count"
11173 );
11174 }
11175 }
11176
11177 #[test]
11180 #[cfg(target_os = "linux")]
11181 fn create_image_f32_dma_rejected() {
11182 let proc = ImageProcessor::new().unwrap();
11183 let result = proc.create_image(
11184 64,
11185 64,
11186 PixelFormat::Rgb,
11187 DType::F32,
11188 Some(TensorMemory::Dma),
11189 edgefirst_tensor::CpuAccess::ReadWrite,
11190 );
11191 assert!(
11192 result.is_err(),
11193 "create_image(F32, Dma) must fail — no DRM fourcc for f32"
11194 );
11195 }
11196
11197 #[test]
11206 #[cfg(target_os = "linux")]
11207 fn import_image_carries_colorimetry() {
11208 use edgefirst_tensor::{ColorEncoding, ColorRange, Colorimetry, TensorMemory};
11209
11210 let expected = Colorimetry::default()
11211 .with_encoding(ColorEncoding::Bt709)
11212 .with_range(ColorRange::Limited);
11213
11214 if !is_dma_available() {
11215 let mut t = TensorDyn::image(
11218 8,
11219 8,
11220 PixelFormat::Rgba,
11221 DType::U8,
11222 Some(TensorMemory::Mem),
11223 edgefirst_tensor::CpuAccess::ReadWrite,
11224 )
11225 .expect("alloc");
11226 assert_eq!(t.colorimetry(), None, "colorimetry must start as None");
11227 t.set_colorimetry(Some(expected));
11228 assert_eq!(
11229 t.colorimetry(),
11230 Some(expected),
11231 "set_colorimetry must round-trip"
11232 );
11233 eprintln!("SKIPPED import_image_carries_colorimetry (DMA unavailable); storage contract verified via TensorDyn");
11234 return;
11235 }
11236
11237 use edgefirst_tensor::{PlaneDescriptor, Tensor};
11240
11241 let rgba_bytes = 64 * 64 * 4; let dma_tensor =
11243 Tensor::<u8>::new(&[rgba_bytes], Some(TensorMemory::Dma), Some("import_test"))
11244 .expect("dma alloc");
11245 let pd =
11246 PlaneDescriptor::new(dma_tensor.dmabuf().expect("dma fd")).expect("PlaneDescriptor");
11247
11248 let proc = ImageProcessor::new().expect("ImageProcessor");
11249 let result = proc.import_image(
11250 pd,
11251 None,
11252 64,
11253 64,
11254 PixelFormat::Rgba,
11255 DType::U8,
11256 Some(expected),
11257 );
11258 let tensor = result.expect("import_image must succeed on DMA fd");
11259 assert_eq!(
11260 tensor.colorimetry(),
11261 Some(expected),
11262 "import_image must store the supplied colorimetry on the returned TensorDyn"
11263 );
11264 }
11265}