2021-03-10
I spent hours and hours (actually, just above 2 hours) thinking about how to communicate to the user of a game engine a way to modify vertex buffers in the game loop.

Here's the scheme I came up with:

Since the winit crate hijacks the main thread with a closure of static lifetime, my simplest option is to allow that closure to capture the variables necessary to the game loop in its scope.

However, the question is: how do I define a couple of callback functions in a nice way that allows the user an interface to the vertex buffers and draw calls of the main closure?

I can't define a single closure and have the user put all of their data in there. Although simple, I need the user to be able to split their execution into different parts of a frame: initialization, event handling and updating. Also, the updating may be split into a buffer mutation step and a draw call step, or it may not, but I would like the freedom!

So, what about sending in several closures? Fine idea, but any closure that is called within the event loop of winit needs a static lifetime, and because it does, it needs to take ownership of all of its enclosed variables. Basically, data cannot be shared between the closures running in the game loop.

So we want to share data between some functions, and we want to call them within a closure with a static lifetime. The data should be user defined, since we don't know what is going to be in it, and therefore of variable size and type.

Ok, that sounds like a good application for a trait! Let's define a trait that requires the user to implement a set of functions on their data type. The actual "host type" can contain anything and be of variable size, we'll just put it in a dynamically allocated pointer and be happy.

This is what I would have done, if I weren't so recently up-to-my-knees with data-orientation and optimization. The above is the obvious object-oriented solution: each function is called using polymorphism. Only, polymorphism requires virtual function tables every time the function is called, and this is a hot loop. No, it's the hot loop, the thing that is going to be called all the time. We don't want to pay for something we don't necessarily need.

So, unless we want to abandon the idea of working within winits event loop, which has been useful thus far, the remaining option is this: function pointers and monomorphism. I mentioned that the user data is of variable size - even with monomorphism, this is still true. Only, the size is still known at compile time using Generic Type Parameters.

Actually, polymorphism using traits is overkill and wrong in this case, since function pointers more directly express what we want: a single place in the code that is called from the closure. Polymorphism would allow for several different definitions of the same function signature, which is not really useful in this case.

Here is the code!

struct Data {}

struct UserAxis<D> {
    init: fn(user_data: &mut D),
    event: fn(user_data: &mut D, event: &Event<()>),
    update: fn(user_data: &mut D),
}

fn init(data: &mut Data) {
    
}

fn event(data: &mut Data, event: &Event<()>) {

}

fn update(data: &mut Data) {

}

fn main() {
    println!("Hello, world!");
    let data = Data {};
    let axis = UserAxis {
        init,
        event,
        update,
    };
    let engine_instance = engine::Engine::start()
        .build()
        .unwrap_or_else(|err| {
            panic!("{}", err);
        });
    engine_instance.run(data, axis);
}

The function pointers are stored in a structure I like to call the "axis" (since... engines... eheh) and the user data is completely user-defined. Goal achieved!

The next step is to actually provide useful parameters to the functions so that they can do things like modify vertex buffers and issue draw calls. The crate I'm using, vulkano, has a built-in command buffer, and I'm in early enough stages to completely ignore vendor abstraction (if I'm ever even going to care, this is very much a private project), so it seems suitable to give the user side the ability to directly add commands to the command buffer with a thin function-based abstraction in between.

vulkano also supports what it calls Buffer Slices, which is the ability to allocate one big buffer of vertices, but issue a draw call on only part of it. Hopefully this means I can stick lots of things into a single buffer and draw only the things that draw calls are issued on, while not paying too much for switching buffers all the time.

I'd like to give the user side control over the allocation and slicing of buffers directly. The other option is to define the primitives that the user can draw, such as quads, cubes, etc. The goal of the engine is after all to draw such primitives, not something fancier. Still, letting the user have control over the vertex buffers directly comes with some benefits in optimization, and I like that!

The next step would be to allow the user control over the defintion of the Vertex structure, which would mean that the user could manually write shaders and define Vertex structures that fit those shaders. This is a lot more difficult because I'd have to allow the user to compile those shaders in runtime, or rebuild the engine every time they change a shader. The first is time-consuming (I'm currently just using vulkanos built-in shader compiler functions that compile shaders at build-time), the second is not very appealing, especially for a project that I might share in the future.

That's for next time!