Skip to content

Commit 82d4085

Browse files
authored
Merge pull request #1 from Victor4X/winit-pixels
Pixels + Winit
2 parents 3bb9f2d + 45cd9e8 commit 82d4085

File tree

3 files changed

+226
-80
lines changed

3 files changed

+226
-80
lines changed

Cargo.toml

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@ edition = "2018"
88
[dependencies]
99
num = "0.4"
1010
image = "0.23.14"
11-
crossbeam = "0.8.1"
11+
crossbeam = "0.8.1"
12+
pixels = "0.5.0"
13+
winit = "0.25.0"
14+
winit_input_helper = "0.10"
15+
log = "0.4"

README.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
# rust-mandelbrot
22
Mandelbrot explorer written in rust
33

4+
## Controls:
5+
WASD -> Translation
6+
ZX -> Zoom
7+
Space -> Reset
8+
49
## TODO:
510
- [x] Multithreading
6-
- [ ] Winit + Pixels
11+
- [X] Winit + Pixels
12+
- [ ] Better argument handling
13+
- [ ] Handle smaller number differences
14+
- [ ] Dynamic explorer render size depending on window size
15+
- [ ] Clean unnecessary
16+
- [ ] Refactor with structs based on (https://github.com/parasyte/pixels/blob/master/examples/conway/src/main.rs)
17+
- [ ] Read out frame rendering times

src/main.rs

+209-78
Original file line numberDiff line numberDiff line change
@@ -1,132 +1,263 @@
1-
use num::{Complex};
2-
use std::str::FromStr;
1+
use num::Complex;
32
use std::env;
3+
use std::str::FromStr;
44

55
use crossbeam;
66

7-
use image::ColorType;
87
use image::png::PngEncoder;
8+
use image::ColorType;
99
use std::fs::File;
1010

11-
fn main() {
12-
let args: Vec<String> = env::args().collect();
13-
if args.len() != 6 {
14-
eprintln!("Usage: {} FILE PIXELS SEPARATOR UPPERLEFT LOWERRIGHT", args[0]);
15-
eprintln!("Example: {} mandel.png 1000x750 -1.20,0.35 -1,0.20", args[0]);
16-
std::process::exit(1);
17-
}
18-
let bounds= parse_pair(&args[2], char::from_str(&args[3]).expect("Seperator conversion failed")).expect("Parsing of image dimensions failed with given arguments");
19-
let upper_left = parse_complex(&args[4]).expect("Parsing of upper left complex number failed");
20-
let lower_right = parse_complex(&args[5]).expect("Parsing of lower right complex number failed");
11+
use log::{debug, error};
2112

22-
let mut pixels = vec![0; bounds.0 * bounds.1];
23-
13+
use pixels::{Error, Pixels, SurfaceTexture};
14+
use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
15+
use winit::event::{Event, VirtualKeyCode};
16+
use winit::event_loop::{ControlFlow, EventLoop};
17+
use winit_input_helper::WinitInputHelper;
2418

25-
// Multithreading stuff here
26-
let threads = 16; // Higher number = More speed
27-
let rows_per_band = bounds.1 / threads + 1;
19+
const SCREEN_WIDTH: usize = 1000;
20+
const SCREEN_HEIGHT: usize = 1000;
21+
22+
fn main() -> Result<(), Error> {
23+
// Argument parsing here. (Custom window size) TODO: Better argument handling
24+
// let args: Vec<String> = env::args().collect();
25+
// if args.len() != 6 {
26+
// eprintln!("Usage: {} FILE PIXELS SEPARATOR UPPERLEFT LOWERRIGHT", args[0]);
27+
// eprintln!("Example: {} mandel.png 1000x750 x -1.20,0.35 -1,0.20", args[0]);
28+
// std::process::exit(1);
29+
// }
30+
31+
// let bounds= parse_pair(&args[2], char::from_str(&args[3]).expect("Seperator conversion failed")).expect("Parsing of image dimensions failed with given arguments");
32+
// let upper_left = parse_complex(&args[4]).expect("Parsing of upper left complex number failed");
33+
// let lower_right = parse_complex(&args[5]).expect("Parsing of lower right complex number failed");
34+
35+
let event_loop = EventLoop::new();
36+
let mut input = WinitInputHelper::new();
37+
let window = create_window("Mandelbrot Explorer", &event_loop);
38+
39+
let bounds = (SCREEN_WIDTH, SCREEN_HEIGHT);
40+
let mut upper_left = Complex { re: -1.0, im: 1.0 };
41+
let mut lower_right = Complex { re: 1.0, im: -1.0 };
42+
43+
//let mut pixels = vec![0; bounds.0 * bounds.1];
2844

29-
{
30-
let bands: Vec<&mut [u8]> = pixels.chunks_mut(rows_per_band * bounds.0).collect();
31-
crossbeam::scope(|spawner| {
32-
for (i, band) in bands.into_iter().enumerate() {
33-
let top = rows_per_band * i;
34-
let height = band.len() / bounds.0;
35-
let band_bounds = (bounds.0, height);
36-
let band_upper_left = pixel_to_point(bounds, (0, top), upper_left, lower_right);
37-
let band_lower_right = pixel_to_point(bounds, (bounds.0, top + height), upper_left, lower_right);
45+
let surface_texture = SurfaceTexture::new(SCREEN_WIDTH as u32, SCREEN_HEIGHT as u32, &window);
46+
let mut pixels = Pixels::new(SCREEN_WIDTH as u32, SCREEN_HEIGHT as u32, surface_texture)
47+
.expect("Pixels failed to initialize");
48+
49+
println!("{}", pixels.get_frame().len()); // Make sure bounds correctly define amount of pixels
50+
51+
render_multi(pixels.get_frame(), bounds, upper_left, lower_right);
52+
53+
event_loop.run(move |event, _, control_flow| {
54+
// The one and only event that winit_input_helper doesn't have for us...
55+
if let Event::RedrawRequested(_) = event {
56+
render_multi(pixels.get_frame(), bounds, upper_left, lower_right);
57+
if pixels
58+
.render()
59+
.map_err(|e| error!("pixels.render() failed: {}", e)) // I probably broke this :P
60+
.is_err()
61+
{
62+
*control_flow = ControlFlow::Exit;
63+
return;
64+
}
65+
}
66+
67+
// For everything else, for let winit_input_helper collect events to build its state.
68+
// It returns `true` when it is time to update our game state and request a redraw.
69+
if input.update(&event) {
70+
// Close events
71+
if input.key_pressed(VirtualKeyCode::Escape) || input.quit() {
72+
*control_flow = ControlFlow::Exit;
73+
return;
74+
}
75+
if input.key_pressed(VirtualKeyCode::W) {
76+
let displacement = Complex {
77+
re: 0.0 * (upper_left.re - lower_right.re),
78+
im: 0.05 * (upper_left.im - lower_right.im),
79+
};
80+
81+
upper_left += displacement;
82+
lower_right += displacement;
83+
window.request_redraw();
84+
}
85+
if input.key_pressed(VirtualKeyCode::A) {
86+
let displacement = Complex {
87+
re: 0.05 * (upper_left.re - lower_right.re),
88+
im: 0.0 * (upper_left.im - lower_right.im),
89+
};
90+
91+
upper_left += displacement;
92+
lower_right += displacement;
93+
window.request_redraw();
94+
}
95+
if input.key_pressed(VirtualKeyCode::S) {
96+
let displacement = Complex {
97+
re: 0.0 * (upper_left.re - lower_right.re),
98+
im: -0.05 * (upper_left.im - lower_right.im),
99+
};
100+
101+
upper_left += displacement;
102+
lower_right += displacement;
103+
window.request_redraw();
104+
}
105+
if input.key_pressed(VirtualKeyCode::D) {
106+
let displacement = Complex {
107+
re: -0.05 * (upper_left.re - lower_right.re),
108+
im: 0.0 * (upper_left.im - lower_right.im),
109+
};
38110

39-
spawner.spawn(move |_| {
40-
render(band, band_bounds, band_upper_left, band_lower_right);
41-
});
111+
upper_left += displacement;
112+
lower_right += displacement;
113+
window.request_redraw();
42114
}
43-
}).unwrap();
44-
}
45115

46-
write_image(&args[1], &pixels, bounds).expect("Image writing failed");
116+
// Zooming TODO: Still not completely centered (for small bounds)
117+
if input.key_pressed(VirtualKeyCode::Z) {
118+
let scalar = 0.10;
119+
120+
upper_left -= scalar*(upper_left-lower_right)/2.0;
121+
lower_right += scalar*(upper_left-lower_right)/2.0;
122+
window.request_redraw();
123+
}
124+
if input.key_pressed(VirtualKeyCode::X) {
125+
let scalar = 0.10;
126+
127+
upper_left += scalar*(upper_left-lower_right)/2.0;
128+
lower_right -= scalar*(upper_left-lower_right)/2.0;
129+
window.request_redraw();
130+
}
131+
132+
// Resetting
133+
if input.key_pressed(VirtualKeyCode::Space) {
134+
upper_left = Complex { re: -1.0, im: 1.0 };
135+
lower_right = Complex { re: 1.0, im: -1.0 };
136+
window.request_redraw();
137+
}
138+
}
139+
});
140+
//write_image(&args[1], &pixels, bounds).expect("Image writing failed"); TODO: Use somewhere else
47141
}
48142

49-
// Write pixel buffer to file
50-
fn write_image(filename: &str, pixels: &[u8], bounds: (usize, usize)) -> Result<(), std::io::Error> {
51-
let output = File::create(filename)?;
143+
// Multithreaded render
144+
fn render_multi(
145+
pixels: &mut [u8],
146+
bounds: (usize, usize),
147+
upper_left: Complex<f64>,
148+
lower_right: Complex<f64>,
149+
) {
150+
println!(
151+
"Rendering between {},{} and {},{}",
152+
upper_left.re, upper_left.im, lower_right.re, lower_right.im
153+
);
154+
155+
// Multithreading stuff here
156+
let threads = 16; // Higher number = More speed
157+
let rows_per_band = bounds.1 / threads + 1;
158+
159+
let bands: Vec<&mut [u8]> = pixels.chunks_mut(rows_per_band * bounds.0 * 4).collect();
160+
crossbeam::scope(|spawner| {
161+
for (i, band) in bands.into_iter().enumerate() {
162+
let top = rows_per_band * i;
163+
let height = band.len() / 4 / bounds.0;
164+
let band_bounds = (bounds.0, height);
165+
let band_upper_left = pixel_to_point(bounds, (0, top), upper_left, lower_right);
166+
let band_lower_right =
167+
pixel_to_point(bounds, (bounds.0, top + height), upper_left, lower_right);
52168

53-
let encoder = PngEncoder::new(output);
54-
match encoder.encode(&pixels, bounds.0 as u32, bounds.1 as u32, ColorType::L8) {
55-
Ok(()) => (),
56-
Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) // This seems scuffed. TODO: Figure out a better way
57-
}; // L8 is 8 bit luminence
58-
Ok(()) // Can error out through the two ? but otherwise return OK(()) -- Ok with a unit
169+
spawner.spawn(move |_| {
170+
render(band, band_bounds, band_upper_left, band_lower_right);
171+
});
172+
}
173+
})
174+
.unwrap();
59175
}
60176

61177
// Render a portion of the mandelbrot set into a given buffer of pixels
62-
fn render(pixels: &mut [u8], bounds: (usize, usize), upper_left: Complex<f64>, lower_right: Complex<f64>) {
63-
assert!(pixels.len() == bounds.0 * bounds.1); // Make sure bounds correctly define amount of pixels
178+
fn render(
179+
pixels: &mut [u8],
180+
bounds: (usize, usize),
181+
upper_left: Complex<f64>,
182+
lower_right: Complex<f64>,
183+
) {
184+
assert!(pixels.len() == bounds.0 * bounds.1 * 4); // Make sure bounds correctly define amount of pixels
64185

65186
for row in 0..bounds.1 {
66187
for column in 0..bounds.0 {
67188
let point = pixel_to_point(bounds, (column, row), upper_left, lower_right);
68-
pixels[row * bounds.0 + column] = match escape_time(point, 255) {
189+
let point_shade = match escape_time(point, 255) {
69190
None => 0,
70-
Some(count) => 255 - count as u8
191+
Some(count) => 255 - count as u8,
71192
};
72-
}
73-
}
74-
}
75193

76-
// Parse string to coordinates
77-
fn parse_pair<T: FromStr>(s: &str, separator: char) -> Option<(T, T)> {
78-
match s.find(separator) { // Find the separator location
79-
None => None,
80-
Some(index) => {
81-
match (T::from_str(&s[..index]), T::from_str(&s[index + 1..])) { // Match on tuple
82-
(Ok(l), Ok(r)) => Some((l, r)),
83-
_ => None
84-
}
194+
let pixel_color = [0, point_shade, point_shade, point_shade];
195+
196+
let pixel_start = (row * bounds.0 + column) * 4;
197+
198+
pixels[pixel_start..pixel_start + 4].copy_from_slice(&pixel_color)
85199
}
86200
}
87201
}
88202

89-
#[test]
90-
fn test_parse_pair() {
91-
assert_eq!(parse_pair::<i32>("", ','), None);
92-
assert_eq!(parse_pair::<i32>("10,", ','), None);
93-
assert_eq!(parse_pair::<i32>(",10", ','), None);
94-
assert_eq!(parse_pair::<i32>("10,20", ','), Some((10, 20)));
95-
}
203+
// Create the application window
204+
fn create_window(title: &str, event_loop: &EventLoop<()>) -> winit::window::Window {
205+
// Create a hidden window so we can estimate a good default window size
206+
let window = winit::window::WindowBuilder::new()
207+
.with_visible(true)
208+
.with_title(title)
209+
.with_inner_size(LogicalSize::new(1000, 1000))
210+
.build(event_loop)
211+
.unwrap();
96212

97-
// Parse string to complex number ex: 1.03,2.58 -> Complex<f64> {re: 1.03, im: 2.58}
98-
fn parse_complex(s: &str) -> Option<Complex<f64>> {
99-
match parse_pair(s, ',') {
100-
Some((re, im)) => Some(Complex { re, im }),
101-
None => None
102-
}
213+
window
103214
}
104215

105216
// Function for mapping a given pixel position in a given image size to a point on the complex plane within two given complex points
106-
fn pixel_to_point(bounds: (usize, usize), pixel: (usize,usize), upper_left: Complex<f64>, lower_right: Complex<f64>) -> Complex<f64> {
217+
fn pixel_to_point(
218+
bounds: (usize, usize),
219+
pixel: (usize, usize),
220+
upper_left: Complex<f64>,
221+
lower_right: Complex<f64>,
222+
) -> Complex<f64> {
107223
// Calculate (width, height) on complex plane
108-
let (width, height) = (lower_right.re - upper_left.re, upper_left.im - lower_right.im);
224+
let (width, height) = (
225+
lower_right.re - upper_left.re,
226+
upper_left.im - lower_right.im,
227+
);
109228

110229
Complex {
111230
re: upper_left.re + pixel.0 as f64 * width / bounds.0 as f64,
112-
im: upper_left.im - pixel.1 as f64 * height / bounds.1 as f64 // Negative to flip the reversed axis in the pixel world
231+
im: upper_left.im - pixel.1 as f64 * height / bounds.1 as f64, // Negative to flip the reversed axis in the pixel world
113232
}
114233
}
115234

116235
#[test]
117236
fn test_pixel_to_point() {
118-
assert_eq!(pixel_to_point((100, 200), (25, 175), Complex {re: -1.0, im: 1.0}, Complex {re: 1.0, im: -1.0}), Complex { re: -0.5, im: -0.75})
237+
assert_eq!(
238+
pixel_to_point(
239+
(100, 200),
240+
(25, 175),
241+
Complex { re: -1.0, im: 1.0 },
242+
Complex { re: 1.0, im: -1.0 }
243+
),
244+
Complex {
245+
re: -0.5,
246+
im: -0.75
247+
}
248+
)
119249
}
120250

121251
// Function for determining the mandelbrot set escape time of a given point on the complex plane
122252
fn escape_time(c: Complex<f64>, limit: usize) -> Option<usize> {
123-
let mut z = Complex {re: 0.0, im: 0.0};
253+
let mut z = Complex { re: 0.0, im: 0.0 };
124254
for i in 0..limit {
125-
if z.norm_sqr() > 4.0 { // Square of the distance to the origin of the complex plane
255+
if z.norm_sqr() > 4.0 {
256+
// Square of the distance to the origin of the complex plane
126257
return Some(i);
127258
}
128259
z = z * z + c;
129260
}
130261

131262
None
132-
}
263+
}

0 commit comments

Comments
 (0)