use impeller_hal::{BlendMode, Extent2D, Material, PixelFormat, TextureDescriptor};
use impeller_hal_vulkan::Validated;
use impeller_hal_vulkan::{ContextConfig, DevicePreference, VulkanContext};
const SIZE: u32 = 16;
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 composite(
ctx: &mut VulkanContext,
background: [f32; 4],
draws: &[([f32; 4], BlendMode)],
) -> [u8; 4] {
let mut tex = ctx
.create_texture(&TextureDescriptor::offscreen(
Extent2D::new(SIZE, SIZE),
PixelFormat::Rgba8Unorm,
))
.expect("texture");
let mut clear = Some(background);
for (color, blend) in draws {
ctx.draw_indexed(
&mut tex,
&FULL,
&QUAD,
Material::solid(*color),
*blend,
clear,
)
.expect("draw");
clear = None;
}
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:?}");
let i = ((SIZE / 2 * SIZE + SIZE / 2) * 4) as usize;
[pixels[i], pixels[i + 1], pixels[i + 2], pixels[i + 3]]
}
fn assert_near(got: [u8; 4], want: [u8; 4], what: &str) {
for i in 0..4 {
assert!(
(got[i] as i32 - want[i] as i32).abs() <= 1,
"{what}: got {got:?}, expected about {want:?}"
);
}
}
#[test]
fn half_alpha_source_over_opaque_lands_halfway() {
let Some(mut ctx) = context() else { return };
let got = composite(
&mut ctx,
[0.0, 0.0, 1.0, 1.0],
&[([1.0, 0.0, 0.0, 0.5], BlendMode::SrcOver)],
);
assert_near(got, [128, 0, 128, 255], "half over opaque");
}
#[test]
fn a_fully_transparent_source_leaves_the_destination_untouched() {
let Some(mut ctx) = context() else { return };
let got = composite(
&mut ctx,
[0.0, 0.0, 1.0, 1.0],
&[([1.0, 0.0, 0.0, 0.0], BlendMode::SrcOver)],
);
assert_near(got, [0, 0, 255, 255], "transparent source");
}
#[test]
fn an_opaque_source_over_replaces_the_destination() {
let Some(mut ctx) = context() else { return };
let got = composite(
&mut ctx,
[0.0, 0.0, 1.0, 1.0],
&[([1.0, 0.0, 0.0, 1.0], BlendMode::SrcOver)],
);
assert_near(got, [255, 0, 0, 255], "opaque source over");
}
#[test]
fn stacked_translucent_draws_accumulate_correctly() {
let Some(mut ctx) = context() else { return };
let got = composite(
&mut ctx,
[0.0, 0.0, 1.0, 1.0],
&[
([1.0, 0.0, 0.0, 0.5], BlendMode::SrcOver),
([1.0, 0.0, 0.0, 0.5], BlendMode::SrcOver),
],
);
assert_near(got, [191, 0, 64, 255], "two stacked halves");
}
#[test]
fn src_overwrites_alpha_where_source_over_would_accumulate_it() {
let Some(mut ctx) = context() else { return };
let half_red = [1.0, 0.0, 0.0, 0.5];
let opaque_blue = [0.0, 0.0, 1.0, 1.0];
let over = composite(&mut ctx, opaque_blue, &[(half_red, BlendMode::SrcOver)]);
let replaced = composite(&mut ctx, opaque_blue, &[(half_red, BlendMode::Src)]);
assert_near(over, [128, 0, 128, 255], "source over");
assert_near(replaced, [128, 0, 0, 128], "src");
}
#[test]
fn stored_color_is_premultiplied() {
let Some(mut ctx) = context() else { return };
let got = composite(
&mut ctx,
[0.0, 0.0, 0.0, 0.0],
&[([1.0, 0.0, 0.0, 0.5], BlendMode::Src)],
);
assert_near(got, [128, 0, 0, 128], "premultiplied storage");
}
#[test]
fn blend_modes_are_cached_as_separate_pipelines() {
let Some(mut ctx) = context() else { return };
for _ in 0..3 {
let over = composite(
&mut ctx,
[0.0, 0.0, 1.0, 1.0],
&[([1.0, 0.0, 0.0, 0.5], BlendMode::SrcOver)],
);
assert_near(over, [128, 0, 128, 255], "source over");
let src = composite(
&mut ctx,
[0.0, 0.0, 1.0, 1.0],
&[([1.0, 0.0, 0.0, 0.5], BlendMode::Src)],
);
assert_near(src, [128, 0, 0, 128], "src");
}
}
#[test]
fn the_software_reference_blends_identically() {
let Some(mut ctx) = context() else { return };
let Ok(mut sw) = Validated::new(DevicePreference::Software) else {
eprintln!("skipping: no software rasterizer");
return;
};
let draws = [
([1.0, 0.0, 0.0, 0.5], BlendMode::SrcOver),
([0.0, 1.0, 0.0, 0.25], BlendMode::SrcOver),
];
let hardware = composite(&mut ctx, [0.0, 0.0, 1.0, 1.0], &draws);
let software = composite(&mut sw, [0.0, 0.0, 1.0, 1.0], &draws);
assert_near(hardware, software, "blending diverged across drivers");
}
fn advanced_context() -> Option<VulkanContext> {
for device in [DevicePreference::Auto, DevicePreference::Software] {
let Ok(ctx) = VulkanContext::with_config(ContextConfig {
device,
validation: true,
}) else {
continue;
};
if ctx.capabilities().advanced_blend {
return Some(ctx);
}
}
eprintln!("skipping: no device offers advanced blending");
None
}
#[test]
fn the_advanced_blend_pipeline_state_is_valid() {
let Some(mut ctx) = advanced_context() else {
return;
};
if !ctx.validation_active() {
eprintln!("skipping: no validation layer installed");
return;
}
for mode in BlendMode::ADVANCED {
composite(
&mut ctx,
[0.2, 0.5, 0.7, 1.0],
&[([0.8, 0.3, 0.6, 0.6], *mode)],
);
}
}
#[test]
fn a_device_without_the_extension_refuses_advanced_modes() {
let Some(mut ctx) = context() else { return };
if ctx.capabilities().advanced_blend {
eprintln!("skipping: the preferred device offers advanced blending");
return;
}
let mut tex = ctx
.create_texture(&TextureDescriptor::offscreen(
Extent2D::new(SIZE, SIZE),
PixelFormat::Rgba8Unorm,
))
.expect("texture");
for mode in BlendMode::ADVANCED {
let result = ctx.draw_indexed(
&mut tex,
&FULL,
&QUAD,
Material::solid([1.0, 0.0, 0.0, 0.5]),
*mode,
None,
);
assert!(
matches!(result, Err(impeller_hal::Error::Unsupported(_))),
"{mode} was accepted by a device that cannot do it"
);
}
ctx.destroy_texture(tex);
}