新增奥日2排障记录:Wwise 线程亲和性故障定位与兼容补丁(文章4篇+补丁包+诊断脚本4个,已脱敏)

This commit is contained in:
SCHOOL
2026-09-23 15:08:27 +08:00
parent 90bcf6176e
commit 6bf9981765
21 changed files with 2815 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
# 诊断脚本说明
这 4 个脚本是 2026-09-16 那轮定位用到的全部工具,用 Python 标准库写的,**不需要装任何第三方包**。
对应的技术报告见 `../文章/奥日2崩溃研究报告.md` 第 5 节(动态实验设计及结果)。
---
## 每个脚本做什么
### 1. `ori_debug.py` — 核心诊断器
用 Windows 调试接口**启动自己的游戏子进程**(不是附加到别的进程,也不装系统钩子),
在音频线程初始化附近的指令上打临时断点(INT3),记录寄存器、参数、返回值和**目标线程自己的** LastError。
这是抓到"`SetThreadAffinityMask` 返回 0、错误码 87"那个关键证据的工具。
```powershell
# 只校验 DLL 哈希,不启动游戏
python ori_debug.py --game "D:\Games\Ori and the Will of the Wisps" --check
# 原始运行:只下断点观察,不做任何修改
python ori_debug.py --game "D:\Games\..." --seconds 40 --output ori-evidence/runtime-original
# 对照实验 A:把掩码截断到进程可用范围(用来否掉"高位越界"假设)
python ori_debug.py --game "D:\Games\..." --clamp-affinity
# 对照实验 B:在进程内把 setter 写入的掩码改成 0(临时验证,不落盘)
python ori_debug.py --game "D:\Games\..." --automatic-affinity
```
**两道安全检查:**
- 启动前先算 `AkSoundEngine.dll` 的 SHA-256,不符就拒绝下断点
- 关闭时只终止**自己创建的那个**进程,不会碰任何其它游戏会话
### 2. `ori_launch.py` — 普通启动器
不带调试器、不改内存,用来验证"**普通启动也通过**"——这才排除了"只有调试器下才成功"的可能。
```powershell
python ori_launch.py --game "D:\Games\Ori and the Will of the Wisps" --label normal-patched-1
```
它会先确认补丁已安装(校验**补丁后**的哈希),没装就直接退出。输出目录是相对于当前目录的
`ori-evidence/<label>/`,不会写到别处。
> 依赖 `ori_debug.py` 里的常量定义,两个文件要放在同一目录。
### 3. `ori_audio_probe.py` — 音频输出验证
只读指定进程的 Windows 音频会话状态和电平,采样 3 秒取峰值。
```powershell
python ori_audio_probe.py <游戏进程 PID>
```
它**不录音、不访问麦克风、不改音量设置**。它证明的是"游戏确实往 Windows 音频管线里送了声音信号"
——比"看到菜单了"强得多。
### 4. `probe_affinity.py` — 独立对照探针
在 Python 自己的短命线程上试一组掩码(`0xFFFFC` 到 `0xFFFFFFFC`)。
```powershell
python probe_affinity.py
```
这个脚本看着不起眼,但它是**否掉两个假设的关键对照**:如果游戏线程设不上这些掩码、
而一个普通线程却能设上,那"核心太多""CPU 坏了""掩码越界"就都解释不通了。
---
## 使用前提
| 项目 | 要求 |
| --- | --- |
| 系统 | Windows x64 |
| Python | 3.x(只用标准库) |
| 权限 | 无需要求管理员,但要能启动游戏 |
| 游戏目录 | **必须自己用 `--game` 指定**,脚本不带任何默认路径 |
## 注意事项
- 脚本里的断点地址(`0x5B2B9`、`0x5B2D8`、`0x121F0` 等)是**针对特定 DLL 构建**的 RVA。
换一个 DLL 版本这些地址就失效了,`ori_debug.py` 的哈希校验会拦住这种情况——**不要去掉那道校验**。
- `--automatic-affinity` 只在进程内存里改,退出即失效,**不会写盘**。磁盘补丁请用补丁包里的安装器。
- 这些脚本是**排查工具**,不是"一键修复"。它们观察和验证,不负责修好你的游戏。
---
## 发布说明
从原工作区复制过来时,改了 2 处:
| 文件 | 改动 |
| --- | --- |
| `ori_debug.py:239` | 原来硬编码了原作者的本地游戏目录,改成 `required=True` |
| `ori_launch.py:10` | 同上 |
原因是这两行硬编码了原作者的本地游戏路径(具体路径已在本说明中略去)。
改成必填参数后,使用者必须自己指定目录——
和补丁包 `build_ori_share.py` 里"公开版不采用研究机器默认路径"的做法一致。
改完已通过 `python -m py_compile` 语法检查。
@@ -0,0 +1,84 @@
"""Read only the target game's Windows audio-session state and volume meter.
Does not record audio, access microphones, or change device/volume settings.
"""
import argparse
import ctypes as c
import json
import time
import uuid
p=argparse.ArgumentParser()
p.add_argument("pid",type=int)
a=p.parse_args()
ole=c.OleDLL("ole32")
P=c.c_void_p
I=c.c_int32
U=c.c_uint32
F=c.c_float
def guid(s): return (c.c_ubyte*16).from_buffer_copy(uuid.UUID(s).bytes_le)
def call(obj,index,types=(),args=()):
vt=c.cast(obj,c.POINTER(c.POINTER(P))).contents
return c.WINFUNCTYPE(I,P,*types)(vt[index])(obj,*args)
def check(hr):
if hr<0: raise OSError(f"COM HRESULT 0x{hr&0xffffffff:08x}")
def qi(obj,iid):
out=P()
check(call(obj,0,(P,c.POINTER(P)),(guid(iid),c.byref(out))))
return out
refs=[]
def keep(obj):
refs.append(obj)
return obj
ole.CoInitializeEx.argtypes=[P,U]
ole.CoCreateInstance.argtypes=[P,P,U,P,c.POINTER(P)]
ole.CoTaskMemFree.argtypes=[P]
check(ole.CoInitializeEx(None,0))
try:
enum=P()
check(ole.CoCreateInstance(guid("BCDE0395-E52F-467C-8E3D-C4579291692E"),None,23,guid("A95664D2-9614-4F35-A746-DE8DB63617E6"),c.byref(enum)))
keep(enum)
devices=P()
check(call(enum,3,(I,U,c.POINTER(P)),(0,1,c.byref(devices))))
keep(devices)
count=U()
check(call(devices,3,(c.POINTER(U),),(c.byref(count),)))
found=0
for i in range(count.value):
dev=P()
check(call(devices,4,(U,c.POINTER(P)),(i,c.byref(dev))))
keep(dev)
manager=P()
check(call(dev,3,(P,U,P,c.POINTER(P)),(guid("77AA99A0-1BD6-484F-8BC7-2C654C9A9B6F"),23,None,c.byref(manager))))
keep(manager)
sessions=P()
check(call(manager,5,(c.POINTER(P),),(c.byref(sessions),)))
keep(sessions)
sc=I()
check(call(sessions,3,(c.POINTER(I),),(c.byref(sc),)))
for j in range(sc.value):
session=P()
check(call(sessions,4,(I,c.POINTER(P)),(j,c.byref(session))))
keep(session)
sc2=keep(qi(session,"BFB7FF88-7239-4FC9-8FA2-07C950BE9C6D"))
pid=U()
check(call(sc2,14,(c.POINTER(U),),(c.byref(pid),)))
if pid.value!=a.pid: continue
found+=1
meter=keep(qi(session,"C02216F6-8C67-4B5B-9D00-D008E73E0064"))
volume=keep(qi(session,"87CE5498-68D6-44E5-9215-6DA47EF883D8"))
state,muted=I(),I()
vol,peak=F(),F()
check(call(session,3,(c.POINTER(I),),(c.byref(state),)))
check(call(volume,4,(c.POINTER(F),),(c.byref(vol),)))
check(call(volume,6,(c.POINTER(I),),(c.byref(muted),)))
maximum=0.0
for _ in range(12):
check(call(meter,3,(c.POINTER(F),),(c.byref(peak),)))
maximum=max(maximum,peak.value)
time.sleep(0.25)
print(json.dumps({"pid":a.pid,"active":state.value==1,"muted":bool(muted.value),"volume":vol.value,"peak_over_3_seconds":maximum}))
if not found: print(json.dumps({"pid":a.pid,"audio_sessions_found":0}))
finally:
for ref in reversed(refs): call(ref,2)
ole.CoUninitialize()
@@ -0,0 +1,245 @@
"""Observe only the launched Ori process; trace Wwise thread setup failures.
Uses Windows debugging APIs and Python's standard library. No DLLs are
installed, and no game files are changed. INT3 breakpoints are confined to
the child process and restored before resuming each instruction.
"""
import argparse
import ctypes as C
from ctypes import wintypes as W
import hashlib
import json
import os
from pathlib import Path
import struct
import sys
import time
sys.stdout.reconfigure(encoding="utf-8", errors="replace", line_buffering=True)
K = C.WinDLL("kernel32", use_last_error=True)
HANDLE = C.c_void_p
DWORD = C.c_uint32
U64 = C.c_uint64
class STARTUPINFO(C.Structure):
_fields_ = [("cb", DWORD), ("reserved", W.LPWSTR), ("desktop", W.LPWSTR),
("title", W.LPWSTR), ("x", DWORD), ("y", DWORD),
("xs", DWORD), ("ys", DWORD), ("xc", DWORD), ("yc", DWORD),
("fill", DWORD), ("flags", DWORD), ("show", W.WORD),
("cb2", W.WORD), ("reserved2", HANDLE),
("stdin", HANDLE), ("stdout", HANDLE), ("stderr", HANDLE)]
class PROCESSINFO(C.Structure):
_fields_ = [("process", HANDLE), ("thread", HANDLE),
("pid", DWORD), ("tid", DWORD)]
class DEBUGEVENT(C.Structure):
_fields_ = [("code", DWORD), ("pid", DWORD), ("tid", DWORD),
("payload", C.c_ubyte * 164)]
def api(name, restype, *argtypes):
fn = getattr(K, name)
fn.restype, fn.argtypes = restype, argtypes
return fn
CreateProcess = api("CreateProcessW", W.BOOL, W.LPCWSTR, W.LPWSTR, HANDLE,
HANDLE, W.BOOL, DWORD, HANDLE, W.LPCWSTR,
C.POINTER(STARTUPINFO), C.POINTER(PROCESSINFO))
Read = api("ReadProcessMemory", W.BOOL, HANDLE, HANDLE, HANDLE, C.c_size_t, HANDLE)
Write = api("WriteProcessMemory", W.BOOL, HANDLE, HANDLE, HANDLE, C.c_size_t, HANDLE)
Protect = api("VirtualProtectEx", W.BOOL, HANDLE, HANDLE, C.c_size_t, DWORD, C.POINTER(DWORD))
Flush = api("FlushInstructionCache", W.BOOL, HANDLE, HANDLE, C.c_size_t)
Wait = api("WaitForDebugEvent", W.BOOL, C.POINTER(DEBUGEVENT), DWORD)
Continue = api("ContinueDebugEvent", W.BOOL, DWORD, DWORD, DWORD)
OpenThread = api("OpenThread", HANDLE, DWORD, W.BOOL, DWORD)
GetContext = api("GetThreadContext", W.BOOL, HANDLE, HANDLE)
SetContext = api("SetThreadContext", W.BOOL, HANDLE, HANDLE)
Close = api("CloseHandle", W.BOOL, HANDLE)
Terminate = api("TerminateProcess", W.BOOL, HANDLE, DWORD)
Detach = api("DebugActiveProcessStop", W.BOOL, DWORD)
SetKill = api("DebugSetProcessKillOnExit", W.BOOL, W.BOOL)
GetAffinity = api("GetProcessAffinityMask", W.BOOL, HANDLE, C.POINTER(U64), C.POINTER(U64))
REGS = {"rax":120,"rcx":128,"rdx":136,"rbx":144,"rsp":152,
"rbp":160,"rsi":168,"rdi":176,"r8":184,"r9":192,
"r10":200,"r11":208,"r12":216,"r13":224,"r14":232,
"r15":240,"rip":248}
BREAKS = {0x5B2B9:"before_priority", 0x5B2BF:"after_priority",
0x5B2D8:"before_affinity", 0x5B2DE:"after_affinity",
0xAD473:"before_affinity_audio", 0xAD479:"after_affinity_audio",
0xBB212:"before_affinity_bank", 0xBB218:"after_affinity_bank",
0xC3364:"before_affinity_monitor", 0xC336A:"after_affinity_monitor"}
EXPECTED_HASH = "8f90a4a3856540cb97d03ca07b4bb10b7a08f7b4ab3c524be2bec1afdd29feca"
def run(args):
game = Path(args.game).resolve()
dll = game / "oriwotw_Data/Plugins/AkSoundEngine.dll"
digest = hashlib.sha256(dll.read_bytes()).hexdigest()
if digest != EXPECTED_HASH:
raise RuntimeError("Unexpected AkSoundEngine.dll hash; refusing breakpoints")
assert C.sizeof(STARTUPINFO)==104 and C.sizeof(PROCESSINFO)==24
assert C.sizeof(DEBUGEVENT)==176
print(json.dumps({"game":str(game),"dll_sha256":digest,"check_only":args.check},ensure_ascii=False))
if args.check:
return
out = Path(args.output).resolve()
out.mkdir(parents=True, exist_ok=True)
log = (out / "trace.jsonl").open("w",encoding="utf-8")
def emit(**kw):
s = json.dumps({"time":time.time(),**kw},ensure_ascii=False)
print(s)
log.write(s+"\n")
log.flush()
pi, si = PROCESSINFO(), STARTUPINFO()
si.cb = C.sizeof(si)
# The command runner uses a private desktop. An explicitly authorized
# game test must use the user's interactive desktop, or Unity may pause
# before audio initialization while its window is invisible.
si.desktop = "winsta0\\default"
si.flags = 1
si.show = 1
cmd = C.create_unicode_buffer('"'+str(game / "oriwotw.exe")+'" -screen-fullscreen 0 -screen-width 1280 -screen-height 720 -logFile "'+str(out / "game.log")+'"')
if not CreateProcess(str(game / "oriwotw.exe"),cmd,None,None,False,2,None,str(game),C.byref(si),C.byref(pi)):
raise C.WinError(C.get_last_error())
emit(event="launched",pid=pi.pid)
process_mask, system_mask = U64(), U64()
if not GetAffinity(pi.process,C.byref(process_mask),C.byref(system_mask)):
raise C.WinError(C.get_last_error())
emit(event="affinity_limits",process_mask=hex(process_mask.value),system_mask=hex(system_mask.value))
threads, bps = {}, {}
pending = None
alive = True
start = time.monotonic()
def read(addr,n):
buf = C.create_string_buffer(n)
if not Read(pi.process,addr,buf,n,None):
return b""
return buf.raw
def patch(addr,data):
old = DWORD()
if not Protect(pi.process,addr,len(data),0x40,C.byref(old)):
raise C.WinError(C.get_last_error())
try:
if not Write(pi.process,addr,data,len(data),None):
raise C.WinError(C.get_last_error())
Flush(pi.process,addr,len(data))
finally:
unused = DWORD()
Protect(pi.process,addr,len(data),old.value,C.byref(unused))
def context(tid):
h = OpenThread(0x1A,False,tid)
if not h:
raise C.WinError(C.get_last_error())
raw = C.create_string_buffer(1248)
addr = (C.addressof(raw)+15)&~15
C.memset(addr,0,1232)
C.c_uint32.from_address(addr+48).value=0x10000B
if not GetContext(h,addr):
Close(h)
raise C.WinError(C.get_last_error())
return h,raw,addr
try:
while time.monotonic()-start < args.seconds:
ev = DEBUGEVENT()
if not Wait(C.byref(ev),500):
err=C.get_last_error()
if err==121:
continue
raise C.WinError(err)
raw=bytes(ev)
status=0x10002
if ev.code==3:
fh,_,th,_,_,_,teb=struct.unpack_from("<QQQQIIQ",raw,16)
threads[ev.tid]=teb
if fh: Close(fh)
elif ev.code==2:
th,teb,_=struct.unpack_from("<QQQ",raw,16)
threads[ev.tid]=teb
if th: Close(th)
elif ev.code==6:
fh,base=struct.unpack_from("<QQ",raw,16)
# Read the PE export directory name to identify the module.
hdr=read(base,4096)
name=""
if hdr[:2]==b"MZ":
pe=struct.unpack_from("<I",hdr,60)[0]
export=struct.unpack_from("<I",hdr,pe+24+112)[0]
ed=read(base+export,40) if export else b""
if ed:
nr=struct.unpack_from("<I",ed,12)[0]
name=read(base+nr,128).split(b"\0")[0].decode("ascii","replace")
if fh: Close(fh)
if name.lower()=="aksoundengine.dll":
emit(event="wwise_loaded",base=hex(base))
if args.automatic_affinity:
addr=base+0x121F0
original=bytes.fromhex("48 85 c9 74 03 89 51 04 c3 cc cc cc cc cc cc cc")
if read(addr,16)!=original:
raise RuntimeError("Affinity setter bytes differ; refusing memory patch")
# Preserve the null check; assign the documented zero
# mask, meaning inherit Windows scheduling, then return.
patch(addr,bytes.fromhex("48 85 c9 74 07 c7 41 04 00 00 00 00 c3 cc cc cc"))
emit(event="automatic_affinity_memory_patch",rva="0x121f0")
for rva,label in BREAKS.items():
addr=base+rva
original=read(addr,1)
bps[addr]=(original,label)
patch(addr,b"\xcc")
elif ev.code==1:
code=struct.unpack_from("<I",raw,16)[0]
addr=struct.unpack_from("<Q",raw,32)[0]
first=struct.unpack_from("<I",raw,168)[0]
if code==0x80000003 and addr in bps:
original,label=bps.pop(addr)
h,buf,ctx=context(ev.tid)
try:
regs={k:C.c_uint64.from_address(ctx+v).value for k,v in REGS.items()}
teb=threads.get(ev.tid,0)
er=read(teb+0x68,4) if teb else b""
props=read(regs["rdi"],12)
emit(event=label,tid=ev.tid,registers={k:hex(v) for k,v in regs.items()},last_error=struct.unpack("<I",er)[0] if er else None,thread_properties=list(struct.unpack("<iII",props)) if props else None)
if label.startswith("before_affinity") and args.clamp_affinity:
fixed=(regs["rdx"] & process_mask.value) or process_mask.value
C.c_uint64.from_address(ctx+REGS["rdx"]).value=fixed
emit(event="clamped_affinity",old=hex(regs["rdx"]),new=hex(fixed))
patch(addr,original)
C.c_uint64.from_address(ctx+248).value=addr
if not SetContext(h,ctx): raise C.WinError(C.get_last_error())
finally:
Close(h)
elif code==0x80000003:
pass
else:
status=0x80010001
if code not in (0x406D1388,0xE06D7363):
emit(event="exception",code=hex(code),address=hex(addr),first_chance=bool(first),tid=ev.tid)
elif ev.code==5:
emit(event="exit",exit_code=struct.unpack_from("<I",raw,16)[0])
alive=False
Continue(ev.pid,ev.tid,status)
if not alive: break
if alive:
emit(event="time_limit",seconds=args.seconds)
finally:
if alive:
# Stop only the exact process created above; never attach to or stop
# any unrelated game session.
Terminate(pi.process,0)
for _ in range(100):
ev=DEBUGEVENT()
if not Wait(C.byref(ev),100): break
Continue(ev.pid,ev.tid,0x10002)
if ev.code==5: break
Close(pi.thread)
Close(pi.process)
log.close()
if __name__=="__main__":
p=argparse.ArgumentParser()
p.add_argument("--game",required=True,help="game root directory containing oriwotw.exe")
p.add_argument("--output",default="ori-evidence/runtime-original")
p.add_argument("--seconds",type=int,default=40)
p.add_argument("--check",action="store_true")
p.add_argument("--clamp-affinity",action="store_true")
p.add_argument("--automatic-affinity",action="store_true")
run(p.parse_args())
@@ -0,0 +1,33 @@
"""Launch the patched game normally, without debugging or memory changes."""
import argparse
import ctypes as C
import hashlib
import json
from pathlib import Path
from ori_debug import STARTUPINFO, PROCESSINFO, CreateProcess, Close
p=argparse.ArgumentParser()
p.add_argument("--game",required=True,help="game root directory containing oriwotw.exe")
p.add_argument("--label",default="normal-patched-1")
a=p.parse_args()
if not a.label.replace("-","").replace("_","").isalnum():
raise SystemExit("Label must contain only letters, numbers, hyphens or underscores")
root=Path(a.game).resolve()
dll=root / "oriwotw_Data/Plugins/AkSoundEngine.dll"
if hashlib.sha256(dll.read_bytes()).hexdigest()!="62b2079a988d23eb251d4e83366ac04068a6f47e0d38efe92a76df022826f9f0":
raise SystemExit("The verified patch is not installed")
out=Path("ori-evidence") / a.label
out.mkdir(parents=True,exist_ok=True)
si,pi=STARTUPINFO(),PROCESSINFO()
si.cb=C.sizeof(si)
si.desktop="winsta0\\default"
si.flags=1
si.show=1
cmd=C.create_unicode_buffer('"'+str(root / "oriwotw.exe")+'" -screen-fullscreen 0 -screen-width 1280 -screen-height 720 -logFile "'+str((out / "game.log").resolve())+'"')
if not CreateProcess(str(root / "oriwotw.exe"),cmd,None,None,False,0,None,str(root),C.byref(si),C.byref(pi)):
raise C.WinError(C.get_last_error())
result={"pid":pi.pid,"game":str(root),"log":str((out / "game.log").resolve()),"debugger":False,"auto_close":False}
(out / "launch.json").write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding="utf-8")
print(json.dumps(result,ensure_ascii=False))
Close(pi.thread)
Close(pi.process)
@@ -0,0 +1,26 @@
"""Test affinity only on this short-lived Python worker thread."""
import ctypes as c
from ctypes import wintypes as w
import json
import threading
k=c.WinDLL("kernel32",use_last_error=True)
k.GetCurrentThread.restype=w.HANDLE
k.GetCurrentProcess.restype=w.HANDLE
k.GetProcessAffinityMask.argtypes=[w.HANDLE,c.POINTER(c.c_size_t),c.POINTER(c.c_size_t)]
k.SetThreadAffinityMask.argtypes=[w.HANDLE,c.c_size_t]
k.SetThreadAffinityMask.restype=c.c_size_t
def worker():
pm,sm=c.c_size_t(),c.c_size_t()
k.GetProcessAffinityMask(k.GetCurrentProcess(),c.byref(pm),c.byref(sm))
print(json.dumps({"process_mask":hex(pm.value),"system_mask":hex(sm.value)}))
for mask in [1,2,3,4,8,0xc,0xf,0xfc,0xfff,0xffffc,0xfffff,0xfffffffc]:
c.set_last_error(0)
old=k.SetThreadAffinityMask(k.GetCurrentThread(),mask)
error=c.get_last_error()
print(json.dumps({"requested":hex(mask),"previous":hex(old),"error":error}))
if old:
if not k.SetThreadAffinityMask(k.GetCurrentThread(),old):
raise c.WinError(c.get_last_error())
t=threading.Thread(target=worker)
t.start()
t.join()
@@ -0,0 +1,322 @@
---
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。本文的实验结论来自本机记录,不把论坛上的类似症状或未经验证的猜测当作根因。
@@ -0,0 +1,410 @@
---
title: "《奥日 2》加载闪退排查记:六月靠猜,九月靠测"
date: 2026-09-23
slug: ori-wotw-crash-troubleshooting-story
description: "同一个闪退,我排查了两次。六月换显卡、禁声卡、刷系统更新,还把虚拟扬声器写成了根因;九月我换了套办法,最后改掉 8 个字节。这是一份两轮排查的对比复盘。"
tags:
- 游戏排障
- Windows
- 排查思路
- 逆向分析
draft: true
---
## 写在前面
这篇复盘想记录的不是"奥日 2 怎么修",而是**同一个故障,我用两套完全不同的办法排查了两次,结果差了多少**。
六月的我,靠"试"。九月的我,靠"测"。
中间隔着三个月,也隔着一次方法论上的重来。下面把两轮分开写,最后再对比它们差在哪。
> 补充一句诚实的说明:六月的记录里,只剩下我自己那一侧说过的话,完整的对话没有留存下来。所以写六月那一半时,我描述的是**我当时看到的现象、我的做法和最后的结果**,而不是去复述谁当时建议了什么。
---
## 一、问题:表现其实一直没变
先把故障说清楚。**六月和九月看到的是同一件事,所以我只写一遍。**
《奥日 2》(Ori and the Will of the Wisps)在 Windows 上有一个很不讲道理的表现:
- **加载画面里小精灵还在跑步,游戏突然闪退。** 没有报错弹窗,没有卡顿预警,就是"啪"一下回到桌面。
- **偶尔能进去,但完全没有声音。** 更折磨人的是这种——你以为这次成功了,结果发现整个世界是静音的。
- 两个不同版本的安装目录,**都会崩**。
- 同一台机器上的 VMware 虚拟机,**却能正常玩**。
最后这条最让人抓狂:同一个系统版本,虚拟机跑得起来,宿主机跑不起来。这几乎在暗示"问题出在宿主机的某个环境差异上",但又说不清是哪个。
就是这样一件事,我折腾了三个月,甚至一度退了款——游戏文件倒是还留在硬盘上。
**关键点是:这两个月里,故障本身没有任何变化。** 变化的是我钻进去的方式。
---
## 二、上半场:六月 —— 我用"试"的办法
### 2.1 当时的思路:可疑就试
六月那轮的基本路数是"列清单、逐个试"。手上的候选办法很多,而且每一个单独看都挺有道理:
- 更新显卡驱动(后来又用 DDU 彻底重装了两次)
- 换显卡——直接把 RX 9070 XT 禁用,改用 Tesla P4 跑
- 禁用可疑的虚拟音频设备
- 调整音频接口和采样率
- 限制 CPU 可用核心(试过好几个配置)
- 跑 DXVK,换掉图形渲染路径
- 刷系统更新
每一个都试了。每一个都没解决。
### 2.2 六月其实摸到过正确方向
这部分现在回头看最有意思:**六月并不是完全没碰到点上。**
6 月 30 日,我当时已经把这条线索交给了 AI 要求独立分析:
> Wwise 创建流式 I/O 设备失败
也就是说,"音频引擎初始化"这个方向,六月的我**已经看到了**。
但问题在于:**我没有把它变成任何一条可以核对的东西。**
它停在了"我知道这里报错了"这个层面。往下本该问的是:
- 这个失败发生在**哪个函数**?
- 调用时的**参数**是什么?
- **返回值**和**错误码**是什么?
- 失败的**下一步**程序做了什么?
这四个问题,六月一个都没问。
### 2.3 六月结出的果:一个写进文档的错误根因
六月那轮最后留下的东西,是我从回收站里翻出来的一份记录。上面白纸黑字写着:
> **Steam Streaming Speakers 虚拟扬声器**干扰了 Wwise 音频引擎的设备枚举。
>
> 随后:虚拟扬声器返回无效参数 → 流式 I/O 设备创建失败 → 虚函数表损坏 → 跳转到堆地址。
>
> **有效修复(关键操作,勿还原)**
我把禁用这个虚拟扬声器,**当成根因和有效修复写进了文档**,还标注了"勿还原"。
而紧接着的记录是:能进游戏了,**还是没有声音**;再开一次,**又闪退**。
这就是问题的核心:**一个本来就间歇性发生的故障,你改点什么,下一次它恰好没崩——这什么也证明不了。**
"禁用扬声器后能进""禁用两个服务后连续两次能进""锁核后能进去——但没声音",这类记录在六月出现了好几次。它们最多说明"这个改动可能影响了触发概率",绝推不出"这个设备就是根因"。
更糟的是,那条"虚函数表损坏"的链条,**没有任何一条设备枚举返回值或者 API 跟踪来支撑它**。它听起来很专业,但它是猜的。
### 2.4 六月的代价
除了没修好,六月还付出了两笔额外的账:
**第一笔:用系统更新当诊断实验。**
6 月 16 日,两次系统更新后机器直接无法正常启动,靠系统还原才救回来;18 日更新终于成功,**游戏照崩不误**。
**第二笔:虚拟机对照混进了太多变量。**
虚拟机确实能出声,但旧日志显示:它先是因为 VMware 共享目录里的音频资源路径失败而无声,之后我又把游戏文件从网络路径拷到了本地。**"更新"和"换路径"是同时发生的,凭什么把功劳算给更新?**
六月这一轮,总结成一句话就是:
> **改了很多变量,但一个都没控制住。**
---
## 三、下半场:九月 —— 我换成"测"的办法
三个月后重新开始,这次定了一条规矩:**先别动手,先看证据。**
下面五步,是六月那四个问题(哪个函数/什么参数/什么返回值/失败之后怎样)一步一步补上答案的过程。
### 第 1 步:把日志里的"噪音"和"重复故障"分开
Unity 日志里塞满了警告,比如大量 `The referenced script ... is missing`。它们看起来很吓人,但数量多、又不指向具体崩溃点,**基本是噪音**。
真正有区分力的,是**重复出现**的那两条:
```text
Wwise: Cannot create streaming I/O device.
WwiseUnity: Failed to initialize the sound engine. Abort.
```
我把手上留存的崩溃报告做了分类:**29 份里,28 份都走到了同一类 Wwise 音频工作线程的崩溃路径**,剩下 1 份是之前试 DXVK 时的图形错误。
**这里的转折点是:不要找"最显眼的报错",要找"每次都出现的报错"。** 一个重复 28 次的现象,比一个新奇的一次性报错有价值得多。
> ⚠️ 顺便澄清一个坑:`streaming I/O device` 里的 I/O 是 Input/Output,指的是**音频引擎内部读取声音数据的机制**,不是设备管理器里那块物理声卡。如果一上来就理解成"声卡坏了",就会像六月一样围着音频驱动打转很久。
(另外要诚实:28/29 只是**留存崩溃样本**的分类,不是启动失败率——成功的启动不会留下崩溃报告。)
### 第 2 步:别轻信崩溃栈里给出的函数名
原始报告把出错的栈帧标成了 `AK::StreamMgr::SetFileLocationResolver`,看起来像"文件位置解析"出了问题。
但没有完整调试符号(PDB)时,工具只能**拿邻近的导出符号名去猜地址**。这个名字不能证明真正执行的就是那个函数。
我算了模块相对地址(RVA)再反汇编,发现真正值得注意的位置是:
```text
AkSoundEngine.dll + 0x5B0C8
AkSoundEngine.dll + 0x5B16E
```
这两处**紧跟在虚函数间接调用之后**。而崩溃时线程想跳转的目标地址,**根本不在正常模块的代码区里**。
还有个细节:Unity 的文本报告写的是"写入冲突(Write to location)",但转储文件里 `ExceptionInformation[0]` 的值是 **8**。按微软文档,参数为 8 表示**用户态数据执行保护(DEP)违规**——也就是"试图执行一段不允许执行的内存"。
"写数据写到坏地址"和"跳到坏地址去执行"是完全两回事。前者像内存问题,后者更像**对象已经失效、取到了一个错误的调用目标**。
**这里的转折点是:文本日志和二进制转储打架时,去看转储。**
### 第 3 步:抓"真正失败的那个调用"
到这一步,我只知道"它跳错了地方",不知道"从哪一步开始错的"。
所以我写了个诊断脚本:让它在自己启动的游戏子进程里,在音频线程初始化附近下临时断点,**记录参数、返回值,以及目标线程自己的错误码**。
原始运行抓到了这样一组数据:
```text
线程优先级: 0
线程核心掩码: 0xFFFFFFFC
线程栈大小: 128 KiB
SetThreadPriority 返回 1 → 成功
SetThreadAffinityMask 返回 0 → 失败
Windows 错误码 87 → ERROR_INVALID_PARAMETER
随后发生 0xC0000005 → 访问冲突
```
`SetThreadAffinityMask` 的作用是告诉系统"这个线程只准在指定的这些 CPU 核心上跑"。掩码里每一位代表一个逻辑处理器。
**这一步补上的,是六月缺的那三个答案**:哪个函数(第 2 步)、什么参数、(第 3 步)、什么返回值(第 3 步)。
### 第 4 步:把假设拿去做实验,而不是当成结论
有了错误码,自然就有了假设:游戏请求的掩码是 `0xFFFFFFFC`,而机器进程报告可用的只有 `0xFFFFF`——**是不是请求了不允许的高位?**
这个假设很有诱惑力,因为它能直接对上微软文档里"掩码包含不允许的位会返回 `ERROR_INVALID_PARAMETER`"。
但实验结果是:
| 实验 | 结果 |
| --- | --- |
| 把掩码从 `0xFFFFFFFC` 截断到 `0xFFFFC` | **仍然失败**,还是错误 87 |
| 写个独立小程序,给自己的线程设同样的掩码 | **成功** |
所以,"高位越界"这个解释**被我自己的实验否掉了**。
这里是九月和六月最大的区别之一:**六月的假设只要"下一次没崩"就能存活;九月的假设必须扛住反例。**
到这里,我承认了一个还没解决的问题:**宿主环境为什么偏偏拒绝这个游戏线程的绑定,我解释不了。** 不是"核心太多",不是"CPU 坏了",也不是某个声卡驱动。
### 第 5 步:反汇编,看"失败之后"发生了什么
既然定位不到"为什么失败",就换个问题问:**失败之后,程序做了什么?**
反汇编这段逻辑之后,看到了一个要命的顺序:
```cpp
thread = CreateThread(/* 创建后立即可运行 */);
if (!ConfigurePriorityAndAffinity(thread)) { // 这里失败了(错误 87)
CloseHandle(thread);
thread = nullptr; // 句柄被清空
return InitializationFailed;
}
// ……后续清理阶段
if (thread != nullptr) { // 句柄是空的,于是跳过等待
WaitForSingleObject(thread, INFINITE);
}
// 然后,销毁线程还在用的那些对象
```
关键点在于:**关闭线程句柄 ≠ 结束线程。**
句柄可以理解成"访问系统对象的一张票"。把票撕了,线程还在跑。微软的 `CloseHandle` 文档写得很清楚:关闭线程句柄**不会**终止对应线程。
于是整个链条闭合了:
```mermaid
sequenceDiagram
participant M as 初始化线程
participant W as 音频读取线程
M->>W: CreateThread,立即可运行
M->>M: SetThreadPriority 成功
M->>M: SetThreadAffinityMask 失败(错误 87)
M->>M: CloseHandle 并清空句柄
Note over M,W: 关掉句柄不会让线程停下来
M->>M: 初始化失败,进入清理流程
Note over M,W: 句柄没了,后面的等待被跳过
W->>W: 通过已经失效的对象做间接调用
W->>W: 目标地址不可执行 → 0xC0000005
```
**这也顺带解释了那个"偶尔进得去但没声音"的诡异现象**:线程调度和内存复用有时间差。初始化失败时,声音引擎处于未就绪状态→没声音;而那个还在跑的线程会不会马上撞上失效地址,要看时机——撞上就闪退,没撞上就放你进去,但依然是静音的。
一个机制,两种表现。到这一步,之前所有零散的现象第一次被同一套解释串起来了。
### 最后改动:8 个字节
既然触发点是"音频线程显式绑定核心失败",那修复思路就很自然:**别让它显式绑定。**
有意思的是,这个 DLL **自己就有**这个分支——它的线程创建逻辑里,掩码为 0 时会直接跳过 `SetThreadAffinityMask` 调用,走正常的系统调度。
所以补丁不是被"发明"出来的,而是**利用了程序里本来就存在的语义**:
```cpp
// 原来的导出函数(简化)
if (properties != nullptr)
properties->dwAffinityMask = suppliedMask;
// 补丁后
if (properties != nullptr)
properties->dwAffinityMask = 0; // 0 = 不显式绑定
```
这里的 `0` **不是"绑到零个核心"**,而是"DLL 看到 0 就不调用绑定 API"。
具体改动:
```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 音频管线里送了声音信号**。
**但我也必须说清楚这个补丁没有解决什么:**
- 我**没有**搞清楚宿主机为什么拒绝这个游戏线程的绑定。这是绕过触发条件,不是根因修复。
- 没有完成长时间、全流程的游玩测试。
- 取消显式绑定可能改变性能特征(显式绑定时可能有它的性能目的)。
- 它**不是**"奥日 2 闪退通用补丁"。只对**特定 DLL 构建的特定故障路径**有效——安装器会校验完整 SHA-256,哈希对不上就拒绝执行。
---
## 四、两轮差在哪
### 4.1 一张对比表
| | **六月** | **九月** |
| --- | --- | --- |
| 核心动作 | 试 | 测 |
| 故障描述 | "加载闪退、没声音" | 同一类 Wwise 线程崩溃,28/29 |
| 对日志的处理 | 看最显眼的报错 | 找重复出现的报错 |
| 对函数名的态度 | 直接采信文本栈 | 算 RVA 反汇编,自己确认 |
| 有没有抓到失败调用 | ❌ 没有 | ✅ 错误 87,参数、返回值齐全 |
| 假设怎么验证 | 改了之后能不能进 | 截断掩码仍失败 → 假设被否 |
| 失败路径 | 没往下看 | 反汇编看到句柄清空、等待被跳过 |
| 结论性质 | 猜的根因(虚拟扬声器) | 有对照支撑的兼容修复 |
| 变量控制 | 同时改好几项 | 一次只改一个 |
| 系统改动 | 刷了系统更新,机器刷崩过 | 全程没动系统 |
| 现象方向 | 六月就摸到了 Wwise(6/30) | 沿着它继续钻到调用层 |
注意表格倒数第二行和第一行的对比——**六月其实站对了门口,只是没进去。**
### 4.2 我的提问变了
这是我觉得两轮之间最实质的差别。
六月我问 AI 最多的句式是:**"怎么办?"**
——于是得到一张清单,然后逐个试。
但六月也不是没较真过,有一句我印象很深:
> 为啥是显卡驱动本身导致的崩溃?依据何在?
**我当时已经在追问依据了。** 可问题在于:那一轮里,"依据"这个词落不到实处——因为没有保存日志、没有控制变量、没有失败调用的返回值,**所谓"依据"最后只能变成"我改了之后确实能进了"。**
九月我问的句式变成了:
> 这里**哪一步最早失败**?
> 这个失败**怎么验证**?
> 改了之后,**那个具体的失败调用消失了没有**?
提问从"要办法"变成"要证据链",排查才开始收敛。
所以老实说,**光是"问得更尖锐"还不够**——六月我也问过"依据何在"。真正让这一轮收敛的是配套的做法:先存证据、一次改一个变量、用返回值而不是"这次能进"判定成败。
### 4.3 关于"AI 的进步"
如果要说这两轮之间有什么"进步",我更愿意把它准确地描述成**排查方法的进步**,而不是一句笼统的"AI 变强了":
- **六月的失败模式,其实是最典型的一种:把"相关性"当成了"因果"。** 改一个设置,恰巧下一次没崩,于是宣布修复。这个陷阱跟工具无关,换谁来都容易掉进去。
- **九月之所以能走通,是因为换了问题。** 不再是"哪个设置能修好它",而是"它在哪一步最早的失败、我怎么验证这一点"。问题一换,手段自然跟着变——动态调试、反汇编、对照实验,都是被这个问题逼出来的。
- **而方法一旦对了,AI 的价值确实不一样了。** 六月它陪着我列清单、逐个试;九月它能配合我下断点读寄存器、算 RVA 反汇编、设计"截断掩码"这种专门用来否掉假设的对照实验,最后还帮着写了一个带哈希校验和还原功能的补丁安装器。
**同一件事,六月做成了"穷举",九月做成了"收敛"。** 差别不在候选办法的多少——六月办法更多——而在于**有没有一套机制,把几十种可能性一路砍到剩下一条**。
---
## 五、如果重来一次
这是这篇复盘真正想留下的东西。
**1. 先存证据,再动手。**
改任何设置之前,把日志复制出来。Unity 崩溃报告一般在 `%LOCALAPPDATA%\Temp\Moon Studios\...\Crashes`,常规日志在 `%USERPROFILE%\AppData\LocalLow\Moon Studios\...`。**每次启动都会覆盖上一次的日志**——这是我六月代价最大的地方。
**2. 找"重复出现的错误",而不是"最吓人的错误"。**
重复 28 次的报错,比一次性的显眼报错有价值得多。
**3. 一次只改一个变量。**
六月的失败不是"不够努力",是"变量太多"。改了驱动又改服务又改采样率,最后谁也说不清是哪一步起了作用——包括我自己。
**4. 用返回值判断,别用"这次能进"判断。**
本来就间歇性发生的故障,改完能进一次,等于什么都没证明。**要看的是"那个具体的失败调用是否消失了"。**
**5. 相信转储,多于相信文本。**
文本说"写入冲突",转储说"执行违规"——两条线索指向完全不同的方向。二进制不会为了可读性美化自己。
**6. 反例出现时,立刻降低旧假设的权重。**
换显卡还是崩、更新驱动没解决、进去之后依然无声——这些反例一出现,就该把"显卡是主因"的优先级往下压,而不是继续找理由维持它。
**7. 系统级改动是最后手段,不是诊断工具。**
为修一个游戏去刷系统更新,代价是机器开不了机、靠还原救回来。第一次严重失败后就该停下来,重新划定边界。
**8. 允许自己承认"不知道"。**
"为什么这个线程的绑定会失败"我至今没答上来。但**知道"哪个调用失败了、绕开它之后是否恢复正常",已经足够修好问题**。承认边界不丢人;把猜测写成结论,才会带着你在错的方向上跑三个月。
---
## 最后
这次排障最有价值的收获,不是那 8 个字节,而是把一个模糊的"加载闪退",一步步拆成了可以观测、可以对照、可以复现的东西:
**哪个线程 → 哪个调用 → 什么参数 → 什么返回值 → 改动之后,在普通启动下是否依然成立。**
六月和九月,看的是同一个故障,走的是两条路。
**排障真正的困难,从来不是"办法不够多",而是判断哪一个是针对你这次故障的。**
---
*本文结论来自本机记录。文中 C++ 风格代码是为解释控制关系而写的伪代码,不是厂商源码。所有关键地址、参数、错误码和验证数据均可复核。*
@@ -0,0 +1,480 @@
# 奥日 2 加载闪退与无声:Wwise 线程亲和性失败的定位与兼容补丁
研究日期:2026-09-16(UTC+8)
研究对象:Windows 版《Ori and the Will of the Wisps》
状态:已定位直接触发链,完成兼容补丁及本机启动、音频输出验证。用户确认最终普通启动是主动关闭。
## 1. 结论及其边界
本机出现的主要故障位于 Wwise 音频引擎的线程初始化阶段:`SetThreadAffinityMask` 调用失败,返回错误 87;随后初始化失败路径关闭并清空线程句柄,而已经启动的线程仍可能继续运行。历史崩溃的异常位置和动态跟踪都与这一失败路径吻合。
兼容补丁将游戏通过指定导出函数设置的音频线程亲和性掩码改为 0。该 DLL 的现有分支把 0 当作“不进行显式绑定”,因此线程交由 Windows 调度,避开失败路径。补丁没有禁用声音,也没有改变系统可用核心数量。
验证结果:
- 排查前留存的 29 份崩溃报告中,28 份落在同类 Wwise 读取线程路径;另 1 份为此前 DXVK 尝试中的 DXGI 崩溃。
- 原版在动态跟踪中重现:优先级设置成功、亲和性设置失败、紧随其后访问冲突。
- 将请求掩码截断到进程报告的可用核心范围,仍然失败。
- 将亲和性配置改为 0 后,音频初始化和后续场景加载通过。
- 写入磁盘补丁后,不带调试器的普通启动也通过;Windows Core Audio 检测到活动、未静音的游戏音频会话和非零音频电平。
- 两个游戏目录均已安装并校验补丁。运行验证集中在版本 A;版本 B 本轮仅完成文件匹配、安装及校验。
**尚未证明的部分:**为什么宿主环境拒绝该游戏线程的亲和性设置,仍未进一步确定。独立测试线程可以接受相同掩码,所以不能把原因直接归为“掩码越界”“核心太多”“ES 处理器缺陷”或某个驱动。对已经释放或失效对象的使用有强证据支持,但本轮没有跟踪每一次内存分配与释放,不能声称完整证明了具体分配器层面的生命周期。
这是针对已确认触发条件的 **workaround(兼容性绕过)**,不是对 Wwise 源码进行完整的线程生命周期修复,也不是所有奥日 2 闪退的通用补丁。
## 2. 故障现象与最初线索
用户描述:加载画面中小精灵跑步时经常闪退;极少数可以进入游戏,却没有声音。此前已尝试限制 CPU 核心、禁用部分声卡或显卡,没有解决。两个游戏目录都出现问题。同系统版本的 VMware 虚拟机可以运行,其中使用的应该是版本 A。
这些信息最初只用于确定调查顺序:
| 线索 | 可以支持的判断 | 不能直接推出的结论 |
| --- | --- | --- |
| 闪退伴随偶尔无声 | 值得检查音频初始化是否共同失败 | 一定是物理声卡驱动损坏 |
| 两个版本都失败 | 应比较共同组件、用户配置及宿主环境 | 两份游戏所有文件都相同 |
| 同系统版本虚拟机可玩 | 环境差异值得调查 | 系统版本、显卡或 CPU 已被单独证明有问题 |
| 限制核心数未解决 | 简单减少游戏可用核心不是已验证方案 | 所有线程调度相关假设均被排除 |
虚拟机的具体 vCPU 拓扑、VMware 版本、音频驱动及游戏文件哈希,本轮没有直接采集。其可运行情况属于用户报告,不是严格控制变量实验。
## 3. 本机环境
### 3.1 系统、处理器与固件
| 项目 | 采集结果 | 来源与解释 |
| --- | --- | --- |
| 操作系统 | Windows 10 Pro for Workstations,22H2,x64 | Windows 注册表;中文名称为专业工作站版 |
| 系统构建 | `19045.6456` | `CurrentBuild` 与 `UBR` |
| Windows 处理器名称 | `Genuine Intel(R) 0000` | 处理器注册表字段 |
| CPU 标识 | `Intel64 Family 6 Model 186 Stepping 2` | 同上 |
| 固件系统产品名 | `i9-13900HK(ES)` | SMBIOS 对应注册表字段;不等于已独立鉴定零售型号 |
| 当前可用逻辑处理器 | 20 | .NET 运行时与进程亲和性查询;不是物理核心数 |
| 后续对照测试的进程/系统掩码 | `0xFFFFF / 0xFFFFF` | `GetProcessAffinityMask` |
| 物理内存 | 约 32 GB;崩溃报告记载 32600 MiB | 原始崩溃日志;本轮没有采集内存条型号、数量或频率 |
| 主板制造商字段 | `Intel` | 固件字段原样记录 |
| 主板产品字段 | `Z790M GAMING WIFI D5` | 固件字段原样记录 |
| BIOS 厂商 | American Megatrends International, LLC. | 固件字段 |
| BIOS 版本/日期 | `Leo-Patch 1.5` / `07/26/2024` | 固件字段 |
ES 表示 Engineering Sample,即工程样品标识。本报告记录这一环境特征,不据此归因。也没有从“20 个逻辑处理器”推算未经验证的物理核心数或大小核分布。
### 3.2 显示环境
游戏日志明确选择了 **AMD Radeon RX 9070 XT**,使用 Direct3D 11、功能级别 11.1,显存报告为 16188 MiB。显卡初始化已完成,不能仅凭机器装了多个显示组件就断定它们参与了这次崩溃。
| 注册的显示组件 | 驱动版本 | 驱动日期 | 说明 |
| --- | --- | --- | --- |
| AMD Radeon RX 9070 XT | `32.0.31041.1004` | 2026-08-17 | 本次游戏日志中的渲染设备 |
| NVIDIA Tesla P4 | `31.0.15.3770` | 2023-10-18 | 注册表中存在;未证明参与本次渲染 |
| Zako Display Adapter | `0.23.25.226` | 2026-04-25 | 注册表中存在 |
| Microsoft Remote Display Adapter | `10.0.19041.5794` | 字段为 2006-06-21 | 存在多个注册项,不代表同时连接多个活动设备 |
### 3.3 音频环境
| 注册的音频组件 | 驱动版本 | 驱动日期 |
| --- | --- | --- |
| Realtek High Definition Audio | `6.0.9239.1` | 2021-09-16 |
| AMD High Definition Audio Device | `10.0.1.41` | 2025-05-26 |
| Steam Streaming Speakers | `17.56.13.764` | 2017-07-20 |
| VB-Audio Virtual Cable | `3.3.1.7` | 2024-10-07 |
| AMD Streaming Audio Device | `1.0.0.14` | 2024-06-05 |
| Microsoft Streaming Service Proxy | `10.0.19041.1` | 2006-06-21 |
这里列的是注册信息,其中可能包含停用、断开或历史设备。本轮没有把某一虚拟音频设备认定为根因,也没有通过卸载驱动来实现修复。
环境采集脚本为 `ori-research/Collect-Environment.ps1`,快照为 `ori-research/environment.json`。它不采集账号名、序列号、许可证或网络地址。
### 3.4 两个游戏版本
| 项目 | 版本 A:目录名含空格 | 版本 B:目录名含下划线 |
| --- | --- | --- |
| Unity 引擎 | `2018.4.11f1 (0dbcfe58b2dc)` | `2018.4.24f1 (5ebab45375c4)` |
| 日志 Build Id | `5257` | `8978` |
| 日志 Project Revision | `140897` | `147855` |
| 日志 Revision Time | `2020-05-12 03:56:26` | `2020-11-18 06:35:46` |
| GameAssembly.dll 大小 | 74,333,696 字节 | 77,572,608 字节 |
| AkSoundEngine.dll 大小 | 2,719,232 字节 | 2,719,232 字节 |
| Wwise SDK | `2019.1.7 Build 7135` | `2019.1.7 Build 7135` |
| 音频 DLL 原始 SHA-256 | 两者相同,见第 8 节 | 两者相同,见第 8 节 |
| 本轮验证覆盖 | 动态故障复现、内存修正、普通启动、音频会话 | 补丁文件匹配、安装及校验 |
Build Id 和 Revision Time 来自日志,不能直接当作游戏平台销售版本号。不能只凭 Unity 或 Wwise 版本字符串判断另一个文件是否适用补丁,必须核对完整哈希。
## 4. 从日志中提取真正有区分力的证据
Unity 日志中出现了大量 `The referenced script ... is missing`、资源卸载信息和其他警告。它们数量多,却没有形成此次崩溃的直接证据链。
最有区分力的是重复出现的两条错误:
```text
Wwise: Cannot create streaming I/O device.
WwiseUnity: Failed to initialize the sound engine. Abort.
```
这里的 I/O 是 Input/Output,即输入输出。“streaming I/O device”指音频引擎内部读取声音数据的机制,不等于用户在设备管理器里看到的物理声卡。
崩溃栈大体如下:
```text
<unknown address>
AkSoundEngine.dll + 0x5B0C8 或 + 0x5B16E
KERNEL32!BaseThreadInitThunk
ntdll!RtlUserThreadStart
```
这表明故障发生在音频 DLL 启动的工作线程上,而不是把某一行普通 Unity 警告简单等同于原因。
### 4.1 导出符号名称可能误导
原始报告把音频栈帧标成 `AK::StreamMgr::SetFileLocationResolver`。没有完整 PDB 调试符号时,工具可能用附近的导出符号解释地址;这个名称不能证明真正执行的函数就是文件位置解析器。
本轮进一步计算模块相对地址并反汇编,才发现 `+0x5B0C5` 和 `+0x5B16B` 附近是虚函数间接调用。`+0x5B0C8`、`+0x5B16E` 分别是这些调用后的返回地址。
### 4.2 文本中的“写入冲突”也需要交叉检查
2026-09-16 原始 `error.log` 写的是 `Write to location ... caused an access violation`。但对应 `crash.dmp` 的异常记录为:
```text
ExceptionCode = 0xC0000005
ExceptionInformation[0] = 0x8
ExceptionInformation[1] = 与异常指令地址相同
```
根据微软的 `EXCEPTION_RECORD` 文档,参数 0 为 0、1、8 时,分别对应读取、写入、用户态 DEP 执行保护违规。本例是最后一种。[4]
结合栈中的间接调用和不属于正常模块代码区的目标地址,更符合“通过失效对象取得了错误的调用目标”,而不是仅仅向某个地址写数据。DEP 是 Data Execution Prevention,即数据执行保护;此次修复保留这个保护机制。
## 5. 动态实验设计及结果
诊断器使用 Windows 调试接口启动自己的子进程,没有附加到其他游戏进程,也没有安装系统钩子。它先校验原始 DLL 的 SHA-256,再在模块加载后设置软件断点。
软件断点把目标指令的首字节临时替换为 `INT3`。断点触发后读取寄存器,恢复原字节并把 RIP 指令指针退回原指令,使原指令正常执行。本轮各断点只捕获首次命中,不构成长期、多实例线程调用的完整追踪器。
Windows x64 调用约定把前几个整数或指针参数放在 RCX、RDX、R8、R9,返回值通常在 RAX。[5] 因此可在 `SetThreadAffinityMask` 前读取 RDX 中的掩码,在调用后读取 RAX。错误码通过目标线程 TEB 的 LastErrorValue 读取,而不是读取调试器自身的 `GetLastError`;本次 x64 TEB 偏移为 `0x68`,它属于诊断实现细节,不应推广为跨架构稳定接口。
### 5.1 实验对照
| 实验 | 修改内容 | 观察结果 | 意义 |
| --- | --- | --- | --- |
| 原始 DLL | 无业务逻辑修改,仅断点跟踪 | 优先级调用返回 1;亲和性调用返回 0,错误码 87;随后访问冲突 | 确认直接失败调用 |
| 截断掩码 | 调用前将 `0xFFFFFFFC` 改为 `0xFFFFC` | 仍返回 0、错误 87,随后崩溃 | 单纯去掉高位不是本机充分修复 |
| 自动调度 | 在进程内将指定 setter 写入的掩码设为 0 | 读取线程属性变为 `[0, 0, 131072]`,通过初始化及后续加载 | 支持回避显式线程绑定 |
| 独立线程探针 | 仅修改探针自身线程的亲和性,并恢复 | 包括 `0xFFFFC`、`0xFFFFFFFC` 在内的测试值返回成功 | 不能将失败概括为全系统普遍不支持 |
| 磁盘补丁后普通启动 | 无调试器、无运行时补丁 | 初始化通过,进入后续场景,检测到声音信号,用户主动退出 | 排除仅在调试器下才成功的情况 |
原始记录的线程参数为:
```text
nPriority = 0
dwAffinityMask = 4294967292 = 0xFFFFFFFC
uStackSize = 131072 = 128 KiB
SetThreadPriority -> 1
SetThreadAffinityMask -> 0
LastErrorValue -> 87 = ERROR_INVALID_PARAMETER
后续异常 -> 0xC0000005
```
微软文档规定线程亲和性应是进程允许范围的子集,并说明不允许的位可导致 `ERROR_INVALID_PARAMETER`。[3] 这给出了最初的合理假设。但本机截断实验仍失败,独立探针又成功,所以研究结论必须保留这些反例,不能只照文档把掩码越界写成最终根因。
### 5.2 时间和样本限制
最初 40 秒的跟踪没有等到音频模块加载,不能把这轮算作成功或失败。用户曾将游戏切到后台,可能影响加载推进。因此,本轮耗时不能用于比较启动性能。
自动调度实验总观察上限为 180 秒,其中从音频模块加载到脚本按计划停止约 118.6 秒;不是连续完整游玩 3 分钟。最后的普通启动没有自动关闭计时器,用户确认是自己关闭的。
29 份历史报告是已产生的崩溃样本,不包含全部启动尝试,且部分来自此前不同排障设置。`28/29` 是崩溃路径分类,不能当作游戏启动失败率,也不能据此计算补丁治愈率。
## 6. 线程生命周期问题的原理
### 6.1 关键静态位置
以下地址都是 RVA(Relative Virtual Address,模块相对虚拟地址),不是固定的运行时绝对地址:
| RVA | 观察到的操作 |
| --- | --- |
| `0x5B090` | 工作线程入口附近 |
| `0x5B0C5` | `call qword ptr [rax+0x18]` |
| `0x5B16B` | `call qword ptr [rax+0x10]` |
| `0x5B294` | 调用 `CreateThread`,创建标志为 0 |
| `0x5B2B9` | 调用 `SetThreadPriority` |
| `0x5B2BF` | 检查优先级调用结果 |
| `0x5B2D8` | 调用 `SetThreadAffinityMask` |
| `0x5B2DE` | 检查亲和性调用结果 |
| `0x5B2E3` 至 `0x5B2ED` | 失败路径关闭线程句柄并清零 |
| `0x5B450` 附近 | 清理函数;线程句柄存在时才等待线程 |
`CreateThread` 没有使用 `CREATE_SUSPENDED`。微软文档明确说明这种情况下线程可能在 `CreateThread` 返回前就开始执行。[2]
### 6.2 为什么关闭句柄不等于停止线程
句柄可以理解为访问系统对象的一张票。关掉票并不会让线程立即停止。微软 `CloseHandle` 文档也明确指出:关闭线程句柄不会终止对应线程。[1]
下面是从已观察控制流抽象出的伪代码,不是厂商源码,也不包含所有字段和分支:
```cpp
threadHandle = CreateThread(/* 创建后立即可运行 */);
if (!ConfigurePriorityAndAffinity(threadHandle)) {
CloseHandle(threadHandle);
threadHandle = nullptr;
return InitializationFailed;
}
// 后续清理函数中的关键关系
SignalStopEvent();
if (threadHandle != nullptr) {
WaitForSingleObject(threadHandle, INFINITE);
CloseHandle(threadHandle);
}
CloseSynchronizationEvents();
// 调用方之后可能销毁或复用线程依赖的对象。
```
问题在于失败路径让“有没有句柄”与“有没有存活线程”不再等价。句柄清空后,后续清理可能跳过等待,仍存活的线程继续访问已经失效的状态。
```mermaid
sequenceDiagram
participant M as 初始化线程
participant W as 音频读取线程
M->>W: CreateThread,立即可运行
M->>M: SetThreadPriority 成功
M->>M: SetThreadAffinityMask 失败,错误 87
M->>M: CloseHandle 并清空句柄
Note over M,W: 关闭句柄不会终止线程
M->>M: 初始化失败,进入清理
Note over M,W: 失去句柄后,后续等待可能被跳过
W->>W: 通过失效对象进行间接调用
W->>W: 目标地址不可执行,0xC0000005
```
静态代码直接证明了句柄关闭、清零及清理等待条件;动态实验直接证明了调用失败和随后发生的访问冲突。对具体对象已经被释放或复用的判断属于由这些证据强烈支持的生命周期推断,而不是本轮完整记录过的每一次内存管理操作。
### 6.3 为什么偶尔进入却无声
线程调度顺序和内存复用存在时间差。如果初始化失败,声音引擎可以处于未就绪状态;异常线程不一定在同一时刻撞上不可执行地址。这为“经常加载闪退、偶尔进入但无声”提供了统一且与实验吻合的解释。
它仍然不意味着所有无声现象都属于本问题:设备选择、静音、声道或其他驱动故障需要各自的证据。
## 7. 补丁设计与精确变更
### 7.1 选择语义上已有的关闭绑定方式
目标函数:
```text
CSharp_AkThreadProperties_dwAffinityMask_set
```
它把 managed/native 边界传入的掩码写入线程属性的 `+4` 字段。导出表检查只发现这个导出名称指向该入口,没有发现其他导出名称与之共用入口。这减少了误改其他导出 setter 的风险,但不应误读为完成了整个 DLL 的所有内部引用证明。
原始线程创建逻辑对掩码 0 有直接分支:不调用 `SetThreadAffinityMask`,继续检查其余初始化条件。补丁利用的就是本 DLL 已经存在的逻辑。
### 7.2 地址、机器码和反汇编
| 项目 | 值 |
| --- | --- |
| PE 首选镜像基址 | `0x180000000` |
| 函数 RVA | `0x121F0` |
| 首选基址下的函数地址 | `0x1800121F0` |
| 文件偏移 | `0x115F0` |
| 校验/替换区域长度 | 16 字节 |
| 实际变化字节数 | 8 |
RVA 和文件偏移分别属于内存映射与磁盘布局,不能混用。运行时还存在 ASLR(Address Space Layout Randomization,地址空间布局随机化),因此动态工具使用“实际加载基址 + RVA”。
原始字节:
```text
48 85 C9 74 03 89 51 04 C3 CC CC CC CC CC CC CC
```
原始逻辑:
```asm
test rcx, rcx
je original_ret
mov dword ptr [rcx+4], edx
original_ret:
ret
```
补丁字节:
```text
48 85 C9 74 07 C7 41 04 00 00 00 00 C3 CC CC CC
```
补丁逻辑:
```asm
test rcx, rcx
je patched_ret
mov dword ptr [rcx+4], 0
patched_ret:
ret
```
变化的 8 个文件偏移为:`0x115F4`、`0x115F5`、`0x115F6`、`0x115F8`、`0x115F9`、`0x115FA`、`0x115FB`、`0x115FC`。
保留空指针检查,修改对应跳转距离。新指令使用原函数尾部的填充区域,未侵入从 `0x12200` 开始的下一段代码。此函数没有新增栈操作或额外寄存器保存逻辑。补丁后的候选文件已经再次反汇编,确认得到上述指令,而不只是做了十六进制替换。
### 7.3 影响范围
补丁影响此 DLL 内经该导出 setter 配置的 Wwise 线程亲和性,可能不止一个读取线程。它允许这些线程继承 Windows 和进程当前的调度限制,不保证覆盖 DLL 内所有绕过 setter 的直接赋值路径。
显式绑定可能本来有性能目的,取消后需要关注长时间游玩的音频稳定性和性能。本轮没有完成帧时间、音频延迟、休眠唤醒或长时间游戏测试。
补丁没有修改 Steam 接口、授权校验、存档格式、系统安全机制或全局处理器设置。
## 8. 安装器的防误用设计
只接受以下原始文件:
```text
SHA-256:
8F90A4A3856540CB97D03CA07B4BB10B7A08F7B4AB3C524BE2BEC1AFDD29FECA
```
补丁后结果必须为:
```text
SHA-256:
62B2079A988D23EB251D4E83366AC04068A6F47E0D38EFE92A76DF022826F9F0
```
安装器 `Ori-AudioFix.ps1` 的行为:
1. 检查游戏进程是否还在运行;修改前必须退出。
2. 对所有指定目录预检查完整哈希、原始指令和已有备份。
3. 在本地根据用户自己的 DLL 构造候选字节,不下载替代游戏 DLL。
4. 建立不覆盖既有备份的 `.before-ori-affinity-fix` 文件,并验证备份。
5. 写入同目录临时文件,验证结果哈希,再替换目标并复核。
6. 已安装相同补丁时跳过重复修改;未知文件或备份异常时停止。
7. 还原时只接受经过验证的原文件备份。
每个文件的替换使用同目录暂存和移动,减少中途写坏目标的风险;多个目录之间不是事务,不能承诺第二个目录失败时第一个自动回滚。输出会逐目录报告结果,备份可以分别用于恢复。
工作区副本已通过“安装 → 再次安装 → 状态检查 → 还原 → 原始哈希一致”的完整测试。分享版安装器还会要求读者明确指定自己的游戏目录。
## 9. 音频验证到底证明了什么
普通启动后的 Core Audio 会话检查结果为:
```json
{
"active": true,
"muted": false,
"volume": 1.0,
"peak_over_3_seconds": 0.010620015673339367
}
```
检查只读取该游戏进程的音频会话状态和电平,不录制声音、不访问麦克风。非零电平说明游戏实际向 Windows 音频管线输出了信号,比单纯看到菜单或没有错误日志更有说服力。
这不等于通过麦克风验证扬声器发声,也不替代用户的长期试听。用户已确认最后一次进程退出是自己关闭,不是再次闪退;本轮没有记录用户对全部音效、长时间游玩或所有关卡的验收。
## 10. 他人如何判断是否属于同一问题
症状相似只是入口,建议按下面顺序核对:
1. 保存原有日志,避免下一次启动覆盖。Unity 报告一般在 `%LOCALAPPDATA%\Temp\Moon Studios\OriAndTheWilloftheWisps\Crashes`;常规日志一般在 `%USERPROFILE%\AppData\LocalLow\Moon Studios\OriAndTheWilloftheWisps`。
2. 检查是否同时出现 Wwise streaming I/O 初始化失败,以及音频工作线程的访问冲突。
3. 对 `AkSoundEngine.dll` 执行 `Get-FileHash -Algorithm SHA256`,与第 8 节比对。
4. 只有哈希匹配时才使用本补丁;未知版本重新定位函数、布局和控制流。
5. 在正常启动方式下验证加载、声音和退出,再延长实际游玩观察时间。
不应只凭 `0xC0000005` 判断适用:这是通用访问冲突代码,显卡、注入组件、坏内存或其他程序错误也可能产生它。
建议读者反馈:Windows 构建号、CPU 标识及逻辑处理器数、Unity/Wwise 版本、原始 DLL 哈希、关键日志、补丁前后结果和测试时长。无需公开完整转储、账号名、序列号或许可证。
## 11. 后续可继续研究的问题
- 查询失败游戏线程的处理器组、CPU Sets、相关调度状态及完整访问权限,解释为什么独立探针通过而游戏线程失败。
- 比较宿主与虚拟机的同一份 DLL、线程属性、API 返回值和环境,而不只比较操作系统版本号。
- 对对象分配、销毁和工作线程退出进行更细跟踪,严格验证释放后使用路径。
- 在版本 B 和其他环境中复现,并进行多次正常启动、长时间游玩和音频连续性测试。
- 若能修改源码,更完整的修复应保证配置失败时仍保留停止和等待线程的能力,确保线程停止后再销毁其依赖对象。是否把亲和性失败降级为非致命错误,要由音频引擎的设计约束决定。
这些属于后续研究议题,不是本轮已经完成的结论。
## 12. 证据索引与参考资料
本地证据:
| 路径(相对工作区) | 用途 |
| --- | --- |
| `ori-evidence/original-2026-09-16-error.log` | 原始崩溃文本 |
| `ori-evidence/runtime-original-long/trace.jsonl` | 原始调用失败的动态证据 |
| `ori-evidence/runtime-clamped/trace.jsonl` | 截断掩码仍失败的反例 |
| `ori-evidence/runtime-automatic/trace.jsonl` | 掩码 0 的临时修正实验 |
| `ori-evidence/normal-patched-1/game.log` | 普通启动的后续场景日志 |
| `ori-evidence/normal-patched-1/audio-status.json` | 音频会话测量 |
| `ori-research/environment.json` | 相关硬件、驱动与系统快照 |
| `ori-research/sources/manifest.json` | 官方文档下载结果和原始 URL |
原始转储本身保留在用户自己的崩溃目录,不放入公开分享包。前述异常参数来自对该转储异常流的只读解析。
官方资料于 2026-09-16 访问并保存本地文本快照:
1. [Microsoft:CloseHandle](https://learn.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-closehandle):关闭句柄与线程终止的区别。
2. [Microsoft:CreateThread](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createthread):创建标志及线程可能提前运行的时序。
3. [Microsoft:SetThreadAffinityMask](https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setthreadaffinitymask):掩码含义、返回值和错误条件。
4. [Microsoft:EXCEPTION_RECORD](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-exception_record):访问冲突参数 0、1、8 的含义。
5. [Microsoft:x64 calling convention](https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170):参数寄存器与返回值。
6. [Microsoft:System Error Codes 0–499](https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-):错误 87 的定义。
工具说明:使用本机已有 PowerShell 7.6.4、Python 3.14 标准库和 GNU objdump,未为此安装额外依赖。全文把本地实测、静态推断、用户报告和未验证事项分开记录,方便复核和后续补充。
## 13. 补充:限制 CPU 核心为什么不能与本补丁混为一谈
用户补充,网上曾看到“限制 CPU 核心”的解决办法。应把操作对象与证据层级分开记录:
| 操作 | 实际改变什么 | 本案例的证据 |
| --- | --- | --- |
| 限制整个游戏的进程亲和性 | 缩小游戏进程可使用的逻辑处理器集合 | 用户此前尝试未解决;保留的旧启动文件包含 `/affinity F`,即低 4 个逻辑处理器。本轮未做所有进程掩码的重新枚举实验 |
| 修改一条音频线程请求的掩码 | 调整 `SetThreadAffinityMask` 调用的参数 | 本轮实测把 `0xFFFFFFFC` 改成 `0xFFFFC`,仍返回错误 87,随后崩溃 |
| 让音频配置的掩码为 0 | 让该 Wwise DLL 走“不调用显式绑定 API”的既有分支 | 本轮临时修正及磁盘补丁后的普通启动均通过,检测到实际音频输出 |
`/affinity F` 中的 F 是十六进制 15,对应 4 个逻辑处理器,不能直接叫“4 个物理核心”。也不等于改了 BIOS、系统核心数或音频 DLL 中的掩码。
进程亲和性给出可运行集合;游戏自己创建线程后,仍可能再次设置线程亲和性。限制进程不会自动修正 DLL 的配置参数,也不会修复失败路径里的线程生命周期。某些设置还可能与线程显式请求范围发生冲突,需要查看实际 API 返回值。
限制核心在其他故障中可能通过降低并发、改变调度时序或绕开特定处理器拓扑起作用。这是一般原理层面的可能性,不是本机已经验证的原因;即使让竞争问题暂时不出现,也不能直接当作源码层面的修复。
**适合公开发表的准确结论:**“用户此前限制整个游戏可用核心未解决;本轮进一步证明,缩小音频线程请求掩码仍失败,而取消音频线程显式绑定有效。”不能改写成“限制 CPU 核心已经被证明对所有奥日 2 玩家无效”。
## 14. 方法归类:用于诊断与兼容性修复的逆向工程
本轮工作属于逆向工程:在没有原始源码的情况下,通过已编译程序的指令、导出表、运行行为和系统接口,推断局部实现及故障机制。
- **反汇编(静态分析):**将机器指令翻译成汇编,检查 `CreateThread`、绑定操作、句柄清理、条件分支和间接调用。本轮实际使用 GNU objdump。
- **动态分析:**让程序运行,在目标位置设置断点,读取寄存器、目标线程错误码和后续异常。
- **最小兼容性修补:**利用已经确认的函数边界及语义,只修改目标 setter,并验证正常启动和音频输出。
这不意味着恢复了完整 C++ 源码。报告中的 C++ 风格代码是解释控制关系的伪代码;原始变量名、类设计及全部调用方语义仍有缺失。PDB 符号不完整时,也必须警惕导出函数名被拿来解释邻近地址。
本次用途是定位故障、恢复兼容性和输出可复核的补丁;研究对象与改动都限于相关音频行为。结论的依据是日志、指令、输入参数、返回值和对照实验,每一步都可以保存与复核。
## 15. 网络方案核查:相同症状不等于相同故障
核查日期:2026-09-16。以下是公开资料核对,不代表本机执行过每一项办法。完整比较见工作区的《奥日2网络方案核查.md》;分享包中的文件名为 `COMMUNITY-REVIEW.zh-CN.md`。
| 方案 | 已读来源与证据 | 与本案例的关系 |
| --- | --- | --- |
| 限制 CPU 核心 | [性能教程](https://www.ghostarrow.com/how-to-improve-performance-in-ori-and-the-will-of-the-wisps)第 18 项针对游玩时接近 100% CPU 占用的卡顿,通过 Process Lasso 在阈值触发时减少核心 | 与本例启动时音频线程错误 87 不是同一份证据;本地三类亲和性操作的区别见第 13 节 |
| 切换声音输出、Sound 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 调整 | [RTX 5070 / 4K HDR 报告](https://steamcommunity.com/app/1057090/discussions/0/582806854239850133/)一次改变多项显示设置后成功 | 不能确定单项贡献;本机原版已在 1280×720 窗口调试中重现绑定失败,因此窗口化本身不够 |
| DXVK | [RTX 50 系列花屏帖](https://steamcommunity.com/app/1057090/discussions/0/695372304943750032/)作者更新报告图形破损消失 | 有图形兼容的成功报告;本机历史上有另一次 DXVK/DXGI 崩溃,不把它当音频修复 |
| 回退 Patch 2、检查系统预览版 | [官方 Patch 3.1](https://steamcommunity.com/app/1057090/discussions/0/2968397584537393710/)提供特定问题的回退分支;[开发者白屏帖](https://steamcommunity.com/app/1057090/discussions/0/3012312119189327120/)初步关联当时 Insider Dev 构建 | 是有条件的历史排障依据。本机为正式版 Win10 22H2,两份游戏的故障音频 DLL 又相同,不能因此断言重装或降级能修好 |
| 校验文件、修复依赖、调整驱动 | [启动排障清单](https://www.ghostarrow.com/ori-and-the-will-of-the-wisps-fix-wont-launch-black-screen-crashes)列出常见办法 | 对文件缺失、依赖加载或具体驱动回归可能有用;不能修正发布文件原有的线程清理逻辑 |
| 安装 Unity 引擎、各种“一键优化” | 同一启动清单提出安装 Unity,但没有给出相应机制或受控证据 | 游戏自带 Unity Player;无法从“使用 Unity 开发”推出“玩家必须安装编辑器”。作为通用修复依据不足 |
评价“玄学”时,指的是不区分场景、缺少对照却声称万能,而不是宣称某个设置永远不会影响程序。例如采样率或调度参数确实可能改变执行路径;但某一次成功,尤其多项修改后的成功,不足以确定因果。
[另一个无声讨论](https://steamcommunity.com/app/1057090/discussions/0/2968398851794293852/)中有采样率、设备、空间音频等多种建议,楼主却明确说未解决;数月后恢复,自己也不知道原因。因此不把建议列表误记为成功清单,也不把后来恢复直接归功于 Windows 更新。
公开页面留档位于 `ori-research/community-sources/`,获取结果见 `ori-community-articles-result.json`。本轮检索没有核对到同时包含本例相同 Wwise 调用、参数和错误 87 的独立复现报告;这不代表它不存在,也不构成首创声明。
@@ -0,0 +1,110 @@
# 奥日 2 闪退的网络方案核查:哪些有依据,哪些被说得太万能
核查日期:2026-09-16。本文比较公开资料与本机实验,不代表在本机逐一执行了所有网络方案。
**结论:同样叫“闪退”,可能发生在音频初始化、图形渲染、系统兼容或文件加载的不同位置。别人的成功记录可以帮助选择实验,但不能替代本机的故障证据。**
本机已经确认的直接触发条件是 Wwise 音频线程调用 `SetThreadAffinityMask` 失败,返回错误 87;随后进入危险的线程清理路径并发生访问冲突。取消相关线程的显式绑定后,普通启动和 Windows 音频输出检查通过。Windows 为什么拒绝这个特定游戏线程的绑定,仍未完全查明。
## 1. 本文怎样判断“有用 / 没用 / 玄学”
- **有条件的依据:**开发者针对特定版本或症状给出处理办法,或玩家明确记录了硬件、改动和恢复结果。只能支持对应条件。
- **可能有用、证据较弱:**存在合理机制,但只有一句成功声明,或者一次改了多项设置,不能确定哪项起作用。
- **本例未解决:**有本机历史反馈或本轮对照实验;必须写明改了什么,不能扩大成对所有机器无效。
- **“玄学化”的建议:**没有对应证据,却宣传成任何闪退都能解决的万能方法。这里说的是因果依据不足,不是证明它在所有情况下都不可能有效。
开发者提供某个排障选项,也不等于证明它能解决每一种音频故障。论坛里“有人建议这么做”更不能直接记成“楼主做完成功”。
## 2. 方案比较
| 网上办法 | 找到的证据与适用场景 | 对本机故障的判断 |
| --- | --- | --- |
| 限制 CPU 核心、Process Lasso CPU Limiter | 性能教程针对游玩时 CPU 约 100% 占用、冻结或卡顿,建议在 98% 阈值时临时减少 1 个核心;没有给出音频线程错误 87 的验证。[S1] | 用户之前限制整个进程未解决;本轮缩小单个音频线程掩码仍失败。不能把这两种操作等同于本补丁的取消显式绑定 |
| 改声音输出设备、Sound API、关闭空间音频 | 官方 Patch 2 专门加入这些选项,用于音频硬件兼容和无声音排障。[S2] | **有官方用途依据。**适合对应输出/接口兼容问题;尚未证明能消除本机绑定失败 |
| 把采样率改成 48 kHz | 一篇启动崩溃帖直接建议 48 kHz,但没有硬件、日志、对照记录或回复。[S3] 另一教程示例却是 96 kHz。[S4] | **机制可能合理,特定数值的证据弱。**换格式可能改变设备协商,但没有证据证明 48 kHz 或 96 kHz 是本机解法 |
| Iris Xe 关闭 Motion Blur(动态模糊) | 游玩 5~20 分钟后冻结的讨论中,一位 Iris Xe 玩家报告关闭后解决,原帖作者随后也确认不再冻结。[S5] | **有针对性成功反馈。**本机主要在加载阶段出现 Wwise 故障,实际渲染设备是 RX 9070 XT,不能直接套用 |
| 窗口化、降低分辨率、调整 HDR | RTX 5070 / 4K HDR 玩家报告同时修改启动参数、缩放、桌面分辨率等后解决。[S6] | **单例且多项混改。**可能处理图形显示路径;本机未修补的调试运行已用 1280×720 窗口模式,仍复现音频故障,因此窗口化本身不够 |
| 用 DXVK 替换图形接口实现 | RTX 50 系列画面破损帖中,作者更新称 DXVK 消除了花屏。[S7] DXVK 将 Direct3D 调用转为 Vulkan 调用 | **有图形问题的成功报告。**帖子主要研究花屏;本机历史上反而有一次 DXVK/DXGI 崩溃。不能当成本例音频故障的通用修复 |
| 切回官方 Patch 2 分支 | 官方 Patch 3.1 公告针对更新后仍有白屏冻结、崩溃或性能问题的配置,先建议降低过高设置,再提供 Patch 2 分支。[S8] | **有特定版本回退的官方依据。**本机两份游戏虽构建不同,故障音频 DLL 却相同;尚无“换旧版就能修好本例”的证据 |
| 更换 Windows 版本 | 2021 年开发者将一类 Patch 3.1 白屏问题初步关联到 Insider Dev 预览版,并称正式版或当时 Beta 版已帮助部分用户。[S9] | **只支持对应预览版兼容问题。**本机是正式版 Win10 22H2,虚拟机同系统版本可运行;不足以支持盲目重装或降级 |
| 验证游戏文件、修复运行库、调整驱动版本 | 启动检查清单包含这些做法。[S4] 缺文件、依赖加载失败、明确的驱动回归各有相应机制 | **按证据选择的常规排障。**校验只能恢复发布文件,不能修复原版已有逻辑;本机已经进入音频线程执行阶段,不能仅凭闪退就断言缺运行库 |
| 关闭叠加层、停用设备或后台软件 | 检查清单和无声讨论都有此类建议。[S4][S10] 注入冲突或默认设备变化有可能影响行为 | **应逐项验证,不能一口气改完就归因。**用户此前禁用部分设备未解决;无声帖楼主也表示建议都没用,数月后恢复且原因不明 |
| 安装 Unity 引擎、清内存、强制高优先级等“一键优化” | 一篇指南直接写“安装 Unity 引擎可以解决”,未给出相应日志或受控结果。[S4] 性能指南也有提优先级、清理缓存等建议,并承认某些设置可能加剧卡顿。[S1] | **作为万能启动修复缺乏依据。**发行游戏携带运行所需的 Unity Player,不能因游戏使用 Unity 就推导必须安装编辑器;调度和内存参数也应有对应瓶颈证据 |
## 3. 限制核心:必须分清的三个操作
### 3.1 限制整个进程
本机保留的旧启动文件含有 `/affinity F`。F 是十六进制掩码,代表低 4 个逻辑处理器,不保证是 4 个物理核心。用户报告此前做过限制且没有解决;本轮没有重新穷举每一种进程掩码。
进程限制规定可运行的处理器范围,但游戏内部仍可能给某个音频线程另设范围。外部缩小范围不会自动修正 DLL 的配置参数,更不会自动修复初始化失败后的线程生命周期;具体结果要看实际调用和返回值。
查到的 Process Lasso 教程甚至不是“开局固定 4 核”:它描述的是 CPU 占用超过阈值后暂时减少核心的规则,目标为运行中的饱和与卡顿。[S1] 这是一种网上真实存在的建议,但不能认定就是用户此前看到的原帖。
### 3.2 缩小一条音频线程请求的掩码
本轮对游戏中的目标调用做过动态对照:
```text
原始请求:0xFFFFFFFC → SetThreadAffinityMask 返回 0,错误 87
截断请求:0x000FFFFC → SetThreadAffinityMask 返回 0,错误 87
```
因此,本例不支持“只是高位超出可用核心,截掉就好”的解释。独立测试程序自己的线程却能接受相同掩码,进一步说明需要研究具体游戏线程的状态,不能直接怪罪核心总数、混合架构或 ES 处理器。
### 3.3 不发起显式绑定
补丁把相应 Wwise 属性设为 0。该 DLL 的既有分支遇到 0 会跳过 `SetThreadAffinityMask`,让线程继承正常调度限制。这里不是把 Windows API 的参数直接传成 0,也不是限制成 0 个核心。
该操作通过了临时修正与磁盘补丁后的普通启动验证,并检测到 Windows 音频管线中的非零输出。因此可发表的准确表述是:
> 我此前限制整个游戏可用核心未解决;进一步实测发现,缩小音频线程请求掩码仍然失败,取消它的显式绑定则通过了本机验证。这不能推导成限制核心对所有奥日 2 故障都无效。
## 4. 为什么同一篇教程有人成功,有人失败
先核对发生阶段和故障模块,再看方案的作用位置:
| 故障线索 | 值得优先核对的方向 |
| --- | --- |
| 游戏未进入引擎就报缺少 DLL、文件校验异常 | 文件完整性和明确缺失的运行依赖 |
| Iris Xe 游玩一段时间后冻结,或特定 GPU 花屏 | 对应渲染设置、驱动与已报告的图形兼容路径 |
| 切换输出设备后无声,空间音频/API 切换有稳定影响 | 默认设备、输出格式、声音接口与设备驱动 |
| Wwise streaming I/O 初始化失败、音频工作线程崩溃,进一步抓到绑定错误 87 | 本研究的线程配置与失败清理路径;补丁还需完整 DLL 哈希匹配 |
“同系统的虚拟机正常”是一条有价值的对照线索,但虚拟机同时改变了处理器拓扑、设备和驱动等条件。它缩小了调查方向,没有单独证明哪个差异就是原因。
想让一次成功有说服力,应保存原设置,每次只改变一项,比较同一日志或失败调用是否消失,并做多次正常启动和实际游玩。尤其本例本来就偶尔能进,单次启动成功不能单独证明修好了。
## 5. 资料的可信范围
- 本轮找到的公开页面中,没有核对到一份同时给出本机相同 Wwise 调用、参数与错误 87 的独立复现报告。这是本轮检索范围的限制,不代表网上不存在,也不能据此宣称首创。
- [S7] 核对了原帖作者更新和所下载页面的回复,没有完整核查其跨页的第 25 条回复,结论仅使用已读的作者自述。
- [S3]、[S6] 页面显示月日但未明确显示年份,本文不擅自补全年份。
- 2020~2021 年的官方办法是历史证据,不能解释为今天应安装早已过时的 Windows 或显卡驱动。
- Windows Central 的早期报道也包含 Xbox 外置存储与恢复游戏的例子,不能直接搬到 Windows PC 当作验证结果;未纳入上表作为 PC 成功证据。
## 6. 来源与本地留档
以下链接于 2026-09-16 核对。公开网页 HTML、文本和获取结果保存在研究工作区的 `ori-research/community-sources/`;公开补丁包只收录本文的摘要和链接,不捆绑网页副本。
- **[S1]** GhostArrow:[How to Improve Performance in Ori and the Will of the Wisps][S1],重点核对第 18 项 CPU Limiter 及其近满载前提。留档:`ghostarrow-performance.txt`。
- **[S2]** 官方公告:[Ori and the Will of the Wisps Patch 2][S2],2020-05-14。核对声音输出设备、Sound API 和空间音频选项的说明。留档:`official-patch2.txt`。
- **[S3]** Steam 玩家:[game crashing on startup fix][S3],建议 48 kHz,正文短且没有回复验证。留档:`steam-audio-48khz.txt`。
- **[S4]** GhostArrow:[Won’t Launch, Black Screen, Crashes][S4],2020 年启动排障清单,含采样率、依赖与安装 Unity 的建议。留档:`ghostarrow-startup.txt`。
- **[S5]** Steam 玩家:[Ori freezing after 5-20 minutes of playing][S5],2024 年 Iris Xe 冻结讨论;第 3、4 条回复报告关闭动态模糊有效。留档:`steam-freezing.txt`。
- **[S6]** Steam 玩家:[Fix for game crashing on launch! (50-series GPU)][S6],RTX 5070 / 4K HDR 作者一次调整了多项显示设置。留档:`steam-gpu-windowed.txt`。
- **[S7]** Steam 玩家:[[WORKAROUND - DXVK] Graphical corruption on RTX 50 series][S7],2025-12-02 发帖,后续作者更新报告 DXVK 消除花屏。留档:`steam-dxvk-rtx50.txt`。
- **[S8]** 官方公告:[Patch 3.1][S8],2020-11-19,包含特定问题的低设置建议和 Patch 2 分支。留档:`official-patch31.txt`。
- **[S9]** 开发者讨论:[White Screen Freeze/Crash][S9],2021-01-07,Insider Dev 预览版的初步关联及用户反馈。留档:`official-white-screen.txt`。
- **[S10]** Steam 玩家:[No sound!!][S10],2020~2021 年讨论;需同时看建议、楼主失败反馈及后来原因不明的恢复。留档:`steam-no-sound.txt`。
[S1]: https://www.ghostarrow.com/how-to-improve-performance-in-ori-and-the-will-of-the-wisps
[S2]: https://steamcommunity.com/app/1057090/discussions/0/4666237625643445999/
[S3]: https://steamcommunity.com/app/1057090/discussions/0/581680031271722194/
[S4]: https://www.ghostarrow.com/ori-and-the-will-of-the-wisps-fix-wont-launch-black-screen-crashes
[S5]: https://steamcommunity.com/app/1057090/discussions/0/5715697730998329692/
[S6]: https://steamcommunity.com/app/1057090/discussions/0/582806854239850133/
[S7]: https://steamcommunity.com/app/1057090/discussions/0/695372304943750032/
[S8]: https://steamcommunity.com/app/1057090/discussions/0/2968397584537393710/
[S9]: https://steamcommunity.com/app/1057090/discussions/0/3012312119189327120/
[S10]: https://steamcommunity.com/app/1057090/discussions/0/2968398851794293852/
@@ -0,0 +1 @@
977280c7054a7ea9059c423239458a5f5f9bb12df51a2a02c22668df362e97bd ori-wwise-affinity-fix-v1.0.0.zip
@@ -0,0 +1,110 @@
# 奥日 2 闪退的网络方案核查:哪些有依据,哪些被说得太万能
核查日期:2026-09-16。本文比较公开资料与本机实验,不代表在本机逐一执行了所有网络方案。
**结论:同样叫“闪退”,可能发生在音频初始化、图形渲染、系统兼容或文件加载的不同位置。别人的成功记录可以帮助选择实验,但不能替代本机的故障证据。**
本机已经确认的直接触发条件是 Wwise 音频线程调用 `SetThreadAffinityMask` 失败,返回错误 87;随后进入危险的线程清理路径并发生访问冲突。取消相关线程的显式绑定后,普通启动和 Windows 音频输出检查通过。Windows 为什么拒绝这个特定游戏线程的绑定,仍未完全查明。
## 1. 本文怎样判断“有用 / 没用 / 玄学”
- **有条件的依据:**开发者针对特定版本或症状给出处理办法,或玩家明确记录了硬件、改动和恢复结果。只能支持对应条件。
- **可能有用、证据较弱:**存在合理机制,但只有一句成功声明,或者一次改了多项设置,不能确定哪项起作用。
- **本例未解决:**有本机历史反馈或本轮对照实验;必须写明改了什么,不能扩大成对所有机器无效。
- **“玄学化”的建议:**没有对应证据,却宣传成任何闪退都能解决的万能方法。这里说的是因果依据不足,不是证明它在所有情况下都不可能有效。
开发者提供某个排障选项,也不等于证明它能解决每一种音频故障。论坛里“有人建议这么做”更不能直接记成“楼主做完成功”。
## 2. 方案比较
| 网上办法 | 找到的证据与适用场景 | 对本机故障的判断 |
| --- | --- | --- |
| 限制 CPU 核心、Process Lasso CPU Limiter | 性能教程针对游玩时 CPU 约 100% 占用、冻结或卡顿,建议在 98% 阈值时临时减少 1 个核心;没有给出音频线程错误 87 的验证。[S1] | 用户之前限制整个进程未解决;本轮缩小单个音频线程掩码仍失败。不能把这两种操作等同于本补丁的取消显式绑定 |
| 改声音输出设备、Sound API、关闭空间音频 | 官方 Patch 2 专门加入这些选项,用于音频硬件兼容和无声音排障。[S2] | **有官方用途依据。**适合对应输出/接口兼容问题;尚未证明能消除本机绑定失败 |
| 把采样率改成 48 kHz | 一篇启动崩溃帖直接建议 48 kHz,但没有硬件、日志、对照记录或回复。[S3] 另一教程示例却是 96 kHz。[S4] | **机制可能合理,特定数值的证据弱。**换格式可能改变设备协商,但没有证据证明 48 kHz 或 96 kHz 是本机解法 |
| Iris Xe 关闭 Motion Blur(动态模糊) | 游玩 5~20 分钟后冻结的讨论中,一位 Iris Xe 玩家报告关闭后解决,原帖作者随后也确认不再冻结。[S5] | **有针对性成功反馈。**本机主要在加载阶段出现 Wwise 故障,实际渲染设备是 RX 9070 XT,不能直接套用 |
| 窗口化、降低分辨率、调整 HDR | RTX 5070 / 4K HDR 玩家报告同时修改启动参数、缩放、桌面分辨率等后解决。[S6] | **单例且多项混改。**可能处理图形显示路径;本机未修补的调试运行已用 1280×720 窗口模式,仍复现音频故障,因此窗口化本身不够 |
| 用 DXVK 替换图形接口实现 | RTX 50 系列画面破损帖中,作者更新称 DXVK 消除了花屏。[S7] DXVK 将 Direct3D 调用转为 Vulkan 调用 | **有图形问题的成功报告。**帖子主要研究花屏;本机历史上反而有一次 DXVK/DXGI 崩溃。不能当成本例音频故障的通用修复 |
| 切回官方 Patch 2 分支 | 官方 Patch 3.1 公告针对更新后仍有白屏冻结、崩溃或性能问题的配置,先建议降低过高设置,再提供 Patch 2 分支。[S8] | **有特定版本回退的官方依据。**本机两份游戏虽构建不同,故障音频 DLL 却相同;尚无“换旧版就能修好本例”的证据 |
| 更换 Windows 版本 | 2021 年开发者将一类 Patch 3.1 白屏问题初步关联到 Insider Dev 预览版,并称正式版或当时 Beta 版已帮助部分用户。[S9] | **只支持对应预览版兼容问题。**本机是正式版 Win10 22H2,虚拟机同系统版本可运行;不足以支持盲目重装或降级 |
| 验证游戏文件、修复运行库、调整驱动版本 | 启动检查清单包含这些做法。[S4] 缺文件、依赖加载失败、明确的驱动回归各有相应机制 | **按证据选择的常规排障。**校验只能恢复发布文件,不能修复原版已有逻辑;本机已经进入音频线程执行阶段,不能仅凭闪退就断言缺运行库 |
| 关闭叠加层、停用设备或后台软件 | 检查清单和无声讨论都有此类建议。[S4][S10] 注入冲突或默认设备变化有可能影响行为 | **应逐项验证,不能一口气改完就归因。**用户此前禁用部分设备未解决;无声帖楼主也表示建议都没用,数月后恢复且原因不明 |
| 安装 Unity 引擎、清内存、强制高优先级等“一键优化” | 一篇指南直接写“安装 Unity 引擎可以解决”,未给出相应日志或受控结果。[S4] 性能指南也有提优先级、清理缓存等建议,并承认某些设置可能加剧卡顿。[S1] | **作为万能启动修复缺乏依据。**发行游戏携带运行所需的 Unity Player,不能因游戏使用 Unity 就推导必须安装编辑器;调度和内存参数也应有对应瓶颈证据 |
## 3. 限制核心:必须分清的三个操作
### 3.1 限制整个进程
本机保留的旧启动文件含有 `/affinity F`。F 是十六进制掩码,代表低 4 个逻辑处理器,不保证是 4 个物理核心。用户报告此前做过限制且没有解决;本轮没有重新穷举每一种进程掩码。
进程限制规定可运行的处理器范围,但游戏内部仍可能给某个音频线程另设范围。外部缩小范围不会自动修正 DLL 的配置参数,更不会自动修复初始化失败后的线程生命周期;具体结果要看实际调用和返回值。
查到的 Process Lasso 教程甚至不是“开局固定 4 核”:它描述的是 CPU 占用超过阈值后暂时减少核心的规则,目标为运行中的饱和与卡顿。[S1] 这是一种网上真实存在的建议,但不能认定就是用户此前看到的原帖。
### 3.2 缩小一条音频线程请求的掩码
本轮对游戏中的目标调用做过动态对照:
```text
原始请求:0xFFFFFFFC → SetThreadAffinityMask 返回 0,错误 87
截断请求:0x000FFFFC → SetThreadAffinityMask 返回 0,错误 87
```
因此,本例不支持“只是高位超出可用核心,截掉就好”的解释。独立测试程序自己的线程却能接受相同掩码,进一步说明需要研究具体游戏线程的状态,不能直接怪罪核心总数、混合架构或 ES 处理器。
### 3.3 不发起显式绑定
补丁把相应 Wwise 属性设为 0。该 DLL 的既有分支遇到 0 会跳过 `SetThreadAffinityMask`,让线程继承正常调度限制。这里不是把 Windows API 的参数直接传成 0,也不是限制成 0 个核心。
该操作通过了临时修正与磁盘补丁后的普通启动验证,并检测到 Windows 音频管线中的非零输出。因此可发表的准确表述是:
> 我此前限制整个游戏可用核心未解决;进一步实测发现,缩小音频线程请求掩码仍然失败,取消它的显式绑定则通过了本机验证。这不能推导成限制核心对所有奥日 2 故障都无效。
## 4. 为什么同一篇教程有人成功,有人失败
先核对发生阶段和故障模块,再看方案的作用位置:
| 故障线索 | 值得优先核对的方向 |
| --- | --- |
| 游戏未进入引擎就报缺少 DLL、文件校验异常 | 文件完整性和明确缺失的运行依赖 |
| Iris Xe 游玩一段时间后冻结,或特定 GPU 花屏 | 对应渲染设置、驱动与已报告的图形兼容路径 |
| 切换输出设备后无声,空间音频/API 切换有稳定影响 | 默认设备、输出格式、声音接口与设备驱动 |
| Wwise streaming I/O 初始化失败、音频工作线程崩溃,进一步抓到绑定错误 87 | 本研究的线程配置与失败清理路径;补丁还需完整 DLL 哈希匹配 |
“同系统的虚拟机正常”是一条有价值的对照线索,但虚拟机同时改变了处理器拓扑、设备和驱动等条件。它缩小了调查方向,没有单独证明哪个差异就是原因。
想让一次成功有说服力,应保存原设置,每次只改变一项,比较同一日志或失败调用是否消失,并做多次正常启动和实际游玩。尤其本例本来就偶尔能进,单次启动成功不能单独证明修好了。
## 5. 资料的可信范围
- 本轮找到的公开页面中,没有核对到一份同时给出本机相同 Wwise 调用、参数与错误 87 的独立复现报告。这是本轮检索范围的限制,不代表网上不存在,也不能据此宣称首创。
- [S7] 核对了原帖作者更新和所下载页面的回复,没有完整核查其跨页的第 25 条回复,结论仅使用已读的作者自述。
- [S3]、[S6] 页面显示月日但未明确显示年份,本文不擅自补全年份。
- 2020~2021 年的官方办法是历史证据,不能解释为今天应安装早已过时的 Windows 或显卡驱动。
- Windows Central 的早期报道也包含 Xbox 外置存储与恢复游戏的例子,不能直接搬到 Windows PC 当作验证结果;未纳入上表作为 PC 成功证据。
## 6. 来源与本地留档
以下链接于 2026-09-16 核对。公开网页 HTML、文本和获取结果保存在研究工作区的 `ori-research/community-sources/`;公开补丁包只收录本文的摘要和链接,不捆绑网页副本。
- **[S1]** GhostArrow:[How to Improve Performance in Ori and the Will of the Wisps][S1],重点核对第 18 项 CPU Limiter 及其近满载前提。留档:`ghostarrow-performance.txt`。
- **[S2]** 官方公告:[Ori and the Will of the Wisps Patch 2][S2],2020-05-14。核对声音输出设备、Sound API 和空间音频选项的说明。留档:`official-patch2.txt`。
- **[S3]** Steam 玩家:[game crashing on startup fix][S3],建议 48 kHz,正文短且没有回复验证。留档:`steam-audio-48khz.txt`。
- **[S4]** GhostArrow:[Won’t Launch, Black Screen, Crashes][S4],2020 年启动排障清单,含采样率、依赖与安装 Unity 的建议。留档:`ghostarrow-startup.txt`。
- **[S5]** Steam 玩家:[Ori freezing after 5-20 minutes of playing][S5],2024 年 Iris Xe 冻结讨论;第 3、4 条回复报告关闭动态模糊有效。留档:`steam-freezing.txt`。
- **[S6]** Steam 玩家:[Fix for game crashing on launch! (50-series GPU)][S6],RTX 5070 / 4K HDR 作者一次调整了多项显示设置。留档:`steam-gpu-windowed.txt`。
- **[S7]** Steam 玩家:[[WORKAROUND - DXVK] Graphical corruption on RTX 50 series][S7],2025-12-02 发帖,后续作者更新报告 DXVK 消除花屏。留档:`steam-dxvk-rtx50.txt`。
- **[S8]** 官方公告:[Patch 3.1][S8],2020-11-19,包含特定问题的低设置建议和 Patch 2 分支。留档:`official-patch31.txt`。
- **[S9]** 开发者讨论:[White Screen Freeze/Crash][S9],2021-01-07,Insider Dev 预览版的初步关联及用户反馈。留档:`official-white-screen.txt`。
- **[S10]** Steam 玩家:[No sound!!][S10],2020~2021 年讨论;需同时看建议、楼主失败反馈及后来原因不明的恢复。留档:`steam-no-sound.txt`。
[S1]: https://www.ghostarrow.com/how-to-improve-performance-in-ori-and-the-will-of-the-wisps
[S2]: https://steamcommunity.com/app/1057090/discussions/0/4666237625643445999/
[S3]: https://steamcommunity.com/app/1057090/discussions/0/581680031271722194/
[S4]: https://www.ghostarrow.com/ori-and-the-will-of-the-wisps-fix-wont-launch-black-screen-crashes
[S5]: https://steamcommunity.com/app/1057090/discussions/0/5715697730998329692/
[S6]: https://steamcommunity.com/app/1057090/discussions/0/582806854239850133/
[S7]: https://steamcommunity.com/app/1057090/discussions/0/695372304943750032/
[S8]: https://steamcommunity.com/app/1057090/discussions/0/2968397584537393710/
[S9]: https://steamcommunity.com/app/1057090/discussions/0/3012312119189327120/
[S10]: https://steamcommunity.com/app/1057090/discussions/0/2968398851794293852/
@@ -0,0 +1,61 @@
{
"date": "2026-09-16",
"historical_crash_reports": {
"total": 29,
"wwise_worker": 28,
"dxgi": 1,
"not_a_failure_rate": true
},
"original_trace": {
"priority_return": 1,
"affinity_mask": "0xFFFFFFFC",
"affinity_return": 0,
"last_error": 87,
"subsequent_exception": "0xC0000005"
},
"original_minidump": {
"exception_code": "0xC0000005",
"operation_parameter": 8,
"interpretation": "user-mode DEP execution violation"
},
"clamped_trace": {
"requested_mask": "0xFFFFC",
"affinity_return": 0,
"last_error": 87
},
"automatic_trace": {
"effective_stream_thread_properties": [
0,
0,
131072
],
"initialization_passed": true,
"observation_limit_seconds": 180,
"closed_by_test_timer": true
},
"normal_launch": {
"debugger": false,
"audio_active": true,
"audio_muted": false,
"volume": 1.0,
"peak_over_3_seconds": 0.010620015673339367,
"closed_manually_user_confirmed": true,
"human_listening_result_recorded": false
},
"scope": {
"host_count": 1,
"runtime_tested_game_build": 5257,
"file_patch_also_installed_build": 8978,
"long_term_gameplay_tested": false,
"reason_windows_refuses_specific_game_thread_fully_determined": false
},
"patch": {
"file_offset": "0x115F0",
"rva": "0x121F0",
"region_bytes": 16,
"changed_bytes": 8,
"original_sha256": "8F90A4A3856540CB97D03CA07B4BB10B7A08F7B4AB3C524BE2BEC1AFDD29FECA",
"patched_sha256": "62B2079A988D23EB251D4E83366AC04068A6F47E0D38EFE92A76DF022826F9F0"
},
"privacy": "Summary only. No game binaries, dumps, raw logs, accounts or save files included."
}
@@ -0,0 +1,8 @@
@echo off
where pwsh.exe >nul 2>&1
if errorlevel 1 (
echo PowerShell 7 is required. This package does not install software.
pause
exit /b 1
)
pwsh.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Interactive.ps1" -Action Install
@@ -0,0 +1,15 @@
#requires -Version 7.0
param([ValidateSet('Install','Restore')][string]$Action='Install')
$ErrorActionPreference='Stop'
try {
Write-Host '奥日 2 Wwise 音频线程兼容补丁 v1.0.0'
Write-Host '先退出游戏。请输入包含 oriwotw.exe 的游戏根目录。'
$directory=(Read-Host '游戏目录').Trim().Trim('"')
if ([string]::IsNullOrWhiteSpace($directory)) { throw '未输入目录,已取消。' }
& (Join-Path $PSScriptRoot 'Ori-AudioFix.ps1') -Action $Action -GameDirectory $directory
Read-Host '完成,按回车关闭' | Out-Null
} catch {
Write-Host $_.Exception.Message -ForegroundColor Red
Read-Host '操作未完成,按回车关闭' | Out-Null
exit 1
}
@@ -0,0 +1,112 @@
#requires -Version 7.0
[CmdletBinding()]
param(
[ValidateSet('Check', 'Install', 'Restore')]
[string]$Action = 'Check',
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string[]]$GameDirectory
)
$ErrorActionPreference = 'Stop'
$originalHash = '8F90A4A3856540CB97D03CA07B4BB10B7A08F7B4AB3C524BE2BEC1AFDD29FECA'
$patchedHash = '62B2079A988D23EB251D4E83366AC04068A6F47E0D38EFE92A76DF022826F9F0'
$offset = 0x115F0
$originalCode = [Convert]::FromHexString('4885C97403895104C3CCCCCCCCCCCCCC')
$patchedCode = [Convert]::FromHexString('4885C97407C7410400000000C3CCCCCC')
function Get-BytesHash([byte[]]$Bytes) {
[Convert]::ToHexString([Security.Cryptography.SHA256]::HashData($Bytes))
}
if ($Action -ne 'Check' -and (Get-Process -Name 'oriwotw' -ErrorAction SilentlyContinue)) {
throw '请先退出所有奥日 2 游戏窗口,再安装或还原补丁。'
}
# Preflight every selected directory before any mutation. Unknown builds,
# unrelated modifications and conflicting backups are never overwritten.
$plans = foreach ($directory in $GameDirectory) {
$root = (Resolve-Path -LiteralPath $directory).Path
$target = Join-Path $root 'oriwotw_Data\Plugins\AkSoundEngine.dll'
$backup = $target + '.before-ori-affinity-fix'
$bytes = [IO.File]::ReadAllBytes($target)
$hash = Get-BytesHash $bytes
if ($hash -notin @($originalHash, $patchedHash)) {
throw "文件版本或内容不匹配,未修改:$target;SHA256=$hash"
}
if (Test-Path -LiteralPath $backup) {
if ((Get-FileHash -LiteralPath $backup -Algorithm SHA256).Hash -ne $originalHash) {
throw "现有备份不匹配,未覆盖:$backup"
}
}
if ($Action -eq 'Restore' -and $hash -eq $patchedHash -and -not (Test-Path -LiteralPath $backup)) {
throw "找不到经过校验的原文件备份,无法还原:$backup"
}
if ($hash -eq $originalHash) {
for ($i = 0; $i -lt $originalCode.Length; $i++) {
if ($bytes[$offset + $i] -ne $originalCode[$i]) { throw '原始指令校验失败。' }
}
$candidate = [byte[]]$bytes.Clone()
[Array]::Copy($patchedCode, 0, $candidate, $offset, $patchedCode.Length)
if ((Get-BytesHash $candidate) -ne $patchedHash) { throw '补丁结果校验失败。' }
} else {
$candidate = $bytes
}
[pscustomobject]@{ Root=$root; Target=$target; Backup=$backup; Hash=$hash; Candidate=$candidate }
}
foreach ($plan in $plans) {
if ($Action -eq 'Check') {
[pscustomobject]@{
Directory=$plan.Root
State=if ($plan.Hash -eq $patchedHash) { '已安装补丁' } else { '原版,符合补丁要求' }
SHA256=$plan.Hash
BackupExists=(Test-Path -LiteralPath $plan.Backup)
}
continue
}
if ($Action -eq 'Install' -and $plan.Hash -eq $patchedHash) {
Write-Host "已安装,无需重复修改:$($plan.Root)"
continue
}
if ($Action -eq 'Restore' -and $plan.Hash -eq $originalHash) {
Write-Host "已经是原文件:$($plan.Root)"
continue
}
if ($Action -eq 'Install') {
if (-not (Test-Path -LiteralPath $plan.Backup)) {
[IO.File]::Copy($plan.Target, $plan.Backup, $false)
}
if ((Get-FileHash -LiteralPath $plan.Backup -Algorithm SHA256).Hash -ne $originalHash) {
throw '备份验证失败;未覆盖游戏 DLL。'
}
$newBytes = $plan.Candidate
$expected = $patchedHash
} else {
$newBytes = [IO.File]::ReadAllBytes($plan.Backup)
$expected = $originalHash
}
# Detect changes made by another program since preflight.
if ((Get-FileHash -LiteralPath $plan.Target -Algorithm SHA256).Hash -ne $plan.Hash) {
throw "检查后文件发生变化,停止修改:$($plan.Target)"
}
if (Get-Process -Name 'oriwotw' -ErrorAction SilentlyContinue) {
throw '游戏刚刚启动,已停止修改;请先退出游戏。'
}
$staging = $plan.Target + '.ori-fix-' + [Guid]::NewGuid().ToString('N') + '.tmp'
try {
$stream = [IO.File]::Open($staging, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
try { $stream.Write($newBytes); $stream.Flush($true) } finally { $stream.Dispose() }
if ((Get-FileHash -LiteralPath $staging -Algorithm SHA256).Hash -ne $expected) {
throw '临时文件验证失败,未覆盖游戏 DLL。'
}
[IO.File]::Move($staging, $plan.Target, $true)
if ((Get-FileHash -LiteralPath $plan.Target -Algorithm SHA256).Hash -ne $expected) {
throw '写入后的校验失败,请保留备份并停止启动游戏。'
}
} finally {
if (Test-Path -LiteralPath $staging) { Remove-Item -LiteralPath $staging }
}
Write-Host "${Action} 成功:$($plan.Target)"
Write-Host "原文件备份:$($plan.Backup)"
}
@@ -0,0 +1,94 @@
# Ori Wwise Affinity Fix v1.0.0
针对《奥日 2:精灵与萤火意志》一个特定 Wwise DLL 构建的非官方兼容补丁。
本机定位到的触发链:音频读取线程的 `SetThreadAffinityMask` 失败(错误 87),初始化失败后的句柄清理与存活线程形成危险时序,随后访问冲突。补丁让通过指定 setter 配置的音频线程使用 Windows 默认调度,避开这条失败路径。
## 适用范围
同时参考故障表现、Wwise streaming I/O 初始化报错、音频工作线程崩溃,以及下面的完整文件哈希。`0xC0000005` 本身不足以判断适用。
原始目标文件:游戏目录下 `oriwotw_Data\Plugins\AkSoundEngine.dll`。
```text
大小:2,719,232 字节
原始 SHA-256:
8F90A4A3856540CB97D03CA07B4BB10B7A08F7B4AB3C524BE2BEC1AFDD29FECA
补丁后 SHA-256:
62B2079A988D23EB251D4E83366AC04068A6F47E0D38EFE92A76DF022826F9F0
```
SDK 字符串相同不代表 DLL 完全相同。安装器会拒绝未知版本,不提供绕过哈希限制的选项。
## 安装
要求 Windows x64 和 PowerShell 7;本包不安装或升级任何软件。
1. 解压整个压缩包。
2. 正常退出所有奥日 2 进程。
3. 双击 `Install.cmd`。
4. 按提示输入包含 `oriwotw.exe` 的游戏根目录。
5. 成功后直接使用原来的游戏入口启动。
文件夹没有写入权限时,脚本会报错;在确认目录正确后,可以由用户在具备该目录写入权限的 PowerShell 7 中执行。脚本不会自行获取管理员权限。
命令行方式:在本包解压目录打开 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'
```
默认动作是 `Check`,只检查。分享版要求明确提供游戏目录,不采用研究机器上的默认路径。
## 还原
退出游戏后双击 `Restore.cmd`,输入同一个游戏目录。也可以执行:
```powershell
& '.\Ori-AudioFix.ps1' -Action Restore -GameDirectory 'D:\Games\Ori and the Will of the Wisps'
```
原文件保留在 DLL 旁边的 `AkSoundEngine.dll.before-ori-affinity-fix`,还原前后都会验证哈希。不要删除该备份。
游戏更新或平台校验可能恢复原文件。若 DLL 已变为新版本,应重新研究兼容性,不要强行套用本补丁。
## 修改原理
目标:`CSharp_AkThreadProperties_dwAffinityMask_set`。保留空指针检查,把它写入的亲和性值改为 0。此 DLL 本身会在掩码为 0 时跳过显式核心绑定。
```text
函数 RVA:0x121F0
文件偏移:0x115F0
校验区域:16 字节;实际变化:8 字节
原始: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
```
这个 0 表示使用 DLL 已有的“不显式绑定”路径,不是向 Windows 请求零个可用核心。补丁不关闭音频,不修改系统全局调度或游戏平台授权接口。
未知哈希、原始指令不符、备份不符或游戏仍在运行时,安装器会停止。多个目录之间不构成整体事务,请逐条检查输出;每份原文件都单独保留备份。
## 验证范围
已在一台 Windows 10 22H2 主机上完成:原始故障跟踪、截断掩码的失败对照、取消绑定后的临时验证、安装后的普通启动和非零音频信号检查。用户确认最后一次普通启动为主动退出。
主测版本 Unity 2018.4.11f1 / Build Id 5257;另一份 Unity 2018.4.24f1 / Build Id 8978 具有相同音频 DLL,已完成安装校验,本轮未单独运行验证。尚无大规模成功率、长时间稳定性或性能结论。
为什么该游戏线程的绑定被拒绝仍未完全查明;这里提供的是有对照实验支持的兼容方案。完整证据层级、限制和微软文档引用见 `RESEARCH.zh-CN.md`。
## 文件清单
- `Ori-AudioFix.ps1`:核心检查、安装和还原逻辑。
- `Interactive.ps1`、`Install.cmd`、`Restore.cmd`:交互入口。
- `RESEARCH.zh-CN.md`:技术报告。
- `COMMUNITY-REVIEW.zh-CN.md`:网络排障方案的来源、适用条件,以及限制核心与本补丁的区别。
- `environment.json`:相关系统、硬件与驱动信息,不含序列号和账号。
- `EVIDENCE.json`:脱敏后的实验结论摘要。
- `SHA256SUMS.txt`:包内文件校验值;另有压缩包外部 SHA-256 文件。
压缩包不含游戏 DLL、完整游戏、原始转储、原始日志或用户存档。报告中的本地原始证据索引用于说明研究来源,不代表这些原始文件随包分发。
反馈建议包含系统构建、CPU 标识与逻辑处理器数、原始 DLL 哈希、关键错误和测试时长。请先移除个人路径,不要在公开评论中上传完整内存转储。
@@ -0,0 +1,480 @@
# 奥日 2 加载闪退与无声:Wwise 线程亲和性失败的定位与兼容补丁
研究日期:2026-09-16(UTC+8)
研究对象:Windows 版《Ori and the Will of the Wisps》
状态:已定位直接触发链,完成兼容补丁及本机启动、音频输出验证。用户确认最终普通启动是主动关闭。
## 1. 结论及其边界
本机出现的主要故障位于 Wwise 音频引擎的线程初始化阶段:`SetThreadAffinityMask` 调用失败,返回错误 87;随后初始化失败路径关闭并清空线程句柄,而已经启动的线程仍可能继续运行。历史崩溃的异常位置和动态跟踪都与这一失败路径吻合。
兼容补丁将游戏通过指定导出函数设置的音频线程亲和性掩码改为 0。该 DLL 的现有分支把 0 当作“不进行显式绑定”,因此线程交由 Windows 调度,避开失败路径。补丁没有禁用声音,也没有改变系统可用核心数量。
验证结果:
- 排查前留存的 29 份崩溃报告中,28 份落在同类 Wwise 读取线程路径;另 1 份为此前 DXVK 尝试中的 DXGI 崩溃。
- 原版在动态跟踪中重现:优先级设置成功、亲和性设置失败、紧随其后访问冲突。
- 将请求掩码截断到进程报告的可用核心范围,仍然失败。
- 将亲和性配置改为 0 后,音频初始化和后续场景加载通过。
- 写入磁盘补丁后,不带调试器的普通启动也通过;Windows Core Audio 检测到活动、未静音的游戏音频会话和非零音频电平。
- 两个游戏目录均已安装并校验补丁。运行验证集中在版本 A;版本 B 本轮仅完成文件匹配、安装及校验。
**尚未证明的部分:**为什么宿主环境拒绝该游戏线程的亲和性设置,仍未进一步确定。独立测试线程可以接受相同掩码,所以不能把原因直接归为“掩码越界”“核心太多”“ES 处理器缺陷”或某个驱动。对已经释放或失效对象的使用有强证据支持,但本轮没有跟踪每一次内存分配与释放,不能声称完整证明了具体分配器层面的生命周期。
这是针对已确认触发条件的 **workaround(兼容性绕过)**,不是对 Wwise 源码进行完整的线程生命周期修复,也不是所有奥日 2 闪退的通用补丁。
## 2. 故障现象与最初线索
用户描述:加载画面中小精灵跑步时经常闪退;极少数可以进入游戏,却没有声音。此前已尝试限制 CPU 核心、禁用部分声卡或显卡,没有解决。两个游戏目录都出现问题。同系统版本的 VMware 虚拟机可以运行,其中使用的应该是版本 A。
这些信息最初只用于确定调查顺序:
| 线索 | 可以支持的判断 | 不能直接推出的结论 |
| --- | --- | --- |
| 闪退伴随偶尔无声 | 值得检查音频初始化是否共同失败 | 一定是物理声卡驱动损坏 |
| 两个版本都失败 | 应比较共同组件、用户配置及宿主环境 | 两份游戏所有文件都相同 |
| 同系统版本虚拟机可玩 | 环境差异值得调查 | 系统版本、显卡或 CPU 已被单独证明有问题 |
| 限制核心数未解决 | 简单减少游戏可用核心不是已验证方案 | 所有线程调度相关假设均被排除 |
虚拟机的具体 vCPU 拓扑、VMware 版本、音频驱动及游戏文件哈希,本轮没有直接采集。其可运行情况属于用户报告,不是严格控制变量实验。
## 3. 本机环境
### 3.1 系统、处理器与固件
| 项目 | 采集结果 | 来源与解释 |
| --- | --- | --- |
| 操作系统 | Windows 10 Pro for Workstations,22H2,x64 | Windows 注册表;中文名称为专业工作站版 |
| 系统构建 | `19045.6456` | `CurrentBuild` 与 `UBR` |
| Windows 处理器名称 | `Genuine Intel(R) 0000` | 处理器注册表字段 |
| CPU 标识 | `Intel64 Family 6 Model 186 Stepping 2` | 同上 |
| 固件系统产品名 | `i9-13900HK(ES)` | SMBIOS 对应注册表字段;不等于已独立鉴定零售型号 |
| 当前可用逻辑处理器 | 20 | .NET 运行时与进程亲和性查询;不是物理核心数 |
| 后续对照测试的进程/系统掩码 | `0xFFFFF / 0xFFFFF` | `GetProcessAffinityMask` |
| 物理内存 | 约 32 GB;崩溃报告记载 32600 MiB | 原始崩溃日志;本轮没有采集内存条型号、数量或频率 |
| 主板制造商字段 | `Intel` | 固件字段原样记录 |
| 主板产品字段 | `Z790M GAMING WIFI D5` | 固件字段原样记录 |
| BIOS 厂商 | American Megatrends International, LLC. | 固件字段 |
| BIOS 版本/日期 | `Leo-Patch 1.5` / `07/26/2024` | 固件字段 |
ES 表示 Engineering Sample,即工程样品标识。本报告记录这一环境特征,不据此归因。也没有从“20 个逻辑处理器”推算未经验证的物理核心数或大小核分布。
### 3.2 显示环境
游戏日志明确选择了 **AMD Radeon RX 9070 XT**,使用 Direct3D 11、功能级别 11.1,显存报告为 16188 MiB。显卡初始化已完成,不能仅凭机器装了多个显示组件就断定它们参与了这次崩溃。
| 注册的显示组件 | 驱动版本 | 驱动日期 | 说明 |
| --- | --- | --- | --- |
| AMD Radeon RX 9070 XT | `32.0.31041.1004` | 2026-08-17 | 本次游戏日志中的渲染设备 |
| NVIDIA Tesla P4 | `31.0.15.3770` | 2023-10-18 | 注册表中存在;未证明参与本次渲染 |
| Zako Display Adapter | `0.23.25.226` | 2026-04-25 | 注册表中存在 |
| Microsoft Remote Display Adapter | `10.0.19041.5794` | 字段为 2006-06-21 | 存在多个注册项,不代表同时连接多个活动设备 |
### 3.3 音频环境
| 注册的音频组件 | 驱动版本 | 驱动日期 |
| --- | --- | --- |
| Realtek High Definition Audio | `6.0.9239.1` | 2021-09-16 |
| AMD High Definition Audio Device | `10.0.1.41` | 2025-05-26 |
| Steam Streaming Speakers | `17.56.13.764` | 2017-07-20 |
| VB-Audio Virtual Cable | `3.3.1.7` | 2024-10-07 |
| AMD Streaming Audio Device | `1.0.0.14` | 2024-06-05 |
| Microsoft Streaming Service Proxy | `10.0.19041.1` | 2006-06-21 |
这里列的是注册信息,其中可能包含停用、断开或历史设备。本轮没有把某一虚拟音频设备认定为根因,也没有通过卸载驱动来实现修复。
环境采集脚本为 `ori-research/Collect-Environment.ps1`,快照为 `ori-research/environment.json`。它不采集账号名、序列号、许可证或网络地址。
### 3.4 两个游戏版本
| 项目 | 版本 A:目录名含空格 | 版本 B:目录名含下划线 |
| --- | --- | --- |
| Unity 引擎 | `2018.4.11f1 (0dbcfe58b2dc)` | `2018.4.24f1 (5ebab45375c4)` |
| 日志 Build Id | `5257` | `8978` |
| 日志 Project Revision | `140897` | `147855` |
| 日志 Revision Time | `2020-05-12 03:56:26` | `2020-11-18 06:35:46` |
| GameAssembly.dll 大小 | 74,333,696 字节 | 77,572,608 字节 |
| AkSoundEngine.dll 大小 | 2,719,232 字节 | 2,719,232 字节 |
| Wwise SDK | `2019.1.7 Build 7135` | `2019.1.7 Build 7135` |
| 音频 DLL 原始 SHA-256 | 两者相同,见第 8 节 | 两者相同,见第 8 节 |
| 本轮验证覆盖 | 动态故障复现、内存修正、普通启动、音频会话 | 补丁文件匹配、安装及校验 |
Build Id 和 Revision Time 来自日志,不能直接当作游戏平台销售版本号。不能只凭 Unity 或 Wwise 版本字符串判断另一个文件是否适用补丁,必须核对完整哈希。
## 4. 从日志中提取真正有区分力的证据
Unity 日志中出现了大量 `The referenced script ... is missing`、资源卸载信息和其他警告。它们数量多,却没有形成此次崩溃的直接证据链。
最有区分力的是重复出现的两条错误:
```text
Wwise: Cannot create streaming I/O device.
WwiseUnity: Failed to initialize the sound engine. Abort.
```
这里的 I/O 是 Input/Output,即输入输出。“streaming I/O device”指音频引擎内部读取声音数据的机制,不等于用户在设备管理器里看到的物理声卡。
崩溃栈大体如下:
```text
<unknown address>
AkSoundEngine.dll + 0x5B0C8 或 + 0x5B16E
KERNEL32!BaseThreadInitThunk
ntdll!RtlUserThreadStart
```
这表明故障发生在音频 DLL 启动的工作线程上,而不是把某一行普通 Unity 警告简单等同于原因。
### 4.1 导出符号名称可能误导
原始报告把音频栈帧标成 `AK::StreamMgr::SetFileLocationResolver`。没有完整 PDB 调试符号时,工具可能用附近的导出符号解释地址;这个名称不能证明真正执行的函数就是文件位置解析器。
本轮进一步计算模块相对地址并反汇编,才发现 `+0x5B0C5` 和 `+0x5B16B` 附近是虚函数间接调用。`+0x5B0C8`、`+0x5B16E` 分别是这些调用后的返回地址。
### 4.2 文本中的“写入冲突”也需要交叉检查
2026-09-16 原始 `error.log` 写的是 `Write to location ... caused an access violation`。但对应 `crash.dmp` 的异常记录为:
```text
ExceptionCode = 0xC0000005
ExceptionInformation[0] = 0x8
ExceptionInformation[1] = 与异常指令地址相同
```
根据微软的 `EXCEPTION_RECORD` 文档,参数 0 为 0、1、8 时,分别对应读取、写入、用户态 DEP 执行保护违规。本例是最后一种。[4]
结合栈中的间接调用和不属于正常模块代码区的目标地址,更符合“通过失效对象取得了错误的调用目标”,而不是仅仅向某个地址写数据。DEP 是 Data Execution Prevention,即数据执行保护;此次修复保留这个保护机制。
## 5. 动态实验设计及结果
诊断器使用 Windows 调试接口启动自己的子进程,没有附加到其他游戏进程,也没有安装系统钩子。它先校验原始 DLL 的 SHA-256,再在模块加载后设置软件断点。
软件断点把目标指令的首字节临时替换为 `INT3`。断点触发后读取寄存器,恢复原字节并把 RIP 指令指针退回原指令,使原指令正常执行。本轮各断点只捕获首次命中,不构成长期、多实例线程调用的完整追踪器。
Windows x64 调用约定把前几个整数或指针参数放在 RCX、RDX、R8、R9,返回值通常在 RAX。[5] 因此可在 `SetThreadAffinityMask` 前读取 RDX 中的掩码,在调用后读取 RAX。错误码通过目标线程 TEB 的 LastErrorValue 读取,而不是读取调试器自身的 `GetLastError`;本次 x64 TEB 偏移为 `0x68`,它属于诊断实现细节,不应推广为跨架构稳定接口。
### 5.1 实验对照
| 实验 | 修改内容 | 观察结果 | 意义 |
| --- | --- | --- | --- |
| 原始 DLL | 无业务逻辑修改,仅断点跟踪 | 优先级调用返回 1;亲和性调用返回 0,错误码 87;随后访问冲突 | 确认直接失败调用 |
| 截断掩码 | 调用前将 `0xFFFFFFFC` 改为 `0xFFFFC` | 仍返回 0、错误 87,随后崩溃 | 单纯去掉高位不是本机充分修复 |
| 自动调度 | 在进程内将指定 setter 写入的掩码设为 0 | 读取线程属性变为 `[0, 0, 131072]`,通过初始化及后续加载 | 支持回避显式线程绑定 |
| 独立线程探针 | 仅修改探针自身线程的亲和性,并恢复 | 包括 `0xFFFFC`、`0xFFFFFFFC` 在内的测试值返回成功 | 不能将失败概括为全系统普遍不支持 |
| 磁盘补丁后普通启动 | 无调试器、无运行时补丁 | 初始化通过,进入后续场景,检测到声音信号,用户主动退出 | 排除仅在调试器下才成功的情况 |
原始记录的线程参数为:
```text
nPriority = 0
dwAffinityMask = 4294967292 = 0xFFFFFFFC
uStackSize = 131072 = 128 KiB
SetThreadPriority -> 1
SetThreadAffinityMask -> 0
LastErrorValue -> 87 = ERROR_INVALID_PARAMETER
后续异常 -> 0xC0000005
```
微软文档规定线程亲和性应是进程允许范围的子集,并说明不允许的位可导致 `ERROR_INVALID_PARAMETER`。[3] 这给出了最初的合理假设。但本机截断实验仍失败,独立探针又成功,所以研究结论必须保留这些反例,不能只照文档把掩码越界写成最终根因。
### 5.2 时间和样本限制
最初 40 秒的跟踪没有等到音频模块加载,不能把这轮算作成功或失败。用户曾将游戏切到后台,可能影响加载推进。因此,本轮耗时不能用于比较启动性能。
自动调度实验总观察上限为 180 秒,其中从音频模块加载到脚本按计划停止约 118.6 秒;不是连续完整游玩 3 分钟。最后的普通启动没有自动关闭计时器,用户确认是自己关闭的。
29 份历史报告是已产生的崩溃样本,不包含全部启动尝试,且部分来自此前不同排障设置。`28/29` 是崩溃路径分类,不能当作游戏启动失败率,也不能据此计算补丁治愈率。
## 6. 线程生命周期问题的原理
### 6.1 关键静态位置
以下地址都是 RVA(Relative Virtual Address,模块相对虚拟地址),不是固定的运行时绝对地址:
| RVA | 观察到的操作 |
| --- | --- |
| `0x5B090` | 工作线程入口附近 |
| `0x5B0C5` | `call qword ptr [rax+0x18]` |
| `0x5B16B` | `call qword ptr [rax+0x10]` |
| `0x5B294` | 调用 `CreateThread`,创建标志为 0 |
| `0x5B2B9` | 调用 `SetThreadPriority` |
| `0x5B2BF` | 检查优先级调用结果 |
| `0x5B2D8` | 调用 `SetThreadAffinityMask` |
| `0x5B2DE` | 检查亲和性调用结果 |
| `0x5B2E3` 至 `0x5B2ED` | 失败路径关闭线程句柄并清零 |
| `0x5B450` 附近 | 清理函数;线程句柄存在时才等待线程 |
`CreateThread` 没有使用 `CREATE_SUSPENDED`。微软文档明确说明这种情况下线程可能在 `CreateThread` 返回前就开始执行。[2]
### 6.2 为什么关闭句柄不等于停止线程
句柄可以理解为访问系统对象的一张票。关掉票并不会让线程立即停止。微软 `CloseHandle` 文档也明确指出:关闭线程句柄不会终止对应线程。[1]
下面是从已观察控制流抽象出的伪代码,不是厂商源码,也不包含所有字段和分支:
```cpp
threadHandle = CreateThread(/* 创建后立即可运行 */);
if (!ConfigurePriorityAndAffinity(threadHandle)) {
CloseHandle(threadHandle);
threadHandle = nullptr;
return InitializationFailed;
}
// 后续清理函数中的关键关系
SignalStopEvent();
if (threadHandle != nullptr) {
WaitForSingleObject(threadHandle, INFINITE);
CloseHandle(threadHandle);
}
CloseSynchronizationEvents();
// 调用方之后可能销毁或复用线程依赖的对象。
```
问题在于失败路径让“有没有句柄”与“有没有存活线程”不再等价。句柄清空后,后续清理可能跳过等待,仍存活的线程继续访问已经失效的状态。
```mermaid
sequenceDiagram
participant M as 初始化线程
participant W as 音频读取线程
M->>W: CreateThread,立即可运行
M->>M: SetThreadPriority 成功
M->>M: SetThreadAffinityMask 失败,错误 87
M->>M: CloseHandle 并清空句柄
Note over M,W: 关闭句柄不会终止线程
M->>M: 初始化失败,进入清理
Note over M,W: 失去句柄后,后续等待可能被跳过
W->>W: 通过失效对象进行间接调用
W->>W: 目标地址不可执行,0xC0000005
```
静态代码直接证明了句柄关闭、清零及清理等待条件;动态实验直接证明了调用失败和随后发生的访问冲突。对具体对象已经被释放或复用的判断属于由这些证据强烈支持的生命周期推断,而不是本轮完整记录过的每一次内存管理操作。
### 6.3 为什么偶尔进入却无声
线程调度顺序和内存复用存在时间差。如果初始化失败,声音引擎可以处于未就绪状态;异常线程不一定在同一时刻撞上不可执行地址。这为“经常加载闪退、偶尔进入但无声”提供了统一且与实验吻合的解释。
它仍然不意味着所有无声现象都属于本问题:设备选择、静音、声道或其他驱动故障需要各自的证据。
## 7. 补丁设计与精确变更
### 7.1 选择语义上已有的关闭绑定方式
目标函数:
```text
CSharp_AkThreadProperties_dwAffinityMask_set
```
它把 managed/native 边界传入的掩码写入线程属性的 `+4` 字段。导出表检查只发现这个导出名称指向该入口,没有发现其他导出名称与之共用入口。这减少了误改其他导出 setter 的风险,但不应误读为完成了整个 DLL 的所有内部引用证明。
原始线程创建逻辑对掩码 0 有直接分支:不调用 `SetThreadAffinityMask`,继续检查其余初始化条件。补丁利用的就是本 DLL 已经存在的逻辑。
### 7.2 地址、机器码和反汇编
| 项目 | 值 |
| --- | --- |
| PE 首选镜像基址 | `0x180000000` |
| 函数 RVA | `0x121F0` |
| 首选基址下的函数地址 | `0x1800121F0` |
| 文件偏移 | `0x115F0` |
| 校验/替换区域长度 | 16 字节 |
| 实际变化字节数 | 8 |
RVA 和文件偏移分别属于内存映射与磁盘布局,不能混用。运行时还存在 ASLR(Address Space Layout Randomization,地址空间布局随机化),因此动态工具使用“实际加载基址 + RVA”。
原始字节:
```text
48 85 C9 74 03 89 51 04 C3 CC CC CC CC CC CC CC
```
原始逻辑:
```asm
test rcx, rcx
je original_ret
mov dword ptr [rcx+4], edx
original_ret:
ret
```
补丁字节:
```text
48 85 C9 74 07 C7 41 04 00 00 00 00 C3 CC CC CC
```
补丁逻辑:
```asm
test rcx, rcx
je patched_ret
mov dword ptr [rcx+4], 0
patched_ret:
ret
```
变化的 8 个文件偏移为:`0x115F4`、`0x115F5`、`0x115F6`、`0x115F8`、`0x115F9`、`0x115FA`、`0x115FB`、`0x115FC`。
保留空指针检查,修改对应跳转距离。新指令使用原函数尾部的填充区域,未侵入从 `0x12200` 开始的下一段代码。此函数没有新增栈操作或额外寄存器保存逻辑。补丁后的候选文件已经再次反汇编,确认得到上述指令,而不只是做了十六进制替换。
### 7.3 影响范围
补丁影响此 DLL 内经该导出 setter 配置的 Wwise 线程亲和性,可能不止一个读取线程。它允许这些线程继承 Windows 和进程当前的调度限制,不保证覆盖 DLL 内所有绕过 setter 的直接赋值路径。
显式绑定可能本来有性能目的,取消后需要关注长时间游玩的音频稳定性和性能。本轮没有完成帧时间、音频延迟、休眠唤醒或长时间游戏测试。
补丁没有修改 Steam 接口、授权校验、存档格式、系统安全机制或全局处理器设置。
## 8. 安装器的防误用设计
只接受以下原始文件:
```text
SHA-256:
8F90A4A3856540CB97D03CA07B4BB10B7A08F7B4AB3C524BE2BEC1AFDD29FECA
```
补丁后结果必须为:
```text
SHA-256:
62B2079A988D23EB251D4E83366AC04068A6F47E0D38EFE92A76DF022826F9F0
```
安装器 `Ori-AudioFix.ps1` 的行为:
1. 检查游戏进程是否还在运行;修改前必须退出。
2. 对所有指定目录预检查完整哈希、原始指令和已有备份。
3. 在本地根据用户自己的 DLL 构造候选字节,不下载替代游戏 DLL。
4. 建立不覆盖既有备份的 `.before-ori-affinity-fix` 文件,并验证备份。
5. 写入同目录临时文件,验证结果哈希,再替换目标并复核。
6. 已安装相同补丁时跳过重复修改;未知文件或备份异常时停止。
7. 还原时只接受经过验证的原文件备份。
每个文件的替换使用同目录暂存和移动,减少中途写坏目标的风险;多个目录之间不是事务,不能承诺第二个目录失败时第一个自动回滚。输出会逐目录报告结果,备份可以分别用于恢复。
工作区副本已通过“安装 → 再次安装 → 状态检查 → 还原 → 原始哈希一致”的完整测试。分享版安装器还会要求读者明确指定自己的游戏目录。
## 9. 音频验证到底证明了什么
普通启动后的 Core Audio 会话检查结果为:
```json
{
"active": true,
"muted": false,
"volume": 1.0,
"peak_over_3_seconds": 0.010620015673339367
}
```
检查只读取该游戏进程的音频会话状态和电平,不录制声音、不访问麦克风。非零电平说明游戏实际向 Windows 音频管线输出了信号,比单纯看到菜单或没有错误日志更有说服力。
这不等于通过麦克风验证扬声器发声,也不替代用户的长期试听。用户已确认最后一次进程退出是自己关闭,不是再次闪退;本轮没有记录用户对全部音效、长时间游玩或所有关卡的验收。
## 10. 他人如何判断是否属于同一问题
症状相似只是入口,建议按下面顺序核对:
1. 保存原有日志,避免下一次启动覆盖。Unity 报告一般在 `%LOCALAPPDATA%\Temp\Moon Studios\OriAndTheWilloftheWisps\Crashes`;常规日志一般在 `%USERPROFILE%\AppData\LocalLow\Moon Studios\OriAndTheWilloftheWisps`。
2. 检查是否同时出现 Wwise streaming I/O 初始化失败,以及音频工作线程的访问冲突。
3. 对 `AkSoundEngine.dll` 执行 `Get-FileHash -Algorithm SHA256`,与第 8 节比对。
4. 只有哈希匹配时才使用本补丁;未知版本重新定位函数、布局和控制流。
5. 在正常启动方式下验证加载、声音和退出,再延长实际游玩观察时间。
不应只凭 `0xC0000005` 判断适用:这是通用访问冲突代码,显卡、注入组件、坏内存或其他程序错误也可能产生它。
建议读者反馈:Windows 构建号、CPU 标识及逻辑处理器数、Unity/Wwise 版本、原始 DLL 哈希、关键日志、补丁前后结果和测试时长。无需公开完整转储、账号名、序列号或许可证。
## 11. 后续可继续研究的问题
- 查询失败游戏线程的处理器组、CPU Sets、相关调度状态及完整访问权限,解释为什么独立探针通过而游戏线程失败。
- 比较宿主与虚拟机的同一份 DLL、线程属性、API 返回值和环境,而不只比较操作系统版本号。
- 对对象分配、销毁和工作线程退出进行更细跟踪,严格验证释放后使用路径。
- 在版本 B 和其他环境中复现,并进行多次正常启动、长时间游玩和音频连续性测试。
- 若能修改源码,更完整的修复应保证配置失败时仍保留停止和等待线程的能力,确保线程停止后再销毁其依赖对象。是否把亲和性失败降级为非致命错误,要由音频引擎的设计约束决定。
这些属于后续研究议题,不是本轮已经完成的结论。
## 12. 证据索引与参考资料
本地证据:
| 路径(相对工作区) | 用途 |
| --- | --- |
| `ori-evidence/original-2026-09-16-error.log` | 原始崩溃文本 |
| `ori-evidence/runtime-original-long/trace.jsonl` | 原始调用失败的动态证据 |
| `ori-evidence/runtime-clamped/trace.jsonl` | 截断掩码仍失败的反例 |
| `ori-evidence/runtime-automatic/trace.jsonl` | 掩码 0 的临时修正实验 |
| `ori-evidence/normal-patched-1/game.log` | 普通启动的后续场景日志 |
| `ori-evidence/normal-patched-1/audio-status.json` | 音频会话测量 |
| `ori-research/environment.json` | 相关硬件、驱动与系统快照 |
| `ori-research/sources/manifest.json` | 官方文档下载结果和原始 URL |
原始转储本身保留在用户自己的崩溃目录,不放入公开分享包。前述异常参数来自对该转储异常流的只读解析。
官方资料于 2026-09-16 访问并保存本地文本快照:
1. [Microsoft:CloseHandle](https://learn.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-closehandle):关闭句柄与线程终止的区别。
2. [Microsoft:CreateThread](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createthread):创建标志及线程可能提前运行的时序。
3. [Microsoft:SetThreadAffinityMask](https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setthreadaffinitymask):掩码含义、返回值和错误条件。
4. [Microsoft:EXCEPTION_RECORD](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-exception_record):访问冲突参数 0、1、8 的含义。
5. [Microsoft:x64 calling convention](https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170):参数寄存器与返回值。
6. [Microsoft:System Error Codes 0–499](https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-):错误 87 的定义。
工具说明:使用本机已有 PowerShell 7.6.4、Python 3.14 标准库和 GNU objdump,未为此安装额外依赖。全文把本地实测、静态推断、用户报告和未验证事项分开记录,方便复核和后续补充。
## 13. 补充:限制 CPU 核心为什么不能与本补丁混为一谈
用户补充,网上曾看到“限制 CPU 核心”的解决办法。应把操作对象与证据层级分开记录:
| 操作 | 实际改变什么 | 本案例的证据 |
| --- | --- | --- |
| 限制整个游戏的进程亲和性 | 缩小游戏进程可使用的逻辑处理器集合 | 用户此前尝试未解决;保留的旧启动文件包含 `/affinity F`,即低 4 个逻辑处理器。本轮未做所有进程掩码的重新枚举实验 |
| 修改一条音频线程请求的掩码 | 调整 `SetThreadAffinityMask` 调用的参数 | 本轮实测把 `0xFFFFFFFC` 改成 `0xFFFFC`,仍返回错误 87,随后崩溃 |
| 让音频配置的掩码为 0 | 让该 Wwise DLL 走“不调用显式绑定 API”的既有分支 | 本轮临时修正及磁盘补丁后的普通启动均通过,检测到实际音频输出 |
`/affinity F` 中的 F 是十六进制 15,对应 4 个逻辑处理器,不能直接叫“4 个物理核心”。也不等于改了 BIOS、系统核心数或音频 DLL 中的掩码。
进程亲和性给出可运行集合;游戏自己创建线程后,仍可能再次设置线程亲和性。限制进程不会自动修正 DLL 的配置参数,也不会修复失败路径里的线程生命周期。某些设置还可能与线程显式请求范围发生冲突,需要查看实际 API 返回值。
限制核心在其他故障中可能通过降低并发、改变调度时序或绕开特定处理器拓扑起作用。这是一般原理层面的可能性,不是本机已经验证的原因;即使让竞争问题暂时不出现,也不能直接当作源码层面的修复。
**适合公开发表的准确结论:**“用户此前限制整个游戏可用核心未解决;本轮进一步证明,缩小音频线程请求掩码仍失败,而取消音频线程显式绑定有效。”不能改写成“限制 CPU 核心已经被证明对所有奥日 2 玩家无效”。
## 14. 方法归类:用于诊断与兼容性修复的逆向工程
本轮工作属于逆向工程:在没有原始源码的情况下,通过已编译程序的指令、导出表、运行行为和系统接口,推断局部实现及故障机制。
- **反汇编(静态分析):**将机器指令翻译成汇编,检查 `CreateThread`、绑定操作、句柄清理、条件分支和间接调用。本轮实际使用 GNU objdump。
- **动态分析:**让程序运行,在目标位置设置断点,读取寄存器、目标线程错误码和后续异常。
- **最小兼容性修补:**利用已经确认的函数边界及语义,只修改目标 setter,并验证正常启动和音频输出。
这不意味着恢复了完整 C++ 源码。报告中的 C++ 风格代码是解释控制关系的伪代码;原始变量名、类设计及全部调用方语义仍有缺失。PDB 符号不完整时,也必须警惕导出函数名被拿来解释邻近地址。
本次用途是定位故障、恢复兼容性和输出可复核的补丁;研究对象与改动都限于相关音频行为。结论的依据是日志、指令、输入参数、返回值和对照实验,每一步都可以保存与复核。
## 15. 网络方案核查:相同症状不等于相同故障
核查日期:2026-09-16。以下是公开资料核对,不代表本机执行过每一项办法。完整比较见工作区的《奥日2网络方案核查.md》;分享包中的文件名为 `COMMUNITY-REVIEW.zh-CN.md`。
| 方案 | 已读来源与证据 | 与本案例的关系 |
| --- | --- | --- |
| 限制 CPU 核心 | [性能教程](https://www.ghostarrow.com/how-to-improve-performance-in-ori-and-the-will-of-the-wisps)第 18 项针对游玩时接近 100% CPU 占用的卡顿,通过 Process Lasso 在阈值触发时减少核心 | 与本例启动时音频线程错误 87 不是同一份证据;本地三类亲和性操作的区别见第 13 节 |
| 切换声音输出、Sound 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 调整 | [RTX 5070 / 4K HDR 报告](https://steamcommunity.com/app/1057090/discussions/0/582806854239850133/)一次改变多项显示设置后成功 | 不能确定单项贡献;本机原版已在 1280×720 窗口调试中重现绑定失败,因此窗口化本身不够 |
| DXVK | [RTX 50 系列花屏帖](https://steamcommunity.com/app/1057090/discussions/0/695372304943750032/)作者更新报告图形破损消失 | 有图形兼容的成功报告;本机历史上有另一次 DXVK/DXGI 崩溃,不把它当音频修复 |
| 回退 Patch 2、检查系统预览版 | [官方 Patch 3.1](https://steamcommunity.com/app/1057090/discussions/0/2968397584537393710/)提供特定问题的回退分支;[开发者白屏帖](https://steamcommunity.com/app/1057090/discussions/0/3012312119189327120/)初步关联当时 Insider Dev 构建 | 是有条件的历史排障依据。本机为正式版 Win10 22H2,两份游戏的故障音频 DLL 又相同,不能因此断言重装或降级能修好 |
| 校验文件、修复依赖、调整驱动 | [启动排障清单](https://www.ghostarrow.com/ori-and-the-will-of-the-wisps-fix-wont-launch-black-screen-crashes)列出常见办法 | 对文件缺失、依赖加载或具体驱动回归可能有用;不能修正发布文件原有的线程清理逻辑 |
| 安装 Unity 引擎、各种“一键优化” | 同一启动清单提出安装 Unity,但没有给出相应机制或受控证据 | 游戏自带 Unity Player;无法从“使用 Unity 开发”推出“玩家必须安装编辑器”。作为通用修复依据不足 |
评价“玄学”时,指的是不区分场景、缺少对照却声称万能,而不是宣称某个设置永远不会影响程序。例如采样率或调度参数确实可能改变执行路径;但某一次成功,尤其多项修改后的成功,不足以确定因果。
[另一个无声讨论](https://steamcommunity.com/app/1057090/discussions/0/2968398851794293852/)中有采样率、设备、空间音频等多种建议,楼主却明确说未解决;数月后恢复,自己也不知道原因。因此不把建议列表误记为成功清单,也不把后来恢复直接归功于 Windows 更新。
公开页面留档位于 `ori-research/community-sources/`,获取结果见 `ori-community-articles-result.json`。本轮检索没有核对到同时包含本例相同 Wwise 调用、参数和错误 87 的独立复现报告;这不代表它不存在,也不构成首创声明。
@@ -0,0 +1,8 @@
@echo off
where pwsh.exe >nul 2>&1
if errorlevel 1 (
echo PowerShell 7 is required. This package does not install software.
pause
exit /b 1
)
pwsh.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Interactive.ps1" -Action Restore
@@ -0,0 +1,9 @@
fa1488f78c9c0a7806cd7ed11b4e207738c1e102e29f9fbf2fe7d5ca1944d036 README.zh-CN.md
87d36bdbdb545d888eef23cd74ce36a1124c3df8d7b37c022e8bf36cb20c7651 Ori-AudioFix.ps1
12f8dd5f0cc8b135b531048d15882aa06f772327a38f68ddd42b2d2c2691f7d2 Interactive.ps1
0f1180257e3f61b12b240146a9087e931cbbf923886fd7b07d2f36f3cb8b1a2f Install.cmd
1b33cfd1d4f9c74af333d1d34c6a24f284d915816560163c6f742b77967d72d8 Restore.cmd
f4f953ce6211cc6bb173d80b9462e76296051bbd568c90429c984738209b6718 RESEARCH.zh-CN.md
1a189e0699ff96be88304e321445cba2d2b4319f45216b8ff032e54d1b508e88 COMMUNITY-REVIEW.zh-CN.md
4d73ec917ffbabf4bed06d63b3b8131f9d040a22cd96c0e7238ac8f8fd619672 environment.json
497a0ed6b6f5e7bd8ef459c708a482a1dadea68811f31edb4650fd4bb928e7de EVIDENCE.json
@@ -0,0 +1,103 @@
{
"CollectedAt": "2026-09-16T22:09:51.9064948+08:00",
"OperatingSystem": {
"Product": "Windows 10 Pro for Workstations",
"Release": "22H2",
"Build": "19045.6456",
"Architecture": "x64"
},
"Processor": {
"ReportedName": "Genuine Intel(R) 0000",
"Identifier": "Intel64 Family 6 Model 186 Stepping 2",
"Vendor": "GenuineIntel",
"FirmwareSystemProduct": "i9-13900HK(ES)",
"AvailableLogicalProcessors": 20,
"CurrentProbeProcessAffinity": "0xFFFFF"
},
"Firmware": {
"SystemManufacturer": "Intel",
"BoardManufacturer": "Intel",
"BoardProduct": "Z790M GAMING WIFI D5",
"BIOSVendor": "American Megatrends International, LLC.",
"BIOSVersion": "Leo-Patch 1.5",
"BIOSDate": "07/26/2024"
},
"RegisteredDisplayDrivers": [
{
"Description": "Zako Display Adapter",
"DriverVersion": "0.23.25.226",
"DriverDate": "4-25-2026",
"Source": "Registered driver class; presence does not prove device is active"
},
{
"Description": "AMD Radeon RX 9070 XT",
"DriverVersion": "32.0.31041.1004",
"DriverDate": "8-17-2026",
"Source": "Registered driver class; presence does not prove device is active"
},
{
"Description": "NVIDIA Tesla P4",
"DriverVersion": "31.0.15.3770",
"DriverDate": "10-18-2023",
"Source": "Registered driver class; presence does not prove device is active"
},
{
"Description": "Microsoft Remote Display Adapter",
"DriverVersion": "10.0.19041.5794",
"DriverDate": "6-21-2006",
"Source": "Registered driver class; presence does not prove device is active"
},
{
"Description": "Microsoft Remote Display Adapter",
"DriverVersion": "10.0.19041.5794",
"DriverDate": "6-21-2006",
"Source": "Registered driver class; presence does not prove device is active"
},
{
"Description": "Microsoft Remote Display Adapter",
"DriverVersion": "10.0.19041.5794",
"DriverDate": "6-21-2006",
"Source": "Registered driver class; presence does not prove device is active"
}
],
"RegisteredAudioDrivers": [
{
"Description": "AMD High Definition Audio Device",
"DriverVersion": "10.0.1.41",
"DriverDate": "5-26-2025",
"Source": "Registered driver class; presence does not prove device is active"
},
{
"Description": "Steam Streaming Speakers",
"DriverVersion": "17.56.13.764",
"DriverDate": "7-20-2017",
"Source": "Registered driver class; presence does not prove device is active"
},
{
"Description": "VB-Audio Virtual Cable",
"DriverVersion": "3.3.1.7",
"DriverDate": "10-7-2024",
"Source": "Registered driver class; presence does not prove device is active"
},
{
"Description": "Realtek High Definition Audio",
"DriverVersion": "6.0.9239.1",
"DriverDate": "9-16-2021",
"Source": "Registered driver class; presence does not prove device is active"
},
{
"Description": "Microsoft Streaming Service Proxy",
"DriverVersion": "10.0.19041.1",
"DriverDate": "6-21-2006",
"Source": "Registered driver class; presence does not prove device is active"
},
{
"Description": "AMD Streaming Audio Device",
"DriverVersion": "1.0.0.14",
"DriverDate": "6-5-2024",
"Source": "Registered driver class; presence does not prove device is active"
}
],
"PowerShellVersion": "7.6.4",
"Privacy": "No account names, machine serial numbers, license keys, network addresses or dump contents collected"
}