Asynchronous Bank Unloading

We load multiple banks from different FMOD projects into our sound environment to handle DLC. When we load a new user vehicle, we have to replace the entire mixing environment. To do this we:

  1. Stop all playing sounds.

  2. Release all instances that we have handles to.

  3. Unload all banks.

  4. We then load the new master bank for the new vehicle and

  5. Load all other unrelated banks that were used in the current level.

  6. Recreate all instances.

We do this complete tear-down and build-up because the sounds in other banks might attach to different DSP chains depending on the bus mixing of the newly loaded master bank.

We discovered today that bank unload is asynchronous, and a particularly heavy bank our sound designer created doesn’t actually unload before we reload it (in step 5) - we then get a duplicate bank error.

Our questions are:

  1. Is an unload/reload of these banks necessary? Or is it adequate to destroy and re-create the instances? We expect the entire mixer is being rebuilt by loading a new master bank.

  2. If the unload and reload of these extra banks is necessary, what is the best way to block until the bank reload completes? Do we need to call “update” on the system repeatedly? Is the bank unload guaranteed to complete in bounded time? Is there any way to use the waiting thread to speed the process up?

1 Like

It’s hard to advise regarding whether you need to unload everything or not as it depends on how your DLC or UGC banks change the mix. If the user generated content is purely additional you shouldn’t need to unload all banks, just the UGC banks. If the user generated content can change the global mix, then unfortunately yes, you will need to unload everything to get back to a clean state.

Unloading and loading of banks are processed in-order asynchronously, I’m not seeing how you got a duplicate bank error. If you want to perform a blocking operation to ensure all async commands and bank processing is complete, use System::flushCommands.

Hi Mathew,

By “duplicate bank error” what I mean to say is that when we call

System::loadBankMemory(ptr, size, FMOD_STUDIO_LOAD_MEMORY, FMOD_STUDIO_LOAD_BANK_NORMAL, &bank)

We get back FMOD_ERR_EVENT_ALREADY_LOADED.

Anyway, System::flushCommands does not seem to be blocking at all. When we call UNLOAD, the bank’s state goes to “Unloading”. Then we call System::Update and System::flushCommands and check the state again and it’s still “Unloading”. I’ve even put a tight while-loop around the Update/flushCommands/getLoadingState loop to see if it eventually finishes and the state never transitions to anything past “Unloading”. We’ve also tried calling System::flushSampleLoading() which didn’t seem to help either.

We’re using FMOD_VERSION 0x00020108

That’s quite strange, are you able to reproduce these issues in one of our examples?
Also, can you confirm that ever function is returning FMOD_OK?

I’ve not tried any of the sample apps. I can try tomorrow if I can find some time to compile them.

All of the calls check for FMOD_OK though and they’re all returning it.

Ok after a ton of trial and error here’s what I’ve learned.

Running the while-loop I mentioned above leads to flushCommands EVENTUALLY blocking (After many calls)…but it blocks forever and hangs.

This only happens with this one specific bank. So we tried removing things from the bank until it worked which lead us to a single referenced event. But it doesn’t appear to be a problem with how we’ve made the event, it seems to be the event itself.

In other words, just cloning that reference event and using that clone instead works just fine. If we go back to the original referenced event it hangs.

If this is of interest to you, I can send someone the bank and project to look at. We looked at the difference between the problem-event and the cloned one and the only differences we see in the XML is the GUIDS but maybe we missed something.

Indeed that is very strange, could you upload to your FMOD profile?
I’d be interested to take a look and get to the bottom of this.

Ok I just uploaded it. It’s fmod_project.zip. Let me know if you need more information. The referenced event is the ‘Roads’ event that’s part of the /environment/ambiance/ambiance event.

So the application starts the /environment/ambiance/ambiance event and as long as ‘Roads’ is triggered, the bad behavior occurs. If Roads is never triggered, or not referenced or cloned and renamed, it behaves properly.

I’ve put your project and event into one of our examples but I cannot reproduce the issues you are seeing, here is the code I am using, can you spot anything different from what you are doing?

#include "fmod_studio.hpp"
#include "common.h"

int FMOD_Main()
{
    Common_Init(nullptr);

    FMOD::Studio::System *system = nullptr;
    ERRCHECK(FMOD::Studio::System::create(&system));
    ERRCHECK(system->initialize(1024, FMOD_STUDIO_INIT_NORMAL, FMOD_INIT_NORMAL, nullptr));

    FMOD::Studio::Bank *masterBank = nullptr;
    ERRCHECK(system->loadBankFile(Common_MediaPath("mathew/unload_repro/Master Bank.bank"), FMOD_STUDIO_LOAD_BANK_NORMAL, &masterBank));

    FMOD::Studio::Bank *stringsBank = nullptr;
    ERRCHECK(system->loadBankFile(Common_MediaPath("mathew/unload_repro/Master Bank.strings.bank"), FMOD_STUDIO_LOAD_BANK_NORMAL, &stringsBank));

    FMOD::Studio::Bank *environmentBank = nullptr;
    ERRCHECK(system->loadBankFile(Common_MediaPath("mathew/unload_repro/environment.bank"), FMOD_STUDIO_LOAD_BANK_NORMAL, &environmentBank));

    FMOD::Studio::EventDescription *loopingAmbienceDescription = nullptr;
    ERRCHECK(system->getEvent("event:/environment/ambiance/ambiance", &loopingAmbienceDescription));

    FMOD::Studio::EventInstance *loopingAmbienceInstance = nullptr;
    ERRCHECK(loopingAmbienceDescription->createInstance(&loopingAmbienceInstance));
    ERRCHECK(loopingAmbienceInstance->start());

    do
    {
        Common_Update();

        if (Common_BtnPress(BTN_ACTION1))
        {
            ERRCHECK(loopingAmbienceInstance->stop(FMOD_STUDIO_STOP_IMMEDIATE));
            ERRCHECK(loopingAmbienceInstance->release());
            ERRCHECK(environmentBank->unload());
            ERRCHECK(system->flushCommands());

            FMOD_STUDIO_LOADING_STATE state = FMOD_STUDIO_LOADING_STATE_LOADED;
            environmentBank->getLoadingState(&state);
            assert(state == FMOD_STUDIO_LOADING_STATE_UNLOADED);
        }

        ERRCHECK(system->update());

        Common_Sleep(50);
    } while (!Common_BtnPress(BTN_QUIT));

    ERRCHECK(stringsBank->unload());
    ERRCHECK(masterBank->unload());
    ERRCHECK(system->release());

    Common_Close();

    return 0;
}

I tried your sample code and I too couldn’t seem to get it to fail like our app no matter what I tried.

I did fix our app however. We discovered that we were not stopping event instances before releasing them on our teardown path toward a bank unload. We thought we were but we were not. Once I added the stop before the release, the bank seems to unload rapidly as you’d expect.

What still doesn’t make sense to me is why I can’t replicate this behavior by removing the “stop” from your sample app. I also tried removing the release…doing anything I could to cause the bank unload to hang but it doesn’t do it. Is there protection in place to kill events that are still running when an unload is called? If so, it’s odd that it kicked in for the sample app but not ours. If not, why doesn’t the sample app hang without stop being called?

Another question is what we’re supposed to do with one-shot events. The docs say that they continue until their natural end and we certainly don’t want to wait for that when we’re trying to unload a bank. We don’t bookkeep the one-shots. We fire and forget them so there’s no easy way for us to stop them like there is for looping events. Is there something else we can do that’s sufficient like calling stop on a bus? Or do we need to start bookkeeping these one-shots?

When you unload a bank it will destroy any Events that only exist in that bank, if they also exist in other banks they will persist. So for your case of unloading all banks, no Events should survive, you shouldn’t need to do any extra bookkeeping for one-shots, unload should take care of it.

If you need an explicit mechanism for stopping everything though, consider Studio::Bus::stopAllEvents on the master bus however it shouldn’t be necessary.

If you manage to reproduce the game behavior in the sample I’d be keen to dig into why it’s not working for you.