手动 PSO缓存 需要运行游戏的构建版本以收集PSO(管线状态对象)信息,而 PSO预缓存 会对可能在组件的MeshDrawCommand渲染期间使用的所有PSO执行自动PSO收集和异步编译。
仅自虚幻引擎5.3起的D3D12 Windows烘焙版本提供PSO预缓存。
概述
图元组件 ( UPrimitiveComponent )会在加载(PostLoad期间)后立即预缓存渲染所需的所有PSO。预缓存会收集所需的所有管线状态信息来编译PSO,包括:
-
材质。
-
顶点工厂。
-
顶点元素信息。
-
特定预缓存参数。
UE利用此信息遍历可以渲染组件的所有潜在网格体通道处理器。每个网格体通道处理器都会添加渲染期间可能需要的潜在PSO初始化程序。后台任务会检查共享PSO缓存,以确保所需数据尚未被预缓存并异步编译这些请求。
当UE为图元组件创建 图元代理 且其所需PSO仍在编译时,有多个选项可用:
-
延迟代理创建,直至PSO编译完成(默认)。这样可以有效跳过绘制,直至PSO准备就绪。
-
将材质替换成引擎中的默认材质。
-
继续,并且可能出现卡顿。绘制将阻止PSO编译。
配置PSO预缓存
以下控制台变量可控制PSO预缓存:
| 控制台变量 | 说明 | 默认状态 |
|---|---|---|
r.PSOPrecaching |
可启用PSO预缓存的全局控制台变量。依赖RHI标记 GRHISupportsPSOPrecaching 。 |
启用 |
r.PSOPrecache.Components |
预缓存组件使用的PSO。 | 启用 |
r.PSOPrecache.Resources |
预缓存所有资源( UStaticMesh 、 USkinnedMesh 等)使用的PSO。这些PSO的渲染状态可能不正确,因为某些状态仅可从组件派生。然而,PSO应为驱动程序提供正确的着色器进行编译。 |
禁用 |
r.PSOPrecache.ProxyCreationWhenPSOReady |
等待组件代理创建,直至所有必需的PSO完成编译。如果在创建代理时,PSO仍在编译,这些PSO将标记为高优先级。 | 启用 |
r.PSOPrecache.ProxyCreationDelayStrategy |
当PSO在组件的代理创建期间仍在编译时,此变量会增加将材质替换成默认材质的选项。这依赖于 r.PSOPrecache.ProxyCreationWhenPSOReady 。有关更多信息,请参阅下文的"代理创建延迟策略"。 |
0(见下文) |
r.PSOPrecaching.WaitForHighPriorityRequestsOnly |
加载期间仅等待高优先级PSO。所有非必要PSO仍将在Gameplay期间编译。当代理需要某个PSO并且尚未完成编译时,该PSO会被标记为高优先级。 | 禁用 |
代理创建延迟策略
r.PSOPrecache.ProxyCreationDelayStrategy 控制台变量依赖 r.PSOPrecache.ProxyCreationWhenPSOReady 控制台变量。如果 ProxyCreationWhenPSOReady 设置为1(启用), ProxyCreationDelayStrategy 将根据其值运行以下行为:
| 值 | 行为 |
|---|---|
| 0 | 跳过绘制,直至PSO准备就绪。 |
| 1 | 退却至引擎的默认材质,直至PSO准备就绪。 |
管理系统资源
PSO预缓存依赖使用后台线程进行异步编译,并对系统内存有影响。本小节旨在介绍用于根据你的项目调整和优化资源使用的可用选项。
内存
为节省运行时系统内存,UE会在编译后删除为预缓存编译的PSO。如果你的应用程序预缓存的PSO量非常大,若不清理,可能会占用大量的应用程序内存空间(数百MB)。
PSO预缓存依赖存在的底层压缩驱动程序缓存。如果运行时需要PSO,显卡驱动程序将从其压缩驱动程序缓存加载PSO。然而,这也可能造成资源耗费,并且从这些缓存进行第一次检索可能需要几毫秒。你可以使用 D3D12.PSOPrecache.KeepLowLevel 在D3D12中禁用预缓存PSO的删除操作。
处理
默认情况下,UE的后台线程用于异步编译PSO。然而,你可以使用单独的PSO预缓存线程池,以减少与前台线程的争用。你可以使用以下任一控制台变量进行相关设置:
| 控制台变量 | 说明 |
|---|---|
r.pso.PrecompileThreadPoolSize |
设置要在池中使用的具体线程数量。 |
r.pso.PrecompileThreadPoolPercentOfHardwareThreads |
将线程池大小设置为可用硬件线程的百分比,并创建该大小的线程池。 |
你可以使用命令行参数 -clearPSODriverCache 强制清除驱动程序缓存,我们建议在测试游戏首次启动体验时进行。
在具有大量核心的PC上进行测试时,我们还建议使用命令行参数 -corelimit=n(其中 n 为核心数),将核心数量限制为8,或适用于消费级PC的其他常规核心数。这样可以确保你能更准确地复制最终用户的体验。
对于评估游戏流畅度的所有测试运行,始终使用 -clearPSODriverCache switch函数。如果没有它,故障可能会被显卡驱动程序所构建和先前运行遗留的PSO缓存掩盖。
验证和跟踪
有多个选项可供验证和跟踪PSO预缓存系统的性能。
你可以通过 r.PSOPrecache.Validation 使用以下值启用验证:
| 值 | 说明 |
|---|---|
| 0 | 禁用。 |
| 1 | 仅采用大概数据的轻量级跟踪(对性能影响最小)。 |
| 2 | 详细跟踪。 |
当PSO预缓存验证激活时,你可以检查使用 stat PSOPrecache 控制台命令收集的统计数据。
统计数据通过PSO预缓存验证系统收集。使用 stat PSOPrecache 控制台命令查看统计数据。
统计数据分为3组:
| 组 | 说明 |
|---|---|
| 仅限着色器PSO(Shader-only PSO) | 这些统计数据仅跟踪所使用的RHI着色器,并忽略PSO中的所有其他状态信息。对于查看是否至少所有着色器都已预缓存,以及其他渲染状态是否缺失/错误,此组非常有用。需要 r.PSOPrecache.Validation.TrackMinimalPSOs 。 |
| 最少PSO(Minimal PSO) | 包含着色器和所有渲染统计数据与顶点元素信息(渲染目标信息除外)。渲染目标信息仅可在绘制时用于验证,但最少PSO统计数据可在MeshDrawCommand构建期间更新和检查。需要 r.PSOPrecache.Validation.TrackMinimalPSOs 。 |
| 完整PSO(Full PSO) | 图形API使用的完整运行时所需PSO状态。这和最少PSO相同,但提供额外的渲染目标信息。 |
对于每个组,跟踪以下参数:
| 参数 | 说明 |
|---|---|
| 未命中(Missed) | 未预缓存但本应预缓存(因为绘制和调度时需要PSO)的PSO数量。可能的原因:着色器、渲染目标状态、顶点属性、渲染目标信息错误。 |
| 未跟踪(Untracked) | 未启用预缓存的PSO的数量。可能的原因:验证被禁用、全局材质、顶点工厂不受支持、网格体通道处理器类型不受支持。在某些调试信息不可用的发布版本中,未跟踪PSO会显示为未命中。 |
| 命中(Hit) | 运行时使用的已成功预缓存的PSO的数量。 |
| 太迟(Too late) | 排队等待预缓存但未在需要时及时编译的PSO的数量。 |
| 已使用(Used) | 在运行时已使用的PSO的数量(以上所有PSO的总和)。 |
| 已预缓存(Precached) | 已预缓存(但不一定已使用)的PSO的数量。 |
着色器管线缓存(Shader Pipeline Cache) 也会提供相关信息来表明检测到多少实际运行时故障是因为PSO编译本身导致。如果要编译运行时PSO的编译时间超过一定毫秒数,则PSO编译会被标记为故障。默认阈值为20毫秒。你可以通过 r.PSO.RuntimeCreationHitchThreshold 修改此值,但该数值应尽可能小。
默认值20毫秒为较大值,因为驱动程序缓存的首次命中可能耗费较长时间。
收集关于PSO预缓存的信息
你可以使用 Visual Studio 调试器和 Unreal Insights 获取关于PSO预缓存的更多信息,并查看为何某些PSO仍在运行时引起故障。仅在启用PSO验证(参见上文"验证和跟踪")时,Insights中才会显示正确的PSO预缓存统计数据。
下方截图显示的是导致运行时故障的PSO预缓存未命中:
点击查看大图。
下方截图显示的是未跟踪PSO引发的故障。这些可能是关卡加载后首次使用的全局着色器:
点击查看大图。
Unreal Insights为你提供了关于PSO编译所引发故障的大致信息。要就上述PSO预缓存未命中源头进行调试,你需要在Visual Studio中使用手动调试。
你可以在全局PSO验证辅助对象中找到更多详细信息。当验证设置为全面跟踪( r.PSOPrecache.Validation=2 )时,会根据网格体通道处理器和顶点工厂类型分组数据,这有助于跟踪某些未命中的源头。这还有助于你更清晰地了解所有预缓存PSO的源头,并找到不应缓存那么多着色器的异常值。
虽然这些每通道和每顶点工厂统计数据未直接公开,但可以在调试期间通过找到收集数据的数据结构检查这些统计数据。统计数据位于 PSOPrecache.cpp :
-
FullPSOPrecacheStatsCollector -
ShadersOnlyPSOPrecacheStatsCollector -
MinimalPSOPrecacheStatsCollector。
下方截图即为示例。
点击查看大图。
通过新引擎功能扩展PSO预缓存
本小节介绍了关于如何为PSO预缓存扩展支持对象的信息。
新的UPrimitiveComponent
UPrimitiveComponent 会收集用来设置PSO初始化程序所需的所有信息。需要材质实例、顶点工厂(带可能的顶点元素集)和可能影响 FMeshPassProcessor 中使用的最终着色器和渲染状态的参数集。
参数存储在 FPSOPrecacheParams 中,正确的默认值在 UPrimitiveComponent::SetupPrecachePSOParams 中设置。
PSO预缓存的基础入口函数为:
/**预缓存图元组件可能使用的所有PSO*/
ENGINE_API virtual void PrecachePSOs();
在大多数情况下,派生组件不需要实现此函数,只需覆盖预缓存参数收集函数:
/**
* 收集PSO预缓存所需的全部数据
*/
struct FComponentPSOPrecacheParams
{
EPSOPrecachePriority Priority = EPSOPrecachePriority::Medium;
UMaterialInterface* MaterialInterface = nullptr;
FPSOPrecacheVertexFactoryDataList VertexFactoryDataList;
FPSOPrecacheParams PSOPrecacheParams;
};
typedef TArray<FComponentPSOPrecacheParams, TInlineAllocator<2> > FComponentPSOPrecacheParamsList;
virtual void CollectPSOPrecacheData(const FPSOPrecacheParams& BasePrecachePSOParams, FComponentPSOPrecacheParamsList& OutParams) {}
功能全面的示例可在 UStaticMeshComponent::CollectPSOPrecacheData 中找到,较简单的用例可在 WaterMeshComponent::CollectPSOPrecacheData 中找到。
新的FVertexFactory
新顶点工厂需要使用标记 EVertexFactoryFlags::SupportsPSOPrecaching 标记其支持PSO预缓存,该标记通过顶点工厂声明宏 IMPLEMENT_VERTEX_FACTORY_TYPE 提供。
然后,顶点工厂必须实现以下函数:
static void GetPSOPrecacheVertexFetchElements(EVertexInputStreamType VertexInputStreamType, FVertexDeclarationElementList& Elements);
FVertexFactory::GetPSOPrecacheVertexFetchElements is used during PSO precaching if no explicit vertex element set is provided.
如果在顶点工厂上设置 EVertexFactoryFlags::SupportsManualVertexFetch 标记,或者如果着色器中使用固定的顶点元素集,则固定顶点元素集将有效。
如果顶点元素列表依赖网格体的顶点缓冲区数据,则需要在 FPSOPrecacheVertexFactoryData 中提供正确的集。这应在 UPrimitiveComponent::CollectPSOPrecacheData 期间进行。有关示例,请参阅 UStaticMeshComponent::CollectPSOPrecacheData 和 FLocalVertexFactory::GetVertexElements 。
新的FMeshPassProcessor
网格体通道处理器必须实现以下函数,以收集在使用给定 FPSOPrecacheParams 绘制某些材质时可能使用的所有PSO:
virtual void CollectPSOInitializers(const FSceneTexturesConfig& SceneTexturesConfig, const FMaterial& Material, const FPSOPrecacheVertexFactoryData& VertexFactoryData, const FPSOPrecacheParams& PreCacheParams, TArray<FPSOPrecacheData>& PSOInitializers) override {}
逻辑和 AddMeshBatch 大体相同(理想情况下可以部分共享),但当在MeshDrawCommand构建时调用 AddMeshBatch 时,PSO预缓存系统会尝试更快地收集信息(组件的PostLoad)。
对于简单示例,请参阅 FDistortionMeshProcessor::CollectPSOInitializers 。对于更复杂的示例,请参阅 FBasePassMeshProcessor::CollectPSOInitializers 。
调试PSO预缓存未命中
调试最少PSO状态时的未命中非常简单,因为这些未命中可以在MeshDrawCommand构建期间触发,而非在绘制时触发。最终渲染目标信息(计算完整PSO时需要)仅在绘制期间提供,这使得调试难度更大。
函数 PSOCollectorStats::FPrecacheStatsCollector::UpdatePrecacheStats 将更新跟踪的内部状态,并且当在运行时发生未命中时很适合脱离调试器。调用堆栈和监视窗口可以提供有关所使用材质、渲染通道、顶点工厂和 FPrimitiveSceneProxy 的更多信息。你也可以使用 ComponentForDebuggingOnly 成员获取有关 UPrimitiveComponent 的信息。
然而,当 UpdatePrecacheStats 运行时,PSO预缓存通常已在该组件上完成。如果你尝试找到为何在该组件PSO预缓存期间使用的着色器或渲染状态不正确,你需要在PSO预缓存期间为该组件和/或给定通道的材质添加一个断点。
在预期缓存期间设置断点的最简单方式是,在MeshDrawCommand building期间找到 Fmaterial 资产名称,并使用该名称在相同MeshPassProcessor的PSO收集期间添加一个断点。然后,将在MeshDrawCommand构建期间使用的PSO状态和在PSO预缓存期间设置的状态进行比较。
你可能还需要检查 FPSOPrecacheParams 的值,因为这些值也可能影响PSO中使用的着色器和渲染状态。
UE_DISABLE_OPTIMIZATION
void FBasePassMeshProcessor::CollectPSOInitializers(const FSceneTexturesConfig& SceneTexturesConfig, const FMaterial& Material, const FPSOPrecacheVertexFactoryData& VertexFactoryData, const FPSOPrecacheParams& PreCacheParams, TArray<FPSOPrecacheData>& PSOInitializers)
{
FString MaterialName = Material.GetAssetName();
if (MaterialName == TEXT("TEST_MATERIAL_NAME"))
{
UE_DEBUG_BREAK();
}
// … 函数的其余部分…
}