Hotdry.
systems

在蓝牙 dongle 上运行 Doom:嵌入式移植的内存与渲染管线裁剪

分析在 nRF52840 蓝牙芯片上移植 Doom 的内存约束与渲染管线优化策略,涵盖 QSPI 访问优化、双缓冲与 DMA协同、纹理缓存策略等关键技术决策。

「如果一个设备能运行 Doom,那就说明它有足够的计算能力。」这句在嵌入式社区流传的玩笑话最近又有了新的验证案例:将经典的第一人称射击游戏 Doom 移植到售价仅 13 美元的蓝牙低功耗 USB 适配器上。这个适配器内部使用的正是 Nordic Semiconductor 的 nRF52840 微控制器,一款专为物联网设备设计的射频芯片。本文将深入分析这次移植中遇到的硬件约束,以及为达成可游玩帧率而采取的关键优化策略。

硬件约束:64 MHz 与 256 KB 的极限挑战

nRF52840 是一款基于 ARM Cortex-M4 的微控制器,主频 64 MHz,内置 256 KB SRAM 和 1 MB 闪存。从数字上看,这颗芯片的算力约为 80 DMIPS(1.25 DMIPS/MHz),内存容量只有原版 Doom 运行时代主流 PC 的四分之一到八分之一。更关键的是,游戏资源(WAD 文件)无法全部装入内部存储 —— 原始商业版 Doom 的 WAD 文件经过转换后超过 5 MB,必须存放于外部 QSPI 闪存中。这意味着渲染管线必须频繁地从外部存储读取纹理数据,而 QSPI 接口的随机访问延迟高达 4.5 微秒,远高于内部 RAM 的单周期访问。

原始的 Nordic 工程师移植尝试仅达到 3-5 FPS,原因是直接通过 SD 卡读取 WAD 文件,并使用了大量占位纹理来规避性能问题。本次移植设定了更高的目标:在 240×240 分辨率下达到平均 20 FPS 以上,最终实际稳定在 30-34.5 FPS。要达成这一目标,必须对渲染管线的每一个环节进行精细的优化。

QSPI 访问优化:避免随机读取的代价

外部 QSPI 闪存的峰值传输速率为 16 MB/s,但这仅在连续读取时才能达到。随机读取(即设置新地址后的首次访问)需要约 4.5 微秒的延迟,对于需要频繁读取不同纹理列的 Doom 渲染管线来说是致命的。Doom 的 3D 渲染按列进行,墙壁和贴图的每一列都需要从存储中读取像素数据,如果每次列读取都触发随机访问,整体帧率将惨不忍睹。

解决方案是将整列数据预先读取到内部 RAM 缓冲区,然后从 RAM 进行随机访问绘制。具体实现中,使用 DMA 进行 QSPI 到 RAM 的连续读取,同时 CPU 执行前一次已加载列的绘制工作,形成流水线化处理。此外,还对 WAD 转换工具进行了修改,在 patch 数据中嵌入每列的字节长度信息,使 DMA 能够一次性抓取完整列数据而无需 CPU 介入解析。这种「预读取 - 缓存 - 绘制」的三阶段流水线将 QSPI 的高延迟隐藏在计算时间之内。

更进一步,项目将大量不随关卡变化的静态数据(如颜色映射表、调色板、贴图定义)缓存到内部闪存。内部闪存的随机读取延迟仅为 4 个 CPU 周期(2 个等待状态),远优于 QSPI 的随机访问性能。关卡加载时,系统会尽可能多地将墙面贴图缓存到内部闪存,剩余无法容纳的贴图则继续使用外部 QSPI,但通过上述的 DMA 预读取机制保持性能。这种分层缓存策略使得 1 MB 内部闪存的利用率达到 90% 以上。

双缓冲与 DMA 协同:隐藏显示传输时间

240×240 分辨率、16 位色的帧缓冲大小为 115,200 字节。ST7789 显示控制器通过 SPI 接口通信,最大时钟频率 32 MHz,传输一帧完整数据需要约 29 毫秒,这意味着理论最大帧率只有 34.5 FPS。更糟糕的是,SPI 传输期间 CPU 如果不能做其他工作,帧率还将进一步下降。

项目实现了完整的双缓冲机制:两个 115,200 字节的帧缓冲交替使用,一个用于显示输出(前端缓冲),另一个用于渲染下一帧(后端缓冲)。当 DMA 将前端缓冲的数据推送到 SPI 传输时,CPU 完全专注于后端缓冲的 3D 渲染计算。由于 SPI 传输需要 29 毫秒,而大多数场景的渲染时间低于这一阈值,CPU 渲染完成后只需等待 DMA 结束即可交换缓冲角色,整个过程没有空闲周期浪费。

颜色格式转换是另一个隐藏的性能瓶颈。Doom 使用 8 位索引颜色(256 色),而 ST7789 需要 16 位 RGB565 格式。传统做法是逐像素转换后写入显示缓冲,但这样每个像素都需要一次查表操作和一次写操作,耗时约 2200 CPU 周期每条扫描线。优化后的方案将 240 像素宽的行拆分为 15 个 16 像素的「切片」,使用两个 512 字节的子缓冲交替工作:当 DMA 发送一个子缓冲时,CPU 填充另一个子缓冲,切片级别的双缓冲使得 8 到 16 位转换的 3.5 毫秒开销完全被 SPI 传输的 29 毫秒所覆盖。经过汇编级别的流水线优化后,转换每个切片仅需约 1000 周期,比原始实现快了一倍多。

内存优化:16 位指针与结构体重排

nRF52840 只有 256 KB RAM,而原始 Doom 的内存需求远超这一限制。项目继承了此前移植工作的内存优化成果,并针对新硬件进行了调整。

最关键的优化是将 32 位指针替换为 16 位指针。由于 RAM 空间只有 256 KB,任何对象的地址都可以用 18 位表示,而由于所有数据结构都是 4 字节对齐的,低两位永远为零,因此 16 位偏移量加上基地址的方案完全可行。设置和读取数据需要额外的地址转换周期,但 64 MHz 的 Cortex-M4 处理这种转换绰绰有余,而节省下来的 4 字节每指针在拥有数百个游戏对象时累积起来就是可观的节省。

结构体成员重排同样贡献了显著的内存节省。 Doom 中巨大的 mobj_t 结构体(游戏对象)在 GBA 移植版中为 140 字节,经过成员重排、移除冗余字段、使用位域等优化后压缩到 92 字节。此外,项目引入了静态 mobj 类型用于无需运动逻辑的对象(如物品和装饰物),将内存占用进一步减半。在 E1M6 等复杂关卡中,游戏对象数量超过 460 个,仅这一项优化就节省了超过 30 KB RAM。

内存池(Memory Pool)替代传统的动态分配也大幅降低了内存碎片和元数据开销。传统的 malloc/free 需要为每个内存块存储 8 字节的元数据,而内存池将元数据压缩到 1 字节每对象。在游戏对象和临时节点的分配中使用预分配的池,使得每对象的额外开销从 27 字节降低到 1 字节。

复合贴图预转换:运行时合成的代价

Doom 的墙面贴图经常由多个「补丁」(patch)组合而成,这种设计允许在不同贴图中复用相同的图形元素,节省存储空间,但代价是运行时需要从多个补丁中读取数据并合成最终列。复合贴图的渲染需要在每次绘制时进行多次随机读取和像素混合,对于追求极致性能的嵌入式平台来说是不可接受的。

项目采取的策略是在 WAD 转换阶段预先将所有复合贴图渲染为单一的大型贴图。转换工具会遍历每个多补丁贴图,将其绘制到一个临时缓冲区,然后将结果作为新的单一补丁写回 WAD。这种预处理使运行时不再需要任何复合贴图合成逻辑,每列贴图数据都可以作为整体连续读取。代价是 WAD 文件大小增加约 1.2 MB(商业版 Doom 从 4.2 MB 增长到 5.4 MB),但这是值得的 —— 预转换消除的运行时开销远大于额外传输时间的损失。

音频与游戏手柄:无线方案的设计权衡

音频系统的实现采用了与渲染类似的流水线思路。 Doom 的音效采样率为 11,025 Hz,单声道 8 位分辨率。项目使用 1024 样本的环形缓冲,updateSound 函数每帧调用一次,计算接下来 924 个样本(预留 100 样本作为安全边界)。8 个音频通道的混合计算在 3.4 毫秒内完成,这要求帧率至少维持在 12 FPS 以上 —— 对于优化后的系统来说这从未成为问题。

有趣的是,为了节省蓝牙 dongle 有限的 GPIO 引脚,音频输出和游戏手柄控制被设计为无线方案。dongle 端运行 Doom 的 nRF52840 通过自定义射频协议向另一个基于 nRF51822 的无线手柄发送音频数据和接收按键状态。手柄端负责将音频数据输出到 PWM 扬声器,同时轮询 8 个按键并回传状态。两个 nRF52 芯片之间的无线通信占用约 4.5 毫秒每帧,但由于采用 DMA 传输,CPU 干预极少。音频数据包的发送间隔约 4.5 毫秒,包含 52 个采样和 2 个用于校验的冗余字节,这种设计容忍单个比特的翻转错误而不会产生可察觉的音频卡顿。

性能对比与工程启示

经过上述优化的 Doom 移植在 nRF52840 上的实际表现如下:E1M1 等简单场景稳定在 34.5 FPS(SPI 硬件限制),E1M2 等复杂场景维持在 30 FPS 以上,即使是最复杂的 E4M2 也能达到 22 FPS。相比之下,同一款芯片上的 Nordic 官方移植仅有 3-5 FPS,而基于 GBA 的移植(约 240×160 分辨率,ARM7 16.78 MHz)在同等场景下约 16 FPS。换算到每像素的效率,这次移植实现了约 2.8 倍的提升。

从工程角度看,这个项目展示了嵌入式系统性能优化的几个核心原则。其一,存储层次结构的利用比绝对性能更重要 —— 将频繁访问的数据放在内部闪存而非外部 QSPI,能够获得数量级的性能提升。其二,流水线化是隐藏延迟的关键 ——DMA 预读取隐藏了 QSPI 延迟,双缓冲隐藏了 SPI 传输延迟,只要找到可以并行的阶段,性能就会显著改善。其三,预处理的代价往往是值得的 ——WAD 转换增加了文件大小和上传时间,但换来了运行时性能的质的飞跃。最后,约束往往是创新的催化剂 —— 有限的 GPIO 迫使设计者采用无线方案,最终不仅解决了引脚问题,还意外地实现了更优雅的系统分区。

这个 13 美元的蓝牙适配器最终运行的 Doom,比 1993 年售价 1500 美元的主流 PC 还要流畅。这既是半导体技术进步的明证,也是软件工程智慧的体现:在任何平台上,通过深入理解硬件特性和精心打磨算法,都能榨取出超乎预期的性能。

资料来源:本文技术细节参考 next-hack.com 的 nRF52840 Doom 移植项目,该项目完整源码和硬件设计已在 GitHub 开源发布。

查看归档