You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于winit实现单线程异步事件循环的60FPS NES模拟器游戏循环?

解决方案:Winit构建NES模拟器的跨平台游戏循环实现

优先方案:利用Winit重绘事件驱动60fps循环

这是最适配Winit生态、跨原生/Wasm环境都稳定的方案,完全规避线程或异步任务的兼容性问题。核心思路是借助显示器垂直同步的节奏,用窗口重绘事件触发每一次模拟器帧迭代:

实现步骤

  1. 初始化时调用Window::request_redraw()触发第一次循环启动
  2. 在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的事件循环是阻塞式的,没有主动驱动异步任务的调度。以下是修复方式:

实现要点

  1. 使用futures::executor::LocalPool管理本地异步任务
  2. 在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中:

实现思路

  1. 将模拟器核心编译为Web Worker模块,通过wasm-bindgen与主线程通信
  2. 主线程(Winit所在线程)发送run_frame指令给Worker,Worker执行完帧逻辑后返回渲染数据
  3. 主线程收到数据后绘制窗口,并再次请求Worker执行下一帧

注意事项

  • Worker无法访问DOM或Winit窗口对象,所有渲染操作必须在主线程完成
  • 数据传递优先使用SharedArrayBuffer(需配置跨源隔离)减少拷贝开销

此方案复杂度较高,仅当主线程阻塞问题明显时才建议使用。

内容的提问来源于stack exchange,提问作者Eduardo Porto

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 08:30:50