유니티 성능 개선
- https://developers.meta.com/horizon/documentation/unity/unity-perf
- CPU / GPU 제약 필요하다.
- Dynamic resolution 할 때, 처음에 작게 잡고 키우면 버퍼가 더 필요하다.
- 듀얼 코어 / 부스터 모드 지원
- 최신 유니티 버전에 대해서 권장이 필요
- GPU resident drawer 성능 비교
- 전통적인 모바일 성능 최적화 방법
Rift 및 Android에 대한 베스트 프랙티스
이 섹션에서는 성능 목표와 개발자를 위한 권장 사항을 설명합니다.
일반 베스트 프랙티스
- 개발 주기를 단축하기 위해 Link for Developers를 사용하십시오.
- 텍스처에 트리리니어(trilinear) 또는 이방성(anisotropic) 필터링을 사용하십시오. 자세한 내용은 Unity 매뉴얼의 텍스처(Textures) 항목을 참조하십시오.
- 메시 기반의 오클루전 컬링(occlusion culling)을 사용하십시오. 자세한 내용은 Unity 매뉴얼의 오클루전 컬링(Occlusion Culling) 항목을 참조하십시오.
- 항상 Forward Rendering 경로를 사용하십시오. 자세한 내용은 Unity 매뉴얼의 Forward Rendering Path 항목을 참조하십시오.
- OVRManager에서 ‘Use Recommended MSAA Levels’를 활성화하십시오. 일반적으로 권장되는 MSAA 레벨은 4x입니다. 만약 ‘Use Recommended MSAA Level’을 활성화하지 않을 계획이라면 이 수준을 사용하십시오.
- OVRManager의 빌드 설정 옵션에서 링크 시간 최적화(Link Time Optimization)를 활성화하십시오(릴리스 빌드에서만). 이 옵션을 설정하면 빌드 시간이 길어질 수 있으므로 최종 릴리스 빌드를 생성할 때만 이 옵션을 설정하십시오.
- LOD 바이어스 이후 과도한 텍스처 해상도(PC에서는 4k x 4k 이상, 모바일에서는 2k x 2k 이상)를 주의하십시오.
- 콜라이더(collider)를 가진 비정적 객체가 본체나 상위 체인에 리지드바디(rigidbody)가 누락되지 않았는지 확인하십시오.
- SSAO, 모션 블러, 글로벌 안개, 패럴랙스 매핑과 같은 비효율적인 효과는 피하십시오.
- 느린 물리 설정(Sleep Threshold 값이 0.005 이하, Default Contact Offset 값이 0.01 이하, Solver Iteration Count 값이 6 이상)도 피하십시오.
- 다중 패스 셰이더(e.g., 레거시 스페큘러)의 과도한 사용을 피하십시오.
- 대형 텍스처 또는 부트스트랩 최적화를 위해 시작 장면에서 많은 프리팹을 사용하는 것을 피하십시오. 대형 텍스처를 사용할 경우, 가능한 한 압축하십시오.
- 실시간 글로벌 조명을 피하십시오.
- 지오메트리나 드로우 콜(draw call) 한계에 근접할 때는 그림자를 비활성화하십시오.
- 과도한 픽셀 조명(Android 장치에서는 1개 이상, Rift에서는 3개 이상)을 피하십시오.
- 과도한 렌더링 스케일(1.2 이상)을 피하십시오.
- 과도한 셰이더 패스(2개 이상)를 피하십시오.
- Unity의 WWW 사용에 주의하고 대형 파일 다운로드에는 사용하지 마십시오. 매우 작은 파일에는 사용할 수 있습니다.
- 음성 채팅이 포함된 Android 앱의 경우, Parties와 관련된 문제를 피하려면 Microphone API를 사용하십시오. Parties에 대한 자세한 내용은 Parties 및 파티 채팅(Party Chat) 주제를 참조하십시오.
다음은 요청하신 텍스트의 한국어 번역입니다:
CPU 및 GPU 레벨 향상
Meta Quest 2, Meta Quest 3, Meta Quest Pro 헤드셋은 v57 운영 체제(2023년 10월에 출시됨) 또는 그 이상의 버전을 사용할 때, 모든 개발자가 애플리케이션 수준에서 성능을 향상시키기 위해 추가 CPU 및 GPU 레벨을 활성화할 수 있습니다. 이러한 더 높은 레벨은 주변 환경이 충분한 냉각을 허용하는 한 사용할 수 있습니다. 이 업데이트는 다음과 같은 클럭 속도 증가를 가능하게 합니다:
CPU | GPU | |
---|---|---|
Meta Quest 2 | 25% (듀얼 코어 모드에서 45%) | 18% |
Meta Quest 3 | 7% | 10% |
Meta Quest Pro | 25% (듀얼 코어 모드에서 45%) | 10% |
이러한 변경 사항을 통해 애플리케이션은 가능한 한 많은 전력을 사용할 수 있으며, 장치의 열 한계를 유지하기 위해 최소한의 제한만 받습니다.
Meta Quest 2 및 Meta Quest Pro
Meta Quest 2 및 Meta Quest Pro에서 애플리케이션이 실행할 수 있는 최대 클럭 레벨은 CPU 레벨 6 및 GPU 레벨 5입니다(참고로 CPU 및 GPU 레벨을 참조).
현재, 애플리케이션은 한 번에 이 최대값 중 하나에만 도달할 수 있습니다.
애플리케이션이 이러한 더 높은 CPU 및 GPU 레벨에서 실행되도록 할 수 있는 기술은 다음과 같습니다:
- ProcessorPerformanceLevel을 Boost로 설정
- 듀얼 코어 모드
- CPU와 GPU 레벨 간의 교환
최대 CPU 및 GPU 레벨에 대한 정보는 CPU 및 GPU 레벨의 “CPU/GPU 레벨 가용성” 섹션을 참조하십시오. 낮은 CPU/GPU 레벨 가용성(예: Passthrough 사용으로 인한 경우)의 애플리케이션도 이러한 기술을 사용할 수 있지만, Passthrough를 사용하지 않는 애플리케이션처럼 동일한 최대 CPU 및 GPU 레벨에 도달하지는 않습니다.
Meta Quest 3
Meta Quest 3에서 애플리케이션이 실행할 수 있는 최대 클럭 레벨은 CPU 레벨 5 및 GPU 레벨 5입니다(참고로 CPU 및 GPU 레벨을 참조).
현재, 애플리케이션은 한 번에 이 최대값 중 하나에만 도달할 수 있습니다.
애플리케이션이 더 높은 CPU 및 GPU 레벨에서 실행되도록 할 수 있는 기술은 다음과 같습니다:
- CPU와 GPU 레벨 간의 교환
최대 CPU 및 GPU 레벨에 대한 정보는 CPU 및 GPU 레벨의 “CPU/GPU 레벨 가용성” 섹션을 참조하십시오. 낮은 CPU/GPU 레벨 가용성(예: Passthrough 사용으로 인한 경우)의 애플리케이션도 이러한 기술을 사용할 수 있지만, Passthrough를 사용하지 않는 애플리케이션처럼 동일한 최대 CPU 및 GPU 레벨에 도달하지는 않습니다.
ProcessorPerformanceLevel을 Boost로 설정
Meta Quest 2 및 Meta Quest Pro에서 애플리케이션은 ProcessorPerformanceLevel을 Boost로 설정하면 최대 CPU 레벨 5 및 GPU 레벨 5까지 실행할 수 있습니다. 이 Boost 설정은 OS v57 이상을 실행하는 모든 Meta Quest 2 및 Meta Quest Pro 헤드셋에서 사용할 수 있습니다.
요구 사항
- 애플리케이션은 높은 레벨을 유지하기 위해 CPU 및 GPU 사용률 임계값을 초과해야 합니다. CPU 및 GPU 사용률 수준에 대한 정보는 각 헤드셋의 CPU 및 GPU 레벨 섹션을 참조하십시오.
- 애플리케이션은 동적 해상도를 활성화해야 GPU 레벨 5에 도달할 수 있습니다. 동적 해상도를 사용하지 않는 애플리케이션은 최대 GPU 레벨 4로 제한됩니다.
- Meta Quest 2 또는 Meta Quest Pro에서만 가능합니다. Meta Quest 3 헤드셋은 SustainedHigh까지만 지원합니다.
- 애플리케이션은 OpenXR 백엔드를 사용해야 합니다. 기존 OVRPlugin 백엔드로 빌드된 애플리케이션은 이러한 향상의 혜택을 누리지 못합니다.
ProcessorPerformanceLevel을 Boost로 설정하는 방법
CPU 및 GPU 레벨 설정에 대한 지침을 따르십시오.
CPU 레벨 5에 대한 주의 사항
CPU 레벨 5에서는 추가적인 CPU 성능으로 큰 성능 향상을 얻을 수 있습니다. 일부 애플리케이션은 이전에 CPU로 인해 72Hz로 제한되었지만, 기존 GPU 비용이 여전히 90Hz 프레임당 GPU 예산에 맞을 경우 즉시 90Hz로 전환될 수 있습니다. 다른 애플리케이션은 물리 연산, AI/경로 찾기 또는 이와 유사한 더 강력한 CPU 작업을 수행하는 데 확장된 계산 예산을 사용할 수 있습니다.
OVRMetrics 도구 또는 Meta Quest Developer Hub를 사용하여 현재 CPU 및 GPU 레벨과 사용량을 프로파일링할 수 있습니다.
GPU 레벨 5에 대한 주의 사항
GPU 레벨 5는 동적 해상도를 활성화해야 사용할 수 있습니다. 동적 해상도를 사용하는 이유는 동적 해상도가 애플리케이션의 “눈 버퍼”(렌더링 기본 텍스처)를 사용 가능한 GPU 여유 공간에 따라 위아래로 조정할 수 있기 때문입니다. 이로 인해 헤드셋이 열로 인해 GPU 레벨 4로 강제로 제한되는 경우에도 해상도를 낮추는 대신 성능을 유지할 수 있습니다. 또한 GPU 레벨 5가 사용 가능할 때 성능을 개선할 수 있으며, 개발자가 별도로 로직을 작성할 필요는 없습니다.
애플리케이션은 GPU 레벨 4를 최고 GPU 예산으로 설정하도록 설계되어야 하며, GPU 레벨 5는 사용자의 환경 조건에 따라 제공되는 품질 향상으로 간주해야 합니다. 특히, GPU 레벨 5는 헤드셋에 충분한 열 여유가 있을 때만 허용됩니다. 따라서 애플리케이션의 기능을 위해 GPU 레벨 5에 의존하거나 최소 프레임 속도 요구 사항을 충족하기 위해 의존해서는 안 됩니다.
고급 GPU 파이프라인과 로드, 스토어, 패스
이 주제에서는 OpenGL과 Vulkan의 다양한 모바일 GPU 렌더링 아키텍처, 올바른 구성과 프로파일링 방법, 그리고 이러한 아키텍처에서 고정 초점 렌더링(FFR)이 어떻게 작동하는지에 대한 개요를 제공합니다. 다음 섹션은 모바일 GPU 렌더러를 다루는 모든 사람에게 필수적인 지식입니다.
개요
래스터라이제이션 기반의 모든 GPU에서, 화면상의 모든 삼각형은 픽셀 위치의 깊이 버퍼의 현재 값에 대해 깊이 테스트를 수행하는 픽셀을 출력합니다. 깊이 테스트를 통과하면, 새로운 색상과 깊이 값을 색상/깊이 첨부 파일에 기록하며, 프레임의 모든 삼각형이 렌더링될 때까지 이 과정을 반복합니다.
이는 모든 삼각형의 모든 픽셀에 대해 GPU가 최소한 한 번의 읽기(깊이 테스트를 위해)와 여러 번의 두 번의 쓰기(색상과 깊이)를 수행해야 함을 의미합니다. 색상 블렌딩의 경우, 최종 색상 값을 계산하기 위해 추가적인 색상 읽기가 필요합니다. 겹치는 기하학적 구조가 많을수록 읽기와 쓰기가 더 많아집니다.
타일드 렌더러
타일드 렌더러는 다음과 같은 사실을 이용하여 읽기와 쓰기의 속도를 최적화합니다:
- 특정 픽셀은 프레임의 다른 픽셀 값에 의존하지 않습니다.
- 프레임 중간에 색상과 깊이의 값을 표시할 필요가 없습니다.
타일드 렌더러는 화면을 타일로 분할하고 순차적으로 렌더링합니다. 각 타일에 대해, 빠르고 작은 캐시(gmem/tilemem)를 사용하여 최종 픽셀 값이 계산될 때까지 읽기와 쓰기를 수행합니다. 그 후 그 값은 향후 사용을 위해 RAM(sysmem)에 저장됩니다.
Qualcomm Snapdragon 835와 Qualcomm Snapdragon XR2 칩셋(Oculus Quest와 Quest 2에서 각각 사용됨)은 모두 1Mb의 타일 메모리를 가지고 있습니다. XR2에서 멀티뷰 렌더링 파이프라인에서 타일은 왼쪽과 오른쪽 눈 뷰를 모두 포함하므로, 4xMSAA, 32비트 색상, 24/8 깊이/스텐실 버퍼로 실행되는 애플리케이션은 96x176 타일을 갖게 됩니다. 이는 픽셀이 (4-색상 + 4-깊이) x 4-msaa = 32바이트의 정보를 포함하기 때문입니다. 96x176x2(멀티뷰)x32 = 약 1Mb입니다. MSAA와 같은 픽셀별 설정을 변경하면 GPU 드라이버가 타일이 포함하는 픽셀 수를 최대화하려고 하므로(화면상의 총 타일 수를 줄이고 캐시 활용도를 최대화하기 위해) 타일 크기가 변경됩니다.
로드와 스토어
이제 타일드 렌더러의 아키텍처를 설명했으므로, 타일별로 워크플로우는 다음과 같이 유추할 수 있습니다:
- RAM에서 타일 메모리로 기존의 깊이와 색상 버퍼 데이터를 로드합니다.
- 모든 삼각형/프래그먼트를 타일 메모리에 렌더링합니다.
- 타일 메모리에서 RAM으로 최종 깊이와 색상 버퍼를 저장합니다.
그러나 이 워크플로우는 최적화되어 있지 않습니다. 첫 번째 단계는 종종 불필요한 대역폭 전송이며, 이전 프레임의 내용은 일반적으로 새 프레임으로 지워지거나 덮어씁니다. 따라서 첫 번째 단계를 피하기 위해 GPU 드라이버에 이전 프레임 내용을 지우거나 무효화하도록 지시할 수 있습니다. OpenGL에서는 프레임버퍼를 바인딩한 후 첫 번째 드로우 콜을 쓰기 전에 glClear 또는 glInvalidateFramebuffer를 사용합니다. Vulkan에서는 더 명시적인 시스템이 있으며, 렌더패스 구성에 loadOp와 storeOp 속성이 포함됩니다. 이는 다음과 같이 나타납니다:
loadOp와 storeOp 속성
여기서 지우기와 무효화의 차이는 최소이며, QCOM GPU는 다른 필요한 설정 작업 뒤에 지우기 비용을 숨기려고 합니다. 그러나 특정 경우에는 측정 가능할 수 있습니다. 그러나 둘 중 하나를 반드시 수행해야 합니다: 지우기를 피하는 것은 표준 PC GPU 최적화이지만, 이 칩셋에서는 성능을 적극적으로 해치며, GPU가 매 프레임마다 이전 프레임 데이터를 RAM에서 타일 메모리로 로드하도록 강제합니다.
세 번째 단계도 프레임이 끝난 후 일부 첨부 파일이 필요하지 않은 경우 불필요한 대역폭 전송입니다. 예를 들어, Vulkan에서 MSAA 첨부 파일과 OpenGL과 Vulkan 모두에서 깊이 첨부 파일은 프레임이 끝난 후 저장할 필요가 없는 경우가 매우 흔합니다. 이 경우 이러한 첨부 파일을 무효화하여 GPU 드라이버에 “타일 메모리에서 RAM으로 해당 내용을 저장하지 말고, 다음 타일 실행 중에 덮어쓸 것이니 필요 없습니다”라고 알려주는 것이 매우 유용합니다. OpenGL에서 동일한 glInvalidateFramebuffer 함수를 사용할 수 있지만, 이 컨텍스트에서 1단계와는 매우 다른 기능을 가지고 있으며, 두 버전 모두 필요합니다. 1단계에서 버퍼를 무효화하는 것은 렌더링 전에 RAM에서 타일 메모리로의 로드 작업을 피하기 위한 것이며, 3단계에서는 렌더링 후 타일 메모리에서 RAM으로의 저장 작업을 피하기 위한 것입니다. Vulkan에서는 renderpass 속성의 storeOp에 VK_ATTACHMENT_STORE_OP_DONT_CARE를 설정하여 이를 수행합니다.
OpenGL에서는 Vulkan처럼 명시적인 storeOp와 플러싱 지시어가 없으므로, 해당 프레임버퍼의 내용이 GPU가 실행하기 전에 glInvalidateFramebuffer를 호출하는 것이 중요합니다. 그렇지 않으면 무효화가 고려되지 않습니다. glFlush 전에 무효화 함수를 호출해야 합니다. 하지만 예를 들어 타이머 쿼리를 삽입하면(특정 지점까지의 작업 시간을 측정하는 것은 플러싱 없이 불가능하므로) 무효화 전에 타이머 쿼리 작업을 삽입하면 무효화가 고려되지 않고 깊이 버퍼를 해결하게 됩니다.
Vulkan에서는 명시적인 MSAA 첨부 파일이 있으며(OpenGL에서는 MSAA 프레임버퍼와 함께 사용해도 텍스처는 MSAA가 아님), 모든 MSAA 서브샘플을 RAM에 저장하지 않도록 하는 것이 중요합니다(이렇게 하면 대역폭이 MSAA 수준에 따라 선형적으로 증가하지만 이득은 없습니다). 따라서 마지막 서브패스의 pResolveAttachment 속성을 사용하여 MSAA가 아닌 첨부 파일을 바인딩하고 그 것만 저장해야 합니다. 아래 스크린샷에서 4xMSAA 첨부 파일은 STORE_OP_DONT_CARE 속성을 가지고 있지만, 1xMSAA 첨부 파일은 STORE_OP_STORE 속성을 가지고 있는 것을 볼 수 있습니다.
MSAA 속성 비교
GPU에는 하드웨어 가속된 MSAA 리졸브 칩이 있으므로 이를 활용하십시오.
도구
로드, 스토어, 렌더패스 구성 등을 표시하는 적절한 도구를 사용하여 GPU가 예상대로 동작하는지 확인하는 것이 중요합니다. 이는 커스텀 엔진을 작성하거나 Unity에서 장면을 빌드할 때 모두 해당됩니다.
ovrgpuprofiler는 이러한 정보를 표시하도록 특별히 설계된 렌더 스테이지 추적 도구입니다. ovrgpuprofiler는 신뢰할 수 있고 마찰이 적은 adb 쉘 도구로 실행하는 데 몇 초밖에 걸리지 않습니다. 다음은 1216x1344 렌더패스에 대한 출력 예제입니다. 이 예제에서는 1단계와 3단계를 올바르게 수행하지 않습니다:
Surface 1 | 1216x1344 | color 32bit, depth 24bit, stencil 8 bit, MSAA 2 | 28 320x192 bins | 10.62 ms | 171 stages : Binning : 0.305ms LoadColor : 0.71ms Render : 2.926ms StoreColor : 1.525ms Preempt : 2.964ms LoadDepthStencil : 0.828ms StoreDepthStencil : 0.871ms
여기서 LoadColor와 LoadDepthStencil(총 10ms 중 1.5ms)뿐만 아니라 StoreDepthStencil 시간을 볼 수 있습니다. 이는 대부분의 경우 필요하지 않습니다.
다중 패스: 파트 1 - 별도의 실행
많은 Quest 타이틀은 간단한 단일 패스 포워드 렌더러를 사용합니다. Quest 2에서는 GPU가 상당히 강력해졌으며, 더 많은 개발자가 더 복잡한 GPU 렌더링 파이프라인을 실행하고자 할 것입니다. 그러나 다중 패스 파이프라인을 작업할 때 주의해야 할 중요한 사항이 있습니다.
OpenGL과 Vulkan 모두에서 다중 패스를 수행하는 기본 방법은 모든 렌더링을 수행하는 메인/포워드 패스가 있고, 그 다음에 그 색상 버퍼를 RAM에 복사하는 것입니다. 그런 다음 해당 색상 버퍼를 두 번째 패스에 텍스처 입력으로 바인딩하여 톤매핑과 같은 효과를 적용하여 최종적으로 컴포지터가 할당한 스왑체인을 생성합니다. 이는 추가적인 패스를 추가하며, RAM으로의 저장을 포함합니다(이는 해상도/대역폭의 요인이며 복잡성과는 무관합니다. 새로운 패스가 단일 드로우 콜인지는 전혀 중요하지 않습니다—스토어는 텍스처 해상도에만 의존하는 고정 오버헤드입니다). 표준 포워드 렌더러의 시각적 품질과 비교할 때, 개발자가 필요하다면 이는 괜찮은 트레이드오프이며 특히 Quest 2에서 그렇습니다.
그러나 OpenGL에서는 여기에 하나의 엄격한 제한이 있습니다. FFR은 텍스처 기반이며 컴포지터에 의해 적용되며, 앱이 아니라 VrApi 스왑체인에 렌더링하는 프레임버퍼에만 영향을 미칩니다(이 경우 두 번째 패스). 따라서 개발자가 FFR을 활성화하면, 프래그먼트 비용이 높은 메인 패스에는 포비에이션이 적용되지 않고, 포비에이션되지 않은 픽셀이 RAM에 저장되며, 그런 다음 저렴한 톤매핑 패스가 해당 픽셀을 포비에이트합니다(이 과정에서 모든 노력을 기울인 정밀도를 잃게 됩니다). OpenGL에서는 컴포지터가 QCOM_texture_foveated 호출을 통해 메인 패스의 FFR 설정을 제어할 수 없으므로 이 문제에 대한 깔끔한 솔루션이 없습니다. 그러나 Vulkan은 RG8_UNORM 포비에이션 제어 텍스처를 통해 컴포지터가 제어하는 포비에이션 매개변수를 개발자에게 제공하며, 개발자는 이를 메인 패스, 최종 패스 또는 기타 렌더패스에 바인딩할 수 있습니다.
타일드 렌더러가 왜 의미가 있는지에 대한 설명을 기억한다면, 그들의 목표는 최종 픽셀이 계산되는 동안 색상과 깊이 버퍼에 대한 여러 번의 읽기와 쓰기를 최적화하는 것입니다. 깊이 버퍼와 MSAA가 없는 전체 화면 효과가 단일 드로우 콜로 렌더링되는 특정 경우, GPU는 실제로 루프에서 읽기/쓰기를 수행하지 않습니다—계산된 단일 프래그먼트가 최종 픽셀 색상이 됩니다. 이 경우 GPU 코어에서 타일메모리를 거치지 않고 바로 RAM으로 가는 것이 더 합리적이며, QCOM GPU는 이러한 동작을 감지하기 위한 휴리스틱을 사용하고 즉시 모드 GPU처럼 동작합니다. 이를 Direct Mode 렌더링이라고 하며, Unity의 톤맵이 실행되는 방식입니다. 우리의 GPU에서 FFR은 타일별 효과이며(해상도가 타일별로 수정됨), Direct Mode로 실행되는 표면에서는 FFR이 비활성화됩니다. 이는 왜 2패스 OpenGL Unity 렌더링이 프로젝트 설정에서 FFR이 활성화되었음에도 FFR을 전혀 받지 않는지를 설명합니다:
- 패스 1은 VrApi 스왑체인에 렌더링하지 않기 때문에 FFR을 받지 않습니다.
- 패스 2는 VrApi 스왑체인에 렌더링하지만, 깊이 버퍼가 없는 전체 화면 패스는 Direct Mode로 실행되어 FFR을 비활성화합니다.
도구 측면에서, 어떤 것이 Direct Mode로 렌더링되는지 알아내는 것은 간단합니다. 전체 표면이 “단일 타일”로 렌더링됩니다:
Surface 1 | 1216x1344 | color 32bit, depth 0bit, stencil 0 bit, MSAA 1 | 1 1216x1344 bins | 2.01 ms | 1 stages : Render 2.01ms
다중 패스: 파트 2 - 서브패스 이론
Vulkan은 타일 친화적인 방식으로 멀티패스 렌더링을 실행하기 위한 더 나은 방법인 서브패스를 도입했습니다. OpenGL ES에서는 ARM_framebuffer_fetch와 같은 확장이 이러한 동작을 시뮬레이트하려고 하지만, 특히 MSAA의 경우 사용을 권장하지 않습니다.
파트 1에서는 패스를 실행하는 “표준” 방법에 대해 설명했습니다. 여기서 GPU는 실제로 패스 1의 모든 것을 실행하고 이를 RAM에 저장한 다음, 패스 2의 모든 것을 실행하여 패스 1의 RAM 내용을 읽고 패스 2의 출력을 RAM에 저장합니다. 그러나 만약 패스 2의 픽셀이 패스 1의 출력에서 자신의 픽셀 좌표만 본다면, 패스 사이의 저장 및 로드를 건너뛰고 타일 메모리에서 순차적으로 실행하여 패스 2의 출력만 RAM에 저장할 수 있습니다. 이는 톤매핑이나 비네팅과 같은 전체 화면 효과에 해당하지만, 블룸과 심도 흐림에는 해당하지 않습니다(이러한 효과는 주어진 픽셀을 색칠하기 위해 주변 픽셀의 값에 의존합니다). 매우 간단한 ovrgpuprofiler -t -v
출력은 다음과 같이 실제로 변경됩니다:
Surface1 (Pass1)
render
store
render
store
Surface2 (Pass2)
render
store
render
store
에서
Surface1
render
render
store
render
render
store
로 변경됩니다.
이것이 서브패스의 목적입니다: 하나의 표면 실행 내에서 유지하고 타일 메모리에서 순차적으로 종속성을 허용하는 것입니다. 이는 Apple의 Metal API에서의 타일 셰이딩과 동일합니다. 이를 사용하여 타일 메모리 내에서 톤매핑 렌더러를 구현할 수 있으며, 서브패스 0은 톤매핑 전의 색상 버퍼를 출력하고, 서브패스 1은 그것을 읽어서(타일 메모리에서!) 톤매핑하여 VrApi 스왑체인에 톤매핑된 결과를 저장합니다. 이 경우 서브패스 0의 출력은 서브패스 1의 입력 첨부 파일(INPUT ATTACHMENT)이라고 합니다. 이 경우 프리 톤매핑 색상 버퍼는 RAM에 저장되지 않으며, 이는 상당한 성능 이점을 제공할 수 있으며, 톤매핑 패스는 RAM 대신 빠른 타일 메모리에서 입력 첨부 파일을 읽습니다.
서브패스는 Unreal Engine 4.25 이상에서 사용되며, 투명 셰이더가 불투명 패스의 깊이를 읽을 수 있도록 옵션을 제공합니다. 엔진은 불투명 객체와 투명 객체를 두 개의 별도 서브패스로 렌더링합니다. 불투명 객체가 먼저 렌더링되고, 불투명 서브패스의 깊이 버퍼가 입력 첨부 파일로 서브패스 1에 바인딩되며, 투명 객체는 픽셀 셰이더에서 이를 읽어 저렴한 깊이 기반 효과를 제공합니다. 이를 통해 동적으로 활성화 또는 비활성화할 수 있는 커스텀 서브패스 기반 톤매퍼를 개발할 수 있습니다.
다중 패스: 파트 3 - 서브패스를 매우 느리게 만드는 방법
GPU 드라이버가 구성한 서브패스를 순차적인 서브패스로 실행하는 대신 별도의 패스로 실행하도록 강제하고 이론적인 성능 향상을 모두 파괴하는 많은 구성이 있다는 것을 이해하는 것이 중요합니다. 어떤 경우에는 결과가 실제로 훨씬 더 느려집니다. 개발자가 마주치는 세 가지 주요 함정은 다음과 같습니다:
-
MSAA에서 MSAA가 아닌 리졸브를 위한 pResolveAttachment 항목이 있는 중간(최종이 아닌) 서브패스.
GPU의 하드웨어 가속 MSAA 리졸브 칩은 타일 메모리에서 RAM으로의 저장 파이프라인에 통합되어 있습니다. 중간 MSAA 서브패스가 pResolveAttachment 속성을 통해 내용을 MSAA가 아닌 것으로 리졸브하도록 요청하면, 이는 렌더패스처럼 내용을 RAM에 저장하도록 강제됩니다. 다음 서브패스는 별도의 패스로 실행해야 하며, 일반적으로 타일 메모리에 있는 입력 첨부 파일을 RAM에서 다시 로드해야 합니다.
이 경우 다음 서브패스가 해결되지 않은 MSAA 입력 첨부 파일을 통해 subpassLoad(input, subsampled) GLSL 함수를 사용하여 한 서브샘플당 한 번씩 읽도록 해야 하며, 수동으로 MSAA 리졸브를 수행해야 합니다. 다양한 MSAA 설정에 따른 성능 트레이드오프를 인지하십시오. Adreno 540(Quest)과 Adreno 650(Quest 2) GPU에서는 서브패스가 두 개의 입력 첨부 서브샘플을 병렬로 읽을 수 있지만 네 개는 읽을 수 없습니다. 따라서 2xMSAA 셰이더 읽기는 “무료”이지만, 4xMSAA 셰이더 읽기는 그렇지 않습니다.
-
저장 작업 첨부 파일이 있는 중간 서브패스.
서브패스의 전체 목적은 마지막 서브패스만 RAM에 내용을 저장하도록 하는 것입니다. 모든 첨부 파일을 살펴보고 마지막 서브패스의 첨부 파일(바람직하게는 MSAA가 아닌 colorattach 또는 리졸브 첨부 파일 - MSAA 서브샘플을 RAM으로 보낼 이유가 없음)에만 STORE_OP_STORE 속성을 갖도록 해야 합니다. 이러한 속성은 렌더패스 실행의 끝에서 RAM에 저장하려는 내용을 포함하며, 서브패스 간의 종속성에는 해당하지 않습니다. MSAA 색상 버퍼를 서브패스 0과 서브패스 1 사이에서 유효하게 유지해야 하는 경우, 저장 작업이 필요하지 않습니다.
-
과도하게 보수적인 pSubpassDependencies.
Vulkan은 서브패스 간의 종속성을 정의하도록 요청하여 무엇을 병렬로 실행할 수 있고 무엇을 실행할 수 없는지를 파악합니다. 예를 들어, dstAccessMask로 VK_ACCESS_SHADER_READ_BIT를 서브패스 종속성으로 제공하면 GPU 드라이버에 “서브패스 0의 출력을 서브패스 1에서 디스크립터 세트를 사용하여 셰이더로 읽어야 합니다”라고 말하는 것입니다. 이는 매우 정상적으로 보이지만, 실제로는 서브패스 모델을 파괴합니다. 만약 서브패스 1이 서브패스 0의 출력을 텍스처 샘플러를 사용하여 읽을 수 있어야 한다면, 이는 서브패스 0의 출력의 모든 텍셀, 즉 완전히 다른 타일에 있는 텍셀도 읽을 수 있음을 의미합니다. 따라서 서브패스 0과 서브패스 1은 타일 메모리에서 순차적으로 실행될 수 없으며, 별도의 렌더패스로 실행됩니다. 여기서 올바른 마스크는 VK_ACCESS_INPUT_ATTACHMENT_READ_BIT이며, 이는 종속성을 동일한 픽셀로만 강제합니다. 이는 입력 첨부 파일의 읽기 함수인 subpassLoad가 UV 매개변수를 가지지 않기 때문입니다.
GPU가 예상대로 동작하는지 확인하기 위해 이러한 코드를 작성하는 개발자는 렌더 스테이지 추적 도구인 ovrgpuprofiler를 사용하여 렌더 빈을 프로파일링하고 순차적으로 실행되고 있는지 또는 로드와 스토어가 있는 다른 표면 실행에서 실행되고 있는지를 확인하는 것이 중요합니다. 이를 통해 얻고 있는 성능을 볼 수 있는 다른 방법은 없습니다.
이 테스트에서 다루지 않은 항목들
테스트와 그 결과 데이터는 드로우 콜의 길이에 매개변수 변경이 미치는 일부 영향을 보여줍니다. 또한 애플리케이션을 설계할 때 인지해야 할 다른 사항들이 있습니다.
메시의 모양과 (화면 공간) 크기
여러 GPU 타일에 나타나는 폴리곤은 추가적인 비용이 발생할 수 있습니다.
불투명 vs. 투명
알파 블렌딩은 저렴하지 않으며, Linear 또는 sRGB 색 공간을 사용하는 경우 추가 비용이 발생합니다. Linear 공간에서는 모든 색상이 샘플링될 때 감마 곡선이 적용되고, 기록될 때 반대로 적용됩니다. 이는 모든 것이 sRGB 공간에 머무를 때보다 블렌딩 작업의 비용을 훨씬 높입니다.
MSAA 수준
MSAA가 무료이거나 항상 켜야 할 만큼 저렴하다는 소문이 떠돌고 있습니다. 실제로는 비용이 있습니다. 그러나 VR에서는 MSAA를 사용할 때 시각적 향상이 사실상 필수적입니다. 이는 증가한 비용을 다른 곳에서 상쇄해야 함을 의미합니다.
프레임 버퍼 페치
Unity는 GL_EXT_shader_framebuffer_fetch를 활용하는 것을 매우 간단하게 만듭니다(단순히 최종 색상을 반환하는 대신 inout
셰이더 매개변수를 사용하여 프래그먼트 셰이더를 작성하면 됩니다). 그러나 이는 비용이 꽤 클 수 있다는 사실을 숨깁니다. 그리고 그 비용은 MSAA 수준에 따라 증가합니다(프레임 버퍼 페치는 프래그먼트가 아닌 샘플별로 처리됩니다).
질문: binning step이 뭐야?
답변:
Binning 단계는 타일 기반 렌더러(tiled renderer)에서 중요한 역할을 하는 초기 처리 단계입니다. 이 단계에서 GPU는 화면에 렌더링될 기하학적 데이터를 효율적으로 관리하기 위해 다음과 같은 작업을 수행합니다:
- 기하학적 처리(Geometry Processing):
- 프레임에 포함된 모든 삼각형이나 기하학적 요소를 처리합니다.
- 각 삼각형의 위치, 크기, 방향 등을 계산하여 화면상의 어떤 영역에 렌더링되어야 하는지 파악합니다.
- 타일 할당(Tile Assignment):
- 화면을 작은 사각형 영역인 타일(tile)로 분할합니다.
- 각 삼각형이 어느 타일에 속하는지 또는 어느 타일에 영향을 미치는지 결정합니다.
- 하나의 삼각형이 여러 타일에 걸쳐 있을 수 있으며, 해당 타일들의 리스트에 이 삼각형을 포함시킵니다.
- 타일별 삼각형 리스트 생성:
- 각 타일에 대해 렌더링해야 할 삼각형들의 목록을 작성합니다.
- 이 목록은 이후의 렌더링 단계에서 사용되며, 타일별로 필요한 기하학적 정보만 처리할 수 있게 해줍니다.
왜 binning 단계가 중요한가요?
- 효율성 향상: binning 단계를 통해 GPU는 필요한 데이터만 집중적으로 처리하므로, 메모리 사용량과 대역폭을 절감할 수 있습니다.
- 병렬 처리 최적화: 타일 단위로 작업을 분할하면 병렬 처리가 용이해져 전체적인 렌더링 속도가 향상됩니다.
- 전력 소비 감소: 모바일 장치에서 중요시되는 전력 효율성을 높일 수 있습니다.
- 오버드로우 감소: 불필요한 픽셀 연산을 줄여 성능을 개선합니다.
예를 들어 설명하자면:
- 화면에 복잡한 3D 장면이 있고, 이 장면은 수많은 삼각형으로 이루어져 있다고 가정합니다.
- binning 단계에서 GPU는 각 삼각형이 화면의 어떤 타일에 영향을 미치는지 계산합니다.
- 그 결과, 타일 A에는 삼각형 1, 5, 9가, 타일 B에는 삼각형 2, 3, 7이 렌더링되어야 한다는 식의 리스트가 만들어집니다.
- 이후의 렌더링 단계에서는 각 타일별로 해당 리스트를 사용하여 필요한 부분만 렌더링합니다.
결론적으로, binning 단계는 타일 기반 렌더링 아키텍처에서 필수적인 과정으로, 화면상의 기하학적 데이터를 타일 단위로 조직화하여 렌더링 효율을 극대화합니다.
CPU 레벨 5에 대한 주의 사항
CPU 레벨 5에서는 추가적인 CPU 성능이 큰 이점을 가져올 수 있습니다. 이전에 CPU에 병목이 있었고 기존의 GPU 비용이 여전히 프레임당 90Hz의 GPU 예산에 맞는 경우, 일부 앱은 즉시 72Hz에서 90Hz로 전환될 수 있습니다. 다른 앱들은 확장된 컴퓨팅 예산을 활용하여 물리 연산, AI/경로 찾기 또는 유사한 더 집약적인 CPU 작업을 수행하기로 선택할 수 있습니다.
현재 CPU 및 GPU 레벨과 활용도는 OVRMetrics Tool이나 Meta Quest Developer Hub를 사용하여 프로파일링할 수 있습니다.
내장 렌더 파이프라인의 렌더링 경로
Unity의 내장 렌더 파이프라인은 다양한 렌더링 경로를 지원합니다. 렌더링 경로는 조명과 셰이딩과 관련된 일련의 작업입니다. 각 렌더링 경로는 서로 다른 기능과 성능 특성을 가지고 있습니다. 프로젝트에 가장 적합한 렌더링 경로를 결정하는 것은 프로젝트의 유형과 대상 하드웨어에 따라 다릅니다.
그래픽스(Graphics) 창에서 프로젝트에서 사용할 렌더링 경로를 선택할 수 있으며, 각 카메라(Camera)에 대해 해당 경로를 재정의할 수도 있습니다.
프로젝트를 실행하는 기기의 GPU가 선택한 렌더링 경로를 지원하지 않는 경우, Unity는 자동으로 낮은 품질의 렌더링 경로를 사용합니다. 예를 들어, 디퍼드 셰이딩(Deferred Shading)을 처리할 수 없는 GPU에서는 Unity가 포워드 렌더링(Forward Rendering)을 사용합니다.
포워드 렌더링
포워드 렌더링은 내장 렌더 파이프라인의 기본 렌더링 경로이며, 범용 렌더링 경로입니다.
포워드 렌더링에서 실시간 조명을 렌더링하는 것은 매우 비용이 많이 듭니다. 이러한 비용을 상쇄하기 위해, 한 번에 Unity가 픽셀당 렌더링해야 하는 조명의 수를 선택할 수 있습니다. Unity는 씬(Scene)의 나머지 조명을 낮은 품질로 렌더링합니다: 버텍스당 또는 객체당으로.
프로젝트에서 많은 양의 실시간 조명을 사용하지 않거나 조명 품질이 프로젝트에서 중요하지 않다면, 이 렌더링 경로가 프로젝트에 좋은 선택일 것입니다.
자세한 내용은 포워드 렌더링 페이지를 참조하세요.
디퍼드 셰이딩
디퍼드 셰이딩은 내장 렌더 파이프라인에서 가장 높은 조명 및 그림자 품질을 제공하는 렌더링 경로입니다.
디퍼드 셰이딩은 GPU 지원이 필요하며 몇 가지 제한 사항이 있습니다. 반투명 객체를 지원하지 않으며(이러한 경우 Unity는 포워드 렌더링을 사용합니다), 직교 투영을 지원하지 않습니다(이러한 카메라에 대해 Unity는 포워드 렌더링을 사용합니다), 또는 하드웨어 안티앨리어싱을 지원하지 않습니다(비슷한 결과를 얻기 위해 후처리 효과를 사용할 수 있습니다). 컬링 마스크(culling masks)에 대한 지원이 제한적이며, Renderer.receiveShadows
플래그를 항상 true로 처리합니다.
프로젝트에 많은 실시간 조명이 있고 높은 수준의 조명 품질이 필요하며, 대상 하드웨어가 디퍼드 셰이딩을 지원한다면, 이 렌더링 경로가 프로젝트에 좋은 선택일 수 있습니다.
이 렌더링 경로의 제한 사항에 대한 조언을 포함한 자세한 내용은 디퍼드 셰이딩 페이지를 참조하세요.
레거시 버텍스 라이트(Legacy Vertex Lit)
레거시 버텍스 라이트는 가장 낮은 조명 품질을 가지며 실시간 그림자를 지원하지 않는 렌더링 경로입니다. 이는 포워드 렌더링 경로의 하위 집합입니다.
Unity 모바일 성능 최적화 방법
모바일 플랫폼에서 Unity를 사용하여 게임이나 애플리케이션을 개발할 때, 성능 최적화는 사용자 경험을 향상시키고 배터리 소모를 줄이며 더 많은 기기에서 원활하게 실행되도록 하는 데 중요합니다. 아래는 Unity 모바일 성능을 최적화하기 위한 주요 방법과 권장 사항입니다.
1. 프로파일링을 통한 성능 분석
- Unity Profiler 사용: Unity Profiler를 사용하여 CPU, GPU, 메모리 사용량 등을 분석하고 성능 병목 지점을 식별합니다.
- 프레임 디버거(Frame Debugger): 렌더링 과정에서 발생하는 문제를 확인하고 최적화할 수 있습니다.
- 플랫폼별 프로파일링 도구: Android의 경우 Android Studio Profiler, iOS의 경우 Xcode Instruments를 사용하여 추가적인 성능 분석을 수행합니다.
2. 드로우 콜(Draw Call) 최적화
- 배치(Batching) 사용: 정적 배치(Static Batching)와 동적 배치(Dynamic Batching)를 활용하여 드로우 콜 수를 줄입니다.
- GPU 인스턴싱(GPU Instancing): 동일한 메시에 대해 여러 개의 객체를 렌더링할 때 GPU 인스턴싱을 사용하여 효율성을 높입니다.
- 재질(Material) 최적화: 가능한 한 재질과 쉐이더를 공유하여 드로우 콜을 최소화합니다.
3. 메쉬와 텍스처 최적화
- 폴리곤 수 감소: 모바일 기기의 제한을 고려하여 메쉬의 폴리곤 수를 줄입니다.
- LOD(Level of Detail) 사용: 객체의 거리에 따라 낮은 디테일의 메쉬를 사용하여 렌더링 부하를 줄입니다.
- 텍스처 압축: 텍스처의 해상도를 적절하게 설정하고 압축 형식을 사용하여 메모리 사용량과 로딩 시간을 줄입니다.
- ETC, ASTC 등 모바일 친화적인 텍스처 압축 형식을 사용합니다.
4. 쉐이더와 재질 최적화
- 모바일용 쉐이더 사용: 모바일에 최적화된 쉐이더를 사용하고 복잡한 셰이딩 계산을 피합니다.
- 셰이더 변종(Shader Variant) 관리: 사용하지 않는 셰이더 변종을 제거하여 빌드 크기를 줄이고 로딩 시간을 개선합니다.
- 조명 계산 최소화: 필요한 경우에만 실시간 조명을 사용하고, 가능하면 라이트맵(Lightmap)을 사용합니다.
5. 조명과 그림자 최적화
- 라이트맵 베이킹: 정적 객체에 대해 라이트맵을 베이킹하여 실시간 조명 계산 부하를 줄입니다.
- 실시간 그림자 최소화: 모바일에서는 실시간 그림자가 성능에 큰 영향을 미치므로 필요한 경우에만 사용합니다.
- 조명 수 제한: 씬에서 활성화된 실시간 조명의 수를 최소화합니다.
6. 물리 및 스크립트 최적화
- 물리 계산 최소화: 불필요한 물리 시뮬레이션을 피하고, Rigidbody와 Collider 사용을 필요한 곳에만 제한합니다.
- 업데이트 호출 최소화: MonoBehaviour의
Update()
,FixedUpdate()
호출을 최소화하고, 가능한 경우 이벤트 기반으로 변경합니다. - 가비지 컬렉션(Garbage Collection) 관리: 메모리 할당과 해제를 최소화하여 GC로 인한 프레임 드롭을 방지합니다.
7. UI 최적화
- Canvas 최적화: Canvas의 변경 범위를 최소화하고, 가능하면 여러 개의 Canvas로 분할합니다.
- UI 요소의 배치 관리: 불필요한 레이아웃 요소와 깊은 계층 구조를 피합니다.
- 동적 폰트 사용 자제: 동적 폰트는 메모리 사용량을 증가시키므로, 가능하면 정적 폰트를 사용합니다.
8. 카메라와 렌더링 설정 최적화
- 카메라 클리핑 평면 조절:
Near Clipping Plane
과Far Clipping Plane
값을 조절하여 불필요한 객체가 렌더링되지 않도록 합니다. - 오클루전 컬링(Occlusion Culling): 보이지 않는 객체를 렌더링하지 않도록 오클루전 컬링을 사용합니다.
- 스카이박스(Skybox) 사용 최소화: 스카이박스 렌더링은 성능에 영향을 미치므로 필요하지 않은 경우에는 단색 배경을 사용합니다.
9. 메모리 관리
- 리소스 언로드: 사용하지 않는 Assets를 적절히 언로드하여 메모리 사용량을 관리합니다.
- 애셋 번들(Asset Bundle) 활용: 필요한 리소스만 로드하고, 동적으로 로드 및 언로드할 수 있도록 애셋 번들을 사용합니다.
- 메모리 누수 방지: 이벤트 리스너와 델리게이트의 해제를 철저히 관리하여 메모리 누수를 방지합니다.
10. 빌드 설정 최적화
- IL2CPP 사용: 스크립트 백엔드를 IL2CPP로 설정하여 성능을 향상시킵니다.
- 스트립핑(Level Stripping): 사용하지 않는 엔진 코드를 제거하여 빌드 크기를 줄입니다.
- 최적화 레벨 설정: 코드 최적화 레벨을 설정하여 성능을 개선합니다.
11. 플랫폼별 최적화
- 안드로이드:
- 멀티스레딩 지원: 안드로이드에서 멀티스레딩을 활성화하여 CPU 활용도를 높입니다.
- 그레들(Gradle) 설정 최적화: 빌드 옵션에서 불필요한 기능을 제거합니다.
- iOS:
- 메탈(Graphics API) 사용: 가능하면 OpenGL ES보다 성능이 우수한 Metal API를 사용합니다.
- 해상도와 품질 설정 조정: 디바이스 성능에 따라 그래픽 품질과 해상도를 조절합니다.
12. 기타 최적화 기법
- 오버드로우(Overdraw) 감소: 투명한 객체의 사용을 최소화하고, 필요한 경우 Z-쓰기(Z-write)를 활용하여 오버드로우를 줄입니다.
- 애니메이션 최적화: 복잡한 애니메이션은 가능한 한 단순화하고, 애니메이션이 필요하지 않은 객체는 Animator를 제거합니다.
- 멀티 스레딩 활용: 작업 시스템(Job System)과 Burst Compiler를 사용하여 멀티코어 CPU의 성능을 최대한 활용합니다.
참고 자료
- Unity 공식 매뉴얼: 모바일 성능 최적화 가이드
- Unity Learn: 성능 최적화를 위한 베스트 프랙티스
위의 방법들을 적용하여 모바일 플랫폼에서 더 나은 성능과 사용자 경험을 제공할 수 있습니다. 최적화는 반복적인 과정이므로, 지속적인 프로파일링과 개선을 통해 최상의 결과를 얻으시기 바랍니다.