如何基于winit实现单线程异步事件循环的60FPS NES模拟器游戏循环?
解决方案:Winit构建NES模拟器的跨平台游戏循环实现
优先方案:利用Winit重绘事件驱动60fps循环
这是最适配Winit生态、跨原生/Wasm环境都稳定的方案,完全规避线程或异步任务的兼容性问题。核心思路是借助显示器垂直同步的节奏,用窗口重绘事件触发每一次模拟器帧迭代:
实现步骤
- 初始化时调用
Window::request_redraw()触发第一次循环启动 - 在
Event::RedrawRequested事件处理中完成:- 执行NES一帧的核心逻辑(CPU指令运行、PPU渲染)
- 将渲染结果绘制到窗口(如wgpu/softbuffer等渲染库)
- 再次调用
request_redraw()触发下一次循环
示例代码
use winit::{ event::{Event, WindowEvent}, event_loop::{ControlFlow, EventLoop}, window::WindowBuilder, }; struct NesEmulator { // 模拟器核心状态:CPU、PPU、内存、控制器等 } impl NesEmulator { fn new() -> Self { // 初始化模拟器硬件状态 Self {} } fn run_frame(&mut self) { // 运行一帧逻辑:执行CPU指令直到PPU完成一帧扫描 } fn render(&self, window: &winit::window::Window) { // 将PPU输出的帧数据绘制到窗口 } } fn main() { let event_loop = EventLoop::new(); let window = WindowBuilder::new().build(&event_loop).unwrap(); let mut emulator = NesEmulator::new(); // 启动循环 window.request_redraw(); event_loop.run(move |event, _, control_flow| { *control_flow = ControlFlow::Wait; match event { Event::RedrawRequested(_) => { emulator.run_frame(); emulator.render(&window); window.request_redraw(); } Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => { *control_flow = ControlFlow::Exit; } _ => {} } }); }
优势
- 自动对齐显示器垂直同步,帧率稳定在60fps,无卡顿
- 原生/Wasm环境完全兼容,无需特殊适配
- 避免手动计时的精度问题(如Wasm定时器精度不足)
修复Async_std Spawn_local方案的问题
你之前的异步方案失败核心原因是:Winit的事件循环是阻塞式的,没有主动驱动异步任务的调度。以下是修复方式:
实现要点
- 使用
futures::executor::LocalPool管理本地异步任务 - 在Winit事件循环的每次迭代中,调用
pool.run_until_stalled()轮询异步任务进度
示例代码
use futures::executor::LocalPool; use futures::task::LocalSpawnExt; use winit::{ event::{Event, WindowEvent}, event_loop::{ControlFlow, EventLoop}, window::WindowBuilder, }; struct NesEmulator { // 模拟器状态 } impl NesEmulator { async fn run_loop(&mut self, window: winit::window::Window) { // 适配Wasm的定时器,原生环境可替换为std::time::Interval let mut interval = wasm_timer::Interval::new(std::time::Duration::from_millis(16)).await; loop { interval.tick().await; self.run_frame(); self.render(&window); } } fn run_frame(&mut self) { /* 帧逻辑 */ } fn render(&self, window: &winit::window::Window) { /* 渲染逻辑 */ } } fn main() { let event_loop = EventLoop::new(); let window = WindowBuilder::new().build(&event_loop).unwrap(); let mut emulator = NesEmulator::new(); let mut pool = LocalPool::new(); let spawner = pool.spawner(); // 启动异步循环任务 spawner.spawn_local(emulator.run_loop(window)).unwrap(); event_loop.run(move |event, _, control_flow| { *control_flow = ControlFlow::Wait; match event { Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => { *control_flow = ControlFlow::Exit; } _ => {} } // 每次事件循环迭代时轮询异步任务 pool.run_until_stalled(); }); }
注意:此方案仍存在Wasm定时器精度不足的问题,帧率稳定性不如重绘事件驱动方案。
替代方案:Web Workers(Wasm环境专用)
如果模拟器核心逻辑计算量极大(如带调试功能、复杂音效处理),担心阻塞主线程,可将模拟器核心放到Web Worker中:
实现思路
- 将模拟器核心编译为Web Worker模块,通过
wasm-bindgen与主线程通信 - 主线程(Winit所在线程)发送
run_frame指令给Worker,Worker执行完帧逻辑后返回渲染数据 - 主线程收到数据后绘制窗口,并再次请求Worker执行下一帧
注意事项
- Worker无法访问DOM或Winit窗口对象,所有渲染操作必须在主线程完成
- 数据传递优先使用
SharedArrayBuffer(需配置跨源隔离)减少拷贝开销
此方案复杂度较高,仅当主线程阻塞问题明显时才建议使用。
内容的提问来源于stack exchange,提问作者Eduardo Porto
相关产品推荐
相关产品推荐

