원문 : https://community.arm.com/developer/tools-software/graphics/b/blog/posts/mali-performance-3-is-egl_5f00_buffer_5f00_preserved-a-good-thing

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

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


이번 주에 필자는 EGL_BUFFER_PRESERVED 를 통해 애플리케이션 프레임버퍼 관리의 영역으로의 전환( diversion )과 어디에서 이 기법을 사용하면 좋을지 결정하는 방법에 대해서 이야기하는 것을 마무리하고 있습니다. 이것은 사용자 인터페이스( user-interface ) 개발과 관련해 고객과 이야기할 때 정기적으로 제기되는 질문이며, 다른 그래픽스 요소들과 마찬가지로 그것의 효율성은 여러분이 수행중인 작업에 달려 있기 때문에, 이 블로그가 그것들을 명확하게 ( 혹은 적어도 약간 덜 혼란스럽게( murky ) ) 만들어 줄 수 있기를 바랍니다.

What is EGL_BUFFER_PRESERVED?

이전 블로그인 [ Mali Performance 2: How to Correctly Handle Framebuffers ] 에서 설명했듯이, 일반적인 상황에서는 윈도우 서피스의 칸텐츠가 한 프레임에서 다음 프레임으로 보존되지 않습니다. Mali 드라이버는 프레임버퍼( framebuffer )의 칸텐츠가 버려진다( discard )고 가정합니다. 그러므로 컬러( color ), 뎁스( depth ), 스텐실( stencil ) 버퍼의 어떠한 스테이트( state )도 유지될 필요가 없습니다. EGL 명세에서는 기본 EGL_SWAP_BEHAVIOR 를 EGL_BUFFER_DESTROYED 로 지정하고 있습니다.

윈도우 서피스를 EGL 을 통해서 생성할 때, 서피스를 EGL_SWAP_BEHAVIOR 를 EGL_BUFFER_PRESERVED 로 구성해서 생성할 수 있습니다. 이는 프레임버퍼의 컬러 데이터가 프레임 N 의 렌더링 끝에서 프레임 N+1 의 렌더링을 위한 컬퍼 버퍼의 시작 컬러로 사용된다는 것을 의미합니다. 이러한 보존은 컬러 버퍼에만 적용된다는 점을 기억하십시오; 뎁스 버퍼와 스텐실 버퍼는 보존되지 않으며, 그것들의 값은 모든 프레임의 끝에서 소실됩니다.

Great, I can render only what changed!

대부분의 사람들이 하는 일반적인 실수는 이 기법이 현존하는 프레임버퍼에 대한 적은 양의 렌더링만을 패치( patch )할 수 있도록 해 준다고 믿는 것입니다. 만약 이전 프레임 이후로 시계의 1 초만이 화면에서 변경되었다고 하면, 그냥 태스크바( taskbar )에서 시계를 수정하기만 하면 될 것입니다. 맞습니까? 아닙니다!

대부분의 실제 시스템들은 N-버퍼링 렌더링 체계( scheme )를 실행하고 있다는 점을 기억하십시오. 일부는 더블( double ) 버퍼링지만, 트리플( triple ) 버퍼링이 일반적이 되고 있습니다. N+1 프레임을 렌더링할 때의 최상위에 추가하고 있는 메모리 버퍼는 N 프레임의 컬러 버퍼가 아니라 N-2 프레임의 컬러 버퍼일 것입니다. EGL_BUFFER_PRESERVED 는, 단순한 패치 연산을 수행하는 것이 아니라, 드라이버로 하여금, 프레임 N 의 컬러 버퍼를 포함하는, 텍스쳐를 적용한( textured ) 사각형을 프레임 N+1 을 위해 작업중인 타일( tile ) 메모리에 렌더링하게 만드는 것입니다.

이전 블로그 중의 하나와 seanellis 의 블로그의 Forward Pixel Kill( FPK )에서 언급했듯이, Mali GPU 패밀리의 최근 멤버들은 겹쳐 그려지는( overdrawn ) 프래그먼트( fragment )들이 GPU 에 대한 주요한 비용이 되기 전에 제거하기 위한 기능을 지원하고 있습니다. 이전 프레임의 최상위에서 겹쳐 그려지는 것이 불투명( 블렌딩( blending ) 이 없고 프래그먼트 셰이더가 "discard" 를 호출하지 않음 )이라면, 겹쳐 그려지는 부분에 대한 리드백( readback )이 억제될 수 있으며, 결과적으로 성능이나 대역폭에 대한 영향이 없어집니다. 또한, EGL_BUFFER_PRESERVED 가 활성화되어 있지만 모든 것을 겹쳐 그리고 싶다면, 그냥 프레임의 렌더링의 시작 부분에서 glClear() 를 호출해서 리드백을 전혀 하지 않도록 할 수 있습니다. 

Is EGL_BUFFER_PRESERVED worth using?

전체 화면 리드백의 필요성을 받아들이면 멀티 프레임 렌덜이 파이프라인의 개념을 생각하기 시작할 때 상대적으로 직관적이 됩니다. 다음 질문은 "사용자 인터페이스 애플리케이션에 EGL_BUFFER_PRESERVED 를 사용해야 하느냐" 는 것입니다.

많은 가치있는 엔지니어링( engineering ) 질문들처럼, 그 대답은 단순하게 "네" 혹은 "아니요" 가 되기보다는 "그때 그때 달라요" 에 가깝습니다.

올바른 시작 컬러를 사용하는 프레임을 생성하기 위한 EGL_BUFFER_PRESERVED 의 비용은 ( FPK 에 의해서 건너 뛰는( killed ) 경우를 제외하면 ) 이전 프레임 데이터에 대한 전체 화면( full-screen ) 로드( load )입니다. 다른 대안은 클리어( clear ) 컬러를 사용해 프레임을 처음부터 다시 렌더링하는 것입니다. EGL_BUFFER_PRESERVED 를 사용하는 것이 좋을지의 여부는 다음의 두 가지 상대적인 비용에 달려 있습니다:

  • 만약 UI 애플리케이션이 투명도를 많이 사용하는 다중의 비압축 레이어들로 구성되어 있다면, EGL_BUFFER_PRESERVED 가 합리적일 것입니다. 하나의 레이어에서 이전 컬러 데이터를 리드백하는 비용은 다중 레이어 + 블렌딩 루트( route )를 통해 처음부터 컬러를 재생성하는 것보다 훨씬 저렴할 것입니다.
  • 만약 대부분이 단일 레이어이며 압축된 텍스쳐를 읽어들이는 단순한 UI 나 2D 게임을 가지고 있다면, EGL_BUFFER_PRESERVED 는 잘못된 것입니다. 이전 프레임의 컬러를 리드백하기 위한 대역폭 부하가 처음부터 프레임을 생성하는 것보다 훨씬 비쌀 것입니다.

이를 항상 칼로 자르듯이 명확하게 나누는 것은 불가능합니다 -- 이 두 개의 극단 사이에는 중간 지대가 존재합니다 -- 그러므로 분석을 수행할 때 주의를 기울여야 합니다. 만약 의심할 여지가 있다면, 제품 플랫폼에서 실행중인 실제 애플리케이션에서, EGL_BUFFER_PRESERVED 를 껐다 켰다 하면서, GPU 성능 카운터들을 사용해 성능을 리뷰( review )하십시오. 실제 장치에서 실제의 경우를 측정하는 것보다 더 나은 답을 주는 것은 없습니다. 이 시리즈의 다른 블로그에서는 애플리케이션 성능 분석을 하는 가이드를 제공하며 앞으로 몇 달 동안 계속해서 이 영역에 더욱 많은 내용들을 추가하도록 하겠습니다.

그런데, 그런 성능 경험을 수행할 때, 최적의 애플리케이션은 EGL_BUFFER_PRESERVED 를 명시적으로 사용( 혹은 비사용 )하도록 설계되어 있는 것이 중요하다는 점을 기억하십시오; 어떤 경로에서든 가장 효율적인 결과를 획득하려면, 일반적으로는 EGL 구성 스위치( configuration switch )를 단순하게 껐다 켰다 하는 것만큼 간단하지는 않습니다( 역주 : 이 옵션이 성능에 큰 영향을 준다는 의미인듯 ).

Mali-DP500 과 같은 ARM Frambuffer Compression( AFBC )가 활성화된 디스플레이 컨트롤러나 Mali-T760 과 같은 GPU 를 사용하는 시스템에서, EGL_BUFFER_PRESERVED 리드백의 대역폭 부하가 엄청나게 줄어들었다는 것을 언급하는 것은 가치가 있습니다. 리드백 대역폭이 압축된 프레임버퍼의 대역폭이므로 일반적으로 비합축 원본보다는 25-50 % 정도 더 작기 때문입니다.

A Better Future?

EGL_BUFFER_PRESERVED 의 동작은 훌륭한 아이디어입니다. 그리고 많은 경우에 여전히 유용하지만, 그것의 이론적인 이점들의 많은 부분들이 N 버퍼링 시스템에서 사라졌습니다. 왜냐하면 이것은 이전 프레임 데이터에 대한 전체 화면 리드백을 필요로 하기 때문입니다.

우리는, 이용가능한 애플리케이션 및 버퍼 보존 체계가 특정 플랫폼에서 N 버퍼 메모리 모델을 명시적으로 노출( 및 이용 )하면, 애플리케이션 -- 특히 사용자 인터페이스 -- 이 훨씬 더 효율적이 될 수 있다고 생각합니다. 만약 애플리케이션이 그 시스템이 더블 버퍼링을 사용하고 있다는 것을 알고 있고 현재 스테이트와 두 프레임 전의 스테이트 사이의 델타를 알고 있다면, 구조적으로 이상적인 렌더링과 변경된 메모리 영역에 대한 합성( compositing )에 가까워질 수 있습니다. 이것은 잠재적으로 대부분 안정적인 상태( steady-state )의 사용자 인터페이스를 위해 에너지 소비와 메모리 대역폭을 줄여줄 수 있습니다.

EGL_KHR_partial_update

EGL_KHR_partial_update 익스텐션( extension )은 애플리케이션이 N 버퍼링 레벨의 시스템-- 버퍼 에이지( buffer age ) --에 대한 질의를 할 수 있도록 허용하고, 그 정보와 버퍼가 마지막으로 렌더링된 후에 애플리케이션에서 변경된 부분에 대한 지식을 사용해, GPU 에 의해 렌더링되어야 하는 "dirty rectangles" 화면 영역을 식별할 수 있도록 허용하기 위해서 설계되었습니다.

이 익스텐션에서 버퍼 에이징 기능은 EGL_EXT_buffer_age 익스텐션에 의해 제공되는 것과 매우 유사합니다만, 타일 기반 렌더링의 경우에는 어떤 타일을 완전히 떨어뜨릴( drop ) 수 있는지 미리 알려 주는 dirty rectangle 을 제공합니다. 왜냐하면 그것들은 수정되지 않는 것이 보장되기 때문입니다. 만약 이 두 개의 익스텐션 중 하나를 선택해야 한다면, 최적의 성능을 위해서는 EGL_KHR_partial_update 기능을 사용하세요; 이러한 이유로 Mali 드라이버는 EGL_EXT_buffer_age 를 노출하지 않습니다.

EGL_KHR_swap_buffers_with_damage

EGL_KHR_swap_buffers_with_damage 익스텐션은 애플리케이션을 위해 시스템 합성 처리에 dirty rectangle 힌트들을 제공하기 위한 수단을 제공합니다. 이는 N 버퍼링 합성기( compositor )들과 디스플레이 컨트롤러들이 EGL_KHR_partial_update 으로부터 클라이언트 렌더링이 획득하는 최적화로부터 이득을 취할 수 있게 해 줍니다.

Do I need to use both extensions to get full benefit?

네, EGL_KHR_partial_update 는 GPU 를 버퍼 생성자( producer )로서 사용하여 애플리케이션이 렌더링하는 내용을 최적화합니다; EGL_KHR_swap_buffers_with_damage 는 버퍼 소비자로서 사용되는 디스플레이에 유효한 출력 이미지를 보낼 수 있도록 하기 위해 시스템 합성기가 리프레시( refresh )해야 하는 내용을 최적화합니다. 각각의 경우에 애플리케이션이 지정해야 하는 damage rectangle 은 일반적으로 다릅니다. 그러므로 두 개의 익스텐션이 모두 필요합니다.

Tune In Next Time

프레임버퍼 관리에 대한 짧은 전환이 마무리되었습니다. 그래서 다음 시간에는 Mali 에서 ARM DS-5 Streamline 을 사용해 애플리케이션 성능 병목과 최적화 기회에 대해서 살펴 보도록 하겠습니다.

TTFN,

Pete

Further reading:


원문 : https://community.arm.com/developer/tools-software/graphics/b/blog/posts/mali-performance-2-how-to-correctly-handle-framebuffers

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

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


이번 주에 렌더링 파이프라인의 하드웨어 중심 관점으로 약간 들어 갔는데요, 지금까지 어떻게 그리고 더욱 중요하게는 언제 Mali 드라이버 스택이 OpenGL ES API 활동( activity )를 렌더링에 필요한 하드웨어 워크로드가 되는지를 살펴 보았습니다. 앞으로 살펴 볼 것이지만, OpenGL ES 는 이 영역에서 특별히 탄탄하게 명세화되어( specified ) 있지는 않습니다. 그래서 개발자들이 피하기 위해서 주의해야만 하는 일반적인 함정들이 존재합니다.

Per-Render Target Rendering: Quick Recap

이전 블로그에서 설명했듯이, Mali 하드웨어 엔진은 2 패스 렌더링 모델 상에서 동작합니다. 렌더 타깃을 위해 모든 지오메트리를 렌더링하고 나서 프래그먼트 처리를 시작합니다. 이는 로컬 메모리에 존재하는 대부분의 작업 상태를 GPU 와 단단히 묶어서 유지할 수 있도록 해 줍니다. 그리고 렌더링 처리를 위해 필요한 전력을 소비하는 외부 DRAM 에 대한 접근을 최소화해 줍니다.

OpenGL ES 를 사용할 때 우리는 이 로컬 메모리 내부에서 프레임 버퍼 데이터의 대부분을 생성하고 사용하고 버릴 수 있습니다. 이는 프레임버퍼들을 외부 메모리에서 읽거나 외부메모리에 쓰는 것을 피하게 해 줍니다. 하지만 컬러 버퍼같은 것들을 유지하기 원한다면 그 버퍼에 대해서는 예외입니다. 하지만 이것은 보장된 동작이 아니며 어떤 패턴의 API 용법들은 GPU 가 추가적인 읽기 및 쓰기를 하도록 강제하는 비효율적인 행위를 유발할 수도 있습니다.

Open GL ES: What is a Render Target?

Open GL ES 에는 두 종류의 렌더 타겟이 존재합니다:

  • 오프 스크린( off-screen ) 윈도우 렌더 타깃들.
  • 오프 스크린 프레임버퍼 렌더 타깃들.

개념적으로는 이것들은 OpenGL ES 에서는 매우 유사합니다; 완전히 동일하지는 않습니다. 하나의 렌더 타깃만이 API 레벨에서 활성화되어 한 번에 하나의 점을 렌더링하는 데 사용될 수 있습니다; glBindFramebuffer( fbo_id ) 호출을 통해 현재의 렌더 타깃이 선택되면, 여기에서 0 인 ID 는 윈도우 렌더 타깃을 되돌려 놓는 데 사용될 수 있습니다( 또한 종종 기본 FBO 라 불리기도 합니다 ).

On-screen Render Targets

온 스크린( on-screen ) 렌더 타깃들은 EGL 에 의해 엄격하게 정의되어 있습니다. 한 프레임을 위한 렌더링 활동은 현재 프레임과 다음 프레임에 대한 매우 명확하게 정의된 경계를 가지고 있습니다; 두 개의 eglSwapBuffers() 호출 사이에서 FBO 0 을 위한 모든 렌더링은 현재 프레임을 위한 렌더링을 정의합니다.

또한 사용중인 컬러, 뎁스( depth ), 스텐실( stencil ) 버퍼들은 칸텍스트( context )가 생성될 때 정의되며, 그것들은 구성( configuration )은 변경될 수 없습니다. 기본적으로 컬러, 뎁스, 스텐실의 값들은 eglSwapBuffers() 후에는 정의되어 있지 않습니다( 역주 : 쓰레기 값이 들어 가 있을 수 있다는 의미인듯 ) - 예전의 값은 이전 프레임으로부터 보존되지 않습니다 - 이는 GPU 드라이버가 그 버퍼들의 사용에 대한 보증된 추정( guaranteed assumption )을 할 수 있도록 해 줍니다. 특히 우리는 뎁스와 스텐실이 일시적인( transient ) 작업 데이터일 뿐이라는 것을 알고 있으며, 결코 메모리로 쓰여질 필요가 없다는 것을 알고 있습니다.

Off-screen Render Targets

오프 스크린 렌더 타깃들은 별로 엄격하게 정의되어 있지 않습니다.

먼저, 드라이버에게 애플리케이션이 FBO 를 렌더링하는 것을 끝냈고 렌더링을 위해 제출될 수 있다는 것을 알려주는 eglSwapBuffers() 와 같은 함수가 존재하지 않습니다; 렌더링 작업을 플러싱하는 것은 다른 API 활동들로부터 추론됩니다. 다음 섹션에서 Mali 드라이버의 지원하는 추론에 대해서 살펴 보겠습니다.

둘째로, 애플리케이션이 컬러, 뎁스, 스텐실 어태치먼트( attachment ) 지점에 부착된( attached ) 버퍼를 사용해 작업을 수행할 수 있는지 여부를 보장하지 않습니다. 애플리케이션은 이것들을 텍스쳐로서 사용하거나 다른 FBO 에다가 다시 부착할 것입니다. 예를 들어 이전 렌더 타깃으로부터 뎁스 값을 다시 로드해 다른 렌더 타깃을 위한 시작 뎁스 값으로 사용하는 것입니다. 애플리케이션이 glInvalidateFramebuffer() 를 호출해서 명시적으로 내용을 버리지 않는다면, OpenGL ES 의 기본 동작은 모든 어태치먼트를 보존합니다. 노트: 이것은 OpenGL ES 3.0 의 새로운 진입점( entry point )입니다; Open GL ES 2.0 에서는 Mali 드라이버가 지원하는 glDiscardFramebufferExt() 확장 진입점을 사용해서 동일한 기능에 접근할 수 있습니다.

Render Target Flush Inference

일반적인 상황에서 Mali 는 렌더 타깃이 "언바운드( unbound )"되었을 때 렌더링 작업을 플러싱하는데, 드라이버가 eglSwapBuffers() 호출을 발견하여 메인 위도우 서피스를 플러싱하는 경우는 예외입니다.

성능이 떨어지는 것을 막기 위해서는 개발자들은 최종 렌더링의 서브셋( sub-set )만을 포함하고 있는 불필요한 플러싱을 피할 필요가 있습니다. 그러므로 프레임 당 오프 스크린 FBO 를 한 번만 바인딩하고 그것을 끝까지 렌더링하는 것을 권장합니다.

잘 구조화된 렌더링 시퀀스( 거의 어쨌든 - 이게 왜 불완전한지는 다음 섹션에서 확인하세요 )는 다음과 같습니다:

반대로 "나쁜" 동작은 다음과 같습니다:

이런 식의 행위는 증분 렌더러( incremental renderer )로 알려져 있으며 그것은 드라이버가 렌더 타깃을 두 번 처리하도록 만듭니다. 첫 번째 처리 패스는 중간( intermediate ) 렌더 스테이트를 메모리( 컬러, 뎁스, 스텐실 )에 모두 씁니다. 그리고 두 번째 패스는 그것을 다시 메모리로 읽어들입니다. 그러므로 그것은 오래된 스테이트의 최상위에 렌더링을 더 "추가"하게 됩니다.

위의 다이어그램에서 보이듯이, 증분 렌더링은 프레임 버퍼 대역폭 관점에서 잘 구조화된 단일 패스 렌더링과 비교했을 때 +400% 의 대역폭 페널티[ 32-bpp 컬러와 D24S8 로 패킹된 뎁스-스텐실이라 가정 ]를 가지고 있음을 알 수 있습니다. 잘 구조화된 렌더링은 중간 스테이트를 메모리에서 다시 읽는 것을 피하고 있습니다.

When to call glClear?

주의깊게 본 독자들은 네 개의 프레임 버퍼들을 위한 렌더링 시퀀스에다가 glClear() 라는 호출을 삽입한 것에 주목했을 것입니다. 이 애플리케이션은 각 렌더 타깃의 렌더링 시퀀스의 초기 부분에서 항상 모든 어태치먼트들에 대한 glClear() 를 호출해야 합니다. 물론 이전의 어태치먼트 칸텐트가 불필요함을 의미합니다. 이는 명시적으로 드라이버에게 이전의 스테이트가 필요하지 않다는 것을 전달하며, 즉 우리는 그것을 메모리로부터 다시 읽어들이는 것을 피할 수 있을 뿐만 아니라 정의되지 않은 버퍼 칸텐트들을 정의된 "clear color" 스테이트에 담습니다.

여기에서 볼 수 있는 일반적인 실수는 프레임버퍼의 일부만을 클리어하는 것입니다; 즉 시저( scissor ) 렉트( rectangle )가 일부 스크린 영역만을 포함한다든가 해서 렌더 타깃의 일부만이 활성화되었을 때 glClear() 를 호출하는 것. 그것이 전체 서피스에 적용될 때 렌더 스테이트가 완전히 버려질 수 있습니다. 그러므로 가능하다면 전체 렌더 타깃에 대한 클리어가 수행되어야 합니다.

When to call glInvalidateFramebuffer?

OpenGL ES API 에서 FBO 를 효율적으로 사용하기 위해서는 애플리케이션이 드라이버에게 컬러/뎁스/스텐실 어태치먼트들이 단지 일시적인 작업 버퍼임을 알려주는 것입니다. 그것들의 값이 현재 렌더 패스에 대한 렌더링 끝에서 버려질 수 있다는 것을 의미합니다. 예를 들어 모든 3D 렌더링은 컬러, 뎁스를 사용하지만, 거의 대부분의 애플리케이션에서는 뎁스 버퍼가 일시적이며 안전하게 무효화될 수 있습니다. 불필요한 버퍼들을 무효화하는 것이 실패하면 그것들에 메모리에 다시 작성되는 결과를 낳습니다. 그렇게 되면 메모리 대역폭을 낭비하게 되고 렌더링 처리에서 소비하는 에너지가 증가합니다.

이 시점에서 가장 일반적인 실수는 glInvalidateFramebuffer() 를 glClear() 와 동일하게 취급해서, 프레임 N 에 대한 무효화 호출을 프레임 N+1 의 FBO 를 처음 사용하는 시점에 하는 것입니다. 이건 너무 늦습니다! 무효화 호출의 목적은 드라이버에게 그 버퍼가 유지될 필요가 없음을 알려주는 것입니다. 그러므로 그런 버퍼들을 생성하는 프레임을 위해 GPU 에 작업 제출을 하도록 수정할 필요가 있습니다. 다음 프레임에 알려주는 것은 원래의 프레임이 처리된 후입니다. 애플리케이션은 드라이버에게 어떤 버퍼들이 프레임버퍼가 플러싱되기 전에 일시적인지 알려주는 것을 보장할 필요가 있습니다. 그러므로 프레임 N 에 있는 일시적인 버퍼들은 프레임 N 에서 FBO 가 언바인딩되기 전에 glInvalidateFramebuffer() 를 호출함으로써 식별될 수 있습니다. 예를 들면:

Summary

이 블로그에서 우리는 Mali 드라이버들이 렌더 패스들의 식별자를 다루는 방식, 일반적인 비효율적인 지점, 애플리케이션 개발자가 OpenGL ES API 를 비효율성을 피할 수 있도록 제어하는 방법 등에 대해서 살펴 보았습니다. 요약에서는 다음과 같이 추천합니다:

  • 각 FBO( FBO 0 이 아닌 것 )를 프레임에서 정확하게 한 번만 바인딩하고 연속되는 API 호출 시퀀스에서 완료될 때까지 렌더링.
  • 기존의 값이 필요로 하지 않는 모든 어태치먼트들에 대해 각 FBO 의 렌더링 시퀀스의 시작점에서 glClear() 를 호출.
  • 중간 스테이트가 그냥 일시적인 작업 버퍼인 모든 어태치먼트들에 대해서 FBO 렌더링 시퀀스의 끝에서 다른 FBO 로 전환되기 전에 glInvalidateFramebuffer() 나 glDiscardFramebufferExt() 를 호출.

다음 시간에는 다음과 관련한 것을 살펴 보겠습니다 -- 윈도우 서피스 컬러를 한 프레임으로부터 다음 프레임을 위한 기본 입력으로서 유지하기 위한 EGL_BUFFER_PRESERVED 의 효율적인 사용과 그것이 성능과 대역폭에 미치는 영향.

Cheers,

Pete.

원문 : https://community.arm.com/developer/tools-software/graphics/b/blog/posts/mali-performance-1-checking-the-pipeline

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

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


이 시리즈에서 처음 몇 개의 블로그 동안, ARM Mali "Midgard" GPU 패밀리가 사용하는 고수준 렌더링 모델에 대해서 설명해 왔습니다. 이 시리즈들의 나머지 부분에서는 ARM 의 시스템 레벨 프로우파일링 툴인 DS-5 Streamline 을 사용하는 방법에 대해서 설명하고, 애플리케이션이 Mali 기반 시스템의 외부에서 최적의 성능을 획득하지 못하는 영역을 식별할 것입니다.

이 블로그에서, 매크로-스케일( macro-scale ) 파이프라이닝과 관련한 이슈들에 대한 디버깅, 항상 GPU 를 바쁘게 만드는 수단, 그리고 프레임 레벨 파이프라인이 멈추는 일반적인 원인에 대해서 살펴 볼 것입니다. 만약 이 시리즈가 처음이라면 첫 번째 블로그를 읽을 것을 권장합니다. 왜햐나면 그것은 여기에서 더 세부적으로 다루게 될 개념에 대해서 소개하고 있기 때문입니다.

노트( note ): 여러분이 이미 DS-5 Streamline 을 가지고 있고 실행중이라고 가정합니다. 만약 아직 실행하지 않았다면, 다양한 Mali 기반 소비자 디바이스에서 설정하는 법에 대한 몇 개의 가이드가 있습니다:

이 블로그의 예제들은 DS-5 v5.16 을 사용해서 수집되었습니다.

What does good content look like?

성능 문제를 분석하기 전에, 우리의 목표와 스트림라인에서 이것이 어떻게 보이는지를 이해하는 것이 좋습니다. 시스템의 성능과 칸텐트의 복잡도에 기반한 두 가지 "좋은" 행위( behavior )들이 존재합니다.

  • GPU 가 병목( bottleneck )인 곳의 칸텐트를 위한 행위.
  • 수직동기화( vsync )가 병목인 곳의 칸텐트를 위한 행위.

이 실험을 위해 필요한 카운터들은 다음과 같습니다:

  • Mali Job Manager Cycles: GPU cycles
    • 이 카운터는 GPU 가 뭔가를 수행하는 클락 사이클에 증가합니다.
  • Mali Job Manager Cycles: JS0 cycles
    • 이 카운터는 GPU 가 프래그먼트 셰이딩을 하는 클락 사이클에 증가합니다.
  • Mali Job Manager Cycles: JS1 cycles
    • 이 카운터는 GPU 가 버텍스 셰이딩을 하거나 타일링을 하는 사이클에 증가합니다.

The GPU is the bottleneck

만약 GPU 가 병목인 곳( 예를 들어 렌더링이 60 FPS 를 달성하기에는 너무 복잡함 )의 칸텐트를 위한 프레임 레벨 렌더링 파이프라인을 생성하고 유지하는 데 성공했다면, 우리는 GPU 워크로드( workload ) 유형( 버텍스나 프래그먼트 처리 )들인 작업들이 항상 전체 능력( capacity )을 이용해 실행되고 있기를 기대하게 됩니다.

거의 대부분의 칸텐트에서 프래그먼트 처리는 GPU 실행에 있어서 가장 지배적인 부분입니다; 애플리케이션들은 보통 버텍스다 한자리 혹은 두자리 수가 더 많은 프래그먼트를 가지고 있습니다. 그러므로 이 시나리오에서는 JS0 이 항상 활성화되어 있고 CPU 와 JS1 은 모든 프레임에서 적어도 일정 시간만큼 유휴상태가 될 것이라 예상할 수 있습니다.

스트림라인을 사용하여 이 카운터 집합들을 수집할 때, 세 가지 활동( activity ) 그래프들을 보게 될 것인데요, 이것들은 GPU 를 위한 날( raw ) 카운터 값들과 함께 툴에 의해 자동으로 생성됩니다. "GPU 프래그먼트" 처리가 완전하게 로드되어 있고, "CPU Activity" 와 "GPU Vertex-Tiling-Compute" 워크로드들은 각 프레임에서 일정시간 동안 유휴상태가 되는 것을 확인할 수 있습니다. 노트 -- 이를 보기 위해서는 1 ms 나 5 ms 의 줌( zoom ) 레벨에 가깝게 확대할 필요가 있습니다 -- 여기에서는 매우 짧은 기간에 대해서 이야기하고 있는 것입니다.

The vsync signal is the bottleneck

vsync 에 의해 조절( throttle )되는 시스템에서는 CPU 와 GPU 가 매 프레임 유휴상태가 되는 것을 기대할 수 있습니다. 왜냐하면 vsync 신호가 발생하고 윈도우 버퍼 교체( swap )이 발생하기 전까지는 다음 프레임을 렌더링하지 않기 때문입니다. 아래 그래프는 스트림 라인에서 이것이 어떻게 보이는지를 보여 줍니다:

만약 여러분이 애플리케이션 개발자가 아니라 플랫폼 통합자( integrator )라면, 60 FPS 로 실행하는 테스트 케이스들은 여러분의 시스템의 DVFS 주파수 선택의 효율성을 리뷰하는 데 있어 좋은 방법이 될 것입니다. 위의 예제에서는 활동들이 나타나는 위치 사이에 많은 시간 간격이 존재합니다. 이는 선택된 DVFS 주파수가 너무 높고 그 GPU 는 필요 이상으로 빠르게 실행중이라는 것을 의미합니다. 이는 플랫폼의 에너지 효율성을 전체적으로 감소시킵니다.

Content issue #1: Limited by vsync but not hitting 60 FPS

더블 버퍼링 시스템에서는 60 FPS 를 달성하지 못하지만 vsync 에는 여전히 제한되는 칸텐트가 존재할 수 있습니다. 이 칸텐트는 위의 그래프와 매우 유사해 보이는데요, 워크로드 사이의 시간 간격이 한 프레임의 길이의 몇 배가 될 것이며, 프레임율( frame rate )은 정확히 최대 스크린의 리프레시율( refresh rate )로 정확히 나눠질 것입니다( 예를 들어 60 PFS 패널은 30 PFS, 20 FPS, 15 FPS 등으로 실행될 수 있습니다 ).

60 FPS 로 실행중인 더블 버퍼링 시스템에서, GPU 는 vsync 버퍼 교체를 위한 시간 안쪽에서 성공적으로 프레임들을 생성하고 있습니다. 아래 그림에서는 두 개의 프레임버퍼( FB0, FB1 )들의 생명주기( lifetime )을 확인할 수 있습니다. 녹색으로 되어 있는 것은 on-screen 의 기간이며 파란색으로 되어 있는 것은 GPU 에 의해 렌더링되고 있는 기간입니다.

GPU 가 이를 수행할 수 있을 만큼 충분히 빠르게 실행되고 있지 않다면, 하나 이상의 vsync 데드라인( deadline )을 놓치게 될 것입니다. 그래서 현재의 프론트 버퍼( front-buffer ) 가 다른 vsync 기간 동안에 스크린상에 남아 있게 됩니다. 아래 다이어그램에서 오렌지 라인이 그어져 있는 곳에서는, 프론트 버퍼가 여전히 스크린에 렌덜이되고 있으며 백 버퍼( back-buffer )는 디스플레이를 위해 큐에 들어 가 있습니다. GPU 는 렌더링할 수 있는 버퍼를 가지고 있지 않기 때문에 대기상태에 빠집니다. GPU 가 45 FPS 이상으로 칸텐트를 실행할 수 있을만큼 충분히 빠름에도 불구하고, 성능은 30 FPS 로 떨어집니다.

안드로이드 윈도우 시스템은 보통 트리플 버퍼링( tripple buffering )을 사용합니다. 그래서 이 문제를 피합니다. 왜냐하면 GPU 가 렌더링하는 데 사용할 버퍼를 가지고 있기 때문입니다. 하지만 여전히 X11 기반 Mali 배포폰에서는 더블 버퍼링을 사용합니다. 만약 이런 이슈가 발생했다면, 성능 최적화를 수행하는 동안에는 vsync 를 끄는 것을 권장합니다; 발생하는 문제를 혼란스럽게 만드는 부차적인 요소들이 없는 상태에서 어떤 것을 최적화해야 할지 결정하는 것이 훨씬 쉽습니다.

Content issue #2: API Calls Break the Pipeline

여러분이 보게 될 두 번째 이슈는 파이프라인 휴식( break )입니다. 이 시나리오에서는 적어도 하나 이상의 CPU 처리 부분이나 GPU 처리 부분이 항상 바쁘지만 동시에 바쁘지는 않습니다; 일종의 직렬화( serialization ) 지점의 형식이 발생한 것입니다.

아래의 예제에서는, 칸텐트가 프래그먼트 중심( dominant )이며, 그래서 프래그먼트 처리가 항상 활성화되어 있을 것이라 기대하고 있지만, 활동이 진동하고 있음을 볼 수 있습니다. GPU 버텍스 처리와 프래그먼트 처리가 직렬화되고 있습니다.

이것의 가장 일반적인 원인은 API 의 동기화 행위를 강제하는 OpenGL ES API 함수를 사용하는 것이고, 이는 드라이버가 모든 대기중( pending )인 연산들을 플러싱( flushing )하게 강제하고, API 요구사항을 존중하기 위해서 렌더링 파이프라인을 비워( drain )버립니다. 가장 일반적인 범인들이 여기에 있습니다:

  • glFinish(): 명시적으로 파이프라인 비우기를 요구합니다.
  • glReadPixels(): 묵시적으로 현재 서피스를 위해 파이프라인 비우기를 요구합니다.
  • glMapBufferRange(): GL_MAP_UNSYNCHRONIZED_BIT 가 설정되지 않았다면; 명시적으로 매핑되고 있는 데이터 리소스를 사용하고 있는 모든 대기중인 서피스들을 위해 파이프라인 비우기를 요구합니다.

이런 API 호출들은 파이프라인 비우기 때문에 빠르게 만들어 지는 것이 거의 불가능합니다. 그러므로 이런 함수들은 가능한 한 피할 것을 제안합니다. OpenGL ES 3.0 이 glReadPixels 함수가 픽셀 복사를 비동기적으로 수행할 수 있는 Pixel Buffer Object( PBO )를 타깃으로 한다는 것은 언급할 가지가 있습니다. 이것은 더 이상 파이프라인 플러싱을 발생시키지 않습니다. 하지만 여러분의 데이터가 도착할 때까지 잠시동안 기다려야만 하며, 메모리 전송은 여전히 상대적으로 비쌉니다.

Content issue #3: NOT GPU limited at all

오늘의 마지막 이슈는 GPU 가 전혀 병목이 아닌 사황입니다. 하지만 종종 형편없는 그래픽스 성능을 보여줄 때가 있습니다.

CPU 가 GPU 가 소비하는 것보다 더 빠르게 새로운 프레임을 생성할 수 있다면, 우리는 그냥 프레임 파이프라인을 유지하기만 할 수 있습니다. 만약 CPU 가 렌더링하는데 5 ms 를 소비하는 프레임을 20 ms 에 만들었다면, 파이프라인은 각 프레임에서 비어 있는 상태로 실행될 것입니다. 아래의 예제에서 GPU 는 매 프레임 유휴상태입니다. 하지만 CPU 는 항상 실행중이고, 이는 CPU 가 GPU 를 따라가지 못한다는 것을 내포하고 있습니다.

"꽉 잡아! CPU 가 단지 25 % 만 로드되었어". 스트림라인은 시스템의 전체 용량을 100% 로 보여 줍니다. 만약 여러분이 4 개의 CPU 코어를 가지고 있다면, 여러분의 시스템에서 단일 프로세서는 하나의 스레드가 최대이므로, 그것은 20 % 로드로 보여질 것입니다. 만약 "CPU Activity" 그래프의 타이틀 박스의 우상단에 있는 화살표를 클릭한다면, 그것은 확장될 것이고 여러분에게 시스템의 CPU 코어 당 개별적인 로드 그래픽스를 제공할 것입니다.

예상했던 대로, 한 코어가 100 % 로드를 사용하고 있습니다. 그러므로 이 스레드는 시스템에서 병목이 되는데요, 이는 전체 성능을 제한하게 됩니다. 이런 문제가 발생하는 여러 가지 원인이 있을 수 있는데요, 그래픽스 동작의 관점에서는 애플리케이션 비효율성보다는 두 가지 주요 원인이 있습니다:

  • glDraw...() 호출이 과도하게 많음.
  • 동적 데이터 업로드가 과도하게 많음.

모든 드로 호출( draw call )은 드라이버에 대한 비용을 가집니다. 컨트롤 구조체들을 만들고 그것들을 GPU 에 제출해야 합니다. 프레임 당 드로 호출의 개수는 유사한 렌더 스테이트( render state )를 가진 오브젝트의 드로잉을 배칭함으로써 최소화되어야 합니다. 배치를 크게 할 것인지 컬링을 고려할 것인지에 대한 균형 문제가 있기는 합니다. 타깃에 따라 다르기도 합니다: 모바일에서 대부분의 고사양 3D 칸텐트는 렌더 타깃 당 대략 100 개의 드로 호출을 사용하며, 많은 2D 게임들은 20-30 개의 드로 호출을 사용합니다( 역주 : 2014 년 이야기입니다 ).

동적 데이터 업로드의 관점에서, 클라이언트 메모리에서 그래픽스 서버로 업로드되는 모든 데이터 버퍼는 드라이버가 그 데이터들을 클라이언트 버퍼에서 서버 버퍼로 복제하는 것을 요구한다는 점에 주의하시기 바랍니다. 만약 서브 버퍼 업데이트보다는 새로운 리소스를 사용한다면, 드라이버가 그 버퍼를 위한 메모리를 할당해야만 합니다. 가장 일반적인 나쁜 짓은 클라이언트측 버텍스 애트리뷰트를 사용하는 것입니다. 정적인 Vertex Buffer Objects( VBOs )를 사용해 그래픽스 메모리에 영구적으로 저장할 수 있으며, 그 버퍼를 다른 일련의 렌더링을 위해서 참조로 사용할 수도 있습니다. 이는 업로드 비용을 한 번만 지불하게 해 주며 여러 렌더링 프레임 동안 그 비용을 분할( amortize )합니다.

Mali 그래픽스 스택이 성능을 전혀 제한하지 않는 경우가 존재합니다. 애플리케이션 로직 자체가 16.6 ms 이상 걸리는 경우에는 OpenGL ES 호출이 무한하게 빠르더라도 60 FPS 를 달성할 수 없습니다. DS-5 스트림라인은 매우 능력있는 소프트웨어 프로우파일러를 포함하고 있으며, 그것은 코드 내에서 병목이 어느 부분인지를 정확하게 식별할 수 있도록 도움을 줄 수 있습니다. 또한 다중 스레드를 사용해서 소프트웨어를 병령화하기 원한다면, 시스템의 다중 CPU 코어 사이에서 균형있는 워크로드를 로드할 수 있도록 도움을 줄 수 있습니다. 하지만 이것은 Mali 의 동작과 직접적으로 연관된 것은 아니기 때문에 여기에서는 다루지 않겠습니다.

Next Time ...

다음 시간에는 Mali 드라이버의 렌더 타깃 유지를 위한 접근법에 대해서 리뷰하겠습니다. 그리고 Frame Buffer Objects( FBOs )를 사용해 이 모델을 멋지게 실행하기 위해서 애플리케이션을 구조화하는 방법에 대해서 리뷰하겠습니다.

댓글과 의견을 환영합니다.

Cheers,

Pete.

주의 : 답이 틀릴 수도 있습니다. 그냥 정리하는 용도로 올립니다. 혹시라도 도움이 필요한 분이 있다면 도움이 되었으면 좋겠네요.

경고 : 숙제하려고 베끼는 데 사용하지 마십시오. 본인의 미래를 망칠 뿐입니다. 나중에 저를 원망하지 마세요.

부탁 : 문제 풀이가 잘못되었으면 지적해 주셨으면 좋겠습니다.



질량이 1.5 kg 인 물체가 질량이 7.5 kg 인 물체 위에 있다. 두 물체는 줄로 연결되어 있다. 이 계의 운동에서 마찰력이 무시된다고 할 때,



( a ) 질량이 1.5 kg 인 물체를 2.5 m/s2 의 가속도 크기로 가속하려면, 질량이 7.5 kg 인 물체에 어떤 크기의 힘을 작용해야 하는가?


( b ) 그 경우 줄에 작용하는 장력의 세기는 얼마인가?


( a )


도르레로 연결해 놓았지만, 사실 이 두 물체는 일렬로 연결되어 있는 것과 같습니다.


아래에 있는 물체의 질량을 M 이라 하고 위에 있는 물체의 질량을 m 이라고 했을 때, 이것을 당기는 힘은 다음과 같이 정의됩니다.


식 1.


실제로 주어진 값을 대입해서 힘의 크기를 구하면 다음과 같습니다.


식 2.


( b )


힘의 일부는 질량 M 인 물체를 움직이는데 사용되고, 장력을 통해 질량 m 인 물체를 움직이게 됩니다.


그러므로 장력 T 는 다음과 같습니다.


식 3.


주의 : 답이 틀릴 수도 있습니다. 그냥 정리하는 용도로 올립니다. 혹시라도 도움이 필요한 분이 있다면 도움이 되었으면 좋겠네요.

경고 : 숙제하려고 베끼는 데 사용하지 마십시오. 본인의 미래를 망칠 뿐입니다. 나중에 저를 원망하지 마세요.

부탁 : 문제 풀이가 잘못되었으면 지적해 주셨으면 좋겠습니다.



질량이 7,600 kg 인 헬리콥터가 질량이 1,400 kg 인 물체를 줄에 매단 채 0.8  m/s2 의 가속도로 수직으로 상승하고 있다.


( a ) 헬리콥터 날개의 양력( 들어 올리는 힘 ) 세기를 구하라.


( b ) 물체를 매단 줄에 작용하는 장력의 세기를 구하라.


( c ) 줄이 헬리콥터를 당기는 힘의 세기를 구하라.


그림 1.


그림 1 의 변수 및 상수들을 다음과 같이 정의합니다.


  • M : 헬리콥터 질량.

  • m : 물체 질량.

  • F : 헬리콥터의 양력.

  • G : 중력.

  • g : 중력 가속도. 위쪽을 + 방향이라 할 때 중력 가속도는 - 방향. 9.8 m/s2

  • Fn : 알짜힘.

  • an : 상승 가속도( 알짜힘 가속도 ). 0.8 m/s2.

  • Tb :  물체를 매단 줄에 작용하는 장력의 세기.

  • Tc : 줄이 헬리콥터를 당기는 장력의 세기.


( a )


위의 조건으로부터 알짜힘을 구할 수 있습니다. 중력과 양력을 더한 힘이 알짜힘입니다.


식 1.


질량과 가속도를 구해 봅시다.


식 2.


가속도를 알았으니 양력을 구할 수 있습니다.


식 3.


( b )( c )


물체와 헬리콥터가 함께 움직이고 있기 때문에, 두 물체가 받는 힘의 비는 질량의 비와 같습니다. 그러므로 줄에 작용하는 장력은 헬리콥터가 운동하는 데 필요한 힘만큼을 빼고 작용합니다.


식 4.


질량이 0 인 줄에서 양 끝에 작용하는 장력의 크기는 동일하므로 결과는 다음과 같습니다.


식 5.


+ Recent posts