6
\$\begingroup\$

I'm learning Rust, and as-is tradition, I'm starting out with a Mandelbrot Set explorer as my first project (although, it just produces images so far). When run, it just creates and writes an image to the CWD. The coordinates and coloring function that are hardcoded produce my avatar. Eventually, there will be a GUI using Druid that allows changing these at runtime, but one step at a time.

text.rs is technically dead code, but I figured I'd include it because it works when hooked up. It produces ASCII images of the Set.

This is the first large chunk of code I've written in Rust, and would like feedback on anything. Not really anything in particular yet. I just want to make sure I'm not doing any bad habits early on. Am I doing it correctly? It feels kind of awkward to set up. Speed will eventually be a concern, so if I'm doing anything wonky that will bite me in terms of performance later on, I'd like to know about that as well.

Actually, I'd like comments on char_for_iterations if possible. It looks off to me. It requires tons of casting, and then also requires as_bytes which seems to be O(1), but I can't find anything concrete saying that. That function gave me a really hard time for some reason.

I'll post the code inline below, but it's also here in case that's easier to read.

main.rs

mod ui;
mod logic;

fn main() {
    let width = 7000;
    let height = width * 2/3;

    let start = std::time::Instant::now();
    ui::image::draw_area("mandelbrot.png", 0.3539265474695936, 0.3607889277625950, 0.3531315391015801, 0.3599939193945812, width, height, 200, 2);
    println!("Elapsed: {:?}", start.elapsed());
}

logic.rs

pub mod mandelbrot_iteration;

logic/mandelbrot_iteration.rs

use num::complex::Complex;

pub type ComplexPoint = Complex<f64>;

fn mandelbrot_iteration(initial: ComplexPoint, current: ComplexPoint) -> ComplexPoint {
    let current_sq = current * current;
    return Complex {
        re: current_sq.re + initial.re,
        im: current_sq.im + initial.im,
    };
}

fn is_under_limit(current: ComplexPoint, infinity_limit: u32) -> bool {
    let real_sq = current.re * current.re;
    let imag_sq = current.im * current.im;

    return (real_sq + imag_sq) <= (infinity_limit * infinity_limit) as f64;
}

pub fn test_point(initial: ComplexPoint, max_iterations: u32, infinity_limit: u32) -> u32 {
    let mut current = initial;

    let mut i = 0;
    while i < max_iterations {
        if is_under_limit(current, infinity_limit) {
            current = mandelbrot_iteration(initial, current);
        } else {
            break;
        }

        i += 1;
    }

    return i;
}

/// Callback arguments: real, imag, display_x, display_y, iters
pub fn test_area
<F: FnMut(f64, f64, u32, u32, u32) -> ()>
(
    real_min: f64,
    real_max: f64,
    imag_min: f64,
    imag_max: f64,
    display_width: u32,
    display_height: u32,
    max_iterations: u32,
    infinity_limit: u32,
    mut point_callback: F,
) -> () {
    let real_length = real_max - real_min;
    let imag_length = imag_max - imag_min;

    let real_step = real_length / display_width  as f64;
    let imag_step = imag_length / display_height as f64;

    for display_y in 0..display_height {
        let imag = imag_min + (display_y as f64) * imag_step;
        for display_x in 0..display_width {
            let real = real_min + (display_x as f64) * real_step;
            let c = Complex::new(real, imag);
            let iters = test_point(c, max_iterations, infinity_limit);
            point_callback(real, imag, display_x, display_y, iters);
        }
    }
}

ui.rs

pub mod text;
pub mod image;

ui/image.rs

use image::{ImageBuffer, Rgb};
use num::{clamp, Num};

use crate::logic::mandelbrot_iteration;

fn wrap<N: Num + Copy>(n: N, min_n: N, max_n: N) -> N {
    let limit = max_n - min_n;
    return (n % limit) + min_n;
}

fn color_f(real: f64, imag: f64, iters: u32) -> Rgb<u8> {
    let f_iters = iters as f64;
    let red = clamp(real * f_iters * 6.0, 0.0, 255.0);
    let green = clamp(imag * f_iters * 3.0, 0.0, 255.0);
    let blue = clamp(f_iters * f_iters * 0.01, 0.0, 255.0);

    return Rgb([red as u8, green as u8, blue as u8]);
}

pub fn draw_area(file_path: &str, real_min: f64, real_max: f64, imag_min: f64, imag_max: f64, display_width: u32, display_height: u32, max_iterations: u32, infinity_limit: u32) -> () {
    let mut buffer = ImageBuffer::new(display_width, display_height);

    let total_pixels = display_width * display_height;
    let report_every = total_pixels as u32 / 10;

    let mut current_pixel = 0;
    mandelbrot_iteration::test_area(real_min, real_max, imag_min, imag_max, display_width, display_height, max_iterations, infinity_limit, |real, imag, x, y, iters | {
        let pixel = buffer.get_pixel_mut(x, y);
        *pixel = color_f(real, imag, iters);

        current_pixel += 1;
        if current_pixel % report_every == 0 {
            let perc = current_pixel as f32 / total_pixels as f32;
            println!("Progress: {}/{} ({}%)", current_pixel, total_pixels, perc * 100.0);
        }
    });

    match buffer.save(file_path) {
        Ok(_) => println!("Saved successfully"),
        Err(err) => println!("Error: {}", err),
    };
}

ui/text.rs

use crate::logic::mandelbrot_iteration;

// Looks better with less chars for some reason
const DENSITY_SORTED_CHARS: &str = " .-:;~*\\/i({vx?@";

fn char_for_iterations(n_iterations: u32, max_iterations: u32) -> char {
    let density_perc = n_iterations as f32 / (max_iterations + 1) as f32;
    let index = ((DENSITY_SORTED_CHARS.len()) as f32 * density_perc) as usize;
    return DENSITY_SORTED_CHARS.as_bytes()[index] as char;
}

pub fn draw_area(real_min: f64, real_max: f64, imag_min: f64, imag_max: f64, display_width: u32, display_height: u32, max_iterations: u32, infinity_limit: u32) -> String {
    let mut result = String::new();

    mandelbrot_iteration::test_area(real_min, real_max, imag_min, imag_max, display_width, display_height, max_iterations, infinity_limit, | _real, _imag, x, _y, iters | {
        result.push(char_for_iterations(iters, max_iterations));

        if x >= display_width - 1 {
            result.push('\n');
        }
    });

    return result;
}
\$\endgroup\$

1 Answer 1

5
\$\begingroup\$

Couple of things I noticed:

  1. infinity_limit is constant for a given call to draw_area. Its only use is in is_under_limit in this form:

    return (real_sq + imag_sq) <= (infinity_limit * infinity_limit) as f64;

    The compiler might be able to figure this out and optimize it but I'd compute infinity_limit * infinity_limit once at the top level and pass it down as an f64. Best case it saves you max_iteration * num_pixel multiplications.

  2. Official Rust style I think is to omit return (i.e. instead of fn myfn() { ...; return x; } write fn myfn() { ...; x }

  3. mandelbrot_iteration can be simplified to

    fn mandelbrot_iteration(initial: ComplexPoint, current: ComplexPoint) -> ComplexPoint {
        current * current + initial
    }
    
  4. test_point can be simplified by moving the is_under_limit check into the while condition:

    pub fn test_point(initial: ComplexPoint, max_iterations: u32, infinity_limit: u32) -> u32 {
        let mut current = initial;
    
        let mut i = 0;
        while is_under_limit(current, infinity_limit) && i < max_iterations {
            current = mandelbrot_iteration(initial, current);
            i += 1;
        }
        i
    }
    
  5. DENSITY_SORTED_CHARS is not used as a string so shouldn't be one - an array of char will be a much better fit. Actually using a slice means you can omit the number of elements:

    const MY_ARRAY: &[char] = &[' ', '.', '-', ':', ';', '~', '*', '\\', '/', 'i', '(', '{', 'v', 'x', '?', '@'];

    A rationale for why string indexing is not provided is given here: https://stackoverflow.com/a/24542502/220986

  6. If you keep away from fractions and compute percentage and keep computations in the integer realm you can probably save a bunch of castings in char_for_iterations. Might lose some precision but I don't think it matters:

    fn char_for_iterations(n_iterations: u32, max_iterations: u32) -> char {
        const scale: u32 = 100;
        let density = n_iterations * scale / (max_iterations + 1);
        let index = (DENSITY_SORTED_CHARS.len() as u32) * density / scale;
        DENSITY_SORTED_CHARS[index as usize]
    }
    

    Scale factor of 100 is basically the equivalent of turning a fraction into a percentage value between 0 and 100. In some basic tests this yields a difference of 1 index in ~6% of all cases. If you set scale to 10_000 then there is no difference (with max_iterations = 200)

\$\endgroup\$
1
  • \$\begingroup\$ Thank you very much. \$\endgroup\$ Commented Jan 23 at 14:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.