원문 : https://community.arm.com/developer/tools-software/graphics/b/blog/posts/the-mali-gpu-an-abstract-machine-part-1---frame-pipelining

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

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


그래픽스 워크로드( workload )를 최적화하는 것은 보통 많은 현대 모바일 애플리케이션에서 필수입니다. 거의 대부분의 렌더링은 이제 렌더링 백엔드( back-end ) 에 기반한 OpenGL ES 에 의해 직접 혹은 간접적으로 다뤄집니다. 내 동료 중의 하나인 Michael McGeagh 는 최근에 Mali-T604 GPU 를 사용하는 그래피컬 애플리케이션들을 프로우파일링하고 최적화하기 위한 목적으로 Google Nexus 10 에서 동작하는 프로우파일링 툴인 Arm DS-5 Streamline 에 대한 작업 가이드를 포스팅했습니다. Streamline 은 강력한 도구이며, 전체 시스템의 동작에 대한 고해상도 가시화를 제공합니다. 하지만 그것은 엔지니어가 데이터를 해석( interpret )하고 문제 영역을 식별하고 수정할 점을 제안하는 데 시간을 쏟게 만듭니다.

그래픽스 최적화에 익숙하지 않은 개발자들에게는, 처음 시작할 때 공부할 것이 좀 있다고 할 수 있습니다. 그러므로 이 새로운 블로그 시리즈는 칸텐트 개발자들에게 필수적인 지식들을 제공해서 Mali GPUs 에서 성공적으로 최적화를 할 수 있도록 하기 위해 작성되었습니다. 이 시리즈의 코스 전반에서, 필자는 기본적인 macro-scale architectural structure 들과 동작들에 대해서 살펴 볼 것이므로, 개발자들은 이것을 어떻게 칸텐트에서 발생할 수 있는 가능한 문제로 해석할 것인지 그리고 Streamline 에서 그것들을 어떻게 확인할 것인지에 대해 걱정할 필요는 없습니다.

Abstract Rendering Machine

애플리케이션의 그래픽스 성능을 성공적으로 분석하기 위해서 필요한 가장 필수적인 조각은 OpenGL ES API 함수들을 밑바탕에 있는 시스템들에 대한 멘탈 모델( 역주 : 한글 링크 )입니다. 이는 엔지니어가 그들이 관찰하고 있는 동작들에 대해 추론할 수 있도록 합니다.

개발자들이 제어할 수 없고 결국에는 제한된 가치인 드라이버 소프트웨어와 하드웨어 서브시스템의 구현 세부사항 때문에 개발자들이 압도되지 않도록 하기 위해서는, 단순된 추상 머신을 정의하는 것이 유용합니다. 이는 관찰되는 동작들에 대해 설명하기 위한 기초로 사용될 수 있습니다. 이 머신에는 세 가지 주요 파트가 있으며, 그것들은 거의 직교적( orthogonal, 역주 : 서로 간섭하지 않고 독립적이어서 따로 따로 이해할 수 있다는 의미인듯 )입니다. 그래서 필자는 각각을 시리즈의 처음 몇 개의 블로그를 통해 순서대로 다루려고 합니다. 하지만 이 모델의 세가지 파트들에 대해 미리 살펴 보면 다음과 같습니다 :

  1. The CPU-GPU rendering pipeline.
  2. Tile-based rendering.
  3. Shader core architecture.

이 블로그에서, 우리는 이들 중 첫 번째인 CPU-GPU rendering pipeline 에 대해서 살펴 보겠습니다.

Synchronous API, Asynchronous Execution

이를 이해하는 데 있어서 중요한 지식의 가장 근본적인 조각은 OpenGL ES API 에서 애플리케이션의 함수 호출과 그 API 호출들이 요구하는 렌더링 연산( operations )들의 실행 사이의 임시적 관계( temporal relationship )입니다. OpenGL ES API 는 애플리케이션의 관점에서는 동기적 API 로서 설계되었습니다( specified ). 애플리케이션은 일련의 함수 호출들을 만들어서 그것의 다음 드로잉 태스크에 필요한 상태( state )를 설정합니다. 그리고 나서 glDraw 함수를 호출해서 -- 보통 드로 콜이라 불립니다 - 실제 드로잉 연산을 발생시킵니다. API 는 동기적이므로, 드로 콜이 만들어진 후의 모든 순차적 API 동작은 렌더링 연산이 이미 실행된 것처럼 행동하도록 설계되어 있습니다. 하지만 거의 대부분의 하드웨어 가속 OpenGL ES 구현들에서는, 이는 드라이버 스택에 의해서 유지되는 정교한 환상입니다.

드로 콜에서도 이와 유사한 것이 있습니다. 드라이버에 의해서 유지되는 두 번째 환상은 프레임 버퍼 교체 완료( end-of-framebuffer flip )입니다. OpenGL ES 애플리케이션을 처음 작성하는 대부분의 개발자들은 eglSwapBuffers 호출이 프런트 버퍼와 백 버퍼를 교환( swap )한다고 말할 겁니다. 이것이 논리적으로는 사실이기는 하지만, 드라이버는 또 다시 동기화에 대한 환상을 유지하고 있는 것입니다; 거의 대부분의 플랫폼에서 물리 버퍼 교환은 한참 후에 발생합니다.

Pipelining

이러한 환상이 필요한 이유는, 여러분이 원하는 성능때문입니다. 만약 렌더링 연산을 실제로 동기적으로 수행하도록 강제한다면, CPU 가 다음 드로 연산을 위한 상태를 생성하느라 바쁠 때 GPU 가 놀게( idle ) 될 것입니다. 그리고 GPU 가 렌더링하고 있는 동안 CPU 가 놀 것입니다. 성능의 관점에서는 이런 유휴 시간( idle time )들은 허용될 수 없습니다.

이 유휴 시간을 제거하기 위해서, 우리는 OpenGL ES 드라이버를 사용해 동기적으로 렌더링되는 것처럼 환상을 유지했습니다. 하지만 실제로는 렌더링과 프레임 교환을 비동기적으로 처리하고 있습니다. 비동기로 실행하면 작은 백로그( backlog ) 작업을 빌드할 수 있으므로, GPU 가 파아프라인의 한쪽 끝에서 오래된 워크로드를 처리하는 곳에서 파이프라인을 만들 수 있고 CPU 는 새로운 작업을 다른 쪽에서 밀어 넣을 수 있습니다. 이 접근 방법의 장점은 파이프라인을 가득 채우면 GPU 에서 실행할 수 있는 작업이 항상 최고의 성능을 제공한다는 것입니다.

Mali GPU 파이프라인에서 작업 유닛( unit )들은 렌더 타깃( render target ) 기반( basis )으로 스케줄링됩니다. 여기에서 렌더 타깃은 윈도우 서피스( surface )이거나 오프스크린 렌더 버퍼일 수 있습니다. 단일 렌더 타깃은 두 단계의 절차로 처리됩니다. 먼저 GPU 는 렌더 타깃 내의 모든 드로 콜들에 대한 버텍스 셰이딩( vertex shading )을 처리합니다. 다음으로 전체 렌더 타깃을 위한 프래그먼트( fragment ) 셰이딩이 처리됩니다. 그러므로 Mali 를 위한 논리 렌더링 파이프라인은 세 가지 스테이지의 파이프라인으로 구별됩니다: CPU 프로세싱 스테이지, 지오메트리( geometry ) 프로세싱 스테이지, 프래그먼트 프로세싱 스테이지.

Pipeline Throttling

관찰력있는 독자들은 위의 그림에 있는 프래그먼트 작업이 CPU 및 지오메트리 처리 스테이지들보다 점점 더 느려지고 세 가지 연산 중에 가장 느리다는 것을 눈치챘을 것입니다. 이런 상황이 비정상적인 것은 아닙니다; 대부분의 칸텐트들은 셰이딩되는 버텍스보다는 프래그먼트를 더욱 많이 가지고 있으므로, 프래그먼트 셰이딩이 보통 주요 처리 연산입니다.

사실은 CPU 작업을 완료하는 것부터 프레임이 렌더링되는 것까지의 지연양( amount of latency )을 최소화하는 것이 바람직합니다 -- 최종 사용자 입장에서는 자신들이 터치 이벤트를 입력하고 있는 터치 스크린 디바이스와 상호작용( interacting )이 스크린의 데이터와 몇 백 밀리세컨드 정도 차이가 나는 상황이 가장 혼란스럽습니다 -- 그래서 우리는 프래그먼트 처리 스테이지를 위해 대기하는 밀린 작업이 너무 많아지는 것을 원하지 않습니다. 간단히 말해, CPU 스레드를 주기적으로 느리게 만드는 메커니즘( mechanism )이 필요합니다. 파이프라인이 이미 성능을 유지하기에 충분할 때 작업 대기열( queuing up )을 중단합니다.

이 조절( throttling ) 메커니즘은 보통 그래픽스 드라이버가 아니라 호스트 윈도우 시스템에 의해 제공됩니다. 예를 들어 안드로이드에서는 버퍼의 방향( orientation )을 알기 전까지는 프레임에서 어떠한 드로 연산도 처리할 수 없습니다. 왜냐하면 유저가 장치를 회전시켜서 프레임 크기를 변화시킬 수 있기 때문입니다. SurfaceFlinger -- 안드로이드 윈도우 서피스 관리자 -- 는 렌더링을 위해서 N 개의 이상의 버퍼가 이미 존재할 때 애플리케이션의 그래픽스 스택에 버퍼를 반환하는 것을 거절함으로써 파이프라인 깊이를 단순하게 제어합니다.

만약 이런 상황이 발생한다면, "N" 에 도달하자 마자 CPU 가 프레임당 한 번씩 유휴상태가 되어, EGL 이나 OpenGL ES API 함수들 내부에서 블락킹( blocking )이 발생하는데, 이는 디스플레이가 보류중인( pending ) 버퍼를 소비하고 새로운 렌더링 연산을 위해서 하나를 비워줄( freeing ) 때까지 유지됩니다.

그래픽스 스택이 디스플레이 리프레시 비율( rate )보다 더 빠르게 실행되고 있을 때 이와 동일한 방식으로 파이프라인 버퍼링에 제약이 가해집니다; 이 시나리오에서는 칸텐트는 디스플레이 컨트롤러에 다음 프론트 버퍼를 교체할 수 있다고 알려주는 수직 공백( vertical blank, vsync )를 기다리면서 "vsync limited" 됩니다. 만약 GPU 가 디스플레이가 그것을 보여줄 수 있는 것보다 빠르게 프레임을 생산한다면, SurfaceFlinger 는 렌더링이 완료되었지만 여전히 화면에 출력되어야 할 버퍼들의 개수를 누적할 것입니다; 심지어 이 버퍼들은 더 이상 Mali 파이프라인의 일부가 아님에도 불구하고, 그것들은 애플리케이션 처리를 위한 N 프레임 제약을 위해 카운트됩니다.

위의 파이프라인 다이어그램에서 보이듯이, 만약 칸텐트가 vsync 제한에 걸리면, CPU 와 GPU 가 완전히 유휴상태가 되는 주기를 가지는 것이 일반적입니다. 플랫폼 동적 전압( Platform dynamic voltage )과 주파수 스케일링( frequency scaling, DVFS )은 일반적으로 이런 시나리오에서 현재의 연산 주파수를 줄이려고 시도합니다. 이를 통해 전압과 에너지 소비가 줄어들지만, DVFS 주파수 선택은 보통 상대적으로 거칠기 때문에 일부 유휴 시간이 예상됩니다.

Summary

이 블로그에서 우리는 OpenGL ES API 에 의해 제공되는 동기화 환상과 API 뒤에서 비동기 렌더링 파이프라인이 실제로 실행되는 이유에 대해 살펴 보았습니다. 다음 시간에 필자는 계속해서 추상 머신 개발에 대해서 이야기할 것입니다. Mali GPU 의 타일 기반 렌더링 접근법에 대해서 살펴 보도록 하겠습니다.

댓글과 질문을 환영합니다.

Pete

+ Recent posts