Crash on LOD change in FSkeletalMeshObjectGPUSkin::GetCachedGeometry -> FGPUSkinCache::GetPositionBuffer due to Niagara's Skeletal Mesh Location Module

We are experiencing a crash in FGPUSkinCache::GetPositionBuffer which is called by FSkeletalMeshObjectGPUSkin::GetCachedGeometry. This is due to Niagara’s “Skeletal Mesh Location” Module on a character. The crash happens when the LOD level of the character changes (e.g. when you are moving back and forth). It does have about a 25% repro rate.

I think this is due to a race condition.

This is the crash in the debugger:

[Image Removed]

The Entry parameter seems to be stale which you can easily tell by the Mode value.

[Image Removed]

It is interesting, that the skin cache entry is perfectly fine if you go one line down in the callstack. You can see that the pointer is not the same. Compare the pointer inside the GetPositionBuffer function …

[Image Removed]

… and the pointer in the calling GetGeometryCache:

[Image Removed]

Note that the pointer in GetGeometryCache is a class field, whereas the pointer in the GetPositionBuffer function is a copy of it. I think, the field gets changed by another thread while AddAsyncComputeWait is waiting for it to finish.

A data breakpoint shows that the entry gets deleted and reassigned on a worker thread due to a LOD change:

[Image Removed]

however, GetGeometryCache is called on the render thread which you can easily see from the crashing callstack! I think this is because skinning runs on UE::RenderCommandPipe::SkeletalMesh.

My understanding is that AddAsyncComputeWait is waiting until the parallel render threads are finished. So you can probably fix the problem by moving the wait up into the GetGeometryCache function before it accesses the entry. The problem is just, AddAsyncComputeWait is a FGPUSkinCache function and the skin cache object is only available through the FGPUSkinCacheEntry pointer. So you’d somehow have to make the FGPUSkinCache object accessible without going through the FGPUSkinCacheEntry pointer. Even something like

if (Entry)

{

Entry->SkinCache->AddAsyncComputeWait(GraphBuilder);

}

is not safe because Entry is not atomic and the thread could be interrupted between the null check and the access.

I hope this helps to provide a fix soon as we currently only have a workaround which is not 100% reliable. Thank you!

PS: The crashing callstack is:

Unhandled Exception: EXCEPTION_ACCESS_VIOLATION reading address 0xffffffffffffffff

(0x1896123) UnrealEditor-Engine.dll!FGPUSkinCache::GetPositionBuffer() [D:\TRS_Perforce\Game\Engine\Source\Runtime\Engine\Private\GPUSkinCache.cpp:2567]

(0x2c0aa9d) UnrealEditor-Engine.dll!FSkeletalMeshObjectGPUSkin::GetCachedGeometry() [D:\TRS_Perforce\Game\Engine\Source\Runtime\Engine\Private\SkeletalRenderGPUSkin.cpp:2468]

(0xa6607d) UnrealEditor-Niagara.dll!UNiagaraDataInterfaceSkeletalMesh::SetShaderParameters() [D:\TRS_Perforce\Game\Engine\Plugins\FX\Niagara\Source\Niagara\Private\NiagaraDataInterfaceSkeletalMesh.cpp:3384]

(0xbc17b4) UnrealEditor-Niagara.dll!FNiagaraGpuComputeDispatch::SetDataInterfaceParameters() [D:\TRS_Perforce\Game\Engine\Plugins\FX\Niagara\Source\Niagara\Private\NiagaraGpuComputeDispatch.cpp:2460]

(0xb76d6c) UnrealEditor-Niagara.dll!FNiagaraGpuComputeDispatch::DispatchStage() [D:\TRS_Perforce\Game\Engine\Plugins\FX\Niagara\Source\Niagara\Private\NiagaraGpuComputeDispatch.cpp:1701]

(0xb89b3d) UnrealEditor-Niagara.dll!FNiagaraGpuComputeDispatch::ExecuteTicks() [D:\TRS_Perforce\Game\Engine\Plugins\FX\Niagara\Source\Niagara\Private\NiagaraGpuComputeDispatch.cpp:1288]

(0xbb00f7) UnrealEditor-Niagara.dll!FNiagaraGpuComputeDispatch::PreInitViews() [D:\TRS_Perforce\Game\Engine\Plugins\FX\Niagara\Source\Niagara\Private\NiagaraGpuComputeDispatch.cpp:2008]

(0x2191425) UnrealEditor-Engine.dll!FFXSystemSet::PreInitViews() [D:\TRS_Perforce\Game\Engine\Source\Runtime\Engine\Private\Particles\FXSystemSet.cpp:131]

(0x15c72bb) UnrealEditor-Renderer.dll!FSceneRenderer::PreVisibilityFrameSetup() [D:\TRS_Perforce\Game\Engine\Source\Runtime\Renderer\Private\SceneVisibility.cpp:4863]

(0x159a837) UnrealEditor-Renderer.dll!FDeferredShadingSceneRenderer::BeginInitViews() [D:\TRS_Perforce\Game\Engine\Source\Runtime\Renderer\Private\SceneVisibility.cpp:5750]

(0x31cb93) UnrealEditor-Renderer.dll!FDeferredShadingSceneRenderer::Render() [D:\TRS_Perforce\Game\Engine\Source\Runtime\Renderer\Private\DeferredShadingRenderer.cpp:1737]

(0x155728f) UnrealEditor-Renderer.dll!RenderViewFamily_RenderThread() [D:\TRS_Perforce\Game\Engine\Source\Runtime\Renderer\Private\SceneRendering.cpp:4873]

(0x14dd030) UnrealEditor-Renderer.dll!`FRendererModule::BeginRenderingViewFamilies’::`116’::<lambda_4>::<lambda_invoker_cdecl>() [D:\TRS_Perforce\Game\Engine\Source\Runtime\Renderer\Private\SceneRendering.cpp:5163]

(0x150c8eb) UnrealEditor-Renderer.dll!`FSceneRenderProcessor::Execute’::`30’::<lambda_4>::operator()() [D:\TRS_Perforce\Game\Engine\Source\Runtime\Renderer\Private\SceneRenderBuilder.cpp:916]

(0x1bd71f) UnrealEditor-RenderCore.dll!`FRenderThreadCommandPipe::EnqueueAndLaunch’::`5’::<lambda_1>::operator()() [D:\TRS_Perforce\Game\Engine\Source\Runtime\RenderCore\Private\RenderingThread.cpp:1554]

(0x1e4698) UnrealEditor-RenderCore.dll!TGraphTask<TFunctionGraphTaskImpl<void __cdecl(void),1> >::ExecuteTask() [D:\TRS_Perforce\Game\Engine\Source\Runtime\Core\Public\Async\TaskGraphInterfaces.h:708]

(0x129fb2) UnrealEditor-Core.dll!UE::Tasks::Private::FTaskBase::TryExecuteTask() [D:\TRS_Perforce\Game\Engine\Source\Runtime\Core\Public\Tasks\TaskPrivate.h:527]

(0x1109bf) UnrealEditor-Core.dll!FNamedTaskThread::ProcessTasksNamedThread() [D:\TRS_Perforce\Game\Engine\Source\Runtime\Core\Private\Async\TaskGraph.cpp:784]

(0x1110be) UnrealEditor-Core.dll!FNamedTaskThread::ProcessTasksUntilQuit() [D:\TRS_Perforce\Game\Engine\Source\Runtime\Core\Private\Async\TaskGraph.cpp:672]

(0x2092f9) UnrealEditor-RenderCore.dll!RenderingThreadMain() [D:\TRS_Perforce\Game\Engine\Source\Runtime\RenderCore\Private\RenderingThread.cpp:321]

(0x20a3a4) UnrealEditor-RenderCore.dll!FRenderingThread::Run() [D:\TRS_Perforce\Game\Engine\Source\Runtime\RenderCore\Private\RenderingThread.cpp:454]

(0x849504) UnrealEditor-Core.dll!FRunnableThreadWin::Run() [D:\TRS_Perforce\Game\Engine\Source\Runtime\Core\Private\Windows\WindowsRunnableThread.cpp:165]

(0x841e1f) UnrealEditor-Core.dll!FRunnableThreadWin::GuardedRun() [D:\TRS_Perforce\Game\Engine\Source\Runtime\Core\Private\Windows\WindowsRunnableThread.cpp:83]

(0x2e8d7) KERNEL32.DLL!UnknownFunction

Steps to Reproduce
This is a race condition, so it’s difficult to reproduce. I’ve attached a project where I can reproduce it somewhat reliably on my machine, but there is no guarantee that it also reproduces for you, unfortunately:

  • Enter PIE
  • Enter scalability 2 into the console (not sure why but seems to help)
  • F8
  • Circle around and through the character with the particles on it

I’ve attached a video of the repro steps as well.

Thanks for pointing this out.

The correct fix is to pull the task wait inside of AddAsyncComputeWait into a separate function, like a static WaitForTasks(FRDGBuilder& GraphBuilder), and then it can access the task through the graph builder blackboard like it does now without needing the skin cache pointer.

Yes, I will do that. I’ll try to get it into 5.7.

Oh, I see. Actually, you can simply make AddAsyncComputeWait static.

You do have to call it before accessing the entry in GetCachedGeometry, though. So you need to pull the wait up into the calling function.

Are you going to create a ticket to have it fixed on your end as well?

Excellent. Thank you!