Files
dusk/.claude/asset.md
T
2026-06-18 14:59:21 -05:00

4.0 KiB

Asset System

Source: src/dusk/asset/

All game assets are packed into dusk.dsk (a zip archive) at build time and served from it at runtime. The asset system manages async loading, reference counting, and platform-specific archive location.

See architecture.md for the high-level overview.


Asset archive

The archive is opened at assetInit(). assetFileExists(filename) checks for a file without loading it. The file path format inside the archive matches the layout of the assets/ source directory.

On each platform, assetInitPlatform() locates the .dsk file (e.g. adjacent to the binary on Linux, on the SD card on PSP).


Entry lifecycle

An assetentry_t represents one file being managed by the system. States:

NOT_STARTED → PENDING_ASYNC → LOADING_ASYNC → PENDING_SYNC → LOADING_SYNC → LOADED
                                                                           └→ ERROR
  • PENDING_ASYNC / LOADING_ASYNC: the background thread is handling I/O (file reads, decompression).
  • PENDING_SYNC / LOADING_SYNC: the main thread needs to finish loading (e.g. uploading to GPU), triggered during assetUpdate().

The async/sync split exists because GPU operations must happen on the main thread.


Using assets

// Acquire a loaded entry (blocks until loaded):
assetentry_t *entry = assetLock(filename, ASSET_LOADER_TYPE_TEXTURE, &input);
errorChain(assetRequireLoaded(entry));

// Use the loaded data:
texture_t *tex = &entry->data.texture.texture;

// Release when done:
assetUnlockEntry(entry);

assetLock finds-or-creates an entry and increments its reference count. assetUnlock / assetUnlockEntry decrements it; when it reaches zero the entry is reclaimed at the next assetUpdate().

To subscribe to async completion instead of blocking:

eventSubscribe(&entry->onLoaded, myCallback, myUser);

Loader types

Type constant File Output struct accessed via
ASSET_LOADER_TYPE_TEXTURE .png etc. entry->data.texture.texture
ASSET_LOADER_TYPE_TILESET tileset descriptor entry->data.tileset.tileset
ASSET_LOADER_TYPE_MESH mesh data entry->data.mesh.mesh
ASSET_LOADER_TYPE_LOCALE .po file internal to locale manager
ASSET_LOADER_TYPE_JSON .json entry->data.json.*

Each loader type registers loadAsync, loadSync, and dispose callbacks in ASSET_LOADER_CALLBACKS[].

The async callback runs on the loader thread; the sync callback runs on the main thread during assetUpdate(). Most loaders do file I/O async and GPU upload sync.

Error handling inside loaders

Use these macros instead of errorThrow / errorChain inside loader callbacks — they also set the entry state to ERROR:

assetLoaderErrorChain(loading, someCall());
assetLoaderErrorThrow(loading, "Descriptive message");

Low-level file I/O (asset/assetfile.h)

assetfile_t wraps a zip_file_t handle and provides streaming reads:

assetFileInit(&file, "textures/player.png", NULL, NULL);
assetFileOpen(&file);
assetFileRead(&file, buffer, size);
assetFileClose(&file);
assetFileDispose(&file);

// Read entire file into a malloc'd buffer:
uint8_t *buf; size_t size;
assetFileReadEntire(&file, &buf, &size);   // caller frees buf

For line-by-line text parsing (assetfilelinereader_t):

assetFileLineReaderInit(&reader, &file, readBuf, readBufSize, outBuf, outBufSize);
while(!reader.eof) {
  errorChain(assetFileLineReaderNext(&reader));
  // reader.outBuffer contains the line, reader.lineLength its length
}

Background loader thread

assetUpdateAsync(thread) is the thread entry point. It calls assetUpdate() in a loop, sleeping briefly between iterations, until threadShouldStop() returns true. The main thread also calls assetUpdate() once per frame to process the sync phase.

Up to ASSET_LOADING_COUNT_MAX (4) entries can be loading concurrently. Up to ASSET_ENTRY_COUNT_MAX (128) entries can exist at once.