For the sake of argument, let’s say we have the following situation. A JUCE OpenGLContext
is attached to the top-level component. Child components A and B want to do some low-level gl rendering (evaluate shaders, show spinning tea-pots, etc.) but want to use the top-level’s context for this.
So here are some of the things you need to worry about when trying to make this work:
1 . You can’t simply delegate the OpenGLRenderer
callbacks of the top-level components to both A and B. This is because both A and B expect OpenGLRenderer::newOpenGLContextCreated
/OpenGLRenderer::openGLContextClosing
on initialisation/destruction. However, the top-level’s newOpenGLContextCreated
will likely be called before A and B have even been constructed yet. There could also be more complicated setups where the construction of B happens much later: for example, if B only appears after some user interaction. Calling newOpenGLContextCreated
from B’s constructor also doesn’t work as newOpenGLContextCreated
must be called on the GL thread otherwise any gl command (like loading textures) will fail in newOpenGLContextCreated
.
To make this work your top-level component needs a very simple work queue where it keeps track whose newOpenGLContextCreated
/openGLContextClosing
it needs to call. It checks this everytime it gets a render
callback.
2 . You need to clip and transform the gl viewport when calling the render
callback of A and B
3 . Dead-locking: if component A or B is deleted then it needs to free it’s opengl resources. However, A’s destructor is called on the message thread and opengl resources can only be released on the gl thread. The naïve approach would be to set a flag in the destructor and then wait in the destructor until the render
callback is invoked again. B can then check the flag in the render callback and safely delete it’s opengl resources, setting another flag which tells the message thread that it can now unblock and continue destructing the component.
Unfortunately, this results in a dead-lock, as JUCE requires the message manager lock when rendering it’s own components via OpenGL. JUCE can’t get the lock though because you are blocking and waiting in B’s destructor.
To workaround this you can instruct JUCE to call a lambda on the gl thread without taking the message manager lock. You are not allowed to render anything in that lambda but you can safely delete/allocate resources.
Here is a class GLContextHolder
I wrote a while back which will help you with the above. It’s currently used in ROLI internal code and I think it’s quite well tested.
You need to instantiate GLContextHolder
as a member variable in your top-level component. It contains the OpenGLContext
for you as a public member so the top-level component should not have another one. The top-level component must also explicitly call detach
in it’s destructor before it deletes any children. A and B then register/deregister for OpenGLRenderer
callbacks via registerOpnGlRenderer
/unregisterOpenGlRenderer
.
class GlContextHolder
: private juce::ComponentListener,
private juce::OpenGLRenderer
{
public:
static GlContextHolder* globalContextHolder;
GlContextHolder (juce::Component& topLevelComponent)
: parent (topLevelComponent)
{
jassert (globalContextHolder == nullptr);
globalContextHolder = this;
context.setRenderer (this);
context.setContinuousRepainting (true);
context.setComponentPaintingEnabled (true);
context.attachTo (parent);
}
//==============================================================================
// The context holder MUST explicitely call detach in their destructor
void detach()
{
jassert (juce::MessageManager::getInstance()->isThisTheMessageThread());
const int n = clients.size();
for (int i = 0; i < n; ++i)
if (juce::Component* comp = clients.getReference(i).c)
comp->removeComponentListener (this);
context.detach();
context.setRenderer (nullptr);
}
//==============================================================================
// Clients MUST call unregisterOpenGlRenderer manually in their destructors!!
void registerOpenGlRenderer (juce::Component* child)
{
jassert (juce::MessageManager::getInstance()->isThisTheMessageThread());
if (dynamic_cast<juce::OpenGLRenderer*> (child) != nullptr)
{
if (findClientIndexForComponent (child) < 0)
{
clients.add (Client (child, (parent.isParentOf (child) ? Client::State::running : Client::State::suspended)));
child->addComponentListener (this);
}
}
else
jassertfalse;
}
void unregisterOpenGlRenderer (juce::Component* child)
{
jassert (juce::MessageManager::getInstance()->isThisTheMessageThread());
const int index = findClientIndexForComponent (child);
if (index >= 0)
{
Client& client = clients.getReference (index);
{
juce::ScopedLock stateChangeLock (stateChangeCriticalSection);
client.nextState = Client::State::suspended;
}
child->removeComponentListener (this);
context.executeOnGLThread ([this] (juce::OpenGLContext&)
{
checkComponents (false, false);
}, true);
client.c = nullptr;
clients.remove (index);
}
}
void setBackgroundColour (const juce::Colour c)
{
backgroundColour = c;
}
juce::OpenGLContext context;
private:
//==============================================================================
void checkComponents (bool isClosing, bool isDrawing)
{
juce::Array<juce::Component*> initClients, runningClients;
{
juce::ScopedLock arrayLock (clients.getLock());
juce::ScopedLock stateLock (stateChangeCriticalSection);
const int n = clients.size();
for (int i = 0; i < n; ++i)
{
Client& client = clients.getReference (i);
if (client.c != nullptr)
{
Client::State nextState = (isClosing ? Client::State::suspended : client.nextState);
if (client.currentState == Client::State::running && nextState == Client::State::running) runningClients.add (client.c);
else if (client.currentState == Client::State::suspended && nextState == Client::State::running) initClients .add (client.c);
else if (client.currentState == Client::State::running && nextState == Client::State::suspended)
{
dynamic_cast<juce::OpenGLRenderer*> (client.c)->openGLContextClosing();
}
client.currentState = nextState;
}
}
}
for (int i = 0; i < initClients.size(); ++i)
dynamic_cast<juce::OpenGLRenderer*> (initClients.getReference (i))->newOpenGLContextCreated();
if (runningClients.size() > 0 && isDrawing)
{
const float displayScale = static_cast<float> (context.getRenderingScale());
const juce::Rectangle<int> parentBounds = (parent.getLocalBounds().toFloat() * displayScale).getSmallestIntegerContainer();
for (int i = 0; i < runningClients.size(); ++i)
{
juce::Component* comp = runningClients.getReference (i);
juce::Rectangle<int> r = (parent.getLocalArea (comp, comp->getLocalBounds()).toFloat() * displayScale).getSmallestIntegerContainer();
glViewport ((GLint) r.getX(),
(GLint) parentBounds.getHeight() - (GLint) r.getBottom(),
(GLsizei) r.getWidth(), (GLsizei) r.getHeight());
juce::OpenGLHelpers::clear (backgroundColour);
dynamic_cast<juce::OpenGLRenderer*> (comp)->renderOpenGL();
}
}
}
//==============================================================================
void componentParentHierarchyChanged (juce::Component& component) override
{
if (Client* client = findClientForComponent (&component))
{
juce::ScopedLock stateChangeLock (stateChangeCriticalSection);
client->nextState = (parent.isParentOf (&component) && component.isVisible() ? Client::State::running : Client::State::suspended);
}
}
void componentVisibilityChanged (juce::Component& component) override
{
if (Client* client = findClientForComponent (&component))
{
juce::ScopedLock stateChangeLock (stateChangeCriticalSection);
client->nextState = (parent.isParentOf (&component) && component.isVisible() ? Client::State::running : Client::State::suspended);
}
}
void componentBeingDeleted (juce::Component& component) override
{
const int index = findClientIndexForComponent (&component);
if (index >= 0)
{
Client& client = clients.getReference (index);
// You didn't call unregister before deleting this component
jassert (client.nextState == Client::State::suspended);
client.nextState = Client::State::suspended;
component.removeComponentListener (this);
context.executeOnGLThread ([this] (juce::OpenGLContext&)
{
checkComponents (false, false);
}, true);
client.c = nullptr;
clients.remove (index);
}
}
//==============================================================================
void newOpenGLContextCreated() override
{
checkComponents (false, false);
}
void renderOpenGL() override
{
juce::OpenGLHelpers::clear (backgroundColour);
checkComponents (false, true);
}
void openGLContextClosing() override
{
checkComponents (true, false);
}
//==============================================================================
juce::Component& parent;
struct Client
{
enum class State
{
suspended,
running
};
Client (juce::Component* comp, State nextStateToUse = State::suspended)
: c (comp), currentState (State::suspended), nextState (nextStateToUse) {}
juce::Component* c = nullptr;
State currentState = State::suspended, nextState = State::suspended;
};
juce::CriticalSection stateChangeCriticalSection;
juce::Array<Client, juce::CriticalSection> clients;
//==============================================================================
int findClientIndexForComponent (juce::Component* comp) const
{
const int n = clients.size();
for (int i = 0; i < n; ++i)
if (comp == clients.getReference (i).c)
return i;
return -1;
}
Client* findClientForComponent (juce::Component* comp) const
{
const int index = findClientIndexForComponent (comp);
if (index >= 0)
return &clients.getReference (index);
return nullptr;
}
//==============================================================================
juce::Colour backgroundColour { juce::Colours::black };
};