323 lines
20 KiB
Markdown
323 lines
20 KiB
Markdown
---
|
||
title: "奥日 2 加载闪退、偶尔进入却无声:一次 Wwise 线程故障排查与 8 字节兼容补丁"
|
||
date: 2026-09-16
|
||
slug: ori-wotw-wwise-thread-affinity-fix
|
||
description: "从崩溃转储和 Windows API 返回值定位 Wwise 音频初始化失败,并通过取消音频线程显式核心绑定完成本机验证。附带哈希校验与还原功能的补丁源码。"
|
||
tags:
|
||
- 游戏排障
|
||
- Windows
|
||
- Unity
|
||
- Wwise
|
||
- 逆向分析
|
||
draft: true
|
||
---
|
||
|
||
<!-- 发布前:填写下载链接与作者信息;按你的博客系统调整 front matter。若支持 Mermaid,可保留图;不支持时删去图,正文已包含完整解释。 -->
|
||
|
||
我遇到的《奥日 2》问题很折磨人:小精灵还在加载界面跑步,游戏就闪退;偶尔成功进去,却完全没有声音。换了两个版本、限制 CPU 核心、禁用部分设备,都没有解决。同系统版本的 VMware 虚拟机反而可以运行。我折腾了数小时,最后甚至退了款,游戏文件倒还留在硬盘上。
|
||
|
||
这次借助 AI 编程助手做了进一步排查,最终找到了一个可以实际验证的触发链:**音频线程绑定 CPU 核心失败,Wwise 的初始化失败路径又留下了仍在运行的线程。**
|
||
|
||
最终补丁改动 8 个字节,让相关音频线程使用 Windows 默认调度。我的机器已经通过普通启动和 Windows 音频输出验证。下面记录证据、原理,以及补丁适用的严格范围。
|
||
|
||
> 这是一个具体 DLL 构建、具体故障路径的非官方兼容补丁,不是所有奥日 2 闪退的通用解法。请先看文件哈希和适用条件。
|
||
|
||
## 想先解决问题:下载、检查、安装、还原
|
||
|
||
补丁包:**[Ori Wwise Affinity Fix v1.0.0]({{PATCH_DOWNLOAD_URL}})**
|
||
完整研究报告:**[详细技术记录]({{RESEARCH_REPORT_URL}})**
|
||
源码仓库或镜像:`{{SOURCE_URL_OR_REMOVE_THIS_LINE}}`
|
||
|
||
分享包只包含脚本、说明与脱敏后的证据摘要。它在本地修改你自己的游戏文件,不附带完整游戏 DLL,也不修改游戏平台授权逻辑。
|
||
|
||
### 适用条件
|
||
|
||
- 游戏常在加载阶段退出,偶尔进入但无声。
|
||
- 日志中出现 `Cannot create streaming I/O device`、`Failed to initialize the sound engine`,崩溃栈指向 `AkSoundEngine.dll` 工作线程。
|
||
- 原始 `AkSoundEngine.dll` 的 **SHA-256 必须完全等于**:
|
||
|
||
```text
|
||
8F90A4A3856540CB97D03CA07B4BB10B7A08F7B4AB3C524BE2BEC1AFDD29FECA
|
||
```
|
||
|
||
DLL 位于游戏目录的 `oriwotw_Data\Plugins` 下。相同游戏名称、相同 Wwise 版本字符串,甚至同样的闪退画面,都不能替代哈希匹配。
|
||
|
||
### 使用方式
|
||
|
||
需要 Windows x64 和 PowerShell 7。先退出游戏,解压补丁包,再双击 `Install.cmd`,按提示粘贴游戏根目录,也就是包含 `oriwotw.exe` 的目录。
|
||
|
||
喜欢命令行的读者,可以在**解压后的补丁目录**打开 PowerShell 7。下面路径只是示例,需要改成你自己的:
|
||
|
||
```powershell
|
||
# 只检查,不修改
|
||
& '.\Ori-AudioFix.ps1' -Action Check -GameDirectory 'D:\Games\Ori and the Will of the Wisps'
|
||
|
||
# 安装补丁并保留经过校验的原文件备份
|
||
& '.\Ori-AudioFix.ps1' -Action Install -GameDirectory 'D:\Games\Ori and the Will of the Wisps'
|
||
|
||
# 还原;请先退出游戏
|
||
& '.\Ori-AudioFix.ps1' -Action Restore -GameDirectory 'D:\Games\Ori and the Will of the Wisps'
|
||
```
|
||
|
||
也可以双击 `Restore.cmd` 还原。备份文件名为:
|
||
|
||
```text
|
||
AkSoundEngine.dll.before-ori-affinity-fix
|
||
```
|
||
|
||
补丁后 DLL 的哈希应为:
|
||
|
||
```text
|
||
62B2079A988D23EB251D4E83366AC04068A6F47E0D38EFE92A76DF022826F9F0
|
||
```
|
||
|
||
版本不匹配时,脚本会停止。请不要修改脚本中的哈希限制强行安装。游戏更新或平台文件校验可能覆盖补丁,更新后的 DLL 需要重新确认适用性。
|
||
|
||
## 我的测试环境
|
||
|
||
本机有一些值得如实记录的特征,尤其是固件和 Windows 对处理器的命名并不相同。它们是复现背景,不是已经证明的故障原因。
|
||
|
||
| 项目 | 环境 |
|
||
| --- | --- |
|
||
| 系统 | Windows 10 专业工作站版,22H2,x64,构建 `19045.6456` |
|
||
| CPU:Windows 报告 | `Genuine Intel(R) 0000` |
|
||
| CPU:固件产品名 | `i9-13900HK(ES)` |
|
||
| CPU 标识 | Family 6 / Model 186 / Stepping 2 |
|
||
| 可用逻辑处理器 | 20;测试进程亲和性 `0xFFFFF` |
|
||
| 内存 | 约 32 GB;崩溃报告为 32600 MiB |
|
||
| 主板固件字段 | `Intel / Z790M GAMING WIFI D5` |
|
||
| BIOS | AMI,`Leo-Patch 1.5`,日期 2024-07-26 |
|
||
| 本次实际渲染设备 | AMD Radeon RX 9070 XT,驱动 `32.0.31041.1004` |
|
||
| 图形接口 | Direct3D 11,功能级别 11.1 |
|
||
| 主测版本 A | Unity `2018.4.11f1`,Build Id `5257` |
|
||
| 另一版本 B | Unity `2018.4.24f1`,Build Id `8978` |
|
||
| 两份游戏共用的音频组件版本 | Wwise `2019.1.7 Build 7135`;原始 DLL 哈希相同 |
|
||
|
||
注册表还存在 NVIDIA Tesla P4、Zako Display Adapter、Microsoft Remote Display Adapter 等显示组件。音频组件包括 Realtek High Definition Audio(`6.0.9239.1`)、AMD High Definition Audio(`10.0.1.41`)、Steam Streaming Speakers(`17.56.13.764`)、VB-Audio Virtual Cable(`3.3.1.7`)和 AMD Streaming Audio Device(`1.0.0.14`)。注册记录不等于它们当时都处于活动状态。
|
||
|
||
VMware 可运行的情况是我此前的观察,本次没有同时采集虚拟机的 vCPU、驱动和文件哈希,所以这里不把它包装成严格的对照实验。
|
||
|
||
## 第一步:把大量日志噪声与重复故障分开
|
||
|
||
Unity 日志里有不少“脚本引用缺失”等警告。真正有价值的线索是,这批历史记录中的 29 次崩溃,有 28 次走到了同一类 Wwise 工作线程路径。
|
||
|
||
对应的关键报错是:
|
||
|
||
```text
|
||
Wwise: Cannot create streaming I/O device.
|
||
WwiseUnity: Failed to initialize the sound engine. Abort.
|
||
```
|
||
|
||
这里的 streaming I/O device 指音频数据读取机制,不是设备管理器中的某块物理声卡。这个区别很关键:如果直接把它理解成声卡坏了,很容易继续围绕音频驱动反复尝试。
|
||
|
||
还要注意,28/29 只是留存崩溃样本的分类,不能当成启动失败率。成功启动不会产生同样的崩溃报告,而且这些记录跨越了此前的不同排障尝试。
|
||
|
||
## 第二步:别完全相信文本栈给出的函数名
|
||
|
||
原始堆栈把出错帧显示成 `AK::StreamMgr::SetFileLocationResolver`。但没有完整调试符号时,工具可能只找到了附近的导出名称。
|
||
|
||
计算相对模块地址并反汇编后,真正值得注意的位置是:
|
||
|
||
```text
|
||
AkSoundEngine.dll + 0x5B0C8
|
||
AkSoundEngine.dll + 0x5B16E
|
||
```
|
||
|
||
这两个位置分别紧跟在虚函数间接调用之后。崩溃线程试图跳转的目标又不在正常的模块代码区。
|
||
|
||
更细的一点:Unity 文本报告写的是“Write to location”,但对应转储中的 `ExceptionInformation[0]` 是 **8**。按微软文档,这意味着用户态数据执行保护(DEP)异常,即尝试执行了不允许执行的内存。
|
||
|
||
所以我没有把“文本日志说写入冲突”当作最终答案,而是继续检查对象和线程的生命周期。
|
||
|
||
## 第三步:记录真正失败的 Windows 调用
|
||
|
||
诊断脚本在自己的游戏子进程内,对音频线程初始化附近设置临时断点,记录参数、返回值和目标线程错误码。原始运行捕获到:
|
||
|
||
```text
|
||
线程优先级:0
|
||
线程核心掩码:0xFFFFFFFC
|
||
线程栈大小:128 KiB
|
||
|
||
SetThreadPriority 返回 1:成功
|
||
SetThreadAffinityMask 返回 0:失败
|
||
Windows 错误码 87:ERROR_INVALID_PARAMETER
|
||
随后发生 0xC0000005 访问冲突
|
||
```
|
||
|
||
`SetThreadAffinityMask` 用来指定线程可以在哪些逻辑 CPU 上运行。掩码里的每一位代表一个逻辑处理器,例如 `0xF` 对应低四位。
|
||
|
||
起初很容易想到:游戏请求 `0xFFFFFFFC`,本机进程报告 `0xFFFFF`,是不是请求了不允许的高位?这个解释必须经过实验。
|
||
|
||
结果是:把掩码缩小到 `0xFFFFC`,游戏线程仍然失败;独立的小程序测试自己的线程时,相同掩码却能成功。因此,**“只要截断多余的位就能修好”没有得到本机实验支持**。
|
||
|
||
这里保留一个尚未解决的问题:宿主机为什么拒绝这个游戏线程的绑定?现有证据不足以把它归因于 ES CPU、混合架构、某个声卡或某个显示驱动。
|
||
|
||
### 网上说的“限制 CPU 核心”,和这次修复有什么区别?
|
||
|
||
我之前确实试过限制游戏可用核心,没有解决;旧启动文件里还留着 `/affinity F`。这里的 F 对应低 4 个**逻辑处理器**,不一定是 4 个物理核心。
|
||
|
||
需要区分三个操作:
|
||
|
||
1. **限制整个游戏进程的核心范围:**改变进程能使用哪些处理器,但游戏内部仍可能对音频线程重新请求绑定。
|
||
2. **缩小音频线程请求的掩码:**本轮实测过,改为 `0xFFFFC` 后仍返回错误 87。
|
||
3. **取消音频线程的显式绑定:**本补丁把配置值设为 0,让 DLL 跳过绑定调用;这才是在本机通过验证的办法。
|
||
|
||
因此,准确说法是“限制核心没有解决我的这个问题”,而不是“限制核心对所有人都没用”。它可能帮助其他并发、时序或处理器拓扑问题;我们也没有穷举所有核心组合。关键仍然是检查相同的错误调用是否消失,而不是只看修改后的某一次是否碰巧启动。
|
||
|
||
## 第四步:找到导致随机崩溃的错误清理关系
|
||
|
||
更严重的问题出现在设置失败以后。
|
||
|
||
反汇编显示,Wwise 先创建一个可以立即运行的线程,再设置优先级和核心绑定。如果设置失败,它会关闭线程句柄并把字段清空。而后续清理逻辑只有在线程句柄仍然存在时才等待线程。
|
||
|
||
**关闭线程句柄,不等于结束线程。**微软的 `CloseHandle` 文档明确写了这一点。
|
||
|
||
下面是控制关系的简化表达,不是厂商源代码:
|
||
|
||
```cpp
|
||
thread = CreateThread(...); // 线程已经可以运行
|
||
|
||
if (!ConfigureThread(thread)) {
|
||
CloseHandle(thread);
|
||
thread = nullptr;
|
||
return Failure;
|
||
}
|
||
|
||
// 后续清理
|
||
if (thread != nullptr) {
|
||
WaitForSingleObject(thread, INFINITE);
|
||
}
|
||
// 此后才应销毁线程仍可能使用的资源。
|
||
```
|
||
|
||
一旦句柄清空但线程还活着,后续清理就可能跳过必要的等待。结合错误的间接调用目标,这强烈支持“仍在运行的线程使用了失效对象”的解释。
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A[音频读取线程开始运行] --> B[设置 CPU 亲和性失败:错误 87]
|
||
B --> C[关闭并清空线程句柄]
|
||
C --> D[初始化失败,进入资源清理]
|
||
D --> E[后续等待可能被跳过]
|
||
E --> F[仍存活线程使用失效状态]
|
||
F --> G[错误间接跳转,访问冲突]
|
||
```
|
||
|
||
本轮直接确认了调用失败、句柄清理分支以及后续异常;没有完整跟踪所有分配和释放事件,所以我把“释放后使用”写成有强证据支持的解释,而不是宣称已经掌握厂商源码层面的全部细节。
|
||
|
||
这个时序也能解释为什么现象不完全固定:音频初始化失败会造成无声,而线程什么时候撞上失效状态,还受调度和内存复用时机影响。
|
||
|
||
## 8 字节补丁:让音频线程交给 Windows 调度
|
||
|
||
这次选用的兼容方案,是利用 DLL 已有的“掩码为 0 时跳过显式绑定”分支。
|
||
|
||
被修改的导出函数是:
|
||
|
||
```text
|
||
CSharp_AkThreadProperties_dwAffinityMask_set
|
||
```
|
||
|
||
原来相当于:
|
||
|
||
```cpp
|
||
if (properties != nullptr)
|
||
properties->dwAffinityMask = suppliedMask;
|
||
```
|
||
|
||
补丁后相当于:
|
||
|
||
```cpp
|
||
if (properties != nullptr)
|
||
properties->dwAffinityMask = 0;
|
||
```
|
||
|
||
注意这里的 0 **不是把线程绑定到“零个核心”**。该音频 DLL 在调用 Windows API 之前就会检查它;值为 0 时直接不调用绑定函数,让线程继承正常的系统调度。
|
||
|
||
对应构建的精确信息:
|
||
|
||
```text
|
||
函数 RVA: 0x121F0
|
||
文件偏移: 0x115F0
|
||
|
||
原始:48 85 C9 74 03 89 51 04 C3 CC CC CC CC CC CC CC
|
||
补丁:48 85 C9 74 07 C7 41 04 00 00 00 00 C3 CC CC CC
|
||
```
|
||
|
||
替换区域为 16 字节,真正变化了其中 8 个字节。空指针检查保留,跳转距离相应调整,使用原函数尾部的填充空间。候选文件经过再次反汇编,确认指令和边界符合预期。
|
||
|
||
这是一种绕过触发条件的兼容修复。若能修改引擎源码,更完整的修复还应确保初始化失败时正确停止并等待线程,之后再释放其依赖资源。
|
||
|
||
## 验证结果
|
||
|
||
| 测试 | 结果 |
|
||
| --- | --- |
|
||
| 原始 DLL 动态跟踪 | 绑定失败,错误 87,随后崩溃 |
|
||
| 仅缩小核心掩码 | 仍然失败 |
|
||
| 进程内取消显式绑定 | 通过音频初始化和后续加载 |
|
||
| 安装补丁后普通启动 | 不依赖调试器,也通过加载 |
|
||
| Windows 音频会话 | 活动、未静音、音量 100%,检测到非零音频信号 |
|
||
| 最后一次退出 | 我主动关闭了游戏 |
|
||
| 安装及还原 | 副本往返测试通过,还原后哈希与原件相同 |
|
||
|
||
音频探针在 3 秒采样中的最大电平约为 `0.01062`。它只读取游戏的音频会话状态,没有录音,也没有访问麦克风。这个结果证明游戏确实向 Windows 音频管线输出了声音信号。
|
||
|
||
本轮主要验证版本 A。版本 B 的音频 DLL 与 A 完全相同,已经安装并校验,但还没有单独进行本轮运行测试。完整长时间游玩、所有关卡、性能和音频延迟测试也还没有完成。
|
||
|
||
<!-- 可选补充,确实测试后再填写,不要保留虚构结果:
|
||
实际试听:{{音乐/音效/对白是否正常}}
|
||
连续游玩:{{测试日期、时长、关卡、是否有新问题}}
|
||
版本 B:{{是否单独验证}}
|
||
其他读者环境:{{经确认的反馈}}
|
||
-->
|
||
|
||
## 分享这个补丁时,我希望保留的限制
|
||
|
||
补丁安装器检查完整 SHA-256、原始指令和备份,已经修补的文件不会重复修改,未知版本会拒绝处理。补丁文件不携带游戏 DLL,读者使用自己的文件生成结果。
|
||
|
||
它影响通过这个 setter 配置的音频线程,不改变系统全局调度,不保证修复所有 `0xC0000005`。取消显式绑定也可能改变性能特征,需要更多环境验证。
|
||
|
||
如果想反馈适用情况,建议附上:Windows 构建号、CPU 标识及逻辑处理器数、Unity/Wwise 版本、DLL 原始哈希、关键报错和测试时长。请先去掉日志中的个人路径;完整崩溃转储不适合直接放在公开评论区。
|
||
|
||
这次研究最有价值的收获,是把模糊的“加载闪退”逐步缩小成可以观测和对照的行为:哪个线程、哪个调用、什么参数、什么返回值,以及修改之后是否在普通启动下仍然成立。
|
||
|
||
## 这算逆向工程吗?
|
||
|
||
算,而且是用于故障诊断和兼容性修复的逆向工程。
|
||
|
||
反汇编把已编译的机器码翻译成汇编,让我们能看到“创建线程后做了什么、失败分支跳到哪里”。动态调试则记录这些指令在本机真正收到的参数和返回值。把两者与崩溃记录对上,才有依据设计补丁。
|
||
|
||
这里没有恢复完整的游戏源码,文中的 C++ 风格代码也是解释逻辑的伪代码。但关键地址、调用参数、错误码和修改字节都保存下来了,其他人可以复核这条推理链。
|
||
|
||
## 网上那些“奥日 2 闪退解决办法”,到底有没有用?
|
||
|
||
我又核对了官方公告和玩家讨论。结论是:**不少办法有适用场景,但“加载闪退”不是一个足以确定根因的诊断。**下面是截至 2026-09-16 的资料核查,不代表我在本机逐项执行过。
|
||
|
||
| 办法 | 我的判断与依据 |
|
||
| --- | --- |
|
||
| 限制 CPU 核心 | **本例此前没解决,不能推成人人无效。**查到的[性能教程](https://www.ghostarrow.com/how-to-improve-performance-in-ori-and-the-will-of-the-wisps)针对 CPU 近满载时的卡顿,使用动态限制规则;没有验证本例 Wwise 错误 87。它也不一定就是我最早看到的那篇教程 |
|
||
| 换声音输出/API、关闭空间音频 | **有官方用途依据。**[Patch 2 公告](https://steamcommunity.com/app/1057090/discussions/0/4666237625643445999/)说明这些选项用于硬件兼容和无声排障;不能据此保证能修复我的线程绑定失败 |
|
||
| 改成 48 kHz | **可能处理格式兼容,具体证据弱。**[相关帖子](https://steamcommunity.com/app/1057090/discussions/0/581680031271722194/)只有简短建议,没有硬件、日志与回复验证;48 kHz 不是已经证明的万能值 |
|
||
| 关闭动态模糊 | **有特定场景的成功反馈。**[Iris Xe 讨论](https://steamcommunity.com/app/1057090/discussions/0/5715697730998329692/)中,两位玩家报告不再冻结。但他们是游玩一段时间后冻结,我是加载时音频初始化失败 |
|
||
| 窗口化、调整 HDR;用 DXVK | **有图形路径的相关反馈。**[RTX 5070 帖](https://steamcommunity.com/app/1057090/discussions/0/582806854239850133/)同时改了多项显示设置,无法分清单项贡献;[RTX 50 帖](https://steamcommunity.com/app/1057090/discussions/0/695372304943750032/)作者称 DXVK 修好了花屏。本机窗口化仍复现原故障,历史 DXVK 尝试还出现过另一类 DXGI 崩溃 |
|
||
| 回退游戏版本或检查 Windows 预览版 | **有特定历史问题的官方依据。**[Patch 3.1 公告](https://steamcommunity.com/app/1057090/discussions/0/2968397584537393710/)提供 Patch 2 回退;[开发者白屏帖](https://steamcommunity.com/app/1057090/discussions/0/3012312119189327120/)初步关联当时 Insider Dev 预览版。我的正式版 Win10 不能直接照搬 |
|
||
| 校验文件、修运行库、换驱动 | **有对应错误时有用。**分别处理损坏文件、缺失依赖和驱动兼容;恢复原版文件无法修正它自身的线程清理逻辑 |
|
||
| 安装 Unity 引擎、套用“一键优化” | **作为万能办法缺乏依据。**[一篇启动指南](https://www.ghostarrow.com/ori-and-the-will-of-the-wisps-fix-wont-launch-black-screen-crashes)确实建议安装 Unity,但游戏自带运行用的 Unity Player,不能由此推出玩家必须安装编辑器,也没有对应对照证据 |
|
||
|
||
我所说的“玄学”,是把缺少证据的相关性讲成必然有效的因果。像改采样率、关闭叠加层、调整核心限制,本身不神秘;问题是有没有对应的故障条件,有没有记录到底是哪一步起作用。
|
||
|
||
还有一个容易误读的例子:[无声讨论帖](https://steamcommunity.com/app/1057090/discussions/0/2968398851794293852/)收到许多建议,但楼主明确回复没用。几个月后游戏恢复声音,楼主也不知道原因。不能把这段故事压缩成“改声音设置/更新系统已验证有效”。
|
||
|
||
对我这次排障,最有分量的区别仍是:缩小音频线程请求掩码后,同一个绑定调用继续失败;让它不再发起显式绑定后,音频初始化及普通启动通过。它比仅凭一次“能进游戏了”更能支持这个补丁的作用机制,但仍不代替长时间游玩验证。
|
||
|
||
分享包附有 `COMMUNITY-REVIEW.zh-CN.md`,保留更详细的适用条件、来源和证据限制。
|
||
|
||
## 参考资料
|
||
|
||
- [Microsoft:CloseHandle](https://learn.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-closehandle)
|
||
- [Microsoft:CreateThread](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createthread)
|
||
- [Microsoft:SetThreadAffinityMask](https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setthreadaffinitymask)
|
||
- [Microsoft:EXCEPTION_RECORD](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-exception_record)
|
||
- [Microsoft:x64 调用约定](https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170)
|
||
- [Microsoft:Windows 错误码 0–499](https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-)
|
||
|
||
官方资料核对日期:2026-09-16。本文的实验结论来自本机记录,不把论坛上的类似症状或未经验证的猜测当作根因。
|