| // Copyright 2024 the Vello Authors |
| // SPDX-License-Identifier: Apache-2.0 OR MIT |
| |
| use std::{ |
| io::ErrorKind, |
| path::{Path, PathBuf}, |
| }; |
| |
| use anyhow::{Result, anyhow, bail}; |
| use image::DynamicImage; |
| use nv_flip::FlipPool; |
| use vello::{ |
| Scene, |
| peniko::{Image, ImageFormat}, |
| }; |
| |
| use crate::{TestParams, render_then_debug, write_png_to_file}; |
| |
| fn comparison_dir() -> PathBuf { |
| Path::new(env!("CARGO_MANIFEST_DIR")).join("comparisons") |
| } |
| |
| #[must_use = "A snapshot test doesn't do anything unless an assertion method is called on it"] |
| /// A scene rendered on the CPU and the same scene rendered on the GPU, and information about their differences. |
| /// |
| /// Use an assertion method or access `statistics` to make a determination based on the result of this test. |
| pub struct GpuCpuComparison { |
| pub statistics: Option<FlipPool>, |
| pub cpu_path: PathBuf, |
| pub gpu_path: PathBuf, |
| pub cpu_rendered: Image, |
| pub gpu_rendered: Image, |
| pub params: TestParams, |
| } |
| |
| impl GpuCpuComparison { |
| /// Assert that that mean value stored in `statistics` is less than `value`. |
| /// |
| /// This is a high-level measure of how different the GPU and CPU results are. |
| /// This should be expected to be small, a large value would represent a |
| /// significant difference between the two results. |
| /// |
| /// However, this value could potentially be non-zero (i.e. there is a slight difference |
| /// between the GPU and CPU results) due to fast math on the GPU or different precisions |
| /// used in the renderers. |
| pub fn assert_mean_less_than(&mut self, value: f32) -> &mut Self { |
| assert!( |
| value < 0.1, |
| "Mean should be less than 0.1 in almost all cases for a successful test" |
| ); |
| if let Some(stats) = &self.statistics { |
| let mean = stats.mean(); |
| if mean > value { |
| self.handle_failure(format!("Expected mean to be less than {value}, got {mean}")) |
| .unwrap(); |
| } |
| } else { |
| // The result image was newly created, and so we know the test will pass |
| } |
| self.handle_success().unwrap(); |
| self |
| } |
| |
| fn handle_success(&mut self) -> Result<()> { |
| match std::fs::remove_file(&self.cpu_path) { |
| Err(e) if e.kind() == ErrorKind::NotFound => (), |
| res => return res.map_err(Into::into), |
| } |
| match std::fs::remove_file(&self.gpu_path) { |
| Err(e) if e.kind() == ErrorKind::NotFound => Ok(()), |
| res => res.map_err(Into::into), |
| } |
| } |
| |
| fn handle_failure(&mut self, message: String) -> Result<()> { |
| write_png_to_file(&self.params, &self.cpu_path, &self.cpu_rendered, None)?; |
| write_png_to_file(&self.params, &self.gpu_path, &self.gpu_rendered, None)?; |
| eprintln!( |
| "Wrote CPU result from test {} to {:?}\n\ |
| Wrote GPU result to {:?}\n", |
| self.params.name, &self.cpu_path, &self.gpu_path |
| ); |
| |
| bail!("{}", message); |
| } |
| } |
| |
| /// Run a scene comparing the outputs from the CPU and GPU renderers |
| pub fn compare_gpu_cpu_sync(scene: Scene, params: TestParams) -> Result<GpuCpuComparison> { |
| pollster::block_on(compare_gpu_cpu(scene, params)) |
| } |
| |
| /// Run a scene comparing the outputs from the CPU and GPU renderers |
| pub async fn compare_gpu_cpu(scene: Scene, mut params: TestParams) -> Result<GpuCpuComparison> { |
| params.use_cpu = false; |
| // TODO: Reuse the same RenderContext? |
| let gpu_rendered = render_then_debug(&scene, ¶ms).await?; |
| params.use_cpu = true; |
| let cpu_rendered = render_then_debug(&scene, ¶ms).await?; |
| |
| let path_root = &comparison_dir(); |
| let cpu_dir = path_root.join("cpu"); |
| std::fs::create_dir_all(&cpu_dir)?; |
| let gpu_dir = path_root.join("gpu"); |
| std::fs::create_dir_all(&gpu_dir)?; |
| let cpu_path = cpu_dir.join(¶ms.name).with_extension(".png"); |
| let gpu_path = path_root.join(¶ms.name).with_extension(".png"); |
| |
| assert!(gpu_rendered.width == cpu_rendered.width && gpu_rendered.height == cpu_rendered.height,); |
| |
| // Compare the images using nv-flip |
| assert_eq!(cpu_rendered.format, ImageFormat::Rgba8); |
| assert_eq!(gpu_rendered.format, ImageFormat::Rgba8); |
| let gpu_rendered_data: DynamicImage = image::RgbaImage::from_raw( |
| cpu_rendered.width, |
| cpu_rendered.height, |
| cpu_rendered.data.as_ref().to_vec(), |
| ) |
| .ok_or(anyhow!("Couldn't create image for cpu result"))? |
| .into(); |
| let gpu_rendered_data = gpu_rendered_data.to_rgb8(); |
| |
| let cpu_rendered_data: DynamicImage = image::RgbaImage::from_raw( |
| cpu_rendered.width, |
| cpu_rendered.height, |
| cpu_rendered.data.as_ref().to_vec(), |
| ) |
| .ok_or(anyhow!("Couldn't create image for cpu result"))? |
| .into(); |
| let cpu_rendered_data = cpu_rendered_data.to_rgb8(); |
| |
| let cpu_flip = nv_flip::FlipImageRgb8::with_data( |
| cpu_rendered_data.width(), |
| cpu_rendered_data.height(), |
| &cpu_rendered_data, |
| ); |
| let gpu_flip = nv_flip::FlipImageRgb8::with_data( |
| gpu_rendered_data.width(), |
| gpu_rendered_data.height(), |
| &gpu_rendered_data, |
| ); |
| |
| let error_map = nv_flip::flip(cpu_flip, gpu_flip, nv_flip::DEFAULT_PIXELS_PER_DEGREE); |
| |
| let pool = FlipPool::from_image(&error_map); |
| |
| Ok(GpuCpuComparison { |
| statistics: Some(pool), |
| cpu_path, |
| gpu_path, |
| cpu_rendered, |
| gpu_rendered, |
| params, |
| }) |
| } |