Writing a game engine in Rust and Vulkan
Tags: rust, vulkan, gamedev, ecs
Published: 2023-09-26 19:15:38

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4kzk0m8hxm1wqw1bzbo5.gif)

## 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:

```rust
#[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);
    })
}
```


### Profiling

And well, after seeing those horrendous benchmark results, I used [flamegraph](https://github.com/brendangregg/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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q0r7aqbq0psk0xly5yov.png)

The problem was that I was passing a [vector](https://doc.rust-lang.org/std/vec/index.html) of tuples to the `Entity` constructor and using the [`DashMap::from_iter_par`](https://docs.rs/dashmap/5.5.3/dashmap/struct.DashMap.html#impl-FromParallelIterator%3C(K,+V)%3E-for-DashMap%3CK,+V,+S%3E) 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](https://docs.rs/rayon/latest/rayon/index.html), 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](https://docs.rs/rustc-hash/latest/rustc_hash/struct.FxHasher.html), 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/feipghu8afh71pslxl5b.jpg)

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](https://github.com/jakoritarleite/woody)