-
Notifications
You must be signed in to change notification settings - Fork 924
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature Request]: Ability to make event_loop async to satisfy WGPU on WASM in Resumed Event #3560
Comments
I see something called |
It has been previously discussed to add an |
this is what i'm trying to work with atm https://github.com/lexi-the-cute/catgirl-engine/blob/e383587c190d7446f69f470951c21465a29d881e/client/src/game/game_loop.rs#L71-L78 |
I do plan to experiment with adding a platform-specific extension for an async event loop to Web in Winit. You should consider handling async stuff out of band, e.g. use |
thanks! i ended up learning about mutexes and made a static mutex to handle this |
Is there any official recommendation or direction on how to a properly setup wgpu on Short of doing something like a static mutex, it's a real struggle to figure out how to create these resources inside the event_loop even with channel passing without a convoluted multi-phase setup with a bunch of
This means your application has to operate with all these three intermediate optional states and enforces that the The documentation seems to suggest this is the recommended approach for Web, but fails to address any possible issues of async:
To clarify, this is primarily targeted at 0.29.0 |
Here's a heavily trimmed down snippet of what I have working. It has three states:
There's an extra channel involved so that the If anyone has other ideas how to do this in a cleaner way without as many runtime checks on codeuse crate::{config::Config, renderer::Renderer};
use anyhow::{anyhow, Context};
use crossbeam::channel::{self, Receiver};
use std::{future::Future, sync::Arc};
use winit::{
event::Event,
event_loop::{EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget},
window::{Fullscreen, Window, WindowBuilder},
};
struct App {
/// Set during initialization, then taken and set to `None` when running because
/// `EventLoopProxy` can only be created on the initial `EventLoop` and not on
/// `&EventLoopWindowTarget`.
pub(crate) init_state: Option<(Config, EventLoopProxy<AppEvent>)>,
pub(crate) state: Option<State>,
}
enum State {
PendingGpuResources {
window: Arc<Window>,
rx: Receiver<anyhow::Result<GpuResources>>,
},
Running(Running),
}
struct Running {
cfg: Config,
tx: EventLoopProxy<AppEvent>,
window: Arc<Window>,
renderer: Renderer,
// additional running state
}
#[derive(Debug)]
pub enum AppEvent {
GpuResourcesUpdate,
Other, // ...other events
}
impl App {
/// Create app and start event loop.
fn run(cfg: Config) -> anyhow::Result<()> {
let event_loop = EventLoopBuilder::<AppEvent>::with_user_event().build()?;
let mut app = App::new(cfg, &event_loop)?;
event_loop.run(move |event, window_target| app.event_loop(event, window_target))?;
Ok(())
}
/// Create a new app.
fn new(cfg: Config, event_loop: &EventLoop<AppEvent>) -> anyhow::Result<Self> {
let tx = event_loop.create_proxy();
Ok(Self {
init_state: Some((cfg, tx)),
state: None,
})
}
/// Create a window and request GPU resources. Transitions from `state` `None` to
/// `Some(PendingGpuResources { .. })`.
pub fn create_window(
&mut self,
event_loop: &EventLoopWindowTarget<AppEvent>,
) -> anyhow::Result<()> {
let (cfg, tx) = self.init_state.as_ref().expect("config already taken");
let window_size = cfg.window_size();
let texture_size = cfg.texture_size();
let window = WindowBuilder::new()
.with_active(true)
.with_inner_size(window_size)
.with_min_inner_size(texture_size)
.with_title(Config::WINDOW_TITLE)
.with_fullscreen(
cfg.renderer
.fullscreen
.then_some(Fullscreen::Borderless(None)),
)
.with_resizable(true)
.build(event_loop)?;
let window = Arc::new(window);
let rx = GpuResources::request(tx.clone(), Arc::clone(&window));
self.state = Some(State::PendingGpuResources { window, rx });
Ok(())
}
/// Initialize the running state after a window and GPU resources are created. Transitions
/// `state` from `Some(PendingGpuResources { .. })` to `Some(Running { .. })`.
fn init_running(
&mut self,
event_loop: &EventLoopWindowTarget<AppEvent>,
) -> anyhow::Result<()> {
match self.state.take() {
Some(State::PendingGpuResources { window, rx }) => {
let resources = rx.try_recv()??;
let (cfg, tx) = self
.init_state
.take()
.expect("config unexpectedly already taken");
let renderer = Renderer::init(
tx.clone(),
Arc::clone(&window),
event_loop,
resources,
cfg.clone(),
)?;
let running = Running {
cfg,
tx,
window,
renderer,
};
self.state = Some(State::Running(running));
}
Some(State::Running(_)) => tracing::warn!("already running"),
None => anyhow::bail!("must create window and request gpu resources first"),
}
Ok(())
}
fn event_loop(
&mut self,
event: Event<AppEvent>,
event_loop: &EventLoopWindowTarget<AppEvent>,
) {
match event {
Event::Resumed => {
if self.state.is_none() {
if let Err(err) = self.create_window(event_loop) {
tracing::error!("failed to create window: {err:?}");
event_loop.exit();
}
};
}
Event::UserEvent(event) => {
if let Some(State::PendingGpuResources { .. }) = &mut self.state {
if let AppEvent::GpuResourcesUpdate = event {
if let Err(err) = self.init_running(event_loop) {
tracing::error!("failed to initialize running state: {err:?}");
event_loop.exit();
return;
}
}
}
}
_ => (), // ...handle other events
}
}
}
pub struct GpuResources {
surface: wgpu::Surface<'static>,
adapter: wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
}
pub fn spawn<F>(future: F)
where
F: Future<Output = ()> + 'static,
{
#[cfg(target_arch = "wasm32")]
wasm_bindgen_futures::spawn_local(future);
#[cfg(not(target_arch = "wasm32"))]
pollster::block_on(future)
}
impl GpuResources {
pub fn request(
proxy_tx: EventLoopProxy<AppEvent>,
window: Arc<Window>,
) -> Receiver<anyhow::Result<Self>> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::default());
// Channel passing to do async out-of-band within the winit event_loop since wasm can't
// execute futures with a return value
let (tx, rx) = channel::bounded(1);
spawn({
async move {
let surface = match instance
.create_surface(Arc::clone(&window))
.context("failed to create wgpu surface")
{
Ok(surface) => surface,
Err(err) => {
proxy_tx.send_event(AppEvent::GpuResourcesUpdate).unwrap();
tx.send(Err(err)).unwrap();
return;
}
};
let Some(adapter) = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await
else {
proxy_tx.send_event(AppEvent::GpuResourcesUpdate).unwrap();
tx.send(Err(anyhow!("failed to find a wgpu adapter")))
.unwrap();
return;
};
// WebGL doesn't support all of wgpu's features, so if
// we're building for the web we'll have to disable some.
let mut required_limits = if cfg!(target_arch = "wasm32") {
wgpu::Limits::downlevel_webgl2_defaults()
} else {
wgpu::Limits::downlevel_defaults()
};
// However, we do want to support the adapters max texture dimension for window size to
// be maximized
required_limits.max_texture_dimension_2d =
adapter.limits().max_texture_dimension_2d;
proxy_tx.send_event(AppEvent::GpuResourcesUpdate).unwrap();
tx.send(
adapter
.request_device(
&wgpu::DeviceDescriptor {
label: Some("app"),
required_features: wgpu::Features::CLEAR_TEXTURE,
required_limits,
},
None,
)
.await
.context("failed to request wgpu device")
.map(|(device, queue)| Self {
surface,
adapter,
device,
queue,
}),
)
.unwrap();
}
});
rx
}
} |
Our solution for stuff like that is that you create an Keep in mind that all of that is not new, in particular. You can also just use |
Do you have an example somewhere? I posted a more detailed example and while it works, it's far from ideal. I especially dislike requiring one channel to notify that another channel has data ready to get around the derived trait limitations. |
https://github.com/notgull/async-winit Though, maybe @notgull wants to bring it more up to date. |
Thanks! However, that doesn't seem compatible with my needs as it doesn't appear to support |
I know that since 0.31 there will be way more freedom because a lot of things will become Though, it's much easier to wrap |
I'm not sure any amount of wrapping would help. I've tried various iterations - the core of the issue is that during the What would be nice would be to be able to cleanly return control from the scheduled event loop callback, let the microtick of The prime example where this gets extra hairy is when using |
There is no official recommendation from Winit on how exactly to do this, but your example is basically what I am using as well. It might be a good idea to add an Winit + |
That would be great! I also discovered during this development that chrome panics sometimes on page refresh with a BorrowMutError inside set_listener that didn't happen prior to initializing wgpu on Resumed (previously it was being done before event_loop was started) - some sort of re-entrant issue when unloading the wasm module I'm guessing. I'll be filing an issue with the stack trace later today. |
I struggling with the same combination: WASM + winit + WGPU I really would love to see a working example of this combination, because I think it'll be one of the main operational modes of games that shall run on browsers and desktops. |
You may need to dig between a few different files but I got this working awhile back with my emulator:
It basically uses a state machine to request async resources on start up, and then sends a custom Ready event when the async resource has finished on a background thread to transition the app into a Running state. |
I would like to request the ability to
.await
code within the event loop, such as inside the Resumed event. This'll allow initializing libraries such as WGPU which require async code to be ran on platforms without thread blocking such as WASM. Right now the only way to run async functions without blocking is to do it outside of the event loop and to keep asyncing everything up to the original function that the program starts at (with wasm_bindgen)I'm trying to support 3 platforms. The platforms are Desktop, Android, and Web Browser. I have the first two platforms down which both wait for the Resumed event to be called before creating the window. This is the only thing preventing me from supporting the web browser
The text was updated successfully, but these errors were encountered: