From 03d753a83feb45dc864301a8ffebcbc4857e3aa6 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Wed, 9 Sep 2026 21:34:47 -0500 Subject: [PATCH] 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 --- .../loader/cutscene/assetcutsceneloader.c | 55 ++++++++++--------- .../loader/cutscene/assetcutsceneloader.h | 16 ++++-- 2 files changed, 40 insertions(+), 31 deletions(-) diff --git a/src/dusk/asset/loader/cutscene/assetcutsceneloader.c b/src/dusk/asset/loader/cutscene/assetcutsceneloader.c index 5475a44b..55976dae 100644 --- a/src/dusk/asset/loader/cutscene/assetcutsceneloader.c +++ b/src/dusk/asset/loader/cutscene/assetcutsceneloader.c @@ -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(); } diff --git a/src/dusk/asset/loader/cutscene/assetcutsceneloader.h b/src/dusk/asset/loader/cutscene/assetcutsceneloader.h index 1eb9609a..c7c8339c 100644 --- a/src/dusk/asset/loader/cutscene/assetcutsceneloader.h +++ b/src/dusk/asset/loader/cutscene/assetcutsceneloader.h @@ -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.