blob: a28a403327e0f6a5954b97520c8a10cf52851dc5 [file] [log] [blame]
// Copyright 2024 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Vello tests.
// LINEBENDER LINT SET - lib.rs - v2
// See https://linebender.org/wiki/canonical-lints/
// These lints aren't included in Cargo.toml because they
// shouldn't apply to examples and tests
#![warn(unused_crate_dependencies)]
#![warn(clippy::print_stdout, clippy::print_stderr)]
// Targeting e.g. 32-bit means structs containing usize can give false positives for 64-bit.
#![cfg_attr(target_pointer_width = "64", warn(clippy::trivially_copy_pass_by_ref))]
// END LINEBENDER LINT SET
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
// The following lints are part of the Linebender standard set,
// but resolving them has been deferred for now.
// Feel free to send a PR that solves one or more of these.
#![allow(
missing_debug_implementations,
unreachable_pub,
missing_docs,
clippy::missing_assert_message,
clippy::shadow_unrelated,
clippy::print_stderr,
clippy::print_stdout,
clippy::allow_attributes_without_reason
)]
use std::env;
use std::fs::File;
use std::io::ErrorKind;
use std::num::NonZeroUsize;
use std::path::Path;
use std::sync::Arc;
use anyhow::{Result, anyhow, bail};
use scenes::{ExampleScene, ImageCache, SceneParams, SimpleText};
use vello::kurbo::{Affine, Vec2};
use vello::peniko::{Blob, Color, Image, ImageFormat, color::palette};
use vello::wgpu::{
self, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, TexelCopyBufferInfo,
TextureDescriptor, TextureFormat, TextureUsages,
};
use vello::{AaConfig, RendererOptions, Scene, util::RenderContext, util::block_on_wgpu};
mod compare;
mod snapshot;
pub use compare::{GpuCpuComparison, compare_gpu_cpu, compare_gpu_cpu_sync};
pub use snapshot::{
Snapshot, SnapshotDirectory, smoke_snapshot_test_sync, snapshot_test, snapshot_test_sync,
};
pub struct TestParams {
pub width: u32,
pub height: u32,
pub base_color: Option<Color>,
pub use_cpu: bool,
pub name: String,
pub anti_aliasing: AaConfig,
}
impl TestParams {
pub fn new(name: impl Into<String>, width: u32, height: u32) -> Self {
Self {
width,
height,
base_color: None,
use_cpu: false,
name: name.into(),
anti_aliasing: AaConfig::Area,
}
}
}
pub fn render_then_debug_sync(scene: &Scene, params: &TestParams) -> Result<Image> {
pollster::block_on(render_then_debug(scene, params))
}
pub async fn render_then_debug(scene: &Scene, params: &TestParams) -> Result<Image> {
let image = get_scene_image(params, scene).await?;
let suffix = if params.use_cpu { "cpu" } else { "gpu" };
let name = format!("{}_{suffix}", &params.name);
let out_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("debug_outputs")
.join(name)
.with_extension("png");
if env_var_relates_to("VELLO_DEBUG_TEST", &params.name, params.use_cpu) {
write_png_to_file(params, &out_path, &image, None)?;
let (width, height) = (image.width, image.height);
println!("Wrote debug result ({width}x{height}) to {out_path:?}");
} else {
match std::fs::remove_file(&out_path) {
Ok(()) => (),
Err(e) if e.kind() == ErrorKind::NotFound => (),
Err(e) => return Err(e.into()),
}
}
Ok(image)
}
pub async fn get_scene_image(params: &TestParams, scene: &Scene) -> Result<Image, anyhow::Error> {
let mut context = RenderContext::new();
let device_id = context
.device(None)
.await
.ok_or_else(|| anyhow!("No compatible device found"))?;
let device_handle = &mut context.devices[device_id];
let device = &device_handle.device;
let queue = &device_handle.queue;
let mut renderer = vello::Renderer::new(
device,
RendererOptions {
use_cpu: params.use_cpu,
num_init_threads: NonZeroUsize::new(1),
antialiasing_support: std::iter::once(params.anti_aliasing).collect(),
pipeline_cache: None,
},
)
.or_else(|_| bail!("Got non-Send/Sync error from creating renderer"))?;
let width = params.width;
let height = params.height;
let render_params = vello::RenderParams {
base_color: params.base_color.unwrap_or(palette::css::BLACK),
width,
height,
antialiasing_method: params.anti_aliasing,
};
let size = Extent3d {
width,
height,
depth_or_array_layers: 1,
};
let target = device.create_texture(&TextureDescriptor {
label: Some("Target texture"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::STORAGE_BINDING | TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = target.create_view(&wgpu::TextureViewDescriptor::default());
renderer
.render_to_texture(device, queue, scene, &view, &render_params)
.or_else(|_| bail!("Got non-Send/Sync error from rendering"))?;
let padded_byte_width = (width * 4).next_multiple_of(256);
let buffer_size = padded_byte_width as u64 * height as u64;
let buffer = device.create_buffer(&BufferDescriptor {
label: Some("val"),
size: buffer_size,
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("Copy out buffer"),
});
encoder.copy_texture_to_buffer(
target.as_image_copy(),
TexelCopyBufferInfo {
buffer: &buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded_byte_width),
rows_per_image: None,
},
},
size,
);
queue.submit([encoder.finish()]);
let buf_slice = buffer.slice(..);
let (sender, receiver) = futures_intrusive::channel::shared::oneshot_channel();
buf_slice.map_async(wgpu::MapMode::Read, move |v| sender.send(v).unwrap());
if let Some(recv_result) = block_on_wgpu(device, receiver.receive()) {
recv_result?;
} else {
bail!("channel was closed");
}
let data = buf_slice.get_mapped_range();
let mut result_unpadded = Vec::<u8>::with_capacity((width * height * 4).try_into()?);
for row in 0..height {
let start = (row * padded_byte_width).try_into()?;
result_unpadded.extend(&data[start..start + (width * 4) as usize]);
}
let data = Blob::new(Arc::new(result_unpadded));
let image = Image::new(data, ImageFormat::Rgba8, width, height);
Ok(image)
}
pub fn write_png_to_file(
params: &TestParams,
out_path: &Path,
image: &Image,
max_size_in_bytes: Option<u64>,
) -> Result<(), anyhow::Error> {
let width = params.width;
let height = params.height;
let mut file = File::create(out_path)?;
let mut encoder = png::Encoder::new(&mut file, width, height);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder.write_header()?;
writer.write_image_data(image.data.data())?;
writer.finish()?;
let size = file.metadata().unwrap().len();
drop(file);
let oversized_path = out_path.with_extension("oversized.png");
if max_size_in_bytes.is_some_and(|max_size_in_bytes| size > max_size_in_bytes) {
std::fs::rename(out_path, &oversized_path)?;
bail!(
"File was oversized, expected {} bytes, got {size} bytes. New file written to {to}",
max_size_in_bytes.unwrap(),
to = oversized_path.display()
);
} else {
// Intentionally do not handle errors here
drop(std::fs::remove_file(oversized_path));
}
Ok(())
}
/// Determine whether the value of the environment variable `env_var`
/// includes a specific test.
/// This is used when updating tests, or dumping the debug output
fn env_var_relates_to(env_var: &'static str, name: &str, use_cpu: bool) -> bool {
if let Ok(val) = env::var(env_var) {
if val.eq_ignore_ascii_case("all")
|| val.eq_ignore_ascii_case("cpu") && use_cpu
|| val.eq_ignore_ascii_case("gpu") && !use_cpu
{
return true;
}
for test in val.split(',') {
if use_cpu {
let test_name = test.trim_end_matches("_cpu");
if test_name.eq_ignore_ascii_case(name) {
return true;
}
} else {
let test_name = test.trim_end_matches("_gpu");
if test_name.eq_ignore_ascii_case(name) {
return true;
}
}
}
}
false
}
pub fn encode_test_scene(mut test_scene: ExampleScene, test_params: &mut TestParams) -> Scene {
let mut inner_scene = Scene::new();
let mut image_cache = ImageCache::new();
let mut text = SimpleText::new();
let mut scene_params = SceneParams {
base_color: None,
complexity: 100,
time: 0.,
images: &mut image_cache,
interactive: false,
resolution: None,
text: &mut text,
};
test_scene
.function
.render(&mut inner_scene, &mut scene_params);
if test_params.base_color.is_none() {
test_params.base_color = scene_params.base_color;
}
if let Some(resolution) = scene_params.resolution {
// Automatically scale the rendering to fill as much of the window as possible
let factor = Vec2::new(test_params.width as f64, test_params.height as f64);
let scale_factor = (factor.x / resolution.x).min(factor.y / resolution.y);
let mut outer_scene = Scene::new();
outer_scene.append(&inner_scene, Some(Affine::scale(scale_factor)));
outer_scene
} else {
inner_scene
}
}