원문 : https://community.arm.com/developer/tools-software/graphics/b/blog/posts/mali-performance-5-an-application-s-performance-responsibilities

주의 : 번역이 개판이므로 이상하면 원문을 참고하세요.

주의 : 허락받고 번역한 것이 아니므로 언제든 내려갈 수 있습니다.


OpenGL ES 를 사용해 Mali GPU 상에서 그래픽스 렌더링 성능을 살펴 보는 블로그 시리즈의 다음 회에 오신 것을 환영합니다. 이번 시간에는 3D 애플리케이션을 개발 중일때 OpenGL ES 호출을 전혀 하지 않은 상태에서 고려할 중요한 애플리케이션 측 최적화중 일부에 대해서 살펴 볼 것입니다. 나중의 블로그에서는 OpenGL ES API 의 용례의 특정 영역들에 대해서 더 세부적으로 살펴 볼 것입니다. 이 블로그에 서술된 기술들은 Mali 전용이 아닙니다. 어떤 GPU 에서든 잘 동작할 것입니다.

With Great Power Comes Great Responsibility

OpenGL ES API 는 드로잉( drawing ) 연산( operation )들에 대한 직렬 스트림( serial stream )을 지정하는데, 그것들은 드로잉 연산들이 처리되는 방식을 제어하기 위한 명시적인 스테이트( state )들과 함께 GPU 가 수행해야 하는 하드웨어 커맨드( command )들로 바뀝니다. 이 연산의 저수준 모드는 애플리케이션에 GPU 가 렌더링 작업들을 수행하는 방식을 제어할 수 있도록 해 줍니다. 그러나 그것은 디바이스 드라이버가 애플리케이션이 렌더링하고자 하는 전체 씬에 대한 정보를 별로 가지고 있지 않다는 것을 의미하기도 합니다. 전체 정보의 결핍은 디바이스 드라이버가 GPU 로 보내는 커맨드 스트림을 재구성할 수 없어서, 최대 효율과 고성능을 달성하기 위해서는, 애플리케이션이 API 를 통해 적절한 렌더링 커맨드들을 순서대로 보내야 한다는 것을 의미합니다. 고성능 렌더링 및 최적화의 첫 번째 규칙은 "작업을 적게 하라" 는 것이며, OpenGL ES API 호출이 밠애하기 전에 애플리케이션에서 그 작업을 시작할 필요가 있습니다.

Geometry Culling

모든 GPU 들은 뷰잉 프러스텀( viewing frustum ) 외부에 존재하거나 카메라와 멀어지는 방향을 향하고 있는 프리미티브들을 버리는 컬링을 수행할 수 있습니다. 이것은 매우 저수준 컬링이며, 이는 프리미티브 별로 수행되고, 버텍스 셰이더가 각 버텍스를 위한 클립-스페이스( clip-space ) 좌표계를 계산한 이후에는 적용될 수 없습니다. 만약 전체 오브젝트가 프러스텀 바깥쪽에 존재한다면, 이것은 GPU 처리 시간, 메모리 대역폭, 에너지를 굉장히 낭비하고 있을 수 있습니다.

3D 애플리케이션이 수행해야 하는 가장 중요한 최적화는 최종 렌더링 결과에서 보이지 않는 오브젝트를 미리 컬링하는 것과 이런 오브젝트들을 위한 OpenGL ES API 호출을 완전히 건너뛰는 것입니다. 여기에서 사용될 수 있는 몇 가지 기법들이 있는데요, 복잡도에 따라서 다양합니다만, 밑에서 몇 가지 예에 대해서 설명하도록 하겠습니다.

Object Bounding Box

애플리케이션을 위한 가장 단순한 정책은 각 오브젝트를 위한 바운딩 박스를 유지하는 것입니다. 박스는 각 축에 대한 최소/최대 버텍스( vertex )들을 가집니다. 8 개의 버텍스를 오브젝트 스페이스에서 클립 스페이스로 계산하는 것은 CPU 상의 소프트웨어에서 각 드로 연산들에 대해서 계산될 수 있을 만큼 충분히 가볍고, 박스는 클립 스페이스 볼륨과의 간섭( intersection )을 위해 테스트될 수 있습니다. 이 테스트에 실패한 오브젝트는 프레임 렌더링에서 배제될 수 있습니다.

화면 공간에서 많은 양을 차지하는 매우 기하학적으로 복잡한 오브젝트들에 대해서는, 오브젝트를 각각의 바운딩 박스를 가지는 더 작은 조각으로 나누는 것이 유용합니다. 이는 현재 카메라 위치에서 이득을 볼 수 있다면 오브젝트의 일부 섹션( section )을 거부하도록 만들어 줍니다.

위의 이미지는 우리의 오래된 Mali 테크 데모중 하나를 보여 줍니다. 인도어( indoor ) 공상 과학 공간 역( station ) 환경을 날아 다닙니다. 최종 3D 렌더링은 왼쪽에 보입니다. 오른쪽의 칸텐트 디버그 뷰는 씬 안의 다양한 오브젝트들의 파랗게 강조된 바운딩 박스들을 보여줍니다.

Scene Hierarchy Bounding Boxes

이 바운딩 박스 정책은 추가로 취할 수 있는 것입니다. 이것은 렌더링되고 있는 월드를 위한 더 완벽한 씬 데이터 구조로 바뀝니다. 예를 들어 월드의 각 빌딩을 위해서 바운딩 박스들이 생성될 수 있으며, 각 빌딩의 각 방들을 위해서 바운딩 박스가 생성될 수 있습니다. 만약 빌딩이 화면 밖에 있다면, 그것은 단일 바운딩 박스 체크에 기반해 빠르게 배제될 것입니다. 빌딩이 포함하고 있는 개별 오브젝트들을 위한 수 백개의 검사가 필요하지 않습니다. 이 계층에서, 방들은 부모 빌딩이 가시적일 때만 검사되며, 그것들의 부모 방이 가시적일 때만 렌더링 가능한 오브젝트들이 테스트됩니다. 이러한 유형의 계층 정책은 실제로는 렌더링을 위해 GPU 로 보내질 워크로드를 변경하지는 않습니다. 하지만 이 검사들을 위한 CPU 성능 부하를 더욱 관리가능하게 만드는 데 도움을 줍니다.

Portal Visibility

많은 게임 월드들에서 뷰잉 프러스텀에 대한 단순한 바운딩 박스 검사들은 많은 중복된 작업들을 제거하게 될 것입니다. 하지만 여전히 제출해야 할 양이 엄청나게 남아 있습니다. 이는 특히 서로 연결된 방들로 구성된 월드에서 일반적입니다. 왜냐하면 공간적으로 인접한 방들의 뷰가 많은 카메라 각도로부터 벽, 바닥, 천장 등에 의해 완전히 차폐될 수 있습니다. 하지만 뷰잉 프러스텀 내부에 포함될 수 있을 정도로 충분히 가깝습니다( 그래서 단순한 바운딩 박스 컬링을 통과합니다 ).

그러므로 바운딩 박스 정책은 미리 계산된 가시성 정보를 사용해 보충될 수 있으며, 이는 씬 내의 오브젝트들에 대한 더 공격적인 컬링을 가능하게 합니다. 예를 들어 아래에 보이는 것처럼 세 개의 방으로 구성된 씬에서, Room C 내부의 어떠한 오브젝트도 플레이어가 서 있는 Room A 에서 보이지 않습니다. 그러므로 플레이어가 Room B 로 이동하기 전까지는 Room C 내부의 모든 오브젝트들에 대한 OpenGL ES 호출을 그냥 건너뛸 수 있습니다.

이러한 유형의 가시성 컬링은 레벨 디자이너( level designer )들에 의해 종종 게임 디자인 요소( factor )로 포함됩니다; 만약 레벨 디자인이 모든 지점에서 가시적인 방들의 개수를 항상 적게 유지할 수 있다면, 게임들이 더 높은 가시 품질과 프레임 율을 달성할 수 있습니다. 이러한 이유로, 인도어 설정들을 사용하는 많은 게임들은 S 와 U 모양의 방과 회랑들을 매우 많이 만듭니다. 왜냐하면 그것들은 문이 적절하게 배치된다면 그 방을 통과하는 시선( line of sight )이 없다는 것을 보장하기 때문입니다.

이 정책은 추가로 취할 수 있는 것입니다. 이는 어떤 경우에는 테스트 바닥에서 심지어 Room B 를 컬링할 수 있도록 해 줍니다. 이는 프러스텀에 대해서 현재 방과 인접한 방을 연결하는 포탈( portal ) -- 문, 창문 등 -- 들의 좌표를 테스트함으로써 수행됩니다. 만약 Room A 와 Room 를 연결하는 포탈이 현재 카메라 각도에서 보이지 않는다면, 우리는 Room B 에 대한 렌더링도 배제할 수 있습니다.

이러한 유형의 개괄적인( broad-brush ) 컬링 검사는 GPU 워크로드를 줄여주는 데 있어서 효율적이며, CPU 나 GPU 드라이버가 자동으로 수행하기에는 불가능합니다 -- 그것들은 렌더링되고 있는 씬에 대한 이러한 수준의 정보를 알 수가 없습니다 -- 그러므로 애플리케이션이 이러한 유형의 빠른 컬링을 수행하는 것이 중요합니다.

Face Culling

GPU 에 대해 화면 외부의 지오메트리들을 전송하지 않는 것 이외에도, 애플리케이션은 가시적인 오브젝트들을 위한 렌더링 스테이트들이 효율적으로 설정되는 것을 보장해야 합니다. 이는 컬링 목적을 위해 불투명 오브젝트들에 대한 후면( back-face ) 컬링을 활성화하는 것을 의미합니다. 이는 GPU 가 카메라로부터 멀어지는 방향을 향하는 삼각형들을 가능한 한 빠르게 배제할 수 있도록 해 줍니다.

Render Order

OpenGL ES 는 깊이( depth ) 버퍼를 제공하는데, 이것은 애플리케이션이 지오메트리를 순서대로 보내는 것을 허용해 주며, 깊이-테스트는 올바른 오브젝트들이 최종 렌더링 결과에 나올 수 있도록 해 줍니다. GPU 에 지오메트리를 순서 없이 렌더링할 수 있기는 하지만, 애플리케이션이 앞에서 뒤( front-to-back ) 순서로 오브젝트를 그린다면 더 효율적입니다. 왜냐하면 이것은 빠른 깊이 및 스텐실 테스트 유닛의 효율성을 극대화하기 때문입니다( The Mali GPU: An Abstract Machine, Part 3 - The  Midgard Shader Core 를 참고하세요 ). 만약 뒤에서 앞의 순서로 오브젝트를 렌더링한다면, GPU 가 일부 프래그먼트( fragment )를 렌더링하는 데 몇 사이클이 소비될 수 있습니다. 카메라에 더 가까운 프래그먼트들을 사용해 나중에 겹쳐그리는 것은 매우 많은 GPU 사이클들을 낭비하게 됩니다.

삼각형들이 완벽하게 정렬( sort )되는 것을 요구하는 것은 아닙니다. 그것은 CPU 사이클 관점에서 매우 무겁습니다; 그냥 거의 올바르게 만든다는 목적을 가지고 있을 뿐입니다. 바운딩 박스를 사용하거나 그냥 월드 스페이스에서의 오브젝트 원점 좌표를 사용하여 오브젝트 기반 정렬을 수행하는 것만으로도 보통 충분합니다; 삼각형들이 약간 순서에 맞지 않아도 GPU 에서 완전한 깊이 테스트에 의해서 깔끔히 정리될 것입니다.

블렌딩되는( blended ) 삼각형들은 뒤에서 앞의 순서로 렌더링되어야 올바른 블렌딩 결과를 얻을 수 있다는 것을 기억하세요. 그러므로 모든 불투명 지오메트리들을 앞에서 뒤의 순서로 먼저 렌더링한 후에 블렌딩되는 삼각형들을 나중에 렌더링할 것을 권장합니다.

Using Server-Side Resources

OpenGL ES 는 클라이언트-서버( client-server ) 메모리 모델을 사용합니다; 클라이언트측 메모리는 애플리케이션과 드라이버에 의해 소유되는 리소스들과 비슷하고, 서버측은 GPU 하드웨어에 의해 소유되는 리소스들과 비슷합니다. 리소스들을 애플리케이션으로부터 서버측으로 전송하는 것은 일반적으로 무겁습니다:

  • 드라이버가 데이터를 포함하기 위한 메모리 버퍼를 할당해야만 합니다.
  • 데이터가 애플리케이션 버퍼로부터 드라이버가 소유한 메모리 버퍼로 복사되어야 합니다.
  • 메모리가 메모리의 GPU 뷰와 일관성( coherent ) 있게 만들어져야 합니다. 통합( unified ) 메모리 구조에서 이는 캐시 유지( maintenance )를 내포하고 있을 것이며, 카드 기반 그래픽스 구조에서 이는 시스템 메모리로부터 전용 그래픽스 RAM 으로의 전체 DMA 전송을 의미할 것입니다.

그래서 초기 OpenGL 구현에서는 -- 지오메트리 처리는 CPU 에서 수행되고 GPU 하드웨어를 전혀 사용하지 않는 구현들에서는 -- OpenGL 과 OpenGL ES 는 지오메트리를 위한 클라이언트측 버퍼들이 API 에 넘겨지도록 하는 많은 API 들을 가지고 있었습니다.

  • glVertexAttributePointer 는 사용자가 버텍스당 애트리뷰트 데이터를 지정할 수 있도록 합니다.
  • glDrawElements 는 사용자가 드로 당 인덱스 데이터를 지정할 수 있도록 합니다.

이런 방식으로 지정되는 클라이언트측 버퍼들을 사용하는 것은 매우 비효율적입니다. 거의 대부분의 경우에 각 프레임에서 사용되는 그 모델들은 변경되지 않습니다. 그러므로 이것은 단순히 드라이버가 메모리를 할당하고 그 데이터를 그래픽스 서버로 전송하는 많은 양의 작업을 별 이득없이 수행하도록 강제합니다. OpenGL ES 의 훨씬 더 효율적인 대안( alternative )들은 애플리케이션이 버텍스 애트리뷰트와 인덱스 정보를 위한 데이터를 서버측 버퍼 오브젝트들에 업로드할 수 있도록 합니다. 이는 일반적으로 레벨 로드 시점에 수행됩니다. 버퍼 오브젝트를 사용할 대의 각 드로 연산에 대한 프레임 당 데이터 트래픽( traffic )은 그냥 GPU 에게 이들 버퍼 오브젝트들이 어디에서 사용되는 지를 알려주는 핸들( handle ) 집합일 뿐이며, 이는 명백히 더 효율적입니다.

이 규칙의 한 가지 예외는 유니폼 버퍼 오브젝트( Uniform Buffer Objects, UBOs )들입니다. 이것들은 셰이더 프로그램들에서 사용되는 드로 호출 당 상수들을 위한 서버측 저장소입니다. 유니폼 값들은 드로 호출 내부의 모든 버텍스 및 프래그먼트 스레드들에 의해 공유되므로, 그것들을 가능한 한 효율적으로 셰이더 코어에 의해 접근될 수 있도록 하는 것이 중요합니다. 그러므로 디바이스 드라이버들은 일반적으로 그것들이 하드웨어 접근 효율성을 최대화하기 위해서 메모리에 패킹( packed )되는 방식을 공격적으로 최적화할 것입니다. 서버측 UBO 들을 사용하는 대신에 드로 호출 당 작은 크기의 유니폼 데이터를 glUniform<x>() 함수 패밀리를 통해 설정하는 것이 선호됩니다. 왜냐하면 그것이 유니폼 데이터가 GPU 에 전달되는 방식에 대한 더 많은 제어를 제공하기 때문입니다. 유니폼 버퍼 오브젝트들은 여전히 버텍스 셰이더에서의 스켈레탈 애니메이션( skeletal animation )을 위해 사용되는 긴 행렬 배열과 같은 큰 유니폼 배열들을 위해서 사용됩니다.

State Batching

OpenGL ES 는 스테이트 기반 API 이며 각 드로잉 연산들을 위해 구성될 수 있는 매우 많은 스테이트 설정들을 가지고 있습니다. 단순하지 않은 씬들에서는 여러 개의 렌더 스테이트들이 사용됩니다. 그러므로 애플리케이션은 일반적으로 스테이트 변경 연산들을 수행해 드로 호출 자체가 제출되기 전에 각 드로 연산들을 위한 구성들을 설정합니다.

드라이버의 CPU 부하를 줄이고 GPU 에서 최적의 성능을 획득하려고 시도할 때 유념해야 할 두 가지 유용한 목표가 존재합니다:

  • 대부분의 스테이트 변경은 낮은 비용을 소비합니다만, 공짜는 아닙니다. 왜냐하면 드라이버가 에러 검사를 수행해야 하며 내부 데이터 구조에 어떤 스테이트를 설정해야 하기 때문입니다.
  • GPU 하드웨어는 상대적으로 큰 묶음( batch )의 작업을 다루도록 설계되어 있습니다. 그러므로 각 드로 연산은 상대적으로 커야만 합니다.

이 두 영역을 모두 개선하기 위한 애플리케이션 최적화의 가장 일반적인 형태 중 하나는 드로 콜 배칭( batching )입니다. 배칭을 통해 같은 렌더 스테이트를 사용하는 여러 개의 오브젝트들을 같은 데이터 버퍼로 미리 패키징하고, 단일 드로 연산을 사용해서 그것들을 렌더링할 수 있습니다. 이는 CPU 로드를 줄여 줍니다. 왜냐하면 더 적은 스테이트 변경과 드로 연산이 GPU 를 위해 패키징되어 있고 이는 GPU 가 더 큰 작업 묶음을 처리하도록 하기 때문입니다. 내 동료인 stacysmith 는 효율적인 배칭을 위한 전용 블로그를 가지고 있습니다: Game Set and Batch. 얼마나 많은 드로 호출들을 단일 프레임에 포함해야 하느냐에 대해서는 어렵거나 빠른 규칙이 존재하지 않습니다. 왜냐하면 대부분이 시스템 하드웨어의 능력( capability )과 원하는 프레임율에 의존하고 있기 때문입니다. 하지만 일반적으로 프레임 당 몇 백개의 드로 호출을 넘지 않는 것을 권장합니다.

또한 최적의 배칭을 하는 것과 깊이 정렬 및 컬링을 통해 중복 작업을 제거하는 것 사이에는 종종 충돌이 존재한다는 것을 기억하십시오. 드로 호출 카운트가 합리적이고 여러분의 시스템이 CPU 제약에 걸려있지 않다면, 일반적으로는 향상된 컬링과 front-to-back 오브젝트 렌더링 순서를 통해 GPU 워크로드를 줄이는 것이 더 낳습니다.

Maintain The Pipeline

이전의 블로그들 중 하나에서 언급했듯이, 최근의 그래픽스 API 들은 동기 실행에 대한 환상을 유지하지만 실제로는 성능을 극대화하기 위해 깊게( deeply ) 파이프라인화되어 있습니다. 애플리케이션은 이 렌더링 파이프라인의 동작을 저해하는 API 호출드을 사용하는 것을 피해야 합니다. 그렇지 않으면 성능은 급격하게 떨어지게 됩니다. 왜냐하면 CPU 는 GPU 가 종료될 때까지 대기하게 되고 GPU 는 CPU 가 더 많은 작업을 던져 줄 때까지 유휴상태가 되기 때문입니다. 이 주제에 대한 더 많은 정보를 얻기 원한다면 Mali Performance 1: Checking the Pipeline 을 참고하세요 -- it is was important to warrant and entire blog in its own right!

Summary

이 블로그에서는 Mali 를 사용하는 고성능 3D 렌더링을 성공적으로 성취하기 위해 고려해야만 하는 중요한 애플리케이션측 최적화들과 동작들에 대해서 살펴 보았습니다. 요약하면 기억해야 할 핵심들은 다음과 같습니다:

  • GPU 에다가 보일 수 있는 가능성이 있는 오브젝트들만 보내세요.
  • 불투명 오브텍트들을 앞에서 뒤의 순서로 렌더링하세요.
  • 서버측 데이터 리소스들은 버퍼 오브젝트들에 저장됩니다. 클라이언트측 리소스들은 그렇지 않습니다.
  • 렌더링 스테이트들을 배칭해서 불필요한 드라이버 부하를 줄이세요.
  • 깊은( deep ) 렌더링 파이프라인을 유지하고 애플리케이션이 파이프라인 고갈( drains )을 유발하지 않도록 하세요.

다음 시간에는 튜닝하고 OpenGL ES API 자체를 사용하는 더 기술적인 관점에 대해서 살펴 보기 시작하도록 하겠습니다. 그리고 코드 샘플들을 추가하도록 하겠습니다.

TTFN,

Pete

+ Recent posts