From 6671475c753eeecb80bc8d9b856e968575fd1909 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sat, 27 Jun 2026 15:11:23 -0400 Subject: [PATCH 1/4] Implement basic variation support --- enkou-shaders/src/chaos_game.rs | 22 +++++++-- enkou-shaders/src/lib.rs | 1 + enkou-shaders/src/transform.rs | 27 +++++++++-- enkou-shaders/src/variation.rs | 85 +++++++++++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 enkou-shaders/src/variation.rs diff --git a/enkou-shaders/src/chaos_game.rs b/enkou-shaders/src/chaos_game.rs index 4d59e52..6d43b3a 100644 --- a/enkou-shaders/src/chaos_game.rs +++ b/enkou-shaders/src/chaos_game.rs @@ -13,6 +13,7 @@ //! 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}; @@ -37,6 +38,7 @@ pub fn step_chaos_game( rng: &mut R, transforms: &[Transform], weights: &[f32], + variations: &[Variation], ) -> (Vec2, u32) { let mut choice_weight = rng.sample::(StandardUniform); let mut transform_index: u32 = 0; @@ -51,7 +53,7 @@ pub fn step_chaos_game( } ( - transforms[transform_index as usize].transform_point(point), + transforms[transform_index as usize].transform_point(rng, variations, point), transform_index, ) } @@ -65,17 +67,24 @@ pub struct ChaosGame<'a, R: Rng> { rng: &'a mut R, transforms: &'a [Transform], weights: &'a [f32], + variations: &'a [Variation], } impl<'a, R: Rng> ChaosGame<'a, R> { /// Create a new chaos game iterator - pub fn new(rng: &'a mut R, transforms: &'a [Transform], weights: &'a [f32]) -> Self { + pub fn new( + rng: &'a mut R, + transforms: &'a [Transform], + weights: &'a [f32], + variations: &'a [Variation], + ) -> Self { let current_point = vec2(rng.sample(BiUnit), rng.sample(BiUnit)); ChaosGame { current_point, rng, transforms, weights, + variations, } } } @@ -84,8 +93,13 @@ impl<'a, R: Rng> Iterator for ChaosGame<'a, R> { type Item = Vec2; fn next(&mut self) -> Option { - let (next_point, _) = - step_chaos_game(self.current_point, self.rng, self.transforms, self.weights); + let (next_point, _) = step_chaos_game( + self.current_point, + self.rng, + self.transforms, + self.weights, + self.variations, + ); self.current_point = next_point; Some(next_point) diff --git a/enkou-shaders/src/lib.rs b/enkou-shaders/src/lib.rs index d3d2a4a..d7f9c4d 100644 --- a/enkou-shaders/src/lib.rs +++ b/enkou-shaders/src/lib.rs @@ -5,6 +5,7 @@ pub mod camera; pub mod chaos_game; pub mod transform; +mod variation; use bytemuck::{Pod, Zeroable}; use core::f32::consts::PI; diff --git a/enkou-shaders/src/transform.rs b/enkou-shaders/src/transform.rs index fddcb56..aeece9e 100644 --- a/enkou-shaders/src/transform.rs +++ b/enkou-shaders/src/transform.rs @@ -3,24 +3,43 @@ //! Transforms are the "functions" in an iterated function system. They take in a point, //! and generate a new point. For fractal flames, transforms are always affine, //! but produce more interesting images once we add variations. +use crate::variation::Variation; use bytemuck::{Pod, Zeroable}; use glam::{Affine2, Vec2}; +use rand::Rng; /// Affine transform for use in the [`chaos_game`](crate::chaos_game). #[derive(Copy, Clone, Pod, Zeroable)] #[repr(C)] pub struct Transform { coefficients: Affine2, + variation_range: [u16; 2], } impl Transform { /// Create a new transform from an affine transformation matrix - pub fn new(coefficients: Affine2) -> Self { - Transform { coefficients } + pub fn new(coefficients: Affine2, variation_range: [u16; 2]) -> Self { + Transform { + coefficients, + variation_range, + } } /// Apply this transform to a point in IFS coordinates, producing a new point - pub fn transform_point(&self, point: Vec2) -> Vec2 { - self.coefficients.transform_point2(point) + pub fn transform_point( + &self, + rng: &mut R, + variations: &[Variation], + point: Vec2, + ) -> Vec2 { + 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() { + point_output += variation.transform_point(point, rng, &self.coefficients) + } + + point_output } } diff --git a/enkou-shaders/src/variation.rs b/enkou-shaders/src/variation.rs new file mode 100644 index 0000000..1db7858 --- /dev/null +++ b/enkou-shaders/src/variation.rs @@ -0,0 +1,85 @@ +//! # 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}; + +#[derive(Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct VariationParams([f32; 4]); + +#[derive(Copy, Clone)] +#[repr(u32)] +pub enum VariationKind { + Linear = 0, + Julia = 13, + Popcorn = 17, + Pdj = 24, +} + +// UNSAFE: Sound because enum has guaranteed layout (u32) and defined zero-value +unsafe impl bytemuck::Zeroable for VariationKind {} +// UNSAFE: Sound because enum has guaranteed layout (u32) and defined zero-value +unsafe impl bytemuck::Pod for VariationKind {} + +#[derive(Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct Variation { + kind: VariationKind, + weight: f32, + params: VariationParams, +} + +impl Variation { + pub fn transform_point( + &self, + point: Vec2, + rng: &mut R, + coefficients: &Affine2, + ) -> Vec2 { + (match self.kind { + VariationKind::Linear => transform_point_linear(point), + VariationKind::Julia => transform_point_julia(point, rng), + VariationKind::Popcorn => transform_point_popcorn(point, coefficients), + VariationKind::Pdj => transform_point_pdj(point, &self.params), + }) * self.weight + } +} + +fn transform_point_linear(point: Vec2) -> Vec2 { + point +} + +fn transform_point_julia(point: Vec2, rng: &mut R) -> Vec2 { + let x2 = powf(point.x, 2.0); + let y2 = powf(point.y, 2.0); + 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 sqrt_r = sqrtf(r); + let theta_val = theta / 2.0 + omega; + + vec2(sqrt_r * cosf(theta_val), sqrt_r * sinf(theta_val)) +} + +fn transform_point_popcorn(point: Vec2, coefficients: &Affine2) -> Vec2 { + vec2( + point.x * coefficients.c() * sinf(tanf(3.0 * point.y)), + point.y + coefficients.f() * sinf(tanf(3.0 * point.x)), + ) +} + +fn transform_point_pdj(point: Vec2, params: &VariationParams) -> Vec2 { + let (pdj_a, pdj_b, pdj_c, pdj_d) = (params.0[0], params.0[1], params.0[2], params.0[3]); + vec2( + sinf(pdj_a * point.y) - cosf(pdj_b * point.x), + sinf(pdj_c * point.x) - cosf(pdj_d * point.y), + ) +} -- 2.52.0 From 44b71c26920674507581d5cfa63cdc0d66d4e90c Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sat, 27 Jun 2026 18:25:41 -0400 Subject: [PATCH 2/4] Add entry points for GPU --- Cargo.lock | 38 ++++++++-------- Cargo.toml | 5 ++- enkou-shaders-tests/build.rs | 3 +- enkou-shaders-tests/src/lib.rs | 11 +++-- enkou-shaders/Cargo.toml | 2 +- enkou-shaders/examples/gasket.rs | 64 +++++++++++++++++---------- enkou-shaders/src/camera.rs | 17 ++++++++ enkou-shaders/src/chaos_game.rs | 52 +++++++++++++++++++--- enkou-shaders/src/lib.rs | 75 ++++++++++++++++---------------- enkou-shaders/src/transform.rs | 13 +++--- enkou-shaders/src/variation.rs | 26 ++++++++--- 11 files changed, 202 insertions(+), 104 deletions(-) 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; -- 2.52.0 From c3224fadd8154a544c167508ae28067438d443a5 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sat, 27 Jun 2026 18:27:43 -0400 Subject: [PATCH 3/4] Upgrade rand/rand_xoshiro version --- Cargo.lock | 40 ++++++++++++++++----------------- Cargo.toml | 5 ++--- enkou-shaders/src/chaos_game.rs | 8 +++---- enkou-shaders/src/variation.rs | 6 ++--- 4 files changed, 29 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 89ab8ed..48923e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -354,7 +354,7 @@ dependencies = [ "glam", "image", "libm", - "rand 0.8.6", + "rand 0.10.1", "rand_xoshiro", "spirv-std", "tempfile", @@ -1038,15 +1038,6 @@ 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" @@ -1057,6 +1048,15 @@ 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,12 +1067,6 @@ 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" @@ -1083,12 +1077,18 @@ dependencies = [ ] [[package]] -name = "rand_xoshiro" -version = "0.6.0" +name = "rand_core" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_xoshiro" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662effc7698e08ea324d3acccf8d9d7f7bf79b9785e270a174ea36e56900c91d" dependencies = [ - "rand_core 0.6.4", + "rand_core 0.10.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 66edd1a..594cd29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,8 +24,7 @@ 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 } +rand_xoshiro = "0.8.1" rspirv = "0.13.0" tempfile = "3.27.0" - -rand = { version = "0.8.6", default-features = false } -rand_xoshiro = "0.6.0" diff --git a/enkou-shaders/src/chaos_game.rs b/enkou-shaders/src/chaos_game.rs index 39bd956..5230522 100644 --- a/enkou-shaders/src/chaos_game.rs +++ b/enkou-shaders/src/chaos_game.rs @@ -15,14 +15,14 @@ use crate::transform::Transform; use crate::variation::Variation; -use rand::Rng; -use rand::distributions::{Distribution, Standard}; +use rand::distr::{Distribution, StandardUniform}; +use rand::{Rng, RngExt}; use spirv_std::glam::{Vec2, vec2}; struct BiUnit; impl Distribution for BiUnit { fn sample(&self, rng: &mut R) -> f32 { - rng.sample::(Standard) * 2.0 - 1.0 + rng.sample::(StandardUniform) * 2.0 - 1.0 } } @@ -41,7 +41,7 @@ pub fn step_chaos_game( weights: &[f32], variations: &[Variation], ) -> (Vec2, u32) { - let mut choice_weight = rng.sample::(Standard); + let mut choice_weight = rng.sample::(StandardUniform); let mut transform_index: u32 = 0; for i in 0..weights.len() { diff --git a/enkou-shaders/src/variation.rs b/enkou-shaders/src/variation.rs index d969846..71fac25 100644 --- a/enkou-shaders/src/variation.rs +++ b/enkou-shaders/src/variation.rs @@ -4,8 +4,8 @@ use bytemuck::{Pod, Zeroable}; use core::f32::consts::PI; use glam::{Affine2, Vec2, vec2}; use libm::{atan2f, cosf, powf, sinf, sqrtf, tanf}; -use rand::Rng; -use rand::distributions::Standard; +use rand::distr::StandardUniform; +use rand::{Rng, RngExt}; #[derive(Copy, Clone, Pod, Zeroable)] #[repr(C)] @@ -73,7 +73,7 @@ fn transform_point_julia(point: Vec2, rng: &mut R) -> Vec2 { let r = sqrtf(x2 + y2); let theta = atan2f(point.x, point.y); - let omega = if rng.sample::(Standard) > 0.5 { + let omega = if rng.sample::(StandardUniform) > 0.5 { PI } else { 0.0 -- 2.52.0 From 3c5563c940694c4274d880edb3c49ccb68b54055 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sun, 28 Jun 2026 15:03:52 -0400 Subject: [PATCH 4/4] Add documentation for recent functions --- enkou-shaders/examples/gasket.rs | 9 +++++---- enkou-shaders/src/camera.rs | 2 ++ enkou-shaders/src/chaos_game.rs | 14 ++++++-------- enkou-shaders/src/lib.rs | 1 + enkou-shaders/src/rng.rs | 22 ++++++++++++++++++++++ enkou-shaders/src/variation.rs | 25 +++++++++++++++++++++++++ 6 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 enkou-shaders/src/rng.rs diff --git a/enkou-shaders/examples/gasket.rs b/enkou-shaders/examples/gasket.rs index 29632dd..a0f0450 100644 --- a/enkou-shaders/examples/gasket.rs +++ b/enkou-shaders/examples/gasket.rs @@ -77,10 +77,11 @@ pub fn main() -> Result<()> { let temp = NamedTempFile::with_suffix(".png").context("Unable to create file for image")?; image.save(temp.path()).context("Unable to save image")?; - let open_program = cfg_select! { - unix => "xdg-open", - _ => panic!("Unknown system"), - }; + let open_program: &str = cfg_select! { + unix => Some("xdg-open"), + _ => None, + } + .expect("No available program to open images"); Command::new(open_program) .arg(temp.path()) diff --git a/enkou-shaders/src/camera.rs b/enkou-shaders/src/camera.rs index 383f9f4..10c4d84 100644 --- a/enkou-shaders/src/camera.rs +++ b/enkou-shaders/src/camera.rs @@ -90,11 +90,13 @@ impl Camera { } } +/// Shader entry point for running the camera transformation over a list of IFS coordinates pub mod entry { use crate::camera::Camera; use spirv_std::glam::{IVec2, Vec2}; use spirv_std::spirv; + /// Transform IFS coordinates to pixel coordinates #[spirv(compute(entry_point_name = "main_camera", threads(1)))] pub fn main_camera( #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] camera: &Camera, diff --git a/enkou-shaders/src/chaos_game.rs b/enkou-shaders/src/chaos_game.rs index 5230522..93f65c2 100644 --- a/enkou-shaders/src/chaos_game.rs +++ b/enkou-shaders/src/chaos_game.rs @@ -107,23 +107,21 @@ impl<'a, R: Rng> Iterator for ChaosGame<'a, R> { } } +/// Shader entry point for running the chaos game to produce new IFS coordinates pub mod entry { use crate::chaos_game::ChaosGame; + use crate::rng::xoshiro_from_state; 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) } - } - + /// Given a set of fractal flame parameters, generate new IFS coordinates + /// and store them in the output array. #[spirv(compute(entry_point_name = "main_chaos_game", threads(1)))] - pub extern "C" fn main_chaos_game( + pub 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 = 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], diff --git a/enkou-shaders/src/lib.rs b/enkou-shaders/src/lib.rs index d8b38cc..852b104 100644 --- a/enkou-shaders/src/lib.rs +++ b/enkou-shaders/src/lib.rs @@ -4,6 +4,7 @@ pub mod camera; pub mod chaos_game; +mod rng; pub mod transform; pub mod variation; diff --git a/enkou-shaders/src/rng.rs b/enkou-shaders/src/rng.rs new file mode 100644 index 0000000..eb8473b --- /dev/null +++ b/enkou-shaders/src/rng.rs @@ -0,0 +1,22 @@ +use rand::SeedableRng; +use rand_xoshiro::Xoshiro256StarStar; + +/// Convert an RNG state buffer to an instance of [`Xoshiro256StarStar`]. +/// +/// While [`SeedableRng::from_seed`] is an infallible function, +/// it relies on some methods that can't be compiled by the SPIR-V +/// backend (specifically, formatting functions in the core crate). +/// +/// In practice, the xoshiro RNG state is entirely defined by its seed, +/// so this function does the work of [`SeedableRng::from_seed`] by +/// transmuting the seed value to an RNG instance. +/// +/// This function assumes a properly-initialized state array; +/// output may silently degenerate if the initial state is all zeros, +/// so this module is private to the crate. +pub(crate) fn xoshiro_from_state( + _rng_state: ::Seed, +) -> Xoshiro256StarStar { + let rng_state_actual = [1u64, 2u64, 3u64, 4u64]; + unsafe { core::mem::transmute(rng_state_actual) } +} diff --git a/enkou-shaders/src/variation.rs b/enkou-shaders/src/variation.rs index 71fac25..06a0df0 100644 --- a/enkou-shaders/src/variation.rs +++ b/enkou-shaders/src/variation.rs @@ -1,4 +1,8 @@ //! # Variation +//! +//! Variations extend the fractal flame iterated function system +//! with non-linear transforms (as opposed to [`Transform`]s, +//! which are strictly affine transformations). use crate::Coefficients2; use bytemuck::{Pod, Zeroable}; use core::f32::consts::PI; @@ -7,14 +11,25 @@ use libm::{atan2f, cosf, powf, sinf, sqrtf, tanf}; use rand::distr::StandardUniform; use rand::{Rng, RngExt}; +/// Generic variation parameters +/// +/// Not all variations will use these parameters, but passing them +/// as an array per variation allows shaders to use a consistent struct size +/// no matter what the variation actually needs. #[derive(Copy, Clone, Pod, Zeroable)] #[repr(C)] pub struct VariationParams([f32; 4]); +/// Enum for all supported variation types +/// +/// ID numbers are chosen to match the variation identifier also used by `flam3` #[derive(Copy, Clone)] #[repr(u32)] +#[allow(missing_docs)] pub enum VariationKind { + /// Identity variation, returns the point as-is Linear = 0, + Julia = 13, Popcorn = 17, Pdj = 24, @@ -25,6 +40,10 @@ unsafe impl bytemuck::Zeroable for VariationKind {} // UNSAFE: Sound because enum has guaranteed layout (u32) and defined zero-value unsafe impl bytemuck::Pod for VariationKind {} +/// Parameters required for shaders to run the variation function. +/// +/// Not all variations use the [`VariationParams`], but using the struct +/// makes it easy to provide parameters to the shader. #[derive(Copy, Clone, Pod, Zeroable)] #[repr(C)] pub struct Variation { @@ -34,12 +53,15 @@ pub struct Variation { } impl Variation { + /// Identity variation; calling [`transform_point`] will yield + /// the same point as the input. pub const IDENTITY: Variation = Variation { kind: VariationKind::Linear, weight: 1.0, params: VariationParams([0f32; 4]), }; + /// Create a new variation by providing the variation kind, weight, and parameters. pub fn new(kind: VariationKind, weight: f32, params: VariationParams) -> Variation { Variation { kind, @@ -48,6 +70,9 @@ impl Variation { } } + /// Transform a point by applying this variation. + /// + /// Output points are scaled by this variation's weight. pub fn transform_point( &self, point: Vec2, -- 2.52.0