[Linux] Cancel active frame wait and defer GTK window destruction during multi-window teardown When tearing down an active secondary window on Linux: 1. Calling gtk_widget_destroy() synchronously from a Dart platform task running inside fl_view_renderer_opengl_draw() -> wait_for_frame() -> fl_task_runner_wait() unrealized the GdkWindow and freed the GtkWindow while gtk_widget_draw_internal() was still active on the C call stack. Add fl_view_begin_destroy() and fl_view_renderer_opengl_cancel_wait() to abort wait_for_frame() immediately, and schedule gtk_widget_destroy() on the GLib main loop in fl_view_monitor_dispose() once the draw stack unwinds. 2. In BaseWindowControllerLinux.destroy(), unreference FlViewMonitor and FlWindowMonitor before closing their Dart NativeCallables so that GTK signals emitted during teardown do not invoke closed callbacks. 3. Suppress listener notifications in _ToplevelWindowControllerLinux once _destroyed is true to prevent window manager signals (such as notify::is-active) from triggering rebuilds on destroyed views.
diff --git a/engine/src/flutter/shell/platform/linux/fl_view.cc b/engine/src/flutter/shell/platform/linux/fl_view.cc index e565a24..a48b9ae 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view.cc
@@ -562,9 +562,20 @@ } } +void fl_view_begin_destroy(FlView* self) { + g_return_if_fail(FL_IS_VIEW(self)); + + if (FL_IS_VIEW_RENDERER_OPENGL(self->renderer)) { + fl_view_renderer_opengl_cancel_wait( + FL_VIEW_RENDERER_OPENGL(self->renderer)); + } +} + static void fl_view_dispose(GObject* object) { FlView* self = FL_VIEW(object); + fl_view_begin_destroy(self); + if (self->cancellable != nullptr) { g_cancellable_cancel(self->cancellable); }
diff --git a/engine/src/flutter/shell/platform/linux/fl_view_monitor.cc b/engine/src/flutter/shell/platform/linux/fl_view_monitor.cc index 79ceac5..8e20baf 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view_monitor.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view_monitor.cc
@@ -6,6 +6,7 @@ #include "flutter/shell/platform/common/isolate_scope.h" #include "flutter/shell/platform/linux/fl_view_monitor.h" +#include "flutter/shell/platform/linux/fl_view_private.h" struct _FlViewMonitor { GObject parent_instance; @@ -29,9 +30,26 @@ } } +static gboolean destroy_toplevel_cb(gpointer user_data) { + GtkWidget* toplevel = GTK_WIDGET(user_data); + gtk_widget_destroy(toplevel); + return G_SOURCE_REMOVE; +} + static void fl_view_monitor_dispose(GObject* object) { FlViewMonitor* self = FL_VIEW_MONITOR(object); + if (self->view != nullptr) { + GtkWidget* toplevel = gtk_widget_get_toplevel(GTK_WIDGET(self->view)); + if (GTK_IS_WINDOW(toplevel)) { + // Defer gtk_widget_destroy to the main loop so it never frees the + // GtkWindow/GdkWindow while a synchronous GtkWidget::draw -> + // wait_for_frame -> fl_task_runner_wait stack frame is active. + g_idle_add_full(G_PRIORITY_HIGH, destroy_toplevel_cb, + g_object_ref(toplevel), g_object_unref); + } + fl_view_begin_destroy(self->view); + } g_clear_object(&self->view); G_OBJECT_CLASS(fl_view_monitor_parent_class)->dispose(object);
diff --git a/engine/src/flutter/shell/platform/linux/fl_view_private.h b/engine/src/flutter/shell/platform/linux/fl_view_private.h index cbdcef2..5d9e1ea 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view_private.h +++ b/engine/src/flutter/shell/platform/linux/fl_view_private.h
@@ -20,6 +20,15 @@ */ FlViewAccessible* fl_view_get_accessible(FlView* view); +/** + * fl_view_begin_destroy: + * @view: an #FlView. + * + * Cancels any pending frame wait and removes the view from the engine before + * its toplevel window is destroyed. + */ +void fl_view_begin_destroy(FlView* view); + G_END_DECLS #endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_VIEW_PRIVATE_H_
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 2a2b1d5..0fc2ab6 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
@@ -36,6 +36,9 @@ // Ensure Flutter and GTK can access the frame stored in the compositor. GMutex frame_mutex; + + // TRUE once teardown has started and wait_for_frame should abort immediately. + gboolean destroyed; }; G_DEFINE_TYPE(FlViewRendererOpenGL, @@ -59,7 +62,8 @@ static gboolean redraw_cb(gpointer user_data) { g_autoptr(FlViewRendererOpenGL) self = FL_VIEW_RENDERER_OPENGL(user_data); - if (self->compositor == nullptr || self->engine == nullptr) { + if (self->destroyed || self->compositor == nullptr || + self->engine == nullptr) { return G_SOURCE_REMOVE; } @@ -91,7 +95,7 @@ gint scale_factor) { gint64 expiry_time = g_get_monotonic_time() + kRenderTimeoutMicroseconds; while (true) { - if (self->compositor == nullptr) { + if (self->destroyed || self->compositor == nullptr) { break; } size_t width = gdk_window_get_width(window) * scale_factor; @@ -156,7 +160,7 @@ // The compositor is created when the widget is realized; if it is not yet // available there is nothing to render beyond the background. - if (self->compositor == nullptr) { + if (self->destroyed || self->compositor == nullptr) { return TRUE; } @@ -170,6 +174,11 @@ wait_for_frame(self, window, scale_factor); } + if (self->destroyed || self->compositor == nullptr) { + g_mutex_unlock(&self->frame_mutex); + return FALSE; + } + // The frame is drawn from an OpenGL texture, so make a context current that // can access it. This is deliberately not cleared once the frame is drawn - // GTK makes the context it needs current before using OpenGL but doesn't @@ -315,3 +324,15 @@ self->sized_to_content = sized_to_content; return self; } + +void fl_view_renderer_opengl_cancel_wait(FlViewRendererOpenGL* self) { + g_return_if_fail(FL_IS_VIEW_RENDERER_OPENGL(self)); + + g_mutex_lock(&self->frame_mutex); + self->destroyed = TRUE; + g_mutex_unlock(&self->frame_mutex); + + if (self->task_runner != nullptr) { + fl_task_runner_stop_wait(self->task_runner); + } +}
diff --git a/engine/src/flutter/shell/platform/linux/fl_view_renderer_opengl.h b/engine/src/flutter/shell/platform/linux/fl_view_renderer_opengl.h index c0d07d5..482f4e8 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view_renderer_opengl.h +++ b/engine/src/flutter/shell/platform/linux/fl_view_renderer_opengl.h
@@ -35,6 +35,14 @@ FlViewRendererOpenGL* fl_view_renderer_opengl_new(FlEngine* engine, gboolean sized_to_content); +/** + * fl_view_renderer_opengl_cancel_wait: + * @renderer: an #FlViewRendererOpenGL. + * + * Cancels any active frame wait on @renderer when the view is being destroyed. + */ +void fl_view_renderer_opengl_cancel_wait(FlViewRendererOpenGL* renderer); + G_END_DECLS #endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_VIEW_RENDERER_OPENGL_H_
diff --git a/packages/flutter/lib/src/widgets/_window_linux.dart b/packages/flutter/lib/src/widgets/_window_linux.dart index ec68b99..650f14e 100644 --- a/packages/flutter/lib/src/widgets/_window_linux.dart +++ b/packages/flutter/lib/src/widgets/_window_linux.dart
@@ -343,6 +343,13 @@ _window.add(_view); } + /// Invokes the window destruction delegate callback at most once. + /// + /// Called synchronously from [destroy] (before the deferred native + /// `gtk_widget_destroy` runs on the GLib main loop) and wired to the + /// native `GtkWidget::destroy` signal for windows closed externally. + void _notifyWindowDestroyed() {} + /// Destroys the native window and releases the monitors watching it. /// /// {@macro flutter.widgets.windowing.experimental} @@ -350,13 +357,21 @@ if (_destroyed) { return; } - _viewMonitor.close(); - _viewMonitor.unref(); - _window.destroy(); - _windowMonitor.close(); - _windowMonitor.unref(); _destroyed = true; + // Unreference the native GObject monitors before closing their Dart + // NativeCallables. Dropping the final GObject reference disconnects all + // g_signal_connect_object handlers on the GtkWindow and FlView so no GTK + // signal emitted during teardown can invoke a closed NativeCallable. + // + // Disposing _viewMonitor also aborts any active wait_for_frame() on the + // renderer and schedules gtk_widget_destroy() on the GLib main loop after + // any active GtkWidget::draw stack frame unwinds. + _viewMonitor.unref(); + _viewMonitor.close(); + _windowMonitor.unref(); + _windowMonitor.close(); _owner.registrar.unregister(rootView.viewId); + _notifyWindowDestroyed(); notifyListeners(); } @@ -510,16 +525,36 @@ /// /// {@macro flutter.widgets.windowing.experimental} mixin _ToplevelWindowControllerLinux on BaseWindowControllerLinux { + VoidCallback? _onDestroy; + + @override + void _notifyWindowDestroyed() { + final VoidCallback? onDestroy = _onDestroy; + _onDestroy = null; + onDestroy?.call(); + } + /// Watches the window for the changes a top level window can undergo. void _createWindowMonitor({required VoidCallback onClose, required VoidCallback onDestroy}) { + _onDestroy = onDestroy; + // Suppress listener notifications once _destroyed is true so GTK signals + // emitted while a window is being torn down (such as notify::is-active + // when an active dialog is closed under a window manager) do not trigger + // widget rebuilds for a destroyed view. + void notifyIfAlive() { + if (!_destroyed) { + notifyListeners(); + } + } + _windowMonitor = _FlWindowMonitor( _window, - onConfigure: notifyListeners, - onStateChanged: notifyListeners, - onIsActiveNotify: notifyListeners, - onTitleNotify: notifyListeners, + onConfigure: notifyIfAlive, + onStateChanged: notifyIfAlive, + onIsActiveNotify: notifyIfAlive, + onTitleNotify: notifyIfAlive, onClose: onClose, - onDestroy: onDestroy, + onDestroy: _notifyWindowDestroyed, ); } @@ -1155,11 +1190,6 @@ _gtkWidgetSetVisual(instance, visual.instance); } - /// Destroy the widget. - void destroy() { - _gtkWindowDestroy(instance); - } - @ffi.Native<ffi.Void Function(ffi.Pointer<ffi.NativeType>, ffi.Bool)>( symbol: 'gtk_widget_set_app_paintable', ) @@ -1196,9 +1226,6 @@ ffi.Pointer<ffi.NativeType> widget, ); - @ffi.Native<ffi.Void Function(ffi.Pointer<ffi.NativeType>)>(symbol: 'gtk_widget_destroy') - external static void _gtkWindowDestroy(ffi.Pointer<ffi.NativeType> widget); - @ffi.Native<ffi.Int Function(ffi.Pointer<ffi.NativeType>)>(symbol: 'gtk_widget_get_scale_factor') external static int _gtkWidgetGetScaleFactor(ffi.Pointer<ffi.NativeType> widget); @@ -1289,8 +1316,9 @@ int rectAnchorDx = 0, int rectAnchorDy = 0, }) { - final ffi.Pointer<_GdkRectangle> rect = _gMalloc0(ffi.sizeOf<_GdkRectangle>()) - .cast<_GdkRectangle>(); + final ffi.Pointer<_GdkRectangle> rect = _gMalloc0( + ffi.sizeOf<_GdkRectangle>(), + ).cast<_GdkRectangle>(); final _GdkRectangle r = rect.ref; r.x = x; r.y = y; @@ -1461,8 +1489,9 @@ /// Set minimum and maximum size of the window. void setGeometryHints({int? minWidth, int? minHeight, int? maxWidth, int? maxHeight}) { - final ffi.Pointer<_GdkGeometry> geometry = _gMalloc0(ffi.sizeOf<_GdkGeometry>()) - .cast<_GdkGeometry>(); + final ffi.Pointer<_GdkGeometry> geometry = _gMalloc0( + ffi.sizeOf<_GdkGeometry>(), + ).cast<_GdkGeometry>(); final _GdkGeometry g = geometry.ref; var geometryMask = 0; if (minWidth != null || minHeight != null) {