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

+ Recent posts