use impeller_hal::{BlendMode, Extent2D, Material, PixelFormat, TextureDescriptor};
use impeller_hal_vulkan::Validated;
use impeller_hal_vulkan::{ContextConfig, DevicePreference, VulkanContext};
const SIZE: u32 = 64;
const CLEAR: [f32; 4] = [0.0, 0.0, 0.0, 1.0];
const RED: [f32; 4] = [1.0, 0.0, 0.0, 1.0];
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
}
}
}
struct Image {
pixels: Vec<u8>,
width: u32,
}
impl Image {
fn at(&self, x: u32, y: u32) -> [u8; 4] {
let i = ((y * self.width + x) * 4) as usize;
[
self.pixels[i],
self.pixels[i + 1],
self.pixels[i + 2],
self.pixels[i + 3],
]
}
fn is_red(&self, x: u32, y: u32) -> bool {
self.at(x, y) == [255, 0, 0, 255]
}
fn is_clear(&self, x: u32, y: u32) -> bool {
self.at(x, y) == [0, 0, 0, 255]
}
fn red_count(&self) -> usize {
self.pixels.chunks_exact(4).filter(|p| p[0] > 128).count()
}
}
fn draw(ctx: &mut VulkanContext, vertices: &[[f32; 2]], indices: &[u32]) -> Image {
let mut tex = ctx
.create_texture(&TextureDescriptor::offscreen(
Extent2D::new(SIZE, SIZE),
PixelFormat::Rgba8Unorm,
))
.expect("texture");
ctx.draw_indexed(
&mut tex,
vertices,
indices,
Material::solid(RED),
BlendMode::Src,
Some(CLEAR),
)
.expect("draw");
let pixels = ctx.read_texture(&mut tex).expect("readback");
ctx.destroy_texture(tex);
Image {
pixels,
width: SIZE,
}
}
fn assert_validation_clean(ctx: &VulkanContext) {
if !ctx.validation_active() {
return;
}
let errors = ctx
.validation_messages()
.into_iter()
.filter(|m| m.severity == impeller_hal_vulkan::ValidationSeverity::Error)
.map(|m| format!("[{}] {}", m.id, m.message))
.collect::<Vec<_>>();
assert!(
errors.is_empty(),
"validation errors:\n{}",
errors.join("\n")
);
}
#[test]
fn a_full_target_quad_covers_every_pixel() {
let Some(mut ctx) = context() else { return };
let verts = [[-1.0, -1.0], [1.0, -1.0], [1.0, 1.0], [-1.0, 1.0]];
let img = draw(&mut ctx, &verts, &[0, 1, 2, 0, 2, 3]);
assert_eq!(
img.red_count(),
(SIZE * SIZE) as usize,
"a full-clip quad must leave no pixel unwritten"
);
assert_validation_clean(&ctx);
}
#[test]
fn clip_space_follows_the_wgsl_convention_with_y_up() {
let Some(mut ctx) = context() else { return };
let verts = [[-1.0, -1.0], [1.0, -1.0], [1.0, 0.0], [-1.0, 0.0]];
let img = draw(&mut ctx, &verts, &[0, 1, 2, 0, 2, 3]);
assert!(
img.is_red(SIZE / 2, SIZE - 4),
"clip Y of -1 should be the bottom of the image under the WGSL convention"
);
assert!(
img.is_clear(SIZE / 2, 4),
"top of the image should be untouched; coordinate space is not being adjusted"
);
assert_validation_clean(&ctx);
}
#[test]
fn geometry_is_not_mirrored_horizontally() {
let Some(mut ctx) = context() else { return };
let verts = [[-1.0, -1.0], [0.0, -1.0], [0.0, 1.0], [-1.0, 1.0]];
let img = draw(&mut ctx, &verts, &[0, 1, 2, 0, 2, 3]);
assert!(
img.is_red(4, SIZE / 2),
"left of the image should be covered"
);
assert!(
img.is_clear(SIZE - 4, SIZE / 2),
"right of the image should be untouched; geometry is flipped in X"
);
assert_validation_clean(&ctx);
}
#[test]
fn a_triangle_covers_about_half_the_quad_it_is_cut_from() {
let Some(mut ctx) = context() else { return };
let verts = [[-1.0, -1.0], [1.0, -1.0], [1.0, 1.0], [-1.0, 1.0]];
let quad = draw(&mut ctx, &verts, &[0, 1, 2, 0, 2, 3]).red_count();
let tri = draw(&mut ctx, &verts, &[0, 1, 2]).red_count();
let ratio = tri as f32 / quad as f32;
assert!(
(ratio - 0.5).abs() < 0.05,
"one triangle of the pair covered {ratio} of the quad"
);
assert_validation_clean(&ctx);
}
#[test]
fn triangle_winding_does_not_affect_coverage() {
let Some(mut ctx) = context() else { return };
let verts = [[-1.0, -1.0], [1.0, -1.0], [1.0, 1.0]];
let ccw = draw(&mut ctx, &verts, &[0, 1, 2]).red_count();
let cw = draw(&mut ctx, &verts, &[0, 2, 1]).red_count();
assert_eq!(ccw, cw, "reversing winding changed coverage");
assert!(ccw > 0);
assert_validation_clean(&ctx);
}
#[test]
fn an_empty_index_buffer_clears_without_drawing() {
let Some(mut ctx) = context() else { return };
let img = draw(&mut ctx, &[[0.0, 0.0]], &[]);
assert_eq!(img.red_count(), 0);
assert!(img.is_clear(0, 0));
assert!(img.is_clear(SIZE - 1, SIZE - 1));
assert_validation_clean(&ctx);
}
#[test]
fn an_out_of_range_index_is_refused_before_reaching_the_gpu() {
let Some(mut ctx) = context() else { return };
let mut tex = ctx
.create_texture(&TextureDescriptor::offscreen(
Extent2D::new(16, 16),
PixelFormat::Rgba8Unorm,
))
.expect("texture");
let result = ctx.draw_indexed(
&mut tex,
&[[0.0, 0.0]],
&[0, 1, 2],
Material::solid(RED),
BlendMode::Src,
Some(CLEAR),
);
assert!(result.is_err());
let result = ctx.draw_indexed(
&mut tex,
&[[0.0, 0.0]],
&[0, 0],
Material::solid(RED),
BlendMode::Src,
Some(CLEAR),
);
assert!(result.is_err());
ctx.destroy_texture(tex);
assert_validation_clean(&ctx);
}
#[test]
fn repeated_draws_reuse_the_cached_pipeline() {
let Some(mut ctx) = context() else { return };
let verts = [[-1.0, -1.0], [1.0, -1.0], [1.0, 1.0]];
let first = draw(&mut ctx, &verts, &[0, 1, 2]).red_count();
for _ in 0..5 {
assert_eq!(draw(&mut ctx, &verts, &[0, 1, 2]).red_count(), first);
}
assert_validation_clean(&ctx);
}
#[test]
fn the_software_reference_rasterizes_identically() {
let Some(mut default_ctx) = context() else {
return;
};
let Ok(mut sw) = Validated::new(DevicePreference::Software) else {
eprintln!("skipping: no software rasterizer");
return;
};
let verts = [[-1.0, -1.0], [1.0, -1.0], [1.0, 1.0]];
let a = draw(&mut default_ctx, &verts, &[0, 1, 2]);
let b = draw(&mut sw, &verts, &[0, 1, 2]);
assert_eq!(
a.pixels, b.pixels,
"hardware and software rasterization disagree on triangle coverage"
);
}