| // 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}", ¶ms.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", ¶ms.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 |
| } |
| } |