Open TheGuyWhoo opened 3 years ago
I am also seeing the same problem with Ardour: two instances of the same Camomile LV2 plugin will work fine, but having two different Camomile plugins causes Ardour to hang.
OS: Debian 11
Architecture: amd64
DAW: Ardour 6.7.0
Camomile version: v1.0.8-beta10
I had a project in Ardour which already used an instance of AlmondOrgan (LV2). That works well. When I add an instance of the Castafiore effect (also LV2) to an audio track, Ardour freezes/hangs. At the same time, Ardour's terminal output is filled with thousands of repetitions of this message:
JUCE Assertion failure in juce_MessageManager.cpp:310
The error message refers to this part of Juce/modules/juce_events/messages/juce_MessageManager.cpp
:
bool MessageManager::Lock::tryAcquire (bool lockIsMandatory) const noexcept
{
auto* mm = MessageManager::instance;
if (mm == nullptr)
{
jassertfalse;
return false;
}
...
}
I don't know the JUCE code well, but the following around line 420 is suspicious. It loops over (a wrapper method of) the tryAcquire
method.
bool MessageManagerLock::attemptLock (Thread* threadToCheck, ThreadPoolJob* jobToCheck)
{
...
// tryEnter may have a spurious abort (return false) so keep checking the condition
while ((threadToCheck == nullptr || ! threadToCheck->threadShouldExit())
&& (jobToCheck == nullptr || ! jobToCheck->shouldExit()))
{
if (mmLock.tryEnter())
break;
}
...
}
Any ideas or comments would be appreciated because I'd like to get this bug fixed!
I added a few lines of debugging code to the attemptLock()
method mentioned in my previous comment. This confirmed that the infinite loop of assertion failures is happening in the while loop which I quoted.
Making use of backtrace, I also obtained a stack-trace showing (sort of) how attemptLock()
is being called:
backtrace returned 31 function calls
/usr/local/lib/lv2/Castafiore.lv2/Castafiore.so(_ZN4juce18MessageManagerLock11attemptLockEPNS_6ThreadEPNS_13ThreadPoolJobE+0x5f)
/usr/local/lib/lv2/Castafiore.lv2/Castafiore.so(_ZN4juce18MessageManagerLockC1EPNS_6ThreadE+0x57)
/usr/local/lib/lv2/Castafiore.lv2/Castafiore.so(_ZN14JuceLv2WrapperC2EdPKPK11LV2_Feature+0x2be)
/usr/local/lib/lv2/Castafiore.lv2/Castafiore.so(+0x6c9d0d)
/opt/Ardour-6.7.0/lib/liblilv-0.so.0(lilv_plugin_instantiate+0x183)
... stack-trace continues with more functions in Ardour code ...
This shows that, when an LV2 plugin is initialised, a JuceLv2Wrapper
object is constructed, and this leads to attemptLock()
being called. At the top of the constructor function (see LV2/juce_LV2_Wrapper.cpp
around line 727), a MessageManagerLock is declared:
{
const MessageManagerLock mmLock;
filter = std::unique_ptr<AudioProcessor>(createPluginFilterOfType (AudioProcessor::wrapperType_LV2));
}
My debugging suggests that, when a second Camomile plugin is initialised by the DAW, the construction of the JuceLv2Wrapper
never gets past this section of code. While it is trying to construct mmLock
, the tryEnter()
method is called repeatedly on the lock in a loop but it fails continuously on the assertion.
Here is how (I think) the MessageManager is created for an LV2 plugin.
Around line 1500 of LV2/juce_LV2_Wrapper.cpp
, in the definition of the class JuceLv2Wrapper
:
private:
#if JUCE_LINUX
SharedResourcePointer<SharedMessageThread> msgThread;
#else
SharedResourcePointer<ScopedJuceInitialiser_GUI> sharedJuceGUI;
#endif
That SharedMessageThread
object, when constructed, should call its own run()
method, which will create a MessageManager provided one doesn't already exist via the getInstance()
method.
struct SharedMessageThread : public Thread
{
void run() override
{
// ...
MessageManager::getInstance()->setCurrentThreadAsMessageThread();
// ...
while ((! threadShouldExit()) && MessageManager::getInstance()->runDispatchLoopUntil (250))
{}
}
}
Debugging shows that:
run()
is called.msgThread
points to the same thread which was created during the loading of the first plugin.Despite the fact that all plugin instances share the SharedMessageThread, the MessageManager instance cannot be found by the second plugin instance - calling MessageManager::getInstanceWithoutCreating()
will return 0
. This, in turn, causes an assertion in the JUCE code to be true when it is required to be false. JUCE just cranks the assertion again and again in a loop, causing the plugin and DAW to hang.
What I don't know is this: should the MessageManager be shared across all instances of Camomile plugins? At the moment, it is shared across instances of the same plugin, but not across instances of two different plugins. @pierreguillot do you have a view? Not a relevant question now.
I have found a fix, but not the fix...
I changed line 133 of CMakeLists.txt
to specify the latest C++ standard:
-set_target_properties(Camomile_LV2 PROPERTIES PREFIX "")
+set_target_properties(Camomile_LV2 PROPERTIES PREFIX "" CXX_STANDARD 20)
and made the SharedMessageThread...well...not shared! The change applies to LV2/juce_LV2_Wrapper.cpp
:
- SharedResourcePointer<SharedMessageThread> msgThread;
+ SharedMessageThread msgThread;
Multiple different Camomile plugins can now be loaded, and the plugins are working. Once, when I closed my Ardour session after inserting and deleting multiple Camomile plugins, it froze, but since then I have worked on the session several times and it has always closed without issue.
After trying a few things out, I've discovered that a message thread created via SharedResourcePointer
is actually shared across all instances of any Camomile plugin. Let's say I have a Bulgroz instance loaded already. I have verified that, when a Castafiore instance is loaded, it begins constructing a SharedResourcePointer<SharedMessageThread>
but stops because it finds that there is already a SharedResourcePointer of that type - namely the one created when Bulgroz was loaded.
This seems really odd to me...I would have expected a SharedResourcePointer to be shared across instances of the same plugin, but it is somehow being shared across all instances of all Camomile plugins.
How is it being shared? Well...
$ objdump -T ./Plugins/builds/Castafiore.lv2/Castafiore.so | grep 'getSharedObjectHolder'
00000000006cc3c3 w DF .text 000000000000000d Base _ZN4juce21SharedResourcePointerI19SharedMessageThreadE21getSharedObjectHolderEv
000000000111d980 u DO .bss 0000000000000018 Base _ZZN4juce21SharedResourcePointerI19SharedMessageThreadE21getSharedObjectHolderEvE6holder
The second line of output seems to refer to a variable in a method of the SharedResourcePointer class. The u
signifies that this is a unique global symbol. According to the man-page for objdump
:
Unique global symbols are a GNU extension to the standard set of ELF symbol bindings. For such a symbol the dynamic linker will make sure that in the entire process there is just one symbol with this name and type in use.
That explains it - this "holder", which keeps track of the object held by the SharedResourcePointer, will have the same value throughout the entire process - meaning the whole of Ardour?
The question now is: why the fu-...I mean, how did this end up being "unique global", and can this be changed?
It wasn't easy to find, but this GCC bug report seems to be the final piece in the puzzle: Problem with C++ unique symbols in plugins
The answer is that specifying -fno-gnu-unique
as an option to GCC will disable the marking of symbols as "unique global".
Raising a PR.
I guess this is fixed, isn't it?
(in the plugin by distrho called ildaeli when I enable 'run in bridge mode' I am able to load multiple instances. Also in reaper when I run carla as dedicated process I am able to load multiple instances)
Whether or not the same issue is present in vst3 idk because those don't load at all but I'll make a separate issue for that. It happens in Carla and reaper, both latest versions on 32 bit linux mint 19. it happens wirh all plugins generated through camomile. Multiple instances of the same plugin does not cause this issue. Carla freezes before it logs anything about the second instance, but here's a log anyway: