小时候玩红白机(或者更准确地说,是各种兼容机),很多画面我到现在都记得:电视机的雪花点、手柄线在地上绕来绕去、开机那一声“哔——”,还有卡带吹两口气再插回去的玄学仪式。
放学回家第一件事不是写作业,是趁家里人还没管到,先打一会儿《马里奥》或者《魂斗罗》,死了就重来,能多过一关都觉得自己厉害得不行。
后来长大了,我突然意识到一件事:我们记忆里的那些画面和声音,其实是硬件在极其有限的条件下“挤出来”的结果。
这也让我一直有个执念:亲手做一个 NES 模拟器——把一台三十多年前的小机器的 CPU/PPU/APU 和各种“怪癖”一点点复刻出来,让它重新跑起来,像复活一样。
一次放弃与一次回头:从 Kotlin 到 Rust × Flutter
其实我在 2022 年就尝试过一次,用 Kotlin 写。那会儿我对 NES 的整体架构不太理解,基本属于“照着别人的项目懵着写”,CPU 部分写了一点点就卡住了,最后没写完就放弃了。
但最近不知道怎么又上头了。于是我重新开坑,这次选了我更喜欢的组合:Rust + Flutter。Rust 的性能和“写对了就很踏实”的感觉很对我胃口;Flutter 的 UI 也更现代,我想做一个自己愿意每天打开、愿意长期打磨的模拟器,而不是一个“能跑就完事”的 demo。
这个项目我叫它 Nesium:一个追求周期精度(cycle-accurate) 的 NES 模拟器。
第一次看到画面:兴奋到有点不真实
我必须承认,当我第一次在自己写的模拟器上看到画面出来的时候,是无比兴奋的。哪怕那张画面有 bug:颜色不对、滚屏怪怪的、偶尔还会抖一下……但那一刻的感觉非常明确——“它活了”。
尤其是跟我 2022 年那次尝试比起来,这几乎是跨越式的进步:那次我连“成果的影子”都看不到,而这次我至少真的把 CPU 和 PPU 推到了能出画面的阶段。
模拟器最折磨人的地方也在这儿:它的正反馈来得太慢。
你写普通业务代码,可能改几行 UI 就能立刻看到效果;但模拟器不行——你至少得把 CPU 跑对一大半、PPU 的时序和寄存器行为也像样,才能看到“最基本的成果”。在此之前,很多时候你面对的只有一堆日志、一个黑屏、或者一个永远不变的死循环。
新手最容易踩的坑:nesdev 文档太“全”,但不“带路”
很多人一开始会去读 nesdev,上面资料确实权威,也非常详细,但对新手来说,它常常是一种灾难:
它不是 step-by-step 的指南,更像一本“硬件百科全书”。
你会在里面看到海量细节:寄存器位含义、open bus、精确到周期的状态机、各种边界行为……信息量大到你很容易被淹没,然后陷入一种状态:
“我好像什么都懂了一点点,但不知道该从哪里开始写。”
我自己也经历过这个阶段。后来我才慢慢明白:做模拟器需要在“足够正确”和“先跑起来”之间找到节奏——先搭一条最小闭环,让 CPU 能跑、PPU 能出画面,再一点点把精度往上拧,用测试 ROM 去逼近真实硬件。
为什么网上一堆 NES 模拟器,我还要重写一个?
原因其实挺“任性”的,但也很现实:
1)兴趣 + 综合工程训练场
模拟器是非常综合的工程:指令级实现、时序同步、音频混音、图形渲染、Mapper 兼容、测试套件回归……每一步都细到能把人逼疯,但也正因为这样,它特别锻炼工程能力。
2)“能跑起来”很快,“写好”很难
写一个能跑起来的 NES 模拟器并不难:CPU 指令能跑、画面出来、声音响了,就能自称“完成”。
但想把它写好,是完全不同的难度:你得尽可能准确还原硬件细节,还要被各种刁钻的测试 ROM 反复拷打。我在项目里专门集成了大量测试 ROM 套件,用来验证 CPU / PPU / APU / Mapper 行为,并维护了通过情况。
我属于那种做事情“既然做了就尽可能做好”的人,所以不太想停在 demo。也因此,我已经写了两个多月,依然还有不少测试没过——这很正常,也说明它大概率会是个要打磨数年的长期项目。
3)我不爱 C/C++,也想要更现代的 UI
很多老牌模拟器是 C 或 C++ 写的。对我这种不怎么爱 C/C++ 的人来说,读代码和折腾构建系统都很痛苦;再加上一些模拟器的 UI 很“工具味”,不太符合我对美观/现代的追求。
所以这次我想用 Rust 把内核做稳,用 Flutter 把体验做漂亮。
Nesium 的整体架构(目前的样子)
我把项目拆成了「内核」和「前端」两层,并且让同一套 Rust 内核可以被不同前端复用。仓库本身是一个 Rust workspace,顶层有 apps/(前端)和 crates/(核心库/胶水层/周边库)这样的组织方式。
一句话概括:
Rust 负责准确模拟硬件;UI 只是“驾驶舱”。
1)Rust 内核:以周期精度为目标
Nesium 的定位是 cycle-accurate,重点在 6502 CPU、PPU、APU 的精确模拟。实现过程中我参考了不少成熟项目的资料与实现思路(尤其是时序、open-bus 行为、音频混音等),少踩了很多坑。
2)两套 UI 前端:一个轻量调试,一个现代体验
目前仓库里提供两种前端:
apps/nesium-egui:轻量桌面前端,适合开发期快速调试。apps/nesium_flutter:Flutter 前端,通过 Rust 的胶水层生成/加载原生动态库驱动模拟器。
你可以把它理解成:
[ Rust 内核 ] <--(FFI/胶水层)--> [ Flutter UI ]
|
+--> [ egui UI(开发调试用)]
3)Web 版:Flutter Web + Web Worker + WASM
仓库也提供 Web 构建方式:Flutter Web + Web Worker + WASM,浏览器里可直接运行(需要自带 ROM)。
4)Mapper 支持:先把“常见的坑”铺开
Mapper 决定兼容性上限。我目前优先覆盖常见 Mapper,并且会在后续持续补齐更刁钻的细节与边界行为。
我真正想把它做到什么程度?
Nesium 的长期方向更偏「精度 + 工具化 + 可扩展」:
- 精度:挑战更多测试 ROM,持续对齐硬件细节。
- 调试工具:反汇编、断点、RAM/VRAM/OAM、各种图像可视化等。
- Lua 脚本:做 TAS/自动化回归/玩法扩展。
- Netplay:网络联机。
这些都不“短平快”,但我觉得它们才是让模拟器从“播放器”变成“放大镜”的关键。
最后:项目在这儿(欢迎围观/提 Issue)
- GitHub:github.com/mikai233/ne…
- Web Demo:mikai233.github.io/nesium/(自带 ROM)
如果你也对模拟器、时序、或者 Rust × Flutter 这种组合感兴趣,欢迎来交流。这个坑我大概率会填很久——毕竟“写出来”和“写好”,从来不是同一件事。