wip multithreaded texture load

This commit is contained in:
veclavtalica
2025-02-09 07:34:16 +03:00
parent 037548436d
commit 5a7d7433d1
5 changed files with 158 additions and 52 deletions

View File

@ -2,6 +2,7 @@
#include "twn_util.h"
#include "twn_util_c.h"
#include "twn_engine_context_c.h"
#include "twn_workers_c.h"
#include <SDL2/SDL.h>
#include <physfs.h>
@ -19,6 +20,18 @@ typedef struct {
} TextureLoadingContext;
static SDL_mutex *textures_load_mutex;
static struct TextureLoadRequest {
Texture result; /* will be copied into cache when it's time, freeing us from locking */
size_t index; /* index into cache->hash to fill */
/* 0 = awaits processing */
/* 1 = in processing */
/* 2 = success */
/* 3 = failure */
uint8_t status;
} *texture_load_queue;
static int load_read_callback(void *user, char *data, int size) {
TextureLoadingContext *context = user;
int read = (int)SDL_RWread(context->rwops, data, 1, size);
@ -42,7 +55,7 @@ static int load_eof_callback(void *user) {
static SDL_Surface *missing_texture_surface;
static uint16_t missing_texture_id;
static uint16_t missing_texture_id = TEXTURE_KEY_INVALID.id;
static SDL_Surface *gen_missing_texture_surface(void) {
Uint32 rmask, gmask, bmask;
@ -57,6 +70,8 @@ static SDL_Surface *gen_missing_texture_surface(void) {
bmask = 0x00ff0000;
#endif
SDL_LockMutex(textures_load_mutex);
if (!missing_texture_surface) {
uint8_t *data = SDL_malloc(64 * 64 * 3);
for (int y = 0; y < 64; ++y) {
@ -74,6 +89,8 @@ static SDL_Surface *gen_missing_texture_surface(void) {
rmask, gmask, bmask, 0);
}
SDL_UnlockMutex(textures_load_mutex);
return missing_texture_surface;
}
@ -230,10 +247,17 @@ static void recreate_current_atlas_texture(TextureCache *cache) {
/* uses the textures currently in the cache to create an array of stbrp_rects */
static stbrp_rect *create_rects_from_cache(TextureCache *cache) {
stbrp_rect *rects = NULL;
bool missing_texture_used = false;
for (size_t i = 0; i < shlenu(cache->hash); ++i) {
if (cache->hash[i].value.loner_texture != 0)
continue;
/* only put it once */
if (!missing_texture_used && cache->hash[i].value.data == missing_texture_surface) {
missing_texture_used = true;
continue;
}
const SDL_Surface *surface_data = cache->hash[i].value.data;
stbrp_rect new_rect = {
.w = surface_data->w,
@ -318,6 +342,7 @@ void textures_cache_init(TextureCache *cache, SDL_Window *window) {
sh_new_arena(cache->hash);
cache->node_buffer = SDL_malloc(ctx.texture_atlas_size * sizeof *cache->node_buffer);
textures_load_mutex = SDL_CreateMutex();
add_new_atlas(cache);
}
@ -346,6 +371,9 @@ void textures_cache_deinit(TextureCache *cache) {
}
shfree(cache->hash);
SDL_DestroyMutex(textures_load_mutex);
arrfree(texture_load_queue);
SDL_free(cache->node_buffer);
}
@ -377,49 +405,90 @@ static enum TextureMode infer_texture_mode(SDL_Surface *surface) {
return result;
}
/* offloads surface load and transparency detection from main thread */
bool textures_load_workers_thread(void) {
/* try grabbing some work */
ssize_t texture_id = -1;
ssize_t queue_index = -1;
char *path = NULL; /* copy of a key, as it's not stable in arena */
static TextureKey textures_load(TextureCache *cache, const char *path) {
/* no need to do anything if it was loaded already */
const ptrdiff_t i = shgeti(cache->hash, path);
if (i >= 0)
return (TextureKey){ (uint16_t)i };
SDL_LockMutex(textures_load_mutex);
for (size_t i = 0; i < arrlenu(texture_load_queue); ++i) {
if (texture_load_queue[i].status == 0) {
texture_id = texture_load_queue[i].index;
path = SDL_strdup(ctx.texture_cache.hash[texture_id].key);
texture_load_queue[i].status = 1; /* mark as in process */
queue_index = i;
break;
}
}
SDL_UnlockMutex(textures_load_mutex);
/* nothing to do, bail */
if (queue_index == -1)
return false;
SDL_Surface *surface = textures_load_surface(path);
if (surface == missing_texture_surface && missing_texture_id != 0)
return (TextureKey){ missing_texture_id };
SDL_assert(texture_id != -1 && queue_index != -1);
Texture new_texture = {
SDL_Surface *const surface = textures_load_surface(path);
SDL_free(path);
Texture const response = {
.data = surface,
.mode = infer_texture_mode(surface),
};
/* it's a "loner texture," it doesn't fit in an atlas so it's not in one */
if (surface->w >= (int)ctx.texture_atlas_size || surface->h >= (int)ctx.texture_atlas_size) {
if (ctx.game.debug) {
if (surface->w > 2048 || surface->h > 2048)
log_warn("Unportable texture dimensions for %s, use 2048x2048 at max", path);
if (!is_power_of_two(surface->w) || !is_power_of_two(surface->h))
log_warn("Unportable texture dimensions for %s, should be powers of 2", path);
SDL_LockMutex(textures_load_mutex);
texture_load_queue[queue_index].result = response;
texture_load_queue[queue_index].status = 2; /* mark success */
/* reuse this id in the future, allowing for draw call merging */
if (surface == missing_texture_surface && missing_texture_id == TEXTURE_KEY_INVALID.id)
missing_texture_id = (uint16_t)texture_id;
SDL_UnlockMutex(textures_load_mutex);
return true;
}
static TextureKey textures_load(TextureCache *cache, const char *path) {
/* at this point we assume that texture isn't loaded */
/* place a dummy for future lookups to know it will be loaded */
/* as well as a place for worker to fill in */
shput(cache->hash, path, (Texture){0});
/* append a new request, use stable indices */
struct TextureLoadRequest const request = {
.index = shlenu(cache->hash) - 1,
};
SDL_LockMutex(textures_load_mutex);
arrpush(texture_load_queue, request);
SDL_UnlockMutex(textures_load_mutex);
/* signal work to do */
SDL_SemPost(workers_job_semaphore);
cache->is_dirty = true;
/* report the newly created slot */
return (TextureKey){ (uint16_t)shlenu(cache->hash) - 1 };
}
/* it's safe to access everything without lock after this returns true and no public api is possible to call */
static bool textures_load_workers_finished(void) {
bool result = true;
SDL_LockMutex(textures_load_mutex);
for (size_t i = 0; i < arrlenu(texture_load_queue); ++i) {
if (texture_load_queue[i].status == 0 || texture_load_queue[i].status == 1) {
result = false;
break;
}
new_texture.loner_texture = create_gpu_texture(TEXTURE_FILTER_NEAREAST, true);
upload_texture_from_surface(new_texture.loner_texture, surface);
new_texture.srcrect = (Rect) { .w = (float)surface->w, .h = (float)surface->h };
} else {
/* will be fully populated as the atlas updates */
new_texture.atlas_index = cache->atlas_index;
cache->is_dirty = true;
}
shput(cache->hash, path, new_texture);
uint16_t const id = (uint16_t)shlenu(cache->hash) - 1;
/* reuse this id for every later missing texture */
if (surface == missing_texture_surface)
missing_texture_id = id;
return (TextureKey){ id };
SDL_UnlockMutex(textures_load_mutex);
return result;
}
@ -427,6 +496,37 @@ void textures_update_atlas(TextureCache *cache) {
if (!cache->is_dirty)
return;
while (!textures_load_workers_finished())
SDL_Delay(1);
/* collect results */
for (size_t i = 0; i < arrlenu(texture_load_queue); ++i) {
SDL_assert(texture_load_queue[i].status == 2);
Texture response = texture_load_queue[i].result;
/* it's a "loner texture," it doesn't fit in an atlas so it's not in one */
if (response.data->w >= (int)ctx.texture_atlas_size || response.data->h >= (int)ctx.texture_atlas_size) {
if (ctx.game.debug) {
if (response.data->w > 2048 || response.data->h > 2048)
log_warn("Unportable texture dimensions for %s, use 2048x2048 at max", cache->hash[texture_load_queue[i].index].key);
if (!is_power_of_two(response.data->w) || !is_power_of_two(response.data->h))
log_warn("Unportable texture dimensions for %s, should be powers of 2", cache->hash[texture_load_queue[i].index].key);
}
response.loner_texture = create_gpu_texture(TEXTURE_FILTER_NEAREAST, true);
upload_texture_from_surface(response.loner_texture, response.data);
response.srcrect = (Rect) { .w = (float)response.data->w, .h = (float)response.data->h };
} else {
/* will be fully populated as the atlas updates */
response.atlas_index = cache->atlas_index;
}
cache->hash[texture_load_queue[i].index].value = response;
}
arrsetlen(texture_load_queue, 0);
/* this function makes a lot more sense if you read stb_rect_pack.h */
stbrp_context pack_ctx; /* target info */
stbrp_init_target(&pack_ctx,