[Linux] Synchronize multi-window renderer lifetime across GTK and raster threads When destroying an active secondary window on Linux: 1. FlView did not retain a strong reference to self->renderer, allowing gtk_container_dispose() on self->event_box to unreference and free self->renderer during dispose() while the Flutter raster thread may still call fl_view_present_layers(self->renderer). Retain a strong reference to self->renderer until fl_view_finalize(). 2. FlViewRendererOpenGL and FlViewRendererSubsurface destroyed frame_mutex and task_runner in dispose() while leaving compositor and frame for finalize(). Move task_runner and frame_mutex cleanup to finalize() so present_layers() can safely access them after dispose(). 3. Synchronize compositor, frame, and render_context teardown under frame_mutex in cleanup_opengl_resources() during unrealize() and dispose() on the GTK main thread. Only clear the GTK thread's current GdkGLContext if this window's render_context is the one currently bound.
diff --git a/engine/src/flutter/shell/platform/linux/fl_view.cc b/engine/src/flutter/shell/platform/linux/fl_view.cc index 6a51c26..e565a24 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view.cc
@@ -156,11 +156,17 @@ // Called when the mouse cursor changes. static void cursor_changed_cb(FlView* self) { + if (self->engine == nullptr) { + return; + } FlMouseCursorHandler* handler = fl_engine_get_mouse_cursor_handler(self->engine); const gchar* cursor_name = fl_mouse_cursor_handler_get_cursor_name(handler); GdkWindow* window = gtk_widget_get_window(gtk_widget_get_toplevel(GTK_WIDGET(self))); + if (window == nullptr) { + return; + } g_autoptr(GdkCursor) cursor = gdk_cursor_new_from_name(gdk_window_get_display(window), cursor_name); gdk_window_set_cursor(window, cursor); @@ -168,6 +174,9 @@ // Set the mouse cursor. static void setup_cursor(FlView* self) { + if (self->engine == nullptr) { + return; + } FlMouseCursorHandler* handler = fl_engine_get_mouse_cursor_handler(self->engine); @@ -180,7 +189,7 @@ // Updates the engine with the current window metrics. static void handle_geometry_changed(FlView* self) { // No updates required when size controlled by Flutter. - if (self->sized_to_content) { + if (self->sized_to_content || self->engine == nullptr) { return; } @@ -556,7 +565,9 @@ static void fl_view_dispose(GObject* object) { FlView* self = FL_VIEW(object); - g_cancellable_cancel(self->cancellable); + if (self->cancellable != nullptr) { + g_cancellable_cancel(self->cancellable); + } g_clear_object(&self->zoom_gesture); g_clear_object(&self->rotate_gesture); @@ -576,7 +587,6 @@ nullptr); } - g_clear_object(&self->renderer); g_clear_object(&self->engine); g_clear_object(&self->window_state_monitor); g_clear_object(&self->scrolling_manager); @@ -657,10 +667,22 @@ return handle_key_event(self, key_event); } +static void fl_view_finalize(GObject* object) { + FlView* self = FL_VIEW(object); + + // Released in finalize rather than dispose so self->renderer remains valid if + // the Flutter raster thread holds a strong reference to this FlView (via + // get_renderable()) while gtk_container_dispose() destroys child widgets. + g_clear_object(&self->renderer); + + G_OBJECT_CLASS(fl_view_parent_class)->finalize(object); +} + static void fl_view_class_init(FlViewClass* klass) { GObjectClass* object_class = G_OBJECT_CLASS(klass); object_class->notify = fl_view_notify; object_class->dispose = fl_view_dispose; + object_class->finalize = fl_view_finalize; GtkWidgetClass* widget_class = GTK_WIDGET_CLASS(klass); widget_class->realize = fl_view_realize; @@ -711,6 +733,10 @@ } break; } + // Retain a strong reference owned by FlView (released in fl_view_finalize). + // Without this, gtk_container_dispose() on self->event_box unrefs and frees + // self->renderer during dispose() while the raster thread may still call + // fl_view_present_layers(self->renderer). g_object_ref_sink(self->renderer); gtk_widget_show(GTK_WIDGET(self->renderer)); gtk_container_add(GTK_CONTAINER(self->event_box), GTK_WIDGET(self->renderer));
diff --git a/engine/src/flutter/shell/platform/linux/fl_view_renderer_opengl.cc b/engine/src/flutter/shell/platform/linux/fl_view_renderer_opengl.cc index f0c973d..2a2b1d5 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view_renderer_opengl.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view_renderer_opengl.cc
@@ -59,7 +59,7 @@ static gboolean redraw_cb(gpointer user_data) { g_autoptr(FlViewRendererOpenGL) self = FL_VIEW_RENDERER_OPENGL(user_data); - if (self->compositor == nullptr) { + if (self->compositor == nullptr || self->engine == nullptr) { return G_SOURCE_REMOVE; } @@ -91,6 +91,9 @@ gint scale_factor) { gint64 expiry_time = g_get_monotonic_time() + kRenderTimeoutMicroseconds; while (true) { + if (self->compositor == nullptr) { + break; + } size_t width = gdk_window_get_width(window) * scale_factor; size_t height = gdk_window_get_height(window) * scale_factor; size_t frame_width, frame_height; @@ -194,18 +197,22 @@ } // Implements FlViewRenderer::present_layers. +// Called on the Flutter raster thread. static void fl_view_renderer_opengl_present_layers(FlViewRenderer* renderer, const FlutterLayer** layers, size_t layers_count) { FlViewRendererOpenGL* self = FL_VIEW_RENDERER_OPENGL(renderer); - // Frames may be presented before the widget is realized and the compositor - // is set up; ignore them. - if (self->compositor == nullptr) { + g_mutex_lock(&self->frame_mutex); + + // Check compositor and frame under frame_mutex so the GTK main thread cannot + // clear and free them in cleanup_opengl_resources() between the null check + // and fl_opengl_frame_composite(). + if (self->compositor == nullptr || self->frame == nullptr) { + g_mutex_unlock(&self->frame_mutex); return; } - g_mutex_lock(&self->frame_mutex); fl_opengl_frame_composite(self->frame, self->compositor, layers, layers_count); g_mutex_unlock(&self->frame_mutex); @@ -217,13 +224,51 @@ g_idle_add(redraw_cb, g_object_ref(self)); } +// Teardown of FlViewRendererOpenGL spans three GObject/GTK stages across two +// threads (the GTK main thread and the Flutter raster thread): +// +// 1. unrealize() [GTK main thread]: Destroys the native X11 GdkWindow. +// 2. dispose() [GTK main thread]: Triggered by gtk_widget_destroy(). Detaches +// references immediately, even if the Flutter raster thread still holds a +// strong reference to FlView (and thus this renderer) inside +// present_layers(). +// 3. finalize() [GTK main thread OR Flutter raster thread]: Runs when the +// final g_object_unref() drops ref_count to 0. +// +// Because FlCompositorOpenGLShader disposal calls +// gdk_gl_context_clear_current() and binds platform_context, +// self->render_context, self->compositor, and self->frame must be released on +// the GTK main thread in unrealize()/dispose() under frame_mutex, whereas +// self->task_runner and self->frame_mutex must remain valid until finalize(). +static void cleanup_opengl_resources(FlViewRendererOpenGL* self) { + g_mutex_lock(&self->frame_mutex); + // Only clear the GTK thread's current GdkGLContext if this window's + // render_context is the one currently bound, preserving any other window's + // active fallback GdkGLContext (see #192137). + if (self->render_context != nullptr && + gdk_gl_context_get_current() == self->render_context) { + gdk_gl_context_clear_current(); + } + g_clear_object(&self->render_context); + g_clear_object(&self->compositor); + g_clear_object(&self->frame); + g_mutex_unlock(&self->frame_mutex); +} + +// Implements GtkWidget::unrealize. +static void fl_view_renderer_opengl_unrealize(GtkWidget* widget) { + FlViewRendererOpenGL* self = FL_VIEW_RENDERER_OPENGL(widget); + + cleanup_opengl_resources(self); + + GTK_WIDGET_CLASS(fl_view_renderer_opengl_parent_class)->unrealize(widget); +} + static void fl_view_renderer_opengl_dispose(GObject* object) { FlViewRendererOpenGL* self = FL_VIEW_RENDERER_OPENGL(object); + cleanup_opengl_resources(self); g_clear_object(&self->engine); - g_clear_object(&self->render_context); - g_clear_object(&self->task_runner); - g_mutex_clear(&self->frame_mutex); G_OBJECT_CLASS(fl_view_renderer_opengl_parent_class)->dispose(object); } @@ -231,13 +276,12 @@ static void fl_view_renderer_opengl_finalize(GObject* object) { FlViewRendererOpenGL* self = FL_VIEW_RENDERER_OPENGL(object); - // The compositor is released here rather than in dispose() so it outlives a - // forced dispose (e.g. gtk_widget_destroy()) and is only freed once the last - // reference is dropped. This keeps it alive for the raster thread, which - // holds a strong reference on the view (and thus this renderer) while - // presenting. - g_clear_object(&self->compositor); - g_clear_object(&self->frame); + // Released in finalize() after the last reference is dropped so an in-flight + // present_layers() call on the Flutter raster thread can safely lock + // frame_mutex, observe self->compositor == nullptr, and unlock after + // gtk_widget_destroy(). + g_clear_object(&self->task_runner); + g_mutex_clear(&self->frame_mutex); G_OBJECT_CLASS(fl_view_renderer_opengl_parent_class)->finalize(object); } @@ -250,6 +294,7 @@ GtkWidgetClass* widget_class = GTK_WIDGET_CLASS(klass); widget_class->realize = fl_view_renderer_opengl_realize; + widget_class->unrealize = fl_view_renderer_opengl_unrealize; widget_class->draw = fl_view_renderer_opengl_draw; FlViewRendererClass* renderer_class = FL_VIEW_RENDERER_CLASS(klass);
diff --git a/engine/src/flutter/shell/platform/linux/fl_view_renderer_subsurface.cc b/engine/src/flutter/shell/platform/linux/fl_view_renderer_subsurface.cc index 2fb99cd..c05f63e 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view_renderer_subsurface.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view_renderer_subsurface.cc
@@ -434,8 +434,6 @@ FlViewRendererSubsurface* self = FL_VIEW_RENDERER_SUBSURFACE(object); g_clear_object(&self->engine); - g_clear_object(&self->task_runner); - g_mutex_clear(&self->frame_mutex); G_OBJECT_CLASS(fl_view_renderer_subsurface_parent_class)->dispose(object); } @@ -450,6 +448,8 @@ // presenting. g_clear_object(&self->compositor); g_clear_object(&self->framebuffer); + g_clear_object(&self->task_runner); + g_mutex_clear(&self->frame_mutex); G_OBJECT_CLASS(fl_view_renderer_subsurface_parent_class)->finalize(object); }
diff --git a/engine/src/flutter/shell/platform/linux/testing/mock_gtk.cc b/engine/src/flutter/shell/platform/linux/testing/mock_gtk.cc index 0c16f89..2ca0faf 100644 --- a/engine/src/flutter/shell/platform/linux/testing/mock_gtk.cc +++ b/engine/src/flutter/shell/platform/linux/testing/mock_gtk.cc
@@ -159,8 +159,16 @@ check_thread(); } +static GdkGLContext* current_gl_context = nullptr; + +GdkGLContext* gdk_gl_context_get_current() { + check_thread(); + return current_gl_context; +} + void gdk_gl_context_clear_current() { check_thread(); + current_gl_context = nullptr; if (mock != nullptr) { mock->gdk_gl_context_clear_current(); } @@ -168,6 +176,7 @@ void gdk_gl_context_make_current(GdkGLContext* context) { check_thread(); + current_gl_context = context; } void gdk_cairo_draw_from_gl(cairo_t* cr,