PCSX2 VU向量单元实现:microVU编译器从IR分析到SSE生成的完整指南
【免费下载链接】pcsx2PCSX2 - The Playstation 2 Emulator项目地址: https://gitcode.com/gh_mirrors/pcsx24/pcsx2
PCSX2 是一款功能强大的 PS2(PlayStation 2)模拟器,其核心难题之一是模拟主机上两颗独特的协处理器——VU 向量单元(VU0/VU1)。PCSX2 采用自研的microVU 动态重编译器,通过 IR(中间表示)分析、流水线建模与 SSE 指令生成,把 VU 微程序实时编译成高速 x86 代码。本文将带你完整解读这条从微码到机器码的编译流水线。
🎮 先搞清楚:VU 是什么,为什么难模拟
PS2 的 EE 主处理器负责通用计算,而图形几何、物理特效等大量并行运算被卸载到两颗 VU 协处理器上。VU 的工作方式很特别:
- 微程序执行:游戏把一小段"微码"(microProgram)写入 VU 内存,VU 从
start_pc开始逐条执行,直到遇到T-bit终止位才交还控制权; - 每周期双指令槽:一条 64 位 VU 指令由"上指令(Upper)"和"下指令(Lower)"组成,分别走浮点管道(VF 寄存器)和整数/分支管道(VI 寄存器);
- 深流水线:除法(Q 寄存器)、EFU 指数运算(P 寄存器)等需要 4~6 个周期才出结果,指令间存在复杂的重排与停顿(stall);
- 状态标志延迟生效:Status / Mac / Clip 三类标志在流水线中最多存在4 个不同版本,跨块传递极易出错。
直接逐周期模拟(解释器)会让游戏跑在龟速,因此 PCSX2 选择JIT 重编译路线。整个 microVU 编译器位于pcsx2/x86/目录,由 microVU.h 末尾的 13 个.inl文件按固定顺序拼成单一编译单元实现:
microVU_Clamp.inl → SSE 浮点钳位 microVU_Analyze.inl → Pass 1:IR 分析 microVU_Alloc.inl → 寄存器分配 microVU_Upper.inl → 上指令 SSE 生成 microVU_Lower.inl → 下指令 SSE 生成 microVU_Compile.inl → 块编译主循环 microVU_Execute.inl → 分发与执行入口 ...这种"巨型头文件拼装"的写法让所有编译路径能共享全局状态(mVUinfo、mVUregs等宏别名,定义在pcsx2/x86/microVU_Misc.h),也解释了为什么该目录文件互相"看不见"却紧密咬合。
⚙️ 编译流水线总览:一条指令的 4 个 Pass
microVU 对每条 VU 指令走 4 个逻辑 Pass(microVU_Misc.h中用pass1~pass4宏定义):
| Pass | 名称 | 作用 | 核心文件 |
|---|---|---|---|
| 0 | Analyze(分析) | 构建 IR:读写寄存器、停顿周期、分支信息 | microVU_Analyze.inl |
| 1 | Recompile(重编译) | 把 IR 翻译为 SSE/x86 指令 | microVU_Upper.inl/microVU_Lower.inl |
| 2 | Logging(日志) | 开发构建下 dump 微程序汇编 | microVU_Log.inl |
| 3 | Flags(标志) | 计算标志寄存器实例的读取/写入版本 | microVU_Flags.inl |
编译的最小单位是microBlock:从块入口的 PC 开始,一直分析到遇到 E-bit(事件位)、分支延迟槽结束或块尾,然后把这一小段流水线"压平"成连续的 x86 代码。
📊 Pass 1:IR 分析——给每条指令建档案
IR 数据结构定义在pcsx2/x86/microVU_IR.h,核心是microOp结构,它为每条 64 位指令记录:
uOp(microUpperOp):上指令读写的 VF 向量(xyzw 四个分量逐一标记)、E/I/M/T/D 位;lOp(microLowerOp):下指令的 VF/VI 读写、分支类型(IBxx、JR、JALR等 11 类)、xgkick延迟周期、以及一组精妙的布尔位——badBranch/evilBranch用来处理"分支延迟槽里再套分支"这种 PS2 游戏的"邪恶"写法;stall:本指令因依赖上一条指令的写回而需要停顿的周期数;sFlag/mFlag/cFlag(microFlagInst):三类标志各自的 write/read/lastWrite 实例索引。
分析函数按操作码家族分组,例如mVUanalyzeFMAC1(普通 FMAC 乘加)、mVUanalyzeFDIV(除法,登记 Q 寄存器延迟)、mVUanalyzeIALU1(整数算术)。每个analyzeReg*辅助函数一边标记寄存器读写,一边用std::max累加停顿周期,从而在编译期就把真实的 VU 流水线时序"烘焙"进 IR。
FMAC 指令 → 读 Fs/Ft(登记停顿)→ 写 Fd(登记 4 周期延迟)→ 标记 Status 标志更新 DIV 指令 → 读 Fs.s → 登记 Q 寄存器 4 周期延迟 → 到点时把 I/D 位搬进 Status 标志microIR结构(microVU_IR.h)则持有整个块的状态:info[]数组(每指令一条microOp)、cycles(块总周期)、constReg[16](VI 寄存器块内常量传播)等。
🔥 Pass 2:SSE 指令生成——VF 进 XMM,VI 进 GPR
生成阶段由microVU_Compile.inl的主循环驱动,对每条 64 位指令依次发射上指令、下指令(必要时通过doSwapOp调整执行顺序并用 XOR 交换技巧备份/还原 VF 寄存器)。
浮点侧:VF 向量直接映射到 XMM 寄存器。上指令的乘加运算通过函数指针表SSE_PS(SSE_ADDPS/SUBPS/MULPS/MAXPS/MINPS)在运行时选择对应的ADDPS/MULPS等 SSE 指令;单精度(.s)变体则走SSE_SS表。
标志计算是全文件最精彩的部分。mVUupdateFlags()(microVU_Upper.inl)用一组紧凑的 SSE 序列同时算出 S/Z/M/U 四个标志位:
xCMPEQ.PS(regT1, regT2) ; 找出零分量 xMOVMSKPS(gprT2, regT1) ; 取零标志掩码 xMOVMSKPS(mReg, regT2) ; 取符号位 → 按 XYZW 掩码组合,写回 Status / Mac 标志寄存器钳位(Clamp):PS2 VU 的溢出/下溢钳位行为与 x86 浮点并不完全一致,microVU_Clamp.inl用比较-选择序列模拟FLT_MAX钳位,并可按游戏配置关闭以提速。
整数侧:VI 寄存器(16 位值)映射到普通 GPR,分支指令在microVU_Branch.inl中生成——条件分支直接发射Jcc,而JR/JALR间接跳转则查跳转缓存(microJumpCache)记录的历史目标,跳过昂贵的块状态搜索。
📦 块与程序:两级缓存让重编译"只做一次"
VU 微程序在游戏运行中可能被反复执行,microVU 用两级结构避免重复编译(定义于microVU.h):
- microProgram:一段完整微程序(含
data[]微码副本和ranges[]已编译 PC 区间列表)。由于 VU 内存可能被游戏改写,mVUcmpProg会逐区间比对当前内存与缓存副本,确认"还是同一个程序"才复用; - microBlock:程序内每个块入口的编译产物,附带160 字节的流水线状态(
microRegInfo)。该结构经过精心布局——把needExactMatch、q、p、xgkick等关键字段打包进quick64,使状态比对可以用两条 64 位比较完成;microBlockManager::search()甚至调用一段运行时分发的动态代码(mVUsearchXMM)来做 160 字节的 SIMD 快速比较。
执行入口mVUsearchProg(microVU.cpp)按start_pc索引程序链表,命中后返回块的 x86 入口指针;每个 VU 还预留64MB 重编译缓存(mVUcacheReserve)和独立的分发器页(dispCache),由startFunct/exitFunct函数指针完成"保存寄存器 → 跳转重编译代码 → 恢复"的边界。
🧮 XMM 寄存器分配:32 个 VF 如何塞进 15 个 XMM
microRegAlloc类(microVU_IR.h)是编译器的心脏之一。VU 有 32 个 VF 向量寄存器,而 x86 只有 15 个可用 XMM(xmm15被保留为 P/Q 寄存器专用槽xmmPQ)。它的策略:
- LRU 式置换:
findFreeRegRec递归挑选"最久未使用"的 XMM 淘汰,count字段记录最后使用序号; - 分量级脏标记:
xyzw位掩码记录哪些分量被写过,写回(mVUsaveReg)时只存脏分量,未修改的寄存器继续保持缓存状态; - 部分写合并:
clearNeeded发现两个 XMM 缓存同一 VF 的不同分量时,用mVUmergeRegs把它们合并成一份完整缓存,避免下次重新从内存加载; - 与 EE 共用状态:VU 指令也可以被 EE 当作 COP2 指令内联执行,
regAllocCOP2模式下分配器会与 EE 重编译器的xmmregs状态双向同步(updateCOP2AllocState),保证跨上下文寄存器语义一致。
🚀 执行入口与性能调优
recMicroVU0::Execute/recMicroVU1::Execute(microVU.cpp)是 EE 侧的调用点:把 TPC 左移 3 位换算成字节 PC,跳转到startFunct分发器,执行cycles个周期后返回,必要时触发 VU 中断(hwIntcIrq)。启用MTVU(多线程 VU)时,VU1 在独立线程运行(pcsx2/MTVU.cpp),由waitMTVU函数完成线程间寄存器同步——这也是保存前必须调用vu1Thread.WaitVU()的原因。
所有可调参数集中在pcsx2/x86/microVU_Misc.h的"Optimization / Debug Options"区,适合进阶玩家阅读源码时对照:
| 选项 | 默认 | 说明 |
|---|---|---|
doRegAlloc | ✅ | XMM 寄存器分配,关闭后每 32 位指令即刷回内存 |
doSFlagInsts等 | ✅ | 多标志实例,精确模拟流水线中的标志延迟 |
doBranchInDelaySlot | ✅ | 正确处理"分支延迟槽里的分支" |
doJumpCaching | ✅ | 间接跳转缓存,加速JR/JALR |
doConstProp | ❌ | vi15 常量传播(部分游戏会显著变慢) |
CHECK_VU_FLAGHACK | 可配置 | 只在读取 Status 标志的块更新标志,显著提速 |
若不想用重编译器,PCSX2 也提供纯解释器实现作为参照:pcsx2/VU0micro.cpp、pcsx2/VU1micro.cpp负责逐指令派发,全部操作码语义实现在 pcsx2/VUops.cpp(约 4800 行)。
📁 关键文件索引
- 编译器入口与数据结构:
pcsx2/x86/microVU.h、pcsx2/x86/microVU.cpp - IR 定义与寄存器分配:
pcsx2/x86/microVU_IR.h - Pass 1 分析:
pcsx2/x86/microVU_Analyze.inl - SSE 生成(上/下指令):
pcsx2/x86/microVU_Upper.inl、pcsx2/x86/microVU_Lower.inl - 块编译主循环:
pcsx2/x86/microVU_Compile.inl - 分支与标志:
pcsx2/x86/microVU_Branch.inl、pcsx2/x86/microVU_Flags.inl - 优化开关:
pcsx2/x86/microVU_Misc.h - VU1 多线程:
pcsx2/MTVU.cpp - 解释器对照实现:
pcsx2/VUops.cpp
总结
PCSX2 的 microVU 编译器是"用编译器的思路解决硬件模拟"的典范:先用 IR 分析把 VU 深流水线的时序规则形式化,再通过两级程序/块缓存消除重复编译,最后借助 LRU 寄存器分配把 32 个 VU 向量寄存器高效压入 XMM 寄存器,生成带精确标志模拟的 SSE 代码。理解了这条流水线,你不仅读懂了 VU 的实现,也收获了一套完整的动态二进制翻译设计范式。
【免费下载链接】pcsx2PCSX2 - The Playstation 2 Emulator项目地址: https://gitcode.com/gh_mirrors/pcsx24/pcsx2
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考