수동 PSO 캐싱 은 게임 빌드를 플레이하여 파이프라인 스테이트 오브젝트(PSO) 정보를 수집해야 하지만, PSO 프리캐싱 은 컴포넌트의 MeshDrawCommand 렌더링 중에 사용할 수 있는 모든 PSO를 자동으로 수집하고 비동기 컴파일을 수행합니다.
PSO 프리캐싱은 언리얼 엔진 5.3부터 D3D12 Windows의 쿠킹된 빌드에서만 사용할 수 있습니다.
개요
프리미티브 컴포넌트(Primitive Components) (UPrimitiveComponent )는 로딩 직후(PostLoad 중) 렌더링에 필요한 모든 PSO를 미리 캐시합니다. 이 프리캐싱은 다음과 같이 컴파일에 필요한 모든 파이프라인 스테이트 정보를 수집합니다.
-
머티리얼
-
버텍스 팩토리
-
버텍스 엘리먼트 정보
-
특정 프리캐싱 파라미터
UE는 이 정보를 사용하여 컴포넌트가 렌더링될 수 있는 가능한 모든 메시 패스 프로세서를 반복작업합니다. 각 메시 패스 프로세서는 렌더링 때 필요하면 사용할 수 있는 PSO 이니셜라이저를 추가합니다. 백그라운드 작업은 공유된 PSO 캐시를 확인하여 필요한 캐시가 미리 캐시되고 있지 않은지 확인하고 이러한 요청을 비동기식으로 컴파일합니다.
UE가 프리미티브 컴포넌트(Primitive Component)의 프리미티브 프록시(Primitive Proxy) 를 생성했고 필요한 PSO를 아직 컴파일 중이라면, 다음과 같은 몇 가지 옵션이 있습니다.
-
PSO 컴파일이 완료될 때까지 프록시 생성을 지연시킵니다(기본값). 그러면 PSO가 준비될 때까지 드로를 효과적으로 건너뜁니다.
-
머티리얼을 엔진의 디폴트 머티리얼로 대체합니다.
-
계속 진행합니다. 히치가 발생할 수 있습니다. 드로는 PSO 컴파일 시 차단됩니다.
PSO 프리캐싱 환경설정
다음과 같은 CVar이 PSO 프리캐싱을 제어합니다.
| CVar | 설명 | 디폴트 스테이트 |
|---|---|---|
r.PSOPrecaching |
PSO 프리캐싱을 활성화하는 글로벌 CVar입니다. RHI 플래그 GRHISupportsPSOPrecaching 에 의존합니다. |
활성화됨(Enabled) |
r.PSOPrecache.Components |
컴포넌트가 사용하는 PSO를 미리 캐시합니다. | 활성화됨 |
r.PSOPrecache.Resources |
모든 리소스(UStaticMesh , USkinnedMesh 등)가 사용하는 PSO를 미리 캐시합니다. 이러한 PSO의 렌더 스테이트는 정확하지 않을 수 있는데, 어떤 스테이트는 컴포넌트에서만 파생될 수 있기 때문입니다. 하지만 PSO는 컴파일할 정확한 셰이더를 드라이버에 제공해야 합니다. |
비활성화됨(Disabled) |
r.PSOPrecache.ProxyCreationWhenPSOReady |
필요한 모든 PSO가 컴파일될 때까지 컴포넌트 프록시 생성을 대기합니다. 프록시 생성 시 PSO가 여전히 컴파일 중이라면, 해당 PSO는 높은 우선순위로 표시됩니다. | 활성화됨 |
r.PSOPrecache.ProxyCreationDelayStrategy |
컴포넌트의 프록시 생성 중에도 여전히 PSO가 컴파일 중인 경우, 머티리얼을 디폴트 머티리얼로 대체할 수 있는 옵션을 추가합니다. 이 CVar은 r.PSOPrecache.ProxyCreationWhenPSOReady 에 의존합니다. 자세한 내용은 아래의 프록시 생성 딜레이 전략(Proxy Creation Delay Strategy)을 참조하세요. |
0(아래 참조) |
r.PSOPrecaching.WaitForHighPriorityRequestsOnly |
로딩 때 높은 우선순위의 PSO만 기다립니다. 필수가 아닌 모든 PSO는 게임플레이 중에 계속 컴파일합니다. PSO는 프록시에서 필요로 하고 아직 컴파일이 끝나지 않은 경우 높은 우선순위로 표시됩니다. | 비활성화됨 |
프록시 생성 딜레이 전략
r.PSOPrecache.ProxyCreationDelayStrategy CVar은 r.PSOPrecache.ProxyCreationWhenPSOReady CVar에 의존합니다. ProxyCreationWhenPSOReady 가 1(활성화)로 설정된 경우, ProxyCreationDelayStrategy 는 그 값에 따라 다음 동작을 실행합니다.
| 값 | 동작 |
|---|---|
| 0 | PSO가 준비될 때까지 드로를 건너뜁니다. |
| 1 | PSO가 준비될 때까지 엔진의 디폴트 머티리얼로 예비 전환합니다. |
시스템 리소스 관리
PSO 프리캐싱은 백그라운드 스레드를 사용하는 비동기 컴파일에 의존하며 시스템 메모리에 영향을 줍니다. 이 섹션에서는 이러한 리소스를 프로젝트에 맞게 사용하도록 조정하고 최적화하는 데 사용할 수 있는 옵션을 설명합니다.
메모리
UE는 런타임 시스템 메모리를 절약하기 위해 컴파일이 끝난 뒤 프리캐싱용으로 컴파일했던 PSO를 삭제합니다. 애플리케이션이 미리 캐시한 PSO의 양이 매우 큰 경우 PSO를 정리하지 않는다면 해당 애플리케이션의 메모리 점유율이 굉장히 커질 수 있습니다.
PSO 프리캐싱은 기본 압축 드라이버 캐시의 존재 여부에 의존합니다. 런타임 시 PSO가 필요하면 그래픽 드라이버가 압축 드라이버 캐시에서 PSO를 로드합니다. 하지만 이런 방식도 리소스 집약적일 수 있으며, 처음에 이러한 캐시에서 가져올 때 몇 밀리초가 소요될 수 있습니다. D3D12.PSOPrecache.KeepLowLevel 을 사용하여 D3D12에서 미리 캐시된 PSO 삭제를 비활성화할 수 있습니다.
프로세싱
기본적으로 UE의 백그라운드 스레드는 PSO의 비동기 컴파일에 사용됩니다. 하지만 별도 PSO 프리캐싱 스레드 풀을 사용하면 포그라운드 스레드와의 경쟁을 줄일 수 있습니다. 다음 CVar 중 하나를 사용하여 이러한 풀을 설정할 수 있습니다.
| CVar | 설명 |
|---|---|
r.pso.PrecompileThreadPoolSize |
풀에서 사용할 정확한 스레드 양을 설정합니다. |
r.pso.PrecompileThreadPoolPercentOfHardwareThreads |
스레드 풀 크기를 사용할 수 있는 하드웨어 스레드의 백분율로 설정하고, 해당 크기의 스레드 풀을 생성합니다. |
명령줄 실행인자 -clearPSODriverCache 를 사용하여 드라이버 캐시를 강제로 정리할 수 있는데, 게임을 최초로 시작하는 환경에서 테스트 용도로 사용하는 것이 좋습니다.
또한, 코어가 많은 PC에서 테스트할 때는 명령줄 실행인자 -corelimit=n (여기서 n 은 코어 수)을 사용하여 코어 수를 8로 제한하거나 소비자 등급 PC의 일반적인 코어 수로 제한하는 것이 좋습니다. 이렇게 하면 최종 사용자 환경을 더 정확하게 모사할 수 있습니다.
게임의 스무드니스를 평가하는 모든 테스트 실행에 일관적으로 -clearPSODriverCache 스위치를 사용하세요. 이 스위치가 없으면 그래픽 드라이버가 빌드하고 이전 실행에서 남은 PSO 캐시가 히치를 가릴 수 있습니다.
유효성 검사 및 트래킹
PSO 프리캐싱 시스템의 퍼포먼스를 검증하고 추적하는 몇 가지 옵션이 있습니다.
r.PSOPrecache.Validation 을 다음 값으로 사용하여 유효성 검사를 활성화할 수 있습니다.
| 값 | 설명 |
|---|---|
| 0 | 비활성화됩니다. |
| 1 | 높은 수준의 숫자만 사용하는 가벼운 트래킹으로 퍼포먼스에 미치는 영향을 최소화합니다. |
| 2 | 상세 트래킹입니다. |
PSO 프리캐싱 유효성 검사가 활성화되면, stat PSOPrecache 콘솔 명령을 사용하여 수집된 통계를 검사할 수 있습니다.
PSO 프리캐싱 유효성 검사 시스템이 수집한 통계입니다. 콘솔 명령 stat PSOPrecache 를 사용하여 통계를 확인할 수 있습니다.
통계는 3개 그룹으로 나뉩니다.
| 그룹 | 설명 |
|---|---|
| 셰이더 전용 PSO(Shader-only PSOs) | 이 통계들은 사용된 RHI 셰이더만 추적하고 PSO의 다른 모든 스테이트 정보는 무시합니다. 적어도 모든 셰이더가 미리 캐시되었는지 혹은 다른 렌더 스테이트에 뭔가 누락되었거나 문제가 있는지 확인하는 데 유용합니다. r.PSOPrecache.Validation.TrackMinimalPSOs 가 필요합니다. |
| 최소 PSO(Minimal PSOs) | 렌더 타깃 정보를 제외한 모든 렌더 통계 및 버텍스 엘리먼트 정보와 셰이더를 포함합니다. 렌더 타깃 정보는 드로 시간 때 유효성 검사에만 사용할 수 있지만, 최소 PSO 통계는 MeshDrawCommand 빌드 중에도 업데이트하고 확인할 수 있습니다. r.PSOPrecache.Validation.TrackMinimalPSOs 가 필요합니다. |
| 전체 PSO(Full PSOs) | 그래픽 API가 사용하는 완전한 런타임 필수 PSO 스테이트입니다. 최소 PSO와 같지만, 더욱 많은 렌더 타깃 정보를 가지고 있습니다. |
각 그룹에서 다음 파라미터를 추적합니다.
| 파라미터 | 설명 |
|---|---|
| 누락됨(Missed) | 미리 캐시되지 않았지만 드로/방출 시간에 필요하기 때문에 미리 캐시했어야 하는 PSO의 수입니다. 가능한 이유로는 잘못된 셰이더, 렌더 타깃 스테이트, 버텍스 어트리뷰트, 렌더 타깃 정보 등이 있습니다. |
| 추적되지 않음(Untracked) | 프리캐싱이 활성화되지 않은 PSO의 수입니다. 가능한 이유로는 유효성 검사 비활성화, 글로벌 머티리얼, 지원되지 않는 버텍스 팩토리, 지원되지 않는 메시 패스 프로세서 타입 등이 있습니다. 특정 디버그 정보가 없는 출시 빌드에서는 추적되지 않은 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 프리캐싱 스테이트는 PSO 유효성 검사가 활성화되었을 때 언리얼 인사이트에서만 확인할 수 있습니다(위의 유효성 검사 및 트래킹 참조).
아래 스크린샷은 PSO 프리캐싱 누락으로 런타임 히치가 발생하는 상황을 보여줍니다.
클릭하면 이미지가 확대됩니다.
다음 스크린샷은 추적되지 않은 PSO에서 발생하는 히치를 대신 보여줍니다. 레벨 로딩 직후 처음 사용되는 글로벌 셰이더일 가능성이 큽니다.
클릭하면 이미지가 확대됩니다.
언리얼 인사이트는 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);
명시적인 버텍스 엘리먼트 세트가 제공되지 않으면 PSO 프리캐싱 중에 FVertexFactory::GetPSOPrecacheVertexFetchElements 가 사용됩니다.
고정 버텍스 엘리먼트 세트는 버텍스 팩토리에 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 와 같지만(이상적으로는 부분적으로 공유 가능), AddMeshBatch 는 MeshDrawCommand 빌드 시점에 호출되지만, PSO 프리캐싱 시스템은 훨씬 일찍 정보를 수집하려고 시도합니다(컴포넌트의 PostLoad).
간단한 예시는 FDistortionMeshProcessor::CollectPSOInitializers 를, 더 포괄적인 예시는 FBasePassMeshProcessor::CollectPSOInitializers 를 참조하세요.
PSO 프리캐싱 누락 디버그
최소 PSO 스테이트의 누락을 디버깅하는 것은 간단합니다. 드로 시간이 아닌 MeshDrawCommand 빌드 때 누락을 트리거할 수 있기 때문입니다. 전체 PSO 계산에 필요한 최종 렌더 타깃 정보는 드로잉 때만 이용할 수 있으므로 디버깅이 더 어렵습니다.
PSOCollectorStats::FPrecacheStatsCollector::UpdatePrecacheStats 함수는 추적된 내부 스테이트를 업데이트하며, 런타임 시 누락이 발생했을 때 디버거로 중단할 수 있는 편리한 위치에 있습니다. 호출 스택과 감시 창에서는 사용된 머티리얼과 렌더 패스, 버텍스 팩토리, FPrimitiveSceneProxy 에 대한 자세한 정보를 확인할 수 있습니다. 또한, ComponentForDebuggingOnly 멤버를 사용하여 UPrimitiveComponent 에 대한 정보를 가져올 수도 있습니다.
하지만 UpdatePrecacheStats 가 실행될 즈음에는 보통 PSO 프리캐싱이 해당 컴포넌트에서 이미 수행된 상태입니다. 해당 컴포넌트의 PSO 프리캐싱 때 잘못된 셰이더 또는 렌더 스테이트를 사용한 이유를 찾으려면, 주어진 패스에서 해당 컴포넌트 및/또는 머티리얼의 PSO 프리캐싱 때 중단점을 추가해야 합니다.
프리캐싱 때 중단점을 설정하는 가장 간단한 방법은 MeshDrawCommand 빌드 중에 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();
}
// … 함수의 나머지…
}