sotrh / learn-wgpu

Guide for using gfx-rs's wgpu library.
https://sotrh.github.io/learn-wgpu/
MIT License
1.48k stars 258 forks source link

Windowing chapter won’t work on some platforms #532

Open JoeOsborn opened 7 months ago

JoeOsborn commented 7 months ago

According to winit’s developers, it’s not permitted to create a surface on many platforms (Mac and android for example) until the Event::Resumed winit event is posted. This is a bummer because it means you need a mini state machine (I like to use an enum for this) to determine what phase in the initialization life cycle you’re in and you probably need a future executed for its side effect on web which means you need a OnceCell or something :/

I solved this a couple different ways in my modular renderer:

  1. if you have an executor already you can do something like the code sample below.
  2. If you are ok driving the single initialization future with the event loop you can do something like https://github.com/JoeOsborn/frenderer/blob/071beb5e547d3de2a6a03681926ca340bf5fbb75/frenderer/src/events.rs#L137 .
  3. It's also possible to put the GPU state behind a cell and just early-exit the closure if it hasn't been initialized yet, or lazily initialize the surface behind an option
    let mut builder = Some(builder);
    let fut = async {
        let mut state = None;
        let elp = winit::event_loop::EventLoop::new().unwrap();
        let mut init: Option<State> = None;

        elp.run(move |event, target| {
            if let winit::event::Event::Resumed = event {
                if let Some(builder) = builder.take() {
                    let window = Arc::new(builder.build(target).unwrap());
                    // maybe add canvas to window
                    // do state init and use .await freely in here
                    state = Some(...initialize Wgpu state and surface...)
                }
            }
            if let Some(state) = state {
                // match on event, do regular winit lifecycle stuff
            }
        })
        .unwrap();
    };
    #[cfg(target_arch = "wasm32")]
    {
        wasm_bindgen_futures::spawn_local(fut);
    }
    #[cfg(not(target_arch = "wasm32"))]
    {
        pollster::block_on(fut);
    }

The async block could instead just cover the WGPU initialization.

sotrh commented 5 months ago

I'm pretty sure that you can't use .await in the run funciton as it's not async. We technically only need to async block to get the Adapter , Device and Queue, so I could split up creating state into creating a Context and creating the Display.

JoeOsborn commented 5 months ago

Yeah, you’re totally right. I had mis-copied some code from another project. I think spawning a future on the resumed event (using wasm bindgen futures or a native executor or using the event loop itself to poll the future) which writes into a oncecell is one good option.

It’s really a pain that creating the surface must happen after the resumed event, and that sometimes creating an adapter without providing a surface causes issues too. If it weren’t for the latter you could make the adapter before entering the event loop, and lazily create the surface. But that can get you the wrong adapter sometimes.

sotrh commented 5 months ago

I'm going to have to circle back to this. I'm working on updating to 0.19 and they changed Surface to use the lifetime of the window. I tried use OnceCell, but the borrow checker didn't approve. Once I get the 0.19 stuff working, I'll revisit this.

JoeOsborn commented 5 months ago

I think if your Window is an Arc, the surface gets a ‘static lifetime and is easier to work with.