From 705c018a29e085f491e628deaee596ad08faffeb Mon Sep 17 00:00:00 2001 From: Maddie <52103563+maddie480@users.noreply.github.com> Date: Sat, 13 Jun 2026 14:13:31 +0200 Subject: [PATCH 1/2] Reapply "Merge pull request #1079 from DashingCat/fast-fmod-bank-loading" This reverts commit 3ead00fde66699da94eee8dbf570b035a616e950. --- Celeste.Mod.mm/Mod/Core/CoreModuleSettings.cs | 5 + Celeste.Mod.mm/Patches/Audio.cs | 101 ++++++++++++++---- 2 files changed, 87 insertions(+), 19 deletions(-) diff --git a/Celeste.Mod.mm/Mod/Core/CoreModuleSettings.cs b/Celeste.Mod.mm/Mod/Core/CoreModuleSettings.cs index f5ca7814a..31ebdceb2 100644 --- a/Celeste.Mod.mm/Mod/Core/CoreModuleSettings.cs +++ b/Celeste.Mod.mm/Mod/Core/CoreModuleSettings.cs @@ -159,6 +159,11 @@ public bool LazyLoading { [SettingIgnore] public int LogHistoryCountToKeep { get; set; } = 3; + [SettingNeedsRelaunch] + [SettingInGame(false)] + [SettingIgnore] // TODO: Show as advanced setting. + public bool? FastFMODBankLoading { get; set; } = null; + [SettingNeedsRelaunch] [SettingInGame(false)] [SettingIgnore] // TODO: Show as advanced setting. diff --git a/Celeste.Mod.mm/Patches/Audio.cs b/Celeste.Mod.mm/Patches/Audio.cs index b79eeed0c..bd18bbab1 100644 --- a/Celeste.Mod.mm/Patches/Audio.cs +++ b/Celeste.Mod.mm/Patches/Audio.cs @@ -11,9 +11,11 @@ using SDL2; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Threading; namespace Celeste { static class patch_Audio { @@ -104,14 +106,7 @@ public static void Init() { } } - // Load any additional banks. - lock (Everest.Content.Map) { - foreach (ModAsset asset in Everest.Content.Map.Values.Where(asset => asset.Type == typeof(AssetTypeBank))) { - if (!ingestedModBankPaths.Contains(asset.PathVirtual)) { - IngestBank(asset); - } - } - } + IngestNewBanks(); AudioInitialized = true; } @@ -122,11 +117,24 @@ public static void IngestNewBanks() { } lock (Everest.Content.Map) { + bool nonBlockingLoad = CoreModule.Settings.FastFMODBankLoading ?? true; + + if (nonBlockingLoad) { + Logger.Info("Audio", "Loading FMOD banks in parallel"); + } + + List<(ModAsset, Bank)> additionalBanksMapping = new List<(ModAsset, Bank)>(); foreach (ModAsset asset in Everest.Content.Map.Values.Where(asset => asset.Type == typeof(AssetTypeBank))) { if (!ingestedModBankPaths.Contains(asset.PathVirtual)) { - IngestBank(asset); + additionalBanksMapping.Add((asset, IngestBank(asset, nonBlockingLoad))); } } + + if (nonBlockingLoad) { + PoolAndLoadBanksAndEvents(additionalBanksMapping); + } + + Logger.Info("Audio", $"Loaded {cachedBankPaths.Count} banks and {cachedModEvents.Count} events"); } } @@ -147,14 +155,45 @@ public static void Unload() { ready = false; } + private static void PoolAndLoadBanksAndEvents(List<(ModAsset, Bank)> additionalBanksMapping) { + while (additionalBanksMapping.Count > 0) { + // we can't use foreach as we remove elements while iterating + for (int i = additionalBanksMapping.Count - 1; i >= 0; i--) { + (ModAsset asset, Bank bank) = additionalBanksMapping[i]; + + RESULT result = bank.getLoadingState(out LOADING_STATE loadingState); + + if (CheckForBankLoadingErrors(result, asset)) { + if (result == RESULT.ERR_EVENT_ALREADY_LOADED) { + bank.unload(); // failed asynchronous banks should be released according to FMOD documentation + } + additionalBanksMapping.RemoveAt(i); + } else if (loadingState != LOADING_STATE.LOADING) { + // assume loading was done correctly + PostProcessBank(bank, asset); + additionalBanksMapping.RemoveAt(i); + } + } + + // wait before pooling loading state again + Thread.Sleep(100); + } + } + + // IngestBank(ModAsset) signature kept for ABI compatibility + public static Bank IngestBank(ModAsset asset) + => IngestBank(asset, false); + /// /// Loads an FMOD Bank from the given asset. /// - public static Bank IngestBank(ModAsset asset) { + public static Bank IngestBank(ModAsset asset, bool nonBlockingLoad) { if (Everest.Flags.IsHeadless) { return new Bank(IntPtr.Zero); } + LOAD_BANK_FLAGS flags = nonBlockingLoad ? LOAD_BANK_FLAGS.NONBLOCKING : LOAD_BANK_FLAGS.NORMAL; + Logger.Verbose("Audio.IngestBank", asset.PathVirtual); ingestedModBankPaths.Add(asset.PathVirtual); @@ -164,8 +203,7 @@ public static Bank IngestBank(ModAsset asset) { RESULT loadResult; if (CoreModule.Settings.UnpackFMODBanks) { - loadResult = system.loadBankFile(asset.GetCachedPath(), LOAD_BANK_FLAGS.NORMAL, out bank); - + loadResult = system.loadBankFile(asset.GetCachedPath(), flags, out bank); } else { IntPtr handle; modBankAssets[handle = (IntPtr) (++modBankHandleLast)] = asset; @@ -181,25 +219,50 @@ public static Bank IngestBank(ModAsset asset) { seekcallback = ModBankSeek }; - loadResult = system.loadBankCustom(info, LOAD_BANK_FLAGS.NORMAL, out bank); + loadResult = system.loadBankCustom(info, flags, out bank); + } + + if (nonBlockingLoad) { + return bank; // error handling and post-processing will be done in init later } - if (loadResult == RESULT.ERR_EVENT_ALREADY_LOADED) { - Logger.Warn("Audio.IngestBank", $"Cannot load {asset.PathVirtual} due to conflicting events!"); + if (CheckForBankLoadingErrors(loadResult, asset)) { return null; } - loadResult.CheckFMOD(); + PostProcessBank(bank, asset); + return bank; + } + + /// + /// Check for loading errors and log them. + ///
+ /// Returns true if an error was detected, false otherwise. + ///
+ private static bool CheckForBankLoadingErrors(RESULT result, ModAsset asset) { + switch (result) { + case RESULT.OK: + return false; + case RESULT.ERR_EVENT_ALREADY_LOADED: + Logger.Warn("Audio.IngestBank", $"Cannot load {asset.PathVirtual} due to conflicting events!"); + return true; + default: + Logger.Error("Audio.IngestBank", $"FMOD bank load failed on {asset.PathVirtual}: {result} ({Error.String(result)})"); + return true; + } + } + + /// + /// Post process the bank (ingest GUIDs and cache it). + /// + private static void PostProcessBank(Bank bank, ModAsset asset) { if (Everest.Content.TryGet(asset.PathVirtual + ".guids", out ModAsset assetGUIDs)) { IngestGUIDs(assetGUIDs); } - patch_Banks.ModCache[asset] = bank; - bank.getID(out Guid id); cachedBankPaths[id] = $"bank:/mods/{asset.PathVirtual["Audio/".Length..]}"; - return bank; } /// From 0d40428ca2c873968ed692b9db5b3e97c73a28ea Mon Sep 17 00:00:00 2001 From: Maddie <52103563+maddie480@users.noreply.github.com> Date: Sat, 13 Jun 2026 14:17:30 +0200 Subject: [PATCH 2/2] potentially fix freeze with fast FMOD bank loading (#1103) Co-authored-by: wuke65536 <2446214230@qq.com> --- Celeste.Mod.mm/Patches/Audio.cs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Celeste.Mod.mm/Patches/Audio.cs b/Celeste.Mod.mm/Patches/Audio.cs index bd18bbab1..94a27d945 100644 --- a/Celeste.Mod.mm/Patches/Audio.cs +++ b/Celeste.Mod.mm/Patches/Audio.cs @@ -58,7 +58,7 @@ public static void Init() { bool fmodLiveUpdate = Settings.Instance.LaunchWithFMODLiveUpdate; Settings.Instance.LaunchWithFMODLiveUpdate |= CoreModule.Settings.LaunchWithFMODLiveUpdateInEverest; - + // Original initialization code { FMOD.Studio.INITFLAGS flags = FMOD.Studio.INITFLAGS.NORMAL; @@ -116,26 +116,26 @@ public static void IngestNewBanks() { return; } - lock (Everest.Content.Map) { - bool nonBlockingLoad = CoreModule.Settings.FastFMODBankLoading ?? true; + List<(ModAsset, Bank)> additionalBanksMapping = new List<(ModAsset, Bank)>(); + bool nonBlockingLoad = CoreModule.Settings.FastFMODBankLoading ?? true; + lock (Everest.Content.Map) { if (nonBlockingLoad) { Logger.Info("Audio", "Loading FMOD banks in parallel"); } - List<(ModAsset, Bank)> additionalBanksMapping = new List<(ModAsset, Bank)>(); foreach (ModAsset asset in Everest.Content.Map.Values.Where(asset => asset.Type == typeof(AssetTypeBank))) { if (!ingestedModBankPaths.Contains(asset.PathVirtual)) { additionalBanksMapping.Add((asset, IngestBank(asset, nonBlockingLoad))); } } + } - if (nonBlockingLoad) { - PoolAndLoadBanksAndEvents(additionalBanksMapping); - } - - Logger.Info("Audio", $"Loaded {cachedBankPaths.Count} banks and {cachedModEvents.Count} events"); + if (nonBlockingLoad) { + PoolAndLoadBanksAndEvents(additionalBanksMapping); } + + Logger.Info("Audio", $"Loaded {cachedBankPaths.Count} banks and {cachedModEvents.Count} events"); } [MonoModReplace] @@ -170,7 +170,9 @@ private static void PoolAndLoadBanksAndEvents(List<(ModAsset, Bank)> additionalB additionalBanksMapping.RemoveAt(i); } else if (loadingState != LOADING_STATE.LOADING) { // assume loading was done correctly - PostProcessBank(bank, asset); + lock (Everest.Content.Map) { + PostProcessBank(bank, asset); + } additionalBanksMapping.RemoveAt(i); } }