Building banks succeeds, loading fails

This isn’t a question but rather a bug report.

I have now wasted quite a bit of time on stuff that is simply bad usability.

  1. In the Mixer window I once added a Send->Reverb to the Master Bus. Build works fine, no warnings whatsoever. Runtime just hangs on loading the soundbank.
  2. Similarly I added a Send->Reverb to something else (maybe the reverb itself, can’t remember, I clicked on many things). Build succeeds, no warnings whatsoever. Runtime seems to be loading ok, but then complains it cannot find any event. Undoing that one step fixed it again.
  3. I saw a video where someone said you’d turn a 3D sound into a 2D sound by deleting that one 3D part from the deck in an event. Did that, again, build succeeds (sensing a pattern here), runtime fails to load the soundbank with FMOD_ERR_NOTREADY.
  4. I did something else, not sure what, now soundbank fails to load with FMOD_ERR_INTERNAL. Randomly removing things I might have added fixed it again.

This is not helpful. When I create some invalid configuration I expect that I get an error on export, pointing out what I might have done wrong. Crashing or hanging the game on soundbank load or simply not working is not an option. How is anyone supposed to debug that, once you have multiple soundbanks that are only loaded on demand ?

Regarding FMOD_ERR_INTERNAL, I have two projects (because of another bug, that I am trying to fix and I wasn’t sure what’s going on so I added a fresh project). Both export their differently named soundbanks into the same directory. Still they seem to affect each other. I now got the same error again. Without changing anything else, I just deleted the soundbanks, then exported first the project that writes “Master Bank.bank” then exported the project that exports a bank with a custom name, now the error is gone.

Hello Jan,

Sorry to hear about the problems you’re having.

Unfortunately I cannot seem to reproduce the errors you’re facing with a few different builds.

Could you please provide your project and game code along with which version of FMOD Studio you’re using to support@fmod.org for us to investigate further?

Thanks,
Richard

1 Like

I have identified one error properly now.

Once you set FMOD_STUDIO_INIT_LIVEUPDATE during initialization, loading a sound bank will fail with FMOD_ERR_NOTREADY when you run a SECOND process that uses fmod.

I did send you a sample app, you can easily test this, just add the flag and run the process twice, the second instance will fail.

This is independent of whether an fmod Studio application is running. I guess it uses some OS wide resource internally and while initialization works fine, somehow sound bank loading cannot handle this.

This is a bit unfortunate, since the way our engine works, it is quite common to start a second instance in parallel. While I can see that actually connecting to two different instances might not work, it should at least degrade gracefully or better, return an error code during initialization, such that I can see that another instance is already running, and try to initialize it again without the flag.

Hello Jan,

That seems to be correct. When FMOD_INIT_LIVEUPDATE is used, it opens up a system-wide port to be used to connect to FMOD Studio.

If you wish to use FMOD_INIT_LIVEUPDATE with multiple instances of FMOD, you will need to call FMOD_ADVANCEDSETTINGS and set the profilePort to be something new for each instance. Please note that FMOD_ADVANCEDSETTINGS needs to be set before initializing FMOD.

For example:

FMOD_ADVANCEDSETTINGS settings = {};
settings.cbSize = sizeof(settings);
settings.profilerPort = 1234;

system->setAdvancedSettings(&settings);

system->init…

You may need to have some smarts in place for detecting which ports are being used if you wish to have Live Update enabled for each instance.

Let me know if this has helped.

Thanks,
Richard

1 Like

That’s good to know, thanks. Currently I am fine with having live update for the first instance that was started, but who knows for how long. I am currently working around this with a system wide named mutex, and don’t add the live update flag, when another instance is already running.

That said, I still think you should change it, that either the initialization fails or returns some kind of information that live update with the default setting won’t work, and make sure that it doesn’t result in a failure when loading a bank, as this is pretty difficult to track down and not obvious at all.

Thanks for your suggestion, Jan. I’ve passed it onto our development team.

This appears to still be the same in v2.00.11 and it took a very long time to figure out what was going on, even with this existing thread. Since I was running many short programs in parallel all with their own FMOD instances, sometimes some processes would collide in this way and other times they wouldn’t.

My use case is running ~100 instances of a test program that checks the memory impact of various project configurations. Since it would be exceedingly slow to run them all one at a time while developing the tool, I made it spawn a process for each test, which speeds things up considerably, but unfortunately breaks FMOD when trying to load banks.

I strongly second Jan’s opinion that a better error is in order since a non-deterministic error that is so vague can easily suck up hours of debugging.

I also observed issues when using a std::lock_guard<std::mutex> lock(gMyGlobalMutex) to prevent bad accesses to my memory analysis data while inside the useralloc, userrealloc, and userdealloc callbacks I registered with FMOD::Memory_Initialize. The following error would occur when attempting to lock the mutex.
image

After removing FMOD_STUDIO_INIT_LIVE_UPDATE from the FMOD_STUDIO_INITFLAGS AND FMOD_INIT_PROFILE_ENABLE from the FMOD_INITFLAGS I was using to initialize FMOD Studio, the errors disappeared. I tried just removing one or the other, but in order for the programs to not abort, they needed to both be removed.

Thanks for the feedback, I agree there should be more meaningful error messages for when a second FMOD process attempts to open the same port as the first. I’ve prioritized that task to be resolved in the coming months.

Both FMOD_STUDIO_INIT_LIVE_UPDATE and FMOD_INIT_PROFILE_ENABLE enable our network layer that opens a listening port, which explains why you must disable both.

I cannot explain why you are getting an abort in your code from your mutex, if you could modify one of our example to demonstrate I could look into it for you.

1 Like

I took the “simple_event” example project and added the following code in “simple_event.cpp”:

Between the includes and the start of FMOD_Main:

// ...
#include "common.h"
// ^^^ Already there
// vvv New stuff

#include <iostream>
#include <mutex>
#include <string>
#include <vector>

std::mutex mTotalBytesMutex;
unsigned int mTotalBytes = 0;
constexpr int gHeaderSize = sizeof(unsigned int);

void* F_CALL allocate(unsigned int size, FMOD_MEMORY_TYPE type, const char* sourcestr) {
    void* fullMemory = new char[gHeaderSize + size];
    unsigned int* header = static_cast<unsigned int*>(fullMemory);
    void* data = static_cast<char*>(fullMemory) + gHeaderSize;
    *header = size;
    {
        std::lock_guard<std::mutex> lock(mTotalBytesMutex);
        mTotalBytes += size;
        Sleep(100);
    }
    return data;
}
void F_CALL deallocate(void* ptr, FMOD_MEMORY_TYPE type, const char* sourcestr) {
    void* fullMemory = static_cast<char*>(ptr) - gHeaderSize;
    unsigned int* header = static_cast<unsigned int*>(fullMemory);
    {
        std::lock_guard<std::mutex> lock(mTotalBytesMutex);
        mTotalBytes -= *header;
        delete[] fullMemory;
        Sleep(100);
    }
}
void* F_CALL reallocate(void* ptr, unsigned int size, FMOD_MEMORY_TYPE type, const char* sourcestr) {
    void* fullMemory = static_cast<char*>(ptr) - gHeaderSize;
    unsigned int* header = static_cast<unsigned int*>(fullMemory);
    void* memory = allocate(size, type, sourcestr);
    memcpy(memory, ptr, size < *header ? size : *header);
    deallocate(ptr, type, sourcestr);
    return memory;
}

FMOD::Studio::System* gStudioSystem = nullptr;
FMOD::System* gCoreSystem = nullptr;

void test(int argc, const char** argv) {
    if (argc == 1) {
        std::cout << "Parent" << std::endl;

        struct Task {
            HANDLE mProcess;
            HANDLE mThread;
        };

        std::vector<Task> tasks;

        constexpr int taskCount = 2;
        for (int i = 0; i < taskCount; ++i) {
            char commandLine[256];
            sprintf_s(commandLine, "\"%s\" %d", argv[0], i);

            STARTUPINFO startupInfo;
            PROCESS_INFORMATION processInfo;

            memset(&startupInfo, 0, sizeof(startupInfo));
            memset(&processInfo, 0, sizeof(processInfo));

            startupInfo.cb = sizeof(startupInfo);

            if (CreateProcess(
                NULL,         // Program to execute
                commandLine,  // Command line
                NULL,         // Process handle not inheritable
                NULL,         // Thread handle not inheritable
                FALSE,        // Set handle inheritance to FALSE
                0,            // No creation flags
                NULL,         // Use parent's environment block
                NULL,         // Use parent's starting directory 
                &startupInfo, // Pointer to STARTUPINFO structure
                &processInfo) // Pointer to PROCESS_INFORMATION structure
                ) {
                Task task;
                task.mProcess = processInfo.hProcess;
                task.mThread = processInfo.hThread;
                tasks.emplace_back(std::move(task));
            }
            else {
                std::cout << "Could not start task: " << i << std::endl;
            }
        }

        while (!tasks.empty()) {
            for (int i = static_cast<int>(tasks.size()) - 1; i >= 0; --i) {
                DWORD exitCode;
                if (GetExitCodeProcess(tasks[i].mProcess, &exitCode)) {
                    if (exitCode == STILL_ACTIVE) {
                        continue;
                    }
                }
                std::cout << "Task finished (" << tasks.size() - 1 << " remaining): " << std::endl;
                CloseHandle(tasks[i].mProcess);
                CloseHandle(tasks[i].mThread);
                tasks.erase(tasks.begin() + i);
            }
        }
    }
    else if (argc == 2) {
        std::cout << "Child[" << argv[1] << "] start" << std::endl;
        FMOD::Memory_Initialize(nullptr, 0, allocate, reallocate, deallocate, FMOD_MEMORY_ALL);
        std::cout << "Child[" << argv[1] << "] memory" << std::endl;
        FMOD::Studio::System::create(&gStudioSystem);
        std::cout << "Child[" << argv[1] << "] create" << std::endl;
        gStudioSystem->getCoreSystem(&gCoreSystem);
        std::cout << "Child[" << argv[1] << "] initialize" << std::endl;
        gStudioSystem->initialize(32, FMOD_STUDIO_INIT_NORMAL | FMOD_STUDIO_INIT_LIVEUPDATE, FMOD_INIT_3D_RIGHTHANDED | FMOD_INIT_PROFILE_ENABLE, nullptr);
        std::cout << "Child[" << argv[1] << "] load master" << std::endl;
        FMOD::Studio::Bank* masterBank = NULL;
        gStudioSystem->loadBankFile(Common_MediaPath("Master.bank"), FMOD_STUDIO_LOAD_BANK_NORMAL, &masterBank);
        std::cout << "Child[" << argv[1] << "] load strings" << std::endl;
        FMOD::Studio::Bank* stringsBank = NULL;
        gStudioSystem->loadBankFile(Common_MediaPath("Master.strings.bank"), FMOD_STUDIO_LOAD_BANK_NORMAL, &stringsBank);
        std::cout << "Child[" << argv[1] << "] finish" << std::endl;
    }
}

extern int Common_Private_Argc;
extern char** Common_Private_Argv;

// ^^^ New stuff
// vvv Already there
int FMOD_Main()
// ...

AND at the start of FMOD_Main:

// ...
int FMOD_Main()
{
// ^^^ Already there
// vvv New stuff
    test(Common_Private_Argc, const_cast<const char**>(Common_Private_Argv));
    return 0;

// ^^^ New stuff
// vvv Already there
    void *extraDriverData = NULL;
// ...

With those modifications the output is not deterministic, but one run looked like the following:

I was able to make the abort happen every single time by adding the Sleep calls in the allocate and deallocate callbacks (which is also the reason the loading of the banks takes so long). I’ve noticed that without the Sleep calls in the allocate and deallocate callbacks, the abort occurs more frequently in banks with more data in them, probably because more allocate and deallocate are called more times. I’ve also noticed that without the Sleep calls, the abort occurs more frequently if you increase the number of processes (the constexpr int taskCount = 1 will control that in the code I’ve provided).