use impeller_hal::{BlendMode, Extent2D, Material, PassDescriptor, PixelFormat};
use impeller_hal_vulkan::{ContextConfig, DevicePreference, VulkanContext};
use impeller_present::{negotiate, PREFERRED_FORMATS};
fn context() -> Option<VulkanContext> {
match VulkanContext::with_config(ContextConfig {
device: DevicePreference::Auto,
validation: true,
}) {
Ok(ctx) => Some(ctx),
Err(e) => {
eprintln!("skipping: no usable Vulkan device ({e})");
None
}
}
}
fn exportable(ctx: &VulkanContext) -> bool {
if ctx.capabilities().dma_buf.can_allocate_scanout() {
return true;
}
eprintln!("skipping: this device cannot allocate exportable images");
false
}
#[test]
fn the_device_advertises_layouts_it_can_render_into() {
let Some(ctx) = context() else { return };
let formats = &ctx.capabilities().render_formats;
if !exportable(&ctx) {
return;
}
assert!(
!formats.is_empty(),
"no renderable formats advertised, so negotiation has nothing to work with"
);
for set in formats {
eprintln!(" {} with {} modifier(s)", set.fourcc, set.modifiers.len());
assert!(!set.modifiers.is_empty(), "{} has no layouts", set.fourcc);
}
let non_linear = formats
.iter()
.any(|s| s.modifiers.iter().any(|m| !m.is_linear()));
if ctx.capabilities().software {
if !non_linear {
eprintln!("skipping: a software rasterizer has only linear layouts to offer");
}
return;
}
assert!(
non_linear,
"only linear layouts were advertised; sharing will cost full bandwidth"
);
}
#[test]
fn an_exported_image_carries_a_descriptor_and_a_real_layout() {
let Some(mut ctx) = context() else { return };
if !exportable(&ctx) {
return;
}
let render = ctx.capabilities().render_formats.clone();
let chosen = negotiate(&render, &render, PREFERRED_FORMATS).expect("negotiated");
eprintln!("negotiated {chosen}");
let set = render
.iter()
.find(|s| s.fourcc == chosen.fourcc)
.expect("the chosen format was advertised");
let format = match chosen.fourcc {
f if f == impeller_hal::Fourcc::ARGB8888 => PixelFormat::Bgra8Unorm,
f if f == impeller_hal::Fourcc::ABGR8888 => PixelFormat::Rgba8Unorm,
f if f == impeller_hal::Fourcc::XRGB2101010 => PixelFormat::Rgb10A2Unorm,
other => panic!("unexpected negotiated format {other}"),
};
let texture = ctx
.create_exportable_texture(Extent2D::new(64, 64), format, &set.modifiers)
.expect("exportable texture");
let modifier = ctx.texture_modifier(&texture).expect("modifier");
assert!(
set.modifiers.contains(&modifier),
"driver chose {modifier}, which was not among the candidates offered"
);
let exported = ctx.export_texture(&texture).expect("export");
assert_eq!(exported.fourcc, chosen.fourcc);
assert_eq!(exported.modifier, modifier);
assert_eq!(exported.planes.len(), 1, "expected a single-plane image");
let plane = &exported.planes[0];
assert!(
plane.stride >= 64 * 4,
"stride {} is too small for a 64-pixel row",
plane.stride
);
plane
.fd
.try_clone()
.expect("the exported descriptor is not an open file");
drop(exported);
ctx.destroy_texture(texture);
}
#[test]
fn an_exported_image_can_still_be_rendered_into() {
let Some(mut ctx) = context() else { return };
if !exportable(&ctx) {
return;
}
let render = ctx.capabilities().render_formats.clone();
let set = render
.iter()
.find(|s| s.fourcc == impeller_hal::Fourcc::ARGB8888)
.expect("a common eight-bit format");
let mut texture = ctx
.create_exportable_texture(
Extent2D::new(32, 32),
PixelFormat::Bgra8Unorm,
&set.modifiers,
)
.expect("exportable texture");
let mut batch = impeller_hal::Batch::new();
batch
.push(
&[[-1.0, -1.0], [1.0, -1.0], [1.0, 1.0], [-1.0, 1.0]],
&[0, 1, 2, 0, 2, 3],
Material::solid([1.0, 0.0, 0.0, 1.0]),
BlendMode::Src,
)
.expect("push");
ctx.submit_batch(
&mut texture,
&batch,
PassDescriptor::clear([0.0, 0.0, 0.0, 1.0]),
)
.expect("submit");
let pixels = ctx.read_texture(&mut texture).expect("readback");
assert_eq!(&pixels[..4], &[0, 0, 255, 255]);
let errors: Vec<_> = ctx
.validation_messages()
.into_iter()
.filter(|m| m.severity == impeller_hal_vulkan::ValidationSeverity::Error)
.collect();
assert!(errors.is_empty(), "validation errors: {errors:?}");
ctx.destroy_texture(texture);
}
#[test]
fn an_ordinary_texture_cannot_be_exported() {
let Some(mut ctx) = context() else { return };
let texture = ctx
.create_texture(&impeller_hal::TextureDescriptor::offscreen(
Extent2D::new(16, 16),
PixelFormat::Rgba8Unorm,
))
.expect("texture");
assert!(ctx.export_texture(&texture).is_err());
assert!(ctx.texture_modifier(&texture).is_err());
ctx.destroy_texture(texture);
}
#[test]
fn exporting_without_candidate_modifiers_is_refused() {
let Some(mut ctx) = context() else { return };
if !exportable(&ctx) {
return;
}
let result = ctx.create_exportable_texture(Extent2D::new(16, 16), PixelFormat::Bgra8Unorm, &[]);
assert!(result.is_err());
}
#[test]
fn exported_descriptors_are_distinct_and_do_not_leak() {
let Some(mut ctx) = context() else { return };
if !exportable(&ctx) {
return;
}
let render = ctx.capabilities().render_formats.clone();
let set = render
.iter()
.find(|s| s.fourcc == impeller_hal::Fourcc::ARGB8888)
.expect("a common eight-bit format");
let mut previous = Vec::new();
for i in 0..32 {
let texture = ctx
.create_exportable_texture(
Extent2D::new(16, 16),
PixelFormat::Bgra8Unorm,
&set.modifiers,
)
.unwrap_or_else(|e| panic!("iteration {i}: {e}"));
let exported = ctx.export_texture(&texture).expect("export");
use std::os::fd::AsRawFd;
previous.push(exported.planes[0].fd.as_raw_fd());
drop(exported);
ctx.destroy_texture(texture);
}
assert_eq!(previous.len(), 32);
}