From fc209a78ade886150bec56c14a39a2405a50c041 Mon Sep 17 00:00:00 2001 From: CrispyPin Date: Fri, 14 Jul 2023 19:25:46 +0200 Subject: [PATCH] multithread rendering with rayon --- Cargo.lock | 76 ++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/generate.rs | 113 +++++++++++++++++++++++++++++------------------- src/main.rs | 22 +++++----- 4 files changed, 156 insertions(+), 56 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 21f6d53..430bc5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -608,6 +608,40 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset 0.9.0", + "scopeguard", +] + [[package]] name = "crossbeam-utils" version = "0.8.16" @@ -1237,6 +1271,7 @@ dependencies = [ "image", "native-dialog", "rand", + "rayon", "serde", "serde_json", ] @@ -1343,6 +1378,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1505,6 +1549,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "num_enum" version = "0.5.11" @@ -1803,6 +1857,28 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "redox_syscall" version = "0.2.16" diff --git a/Cargo.toml b/Cargo.toml index ac55378..ad12788 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ eframe = "0.22.0" image = { version = "0.24.6", default-features = false, features = ["png"] } native-dialog = "0.6.4" rand = "0.8.5" +rayon = "1.7.0" serde = { version = "1.0.171", features = ["derive"] } serde_json = "1.0.102" diff --git a/src/generate.rs b/src/generate.rs index dc2e2b9..babe5da 100644 --- a/src/generate.rs +++ b/src/generate.rs @@ -1,13 +1,14 @@ use eframe::epaint::Vec2; use image::{Rgb, RgbImage}; +use rayon::prelude::{IntoParallelIterator, ParallelIterator}; use serde::{Deserialize, Serialize}; #[derive(Clone, Serialize, Deserialize)] pub struct RenderOptions { - pub width: u32, - pub height: u32, + pub width: usize, + pub height: usize, pub unit_width: f64, - pub iterations: u32, + pub max_iter: u16, pub cx: f64, pub cy: f64, pub fill_style: FillStyle, @@ -25,7 +26,7 @@ impl Default for RenderOptions { width: 512, height: 512, unit_width: 4.0, - iterations: 128, + max_iter: 128, cx: 0.4, cy: -0.2, fill_style: FillStyle::Bright, @@ -33,57 +34,79 @@ impl Default for RenderOptions { } } -pub fn view_point(q: &RenderOptions, image: RgbImage) -> RgbImage { - apply_fn(image, q, |x, y| { - let len = (Vec2::new(x as f32, y as f32) - Vec2::new(q.cx as f32, q.cy as f32)).length(); - if len < 0.03 { - Some(Rgb([0, 120, 120])) - } else if len < 0.04 { - Some(Rgb([255; 3])) - } else { - None - } - }) -} +pub fn render_c(q: &RenderOptions, mut image: RgbImage) -> RgbImage { + let width = q.width as f32; + let height = q.height as f32; + let ppu = width / (q.unit_width as f32); -pub fn render(q: &RenderOptions, color: (u8, u8, u8)) -> RgbImage { - let img = RgbImage::new(q.width, q.height); - apply_fn(img, q, |x, y| { - let i = julia(x, y, q.cx, q.cy, q.iterations); - if q.fill_style == FillStyle::Black && i == q.iterations { - None - } else { - let i = i.min(255) as u8; - Some(Rgb([ - i.saturating_mul(color.0), - i.saturating_mul(color.1), - i.saturating_mul(color.2), - ])) - } - }) -} - -fn apply_fn(mut image: RgbImage, q: &RenderOptions, op: F) -> RgbImage -where - F: Fn(f64, f64) -> Option>, -{ - let width = q.width as f64; - let height = q.height as f64; - let ppu = width / q.unit_width; + let target = Vec2::new(q.cx as f32, q.cy as f32); for y in 0..q.height { for x in 0..q.width { - let sx = (x as f64 - width / 2.0) / ppu; - let sy = (y as f64 - height / 2.0) / ppu; - if let Some(pixel) = op(sx, sy) { - image.put_pixel(x, y, pixel); + let sx = (x as f32 - width / 2.0) / ppu; + let sy = (y as f32 - height / 2.0) / ppu; + + let len = (Vec2::new(sx, sy) - target).length(); + if len < 0.03 { + image.put_pixel(x as u32, y as u32, Rgb([0, 120, 120])); + } else if len < 0.04 { + image.put_pixel(x as u32, y as u32, Rgb([255; 3])); } } } image } -fn julia(mut x: f64, mut y: f64, cx: f64, cy: f64, max_iter: u32) -> u32 { +pub fn color_iteration(iter: u16, color: (u8, u8, u8)) -> Rgb { + let i = iter.min(255) as u8; + Rgb([ + i.saturating_mul(color.0), + i.saturating_mul(color.1), + i.saturating_mul(color.2), + ]) +} + +pub fn render_julia(q: &RenderOptions, color: (u8, u8, u8)) -> RgbImage { + let mut image = RgbImage::new(q.width as u32, q.height as u32); + + let width = q.width as f64; + let height = q.height as f64; + let ppu = width / q.unit_width; + + let fill = match q.fill_style { + FillStyle::Black => Rgb([0; 3]), + FillStyle::Bright => color_iteration(q.max_iter, color), + }; + + (0..q.height) + .into_par_iter() + .map(|y| { + let mut row = Vec::with_capacity(q.width); + for x in 0..q.width { + let sx = (x as f64 - width / 2.0) / ppu; + let sy = (y as f64 - height / 2.0) / ppu; + let i = julia(sx, sy, q.cx, q.cy, q.max_iter); + + if i == q.max_iter { + row.push(fill); + } else { + row.push(color_iteration(i, color)); + } + } + row + }) + .collect::>() + .into_iter() + .enumerate() + .for_each(|(y, row)| { + for (x, i) in row.into_iter().enumerate() { + image.put_pixel(x as u32, y as u32, i); + } + }); + image +} + +fn julia(mut x: f64, mut y: f64, cx: f64, cy: f64, max_iter: u16) -> u16 { let mut iter = 0; while (x * x + y * y) < 4.0 && iter < max_iter { (x, y) = ( diff --git a/src/main.rs b/src/main.rs index 82d8118..2ad86de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ use eframe::{ epaint::{TextureHandle, Vec2}, Frame, NativeOptions, }; -use generate::{render, view_point, FillStyle, RenderOptions}; +use generate::{render_c, render_julia, FillStyle, RenderOptions}; use image::EncodableLayout; use native_dialog::FileDialog; use serde::{Deserialize, Serialize}; @@ -47,7 +47,7 @@ struct JuliaGUI { #[serde(skip)] export_render_ms: Option, export_res_power: u8, - export_iterations: u32, + export_max_iter: u16, #[serde(skip)] export_path: PathBuf, #[serde(skip)] @@ -77,7 +77,7 @@ impl Default for JuliaGUI { preview_render_ms: 0.0, export_render_ms: None, export_res_power: 3, - export_iterations: 512, + export_max_iter: 512, export_path: "".into(), settings_changed: true, preview_point: false, @@ -113,7 +113,7 @@ impl JuliaGUI { RenderJob::Exit => break, RenderJob::Render(path, options, color) => { let start_time = SystemTime::now(); - let image = render(&options, color); + let image = render_julia(&options, color); if let Err(err) = image.save(&path) { println!("Failed to save render: {err}"); } @@ -142,9 +142,9 @@ impl JuliaGUI { fn update_preview(&mut self) { let start_time = SystemTime::now(); - let mut frame = render(&self.settings, self.color); + let mut frame = render_julia(&self.settings, self.color); if self.preview_point { - frame = view_point(&self.settings, frame); + frame = render_c(&self.settings, frame); } if let Some(preview) = &mut self.preview { @@ -166,7 +166,7 @@ impl JuliaGUI { let settings = RenderOptions { width: self.settings.width * res_mul, height: self.settings.height * res_mul, - iterations: self.export_iterations, + max_iter: self.export_max_iter, ..self.settings.clone() }; @@ -272,8 +272,8 @@ impl eframe::App for JuliaGUI { let set_blue = ui.add(Slider::new(&mut self.color.2, 0..=16)); ui.label("Preview iterations:"); - let set_iter = ui - .add(Slider::new(&mut self.settings.iterations, 5..=256).clamp_to_range(false)); + let set_iter = + ui.add(Slider::new(&mut self.settings.max_iter, 5..=256).clamp_to_range(false)); ui.horizontal(|ui| { ui.label("Preview resolution:"); @@ -313,7 +313,7 @@ impl eframe::App for JuliaGUI { }); ui.label("Render iterations:"); - ui.add(Slider::new(&mut self.export_iterations, 5..=1024).clamp_to_range(false)); + ui.add(Slider::new(&mut self.export_max_iter, 5..=1024).clamp_to_range(false)); ui.label("Render resolution:"); ui.add(Slider::new(&mut self.export_res_power, 0..=6).clamp_to_range(false)); ui.label(format!( @@ -350,7 +350,7 @@ impl eframe::App for JuliaGUI { let predicted_render_time = (self.preview_render_ms * (1 << (self.export_res_power * 2)) as f64 - * (self.export_iterations as f64 / self.settings.iterations as f64) + * (self.export_max_iter as f64 / self.settings.max_iter as f64) / 1000.0) .floor(); if predicted_render_time < 60.0 {