OpenGL Bug: Intermittently Dimmed Component when Visibility Enabled (macOS/iOS only)

Hi there,

It seems there’s some cache/optimization policy that will resort to outputting opengl rendering at half alpha/brightness when a component’s visibility is enabled under moderate CPU stress.

Anyone experienced with juce/opengl have any similar experiences with some advice for fixes or workaround?

Is this an issue related to a (older) plugin host’s opengl handling?

Thanks for your help.

Details

Workaround Attempts

  • Enabling setMouseRepaintsOnActivity successfully undims the dimmed component on mouse enter, but also wrongly dims an undimmed component.
  • Forcing manual repaints both on component and opengl as a response to visibility changes doesn’t appear to fix the issue.
  • Briefly showing/hiding an overlapping dummy black component as a response to visibility changes doesn’t appear to fix the issue.

Background

The app consists of components that display a simple 2D fragment shader that are shown/hidden dynamically.

  • Fragment shaders that ouput shapes are rendered without any transform/scaling, or any issues other than dimming.
  • To debug, fragment shaders have been altered output only a simple colour without any calculation (i.e. `out vec4 colour = vec4(1.,0.,1.,1.)).
  • Top level component initializes a single OpenGLContext as follows (i.e. there is no dynamic attach/detach):
    openGL_context.setOpenGLVersionRequired(OpenGLContext::OpenGLVersion::openGL3_2);
    openGL_context.setComponentPaintingEnabled(true);
    openGL_context.setContinuousRepainting(true);
    openGL_context.setRenderer(this);
    openGL_context.attachTo(*this);
  • Component::setOpaque() is false and not applicable since Component::paint() is not overidden.
  • Component::setBufferedToImage() is false.
  • Debug log outputs Component::getAlpha()=false and Component::getCachedComponentImage()=nullptr on both mouseDown() and newOpenGLContextCreated().

Bug

When an opengl shader component is shown with setVisible(true), it will sometimes be shown as significantly dimmed (unsure if it’s alpha or brightness at 50%).

  • Only observed when hosted as a plugin – both in Renoise on macOS (VST3) and AUM on iOS (AUv3).
  • Showing and hiding an overlapping component (via manual user event) always appears to correctly undim the dimmed component.
  • Bug is induced more easily when CPU is higher than normal – is this evidence of juce opengl vs message thread sync issues?
  • The Bug appears to be relatively new, i.e. the same opengl app code originated some time in juce 7.xx whereas the current dep is up to date with juce 8.0.8.

Similar Topics

1 Like

Random idea: maybe this is related to macOS/iOS HDR mode? Maybe JUCE is telling the OS your OpenGL components are HDR content resulting in the rendering being less bright?

Whenever you play HDR videos on macOS/iOS you will notice how other SDR content on the display gets dimmed.

1 Like

Thanks for the issue report. Unfortunately this doesn’t sound like any issue that we’ve encountered previously.

The OpenGL context in JUCE works by rendering components into an intermediate framebuffer, which is later displayed on the screen. The renderer keeps track of dirty regions, and attempts to render only areas of the framebuffer that need repainting. If a custom renderer is set, then on a given frame the custom renderer will be called before the component framebuffer is blended over the top. So, I think there’s a chance that you might see this kind of behaviour if the custom renderer has left the opengl context in an unexpected state. This is just a guess, though!

Some things to check:

  • Does the problem go away if you disable component rendering (setComponentPaintingEnabled (false))? If so, then that might suggest a problem with the framebuffer content or blending mode.
  • In your OpenGL renderer, are you always drawing to every pixel of the viewport? If not, do you see the same problem if you completely fill the viewport with an opaque colour on each frame?
  • You could try using git bisect to find the JUCE commit that introduced the problem. This assumes:
    • the behaviour you’re seeing is a regression, i.e. this used to work correctly with an older JUCE version,
    • you can reproduce the faulty behaviour reliably, and
    • you’re able to rebuild your application with an arbitrary JUCE version.
1 Like

Thanks heaps! This didn’t even occur to me, so I checked on a second display which is old and not hdr compatible and was able to reproduce the bug unfortunately.

1 Like

Disabling component painting as you suggested does seem to work. Hopefully it’s not just a false side effect as a result of relieving cpu stress.

So, I have tried resetting the gl blend state at the start of every renderOpenGL() callback with no success.

Yes, each juce::Component renders a simple rectangle vertex for a fragment shader that draws the shapes/colours.

Given that there are multiple components/shaders shown/hidden in groups, I have altered one of the component’s fragment shader to ouput an opaque colour and still get the problem.

I might be misunderstanding you though, how do you suggest “filling the viewport with an opaque colour”? do you mean via juce gl extensions? Because I have not tried that yet.

Interesting, thanks. To work out whether this is related to system load, you could try re-enabling component painting but commenting out the bodies of the paint functions, and/or leaving component painting disabled but increasing processing load somehow. As a first approximation maybe you could add some sleep calls into the GL rendering callback to simulate higher load.

It sounds like you’re already doing what I’m suggesting, i.e. drawing a full-frame quad, assuming the colours produced by the fragment shader are fully opaque.

There may be some similarities between the rendering in your application, and that in OpenGLDemo2D, from the JUCE repo and DemoRunner. It would be helfpul to know whether you’re able to make that demo fail in the same way. If so, then we may be able to reproduce the issue locally, which will make it easier for us to debug.

1 Like

Confirmed bug not present in previous juce version 8.0.7.

When I thought I checked previous version before my original post I probably forget to clear my dep cache and didn’t switch properly – sorry.

Additionally, the bug is not “intermittent” and can now be reproduced consistently without applying cpu stress.

Further, CPU usage almost doubles: 8.0.7 @ ~3.5% vs 8.0.8 @ 7.0%+ but I can live without this if absolutely necessary.

Narrowed problem area to OpenGLContext::copyTexture() when called from drawComponentBuffer(). To illustrate, commenting out the following line no longer dims any opengl rendering while setComponentPainting is enabled (but of course this line is essential):

            glDrawArrays (GL_TRIANGLE_STRIP, 0, 4);

Something tells me it has to do with the depth test but playing around with this idea lead nowhere.
I get this feeling because the dimming occurs even when components are just coloured via OpenGLHelpers::clear(Colours::red) – that is, without any component graphics pipeline (vertex buffers, shader programs, etc) actively rendering.

But I’m still currently in the process of going through all the 8.0.8 changes thoroughly and thought I’d post this in case you see something I’m obviously missing.

Unfortunately, there is no similarity since the demo does a shader render within a Component::paint() call whereas my app does it through the juce::OpenGLRenderer interface. Further, the demo’s OpenGLContext does not have setComponentPainting enabled and so does not call drawComponentBuffer().
I am considering switching over to do things more like the demo but would need to explore if the performance is comparable given that I need animation (continuous repainting).

Reporting in with a workaround resolution…

I have found that this issue is affected by Component::setPaintingIsUnclipped(true) and Component::setAlpha(). Given that I need unclipped painting, I have resolved this issue for my case by just removing all usage of Component::setAlpha().

Confusingly, only a few buttons nested deep in the component tree used setAlpha() and each call it once on construction. Even so, removing these calls resolves the dimming issue displayed on opengl rendered components that do not share any component parents with those buttons (other than the top level component).

I can see both arguments why this would be either expected or unexpected behaviour, i.e. which is bugged ? 8.0.7 or 8.0.8? So I can see why a juce fix isn’t necessary here.

Anyways, I’m counting this as finished but for those curious here is a repo I made that tries to simulate the conditions in my app but it fails to cause any issues/bugs.

GitHub Repo: juce-opengl-renderer-dimming-issue

Clicking Cause Bug should cause the bug which is to dim the text “DIMMED?”, given that:

  • causeBugBtn_.setAlpha() is called at startup.
  • causeBugBtn_.onClick toggles visibility in sibling components.
  • Both component painting and continuous repainting is enabled on the opengl context.

PluginEditor.cpp

#include "PluginProcessor.h"
#include "PluginEditor.h"

#include <juce_opengl/juce_opengl.h>

//==============================================================================

#define NUM_OGL_COMPONENTS 4
#define STATE_CHANGE_MS    3000

//==============================================================================

struct OGLComponent
    : public juce::Component
    , public juce::OpenGLRenderer {
    auto genColour() const -> juce::Colour {
        auto const h = juce::Random::getSystemRandom().nextFloat();
        return juce::Colour::fromHSV(h, 1.f, 1.f, 1.f);
    }
    void paint(juce::Graphics &g) override {
        g.setColour(genColour());
        g.drawRect(getLocalBounds(), 5.f);
        g.setColour(juce::Colours::black);
        g.setFont(juce::Font{juce::FontOptions{16.f}});
        g.drawText(
            "DIMMED?", getLocalBounds(), juce::Justification::centred
        );
    }
    void newOpenGLContextCreated() override {
    }
    void renderOpenGL() override {
        auto const t = juce::Time::getMillisecondCounter();
        if (t - colourChanged_ >= STATE_CHANGE_MS) {
            colourChanged_ = t;
            fillColour_    = genColour();
        }
        juce::OpenGLHelpers::clear(fillColour_);
    }
    void openGLContextClosing() override {
    }
    juce::uint32 colourChanged_ = juce::Time::getMillisecondCounter();
    juce::Colour fillColour_    = genColour();
};

//==============================================================================

struct Canvas
    : public juce::Component
    , public juce::OpenGLRenderer {
    Canvas() {
        setPaintingIsUnclipped(true);
        setRepaintsOnMouseActivity(true);

        oglContext_.setOpenGLVersionRequired(
            juce::OpenGLContext::OpenGLVersion::openGL3_2
        );
        oglContext_.setComponentPaintingEnabled(true);
        oglContext_.setContinuousRepainting(true);
        oglContext_.setRenderer(this);
        oglContext_.attachTo(*this);

        causeBugBtn_.setPaintingIsUnclipped(true);
        causeBugBtn_.setClickingTogglesState(true);
        causeBugBtn_.setAlpha(0.5f);
        causeBugBtn_.onClick = [&] {
            for (auto &c : oglChildren_) {
                c->setVisible(juce::Random::getSystemRandom().nextBool());
            }
            repaint();
        };
        addAndMakeVisible(causeBugBtn_);

        for (auto &c : oglChildren_) {
            c = std::make_unique<OGLComponent>();
            c->setPaintingIsUnclipped(true);
            c->setRepaintsOnMouseActivity(true);
            addAndMakeVisible(*c);
        }
    }
    ~Canvas() override {
        oglContext_.detach();
        oglContext_.setRenderer(nullptr);
    }
    void resized() override {
        auto b = getLocalBounds();
        causeBugBtn_.setBounds(b.removeFromBottom(getHeight() / 4));
        auto const cw = getWidth() / static_cast<int>(oglChildren_.size());
        for (auto &c : oglChildren_) {
            c->setBounds(b.removeFromLeft(cw));
        }
    }
    void newOpenGLContextCreated() override {
        for (auto &c : oglChildren_) {
            c->newOpenGLContextCreated();
        }
    }
    void renderOpenGL() override {
        juce::OpenGLHelpers::clear(juce::Colours::black);
        auto const scale = oglContext_.getRenderingScale();

        auto const t = juce::Time::getMillisecondCounter();
        if (t - visibilityChanged_ >= STATE_CHANGE_MS) {
            visibilityChanged_ = t;
            for (auto &c : oglChildren_) {
                c->setVisible(juce::Random::getSystemRandom().nextBool());
            }
        }

        for (auto &c : oglChildren_) {
            if (! c->isVisible()) {
                continue;
            }
            auto const clip =
                c->getBounds().withY(getHeight() - c->getBottom());
            auto const sc  = clip.toFloat() * scale;
            auto const sci = sc.toNearestIntEdges();
            juce::gl::glViewport(
                sci.getX(), sci.getY(), sci.getWidth(), sci.getHeight()
            );
            juce::gl::glEnable(juce::gl::GL_SCISSOR_TEST);
            juce::gl::glScissor(
                sci.getX(), sci.getY(), sci.getWidth(), sci.getHeight()
            );
            c->renderOpenGL();
            juce::gl::glDisable(juce::gl::GL_SCISSOR_TEST);
        }
    }
    void openGLContextClosing() override {
        for (auto &c : oglChildren_) {
            c->openGLContextClosing();
        }
    }

    juce::OpenGLContext                                           oglContext_;
    std::array<std::unique_ptr<OGLComponent>, NUM_OGL_COMPONENTS> oglChildren_;
    juce::TextButton causeBugBtn_{"Cause Bug: Text should dim!"};
    juce::uint32     visibilityChanged_ = juce::Time::getMillisecondCounter();
};

//==============================================================================

AudioPluginAudioProcessorEditor::AudioPluginAudioProcessorEditor(
    AudioPluginAudioProcessor &p
)
    : AudioProcessorEditor(&p)
    , canvas_(std::make_unique<Canvas>()) {
    addAndMakeVisible(*canvas_);
    setSize(400, 300);
}

AudioPluginAudioProcessorEditor::~AudioPluginAudioProcessorEditor() {
}

//==============================================================================

void
AudioPluginAudioProcessorEditor::resized() {
    canvas_->setBounds(getLocalBounds());
}