mirror of
https://github.com/CrispyPin/julia-fractal-renderer.git
synced 2024-11-22 10:00:26 +01:00
multithread rendering with rayon
This commit is contained in:
parent
59739cfc1c
commit
fc209a78ad
4 changed files with 156 additions and 56 deletions
76
Cargo.lock
generated
76
Cargo.lock
generated
|
@ -608,6 +608,40 @@ dependencies = [
|
||||||
"cfg-if",
|
"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]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.16"
|
version = "0.8.16"
|
||||||
|
@ -1237,6 +1271,7 @@ dependencies = [
|
||||||
"image",
|
"image",
|
||||||
"native-dialog",
|
"native-dialog",
|
||||||
"rand",
|
"rand",
|
||||||
|
"rayon",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
@ -1343,6 +1378,15 @@ dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memoffset"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minimal-lexical"
|
name = "minimal-lexical"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
@ -1505,6 +1549,16 @@ dependencies = [
|
||||||
"autocfg",
|
"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]]
|
[[package]]
|
||||||
name = "num_enum"
|
name = "num_enum"
|
||||||
version = "0.5.11"
|
version = "0.5.11"
|
||||||
|
@ -1803,6 +1857,28 @@ version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9"
|
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]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
|
|
|
@ -10,6 +10,7 @@ eframe = "0.22.0"
|
||||||
image = { version = "0.24.6", default-features = false, features = ["png"] }
|
image = { version = "0.24.6", default-features = false, features = ["png"] }
|
||||||
native-dialog = "0.6.4"
|
native-dialog = "0.6.4"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
rayon = "1.7.0"
|
||||||
serde = { version = "1.0.171", features = ["derive"] }
|
serde = { version = "1.0.171", features = ["derive"] }
|
||||||
serde_json = "1.0.102"
|
serde_json = "1.0.102"
|
||||||
|
|
||||||
|
|
113
src/generate.rs
113
src/generate.rs
|
@ -1,13 +1,14 @@
|
||||||
use eframe::epaint::Vec2;
|
use eframe::epaint::Vec2;
|
||||||
use image::{Rgb, RgbImage};
|
use image::{Rgb, RgbImage};
|
||||||
|
use rayon::prelude::{IntoParallelIterator, ParallelIterator};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct RenderOptions {
|
pub struct RenderOptions {
|
||||||
pub width: u32,
|
pub width: usize,
|
||||||
pub height: u32,
|
pub height: usize,
|
||||||
pub unit_width: f64,
|
pub unit_width: f64,
|
||||||
pub iterations: u32,
|
pub max_iter: u16,
|
||||||
pub cx: f64,
|
pub cx: f64,
|
||||||
pub cy: f64,
|
pub cy: f64,
|
||||||
pub fill_style: FillStyle,
|
pub fill_style: FillStyle,
|
||||||
|
@ -25,7 +26,7 @@ impl Default for RenderOptions {
|
||||||
width: 512,
|
width: 512,
|
||||||
height: 512,
|
height: 512,
|
||||||
unit_width: 4.0,
|
unit_width: 4.0,
|
||||||
iterations: 128,
|
max_iter: 128,
|
||||||
cx: 0.4,
|
cx: 0.4,
|
||||||
cy: -0.2,
|
cy: -0.2,
|
||||||
fill_style: FillStyle::Bright,
|
fill_style: FillStyle::Bright,
|
||||||
|
@ -33,57 +34,79 @@ impl Default for RenderOptions {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view_point(q: &RenderOptions, image: RgbImage) -> RgbImage {
|
pub fn render_c(q: &RenderOptions, mut image: RgbImage) -> RgbImage {
|
||||||
apply_fn(image, q, |x, y| {
|
let width = q.width as f32;
|
||||||
let len = (Vec2::new(x as f32, y as f32) - Vec2::new(q.cx as f32, q.cy as f32)).length();
|
let height = q.height as f32;
|
||||||
if len < 0.03 {
|
let ppu = width / (q.unit_width as f32);
|
||||||
Some(Rgb([0, 120, 120]))
|
|
||||||
} else if len < 0.04 {
|
|
||||||
Some(Rgb([255; 3]))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render(q: &RenderOptions, color: (u8, u8, u8)) -> RgbImage {
|
let target = Vec2::new(q.cx as f32, q.cy as f32);
|
||||||
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<F>(mut image: RgbImage, q: &RenderOptions, op: F) -> RgbImage
|
|
||||||
where
|
|
||||||
F: Fn(f64, f64) -> Option<Rgb<u8>>,
|
|
||||||
{
|
|
||||||
let width = q.width as f64;
|
|
||||||
let height = q.height as f64;
|
|
||||||
let ppu = width / q.unit_width;
|
|
||||||
|
|
||||||
for y in 0..q.height {
|
for y in 0..q.height {
|
||||||
for x in 0..q.width {
|
for x in 0..q.width {
|
||||||
let sx = (x as f64 - width / 2.0) / ppu;
|
let sx = (x as f32 - width / 2.0) / ppu;
|
||||||
let sy = (y as f64 - height / 2.0) / ppu;
|
let sy = (y as f32 - height / 2.0) / ppu;
|
||||||
if let Some(pixel) = op(sx, sy) {
|
|
||||||
image.put_pixel(x, y, pixel);
|
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
|
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<u8> {
|
||||||
|
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::<Vec<_>>()
|
||||||
|
.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;
|
let mut iter = 0;
|
||||||
while (x * x + y * y) < 4.0 && iter < max_iter {
|
while (x * x + y * y) < 4.0 && iter < max_iter {
|
||||||
(x, y) = (
|
(x, y) = (
|
||||||
|
|
22
src/main.rs
22
src/main.rs
|
@ -13,7 +13,7 @@ use eframe::{
|
||||||
epaint::{TextureHandle, Vec2},
|
epaint::{TextureHandle, Vec2},
|
||||||
Frame, NativeOptions,
|
Frame, NativeOptions,
|
||||||
};
|
};
|
||||||
use generate::{render, view_point, FillStyle, RenderOptions};
|
use generate::{render_c, render_julia, FillStyle, RenderOptions};
|
||||||
use image::EncodableLayout;
|
use image::EncodableLayout;
|
||||||
use native_dialog::FileDialog;
|
use native_dialog::FileDialog;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -47,7 +47,7 @@ struct JuliaGUI {
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
export_render_ms: Option<f64>,
|
export_render_ms: Option<f64>,
|
||||||
export_res_power: u8,
|
export_res_power: u8,
|
||||||
export_iterations: u32,
|
export_max_iter: u16,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
export_path: PathBuf,
|
export_path: PathBuf,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
|
@ -77,7 +77,7 @@ impl Default for JuliaGUI {
|
||||||
preview_render_ms: 0.0,
|
preview_render_ms: 0.0,
|
||||||
export_render_ms: None,
|
export_render_ms: None,
|
||||||
export_res_power: 3,
|
export_res_power: 3,
|
||||||
export_iterations: 512,
|
export_max_iter: 512,
|
||||||
export_path: "".into(),
|
export_path: "".into(),
|
||||||
settings_changed: true,
|
settings_changed: true,
|
||||||
preview_point: false,
|
preview_point: false,
|
||||||
|
@ -113,7 +113,7 @@ impl JuliaGUI {
|
||||||
RenderJob::Exit => break,
|
RenderJob::Exit => break,
|
||||||
RenderJob::Render(path, options, color) => {
|
RenderJob::Render(path, options, color) => {
|
||||||
let start_time = SystemTime::now();
|
let start_time = SystemTime::now();
|
||||||
let image = render(&options, color);
|
let image = render_julia(&options, color);
|
||||||
if let Err(err) = image.save(&path) {
|
if let Err(err) = image.save(&path) {
|
||||||
println!("Failed to save render: {err}");
|
println!("Failed to save render: {err}");
|
||||||
}
|
}
|
||||||
|
@ -142,9 +142,9 @@ impl JuliaGUI {
|
||||||
|
|
||||||
fn update_preview(&mut self) {
|
fn update_preview(&mut self) {
|
||||||
let start_time = SystemTime::now();
|
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 {
|
if self.preview_point {
|
||||||
frame = view_point(&self.settings, frame);
|
frame = render_c(&self.settings, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(preview) = &mut self.preview {
|
if let Some(preview) = &mut self.preview {
|
||||||
|
@ -166,7 +166,7 @@ impl JuliaGUI {
|
||||||
let settings = RenderOptions {
|
let settings = RenderOptions {
|
||||||
width: self.settings.width * res_mul,
|
width: self.settings.width * res_mul,
|
||||||
height: self.settings.height * res_mul,
|
height: self.settings.height * res_mul,
|
||||||
iterations: self.export_iterations,
|
max_iter: self.export_max_iter,
|
||||||
..self.settings.clone()
|
..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));
|
let set_blue = ui.add(Slider::new(&mut self.color.2, 0..=16));
|
||||||
|
|
||||||
ui.label("Preview iterations:");
|
ui.label("Preview iterations:");
|
||||||
let set_iter = ui
|
let set_iter =
|
||||||
.add(Slider::new(&mut self.settings.iterations, 5..=256).clamp_to_range(false));
|
ui.add(Slider::new(&mut self.settings.max_iter, 5..=256).clamp_to_range(false));
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label("Preview resolution:");
|
ui.label("Preview resolution:");
|
||||||
|
@ -313,7 +313,7 @@ impl eframe::App for JuliaGUI {
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.label("Render iterations:");
|
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.label("Render resolution:");
|
||||||
ui.add(Slider::new(&mut self.export_res_power, 0..=6).clamp_to_range(false));
|
ui.add(Slider::new(&mut self.export_res_power, 0..=6).clamp_to_range(false));
|
||||||
ui.label(format!(
|
ui.label(format!(
|
||||||
|
@ -350,7 +350,7 @@ impl eframe::App for JuliaGUI {
|
||||||
|
|
||||||
let predicted_render_time = (self.preview_render_ms
|
let predicted_render_time = (self.preview_render_ms
|
||||||
* (1 << (self.export_res_power * 2)) as f64
|
* (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)
|
/ 1000.0)
|
||||||
.floor();
|
.floor();
|
||||||
if predicted_render_time < 60.0 {
|
if predicted_render_time < 60.0 {
|
||||||
|
|
Loading…
Reference in a new issue