Chunks, models, and more

This commit is contained in:
2026-07-01 08:35:01 -05:00
parent a34831aa02
commit a9ab8dc1d8
94 changed files with 941 additions and 130 deletions
+19 -9
View File
@@ -411,16 +411,26 @@ errorret_t assetDispose(void) {
assertIsMainThread("Must be called from the main thread.");
threadStop(&ASSET.loadThread);
// Dispose every non-null entry so type-specific dispose callbacks
// (e.g. assetScriptDispose freeing jerry values) run before the
// scripting engine is torn down.
assetentry_t *entry = ASSET.entries;
// Drain-dispose: 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 {
if(entry->type != ASSET_LOADER_TYPE_NULL) {
errorChain(assetEntryDispose(entry));
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
any = false;
assetentry_t *e = ASSET.entries;
do {
if(e->type == ASSET_LOADER_TYPE_NULL) { e++; continue; }
if(e->state != ASSET_ENTRY_STATE_LOADED) { e++; continue; }
if(e->refs.count > 0) { e++; continue; }
errorChain(assetEntryDispose(e));
any = true;
e++;
} while(e < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
} while(any);
// Cleanup zip file.
if(ASSET.zip != NULL) {
+6
View File
@@ -16,6 +16,12 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
.dispose = assetMeshDispose
},
[ASSET_LOADER_TYPE_MODEL] = {
.loadSync = assetModelLoaderSync,
.loadAsync = assetModelLoaderAsync,
.dispose = assetModelDispose
},
[ASSET_LOADER_TYPE_TEXTURE] = {
.loadSync = assetTextureLoaderSync,
.loadAsync = assetTextureLoaderAsync,
+4
View File
@@ -7,6 +7,7 @@
#pragma once
#include "asset/loader/dmf/assetmeshloader.h"
#include "asset/loader/dmf/assetmodelloader.h"
#include "asset/loader/display/assettextureloader.h"
#include "asset/loader/display/assettilesetloader.h"
#include "asset/loader/locale/assetlocaleloader.h"
@@ -17,6 +18,7 @@ typedef enum {
ASSET_LOADER_TYPE_NULL,
ASSET_LOADER_TYPE_MESH,
ASSET_LOADER_TYPE_MODEL,
ASSET_LOADER_TYPE_TEXTURE,
ASSET_LOADER_TYPE_TILESET,
ASSET_LOADER_TYPE_LOCALE,
@@ -28,6 +30,7 @@ typedef enum {
typedef union {
assetmeshloaderloading_t mesh;
assetmodelloaderloading_t model;
assettextureloaderloading_t texture;
assettilesetloaderloading_t tileset;
assetlocaleloaderloading_t locale;
@@ -37,6 +40,7 @@ typedef union {
typedef union {
assetmeshoutput_t mesh;
assetmodeloutput_t model;
assettextureoutput_t texture;
assettilesetoutput_t tileset;
assetlocaleoutput_t locale;
@@ -100,8 +100,8 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
) {
nameLen++;
}
memoryCopy(out->meshNames[m], data + offset, nameLen);
out->meshNames[m][nameLen] = '\0';
memoryCopy(out->modelNames[m], data + offset, nameLen);
out->modelNames[m][nameLen] = '\0';
offset += nameLen + 1;
memoryCopy(
@@ -34,7 +34,7 @@ typedef struct {
typedef struct {
tile_t tiles[CHUNK_TILE_COUNT];
uint8_t meshCount;
char_t meshNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
} assetchunkoutput_t;
@@ -51,7 +51,7 @@ errorret_t assetChunkLoaderAsync(assetloading_t *loading);
/**
* Synchronous loader for chunk assets. Validates the DCF binary previously
* read by the async phase and populates the output assetchunkoutput_t with
* tile data and DMF mesh names.
* tile data and model paths.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
+1
View File
@@ -6,4 +6,5 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
assetmeshloader.c
assetmodelloader.c
)
@@ -0,0 +1,171 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetmodelloader.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "asset/asset.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
#include "asset/loader/json/assetjsonloader.h"
#include "display/texture/texture.h"
#include "yyjson.h"
errorret_t assetModelLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread.");
errorOk();
}
errorret_t assetModelLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_MODEL, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
switch(loading->loading.model.state) {
case ASSET_MODEL_LOADING_STATE_INITIAL:
loading->loading.model.state = ASSET_MODEL_LOADING_STATE_LOCK_ASSETS;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
break;
case ASSET_MODEL_LOADING_STATE_LOCK_ASSETS: {
// Lock the model's JSON descriptor as a JSON sub-asset. The entry
// key is prefixed with "json:" to avoid a type-collision with the
// model entry itself (both share the same filename). The JSON loader
// reads the real file path supplied in the input.
char_t jsonKey[ASSET_FILE_NAME_MAX];
stringFormat(
jsonKey, sizeof(jsonKey), "json:%s", loading->entry->name
);
assetloaderinput_t jsonInput;
memoryZero(&jsonInput, sizeof(jsonInput));
stringCopy(
jsonInput.json.path, loading->entry->name, ASSET_FILE_NAME_MAX
);
assetentry_t *jsonEntry = assetLock(
jsonKey, ASSET_LOADER_TYPE_JSON, &jsonInput
);
errorret_t ret = assetRequireLoaded(jsonEntry);
if(errorIsNotOk(ret)) {
assetUnlockEntry(jsonEntry);
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
errorChain(ret);
}
yyjson_val *root = yyjson_doc_get_root(jsonEntry->data.json);
// Parse required mesh path.
yyjson_val *meshVal = yyjson_obj_get(root, "mesh");
if(!meshVal || !yyjson_is_str(meshVal)) {
assetUnlockEntry(jsonEntry);
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
errorThrow("Model JSON missing 'mesh' string");
}
const char *meshStr = yyjson_get_str(meshVal);
size_t meshLen = yyjson_get_len(meshVal);
if(meshLen >= ASSET_FILE_NAME_MAX) {
assetUnlockEntry(jsonEntry);
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
errorThrow("Model mesh path exceeds max length");
}
char_t meshName[ASSET_FILE_NAME_MAX];
memoryCopy(meshName, meshStr, meshLen + 1);
// Parse optional texture path (absent = color only, no texture).
char_t textureName[ASSET_FILE_NAME_MAX];
textureName[0] = '\0';
yyjson_val *texVal = yyjson_obj_get(root, "texture");
if(texVal && yyjson_is_str(texVal)) {
const char *texStr = yyjson_get_str(texVal);
size_t texLen = yyjson_get_len(texVal);
if(texLen >= ASSET_FILE_NAME_MAX) {
assetUnlockEntry(jsonEntry);
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
errorThrow("Model texture path exceeds max length");
}
memoryCopy(textureName, texStr, texLen + 1);
}
// Parse optional color (RGBA array 0-255, default white).
color_t color = COLOR_WHITE;
yyjson_val *colorVal = yyjson_obj_get(root, "color");
if(colorVal && yyjson_is_arr(colorVal)) {
uint8_t ch[4] = {255, 255, 255, 255};
size_t colorIdx, colorLen;
yyjson_val *colorElem;
yyjson_arr_foreach(colorVal, colorIdx, colorLen, colorElem) {
if(colorIdx >= 4) break;
if(yyjson_is_int(colorElem)) {
ch[colorIdx] = (uint8_t)yyjson_get_int(colorElem);
}
}
color.r = ch[0];
color.g = ch[1];
color.b = ch[2];
color.a = ch[3];
}
// Release JSON entry; all needed fields are now in local variables.
assetUnlockEntry(jsonEntry);
// Lock and load the mesh sub-asset.
assetentry_t *meshEntry = assetLock(meshName, ASSET_LOADER_TYPE_MESH, NULL);
ret = assetRequireLoaded(meshEntry);
if(errorIsNotOk(ret)) {
assetUnlockEntry(meshEntry);
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
errorChain(ret);
}
// Lock and load the optional texture sub-asset.
assetentry_t *texEntry = NULL;
if(textureName[0] != '\0') {
assetloaderinput_t texInput = { .texture = TEXTURE_FORMAT_RGBA };
texEntry = assetLock(textureName, ASSET_LOADER_TYPE_TEXTURE, &texInput);
ret = assetRequireLoaded(texEntry);
if(errorIsNotOk(ret)) {
assetUnlockEntry(texEntry);
assetUnlockEntry(meshEntry);
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
errorChain(ret);
}
}
assetmodeloutput_t *out = &loading->entry->data.model;
out->meshEntry = meshEntry;
out->texEntry = texEntry;
out->color = color;
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
break;
}
default:
errorOk();
}
errorOk();
}
errorret_t assetModelDispose(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_MODEL, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
assetmodeloutput_t *out = &entry->data.model;
if(out->meshEntry != NULL) {
assetUnlockEntry(out->meshEntry);
out->meshEntry = NULL;
}
if(out->texEntry != NULL) {
assetUnlockEntry(out->texEntry);
out->texEntry = NULL;
}
errorOk();
}
@@ -0,0 +1,58 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "display/color.h"
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef enum {
ASSET_MODEL_LOADING_STATE_INITIAL,
ASSET_MODEL_LOADING_STATE_LOCK_ASSETS
} assetmodelloadingstate_t;
typedef struct {
assetmodelloadingstate_t state;
} assetmodelloaderloading_t;
typedef struct {
assetentry_t *meshEntry;
assetentry_t *texEntry;
color_t color;
} assetmodeloutput_t;
/**
* Async loader stub for model assets. Model loading is performed entirely
* on the main thread; this callback is never reached.
*
* @param loading Loading information for the asset being loaded.
* @returns Always success.
*/
errorret_t assetModelLoaderAsync(assetloading_t *loading);
/**
* Synchronous loader for model assets. Locks the JSON descriptor file as
* a sub-asset, waits for it to load, parses the mesh path, optional
* texture path, and color tint, then releases the JSON entry. Locks and
* waits for mesh and optional texture sub-assets sequentially to stay
* within ASSET_LOADING_COUNT_MAX slot limits.
*
* @param loading Loading information for the asset being loaded.
* @returns Error code indicating success or failure.
*/
errorret_t assetModelLoaderSync(assetloading_t *loading);
/**
* Disposer for model assets. Releases the locks on the mesh and texture
* sub-asset entries.
*
* @param entry Asset entry containing the model to dispose.
* @returns Error code indicating success or failure.
*/
errorret_t assetModelDispose(assetentry_t *entry);
+5 -3
View File
@@ -22,9 +22,11 @@ errorret_t assetJsonLoaderAsync(assetloading_t *loading) {
assertNull(loading->loading.json.buffer, "Buffer already defined?");
assetfile_t *file = &loading->loading.json.file;
assetLoaderErrorChain(loading,
assetFileInit(file, loading->entry->name, NULL, NULL)
);
const char_t *filePath = (
loading->entry->input != NULL &&
loading->entry->input->json.path[0] != '\0'
) ? loading->entry->input->json.path : loading->entry->name;
assetLoaderErrorChain(loading, assetFileInit(file, filePath, NULL, NULL));
if(file->size > ASSET_JSON_FILE_SIZE_MAX) {
assetLoaderErrorThrow(loading, "JSON exceeds maximum allowed size");
+10 -1
View File
@@ -14,7 +14,16 @@
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef struct { void *nothing; } assetjsonloaderinput_t;
/**
* Optional file path override. When path[0] is non-zero the JSON loader
* reads this path from the archive instead of loading->entry->name. Use
* this to decouple the asset pool key from the actual file location (e.g.
* when a parent loader locks a JSON entry under a synthetic key to avoid
* a type-collision with an entry of the same name but different type).
*/
typedef struct {
char_t path[ASSET_FILE_NAME_MAX];
} assetjsonloaderinput_t;
typedef enum {
ASSET_JSON_LOADING_STATE_INITIAL,
+1 -1
View File
@@ -17,7 +17,7 @@ console_t CONSOLE;
void consoleInit(void) {
memoryZero(&CONSOLE, sizeof(console_t));
CONSOLE.visible = false;
CONSOLE.visible = true;
#ifdef DUSK_CONSOLE_POSIX
threadMutexInit(&CONSOLE.printMutex);
+2 -2
View File
@@ -22,10 +22,10 @@ typedef struct chunk_s {
assetentry_t *dcfEntry;
uint8_t meshCount;
char_t meshNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
mat4 meshModels[CHUNK_MESH_COUNT_MAX];
assetentry_t *meshEntries[CHUNK_MESH_COUNT_MAX];
assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
uint8_t entities[CHUNK_ENTITY_COUNT_MAX];
} chunk_t;
+59 -64
View File
@@ -148,9 +148,9 @@ void mapChunkUnload(chunk_t *chunk) {
}
for(uint8_t m = 0; m < chunk->meshCount; m++) {
if(chunk->meshEntries[m] == NULL) continue;
assetUnlockEntry(chunk->meshEntries[m]);
chunk->meshEntries[m] = NULL;
if(chunk->modelEntries[m] == NULL) continue;
assetUnlockEntry(chunk->modelEntries[m]);
chunk->modelEntries[m] = NULL;
}
chunk->meshCount = 0;
}
@@ -183,6 +183,7 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
}
errorret_t mapWaitForChunks() {
// Phase 1: Wait for all DCF entries to finish loading.
bool_t anyPending;
do {
anyPending = false;
@@ -190,76 +191,70 @@ errorret_t mapWaitForChunks() {
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = &MAP.chunks[i];
if(chunk->dcfEntry == NULL) continue;
if(chunk->dcfEntry != NULL) {
assetentrystate_t state = chunk->dcfEntry->state;
assetentrystate_t state = chunk->dcfEntry->state;
if(state == ASSET_ENTRY_STATE_LOADED) {
uint8_t meshCount = chunk->dcfEntry->data.chunk.meshCount;
memoryCopy(
chunk->tiles,
chunk->dcfEntry->data.chunk.tiles,
sizeof(chunk->tiles)
);
worldpos_t wp;
chunkPosToWorldPos(&chunk->position, &wp);
vec3 wpf = { (float_t)wp.x, (float_t)wp.y, (float_t)wp.z };
if(state == ASSET_ENTRY_STATE_LOADED) {
uint8_t meshCount = chunk->dcfEntry->data.chunk.meshCount;
memoryCopy(
chunk->tiles,
chunk->dcfEntry->data.chunk.tiles,
sizeof(chunk->tiles)
for(uint8_t m = 0; m < meshCount; m++) {
stringCopy(
chunk->modelNames[m],
chunk->dcfEntry->data.chunk.modelNames[m],
CHUNK_MESH_NAME_MAX
);
worldpos_t wp;
chunkPosToWorldPos(&chunk->position, &wp);
vec3 wpf = { (float_t)wp.x, (float_t)wp.y, (float_t)wp.z };
for(uint8_t m = 0; m < meshCount; m++) {
stringCopy(
chunk->meshNames[m],
chunk->dcfEntry->data.chunk.meshNames[m],
CHUNK_MESH_NAME_MAX
);
glm_vec3_copy(
chunk->dcfEntry->data.chunk.meshOffsets[m],
chunk->meshOffsets[m]
);
vec3 pos;
glm_vec3_add(wpf, chunk->meshOffsets[m], pos);
glm_translate_make(chunk->meshModels[m], pos);
}
assetUnlockEntry(chunk->dcfEntry);
chunk->dcfEntry = NULL;
for(uint8_t m = 0; m < meshCount; m++) {
chunk->meshEntries[m] = assetLock(
chunk->meshNames[m], ASSET_LOADER_TYPE_MESH, NULL
);
if(chunk->meshEntries[m] == NULL) {
for(uint8_t j = 0; j < m; j++) {
assetUnlockEntry(chunk->meshEntries[j]);
chunk->meshEntries[j] = NULL;
}
errorThrow("Failed to lock mesh: %s", chunk->meshNames[m]);
}
}
chunk->meshCount = meshCount;
// Fall through to the mesh-state check below so cached meshes
// don't force an unnecessary extra sleep iteration.
} else if(state == ASSET_ENTRY_STATE_ERROR) {
assetUnlockEntry(chunk->dcfEntry);
chunk->dcfEntry = NULL;
memorySet(chunk->tiles, TILE_SHAPE_GROUND, sizeof(chunk->tiles));
continue;
} else {
anyPending = true;
continue;
}
}
for(uint8_t m = 0; m < chunk->meshCount; m++) {
if(chunk->meshEntries[m] == NULL) continue;
if(chunk->meshEntries[m]->state != ASSET_ENTRY_STATE_LOADED) {
anyPending = true;
break;
glm_vec3_copy(
chunk->dcfEntry->data.chunk.meshOffsets[m],
chunk->meshOffsets[m]
);
vec3 pos;
glm_vec3_add(wpf, chunk->meshOffsets[m], pos);
glm_translate_make(chunk->meshModels[m], pos);
}
assetUnlockEntry(chunk->dcfEntry);
chunk->dcfEntry = NULL;
chunk->meshCount = meshCount;
} else if(state == ASSET_ENTRY_STATE_ERROR) {
assetUnlockEntry(chunk->dcfEntry);
chunk->dcfEntry = NULL;
memorySet(chunk->tiles, TILE_SHAPE_GROUND, sizeof(chunk->tiles));
} else {
anyPending = true;
}
}
if(anyPending) usleep(1000);
} while(anyPending);
// Phase 2: Load model entries one at a time.
// Models are locked and waited on sequentially so that only one model
// entry occupies a loading slot at a time. This leaves the remaining
// slots free for the model's mesh and texture sub-assets, preventing
// the slot-exhaustion deadlock that occurs when multiple models are
// assigned slots simultaneously and each blocks waiting for sub-assets
// that have nowhere to run.
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = &MAP.chunks[i];
for(uint8_t m = 0; m < chunk->meshCount; m++) {
if(chunk->modelEntries[m] != NULL) continue;
chunk->modelEntries[m] = assetLock(
chunk->modelNames[m], ASSET_LOADER_TYPE_MODEL, NULL
);
if(chunk->modelEntries[m] == NULL) {
errorThrow("Failed to lock model: %s", chunk->modelNames[m]);
}
errorChain(assetRequireLoaded(chunk->modelEntries[m]));
}
}
errorOk();
}
+16 -32
View File
@@ -24,27 +24,8 @@
#include "asset/loader/assetloader.h"
#include "asset/loader/assetentry.h"
#define TEXTURE_CHUNK_SIZE 16
static texture_t TEXTURE_CHUNK;
static color_t TEXTURE_CHUNK_PIXELS[TEXTURE_CHUNK_SIZE * TEXTURE_CHUNK_SIZE];
errorret_t sceneOverworldInit(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null");
for(uint32_t i = 0; i < TEXTURE_CHUNK_SIZE * TEXTURE_CHUNK_SIZE; i++) {
uint8_t r = (uint8_t)((i & 7) * 36);
uint8_t g = (uint8_t)(((i >> 3) & 7) * 36);
uint8_t b = (uint8_t)((i >> 6) * 85);
TEXTURE_CHUNK_PIXELS[i] = color4b(r, g, b, 255);
}
errorChain(textureInit(
&TEXTURE_CHUNK,
TEXTURE_CHUNK_SIZE, TEXTURE_CHUNK_SIZE,
TEXTURE_FORMAT_RGBA,
(texturedata_t){ .rgbaColors = TEXTURE_CHUNK_PIXELS }
));
errorOk();
}
@@ -138,25 +119,31 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) {
}
errorret_t sceneOverworldDrawChunks() {
shadermaterial_t chunkMaterial = {
.unlit = {
.color = COLOR_WHITE,
.texture = &TEXTURE_CHUNK
}
};
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = &MAP.chunks[i];
if(chunk->meshCount == 0) continue;
for(uint8_t m = 0; m < chunk->meshCount; m++) {
if(chunk->meshEntries[m] == NULL) continue;
if(chunk->modelEntries[m] == NULL) continue;
if(chunk->modelEntries[m]->state != ASSET_ENTRY_STATE_LOADED) continue;
assetmodeloutput_t *model = &chunk->modelEntries[m]->data.model;
if(model->meshEntry == NULL) continue;
texture_t *tex = model->texEntry != NULL
? &model->texEntry->data.texture
: NULL;
shadermaterial_t mat = {
.unlit = { .color = model->color, .texture = tex }
};
errorChain(shaderSetMatrix(
&SHADER_UNLIT, SHADER_UNLIT_MODEL, chunk->meshModels[m]
));
errorChain(shaderSetMaterial(&SHADER_UNLIT, &chunkMaterial));
errorChain(meshDraw(&chunk->meshEntries[m]->data.mesh.mesh, 0, -1));
errorChain(shaderSetMaterial(&SHADER_UNLIT, &mat));
errorChain(meshDraw(
&model->meshEntry->data.mesh.mesh, 0, -1
));
}
}
@@ -171,8 +158,5 @@ errorret_t sceneOverworldDrawChunks() {
errorret_t sceneOverworldDispose(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null");
errorChain(textureDispose(&TEXTURE_CHUNK));
errorOk();
}