GitHub 今日推荐|ps2-web-recomp:浏览器里跑 PS2 游戏的静态重编译方案

GitHub 今日推荐|ps2-web-recomp:浏览器里跑 PS2 游戏的静态重编译方案

一句话看懂

项目地址:github.com/snuri00/ps2...

ps2-web-recomp 把 PlayStation 2 游戏的机器码提前翻译成 C++ 再编译为 WebAssembly,让游戏在浏览器运行时绕过解释执行的性能损耗。《GT赛车4》在 Chrome 中以 50fps 运行,《战神2》原生分辨率达到 60fps,双倍分辨率稳定在 48-60fps。游戏数据通过 HTTP Range 按需读取,初次加载只下载可执行文件,其余资源延迟获取。项目代码 84.8 万字节 C++ 加 3.5 万字节 WGSL,现有 853 星,2024 年 10 月创建。

它解决什么问题

传统 PS2 模拟器在浏览器中跑时,每次游戏读取一条指令都要解释执行,CPU 开销大。静态重编译把游戏的 Emotion Engine 机器码事先翻译成 C++ 函数,编译为 WebAssembly 后浏览器直接执行原生指令。这样避开了运行时解释循环,性能从首次优化前的 3fps 跃升至约 70fps。Graphics Synthesizer 的 4MB 显存和光栅化用 WebGPU 计算着色器实现,把 GPU 派遣次数从每帧 650 次降到 80 次,减少 CPU 与 GPU 同步开销。

PS2 硬件包含两颗 CPU(EE 和 IOP)、两个向量单元(VU0、VU1)和专用 GPU,传统模拟器需要在运行时调度这些组件的状态切换。本项目把 EE 和 VU1 微程序都预编译,IOP 驱动用 C++ 重写,运行时只需维护硬件状态和 DMA 传输,省去控制流分析的循环。

核心概念速览

静态重编译:游戏机器码预先翻译成目标平台代码。在本项目中,EE 指令翻译为 C++ 函数,以 16KiB 为单位分块,每块对应一段连续的游戏逻辑。

Emotion Engine:PS2 主 CPU,采用 R5900 架构,有 128 位寄存器和分支延迟槽。重编译器保留这些硬件特性,生成的 C++ 代码逐条模拟寄存器操作和延迟槽行为。

Vector Unit:PS2 的 SIMD 协处理器,做 3D 运算。VU1 执行游戏上传的微程序,项目将其编译为 SIMD C++,模拟乘加累加器的流水线延迟和状态标志。

Graphics Synthesizer:PS2 的 GPU,有 4MB 显存,支持瓦片渲染和可编程混合。项目用 WebGPU 计算着色器实现显存、瓦片光栅化和混合逻辑,避开传统图形 API 的限制。

架构拆解

项目分五个模块:

recomp :读取游戏的 ELF 可执行文件,跟踪控制流,识别函数和分支目标,生成 C++ 代码。recomp/main.cpp 解析 ELF 找到入口点,recomp/analysis.cpp 扫描跳转和调用指令建立控制流图,recomp/codegen.cpp 将 R5900 指令翻译为 C++ 语句。

runtime :实现 EE 内核(线程、信号量、DMA、垂直同步)、IOP 模拟器(R3000 解释执行和 C++ 重写的驱动)、GS 渲染、VU 执行。runtime/src/gs/gpu/gpu.cpp 管理 WebGPU 渲染管线,装箱瓦片和派遣光栅化任务。

host :分为原生窗口(host/native.cpp)和 Web 平台(host/web.cpp)两个后端。前者用 SDL2 和 wgpu-native 创建窗口,后者用 Emscripten 绑定浏览器的 WebGPU 接口。两者提供统一的主循环和设备接口给 runtime。

scripts :自动化移植流程。scripts/port.sh 调用 eerecomp 翻译游戏,无头运行游戏记录 VU 微程序,生成 Web 页面和原生可执行文件。scripts/build_web.sh 用 Emscripten 编译 C++ 为 WebAssembly。

gs/gpu:WebGPU 渲染管线和批次管理。编译 WGSL 着色器实现瓦片光栅化,管理纹理上传和派遣队列。纹理上传提前到不依赖的三角形之前,减少批次分割。

数据流向:scripts 调用 recomp 翻译游戏 → recomp 生成调用 runtime API 的 C++ → runtime 通过 host 层访问 GPU 和音频 → host/web.cpp 提交计算管线和渲染队列到浏览器 WebGPU → gs/gpu 编译瓦片光栅化着色器派遣到 GPU。

关键实现走读

代码生成器需要将 R5900 寄存器编号映射为 C++ 表达式,这段辅助函数展示了转换规则:

cpp 复制代码
std::string hex(std::uint32_t v) { return std::format("0x{:08X}u", v); }
std::string label(std::uint32_t v) { return std::format("L_{:08X}", v); }
std::string w(std::uint32_t r) { return r ? std::format("c.r[{}].w[0]", r) : "0u"; }
std::string d(std::uint32_t r) { return r ? std::format("c.r[{}].d[0]", r) : "0ull"; }
std::string sd(std::uint32_t r) { return r ? std::format("c.r[{}].sd[0]", r) : "0ll"; }
std::string reg(std::uint32_t r) { return std::format("c.r[{}]", r); }
std::string f(std::uint32_t r) { return std::format("c.f[{}]", r); }
std::string vf(std::uint32_t r) { return std::format("c.vu0.vf[{}]", r); }
std::string vi(std::uint32_t r) { return std::format("c.vu0.vi[{}]", r); }

做了什么: w 访问 32 位字,d 访问 64 位双字,sd 访问有符号双字,reg 访问完整 128 位寄存器。vf 和 vi 访问 VU0 的向量浮点和整数寄存器。label 把地址转为跳转标签,hex 格式化立即数。

**为什么这样写:**R5900 寄存器 0 始终为零,函数检查寄存器号,返回字面量零避免生成无用的读写。EE 寄存器是 128 位的,访问时需指定宽度(字、双字、四字),函数名反映了这些宽度。

**没有它会怎样:**生成的 C++ 代码会大量出现 c.r[0] 的读写,编译器虽能优化但会增加代码体积。标签和立即数格式不统一会导致生成的代码难以阅读和调试。

VU 寄存器有特殊处理规则,累加器和寄存器零的行为与通用寄存器不同:

cpp 复制代码
std::string vf(unsigned i, int lane_index) {
    if (!i) return lane_index == 3 ? "0x3F800000u" : "0u";
    return std::format("r.vf[{}].w[{}]", i, lane_index);
}

std::string vreg(unsigned i) { return i ? std::format("vload(r.vf[{}])", i) : "v4u{0u, 0u, 0u, 0x3F800000u}"; }

std::string shadow(int reg) { return reg == kAcc ? "nacc" : std::format("n{}", reg); }
std::string raw(int reg) { return reg == kAcc ? "r.acc" : std::format("r.vf[{}]", reg); }

做了什么: vf 单独访问向量的一个通道,寄存器零的前三通道返回 0,第四通道(w 分量)返回 1.0 的浮点表示 0x3F800000。vreg 加载完整向量,寄存器零返回 (0, 0, 0, 1)。shadow 和 raw 区分流水线影子寄存器和实际寄存器,累加器用独立变量存储。

**为什么这样写:**PS2 的向量单元约定寄存器零是 (0, 0, 0, 1),w 分量为 1 简化齐次坐标运算。VU 流水线有多周期延迟,指令写入的值要几个周期后才对后续指令可见,影子寄存器保存尚未提交的值。累加器是单独的硬件资源,不在寄存器文件中。

**没有它会怎样:**不返回特殊值会导致矩阵乘法和投影计算错误。不区分影子和实际寄存器会违背 VU 的流水线时序,游戏依赖精确时序的代码会崩溃或渲染错误。

动手上手

克隆仓库:

bash 复制代码
git clone https://github.com/snuri00/ps2-web-recomp.git
cd ps2-web-recomp

移植游戏:

bash 复制代码
scripts/port.sh mygame "My Game.iso"

这会在 build/web-mygame/ 生成 WebAssembly 和 HTML 文件,在 games/mygame/ 生成配置。过程中脚本会无头运行游戏,按下虚拟手柄的 start 和 cross 按钮收集 VU1 微程序。

**验证结果:**检查 build/web-mygame/index.html 是否存在,games/mygame/ 目录下应有 vu_programs/ 文件夹存放编译后的 VU 代码。

启动 Web 服务器:

bash 复制代码
python3 scripts/serve.py build/web-mygame games/mygame/disc.iso

**验证结果:**终端显示监听端口(默认 8000),浏览器访问 http://localhost:8000 应能看到游戏加载界面。Linux 用户需在 Chrome 启动时加 --enable-unsafe-webgpu 标志。

原生运行(可选):

**验证结果:**SDL2 窗口弹出,游戏画面渲染。需要 SDL2 开发文件和 wgpu-native 库。

应用场景

移植新游戏 :提供合法的 ISO 镜像,运行 port.sh 生成网页。脚本探索游戏启动流程,记录所有 VU1 微程序后编译为 C++。可能遇到未实现的系统调用,需修改 runtime 补充功能。

浏览器游玩 :启用 WebGPU 的 Chrome 或 Edge 访问生成的页面。Linux 平台需手动开启 unsafe-webgpu 标志,macOS 和 Windows 在较新版本浏览器中默认支持。集成 GPU 上双倍分辨率帧率会下降,独立显卡表现更好。

原生运行 :用 --native-only 构建,游戏在 SDL2 窗口播放,适合开发调试。原生版本可使用系统的 GPU 驱动,避开浏览器的沙箱限制,性能略高于 Web 版本。

按需加载:游戏镜像通过 HTTP Range 请求分段读取。初次加载只下载几 MB 的可执行文件,进入关卡后才读取场景数据。《GT赛车4》每帧上传 3MB 纹理,缓存策略减少 60% 数据传输。

独立分析

嵌套 setTimeout 被浏览器限流后帧率从 40-50fps 波动,改用 MessageChannel 稳定到 50fps。这是独立分析的结论:浏览器对定时器有最小间隔限制,MessageChannel 的消息队列绕过了这个限制,每帧的调度延迟更低。

分支位于 16KiB 单元末尾时,延迟槽可能被执行两次。《战神2》触发了这个重编译器 bug,表现为角色跳跃高度异常。独立分析:单元边界的跳转需要跨越到下一个单元,延迟槽指令在当前单元末尾执行一次,进入目标单元时又执行一次。修复方法是检测边界跳转,生成代码时显式标记延迟槽已执行。

纹理上传提前到不依赖的三角形之前,GPU 派遣次数从每帧 650 次降到 80 次。独立分析:GS 的显存写入和渲染命令交错时,每次写入会打断当前批次。提前上传纹理后,连续的渲染命令可以合并为一个派遣,减少 CPU 与 GPU 同步的开销。

与 PCSX2 比较:PCSX2 解释执行主 CPU,本项目静态重编译,性能高但移植每款游戏需要额外步骤。记忆卡格式兼容,可用 PCSX2 的存档。与 Play! 比较:IPU 的 VLC 表改编自 Play!,但本项目用 WebGPU 计算着色器实现 GS,Play! 用软件光栅化。姊妹项目 psp-web-recomp 采用相同方法移植 PSP 游戏。

局限与风险

游戏数据未包含,需要自备合法的 PS2 游戏镜像。项目不提供游戏文件,用户需拥有实体光盘或数字购买凭证。

WebGPU 兼容性受限。Chrome 和 Edge 在 Windows、macOS、部分 Linux 发行版上支持,Safari 尚未完整实现计算着色器,Firefox 需手动启用实验性标志。

某些 IOP 模块或 GS 特性可能未实现。PS2 硬件复杂,部分游戏依赖的外设、音频编解码、网络适配器功能尚未覆盖。遇到未知系统调用时游戏会卡住或崩溃。

集成 GPU 上双倍或三倍分辨率帧率下降。WebGPU 计算着色器需要大量内存带宽,集成显卡与 CPU 共享内存,高分辨率时带宽不足导致帧率低于 30fps。独立显卡表现更好。

记忆卡操作需要浏览器的持久化存储权限。IndexedDB 配额不足时存档可能丢失,用户需手动导出存档文件备份。

结论卡片

**适合:**拥有合法 PS2 游戏的玩家、静态重编译研究者、WebGPU 开发者。《GT赛车4》和《战神2》已验证可玩,其他游戏需要试错和补充 runtime 功能。

**不适合:**无游戏拷贝者、Safari 用户、期望所有游戏即插即用者。项目需要每款游戏单独移植,自动化脚本无法覆盖所有硬件特性。

**关注方向:**更多游戏的移植记录会在仓库 issues 中更新,IOP 模块和 GS 渲染特性的实现进度决定兼容性范围。WebGPU 标准的浏览器采用率上升后,Linux 和移动平台的可用性会改善。


项目地址:github.com/snuri00/ps2...

原文地址:tools.wflynn.cn/aitools/git...

关注公众号「YourTools」,每天推送 GitHub 趋势与开源工具速报:

相关推荐
技术不好的崎鸣同学1 小时前
第七章:模块踩踏以覆写导出函数绕过CFG
c++·windows·安全·系统安全
特立独行的猫a1 小时前
我用仓颉写了个「仓颉版 Tauri」,又拿它写了个 Markdown 软件
开源·仓颉·cj-tauri·markdown软件
沫璃染墨1 小时前
从零入门计算机网络系列(五):UDP套接字编程实战——从核心API到客户端与服务器通信
linux·网络·c++·vscode·网络协议·udp
独孤九剑打醒他1 小时前
【原创开源·航空篇】太阳能水闭环氢能航空:双燃料掺氢助燃与露天储氢安全模型
架构·开源·汽车·硬件工程·能源
阿里嘎多学长1 小时前
2026-10-13 GitHub 热点项目精选
开发语言·程序员·github·代码托管
hetao17338372 小时前
联盟场-提高组-R4 & ZYZSC-S-Round 7
c++·算法
hetao17338372 小时前
联盟场-提高组-R2 & ZYZSC-S-Round 5
c++·算法
OxYGC3 小时前
[玩转大模型] 第十一篇:售卖自己的 Skill——买断、订阅、定制、开源+付费增强,四条路到底哪条能收到钱
开源·大模型·agent·变现·skill
X北辰北3 小时前
C++ 进阶第二阶段总结|STL容器全家桶、迭代器失效、STL算法、序列化入门(工程必备)
开发语言·c++·算法