diff --git a/Cargo.lock b/Cargo.lock index 48923e5..89ab8ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -354,7 +354,7 @@ dependencies = [ "glam", "image", "libm", - "rand 0.10.1", + "rand 0.8.6", "rand_xoshiro", "spirv-std", "tempfile", @@ -1038,6 +1038,15 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.4" @@ -1048,15 +1057,6 @@ dependencies = [ "rand_core 0.9.5", ] -[[package]] -name = "rand" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" -dependencies = [ - "rand_core 0.10.1", -] - [[package]] name = "rand_chacha" version = "0.9.0" @@ -1067,6 +1067,12 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + [[package]] name = "rand_core" version = "0.9.5" @@ -1076,19 +1082,13 @@ dependencies = [ "getrandom 0.3.4", ] -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - [[package]] name = "rand_xoshiro" -version = "0.8.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662effc7698e08ea324d3acccf8d9d7f7bf79b9785e270a174ea36e56900c91d" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" dependencies = [ - "rand_core 0.10.1", + "rand_core 0.6.4", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 54064db..66edd1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,8 @@ bytemuck = { version = "1.25.0", features = ["derive"] } glam = { version = "0.33.1", default-features = false, features = ["bytemuck", "scalar-math"] } image = { version = "0.25.10", default-features = false, features = ["default-formats"]} libm = "0.2.16" -rand = { version = "0.10.1", default-features = false } rspirv = "0.13.0" -rand_xoshiro = "0.8.1" tempfile = "3.27.0" + +rand = { version = "0.8.6", default-features = false } +rand_xoshiro = "0.6.0" diff --git a/enkou-shaders-tests/build.rs b/enkou-shaders-tests/build.rs index 09bd720..117b150 100644 --- a/enkou-shaders-tests/build.rs +++ b/enkou-shaders-tests/build.rs @@ -1,5 +1,5 @@ use cargo_gpu_install::install::Install; -use cargo_gpu_install::spirv_builder::{ShaderPanicStrategy, SpirvMetadata}; +use cargo_gpu_install::spirv_builder::{Capability, ShaderPanicStrategy, SpirvMetadata}; use std::path::PathBuf; pub fn main() -> anyhow::Result<()> { @@ -16,6 +16,7 @@ pub fn main() -> anyhow::Result<()> { builder.build_script.defaults = true; builder.shader_panic_strategy = ShaderPanicStrategy::SilentExit; builder.spirv_metadata = SpirvMetadata::Full; + builder.capabilities = vec![Capability::Int8, Capability::Int16, Capability::Int64]; let compile_result = builder.build()?; let spv_path = compile_result.module.unwrap_single(); diff --git a/enkou-shaders-tests/src/lib.rs b/enkou-shaders-tests/src/lib.rs index aa6eaaa..5827196 100644 --- a/enkou-shaders-tests/src/lib.rs +++ b/enkou-shaders-tests/src/lib.rs @@ -56,12 +56,15 @@ mod test { } #[test] - pub fn has_entry_main_fs() { - assert!(has_entry_point(ExecutionModel::Fragment, "main_fs")) + pub fn has_entry_main_chaos_game() { + assert!(has_entry_point( + ExecutionModel::GLCompute, + "main_chaos_game" + )) } #[test] - pub fn has_entry_main_vs() { - assert!(has_entry_point(ExecutionModel::Vertex, "main_vs")) + pub fn has_entry_main_camera() { + assert!(has_entry_point(ExecutionModel::GLCompute, "main_camera")) } } diff --git a/enkou-shaders/Cargo.toml b/enkou-shaders/Cargo.toml index 6bd320c..23d34ef 100644 --- a/enkou-shaders/Cargo.toml +++ b/enkou-shaders/Cargo.toml @@ -14,10 +14,10 @@ bytemuck.workspace = true glam.workspace = true libm.workspace = true rand.workspace = true +rand_xoshiro.workspace = true spirv-std.workspace = true [dev-dependencies] anyhow.workspace = true image.workspace = true -rand_xoshiro.workspace = true tempfile.workspace = true diff --git a/enkou-shaders/examples/gasket.rs b/enkou-shaders/examples/gasket.rs index 51dfb6e..29632dd 100644 --- a/enkou-shaders/examples/gasket.rs +++ b/enkou-shaders/examples/gasket.rs @@ -1,36 +1,54 @@ use anyhow::{Context, Result}; use enkou_shaders::Coefficients2; use enkou_shaders::camera::Camera; -use enkou_shaders::chaos_game::ChaosGame; +use enkou_shaders::camera::entry::main_camera; +use enkou_shaders::chaos_game::entry::main_chaos_game; use enkou_shaders::transform::Transform; -use glam::{Affine2, UVec2, Vec2, uvec2}; +use enkou_shaders::variation::Variation; +use glam::{Affine2, IVec2, UVec2, Vec2, uvec2}; use image::{GrayImage, Luma}; -use rand::SeedableRng; -use rand_xoshiro::Xoshiro256StarStar; use std::mem; use std::process::Command; use tempfile::NamedTempFile; -const ITERATIONS: u32 = 50_000; const ITERATIONS_DISCARD: u32 = 20; +const ITERATIONS: u32 = 50_000; const IMAGE_DIMENSION: UVec2 = uvec2(600, 600); pub fn main() -> Result<()> { - let seed: u64 = 4; // chosen by fair dice roll - let mut rng = Xoshiro256StarStar::seed_from_u64(seed); - let transforms = [ // F_0: (x / 2, y / 2) - Transform::new(Affine2::from_coefficients(0.5, 0.0, 0.0, 0.0, 0.5, 0.0)), + Transform::new( + Affine2::from_coefficients(0.5, 0.0, 0.0, 0.0, 0.5, 0.0), + uvec2(0, 1), + ), // F_1: ((x + 1) / 2, y / 2) - Transform::new(Affine2::from_coefficients(0.5, 0.0, 0.5, 0.0, 0.5, 0.0)), + Transform::new( + Affine2::from_coefficients(0.5, 0.0, 0.5, 0.0, 0.5, 0.0), + uvec2(0, 1), + ), // F_2: (x / 2, (y + 1) / 2) - Transform::new(Affine2::from_coefficients(0.5, 0.0, 0.0, 0.0, 0.5, 0.5)), + Transform::new( + Affine2::from_coefficients(0.5, 0.0, 0.0, 0.0, 0.5, 0.5), + uvec2(0, 1), + ), ]; let weights = [1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0]; - let mut image = GrayImage::new(IMAGE_DIMENSION.x, IMAGE_DIMENSION.y); + let variations = [Variation::IDENTITY]; + + let mut output_points_ifs = Vec::new(); + output_points_ifs.resize(ITERATIONS as usize, Vec2::ZERO); + + main_chaos_game( + ITERATIONS_DISCARD, + &[4u8], + &transforms, + &weights, + &variations, + &mut output_points_ifs, + ); // The gasket is defined on the range [0, 1] for both X and Y let camera = Camera::new( @@ -41,18 +59,20 @@ pub fn main() -> Result<()> { IMAGE_DIMENSION.as_vec2(), ); - let mut chaos_game = ChaosGame::new(&mut rng, &transforms, &weights); - for i in 0..ITERATIONS { - let next_point = chaos_game.next().unwrap(); + let mut output_points_pixel = Vec::new(); + output_points_pixel.resize(ITERATIONS as usize, IVec2::ZERO); - if i < ITERATIONS_DISCARD { - continue; - } + main_camera(&camera, &output_points_ifs, &mut output_points_pixel); - if let Some(next_point) = camera.transform_point_to_image(next_point) { - image.put_pixel(next_point.x, next_point.y, Luma([255u8])) - } - } + let mut image = GrayImage::new(IMAGE_DIMENSION.x, IMAGE_DIMENSION.y); + let dimensions = image.dimensions(); + output_points_pixel + .iter() + .skip_while(|p| { + p.x < 0 || (p.x as u32) > dimensions.0 || p.y < 0 || (p.y as u32) > dimensions.1 + }) + .map(|p| (p.x as u32, p.y as u32)) + .for_each(|(x, y)| image.put_pixel(x, y, Luma([255u8]))); let temp = NamedTempFile::with_suffix(".png").context("Unable to create file for image")?; image.save(temp.path()).context("Unable to save image")?; diff --git a/enkou-shaders/src/camera.rs b/enkou-shaders/src/camera.rs index 21c6867..383f9f4 100644 --- a/enkou-shaders/src/camera.rs +++ b/enkou-shaders/src/camera.rs @@ -90,6 +90,23 @@ impl Camera { } } +pub mod entry { + use crate::camera::Camera; + use spirv_std::glam::{IVec2, Vec2}; + use spirv_std::spirv; + + #[spirv(compute(entry_point_name = "main_camera", threads(1)))] + pub fn main_camera( + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] camera: &Camera, + #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] coordinates_ifs: &[Vec2], + #[spirv(storage_buffer, descriptor_set = 1, binding = 0)] coordinates_pixel: &mut [IVec2], + ) { + for i in 0..coordinates_ifs.len() { + coordinates_pixel[i] = camera.transform_point(coordinates_ifs[i]) + } + } +} + #[cfg(test)] mod test { use crate::camera::Camera; diff --git a/enkou-shaders/src/chaos_game.rs b/enkou-shaders/src/chaos_game.rs index 6d43b3a..39bd956 100644 --- a/enkou-shaders/src/chaos_game.rs +++ b/enkou-shaders/src/chaos_game.rs @@ -12,16 +12,17 @@ //! //! This algorithm is also known as the ["chaos game"](https://en.wikipedia.org/wiki/Chaos_game), //! and it forms the basic system for producing images. + use crate::transform::Transform; use crate::variation::Variation; -use glam::{Vec2, vec2}; -use rand::distr::{Distribution, StandardUniform}; -use rand::{Rng, RngExt}; +use rand::Rng; +use rand::distributions::{Distribution, Standard}; +use spirv_std::glam::{Vec2, vec2}; struct BiUnit; impl Distribution for BiUnit { fn sample(&self, rng: &mut R) -> f32 { - rng.sample::(StandardUniform) * 2.0 - 1.0 + rng.sample::(Standard) * 2.0 - 1.0 } } @@ -40,11 +41,11 @@ pub fn step_chaos_game( weights: &[f32], variations: &[Variation], ) -> (Vec2, u32) { - let mut choice_weight = rng.sample::(StandardUniform); + let mut choice_weight = rng.sample::(Standard); let mut transform_index: u32 = 0; - for weight in weights { - choice_weight -= weight; + for i in 0..weights.len() { + choice_weight -= weights[i]; if choice_weight <= 0.0 { break; } @@ -105,3 +106,40 @@ impl<'a, R: Rng> Iterator for ChaosGame<'a, R> { Some(next_point) } } + +pub mod entry { + use crate::chaos_game::ChaosGame; + use crate::transform::Transform; + use crate::variation::Variation; + use glam::Vec2; + use rand_xoshiro::Xoshiro256StarStar; + use spirv_std::spirv; + + fn xoshiro_from_state(rng_state: [u8; 32]) -> Xoshiro256StarStar { + let mut rng_state_actual = [1u64, 2u64, 3u64, 4u64]; + unsafe { core::mem::transmute(rng_state_actual) } + } + + #[spirv(compute(entry_point_name = "main_chaos_game", threads(1)))] + pub extern "C" fn main_chaos_game( + #[spirv(spec_constant(id = 1, default = 20))] iteration_discard: u32, + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] rng_seed: &[u8], + #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] transforms: &[Transform], + #[spirv(storage_buffer, descriptor_set = 0, binding = 2)] weights: &[f32], + #[spirv(storage_buffer, descriptor_set = 0, binding = 3)] variations: &[Variation], + #[spirv(storage_buffer, descriptor_set = 1, binding = 0)] output: &mut [Vec2], + ) { + let rng_seed_actual = [0u8; 32]; + + let mut rng = xoshiro_from_state(rng_seed_actual); + let mut chaos_game = ChaosGame::new(&mut rng, transforms, weights, variations); + + for _ in 0..iteration_discard { + chaos_game.next().unwrap(); + } + + for i in 0..output.len() { + output[i] = chaos_game.next().unwrap(); + } + } +} diff --git a/enkou-shaders/src/lib.rs b/enkou-shaders/src/lib.rs index d7f9c4d..d8b38cc 100644 --- a/enkou-shaders/src/lib.rs +++ b/enkou-shaders/src/lib.rs @@ -5,14 +5,9 @@ pub mod camera; pub mod chaos_game; pub mod transform; -mod variation; +pub mod variation; -use bytemuck::{Pod, Zeroable}; -use core::f32::consts::PI; -use glam::{Affine2, Vec3, Vec4, vec2, vec3}; -#[cfg(target_arch = "spirv")] -use spirv_std::num_traits::Float; -use spirv_std::spirv; +use glam::Affine2; /// Utility trait to convert between `flam3` notation and [`glam`]. #[allow(missing_docs)] @@ -39,6 +34,28 @@ pub trait Coefficients2 { /// ``` fn from_coefficients(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) -> Affine2; + /// Convert affine transformation coefficients to the [`glam`] representation. + /// Parameters use the following form: + /// + /// ```text + /// (a * x + b * y + c, d * x + e * y + f) + /// ``` + /// + /// ``` + /// # use glam::{Affine2, vec2}; + /// # use crate::enkou_shaders::Coefficients2; + /// let coefs = Affine2::from_coefficients_arr([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); + /// let (x, y) = (7.0, 8.0); + /// assert_eq!( + /// coefs.transform_point2(vec2(x, y)), + /// vec2( + /// coefs.a() * x + coefs.b() * y + coefs.c(), + /// coefs.d() * x + coefs.e() * y + coefs.f() + /// ) + /// ); + /// ``` + fn from_coefficients_arr(coefficients: [f32; 6]) -> Affine2; + fn a(&self) -> f32; fn b(&self) -> f32; fn c(&self) -> f32; @@ -48,10 +65,23 @@ pub trait Coefficients2 { } impl Coefficients2 for Affine2 { + #[inline] fn from_coefficients(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) -> Affine2 { Affine2::from_cols_array(&[a, d, b, e, c, f]) } + #[inline] + fn from_coefficients_arr(coefficients: [f32; 6]) -> Affine2 { + Affine2::from_coefficients( + coefficients[0], + coefficients[1], + coefficients[2], + coefficients[3], + coefficients[4], + coefficients[5], + ) + } + fn a(&self) -> f32 { self.matrix2.x_axis.x } @@ -76,34 +106,3 @@ impl Coefficients2 for Affine2 { self.translation.y } } - -#[derive(Copy, Clone, Pod, Zeroable)] -#[repr(C)] -#[allow(missing_docs)] -pub struct ShaderConstants { - pub width: u32, - pub height: u32, - pub time: f32, -} - -#[spirv(fragment)] -#[allow(missing_docs)] -pub fn main_fs(vtx_color: Vec3, output: &mut Vec4) { - *output = Vec4::from((vtx_color, 1.)); -} - -#[spirv(vertex)] -#[allow(missing_docs)] -pub fn main_vs( - #[spirv(vertex_index)] vert_id: i32, - #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] constants: &ShaderConstants, - #[spirv(position)] vtx_pos: &mut Vec4, - vtx_color: &mut Vec3, -) { - let speed = 0.4; - let time = constants.time * speed + vert_id as f32 * (2. * PI * 120. / 360.); - let position = vec2(f32::sin(time), f32::cos(time)); - *vtx_pos = Vec4::from((position, 0.0, 1.0)); - - *vtx_color = [vec3(1., 0., 0.), vec3(0., 1., 0.), vec3(0., 0., 1.)][vert_id as usize % 3]; -} diff --git a/enkou-shaders/src/transform.rs b/enkou-shaders/src/transform.rs index aeece9e..3b8badc 100644 --- a/enkou-shaders/src/transform.rs +++ b/enkou-shaders/src/transform.rs @@ -5,7 +5,7 @@ //! but produce more interesting images once we add variations. use crate::variation::Variation; use bytemuck::{Pod, Zeroable}; -use glam::{Affine2, Vec2}; +use glam::{Affine2, UVec2, Vec2}; use rand::Rng; /// Affine transform for use in the [`chaos_game`](crate::chaos_game). @@ -13,12 +13,12 @@ use rand::Rng; #[repr(C)] pub struct Transform { coefficients: Affine2, - variation_range: [u16; 2], + variation_range: UVec2, } impl Transform { /// Create a new transform from an affine transformation matrix - pub fn new(coefficients: Affine2, variation_range: [u16; 2]) -> Self { + pub fn new(coefficients: Affine2, variation_range: UVec2) -> Self { Transform { coefficients, variation_range, @@ -35,8 +35,11 @@ impl Transform { let point = self.coefficients.transform_point2(point); let mut point_output = Vec2::ZERO; - let variation_range = self.variation_range[0] as usize..self.variation_range[1] as usize; - for variation in variations[variation_range].iter() { + + let variation_start = self.variation_range.x; + let variation_end = self.variation_range.y; + for variation_index in variation_start..variation_end { + let ref variation = variations[variation_index as usize]; point_output += variation.transform_point(point, rng, &self.coefficients) } diff --git a/enkou-shaders/src/variation.rs b/enkou-shaders/src/variation.rs index 1db7858..d969846 100644 --- a/enkou-shaders/src/variation.rs +++ b/enkou-shaders/src/variation.rs @@ -1,12 +1,11 @@ //! # Variation - use crate::Coefficients2; use bytemuck::{Pod, Zeroable}; use core::f32::consts::PI; use glam::{Affine2, Vec2, vec2}; use libm::{atan2f, cosf, powf, sinf, sqrtf, tanf}; -use rand::distr::Bernoulli; -use rand::{Rng, RngExt}; +use rand::Rng; +use rand::distributions::Standard; #[derive(Copy, Clone, Pod, Zeroable)] #[repr(C)] @@ -35,6 +34,20 @@ pub struct Variation { } impl Variation { + pub const IDENTITY: Variation = Variation { + kind: VariationKind::Linear, + weight: 1.0, + params: VariationParams([0f32; 4]), + }; + + pub fn new(kind: VariationKind, weight: f32, params: VariationParams) -> Variation { + Variation { + kind, + weight, + params, + } + } + pub fn transform_point( &self, point: Vec2, @@ -60,8 +73,11 @@ fn transform_point_julia(point: Vec2, rng: &mut R) -> Vec2 { let r = sqrtf(x2 + y2); let theta = atan2f(point.x, point.y); - let omega_choice = rng.sample(Bernoulli::new(0.5).unwrap()); - let omega = if omega_choice { PI } else { 0.0 }; + let omega = if rng.sample::(Standard) > 0.5 { + PI + } else { + 0.0 + }; let sqrt_r = sqrtf(r); let theta_val = theta / 2.0 + omega;