//! Reference plots for different backends. //! //! Use the `SNAPSHOT_UPDATE` to generate / update snapshot images: //! //! ```bash //! SNAPSHOT_UPDATE=1 cargo run test --test references --features cpu_backend //! ``` use image::{DynamicImage, ImageError}; use std::{env, io, path::Path}; use julia_set::{Backend, ImageBuffer, Params, Render}; const IMAGE_SIZE: [u32; 2] = [360, 360]; fn generate_image + Default>(function: F, params: &Params) -> ImageBuffer { B::default() .create_program(function) .unwrap() .render(params) .unwrap() } fn compare_to_reference(reference_filename: &str, image: &ImageBuffer) { const ROOT_PATH: &str = env!("CARGO_MANIFEST_DIR"); let reference_path = Path::new(ROOT_PATH) .join("tests") .join("__snapshots__") .join(reference_filename); let reference_image = match image::open(&reference_path) { Ok(DynamicImage::ImageLuma8(image)) => Some(image), Ok(_) => panic!("Unexpected image format"), Err(ImageError::IoError(ref io_error)) if io_error.kind() == io::ErrorKind::NotFound => { None } Err(other_error) => panic!("Error opening reference file: {:?}", other_error), }; if let Some(ref reference_image) = reference_image { let image_diff = ImageDiff::new(reference_image, &image); println!("{}: {:?}", reference_filename, image_diff); image_diff.assert_is_sound(); return; } else if let Ok(snapshot_update) = env::var("SNAPSHOT_UPDATE") { if snapshot_update == "1" { // Store the reference image. image .save(&reference_path) .expect("Cannot save reference image"); return; } } panic!("Snapshot `{}` not found", reference_filename); } #[derive(Debug)] struct ImageDiff { /// Percentage of differing image pixels. differing_pixels: f32, /// Mean difference in pixel luma across all pixels in the image. mean_difference: f32, } impl ImageDiff { const MAX_DIFFERING_PIXELS: f32 = 0.1; const MAX_MEAN_DIFFERENCE: f32 = 2.0; fn pixel_quantity(image: &ImageBuffer, quantity: u32) -> f32 { let pixel_count = image.width() * image.height(); quantity as f32 / pixel_count as f32 } fn new(expected: &ImageBuffer, actual: &ImageBuffer) -> Self { assert_eq!(expected.width(), actual.width()); assert_eq!(expected.height(), actual.height()); let mut differing_count = 0_u32; let mut total_diff = 0_u32; for (expected_pixel, actual_pixel) in expected.pixels().zip(actual.pixels()) { let diff = if expected_pixel[0] > actual_pixel[0] { expected_pixel[0] - actual_pixel[0] } else { actual_pixel[0] - expected_pixel[0] }; if diff > 0 { differing_count += 1; total_diff += diff as u32; } } let differing_pixels = Self::pixel_quantity(&expected, differing_count); let mean_difference = Self::pixel_quantity(&expected, total_diff); Self { differing_pixels, mean_difference, } } fn assert_is_sound(&self) { assert!( self.differing_pixels <= Self::MAX_DIFFERING_PIXELS, "{:?}", self ); assert!( self.mean_difference <= Self::MAX_MEAN_DIFFERENCE, "{:?}", self ); } } mod cubic { use super::*; #[cfg(any( feature = "dyn_cpu_backend", feature = "opencl_backend", feature = "vulkan_backend" ))] fn create_function() -> julia_set::Function { "z * z * z - 0.39".parse().unwrap() } const SNAPSHOT_FILENAME: &str = "cubic.png"; fn render_params() -> Params { Params::new(IMAGE_SIZE, 2.5).with_infinity_distance(2.5) } #[test] #[cfg(feature = "dyn_cpu_backend")] fn cpu_backend() { let image = generate_image::<_, julia_set::Cpu>(&create_function(), &render_params()); compare_to_reference(SNAPSHOT_FILENAME, &image); } #[test] #[cfg(feature = "cpu_backend")] fn cpu_backend_with_native_function() { let image = generate_image::<_, julia_set::Cpu>(|z| z * z * z - 0.39, &render_params()); compare_to_reference(SNAPSHOT_FILENAME, &image); } #[test] #[cfg(feature = "vulkan_backend")] fn vulkan_backend() { let image = generate_image::<_, julia_set::Vulkan>(&create_function(), &render_params()); compare_to_reference(SNAPSHOT_FILENAME, &image); } #[test] #[cfg(feature = "opencl_backend")] fn opencl_backend() { let image = generate_image::<_, julia_set::OpenCl>(&create_function(), &render_params()); compare_to_reference(SNAPSHOT_FILENAME, &image); } } mod exp { use super::*; use num_complex::Complex32; #[cfg(any( feature = "dyn_cpu_backend", feature = "opencl_backend", feature = "vulkan_backend" ))] fn create_function() -> julia_set::Function { "exp(z ^ -4) + 0.15i".parse().unwrap() } const SNAPSHOT_FILENAME: &str = "exp.png"; fn render_params() -> Params { Params::new(IMAGE_SIZE, 4.0).with_infinity_distance(9.0) } #[test] #[cfg(feature = "dyn_cpu_backend")] fn cpu_backend() { let image = generate_image::<_, julia_set::Cpu>(&create_function(), &render_params()); compare_to_reference(SNAPSHOT_FILENAME, &image); } #[test] #[cfg(feature = "cpu_backend")] fn cpu_backend_with_native_function() { let image = generate_image::<_, julia_set::Cpu>( |z: Complex32| z.powi(-4).exp() + Complex32::new(0.0, 0.15), &render_params(), ); compare_to_reference(SNAPSHOT_FILENAME, &image); } #[test] #[cfg(feature = "vulkan_backend")] fn vulkan_backend() { let image = generate_image::<_, julia_set::Vulkan>(&create_function(), &render_params()); compare_to_reference(SNAPSHOT_FILENAME, &image); } #[test] #[cfg(feature = "opencl_backend")] fn opencl_backend() { let image = generate_image::<_, julia_set::OpenCl>(&create_function(), &render_params()); compare_to_reference(SNAPSHOT_FILENAME, &image); } } mod flower { use super::*; use num_complex::Complex32; #[cfg(any( feature = "dyn_cpu_backend", feature = "opencl_backend", feature = "vulkan_backend" ))] fn create_function() -> julia_set::Function { "0.8*z + z/atanh(z^-4)".parse().unwrap() } const SNAPSHOT_FILENAME: &str = "flower.png"; fn render_params() -> Params { Params::new(IMAGE_SIZE, 2.0).with_infinity_distance(10.0) } #[test] #[cfg(feature = "dyn_cpu_backend")] fn cpu_backend() { let image = generate_image::<_, julia_set::Cpu>(&create_function(), &render_params()); compare_to_reference(SNAPSHOT_FILENAME, &image); } #[test] #[cfg(feature = "cpu_backend")] fn cpu_backend_with_native_function() { let image = generate_image::<_, julia_set::Cpu>( |z: Complex32| z * 0.8 + z / z.powi(-4).atanh(), &render_params(), ); compare_to_reference(SNAPSHOT_FILENAME, &image); } #[test] #[cfg(feature = "vulkan_backend")] fn vulkan_backend() { let image = generate_image::<_, julia_set::Vulkan>(&create_function(), &render_params()); compare_to_reference(SNAPSHOT_FILENAME, &image); } #[test] #[cfg(feature = "opencl_backend")] fn opencl_backend() { let image = generate_image::<_, julia_set::OpenCl>(&create_function(), &render_params()); compare_to_reference(SNAPSHOT_FILENAME, &image); } } mod hills { use super::*; use num_complex::Complex32; #[cfg(any( feature = "dyn_cpu_backend", feature = "opencl_backend", feature = "vulkan_backend" ))] fn create_function() -> julia_set::Function { "1i * acosh(cosh(1i * z) - arg(z)^-2) - 0.05 + 0.05i" .parse() .unwrap() } const SNAPSHOT_FILENAME: &str = "hills.png"; fn render_params() -> Params { Params::new(IMAGE_SIZE, 8.0) .with_view_center([-9.41, 0.0]) .with_infinity_distance(5.0) } #[test] #[cfg(feature = "dyn_cpu_backend")] fn cpu_backend() { let image = generate_image::<_, julia_set::Cpu>(&create_function(), &render_params()); compare_to_reference(SNAPSHOT_FILENAME, &image); } #[test] #[cfg(feature = "cpu_backend")] fn cpu_backend_with_native_function() { let image = generate_image::<_, julia_set::Cpu>( |z: Complex32| { Complex32::i() * ((Complex32::i() * z).cosh() - z.arg().powi(-2)).acosh() + Complex32::new(-0.05, 0.05) }, &render_params(), ); compare_to_reference(SNAPSHOT_FILENAME, &image); } #[test] #[cfg(feature = "vulkan_backend")] fn vulkan_backend() { let image = generate_image::<_, julia_set::Vulkan>(&create_function(), &render_params()); compare_to_reference(SNAPSHOT_FILENAME, &image); } #[test] #[cfg(feature = "opencl_backend")] fn opencl_backend() { let image = generate_image::<_, julia_set::OpenCl>(&create_function(), &render_params()); compare_to_reference(SNAPSHOT_FILENAME, &image); } } mod spiral { use super::*; use num_complex::Complex32; #[cfg(any( feature = "dyn_cpu_backend", feature = "opencl_backend", feature = "vulkan_backend" ))] fn create_function() -> julia_set::Function { "z + tanh(sqrt(z)) - 0.18 + 0.5i".parse().unwrap() } const SNAPSHOT_FILENAME: &str = "spiral.png"; fn render_params() -> Params { Params::new(IMAGE_SIZE, 2.3) .with_view_center([-6.84, 1.15]) .with_infinity_distance(9.0) } #[test] #[cfg(feature = "dyn_cpu_backend")] fn cpu_backend() { let image = generate_image::<_, julia_set::Cpu>(&create_function(), &render_params()); compare_to_reference(SNAPSHOT_FILENAME, &image); } #[test] #[cfg(feature = "cpu_backend")] fn cpu_backend_with_native_function() { let image = generate_image::<_, julia_set::Cpu>( |z: Complex32| z + z.sqrt().tanh() + Complex32::new(-0.18, 0.5), &render_params(), ); compare_to_reference(SNAPSHOT_FILENAME, &image); } #[test] #[cfg(feature = "vulkan_backend")] fn vulkan_backend() { let image = generate_image::<_, julia_set::Vulkan>(&create_function(), &render_params()); compare_to_reference(SNAPSHOT_FILENAME, &image); } #[test] #[cfg(feature = "opencl_backend")] fn opencl_backend() { let image = generate_image::<_, julia_set::OpenCl>(&create_function(), &render_params()); compare_to_reference(SNAPSHOT_FILENAME, &image); } }