I'm building a game engine / CPU 3D renderer to learn the basics of rendering following a course.
I'm trying to organize the code so that things are encapsulated and I can provide a public API that's easy to use.
For this, I want to provide this engine.on_update(|&mut engine| {}) method so users can pass in their own code and use the engine in there.
This callback is then executed as part of the engine's update loop.
Here is what this looks like now (I trimmed the code to make this easier as its already quite long)
This is the engine module.
use std::time::{Duration, Instant};
use sdl2::{
event::Event,
keyboard::Keycode,
pixels::PixelFormatEnum,
render::{Canvas, Texture, UpdateTextureError},
video::Window,
EventPump,
};
use crate::buffer::{ClearAuto, ClearColor, ColorBuffer, Drawable};
use crate::utils::NumOption;
pub struct EngineConfigParams {
pub window_title: Option<String>,
pub width: Option<usize>,
pub height: Option<usize>,
pub clear_color: Option<u32>,
pub fps: Option<u32>,
}
impl Default for EngineConfigParams {
fn default() -> Self {
EngineConfigParams {
window_title: None,
width: None,
height: None,
clear_color: None,
fps: None,
}
}
}
pub struct EngineConfig {
window_title: String,
width: usize,
height: usize,
clear_color: u32,
fps: u32,
}
impl EngineConfig {
pub fn new(params: EngineConfigParams) -> Self {
let default = EngineConfig::default();
EngineConfig {
window_title: params.window_title.unwrap_or(default.window_title),
width: params.width.unwrap_gt_or(0, default.width),
height: params.height.unwrap_gt_or(0, default.height),
clear_color: params.clear_color.unwrap_or(default.clear_color),
fps: params.fps.unwrap_gt_or(0, default.fps),
}
}
pub fn window_title(&self) -> &String {
&self.window_title
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
pub fn clear_color(&self) -> u32 {
self.clear_color
}
pub fn fps(&self) -> u32 {
self.fps
}
}
impl Default for EngineConfig {
fn default() -> Self {
EngineConfig {
window_title: "3D renderer".to_string(),
width: 800,
height: 600,
clear_color: 0xFF000000,
fps: 60,
}
}
}
type EngineUpdateFn<'a> = &'a mut dyn FnMut(&mut EngineCore);
pub struct Engine<'a> {
core: EngineCore,
update: Option<EngineUpdateFn<'a>>,
previous_frame_time: Instant,
target_frame_time: Duration,
}
impl<'a> Engine<'a> {
pub fn build(mut config: EngineConfig) -> Engine<'a> {
let ctx = sdl2::init().unwrap();
let video = ctx.video().unwrap();
match video.display_mode(0, 0) {
Ok(mode) => {
config.width = mode.w as usize;
config.height = mode.h as usize;
println!("Display mode: {:?}", mode);
}
Err(e) => eprintln!(
"Failed to get display mode: {}, using default width and height",
e
),
};
let width = config.width;
let height = config.height;
let window = video
.window(&config.window_title, width as u32, height as u32)
.borderless()
.position_centered()
.fullscreen()
.build()
.unwrap();
let canvas = window.into_canvas().build().unwrap();
let color_buffer = ColorBuffer::new(width, height);
let event_pump = ctx.event_pump().unwrap();
println!("WindowCtx w: {} h: {}", width, height);
let fps = config.fps;
Engine {
core: EngineCore {
config,
canvas,
color_buffer,
event_pump,
},
update: None,
previous_frame_time: Instant::now(),
target_frame_time: Duration::new(0, 1_000_000_000u32 / fps),
}
}
pub fn config(&self) -> &EngineConfig {
&self.core.config
}
pub fn on_update(&mut self, f: EngineUpdateFn<'a>) {
self.update = Some(f);
self.update();
}
pub fn user_update(&mut self) {
self.update.as_mut().unwrap()(&mut self.core);
}
pub fn update(&mut self) {
self.target_frame_time = Duration::new(0, 1_000_000_000u32 / self.core.config.fps);
let texture_creator = self.core.canvas.texture_creator();
let mut texture = texture_creator
.create_texture(
PixelFormatEnum::ARGB8888,
sdl2::render::TextureAccess::Streaming,
self.core.config.width as u32,
self.core.config.height as u32,
)
.unwrap();
let mut running = true;
while running {
self.previous_frame_time = Instant::now();
running = self.core.process_input();
self.user_update();
self.core.render_buffer(&mut texture).unwrap();
self.core.clear();
let now = Instant::now();
let frame_time = now - self.previous_frame_time;
println!(
"Time this frame {}ms {} FPS",
frame_time.as_millis(),
1000u128 / frame_time.as_millis()
);
if frame_time.as_nanos() < self.target_frame_time.as_nanos() {
::std::thread::sleep(self.target_frame_time - frame_time);
}
}
}
}
pub struct EngineCore {
config: EngineConfig,
canvas: Canvas<Window>,
color_buffer: ColorBuffer,
event_pump: EventPump,
}
impl EngineCore {
fn process_input(&mut self) -> bool {
for event in self.event_pump.poll_iter() {
match event {
Event::Quit { .. }
| Event::KeyDown {
keycode: Some(Keycode::Escape),
..
} => {
println!("Received quit event, shutting down");
return false;
}
_ => {}
}
}
true
}
fn render_buffer(&mut self, texture: &mut Texture) -> Result<(), UpdateTextureError> {
self.copy_buffer_to_canvas(texture)?;
self.canvas.present();
Ok(())
}
fn copy_buffer_to_canvas(&mut self, texture: &mut Texture) -> Result<(), UpdateTextureError> {
texture.update(None, self.color_buffer.pixels(), self.config.width * 4)?;
self.canvas.copy(texture, None, None).unwrap();
Ok(())
}
pub fn config(&self) -> &EngineConfig {
&self.config
}
}
impl ClearAuto for EngineCore {
fn clear(&mut self) {
self.color_buffer.clear(self.config.clear_color);
}
}
impl Drawable for EngineCore {
fn draw_grid(&mut self, spacing: usize, color: Option<u32>) {
self.color_buffer.draw_grid(spacing, color);
}
fn draw_rect(&mut self, x: usize, y: usize, width: usize, height: usize, color: u32) {
self.color_buffer.draw_rect(x, y, width, height, color);
}
fn draw_point(&mut self, x: usize, y: usize, color: u32) {
self.color_buffer.draw_point(x, y, color);
}
}
And in main.rs (Again trimmed of the mesh/points manipulations)
use renderer3d::prelude::*;
use vecx::Vec3;
pub fn main() {
let mut eng = Engine::build(EngineConfig {
window_title: "3d Renderer".to_string(),
width: 800,
height: 600,
clear_color: 0xFF000000,
});
let points = build_cube();
let fov = 640.0;
let cam_pos = Vec3(0.0, 0.0, -5.0);
let mut rotation = Vec3(0.0, 0.0, 0.0);
println!("Start update");
eng.on_update(&mut |eng: &mut Engine| {
eng.draw_grid(10, Some(0xFF333333));
rotation = rotation + Vec3(0.01, 0.02, 0.0);
let mut points = transform_points(&points, rotation);
points = project_points(&points, cam_pos, fov);
points.iter().for_each(|point| {
let mut x = point.x();
let mut y = point.y();
x += eng.config().width as f64 / 2.0;
y += eng.config().height as f64 / 2.0;
eng.draw_rect(x as usize, y as usize, 4, 4, 0xFFFF0000);
});
});
}
As you can see, it's important that this callback can access and mutate variables from the outer scope.
I've been through a lot of trial and error and I ended up finding something that works but the fact I have to do unsafe code makes me wonder if that's really the correct way to do this.
Especially I'm worried that I'm trying to design this too much as I would in other less safe/restrictive languages and that I'm missing how to design this properly in Rust.
Update
Here is the full final code after applying the accepted answer's solution
engine module
use std::time::{Duration, Instant};
use sdl2::{
event::Event,
keyboard::Keycode,
pixels::PixelFormatEnum,
render::{Canvas, Texture, UpdateTextureError},
video::Window,
EventPump,
};
use crate::buffer::{ClearAuto, ClearColor, ColorBuffer, Drawable};
use crate::utils::NumOption;
pub struct EngineConfigParams {
pub window_title: Option<String>,
pub width: Option<usize>,
pub height: Option<usize>,
pub clear_color: Option<u32>,
pub fps: Option<u32>,
}
impl Default for EngineConfigParams {
fn default() -> Self {
EngineConfigParams {
window_title: None,
width: None,
height: None,
clear_color: None,
fps: None,
}
}
}
pub struct EngineConfig {
window_title: String,
width: usize,
height: usize,
clear_color: u32,
fps: u32,
}
impl EngineConfig {
pub fn new(params: EngineConfigParams) -> Self {
let default = EngineConfig::default();
EngineConfig {
window_title: params.window_title.unwrap_or(default.window_title),
width: params.width.unwrap_gt_or(0, default.width),
height: params.height.unwrap_gt_or(0, default.height),
clear_color: params.clear_color.unwrap_or(default.clear_color),
fps: params.fps.unwrap_gt_or(0, default.fps),
}
}
pub fn window_title(&self) -> &String {
&self.window_title
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
}
impl Default for EngineConfig {
fn default() -> Self {
EngineConfig {
window_title: "3D renderer".to_string(),
width: 800,
height: 600,
clear_color: 0xFF000000,
fps: 60,
}
}
}
type EngineUpdateFn<'a> = &'a mut dyn FnMut(&mut EngineCore);
pub struct Engine<'a> {
core: EngineCore,
update: Option<EngineUpdateFn<'a>>,
previous_frame_time: Instant,
target_frame_time: Duration,
}
impl<'a> Engine<'a> {
pub fn build(mut config: EngineConfig) -> Engine<'a> {
let ctx = sdl2::init().unwrap();
let video = ctx.video().unwrap();
config = EngineConfig {
width: config.width,
height: config.height,
..EngineConfig::default()
};
match video.display_mode(0, 0) {
Ok(mode) => {
config.width = mode.w as usize;
config.height = mode.h as usize;
println!("Display mode: {:?}", mode);
}
Err(e) => eprintln!(
"Failed to get display mode: {}, using default width and height",
e
),
};
let width = config.width;
let height = config.height;
let window = video
.window(&config.window_title, width as u32, height as u32)
.borderless()
.position_centered()
.fullscreen()
.build()
.unwrap();
let canvas = window.into_canvas().build().unwrap();
let color_buffer = ColorBuffer::new(width, height);
let event_pump = ctx.event_pump().unwrap();
println!("WindowCtx w: {} h: {}", width, height);
let fps = config.fps;
Engine {
core: EngineCore {
config,
canvas,
color_buffer,
event_pump,
},
update: None,
previous_frame_time: Instant::now(),
target_frame_time: Duration::new(0, 1_000_000_000u32 / fps),
}
}
pub fn config(&self) -> &EngineConfig {
&self.core.config
}
pub fn on_update(&mut self, f: EngineUpdateFn<'a>) {
self.update = Some(f);
self.update();
}
pub fn user_update(&mut self) {
self.update.as_mut().unwrap()(&mut self.core);
}
pub fn update(&mut self) {
self.target_frame_time = Duration::new(0, 1_000_000_000u32 / self.core.config.fps);
let texture_creator = self.core.canvas.texture_creator();
let mut texture = texture_creator
.create_texture(
PixelFormatEnum::ARGB8888,
sdl2::render::TextureAccess::Streaming,
self.core.config.width as u32,
self.core.config.height as u32,
)
.unwrap();
let mut running = true;
while running {
self.previous_frame_time = Instant::now();
running = self.core.process_input();
self.user_update();
self.core.render_buffer(&mut texture).unwrap();
self.core.clear();
let now = Instant::now();
let frame_time = now - self.previous_frame_time;
println!(
"Time this frame {}ms {} FPS",
frame_time.as_millis(),
1000u128 / frame_time.as_millis()
);
if frame_time.as_nanos() < self.target_frame_time.as_nanos() {
::std::thread::sleep(self.target_frame_time - frame_time);
}
}
}
}
pub struct EngineCore {
config: EngineConfig,
canvas: Canvas<Window>,
color_buffer: ColorBuffer,
event_pump: EventPump,
}
impl EngineCore {
fn process_input(&mut self) -> bool {
for event in self.event_pump.poll_iter() {
match event {
Event::Quit { .. }
| Event::KeyDown {
keycode: Some(Keycode::Escape),
..
} => {
println!("Received quit event, shutting down");
return false;
}
_ => {}
}
}
true
}
fn render_buffer(&mut self, texture: &mut Texture) -> Result<(), UpdateTextureError> {
self.copy_buffer_to_canvas(texture)?;
self.canvas.present();
Ok(())
}
fn copy_buffer_to_canvas(&mut self, texture: &mut Texture) -> Result<(), UpdateTextureError> {
texture.update(None, self.color_buffer.pixels(), self.config.width * 4)?;
self.canvas.copy(texture, None, None).unwrap();
Ok(())
}
pub fn config(&self) -> &EngineConfig {
&self.config
}
}
impl ClearAuto for EngineCore {
fn clear(&mut self) {
self.color_buffer.clear(self.config.clear_color);
}
}
impl Drawable for EngineCore {
fn draw_grid(&mut self, spacing: usize, color: Option<u32>) {
self.color_buffer.draw_grid(spacing, color);
}
fn draw_rect(&mut self, x: usize, y: usize, width: usize, height: usize, color: u32) {
self.color_buffer.draw_rect(x, y, width, height, color);
}
fn draw_point(&mut self, x: usize, y: usize, color: u32) {
self.color_buffer.draw_point(x, y, color);
}
}
main.rs
use renderer3d::{prelude::*, Mesh};
use vecx::Vec3;
pub fn main() {
let mut eng = Engine::build(EngineConfig::new(EngineConfigParams {
window_title: Some("3d Renderer".to_string()),
..EngineConfigParams::default()
}));
let cube = Mesh::cube();
let fov = 640.0;
let cam_pos = Vec3(0.0, 0.0, -5.0);
let mut rotation = Vec3(0.0, 0.0, 0.0);
println!("Start update");
eng.on_update(&mut |eng| {
eng.draw_grid(10, Some(0xFF333333));
rotation = rotation + Vec3(0.01, 0.02, 0.0);
let mut points = cube
.face_vertices()
.map(|fv| transform_points(&fv, rotation))
.flatten()
.collect();
points = project_points(&points, cam_pos, fov);
points.iter().for_each(|point| {
let mut x = point.x();
let mut y = point.y();
x += eng.config().width() as f64 / 2.0;
y += eng.config().height() as f64 / 2.0;
eng.draw_rect(x as usize, y as usize, 4, 4, 0xFFFF0000);
});
});
}
fn transform_points(points: &Vec<Vec3>, rotation: Vec3) -> Vec<Vec3> {
points.iter().map(|p| p.rot(rotation)).collect()
}
fn project_points(points: &Vec<Vec3>, cam_pos: Vec3, fov: f64) -> Vec<Vec3> {
points
.iter()
.map(|point| {
let z = point.z() + cam_pos.z();
let mut x = point.x() / z;
let mut y = point.y() / z;
x *= fov;
y *= fov;
Vec3(x, y, z)
})
.collect()
}
The problem is not the variables from the outer scope, it's the fact that the callback has mutable access to Engine but it itself comes from engine, so there are two coexisting mutable references. Theoretically, it could for example access the captured variables both by the data pointer in Engine and by using them directly from different threads, causing a data race.
Since you probably don't need access to the callback inside the callback, I would split Engine into two structs: one will contain the callback and the other, and have an update() and on_update() methods, and the other will include all other stuff. This way, the callback will take a mutable reference to the inner struct, which it can - since it is not contained within, but will still be able to call all engine's functions.
I'm very new to rust and I'm trying to creating an AVL Tree. I used Rc because I want every node to be owned by nodes above it and RefCell to have it mutable internally.
I've started to build the "insert" method, but it isn't inserting a new node in the correct place, rather its replace the "root" node with the new node and I'm having trouble understanding why. I believe that this may be an ownership issue but I'm not sure.
use std::cell::RefCell;
use std::cmp::Ordering;
use std::rc::Rc;
#[derive(Debug)]
pub struct BaseNode {
pub key: u32,
pub left: AvlTree,
pub right: AvlTree,
}
pub type AvlNode = Rc<RefCell<BaseNode>>;
pub type AvlTree = Option<AvlNode>;
impl BaseNode {
fn new(key: u32) -> Self {
Self {
key: key,
left: None,
right: None,
}
}
}
trait AvlNodeTrait {
fn new_node(key: u32) -> Self;
}
impl AvlNodeTrait for AvlNode {
fn new_node(key: u32) -> Self {
Rc::new(RefCell::new(BaseNode::new(key)))
}
}
pub trait AvlTreeTrait {
fn new() -> Self;
fn left(&self) -> Self;
fn right(&self) -> Self;
fn insert(&mut self, key: u32);
fn dupe(&self) -> Self;
fn set(&mut self, node: AvlNode);
}
impl AvlTreeTrait for AvlTree {
fn new() -> Self {
None
}
fn left(&self) -> Self {
if let Some(node) = self {
return node.borrow().right.dupe();
}
panic!("Trying to get Left of None!")
}
fn right(&self) -> Self {
if let Some(node) = self {
return node.borrow().right.dupe();
}
panic!("Trying to get right of None!")
}
fn dupe(&self) -> Self {
match self {
Some(node) => Some(Rc::clone(&node)),
None => None,
}
}
fn set(&mut self, node: AvlNode) {
*self = Some(Rc::clone(&node));
}
fn insert(&mut self, key: u32) {
let node = AvlNode::new_node(key);
let mut curr_tree = self;
let mut curr_key = 0;
while !curr_tree.is_none() {
if let Some(node) = &curr_tree {
curr_key = node.borrow().key;
if key > curr_key {
*curr_tree = curr_tree.left()
} else if key < curr_key {
*curr_tree = curr_tree.right()
} else {
return;
}
}
}
*curr_tree = Some(Rc::clone(&node));
}
}
fn main() {
let mut tree = AvlTree::new();
println!("{:?}", tree); // None
tree.insert(5);
println!("{:?}", tree); // Some(RefCell { value: BaseNode { key: 5, left: None, right: None } })
tree.insert(56);
println!("{:?}", tree); // Some(RefCell { value: BaseNode { key: 2, left: None, right: None } })
}
I would say the use of RefCell is quite unnecessary and potentially unsafe in this context. RefCell hands over the ownership/borrow checking to the runtime instead of doing them during compile time. This can lead to your program to panic in case it violates any of the borrowing rules.
I would prefer a "recursive" type looking something like this:
struct AVLTree<T> {
val: T,
left: Option<Box<AVLTree<T>>>,
right: Option<Box<AVLTree<T>>>
}
This will of course introduce some overhead due to memory allocation.
I am writing a binary tree structure, and I have problems at the insert function. After calling the function to insert data in my node, the node is not modified.
use std::ptr;
#[derive(Clone, Copy)]
struct Node<T> {
data: Option<T>,
left: *mut Node<T>,
right: *mut Node<T>,
}
impl<T> Node<T> {
pub fn new(data: T) -> Node<T> {
Node {
data: Some(data),
left: ptr::null_mut(),
right: ptr::null_mut(),
}
}
pub fn insert(mut self, data: T) {
let tmp = self.data.unwrap();
self.data = None;
self.left = &mut Node::new(data);
self.right = &mut Node::new(tmp);
}
}
impl<T: std::fmt::Display> std::fmt::Display for Node<T>
where T: std::fmt::Debug
{
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.data {
Some(ref x) => {
write!(f, "Node data: {}, left: {}, right: {}", x,
if self.left.is_null() {"null"} else {"not null"},
if self.right.is_null() {"null"} else {"not null"},
)
}
None => {
write!(f, "Node data: None, left: {}, right: {}",
if self.left.is_null() {"null"} else {"not null"},
if self.right.is_null() {"null"} else {"not null"},
)
}
}
}
}
fn main() {
let mut root: Node<i32> = Node::new(32);
println!("Root before insert : {}", root);
root.insert(42);
println!("Root after insert: {}", root);
}
And a trace of execution
Root before insert : Node data: 32, left: null, right: null
Root after insert : Node data: 32, left: null, right: null
My node is not modified after the function insert. What can I do to solve this?
Since you appear to be quite confused, let's get you started!
First of all, there is no reason to use raw pointers. Furthermore, I advise AGAINST using Copy as it just hides ownership issues. Finally, it's much easier to derive Debug than implement the formatting manually (and it allows experimenting with the layout of the struct in a more automated fashion).
#[derive(Debug)]
struct Node<T> {
data: Option<T>, // are you sure about using Option here?
left: Option<Box<Node<T>>>,
right: Option<Box<Node<T>>>,
}
So, instead of raw pointers, we use a Box pointer which we place into an Option to handle the null case.
The construction is now:
impl<T> Node<T> {
fn new(data: T) -> Node<T> {
Node {
data: Some(data),
left: None,
right: None,
}
}
}
Nothing outstanding, let's move on to insert:
impl<T> Node<T> {
fn insert(&mut self, data: T) {
let current = self.data.take().expect("Cannot insert in empty node!");
self.left = Some(Box::new(Node::new(current)));
self.right = Some(Box::new(Node::new(data)));
}
}
And we can move on to the display (using Debug with "{:?}"):
fn main() {
let mut root: Node<i32> = Node::new(32);
println!("Root before insert : {:?}", root);
root.insert(42);
println!("Root after insert: {:?}", root);
}
And it works!
I am struggling to code a delete method for a BST:
use std::cmp::Ordering::{Less,Equal,Greater};
type Child = Option<Box<Node>>;
#[derive(Debug)]
struct Node {
val: i32,
left: Child,
right: Child,
}
impl Node {
fn new(val: i32) -> Node {
Node {
val: val,
left: None,
right: None,
}
}
fn delete(&mut self, val: i32) {
//find the node for deletion
let mut node: &mut Node = self;
loop {
match val.cmp(&self.val) {
Less => {
if self.left.is_some() {
node = &mut (self.left.as_mut().unwrap());
} else {
break;
}
}
Equal => break,
Greater => node = panic!("nyi"),
}
}
// once found, check if the node has children and delete accordingly
}
}
fn main() {
let mut node = Node::new(10);
node.right = Some(Box::new(Node::new(20)));
}
I have tried loads of different techniques using recursion and iteration but the borrow checker complains. For example:
error: borrowed value does not live long enough
node = &mut (self.left.as_mut().unwrap());
^~~~~~~~~~~~~~~~~~~~~~~~~~~
Is it possible to write a delete function using safe Rust?
This recursive solution compiles:
use std::cmp::Ordering::{Less,Equal,Greater};
type Child = Option<Box<Node>>;
#[derive(Debug)]
struct Node {
val: i32,
left: Child,
right: Child,
}
impl Node {
fn new(val: i32) -> Node {
Node {
val: val,
left: None,
right: None,
}
}
fn delete(&mut self, val: i32) {
fn find_node_to_delete(node: &mut Node, val: i32) -> Option<&mut Node> {
match val.cmp(&node.val) {
Less => {
if let Some(ref mut left) = node.left {
find_node_to_delete(left, val)
} else {
None
}
}
Equal => Some(node),
Greater => unimplemented!(),
}
}
//find the node for deletion
let node = find_node_to_delete(self, val);
// once found, check if the node has children and delete accordingly
}
}
fn main() {
let mut node = Node::new(10);
node.right = Some(Box::new(Node::new(20)));
}