demake:一份源码工程编译出任意复古主机的 ROM
文章摘要
demake 是英国伦敦的技术主管 George Stephenson(HN 用户名 gste)的业余项目,目标听上去有点狂:写一份源码工程,编译出几乎所有主流 8/16 位时代游戏机和掌机能真正跑起来的 ROM。项目遵循 MIT 协议开源,同时提供网页应用、命令行工具和 NPM 包,网页版完全在浏览器本地运行,不上传任何素材。
按作者在 Show HN 里的自述,这个项目最初并不是奔着”跨主机游戏引擎”去的,而是为解决一个很具体的图形问题:生成式 AI 已经能画出复古风格的像素精灵图,但它没法遵守真实硬件在像素与颜色上的硬约束——主调色板、每格图块的颜色数上限、属性网格、图块预算等等。demake 要补的就是这块缺口,让 AI Agent 能端到端地做一款复古游戏:生成美术素材、把它转成符合硬件规格的数据和显示代码、再构建出一个能启动的 ROM。做着做着作者发现,既然规格是数据驱动的,那就可以”扇出”到任意主机和掌机的规格上去。
项目按输入类型分成四个”demaker”:art 吃任意图片,产出合规美术、调色板、图块映射、汇编/C/二进制数据乃至可启动 ROM;game 吃一份 Demotic 脚本加美术,产出”一款游戏、每台主机各一份”;music 吃 MIDI 轨道,产出芯片音乐;sound 吃 WAV 音效,产出经过排布和优先级安排的芯片音效。四者共用同一套引擎、同一份确定性保证,以及同一种验证方式——真实 ROM 在真实模拟器里启动,与工具声称的输出逐像素、逐 tick、逐寄存器写入地比对。目前 prep 和 inspect 覆盖从 Game Boy 到任天堂 DS 的 21 台机器,--format rom 的逐像素模拟器验证覆盖十余台,其中 Virtual Boy 是双眼都验证(它的显示是两组 LED 阵列,视频处理器把每一帧按场景声明的景深画两遍)。
真正的核心创新是作者自创的声明式语言 Demotic(.dmt 文件)。名字取自罗塞塔石碑上的”世俗体”——同一段文本以圣书体、希腊文和世俗体三种书写并存,Demotic 也要把同一款游戏带到每台主机上,而且是用人(或者模型)真正写得出来的语言层级。它的中心原则是一条铁律:Demotic 描述游戏,Demakefile 描述构建。.dmt 文件里不出现任何主机名、任何调色板、任何像素;游戏中的距离和速度全部用相对单位表达(vw、vh、vmin、vmax),这正是它能跨屏幕尺寸移植的原因。作者在 CI 里把这条原则做成了可验证的属性:删掉 Demakefile 游戏必须玩法完全一致,只是产物变了;Demakefile 里的任何东西都不能改变玩法。
实现层面有三个关键决策。其一是”约束地模拟,自由地渲染”:游戏状态一律以 16.16 定点数、按固定逻辑 tick 推进,浏览器预览和真实硬件上完全一致,只有渲染是自由的(SVG 美术、任意分辨率、tick 之间插值)。因此预览不是第二套会漂移的实现,预览本身就是规格。代价是刻意的:模拟里没有浮点数、没有墙上时钟、没有宿主随机数(random 走语言自己的带种子生成器),全局只有一条取整规则——向下取整,也就是 6502 或 Z80 上算术右移的行为。其二是先把语言前端编译成一个中间的 Program(场景、对象、控制、规则的解析表,常量全部折叠成定点数),再由后端把 Program 编译成目标机器的原生代码。作者坦承这推翻了文档里更早的一个决定——原本打算让 Program 本身就是交付物,每台主机配一个小型固定引擎去解释它,理由是 N+M 的工作量优于 N×M。这个账算工作量是对的,算机器是错的:解释器每条规则、每个对象、每个 tick 都要取规则记录、按触发器分发、用栈式求值器走一遍表达式树、再通过指针寻址每个属性,光一个 Pong 就要吃掉大约三个 Game Boy 帧。其三是 .test.dmt 测试套件,能一次性断言游戏在每台主机上都还玩得对。
作者刻意把 Demotic 和 Demakefile 都设计得又小又扁平、按行组织,正是为了让模型能可靠地写和改。目前 demake build pong.dmt -o pong.gb 能把游戏编译成 32KiB 的 Game Boy 卡带机器码,顺手把它引用的美术也 demake 掉;换成 -c nes、-c sms、-c gg、-c md 就编译到 6502、Z80 和 68000。构建甚至不需要你装汇编器,因为汇编器是项目自带的——这也是网页版能在页面里直接编译并玩到与命令行完全相同字节的原因。Demotic 明确划定了范围:11 台带多色硬件精灵和可比按键集的图块机器。被刻意排除的机器各有各的理由——SG-1000/TMS9918 每扫描线只有 4 个精灵且每精灵单色,没有滚动寄存器所以摄像机编译不出来,1KB 工作 RAM 也顶不住游戏需要的 700–950 字节;Supervision、Game.com、Lynx 这类纯帧缓冲掌机根本没有硬件精灵;Neo Geo 没有图块背景层,它的背景就是精灵;Atari 2600、Virtual Boy、Intellivision 则各自以不同方式打破模型。作者对此的表述很诚实:所谓”每一台主机”实际形状是若干 profile,而不是一个最小公分母。
HN 评论精华
这条 Show HN 拿到 40 分,讨论量不大但含金量高,作者本人在串里逐条回应。
-
bensyverson 给出了最有价值的一条建议。他很喜欢”用一种 LLM 容易理解的语言声明式地描述游戏”这个思路,追问还有没有别的 demo 能展示语言的表达力,比如能不能做平台跳跃或冒险类游戏。更关键的是他提出了设备能力守卫(capability guard)的需求:同一档次里 SNES 和 NES 的能力差距、Game Boy 和 SNES 能做的事差距都足够大,应该有办法表达”更强的机器多画点东西”,甚至可以做成动态的,比如”敌人数量 = 该机器支持的精灵数 - 2”。作者回应说更多示例其实在屏幕底部那条栏里(他也承认这不够显眼),并表示自己有一个 Demakefile 的概念用于处理实现层面的差异,比如让美术 demaker 针对特定平台采用特定画风。但他坦率指出这块是灰色地带:这类”强机器多画敌人”的逻辑理论上必须写进声明式脚本 Demotic 里,那就得在语言层面区分主机的强弱等级,而目前的设计是一份脚本通吃所有机器。
-
benj111 提了一个很扎实的技术问题:不同屏幕尺寸下怎么避免舍入误差?同一个东西在一个版本里是 1 像素,在另一个版本里可能是 2×2 或 1×2。他还顺带吐槽了 Pong demo 的方向——网(net)的画法暗示球该左右运动,实际却是上下运动。
-
Dwedit 报了一个真实 bug:输入处理有问题,左右键的某些按下/松开组合会让球拍卡住持续向一个方向移动,同时按住左右键时尤其明显。作者确认自己也复现了,表示会尽快发修复。
-
andai 分享了一篇关于边缘感知像素化(edge-aware pixelation)的文章,认为用在 AI 生成的像素画上会得到更好的结果。他还抛出一个更普遍的问题:网上大量像素画其实是被放大过的,有没有好办法还原回原始分辨率?理论上只要采样每个色块的中心即可,但他没找到趁手的传统图像编辑器方案。
-
anthk 从 Lisp 角度提出替代路线:这套语法可以轻松映射到 Common Lisp 或 Scheme,宏能干掉九成的工作;他还提到 Common Lisp 有个叫 clog 的 Web 开发包。他的核心观点是——如果游戏能像 GB Studio 那样被足够结构化地表达,那根本不需要 AI 生成。他另外发了一条纯属”许愿”的评论,想看用生化危机式固定视角引擎(或者更接近的《寄生前夜》引擎)做的《心灵杀手》PSX demake,理由是 PS1 从《鬼屋魔影:新噩梦》起就有手电筒光效,固定摄像机映射的场景足以捕捉环境氛围,过场动画直接上 WAV/MPEG 视频就行。