Skip to content

Instantly share code, notes, and snippets.

@dgerrells
Created November 8, 2024 20:52
Show Gist options
  • Save dgerrells/cc8557b392df44c9888fcc70a010de46 to your computer and use it in GitHub Desktop.
Save dgerrells/cc8557b392df44c9888fcc70a010de46 to your computer and use it in GitHub Desktop.
how fast is rust
# how fast is rust
This contains all the various versions and iterations for a project with the goal of simulating as many particles as possible. There are several versions as it works towards the end of simd and multi-threaded. There are also a few wasm versions with the required tooling setup. This does require two different cargo toml files. There is a build web script which runs 3 steps to produce a fast and small wasm file but you will need to update the input output locations.
[package]
name = "how_fast_is_rust"
version = "0.1.0"
edition = "2021"
[dependencies]
minifb = "0.27"
rand = "0.8"
wasm-bindgen = "0.2.95"
[profile.release]
opt-level = 3
jemalloc = true
[package]
name = "how_fast_is_rust"
version = "0.1.0"
edition = "2021"
[dependencies]
getrandom = { version = "0.2", features = ["js"] }
rand = "0.8"
wasm-bindgen = "0.2.95"
[profile.release]
opt-level = 3
lto = true
[lib]
crate-type = ["cdylib"]
#/binbash
cargo build --target=wasm32-unknown-unknown --release
wasm-bindgen ./target/wasm32-unknown-unknown/release/how_fast_is_rust.wasm --out-dir /sabby-rust-basic --target web
wasm-opt /how_fast_is_rust_bg.wasm -o /how_fast_is_rust_bg.wasm -O3 --strip-debug
pub mod thread_pool;
use std::sync::{Arc, Mutex, Condvar, mpsc};
use std::thread;
use std::thread::JoinHandle;
// I will be real, chatgpt helped me in this file.
// I don't like it and over time I am sure I will get better.
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Message>,
job_count: Arc<(Mutex<usize>, Condvar)>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
enum Message {
NewJob(Job),
}
impl ThreadPool {
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0); // nifty
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let job_count = Arc::new((Mutex::new(0), Condvar::new()));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver), Arc::clone(&job_count)));
}
ThreadPool {
workers,
sender,
job_count,
}
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let (lock, _) = &*self.job_count;
*lock.lock().unwrap() += 1;
let job = Box::new(f);
self.sender.send(Message::NewJob(job)).unwrap();
}
pub fn wait_for_completion(&self) {
let (lock, cvar) = &*self.job_count;
let mut count = lock.lock().unwrap();
while *count > 0 {
count = cvar.wait(count).unwrap();
}
}
}
pub struct Worker {
id: usize,
handle: Option<JoinHandle<()>>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Message>>>, job_count: Arc<(Mutex<usize>, Condvar)>) -> Worker {
let handle = thread::spawn(move || loop {
let message = receiver.lock().unwrap().recv();
match message {
Ok(Message::NewJob(job)) => {
job();
let (lock, cvar) = &*job_count;
let mut count = lock.lock().unwrap();
*count -= 1;
cvar.notify_all();
}
Err(_) => break,
}
});
Worker {
id,
handle: Some(handle),
}
}
}
use minifb::{Key, MouseButton, Window, WindowOptions};
use rand::Rng;
use std::time::Instant;
const WIDTH: usize = 1200;
const HEIGHT: usize = 800;
const PARTICLE_COUNT: usize = 5_000_000;
const GRAVITY_STRENGTH: f32 = 205.5*8.0;
#[repr(align(16))]
struct Particle {
x: f32,
y: f32,
dx: f32,
dy: f32,
}
fn generate_particles(count: usize, width: usize, height: usize) -> Vec<Particle> {
let mut rng = rand::thread_rng();
let mut particles = Vec::with_capacity(count);
for _ in 0..count {
particles.push(Particle {
x: rng.gen_range(0.0..width as f32),
y: rng.gen_range(0.0..height as f32),
dx: rng.gen_range(-1.0..1.0),
dy: rng.gen_range(-1.0..1.0),
});
}
particles
}
fn main() {
let mut particles = generate_particles(PARTICLE_COUNT, WIDTH, HEIGHT);
let mut buffer: Vec<u32> = vec![0; WIDTH * HEIGHT];
let mut window = Window::new("Safu", WIDTH, HEIGHT, WindowOptions::default())
.unwrap_or_else(|e| {
panic!("{}", e);
});
let mut mouse_pos = (WIDTH as f32 / 2.0, HEIGHT as f32 / 2.0);
let mut mouse_pressed = false;
let friction_per_second = 0.99_f32.powf(60.0);
let mut last_frame_time = Instant::now();
let mut frame_count = 0;
let mut last_fps_time = Instant::now();
let mut fps = 0;
while window.is_open() && !window.is_key_down(Key::Escape) {
let now = Instant::now();
let delta_time = now.duration_since(last_frame_time).as_secs_f32();
last_frame_time = now;
if let Some((mx, my)) = window.get_mouse_pos(minifb::MouseMode::Clamp) {
mouse_pos = (mx, my);
}
mouse_pressed = window.get_mouse_down(MouseButton::Left);
let dt = delta_time;
for particle in particles.iter_mut() {
if mouse_pressed {
let dx = mouse_pos.0 - particle.x;
let dy = mouse_pos.1 - particle.y;
let distance = (dx * dx + dy * dy).sqrt();
if distance > 0.2 {
let inv_gravity = GRAVITY_STRENGTH / distance;
particle.dx += dx * inv_gravity * dt;
particle.dy += dy * inv_gravity * dt;
}
}
let friction = friction_per_second.powf(dt);
particle.dx *= friction;
particle.dy *= friction;
particle.x += particle.dx * dt;
particle.y += particle.dy * dt;
}
buffer.iter_mut().for_each(|pixel| *pixel = 0);
for particle in &particles {
let x = particle.x as usize;
let y = particle.y as usize;
let red = (particle.x / WIDTH as f32 * 255.0 * 0.8) as u32;
let green = (particle.y / HEIGHT as f32 * 255.0 * 0.8) as u32;
let blue = (255.0*0.5) as u32;
let color = (red << 16) | (green << 8) | blue;
let idx = (y.max(0).min(HEIGHT-1) * WIDTH + x.max(0).min(WIDTH-1));
buffer[idx] = color;
}
window.update_with_buffer(&buffer, WIDTH, HEIGHT).unwrap();
frame_count += 1;
if now.duration_since(last_fps_time).as_secs_f32() >= 1.0 {
fps = frame_count;
frame_count = 0;
last_fps_time = now;
println!("FPS: {} {}", fps, delta_time);
}
}
}
use minifb::{Key, MouseButton, Window, WindowOptions};
use std::mem::MaybeUninit;
use rand::Rng;
use std::time::{Instant};
const WIDTH: usize = 1200;
const HEIGHT: usize = 800;
const PARTICLE_COUNT: usize = 5_000_000;
const GRAVITY_STRENGTH: f32 = 200.5;
const MAX_PERF_SAMPLE_FRAMES: usize = 1000;
fn main() {
let mut particles: Box<[MaybeUninit<f32>]> = Box::new_uninit_slice(PARTICLE_COUNT*4);
for i in 0..(PARTICLE_COUNT * 4) {
particles[i].write(0.0);
}
let mut particles: Box<[f32]> = unsafe { std::mem::transmute(particles) };
let mut rng = rand::thread_rng();
let mut buffer: Vec<u32> = vec![0; WIDTH * HEIGHT];
let mut window = Window::new("Safu", WIDTH, HEIGHT, WindowOptions::default())
.unwrap_or_else(|e| {
panic!("{}", e);
});
window.set_target_fps(120);
let mut i: usize = 0;
while i < PARTICLE_COUNT {
let idx = i*4;
particles[idx] = rng.gen_range(0.0..WIDTH as f32);
particles[idx+1] = rng.gen_range(0.0..HEIGHT as f32);
particles[idx+2] = rng.gen_range(-1.0..1.0);
particles[idx+3] = rng.gen_range(-1.0..1.0);
i += 1;
}
let mut mouse_pos = (WIDTH as f32 / 2.0, HEIGHT as f32 / 2.0);
let mut mouse_pressed = false;
let friction_per_second = 0.99_f32.powf(60.0);
let mut frame_count = 0;
let mut last_fps_time = Instant::now();
let mut last_frame_time = Instant::now();
let mut simulation_times = vec![];
let mut drawing_times = vec![];
let mut fps = 0;
while window.is_open() && !window.is_key_down(Key::Escape) {
let now = Instant::now();
let delta_time = now.duration_since(last_frame_time).as_secs_f32();
last_frame_time = now;
let sim_start = Instant::now();
if let Some((mx, my)) = window.get_mouse_pos(minifb::MouseMode::Clamp) {
mouse_pos = (mx, my);
}
mouse_pressed = window.get_mouse_down(MouseButton::Left);
for particle in particles.chunks_mut(4) {
if mouse_pressed {
let dx = mouse_pos.0 - particle[0];
let dy = mouse_pos.1 - particle[1];
let distance = (dx * dx + dy * dy).sqrt();
if distance > 1.0 {
let inv_gravity = GRAVITY_STRENGTH / distance;
particle[2] += dx * inv_gravity * 8.0 * delta_time;
particle[3] += dy * inv_gravity * 8.0 * delta_time;
}
}
let friction = friction_per_second.powf(delta_time);
particle[2] *= friction;
particle[3] *= friction;
particle[0] += particle[2] * delta_time;
particle[1] += particle[3] * delta_time;
}
simulation_times.push(sim_start.elapsed().as_secs_f32());
if simulation_times.len() > MAX_PERF_SAMPLE_FRAMES {
simulation_times.remove(0);
}
let draw_start = Instant::now();
buffer.iter_mut().for_each(|pixel| *pixel = 0);
for particle in particles.chunks_mut(4) {
let x = particle[0] as usize;
let y = particle[1] as usize;
if x < WIDTH && y < HEIGHT && x > 0 && y > 0 {
let red = (particle[0] / WIDTH as f32 * 255.0 * 0.8) as u32;
let green = (particle[1] / HEIGHT as f32 * 255.0 * 0.8) as u32;
let blue = (255.0*0.5) as u32;
let color = (red << 16) | (green << 8) | blue;
buffer[y * WIDTH + x] = color;
}
}
window.update_with_buffer(&buffer, WIDTH, HEIGHT).unwrap();
drawing_times.push(draw_start.elapsed().as_secs_f32());
if drawing_times.len() > MAX_PERF_SAMPLE_FRAMES {
drawing_times.remove(0);
}
frame_count += 1;
if now.duration_since(last_fps_time).as_secs_f32() >= 1.0 {
fps = frame_count;
frame_count = 0;
last_fps_time = now;
let avg_simulation_time: f32 = simulation_times.iter().sum::<f32>() / simulation_times.len() as f32;
let avg_drawing_time: f32 = drawing_times.iter().sum::<f32>() / drawing_times.len() as f32;
println!("FPS: {}", fps);
println!("Avg Simulation Time: {:.5}", avg_simulation_time*1000.0);
println!("Avg Drawing Time: {:.5}", avg_drawing_time*1000.0);
}
}
}
use minifb::{Key, MouseButton, Window, WindowOptions};
use rand::Rng;
use std::mem::MaybeUninit;
use std::time::Instant;
use rayon::prelude::*;
const WIDTH: usize = 1200;
const HEIGHT: usize = 800;
const PARTICLE_COUNT: usize = 20_000_000;
const GRAVITY_STRENGTH: f32 = 200.5;
const MAX_PERF_SAMPLE_FRAMES: usize = 100;
fn main() {
let mut particles: Box<[MaybeUninit<f32>]> = Box::new_uninit_slice(PARTICLE_COUNT * 4);
for i in 0..(PARTICLE_COUNT * 4) {
particles[i].write(0.0);
}
let mut particles: Box<[f32]> = unsafe { std::mem::transmute(particles) };
let mut rng = rand::thread_rng();
let mut buffer: Vec<u32> = vec![0; WIDTH * HEIGHT];
let mut window =
Window::new("Safu", WIDTH, HEIGHT, WindowOptions::default()).unwrap_or_else(|e| {
panic!("{}", e);
});
window.set_target_fps(120);
let mut i: usize = 0;
while i < PARTICLE_COUNT {
let idx = i * 4;
particles[idx] = rng.gen_range(0.0..WIDTH as f32);
particles[idx + 1] = rng.gen_range(0.0..HEIGHT as f32);
particles[idx + 2] = rng.gen_range(-1.0..1.0);
particles[idx + 3] = rng.gen_range(-1.0..1.0);
i += 1;
}
let mut mouse_pos = (WIDTH as f32 / 2.0, HEIGHT as f32 / 2.0);
let mut mouse_pressed = false;
let friction_per_second = 0.99_f32.powf(60.0);
let mut frame_count = 0;
let mut last_fps_time = Instant::now();
let mut last_frame_time = Instant::now();
let mut simulation_times = vec![];
let mut drawing_times = vec![];
let mut fps = 0;
while window.is_open() && !window.is_key_down(Key::Escape) {
let now = Instant::now();
let delta_time = now.duration_since(last_frame_time).as_secs_f32();
last_frame_time = now;
let sim_start = Instant::now();
if let Some((mx, my)) = window.get_mouse_pos(minifb::MouseMode::Clamp) {
mouse_pos = (mx, my);
}
mouse_pressed = window.get_mouse_down(MouseButton::Left);
particles.par_chunks_mut(4).with_min_len(10000).for_each(|particle| {
if mouse_pressed {
let dx = mouse_pos.0 - particle[0];
let dy = mouse_pos.1 - particle[1];
let distance = (dx * dx + dy * dy).sqrt();
if distance > 1.0 {
let inv_gravity = GRAVITY_STRENGTH / distance;
particle[2] += dx * inv_gravity * 8.0 * delta_time;
particle[3] += dy * inv_gravity * 8.0 * delta_time;
}
}
let friction = friction_per_second.powf(delta_time);
particle[2] *= friction;
particle[3] *= friction;
particle[0] += particle[2] * delta_time;
particle[1] += particle[3] * delta_time;
});
simulation_times.push(sim_start.elapsed().as_secs_f32());
if simulation_times.len() > MAX_PERF_SAMPLE_FRAMES {
simulation_times.remove(0);
}
let draw_start = Instant::now();
buffer.iter_mut().for_each(|pixel| *pixel = 0);
for particle in particles.chunks_mut(4) {
let x = particle[0] as usize;
let y = particle[1] as usize;
if x >= WIDTH || y >= HEIGHT || x <= 0 || y <= 0 {
continue;
}
let red = (particle[0] as f32 / WIDTH as f32 * 255.0 * 0.8) as u8;
let green = (particle[1] as f32 / HEIGHT as f32 * 255.0 * 0.8) as u8;
let blue = (255.0 * 0.5) as u8;
let buffer_index = y * WIDTH + x;
let existing_color = buffer[buffer_index];
let existing_red = ((existing_color >> 16) & 0xFF) as u8;
let existing_green = ((existing_color >> 8) & 0xFF) as u8;
let existing_blue = (existing_color & 0xFF) as u8;
let combined_red = existing_red.saturating_add(red);
let combined_green = existing_green.saturating_add(green);
let combined_blue = existing_blue.saturating_add(blue);
let combined_color = ((combined_red as u32) << 16) | ((combined_green as u32) << 8) | (combined_blue as u32);
buffer[buffer_index] = combined_color;
}
window.update_with_buffer(&buffer, WIDTH, HEIGHT).unwrap();
drawing_times.push(draw_start.elapsed().as_secs_f32());
if drawing_times.len() > MAX_PERF_SAMPLE_FRAMES {
drawing_times.remove(0);
}
frame_count += 1;
if now.duration_since(last_fps_time).as_secs_f32() >= 1.0 {
fps = frame_count;
frame_count = 0;
last_fps_time = now;
let avg_simulation_time: f32 =
simulation_times.iter().sum::<f32>() / simulation_times.len() as f32;
let avg_drawing_time: f32 =
drawing_times.iter().sum::<f32>() / drawing_times.len() as f32;
println!("FPS: {}", fps);
println!("Avg Simulation Time: {:.5}", avg_simulation_time * 1000.0);
println!("Avg Drawing Time: {:.5}", avg_drawing_time * 1000.0);
}
}
}
use minifb::{Key, MouseButton, Window, WindowOptions};
use rand::Rng;
use std::sync::mpsc;
use std::thread;
use std::time::Instant;
const WIDTH: usize = 1200;
const HEIGHT: usize = 800;
const PARTICLE_COUNT: usize = 20_000_000;
const THREAD_COUNT: usize = 8;
const PARTICLE_STRIDE: usize = 4;
const GRAVITY_STRENGTH: f32 = 200.5;
const MAX_PERF_SAMPLE_FRAMES: usize = 100;
fn main() {
let mut particles: Box<[f32]> = vec![0.0; PARTICLE_COUNT * PARTICLE_STRIDE].into_boxed_slice();
let mut rng = rand::thread_rng();
let mut buffer: Vec<u32> = vec![0; WIDTH * HEIGHT];
let mut p_count_buffer = vec![0 as u8; WIDTH * HEIGHT];
let mut window = Window::new("Safu", WIDTH, HEIGHT, WindowOptions::default()).unwrap_or_else(|e| {
panic!("{}", e);
});
window.set_target_fps(120);
let mut i: usize = 0;
while i < PARTICLE_COUNT {
let idx = i * PARTICLE_STRIDE;
particles[idx] = rng.gen_range(0.0..WIDTH as f32);
particles[idx + 1] = rng.gen_range(0.0..HEIGHT as f32);
particles[idx + 2] = rng.gen_range(-1.0..1.0);
particles[idx + 3] = rng.gen_range(-1.0..1.0);
i += 1;
}
let mut mouse_pos = (WIDTH as f32 / 2.0, HEIGHT as f32 / 2.0);
let mut mouse_pressed = false;
let friction_per_second = 0.99_f32.powf(60.0);
let mut frame_count = 0;
let mut last_fps_time = Instant::now();
let mut last_frame_time = Instant::now();
let mut simulation_times = vec![];
let mut drawing_times = vec![];
let mut fps = 0;
while window.is_open() && !window.is_key_down(Key::Escape) {
let now = Instant::now();
let delta_time = now.duration_since(last_frame_time).as_secs_f32();
last_frame_time = now;
let sim_start = Instant::now();
if let Some((mx, my)) = window.get_mouse_pos(minifb::MouseMode::Clamp) {
mouse_pos = (mx, my);
}
mouse_pressed = window.get_mouse_down(MouseButton::Left);
let (tx, rx) = mpsc::channel();
let chunk_size = PARTICLE_COUNT / THREAD_COUNT;
let particles_ptr = particles.as_mut_ptr();
for t in 0..THREAD_COUNT {
let tx = tx.clone();
let mouse_pos = mouse_pos;
let mut buffer = vec![0 as u8; WIDTH * HEIGHT];
let start = t * chunk_size * PARTICLE_STRIDE;
let end = if t == THREAD_COUNT - 1 {
PARTICLE_COUNT * PARTICLE_STRIDE
} else {
(t + 1) * chunk_size * PARTICLE_STRIDE
};
unsafe {
let particles_chunk = std::slice::from_raw_parts_mut(particles_ptr.add(start), end - start);
thread::spawn(move || {
for particle in particles_chunk.chunks_mut(PARTICLE_STRIDE) {
if mouse_pressed {
let dx = mouse_pos.0 - particle[0];
let dy = mouse_pos.1 - particle[1];
let distance = dx * dx + dy * dy;
if distance > 2.0 {
let inv_gravity = GRAVITY_STRENGTH / distance.sqrt();
particle[2] += dx * inv_gravity * 8.0 * delta_time;
particle[3] += dy * inv_gravity * 8.0 * delta_time;
}
}
let friction = friction_per_second.powf(delta_time);
particle[2] *= friction;
particle[3] *= friction;
particle[0] += particle[2] * delta_time;
particle[1] += particle[3] * delta_time;
let x = particle[0] as usize;
let y = particle[1] as usize;
if x >= WIDTH || y >= HEIGHT || x <= 0 || y <= 0 {
continue;
}
let buffer_index = y * WIDTH + x;
let p_count = buffer[buffer_index];
buffer[buffer_index] = p_count.saturating_add(1);
}
tx.send(buffer).unwrap();
});
}
}
drop(tx);
p_count_buffer.iter_mut().for_each(|c| *c = 0);
for local_buffer in rx {
for (i, &count) in local_buffer.iter().enumerate() {
p_count_buffer[i] = p_count_buffer[i].saturating_add(count);
}
}
simulation_times.push(sim_start.elapsed().as_secs_f32());
if simulation_times.len() > MAX_PERF_SAMPLE_FRAMES {
simulation_times.remove(0);
}
let draw_start = Instant::now();
buffer.iter_mut().for_each(|pixel| *pixel = 0);
for (i, &count) in p_count_buffer.iter().enumerate() {
let x = i % WIDTH;
let y = i / WIDTH;
let r = (x as f32 / WIDTH as f32 * 12.0 * count as f32) as u8;
let g = (y as f32 / HEIGHT as f32 * 12.0 * count as f32) as u8;
let b = (10.0 * count as f32) as u8;
buffer[i] = ((r as u32) << 16) | ((g as u32) << 8) | (b as u32);
}
window.update_with_buffer(&buffer, WIDTH, HEIGHT).unwrap();
drawing_times.push(draw_start.elapsed().as_secs_f32());
if drawing_times.len() > MAX_PERF_SAMPLE_FRAMES {
drawing_times.remove(0);
}
frame_count += 1;
if now.duration_since(last_fps_time).as_secs_f32() >= 1.0 {
fps = frame_count;
frame_count = 0;
last_fps_time = now;
let avg_simulation_time: f32 =
simulation_times.iter().sum::<f32>() / simulation_times.len() as f32;
let avg_drawing_time: f32 =
drawing_times.iter().sum::<f32>() / drawing_times.len() as f32;
println!("FPS: {}", fps);
println!("Avg Simulation Time: {:.5}", avg_simulation_time * 1000.0);
println!("Avg Drawing Time: {:.5}", avg_drawing_time * 1000.0);
}
}
}
use how_fast_is_rust::thread_pool::ThreadPool;
use minifb::{Key, MouseButton, Window, WindowOptions};
use rand::Rng;
use std::time::Instant;
const WIDTH: usize = 1200;
const HEIGHT: usize = 800;
const PARTICLE_COUNT: usize = 10_000_000;
const THREAD_COUNT: usize = 8;
const PARTICLE_STRIDE: usize = 2;
const GRAVITY_STRENGTH: f32 = 200.5;
const MAX_PERF_SAMPLE_FRAMES: usize = 100;
fn main() {
let mut shared_pos_buffer = vec![0.0; PARTICLE_COUNT * PARTICLE_STRIDE].into_boxed_slice();
let mut shared_vel_buffer = vec![0.0; PARTICLE_COUNT * PARTICLE_STRIDE].into_boxed_slice();
let mut shared_p_count_buffer = vec![0 as u8; WIDTH * HEIGHT * THREAD_COUNT].into_boxed_slice();
let mut rng = rand::thread_rng();
let mut frame_buffer: Vec<u32> = vec![0; WIDTH * HEIGHT];
let mut p_count_buffer = vec![0 as u8; WIDTH * HEIGHT];
let mut window = Window::new("Safu", WIDTH, HEIGHT, WindowOptions::default()).unwrap_or_else(|e| {
panic!("{}", e);
});
window.set_target_fps(120);
let mut i: usize = 0;
while i < PARTICLE_COUNT {
let idx = i * PARTICLE_STRIDE;
shared_pos_buffer[idx] = rng.gen_range(0.0..WIDTH as f32);
shared_pos_buffer[idx + 1] = rng.gen_range(0.0..HEIGHT as f32);
shared_vel_buffer[idx] = rng.gen_range(-2.0..2.0);
shared_vel_buffer[idx + 1] = rng.gen_range(-2.0..2.0);
i += 1;
}
let mut mouse_pos = (WIDTH as f32 / 2.0, HEIGHT as f32 / 2.0);
let mut mouse_pressed = false;
let friction_per_second = 0.99_f32.powf(60.0);
let mut frame_count = 0;
let mut last_fps_time = Instant::now();
let mut last_frame_time = Instant::now();
let mut simulation_times = vec![];
let mut drawing_times = vec![];
let mut fps = 0;
let pool = ThreadPool::new(THREAD_COUNT);
let chunk_size = PARTICLE_COUNT / THREAD_COUNT;
let shared_pos_buff_ptr = shared_pos_buffer.as_mut_ptr();
let shared_vel_buff_ptr = shared_vel_buffer.as_mut_ptr();
let shared_p_count_buff_ptr = shared_p_count_buffer.as_mut_ptr();
while window.is_open() && !window.is_key_down(Key::Escape) {
let now = Instant::now();
let delta_time = now.duration_since(last_frame_time).as_secs_f32();
last_frame_time = now;
let sim_start = Instant::now();
if let Some((mx, my)) = window.get_mouse_pos(minifb::MouseMode::Clamp) {
mouse_pos = (mx, my);
}
mouse_pressed = window.get_mouse_down(MouseButton::Left);
// clear p_counts
shared_p_count_buffer.iter_mut().for_each(|c| *c = 0);
for t in 0..THREAD_COUNT {
let mouse_pos = mouse_pos;
let p_data_start = t * chunk_size * PARTICLE_STRIDE;
let p_data_end = if t == THREAD_COUNT - 1 {
PARTICLE_COUNT * PARTICLE_STRIDE
} else {
(t + 1) * chunk_size * PARTICLE_STRIDE
};
let p_count_start = t * WIDTH * HEIGHT;
let p_count_end = if t == THREAD_COUNT - 1 {
WIDTH * HEIGHT
} else {
(t + 1) * WIDTH * HEIGHT
};
unsafe {
let pos_chunk = std::slice::from_raw_parts_mut(shared_pos_buff_ptr.add(p_data_start), p_data_end - p_data_start);
let vel_chunk = std::slice::from_raw_parts_mut(shared_vel_buff_ptr.add(p_data_start), p_data_end - p_data_start);
let p_count_chunk = std::slice::from_raw_parts_mut(shared_p_count_buff_ptr.add(p_count_start), p_count_end - p_count_start);
let friction = friction_per_second.powf(delta_time);
pool.execute(move || {
for (pos, vel) in pos_chunk.chunks_mut(2).zip(vel_chunk.chunks_mut(2)) {
if mouse_pressed {
let dx = mouse_pos.0 - pos[0];
let dy = mouse_pos.1 - pos[1];
let distance = (dx * dx + dy * dy).sqrt();
if distance > 2.0 {
let inv_gravity = GRAVITY_STRENGTH / distance;
vel[0] += dx * inv_gravity * 8.0 * delta_time;
vel[1] += dy * inv_gravity * 8.0 * delta_time;
}
}
vel[0] *= friction;
vel[1] *= friction;
pos[0] += vel[0] * delta_time;
pos[1] += vel[1] * delta_time;
let x = pos[0] as usize;
let y = pos[1] as usize;
if x >= WIDTH || y >= HEIGHT || x <= 0 || y <= 0 {
continue;
}
let buffer_index = y * WIDTH + x;
let p_count = p_count_chunk[buffer_index];
p_count_chunk[buffer_index] = p_count.saturating_add(1);
}
});
}
}
pool.wait_for_completion();
simulation_times.push(sim_start.elapsed().as_secs_f32());
if simulation_times.len() > MAX_PERF_SAMPLE_FRAMES {
simulation_times.remove(0);
}
let draw_start = Instant::now();
p_count_buffer.iter_mut().for_each(|c| *c = 0);
for counts in shared_p_count_buffer.chunks_mut(WIDTH*HEIGHT) {
for (i, &count) in counts.iter().enumerate() {
p_count_buffer[i] = p_count_buffer[i].saturating_add(count);
}
}
frame_buffer.iter_mut().for_each(|pixel| *pixel = 0);
for (i, &count) in p_count_buffer.iter().enumerate() {
let x = i % WIDTH;
let y = i / WIDTH;
let r = (x as f32 / WIDTH as f32 * 55.0 * count.clamp(0, 4) as f32) as u8;
let g = (y as f32 / HEIGHT as f32 * 55.0 * count.clamp(0, 4) as f32) as u8;
let b = (55.0 * count.clamp(0, 4) as f32) as u8;
frame_buffer[i] = ((r as u32) << 16) | ((g as u32) << 8) | (b as u32);
}
window.update_with_buffer(&frame_buffer, WIDTH, HEIGHT).unwrap();
drawing_times.push(draw_start.elapsed().as_secs_f32());
if drawing_times.len() > MAX_PERF_SAMPLE_FRAMES {
drawing_times.remove(0);
}
frame_count += 1;
if now.duration_since(last_fps_time).as_secs_f32() >= 1.0 {
fps = frame_count;
frame_count = 0;
last_fps_time = now;
let avg_simulation_time: f32 =
simulation_times.iter().sum::<f32>() / simulation_times.len() as f32;
let avg_drawing_time: f32 =
drawing_times.iter().sum::<f32>() / drawing_times.len() as f32;
println!("FPS: {}", fps);
println!("Avg Simulation Time: {:.5}", avg_simulation_time * 1000.0);
println!("Avg Drawing Time: {:.5}", avg_drawing_time * 1000.0);
}
}
}
#![feature(portable_simd)]
use std::simd::{cmp::SimdPartialOrd, f32x8, simd_swizzle, StdFloat};
use how_fast_is_rust::thread_pool::ThreadPool;
use minifb::{Key, MouseButton, Window, WindowOptions};
use rand::Rng;
use std::time::Instant;
const WIDTH: usize = 1200;
const HEIGHT: usize = 800;
const PARTICLE_COUNT: usize = 100_000_000;
const THREAD_COUNT: usize = 8;
const PARTICLE_STRIDE: usize = 2;
const GRAVITY_STRENGTH: f32 = 200.5 * 7.0;
const MAX_PERF_SAMPLE_FRAMES: usize = 10;
fn main() {
let mut shared_pos_buffer = vec![0.0; PARTICLE_COUNT * PARTICLE_STRIDE].into_boxed_slice();
let mut shared_vel_buffer = vec![0.0; PARTICLE_COUNT * PARTICLE_STRIDE].into_boxed_slice();
let mut shared_p_count_buffer = vec![0 as u8; WIDTH * HEIGHT * THREAD_COUNT].into_boxed_slice();
let mut rng = rand::thread_rng();
let mut frame_buffer: Vec<u32> = vec![0; WIDTH * HEIGHT];
let mut p_count_buffer = vec![0 as u8; WIDTH * HEIGHT];
let mut window = Window::new("Safu", WIDTH, HEIGHT, WindowOptions::default()).unwrap_or_else(|e| {
panic!("{}", e);
});
window.set_target_fps(120);
let mut i: usize = 0;
while i < PARTICLE_COUNT {
let idx = i * PARTICLE_STRIDE;
shared_pos_buffer[idx] = rng.gen_range(0.0..WIDTH as f32);
shared_pos_buffer[idx + 1] = rng.gen_range(0.0..HEIGHT as f32);
shared_vel_buffer[idx] = rng.gen_range(-2.0..2.0);
shared_vel_buffer[idx + 1] = rng.gen_range(-2.0..2.0);
i += 1;
}
let mut mouse_pos = (WIDTH as f32 / 2.0, HEIGHT as f32 / 2.0);
let mut mouse_pressed = false;
let friction_per_second = 0.99_f32.powf(60.0);
let mut frame_count = 0;
let mut last_fps_time = Instant::now();
let mut last_frame_time = Instant::now();
let mut simulation_times = vec![];
let mut drawing_times = vec![];
let mut fps = 0;
let pool = ThreadPool::new(THREAD_COUNT);
let chunk_size = PARTICLE_COUNT / THREAD_COUNT;
let shared_pos_buff_ptr = shared_pos_buffer.as_mut_ptr();
let shared_vel_buff_ptr = shared_vel_buffer.as_mut_ptr();
let shared_p_count_buff_ptr = shared_p_count_buffer.as_mut_ptr();
while window.is_open() && !window.is_key_down(Key::Escape) {
let now = Instant::now();
let delta_time = now.duration_since(last_frame_time).as_secs_f32();
last_frame_time = now;
let sim_start = Instant::now();
if let Some((mx, my)) = window.get_mouse_pos(minifb::MouseMode::Clamp) {
mouse_pos = (mx, my);
}
mouse_pressed = window.get_mouse_down(MouseButton::Left);
// clear p_counts
shared_p_count_buffer.iter_mut().for_each(|c| *c = 0);
for t in 0..THREAD_COUNT {
let mouse_pos = mouse_pos;
let p_data_start = t * chunk_size * PARTICLE_STRIDE;
let p_data_end = if t == THREAD_COUNT - 1 {
PARTICLE_COUNT * PARTICLE_STRIDE
} else {
(t + 1) * chunk_size * PARTICLE_STRIDE
};
let p_count_start = t * WIDTH * HEIGHT;
let p_count_end = if t == THREAD_COUNT - 1 {
WIDTH * HEIGHT
} else {
(t + 1) * WIDTH * HEIGHT
};
unsafe {
let pos_chunk = std::slice::from_raw_parts_mut(shared_pos_buff_ptr.add(p_data_start), p_data_end - p_data_start);
let vel_chunk = std::slice::from_raw_parts_mut(shared_vel_buff_ptr.add(p_data_start), p_data_end - p_data_start);
let p_count_chunk = std::slice::from_raw_parts_mut(shared_p_count_buff_ptr.add(p_count_start), p_count_end - p_count_start);
let friction = friction_per_second.powf(delta_time);
pool.execute(move || {
let gravity_vec = f32x8::splat(GRAVITY_STRENGTH);
let delta_time_vec = f32x8::splat(delta_time);
let friction_vec = f32x8::splat(friction);
let mouse_pos_vec = f32x8::from_array([mouse_pos.0, mouse_pos.1, mouse_pos.0, mouse_pos.1, mouse_pos.0, mouse_pos.1, mouse_pos.0, mouse_pos.1]);
let threshold_vec = f32x8::splat(2.0);
for (pos, vel) in pos_chunk.chunks_exact_mut(8).zip(vel_chunk.chunks_exact_mut(8)) {
let mut pos_vec = f32x8::from_slice(pos);
let mut vel_vec = f32x8::from_slice(vel);
if mouse_pressed {
let delta = mouse_pos_vec - pos_vec;
let distance_sq = delta * delta;
let distance_vec: f32x8 = (simd_swizzle!(distance_sq, [1, 0, 3, 2, 5, 4, 7, 6]) + distance_sq).sqrt();
let mask = distance_vec.simd_gt(threshold_vec);
let inv_gravity = gravity_vec / distance_vec;
let force = delta * inv_gravity * delta_time_vec;
vel_vec = mask.select(vel_vec + force, vel_vec);
}
vel_vec *= friction_vec;
pos_vec += vel_vec * delta_time_vec;
pos.copy_from_slice(pos_vec.as_array());
vel.copy_from_slice(vel_vec.as_array());
for j in (0..8).step_by(2) {
let x = pos[j] as usize;
let y = pos[j + 1] as usize;
let buffer_index = y.max(0).min(HEIGHT-1) * WIDTH + x.max(0).min(WIDTH-1);
let p_count = p_count_chunk.get_unchecked(buffer_index);
p_count_chunk[buffer_index] = p_count.saturating_add(1);
}
}
});
}
}
pool.wait_for_completion();
simulation_times.push(sim_start.elapsed().as_secs_f32());
if simulation_times.len() > MAX_PERF_SAMPLE_FRAMES {
simulation_times.remove(0);
}
let draw_start = Instant::now();
p_count_buffer.iter_mut().for_each(|c| *c = 0);
for counts in shared_p_count_buffer.chunks_mut(WIDTH*HEIGHT) {
for (i, &count) in counts.iter().enumerate() {
p_count_buffer[i] = p_count_buffer[i].saturating_add(count);
}
}
frame_buffer.iter_mut().for_each(|pixel| *pixel = 0);
for (i, &count) in p_count_buffer.iter().enumerate() {
let x = i % WIDTH;
let y = i / WIDTH;
let r = (x as f32 / WIDTH as f32 * 10.0 * count.clamp(0, 30) as f32) as u8;
let g = (y as f32 / HEIGHT as f32 * 10.0 * count.clamp(0, 30) as f32) as u8;
let b = (10.0 * count.clamp(0, 30) as f32) as u8;
frame_buffer[i] = ((r as u32) << 16) | ((g as u32) << 8) | (b as u32);
}
window.update_with_buffer(&frame_buffer, WIDTH, HEIGHT).unwrap();
drawing_times.push(draw_start.elapsed().as_secs_f32());
if drawing_times.len() > MAX_PERF_SAMPLE_FRAMES {
drawing_times.remove(0);
}
frame_count += 1;
if now.duration_since(last_fps_time).as_secs_f32() >= 1.0 {
fps = frame_count;
frame_count = 0;
last_fps_time = now;
let avg_simulation_time: f32 =
simulation_times.iter().sum::<f32>() / simulation_times.len() as f32;
let avg_drawing_time: f32 =
drawing_times.iter().sum::<f32>() / drawing_times.len() as f32;
println!("FPS: {}", fps);
println!("Avg Simulation Time: {:.5}", avg_simulation_time * 1000.0);
println!("Avg Drawing Time: {:.5}", avg_drawing_time * 1000.0);
}
}
}
use minifb::{Key, MouseButton, Window, WindowOptions};
use rand::Rng;
use std::time::{Instant};
use std::f32::consts::PI;
const WIDTH: usize = 600;
const HEIGHT: usize = 400;
const PARTICLE_COUNT: usize = 80_000;
const GRAVITY_STRENGTH: f32 = 105.0;
#[repr(align(16))]
struct Particle {
x: f32,
y: f32,
angle: f32,
speed: f32,
_type: u8,
}
fn main() {
let width = WIDTH as f32;
let height = HEIGHT as f32;
let spawn_width = 100;
let spawn_height = 100;
let mut rng = rand::thread_rng();
let mut particles = Vec::with_capacity(PARTICLE_COUNT);
for _ in 0..PARTICLE_COUNT {
particles.push(Particle {
x: rng.gen_range((WIDTH / 2 - spawn_width / 2) as f32..(WIDTH/2 + spawn_width/2) as f32),
y: rng.gen_range((HEIGHT / 2 - spawn_height / 2) as f32..(HEIGHT/2 + spawn_height/2) as f32),
angle: rng.gen_range(0.0..360.0) * PI / 180.0,
speed: 80.0 + rng.gen_range(0.0..10.0),
_type: rng.gen_range(0..40),
});
}
let mut frame_buff: Vec<u32> = vec![0; WIDTH * HEIGHT];
let mut trail_buff: Vec<f32> = vec![0.0; WIDTH * HEIGHT];
let mut window = Window::new("Safu", WIDTH, HEIGHT, WindowOptions::default())
.unwrap_or_else(|e| {
panic!("{}", e);
});
let mut mouse_pos = (WIDTH as f32 / 2.0, HEIGHT as f32 / 2.0);
let mut mouse_pressed = false;
let deposition: f32 = 3.0;
let sense_dist: f32 = 18.0;
let sense_angle: f32 = PI / 180.0 * 18.0;
let rotate_angle_dist: f32 = PI / 180.0 * 12.0;
let rotation_speed: f32 = 4.0;
let sensor_size: i32 = 1;
let diffusion_rate: f32 = 24.0;
let blur_size = 1;
let mut last_frame_time = Instant::now();
while window.is_open() && !window.is_key_down(Key::Escape) {
let now = Instant::now();
let delta_time = now.duration_since(last_frame_time).as_secs_f32();
last_frame_time = now;
if let Some((mx, my)) = window.get_mouse_pos(minifb::MouseMode::Clamp) {
mouse_pos = (mx, my);
}
mouse_pressed = window.get_mouse_down(MouseButton::Left);
for p in particles.iter_mut() {
let dt = delta_time;
let mut nx = p.x + p.speed * p.angle.cos() * dt;
let mut ny = p.y + p.speed * p.angle.sin() * dt;
if mouse_pressed {
let dx = mouse_pos.0 - nx;
let dy = mouse_pos.1 - ny;
let dist = (dx*dx + dy*dy).sqrt();
if dist < 110.0 {
nx -= dx * GRAVITY_STRENGTH/dist *dt * 1.5;
ny -= dy * GRAVITY_STRENGTH/dist *dt * 1.5;
p.angle *= -1.0;
}
nx += dx * GRAVITY_STRENGTH/dist *dt * 0.25;
ny += dy * GRAVITY_STRENGTH/dist *dt * 0.25;
}
if nx <= 0.0 || nx >= width || ny <= 0.0 || ny >= height {
p.angle = rng.gen_range(-1.0..1.0) * 2.0 * PI;
continue;
}
p.x = nx;
p.y = ny;
if p._type <= 2 {
trail_buff[p.y as usize * WIDTH + p.x as usize] -= deposition * dt;
} else if p._type == 9 {
trail_buff[p.y as usize * WIDTH + p.x as usize] -= deposition * dt * 2.30;
} else {
trail_buff[p.y as usize * WIDTH + p.x as usize] += deposition * dt;
}
let front = sense(&p, 0.0, sense_dist, sensor_size, &trail_buff);
let left = sense(&p, -sense_angle, sense_dist, sensor_size, &trail_buff);
let right = sense(&p, sense_angle, sense_dist, sensor_size, &trail_buff);
if front > left && front > right {
continue;
}
if front < left && front < right {
if rng.gen_range(0.0..1.0) > 0.5 {
p.angle += rotate_angle_dist * rotation_speed * dt;
} else {
p.angle -= rotate_angle_dist * rotation_speed * dt;
}
continue;
}
if left < right {
p.angle += rotate_angle_dist * rotation_speed * dt;
} else {
p.angle -= rotate_angle_dist * rotation_speed * dt;
}
}
diffuse(&mut trail_buff, diffusion_rate, delta_time, blur_size);
frame_buff.iter_mut().for_each(|pixel| *pixel = 0);
for y in 0..HEIGHT {
for x in 0..WIDTH {
let index = y * WIDTH + x;
let trail_intensity = trail_buff[index];
let intensity_byte = (trail_intensity * 255.0).min(255.0).max(0.0) as f32;
let sx = x as f32 / width;
let sy = y as f32 / height;
let r = (intensity_byte*sy) as u32;
let g = (intensity_byte*sx) as u32;
let b = (intensity_byte*0.5) as u32;
let packed_pixel = (r << 16) | (g << 8) | b;
frame_buff[index] = packed_pixel;
}
}
for particle in particles.iter() {
let x = particle.x as usize;
let y = particle.y as usize;
if x < WIDTH && y < HEIGHT && x > 0 && y > 0 {
let red = (particle.y / height * 255.0 * 0.1) as u32;
let green = (particle.x / width * 255.0 * 0.1) as u32;
let blue = (255.0 * 0.1) as u32;
let current_color = frame_buff[y * WIDTH + x];
let current_red = (current_color >> 16) & 0xFF;
let current_green = (current_color >> 8) & 0xFF;
let current_blue = current_color & 0xFF;
let blended_red = (current_red + red).min(255);
let blended_green = (current_green + green).min(255);
let blended_blue = (current_blue + blue).min(255);
let blended_color = (blended_red << 16) | (blended_green << 8) | blended_blue;
frame_buff[y * WIDTH + x] = blended_color;
}
}
window.update_with_buffer(&frame_buff, WIDTH, HEIGHT).unwrap();
}
}
#[inline]
fn sense(p: &Particle, angle_offset: f32, sense_dist: f32, sense_size: i32, trail_map: &Vec<f32>) -> f32 {
let sensor_angle = p.angle + angle_offset;
let sense_x = p.x + sense_dist * sensor_angle.cos();
let sense_y = p.y + sense_dist * sensor_angle.sin();
let mut sum: f32 = 0.0;
for offset_x in -sense_size..sense_size {
for offset_y in -sense_size..sense_size {
let sample_x = (sense_x as i32 + offset_x).clamp(0, WIDTH as i32 - 1);
let sample_y = (sense_y as i32 + offset_y).clamp(0, HEIGHT as i32 - 1);
sum += trail_map[sample_y as usize * WIDTH + sample_x as usize]
}
}
sum
}
fn diffuse(trail_map: &mut Vec<f32>, diffusion_rate: f32, dt: f32, blur_size: isize) {
let mut new_map = trail_map.clone();
let df: f32 = dt * diffusion_rate;
for y in 0..HEIGHT {
for x in 0..WIDTH {
let index = y * WIDTH + x;
let current_value = trail_map[index];
let mut total_contribution = current_value;
let mut count = 1.0;
for dy in -blur_size..=blur_size {
for dx in -blur_size..=blur_size {
if dx == 0 && dy == 0 {
continue;
}
let nx = x as isize + dx;
let ny = y as isize + dy;
if nx >= 0 && nx < WIDTH as isize && ny >= 0 && ny < HEIGHT as isize {
let neighbor_index = ny as usize * WIDTH + nx as usize;
total_contribution += trail_map[neighbor_index];
count += 1.0;
}
}
}
let new_value = (current_value + (df * (total_contribution / count))) * (1.0-df);
new_map[index] = new_value;
}
}
for (i, val) in new_map.iter().enumerate() {
trail_map[i] = *val;
}
}
<!DOCTYPE html>
<html lang="en" style="user-select: none">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sabby Rust</title>
<link rel="icon" href="./favicon.ico" type="image/x-icon" />
<style>
body,
html,
div,
canvas {
-webkit-user-select: none;
-moz-user-select: none;
-webkit-touch-callout: none;
-ms-user-select: none;
user-select: none;
outline: none;
}
</style>
</head>
<body
style="
margin: 0;
width: 100vw;
height: 100vh;
background: black;
-webkit-touch-callout: none;
overflow: hidden;
"
>
<div>
<canvas id="canvas"></canvas>
</div>
<script type="module">
import init, { InitInput, TickInput, init as initSimulation, tick } from './how_fast_is_rust.js';
async function runSimulation() {
const instance = await init();
const particle_count = 500_000;
const gravity = 205.5 * 8.0;
const canvas = document.getElementById('canvas');
canvas.style.touchAction = 'none';
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ctx = canvas.getContext('2d');
let imageData = ctx.createImageData(canvas.width, canvas.height);
let frameBuffer = new Uint32Array(imageData.data.buffer);
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
imageData = ctx.createImageData(canvas.width, canvas.height);
frameBuffer = new Uint32Array(imageData.data.buffer);
initSimulation(new InitInput(canvas.width, canvas.height, particle_count, gravity));
}
resize();
let mousePressed = false;
let mouseX = canvas.width / 2;
let mouseY = canvas.height / 2;
window.addEventListener('resize', resize);
window.addEventListener('mousedown', () => mousePressed = true);
window.addEventListener('mouseup', () => mousePressed = false);
window.addEventListener('mousemove', (event) => {
mouseX = event.offsetX;
mouseY = event.offsetY;
});
window.addEventListener('touchend', (event) => {
mousePressed = false;
});
window.addEventListener('touchmove', (event) => {
mousePressed = true;
mouseX = event.touches[0].clientX;
mouseY = event.touches[0].clientY;
});
window.addEventListener('touchstart', (event) => {
event.preventDefault();
mousePressed = true;
mouseX = event.touches[0].clientX;
mouseY = event.touches[0].clientY;
});
let prev = 0;
function render(timestamp = 0) {
const dt = (timestamp - prev)/1000;
prev = timestamp;
const frame_buff_ptr = tick(new TickInput(dt, mouseX, mouseY, mousePressed));
const buff = new Uint8Array(instance.memory.buffer, frame_buff_ptr, canvas.width * canvas.height*4);
for(let i = 0; i < frameBuffer.length;i++) {
const r = buff[i*4];
const g = buff[i*4+1];
const b = buff[i*4+2];
const a = buff[i*4+3];
frameBuffer[i] = (r << 24) | (g << 16) | (b << 8) | a;
}
ctx.putImageData(imageData, 0, 0);
ctx.drawImage(canvas, 0, 0);
requestAnimationFrame(render);
}
render();
}
runSimulation().catch(e => console.error("Could not run simulation:", e));
</script>
</body>
</html>
#![no_main]
use rand::Rng;
use wasm_bindgen::prelude::*;
#[repr(align(16))]
struct Particle {
x: f32,
y: f32,
// sx: f32,
// sy: f32,
dx: f32,
dy: f32,
}
static mut PARTICLES: Vec<Particle> = Vec::new();
static mut BUFFER: Vec<u32> = Vec::new();
static mut WIDTH: usize = 1;
static mut HEIGHT: usize = 1;
static mut GRAVITY_STRENGTH: f32 = 205.5 * 6.0;
#[wasm_bindgen]
pub struct TickInput {
pub dt: f32,
pub touch_x: f32,
pub touch_y: f32,
pub is_touch_down: bool,
}
#[wasm_bindgen]
impl TickInput {
#[wasm_bindgen(constructor)]
pub fn new(dt: f32, touch_x: f32, touch_y: f32, is_touch_down: bool) -> TickInput {
TickInput { dt, touch_x, touch_y, is_touch_down }
}
}
fn generate_particles(count: usize, width: usize, height: usize) -> Vec<Particle> {
let mut rng = rand::thread_rng();
let mut particles = Vec::with_capacity(count);
for _ in 0..count {
let x = rng.gen_range(0.0..width as f32);
let y = rng.gen_range(0.0..height as f32);
particles.push(Particle {
x,
y,
// sx: x,
// sy: y,
dx: rng.gen_range(-1.0..1.0),
dy: rng.gen_range(-1.0..1.0),
});
}
particles
}
#[wasm_bindgen]
pub struct InitInput {
pub width: usize,
pub height: usize,
pub particle_count: usize,
pub gravity: f32,
}
#[wasm_bindgen]
impl InitInput {
#[wasm_bindgen(constructor)]
pub fn new(width: usize, height: usize, particle_count: usize, gravity: f32) -> InitInput {
InitInput { width, height, particle_count, gravity }
}
}
#[wasm_bindgen]
pub fn init(input: InitInput) {
unsafe {
WIDTH = input.width;
HEIGHT = input.height;
if PARTICLES.len() != input.particle_count {
PARTICLES = generate_particles(input.particle_count, WIDTH, HEIGHT);
}
BUFFER = vec![0; WIDTH * HEIGHT];
}
}
#[wasm_bindgen]
pub fn tick(input: TickInput) -> *const u32 {
let friction_per_second = 0.985_f32.powf(60.0);
let dt = input.dt;
let touch_x = input.touch_x;
let touch_y = input.touch_y;
let is_touch_down = input.is_touch_down;
unsafe {
for particle in PARTICLES.iter_mut() {
if is_touch_down {
let dx = touch_x - particle.x;
let dy = touch_y - particle.y;
let distance = (dx * dx + dy * dy).sqrt();
if distance > 0.2 {
let inv_gravity = GRAVITY_STRENGTH / distance;
particle.dx += dx * inv_gravity * dt;
particle.dy += dy * inv_gravity * dt;
}
}
// let dx = particle.sx - particle.x;
// let dy = particle.sy - particle.y;
// let distance = (dx * dx + dy * dy).sqrt();
// if distance > 4.0 {
// let mut inv_gravity = 200.0 / distance;
// if distance > 16.0 {
// inv_gravity *= 2.5;
// }
// particle.dx += dx * inv_gravity * dt;
// particle.dy += dy * inv_gravity * dt;
// }
// if distance > 30.0 {
// let inv_gravity = 2_000.0 / distance;
// particle.dx += dx * inv_gravity * dt;
// particle.dy += dy * inv_gravity * dt;
// }
let friction = friction_per_second.powf(dt);
particle.dx *= friction;
particle.dy *= friction;
particle.x += particle.dx * dt;
particle.y += particle.dy * dt;
}
BUFFER.iter_mut().for_each(|pixel| *pixel = 0);
for particle in &PARTICLES {
let x = particle.x as usize;
let y = particle.y as usize;
let idx = y.max(0).min(HEIGHT-1) * WIDTH + x.max(0).min(WIDTH-1);
let old_c = BUFFER[idx];
let r = (old_c >> 24) & 0xFF;
let g = (old_c >> 16) & 0xFF;
let b = (old_c >> 8) & 0xFF;
let red = r + (particle.y / HEIGHT as f32 * 255.0 * 0.5) as u32;
let green = g + (particle.x / WIDTH as f32 * 255.0 * 0.5) as u32;
let blue = b + (255.0 * 0.8) as u32;
let color = (red.min(255) << 24) | (green.min(255) << 16) | (blue.min(255) << 8) | 255;
BUFFER[idx] = color;
}
BUFFER.as_ptr()
}
}
#![no_main]
use wasm_bindgen::prelude::*;
use rand::Rng;
use std::f32::consts::PI;
const GRAVITY_STRENGTH: f32 = 105.0;
#[repr(align(16))]
struct Particle {
x: f32,
y: f32,
angle: f32,
speed: f32,
_type: u8,
}
#[wasm_bindgen]
pub struct ParticleSimulator {
width: usize,
height: usize,
particles: Vec<Particle>,
frame_buff: Vec<u32>,
trail_buff: Vec<f32>,
rng: rand::rngs::ThreadRng,
options: SimulationOptions,
}
#[wasm_bindgen]
pub struct SimulationOptions {
particle_count: usize,
blur_size: isize,
deposition: f32,
sense_dist: f32,
sense_angle: f32,
rotate_angle_dist: f32,
rotation_speed: f32,
sensor_size: i32,
diffusion_rate: f32,
}
#[wasm_bindgen]
impl ParticleSimulator {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
utils::set_panic_hook();
let options = SimulationOptions {
particle_count: 80_000,
blur_size: 1,
deposition: 3.0,
sense_dist: 20.0,
sense_angle: PI / 180.0 * 15.0,
rotate_angle_dist: PI / 180.0 * 12.0,
rotation_speed: 4.0,
sensor_size: 1,
diffusion_rate: 24.0,
};
let rng = rand::thread_rng();
ParticleSimulator {
width: 0,
height: 0,
particles: Vec::with_capacity(options.particle_count),
frame_buff: vec![0; 1],
trail_buff: vec![0.0; 1],
rng,
options,
}
}
#[no_mangle]
pub fn init(&mut self, width: usize, height: usize, particle_count: usize, blur_size: isize, diffusion_rate: f32, deposition: f32 , sense_size: i32, speed: f32) {
self.width = width;
self.height = height;
self.options.particle_count = particle_count;
self.options.blur_size = blur_size;
self.options.sensor_size = sense_size;
self.options.diffusion_rate = diffusion_rate;
self.options.deposition = deposition;
self.particles.clear();
self.particles.reserve(particle_count);
self.frame_buff = vec![0; width * height];
self.trail_buff = vec![0.0; width * height];
let spawn_width = self.width/4;
let spawn_height = self.height/4;
for _ in 0..particle_count {
self.particles.push(Particle {
x: self.rng.gen_range((self.width / 2 - spawn_width / 2) as f32..(self.width/2 + spawn_width/2) as f32),
y: self.rng.gen_range((self.height / 2 - spawn_height / 2) as f32..(self.height/2 + spawn_height/2) as f32),
angle: self.rng.gen_range(0.0..360.0) * PI / 180.0,
speed: speed + self.rng.gen_range(0.0..10.0),
_type: self.rng.gen_range(0..80),
});
}
}
#[no_mangle]
pub fn simulate(&mut self, mouse_x: f32, mouse_y: f32, mouse_pressed: bool, delta_time: f64) -> *const u32 {
let dt = delta_time as f32;
for p in self.particles.iter_mut() {
let mut nx = p.x + p.speed * p.angle.cos() * dt;
let mut ny = p.y + p.speed * p.angle.sin() * dt;
if mouse_pressed {
let dx = mouse_x - nx;
let dy = mouse_y - ny;
let dist = (dx*dx + dy*dy).sqrt();
if dist < 80.0 {
nx -= dx * GRAVITY_STRENGTH/dist * dt * 1.5;
ny -= dy * GRAVITY_STRENGTH/dist * dt * 1.5;
p.angle *= -1.0;
}
nx += dx * GRAVITY_STRENGTH/dist * dt * 0.25;
ny += dy * GRAVITY_STRENGTH/dist * dt * 0.25;
}
if nx <= 0.0 || nx >= self.width as f32 || ny <= 0.0 || ny >= self.height as f32 {
p.angle = self.rng.gen_range(-1.0..1.0) * 2.0 * PI;
continue;
}
p.x = nx;
p.y = ny;
if p._type <= 3 {
self.trail_buff[p.y as usize * self.width + p.x as usize] -= self.options.deposition * dt;
} else if p._type == 4 {
self.trail_buff[p.y as usize * self.width + p.x as usize] -= self.options.deposition * dt * 1.30;
} else {
self.trail_buff[p.y as usize * self.width + p.x as usize] += self.options.deposition * dt;
}
let front = sense(p, &self.trail_buff, 0.0, self.options.sense_dist, self.options.sensor_size, self.width, self.height);
let left = sense(p, &self.trail_buff, -self.options.sense_angle, self.options.sense_dist, self.options.sensor_size, self.width, self.height);
let right = sense(p, &self.trail_buff, self.options.sense_angle, self.options.sense_dist, self.options.sensor_size, self.width, self.height);
// let front: f32 = 0.0;
// let left: f32 = 0.0;
// let right: f32 = 0.0;
if front > left && front > right {
continue;
}
if front < left && front < right {
if self.rng.gen_range(0.0..1.0) > 0.5 {
p.angle += self.options.rotate_angle_dist * self.options.rotation_speed * dt;
} else {
p.angle -= self.options.rotate_angle_dist * self.options.rotation_speed * dt;
}
continue;
}
if left < right {
p.angle += self.options.rotate_angle_dist * self.options.rotation_speed * dt;
} else {
p.angle -= self.options.rotate_angle_dist * self.options.rotation_speed * dt;
}
}
self.diffuse(self.options.diffusion_rate, dt, self.options.blur_size);
self.frame_buff.iter_mut().for_each(|pixel| *pixel = 0);
for y in 0..self.height {
for x in 0..self.width {
let index = y * self.width + x;
let trail_intensity = self.trail_buff[index];
let intensity_byte = (trail_intensity * 255.0).min(255.0).max(0.0) as f32;
let sx = x as f32 / self.width as f32;
let sy = y as f32 / self.height as f32;
let r = (intensity_byte*sx) as u32;
let g = (intensity_byte*sy) as u32;
let b = (intensity_byte*(1.0-sx)) as u32;
let packed_pixel = (r << 24) | (g << 16) | (b << 8 ) | 255;
self.frame_buff[index] = packed_pixel;
}
}
for particle in self.particles.iter() {
let x = particle.x as usize;
let y = particle.y as usize;
if x < self.width && y < self.height && x > 0 && y > 0 {
let sx = x as f32 / self.width as f32;
let sy = y as f32 / self.height as f32;
let red = (sx * 255.0 * 0.15) as u8;
let green = (sy * 255.0 * 0.15) as u8;
let blue = (255.0 * (1.0-sx) * 0.15) as u8;
let current_color = self.frame_buff[y * self.width + x];
let current_red = ((current_color >> 24) & 0xFFFFFF) as u8;
let current_green = ((current_color >> 16) & 0xFFFFFF) as u8;
let current_blue = ((current_color >> 8) & 0xFFFFFF) as u8;
let blended_red = current_red.saturating_add(red);
let blended_green = current_green.saturating_add(green);
let blended_blue = current_blue.saturating_add(blue);
let blended_color: u32 = ((blended_red as u32) << 24) | ((blended_green as u32) << 16) | ((blended_blue as u32) << 8 ) | 255;
self.frame_buff[y * self.width + x] = blended_color;
}
}
self.frame_buff.as_ptr()
}
fn diffuse(&mut self, diffusion_rate: f32, dt: f32, blur_size: isize) {
let mut new_map = self.trail_buff.clone();
let df: f32 = dt * diffusion_rate;
for y in 0..self.height {
for x in 0..self.width {
let index = y * self.width + x;
let current_value = self.trail_buff[index];
let mut total_contribution = current_value;
let mut count = 1.0;
for dy in -blur_size..=blur_size {
for dx in -blur_size..=blur_size {
if dx == 0 && dy == 0 {
continue;
}
let nx = x as isize + dx;
let ny = y as isize + dy;
if nx >= 0 && nx < self.width as isize && ny >= 0 && ny < self.height as isize {
let neighbor_index = ny as usize * self.width + nx as usize;
total_contribution += self.trail_buff[neighbor_index];
count += 1.0;
}
}
}
let new_value = (current_value + (df * (total_contribution / count))) * (1.0 - df);
new_map[index] = new_value;
}
}
for (i, val) in new_map.iter().enumerate() {
self.trail_buff[i] = *val;
}
}
}
mod utils {
pub fn set_panic_hook() {
// https://github.com/rustwasm/console_error_panic_hook#readme
#[cfg(feature = "console_error_panic_hook")]
{
console_error_panic_hook::set_once();
}
}
}
fn sense(p: &Particle, trail_buff: &Vec<f32>, angle_offset: f32, sense_dist: f32, sense_size: i32, width: usize, height: usize,) -> f32 {
let sensor_angle = p.angle + angle_offset;
let sense_x = p.x + sense_dist * sensor_angle.cos();
let sense_y = p.y + sense_dist * sensor_angle.sin();
let mut sum: f32 = 0.0;
for offset_x in -sense_size..sense_size {
for offset_y in -sense_size..sense_size {
let sample_x = (sense_x as i32 + offset_x).clamp(0, width as i32 - 1);
let sample_y = (sense_y as i32 + offset_y).clamp(0, height as i32 - 1);
sum += trail_buff[sample_y as usize * width + sample_x as usize];
}
}
sum
}
<!DOCTYPE html>
<html lang="en" style="user-select: none">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sabby Rust</title>
<link rel="icon" href="./favicon.ico" type="image/x-icon" />
<style>
body,
html,
div,
canvas {
-webkit-user-select: none;
-moz-user-select: none;
-webkit-touch-callout: none;
-ms-user-select: none;
user-select: none;
outline: none;
}
</style>
</head>
<body
style="
margin: 0;
width: 100vw;
height: 100vh;
background: black;
-webkit-touch-callout: none;
overflow: hidden;
"
>
<div>
<canvas id="canvas"></canvas>
</div>
<script type="module">
import init, { ParticleSimulator } from './how_fast_is_rust.js';
async function runSimulation() {
const instance = await init();
const canvas = document.getElementById('canvas');
canvas.style.touchAction = 'none';
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ctx = canvas.getContext('2d');
let imageData = ctx.createImageData(canvas.width, canvas.height);
let frameBuffer = new Uint32Array(imageData.data.buffer);
const simulator = new ParticleSimulator();
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
imageData = ctx.createImageData(canvas.width, canvas.height);
frameBuffer = new Uint32Array(imageData.data.buffer);
simulator.init(canvas.width, canvas.height, 50000, 0, 14, 5.5, 1, 80);
}
resize();
let mousePressed = false;
let mouseX = canvas.width / 2;
let mouseY = canvas.height / 2;
window.addEventListener('resize', resize);
window.addEventListener('mousedown', () => mousePressed = true);
window.addEventListener('mouseup', () => mousePressed = false);
window.addEventListener('mousemove', (event) => {
mouseX = event.offsetX;
mouseY = event.offsetY;
});
window.addEventListener('touchend', (event) => {
mousePressed = false;
});
window.addEventListener('touchmove', (event) => {
mousePressed = true;
mouseX = event.touches[0].clientX;
mouseY = event.touches[0].clientY;
});
window.addEventListener('touchstart', (event) => {
event.preventDefault();
mousePressed = true;
mouseX = event.touches[0].clientX;
mouseY = event.touches[0].clientY;
});
let prev = 0;
function render(timestamp = 0) {
const dt = (timestamp - prev)/1000;
prev = timestamp;
const frame_buff_ptr = simulator.simulate(mouseX, mouseY, mousePressed, 0.016);
const buff = new Uint8Array(instance.memory.buffer, frame_buff_ptr, canvas.width * canvas.height*4);
for(let i = 0; i < frameBuffer.length;i++) {
const r = buff[i*4];
const g = buff[i*4+1];
const b = buff[i*4+2];
const a = buff[i*4+3];
frameBuffer[i] = (r << 24) | (g << 16) | (b << 8) | a;
}
ctx.putImageData(imageData, 0, 0);
// ctx.scale(2,2);
ctx.drawImage(canvas, 0, 0);
requestAnimationFrame(render);
}
render();
}
runSimulation().catch(e => console.error("Could not run simulation:", e));
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment