397 lines
14 KiB
C
397 lines
14 KiB
C
#include "twn_draw_c.h"
|
|
#include "twn_draw.h"
|
|
#include "twn_engine_context_c.h"
|
|
#include "twn_util.h"
|
|
#include "twn_workers_c.h"
|
|
#include "twn_textures_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>
|
|
#include <SDL2/SDL.h>
|
|
|
|
/* TODO: it might make sense to have a separate path for really small models, collecting them together */
|
|
|
|
static struct ModelCacheItem {
|
|
char *key;
|
|
struct ModelCacheItemValue {
|
|
/* UncoloredSpaceTriangle to use indices against */
|
|
VertexBuffer vertices;
|
|
|
|
/* array or uint16_t or uint32_t, depending on length */
|
|
/* populated in such way that shared textures are combined into continuous range */
|
|
VertexBuffer *indices;
|
|
|
|
// /* note: this whole scheme only works without taking normals into account, but it's quite fast */
|
|
// struct ModelCacheIndexRange {
|
|
// Rect srcrect;
|
|
// size_t offset;
|
|
// size_t length;
|
|
// TextureKey texture;
|
|
// } *ranges;
|
|
|
|
/* cached base command, modified for ranges */
|
|
DeferredCommand *commands;
|
|
} 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 struct ModelLoadRequest {
|
|
char const *path;
|
|
fastObjMesh *mesh;
|
|
enum {
|
|
/* not yet started, only path is available */
|
|
MODEL_LOAD_REQUEST_WAITING,
|
|
/* initial load of data, unrelated to graphics state and thus applicable to running in worker threads */
|
|
MODEL_LOAD_REQUEST_LOADING,
|
|
/* mesh is loaded and awaits to be prepared and loaded onto gpu */
|
|
MODEL_LOAD_REQUEST_LOADED,
|
|
} stage;
|
|
} *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);
|
|
}
|
|
|
|
|
|
/* it's safe to access everything without lock after this returns true and no public api is possible to call */
|
|
bool models_load_workers_finished(void) {
|
|
bool result = true;
|
|
SDL_LockMutex(model_load_mutex);
|
|
for (size_t i = 0; i < arrlenu(model_load_queue); ++i) {
|
|
if (model_load_queue[i].stage != MODEL_LOAD_REQUEST_LOADED) {
|
|
result = false;
|
|
break;
|
|
}
|
|
}
|
|
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 models_load_workers_thread(void) {
|
|
/* attempt to grab something to work on */
|
|
char const *request_path = NULL;
|
|
ssize_t queue_index = -1;
|
|
SDL_LockMutex(model_load_mutex);
|
|
for (size_t i = 0; i < arrlenu(model_load_queue); ++i) {
|
|
if (model_load_queue[i].stage == MODEL_LOAD_REQUEST_WAITING) {
|
|
request_path = model_load_queue[i].path;
|
|
queue_index = i;
|
|
model_load_queue[i].stage = MODEL_LOAD_REQUEST_LOADING;
|
|
break;
|
|
}
|
|
}
|
|
SDL_UnlockMutex(model_load_mutex);
|
|
/* nothing to do, bail */
|
|
if (queue_index == -1)
|
|
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: would be nice if we could start dependency texture load immediately */
|
|
fastObjMesh *const mesh = fast_obj_read_with_callbacks(request_path, &callbacks, NULL);
|
|
|
|
SDL_LockMutex(model_load_mutex);
|
|
model_load_queue[queue_index].mesh = mesh;
|
|
model_load_queue[queue_index].stage = MODEL_LOAD_REQUEST_LOADED;
|
|
SDL_UnlockMutex(model_load_mutex);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void draw_model(const char *model,
|
|
Vec3 position,
|
|
Vec3 rotation,
|
|
Vec3 scale)
|
|
{
|
|
/* TODO: make this all work. */
|
|
SDL_assert_always(false);
|
|
|
|
/* if model is missing, queue it up for loading */
|
|
struct ModelCacheItem const *item;
|
|
/* reuse the key from model_cache */
|
|
char *modelcopy;
|
|
if (!(item = shgetp_null(model_cache, model))) {
|
|
modelcopy = SDL_strdup(model);
|
|
shput(model_cache, modelcopy, (struct ModelCacheItemValue) {0});
|
|
SDL_LockMutex(model_load_mutex);
|
|
struct ModelLoadRequest const request = {
|
|
.stage = MODEL_LOAD_REQUEST_WAITING,
|
|
.path = modelcopy,
|
|
};
|
|
arrpush(model_load_queue, request);
|
|
SDL_UnlockMutex(model_load_mutex);
|
|
workers_add_job();
|
|
} else
|
|
modelcopy = item->key;
|
|
|
|
struct ModelDrawCommand const command = {
|
|
.model = modelcopy,
|
|
.position = position,
|
|
.rotation = rotation,
|
|
.scale = scale
|
|
};
|
|
|
|
arrpush(model_draw_commands, command);
|
|
}
|
|
|
|
|
|
/* prepare vertex buffers before textures are ready */
|
|
void models_update_pre_textures(void) {
|
|
/* TODO: instead of waiting for all we could start uploading when there's something to upload */
|
|
/* it's unlikely to happen, but could improve the worst cases */
|
|
while (!models_load_workers_finished())
|
|
SDL_Delay(1);
|
|
|
|
/* TODO: it might be better to parallelize this part by sending buffer mappings to workers */
|
|
for (size_t i = 0; i < arrlenu(model_load_queue); ++i) {
|
|
fastObjMesh *const mesh = model_load_queue[i].mesh;
|
|
SDL_assert(mesh && model_load_queue[i].stage == MODEL_LOAD_REQUEST_LOADED);
|
|
struct ModelCacheItem *const item = shgetp(model_cache, model_load_queue[i].path);
|
|
|
|
/* calculate required vertex and index buffers */
|
|
/* texture specific index buffers, to later me merged */
|
|
uint32_t **indices = NULL;
|
|
arrsetlen(indices, mesh->texture_count);
|
|
SDL_memset(indices, 0, mesh->texture_count * sizeof (uint32_t *));
|
|
|
|
/* vertices are shared for all subcommands */
|
|
struct ModelVertex {
|
|
Vec3 position;
|
|
Vec2 uv;
|
|
} *vertices = NULL;
|
|
|
|
for (unsigned int o = 0; o < mesh->object_count; ++o) {
|
|
/* we assume that vertices are only shared within the same object, */
|
|
/* which allows us to keep hash table small in most cases */
|
|
/* it should work great for quake style brush based models */
|
|
struct ModelVertexIndexItem {
|
|
struct ModelVertexIndexItemKey {
|
|
uint32_t vertex_index;
|
|
uint32_t uv_index;
|
|
} key;
|
|
uint32_t value; /* index to vertex */
|
|
} *merge_hash = NULL;
|
|
|
|
fastObjGroup const *const object = &mesh->objects[o];
|
|
size_t idx = 0;
|
|
|
|
for (unsigned int f = 0; f < object->face_count; ++f) {
|
|
unsigned int const fv = mesh->face_vertices[object->face_offset + f];
|
|
unsigned int const mi = mesh->face_materials[object->face_offset + f];
|
|
/* TODO: handle missing */
|
|
fastObjMaterial const *const m = mesh->materials ? &mesh->materials[mi] : NULL;
|
|
|
|
/* unwrap polygon fans into triangles, first point is reused for all following */
|
|
fastObjIndex const i0 = mesh->indices[object->index_offset + idx];
|
|
ptrdiff_t i0_hash = hmgeti(merge_hash, ((struct ModelVertexIndexItemKey) { i0.p, i0.t }));
|
|
if (i0_hash == -1) {
|
|
hmput(merge_hash, ((struct ModelVertexIndexItemKey) { i0.p, i0.t }), arrlenu(vertices));
|
|
arrpush(vertices, ((struct ModelVertex) {
|
|
(Vec3) { mesh->positions[3 * i0.p + 0] / 64, mesh->positions[3 * i0.p + 1] / 64, mesh->positions[3 * i0.p + 2] / 64 },
|
|
(Vec2) { mesh->texcoords[2 * i0.t + 0], mesh->texcoords[2 * i0.t + 1] }
|
|
}));
|
|
i0_hash = hmlen(merge_hash) - 1;
|
|
// i0_hash = arrlenu(vertices) - 1;
|
|
}
|
|
|
|
/* other fan points over shifting by 1 window */
|
|
for (unsigned int t = 0; t < fv - 2; ++t) {
|
|
fastObjIndex const i1 = mesh->indices[object->index_offset + idx + 1 + t];
|
|
ptrdiff_t i1_hash = hmgeti(merge_hash, ((struct ModelVertexIndexItemKey) { i1.p, i1.t }));
|
|
if (i1_hash == -1) {
|
|
hmput(merge_hash, ((struct ModelVertexIndexItemKey) { i1.p, i1.t }), arrlenu(vertices));
|
|
arrpush(vertices, ((struct ModelVertex) {
|
|
(Vec3) { mesh->positions[3 * i1.p + 0] / 64, mesh->positions[3 * i1.p + 1] / 64, mesh->positions[3 * i1.p + 2] / 64 },
|
|
(Vec2) { mesh->texcoords[2 * i1.t + 0], mesh->texcoords[2 * i1.t + 1] }
|
|
}));
|
|
i1_hash = hmlen(merge_hash) - 1;
|
|
// i1_hash = arrlenu(vertices) - 1;
|
|
}
|
|
|
|
fastObjIndex const i2 = mesh->indices[object->index_offset + idx + 2 + t];
|
|
ptrdiff_t i2_hash = hmgeti(merge_hash, ((struct ModelVertexIndexItemKey) { i2.p, i2.t }));
|
|
if (i2_hash == -1) {
|
|
hmput(merge_hash, ((struct ModelVertexIndexItemKey) { i2.p, i2.t }), arrlenu(vertices));
|
|
arrpush(vertices, ((struct ModelVertex) {
|
|
(Vec3) { mesh->positions[3 * i2.p + 0] / 64, mesh->positions[3 * i2.p + 1] / 64, mesh->positions[3 * i2.p + 2] / 64 },
|
|
(Vec2) { mesh->texcoords[2 * i2.t + 0], mesh->texcoords[2 * i2.t + 1] }
|
|
}));
|
|
i2_hash = hmlen(merge_hash) - 1;
|
|
// i2_hash = arrlenu(vertices) - 1;
|
|
}
|
|
|
|
arrpush(indices[m->map_Kd], (uint32_t)i0_hash);
|
|
arrpush(indices[m->map_Kd], (uint32_t)i1_hash);
|
|
arrpush(indices[m->map_Kd], (uint32_t)i2_hash);
|
|
}
|
|
|
|
idx += fv;
|
|
}
|
|
|
|
hmfree(merge_hash);
|
|
}
|
|
|
|
if (mesh->color_count != 0)
|
|
log_warn("TODO: color in models isn't yet supported");
|
|
|
|
/* upload vertices */
|
|
VertexBuffer vertex_buffer = create_vertex_buffer();
|
|
specify_vertex_buffer(vertex_buffer, vertices, arrlenu(vertices) * sizeof (struct ModelVertex));
|
|
item->value.vertices = vertex_buffer;
|
|
|
|
/* collect texture usages into index ranges */
|
|
/* TODO: force repeating texture upload before its used in drawing */
|
|
for (size_t t = 0; t < arrlenu(indices); ++t) {
|
|
VertexBuffer index_buffer = create_vertex_buffer();
|
|
specify_vertex_buffer(index_buffer, indices[t], arrlenu(indices[i]) * sizeof (uint32_t));
|
|
arrpush(item->value.indices, index_buffer);
|
|
|
|
/* build command */
|
|
DeferredCommandDraw command = {0};
|
|
|
|
command.vertices = (AttributeArrayPointer) {
|
|
.arity = 3,
|
|
.type = TWN_FLOAT,
|
|
.stride = sizeof (struct ModelVertex),
|
|
.offset = offsetof (struct ModelVertex, position),
|
|
.buffer = vertex_buffer
|
|
};
|
|
|
|
command.texcoords = (AttributeArrayPointer) {
|
|
.arity = 2,
|
|
.type = TWN_FLOAT,
|
|
.stride = sizeof (struct ModelVertex),
|
|
.offset = offsetof (struct ModelVertex, uv),
|
|
.buffer = vertex_buffer
|
|
};
|
|
|
|
TextureKey const texture_key = textures_get_key(&ctx.texture_cache, mesh->textures[t].name);
|
|
|
|
command.textured = true;
|
|
command.texture_key = texture_key;
|
|
command.texture_repeat = true;
|
|
|
|
command.element_buffer = index_buffer;
|
|
command.element_count = (uint32_t)(arrlenu(indices[t]));
|
|
command.range_end = (uint32_t)(arrlenu(indices[t]));
|
|
|
|
/* TODO: support alpha blended case? */
|
|
TextureMode mode = textures_get_mode(&ctx.texture_cache, texture_key);
|
|
if (mode == TEXTURE_MODE_GHOSTLY)
|
|
mode = TEXTURE_MODE_SEETHROUGH;
|
|
|
|
command.texture_mode = mode;
|
|
command.pipeline = PIPELINE_SPACE;
|
|
|
|
command.depth_range_high = depth_range_high;
|
|
command.depth_range_low = depth_range_low;
|
|
|
|
DeferredCommand final_command = {
|
|
.type = DEFERRED_COMMAND_TYPE_DRAW,
|
|
.draw = command
|
|
};
|
|
|
|
arrpush(item->value.commands, final_command);
|
|
|
|
arrfree(indices[i]);
|
|
}
|
|
|
|
arrfree(vertices);
|
|
arrfree(indices);
|
|
|
|
/* TODO: sort ranges based on length in assumption that bigger mesh parts will occlude more */
|
|
}
|
|
}
|
|
|
|
|
|
/* adjust uvs into atlases when needed */
|
|
void models_update_post_textures(void) {
|
|
SDL_assert(!ctx.texture_cache.is_dirty);
|
|
|
|
arrsetlen(model_load_queue, 0);
|
|
}
|
|
|
|
|
|
void finally_draw_models(void) {
|
|
for (int i = 0; i < arrlen(model_draw_commands); ++i) {
|
|
struct ModelDrawCommand *const command = &model_draw_commands[i];
|
|
struct ModelCacheItem *const cache = shgetp(model_cache, command->model);
|
|
for (int c = 0; c < arrlen(cache->value.commands); ++c) {
|
|
arrpush(deferred_commands, cache->value.commands[c]);
|
|
}
|
|
}
|
|
|
|
arrsetlen(model_draw_commands, 0);
|
|
}
|
|
|
|
|
|
/* drop model caches */
|
|
void free_model_cache(void) {
|
|
for (size_t i = 0; i < shlenu(model_cache); ++i) {
|
|
// fast_obj_destroy(model_cache[i].value.mesh);
|
|
SDL_free(model_cache[i].key);
|
|
}
|
|
|
|
shfree(model_cache);
|
|
}
|
|
|
|
|
|
void models_state_init(void) {
|
|
if (model_load_initialized)
|
|
return;
|
|
model_load_mutex = SDL_CreateMutex();
|
|
model_load_initialized = true;
|
|
}
|
|
|
|
|
|
void models_state_deinit(void) {
|
|
if (!model_load_initialized)
|
|
return;
|
|
free_model_cache();
|
|
arrfree(model_load_queue);
|
|
SDL_DestroyMutex(model_load_mutex);
|
|
model_load_initialized = false;
|
|
}
|