More Fun OpenGL bugs and woes on JUCE 8 + DPI scaling

Hi, I see recently this has been worked on which is great! I’m very excited about better support for higher DPI and this is one reason I’m updating to JUCE 8.

Some things seem to work great out of the box, eg. within AudioPluginHost almost everything is as expected. Now when testing in Ableton - whether VST or VST3 - after disabling “auto-scaling”… things start falling apart:

  1. When reattaching a context to a priorly rendered component, the viewport is set incorrectly. Looks like it’s offset by scale / height or similar. This is “fixed” the moment you resize after a short while (and can thus sort of be worked around).

  2. Creating a separate, full screen kiosk mode component rendered through OpenGL hits the following `jassert`:

    and while the full screen mode works, the viewport is incorrectly scaled out of bounds, sort of inversely to issue 1.

  3. When exiting full screen, the old window is incorrectly resized inversely by the scaling, however the contents are not.

  4. Lastly, when I drag a window across monitors within Ableton Live, JUCE crashes within HWNDComponentPeer::setCustomPlatformScaleFactor. In the video I wasn’t able to reproduce that suddenly, instead it weirdly massively inflates or deflates my window size.

I have sort of narrated these issues in a video where you can see it: https://www.youtube.com/watch?v=Dt-RFEauG7c

Setup

  • Windows 11 24H2
  • Ableton 11.3.4
  • JUCE 8 @ develop 2ea448ffcc4ee5e1f81c8d827f91829b9156d1ec
  • NVIDIA 576.88
  • 3840x2160 @ 120hz with 150% scaling

This should all be reproducible at this commit of this PR if you want (it should also be very easy to build the plugin).

Or if you need further details, I’m happy to assist.

2 Likes

Thanks for reporting. I have a few questions:

  • Live 11.3.4 is quite old, and may contain bugs that have been fixed in later versions. Have you tested in the latest version of Live 12, and do you see the same issues there?
  • Have you tested in any other non-JUCE hosts? Do you see the same issues in those hosts?
  • Nvidia driver 576 is also quite old, 591 is available at time of writing. Does updating this driver have any effect?
  • It’s helpful to have an example project that demonstrates the problem. However, the provided project is a little difficult to debug as it contains a lot of extra unrelated code. If possible, it would be helpful if you could provide a minimal plugin that demonstrates the problem. This might be as simple as a blank project with an OpenGL context that continuously repaints its editor, with a button to enable kiosk mode.

Re. issue 1, are you just calling detach() and then attachTo() to switch the GL context between two components? I tried modifying the AudioPluginDemo to add an OpenGLContext, and then used a timer to switch the context between the two sliders. This appears to reproduce a similar issue to the one you reported, and I’m investigating a fix.

Re. issues 2, 3, 4, I wasn’t able to reproduce these behaviours in Live 11.3.43 or Live 12.3.5b3. Please could you try updating your copy of Live and check whether that helps?

I tested in Live 12.3.2, same behaviour. I should maybe add I have two monitors, and only one of them have DPI scaling enabled - perhaps this is relevant to triggering 2, 4.

FL, Reaper and Bitwig works exactly like the JUCE host (ie. as expected), except for additional issue 5: Exiting kiosk mode doesn’t quite work correctly. It sticks with a black screen, you can’t tab away but isn’t quite hung. With a ctrl+alt+delete it resets correctly back. This only happens on the primary DPI scaled display, not the other.

I could look at a minimal repro, not sure when I get around to that.

Thanks for reporting, a fix for this issue is now available on the develop branch:

Confirmed that this fixes issue 1, thanks!

I have made a minimal repro for you in this repo: GitHub - jthorborg/OpenGLDPIRepro: https://forum.juce.com/t/more-fun-opengl-bugs-and-woes-on-juce-8-dpi-scaling

Just press the big button.

This should reproduce issues 2 and 4 and in Ableton Live, with a minor detail change: on 2) it looks like the viewport is correct but the DPI scaling isn’t, so the OpenGL output so look significantly coarser than the standalone model. I suspect 3 might be a side-effect.

In standalone / AudioPluginHost, you should see dragging the plugin editor across to a non-scaled (or differently scaled?) display cause the weird progressively smaller resizing. In Ableton Live, it should instead crash.

I’ve now been able to investigate further. Thanks for the code example, which made it much easier to debug the problems you were seeing.

It looks like this happens because the thread that creates the new desktop window is not DPI-aware, but JUCE’s message window is. You can avoid the assertion by using a ScopedThreadDPIAwarenessSetter when adding the window to the desktop:

{
    // Required to avoid hitting DPI awareness mismatch assertion when creating new peer
   #if JUCE_WINDOWS
    const juce::ScopedThreadDPIAwarenessSetter scope{getWindowHandle()};
   #endif

    glWindow->addToDesktop(juce::ComponentPeer::StyleFlags::windowAppearsOnTaskbar);
}

This in turn requires setting the preprocessor definition JUCE_GUI_BASICS_INCLUDE_SCOPED_THREAD_DPI_AWARENESS_SETTER to 1. I recommend doing this globally by adding this entry to the project’s preprocessor flags in your Projucer project.

After adding this DPI awareness scope, I then observed that the GL window failed to display properly until I called setOpaque (true) in the constructor of InnerWindow. It’s generally good practice to call setOpaque (true) on any component that is used as a top-level window.

After adding the ScopedThreadDPIAwarenessSetter, I’m not seeing this behaviour.

I believe these are two separate issues.

I was able to reproduce the crash by entering and leaving kiosk mode, and then dragging the editor between displays. The crash was caused because a scale factor listener was not being correctly removed, so the peer attempted to call a listener callback on a dangling pointer. This bug will require a patch to JUCE itself. I’ve got a potential fix in progress, and I’ll update this thread once we have an update to share.

The issue where the plugin window grows/shrinks when dragged between windows appears to be a Live bug. I added some logging around the following VST3 API functions:

  • setContentScaleFactor, called when the host wants to set a new scale factor on the plugin
  • onSize, called by the host to set a new editor size in physical pixels
  • IPlugFrame::resizeView, called by the plugin to ask the host for a new editor size in physical pixels

The call sequence when moving an editor between displays is documented here:

However, in both Live 11 and 12, we see a sequence like the following when dragging from a primary display with a scale of 1.25 to a secondary display with a scale of 1:

setContentScaleFactor: 1            // Live notifies plugin that it should draw at scale of 1.
will request new size: 0 0 400 300  // Plugin asks for a 400x300 window.
setContentScaleFactor: 1.25         // *Inside* the request for a 400x300 window, Live reentrantly sets a new scale.
will request new size: 0 0 500 375  // The plugin thinks it's going back to the 1.25 scale display, asks for 500x375.
did request new size: 0 0 500 375   // That call completes.
onSize 0 0 400 300                  // Live informs the plugin that it should draw inside a 400x300 window.
                                    // At this point, the plugin believes it's drawing at 1.25 scale in a 400x300 window.
did request new size: 0 0 400 300   // Outer size request completes.
setContentScaleFactor: 1            // Live attempts to set a scale of 1 again.
will request new size: 0 0 320 240  // The plugin thinks it uses 400x300 pixels at 1.25 scaling, so it requests 320x240 at 1 scaling to maintain logical size.

... etc.

Unfortunately there’s not much we can do about this on the plugin side. The bad behaviour happens because Live reentrantly calls API functions that are not intended to be called reentrantly. We’ll report this bug to Ableton, and I suggest that you do, too.

This has been reported to Ableton as bug report L12-BUG-4865.

Thanks for reporting. A fix for this issue is now available on the develop branch:

1 Like

(The discovery happened at a late stage of JUCE7, but the relevant code parts have remained the same)

This also partly fixes my scaling issue (host is high-dpi but uses ScopedDPIAwarenessDisabler), the position is better, but the scaling still wrong. (I guess its the same More Fun OpenGL bugs and woes on JUCE 8 + DPI scaling - #6 by mayae )

However, I still need to change the size of the plugin window, otherwise SetWindowPos() not “repair” the wrong scaling.

I guess it’s a driver issue.

Here is the fix:

I removed updateWindowPosition() from createNativeWindow(), its called later anyway from updateViewportSize() (If I apply just the same size; windows doesn’t seem to update the scaling)

So maybe it would be better when the first updateWindowPosition() is called async/later, after the creation of the windows (internally) has finished.

Does this make sense?

Thank you for your work, it’s much appreciated!

This sort of almost works, I’m testing through issues. There’s an edge case where I enter kiosk mode immediately when the plugin editor is spawned (open/closing editor through host), and in those circumstances, `getWindowHandle()` returns null. So I can avoid that by adding a small delay…

When playing around further, I hit this crash:

Signalizer.vst3!juce::ComponentPeer::handleMovedOrResized() Line 352 C++ Signalizer.vst3!juce::HWNDComponentPeer::handlePositionChanged() Line 3415 C++ Signalizer.vst3!juce::HWNDComponentPeer::peerWindowProc(HWND__ * h, unsigned int message, unsigned __int64 wParam, __int64 lParam) Line 3962 C++ Signalizer.vst3!juce::HWNDComponentPeer::windowProc(HWND__ * h, unsigned int message, unsigned __int64 wParam, __int64 lParam) Line 3588 C++ user32.dll!00007fffa5ef7846() Unknown user32.dll!00007fffa5ef70cc() Unknown user32.dll!00007fffa5f30483() Unknown ntdll.dll!00007fffa69c5be4() Unknown win32u.dll!00007fffa3c416c4() Unknown Signalizer.vst3!juce::HWNDComponentPeer::setBoundsPhysical(const juce::Rectangle<int> & bounds, bool isNowFullScreen) Line 4319 C++Signalizer.vst3!juce::HWNDComponentPeer::setBounds(const juce::Rectangle & bounds, bool isNowFullScreen) Line 1418 C++
Signalizer.vst3!juce::ComponentPeer::updateBounds() Line 86 C++
Signalizer.vst3!juce::Component::setBounds(int x, int y, int w, int h) Line 1065 C++
Signalizer.vst3!juce::Component::setBounds(juce::Rectangle r) Line 1138 C++
Signalizer.vst3!juce::Desktop::setKioskModeComponent(juce::Component * componentToUse, bool allowMenusAndBars) Line 318 C++
Signalizer.vst3!juce::HWNDComponentPeer::peerWindowProc(HWND__ * h, unsigned int message, unsigned __int64 wParam, int64 lParam) Line 4034 C++
Signalizer.vst3!juce::HWNDComponentPeer::windowProc(HWND
* h, unsigned int message, unsigned __int64 wParam, __int64 lParam) Line 3588 C++
user32.dll!00007fffa5ef7846() Unknown
user32.dll!00007fffa5ef70cc() Unknown
user32.dll!00007fffa5f27e93() Unknown
ntdll.dll!00007fffa69c5be4() Unknown
win32u.dll!00007fffa3c412c4() Unknown
user32.dll!00007fffa5eee0ef() Unknown
user32.dll!00007fffa5eee078() Unknown
Ableton Live 11 Suite.exe!000000014254c191() Unknown

So in response to `WM_ACTIVEAPP`, callbacks are ultimately made on a component peer that has been freed (all memory is 0xDD). I can’t see how this can happen, since peers are only ever returned through the Desktop ultimately, and the desktop peer list at that point in time does not contain the peer the callback was made on. So it looks to me like the peer is somehow deleted as a result of this callstack somewhere.

I can see in my old JUCE copy I commented the call to setKioskModeComponent() over 10 years ago without much explanation beyond “getting fullscreen to work”. Perhaps this bug has been there all this time, and only exercised under my weird circumstances?

e: Looks like it can be possible in `ComponentPeer::handleMovedOrResized()` to call back into somewhere else, circumstantially deleting the peer.

I can repro this behaviour, getWindowHandle() indeed returns null during the constructor of the AudioProcessorEditor. However, this doesn’t seem to cause problems for me: I don’t hit the DPI-awareness-mismatch assertion; and the editor and fullscreen window seem to open with the correct dimensions and scale factor. Is the null window handle causing problems for you? If so, please describe the problem along with steps to reproduce the issue.

I’m also struggling to see what could cause a crash at the location you’ve shown. From the stack trace, it looks like the following has happened:

  • WM_ACTIVATEAPP got called, deactivating the host.
  • In response, the plugin exits kiosk mode.
  • While deactivating kiosk mode, JUCE attempts to resize the previous kiosk-mode component.

JUCE uses a raw pointer to hold on to the current kiosk component, so I wonder whether something’s happening to destroy that component while it’s still registered as the kiosk component. If so, the following call to leave kiosk mode may crash as it attempts to resize a component that no longer exists.

Are you seeing this crash in your minimal example plugin, or your original plugin, or both? Can you see anywhere that you might be destroying components while they are still set as the kiosk-mode component?

I’ve pushed a small change that might address the new crash you’re seeing:

Please try updating and check whether the issue persists. Thanks!

Hi, we’ve pushed a few more scaling-related fixes to the develop branch today. Please try updating and let us know if you’re still running into problems. Thanks!

Chiming in, it looks juicy, I appreciate your efforts! It’s just been a busy week at work, and now for a week of vacation, but then I’ll test and get back to you!