How to integrate winit 0.30 with async

I've a frame-rate (FPS) for a system which I want to run manually using one of my tickers. (I've one that uses window.setTimeout (web) / tokio::time::ticker (native) and other that uses window.requestAnimationFrame (web) / tokio::time::ticker (native).) (These are also important because these tickers provide the elapsed delta.)

Project entry points are async (whether they wrap everything with an infinite loop or not relies on if it's a native platform versus the web).

On web, bootstrap occurs slightly different, using wasm-bindgen-futures instead of Tokio. On other platforms, bootstrap uses Tokio (with the Android case a bit more wrappy using the android_activity crate).

Here's an example taken from the winit documentation doing event handling:

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

#[derive(Default)]
struct App {
    window: Option<Window>,
}

impl ApplicationHandler for App {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap());
    }

    fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
        match event {
            WindowEvent::CloseRequested => {
                println!("The close button was pressed; stopping");
                event_loop.exit();
            },
            WindowEvent::RedrawRequested => {
                // Redraw the application.
                //
                // It's preferable for applications that do not render continuously to render in
                // this event rather than in AboutToWait, since rendering in here allows
                // the program to gracefully handle redraws requested by the OS.

                // Draw.

                // Queue a RedrawRequested event.
                //
                // You only need to call this if you've determined that you need to redraw in
                // applications which do not always need to. Applications that redraw continuously
                // can render here instead.
                self.window.as_ref().unwrap().request_redraw();
            }
            _ => (),
        }
    }
}

fn main() {
    let event_loop = EventLoop::new().unwrap();

    // ControlFlow::Poll continuously runs the event loop, even if the OS hasn't
    // dispatched any events. This is ideal for games and similar applications.
    event_loop.set_control_flow(ControlFlow::Poll);

    // ControlFlow::Wait pauses the event loop if no events are available to process.
    // This is ideal for non-game applications that only update in response to user
    // input, and uses significantly less power/CPU time than ControlFlow::Poll.
    event_loop.set_control_flow(ControlFlow::Wait);

    let mut app = App::default();
    event_loop.run_app(&mut app);
}

Seems fine, but my entry point is async, so it'd conflict with winit's EventLoop (and I have a specific frame rate to target, as well). The system (rendering graphics, handling user input, executing Entity-Component-System patterns perhaps) might not be async itself though, I guess?

Also, by what everything indicates, for web the EventLoop should use .spawn_app(...) rather than .run_app(...).

Most likely you should run winit on the main thread, and Tokio in a background thread. Then have them communicate with each other. I wouldn't put them on the same thread.

3 Likes

Thanks for the tip. My concern was maybe I couldn't spawn a thread for that on the web...? Or, according to Google, latest browsers support it, so it's fine?

I do rely on some static variables, but they are not thread-local, so it's fine I guess. (And the AndroidApp struct from android-activity is also Send + Sync, so fine, I suppose.)

Researching here, winit::event::WindowEvent implements Send + Sync, so multi-threading should be feasible.

Hmm, it does look like I already asked that earlier, not sure... But not sure I was using winit. I decided to try it since it implements pointer and keyboard events for all platforms (and also does the GLFW equivalent windowing stuff).

IIRC on the Web run_app() returns to the JS event loop internally somehow (or maybe that's spawn_app()? never tried it), so tokio spawns just get queued up on that like everything else. Use an EventLoopProxy to send user events back into the UI event loop to respond, and it should "just work" portably.

1 Like

The way docs explain looks like you're right, but at the same time states it's not available on web.

Because of the exception it throws in JS after run_app(), I'd rather use spawn_app() instead on wasm32.

it's, confusingly, not (web and exception-handling), equivalent to not web or not exception-handling, so you can use it on the Web but only if you don't have the exception-handling feature enabled. So yeah, you'll want at least that minimal split there.

1 Like

Got it!

Hmm, about multi-threading, what is the most efficient way to push events from winit to the Tokio's thread? Should I end up using a mutable shared Vec that accumulates these events and drain the Vec from the Tokio's thread?

For cursor movement it seems like overkill, but I don't see other way around

I would probably start with either tokio::sync::mpsc or just calling tokio::spawn.

1 Like

I suggested just using spawn(), and let tokio figure it out, it also means you don't need threading on the Web. That's overkill for individual events, but a simple mpsc channel would be perfect for that (using tokio's to avoid blocking the JS event loop, of course)

edit: dang ninjas.

2 Likes

I saw that mpsc stuff sometimes and now I finally see a reason to use it for me... :slight_smile:

Indeed, for web it might be better not to use multi-threading. (Tokio isn't even available there, so its mpsc module wouldn't even apply anyway.)

Note to me: Decouple winit events from app's main loop (as mpsc's rx sleeps), by running winit event handlers in a separate background future's loop (future_exec).

Yeah, that's where I was saying to use tokio's mpsc, since that yields to the event loop.

I think you should be able to use at least that: tokio - Rust

Just make sure you don't enable full or rt-multi-threaded on wasm. You can control that per-target with something like:

[dependencies.tokio]
version = "1.47.1"
# supported on all platforms
features = ["rt", "sync", "macros"]

[target.'cfg(not(target_family = "wasm"))'.dependencies.tokio]
# not supported on wasm
features = ["rt-multi-thread"]
1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.