#![cfg(all(feature = "opengl", any(target_os = "linux", target_os = "windows")))]
use edgefirst_image::{Crop, Flip, GLProcessorThreaded, ImageProcessorTrait, Rotation};
use edgefirst_tensor::{
CpuAccess, DType, PixelFormat, Region, TensorDyn, TensorMapTrait, TensorMemory,
};
use std::sync::{Arc, Mutex};
use tracing_subscriber::layer::SubscriberExt;
const W: usize = 320;
const H: usize = 240;
const BPP: usize = 4;
const NEW_SPAN: &str = "<new_span>";
type Records = Arc<Mutex<Vec<(String, String, String)>>>;
struct Capture(Records);
struct Visit<'a>(&'a str, &'a Records);
impl tracing::field::Visit for Visit<'_> {
fn record_debug(&mut self, f: &tracing::field::Field, v: &dyn std::fmt::Debug) {
self.1
.lock()
.unwrap()
.push((self.0.to_string(), f.name().to_string(), format!("{v:?}")));
}
fn record_str(&mut self, f: &tracing::field::Field, v: &str) {
self.1
.lock()
.unwrap()
.push((self.0.to_string(), f.name().to_string(), v.to_string()));
}
}
impl<S> tracing_subscriber::Layer<S> for Capture
where
S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
{
fn on_new_span(
&self,
attrs: &tracing::span::Attributes<'_>,
_id: &tracing::span::Id,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
self.0.lock().unwrap().push((
attrs.metadata().name().to_string(),
NEW_SPAN.to_string(),
String::new(),
));
}
fn on_record(
&self,
id: &tracing::span::Id,
values: &tracing::span::Record<'_>,
ctx: tracing_subscriber::layer::Context<'_, S>,
) {
let name = ctx
.span(id)
.map(|s| s.name().to_string())
.unwrap_or_default();
values.record(&mut Visit(&name, &self.0));
}
}
fn skip(why: &str) {
use std::io::Write;
let _ = writeln!(&mut std::io::stderr(), "SKIPPED: {why}");
}
fn image(w: usize, h: usize, memory: TensorMemory) -> Option<TensorDyn> {
match TensorDyn::image(
w,
h,
PixelFormat::Rgba,
DType::U8,
Some(memory),
CpuAccess::ReadWrite,
) {
Ok(t) if t.memory() == memory => Some(t),
_ => None,
}
}
fn values_for(seen: &[(String, String, String)], field: &str) -> Vec<String> {
seen.iter()
.filter(|(span, f, _)| span == "image.convert.gl" && f == field)
.map(|(_, _, v)| v.clone())
.collect()
}
#[test]
fn the_convert_span_records_the_feed_fields() {
let records: Records = Arc::new(Mutex::new(Vec::new()));
tracing::subscriber::set_global_default(
tracing_subscriber::registry().with(Capture(records.clone())),
)
.expect("install the capturing subscriber");
let mut gl = match GLProcessorThreaded::new(None) {
Ok(gl) => gl,
Err(e) => {
assert!(
!std::env::var("HAL_TEST_REQUIRE_GL").is_ok_and(|v| v == "1"),
"HAL_TEST_REQUIRE_GL=1 but the GL backend failed to come up: {e}"
);
skip(&format!("no GL backend: {e}"));
return;
}
};
let src = image(W, H, TensorMemory::Mem).expect("host source");
{
let mut m = src.map_bytes(CpuAccess::Write).expect("map source");
m.as_mut_slice().fill(0x5A);
}
let mut dst = image(W, H, TensorMemory::Mem).expect("host destination");
gl.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
.expect("host-memory convert");
let seen = records.lock().unwrap().clone();
assert!(
seen.iter()
.any(|(span, field, _)| span == "image.convert.gl" && field == NEW_SPAN),
"the capture never saw an image.convert.gl span, so it cannot testify about \
the feed fields; records seen: {seen:?}"
);
let feeds = values_for(&seen, "src_feed");
assert!(
!feeds.is_empty(),
"a u8 convert recorded no src_feed on image.convert.gl; records seen: {seen:?}"
);
assert!(
feeds
.iter()
.all(|v| matches!(v.as_str(), "import" | "pbo" | "upload")),
"src_feed must be one of import|pbo|upload, saw {feeds:?}"
);
if let Some(dma_src) = image(W, H, TensorMemory::DmaBuf) {
{
let mut m = dma_src.map_bytes(CpuAccess::Write).expect("map dma source");
m.as_mut_slice().fill(0x33);
}
let _ = gl.convert(
&dma_src,
&mut dst,
Rotation::None,
Flip::None,
Crop::default(),
);
}
let src_feeds = values_for(&records.lock().unwrap().clone(), "src_feed");
eprintln!("CAPTURE src_feed on image.convert.gl: {src_feeds:?}");
let dst_feeds = values_for(&records.lock().unwrap().clone(), "dst_feed");
assert!(
dst_feeds.iter().any(|v| v == "mapped_texture"),
"a host-memory destination recorded no dst_feed; values seen: {dst_feeds:?}"
);
assert!(
dst_feeds
.iter()
.all(|v| matches!(v.as_str(), "zero_copy" | "mapped_texture" | "pbo")),
"dst_feed must be one of zero_copy|mapped_texture|pbo, saw {dst_feeds:?}"
);
if let Some(mut zc) = image(W, H, TensorMemory::DmaBuf) {
let _ = gl.convert(&src, &mut zc, Rotation::None, Flip::None, Crop::default());
}
let feeds = values_for(&records.lock().unwrap().clone(), "dst_feed");
assert!(
feeds
.iter()
.all(|v| matches!(v.as_str(), "zero_copy" | "mapped_texture" | "pbo")),
"dst_feed must be one of zero_copy|mapped_texture|pbo, saw {feeds:?}"
);
if feeds.iter().any(|v| v == "zero_copy") {
eprintln!("CAPTURE dst_feed zero_copy: present");
} else {
skip("no zero-copy destination render on this host, so dst_feed=zero_copy is unreachable");
}
eprintln!("CAPTURE dst_feed values: {feeds:?}");
#[cfg(target_os = "linux")]
let (dst_fmt, dst_dtype) = (PixelFormat::PlanarRgb, DType::F16);
#[cfg(target_os = "windows")]
let (dst_fmt, dst_dtype) = (PixelFormat::Rgba, DType::F32);
match TensorDyn::image(
W,
H,
dst_fmt,
dst_dtype,
Some(TensorMemory::DmaBuf),
CpuAccess::ReadWrite,
) {
Ok(t) if t.memory() == TensorMemory::DmaBuf => {
let float_src = image(W, H, TensorMemory::Mem).expect("host float-route source");
{
let mut m = float_src
.map_bytes(CpuAccess::Write)
.expect("map float src");
m.as_mut_slice().fill(0x40);
}
let mut float_dst = t;
records.lock().unwrap().clear();
match gl.convert(
&float_src,
&mut float_dst,
Rotation::None,
Flip::None,
Crop::default(),
) {
Ok(()) => {
let seen = records.lock().unwrap().clone();
let src_feeds = values_for(&seen, "src_feed");
let dst_feeds = values_for(&seen, "dst_feed");
assert!(
src_feeds
.iter()
.any(|v| matches!(v.as_str(), "import" | "pbo" | "upload")),
"a float convert recorded no src_feed on image.convert.gl; records: {seen:?}"
);
assert!(
dst_feeds
.iter()
.any(|v| matches!(v.as_str(), "zero_copy" | "pbo")),
"a float convert recorded no dst_feed on image.convert.gl; records: {seen:?}"
);
eprintln!(
"CAPTURE float src_feed={src_feeds:?} dst_feed={dst_feeds:?} for {dst_fmt:?}/{dst_dtype:?}"
);
}
Err(e) => {
skip(&format!(
"GL float convert not routable on this host ({dst_fmt:?}/{dst_dtype:?}): {e}"
));
}
}
}
Ok(t) => {
skip(&format!(
"float destination allocated as {:?}, not the zero-copy buffer the GL float \
route serves",
t.memory()
));
}
Err(e) => {
skip(&format!(
"no zero-copy {dst_fmt:?}/{dst_dtype:?} image: {e}"
));
}
}
let Some(canvas) = image(64, 64, TensorMemory::DmaBuf) else {
skip("no zero-copy RGBA image here, so no destination import to refuse");
return;
};
let pitch = canvas.effective_row_stride().unwrap_or(64 * BPP);
let offset = 8 * pitch + 8 * BPP;
assert!(
!offset.is_multiple_of(64),
"precondition: (8,8) at pitch {pitch} is byte offset {offset}, which must be \
unaligned for a driver to have anything to refuse"
);
let fresh = canvas
.view(Region::new(8, 8, 16, 16))
.expect("destination view");
let mut tile = TensorDyn::import_descriptor(&fresh.descriptor_pinned(None))
.expect("rebuild the destination view from its descriptor");
tile.set_plane_offset(offset);
let tile_src = image(16, 16, TensorMemory::Mem).expect("tile source");
let before = gl
.convert_stats()
.expect("convert stats")
.dst_import_fallbacks;
let _ = gl.convert(
&tile_src,
&mut tile,
Rotation::None,
Flip::None,
Crop::default(),
);
let refused = gl
.convert_stats()
.expect("convert stats")
.dst_import_fallbacks
- before;
if refused == 0 {
skip(&format!(
"no destination import was refused at offset {offset} — either this \
driver accepts it (Mali, V3D) or this host attempts no zero-copy \
destination import at all — so there is no dst_feed to observe"
));
return;
}
let seen = records.lock().unwrap().clone();
assert!(
values_for(&seen, "dst_feed")
.iter()
.any(|v| v == "mapped_texture"),
"the refused destination import recorded no dst_feed on image.convert.gl; \
records seen: {seen:?}"
);
eprintln!("CAPTURE dst_feed on the refused route: mapped_texture");
}