blob: 497b70881cab631911d65a08384fc93c48b58726 [file] [log] [blame]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use crate::{
Attribute, AttributeInput, DEFAULT_CPU_F32_TOLERANCE, DEFAULT_CPU_U8_TOLERANCE,
DEFAULT_HYBRID_TOLERANCE, parse_int_lit, parse_string_lit,
};
use proc_macro::TokenStream;
use proc_macro2::Ident;
use quote::quote;
use syn::{ItemFn, parse_macro_input};
struct Arguments {
/// The width of the scene.
width: u16,
/// The height of the scene.
height: u16,
/// The (additional) maximum tolerance for how much two pixels are allowed to deviate from each other
/// when comparing to the reference images. Some renderers already have an existing tolerance
/// (see the constants at the top of the file), this value will simply be added
/// to the currently existing threshold. See the top of the file for an explanation of
/// how exactly the tolerance is interpreted.
cpu_u8_tolerance: u8,
/// Same as above, but for the hybrid renderer.
hybrid_tolerance: u8,
/// Whether the background should be transparent (the default is white).
transparent: bool,
/// Whether the test should not be run on the CPU (`vello_cpu`).
skip_cpu: bool,
/// Whether the test should not be run on the GPU (`vello_hybrid`).
skip_hybrid: bool,
/// Whether no reference image should actually be created (for tests that only check
/// for panics, but are not interested in the actual output).
no_ref: bool,
/// A reason for ignoring a test.
ignore_reason: Option<String>,
}
impl Default for Arguments {
fn default() -> Self {
Self {
width: 100,
height: 100,
cpu_u8_tolerance: 0,
hybrid_tolerance: 0,
transparent: false,
skip_cpu: false,
skip_hybrid: false,
no_ref: false,
ignore_reason: None,
}
}
}
pub(crate) fn vello_test_inner(attr: TokenStream, item: TokenStream) -> TokenStream {
let attrs = parse_macro_input!(attr as AttributeInput);
let input_fn = parse_macro_input!(item as ItemFn);
let input_fn_name = input_fn.sig.ident.clone();
let u8_fn_name = Ident::new(&format!("{}_cpu_u8", input_fn_name), input_fn_name.span());
let f32_fn_name = Ident::new(&format!("{}_cpu_f32", input_fn_name), input_fn_name.span());
let hybrid_fn_name = Ident::new(&format!("{}_hybrid", input_fn_name), input_fn_name.span());
let webgl_fn_name = Ident::new(
&format!("{}_hybrid_webgl", input_fn_name),
input_fn_name.span(),
);
// TODO: Tests with the same names in different modules can clash, see
// https://github.com/linebender/vello/pull/925#discussion_r2070710362.
// We should take the module path into consideration for naming the tests.
let input_fn_name_str = input_fn_name.to_string();
let u8_fn_name_str = u8_fn_name.to_string();
let f32_fn_name_str = f32_fn_name.to_string();
let hybrid_fn_name_str = hybrid_fn_name.to_string();
let webgl_fn_name_str = webgl_fn_name.to_string();
let Arguments {
width,
height,
cpu_u8_tolerance,
mut hybrid_tolerance,
transparent,
skip_cpu,
mut skip_hybrid,
ignore_reason,
no_ref,
} = parse_args(&attrs);
// Wasm doesn't have access to the filesystem. For wasm, inline the snapshot bytes into the
// binary.
let reference_image_name = Ident::new(
&format!(
"{}_REFERENCE_IMAGE",
input_fn_name.to_string().to_uppercase()
),
input_fn_name.span(),
);
let reference_image_const = if !no_ref {
quote! {
#[cfg(target_arch = "wasm32")]
const #reference_image_name: &[u8] = include_bytes!(
concat!(env!("CARGO_MANIFEST_DIR"), "/snapshots/", #input_fn_name_str, ".png")
);
#[cfg(not(target_arch = "wasm32"))]
const #reference_image_name: &[u8] = &[];
}
} else {
quote! {
const #reference_image_name: &[u8] = &[];
}
};
let cpu_u8_tolerance = cpu_u8_tolerance + DEFAULT_CPU_U8_TOLERANCE;
// Since f32 is our gold standard, we always require exact matches for this one.
let cpu_f32_tolerance = DEFAULT_CPU_F32_TOLERANCE;
hybrid_tolerance += DEFAULT_HYBRID_TOLERANCE;
// These tests currently don't work with `vello_hybrid`.
skip_hybrid |= {
input_fn_name_str.contains("clip")
|| input_fn_name_str.contains("compose")
|| input_fn_name_str.contains("gradient")
|| input_fn_name_str.contains("image")
|| input_fn_name_str.contains("layer")
|| input_fn_name_str.contains("mask")
|| input_fn_name_str.contains("mix")
|| input_fn_name_str.contains("opacity")
|| input_fn_name_str.contains("blurred_rounded_rect")
};
let empty_snippet = quote! {};
let ignore_snippet = if let Some(reason) = ignore_reason {
quote! {#[ignore = #reason]}
} else {
quote! {#[ignore]}
};
let ignore_hybrid = if skip_hybrid {
ignore_snippet.clone()
} else {
empty_snippet.clone()
};
let ignore_cpu = if skip_cpu {
ignore_snippet.clone()
} else {
empty_snippet.clone()
};
let cpu_snippet = |fn_name: Ident,
fn_name_str: String,
tolerance: u8,
is_reference: bool,
render_mode: proc_macro2::TokenStream| {
quote! {
#ignore_cpu
#[test]
fn #fn_name() {
use crate::util::{
check_ref, get_ctx
};
use vello_cpu::{RenderContext, RenderMode};
let mut ctx = get_ctx::<RenderContext>(#width, #height, #transparent);
#input_fn_name(&mut ctx);
if !#no_ref {
check_ref(&ctx, #input_fn_name_str, #fn_name_str, #tolerance, #is_reference, #render_mode, #reference_image_name);
}
}
}
};
let u8_snippet = cpu_snippet(
u8_fn_name,
u8_fn_name_str,
cpu_u8_tolerance,
false,
quote! { RenderMode::OptimizeSpeed },
);
let f32_snippet = cpu_snippet(
f32_fn_name,
f32_fn_name_str,
cpu_f32_tolerance,
true,
quote! { RenderMode::OptimizeQuality },
);
let expanded = quote! {
#input_fn
#reference_image_const
#u8_snippet
#f32_snippet
#ignore_hybrid
#[test]
fn #hybrid_fn_name() {
use crate::util::{
check_ref, get_ctx
};
use vello_hybrid::Scene;
use vello_cpu::RenderMode;
let mut ctx = get_ctx::<Scene>(#width, #height, #transparent);
#input_fn_name(&mut ctx);
if !#no_ref {
check_ref(&ctx, #input_fn_name_str, #hybrid_fn_name_str, #hybrid_tolerance, false, RenderMode::OptimizeSpeed, #reference_image_name);
}
}
#ignore_hybrid
#[cfg(all(target_arch = "wasm32", feature = "webgl"))]
#[wasm_bindgen_test::wasm_bindgen_test]
async fn #webgl_fn_name() {
use crate::util::{
check_ref, get_ctx
};
use vello_hybrid::Scene;
use vello_cpu::RenderMode;
let mut ctx = get_ctx::<Scene>(#width, #height, #transparent);
#input_fn_name(&mut ctx);
if !#no_ref {
check_ref(&ctx, #input_fn_name_str, #webgl_fn_name_str, #hybrid_tolerance, false, RenderMode::OptimizeSpeed, #reference_image_name);
}
}
};
expanded.into()
}
fn parse_args(attribute_input: &AttributeInput) -> Arguments {
let mut args = Arguments::default();
for arg in &attribute_input.args {
match arg {
Attribute::KeyValue { key, expr, .. } => {
let key_str = key.to_string();
match key_str.as_str() {
"ignore" => {
args.skip_cpu = true;
args.skip_hybrid = true;
args.ignore_reason = Some(parse_string_lit(expr, "ignore"));
}
"width" => args.width = parse_int_lit(expr, "width"),
"height" => args.height = parse_int_lit(expr, "height"),
"cpu_u8_tolerance" => {
args.cpu_u8_tolerance = parse_int_lit(expr, "cpu_u8_tolerance")
.try_into()
.expect("value to fit for cpu_tolerance.");
}
"hybrid_tolerance" => {
args.hybrid_tolerance = parse_int_lit(expr, "hybrid_tolerance")
.try_into()
.expect("value to fit for hybrid_tolerance.");
}
_ => panic!("unknown pair attribute {}", key_str),
}
}
Attribute::Flag(flag_ident) => {
let flag_str = flag_ident.to_string();
match flag_str.as_str() {
"transparent" => args.transparent = true,
"skip_cpu" => args.skip_cpu = true,
"skip_hybrid" => args.skip_hybrid = true,
"no_ref" => args.no_ref = true,
"ignore" => {
args.skip_cpu = true;
args.skip_hybrid = true;
}
_ => panic!("unknown flag attribute {}", flag_str),
}
}
}
}
args
}