DEV Community

Cover image for Writing a game engine in Rust and Vulkan
João Koritar
João Koritar

Posted on

Writing a game engine in Rust and Vulkan

Since I'm learning about game engine development, I'll take notes and document the things I learn and my development process.

At the time of writing this post, I have a very basic 3D rendering engine with a custom ECS (Entity Component System) architecture. I can draw simple geometries with custom colors, move the camera around, as well as rotate it with the mouse, as shown in this GIF:

Running the engine using cargo

Doing premature optimization

I spent last weekend working on improving my ECS, and I'll show my process for doing it.

Benchmark

The first step was to benchmark the library's spawn and query performance, and the results were neither decent nor satisfactory:

Spawning 1 million entities (very basic entities with 1-2 components each) took 5 seconds.

Querying those entities took almost 3 seconds.

The code for spawning looks something like this:

#[derive(Component)]
struct Position(u128, u128);

#[derive(Component)]
struct Velocity(u128);

fn main() {
    let mut world = World::new();

    for i in 0..=1_000_000 {
        world.spawn((Position(i, i), Velocity(i)));
        world.spawn(Velocity(i));
    }

    let mut query = world.query::<(&Position, &Velocity)>();

    query.iter().for_each(|(position, velocity)| {
        assert_eq!(position.0, position.1);
        assert_eq!(position.0, velocity.0);
    })
}
Enter fullscreen mode Exit fullscreen mode

Profiling

And well, after seeing those horrendous benchmark results, I used flamegraph to determine what was consuming the most CPU time.

Spawning

The first thing I noticed was that the Entity creation had the largest share of the result, as you can see below:

Stack trace visualization with an arrow pointing to the Entity::new method

The problem was that I was passing a vector of tuples to the Entity constructor and using the DashMap::from_iter_par method, which creates the map from a parallel iterator. However, for some reason, it was taking a lot of time to do it.

The solution I've come to is that instead of creating the map from the vector and storing it, I stored the vector itself. It became significantly faster and didn't affect query performance.

Querying

The query performance wasn't too bad; indeed, it was okay since I was doing it single-threaded. Anyway, I did some refactoring to be able to use the parallel iterator from rayon, and it improved a bit, though not drastically. Then, I noticed that DashMap was using Rust's default hasher, which isn't quite as speedy as I wanted. So, I changed it to use FxHasher, which is much faster than the default one. With this change, the query operation now takes 0.4 seconds, as shown in the following image:

Image showing time elapsed for query

You can ignore the Positions and Velocity logs since I was using them just for debugging purposes.

Well, this was my process of refactoring and optimizing some parts of my ECS library. I hope you liked it. See you in my next post.

GitHub repository: woody

Top comments (0)