Skip to content

修复长时间对局中的资源泄漏:崩溃、静音与菜单退化 - #2

Merged
NoCLin merged 7 commits into
ra2-games:mainfrom
Yohoa:fix/long-session-leaks
Sep 21, 2026
Merged

NoCLin merged 7 commits into
ra2-games:mainfrom
Yohoa:fix/long-session-leaks

Conversation

@Yohoa

@Yohoa Yohoa commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

背景

长时间对局(约 30 分钟,多电脑混战)会依次出现三个现象:先变慢,然后声音完全消失,最后崩溃在 动态 stub 区不足。本 PR 修复这三个现象的根因,以及排查过程中发现的同类缺陷。

排查用的是原始 game.exe(sha256 06f99496…)加玩家自有资源,在无头 Chrome 中自动开局并定时采样模拟器内部状态。

已确认修复的三个现象

崩溃:CoCreateInstance 对客体注册类的桥接,每次调用都从动态 stub 区 bump 分配一段代码且从不回收。RA2 的每个单位创建移动组件都会走这里。实测(北极圈,4 个电脑)激烈交战时约 1000 逻辑帧泄漏约 600 段,约 4 万帧耗尽 1MB 区域;改用可复用回调槽后,同场景 stub 指针全程不动。

声音消失:AudioWorkletProcessor 在收到 destroy 后仍从 process() 返回 true,浏览器因而无法回收已断开的节点。实测音频线程上累积到 73 个僵尸处理器(同时在用的为 0),音频渲染跟不上实时,AudioContext.currentTime 每 30 秒只前进约 5 秒,游戏随即静音。销毁后返回 false 即可回收。玩家侧已确认修复后长局不再丢声音。

变慢:玩家侧确认随上述修复一并消失。推测与音频线程饱和后游戏反复查询播放位置、加锁写入有关,未单独证实。

同类缺陷

  • 内存不足时返回成功但对象在地址 0:createComObject / createSurface / createPalette / createSoundBuffer 在堆耗尽时返回“成功”,调用方随后向客体低地址内存写入画面与 PCM 数据。现统一返回 DDERR_/DSERR_OUTOFMEMORY。
  • 线程 id 与栈永不回收:线程退出后栈(最多 1MB)不释放、id 不复用。测试证实 64 次创建/退出后 CreateThread 全部失败,且 4MB 客体内存永久丢失。现在退出后释放栈、关闭句柄后回收 id,并在复用 id 时清空兼容锁深度与 x87 保存区(否则新线程会继承上一个线程的状态)。句柄关闭后仍保留映射,避免已阻塞的等待方失效。
  • 窗口镜像表被耗尽:镜像索引为 hwnd - 0x2000 且无回收,约 30 次菜单切换后所有新窗口永久退化为完整 hypercall。改为按 2 的幂环绕(896→512),每项记录属主 HWND,客体桩比对不符即回退;槽位释放时交还给仍存活的窗口。HWND 本身不复用:实测复用会让新对话框继承已销毁窗口的消息,菜单变空白。
  • EnumDisplayModes:与崩溃同类的每次调用泄漏,改用回调槽与复用的描述符。
  • EnumSessions:大厅轮询期间每次为每个房间分配且不释放,改为延迟一代释放。
  • OleSaveToStream 桥接:同样的每次分配与 20 字节永久堆占用,改用回调槽,收尾在 CLI 下释放。RA2/YR 仍按原策略跳过该链路,本 PR 不改变该策略。

存档路径

存档此前在 KERNEL32.DLL!GetDateFormatA 处以未实现中止。本 PR 实现 GetDateFormatA / GetTimeFormatA,按客体字节展开格式串(双字节字符的尾字节可能正好是 y、d,按字符处理会误判为字段),默认使用纯数字 ASCII 图案以避免与游戏代码页不符。

实测储存界面已能正常打开并显示时间戳,不再中止。但存档仍未真正写盘:RA2/YR 通过 skipGuestOleSaveToStream 跳过对象序列化,文件因而没有内容。该策略是仓库既有的有意选择(客体 Save 返回时会在 IRQ 边界破坏 v86),本 PR 未触碰,存档能力仍不可用。

开发诊断

新增仅在 import.meta.env.DEV 下每 30 秒输出的两类日志:模拟器侧的堆占用、动态 stub 用量、声音缓冲与逻辑帧率;浏览器侧的 AudioContext 状态、音频时钟增量、各类播放节点数量与本周期播放/写入量,并在 AudioContext 状态变化时立即告警。定位声音消失正是靠音频时钟增量从 +30s 降到 +5s 这条线索。生产构建不会启动定时器。该部分为独立提交,如不需要可单独剔除。

验证

  • pnpm run check:119 个测试文件、822 个测试 + 构建,全部通过。
  • 新增 11 个回归用例,均在修复前失败。
  • 真实游戏:原始 game.exe 启动、菜单反复开关 6 轮、北极圈与岛屿之战进入战场并持续推进;修复前后同场景对比 stub 与音频线程处理器数量。
  • 玩家侧长局(逻辑帧 10.6 万到 22.5 万,约 55 分钟)确认不再崩溃、不再变慢、不再丢声音。

未验证范围

  • 变慢与音频线程饱和之间的因果关系未单独证实。
  • 点击储存后的实际写盘流程仍不可用,原因见上。
  • worker 模式下 getState 恒返回 null,游戏的播放位置只能按墙上时钟推算,与主线程模式行为不一致。本 PR 未改动,作为已知问题记录。
  • 多人联机路径未做真实对局验证。

🤖 Generated with Claude Code

@shiny

shiny commented Sep 21, 2026

Copy link
Copy Markdown

是的就是我遇到的问题!当时声音消失后我又换了一个蓝牙耳机重连,变成一种很奇怪的卡顿、机械的声音。再玩下去就崩溃了。

Yohoa and others added 7 commits September 21, 2026 20:55
长局(约 30 分钟)会崩溃、变慢、丢声音,根因是多处只增不减的资源,以及
内存不足时报告成功的路径。真实游戏(北极圈、岛屿之战)验证:旧代码在激烈
交战中每 1000 逻辑帧泄漏约 600 个 stub,约 4 万帧耗尽 1MB 动态 stub 区;
修复后同场景 stub 指针全程不动。

- ole32: CoCreateInstance 的两个桥接改用可复用的回调槽,不再每次调用从
  动态 stub 区 bump 分配;redirectGuestCoCreate 的暂存数据移入槽位尾部,
  同时不再每次泄漏 20 字节堆内存。
- state: 回调桥长度检查提前到暂存区之前,避免代码覆盖槽位尾部数据。
- directx: createComObject/createSurface/createPalette/createSoundBuffer 在
  堆耗尽时返回失败,调用方报告 DDERR_/DSERR_OUTOFMEMORY。此前它们返回
  “成功”但把对象写到客体地址 0,游戏会向低地址内存写入画面与 PCM 数据。
- directx: EnumDisplayModes 改用可复用回调槽与复用的 DDSURFACEDESC 暂存,
  不再每次调用泄漏 stub 与 108 字节堆内存。
- kernel32: 线程退出后释放栈内存,句柄关闭后回收线程 id;复用 id 时清空
  兼容锁深度与 x87 保存区。此前 64 次创建/退出后 CreateThread 全部失败,
  且每个线程的栈(最多 1MB)永久泄漏。句柄关闭后仍保留映射,等待方不受影响。
- kernel32: 实现 FileTimeToLocalFileTime / FileTimeToSystemTime,存档流程
  此前在这里因未实现而中止;偏移越界时按 Windows 语义失败而非回绕。
- win32/pe/state: 客体窗口镜像表改为按 2 的幂环绕(896→512),每项记录属主
  HWND,快速桩比对不符即回退 hypercall。此前每个创建过的窗口永久占用一个
  槽位,约 30 次菜单切换后所有新窗口永久退化为完整 hypercall。HWND 本身
  不复用:游戏会缓存旧句柄,复用会让新对话框继承已销毁窗口的消息。
- dplayx: EnumSessions 的客体暂存延迟一代释放,大厅轮询不再持续泄漏堆。
- pcmStreamWorklet: 处理器收到 destroy 后 process() 返回 false。此前已断开
  的节点因为始终声称仍在处理而无法被回收,真实对局中音频线程上累积到 73 个
  僵尸处理器(同时在用的为 0),音频渲染跟不上实时,游戏声音消失。

验证:pnpm run check(119 个测试文件、817 个测试 + 构建);新增回归测试在
修复前均失败。真实游戏验证:原始 game.exe 06f99496 启动、菜单反复开关 6 轮、
北极圈与岛屿之战进入战场并持续推进。未验证:30 分钟以上真实对局的声音与
帧率表现需玩家侧确认。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
长局问题只在真实对局里暴露,此前只能靠崩溃报告倒推。这些日志用于区分
“游戏侧不再输出声音”“浏览器音频被挂起”“音频设备跟不上实时”,并观察堆、
动态 stub 区与声音缓冲区是否随时间增长。定位声音消失正是靠音频时钟增量
从 +30s 逐步降到 +5s 这一条线索。

- vmCore: 运行中每 30 秒输出逻辑帧率、堆占用与空闲块、动态 stub 用量、
  声音缓冲与表面数量;随轮询定时器一起清理。
- audio: WebAudioPcmSink 输出 AudioContext 状态、音频时钟增量、各类播放
  节点数量与本周期 Play/Stop/写入量;AudioContext 状态变化立即告警。
  worklet 额外上报音频线程上存活的处理器数量,用于发现无法回收的节点。
- stateGraphics: 新增只读的资源计数查询,供上述诊断使用。

仅在 import.meta.env.DEV 且非测试环境下启用,生产构建不会启动定时器。

验证:pnpm run check(119 个测试文件、817 个测试 + 构建);真实游戏中确认
两类日志均按 30 秒输出,且生产构建产物中不含定时器启动代码。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
存档界面此前在 GetDateFormatA 处以未实现中止。两个函数按 Win32 语义实现:
容量为 0 时返回所需长度,容量不足时置 ERROR_INSUFFICIENT_BUFFER 并返回 0,
非法 SYSTEMTIME 置 ERROR_INVALID_PARAMETER,空指针表示当前本地时间,星期几
由日期推导而非采信调用方字段。

格式串按客体字节展开而非先解码:双字节字符的尾字节可能正好是 'y' 或 'd',
按字符处理会把它误判为字段。引号内为字面量('' 表示一个引号),DBCS 前导
字节连同尾字节原样透传。

默认格式使用纯数字的 ASCII 图案(日期 yyyy/M/d,时间 H:mm:ss),不输出本地化
的月份、星期与上午/下午:shim 对所有安装只报告一个 LCID,本地化名称必须与
游戏所用代码页完全一致才不会乱码。调用方显式传入格式串时仍支持 ddd/MMMM
等字段,此时使用 ASCII 名称。

验证:pnpm run check(119 个测试文件、822 个测试 + 构建);新增 5 个用例覆盖
默认格式、自定义格式(含 DBCS 尾字节为 ASCII 字母的字面量)、容量查询、缓冲
不足与非法输入。真实游戏验证:原始 game.exe 06f99496 进入战场后打开储存界面,
不再出现未实现中止,槽位正确显示 2026/9/20 1:23:36。未验证:点击储存后的实际
写盘流程,自动化点击未能触发保存,需在真人操作下确认。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
该桥接此前每次调用都从动态 stub 区 bump 分配代码,并永久占用 20 字节堆内存
存放 CLSID 与写入计数。改为复用回调槽,暂存数据放在槽位尾部;收尾在 CLI 下
释放槽位后再返回,与其他客体回调桥一致。

RA2/YR 当前仍通过 skipGuestOleSaveToStream 跳过这条链路(客体 Save 返回时会在
IRQ 边界破坏 v86),本次不改变该策略,仅修复桥接本身的资源占用与返回路径。
对应测试改为断言新的 push/ret 收尾,仍校验返回地址与三次客体 vtable 调用。

验证:pnpm run check(119 个测试文件、822 个测试 + 构建)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
…caching and signed length validation

Co-Authored-By: Claude Code <noreply@anthropic.com>
…escriptor

Co-Authored-By: Claude Code <noreply@anthropic.com>
@NoCLin
NoCLin force-pushed the fix/long-session-leaks branch 2 times, most recently from 77b4d03 to 2f0b3aa Compare September 21, 2026 13:18
@NoCLin
NoCLin merged commit f25986b into ra2-games:main Sep 21, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants