Skip to content

fix GL context teardown regression from QOpenGLWidget to QOpenGLWindow - #510

Merged
nschimme merged 1 commit into
MUME:masterfrom
nschimme:fix-windows-gl
Apr 8, 2026
Merged

fix GL context teardown regression from QOpenGLWidget to QOpenGLWindow#510
nschimme merged 1 commit into
MUME:masterfrom
nschimme:fix-windows-gl

Conversation

@nschimme

@nschimme nschimme commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

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().

Summary by Sourcery

Ensure OpenGL resources are cleaned up safely when the context is destroyed and prevent error-checking loops during teardown.

Bug Fixes:

  • Avoid infinite error-checking loops by skipping OpenGL error checks when no context is current.
  • Prevent GL resource deletion after the OpenGL context is destroyed by cleaning up MapCanvas and weather resources on QOpenGLContext::aboutToBeDestroyed and removing teardown-time mesh operations from MainWindow destruction.
  • Guard legacy VBO and shader program reset paths to avoid invalid GL calls when the OpenGL functions object has expired, logging leaks instead.

Enhancements:

  • Add explicit cleanup for GLWeather resources and a reset helper for UniqueMesh to centralize GL resource teardown behavior.

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().
@sourcery-ai

sourcery-ai Bot commented Apr 7, 2026

Copy link
Copy Markdown

Reviewer's Guide

Ensures all OpenGL resource teardown happens while a valid context is current by wiring cleanup to QOpenGLContext::aboutToBeDestroyed, adding missing weather cleanup, hardening error checking and legacy GL resource reset paths, and avoiding GL-related work during MainWindow destruction.

Sequence diagram for OpenGL context teardown and cleanupOpenGL flow

sequenceDiagram
    participant QOpenGLContext
    participant MapCanvas
    participant Batches
    participant GLWeather
    participant Textures
    participant GLFont
    participant OpenGL

    QOpenGLContext->>MapCanvas: aboutToBeDestroyed
    MapCanvas->>MapCanvas: cleanupOpenGL
    MapCanvas->>Batches: resetExistingMeshesAndIgnorePendingRemesh
    MapCanvas->>GLWeather: cleanup
    MapCanvas->>Textures: destroyAll
    MapCanvas->>GLFont: cleanup
    MapCanvas->>OpenGL: cleanup
Loading

Class diagram for updated OpenGL cleanup and legacy resource management

classDiagram
    class MapCanvas {
        +cleanupOpenGL() void
    }

    class GLWeather {
        +GLWeather(OpenGL gl, GameObserver observer, FrameManager frameManager)
        +~GLWeather() void
        +cleanup() void
        +updateFromGame() void
    }

    class Legacy_Functions {
        +getUboManager() UboManager&
        +checkError() void
    }

    class Legacy_VBO {
        -m_weakFunctions
        +reset() void
    }

    class Legacy_Program {
        -m_weakFunctions
        +reset() void
    }

    class UniqueMesh {
        -m_mesh
        +~UniqueMesh() void
        +reset() void
        +render(GLRenderState rs) void
        +operator_bool() bool
    }

    MapCanvas o-- GLWeather : owns
    MapCanvas o-- UniqueMesh : uses
    Legacy_VBO --> Legacy_Functions : uses
    Legacy_Program --> Legacy_Functions : uses
    Legacy_Functions --> UboManager : returns

    class UboManager {
        +unregisterRebuildFunction(SharedVboEnum enumValue) void
    }
Loading

File-Level Changes

Change Details Files
Move MapCanvas GL teardown from destructor to QOpenGLContext::aboutToBeDestroyed and include weather resources in cleanup.
  • Remove MapCanvas::~MapCanvas() call to cleanupOpenGL() so destruction no longer assumes a current context.
  • Connect QOpenGLContext::aboutToBeDestroyed to MapCanvas::cleanupOpenGL() with a direct connection inside initializeGL(), ensuring cleanup runs while the context is still current.
  • Extend MapCanvas::cleanupOpenGL() to call m_weather.cleanup() alongside existing mesh, texture, font, and OpenGL cleanup.
  • Add GLWeather::cleanup() method to release simulation, particle, atmosphere, and time-of-day resources explicitly.
src/display/mapcanvas.cpp
src/display/mapcanvas_gl.cpp
src/opengl/Weather.cpp
src/opengl/Weather.h
Harden legacy GL object reset paths to avoid calling GL after the Functions object/context is gone and to log leaks instead.
  • Change VBO::reset() to only call glDeleteBuffers when the weak_ptr to Functions can be locked, otherwise log a critical message that the VBO is leaked.
  • Change Program::reset() similarly to only call glDeleteProgram when the Functions weak_ptr is still valid and log a critical message on leak.
  • Include QDebug in VBO.cpp to support the new logging.
src/opengl/legacy/VBO.cpp
Guard OpenGL error checking and mesh cleanup utilities against missing contexts and make teardown safer.
  • Guard Functions::checkError() by returning early when QOpenGLContext::currentContext() is null to avoid infinite glGetError() loops on drivers that misbehave without a context.
  • Include QOpenGLContext in Legacy.cpp for the new guard.
  • Add UniqueMesh::reset() helper that forwards to the underlying mesh reset, providing a simple way to drop mesh resources without assuming destructor-time GL availability.
src/opengl/legacy/Legacy.cpp
src/opengl/OpenGLTypes.h
Avoid triggering GL work during MainWindow destruction.
  • Remove the call to forceNewFile() from MainWindow::~MainWindow() so teardown no longer triggers GL mesh creation/destruction after the GL context has been destroyed.
src/mainwindow/mainwindow.cpp

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/opengl/legacy/VBO.cpp" line_range="30-33" />
<code_context>
         }
-        auto sharedFunctions = std::exchange(m_weakFunctions, {}).lock();
-        deref(sharedFunctions).glDeleteBuffers(1, &vbo);
+        if (auto sharedFunctions = std::exchange(m_weakFunctions, {}).lock()) {
+            sharedFunctions->glDeleteBuffers(1, &vbo);
+        } else {
+            qCritical() << "Legacy::Functions is no longer valid, leaking VBO" << vbo;
+        }
     }
</code_context>
<issue_to_address>
**suggestion (bug_risk):** After logging the leak, consider clearing or marking the VBO handle to avoid repeated leak reports or accidental reuse.

When `m_weakFunctions` has expired, we log the leak but leave `vbo` unchanged. If `reset()` (or a destructor calling it) runs again on the same instance, we’ll log the same leak repeatedly and still hold a stale handle. Consider setting `vbo = 0` after logging (and doing the same for shader programs) to prevent repeated critical logs and reduce the risk of future code treating the stale handle as valid.

Suggested implementation:

```cpp
        if (auto sharedFunctions = std::exchange(m_weakFunctions, {}).lock()) {
            sharedFunctions->glDeleteBuffers(1, &vbo);
        } else {
            qCritical() << "Legacy::Functions is no longer valid, leaking VBO" << vbo;
            vbo = 0;
        }

```

Apply the same pattern to any similar teardown/reset logic for shader programs (or other GL objects) in this or related files: after logging that the GL functions are unavailable and that a resource will be leaked, explicitly reset the corresponding handle (e.g. program ID) to 0 to avoid repeated leak logs and accidental reuse.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/opengl/legacy/VBO.cpp
Comment on lines +30 to +33
if (auto sharedFunctions = std::exchange(m_weakFunctions, {}).lock()) {
sharedFunctions->glDeleteBuffers(1, &vbo);
} else {
qCritical() << "Legacy::Functions is no longer valid, leaking VBO" << vbo;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): After logging the leak, consider clearing or marking the VBO handle to avoid repeated leak reports or accidental reuse.

When m_weakFunctions has expired, we log the leak but leave vbo unchanged. If reset() (or a destructor calling it) runs again on the same instance, we’ll log the same leak repeatedly and still hold a stale handle. Consider setting vbo = 0 after logging (and doing the same for shader programs) to prevent repeated critical logs and reduce the risk of future code treating the stale handle as valid.

Suggested implementation:

        if (auto sharedFunctions = std::exchange(m_weakFunctions, {}).lock()) {
            sharedFunctions->glDeleteBuffers(1, &vbo);
        } else {
            qCritical() << "Legacy::Functions is no longer valid, leaking VBO" << vbo;
            vbo = 0;
        }

Apply the same pattern to any similar teardown/reset logic for shader programs (or other GL objects) in this or related files: after logging that the GL functions are unavailable and that a resource will be leaked, explicitly reset the corresponding handle (e.g. program ID) to 0 to avoid repeated leak logs and accidental reuse.

@nschimme
nschimme merged commit 91be79e into MUME:master Apr 8, 2026
18 checks passed
@nschimme
nschimme deleted the fix-windows-gl branch April 8, 2026 00:13
@codecov

codecov Bot commented Apr 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 25.05%. Comparing base (93d43e3) to head (6756a09).
⚠️ Report is 4 commits behind head on master.

Files with missing lines Patch % Lines
src/opengl/legacy/VBO.cpp 0.00% 7 Missing ⚠️
src/opengl/Weather.cpp 0.00% 5 Missing ⚠️
src/display/mapcanvas_gl.cpp 0.00% 2 Missing ⚠️
src/opengl/legacy/Legacy.cpp 0.00% 2 Missing ⚠️
src/opengl/OpenGLTypes.h 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #510      +/-   ##
==========================================
- Coverage   25.06%   25.05%   -0.01%     
==========================================
  Files         510      510              
  Lines       42275    42286      +11     
  Branches     4574     4575       +1     
==========================================
  Hits        10596    10596              
- Misses      31679    31690      +11     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant