use impeller_hal::{
Batch, BlendMode, Extent2D, Material, PassDescriptor, PixelFormat, TextureDescriptor,
};
use impeller_hal_vulkan::{ContextConfig, DevicePreference, VulkanContext};
const SIZE: u32 = 64;
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
}
}
}
const DIAGONAL: [[f32; 2]; 3] = [[-1.0, -1.0], [1.0, -1.0], [-1.0, 0.35]];
const COVERAGE: f32 = 0.3375;
fn render(ctx: &mut VulkanContext, samples: u32) -> Vec<u8> {
let mut batch = Batch::new();
batch
.push(
&DIAGONAL,
&[0, 1, 2],
Material::solid([1.0, 1.0, 1.0, 1.0]),
BlendMode::Src,
)
.expect("push");
let mut tex = ctx
.create_texture(&TextureDescriptor::offscreen(
Extent2D::new(SIZE, SIZE),
PixelFormat::Rgba8Unorm,
))
.expect("texture");
ctx.submit_batch(
&mut tex,
&batch,
PassDescriptor::clear([0.0, 0.0, 0.0, 1.0]).with_samples(samples),
)
.expect("submit");
let pixels = ctx.read_texture(&mut tex).expect("readback");
ctx.destroy_texture(tex);
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 partial_coverage(pixels: &[u8]) -> usize {
pixels
.chunks_exact(4)
.filter(|p| p[0] > 0 && p[0] < 255)
.count()
}
fn coverage_levels(pixels: &[u8]) -> usize {
pixels
.chunks_exact(4)
.map(|p| p[0])
.collect::<std::collections::BTreeSet<u8>>()
.len()
}
fn covered(pixels: &[u8]) -> usize {
pixels.chunks_exact(4).filter(|p| p[0] > 127).count()
}
#[test]
fn single_sampled_edges_are_all_or_nothing() {
let Some(mut ctx) = context() else { return };
let pixels = render(&mut ctx, 1);
assert_eq!(
partial_coverage(&pixels),
0,
"a single-sampled edge should have no intermediate values"
);
}
#[test]
fn multisampling_produces_partial_coverage_along_the_edge() {
let Some(mut ctx) = context() else { return };
if !ctx.capabilities().sample_counts.supports(4) {
eprintln!("skipping: 4x not supported");
return;
}
let pixels = render(&mut ctx, 4);
let partial = partial_coverage(&pixels);
assert!(
partial >= SIZE as usize / 2,
"expected partial coverage along the edge, got {partial} pixels"
);
}
#[test]
fn the_requested_sample_count_is_actually_used() {
let Some(mut ctx) = context() else { return };
let mut seen = Vec::new();
for samples in [1u32, 2, 4, 8] {
if !ctx.capabilities().sample_counts.supports(samples) {
continue;
}
let levels = coverage_levels(&render(&mut ctx, samples));
assert!(
levels <= samples as usize + 1,
"{samples}x produced {levels} levels, more than resolving {samples} samples allows"
);
seen.push((samples, levels));
}
let best = seen.last().copied().expect("at least one sample count");
assert!(
best.1 > 2,
"the highest supported count ({}x) produced only {} levels",
best.0,
best.1
);
for pair in seen.windows(2) {
assert!(
pair[1].1 >= pair[0].1,
"{}x gave {} levels but {}x gave {}",
pair[0].0,
pair[0].1,
pair[1].0,
pair[1].1
);
}
}
#[test]
fn more_samples_never_reduce_the_number_of_intermediate_values() {
let Some(mut ctx) = context() else { return };
let mut previous = 0;
for samples in [1u32, 2, 4, 8] {
if !ctx.capabilities().sample_counts.supports(samples) {
continue;
}
let partial = partial_coverage(&render(&mut ctx, samples));
assert!(
partial >= previous,
"{samples}x produced {partial} intermediate pixels, fewer than the previous {previous}"
);
previous = partial;
}
assert!(previous > 0, "no sample count above one was supported");
}
#[test]
fn multisampling_does_not_move_the_shape() {
let Some(mut ctx) = context() else { return };
if !ctx.capabilities().sample_counts.supports(4) {
eprintln!("skipping: 4x not supported");
return;
}
let aliased = covered(&render(&mut ctx, 1));
let smooth = covered(&render(&mut ctx, 4));
let expected = (SIZE * SIZE) as f32 * COVERAGE;
for (what, count) in [("aliased", aliased), ("multisampled", smooth)] {
let ratio = count as f32 / expected;
assert!(
(ratio - 1.0).abs() < 0.05,
"{what} covered {count}, expected about {expected}"
);
}
}
#[test]
fn interior_pixels_are_untouched_by_multisampling() {
let Some(mut ctx) = context() else { return };
if !ctx.capabilities().sample_counts.supports(4) {
eprintln!("skipping: 4x not supported");
return;
}
let pixels = render(&mut ctx, 4);
let at = |x: u32, y: u32| pixels[((y * SIZE + x) * 4) as usize];
assert_eq!(at(2, SIZE - 3), 255, "deep interior");
assert_eq!(at(SIZE - 3, 2), 0, "deep exterior");
}
#[test]
fn an_unsupported_sample_count_is_refused() {
let Some(mut ctx) = context() else { return };
let mut batch = Batch::new();
batch
.push(
&DIAGONAL,
&[0, 1, 2],
Material::solid([1.0; 4]),
BlendMode::Src,
)
.expect("push");
let mut tex = ctx
.create_texture(&TextureDescriptor::offscreen(
Extent2D::new(16, 16),
PixelFormat::Rgba8Unorm,
))
.expect("texture");
let result = ctx.submit_batch(
&mut tex,
&batch,
PassDescriptor::clear([0.0; 4]).with_samples(3),
);
assert!(result.is_err(), "3x should be refused");
let result = ctx.submit_batch(
&mut tex,
&batch,
PassDescriptor::clear([0.0; 4]).with_samples(128),
);
assert!(result.is_err(), "128x should be refused");
ctx.destroy_texture(tex);
}
#[test]
fn a_multisampled_pass_that_would_preserve_is_refused() {
let Some(mut ctx) = context() else { return };
if !ctx.capabilities().sample_counts.supports(4) {
return;
}
let mut batch = Batch::new();
batch
.push(
&DIAGONAL,
&[0, 1, 2],
Material::solid([1.0; 4]),
BlendMode::Src,
)
.expect("push");
let mut tex = ctx
.create_texture(&TextureDescriptor::offscreen(
Extent2D::new(16, 16),
PixelFormat::Rgba8Unorm,
))
.expect("texture");
let result = ctx.submit_batch(&mut tex, &batch, PassDescriptor::preserve().with_samples(4));
let Err(impeller_hal::Error::Unsupported(message)) = result else {
panic!("expected a refusal, got {result:?}");
};
assert!(
message.contains("clear color") && message.contains("one sample"),
"the refusal names neither remedy: {message}"
);
ctx.submit_batch(
&mut tex,
&batch,
PassDescriptor::clear([0.0; 4]).with_samples(4),
)
.expect("clearing makes it drawable");
ctx.submit_batch(&mut tex, &batch, PassDescriptor::preserve())
.expect("one sample makes it drawable");
ctx.destroy_texture(tex);
}