【免费下载链接】Eglantine
VERY experimental Nintendo Switch 2 emulator
Eglantine 是一个高度实验性的Nintendo Switch 2 模拟器,而让它跑起来的心脏,是名为Dynarmic的开源动态重编译器(CPU 模拟核心)。本文将用通俗易懂的语言,完整拆解 Dynarmic "解码 → 翻译 → 优化 → 发射" 的 JIT 编译流水线,讲清块链接、返回栈缓冲(RSB)、Fastmem 三大性能优化的设计原理,并带你 10 分钟定位源码中每个关键模块 🧭
一、为什么 CPU 模拟是模拟器的"心脏"
模拟器的本质,是在你的 x86-64 或 AArch64 电脑上"造"出一颗 Armv8 处理器。Eglantine 采用 HLE(High-Level Emulation)策略:不逐位复刻硬件,而是模拟游戏可见的行为接口(详见 docs/DesignOverview.md)。
在 CPU 这一环,业界有两类主流方案:
| 方案 | 原理 | 速度 | 典型代表 |
|---|---|---|---|
| 逐条解释器 | 每条指令实时查表执行 | 慢 | 教学项目 |
| 动态重编译器 | 把一段段"基本块"提前翻译成宿主机器码缓存执行 | 快(接近原生) | Dynarmic、QEMU TCG |
Eglantine 选择了后者,且每个模拟 CPU 核心都拥有一个独立的 Jit 实例,由 src/core/cpu_manager.h 中的CpuManager调度多核 guest 线程与空闲线程。对外的统一抽象接口是 src/core/arm/arm_interface.h 的ArmInterface类——RunThread跑到事件发生、StepThread单步执行(调试用)、SignalInterrupt提前打断,Dynarmic 与备选的 NCE 原生执行器都实现这一接口,可以无缝替换。
二、Dynarmic 五级流水线:一条指令的"变形记"
当 Eglantine 调用Jit::Run()后,Dynarmic 会循环地"取一段 guest 指令 → 翻译 → 执行"。每条指令要经过五个阶段(完整流程见 docs/dynarmic/Design.md):
| 阶段 | 做什么 | 源码位置 |
|---|---|---|
| 1️⃣ Decoding 解码 | 识别指令类型,切分出各字段 | src/dynarmic/src/dynarmic/frontend/ |
| 2️⃣ Translation 翻译 | 生成高层 SSA 中间表示(IR) | src/dynarmic/src/dynarmic/frontend/A64/translate/ |
| 3️⃣ Optimization 优化 | 消除冗余微指令、常量传播 | src/dynarmic/src/dynarmic/ir/opt_passes.cpp |
| 4️⃣ Emission 发射 | 生成宿主可执行代码写入内存 | src/dynarmic/src/dynarmic/backend/ |
| 5️⃣ Execution 执行 | 宿主 CPU 跳转到生成的代码直接跑 | src/dynarmic/src/dynarmic/backend/x64/block_of_code.cpp |
1. 解码:一张"位模式查表"的艺术
解码器采用双分派(double dispatch):每条 ARM 指令在指令表里占一行,例如:
INST(&V::arm_ADC_imm, "ADC (imm)", "cccc0010101Snnnnddddrrrrvvvvvvvv")第三个参数就是指令的位模式:0/1表示该位必须匹配,-表示任意,连续相同字符(如cccc)表示一个字段。当一条指令的位与模式吻合时,解码器自动调用访客类中对应字段数量的成员函数——字段数不匹配会直接编译报错,相当于把"漏掉一条指令"变成了编译期错误,非常严谨。ARM 的 ARM/Thumb16/Thumb32 三种编码各有一张表,见 src/dynarmic/src/dynarmic/frontend/A32/decoder/arm.h。
2. 翻译:生成有序 SSA 中间表示
解码之后,访客函数把指令"翻译"成一串IR 微指令。比如ADC(带进位加法)会被翻译成AddWithCarry+ 标志位更新几条微指令,完整列表在 src/dynarmic/src/dynarmic/ir/opcodes.inc。
Dynarmic 的 IR 有几个鲜明特点:
- 有序 SSA(Static Single Assignment):每个值只被定义一次,天然利于优化;
- 上下文微指令显式化:
GetRegister/SetRegister、GetNFlag/SetZFlag等读写 CPU 状态的微指令是 IR 的一等公民,N/Z/C/V 四个标志各自作为独立值流转,为后续优化留足空间; - 基本块 + 终结指令(Terminal):每个块以
ReturnToDispatch(回调度器)、LinkBlock(直跳下一块)等终结指令收尾,这正是第三部分性能优化的"开关"。
3~5. 优化、发射与执行
优化路径(opt_passes.cpp)会做死代码消除、常量传播、Set→Get冗余对消除等。随后EmitX64等发射器把 IR 编译成宿主机器码,写入一块预分配的代码缓存——Eglantine 在 x64 上给每个 Jit 分配 512MiB(见 src/core/arm/dynarmic/arm_dynarmic_64.cpp)。之后宿主 CPU 直接跳进去执行,翻译的代价被摊薄到整个基本块上。
三、三大性能优化:块链接、RSB 与 Fastmem
Dynarmic 的快,主要快在"少走回头路"。以下三个优化在 src/dynarmic/src/dynarmic/interface/optimization_flags.h 中都有对应标志位。
⚡ 块链接(BlockLinking):跳过调度器
传统做法是每块执行完就"返回调度器"查表找下一块——这很贵。若下一块地址在翻译期就可确定,Dynarmic 直接让当前块jump到下一个已编译块的入口,调度器查找直接归零。
⚡ 返回栈缓冲(RSB):预测函数返回
ret的返回地址编译期未知怎么办?Dynarmic 模拟硬件的返回栈缓冲:执行call时把"预测的返回地址"压入 RSB,执行ret时弹出比对,命中就直接跳到预测块,未命中才回调度器。
两个工程细节值得品味:
- 编译块不能只用 PC 唯一标识(FPCR 状态也影响代码),于是用 64 位
UniqueHash=PC | FPCR | T | E来标识; - 名字虽叫"栈",实际实现是8 项的环形缓冲——比真实硬件的 3 项更大,实测 8 项性能最佳,超过 8 项反而下降。
⚡ Fastmem:让 OS 帮你查内存
常规做法是每条 guest 内存访问都调用MemoryRead/MemoryWrite回调(push + call + pop,指令缓存友好性也差)。Fastmem 的思路是把 guest 虚拟地址空间 1:1 映射到宿主一块专用 arena:
- 地址命中 → 直接执行普通宿主 load/store,零回调;
- 访问非法 → 触发宿主页错误,由信号处理器捕获并回落到内存回调,必要时重新编译该块(见 config.h 中
fastmem_pointer相关配置)。
这种"借用操作系统页表 + 快速信号派发"的机制在 Linux/Windows 上收益最大,也是 Eglantine 在CpuAccuracy = Auto时的默认选择。
四、Eglantine 的集成方式:五种 CpuAccuracy 精度模式
Eglantine 把 Dynarmic 封装在 src/core/arm/dynarmic/arm_dynarmic_64.cpp 中。DynarmicCallbacks64实现了全部UserCallbacks:内存读写、CallSVC(系统调用触发模拟内核)、AddTicks/GetTicksRemaining(周期计费)、GetCNTPCT(guest 时钟,Eglantine 按 Switch 2 固件设定为31.25 MHz,见 arm_dynarmic_64.cpp#L272-L274)。
用户能感受到的核心开关是cpu_accuracy(枚举定义见 src/common/settings_enums.h#L142),MakeJit函数会据此配置优化组合:
| 模式 | 定位 | 打开的优化 |
|---|---|---|
| Auto(默认) | 速度与兼容性平衡 | 全部安全优化 + Fastmem + UnfuseFMA + 忽略全局监视器 |
| Accurate | 精度优先 | 仅安全优化 |
| Unsafe | 极致性能 | 按需勾选各项 unsafe 优化(FMA 非融合、放宽浮点误差、Fastmem 64 位检查等) |
| Paranoid | 调试优化的"恐慌模式" | 关闭一切优化,用于对照验证 |
| Debugging | 逐项排查 | 逐项关闭块链接、RSB、FastDispatch 等,定位问题来源 |
📌 新手建议:默认Auto即可;遇到特定游戏崩溃时,可切到Debugging逐项排除优化项,这正是这套"可插拔优化标志"设计的价值所在。
五、源码地图:10 分钟定位每个模块
| 想理解什么 | 直接打开 |
|---|---|
| 总体设计、流水线、RSB 原理 | docs/dynarmic/Design.md |
| 库的用法与边界(只模拟用户态等限制) | src/dynarmic 的 README |
| A64 接口与全部可配置项 | src/dynarmic/src/dynarmic/interface/A64/config.h |
| Jit 对外 API(Run/Step/寄存器读写) | src/dynarmic/src/dynarmic/interface/A64/a64.h |
| IR 基本块数据结构 | src/dynarmic/src/dynarmic/ir/basic_block.h |
| Eglantine 侧回调与配置装配 | src/core/arm/dynarmic/arm_dynarmic_64.cpp |
| CPU 线程/多核调度 | src/core/cpu_manager.h |
| AArch64 主机上的备选原生执行器 NCE | src/core/arm/nce/ |
值得一提的是,Dynarmic 在此仓库中还扩展了LoongArch64 与 RISC-V64宿主后端(见 src/dynarmic/src/dynarmic/backend/),比上游支持的宿主更多。
六、延伸阅读
- 完整设计文档:docs/dynarmic/Design.md(含 RSB 伪代码与 Fastmem 图解)
- 模拟器整体架构:docs/DesignOverview.md
- 构建指南:docs/Build.md
下图是 Eglantine 在 IDE 中配置好的构建环境,选好 Desktop Kit 后即可编译包含 Dynarmic 在内的完整项目:
小结:Dynarmic 用"指令表解码 → SSA 翻译 → 可插拔优化 → 宿主代码发射"的流水线,加上块链接、RSB、Fastmem 三大优化,把 ARM64 guest 指令高效地"种"到你的宿主 CPU 上;而 Eglantine 则通过ArmInterface抽象与五级CpuAccuracy模式,在速度、精度与可调试性之间取得了工程化的平衡。理解这套机制,你就抓住了 Switch 2 模拟器性能调优的主线 🎮
【免费下载链接】Eglantine
VERY experimental Nintendo Switch 2 emulator
相关推荐
Dynarmic 动态重编译器设计剖析:解码、IR、寄存器分配、RSB 与 Fastmem
Dynarmic 动态重编译器设计剖析:解码、IR、寄存器分配、RSB 与 Fastmem 本文以 Dynarmic 官方设计文档为骨架,逐层拆解这个被 Egl
Eglantine 中的 Dynarmic 动态重编译器:目标架构、宿主接口、优化机制与精度边界
Eglantine 中的 Dynarmic 动态重编译器:目标架构、宿主接口、优化机制与精度边界 Dynarmic 是 Eglantine 用来执行 Guest
Eglantine 架构设计总览:以 HLE(高级模拟)为核心重构 Nintendo Switch 2 模拟器子系统
Eglantine 架构设计总览:以 HLE(高级模拟)为核心重构 Nintendo Switch 2 模拟器子系统 Eglantine 是一个实验性的 Nin
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考