Ableton Windows OpenGL "freeze-out" on high work load and multi-instance

Hi,

I have a pretty bad problem with OpenGL UI’s and Ableton on Windows.

I uploaded a video demonstrating the issue here: Video showcasing the issue

And also uploaded a simple test project for reproduction here: Test Project

Once an UI has a little more work to do, Ableton will completely “lock you out” from interacting with it, when you open more than one of these “heavy duty” UIs. Depending on the workload the issue appears on 2 or 3 or more simultaneous UI instances.

The interesting part is, that Ableton doesn’t really seem to be frozen, as the save-popup on Window close indicates and also task manager is fine (although showing higher GPU usage, 5-10%). But any interaction with the mouse on Ableton is just gone, once you open the 2nd UI window.

The build in my video was made on the latest Master branch. I’m on Windows 11, but the issue also occurs on Windows 10. We had users report this over a range of our plugins and unfortunately the issue can be reproduced with my simple test project up there. It only happens on Windows when OpenGL is active. When I’m disabling OpenGL, everything is fine, although performance of course suffers.

I’m testing this on an AMD Radeon RX5700XT (latest driver), which should well be able to handle this. Users who report the issue are running high-end graphic cards like RTX 2080 (and generally high-end machines) as well.

Of course my test project is meant to stress-test this issue, but I have the exact same issue on UIs, that are just a tad bit more complex - a couple sliders and knobs, maybe one visualiser. The more simple UIs of our plugin range allow for 4-5 instances before this issue occurs.

Since I am able to reproduce this with my super simple test project up there, I would assume the issue lies with JUCE, but I’m happy to be told what I’m doing wrong instead. I get, that the UIs might start lagging when I’m doing too much work, but this complete freeze-out just because of opening a 2nd instance seems bad. I can easily open 10 instances of this as Standalone builds without any issues (other than performance starting to suffer a bit after 7-8 instances).

//Edit: Also, no issues visible on Visual Studio Debugger, when this happens, everything seems to be running smoothly as far as I can tell…

I have had similar issues in Cubase on Windows.

Every component repainting (especially moving) on the OpenGL-thread requires there MML-lock. (And something like painting complex shapes, will also involve the CPU, because JUCE renders the shape into edge-tables first on the CPU)

I guess because there is only a limited time on the message-thread - which is also shared with the host - this can cause unresponsive behaviour. (And too much OpenGL time, also)

Besides things what the JUCE-Team can do, (reserve some time for the message thread, or other optimisations) you can try to offload your animations to pure low level OpenGL inside the renderOpenGL() callback.

Because this doesn’t involve the message-thread.

I also use the OpenGL Context inside the GL-Callback, so I can comfortably draw 2D Graphics via the juce::Graphics class (At least if you don’t use any text-rendering which requires the message thread)
An earlier JUCE-Demo did the same, I wonder if its officially supported for this application)

something like this

 void MyComponent::renderOpenGL() override
 {
    std::unique_ptr<LowLevelGraphicsContext> glContext (createOpenGLGraphicsContext (*glContext,width, height);
    Graphics g (* glContext);
    g.drawEllipse (0.f, 0.f, 20.f,20.f, 1.f);
    g.drawSomethingWithoutInvolvingMessageThreadEtc()
 ...

Another thing you can do, If a component a only moves, but doesn’t needs to be redrawn, set setBufferedToImage(true), which maybe helps.

And of cause using a profiler helps, to find performance spots on the repainting routines.

I’m curious what the others say.

Of course, your problem could be due to something else entirely.

PS:
If wish there would be something like setContinuesRepaint on Component-Basis. Which let the OpenGL Renderer calling the paint()-methods without locking the Message-Thread directly. (Of cause paint needs to be thread safe designed then)

Edit: renamed Renderer to Context, to make it clear

1 Like

Alright! So, thanks to that amazing input from @chkn I figured out what I needed to figure out.

His approach with the GLRenderer is the right one, just one note:

only one glcontext and renderer per plugin/app - i first tried to have each ball in my example have its own Renderer and context. That won’t work. Rather, I just derived the PluginEditor from OpenGLRenderer, implemented the callbacks, and then iterate over the balls, supplying them with a graphics context to render on, as suggested by chkn.

I published the working implementation on its own branch over here:

So on the main branch you will find the problematic implementation for that rendering code, on the GLRenderer branch you will find the whole thing fixed.

Yay, case closed, thanks @chkn

2 Likes

Interesting!

Keep in mind, that with this implementation paint() will be called concurrent form the main-thread. So getLocalBounds() is not thread safe and shouldn’t be called from there. You need to find other methods to share the data between the message-thread and openGL thread, if you don’t want to lock the message-thread.

Also you still use components for the ball-Objects, but draw them only through openGL. I’m not sure if this is a clean implementation, but as a tech-demo this is interesting.

Would be interesting what other JUCE-users think about this “creative” way of using the OpenGL-GraphicsContext, to allow smooth concurrent drawing of simple 2D Graphics, without using LowLevel OpenGL Code and without locking the MessageThread.

1 Like

Yes I immediately noticed these issues when trying to apply this onto my existing plugin code :wink: But I think the path is clear anyways.

Also yes, re thread safety. I guess using ScopedLock locking will be fine in this case though, right? Or should this be handled lock-free? So say, I write the local bounds to a thread safe variable during resized ()… should be fine to just lock, right?

//Edit:
I just added a lock and also renamed the paint function in the balls to paintGl() to avoid the double painting. I left the balls as Component, because why not use the positioning functions anyways, while taking care of thread safety myself.

//Edit 2: This broke things, to the way they were before :-O… i’ll report back once i have more :smiley:

//Edit 3: Removed the locks, removed the Component base from the Ball class. Now it’s super performant and doesn’t cause issues anymore. Seems lock-free is the way to go here as well… Guess I’m gonna have a great week :smiley: At least now I know how to actually render on the GL thread using the Graphics class, this is a big step forward.

So, thanks to @chkn again for setting me on the right path here, hope this helps anyone encountering this in the future

I was under the impression there was just no way to use OpenGL on windows without using OpenGL directly. Nice trick!

The strange question then is, If it’s as simple as this, then why does the OpenGL renderer itself not function in this way?

1 Like

then why does the OpenGL renderer itself not function in this way?

because the “normal” juce-OpenGL render…

  1. is just a replacement for the native os-induced component repainting, where the paint()-method is guaranteed to happen on the message thread, and you don’t need to care about thread safety.

  2. only repaints when something needs to be repainted, and buffers the result in a bitmap-cache. So when nothing needs to be repainted, the render simply paints the cached bitmap again, which can be done on OpenGL very quickly

The above method is not a replacement for the gl renderer, it is only interesting for the parts of your GUI which need to be constantly repainted (with full fps, maybe the VU-Meter, Scrolling-Waveform etc…)) because it doesn’t interrupt the message-thread. (But of course you need to deal with all thread-safety issues yourself)

It could be that this trick, may have unwanted consequences, because createOpenGLGraphicsContext() is may not to designed to be used next to the OpenGL Render. So at the moment I only would use it as an experimental feature and not in productive code, at least until we a have an official “go” from someone in the juce-team. @reuk ?

I just saw that the JUCE-Demo still exactly uses the same technique in drawBackground2DStuff() in OpenGLDemo.h .

In practice I did a few stress-tests with my plugin.

If I am running multiple plugin-instances (each with one OpenGLContext) and I initiate them in parallel I get sometimes multiple GL-Errors on different occasions. (Especially on Apple M1)

I guess the problem is when multiple contexts in parallel initiate their shader-code or other stuff and using the GL-Render at the same time etc… this can be problematic (not a OpenGL expert here)