Skip to content

[Windows][Skia] OpenGL resource context remains current after engine shutdown #193591

Closed

Description

@XDevNexGen

Steps to reproduce

This is reproducible in the engine's native embedder tests, without an app or plugin.

  1. Check out flutter/flutter at 3829a94 and sync engine dependencies.
  2. Add the regression test included below to engine/src/flutter/shell/platform/embedder/tests/embedder_gl_unittests.cc. Leave the production implementation unchanged.
  3. From engine/src, configure and build:
python flutter/tools/gn --unoptimized --no-lto
ninja -C out/host_debug_unopt embedder_unittests
out/host_debug_unopt/embedder_unittests.exe --gtest_filter=EmbedderTest.ShutdownReleasesOpenGLResourceContextOnItsThread

Expected results

After synchronous engine shutdown, the resource context has been cleared on the I/O thread that made it current.

Actual results

The test-only baseline fails at both post-shutdown assertions:

context.resource_released: Actual false, Expected true
context.resource_current:  Actual true,  Expected false
1 FAILED TEST
EXIT=1

Applying the Skia release hook makes the same test pass. Validation:

Windows x64, host_debug_unopt (--unoptimized --no-lto), source 3829a944235cd8e25142d72302dc89db4cd16934:

  • New regression with unchanged production code: failed both expected release assertions (exit 1).
  • With the two Skia production changes: the same regression passed (exit 0).
  • Full embedder_unittests: 180 passed, 8 skipped, 1 pre-existing disabled test; no failures (exit 0).
  • Shutdown regression repeated 100 times: all passed (exit 0).
  • Changed-line clang-format and git diff --check: passed.

The eight skips are five AOT-only checks in this JIT build and three golden checks restricted to Linux. No test was disabled or skip condition changed by this patch. Other platforms remain for upstream CI.

Cause and proposed fix

Shell teardown already calls PlatformViewEmbedder::ReleaseResourceContext on the I/O thread after releasing the last IO manager. PlatformViewEmbedder forwards this to the surface. EmbedderSurfaceGLSkia does not override the default no-op, so its resource context remains current. EGL can defer destruction while the context remains current on a thread.

Implement the existing release hook in EmbedderSurfaceGLSkia and clear the current context through the existing embedder callback. GLES Impeller already implements analogous cleanup; this report does not claim a new Impeller fix.

The EGL reference specifies that context destruction is deferred until the context is no longer current to any thread: https://registry.khronos.org/EGL/sdk/docs/man/html/eglDestroyContext.xhtml

Related prior investigation: #138893 (comment) identified the same EGL thread-binding mechanism on December 12, 2023. The reporter closed the issue on July 18, 2024, and it was locked on August 1, 2024; no fixing PR is linked there. This focused issue provides a standalone native reproduction of the missing Skia resource-context release at 3829a94.

Code sample

diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_gl_unittests.cc b/engine/src/flutter/shell/platform/embedder/tests/embedder_gl_unittests.cc
index c3e254eedc2..10aa0a555e8 100644
--- a/engine/src/flutter/shell/platform/embedder/tests/embedder_gl_unittests.cc
+++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_gl_unittests.cc
@@ -6,7 +6,10 @@
 #define FML_USED_ON_EMBEDDER
 
 #include <atomic>
+#include <mutex>
 #include <string>
+#include <thread>
+#include <utility>
 #include <vector>
 
 #include "GLES3/gl3.h"
@@ -52,6 +55,64 @@ TEST_F(EmbedderTest, CanCreateOpenGLRenderingEngine) {
   ASSERT_TRUE(engine.is_valid());
 }
 
+namespace {
+
+class ResourceReleaseTestContext final : public EmbedderTestContextGL {
+ public:
+  explicit ResourceReleaseTestContext(std::string assets_path)
+      : EmbedderTestContextGL(std::move(assets_path)) {
+    auto& config = GetRendererConfig().open_gl;
+    make_resource_ = config.make_resource_current;
+    clear_current_ = config.clear_current;
+    config.make_resource_current = [](void* data) {
+      auto* self = static_cast<ResourceReleaseTestContext*>(
+          static_cast<EmbedderTestContext*>(data));
+      const bool current = self->make_resource_(data);
+      if (current) {
+        std::scoped_lock lock(self->mutex_);
+        self->resource_thread_ = std::this_thread::get_id();
+        self->resource_current = true;
+      }
+      return current;
+    };
+    config.clear_current = [](void* data) {
+      auto* self = static_cast<ResourceReleaseTestContext*>(
+          static_cast<EmbedderTestContext*>(data));
+      const bool cleared = self->clear_current_(data);
+      std::scoped_lock lock(self->mutex_);
+      if (cleared && self->resource_thread_ == std::this_thread::get_id()) {
+        self->resource_current = false;
+        self->resource_released = true;
+      }
+      return cleared;
+    };
+  }
+  std::atomic<bool> resource_current = false;
+  std::atomic<bool> resource_released = false;
+
+ private:
+  BoolCallback make_resource_;
+  BoolCallback clear_current_;
+  std::mutex mutex_;
+  std::thread::id resource_thread_;
+};
+
+}  // namespace
+
+TEST_F(EmbedderTest, ShutdownReleasesOpenGLResourceContextOnItsThread) {
+  ResourceReleaseTestContext context(GetFixturesDirectory());
+  EmbedderConfigBuilder builder(context);
+  builder.AddCommandLineArgument("--enable-impeller=false");
+  builder.SetSurface(DlISize(1, 1));
+  auto engine = builder.LaunchEngine();
+  ASSERT_TRUE(engine.is_valid());
+  ASSERT_TRUE(context.resource_current);
+  ASSERT_FALSE(context.resource_released);
+  engine.reset();
+  EXPECT_TRUE(context.resource_released);
+  EXPECT_FALSE(context.resource_current);
+}
+
 //------------------------------------------------------------------------------
 /// If an incorrectly configured compositor is set on the engine, the engine
 /// must fail to launch instead of failing to render a frame at a later point in

Logs

Full output from the regression before and after the fix follows. Both runs emit the same NativeSymbolResolver (SymInitialize 87) diagnostic; it does not prevent either test run. EXIT records the process exit code.

Before the fix
[INFO:flutter/testing/test_timeout_listener.cc(75)] Test timeout of 300 seconds per test case will be enforced.
Note: Google Test filter = EmbedderTest.ShutdownReleasesOpenGLResourceContextOnItsThread
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from EmbedderTest
[ RUN      ] EmbedderTest.ShutdownReleasesOpenGLResourceContextOnItsThread
Failed to init NativeSymbolResolver (SymInitialize 87)
../../flutter/shell/platform/embedder/tests/embedder_gl_unittests.cc(112): error: Value of: context.resource_released
  Actual: false
Expected: true

../../flutter/shell/platform/embedder/tests/embedder_gl_unittests.cc(113): error: Value of: context.resource_current
  Actual: true
Expected: false

[  FAILED  ] EmbedderTest.ShutdownReleasesOpenGLResourceContextOnItsThread (334 ms)
[----------] 1 test from EmbedderTest (334 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (334 ms total)
[  PASSED  ] 0 tests.
[  FAILED  ] 1 test, listed below:
[  FAILED  ] EmbedderTest.ShutdownReleasesOpenGLResourceContextOnItsThread

 1 FAILED TEST

EXIT=1
After the fix
[INFO:flutter/testing/test_timeout_listener.cc(75)] Test timeout of 300 seconds per test case will be enforced.
Note: Google Test filter = EmbedderTest.ShutdownReleasesOpenGLResourceContextOnItsThread
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from EmbedderTest
[ RUN      ] EmbedderTest.ShutdownReleasesOpenGLResourceContextOnItsThread
Failed to init NativeSymbolResolver (SymInitialize 87)
[       OK ] EmbedderTest.ShutdownReleasesOpenGLResourceContextOnItsThread (348 ms)
[----------] 1 test from EmbedderTest (348 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (349 ms total)
[  PASSED  ] 1 test.

EXIT=0

Environment

  • Windows 11 Pro, x64, build 26200; physical desktop.
  • Installed graphics adapters: NVIDIA GeForce RTX 4090 (driver 32.0.15.9186) and AMD Radeon Graphics (32.0.21043.5001).
  • Engine source: 3829a94.
  • Build: host_debug_unopt, Skia explicitly selected by the regression test.
  • Compiler: Flutter-vended Fuchsia clang 24.0.0git, revision 07d67299a15ce03b053736e2d31a668ee0576987.
  • No framework CLI command is needed for this native-engine reproduction.

Flutter Doctor output

This reproduction runs the native engine test executable directly, without the Flutter framework CLI or an application project. The engine revision, build configuration, compiler and host details are listed above; flutter doctor from a separate installed SDK would describe a different build.

Earlier downstream measurements (separate engine revision)

On Flutter 4cf2416, repeated Windows Skia window/engine creation and destruction reached 1,121,796,096 private committed bytes after 200 cycles with the stock engine. With the source repair, 1,000 cycles settled at 243,449,856 bytes (267,878,400 after warm-up), measured while the parent process was still running.

These are downstream application measurements from the pinned revision, not memory benchmarks of current upstream. The native test above is the standalone reproduction for this report.

Activity

  1. added
    engineflutter/engine related. See also e: labels.
    platform-windowsBuilding on or for Windows specifically
    perf: memoryPerformance issues related to memory
    on Sep 30, 2026
  2. mbcorona commented on Sep 30, 2026

    @mbcorona
    Member

    Confirmed on master. EmbedderSurfaceGLImpeller overrides ReleaseResourceContext but EmbedderSurfaceGLSkia does not, so it inherits the base no op and the Skia resource context stays current after shutdown. Per the EGL spec destruction is deferred while a context remains current, which is the leak. Matches your native repro.

    Routing to team-engine.

  3. added
    team-windowsOwned by the Windows platform team
    and removed on Oct 5, 2026
  4. added
    P2Important issues not at the top of the work list
    triaged-windowsTriaged by the Windows platform team
    on Oct 7, 2026
  5. added a commit that references this issue on Nov 12, 2026
    f7b19ef
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work liste: openglengineflutter/engine related. See also e: labels.perf: memoryPerformance issues related to memoryplatform-windowsBuilding on or for Windows specificallyteam-windowsOwned by the Windows platform teamtriaged-windowsTriaged by the Windows platform team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions