在引用了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是否确实存在线程安全的潜在问题?能否提供一些解决的思路?
Liu.Wei
(Liu.Wei)
2
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 })); 这一行,我后续会尝试从这一个方向去排查。
Liu.Wei
(Liu.Wei)
4