diff --git a/src/dusk/asset/CMakeLists.txt b/src/dusk/asset/CMakeLists.txt index 39e32cab..0e87c96f 100644 --- a/src/dusk/asset/CMakeLists.txt +++ b/src/dusk/asset/CMakeLists.txt @@ -7,6 +7,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC asset.c + assetbatch.c assetfile.c ) diff --git a/src/dusk/asset/asset.c b/src/dusk/asset/asset.c index aa55e948..791d016c 100644 --- a/src/dusk/asset/asset.c +++ b/src/dusk/asset/asset.c @@ -82,17 +82,23 @@ errorret_t assetRequireLoaded(assetentry_t *entry) { assertNotNull(entry, "Entry cannot be NULL."); assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type."); - // Already loaded? if(entry->state == ASSET_ENTRY_STATE_LOADED) { errorOk(); } - // Not loaded, just spin the wheel + // Lock to prevent the reaper from collecting the entry mid-spin. + assetEntryLock(entry); + while(entry->state != ASSET_ENTRY_STATE_LOADED) { usleep(1000); - errorChain(assetUpdate()); + errorret_t ret = assetUpdate(); + if(errorIsNotOk(ret)) { + assetEntryUnlock(entry); + errorChain(ret); + } } + assetEntryUnlock(entry); errorOk(); } @@ -246,9 +252,9 @@ errorret_t assetUpdate(void) { } while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX); - // Reap entries that have no external locks (refs.count == 1 means only the - // system hold remains). Only safe to reap LOADED and NOT_STARTED states — - // mid-load entries are left for the next cycle. + // Reap entries that have no external locks (refs.count == 0) AND have been + // explicitly locked at least once. Entries that were never locked are left + // alone — raw-pointer callers hold no ref but should not lose the entry. entry = ASSET.entries; do { if(entry->state != ASSET_ENTRY_STATE_LOADED) { @@ -261,6 +267,11 @@ errorret_t assetUpdate(void) { continue; } + if(!entry->wasLocked) { + entry++; + continue; + } + if(entry->refs.count > 0) { entry++; continue; diff --git a/src/dusk/asset/assetbatch.c b/src/dusk/asset/assetbatch.c new file mode 100644 index 00000000..4bcb646d --- /dev/null +++ b/src/dusk/asset/assetbatch.c @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "assetbatch.h" +#include "asset.h" +#include "assert/assert.h" +#include "util/memory.h" +#include + +void assetBatchInit( + assetbatch_t *batch, + const uint16_t count, + const assetbatchdesc_t *descs +) { + assertNotNull(batch, "Batch cannot be NULL."); + assertNotNull(descs, "Descs cannot be NULL."); + assertTrue(count > 0, "Count must be greater than 0."); + assertTrue(count <= ASSET_BATCH_COUNT_MAX, "Count exceeds ASSET_BATCH_COUNT_MAX."); + + memoryZero(batch, sizeof(assetbatch_t)); + batch->count = count; + + for(uint16_t i = 0; i < count; i++) { + // Copy input into batch-owned storage so the descriptor need not persist. + batch->inputs[i] = descs[i].input; + batch->entries[i] = assetLock(descs[i].path, descs[i].type, &batch->inputs[i]); + } +} + +void assetBatchLock(assetbatch_t *batch) { + assertNotNull(batch, "Batch cannot be NULL."); + for(uint16_t i = 0; i < batch->count; i++) { + assetEntryLock(batch->entries[i]); + } +} + +void assetBatchUnlock(assetbatch_t *batch) { + assertNotNull(batch, "Batch cannot be NULL."); + for(uint16_t i = 0; i < batch->count; i++) { + assetEntryUnlock(batch->entries[i]); + } +} + +bool_t assetBatchIsLoaded(const assetbatch_t *batch) { + assertNotNull(batch, "Batch cannot be NULL."); + for(uint16_t i = 0; i < batch->count; i++) { + if(batch->entries[i]->state != ASSET_ENTRY_STATE_LOADED) return false; + } + return true; +} + +bool_t assetBatchHasError(const assetbatch_t *batch) { + assertNotNull(batch, "Batch cannot be NULL."); + for(uint16_t i = 0; i < batch->count; i++) { + if(batch->entries[i]->state == ASSET_ENTRY_STATE_ERROR) return true; + } + return false; +} + +errorret_t assetBatchRequireLoaded(assetbatch_t *batch) { + assertNotNull(batch, "Batch cannot be NULL."); + + bool_t allDone; + do { + allDone = true; + for(uint16_t i = 0; i < batch->count; i++) { + const assetentrystate_t state = batch->entries[i]->state; + if(state == ASSET_ENTRY_STATE_ERROR) { + errorThrow("Asset '%s' failed to load.", batch->entries[i]->name); + } + if(state != ASSET_ENTRY_STATE_LOADED) { + allDone = false; + } + } + if(!allDone) { + usleep(1000); + errorChain(assetUpdate()); + } + } while(!allDone); + + errorOk(); +} + +void assetBatchDispose(assetbatch_t *batch) { + assertNotNull(batch, "Batch cannot be NULL."); + for(uint16_t i = 0; i < batch->count; i++) { + assetUnlockEntry(batch->entries[i]); + } + memoryZero(batch, sizeof(assetbatch_t)); +} diff --git a/src/dusk/asset/assetbatch.h b/src/dusk/asset/assetbatch.h new file mode 100644 index 00000000..db71a115 --- /dev/null +++ b/src/dusk/asset/assetbatch.h @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "asset/loader/assetentry.h" +#include "asset/loader/assetloader.h" + +#define ASSET_BATCH_COUNT_MAX 64 + +typedef struct { + const char_t *path; + assetloadertype_t type; + assetloaderinput_t input; +} assetbatchdesc_t; + +typedef struct { + assetentry_t *entries[ASSET_BATCH_COUNT_MAX]; + assetloaderinput_t inputs[ASSET_BATCH_COUNT_MAX]; + uint16_t count; +} assetbatch_t; + +/** + * Initialises the batch from an array of descriptors. Each entry is locked + * and queued for loading immediately. + * + * @param batch Batch to initialise. + * @param descs Array of entry descriptors (need not outlive this call). + * @param count Number of descriptors (must be <= ASSET_BATCH_COUNT_MAX). + */ +void assetBatchInit( + assetbatch_t *batch, + uint16_t count, + const assetbatchdesc_t *descs +); + +/** + * Acquires one additional lock on every entry in the batch. + * + * @param batch Batch to lock. + */ +void assetBatchLock(assetbatch_t *batch); + +/** + * Releases one lock from every entry in the batch. When an entry's lock + * count reaches zero it will be reaped on the next assetUpdate. + * + * @param batch Batch to unlock. + */ +void assetBatchUnlock(assetbatch_t *batch); + +/** + * Returns true if every entry in the batch has finished loading. + * + * @param batch Batch to query. + */ +bool_t assetBatchIsLoaded(const assetbatch_t *batch); + +/** + * Returns true if any entry in the batch is in an error state. + * + * @param batch Batch to query. + */ +bool_t assetBatchHasError(const assetbatch_t *batch); + +/** + * Blocks until every entry is loaded. Returns an error if any entry fails. + * + * @param batch Batch to wait on. + */ +errorret_t assetBatchRequireLoaded(assetbatch_t *batch); + +/** + * Releases the batch's lock on every entry and clears the batch. After this + * call the batch struct may be reused with assetBatchInit. + * + * @param batch Batch to dispose. + */ +void assetBatchDispose(assetbatch_t *batch); diff --git a/src/dusk/asset/loader/assetentry.c b/src/dusk/asset/loader/assetentry.c index cd86105f..c8a815b7 100644 --- a/src/dusk/asset/loader/assetentry.c +++ b/src/dusk/asset/loader/assetentry.c @@ -33,6 +33,7 @@ void assetEntryInit( void assetEntryLock(assetentry_t *entry) { assertNotNull(entry, "Entry cannot be NULL"); assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type."); + entry->wasLocked = true; refLock(&entry->refs); } diff --git a/src/dusk/asset/loader/assetentry.h b/src/dusk/asset/loader/assetentry.h index aa346948..ab8c38ca 100644 --- a/src/dusk/asset/loader/assetentry.h +++ b/src/dusk/asset/loader/assetentry.h @@ -30,6 +30,11 @@ typedef struct assetentry_s { assetentrystate_t state; // What is referencing this asset entry. ref_t refs; + // True once assetEntryLock has been called at least once. The reaper only + // collects entries that have been explicitly locked (and later unlocked to + // zero). Entries that nobody has ever locked are left alone so raw-pointer + // callers (tests, requireLoaded before locking) are not surprised. + bool_t wasLocked; // Data that will be passed to the loader about how it should load. assetloaderinput_t *input; } assetentry_t; diff --git a/src/dusk/display/text/text.c b/src/dusk/display/text/text.c index fe8cc2ba..e7878df7 100644 --- a/src/dusk/display/text/text.c +++ b/src/dusk/display/text/text.c @@ -17,9 +17,7 @@ font_t FONT_DEFAULT; errorret_t textInit(void) { - assetloaderinput_t input = { - .texture = TEXTURE_FORMAT_RGBA - }; + assetloaderinput_t input = { .texture = TEXTURE_FORMAT_RGBA }; assetentry_t *entryTexture = assetLock( "ui/minogram.png", ASSET_LOADER_TYPE_TEXTURE, &input ); @@ -95,7 +93,9 @@ errorret_t textDraw( float_t posX = x; float_t posY = y; - errorChain(shaderSetTexture(&SHADER_UNLIT, SHADER_UNLIT_TEXTURE, font->texture)); + errorChain(shaderSetTexture( + &SHADER_UNLIT, SHADER_UNLIT_TEXTURE, font->texture + )); #if MESH_ENABLE_COLOR #else