egui icon indicating copy to clipboard operation
egui copied to clipboard

Eframe application window does not close

Open tye-exe opened this issue 8 months ago • 12 comments

Describe the bug There is bug with eframe where the application window remains open and cannot be closed while the program is running. If the previous window is left in this state for too long, the program is reported as not responding, even though code execution that does not involve the GUI still continues.

The application window is removed when the entire program exits. Alternatively, calling the "run_native" method replaces the previous window with a new one.

To Reproduce Steps to reproduce the behavior:

  1. Clone the repository that demonstrates the issue: https://github.com/tye-exe/egui_no_close .
  2. Run the application with cargo run.
  3. Attempt to close the window by clicking the "x" button.
  4. The window will remain open.

(The example program must be stopped via "Ctrl+C")

Expected behavior There should be no eframe window open after it is closed.

Video Example See here (window is replaced via calling "run_native").

Desktop:

  • OS: NixOS 24.11 (to reproduce entire system see my config)
  • Browser N/A
  • Version N/A

Additional context Attempting to update to winit master branch (currently https://github.com/rust-windowing/winit/tree/cdbdd974fbf79b82b3fb1a4bc84ed717312a3bd2) did not resolve this issue.

tye-exe avatar Apr 16 '25 14:04 tye-exe

I am currently trying to resolve this issue in eframe, but progress is slow as i'm unsure as to what i am doing.

I know that this is possible, as running the winit run on demand example shows the expected behavior.

Any additional help would be appreciated.

tye-exe avatar May 08 '25 07:05 tye-exe

A fun thing that i found out is that this code: https://github.com/emilk/egui/blob/81b7e7f05a6b03fa2cd5bdc6d4ce5f598e16c628/crates/eframe/src/native/glow_integration.rs#L793-L798 does not get run on window termination.

This is because self.integration.should_close() is false when it is run the first (and only) time.

I've got some output logs showing this (ignore the random prints, i'm just trying anything):
[2025-05-17T20:20:45Z DEBUG eframe::native::glow_integration] Received WindowEvent::CloseRequested for viewport Some("FFFF")
I am either a sub-viewport or epi is not ready to close
ROOT Viewport ID: "FFFF"
My Viewport ID: "FFFF"
Am i root: true
event
[2025-05-17T20:20:45Z DEBUG eframe::native::epi_integration] Closing root viewport (ViewportCommand::CancelClose was not sent)
Closing root viewport (ViewportCommand::CancelClose was not sent)
Exiting winit app
Exit event loop
[2025-05-17T20:20:45Z DEBUG eframe::native::run] Asking to exit event loop…
[2025-05-17T20:20:45Z DEBUG eframe::native::run] eframe window closed
SystemTime { tv_sec: 1747513245, tv_nsec: 857124481 } Window Dropped
[2025-05-17T20:20:45Z WARN  egui_glow::painter] You forgot to call destroy() on the egui glow painter. Resources will leak!
^C⏎

I'm not sure if it is at all relevant, but it might be useful to note down so here i am.

tye-exe avatar May 17 '25 20:05 tye-exe

I have also found out that this issue does not seem to be a new one, but rather a sort of zombie bug that keeps appearing and disappearing. It was apparently patched in issue https://github.com/emilk/egui/issues/2892.

I tried to run an application with this version of egui and eframe, but when i closed it i got this output:

Output
warning: queue 0x7fd64c000ca0 destroyed while proxies still attached:
  zwp_primary_selection_offer_v1#4278190081 still attached
  wl_data_offer#4278190080 still attached
  zwp_primary_selection_device_v1#39 still attached
  zwp_primary_selection_device_manager_v1#22 still attached
  wl_data_device#38 still attached
  wl_data_device_manager#37 still attached
  wl_seat#36 still attached
  wl_registry#34 still attached

And when the window tried to reopen i got: fish: Job 1, 'cargo run' terminated by signal SIGSEGV (Address boundary error.

The latest version that does not suffer from the error issue on my computer is 0.25.0 version. However, this still has the current no close bug.

tye-exe avatar May 17 '25 20:05 tye-exe

I've had some potential progress attempting to recreate this issue.

That is by holding a strong reference to the Window struct (as it is contained within an Arc), this keeps the window open while the other strong reference is held. The resulting behavior between this and what occurs in eframe is very similar. I will be focusing my efforts on trying to ensure that that there are no references in eframe to the window that persist after control is returned.

Example code using winit template

This example won't run on it's own. Modify the winit template if you wish to try this yourself.

#![allow(clippy::single_match)]

use std::sync::Arc;
use std::time::Duration;

use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::platform::run_on_demand::EventLoopExtRunOnDemand;
use winit::window::{Window, WindowId};

use crate::fill;

#[derive(Default)]
pub struct App {
    idx: usize,
    window_id: Option<WindowId>,
    /// Modified to be an Arc
    window: Option<Arc<Window>>,
}

impl<T: 'static> ApplicationHandler<T> for App {
    fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
        if let Some(window) = self.window.as_ref() {
            window.request_redraw();
        }
    }

    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        let window_attributes = Window::default_attributes()
            .with_title("Fantastic window!")
            .with_inner_size(winit::dpi::LogicalSize::new(128.0, 128.0));
        let window = event_loop.create_window(window_attributes).unwrap();
        self.window_id = Some(window.id());
        self.window = Some(window.into());

        // Thread holds lock
        let win = self.window.clone();
        std::thread::spawn(move || {
            let id = std::thread::current().id();
            println!("Grabbed Window: {} : id {id:?}", win.is_some());
            std::thread::sleep(Duration::from_secs(20));
            println!("Dropping Grab: {} : id {id:?}", win.is_some());
        });
    }

    fn window_event(
        &mut self,
        event_loop: &ActiveEventLoop,
        window_id: WindowId,
        event: WindowEvent,
    ) {
        if event == WindowEvent::Destroyed && self.window_id == Some(window_id) {
            log::info!("Window {} Destroyed", self.idx);
            self.window_id = None;
            event_loop.exit();
            return;
        }

        let window = match self.window.as_mut() {
            Some(window) => window,
            None => return,
        };

        match event {
            WindowEvent::CloseRequested => {
                log::info!("Window {} CloseRequested", self.idx);
                fill::cleanup_window(window);
                self.window = None;
            }
            WindowEvent::RedrawRequested => {
                fill::fill_window(window);
            }
            _ => (),
        }
    }
}

pub fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut event_loop = EventLoop::new().unwrap();
    let mut app = App {
        idx: 1,
        ..Default::default()
    };

    loop {
        event_loop.run_app_on_demand(&mut app)?;
        println!("Waiting 1 second");
        std::thread::sleep(Duration::from_secs(1));
    }
}

tye-exe avatar May 18 '25 10:05 tye-exe

I modified WinitApp::window() to return Option<Weak<Window>>, but this did not solve the issue. I will try modifying other parts of the code.

Modified section:

https://github.com/emilk/egui/blob/81b7e7f05a6b03fa2cd5bdc6d4ce5f598e16c628/crates/eframe/src/native/winit_integration.rs#L66-L69

tye-exe avatar May 18 '25 10:05 tye-exe

I'm running into this issue with MacOS as well. The window just hangs as the application continues.

GodTamIt avatar May 27 '25 23:05 GodTamIt

Good to know it's not just me experiencing this issue.

I'll be happy to share any resources that i have if you're able to contribute at all.

tye-exe avatar May 28 '25 05:05 tye-exe

As a further update, i changed the Viewport data structure to only allow for weak references to be given out from the window: Option<Arc<Window>> attribute. (I did this by moving it into a separate module and only exposing getter functions).

Viewport Modifications There are some extra methods for modifying a window, for convince.
use std::sync::{Arc, Weak};

use ahash::HashSet;
use egui::{
    DeferredViewportUiCallback, ViewportBuilder, ViewportClass, ViewportIdPair, ViewportInfo,
};
use winit::window::Window;

pub(crate) struct Viewport {
    pub(crate) ids: ViewportIdPair,
    pub(crate) class: ViewportClass,
    pub(crate) builder: ViewportBuilder,
    pub(crate) deferred_commands: Vec<egui::viewport::ViewportCommand>,
    pub(crate) info: ViewportInfo,
    pub(crate) actions_requested: HashSet<egui_winit::ActionRequested>,

    /// The user-callback that shows the ui.
    /// None for immediate viewports.
    pub(crate) viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,

    // These three live and die together.
    // TODO(emilk): clump them together into one struct!
    pub(crate) gl_surface: Option<glutin::surface::Surface<glutin::surface::WindowSurface>>,
    window: Option<Arc<Window>>,
    pub(crate) egui_winit: Option<egui_winit::State>,
}

impl Viewport {
    pub(crate) fn new(
        ids: ViewportIdPair,
        class: ViewportClass,
        builder: ViewportBuilder,
        deferred_commands: Vec<egui::viewport::ViewportCommand>,
        info: ViewportInfo,
        actions_requested: HashSet<egui_winit::ActionRequested>,
        viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
        gl_surface: Option<glutin::surface::Surface<glutin::surface::WindowSurface>>,
        window: Option<Arc<Window>>,
        egui_winit: Option<egui_winit::State>,
    ) -> Self {
        Self {
            ids,
            class,
            builder,
            deferred_commands,
            info,
            actions_requested,
            viewport_ui_cb,
            gl_surface,
            window,
            egui_winit,
        }
    }

    pub fn window(&self) -> Option<Weak<Window>> {
        self.window.as_ref().map(|win| Arc::downgrade(win))
    }

    pub fn window_ref(&self) -> Option<Arc<Window>> {
        self.window()
            .as_ref()
            .map(|win| Weak::upgrade(win))
            .flatten()
    }

    /// Yes this could leak a strong reference, but this is a proof of concept to get it working enough, not working perfect.
    pub fn window_modify(&mut self, callback: impl FnOnce(&mut Arc<Window>) -> ()) {
        if let Some(ref mut win) = self.window {
            callback(win)
        }
    }

    pub fn window_set_none(&mut self) {
        self.window = None;
    }
}

However, this also did not solve the issue.

tye-exe avatar May 28 '25 05:05 tye-exe

I have created a stripped-down version of eframe that i will do further testing on. This reduces the amount of code that i need to examine, so hopefully the issue will be easier to find.

https://github.com/tye-exe/eframe_rebuilt

tye-exe avatar May 28 '25 08:05 tye-exe

In regard to the Window being dropped, I can verify that it is dropped when a close is requested by adding a print statement to impl Drop for Window on a local copy of winit and checking the output. This shows that the window is dropped when expected.

Git diff for anyone interested
diff --git a/src/window.rs b/src/window.rs
index 0db22bb3..d42935ff 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -1,5 +1,6 @@
 //! The [`Window`] struct and associated types.
 use std::fmt;
+use std::time::SystemTime;
 
 use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size};
 use crate::error::{ExternalError, NotSupportedError};
@@ -55,7 +56,9 @@ impl Drop for Window {
             if let Some(Fullscreen::Exclusive(_)) = w.fullscreen().map(|f| f.into()) {
                 w.set_fullscreen(None);
             }
-        })
+        });
+
+        println!("{:?} Window Dropped", SystemTime::now());
     }
 }

Which gives the output of this video:

https://github.com/user-attachments/assets/5b230410-8ad3-42cc-8711-3e774a59130e


So it's safe to say that the any reference to the Window is not the problem.

tye-exe avatar Jun 03 '25 16:06 tye-exe

@tye-exe is it possible that the OS's window manager expects that, once a window is attached, the process must terminate before it closes the last window (to prevent zombie processes)?

GodTamIt avatar Jun 04 '25 17:06 GodTamIt

Potentially, but eframe should not need to do this, as the winit run on demand example demonstrates that the desired behavior is possible.

https://github.com/user-attachments/assets/e9d675bb-c53b-407b-b096-714020cc5cf2

(cargo warning is a remnant of the drop logging)

tye-exe avatar Jun 05 '25 05:06 tye-exe

Thanks for working on this!

I observed the same problem on Windows and on Linux (Fedora 42 with KDE).

A workaround for Linux is to force X11. Obviously this doesn't work on Windows.

Another workaround that I found was to run the window in a spawned thread and then wait for the thread to terminate. This seems to work on Linux and for Windows. Though, I need to call with_any_thread for each platform. Here a complete example for Linux with Wayland:

use eframe::egui;

fn main() {
    std::thread::spawn(move || {
        let options = eframe::NativeOptions {
            event_loop_builder: Some(Box::new(move |builder| {
                use winit::platform::wayland::EventLoopBuilderExtWayland;

                builder.with_any_thread(true);
            })),
            ..Default::default()
        };

        eframe::run_simple_native("Test", options, move |ctx, _frame| {
            egui::CentralPanel::default().show(ctx, |ui| {
                ui.label(format!("Close this window"));
            });
        })
        .unwrap();
    })
    .join()
    .unwrap();

    loop {
        println!("Window is closed");
        std::thread::sleep(std::time::Duration::from_millis(1000));
    }
}

jakoschiko avatar Jun 17 '25 13:06 jakoschiko

I tried adapting what you suggested, and it did work if you only need to open and close the window once (at least on my stripped down version of eframe), but if you need to do this multiple times, then it gives the error: called 'Result::unwrap()' on an 'Err' value: WinitEventLoop(RecreationAttempt).

You can see my adaption here: https://github.com/tye-exe/eframe_rebuilt/blob/2fa6684b79f23130ac726d63f8cfd50c519693e8/threaded_app/src/main.rs

Inline:
// Code adapted from: https://github.com/emilk/egui/issues/6757#issuecomment-2980399010

fn main() {
    env_logger::init();

    loop {
        spawn_gui();
        std::thread::sleep(std::time::Duration::from_secs(1));
    }
}

fn spawn_gui() {
    std::thread::spawn(move || {
        let options = eframe_rebuilt::epi::NativeOptions {
            event_loop_builder: Some(Box::new(move |builder| {
                use winit::platform::wayland::EventLoopBuilderExtWayland;

                builder.with_any_thread(true);
            })),
            ..Default::default()
        };

        eframe_rebuilt::run_native("test", options, Box::new(|_| Ok(Box::new(MyApp {})))).unwrap();
    })
    .join()
    .unwrap();
}

struct MyApp {}

impl eframe_rebuilt::epi::App for MyApp {
    fn update(&mut self, ctx: &egui::Context, frame: &mut eframe_rebuilt::epi::Frame) {
        //
    }
}

tye-exe avatar Jun 18 '25 13:06 tye-exe

I made a script that compares the outputs of running eframe on the main thread and running it on a spawned thread, and the output is almost identical, so at a surface level the difference between it closing instantly and closing late is not currently output by the logs.

See my recreation repository for generating logs to compare: https://github.com/tye-exe/eframe_rebuilt/tree/5246848056d01d2c457b184bbca253a9a58fd50c

tye-exe avatar Jun 18 '25 14:06 tye-exe

After testing on both Wayland and X11, I have found that the window closes as expected on X11, but lingers on Wayland. I will focus my efforts on investigating Wayland specific code.

I had to take a break investigating this issue due to life getting busy. I will be able to dedicate more time now.

tye-exe avatar Sep 23 '25 14:09 tye-exe

A current work-around is to force winit to compile to X11 via unsetting the "WAYLAND_DISPLAY" environment variable (or setting it to nothing). The application can then run on Wayland using an X11 compatibility layer, such as XWayland, which allows for the window to close normally.

I have also investigated when a decent selection of the references counted attributes in ./crates/eframe/src/native/glow_integration.rs (this file), which all seem to be dropped when the window is "closed", so the issue might be the finial frame not being cleaned up rather than a dangling reference. I go into more detail in my blog post.

tye-exe avatar Sep 24 '25 12:09 tye-exe

I have found a solution. See my PR for more information

tye-exe avatar Sep 26 '25 13:09 tye-exe