Move cutscene JSON parsing entirely into the async loader phase

File read, yyjson parse, and all item dispatch now happen in
assetCutsceneLoaderAsync - none of it touches the main thread now that
CUTSCENE items defer their nested asset resolution to start time. The
sync phase is now just a state flip, kept only so assetUpdate()'s
onLoaded dispatch (which only fires for entries finishing via the sync
path) still runs.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-09-09 21:34:47 -05:00
co-authored by Claude Sonnet 5
parent cf51ed1988
commit 03d753a83f
2 changed files with 40 additions and 31 deletions
@@ -101,8 +101,6 @@ errorret_t assetCutsceneLoaderAsync(assetloading_t *loading) {
errorOk(); errorOk();
} }
assertNull(loading->loading.cutscene.buffer, "Buffer already defined?");
assetfile_t *file = &loading->loading.cutscene.file; assetfile_t *file = &loading->loading.cutscene.file;
assetLoaderErrorChain(loading, assetLoaderErrorChain(loading,
assetFileInit(file, loading->entry->name, NULL, NULL) assetFileInit(file, loading->entry->name, NULL, NULL)
@@ -125,35 +123,17 @@ errorret_t assetCutsceneLoaderAsync(assetloading_t *loading) {
assetLoaderErrorChain(loading, assetFileClose(file)); assetLoaderErrorChain(loading, assetFileClose(file));
assetLoaderErrorChain(loading, assetFileDispose(file)); assetLoaderErrorChain(loading, assetFileDispose(file));
loading->loading.cutscene.buffer = buffer; // Parsing is pure CPU work with no main-thread dependency (every item
loading->loading.cutscene.size = fileSize; // type's load callback just parses fields - see cutsceneCutsceneLoad's
loading->loading.cutscene.state = ASSET_CUTSCENE_LOADING_STATE_PARSE; // doc comment for how CUTSCENE items avoid needing one), so it happens
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; // right here instead of bouncing to the sync phase.
errorOk();
}
errorret_t assetCutsceneLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_CUTSCENE, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
if(loading->loading.cutscene.state == ASSET_CUTSCENE_LOADING_STATE_INITIAL) {
loading->loading.cutscene.state = ASSET_CUTSCENE_LOADING_STATE_READ_FILE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
}
assetcutsceneoutput_t *out = &loading->entry->data.cutscene; assetcutsceneoutput_t *out = &loading->entry->data.cutscene;
uint8_t *buffer = loading->loading.cutscene.buffer;
assertNotNull(buffer, "Cutscene data should have been loaded by now.");
size_t bufferSize = loading->loading.cutscene.size;
yyjson_doc *doc = yyjson_read( yyjson_doc *doc = yyjson_read(
(char *)buffer, bufferSize, (char *)buffer, fileSize,
YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS
); );
memoryFree(buffer); memoryFree(buffer);
loading->loading.cutscene.buffer = NULL;
if(doc == NULL) { if(doc == NULL) {
assetLoaderErrorThrow(loading, "Failed to parse cutscene JSON"); assetLoaderErrorThrow(loading, "Failed to parse cutscene JSON");
@@ -210,6 +190,31 @@ errorret_t assetCutsceneLoaderSync(assetloading_t *loading) {
out->cutscene.pause = pause; out->cutscene.pause = pause;
out->cutscene.dataSize = 0; out->cutscene.dataSize = 0;
loading->loading.cutscene.state = ASSET_CUTSCENE_LOADING_STATE_PARSE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
errorret_t assetCutsceneLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_CUTSCENE, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
if(loading->loading.cutscene.state == ASSET_CUTSCENE_LOADING_STATE_INITIAL) {
loading->loading.cutscene.state = ASSET_CUTSCENE_LOADING_STATE_READ_FILE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
}
// Everything else already happened in the async phase - see
// assetCutsceneLoaderAsync. This flip still has to happen from here
// rather than the async phase setting LOADED directly, since
// assetUpdate() only fires an entry's onLoaded callback for entries
// that finish via this sync path.
assertTrue(
loading->loading.cutscene.state == ASSET_CUTSCENE_LOADING_STATE_PARSE,
"Unexpected cutscene loading state."
);
loading->entry->state = ASSET_ENTRY_STATE_LOADED; loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk(); errorOk();
} }
@@ -30,8 +30,6 @@ typedef enum {
typedef struct { typedef struct {
assetfile_t file; assetfile_t file;
assetcutsceneloadingstate_t state; assetcutsceneloadingstate_t state;
uint8_t *buffer;
size_t size;
} assetcutsceneloaderloading_t; } assetcutsceneloaderloading_t;
typedef struct { typedef struct {
@@ -80,8 +78,11 @@ void assetCutsceneFreeParsed(assetcutsceneoutput_t *out);
/** /**
* Asynchronous loader for cutscene assets. Reads the raw JSONC file bytes * Asynchronous loader for cutscene assets. Reads the raw JSONC file bytes
* into the loading buffer so the sync phase can parse without blocking the * and, in the same call, parses them into a heap-allocated cutsceneitem_t
* main thread on I/O. * 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.
* *
* @param loading Loading information for the asset being loaded. * @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation. * @return Error code indicating success or failure of the load operation.
@@ -89,8 +90,11 @@ void assetCutsceneFreeParsed(assetcutsceneoutput_t *out);
errorret_t assetCutsceneLoaderAsync(assetloading_t *loading); errorret_t assetCutsceneLoaderAsync(assetloading_t *loading);
/** /**
* Synchronous loader for cutscene assets. Parses the JSONC previously read * Synchronous loader for cutscene assets. All actual work already
* by the async phase into a heap-allocated cutsceneitem_t array + pool. * happened in the async phase - this only flips the entry to LOADED,
* which must happen through this sync callback (rather than the async
* phase setting it directly) so assetUpdate()'s onLoaded dispatch, which
* only fires for entries finishing via the sync path, still runs.
* *
* @param loading Loading information for the asset being loaded. * @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation. * @return Error code indicating success or failure of the load operation.