Messages are only being handled while the editor is open

When using JUCE 6.0.8 to create a VST3 plugin on Linux, none of JUCE’s asynchronous messaging utilities relying on the global MessageManager work while the editor is closed. This means that among other things AsyncUpdaters, Timers, and calls to juce::MessageManager::callAsync() won’t do anything until the user opens the plugin’s editor. This seems to be consistent across Bitwig Studio, REAPER, Ardour, Renoise and Carla (which uses JUCE’s own plugin hosting). A minimal example of this (courtesy of @eyalamir) would involve adding a simple default constructed timer to an audio processor class like so:

struct MyTimer : juce::Timer {
    MyTimer() { startTimerHz(1); }

    void timerCallback() override { std::cout << "Hello, world!" << std::endl; }
};

class MyProcessor : public juce::AudioProcessor {
    ...

    MyTimer t;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MyProcessor)
}

This timer only procs (and prints the message) while the editor is open.

1 Like

What if your processor class inherits from Timer directly?

class myProcessor : public juce::AudioProcessor, private juce::Timer 
{
public:
  // Constructor
  myProcessor()
  {
    startTimerHz(25);
  }

private:
  void timerCallback() override { std::cout << "Hello, world!" << std::endl; }
};

That also doesn’t do anything unless the editor is open.

On Linux there is no global event loop available like the other platforms so plug-ins need to hook into the host’s. In VST3 plug-ins this is done via the IRunLoop interface which is retrieved from the IPlugFrame callback interface of the IPlugView. This is only available when an editor is active so there’s no way to get event loop callbacks like Timers, AsyncUpdaters etc. without it.

How is a plugin supposed to perform potentially expensive computations when a parameter changes? AudioProcessorParameter::Listener suggests using AsyncUpdater, but if JUCE’s message loop not running while the editor is closed is intentional behavior and not a bug then that’s not an option. Should I just ignore JUCE’s whole messaging system and roll my own?

What is preventing a ‘SharedMessageThread’ as the one used in the VST2 wrapper ?

2 Likes

What is your use case? Are you not providing an editor at all or do you need to do processing whilst the editor is closed?

Yes, we roll our own message thread in the VST2 wrapper because there is no explicit support for hooking into the host’s run loop. It’s hard to say without investigating what the implications of doing this alongside the IRunLoop would be in the VST3 wrapper.

In this case it’s about handling computations triggered by parameter changes. Let’s say I have an FFT window size parameter. When that changes, I need to potentially do a bunch of allocations and some locking to setup new buffers so that on the next audio processing cycle the old and the newly resized buffers can just get swapped using a pointer. The JUCE docs suggest doing such potentially expensive and blocking operations from an AsyncUpdater, but those won’t fire unless the editor is open. This for instance makes it impossible to change those parameters from the host’s generic editor (or through automation, although a setting like this of course shouldn’t be automated).

EDIT: Another thing worth adding is that IRunLoop really is supposed to only be used for GUI operations, and in fact it can only be used for those things since the interface is implemented by IPlugFrame. So for general message handling JUCE really has to spin up its own message thread on Linux.

To me personally, the main advantage would be that all of my existing JUCE-based code expects the message thread to always run (for similar reasons as the OP), and if it won’t I’d have to modify a lot of my shared code whenever I choose to deploy Linux plugins (which I haven’t yet).

5 Likes

We’ll look into this. It sounds like the right solution is to do something similar to the VST2 message loop but it’ll require a bit of testing to see how it interacts with the host’s IRunLoop.

3 Likes

We’ve pushed some commits to the develop branch which should fix this. As predicted, the interaction between the host’s run loop and the shared message thread is a little tricky and there is some variation in how hosts implement the run loop interface so any feedback on the new changes would be appreciated.

3 Likes

I’ve only briefly experimented with this by reintroducing AsyncUpdater to a Linux VST3 plugin, but everything seems to be working great now! Tested with JUCE commit 1a5fb5992a1a4e28e998708ed8dce2cc864a30d7 in Bitwig Studio 3.3.7 and REAPER 6.26. Thanks a lot for the quick fix!

Hi ed95,

I observe the following behavior when comparing the current master with the current develop branch and testing on two different Bitwig Studio versions (3.2.8 which is the latest I have a license for and 3.3.7 the latest).

Using the master branch, the plugin loads fine on both versions, however I need to call an explicit sendChangeMessage to get my value updated (as expected).

Using the develop branch, the plugin does not load at all anymore on 3.2.8, unfortunately, while it works with 3.3.7. The async updater thread also seems to work fine on the newer version, since I receive the value updates without an explicit call to sendChangeMessage.

So while it was fixed in the newer version, there was some change introduced at the same time that makes loading of the plugin fail altogether on older versions. I attached a summary and the debug output that I get from Bitwig on the console. It seems there is some problem with reading the plugin metadata? Might just be a symptom and not cause of the problem though, I guess.
debug-log.txt (3.5 KB)

Thanks a lot for looking into this!
Patric

I’ve just tried to reproduce this with Bitwig 3.2.8 and I’m able to load the AudioPluginDemo VST3 built from the tip of develop. Can you try with the demo plug-in?

I just carefully retested all the combinations, with my own plugin (GitHub - drlight-code/osccontrol-light: An audio plugin that speaks OSC.) as well as the AudioPluginDemo. I observe the following:

AudioPluginDemo
develop / 3.2.8: :x:
develop / 3.3.7: :white_check_mark:
master / 3.2.8: :x:
master / 3.3.7: :white_check_mark:

osccontrol-light (as before)
develop / 3.2.8: :x:
develop / 3.3.7: :white_check_mark:
master / 3.2.8: :white_check_mark:
master / 3.3.7: :white_check_mark:

So interestingly, I can’t load develop for 3.2.8 just as with my plugin, while the demo plugin also fails on master here. This was tested on a Debian testing/bullseye system with gcc 10.2.1. What distro are you using? Maybe I can try to replicate it there.

Did you check why the plugin doesn’t load anymore? (I assume you mean that the plugin crashes during initialization) If you haven’t already, use a debug build, and then attach gdb to Bitwig’s plugin host process. Since the plugin crashes during initialization that makes it a bit more difficult, but three possible options (in order from easy to difficult) are:

  1. Using the ‘Within Bitwig’ plug-in hosting mode and attaching gdb to Bitwig’s audio engine process. This of course only works if the initial plugin scan has succeeded, since those are always done in separate BitwigPluginHost64 processes.

  2. Using a busy loop in a shell to attach gdb to the plugin host process right when it starts. This is probably the best solution here. You can use this Bash oneliner to wait for a BitwigPluginHost64 process to spawn and immediately attach to it. The use of sudo is necessary here because your user probably doesn’t have permissions to ptrace those Bitwig processes (unless sysctl kernel.yama.ptrace_scope is 0, but that’s a security issue in its own right).

    sudo true; pid=""; while [[ -z $pid ]]; do pid=$(pidof -s BitwigPluginHost64); done; sudo -E gdb -p "$pid"
    
  3. The third option, if the second option is somehow too slow (which it shouldn’t be), would be to replace /opt/bitwig-studio/bin/BitwigPluginHost64 with a wrapper that spawns a terminal which launches the actual BitwigPluginHost64 process running under gdb. But that really shouldn’t be necessary here.

1 Like
#0  0x0000000001472dde in XCreateGC ()
#1  0x00007fdb78f3c305 in XOpenDisplay () from /usr/lib/x86_64-linux-gnu/libX11.so.6
#2  0x00007fdb2ab5a32a in juce::XWindowSystem::initialiseXDisplay (this=0x7fdb1c000ba0) at /local/pschmitz/development/JUCE/modules/juce_gui_basics/native/x11/juce_linux_XWindowSystem.cpp:2978
#3  0x00007fdb2ab53696 in juce::XWindowSystem::XWindowSystem (this=0x7fdb1c000ba0) at /local/pschmitz/development/JUCE/modules/juce_gui_basics/native/x11/juce_linux_XWindowSystem.cpp:1433
#4  0x00007fdb2a7ff655 in juce::SingletonHolder<juce::XWindowSystem, juce::CriticalSection, false>::getWithoutChecking (this=0x7fdb2b0e0220 <juce::XWindowSystem::singletonHolder>) at /local/pschmitz/development/JUCE/modules/juce_core/memory/juce_Singleton.h:106
#5  0x00007fdb2a7f3e6b in juce::SingletonHolder<juce::XWindowSystem, juce::CriticalSection, false>::get (this=0x7fdb2b0e0220 <juce::XWindowSystem::singletonHolder>) at /local/pschmitz/development/JUCE/modules/juce_core/memory/juce_Singleton.h:90
#6  0x00007fdb2a7e03f0 in juce::XWindowSystem::getInstance () at /local/pschmitz/development/JUCE/modules/juce_gui_basics/native/x11/juce_linux_XWindowSystem.h:173
#7  0x00007fdb2a7e0df4 in juce::MessageThread::start()::{lambda()#1}::operator()() const (__closure=0xf092998) at /local/pschmitz/development/JUCE/modules/juce_audio_plugin_client/VST3/../utility/juce_LinuxMessageThread.h:63
#8  0x00007fdb2a825f3a in std::__invoke_impl<void, juce::MessageThread::start()::{lambda()#1}>(std::__invoke_other, juce::MessageThread::start()::{lambda()#1}&&) (__f=...) at /usr/include/c++/10/bits/invoke.h:60
#9  0x00007fdb2a825eef in std::__invoke<juce::MessageThread::start()::{lambda()#1}>(juce::MessageThread::start()::{lambda()#1}&&) (__fn=...) at /usr/include/c++/10/bits/invoke.h:95
#10 0x00007fdb2a825e9c in std::thread::_Invoker<std::tuple<juce::MessageThread::start()::{lambda()#1}> >::_M_invoke<0ul>(std::_Index_tuple<0ul>) (this=0xf092998) at /usr/include/c++/10/thread:264
#11 0x00007fdb2a825e70 in std::thread::_Invoker<std::tuple<juce::MessageThread::start()::{lambda()#1}> >::operator()() (this=0xf092998) at /usr/include/c++/10/thread:271
#12 0x00007fdb2a825dcc in std::thread::_State_impl<std::thread::_Invoker<std::tuple<juce::MessageThread::start()::{lambda()#1}> > >::_M_run() (this=0xf092990) at /usr/include/c++/10/thread:215
#13 0x00000000025b344f in ?? ()
#14 0x00007fdb79053ea7 in start_thread (arg=<optimized out>) at pthread_create.c:477
#15 0x00007fdb78ad5def in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

It seems to be related to the X11 display. I’ll keep investigating, just wanted to drop this here already. This is develop on 3.2.8.

My plugin fails with the same callstack. BTW this is a lifesaver to debug my own code as well, I wasn’t aware that it would be so easily possible to attach to the plugin process. Could have saved me quite some hours of “printf debugging” (into a logfile, that is). Thanks a lot for that!

1 Like
Package: libx11-6
Source: libx11
Version: 2:1.7.0-2
so-name: /usr/lib/x86_64-linux-gnu/libX11.so.6.4.0

With DAWs that don’t sandbox their plugins like REAPER, you can of course just launch the entire DAW under GDB. But with Bitwig just attaching GDB to a running plugin host process works great. I’m sure there’s also a hidden environment variable somewhere to launch the Bitwig plugin host processes under GDB, but I don’t know what that would be (I do know there’s one for specifying an alternative path to the plugin hosts from when I dug around in Bitwig’s .jar to find some tools to make debugging easier, before I realized that it pipes all plugin output to ~/.BitwigStudio/log/engine.log).

1 Like