Niagara GPU Ribbon多线程数据竞争引发崩溃问题

在引用了Niagara GPU Ribbon发射器的Sequencer中,反复拖动播放条,或者随机选择到包含Ribbon粒子​的某一帧,会有较高概率引发崩溃。此崩溃出现于制作较为复杂的Niagara粒子中(包含多个CPU发射器和GPU发射器),简单粒子无法复现。

崩溃步骤可见附件中的视频(Crash1, Crash2)。​

经过排查,发现是在RenderRibbon的GetDynamicMeshElements过程中,​调用SourceParticleData->GetNumInstances()获取到的数值发生了变化(从>2的数值变为0)。

导致后续再调用​GenerateIndexBufferForView()时没有对RenderingViewResources的IndirectDrawBuffer和IndexBuffer进行有效的初始化,最终引发空指针访问的崩溃。

进一步调试发现,​GetDynamicMeshElements是位于异步线程(Background Workder Thread),在执行的过程中,渲染线程执行了FNiagaraGpuComputeDispatch::ExcuteTicks,在调用CurrentData->SwapGPU(FinalSimStageDataBuffer)时,会对Buffer中的InstanceNum做一个交换:Swap(NumInstances, BufferToSwap->NumInstances)。正是在这里,NumInstances被修改为0。导致了异步线程中读取的NumInstances错误,引发崩溃。

在GetDynamicMeshElements的起始位置,若NumInstances<2会直接Return,但是中途被修改为0则与这一逻辑产生了冲突。

我提供了一些调用堆栈和输出Log的截图(CrashLog1, CrashLog2),可以验证我上述的推测。​

由于我们项目对引擎做了改动(包括Sequencer),无法确定是否是某些改动导致,也无法导出资源到官方引擎验证。但是从代码逻辑层面,Niagara GPU Ribbon是否确实存在线程安全的潜在问题?能否提供一些解决的思路?​

Hi,

你好,我大致看了一下代码,我有个疑问是FNiagaraGpuComputeDispatch::ExcuteTicks虽然是发生在渲染线程,但是看起来应该并不会跟GetDynamicMeshElements的任务并行,因为FNiagaraGpuComputeDispatch::ExcuteTicks发起的地方应该是一个单独的RenderCommand,而GetDynamicMeshElements发起的地方也是另一个RenderCommand,GetDynamicMeshElements的task应该会在FinishGatherDynamicMeshElements的时候结束掉。

你能确认一下崩溃时FNiagaraGpuComputeDispatch::ExcuteTicks是从哪里触发的吗?[Image Removed]

我无法完全确定崩溃前的一次ExecuteTick的调用堆栈,不过大概率是从Renderer的BeginInitViews()->PostInitViews()->FNiagaraGpuComputeDispatch::ExecuteTicks,这次ExecuteTicks应该并非从FNiagaGpuGomputeDispatch::Tick中调用(游戏线程)。如图所示。[Image Removed],开启了GetDynamic的异步任务,然后同时FXSystem->PostInitViews引发了SwapGPUBuffer,这个逻辑似乎就说得通了。

[Image Removed]不过这个问题似乎是在我们从UE5.4升级到UE5.5之后出现的,NiagaraGpuComputeDispatch.cpp这个文件确实新增了一些改动,包括你截图中的UE::RenderCommandPipe::FSyncScope Scope(MakeArrayView({ &UE::RenderCommandPipe::SkeletalMesh })); 这一行,我后续会尝试从这一个方向去排查。

感谢提供信息,这个问题已经在5.6里修复了,可以看一下 https://github.com/EpicGames/UnrealEngine/commit/601176b18f390c54d53aed59b9d42385f099b6b2

好的 谢谢~!