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();
}
assertNull(loading->loading.cutscene.buffer, "Buffer already defined?");
assetfile_t *file = &loading->loading.cutscene.file;
assetLoaderErrorChain(loading,
assetFileInit(file, loading->entry->name, NULL, NULL)
@@ -125,35 +123,17 @@ errorret_t assetCutsceneLoaderAsync(assetloading_t *loading) {
assetLoaderErrorChain(loading, assetFileClose(file));
assetLoaderErrorChain(loading, assetFileDispose(file));
loading->loading.cutscene.buffer = buffer;
loading->loading.cutscene.size = fileSize;
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();
}
// Parsing is pure CPU work with no main-thread dependency (every item
// 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;
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(
(char *)buffer, bufferSize,
(char *)buffer, fileSize,
YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS
);
memoryFree(buffer);
loading->loading.cutscene.buffer = NULL;
if(doc == NULL) {
assetLoaderErrorThrow(loading, "Failed to parse cutscene JSON");
@@ -210,6 +190,31 @@ errorret_t assetCutsceneLoaderSync(assetloading_t *loading) {
out->cutscene.pause = pause;
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;
errorOk();
}
@@ -30,8 +30,6 @@ typedef enum {
typedef struct {
assetfile_t file;
assetcutsceneloadingstate_t state;
uint8_t *buffer;
size_t size;
} assetcutsceneloaderloading_t;
typedef struct {
@@ -80,8 +78,11 @@ void assetCutsceneFreeParsed(assetcutsceneoutput_t *out);
/**
* Asynchronous loader for cutscene assets. Reads the raw JSONC file bytes
* into the loading buffer so the sync phase can parse without blocking the
* main thread on I/O.
* 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.
*
* @param loading Loading information for the asset being loaded.
* @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);
/**
* Synchronous loader for cutscene assets. Parses the JSONC previously read
* by the async phase into a heap-allocated cutsceneitem_t array + pool.
* Synchronous loader for cutscene assets. All actual work already
* 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.
* @return Error code indicating success or failure of the load operation.