0.0 GPU 드라이버

대학을 졸업한 후, 저는 OpenGL(즉, GPU 드라이버)을 개발하는 시스템 소프트웨어 엔지니어로 첫 직장에 입사하게 되었습니다. 하지만 당시만 해도 드라이버 프로그램에 대한 이해는 매우 부족했습니다. 대학에서는 GPU와 드라이버 소프트웨어에 대해 깊이 있게 배우지 않기 때문에, 드라이버는 하드웨어와 애플리케이션 사이를 연결하는 소프트웨어라는 정도만 알고 있었죠. 드라이버가 실제로 어떻게 동작하는지, 어떤 데이터를 하드웨어에 전달하는지 전혀 몰랐습니다. 이 팀에서 직접 드라이버 개발을 하며 비로소 GPU 드라이버가 어떻게 작동하는지, 그래픽 렌더링에서 얼마나 중요한 역할을 하는지 깊이 이해하게 되었습니다.

그래픽스 엔지니어로서 우리는 화면에 픽셀을 그리는 프로그램을 작성합니다. 그리기가 즉각적으로 보일지라도, 그 이면에는 많은 일들이 일어납니다. WebGPU API를 제대로 이해하고 활용하려면, GPU 드라이버와 렌더링 파이프라인이 어떻게 동작하는지 기본적으로 알아야 합니다. GPU 드라이버는 비디오 게임과 같은 그래픽 애플리케이션, 운영체제, 그리고 GPU 하드웨어 사이의 중간자 역할을 합니다. 드라이버와 렌더링 파이프라인의 역할을 이해하면, GPU 하드웨어를 제어하도록 설계된 그래픽스 API를 더 잘 이해하고 활용할 수 있습니다.

GPU 드라이버는 픽셀을 생성하는 블랙박스처럼 동작한다
GPU 드라이버는 픽셀을 생성하는 블랙박스처럼 동작한다

GPU 드라이버는 하나의 프로그램이 아니라, 여러 가지 기능을 담당하는 여러 프로그램들의 집합입니다. 일부는 비디오 압축/해제 역할을 하고, 일부는 범용 계산을 담당합니다. 하지만 가장 핵심적인 기능은 실시간 3D 그래픽 렌더링을 가능하게 하는 것입니다.

GPU 드라이버를 그래픽스 사양(specification)이라고도 부르는 경우가 있어 혼란스러울 수 있습니다. 이는 실제 구현 뒤에는 여러 회사가 합의한 API 사양이 있기 때문입니다. 예를 들어, 윈도우에서 게임에 가장 많이 쓰이는 DirectX는 마이크로소프트가 주도합니다. OpenGL은 3D 그래픽에 관여하는 주요 기업들의 연합체인 크로노스 그룹(Khronos Group)이 정의합니다. 이런 API의 실제 구현은 엔비디아(NVIDIA), AMD, 인텔(Intel)과 같은 GPU 제조사가 맡으며, 드라이버 형태로 우리 컴퓨터에 배포되고 설치됩니다.

오랜 기간 OpenGL과 DirectX는 실시간 3D 그래픽의 양대 산맥이었습니다. 하지만 최근에는 Vulkan, PC용 DirectX 12, Mac용 Metal 같은 새로운 API가 등장했습니다. DirectX 12는 DirectX의 후속처럼 보이지만, 사실상 DirectX 11과는 전혀 다른 완전히 새로운 디자인입니다.

새로운 그래픽 표준의 등장은 API 설계 사고방식의 변화를 보여줍니다. 예전 세대의 API는 모든 용도에 두루두루 맞게 만들려다보니 복잡하고 느려졌습니다. 반면 최신 API는 가볍고, 세밀한 튜닝과 성능 최적화 책임을 개발자에게 더 많이 넘깁니다. 이는 그래픽 개발 난이도를 높이지만, 성능과 제어력을 더 크게 끌어올립니다. 최근에는 게임 엔진 같은 상위 그래픽 미들웨어가 발전하여, 특정 목적에 맞는 개발 효율성을 제공합니다. 예를 들어 게임을 개발한다면, 저수준 API로 직접 만드는 것보다는 게임 엔진을 사용하는 게 더 바람직합니다.

웹 3D API는 네이티브 드라이버 위에 구축된 래퍼다
웹 3D API는 네이티브 드라이버 위에 구축된 래퍼다

웹용 3D 그래픽스의 발전 경로도 비슷합니다. WebGL, WebGL2는 OpenGL ES 2, 3을 기반으로 만들어진 첫 번째 웹 3D API입니다. 성능이 낮은 기기에서도 잘 돌아가도록 단순화되었고, 하위 호환성을 포기했습니다. 다른 그래픽스 API와 달리, WebGL 등 웹 API는 하드웨어 벤더가 드라이버로 구현하는 것이 아니라 웹 브라우저가 구현합니다. 브라우저는 이 API를 네이티브 API로 변환합니다. 예를 들어, 크롬에는 ANGLE이라는 서브시스템이 WebGL을 구현하고, 윈도우에서는 DirectX로 변환해 동작합니다.

최근까지 웹에서 가장 널리 쓰인 3D 그래픽스 API는 WebGL, WebGL2였습니다. 그러나 이제는 경량화 원칙을 따르는 새로운 세대의 3D API, WebGPU가 WebGL을 대체할 준비를 하고 있습니다. 지금이 바로 이 새로운 표준을 배우기에 좋은 시기입니다.

이제 그래픽스 API의 큰 그림을 이해했으니, 그래픽 드라이버가 실제로 무슨 일을 하는지 살펴봅시다.

운영체제는 커널 모드(kernel mode)와 사용자 모드(user mode)라는 두 가지 모드로 동작합니다. 커널 모드는 운영체제의 핵심 기능을 담당하며, 사용자 모드는 중요도가 낮고 대부분의 애플리케이션이 이 모드에서 실행됩니다. 커널 모드는 단 하나만 실행되고, 많은 애플리케이션들이 사용자 모드로 실행됩니다.

각 애플리케이션은 자체 사용자 모드 인스턴스를 로드하며, 커널 모드 드라이버와 통신해 GPU와 연결한다.
각 애플리케이션은 자체 사용자 모드 인스턴스를 로드하며, 커널 모드 드라이버와 통신해 GPU와 연결한다.

GPU 드라이버 역시 구조가 비슷합니다. 일부 드라이버는 커널 모드로 동작하여 GPU 하드웨어에 그리기 명령을 전달하고, 리소스를 할당 및 전송하며, CPU와 GPU 간 동기화를 담당합니다. 커널 모드 GPU 드라이버는 시스템에 단 하나만 존재합니다. 그 외 구성요소들은 동적 라이브러리로 구현되어 사용자 모드에서 동작하며, OpenGL, DirectX처럼 애플리케이션이 사용하는 상위 인터페이스를 제공합니다. GPU 드라이버 구조는 뒤에서 더 자세히 다룹니다.

이제 사용자 모드 드라이버와 커널 모드 드라이버가 운영체제와 함께 그래픽스 서브시스템을 어떻게 구성하는지 알아봅시다.

시스템이 동작하는 동안, 그래픽 작업이 필요한 모든 애플리케이션마다 사용자 모드 드라이버 인스턴스가 로드됩니다. 즉, 여러 개의 사용자 모드 드라이버 인스턴스가 동시에 존재할 수 있습니다. 반면, 커널 모드 드라이버는 부팅 시 한 번만 로드되어, 모든 사용자 애플리케이션과 시스템(데스크탑 등)이 공유합니다.

그래픽스 드라이버는 세 가지 주요 역할을 합니다. 첫째, 실시간 컴파일러로서 개발자가 호출한 API를 GPU 하드웨어가 이해할 수 있는 머신 코드로 변환합니다. 둘째, 리소스 관리자 역할로 GPU 메모리를 할당/해제합니다. 셋째, 스케줄러 역할로 작업을 GPU에 보내고 CPU와 GPU 간 동기화 인프라를 제공합니다. 각 역할을 자세히 살펴봅시다.

CPU 프로그램은 실시간 컴파일러가 필요 없어 보이는데, 왜 그래픽스 애플리케이션에는 필요할까요? 이유는 다음과 같습니다:

  1. GPU 하드웨어에는 표준화된 명령어 세트가 없습니다. CPU는 x86, ARM 등 공개 표준 명령어가 있어, 특정 아키텍처용으로 빌드된 프로그램은 어느 회사 CPU에서나 동작합니다. GPU는 엔비디아, AMD 등 제조사마다 명령어가 달라, 실제 하드웨어에 맞게 머신 코드를 생성해야 합니다.
  2. GPU의 주된 목적은 렌더링이지만, 그 렌더링 내용은 컴파일 타임에 확정할 수 없습니다. 예를 들어, 시계 앱의 화면은 현재 시간에 따라 달라지며, 그에 따라 CPU 코드는 GPU 그리기 명령을 실시간으로 만들어 전송합니다. 즉, 그리기 명령은 실행 중에 매번 새로 생성되어야 하죠.

리소스 관리 측면에서, GPU 드라이버는 GPU 메모리 할당을 담당합니다. 텍스처, 도형 정보가 담긴 버퍼, 셰이더 프로그램 등 다양한 리소스가 이에 해당합니다. 셰이더 프로그램은 GPU에서 실행되어 픽셀을 출력합니다. 셰이더 작성법은 이후 장에서 자세히 다룹니다.

GPU 메모리는 한정되어 있기 때문에, 드라이버가 효율적으로 관리해야 합니다. 예를 들어, 사용하지 않는 리소스를 CPU 메모리로 임시로 옮기는 등, GPU 메모리 사용을 최적화해야 더 많은 그래픽 애플리케이션이 동시에 동작할 수 있습니다. 또, CPU와 GPU 메모리 간 데이터 전송 속도가 느리기 때문에, 드라이버는 전송량을 최소화하여 성능을 극대화해야 합니다.

세 번째 역할은 작업 스케줄러이자 동기화 인프라입니다.

모든 사용자 애플리케이션은 드로우 명령과 리소스 업로드 요청을 커널 모드 드라이버로 보냅니다. 여기서 "업로드"란 CPU 메모리에서 GPU 메모리로 데이터를 복사하는 과정이며, 반대로 GPU에서 CPU로 옮기는 것은 "다운로드"라 합니다.

커널 모드 드라이버는 이 요청을 실제 GPU 하드웨어로 전송하고, 작업이 끝나면 시스템에 알립니다. 이 과정은 마치 자바스크립트 이벤트 루프처럼, 각 요청이 비동기 함수 호출처럼 작동합니다.

하지만 스케줄링은 그리 단순하지 않습니다. 커널 모드 드라이버는 시스템 전체가 공유하기 때문에, 워크로드를 공평하게 관리해야 합니다. 만약 GPU가 오래 걸리는 작업에 막히면 시스템 전체가 멈출 수 있는데, 이는 자바스크립트에서 오래 걸리는 함수가 브라우저를 멈추게 하는 것과 비슷합니다.

또, 드라이버는 CPU와 GPU 간 동기화를 지원해 데이터 경합을 방지합니다. 예를 들어, 3D 장면을 렌더링한 뒤 이미지를 저장하려면, 드라이버가 렌더링 완료 시점을 알려주어야 합니다.

실제 예를 들면, 게임 같은 그래픽 애플리케이션을 실행하면, 운영체제는 해당 사용자 모드 드라이버를 로드하고, API가 제공하는 함수들을 호출해 GPU와 상호작용을 시작합니다. 게임은 API를 통해 버퍼, 텍스처 등 리소스를 할당하고, 셰이더 프로그램으로 렌더링 동작을 정의합니다. 렌더링 중에는 도형 데이터를 API로 전달합니다. 사용자 모드 드라이버는 이러한 함수 호출을 GPU가 이해할 수 있는 저수준 명령과 데이터로 변환해 커널 모드 드라이버에 시스템 콜로 전달합니다. 커널 모드 드라이버는 시스템 전체의 모든 GPU 요청을 받아 큐에 넣고, 공평하고 효율적으로 작업이 실행되도록 관리합니다.

정리하면, 이번 장에서는 그래픽스 API의 흐름, GPU 드라이버 아키텍처, 사용자 모드/커널 모드 드라이버/운영체제가 GPU 하드웨어를 제어하는 구조를 살펴봤습니다. 또, GPU 드라이버가 수행하는 세 가지 주요 역할도 정리했습니다. 하지만 드라이버는 GPU의 조수에 불과하며, 실제 렌더링은 GPU 하드웨어가 'GPU 파이프라인'이라는 개념에 따라 직접 수행합니다. 다음 장에서는 그래픽스 API 설계의 근간이 되는 GPU 파이프라인 개념을 자세히 배워봅니다.

GitHub에서 의견 남기기