From 1e8311fc04c014f4ead958b97c2e81d35d71826c Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Mon, 1 Jun 2026 22:34:44 -0500 Subject: [PATCH 1/3] Add asset batch --- src/dusk/asset/CMakeLists.txt | 1 + src/dusk/asset/assetbatch.c | 94 +++++++++++++++++++++++++++++++++++ src/dusk/asset/assetbatch.h | 82 ++++++++++++++++++++++++++++++ src/dusk/display/text/text.c | 36 ++++++++------ 4 files changed, 197 insertions(+), 16 deletions(-) create mode 100644 src/dusk/asset/assetbatch.c create mode 100644 src/dusk/asset/assetbatch.h 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/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/display/text/text.c b/src/dusk/display/text/text.c index fe8cc2ba..d87f33ff 100644 --- a/src/dusk/display/text/text.c +++ b/src/dusk/display/text/text.c @@ -10,35 +10,37 @@ #include "util/memory.h" #include "display/spritebatch/spritebatch.h" #include "asset/asset.h" +#include "asset/assetbatch.h" #include "asset/loader/display/assettextureloader.h" #include "asset/loader/display/assettilesetloader.h" #include "display/shader/shaderunlit.h" font_t FONT_DEFAULT; +assetbatch_t TEXT_BATCH; errorret_t textInit(void) { - assetloaderinput_t input = { - .texture = TEXTURE_FORMAT_RGBA - }; - assetentry_t *entryTexture = assetLock( - "ui/minogram.png", ASSET_LOADER_TYPE_TEXTURE, &input - ); - assetentry_t *entryTileset = assetLock( - "ui/minogram.dtf", ASSET_LOADER_TYPE_TILESET, NULL - ); - errorChain(assetRequireLoaded(entryTexture)); - errorChain(assetRequireLoaded(entryTileset)); + assetBatchInit(&TEXT_BATCH, 2, (assetbatchdesc_t[]){ + { + .path = "ui/minogram.png", + .type = ASSET_LOADER_TYPE_TEXTURE, + .input = { .texture = TEXTURE_FORMAT_RGBA } + }, + { + .path = "ui/minogram.dtf", + .type = ASSET_LOADER_TYPE_TILESET + }, + }); + errorChain(assetBatchRequireLoaded(&TEXT_BATCH)); - FONT_DEFAULT.texture = &entryTexture->data.texture; - FONT_DEFAULT.tileset = &entryTileset->data.tileset; + FONT_DEFAULT.texture = &TEXT_BATCH.entries[0]->data.texture; + FONT_DEFAULT.tileset = &TEXT_BATCH.entries[1]->data.tileset; errorOk(); } errorret_t textDispose(void) { FONT_DEFAULT.texture = NULL; FONT_DEFAULT.tileset = NULL; - assetUnlock("ui/minogram.png"); - assetUnlock("ui/minogram.dtf"); + assetBatchDispose(&TEXT_BATCH); errorOk(); } @@ -95,7 +97,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 From 88903fee94f97255a737b8dfcf140ed8706cb891 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Mon, 1 Jun 2026 22:36:02 -0500 Subject: [PATCH 2/3] No need for asset batching on text.c --- src/dusk/display/text/text.c | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/dusk/display/text/text.c b/src/dusk/display/text/text.c index d87f33ff..e7878df7 100644 --- a/src/dusk/display/text/text.c +++ b/src/dusk/display/text/text.c @@ -10,37 +10,33 @@ #include "util/memory.h" #include "display/spritebatch/spritebatch.h" #include "asset/asset.h" -#include "asset/assetbatch.h" #include "asset/loader/display/assettextureloader.h" #include "asset/loader/display/assettilesetloader.h" #include "display/shader/shaderunlit.h" font_t FONT_DEFAULT; -assetbatch_t TEXT_BATCH; errorret_t textInit(void) { - assetBatchInit(&TEXT_BATCH, 2, (assetbatchdesc_t[]){ - { - .path = "ui/minogram.png", - .type = ASSET_LOADER_TYPE_TEXTURE, - .input = { .texture = TEXTURE_FORMAT_RGBA } - }, - { - .path = "ui/minogram.dtf", - .type = ASSET_LOADER_TYPE_TILESET - }, - }); - errorChain(assetBatchRequireLoaded(&TEXT_BATCH)); + assetloaderinput_t input = { .texture = TEXTURE_FORMAT_RGBA }; + assetentry_t *entryTexture = assetLock( + "ui/minogram.png", ASSET_LOADER_TYPE_TEXTURE, &input + ); + assetentry_t *entryTileset = assetLock( + "ui/minogram.dtf", ASSET_LOADER_TYPE_TILESET, NULL + ); + errorChain(assetRequireLoaded(entryTexture)); + errorChain(assetRequireLoaded(entryTileset)); - FONT_DEFAULT.texture = &TEXT_BATCH.entries[0]->data.texture; - FONT_DEFAULT.tileset = &TEXT_BATCH.entries[1]->data.tileset; + FONT_DEFAULT.texture = &entryTexture->data.texture; + FONT_DEFAULT.tileset = &entryTileset->data.tileset; errorOk(); } errorret_t textDispose(void) { FONT_DEFAULT.texture = NULL; FONT_DEFAULT.tileset = NULL; - assetBatchDispose(&TEXT_BATCH); + assetUnlock("ui/minogram.png"); + assetUnlock("ui/minogram.dtf"); errorOk(); } From 3770ae1645261e8a64e84b6dcc0634fb17dd4a8f Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Tue, 2 Jun 2026 07:35:28 -0500 Subject: [PATCH 3/3] Fix tests? --- src/dusk/asset/asset.c | 23 +++++++++++++++++------ src/dusk/asset/loader/assetentry.c | 1 + src/dusk/asset/loader/assetentry.h | 5 +++++ 3 files changed, 23 insertions(+), 6 deletions(-) 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/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;