Stuttery resizing when OpenGLContext is attached

Hi everyone,

I updated my project to the latest develop branch (commit state as of March 3, 2026) and unfortunately I’m seeing very bad stuttering / choppy behaviour when dragging the resizebar, but only when an OpenGLContext is attached.

Without the OpenGLContext everything resizes smoothly .

I’ve attached:

· a short video clearly showing the stuttering .

· a minimal example project (zipped source code) that reproduces the issue .

Thank you all for your help and time!

RsizebarOGLContextBug.zip (5.8 KB)

Additionally, I’d like to mention that in the attached minimal example the issue is somewhat difficult to reproduce consistently. However, in a more complex application with additional controls and a more elaborate UI an update/repaint is sometimes missed: after dragging the resize bar and releasing the mouse button, the view does not refresh. When this happens, the UI fails to update properly and components visually overlap. Only dragging the resize bar again triggers another update/repaint cycle, which then corrects the layout. This issue has never occurred so far in JUCE 8.0.11.

@reuk Additionally, this regression seems to have exactly started with this commit:

Direct2D: Use WM_PAINT and vblank callbacks to drive painting
Commit hash: 258203706cdfc1357effe8cc752672b793cea37d
Commit date: 17.12.2025

Has anyone from the juce team had a chance to look at the commit yet? I tried reverting just this commit, but there are dependencies on other changes. The GUI then stops rendering altogether. I also tried rolling the branch back to the point before this change, but then I see that white rectangle again, which I described in this post Native title bar causes white rectangle before window initialisation - General JUCE discussion - JUCE.

Sorry for the delayed response. Thanks for the example code, I just tested it out against the faulty commit you mentioned (Use WM_PAINT…) and also against develop.

So far I haven’t been able to reproduce the behaviour from your video, so I’m not sure what’s causing the problem you’re seeing.

However, we did recently push a fix for another issue related to sluggish Direct2D rendering:

Please could you try updating to the latest version of JUCE and let us know whether you’re still able to reproduce the issue? Thanks!

Thank you very much for your response! Unfortunately, on my Dell Precision 3581 it still looks bad in JUCE 8.0.13. On the Intel i7-13700H/Iris Xe Graphics, it seems that nothing has changed. Below, I also attached a video showing how it looks on the dedicated RTX A1000 Laptop GPU.

Intel Iris Xe Graphics:

RTX A1000 Laptop:

I’m still not able to reproduce the issue - I’ve tried on a machine with integrated Intel graphics now.

Given that you’re seeing different behaviour on different graphics cards, my guess is that the symptoms depend on the graphics driver to some extent. Are your graphics drivers up-to-date?

Is there anything else about your setup that I might need to replicate in order to trigger the issue? This might include things like screen refresh rate and scaling factor.

What are the steps you’re taking to reproduce the issue? Are you just dragging the resizer bar with the mouse as shown? How long does the issue normally take to trigger?

Are you absolutely certain that the issue started with the commit you highlighted above? Have you checked the immediately preceding commit to ensure that the problem isn’t present there?

Looking at the commit that introduced the issue, I’m not seeing any obvious culprit. We are now calling InvalidateRect in D2DRenderContext::repaint(), so I wonder whether it’s something to do with invalidating a region behind an OpenGL context. You could try commenting out that call and seeing whether it has any effect on the smoothness of the resizing. Note that this will cause other graphical issues, I’m just trying to work out whether that might be contributing to the problem.

I have the latest drivers installed from Windows Update and “Dell Command | Update”. Over the last few months, I updated them several times and did not notice any changes in the application’s behavior during resizing.

On my system, I can choose between two refresh rates: 60.02 Hz (default) and 48.02 Hz. Changing between them makes no difference in my demo application.

My resolution is 1920x1080 with scaling set to 125%. Changing both of these settings to other values also did not make any difference.

No extra steps are needed. I am dragging the resizer bar with the mouse, as shown in the video. I have not noticed any specific pattern. The issue appears randomly. Sometimes at the beginning, and sometimes only after some time.

Yes, I am absolutely sure. I also checked the immediately preceding commit. I am referring here to the bug shown in the video. As for the issue with the missing final update, I am not completely sure, but it seems related. Visually, it looks the same as in the video, except that it no longer recovers afterward.

I commented out InvalidateRect in D2DRenderContext::repaint(). It looks much better now. On Intel, only the resize bar sometimes remains in its previous position. On NVIDIA, the white background is also sometimes still visible, but overall there is a significant improvement.

Intel Iris Xe Graphics:

RTX A1000 Laptop:

I checked once again. I am 100% sure that the problem appears starting with the “Direct2D: Use WM_PAINT and vblank callbacks to drive painting” commit. In “Direct2D: Remove SwapChainThread completely …”, resizing still looks normal.

I still haven’t been able to repro the problem, but I do have a hypothesis:

Prior to Direct2D: Use WM_PAINT and vblank callbacks to drive painting, we would paint the non-OpenGL part of the window on every vblank.

After that commit, we only repaint the non-OpenGL part of the window after it receives a WM_PAINT message. WM_PAINT is sent at a lower priority than other types of message, so if the main thread is busy processing other messages, then the paint will be delayed.

I suspect that this change is delaying the repaint of the non-OpenGL window content until after the message thread is done processing messages related to resizing the GL context, which makes it look like the JUCE drawing is lagging behind the resize of the GL pane.

If that is the case, I’m not sure how best to address the issue, since this throttling behaviour is the main reason we made the switch to using WM_PAINT to trigger painting! If your paint routine is very expensive, it’s important that the system is able to process other events so that the app remains (somewhat) responsive.

Are there other approaches you could try to achieve the functionality you’re looking for? One idea would be to attach the OpenGL context to the top-level window (so that it contains the resizer bar). This way, the same GL context will be used to render the left-hand panel and the right-hand panel. You can then use glViewport() to adjust the portion of the window where the GL content will display.

I’ve adjusted your example code to show this approach. Please could you check whether this still causes any glitches?

class ColorPane final : public juce::Component
{
public:
    explicit ColorPane(juce::Colour backgroundColour)
        : bgColour(backgroundColour)
    {
        setOpaque(true);
    }

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

    juce::MouseCursor getMouseCursor() override
    {
        return juce::MouseCursor::NormalCursor;
    }

private:
    const juce::Colour bgColour;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ColorPane)
};

class OGLContextPane final : public juce::Component,
                             public juce::OpenGLRenderer
{
public:
    explicit OGLContextPane (juce::OpenGLContext& x)
        : ctx (x)
    {
        setOpaque(true);
    }

    void paint(juce::Graphics&) override {}

    void newOpenGLContextCreated() override
    {
        using namespace juce::gl;

        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

        const char* vertexShaderSource =
            "#version 150\n"
            "in vec2 position;\n"
            "void main() {\n"
            "    gl_Position = vec4(position, 0.0, 1.0);\n"
            "}\n";

        const char* fragmentShaderSource =
            "#version 150\n"
            "out vec4 fragColour;\n"
            "void main() {\n"
            "    fragColour = vec4(1.0, 0.0, 0.0, 1.0);\n"
            "}\n";

        program = std::make_unique<juce::OpenGLShaderProgram>(ctx);

        if(!program->addVertexShader(vertexShaderSource) ||
            !program->addFragmentShader(fragmentShaderSource) ||
            !program->link())
        {
            DBG("Shader link failed: " << program->getLastError());
            jassertfalse;
            return;
        }

        position = std::make_unique<juce::OpenGLShaderProgram::Attribute>(*program, "position");


        const float vertices[] = {
            -0.7f, -0.7f,
             0.7f, -0.7f,
             0.7f,  0.7f,

            -0.7f, -0.7f,
             0.7f,  0.7f,
            -0.7f,  0.7f
        };

        using namespace juce::gl;

        glGenBuffers(1, &vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    }

    void renderOpenGL() override
    {
        using namespace juce::gl;

        GLint oldViewport[4]{};
        glGetIntegerv (GL_VIEWPORT, oldViewport);
        const juce::ScopeGuard scope { [&] { glViewport (oldViewport[0], oldViewport[1], oldViewport[2], oldViewport[3]); } };

        const auto peerScale = std::invoke ([&]
        {
            if (auto* p = getPeer())
                return p->getPlatformScaleFactor();

            return 1.0;
        });

        const auto logicalArea = getTopLevelComponent()->getLocalArea (this, getLocalBounds());
        const auto physicalArea = logicalArea * getApproximateScaleFactorForComponent (getTopLevelComponent()) * peerScale;

        glViewport (physicalArea.getX(), physicalArea.getY(), physicalArea.getWidth(), physicalArea.getHeight());

        if(!juce::OpenGLHelpers::isContextActive() || program == nullptr || position == nullptr)
            return;

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        program->use();

        glBindBuffer(GL_ARRAY_BUFFER, vbo);

        glEnableVertexAttribArray(position->attributeID);
        glVertexAttribPointer(position->attributeID, 2, GL_FLOAT, GL_FALSE,
            2 * sizeof(float), nullptr);

        glDrawArrays(GL_TRIANGLES, 0, 6);

        glDisableVertexAttribArray(position->attributeID);
    }

    void openGLContextClosing() override
    {
        using namespace juce::gl;
        if(vbo != 0)
        {
            glDeleteBuffers(1, &vbo);
            vbo = 0;
        }
        position.reset();
        program.reset();
    }

    void resized() override {}

private:
    juce::OpenGLContext& ctx;
    std::unique_ptr<juce::OpenGLShaderProgram> program;
    std::unique_ptr<juce::OpenGLShaderProgram::Attribute> position;

    GLuint vbo = 0;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(OGLContextPane)
};

class MainComponent : public juce::Component
{
public:
    MainComponent()
    {
        leftPane = std::make_unique<ColorPane>(juce::Colours::seagreen);
        rightPane = std::make_unique<OGLContextPane> (ctx);

        addAndMakeVisible (*leftPane);
        addAndMakeVisible (*rightPane);

        layout.setItemLayout (0,  50, 1000, -0.6);
        layout.setItemLayout (1,   6,    6,   6);
        layout.setItemLayout (2,  50, 1000, -0.4);

        divider = std::make_unique<juce::StretchableLayoutResizerBar> (&layout, 1, true);
        addAndMakeVisible (divider.get());

        setSize (800, 500);

        ctx.setOpenGLVersionRequired(juce::OpenGLContext::openGL4_3);
        ctx.setRenderer(rightPane.get());
        ctx.attachTo(*this);
        ctx.setContinuousRepainting(true);
    }

    ~MainComponent() override
    {
        ctx.detach();
    }

    void resized() override
    {
        juce::Component* items[] = { leftPane.get(), divider.get(), rightPane.get() };

        layout.layOutComponents (items, 3, 0, 0, getWidth(), getHeight(), false, true);
    }

private:
    juce::OpenGLContext ctx;
    std::unique_ptr<ColorPane> leftPane;
    std::unique_ptr<OGLContextPane> rightPane;
    std::unique_ptr<juce::StretchableLayoutResizerBar> divider;

    juce::StretchableLayoutManager layout;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

Thank you for your code! So, does that mean mixing OpenGL and Direct2D is generally not recommended in JUCE?

At the beginning, in our actual application, we were using the OpenGL renderer (OpenGLContext was attached to the top-level component). More recently, we also added a custom WebView implementation, because the built-in JUCE one is currently limited to a single instance (see: JUCE forum discussion).

Your code fixed the issue, but unfortunately going back to the OpenGL renderer is a no-go for us. The WebView and OpenGLContext attached to the top-level component conflict with each other. At the same time, I also do not want to lose the improved responsiveness that Direct2D provides.

Are there any other possible approaches that could be considered?

I would also like to point out that the issue occurs on a 2024 MacBook Pro as well.

Might it have something to do with this?

It seems that there are circumstances where there is no proper update synch happening between the main thread paint code and what is going on on the OpenGL thread…

It depends on the use-case. Situations where OpenGL is used for a meter/display of a fixed size should work fine. The problem is syncing the JUCE painting with the changing size of the OpenGL context.

I can’t think of any solutions that allow to continuously resize the OpenGL context while keeping the JUCE painting in-sync. I know this used to work on Windows, so I suppose an option on the peer to temporarily force painting on vblank might help (you could set this option when starting a resize and turn it off again afterwards). But, it sounds like you’re also seeing problems in macOS, and those issues will have a different cause and I’m not sure whether there’s a similar workaround on that platform. I remember investigating that issue previously and failing to make any inroads.

Given that the current design of the program is accentuating the continuous-resize issues on multiple platforms, perhaps you could consider adjusting the design a bit. One option might be to adjust your program so that the sidebar has a few preset sizes with an expand/collapse toggle to switch between them. Another option would be to disable the OpenGL view during the resize and reenable it afterwards.