Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Celeste.Mod.mm/Mod/Core/CoreModuleSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
105 changes: 85 additions & 20 deletions Celeste.Mod.mm/Patches/Audio.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -56,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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -121,13 +116,26 @@ public static void IngestNewBanks() {
return;
}

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

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

[MonoModReplace]
Expand All @@ -147,14 +155,47 @@ 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
lock (Everest.Content.Map) {
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);

/// <summary>
/// Loads an FMOD Bank from the given asset.
/// </summary>
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);

Expand All @@ -164,8 +205,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;
Expand All @@ -181,25 +221,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 (loadResult == RESULT.ERR_EVENT_ALREADY_LOADED) {
Logger.Warn("Audio.IngestBank", $"Cannot load {asset.PathVirtual} due to conflicting events!");
if (nonBlockingLoad) {
return bank; // error handling and post-processing will be done in init later
}

if (CheckForBankLoadingErrors(loadResult, asset)) {
return null;
}

loadResult.CheckFMOD();
PostProcessBank(bank, asset);

return bank;
}

/// <summary>
/// Check for loading errors and log them.
/// <br/>
/// Returns <c>true</c> if an error was detected, <c>false</c> otherwise.
/// </summary>
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;
}
}

/// <summary>
/// Post process the bank (ingest GUIDs and cache it).
/// </summary>
private static void PostProcessBank(Bank bank, ModAsset asset) {
if (Everest.Content.TryGet<AssetTypeGUIDs>(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;
}

/// <summary>
Expand Down
Loading