Smithay / wayland-rs

Rust implementation of the wayland protocol (client and server).
MIT License
1.09k stars 123 forks source link

wayland-client blocking dispatch with timeout #634

Open fredizzimo opened 1 year ago

fredizzimo commented 1 year ago

It would be nice to have a blocking_dispatch with a timeout.

I'm trying to implement frame callbacks for Neovide, were we have the main logic and rendering loop in one thread, and the event processing in another thread. I want to add frame callbacks to the render thread, but with a timeout, so that it still can do some processing when a callback is not received for some time, maybe because the window is hidden or something.

I manged to get it to almost work by following the Integrating the event queue with other sources of events example, and polling the socket with a timeout.

However, there's one problem, I see regular short freezes, maybe once every other minute or so and tracked down the cause to the same as blocking_read is avoiding https://github.com/Smithay/wayland-rs/blob/90a9ad1f8f1fdef72e96d3c48bdb76b53a7722ff/wayland-client/src/event_queue.rs#L396-L404

And I confirmed it by adding a short sleep between dispatch_pending and prepare_read, a 2 ms delay make the short pauses very frequent.

The problem is that I can't use the same workaround myself, since inner is private. So either that needs to be exposed, or better, provide a timeout for blocking_dispatch.

Edit: I think I could create yet another thread, and call 'blocking_dispatch` from that and handles the frame callbacks, and then uses a condition variable for example to signal when the frame callback happens, so that the rendering thread can wait for that with a timeout instead. But that seems like an unnecessary complicated thing to do.

My current work in progress code is here https://github.com/fredizzimo/neovide/blob/12aa5c74f330aed918a84eed561b262cbd1980c8/src/renderer/vsync_wayland.rs

fredizzimo commented 1 year ago

One workaround that seems to work is to move the dispatch_pending call after calling prepare_read only actually poll the socket and read if it returns 0.

So something like this

pub fn wait_for_vsync(&mut self) {
    while !self.vsync_signaled.load(Ordering::Relaxed) {
        self.event_queue.flush().unwrap();
        let read_guard = self.event_queue.prepare_read().unwrap();
        if self
            .event_queue
            .dispatch_pending(&mut self.dispatcher)
            .unwrap()
            == 0
        {
            let mut fds = [PollFd::new(
                read_guard.connection_fd().as_raw_fd(),
                PollFlags::POLLIN | nix::poll::PollFlags::POLLERR,
            )];

            let n = loop {
                match nix::poll::poll(&mut fds, 1000) {
                    Ok(n) => break n,
                    Err(nix::errno::Errno::EINTR) => continue,
                    Err(_) => break 0,
                }
            };
            if n > 0 {
                read_guard.read().unwrap();
            }
        }
    }
    self.vsync_signaled.store(false, Ordering::Relaxed);
    let _callback = self.wl_surface.frame(&self.event_queue_handle, ());
}

There could be another dispatch_pending check before that to avoid locking if not necessary. But I don't think that matters in my use case, so I left it out.

elinorbgr commented 1 year ago

Yeah, reading and processing events from multiple threads at the same time is kind of a pain point. I think your workaround is a good way to handle it, but I agree adding a blocking dispatch with timeout API to wayland-client would probably be the best, and I see no reason not to.