From a79ee429b486a32d279259b2183e2521f8f8eb5a Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Mon, 1 Jun 2026 10:57:40 -0500 Subject: [PATCH] Prepping for async --- src/dusk/asset/asset.c | 169 +++++++++++--- src/dusk/asset/asset.h | 15 +- src/dusk/asset/assetloader.h | 48 ---- src/dusk/asset/loader/assetentry.c | 2 +- src/dusk/asset/loader/assetentry.h | 4 +- src/dusk/asset/loader/assetloader.h | 28 ++- src/dusk/asset/loader/assetloading.h | 3 + .../asset/loader/display/assetmeshloader.c | 50 ++-- .../asset/loader/display/assettextureloader.c | 17 +- .../asset/loader/display/assettilesetloader.c | 41 +++- src/dusk/asset/loader/json/assetjsonloader.c | 28 ++- .../asset/loader/locale/assetlocaleloader.c | 10 +- src/dusk/display/text/text.c | 1 + src/dusk/error/error.c | 5 +- src/dusk/error/error.h | 47 ++-- src/dusk/thread/thread.h | 1 + src/dusk/thread/threadlocal.h | 17 ++ test/CMakeLists.txt | 2 + test/error/CMakeLists.txt | 9 + test/error/test_error.c | 187 +++++++++++++++ test/thread/CMakeLists.txt | 9 + test/thread/test_thread.c | 216 ++++++++++++++++++ 22 files changed, 745 insertions(+), 164 deletions(-) delete mode 100644 src/dusk/asset/assetloader.h create mode 100644 src/dusk/thread/threadlocal.h create mode 100644 test/error/CMakeLists.txt create mode 100644 test/error/test_error.c create mode 100644 test/thread/CMakeLists.txt create mode 100644 test/thread/test_thread.c diff --git a/src/dusk/asset/asset.c b/src/dusk/asset/asset.c index 7c3d75d0..1432914d 100644 --- a/src/dusk/asset/asset.c +++ b/src/dusk/asset/asset.c @@ -11,15 +11,23 @@ #include "assert/assert.h" #include "engine/engine.h" #include "util/string.h" +#include "console/console.h" +#include asset_t ASSET; errorret_t assetInit(void) { memoryZero(&ASSET, sizeof(asset_t)); + for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) { + threadMutexInit(&ASSET.loading[i].mutex); + } + // assetInitPlatform must either define ASSET.zip or throw an error. errorChain(assetInitPlatform()); assertNotNull(ASSET.zip, "Asset zip null without error."); + threadInit(&ASSET.loadThread, assetUpdateAsync); + threadStart(&ASSET.loadThread); errorOk(); } @@ -88,68 +96,120 @@ errorret_t assetRequireLoaded(assetentry_t *entry) { } errorret_t assetUpdate(void) { - // Is there any pending entries? - assetentry_t *entry = ASSET.entries; - assetloading_t *loading; + // Determine how many available loading slots we have. + assetloading_t *availableLoading[ASSET_LOADING_COUNT_MAX]; + uint8_t availableLoadingCount = 0; + assetloading_t *loading = ASSET.loading; + assetentry_t *entry; + + do { - // Is this asset "ready to start loading" ? - if(entry->type == ASSET_LOADER_TYPE_NULL) { - entry++; + // We only care about NULL entry references. Nothing async touches this so + // it's fine to use raw here. + if(loading->entry != NULL) { + loading++; continue; } + availableLoading[availableLoadingCount++] = loading; + loading++; + } while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX); - if(entry->state != ASSET_ENTRY_STATE_NOT_STARTED) { - entry++; - continue; - } - // Yes, this is ready to load, but we need to see if we have a loading slot - loading = ASSET.loading; - bool_t found = false; + // Now we can check for pending asset entries, we can't do anything if there + // is no available slots though. + if(availableLoadingCount > 0) { + entry = ASSET.entries; do { - if(loading->type != ASSET_LOADER_TYPE_NULL) { - loading++; + // Is this asset "ready to start loading" ? + if(entry->type == ASSET_LOADER_TYPE_NULL) { + entry++; continue; } - found = true; - break; - } while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX); + // We only care about assets not started. + if(entry->state != ASSET_ENTRY_STATE_NOT_STARTED) { + entry++; + continue; + } - if(!found) { - // No loading slot, try again next frame. + // Pop a loading slot for this asset entry. + loading = availableLoading[--availableLoadingCount]; + + // Start loading this asset. + assetEntryStartLoading(entry, loading); entry++; - continue; - } - // Start loading this asset. - assetEntryStartLoading(entry, loading); - entry++; - } while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX); + // Did we run out of loading slots? + if(availableLoadingCount == 0) { + break; + } + } while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX); + } - // At this point we have to see the state of all the loading assets. + // Now walk over all the loading slots and see what needs to be done. loading = ASSET.loading; do { + // Is the loading slot in use? Entry can only be modified synchronously. if(loading->entry == NULL) { loading++; continue; } + // Lock the loading slot. This will prevent any async modifications. + threadMutexLock(&loading->mutex); + + // Check the state of the entry. switch(loading->entry->state) { + // This thing is pending synchronous loading. case ASSET_ENTRY_STATE_PENDING_SYNC: - // Begin sync loading + // Perform sync load. loading->entry->state = ASSET_ENTRY_STATE_LOADING_SYNC; - errorret_t ret = ASSET_LOADER_CALLBACKS[loading->type].loadSync(loading); + errorret_t ret = ( + ASSET_LOADER_CALLBACKS[loading->type].loadSync(loading) + ); + + // After a sync load, these are the only valid states. + assertTrue( + loading->entry->state == ASSET_ENTRY_STATE_LOADED || + loading->entry->state == ASSET_ENTRY_STATE_ERROR || + loading->entry->state == ASSET_ENTRY_STATE_PENDING_SYNC || + loading->entry->state == ASSET_ENTRY_STATE_PENDING_ASYNC, + "Loader did not set entry state to loaded or error on finished load." + ); + + // If an error occured these things need to be true, basically just + // ensuring the sync loader is setting the error correctly. if(ret.code != ERROR_OK) { - loading->entry->state = ASSET_ENTRY_STATE_ERROR; - return ret; + assertTrue( + loading->entry->state == ASSET_ENTRY_STATE_ERROR, + "Loader did not set entry state to error on failed load." + ); } - loading->entry->state = ASSET_ENTRY_STATE_LOADED; + threadMutexUnlock(&loading->mutex); loading++; break; + case ASSET_ENTRY_STATE_LOADING_SYNC: + assertUnreachable( + "Entry is in a pending sync state still?" + ); + break; + + // Done loading, we can just free it up. + case ASSET_ENTRY_STATE_LOADED: + loading->entry = NULL; + threadMutexUnlock(&loading->mutex); + loading++; + break; + + case ASSET_ENTRY_STATE_ERROR: + threadMutexUnlock(&loading->mutex); + errorThrow("Failed to load asset asynchronously."); + break; + default: + threadMutexUnlock(&loading->mutex); loading++; continue; } @@ -157,7 +217,52 @@ errorret_t assetUpdate(void) { errorOk(); } +void assetUpdateAsync(thread_t *thread) { + while(!threadShouldStop(thread)) { + // Walk over each asset + assetloading_t *loading; + loading = ASSET.loading; + + do { + threadMutexLock(&loading->mutex); + + if(loading->entry == NULL) { + threadMutexUnlock(&loading->mutex); + loading++; + continue; + } + + switch(loading->entry->state) { + case ASSET_ENTRY_STATE_PENDING_ASYNC: + loading->entry->state = ASSET_ENTRY_STATE_LOADING_ASYNC; + printf("Simulated async load\n"); + sleep(1); + threadMutexUnlock(&loading->mutex); + loading++; + break; + + case ASSET_ENTRY_STATE_LOADING_ASYNC: + assertUnreachable( + "Entry is in a pending async state still?" + ); + break; + + default: + threadMutexUnlock(&loading->mutex); + loading++; + continue; + } + } while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX); + } +} + errorret_t assetDispose(void) { + threadStop(&ASSET.loadThread); + + for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) { + threadMutexDispose(&ASSET.loading[i].mutex); + } + // Cleanup zip file. if(ASSET.zip != NULL) { if(zip_close(ASSET.zip) != 0) { diff --git a/src/dusk/asset/asset.h b/src/dusk/asset/asset.h index a7bd7931..13162d03 100644 --- a/src/dusk/asset/asset.h +++ b/src/dusk/asset/asset.h @@ -9,7 +9,7 @@ #include "error/error.h" #include "asset/assetplatform.h" #include "assetfile.h" - +#include "thread/thread.h" #include "asset/loader/assetentry.h" #include "asset/loader/assetloading.h" @@ -30,6 +30,9 @@ typedef struct asset_s { zip_t *zip; assetplatform_t platform; + // Background loading thread. + thread_t loadThread; + // Assets that are mid loading. assetloading_t loading[ASSET_LOADING_COUNT_MAX]; assetentry_t entries[ASSET_ENTRY_COUNT_MAX]; @@ -76,11 +79,19 @@ errorret_t assetRequireLoaded(assetentry_t *entry); /** * Updates the asset system. - * + * * @return An error code if the asset system could not be updated properly. */ errorret_t assetUpdate(void); +/** + * Starts the background asset loading thread. The thread runs assetUpdate + * in a loop with a short sleep until stopped. + * + * @param thread The thread runner. + */ +void assetUpdateAsync(thread_t *thread); + /** * Disposes/cleans up the asset system. * diff --git a/src/dusk/asset/assetloader.h b/src/dusk/asset/assetloader.h deleted file mode 100644 index 2944fd2b..00000000 --- a/src/dusk/asset/assetloader.h +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -typedef enum assetloadertype_e { - ASSET_LOADER_TYPE_NULL, - - ASSET_LOADER_TYPE_MESH, - ASSET_LOADER_TYPE_TEXTURE, - ASSET_LOADER_TYPE_SHADER, - - ASSET_LOADER_TYPE_COUNT -} assetloadertype_t; - -typedef union { - void *nothing; -} assetloaderloading_t; - -typedef union { - void *nothing; -} assetloaderoutput_t; - -typedef enum { - ASSET_LOADER_LOADING_STATE_NOT_STARTED, - ASSET_LOADER_LOADING_STATE_PENDING_ASYNC, - ASSET_LOADER_LOADING_STATE_LOADING_ASYNC, - ASSET_LOADER_LOADING_STATE_PENDING_SYNC, - ASSET_LOADER_LOADING_STATE_LOADING_SYNC, - ASSET_LOADER_LOADING_STATE_LOADED, - ASSET_LOADER_LOADING_STATE_ERROR -} assetloaderloadingstate_t; - -typedef struct { - assetloadertype_t type; - assetloaderloadingstate_t state; - assetloaderloading_t loading; -} assetloadingentry_t; - -typedef struct { - assetloadertype_t type; - assetloaderoutput_t output; -} assetentry_t; \ No newline at end of file diff --git a/src/dusk/asset/loader/assetentry.c b/src/dusk/asset/loader/assetentry.c index a23763fb..cd86105f 100644 --- a/src/dusk/asset/loader/assetentry.c +++ b/src/dusk/asset/loader/assetentry.c @@ -55,7 +55,7 @@ void assetEntryStartLoading( ); entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; - memoryZero(loading, sizeof(assetloading_t)); + memoryZero(&loading->loading, sizeof(assetloaderloading_t)); loading->type = entry->type; loading->entry = entry; // At this point the asset manager will manage this thing's loading diff --git a/src/dusk/asset/loader/assetentry.h b/src/dusk/asset/loader/assetentry.h index 674b4a72..aa346948 100644 --- a/src/dusk/asset/loader/assetentry.h +++ b/src/dusk/asset/loader/assetentry.h @@ -11,8 +11,8 @@ typedef enum { ASSET_ENTRY_STATE_NOT_STARTED, - // ASSET_ENTRY_STATE_PENDING_ASYNC, - // ASSET_ENTRY_STATE_LOADING_ASYNC, + ASSET_ENTRY_STATE_PENDING_ASYNC, + ASSET_ENTRY_STATE_LOADING_ASYNC, ASSET_ENTRY_STATE_PENDING_SYNC, ASSET_ENTRY_STATE_LOADING_SYNC, ASSET_ENTRY_STATE_LOADED, diff --git a/src/dusk/asset/loader/assetloader.h b/src/dusk/asset/loader/assetloader.h index 36bb8091..ef9066b9 100644 --- a/src/dusk/asset/loader/assetloader.h +++ b/src/dusk/asset/loader/assetloader.h @@ -59,4 +59,30 @@ typedef struct { assetloaderdisposecallback_t *dispose; } assetloadercallbacks_t; -extern assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT]; \ No newline at end of file +extern assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT]; + +/** + * Shorthand method to both chain an error (against the loader state) and to + * set the asset entry state to error. + * + * @param loading The asset loading slot. + * @param ret The error return value to check and chain if it's an error. + */ +#define assetLoaderErrorChain(loading, ret) {\ + if(ret.code != ERROR_OK) { \ + loading->entry->state = ASSET_ENTRY_STATE_ERROR; \ + errorChain(ret); \ + } \ +} + +/** + * Shorthand method to both throw an error (against the loader state) and to + * set the asset entry state to error. + * + * @param loading The asset loading slot. + * @param ... Format string and arguments for the error message. + */ +#define assetLoaderErrorThrow(loading, ...) {\ + loading->entry->state = ASSET_ENTRY_STATE_ERROR; \ + errorThrow(__VA_ARGS__); \ +} diff --git a/src/dusk/asset/loader/assetloading.h b/src/dusk/asset/loader/assetloading.h index 546c553e..b517c09f 100644 --- a/src/dusk/asset/loader/assetloading.h +++ b/src/dusk/asset/loader/assetloading.h @@ -8,10 +8,13 @@ #pragma once #include "assetloader.h" #include "asset/assetfile.h" +#include "thread/threadmutex.h" typedef struct assetentry_s assetentry_t; typedef struct assetloading_s { + // Protects entry pointer and entry->state from concurrent access. + threadmutex_t mutex; // What type of asset is being loaded. assetloadertype_t type; // Referral back to the asset entry that will be kept alive after load done. diff --git a/src/dusk/asset/loader/display/assetmeshloader.c b/src/dusk/asset/loader/display/assetmeshloader.c index 58bb9d9f..e856c14b 100644 --- a/src/dusk/asset/loader/display/assetmeshloader.c +++ b/src/dusk/asset/loader/display/assetmeshloader.c @@ -20,16 +20,24 @@ errorret_t assetMeshLoaderSync(assetloading_t *loading) { assetfile_t *file = &loading->loading.mesh.file; assetmeshinputaxis_t axis = loading->entry->input->mesh; - errorChain(assetFileInit(file, loading->entry->name, NULL, NULL)); - errorChain(assetFileOpen(file)); + assetLoaderErrorChain(loading, assetFileInit( + file, loading->entry->name, NULL, NULL + )); + assetLoaderErrorChain(loading, assetFileOpen(file)); // Skip the 80-byte STL header - errorChain(assetFileRead(file, NULL, 80)); - if(file->lastRead != 80) errorThrow("Failed to skip STL header"); + assetLoaderErrorChain(loading, assetFileRead(file, NULL, 80)); + if(file->lastRead != 80) { + assetLoaderErrorThrow(loading, "Failed to skip STL header."); + } uint32_t triangleCount; - errorChain(assetFileRead(file, &triangleCount, sizeof(uint32_t))); - if(file->lastRead != sizeof(uint32_t)) errorThrow("Failed to read tri count"); + assetLoaderErrorChain( + loading, assetFileRead(file, &triangleCount, sizeof(uint32_t)) + ); + if(file->lastRead != sizeof(uint32_t)) { + assetLoaderErrorThrow(loading, "Failed to read tri count"); + } triangleCount = endianLittleToHost32(triangleCount); out->vertices = memoryAllocate(sizeof(meshvertex_t) * triangleCount * 3); @@ -42,19 +50,25 @@ errorret_t assetMeshLoaderSync(assetloading_t *loading) { if(ret.code != ERROR_OK) { memoryFree(verts); out->vertices = NULL; - errorChain(ret); + assetLoaderErrorChain(loading, ret); } if(file->lastRead != sizeof(triData)) { memoryFree(verts); out->vertices = NULL; - errorThrow("Failed to read triangle data"); + assetLoaderErrorThrow(loading, "Failed to read triangle data"); } for(uint8_t j = 0; j < 3; j++) { #if MESH_ENABLE_COLOR - verts[i * 3 + j].color.r = (uint8_t)(endianLittleToHostFloat(triData.normal[0]) * 255.0f); - verts[i * 3 + j].color.g = (uint8_t)(endianLittleToHostFloat(triData.normal[1]) * 255.0f); - verts[i * 3 + j].color.b = (uint8_t)(endianLittleToHostFloat(triData.normal[2]) * 255.0f); + verts[i * 3 + j].color.r = ( + (uint8_t)(endianLittleToHostFloat(triData.normal[0]) * 255.0f) + ); + verts[i * 3 + j].color.g = ( + (uint8_t)(endianLittleToHostFloat(triData.normal[1]) * 255.0f) + ); + verts[i * 3 + j].color.b = ( + (uint8_t)(endianLittleToHostFloat(triData.normal[2]) * 255.0f) + ); verts[i * 3 + j].color.a = 0xFF; #endif @@ -62,7 +76,9 @@ errorret_t assetMeshLoaderSync(assetloading_t *loading) { verts[i * 3 + j].uv[1] = 0.0f; for(uint8_t k = 0; k < 3; k++) { - verts[i * 3 + j].pos[k] = endianLittleToHostFloat(triData.positions[j][k]); + verts[i * 3 + j].pos[k] = endianLittleToHostFloat( + triData.positions[j][k] + ); } switch(axis) { @@ -104,17 +120,21 @@ errorret_t assetMeshLoaderSync(assetloading_t *loading) { if(ret.code != ERROR_OK) { memoryFree(verts); out->vertices = NULL; - errorChain(ret); + assetLoaderErrorChain(loading, ret); } assetFileDispose(file); - ret = meshInit(&out->mesh, MESH_PRIMITIVE_TYPE_TRIANGLES, triangleCount * 3, verts); + ret = meshInit( + &out->mesh, MESH_PRIMITIVE_TYPE_TRIANGLES, triangleCount * 3, verts + ); if(ret.code != ERROR_OK) { + loading->entry->state = ASSET_ENTRY_STATE_ERROR; memoryFree(verts); out->vertices = NULL; - errorChain(ret); + assetLoaderErrorChain(loading, ret); } + loading->entry->state = ASSET_ENTRY_STATE_LOADED; errorOk(); } diff --git a/src/dusk/asset/loader/display/assettextureloader.c b/src/dusk/asset/loader/display/assettextureloader.c index 436e60cb..8ff681bd 100644 --- a/src/dusk/asset/loader/display/assettextureloader.c +++ b/src/dusk/asset/loader/display/assettextureloader.c @@ -55,16 +55,15 @@ int assetTextureEOF(void *user) { errorret_t assetTextureLoaderSync(assetloading_t *loading) { assertNotNull(loading, "Loading cannot be NULL"); - // Init the file assetfile_t *file = &loading->loading.texture.file; - errorChain(assetFileInit( + assetLoaderErrorChain(loading, assetFileInit( file, loading->entry->name, NULL, &loading->entry->data.texture )); - errorChain(assetFileOpen(file)); + assetLoaderErrorChain(loading, assetFileOpen(file)); // Determine channels int channelsDesired; @@ -74,7 +73,7 @@ errorret_t assetTextureLoaderSync(assetloading_t *loading) { break; default: - errorThrow("Bad texture format."); + assetLoaderErrorThrow(loading, "Bad texture format."); } // Load image pixels. @@ -89,13 +88,13 @@ errorret_t assetTextureLoaderSync(assetloading_t *loading) { ); // Close out the file. - errorChain(assetFileClose(file)); - errorChain(assetFileDispose(file)); + assetLoaderErrorChain(loading, assetFileClose(file)); + assetLoaderErrorChain(loading, assetFileDispose(file)); // Ensure we loaded correctly. if(loading->loading.texture.data == NULL) { const char_t *errorStr = stbi_failure_reason(); - errorThrow("Failed to load texture from file %s.", errorStr); + assetLoaderErrorThrow(loading, "Failed to load texture from file %s.", errorStr); } // Fixes a specific bug probably with Dolphin but for now just assuming endian @@ -107,7 +106,7 @@ errorret_t assetTextureLoaderSync(assetloading_t *loading) { } // Create the texture. - errorChain(textureInit( + assetLoaderErrorChain(loading, textureInit( (texture_t*)&loading->entry->data.texture, (int32_t)width, (int32_t)height, loading->entry->input->texture, @@ -118,6 +117,8 @@ errorret_t assetTextureLoaderSync(assetloading_t *loading) { // Free the pixels. stbi_image_free(loading->loading.texture.data); + + loading->entry->state = ASSET_ENTRY_STATE_LOADED; errorOk(); } diff --git a/src/dusk/asset/loader/display/assettilesetloader.c b/src/dusk/asset/loader/display/assettilesetloader.c index 28ce00a0..99ba5378 100644 --- a/src/dusk/asset/loader/display/assettilesetloader.c +++ b/src/dusk/asset/loader/display/assettilesetloader.c @@ -19,23 +19,25 @@ errorret_t assetTilesetLoaderSync(assetloading_t *loading) { assetfile_t *file = &loading->loading.tileset.file; tileset_t *out = &loading->entry->data.tileset; - errorChain(assetFileInit(file, loading->entry->name, NULL, NULL)); + assetLoaderErrorChain( + loading, assetFileInit(file, loading->entry->name, NULL, NULL) + ); uint8_t *entire = memoryAllocate(file->size); - errorChain(assetFileOpen(file)); - errorChain(assetFileRead(file, entire, file->size)); - errorChain(assetFileClose(file)); - errorChain(assetFileDispose(file)); + assetLoaderErrorChain(loading, assetFileOpen(file)); + assetLoaderErrorChain(loading, assetFileRead(file, entire, file->size)); + assetLoaderErrorChain(loading, assetFileClose(file)); + assetLoaderErrorChain(loading, assetFileDispose(file)); assertTrue(file->lastRead == file->size, "Failed to read entire file."); if(entire[0] != 'D' || entire[1] != 'T' || entire[2] != 'F') { memoryFree(entire); - errorThrow("Invalid tileset header"); + assetLoaderErrorThrow(loading, "Invalid tileset header"); } if(entire[3] != 0x00) { memoryFree(entire); - errorThrow("Unsupported tileset version"); + assetLoaderErrorThrow(loading, "Unsupported tileset version"); } out->tileWidth = endianLittleToHost16(*(uint16_t *)(entire + 4)); @@ -43,21 +45,36 @@ errorret_t assetTilesetLoaderSync(assetloading_t *loading) { out->columns = endianLittleToHost16(*(uint16_t *)(entire + 8)); out->rows = endianLittleToHost16(*(uint16_t *)(entire + 10)); - if(out->tileWidth == 0) { memoryFree(entire); errorThrow("Tile width cannot be 0"); } - if(out->tileHeight == 0) { memoryFree(entire); errorThrow("Tile height cannot be 0"); } - if(out->columns == 0) { memoryFree(entire); errorThrow("Column count cannot be 0"); } - if(out->rows == 0) { memoryFree(entire); errorThrow("Row count cannot be 0"); } + if(out->tileWidth == 0) { + memoryFree(entire); + assetLoaderErrorThrow(loading, "Tile width cannot be 0"); + } + + if(out->tileHeight == 0) { + memoryFree(entire); + assetLoaderErrorThrow(loading, "Tile height cannot be 0"); + } + if(out->columns == 0) { + memoryFree(entire); + assetLoaderErrorThrow(loading, "Column count cannot be 0"); + } + if(out->rows == 0) { + memoryFree(entire); + assetLoaderErrorThrow(loading, "Row count cannot be 0"); + } out->uv[0] = endianLittleToHostFloat(*(float *)(entire + 16)); out->uv[1] = endianLittleToHostFloat(*(float *)(entire + 20)); if(out->uv[1] < 0.0f || out->uv[1] > 1.0f) { memoryFree(entire); - errorThrow("Invalid v0 value in tileset"); + assetLoaderErrorThrow(loading, "Invalid v0 value in tileset"); } out->tileCount = out->columns * out->rows; memoryFree(entire); + + loading->entry->state = ASSET_ENTRY_STATE_LOADED; errorOk(); } diff --git a/src/dusk/asset/loader/json/assetjsonloader.c b/src/dusk/asset/loader/json/assetjsonloader.c index 8977caa6..2194622d 100644 --- a/src/dusk/asset/loader/json/assetjsonloader.c +++ b/src/dusk/asset/loader/json/assetjsonloader.c @@ -16,18 +16,28 @@ errorret_t assetJsonLoaderSync(assetloading_t *loading) { assertTrue(loading->type == ASSET_LOADER_TYPE_JSON, "Invalid type."); assetfile_t *file = &loading->loading.json.file; - errorChain(assetFileInit(file, loading->entry->name, NULL, NULL)); + assetLoaderErrorChain( + loading, assetFileInit(file, loading->entry->name, NULL, NULL) + ); if(file->size > ASSET_JSON_FILE_SIZE_MAX) { - errorThrow("JSON exceeds maximum allowed size"); + assetLoaderErrorThrow(loading, "JSON exceeds maximum allowed size"); } uint8_t *buffer = memoryAllocate(file->size); - errorChain(assetFileOpen(file)); - errorChain(assetFileRead(file, buffer, file->size)); + assetLoaderErrorChain( + loading, assetFileOpen(file) + ); + assetLoaderErrorChain( + loading, assetFileRead(file, buffer, file->size) + ); assertTrue(file->lastRead == file->size, "Failed to read entire JSON file."); - errorChain(assetFileClose(file)); - errorChain(assetFileDispose(file)); + assetLoaderErrorChain( + loading, assetFileClose(file) + ); + assetLoaderErrorChain( + loading, assetFileDispose(file) + ); loading->entry->data.json = yyjson_read( (char *)buffer, @@ -36,7 +46,11 @@ errorret_t assetJsonLoaderSync(assetloading_t *loading) { ); memoryFree(buffer); - if(!loading->entry->data.json) errorThrow("Failed to parse JSON"); + if(!loading->entry->data.json) { + assetLoaderErrorThrow(loading, "Failed to parse JSON"); + } + + loading->entry->state = ASSET_ENTRY_STATE_LOADED; errorOk(); } diff --git a/src/dusk/asset/loader/locale/assetlocaleloader.c b/src/dusk/asset/loader/locale/assetlocaleloader.c index 606bc398..9eda04d6 100644 --- a/src/dusk/asset/loader/locale/assetlocaleloader.c +++ b/src/dusk/asset/loader/locale/assetlocaleloader.c @@ -20,12 +20,14 @@ errorret_t assetLocaleLoaderSync(assetloading_t *loading) { assetlocalefile_t *localeFile = &loading->entry->data.locale; memoryZero(localeFile, sizeof(assetlocalefile_t)); - errorChain(assetFileInit(&localeFile->file, loading->entry->name, NULL, NULL)); - errorChain(assetFileOpen(&localeFile->file)); + assetLoaderErrorChain(loading, assetFileInit(&localeFile->file, loading->entry->name, NULL, NULL)); + assetLoaderErrorChain(loading, assetFileOpen(&localeFile->file)); char_t buffer[1024]; - errorChain(assetLocaleGetString(localeFile, "", 0, buffer, sizeof(buffer))); - errorChain(assetLocaleParseHeader(localeFile, buffer, sizeof(buffer))); + assetLoaderErrorChain(loading, assetLocaleGetString(localeFile, "", 0, buffer, sizeof(buffer))); + assetLoaderErrorChain(loading, assetLocaleParseHeader(localeFile, buffer, sizeof(buffer))); + + loading->entry->state = ASSET_ENTRY_STATE_LOADED; errorOk(); } diff --git a/src/dusk/display/text/text.c b/src/dusk/display/text/text.c index 0cc31eaf..e71310a7 100644 --- a/src/dusk/display/text/text.c +++ b/src/dusk/display/text/text.c @@ -25,6 +25,7 @@ errorret_t textInit(void) { ); assetentry_t *entryTileset = assetGetEntry( "ui/minogram.dtf", ASSET_LOADER_TYPE_TILESET, NULL + // "ui/minogram.dtx", ASSET_LOADER_TYPE_TILESET, NULL ); errorChain(assetRequireLoaded(entryTexture)); errorChain(assetRequireLoaded(entryTileset)); diff --git a/src/dusk/error/error.c b/src/dusk/error/error.c index cde67a0f..b2c0d4d7 100644 --- a/src/dusk/error/error.c +++ b/src/dusk/error/error.c @@ -11,7 +11,7 @@ #include "util/string.h" #include "log/log.h" -errorstate_t ERROR_STATE = { 0 }; +THREAD_LOCAL errorstate_t ERROR_STATE = { 0 }; errorret_t errorThrowImpl( errorstate_t *state, @@ -64,7 +64,7 @@ errorret_t errorOkImpl() { ERROR_STATE.code == ERROR_OK, "Global error state is not OK (Likely missing errorCatch)" ); - + return (errorret_t) { .code = ERROR_OK, .state = NULL @@ -118,6 +118,7 @@ void errorCatch(errorret_t retval) { // Clear the error state memoryFree((void*)retval.state->message); + memoryFree((void*)retval.state->lines); retval.state->code = ERROR_OK; } diff --git a/src/dusk/error/error.h b/src/dusk/error/error.h index ccd4c844..26b2de12 100644 --- a/src/dusk/error/error.h +++ b/src/dusk/error/error.h @@ -7,6 +7,7 @@ #pragma once #include "dusk.h" +#include "thread/threadlocal.h" typedef uint8_t errorcode_t; @@ -26,7 +27,7 @@ static const errorcode_t ERROR_NOT_OK = 1; static const char_t *ERROR_PRINT_FORMAT = "Error (%d): %s\n%s"; static const char_t *ERROR_LINE_FORMAT = " at %s:%d in function %s\n"; -extern errorstate_t ERROR_STATE; +extern THREAD_LOCAL errorstate_t ERROR_STATE; /** * Sets the error state with the provided code and message. @@ -52,7 +53,7 @@ errorret_t errorThrowImpl( /** * Returns an error state with no error. - * + * * @return An error state with code ERROR_OK. */ errorret_t errorOkImpl(); @@ -88,34 +89,6 @@ void errorCatch(errorret_t retval); */ errorret_t errorPrint(const errorret_t retval); -/** - * Creates an error with a specific error state. - * - * @param state The error state to set. - * @param message The format string for the error message. - * @param ... Additional arguments for the format string. - * @return The error code. - */ -#define errorCreate(state, message, ... ) \ - errorThrowImpl(\ - (state), ERROR_NOT_OK, __FILE__, __func__, __LINE__, (message), \ - ##__VA_ARGS__ \ - ) - -/** - * Throws an error with a specific error state. - * - * @param state The error state to set. - * @param message The format string for the error message. - * @param ... Additional arguments for the format string. - * @return The error code. - */ -#define errorThrowState(state, message, ... ) \ - return errorThrowImpl(\ - (state), ERROR_NOT_OK, __FILE__, __func__, __LINE__, (message), \ - ##__VA_ARGS__ \ - ) - /** * Throws an error with a formatted message. * @@ -162,4 +135,18 @@ errorret_t errorPrint(const errorret_t retval); #define errorOk() \ return errorOkImpl() +/** + * Returns non-zero if retval indicates success. + * + * @param retval errorret_t to test. + */ +#define errorIsOk(retval) ((retval).code == ERROR_OK) + +/** + * Returns non-zero if retval indicates failure. + * + * @param retval errorret_t to test. + */ +#define errorIsNotOk(retval) ((retval).code != ERROR_OK) + // EOF \ No newline at end of file diff --git a/src/dusk/thread/thread.h b/src/dusk/thread/thread.h index 3b1f352a..61c4454a 100644 --- a/src/dusk/thread/thread.h +++ b/src/dusk/thread/thread.h @@ -6,6 +6,7 @@ */ #pragma once +#include "thread/threadlocal.h" #include "thread/threadmutex.h" typedef struct thread_s thread_t; diff --git a/src/dusk/thread/threadlocal.h b/src/dusk/thread/threadlocal.h new file mode 100644 index 00000000..d69b48e2 --- /dev/null +++ b/src/dusk/thread/threadlocal.h @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +#ifdef DUSK_THREAD_PTHREAD + #define THREAD_LOCAL __thread +#endif + +#ifndef THREAD_LOCAL + #error "No threading implementation found, cannot define THREAD_LOCAL." +#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 3acf07a3..c172972d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -4,6 +4,8 @@ # https://opensource.org/licenses/MIT add_subdirectory(assert) +add_subdirectory(error) +add_subdirectory(thread) add_subdirectory(display) # add_subdirectory(rpg) # add_subdirectory(item) diff --git a/test/error/CMakeLists.txt b/test/error/CMakeLists.txt new file mode 100644 index 00000000..c233a159 --- /dev/null +++ b/test/error/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +include(dusktest) + +# Tests +dusktest(test_error.c) diff --git a/test/error/test_error.c b/test/error/test_error.c new file mode 100644 index 00000000..aa7e7090 --- /dev/null +++ b/test/error/test_error.c @@ -0,0 +1,187 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "dusktest.h" +#include "error/error.h" +#include "thread/thread.h" +#include "util/memory.h" + +// Helper that throws an error. +static errorret_t helper_throw(void) { + errorThrow("Test error %d", 42); +} + +// Helper that returns ok. +static errorret_t helper_ok(void) { + errorOk(); +} + +// Helper that chains to helper_throw. +static errorret_t helper_chain(void) { + errorChain(helper_throw()); + errorOk(); +} + +static void test_errorThrow(void **state) { + errorret_t ret = helper_throw(); + + assert_int_not_equal(ret.code, ERROR_OK); + assert_non_null(ret.state); + assert_non_null(ret.state->message); + assert_non_null(ret.state->lines); + + // Message should contain our format argument + assert_non_null(strstr(ret.state->message, "42")); + + errorCatch(ret); + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +static void test_errorOk(void **state) { + errorret_t ret = helper_ok(); + + assert_int_equal(ret.code, ERROR_OK); + assert_null(ret.state); + + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +static void test_errorIsOk(void **state) { + errorret_t ok = helper_ok(); + assert_true(errorIsOk(ok)); + assert_false(errorIsNotOk(ok)); + + errorret_t err = helper_throw(); + assert_false(errorIsOk(err)); + assert_true(errorIsNotOk(err)); + + errorCatch(err); + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +static void test_errorChain(void **state) { + errorret_t ret = helper_chain(); + + // Error propagated up + assert_int_not_equal(ret.code, ERROR_OK); + assert_non_null(ret.state); + assert_non_null(ret.state->lines); + + // Lines should contain at least two stack entries + int32_t count = 0; + const char_t *p = ret.state->lines; + while((p = strstr(p, " at ")) != NULL) { + count++; + p++; + } + assert_true(count >= 2); + + errorCatch(ret); + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +static void test_errorCatch_ok(void **state) { + // Catching an ok ret should be a no-op + errorret_t ret = helper_ok(); + errorCatch(ret); + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +static void test_errorCatch_error(void **state) { + errorret_t ret = helper_throw(); + assert_int_not_equal(ret.code, ERROR_OK); + + errorCatch(ret); + + // After catch the global state should be cleared + assert_int_equal(ERROR_STATE.code, ERROR_OK); + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +// --- Threaded tests --- + +typedef struct { + errorcode_t capturedCode; + bool_t messageHas42; +} thread_test_data_t; + +static void helper_thread_throw(thread_t *thread) { + errorret_t ret = helper_throw(); + thread_test_data_t *data = (thread_test_data_t *)thread->data; + data->capturedCode = ERROR_STATE.code; + data->messageHas42 = (strstr(ret.state->message, "42") != NULL); + errorCatch(ret); +} + +static void test_error_thread_isolation(void **state) { + // Main thread state is clean before the test. + assert_int_equal(ERROR_STATE.code, ERROR_OK); + + thread_test_data_t data = { .capturedCode = ERROR_OK, .messageHas42 = false }; + + thread_t thread; + threadInit(&thread, helper_thread_throw); + thread.data = &data; + threadStart(&thread); + threadStop(&thread); + + // Worker saw ERROR_NOT_OK in its own ERROR_STATE. + assert_int_equal(data.capturedCode, ERROR_NOT_OK); + assert_true(data.messageHas42); + + // Main thread ERROR_STATE was not touched by the worker. + assert_int_equal(ERROR_STATE.code, ERROR_OK); + + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +#define CONCURRENT_THREAD_COUNT 4 + +static thread_test_data_t concurrent_data[CONCURRENT_THREAD_COUNT]; +static thread_t concurrent_threads[CONCURRENT_THREAD_COUNT]; + +static void test_error_concurrent_throw(void **state) { + for(int32_t i = 0; i < CONCURRENT_THREAD_COUNT; i++) { + concurrent_data[i].capturedCode = ERROR_OK; + concurrent_data[i].messageHas42 = false; + threadInit(&concurrent_threads[i], helper_thread_throw); + concurrent_threads[i].data = &concurrent_data[i]; + } + + for(int32_t i = 0; i < CONCURRENT_THREAD_COUNT; i++) { + threadStart(&concurrent_threads[i]); + } + + for(int32_t i = 0; i < CONCURRENT_THREAD_COUNT; i++) { + threadStop(&concurrent_threads[i]); + } + + // Every worker must have seen its own independent error. + for(int32_t i = 0; i < CONCURRENT_THREAD_COUNT; i++) { + assert_int_equal(concurrent_data[i].capturedCode, ERROR_NOT_OK); + assert_true(concurrent_data[i].messageHas42); + } + + // Main thread is still clean. + assert_int_equal(ERROR_STATE.code, ERROR_OK); + + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_errorThrow), + cmocka_unit_test(test_errorOk), + cmocka_unit_test(test_errorIsOk), + cmocka_unit_test(test_errorChain), + cmocka_unit_test(test_errorCatch_ok), + cmocka_unit_test(test_errorCatch_error), + cmocka_unit_test(test_error_thread_isolation), + cmocka_unit_test(test_error_concurrent_throw), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/test/thread/CMakeLists.txt b/test/thread/CMakeLists.txt new file mode 100644 index 00000000..9c865744 --- /dev/null +++ b/test/thread/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +include(dusktest) + +# Tests +dusktest(test_thread.c) diff --git a/test/thread/test_thread.c b/test/thread/test_thread.c new file mode 100644 index 00000000..039d128e --- /dev/null +++ b/test/thread/test_thread.c @@ -0,0 +1,216 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "dusktest.h" +#include "thread/thread.h" +#include "util/memory.h" + +// --- Helpers --- + +static void helper_noop(thread_t *thread) { + // intentionally empty: one-shot thread that exits immediately +} + +static void helper_loop(thread_t *thread) { + while(!threadShouldStop(thread)) {} +} + +static void helper_write_data(thread_t *thread) { + int32_t *value = (int32_t *)thread->data; + *value = 42; +} + +// --- thread_t tests --- + +static void test_threadInit(void **state) { + thread_t thread; + threadInit(&thread, helper_noop); + + assert_int_equal(thread.state, THREAD_STATE_STOPPED); + assert_ptr_equal(thread.callback, helper_noop); + assert_null(thread.data); +} + +static void test_thread_start_stop(void **state) { + thread_t thread; + threadInit(&thread, helper_noop); + threadStart(&thread); + threadStop(&thread); + + assert_int_equal(thread.state, THREAD_STATE_STOPPED); +} + +static void test_thread_should_stop(void **state) { + // threadStop blocks until STOPPED — if threadShouldStop is broken the + // looping callback never exits and this test hangs / times out. + thread_t thread; + threadInit(&thread, helper_loop); + threadStart(&thread); + threadStop(&thread); + + assert_int_equal(thread.state, THREAD_STATE_STOPPED); +} + +static void test_thread_data(void **state) { + int32_t value = 0; + + thread_t thread; + threadInit(&thread, helper_write_data); + thread.data = &value; + threadStart(&thread); + threadStop(&thread); + + // After threadStop the callback has definitely run. + assert_int_equal(value, 42); +} + +static void test_thread_restart(void **state) { + // A thread can be started, stopped, and started again. + thread_t thread; + threadInit(&thread, helper_noop); + + threadStart(&thread); + threadStop(&thread); + assert_int_equal(thread.state, THREAD_STATE_STOPPED); + + // Re-initialise so threadId / state are reset, then start again. + threadInit(&thread, helper_noop); + threadStart(&thread); + threadStop(&thread); + assert_int_equal(thread.state, THREAD_STATE_STOPPED); +} + +// --- threadmutex_t tests --- + +static void test_threadMutex_lock_unlock(void **state) { + threadmutex_t mutex; + threadMutexInit(&mutex); + + threadMutexLock(&mutex); + threadMutexUnlock(&mutex); + + threadMutexDispose(&mutex); +} + +// Shared data for try-lock test. Uses volatile phase to coordinate the two +// threads without introducing a second mutex. +typedef struct { + threadmutex_t *target; + volatile int32_t phase; + bool_t resultWhileLocked; + bool_t resultAfterUnlock; +} trylock_data_t; + +static void helper_trylock(thread_t *thread) { + trylock_data_t *data = (trylock_data_t *)thread->data; + + // Phase 1: main holds the lock — trylock must fail. + while(data->phase != 1) {} + data->resultWhileLocked = threadMutexTryLock(data->target); + data->phase = 2; + + // Phase 3: main released the lock — trylock must succeed. + while(data->phase != 3) {} + data->resultAfterUnlock = threadMutexTryLock(data->target); + if(data->resultAfterUnlock) { + threadMutexUnlock(data->target); + } + data->phase = 4; +} + +static void test_threadMutex_try_lock(void **state) { + threadmutex_t mutex; + threadMutexInit(&mutex); + + trylock_data_t data = { + .target = &mutex, + .phase = 0, + .resultWhileLocked = false, + .resultAfterUnlock = false + }; + + thread_t thread; + threadInit(&thread, helper_trylock); + thread.data = &data; + threadStart(&thread); + + // Hold the lock, then let the helper try. + threadMutexLock(&mutex); + data.phase = 1; + while(data.phase != 2) {} + assert_false(data.resultWhileLocked); + + // Release, then let the helper try again. + threadMutexUnlock(&mutex); + data.phase = 3; + while(data.phase != 4) {} + assert_true(data.resultAfterUnlock); + + threadStop(&thread); + threadMutexDispose(&mutex); +} + +// Mutual-exclusion test: N threads each increment a shared counter M times +// under a mutex. The final value must be exactly N*M. +#define MUTEX_THREADS 4 +#define MUTEX_ITERATIONS 10000 + +typedef struct { + threadmutex_t *mutex; + int32_t *counter; +} counter_data_t; + +static counter_data_t counter_thread_data[MUTEX_THREADS]; +static thread_t counter_threads[MUTEX_THREADS]; + +static void helper_increment(thread_t *thread) { + counter_data_t *data = (counter_data_t *)thread->data; + for(int32_t i = 0; i < MUTEX_ITERATIONS; i++) { + threadMutexLock(data->mutex); + (*data->counter)++; + threadMutexUnlock(data->mutex); + } +} + +static void test_threadMutex_mutual_exclusion(void **state) { + threadmutex_t mutex; + threadMutexInit(&mutex); + int32_t counter = 0; + + for(int32_t i = 0; i < MUTEX_THREADS; i++) { + counter_thread_data[i].mutex = &mutex; + counter_thread_data[i].counter = &counter; + threadInit(&counter_threads[i], helper_increment); + counter_threads[i].data = &counter_thread_data[i]; + } + + for(int32_t i = 0; i < MUTEX_THREADS; i++) { + threadStart(&counter_threads[i]); + } + + for(int32_t i = 0; i < MUTEX_THREADS; i++) { + threadStop(&counter_threads[i]); + } + + assert_int_equal(counter, MUTEX_THREADS * MUTEX_ITERATIONS); + + threadMutexDispose(&mutex); +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_threadInit), + cmocka_unit_test(test_thread_start_stop), + cmocka_unit_test(test_thread_should_stop), + cmocka_unit_test(test_thread_data), + cmocka_unit_test(test_thread_restart), + cmocka_unit_test(test_threadMutex_lock_unlock), + cmocka_unit_test(test_threadMutex_try_lock), + cmocka_unit_test(test_threadMutex_mutual_exclusion), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +}