/* SPDX-License-Identifier: MIT */ /* * konassets.c * * Copyright (c) 2026 KonAki * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include "konassets.h" #include #include #include #ifdef KON_MODULE_IMAGE #include "stb_image.h" #endif #ifdef KON_MODULE_AUDIO #include "miniaudio.h" #endif #define KON_ASSET_LOADERS_ 32 typedef struct kon_assetEntry_ { char *name; void *object; int type; int refs; size_t bytes; int loose; int unbound; /* set while collecting, once the object has given back what it was using */ } kon_assetEntry_t; struct kon_assets { kon_pack_t *pack; char *looseDir; kon_assetLoader_t loaders[KON_ASSET_LOADERS_]; int loaderUsed[KON_ASSET_LOADERS_]; int nextCustom; kon_assetEntry_t *entries; int count, capacity; int *table; /* hash of name to entry number plus one, 0 is empty. the size is a power of two */ int tableSize; const char *error; }; /*** built in loaders ***/ static void *kon_loadBlob_(const uint8_t *data, size_t size, size_t *bytes) { kon_blob_t *blob = malloc(sizeof(kon_blob_t)); if (!blob) return NULL; blob->data = malloc(size + 1); if (!blob->data) { free(blob); return NULL; } memcpy(blob->data, data, size); blob->data[size] = 0; blob->size = size; *bytes = size; return blob; } static void kon_unloadBlob_(void *object) { kon_blob_t *blob = object; free(blob->data); free(blob); } #ifdef KON_MODULE_IMAGE static void *kon_loadImageAsset_(const uint8_t *data, size_t size, size_t *bytes) { int width, height, channels; unsigned char *pixels; kon_image *image; if (size > INT_MAX) return NULL; pixels = stbi_load_from_memory(data, (int)size, &width, &height, &channels, 4); if (!pixels) return NULL; image = kon_loadImage(pixels, width, height, konFormatRGBA8); stbi_image_free(pixels); if (image) *bytes = (size_t)width * (size_t)height * 4; return image; } static void kon_unloadImageAsset_(void *object) { kon_freeImage(object); } #endif #ifdef KON_MODULE_AUDIO #define KON_SOUND_MAX_BYTES_ ((uint64_t)1 << 30) static void *kon_loadSoundAsset_(const uint8_t *data, size_t size, size_t *bytes) { ma_decoder_config config = ma_decoder_config_init(ma_format_s16, 0, 0); ma_decoder decoder; kon_sound_t *sound; int16_t *frames = NULL; uint64_t total = 0, capacity = 0; uint32_t channels; if (ma_decoder_init_memory(data, size, &config, &decoder) != MA_SUCCESS) return NULL; channels = (uint32_t)decoder.outputChannels; if (channels == 0) { ma_decoder_uninit(&decoder); return NULL; } /* the length is not always known up front, so read until the decoder runs dry */ for (;;) { ma_uint64 got = 0; if (total == capacity) { uint64_t grown = capacity ? capacity * 2 : 16384; int16_t *more; if (grown * channels * sizeof(int16_t) > KON_SOUND_MAX_BYTES_) break; more = realloc(frames, (size_t)(grown * channels * sizeof(int16_t))); if (!more) { free(frames); ma_decoder_uninit(&decoder); return NULL; } frames = more; capacity = grown; } if (ma_decoder_read_pcm_frames(&decoder, frames + total * channels, capacity - total, &got) != MA_SUCCESS && got == 0) break; if (got == 0) break; total += got; } sound = malloc(sizeof(kon_sound_t)); if (!sound || total == 0) { free(sound); free(frames); ma_decoder_uninit(&decoder); return NULL; } sound->frames = frames; sound->frameCount = total; sound->channels = channels; sound->sampleRate = (uint32_t)decoder.outputSampleRate; *bytes = (size_t)(total * channels * sizeof(int16_t)); ma_decoder_uninit(&decoder); return sound; } static void kon_unloadSoundAsset_(void *object) { kon_sound_t *sound = object; free(sound->frames); free(sound); } #endif /*** private helper ***/ static int kon_extensionMatches_(const char *name, const char *extensions) { const char *dot = strrchr(name, '.'); const char *slash = strrchr(name, '/'); size_t length; if (!dot || (slash && dot < slash) || !extensions) return 0; length = strlen(dot); while (*extensions) { const char *end; size_t tokenLength; while (*extensions == ' ') extensions++; end = extensions; while (*end && *end != ' ') end++; tokenLength = (size_t)(end - extensions); if (tokenLength == length) { size_t i; for (i = 0; i < length; i++) { char c = dot[i]; if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); if (c != extensions[i]) break; } if (i == length) return 1; } extensions = end; } return 0; } static int kon_nameIsSafe_(const char *name) { const char *segment = name; if (!name || name[0] == '\0' || name[0] == '/') return 0; for (const char *c = name; ; c++) { if (*c == '\\' || *c == ':') return 0; if (*c == '/' || *c == '\0') { if (c - segment == 2 && segment[0] == '.' && segment[1] == '.') return 0; if (*c == '\0') break; segment = c + 1; } } return 1; } static int kon_typeForName_(const kon_assets_t *assets, const char *name) { for (int i = 1; i < KON_ASSET_LOADERS_; i++) { if (assets->loaderUsed[i] && kon_extensionMatches_(name, assets->loaders[i].extensions)) return i; } return KON_ASSET_BLOB; } static uint32_t kon_hashName_(const char *name) { uint32_t hash = 2166136261u; while (*name) { hash ^= (uint8_t)*name++; hash *= 16777619u; } return hash; } static void kon_tableInsert_(kon_assets_t *assets, int index) { uint32_t mask = (uint32_t)assets->tableSize - 1; uint32_t slot = kon_hashName_(assets->entries[index].name) & mask; while (assets->table[slot]) slot = (slot + 1) & mask; assets->table[slot] = index + 1; } /* the table is built again whenever entries move or it gets too full. if it can't be built, lookups just search the list */ static void kon_tableRebuild_(kon_assets_t *assets) { int size = 64; int *table; while (size < assets->count * 2 + 2) size *= 2; if (size != assets->tableSize) { table = realloc(assets->table, (size_t)size * sizeof *table); if (!table) { free(assets->table); assets->table = NULL; assets->tableSize = 0; return; } assets->table = table; assets->tableSize = size; } memset(assets->table, 0, (size_t)assets->tableSize * sizeof *assets->table); for (int i = 0; i < assets->count; i++) kon_tableInsert_(assets, i); } static kon_assetEntry_t *kon_findEntry_(const kon_assets_t *assets, const char *name) { if (assets->tableSize) { uint32_t mask = (uint32_t)assets->tableSize - 1; uint32_t slot = kon_hashName_(name) & mask; while (assets->table[slot]) { kon_assetEntry_t *entry = &assets->entries[assets->table[slot] - 1]; if (strcmp(entry->name, name) == 0) return entry; slot = (slot + 1) & mask; } return NULL; } for (int i = 0; i < assets->count; i++) { if (strcmp(assets->entries[i].name, name) == 0) return &assets->entries[i]; } return NULL; } static uint8_t *kon_readFile_(kon_assets_t *assets, const char *name, size_t *size, int *loose) { *loose = 0; #ifdef KON_DEVELOPER if (assets->looseDir) { size_t length = strlen(assets->looseDir) + strlen(name) + 2; char *path = malloc(length); FILE *f; if (!path) { assets->error = "out of memory"; return NULL; } snprintf(path, length, "%s/%s", assets->looseDir, name); f = fopen(path, "rb"); free(path); if (f) { long end; uint8_t *data = NULL; if (fseek(f, 0, SEEK_END) == 0 && (end = ftell(f)) >= 0 && fseek(f, 0, SEEK_SET) == 0) { data = malloc((size_t)end + 1); if (data && fread(data, 1, (size_t)end, f) == (size_t)end) { data[end] = 0; *size = (size_t)end; *loose = 1; fclose(f); return data; } free(data); } fclose(f); assets->error = "could not read the loose file"; return NULL; } } #endif if (assets->pack) { int index = kon_packFind(assets->pack, name); if (index >= 0) { uint8_t *data = kon_packRead(assets->pack, index, size); if (!data) assets->error = kon_packError(); return data; } } assets->error = "file not found"; return NULL; } static void kon_unloadEntry_(kon_assets_t *assets, kon_assetEntry_t *entry) { if (assets->loaderUsed[entry->type] && assets->loaders[entry->type].unload) { assets->loaders[entry->type].unload(entry->object); } free(entry->name); } /*** assets ***/ static void kon_addLoader_(kon_assets_t *assets, int type, const char *name, const char *extensions, void *(*load)(const uint8_t *, size_t, size_t *), void (*unload)(void *)) { assets->loaders[type].name = name; assets->loaders[type].extensions = extensions; assets->loaders[type].load = load; assets->loaders[type].unload = unload; assets->loaders[type].bind = NULL; assets->loaders[type].unbind = NULL; assets->loaderUsed[type] = 1; } kon_assets_t *kon_assetsCreate(kon_pack_t *pack, const char *looseDir) { kon_assets_t *assets = calloc(1, sizeof(kon_assets_t)); if (!assets) return NULL; assets->pack = pack; assets->error = "no error"; assets->nextCustom = KON_ASSET_CUSTOM; #ifdef KON_DEVELOPER if (looseDir) { assets->looseDir = malloc(strlen(looseDir) + 1); if (!assets->looseDir) { free(assets); return NULL; } strcpy(assets->looseDir, looseDir); } #else (void)looseDir; #endif kon_addLoader_(assets, KON_ASSET_BLOB, "blob", NULL, kon_loadBlob_, kon_unloadBlob_); #ifdef KON_MODULE_IMAGE kon_addLoader_(assets, KON_ASSET_IMAGE, "image", ".png .jpg .jpeg .bmp .tga .gif", kon_loadImageAsset_, kon_unloadImageAsset_); #endif #ifdef KON_MODULE_AUDIO kon_addLoader_(assets, KON_ASSET_SOUND, "sound", ".wav .mp3 .flac", kon_loadSoundAsset_, kon_unloadSoundAsset_); #endif return assets; } void kon_assetsDestroy(kon_assets_t *assets) { if (!assets) return; for (int i = 0; i < assets->count; i++) kon_unloadEntry_(assets, &assets->entries[i]); free(assets->entries); free(assets->table); free(assets->looseDir); free(assets); } int kon_assetsRegister(kon_assets_t *assets, const kon_assetLoader_t *loader) { int type; if (!assets || !loader || !loader->load || !loader->extensions || !loader->name) return -1; if (assets->nextCustom >= KON_ASSET_LOADERS_) return -1; type = assets->nextCustom++; kon_addLoader_(assets, type, loader->name, loader->extensions, loader->load, loader->unload); assets->loaders[type].bind = loader->bind; assets->loaders[type].unbind = loader->unbind; return type; } void *kon_assetGet(kon_assets_t *assets, const char *name) { return kon_assetGetAs(assets, name, -1); } void *kon_assetGetAs(kon_assets_t *assets, const char *name, int type) { kon_assetEntry_t *entry; uint8_t *data; size_t size = 0, bytes = 0; int loose, found; void *object; if (!assets) return NULL; if (!kon_nameIsSafe_(name)) { assets->error = "bad asset name"; return NULL; } entry = kon_findEntry_(assets, name); if (entry) { if (type >= 0 && entry->type != type) { assets->error = "asset is a different type"; return NULL; } entry->refs++; return entry->object; } found = kon_typeForName_(assets, name); if (type >= 0 && found != type) { assets->error = "asset is a different type"; return NULL; } data = kon_readFile_(assets, name, &size, &loose); if (!data) return NULL; object = assets->loaders[found].load(data, size, &bytes); free(data); if (!object) { assets->error = "could not decode the file"; return NULL; } if (assets->count == assets->capacity) { int capacity = assets->capacity ? assets->capacity * 2 : 32; kon_assetEntry_t *grown = realloc(assets->entries, (size_t)capacity * sizeof(kon_assetEntry_t)); if (!grown) { assets->loaders[found].unload(object); assets->error = "out of memory"; return NULL; } assets->entries = grown; assets->capacity = capacity; } entry = &assets->entries[assets->count]; entry->name = malloc(strlen(name) + 1); if (!entry->name) { assets->loaders[found].unload(object); assets->error = "out of memory"; return NULL; } strcpy(entry->name, name); entry->object = object; entry->type = found; entry->refs = 1; entry->bytes = bytes; entry->loose = loose; entry->unbound = 0; assets->count++; if (assets->tableSize && assets->count * 2 <= assets->tableSize) kon_tableInsert_(assets, assets->count - 1); else kon_tableRebuild_(assets); /* this can load more assets and move the entries, so entry is not used after it */ if (assets->loaders[found].bind) assets->loaders[found].bind(object, assets); return object; } void *kon_assetFind(const kon_assets_t *assets, const char *name) { kon_assetEntry_t *entry; if (!assets || !name) return NULL; entry = kon_findEntry_(assets, name); return entry ? entry->object : NULL; } void kon_assetRelease(kon_assets_t *assets, const char *name) { kon_assetEntry_t *entry; if (!assets || !name) return; entry = kon_findEntry_(assets, name); if (entry && entry->refs > 0) entry->refs--; } int kon_assetUnload(kon_assets_t *assets, const char *name) { kon_assetEntry_t *entry; int index; if (!assets || !name) return 0; entry = kon_findEntry_(assets, name); if (!entry) return 0; if (entry->refs > 0) entry->refs--; if (entry->refs > 0) return 0; /* unbind can give back uses of other assets, which never moves the entries, so this one is still where it was */ index = (int)(entry - assets->entries); if (assets->loaders[entry->type].unbind) assets->loaders[entry->type].unbind(entry->object, assets); entry = &assets->entries[index]; kon_unloadEntry_(assets, entry); memmove(entry, entry + 1, (size_t)(assets->count - index - 1) * sizeof *entry); assets->count--; kon_tableRebuild_(assets); return 1; } int kon_assetsCollect(kon_assets_t *assets) { int freed = 0, again; if (!assets) return 0; /* an object that is freed can give back the last use of another one, so go around until nothing changes */ do { int kept = 0, before = freed; /* first let everything that is not used say what it was using, which only lowers the use counts of others */ for (int i = 0; i < assets->count; i++) { kon_assetEntry_t *entry = &assets->entries[i]; if (entry->refs > 0 || entry->unbound) continue; entry->unbound = 1; if (assets->loaders[entry->type].unbind) assets->loaders[entry->type].unbind(entry->object, assets); } for (int i = 0; i < assets->count; i++) { if (assets->entries[i].refs > 0 || !assets->entries[i].unbound) { assets->entries[kept++] = assets->entries[i]; } else { kon_unloadEntry_(assets, &assets->entries[i]); freed++; } } assets->count = kept; kon_tableRebuild_(assets); /* something that just became unused has to be looked at again */ again = freed != before; for (int i = 0; i < assets->count && !again; i++) again = assets->entries[i].refs <= 0 && !assets->entries[i].unbound; } while (again); return freed; } int kon_assetsCount(const kon_assets_t *assets) { return assets ? assets->count : 0; } int kon_assetsInfo(const kon_assets_t *assets, int index, kon_assetInfo_t *info) { const kon_assetEntry_t *entry; if (!assets || !info || index < 0 || index >= assets->count) return 0; entry = &assets->entries[index]; info->name = entry->name; info->typeName = assets->loaderUsed[entry->type] ? assets->loaders[entry->type].name : "?"; info->type = entry->type; info->refs = entry->refs; info->bytes = entry->bytes; info->loose = entry->loose; return 1; } size_t kon_assetsBytes(const kon_assets_t *assets) { size_t total = 0; if (!assets) return 0; for (int i = 0; i < assets->count; i++) total += assets->entries[i].bytes; return total; } void kon_assetsDump(const kon_assets_t *assets, FILE *out) { kon_assetInfo_t info; if (!assets || !out) return; fprintf(out, "%d assets, %lu bytes\n", assets->count, (unsigned long)kon_assetsBytes(assets)); for (int i = 0; kon_assetsInfo(assets, i, &info); i++) { fprintf(out, "%-32s %-8s refs %-3d %10lu bytes%s\n", info.name, info.typeName, info.refs, (unsigned long)info.bytes, info.loose ? " loose" : ""); } } /*** groups ***/ struct kon_assetGroup { kon_assets_t *assets; char **names; char *state; /* 0 waiting, 1 loaded, 2 failed */ int count, capacity, next; }; kon_assetGroup_t *kon_assetGroupCreate(kon_assets_t *assets) { kon_assetGroup_t *group; if (!assets) return NULL; group = calloc(1, sizeof *group); if (group) group->assets = assets; return group; } int kon_assetGroupQueue(kon_assetGroup_t *group, const char *name) { char *copy; if (!group || !name) return 0; if (group->count == group->capacity) { int capacity = group->capacity ? group->capacity * 2 : 16; char **names = realloc(group->names, (size_t)capacity * sizeof *names); char *state; if (!names) return 0; group->names = names; state = realloc(group->state, (size_t)capacity); if (!state) return 0; group->state = state; group->capacity = capacity; } copy = malloc(strlen(name) + 1); if (!copy) return 0; strcpy(copy, name); group->names[group->count] = copy; group->state[group->count] = 0; group->count++; return 1; } int kon_assetGroupQueueArray(kon_assetGroup_t *group, const char *const *names, int count) { int i; if (!group || !names) return -1; for (i = 0; count < 0 ? names[i] != NULL : i < count; i++) { if (!kon_assetGroupQueue(group, names[i])) return -1; } return i; } int kon_assetGroupQueueList(kon_assetGroup_t *group, const char *listName) { kon_blob_t *blob; char *line; int queued = 0; if (!group || !listName) return -1; blob = kon_assetGetAs(group->assets, listName, KON_ASSET_BLOB); if (!blob) return -1; /* the blob has a 0 after the end, so each line is cut out in place and put back */ line = (char *)blob->data; while (*line) { char *end = line; char *start, *stop; while (*end && *end != '\n') end++; start = line; stop = end; while (start < stop && (*start == ' ' || *start == '\t' || *start == '\r')) start++; while (stop > start && (stop[-1] == ' ' || stop[-1] == '\t' || stop[-1] == '\r')) stop--; if (stop > start && *start != '#') { char saved = *stop; int ok; *stop = 0; ok = kon_assetGroupQueue(group, start); *stop = saved; if (!ok) break; queued++; } line = *end ? end + 1 : end; } kon_assetRelease(group->assets, listName); return queued; } int kon_assetGroupStep(kon_assetGroup_t *group) { if (!group) return 0; while (group->next < group->count && group->state[group->next] != 0) group->next++; if (group->next < group->count) { int i = group->next++; if (kon_assetGet(group->assets, group->names[i])) { group->state[i] = 1; } else { group->state[i] = 2; #ifdef KON_DEVELOPER fprintf(stderr, "asset group: could not load %s: %s\n", group->names[i], kon_assetsError(group->assets)); #endif } } while (group->next < group->count && group->state[group->next] != 0) group->next++; return group->count - group->next; } int kon_assetGroupLoadAll(kon_assetGroup_t *group) { int failed = 0, i; if (!group) return 0; while (kon_assetGroupStep(group) > 0) {} for (i = 0; i < group->count; i++) if (group->state[i] == 2) failed++; return failed; } void kon_assetGroupProgress(const kon_assetGroup_t *group, int *loaded, int *total) { int i, done = 0; if (group) for (i = 0; i < group->count; i++) if (group->state[i] == 1) done++; if (loaded) *loaded = done; if (total) *total = group ? group->count : 0; } int kon_assetGroupUnload(kon_assetGroup_t *group, const char *name) { int i, freed = 0; if (!group || !name) return 0; for (i = 0; i < group->count; i++) { if (strcmp(group->names[i], name) == 0) break; } if (i == group->count) return 0; if (group->state[i] == 1) freed = kon_assetUnload(group->assets, name); free(group->names[i]); memmove(&group->names[i], &group->names[i + 1], (size_t)(group->count - i - 1) * sizeof *group->names); memmove(&group->state[i], &group->state[i + 1], (size_t)(group->count - i - 1)); group->count--; if (i < group->next) group->next--; return freed; } void kon_assetGroupFree(kon_assetGroup_t *group) { int i; if (!group) return; for (i = 0; i < group->count; i++) { if (group->state[i] == 1) kon_assetRelease(group->assets, group->names[i]); free(group->names[i]); } kon_assetsCollect(group->assets); free(group->names); free(group->state); free(group); } const char *kon_assetsError(const kon_assets_t *assets) { return assets ? assets->error : "no assets"; }