Question about WaitableEvents and OpenGL thread calling

Hey yall,

I’m pretty hard stuck on this problem and have made very little progress. Unfortunately its a little complicated so I’ll do my best to keep it brief.

I have a multi renderer OpenGL setup. Everything was working fine, but then I added a desktop popup which also utilizes OpenGL, however this popup has to spawn its own context. For some reason with the second context, I just keep hitting deadlocks.

Heres an example of one deadlock happening.

GLShaderRenderer::~GLShaderRenderer()
{
    clearDropShadowItems();
    getRenderer()->removeRenderingTarget(this);
    
    if (!getRenderer()->isMainWindowClosing() && !doNotWaitToDelete)
        canDeleteEvent.wait(-1);
}

This is the destructor for my shader renderer class. The getRenderer function just gets the GLRenderBase for that ShaderRenderer and then calls remove target. This is what remove target looks like

void GLRenderBase::removeRenderingTarget(GLShaderRenderer* targetToRemove)
{
    if (!renderingTargets.contains(targetToRemove))
    {
        targetToRemove->signalDeletionSafe();
        return;
    }
    
    if (!openGLContext->isAttached())
    {
        targetToRemove->signalDeletionSafe();
        juce::ScopedLock scopedLock(renderingTargetsLock);
        auto result = renderingTargets.removeFirstMatchingValue(targetToRemove);
    }

    WeakReference<GLShaderRenderer> weakRef = targetToRemove;
    if (openGLContext->isAttached())
    {
        openGLContext->executeOnGLThread([weakRef](OpenGLContext& callerContext)
            {
                if (weakRef.get())
                {
                    weakRef->openGLContextClosing();
                    weakRef->signalDeletionSafe();
                }
            }, false);
    }      

    juce::ScopedLock scopedLock(renderingTargetsLock);
    auto result = renderingTargets.removeFirstMatchingValue(targetToRemove);
}

The deadlock is happening because the waitableEvent never gets signaled, because the lambda never executes, because the GLThread seems to be also blocked.

This is where I need help as I think it requires some deep juce understanding. Does the MessageThread trigger the GLThread working or something? Because I don’t understand why it would be blocked otherwise. I also don’t understand why this works with 1 context, but blocks with 2. If the MessageThread does block the GLThread, how should I go about handling this?

Any help or insight appreciated. If more context is needed let me know.

okay so after a little more digging it seems in juce::OpenGLContext::renderFrame

    {
       if (! isFlagSet (state, StateFlags::initialised))
       {
            switch (initialiseOnThread())
            {
                case InitResult::fatal:
                case InitResult::retry: return RenderStatus::noWork;
                case InitResult::success: break;
            }
        }

        state |= StateFlags::initialised;

       #if JUCE_IOS
        if (backgroundProcessCheck.isBackgroundProcess())
            return RenderStatus::noWork;
       #endif

        std::optional<MessageManager::Lock::ScopedTryLockType> scopedLock;
        ScopedContextActivator contextActivator;

        const auto stateToUse = state.fetch_and (StateFlags::persistent);

       #if JUCE_MAC
        // On macOS, we use a display link callback to trigger repaints, rather than
        // letting them run at full throttle
        const auto noAutomaticRepaint = true;
       #else
        const auto noAutomaticRepaint = ! context.continuousRepaint;
       #endif

        if (! isFlagSet (stateToUse, StateFlags::pendingRender) && noAutomaticRepaint)
            return RenderStatus::noWork;

        const auto isUpdating = isFlagSet (stateToUse, StateFlags::paintComponents);

        if (context.renderComponents && isUpdating)
        {
            bool abortScope = false;
            // If we early-exit here, we need to restore these flags so that the render is
            // attempted again in the next time slice.
            const ScopeGuard scope { [&] { if (! abortScope) state |= stateToUse; } };

            // This avoids hogging the message thread when doing intensive rendering.
            std::this_thread::sleep_until (lastMMLockReleaseTime + std::chrono::milliseconds { 2 });

            if (renderThread->isListChanging())
                return RenderStatus::messageThreadAborted;

            doWorkWhileWaitingForLock (contextActivator);

            scopedLock.emplace (mmLock);

            // If we can't get the lock here, it's probably because a context has been removed
            // on the main thread.
            // We return, just in case this renderer needs to be removed from the rendering thread.
            // If another renderer is being removed instead, then we should be able to get the lock
            // next time round.
            if (! scopedLock->isLocked())
                return RenderStatus::messageThreadAborted;

            abortScope = true;
        }

        if (! contextActivator.activate (context))
            return RenderStatus::noWork;

        {
            NativeContext::Locker locker (*nativeContext);

            JUCE_CHECK_OPENGL_ERROR

            doWorkWhileWaitingForLock (contextActivator);

            const auto currentAreaAndScale = areaAndScale.get();
            const auto viewportArea = currentAreaAndScale.area;

            if (context.renderer != nullptr)
            {
                glViewport (0, 0, viewportArea.getWidth(), viewportArea.getHeight());
                context.currentRenderScale = currentAreaAndScale.scale;
                context.renderer->renderOpenGL();
                clearGLError();

                bindVertexArray();
            }

            if (context.renderComponents)
            {
                if (isUpdating)
                {
                    paintComponent (currentAreaAndScale);

                    if (! isFlagSet (state, StateFlags::initialised))
                        return RenderStatus::noWork;

                    scopedLock.reset();
                    lastMMLockReleaseTime = std::chrono::steady_clock::now();
                }

                glViewport (0, 0, viewportArea.getWidth(), viewportArea.getHeight());
                drawComponentBuffer();
            }
        }

        nativeContext->swapBuffers();
        return RenderStatus::nominal;
    }

I’m hitting

            // If we can't get the lock here, it's probably because a context has been removed
            // on the main thread.
            // We return, just in case this renderer needs to be removed from the rendering thread.
            // If another renderer is being removed instead, then we should be able to get the lock
            // next time round.
            if (! scopedLock->isLocked())
                return RenderStatus::messageThreadAborted;

The hint would suggest a context has been removed, however I don’t believe thats the case here. I assume because this is aborting this would be why the lambda isn’t executing. I’m not sure why the lock can’t acquire though

okay another update,

I switched my paint method to automatic repainting, which solves the deadlock for some reason, but theres still some serious bog down on certain things with the 2 contexts open.
Luckily this issue is an edge case, and atleast no longer a critical error, but still not sure why this is happening

1 Like