God of War on PSP recompiled to
God of War on PSP, recompiled to WebAssembly and running in the browser
推荐理由
极具价值的底层工程实践,详细拆解了从MIPS到Wasm的转换、WebGL渲染映射及关键性能调优细节,适合对编译器、图形学与高性能前端感兴趣的开发者参考。
PSP Web Recomp:在浏览器中运行 PSP 游戏,无需模拟器。游戏的 MIPS 机器码会提前翻译为 C++,编译为 WebAssembly,并链接到对 PSP 操作系统和图形芯片的小型重新实现上,后者使用 WebGL2 进行渲染。观看完整录像(1:46)标题菜单 | 在浏览器中 | 全屏 1440×816 以这种方式打开的第一个游戏是《战神:奥林匹斯之链》。它可以从启动一直运行到菜单、过场动画和战斗场景。在笔记本电脑上的 Chrome 和 Firefox 中,目前测量的场景帧率为每秒 60 帧,分辨率最高可达 PSP 原生分辨率的四倍,并且在带有屏幕触控控制的手机上也能运行。音乐、语音和音效均正常工作。电影片段暂时跳过。《战神:斯巴达之魂》也通过了相同的脚本测试。它只需要 PSP 的 DRM 解密来处理一个小型文件,少量系统调用以及一项光照修复,无需性能优化工作:它在三倍原生分辨率下以每秒 55 到 60 帧的速度运行。此处不包含任何游戏数据。你需要提供自己拥有的游戏光盘镜像,该仓库中的脚本会将其转换为你本机上的网页。## 工作原理 重新编译。PSPRecomp 分析解密的可执行文件,找到其函数并为它们生成 C++ 代码,每个翻译单元包含 16 KiB 的客户机代码。这些代码针对寄存器文件和 PSP 内存模型运行。本项目为 PSPRecomp 添加了一些修复(在 patches/ 中)及其新的目标平台,即 profile/ 中的 web 配置。小型 PSP 内核。无论游戏向 PSP 操作系统请求什么,都由 profile/host 中的高层模拟来响应:包括具有信号量、事件标志和回调的合作线程、内存分区、文件系统(通过 HTTP Range 请求流式传输光盘数据,因此仅预先下载可执行文件)、控制器、音频输出、存档数据和消息对话框,以及电影播放器的记账功能。客户机时间按帧推进,因此无论主机运行速度多快,游戏看到的都是稳定的 60 Hz。图形处理。GE(PSP 的图形芯片)接收显示列表。ge.cpp 在 CPU 上解码这些列表,包括顶点格式、蒙皮、光照、纹理生成、裁剪和背面剔除,并将批处理的三角形和渲染状态交给 ge_gl.cpp。在那里,帧缓冲区根据其显存位置作为 WebGL 渲染目标,因此那些渲染到纹理再读回的效果可以保留在 GPU 上。像素格式重解释(游戏将 32 位缓冲区读取为 16 位纹理并返回),PSP 的 alpha 通道模板、雾效和块传输也在 GPU 上模拟,并且一切渲染速度可以达到原生 480×272 分辨率的一到四倍。拥有两个线程,就像 PSP 一样。在 PSP 上,图形芯片在处理一帧的显示列表时,CPU 正在准备下一帧,游戏也是围绕这一机制编写的。在这里,GE 及其 WebGL 上下文在 OffscreenCanvas 的工作线程上运行:排队一个列表会立即返回,只有显式的同步调用才会等待。一帧的成本取决于哪个线程更忙,而不是两者之和。声音。音效来自对 PSP 语音合成器的重新实现(32 个具有音高和 ADSR 包络的 ADPCM 声道),音乐和语音来自使用 FFmpeg 解码器解码的 ATRAC3+ 流,混合输出发送到 AudioWorklet。## 从追求速度中汲取的教训第一个可玩版本每秒仅运行 6 帧。达到 60 帧的大部分进展来自于找出时间实际消耗在哪里,而不是让渲染器变得更快。《战神》在不等待垂直空白的情况下交换其帧缓冲区,因此没有任何阻碍,游戏每向屏幕呈现一帧就绘制大约八帧。在一个空白周期内,持有交换两次的线程直到下一个空白周期才释放,这是 PPSSPP 也使用的一种技巧,仅此一项就将每显示帧的工作量减少了八倍。在 WebAssembly 中,通过 std::chrono 读取时钟会经过 clock_gettime 以及在 JavaScript 中进行的大整数转换。那些每个图元都读取时钟的分析计时器占用了约三分之一的帧时间,直到它们被修改为仅在开启分析时才读取 performance.now()。Firefox 会将每个 WebGL 缓冲区上传复制到其 GPU 进程,并且在索引缓冲区发生任何更改后,它会在下一次绘制时重新验证整个缓冲区。因此,共享的 4 MB 索引环导致 Firefox 降至每秒 3 帧;为每次绘制提供独立的小型索引缓冲区解决了这个问题。相反的做法,即逐块写入顶点数据到一个大缓冲区中,对任何浏览器都没有帮助,反而导致手机卡顿,因为移动 GPU 驱动程序在 GPU 可能仍在读取的缓冲区被修改时会等待或复制。PSP 将其模板缓冲区保存在帧缓冲区的 alpha 通道中,《战神》利用它进行投影阴影并限制模糊pass。通过全屏 pass 将模板和 alpha 相互镜像是正确的,但在 4 倍分辨率下每帧增加了 6000 万个像素的处理成本。追踪哪些矩形、哪些模板位以及哪些常量值实际发生了更改,将这一开销降至约 700 万。最后,《战神》对每一帧的显示列表进行排队,并在等待之前继续处理下一帧。让图形引擎(GE)在独立线程上运行,使得游戏加图形的繁忙战斗成本从 Firefox 中的约 17 毫秒变为两者中较大的那个,约 10 毫秒。## 第二款游戏《鬼魂斯巴达》无需更改脚本或重编译器,便通过 port.sh 从 ZIP 文件变为页面,然后在启动时永远等待。它打开一个带有 PSP DRM 标志的 176 字节文件,将其密钥交给 sceIoIoctl 并检查读取返回的内容。该文件位于 PGD 格式中,由 amctrl.prx 使用 KIRK 加密引擎解密,对于光盘游戏而言,这归结为使用来自 KIRK 密钥库的三个密钥进行的 AES-128 加密:基于 CMAC 的头校验和数据计数器模式。profile/host/pgd.cpp 实现了它。下一个问题是天空呈现白色,且菜单有相同的白色雾状效果。对一帧的绘制过程进行二分查找,发现有一层云层开启了光照,其不透明度来自全局环境光的 alpha 值,而光照代码遗漏了这一因素。性能方面无需额外工作:每帧耗时 6 到 8 毫秒,与《奥林匹斯之链》相同。## 移植你自己的游戏你需要 git、CMake、Ninja、C++20 编译器和 Python 3。所有内容均在 Linux 上运行过;macOS 应能构建浏览器版本但未经验证,而本地测试运行程序需要 EGL 和 OpenGL ES 头文件(Debian 和 Ubuntu 上的 libegl-dev 和 libgles-dev)。预计每个游戏的提取光盘、生成的代码和构建物大约占用 2 GB 磁盘空间。``` git clone https://github.com/snuri00/psp-web-recomp.git cd psp-web-recomp scripts/setup.sh # PSPRecomp、补丁和 Emscripten SDK PSP_DECRYPT=/path/to/decrypter scripts/port.sh mygame "My Game.iso" scripts/serve.sh mygame # http://localhost:8613/ ``` port.sh 提取光盘(ISO、包含 ISO 的 ZIP 文件或已提取的文件夹),解密 PSP_GAME/SYSDIR/EBOOT.BIN,将其转换为 C++,编写用于流式传输光盘其余部分的清单,并构建页面。大约需要几分钟;在8核笔记本上,God of War 从 ZIP 包到可玩页面只需约四分钟。添加 --native 参数还可以构建一个无头运行器,它能将帧转储为图像并录制音频,这是查看新游戏进展的最快方式。零售光盘上的可执行文件是加密的。PSP_DECRYPT 指代任何被调用为工具的工具,并输出明文 ELF 文件,例如 DecEboot 或 pspdecrypt。PPSSPP 也可以在运行游戏时转储解密后的可执行文件(设置、工具、开发者工具)。
更进一步:量化金融体系
看懂新闻只是起点——沿量化金融路径,把它变成能交付的工程能力