Prepping for async
This commit is contained in:
+137
-32
@@ -11,15 +11,23 @@
|
||||
#include "assert/assert.h"
|
||||
#include "engine/engine.h"
|
||||
#include "util/string.h"
|
||||
#include "console/console.h"
|
||||
#include <unistd.h>
|
||||
|
||||
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) {
|
||||
|
||||
+13
-2
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -59,4 +59,30 @@ typedef struct {
|
||||
assetloaderdisposecallback_t *dispose;
|
||||
} 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
|
||||
#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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+17
-30
@@ -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
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "thread/threadlocal.h"
|
||||
#include "thread/threadmutex.h"
|
||||
|
||||
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
|
||||
|
||||
add_subdirectory(assert)
|
||||
add_subdirectory(error)
|
||||
add_subdirectory(thread)
|
||||
add_subdirectory(display)
|
||||
# add_subdirectory(rpg)
|
||||
# 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