Logic Pro: Plugin Editor Destructor not always called on window close

We’ve got a very peculiar bug with our JUCE plugin in Logic Pro.

In some situations, when the user closes the Plugin Editor, the destructor of our juce::AudioProcessorEditor does NOT get called. After this has happened, if we reopen the plugin, the constructor of our juce::AudioProcessorEditor does NOT get called. Yet, the plugin UI does appear on screen and mostly work (with some broken parts). We verified this behavior in Logic with a debugger.

This behavior does not happen all the time. Usually we get regular constructor/destructor calls for plugin window open/close. But certain actions in our UI seem to trigger this no destruct/construct behavior occasionally.

Is it valid for Logic Pro to NOT destruct your Plugin Editor when you close the plugin editor window? Has anyone else ever experienced this behavior?

I am aware that DAWs will sometimes call the constructor/destructor multiple times before fully opening a plugin editor, but I have never heard of a DAW keeping your plugin editor alive even when you close the plugin window.

We’ve got a hunch that Logic may not be the culprit here, and that maybe we have a smart pointer keeping our Plugin Editor alive longer than it should be.

But as I am investigating, I was wondering if anyone else had ever experienced this with Logic specifically.

Thanks!

Specs:
Apple M1, JUCE 6.1.6

I don’t know of any requirement that the editor component be destroyed when closing the editor window. That seems to be the norm, but it’s up to the host, as far as I know. But to the point with Logic, I don’t recall ever seeing this behavior of it not destroying our editor.

2 Likes

Thanks for the feedback. That’s good to know.

True, Logic is not always deleting editor, i’ve seen that with our plugins. Live and Bitwig also expose similar behaviour, also on windows.

I think the problem in Live/Bitwig is that editor is preserved only with 1st plugin closing. After that, it’s working as usual, properly creating and destroying editor.

Anway, in my case, I added timer, which is constantly checking editor visibility, because there is no guarantee, that destructor will be called, when plugin editor is closed.

Check this post:

1 Like

What exactly are you doing with the timer if it determines the editor is actually invisible?
Are you manually killing off the PluginEditor?

I’m still not sure that our code is not the culprit here instead of Logic.
But you do say you have seen this same behavior. I just wonder if we actually have the same error in our code.

Our Processor components never explicitly address their Editor components in any way at all. All Editor-related work is done via the Editor component querying the Processor for information it needs during a timer callback in the Editor itself. We always assume that the Editor may or may not exist at any given time. That’s the only true safe way to deal with it, I would think.

2 Likes

In my specific case, when editor is closed, i have to remove some other top level windows (hosted plugins) from screen and vice versa. If editor would be properly destroyed, those windows would be automatically destroyed, too.

1 Like

After investigating this issue further, we were able to prevent the behavior of Logic Pro where the PluginEditor is sometimes not destroyed when it is closed.

In our case, we found that calling juce::Component methods, such as setVisible(), setBounds(), setPosition(), etc, from within any Component::paint() function caused Logic to not destroy the plugin editor when it was next closed.

Side Note: We are using OpenGL to render JUCE Components, so since paint() is called by the OpenGL thread in this case, the Component calls in paint() could’ve been data races on Component data with the JUCE Message Thread. (We were getting threading warnings for these calls in XCode but only when testing Audio Unit in Ableton. Removing these Component method calls from inside the paint() function resolved these threading warnings.)

Additionally, we found that closing any juce::PopupMenu causes the PluginEditor not to be destroyed when the editor is next closed in Logic Pro (AU). Specifically in juce_PopupMenu.cpp in the function PopupMenuCompletionCallback::modalStateFinished the call to toFront():

if (auto* topLevel = focusComponent->getTopLevelComponent())
    topLevel->toFront (true);

causes the PluginEditor not to be destroyed when it is next closed. Commenting out these lines resolves the issue.

Digging deeper, the specific cause of the closing Popup issue is found in juce_mac_NSViewComponentPeer.mm in NSViewComponentPeer::toFront()

if (makeActiveWindow)
    [window makeKeyAndOrderFront: nil]
else
    [window orderFront: nil]

both the lines which call window methods makeKeyAndOrderFront and orderFront trigger the unexpected behavior in Logic Pro, but only when these methods are called on the Top Level Component (plugin editor window) as seen in PopupMenuCompletionCallback::modalStateFinished callback.

This indicates to me that this behavior of Logic should NOT be expected as prior posts in this thread have indicated. Maybe this is a bug in Logic, or some kind of unspoken Audio Unit rule that you should not modify window ordering in Logic? We already know that Logic dislikes AUs spawning additional windows, hence the magenta tinting on transparent parts of PopupMenus that are shown outside the PluginWindow:

Similarly, maybe Logic dislikes ordering (e.g. orderFront) of Audio Unit windows and prevents the PluginEditor from being destroyed because ordering windows insinuates the AU may have multiple windows?

Note: I am using JUCE 6.1.6 and have not tested with the latest JUCE. I have verified at least that the code I reference relating to PopupMenu and NSViewComponentPeer still exists in JUCE 7.0.5.

@reuk Have you ever seen behavior like this in Logic Pro (or any other DAW)? As a JUCE plugin dev, should I assume it is legal for any DAW to NOT destroy my PluginEditor when the editor window gets closed? Thank you for any insight!

1 Like

AFAIK this is considered bad design and should be avoided: in paint() you should only draw the current state of whatever needs to be painted, but no changes to UI elements should happen there.
As a rule of thumb, inside paint() you should only call methods that are marked const in Component and its subclasses.

3 Likes

Good to know, thanks! We had realized we were getting infinite paint loops due to our calling Component methods like this in the paint function. For example, our paint() method might call setVisible() on a child component which could trigger a repaint() which would call paint() and repeat. I am surprised that JUCE docs for Component::paint() do not warn about this as I think it is easy for beginners (and me when I am not thinking) to make this mistake.

Besides this issue, I am still surprised Logic Pro does not call the destructor of the PluginEditor after you have closed a PopupMenu and then close the plugin window (detailed in prior post).

It would be helpful to know if we should expect a DAW to NOT destroy a PluginEditor when it is closed.

In my case, we are using some custom OpenGL components, and knowing the answer to this question would influence how we initialize these custom OpenGL components (either via constructors or via only newOpenGLContextCreated()).

I wrote a test to confirm the PopupMenu problem does exist in JUCE 7.0.5 (master branch in git).

For Audio Units running in Logic Pro on Apple M1 macs, if you close any juce::PopupMenu, the next time you close the plugin editor, the juce::AudioProcessorEditor destructor will NOT be called. The usual behavior is that when you close the plugin editor, it calls the destructor of juce::AudioProcessorEditor.

Here’s a repo with code that reproduces the issue:

In this test plugin, I have a button that opens a popup menu, a label that shows the time of construction, and a label that shows the current time.

Expected Logic Behavior

  1. Open the plugin
  2. Take note of the Construction Time value in the top left.
  3. Close the plugin (make sure you do not open/close the popup menu)
  4. Reopen the plugin
  5. Notice the Construction Time has changed.

In this sequence, Logic properly destroys and reconstructs the juce::AudioProcessorEditor.

Unexpected Logic Behavior

  1. Open the plugin
  2. Take note of the Construction Time value in the top left.
  3. Click the button to open the PopupMenu
  4. Either choose an option in the PopupMenu or close the menu by clicking outside of it.
  5. Close the plugin (make sure you do not open/close the popup menu)
  6. Reopen the plugin
  7. Notice the Construction Time has NOT changed. It is the same construction time from last plugin open.

In this sequence, Logic does NOT call the destructor of juce::AudioProcessorEditor when the plugin window is closed, and does NOT call the constructor of juce::AudioProcessorEditor when the plugin window is reopened.

This can be confirmed by placing breakpoints in XCode, assuming you disable macOS SIP to debug in Logic.

In my prior post, I explain the code that triggers this behavior in Logic:

That might be the usual behavior, but the DAW is not required to behave that way.

It’s perfectly fine for it to keep your editor instance upon close and simply hide it, then show it again the next time it is asked to display the plug-in window for that plug-in instance (in which case, it is not required to call its constructor either, because it was already constructed the first time you used it, and not destructed when hiding).

The only requirement I am aware of, is that the DAW must make sure that the editor window doesn’t outlive the plug-in instance.

Brought to the extreme (maybe there are DAWs out there that work that way), the DAW may even keep your plug-in processor alive when you remove it from a track, and for example destroy it later on with a “garbage collection” mechanism.

In a similar situation, the plug-in processor and its editor may remain in memory far longer than their presence in the DAW UI might suggest, but still that would be conformant, as long as everything is cleaned up by the time the DAW is quitted.

1 Like

Thank you for your feedback! This is good to know and your claims match what I’ve heard from other JUCE devs. It is probably best that I make sure my code works even in the scenario where the DAW chooses not to destroy the editor when the window is closed.

Regardless, I think it is very peculiar that windowing methods orderFront and makeKeyAndOrderFront can cause this behavior consistently in Logic Pro. This may just be the way that Logic works, but I am curios if Logic actually does not want us to be calling these methods.

The two forum threads linked below do indicate that Logic does not want AUs to use multiple windows. I wonder if the no-destruction behavior I am observing in Logic is a response to code that deals with multiple AU windows.

“Logic does not want” is a strange statement. The tinting for example is a security feature, which was designed by the macOS Team and implemented inside macOS. Logic has nothing to do with it and can’t do anything about it. This feature exists to prohibit malicious code from overlaying windows of other applications to potentially steal inputs (e.h. passwords).

The multi-window issue: I am not sure about the technical reason for it, but under Apple Silicon all AudioUnits are no longer part of the Logic process (for security, privacy and also stability reasons) and mapped into the plugin window of Logic by “macOS magic”, similar to the file selector having access to the whole file system, even if the application presenting it does not. It might be a bug or limitation of this process behavior or it might even be a subtle bug in the AudioUnit, which triggers macOS to keep a reference to the window around, which results in it not being destroyed when closed. I simply do not know.

3 Likes

Thank you very much for your feedback! It is great to hear from you directly on this.

It is good to know there is not a known rule we are violating by using windowing methods orderFront and makeKeyAndOrderFront. Also, thank you for the extra info on the magenta tinting. Good to know it is a macOS wide feature.

Based on what I’ve heard in this thread, as JUCE plugin developers, we should expect the scenario where a DAW chooses not to destroy a PluginEditor even when the window is closed. This behavior can be tested for in Logic (10.7.7 on M1) by opening/closing a Popup menu then reopening the plugin editor (in JUCE 7.0.5).

If your plugin is using custom OpenGL components, you may see visual glitches in this scenario (e.g. all custom OpenGL components are black). When a non-destructed plugin window is made visible again, the PluginEditor does not get constructed again, yet the OS may decide to provide a new OpenGLContext to be used on reopen. If you trigger the initialization of custom OpenGL programs from the constructor of your PluginEditor, then the OpenGL initialization would not happen in this scenario resulting in black components. Make sure all OpenGL initialization is triggered via OpenGLRenderer::newOpenGLContextCreated as this method gets called in this scenario even when the constructor is not called.