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

+ Recent posts