Writing applications with Kaigen

The easiest way to start with Kaigen is to pick one of the templates in Kaigen Studio, open the code, and ask your AI to explain it at a high level.

The current templates use a lot of advanced features to showcase what the engine can do. This does mean they can look a little intimidating for learning, though. We have plans to release a template that contains several small demos showing engine features in isolation. In the meantime, a good template to start with is the "Starting Template".

Just three functions

One of the core pillars of Kaigen is that we don't take flow control away from the user. That means every Kaigen application really just needs three functions:

On each platform, Kaigen handles initializing the window, managing the clock, vsync, input, thread sync, audio sync, etc., and hands the frame loop directly to your application, so you have full control of it.

my_game.c
typedef struct {
  // everything your application keeps between frames
} AppState;

HZ_APP_API size_t app_state_size(void) { return sizeof(AppState); }

HZ_APP_API void app_init(AppMemory *memory) {
  AppState *state = memory->state;
  // create your systems, load your scene
}

HZ_APP_API void app_update_and_render(AppMemory *memory) {
  AppState *state = memory->state;
  // update and draw one frame
}

app_init and app_update_and_render run on every core at the same time, because Kaigen is SPMD. More on that in Every core runs your code.

Configuration

All of your application's configuration is yours, and you can set it up however you want. Usually that happens in app_init.

To configure the engine itself (window title and size, vsync, frame pacing, how many threads to use), define hz_app_config anywhere in your code. Anything you leave out uses the engine default.

my_game.c
const HzAppConfig hz_app_config = {
  .window_title = "My Game",
  .window_width = 1600,
  .window_height = 900,
};

All the options are in hz/src/app/app.h.

Every core runs your code

Kaigen is SPMD: single program, multiple data. That means that when the app starts, the engine creates all of the application threads according to your configuration, and every one of them runs app_init and app_update_and_render, at the same time, in lock-step. We call each of these threads a lane.

Thanks to Ryan Fleury's Multi-Core By Default for inspiring this idea.

SPMD makes multi-threaded code easy to write, but we understand raw threads can feel hardcore. You get to pick how much help you want. From the most help to the most control:

  1. Use the ECS for your game entities. Systems say what they read and write, and the engine spreads them across lanes and orders them for you.
An ECS system
ecs_system(move_system, EcsInOut(Position), EcsIn(Velocity)) {
  for (i32 i = 0; i < it->count; i++) {
    Position *p = ecs_sys_get_component_mut(Position, i);
    const Velocity *v = ecs_sys_get_component(Velocity, i);
    p->value = v3_add(p->value, v3_scale(v->value, it->delta_time));
  }
}

ecs_schedule(world, move_system);
ecs_flush(world, dt);
  1. Use lane kernels for your own arrays. You declare what a function reads and writes, and you get the same checks as ECS systems: writing to something you declared as input won't compile, and in debug builds two lanes touching the same memory without a lane_sync() is reported with both lines. It runs as fast as writing the lanes by hand. hz/src/lib/lane_kernel.h
A lane kernel
lane_kernel(update_particles, LaneInOut(Particle, particles)) {
  for (u32 i = it->min; i < it->max; i++) {
    update_particle(&particles[i]);
  }
}

lane_run(update_particles, particle_count, .particles = particles);
The same loop, inline
lane_foreach(i, particle_count, LaneInOut(Particle, particles)) {
  update_particle(&particles[i]);
}
lane_sync();
Several kernels, scheduled
// once, at setup, on one lane
lane_schedule_init(&state->sched, allocator, 16);

// every frame: independent kernels run in parallel
lane_schedule(&state->sched, update_particles, particle_count, .particles = particles);
lane_schedule(&state->sched, update_agents, agent_count, .agents = agents);
lane_schedule_run(&state->sched);
  1. Use lanes directly when you want full control. lane_range(count) gives each lane its slice of a loop, lane_sync() waits until every lane gets there, and is_main_thread() is for work that has to happen on one lane. Every lane has to reach the same lane_sync() calls; in debug builds the engine tells you which lanes didn't, and where.
Splitting a loop across every lane
Range(u32) r = lane_range(particle_count);
for (u32 i = r.min; i < r.max; i++) {
  update_particle(&particles[i]);
}
lane_sync();
Work on one lane
if (is_main_thread()) {
  draw_menu(state); // the UI lives on one lane
}
  1. Run your game logic under is_main_thread(). This takes you back to the default of every other engine, while heavy work like rendering and physics still runs multi-threaded. Keep calling the engine's update functions from every lane, though, or the app hangs.
Game logic on one lane
HZ_APP_API void app_update_and_render(AppMemory *memory) {
  AppState *state = memory->state;
  if (is_main_thread()) {
    update_game(state);
  }
  lane_sync();
  ecs_flush(state->world, memory->dt); // engine updates: every lane
}

The main systems

These are the systems you'll use most. Each one is a header in the engine, and the headers are written to be read.

If you're not sure where something lives or whether Kaigen has the feature you need, ask your AI, or yell at Gabriel on Discord :).