diff --git a/thermion_dart/native/src/opengl/linux/LinuxOpenGLContext.cpp b/thermion_dart/native/src/opengl/linux/LinuxOpenGLContext.cpp index 1310aa786..86963d7c3 100644 --- a/thermion_dart/native/src/opengl/linux/LinuxOpenGLContext.cpp +++ b/thermion_dart/native/src/opengl/linux/LinuxOpenGLContext.cpp @@ -64,6 +64,35 @@ class ScopedEglThreadState { EGLenum _api; }; +static std::string ResolveDrmDevicePath(EGLDisplay display) { + if (display != EGL_NO_DISPLAY) { + auto queryDisplayAttrib = + reinterpret_cast( + eglGetProcAddress("eglQueryDisplayAttribEXT")); + auto queryDeviceString = + reinterpret_cast( + eglGetProcAddress("eglQueryDeviceStringEXT")); + EGLAttrib deviceAttribute = 0; + if (queryDisplayAttrib && queryDeviceString && + queryDisplayAttrib( + display, EGL_DEVICE_EXT, &deviceAttribute)) { + auto device = reinterpret_cast(deviceAttribute); + const char* path = queryDeviceString( + device, EGL_DRM_RENDER_NODE_FILE_EXT); + if (!path || path[0] == '\0') { + path = queryDeviceString(device, EGL_DRM_DEVICE_FILE_EXT); + } + if (path && path[0] != '\0') { + return path; + } + } + } + + // Retain the historic default for non-Flutter/headless EGL stacks that do + // not expose EGL_EXT_device_query. + return "/dev/dri/renderD128"; +} + class LinuxOpenGLContext::Impl { public: ~Impl() { @@ -117,14 +146,19 @@ class LinuxOpenGLContext::Impl { explicit Impl(void* borrowedDisplay) { std::cerr << "[ThermionGL:Context] Initializing EGL/GBM..." << std::endl; - // Step 1: Open DRM render node - _drmFd = open("/dev/dri/renderD128", O_RDWR); + // Step 1: Open the render node backing Flutter's EGLDisplay. Hardcoding + // renderD128 can select a different GPU on multi-GPU systems, making + // the exported DMA-BUF impossible for Flutter to import. + const std::string drmDevicePath = ResolveDrmDevicePath( + static_cast(borrowedDisplay)); + _drmFd = open(drmDevicePath.c_str(), O_RDWR); if (_drmFd < 0) { - _lastError = "Failed to open /dev/dri/renderD128"; - LOG_ERROR("Failed to open /dev/dri/renderD128"); + _lastError = "Failed to open " + drmDevicePath; + std::cerr << "[ThermionGL:Context] " << _lastError << std::endl; return; } - std::cerr << "[ThermionGL:Context] DRM fd=" << _drmFd << std::endl; + std::cerr << "[ThermionGL:Context] DRM device=" << drmDevicePath + << " fd=" << _drmFd << std::endl; // Step 2: Create GBM device _gbmDevice = gbm_create_device(_drmFd); diff --git a/thermion_flutter/thermion_flutter/linux/egl_texture.cc b/thermion_flutter/thermion_flutter/linux/egl_texture.cc index 9955cdbe8..e1127c5ed 100644 --- a/thermion_flutter/thermion_flutter/linux/egl_texture.cc +++ b/thermion_flutter/thermion_flutter/linux/egl_texture.cc @@ -6,6 +6,7 @@ #include #include #include +#include #ifndef GL_TEXTURE_EXTERNAL_OES #define GL_TEXTURE_EXTERNAL_OES 0x8D65 @@ -26,17 +27,33 @@ static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC s_glEGLImageTargetTexture2DOES = null struct DeferredReadyResponse { FlMethodCall* method_call; int64_t texture_id; + ThermionTextureGL* texture; }; 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); + if (response->texture->destroyed) { + fl_method_call_respond( + response->method_call, + FL_METHOD_RESPONSE(fl_method_error_response_new( + "DESTROYED", + "Texture destroyed before readiness response", nullptr)), + nullptr); + } else { + 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); + } + return G_SOURCE_REMOVE; +} + +static void destroy_deferred_ready_response(gpointer user_data) { + auto* response = static_cast(user_data); g_object_unref(response->method_call); + g_object_unref(response->texture); delete response; - return G_SOURCE_REMOVE; } static void ensure_egl_procs() { @@ -47,6 +64,21 @@ static void ensure_egl_procs() { } } +// Serializes populate() (Flutter's raster thread) against the plugin's +// release_texture() (platform thread). Every populate() return path unlocks. +struct TextureLockGuard { + explicit TextureLockGuard(GMutex& mutex) : mutex(mutex) { + g_mutex_lock(&mutex); + } + ~TextureLockGuard() { + g_mutex_unlock(&mutex); + } + TextureLockGuard(const TextureLockGuard&) = delete; + TextureLockGuard& operator=(const TextureLockGuard&) = delete; + + GMutex& mutex; +}; + G_DEFINE_TYPE(ThermionTextureGL, thermion_texture_gl, fl_texture_gl_get_type()) @@ -84,12 +116,49 @@ thermion_texture_populate(FlTextureGL *texture, ThermionTextureGL *self = THERMION_TEXTURE_GL(texture); + // Serialize against release_texture() on the platform thread. Either it + // completes first (and the destroyed check below makes populate a no-op) + // or this populate finishes first and the release observes the consumer + // resources it must clean up. Without this lock, a destroy issued while + // populate is importing the DMA-BUF can close the producer's fd mid-import. + TextureLockGuard lockGuard(self->lock); + + if (self->destroyed) { + g_set_error(error, g_quark_from_static_string("thermion"), 4, + "Texture destroyed before populate"); + return FALSE; + } + + // This callback is the only place Flutter guarantees its raster context + // is current. Capture it for owner-aware cleanup for every transport, + // including Vulkan-produced DMA-BUF textures. + EGLContext flutterContext = eglGetCurrentContext(); + EGLDisplay flutterDisplay = eglGetCurrentDisplay(); + if (flutterContext != EGL_NO_CONTEXT && + flutterDisplay != EGL_NO_DISPLAY && + (thermion_flutter_render_context != flutterContext || + thermion_flutter_render_display != flutterDisplay)) { + thermion_flutter_render_context = flutterContext; + thermion_flutter_render_display = flutterDisplay; + 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); + } + } + } + // 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(); if (flutterContext == EGL_NO_CONTEXT || flutterDisplay == EGL_NO_DISPLAY) { g_set_error(error, g_quark_from_static_string("thermion"), 1, @@ -132,21 +201,6 @@ thermion_texture_populate(FlTextureGL *texture, // Capture Flutter's render context for Filament initialization. // This is the ONLY place where Flutter's render context is current. - thermion_flutter_render_context = flutterContext; - thermion_flutter_render_display = flutterDisplay; - 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, @@ -158,16 +212,21 @@ thermion_texture_populate(FlTextureGL *texture, // 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) { + for (guint i = 0; i < self->pending_ready_calls->len; i++) { + auto* methodCall = static_cast( + g_ptr_array_index(self->pending_ready_calls, i)); auto* response = new DeferredReadyResponse{ - self->pending_ready_call, + methodCall, static_cast(self->gl_texture_id), + THERMION_TEXTURE_GL(g_object_ref(self)), }; - self->pending_ready_call = nullptr; g_idle_add_full( G_PRIORITY_DEFAULT_IDLE, respond_texture_ready, response, - nullptr); + destroy_deferred_ready_response); } + // Ownership of every FlMethodCall reference moved to its idle + // response. + g_ptr_array_set_size(self->pending_ready_calls, 0); } *target = GL_TEXTURE_2D; @@ -259,42 +318,56 @@ thermion_texture_populate(FlTextureGL *texture, static void thermion_texture_gl_dispose(GObject* object) { ThermionTextureGL *self = THERMION_TEXTURE_GL(object); - // Clean up any pending deferred method call - if (self->pending_ready_call) { - fl_method_call_respond(self->pending_ready_call, - FL_METHOD_RESPONSE(fl_method_error_response_new( - "DESTROYED", "Texture destroyed before populate", nullptr)), nullptr); - g_object_unref(self->pending_ready_call); - self->pending_ready_call = nullptr; - } - - 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; + // Native GL/EGL resources are released explicitly by the plugin while + // their owning contexts are available. GObject disposal only resolves + // outstanding method calls and releases their references. Disposal runs + // on the platform thread for textures that failed to register (never + // visible to the raster thread), so the mutex is not needed here; the + // plugin retains successfully-registered shells instead of unref'ing + // them (see release_texture) precisely so a late populate cannot touch + // a finalized object. + if (self->pending_ready_calls) { + for (guint i = 0; i < self->pending_ready_calls->len; i++) { + auto* methodCall = static_cast( + g_ptr_array_index(self->pending_ready_calls, i)); + fl_method_call_respond( + methodCall, + FL_METHOD_RESPONSE(fl_method_error_response_new( + "DESTROYED", "Texture destroyed before populate", + nullptr)), + nullptr); + g_object_unref(methodCall); + } + g_ptr_array_set_size(self->pending_ready_calls, 0); + g_ptr_array_unref(self->pending_ready_calls); + self->pending_ready_calls = nullptr; } - if (self->gl_texture_id != 0) { - glDeleteTextures(1, &self->gl_texture_id); - self->gl_texture_id = 0; + if (self->owns_dmabuf_fd && self->dmabuf_fd >= 0) { + close(self->dmabuf_fd); } + self->dmabuf_fd = -1; + self->owns_dmabuf_fd = FALSE; - if (self->egl_image != EGL_NO_IMAGE_KHR && s_eglDestroyImageKHR) { - EGLDisplay display = eglGetCurrentDisplay(); - if (display != EGL_NO_DISPLAY) { - s_eglDestroyImageKHR(display, self->egl_image); - } - self->egl_image = EGL_NO_IMAGE_KHR; + if (self->gl_texture_id != 0 || + self->egl_image != EGL_NO_IMAGE_KHR) { + std::cerr + << "[ThermionEGL] Texture disposed before explicit native cleanup" + << std::endl; } G_OBJECT_CLASS(thermion_texture_gl_parent_class)->dispose(object); } +static void thermion_texture_gl_finalize(GObject* object) { + ThermionTextureGL *self = THERMION_TEXTURE_GL(object); + g_mutex_clear(&self->lock); + G_OBJECT_CLASS(thermion_texture_gl_parent_class)->finalize(object); +} + void thermion_texture_gl_class_init(ThermionTextureGLClass* klass) { G_OBJECT_CLASS(klass)->dispose = thermion_texture_gl_dispose; + G_OBJECT_CLASS(klass)->finalize = thermion_texture_gl_finalize; FL_TEXTURE_GL_CLASS(klass)->populate = thermion_texture_populate; } @@ -304,6 +377,7 @@ void thermion_texture_gl_init(ThermionTextureGL* self) { self->height = 0; self->registrar = nullptr; self->dmabuf_fd = -1; + self->owns_dmabuf_fd = FALSE; self->stride = 0; self->offset = 0; self->drm_format = 0; @@ -312,7 +386,9 @@ void thermion_texture_gl_init(ThermionTextureGL* self) { self->initialized = FALSE; self->surface_id = -1; self->kind = THERMION_TEXTURE_KIND_DMA_BUF; - self->pending_ready_call = nullptr; + self->pending_ready_calls = g_ptr_array_new(); + self->destroyed = FALSE; + g_mutex_init(&self->lock); } ThermionTextureGL* thermion_texture_gl_create( @@ -324,7 +400,24 @@ ThermionTextureGL* thermion_texture_gl_create( textureGL->width = info.width; textureGL->height = info.height; textureGL->registrar = registrar; - textureGL->dmabuf_fd = info.dmabuf_fd; + // Own a separate descriptor for the consumer-side EGLImage import so the + // producer closing its fd at teardown can never invalidate a concurrent + // or subsequent import. The DMA-BUF memory itself stays alive until every + // descriptor referencing it is closed. + const int consumerFd = dup(info.dmabuf_fd); + if (consumerFd >= 0) { + textureGL->dmabuf_fd = consumerFd; + textureGL->owns_dmabuf_fd = TRUE; + } else { + // Extremely unlikely (fd exhaustion); borrow the producer's + // descriptor instead. It is never closed on the consumer side — the + // producer's surface teardown owns it — and the populate/destroy + // mutex already prevents an import from racing that teardown. + std::cerr << "[ThermionEGL] dup() of dmabuf fd failed; borrowing producer fd" + << std::endl; + textureGL->dmabuf_fd = info.dmabuf_fd; + textureGL->owns_dmabuf_fd = FALSE; + } textureGL->stride = info.stride; textureGL->offset = info.offset; textureGL->drm_format = info.drm_format; diff --git a/thermion_flutter/thermion_flutter/linux/egl_texture.h b/thermion_flutter/thermion_flutter/linux/egl_texture.h index 1ff596412..e03664549 100644 --- a/thermion_flutter/thermion_flutter/linux/egl_texture.h +++ b/thermion_flutter/thermion_flutter/linux/egl_texture.h @@ -36,8 +36,13 @@ struct _ThermionTextureGL { uint32_t width; uint32_t height; FlTextureRegistrar* registrar; - // dmabuf info for lazy EGL import (DMA-BUF path only) + // dmabuf info for lazy EGL import (DMA-BUF path only). dmabuf_fd is a + // consumer-owned dup() of the producer's descriptor, so producer teardown + // cannot invalidate a concurrent import. int dmabuf_fd; + // Whether dispose() must close dmabuf_fd (FALSE only when dup() failed at + // creation and the producer's descriptor is merely borrowed). + gboolean owns_dmabuf_fd; uint32_t stride; uint32_t offset; uint32_t drm_format; @@ -46,8 +51,16 @@ struct _ThermionTextureGL { gboolean initialized; int64_t surface_id; // for Blit() and destruction ThermionTextureKind kind; - // Deferred "awaitTextureReady" response (stored until populate creates the GL texture) - FlMethodCall* pending_ready_call; + // Deferred awaitTextureReady responses stored until populate creates the + // bootstrap GL texture. Each entry owns a FlMethodCall reference. + GPtrArray* pending_ready_calls; + // Set before native resources are released. Queued idle responses use this + // to avoid publishing a texture ID after cancellation. + gboolean destroyed; + // Serializes populate() (Flutter's raster thread) against the plugin's + // release_texture() (platform thread). Guards pending_ready_calls, the + // lazy import below, and the destroyed flag. + GMutex lock; }; typedef struct _ThermionTextureGL ThermionTextureGL; diff --git a/thermion_flutter/thermion_flutter/linux/thermion_flutter_plugin.cc b/thermion_flutter/thermion_flutter/linux/thermion_flutter_plugin.cc index cf22c5eb0..20effb46e 100644 --- a/thermion_flutter/thermion_flutter/linux/thermion_flutter_plugin.cc +++ b/thermion_flutter/thermion_flutter/linux/thermion_flutter_plugin.cc @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -118,10 +119,18 @@ struct _ThermionFlutterPlugin // Shared std::vector *textures; + // Shells of destroyed textures, retained until plugin teardown. See + // release_texture() for why they cannot be unreffed eagerly. + std::vector *retired_textures; }; G_DEFINE_TYPE(ThermionFlutterPlugin, thermion_flutter_plugin, g_object_get_type()) +// Global plugin instance used by Dart's direct post-render texture notifier. +// Clear it before disposal starts so callbacks cannot acquire an object whose +// native resources are being torn down. +static ThermionFlutterPlugin* g_plugin_instance = nullptr; + static void destroy_all_contexts(ThermionFlutterPlugin *self) { if (self->vulkan_context) @@ -143,11 +152,15 @@ static void destroy_all_contexts(ThermionFlutterPlugin *self) } self->flutter_egl_api = EGL_NONE; self->egl_display = EGL_NO_DISPLAY; - 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; + // The thermion_flutter_render_* globals are deliberately NOT reset here. + // They describe Flutter's raster context/display, which outlives every + // plugin-owned context, and destroying our contexts (bootstrap + // cancellation, destroyContext, re-init) must not invalidate them: + // resetting them mid-session leaks bootstrap GL names and breaks a + // sibling viewer's pending getDriverPlatform (CONTEXT_NOT_READY). They + // are cleared in thermion_flutter_plugin_dispose, when the engine itself + // is going away, and re-captured by populate() whenever Flutter's context + // differs from the captured one. self->backend_type = 0; } @@ -197,9 +210,14 @@ static EGLContext create_flutter_utility_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( +static bool ensure_flutter_cleanup_context( ThermionFlutterPlugin *self) { + if (self->flutter_utility_egl_context != EGL_NO_CONTEXT) + { + return true; + } + EGLDisplay display = thermion_flutter_render_display; EGLContext flutterContext = thermion_flutter_render_context; EGLenum api = thermion_flutter_render_api; @@ -233,7 +251,6 @@ static bool initialize_bootstrap_cleanup_context( self->flutter_egl_api = api; self->flutter_utility_egl_context = utilityContext; self->egl_display = display; - self->backend_type = BACKEND_OPENGL; return true; } @@ -451,6 +468,8 @@ static FlMethodResponse *handle_create_texture_vulkan(ThermionFlutterPlugin *sel filament::backend::Platform::ExternalImageHandle reclaim(extImg); self->external_images->erase(surfaceId); self->vulkan_context->DestroyRenderingSurface(surfaceId); + textureGL->destroyed = TRUE; + g_object_unref(textureGL); return FL_METHOD_RESPONSE(fl_method_error_response_new( "REGISTER_FAILED", "Failed to register texture with Flutter", nullptr)); } @@ -487,6 +506,8 @@ static FlMethodResponse *handle_create_texture_opengl_dmabuf(ThermionFlutterPlug if (!fl_texture_registrar_register_texture(self->texture_registrar, flTexture)) { self->opengl_context->DestroyRenderingSurface(surfaceId); + textureGL->destroyed = TRUE; + g_object_unref(textureGL); return FL_METHOD_RESPONSE(fl_method_error_response_new( "REGISTER_FAILED", "Failed to register texture with Flutter", nullptr)); } @@ -589,113 +610,172 @@ static FlMethodResponse *handle_create_texture(ThermionFlutterPlugin *self, FlMe } } -static FlMethodResponse *handle_destroy_texture(ThermionFlutterPlugin *self, FlMethodCall *method_call) +// Caller must hold texture->lock. Responses complete Dart futures on the +// platform event loop, not synchronously, so responding under the lock is +// safe from re-entrancy. +static void reject_pending_ready_calls(ThermionTextureGL *texture) { - FlValue *args = fl_method_call_get_args(method_call); - int64_t flutterTextureId = fl_value_get_int(args); + if (!texture->pending_ready_calls) + { + return; + } + for (guint i = 0; i < texture->pending_ready_calls->len; i++) + { + auto *methodCall = static_cast( + g_ptr_array_index(texture->pending_ready_calls, i)); + fl_method_call_respond( + methodCall, + FL_METHOD_RESPONSE(fl_method_error_response_new( + "DESTROYED", + "Texture destroyed before Flutter populated it", nullptr)), + nullptr); + g_object_unref(methodCall); + } + g_ptr_array_set_size(texture->pending_ready_calls, 0); +} - for (auto it = self->textures->begin(); it != self->textures->end(); ++it) +static bool delete_flutter_gl_texture( + ThermionFlutterPlugin *self, GLuint textureId, const char *description) +{ + if (textureId == 0) { - ThermionTextureGL *tex = *it; - if (fl_texture_get_id(FL_TEXTURE(tex)) == flutterTextureId) + return true; + } + if (!ensure_flutter_cleanup_context(self)) + { + std::cerr << "[ThermionGL] Cannot delete " << description << " " + << textureId << ": owner context unavailable" << std::endl; + return false; + } + + EglContextGuard guard(self->egl_display); + if (!eglBindAPI(self->flutter_egl_api) || + !eglMakeCurrent( + self->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, + self->flutter_utility_egl_context)) + { + std::cerr << "[ThermionGL] Cannot make owner context current while deleting " + << description << " " << textureId << ": 0x" + << std::hex << eglGetError() << std::dec << std::endl; + return false; + } + glDeleteTextures(1, &textureId); + return true; +} + +static void release_texture( + ThermionFlutterPlugin *self, + ThermionTextureGL *texture, + gboolean unregisterTexture) +{ + // Serialize with populate() on the raster thread: either populate + // completes first and the snapshot below includes the consumer resources + // to clean up, or release wins and populate observes `destroyed` and + // bails without touching the producer's DMA-BUF. + g_mutex_lock(&texture->lock); + const int64_t surfaceId = texture->surface_id; + const GLuint glTextureId = texture->gl_texture_id; + const EGLImage eglImage = texture->egl_image; + const ThermionTextureKind kind = texture->kind; + + texture->destroyed = TRUE; + reject_pending_ready_calls(texture); + g_mutex_unlock(&texture->lock); + + if (unregisterTexture && self->texture_registrar) + { + fl_texture_registrar_unregister_texture( + self->texture_registrar, FL_TEXTURE(texture)); + } + + // Bootstrap and DMA-BUF consumer texture names both belong to Flutter's + // raster share group, regardless of whether Filament uses OpenGL or Vulkan. + delete_flutter_gl_texture( + self, glTextureId, + kind == THERMION_TEXTURE_KIND_CONTEXT_BOOTSTRAP + ? "Flutter bootstrap texture" + : "Flutter DMA-BUF texture"); + + if (kind == THERMION_TEXTURE_KIND_DMA_BUF) + { + destroy_egl_image(thermion_flutter_render_display, eglImage); + + // Dispatch producer teardown by ownership rather than by which producer + // context happens to exist: surface ids are per-context counters, so a + // misroute can destroy an unrelated live surface in the other context. + const bool vulkanOwned = + self->external_images->find(surfaceId) != self->external_images->end(); + if (vulkanOwned) { - int64_t surfaceId = tex->surface_id; - ThermionTextureKind textureKind = tex->kind; - GLuint glTextureId = tex->gl_texture_id; - EGLImage eglImage = tex->egl_image; - FlMethodCall *pendingReadyCall = tex->pending_ready_call; - tex->pending_ready_call = nullptr; - - if (pendingReadyCall) + // Filament owns the ExternalImage after import. Drop only our raw + // pointer before destroying the producer surface. + self->external_images->erase(surfaceId); + if (self->vulkan_context) { - 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); + self->vulkan_context->DestroyRenderingSurface(surfaceId); } + } + else if (self->opengl_context) + { + self->opengl_context->DestroyRenderingSurface(surfaceId); + } + else if (self->vulkan_context) + { + self->vulkan_context->DestroyRenderingSurface(surfaceId); + } + } - fl_texture_registrar_unregister_texture(self->texture_registrar, FL_TEXTURE(tex)); + g_mutex_lock(&texture->lock); + texture->gl_texture_id = 0; + texture->egl_image = EGL_NO_IMAGE_KHR; + if (texture->owns_dmabuf_fd && texture->dmabuf_fd >= 0) + { + close(texture->dmabuf_fd); + } + texture->dmabuf_fd = -1; + texture->owns_dmabuf_fd = FALSE; + g_mutex_unlock(&texture->lock); + + // The GObject shell is retained, not unreffed: Flutter's raster thread + // reaches populate() through a raw registrar lookup that holds no + // reference and is not synchronized with unregistration, so dropping the + // last reference here could finalize the object — and its mutex — while + // an already-started populate is still running. All heavyweight native + // resources were released above; only this small shell survives until + // plugin teardown. + self->retired_textures->push_back(texture); +} - gboolean initializedOnlyForBootstrapCleanup = FALSE; - if (self->backend_type == BACKEND_OPENGL) - { - if (textureKind == THERMION_TEXTURE_KIND_CONTEXT_BOOTSTRAP) - { - if (glTextureId != 0 && - self->flutter_utility_egl_context == EGL_NO_CONTEXT) - { - // A bootstrap may be cancelled after populate() but before Dart - // requests the driver platform. Create only a cleanup context in - // Flutter's share group; do not initialize Filament or GBM. - initializedOnlyForBootstrapCleanup = - initialize_bootstrap_cleanup_context(self); - } - if (glTextureId != 0 && - self->flutter_utility_egl_context != EGL_NO_CONTEXT) - { - EglContextGuard guard(self->egl_display); - eglBindAPI(self->flutter_egl_api); - eglMakeCurrent(self->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, - self->flutter_utility_egl_context); - glDeleteTextures(1, &glTextureId); - } - } - else if (self->opengl_context) - { - // 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 && - 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); - } - } - else - { - // Vulkan path: clean up external image - auto extIt = self->external_images->find(surfaceId); - if (extIt != self->external_images->end()) - { - // Filament holds a reference to this ExternalImage and releases it when the - // texture is destroyed; ExternalImage's destructor is protected to forbid manual deletion. - // Only drop the raw pointer. - self->external_images->erase(extIt); - } - if (self->vulkan_context) - { - self->vulkan_context->DestroyRenderingSurface(surfaceId); - } - } +static void destroy_all_textures(ThermionFlutterPlugin *self) +{ + if (!self->textures) + { + return; + } + while (!self->textures->empty()) + { + ThermionTextureGL *texture = self->textures->back(); + self->textures->pop_back(); + release_texture(self, texture, TRUE); + } +} +static FlMethodResponse *handle_destroy_texture(ThermionFlutterPlugin *self, FlMethodCall *method_call) +{ + FlValue *args = fl_method_call_get_args(method_call); + int64_t flutterTextureId = fl_value_get_int(args); + + for (auto it = self->textures->begin(); it != self->textures->end(); ++it) + { + ThermionTextureGL *texture = *it; + if (fl_texture_get_id(FL_TEXTURE(texture)) == flutterTextureId) + { + const bool bootstrapOnly = + texture->kind == THERMION_TEXTURE_KIND_CONTEXT_BOOTSTRAP && + self->opengl_context == nullptr && self->vulkan_context == nullptr; 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->egl_image = EGL_NO_IMAGE_KHR; - g_object_unref(tex); - } - if (initializedOnlyForBootstrapCleanup) + release_texture(self, texture, TRUE); + if (bootstrapOnly) { destroy_all_contexts(self); } @@ -707,12 +787,23 @@ static FlMethodResponse *handle_destroy_texture(ThermionFlutterPlugin *self, FlM return FL_METHOD_RESPONSE(fl_method_success_response_new(result)); } +static void mark_texture_frame_available( + ThermionFlutterPlugin *self, ThermionTextureGL *texture) +{ + // Vulkan may render into an intermediate image that must be copied to the + // DMA-BUF exported to Flutter. OpenGL renders into the export directly. + if (self->backend_type == BACKEND_VULKAN && self->vulkan_context && + self->vulkan_context->NeedsBlit(texture->surface_id)) + { + self->vulkan_context->BlitToExport(texture->surface_id); + } + + fl_texture_registrar_mark_texture_frame_available( + self->texture_registrar, FL_TEXTURE(texture)); +} + static FlMethodResponse *handle_mark_texture_frame_available(ThermionFlutterPlugin *self, FlMethodCall *method_call) { - static auto lastMark = std::chrono::high_resolution_clock::now(); - auto now = std::chrono::high_resolution_clock::now(); - auto intervalUs = std::chrono::duration_cast(now - lastMark).count(); - lastMark = now; FlValue *args = fl_method_call_get_args(method_call); int64_t flutterTextureId = fl_value_get_int(args); @@ -721,17 +812,7 @@ static FlMethodResponse *handle_mark_texture_frame_available(ThermionFlutterPlug { if (fl_texture_get_id(FL_TEXTURE(tex)) == flutterTextureId) { - // Vulkan path may need blit; OpenGL path never needs blit - if (self->backend_type == BACKEND_VULKAN && self->vulkan_context) - { - if (self->vulkan_context->NeedsBlit(tex->surface_id)) - { - self->vulkan_context->BlitToExport(tex->surface_id); - } - } - - fl_texture_registrar_mark_texture_frame_available( - self->texture_registrar, FL_TEXTURE(tex)); + mark_texture_frame_available(self, tex); break; } } @@ -742,6 +823,7 @@ static FlMethodResponse *handle_mark_texture_frame_available(ThermionFlutterPlug static FlMethodResponse *handle_destroy_context(ThermionFlutterPlugin *self) { + destroy_all_textures(self); destroy_all_contexts(self); g_autoptr(FlValue) result = fl_value_new_null(); return FL_METHOD_RESPONSE(fl_method_success_response_new(result)); @@ -759,19 +841,28 @@ static void handle_await_texture_ready(ThermionFlutterPlugin *self, FlMethodCall { if (fl_texture_get_id(FL_TEXTURE(tex)) == flutterTextureId) { - if (tex->gl_texture_id != 0) + // The check-then-defer must be atomic against populate() draining the + // array on the raster thread. + g_mutex_lock(&tex->lock); + const GLuint glTextureId = tex->gl_texture_id; + if (glTextureId == 0) + { + g_object_ref(method_call); + g_ptr_array_add(tex->pending_ready_calls, method_call); + } + g_mutex_unlock(&tex->lock); + + if (glTextureId != 0) { // Already ready (populate already ran) g_autoptr(FlValue) result = fl_value_new_int( - static_cast(tex->gl_texture_id)); + static_cast(glTextureId)); fl_method_call_respond(method_call, FL_METHOD_RESPONSE(fl_method_success_response_new(result)), nullptr); } else { - // Defer — store method_call, respond later from populate() - g_object_ref(method_call); - tex->pending_ready_call = method_call; + // Defer — retain every caller and respond after populate(). std::cerr << "[ThermionGL] awaitTextureReady: deferred for flutterId=" << flutterTextureId << std::endl; } return; @@ -841,13 +932,38 @@ static void thermion_flutter_plugin_dispose(GObject *object) { ThermionFlutterPlugin *self = FLUTTER_FILAMENT_PLUGIN(object); + if (g_plugin_instance == self) + { + g_plugin_instance = nullptr; + } + + // Release Flutter texture objects while their producer and consumer owner + // contexts are still available. + destroy_all_textures(self); destroy_all_contexts(self); + // The engine is going away: the captured raster context/display are dead. + // destroy_all_contexts deliberately keeps these during the session (see + // the comment there). + 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; + if (self->textures) { delete self->textures; self->textures = nullptr; } + if (self->retired_textures) + { + // Shells are deleted without unreffing: plugin disposal races engine + // teardown, and a late populate() on the raster thread must never see a + // finalized texture object. The memory is reclaimed at process exit. + delete self->retired_textures; + self->retired_textures = nullptr; + } if (self->external_images) { delete self->external_images; @@ -871,6 +987,7 @@ static void thermion_flutter_plugin_init(ThermionFlutterPlugin *self) self->egl_display = EGL_NO_DISPLAY; self->opengl_context = nullptr; self->textures = new std::vector(); + self->retired_textures = new std::vector(); self->external_images = new std::unordered_map(); } @@ -881,9 +998,6 @@ static void method_call_cb(FlMethodChannel *channel, FlMethodCall *method_call, thermion_flutter_plugin_handle_method_call(plugin, method_call); } -// Global plugin instance used by Dart's direct post-render texture notifier. -static ThermionFlutterPlugin* g_plugin_instance = nullptr; - void thermion_flutter_plugin_register_with_registrar(FlPluginRegistrar *registrar) { ThermionFlutterPlugin *plugin = FLUTTER_FILAMENT_PLUGIN( @@ -914,15 +1028,14 @@ void* thermion_flutter_get_plugin_handle() { return g_plugin_instance; } -// Called from Dart after the common render future completes. Marks all -// textures as frame-available so Flutter picks up the new content on its next -// raster pass. +// Called from Dart after the common render future completes. Publish any +// Vulkan exports, then mark every texture available for Flutter's next raster +// pass. extern "C" __attribute__((visibility("default"))) void thermion_flutter_mark_textures(void* pluginPtr) { auto* self = FLUTTER_FILAMENT_PLUGIN(pluginPtr); if (!self || !self->textures || !self->texture_registrar) return; for (auto* tex : *self->textures) { - fl_texture_registrar_mark_texture_frame_available( - self->texture_registrar, FL_TEXTURE(tex)); + mark_texture_frame_available(self, tex); } }