use impeller_hal::{Batch, BlendMode, Extent2D, HalFence, Material, PassDescriptor, PixelFormat};
use impeller_hal::{TextureDescriptor, FRAME_WAIT_TIMEOUT};
use impeller_hal_vulkan::{ContextConfig, DevicePreference, VulkanContext};
const FULL: [[f32; 2]; 4] = [[-1.0, -1.0], [1.0, -1.0], [1.0, 1.0], [-1.0, 1.0]];
const QUAD: [u32; 6] = [0, 1, 2, 0, 2, 3];
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 scene() -> Batch {
let mut batch = Batch::new();
batch
.push(
&FULL,
&QUAD,
Material::solid([1.0, 0.0, 0.0, 1.0]),
BlendMode::Src,
)
.expect("push");
batch
}
fn target(ctx: &mut VulkanContext) -> impeller_hal_vulkan::VulkanTexture {
ctx.create_texture(&TextureDescriptor::offscreen(
Extent2D::new(64, 64),
PixelFormat::Rgba8Unorm,
))
.expect("texture")
}
fn assert_clean(ctx: &VulkanContext) {
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:?}");
}
#[test]
fn a_deferred_submission_returns_a_fence_that_eventually_signals() {
let Some(mut ctx) = context() else { return };
let mut tex = target(&mut ctx);
let batch = scene();
let fence = ctx
.submit_batch_deferred(&mut tex, &batch, PassDescriptor::clear([0.0; 4]))
.expect("deferred submit");
assert!(
fence.wait(FRAME_WAIT_TIMEOUT).expect("wait"),
"the fence never signalled"
);
assert!(fence.is_signaled().expect("status"));
ctx.retire_fence(fence);
let pixels = ctx.read_texture(&mut tex).expect("readback");
assert_eq!(&pixels[..4], &[255, 0, 0, 255]);
ctx.destroy_texture(tex);
assert_clean(&ctx);
}
#[test]
fn a_fence_exports_a_sync_file_before_the_work_completes() {
let Some(mut ctx) = context() else { return };
if !ctx.capabilities().sync.export_sync_file {
eprintln!("skipping: this device cannot export a sync_file");
return;
}
let mut tex = target(&mut ctx);
let batch = scene();
let fence = ctx
.submit_batch_deferred(&mut tex, &batch, PassDescriptor::clear([0.0; 4]))
.expect("deferred submit");
assert!(fence.is_exportable());
match fence.export_sync_file() {
Ok(fd) => {
fd.try_clone()
.expect("the exported sync_file is not an open descriptor");
}
Err(e) => {
let text = e.to_string();
assert!(
text.contains("already signalled"),
"unexpected export failure: {text}"
);
}
}
assert!(fence.wait(FRAME_WAIT_TIMEOUT).expect("wait"));
ctx.retire_fence(fence);
ctx.destroy_texture(tex);
assert_clean(&ctx);
}
#[test]
fn an_exported_sync_file_is_independent_of_the_fence_it_came_from() {
let Some(mut ctx) = context() else { return };
if !ctx.capabilities().sync.export_sync_file {
return;
}
let mut tex = target(&mut ctx);
let batch = scene();
let fence = ctx
.submit_batch_deferred(&mut tex, &batch, PassDescriptor::clear([0.0; 4]))
.expect("deferred submit");
let exported = fence.export_sync_file().ok();
assert!(fence.wait(FRAME_WAIT_TIMEOUT).expect("wait"));
ctx.retire_fence(fence);
if let Some(fd) = exported {
fd.try_clone()
.expect("the sync_file was closed when the fence was retired");
}
ctx.destroy_texture(tex);
assert_clean(&ctx);
}
#[test]
fn repeated_deferred_submissions_do_not_exhaust_resources() {
let Some(mut ctx) = context() else { return };
let mut tex = target(&mut ctx);
let batch = scene();
for i in 0..64 {
let fence = ctx
.submit_batch_deferred(&mut tex, &batch, PassDescriptor::clear([0.0; 4]))
.unwrap_or_else(|e| panic!("iteration {i}: {e}"));
let _ = fence.export_sync_file();
assert!(fence.wait(FRAME_WAIT_TIMEOUT).expect("wait"));
ctx.retire_fence(fence);
}
let pixels = ctx.read_texture(&mut tex).expect("readback");
assert_eq!(&pixels[..4], &[255, 0, 0, 255]);
ctx.destroy_texture(tex);
assert_clean(&ctx);
}
#[test]
fn deferred_and_waiting_submission_produce_the_same_pixels() {
let Some(mut ctx) = context() else { return };
let batch = scene();
let mut deferred_target = target(&mut ctx);
let fence = ctx
.submit_batch_deferred(
&mut deferred_target,
&batch,
PassDescriptor::clear([0.0, 0.0, 1.0, 1.0]),
)
.expect("deferred submit");
assert!(fence.wait(FRAME_WAIT_TIMEOUT).expect("wait"));
ctx.retire_fence(fence);
let deferred = ctx.read_texture(&mut deferred_target).expect("readback");
ctx.destroy_texture(deferred_target);
let mut waiting_target = target(&mut ctx);
ctx.submit_batch(
&mut waiting_target,
&batch,
PassDescriptor::clear([0.0, 0.0, 1.0, 1.0]),
)
.expect("submit");
let waiting = ctx.read_texture(&mut waiting_target).expect("readback");
ctx.destroy_texture(waiting_target);
assert_eq!(deferred, waiting, "deferring changed what was rendered");
assert_clean(&ctx);
}
#[test]
fn a_deferred_multisampled_pass_is_refused_rather_than_leaking() {
let Some(mut ctx) = context() else { return };
if !ctx.capabilities().sample_counts.supports(4) {
return;
}
let mut tex = target(&mut ctx);
let batch = scene();
let result = ctx.submit_batch_deferred(
&mut tex,
&batch,
PassDescriptor::clear([0.0; 4]).with_samples(4),
);
assert!(result.is_err());
ctx.destroy_texture(tex);
assert_clean(&ctx);
}
#[test]
fn retiring_one_frame_does_not_free_another_frame_s_resources() {
let Some(mut ctx) = context() else { return };
let batch = scene();
let mut first_target = target(&mut ctx);
let mut second_target = target(&mut ctx);
let first = ctx
.submit_batch_deferred(&mut first_target, &batch, PassDescriptor::clear([0.0; 4]))
.expect("first submission");
let second = ctx
.submit_batch_deferred(&mut second_target, &batch, PassDescriptor::clear([0.0; 4]))
.expect("second submission");
assert!(first.wait(FRAME_WAIT_TIMEOUT).expect("wait"));
ctx.retire_fence(first);
assert!(second.wait(FRAME_WAIT_TIMEOUT).expect("wait"));
ctx.retire_fence(second);
for tex in [&mut first_target, &mut second_target] {
let pixels = ctx.read_texture(tex).expect("readback");
assert_eq!(&pixels[..4], &[255, 0, 0, 255]);
}
ctx.destroy_texture(first_target);
ctx.destroy_texture(second_target);
assert_clean(&ctx);
}
#[test]
fn frames_may_be_retired_out_of_the_order_they_were_submitted() {
let Some(mut ctx) = context() else { return };
let batch = scene();
let mut first_target = target(&mut ctx);
let mut second_target = target(&mut ctx);
let first = ctx
.submit_batch_deferred(&mut first_target, &batch, PassDescriptor::clear([0.0; 4]))
.expect("first submission");
let second = ctx
.submit_batch_deferred(&mut second_target, &batch, PassDescriptor::clear([0.0; 4]))
.expect("second submission");
assert!(second.wait(FRAME_WAIT_TIMEOUT).expect("wait"));
ctx.retire_fence(second);
assert!(first.wait(FRAME_WAIT_TIMEOUT).expect("wait"));
ctx.retire_fence(first);
for tex in [&mut first_target, &mut second_target] {
let pixels = ctx.read_texture(tex).expect("readback");
assert_eq!(&pixels[..4], &[255, 0, 0, 255]);
}
ctx.destroy_texture(first_target);
ctx.destroy_texture(second_target);
assert_clean(&ctx);
}
#[test]
fn an_exported_sync_file_actually_signals() {
let Some(mut ctx) = context() else { return };
let batch = scene();
let mut tex = target(&mut ctx);
let fence = ctx
.submit_batch_deferred(&mut tex, &batch, PassDescriptor::clear([0.0; 4]))
.expect("deferred submission");
let Ok(fd) = fence.export_sync_file() else {
eprintln!("skipping: this device cannot export a sync_file");
ctx.retire_fence(fence);
ctx.destroy_texture(tex);
return;
};
let borrowed = std::os::fd::AsFd::as_fd(&fd);
let mut fds = [rustix::event::PollFd::new(
&borrowed,
rustix::event::PollFlags::IN,
)];
let ready = rustix::event::poll(
&mut fds,
Some(&rustix::event::Timespec {
tv_sec: 2,
tv_nsec: 0,
}),
)
.expect("poll");
assert!(
ready > 0,
"the exported sync_file never signalled, so anything waiting on it \
— a display commit above all — would wait forever"
);
ctx.retire_fence(fence);
ctx.destroy_texture(tex);
assert_clean(&ctx);
}