wip model loading + workers

This commit is contained in:
veclavtalica
2025-02-07 10:19:36 +03:00
parent cb88b4bcc5
commit 7040d6f218
12 changed files with 1881 additions and 2 deletions

View File

@ -393,7 +393,10 @@ static void render_2d(void) {
}
/* TODO: benchmark which order works best for expected cases */
static void render_space(void) {
finally_draw_models();
/* nothing to do, abort */
/* as space pipeline isn't used we can have fewer changes and initialization costs */
if (hmlenu(ctx.uncolored_mesh_batches) != 0 || hmlenu(ctx.billboard_batches) != 0) {

View File

@ -132,7 +132,7 @@ typedef struct MeshBatch {
/* TODO: use atlas id instead */
typedef struct MeshBatchItem {
TextureKey key;
struct TextureKey key;
struct MeshBatch value;
} MeshBatchItem;
@ -339,4 +339,7 @@ void finally_draw_command(DeferredCommandDraw command);
void issue_deferred_draw_commands(void);
bool model_load_workers_thread(void);
void finally_draw_models(void);
#endif

170
src/rendering/twn_model.c Normal file
View File

@ -0,0 +1,170 @@
#include "twn_draw_c.h"
#include "twn_draw.h"
#include "twn_util.h"
#include "twn_workers_c.h"
#define FAST_OBJ_IMPLEMENTATION
#define FAST_OBJ_REALLOC SDL_realloc
#define FAST_OBJ_FREE SDL_free
#include <fast_obj.h>
#include <stb_ds.h>
#include <physfs.h>
#include <physfsrwops.h>
static struct ModelCacheItem {
char const *key;
struct ModelCacheItemValue {
fastObjMesh *mesh;
} value;
} *model_cache;
/* TODO: store index to model cache instead */
static struct ModelDrawCommand {
char *model;
Vec3 position;
Vec3 rotation;
Vec3 scale;
} *model_draw_commands;
/* deferred queue of model files to load from worker threads */
static SDL_mutex *model_load_mutex;
static char **model_load_queue;
static bool model_load_initialized;
/* use streaming via callbacks to reduce memory congestion */
static void model_load_callback_close(void *handle, void *udata) {
(void)udata;
((SDL_RWops *)handle)->close(handle);
}
static void *model_load_callback_open(const char *path, void *udata) {
(void)udata;
return PHYSFSRWOPS_openRead(path);
}
static size_t model_load_callback_read(void *handle, void *dst, size_t bytes, void *udata) {
(void)udata;
return ((SDL_RWops *)handle)->read(handle, dst, 1, bytes);
}
static unsigned long model_load_callback_size(void *handle, void *udata) {
(void)udata;
return ((SDL_RWops *)handle)->size(handle);
}
/* TODO: is there a way to do this nicely while locking main thread? */
/* sleeping over atomic counter might be good enough i guess */
static bool model_load_workers_finished(void) {
bool result;
SDL_LockMutex(model_load_mutex);
result = arrlenu(model_load_queue) == 0;
SDL_UnlockMutex(model_load_mutex);
return result;
}
/* entry point for workers, polled every time a job semaphore is posted */
/* returns false if there was nothing to do */
bool model_load_workers_thread(void) {
/* attempt to grab something to work on */
char *load_request = NULL;
SDL_LockMutex(model_load_mutex);
if (arrlenu(model_load_queue) != 0)
load_request = arrpop(model_load_queue);
SDL_UnlockMutex(model_load_mutex);
/* nothing to do, bail */
if (!load_request)
return false;
fastObjCallbacks const callbacks = {
.file_close = model_load_callback_close,
.file_open = model_load_callback_open,
.file_read = model_load_callback_read,
.file_size = model_load_callback_size
};
/* TODO: immediately create jobs for missing textures */
fastObjMesh *mesh = fast_obj_read_with_callbacks(load_request, &callbacks, NULL);
SDL_LockMutex(model_load_mutex);
struct ModelCacheItem *item = shgetp(model_cache, load_request);
item->value.mesh = mesh;
SDL_UnlockMutex(model_load_mutex);
return true;
}
void draw_model(const char *model,
Vec3 position,
Vec3 rotation,
Vec3 scale)
{
if (!model_load_initialized) {
model_load_mutex = SDL_CreateMutex();
model_load_initialized = true;
}
/* make sure not to reference parameter longer than duration of this function */
char *model_copy = SDL_strdup(model);
model = NULL; /* trap */
struct ModelDrawCommand const command = {
.model = model_copy,
.position = position,
.rotation = rotation,
.scale = scale
};
arrpush(model_draw_commands, command);
/* if model is missing, queue it up for loading */
SDL_LockMutex(model_load_mutex);
if (!(shgetp_null(model_cache, model_copy))) {
shput(model_cache, model_copy, (struct ModelCacheItemValue){0});
arrpush(model_load_queue, model_copy);
SDL_SemPost(workers_job_semaphore);
}
SDL_UnlockMutex(model_load_mutex);
}
void finally_draw_models(void) {
while (!model_load_workers_finished()) {
(void)0;
}
/* TODO: have special path for them, preserving the buffers and potentially using instanced draw */
for (int i = 0; i < arrlen(model_draw_commands); ++i) {
struct ModelDrawCommand const *const command = &model_draw_commands[i];
fastObjMesh const *const mesh = model_cache[shgeti(model_cache, command->model)].value.mesh;
for (unsigned int g = 0; g < mesh->group_count; ++g) {
fastObjGroup const *const group = &mesh->groups[g];
unsigned int idx = 0;
for (unsigned int f = 0; f < group->face_count; ++f) {
unsigned int const vertices = mesh->face_vertices[group->face_offset + f];
// fastObjTexture const *const texture = &mesh->textures[group->face_offset + f];
// log_info("material: %s", material->name);
/* TODO: support arbitrary fans */
SDL_assert(vertices == 4);
fastObjIndex i0 = mesh->indices[group->index_offset + idx + 0];
fastObjIndex i1 = mesh->indices[group->index_offset + idx + 1];
fastObjIndex i2 = mesh->indices[group->index_offset + idx + 2];
fastObjIndex i3 = mesh->indices[group->index_offset + idx + 3];
draw_quad(
"asd",
(Vec3) { mesh->positions[3 * i0.p + 0], mesh->positions[3 * i0.p + 1], mesh->positions[3 * i0.p + 2] },
(Vec3) { mesh->positions[3 * i1.p + 0], mesh->positions[3 * i1.p + 1], mesh->positions[3 * i1.p + 2] },
(Vec3) { mesh->positions[3 * i2.p + 0], mesh->positions[3 * i2.p + 1], mesh->positions[3 * i2.p + 2] },
(Vec3) { mesh->positions[3 * i3.p + 0], mesh->positions[3 * i3.p + 1], mesh->positions[3 * i3.p + 2] },
(Rect) { .w = 64, .h = 64 },
(Color) { 255, 255, 255, 255 }
);
idx += vertices;
}
}
}
arrsetlen(model_draw_commands, 0);
}

View File

@ -11,6 +11,7 @@
#include "twn_util.c"
#include "twn_filewatch.c"
#include "twn_timer.c"
#include "twn_workers.c"
#include "rendering/twn_circles.c"
#include "rendering/twn_draw.c"
@ -21,3 +22,4 @@
#include "rendering/twn_quads.c"
#include "rendering/twn_triangles.c"
#include "rendering/twn_billboards.c"
#include "rendering/twn_model.c"

View File

@ -7,6 +7,7 @@
#include "twn_game_object_c.h"
#include "twn_textures_c.h"
#include "twn_timer_c.h"
#include "twn_workers_c.h"
#include <SDL2/SDL.h>
#include <physfs.h>
@ -724,7 +725,7 @@ static void clean_up(void) {
toml_free(ctx.config_table);
PHYSFS_deinit();
workers_deinit();
SDL_free(ctx.base_dir);
SDL_free(ctx.title);
SDL_GL_DeleteContext(ctx.gl_context);
@ -861,6 +862,7 @@ int enter_loop(int argc, char **argv) {
ctx.game.initialization_needed = true;
SDL_InitSubSystem(SDL_INIT_EVENTS);
workers_init(SDL_GetCPUCount());
profile_end("startup");

69
src/twn_workers.c Normal file
View File

@ -0,0 +1,69 @@
#include "twn_util.h"
#include "twn_workers_c.h"
#include "rendering/twn_draw_c.h"
SDL_sem *workers_job_semaphore;
static SDL_Thread *workers_pool[MAX_WORKERS];
static size_t workers_pool_size;
static SDL_mutex *workers_mutex;
static bool workers_should_exit;
/* logic is such that when job is posted, worker threads attempt to grab it from any possible entry point */
/* if it did something, which is signaled by `true` return, go back to waiting on semaphore, so that it's decremented properly */
static int worker_thread(void *udata) {
(void)udata;
while (true) {
/* check whether loop should end */
SDL_LockMutex(workers_mutex);
if (workers_should_exit) {
SDL_UnlockMutex(workers_mutex);
break;
}
SDL_UnlockMutex(workers_mutex);
/* wait and occasionally go back to check whether it all should end */
if (SDL_SemWaitTimeout(workers_job_semaphore, 100) == SDL_MUTEX_TIMEDOUT)
continue;
if (model_load_workers_thread())
continue;
}
return 0;
}
/* TODO: have a path for platforms without thread support? */
/* TODO: limit stack size? */
bool workers_init(size_t worker_count) {
SDL_assert(workers_pool_size == 0);
if (worker_count > MAX_WORKERS)
worker_count = MAX_WORKERS;
/* spawn a bunch of detached threads without references to them */
for (size_t i = 0; i < worker_count; ++i) {
SDL_Thread *thread = SDL_CreateThread(worker_thread, "worker", NULL);
SDL_assert_always(thread);
SDL_DetachThread(thread);
}
workers_pool_size = worker_count;
workers_job_semaphore = SDL_CreateSemaphore(0);
workers_mutex = SDL_CreateMutex();
return true;
}
void workers_deinit(void) {
SDL_LockMutex(workers_mutex);
workers_should_exit = true;
SDL_UnlockMutex(workers_mutex);
/* TODO: that's not correct */
SDL_DestroyMutex(workers_mutex);
SDL_DestroySemaphore(workers_job_semaphore);
workers_pool_size = 0;
}

17
src/twn_workers_c.h Normal file
View File

@ -0,0 +1,17 @@
#ifndef TWN_WORKERS_C_H
#define TWN_WORKERS_C_H
#include <SDL2/SDL.h>
#include <stdbool.h>
#define MAX_WORKERS 9
/* workers are waiting on this, increment this value when some work needs to be done */
/* for now every possible job path is hardcoded in twn_workers.c itself */
extern SDL_sem *workers_job_semaphore;
bool workers_init(size_t worker_count);
void workers_deinit(void);
#endif