렌더링 API를 이용하기 위한 기반
3D를 제대로 코딩이나 해본 것은 그래픽스 과목을 재수강하며 과제를 치열하게 할 때였다. 초수강 때는 워낙 다른 전공들을 많이 듣고 있던 탓에 그래픽스 자체에 신경을 많이 못 쓰기도 했지만, 내 프로그래밍에 대한 자만심이 큰 탓에 제대로 해내지도 못하고 망쳐버렸었다. '3D라고 뭐 다르겠어? 그냥 코딩하면 되겠지.' 절대 그렇지 않다는 것을 재수강할 때 절실히 깨달았기 때문에 초수강하던 때로 돌아간다면 당시의 내게 늘어놓은 수많은 삽질들을 접고 처음부터 제대로 하라고 소리 지르고 싶다. 하지만 어쩌겠나, 이미 나는 5차원의 벽장 너머에 갇혀있는 걸.
어쨌든, 재수강을 거치면서 OpenGL에 조금은 감을 잡고 있던 당시의 나는 과제를 하면서 몇 가지 의문점들을 가지고 있었다. glFinish()와 glFlush()의 차이라든가, swapBuffers()라는 함수는 대체 뭘 하는 것인지, surface란 무엇이고, 왜 나는 찬란한 2010년대에 살고 있음에도 불구하고 glut라는 2000년 초반에 이미 중단된 녀석을 써야지만 OpenGL을 쓸 수 있는 것인가 등이 그것이었다.
이런 궁금증들은 곧 과제 제출일이 임박해짐에 따라 말도 안 되게 엉켜버린 GL의 state들을 잡는데 쓸 정신 가닥이 모자란 덕분에 쏙 들어가 버렸다. 이후 대학원을 진학하면서 계속 GL을 사용해야 했기 때문에 몇 가지 궁금증들은 해결할 수 있었지만, 여전히 glut의 지배 하에서 어떻게든 코딩하고 있을 뿐이었다.
나는 Windows 환경에서 계속 작업을 진행했기 때문에 WGL을 직접 사용하는 방법을 따로 익히기도 했다. WGL은 Win32 API를 이용해서 직접 Window를 생성한 뒤에 그로부터 GL을 렌더링할 수 있도록 돕는 API이다. 그러나 막상 익혀둔 이걸 사용하진 못했다. 아무래도 다른 연구 개발자들과 함께 작업하는데 잘 쓰고 있던 프로그램을 강제로 내 방식대로 고칠 수 없기도 하거니와, 이미 만들어진 모듈을 근본에서부터 바꾸는 것엔 들여야 할 시간이 너무 과하게 컸기 때문이다. 결국 WGL을 익힌 것은 그저 그런 걸 쓸 수도 있다는 것을 알게 된 것에 불과했다. 뭐, 당장 그거 모른다고 엄청난 문제가 되진 않았으니 나도 연구와 개발에만 집중하기로 하고 근본적인 물음들은 또 뒤로 미루게 됐다.
그러다 더 이상은 이런 물음들을 밀린 학습지 바라보듯 내버려둘 수 없게 돼버렸는데, 요즘 회사에서 하는 일이 바로 위의 질문들을 해결해야만 하는 일이기 때문이다. 좀 더 구체적으로 말하자면, 개발 중인 플랫폼에 EGL을 쓸 수 있도록 지원하는 일을 하고 있다. 이 글을 통해 내가 지금까지 일하면서 파악한 내용들을 토대로 과거의 내가 가졌던 위의 의문들에 답을 줄 것이다.
우선 그를 위해 앞서 EGL이 뭔지 짚고 넘어갈 필요가 있다. EGL이라는 API는 Windows의 WGL과 그 용도가 동일한데, WGL이 Windows에 종속적인 API인 것에 반해, EGL은 플랫폼 독립적이라는 차이가 있다. EGL을 지원하는 플랫폼에서라면 OpenGL을 사용하고자 할 때 EGL을 그 바탕에 두고 OpenGL을 사용할 수 있다. 그런데 EGL을 바탕에 도구 OpenGL을 사용한다는 것은 또 무슨 의미인가?
OpenGL은 3D 렌더링을 위해 GPU에게 내릴 명령들과 그 명령들이 어떻게 진행될 것인지, 어떻게 그 진행의 상태들을 조작할 수 있는지 등을 정의할 뿐, 그것이 구체적으로 특정 플랫폼 하에서 어떻게 GUI로 구성되는 윈도우로써 동작하는지 등은 정의하지 않는다. 뿐만 아니라, 흔하게 말하고들 하는 더블 버퍼링이나 그 이상의 스왑 체인 등에 대해서도 정의하는 바가 없다. GL 함수들은 호출되면 그 즉시 수행되는 것이 아니라 그 함수에 해당하는 GPU 명령을 잘 모아두었다가 언젠가 GPU로 밀어 넣는다고 하는데, 그 명령들을 모아두는 무언가에 대한 실체 또한 언제, 어떻게 생성되고 관리되는지를 다루지 않는다. 그런 것들은 EGL과 같은 별개 API가 처리해주어야 할 영역이기 때문이다.
Khronos에선 EGL을 Khronos의 렌더링 API들과 플랫폼의 윈도우 시스템 사이의 인터페이스라고 정의한다. 윈도우 시스템은 짧게 말하자면 그래픽 요소로 프로그램들을 보여주는 체계다. Windows의 여러 윈도우를 띄우고 이 윈도우의 크기를 조절하거나 여러 윈도우를 겹쳐놓는 등의 처리부터 해상도 제어, 모니터로의 출력 등을 관장하는 것은 바로 이 윈도우 시스템을 통해서 이뤄진다. Windows의 DWM이라는 프로세스가 이를 관리한다고 알려져 있다. 이와 관련한 구체적인 내용은 별도의 위키에서 확인할 수 있다. (Wiki: Compositing Window Manager)
Windows에선 자체적으로 사용하는 WGL 뿐만 아니라 EGL 또한 지원한다. 그러므로 위를 Windows에 특정해서 풀어쓰자면, Khronos의 대표적 렌더링 API인 OpenGL과 Windows의 DWM을 EGL로 연결할 수 있다는 얘기다. 다시 말해, Windows에서 EGL을 이용하면 Windows의 윈도우에 렌더링을 수행할 수 있다는 의미이다. 쉽게 설명하기 위해 화면에 렌더링하는 데에만 쓰는 것처럼 설명했지만, 실제로는 윈도우에 렌더링하는 On-screen 렌더링뿐만 아니라, Off-screen 렌더링도 지원하며 그 외에도 각종 동기화 처리나 주기, 렌더링 컨택스트 등을 관리하므로, 플랫폼 그 자체에 rendering API를 사용할 수 있도록 연결시키는 용도로 봐야 한다. 이쯤 하면 EGL이 무엇인지는 어렴풋이나마 파악했으리라 예상되는데, 그렇다면 이제 EGL의 용어를 토대로 질문에 답을 내려줄 수 있게 됐으니 차근차근 짚어보자.
우선 Surface란 무엇인지부터 명확히 해야 한다. EGL에서 말하는 surface란 실제 렌더링 되는 대상이라고 볼 수 있는 이미지 버퍼를 추상화한 개념이며 이를 EGLSurface라는 객체로 표현한다. OpenGL을 통해 draw를 수행한 결과는 어떤 이미지 버퍼에 그려져야 하는데, 이때 이 이미지 버퍼를 직접 사용하는 대신 surface를 쓰는 것이다. 그리고 Surface는 내부에 다음과 같은 정보들을 가지게 된다.
* RGBA 각 색과 Depth에 몇 비트나 사용할지 등 - 렌더링 특성들을 결정하는 정보
* 이미지의 사이즈는 얼마나 될 것인지, 어떤 용도로 사용되는지 등 - 자체적인 정보
* 위와 같은 이러한 정보들을 통해 생성된 이미지 버퍼
Surface 추상화의 이점은 생성 이후 이미지 자체에 대해 신경 쓰지 않아도 된다는 점에 있는데, Microsoft의 DirectX나 차세대 API인 Vulkan에서의 Surface에서는 이미지 버퍼를 관리하는 부분을 따로 SwapChain으로 분리하여 더욱 엄격하게 개념들을 나누는 방식으로 나아갔다.
또한 EGLSurface는 EGL 1.4 표준에서 총 3개의 종류로 나뉘는데, 종류가 나뉨에 따라 그 성질에서 일부 차이가 생긴다. 대표적인 것으로 Window surface가 있는데, On-screen 렌더링을 담당한다. on-screen 렌더링은 렌더링 결과가 화면에 도시되는 것을 말하는데, 이를 위해 내부에선 복잡한 일련의 과정을 거친다. 예를 들어 화면에 표시되는 중인 버퍼에 다시 그릴 경우 그려지는 내용이 화면에 표시되는 도중에 변경될 수 있는 가능성이 있다.
이러한 현상을 tearing이라 하며, 이를 막기 위해 window surface는 1장보다 많은 이미지 버퍼를 가질 수 있다. 이때 그 개수는 EGL을 제공하는 드라이버에 따라 차이가 날 수 있다. EGL에서 유저는 이 이미지 버퍼의 개수가 몇 장인지 파악할 수는 없지만, 렌더링을 마친 후에 eglSwapBuffers()를 호출함으로써 surface 내부의 막 렌더링을 마친 버퍼(front-buffer)를 화면에 도시하는 용도로 사용하도록 하고, 그와 동시에 사용 중이지 않은 buffer들 중에서 하나(back-buffer)를 다시 draw하는 데 사용하도록 설정할 수 있다. 이것을 통해 윈도우에서 렌더링 된 이미지가 화면으로 내보내진 동안 동시에 다음 프레임을 렌더링하는 병렬적인 작업이 가능해진다.
잠시 eglSwapBuffers()를 간단하게 짚고 가자면, eglSwapBuffers()는 말 그대로 화면에 도시되는 용도로 사용되는 이미지 버퍼와 아닌 것을 교체하는 역할을 하기 때문에, 이미지 버퍼가 두 장 이상 존재함을 가정하고 있다. Off-screen 렌더링을 위한 Pbuffer surface에선 한 장의 버퍼만 생성되기 때문에 eglSwapBuffers()를 사용하여도 버퍼가 바뀔 것이 없기 때문에 의미가 없다.
또한 eglSwapBuffers()는 암시적 동기화를 지원하는데, 마치 glFlush()를 내부에서 수행하는 양, 호출 직후 모든 GL 명령들을 GPU로 전달한다. 또한 on-screen 렌더링 되는 surface에 대해선 윈도우 시스템과 연계하여 화면에 도시하라는 명령도 수행하므로, 굉장히 다양한 일을 하나의 함수에서 수행하는 것이다. glFlush()가 잠시 또 언급됐는데, 아래에서 다시 구체적으로 얘기할 것이다.
다시 돌아와서, 나머지 종류의 surface로는 Pixmap surface가 있는데, 이는 pixmap을 지원하는 플랫폼일 경우에 사용 가능하다. Pixmap이란 것도 on-screen을 위한 용도로 사용된다. 단, 윈도우 시스템과 공유하는 memory영역에 렌더링하는 용도로 사용할 때 pixmap surface가 쓰인다.
이 외에도 surface이 EGLStream을 이용하는 Stream surface로도 생성될 수 있지만, 이는 EGL의 확장 기능인 EGLStream까지 설명해야 하니 나중에 다시 다룰 기회가 생기면 다루도록 하고, surface에 대해선 이쯤 해도 괜찮을 것 같다.
렌더링 될 타겟에 대한 내용을 다루는 것이 EGLSurface라 한다면, 이 EGLSurface에 렌더링 GL 명령을 모아두는 것을 렌더링 컨택스트라고 한다. EGL에선 이런 렌더링 컨택스트를 EGLContext라는 이름의 객체로 표현한다. EGLContext를 사용해서 GL 명령들을 기록하기 위해선 먼저 현재 수행 중인 스레드에 대해 eglMakeCurrent()를 호출해야 한다. 이 함수에 EGLSurface도 같이 인자로 사용된다. 이는 즉, EGLSurface에 EGLContext에 쌓아둔 명령들을 사용할 것이라는 의미이다. 함수 호출 이후, 따라서 호출되는 GL 함수들의 실제 명령들은 이 EGLContext에 쌓이고 GPU가 임의로 명령들을 가져가서 수행한다.
쌓아둔 명령들은 대체적으로 GPU가 임의로 수행하지만, 강제로 GPU로 보내버리는 방법도 있다. 이때 명령을 보내는 것은 명시적이거나 암시적으로 수행할 수 있다. Rendering API인 OpenGL이 명시적으로 수행하는 방법은 두 가지인데 첫 번째로는 glFlush()를 사용하는 것이다. glFlush()를 호출하면, 현재 스레드에 eglMakeCurrent()를 통해 연결된 EGLContext에 아직 전달되지 않고 쌓여있는 GL 명령들을 모두 GPU로 보내버린다. glFlush()는 명령의 전달 완료 시점에서 바로 반환하는 함수이다. 해당 명령으로 인한 렌더링 결과 종료까지 CPU 코드의 진행을 멈추게 하고자 할 경우 이와 유사한 명시적 명령인 glFinish()를 사용한다. glFinish()는 완벽한 렌더링 결과를 얻고 나서야 함수가 반환될 것을 보장한다. 하지만 대체적으로 Window surface를 사용하는 경우엔 이미 eglSwapBuffers()를 이용하여 front/back 버퍼에 대해 병렬적으로 접근/렌더링을 하므로 이를 이용하는 편이 새로운 프레임을 더 빠르고 안정적으로 얻을 수 있어 보편적인 경우에 더 좋은 성능을 발휘한다. eglSwapBuffers()는 암시적으로 모든 GL 명령들을 현재 surface에 적용하라고 보낸 뒤에서야 이미지 버퍼를 swap 한다. 즉, glFlush()를 드라이버 차원에서 수행해준다고 볼 수 있다.
이 정도면 적당히 EGL을 이용한 GL 렌더링을 위한 정도는 충분하다. 물론, 기반이 되는 사안들을 더 파악하는 것이 당연히 필요하다. 한 스레드 안에서 두 개 이상의 렌더링 컨택스트와 surface들을 보유하고, 각각에 대해 정상적으로 렌더링 되도록 하기 위한 조건이라든가, 동기화 방법이라든가, 또는 멀티 스레딩은 또 어떻게 다른가 하는 것들 말이다. 또한 잠깐 언급했지만, EGL은 각 그래픽카드 제조사나 플랫폼 개발자들의 이해관계에 따라 추가되는 확장들 또한 다양하므로 필요한 경우엔 이를 사용하려는 플랫폼, 장치에 따라 파악해야 할 필요가 있다. 물론 이는 OpenGL 또한 마찬가지이다.
여러 모로 파악하고 이해하는데 충분한 시간을 일하면서나마 들이게 돼서 나도 어느 정도 쓸 만큼은 익숙해지게 된 것 같다. 같이 일하는 동료들하고 얘기하면서도 충분히 머릿속으로 정리했다고 생각했는데, 글로 한 번 정리하려고 보니 헷갈리는 부분들도 생겨서 다시 찾아보기도 하는 등 불확실한 면을 좀 더 확실히 없앨 수 있었다. 만약 앞으로 EGL 관련해서 글을 쓰게 된다면 아마 개발일지 같은 항목에서 문제를 해결한 내용들에 대해서 다루지 않을까 싶다. 아무튼 생각보다 길어지게 됐으니 오늘은 이쯤에서 정리하고 마친다.