From 42d307d6f29dd425206405cee92cc983ab33008a Mon Sep 17 00:00:00 2001 From: cobaltgit Date: Fri, 17 Jul 2026 16:04:54 +0100 Subject: [PATCH 1/4] lazy AUDO + support for mmapping audio groups --- src/audio/miniaudio/ma_audio_system.c | 11 ++++++-- src/audio/openal/al_audio_system.c | 11 ++++++-- src/audio/web/web_audio_system.c | 9 +++++-- src/data_win.c | 36 ++++++++++++++++++++++++--- src/data_win.h | 4 +++ src/desktop/main.c | 7 ++++++ 6 files changed, 68 insertions(+), 10 deletions(-) diff --git a/src/audio/miniaudio/ma_audio_system.c b/src/audio/miniaudio/ma_audio_system.c index 3ec79ec7c..3ee9d4ae4 100644 --- a/src/audio/miniaudio/ma_audio_system.c +++ b/src/audio/miniaudio/ma_audio_system.c @@ -261,7 +261,9 @@ static int32_t maPlaySound(AudioSystem* audio, int32_t soundIndex, int32_t prior return -1; } - AudioEntry* entry = &ma->base.audioGroups[sound->audioGroup]->audo.entries[sound->audioFile]; + DataWin* audioGroup = ma->base.audioGroups[sound->audioGroup]; + DataWin_loadAudoIfNeeded(audioGroup, (uint32_t)sound->audioFile); + AudioEntry* entry = &audioGroup->audo.entries[sound->audioFile]; ma_decoder_config decoderConfig = ma_decoder_config_init_default(); result = ma_decoder_init_memory(entry->data, entry->dataSize, &decoderConfig, &slot->decoder); @@ -684,7 +686,9 @@ static float maGetSoundLength(AudioSystem* audio, int32_t soundOrInstance) { ma_result decResult; if (inAudo) { if (0 > sound->audioFile || (uint32_t) sound->audioFile >= ma->base.audioGroups[sound->audioGroup]->audo.count) return 0.0f; - AudioEntry* entry = &ma->base.audioGroups[sound->audioGroup]->audo.entries[sound->audioFile]; + DataWin* audioGroup = ma->base.audioGroups[sound->audioGroup]; + DataWin_loadAudoIfNeeded(audioGroup, (uint32_t)sound->audioFile); + AudioEntry* entry = &audioGroup->audo.entries[sound->audioFile]; ma_decoder_config decoderConfig = ma_decoder_config_init_default(); decResult = ma_decoder_init_memory(entry->data, entry->dataSize, &decoderConfig, &decoder); } else { @@ -750,6 +754,9 @@ static void maGroupLoad(AudioSystem* audio, int32_t groupIndex) { DataWinParserOptions options = {0}; options.parseAudo = true; + options.lazyLoadAudio = audio->dw->lazyLoadAudio; + if (audio->dw->mappedFile) + options.loadType = DATAWINLOADTYPE_MAP_FILE; DataWin *audioGroup = DataWin_parse(((MaAudioSystem*)audio)->fileSystem->vtable->resolvePath(((MaAudioSystem*)audio)->fileSystem, buf), options); arrput(audio->audioGroups, audioGroup); free(buf); diff --git a/src/audio/openal/al_audio_system.c b/src/audio/openal/al_audio_system.c index a2223b03f..9c142b659 100644 --- a/src/audio/openal/al_audio_system.c +++ b/src/audio/openal/al_audio_system.c @@ -372,7 +372,9 @@ static int32_t maPlaySound(AudioSystem* audio, int32_t soundIndex, int32_t prior return -1; } - AudioEntry* entry = &ma->base.audioGroups[sound->audioGroup]->audo.entries[sound->audioFile]; + DataWin* audioGroup = ma->base.audioGroups[sound->audioGroup]; + DataWin_loadAudoIfNeeded(audioGroup, (uint32_t)sound->audioFile); + AudioEntry* entry = &audioGroup->audo.entries[sound->audioFile]; WAVFile wav = WAV_ParseFileData(entry->data); uint32_t format; @@ -773,7 +775,9 @@ static float maGetSoundLength(AudioSystem* audio, int32_t soundOrInstance) { bool inAudo = !isRegular || isEmbedded || isCompressed; if (inAudo) { if (0 > sound->audioFile || (uint32_t) sound->audioFile >= ma->base.audioGroups[sound->audioGroup]->audo.count) return 0.0f; - AudioEntry* entry = &ma->base.audioGroups[sound->audioGroup]->audo.entries[sound->audioFile]; + DataWin* audioGroup = ma->base.audioGroups[sound->audioGroup]; + DataWin_loadAudoIfNeeded(audioGroup, (uint32_t)sound->audioFile); + AudioEntry* entry = &audioGroup->audo.entries[sound->audioFile]; WAVFile wav = WAV_ParseFileData(entry->data); float seconds = 0.0f; if (wav.header.byte_rate > 0) seconds = (float) wav.header.data_size / (float) wav.header.byte_rate; @@ -824,6 +828,9 @@ static void maGroupLoad(AudioSystem* audio, int32_t groupIndex) { DataWinParserOptions options = {0}; options.parseAudo = true; + options.lazyLoadAudio = audio->dw->lazyLoadAudio; + if (audio->dw->mappedFile) + options.loadType = DATAWINLOADTYPE_MAP_FILE; DataWin *audioGroup = DataWin_parse(((AlAudioSystem*)audio)->fileSystem->vtable->resolvePath(((AlAudioSystem*)audio)->fileSystem, buf), options); arrput(audio->audioGroups, audioGroup); free(buf); diff --git a/src/audio/web/web_audio_system.c b/src/audio/web/web_audio_system.c index 479913222..77cd3d123 100644 --- a/src/audio/web/web_audio_system.c +++ b/src/audio/web/web_audio_system.c @@ -207,7 +207,9 @@ static int32_t webPlaySound(AudioSystem* audio, int32_t soundIndex, int32_t prio return -1; } - AudioEntry* entry = &ma->base.audioGroups[sound->audioGroup]->audo.entries[sound->audioFile]; + DataWin* audioGroup = ma->base.audioGroups[sound->audioGroup]; + DataWin_loadAudoIfNeeded(audioGroup, (uint32_t)sound->audioFile); + AudioEntry* entry = &audioGroup->audo.entries[sound->audioFile]; ma_decoder_config decoderConfig = ma_decoder_config_init_default(); result = ma_decoder_init_memory(entry->data, entry->dataSize, &decoderConfig, &slot->decoder); @@ -547,7 +549,9 @@ static float webGetSoundLength(AudioSystem* audio, int32_t soundOrInstance) { ma_result decResult; if (inAudo) { if (0 > sound->audioFile || (uint32_t) sound->audioFile >= ma->base.audioGroups[sound->audioGroup]->audo.count) return 0.0f; - AudioEntry* entry = &ma->base.audioGroups[sound->audioGroup]->audo.entries[sound->audioFile]; + DataWin* audioGroup = ma->base.audioGroups[sound->audioGroup]; + DataWin_loadAudoIfNeeded(audioGroup, (uint32_t)sound->audioFile); + AudioEntry* entry = &audioGroup->audo.entries[sound->audioFile]; ma_decoder_config decoderConfig = ma_decoder_config_init_default(); decResult = ma_decoder_init_memory(entry->data, entry->dataSize, &decoderConfig, &decoder); } else { @@ -612,6 +616,7 @@ static void webGroupLoad(AudioSystem* audio, int32_t groupIndex) { DataWinParserOptions options = {0}; options.parseAudo = true; + options.lazyLoadAudio = audio->dw->lazyLoadAudio; DataWin *audioGroup = DataWin_parse(((WebAudioSystem*)audio)->fileSystem->vtable->resolvePath(((WebAudioSystem*)audio)->fileSystem, buf), options); arrput(audio->audioGroups, audioGroup); } diff --git a/src/data_win.c b/src/data_win.c index 23e9e4839..99fb9c5ef 100644 --- a/src/data_win.c +++ b/src/data_win.c @@ -2569,7 +2569,7 @@ void DataWin_loadTxtrIfNeeded(DataWin* dw, uint32_t textureId) { } } -static void parseAUDO(BinaryReader* reader, DataWin* dw) { +static void parseAUDO(BinaryReader* reader, DataWin* dw, bool loadAudioDataLazily) { Audo* a = &dw->audo; uint32_t count; @@ -2586,7 +2586,9 @@ static void parseAUDO(BinaryReader* reader, DataWin* dw) { a->entries[i].dataSize = BinaryReader_readUint32(reader); a->entries[i].dataOffset = (uint32_t)BinaryReader_getPosition(reader); // Load audio data into owned buffer - if (dw->mappedFile) { + if (loadAudioDataLazily) { + a->entries[i].data = nullptr; + } else if (dw->mappedFile) { a->entries[i].data = dw->mappedFile + a->entries[i].dataOffset; } else if (a->entries[i].dataSize > 0) { a->entries[i].data = (uint8_t *)safeMalloc(a->entries[i].dataSize); @@ -2598,6 +2600,31 @@ static void parseAUDO(BinaryReader* reader, DataWin* dw) { free(ptrs); } +void DataWin_loadAudoIfNeeded(DataWin* dw, uint32_t audioEntryId) { + Audo* a = &dw->audo; + AudioEntry* entry = &a->entries[audioEntryId]; + + if (!entry->present || entry->dataSize == 0) return; + if (entry->data != nullptr) return; + + if (!dw->lazyLoadFile) { + fprintf(stderr, "loadAudoIfNeeded: called without a lazy load file.\n"); + return; + } + + entry->data = (uint8_t *)safeMalloc(entry->dataSize); + + memset(entry->data, 0, entry->dataSize); + long old_seek = ftell(dw->lazyLoadFile); + fseek(dw->lazyLoadFile, entry->dataOffset, SEEK_SET); + size_t read = fread(entry->data, 1, entry->dataSize, dw->lazyLoadFile); + fseek(dw->lazyLoadFile, old_seek, SEEK_SET); + + if (read != entry->dataSize) { + fprintf(stderr, "loadAudoIfNeeded: couldn't read %u bytes to load audio entry %u.\n", entry->dataSize, audioEntryId); + } +} + // ===[ MAIN PARSE FUNCTION ]=== DataWin* DataWin_parse(const char* filePath, DataWinParserOptions options) { @@ -2838,7 +2865,7 @@ DataWin* DataWin_parse(const char* filePath, DataWinParserOptions options) { } else if (options.parseTxtr && memcmp(chunkName, "TXTR", 4) == 0) { parseTXTR(&reader, dw, chunkEnd, options.lazyLoadTextures); } else if (options.parseAudo && memcmp(chunkName, "AUDO", 4) == 0) { - parseAUDO(&reader, dw); + parseAUDO(&reader, dw, options.lazyLoadAudio); } else { printf("Unknown chunk: %.4s (length %u at offset 0x%zX)\n", chunkName, chunkLength, chunkDataStart - 8); } @@ -2870,7 +2897,8 @@ DataWin* DataWin_parse(const char* filePath, DataWinParserOptions options) { // If lazy-loading rooms, keep the file handle open for DataWin_loadRoomPayload, otherwise close it now dw->lazyLoadRooms = options.lazyLoadRooms; dw->lazyLoadTextures = options.lazyLoadTextures; - if (options.lazyLoadRooms || options.lazyLoadTextures) { + dw->lazyLoadAudio = options.lazyLoadAudio; + if (options.lazyLoadRooms || options.lazyLoadTextures || options.lazyLoadAudio) { dw->lazyLoadFile = file; dw->lazyLoadFilePath = safeStrdup(filePath); dw->fileSize = (size_t) fileSize; diff --git a/src/data_win.h b/src/data_win.h index e6e17eb49..e6af39c8a 100644 --- a/src/data_win.h +++ b/src/data_win.h @@ -50,6 +50,8 @@ typedef struct { bool lazyLoadRooms; // If true, TXTR objects will be loaded on demand via DataWin_loadTxtrIfNeeded, and unloaded if memory is tight. bool lazyLoadTextures; + // If true, AUDO objects will be loaded on demand via DataWin_loadAudoIfNeeded, and unloaded if memory is tight. + bool lazyLoadAudio; // When lazyLoadRooms is true, this list indicates which rooms should be loaded during load time instead of demand. They will also not be freed. StringBooleanEntry* eagerlyLoadedRooms; @@ -938,6 +940,7 @@ struct DataWin { size_t fileSize; // cached size of the DataWin, captured at parse time. Used for platforms where fseek(SEEK_END)+ftell is unreliable due to buffering (like the PlayStation 2). bool lazyLoadRooms; // mirrors the parser option so Runner can branch without re-reading options bool lazyLoadTextures; // ditto, but with TXTR pages + bool lazyLoadAudio; // ditto, but with AUDO entries }; DataWin* DataWin_parse(const char* filePath, DataWinParserOptions options); @@ -958,5 +961,6 @@ void DataWin_bumpVersionTo(DataWin* dw, uint32_t major, uint32_t minor, uint32_t void GamePath_computeInternal(GamePath* path); PathPositionResult GamePath_getPosition(GamePath* path, float t); void DataWin_loadTxtrIfNeeded(DataWin* dw, uint32_t textureId); +void DataWin_loadAudoIfNeeded(DataWin* dw, uint32_t audioEntryId); #endif /* _BS_DATA_WIN_H_ */ diff --git a/src/desktop/main.c b/src/desktop/main.c index d55cb3bc1..f68f0208c 100644 --- a/src/desktop/main.c +++ b/src/desktop/main.c @@ -249,6 +249,7 @@ typedef struct { bool lazyRooms; StringBooleanEntry* eagerRooms; // stb_ds string-keyed set of room names bool lazyTextures; + bool lazyAudio; DataWinLoadType loadType; int profilerFramesBetween; // 0 = disabled #ifdef ENABLE_VM_OPCODE_PROFILER @@ -435,6 +436,7 @@ static void printUsage(const char *argv0) { " --save-folder - Set the directory will save files will be stored\n" " --game-args - Arguments to pass to the game\n" " --lazy-textures - Load textures into VRAM on first use, improving startup times\n" + " --lazy-audio - Load audio into RAM on first use, reducing memory usage\n" " --load-type - Specify how data.win is loaded, per-chunk or all at once\n" #ifdef EABLE_VM_OPCODE_PROFILER " --profile-opcodes - Rank which GML opcodes were executed the most\n" @@ -494,6 +496,7 @@ static void parseCommandLineArgs(CommandLineArgs* args, int argc, char* argv[]) {"save-folder", required_argument, nullptr, 'B'}, {"game-args", required_argument, nullptr, 'N'}, {"lazy-textures", no_argument, nullptr, 'L'}, + {"lazy-audio", no_argument, nullptr, 'K'}, {"load-type", required_argument, nullptr, 999}, #ifdef ENABLE_VM_OPCODE_PROFILER {"profile-opcodes", no_argument, nullptr, 'Q'}, @@ -589,6 +592,9 @@ static void parseCommandLineArgs(CommandLineArgs* args, int argc, char* argv[]) case 'L': args->lazyTextures = true; break; + case 'K': + args->lazyAudio = true; + break; case 'e': shput(args->eventsToBeTraced, optarg, true); break; @@ -1051,6 +1057,7 @@ int main(int argc, char* argv[]) { options.loadType = args.loadType; options.lazyLoadRooms = args.lazyRooms; options.lazyLoadTextures = args.lazyTextures; + options.lazyLoadAudio = args.lazyAudio; options.eagerlyLoadedRooms = args.eagerRooms; DataWin* dataWin = DataWin_parse(currentDataWinPath, options); From c6faa5cafb907cf603d31fdcbe0da30f51774278 Mon Sep 17 00:00:00 2001 From: cobaltgit Date: Fri, 17 Jul 2026 16:09:38 +0100 Subject: [PATCH 2/4] comment thingy --- src/data_win.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data_win.h b/src/data_win.h index e6af39c8a..c0854b496 100644 --- a/src/data_win.h +++ b/src/data_win.h @@ -50,7 +50,7 @@ typedef struct { bool lazyLoadRooms; // If true, TXTR objects will be loaded on demand via DataWin_loadTxtrIfNeeded, and unloaded if memory is tight. bool lazyLoadTextures; - // If true, AUDO objects will be loaded on demand via DataWin_loadAudoIfNeeded, and unloaded if memory is tight. + // If true, AUDO objects will be loaded on demand via DataWin_loadAudoIfNeeded. bool lazyLoadAudio; // When lazyLoadRooms is true, this list indicates which rooms should be loaded during load time instead of demand. They will also not be freed. From 366c98408fb2e153f7c5f75653e65d813168e992 Mon Sep 17 00:00:00 2001 From: cobaltgit Date: Fri, 24 Jul 2026 22:00:37 +0100 Subject: [PATCH 3/4] fix(openal): add back lazy audio, remove mmap for now (will be in a separate PR to investigate memory savings) --- src/audio/miniaudio/ma_audio_system.c | 2 -- src/audio/openal/al_audio_system.c | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/audio/miniaudio/ma_audio_system.c b/src/audio/miniaudio/ma_audio_system.c index 528685875..2675ec035 100644 --- a/src/audio/miniaudio/ma_audio_system.c +++ b/src/audio/miniaudio/ma_audio_system.c @@ -755,8 +755,6 @@ static void maGroupLoad(AudioSystem* audio, int32_t groupIndex) { DataWinParserOptions options = {0}; options.parseAudo = true; options.lazyLoadAudio = audio->dw->lazyLoadAudio; - if (audio->dw->mappedFile) - options.loadType = DATAWINLOADTYPE_MAP_FILE; DataWin *audioGroup = DataWin_parse(((MaAudioSystem*)audio)->fileSystem->vtable->resolvePath(((MaAudioSystem*)audio)->fileSystem, buf), options); arrput(audio->audioGroups, audioGroup); free(buf); diff --git a/src/audio/openal/al_audio_system.c b/src/audio/openal/al_audio_system.c index 8d71580f0..84bafc751 100644 --- a/src/audio/openal/al_audio_system.c +++ b/src/audio/openal/al_audio_system.c @@ -163,6 +163,7 @@ static char* resolveExternalPath(AlAudioSystem* ma, Sound* sound) { static void maInit(AudioSystem* audio, DataWin* dataWin, FileSystem* fileSystem) { AlAudioSystem* ma = (AlAudioSystem*) audio; + ma->base.dw = dataWin; arrput(ma->base.audioGroups, dataWin); ma->fileSystem = fileSystem; @@ -442,6 +443,7 @@ static int32_t maPlaySound(AudioSystem* audio, int32_t soundIndex, int32_t prior return -1; } + DataWin_loadAudoIfNeeded(ma->base.audioGroups[sound->audioGroup], (uint32_t)sound->audioFile); AudioEntry* entry = &ma->base.audioGroups[sound->audioGroup]->audo.entries[sound->audioFile]; uint32_t channels = 0; @@ -888,6 +890,7 @@ static float maGetSoundLength(AudioSystem* audio, int32_t soundOrInstance) { bool inAudo = !isRegular || isEmbedded || isCompressed; if (inAudo) { if (0 > sound->audioFile || (uint32_t) sound->audioFile >= ma->base.audioGroups[sound->audioGroup]->audo.count) return 0.0f; + DataWin_loadAudoIfNeeded(ma->base.audioGroups[sound->audioGroup], (uint32_t)sound->audioFile); AudioEntry* entry = &ma->base.audioGroups[sound->audioGroup]->audo.entries[sound->audioFile]; uint32_t channels, sampleRate, bitsPerSample, audioDataLen; @@ -946,8 +949,6 @@ static void maGroupLoad(AudioSystem* audio, int32_t groupIndex) { DataWinParserOptions options = {0}; options.parseAudo = true; options.lazyLoadAudio = audio->dw->lazyLoadAudio; - if (audio->dw->mappedFile) - options.loadType = DATAWINLOADTYPE_MAP_FILE; DataWin *audioGroup = DataWin_parse(((AlAudioSystem*)audio)->fileSystem->vtable->resolvePath(((AlAudioSystem*)audio)->fileSystem, buf), options); arrput(audio->audioGroups, audioGroup); free(buf); From 17b80f62f2248a035ed2b12290e34ebd0c7d13fe Mon Sep 17 00:00:00 2001 From: cobaltgit Date: Tue, 28 Jul 2026 18:55:50 +0100 Subject: [PATCH 4/4] whitespace --- src/desktop/main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/desktop/main.c b/src/desktop/main.c index 377083801..b3b0626b0 100644 --- a/src/desktop/main.c +++ b/src/desktop/main.c @@ -435,7 +435,7 @@ static void printUsage(const char *argv0) { " --save-folder - Set the directory will save files will be stored\n" " --game-args - Arguments to pass to the game\n" " --lazy-textures - Load textures into VRAM on first use, improving startup times\n" - " --lazy-audio - Load audio into RAM on first use, reducing memory usage\n" + " --lazy-audio - Load audio into RAM on first use, reducing memory usage\n" " --load-type - Specify how data.win is loaded, per-chunk or all at once\n" #ifdef EABLE_VM_OPCODE_PROFILER " --profile-opcodes - Rank which GML opcodes were executed the most\n"