Skip to content

Commit 0b9b2a9

Browse files
committed
fix GL context teardown regression from QOpenGLWidget to QOpenGLWindow
Commit aa07211 migrated MapCanvas from QOpenGLWidget to QOpenGLWindow. Under QOpenGLWidget, the GL context is still current during destruction, so calling cleanupOpenGL() from ~MapCanvas() was safe. Under QOpenGLWindow, the native context is destroyed before C++ destructors run, making any GL call from a destructor operate without a current context. On NVIDIA drivers, glGetError() returns GL_INVALID_OPERATION indefinitely when called without a current context, causing the checkError() loop to spin forever. Two destructor call paths triggered this: 1. MainWindow::~MainWindow() called forceNewFile() -> slot_dataLoaded() -> forceUpdateMeshes(), which created and destroyed GL meshes after the context was gone. 2. GLWeather was not included in cleanupOpenGL(), so its particle meshes were destroyed in ~MapCanvas() after the context was gone. Fix: - Connect QOpenGLContext::aboutToBeDestroyed -> cleanupOpenGL() in initializeGL(), so GL resources are freed while the context is still current. (The call in ~MapCanvas() is removed.) - Add m_weather.cleanup() to cleanupOpenGL(), matching the existing cleanup of m_batches and m_glFont. - Remove forceNewFile() from MainWindow::~MainWindow() to avoid triggering GL mesh creation/destruction during teardown. - Guard checkError() to return early when no context is current, as a safety net against future oversights. - Guard VBO::reset() and Program::reset() to skip GL deletion and log a qCritical when the Functions weak_ptr has expired, consistent with the existing behaviour in VAO::reset().
1 parent 66643be commit 0b9b2a9

8 files changed

Lines changed: 39 additions & 8 deletions

File tree

src/display/mapcanvas.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,6 @@ MapCanvas::~MapCanvas()
9494
if (pmc == this) {
9595
pmc = nullptr;
9696
}
97-
98-
cleanupOpenGL();
9997
}
10098

10199
MapCanvas *MapCanvas::getPrimary()

src/display/mapcanvas_gl.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ void MapCanvas::cleanupOpenGL()
125125
// note: m_batchedMeshes co-owns textures created by MapCanvasData,
126126
// and it also owns the lifetime of some OpenGL objects (e.g. VBOs).
127127
m_batches.resetExistingMeshesAndIgnorePendingRemesh();
128+
m_weather.cleanup();
128129
m_textures.destroyAll();
129130
getGLFont().cleanup();
130131
getOpenGL().cleanup();
@@ -306,6 +307,14 @@ void MapCanvas::initializeGL()
306307
this->updateTextures();
307308
m_frameManager.requestUpdate();
308309
});
310+
311+
// Clean up GL resources while the context is still current.
312+
// The destructor is too late — Qt destroys the context before ~MapCanvas() runs.
313+
connect(context(),
314+
&QOpenGLContext::aboutToBeDestroyed,
315+
this,
316+
&MapCanvas::cleanupOpenGL,
317+
Qt::DirectConnection);
309318
}
310319

311320
/* Direct means it is always called from the emitter's thread */

src/mainwindow/mainwindow.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ static void addApplicationFont()
102102

103103
MainWindow::~MainWindow()
104104
{
105-
forceNewFile();
106105
mmqt::rdisconnect(this);
107106
async_tasks::cleanup();
108107
delete m_listener;

src/opengl/OpenGLTypes.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,7 @@ class NODISCARD UniqueMesh final
401401
~UniqueMesh() = default;
402402
DEFAULT_MOVES_DELETE_COPIES(UniqueMesh);
403403

404+
void reset() { m_mesh.reset(); }
404405
void render(const GLRenderState &rs) const { deref(m_mesh).render(rs); }
405406
NODISCARD explicit operator bool() const { return m_mesh != nullptr; }
406407
};

src/opengl/Weather.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,14 @@ GLWeather::~GLWeather()
202202
m_gl.getUboManager().unregisterRebuildFunction(Legacy::SharedVboEnum::WeatherBlock);
203203
}
204204

205+
void GLWeather::cleanup()
206+
{
207+
m_simulation.reset();
208+
m_particles.reset();
209+
m_atmosphere.reset();
210+
m_timeOfDay.reset();
211+
}
212+
205213
void GLWeather::updateFromGame()
206214
{
207215
switch (m_observer.getWeather()) {

src/opengl/Weather.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ class NODISCARD GLWeather final
8686
GameObserver &observer,
8787
FrameManager &frameManager);
8888
~GLWeather();
89-
89+
void cleanup();
9090
DELETE_CTORS_AND_ASSIGN_OPS(GLWeather);
9191

9292
public:

src/opengl/legacy/Legacy.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
#include <QDebug>
3535
#include <QFile>
3636
#include <QMessageLogContext>
37+
#include <QOpenGLContext>
3738
#include <QOpenGLExtraFunctions>
3839
#include <QOpenGLTexture>
3940

@@ -354,6 +355,12 @@ UboManager &Functions::getUboManager()
354355
/// This only exists so we can detect errors in contexts that don't support \c glDebugMessageCallback().
355356
void Functions::checkError()
356357
{
358+
// glGetError() returns GL_INVALID_OPERATION indefinitely when called without a current context
359+
// (e.g. on NVIDIA drivers), causing an infinite loop. Skip the check if no context is current.
360+
if (QOpenGLContext::currentContext() == nullptr) {
361+
return;
362+
}
363+
357364
#define CASE(x) \
358365
case (x): \
359366
qCritical() << "OpenGL error" << #x; \

src/opengl/legacy/VBO.cpp

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
#include "VBO.h"
55

6+
#include <QDebug>
7+
68
namespace Legacy {
79
bool LOG_VBO_ALLOCATIONS = false;
810
bool LOG_VBO_STATIC_UPLOADS = false;
@@ -25,8 +27,11 @@ void VBO::reset()
2527
if (LOG_VBO_ALLOCATIONS) {
2628
qInfo() << this << "Freeing VBO" << vbo;
2729
}
28-
auto sharedFunctions = std::exchange(m_weakFunctions, {}).lock();
29-
deref(sharedFunctions).glDeleteBuffers(1, &vbo);
30+
if (auto sharedFunctions = std::exchange(m_weakFunctions, {}).lock()) {
31+
sharedFunctions->glDeleteBuffers(1, &vbo);
32+
} else {
33+
qCritical() << "Legacy::Functions is no longer valid, leaking VBO" << vbo;
34+
}
3035
}
3136
assert(m_weakFunctions.lock() == nullptr);
3237
}
@@ -77,8 +82,12 @@ void Program::reset()
7782
if (LOG_VBO_ALLOCATIONS) {
7883
qInfo() << this << "Freeing Shader Program" << program;
7984
}
80-
auto sharedFunctions = std::exchange(m_weakFunctions, {}).lock();
81-
deref(sharedFunctions).glDeleteProgram(program);
85+
if (auto sharedFunctions = std::exchange(m_weakFunctions, {}).lock()) {
86+
sharedFunctions->glDeleteProgram(program);
87+
} else {
88+
qCritical() << "Legacy::Functions is no longer valid, leaking shader program"
89+
<< program;
90+
}
8291
}
8392
assert(m_weakFunctions.lock() == nullptr);
8493
}

0 commit comments

Comments
 (0)