Prepping for async
This commit is contained in:
+137
-32
@@ -11,15 +11,23 @@
|
|||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "engine/engine.h"
|
#include "engine/engine.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
|
#include "console/console.h"
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
asset_t ASSET;
|
asset_t ASSET;
|
||||||
|
|
||||||
errorret_t assetInit(void) {
|
errorret_t assetInit(void) {
|
||||||
memoryZero(&ASSET, sizeof(asset_t));
|
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.
|
// assetInitPlatform must either define ASSET.zip or throw an error.
|
||||||
errorChain(assetInitPlatform());
|
errorChain(assetInitPlatform());
|
||||||
assertNotNull(ASSET.zip, "Asset zip null without error.");
|
assertNotNull(ASSET.zip, "Asset zip null without error.");
|
||||||
|
threadInit(&ASSET.loadThread, assetUpdateAsync);
|
||||||
|
threadStart(&ASSET.loadThread);
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -88,68 +96,120 @@ errorret_t assetRequireLoaded(assetentry_t *entry) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
errorret_t assetUpdate(void) {
|
errorret_t assetUpdate(void) {
|
||||||
// Is there any pending entries?
|
// Determine how many available loading slots we have.
|
||||||
assetentry_t *entry = ASSET.entries;
|
assetloading_t *availableLoading[ASSET_LOADING_COUNT_MAX];
|
||||||
assetloading_t *loading;
|
uint8_t availableLoadingCount = 0;
|
||||||
|
assetloading_t *loading = ASSET.loading;
|
||||||
|
assetentry_t *entry;
|
||||||
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
// Is this asset "ready to start loading" ?
|
// We only care about NULL entry references. Nothing async touches this so
|
||||||
if(entry->type == ASSET_LOADER_TYPE_NULL) {
|
// it's fine to use raw here.
|
||||||
entry++;
|
if(loading->entry != NULL) {
|
||||||
|
loading++;
|
||||||
continue;
|
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
|
// Now we can check for pending asset entries, we can't do anything if there
|
||||||
loading = ASSET.loading;
|
// is no available slots though.
|
||||||
bool_t found = false;
|
if(availableLoadingCount > 0) {
|
||||||
|
entry = ASSET.entries;
|
||||||
do {
|
do {
|
||||||
if(loading->type != ASSET_LOADER_TYPE_NULL) {
|
// Is this asset "ready to start loading" ?
|
||||||
loading++;
|
if(entry->type == ASSET_LOADER_TYPE_NULL) {
|
||||||
|
entry++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
found = true;
|
// We only care about assets not started.
|
||||||
break;
|
if(entry->state != ASSET_ENTRY_STATE_NOT_STARTED) {
|
||||||
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
|
entry++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if(!found) {
|
// Pop a loading slot for this asset entry.
|
||||||
// No loading slot, try again next frame.
|
loading = availableLoading[--availableLoadingCount];
|
||||||
|
|
||||||
|
// Start loading this asset.
|
||||||
|
assetEntryStartLoading(entry, loading);
|
||||||
entry++;
|
entry++;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start loading this asset.
|
// Did we run out of loading slots?
|
||||||
assetEntryStartLoading(entry, loading);
|
if(availableLoadingCount == 0) {
|
||||||
entry++;
|
break;
|
||||||
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
}
|
||||||
|
} 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;
|
loading = ASSET.loading;
|
||||||
do {
|
do {
|
||||||
|
// Is the loading slot in use? Entry can only be modified synchronously.
|
||||||
if(loading->entry == NULL) {
|
if(loading->entry == NULL) {
|
||||||
loading++;
|
loading++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lock the loading slot. This will prevent any async modifications.
|
||||||
|
threadMutexLock(&loading->mutex);
|
||||||
|
|
||||||
|
// Check the state of the entry.
|
||||||
switch(loading->entry->state) {
|
switch(loading->entry->state) {
|
||||||
|
// This thing is pending synchronous loading.
|
||||||
case ASSET_ENTRY_STATE_PENDING_SYNC:
|
case ASSET_ENTRY_STATE_PENDING_SYNC:
|
||||||
// Begin sync loading
|
// Perform sync load.
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_LOADING_SYNC;
|
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) {
|
if(ret.code != ERROR_OK) {
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
assertTrue(
|
||||||
return ret;
|
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++;
|
loading++;
|
||||||
break;
|
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:
|
default:
|
||||||
|
threadMutexUnlock(&loading->mutex);
|
||||||
loading++;
|
loading++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -157,7 +217,52 @@ errorret_t assetUpdate(void) {
|
|||||||
errorOk();
|
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) {
|
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.
|
// Cleanup zip file.
|
||||||
if(ASSET.zip != NULL) {
|
if(ASSET.zip != NULL) {
|
||||||
if(zip_close(ASSET.zip) != 0) {
|
if(zip_close(ASSET.zip) != 0) {
|
||||||
|
|||||||
+13
-2
@@ -9,7 +9,7 @@
|
|||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "asset/assetplatform.h"
|
#include "asset/assetplatform.h"
|
||||||
#include "assetfile.h"
|
#include "assetfile.h"
|
||||||
|
#include "thread/thread.h"
|
||||||
#include "asset/loader/assetentry.h"
|
#include "asset/loader/assetentry.h"
|
||||||
#include "asset/loader/assetloading.h"
|
#include "asset/loader/assetloading.h"
|
||||||
|
|
||||||
@@ -30,6 +30,9 @@ typedef struct asset_s {
|
|||||||
zip_t *zip;
|
zip_t *zip;
|
||||||
assetplatform_t platform;
|
assetplatform_t platform;
|
||||||
|
|
||||||
|
// Background loading thread.
|
||||||
|
thread_t loadThread;
|
||||||
|
|
||||||
// Assets that are mid loading.
|
// Assets that are mid loading.
|
||||||
assetloading_t loading[ASSET_LOADING_COUNT_MAX];
|
assetloading_t loading[ASSET_LOADING_COUNT_MAX];
|
||||||
assetentry_t entries[ASSET_ENTRY_COUNT_MAX];
|
assetentry_t entries[ASSET_ENTRY_COUNT_MAX];
|
||||||
@@ -76,11 +79,19 @@ errorret_t assetRequireLoaded(assetentry_t *entry);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the asset system.
|
* Updates the asset system.
|
||||||
*
|
*
|
||||||
* @return An error code if the asset system could not be updated properly.
|
* @return An error code if the asset system could not be updated properly.
|
||||||
*/
|
*/
|
||||||
errorret_t assetUpdate(void);
|
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.
|
* Disposes/cleans up the asset system.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -55,7 +55,7 @@ void assetEntryStartLoading(
|
|||||||
);
|
);
|
||||||
|
|
||||||
entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||||
memoryZero(loading, sizeof(assetloading_t));
|
memoryZero(&loading->loading, sizeof(assetloaderloading_t));
|
||||||
loading->type = entry->type;
|
loading->type = entry->type;
|
||||||
loading->entry = entry;
|
loading->entry = entry;
|
||||||
// At this point the asset manager will manage this thing's loading
|
// At this point the asset manager will manage this thing's loading
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
ASSET_ENTRY_STATE_NOT_STARTED,
|
ASSET_ENTRY_STATE_NOT_STARTED,
|
||||||
// ASSET_ENTRY_STATE_PENDING_ASYNC,
|
ASSET_ENTRY_STATE_PENDING_ASYNC,
|
||||||
// ASSET_ENTRY_STATE_LOADING_ASYNC,
|
ASSET_ENTRY_STATE_LOADING_ASYNC,
|
||||||
ASSET_ENTRY_STATE_PENDING_SYNC,
|
ASSET_ENTRY_STATE_PENDING_SYNC,
|
||||||
ASSET_ENTRY_STATE_LOADING_SYNC,
|
ASSET_ENTRY_STATE_LOADING_SYNC,
|
||||||
ASSET_ENTRY_STATE_LOADED,
|
ASSET_ENTRY_STATE_LOADED,
|
||||||
|
|||||||
@@ -59,4 +59,30 @@ typedef struct {
|
|||||||
assetloaderdisposecallback_t *dispose;
|
assetloaderdisposecallback_t *dispose;
|
||||||
} assetloadercallbacks_t;
|
} assetloadercallbacks_t;
|
||||||
|
|
||||||
extern assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT];
|
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__); \
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,10 +8,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "assetloader.h"
|
#include "assetloader.h"
|
||||||
#include "asset/assetfile.h"
|
#include "asset/assetfile.h"
|
||||||
|
#include "thread/threadmutex.h"
|
||||||
|
|
||||||
typedef struct assetentry_s assetentry_t;
|
typedef struct assetentry_s assetentry_t;
|
||||||
|
|
||||||
typedef struct assetloading_s {
|
typedef struct assetloading_s {
|
||||||
|
// Protects entry pointer and entry->state from concurrent access.
|
||||||
|
threadmutex_t mutex;
|
||||||
// What type of asset is being loaded.
|
// What type of asset is being loaded.
|
||||||
assetloadertype_t type;
|
assetloadertype_t type;
|
||||||
// Referral back to the asset entry that will be kept alive after load done.
|
// Referral back to the asset entry that will be kept alive after load done.
|
||||||
|
|||||||
@@ -20,16 +20,24 @@ errorret_t assetMeshLoaderSync(assetloading_t *loading) {
|
|||||||
assetfile_t *file = &loading->loading.mesh.file;
|
assetfile_t *file = &loading->loading.mesh.file;
|
||||||
assetmeshinputaxis_t axis = loading->entry->input->mesh;
|
assetmeshinputaxis_t axis = loading->entry->input->mesh;
|
||||||
|
|
||||||
errorChain(assetFileInit(file, loading->entry->name, NULL, NULL));
|
assetLoaderErrorChain(loading, assetFileInit(
|
||||||
errorChain(assetFileOpen(file));
|
file, loading->entry->name, NULL, NULL
|
||||||
|
));
|
||||||
|
assetLoaderErrorChain(loading, assetFileOpen(file));
|
||||||
|
|
||||||
// Skip the 80-byte STL header
|
// Skip the 80-byte STL header
|
||||||
errorChain(assetFileRead(file, NULL, 80));
|
assetLoaderErrorChain(loading, assetFileRead(file, NULL, 80));
|
||||||
if(file->lastRead != 80) errorThrow("Failed to skip STL header");
|
if(file->lastRead != 80) {
|
||||||
|
assetLoaderErrorThrow(loading, "Failed to skip STL header.");
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t triangleCount;
|
uint32_t triangleCount;
|
||||||
errorChain(assetFileRead(file, &triangleCount, sizeof(uint32_t)));
|
assetLoaderErrorChain(
|
||||||
if(file->lastRead != sizeof(uint32_t)) errorThrow("Failed to read tri count");
|
loading, assetFileRead(file, &triangleCount, sizeof(uint32_t))
|
||||||
|
);
|
||||||
|
if(file->lastRead != sizeof(uint32_t)) {
|
||||||
|
assetLoaderErrorThrow(loading, "Failed to read tri count");
|
||||||
|
}
|
||||||
triangleCount = endianLittleToHost32(triangleCount);
|
triangleCount = endianLittleToHost32(triangleCount);
|
||||||
|
|
||||||
out->vertices = memoryAllocate(sizeof(meshvertex_t) * triangleCount * 3);
|
out->vertices = memoryAllocate(sizeof(meshvertex_t) * triangleCount * 3);
|
||||||
@@ -42,19 +50,25 @@ errorret_t assetMeshLoaderSync(assetloading_t *loading) {
|
|||||||
if(ret.code != ERROR_OK) {
|
if(ret.code != ERROR_OK) {
|
||||||
memoryFree(verts);
|
memoryFree(verts);
|
||||||
out->vertices = NULL;
|
out->vertices = NULL;
|
||||||
errorChain(ret);
|
assetLoaderErrorChain(loading, ret);
|
||||||
}
|
}
|
||||||
if(file->lastRead != sizeof(triData)) {
|
if(file->lastRead != sizeof(triData)) {
|
||||||
memoryFree(verts);
|
memoryFree(verts);
|
||||||
out->vertices = NULL;
|
out->vertices = NULL;
|
||||||
errorThrow("Failed to read triangle data");
|
assetLoaderErrorThrow(loading, "Failed to read triangle data");
|
||||||
}
|
}
|
||||||
|
|
||||||
for(uint8_t j = 0; j < 3; j++) {
|
for(uint8_t j = 0; j < 3; j++) {
|
||||||
#if MESH_ENABLE_COLOR
|
#if MESH_ENABLE_COLOR
|
||||||
verts[i * 3 + j].color.r = (uint8_t)(endianLittleToHostFloat(triData.normal[0]) * 255.0f);
|
verts[i * 3 + j].color.r = (
|
||||||
verts[i * 3 + j].color.g = (uint8_t)(endianLittleToHostFloat(triData.normal[1]) * 255.0f);
|
(uint8_t)(endianLittleToHostFloat(triData.normal[0]) * 255.0f)
|
||||||
verts[i * 3 + j].color.b = (uint8_t)(endianLittleToHostFloat(triData.normal[2]) * 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;
|
verts[i * 3 + j].color.a = 0xFF;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -62,7 +76,9 @@ errorret_t assetMeshLoaderSync(assetloading_t *loading) {
|
|||||||
verts[i * 3 + j].uv[1] = 0.0f;
|
verts[i * 3 + j].uv[1] = 0.0f;
|
||||||
|
|
||||||
for(uint8_t k = 0; k < 3; k++) {
|
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) {
|
switch(axis) {
|
||||||
@@ -104,17 +120,21 @@ errorret_t assetMeshLoaderSync(assetloading_t *loading) {
|
|||||||
if(ret.code != ERROR_OK) {
|
if(ret.code != ERROR_OK) {
|
||||||
memoryFree(verts);
|
memoryFree(verts);
|
||||||
out->vertices = NULL;
|
out->vertices = NULL;
|
||||||
errorChain(ret);
|
assetLoaderErrorChain(loading, ret);
|
||||||
}
|
}
|
||||||
assetFileDispose(file);
|
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) {
|
if(ret.code != ERROR_OK) {
|
||||||
|
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||||
memoryFree(verts);
|
memoryFree(verts);
|
||||||
out->vertices = NULL;
|
out->vertices = NULL;
|
||||||
errorChain(ret);
|
assetLoaderErrorChain(loading, ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,16 +55,15 @@ int assetTextureEOF(void *user) {
|
|||||||
errorret_t assetTextureLoaderSync(assetloading_t *loading) {
|
errorret_t assetTextureLoaderSync(assetloading_t *loading) {
|
||||||
assertNotNull(loading, "Loading cannot be NULL");
|
assertNotNull(loading, "Loading cannot be NULL");
|
||||||
|
|
||||||
|
|
||||||
// Init the file
|
// Init the file
|
||||||
assetfile_t *file = &loading->loading.texture.file;
|
assetfile_t *file = &loading->loading.texture.file;
|
||||||
errorChain(assetFileInit(
|
assetLoaderErrorChain(loading, assetFileInit(
|
||||||
file,
|
file,
|
||||||
loading->entry->name,
|
loading->entry->name,
|
||||||
NULL,
|
NULL,
|
||||||
&loading->entry->data.texture
|
&loading->entry->data.texture
|
||||||
));
|
));
|
||||||
errorChain(assetFileOpen(file));
|
assetLoaderErrorChain(loading, assetFileOpen(file));
|
||||||
|
|
||||||
// Determine channels
|
// Determine channels
|
||||||
int channelsDesired;
|
int channelsDesired;
|
||||||
@@ -74,7 +73,7 @@ errorret_t assetTextureLoaderSync(assetloading_t *loading) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
errorThrow("Bad texture format.");
|
assetLoaderErrorThrow(loading, "Bad texture format.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load image pixels.
|
// Load image pixels.
|
||||||
@@ -89,13 +88,13 @@ errorret_t assetTextureLoaderSync(assetloading_t *loading) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Close out the file.
|
// Close out the file.
|
||||||
errorChain(assetFileClose(file));
|
assetLoaderErrorChain(loading, assetFileClose(file));
|
||||||
errorChain(assetFileDispose(file));
|
assetLoaderErrorChain(loading, assetFileDispose(file));
|
||||||
|
|
||||||
// Ensure we loaded correctly.
|
// Ensure we loaded correctly.
|
||||||
if(loading->loading.texture.data == NULL) {
|
if(loading->loading.texture.data == NULL) {
|
||||||
const char_t *errorStr = stbi_failure_reason();
|
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
|
// 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.
|
// Create the texture.
|
||||||
errorChain(textureInit(
|
assetLoaderErrorChain(loading, textureInit(
|
||||||
(texture_t*)&loading->entry->data.texture,
|
(texture_t*)&loading->entry->data.texture,
|
||||||
(int32_t)width, (int32_t)height,
|
(int32_t)width, (int32_t)height,
|
||||||
loading->entry->input->texture,
|
loading->entry->input->texture,
|
||||||
@@ -118,6 +117,8 @@ errorret_t assetTextureLoaderSync(assetloading_t *loading) {
|
|||||||
|
|
||||||
// Free the pixels.
|
// Free the pixels.
|
||||||
stbi_image_free(loading->loading.texture.data);
|
stbi_image_free(loading->loading.texture.data);
|
||||||
|
|
||||||
|
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,23 +19,25 @@ errorret_t assetTilesetLoaderSync(assetloading_t *loading) {
|
|||||||
assetfile_t *file = &loading->loading.tileset.file;
|
assetfile_t *file = &loading->loading.tileset.file;
|
||||||
tileset_t *out = &loading->entry->data.tileset;
|
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);
|
uint8_t *entire = memoryAllocate(file->size);
|
||||||
errorChain(assetFileOpen(file));
|
assetLoaderErrorChain(loading, assetFileOpen(file));
|
||||||
errorChain(assetFileRead(file, entire, file->size));
|
assetLoaderErrorChain(loading, assetFileRead(file, entire, file->size));
|
||||||
errorChain(assetFileClose(file));
|
assetLoaderErrorChain(loading, assetFileClose(file));
|
||||||
errorChain(assetFileDispose(file));
|
assetLoaderErrorChain(loading, assetFileDispose(file));
|
||||||
assertTrue(file->lastRead == file->size, "Failed to read entire file.");
|
assertTrue(file->lastRead == file->size, "Failed to read entire file.");
|
||||||
|
|
||||||
if(entire[0] != 'D' || entire[1] != 'T' || entire[2] != 'F') {
|
if(entire[0] != 'D' || entire[1] != 'T' || entire[2] != 'F') {
|
||||||
memoryFree(entire);
|
memoryFree(entire);
|
||||||
errorThrow("Invalid tileset header");
|
assetLoaderErrorThrow(loading, "Invalid tileset header");
|
||||||
}
|
}
|
||||||
|
|
||||||
if(entire[3] != 0x00) {
|
if(entire[3] != 0x00) {
|
||||||
memoryFree(entire);
|
memoryFree(entire);
|
||||||
errorThrow("Unsupported tileset version");
|
assetLoaderErrorThrow(loading, "Unsupported tileset version");
|
||||||
}
|
}
|
||||||
|
|
||||||
out->tileWidth = endianLittleToHost16(*(uint16_t *)(entire + 4));
|
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->columns = endianLittleToHost16(*(uint16_t *)(entire + 8));
|
||||||
out->rows = endianLittleToHost16(*(uint16_t *)(entire + 10));
|
out->rows = endianLittleToHost16(*(uint16_t *)(entire + 10));
|
||||||
|
|
||||||
if(out->tileWidth == 0) { memoryFree(entire); errorThrow("Tile width cannot be 0"); }
|
if(out->tileWidth == 0) {
|
||||||
if(out->tileHeight == 0) { memoryFree(entire); errorThrow("Tile height cannot be 0"); }
|
memoryFree(entire);
|
||||||
if(out->columns == 0) { memoryFree(entire); errorThrow("Column count cannot be 0"); }
|
assetLoaderErrorThrow(loading, "Tile width cannot be 0");
|
||||||
if(out->rows == 0) { memoryFree(entire); errorThrow("Row count 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[0] = endianLittleToHostFloat(*(float *)(entire + 16));
|
||||||
out->uv[1] = endianLittleToHostFloat(*(float *)(entire + 20));
|
out->uv[1] = endianLittleToHostFloat(*(float *)(entire + 20));
|
||||||
|
|
||||||
if(out->uv[1] < 0.0f || out->uv[1] > 1.0f) {
|
if(out->uv[1] < 0.0f || out->uv[1] > 1.0f) {
|
||||||
memoryFree(entire);
|
memoryFree(entire);
|
||||||
errorThrow("Invalid v0 value in tileset");
|
assetLoaderErrorThrow(loading, "Invalid v0 value in tileset");
|
||||||
}
|
}
|
||||||
|
|
||||||
out->tileCount = out->columns * out->rows;
|
out->tileCount = out->columns * out->rows;
|
||||||
memoryFree(entire);
|
memoryFree(entire);
|
||||||
|
|
||||||
|
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,18 +16,28 @@ errorret_t assetJsonLoaderSync(assetloading_t *loading) {
|
|||||||
assertTrue(loading->type == ASSET_LOADER_TYPE_JSON, "Invalid type.");
|
assertTrue(loading->type == ASSET_LOADER_TYPE_JSON, "Invalid type.");
|
||||||
|
|
||||||
assetfile_t *file = &loading->loading.json.file;
|
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) {
|
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);
|
uint8_t *buffer = memoryAllocate(file->size);
|
||||||
errorChain(assetFileOpen(file));
|
assetLoaderErrorChain(
|
||||||
errorChain(assetFileRead(file, buffer, file->size));
|
loading, assetFileOpen(file)
|
||||||
|
);
|
||||||
|
assetLoaderErrorChain(
|
||||||
|
loading, assetFileRead(file, buffer, file->size)
|
||||||
|
);
|
||||||
assertTrue(file->lastRead == file->size, "Failed to read entire JSON file.");
|
assertTrue(file->lastRead == file->size, "Failed to read entire JSON file.");
|
||||||
errorChain(assetFileClose(file));
|
assetLoaderErrorChain(
|
||||||
errorChain(assetFileDispose(file));
|
loading, assetFileClose(file)
|
||||||
|
);
|
||||||
|
assetLoaderErrorChain(
|
||||||
|
loading, assetFileDispose(file)
|
||||||
|
);
|
||||||
|
|
||||||
loading->entry->data.json = yyjson_read(
|
loading->entry->data.json = yyjson_read(
|
||||||
(char *)buffer,
|
(char *)buffer,
|
||||||
@@ -36,7 +46,11 @@ errorret_t assetJsonLoaderSync(assetloading_t *loading) {
|
|||||||
);
|
);
|
||||||
memoryFree(buffer);
|
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();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,14 @@ errorret_t assetLocaleLoaderSync(assetloading_t *loading) {
|
|||||||
|
|
||||||
assetlocalefile_t *localeFile = &loading->entry->data.locale;
|
assetlocalefile_t *localeFile = &loading->entry->data.locale;
|
||||||
memoryZero(localeFile, sizeof(assetlocalefile_t));
|
memoryZero(localeFile, sizeof(assetlocalefile_t));
|
||||||
errorChain(assetFileInit(&localeFile->file, loading->entry->name, NULL, NULL));
|
assetLoaderErrorChain(loading, assetFileInit(&localeFile->file, loading->entry->name, NULL, NULL));
|
||||||
errorChain(assetFileOpen(&localeFile->file));
|
assetLoaderErrorChain(loading, assetFileOpen(&localeFile->file));
|
||||||
|
|
||||||
char_t buffer[1024];
|
char_t buffer[1024];
|
||||||
errorChain(assetLocaleGetString(localeFile, "", 0, buffer, sizeof(buffer)));
|
assetLoaderErrorChain(loading, assetLocaleGetString(localeFile, "", 0, buffer, sizeof(buffer)));
|
||||||
errorChain(assetLocaleParseHeader(localeFile, buffer, sizeof(buffer)));
|
assetLoaderErrorChain(loading, assetLocaleParseHeader(localeFile, buffer, sizeof(buffer)));
|
||||||
|
|
||||||
|
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ errorret_t textInit(void) {
|
|||||||
);
|
);
|
||||||
assetentry_t *entryTileset = assetGetEntry(
|
assetentry_t *entryTileset = assetGetEntry(
|
||||||
"ui/minogram.dtf", ASSET_LOADER_TYPE_TILESET, NULL
|
"ui/minogram.dtf", ASSET_LOADER_TYPE_TILESET, NULL
|
||||||
|
// "ui/minogram.dtx", ASSET_LOADER_TYPE_TILESET, NULL
|
||||||
);
|
);
|
||||||
errorChain(assetRequireLoaded(entryTexture));
|
errorChain(assetRequireLoaded(entryTexture));
|
||||||
errorChain(assetRequireLoaded(entryTileset));
|
errorChain(assetRequireLoaded(entryTileset));
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include "log/log.h"
|
#include "log/log.h"
|
||||||
|
|
||||||
errorstate_t ERROR_STATE = { 0 };
|
THREAD_LOCAL errorstate_t ERROR_STATE = { 0 };
|
||||||
|
|
||||||
errorret_t errorThrowImpl(
|
errorret_t errorThrowImpl(
|
||||||
errorstate_t *state,
|
errorstate_t *state,
|
||||||
@@ -64,7 +64,7 @@ errorret_t errorOkImpl() {
|
|||||||
ERROR_STATE.code == ERROR_OK,
|
ERROR_STATE.code == ERROR_OK,
|
||||||
"Global error state is not OK (Likely missing errorCatch)"
|
"Global error state is not OK (Likely missing errorCatch)"
|
||||||
);
|
);
|
||||||
|
|
||||||
return (errorret_t) {
|
return (errorret_t) {
|
||||||
.code = ERROR_OK,
|
.code = ERROR_OK,
|
||||||
.state = NULL
|
.state = NULL
|
||||||
@@ -118,6 +118,7 @@ void errorCatch(errorret_t retval) {
|
|||||||
|
|
||||||
// Clear the error state
|
// Clear the error state
|
||||||
memoryFree((void*)retval.state->message);
|
memoryFree((void*)retval.state->message);
|
||||||
|
memoryFree((void*)retval.state->lines);
|
||||||
retval.state->code = ERROR_OK;
|
retval.state->code = ERROR_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-30
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "dusk.h"
|
#include "dusk.h"
|
||||||
|
#include "thread/threadlocal.h"
|
||||||
|
|
||||||
typedef uint8_t errorcode_t;
|
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_PRINT_FORMAT = "Error (%d): %s\n%s";
|
||||||
static const char_t *ERROR_LINE_FORMAT = " at %s:%d in function %s\n";
|
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.
|
* Sets the error state with the provided code and message.
|
||||||
@@ -52,7 +53,7 @@ errorret_t errorThrowImpl(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an error state with no error.
|
* Returns an error state with no error.
|
||||||
*
|
*
|
||||||
* @return An error state with code ERROR_OK.
|
* @return An error state with code ERROR_OK.
|
||||||
*/
|
*/
|
||||||
errorret_t errorOkImpl();
|
errorret_t errorOkImpl();
|
||||||
@@ -88,34 +89,6 @@ void errorCatch(errorret_t retval);
|
|||||||
*/
|
*/
|
||||||
errorret_t errorPrint(const 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.
|
* Throws an error with a formatted message.
|
||||||
*
|
*
|
||||||
@@ -162,4 +135,18 @@ errorret_t errorPrint(const errorret_t retval);
|
|||||||
#define errorOk() \
|
#define errorOk() \
|
||||||
return errorOkImpl()
|
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
|
// EOF
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include "thread/threadlocal.h"
|
||||||
#include "thread/threadmutex.h"
|
#include "thread/threadmutex.h"
|
||||||
|
|
||||||
typedef struct thread_s thread_t;
|
typedef struct thread_s thread_t;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
# https://opensource.org/licenses/MIT
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
add_subdirectory(assert)
|
add_subdirectory(assert)
|
||||||
|
add_subdirectory(error)
|
||||||
|
add_subdirectory(thread)
|
||||||
add_subdirectory(display)
|
add_subdirectory(display)
|
||||||
# add_subdirectory(rpg)
|
# add_subdirectory(rpg)
|
||||||
# add_subdirectory(item)
|
# add_subdirectory(item)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user