Summary
When another ANGLE consumer that shares the bundled _flutter_gl EGL display/context tears down its state before FlutterAngle.dispose() runs, the dispose path throws on debug and trips a fatal libANGLE assertion on release, aborting the process.
In our case the co-resident consumer is media_kit_video (Player.dispose() releases its EGL surface), but the same race applies to any other ANGLE-bound plugin loaded alongside flutter_angle.
Environment
flutter_angle: 0.4.1 (main HEAD, verified — same vulnerable code path was already present at 0.3.9)
three_js_core: 0.2.7 (the consumer in our app)
Flutter: 3.41.3
Platform: Windows 10 / 11 desktop, release build (flutter build windows --release)
GPU: Intel(R) Iris(R) Xe Graphics, Direct3D 11
ANGLE: ANGLE 2.1.14811 (git hash a29ef6129501), OpenGL ES 3.0.0
Co-resident plugin: media_kit + media_kit_video + media_kit_libs_video
Symptom
On a Flutter desktop app that uses flutter_angle (via three_js_core) and media_kit_video in the same process, disposing a three_js-based widget after media_kit's Player has been opened and closed once produces:
Debug builds — Flutter catches the Dart-level exception, the app survives but the three_js visualisation is gone:
Uncaught exception
Failed to make current using display [Pointer: ...], draw [...], read [...], context [...].
EGL error EglError.badSurface (12301)
StackTrace:
#0 EGL.eglMakeCurrent (package:flutter_angle/desktop/lib_egl.dart:418:5)
#1 FlutterAngle.deleteTexture (package:flutter_angle/desktop/angle.dart:673:16)
#2 FlutterAngle.dispose.<anonymous closure> (package:flutter_angle/desktop/angle.dart:706:18)
#3 List.forEach (dart:core-patch/growable_array.dart:425:8)
#4 FlutterAngle.dispose (package:flutter_angle/desktop/angle.dart:705:15)
#5 ThreeJS.dispose (package:three_js_core/others/three_viewer.dart:198:12)
Release builds — process dies:
FATAL: Surface.cpp:528 (egl::Surface::releaseTexImage):
! Assert failed in egl::Surface::releaseTexImage
(../../src/libANGLE/Surface.cpp:528): context
Lost connection to device.
Sometimes the FATAL is Context.h:524 (gl::Context::skipValidation): !isContextLost() || !mSkipValidation instead — same underlying race, different assertion fires depending on timing.
Windows reports a runner.exe Application Error event with faulting module libGLESv2.dll, offset 0xefe4f2, exception 0xc0000005 write to 0x0.
Reproduction
In a Flutter Windows app:
Add media_kit, media_kit_video, media_kit_libs_video as dependencies and play any short video via Player.open(Media.memory(...)) then close the player.
Add three_js_core / flutter_angle and render any scene that ends up calling FlutterAngle.dispose([textures]) on widget unmount.
Build release (flutter build windows --release), launch the EXE.
Display the three_js scene, trigger the media_kit playback path, then dispose the three_js widget (or close the window with both alive).
The process terminates with the FATAL: Surface.cpp:528 releaseTexImage line above.
Root cause
media_kit_video and flutter_angle are two independent consumers of the same ANGLE/D3D11 state via the bundled _flutter_gl package — same libGLESv2.dll, same libEGL.dll, same EGLDisplay. When media_kit's Player.dispose() releases its EGL surface, the shared display/context is left in a "lost" state.
Subsequently FlutterAngle.deleteTexture (called from FlutterAngle.dispose) calls _libEGL!.eglMakeCurrent(_display, texture.surfaceId, ..., _baseAppContext) on that lost state (angle.dart:673). eglMakeCurrent returns EGL_BAD_SURFACE and lib_egl.dart:eglMakeCurrent throws EglException.
That exception propagates out of dispose and:
On debug: Flutter's framework catches it and prints the stack. The app survives but FlutterAngle's native resources are now in a partially-disposed state.
On release: the same EGL operations on the lost context trip ANGLE's C++ assertions. The C++ FATAL calls abort() and the Dart-level try/catch cannot intercept it.
The OS would reclaim any leaked GPU handles at process exit anyway, so the EGL calls inside dispose are best-effort by nature — but currently they don't behave as best-effort.
Proposed fix
Make FlutterAngle.dispose() and FlutterAngle.deleteTexture() defensive:
dispose becomes idempotent — early-return on _disposed, and set _disposed = true before any EGL/GL call so a mid-dispose throw can't strand the instance.
Wrap each native call (per-texture deleteTexture invocations, eglDestroyContext, _libEGL.dispose()) in its own try/catch that logs and continues.
In deleteTexture, wrap the platform-specific EGL/GL teardown block (including the platform-channel deleteTexture call) in try/catch.
Result: a co-resident ANGLE consumer can invalidate the shared EGL state without crashing the process; flutter_angle leaks zero handles because the OS reclaims them at process exit anyway.
I have a working patch on a fork:
Repo: https://github.com/Zeisberg-GmbH/flutter_angle
Branch: fix/defensive-dispose-DHFT2102-2681
Commit: Zeisberg-GmbH@06abbe4
Diff: +69 / −33 in flutter_angle/lib/desktop/angle.dart
The patch is against the Zeisberg fork's main (which trails upstream slightly) but applies trivially to upstream main — only the surrounding line numbers shift. Verified end-to-end on a production Windows release build.
Happy to open it as a PR against this repo — let me know which branch you'd like it targeted at.
Drive-by cleanup in the same patch
At flutter_angle/lib/desktop/angle.dart:678:
angleConsole.warning('There is no active FlutterGL Texture to delete');
fires unconditionally between two conditional blocks, on every successful deleteTexture call — looks like a stray debug log. The patch removes it. If it was intentional, easy to revert that one line.
Summary
When another ANGLE consumer that shares the bundled _flutter_gl EGL display/context tears down its state before FlutterAngle.dispose() runs, the dispose path throws on debug and trips a fatal libANGLE assertion on release, aborting the process.
In our case the co-resident consumer is media_kit_video (Player.dispose() releases its EGL surface), but the same race applies to any other ANGLE-bound plugin loaded alongside flutter_angle.
Environment
flutter_angle: 0.4.1 (main HEAD, verified — same vulnerable code path was already present at 0.3.9)
three_js_core: 0.2.7 (the consumer in our app)
Flutter: 3.41.3
Platform: Windows 10 / 11 desktop, release build (flutter build windows --release)
GPU: Intel(R) Iris(R) Xe Graphics, Direct3D 11
ANGLE: ANGLE 2.1.14811 (git hash a29ef6129501), OpenGL ES 3.0.0
Co-resident plugin: media_kit + media_kit_video + media_kit_libs_video
Symptom
On a Flutter desktop app that uses flutter_angle (via three_js_core) and media_kit_video in the same process, disposing a three_js-based widget after media_kit's Player has been opened and closed once produces:
Debug builds — Flutter catches the Dart-level exception, the app survives but the three_js visualisation is gone:
Uncaught exception
Failed to make current using display [Pointer: ...], draw [...], read [...], context [...].
EGL error EglError.badSurface (12301)
StackTrace:
Release builds — process dies:
Windows reports a runner.exe Application Error event with faulting module libGLESv2.dll, offset 0xefe4f2, exception 0xc0000005 write to 0x0.
Reproduction
In a Flutter Windows app:
Add media_kit, media_kit_video, media_kit_libs_video as dependencies and play any short video via Player.open(Media.memory(...)) then close the player.
Add three_js_core / flutter_angle and render any scene that ends up calling FlutterAngle.dispose([textures]) on widget unmount.
Build release (flutter build windows --release), launch the EXE.
Display the three_js scene, trigger the media_kit playback path, then dispose the three_js widget (or close the window with both alive).
The process terminates with the FATAL: Surface.cpp:528 releaseTexImage line above.
Root cause
media_kit_video and flutter_angle are two independent consumers of the same ANGLE/D3D11 state via the bundled _flutter_gl package — same libGLESv2.dll, same libEGL.dll, same EGLDisplay. When media_kit's Player.dispose() releases its EGL surface, the shared display/context is left in a "lost" state.
Subsequently FlutterAngle.deleteTexture (called from FlutterAngle.dispose) calls _libEGL!.eglMakeCurrent(_display, texture.surfaceId, ..., _baseAppContext) on that lost state (angle.dart:673). eglMakeCurrent returns EGL_BAD_SURFACE and lib_egl.dart:eglMakeCurrent throws EglException.
That exception propagates out of dispose and:
On debug: Flutter's framework catches it and prints the stack. The app survives but FlutterAngle's native resources are now in a partially-disposed state.
On release: the same EGL operations on the lost context trip ANGLE's C++ assertions. The C++ FATAL calls abort() and the Dart-level try/catch cannot intercept it.
The OS would reclaim any leaked GPU handles at process exit anyway, so the EGL calls inside dispose are best-effort by nature — but currently they don't behave as best-effort.
Proposed fix
Make FlutterAngle.dispose() and FlutterAngle.deleteTexture() defensive:
dispose becomes idempotent — early-return on _disposed, and set _disposed = true before any EGL/GL call so a mid-dispose throw can't strand the instance.
Wrap each native call (per-texture deleteTexture invocations, eglDestroyContext, _libEGL.dispose()) in its own try/catch that logs and continues.
In deleteTexture, wrap the platform-specific EGL/GL teardown block (including the platform-channel deleteTexture call) in try/catch.
Result: a co-resident ANGLE consumer can invalidate the shared EGL state without crashing the process; flutter_angle leaks zero handles because the OS reclaims them at process exit anyway.
I have a working patch on a fork:
Repo: https://github.com/Zeisberg-GmbH/flutter_angle
Branch: fix/defensive-dispose-DHFT2102-2681
Commit: Zeisberg-GmbH@06abbe4
Diff: +69 / −33 in flutter_angle/lib/desktop/angle.dart
The patch is against the Zeisberg fork's main (which trails upstream slightly) but applies trivially to upstream main — only the surrounding line numbers shift. Verified end-to-end on a production Windows release build.
Happy to open it as a PR against this repo — let me know which branch you'd like it targeted at.
Drive-by cleanup in the same patch
At flutter_angle/lib/desktop/angle.dart:678:
angleConsole.warning('There is no active FlutterGL Texture to delete');
fires unconditionally between two conditional blocks, on every successful deleteTexture call — looks like a stray debug log. The patch removes it. If it was intentional, easy to revert that one line.