438 lines
12 KiB
C
438 lines
12 KiB
C
/**
|
|
* Copyright (c) 2026 Dominic Masters
|
|
*
|
|
* This software is released under the MIT License.
|
|
* https://opensource.org/licenses/MIT
|
|
*/
|
|
|
|
#include "asset.h"
|
|
#include "util/memory.h"
|
|
#include "util/string.h"
|
|
#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();
|
|
}
|
|
|
|
bool_t assetFileExists(const char_t *filename) {
|
|
assertStrLenMax(filename, ASSET_FILE_NAME_MAX, "Filename too long.");
|
|
|
|
zip_int64_t idx = zip_name_locate(ASSET.zip, filename, 0);
|
|
if(idx < 0) return false;
|
|
return true;
|
|
}
|
|
|
|
assetentry_t * assetGetEntry(
|
|
const char_t *name,
|
|
const assetloadertype_t type,
|
|
assetloaderinput_t *input
|
|
) {
|
|
// Is there an existing asset?
|
|
assetentry_t *entry = ASSET.entries;
|
|
do {
|
|
if(entry->type == ASSET_LOADER_TYPE_NULL) {
|
|
entry++;
|
|
continue;
|
|
}
|
|
if(stringEquals(entry->name, name)) {
|
|
assertTrue(entry->type == type, "Asset entry type mismatch.");
|
|
return entry;
|
|
}
|
|
entry++;
|
|
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
|
|
|
// We did not find one existing. Find first available slot, reaping
|
|
// zero-ref entries to make room if none are immediately available.
|
|
bool_t reaped = false;
|
|
for(;;) {
|
|
entry = ASSET.entries;
|
|
do {
|
|
if(entry->type != ASSET_LOADER_TYPE_NULL) {
|
|
entry++;
|
|
continue;
|
|
}
|
|
|
|
if(entry->state == ASSET_ENTRY_STATE_NOT_STARTED) {
|
|
assetEntryInit(entry, name, type, input);
|
|
return entry;
|
|
}
|
|
entry++;
|
|
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
|
|
|
if(reaped) break;
|
|
reaped = true;
|
|
errorCatch(assetReapUnused());
|
|
}
|
|
|
|
assertUnreachable("No available asset entry slots.");
|
|
return NULL;
|
|
}
|
|
|
|
uint32_t assetGetEntriesOfType(
|
|
assetentry_t **outEntries,
|
|
const assetloadertype_t type
|
|
) {
|
|
assertNotNull(outEntries, "Output entries cannot be NULL.");
|
|
|
|
uint32_t count = 0;
|
|
assetentry_t *entry = ASSET.entries;
|
|
do {
|
|
if(entry->type == type) {
|
|
outEntries[count++] = entry;
|
|
}
|
|
entry++;
|
|
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
|
|
|
return count;
|
|
}
|
|
|
|
errorret_t assetRequireLoaded(assetentry_t *entry) {
|
|
assertNotNull(entry, "Entry cannot be NULL.");
|
|
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
|
|
assertIsMainThread("Currently only works on main thread.");
|
|
|
|
if(entry->state == ASSET_ENTRY_STATE_LOADED) {
|
|
errorOk();
|
|
}
|
|
|
|
// Lock to prevent the reaper from collecting the entry mid-spin.
|
|
assetEntryLock(entry);
|
|
|
|
while(entry->state != ASSET_ENTRY_STATE_LOADED) {
|
|
usleep(1000);
|
|
errorret_t ret = assetUpdate();
|
|
if(errorIsNotOk(ret)) {
|
|
assetEntryUnlock(entry);
|
|
errorChain(ret);
|
|
}
|
|
}
|
|
|
|
assetEntryUnlock(entry);
|
|
errorOk();
|
|
}
|
|
|
|
errorret_t assetRequireDisposed(assetentry_t *entry) {
|
|
assertNotNull(entry, "Entry cannot be NULL.");
|
|
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
|
|
assertIsMainThread("Currently only works on main thread.");
|
|
|
|
if(entry->type == ASSET_LOADER_TYPE_NULL) {
|
|
errorOk();
|
|
}
|
|
|
|
if(
|
|
entry->state == ASSET_ENTRY_STATE_NOT_STARTED ||
|
|
entry->state == ASSET_ENTRY_STATE_ERROR
|
|
) {
|
|
errorOk();
|
|
}
|
|
|
|
assertTrue(
|
|
entry->refs.count == 0,
|
|
"Cannot require disposal of an entry with active references."
|
|
);
|
|
|
|
// Lock to prevent the reaper from collecting the entry mid-spin.
|
|
assetEntryLock(entry);
|
|
|
|
while(entry->type != ASSET_LOADER_TYPE_NULL) {
|
|
usleep(1000);
|
|
errorret_t ret = assetUpdate();
|
|
if(errorIsNotOk(ret)) {
|
|
assetEntryUnlock(entry);
|
|
errorChain(ret);
|
|
}
|
|
}
|
|
|
|
assetEntryUnlock(entry);
|
|
errorOk();
|
|
}
|
|
|
|
assetentry_t * assetLock(
|
|
const char_t *name,
|
|
const assetloadertype_t type,
|
|
assetloaderinput_t *input
|
|
) {
|
|
assetentry_t *entry = assetGetEntry(name, type, input);
|
|
assetEntryLock(entry);
|
|
return entry;
|
|
}
|
|
|
|
void assetUnlock(const char_t *name) {
|
|
assertNotNull(name, "Name cannot be NULL.");
|
|
|
|
assetentry_t *entry = ASSET.entries;
|
|
do {
|
|
if(
|
|
entry->type != ASSET_LOADER_TYPE_NULL &&
|
|
stringEquals(entry->name, name)
|
|
) {
|
|
assetEntryUnlock(entry);
|
|
return;
|
|
}
|
|
entry++;
|
|
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
|
|
|
assertUnreachable("Asset entry not found for unlock.");
|
|
}
|
|
|
|
void assetUnlockEntry(assetentry_t *entry) {
|
|
assertNotNull(entry, "Entry cannot be NULL.");
|
|
assetEntryUnlock(entry);
|
|
}
|
|
|
|
errorret_t assetReapUnused(void) {
|
|
assertIsMainThread("assetReapUnused must be called from the main thread.");
|
|
|
|
// Repeatedly find and dispose zero-ref LOADED entries until none remain.
|
|
// This handles dependency chains where an entry (e.g. a model) holds refs
|
|
// on child entries (mesh, texture): dispose parents first so child ref
|
|
// counts drop to zero, then pick up the children on the next pass. Without
|
|
// this, a forward-only scan fails when a shared child entry appears before
|
|
// a parent that still holds a ref to it.
|
|
bool_t any;
|
|
do {
|
|
any = false;
|
|
assetentry_t *entry = ASSET.entries;
|
|
do {
|
|
if(entry->type == ASSET_LOADER_TYPE_NULL) { entry++; continue; }
|
|
if(entry->state != ASSET_ENTRY_STATE_LOADED) { entry++; continue; }
|
|
if(entry->refs.count > 0) { entry++; continue; }
|
|
errorChain(assetEntryDispose(entry));
|
|
any = true;
|
|
entry++;
|
|
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
|
} while(any);
|
|
|
|
errorOk();
|
|
}
|
|
|
|
errorret_t assetUpdate(void) {
|
|
assertIsMainThread("assetUpdate must be called from the main thread.");
|
|
|
|
// 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 {
|
|
// 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);
|
|
|
|
|
|
// 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 {
|
|
// Is this asset "ready to start loading" ?
|
|
if(entry->type == ASSET_LOADER_TYPE_NULL) {
|
|
entry++;
|
|
continue;
|
|
}
|
|
|
|
// We only care about assets not started.
|
|
if(entry->state != ASSET_ENTRY_STATE_NOT_STARTED) {
|
|
entry++;
|
|
continue;
|
|
}
|
|
|
|
// Pop a loading slot for this asset entry.
|
|
loading = availableLoading[--availableLoadingCount];
|
|
|
|
// Start loading this asset.
|
|
assetEntryStartLoading(entry, loading);
|
|
entry++;
|
|
|
|
// Did we run out of loading slots?
|
|
if(availableLoadingCount == 0) {
|
|
break;
|
|
}
|
|
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
|
}
|
|
|
|
// 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:
|
|
loading->entry->state = ASSET_ENTRY_STATE_LOADING_SYNC;
|
|
// Unlock before calling loadSync. The sync loader may re-enter
|
|
// assetUpdate (e.g. a script loading another asset), and the async
|
|
// thread never touches LOADING_SYNC entries, so this is safe.
|
|
threadMutexUnlock(&loading->mutex);
|
|
|
|
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(errorIsNotOk(ret)) {
|
|
errorCatch(errorPrint(ret));
|
|
assertTrue(
|
|
loading->entry->state == ASSET_ENTRY_STATE_ERROR,
|
|
"Loader did not set entry state to error on failed load."
|
|
);
|
|
} else if(loading->entry->state == ASSET_ENTRY_STATE_LOADED) {
|
|
assetentry_t *loadedEntry = loading->entry;
|
|
loading->entry = NULL;
|
|
eventInvoke(&loadedEntry->onLoaded, loadedEntry);
|
|
}
|
|
|
|
loading++;
|
|
break;
|
|
|
|
case ASSET_ENTRY_STATE_LOADING_SYNC:
|
|
// A re-entrant assetUpdate call (e.g. from a script loading another
|
|
// asset) will see this entry mid-sync-load. Skip it.
|
|
threadMutexUnlock(&loading->mutex);
|
|
loading++;
|
|
continue;
|
|
|
|
// 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: {
|
|
assetentry_t *errEntry = loading->entry;
|
|
loading->entry = NULL;
|
|
threadMutexUnlock(&loading->mutex);
|
|
eventInvoke(&errEntry->onError, errEntry);
|
|
errorThrow("Failed to load asset asynchronously.");
|
|
break;
|
|
}
|
|
|
|
default:
|
|
threadMutexUnlock(&loading->mutex);
|
|
loading++;
|
|
continue;
|
|
}
|
|
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
|
|
|
|
errorOk();
|
|
}
|
|
|
|
void assetUpdateAsync(thread_t *thread) {
|
|
assertNotMainThread("assetUpdateAsync must not run on the main 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;
|
|
assertNotNull(
|
|
ASSET_LOADER_CALLBACKS[loading->type].loadAsync,
|
|
"Loader does not support async loading."
|
|
);
|
|
errorret_t ret = (
|
|
ASSET_LOADER_CALLBACKS[loading->type].loadAsync(loading)
|
|
);
|
|
|
|
if(errorIsNotOk(ret)) {
|
|
errorCatch(errorPrint(ret));
|
|
assertTrue(
|
|
loading->entry->state == ASSET_ENTRY_STATE_ERROR,
|
|
"Loader did not set entry state to error on failed load."
|
|
);
|
|
}
|
|
|
|
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);
|
|
|
|
if(threadShouldStop(thread)) break;
|
|
usleep(1000);
|
|
}
|
|
}
|
|
|
|
errorret_t assetDispose(void) {
|
|
assertIsMainThread("Must be called from the main thread.");
|
|
threadStop(&ASSET.loadThread);
|
|
|
|
errorChain(assetReapUnused());
|
|
|
|
// Cleanup zip file.
|
|
if(ASSET.zip != NULL) {
|
|
if(zip_close(ASSET.zip) != 0) {
|
|
errorThrow("Failed to close asset zip archive.");
|
|
}
|
|
ASSET.zip = NULL;
|
|
}
|
|
|
|
errorChain(assetDisposePlatform());
|
|
errorOk();
|
|
} |