Long pause when using a control in Cubase VST3 plug-in

High when I test my finished VST3 plug-in in Cubase I’m getting a 2 second pause when I first click a control, then afterwards it seems to run smoothly. I can’t use VST3 because of this.
I’m using OpenGL which renders 2 quads. They do use a simple shader that renders a frequency map and a loudness graphic, both set to setContinuousRepainting.

This is only a Cubase problem, but I’m surprised it hasn’t been spotted before.
I suspect it might be something to do with Cubase wanting to repaint quite quickly compared to other hosts.
edit running the same build as VST2 is fine in the same host (Cubase LE Elements 9).
I’m using a brand new iMac with Mojave, and 5.4.1 of Juce.
Anybody have any ideas?

2 Likes

It’s always Cubase and VST3, right?

Have you tried disabling setContinuousRepainting or trying to pinpoint where the hickup is?

Out there question - is the control doing anything with MIDI? Is it tied to the start of playback at the beginning of a project in Cubase? I’ve had a lot of problems (going back to Cubase 7) with MIDI and playback missing MIDI input at the beginning of a project, it’s a bug that I don’t think has been fixed and is very temperamental.

Yes VST3 Cubase, of all the hosts it would happen to, it would be Steinberg’s baby!! :slightly_smiling_face:
There is no MIDI on the plug-in.
This is just one instance in a completetely empty project. I boot Cubase as ‘empty’ then add an audio track then add my plug-in to an insert, and that’s it. It’s not even playing anything, that makes no difference anyhow.
Disabling continuous repaint will just break my plug-in, but I’ll try it to see if anything changes anything.

Turning off continuous repainting on both panels fixes the issue, but only turning off ONE of them reduces the pause - it’s still about .5 seconds but it’s still there.
Anyway my plug-in needs continuous updates, is there a way of slowing the updates down? It would be OK if they were just half, I could live with that, a bit of a compromise, but an OK one I suppose…

Just tried the develop branch - same problem.
edit this is ALL in Release BTW! Debug is just as bad.
edit2 It doesn’t matter what I set SetSwapInterval to either, it could be 0,1 or 300. (Incidently I had to set the interval in newOpenGLContextCreated() because it didn’t work at all in the constructor)

edit3

OK I think I may have found a compromise, it turns out that using SetSwapInterval(0)
actually works and un-sticks my Juce controls. BUT it renders at lightning speed now which fooks up my graphical, frame based animations. Not to mention it quite scarily could interfere with other software, not to mention other instances.
So now I’m going to have to put in my own timer in my renderOpenGL function which skips frames - Why do I have to do this crap!! [whimpers silently, goes to lie down somewhere…]

edit4
This of course creates a lot of tearing where the vsync is missed but it can’t be helped at the moment, I’ll just make it a VST3 specific thing in my code.

I’m seeing this issue as well, where mouse down events in Cubase 10.5 don’t seem to get picked up for 1-2secs, when I have an OpenGLRenderer with setContinuousRepainting(true). setSwapInterval(0) also has no effect on the mouse lag. It seems that only setting setContinuousRepainting(false) solves the issue, I’m relying on setContinuousRepainting(true) in order to get smooth rendering, as using a Timer can be choppy due to the lack of precision.

Has anyone else encountered this, and been able to work around it while keeping continuous repainting?

Ping. Still an issue. Has anyone found anything new on this front?

Same issue here.
I made a slightly modified version of JUCE’s AudioPlugin example which reproduces the issue in Cubase 11.
The TextButton in the example sometimes takes a while to respond to clicks. If you’re lucky and click the button fast enough, you’ll also be able to see the disappearing JUCE logo getting frozen while the button is unresponsive.

From what I can see, removing the following code in NSViewComponentPeer::toFront(), which gets called every time the button is clicked, resolves the issue:

        if (isSharedWindow)
            [[view superview] addSubview: view
                              positioned: NSWindowAbove
                              relativeTo: nil];

I guess this somehow interacts badly with the continuous repainting mode.
I don’t know why this is an issue only on Cubase - the same code path is reached also when running in Reaper, for example, but apparently there’s no responsiveness issue there.

3 Likes

Hey that sounds pretty much like the issue we reported here a few days ago:

We talked to the Cubase Developers a bit and there was the assumption that the reason for this issue only occurring with Cubase 11 could be related to the combination of the macOS SDKs both the plugin and Cubase have been built against. We are still seeing this issue on only a limited set of our own macs and didn’t succeed to build a reproducible test plugin that confirms this assumption

5 Likes

Looks like reverting 281ae0b, which introduces taking an extra lock in WaitableEvent::signal() in the CVDisplayLink callback, resolves the issue. That commit message says it fixes a deadlock (the forum thread for more details) so I guess a more complex fix than just reverting is needed; however, taking the extra lock inside the CVDisplayLink callback appears to be problematic.

2 Likes

Pushed two different fixes, both seem to work. Would appreciate feedback.

The first fix is a simple workaround: modify NSViewComponentPeer::toFront() to only bring the view to the front if it’s not already the front one. The responsiveness issue can probably still occur but much more rarely, so the issue becomes minor.

The second fix replaces juce::WaitableEvent with a std::condition_varaible for signalling repainting in OpenGLContext. This eliminates the lock on notification which juce::WaitableEvent does, which IIUC is needed to ensure robust notification of future waiters. std::condition_variable doesn’t ensure that, and I don’t think we need that assurance here.

Why we might want the assurance of notifying future waits is in cases when the render loop isn’t keeping up, then it’s possible the CVDisplayLink callback will make a second notification before the render loop finishes handling the first, the next frame will then only start to render on the third notification, instead of immediately when the first render finishes. So this could make cases where the render loop isn’t keeping up worse.

But, I think of more significance is that not taking a lock on notification will improve performance, possibly making previously not-keeping-up render loops keep up.

EDIT: Thinking more about this, we can probably add a similar std::atomic<bool> triggered data member like juce::WaitableEvent to catch missed notifications, which will work fine 99% of the time (the remaining 1% would be if we accidently reset an “extra” notification instead of the “original” one which we meant to reset. Though maybe this could be resolved by using an std::atomic<int>? And maybe this could also be used in juce::WaitableEvent to also remove the lock-on-notification there?)

5 Likes

Thanks for looking into this so thoroughly and for the example code!

The first fix seems like a sensible addition and something we should probably be doing anyway since we don’t want to be reordering subviews unless it’s necessary. I’ve pushed 3195db1 that adds some checks to ComponentPeer::toFront() and toBehind().

After doing some debugging the issue isn’t the extra lock that is taken in WaitableEvent::signal() - this is required to guard against spurious wakeups and missed notifications (see here) - although we can do some minor optimisation by reducing it’s scope slightly since it’s not needed around std::condition_variable::notify_all() (see point 3 here). I’ve pushed this in 76e9a76.

The actual issue is that the OpenGL render thread is getting out-of-sync with the CVDisplayLink callback, and presumably the refresh rate of the screen, hence the “lag”. The GL render thread is basically doing this:

while (! shouldExit)
{
    repaintEvent.wait (-1);
    renderFrame();
}

where repaintEvent is being signalled by the CVDisplayLink callback. However if the callback happens whilst the GL thread is in renderFrame(), it sets the repaint condition to true and the next wait() returns immediately causing the painting to be out of sync. Using a manual reset for the repaintEvent and resetting it like this:

while (! shouldExit)
{
    repaintEvent.wait (-1);
    renderFrame();
    repaintEvent.reset();
}

means that we’ll be in sync with the CVDisplayLink callback and the drawing is much smoother. Of course if renderFrame() takes the whole of its allotted CPU slot, then there’s a chance we end up halving the effective frame rate but dropping frames should be a relatively rare occurrence and in the typical case this results in much smoother rendering. I’ve pushed this to develop here:

and am no longer able to reproduce the laggy rendering in Cubase. Please try it out and let me know if you run into any more issues.

4 Likes

Thank you!

Great.

Were you able to reproduce the lag issue with the extra lock removed (that is with just this fix applied)?

Perfect, I noticed the same but preferred not touch WaitableEvent code

The lag is sometimes major, more than a second. How can the render thread being out-of-sync with the screen refresh rate cause this much lag? Even if it’s out of sync, an updated image -is- being redrawn between screen refreshes, so it should appear on the next screen refresh.
Also, the main thread is visibly hung, waiting on some lock (the OpenGL context lock?), and just being out of sync doesn’t (trivially, at least) explain that. I thought maybe the too-immediate rendering you described is constantly locking the OpenGL context thread (via NativeContext::Locker locker) but there’s a mechanism against starving the main thread under the comment // This avoids hogging the message thread when doing intensive rendering., so I don’t see how that would happen.

I can’t reproduce it either when cherry-picking just your fix.

So that commit fixed the lag for me, but just removing the lock from WaitableEvent::signal() did not which led me to believe it’s not the lock that is causing the issues and is instead due to how we are resetting/signalling the event. condition_variable on its own will behave the same as a manually reset WaitableEvent in that it’ll block on each wait() call.

It also seems unlikely to me that taking a lock at screen refresh rate would be the cause of performance issues since you’d only expect to hit those kind of bottlenecks when it’s really being hammered.

Were you able to verify this or did it just look hung due to the dropped frames?

Great!

2 Likes

Good point.

I was able to verify this via timing the difference between log messages I added before and after the subviews rearrangement operation. @PluginPenguin is showing the same stack trace in more detail his thread with a blocking wait on a mutex from within a call to CGLIsEnabled.

2 Likes

I had similar sluggish ui behaviour, not only with Cubase 11 but also with Ableton Live and even with the Juce Pluginhost - only with VST3, not AU. The fix solved the issue.

4 Likes

It looks like one of the recent changes (WaitableEvent: Release lock before calling notify_all() on condition… · juce-framework/JUCE@76e9a76 · GitHub) has caused a bug in WaitableEvent:

  • If I understand WaitableEvent correctly, a thread that .wait()s for it can also safely destruct the WaitableEvent after waiting on it
  • Before the change, the lock in WaitableEvent::signal() guaranteed that the .wait() would block until .signal() no longer needs the object alive. But now, the object can be destructed during the .signal() call
  • I added a unit-test to reproduce this problem in Add failing test for recent WaitableEvent change · juce-framework/JUCE@122bfaf · GitHub

Also, @danradix told me that this fix isn’t essential for the Cubase VST3 problem, hence we safely reverted it on the SR branch

2 Likes

Thanks, we’ve added this to develop:

4 Likes