Skip to content

[Linux] Synchronize multi-window renderer lifetime across GTK and raster threads - #193533

Open
jtmcdole wants to merge 1 commit into
windowing_test_egl_desyncfrom
windowing_renderer_sync
Open

jtmcdole wants to merge 1 commit into
windowing_test_egl_desyncfrom
windowing_renderer_sync

Conversation

@jtmcdole

@jtmcdole jtmcdole commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Synchronizes renderer lifetime and resource teardown across the GTK main thread and the Flutter raster thread during secondary window destruction.

  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 in-flight present_layers() calls on the raster thread can safely lock frame_mutex 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 active GdkGLContext if this window render_context is the one currently bound.

Part of stack #193534.

Pre-launch Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read the [Tree Hygiene] wiki page, which explains my responsibilities.
  • I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
  • I signed the [CLA].
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant in-code documentation (doc comments with ///).
  • If this PR introduces a new feature or capability, I created and linked a website documentation issue or PR in [flutter/website] (or verified none is needed).
  • I added new tests to check the change I am making, or this PR is [test-exempt].
  • I followed the [breaking change policy] and added [Data Driven Fixes] where supported.
  • All existing and new tests are passing.

@jtmcdole
jtmcdole requested a review from a team as a code owner September 29, 2026 23:02
@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Sep 29, 2026
@github-actions github-actions Bot added engine flutter/engine related. See also e: labels. platform-linux Building on or for Linux specifically a: desktop Running on desktop team-linux Owned by the Linux platform team labels Sep 29, 2026
@jtmcdole
jtmcdole added this pull request to stack #193534 September 29, 2026 23:03

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request improves resource lifecycle management and adds null safety checks in the Flutter Linux platform shell. It introduces several null checks for the engine, window, and cancellable objects in fl_view.cc and fl_view_renderer_opengl.cc. To prevent race conditions and crashes during teardown, resource cleanup for the renderer, task runner, and frame mutex has been deferred from the dispose stage to the finalize stage. Additionally, the mock GTK testing framework has been updated to track the current GL context. There are no review comments, and I have no feedback to provide.

@jtmcdole
jtmcdole force-pushed the windowing_renderer_sync branch 2 times, most recently from 6040e2b to 8c7f34b Compare October 1, 2026 22:52
…ter 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.

// Called when the mouse cursor changes.
static void cursor_changed_cb(FlView* self) {
if (self->engine == nullptr) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is unnecessary, because the callback should have been cancelled before it was set to NULL. It looks like this is the model not understanding the GObject callback system.

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, engine should always be non-null

// 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).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comments seems overly wordy - very much a model leaving its logic trail behind.

FlView* self = FL_VIEW(object);

g_cancellable_cancel(self->cancellable);
if (self->cancellable != nullptr) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed, because g_cancellable_cancel handles nullptr being passed to it (for this exact sort of reason).

@robert-ancell robert-ancell left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've run out of time reviewing this, I will have another look next week. The changes are OK, but the actual fix is lost in the noise. There are a number of unnecessary changes and some heavy commenting that either suggests the code is not clearly written or the AI model is leaving a breadcrumb trail for the next time it looks. I'd like this to slim down to just the change that is required (probably could be done with a few more challenges to the AI).

@saitatter

Copy link
Copy Markdown

I reproduced the FlView → renderer lifetime gap with the released 3.47.7 GTK library and in an engine unit test on master 215858ff8a1e51fb5481c7c7cc4c6d4be54b8ae0.

The test below deterministically observes renderer finalization while the caller still owns the view. It fails on master and passes when renderer release is moved from fl_view_dispose() to fl_view_finalize(), as proposed here. It does not require a realized window, plugins, sleeps or a live raster thread.

TEST_F(FlViewTest, RendererSurvivesWhileViewReferenced) {
  FlView* view = fl_view_new(project);
  g_object_ref_sink(view);
  g_autoptr(FlEngine) view_engine =
      FL_ENGINE(g_object_ref(fl_view_get_engine(view)));
  StartEngine(view_engine);
  g_autoptr(GList) children = gtk_container_get_children(GTK_CONTAINER(view));
  ASSERT_NE(children, nullptr);
  ASSERT_TRUE(GTK_IS_BIN(children->data));
  GtkWidget* renderer = gtk_bin_get_child(GTK_BIN(children->data));
  ASSERT_NE(renderer, nullptr);
  bool renderer_finalized = false;
  g_object_set_data_full(G_OBJECT(renderer), "test-renderer-finalized",
                        &renderer_finalized, [](gpointer data) {
                          *static_cast<bool*>(data) = true;
                        });

  fl_gtk_widget_destroy(GTK_WIDGET(view));
  EXPECT_FALSE(renderer_finalized);
  g_object_unref(view);
  EXPECT_TRUE(renderer_finalized);
  if (!renderer_finalized) {
    g_object_steal_data(G_OBJECT(renderer), "test-renderer-finalized");
  }
}

The qdata destroy notifier observes finalization; a weak-pointer notification would be insufficient because forced disposal can clear weak references before finalization.

Separately, realizing and forcibly disposing a software renderer while retaining it exposed a null task-runner crash. That backend is not changed by this PR; I reported it in #196545 and proposed the complementary two-file fix in #196546.

AI-assisted investigation; the failing/passing controls were built and executed locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: desktop Running on desktop CICD Run CI/CD engine flutter/engine related. See also e: labels. platform-linux Building on or for Linux specifically team-linux Owned by the Linux platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants