Summary
UE5.8 Take Recorder Niagara Sim Cache 崩溃:根因定位与修复方案
0. 文档状态
| 项目 | 内容 |
|---|---|
| 记录日期 | 2026-08-06(第二轮独立调查,取代旧报告的"未定位到引擎源码函数"结论) |
| 前置文档 | UE58_Niagara_TakeRecorder_SimCache_Crash_Report.md(现象与测试矩阵仍有效,其"当前推断"章节已被本文取代) |
| 调查状态 | 已定位到引擎源码级根因,崩溃函数、触发数据、竞态时序均有独立证据 |
| 修复状态 | 项目级自动修复钩子已安装(Content/Python/tr_niagara_cache_fix.py),待真实录制回归验证 |
| 引擎版本 | 5.8.1-56057345+++UE5+Release-5.8(Launcher 安装版,含完整源码,无 PDB) |
1. 一句话根因
Take Recorder 停止录制时在同一帧内同步保存了 take 资产,而 Niagara Sim Cache 的终结函数 EndWrite() 被推迟到下一帧才执行且不标脏包——导致磁盘上的缓存永远带着未转换的"插值映射表"(内容是粒子 UniqueID 而非帧间索引)。一旦资产从磁盘重新加载(编辑器重启,或 MRQ 触发 GC 后重载),任何"存活帧→空帧"边界上的插值读取都会对空缓冲区解引用,产生 EXCEPTION_ACCESS_VIOLATION reading 0x0。
2. 崩溃点符号化(无 PDB,导出符号 + 反汇编定位)
52 个崩溃转储中 7 种不同 PCallStackHash 实为同一崩溃点,顶部 5 帧偏移完全一致,仅调用入口不同(游戏线程 UNiagaraComponent::TickComponent 或 TaskGraph 异步任务):
| 偏移 | 函数(导出符号最近邻 + 运行时函数表) |
|---|---|
+84c261 |
UNiagaraSimCache::Read(float, FNiagaraSystemInstance*)(EXACT) |
+84ec79 |
UNiagaraSimCache::ReadFrame(int32, float, FNiagaraSystemInstance*)(EXACT) |
+84d179 |
FNiagaraSimCacheHelper::ReadDataBuffer(内联于 NiagaraSimCache.cpp 编译单元) |
+84ce1b |
ReadFloatBuffers/ReadCustomBuffers 内部帧 |
+7efd15 |
插值拷贝函数(InterpFloats/InterpPositions 家族)内的崩溃指令 |
崩溃指令反汇编(UnrealEditor-Niagara.dll RVA 0x7efd15):
7efcfc: mov rax, [rsi] ; InterpMapping 数组
7efd08: mov edx, [rax+rcx*4] ; idx = InterpMapping[i]
7efd0b: cmp edx, -1 ; INDEX_NONE 则跳过帧B
7efd15: movss xmm0, [rbp+rdx*4] ; ← 崩溃:帧B FloatData 基址(rbp)为 NULL
7efd1b: subss xmm0, xmm1 ; lerp(A, B, FrameFraction)
语义:Dest[i] = lerp(SourceA[i], SourceB[InterpMapping[i]], FrameFraction)。当插值目标帧 B 的粒子数为 0 时其 FloatData 为空数组(GetData()==nullptr);此时只要 InterpMapping[i] != INDEX_NONE 就必然读空指针。
3. 引擎源码证据链(均为 5.8.1 随引擎安装的源码)
-
插值映射的正确形态由
EndWrite()生成
Niagara/Private/NiagaraSimCache.cpp:785-870——UNiagaraSimCache::EndWrite()把每帧InterpMapping从"写入时暂存的粒子 UniqueID"转换为"指向下一帧同 ID 粒子的索引,粒子消亡则 INDEX_NONE",并把最后一帧全部置 INDEX_NONE。
关键事实:UniqueID 是从 0 开始的小整数,未转换时看起来就像合法索引,且写入路径(NiagaraSimCacheHelper.h:719-725 WriteDataBuffer)暂存的正是 UniqueID。 -
Take Recorder 的 EndWrite 被推迟一帧
NiagaraSimCaching/.../MovieSceneNiagaraTrackRecorder.cpp:FinalizeTrackImpl()(第 198-201 行)只设置bRequestFinalize = true;- 真正的
EndWrite(true)在OnRecordFrame(绑定于GEngine::OnPostEditorTick,第 257-275 行)里执行——即停止录制后的下一次编辑器 Tick; EndWrite前后没有任何Modify()/MarkPackageDirty()。
-
take 资产在同一帧内被同步保存(早于 EndWrite)
TakeRecorder.cpp::StopInternal:第 1337 行Sources->StopRecording(...)(内部触发各 track recorder 的 Finalize 标记)→ 第 1409/1415 行同步保存根序列;
TakeRecorderSources.cpp::StopRecording第 700-717 行同步保存所有 subsequence 资产(Niagara Cache 的 SimCache 对象正是 outer 在 subscene 的 section 上)。
⇒ 保存发生在 EndWrite 之前,且 EndWrite 之后包不脏、永远不会再落盘。 -
插值是引擎强制开启的,不是项目配置
MovieSceneNiagaraCacheTrack.cpp:85-86:每个 Take Recorder 创建的 Niagara Cache section 都被强制CacheParameters.bAllowInterpolation = true; bAllowVelocityExtrapolation = true;(FNiagaraSimCacheCreateParameters本身的默认值是 false)。
⇒ 所有 UE5.8 用户用 Take Recorder 录 Niagara 都会生成带插值映射的缓存,全部暴露在此竞态下。
4. 数据证据(本工程实测)
4.1 缓存内容(无头探针,read_position_attribute 逐帧计数)
两把枪的缓存(Scene_1_05 两个 subscene 内嵌 NiagaraSimCache_0,30 帧,时长 0.9667s):
Actor1: [16, 10, 0, 0, ..., 0] ← 帧0=16粒子, 帧1=10, 帧2起全空
Actor2: [16, 12, 0, 0, ..., 0]
死亡边界帧对 = (帧1 → 帧2空)。枪口闪光只存活 2 帧,其余 28 帧为空帧。
section CacheParameters 实测:allow_interpolation=True, interpolate_all_types=True, allow_velocity_extrapolation=True(RenderingOnly 捕获模式)。
4.2 磁盘字节证据(毒资产指纹)
对两个 subscene .uasset 的二进制扫描:
- 偏移
0x13d1d处均存在连续递增 uint32 序列 0..15(帧 0 的 InterpMapping = 原始 UniqueID,16 个粒子按出生顺序编号); - 全文件不存在任何 ≥4 dword 的
0xFFFFFFFF串——若EndWrite执行过,帧 1 的映射应为 10~12 个连续INDEX_NONE(0xFFFFFFFF)。
⇒ 两份缓存在磁盘上均为未终结的"生"数据。这同时提供了免崩溃的毒资产检测指纹:
- 毒:帧 0 映射为纯递增 0..N-1 且无 FF 串;
- 健康:帧(死亡边界)映射为连续
FFFFFFFF。
4.3 运行时复现
- 无 Sequencer/MRQ/TakeRecorder 参与:无头编辑器中
SetSimCache+ DesiredAge 扫描 Actor1 缓存 → 进程在死亡边界年龄段崩溃(exit -1,日志戛然而止)。 - 用户 52 个崩溃转储中全部 AV 崩溃与该签名一致(含
F872B158...主哈希,读地址 0x0)。 - 注意:
-RenderOffscreen无头编辑器不可靠(视口非实时 → Niagara 组件不 tick → 读取路径不执行,会产生假阴性)。运行时验证必须在真实编辑器或-game模式做。
5. 症状完整解释
| 症状 | 机制 |
|---|---|
| 刚录完直接播放 sequence 没问题 | 停止录制的下一帧 EndWrite 修正了内存中的映射表;本会话内评估用的都是这份健康内存数据 |
| 进 MRQ 闪退 | MRQ 渲染会关闭 Sequencer 编辑器并激进 GC → subscene 资产从磁盘重载(生数据);MRQ 逐帧确定性采样必然落进"存活帧→空帧"的插值窗口 |
| 重启编辑器后连播放都闪退 | 一切从磁盘加载,播放时实时采样落进死亡边界窗口即崩(窗口宽度=该帧对的实际录制间隔,命中与否与帧时序有关,故偶发"第一遍过、第二遍崩") |
| 旧报告 T09/T10"第一枪过、第二枪崩" | 两份缓存都是毒资产;差异只在死亡边界窗口的宽度/位置与采样时序的命中概率 |
| 崩溃前后资产 SHA-256 不变 | 本质不是资产被"写坏",而是从未被写对;EndWrite 修正只存在于内存,从不落盘 |
6. 修复方案
6.1 已安装:项目级自动修复钩子(零引擎改动)
Content/Python/tr_niagara_cache_fix.py(由 Content/Python/init_unreal.py 自动加载):
- 监听
TakeRecorderSubsystem.TakeRecorderFinished; - 等 5 个编辑器 Tick(确保推迟的
EndWrite已执行、内存映射已转换); - 递归查找该 take 及其所有 subscene 中带
MovieSceneNiagaraCacheTrack的序列,强制重存(save_loaded_asset(only_if_is_dirty=False))。
效果:磁盘资产 = 终结后的健康数据,重启/MRQ 均安全。对录制流程零侵入。
6.2 现有毒 take 的处置
已存在的 Scene_1_04、Scene_1_05 各 subscene 缓存无法脚本原位修复(EndWrite 未暴露给脚本,内存中也已是生数据)。选项:
- 重录 take(推荐,钩子已装,新 take 自动健康);
- Sequencer 里对 Cache Track 执行重录(Cache re-record,走
CacheTrackRecorder流程),完成后手动保存 subscene——保存发生在 EndWrite 之后,同样治愈; - 维持旧规避:静音 Cache Track + Auto Activate 实时特效。
6.3 可选加固:关闭插值录制
录制前把 Cache section 的 CacheParameters.bAllowInterpolation / bAllowVelocityExtrapolation 设为 false:缓存不再生成插值映射,彻底免疫此竞态(30fps 录制 30fps 渲染无画质损失)。可作为对钩子的双保险,但装了 6.1 后非必需。
6.4 引擎级正确修复(建议随 bug 报告提交 Epic)
任选其一即可根治:
UMovieSceneNiagaraTrackRecorder::FinalizeTrackImpl()内同步调用EndWrite(不再等下一帧);或OnRecordFrame的 finalize 分支在EndWrite后对 SimCache 的包MarkPackageDirty()(让后续保存能落盘);或UTakeRecorderSources::StopRecording把 subscene 保存推迟到所有 track recorder 真正终结之后。- 防御性修复:
FNiagaraSimCacheHelper::ReadCustomBuffers在帧 BFloatData为空时跳过插值(按 A 帧直读),使生数据最多表现为画面错误而非崩溃。
7. 验收清单(对应旧报告第 13 节)
- 钩子机制端到端验证(2026-08-06 无头编辑器实测):TakeRecorderFinished 委托触发 → 5 tick 后自动重存正确的 subscene,引擎日志
[TRNiagaraCacheFix] installed/re-saved 1/1 sequence(s)(测试 takeScene_1_06,无头下 Niagara 不 tick 故只捕 1 帧,字节指纹判读需真实编辑器录制的多帧样本); - 真实编辑器录制新 take
Scene_1_07(2026-08-06):暴露钩子 v1 缺陷——面板 UI 发起的录制不会广播TakeRecorderSubsystem.TakeRecorderFinished,钩子未触发,4 个 subscene 落盘即毒指纹(0..15原始 UniqueID、零 FF 串)。钩子 v2 已加轮询兜底(is_recording()状态沿),任何发起方式均可捕获; - 会话内手动触发
_resave(re-saved 4/4)后字节指纹翻转为健康态:每个 subscene 出现 10~13 dword 的FFFFFFFF死亡边界映射串、原始递增串消失(Actor2 残留0..6为正确的原位存活重映射)——毒→愈的同资产对照证据成立; - 新 take
Scene_1_07(治愈后):MRQ/MRG 连续渲染 2 次不闪退、特效画面正确(2026-08-06 用户实测;唯一弹窗为既有的 TSRFTSRRejectShadingCSPSO 预缓存 ensure,哈希8AC020C8,自 08-03 即存在,与本 bug 无关,可用r.PSOPrecache.Validation=0关闭); - 新 take:冷重启后父序列连续播放 ≥5 次不闪退(建议顺手验证);
- 旧毒 take(Scene_1_04/1_05)重录后同样通过上述检查。
8. Epic 报告模板(更新版)
UE 5.8.1 Take Recorder + Niagara Sim Cache: recorded take assets are saved
before UNiagaraSimCache::EndWrite runs, leaving un-finalized interpolation
mappings on disk -> guaranteed EXCEPTION_ACCESS_VIOLATION (read 0x0) on reload.
Chain:
1. UMovieSceneNiagaraCacheTrack::CreateNewSection forces
CacheParameters.bAllowInterpolation = true (MovieSceneNiagaraCacheTrack.cpp:85).
2. During recording, WriteDataBuffer stages raw particle UniqueIDs in each
frame's InterpMapping (NiagaraSimCacheHelper.h:719-725).
3. UNiagaraSimCache::EndWrite converts these to next-frame indices /
INDEX_NONE (NiagaraSimCache.cpp:812-870). For Take Recorder this call is
deferred to the next editor tick (MovieSceneNiagaraTrackRecorder.cpp:198,270)
and does not dirty the package.
4. UTakeRecorder::StopInternal saves the take and every subscene synchronously
in the same stack (TakeRecorder.cpp:1409, TakeRecorderSources.cpp:714) -
i.e. BEFORE EndWrite. The corrected mapping never reaches disk.
5. Raw UniqueIDs alias valid indices. After the asset is reloaded (editor
restart, or MRQ closing Sequencer + GC), any interpolated read whose next
frame has fewer/zero particles dereferences a null FloatData buffer:
UNiagaraSimCache::Read -> ReadFrame -> interp copy, crash offsets
+7efd15/+84ce1b/+84d179/+84ec79/+84c261 in UnrealEditor-Niagara,
PCallStackHash F872B158DE32273B333D43CEE376A5A37A52D6CD.
Repro (100%, no MRQ needed): record any burst-type Niagara effect (particles
die before recording stops) with Take Recorder; restart the editor; play the
take (or set the cache on a component with DesiredAge sweeping across the
last-alive -> first-empty frame pair).
Disk evidence: the saved subscene uasset contains the frame-0 mapping as a
strictly ascending 0..N-1 uint32 run and no INDEX_NONE runs anywhere;
re-saving the asset after EndWrite has run produces the expected INDEX_NONE
runs and eliminates the crash.
Suggested fixes: call EndWrite synchronously in FinalizeTrackImpl (or dirty
the package after the deferred EndWrite / save subscenes after finalize), and
defensively skip interpolation in FNiagaraSimCacheHelper::ReadCustomBuffers
when the target frame's buffers are empty.
9. 本轮新增工具
| 工具 | 位置 | 用途 |
|---|---|---|
| 自动修复钩子 | Content/Python/tr_niagara_cache_fix.py |
录制后自动重存,根治新 take |
| 任务注入 shim | Content/Python/init_unreal.py |
加载钩子;CLAUDE_UE_TASK 环境变量注入无头自动化脚本 |
| 缓存探针 | scratchpad probe_cache.py |
无崩溃 dump 缓存参数/逐帧粒子数 |
| 毒资产字节检测 | 见 4.2 指纹(可按需脚本化) | 免运行时判断 take 是否健康 |
| 符号化脚本 | scratchpad symbolicate.py / disasm.py |
无 PDB 的导出符号最近邻定位 + 反汇编 |
What Type of Bug are you experiencing?
Rendering (Graphics / Niagara)
Steps to Reproduce
| Take Recorder刚录完包含Niagara的片段,直接播放 sequence 没问题 | 停止录制的下一帧 EndWrite 修正了内存中的映射表;本会话内评估用的都是这份健康内存数据 |
| 进 MRQ 闪退 | MRQ 渲染会关闭 Sequencer 编辑器并激进 GC → subscene 资产从磁盘重载(生数据);MRQ 逐帧确定性采样必然落进"存活帧→空帧"的插值窗口 |
| 重启编辑器后连播放都闪退 | 一切从磁盘加载,播放时实时采样落进死亡边界窗口即崩(窗口宽度=该帧对的实际录制间隔,命中与否与帧时序有关,故偶发"第一遍过、第二遍崩") |
| 旧报告 T09/T10"第一枪过、第二枪崩" | 两份缓存都是毒资产;差异只在死亡边界窗口的宽度/位置与采样时序的命中概率 |
| 崩溃前后资产 SHA-256 不变 | 本质不是资产被"写坏",而是从未被写对;EndWrite 修正只存在于内存,从不落盘 |
Expected Result
Take Recorder 录制 → Sequencer 播放 → MRQ 渲染,特效(Niagara Sim Cache)状态正常,全程无闪退。
Observed Result
Take Recorder 录制(Char Niagara) → Sequencer 播放 → MRQ 渲染 → Unreal5.8 Crash
Affects Versions
5.8
Platform(s)
Windows

