wip multithreaded texture load
This commit is contained in:
@ -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,
|
||||
|
Reference in New Issue
Block a user