Skip to content

Instantly share code, notes, and snippets.

@gabrieldechichi
Created January 8, 2026 19:48
Show Gist options
  • Select an option

  • Save gabrieldechichi/17e13f9e2e8d8e5abb88019ab9efdc15 to your computer and use it in GitHub Desktop.

Select an option

Save gabrieldechichi/17e13f9e2e8d8e5abb88019ab9efdc15 to your computer and use it in GitHub Desktop.
demo_ecs_boids.c
#include "context.h"
#include "lib/thread_context.h"
#include "lib/typedefs.h"
#include "lib/string_builder.h"
#include "os/os.h"
#include "lib/math.h"
#include "lib/hash.h"
#include "lib/random.h"
#include "gpu_backend.h"
#include "renderer.h"
#include "camera.h"
#include "input.h"
#include "app.h"
#include "mesh.h"
#include "assets.h"
#include "ui_system.h"
#include "shaders/fish_vs.h"
#include "shaders/fish_instanced_vs.h"
#include "shaders/fish_fs.h"
#define NUM_BOIDS 100000
#define NUM_TARGETS 2
#define NUM_OBSTACLES 1
#define INSTANCE_BATCH_SIZE (1024 * 16)
#define NUM_INSTANCE_BATCHES ((NUM_BOIDS + INSTANCE_BATCH_SIZE - 1) / INSTANCE_BATCH_SIZE)
#define GRID_SIZE 8192
#define MAX_PER_BUCKET 256
#define CELL_SIZE 8.0f
#define BOID_SEPARATION_WEIGHT 1.0f
#define BOID_ALIGNMENT_WEIGHT 1.0f
#define BOID_TARGET_WEIGHT 2.0f
#define BOID_OBSTACLE_AVERSION_DISTANCE 30.0f
#define BOID_MOVE_SPEED 25.0f
HZ_ECS_COMPONENT()
typedef struct { f32 x, y, z; } Position;
HZ_ECS_COMPONENT()
typedef struct { f32 x, y, z; } Heading;
HZ_ECS_COMPONENT()
typedef struct { u32 index; } BoidIndex;
HZ_ECS_COMPONENT()
typedef struct { u8 dummy; } BoidTag;
HZ_ECS_COMPONENT()
typedef struct { f32 x, y, z; } Scale;
HZ_ECS_COMPONENT()
typedef struct { mat4 value; } LocalToWorld;
HZ_ECS_COMPONENT()
typedef struct {
GpuMesh_Handle mesh;
Material_Handle material;
} MeshRenderer;
HZ_ECS_COMPONENT()
typedef struct {
const struct SampledAnimationClip *clip;
f32 current_time;
vec3 *dest_position;
} AnimationPlayer;
typedef struct SampledAnimationClip {
f32 sample_rate;
i32 frame_count;
const vec3 *positions;
const versor *rotations;
} SampledAnimationClip;
typedef struct {
f32 px, py, pz;
f32 hx, hy, hz;
} BoidBucketEntry;
typedef struct {
u32 count;
f32 sum_align_x, sum_align_y, sum_align_z;
f32 sum_sep_x, sum_sep_y, sum_sep_z;
i32 nearest_target_idx;
i32 nearest_obstacle_idx;
f32 nearest_obstacle_dist;
BoidBucketEntry entries[MAX_PER_BUCKET];
} BoidBucket;
typedef struct {
EcsWorld world;
AssetSystem assets;
InputSystem input;
Camera camera;
UISystem ui;
UIFont font;
GpuMesh_Handle fish_mesh;
Material_Handle fish_material;
GpuTexture fish_albedo_tex, fish_tint_tex, fish_metallic_gloss_tex;
GpuTexture shark_albedo_tex, shark_metallic_gloss_tex;
vec3 target_positions[NUM_TARGETS];
vec3 obstacle_positions[NUM_OBSTACLES];
EcsEntity target_entities[NUM_TARGETS];
EcsEntity obstacle_entities[NUM_OBSTACLES];
BoidBucket buckets[GRID_SIZE];
mat4 instance_data[NUM_BOIDS];
InstanceBuffer_Handle instance_buffers[NUM_INSTANCE_BATCHES];
f32 total_time;
} GameState;
global GameState state = {0};
// hardcoded animation and scene data (extracted from Unity)
#include "./Shark_animation.c"
#include "./Target01_animation.c"
#include "./Target02_animation.c"
#include "./exported_scene_loader.c"
void sample_animation_position(const SampledAnimationClip *clip, f32 time, vec3 out_pos) {
f32 duration = clip->sample_rate * (f32)(clip->frame_count - 1);
while (time >= duration) time -= duration;
if (time < 0.0f) time = 0.0f;
f32 frame_f = time / clip->sample_rate;
i32 frame0 = (i32)frame_f;
i32 frame1 = frame0 + 1;
if (frame1 >= clip->frame_count) frame1 = clip->frame_count - 1;
f32 t = frame_f - (f32)frame0;
const vec3 *p0 = &clip->positions[frame0];
const vec3 *p1 = &clip->positions[frame1];
out_pos[0] = (*p0)[0] + t * ((*p1)[0] - (*p0)[0]);
out_pos[1] = (*p0)[1] + t * ((*p1)[1] - (*p0)[1]);
out_pos[2] = (*p0)[2] + t * ((*p1)[2] - (*p0)[2]);
}
void sample_animation_rotation(const SampledAnimationClip *clip, f32 time, versor out_rot) {
f32 duration = clip->sample_rate * (f32)(clip->frame_count - 1);
while (time >= duration) time -= duration;
if (time < 0.0f) time = 0.0f;
f32 frame_f = time / clip->sample_rate;
i32 frame0 = (i32)frame_f;
i32 frame1 = frame0 + 1;
if (frame1 >= clip->frame_count) frame1 = clip->frame_count - 1;
f32 t = frame_f - (f32)frame0;
glm_quat_slerp((f32 *)&clip->rotations[frame0], (f32 *)&clip->rotations[frame1], t, out_rot);
}
HZ_ECS_SYSTEM()
void play_animations_system(EcsCtx *ctx,
Position *positions,
AnimationPlayer *players,
u32 count) {
f32 dt = ctx->delta_time;
if (dt > 0.05f) dt = 0.05f;
for (u32 i = 0; i < count; i++) {
AnimationPlayer *player = &players[i];
player->current_time += dt;
f32 duration = player->clip->sample_rate * (f32)(player->clip->frame_count - 1);
while (player->current_time >= duration) {
player->current_time -= duration;
}
vec3 sampled_pos;
sample_animation_position(player->clip, player->current_time, sampled_pos);
positions[i].x = sampled_pos[0];
positions[i].y = sampled_pos[1];
positions[i].z = sampled_pos[2];
if (player->dest_position) {
(*player->dest_position)[0] = sampled_pos[0];
(*player->dest_position)[1] = sampled_pos[1];
(*player->dest_position)[2] = sampled_pos[2];
}
}
}
HZ_ECS_SYSTEM()
void BuildAnimatedTransformSystem(EcsCtx *ctx,
const Position *positions,
const AnimationPlayer *players,
const Scale *scales,
LocalToWorld *transforms,
u32 count) {
for (u32 i = 0; i < count; i++) {
const Position *pos = &positions[i];
const AnimationPlayer *player = &players[i];
const Scale *scale = &scales[i];
versor anim_rot;
sample_animation_rotation(player->clip, player->current_time, anim_rot);
quaternion fish_orient;
quat_from_euler(VEC3(RAD(90), RAD(180), 0), fish_orient);
versor rot;
glm_quat_mul(anim_rot, fish_orient, rot);
mat4 model;
glm_mat4_identity(model);
glm_translate(model, (vec3){pos->x, pos->y, pos->z});
mat4 rot_mat;
glm_quat_mat4(rot, rot_mat);
glm_mat4_mul(model, rot_mat, model);
glm_scale(model, (vec3){scale->x, scale->y, scale->z});
glm_mat4_copy(model, transforms[i].value);
}
}
HZ_ECS_SYSTEM()
void render_mesh_system(EcsCtx *ctx,
const LocalToWorld *transforms,
const MeshRenderer *renderers,
u32 count) {
for (u32 i = 0; i < count; i++) {
renderer_draw_mesh(renderers[i].mesh, renderers[i].material, transforms[i].value);
}
}
HZ_ECS_SYSTEM(filter = BoidTag)
void insert_boids_system(EcsCtx *ctx,
const Position *positions,
const Heading *headings,
const BoidIndex *indices,
u32 count) {
for (u32 i = 0; i < count; i++) {
f32 px = positions[i].x, py = positions[i].y, pz = positions[i].z;
u32 hash = spatial_hash_3f(px, py, pz, CELL_SIZE) % GRID_SIZE;
u32 slot = ins_atomic_u32_inc_eval(&state.buckets[hash].count) - 1;
if (slot < MAX_PER_BUCKET) {
BoidBucketEntry *entry = &state.buckets[hash].entries[slot];
entry->px = px;
entry->py = py;
entry->pz = pz;
entry->hx = headings[i].x;
entry->hy = headings[i].y;
entry->hz = headings[i].z;
}
}
}
HZ_ECS_SYSTEM(iter_mode = range, iter_count = GRID_SIZE, sync = barrier)
void merge_cells_system(EcsCtx *ctx, u32 count) {
for (u32 i = 0; i < count; i++) {
i32 bucket_idx = ctx->offset + i;
BoidBucket *bucket = &state.buckets[bucket_idx];
if (bucket->count == 0) continue;
u32 n = bucket->count;
if (n > MAX_PER_BUCKET) n = MAX_PER_BUCKET;
f32 sum_ax = 0, sum_ay = 0, sum_az = 0;
f32 sum_sx = 0, sum_sy = 0, sum_sz = 0;
for (u32 j = 0; j < n; j++) {
BoidBucketEntry *e = &bucket->entries[j];
sum_ax += e->hx; sum_ay += e->hy; sum_az += e->hz;
sum_sx += e->px; sum_sy += e->py; sum_sz += e->pz;
}
bucket->sum_align_x = sum_ax;
bucket->sum_align_y = sum_ay;
bucket->sum_align_z = sum_az;
bucket->sum_sep_x = sum_sx;
bucket->sum_sep_y = sum_sy;
bucket->sum_sep_z = sum_sz;
f32 first_px = bucket->entries[0].px;
f32 first_py = bucket->entries[0].py;
f32 first_pz = bucket->entries[0].pz;
f32 nearest_target_dist_sq = 1e18f;
i32 nearest_target_idx = 0;
for (i32 t = 0; t < NUM_TARGETS; t++) {
f32 dx = state.target_positions[t][0] - first_px;
f32 dy = state.target_positions[t][1] - first_py;
f32 dz = state.target_positions[t][2] - first_pz;
f32 dist_sq = dx * dx + dy * dy + dz * dz;
if (dist_sq < nearest_target_dist_sq) {
nearest_target_dist_sq = dist_sq;
nearest_target_idx = t;
}
}
bucket->nearest_target_idx = nearest_target_idx;
f32 nearest_obs_dist_sq = 1e18f;
i32 nearest_obs_idx = 0;
for (i32 o = 0; o < NUM_OBSTACLES; o++) {
f32 dx = state.obstacle_positions[o][0] - first_px;
f32 dy = state.obstacle_positions[o][1] - first_py;
f32 dz = state.obstacle_positions[o][2] - first_pz;
f32 dist_sq = dx * dx + dy * dy + dz * dz;
if (dist_sq < nearest_obs_dist_sq) {
nearest_obs_dist_sq = dist_sq;
nearest_obs_idx = o;
}
}
bucket->nearest_obstacle_idx = nearest_obs_idx;
bucket->nearest_obstacle_dist = sqrtf(nearest_obs_dist_sq);
}
}
HZ_ECS_SYSTEM(filter = BoidTag, depends_on = merge_cells_system)
void steer_boids_system(EcsCtx *ctx,
Position *positions,
Heading *headings,
u32 count) {
f32 dt = ctx->delta_time;
if (dt > 0.05f) dt = 0.05f;
for (u32 i = 0; i < count; i++) {
f32 px = positions[i].x, py = positions[i].y, pz = positions[i].z;
f32 forward_x = headings[i].x, forward_y = headings[i].y, forward_z = headings[i].z;
u32 hash = spatial_hash_3f(px, py, pz, CELL_SIZE) % GRID_SIZE;
BoidBucket *bucket = &state.buckets[hash];
u32 n = bucket->count;
if (n > MAX_PER_BUCKET) n = MAX_PER_BUCKET;
f32 alignment_x, alignment_y, alignment_z;
f32 separation_x, separation_y, separation_z;
i32 neighbor_count;
if (n == 0) {
neighbor_count = 1;
alignment_x = forward_x; alignment_y = forward_y; alignment_z = forward_z;
separation_x = px; separation_y = py; separation_z = pz;
} else {
neighbor_count = (i32)n;
alignment_x = bucket->sum_align_x;
alignment_y = bucket->sum_align_y;
alignment_z = bucket->sum_align_z;
separation_x = bucket->sum_sep_x;
separation_y = bucket->sum_sep_y;
separation_z = bucket->sum_sep_z;
}
f32 inv_count = 1.0f / (f32)neighbor_count;
i32 nearest_obstacle_idx = bucket->nearest_obstacle_idx;
f32 obs_x = state.obstacle_positions[nearest_obstacle_idx][0];
f32 obs_y = state.obstacle_positions[nearest_obstacle_idx][1];
f32 obs_z = state.obstacle_positions[nearest_obstacle_idx][2];
f32 nearest_obstacle_dist = bucket->nearest_obstacle_dist;
i32 nearest_target_idx = bucket->nearest_target_idx;
f32 tgt_x = state.target_positions[nearest_target_idx][0];
f32 tgt_y = state.target_positions[nearest_target_idx][1];
f32 tgt_z = state.target_positions[nearest_target_idx][2];
// Alignment
f32 avg_ax = alignment_x * inv_count, avg_ay = alignment_y * inv_count, avg_az = alignment_z * inv_count;
f32 align_dx = avg_ax - forward_x, align_dy = avg_ay - forward_y, align_dz = avg_az - forward_z;
f32 align_len = sqrtf(align_dx * align_dx + align_dy * align_dy + align_dz * align_dz);
f32 align_rx = 0, align_ry = 0, align_rz = 0;
if (align_len > 0.0001f) {
f32 inv = BOID_ALIGNMENT_WEIGHT / align_len;
align_rx = align_dx * inv; align_ry = align_dy * inv; align_rz = align_dz * inv;
}
// Separation
f32 sep_dx = px * (f32)neighbor_count - separation_x;
f32 sep_dy = py * (f32)neighbor_count - separation_y;
f32 sep_dz = pz * (f32)neighbor_count - separation_z;
f32 sep_len = sqrtf(sep_dx * sep_dx + sep_dy * sep_dy + sep_dz * sep_dz);
f32 sep_rx = 0, sep_ry = 0, sep_rz = 0;
if (sep_len > 0.0001f) {
f32 inv = BOID_SEPARATION_WEIGHT / sep_len;
sep_rx = sep_dx * inv; sep_ry = sep_dy * inv; sep_rz = sep_dz * inv;
}
// Target seeking
f32 target_dx = tgt_x - px, target_dy = tgt_y - py, target_dz = tgt_z - pz;
f32 target_len = sqrtf(target_dx * target_dx + target_dy * target_dy + target_dz * target_dz);
f32 target_rx = 0, target_ry = 0, target_rz = 0;
if (target_len > 0.0001f) {
f32 inv = BOID_TARGET_WEIGHT / target_len;
target_rx = target_dx * inv; target_ry = target_dy * inv; target_rz = target_dz * inv;
}
// Obstacle avoidance
f32 obs_dx = px - obs_x, obs_dy = py - obs_y, obs_dz = pz - obs_z;
f32 obs_len = sqrtf(obs_dx * obs_dx + obs_dy * obs_dy + obs_dz * obs_dz);
f32 avoid_hx = 0, avoid_hy = 0, avoid_hz = 0;
if (obs_len > 0.0001f) {
f32 inv = 1.0f / obs_len;
f32 nx = obs_dx * inv, ny = obs_dy * inv, nz = obs_dz * inv;
avoid_hx = (obs_x + nx * BOID_OBSTACLE_AVERSION_DISTANCE) - px;
avoid_hy = (obs_y + ny * BOID_OBSTACLE_AVERSION_DISTANCE) - py;
avoid_hz = (obs_z + nz * BOID_OBSTACLE_AVERSION_DISTANCE) - pz;
}
// Combined steering
f32 normal_x = align_rx + sep_rx + target_rx;
f32 normal_y = align_ry + sep_ry + target_ry;
f32 normal_z = align_rz + sep_rz + target_rz;
f32 normal_len = sqrtf(normal_x * normal_x + normal_y * normal_y + normal_z * normal_z);
if (normal_len > 0.0001f) {
f32 inv = 1.0f / normal_len;
normal_x *= inv; normal_y *= inv; normal_z *= inv;
} else {
normal_x = forward_x; normal_y = forward_y; normal_z = forward_z;
}
f32 target_fx, target_fy, target_fz;
if (nearest_obstacle_dist - BOID_OBSTACLE_AVERSION_DISTANCE < 0) {
target_fx = avoid_hx; target_fy = avoid_hy; target_fz = avoid_hz;
} else {
target_fx = normal_x; target_fy = normal_y; target_fz = normal_z;
}
f32 new_hx = forward_x + dt * (target_fx - forward_x);
f32 new_hy = forward_y + dt * (target_fy - forward_y);
f32 new_hz = forward_z + dt * (target_fz - forward_z);
f32 new_len = sqrtf(new_hx * new_hx + new_hy * new_hy + new_hz * new_hz);
if (new_len > 0.0001f) {
f32 inv = 1.0f / new_len;
new_hx *= inv; new_hy *= inv; new_hz *= inv;
}
f32 move_dist = BOID_MOVE_SPEED * dt;
positions[i].x = px + new_hx * move_dist;
positions[i].y = py + new_hy * move_dist;
positions[i].z = pz + new_hz * move_dist;
headings[i].x = new_hx;
headings[i].y = new_hy;
headings[i].z = new_hz;
}
}
HZ_ECS_SYSTEM(filter = BoidTag)
void build_matrices_system(EcsCtx *ctx,
const Position *positions,
const Heading *headings,
const BoidIndex *indices,
u32 count) {
for (u32 i = 0; i < count; i++) {
u32 idx = indices[i].index;
mat4 *model = &state.instance_data[idx];
vec3 pos = {positions[i].x, positions[i].y, positions[i].z};
vec3 dir = {headings[i].x, headings[i].y, headings[i].z};
quaternion heading_rot;
quat_look_at_dir(dir, heading_rot);
quaternion fish_orient;
quat_from_euler(VEC3(RAD(90), RAD(180), 0), fish_orient);
quaternion rot;
glm_quat_mul(heading_rot, fish_orient, rot);
vec3 scale = {0.01f, 0.01f, 0.01f};
mat_trs(pos, rot, scale, *model);
}
}
void app_init(AppMemory *memory) {
if (!is_main_thread()) return;
AppContext *app_ctx = app_ctx_current();
ecs_world_init(&state.world, &app_ctx->arena);
ECS_SYSTEM(&state.world, play_animations_system);
ECS_SYSTEM(&state.world, build_animated_transforms_system);
ECS_SYSTEM(&state.world, render_mesh_system);
ECS_SYSTEM(&state.world, insert_boids_system);
ECS_SYSTEM(&state.world, merge_cells_system);
ECS_SYSTEM(&state.world, steer_boids_system);
ECS_SYSTEM(&state.world, build_matrices_system);
state.input = input_init();
state.camera = camera_init(VEC3(0, 11.6, -0.4), VEC3(RAD(-1.066f), 0, 0), 60.0f);
renderer_init(&app_ctx->arena, app_ctx->num_threads,
(u32)memory->canvas_width, (u32)memory->canvas_height, 4);
asset_system_init(&state.assets, 64);
exported_scene_init(&state.assets);
state.ui = ui_init(&app_ctx->arena, &state.assets);
state.font = ui_load_font(&state.ui, &app_ctx->arena, "public/Roboto-Regular.hza");
state.fish_albedo_tex = gpu_make_texture("public/fishAlbedo2.png");
state.fish_tint_tex = gpu_make_texture("public/tints.png");
state.fish_metallic_gloss_tex = gpu_make_texture("public/fishMetallicGloss.png");
state.shark_albedo_tex = gpu_make_texture("public/SharkAlbedo.png");
state.shark_metallic_gloss_tex = gpu_make_texture("public/SharkMetallicGloss.png");
//TODO: need to add support for "run once" systems
//alternatively this could be split by all threads if were not early exiting above
f32 spawn_radius = 15.0f;
vec3 spawn_center = {20.0f, 5.0f, -120.0f};
for (i32 i = 0; i < NUM_BOIDS; i++) {
EcsEntity e = ecs_entity_new(&state.world);
UnityRandom rng = unity_random_new((u32)(i + 1) * 0x9F6ABC1u);
f32 rx = unity_random_next_f32(&rng) - 0.5f;
f32 ry = unity_random_next_f32(&rng) - 0.5f;
f32 rz = unity_random_next_f32(&rng) - 0.5f;
f32 len = sqrtf(rx * rx + ry * ry + rz * rz);
f32 hx = 0, hy = 1, hz = 0;
if (len > 0.0001f) {
f32 inv = 1.0f / len;
hx = rx * inv; hy = ry * inv; hz = rz * inv;
}
ecs_set(&state.world, e, Position, {
.x = spawn_center[0] + hx * spawn_radius,
.y = spawn_center[1] + hy * spawn_radius,
.z = spawn_center[2] + hz * spawn_radius
});
ecs_set(&state.world, e, Heading, {.x = hx, .y = hy, .z = hz});
ecs_set(&state.world, e, BoidIndex, {.index = (u32)i});
ecs_add(&state.world, e, BoidTag);
}
//TODO: (same as above)
const SampledAnimationClip *target_clips[NUM_TARGETS] = {&Target01_animation, &Target02_animation};
for (i32 i = 0; i < NUM_TARGETS; i++) {
EcsEntity e = ecs_entity_new(&state.world);
state.target_entities[i] = e;
vec3 initial_pos;
sample_animation_position(target_clips[i], 0.0f, initial_pos);
ecs_set(&state.world, e, Position, {.x = initial_pos[0], .y = initial_pos[1], .z = initial_pos[2]});
ecs_set(&state.world, e, AnimationPlayer, {
.clip = target_clips[i],
.current_time = 0.0f,
.dest_position = &state.target_positions[i],
});
glm_vec3_copy(initial_pos, state.target_positions[i]);
}
//TODO: (same as above)
for (i32 i = 0; i < NUM_OBSTACLES; i++) {
EcsEntity e = ecs_entity_new(&state.world);
state.obstacle_entities[i] = e;
vec3 initial_pos;
sample_animation_position(&Shark_animation, 0.0f, initial_pos);
ecs_set(&state.world, e, Position, {.x = initial_pos[0], .y = initial_pos[1], .z = initial_pos[2]});
ecs_set(&state.world, e, AnimationPlayer, {
.clip = &Shark_animation,
.current_time = 0.0f,
.dest_position = &state.obstacle_positions[i],
});
glm_vec3_copy(initial_pos, state.obstacle_positions[i]);
}
// TODO: reusable system for instanced rendering?
for (u32 i = 0; i < NUM_INSTANCE_BATCHES; i++) {
state.instance_buffers[i] = renderer_create_instance_buffer(&(InstanceBufferDesc){
.stride = sizeof(mat4),
.max_instances = INSTANCE_BATCH_SIZE,
});
}
LOG_INFO("Boids demo initialized: % boids", FMT_UINT(NUM_BOIDS));
}
void app_update_and_render(AppMemory *memory) {
state.total_time = memory->total_time;
// clear spatial hash
// TODO: PreUpdate/Update/PostUpdate systems?
Range_u64 range = lane_range(GRID_SIZE);
for (u64 i = range.min; i < range.max; i++) {
state.buckets[i].count = 0;
}
asset_system_update(&state.assets);
exported_scene_update(&state.assets);
if (is_main_thread()) {
input_update(&state.input, &memory->input_events, memory->total_time);
camera_update(&state.camera, memory->canvas_width, memory->canvas_height);
}
renderer_begin_frame(state.camera.view, state.camera.proj,
(GpuColor){2.0f / 255.0f, 94.0f / 255.0f, 131.0f / 255.0f, 1.0f},
memory->total_time);
ecs_progress(&state.world, memory->dt);
exported_scene_draw();
// draw boids
Range_u64 batch_range = lane_range(NUM_INSTANCE_BATCHES);
for (u64 batch = batch_range.min; batch < batch_range.max; batch++) {
u32 start = (u32)batch * INSTANCE_BATCH_SIZE;
u32 count = INSTANCE_BATCH_SIZE;
if (start + count > NUM_BOIDS) count = NUM_BOIDS - start;
renderer_update_instance_buffer(state.instance_buffers[batch], &state.instance_data[start], count);
renderer_draw_mesh_instanced(state.fish_mesh, state.fish_material, state.instance_buffers[batch]);
}
renderer_end_frame();
if (is_main_thread()) {
input_end_frame(&state.input);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment