Skip to content

언리얼 렌더링 시스템 분석(12) - 모바일 단말기 특별 주제 3부(렌더링 최적화)

🌐 원문 링크: 剖析虚幻渲染体系(12)- 移动端专题Part 3(渲染优化) (cnblogs.com/timlly)
📅 원문 발행일: 2021-11-18 | ✍️ 저자: Timlly (00 / )

💡 시리즈 분류: Mobile Optimization국내 업계 표준 용어 감수 및 수식/도해 복원 적용 완료


이전 장에서는 모바일 GPU 아키텍처의 특성과 메커니즘을 자세히 분석했으며, 이를 통해 고성능 렌더링 코드와 애플리케이션을 얻기 위한 몇 가지 원칙을 추상화할 수 있습니다.

원활하고 효율적이며 좋은 경험을 얻으려면 모든 애플리케이션이 성능 최적화에 주의를 기울이고 이를 전체적으로 구현해야 합니다. 애플리케이션의 성능 최적화는 다음과 같은 삼각형 루프로 나뉩니다.

첫 번째 단계는 애플리케이션의 전반적인 성능을 분석하는 것입니다.

두 번째 단계는 도구를 사용하여 성능 병목 현상을 찾는 것입니다.

세 번째 단계는 애플리케이션을 수정하는 것입니다. 재귀분석의 첫 번째 단계로 돌아갑니다.

이 삼각형 사이클은 언제 멈추나요? 즉, 애플리케이션의 성능은 프로젝트 초기에 명시된 기준(고, 중, 저화질에 대한 프레임 수, DC 및 트라이앵글 수 등)에 도달했으며, 애플리케이션이 효율성 한계에 도달한 후 입출력 비율이 매우 작은 길의 끝에 도달한 것으로 알려져 있습니다.

이 문서에서는 다음 개념을 다룹니다.

이름 별칭 설명

USC(통합 셰이딩 클러스터) 셰이딩 클러스터, 셰이딩 유닛, 실행 유닛 일반적으로 전체 작업 그룹을 실행할 수 있는 그래픽 코어의 반자동 부분입니다. 텍스처 유닛과 같은 다른 대형 구성요소는 USC 간에 공유될 수 있습니다.

핵심 프로세서, 그래픽 코어 그래픽 코어의 거의 완전히 자율적인 부분입니다. 일반적으로 이는 USC 모음이며 텍스처 단위와 같이 지원되는 하드웨어도 포함됩니다.

작업 스레드 그룹, 워프, 웨이브프론트 USC에서 실행되는 기본 스레드 그룹인 PowerVR Rogue 코어는 32개의 스레드로 구성됩니다.

공유 공유 변수 공유 메모리에 저장된 변수입니다.

불변/균일 const/균일 변수, 균일 블록, 균일 버퍼 상수 메모리에 저장된 변수, 블록, 버퍼.

아래와 같이 PowerVR Rogue 하드웨어 아키텍처 및 데이터 흐름 상호 작용 다이어그램을 보완합니다.

PowerVR Rogue의 USC(Unified Shading Cluster)는 다음과 같습니다.

또한, 이 장에서 광범위하게 다루는 프래그먼트의 개념을 추가하겠습니다.

조각은 GPU 내부의 기하학적 구조를 래스터라이제이션한 후 형성된 가장 작은 표현 단위입니다. 일련의 조각 작업(알파 테스트, 깊이 테스트, 템플릿 테스트 등)을 거친 후 최종적으로 렌더링 텍스처에 기록되어 픽셀이 될 수 있습니다. 따라서 조각은 픽셀이 아니지만 픽셀이 될 확률을 갖는다.

그러나 D3D 또는 UE 내에는 조각이라는 개념이 없으며 픽셀에는 조각이 포함되어 있습니다.

12.6.1 렌더링 파이프라인 최적화

12.6.1.1 새로운 기능 사용

  • 가변 속도 셰이딩

**가변 속도 셰이딩(VRS, 가변 속도 셰이딩)**을 사용하면 픽셀 셰이더가 한 번에 하나 이상의 픽셀을 셰이딩할 수 있으므로 셰이딩 계산이 픽셀 또는 픽셀 그룹을 나타낼 수 있습니다. VRS는 앤티앨리어싱 기술의 역 솔루션입니다. 앤티앨리어싱 기술은 각 픽셀을 더 자주 샘플링하여 매우 다양한 콘텐츠를 부드럽게 만들어 앨리어싱과 들쭉날쭉한 가장자리를 방지합니다. 그러나 렌더링할 표면의 색상 변화가 크지 않거나 후속 패스(예: 모션 블러)에서 흐려지는 경우 각 픽셀에서 음영 계산을 수행하는 것이 비효율적인 경우가 많습니다.

VRS를 사용하면 개발자는 셰이딩 속도를 지정할 수 있습니다. 여기서는 한 픽셀에 대해 하나의 셰이더 계산만 수행되고 결과 작업은 지정된 픽셀 그룹 구성에 적용됩니다. 올바르게 사용하면 GPU 렌더링의 부하를 크게 줄여 전력을 절약하고 성능을 향상시키면서 시각적 품질 손실이 없어야 합니다.

VRS 다이어그램. 그림은 색상 변경 빈도에 따라 다른 색상 비율을 사용합니다. 변화율이 높은 부분(자동차 등)에는 높은 색상율을 사용하고, 반대 방향(왼쪽 아래, 오른쪽 아래 도로 등)에는 낮은 색상율을 사용합니다.

VRS가 지원하는 공통 셰이딩 속도 및 런타임 메커니즘. 노란색 점은 채색 좌표이고, 녹색 점은 노란색 점을 직접 재사용한 채색 결과입니다.

렌더링 파이프라인에서 VRS의 작동 메커니즘. VRS는 지정된 셰이딩 속도를 사용하여 래스터라이제이션 단계에서 래스터라이제이션를 수행한 다음 PS에 들어간 후 증폭합니다.

UE는 재료 속성 템플릿에서 각 재료에 대한 음영 비율을 설정할 수 있습니다.

VRS 최적화의 핵심 아이디어는 계산 횟수를 줄이고 주변 계산 지점의 결과를 재사용하여 렌더링 효율성을 높이는 것입니다. VRS 사용에 적합한 상황:

  • 색상변화율이 낮은 물체.

  • 모션 블러 영역의 개체.

  • 뎁스 오브 필드(DoF) 밖의 물체.

모바일에서 VRS를 사용하려면 다양한 그래픽 API 확장에 의존해야 합니다.

cpp
// ------ OpenGLES ------
// Qualcomm 
QCOM_shading_rate
GL_SHADING_RATE_1X1_PIXELS_QCOM
GL_SHADING_RATE_1X2_PIXELS_QCOM
......

// Arm / Imagination Tech
(不支持)

// ------ Vulkan ------
VK_KHR_fragment_shading_rate
  • OpenGL 대신 Vulkan을 사용하세요.

OpenGL과 같은 기존 API와 비교하여 Vulkan은 GPU 메모리, 동기화 및 기타 리소스를 정확하게 제어하고, 런타임 확인을 피하고, 명령 대기열 기반 메커니즘을 사용하고, 전역 상태가 없는 등의 작업을 수행할 수 있는 멀티스레딩 및 경량 드라이버 계층을 지원합니다(아래 그림).

Vulkan의 고급 설계 개념 덕분에 렌더링 성능이 더 높으며 일반적으로 CPU, GPU, 대역폭, 에너지 소비 및 기타 지표 측면에서 OpenGL보다 우수합니다. 그러나 애플리케이션 자체의 CPU 또는 GPU 로드가 높으면 Vulkan 사용의 이점이 그다지 명확하지 않을 수 있습니다.

  • 오클루전 컬링을 사용합니다.

오클루전 컬링은 GPU 파이프라인에 들어가 대역폭과 컴퓨팅 리소스를 점유하는 것을 방지하기 위해 화면의 작은 부분을 차지하는 막힌 개체나 멀리 있는 개체를 미리 제거할 수 있습니다. 이동 단말기에서 UE의 폐색 제거는 두 프레임만큼 지연됩니다(BasePass가 끝날 때까지 깊이 버퍼를 사용할 수 없기 때문에 결과를 사용할 수 있도록 하려면 또 다른 프레임 지연이 필요함).

그런 다음 RHI 스레드는 오클루전 쿼리의 결과를 기다립니다. 폐색 쿼리의 결과는 렌더링 스레드에서 사용됩니다. 두 프레임만큼 지연되므로 렌더링 스레드는 가시성을 계산할 때 기다릴 필요가 없습니다.

오클루전 컬링을 사용할 때는 다음 권장 사항을 따라야 합니다.

  1. 동기 대기는 매우 비효율적이므로 필요할 때만 쿼리 결과를 반환하고 기다리지 마십시오.

  2. 교합의 경우 필요한 경우에만 정확한 개수 옵션을 사용하십시오. OpenGL ES는 GL_ANY_SAMPLES_PASSED를 사용하고 Vulkan은 폐색 수를 실제로 알아야 하는 경우가 아니면 VK_QUERY_Control_PRECISE_BIT = false를 사용합니다.

  3. 그리기 호출에서 참조된 리소스를 수정하지 마세요.

  4. glMapBufferRange()와 함께 GL_MAP_INVALIDATE_BUFFER / GL_MAP_INVALIDATE_RANGE를 사용하지 마십시오. 이러한 플래그는 일부 드라이버 버전에서 불필요한 리소스 복사본 생성을 트리거하기 때문입니다.

12.6.1.2 파이프라인 최적화

  • 테셀레이션 중에 하위 픽셀을 제거합니다.

테셀레이션은 다른 게임 하위 시스템이 저해상도 메시 표현에서 작동할 수 있도록 하여 세부 수준을 높이고 메모리 대역폭과 CPU 주기를 줄일 수 있습니다. 그러나 테셀레이션 수준이 높으면 하위 픽셀 삼각형이 생성되어 래스터라이제이션 활용도가 낮아질 수 있습니다. 하위 픽셀 삼각형을 방지하는 테셀레이션 요소를 계산하려면 거리, 화면 공간 크기 또는 기타 적응형 측정값을 사용하는 것이 중요합니다.

  • 테셀레이션 중에 뒷면 컬링을 켭니다.

기본 요소의 뒷면 컬링은 중복 픽셀이 픽셀 셰이더에 들어가는 것을 방지하여 성능을 향상시킵니다.

  • 사용하지 않는 렌더 타겟 또는 셰이더 리소스를 삭제합니다.

더 많은 RT 또는 셰이더 리소스를 운영하면 대역폭이 늘어나고 성능이 저하됩니다. 따라서 참조되지 않는 리소스를 삭제해 보십시오.

  • GMEM 로딩을 피하세요.

각 패스가 렌더링되기 전에 그래픽 API를 호출하여 RT를 명시적으로 정리해야 합니다.

OpenGL ES: glClear()

불칸: LOAD_OP_CLEAR / LOAD_OP_DONT_CARE

  • 서브패스 또는 PLS를 사용하세요.

Vulkan의 서브패스(또는 OpenGL ES의 PLS)를 사용하면 여러 패스의 데이터를 GMEM(타일 버퍼)에 지속적으로 저장할 수 있으므로 GMEM에서 전역 메모리로 데이터가 반복적으로 전송되는 것을 방지하여 대역폭과 대기 시간을 줄일 수 있습니다.

  • **PSO 캐시를 사용합니다.**런타임 중에 PSO 개체를 생성하면 CPU 성능이 소모됩니다. 머티리얼이 사용하는 Shader를 오프라인 단계에서 모아서 컴파일하고 바이너리 파일로 저장하여 다음 런타임 호출 시 Cache 파일을 직접 읽어 PSO 객체로 변환할 수 있도록 하면 CPU 부하를 줄일 수 있습니다. 다음 그림은 UE의 PSO 캐싱 메커니즘을 보여줍니다.

자세한 내용은 UE 공식 문서: PSO Caching을 참고하세요.

  • 깊이 전용(Z 전용) 렌더링을 사용합니다.

GPU에는 응용 프로그램이 그림자 맵을 렌더링할 때와 같이 일반 모드보다 두 배 빠른 속도로 Z 전용 픽셀을 쓰는 특수 모드가 있습니다.

GPU를 이 모드로 전환하는 방법에는 두 가지가 있습니다.

  1. 그래픽 API는 하드웨어가 이 특수 렌더링 모드에 들어갈 수 있음을 명확하게 나타냅니다.

  2. 애플리케이션은 특정 렌더링 상태를 드라이버에 표시합니다. 예: 빈 조각 셰이더를 사용하고 프레임 버퍼 쓰기 마스크를 비활성화합니다.

일부 렌더링 프로그램이나 엔진(예: UE)은 전용 PrePass를 사용하여 Early-Z 계산을 최대한 활용하기 위해 심도를 렌더링합니다. 그러나 모바일 GPU는 주의해서 다루어야 하며 실제 테스트를 거쳐야 합니다.

  • 간접 색인 그리기 인터페이스를 사용합니다.

간접 그리기 호출은 오버헤드를 CPU에서 GPU로 이동하여 CPU 및 GPU 대역폭을 줄입니다. 예를 들어 로드 시 그리기 호출 매개변수를 캐시하여 메시가 버퍼링된 개체 저장소에서 렌더링되도록 합니다. 이러한 캐시된 데이터는 glDrawArraysIndirect 또는 glDrawElementsIndirect의 입력 매개변수로 사용될 수 있습니다.

지원하려면 OpenGL ES 3.1이 필요합니다.

  • 드로우 콜 최적화.

재료를 병합하는 동안 기하학적 개체를 병합합니다.

  • CPU 바인딩이 없더라도 일괄 처리를 사용하여 에너지 소비를 줄입니다.

  • 인스턴스를 사용하세요.

  • 비직접 색인 그리기를 사용합니다.

  • 적은 양의 물체를 여러 번 그리는 것을 피하세요.

  • 높음, 중간, 낮음 이미지 품질에 따라 적절한 Draw Call 수를 설정합니다.

일괄 처리를 사용하는 경우 정점의 총 개수 제한에 주의하세요. 이는 인덱스의 표현 범위를 초과할 수 없습니다(보통 최대 65k). 또한 병합이나 일괄 처리 후 객체 경계 상자가 너무 크면 Frustum Cullinig 및 Occlusion Culling과 같은 기술을 컬링에 효과적으로 사용할 수 없기 때문에 성능 저하가 발생할 수 있습니다.

또한 제출된 기하학적 객체의 인접성에 주의를 기울이고 동일한 타일 내에 속하도록 노력하여 적용되는 타일 수를 줄이고 대역폭을 줄이며 캐시 적중률을 향상시켜야 합니다.

위: 좋은 기하학적 개체 제출 순서; 하단: 기하학적 개체 제출 순서가 잘못되었습니다.

  • 알파 테스트를 비활성화/폐기합니다.

알파 테스트는 TBR의 일반적인 프로세스를 방해하고 특히 PowerVR에서 렌더링 파이프라인을 정지시킵니다(알파 테스트 단계는 깊이를 HSR 단계에 다시 기록합니다).

TB(D)R은 일반적으로 불투명한 물체를 렌더링할 때 Early-Z 기술과 특수 숨겨진 표면 제거 기술(HSR, FPK)을 켜기 때문에 이 단계에서 깊이 테스트가 켜지고, 깊이 테스트를 통과한 조각의 깊이가 기록됩니다. 그러나 Shader에서 Alpha Test가 켜져 있거나 Discard를 사용하는 경우에는 Early-Z/hidden Surface Removal 기술 단계에서 조각의 깊이가 유효한지 여부를 판단할 수 없습니다. PS, 알파 테스트 및 기타 단계가 완료될 때까지 기다려야 합니다.

이런 방식으로는 HSR 기술의 장점을 충분히 활용하지 못하여 렌더링 성능이 저하됩니다.

Alpha Test 대신 Alpha Blend를 사용할 수 있습니다. 알파 테스트가 꼭 필요한 경우 개체의 렌더링 순서는 불투명 -> 알파 테스트 -> 혼합 순서를 따라야 합니다.

  • 알파 블렌드를 최소화합니다.

그 이유는 PowerVR GPU와 같은 지연 렌더러가 조각 셰이더가 조각을 처리하기 전에 조각의 가시성을 계산하여 출력 이미지에서 보이지 않는 조각이 불필요하게 처리되는 것을 방지하기 때문입니다. 투명한 물체가 필요한 경우 투명한 물체의 수를 최소한으로 유지하십시오.

Alpha Blend는 깊이에 쓸 수 없고 HSR/FPK를 완전히 활용할 수 없기 때문에 Overdraw가 발생하여 대역폭과 데이터 전송량이 증가합니다.

꼭 필요한 경우 다음과 같은 최적화 제안을 따르세요.

  1. 부동 소수점 숫자 대신 unorm 형식을 사용하는 것을 우선시합니다. (참고: 이 제안은 Arm Mali에서 제공한 것입니다. 다른 GPU는 다를 수 있습니다. 실제 측정을 기반으로 합니다.)

  2. 불투명한 객체인 경우 적용 범위에 대한 혼합 및 알파트를 비활성화해야 합니다.

  3. MSAA 데이터를 전달하는 부동 소수점 프레임 버퍼에는 블렌딩을 사용하지 마십시오.

  4. 과도한 OverDraw를 피하십시오. 픽셀별로 생성된 혼합 레이어 수를 모니터링합니다. 단순한 셰이더의 경우에도 블렌딩 레이어 수가 많으면 조각 수가 많기 때문에 클럭 사이클을 빠르게 소모할 수 있습니다.

  5. 큰 UI 요소를 불투명한 부분과 투명한 부분으로 나누는 것을 고려해보세요. 그런 다음 불투명한 부분과 투명한 부분을 별도로 그릴 수 있으므로 Early-ZS 또는 FPK/HSR이 불투명한 부분 아래의 OverDraw를 제거할 수 있습니다.

  6. 프래그먼트 셰이더에서 알파를 1.0으로 설정하여 블렌딩을 비활성화하지 마십시오.

  • Early-Z 및 FPK/HSR을 최대한 활용하여 가려진 픽셀을 제거합니다.

Early-Z를 최대한 활용하려면 개체 그리기 순서가 다음과 같아야 합니다.

  1. 불투명한 물체를 그립니다. 앞에서 뒤로 그립니다.

  2. 마스크된 개체를 그립니다. 앞에서 뒤로 그립니다.

  3. 반투명한 물체를 그립니다. 뒤에서 앞으로 그립니다.

TBR 아키텍처를 광범위하게 지원하는 모바일 GPU의 경우 프리패스 그리기 전용 깊이를 활성화하지 않는 것이 좋습니다. 그렇지 않으면 대역폭과 그리기 호출이 늘어납니다.

또한 불투명한 개체를 그릴 때는 다음을 수행해 보세요.

  1. 폐기 문을 비활성화합니다.

  2. 적용 범위에 대한 알파를 비활성화합니다.

  3. 프래그먼트 셰이더의 깊이를 수정하는 것은 금지되어 있습니다.

위 사항 중 하나라도 위반하면 Early-Z가 비활성화되고 Late-Z가 강제로 사용되므로 렌더링 효율성이 저하됩니다.

  • 자르기 및 테스트를 완전히 활성화합니다.

자르기 기술에는 오클루전 컬링, 프러스텀 클리핑, 가위, 거리 클리핑, LOD 등이 포함됩니다.

테스트에는 뒷면 테스트, 깊이 테스트, 스텐실 테스트 등이 포함되지만 투명성 테스트는 비활성화됩니다.

  • Z-프리패스를 비활성화합니다.

모바일 GPU에는 일반적으로 TBR 구조를 기반으로 하는 픽셀 수준 컬링이 내장되어 있어 깊이를 특별히 그릴 필요가 없습니다. UE는 모바일 단말에서 기본적으로 Z-Prepass를 비활성화합니다.

  • 스텐실 버퍼 업데이트를 최소화합니다.
  1. 값이 동일하면 REPLACE 대신 KEEP을 사용하십시오.

  2. 일부 렌더러(예: UE)는 조명을 사용하여 패스 쌍을 그립니다. 첫 번째 패스는 템플릿 버퍼를 만드는 데 사용되고 두 번째 패스는 마스크되지 않은 조각의 색상을 지정하는 데 사용됩니다. 다음 조명 페어링을 준비하기 위해 두 번째 패스에서 템플릿 값을 재설정할 수 있으므로 별도의 템플릿 정리 작업이 필요하지 않습니다.

UE의 모바일 씬 렌더러는 조명을 그릴 때 이 템플릿 정리 최적화 방법을 사용합니다.

  • 그래픽 API를 올바르게 호출합니다.

목표 성능이 달성되지 않으면 GPU를 유휴 상태로 만드는 방식으로 API를 사용하지 마십시오.

  • 렌더링 파이프라인에서 펜스 및 쿼리 개체를 너무 일찍 기다리지 마십시오.

  • glMapBufferRange()를 호출할 때 GL_MAP_UNSYNCHRONIZED 플래그를 사용하여 렌더링 파이프라인이 정지되는 것을 방지하기 위해 비동기성을 활성화합니다.

  • 다음 인터페이스를 동기식으로 호출하지 마세요.

glFlush(). 그러나 일부 GPU(예: PowerVR)는 이중 버퍼링 메커니즘으로 인해 호출 스레드를 차단하지 않습니다.

  • glFinish()

  • glReadPixels()

  • glWaitSync()

  • glClientWaitSync()

-eglClientWaitSync()

  • GL_MAP_UNSYNCHRONIZED 플래그가 없는 glMapBufferRange()

위 인터페이스에 대한 불필요한 호출을 피하세요. 통화는 적을수록 좋습니다.

  • 렌더 패스를 분할하기 위해 glFlush()를 사용하지 마십시오. 드라이버(Mali)는 필요할 때 자동으로 플러시됩니다.

  • 가능할 때마다 Clear를 수행하십시오. 그리기 전이나 렌더링 패스 시작 시 glClear/glDiscardFramebufferEXT/glInvalidateFramebuffer를 사용하여 렌더링 텍스처를 정리하여 GPU가 이전 프레임의 데이터를 타일 버퍼로 읽는 것을 방지하고 대역폭을 절약하세요. Vulkan은 loadOp를 사용합니다.

  • 가능하다면 glColorMask를 사용하여 작성할 필요가 없는 색상 채널을 마스크하세요.

위의 제안 사항을 위반하면 다음과 같은 결과가 발생할 수 있습니다.

  1. 파이프라인이 소진되면 버블 생성 중에 GPU가 부분적으로 유휴 상태가 되어 성능이 저하됩니다.

  2. 시스템의 동적 전압 및 주파수 스케일링 전원 관리 로직과의 상호 작용에 따라 일부 성능 불안정이 발생할 수 있습니다.

  • 명령 버퍼를 최적화합니다.
  1. 최상의 성능을 위해서는 ONE_TIME_SUBMIT_BIT 플래그를 설정하십시오. 꼭 필요한 경우가 아니면 SIMULTANEOUS_USE_BIT를 설정하지 마세요.

  2. 동기 명령 버퍼를 사용하는 대신 프레임별 명령 버퍼를 구축하십시오.

  3. 애플리케이션 로직에서 매번 동일한 명령 시퀀스를 재생하는 것이 대안이라면 SIMULTANEOUS_USE_BIT를 사용하십시오. 이는 명령을 수동으로 재생하는 애플리케이션보다 더 효율적이지만, 한 번에 버퍼를 커밋하는 것보다 덜 효율적입니다.

  4. RESET_COMMAND_BUFFER_BIT가 설정된 명령 풀을 사용하지 마십시오. 이렇게 하면 드라이버가 풀의 모든 명령 버퍼에 대해 단일 대형 할당자를 사용할 수 없기 때문에 메모리 관리 오버헤드가 증가합니다.

  5. 보조 명령 버퍼를 사용하여 멀티스레드 렌더링 패스 구성을 허용합니다.

  6. 프레임당 보조 명령 버퍼 호출 수를 최소화합니다.

  • 설명자 세트 및 레이아웃을 최적화합니다.
  1. 가능한 한 많은 설명자 세트 바인딩 공간을 압축합니다.

  2. 설명자 풀을 재설정하고 새 설명자 세트를 재할당하는 대신 할당되었지만 더 이상 참조되지 않는 설명자 세트를 업데이트합니다.

  3. 동일한 정보가 업데이트되지 않도록 미리 할당된 설명자 세트를 재사용합니다.

  4. VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC 또는 VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC을 사용하여 동일한 UBO 또는 SSBO를 바인딩하지만 오프셋은 다릅니다. 또 다른 옵션은 더 많은 설명자 세트를 구축하는 것입니다.

  5. 디스크립터 세트에 공백을 두지 마십시오. 이는 공간을 낭비하고 액세스 연속성을 차단합니다.

  6. 복사 및 병합에는 여전히 비용이 들기 때문에 사용하지 않은 항목을 남겨두지 마십시오.

  7. 성능이 중요한 코드 경로의 설명자 풀에서 설명자 세트를 할당하지 마십시오.

  8. 바인딩 오프셋을 변경할 계획이 없다면 DYNAMIC_OFFSET UBO/SSBO를 사용하지 마십시오. 동적 오프셋을 처리하는 데 약간의 추가 비용이 발생하기 때문입니다. 비효율적인 설명자 세트 및 레이아웃 최적화되지 않은 Vulkan 설명자 세트 및 레이아웃의 부정적인 영향으로 인해 그리기 호출의 CPU 소비가 증가할 수 있습니다.

  • 파이프라인 버블 렌더링을 피하세요(유휴).

렌더링 파이프라인 버블은 다음과 같은 상황에서 발생합니다.

  1. 명령 버퍼가 충분히 자주 제출되지 않습니다. 명령 버퍼를 자주 제출하면 GPU 처리 대기열의 작업량이 줄어들어 잠재적인 오케스트레이션 기회가 제한됩니다.

  2. 데이터 의존성. 렌더링 패스 M과 N이 있고 M은 이후 단계에 있다고 가정합니다. 파이프라인 초기에 M이 N을 사용할 때 데이터 종속성이 발생합니다. 데이터 종속성으로 인해 지연이 발생하며, 그 동안 결과 생성 지연을 숨기기 위해 충분한 작업을 수행해야 합니다.

렌더링 파이프라인 버블 다이어그램. 그림은 CPU, VS, PS에 버블이 있음을 보여줍니다.

다음 제안 사항은 줄 거품을 줄일 수 있습니다.

  1. 명령 버퍼를 자주 제출하십시오. 예를 들어, 프레임의 각 주요 렌더 패스 이후가 아니라 렌더 패스 중에 커밋하는 것은 적절하지 않습니다.

  2. 특정 조건으로 인해 거품이 발생하는 경우 거품 채우기 기술을 사용해 보세요. 예를 들어 두 렌더링 패스 사이에 독립적인 작업 부하를 삽입합니다.

  3. 종속 데이터가 사용되는 단계보다 이전 파이프라인 단계에서 종속 데이터를 생성하는 것을 고려하십시오. 예를 들어 컴퓨팅 단계는 버텍스 셰이딩 단계에 대한 입력 데이터를 생성하는 데 적합합니다. 프래그먼트 단계는 버텍스 셰이딩 단계 파이프라인보다 실행이 늦기 때문에 부적절합니다. 그렇지 않으면 정체 및 지연이 발생합니다.

  4. 나중에 파이프라인에서 종속 데이터를 처리하는 것을 고려하세요. 예를 들어, 컴퓨팅 셰이딩이 프래그먼트 셰이딩을 사용하는 것보다 프래그먼트 셰이딩이 다른 프래그먼트 셰이딩의 출력을 사용하는 것이 더 좋습니다.

  5. 펜스를 사용하여 GPU에서 CPU로 데이터를 비동기식으로 읽습니다. GPU에서 CPU로 데이터를 동기적으로 읽는 인터페이스를 호출하지 마십시오. 그렇지 않으면 전체 렌더링 파이프라인이 심각하게 지연될 수 있습니다.

또한 다음 제안을 사용하여 렌더링 파이프라인을 최적화할 수 있습니다.

  1. 파이프라인 어디에서나 GPU 데이터를 불필요하게 기다리지 마십시오.

  2. 모든 렌더링 패스를 제출하기 위해 프레임이 끝날 때까지 기다리지 마십시오.

  3. 대기 시간을 숨기기 위한 충분한 중간 작업 없이 파이프라인에서 역방향 데이터 종속성을 생성하지 마십시오.

  4. vkQueueWaitIdle() 또는 vkDeviceWaitIdle()을 사용하지 마세요.

  • 파이프라인 동기화를 올바르게 사용하십시오.

최신 그래픽 API(예: Vulkan)에는 매우 세분화된 파이프라인 단계가 있습니다.

python
typedef enum VkPipelineStageFlagBits
{
    VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT = 0x00000001,
    
    // Vertex Stages
    VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT = 0x00000002,
    VK_PIPELINE_STAGE_VERTEX_INPUT_BIT = 0x00000004,
    VK_PIPELINE_STAGE_VERTEX_SHADER_BIT = 0x00000008,
    VK_PIPELINE_STAGE_TESSELLATION_CONTROL_SHADER_BIT = 0x00000010,
    VK_PIPELINE_STAGE_TESSELLATION_EVALUATION_SHADER_BIT = 0x00000020,
    VK_PIPELINE_STAGE_GEOMETRY_SHADER_BIT = 0x00000040,
    // Fragment Stages
    VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT = 0x00000080,
    VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT = 0x00000100,
    VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT = 0x00000200,
    VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT = 0x00000400,
    // Compute Stages
    VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT = 0x00000800,
    VK_PIPELINE_STAGE_TRANSFER_BIT = 0x00001000,
    VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT = 0x00002000,
    
    VK_PIPELINE_STAGE_HOST_BIT = 0x00004000,
    VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT = 0x00008000,
    VK_PIPELINE_STAGE_ALL_COMMANDS_BIT = 0x00010000,
    
    (......)
} VkPipelineStageFlagBits;

최신 그래픽 API(예: Vulkan)에는 수많은 동기화 개체도 포함되어 있습니다.

  1. 단일 대기열 내에서 세분화된 동기화에 사용되는 서브패스 종속성, 파이프라인 장벽, 이벤트 등.

  2. 세마포어(신호)는 대기열 전체에 걸쳐 더 큰 종속성을 위해 사용됩니다.

파이프라인 종속성에는 srcStagedstStage라는 두 가지 변수가 있습니다. srcStage는 기다려야 하는 파이프라인 단계를 나타내고, dstStage는 처리가 시작되기 전에 동기화를 기다려야 하는 파이프라인 단계를 나타냅니다.

병렬 효율성을 높이고 파이프라인 버블을 줄이려면 srcStage는 가능한 한 빨리, dstStage는 최대한 늦는 것이 좋습니다. srcStageVK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT인 경우 최악의 성능을 얻습니다.

세마포어는 pWaitDstStages를 사용하여 특정 단계를 지정할 수 있습니다.

보다 구체적으로 다음 지침을 따르면 더 나은 렌더링 효율성을 얻을 수 있습니다.

  1. srcStageMask는 일찍 설정할수록 좋습니다.

  2. dstStageMask는 나중에 설정할수록 좋습니다.

  3. 종속성이 순방향(예: srcStageMask는 버텍스 또는 계산, dstStageMask는 조각)인지 역방향(예: srcStageMask는 조각, dstStageMask는 버텍스 또는 계산)인지 확인합니다. 역방향 종속성 사용을 최소화합니다.

  4. 역방향 종속성이 실제로 필요한 경우 리소스 생성과 소비 사이에 충분한 지연을 추가하여 역방향 종속성으로 인한 일정 거품을 숨깁니다.

  5. srcStageMask = ALL_GRAPHICS_BIT 및 dstStageMask = FRAGMENT_SHADER_BIT를 사용하여 두 렌더링 패스를 서로 동기화합니다.

  6. 제로 복사 알고리즘이 가장 효과적이므로 TRANSFER 복사 작업의 사용을 최소화하세요. TRANSFER 복사본이 하드웨어 파이프라인에 미치는 영향에 세심한 주의를 기울이세요.

  7. 대기열 내 장벽은 필요한 경우에만 사용하고 장벽 사이에 가능한 한 많은 작업을 예약하십시오.

  8. 하드웨어를 유휴 상태로 두지 마십시오.

  9. 겹치는 버텍스/계산 및 조각을 처리하는 것을 잊지 마십시오.

  10. 다음 srcStageMask - dstStageMask 동기화 조합은 파이프라인을 완전히 소모시키므로 사용하지 마십시오.

cpp
BOTTOM_OF_PIPE_BIT to TOP_OF_PIPE_BIT
ALL_GRAPHICS_BIT to ALL_GRAPHICS_BIT
ALL_COMMANDS_BIT to ALL_COMMANDS_BIT
  1. 파이프라인 장벽을 병합하는 경우 잘못된 종속성을 도입하지 않도록 주의하세요. 버텍스/조각의 겹침을 끊고 불필요한 버블을 생성하지 않도록 주의하세요.

  2. VkEvent 신호를 사용하지 말고 즉시 이벤트를 기다리십시오. vkCmdPipelineBarrier()를 사용하십시오.

  3. 단일 대기열에서 종속성 관리를 위해 VkSemaphore를 사용하지 마십시오.

  4. 렌더링 파이프라인에 유휴 공간을 너무 많이 두지 마십시오(그렇지 않으면 성능이 저하됩니다). 렌더링 파이프라인의 유휴 공간이 너무 적지 않도록 하십시오(그렇지 않으면 오류가 발생할 수 있습니다).

  • 파이프라인 리소스를 올바르게 처리합니다.

OpenGL ES는 기본 실행이 비동기식일 수 있고 그리기 호출 시 데이터 리소스의 상태를 반영해야 하는 경우에도 애플리케이션 개발자에게 동기식 렌더링 모델을 제공합니다. 보류 중인 그리기 호출이 여전히 리소스를 참조하는 동안 애플리케이션이 리소스를 수정하는 경우 드라이버는 정확성을 보장하기 위해 회피 조치를 취해야 합니다.

이러한 리소스를 처리하는 드라이버의 동기화 동작은 GPU 공급업체에 따라 다릅니다. 예를 들어 Mali 드라이버는 리소스 참조 횟수가 0에 도달할 때까지 차단하고 기다리는 것을 방지합니다. 그렇게 하면 파이프라인이 소진되고 성능이 저하될 수 있기 때문입니다. Mali GPU는 완전히 새로운 버전의 리소스를 생성하며, 보류 중인 그리기 호출이 완료되고 해당 참조 수가 0으로 떨어질 때까지 리소스의 이전 버전 또는 고스트 버전이 유지됩니다. 일부 다른 드라이버(예: PowerVR)는 이 프레임의 렌더링 파이프라인을 차단하고 다음 프레임까지 처리를 지연시켜 성능 저하를 유발합니다.

이 동작은 비용이 많이 들고 새 리소스에 메모리를 할당하고 완료되면 빈 리소스를 정리해야 합니다. 업데이트가 완전한 교체가 아닌 경우 이전 리소스 버퍼에서 새 리소스 버퍼로 복사해야 합니다.

리소스를 최적화하려면 다음 권장 사항을 따라야 합니다.

  1. 대기열에 있는 그리기 호출에서 참조하는 리소스를 수정하지 않으려면 N 버퍼 리소스를 사용하고 파이프라인을 통해 동적 리소스 업데이트를 수행할 수 있습니다.

  2. GL_MAP_UNSYNCHRONIZED 플래그를 사용하여 glMapBufferRange()를 사용하여 동적 드로잉 호출에 의해 여전히 참조되는 버퍼의 참조되지 않은 영역을 채울 수 있습니다. glMapBufferRange()와 함께 GL_MAP_INVALIDATE_BUFFER / GL_MAP_INVALIDATE_RANGE를 사용하지 마십시오. 이러한 플래그는 일부 드라이버 버전에서 불필요한 리소스 복사본 생성을 트리거할 수 있습니다.

  • 텍스처 리소스를 효율적으로 업로드합니다.

텍스처 리소스를 그래픽 하드웨어에 업로드할 때 압축되지 않은 텍스처는 선형 스캔 라인에 업로드되고, 압축된 텍스처는 블록별로 업로드됩니다.

일부 GPU(예: PowerVR)는 내부적으로 고유한 레이아웃을 사용하여 메모리 액세스 지역성과 캐시 효율성을 향상합니다. 데이터 재포맷은 전용 하드웨어에 의해 온칩으로 수행되므로 매우 빠릅니다. 다음 단계를 수행하면 성능이 향상될 수 있습니다.

  1. 초기화와 같이 성능이 중요하지 않은 기간 동안 텍스처를 업로드합니다. 텍스처 로딩과 관련된 프레임 속도 저하를 방지하는 데 도움이 됩니다.

  2. 프레임(중간 프레임) 동안 해당 프레임에 이미 사용된 텍스처 개체에 텍스처 데이터를 업로드하지 마세요.

  3. 텍스처 업로드가 완료된 후 준비 단계를 수행합니다. 여전히 텍스처 로딩과 관련된 프레임 속도 저하를 방지하는 데 도움이 됩니다.

앞서 언급한 워밍업 단계를 통해 텍스처가 즉시 완전히 업로드됩니다. 기본적으로 glTexImage2D는 즉시 업로드에 필요한 모든 처리를 수행하지 않으며 텍스처는 처음 사용할 때 완전히 업로드됩니다. 화면에 일련의 삼각형을 그리거나 문제의 텍스처 개체를 바인딩하여 강제로 업로드할 수 있습니다.

12.6.1.3 대역폭 최적화

  • **데이터가 저장되는 위치에 주의하세요.**예: RAM, VRAM, 타일 버퍼, GPU 캐시를 사용하여 불필요한 데이터 전송을 줄입니다.

  • **데이터 액세스 유형에 중점을 둡니다.**예: 읽기 전용인지 쓰기 전용 작업인지, 원자성 작업이 필요한지, 캐시 일관성이 필요한지 여부.

  • **데이터 캐싱의 타당성에 중점을 둡니다. 하드웨어는 후속 작업을 위해 GPU의 빠른 액세스를 위해 데이터를 캐시할 수 있습니다.**캐시 적중률은 다음 사항을 통해 향상될 수 있습니다.

전송 속도를 향상시키고 클라이언트 측 버텍스 데이터 버퍼가 가능한 적은 수의 그리기 호출에 사용되도록 보장합니다. 이상적으로는 응용 프로그램에서 이를 사용해서는 안 됩니다.

  • 디스패치 또는 드로우 콜을 수행할 때 GPU가 액세스해야 하는 데이터 양을 줄입니다. 이를 통해 가능한 한 많은 데이터를 캐시 라인에 배치할 수 있으며 적중률이 향상됩니다.

  • **텍스처 압축 형식을 사용합니다.**ASTC에 우선순위가 부여되고 ETC, PVRTC, BC 및 기타 압축 형식이 그 뒤를 따릅니다. GPU 하드웨어는 일반적으로 이러한 압축 형식을 지원하고 이를 신속하게 인코딩 및 디코딩할 수 있으며 한 번에 더 많은 텍셀 콘텐츠를 GPU의 캐시 라인으로 읽어 캐시 적중률을 향상시킬 수 있습니다.

  • **비트 수가 적은 픽셀 형식을 사용합니다.**예를 들어 RGB565는 RGB888보다 비트가 8개 적고 ASTC_6X6은 ASTC_4x4를 대체합니다. Adreno에서 지원하는 픽셀 형식은 사양 시트를 참조하세요.

  • **고정밀도(FP32) 데이터 대신 반정밀도(예: FP16)를 사용하세요.**모델 버텍스, 인덱스 데이터 등 AOS 대신 SOA(Structure of Array) 데이터 레이아웃을 사용할 수 있습니다.

  • **렌더링을 위해 해상도를 줄이고 나중에 확대하세요.**대역폭, 계산량을 줄이고 장치 발열을 줄일 수 있습니다.

  • **도면 수를 최소화하세요.**그리기 수를 줄이면 CPU와 GPU 사이, 그리고 GPU 내에서 대역폭과 소비가 줄어들 수 있습니다.

  • 데이터가 온칩 내에 저장되어 있는지 확인하세요.

PLS 및 Subpass 기능을 사용하면 모바일 측에서 디퍼드 렌더링, 파티클 소프트 믹싱 등을 구현할 수 있습니다. 다음 표는 디퍼드 렌더링을 구현할 때 PowerVR GX6250에서 사용하는 다양한 비트 수와 성능 간의 관계를 보여줍니다.

구성 시간/프레임(ms)

96비트+D32 20

128비트+D32 21

160비트+D32 23

192비트+D32 24

224비트+D32 28

256비트+D32 29

288비트+D32 39

위에서 볼 수 있듯이 비트 수가 256보다 크고 GX6250의 최대 비트 수를 초과하면 데이터가 온칩에 완전히 저장되지 않고 글로벌 메모리로 오버플로되어 각 프레임 시간이 10ms씩 34.5% 증가하게 됩니다.

따라서 각 픽셀의 데이터는 데이터가 온칩에 완전히 수용될 수 있도록 신중하게 조립, 최적화 및 압축되어 성능을 효과적으로 향상시키고 대역폭을 절약할 수 있습니다.

  • 중복 복사본을 피하세요.

동일한 메모리(CPU, 그래픽 코어, 카메라 인터페이스, 비디오 디코더 등)를 사용하는 하드웨어 구성 요소가 모두 중간 복사 없이 동일한 데이터에 액세스하도록 보장합니다.

  • **버퍼 및 텍스처와 같은 메모리를 생성하려면 올바른 태그를 사용하십시오.**일부 Mali GPU(예: Bifrost)는 다음과 같은 태그 조합을 수행합니다.
  1. DEVICE_LOCAL_BIT | HOST_VISIBLE_BIT | HOST_COHERENT_BIT

  2. DEVICE_LOCAL_BIT | HOST_VISIBLE_BIT | HOST_CACHED_BIT

  3. DEVICE_LOCAL_BIT | HOST_VISIBLE_BIT | HOST_COHERENT_BIT | HOST_CACHED_BIT

  4. DEVICE_LOCAL_BIT | LAZILY_ALLOCATED_BIT

HOST_VISIBLE_BIT의 메모리 유형 | HOST_COHERENT_BIT | HOST_CACHED_BIT는 다음과 같이 설명됩니다.

  1. 수동 동기화 없이 메모리의 GPU 보기와 일치하도록 CPU에 캐시 스토리지를 제공합니다.

  2. 칩셋이 CPU와 GPU 간의 하드웨어 일관성 프로토콜을 지원하는 경우 GPU는 이 태그 조합을 지원합니다.

  3. 하드웨어의 일관성으로 인해 수동 동기화 작업의 오버헤드를 방지합니다. 캐시된 일관된 메모리는 사용 가능한 경우 캐시된 일관성 없는 메모리 유형보다 우선합니다.

  4. CPU의 응용 소프트웨어가 매핑하고 읽는 데 사용해야 하는 리소스입니다.

  5. 하드웨어 일관성의 전력 소비는 매우 작으므로 CPU의 쓰기 전용 리소스에는 사용할 수 없습니다. 쓰기 전용 리소스의 경우 일관된 메모리 유형은 캐시되지 않음을 사용하여 CPU 캐시를 우회합니다.

LAZILY_ALLOCATED 메모리 유형에 대한 설명:

  1. 처음에는 물리적 메모리 페이지가 아닌 GPU 가상 주소 공간만 지원하는 특수 메모리 유형입니다. 메모리에 액세스하면 필요에 따라 물리적 페이지가 할당됩니다.

  2. VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT를 사용하여 생성된 임시 첨부 파일과 함께 사용해야 합니다. 임시 이미지의 목적은 프레임 버퍼 연결로 사용되는 것이며 실제 메모리 사용을 피하기 위해 단일 렌더링 프로세스 중에만 존재합니다.

  3. 데이터를 글로벌 메모리에 다시 쓸 수 없습니다.

Vulkan 메모리 태그 사용에 대한 권장 사항은 다음과 같습니다.

  1. 불변 리소스의 경우 HOST_VISIBLE | HOST_COHERENT 메모리.

  2. CPU의 쓰기 전용 리소스의 경우 HOST_VISIBLE | HOST_COHERENT 메모리.

  3. memcpy()를 사용하여 HOST_VISIBLE | HOST_COHERENT 메모리를 사용하거나 CPU 쓰기 결합 장치의 최적 효율성을 위해 순차적으로 씁니다.

  4. HOST_VISIBLE | HOST_COHERENT | HOST_CACHED 메모리는 리소스를 CPU로 다시 읽어옵니다. 이 조합이 가능하지 않으면 HOST_VISIBLE | 호스트_캐시되었습니다.

  5. 단일 렌더링 패스 중에만 존재하는 임시 프레임 버퍼 연결에 대해 LAZILY_ALLOCATED 메모리를 사용합니다.

  6. TRANSIENT_ATTACHMENT 프레임 버퍼 연결에는 LAZILY_ALLOCATED 메모리만 사용하십시오.

  7. 버퍼 매핑 및 매핑 해제는 CPU 성능을 소모합니다. 따라서 균일 버퍼, 데이터 버퍼, 동적 버텍스 데이터 버퍼 등 자주 액세스되는 버퍼는 영구적으로 매핑되어야 합니다.

  • 무복사 경로를 최대한 사용하세요.

아래 그림과 같이 EglImage를 사용하면 카메라와 OpenCL은 원본 이미지 데이터를 공유하고 OpenCL과 OpenGL ES는 최종 이미지 데이터를 공유하므로 복사가 발생하지 않습니다.

  • 그룹 메모리 액세스.

컴파일러는 여러 경험적 방법을 사용하여 읽기 또는 쓰기 작업의 급증으로 결합될 수 있는 커널의 메모리 액세스 패턴을 식별할 수 있습니다. 컴파일러가 이 최적화를 구현하려면 메모리 액세스를 최대한 가깝게 그룹화해야 합니다.

예를 들어, 코어 시작 부분에 읽기를 배치하고 코어 끝 부분에 쓰기를 배치하면 최고의 효율성을 얻을 수 있습니다. 더 큰 데이터 유형(예: 벡터)에 대한 액세스도 가능할 때마다 단일 전송으로 컴파일됩니다. 1개의 float4를 로드하는 것이 4개의 개별 float 값을 로드하는 것보다 낫습니다.

  • 공유/로컬 메모리를 합리적으로 사용하십시오.

Shader의 초기 단계(예: 초기화)에서는 자주 액세스하는 데이터를 먼저 공유/로컬 메모리로 읽어 액세스 속도를 향상시킬 수 있습니다.

  • 행 우선 순서로 메모리에 액세스합니다.

GPU는 일반적으로 행 인접 데이터를 GPU 캐시로 미리 읽습니다. 행 우선 방식으로 셰이더 알고리즘에 접근하면 캐시 적중률이 향상되고 대역폭이 줄어들 수 있습니다.

  • GPU 특정 대역폭 최적화.

Mali의 거래 제거는 다음 상황에만 적용 가능합니다.

  1. 샘플링 데이터는 1입니다.

  2. mimap 레벨은 1입니다.

  3. 이미지는 COLOR_ATTACHMENT_BIT를 사용합니다.

  4. 이미지는 TRANSIENT_ATTACHMENT_BIT를 사용하지 않습니다.

  5. 단색 부착물을 사용하세요. (Mali-G51 GPU 이상에는 이 제한이 없습니다)

  6. 유효 타일 크기는 16x16 픽셀이며 픽셀 데이터 저장소에 따라 유효 타일 크기가 결정됩니다.

Mali GPU는 비디오 메모리와 대역폭을 줄일 수 있는 AFBC 텍스처도 지원합니다.

12.6.2 리소스 최적화

12.6.2.1 텍스처 최적화

  • 압축 형식을 사용합니다.

ASTC는 뛰어난 압축률, 원본 이미지에 가까운 이미지 품질, 더 많은 플랫폼에 대한 적응성으로 인해 선호되는 텍스처 압축 형식이 되었습니다. 따라서 가능하면 ASTC를 사용해 보십시오. 일부 고대 장비가 ASTC를 지원하지 않는 한 ETC 및 PVRTC와 같은 텍스처 압축 형식을 사용하는 것이 좋습니다. 자세한 내용은 12.4.14 적응형 확장 가능 텍스처 압축을 참조하세요.

  • 가능할 때마다 Mipmap을 사용하십시오.

텍스처 밉맵은 메모리 사용량을 늘려 텍스처를 샘플링할 때 사용되는 데이터 양을 줄여 대역폭을 줄이고 버퍼 적중률을 향상시키며 이미지 품질을 향상시킵니다. 물고기와 곰의 발을 모두 얻을 수 있는데 왜 안 되겠습니까? 구체적으로 다음과 같은 측면에서 나타납니다.

  1. 그래픽 렌더링 성능을 향상시키기 위해 텍스처 캐시 효율성을 크게 향상시킵니다. 특히 강력한 감소의 경우 텍스처 데이터가 타일 메모리에 설치될 가능성이 높습니다.

  2. 밉매핑 없이 텍스처 샘플링 부족으로 인한 앨리어싱을 줄여 이미지 품질을 향상시킵니다.

그러나 밉맵을 사용하면 메모리 사용량이 33% 증가합니다. 다음 상황은 피해야 합니다.

  1. 예를 들어 이미지가 아닌 데이터(인덱스 또는 깊이 텍스처)가 포함된 텍스처에는 필터링을 제대로 적용할 수 없습니다.

  2. 텍셀이 항상 픽셀에 일대일로 매핑되는 UI 요소와 같이 결코 축소되지 않는 텍스처.

  • 패키지 아틀라스를 사용하세요.

아틀라스를 패키징한 후 일괄 렌더링하거나 렌더링을 인스턴스화하여 CPU 및 GPU 대역폭을 줄일 수 있습니다.

  • 치수는 2의 N승으로 유지됩니다.

현재 그래픽 API는 이미 2N 전력 크기(NPOT)가 아닌 텍스처를 지원하지만 텍스처 크기를 2N 전력(POT)으로 유지하는 것이 좋습니다.

  1. 대부분의 경우 POT 텍스처는 NPOT 텍스처보다 선호되어야 합니다. 이는 하드웨어 및 드라이버 최적화가 작동할 수 있는 최고의 기회를 제공하기 때문입니다. (예: 텍스처 압축, Mimap 생성, 캐시 라인 정렬 등)

  2. 2D 애플리케이션은 NPOT 텍스처를 사용해도 성능 손실이 발생하지 않아야 합니다(업로드할 때 제외). 2D 애플리케이션은 NPOT 텍스처가 일대일 텍셀 대 픽셀 매핑으로 표시되는 UI 요소를 렌더링하는 브라우저 또는 기타 애플리케이션일 수 있습니다.

  3. 텍스처 업로드 시 하드웨어 최적화가 가능하도록 텍스처의 길이와 너비가 32픽셀의 배수인지 확인합니다.

  • 텍스처 크기를 최소화합니다.

  • 텍스처 비트 깊이를 최소화합니다.

  • 텍스처 구성요소 수를 최소화합니다.

  • **여러 맵을 압축하려면 텍스처 채널을 사용하세요.**예를 들어 재질의 러프니스, 반사성, 메탈릭, AO 및 기타 텍스처를 동일한 텍스처의 RGBA 채널에 패키징합니다.

12.6.2.2 버텍스 최적화

  • **분리된 위치를 사용한 엇갈린 버텍스 레이아웃.**자세한 내용은 12.4.11 인덱스 기반 버텍스 셰이딩을 참조하세요.

  • **적절한 버텍스 및 인덱스 저장 형식을 사용합니다.**데이터 정확도를 낮추면 메모리와 대역폭이 줄어들 수 있으며 컴퓨팅 장치에서 수행되는 계산량이 늘어날 수 있습니다. 현재 주류 모바일 GPU에서 지원되는 버텍스 형식은 다음과 같습니다.

cpp
GL_BYTE
GL_UNSIGNED_BYTE
GL_SHORT
GL_UNSIGNED_SHORT
GL_FIXED
GL_FLOAT
GL_HALF_FLOAT
GL_INT_2_10_10_10_REV
GL_UNSIGNED_INT_2_10_10_10_REV
  • 기하학적 객체 인스턴스화를 고려하세요. 최신 모바일 GPU는 일반적으로 대역폭을 줄이기 위해 소량의 기하학적 데이터를 제출하여 여러 번 그릴 수 있는 인스턴스 렌더링을 지원합니다. 각 인스턴스는 색상, 변환 매트릭스, 조명 등과 같은 자체 데이터를 가질 수 있습니다. 일반적으로 나무, 잔디, 건물, 군인 그룹 및 기타 개체에 사용됩니다.

  • **기본 유형은 삼각형을 사용합니다.**최신 GPU는 삼각형을 처리하도록 설계되었습니다. 사각형 등이라면 효율성이 저하될 가능성이 높습니다.

  • **인덱스 배열 크기를 줄입니다.**예를 들어 단순 목록 형식 대신 스트립 형식을 사용하고, 퇴화된 삼각형 대신 원본 유효 인덱스를 사용합니다.

  • 변환 후 캐시의 경우 인덱스를 로컬로 최적화하세요.

  • **공간 일관성이 낮은 인덱스 버퍼를 사용하지 마세요.**캐시 적중률이 감소합니다.

  • 균일한 버퍼 크기 제한을 해결하려면 인스턴스 속성을 사용하세요. 예를 들어 16KB 통합 버퍼입니다.

  • 인스턴스당 2^n 꼭짓점을 사용합니다.

  • 인스턴스별 속성 데이터 대신 유니폼 버퍼 또는 셰이더 스토리지 버퍼에 대한 gl_InstanceID를 사용하여 인덱스 조회의 우선순위를 지정합니다.

12.6.2.3 그리드 최적화

  • LOD를 사용하세요.

메시를 사용하는 LOD는 렌더링 성능을 향상시키고 대역폭을 줄일 수 있습니다. 반대로, LOD를 사용하지 않으면 성능 병목 현상이 발생합니다.

다른 LOD가 있는 동일한 그리드의 와이어프레임 모드.

다음은 낭비되는 컴퓨팅 및 메모리 리소스의 예입니다.

  1. 많은 수의 다각형을 사용하는 개체는 멀리 있는 배경 개체와 같이 화면의 작은 영역을 덮지 않습니다.

  2. 카메라 각도나 자르기(예: 시야 원뿔 외부의 개체)로 인해 다각형을 사용하는 세부 정보가 표시되지 않습니다.

  3. 객체에 대해 많은 수의 기본 요소를 사용하십시오. 실제로 시각적 효과를 잃지 않으면서도 더 적은 수의 그래픽 요소로 그릴 수 있습니다.

  • **모델을 단순화하고 정점을 병합합니다.**가까이 인접한 정점을 병합하면 메쉬 버텍스 수를 효과적으로 줄일 수 있으며 메쉬 단순화 기술을 사용하여 좋은 LOD 데이터를 생성할 수 있습니다.

  • **서로 가까이 있는 작은 메시를 오프라인으로 병합합니다.**모래, 자갈, 식물 등

  • 단일 메시의 버텍스 수는 65,000개를 초과할 수 없습니다. 주로 모바일 단말기의 버텍스 인덱스 정밀도가 16비트이고, 최대값이 65535이기 때문입니다.

  • **보이지 않는 기본 요소를 삭제합니다.**예를 들어 상자 내부의 삼각형입니다.

  • 노멀 맵과 범프 맵이 포함된 간단한 기하학적 개체를 사용하여 세부 정보를 추가하세요.

  • 작은 면적의 삼각형은 피하세요.

쿼드 그리기 메커니즘은 작은 면적의 삼각형에 대한 OverDraw를 크게 향상시킵니다. PowerVR 하드웨어에서 32픽셀 미만을 차지하는 삼각형의 경우 래스터라이제이션 효율성이 영향을 받아 성능 병목 현상이 발생합니다.

작은 삼각형을 많이 제출하면 하드웨어가 꼭짓점 단계에서 이를 처리하는 데 많은 시간을 소비할 수 있습니다. 여기서 주요 영향은 크기보다는 삼각형의 수입니다. 특히 타일 가속기(TA) 고정 기능 하드웨어에 병목 현상이 발생합니다. 작은 삼각형이 많으면 시스템 메모리에 있는 매개변수 버퍼에 대한 액세스 수가 증가하고 메모리 대역폭 사용량이 늘어납니다.

  • 그리드의 각 프리미티브가 최소 10~20픽셀을 생성할 수 있는지 확인하세요.

  • **거의 정삼각형을 사용합니다.**면적 대 측면 길이의 비율을 최대화하고 생성된 조각 쿼드 수를 줄일 수 있습니다.

  • 길쭉한 삼각형은 피하세요.

작은 삼각형과 마찬가지로 가는 삼각형(아래 그림에서 빨간색으로 표시)도 잘못된 픽셀을 더 많이 생성하고 GPU 리소스를 더 많이 차지하며 오버드로우를 증가시킵니다.

  • **가리비 또는 유사한 기하학적 레이아웃을 사용하지 마십시오.**삼각형 팬의 중심점에는 삼각형 밀도가 높기 때문에 각 삼각형의 픽셀 적용 범위가 매우 낮습니다. 타일 ​​축 정렬 절단을 고려할 수 있지만 더 많은 삼각형이 발생합니다. (아래 사진)

섹터(왼쪽)(오른쪽)의 타일 축 정렬 절단으로 생성된 삼각형의 수입니다.

12.6.3 셰이더 최적화

12.6.3.1 명령문 최적화

  • 적절한 데이터 유형을 사용하십시오.

코드에서 가장 적절한 데이터 유형을 사용하면 컴파일러와 드라이버가 셰이더 지시어 쌍을 포함하여 코드를 최적화할 수 있습니다. float 대신 vec4 데이터 유형을 사용하면 컴파일러가 최적화를 수행하지 못할 수 있습니다.

hlsl
int4 ResultOfA(int4 a) 
{
    return a + 1; // int4 및 int, 오직 1개 .
}

int4 ResultOfA(int4 a) 
{
    return a + 1.0; // int4 및 float, 3개 : int4 -> float4 -> -> int4
}
  • 유형 변환을 줄입니다.
cpp
uniform sampler2D ColorTexture;
in vec2 TexC;
vec3 light(in vec3 amb, in vec3 diff)
{
    // 텍스처(Texture)샘플링반환(Return)vec4, 변환(Transform/Convert)vec3, 1개 .
    vec3 Color = texture(ColorTexture, TexC); 
    Color *= diff + amb;
    return Color;
}

// 로써 코드 내에서 , 입력파라미터 /변수/반환(Return)는 vec4, 타입/유형 변환(Transform/Convert), 코드 1개 .
uniform sampler2D ColorTexture;
in vec2 TexC;
vec4 light(in vec4 amb, in vec4 diff)
{
    vec4 Color = texture(Color, TexC);
    Color *= diff + amb;
    return Color;
}
  • 패킹된 스칼라 상수.

스칼라 상수를 4개 채널로 구성된 벡터에 채우면 하드웨어 획득 효율성이 크게 향상됩니다. GPU 골격 애니메이션 시스템에서는 스킨 처리된 뼈의 수를 늘릴 수 있습니다.

cpp
float scale, bias;  // 개 float.
vec4 a = Pos * scale + bias; // 개 .

vec2 scaleNbias; // 개 float개 vec2
vec4 a = Pos * scaleNbias.x + scaleNbias.y; // 개 (mad).
  • 스칼라 연산을 사용합니다.

동일한 벡터화된 출력에는 더 많은 시간 주기가 필요하므로 스칼라 연산의 벡터화에 주의하세요. 예를 들어:

cpp
highp vec4 v1, v2;
highp float x, y;

// Bad!!
v2 = (v1 * x) * y; // vector*scalarvector*scalar8개 스칼라 muladd.
// Good!!
v2 = v1 * (x * y); // scalar*scalarvector*scalar5개 스칼라 muladd.

12.6.3.2 상태 최적화

  • 가능하면 const를 사용하세요.

올바르게 사용하면 const 키워드는 상당한 성능 향상을 제공할 수 있습니다. 예를 들어, main() 블록 외부에 const 배열을 선언하는 셰이더는 없는 셰이더보다 성능이 훨씬 좋습니다.

또 다른 예는 배열 멤버를 참조하기 위해 const 값을 사용하는 것입니다. 값이 const이면 GPU는 숫자가 변경되지 않을 것임을 미리 알고 셰이더를 실행하기 전에 데이터를 미리 가져올 수 있으므로 지연이 줄어듭니다.

  • 쉐이더 명령의 수를 합리적으로 유지하세요.

너무 긴 셰이더는 종종 비효율적입니다. 예를 들어 텍스처 가져오기 수에 비해 셰이더에 많은 명령어 슬롯을 포함해야 하는 경우 알고리즘을 여러 부분으로 나누는 것을 고려하세요.

알고리즘의 일부로 생성된 값은 텍스처에 저장한 후 텍스처를 샘플링하여 검색할 수 있습니다. 그러나 이 접근 방식은 메모리 대역폭 측면에서 비용이 많이 듭니다. 다음 상황에서도 텍스처 샘플링 효율성이 감소합니다.

  1. 삼선형, 이방성 필터링, 넓은 텍스처 형식, 3D 및 큐브 맵 텍스처, 텍스처 투영을 사용합니다.

  2. 다양한 Lod 그라데이션으로 텍스처 검색을 사용합니다.

  3. 픽셀 쿼드 전체에 대한 그라데이션 계산.

  • 셰이더 명령 수를 최소화하세요.

최신 셰이더 컴파일러는 명령어별 최적화를 수행하는 경우가 많지만 자동으로 효과적이지는 않습니다. 수동으로 개입하여 셰이더를 분석하고 명령을 최대한 줄여야 하는 경우가 많습니다. 하나의 명령어를 저장하는 것조차 가치가 있습니다.

  • uber-shader를 사용하지 마세요.

uber-shader는 정적 분기를 사용하여 여러 셰이더를 단일 셰이더로 결합합니다. 상태 변경 및 일괄 그리기 호출을 줄이려는 경우에 적합합니다. 그러나 GPR 수를 늘리면 성능에 영향을 미치는 것이 일반적입니다.

  • 텍스처를 효율적으로 샘플링합니다.

텍스처를 샘플링(필터링)하는 방법에는 여러 가지가 있으며 성능과 효과는 일반적으로 반비례합니다.

일부 필터링 유형의 텍스처 및 해당 렌더링.

텍스처를 효율적으로 샘플링하려면 다음 규칙을 따라야 합니다.

  1. 무작위 액세스를 피하고 동일한 2x2 픽셀 쿼드 내에서 샘플링을 유지하십시오. 적중률이 높고 셰이더가 더 효율적입니다.

  2. 3D 텍스처 사용을 피하세요. 체적 텍스처에서 데이터를 얻는 것은 결과 값을 계산하기 위해 수행해야 하는 복잡한 필터링으로 인해 일반적으로 비용이 많이 듭니다.

  3. 셰이더 텍스처 샘플 수를 제한하십시오. 셰이더에서 4개의 샘플러를 사용하는 것은 허용되지만 더 많은 텍스처를 샘플링하면 성능 병목 현상이 발생할 수 있습니다.

  4. 모든 텍스처를 압축합니다. 이를 통해 메모리 사용량이 향상되고 렌더링 파이프라인에서 텍스처 지연이 줄어듭니다.

  5. Mipmap을 켜는 것을 고려해보세요. Mipmap은 텍스처 가져오기를 통합하고 메모리 공간을 늘리는 대신 성능을 향상시키는 데 도움이 됩니다. 또한 대역폭을 줄이고 캐시 적중률을 향상시킬 수 있습니다.

  6. 간단한 텍스처 필터링을 사용해 보세요. 높은 성능에서 낮은 성능까지(낮은 효과에서 높은 효과까지) 샘플링 방법: 최근접, 이중선형, 3차, 삼선형 및 이방성. 샘플링 방법이 복잡할수록 더 많은 데이터를 읽게 되어 메모리 액세스 대역폭이 증가하고 캐시 적중률이 감소하며 대기 시간이 길어집니다. 이에 특별한 주의를 기울여야 합니다.

  7. texelFetch/texture()를 먼저 사용하세요. 이는 일반적으로 텍스처 샘플링보다 더 효율적입니다(그러나 도구 분석 및 검증이 필요함).

  8. 미리 계산된 텍스처 LUT에 주의하세요. 실시간 렌더링에서는 복잡한 계산 결과를 텍스처로 인코딩하여 조회 테이블(예: IBL의 방사조도 맵, 피부 지하 산란 사전 통합 맵)로 사용하는 것이 일반적입니다. 이 접근 방식은 셰이더에 병목 현상이 발생하는 경우에만 성능을 향상시킵니다. 조회 테이블의 함수 매개변수와 텍스처 좌표가 인접한 조각 간에 크게 다른 경우 캐시 효율성에 영향을 미칩니다. 이 방법이 실제적인 개선을 제공하는지 여부를 확인하려면 성능 프로파일링을 수행해야 합니다.

  9. Highp 샘플러 대신 Mediump 샘플러를 사용하세요. 후자는 전자의 절반 속도입니다.

  10. AF(이방성 필터링) 최적화 제안:

(1) 먼저 2x 이방성을 사용하여 품질 요구 사항을 충족하는지 평가합니다. 샘플 크기가 높을수록 품질이 향상될 수 있지만 수익이 감소하고 성능 비용에 비례하지 않는 경우가 많습니다.

(2) 삼선형 등방성 대신 2x 이중선형 이방성을 사용하는 것을 고려하십시오. 이방성이 높은 영역에서는 2x 이중선형 알고리즘이 더 빠르고 이미지 품질도 더 좋습니다. 이중선형 필터링으로 전환하면 밉맵 수준 사이의 전환 지점에서 이음새를 볼 수 있습니다.

(3) 가장 많은 이점을 얻는 개체에만 이방성 및 삼선형 필터링을 사용합니다. 8x 삼선형 이방성은 단순 이중선형 필터링보다 16배 더 비쌉니다.

  • 종속 텍스처 읽기(종속 텍스처 읽기)를 피하십시오.

종속 텍스처 읽기는 텍스처 좌표가 (일부 일반적인 변형이 아닌) 셰이더의 일부 계산에 따라 달라지는 특수한 종류의 텍스처 읽기입니다. 이 계산값을 미리 알 수 없기 때문에 텍스처 데이터를 미리 가져올 수 없으므로 셰이더 처리 중 캐시 적중률이 감소하고 지연이 발생합니다.

버텍스 셰이딩 텍스처 조회는 프래그먼트 셰이딩의 zw 채널 변경을 기반으로 한 텍스처 읽기와 마찬가지로 항상 텍스처 읽기에 종속되는 것으로 처리됩니다. 일부 드라이버 및 플랫폼 버전에서는 잘못된 w가 있는 Vec3 또는 Vec4가 제공되면 Texture2DProj()를 종속 텍스처로 읽을 수도 있습니다.

텍스처 읽기에 의존하는 것과 관련된 비용은 하드웨어 스레드 스케줄링을 통해 어느 정도 상쇄될 수 있습니다. 특히 셰이더에는 많은 수학이 필요하기 때문입니다. 이 프로세스에는 스레드 스케줄러가 현재 스레드를 일시 중지하고 USC의 처리를 다른 스레드로 교체하는 작업이 포함됩니다. 이 교체된 스레드는 가능한 한 많이 처리되며 텍스처 가져오기가 완료되면 원래 스레드가 다시 교체됩니다(아래 그림).

GPU의 컨텍스트는 캐시나 메모리에 액세스해야 하므로 여러 클록 사이클이 지연됩니다. 이때 스케줄러는 ALU를 활용하기 위해 두 번째 Context 세트를 활성화합니다.

GPU에서 사용 가능한 컨텍스트가 많을수록 컴퓨팅 장치의 처리량이 더 많이 향상될 수 있습니다. 위 그림의 18개 그룹 컨텍스트 아키텍처는 처리량을 극대화할 수 있습니다.

하드웨어는 메모리 대기 시간을 숨기기 위해 최선을 다하지만, 좋은 성능을 위해서는 가능하면 텍스처 읽기에 의존하지 않는 것이 좋습니다. 애플리케이션은 프래그먼트 셰이더가 실행되기 전에 텍스처 좌표를 계산하려고 합니다.

  • 동적 분기 사용을 피하세요.

동적 분기는 셰이더 명령 시간을 지연시키지만 분기 조건이 일정하면 컴파일러가 이를 최적화합니다. 그렇지 않으면 조건문이 유니폼, 변수 변수와 관련된 경우 최적화가 불가능합니다. 기타 제안 사항:

  1. 공간적으로 인접한 셰이딩 스레드의 동적 분기를 최소화합니다.

  2. min(), max(), 클램프(), mix(), saturate()와 같은 내장 함수를 사용하여 분기 문을 피하십시오.

  3. 계산에 비해 분기의 이점을 검토합니다. 예를 들어, 조명 계산을 위해 카메라에서 임계값 거리 이상의 픽셀을 건너뛰는 것이 직접 계산을 수행하는 것보다 빠른 경우가 많습니다.

  • 패키지 셰이더 보간 데이터.

셰이더 보간에는 픽셀 셰이더에 데이터를 전달하기 위해 **GPR(범용 레지스터, 범용 레지스터)**이 필요합니다. GPR의 수는 제한되어 있습니다. 가득 차면 Stall이 발생하므로 사용을 줄이십시오.

유니폼을 사용할 수 있다면 다양하게 사용할 필요가 없습니다. 두 개의 vec2 텍스처 좌표를 vec4에 넣는 것과 같이 사용 여부에 관계없이 모든 가변에는 4개의 구성 요소가 있으므로 값을 함께 묶습니다. 보다 창의적인 패키징 및 실시간 데이터 압축 방법도 있습니다.

  • 셰이더 GRP 사용량을 줄입니다.

**GPR(General Purpose Register, General Purpose Register)**을 더 많이 점유한다는 것은 계산량이 많다는 것을 의미합니다. 사용 가능한 레지스터가 충분하지 않으면 레지스터 오버플로가 발생하여 성능이 저하될 수 있습니다. 다음 조치를 취하면 GRP 사용량을 줄일 수 있습니다.

  1. 더 간단한 셰이더를 사용하십시오.

  2. GLSL을 수정하여 하나의 명령어라도 줄이면 GPR의 점유가 줄어들 수 있습니다.

  3. 루프를 풀어서 GPR을 저장할 수도 있지만 이는 셰이더 컴파일러에 따라 다릅니다.

  4. 선택한 최종 솔루션이 가장 효율적이도록 대상 플랫폼에 따라 셰이더를 구성합니다.

  5. 언롤링 루프는 텍스처 획득을 셰이더 상단에 배치하는 경향이 있으므로 여러 텍스처 좌표를 저장하고 동시에 결과를 얻으려면 더 많은 GPR이 필요합니다.

  6. 전역변수와 지역변수의 개수를 최소화하세요. 지역 변수의 범위를 줄입니다.

  7. 데이터 차원을 최소화하십시오. 예를 들어 2차원을 사용할 수 있다면 3차원을 사용하지 마세요.

  8. 정밀도가 낮은 데이터 유형을 사용하십시오. FP32 대신 FP16과 같은 것입니다.

  • 셰이더에서 지속적인 수학 연산을 피하세요.

셰이더가 등장한 이후 출시된 거의 모든 게임은 셰이더 상수에 불필요한 수학 지침을 사용했습니다. 이러한 계산을 CPU로 이동하려면 이러한 명령을 셰이더에서 인식해야 합니다. 컴파일된 코드에서 셰이더 상수의 계산을 식별하는 것이 더 쉬울 수 있습니다.

  • 픽셀 셰이더에서는 폐기 및 기타 명령문을 사용하지 마십시오.

일부 개발자는 픽셀 셰이더의 픽셀을 수동으로 삭제(종료라고도 함)하면 성능이 향상될 수 있다고 믿습니다. 실제로는 다음과 같은 이유로 그렇게 간단하지 않습니다.

  1. 스레드의 일부 픽셀이 종료되었지만 동일한 쿼드의 다른 픽셀은 종료되지 않은 경우 셰이더는 계속 실행됩니다.

  2. 컴파일러가 마이크로코드를 생성하는 방법에 따라 다릅니다.

  3. 특정 하드웨어 아키텍처(예: PowerVR)는 TBDR 최적화를 비활성화하여 렌더링 파이프라인 및 데이터 쓰기 저장이 중단됩니다.

  • 픽셀 셰이더에서 심도를 수정하지 마세요.

이유는 앞선 것과 비슷하다.

  • VS에서 텍스처 샘플링을 피하세요.

현재 주류 GPU는 이미 통합 셰이더 아키텍처를 사용하고 있지만 VS와 PS의 실행 성능은 비슷합니다. 그러나 VS의 텍스처 작업이 로컬인지 그리고 텍스처가 압축된 형식을 사용하는지 확인해야 합니다.

  • 특별 그리기 호출 분할.

GPR 및/또는 텍스처 캐시로 인해 셰이더에 병목 현상이 발생하는 경우 그리기 호출을 여러 패스로 분할하면 실제로 성능이 향상될 수 있습니다. 그러나 결과는 예측하기 어렵기 때문에 실제 성능 테스트를 거쳐야 합니다.

  • 가능하면 정밀도가 낮은 부동 소수점 숫자를 사용하세요.

FP16의 컴퓨팅 성능은 일반적으로 FP32의 두 배이므로 셰이더에서는 정밀도가 낮은 부동 소수점 숫자가 최대한 사용됩니다.

python
precision mediump float;

#ifdef GL_FRAGMENT_PRECISION_HIGH
    #define NEED_HIGHP highp
#else
    #define NEED_HIGHP mediump
#endif
        
varying vec2 vSmallTexCoord;
varying NEED_HIGHP vec2 vLargeTexCoord;

UE는 또한 부동 소수점 숫자를 캡슐화하여 다양한 플랫폼과 이미지 품질에 따라 부동 소수점 숫자의 정밀도를 자유롭게 전환할 수 있습니다.

  • PS 작업을 최대한 VS로 마이그레이션하세요.

일반적으로 버텍스 수는 픽셀 수보다 훨씬 적습니다. 계산을 픽셀 셰이더에서 버텍스 셰이더로 마이그레이션하면 GPU 작업 부하가 줄어들어 중복 계산을 제거하는 데 도움이 됩니다.

예를 들어, 조명 계산의 디퓨즈와 스페큘러 반사를 분할하고 PS에서 스페큘러 반사를 유지하면서 디퓨즈를 VS로 마이그레이션하면 효과와 효율성 사이에서 균형이 잘 잡힌 조명 결과를 얻을 수 있습니다.

  • 균일/균일 버퍼 최적화.
  1. 균일한 데이터를 가능한 한 작게 유지하십시오. 대부분의 GPU에서 특정 셰이더를 제대로 실행하려면 128바이트를 넘지 않아야 합니다.

  2. #define, Vulkan의 전용 상수 또는 셰이딩 소스의 정적 구문을 사용하여 유니폼을 OpenGL ES의 컴파일 시간 상수로 변경합니다.

  3. 균일 벡터 또는 행렬에서 항상 0 또는 1인 요소와 같은 상수를 사용하지 마십시오.

  4. 버퍼에서 유니폼을 로드하는 대신 glUniform()을 사용하여 유니폼을 설정하는 데 우선순위를 둡니다.

  5. 균일한 배열을 동적으로 인덱싱하지 마세요.

  6. 인스턴스화를 과도하게 사용하지 마십시오. gl_InstanceID를 사용하여 인스턴스화된 유니폼에 액세스하는 것은 동적 인덱스이므로 레지스터 매핑된 유니폼을 사용할 수 없습니다.

  7. 유니폼 관련 계산을 가능한 한 CPU의 응용 계층으로 이동하십시오.

  8. 셰이더 저장 버퍼 대신 균일한 버퍼를 사용해 보십시오. 균일한 버퍼 공간이 충분하다면 이를 사용해 보십시오. 균일한 버퍼 개체가 GLSL에서 정적으로 색인화되고 충분히 작은 경우 드라이버나 컴파일러는 이를 기본 균일 블록 전역 변수에 사용되는 동일한 하드웨어 상수 RAM에 매핑할 수 있습니다.

  • UBO 설치 공간을 최대한 작게 유지하세요.

UBO가 8k 미만이면 상수 메모리에 넣을 수 있어 더 높은 성능을 얻을 수 있습니다. 그렇지 않으면 전역 메모리에 저장되어 액세스 시간이 크게 늘어납니다.

  • 더 나은 셰이딩 알고리즘을 선택하세요.

더 좋고 효율적인 알고리즘을 선택하는 것이 낮은 수준(명령 수준) 최적화보다 더 중요합니다. 전자는 성능을 크게 향상시킬 수 있기 때문입니다.

  • 적절한 좌표 공간을 선택하세요.

버텍스 셰이더의 일반적인 실수는 모델 공간, 월드 공간, 뷰 공간 및 클립 공간 간에 불필요한 변환을 수행하는 것입니다. 모델 세계 변환이 강체 변환(회전, 평행 이동, 미러링, 조명 등만 해당)인 경우 모델 공간에서 직접 계산할 수 있습니다.

각 정점의 위치를 ​​월드 또는 뷰 공간으로 변환하는 대신 유니폼(조명 위치 및 방향 등)을 모델 공간으로 변환하는 것이 더 좋습니다. 이는 메시별 작업이고 덜 계산 집약적이기 때문입니다. 특정 공간을 사용해야 하는 경우(예: 큐브 매핑 반사) 동일한 셰이더에서 여러 좌표 공간을 사용하지 않도록 전체 셰이더에 대해 이 공간을 사용하는 것이 가장 좋습니다.

  • 보간(가변) 변수를 최적화합니다.

보간 변수의 수를 줄이고, 보간 변수의 크기를 줄이고, 쓸모 없는(프래그먼트 셰이더에서 사용되지 않는) 보간 변수를 제거하고, 압축하고, 가능한 경우 낮은 정밀도에서 중간 정도의 정밀도 데이터 유형을 사용하십시오.

  • Atomic을 최적화합니다.

원자적 연산은 많은 컴퓨팅 알고리즘과 일부 조각 알고리즘에서 일반적입니다. 약간의 수정을 통해 원자적 연산을 통해 직렬 방식이 아닌 고도로 병렬화된 GPU에서 많은 알고리즘을 구현할 수 있습니다.

Atom의 주요 성능 문제는 경합입니다. 원자적 작업은 다양한 셰이더 코어에서 발생합니다. 동일한 캐시 라인을 달성하려면 L2 캐시에 대한 데이터 일관성 액세스가 필요합니다.

단일 셰이더 코어 내에서 원자 작업을 유지하면 경합이 방지됩니다. 여기서 원자 작업은 셰이더 코어가 L1에서 필요한 캐시 라인을 제어할 때 가장 효율적입니다. 다음은 구체적인 최적화 제안 사항입니다.

  1. 알고리즘 설계에 원자를 사용할 때 경합을 피하는 방법을 고려하십시오.

  2. 동일한 캐시 라인에서 여러 원자가 경쟁하는 것을 방지하려면 원자 간격을 64바이트로 설정하는 것이 좋습니다.

  3. 공유 메모리 원자에 축적되어 경합을 상각할 수 있는지 고려하십시오. 그런 다음 스레드 중 하나가 작업 그룹 끝에서 전역 원자 작업을 푸시하도록 합니다.

  • 명령어 캐시를 최대한 활용하세요.

셰이더 코어 명령 캐시는 성능에 영향을 미치는 요소로 종종 간과됩니다. 동시에 실행되는 스레드 수가 많기 때문에 성능에 대한 명령어 캐시의 중요성이 충분히 강조됩니다. 최적화 제안은 다음과 같습니다.

  1. 더 적은 수의 스레드를 사용하는 긴 셰이더 대신 더 많은 스레드를 사용하는 짧은 셰이더를 사용하십시오. 셰이더 명령이 짧을수록 캐시에 적중될 가능성이 더 높습니다.

  2. 동적 분기 없이 셰이더를 사용하세요. 동적 분기는 시간적 지역성을 줄이고 캐시 압력을 증가시킵니다.

  3. 일부 풀면 도움이 될 수 있지만 루프를 너무 공격적으로 펼치지 마십시오.

  4. 동일한 소스 코드에서 중복된 셰이더 프로그램이나 바이너리를 생성하지 마십시오.

  5. 동일한 타일 메모리에서 여러 개의 가시적 조각 음영(즉, 오버드로우)에 주의하십시오. Early-ZS 또는 FPK/HSR에 의해 제거되지 않은 모든 조각 셰이더를 로드하고 실행해야 하므로 캐시 압력이 증가합니다.

12.6.3.3 어셈블리 수준 최적화

셰이더 하위 수준 최적화는 성능이 극도로 민감한 곳이나 최적화의 후반 단계에서만 주의를 기울여 실행하는 것이 좋습니다. 그렇지 않으면 두 배의 노력으로 절반의 결과를 얻을 수 있습니다.

GPU 명령어 세트의 경우 많은 명령어가 1클록 주기에 완료될 수 있지만 일부 명령어에는 여러 주기가 필요합니다. 다음 그림은 PowerVR이 1 클록 주기에 완료할 수 있는 명령 중 일부를 보여줍니다.

최대 성능 측정을 위해 PowerVR 500MHz G6400을 예로 들면 공통 명령어의 최대 성능 데이터는 다음과 같습니다.

데이터 유형 작동 단일 명령어 피연산자 단일 명령 시계 이론적인 처리량

16비트 부동 소수점 제품의 합계 6 1 (0.5 × 4 × 16 × 6) ¼ 1 = 192GFLOPS

플로트 곱하기 및 더하기 4 1 (0.5 × 4 × 16 × 4) ¼ 1 = 128GFLOPS

플로트 곱하기 2 1 (0.5 × 4 × 16 × 2) ¼ 1 = 64GFLOPS

플로트 추가 2 1 (0.5 × 4 × 16 × 2) ¼ 1 = 64GFLOPS

플로트 나누기A 1 4 (0.5 × 4 × 16 × 1) ¼ 4 = 8GFLOPS

플로트 나누기B 1 2 (0.5 × 4 × 16 × 1) ¼ 2 = 16GFLOPS

정수 곱하기 및 더하기 2 1 (0.5 × 4 × 16 × 2) ¼ 1 = 64 길롭스

정수 곱하기 1 1 (0.5 × 4 × 16 × 1) ¼ 1 = 32 길롭스

정수 추가 1 1 (0.5 × 4 × 16 × 1) ¼ 1 = 32 길롭스

정수 나누기 1 30 (0.5 × 4 × 16 × 1) ¼ 30 = 1.07 길롭스

성능 추정치는 이론적 최고값을 기준으로 계산됩니다. 실제로 다양한 종속성, 주파수 감소, 컨텍스트 전환 등으로 인해 실제 피크 값에 도달하지 못할 수도 있습니다.

기본적으로 컴파일러는 부동 소수점 나누기를 두 가지 범위 축소로 구현한 다음 역수 및 곱셈 명령어를 구현하며 4개의 루프가 필요합니다.

또한 정수 나누기는 매우 비효율적이므로 피해야 한다는 점을 언급하는 것이 중요합니다. 먼저 float로 변환한 다음 나눌 수 있습니다.

자세한 명령어 사용 정보는 복잡한 작업을 참조하세요.

다음은 일반적인 저수준 최적화 조치입니다(PowerVR을 예로 들면 다른 GPU는 유사하지만 정확히 동일하지는 않으며 실제 측정이 우선해야 합니다).

  1. USC 코어를 최대한 활용하려면 수학 표현식을 항상 MAD(곱셈-덧셈) 형식으로 작성해야 합니다. 예를 들어 MAD 형식을 사용하도록 다음 표현식을 변경하면 주기 비용을 50%까지 줄일 수 있습니다.
cpp
fragColor.x = (t.x + t.y) * (t.x - t.y); // 2 cycles
{sop, sop, sopmov}
{sop, sop}
-->
fragColor.x = t.x * t.x + (-t.y * t.y); // 1 cycle
{sop, sop}
  1. 일반적으로 역수 형식은 명령어 RCP에 의해 직접 지원되므로 나눗셈을 역수 형식으로 작성하는 것이 가장 좋습니다. 수학적 표현의 단순화를 완성하면 성능이 더욱 향상될 수 있습니다.
cpp
fragColor.x = (t.x * t.y + t.z) / t.x; // 3 cycles
{sop, sop, sopmov}
{frcp}
{sop, sop}
-->
fragColor.x = t.y + t.z * (1.0 / t.x); // 2 cycles
{frcp}
{sop, sop}
  1. sign(x)의 결과는 다음과 같습니다.
cpp
if (x > 0)
{
    return 1;
}
else if(x < 0)
{
    return -1;
}
else
{
    return 0;
}

그러나 기호를 얻기 위해 기호를 사용하는 것은 최적의 선택이 아닙니다.

cpp
fragColor.x = sign(t.x) * t.y; // 3 cycles
{mov, pck, tstgez, mov}
{mov, pck, tstgez, mov}
{sop, sop}
-->
fragColor.x = (t.x >= 0.0 ? 1.0 : -1.0) * t.y; // 2 cycles
{mov, pck, tstgez, mov}
{sop, sop}
  1. sqrt 대신 inversesqrt를 사용하십시오.
cpp
fragColor.x = sqrt(t.x) > 0.5 ? 0.5 : 1.0; // 3 cycles
{frsq}
{frcp}
{mov, mov, pck, tstg, mov}
-->
fragColor.x = (t.x * inversesqrt(t.x)) > 0.5 ? 0.5 : 1.0; // 2 cycles
{frsq}
{fmul, pck, tstg, mov}
  1. 정규화의 부정 최적화:
cpp
fragColor.xyz = normalize(-t.xyz); // 7 cycles
{mov, mov, mov}
{fmul, mov}
{fmad, mov}
{fmad, mov}
{frsq}
{fmul, fmul, mov, mov}
{fmul, mov}
-->
fragColor.xyz = -normalize(t.xyz); // 6 cycles
{fmul, mov}
{fmad, mov}
{fmad, mov}
{frsq}
{fmul, fmul, mov, mov}
{fmul, mov}
  1. 복근, 도트, 네거티브, 클램프, 채도 등의 최적화:
cpp
// abs
fragColor.x = abs(t.x * t.y); // 2 cycles
{sop, sop}
{mov, mov, mov}
-->
fragColor.x = abs(t.x) * abs(t.y); // 1 cycle
{sop, sop}

// dot
fragColor.x = -dot(t.xyz, t.yzx); // 3 cycles
{sop, sop, sopmov}
{sop, sop}
{mov, mov, mov}
-->
fragColor.x = dot(-t.xyz, t.yzx); // 2 cycles
{sop, sop, sopmov}
{sop, sop}

// clamp
fragColor.x = 1.0 - clamp(t.x, 0.0, 1.0); // 2 cycles
{sop, sop, sopmov}
{sop, sop}
-->
fragColor.x = clamp(1.0 - t.x, 0.0, 1.0); // 1 cycle
{sop, sop}

// min / clamp
fragColor.x = min(dot(t, t), 1.0) > 0.5 ? t.x : t.y; // 5 cycles
{sop, sop, sopmov}
{sop, sop}
{mov, fmad, tstg, mov}
{mov, mov, pck, tstg, mov}
{mov, mov, tstz, mov}
-->
fragColor.x = clamp(dot(t, t), 0.0, 1.0) > 0.5 ? t.x : t.y; // 4 cycles
{sop, sop, sopmov}
{sop, sop}
{fmad, mov, pck, tstg, mov}
{mov, mov, tstz, mov}
  1. 경험치, 로그, 힘:
cpp
// exp2
fragColor.x = exp2(t.x); // one cycle
{fexp}

// exp
float exp( float x )
{
    return exp2(x * 1.442695); // 2 cycles
    {sop, sop}
    {fexp}
}

// log2
fragColor.x = log2(t.x); // 1 cycle
{flog}

// log
float log( float x )
{
    return log2(x * 0.693147); // 2 cycles
    {sop, sop}
    {flog}
}

// pow
float pow( float x, float y )
{
    return exp2(log2(x) * y); // 3 cycles
    {flog}
    {sop, sop}
    {fexp}
}

실행 효율성은 높음에서 낮음으로: exp2 = log2 > exp = log > pow.

  1. 신, 코스, 신, 코시:
cpp
// sin
fragColor.x = sin(t.x); // 4 cycles
{fred}
{fred}
{fsinc}
{fmul, mov} // plus conditional

// cos
fragColor.x = cos(t.x); // 4 cycles
{fred}
{fred}
{fsinc}
{fmul, mov} // plus conditional

// cosh
fragColor.x = cosh(t.x); // 3 cycles
{fmul, fmul, mov, mov}
{fexp}
{sop, sop}

// sinh
fragColor.x = sinh(t.x); // 3 cycles
{fmul, fmul, mov, mov}
{fexp}
{sop, sop}

높은 수준에서 낮은 수준의 실행 효율성: sinh = cosh > sin = cos.

  1. Asin, Acos, Atan, 도 및 라디안:
cpp
fragColor.x = asin(t.x); // 67 cycles
fragColor.x = acos(t.x); // 79 cycles
fragColor.x = atan(t.x); // 12 cycles (판정/확인 조건 )

fragColor.x = degrees(t.x); // 1 cycle
{sop, sop}

fragColor.x = radians(t.x); // 1 cycle
{sop, sop}

위에서 볼 수 있듯이 acos와 asin은 최대 79클럭 사이클까지 극도로 비효율적입니다. 그 다음에는 atan, 12시간 주기; 가장 빠른 것은 도와 라디안, 1 클럭 사이클입니다.

  1. 벡터와 행렬:
cpp
fragColor = t * m1; // 4x4 matrix, 8 cycles
{mov}
{wdf}
{sop, sop, sopmov}
{sop, sop, sopmov}
{sop, sop}
{sop, sop, sopmov}
{sop, sop, sopmov}
{sop, sop}

fragColor.xyz = t.xyz * m2; // 3x3 matrix, 4 cycles
{sop, sop, sopmov}
{sop, sop}
{sop, sop, sopmov}
{sop, sop}

벡터와 행렬은 차원이 적을수록 더 효율적이므로 차원을 최대한 줄이도록 노력하세요.

  1. 스칼라 및 벡터 연산:
cpp
fragColor.x = length(t-v); // 7 cycles
fragColor.y = distance(v, t);
{sopmad, sopmad, sopmad, sopmad}
{sop, sop, sopmov}
{sopmad, sopmad, sopmad, sopmad}
{sop, sop, sopmov}
{sop, sop}
{frsq}
{frcp}
-->
fragColor.x = length(t-v); // 9 cycles
fragColor.y = distance(t, v);
{mov}
{wdf}
{sopmad, sopmad, sopmad, sopmad}
{sop, sop, sopmov}
{sop, sop, sopmov}
{sop, sop}
{frsq}
{frcp}
{mov}

fragColor.xyz = normalize(t.xyz); // 6 cycles
{fmul, mov}
{fmad, mov}
{fmad, mov}
{frsq}
{fmul, fmul, mov, mov}
{fmul, mov}
-->
fragColor.xyz = inversesqrt( dot(t.xyz, t.xyz) ) * t.xyz; // 5 cycles
{sop, sop, sopmov}
{sop, sop}
{frsq}
{sop, sop}
{sop, sop}

fragColor.xyz = 50.0 * normalize(t.xyz); // 7 cycles
{fmul, mov}
{fmad, mov}
{fmad, mov}
{frsq}
{fmul, fmul, mov, mov}
{fmul, fmul, mov, mov}
{sop, sop}
-->
fragColor.xyz = (50.0 * inversesqrt( dot(t.xyz, t.xyz) )) * t.xyz; // 6 cycles
{sop, sop, sopmov}
{sop, sop}
{frsq}
{sop, sop, sopmov}
{sop, sop}
{sop, sop}

다음은 GLSL 내장 함수 중 일부를 확장한 형태입니다:

cpp
vec3 cross( vec3 a, vec3 b )
{
    return vec3( a.y * b.z - b.y * a.z,
                 a.z * b.x - b.z * a.x,
                 a.x * b.y - b.y * a.y );
}

float distance( vec3 a, vec3 b )
{
    vec3 tmp = a – b;
    return sqrt( dot(tmp, tmp) );
}

float dot( vec3 a, vec3 b )
{
    return a.x * b.x + a.y * b.y + a.z * b.z;
}

vec3 faceforward( vec3 n, vec3 I, vec3 Nref )
{
    if( dot( Nref, I ) < 0 ) 
    { 
      return n;
    }
    else
    {
      returnn:
    }
}

float length( vec3 v )
{
    return sqrt( dot(v, v) );
}

vec3 normalize( vec3 v )
{
    return v / sqrt( dot(v, v) );
}

vec3 reflect( vec3 N, vec3 I )
{
    return I - 2.0 * dot(N, I) * N;
}

vec3 refract( vec3 n, vec3 I, float eta )
{
    float k = 1.0 - eta * eta * (1.0 - dot(N, I) * dot(N, I));
    if (k < 0.0)
        return 0.0; 
    else
        return eta * I - (eta * dot(N, I) + sqrt(k)) * N;
}
  1. 그룹화 작업.

스칼라와 벡터를 한 번에 그룹화하면 효율성이 향상됩니다.

cpp
fragColor.xyz = t.xyz * t.x * t.y * t.wzx * t.z * t.w; // 7 cycles
{sop, sop, sopmov}
{sop, sop, sopmov}
{sop, sop}
{sop, sop, sopmov}
{sop, sop}
{sop, sop, sopmov}
{sop, sop}
-->
fragColor.xyz = (t.x * t.y * t.z * t.w) * (t.xyz * t.wzx); // 4 cycles
{sop, sop, sopmov}
{sop, sop, sopmov}
{sop, sop}
{sop, sop}

위의 조립 지침에서는 PowerVR GPU를 예로 들어 설명합니다. 다른 것들은 유사하지만 정확히 동일하지 않을 수 있으며 특정 플랫폼에 따라 최적화해야 합니다.

12.6.4 포괄적인 최적화

12.6.4.1 조명 및 그림자 최적화

포워드 렌더링은 동적 광원이 거의 없는 단순한 장면에 적합합니다.

전통적인 디퍼드 렌더링은 동적 광원이 많은 장면(특히 소규모 로컬 광원)에 적합합니다. 그러나 타일 내 버퍼의 대역폭 비트 수로 인해 너무 많은 기하학적 표면 정보를 저장할 수 없습니다.

컴퓨트 셰이더를 기반으로 하는 조명 기술(예: 타일 지연, 클러스터 지연, 전달+)은 MRT 데이터가 타일의 버퍼를 초과하여 엄청난 액세스 주기와 지연을 초래할 가능성이 높기 때문에 전역 메모리에 데이터를 기록합니다. 모바일 사용에는 권장되지 않습니다.

섀도잉 기술은 많지만 TBR 하드웨어 아키텍처에 가장 적합한 섀도잉 기술은 스텐실 섀도잉입니다. TBR의 GPU 하드웨어는 스텐실 버퍼 처리에 매우 뛰어나기 때문에 데이터는 타일 메모리에 저장되며 시스템 메모리에 쓸 필요가 없습니다. 하드 셰이딩이 허용되는 경우 스텐실 셰이딩 알고리즘을 우선적으로 사용해야 합니다.

결과를 칩 외부 메모리(예: 섀도우 맵)에 기록해야 하는 기술은 일반적으로 타일 온칩 메모리에서 완전히 계산되는 기술보다 성능이 떨어집니다.

SSAO 기술을 사용하려면 깊이 버퍼에 대한 빈번한 무작위 High-Span 액세스를 방지하기 위해 HZB(계층적 Z-버퍼)를 사용하여 가속하는 것이 가장 좋습니다.

SSR 기술을 사용해야 하는 경우 장면 색상 프레임 버퍼를 미리 다운샘플링하세요(OpenGL ES 인터페이스 glFramebufferTexture2DDownsampleIMG 사용).

기존 블록 및 클러스터 조명 대신 템플릿 클리핑 조명 알고리즘을 사용하는 데 우선순위를 둡니다.

모바일 측의 조명에 대해 특별한 최적화가 이루어졌습니다. 예를 들어, 필라멘트는 조명의 가시성 기능을 단순화합니다.

단순화된 가시성 공식은 다음과 같습니다.

해당 구현 코드:

cpp
float V_SmithGGXCorrelated_Fast(float roughness, float NoV, float NoL) 
{
    // Hammon 2017, "PBR Diffuse Lighting for GGX+Smith Microsurfaces"
    return 0.5 / mix(2.0 * NoL * NoV, NoL + NoV, roughness);
}

IBL 조명 부분의 경우 Filament는 Diffuse Map을 포기하고 Specular Map(거칠기가 1일 때 Mimap 수준)을 직접 채택하여 다음을 시뮬레이션했습니다.

그러나 Specular Map에는 레벨이 5개만 있고 가장 작은 크기는 16x16입니다.

Mimap 수준은 다음과 같이 거칠기에 매핑됩니다.

밉맵 수준 러프니스

0 0.000

1 0.018

2 0.086

3 0.250

4 1.000

IBL 텍스처를 저장할 때 RGBM 형식은 사용되지 않습니다(품질이 표준에 미치지 못하기 때문). 대신 R11G11B10F 형식을 사용하고 RGBA8888로 재구성하여 PNG 형식으로 저장합니다.

금속 물체의 경우 기존 PBR 조명의 에너지 비보존 문제를 해결하기 위해 Filament는 Lagarde & Golubev의 솔루션을 채택했습니다.

기존 PBR은 금속 재료를 계산할 때 에너지 비보존 문제가 있습니다. 윗줄은 비보존 전설이다. 표시가 어두울수록 더 많은 에너지가 손실됩니다. 아래쪽 줄은 촬영이 복구된 후의 범례입니다. (명확하게 보려면 각별히 주의해야 합니다.)

필라멘트는 금속 조명의 비보수적 BRDF 공식을 수정합니다.

구현된 코드는 다음과 같습니다.

cpp
// 의 IBL계산/산출(Calculate)코드
const float V = Visibility(…) * NoL * (VoH / NoH);
const float F = pow5(1.0f - VoH);
r.x += V * (1.0f - F);
r.y += V * F;

// Filament보정/수정(Fix)의 코드
const float V = Visibility(…) * NoL * (VoH / NoH);
const float F = pow5(1.0f - VoH);
r.x += V * F;
r.y += V;

AO 측면에서 Filament는 다중 바운스 효과를 시뮬레이션합니다.

필라멘트의 멀티 바운스 AO 효과 비교 차트. 상단: 다중 바운스 꺼짐; 하단: 다중 바운스 켜짐. 눈과 귀가 조금 더 밝아졌습니다.

AO 다중 바운스에 대한 시뮬레이션 코드는 다음과 같습니다.

cpp
vec3 gtaoMultiBounce(float visibility, const vec3 albedo) 
{
 // Jimenez et al. 2016,
 // “Practical Realtime Strategies for Accurate Indirect Occlusion"
 vec3 a = 2.0404 * albedo - 0.3324;
 vec3 b = -4.7951 * albedo + 0.6417;
 vec3 c = 2.7552 * albedo + 0.6903;
 return max(vec3(visibility), ((visibility * a + b) * visibility + c) * visibility);
}

diffuseLobe *= gtaoMultiBounce(ao, diffuseColor);

그림자에 대한 최적화 기술도 많이 있습니다. 예를 들어, 다음 그림은 뷰 프러스텀 경계 상자 대신 객체 경계 상자를 계산하여 그림자 맵의 크기를 줄이는 방법을 보여주는 샘플 분포 그림자 맵(SDSM, Sample Distribution Shadow Map) 기술입니다.

SDSM은 또한 HZB를 구성하고 이전 프레임의 HZB를 사용하여 GPU 지연을 방지하고 CS를 사용하여 캐스케이드 서브 섀도우 맵의 거리를 생성합니다. 생성된 HZB는 계단식 하위 그림자 맵을 빠르게 자르는 데 사용할 수 있습니다. 이러한 최적화 조치를 통해 SDSM은 각 캐스케이드의 프리미티브 수의 균형을 맞추고, 섀도우 맵 해상도와 출력 해상도의 균형을 맞추고, 더 작은 해상도로 비 SDSM 방법과 유사한 섀도우 효과를 얻을 수 있습니다. 다음은 SDSM과 비SDSM의 효과를 비교한 차트입니다.

상단: 일반 CSM 섀도우; 하단: SDSM 섀도우.

성능 측면에서도 SDSM은 다음을 능가합니다.

저사양 장치를 사용하는 경우 섀도우 맵 대신 얼룩 섀도우를 사용해 볼 수 있습니다.

왼쪽: 그림자 맵; 오른쪽: 얼룩 그림자.

고급 조명 측면에서는 Forward+ 및 Light Prepass와 같은 조명 렌더링 기술을 사용해 볼 수 있습니다. 다음은 모바일 GPU의 다양한 조명 기술 비교 차트입니다.

또한 MatCap 기술을 사용하여 성능과 효과의 적절한 균형을 얻을 수 있는 IBL 효과를 얻을 수 있습니다.

MatCap 기술을 사용하여 얻은 렌더링 효과.

12.6.4.2 포스트 프로세스 최적화

사후 처리 효과는 더 많은 대역폭을 차지하므로 필요하지 않은 경우 모든 사후 처리 효과를 끄는 것이 좋습니다.

후처리가 정말로 필요한 경우 일반적인 최적화 방법은 다음과 같습니다.

  1. 여러 포스트 프로세싱 효과를 하나의 셰이더로 결합하여 완성합니다.

  2. 해상도를 줄이고 포스트 프로세스 파이프라인를 계산합니다.

  3. Tile 내에서 사후 처리 데이터에 계속 액세스하도록 하세요.

  4. 주변 픽셀 데이터에 접근하지 마세요. 필요한 경우 캐시 적중률을 높이기 위해 지역성과 적시성을 유지하려고 노력하세요.

  5. 전용 알고리즘 최적화. 예를 들어 가우시안 블러는 수평 블러 + 수직 블러(분리된 컨볼루션 커널)로 분할됩니다. Filament는 모바일 장치에 최적화된 톤매핑을 제공합니다.

cpp
// 의 ACES톤매핑(Tonemapping)。
vec3 Tonemap_ACES(const vec3 x)
{
    // Narkowicz 2015, "ACES Filmic Tone Mapping Curve”
    const float a = 2.51;
    const float b = 0.03;
    const float c = 2.43;
    const float d = 0.59;
    const float e = 0.14;
    return (x * (a * x + b)) / (x * (c * x + d) + e);
}

// 모바일 플랫폼(Mobile)의 톤매핑(Tonemapping)
vec3 Tonemap_Mobile(const vec3 x) 
{
    // Transfer function baked in,
    // don’t use with sRGB OETF!
    return x / (x + 0.155) * 1.019;
}

단순화된 톤매핑 곡선은 매우 유사합니다.

Arm은 다양한 품질 수준에서 일반적으로 사용되는 포스트 프로세스 파이프라인에 대한 기술 참조를 제공합니다.

12.6.4.3 스프라이트 렌더링 최적화

스프라이트 렌더링을 최적화하는 일반적인 방법에는 스프라이트 수 제어, 화면의 스프라이트 영역 제어, 빈 영역 줄이기 등이 있습니다.

최신 GPU는 프리미티브 수의 증가에 그다지 민감하지 않지만 Alpha Blend가 켜진 상태에서 스프라이트의 빈 영역이 증가하는 데는 더 민감합니다. 비효율적인 프래그먼트 셰이딩 처리가 많이 낭비되기 때문입니다. 공백 낭비를 방지하는 효과적인 방법은 스프라이트 그리기의 기하학적 복잡성을 높이는 것입니다. 투명성의 낭비를 줄이기 위해 기하학적 복잡성을 증가시킴으로써 성능이 크게 향상될 수 있습니다.

과거에는 엘프를 사용하여 입자 효과를 시뮬레이션할 때 사각형으로 원을 그렸습니다. 이때 사각형은 공백으로 둘러싸여 있었고, 낭비율은 무려 22%에 달했다. 4면 폴리곤을 12면 폴리곤으로 늘리면 낭비되는 조각 처리량을 3%로 줄일 수 있습니다. 그들의 공식과 결과는 다음과 같이 비교됩니다.

4면체와 12면체의 낭비조각 처리 비율 비교. 그림의 왼쪽은 4면 다각형으로 폐기물 비율이 21.4%입니다. 그림의 오른쪽은 12면의 다각형으로 폐기물 비율이 2.9%입니다.

8면 다각형을 사용하여 둥근 반투명 개체를 그리는 범례입니다.

또한 불투명 및 반투명 개체(예: UI 요소)를 별도의 그리기 제출로 분할할 수 있습니다. 그림 제출 권장 순서는 다음과 같습니다.

  1. 불투명한 장면 스프라이트 요소.

  2. 반투명 장면 스프라이트 요소.

  3. 반투명 UI 요소.

입자 효과의 경우 멀리 있는 입자의 선명도는 중요하지 않으며 가장 간단한 텍스처 필터링 방법(예: 가장 가까운 지점)을 사용할 수 있습니다.

12.6.4.4 GPU 작업 부하 균형 조정

GPU 집약적인 애플리케이션의 경우 GPU 측에서 병목 현상이 발생하는 경우가 많으며, 그 이유는 GPU의 다양한 구성 요소의 작업 부하가 불균형하여 성능 병목 현상이 발생하기 때문입니다.

GPU의 다양한 구성 요소의 작업 부하를 적절하게 할당함으로써 병목 현상을 효과적으로 제거하고 GPU를 최대한 활용하며 렌더링 성능을 향상시킬 수 있습니다. 워크로드를 차별화하는 GPU 리소스는 다음과 같습니다.

  1. ALU(논리연산장치)

  2. 텍스처링 로드

  3. ISP 로드(이미지 통합 프로세서 로딩)

  4. 렌더러 활성(렌더러 활동)

  5. 타일러 액티브

GPU 제조업체에서 제공하는 프로파일러(예: Snapdragon Profiler 및 PVRMonitor)를 통해 역학을 효과적으로 모니터링할 수 있습니다. 다음은 균형 잡힌 워크로드에 대한 최적화된 설명입니다.

  1. 사전 계산을 사용하고 결과를 LUT(룩업 테이블)에 저장하면 ALU의 작업을 Texturing Load로 전송할 수 있습니다.

  2. 텍스처 수집 대신 프로그램 텍스처 기능을 사용하여 텍스처링 로드 작업을 ALU로 전송합니다.

  3. 깊이 및 스텐실 테스트를 사용하여 셰이더 호출을 줄여 텍스처 로드 또는 ALU 작업을 줄입니다.

  4. ALU 기반 알파 테스트를 사용하여 깊이 프리패스와 깊이 테스트를 교환할 수 있습니다. 이는 드로우 콜과 지오메트리 오버헤드를 증가시키지만 ALU 작업량과 레지스터 압력을 크게 줄일 수 있습니다.

  5. LOD 전환 효과를 달성하기 위한 알파 테스트와 노이즈 기능의 조합은 ALU 작업 부하를 크게 증가시킵니다. 이 경우 ALU 워크로드를 ISP로 전송하기 위해 템플릿 프리패스를 수행할 수 있습니다.

  6. 보다 복잡한 셰이더, 고해상도 텍스처를 제공하고 다각형 수를 늘려 충실도가 높은 이미지 품질을 향상시킬 수 있습니다. 렌더링이 병목 현상에 도달하면 조각 셰이더의 복잡성을 늘리는 것보다 다각형 수를 늘리는 것이 좋습니다.

12.6.4.5 컴퓨트 셰이더 최적화

CS(컴퓨트 셰이더) 이전에는 OpenGL ES에서 당황스러운 병렬 컴퓨팅을 노출하는 여러 가지 방법이 있었습니다.

  1. 쿼드를 래스터라이제이션하고 픽셀 셰이더에서 임의 계산을 수행한 다음 결과를 텍스처에 씁니다.

  2. 변환 피드백을 사용하여 버텍스 셰이더에서 임의의 계산을 수행합니다.

이러한 방법에는 많은 제한이 있습니다. 예를 들어 셰이더는 다른 셰이더를 감지할 수 없으며 데이터 쓰기 대상이 제한됩니다(VS는 gl_Position 및 변수 레지스터에만 쓸 수 있고 PS는 지정된 RenderTarget에만 쓸 수 있습니다).

컴퓨트 셰이더에는 위의 제한 사항이 없습니다. 모든 입력 및 출력 데이터 소스를 지정할 수 있으며 기존 렌더링 파이프라인을 실행할 필요가 없습니다. 맞춤형 계산을 편리하고 효율적이며 유연하게 실행할 수 있습니다.

각 컴퓨트 셰이더가 작업을 디스패치할 때 작업 그룹 수와 각 작업 그룹의 스레드 수를 지정할 수 있습니다.

상단: 매번 Compute Shader를 파견하는 Work Group의 개략도; 하단: 각 작업 그룹에는 여러 스레드가 있으며 이러한 스레드에는 공유 메모리가 있습니다.

Compute Shader 작업을 위한 의사코드는 다음과 같습니다:

cpp
for (int w = 0; w < NUM_WORK_GROUPS; w++)
{
    // 병렬 。
    parallel_for (int i = 0; i < THREADS_IN_WORK_GROUP; i++)
    {
        execute_compute_thread(w, i);
    }
}

작업 그룹 크기에 대한 권장 사항은 다음과 같습니다.

  1. 작업 그룹의 기본 크기로 64를 사용합니다. 작업 그룹당 64개 이상의 스레드를 사용하지 마십시오.

  2. 작업 그룹 크기로 4의 배수를 사용합니다.

  3. 특히 장벽이나 공유 메모리를 사용하는 경우 큰 작업 그룹보다 작은 작업 그룹 크기를 시도하십시오.

  4. 이미지나 텍스처를 처리할 때 정사각형 실행 크기(예: 8x8)를 사용하여 최적의 2D 캐시 집약성을 활용하세요.

  5. 작업 그룹이 각 작업 그룹의 작업을 완료해야 하는 경우 작업을 두 개의 채널로 분할하는 것을 고려하십시오. 이렇게 하면 커널의 대부분 스레드에 대한 장벽과 유휴 간격이 방지됩니다. 소규모 작업 그룹의 장벽으로 인해 성능 비용도 발생합니다.

  6. Compute Shader의 성능은 항상 직관적이지 않으므로 성능을 지속적으로 측정해야 합니다.

공유 메모리의 권장 사항은 다음과 같습니다.

  1. 공유 메모리를 사용하여 작업 그룹의 스레드 간에 중요하거나 복잡한 계산을 공유합니다.

  2. 공유 메모리를 가능한 한 작게 유지하십시오. 이렇게 하면 데이터 캐시의 급격한 변화를 줄일 수 있습니다.

  3. 정밀도와 데이터 폭을 줄여 필요한 공유 메모리 크기를 줄입니다.

  4. 공유 데이터에 동기적으로 접근하려면 장벽을 설정해야 합니다. 데스크톱 개발에서 포팅된 셰이더 코드는 GPU 관련 가정으로 인해 일부 장벽을 무시하는 경우가 있습니다. 그러나 이 가정은 모바일 GPU에서 사용하기에 안전하지 않습니다.

  5. 장벽을 삽입하는 것과 비교할 때 알고리즘을 여러 셰이더로 분할하는 것이 계산상 더 효율적입니다.

  6. 장애물의 경우 소규모 작업 그룹이 더 적은 양을 소비합니다.

  7. 전역 메모리에서 공유 메모리로 데이터를 복사하지 마십시오. 이렇게 하면 캐시 적중률이 감소합니다.

  8. 코드 구현에 공유 메모리를 사용하지 마십시오. 예를 들어:

cpp
if (localInvocationID == 0) 
{
    common_setup();
}

barrier();
(.....) // 스레드(Thread)의 shader로직
barrier();

if (localInvocationID == 0) 
{
    result_reduction();
}

위 코드에서 common_setup 및 result_reduction에는 하나의 스레드만 필요하며 작업 그룹의 다른 스레드는 대기하므로 Stall 및 유휴 현상이 발생합니다.

common_setup 및 result_reduction에는 더 적은 스레드가 필요하므로 위 코드를 세 개의 셰이더로 분할하는 것이 더 좋습니다.

그런데, 당황스러운 점은 TAA, SSGI 등 UE의 CS 코드에 이러한 병합 코드가 다수 사용된다는 점이다.

이미지(또는 텍스처) 처리에 대한 제안은 다음과 같습니다.

  1. 가변 보간을 사용할 때 텍스처 좌표는 고정 기능 하드웨어를 사용하여 보간됩니다. 결과적으로 더 유용한 워크로드를 위해 셰이더 주기를 확보합니다.

  2. Tile-Writeback 하드웨어 및 셰이더 코드를 사용하여 메모리에 쓰기를 병렬로 수행할 수 있습니다.

  3. imageStore() 좌표의 범위를 확인할 필요가 없습니다. 프레임을 완전히 세분화하지 않은 작업그룹을 사용하면 문제가 발생할 수 있습니다.

  4. 프레임 버퍼 압축 및 트랜잭션 제거(Mali GPU만 해당)를 수행할 수 있습니다.

이미지 처리에 컴퓨팅을 사용하면 다음과 같은 이점이 있습니다.

  1. 일부 알고리즘의 추가 전송을 피하기 위해 인접한 픽셀 간의 공유 데이터 세트를 사용할 수 있습니다.

  2. 각 스레드에서 더 큰 작업 세트를 사용하는 것이 더 쉽기 때문에 일부 알고리즘에 대한 추가 전송을 피할 수 있습니다.

  3. 여러 조각 렌더링 패스가 필요한 FFT(고속 푸리에 변환)와 같은 복잡한 알고리즘은 일반적으로 단일 계산 디스패치(디스패치)로 결합될 수 있습니다.

12.6.4.6 다중 코어 병렬

멀티코어는 현재 모바일 SoC에서 주류 CPU의 표준 구성이 되었습니다(대부분의 CPU는 8코어 이상에 도달했습니다). 렌더링 효과를 향상시키기 위해 멀티 코어의 병렬 기능을 사용하는 방법은 매우 크고 어려운 작업입니다.

첫째, 병렬 효율성을 향상시키기 위해 명령 버퍼의 멀티 코어 생성 및 실행을 허용하는 최신 그래픽 API(DirectX12, Vulkan, Metal)의 기능을 최대한 활용하십시오.

Vulkan 그래픽 API는 명령 버퍼 다이어그램을 병렬로 생성합니다.

Filament의 운영 체제 병렬 렌더링 다이어그램은 다음과 같습니다.

필라멘트 운영 체제의 범례를 단순화했습니다. 각 블록은 자체적으로 N개의 작업을 생성할 수 있는 새로운 상위 작업을 나타냅니다. 이 시스템의 모든 루프는 다중 스레드 및 작업 기반입니다.

UE는 병렬 렌더링을 위해 TaskGraph를 사용합니다. 이 분야에 대한 더 많은 기술은 언리얼 렌더링 시스템 분석(02) - 멀티스레드 렌더링을 참조하세요.

12.6.4.7 기타 포괄적인 최적화

  • 시스템 통합 최적화

대부분의 모바일 플랫폼은 화면 찢김을 방지하기 위해 수직 동기화 신호를 사용하여 버퍼 스왑을 표시합니다. GPU가 수직 동기화 주기보다 느리게 렌더링되는 경우 버퍼가 두 개만 포함된 스왑 체인으로 인해 GPU가 쉽게 정지될 수 있습니다. 스왑 체인 최적화를 위한 권장 사항:

  1. 애플리케이션이 항상 vsync보다 느리게 실행되는 경우 스왑 체인에서 두 표면을 사용하지 마십시오.

  2. 애플리케이션이 항상 vsync보다 빠르게 실행되는 경우 스왑 체인의 두 표면을 사용하면 메모리 소비를 줄일 수 있습니다.

  3. 애플리케이션이 때때로 vsync보다 느리게 실행되는 경우 스왑 체인에서 3개의 표면을 사용하면 애플리케이션에 최상의 성능을 제공할 수 있습니다.

  • MRT를 효율적으로 이용

**MRT(Multiple Render Target)**는 모바일 단말기에서 일반적으로 지원됩니다. 일반적인 사용 사례는 렌더링 지연입니다. 기하학 통과 단계에서는 표면의 기하학적 정보(기본 색상, 노멀, 깊이, 재질)를 저장하기 위해 MRT가 필요합니다.

TBDR은 타일 버퍼(PLS, Subpass 등)를 사용하여 MRT 데이터를 캐시에 유지함으로써 메모리 데이터 액세스 속도를 향상시키고 대기 시간을 줄일 수 있습니다.

대부분의 모바일 GPU에서 제대로 작동하기 위해 MRT의 픽셀당 데이터 크기는 128비트(16바이트) + 깊이 스텐실 버퍼 내에서 제어됩니다. 일부 최신 GPU에서는 256비트(32바이트) + 깊이 스텐실 버퍼로 늘릴 수 있습니다. 한도를 초과하고 타일의 버퍼가 부족한 경우 GPU는 데이터를 글로벌 메모리에 강제로 저장하여 데이터 작업 속도를 크게 저하시킵니다.

메모리 트랜잭션 및 성능 고려 사항 외에도 렌더 대상이 시스템 메모리에서 오버플로되면 모든 렌더 대상 형식이 시스템 메모리 버스에서 최대 속도로 지원되지는 않습니다. 따라서 GPU에서 사용 가능한 포맷 및 TPU(텍스처 처리 장치)에 따라 전송 속도가 더욱 줄어들 수 있습니다. PowerVR GPU의 경우 다음 형식과 속도는 다음과 같이 관련됩니다.

  1. RGBA8은 최고 속도로 읽을 수 있습니다.

  2. RGB10A2는 최대 속도에 가깝게 읽을 수 있습니다.

  3. RG11B10은 절반 속도로만 읽을 수 있습니다.

  4. RGBA16F는 절반 속도로만 읽을 수 있습니다.

  5. RGBA32F는 1/4 최고 속도로만 읽을 수 있습니다(이중선형 필터링 없음).

  • 적절한 HDR 픽셀 형식을 선택하세요

HDR의 경우 선택할 수 있는 여러 형식이 있습니다. 고려해야 할 요소에는 메모리 대역폭, 정확도(품질), 알파 지원 등이 포함됩니다. 하드웨어 자체에서 지원하는 HDR 텍스처 형식의 경우 RGB10A2 또는 RGBA16F를 사용할 수 있지만 이로 인해 대역폭이 증가합니다. 이러한 텍스처는 품질, 성능(필터링) 및 메모리 대역폭 사용량 간의 적절한 균형을 제공합니다.

RGBM 및 RGBdiv8 텍스처 형식 모두 개발자가 셰이더에서 인코딩 및 디코딩 기능을 구현해야 하며, 하드웨어에서 지원되지 않기 때문에 추가 USC 주기가 필요합니다. 응용 프로그램이 USC로 제한되는 경우 이러한 형식을 사용하면 안 됩니다. 이들의 장점은 RGBA8과 동일한 대역폭 비용으로 메모리 대역폭이 매우 낮다는 것입니다. 애플리케이션이 메모리 대역폭으로 제한되는 경우 이러한 형식을 연구하는 것이 유용할 수 있습니다. RGBM과 HDR Color 간의 인코딩 및 디코딩 코드는 다음과 같습니다.

hlsl
// HDR컬러 RGBM.
float4 RGBMEncode( float3 color ) 
{
    float4 rgbm;
    color *= 1.0 / 6.0;
    // HDR컬러 의 계수까지 Alpha채널/패스(Pass) 내에서 .
    rgbm.a = saturate( max( max( color.r, color.g ), max( color.b, 1e-6 ) ) );
    rgbm.a = ceil( rgbm.a * 255.0 ) / 255.0;
    rgbm.rgb = color / rgbm.a;
    return rgbm;
}

// RGBMHDR컬러 .
float3 RGBMDecode( float4 rgbm ) 
{
    return 6.0 * rgbm.rgb * rgbm.a;
}

일반적인 HDR 형식은 다음 표에 자세히 설명되어 있습니다.

텍스처 형식 대역폭 소비 USC 소비 필터 정확도 알파

RGB10A2 1xRGBA8 없음 하드웨어 가속, RGBA8보다 약간 느림 알파 정밀도를 희생하면서 RGB 채널의 정밀도가 높아졌습니다. 네 가지 값만

RGBA16F 2xRGBA8 없음 하드웨어 가속, 0.5x RGBA8 RGBA8의 정확도보다 훨씬 높음 지원하다

RG11B10F 2xRGBA8 없음 하드웨어 가속, 0.5x RGBA8 RGBA16F와 동일 지원되지 않음

RGBA32F 4xRGBA8 없음 하드웨어 가속, 0.25x RGBA8, 가장 가까운 지점만 지원 RGBA16F의 정확도보다 훨씬 높음 지원하다

RGBM(RGBA8) 1xRGBA8 데이터 인코딩/디코딩 하드웨어는 이 형식 필터링을 지원하지 않습니다. RGB 값 범위가 RGBA8보다 높습니다. 지원되지 않음

RGBdiv8(RGBA8) 1xRGBA8 RGBM보다 약간 더 복잡함 하드웨어는 이 형식 필터링을 지원하지 않습니다. 위와 동일 지원되지 않음

FP16 또는 FP32 대신 압축된 32비트 형식(RGB10_A2, RGB9_E5)에 우선순위를 부여할 수 있습니다.

  • 올바른 앤티앨리어싱 선택

MSAA는 순방향 렌더링에 적합하고 TAA는 디퍼드 렌더링에 적합합니다. 또한 형태학적 분석 안티앨리어싱 기술은 다양하며 효율성은 높은 것부터 낮은 것 순으로 FXAA, CMAA, MLAA, SMAA입니다.

따라서 프로젝트 상황에 따라 적절한 앤티앨리어싱을 선택하고, 이미지 품질이 높음, 중간, 낮음에 따라 다양한 앤티앨리어싱 기술을 선택할 수도 있습니다.

MSAA를 사용하는 경우 효과와 효율성 사이의 균형을 더 잘 이루려면 4x가 선호됩니다. Tile 내에서 MSAA 구문 분석을 사용하고 glBlitFramebuffer()와 같은 인터페이스를 사용하여 명시적인 구문 분석을 피하세요.

앤티앨리어싱을 켜고 끄면서 성능을 모니터링, 분석 및 비교합니다.

필라멘트는 하이라이트에 대해 특별한 앤티앨리어싱 필터링 알고리즘(러프니스 수정)을 수행합니다.

cpp
float normalFiltering(float perceptualRoughness, const vec3 worldNormal) 
{
 // Kaplanyan 2016, "Stable specular highlights"
 // Tokuyoshi 2017, "Error Reduction and Simplification for Shading Anti-Aliasing"
 // Tokuyoshi and Kaplanyan 2019, "Improved Geometric Specular Antialiasing"
 vec3 du = dFdx(worldNormal);
 vec3 dv = dFdy(worldNormal);
 float variance = specularAntiAliasingVariance * (dot(du, du) + dot(dv, dv));
 float roughness = perceptualRoughnessToRoughness(perceptualRoughness);
 float kernelRoughness = min(2.0 * variance, specularAntiAliasingThreshold);
 float squareRoughness = saturate(roughness * roughness + kernelRoughness);
 return roughnessToPerceptualRoughness(sqrt(squareRoughness));
}
materialRoughness = normalFiltering(materialRoughness, getWorldGeometricNormalVector());
  • 최대한 미리 계산을 추가

처리할 인스턴스가 더 적은 파이프라인에서 계산을 더 앞쪽으로 이동하면 총 계산 수를 줄일 수 있습니다. 계산 체인은 다음과 같습니다.

효율성이 높은 것부터 낮은 것 순으로 사전 계산, CPU 애플리케이션 계층 계산, 버텍스 셰이더, 픽셀 셰이더입니다.

조명 계산을 예로 들면, 렌더링 효율성은 라이트 맵, IBL, 정점별 조명, 픽셀별 조명 등 높은 것부터 낮은 것까지입니다.

그러나 효율성이 높다는 것은 제어 가능성이 낮다는 것을 의미하므로 효율성과 효율성 사이의 균형을 맞춰야 합니다.

  • 기타 최적화

CPU 또는 GPU 언더클럭으로 인한 성능 저하를 방지하려면 전력 소비 및 장치 온도에 주의하십시오.

해상도나 프레임 속도를 다운그레이드하거나 일부 전략에 따라 동적으로 조정하는 것을 고려해보세요.

IO 부하가 높은 데이터를 미리 로드하고 캐시합니다. 가능할 때마다 비용이 많이 드는 작업을 미리 계산하십시오.

UI 인터페이스 뒤에 개체를 숨깁니다. (아래 사진)

품질 수준을 나누고 매개변수 사양을 공식화하며 수준에 따라 다양한 소비 기술을 선택합니다.

동적 그리드 배치를 고려하십시오(UE에는 이 기능이 없으며 직접 구현해야 합니다).

감소된 해상도에서 반투명 객체를 렌더링한 다음 이를 확대하여 장면 색상에 혼합합니다. (아래 사진)

또한 멀티 스레드 동기화, 오클루전 컬링 쿼리, 장벽, 렌더링 패스, GPU 리소스 생성 및 업로드, 정적 정적, 메모리 사용량 및 누수, 비디오 메모리 사용량, VS 및 PS 성능 비율 등의 소비 및 최적화에도 주의를 기울여야 합니다.

다음은 A Year in a FortniteFornite가 모바일 측에서 만든 최적화 범례 중 일부입니다.

설명 테이블을 최적화(캐싱)한 후의 Fornite 성능 비교 차트입니다. 위: 최적화 전; 하단: 최적화 후.

Fornite 최적화 동기화 소비 전후 비교. 위: 최적화 전; 하단: 최적화 후.

렌더링 패스를 최적화하는 Fornite의 전후 비교. 왼쪽: 최적화 전; 오른쪽: 최적화 후.

버텍스 및 인덱스 버퍼를 비동기적으로 생성한 후 Fornite의 효과 비교.

Fornite의 텍스처 업로드 최적화 효과 비교(분산 및 함께 패키지)

Traha를 Vulkan으로 포팅할 때의 과제는 파이프라인 장벽을 최적화한 비교 차트입니다.

Call of Duty Mobile의 적응형 성능은 성능, 에너지 소비, 온도 및 기타 매개변수를 모니터링하고 자동으로 동적으로 조정합니다.

12.6.5 XR 최적화

XR 렌더링에는 일반적으로 다음과 같은 특징이 있습니다.

  1. 고해상도. XR 장치의 해상도(1536x1536, 2K, 4K)는 일반 모바일 장치(720p, 1080p)보다 높습니다.

  2. 새로 고침 빈도가 높아졌습니다. 일반 모바일 기기의 재생률은 일반적으로 60Hz 이하(예: 30Hz)이지만, 3D에서 사용자가 더 나은 경험을 하고 어지러움을 방지하려면 XR 기기는 60Hz 이상(72Hz, 100Hz, 120Hz)을 유지해야 합니다.

  3. 안티앨리어싱(Anti-aliasing)을 반드시 수행해야 합니다. 앤티앨리어싱 기술이 없으면 XR 장치의 렌더링 품질이 심각하게 앨리어싱되고 깜박입니다(화면이 눈에 더 가깝기 때문).

  4. 각 프레임은 두 번 렌더링되어야 합니다(사람의 눈은 두 개임).

위의 특수 설정으로 인해 XR 장치에 필요한 대역폭은 일반 모바일 장치의 9배 이상입니다. 따라서 전력 및 열 방출 제한과 함께 XR 장비는 성능을 극도로 요구하며 최적화 기술 요구 사항은 더욱 엄격합니다.

다음은 일반적인 XR 렌더링 최적화 기술입니다.

12.6.5.1 포비티드 렌더링

인간의 눈은 고정점의 중심 정의에 대한 요구 사항이 높기 때문에 중심점에서 멀어질수록 필요한 정의는 감소합니다.

Foveation 포인트 렌더링 원리. 원리는 시선 지점에 가까울수록 동일한 입체각으로 덮이는 영역이 작아지고(더 많은 픽셀이 필요함) 그 반대의 경우도 마찬가지입니다(더 적은 픽셀이 필요함).

Qualcomm의 XR 전용 칩은 포비티드 렌더링 기술을 사용하여 성능을 25% 향상하고 렌더링 해상도를 높입니다.

Qualcomm은 개발자가 세부 매개변수를 사용하여 포비티드 렌더링의 세부 사항을 정밀하게 제어할 수 있도록 하는 OpenGL ES 또는 Vulkan 확장 기능을 활용합니다.

포비티드 렌더링 효과와 오프 포커스 선명도 곡선은 다음과 같습니다.

Mali의 포비티드 렌더링 기술을 사용하면 일반적으로 프레임 버퍼 크기를 35%, 총 소비량을 20%, 프래그먼트 셰이더 소비량을 40% 줄일 수 있지만, 버텍스 셰이더 소비량은 52% 증가합니다.

12.6.5.2 멀티뷰

Qualcomm의 XR 전용 칩은 고급 모드에서 멀티뷰 렌더링을 구현합니다.

VR과 같은 렌더링을 최적화하는 데 사용되는 MultiView 비교 차트입니다. 위: MultiView 모드 없이 렌더링, 두 눈 각각 그리기 지침을 제출합니다. 중간: 기본 MultiView 모드, 제출 지침을 재사용하고 GPU 레이어에 추가 명령 목록을 복사합니다. 하단: DC, 명령 목록 및 형상 정보를 재사용할 수 있는 고급 MultiView 모드.

멀티뷰 렌더링 기술을 사용하면 CPU 시간을 3349% 절약하고 에너지 소비를 533% 줄일 수 있습니다.

Mali GPU의 멀티뷰 구현은 Qualcomm의 멀티뷰 구현과 다릅니다. Fragment Shader 이전에는 동일한 데이터가 공유되지만 Fragment Shader 이후에는 왼쪽 눈과 오른쪽 눈이 구분됩니다.

Vulkan은 또한 Multiview에 대한 최적화 기술을 제공하는데, 이는 두 눈에 대한 서로 다른 명령만 기록하고, 다중 뷰 관련 렌더링 채널을 제공하고, VIEW_LOCAL 태그를 사용하여 타일 활용도와 캐시 적중률을 향상시키는 것으로 나타났습니다.

다른 플랫폼이나 그래픽 API도 Multiview를 다르게 구현합니다.

12.6.5.3 스테레오 렌더링

입체 렌더링은 두 눈을 하나의 패스로 결합하여 렌더링을 수행하므로 드로우 콜이 줄어듭니다. 가장 먼저 해야 할 일은 두 눈의 보기 프러스텀를 하나로 병합하는 것입니다.

그런 다음 병합된 뷰 프러스텀를 사용하여 일반 렌더링 프로세스를 시작합니다. 카메라 병합 전략에 따르면 평행, 전치, 틸트-시프트의 세 가지 유형으로 나눌 수 있습니다.

입체 렌더링을 위한 3가지 카메라 병합 전략. 왼쪽: 평행; 중간: 전치; 오른쪽: 축 이동.

아래 그림은 병렬 모드의 3차원 렌더링 효과입니다(왼쪽과 오른쪽 그림이 약간 오프셋되어 있음).

12.6.5.4 지연 숨기기

XR 장치의 경우 허용되는 최대 지연 시간은 20ms입니다. 즉, 디스플레이가 20ms 이내에 표시될 때까지 논리적 데이터(예: 장치 자세)를 얻을 수 없습니다. 장치가 60fps로 실행된다고 가정할 때 두 프레임의 지연이 있는 경우 첫 번째 프레임부터 논리 데이터가 렌더링되는 시간까지의 총 시간은 50ms가 됩니다! ! (아래 사진)

XR 애플리케이션은 일반적인 단순 애플리케이션과 다릅니다. 멀티코어를 활용하기 위해서는 필연적으로 멀티스레드 렌더링이 도입되기 때문에 각 스레드 사이에는 대기와 지연이 존재합니다.

일반 애플리케이션 렌더링 흐름도.

게임 스레드와 렌더링 스레드를 분리하는 모델은 로직 시뮬레이션과 렌더링 레이어를 분리할 수 있지만 지연이 발생합니다.

XR 장치의 자세(Pose) 및 기타 정보를 게임 스레드에서 쿼리하면 큰 지연이 발생하여 사용자가 어지러움증과 지연감을 느끼게 됩니다. 지연을 줄이기 위해 렌더링 스레드의 초기 단계에서 자세 정보를 다시 쿼리하면 이 문제를 해결하는 것도 매우 간단합니다.

물론 프레임 속도를 높이고, 병렬 작업을 합리적으로 배치하여 지속 시간을 단축하고, 단일 버퍼링과 같은 조치를 사용하여 지연을 줄일 수도 있습니다.

또한 Arm은 VR에서 UE4 적용을 최적화하여 원래 3프레임 지연을 1~2프레임 지연으로 줄이려고 노력하고 있습니다.

  • 시스템 수준 최적화.

예를 들어 Google의 Android 시스템 엔지니어는 Android의 원래 삼중 버퍼링을 이중 버퍼링으로 최적화하여 렌더링 지연을 3프레임에서 1프레임으로 줄였습니다.

안드로이드 최적화 렌더링 지연 비교 차트. 위: 최적화 없음; 하단: 최적화가 수행되었습니다.

12.6.5.5 기술 사양 개발

XR 장치는 일반 모바일 장치보다 더 엄격하기 때문에 공식화할 수 있는 표준도 더 엄격해질 것입니다.

다음은 기술 사양과 관련하여 Oculus의 공식 제안 사항입니다.

1. 드로우콜

장비 드로우콜 수량 장면 복잡성

퀘스트 1 50~150 높은

퀘스트 1 150~250 안으로

퀘스트 1 200~400 낮음

퀘스트 2 80~200 높은

퀘스트 2 200~300 안으로

퀘스트 2 400~600 낮음

2. 삼각형 수

장비 삼각형의 수

퀘스트 1 350,000~500,000

퀘스트 2 750,000~100만

위의 두 매개변수 외에도 프레임 속도, 해상도, 메모리, 비디오 메모리, 지연, 전력 소비, 배터리 수명, 장치 온도와 같은 기술 매개변수에도 주의를 기울여야 합니다.

모바일 장치는 열과 에너지 균형에 주의가 필요합니다.

12.6.5.6 기타 XR 최적화

로컬 큐브맵에 기반한 최적화된 렌더링 기술은 동적 부드러운 그림자, 반사 및 기타 효과를 효율적으로 달성할 수 있는 로컬 큐브맵 최적화에 기반한 렌더링 기술을 제안합니다.

로컬 큐브맵의 개념과 계산 과정.

로컬 큐브맵을 기반으로 한 동적 소프트 섀도우 키 범례 및 구현입니다.

로컬 큐브맵을 기반으로 한 동적 반사 키 범례 및 구현입니다.

Crytek이 VR용 3차원 UI를 구축하는 방법 VR에서 글꼴을 렌더링하는 세 가지 기술인 텍스처로 렌더링한 다음 모델로 매핑, 거리 필드 및 3D 모델링을 언급하고 이러한 방법의 장점과 단점을 자세히 비교했습니다.

텍스처 렌더링은 작은 글꼴에 대해 충분히 최적화되지 않았으며 더 높은 해상도가 필요합니다.

디스턴스 필드 글꼴은 섬세한 전환과 우수한 앤티앨리어싱을 제공합니다. 그러나 구현이 복잡하고 소비량이 높습니다.

3D 그리드 글꼴. 이는 우수한 앤티앨리어싱 기술 지원이 필요하며 장기적으로 최선의 선택이 될 수 있습니다.

12.6.6 디버깅 도구

성능 최적화와 디버깅 분석 도구는 밀접하게 연관되어 있습니다. 속담처럼, 일을 잘하고 싶다면 먼저 도구를 갈고 닦아야 합니다.

RenderDoc, PIX, Visual Studio, XCode 및 기타 소프트웨어나 IDE에서 제공하는 기존 성능 분석 외에도 GPU 제조업체는 자체 하드웨어에 대한 보다 전문적이고 심층적인 분석 도구를 제공합니다. 다음은 일반적으로 사용되는 제조업체 및 해당 분석 도구의 표입니다.

GPU 제조업체 GPU 분석 소프트웨어

퀄컴 아드레노 스냅드래곤 프로파일러

말리 암 모바일 스튜디오

상상기술 파워VR PowerVR 그래픽 도구

Qualcomm의 Snapdragon Profiler를 예로 들어 보겠습니다. SoC의 실시간 활동을 모니터링하고(Realtime), 특정 기간 동안 시스템 및 드라이버 작업 부하를 추적하고(Trace), 특정 프레임의 특정 렌더링 상태, 프로세스 및 리소스를 분석할 수 있습니다(Snapshot).

Snapdragon의 실시간 페이지.

Snapdragon의 추적 페이지.

Snapdragon의 스냅샷 페이지.

Snapdragon의 강력한 모니터링 기능을 사용하면 스레드, 드라이버 및 GPU 구성 요소의 소비를 확인하고 성능 병목 현상을 찾아 지연, 전력 등을 최적화할 수 있습니다. 특정 최적화 사례는 애플리케이션 병목 현상 식별을 참조하세요.

12.7 이 기사 요약

이 기사에서는 주로 UE 모바일 단말의 장면 렌더러의 주요 프로세스, 순방향 및 디퍼드 렌더링 프로세스, 모바일 단말의 렌더링 특성 및 조명 알고리즘에 대해 설명합니다. 다음 두 부분에서는 UE를 넘어 현재 모바일 단말기와 관련된 전용 렌더링 기술을 자세히 설명하고 모바일 단말기 GPU 아키텍처 및 작동 메커니즘을 설명하며 마지막으로 자세한 렌더링 최적화 제안을 제공합니다.

모바일 게임 최적화에 관해서는 저자가 작성한 모바일 게임 성능 최적화를 위한 일반적인 기술에 대한 다른 기사를 보충 자료로 참조할 수 있습니다.

모바일 주제는 세 부분으로 나누어져 있으며 총 단어 수는 거의 60,000 단어입니다. 100여종 이상의 다양한 문헌, 자료, 논문을 참고하고 있어 가장 많이 참고한 자료입니다. 논문의 정리, 기획, 연구부터 집필, 수정, 출판까지 총 한달 넘게 걸렸습니다.

사람들이 한밤중에 잠자리에 들어야 할 때, 주말에 쉬고 쉬어야 할 때, 저자는 여전히 맹렬하게 글을 쓰고 있습니다. 여가 시간을 거의 다 지쳤음에도 불구하고 성취감이 가득합니다. UE와 모바일 렌더링을 배우고, 국내 그래픽 렌더링 기술의 발전을 위해 함께 노력하는 모든 학생들에게 도움과 참고가 되기를 바랍니다.

12.7.1 이 기사에 대한 생각

평소와 마찬가지로 이 기사에서는 UE와 모바일 렌더링에 대한 숙달과 이해를 심화하고 이해하는 데 도움이 되는 몇 가지 작은 생각을 정리했습니다.

  • UE 모바일 씬 렌더러의 주요 프로세스에 대해 설명해주세요.

  • UE 모바일 단말에서 Forward 및 Deferred Rendering의 주요 프로세스에 대해 설명해주세요.

  • UE 모바일 단말의 빛과 그림자 알고리즘과 최적화에 대해 설명해주세요.

  • TBR, Subpass 등 현재 모바일 전용 렌더링 기술에 대해 설명해주세요.

  • 모바일 단말기의 일반적인 렌더링 최적화 기술은 무엇입니까? 몇 가지 예를 들어주세요.

팀 모집

블로거 팀은 UE4를 사용하여 새로운 몰입형 경험 제품을 개발하고 있으며, 위대한 목표를 달성하기 위해 함께 일하고 협력할 각계각층의 현자들이 시급히 필요합니다. 현재 아래와 같은 직무를 긴급 모집하고 있습니다.

  • UE 로직 개발.

  • UE 엔진 프로그램.

  • UE 그래픽 렌더링.

  • TA(기술, 예술).

요구사항:

  • 견고한 기술 기반.

  • 기술에 대한 열정이 높습니다.

  • 자기 동기가 좋습니다.

  • 커뮤니케이션 및 협업 능력이 좋은 분.

  • UE 사용 경험이나 모바일 단말 개발 경험이 있으면 우대합니다.

관심이 있거나 더 알고 싶으시면 블로거의 WeChat 계정을 추가하세요: 81079389(Blog Park에 지원한다고 표시). 또는 이력서를 블로거의 이메일 주소: 81079389#qq.com(#을 @로 바꾸세요)으로 보내주세요.

각계각층의 영웅들이 만나기를 기다립니다.

특별 지침

  • 모든 참고문헌의 저자에게 감사드립니다. 일부 사진은 참고 자료와 인터넷에서 가져온 것이므로 삭제되었습니다.

  • 이 시리즈의 기사는 저자가 직접 작성한 것이며 블로그에만 게시됩니다. 이 글의 링크를 공유하셔도 좋지만 무단 전재는 허용되지 않습니다!

  • 계속되는 일련의 기사. 전체 목록을 보려면 콘텐츠 개요를 클릭하세요.

  • 계속되는 일련의 기사. 전체 목록을 보려면 콘텐츠 개요를 클릭하세요.

  • 계속되는 일련의 기사. 전체 목록을 보려면 콘텐츠 개요를 클릭하세요.








📚 참고 자료 및 공식 링크 (References)


🔗 연관 지식 베이스 (Wiki & Tools)

Based on the legendary technical series by Timlly (0向往0 / 毛星云)