Skip to content

[Linux] Cancel active frame wait and defer GTK window destruction during multi-window teardown - #193126

Open
jtmcdole wants to merge 1 commit into
windowing_renderer_syncfrom
windowing_test_linux
Open

jtmcdole wants to merge 1 commit into
windowing_renderer_syncfrom
windowing_test_linux

Conversation

@jtmcdole

@jtmcdole jtmcdole commented Sep 21, 2026 •

Copy link
Copy Markdown
Member

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.

sequenceDiagram
    participant Dart as Dart Isolate (GTK Thread)
    participant GTK as GTK3 / GLib Main Loop (GTK Thread)
    participant Raster as Flutter Engine Raster Thread

    rect
    Note over GTK,Raster: Phase 2: Frame Presentation & Re-entrant Draw Wait
    Raster->>Raster: get_renderable(view_id) [acquires g_object_ref(FlView)]
    Raster->>Raster: present_layers(): lock frame_mutex, composite into self->frame
    Raster->>GTK: fl_task_runner_stop_wait() + g_idle_add(redraw_cb)
    Raster->>Raster: g_object_unref(FlView)
    GTK->>GTK: gtk_widget_draw_internal() -> fl_view_renderer_opengl_draw()
    GTK->>GTK: wait_for_frame() -> fl_task_runner_wait() [pumps Dart tasks while draw is on stack!]
    end

    rect
    Note over Dart,GTK: Phase 3: Window Destroy Called Inside fl_task_runner_wait()
    Dart->>GTK: controller.destroy() -> _viewMonitor.unref()
    GTK->>GTK: fl_view_monitor_dispose() -> fl_view_begin_destroy()
    GTK->>GTK: fl_view_renderer_opengl_cancel_wait(): sets self->destroyed = TRUE & wakes task_runner
    GTK->>GTK: Queues g_idle_add_full(G_PRIORITY_HIGH, destroy_toplevel_cb, g_object_ref(toplevel))
    GTK->>GTK: fl_window_monitor_dispose(): disconnects GTK signals
    Dart->>Dart: _notifyWindowDestroyed() -> dialogController.value = null
    GTK->>GTK: wait_for_frame() resumes, sees self->destroyed == TRUE, breaks immediately!
    GTK->>GTK: fl_view_renderer_opengl_draw() returns FALSE cleanly to gtk_widget_draw_internal()
    end

    rect
    Note over GTK,Raster: Phase 4: Deferred Unrealize, Dispose & Finalize
    GTK->>GTK: destroy_toplevel_cb() runs on clean main loop stack -> gtk_widget_destroy(toplevel)
    GTK->>GTK: cleanup_opengl_resources(): locks frame_mutex, clears render_context (if current), frees compositor & frame
    GTK->>GTK: fl_view_finalize() & fl_view_renderer_opengl_finalize(): frees task_runner & frame_mutex
    end
Loading

fixes: #192653
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.

@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Sep 21, 2026

@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 updates the .ci.yaml configuration file by adding the xvfb: "1" property to a target's properties. I have no feedback to provide.

@jtmcdole
jtmcdole requested a review from gaaclarke September 21, 2026 21:46
gaaclarke
gaaclarke previously approved these changes Sep 21, 2026
@jtmcdole
jtmcdole marked this pull request as draft September 21, 2026 21:52
@flutter-dashboard

Copy link
Copy Markdown

This pull request has been changed to a draft. The currently pending flutter-gold status will not be able to resolve until a new commit is pushed or the change is marked ready for review again.

For more guidance, visit Writing a golden file test for package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

@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 22, 2026
@jtmcdole

Copy link
Copy Markdown
Member Author

Gemini suggested changes (investigating):

  1. Linux windowing_test in .ci.yaml was missing xvfb: "1", causing the GTK application to abort immediately on startup in headless CI because DISPLAY was unset.
  2. With xvfb: "1" enabled, creating a secondary FlView (WindowControllerLinux) while an existing FlView is rendering on X11 crashes with:
Gdk-ERROR **: The program 'com.example.windowing_test' received an X Window System error.
The error was 'BadAccess (attempt to access private resource denied)'.
(Details: serial 321 error_code 10 request_code 150 (GLX) minor_code 26)

After #192137 (45f4e3db32b), fl_view_renderer_opengl_draw() deliberately leaves the view's GdkGLContext (which uses GLX on X11) current on the GTK main thread. When a secondary FlView is realized on the GTK main thread, fl_view_renderer_opengl_realize() calls fl_compositor_opengl_new(), which binds the engine's EGL context via eglMakeCurrent().

Under libglvnd, calling eglMakeCurrent on the GTK main thread while a GLX context (GdkGLContext) is current overwrites libglvnd's thread-local current context pointer with the EGL context without unbinding the GLX context or sending an X_GLXMakeContextCurrent(None) request to the X server, causing client-server context state desynchronization. When GTK subsequently calls gdk_gl_context_make_current() (glXMakeContextCurrent, GLX minor_code 26), libGLX sends oldContextTag = 0 for a context the X server still considers active, triggering BadAccess.

Additionally, fl_compositor_opengl_new() called fl_opengl_manager_make_current() (which binds the raster thread's render_context rather than platform_context) and did not release the EGL context on completion, leaving an EGL context bound on the GTK main thread for secondary views (view_id != kFlutterImplicitViewId).

API References

  • gdk_gl_context_clear_current:

    "Clears the current GdkGLContext. Any OpenGL calls after this function returns will be ignored until gdk_gl_context_make_current() is called."

  • eglMakeCurrent:

    "If the calling thread already has a current rendering context of the same client API type as ctx, then that context is flushed and marked as no longer current."
    "To release the current context without assigning a new one, set ctx to EGL_NO_CONTEXT and set draw and read to EGL_NO_SURFACE."

  • glXMakeContextCurrent:

    "BadAccess is generated if ctx was current to another thread at the time glXMakeContextCurrent was called."

Solution

  • Call gdk_gl_context_clear_current() in fl_opengl_manager_make_platform_current() before calling eglMakeCurrent() so any active GdkGLContext is cleanly unbound on both GdkDisplay and the X server before
    binding the EGL platform_context.
  • Update fl_compositor_opengl_new() to use fl_opengl_manager_make_platform_current() instead of fl_opengl_manager_make_current(), and call fl_opengl_manager_clear_current() after shader setup, compositor
    initialization, and shader disposal.
  • Add xvfb: "1" to Linux windowing_test in .ci.yaml.

@jtmcdole jtmcdole added CICD Run CI/CD and removed CICD Run CI/CD labels Sep 22, 2026
@jtmcdole

Copy link
Copy Markdown
Member Author

@jtmcdole

jtmcdole commented Sep 22, 2026 •

Copy link
Copy Markdown
Member Author

This still failed - but the X11 tests are passing. Now the failure is in 2 tests that require a window manager on the headless bot.

@robert-ancell - can you help out here? This is our last bringup:true test on the dashboard!

@jtmcdole
jtmcdole marked this pull request as ready for review September 22, 2026 22:15
@jtmcdole
jtmcdole requested a review from a team as a code owner September 22, 2026 22:15

@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 addresses OpenGL context state desynchronization under libglvnd by clearing the GdkGLContext before making the platform context current. Key changes include updating fl_opengl_manager_make_platform_current to clear the current GdkGLContext, clearing contexts in the compositor and shader, and adding corresponding unit tests. Feedback highlights a temporary DO NOT SUBMIT flag in .ci.yaml, warns of potential side effects from clearing the OpenGL context inside the setup_shader helper, and identifies a potential memory leak in the test due to overwriting the fixture's compositor member.

Comment thread .ci.yaml Resolved
glBufferData(GL_ARRAY_BUFFER, sizeof(vertex_data), vertex_data,
GL_STATIC_DRAW);

fl_opengl_manager_clear_current(self->opengl_manager);

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.

medium

Clearing the OpenGL context inside a helper function like setup_shader can lead to unexpected side effects if the caller expects the context to remain current, or if the caller is responsible for managing the context lifecycle. It is generally cleaner and safer to have the caller (which made the context current) also clear it.

References
  1. Optimize for readability and maintainability by keeping context management balanced and predictable. (link)

@robert-ancell

Copy link
Copy Markdown
Contributor

This still failed - but the X11 tests are passing. Now the failure is in 2 tests that require a window manager on the headless bot.

@robert-ancell - can you help out here? This is our last bringup:true test on the dashboard!

I think this CI is all Google specific? So I'm not sure how it works. Looking at engine/src/flutter/testing/xvfb.py shows it runs openbox if it is installed, so this might be just a case of making sure devicelab has this available?

@jtmcdole

Copy link
Copy Markdown
Member Author

This still failed - but the X11 tests are passing. Now the failure is in 2 tests that require a window manager on the headless bot.
@robert-ancell - can you help out here? This is our last bringup:true test on the dashboard!

I think this CI is all Google specific? So I'm not sure how it works. Looking at engine/src/flutter/testing/xvfb.py shows it runs openbox if it is installed, so this might be just a case of making sure devicelab has this available?

Thanks, I'll check in the morning if openbox is available or can be made available on the machines. One step closer to no bringup:true.

@jtmcdole
jtmcdole force-pushed the windowing_test_linux branch from 7c91ba9 to 0e0e5e6 Compare September 23, 2026 22:06
@jtmcdole

Copy link
Copy Markdown
Member Author

Adding some code to bring up openbox got the resizing tests to work, but there are most tests that fail after. This is a spicy test group....

@github-actions github-actions Bot added the framework flutter/packages/flutter repository. See also f: labels. label Sep 23, 2026
@jtmcdole

Copy link
Copy Markdown
Member Author

Hit some built in flakes. (2 failures -> pass). still investigating but my longer term view is:

  1. break this up. xvfb:"1" just to get the build building
  2. write an issue about the other failures and pass it to someone that really knows GTK.

@jtmcdole

Copy link
Copy Markdown
Member Author

OK - trying to deflake this test by running it 16 times. I will say that this is very AI-agent delivered.

# Build ID Status First Attempt (flaky) CRITICAL / ERROR WARNING / Notes
1 8669823180179404913 SUCCESS ✅ flaky: false (1/1) None Clean
2 8669823178675407009 SUCCESS ✅ flaky: false (1/1) None WARNING: Timed out waiting for OpenGL frame of size 800x600 (have 1x1)
3 8669823177034190529 SUCCESS ✅ flaky: false (1/1) None WARNING: Timed out waiting for OpenGL frame of size 800x600 (have 1x1)
4 8669821346653741361 SUCCESS ⚠️ flaky: true (1 failure, passed on 2nd) CRITICAL: fl_engine_get_mouse_cursor_handler(nullptr) (FL_IS_ENGINE(self)) Attempt 1 hit cursor_changed_cb() after fl_view_begin_destroy() cleared self->engine; Attempt 2 passed cleanly
5 8669821343177405409 SUCCESS ✅ flaky: false (1/1) None Clean
6 8669821340563019089 SUCCESS ⚠️ flaky: true (1 failure, passed on 2nd) CRITICAL: fl_engine_get_mouse_cursor_handler(nullptr) (FL_IS_ENGINE(self)) Attempt 1 hit cursor_changed_cb() after fl_view_begin_destroy() cleared self->engine; Attempt 2 passed cleanly
7 8669820751505996529 SUCCESS ✅ flaky: false (1/1) None Clean
8 8669820750073104129 SUCCESS ✅ flaky: false (1/1) None WARNING: Timed out waiting for OpenGL frame of size 800x600 (have 1x1)
9 8669820748717472801 SUCCESS ✅ flaky: false (1/1) None Clean
10 8669820747100312961 SUCCESS ✅ flaky: false (1/1) None Clean
11 8669820745155647729 SUCCESS ✅ flaky: false (1/1) None Clean
12 8669820743918593057 SUCCESS ✅ flaky: false (1/1) None WARNING: Timed out waiting for OpenGL frame of size 800x600 (have 1x1)
13 8669820742414882657 SUCCESS ✅ flaky: false (1/1) None WARNING: Timed out waiting for OpenGL frame of size 800x600 (have 1x1)
14 8669820740988831649 SUCCESS ✅ flaky: false (1/1) None WARNING: Timed out waiting for OpenGL frame of size 800x600 (have 1x1)
15 8669820739613289569 SUCCESS ✅ flaky: false (1/1) None Clean
16 8669820738260266593 SUCCESS ✅ flaky: false (1/1) None Clean

Summary

  • Overall Build Status: 16 / 16 (100%) SUCCESS.
  • First-Attempt Pass Rate: 14 / 16 (87.5%) flaky: false (16 passes out of 18 total test executions).
  • Zero Gdk-CRITICAL / Gtk-CRITICAL assertions: The GDK_IS_WINDOW / GTK_IS_WIDGET use-after-free during GtkWidget::draw is completely eliminated across
    all 18 executions.
  • Follow-up fix for the 2 retries (#4 and #6): Both retries hit cursor_changed_cb() after fl_view_begin_destroy() cleared self->engine before
    destroy_toplevel_cb() disposed FlView; fixed in fl_view.cc by keeping g_clear_object(&self->engine) in fl_view_dispose() and guarding cursor_changed_cb().

@jtmcdole

Copy link
Copy Markdown
Member Author

Every single one—50 out of 50 (100.0%)—passed on the first attempt (flaky: false) with zero retries and zero CRITICAL / ERROR messages.

Well, that's nice.

@jtmcdole
jtmcdole force-pushed the windowing_test_linux branch from 498dedd to 5353edd Compare September 24, 2026 22:45
@jtmcdole jtmcdole changed the title Add xvfb: "1" to Linux windowing_test [Linux] Fix multi-window OpenGL/GTK lifecycle and enable Xvfb/openbox for windowing_test Sep 24, 2026
Comment on lines +21 to +25
wmProcess = await startProcess('openbox', const <String>[
'--sm-disable',
'--startup',
'echo $readyMarker',
]);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

openbox is the windowmanager used by xvfb.py testing in the engine; it is available on the luci bots.


// Determine once whether glBlitFramebuffer is available on this driver.
fl_opengl_manager_make_current(opengl_manager);
fl_opengl_manager_make_platform_current(opengl_manager);

@jtmcdole jtmcdole Sep 24, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This looks like a bug that was using the wrong context... switched to platform context instead

@jtmcdole
jtmcdole requested a review from mattkae September 25, 2026 00:52
@jtmcdole

Copy link
Copy Markdown
Member Author

@robert-ancell / @mattkae - I believe this is ready for review. I am not a GTK expert though and would appreciate feedback. I ran ~100 tests on our CI and they were all non-flakey with these changes.

@jtmcdole
jtmcdole force-pushed the windowing_test_linux branch from 5353edd to 91c7c66 Compare September 28, 2026 17:28
@jtmcdole

Copy link
Copy Markdown
Member Author

force push to merge conflicting changes at master.

@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.

There's a lot going on in this PR. I think cherry picking out the individual fixes for the Linux embedder is the safer way to be sure on these and make them easier to understand and bisect on if we have future issues. If you don't want to do that I'm happy to do those and then this PR should slim right down to just enabling the tests on CI.

@jtmcdole

Copy link
Copy Markdown
Member Author

There's a lot going on in this PR. I think cherry picking out the individual fixes for the Linux embedder is the safer way to be sure on these and make them easier to understand and bisect on if we have future issues. If you don't want to do that I'm happy to do those and then this PR should slim right down to just enabling the tests on CI.

I have the three commits that I could stack. I'll do that and sent them out individually.

@jtmcdole
jtmcdole changed the base branch from master to windowing_test_egl_desync September 29, 2026 22:50
@jtmcdole
jtmcdole added this pull request to stack #193531 September 29, 2026 22:50
@jtmcdole jtmcdole changed the title [Linux] Fix multi-window OpenGL/GTK lifecycle and enable Xvfb/openbox for windowing_test [Linux] Synchronize multi-window teardown across GTK draw and raster threads Sep 29, 2026
@jtmcdole
jtmcdole force-pushed the windowing_test_linux branch from 91c7c66 to d2df67d Compare September 29, 2026 23:02
@jtmcdole
jtmcdole removed this pull request from stack #193531 September 29, 2026 23:02
@jtmcdole
jtmcdole changed the base branch from windowing_test_egl_desync to windowing_renderer_sync September 29, 2026 23:03
@jtmcdole
jtmcdole added this pull request to stack #193534 September 29, 2026 23:03
@jtmcdole jtmcdole changed the title [Linux] Synchronize multi-window teardown across GTK draw and raster threads [Linux] Cancel active frame wait and defer GTK window destruction during multi-window teardown Sep 29, 2026
@jtmcdole
jtmcdole force-pushed the windowing_test_linux branch from d2df67d to c27b022 Compare September 29, 2026 23:14
@jtmcdole
jtmcdole force-pushed the windowing_test_linux branch from c27b022 to 0b76221 Compare October 1, 2026 22:52
…ing 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.

@mattkae mattkae 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.

Small points, but looks good overall! Deferring to @robert-ancell for the final review

Comment on lines +568 to +571
if (FL_IS_VIEW_RENDERER_OPENGL(self->renderer)) {
fl_view_renderer_opengl_cancel_wait(
FL_VIEW_RENDERER_OPENGL(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.

Instead of this check, should we just define fl_view_renderer_cancel_wait on each renderer, and then have it be a cancel for the GL renderer? Just thinking it might be a better pattern instead of basically static_casting

Comment on lines +532 to +534
final VoidCallback? onDestroy = _onDestroy;
_onDestroy = null;
onDestroy?.call();

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.

Can you leave a comment as to why you're capturing this first?

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. framework flutter/packages/flutter repository. See also f: 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.

Linux windowing_test failing

4 participants