Blinn 과 Newell 은 1976 년에 reflection mapping( environment mapping 이라 불리기도 함 ) 이라는 기법을 소개했는데요, 이는 거울같은 서피스로부터의 반사를 시뮬레이션합니다. 그 공식은 다음과 같습니다[ 1 ].


식 1. Reflection Vector 계산


여기에서 N 은 서피스의 노멀( normal ) 벡터이며 V 는 서피스로부터 뷰어( viewer )를 향하는 벡터입니다. 그리고 R 은 반사 벡터입니다. N 과 V 는 모두 정규화( normalize )되어 있다고 가정합니다.


이 식을 처음 보면 상당히 이해하기 힘든데요, 그림을 그려서 관계를 살펴 보면 쉽게 이해할 수 있습니다. R 은 N 과 V 가 이루는 평면상에 존재하게 되므로 사실 이는 2D 수학입니다.


그림 1. Reflection Vector 계산 과정.


먼저 N 과 V 가 모두 정규화되어 있기 때문에 dot( N, V ) 는 VN 에 사영했을 때의 길이가 됩니다. 이게 마름모를 형성하고 있으므로 그것을 2 배 한 만큼 N 방향으로 밀어 낸 후에 -V 방향을 더하면 R 이 나옵니다.


그런데 셰이더를 작성할 때 이걸 일일히 계산할 필요는 없습니다. HLSL 내장함수로 reflect( I, N ) 라는 것이 존재합니다.


Blinn 과 Newell 의 공식과 차이가 있는데요, 뷰어와 노멀의 관계에서 반사 벡터를 구하는 것이 아니라, 입사( incident ) 벡터와 노멀의 관계에서 뷰어를 구한다는 것입니다.


식 2. HLSL reflect( i, n ) 의 계산식.


그림 1 처럼 직접 그려 보시면 이해가 갈 것이라 생각합니다. 자세한 내용은 [ reflect ] 에서 확인하세요.


참고자료


[ 1 ] 38p. TEXTURING & MODELING. A Procedural Approach. THIRD EDITION.

원문 : https://community.arm.com/developer/tools-software/graphics/b/blog/posts/mali-performance-7-accelerating-2d-rendering-using-opengl-es

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

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


성능 블로그를 쓴지 한참 지났습니다. 지난 밤동안 작성하고 싶은 블로그에 대한 점심 커피 토론 중 하나가 wasim abbas 가 만든 기술 데모가 되었습니다. 글을 다시 쓰게 만들어 준 그에게 감사드립니다. 이번에는 2D 렌더링과 OpenGL ES 가 도울 수 있는 부분에 대해서 살펴 보도록 하겠습니다.

매우 많은 모바일 칸텐트들은 여전히 2D 게이밍이거나 2D 유저 인터페이스 애플리케이션입니다. 애플리케이션들은 스크린상에 생성되는 스프라이트( sprite ) 레이어들이나 UI 요소( element )들을 렌더링합니다. 이런 거의 대부분의 애플리케이션들은 OpenGL ES 를 사용해 렌더링을 수행하는데요, 일부 애플리케이션들은 3D 기능들을 사용하며, 알파 투명도를 다루기 위해 블렌딩을 사용하는 전통적인 back-to-front 알고리즘의 단순함( simplicity )을 선호합니다.

이 접근법이 동작하기는 하지만, 하드웨어의 3D 기능들을 이용하지는 않습니다. 그래서 많은 경우에 GPU 는 필요 이상으로 더 빡세게( harder ) 작동합니다. 이것의 영향은 저성능부터 포함된 GPU 에 의존하는 배터리 수명까지 다양합니다. 그리고 이 영향들은 모바일 디바이스에서 고해상도를 향할수록 더 증폭되는 경향이 있습니다. 이 블로그는 스프라이트 렌더링 엔진들에 대한 간단한 변경사항들에 대해서 살펴 볼 것입니다. 그 엔진은 3D 렌더링 API 가 제공하는 도구들과 비교했을 때 렌더링을 훨씬 더 빠르게 만들어 주며, 에너지 효율이 더 높습니다.

Performance inefficency of 2D content

2D 게임들에서 OpenGL ES 프래그먼트 셰이더들은 보통 단순합니다( trivial ) -- 텍스쳐 좌표를 보간( interpolate )하고, 텍스쳐 샘플을 로드( load )하고, 프레임 버퍼에 블렌딩( blend )합니다 -- 그러므로 최적화할 부분이 많지 않습니다. 이러한 칸텐트 유형을 위한 성능 최적화는 거의 대부분 중복 작업을 완전히 없애는 방식을 찾는 것과 관련이 있습니다. 그러므로 일부 프래그먼트에서는 셰이더가 심지어는 아예 실행되지 않습니다.

소개 섹션에서의 그림은 배경 레이어에 위에서의 사각 스프라이트의 전형적인 블리트( blit, 역주 : 블록 데이터를 빠르게 메모리로 옮기거나 복사하는 기법 )를 보여 줍니다; 실드 스프라이트의 바깥 부분이 투명( transparent )이며, 경계 부분은 부분적으로 투명합니다. 그래서 배경으로 갈수록 에일리어싱( aliasing ) 아티팩트( artifact )없이 완전히 투명하게 사라집니다. 그리고 몸통 부분은 불투명( opaque )합니다. 이 스프라이트 블리트는 알파 블렌딩을 활성화한 상태로 프레임버퍼에서 back-to-front 렌더링 순서로 최상위에 렌더링됩니다.

여기에서 비효율성이 발생하는 두 가지 주요 원인이 존재합니다:

  • 먼저, 이 스프라이트를 둘러 싼 주변 영역들이 완전히 투명하므로, 그것은 출력 결과에 전혀 영향을 미치지 않고 있습니다. 하지만 처리하는데 시간은 걸립니다.
  • 둘째로, 스프라이트의 몸통 부분은 완전히 불투명하므로, 그 밑에 있는 배경 많은 픽셀들을 완전히 무시하고 있습니다. 그래픽스 드라이버는 배경이 무시되는 빠르게 알 수 없기 때문에, 이 배경 프래그먼트들이 GPU 에 의해 렌더링되어야만 합니다. 이는 최종 씬에 유용하게 영향을 주지 않는 뭔가를 렌더링하기 위해서 처리 사이클과 나노줄( nanojules )의 에너지를 낭비합니다.

이는 상대적으로 단일 오우버드로( overdraw ) 만이 존재하는 합성 예제이지만, 1080p 화면에서 렌더링된 프래그먼트가 절반 이상 중복되는 실제 애플리케이션을 볼 수 있습니다. 만약 애플리케이션들이 OpenGL ES 를 다른 방식으로 사용해 이 중복을 제거할 수 있다면, GPU 는 애플리케이션을 더 빠르게 렌더링할 수 있거나, clock rate 와 operating voltage 를 줄이기 위해서 생성된 성능 헤드룸( performance headroom )을 사용할 수 있을 것입니다. 즉 상당한 양의 에너지를 절약할 수 있습니다. 이런 결과들은 어느 하나라도 매력적입니다. 그래서 "애플리케이션 개발자가 이를 성취하기 위한 방법은 무엇인가?" 라는 질문을 할 수 있습니다.

Test scene

이 블로그를 위해서, 위의 실드 아이콘들을 cover-flow 스타일의 정렬로 구성된 간단한 테스트 씬을 렌더링할 것입니다. 하지만 그 기법은 불투명 영역을 가진 모든 스프라이트 셋에 대해 동작할 것입니다. 테스트 씬은 아래와 같이 렌더링합니다:

여기에서 각 실드 아이콘들은 실제로는 알파 투명도를 사용하여 보이지 않는 부분들을 가리는 사각 프라이트입니다.

Tools of the trade

전통적인 전용 2D 렌더링 하드웨어에는, 사용할 옵션들이 별로 없었습니다; 애플리케이션은 스프라이트 레이어를 back-to-front 로 렌더링해 블렌딩 함수가 올바로 작동하도록 보장해야만 했습니다. 우리의 경우에는 애플리케이션이 3D API 를 사용하여 2D 씬을 렌더링하며, 3D API 가 애플리케이션에 중복 작업 제거를 위해 제공하는 도구가 무엇이냐는 질문을 할 수 있습니다.

완전한 3D 씬을 렌더링하는 데 있어 중복 작업을 제거하기 위해 사용되는 기본 도구는 깊이( depth ) 테스트입니다. 삼각형 안의 모든 버텍스들은 그것의 위치에 "Z" 요소를 가지고 있으며, 이는 버텍스 셰이더로부터 노출됩니다. 이 Z 값은 버텍스와 카메라의 거리를 인코딩하며, 래스터화( rasterization ) 프로세스는 버텍스 값을 보간해서, 프래그먼트 셰이딩을 할 필요가 있는, 각 프래그먼트에 깊이를 할당합니다. 이 프래그먼트 깊이 값은 현재 깊이 버퍼에 저장된 현존하는 값들에 대해 검사될 수 있습니다. 만약 그 값이 프레임버퍼에 있는 현재 데이터보다 카메라와 가깝지 않다면, GPU 는 그 프래그먼트를 버리게( discard ) 되는데, 그려면 그것은 셰이더 코어에 제출되지 않습니다. 왜냐하면 그 프래그먼트가 불필요하다는 것을 안전하게 알고 있기 때문입니다.

Using depth testing in "2D" rendering

스프라이트 렌더링 엔진은 이미 각 스프라이트의 레이어링( layering )을 트래킹했습니다. 그래서 그것들은 올바른 순으로 쌓여( stack )있습니다. 그래서 우리는 이 레이어 번호를 GPU 에 보내진 각 스프라이트의 버텍스에 할당된 Z 좌표 값으로 매핑할 수 있습니다. 그리고 실제로 그것이 3D 깊이를 가진 것처럼 씬을 렌더링할 수 있습니다. 만약 깊이 어태치먼트( attachment )를 가진 프레임버퍼를 사용하고, 깊이-쓰기( depth-write )를 활성화하고, 스프라이트들과 배경 이미지를 front-to-back 순서로 렌더링한다면( 예를 들어 back-to-front 인 일반적인 블리팅 패스의 반대 순서로 ), 깊이 테스트가 스프라이트와 백그라운드의 다른 스프라이트에 의해 가려지는 부분을 제거할 것입니다다.

이를 우리의 간단한 테스트 씬을 위해 실행하면, 다음의 결과르 얻을 수 있습니다:

오! 뭔가 잘못되었습니다.

여기에서 문제는 사각 스프라이트 지오메트리가 정확하게 불투명 픽셀의 모양과 일치하지 않는다는 것입니다. 카메라와 더 가까운 스프라이트의 투명 부분이 알파테스트로 인해 어떠한 컬러 값도 생성하지 않고 있음에도 여전히 깊이 값을 설정하고 있습니다. 아래쪽 레이더의 스프라이트가 렌더링될 때, 깊이 테스트를 하면 이전의 스프라이트의 투명 부분의 밑에서 가시적이어야만 하는 부분들이 부적절하게 제거( kill )된다는 것을 의미합니다. 그래서 OpenGL ES 클리어 컬러만이 보이고 있습니다.

Sprite geometry

이 문제를 해결하기 위해서, 스프라이트를 위해 유용한 지오메트리를 설정하는 방법에 대한 연구를 좀 할 필요가 있었습니다. 스프라이트 내에서 완전히 불투명한 픽셀을 위해 front-to-back 으로 렌더링할 때 깊이 값을 안전하게 설정할 수 있습니다. 그래서 스프라이트 아틀라스 생성은 각 스프라이트를 위해 두 가지 셋( set )을 제공할 필요가 있습니다. 한 셋은 아래의 가운데 이미지에서의 녹색 영역으로 지정되어 있으며, 이는 불투명 지오메트리만을 다룹니다. 그리고 두 번째 셋은 아래의 오른쪽 이미지에서 녹색 영역으로 지정되어 있으며, 완전히 투명( 완전히 버려질 수 있는 경우 )하지 않은 모든 것들을 포함하고 있습니다.

버텍스들은 상대적으로 비쌉니다. 그래서 이런 지오메트리 셋을 생성할 때는 가능한 한 적은 개수의 부가적인 지오메트리들을 사용하십시오. 불투명 영역은 완전히 불투명한 픽셀들만을 포함해야 합니다. 하지만 투명 영역은 안전하게 불투명 픽셀들과 완전하게 투명한 픽셀들을 부수 효과( side-effect )없이 포함할 수 있습니다. 그러므로 "best fit" 를 획득하려고 시도할 필요없이 "good fit" 를 위해 대충 근사계산하십시오. 일부 스프라이트의 경우에는, 불투명 영역을 생성할 가치가 없다는 것을 기억하세요( 불투명 텍셀을 가지고 있지 않거나 매우 작은 영역만이 불투명할 때 ). 그래서 일부 스프라이트들은 투명 렌더링으로 렌더링될 단일 영역만으로 구성될 수도 있습니다. 경험적으로 볼 때, 불투명 영역이 256 픽셀보다 적을 때, 그것들은 부가적인 지오메트리 복잡성을 사용해 경계처리될 가치가 없습니다. 하지만 항상 시도하고 확인할 가치는 있습니다.

이런 지오메트리를 생성하는 것은 상대적으로 성가십니다. 하지만 스프라이트 텍스쳐 아틀라스들은 보통 정적이며 이것은 애플리케이션 칸텐트 저작 과정에의 일부로 오프라인으로 수행될 수 있습니다. 그리고 런타임에 플랫폼 상에서 라이브로 수행될 필요가 없습니다.

Draw algorithm

각 스프라이트에 대해 두 개의 지오메트리 셋을 사용하여, 이제 테스트 씬의 최적화된 버전을 렌더링할 수 있습니다. 먼저 불투명 스프라이트 영역들과 배경을 font-to-back 순서로 렌더링합니다. 이 때 깊이 테스팅과 깊이-쓰기를 활성화합니다. 그 결과는 다음과 같습니다: 

다른 스프라이트 밑에서 가려진 스프라이트나 백그라운드의 영역은 절약될 수 있는 렌더링 작업입니다. 왜냐하면 그것은 셰이딩이 발생하기 전에 빠른 깊이 테스트를 통해 제거되었기 때문입니다.

불투명 지오메트리를 렌덜이했다면, 이제 각 스프라이트의 투명영역을 back-to-front 순서로 렌더링할 수 있습니다. 깊이 테스트를 켜 둔 채로 남겨 놨기 때문에, 아래쪽 레이어들의 스프라이트들은 논리적으로 위쪽에 있는 레이어들의 불투명 영역을 덮어쓰지 않습니다. 하지만 조금이라도 전력을 절약하려면 깊이 버퍼를 비활성화하십시오.

만약 불투명 스테이지의 컬러 출력을 지워버리고 깊이 값을 유지하고 투명 패스를 그리면, 이 패스에 의해서 추가된 부가적인 렌더링 결과를 가시화할 수 있습니다. 이는 아래 그림처럼 보입니다:

링들 외부의 불완전한 영역들은 작업이 절약된 영역을 가리킵니다. 왜냐하면 없어진 부분들은 첫 번째 드로잉 패스에서 렌덜이된 카메라에 더 가까운 불투명 스프라이트 영역의 깊이 값을 사용하는 깊이 테스트에 의해 제거되었기 때문입니다.

두 패스를 모두 같은 이미지에 함께 배치하고 렌더링하면, 원래의 back-to-front 렌더링과 동일한 가시적 결과를 다시 얻을 수 있습니다:

하지만 35 % 정도 더 적은 프래그먼트 스레드들이 시작되었고, 이는 이 씬을 렌더링하기 위해서 필요한 MHz 의 35 % 를 줄인 것으로 해석됩니다. 성공적입니다!

필요로 하는 마지막 연산 논리( operational logic )는 우리가 씬에 추가한 깊이 버퍼가 메모리에 다시 쓰여지지 않음을 보장하는 것입니다. 만약 애플리케이션이 EGL 윈도우 서피스에 직접적으로 렌더링하고 있다면, 여기에서 할 일은 없습니다. 왜냐하면 윈도우 서피스를 위한 깊이가 묵시적으로 버려지기 때문입니다. 하지만 여러분의 엔진이 오프스크린 FBO 에 렌더링하고 있다면, 오프스크린 타깃으로부터 FBO 바인딩을 변경하기 전에 ( OpenGL ES 3.0 이상에서는 ) glInvalidFramebuffer() 이나 ( OpenGL ES 2.0 에서는 ) glDiscardFramebufferEXT() 에 대한 호출을 추가하는 것을 보장하십시오. 세부사항을 원한다면 이 블로그를 확인하세요.

Summary

이 블로그에서 우리는 깊이 테스팅과 깊이-지향( depth-aware ) 스프라이트 기법의 사용이 3D 그래픽스 하드웨어를 사용하는 렌더링을 가속하는데 사용되는 방법에 대해서 살펴 보았습니다.

스프라이트의 불투명 영역과 투명 영역 사이의 분리를 제공하기 위해 부가적인 지오메트리를 추가하는 것은 복잡도를 증가시킵니다. 그래서 스프라이트들을 위한 버텍스의 개수를 최소화하려고 해야 하며, 그렇지 않으면 부가적인 버텍스와 작은 삼각형 크기를 처리하는데 드는 비용이 이점을 없애버릴 것입니다. 부가적인 지오메트리가 너무 복잡해질 것이 요구되거나 화면을 덮고 있는 영역이 너무 작다면, 그냥 불투명 영역을 무시하고 전체 스프라이트를 투명인 것처럼 렌더링하십시오.

이 기법이 3D 게임에서 2D UI 요소들을 렌더링하기 위해서도 사용될 수 있다는 것을 언급할 가치가 있습니다. 3D 씬을 렌더링하기 전에 near clip plane 과 매우 가까운 깊이로 UI 의 불투명 부분을 깊이와 함께 렌더링하십시오. 그리고 나서 3D 씬을 일반적인 경우처럼 렌더링하십시오( 불투명 UI 요소들의 뒤에 있는 부분들이 건너뛰어지게 될 것입니다 ). 마지막으로 UI 의 투명 부분들이 3D 출력의 최상위에서 렌더링되고 블렌딩될 수 있습니다. 3D 지오메트리들이 UI 요소에 간섭하지 않는다는 것을 보장하기 위해서 glDepthRange() 를 사용해 3D 패스에 의해 노출되는 깊이 값들의 범위를 매우 약간 제한할 수 있습니다. 이는 UI 요소들이 3D 렌더링보다 항상 near clip plane 에 더 가깝도록 보장해 줍니다.

Tune in next time,

Pete.

원문 : https://community.arm.com/developer/tools-software/graphics/b/blog/posts/mali-performance-6-efficiently-updating-dynamic-resources

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

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


지난 번 블로그에서는 애플리케이션이 3D 칸텐트로부터 최적의 성능을 끌어낼 수 있도록 효율적으로 구현해야만 하는 중요한 영역들에 대해서 살펴 보았습니다. 씬의 큰 부분에 대한 개괄( Broad-Brush ) 컬링같은 것은 그 부분이 보이지 않음을 보장하기 때문에, 그것들이 GPU 에 전혀 보내지지 않게 됩니다. 이 블로그의 댓글 중의 하나에서 seanlumly01 님은 "드로 호출 사이에서 텍스쳐들을 수정하는 애플리케이션에 대해서는 성능 페널티가 없나요?" 라고 질문하셨습니다. 매우 좋은 질문입니다. 그것에 대한 답은 사소하지 않다( non-trivial )입니다. 그러므로 완전한 대답은 이 블로그 포스트로 연기해 놓았습니다.

Pipelined Rendering

리소스 관리를 해야 할 때 가장 중요한 것은 OpenGL ES 구현들은 거의 모두 심하게( heavily ) 파이프라인화되어 있다는 사실입니다. 이는 이전의 블로그에서 자세하게 언급되어 있지만, 요약을 하자면...

무언가를 그리기 위해서 glDraw...() 를 호출할 때, 그 드로는 즉시( instantly ) 이루어지는 것이 아닙니다. 대신에 GPU 에게 이 드로를 어떻게 수행해야 하는지를 알려 주는 커맨드가 연산 큐에 추가되며, 이것은 나중에 어떤 지점에서 수행됩니다. 이와 유사하게 eglSwapBuffers() 는 화면의 프론트 버퍼와 백 버퍼를 실제로 교환하는 게 아닙니다. 실제로는 그래픽스 스택( stack )에 애플리케이션이 렌더링과 렌더링을 표현하는 큐들에 대한 프레임 생성( composing )을 완료했음을 통보할 뿐입니다. 둘 다 행위에 대한 논리적인 명세 -- API 호출 -- GPU 상의 실제 작업 처리가 수십 밀리세컨드 길이일 수 있는 버퍼링 프로세스에 의해서 분리되어 있는 경우입니다.

Resource Dependencies

대부분의 경우에, OpenGL ES 는 동기화( synchronous ) 프로그래밍 모델을 정의합니다. 몇 가지 명시적인 예외들을 제외하고는, 드로 호출을 만들 때, 렌더링은 드로 호출이 만들어진 지점에서 발생하는 것처럼 보여야 하며, ( API 함수 호출과 이전에 지정된 GPU 커맨들에 기반해 ) 그 시간과 지점에 화면상의 모든 커맨드 플래그들, 텍스쳐들, 버퍼들의 상태를 올바로 반영하는 것처럼 보여야 합니다. 동기화 렌더링처럼 보여지는 것은 API 의 기저에 깔린 드라이버 스택이 만드는 정교한 환상입니다. 그것은 잘 동작하지만, 여러분이 최적의 성능과 최소한의 CPU 부하를 획득하고자 한다면 애플리케이션의 동작에 약간의 제약이 걸립니다.

앞에서 언급한 파이프라인 프로세스 때문에, 이 동기화 환상은 텍스쳐나 버프를 읽어들이는 대기중인( pending ) 드로 호출이 GPU 상에서 실제로 그 드로 호출에 대한 렌더링이 완료될 때까지 그 리소스에 대한 수정 잠금( modification lock )을 유지해야 한다는 것을 의미합니다.

예를 들어 다음과 같은 코드 시퀀스를 가진다고 합시다:

그러면 첫 번째 드로 호출이 실제로 GPU 에 의해 처리가 되기 전까지는 glTexSubImage2D() 가 텍스쳐 메모리를 수정할 수 없습니다. 그렇지 않으면 첫 번째 렌더링 호출이 API 호출이 만들어진 지점에서의 GL 에 대한 스테이트( state )를 올바로 반영하지 못할 것입니다( 텍스쳐 버전 2 가 아니라 텍스쳐 버전 1 을 반영하는 물리 메모리의 칸텐츠를 사용해 드로를 렌더링할 필요가 있습니다 ). OpenGL ES 드라이버가 소비하는 시간의 많은 부분들은 이처럼 동기화 프로그래밍 "환상"을 유지하고 그 연산들이 너무 빠르거나( 리소스가 이용가능하기 전에 ) 느리게( 리소스 수정이 이미 된 후에 ) 발생하지 않도록 보장하기 위해 리소스 종속성들을 트래킹( tracking )하는 데 사용됩니다.

Breaking Resource Dependencies

리소스 종속성 충돌이 발생하는 시나리오에서 -- 예를 들어 여전히 대기중인 읽기 잠금( read lock )을 가지고 있을 때 버퍼 쓰기가 요구되는 경우 -- Mali 드라이버들은 어떤 특별한 처리 없이는 리소스 수정을 즉시 적용할 수 없습니다; 여기에 충돌을 자동으로 해결하기 위해 드라이버들에 열려 있는 여러 개의 가능한 방법들을 정리했습니다.

Pipeline Finish

충돌된 리소스들을 해결하기 위해, 대기중인 모든 GPU 에 대한 읽기와 쓰기를 하는 곳에서, 렌더링 파이프라인을 비울( drain ) 수 있습니다. 그 피니시( finish )가 완료된 후에, 리소스의 수정을 일반적인 것처럼 처리할 수 있습니다. 만약 이러한 일이 프레임버퍼 드로잉을 통해 발생한다면, 중간( intermediate ) 렌더링 스테이트를 메인 메모리에 강제로 플러싱( flush )하도록 하는 곳에서 점증적( incremental ) 렌더링 비용이 발생합니다; 세부사항에 대해서 더 알고자 한다면 이 블로그를 참고하세요.

파이프라인을 완전히 비우는 것은 CPU 가 다음 워크로드를 생성하는 동안 GPU 가 유휴상태가 된다는 것을 의미합니다. 이는 하드웨어 사이클을 낭비하는 것이며, 경험적으로 볼 때 좋지 않은 해결책입니다.

Resource Ghosting

약간의 메모리를 더 낭비한다면 동기화 프로그래밍 모델의 환상과 애플리케이션의 즉시 업데이트 처리를 모두 유지할 수 있습니다. ( 만약 그 변경이 부분 버퍼와 텍스쳐만을 대체하는 것이라면 ) 현재 리소스 메모리의 물리 칸텐츠를 수정하지 않고, 응용프로그램의 업데이트와 원본 버퍼로부터 데이터를 조립함으로써, 새로운 버전의 논리 텍스쳐 리소스를 간단하게 생성할 수 있습니다. 리소스의 최신 버전은 API 레벨에서 모든 연산을 위해 사용되며, 예전 버전들은 대기중인 렌더링이 완료될 때까지만 필요합니다. 이 시점에서 그것들의 메모리를 해제할 수 있습니다. 이러한 접근법은 리소스 고스팅( ghosting ) 혹은 copy-on-write 라고 알려져 있습니다( 역주 : Copy-On-Write 기법에 대해서는 [ [Study] Copy On Write (COW) ] 라는 포스트를 참고하세요 ).

이는 드라이버들이 가장 일반적으로 사용하는 접근법입니다. 왜냐하면 그것은 파이프라인을 온전히 유지하면서 GPU 하드웨어를 바쁘게 유지할 수 있도록 보장하기 때문입니다. 이 접근법의 단점은 고스트 리소스들이 살아 있는 동안의 추가적인 메모리들과 새로운 버전의 리소스를 메모리에서 할당하고 조합하기 위한 일부 추가적인 처리 로드( Processing load )입니다.

기억할 점이 하나 더 있는데요, 리소스 고스팅이 항상 가능한 것은 아니라는 것입니다; 특히 리소스들이 UMP, Gralloc, dma_buf 같은 메모리 공유 API 를 사용해서 외부 소스로부터 임포트( import )될 때입니다. 이런 경우에, 카메라, 비디오 디코더, 이미지 프로세서같은 다른 드라이버들이 이런 버퍼에다가 쓰기작업을 할 수 있으며, Mali 드라이버는 그런 작업이 발생했는지 여부를 알 방법이 없습니다. 이런 경우에, 일반적으로는 copy-on-write 메커니즘( mechanism )을 적용할 수 없습니다. 그래서 드라이버는 이 문제를 해결하기 위해서 대기중인 종속성들을 위해 멈춰서 대기하는 경향이 있습니다. 대부분의 애플리케이션의 경우에 이런 부분에 대해서 걱정할 필요는 없습니다. 하지만 다른 미디어 가속기( accelerator )로부터 가지고 온 버퍼들을 가지고 작업할 때는 주의해야 합니다.

Application Overrides

파이프라인 깊이 때문에 모든 하드웨어 렌더링 시스템들 상에서 리소스 종속성들이 문제가 된다는 점을 감안하면, 최신 버전의 OpenGL ES 에는, 응용프로그램 개발자가 필요할 때 순수하게 동기화 렌더링 환상을 재정의해( override ) 더욱 세부적으로 제어할 수 있도록 하는, 기능들이 추가되었다는 사실이 별로 놀랍지 않습니다.

OpenGL 3.0 의 glMapBufferRange() 함수는 애플리케이션 개발자가 버퍼를 애플리케이션의 CPU 주소 공간으로 매핑할 수 있도록 해 줍니다. 버퍼를 매핑하는 것은 애플리케이션이 GL_MAP_UNSYNCHRONIZED_BIT 라는 접근 플래그를 지정할 수 있도록 하는데, 이는 대충 해석하면 "리소스 종속성은 신경 꺼. 나는 내가 뭘 하고 있는지 알고 있어" 비트입니다. 버퍼 매핑이 비동기화되면, 드라이버는 동기화 렌더링 환상을 강제하려 시도하지 않으며, 애플리케이션은, 대기중인 렌더링 연산으로부터 여전히 참조되고 있는, 버퍼 영역들을 수정할 수 있습니다. 그러므로 버퍼 업데이트가 에러를 만든다면, 그러한 연산들을 위한 렌더링이 잘못될 수 있습니다.

Working With Resource Dependencies

GL_MAP_UNSYNCHRONIZED_BIT 와 같은 기능들을 직접적으로 사용하는 것과 더불어, 많은 애플리케이션들은, 지나친 고스팅 부하를 발생시키지 않고 유연한 렌더링을 생성하기 위해 파이프라인화된, 리소스 용법( usage )에 대한 지식을 사용해 작업하고 있습니다.

Seperate Out Volatile Resources

정적 리소스와 휘발성( volatile ) 리소스들이 분리되어 있음을 보장함으로써, 고스팅을 덜 비싸게 수행할 수 있습니다. 이는 할당되거나 복사될 필요가 있는 메모리 영역을 가능한 한 작게 만듭니다. 예를 들어, glTexSubImage2D() 를 사용해서 업데이트되는 애니메이션되는 글자( glyph )가 절대 변경되지 않는 정적 이미지들을 사용하는 텍스쳐 아틀라스를 공유하지 않음을 보장하거나, CPU 상에서 ( 애트리뷰트나 인덱스 업데이트를 통해 ) 소프트웨어적으로 애니메이션되는 모델이 정적 모델들과 같은 버퍼 안에 존재하지 않음을 보장하는 것입니다.

Batch Updates

이상적으로 FBO 렌더링이 발생하기 전에 대부분의 리소스 업데이트를 단일 블록( block )에서 수행( 하나의 큰 업데이트나 다중의 연속된 서브 버퍼/텍스쳐 업데이트들 )함으로써, 버퍼 업데이트와 관련된 부하를 감소시키고 고스팅된 복사의 개수를 최소화할 수 있습니다. 다음과 같이 드로 호출에 리소스 업데이트를 삽입하는 것을 피하십시오...

... GL_MAP_UNSYNCHRONIZED_BIT 를 사용할 수 있지 않는다면 말입니다. 이걸 다음과 같이 변경하는 것이 보통 훨씬 효율적입니다: 

Application Pipelined Resources

애플리케이션의 성능을 더욱 예측 가능하게 만들고, 드라이버에서 메모리를 재할당하는 고스팅의 부하를 피하고자 한다면, 적용할 만한 기법이 하나 있습니다. 렌더링 파이프라인에 존재하는 각 지연 시간 프레임마다 애플리케이션에서 각 휘발성 리소스들의 다중 복사본을 명시적으로 생성하는 것입니다( 일반적으로 Android 같은 시스템에 대해서는 3 개 입니다 ). 이 리소스들은 라운드-로빈( round-robin ) 순서로 사용되므로, 리소스에 대한 다음 변경이 발생할 때는 그 리소스를 사용하는 대기중인 렌더링이 완료되어야 합니다. 이는 애플리케이션의 변경들이 드라이버에서의 특별한 처리를 요구하지 않고 물리 메모리에 직접적으로 제출( commit )될 수 있음을 의미합니다.

애플리케이션의 파이프라인 길이를 결정하는 쉬운 방법은 없습니다. 하지만 텍스쳐를 사용하는 드로 호출 뒤에다가 glFenceSync() 를 호출함으로서 펜스 오브젝트를 삽입하고, N 프레임 후에 변경을 만들기 바로 직전에 glClientWaitSync() 를 호출함으로써 펜스 오브젝트를 빼내는 식으로, 디바이스에서 경험적으로 테스트해 볼 수는 있습니다. 만약 GL_TIMEOUT_EXPIRED 를 반환한다면, 렌더링이 여전히 대기중인 것이고, 여러분이 사용하고 있는 리소스 풀에다가 추가적인 리소스 버전을 추가할 필요가 있습니다.

좋은 질문을 찾아 준 Sean 에게 감사드립니다. 이게 대답이 되었으면 좋겠네요!

Cheers,

Pete

원문 : 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

원문 : https://community.arm.com/developer/tools-software/graphics/b/blog/posts/mali-performance-4-principles-of-high-performance-rendering

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

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


이 시리즈의 이전 블로그에서, 타일 기반 렌더링 구조를 가능한 한 효율적으로 사용하기 위한 기본적인 필수 항목들에 대해서 살펴 보았습니다. 특히 불필요한 메모리 접근을 최소화하기 위해서 가장 효율적으로 프레임버퍼를 사용하는 애플리케이을 만드는 방법에 대해서 보여 주었습니다. 그런 기본들에서 벗어나서, 필자는 이제 OpenGL ES API 를 가장 효율적으로 사용해 Mali 를 사용하는 플랫폼에서 최상의 결과를 획득하는 방법의 세부사항에 대해서 살펴 보기 시작할 겁니다. 하지만 그렇게 하기 전에, 성능 최적화의 5 개 원칙에 대해서 소개하자 합니다.

Principle 1: Know Your Goals

최적화를 시작할 때, 그 활동은 끝내는 시점에 대한 매우 명확한 목표들을 가지고 있어야 합니다. 최적화에 대한 많은 목표들이 존재합니다; 가장 일반적인 것들을 이야기하자면 더 빠른 성능, 더 낮은 전력 소비, 더 낮은 메모리 대역폭, 더 낮은 CPU 부하 등이 있습니다. 애플리케이션을 리뷰할 때 보게되는 문제의 종류들은 여러분이 해야 할 개선의 종류에 따라서 매우 다양합니다. 그러므로 이를 시작부터 정확하게 하는 것이 매우 중요합니다.

또한 작은 성취를 위해 점점 더 많은 시간을 소비하기 쉽상입니다. 그리고 많은 최적화들이 애플리케이션의 복잡도를 증가시키고 유지보수 문제를 발생시킬 것입니다. "이제 충분해" 라고 이야기할 때를 결정하기 위해서, 작업을 하는 동안 정기적으로 진척도를 리뷰하고, 이 시점에 도달하면 작업을 중단하세요.

Principle 2: Don't Just Try to Make Things Fast

필자는 종종 Mali 를 사용하는 개발자들로부터 칸텐트의 특정 부분을 더 빠르게 실행할 수 있는 방법에 대해서 알려달라는 질문을 받습니다. 이러한 유형의 질문은 더 세부적인 질문들로 이어지게 마련입니다. 셰이더 코드의 특정 부분으로부터 좀 더 나은 성능을 짜내려면 어떻게 해야 하는지, Mali 구조에 가장 들어 맞는 특정 지오메트리( geometry ) 메시( mesh )를 튜닝( tune )하려면 어떻게 해야 하는지 등이 있습니다. 이런 것들은 모두 유효한 질문들이기는 하지만, 제 생각에는 프로세스 초기에 최적화 활동 범위를 지나치게 좁혔고, 가장 유망한 공격 수단들을 살펴 보지 않은 채 남겨둔 것으로 보입니다.

두 질문 모두 고정 워크로드들을 최적화하려고 시도하는 것이며, 둘 다 워크로드가 충분하다는 가정을 내포하고 만들어진 것입니다. 실제 그래픽스에서는 씬들이 종종 엄청난 양의 중복 - 화면을 벗어난 오브젝트들, 다른 오브젝트에 의해서 겹쳐 그려지는 오브젝트들, 삼각형의 절반이 사용자가 볼 수 없는 곳을 향하는 오브젝트들 등 - 을 포함하며, 그것들은 최종 렌더링에 어떠한 기여도 하지 않습니다. 그러므로 최적화 활동들은 두 개의 기본적인 질문들에 대답하려고 시도할 필요가 있습니다:

  1. 어떻게 씬으로부터 중복 작업들을 가능한한 효율적으로 줄일 수 있을까?
  2. 어떻게 남아 있는 것들에 대한 성능을 미세 조정( fine tune )할 수 있을까?

요약하면 - 무엇인가를 마냥 빠르게 만들려고 시도하지 말고, 가능할 때마다 그렇게 하는 것을 피하려고 시도하십시오! 이런 "작업 회피( work avoidance )"들의 일부는 애플리케이션에서 전적으로 처리되어야 하지만, 많은 경우에 OpenGL ES 와 Mali 는 그것들을 올바로 수행하는 데 도움을 줄 수 있는 도구들을 제공합니다. 뒤쪽의 블로그에서 이에 대해서 더 다루도록 하겠습니다.

Principle 3: Graphics is Art not Science

여러분이 CPU 상에서 전통적인 알고리즘을 최적화하고 있다면, 그것은 보통 올바른 답변이지만, 그렇지 않은 시스템에서는 잘못된 답일 것입니다. 그래픽스 워크로드의 경우에, 우리는 단순하게 가능한한 빠르게 보기 좋은 그림을 생성하려고 시도합니다; 만약 최적화된 버전이 정확하지 않아도 사람들은 알아 차리지 못할 것입니다. 그러므로 성능에 도움이 된다면 알고리즘을 사용하는 것을 두려워하지 마십시오.

그래픽스를 위한 최적화 활동은 사용된 알고리즘을 살펴 봐야 합니다. 그리고 그것의 비용이 그것이 가져오는 가시적인 이점들에 비해 합당하지 않다면, 그것을 없애버리고 완전히 다른 알고리즘으로 대체하는 것을 두려워하지 마십시오. 실시간 렌더링은 아트 형식이며 최적화와 성능은 그 아트의 일부입니다. 많은 경우에 부드러운 프레임율( framerate )과 빠른 성능이 단일 프레임에 포함되는 약간의 디테일( detail )보다는 더 중요합니다.

Principle 4: Data Matters

GPU 들은 데이터 영역( data-plane ) 프로세서( 역주 : [ Single-Chip Control/Data-Plane Processors ] 참조 )들이며, 그래픽스 렌더링 성능은 종종 데이터 영역 문제에 의해 지배됩니다. 많은 개발자들은 문제를 확인하기 위해서 OpenGL ES API 함수 호출 시퀀스들을 살펴 보느라 많은 시간을 허비하는데, 그들이 그런 함수들에 넘긴 데이터를 살펴 보지는 않습니다. 이는 거의 항상 심각한 실수가 됩니다.

물론 OpenGL ES API 호출 시퀀스들은 중요합니다. 그리고 많은 로직 이슈들이 최적화 작업을 하는 동안 이것들을 살펴 봄으로써 발견될 수 있습니다. 하지만 데이터 애샛( asset )의 포맷( format ), 크기, 패킹( packing )이 매우 중요하며, 무엇인가를 더 빠르게 만들기 위한 기회를 살필 때 잊어서는 안 되는 것입니다.

Principle 5: Measure Early, Measure Often

씬 렌더링 성능에서 단일 드로( draw ) 호출의 영향은 API 레벨에서 이야기하는 것은 보통 불가능합니다. 그리고 많은 경우에 무해하게 보이는 드로 콜들이 종종 큰 성능 부하의 일부가 되기도 합니다. 필자는 많은 성능 팀들이 며칠 혹은 심지어는 몇 주 동안 최적화를 하는데 시간을 보내고 나서야 뒤늦게 그들이 튜닝하던 셰이더가 단지 전체 씬의 비용 중의 1 % 밖에 기여하지 않음을 깨닫는 것을 보아 왔습니다. 그래서 그들이 2 배 더 빠르게 만드는 환상적인 작업을 했지만, 전체 성능은 0.5 % 만이 개선되었습니다.

필자는 항상 DS-5 Streamline 과 같은 툴을 사용해 일찍 그리고 자주 측정하는 것을 권장합니다. DS-5 Streamline 을 사용하면 통합 하드웨어 성능 카운터들을 통해 GPU 하드웨어 성능의 정확한 관점을 획득할 수 있으며, Mali Graphics Debugger 를 사용하면 렌더링 워크로드에 기여하는 드로 호출이 무엇인지 찾을 수 있습니다. 최적화 주요 지점( hot spot )을 식별하는 것 뿐만 아니라 애플리케이션이 여러분이 원하는 대로 동작하고 있는지 확인하기 위한 새너티( sanity ) 체크를 하기 위해서도 성능 카운터를 사용할 수 있습니다. 예를 들어 프레임 당 픽셀( pixel ) 개수, 텍셀( texel ) 개수, 메모리 접근 횟수들을 수동으로( 역주 : 애플리케이션에 삽입한 코드를 이야기하는 듯 ) 수집하고 이를 하드웨어의 카운터들과 비교하십시오. 만약 기대했던 것보다 2 배 많은 렌더링된 픽셀들을 확인했다면, 셰이더를 튜닝하는 것보다 훨씬 더 많은 이점을 주는 먼저 조사해야만 하는 구조적인 이슈들이 존재할 수도 있습니다.

Next Time

그래픽스에서 최적의 성능은 애플리케이션이 OpenGL ES API 에 데이터를 제출하는 방식의 구조적인 부분을 만들 때 가장 방해를 받습니다, so in my next blog I will be looking at some of the things an application might want to consider when trying very hard to not do any work at all.

TTFN,

Pete

+ Recent posts