setOpaque and OpenGL JUCE 8

After i switched from JUCE 7 to 8 I noticed a different behaviour with OpenGL and opaque components.

In my plugin I attach the GL-context to the PluginEditor, and also add a renderer, to draw multiple OpenGL enhanced controls. I add a full-screen background component and then apply a “hack” so that individual areas rendered by renderOpenGL() shine through. I use components where setOpaque(true) is set (but actually nothing in the paint method), so that the OpenGL-enhanced meters can be seen.

This worked great with JUCE 7, but with JUCE 8, it seems these areas also (sometimes) rendered behind opaque components. That’s not a problem; I can simply make sure that nothing is drawn in these areas myself.

I’m just wondering whether this new behaviour is intentional or whether it’s an unintended bug in JUCE 8 (component repaint behind opaque components).

There have been changes in this area but that does seem surprising in fact I would expect setOpaque to prevent more components from being rendered than older versions of JUCE. Could you possibly share a simple example please?

Sure: Platfrom Windows 11, HighDPI

class OpaqueComponent : public juce::Component
{
public:
    OpaqueComponent (juce::Colour c_) : c (c_) { setOpaque (true); }

    void paint (juce::Graphics& g) { g.fillAll (c); };

    juce::Colour c;
};

class MainComponent : public juce::Component, public juce::OpenGLRenderer
{
public:
    OpaqueComponent background{ juce::Colours::green };
    OpaqueComponent filledOpaque{ juce::Colours::yellow };
    OpaqueComponent transparentOpaque{ juce::Colours::transparentWhite };

    MainComponent()
    {
        setSize (600, 400);
        glContext.setRenderer (this);
        glContext.setContinuousRepainting (true);
        glContext.attachTo (*this);

        addAndMakeVisible (&background);
        background.addAndMakeVisible (&filledOpaque);
        background.addAndMakeVisible (&transparentOpaque);
    }

    ~MainComponent() override {}

    //==============================================================================
    void paint (juce::Graphics&) override {}
    void resized() override
    {
        background.setBounds (getLocalBounds());
        filledOpaque.setBounds (100, 100, 100, 100);
        transparentOpaque.setBounds (300, 100, 100, 100);
    };

    void newOpenGLContextCreated() override {}

    void renderOpenGL() override { juce::OpenGLHelpers::clear (juce::Colours::blue); }

    void openGLContextClosing() override {}

    juce::OpenGLContext glContext;

private:
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

JUCE7

JUCE8

In that example it seems to me that JUCE 7 is the one misbehaving. The background component (green) is opaque and completely covers the MainComponent (blue) so there is no need to render the paint call in the MainComponent at all. That’s why the transparent component shows green underneath. That component had to be rendered regardless. We could clip the green component in that area too but clipping has a cost attached to it (albeit small) and in most cases as the component needs painting anyway there is little to no saving to be made.

In the JUCE 7 case I think background is detecting that filledOpaque and transparentOpaque are opaque and clipping the context, but for some reason MainComponent doesn’t appear to be detecting that background is opaque? or maybe it does but because it’s the top level component it doesn’t end up clipping it’s bounds. Regardless of the reason I can’t see any case in which revealing the MainComponent is the sensible choice (unless I’m missing something?). The only other “sensible” option I see here is having a completely transparent square instead of the blue square.

Looking in more detail I see renderOpenGL() will always be called which makes sense now why you see it through the transparent opaque component in JUCE 7. Although I’m not sure it makes sense to rely on anything that renders behind an opaque component. I don’t think I would consider this to be a bug.

JUCE7 is just very strict, about not “component-repainting” the area, behind any opaque components.

I could use this as a trick, as I initially described, to cut out pieces, to show the openGL layer. It worked very consistently until JUCE8, for me it was not a bug, its was a feature, especially when you use multiple different OpenGL enhanced controls, but only one openGL context.

(Because there is no easy way to use openGL above components, only behind)

For the non-GL fallback mode, I could use the same opaque place-holder components, to render the controls natively, which was a nice comfort.

JUCE8 repaints the complete area when it’s partially covered by opaque components (instead the non covered area), this is the difference.

JUCE 7 also repaints the complete area when it’s partially covered by opaque components, it’s just that before it does so it clips all regions in the context covered by those opaque components, so it won’t be displayed (but the work is done).

To the best of my knowledge this was never meant to be a “feature” it was just an implementation detail. The idea was that it would clip all the regions covered by opaque components, if as a result of that, the clip region became empty, it could skip painting the component, this was the feature.

In other words clipping the component is normally only a time saver if it results in the context being entirely clipped such that the paint call can be avoided altogether.

Looking at the JUCE 7 docs it said this…

Components that always paint all of their contents with solid colour and
thus completely cover any components behind them should use this method
to tell the repaint system that they are opaque.

This information is used to optimise drawing, because it means that
objects underneath opaque windows don’t need to be painted.

There’s no mention of clipping other components.

Regardless I tried reintroducing clipping and at least in my tests it is noticeably more expensive. For example in the software renderer I have tests that increase a single paint call by as much as 27ms on a modern MacBook Pro!

Given that we’re talking about an un-documented observable side effect, I’m not sure I could justify re-introducing the clipping behaviour you’re relying on in JUCE 7. It seems it wouldn’t be worth it for all the customers who are using opaque components as documented.

What you are asking for sounds like a different feature altogether. I have some ideas of how we could offer this if we ever were to add support for it (for that it would help to have a clearer picture of when this is useful).

However, for now how about something like this? It should clip the context much like JUCE 7 does without the need to rely on any undocumented behaviour.

class PortalComponent : public juce::Component {};

class PortalRenderer
{
public:
    PortalRenderer (juce::Component& c)
        : attachedComponent (&c)
    {
        attachedComponent->setCachedComponentImage (std::make_unique<Impl> (this).release());
    }

    ~PortalRenderer()
    {
        attachedComponent->setCachedComponentImage (nullptr);
    }

private:
    class Impl : public juce::CachedComponentImage
    {
    public:
        Impl (PortalRenderer* r) : renderer (r) {}

        void paint (juce::Graphics& g) final
        {
            openPortals (*renderer->attachedComponent, g);
            renderer->attachedComponent->paintEntireComponent (g, false);
        }

        bool invalidateAll() final { return true; }
        bool invalidate (const juce::Rectangle<int>&) final { return true; }
        void releaseResources() final {}

    private:
        static void openPortals (const juce::Component& component, juce::Graphics& g)
        {
            for (const auto* child : component.getChildren())
            {
                if (dynamic_cast<const PortalComponent*> (child))
                    g.excludeClipRegion (component.getLocalArea (child, child->getLocalBounds()));

                openPortals (*child, g);
            }
        }

        PortalRenderer* renderer;
    };

    juce::Component* attachedComponent;
};

class ColourComponent : public juce::Component
{
public:
    ColourComponent (juce::Colour c) : colour (c) {}
    void paint (juce::Graphics& g) { g.fillAll (colour); };

private:
    juce::Colour colour;
};

class MainComponent : public juce::Component, public juce::OpenGLRenderer
{
public:
    ColourComponent background { juce::Colours::green };
    PortalRenderer portalRenderer { background };
    ColourComponent filled { juce::Colours::yellow };
    PortalComponent portal;

    MainComponent()
    {
        setSize (600, 400);
        glContext.setRenderer (this);
        glContext.setContinuousRepainting (true);
        glContext.attachTo (*this);

        addAndMakeVisible (&background);
        background.addAndMakeVisible (&filled);
        background.addAndMakeVisible (&portal);
    }

    ~MainComponent() override {}

    //==============================================================================
    void paint (juce::Graphics& g) override
    {
        g.fillAll (juce::Colours::blue);
    }

    void resized() override
    {
        background.setBounds (getLocalBounds());
        filled.setBounds (100, 100, 100, 100);
        portal.setBounds (300, 100, 100, 100);
    };

    void newOpenGLContextCreated() override {}

    void renderOpenGL() override { juce::OpenGLHelpers::clear (juce::Colours::blue); }

    void openGLContextClosing() override {}

    juce::OpenGLContext glContext;

private:
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

Thank you very much for your efforts. I will definitely look into it. :grinning_face:

After posting I noticed an issue with the implementation, the previous version would cut a portal even when components were in front of it. This version should allow components to render in front of a portal component.

class PortalRenderer
{
public:
    PortalRenderer (juce::Component& c)
        : attachedComponent (&c)
    {
        attachedComponent->setCachedComponentImage (std::make_unique<Impl> (this).release());
    }

    ~PortalRenderer()
    {
        attachedComponent->setCachedComponentImage (nullptr);
    }

private:
    class Impl : public juce::CachedComponentImage
    {
    public:
        Impl (PortalRenderer* r) : renderer (r) {}

        void paint (juce::Graphics& g) final
        {
            juce::RectangleList<int> regions;
            regions.add (getChildPortalRegions (*renderer->attachedComponent));
            regions.consolidate();

            for (const auto& region : regions)
                g.excludeClipRegion (region);

            renderer->attachedComponent->paintEntireComponent (g, false);
        }

        bool invalidateAll() final { return true; }
        bool invalidate (const juce::Rectangle<int>&) final { return true; }
        void releaseResources() final {}

    private:
        auto getRelativeComponentBounds (const juce::Component& component)
        {
            return renderer->attachedComponent->getLocalArea (&component, component.getLocalBounds());
        }

        juce::RectangleList<int> getChildPortalRegions (const juce::Component& component)
        {
            juce::RectangleList<int> regions;

            for (const auto* child : component.getChildren())
            {
                if (dynamic_cast<const PortalComponent*> (child))
                    regions.addWithoutMerging (getRelativeComponentBounds (*child));
                else
                    regions.subtract (getRelativeComponentBounds (*child));

                regions.add (getChildPortalRegions (*child));
            }

            return regions;
        }

        PortalRenderer* renderer;
    };

    juce::Component* attachedComponent;
};
1 Like