use impeller_hal::{
Batch, BlendMode, ClipState, ColorFilter, Extent2D, Material, PassDescriptor, PixelFormat,
Scissor, TextureDescriptor,
};
use impeller_hal_vulkan::{ContextConfig, DevicePreference, VulkanContext};
const SIZE: Extent2D = Extent2D {
width: 32,
height: 32,
};
const QUAD: [u32; 6] = [0, 1, 2, 0, 2, 3];
const BACKGROUND: [f32; 4] = [0.0, 0.0, 0.0, 1.0];
const WHITE: [f32; 4] = [1.0, 1.0, 1.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
}
}
}
fn rect(x0: f32, y0: f32, x1: f32, y1: f32) -> [[f32; 2]; 4] {
let to_clip = |x: f32, y: f32| {
[
x / SIZE.width as f32 * 2.0 - 1.0,
1.0 - y / SIZE.height as f32 * 2.0,
]
};
[
to_clip(x0, y0),
to_clip(x1, y0),
to_clip(x1, y1),
to_clip(x0, y1),
]
}
const FULL: [[f32; 2]; 4] = [[-1.0, -1.0], [1.0, -1.0], [1.0, 1.0], [-1.0, 1.0]];
fn render(ctx: &mut VulkanContext, batch: &Batch, samples: u32) -> Vec<u8> {
let mut target = ctx
.create_texture(&TextureDescriptor::offscreen(SIZE, PixelFormat::Rgba8Unorm))
.expect("texture");
ctx.submit_batch(
&mut target,
batch,
PassDescriptor::clear(BACKGROUND).with_samples(samples),
)
.expect("submit");
let pixels = ctx.read_texture(&mut target).expect("readback");
ctx.destroy_texture(target);
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:?}");
pixels
}
fn lit(pixels: &[u8], x: u32, y: u32) -> bool {
pixels[((y * SIZE.width + x) * 4) as usize] > 128
}
fn bounds(pixels: &[u8]) -> Option<Scissor> {
let (mut x0, mut y0, mut x1, mut y1) = (u32::MAX, u32::MAX, 0u32, 0u32);
let mut any = false;
for y in 0..SIZE.height {
for x in 0..SIZE.width {
if lit(pixels, x, y) {
any = true;
x0 = x0.min(x);
y0 = y0.min(y);
x1 = x1.max(x);
y1 = y1.max(y);
}
}
}
any.then(|| Scissor::new(x0, y0, x1 - x0 + 1, y1 - y0 + 1))
}
#[test]
fn a_stencil_clip_confines_content_to_the_clip_geometry() {
let Some(mut ctx) = context() else { return };
let mut batch = Batch::new();
batch
.push_with(
&rect(6.0, 4.0, 20.0, 25.0),
&QUAD,
Material::solid(WHITE),
ColorFilter::None,
BlendMode::Src,
None,
ClipState::narrow(0),
)
.expect("clip");
batch
.push_with(
&FULL,
&QUAD,
Material::solid(WHITE),
ColorFilter::None,
BlendMode::Src,
None,
ClipState::content(1),
)
.expect("content");
let pixels = render(&mut ctx, &batch, 1);
assert_eq!(bounds(&pixels), Some(Scissor::new(6, 4, 14, 21)));
}
#[test]
fn a_clip_draw_writes_no_color_of_its_own() {
let Some(mut ctx) = context() else { return };
let mut batch = Batch::new();
batch
.push_with(
&rect(6.0, 4.0, 20.0, 25.0),
&QUAD,
Material::solid(WHITE),
ColorFilter::None,
BlendMode::Src,
None,
ClipState::narrow(0),
)
.expect("clip");
let pixels = render(&mut ctx, &batch, 1);
assert_eq!(bounds(&pixels), None, "the clip geometry painted itself");
}
#[test]
fn nested_clips_intersect() {
let Some(mut ctx) = context() else { return };
let mut batch = Batch::new();
batch
.push_with(
&rect(4.0, 4.0, 24.0, 20.0),
&QUAD,
Material::solid(WHITE),
ColorFilter::None,
BlendMode::Src,
None,
ClipState::narrow(0),
)
.expect("outer");
batch
.push_with(
&rect(12.0, 10.0, 30.0, 28.0),
&QUAD,
Material::solid(WHITE),
ColorFilter::None,
BlendMode::Src,
None,
ClipState::narrow(1),
)
.expect("inner");
batch
.push_with(
&FULL,
&QUAD,
Material::solid(WHITE),
ColorFilter::None,
BlendMode::Src,
None,
ClipState::content(2),
)
.expect("content");
let pixels = render(&mut ctx, &batch, 1);
assert_eq!(
bounds(&pixels),
Some(Scissor::new(12, 10, 12, 10)),
"the two clips did not intersect"
);
}
#[test]
fn widening_undoes_a_clip_for_what_follows() {
let Some(mut ctx) = context() else { return };
let mut batch = Batch::new();
let clip = rect(4.0, 4.0, 16.0, 16.0);
batch
.push_with(
&clip,
&QUAD,
Material::solid(WHITE),
ColorFilter::None,
BlendMode::Src,
None,
ClipState::narrow(0),
)
.expect("narrow");
batch
.push_with(
&clip,
&QUAD,
Material::solid(WHITE),
ColorFilter::None,
BlendMode::Src,
None,
ClipState::widen(1),
)
.expect("widen");
batch
.push_with(
&FULL,
&QUAD,
Material::solid(WHITE),
ColorFilter::None,
BlendMode::Src,
None,
ClipState::content(0),
)
.expect("content");
let pixels = render(&mut ctx, &batch, 1);
assert_eq!(
bounds(&pixels),
Some(Scissor::covering(SIZE)),
"widening did not restore the previous clip"
);
}
#[test]
fn a_scissor_and_a_stencil_clip_both_apply() {
let Some(mut ctx) = context() else { return };
let mut batch = Batch::new();
batch
.push_with(
&rect(4.0, 4.0, 24.0, 24.0),
&QUAD,
Material::solid(WHITE),
ColorFilter::None,
BlendMode::Src,
None,
ClipState::narrow(0),
)
.expect("clip");
batch
.push_with(
&FULL,
&QUAD,
Material::solid(WHITE),
ColorFilter::None,
BlendMode::Src,
Some(Scissor::new(10, 0, 30, 12)),
ClipState::content(1),
)
.expect("content");
let pixels = render(&mut ctx, &batch, 1);
assert_eq!(bounds(&pixels), Some(Scissor::new(10, 4, 14, 8)));
}
#[test]
fn a_batch_that_clips_nothing_still_renders() {
let Some(mut ctx) = context() else { return };
let mut batch = Batch::new();
batch
.push(&FULL, &QUAD, Material::solid(WHITE), BlendMode::Src)
.expect("content");
assert!(!batch.uses_stencil());
let pixels = render(&mut ctx, &batch, 1);
assert_eq!(bounds(&pixels), Some(Scissor::covering(SIZE)));
}
#[test]
fn a_stencil_clip_works_multisampled() {
let Some(mut ctx) = context() else { return };
if !ctx.capabilities().sample_counts.supports(4) {
eprintln!("skipping: 4x not supported");
return;
}
let mut batch = Batch::new();
batch
.push_with(
&rect(6.0, 4.0, 20.0, 25.0),
&QUAD,
Material::solid(WHITE),
ColorFilter::None,
BlendMode::Src,
None,
ClipState::narrow(0),
)
.expect("clip");
batch
.push_with(
&FULL,
&QUAD,
Material::solid(WHITE),
ColorFilter::None,
BlendMode::Src,
None,
ClipState::content(1),
)
.expect("content");
let pixels = render(&mut ctx, &batch, 4);
assert_eq!(bounds(&pixels), Some(Scissor::new(6, 4, 14, 21)));
}
#[test]
fn a_clip_stack_deeper_than_the_stencil_is_refused() {
let Some(mut ctx) = context() else { return };
let mut batch = Batch::new();
batch
.push_with(
&FULL,
&QUAD,
Material::solid(WHITE),
ColorFilter::None,
BlendMode::Src,
None,
ClipState::narrow(255),
)
.expect("push");
let mut target = ctx
.create_texture(&TextureDescriptor::offscreen(SIZE, PixelFormat::Rgba8Unorm))
.expect("texture");
let result = ctx.submit_batch(&mut target, &batch, PassDescriptor::clear(BACKGROUND));
ctx.destroy_texture(target);
assert!(
matches!(result, Err(impeller_hal::Error::LimitExceeded { .. })),
"a clip stack past the stencil's range was accepted: {result:?}"
);
}