--- 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 --- 我遇到的《奥日 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 完全相同,已经安装并校验,但还没有单独进行本轮运行测试。完整长时间游玩、所有关卡、性能和音频延迟测试也还没有完成。 ## 分享这个补丁时,我希望保留的限制 补丁安装器检查完整 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。本文的实验结论来自本机记录,不把论坛上的类似症状或未经验证的猜测当作根因。