如何用Wasm+winit实现持续的动画循环?
我有一段在原生窗口中可正常运行的Rust代码:
event_loop.run(move |event, _, control_flow| { control_flow.set_poll(); match event { Event::WindowEvent { event: WindowEvent::Resized(size), .. } => { // Reconfigure the surface with the new size config.width = size.width; config.height = size.height; driver.surface.configure(&driver.device, &config); // On macos the window needs to be redrawn manually after resizing window.request_redraw(); } Event::RedrawEventsCleared => { lbm.iterate(&driver, 100); console::log_1(&format!("{}", lbm.get_compute_num()).into()); } Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => *control_flow = ControlFlow::Exit, _ => {} } });
使用wasm-pack编译后在简易HTML页面中调用包含该事件循环的函数时,仅能得到第一帧的结果。我了解到winit的事件循环无法直接触发更新,因为浏览器的requestAnimationFrame控制着更新时机,winit文档中提到:
Web: Events are queued and usually sent when requestAnimationFrame fires but sometimes the events in the queue may be sent before the next requestAnimationFrame callback, for example when the scaling of the page has changed. This should be treated as an implementation detail which should not be relied on.
但我不清楚在此场景下如何调用requestAnimationFrame来创建持续循环。注:RedrawEventsCleared中的控制台日志仅会被调用一次。
在Web环境下,winit的事件循环需要和浏览器的requestAnimationFrame配合才能实现持续更新,核心思路是在每次事件处理完成后,主动请求下一次动画帧,唤醒winit的事件循环继续处理事件。
1. 绑定浏览器的requestAnimationFrame函数
通过wasm-bindgen引入浏览器的API,在Rust中调用requestAnimationFrame:
use wasm_bindgen::prelude::*; #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_namespace = window)] fn requestAnimationFrame(callback: &Closure<dyn FnMut()>) -> f64; }
2. 创建循环触发的动画回调
由于事件循环和相关变量需要被异步回调长期持有,用Arc<Mutex>包裹共享变量,避免所有权冲突:
use std::sync::{Arc, Mutex}; // 将需要跨回调访问的变量用Arc<Mutex>包裹 let event_loop = Arc::new(Mutex::new(event_loop)); let window = Arc::new(Mutex::new(window)); let driver = Arc::new(Mutex::new(driver)); let lbm = Arc::new(Mutex::new(lbm)); let config = Arc::new(Mutex::new(config)); // 创建递归调用的动画回调 let mut animation_callback = Closure::wrap(Box::new(move || { // 唤醒winit事件循环处理待处理事件 event_loop.lock().unwrap().wakeup(); // 再次请求下一个动画帧,形成循环 requestAnimationFrame(&animation_callback); }) as Box<dyn FnMut()>); // 启动第一帧触发 requestAnimationFrame(&animation_callback); // 防止闭包被Rust所有权系统回收,必须调用forget animation_callback.forget();
3. 调整事件循环的控制逻辑
保持事件循环的poll模式,确保每次被唤醒时能正常处理事件:
event_loop.run(move |event, _, control_flow| { control_flow.set_poll(); // 维持poll模式,等待外部唤醒 match event { Event::WindowEvent { event: WindowEvent::Resized(size), .. } => { let mut config = config.lock().unwrap(); config.width = size.width; config.height = size.height; let driver = driver.lock().unwrap(); driver.surface.configure(&driver.device, &config); window.lock().unwrap().request_redraw(); } Event::RedrawEventsCleared => { let mut lbm = lbm.lock().unwrap(); let driver = driver.lock().unwrap(); lbm.iterate(&driver, 100); console::log_1(&format!("{}", lbm.get_compute_num()).into()); } Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => *control_flow = ControlFlow::Exit, _ => {} } });
关键要点
- Web环境下winit事件循环不会自动循环,必须通过
requestAnimationFrame主动唤醒,因为浏览器掌控页面刷新时机。 Arc<Mutex>用于解决异步回调中的变量共享和所有权问题,虽然WebAssembly是单线程,但异步回调会导致变量生命周期冲突。- 必须调用
animation_callback.forget(),否则闭包会被Rust的自动回收机制清理,导致后续帧无法触发。
内容的提问来源于stack exchange,提问作者Pap113

