Switch cutscene system to a set of pre deffed buffers

This commit is contained in:
2026-09-13 09:58:57 -05:00
parent 6c11d8165d
commit 49b97ba508
12 changed files with 184 additions and 104 deletions
+6
View File
@@ -50,6 +50,12 @@ if(DUSK_BACKTRACE)
)
endif()
if(DUSK_TRACK_MEMORY)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_TRACK_MEMORY
)
endif()
# Includes
target_include_directories(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
@@ -76,20 +76,13 @@ size_t assetCutsceneComputePoolSize(yyjson_val *itemsArr) {
return size;
}
void assetCutsceneFreeParsed(assetcutsceneoutput_t *out) {
if(out->items != NULL) {
memoryFree(out->items);
out->items = NULL;
}
if(out->pool != NULL) {
memoryFree(out->pool);
out->pool = NULL;
}
if(out->doc != NULL) {
yyjson_doc_free(out->doc);
out->doc = NULL;
}
}
// Raw file-read scratch space - fixed-size (ASSET_CUTSCENE_FILE_SIZE_MAX is
// already a hard cap below) rather than malloc'd per load. Safe to share:
// ASSET only ever runs one async load at a time on its single load thread
// (see assetUpdateAsync), and yyjson_read copies everything it needs out
// of this buffer into the doc before this function returns, so it's free
// to be overwritten by the next load.
static uint8_t CUTSCENE_FILE_BUFFER[ASSET_CUTSCENE_FILE_SIZE_MAX];
errorret_t assetCutsceneLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
@@ -113,9 +106,10 @@ errorret_t assetCutsceneLoaderAsync(assetloading_t *loading) {
}
size_t fileSize = (size_t)file->size;
uint8_t *buffer = memoryAllocate(fileSize);
assetLoaderErrorChain(loading, assetFileOpen(file));
assetLoaderErrorChain(loading, assetFileRead(file, buffer, fileSize));
assetLoaderErrorChain(
loading, assetFileRead(file, CUTSCENE_FILE_BUFFER, fileSize)
);
assertTrue(
file->lastRead == file->size,
"Failed to read entire cutscene file."
@@ -127,48 +121,55 @@ errorret_t assetCutsceneLoaderAsync(assetloading_t *loading) {
// type's load callback just parses fields - see cutsceneCutsceneLoad's
// doc comment for how CUTSCENE items avoid needing one), so it happens
// right here instead of bouncing to the sync phase.
assetcutsceneoutput_t *out = &loading->entry->data.cutscene;
yyjson_doc *doc = yyjson_read(
(char *)buffer, fileSize,
(char *)CUTSCENE_FILE_BUFFER, fileSize,
YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS
);
memoryFree(buffer);
if(doc == NULL) {
assetLoaderErrorThrow(loading, "Failed to parse cutscene JSON");
}
out->doc = doc;
yyjson_val *root = yyjson_doc_get_root(doc);
yyjson_val *itemsArr = yyjson_obj_get(root, "items");
if(!yyjson_is_arr(itemsArr)) {
assetCutsceneFreeParsed(out);
yyjson_doc_free(doc);
assetLoaderErrorThrow(loading, "Cutscene JSON is missing an 'items' array");
}
size_t itemCount = yyjson_arr_size(itemsArr);
if(itemCount > UINT8_MAX) {
assetCutsceneFreeParsed(out);
assetLoaderErrorThrow(loading, "Cutscene has more than 255 items");
if(itemCount > CUTSCENE_LOADED_ITEMS_MAX) {
yyjson_doc_free(doc);
assetLoaderErrorThrow(
loading, "Cutscene has more than CUTSCENE_LOADED_ITEMS_MAX items"
);
}
cutscenepause_t pause = CUTSCENE_PAUSE_DEFAULT;
yyjson_val *pauseArr = yyjson_obj_get(root, "pause");
if(yyjson_is_arr(pauseArr)) {
if(!cutsceneJsonParsePauseFlags(pauseArr, &pause)) {
assetCutsceneFreeParsed(out);
yyjson_doc_free(doc);
assetLoaderErrorThrow(
loading, "Cutscene JSON has an invalid 'pause' flag"
);
}
}
out->items = memoryAllocate(itemCount * sizeof(cutsceneitem_t));
memoryZero(out->items, itemCount * sizeof(cutsceneitem_t));
size_t poolSize = assetCutsceneComputePoolSize(itemsArr);
out->pool = poolSize > 0 ? memoryAllocate(poolSize) : NULL;
if(poolSize > CUTSCENE_LOADED_POOL_SIZE_MAX) {
yyjson_doc_free(doc);
assetLoaderErrorThrow(
loading, "Cutscene needs more pool space than CUTSCENE_LOADED_POOL_SIZE_MAX"
);
}
// Everything structural has validated by this point, so it's safe to
// start overwriting the shared CUTSCENE_LOADED_ITEMS/POOL buffers - a
// per-item parse failure past this point still leaves them partially
// overwritten, but that's fine: any load failure here already sends the
// game into the fatal-error overlay (see cutsceneSystemLoad/
// cutsceneCutsceneResolve), so nothing keeps running against stale data.
memoryZero(CUTSCENE_LOADED_ITEMS, itemCount * sizeof(cutsceneitem_t));
size_t poolOffset = 0;
yyjson_arr_iter itemIter = yyjson_arr_iter_with(itemsArr);
@@ -176,19 +177,24 @@ errorret_t assetCutsceneLoaderAsync(assetloading_t *loading) {
uint8_t index = 0;
while((itemVal = yyjson_arr_iter_next(&itemIter)) != NULL) {
errorret_t itemResult = assetCutsceneParseItem(
loading, itemVal, &out->items[index], out->pool, &poolOffset, index
loading, itemVal, &CUTSCENE_LOADED_ITEMS[index],
CUTSCENE_LOADED_POOL, &poolOffset, index
);
if(errorIsNotOk(itemResult)) {
assetCutsceneFreeParsed(out);
yyjson_doc_free(doc);
errorChain(itemResult);
}
index++;
}
out->cutscene.items = out->items;
out->cutscene.itemCount = (uint8_t)itemCount;
out->cutscene.pause = pause;
out->cutscene.dataSize = 0;
// Takes ownership of doc - marker/cutscene-reference name fields point
// directly into it (see cutsceneCutsceneRefLoad/cutsceneMarkerLoad), so
// it must outlive this load, replacing whatever the previously loaded
// cutscene left behind.
cutsceneLoadedSetDoc(doc);
CUTSCENE_LOADED_SCENE.itemCount = (uint8_t)itemCount;
CUTSCENE_LOADED_SCENE.pause = pause;
CUTSCENE_LOADED_SCENE.dataSize = 0;
loading->loading.cutscene.state = ASSET_CUTSCENE_LOADING_STATE_PARSE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
@@ -224,7 +230,10 @@ errorret_t assetCutsceneDispose(assetentry_t *entry) {
assertTrue(entry->type == ASSET_LOADER_TYPE_CUTSCENE, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
assetCutsceneFreeParsed(&entry->data.cutscene);
// Nothing to free here - a loaded cutscene's data lives in the fixed,
// shared CUTSCENE_LOADED_SCENE/ITEMS/POOL globals (see cutscenesystem.h),
// not per-entry storage; this entry only ever drove the read+parse
// pipeline into them.
errorOk();
}
@@ -8,6 +8,7 @@
#pragma once
#include "asset/assetfile.h"
#include "rpg/cutscene/cutscene.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/item/json/cutscenejsonpool.h"
#include "yyjson.h"
@@ -33,10 +34,13 @@ typedef struct {
} assetcutsceneloaderloading_t;
typedef struct {
cutscene_t cutscene;
cutsceneitem_t *items;
uint8_t *pool;
yyjson_doc *doc;
// Nothing - a loaded cutscene's data lives entirely in the fixed,
// shared CUTSCENE_LOADED_SCENE/ITEMS/POOL globals (see cutscenesystem.h)
// that assetCutsceneLoaderAsync parses directly into, not per-entry
// storage. This entry only ever exists to drive the read+parse
// pipeline, and is unlocked (see cutsceneSystemLoad/
// cutsceneCutsceneResolve) the instant that finishes.
uint8_t unused;
} assetcutsceneoutput_t;
/**
@@ -67,22 +71,15 @@ errorret_t assetCutsceneParseItem(
const uint8_t index
);
/**
* Frees whatever of out->items/out->pool/out->doc is currently non-NULL
* and clears them - shared by both the normal disposer and every parse
* failure path's cleanup.
*
* @param out The output to free.
*/
void assetCutsceneFreeParsed(assetcutsceneoutput_t *out);
/**
* Asynchronous loader for cutscene assets. Reads the raw JSONC file bytes
* and, in the same call, parses them into a heap-allocated cutsceneitem_t
* array + pool - none of that work touches the main thread (every item
* type's load callback is pure parsing now; CUTSCENE items defer their
* nested asset resolution to cutsceneCutsceneStart), so doing it all here
* keeps a large cutscene from ever blocking a frame.
* and, in the same call, parses them directly into the fixed, shared
* CUTSCENE_LOADED_ITEMS/CUTSCENE_LOADED_POOL buffers (see cutscenesystem.h)
* - no heap allocation at all, since only one cutscene is ever resident.
* None of that work touches the main thread (every item type's load
* callback is pure parsing now; CUTSCENE items defer their nested asset
* resolution to cutsceneCutsceneStart), so doing it all here keeps a large
* cutscene from ever blocking a frame.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
+23 -1
View File
@@ -15,6 +15,21 @@
cutscenesystem_t CUTSCENE_SYSTEM;
cutsceneitem_t CUTSCENE_LOADED_ITEMS[CUTSCENE_LOADED_ITEMS_MAX];
uint8_t CUTSCENE_LOADED_POOL[CUTSCENE_LOADED_POOL_SIZE_MAX];
cutscene_t CUTSCENE_LOADED_SCENE = {
.items = CUTSCENE_LOADED_ITEMS,
.itemCount = 0,
.pause = CUTSCENE_PAUSE_NONE,
.dataSize = 0
};
static yyjson_doc *CUTSCENE_LOADED_DOC = NULL;
void cutsceneLoadedSetDoc(yyjson_doc *doc) {
if(CUTSCENE_LOADED_DOC != NULL) yyjson_doc_free(CUTSCENE_LOADED_DOC);
CUTSCENE_LOADED_DOC = doc;
}
void cutsceneSystemInit() {
memoryZero(&CUTSCENE_SYSTEM, sizeof(cutscenesystem_t));
}
@@ -69,13 +84,18 @@ void cutsceneSystemLoad(const char_t *file) {
assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_CUTSCENE, NULL);
errorret_t result = assetRequireLoaded(entry);
// Released the instant loading finishes - the loader already parsed
// everything we need into CUTSCENE_LOADED_SCENE/ITEMS/POOL, so nothing
// further depends on this entry (see cutscenesystem.h's doc comment on
// CUTSCENE_LOADED_ITEMS_MAX for why only one cutscene stays resident).
assetUnlockEntry(entry);
if(errorIsNotOk(result)) {
errorCatch(errorPrint(result));
uiFatalErrorOpen(NULL);
return;
}
cutsceneSystemStartCutscene(&entry->data.cutscene.cutscene);
cutsceneSystemStartCutscene(&CUTSCENE_LOADED_SCENE);
}
void cutsceneRestart(void) {
@@ -259,4 +279,6 @@ void cutsceneSystemDispose() {
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
CUTSCENE_SYSTEM.textCache[0] = '\0';
CUTSCENE_SYSTEM.onComplete = NULL;
cutsceneLoadedSetDoc(NULL);
}
+33
View File
@@ -26,6 +26,39 @@ typedef struct entity_s entity_t;
// CUTSCENE_ITEM_TYPE_KEYBOARD item is never truncated caching it here.
#define CUTSCENE_TEXT_CACHE_MAX 64
// Only one JSON-authored cutscene is ever loaded/playing at a time - a
// CUTSCENE item hands control off completely (see cutsceneCutsceneStart)
// and cutsceneSystemLoad/cutsceneCutsceneResolve release their asset lock
// the instant loading finishes - so there's no benefit to malloc'ing a
// fresh items/pool buffer per load. Instead the asset loader
// (assetCutsceneLoaderAsync) parses directly into these fixed, static
// buffers. Bump these if a real cutscene ever needs more; C-authored
// cutscenes (the CUTSCENE(...) macro) don't use these at all - they're
// compiled directly into their own static const arrays.
#define CUTSCENE_LOADED_ITEMS_MAX 128
#define CUTSCENE_LOADED_POOL_SIZE_MAX 4096
extern cutsceneitem_t CUTSCENE_LOADED_ITEMS[CUTSCENE_LOADED_ITEMS_MAX];
extern uint8_t CUTSCENE_LOADED_POOL[CUTSCENE_LOADED_POOL_SIZE_MAX];
// The cutscene_t for whatever was last parsed into CUTSCENE_LOADED_ITEMS/
// CUTSCENE_LOADED_POOL - .items always points at CUTSCENE_LOADED_ITEMS;
// itemCount/pause/dataSize are overwritten by the loader on each load.
extern cutscene_t CUTSCENE_LOADED_SCENE;
/**
* Frees whatever yyjson_doc currently backs CUTSCENE_LOADED_ITEMS' string
* fields (marker/cutscene-reference names point directly into it - see
* cutsceneCutsceneRefLoad/cutsceneMarkerLoad) and takes ownership of doc
* instead. Called by the cutscene asset loader once a fresh parse into
* CUTSCENE_LOADED_ITEMS/CUTSCENE_LOADED_POOL completes successfully, and
* from cutsceneSystemDispose to release the last one at shutdown.
*
* @param doc The new doc to take ownership of (NULL just frees/clears the
* current one).
*/
void cutsceneLoadedSetDoc(yyjson_doc *doc);
typedef struct {
const cutscene_t *scene;
uint8_t currentItem;
+10 -4
View File
@@ -373,10 +373,13 @@ void cutsceneCutsceneStart(
) {
const cutscene_t *cutscene = item->cutsceneRef.cutscene;
if(cutscene == NULL && item->cutsceneRef.name != NULL) {
// Not cached back into item - with only one shared, fixed load buffer
// (see cutscenesystem.h's CUTSCENE_LOADED_ITEMS_MAX doc comment),
// every JSON-authored reference resolves to the same fixed address
// every time anyway, and item itself lives inside that very buffer -
// caching a pointer into it here would go stale (or, worse, get
// clobbered mid-resolve) the moment a later load overwrites it.
cutscene = cutsceneCutsceneResolve(item->cutsceneRef.name);
// Cache the resolved pointer back so replaying this same item (e.g.
// a looping intro) never re-resolves/re-locks the asset again.
((cutsceneitem_t *)item)->cutsceneRef.cutscene = cutscene;
}
if(cutscene != NULL) cutsceneSystemStartCutscene(cutscene);
}
@@ -394,13 +397,16 @@ const cutscene_t * cutsceneCutsceneResolve(const char_t *name) {
assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_CUTSCENE, NULL);
errorret_t result = assetRequireLoaded(entry);
// Released the instant loading finishes - see cutsceneSystemLoad's doc
// comment for why nothing further depends on this entry once loaded.
assetUnlockEntry(entry);
if(errorIsNotOk(result)) {
errorCatch(errorPrint(result));
uiFatalErrorOpen(NULL);
return NULL;
}
return &entry->data.cutscene.cutscene;
return &CUTSCENE_LOADED_SCENE;
}
errorret_t cutsceneCutsceneLoad(
@@ -22,8 +22,8 @@ ENTITY_GLOBAL_CALLBACK(3) {
}
ENTITY_GLOBAL_CALLBACK(4) {
create->entity->interact.type = ENTITY_INTERACT_CUTSCENE;
create->entity->interact.data.cutscene = cutsceneCutsceneResolve("battletest");
create->entity->interact.type = ENTITY_INTERACT_CUTSCENE_NAME;
create->entity->interact.data.cutsceneName = "battletest";
}
static const entityglobaldef_t ENTITY_GLOBAL_LIST[] = {
@@ -27,6 +27,15 @@ void entityInteractWith(entity_t *player, entity_t *target) {
);
break;
case ENTITY_INTERACT_CUTSCENE_NAME: {
const cutscene_t *cutscene =
cutsceneCutsceneResolve(target->interact.data.cutsceneName);
if(cutscene != NULL) {
cutsceneSystemStartCutsceneWith(cutscene, player, target);
}
break;
}
case ENTITY_INTERACT_PRINT:
uiTextboxMainSetText(target->interact.data.message);
@@ -18,6 +18,7 @@ typedef enum {
ENTITY_INTERACT_NULL = 0,
ENTITY_INTERACT_CUTSCENE,
ENTITY_INTERACT_CUTSCENE_NAME,
ENTITY_INTERACT_PRINT,
ENTITY_INTERACT_CALLBACK,
@@ -28,7 +29,18 @@ typedef enum {
* Per-type data for an entity's interact component.
*/
typedef union {
// A compile-time cutscene_t* (e.g. CUTSCENE_REFERENCE(...)) - safe to
// hold onto indefinitely, since C-authored cutscenes are compiled
// directly into their own static const arrays, never the shared
// CUTSCENE_LOADED_ITEMS buffer (see cutscenesystem.h).
const cutscene_t *cutscene;
// A bare cutscene name (as passed to cutsceneCutsceneResolve), resolved
// lazily at interact time rather than once up front - only one
// JSON-authored cutscene is ever resident at a time (see
// CUTSCENE_LOADED_ITEMS_MAX's doc comment), so resolving early and
// holding onto the result here would go stale the moment any other
// cutscene loads in the meantime.
const char_t *cutsceneName;
const char_t *message;
void (*callback)(entity_t *player, entity_t *target);
} entityinteractdata_t;
+17 -21
View File
@@ -26,21 +26,15 @@
#define UI_MAIN_MENU_INDEX_QUIT 2
uimainmenu_t UI_MAIN_MENU;
assetentry_t *MAIN_MENU_CUTSCENE_ENTRY = NULL;
cutscene_t *uiMainMenuLoadCutscene(void) {
if(MAIN_MENU_CUTSCENE_ENTRY == NULL) {
MAIN_MENU_CUTSCENE_ENTRY = assetLock(
"cutscenes/main_menu.jsonc", ASSET_LOADER_TYPE_CUTSCENE, NULL
);
}
errorret_t result = assetRequireLoaded(MAIN_MENU_CUTSCENE_ENTRY);
if(errorIsNotOk(result)) {
errorCatch(errorPrint(result));
assertTrue(false, "Failed to load main menu cutscene asset");
}
return &MAIN_MENU_CUTSCENE_ENTRY->data.cutscene.cutscene;
const cutscene_t *uiMainMenuLoadCutscene(void) {
// Delegates entirely to cutsceneCutsceneResolve rather than keeping our
// own locked-forever entry, since only one JSON-authored cutscene is
// ever resident at a time (see CUTSCENE_LOADED_ITEMS_MAX's doc comment
// in cutscenesystem.h) - caching an entry pointer here across calls
// would go stale the moment any other cutscene loads in the meantime.
// May return NULL on failure (already reported via uiFatalErrorOpen).
return cutsceneCutsceneResolve("main_menu");
}
void uiMainMenuSelectSaveResult(const uint8_t slotIndex, void *user) {
@@ -79,9 +73,9 @@ void uiMainMenuOpenSelectSave(void *userData) {
void uiMainMenuStartGame(void) {
if(CUTSCENE_SYSTEM.scene == NULL) {
cutsceneSystemStartCutsceneAndGoToMarker(
uiMainMenuLoadCutscene(), "NEW_GAME"
);
const cutscene_t *cutscene = uiMainMenuLoadCutscene();
if(cutscene == NULL) return;
cutsceneSystemStartCutsceneAndGoToMarker(cutscene, "NEW_GAME");
} else {
cutsceneGoTo("NEW_GAME");
}
@@ -91,9 +85,9 @@ void uiMainMenuStartGame(void) {
void uiMainMenuOptions(void) {
if(CUTSCENE_SYSTEM.scene == NULL) {
cutsceneSystemStartCutsceneAndGoToMarker(
uiMainMenuLoadCutscene(), "OPTIONS"
);
const cutscene_t *cutscene = uiMainMenuLoadCutscene();
if(cutscene == NULL) return;
cutsceneSystemStartCutsceneAndGoToMarker(cutscene, "OPTIONS");
} else {
cutsceneGoTo("OPTIONS");
}
@@ -101,7 +95,9 @@ void uiMainMenuOptions(void) {
void uiMainMenuQuit(void) {
if(CUTSCENE_SYSTEM.scene == NULL) {
cutsceneSystemStartCutsceneAndGoToMarker(uiMainMenuLoadCutscene(), "QUIT");
const cutscene_t *cutscene = uiMainMenuLoadCutscene();
if(cutscene == NULL) return;
cutsceneSystemStartCutsceneAndGoToMarker(cutscene, "QUIT");
} else {
cutsceneGoTo("QUIT");
}
+9 -8
View File
@@ -39,17 +39,18 @@ errorret_t uiMainMenuInit(void);
void uiMainMenuOpen(void);
/**
* Lazily locks/loads the main menu cutscene asset (assets/cutscenes/
* Loads (or re-loads) the main menu cutscene asset (assets/cutscenes/
* main_menu.jsonc, parsed directly at load time rather than compiled in)
* and returns it, asserting on failure. Kept resident for the rest of the
* process once loaded - the persistent cutscene is what actually drives
* this whole screen (opening this panel is its first item's job, see
* UI_SHOW/rpg/cutscene/item/ui/cutsceneuishow.h), this is just the shared
* accessor for the "Start Game" flow below to jump/restart it.
* and returns it - the shared accessor for the "Start Game"/Options/Quit
* flows below to jump/restart it. Only one JSON-authored cutscene is ever
* resident at a time (see CUTSCENE_LOADED_ITEMS_MAX's doc comment in
* cutscenesystem.h), so this resolves fresh on every call rather than
* caching anything.
*
* @return The main menu cutscene.
* @return The main menu cutscene, or NULL on failure (already reported via
* uiFatalErrorOpen).
*/
cutscene_t *uiMainMenuLoadCutscene(void);
const cutscene_t *uiMainMenuLoadCutscene(void);
/**
* Starts the "Start Game" flow: jumps the already-running main menu