Skip to content

FlutterAngle.dispose() crashes the process on Windows release when a co-resident ANGLE consumer (e.g. media_kit_video) has invalidated the shared EGL state #52

@Vera-Spoettl

Description

@Vera-Spoettl

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions