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);
}
}