一个工作 8 年的软件工程师说,自己其实“不懂计算机到底怎么工作”。这句话比“我写了个 Game Boy 模拟器”更抓人。
Nick Kossolapov 的路径很清楚:先做 NAND to Tetris,再写 CHIP-8 模拟器,最后用 F# 做出 Fame Boy。它能跑部分 Game Boy 游戏,有声音,能在桌面端和浏览器里运行,源码也放到了 GitHub。
别把它当商业发布。也别替它吹“完整兼容所有 ROM”。原文展示了 Pokémon、Tetris 等例子,说明完成度不低;但 Fame Boy 本质上还是一个个人学习项目。
Fame Boy 到底做成了什么
这件事最先该看三个事实:F#、Game Boy、网页可玩。
Game Boy 不是一个简单玩具。它有 CPU、内存、PPU 图形单元、APU 声音单元、计时器、输入和各种 IO 行为。模拟器要做的事,是用软件把这些硬件节奏重新拼出来。
Fame Boy 的完成度,主要体现在它不是只把画面跑起来。声音也做了,桌面端和网页端也打通了。对个人项目来说,这已经越过了“写个 CPU 解码器玩玩”的阶段。
| 读者关心的问题 | 目前能确认的事实 | 该怎么理解 |
|---|---|---|
| 发生了什么 | 作者用 F# 写了 Game Boy 模拟器 Fame Boy | 个人学习与开源项目,不是商业产品 |
| 能跑到什么程度 | 展示了 Pokémon、Tetris 等例子,支持声音、桌面端、网页端 | 可运行部分游戏,但不能推成完整兼容 |
| 为什么重要 | 作者从 NAND to Tetris、CHIP-8 一路递进到 Game Boy | 这是系统理解训练,不是单纯怀旧 |
| 谁最该看 | 模拟器、底层计算机、函数式编程爱好者;长期写业务代码的工程师 | 前者看实现取舍,后者看机器如何藏在抽象下面 |
我更看重的是它的边界设计。模拟器核心不关心前端是桌面还是网页,只通过几个窄接口交流。
| 接口 | 作用 | 价值 |
|---|---|---|
framebuffer | 输出 160×144 像素灰阶画面 | 把显示压成数据问题 |
audiobuffer | 提供 32768 Hz 环形音频缓冲 | 声音参与节奏同步,不是装饰 |
stepEmulator() | 执行一条 CPU 指令并返回周期数 | 前端靠它控制速度 |
getJoypadState() | 读取按键状态 | 输入被压成简单回调 |
这类边界很朴素,但很有工程味。核心越干净,桌面端、网页端就越不容易把模拟器本体拖乱。
内部结构也沿着 Game Boy 的机器来拆:CPU、Memory、PPU、APU、IO Controller。真实硬件是并行工作的,模拟器只能在单线程里顺序推进。于是 stepper 成了总调度器:CPU 走一步,计时器、串口、声音、图像按周期补上。
这就是模拟器项目的门槛。不是“我会写循环”,而是你得承认时间本身也是状态。
技术看点不在 F# 炫技,而在取舍
F# 的存在感,主要在 CPU 指令建模。
作者用联合类型把 512 个 opcode 抽象成 58 类指令。比如 load 指令不再是一堆硬编码编号,而是拆成 From 和 To。好处直接:很多非法状态可以提前被类型系统挡住。
但这个模型也不完美。作者提到 0x76/HALT 的问题:它在模式上像一个不存在的 Load(Indirect, Indirect),类型系统没有自然排除它,解码器实际会把它转成 HALT。
这处瑕疵反而有价值。它提醒人:类型系统能收紧边界,但不能替你理解硬件。抽象有用,前提是别把抽象当真理。
性能部分更现实。
CHIP-8 模拟器可以写得更纯函数,复制数组也还能忍。Game Boy 快得多,继续这么干就会卡在性能上。Fame Boy 为了速度大量使用 mutable,共享 VRAM 和 OAM 数组。
这不是函数式编程输了。是工程师承认机器还在那里。
作者还提到一次 Flags 重构:把 Flags 模块改成一组 inline 纯函数,减少堆分配,FPS 提升约 10%。这比口号有说服力。不是“纯”赢,也不是“脏”赢,是知道哪一层该纯,哪一层该贴近内存。
| 路线选择 | 好处 | 代价 |
|---|---|---|
| 用 F# 联合类型建模指令 | opcode 更可读,非法状态更少 | 硬件边角案例仍要手工兜底 |
| 模拟器核心与前端隔离 | 桌面端、网页端复用核心 | 接口必须足够窄,不能偷懒外泄状态 |
使用 mutable 和共享数组 | 性能更稳,更接近机器行为 | 放弃纯函数洁癖,调试纪律更重要 |
| 用 AI 生成测试用例 | 枯燥工作可加速,结果可校验 | AI 不是核心逻辑作者,不能替代理解 spec |
AI 的位置也要摆正。
原文里,AI 主要用来根据技术规格生成测试用例。作者仍然要读 spec、写实现、跑测试、修逻辑。这个用法很健康:把 AI 放在可验证、可回滚、可对照的位置。
我不太买账的是那种“AI 帮我造了一个复杂系统”的叙事。模拟器这种项目,错一个 flag、漏一个 cycle、误解一个寄存器行为,表面能跑,后面就会崩。AI 可以递扳手,不能替你摸清机器。
对开发者的影响:该学什么,该怎么用
这篇项目最相关的读者,其实只有两类。
一类是对模拟器、底层计算机、函数式编程感兴趣的开发者。你可以直接去看它的源码和网页试玩,把重点放在三处:opcode 如何建模,CPU/Memory/PPU/APU 如何分层,核心如何通过 framebuffer、audiobuffer、stepEmulator、getJoypadState 接到前端。
别急着学语法。先看边界。
另一类是长期写业务代码、想补系统理解的软件工程师。对你来说,Fame Boy 不一定是“照着复刻一个 Game Boy”。更现实的动作是:先走作者那条递进路线,NAND to Tetris,再 CHIP-8,再考虑 Game Boy。
这条路不酷,但有效。它把抽象一层层往下拆,直到你绕不开寄存器、内存、周期和像素。
这里要加一个限制:如果你的目标是找最高性能模拟器,或者找成熟工具跑 ROM,Fame Boy 不是最合适的对象。它目前更像学习样本。看它,不是为了找一个终点,而是看一个工程师怎样从简单机器爬到复杂机器。
接下来最该观察的,也不是它能不能立刻变成“全兼容”。那需要大量 ROM 测试、硬件边角行为验证、音视频同步细节和性能调优。原文没有给出这种承诺。
更值得看的是三件小事:
- 类型建模还能不能继续收紧,尤其是 HALT 这类边角案例。
- 网页端与桌面端是否继续保持核心隔离,而不是越写越互相污染。
- AI 生成测试能不能覆盖更多 spec 细节,而不是停在样例级辅助。
扯远一点,这有点像早期电气工程师拆电机。不完全一样,但重复的是同一种训练:你不能只会使用装置,还得知道力从哪里来,损耗在哪里发生。
今天的软件行业太擅长提供舒适抽象。框架替你管请求,云服务替你管机器,数据库替你管存储,AI 还试图替你管一部分代码。好处很大,代价也清楚:人越来越容易把计算机误会成 JSON、API 和配置文件。
古话说,“学然后知不足”。Fame Boy 这类项目的价值就在这里。它不靠宏大叙事打动人,只是让你重新看见:抽象下面有铁,铁下面有时钟。
所以我不把它看成怀旧玩具。它更像一次小型返工训练:用一台 1989 年的掌机,逼现代工程师重新承认,计算机不是魔法,抽象也不是免死金牌。
开头那句“不懂计算机怎么工作”,最后其实有了答案。不是多读几篇文章就懂,而是亲手把 CPU、内存、PPU、APU 接起来,看到像素,听到声音,再接受那些不漂亮但必要的取舍。
