openGL Component Crash on Shutdown

I’m creating component that uses openGL for display rather than paint because I need to efficiently draw a large number of rectangles which will be the note indicators on a sort of piano roll, a class called “ScrollingNoteViewer”.

As a learning exercise I created a simplified version of openGLAppExample that uses vertex buffers and shaders to draw a few rectangle of different colours, and this works fine. I’m now trying to port this to be not a standalone app, but a component I can use in the plugin I’m developing.

I changed my non openGL component class as follows:

class ScrollingNoteViewer: public Component, private OpenGLContext, private OpenGLRenderer, private Timer

…and ported my openGL code to it. It displays the rectangles fine in my VST but crashes when I close the plugin’s window. I’m probably missing some step in shutting down the openGL context.

The assert is triggered by drawComponentBuffer() below, which is part of juce_OpenGLContext.cpp :

    if (context.renderComponents)
    {
        if (isUpdating)
        {
            paintComponent();
            if (! hasInitialised)
                return false;
            mmLock = nullptr;
            lastMMLockReleaseTime = Time::getMillisecondCounter();
        }
        glViewport (0, 0, viewportArea.getWidth(), viewportArea.getHeight());
        drawComponentBuffer();  //This triggers an assert on shutdown
    }
    context.swapBuffers();
    OpenGLContext::deactivateCurrentContext();

You can see that on shutdown if (context.renderComponents) is still true which is probably not correct.

What do I need to do in my component to tell the renderer it’s closing?

My complete code for the header and implementation of ScrollingNoteViewer is below.

#ifndef __JUCE_HEADER_B11C4CC4490D982E__
#define __JUCE_HEADER_B11C4CC4490D982E__

//[Headers]     -- You can add your own extra header files here --
#include "../JuceLibraryCode/JuceHeader.h"
#include "PluginProcessor.h"
//[/Headers]

struct Vertex  // class storing the information about a single vertex
{
    float position[2];
    float colour[3];
};

class ScrollingNoteViewer  :    public Component, private OpenGLContext, private OpenGLRenderer, private Timer
{
public:
    
    ScrollingNoteViewer (ReExpressorAudioProcessor*);
    ~ScrollingNoteViewer();
    
    OpenGLContext openGLContext;    
    virtual void newOpenGLContextCreated() override;
    virtual void renderOpenGL() override;
    virtual void openGLContextClosing() override;
    
    Matrix3D<float> getProjectionMatrix() const;
    Matrix3D<float> getViewMatrix() const;
    void setRectangleColour (int rect, float r, float g, float b);
    void addRectangle(Array<Vertex> &verts, float x, float y, float w, float h, float red);
    void createShaders();
    
    void paint (Graphics& g) override;
    void resized() override;
    String getNoteText (const int midiNoteNumber);
    
    enum ColourIds
    {
        whiteNoteColourId               = 0x1005000,
        blackNoteColourId               = 0x1005001,
        keySeparatorLineColourId        = 0x1005002,
        mouseOverKeyOverlayColourId     = 0x1005003,  /**< Will be overlaid on the normal note colour.*/
        keyDownOverlayColourId          = 0x1005004,  /**< Will be overlaid on the normal note colour.*/
        textLabelColourId               = 0x1005005,
        upDownButtonBackgroundColourId  = 0x1005006,
        upDownButtonArrowColourId       = 0x1005007,
        shadowColourId                  = 0x1005008
    };
    
    // This class manages the uniform values that the demo shaders use.
    struct Uniforms
    {
        Uniforms (OpenGLContext& openGLContext, OpenGLShaderProgram& shaderProgram)
        {
            projectionMatrix = createUniform (openGLContext, shaderProgram, "projectionMatrix");
            viewMatrix       = createUniform (openGLContext, shaderProgram, "viewMatrix");
        }
        
        ScopedPointer<OpenGLShaderProgram::Uniform> projectionMatrix, viewMatrix;
        
    private:
        static OpenGLShaderProgram::Uniform* createUniform (OpenGLContext& openGLContext,
                                                            OpenGLShaderProgram& shaderProgram,
                                                            const char* uniformName)
        {
            if (openGLContext.extensions.glGetUniformLocation (shaderProgram.getProgramID(), uniformName) < 0)
                return nullptr;
            
            return new OpenGLShaderProgram::Uniform (shaderProgram, uniformName);
        }
    };
    
private:
    ReExpressorAudioProcessor *processor;
    double timeInTicks;
    double startTime;
    double timeRange;
    double prevTimeInTicks = -1.0;
    float wKbd;
    int maxNote;
    int minNote;
    bool compressNotes;
    float lineWidthRatio; //As fraction of key width
    int octaveNumForMiddleC;
    int beatsPerBar = 4;
    int beatsHistory = 4;
    int beatsInDisplay = 20;
    int upperBorderThickness;
    int leftMargin;
    
    int frameCounter = 0;
    GLuint vertexBuffer, indexBuffer;
    int numIndices;
    float delta = 0.002;
    float x;
    float y;
    float pos = 0;
    float desktopScale;
    
    Array<Vertex> vertices;
    const char* vertexShader;
    const char* fragmentShader;
    
    ScopedPointer<OpenGLShaderProgram> shader;
    ScopedPointer<OpenGLShaderProgram::Attribute> position, sourceColour;
    ScopedPointer<Uniforms> uniforms;
    
    String newVertexShader, newFragmentShader;
    
    void timerCallback() override {
        x += delta;
        if (x >= 1.0)
            delta = - 0.002;
        else if (x<=0.0)
            delta = 0.001;
    }
    
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ScrollingNoteViewer)
};

//[EndFile] You can add extra defines here...
//[/EndFile]

#endif   // __JUCE_HEADER_B11C4CC4490D982E__


#include "ScrollingNoteViewer.h"
#include <array>

//[MiscUserDefs] You can add your own user definitions and misc code here...
//[/MiscUserDefs]

//==============================================================================
ScrollingNoteViewer::ScrollingNoteViewer (ReExpressorAudioProcessor* p) :
processor(p),
wKbd(24.f),
maxNote(84),
minNote(59),
compressNotes(false),
lineWidthRatio(0.8f), //As fraction of key width
octaveNumForMiddleC (3),
upperBorderThickness(14),
leftMargin(24)
{
    setPaintingIsUnclipped(true);
    //setOpaque (true);
    startTimerHz(1000);
    x = 0;
    y = 0;
    position = nullptr;
    sourceColour = nullptr;
    openGLContext.setRenderer (this);
    openGLContext.attachTo (*this);
    openGLContext.setContinuousRepainting (true);
}

ScrollingNoteViewer::~ScrollingNoteViewer()
{
}

void ScrollingNoteViewer::addRectangle(Array<Vertex> &verts, float x, float y, float w, float h, float red)
{
    Vertex v0 =
    {
        { x, y-h},
        { red, 0.0f, 0.0f}
    };
    verts.add(v0);
    Vertex v1 =
    {
        { x, y},
        { red, 0.0f, 0.0f}
    };
    verts.add(v1);
    Vertex v2 =
    {
        { x+w, y-h},
        { red, 0.0f, 0.0f}
    };
    verts.add (v2);
    Vertex v3 =
    {
        { x+w, y},
        { red, 0.0f, 0.0f}
    };
    verts.add (v3);
}

Matrix3D<float> ScrollingNoteViewer::getProjectionMatrix() const
{
    float w = 1.0f / (0.5f + 0.1f);
    float h = w * getLocalBounds().toFloat().getAspectRatio (false);
    return Matrix3D<float>::fromFrustum (-w, w, -h, h, 4.0f, 30.0f);
}

Matrix3D<float> ScrollingNoteViewer::getViewMatrix() const
{
    Matrix3D<float> viewMatrix (Vector3D<float> (-.3f, 0.0f, -4.0f));
    Matrix3D<float> rotationMatrix = viewMatrix.rotated (Vector3D<float> (0.f, 0.0f, 0.0f));
    return rotationMatrix * viewMatrix;
}

void ScrollingNoteViewer::setRectangleColour (int rect, float r, float g, float b)
{
    float colour[3] = {r,g,b};
    openGLContext.extensions.glBindBuffer (GL_ARRAY_BUFFER, vertexBuffer);
    glBufferSubData(GL_ARRAY_BUFFER,
                    sizeof(Vertex)*(4*rect) + 8,
                    sizeof(colour),
                    colour);
    glBufferSubData(GL_ARRAY_BUFFER,
                    sizeof(Vertex)*(4*rect+1) + 8,
                    sizeof(colour),
                    colour);
    glBufferSubData(GL_ARRAY_BUFFER,
                    sizeof(Vertex)*(4*rect+2) + 8,
                    sizeof(colour),
                    colour);
    glBufferSubData(GL_ARRAY_BUFFER,
                    sizeof(Vertex)*(4*rect+3) + 8,
                    sizeof(colour),
                    colour);
    openGLContext.extensions.glBindBuffer (GL_ARRAY_BUFFER, 0);
}

//initialise
void ScrollingNoteViewer::newOpenGLContextCreated()
{
    createShaders();
    
    Array<Vertex> vertices;
    ScrollingNoteViewer::addRectangle(vertices, -1.0f, 0.5f, 0.3f, 0.5f,0.5f);
    ScrollingNoteViewer::addRectangle(vertices, 0.5f, 0.5f, 1.0f, 0.5f,0.2f);
    ScrollingNoteViewer::addRectangle(vertices, 0.0f, -0.8f, 1.0f, 0.2f,0.7f);
    
    openGLContext.extensions.glGenBuffers (1, &vertexBuffer);
    openGLContext.extensions.glBindBuffer (GL_ARRAY_BUFFER, vertexBuffer);
    openGLContext.extensions.glBufferData (GL_ARRAY_BUFFER,
                                           static_cast<GLsizeiptr> (static_cast<size_t> (vertices.size()) * sizeof (Vertex)),
                                           vertices.getRawDataPointer(), GL_DYNAMIC_DRAW);
    Array<int> indices;
    for (int i=0;i<vertices.size()/4;i++)
    {
        indices.add(0+4*i);
        indices.add(1+4*i);
        indices.add(2+4*i);
        indices.add(1+4*i);
        indices.add(2+4*i);
        indices.add(3+4*i);
    }
    
    numIndices = 6*(vertices.size()/4);
    //generate buffer object name(s) (names are ints) (indexBuffer is an GLuint)
    openGLContext.extensions.glGenBuffers (1, &indexBuffer); //Gets id of indexBuffer
    
    //bind a named buffer object (to a buffer type such as GL_ELEMENT_ARRAY_BUFFER)
    openGLContext.extensions.glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
    openGLContext.extensions.glBufferData (GL_ELEMENT_ARRAY_BUFFER,
                                           static_cast<GLsizeiptr> (static_cast<size_t> (numIndices) * sizeof (juce::uint32)),
                                           indices.getRawDataPointer(), GL_STATIC_DRAW);
    ////Projection & View Matrices
    if (uniforms->projectionMatrix != nullptr)
        uniforms->projectionMatrix->setMatrix4 (getProjectionMatrix().mat, 1, false);
    
    if (uniforms->viewMatrix != nullptr)
        uniforms->viewMatrix->setMatrix4 (getViewMatrix().mat, 1, false);
}

//render
void ScrollingNoteViewer::renderOpenGL()
{
    ++frameCounter;
    jassert (OpenGLHelpers::isContextActive());
    
    desktopScale = (float) openGLContext.getRenderingScale();
    OpenGLHelpers::clear (Colour::greyLevel (0.1f));
    
    glEnable (GL_BLEND);
    glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glViewport (0, 0, roundToInt (desktopScale * getWidth()), roundToInt (desktopScale * getHeight()));
    
    shader->use();
    
    setRectangleColour(0, 1.0f, 1.0f, (x+1.0f)/2.0f);
    setRectangleColour(1, 1.0f, (x+1.0f)/2.0f, 1.0f);
    setRectangleColour(2, 1.0f, (x+1.0f)/2.0f, 0.5f);
    
    openGLContext.extensions.glBindBuffer (GL_ARRAY_BUFFER, vertexBuffer);
    openGLContext.extensions.glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, indexBuffer);

    if (position != nullptr)
    {
        openGLContext.extensions.glVertexAttribPointer (position->attributeID, 2, GL_FLOAT, GL_FALSE, sizeof (Vertex), 0);
        openGLContext.extensions.glEnableVertexAttribArray (position->attributeID);
    }
    
    if (sourceColour != nullptr)
    {
        openGLContext.extensions.glVertexAttribPointer (sourceColour->attributeID, 3, GL_FLOAT, GL_FALSE, sizeof (Vertex), (GLvoid*) (sizeof (float) * 2));
        openGLContext.extensions.glEnableVertexAttribArray (sourceColour->attributeID);
    }
    
    glDrawElements (GL_TRIANGLES, numIndices, GL_UNSIGNED_INT, 0);
    
    if (position != nullptr)
        openGLContext.extensions.glDisableVertexAttribArray (position->attributeID);
    if (sourceColour != nullptr)
        openGLContext.extensions.glDisableVertexAttribArray (sourceColour->attributeID);
    
    openGLContext.extensions.glBindBuffer (GL_ARRAY_BUFFER, 0);
    openGLContext.extensions.glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, 0);
}

//shutdown openGL
void ScrollingNoteViewer::openGLContextClosing()
{
    shader = nullptr;
    uniforms = nullptr;
    openGLContext.extensions.glDeleteBuffers (1, &vertexBuffer);
    openGLContext.extensions.glDeleteBuffers (1, &indexBuffer);
}

void ScrollingNoteViewer::createShaders()
{
    vertexShader =
    "attribute vec4 position;\n"
    "attribute vec3 sourceColour;\n"
    "\n"
    "uniform mat4 projectionMatrix;\n"
    "uniform mat4 viewMatrix;\n"
    "\n"
    "varying vec3 destinationColour;\n"
    "\n"
    "void main()\n"
    "{\n"
    "    destinationColour = sourceColour;\n"
    "    gl_Position = projectionMatrix * viewMatrix * position;\n"
    "}\n";
    
    fragmentShader =
#if JUCE_OPENGL_ES
    "varying lowp vec3 destinationColour;\n"
#else
    "varying vec3 destinationColour;\n"
#endif
    "\n"
    "void main()\n"
    "{\n"
    "    gl_FragColor = vec4(destinationColour,1.0); \n"
    "}\n";
    
    ScopedPointer<OpenGLShaderProgram> newShader (new OpenGLShaderProgram (openGLContext));
    String statusText;
    
    if (newShader->addVertexShader (OpenGLHelpers::translateVertexShaderToV3 (vertexShader))
        && newShader->addFragmentShader (OpenGLHelpers::translateFragmentShaderToV3 (fragmentShader))
        && newShader->link())
    {
        shader = newShader;
        uniforms = nullptr;
        shader->use();
        
        if (openGLContext.extensions.glGetAttribLocation (shader->getProgramID(), "position") < 0)
            position      = nullptr;
        else
            position      = new OpenGLShaderProgram::Attribute (*shader,    "position");
        
        if (openGLContext.extensions.glGetAttribLocation (shader->getProgramID(), "sourceColour") < 0)
            sourceColour      = nullptr;
        else
            sourceColour  = new OpenGLShaderProgram::Attribute (*shader,    "sourceColour");
        
        uniforms   = new Uniforms (openGLContext, *shader);
        std::cout << "GLSL: v" + String (OpenGLShaderProgram::getLanguageVersion(), 2) + " Shaders created" << "\n";
    }
    else
    {
        std::cout <<  newShader->getLastError()<<"\n";
    }
}

//==============================================================================
void ScrollingNoteViewer::paint (Graphics& g)
{

}

void ScrollingNoteViewer::resized()
{
}

String ScrollingNoteViewer::getNoteText (const int midiNoteNumber)
{
    return MidiMessage::getMidiNoteName (midiNoteNumber, true, true, octaveNumForMiddleC);
}

//==============================================================================
#if 0
/*  -- Projucer information section --
 
 This is where the Projucer stores the metadata that describe this GUI layout, so
 make changes in here at your peril!
 
 BEGIN_JUCER_METADATA
 
 <JUCER_COMPONENT documentType="Component" className="ScrollingNoteViewer" componentName=""
 parentClasses="public Component" constructorParams="" variableInitialisers=""
 snapPixels="8" snapActive="1" snapShown="1" overlayOpacity="0.330"
 fixedSize="0" initialWidth="600" initialHeight="400">
 <BACKGROUND backgroundColour="ffffffff"/>
 </JUCER_COMPONENT>
 
 END_JUCER_METADATA
 */
#endif
//[EndFile] You can add extra defines here...
//[/EndFile]

Here’s an update:

I changed openGLContextClosing as follows:

//shutdown openGL
void ScrollingNoteViewer::openGLContextClosing()
{    
    shader = nullptr;
    uniforms = nullptr;
    openGLContext.extensions.glDeleteBuffers (1, &vertexBuffer);
    openGLContext.extensions.glDeleteBuffers (1, &indexBuffer);
    detach();  //Added this 
    setComponentPaintingEnabled(false);  //Added this
}

Now I can open and close the vst window once with no crash, but if I open it a second time it crashes on the next close.

fyi this is a “known problem” from at least a year and a half ago: GL crashing on editor close

There is a fix for some aspects of this bug on develop. See the issue tracker here.

I just pulled the latest dev branch and rebuilt my example. Unfortunately it now crashes on startup.

It seems to continually be creating the editor in:

AudioProcessorEditor* AudioProcessor::createEditorIfNeeded()
{
    if (activeEditor != nullptr)
        return activeEditor;

    AudioProcessorEditor* const ed = createEditor();

    if (ed != nullptr)
    {
        // you must give your editor comp a size before returning it..
        jassert (ed->getWidth() > 0 && ed->getHeight() > 0);

        const ScopedLock sl (callbackLock);
        activeEditor = ed;
    }

    // You must make your hasEditor() method return a consistent result!
    jassert (hasEditor() == (ed != nullptr));

    return ed;
}

And then deleting the editor in:

void AudioProcessor::editorBeingDeleted (AudioProcessorEditor* const editor) noexcept
{
    const ScopedLock sl (callbackLock);

    if (activeEditor == editor)
        activeEditor = nullptr;
}

I’m don’t know what finally causes it to crash. The main message thread is at libobjc.A.dylibobjc_msgSend, called from libdyld.dylibstart.

I’m not sure whether this is caused by some other problem in my code interacting with a recent change in the dev branch, or that the underlying problem isn’t fixed. Note that before updating from the dev branch my code did work other than asserting on closing the window.

P.S. I’m on OS X 10.11.5.

1 Like

Yes please try pulling again. I think I might of just fixed that.

I pulled again. Now it displays my window for about a second before
crashing in the same way as before. Previously I didn’t see the window at
all.

  • Chris
1 Like

You see, we never used to call componentHierachyChanged when the daw would remove your editor from the screen. We did call it when it was added. So this was an inconsistency and fixing this also gets rid of the Opengl bug. But I think a lot of code (also at at least one point in JUCE) people were relying on the wrong behaviour. I’ll discuss this with Jules tomorrow what to do here. Sorry for the pain!!

1 Like

Actually with more testing I find that the window displays anywhere from a
very short time, up to a second or two before crashing, seemingly randomly
from one identical run to the next.

  • Chris

The only thing we’ve really changed is that we call componentHierachyChanged when someone calls removeFromDesktop to your component (or any of it’s parents). Is there any obvious place in your plug-in where you do something strange when componentHierachyChanged is invoked?

It seems like the same behavior occurs with the juce demo plugin as well

The ui of the plugin is trivial so far. Below is the entire editor.

Also, I rolled back my version of dev to just before the change to prevent
removing a component twice, the crash on startup goes away, and if
setContinuousRepainting
is set to false the crash on exit goes away and everything seems to work
fine.

#ifndef PLUGINEDITOR_H_INCLUDED
#define PLUGINEDITOR_H_INCLUDED

#include "../JuceLibraryCode/JuceHeader.h"
#include "PluginProcessor.h"
//#include "Main Screen.h"
#include "ScrollingNoteViewer.h"

//==============================================================================
/**
*/
class ReExpressorAudioProcessorEditor  :    public AudioProcessorEditor,
                                            private Timer
{
public:
    ReExpressorAudioProcessorEditor (ReExpressorAudioProcessor*);
    ~ReExpressorAudioProcessorEditor();

    //==============================================================================
    void paint (Graphics&) override;
    void resized() override;
    void timerCallback() override;
    double timeInTicks;
    double prevTimeInTicks = -1.0;
private:
    ReExpressorAudioProcessor *processor;
    ScrollingNoteViewer viewer;

    ScopedPointer<ResizableCornerComponent> resizer;
    ComponentBoundsConstrainer resizeLimits;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ReExpressorAudioProcessorEditor)
};


#endif  // PLUGINEDITOR_H_INCLUDED


#include "PluginProcessor.h"
#include "PluginEditor.h"
//==============================================================================
ReExpressorAudioProcessorEditor::ReExpressorAudioProcessorEditor (ReExpressorAudioProcessor *p)
    : AudioProcessorEditor (p), processor (p), viewer(p)
{
    addAndMakeVisible(viewer);
    addAndMakeVisible (resizer = new ResizableCornerComponent (this, &resizeLimits));
    resizeLimits.setSizeLimits (150, 100, 900, 700);
    setSize (700,500);
    startTimerHz (30); //VST Editor crashes on open if this is set too slow?
}

ReExpressorAudioProcessorEditor::~ReExpressorAudioProcessorEditor()
{
}

//==============================================================================
void ReExpressorAudioProcessorEditor::paint (Graphics& g)
{
    g.fillAll(Colours::aqua);
}

void ReExpressorAudioProcessorEditor::resized()
{
    viewer.setSize(getWidth()-50,getHeight()-50);
    resizer->setBounds (getWidth() - 16, getHeight() - 16, 16, 16);
}

void ReExpressorAudioProcessorEditor::timerCallback()
{
    //if (processor->isPlaying)
        viewer.repaint();
}

I should add that I’m using the Juce Plugin Host for this testing, and my
plugin is a VST2.

I think I need to rollback my “fixes”. Seems to be problematic that removeFromDesktop is called when the DAW is about remove it itself. We’ll probably need some kind of callback like componentPeerWillChange or so.

I’ve tried this same direction too and also couldn’t make it work.

I saw that Jules wrote that “The OSX GL calls seem to be thread-safe and although they fail to draw, that’s actually ok because the window is off-screen, and everything survives if you ignore (or disable) the assertions” (GL crashing on editor close),
so the fix that I ended up suggesting is “non-pervasive” change which just adds extra checks at the assertion to avoid these false-positives.

The crash when the editor is closed seems to only occur if setContinuousRepainting is true. Would it be a solution to have your plugin always setContinuousRepainting to false before closing? Is a plugin notified that it’s closing soon enough to do this?

In my plugin I don’t need continuous repainting for now so disabling it will allow me to proceed. It would still be good to have a more general fix as I may eventually need it.

Yep that’s known. What fabian tried is exactly in the spirit of your suggestion - getting the component notified that it is closing to avoid the crash.

Is a plugin notified that it’s closing soon enough to do this?

I’d say that it’s still a mystery at this point…

I also found that I can set setContinuousRepainting to false and then update my plugin continually by calling repaint in a timer. In this case my component updates continually as if setContinuousRepainting were true, but doesn’t crash on exit.

I’m not sure if there are any disadvantages to this. Possibly that its not as efficient as setting continuousRepainting to true? Or that the refresh rate is not synchronized to the frame rate?

Well with the fix that I have on develop, it is notified soon enough. But so many parts of JUCE relied on this notification not being there, that it breaks quite a lot of code.

Well yes that works as the repaints will be done on the message thread. The point of continuous repaint is that all painting will be done on a separate thread, freeing up the message thread to do other valuable work. However, the OpenGL context becomes invalid once the host decides to remove the editor’s NSView from the screen. The NSView is still there however, it’s just been removed from the screen, so no de-allocations have had a chance to be invoked.

The fix that I put on develop is to use a native callback willBeRemovedFromWindow to let the OpenGL context know in advance that it will be removed from the screen. I’ve tried to make this workaround as general as possible by invoking JUCE’s parentHierachyChanged callback (which in turn would destroy the OpenGL context) - but so much code out there relies on this not being called just before destruction.

I’ve just talked to Jules about this and we will rework the fix to not call the parentHierachyChanged callback but just have JUCE internally detach the OpenGL context when an editor’s view is about to be removed from the screen. I’ll push this to develop soon and post an update. Sorry for the back and forth on this.

Here is what seems to be a related problem.

SetContinuousRepainting to false and call repaint in a timer. This works a lot of the time, but this causes occasional asserts when the component is opened. The faster the repaint rate the less likely the assert is to happen. If setContinuousRepainting is false and repaint is never called, it always asserts on open.

The assert happens in context.copyTexture() in juce_OpenGLContext.cpp here:

void drawComponentBuffer()
{
 #if ! JUCE_ANDROID
  glEnable (GL_TEXTURE_2D);
  clearGLError();
 #endif

 #if JUCE_WINDOWS
  // some stupidly old drivers are missing this function, so try to at least avoid a crash here,
  // but if you hit this assertion you may want to have your own version check before using the
  // component rendering stuff on such old drivers.
  jassert (context.extensions.glActiveTexture != nullptr);
  if (context.extensions.glActiveTexture != nullptr)
 #endif
      context.extensions.glActiveTexture (GL_TEXTURE0);

  glBindTexture (GL_TEXTURE_2D, cachedImageFrameBuffer.getTextureID());
  bindVertexArray();

  const Rectangle<int> cacheBounds (cachedImageFrameBuffer.getWidth(), cachedImageFrameBuffer.getHeight());
  ////Asserts in call to copyTexture
  context.copyTexture (cacheBounds, cacheBounds, cacheBounds.getWidth(), cacheBounds.getHeight(), false);
  glBindTexture (GL_TEXTURE_2D, 0);
  JUCE_CHECK_OPENGL_ERROR
  }

It’s trying to restore a cached texture representing the component. If repaint hasn’t been called, presumably the cache has not been created and it asserts.

The assert on open goes away if setContinuousRepainting is true, but now it assets on shutdown.

For now I’ll try selectively disabling the assert as suggested by yairadix.