From 021859b153138d72c9fd5ed83f799c1ace33085e Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Thu, 20 Aug 2026 16:40:40 +0800 Subject: [PATCH 01/13] fix(linux): bootstrap Filament on Flutter EGL display --- .../integration_test/lifecycle_test.dart | 21 ++- .../include/opengl/linux/LinuxOpenGLContext.h | 4 +- .../src/opengl/linux/LinuxOpenGLContext.cpp | 37 +++-- ...d_channel_platform_texture_descriptor.dart | 25 +++ .../src/native_texture_surface_manager.dart | 11 ++ .../platform_texture_descriptor_registry.dart | 18 +- ...rm_texture_descriptor_registry_native.dart | 25 ++- .../thermion_flutter_plugin_initializer.dart | 157 ++++++++++++++++++ .../src/thermion_flutter_plugin_native.dart | 30 ++++ .../lib/src/thermion_flutter_plugin.dart | 17 +- .../lib/src/widgets/src/viewer_widget.dart | 20 +-- .../thermion_flutter/linux/egl_texture.cc | 14 ++ .../thermion_flutter/linux/egl_texture.h | 7 + .../linux/thermion_flutter_plugin.cc | 117 ++++++++----- ...rmion_flutter_plugin_initializer_test.dart | 114 +++++++++++++ 15 files changed, 538 insertions(+), 79 deletions(-) create mode 100644 thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_initializer.dart create mode 100644 thermion_flutter/thermion_flutter/test/thermion_flutter_plugin_initializer_test.dart diff --git a/examples/flutter/quickstart/integration_test/lifecycle_test.dart b/examples/flutter/quickstart/integration_test/lifecycle_test.dart index e7963af2f..db156b379 100644 --- a/examples/flutter/quickstart/integration_test/lifecycle_test.dart +++ b/examples/flutter/quickstart/integration_test/lifecycle_test.dart @@ -38,6 +38,23 @@ import 'package:thermion_flutter/src/platform/src/frame_scheduler.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + Future pumpUntilCompleted( + WidgetTester tester, + Completer completion, { + Duration timeout = const Duration(seconds: 30), + }) async { + final stopwatch = Stopwatch()..start(); + while (!completion.isCompleted && stopwatch.elapsed < timeout) { + // Linux OpenGL initialization needs a frame containing the deferred + // bootstrap Texture before Filament can import Flutter's EGL context. + await tester.pump(const Duration(milliseconds: 16)); + } + if (!completion.isCompleted) { + throw TimeoutException('Future not completed', timeout); + } + await completion.future; + } + Future pumpViewer(WidgetTester tester) async { final sun = DirectLight.sun(direction: Vector3(0.7, -1, -0.8).normalized()); await tester.pumpWidget( @@ -247,11 +264,11 @@ void main() { ), ); - await available.future.timeout(const Duration(seconds: 30)); + await pumpUntilCompleted(tester, available); await tester.pump(); await tester.pumpWidget(const SizedBox.shrink()); - await disposalStarted.future.timeout(const Duration(seconds: 30)); + await pumpUntilCompleted(tester, disposalStarted); // Viewer disposal continues with scene/view/camera destruction after // onDispose callbacks. Give that render-thread work time to drain before diff --git a/thermion_dart/native/include/opengl/linux/LinuxOpenGLContext.h b/thermion_dart/native/include/opengl/linux/LinuxOpenGLContext.h index f61ddde46..eb3d98d10 100644 --- a/thermion_dart/native/include/opengl/linux/LinuxOpenGLContext.h +++ b/thermion_dart/native/include/opengl/linux/LinuxOpenGLContext.h @@ -19,7 +19,9 @@ namespace thermion::opengl::linux_platform { */ class LinuxOpenGLContext { public: - LinuxOpenGLContext(); + // When provided, eglDisplay is borrowed from Flutter and is never + // terminated by this context. + explicit LinuxOpenGLContext(void* eglDisplay = nullptr); ~LinuxOpenGLContext(); int64_t CreateRenderingSurface(uint32_t width, uint32_t height); diff --git a/thermion_dart/native/src/opengl/linux/LinuxOpenGLContext.cpp b/thermion_dart/native/src/opengl/linux/LinuxOpenGLContext.cpp index 52d0fabac..6fb0d4729 100644 --- a/thermion_dart/native/src/opengl/linux/LinuxOpenGLContext.cpp +++ b/thermion_dart/native/src/opengl/linux/LinuxOpenGLContext.cpp @@ -35,10 +35,10 @@ class LinuxOpenGLContext::Impl { _context = EGL_NO_CONTEXT; } - if (_display != EGL_NO_DISPLAY) { + if (_ownsDisplay && _display != EGL_NO_DISPLAY) { eglTerminate(_display); - _display = EGL_NO_DISPLAY; } + _display = EGL_NO_DISPLAY; if (_gbmDevice) { gbm_device_destroy(_gbmDevice); @@ -51,7 +51,7 @@ class LinuxOpenGLContext::Impl { } } - Impl() { + explicit Impl(void* borrowedDisplay) { std::cerr << "[ThermionGL:Context] Initializing EGL/GBM..." << std::endl; // Step 1: Open DRM render node @@ -76,17 +76,24 @@ class LinuxOpenGLContext::Impl { // Using eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, ...) ties the // display to the GPU that owns the render node. On NVIDIA systems // this selects NVIDIA's EGL instead of Mesa's software fallback. - PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT = - (PFNEGLGETPLATFORMDISPLAYEXTPROC)eglGetProcAddress("eglGetPlatformDisplayEXT"); - if (eglGetPlatformDisplayEXT) { - _display = eglGetPlatformDisplayEXT(EGL_PLATFORM_GBM_KHR, - _gbmDevice, nullptr); - } + _display = static_cast(borrowedDisplay); if (_display == EGL_NO_DISPLAY) { - // Fallback to default display - std::cerr << "[ThermionGL:Context] GBM platform display failed, " - << "trying default" << std::endl; - _display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + _ownsDisplay = true; + PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT = + (PFNEGLGETPLATFORMDISPLAYEXTPROC)eglGetProcAddress("eglGetPlatformDisplayEXT"); + if (eglGetPlatformDisplayEXT) { + _display = eglGetPlatformDisplayEXT(EGL_PLATFORM_GBM_KHR, + _gbmDevice, nullptr); + } + if (_display == EGL_NO_DISPLAY) { + // Fallback to default display + std::cerr << "[ThermionGL:Context] GBM platform display failed, " + << "trying default" << std::endl; + _display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + } + } else { + std::cerr << "[ThermionGL:Context] Reusing Flutter EGLDisplay" + << std::endl; } if (_display == EGL_NO_DISPLAY) { LOG_ERROR("Failed to get EGL display"); @@ -226,6 +233,7 @@ class LinuxOpenGLContext::Impl { private: EGLDisplay _display = EGL_NO_DISPLAY; + bool _ownsDisplay = false; EGLContext _context = EGL_NO_CONTEXT; struct gbm_device* _gbmDevice = nullptr; int _drmFd = -1; @@ -237,7 +245,8 @@ class LinuxOpenGLContext::Impl { // Public API delegates to Impl -LinuxOpenGLContext::LinuxOpenGLContext() : pImpl(std::make_unique()) {} +LinuxOpenGLContext::LinuxOpenGLContext(void* eglDisplay) + : pImpl(std::make_unique(eglDisplay)) {} LinuxOpenGLContext::~LinuxOpenGLContext() = default; diff --git a/thermion_flutter/thermion_flutter/lib/src/platform/src/method_channel_platform_texture_descriptor.dart b/thermion_flutter/thermion_flutter/lib/src/platform/src/method_channel_platform_texture_descriptor.dart index 5c7cecf6f..fa37cdcc1 100644 --- a/thermion_flutter/thermion_flutter/lib/src/platform/src/method_channel_platform_texture_descriptor.dart +++ b/thermion_flutter/thermion_flutter/lib/src/platform/src/method_channel_platform_texture_descriptor.dart @@ -55,6 +55,31 @@ class MethodChannelPlatformTextureDescriptor extends PlatformTextureDescriptor { ); } + /// Registers a Flutter texture whose GL name is allocated by Flutter's + /// raster context during the first populate callback. + /// + /// This allocation deliberately bypasses the normal native texture creation + /// path because Filament's OpenGL context does not exist yet. + static Future + allocateContextBootstrap(MethodChannel channel, int width, int height) async { + final flutterTextureId = await channel.invokeMethod( + 'createContextBootstrap', + [width, height], + ); + if (flutterTextureId == null || flutterTextureId < 0) { + throw StateError('Failed to create Flutter context bootstrap texture'); + } + return MethodChannelPlatformTextureDescriptor( + channel, + flutterTextureId: flutterTextureId, + hardwareId: 0, + windowHandle: 0, + width: width, + height: height, + deferred: true, + ); + } + /// Waits for populate() to create the GL texture (deferred path). /// Returns the hardware texture ID once ready. @override diff --git a/thermion_flutter/thermion_flutter/lib/src/platform/src/native_texture_surface_manager.dart b/thermion_flutter/thermion_flutter/lib/src/platform/src/native_texture_surface_manager.dart index 4f91bfc48..e75d9bc3b 100644 --- a/thermion_flutter/thermion_flutter/lib/src/platform/src/native_texture_surface_manager.dart +++ b/thermion_flutter/thermion_flutter/lib/src/platform/src/native_texture_surface_manager.dart @@ -79,6 +79,17 @@ class NativeTextureSurfaceManager { bool get hasUnavailableSurfaces => registry.hasUnavailableSurfaces; + /// Creates the pre-engine texture that Flutter populates on its raster + /// thread, allowing native code to capture the EGL context that Filament + /// must share with. + Future createContextBootstrap() { + return registry.serialized(registry.createContextBootstrap); + } + + Future destroyContextBootstrap(PlatformTextureDescriptor descriptor) { + return registry.serialized(() => registry.destroy(descriptor)); + } + Future getFilamentRenderingContext( Backend backend, ) { diff --git a/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry.dart b/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry.dart index bfc2a73ab..f8bd9b3d6 100644 --- a/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry.dart +++ b/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry.dart @@ -34,8 +34,22 @@ class PlatformTextureDescriptorRegistry { bool contains(PlatformTextureDescriptor descriptor) => _descriptors.any((candidate) => identical(candidate, descriptor)); - Future create(int width, int height) async { - final descriptor = await _allocator(width, height); + Future create(int width, int height) { + return createWith(_allocator, width, height); + } + + /// Allocates and tracks a descriptor using a specialized allocator. + /// + /// Most descriptors use the registry's default allocator. Initialization + /// textures are the exception: Linux OpenGL must register a deferred Flutter + /// texture before the Filament context exists, so it cannot use the normal + /// platform-surface allocation path. + Future createWith( + PlatformTextureDescriptorAllocator allocator, + int width, + int height, + ) async { + final descriptor = await allocator(width, height); _descriptors.add(descriptor); return descriptor; } diff --git a/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry_native.dart b/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry_native.dart index 6e4bdc2d0..71ba659be 100644 --- a/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry_native.dart +++ b/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry_native.dart @@ -11,9 +11,8 @@ import 'method_channel_platform_texture_descriptor.dart'; import 'platform_texture_descriptor.dart'; import 'platform_texture_descriptor_registry.dart'; -typedef TextureMutationRunner = Future Function( - Future Function() operation, -); +typedef TextureMutationRunner = + Future Function(Future Function() operation); class FilamentRenderingContext { const FilamentRenderingContext({ @@ -94,6 +93,26 @@ class NativePlatformTextureDescriptorRegistry throw UnsupportedError('Platform textures are not supported on $Platform'); } + /// Registers the texture used to import Flutter's actual raster EGL context + /// before Filament initializes. + Future createContextBootstrap() { + if (!Platform.isLinux) { + throw UnsupportedError( + 'Flutter context bootstrapping is only supported on Linux', + ); + } + return createWith( + (width, height) => + MethodChannelPlatformTextureDescriptor.allocateContextBootstrap( + channel, + width, + height, + ), + 1, + 1, + ); + } + Future getFilamentRenderingContext( Backend backend, ) async { diff --git a/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_initializer.dart b/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_initializer.dart new file mode 100644 index 000000000..146bad260 --- /dev/null +++ b/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_initializer.dart @@ -0,0 +1,157 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; + +import 'platform_texture_descriptor.dart'; + +typedef ContextBootstrapAllocator = + Future Function(); +typedef ContextBootstrapDestroyer = + Future Function(PlatformTextureDescriptor descriptor); + +/// Hosts the Flutter-side prerequisites for initializing the native plugin, +/// then runs [initialize]. +/// +/// Linux OpenGL needs a real [Texture] layer before Filament can initialize. +/// Other platforms skip that handshake and invoke [initialize] immediately. +/// Keeping the layer and descriptor lifecycle here prevents viewer widgets +/// from depending on EGL or deferred texture details. +class ThermionFlutterPluginInitializer extends StatefulWidget { + const ThermionFlutterPluginInitializer({ + super.key, + required this.initialize, + required this.child, + this.createContextBootstrap, + this.destroyContextBootstrap, + }); + + final Future Function() initialize; + final Widget child; + final ContextBootstrapAllocator? createContextBootstrap; + final ContextBootstrapDestroyer? destroyContextBootstrap; + + @override + State createState() => + _ThermionFlutterPluginInitializerState(); +} + +class _ThermionFlutterPluginInitializerState + extends State { + PlatformTextureDescriptor? _descriptor; + Future? _destroyFuture; + bool _disposing = false; + + @override + void initState() { + super.initState(); + unawaited( + _bootstrap().catchError((Object error, StackTrace stack) { + if (_disposing) return; + FlutterError.reportError( + FlutterErrorDetails( + exception: error, + stack: stack, + library: 'thermion_flutter', + context: ErrorDescription( + 'while initializing a Thermion Flutter widget', + ), + ), + ); + }), + ); + } + + Future _bootstrap() async { + final descriptor = await widget.createContextBootstrap?.call(); + _descriptor = descriptor; + + try { + if (_disposing) return; + + if (descriptor != null) { + if (mounted) { + setState(() {}); + } + try { + descriptor.hardwareId = await descriptor.awaitTextureReady(); + } catch (_) { + // Destroying the descriptor is how dispose() cancels a pending + // native populate handshake. + if (_disposing) return; + rethrow; + } + } + + if (_disposing) return; + await widget.initialize(); + } finally { + if (identical(_descriptor, descriptor)) { + _descriptor = null; + } + if (descriptor != null && mounted && !_disposing) { + // Remove the Texture layer before unregistering its native texture. + setState(() {}); + await WidgetsBinding.instance.endOfFrame; + } + if (descriptor != null) { + await _destroy(descriptor); + } + } + } + + Future _destroy(PlatformTextureDescriptor descriptor) { + return _destroyFuture ??= + widget.destroyContextBootstrap?.call(descriptor) ?? + descriptor.destroy(); + } + + @override + void dispose() { + _disposing = true; + final descriptor = _descriptor; + if (descriptor != null) { + unawaited( + _destroy(descriptor).catchError((Object error, StackTrace stack) { + FlutterError.reportError( + FlutterErrorDetails( + exception: error, + stack: stack, + library: 'thermion_flutter', + context: ErrorDescription( + 'while cancelling Flutter context initialization', + ), + ), + ); + }), + ); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final descriptor = _descriptor; + if (descriptor == null) { + return widget.child; + } + + return Stack( + fit: StackFit.expand, + alignment: Alignment.topLeft, + children: [ + widget.child, + Align( + alignment: Alignment.topLeft, + child: SizedBox.square( + dimension: 1, + child: Texture( + textureId: descriptor.flutterTextureId, + filterQuality: FilterQuality.none, + freeze: false, + ), + ), + ), + ], + ); + } +} diff --git a/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_native.dart b/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_native.dart index d9044c714..ec140262d 100644 --- a/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_native.dart +++ b/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_native.dart @@ -1,12 +1,15 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart' hide View; // ignore: implementation_imports import 'package:thermion_dart/src/filament/src/implementation/ffi_filament_app.dart'; import 'native_rendering_lifecycle_controller.dart'; import 'native_texture_surface_manager.dart'; import 'platform_texture_descriptor.dart'; +import 'thermion_flutter_plugin_initializer.dart'; import '../../../thermion_flutter.dart'; /// Initializes the native Filament application and delegates frame lifecycle @@ -159,6 +162,33 @@ class ThermionFlutterPluginImpl extends ThermionFlutterPlugin { _lifecycle.resumeExplicitly(); } + Future _createContextBootstrap() async { + if (!Platform.isLinux || + _resolveBackend() != Backend.OPENGL || + FilamentApp.instance != null) { + return null; + } + return _textureSurfaces.createContextBootstrap(); + } + + Future _destroyContextBootstrap(PlatformTextureDescriptor descriptor) { + return _textureSurfaces.destroyContextBootstrap(descriptor); + } + + @internal + @override + Widget buildInitializationScope({ + required Future Function() initialize, + required Widget child, + }) { + return ThermionFlutterPluginInitializer( + initialize: initialize, + child: child, + createContextBootstrap: _createContextBootstrap, + destroyContextBootstrap: _destroyContextBootstrap, + ); + } + @override Future createTextureAndBindToView( View view, diff --git a/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart b/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart index 9c4753033..c79f9fdef 100644 --- a/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart +++ b/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart @@ -1,11 +1,12 @@ import 'dart:async'; - +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart' hide View; import 'package:thermion_dart/thermion_dart.dart'; // ignore: implementation_imports import 'package:thermion_dart/src/filament/src/implementation/ffi_filament_app.dart'; import 'package:thermion_flutter/src/options.dart'; import 'package:thermion_flutter/src/platform/src/platform_texture_descriptor.dart'; - +import 'package:thermion_flutter/src/platform/src/thermion_flutter_plugin_initializer.dart'; import 'platform/platform.dart'; import 'package:logging/logging.dart'; @@ -83,6 +84,18 @@ abstract class ThermionFlutterPlugin { FilamentApp.instance?.setTargetFramerate(fps); } + /// Hosts any Flutter-side prerequisites while [initialize] creates a viewer. + @internal + Widget buildInitializationScope({ + required Future Function() initialize, + required Widget child, + }) { + return ThermionFlutterPluginInitializer( + initialize: initialize, + child: child, + ); + } + /// Creates a rendering surface and binds to the given [View]. /// This is an internal method, don't call this yourself unless you are a /// thermion package developer. diff --git a/thermion_flutter/thermion_flutter/lib/src/widgets/src/viewer_widget.dart b/thermion_flutter/thermion_flutter/lib/src/widgets/src/viewer_widget.dart index 44f169778..83a892f9e 100644 --- a/thermion_flutter/thermion_flutter/lib/src/widgets/src/viewer_widget.dart +++ b/thermion_flutter/thermion_flutter/lib/src/widgets/src/viewer_widget.dart @@ -90,16 +90,7 @@ class _ViewerWidgetState extends State { late final _logger = Logger(runtimeType.toString()); - @override - void initState() { - super.initState(); - _initialization = _createViewer(); - unawaited( - _initialization!.catchError((Object error, StackTrace stack) { - _reportAsyncError('initialization', error, stack); - }), - ); - } + Future _initializeViewer() => _initialization ??= _createViewer(); Future _createViewer() async { // Override options if this widget needs highlights @@ -123,6 +114,7 @@ class _ViewerWidgetState extends State { ); } + if (_disposing) return; final viewer = await ThermionFlutterPlugin.createViewer(); this.viewer = viewer; if (_disposing) { @@ -418,6 +410,12 @@ class _ViewerWidgetState extends State { @override Widget build(BuildContext context) { - return viewport != null ? SizedBox.expand(child: viewport) : widget.initial; + final child = viewport == null + ? widget.initial + : SizedBox.expand(child: viewport); + return ThermionFlutterPlugin.instance.buildInitializationScope( + initialize: _initializeViewer, + child: child, + ); } } diff --git a/thermion_flutter/thermion_flutter/linux/egl_texture.cc b/thermion_flutter/thermion_flutter/linux/egl_texture.cc index 197564d6a..72f6b0b7f 100644 --- a/thermion_flutter/thermion_flutter/linux/egl_texture.cc +++ b/thermion_flutter/thermion_flutter/linux/egl_texture.cc @@ -338,6 +338,20 @@ ThermionTextureGL* thermion_texture_gl_create( return textureGL; } +ThermionTextureGL* thermion_texture_gl_create_context_bootstrap( + uint32_t width, + uint32_t height, + FlTextureRegistrar* registrar) +{ + auto textureGL = THERMION_TEXTURE_GL( + g_object_new(thermion_texture_gl_get_type(), nullptr)); + textureGL->width = width; + textureGL->height = height; + textureGL->registrar = registrar; + textureGL->use_direct_sharing = TRUE; + return textureGL; +} + ThermionTextureGL* thermion_texture_gl_create_shared( uint32_t width, uint32_t height, GLuint gl_texture_id, diff --git a/thermion_flutter/thermion_flutter/linux/egl_texture.h b/thermion_flutter/thermion_flutter/linux/egl_texture.h index 14c285a78..e4c1180f1 100644 --- a/thermion_flutter/thermion_flutter/linux/egl_texture.h +++ b/thermion_flutter/thermion_flutter/linux/egl_texture.h @@ -69,6 +69,13 @@ FLUTTER_PLUGIN_EXPORT ThermionTextureGL* thermion_texture_gl_create( int64_t surface_id, FlTextureRegistrar* registrar); +// A 1x1 texture populated on Flutter's raster thread solely to capture the +// EGLDisplay/EGLContext before Filament initializes. +FLUTTER_PLUGIN_EXPORT ThermionTextureGL* thermion_texture_gl_create_context_bootstrap( + uint32_t width, + uint32_t height, + FlTextureRegistrar* registrar); + // EGLImage bridge path: wraps a GL texture + EGLImage. On first populate, // the EGLImage is imported into a new texture on Flutter's own GL context. FLUTTER_PLUGIN_EXPORT ThermionTextureGL* thermion_texture_gl_create_shared( diff --git a/thermion_flutter/thermion_flutter/linux/thermion_flutter_plugin.cc b/thermion_flutter/thermion_flutter/linux/thermion_flutter_plugin.cc index 074b3ccee..4dce2a3d3 100644 --- a/thermion_flutter/thermion_flutter/linux/thermion_flutter_plugin.cc +++ b/thermion_flutter/thermion_flutter/linux/thermion_flutter_plugin.cc @@ -94,13 +94,14 @@ static void destroy_all_contexts(ThermionFlutterPlugin *self) delete self->vulkan_context; self->vulkan_context = nullptr; } + if (self->utility_egl_context != EGL_NO_CONTEXT && + self->egl_display != EGL_NO_DISPLAY) + { + eglDestroyContext(self->egl_display, self->utility_egl_context); + self->utility_egl_context = EGL_NO_CONTEXT; + } if (self->use_direct_opengl) { - if (self->utility_egl_context != EGL_NO_CONTEXT) - { - eglDestroyContext(self->egl_display, self->utility_egl_context); - self->utility_egl_context = EGL_NO_CONTEXT; - } self->flutter_egl_context = EGL_NO_CONTEXT; self->egl_display = EGL_NO_DISPLAY; self->egl_config = nullptr; @@ -143,40 +144,7 @@ static void ensure_opengl_context(ThermionFlutterPlugin *self) EGLContext flutterCtx = thermion_flutter_render_context; EGLDisplay flutterDpy = thermion_flutter_render_display; - if (flutterCtx == EGL_NO_CONTEXT) { - // No deferred populate yet. Try to get GDK's GL context deterministically - // rather than relying on whatever happens to be current on this thread. - GdkDisplay *gdkDisplay = gdk_display_get_default(); - if (gdkDisplay) { - GdkGLContext *gdkCtx = gdk_gl_context_get_current(); - if (!gdkCtx) { - // Create a temporary GDK GL context and make it current so we can - // query the underlying EGL state. - GdkWindow *gdkWindow = gdk_screen_get_root_window( - gdk_display_get_default_screen(gdkDisplay)); - if (gdkWindow) { - GError *error = nullptr; - gdkCtx = gdk_window_create_gl_context(gdkWindow, &error); - if (gdkCtx && !error) { - gdk_gl_context_make_current(gdkCtx); - std::cerr << "[ThermionGL] Made GDK GL context current" << std::endl; - } else { - if (error) { - std::cerr << "[ThermionGL] GDK GL context creation failed: " - << error->message << std::endl; - g_error_free(error); - } - } - } - } - } - flutterCtx = eglGetCurrentContext(); - flutterDpy = eglGetCurrentDisplay(); - if (flutterCtx != EGL_NO_CONTEXT) { - std::cerr << "[ThermionGL] Using GDK EGL context (no deferred populate yet)" - << std::endl; - } - } else { + if (flutterCtx != EGL_NO_CONTEXT) { std::cerr << "[ThermionGL] Using Flutter render context=" << (void*)flutterCtx << std::endl; } @@ -202,7 +170,9 @@ static void ensure_opengl_context(ThermionFlutterPlugin *self) EGLint configAttribs[] = { EGL_CONFIG_ID, configId, EGL_NONE }; eglChooseConfig(flutterDpy, configAttribs, &config, 1, &numConfigs); - if (numConfigs > 0 && config != nullptr) + if (clientType == EGL_OPENGL_API && + (glMajor > 4 || (glMajor == 4 && glMinor >= 1)) && + numConfigs > 0 && config != nullptr) { // Bind the appropriate API (GL or GLES) to match Flutter eglBindAPI(clientType == EGL_OPENGL_ES_API ? EGL_OPENGL_ES_API : EGL_OPENGL_API); @@ -250,10 +220,18 @@ static void ensure_opengl_context(ThermionFlutterPlugin *self) } } - // Fallback: use the standalone LinuxOpenGLContext (GBM/DMA-BUF path) - std::cerr << "[ThermionGL] Using LinuxOpenGLContext fallback (DMA-BUF)" << std::endl; - self->opengl_context = new thermion::opengl::linux_platform::LinuxOpenGLContext(); - self->backend_type = BACKEND_OPENGL; + // Flutter normally uses GLES on Linux, while Filament requires desktop GL. + // Keep the existing DMA-BUF transport, but create Filament's desktop context + // on Flutter's already-initialized display instead of racing a second one. + if (thermion_flutter_render_display != EGL_NO_DISPLAY) + { + std::cerr << "[ThermionGL] Using Flutter EGLDisplay with DMA-BUF transport" + << std::endl; + self->opengl_context = + new thermion::opengl::linux_platform::LinuxOpenGLContext( + reinterpret_cast(thermion_flutter_render_display)); + self->backend_type = BACKEND_OPENGL; + } } static FlMethodResponse *handle_get_driver_platform(ThermionFlutterPlugin *self, FlMethodCall *method_call) @@ -267,6 +245,13 @@ static FlMethodResponse *handle_get_driver_platform(ThermionFlutterPlugin *self, if (backend == BACKEND_OPENGL) { ensure_opengl_context(self); + if (!self->use_direct_opengl && !self->opengl_context) + { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + "CONTEXT_NOT_READY", + "Display the Linux OpenGL bootstrap texture before initialization", + nullptr)); + } if (self->use_direct_opengl) { platform = reinterpret_cast(self->thermion_platform); @@ -297,6 +282,13 @@ static FlMethodResponse *handle_get_shared_context(ThermionFlutterPlugin *self, if (backend == BACKEND_OPENGL) { ensure_opengl_context(self); + if (!self->use_direct_opengl && !self->opengl_context) + { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + "CONTEXT_NOT_READY", + "Display the Linux OpenGL bootstrap texture before initialization", + nullptr)); + } if (self->use_direct_opengl) { // Direct sharing: return Flutter's context (Filament contexts share with it) @@ -507,6 +499,14 @@ static FlMethodResponse *handle_create_texture_opengl(ThermionFlutterPlugin *sel { ensure_opengl_context(self); + if (!self->use_direct_opengl && !self->opengl_context) + { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + "CONTEXT_NOT_READY", + "Display the Linux OpenGL bootstrap texture before initialization", + nullptr)); + } + if (self->use_direct_opengl) { return handle_create_texture_opengl_direct(self, width, height); @@ -514,6 +514,31 @@ static FlMethodResponse *handle_create_texture_opengl(ThermionFlutterPlugin *sel return handle_create_texture_opengl_dmabuf(self, width, height); } +static FlMethodResponse *handle_create_context_bootstrap( + ThermionFlutterPlugin *self, FlMethodCall *method_call) +{ + FlValue *args = fl_method_call_get_args(method_call); + int width = fl_value_get_int(fl_value_get_list_value(args, 0)); + int height = fl_value_get_int(fl_value_get_list_value(args, 1)); + ThermionTextureGL *textureGL = + thermion_texture_gl_create_context_bootstrap( + static_cast(width), static_cast(height), + self->texture_registrar); + FlTexture *flTexture = FL_TEXTURE(textureGL); + if (!fl_texture_registrar_register_texture(self->texture_registrar, flTexture)) + { + g_object_unref(textureGL); + return FL_METHOD_RESPONSE(fl_method_error_response_new( + "REGISTER_FAILED", "Failed to register context bootstrap", nullptr)); + } + self->textures->push_back(textureGL); + self->backend_type = BACKEND_OPENGL; + fl_texture_registrar_mark_texture_frame_available( + self->texture_registrar, flTexture); + return FL_METHOD_RESPONSE(fl_method_success_response_new( + fl_value_new_int(fl_texture_get_id(flTexture)))); +} + static FlMethodResponse *handle_create_texture(ThermionFlutterPlugin *self, FlMethodCall *method_call) { @@ -699,6 +724,10 @@ static void thermion_flutter_plugin_handle_method_call( { response = handle_get_driver_platform(self, method_call); } + else if (strcmp(method, "createContextBootstrap") == 0) + { + response = handle_create_context_bootstrap(self, method_call); + } else if (strcmp(method, "getSharedContext") == 0) { response = handle_get_shared_context(self, method_call); diff --git a/thermion_flutter/thermion_flutter/test/thermion_flutter_plugin_initializer_test.dart b/thermion_flutter/thermion_flutter/test/thermion_flutter_plugin_initializer_test.dart new file mode 100644 index 000000000..8299b279a --- /dev/null +++ b/thermion_flutter/thermion_flutter/test/thermion_flutter_plugin_initializer_test.dart @@ -0,0 +1,114 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:thermion_flutter/src/platform/src/platform_texture_descriptor.dart'; +import 'package:thermion_flutter/src/platform/src/thermion_flutter_plugin_initializer.dart'; + +void main() { + testWidgets( + 'initializes only after the bootstrap texture is ready and then destroys it', + (tester) async { + final descriptor = _BootstrapDescriptor(); + final events = []; + descriptor.events = events; + + await tester.pumpWidget( + ThermionFlutterPluginInitializer( + createContextBootstrap: () async { + events.add('create'); + return descriptor; + }, + destroyContextBootstrap: (descriptor) async { + events.add('destroy'); + await descriptor.destroy(); + }, + initialize: () async { + events.add('initialize'); + }, + child: const SizedBox.expand(), + ), + ); + await tester.pump(); + + expect(find.byType(Texture), findsOneWidget); + expect(events, ['create', 'await']); + + descriptor.completeReady(42); + await tester.pump(); + await tester.pump(); + + expect(descriptor.hardwareId, 42); + expect(find.byType(Texture), findsNothing); + expect(events, ['create', 'await', 'initialize', 'destroy']); + expect(descriptor.destroyCount, 1); + }, + ); + + testWidgets('disposing cancels a pending bootstrap exactly once', ( + tester, + ) async { + final descriptor = _BootstrapDescriptor(); + var initializeCount = 0; + + await tester.pumpWidget( + ThermionFlutterPluginInitializer( + createContextBootstrap: () async => descriptor, + initialize: () async { + initializeCount++; + }, + child: const SizedBox.expand(), + ), + ); + await tester.pump(); + expect(find.byType(Texture), findsOneWidget); + + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pump(); + + expect(descriptor.destroyCount, 1); + expect(initializeCount, 0); + expect(tester.takeException(), isNull); + }); +} + +class _BootstrapDescriptor extends PlatformTextureDescriptor { + _BootstrapDescriptor() + : super(flutterTextureId: 7, hardwareId: 0, width: 1, height: 1); + + final _ready = Completer(); + int destroyCount = 0; + bool _destroyed = false; + List? events; + + @override + bool get deferred => true; + + @override + bool get destroyed => _destroyed; + + @override + Future awaitTextureReady() { + events?.add('await'); + return _ready.future; + } + + void completeReady(int textureId) { + if (!_ready.isCompleted) { + _ready.complete(textureId); + } + } + + @override + Future destroy() async { + if (_destroyed) return; + _destroyed = true; + destroyCount++; + if (!_ready.isCompleted) { + _ready.completeError(StateError('destroyed')); + } + } + + @override + void markTextureFrameAvailable() {} +} From a2cac9db7dfb48ceacc470500e3846b76d7207d4 Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Thu, 20 Aug 2026 16:43:01 +0800 Subject: [PATCH 02/13] fix(linux): isolate desktop EGL operations --- .../include/opengl/linux/LinuxOpenGLContext.h | 14 +- .../include/opengl/linux/LinuxOpenGLTexture.h | 4 +- .../src/opengl/linux/LinuxOpenGLContext.cpp | 387 +++++++++--- .../src/opengl/linux/LinuxOpenGLTexture.cpp | 89 ++- .../thermion_flutter/linux/egl_texture.cc | 128 ++-- .../thermion_flutter/linux/egl_texture.h | 20 +- .../linux/thermion_flutter_plugin.cc | 593 ++++++++++++++---- 7 files changed, 934 insertions(+), 301 deletions(-) diff --git a/thermion_dart/native/include/opengl/linux/LinuxOpenGLContext.h b/thermion_dart/native/include/opengl/linux/LinuxOpenGLContext.h index eb3d98d10..7fc0c7171 100644 --- a/thermion_dart/native/include/opengl/linux/LinuxOpenGLContext.h +++ b/thermion_dart/native/include/opengl/linux/LinuxOpenGLContext.h @@ -15,15 +15,21 @@ namespace thermion::opengl::linux_platform { * PlatformEGL can create its own context in the same share group — GL texture * IDs are then valid in both contexts. * - * GetPlatform() returns nullptr so Filament auto-creates a default PlatformEGL. + * GetPlatform() returns Thermion's EGLHeadless platform bound to the same + * EGLDisplay as the producer context. */ class LinuxOpenGLContext { public: - // When provided, eglDisplay is borrowed from Flutter and is never - // terminated by this context. + // If eglDisplay is non-null, it is borrowed and must already be + // initialized. This is the preferred Flutter path: Filament gets a + // desktop-GL context on Flutter's existing EGLDisplay without starting a + // second NVIDIA EGL display alongside the raster thread. explicit LinuxOpenGLContext(void* eglDisplay = nullptr); ~LinuxOpenGLContext(); + bool IsValid() const; + const char* GetLastError() const; + int64_t CreateRenderingSurface(uint32_t width, uint32_t height); void DestroyRenderingSurface(int64_t surfaceId); @@ -31,7 +37,7 @@ class LinuxOpenGLContext { SurfaceExportInfo GetSurfaceExportInfo(int64_t surfaceId); void* GetSharedContext(); // Returns our EGLContext for Filament sharing - void* GetPlatform(); // Returns nullptr (Filament creates default PlatformEGL) + void* GetPlatform(); // Returns ThermionPlatformEGLHeadless private: class Impl; diff --git a/thermion_dart/native/include/opengl/linux/LinuxOpenGLTexture.h b/thermion_dart/native/include/opengl/linux/LinuxOpenGLTexture.h index f54ba0a6f..eca685127 100644 --- a/thermion_dart/native/include/opengl/linux/LinuxOpenGLTexture.h +++ b/thermion_dart/native/include/opengl/linux/LinuxOpenGLTexture.h @@ -30,7 +30,7 @@ class LinuxOpenGLTexture { ~LinuxOpenGLTexture(); static std::unique_ptr create( - EGLDisplay display, EGLContext context, + EGLDisplay display, EGLContext context, EGLSurface surface, struct gbm_device* gbm, uint32_t width, uint32_t height); GLuint GetGLTextureId() const { return _glTextureId; } @@ -58,6 +58,8 @@ class LinuxOpenGLTexture { // Store display for cleanup EGLDisplay _display = EGL_NO_DISPLAY; + EGLContext _context = EGL_NO_CONTEXT; + EGLSurface _surface = EGL_NO_SURFACE; }; } // namespace thermion::opengl::linux_platform diff --git a/thermion_dart/native/src/opengl/linux/LinuxOpenGLContext.cpp b/thermion_dart/native/src/opengl/linux/LinuxOpenGLContext.cpp index 6fb0d4729..718df0802 100644 --- a/thermion_dart/native/src/opengl/linux/LinuxOpenGLContext.cpp +++ b/thermion_dart/native/src/opengl/linux/LinuxOpenGLContext.cpp @@ -1,9 +1,17 @@ #include "opengl/linux/LinuxOpenGLContext.h" #include "opengl/linux/LinuxOpenGLTexture.h" +#include +#include +#include +#include #include -#include #include +#include +#include +#include +#include +#include #include #include @@ -19,26 +27,82 @@ namespace thermion::opengl::linux_platform { +class ScopedEglThreadState { +public: + ScopedEglThreadState() + : _display(eglGetCurrentDisplay()), + _context(eglGetCurrentContext()), + _draw(eglGetCurrentSurface(EGL_DRAW)), + _read(eglGetCurrentSurface(EGL_READ)), + _api(eglQueryAPI()) { + if (_context != EGL_NO_CONTEXT && _display != EGL_NO_DISPLAY) { + eglMakeCurrent( + _display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + } + } + + ~ScopedEglThreadState() { + EGLDisplay currentDisplay = eglGetCurrentDisplay(); + if (currentDisplay != EGL_NO_DISPLAY) { + eglMakeCurrent(currentDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, + EGL_NO_CONTEXT); + } + eglBindAPI(_api); + if (_context != EGL_NO_CONTEXT && _display != EGL_NO_DISPLAY) { + eglMakeCurrent(_display, _draw, _read, _context); + } + } + + ScopedEglThreadState(const ScopedEglThreadState&) = delete; + ScopedEglThreadState& operator=(const ScopedEglThreadState&) = delete; + +private: + EGLDisplay _display; + EGLContext _context; + EGLSurface _draw; + EGLSurface _read; + EGLenum _api; +}; + class LinuxOpenGLContext::Impl { public: ~Impl() { - _surfaces.clear(); + // The desktop producer context never becomes current on Flutter's + // platform or raster threads. Keep destruction on an isolated EGL + // thread for the same reason as initialization and texture creation. + if (_eglThread.joinable()) { + RunOnEglThread([this]() { + _surfaces.clear(); + }); + } if (_platform) { ThermionPlatformEGLHeadless_Destroy(_platform); _platform = nullptr; } - if (_context != EGL_NO_CONTEXT && _display != EGL_NO_DISPLAY) { - eglMakeCurrent(_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - eglDestroyContext(_display, _context); - _context = EGL_NO_CONTEXT; - } - - if (_ownsDisplay && _display != EGL_NO_DISPLAY) { - eglTerminate(_display); + if (_eglThread.joinable()) { + RunOnEglThread([this]() { + if (_context != EGL_NO_CONTEXT && + _display != EGL_NO_DISPLAY) { + eglBindAPI(EGL_OPENGL_API); + eglMakeCurrent( + _display, EGL_NO_SURFACE, EGL_NO_SURFACE, + EGL_NO_CONTEXT); + if (_producerSurface != EGL_NO_SURFACE) { + eglDestroySurface(_display, _producerSurface); + _producerSurface = EGL_NO_SURFACE; + } + eglDestroyContext(_display, _context); + _context = EGL_NO_CONTEXT; + } + if (_ownsDisplay && _display != EGL_NO_DISPLAY) { + eglTerminate(_display); + } + _display = EGL_NO_DISPLAY; + }); + StopEglThread(); } - _display = EGL_NO_DISPLAY; if (_gbmDevice) { gbm_device_destroy(_gbmDevice); @@ -57,6 +121,7 @@ class LinuxOpenGLContext::Impl { // Step 1: Open DRM render node _drmFd = open("/dev/dri/renderD128", O_RDWR); if (_drmFd < 0) { + _lastError = "Failed to open /dev/dri/renderD128"; LOG_ERROR("Failed to open /dev/dri/renderD128"); return; } @@ -65,6 +130,7 @@ class LinuxOpenGLContext::Impl { // Step 2: Create GBM device _gbmDevice = gbm_create_device(_drmFd); if (!_gbmDevice) { + _lastError = "Failed to create GBM device"; LOG_ERROR("Failed to create GBM device"); close(_drmFd); _drmFd = -1; @@ -72,36 +138,53 @@ class LinuxOpenGLContext::Impl { } std::cerr << "[ThermionGL:Context] GBM device created OK" << std::endl; - // Step 3: Get EGL display from the GBM device. - // Using eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, ...) ties the - // display to the GPU that owns the render node. On NVIDIA systems - // this selects NVIDIA's EGL instead of Mesa's software fallback. + // Step 3: Reuse Flutter's initialized display when available. NVIDIA's + // EGL implementation can corrupt the concurrently rendering Flutter + // context if another platform display is initialized in-process. _display = static_cast(borrowedDisplay); - if (_display == EGL_NO_DISPLAY) { - _ownsDisplay = true; + if (_display != EGL_NO_DISPLAY) { + _ownsDisplay = false; + // eglInitialize is idempotent for an initialized EGLDisplay. Do + // this only after populate() has captured Flutter's real display: + // on NVIDIA it also makes the desktop-GL client API usable on the + // display that Flutter initialized for GLES. + EGLint major = 0; + EGLint minor = 0; + if (!eglInitialize(_display, &major, &minor)) { + _lastError = + "Failed to initialize Flutter's captured EGLDisplay"; + LOG_ERROR("Failed to initialize Flutter EGL display"); + _display = EGL_NO_DISPLAY; + return; + } + std::cerr << "[ThermionGL:Context] Using Flutter EGL display=" + << _display << " (" << major << "." << minor << ")" + << std::endl; + } else { + // Non-Flutter fallback: obtain a display tied to the GBM device. PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT = - (PFNEGLGETPLATFORMDISPLAYEXTPROC)eglGetProcAddress("eglGetPlatformDisplayEXT"); + (PFNEGLGETPLATFORMDISPLAYEXTPROC)eglGetProcAddress( + "eglGetPlatformDisplayEXT"); if (eglGetPlatformDisplayEXT) { - _display = eglGetPlatformDisplayEXT(EGL_PLATFORM_GBM_KHR, - _gbmDevice, nullptr); + _display = eglGetPlatformDisplayEXT( + EGL_PLATFORM_GBM_KHR, _gbmDevice, nullptr); } if (_display == EGL_NO_DISPLAY) { - // Fallback to default display - std::cerr << "[ThermionGL:Context] GBM platform display failed, " - << "trying default" << std::endl; + std::cerr + << "[ThermionGL:Context] GBM platform display failed, " + "trying default" + << std::endl; _display = eglGetDisplay(EGL_DEFAULT_DISPLAY); } - } else { - std::cerr << "[ThermionGL:Context] Reusing Flutter EGLDisplay" - << std::endl; - } - if (_display == EGL_NO_DISPLAY) { - LOG_ERROR("Failed to get EGL display"); - return; - } - { + if (_display == EGL_NO_DISPLAY) { + _lastError = "Failed to obtain an EGLDisplay"; + LOG_ERROR("Failed to get EGL display"); + return; + } + _ownsDisplay = true; EGLint major, minor; if (!eglInitialize(_display, &major, &minor)) { + _lastError = "Failed to initialize EGLDisplay"; LOG_ERROR("Failed to initialize EGL display"); _display = EGL_NO_DISPLAY; return; @@ -110,75 +193,129 @@ class LinuxOpenGLContext::Impl { << major << "." << minor << std::endl; } - // Step 4: Choose EGL config - // Must bind EGL_OPENGL_API (not ES) to match Filament's PlatformEGLHeadless - // which uses full OpenGL 4.1 on Linux desktop. - eglBindAPI(EGL_OPENGL_API); - - EGLint configAttribs[] = { - EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, - EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, - EGL_RED_SIZE, 8, - EGL_GREEN_SIZE, 8, - EGL_BLUE_SIZE, 8, - EGL_ALPHA_SIZE, 8, - EGL_NONE - }; - - EGLConfig config; - EGLint numConfigs; - if (!eglChooseConfig(_display, configAttribs, &config, 1, &numConfigs) || numConfigs == 0) { - LOG_ERROR("Failed to choose EGL config"); - return; - } + // Initialize desktop EGL on an isolated thread. Flutter's platform + // thread can retain GTK/GDK EGL state, and NVIDIA returns + // EGL_BAD_ACCESS when a second client API is activated there. + StartEglThread(); + RunOnEglThread([this]() { + // Step 4: Choose EGL config + // Must bind EGL_OPENGL_API (not ES) to match Filament's + // PlatformEGLHeadless which uses full OpenGL 4.1 on Linux desktop. + ScopedEglThreadState eglThreadState; + if (!eglBindAPI(EGL_OPENGL_API)) { + _lastError = "Failed to bind desktop OpenGL"; + LOG_ERROR("Failed to bind desktop OpenGL"); + return; + } - // Step 5: Create EGL context (OpenGL 4.1 to match Filament's PlatformEGLHeadless) - EGLint contextAttribs[] = { - EGL_CONTEXT_MAJOR_VERSION, 4, - EGL_CONTEXT_MINOR_VERSION, 1, - EGL_NONE - }; + EGLint configAttribs[] = { + EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, + EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_NONE + }; - _context = eglCreateContext(_display, config, EGL_NO_CONTEXT, contextAttribs); - if (_context == EGL_NO_CONTEXT) { - EGLint err = eglGetError(); - std::cerr << "[ThermionGL:Context] Failed to create EGL context, error: 0x" - << std::hex << err << std::dec << std::endl; - return; - } + EGLConfig config; + EGLint numConfigs; + if (!eglChooseConfig( + _display, configAttribs, &config, 1, &numConfigs) || + numConfigs == 0) { + _lastError = "Failed to choose a desktop OpenGL EGLConfig"; + LOG_ERROR("Failed to choose EGL config"); + return; + } - // Verify the context works by briefly making it current, then restore - // whatever context was active (likely Flutter/GDK's). - EGLDisplay prevDisplay = eglGetCurrentDisplay(); - EGLContext prevContext = eglGetCurrentContext(); - EGLSurface prevDraw = eglGetCurrentSurface(EGL_DRAW); - EGLSurface prevRead = eglGetCurrentSurface(EGL_READ); - - if (!eglMakeCurrent(_display, EGL_NO_SURFACE, EGL_NO_SURFACE, _context)) { - // Some drivers require a pbuffer surface - EGLint pbufferAttribs[] = { - EGL_WIDTH, 1, - EGL_HEIGHT, 1, + // Step 5: Create EGL context (OpenGL 4.1 to match Filament's + // PlatformEGLHeadless). + EGLint contextAttribs[] = { + EGL_CONTEXT_MAJOR_VERSION, 4, + EGL_CONTEXT_MINOR_VERSION, 1, EGL_NONE }; - EGLSurface pbuffer = eglCreatePbufferSurface(_display, config, pbufferAttribs); - if (pbuffer != EGL_NO_SURFACE) { - eglMakeCurrent(_display, pbuffer, pbuffer, _context); + + _context = eglCreateContext( + _display, config, EGL_NO_CONTEXT, contextAttribs); + if (_context == EGL_NO_CONTEXT) { + _lastError = "Failed to create desktop OpenGL 4.1 EGLContext"; + EGLint err = eglGetError(); + std::cerr + << "[ThermionGL:Context] Failed to create EGL context, " + "error: 0x" + << std::hex << err << std::dec << std::endl; + return; + } + + EGLint surfacelessError = EGL_SUCCESS; + if (!eglMakeCurrent( + _display, EGL_NO_SURFACE, EGL_NO_SURFACE, _context)) { + surfacelessError = eglGetError(); + // Some drivers require a pbuffer surface. + EGLint pbufferAttribs[] = { + EGL_WIDTH, 1, + EGL_HEIGHT, 1, + EGL_NONE + }; + _producerSurface = + eglCreatePbufferSurface(_display, config, pbufferAttribs); + if (_producerSurface == EGL_NO_SURFACE) { + EGLint pbufferError = eglGetError(); + std::ostringstream message; + message + << "Failed to create desktop OpenGL producer pbuffer " + << "(surfaceless error 0x" << std::hex + << surfacelessError << ", pbuffer error 0x" + << pbufferError << ")"; + _lastError = message.str(); + LOG_ERROR("Failed to create EGL producer pbuffer"); + eglDestroyContext(_display, _context); + _context = EGL_NO_CONTEXT; + return; + } + if (!eglMakeCurrent( + _display, _producerSurface, _producerSurface, + _context)) { + EGLint pbufferCurrentError = eglGetError(); + std::ostringstream message; + message + << "Failed to make desktop OpenGL producer context " + "current (surfaceless error 0x" + << std::hex << surfacelessError + << ", pbuffer error 0x" << pbufferCurrentError << ")"; + _lastError = message.str(); + LOG_ERROR("Failed to make EGL context current"); + eglDestroySurface(_display, _producerSurface); + _producerSurface = EGL_NO_SURFACE; + eglDestroyContext(_display, _context); + _context = EGL_NO_CONTEXT; + return; + } } - } - std::cerr << "[ThermionGL:Context] EGL context created OK" << std::endl; + std::cerr << "[ThermionGL:Context] EGL context created OK" + << std::endl; + }); + } - // Restore the previous context so we don't clobber Flutter/GDK's state - if (prevContext != EGL_NO_CONTEXT && prevDisplay != EGL_NO_DISPLAY) { - eglMakeCurrent(prevDisplay, prevDraw, prevRead, prevContext); - } else { - eglMakeCurrent(_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - } + bool IsValid() const { + return _display != EGL_NO_DISPLAY && + _context != EGL_NO_CONTEXT && + _gbmDevice != nullptr; + } + + const char* GetLastError() const { + return _lastError.c_str(); } int64_t CreateRenderingSurface(uint32_t width, uint32_t height) { - auto texture = LinuxOpenGLTexture::create(_display, _context, _gbmDevice, width, height); + std::unique_ptr texture; + RunOnEglThread([&]() { + texture = LinuxOpenGLTexture::create( + _display, _context, _producerSurface, _gbmDevice, width, + height); + }); if (!texture) { LOG_ERROR("Failed to create OpenGL rendering surface"); return -1; @@ -195,7 +332,9 @@ class LinuxOpenGLContext::Impl { } void DestroyRenderingSurface(int64_t surfaceId) { - _surfaces.erase(surfaceId); + RunOnEglThread([this, surfaceId]() { + _surfaces.erase(surfaceId); + }); } uint32_t GetGLTextureId(int64_t surfaceId) { @@ -232,12 +371,68 @@ class LinuxOpenGLContext::Impl { } private: + void StartEglThread() { + _eglThread = std::thread([this]() { + while (true) { + std::function task; + { + std::unique_lock lock(_taskMutex); + _taskReady.wait(lock, [this]() { + return _stopEglThread || !_tasks.empty(); + }); + if (_stopEglThread && _tasks.empty()) { + break; + } + task = std::move(_tasks.front()); + _tasks.pop_front(); + } + task(); + } + eglReleaseThread(); + }); + } + + void RunOnEglThread(std::function task) { + auto completed = std::make_shared>(); + auto result = completed->get_future(); + { + std::lock_guard lock(_taskMutex); + _tasks.emplace_back( + [task = std::move(task), completed = std::move(completed)]() { + try { + task(); + completed->set_value(); + } catch (...) { + completed->set_exception(std::current_exception()); + } + }); + } + _taskReady.notify_one(); + result.get(); + } + + void StopEglThread() { + { + std::lock_guard lock(_taskMutex); + _stopEglThread = true; + } + _taskReady.notify_one(); + _eglThread.join(); + } + EGLDisplay _display = EGL_NO_DISPLAY; - bool _ownsDisplay = false; EGLContext _context = EGL_NO_CONTEXT; + EGLSurface _producerSurface = EGL_NO_SURFACE; + bool _ownsDisplay = false; + std::string _lastError; struct gbm_device* _gbmDevice = nullptr; int _drmFd = -1; ThermionPlatformEGLHeadlessHandle _platform = nullptr; + std::thread _eglThread; + std::mutex _taskMutex; + std::condition_variable _taskReady; + std::deque> _tasks; + bool _stopEglThread = false; std::unordered_map> _surfaces; int64_t _nextSurfaceId = 1; @@ -250,6 +445,14 @@ LinuxOpenGLContext::LinuxOpenGLContext(void* eglDisplay) LinuxOpenGLContext::~LinuxOpenGLContext() = default; +bool LinuxOpenGLContext::IsValid() const { + return pImpl->IsValid(); +} + +const char* LinuxOpenGLContext::GetLastError() const { + return pImpl->GetLastError(); +} + int64_t LinuxOpenGLContext::CreateRenderingSurface(uint32_t width, uint32_t height) { return pImpl->CreateRenderingSurface(width, height); } diff --git a/thermion_dart/native/src/opengl/linux/LinuxOpenGLTexture.cpp b/thermion_dart/native/src/opengl/linux/LinuxOpenGLTexture.cpp index ee27d9560..d629db0d6 100644 --- a/thermion_dart/native/src/opengl/linux/LinuxOpenGLTexture.cpp +++ b/thermion_dart/native/src/opengl/linux/LinuxOpenGLTexture.cpp @@ -26,9 +26,68 @@ static void ensureExtensionFunctions() { namespace thermion::opengl::linux_platform { +class ScopedEglContext { +public: + ScopedEglContext( + EGLDisplay display, EGLContext context, EGLSurface surface) + : _targetDisplay(display), + _targetSurface(surface), + _previousDisplay(eglGetCurrentDisplay()), + _previousContext(eglGetCurrentContext()), + _previousDraw(eglGetCurrentSurface(EGL_DRAW)), + _previousRead(eglGetCurrentSurface(EGL_READ)), + _previousApi(eglQueryAPI()) { + if (_previousContext != EGL_NO_CONTEXT && + _previousDisplay != EGL_NO_DISPLAY) { + eglMakeCurrent(_previousDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, + EGL_NO_CONTEXT); + } + eglBindAPI(EGL_OPENGL_API); + _current = eglMakeCurrent( + _targetDisplay, _targetSurface, _targetSurface, context); + } + + ~ScopedEglContext() { + if (_targetDisplay != EGL_NO_DISPLAY) { + eglMakeCurrent(_targetDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, + EGL_NO_CONTEXT); + } + eglBindAPI(_previousApi); + if (_previousContext != EGL_NO_CONTEXT && + _previousDisplay != EGL_NO_DISPLAY) { + eglMakeCurrent(_previousDisplay, _previousDraw, _previousRead, + _previousContext); + } + } + + bool current() const { return _current == EGL_TRUE; } + + ScopedEglContext(const ScopedEglContext&) = delete; + ScopedEglContext& operator=(const ScopedEglContext&) = delete; + +private: + EGLDisplay _targetDisplay; + EGLSurface _targetSurface; + EGLDisplay _previousDisplay; + EGLContext _previousContext; + EGLSurface _previousDraw; + EGLSurface _previousRead; + EGLenum _previousApi; + EGLBoolean _current = EGL_FALSE; +}; + LinuxOpenGLTexture::~LinuxOpenGLTexture() { if (_glTextureId != 0) { - glDeleteTextures(1, &_glTextureId); + ScopedEglContext context(_display, _context, _surface); + if (context.current()) { + glDeleteTextures(1, &_glTextureId); + } else { + std::cerr + << "[ThermionGL:Texture] Failed to make owner context current " + "while deleting texture " + << _glTextureId << ", EGL error: 0x" << std::hex + << eglGetError() << std::dec << std::endl; + } _glTextureId = 0; } @@ -49,7 +108,7 @@ LinuxOpenGLTexture::~LinuxOpenGLTexture() { } std::unique_ptr LinuxOpenGLTexture::create( - EGLDisplay display, EGLContext context, + EGLDisplay display, EGLContext context, EGLSurface surface, struct gbm_device* gbm, uint32_t width, uint32_t height) { ensureExtensionFunctions(); @@ -128,13 +187,16 @@ std::unique_ptr LinuxOpenGLTexture::create( return nullptr; } - // Step 4: Make our context current so we can create GL objects - EGLSurface prevDrawSurface = eglGetCurrentSurface(EGL_DRAW); - EGLSurface prevReadSurface = eglGetCurrentSurface(EGL_READ); - EGLContext prevContext = eglGetCurrentContext(); - - if (eglGetCurrentContext() != context) { - eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, context); + // Step 4: Make our context current so we can create GL objects. + ScopedEglContext scopedContext(display, context, surface); + if (!scopedContext.current()) { + std::cerr << "[ThermionGL:Texture] Failed to make owner context " + "current, EGL error: 0x" + << std::hex << eglGetError() << std::dec << std::endl; + s_eglDestroyImageKHR(display, eglImage); + close(dmaBufFd); + gbm_bo_destroy(bo); + return nullptr; } // Step 5: Create GL texture and bind EGLImage to it @@ -159,18 +221,11 @@ std::unique_ptr LinuxOpenGLTexture::create( s_eglDestroyImageKHR(display, eglImage); close(dmaBufFd); gbm_bo_destroy(bo); - // Restore previous context - eglMakeCurrent(display, prevDrawSurface, prevReadSurface, prevContext); return nullptr; } glBindTexture(GL_TEXTURE_2D, 0); - // Restore previous context - if (prevContext != context) { - eglMakeCurrent(display, prevDrawSurface, prevReadSurface, prevContext); - } - std::cerr << "[ThermionGL:Texture] GL texture created: id=" << glTextureId << " " << width << "x" << height << std::endl; @@ -187,6 +242,8 @@ std::unique_ptr LinuxOpenGLTexture::create( texture->_drmFormat = drmFormat; texture->_drmModifier = drmModifier; texture->_display = display; + texture->_context = context; + texture->_surface = surface; return texture; } diff --git a/thermion_flutter/thermion_flutter/linux/egl_texture.cc b/thermion_flutter/thermion_flutter/linux/egl_texture.cc index 72f6b0b7f..c0c5db7a6 100644 --- a/thermion_flutter/thermion_flutter/linux/egl_texture.cc +++ b/thermion_flutter/thermion_flutter/linux/egl_texture.cc @@ -19,6 +19,22 @@ static PFNEGLCREATEIMAGEKHRPROC s_eglCreateImageKHR = nullptr; static PFNEGLDESTROYIMAGEKHRPROC s_eglDestroyImageKHR = nullptr; static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC s_glEGLImageTargetTexture2DOES = nullptr; +struct DeferredReadyResponse { + FlMethodCall* method_call; + int64_t texture_id; +}; + +static gboolean respond_texture_ready(gpointer user_data) { + auto* response = static_cast(user_data); + g_autoptr(FlValue) result = fl_value_new_int(response->texture_id); + fl_method_call_respond( + response->method_call, + FL_METHOD_RESPONSE(fl_method_success_response_new(result)), nullptr); + g_object_unref(response->method_call); + delete response; + return G_SOURCE_REMOVE; +} + static void ensure_egl_procs() { if (!s_eglCreateImageKHR) { s_eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR"); @@ -69,49 +85,69 @@ thermion_texture_populate(FlTextureGL *texture, // Flutter's render context (which is guaranteed current during populate). if (self->use_direct_sharing) { if (self->gl_texture_id == 0) { - // First populate — create GL texture on Flutter's render context - // Fill with solid green for visual verification - size_t pixelCount = self->width * self->height; - std::vector greenPixels(pixelCount * 4); - for (size_t i = 0; i < pixelCount; i++) { - greenPixels[i * 4 + 0] = 0; // R - greenPixels[i * 4 + 1] = 255; // G - greenPixels[i * 4 + 2] = 0; // B - greenPixels[i * 4 + 3] = 255; // A + EGLContext flutterContext = eglGetCurrentContext(); + EGLDisplay flutterDisplay = eglGetCurrentDisplay(); + if (flutterContext == EGL_NO_CONTEXT || + flutterDisplay == EGL_NO_DISPLAY) { + g_set_error(error, g_quark_from_static_string("thermion"), 1, + "Flutter did not make an EGL context current"); + return FALSE; } + + // First populate — create a transparent GL texture on Flutter's + // raster context. Its purpose is to import the actual context, not + // to display application content. + std::vector pixels(self->width * self->height * 4, 0); + while (glGetError() != GL_NO_ERROR) {} glGenTextures(1, &self->gl_texture_id); glBindTexture(GL_TEXTURE_2D, self->gl_texture_id); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, self->width, self->height, 0, - GL_RGBA, GL_UNSIGNED_BYTE, greenPixels.data()); + GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glBindTexture(GL_TEXTURE_2D, 0); + GLenum glError = glGetError(); + if (self->gl_texture_id == 0 || glError != GL_NO_ERROR) { + if (self->gl_texture_id != 0) { + glDeleteTextures(1, &self->gl_texture_id); + self->gl_texture_id = 0; + } + g_set_error( + error, g_quark_from_static_string("thermion"), 2, + "Failed to create Flutter context bootstrap texture " + "(GL error 0x%x)", + glError); + return FALSE; + } self->surface_id = static_cast(self->gl_texture_id); - TRACE( "[DirectPop] Created deferred GL texture %u (%ux%u) on ctx=%p\n", + TRACE( "[DirectPop] Created bootstrap GL texture %u (%ux%u) on ctx=%p\n", self->gl_texture_id, self->width, self->height, - (void*)eglGetCurrentContext()); + (void*)flutterContext); // Capture Flutter's render context for Filament initialization. // This is the ONLY place where Flutter's render context is current. - if (thermion_flutter_render_context == EGL_NO_CONTEXT) { - thermion_flutter_render_context = eglGetCurrentContext(); - thermion_flutter_render_display = eglGetCurrentDisplay(); - TRACE( "[DirectPop] Captured Flutter render context=%p display=%p\n", - (void*)thermion_flutter_render_context, - (void*)thermion_flutter_render_display); - } - - // Resolve pending awaitTextureReady method call + thermion_flutter_render_context = flutterContext; + thermion_flutter_render_display = flutterDisplay; + TRACE( "[DirectPop] Captured Flutter render context=%p display=%p\n", + (void*)thermion_flutter_render_context, + (void*)thermion_flutter_render_display); + + // Do not resolve awaitTextureReady from inside populate(). Dart + // may immediately initialize another EGL client API when the + // Future completes. Queue the response on Flutter's platform loop + // so this raster callback has fully returned first. if (self->pending_ready_call) { - g_autoptr(FlValue) result = fl_value_new_int( - static_cast(self->gl_texture_id)); - fl_method_call_respond(self->pending_ready_call, - FL_METHOD_RESPONSE(fl_method_success_response_new(result)), nullptr); - g_object_unref(self->pending_ready_call); + auto* response = new DeferredReadyResponse{ + self->pending_ready_call, + static_cast(self->gl_texture_id), + }; self->pending_ready_call = nullptr; + g_idle_add_full( + G_PRIORITY_DEFAULT_IDLE, respond_texture_ready, response, + nullptr); } } @@ -122,10 +158,9 @@ thermion_texture_populate(FlTextureGL *texture, return TRUE; } - // EGLImage bridge path: the source texture lives on Filament's context, - // but Flutter's rendering context is in a different share group. - // Bridge via EGLImage: on first populate, import the EGLImage into a new - // GL texture on Flutter's current context. + // EGLImage bridge path: the source texture lives on Filament's desktop + // context. On first populate, bind its EGLImage to a Flutter-owned texture + // name on Flutter's current context. if (self->use_egl_image) { if (!self->initialized) { ensure_egl_procs(); @@ -316,6 +351,7 @@ void thermion_texture_gl_init(ThermionTextureGL* self) { self->surface_id = -1; self->use_egl_image = FALSE; self->use_direct_sharing = FALSE; + self->is_context_bootstrap = FALSE; self->pending_ready_call = nullptr; } @@ -338,20 +374,6 @@ ThermionTextureGL* thermion_texture_gl_create( return textureGL; } -ThermionTextureGL* thermion_texture_gl_create_context_bootstrap( - uint32_t width, - uint32_t height, - FlTextureRegistrar* registrar) -{ - auto textureGL = THERMION_TEXTURE_GL( - g_object_new(thermion_texture_gl_get_type(), nullptr)); - textureGL->width = width; - textureGL->height = height; - textureGL->registrar = registrar; - textureGL->use_direct_sharing = TRUE; - return textureGL; -} - ThermionTextureGL* thermion_texture_gl_create_shared( uint32_t width, uint32_t height, GLuint gl_texture_id, @@ -372,6 +394,24 @@ ThermionTextureGL* thermion_texture_gl_create_shared( return textureGL; } +ThermionTextureGL* thermion_texture_gl_create_context_bootstrap( + uint32_t width, uint32_t height, + FlTextureRegistrar* registrar) +{ + auto textureGL = THERMION_TEXTURE_GL( + g_object_new(thermion_texture_gl_get_type(), nullptr)); + textureGL->width = width; + textureGL->height = height; + textureGL->registrar = registrar; + textureGL->use_direct_sharing = TRUE; + textureGL->is_context_bootstrap = TRUE; + // populate() creates gl_texture_id while Flutter's raster context is + // current and resolves the pending awaitTextureReady call. + textureGL->gl_texture_id = 0; + + return textureGL; +} + void thermion_texture_gl_destroy(ThermionTextureGL* texture) { if (texture && texture->registrar) { fl_texture_registrar_unregister_texture(texture->registrar, FL_TEXTURE(texture)); diff --git a/thermion_flutter/thermion_flutter/linux/egl_texture.h b/thermion_flutter/thermion_flutter/linux/egl_texture.h index e4c1180f1..b9a282121 100644 --- a/thermion_flutter/thermion_flutter/linux/egl_texture.h +++ b/thermion_flutter/thermion_flutter/linux/egl_texture.h @@ -47,6 +47,8 @@ struct _ThermionTextureGL { GLuint flutter_gl_texture_id; // Direct sharing path: same EGL share group as Flutter, no EGLImage needed gboolean use_direct_sharing; + // Pre-engine texture used only to capture Flutter's raster EGL context. + gboolean is_context_bootstrap; // Deferred "awaitTextureReady" response (stored until populate creates the GL texture) FlMethodCall* pending_ready_call; }; @@ -69,13 +71,6 @@ FLUTTER_PLUGIN_EXPORT ThermionTextureGL* thermion_texture_gl_create( int64_t surface_id, FlTextureRegistrar* registrar); -// A 1x1 texture populated on Flutter's raster thread solely to capture the -// EGLDisplay/EGLContext before Filament initializes. -FLUTTER_PLUGIN_EXPORT ThermionTextureGL* thermion_texture_gl_create_context_bootstrap( - uint32_t width, - uint32_t height, - FlTextureRegistrar* registrar); - // EGLImage bridge path: wraps a GL texture + EGLImage. On first populate, // the EGLImage is imported into a new texture on Flutter's own GL context. FLUTTER_PLUGIN_EXPORT ThermionTextureGL* thermion_texture_gl_create_shared( @@ -85,11 +80,18 @@ FLUTTER_PLUGIN_EXPORT ThermionTextureGL* thermion_texture_gl_create_shared( int64_t surface_id, FlTextureRegistrar* registrar); +// Pre-engine initialization path. The GL texture is created lazily from +// populate(), while Flutter's raster EGL context is current. +FLUTTER_PLUGIN_EXPORT ThermionTextureGL* +thermion_texture_gl_create_context_bootstrap( + uint32_t width, uint32_t height, + FlTextureRegistrar* registrar); + FLUTTER_PLUGIN_EXPORT void thermion_texture_gl_destroy(ThermionTextureGL* texture); // Flutter's render context, captured during the first deferred populate(). -// Used by ensure_opengl_context() to create Filament contexts in the correct -// EGL share group (Flutter's Group A, not GDK's Group B). +// Used by ensure_opengl_context() to select the compatible direct or DMA-BUF +// pathway on Flutter's actual EGLDisplay. extern EGLContext thermion_flutter_render_context; extern EGLDisplay thermion_flutter_render_display; diff --git a/thermion_flutter/thermion_flutter/linux/thermion_flutter_plugin.cc b/thermion_flutter/thermion_flutter/linux/thermion_flutter_plugin.cc index 4dce2a3d3..88fee72c4 100644 --- a/thermion_flutter/thermion_flutter/linux/thermion_flutter_plugin.cc +++ b/thermion_flutter/thermion_flutter/linux/thermion_flutter_plugin.cc @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -25,6 +26,34 @@ #define EGL_GL_TEXTURE_LEVEL_KHR 0x30BC #endif +// Calling Epoxy's eglDestroyImageKHR wrapper without a current EGL context +// makes provider selection depend on thread-local state. Teardown deliberately +// runs after Flutter releases its texture, so resolve the EGL entry point +// directly and let EGL dispatch from the explicit display argument. +static bool destroy_egl_image(EGLDisplay display, EGLImage image) +{ + if (display == EGL_NO_DISPLAY || image == EGL_NO_IMAGE_KHR) + { + return true; + } + + static PFNEGLDESTROYIMAGEKHRPROC destroyImage = + reinterpret_cast( + eglGetProcAddress("eglDestroyImageKHR")); + if (!destroyImage) + { + std::cerr << "[ThermionGL] eglDestroyImageKHR is unavailable" << std::endl; + return false; + } + if (!destroyImage(display, image)) + { + std::cerr << "[ThermionGL] eglDestroyImageKHR failed: 0x" + << std::hex << eglGetError() << std::dec << std::endl; + return false; + } + return true; +} + #include "vulkan/linux/LinuxVulkanContext.h" #include "LinuxOpenGLContext.h" #include "ThermionPlatformEGLHeadlessAPI.h" @@ -39,16 +68,33 @@ struct EglContextGuard { EGLContext prevCtx; EGLSurface prevDraw; EGLSurface prevRead; - EGLDisplay display; + EGLenum prevApi; + EGLDisplay prevDisplay; + EGLDisplay targetDisplay; EglContextGuard(EGLDisplay dpy) : prevCtx(eglGetCurrentContext()), prevDraw(eglGetCurrentSurface(EGL_DRAW)), prevRead(eglGetCurrentSurface(EGL_READ)), - display(dpy) {} + prevApi(eglQueryAPI()), + prevDisplay(eglGetCurrentDisplay()), + targetDisplay(dpy) { + // EGL does not allow switching client APIs while any context is current on + // the thread. Flutter's platform thread can have a GLES context current, + // so release it before binding desktop GL for plugin operations. + if (prevCtx != EGL_NO_CONTEXT && prevDisplay != EGL_NO_DISPLAY) { + eglMakeCurrent( + prevDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + } + } ~EglContextGuard() { - eglMakeCurrent(display, prevDraw, prevRead, prevCtx); + eglMakeCurrent( + targetDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + eglBindAPI(prevApi); + if (prevCtx != EGL_NO_CONTEXT && prevDisplay != EGL_NO_DISPLAY) { + eglMakeCurrent(prevDisplay, prevDraw, prevRead, prevCtx); + } } EglContextGuard(const EglContextGuard&) = delete; @@ -70,16 +116,22 @@ struct _ThermionFlutterPlugin thermion::vulkan::linux_platform::LinuxVulkanContext *vulkan_context; std::unordered_map *external_images; - // OpenGL path — direct sharing with Flutter's EGL context (preferred) + // OpenGL path — imports Flutter's EGL context before Filament starts. EGLContext flutter_egl_context; // Flutter's own context (captured, not owned) - EGLContext utility_egl_context; // our context in Flutter's share group (for GL ops) - EGLDisplay egl_display; // shared EGL display + EGLenum flutter_egl_api; + // Used for plugin GL operations when Flutter exposes desktop OpenGL. + EGLContext utility_egl_context; + // Used to release Flutter-owned texture names when Flutter exposes GLES and + // Filament must run through the cross-API DMA-BUF pathway. + EGLContext flutter_utility_egl_context; + EGLDisplay egl_display; // Flutter's EGL display EGLConfig egl_config; // config matching Flutter's context - gboolean use_direct_opengl; // TRUE if direct sharing path succeeded + gboolean use_direct_opengl; // TRUE only for compatible desktop GL void* thermion_platform; // standalone OpenGLPlatform (EGLHeadless) // OpenGL path — fallback (LinuxOpenGLContext with GBM/DMA-BUF) thermion::opengl::linux_platform::LinuxOpenGLContext *opengl_context; + std::string opengl_initialization_error; // Shared std::vector *textures; @@ -94,29 +146,36 @@ static void destroy_all_contexts(ThermionFlutterPlugin *self) delete self->vulkan_context; self->vulkan_context = nullptr; } + if (self->thermion_platform) + { + ThermionPlatformEGLHeadless_Destroy(self->thermion_platform); + self->thermion_platform = nullptr; + } + if (self->flutter_utility_egl_context != EGL_NO_CONTEXT && + self->egl_display != EGL_NO_DISPLAY) + { + eglDestroyContext( + self->egl_display, self->flutter_utility_egl_context); + self->flutter_utility_egl_context = EGL_NO_CONTEXT; + } if (self->utility_egl_context != EGL_NO_CONTEXT && self->egl_display != EGL_NO_DISPLAY) { eglDestroyContext(self->egl_display, self->utility_egl_context); self->utility_egl_context = EGL_NO_CONTEXT; } - if (self->use_direct_opengl) - { - self->flutter_egl_context = EGL_NO_CONTEXT; - self->egl_display = EGL_NO_DISPLAY; - self->egl_config = nullptr; - self->use_direct_opengl = FALSE; - if (self->thermion_platform) - { - ThermionPlatformEGLHeadless_Destroy(self->thermion_platform); - self->thermion_platform = nullptr; - } - } if (self->opengl_context) { delete self->opengl_context; self->opengl_context = nullptr; } + self->flutter_egl_context = EGL_NO_CONTEXT; + self->flutter_egl_api = EGL_NONE; + self->egl_display = EGL_NO_DISPLAY; + self->egl_config = nullptr; + self->use_direct_opengl = FALSE; + thermion_flutter_render_context = EGL_NO_CONTEXT; + thermion_flutter_render_display = EGL_NO_DISPLAY; self->backend_type = 0; } @@ -129,109 +188,221 @@ static void ensure_vulkan_context(ThermionFlutterPlugin *self) } } -static void ensure_opengl_context(ThermionFlutterPlugin *self) +static bool ensure_opengl_context(ThermionFlutterPlugin *self) { if (self->use_direct_opengl || self->opengl_context) { - return; // already initialized + return true; // already initialized + } + self->opengl_initialization_error.clear(); + + // Flutter's raster context is only guaranteed to be current inside an + // FlTextureGL populate callback. Dart must display and await the deferred + // context-bootstrap texture before requesting Filament's driver platform. + EGLContext flutterCtx = thermion_flutter_render_context; + EGLDisplay flutterDpy = thermion_flutter_render_display; + if (flutterCtx == EGL_NO_CONTEXT || flutterDpy == EGL_NO_DISPLAY) + { + std::cerr + << "[ThermionGL] Flutter raster context is not ready; " + "create and await the context bootstrap texture before " + "initializing Filament" + << std::endl; + self->opengl_initialization_error = + "Flutter's raster EGL context has not been imported"; + return false; + } + + EGLint clientType = 0; + EGLint glMajor = 0; + EGLint glMinor = 0; + EGLint configId = 0; + if (!eglQueryContext( + flutterDpy, flutterCtx, EGL_CONTEXT_CLIENT_TYPE, &clientType) || + !eglQueryContext( + flutterDpy, flutterCtx, EGL_CONTEXT_MAJOR_VERSION_KHR, &glMajor) || + !eglQueryContext( + flutterDpy, flutterCtx, EGL_CONTEXT_MINOR_VERSION_KHR, &glMinor) || + !eglQueryContext(flutterDpy, flutterCtx, EGL_CONFIG_ID, &configId)) + { + std::cerr << "[ThermionGL] Could not query Flutter's EGL context: 0x" + << std::hex << eglGetError() << std::dec << std::endl; + self->opengl_initialization_error = + "Could not query Flutter's raster EGL context"; + return false; } - // === Use Flutter's render context === - // The deferred texture path captures Flutter's render context during the - // first populate() call. Fall back to the current context (GDK) if no - // deferred texture has been populated yet. + std::cerr << "[ThermionGL] Imported Flutter raster context=" + << (void *)flutterCtx + << " display=" << (void *)flutterDpy + << " type=0x" << std::hex << clientType << std::dec + << " (" << (clientType == EGL_OPENGL_ES_API ? "GLES" : "GL") << ")" + << " version=" << glMajor << "." << glMinor + << " config_id=" << configId << std::endl; + + EGLConfig flutterConfig = nullptr; + EGLint numConfigs = 0; + EGLint configAttribs[] = { EGL_CONFIG_ID, configId, EGL_NONE }; + if (!eglChooseConfig( + flutterDpy, configAttribs, &flutterConfig, 1, &numConfigs) || + numConfigs == 0 || flutterConfig == nullptr) { - EGLContext flutterCtx = thermion_flutter_render_context; - EGLDisplay flutterDpy = thermion_flutter_render_display; + std::cerr << "[ThermionGL] Could not find Flutter's EGL config " + << configId << ": 0x" << std::hex << eglGetError() + << std::dec << std::endl; + self->opengl_initialization_error = + "Could not resolve Flutter's EGLConfig"; + return false; + } + + if (clientType != EGL_OPENGL_API && + clientType != EGL_OPENGL_ES_API) + { + std::cerr << "[ThermionGL] Unsupported Flutter EGL client API: 0x" + << std::hex << clientType << std::dec << std::endl; + self->opengl_initialization_error = + "Flutter uses an unsupported EGL client API"; + return false; + } - if (flutterCtx != EGL_NO_CONTEXT) { - std::cerr << "[ThermionGL] Using Flutter render context=" - << (void*)flutterCtx << std::endl; + EGLint ctxAttribs[] = { + EGL_CONTEXT_MAJOR_VERSION, glMajor > 0 ? glMajor : 3, + EGL_CONTEXT_MINOR_VERSION, glMinor > 0 ? glMinor : 0, + EGL_NONE + }; + EGLenum api = static_cast(clientType); + auto createFlutterUtilityContext = [&]() -> EGLContext + { + if (!eglBindAPI(api)) + { + std::cerr << "[ThermionGL] Could not bind Flutter's EGL API: 0x" + << std::hex << eglGetError() << std::dec << std::endl; + self->opengl_initialization_error = + "Could not bind Flutter's EGL client API"; + return EGL_NO_CONTEXT; } + EGLContext context = + eglCreateContext(flutterDpy, flutterConfig, flutterCtx, ctxAttribs); + if (context == EGL_NO_CONTEXT) + { + std::cerr << "[ThermionGL] Could not create a context shared with " + "Flutter: 0x" + << std::hex << eglGetError() << std::dec << std::endl; + self->opengl_initialization_error = + "Could not create a utility context shared with Flutter"; + } + return context; + }; - if (flutterCtx != EGL_NO_CONTEXT && flutterDpy != EGL_NO_DISPLAY) + if (api == EGL_OPENGL_ES_API) + { + // Filament's Linux backend is compiled for desktop OpenGL. EGL object + // sharing cannot cross the GLES / desktop-GL API boundary. This is the + // case handled by PR #136's DMA-BUF bridge. Keep both APIs on Flutter's + // captured EGLDisplay; a second GBM EGLDisplay can corrupt NVIDIA's + // concurrently rendering Flutter context. + self->opengl_context = + new thermion::opengl::linux_platform::LinuxOpenGLContext( + reinterpret_cast(flutterDpy)); + if (!self->opengl_context->IsValid()) { - EGLint clientType = 0, glMajor = 0, glMinor = 0, configId = 0; - eglQueryContext(flutterDpy, flutterCtx, EGL_CONTEXT_CLIENT_TYPE, &clientType); - eglQueryContext(flutterDpy, flutterCtx, EGL_CONTEXT_MAJOR_VERSION_KHR, &glMajor); - eglQueryContext(flutterDpy, flutterCtx, EGL_CONTEXT_MINOR_VERSION_KHR, &glMinor); - eglQueryContext(flutterDpy, flutterCtx, EGL_CONFIG_ID, &configId); - - std::cerr << "[ThermionGL] Captured Flutter context=" << (void *)flutterCtx - << " display=" << (void *)flutterDpy - << " type=0x" << std::hex << clientType << std::dec - << " (" << (clientType == EGL_OPENGL_ES_API ? "GLES" : "GL") << ")" - << " version=" << glMajor << "." << glMinor - << " config_id=" << configId << std::endl; - - // Get the matching EGL config - EGLConfig config = nullptr; - EGLint numConfigs = 0; - EGLint configAttribs[] = { EGL_CONFIG_ID, configId, EGL_NONE }; - eglChooseConfig(flutterDpy, configAttribs, &config, 1, &numConfigs); - - if (clientType == EGL_OPENGL_API && - (glMajor > 4 || (glMajor == 4 && glMinor >= 1)) && - numConfigs > 0 && config != nullptr) - { - // Bind the appropriate API (GL or GLES) to match Flutter - eglBindAPI(clientType == EGL_OPENGL_ES_API ? EGL_OPENGL_ES_API : EGL_OPENGL_API); - - // Create utility context sharing with Flutter - EGLint ctxAttribs[] = { - EGL_CONTEXT_MAJOR_VERSION, glMajor > 0 ? glMajor : 3, - EGL_CONTEXT_MINOR_VERSION, glMinor > 0 ? glMinor : 0, - EGL_NONE - }; - EGLContext utilityCtx = eglCreateContext(flutterDpy, config, flutterCtx, ctxAttribs); - - if (utilityCtx != EGL_NO_CONTEXT) - { - self->flutter_egl_context = flutterCtx; - self->utility_egl_context = utilityCtx; - self->egl_display = flutterDpy; - self->egl_config = config; - self->use_direct_opengl = TRUE; - self->backend_type = BACKEND_OPENGL; - self->thermion_platform = - ThermionPlatformEGLHeadless_Create(flutterDpy); - - std::cerr << "[ThermionGL] Created utility context=" << (void *)utilityCtx - << " (shared with Flutter)" << std::endl; - return; - } - else - { - std::cerr << "[ThermionGL] eglCreateContext(shared) failed: 0x" - << std::hex << eglGetError() << std::dec - << ", falling back" << std::endl; - } - } - else - { - std::cerr << "[ThermionGL] Could not find EGL config for config_id=" - << configId << ", falling back" << std::endl; - } + self->opengl_initialization_error = + self->opengl_context->GetLastError(); + delete self->opengl_context; + self->opengl_context = nullptr; + self->backend_type = 0; + return false; } - else + EGLContext flutterUtilityCtx = createFlutterUtilityContext(); + if (flutterUtilityCtx == EGL_NO_CONTEXT) { - std::cerr << "[ThermionGL] No EGL context current on this thread," - << " falling back to LinuxOpenGLContext" << std::endl; + delete self->opengl_context; + self->opengl_context = nullptr; + self->backend_type = 0; + return false; } + self->flutter_egl_context = flutterCtx; + self->flutter_egl_api = api; + self->flutter_utility_egl_context = flutterUtilityCtx; + self->egl_display = flutterDpy; + self->egl_config = flutterConfig; + self->backend_type = BACKEND_OPENGL; + std::cerr + << "[ThermionGL] Flutter uses GLES; selected same-display " + "GBM/DMA-BUF OpenGL fallback" + << std::endl; + return true; } - // Flutter normally uses GLES on Linux, while Filament requires desktop GL. - // Keep the existing DMA-BUF transport, but create Filament's desktop context - // on Flutter's already-initialized display instead of racing a second one. - if (thermion_flutter_render_display != EGL_NO_DISPLAY) + if (glMajor < 4 || (glMajor == 4 && glMinor < 1)) { - std::cerr << "[ThermionGL] Using Flutter EGLDisplay with DMA-BUF transport" + std::cerr << "[ThermionGL] Flutter desktop OpenGL " << glMajor << "." + << glMinor << " is below Filament's 4.1 requirement; " + "selected same-display GBM/DMA-BUF fallback" << std::endl; self->opengl_context = new thermion::opengl::linux_platform::LinuxOpenGLContext( - reinterpret_cast(thermion_flutter_render_display)); + reinterpret_cast(flutterDpy)); + if (!self->opengl_context->IsValid()) + { + self->opengl_initialization_error = + self->opengl_context->GetLastError(); + delete self->opengl_context; + self->opengl_context = nullptr; + self->backend_type = 0; + return false; + } + EGLContext flutterUtilityCtx = createFlutterUtilityContext(); + if (flutterUtilityCtx == EGL_NO_CONTEXT) + { + delete self->opengl_context; + self->opengl_context = nullptr; + self->backend_type = 0; + return false; + } + self->flutter_egl_context = flutterCtx; + self->flutter_egl_api = api; + self->flutter_utility_egl_context = flutterUtilityCtx; + self->egl_display = flutterDpy; + self->egl_config = flutterConfig; self->backend_type = BACKEND_OPENGL; + return true; } + + EGLContext flutterUtilityCtx = createFlutterUtilityContext(); + if (flutterUtilityCtx == EGL_NO_CONTEXT) + { + return false; + } + self->flutter_egl_context = flutterCtx; + self->flutter_egl_api = api; + self->utility_egl_context = flutterUtilityCtx; + self->egl_display = flutterDpy; + self->egl_config = flutterConfig; + self->use_direct_opengl = TRUE; + self->backend_type = BACKEND_OPENGL; + self->thermion_platform = ThermionPlatformEGLHeadless_Create(flutterDpy); + if (!self->thermion_platform) + { + eglDestroyContext(flutterDpy, flutterUtilityCtx); + self->flutter_egl_context = EGL_NO_CONTEXT; + self->flutter_egl_api = EGL_NONE; + self->utility_egl_context = EGL_NO_CONTEXT; + self->egl_display = EGL_NO_DISPLAY; + self->egl_config = nullptr; + self->use_direct_opengl = FALSE; + self->backend_type = 0; + std::cerr << "[ThermionGL] Could not create the Filament EGL platform" + << std::endl; + self->opengl_initialization_error = + "Could not create Filament's EGL platform"; + return false; + } + + std::cerr << "[ThermionGL] Created desktop OpenGL utility context=" + << (void *)flutterUtilityCtx << " (shared with Flutter)" + << std::endl; + return true; } static FlMethodResponse *handle_get_driver_platform(ThermionFlutterPlugin *self, FlMethodCall *method_call) @@ -244,12 +415,18 @@ static FlMethodResponse *handle_get_driver_platform(ThermionFlutterPlugin *self, int64_t platform = 0; if (backend == BACKEND_OPENGL) { - ensure_opengl_context(self); - if (!self->use_direct_opengl && !self->opengl_context) + gboolean rasterContextReady = + thermion_flutter_render_context != EGL_NO_CONTEXT && + thermion_flutter_render_display != EGL_NO_DISPLAY; + if (!ensure_opengl_context(self)) { return FL_METHOD_RESPONSE(fl_method_error_response_new( - "CONTEXT_NOT_READY", - "Display the Linux OpenGL bootstrap texture before initialization", + rasterContextReady ? "OPENGL_INITIALIZATION_FAILED" + : "CONTEXT_NOT_READY", + rasterContextReady + ? self->opengl_initialization_error.c_str() + : "Flutter's raster EGL context must be imported before " + "Filament OpenGL initialization", nullptr)); } if (self->use_direct_opengl) @@ -281,17 +458,23 @@ static FlMethodResponse *handle_get_shared_context(ThermionFlutterPlugin *self, int64_t sharedCtx = 0; if (backend == BACKEND_OPENGL) { - ensure_opengl_context(self); - if (!self->use_direct_opengl && !self->opengl_context) + gboolean rasterContextReady = + thermion_flutter_render_context != EGL_NO_CONTEXT && + thermion_flutter_render_display != EGL_NO_DISPLAY; + if (!ensure_opengl_context(self)) { return FL_METHOD_RESPONSE(fl_method_error_response_new( - "CONTEXT_NOT_READY", - "Display the Linux OpenGL bootstrap texture before initialization", + rasterContextReady ? "OPENGL_INITIALIZATION_FAILED" + : "CONTEXT_NOT_READY", + rasterContextReady + ? self->opengl_initialization_error.c_str() + : "Flutter's raster EGL context must be imported before " + "Filament OpenGL initialization", nullptr)); } if (self->use_direct_opengl) { - // Direct sharing: return Flutter's context (Filament contexts share with it) + // Filament creates its own driver context in Flutter's object group. sharedCtx = reinterpret_cast(self->flutter_egl_context); } else @@ -364,10 +547,11 @@ static FlMethodResponse *handle_create_texture_vulkan(ThermionFlutterPlugin *sel static FlMethodResponse *handle_create_texture_opengl_direct(ThermionFlutterPlugin *self, int width, int height) { - // EGLImage bridge: create GL texture on utility context (Group B, same as - // Filament), then bridge to Flutter's render context (Group A) via EGLImage. + // EGLImage bridge: create the source texture on the desktop-GL utility + // context shared by Filament, then let populate() bind the image to a + // Flutter-owned texture name. // - Filament imports the GL texture directly (same share group) - // - Flutter imports the EGLImage in populate() (cross share group) + // - Flutter binds the EGLImage in populate() EGLDisplay display = self->egl_display; GLuint glTexId = 0; @@ -376,13 +560,28 @@ static FlMethodResponse *handle_create_texture_opengl_direct(ThermionFlutterPlug { EglContextGuard guard(display); - // Make utility context current (Group B) - if (!eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, self->utility_egl_context)) + // Make the desktop-GL utility context current. + if (!eglBindAPI(EGL_OPENGL_API)) { - std::cerr << "[ThermionGL] Failed to make utility context current: 0x" - << std::hex << eglGetError() << std::dec << std::endl; + EGLint error = eglGetError(); + std::cerr << "[ThermionGL] Failed to bind desktop OpenGL: 0x" + << std::hex << error << std::dec << std::endl; + return FL_METHOD_RESPONSE(fl_method_error_response_new( + "EGL_BIND_ERROR", "Failed to bind desktop OpenGL", nullptr)); + } + if (!eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, + self->utility_egl_context)) + { + EGLint error = eglGetError(); + std::cerr << "[ThermionGL] Failed to make plugin texture context " + "current: 0x" + << std::hex << error << std::dec + << " context=" << (void*)self->utility_egl_context + << std::endl; + g_autofree gchar *message = g_strdup_printf( + "Failed to make plugin texture context current (EGL 0x%x)", error); return FL_METHOD_RESPONSE(fl_method_error_response_new( - "EGL_ERROR", "Failed to make utility context current", nullptr)); + "EGL_MAKE_CURRENT_ERROR", message, nullptr)); } // Create GL texture on utility context @@ -396,7 +595,7 @@ static FlMethodResponse *handle_create_texture_opengl_direct(ThermionFlutterPlug glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glBindTexture(GL_TEXTURE_2D, 0); - // Create EGLImage from the GL texture (bridges Group B → Group A) + // Create an EGLImage from the GL texture. // Must be called while the owning context is current EGLint imageAttribs[] = { EGL_GL_TEXTURE_LEVEL_KHR, 0, EGL_NONE }; eglImage = eglCreateImageKHR( @@ -413,6 +612,7 @@ static FlMethodResponse *handle_create_texture_opengl_direct(ThermionFlutterPlug << std::hex << err << std::dec << std::endl; { EglContextGuard guard(display); + eglBindAPI(EGL_OPENGL_API); eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, self->utility_egl_context); glDeleteTextures(1, &glTexId); } @@ -431,9 +631,10 @@ static FlMethodResponse *handle_create_texture_opengl_direct(ThermionFlutterPlug FlTexture *flTexture = FL_TEXTURE(textureGL); if (!fl_texture_registrar_register_texture(self->texture_registrar, flTexture)) { - eglDestroyImageKHR(display, eglImage); + destroy_egl_image(display, eglImage); { EglContextGuard guard(display); + eglBindAPI(EGL_OPENGL_API); eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, self->utility_egl_context); glDeleteTextures(1, &glTexId); } @@ -497,13 +698,18 @@ static FlMethodResponse *handle_create_texture_opengl_dmabuf(ThermionFlutterPlug static FlMethodResponse *handle_create_texture_opengl(ThermionFlutterPlugin *self, int width, int height) { - ensure_opengl_context(self); - - if (!self->use_direct_opengl && !self->opengl_context) + gboolean rasterContextReady = + thermion_flutter_render_context != EGL_NO_CONTEXT && + thermion_flutter_render_display != EGL_NO_DISPLAY; + if (!ensure_opengl_context(self)) { return FL_METHOD_RESPONSE(fl_method_error_response_new( - "CONTEXT_NOT_READY", - "Display the Linux OpenGL bootstrap texture before initialization", + rasterContextReady ? "OPENGL_INITIALIZATION_FAILED" + : "CONTEXT_NOT_READY", + rasterContextReady + ? self->opengl_initialization_error.c_str() + : "Cannot create an OpenGL texture before Flutter's raster " + "context is ready", nullptr)); } @@ -518,25 +724,47 @@ static FlMethodResponse *handle_create_context_bootstrap( ThermionFlutterPlugin *self, FlMethodCall *method_call) { FlValue *args = fl_method_call_get_args(method_call); + if (args == nullptr || fl_value_get_type(args) != FL_VALUE_TYPE_LIST || + fl_value_get_length(args) < 2) + { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + "INVALID_ARGUMENTS", "Expected bootstrap texture width and height", + nullptr)); + } + int width = fl_value_get_int(fl_value_get_list_value(args, 0)); int height = fl_value_get_int(fl_value_get_list_value(args, 1)); + if (width <= 0 || height <= 0) + { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + "INVALID_ARGUMENTS", "Bootstrap texture dimensions must be positive", + nullptr)); + } + ThermionTextureGL *textureGL = thermion_texture_gl_create_context_bootstrap( static_cast(width), static_cast(height), self->texture_registrar); FlTexture *flTexture = FL_TEXTURE(textureGL); - if (!fl_texture_registrar_register_texture(self->texture_registrar, flTexture)) + if (!fl_texture_registrar_register_texture( + self->texture_registrar, flTexture)) { g_object_unref(textureGL); return FL_METHOD_RESPONSE(fl_method_error_response_new( - "REGISTER_FAILED", "Failed to register context bootstrap", nullptr)); + "REGISTER_FAILED", + "Failed to register Flutter context bootstrap texture", nullptr)); } + self->textures->push_back(textureGL); self->backend_type = BACKEND_OPENGL; fl_texture_registrar_mark_texture_frame_available( self->texture_registrar, flTexture); - return FL_METHOD_RESPONSE(fl_method_success_response_new( - fl_value_new_int(fl_texture_get_id(flTexture)))); + + int64_t flutterTextureId = fl_texture_get_id(flTexture); + std::cerr << "[ThermionGL] Registered context bootstrap texture, flutterId=" + << flutterTextureId << std::endl; + g_autoptr(FlValue) result = fl_value_new_int(flutterTextureId); + return FL_METHOD_RESPONSE(fl_method_success_response_new(result)); } @@ -567,25 +795,104 @@ static FlMethodResponse *handle_destroy_texture(ThermionFlutterPlugin *self, FlM if (fl_texture_get_id(FL_TEXTURE(tex)) == flutterTextureId) { int64_t surfaceId = tex->surface_id; + gboolean useDirectSharing = tex->use_direct_sharing; + gboolean useEglImage = tex->use_egl_image; + gboolean isContextBootstrap = tex->is_context_bootstrap; + GLuint glTextureId = tex->gl_texture_id; + GLuint flutterGlTextureId = tex->flutter_gl_texture_id; + EGLImage eglImage = tex->egl_image; + FlMethodCall *pendingReadyCall = tex->pending_ready_call; + tex->pending_ready_call = nullptr; + + if (pendingReadyCall) + { + fl_method_call_respond( + pendingReadyCall, + FL_METHOD_RESPONSE(fl_method_error_response_new( + "DESTROYED", + "Texture destroyed before Flutter populated it", nullptr)), + nullptr); + g_object_unref(pendingReadyCall); + } fl_texture_registrar_unregister_texture(self->texture_registrar, FL_TEXTURE(tex)); + gboolean initializedOnlyForBootstrapCleanup = FALSE; if (self->backend_type == BACKEND_OPENGL) { - if (tex->use_direct_sharing || tex->use_egl_image) + if (useDirectSharing || useEglImage) { - // Direct sharing / EGLImage bridge: delete the source GL texture on utility context - GLuint texId = tex->gl_texture_id; - if (texId != 0 && self->utility_egl_context != EGL_NO_CONTEXT) + // EGLImage bridge: source names belong to Filament's utility group. + // Bootstrap names belong to Flutter's group. + if (glTextureId != 0 && + self->utility_egl_context == EGL_NO_CONTEXT && + self->flutter_utility_egl_context == EGL_NO_CONTEXT && + useDirectSharing) + { + // A bootstrap may be cancelled after populate() but before Dart + // requests the driver platform. Import the captured context now so + // its GL object can still be deleted from the correct share group. + initializedOnlyForBootstrapCleanup = + isContextBootstrap && ensure_opengl_context(self); + } + EGLContext sourceContext = useDirectSharing && + self->flutter_utility_egl_context != EGL_NO_CONTEXT + ? self->flutter_utility_egl_context + : self->utility_egl_context; + if (glTextureId != 0 && sourceContext != EGL_NO_CONTEXT) { EglContextGuard guard(self->egl_display); + eglBindAPI(useDirectSharing + ? self->flutter_egl_api + : EGL_OPENGL_API); eglMakeCurrent(self->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, - self->utility_egl_context); - glDeleteTextures(1, &texId); + sourceContext); + glDeleteTextures(1, &glTextureId); + } + + if (useEglImage) + { + EGLContext flutterContext = + self->flutter_utility_egl_context != EGL_NO_CONTEXT + ? self->flutter_utility_egl_context + : self->utility_egl_context; + if (flutterGlTextureId != 0 && + flutterContext != EGL_NO_CONTEXT) + { + EglContextGuard guard(self->egl_display); + eglBindAPI(self->flutter_egl_api); + eglMakeCurrent(self->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, + flutterContext); + glDeleteTextures(1, &flutterGlTextureId); + } + if (eglImage != EGL_NO_IMAGE_KHR) + { + destroy_egl_image(self->egl_display, eglImage); + } } } else if (self->opengl_context) { + // DMA-BUF fallback: populate() created the consumer texture and + // EGLImage in Flutter's GLES share group. Release those before the + // producer destroys the backing GBM buffer. + if (glTextureId != 0 && + self->flutter_utility_egl_context != EGL_NO_CONTEXT) + { + EglContextGuard guard(self->egl_display); + eglBindAPI(self->flutter_egl_api); + if (eglMakeCurrent( + self->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, + self->flutter_utility_egl_context)) + { + glDeleteTextures(1, &glTextureId); + } + } + if (eglImage != EGL_NO_IMAGE_KHR && + self->egl_display != EGL_NO_DISPLAY) + { + destroy_egl_image(self->egl_display, eglImage); + } self->opengl_context->DestroyRenderingSurface(surfaceId); } } @@ -607,6 +914,20 @@ static FlMethodResponse *handle_destroy_texture(ThermionFlutterPlugin *self, FlM } self->textures->erase(it); + if (self->backend_type == BACKEND_OPENGL) + { + // The plugin retains each factory's construction reference in + // addition to the registrar's reference. All OpenGL/EGL objects have + // now been deleted from their owning contexts. + tex->gl_texture_id = 0; + tex->flutter_gl_texture_id = 0; + tex->egl_image = EGL_NO_IMAGE_KHR; + g_object_unref(tex); + } + if (initializedOnlyForBootstrapCleanup) + { + destroy_all_contexts(self); + } break; } } @@ -786,7 +1107,9 @@ static void thermion_flutter_plugin_init(ThermionFlutterPlugin *self) self->view = nullptr; self->vulkan_context = nullptr; self->flutter_egl_context = EGL_NO_CONTEXT; + self->flutter_egl_api = EGL_NONE; self->utility_egl_context = EGL_NO_CONTEXT; + self->flutter_utility_egl_context = EGL_NO_CONTEXT; self->egl_display = EGL_NO_DISPLAY; self->egl_config = nullptr; self->use_direct_opengl = FALSE; From eb7551076082767adfc42ca3c75b7d336a75b558 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 21 Aug 2026 02:56:34 +0000 Subject: [PATCH 03/13] chore: update generated artifacts + format (CI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with GitHub Actions --- .../src/platform_texture_descriptor_registry_native.dart | 5 +++-- .../platform/src/thermion_flutter_plugin_initializer.dart | 5 +++-- .../thermion_flutter/lib/src/thermion_flutter_plugin.dart | 2 ++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry_native.dart b/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry_native.dart index 71ba659be..ad63e682e 100644 --- a/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry_native.dart +++ b/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry_native.dart @@ -11,8 +11,9 @@ import 'method_channel_platform_texture_descriptor.dart'; import 'platform_texture_descriptor.dart'; import 'platform_texture_descriptor_registry.dart'; -typedef TextureMutationRunner = - Future Function(Future Function() operation); +typedef TextureMutationRunner = Future Function( + Future Function() operation, +); class FilamentRenderingContext { const FilamentRenderingContext({ diff --git a/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_initializer.dart b/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_initializer.dart index 146bad260..f843a7a5e 100644 --- a/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_initializer.dart +++ b/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_initializer.dart @@ -6,8 +6,9 @@ import 'platform_texture_descriptor.dart'; typedef ContextBootstrapAllocator = Future Function(); -typedef ContextBootstrapDestroyer = - Future Function(PlatformTextureDescriptor descriptor); +typedef ContextBootstrapDestroyer = Future Function( + PlatformTextureDescriptor descriptor, +); /// Hosts the Flutter-side prerequisites for initializing the native plugin, /// then runs [initialize]. diff --git a/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart b/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart index c79f9fdef..9e928c450 100644 --- a/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart +++ b/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart @@ -1,4 +1,5 @@ import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart' hide View; import 'package:thermion_dart/thermion_dart.dart'; @@ -7,6 +8,7 @@ import 'package:thermion_dart/src/filament/src/implementation/ffi_filament_app.d import 'package:thermion_flutter/src/options.dart'; import 'package:thermion_flutter/src/platform/src/platform_texture_descriptor.dart'; import 'package:thermion_flutter/src/platform/src/thermion_flutter_plugin_initializer.dart'; + import 'platform/platform.dart'; import 'package:logging/logging.dart'; From 7975fe21dbb8b53fec178f82758b43d9e548610c Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Fri, 21 Aug 2026 13:16:39 +0800 Subject: [PATCH 04/13] ci(linux): smoke test quickstart on X11 and Wayland --- .github/workflows/run-flutter-builds.yml | 60 ++++++++++++++++++- .../display_server_smoke_test.dart | 60 +++++++++++++++++++ examples/flutter/quickstart/lib/main.dart | 7 +++ 3 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 examples/flutter/quickstart/integration_test/display_server_smoke_test.dart diff --git a/.github/workflows/run-flutter-builds.yml b/.github/workflows/run-flutter-builds.yml index bcd64586a..4bf362d9e 100644 --- a/.github/workflows/run-flutter-builds.yml +++ b/.github/workflows/run-flutter-builds.yml @@ -110,7 +110,7 @@ jobs: shell: bash run: | sudo apt-get update -y - sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libdrm-dev libegl1 libegl1-mesa-dev libc++-dev libc++abi-dev + sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libdrm-dev libegl1 libegl1-mesa-dev libgl1-mesa-dri libc++-dev libc++abi-dev xvfb weston # Same priming as the Windows/macOS unit test steps above: fires the # build hooks so the Filament headers are extracted and @@ -128,6 +128,61 @@ jobs: if: matrix.name == 'Linux' run: cd quickstart && flutter pub get && flutter build linux + - name: Run quickstart viewer smoke test (X11) + if: matrix.name == 'Linux' + shell: bash + run: | + set -o pipefail + GDK_BACKEND=x11 LIBGL_ALWAYS_SOFTWARE=1 \ + timeout 8m xvfb-run -a \ + -e "${RUNNER_TEMP}/xvfb.log" \ + -s '-screen 0 1280x720x24' \ + flutter test integration_test/display_server_smoke_test.dart \ + -d linux 2>&1 | tee "${RUNNER_TEMP}/display-smoke-x11.log" + working-directory: examples/flutter/quickstart + + - name: Run quickstart viewer smoke test (Wayland) + if: matrix.name == 'Linux' + shell: bash + run: | + set -o pipefail + export XDG_RUNTIME_DIR="${RUNNER_TEMP}/wayland-runtime" + export WAYLAND_DISPLAY=wayland-ci + export LIBGL_ALWAYS_SOFTWARE=1 + mkdir -p "${XDG_RUNTIME_DIR}" + chmod 700 "${XDG_RUNTIME_DIR}" + + weston \ + --backend=headless-backend.so \ + --use-gl \ + --no-config \ + --socket="${WAYLAND_DISPLAY}" \ + --width=1280 \ + --height=720 \ + --idle-time=0 \ + --log="${RUNNER_TEMP}/weston.log" & + weston_pid=$! + trap 'kill "${weston_pid}" 2>/dev/null || true' EXIT + + for _ in {1..50}; do + if [[ -S "${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}" ]]; then + break + fi + if ! kill -0 "${weston_pid}" 2>/dev/null; then + cat "${RUNNER_TEMP}/weston.log" + exit 1 + fi + sleep 0.1 + done + test -S "${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}" + + unset DISPLAY + GDK_BACKEND=wayland \ + timeout 8m flutter test \ + integration_test/display_server_smoke_test.dart \ + -d linux 2>&1 | tee "${RUNNER_TEMP}/display-smoke-wayland.log" + working-directory: examples/flutter/quickstart + - name: Build picking (Linux) if: matrix.name == 'Linux' run: cd picking && flutter pub get && flutter build linux @@ -139,6 +194,9 @@ jobs: name: build-logs-flutter-${{ matrix.name }} path: | ${{ github.workspace }}/thermion_dart/.dart_tool/thermion_dart/log/build.log + ${{ runner.temp }}/display-smoke-*.log + ${{ runner.temp }}/weston.log + ${{ runner.temp }}/xvfb.log retention-days: 5 android-examples: diff --git a/examples/flutter/quickstart/integration_test/display_server_smoke_test.dart b/examples/flutter/quickstart/integration_test/display_server_smoke_test.dart new file mode 100644 index 000000000..4be271ea9 --- /dev/null +++ b/examples/flutter/quickstart/integration_test/display_server_smoke_test.dart @@ -0,0 +1,60 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:quickstart/main.dart'; +import 'package:thermion_flutter/thermion_flutter.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets( + 'adds, initializes, renders, and removes a viewer', + (tester) async { + await tester.pumpWidget(const MyApp()); + expect(find.text('No viewers mounted'), findsOneWidget); + + // Exercise the real quickstart interaction instead of mounting a + // ViewerWidget directly in the test. + await tester.tap(find.text('Add')); + await tester.pump(); + expect(find.byType(ViewerWidget), findsOneWidget); + + await _pumpUntil(tester, find.byKey(const ValueKey('viewer-ready-1'))); + + // Keep the native render loop alive for several frames after the viewer + // callback. Startup-only success is not enough: the EGL transport must + // remain usable once Flutter begins consuming frames. + for (var frame = 0; frame < 30; frame++) { + await tester.pump(const Duration(milliseconds: 16)); + } + expect(tester.takeException(), isNull); + + await tester.tap(find.text('Remove')); + await _pumpUntil(tester, find.byType(ViewerWidget), present: false); + expect(tester.takeException(), isNull); + }, + timeout: const Timeout(Duration(minutes: 3)), + ); +} + +Future _pumpUntil( + WidgetTester tester, + Finder finder, { + bool present = true, + Duration timeout = const Duration(seconds: 90), +}) async { + final stopwatch = Stopwatch()..start(); + while ((finder.evaluate().isNotEmpty != present) && + stopwatch.elapsed < timeout) { + await tester.pump(const Duration(milliseconds: 16)); + } + if (finder.evaluate().isNotEmpty != present) { + throw TimeoutException( + 'Timed out waiting for ${finder.describeMatch(Plurality.one)} to be ' + '${present ? 'present' : 'absent'}', + timeout, + ); + } +} diff --git a/examples/flutter/quickstart/lib/main.dart b/examples/flutter/quickstart/lib/main.dart index bc276c842..01d6065bb 100644 --- a/examples/flutter/quickstart/lib/main.dart +++ b/examples/flutter/quickstart/lib/main.dart @@ -706,6 +706,13 @@ class _ViewerTileState extends State<_ViewerTile> { ), ), ), + // Exposes completion of the real ViewerWidget initialization to + // native integration tests without changing the visible UI. + if (_viewer != null) + KeyedSubtree( + key: ValueKey('viewer-ready-${widget.index}'), + child: const SizedBox.shrink(), + ), // Top scrim so overlay controls stay legible on bright skyboxes. const Positioned( left: 0, From 4d0fba2bac06f3af99732ec424a80ee0454b4990 Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Fri, 21 Aug 2026 13:30:45 +0800 Subject: [PATCH 05/13] fix(linux): capture Flutter GL version during bootstrap --- .../thermion_flutter/linux/egl_texture.cc | 24 +++++++++++++++++-- .../thermion_flutter/linux/egl_texture.h | 3 +++ .../linux/thermion_flutter_plugin.cc | 18 +++++++------- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/thermion_flutter/thermion_flutter/linux/egl_texture.cc b/thermion_flutter/thermion_flutter/linux/egl_texture.cc index c0c5db7a6..5cb0465b2 100644 --- a/thermion_flutter/thermion_flutter/linux/egl_texture.cc +++ b/thermion_flutter/thermion_flutter/linux/egl_texture.cc @@ -2,6 +2,7 @@ #include "Log.hpp" #include +#include #include #include #include @@ -13,6 +14,9 @@ // Flutter's render context, captured during the first deferred populate(). EGLContext thermion_flutter_render_context = EGL_NO_CONTEXT; EGLDisplay thermion_flutter_render_display = EGL_NO_DISPLAY; +EGLenum thermion_flutter_render_api = EGL_NONE; +EGLint thermion_flutter_render_gl_major = 0; +EGLint thermion_flutter_render_gl_minor = 0; // EGL function pointers (resolved at runtime) static PFNEGLCREATEIMAGEKHRPROC s_eglCreateImageKHR = nullptr; @@ -131,9 +135,25 @@ thermion_texture_populate(FlTextureGL *texture, // This is the ONLY place where Flutter's render context is current. thermion_flutter_render_context = flutterContext; thermion_flutter_render_display = flutterDisplay; - TRACE( "[DirectPop] Captured Flutter render context=%p display=%p\n", + thermion_flutter_render_api = eglQueryAPI(); + + const char* version = reinterpret_cast( + glGetString(GL_VERSION)); + if (version) { + if (std::sscanf(version, "OpenGL ES %d.%d", + &thermion_flutter_render_gl_major, + &thermion_flutter_render_gl_minor) != 2) { + std::sscanf(version, "%d.%d", + &thermion_flutter_render_gl_major, + &thermion_flutter_render_gl_minor); + } + } + TRACE( "[DirectPop] Captured Flutter render context=%p display=%p API=0x%x version=%d.%d\n", (void*)thermion_flutter_render_context, - (void*)thermion_flutter_render_display); + (void*)thermion_flutter_render_display, + thermion_flutter_render_api, + thermion_flutter_render_gl_major, + thermion_flutter_render_gl_minor); // Do not resolve awaitTextureReady from inside populate(). Dart // may immediately initialize another EGL client API when the diff --git a/thermion_flutter/thermion_flutter/linux/egl_texture.h b/thermion_flutter/thermion_flutter/linux/egl_texture.h index b9a282121..27dcf45d4 100644 --- a/thermion_flutter/thermion_flutter/linux/egl_texture.h +++ b/thermion_flutter/thermion_flutter/linux/egl_texture.h @@ -94,5 +94,8 @@ FLUTTER_PLUGIN_EXPORT void thermion_texture_gl_destroy(ThermionTextureGL* textur // pathway on Flutter's actual EGLDisplay. extern EGLContext thermion_flutter_render_context; extern EGLDisplay thermion_flutter_render_display; +extern EGLenum thermion_flutter_render_api; +extern EGLint thermion_flutter_render_gl_major; +extern EGLint thermion_flutter_render_gl_minor; #endif diff --git a/thermion_flutter/thermion_flutter/linux/thermion_flutter_plugin.cc b/thermion_flutter/thermion_flutter/linux/thermion_flutter_plugin.cc index 88fee72c4..6fb29511b 100644 --- a/thermion_flutter/thermion_flutter/linux/thermion_flutter_plugin.cc +++ b/thermion_flutter/thermion_flutter/linux/thermion_flutter_plugin.cc @@ -176,6 +176,9 @@ static void destroy_all_contexts(ThermionFlutterPlugin *self) self->use_direct_opengl = FALSE; thermion_flutter_render_context = EGL_NO_CONTEXT; thermion_flutter_render_display = EGL_NO_DISPLAY; + thermion_flutter_render_api = EGL_NONE; + thermion_flutter_render_gl_major = 0; + thermion_flutter_render_gl_minor = 0; self->backend_type = 0; } @@ -213,16 +216,13 @@ static bool ensure_opengl_context(ThermionFlutterPlugin *self) return false; } - EGLint clientType = 0; - EGLint glMajor = 0; - EGLint glMinor = 0; + EGLenum clientType = thermion_flutter_render_api; + EGLint glMajor = thermion_flutter_render_gl_major; + EGLint glMinor = thermion_flutter_render_gl_minor; EGLint configId = 0; - if (!eglQueryContext( - flutterDpy, flutterCtx, EGL_CONTEXT_CLIENT_TYPE, &clientType) || - !eglQueryContext( - flutterDpy, flutterCtx, EGL_CONTEXT_MAJOR_VERSION_KHR, &glMajor) || - !eglQueryContext( - flutterDpy, flutterCtx, EGL_CONTEXT_MINOR_VERSION_KHR, &glMinor) || + if ((clientType != EGL_OPENGL_API && + clientType != EGL_OPENGL_ES_API) || + glMajor <= 0 || !eglQueryContext(flutterDpy, flutterCtx, EGL_CONFIG_ID, &configId)) { std::cerr << "[ThermionGL] Could not query Flutter's EGL context: 0x" From 46737d13aff10061d3e6fbc413584c560528438c Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Fri, 21 Aug 2026 13:42:16 +0800 Subject: [PATCH 06/13] ci(linux): expose llvmpipe OpenGL 4.5 --- .github/workflows/run-flutter-builds.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/run-flutter-builds.yml b/.github/workflows/run-flutter-builds.yml index 4bf362d9e..8874e77f2 100644 --- a/.github/workflows/run-flutter-builds.yml +++ b/.github/workflows/run-flutter-builds.yml @@ -134,6 +134,7 @@ jobs: run: | set -o pipefail GDK_BACKEND=x11 LIBGL_ALWAYS_SOFTWARE=1 \ + MESA_GL_VERSION_OVERRIDE=4.5 \ timeout 8m xvfb-run -a \ -e "${RUNNER_TEMP}/xvfb.log" \ -s '-screen 0 1280x720x24' \ @@ -149,6 +150,7 @@ jobs: export XDG_RUNTIME_DIR="${RUNNER_TEMP}/wayland-runtime" export WAYLAND_DISPLAY=wayland-ci export LIBGL_ALWAYS_SOFTWARE=1 + export MESA_GL_VERSION_OVERRIDE=4.5 mkdir -p "${XDG_RUNTIME_DIR}" chmod 700 "${XDG_RUNTIME_DIR}" From 97040e73d5b85d97c3461ab97b445483629674e9 Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Fri, 21 Aug 2026 14:57:32 +0800 Subject: [PATCH 07/13] ci(linux): run display smoke on GPU runner --- .github/workflows/linux-display-smoke.yml | 126 ++++++++++++++++++++++ .github/workflows/run-flutter-builds.yml | 62 +---------- 2 files changed, 127 insertions(+), 61 deletions(-) create mode 100644 .github/workflows/linux-display-smoke.yml diff --git a/.github/workflows/linux-display-smoke.yml b/.github/workflows/linux-display-smoke.yml new file mode 100644 index 000000000..1026d2803 --- /dev/null +++ b/.github/workflows/linux-display-smoke.yml @@ -0,0 +1,126 @@ +name: Linux Display Smoke + +on: + workflow_dispatch: + inputs: + ref: + description: Git ref to test + required: false + type: string + x11-display: + description: X11 display exposed to the runner + required: false + default: ':0' + type: string + +jobs: + quickstart: + name: Quickstart viewer (X11 + Wayland) + runs-on: [self-hosted, linux, x64] + timeout-minutes: 30 + env: + DISPLAY: ${{ inputs.x11-display }} + defaults: + run: + working-directory: examples/flutter/quickstart + shell: bash + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref || github.ref }} + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.44.8' + channel: stable + architecture: X64 + cache: true + pub-cache: true + + - name: Install Linux dependencies + working-directory: . + run: | + sudo apt-get update -y + sudo apt-get install -y \ + clang cmake ninja-build pkg-config \ + libgtk-3-dev liblzma-dev libdrm-dev \ + libegl1 libegl1-mesa-dev libc++-dev libc++abi-dev \ + weston x11-utils + + # Prime generated headers before the example's CMake configure step. + - name: Run thermion_flutter unit tests + working-directory: thermion_flutter/thermion_flutter + run: | + flutter pub get + flutter test + + - name: Build quickstart + run: | + flutter pub get + flutter build linux + + - name: Verify GPU and X11 session + run: | + if [[ ! -r /dev/dri/renderD128 || ! -w /dev/dri/renderD128 ]]; then + echo '::error::The runner user needs read/write access to /dev/dri/renderD128' + exit 1 + fi + if ! xdpyinfo >/dev/null; then + echo "::error::Start the runner from the logged-in X11 session or provide a usable x11-display input (currently ${DISPLAY})" + exit 1 + fi + + - name: Run viewer smoke test (X11) + run: | + set -o pipefail + GDK_BACKEND=x11 timeout 8m flutter test \ + integration_test/display_server_smoke_test.dart \ + -d linux 2>&1 | tee "${RUNNER_TEMP}/display-smoke-x11.log" + + - name: Run viewer smoke test (Wayland) + run: | + set -o pipefail + export XDG_RUNTIME_DIR="${RUNNER_TEMP}/wayland-runtime" + export WAYLAND_DISPLAY=wayland-ci + mkdir -p "${XDG_RUNTIME_DIR}" + chmod 700 "${XDG_RUNTIME_DIR}" + + weston \ + --backend=headless-backend.so \ + --use-gl \ + --no-config \ + --socket="${WAYLAND_DISPLAY}" \ + --width=1280 \ + --height=720 \ + --idle-time=0 \ + --log="${RUNNER_TEMP}/weston.log" & + weston_pid=$! + trap 'kill "${weston_pid}" 2>/dev/null || true' EXIT + + for _ in {1..50}; do + if [[ -S "${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}" ]]; then + break + fi + if ! kill -0 "${weston_pid}" 2>/dev/null; then + cat "${RUNNER_TEMP}/weston.log" + exit 1 + fi + sleep 0.1 + done + test -S "${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}" + + unset DISPLAY + GDK_BACKEND=wayland timeout 8m flutter test \ + integration_test/display_server_smoke_test.dart \ + -d linux 2>&1 | tee "${RUNNER_TEMP}/display-smoke-wayland.log" + + - name: Upload display-server logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: linux-display-smoke-logs + path: | + ${{ runner.temp }}/display-smoke-*.log + ${{ runner.temp }}/weston.log + retention-days: 5 diff --git a/.github/workflows/run-flutter-builds.yml b/.github/workflows/run-flutter-builds.yml index 8874e77f2..bcd64586a 100644 --- a/.github/workflows/run-flutter-builds.yml +++ b/.github/workflows/run-flutter-builds.yml @@ -110,7 +110,7 @@ jobs: shell: bash run: | sudo apt-get update -y - sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libdrm-dev libegl1 libegl1-mesa-dev libgl1-mesa-dri libc++-dev libc++abi-dev xvfb weston + sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libdrm-dev libegl1 libegl1-mesa-dev libc++-dev libc++abi-dev # Same priming as the Windows/macOS unit test steps above: fires the # build hooks so the Filament headers are extracted and @@ -128,63 +128,6 @@ jobs: if: matrix.name == 'Linux' run: cd quickstart && flutter pub get && flutter build linux - - name: Run quickstart viewer smoke test (X11) - if: matrix.name == 'Linux' - shell: bash - run: | - set -o pipefail - GDK_BACKEND=x11 LIBGL_ALWAYS_SOFTWARE=1 \ - MESA_GL_VERSION_OVERRIDE=4.5 \ - timeout 8m xvfb-run -a \ - -e "${RUNNER_TEMP}/xvfb.log" \ - -s '-screen 0 1280x720x24' \ - flutter test integration_test/display_server_smoke_test.dart \ - -d linux 2>&1 | tee "${RUNNER_TEMP}/display-smoke-x11.log" - working-directory: examples/flutter/quickstart - - - name: Run quickstart viewer smoke test (Wayland) - if: matrix.name == 'Linux' - shell: bash - run: | - set -o pipefail - export XDG_RUNTIME_DIR="${RUNNER_TEMP}/wayland-runtime" - export WAYLAND_DISPLAY=wayland-ci - export LIBGL_ALWAYS_SOFTWARE=1 - export MESA_GL_VERSION_OVERRIDE=4.5 - mkdir -p "${XDG_RUNTIME_DIR}" - chmod 700 "${XDG_RUNTIME_DIR}" - - weston \ - --backend=headless-backend.so \ - --use-gl \ - --no-config \ - --socket="${WAYLAND_DISPLAY}" \ - --width=1280 \ - --height=720 \ - --idle-time=0 \ - --log="${RUNNER_TEMP}/weston.log" & - weston_pid=$! - trap 'kill "${weston_pid}" 2>/dev/null || true' EXIT - - for _ in {1..50}; do - if [[ -S "${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}" ]]; then - break - fi - if ! kill -0 "${weston_pid}" 2>/dev/null; then - cat "${RUNNER_TEMP}/weston.log" - exit 1 - fi - sleep 0.1 - done - test -S "${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}" - - unset DISPLAY - GDK_BACKEND=wayland \ - timeout 8m flutter test \ - integration_test/display_server_smoke_test.dart \ - -d linux 2>&1 | tee "${RUNNER_TEMP}/display-smoke-wayland.log" - working-directory: examples/flutter/quickstart - - name: Build picking (Linux) if: matrix.name == 'Linux' run: cd picking && flutter pub get && flutter build linux @@ -196,9 +139,6 @@ jobs: name: build-logs-flutter-${{ matrix.name }} path: | ${{ github.workspace }}/thermion_dart/.dart_tool/thermion_dart/log/build.log - ${{ runner.temp }}/display-smoke-*.log - ${{ runner.temp }}/weston.log - ${{ runner.temp }}/xvfb.log retention-days: 5 android-examples: From 4d850a06b3f1580a79ca088a35e59c8e7c72b559 Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Fri, 21 Aug 2026 15:24:10 +0800 Subject: [PATCH 08/13] ci(linux): make display smoke runnable on the Fedora GPU runner - TEMPORARY push trigger on this branch while under test on the self-hosted runner; remove before merging. - Fall back to :0 when the x11-display input is absent (push events have no inputs context, which would otherwise blank DISPLAY). - Distro-aware dependency step: keep apt-get for Ubuntu, add a dnf path for Fedora that no-ops when deps are preinstalled and sudo needs a password. Co-Authored-By: Claude --- .github/workflows/linux-display-smoke.yml | 36 ++++++++++++++++++----- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/.github/workflows/linux-display-smoke.yml b/.github/workflows/linux-display-smoke.yml index 1026d2803..f72cae524 100644 --- a/.github/workflows/linux-display-smoke.yml +++ b/.github/workflows/linux-display-smoke.yml @@ -1,6 +1,10 @@ name: Linux Display Smoke on: + # TEMPORARY: auto-run on pushes to this branch while it is under test on the + # self-hosted GPU runner. Remove before merging. + push: + branches: [fix/linux-egl-context-bootstrap] workflow_dispatch: inputs: ref: @@ -19,7 +23,9 @@ jobs: runs-on: [self-hosted, linux, x64] timeout-minutes: 30 env: - DISPLAY: ${{ inputs.x11-display }} + # Push events have no inputs context; fall back to :0 so the runner's + # desktop session display is used. + DISPLAY: ${{ inputs.x11-display || ':0' }} defaults: run: working-directory: examples/flutter/quickstart @@ -41,12 +47,28 @@ jobs: - name: Install Linux dependencies working-directory: . run: | - sudo apt-get update -y - sudo apt-get install -y \ - clang cmake ninja-build pkg-config \ - libgtk-3-dev liblzma-dev libdrm-dev \ - libegl1 libegl1-mesa-dev libc++-dev libc++abi-dev \ - weston x11-utils + if command -v apt-get >/dev/null; then + sudo apt-get update -y + sudo apt-get install -y \ + clang cmake ninja-build pkg-config \ + libgtk-3-dev liblzma-dev libdrm-dev \ + libegl1 libegl1-mesa-dev libc++-dev libc++abi-dev \ + weston x11-utils + elif command -v dnf >/dev/null; then + # Fedora self-hosted runner. Dependencies are preinstalled on the + # image; only fetch missing ones, and only with passwordless sudo. + if ! sudo -n true 2>/dev/null; then + echo '::notice::No passwordless sudo; assuming dependencies are preinstalled' + exit 0 + fi + sudo dnf install -y \ + clang cmake ninja-build pkgconf-pkg-config \ + gtk3-devel libdrm-devel mesa-libEGL-devel xz-devel \ + libcxx-devel libcxxabi-devel \ + weston xdpyinfo + else + echo '::warning::No apt-get or dnf found; assuming dependencies are preinstalled' + fi # Prime generated headers before the example's CMake configure step. - name: Run thermion_flutter unit tests From 3899baf77a8ea64626bb5398a84716efa8f32232 Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Fri, 21 Aug 2026 21:49:39 +0800 Subject: [PATCH 09/13] refactor(flutter): keep widget-building out of the plugin API - ThermionFlutterPluginInitializer -> ThermionTextureBootstrap, moved into the widgets layer next to ViewerWidget; the name now says what it does (deferred texture handshake) instead of implying a scope. - The plugin no longer constructs widgets: buildInitializationScope is replaced by createContextBootstrap/destroyContextBootstrap, which default to null/no-op for platforms without the Linux OpenGL prerequisite. - The injected allocator pair stays a unit: with an injected create hook, destruction falls back to the descriptor, never the plugin. No behavior change; sequencing and dispose-races are covered by texture_bootstrap_test and lifecycle_test. Co-Authored-By: Claude --- .../src/thermion_flutter_plugin_native.dart | 22 ++---- .../lib/src/thermion_flutter_plugin.dart | 24 ++++--- .../src/texture_bootstrap.dart} | 68 ++++++++++++++----- .../lib/src/widgets/src/viewer_widget.dart | 4 +- ..._test.dart => texture_bootstrap_test.dart} | 6 +- 5 files changed, 74 insertions(+), 50 deletions(-) rename thermion_flutter/thermion_flutter/lib/src/{platform/src/thermion_flutter_plugin_initializer.dart => widgets/src/texture_bootstrap.dart} (60%) rename thermion_flutter/thermion_flutter/test/{thermion_flutter_plugin_initializer_test.dart => texture_bootstrap_test.dart} (94%) diff --git a/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_native.dart b/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_native.dart index ec140262d..5586c916d 100644 --- a/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_native.dart +++ b/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_native.dart @@ -2,14 +2,12 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart' hide View; // ignore: implementation_imports import 'package:thermion_dart/src/filament/src/implementation/ffi_filament_app.dart'; import 'native_rendering_lifecycle_controller.dart'; import 'native_texture_surface_manager.dart'; import 'platform_texture_descriptor.dart'; -import 'thermion_flutter_plugin_initializer.dart'; import '../../../thermion_flutter.dart'; /// Initializes the native Filament application and delegates frame lifecycle @@ -162,7 +160,9 @@ class ThermionFlutterPluginImpl extends ThermionFlutterPlugin { _lifecycle.resumeExplicitly(); } - Future _createContextBootstrap() async { + @internal + @override + Future createContextBootstrap() async { if (!Platform.isLinux || _resolveBackend() != Backend.OPENGL || FilamentApp.instance != null) { @@ -171,22 +171,10 @@ class ThermionFlutterPluginImpl extends ThermionFlutterPlugin { return _textureSurfaces.createContextBootstrap(); } - Future _destroyContextBootstrap(PlatformTextureDescriptor descriptor) { - return _textureSurfaces.destroyContextBootstrap(descriptor); - } - @internal @override - Widget buildInitializationScope({ - required Future Function() initialize, - required Widget child, - }) { - return ThermionFlutterPluginInitializer( - initialize: initialize, - child: child, - createContextBootstrap: _createContextBootstrap, - destroyContextBootstrap: _destroyContextBootstrap, - ); + Future destroyContextBootstrap(PlatformTextureDescriptor descriptor) { + return _textureSurfaces.destroyContextBootstrap(descriptor); } @override diff --git a/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart b/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart index 9e928c450..4c35fbace 100644 --- a/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart +++ b/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart @@ -1,13 +1,11 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart' hide View; import 'package:thermion_dart/thermion_dart.dart'; // ignore: implementation_imports import 'package:thermion_dart/src/filament/src/implementation/ffi_filament_app.dart'; import 'package:thermion_flutter/src/options.dart'; import 'package:thermion_flutter/src/platform/src/platform_texture_descriptor.dart'; -import 'package:thermion_flutter/src/platform/src/thermion_flutter_plugin_initializer.dart'; import 'platform/platform.dart'; @@ -86,16 +84,20 @@ abstract class ThermionFlutterPlugin { FilamentApp.instance?.setTargetFramerate(fps); } - /// Hosts any Flutter-side prerequisites while [initialize] creates a viewer. + /// Allocates the throwaway external texture that must be composited by + /// Flutter before the native viewer can be created. Returns null when the + /// running platform has no such prerequisite and the caller may initialize + /// immediately. + /// + /// Consumed by ThermionTextureBootstrap; not part of the public API. @internal - Widget buildInitializationScope({ - required Future Function() initialize, - required Widget child, - }) { - return ThermionFlutterPluginInitializer( - initialize: initialize, - child: child, - ); + Future createContextBootstrap() async => null; + + /// Releases a descriptor returned by [createContextBootstrap]; only ever + /// called with a non-null descriptor. Not part of the public API. + @internal + Future destroyContextBootstrap(PlatformTextureDescriptor descriptor) { + return descriptor.destroy(); } /// Creates a rendering surface and binds to the given [View]. diff --git a/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_initializer.dart b/thermion_flutter/thermion_flutter/lib/src/widgets/src/texture_bootstrap.dart similarity index 60% rename from thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_initializer.dart rename to thermion_flutter/thermion_flutter/lib/src/widgets/src/texture_bootstrap.dart index f843a7a5e..8a6b5c352 100644 --- a/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_initializer.dart +++ b/thermion_flutter/thermion_flutter/lib/src/widgets/src/texture_bootstrap.dart @@ -1,8 +1,10 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; - -import 'platform_texture_descriptor.dart'; +// ignore: implementation_imports +import 'package:thermion_flutter/src/platform/src/platform_texture_descriptor.dart'; +// ignore: implementation_imports +import 'package:thermion_flutter/src/thermion_flutter_plugin.dart'; typedef ContextBootstrapAllocator = Future Function(); @@ -10,15 +12,27 @@ typedef ContextBootstrapDestroyer = Future Function( PlatformTextureDescriptor descriptor, ); -/// Hosts the Flutter-side prerequisites for initializing the native plugin, -/// then runs [initialize]. +/// Sequences [initialize] after the texture handshake some platforms require +/// before the native viewer can be created. +/// +/// Linux OpenGL cannot create Filament's shared GL context until Flutter has +/// composited an external texture at least once. Until then there is nothing +/// on the Flutter side for Filament's context to share with. So this widget +/// renders a 1x1 [Texture] backed by a bootstrap descriptor, waits for the +/// engine to populate it ([PlatformTextureDescriptor.awaitTextureReady]), +/// runs [initialize], and only then removes the layer and destroys the +/// descriptor. +/// +/// When the allocator returns null (every platform without the prerequisite, +/// and any viewer created after the first one), the handshake is skipped and +/// [initialize] runs immediately. /// -/// Linux OpenGL needs a real [Texture] layer before Filament can initialize. -/// Other platforms skip that handshake and invoke [initialize] immediately. -/// Keeping the layer and descriptor lifecycle here prevents viewer widgets -/// from depending on EGL or deferred texture details. -class ThermionFlutterPluginInitializer extends StatefulWidget { - const ThermionFlutterPluginInitializer({ +/// The allocator pair comes from [ThermionFlutterPlugin] by default; tests +/// may inject their own. If a create hook is injected, destruction stays +/// within the injected pair (falling back to the descriptor itself) and the +/// plugin is never consulted. +class ThermionTextureBootstrap extends StatefulWidget { + const ThermionTextureBootstrap({ super.key, required this.initialize, required this.child, @@ -32,16 +46,17 @@ class ThermionFlutterPluginInitializer extends StatefulWidget { final ContextBootstrapDestroyer? destroyContextBootstrap; @override - State createState() => - _ThermionFlutterPluginInitializerState(); + State createState() => + _ThermionTextureBootstrapState(); } -class _ThermionFlutterPluginInitializerState - extends State { +class _ThermionTextureBootstrapState extends State { PlatformTextureDescriptor? _descriptor; Future? _destroyFuture; bool _disposing = false; + bool get _injected => widget.createContextBootstrap != null; + @override void initState() { super.initState(); @@ -63,7 +78,7 @@ class _ThermionFlutterPluginInitializerState } Future _bootstrap() async { - final descriptor = await widget.createContextBootstrap?.call(); + final descriptor = await _allocate(); _descriptor = descriptor; try { @@ -100,10 +115,27 @@ class _ThermionFlutterPluginInitializerState } } + Future _allocate() { + final create = widget.createContextBootstrap; + if (create != null) { + return create(); + } + return ThermionFlutterPlugin.instance.createContextBootstrap(); + } + Future _destroy(PlatformTextureDescriptor descriptor) { - return _destroyFuture ??= - widget.destroyContextBootstrap?.call(descriptor) ?? - descriptor.destroy(); + return _destroyFuture ??= () { + final destroy = widget.destroyContextBootstrap; + if (destroy != null) { + return destroy(descriptor); + } + if (_injected) { + return descriptor.destroy(); + } + return ThermionFlutterPlugin.instance.destroyContextBootstrap( + descriptor, + ); + }(); } @override diff --git a/thermion_flutter/thermion_flutter/lib/src/widgets/src/viewer_widget.dart b/thermion_flutter/thermion_flutter/lib/src/widgets/src/viewer_widget.dart index 83a892f9e..a595df874 100644 --- a/thermion_flutter/thermion_flutter/lib/src/widgets/src/viewer_widget.dart +++ b/thermion_flutter/thermion_flutter/lib/src/widgets/src/viewer_widget.dart @@ -4,6 +4,8 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:thermion_flutter/thermion_flutter.dart' hide Texture; +import 'texture_bootstrap.dart'; + enum ManipulatorType { NONE, ORBIT, FREE_FLIGHT } class ViewerWidget extends StatefulWidget { @@ -413,7 +415,7 @@ class _ViewerWidgetState extends State { final child = viewport == null ? widget.initial : SizedBox.expand(child: viewport); - return ThermionFlutterPlugin.instance.buildInitializationScope( + return ThermionTextureBootstrap( initialize: _initializeViewer, child: child, ); diff --git a/thermion_flutter/thermion_flutter/test/thermion_flutter_plugin_initializer_test.dart b/thermion_flutter/thermion_flutter/test/texture_bootstrap_test.dart similarity index 94% rename from thermion_flutter/thermion_flutter/test/thermion_flutter_plugin_initializer_test.dart rename to thermion_flutter/thermion_flutter/test/texture_bootstrap_test.dart index 8299b279a..a60a38d14 100644 --- a/thermion_flutter/thermion_flutter/test/thermion_flutter_plugin_initializer_test.dart +++ b/thermion_flutter/thermion_flutter/test/texture_bootstrap_test.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:thermion_flutter/src/platform/src/platform_texture_descriptor.dart'; -import 'package:thermion_flutter/src/platform/src/thermion_flutter_plugin_initializer.dart'; +import 'package:thermion_flutter/src/widgets/src/texture_bootstrap.dart'; void main() { testWidgets( @@ -14,7 +14,7 @@ void main() { descriptor.events = events; await tester.pumpWidget( - ThermionFlutterPluginInitializer( + ThermionTextureBootstrap( createContextBootstrap: () async { events.add('create'); return descriptor; @@ -52,7 +52,7 @@ void main() { var initializeCount = 0; await tester.pumpWidget( - ThermionFlutterPluginInitializer( + ThermionTextureBootstrap( createContextBootstrap: () async => descriptor, initialize: () async { initializeCount++; From 7217dc96d0c633ddf667c7a4121ae37ed5c452ef Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 21 Aug 2026 13:51:14 +0000 Subject: [PATCH 10/13] chore: update generated artifacts + format (CI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with GitHub Actions --- .../lib/src/widgets/src/texture_bootstrap.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/thermion_flutter/thermion_flutter/lib/src/widgets/src/texture_bootstrap.dart b/thermion_flutter/thermion_flutter/lib/src/widgets/src/texture_bootstrap.dart index 8a6b5c352..a934c1425 100644 --- a/thermion_flutter/thermion_flutter/lib/src/widgets/src/texture_bootstrap.dart +++ b/thermion_flutter/thermion_flutter/lib/src/widgets/src/texture_bootstrap.dart @@ -132,9 +132,7 @@ class _ThermionTextureBootstrapState extends State { if (_injected) { return descriptor.destroy(); } - return ThermionFlutterPlugin.instance.destroyContextBootstrap( - descriptor, - ); + return ThermionFlutterPlugin.instance.destroyContextBootstrap(descriptor); }(); } From e6f1b5bd3f125b4f321a49c2e61500d605176710 Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Fri, 21 Aug 2026 22:07:17 +0800 Subject: [PATCH 11/13] ci(linux): drop temporary push trigger from display smoke The branch was validated on the self-hosted X11/Wayland runner; keep the workflow dispatch-only for future manual runs. Co-Authored-By: Claude --- .github/workflows/linux-display-smoke.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/linux-display-smoke.yml b/.github/workflows/linux-display-smoke.yml index f72cae524..dd01d87da 100644 --- a/.github/workflows/linux-display-smoke.yml +++ b/.github/workflows/linux-display-smoke.yml @@ -1,10 +1,6 @@ name: Linux Display Smoke on: - # TEMPORARY: auto-run on pushes to this branch while it is under test on the - # self-hosted GPU runner. Remove before merging. - push: - branches: [fix/linux-egl-context-bootstrap] workflow_dispatch: inputs: ref: @@ -23,8 +19,8 @@ jobs: runs-on: [self-hosted, linux, x64] timeout-minutes: 30 env: - # Push events have no inputs context; fall back to :0 so the runner's - # desktop session display is used. + # Fall back to :0 (the runner's desktop session display) when the input + # is not provided. DISPLAY: ${{ inputs.x11-display || ':0' }} defaults: run: From 9aa4b3d164a322abb632e50a55516439c2a187cb Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Fri, 21 Aug 2026 23:03:52 +0800 Subject: [PATCH 12/13] refactor(linux): simplify EGL bootstrap transport --- .../src/opengl/linux/LinuxOpenGLContext.cpp | 57 +- ...d_channel_platform_texture_descriptor.dart | 25 - .../src/native_texture_surface_manager.dart | 11 - .../platform_texture_descriptor_registry.dart | 18 +- ...rm_texture_descriptor_registry_native.dart | 20 - .../src/thermion_flutter_plugin_native.dart | 26 +- .../lib/src/thermion_flutter_plugin.dart | 15 +- .../src/widgets/src/texture_bootstrap.dart | 79 ++- .../thermion_flutter/linux/egl_texture.cc | 103 +--- .../thermion_flutter/linux/egl_texture.h | 28 +- .../linux/thermion_flutter_plugin.cc | 511 +++++------------- .../test/texture_bootstrap_test.dart | 78 +-- 12 files changed, 272 insertions(+), 699 deletions(-) diff --git a/thermion_dart/native/src/opengl/linux/LinuxOpenGLContext.cpp b/thermion_dart/native/src/opengl/linux/LinuxOpenGLContext.cpp index 718df0802..1310aa786 100644 --- a/thermion_dart/native/src/opengl/linux/LinuxOpenGLContext.cpp +++ b/thermion_dart/native/src/opengl/linux/LinuxOpenGLContext.cpp @@ -76,13 +76,12 @@ class LinuxOpenGLContext::Impl { }); } - if (_platform) { - ThermionPlatformEGLHeadless_Destroy(_platform); - _platform = nullptr; - } - if (_eglThread.joinable()) { RunOnEglThread([this]() { + if (_platform) { + ThermionPlatformEGLHeadless_Destroy(_platform); + _platform = nullptr; + } if (_context != EGL_NO_CONTEXT && _display != EGL_NO_DISPLAY) { eglBindAPI(EGL_OPENGL_API); @@ -144,22 +143,6 @@ class LinuxOpenGLContext::Impl { _display = static_cast(borrowedDisplay); if (_display != EGL_NO_DISPLAY) { _ownsDisplay = false; - // eglInitialize is idempotent for an initialized EGLDisplay. Do - // this only after populate() has captured Flutter's real display: - // on NVIDIA it also makes the desktop-GL client API usable on the - // display that Flutter initialized for GLES. - EGLint major = 0; - EGLint minor = 0; - if (!eglInitialize(_display, &major, &minor)) { - _lastError = - "Failed to initialize Flutter's captured EGLDisplay"; - LOG_ERROR("Failed to initialize Flutter EGL display"); - _display = EGL_NO_DISPLAY; - return; - } - std::cerr << "[ThermionGL:Context] Using Flutter EGL display=" - << _display << " (" << major << "." << minor << ")" - << std::endl; } else { // Non-Flutter fallback: obtain a display tied to the GBM device. PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT = @@ -182,15 +165,6 @@ class LinuxOpenGLContext::Impl { return; } _ownsDisplay = true; - EGLint major, minor; - if (!eglInitialize(_display, &major, &minor)) { - _lastError = "Failed to initialize EGLDisplay"; - LOG_ERROR("Failed to initialize EGL display"); - _display = EGL_NO_DISPLAY; - return; - } - std::cerr << "[ThermionGL:Context] EGL initialized: " - << major << "." << minor << std::endl; } // Initialize desktop EGL on an isolated thread. Flutter's platform @@ -198,6 +172,21 @@ class LinuxOpenGLContext::Impl { // EGL_BAD_ACCESS when a second client API is activated there. StartEglThread(); RunOnEglThread([this]() { + EGLint major = 0; + EGLint minor = 0; + if (!eglInitialize(_display, &major, &minor)) { + _lastError = _ownsDisplay + ? "Failed to initialize EGLDisplay" + : "Failed to initialize Flutter's captured EGLDisplay"; + LOG_ERROR("Failed to initialize EGL display"); + _display = EGL_NO_DISPLAY; + return; + } + std::cerr << "[ThermionGL:Context] Using " + << (_ownsDisplay ? "GBM" : "Flutter") + << " EGL display=" << _display << " (" << major << "." + << minor << ")" << std::endl; + // Step 4: Choose EGL config // Must bind EGL_OPENGL_API (not ES) to match Filament's // PlatformEGLHeadless which uses full OpenGL 4.1 on Linux desktop. @@ -364,9 +353,11 @@ class LinuxOpenGLContext::Impl { } void* GetPlatform() { - if (!_platform) { - _platform = ThermionPlatformEGLHeadless_Create(_display); - } + RunOnEglThread([this]() { + if (!_platform) { + _platform = ThermionPlatformEGLHeadless_Create(_display); + } + }); return _platform; } diff --git a/thermion_flutter/thermion_flutter/lib/src/platform/src/method_channel_platform_texture_descriptor.dart b/thermion_flutter/thermion_flutter/lib/src/platform/src/method_channel_platform_texture_descriptor.dart index fa37cdcc1..5c7cecf6f 100644 --- a/thermion_flutter/thermion_flutter/lib/src/platform/src/method_channel_platform_texture_descriptor.dart +++ b/thermion_flutter/thermion_flutter/lib/src/platform/src/method_channel_platform_texture_descriptor.dart @@ -55,31 +55,6 @@ class MethodChannelPlatformTextureDescriptor extends PlatformTextureDescriptor { ); } - /// Registers a Flutter texture whose GL name is allocated by Flutter's - /// raster context during the first populate callback. - /// - /// This allocation deliberately bypasses the normal native texture creation - /// path because Filament's OpenGL context does not exist yet. - static Future - allocateContextBootstrap(MethodChannel channel, int width, int height) async { - final flutterTextureId = await channel.invokeMethod( - 'createContextBootstrap', - [width, height], - ); - if (flutterTextureId == null || flutterTextureId < 0) { - throw StateError('Failed to create Flutter context bootstrap texture'); - } - return MethodChannelPlatformTextureDescriptor( - channel, - flutterTextureId: flutterTextureId, - hardwareId: 0, - windowHandle: 0, - width: width, - height: height, - deferred: true, - ); - } - /// Waits for populate() to create the GL texture (deferred path). /// Returns the hardware texture ID once ready. @override diff --git a/thermion_flutter/thermion_flutter/lib/src/platform/src/native_texture_surface_manager.dart b/thermion_flutter/thermion_flutter/lib/src/platform/src/native_texture_surface_manager.dart index e75d9bc3b..4f91bfc48 100644 --- a/thermion_flutter/thermion_flutter/lib/src/platform/src/native_texture_surface_manager.dart +++ b/thermion_flutter/thermion_flutter/lib/src/platform/src/native_texture_surface_manager.dart @@ -79,17 +79,6 @@ class NativeTextureSurfaceManager { bool get hasUnavailableSurfaces => registry.hasUnavailableSurfaces; - /// Creates the pre-engine texture that Flutter populates on its raster - /// thread, allowing native code to capture the EGL context that Filament - /// must share with. - Future createContextBootstrap() { - return registry.serialized(registry.createContextBootstrap); - } - - Future destroyContextBootstrap(PlatformTextureDescriptor descriptor) { - return registry.serialized(() => registry.destroy(descriptor)); - } - Future getFilamentRenderingContext( Backend backend, ) { diff --git a/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry.dart b/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry.dart index f8bd9b3d6..bfc2a73ab 100644 --- a/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry.dart +++ b/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry.dart @@ -34,22 +34,8 @@ class PlatformTextureDescriptorRegistry { bool contains(PlatformTextureDescriptor descriptor) => _descriptors.any((candidate) => identical(candidate, descriptor)); - Future create(int width, int height) { - return createWith(_allocator, width, height); - } - - /// Allocates and tracks a descriptor using a specialized allocator. - /// - /// Most descriptors use the registry's default allocator. Initialization - /// textures are the exception: Linux OpenGL must register a deferred Flutter - /// texture before the Filament context exists, so it cannot use the normal - /// platform-surface allocation path. - Future createWith( - PlatformTextureDescriptorAllocator allocator, - int width, - int height, - ) async { - final descriptor = await allocator(width, height); + Future create(int width, int height) async { + final descriptor = await _allocator(width, height); _descriptors.add(descriptor); return descriptor; } diff --git a/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry_native.dart b/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry_native.dart index ad63e682e..6e4bdc2d0 100644 --- a/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry_native.dart +++ b/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry_native.dart @@ -94,26 +94,6 @@ class NativePlatformTextureDescriptorRegistry throw UnsupportedError('Platform textures are not supported on $Platform'); } - /// Registers the texture used to import Flutter's actual raster EGL context - /// before Filament initializes. - Future createContextBootstrap() { - if (!Platform.isLinux) { - throw UnsupportedError( - 'Flutter context bootstrapping is only supported on Linux', - ); - } - return createWith( - (width, height) => - MethodChannelPlatformTextureDescriptor.allocateContextBootstrap( - channel, - width, - height, - ), - 1, - 1, - ); - } - Future getFilamentRenderingContext( Backend backend, ) async { diff --git a/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_native.dart b/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_native.dart index 5586c916d..377bf1cb0 100644 --- a/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_native.dart +++ b/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_native.dart @@ -8,6 +8,7 @@ import 'package:thermion_dart/src/filament/src/implementation/ffi_filament_app.d import 'native_rendering_lifecycle_controller.dart'; import 'native_texture_surface_manager.dart'; import 'platform_texture_descriptor.dart'; +import 'platform_texture_descriptor_registry_native.dart'; import '../../../thermion_flutter.dart'; /// Initializes the native Filament application and delegates frame lifecycle @@ -162,19 +163,36 @@ class ThermionFlutterPluginImpl extends ThermionFlutterPlugin { @internal @override - Future createContextBootstrap() async { + Future createContextBootstrap() async { if (!Platform.isLinux || _resolveBackend() != Backend.OPENGL || FilamentApp.instance != null) { return null; } - return _textureSurfaces.createContextBootstrap(); + final textureId = await NativePlatformTextureDescriptorRegistry.channel + .invokeMethod('createContextBootstrap', const [1, 1]); + if (textureId == null || textureId < 0) { + throw StateError('Failed to create Flutter context bootstrap texture'); + } + return textureId; } @internal @override - Future destroyContextBootstrap(PlatformTextureDescriptor descriptor) { - return _textureSurfaces.destroyContextBootstrap(descriptor); + Future awaitContextBootstrap(int textureId) async { + await NativePlatformTextureDescriptorRegistry.channel.invokeMethod( + 'awaitTextureReady', + textureId, + ); + } + + @internal + @override + Future destroyContextBootstrap(int textureId) async { + await NativePlatformTextureDescriptorRegistry.channel.invokeMethod( + 'destroyTexture', + textureId, + ); } @override diff --git a/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart b/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart index 4c35fbace..391c4a786 100644 --- a/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart +++ b/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart @@ -91,14 +91,17 @@ abstract class ThermionFlutterPlugin { /// /// Consumed by ThermionTextureBootstrap; not part of the public API. @internal - Future createContextBootstrap() async => null; + Future createContextBootstrap() async => null; - /// Releases a descriptor returned by [createContextBootstrap]; only ever - /// called with a non-null descriptor. Not part of the public API. + /// Waits until Flutter has populated a texture returned by + /// [createContextBootstrap]. Not part of the public API. @internal - Future destroyContextBootstrap(PlatformTextureDescriptor descriptor) { - return descriptor.destroy(); - } + Future awaitContextBootstrap(int textureId) async {} + + /// Releases a texture returned by [createContextBootstrap]. Not part of the + /// public API. + @internal + Future destroyContextBootstrap(int textureId) async {} /// Creates a rendering surface and binds to the given [View]. /// This is an internal method, don't call this yourself unless you are a diff --git a/thermion_flutter/thermion_flutter/lib/src/widgets/src/texture_bootstrap.dart b/thermion_flutter/thermion_flutter/lib/src/widgets/src/texture_bootstrap.dart index a934c1425..82b3d76e7 100644 --- a/thermion_flutter/thermion_flutter/lib/src/widgets/src/texture_bootstrap.dart +++ b/thermion_flutter/thermion_flutter/lib/src/widgets/src/texture_bootstrap.dart @@ -2,15 +2,11 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; // ignore: implementation_imports -import 'package:thermion_flutter/src/platform/src/platform_texture_descriptor.dart'; -// ignore: implementation_imports import 'package:thermion_flutter/src/thermion_flutter_plugin.dart'; -typedef ContextBootstrapAllocator = - Future Function(); -typedef ContextBootstrapDestroyer = Future Function( - PlatformTextureDescriptor descriptor, -); +typedef ContextBootstrapAllocator = Future Function(); +typedef ContextBootstrapWaiter = Future Function(int textureId); +typedef ContextBootstrapDestroyer = Future Function(int textureId); /// Sequences [initialize] after the texture handshake some platforms require /// before the native viewer can be created. @@ -18,31 +14,29 @@ typedef ContextBootstrapDestroyer = Future Function( /// Linux OpenGL cannot create Filament's shared GL context until Flutter has /// composited an external texture at least once. Until then there is nothing /// on the Flutter side for Filament's context to share with. So this widget -/// renders a 1x1 [Texture] backed by a bootstrap descriptor, waits for the -/// engine to populate it ([PlatformTextureDescriptor.awaitTextureReady]), -/// runs [initialize], and only then removes the layer and destroys the -/// descriptor. +/// renders a 1x1 [Texture], waits for the engine to populate it, runs +/// [initialize], and only then removes the layer and destroys the texture. /// /// When the allocator returns null (every platform without the prerequisite, /// and any viewer created after the first one), the handshake is skipped and /// [initialize] runs immediately. /// -/// The allocator pair comes from [ThermionFlutterPlugin] by default; tests -/// may inject their own. If a create hook is injected, destruction stays -/// within the injected pair (falling back to the descriptor itself) and the -/// plugin is never consulted. +/// The lifecycle hooks come from [ThermionFlutterPlugin] by default; tests may +/// inject their own. class ThermionTextureBootstrap extends StatefulWidget { const ThermionTextureBootstrap({ super.key, required this.initialize, required this.child, this.createContextBootstrap, + this.awaitContextBootstrap, this.destroyContextBootstrap, }); final Future Function() initialize; final Widget child; final ContextBootstrapAllocator? createContextBootstrap; + final ContextBootstrapWaiter? awaitContextBootstrap; final ContextBootstrapDestroyer? destroyContextBootstrap; @override @@ -51,12 +45,10 @@ class ThermionTextureBootstrap extends StatefulWidget { } class _ThermionTextureBootstrapState extends State { - PlatformTextureDescriptor? _descriptor; + int? _textureId; Future? _destroyFuture; bool _disposing = false; - bool get _injected => widget.createContextBootstrap != null; - @override void initState() { super.initState(); @@ -78,20 +70,20 @@ class _ThermionTextureBootstrapState extends State { } Future _bootstrap() async { - final descriptor = await _allocate(); - _descriptor = descriptor; + final textureId = await _allocate(); + _textureId = textureId; try { if (_disposing) return; - if (descriptor != null) { + if (textureId != null) { if (mounted) { setState(() {}); } try { - descriptor.hardwareId = await descriptor.awaitTextureReady(); + await _awaitReady(textureId); } catch (_) { - // Destroying the descriptor is how dispose() cancels a pending + // Destroying the texture is how dispose() cancels a pending // native populate handshake. if (_disposing) return; rethrow; @@ -101,21 +93,21 @@ class _ThermionTextureBootstrapState extends State { if (_disposing) return; await widget.initialize(); } finally { - if (identical(_descriptor, descriptor)) { - _descriptor = null; + if (_textureId == textureId) { + _textureId = null; } - if (descriptor != null && mounted && !_disposing) { + if (textureId != null && mounted && !_disposing) { // Remove the Texture layer before unregistering its native texture. setState(() {}); await WidgetsBinding.instance.endOfFrame; } - if (descriptor != null) { - await _destroy(descriptor); + if (textureId != null) { + await _destroy(textureId); } } } - Future _allocate() { + Future _allocate() { final create = widget.createContextBootstrap; if (create != null) { return create(); @@ -123,26 +115,31 @@ class _ThermionTextureBootstrapState extends State { return ThermionFlutterPlugin.instance.createContextBootstrap(); } - Future _destroy(PlatformTextureDescriptor descriptor) { + Future _awaitReady(int textureId) { + final wait = widget.awaitContextBootstrap; + if (wait != null) { + return wait(textureId); + } + return ThermionFlutterPlugin.instance.awaitContextBootstrap(textureId); + } + + Future _destroy(int textureId) { return _destroyFuture ??= () { final destroy = widget.destroyContextBootstrap; if (destroy != null) { - return destroy(descriptor); - } - if (_injected) { - return descriptor.destroy(); + return destroy(textureId); } - return ThermionFlutterPlugin.instance.destroyContextBootstrap(descriptor); + return ThermionFlutterPlugin.instance.destroyContextBootstrap(textureId); }(); } @override void dispose() { _disposing = true; - final descriptor = _descriptor; - if (descriptor != null) { + final textureId = _textureId; + if (textureId != null) { unawaited( - _destroy(descriptor).catchError((Object error, StackTrace stack) { + _destroy(textureId).catchError((Object error, StackTrace stack) { FlutterError.reportError( FlutterErrorDetails( exception: error, @@ -161,8 +158,8 @@ class _ThermionTextureBootstrapState extends State { @override Widget build(BuildContext context) { - final descriptor = _descriptor; - if (descriptor == null) { + final textureId = _textureId; + if (textureId == null) { return widget.child; } @@ -176,7 +173,7 @@ class _ThermionTextureBootstrapState extends State { child: SizedBox.square( dimension: 1, child: Texture( - textureId: descriptor.flutterTextureId, + textureId: textureId, filterQuality: FilterQuality.none, freeze: false, ), diff --git a/thermion_flutter/thermion_flutter/linux/egl_texture.cc b/thermion_flutter/thermion_flutter/linux/egl_texture.cc index 5cb0465b2..9955cdbe8 100644 --- a/thermion_flutter/thermion_flutter/linux/egl_texture.cc +++ b/thermion_flutter/thermion_flutter/linux/egl_texture.cc @@ -84,10 +84,9 @@ thermion_texture_populate(FlTextureGL *texture, ThermionTextureGL *self = THERMION_TEXTURE_GL(texture); - // Direct sharing path: texture is directly visible from Flutter's context. - // If gl_texture_id == 0, this is a deferred texture — create it now on - // Flutter's render context (which is guaranteed current during populate). - if (self->use_direct_sharing) { + // The bootstrap texture is allocated while Flutter's render context is + // current, solely to capture that context before Filament initializes. + if (self->kind == THERMION_TEXTURE_KIND_CONTEXT_BOOTSTRAP) { if (self->gl_texture_id == 0) { EGLContext flutterContext = eglGetCurrentContext(); EGLDisplay flutterDisplay = eglGetCurrentDisplay(); @@ -178,54 +177,7 @@ thermion_texture_populate(FlTextureGL *texture, return TRUE; } - // EGLImage bridge path: the source texture lives on Filament's desktop - // context. On first populate, bind its EGLImage to a Flutter-owned texture - // name on Flutter's current context. - if (self->use_egl_image) { - if (!self->initialized) { - ensure_egl_procs(); - if (!s_glEGLImageTargetTexture2DOES) { - g_set_error(error, g_quark_from_string("thermion"), 1, - "glEGLImageTargetTexture2DOES not available"); - return FALSE; - } - - if (self->egl_image == EGL_NO_IMAGE_KHR) { - g_set_error(error, g_quark_from_string("thermion"), 2, - "No EGLImage available"); - return FALSE; - } - - // Create a new texture on Flutter's context and bind the EGLImage - glGenTextures(1, &self->flutter_gl_texture_id); - glBindTexture(GL_TEXTURE_2D, self->flutter_gl_texture_id); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - - while (glGetError() != GL_NO_ERROR) {} - - s_glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, self->egl_image); - - GLenum glErr = glGetError(); - if (glErr != GL_NO_ERROR) { - std::cerr << "[ThermionEGL] GL error after EGLImage import: 0x" - << std::hex << glErr << std::dec << std::endl; - } - - glBindTexture(GL_TEXTURE_2D, 0); - self->initialized = TRUE; - } - - *target = GL_TEXTURE_2D; - *name = self->flutter_gl_texture_id; - *width = self->width; - *height = self->height; - return TRUE; - } - - // DMA-BUF path (fallback): lazy-init EGLImage import on first populate + // DMA-BUF path: lazy-init EGLImage import on first populate if (!self->initialized) { ensure_egl_procs(); @@ -316,27 +268,18 @@ static void thermion_texture_gl_dispose(GObject* object) { self->pending_ready_call = nullptr; } - if (self->use_direct_sharing) { - // Direct sharing: texture is owned by the plugin (deleted on utility context). + if (self->kind == THERMION_TEXTURE_KIND_CONTEXT_BOOTSTRAP) { + // Bootstrap texture is owned by the plugin and deleted on a context in + // Flutter's share group. // Nothing to clean up here — just zero out. self->gl_texture_id = 0; G_OBJECT_CLASS(thermion_texture_gl_parent_class)->dispose(object); return; } - if (self->use_egl_image) { - // EGLImage path: clean up the Flutter-side texture (created on Flutter's context) - if (self->flutter_gl_texture_id != 0) { - glDeleteTextures(1, &self->flutter_gl_texture_id); - self->flutter_gl_texture_id = 0; - } - // The source texture (gl_texture_id) is cleaned up by the plugin. + if (self->gl_texture_id != 0) { + glDeleteTextures(1, &self->gl_texture_id); self->gl_texture_id = 0; - } else { - if (self->gl_texture_id != 0) { - glDeleteTextures(1, &self->gl_texture_id); - self->gl_texture_id = 0; - } } if (self->egl_image != EGL_NO_IMAGE_KHR && s_eglDestroyImageKHR) { @@ -357,7 +300,6 @@ void thermion_texture_gl_class_init(ThermionTextureGLClass* klass) { void thermion_texture_gl_init(ThermionTextureGL* self) { self->gl_texture_id = 0; - self->flutter_gl_texture_id = 0; self->width = 0; self->height = 0; self->registrar = nullptr; @@ -369,9 +311,7 @@ void thermion_texture_gl_init(ThermionTextureGL* self) { self->egl_image = EGL_NO_IMAGE_KHR; self->initialized = FALSE; self->surface_id = -1; - self->use_egl_image = FALSE; - self->use_direct_sharing = FALSE; - self->is_context_bootstrap = FALSE; + self->kind = THERMION_TEXTURE_KIND_DMA_BUF; self->pending_ready_call = nullptr; } @@ -394,26 +334,6 @@ ThermionTextureGL* thermion_texture_gl_create( return textureGL; } -ThermionTextureGL* thermion_texture_gl_create_shared( - uint32_t width, uint32_t height, - GLuint gl_texture_id, - EGLImage egl_image, - int64_t surface_id, - FlTextureRegistrar* registrar) -{ - auto textureGL = THERMION_TEXTURE_GL(g_object_new(thermion_texture_gl_get_type(), nullptr)); - textureGL->width = width; - textureGL->height = height; - textureGL->gl_texture_id = gl_texture_id; - textureGL->egl_image = egl_image; - textureGL->surface_id = surface_id; - textureGL->registrar = registrar; - textureGL->use_egl_image = TRUE; - // initialized = FALSE so populate() will import the EGLImage on first call - - return textureGL; -} - ThermionTextureGL* thermion_texture_gl_create_context_bootstrap( uint32_t width, uint32_t height, FlTextureRegistrar* registrar) @@ -423,8 +343,7 @@ ThermionTextureGL* thermion_texture_gl_create_context_bootstrap( textureGL->width = width; textureGL->height = height; textureGL->registrar = registrar; - textureGL->use_direct_sharing = TRUE; - textureGL->is_context_bootstrap = TRUE; + textureGL->kind = THERMION_TEXTURE_KIND_CONTEXT_BOOTSTRAP; // populate() creates gl_texture_id while Flutter's raster context is // current and resolves the pending awaitTextureReady call. textureGL->gl_texture_id = 0; diff --git a/thermion_flutter/thermion_flutter/linux/egl_texture.h b/thermion_flutter/thermion_flutter/linux/egl_texture.h index 27dcf45d4..1ff596412 100644 --- a/thermion_flutter/thermion_flutter/linux/egl_texture.h +++ b/thermion_flutter/thermion_flutter/linux/egl_texture.h @@ -21,6 +21,11 @@ G_BEGIN_DECLS +typedef enum { + THERMION_TEXTURE_KIND_DMA_BUF, + THERMION_TEXTURE_KIND_CONTEXT_BOOTSTRAP, +} ThermionTextureKind; + #define THERMION_TEXTURE_GL(obj) \ (G_TYPE_CHECK_INSTANCE_CAST((obj), thermion_texture_gl_get_type(), \ ThermionTextureGL)) @@ -40,15 +45,7 @@ struct _ThermionTextureGL { EGLImage egl_image; gboolean initialized; int64_t surface_id; // for Blit() and destruction - // EGLImage bridge path: texture bridged from Filament's context to - // Flutter's render context via EGLImage. - gboolean use_egl_image; - // Flutter-side GL texture (created on Flutter's context, backed by egl_image) - GLuint flutter_gl_texture_id; - // Direct sharing path: same EGL share group as Flutter, no EGLImage needed - gboolean use_direct_sharing; - // Pre-engine texture used only to capture Flutter's raster EGL context. - gboolean is_context_bootstrap; + ThermionTextureKind kind; // Deferred "awaitTextureReady" response (stored until populate creates the GL texture) FlMethodCall* pending_ready_call; }; @@ -71,15 +68,6 @@ FLUTTER_PLUGIN_EXPORT ThermionTextureGL* thermion_texture_gl_create( int64_t surface_id, FlTextureRegistrar* registrar); -// EGLImage bridge path: wraps a GL texture + EGLImage. On first populate, -// the EGLImage is imported into a new texture on Flutter's own GL context. -FLUTTER_PLUGIN_EXPORT ThermionTextureGL* thermion_texture_gl_create_shared( - uint32_t width, uint32_t height, - GLuint gl_texture_id, - EGLImage egl_image, - int64_t surface_id, - FlTextureRegistrar* registrar); - // Pre-engine initialization path. The GL texture is created lazily from // populate(), while Flutter's raster EGL context is current. FLUTTER_PLUGIN_EXPORT ThermionTextureGL* @@ -90,8 +78,8 @@ thermion_texture_gl_create_context_bootstrap( FLUTTER_PLUGIN_EXPORT void thermion_texture_gl_destroy(ThermionTextureGL* texture); // Flutter's render context, captured during the first deferred populate(). -// Used by ensure_opengl_context() to select the compatible direct or DMA-BUF -// pathway on Flutter's actual EGLDisplay. +// Used by ensure_opengl_context() to create the DMA-BUF producer and consumer +// contexts on Flutter's actual EGLDisplay. extern EGLContext thermion_flutter_render_context; extern EGLDisplay thermion_flutter_render_display; extern EGLenum thermion_flutter_render_api; diff --git a/thermion_flutter/thermion_flutter/linux/thermion_flutter_plugin.cc b/thermion_flutter/thermion_flutter/linux/thermion_flutter_plugin.cc index 6fb29511b..a1636a245 100644 --- a/thermion_flutter/thermion_flutter/linux/thermion_flutter_plugin.cc +++ b/thermion_flutter/thermion_flutter/linux/thermion_flutter_plugin.cc @@ -18,14 +18,6 @@ #include "egl_texture.h" -// EGL_KHR_gl_texture_2D_image constants (in case epoxy doesn't define them) -#ifndef EGL_GL_TEXTURE_2D_KHR -#define EGL_GL_TEXTURE_2D_KHR 0x30B1 -#endif -#ifndef EGL_GL_TEXTURE_LEVEL_KHR -#define EGL_GL_TEXTURE_LEVEL_KHR 0x30BC -#endif - // Calling Epoxy's eglDestroyImageKHR wrapper without a current EGL context // makes provider selection depend on thread-local state. Teardown deliberately // runs after Flutter releases its texture, so resolve the EGL entry point @@ -56,7 +48,6 @@ static bool destroy_egl_image(EGLDisplay display, EGLImage image) #include "vulkan/linux/LinuxVulkanContext.h" #include "LinuxOpenGLContext.h" -#include "ThermionPlatformEGLHeadlessAPI.h" #include "vulkan/ExternalVulkanImage.h" // Backend type constants (match Dart Backend enum indices) @@ -117,19 +108,11 @@ struct _ThermionFlutterPlugin std::unordered_map *external_images; // OpenGL path — imports Flutter's EGL context before Filament starts. - EGLContext flutter_egl_context; // Flutter's own context (captured, not owned) EGLenum flutter_egl_api; - // Used for plugin GL operations when Flutter exposes desktop OpenGL. - EGLContext utility_egl_context; - // Used to release Flutter-owned texture names when Flutter exposes GLES and - // Filament must run through the cross-API DMA-BUF pathway. + // Used to release Flutter-owned texture names imported from DMA-BUF. EGLContext flutter_utility_egl_context; EGLDisplay egl_display; // Flutter's EGL display - EGLConfig egl_config; // config matching Flutter's context - gboolean use_direct_opengl; // TRUE only for compatible desktop GL - void* thermion_platform; // standalone OpenGLPlatform (EGLHeadless) - - // OpenGL path — fallback (LinuxOpenGLContext with GBM/DMA-BUF) + // OpenGL producer context with GBM/DMA-BUF transport. thermion::opengl::linux_platform::LinuxOpenGLContext *opengl_context; std::string opengl_initialization_error; @@ -146,11 +129,6 @@ static void destroy_all_contexts(ThermionFlutterPlugin *self) delete self->vulkan_context; self->vulkan_context = nullptr; } - if (self->thermion_platform) - { - ThermionPlatformEGLHeadless_Destroy(self->thermion_platform); - self->thermion_platform = nullptr; - } if (self->flutter_utility_egl_context != EGL_NO_CONTEXT && self->egl_display != EGL_NO_DISPLAY) { @@ -158,22 +136,13 @@ static void destroy_all_contexts(ThermionFlutterPlugin *self) self->egl_display, self->flutter_utility_egl_context); self->flutter_utility_egl_context = EGL_NO_CONTEXT; } - if (self->utility_egl_context != EGL_NO_CONTEXT && - self->egl_display != EGL_NO_DISPLAY) - { - eglDestroyContext(self->egl_display, self->utility_egl_context); - self->utility_egl_context = EGL_NO_CONTEXT; - } if (self->opengl_context) { delete self->opengl_context; self->opengl_context = nullptr; } - self->flutter_egl_context = EGL_NO_CONTEXT; self->flutter_egl_api = EGL_NONE; self->egl_display = EGL_NO_DISPLAY; - self->egl_config = nullptr; - self->use_direct_opengl = FALSE; thermion_flutter_render_context = EGL_NO_CONTEXT; thermion_flutter_render_display = EGL_NO_DISPLAY; thermion_flutter_render_api = EGL_NONE; @@ -191,9 +160,120 @@ static void ensure_vulkan_context(ThermionFlutterPlugin *self) } } +static EGLContext create_flutter_utility_context( + ThermionFlutterPlugin *self, + EGLDisplay display, + EGLConfig config, + EGLContext sharedContext, + EGLenum api, + EGLint major, + EGLint minor) +{ + EglContextGuard guard(display); + if (!eglBindAPI(api)) + { + self->opengl_initialization_error = + "Could not bind Flutter's EGL client API"; + return EGL_NO_CONTEXT; + } + + EGLint attributes[] = { + EGL_CONTEXT_MAJOR_VERSION, major, + EGL_CONTEXT_MINOR_VERSION, minor, + EGL_NONE}; + EGLContext context = + eglCreateContext(display, config, sharedContext, attributes); + if (context == EGL_NO_CONTEXT) + { + std::cerr << "[ThermionGL] Could not create a context shared with " + "Flutter: 0x" + << std::hex << eglGetError() << std::dec << std::endl; + self->opengl_initialization_error = + "Could not create a utility context shared with Flutter"; + } + return context; +} + +// Creates only the context needed to delete a populated bootstrap texture. +// This avoids initializing Filament's platform and GBM producer when a widget +// is disposed between the raster handshake and engine initialization. +static bool initialize_bootstrap_cleanup_context( + ThermionFlutterPlugin *self) +{ + EGLDisplay display = thermion_flutter_render_display; + EGLContext flutterContext = thermion_flutter_render_context; + EGLenum api = thermion_flutter_render_api; + EGLint major = thermion_flutter_render_gl_major; + EGLint minor = thermion_flutter_render_gl_minor; + EGLint configId = 0; + if (display == EGL_NO_DISPLAY || flutterContext == EGL_NO_CONTEXT || + (api != EGL_OPENGL_API && api != EGL_OPENGL_ES_API) || + !eglQueryContext(display, flutterContext, EGL_CONFIG_ID, &configId)) + { + return false; + } + + EGLConfig config = nullptr; + EGLint configCount = 0; + EGLint configAttributes[] = {EGL_CONFIG_ID, configId, EGL_NONE}; + if (!eglChooseConfig( + display, configAttributes, &config, 1, &configCount) || + configCount == 0 || config == nullptr) + { + return false; + } + + EGLContext utilityContext = create_flutter_utility_context( + self, display, config, flutterContext, api, major, minor); + if (utilityContext == EGL_NO_CONTEXT) + { + return false; + } + + self->flutter_egl_api = api; + self->flutter_utility_egl_context = utilityContext; + self->egl_display = display; + self->backend_type = BACKEND_OPENGL; + return true; +} + +static bool initialize_opengl_dmabuf( + ThermionFlutterPlugin *self, + EGLDisplay display, + EGLConfig config, + EGLContext flutterContext, + EGLenum api, + EGLint major, + EGLint minor) +{ + auto context = new thermion::opengl::linux_platform::LinuxOpenGLContext( + reinterpret_cast(display)); + if (!context->IsValid()) + { + self->opengl_initialization_error = context->GetLastError(); + delete context; + return false; + } + + EGLContext utilityContext = create_flutter_utility_context( + self, display, config, flutterContext, api, major, minor); + if (utilityContext == EGL_NO_CONTEXT) + { + delete context; + return false; + } + + self->opengl_context = context; + self->flutter_egl_api = api; + self->flutter_utility_egl_context = utilityContext; + self->egl_display = display; + self->backend_type = BACKEND_OPENGL; + return true; +} + static bool ensure_opengl_context(ThermionFlutterPlugin *self) { - if (self->use_direct_opengl || self->opengl_context) + if (self->opengl_context) { return true; // already initialized } @@ -255,154 +335,12 @@ static bool ensure_opengl_context(ThermionFlutterPlugin *self) return false; } - if (clientType != EGL_OPENGL_API && - clientType != EGL_OPENGL_ES_API) - { - std::cerr << "[ThermionGL] Unsupported Flutter EGL client API: 0x" - << std::hex << clientType << std::dec << std::endl; - self->opengl_initialization_error = - "Flutter uses an unsupported EGL client API"; - return false; - } - - EGLint ctxAttribs[] = { - EGL_CONTEXT_MAJOR_VERSION, glMajor > 0 ? glMajor : 3, - EGL_CONTEXT_MINOR_VERSION, glMinor > 0 ? glMinor : 0, - EGL_NONE - }; - EGLenum api = static_cast(clientType); - auto createFlutterUtilityContext = [&]() -> EGLContext - { - if (!eglBindAPI(api)) - { - std::cerr << "[ThermionGL] Could not bind Flutter's EGL API: 0x" - << std::hex << eglGetError() << std::dec << std::endl; - self->opengl_initialization_error = - "Could not bind Flutter's EGL client API"; - return EGL_NO_CONTEXT; - } - EGLContext context = - eglCreateContext(flutterDpy, flutterConfig, flutterCtx, ctxAttribs); - if (context == EGL_NO_CONTEXT) - { - std::cerr << "[ThermionGL] Could not create a context shared with " - "Flutter: 0x" - << std::hex << eglGetError() << std::dec << std::endl; - self->opengl_initialization_error = - "Could not create a utility context shared with Flutter"; - } - return context; - }; - - if (api == EGL_OPENGL_ES_API) - { - // Filament's Linux backend is compiled for desktop OpenGL. EGL object - // sharing cannot cross the GLES / desktop-GL API boundary. This is the - // case handled by PR #136's DMA-BUF bridge. Keep both APIs on Flutter's - // captured EGLDisplay; a second GBM EGLDisplay can corrupt NVIDIA's - // concurrently rendering Flutter context. - self->opengl_context = - new thermion::opengl::linux_platform::LinuxOpenGLContext( - reinterpret_cast(flutterDpy)); - if (!self->opengl_context->IsValid()) - { - self->opengl_initialization_error = - self->opengl_context->GetLastError(); - delete self->opengl_context; - self->opengl_context = nullptr; - self->backend_type = 0; - return false; - } - EGLContext flutterUtilityCtx = createFlutterUtilityContext(); - if (flutterUtilityCtx == EGL_NO_CONTEXT) - { - delete self->opengl_context; - self->opengl_context = nullptr; - self->backend_type = 0; - return false; - } - self->flutter_egl_context = flutterCtx; - self->flutter_egl_api = api; - self->flutter_utility_egl_context = flutterUtilityCtx; - self->egl_display = flutterDpy; - self->egl_config = flutterConfig; - self->backend_type = BACKEND_OPENGL; - std::cerr - << "[ThermionGL] Flutter uses GLES; selected same-display " - "GBM/DMA-BUF OpenGL fallback" - << std::endl; - return true; - } - - if (glMajor < 4 || (glMajor == 4 && glMinor < 1)) - { - std::cerr << "[ThermionGL] Flutter desktop OpenGL " << glMajor << "." - << glMinor << " is below Filament's 4.1 requirement; " - "selected same-display GBM/DMA-BUF fallback" - << std::endl; - self->opengl_context = - new thermion::opengl::linux_platform::LinuxOpenGLContext( - reinterpret_cast(flutterDpy)); - if (!self->opengl_context->IsValid()) - { - self->opengl_initialization_error = - self->opengl_context->GetLastError(); - delete self->opengl_context; - self->opengl_context = nullptr; - self->backend_type = 0; - return false; - } - EGLContext flutterUtilityCtx = createFlutterUtilityContext(); - if (flutterUtilityCtx == EGL_NO_CONTEXT) - { - delete self->opengl_context; - self->opengl_context = nullptr; - self->backend_type = 0; - return false; - } - self->flutter_egl_context = flutterCtx; - self->flutter_egl_api = api; - self->flutter_utility_egl_context = flutterUtilityCtx; - self->egl_display = flutterDpy; - self->egl_config = flutterConfig; - self->backend_type = BACKEND_OPENGL; - return true; - } - - EGLContext flutterUtilityCtx = createFlutterUtilityContext(); - if (flutterUtilityCtx == EGL_NO_CONTEXT) - { - return false; - } - self->flutter_egl_context = flutterCtx; - self->flutter_egl_api = api; - self->utility_egl_context = flutterUtilityCtx; - self->egl_display = flutterDpy; - self->egl_config = flutterConfig; - self->use_direct_opengl = TRUE; - self->backend_type = BACKEND_OPENGL; - self->thermion_platform = ThermionPlatformEGLHeadless_Create(flutterDpy); - if (!self->thermion_platform) - { - eglDestroyContext(flutterDpy, flutterUtilityCtx); - self->flutter_egl_context = EGL_NO_CONTEXT; - self->flutter_egl_api = EGL_NONE; - self->utility_egl_context = EGL_NO_CONTEXT; - self->egl_display = EGL_NO_DISPLAY; - self->egl_config = nullptr; - self->use_direct_opengl = FALSE; - self->backend_type = 0; - std::cerr << "[ThermionGL] Could not create the Filament EGL platform" - << std::endl; - self->opengl_initialization_error = - "Could not create Filament's EGL platform"; - return false; - } - - std::cerr << "[ThermionGL] Created desktop OpenGL utility context=" - << (void *)flutterUtilityCtx << " (shared with Flutter)" - << std::endl; - return true; + // Filament uses desktop OpenGL while Flutter may expose either desktop GL + // or GLES. Keep one transport for both cases: render into a GBM buffer on + // Flutter's EGLDisplay and import it into Flutter through DMA-BUF. + return initialize_opengl_dmabuf( + self, flutterDpy, flutterConfig, flutterCtx, + static_cast(clientType), glMajor, glMinor); } static FlMethodResponse *handle_get_driver_platform(ThermionFlutterPlugin *self, FlMethodCall *method_call) @@ -429,14 +367,7 @@ static FlMethodResponse *handle_get_driver_platform(ThermionFlutterPlugin *self, "Filament OpenGL initialization", nullptr)); } - if (self->use_direct_opengl) - { - platform = reinterpret_cast(self->thermion_platform); - } - else - { - platform = reinterpret_cast(self->opengl_context->GetPlatform()); - } + platform = reinterpret_cast(self->opengl_context->GetPlatform()); } else { @@ -472,15 +403,7 @@ static FlMethodResponse *handle_get_shared_context(ThermionFlutterPlugin *self, "Filament OpenGL initialization", nullptr)); } - if (self->use_direct_opengl) - { - // Filament creates its own driver context in Flutter's object group. - sharedCtx = reinterpret_cast(self->flutter_egl_context); - } - else - { - sharedCtx = reinterpret_cast(self->opengl_context->GetSharedContext()); - } + sharedCtx = reinterpret_cast(self->opengl_context->GetSharedContext()); } else { @@ -545,120 +468,6 @@ static FlMethodResponse *handle_create_texture_vulkan(ThermionFlutterPlugin *sel return FL_METHOD_RESPONSE(fl_method_success_response_new(result)); } -static FlMethodResponse *handle_create_texture_opengl_direct(ThermionFlutterPlugin *self, int width, int height) -{ - // EGLImage bridge: create the source texture on the desktop-GL utility - // context shared by Filament, then let populate() bind the image to a - // Flutter-owned texture name. - // - Filament imports the GL texture directly (same share group) - // - Flutter binds the EGLImage in populate() - - EGLDisplay display = self->egl_display; - GLuint glTexId = 0; - EGLImage eglImage = EGL_NO_IMAGE_KHR; - - { - EglContextGuard guard(display); - - // Make the desktop-GL utility context current. - if (!eglBindAPI(EGL_OPENGL_API)) - { - EGLint error = eglGetError(); - std::cerr << "[ThermionGL] Failed to bind desktop OpenGL: 0x" - << std::hex << error << std::dec << std::endl; - return FL_METHOD_RESPONSE(fl_method_error_response_new( - "EGL_BIND_ERROR", "Failed to bind desktop OpenGL", nullptr)); - } - if (!eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, - self->utility_egl_context)) - { - EGLint error = eglGetError(); - std::cerr << "[ThermionGL] Failed to make plugin texture context " - "current: 0x" - << std::hex << error << std::dec - << " context=" << (void*)self->utility_egl_context - << std::endl; - g_autofree gchar *message = g_strdup_printf( - "Failed to make plugin texture context current (EGL 0x%x)", error); - return FL_METHOD_RESPONSE(fl_method_error_response_new( - "EGL_MAKE_CURRENT_ERROR", message, nullptr)); - } - - // Create GL texture on utility context - glGenTextures(1, &glTexId); - glBindTexture(GL_TEXTURE_2D, glTexId); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, - GL_RGBA, GL_UNSIGNED_BYTE, nullptr); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glBindTexture(GL_TEXTURE_2D, 0); - - // Create an EGLImage from the GL texture. - // Must be called while the owning context is current - EGLint imageAttribs[] = { EGL_GL_TEXTURE_LEVEL_KHR, 0, EGL_NONE }; - eglImage = eglCreateImageKHR( - display, self->utility_egl_context, - EGL_GL_TEXTURE_2D_KHR, - (EGLClientBuffer)(uintptr_t)glTexId, - imageAttribs); - } // guard restores previous context - - if (eglImage == EGL_NO_IMAGE_KHR) - { - EGLint err = eglGetError(); - std::cerr << "[ThermionGL] eglCreateImageKHR failed: 0x" - << std::hex << err << std::dec << std::endl; - { - EglContextGuard guard(display); - eglBindAPI(EGL_OPENGL_API); - eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, self->utility_egl_context); - glDeleteTextures(1, &glTexId); - } - return FL_METHOD_RESPONSE(fl_method_error_response_new( - "EGL_ERROR", "Failed to create EGLImage from GL texture", nullptr)); - } - - // Register with Flutter using the EGLImage bridge path — populate() will - // import the EGLImage into a new texture on Flutter's render context - ThermionTextureGL *textureGL = thermion_texture_gl_create_shared( - static_cast(width), static_cast(height), - glTexId, eglImage, - static_cast(glTexId), // surface_id = GL texture ID (for Filament import) - self->texture_registrar); - - FlTexture *flTexture = FL_TEXTURE(textureGL); - if (!fl_texture_registrar_register_texture(self->texture_registrar, flTexture)) - { - destroy_egl_image(display, eglImage); - { - EglContextGuard guard(display); - eglBindAPI(EGL_OPENGL_API); - eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, self->utility_egl_context); - glDeleteTextures(1, &glTexId); - } - return FL_METHOD_RESPONSE(fl_method_error_response_new( - "REGISTER_FAILED", "Failed to register texture with Flutter", nullptr)); - } - - self->textures->push_back(textureGL); - int64_t flutterTextureId = fl_texture_get_id(flTexture); - - std::cerr << "[ThermionGL] EGLImage bridge: GL=" << glTexId - << " EGLImage=" << (void*)eglImage - << " flutterId=" << flutterTextureId - << " (" << width << "x" << height << ")" << std::endl; - - g_autoptr(FlValue) result = fl_value_new_list(); - fl_value_append_take(result, fl_value_new_int(flutterTextureId)); - fl_value_append_take(result, fl_value_new_int(static_cast(glTexId))); // hardwareId = GL texture (visible to Filament) - fl_value_append_take(result, fl_value_new_int(0)); - - return FL_METHOD_RESPONSE(fl_method_success_response_new(result)); -} - -// DMA-BUF fallback path for OpenGL static FlMethodResponse *handle_create_texture_opengl_dmabuf(ThermionFlutterPlugin *self, int width, int height) { int64_t surfaceId = self->opengl_context->CreateRenderingSurface( @@ -713,10 +522,6 @@ static FlMethodResponse *handle_create_texture_opengl(ThermionFlutterPlugin *sel nullptr)); } - if (self->use_direct_opengl) - { - return handle_create_texture_opengl_direct(self, width, height); - } return handle_create_texture_opengl_dmabuf(self, width, height); } @@ -795,11 +600,8 @@ static FlMethodResponse *handle_destroy_texture(ThermionFlutterPlugin *self, FlM if (fl_texture_get_id(FL_TEXTURE(tex)) == flutterTextureId) { int64_t surfaceId = tex->surface_id; - gboolean useDirectSharing = tex->use_direct_sharing; - gboolean useEglImage = tex->use_egl_image; - gboolean isContextBootstrap = tex->is_context_bootstrap; + ThermionTextureKind textureKind = tex->kind; GLuint glTextureId = tex->gl_texture_id; - GLuint flutterGlTextureId = tex->flutter_gl_texture_id; EGLImage eglImage = tex->egl_image; FlMethodCall *pendingReadyCall = tex->pending_ready_call; tex->pending_ready_call = nullptr; @@ -820,60 +622,30 @@ static FlMethodResponse *handle_destroy_texture(ThermionFlutterPlugin *self, FlM gboolean initializedOnlyForBootstrapCleanup = FALSE; if (self->backend_type == BACKEND_OPENGL) { - if (useDirectSharing || useEglImage) + if (textureKind == THERMION_TEXTURE_KIND_CONTEXT_BOOTSTRAP) { - // EGLImage bridge: source names belong to Filament's utility group. - // Bootstrap names belong to Flutter's group. if (glTextureId != 0 && - self->utility_egl_context == EGL_NO_CONTEXT && - self->flutter_utility_egl_context == EGL_NO_CONTEXT && - useDirectSharing) + self->flutter_utility_egl_context == EGL_NO_CONTEXT) { // A bootstrap may be cancelled after populate() but before Dart - // requests the driver platform. Import the captured context now so - // its GL object can still be deleted from the correct share group. + // requests the driver platform. Create only a cleanup context in + // Flutter's share group; do not initialize Filament or GBM. initializedOnlyForBootstrapCleanup = - isContextBootstrap && ensure_opengl_context(self); + initialize_bootstrap_cleanup_context(self); } - EGLContext sourceContext = useDirectSharing && - self->flutter_utility_egl_context != EGL_NO_CONTEXT - ? self->flutter_utility_egl_context - : self->utility_egl_context; - if (glTextureId != 0 && sourceContext != EGL_NO_CONTEXT) + if (glTextureId != 0 && + self->flutter_utility_egl_context != EGL_NO_CONTEXT) { EglContextGuard guard(self->egl_display); - eglBindAPI(useDirectSharing - ? self->flutter_egl_api - : EGL_OPENGL_API); + eglBindAPI(self->flutter_egl_api); eglMakeCurrent(self->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, - sourceContext); + self->flutter_utility_egl_context); glDeleteTextures(1, &glTextureId); } - - if (useEglImage) - { - EGLContext flutterContext = - self->flutter_utility_egl_context != EGL_NO_CONTEXT - ? self->flutter_utility_egl_context - : self->utility_egl_context; - if (flutterGlTextureId != 0 && - flutterContext != EGL_NO_CONTEXT) - { - EglContextGuard guard(self->egl_display); - eglBindAPI(self->flutter_egl_api); - eglMakeCurrent(self->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, - flutterContext); - glDeleteTextures(1, &flutterGlTextureId); - } - if (eglImage != EGL_NO_IMAGE_KHR) - { - destroy_egl_image(self->egl_display, eglImage); - } - } } else if (self->opengl_context) { - // DMA-BUF fallback: populate() created the consumer texture and + // DMA-BUF path: populate() created the consumer texture and // EGLImage in Flutter's GLES share group. Release those before the // producer destroys the backing GBM buffer. if (glTextureId != 0 && @@ -920,7 +692,6 @@ static FlMethodResponse *handle_destroy_texture(ThermionFlutterPlugin *self, FlM // addition to the registrar's reference. All OpenGL/EGL objects have // now been deleted from their owning contexts. tex->gl_texture_id = 0; - tex->flutter_gl_texture_id = 0; tex->egl_image = EGL_NO_IMAGE_KHR; g_object_unref(tex); } @@ -950,17 +721,6 @@ static FlMethodResponse *handle_mark_texture_frame_available(ThermionFlutterPlug { if (fl_texture_get_id(FL_TEXTURE(tex)) == flutterTextureId) { - // Direct OpenGL path: ensure Filament's rendering is flushed - // before Flutter reads the texture - if (self->backend_type == BACKEND_OPENGL && self->use_direct_opengl) - { - static int markCount = 0; - markCount++; - if (markCount <= 5 || markCount % 60 == 0) { - TRACE( "[MarkFrame] #%d tex_id=%lld\n", markCount, (long long)tex->surface_id); - } - } - // Vulkan path may need blit; OpenGL path never needs blit if (self->backend_type == BACKEND_VULKAN && self->vulkan_context) { @@ -1106,14 +866,9 @@ static void thermion_flutter_plugin_init(ThermionFlutterPlugin *self) self->backend_type = 0; self->view = nullptr; self->vulkan_context = nullptr; - self->flutter_egl_context = EGL_NO_CONTEXT; self->flutter_egl_api = EGL_NONE; - self->utility_egl_context = EGL_NO_CONTEXT; self->flutter_utility_egl_context = EGL_NO_CONTEXT; self->egl_display = EGL_NO_DISPLAY; - self->egl_config = nullptr; - self->use_direct_opengl = FALSE; - self->thermion_platform = nullptr; self->opengl_context = nullptr; self->textures = new std::vector(); self->external_images = new std::unordered_map(); diff --git a/thermion_flutter/thermion_flutter/test/texture_bootstrap_test.dart b/thermion_flutter/thermion_flutter/test/texture_bootstrap_test.dart index a60a38d14..cf5a1255f 100644 --- a/thermion_flutter/thermion_flutter/test/texture_bootstrap_test.dart +++ b/thermion_flutter/thermion_flutter/test/texture_bootstrap_test.dart @@ -2,26 +2,32 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:thermion_flutter/src/platform/src/platform_texture_descriptor.dart'; import 'package:thermion_flutter/src/widgets/src/texture_bootstrap.dart'; void main() { testWidgets( 'initializes only after the bootstrap texture is ready and then destroys it', (tester) async { - final descriptor = _BootstrapDescriptor(); + const textureId = 7; final events = []; - descriptor.events = events; + final ready = Completer(); + var destroyCount = 0; await tester.pumpWidget( ThermionTextureBootstrap( createContextBootstrap: () async { events.add('create'); - return descriptor; + return textureId; }, - destroyContextBootstrap: (descriptor) async { + awaitContextBootstrap: (id) async { + expect(id, textureId); + events.add('await'); + await ready.future; + }, + destroyContextBootstrap: (id) async { + expect(id, textureId); events.add('destroy'); - await descriptor.destroy(); + destroyCount++; }, initialize: () async { events.add('initialize'); @@ -34,26 +40,33 @@ void main() { expect(find.byType(Texture), findsOneWidget); expect(events, ['create', 'await']); - descriptor.completeReady(42); + ready.complete(); await tester.pump(); await tester.pump(); - expect(descriptor.hardwareId, 42); expect(find.byType(Texture), findsNothing); expect(events, ['create', 'await', 'initialize', 'destroy']); - expect(descriptor.destroyCount, 1); + expect(destroyCount, 1); }, ); testWidgets('disposing cancels a pending bootstrap exactly once', ( tester, ) async { - final descriptor = _BootstrapDescriptor(); + final ready = Completer(); var initializeCount = 0; + var destroyCount = 0; await tester.pumpWidget( ThermionTextureBootstrap( - createContextBootstrap: () async => descriptor, + createContextBootstrap: () async => 7, + awaitContextBootstrap: (_) => ready.future, + destroyContextBootstrap: (_) async { + destroyCount++; + if (!ready.isCompleted) { + ready.completeError(StateError('destroyed')); + } + }, initialize: () async { initializeCount++; }, @@ -66,49 +79,8 @@ void main() { await tester.pumpWidget(const SizedBox.shrink()); await tester.pump(); - expect(descriptor.destroyCount, 1); + expect(destroyCount, 1); expect(initializeCount, 0); expect(tester.takeException(), isNull); }); } - -class _BootstrapDescriptor extends PlatformTextureDescriptor { - _BootstrapDescriptor() - : super(flutterTextureId: 7, hardwareId: 0, width: 1, height: 1); - - final _ready = Completer(); - int destroyCount = 0; - bool _destroyed = false; - List? events; - - @override - bool get deferred => true; - - @override - bool get destroyed => _destroyed; - - @override - Future awaitTextureReady() { - events?.add('await'); - return _ready.future; - } - - void completeReady(int textureId) { - if (!_ready.isCompleted) { - _ready.complete(textureId); - } - } - - @override - Future destroy() async { - if (_destroyed) return; - _destroyed = true; - destroyCount++; - if (!_ready.isCompleted) { - _ready.completeError(StateError('destroyed')); - } - } - - @override - void markTextureFrameAvailable() {} -} From e5735d0f495ddd3d25b14023e70d727a4607fe79 Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Sat, 22 Aug 2026 10:54:07 +0800 Subject: [PATCH 13/13] fix(flutter): bypass bootstrap off Linux OpenGL --- .../src/thermion_flutter_plugin_native.dart | 11 ++++++++--- .../lib/src/thermion_flutter_plugin.dart | 6 ++++++ .../lib/src/widgets/src/viewer_widget.dart | 19 +++++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_native.dart b/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_native.dart index 377bf1cb0..a974ba2b6 100644 --- a/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_native.dart +++ b/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_native.dart @@ -161,12 +161,17 @@ class ThermionFlutterPluginImpl extends ThermionFlutterPlugin { _lifecycle.resumeExplicitly(); } + @internal + @override + bool get requiresContextBootstrap => + Platform.isLinux && + _resolveBackend() == Backend.OPENGL && + FilamentApp.instance == null; + @internal @override Future createContextBootstrap() async { - if (!Platform.isLinux || - _resolveBackend() != Backend.OPENGL || - FilamentApp.instance != null) { + if (!requiresContextBootstrap) { return null; } final textureId = await NativePlatformTextureDescriptorRegistry.channel diff --git a/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart b/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart index 391c4a786..972cd2cab 100644 --- a/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart +++ b/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart @@ -84,6 +84,12 @@ abstract class ThermionFlutterPlugin { FilamentApp.instance?.setTargetFramerate(fps); } + /// Whether viewer initialization must wait for Flutter to composite a + /// bootstrap texture. False on every platform except Linux OpenGL before + /// the first engine is created. + @internal + bool get requiresContextBootstrap => false; + /// Allocates the throwaway external texture that must be composited by /// Flutter before the native viewer can be created. Returns null when the /// running platform has no such prerequisite and the caller may initialize diff --git a/thermion_flutter/thermion_flutter/lib/src/widgets/src/viewer_widget.dart b/thermion_flutter/thermion_flutter/lib/src/widgets/src/viewer_widget.dart index dea8f32b7..2d5040d02 100644 --- a/thermion_flutter/thermion_flutter/lib/src/widgets/src/viewer_widget.dart +++ b/thermion_flutter/thermion_flutter/lib/src/widgets/src/viewer_widget.dart @@ -89,9 +89,25 @@ class _ViewerWidgetState extends State { Future? _tearDownFuture; Future? _inputHandlerUpdate; bool _disposing = false; + late final bool _requiresContextBootstrap; late final _logger = Logger(runtimeType.toString()); + @override + void initState() { + super.initState(); + _requiresContextBootstrap = + ThermionFlutterPlugin.instance.requiresContextBootstrap; + if (!_requiresContextBootstrap) { + _initialization = _createViewer(); + unawaited( + _initialization!.catchError((Object error, StackTrace stack) { + _reportAsyncError('initialization', error, stack); + }), + ); + } + } + Future _initializeViewer() => _initialization ??= _createViewer(); Future _createViewer() async { @@ -429,6 +445,9 @@ class _ViewerWidgetState extends State { final child = viewport == null ? widget.initial : SizedBox.expand(child: viewport); + if (!_requiresContextBootstrap) { + return child; + } return ThermionTextureBootstrap( initialize: _initializeViewer, child: child,