UE5.5.3版本,在IOS平台上打开了r.PSOPrecache.Resources=1后进入关卡出现概率Crash

在DeviceProile里面如下设置:

+CVars=r.PSOPrecaching=1

+CVars=r.PSOPrecache.Components=1

+CVars=r.PSOPrecache.Resources=1

+CVars=r.PSOPrecache.Mode=0

+CVars=r.PSOPrecache.ProxyCreationWhenPSOReady=1

+CVars=r.PSOPrecache.ProxyCreationDelayStrategy=1

+CVars=r.PSOPrecache.DrawnComponentBoostStrategy=1

+CVars=r.PSOPrecache.GlobalShaders=0

然后控制台open level方式进入我的一个有一定复杂度的关卡,大概有30%几率左右会发生Crash。IOS平台,测试机是Iphone13pro,但多台Iphone包括iphone14,iphone15等都出现这个Crash。

后面确定关了r.PSOPrecache.Resources就没有出现了。

Steps to Reproduce
用XCode联调真机进行Crash的复现,每次都是在进入关卡之后的0-5秒内,现场如图

[Image Removed]​

​直接崩溃原因是在MetalPipeline.cpp的GetRenderPipeline方法中的这一段代码

`if (Desc == nullptr)
{
// By default there’ll be more threads trying to read this than to write it.
EventsMutex.ReadLock();

// Try to find a pipeline creation event for this key. If it’s found, we already have a thread creating this pipeline and we just have to wait.
TSharedPtr<FPThreadEvent, ESPMode::ThreadSafe> Event = PipelineEvents.FindRef(Key);

EventsMutex.ReadUnlock();

bool bCompile = false;
if (!Event.IsValid())
{
// Create an event other threads can use to wait if they request the same pipeline this thread is creating
EventsMutex.WriteLock();

Event = PipelineEvents.FindRef(Key);
if (!Event.IsValid())
{
Event = PipelineEvents.Add(Key, MakeShareable(new FPThreadEvent()));
Event->Create(true);
bCompile = true;
}
check(Event.IsValid());

EventsMutex.WriteUnlock();
}

if (bCompile)
{
const double CompilationStartTime = FPlatformTime::Seconds();
Desc = CreateMTLRenderPipeline(Device, bSync, Key, Init, State);
const float CompilationDuration = static_cast(FPlatformTime::Seconds() - CompilationStartTime);

AccumulatePSOMetrics(CompilationDuration);

if (Desc != nullptr)
{
PipelineMutex.WriteLock();

Pipelines.Add(Key, Desc);
ReverseLookup.Add(Desc, Key);

PipelineMutex.WriteUnlock();
}

EventsMutex.WriteLock();

Event->Trigger();
PipelineEvents.Remove(Key);

EventsMutex.WriteUnlock();
}
else
{
check(Event.IsValid());
Event->Wait();

PipelineMutex.ReadLock();
Desc = Pipelines.FindRef(Key);
PipelineMutex.ReadUnlock();
check(Desc);
}
}`​其中的TSharedPtr<FPThreadEvent, ESPMode::ThreadSafe> Event析构时发生Crash

[Image Removed]

看起来像是某种情况这个智能指针的引用计数出现问题了?

还有个信息是:最开始出现Crash的时候有怀疑过是否某个Shader编译导致,后面在​GetRenderPipeline方法的开头补了个Log输出编译的ShaderHash,再对应去找到哪个材质。但发现几次Crash的材质都不相同,看起来不像是某个材质出问题导致的。

看起来更像是​r.PSOPrecache.Resources这个功能在Metal平台上有问题

Hi,

你好,我搜了一下,内部没有发现类似的情况,可能是因为我们内部的项目没有开启过r.PSOPrecache.Resources,所以没有遇到过。不确定能否用一个简单的工程复现这个问题?

另外我看了一下代码,对几个问题有点好奇:

  1. 从崩溃的位置上看,我不太确定为什么出现崩溃(是TSharedPtr.SharedReferenceCount无效了吗?),因为从代码上看Event是一个TSharedPtr类型,这个类型没有析构函数,所以应该就是默认的析构函数,所以应该会走到SharedReferenceCount的FSharedReferencer的析构函数,因为TSharedPtr实际是一个类的实例,每个线程创建的Event应该是在不同的地址,FSharedReferencer也是一样,所以应该不存在同一块地址被析构两次的问题,所以我没看出来为什么会有问题。
  2. 根据上面的观察,没有看出来跟r.PSOPrecache.Resources功能有什么关系,理论上即便上面的崩溃有问题,应该也属于底层问题,即便不用r.PSOPrecache.Resources应该也有问题才对。另外想再确认一下,这个问题是否只在IOS上遇到?是否在Android等其他平台见到过?

好的,我暂时也没有特别好的想法和建议。

从代码上看,Event不是一个指针,所以理论上不应该出现多次free的问题(如果我有看错麻烦指出),PipelineEvents这个Map的Value(TSharedPtr类型)也有ThreadSafe的标记,所以理论上应该也没问题。不知道debug版能否运行和复现问题,如果可以,下次崩溃了,可以再看一下现场是否有更多的线索。

这里的Event指针不是从PipelineEvents这个Map里取的么,这个Map看起来是跨线程的。不过确实很神奇,这里各种读写锁的操作看起来没问题,并且这一段代码很底层了如果有问题那早出大问题了。但就是这个智能指针在析构时crash了。

补充一些已知信息是:

1.这个问题应该很难简单工程复现,进测试场景没出现过,只有进目前最复杂的大地图关卡概率变高(20%左右偶现)

2.基本确定是PSOPrecache整套功能开启后出现的,并且经过了1-2周验证只要r.PSOPrecache.Resources=1就会有问题,把这个从Resource开始编译PSO关了就没出现Crash了。

3.仅在IOS上出现,Android没出现过。