(이하는 A trip through the Graphics Pipeline 2011, part 1 를 날림번역한 것임다.)

Part 1 - 소프트웨어 계층


응용 프로그램

니가 짠 코드ㅇㅇ. 니가 싼 버그(...)도 포함. (주:D3D나 OpenGL API 를 호출하는 코드가 포함된 프로그램을 얘기한다고 보면 될듯)


API 런타임

API 런타임은 다음과 같은 요청들을 니가 짠 코드로부터 받게 된다.
  • 리소스 생성
  • 스테이트 설정
  • 드로우콜 등
API 런타임은 넘겨받은 요청에 다음과 같은 처리를 한 뒤 작업들을 묶어서(batch) 유저 모드 드라이버(UMD)에게 넘겨준다. UMD 에 대한 설명은 바로 다음 섹션임.
  • 설정된 스테이트를 유지, 관리
  • 파라미터 유효성이나 그 외 에러나 일관성 검사 수행
  • 유저가 접근 가능한 리소스 관리
  • 셰이더 유효성 검사나 링키지(linkage)처리(D3D 는 이 단계에서 처리하지만, OpenGL 은 드라이버단에서 처리함)
  • 그 외 몇가지 작업들


유저 모드 그래픽 드라이버(user mode graphics driver, UMD)

CPU단의 마법들이 일어나는 곳. 니가 짠 코드가 호출한 API 에서 크래시가 발생할 때 운영체제가 "여기서 디졌어염 뿌우"하고 일러바치는 곳. 엔비디아는 nvd3dum.dll, AMD는 atiumd*.dll 뭐 그런 거.

유저모드라는 이름에서 알 수 있듯 응용 프로그램 코드와 같은 컨텍스트, 주소공간에서 돌아가고 권한 상승도 없다.

D3D가 호출하는 저수준 API(DDI)를 구현한 곳. DDI는 D3D API 와 유사한 형태인데, 메모리 관리와 같은 부분이 좀 더 명시적으로 처리되는 등의 차이가 있음.

(주 : 이 부분 부터는 순서도 좀 다르게 편집했고 의역도 엄청 들어감미다..)

셰이더 컴파일 과정의 일부가 처리되는 곳. 물론 셰이더는 HLSL컴파일러나 D3D API 런타임에서 처리되는 부분도 있기는 한데, 이러한 과정을 거친 것은 shader token stream 이며 UMD는 이를 받아 하드웨어와 밀접한 저수준의 컴파일단계를 처리한다.

좀 더 상세히 설명하자면, UMD 이전 단계에서의 셰이더 코드는 다음과 같은 검사를 거치게 된다.
  • 문법적으로 올바른지
  • D3D 제한사항들을 올바르게 지키고 있는지. 제한사항은 다음과 같다.
    • 올바른 타입을 사용하였는가
    • 텍스처/샘플 갯수 제한을 넘지 않았는가
    • 상수 버퍼 갯수 제한을 넘지 않았는가 등.
또한 여러 고수준 최적화가 적용된다. 이는 다음과 같다.
  • 다양한 루프 최적화
  • 사용하지 않는 코드 제거
  • constant propagation (주 : 어떤 수식이 컴파일타임에 상수로 계산될 수 있으면 그 식을 상수로 치환하는 것)
  • 분기 예측 등
UMD는 이러한 비싼 최적화들이 적용된 결과물을 넘겨받기 때문에 그만큼 부담을 덜게 된다.

UMD에서 수행되는 여러가지 저수준 최적화도 있는데, 이는 다음과 같다.
  • 레지스터 할당
  • loop unrolling (주 : 루프를 반복된 코드로 확장하는 것) 등
간단히 얘기하자면, 중간코드(Intermediate Representation, IR) 로 변환된 것을 뭔가 조금 더 컴파일해주는 정도라고 할 수 있는데, 이는 (저수준)컴파일러가 뭔가 좋은 결과를 내기 위해 엄청난 일을 할 필요가 없을만큼 D3D 바이트코드가 셰이더 하드웨어에 충분히 가깝기 때문이다. 하지만 그럼에도 불구하고 D3D가 알 필요도 없고 신경쓰지도 않는 하드웨어와 관련된 세부사항들이 있기에, 이 단계는 결코 사소한 단계라고 볼 수 없다. 이러한 세부사항들은 다음과 같다.
  • 하드웨어 자원 제한
  • 스케줄링 제약

그리고 유명한 게임들에 대해서는 엔비댜와 암드의 프로그래머들이 그 셰이더를 디벼보고 직접 새로 최적화해 작성한 셰이더가 대신 돌아가게 하는 경우가 있는데, 그러한 해당 게임의 검출과 셰이더 교체 또한 이 UMD 단에서 이뤄진다.

그리고 또한 몇몇 API state 들은 최종적으로 셰이더에 컴파일되어 적용된다. 예를 들어 드물게 사용되는 기능인 텍스처 경계 상태(texture border state)는 샘플러 코드에 구현되는 것이 아니라 셰이더에 추가 코드로서 에뮬레이션된다.(혹은 쌩까고 아예 동작하지 않도록 무시되기도 함). 이 말인 즉슨 한 셰이더가 대해 각각의 state 조합들이 적용된 여러 버전의 셰이더들이 존재할 수 있다는 얘기임.

덧붙여 말하자면 이게 바로 어떠한 셰이더나 리소스를 새로이 사용할 때에 지연이 발생하는 이유임ㅇㅇ. 왜냐면 수많은 생성/컴파일 작업이 드라이버에 의해 가능한한 늦춰지거든. 언제까지? 걔가 필요할 때 까지. 그래픽스 프로그래머들은 이 이야기를 다른 방향으로 접해서 알고 있는데, 그건 바로 뭔가가 그저 메모리만 공간만 확보받은 채로 있는 게 아니라 확실하게 생성되었음을 보장받으려면 그 뭔가를 사용하는 더미 드로우콜을 날려주는 것으로 "워밍업"을 하라는 것이다. 지저분하고 번거롭지만 이건 글쓴이가 1999년에 3D 하드웨어를 처음 시작했을 때 부터 주욱 그래왔으니 어쩌겠음? 그냥 익숙해지셈.

UMD는 또한 D3D9 "legacy" 셰이더들과 fixed pipeline 들도 처리한다. 물론 그들은 새로운 버전의 셰이더로 변환되어 사용된다. (그리고 그렇게 처리한지도 좀 되었음)

텍스처 생성 명령 처리 같은 메모리 관리도 UMD 에서 처리된다. 실질적으로는 KMD(kernel mode driver) 로 부터 얻은 커다란 메모리 영역을 다시 작은 영역으로 나눠 할당하는 것이다. 비디오 메모리의 어느 영역을 UMD가 볼 수 있는지, 그리고 시스템 메모리의 어느 영역에 GPU 가 접근할 수 있는지 매핑하는 것은 KMD의 권한이므로 UMD가 처리할 수 없다.

UMD는 텍스처의 swizzling 처리(주:원문 글쓴이의 다른 글로 링크함 shader 에서 쓰는 swizzling 과는 다른 개념이며 텍스처 처리시 캐히 히트율을 높이기 위해 연속된 공간에 배치되는 픽셀의 순서를 바꾸어 저장한 텍스처 데이터를 어드레싱하기 위해 주소(오프셋)값의 비트 순서를 바꾸는 것을 말함..인데 언젠가는 원문도 각잡고 번역하든 요약정리를 하든 할 것임. 일단 그 전에는 원문 링크로...아. 이런 건 각주 시스템이 있으면 각주로 다는 게 좋을 거 같네ㅋ)를 하고 시스템 메모리와 (매핑된) 비디오 메모리간의 전송을 스케줄링하는 것과 같은 일을 처리하기도 한다. 물론 swizzling 의 경우 GPU 가 3D 파이프라인이 아닌 2D blitting 유닛을 통해 처리할 수도 있는데, 이렇게 할 수 없는 경우 UMD 에 의해서 CPU단에서 처리되는 것이지만. 가장 중요한 사실은 UMD 가 커맨드 버퍼(혹은 DMA 버퍼. 원문 글쓴이께서 커맨드버퍼와 DMA버퍼라는 단어를 섞어서 쓰시겠답니다ㅋ)의 내용을 작성한다는 것인데, 이 커맨드 버퍼는 KMD가 할당한 것이다. API런타임을 통해 요청한 상태 전환, 그리기 작업 등은 UMD에 의해 하드웨어가 알아먹는 커맨드로 변환된다. 텍스처나 셰이더를 비디오 메모리로 업로드하는 작업 등과 같은 많은 작업들이 이러한 변환에 의해 알아서 호출되게 된다.

일반적으로 드라이버는 UMD로 최대한 많은 처리 요청을 밀어넣으려 할 것인데, 왜냐면 UMD는 유저모드 코드이기 때문이다. 그래서 UMD에 의해 처리되는 작업들은 비싼 커널모드 전환이 필요치 않고, 자유롭게 메모리를 할당할 수 있으며, 작업들을 멀티스레드로 돌릴 수 있고, 등등등.. 왜냐면 UMD는 보통의 DLL로 만들어져 있으니까. (물론 애플리케이션에 의해 바로 로딩되는 것은 아니고 API에 의해 로딩되는 것이긴 하지만.) 이는 드라이버 개발도중 만약 UMD가 크래시하면 이를 사용하는 앱 역시 크래시하지만 시스템은 크래시하지 않고, 시스템이 돌아가는 동안 간단히 교체할 수 있으며(왜냐면 그냥 DLL이니까!), 일반적인 디버거로 디버깅할 수 있는 등의 장점이 있다. 편리하고 효율적이져.


"유저 모드 드라이버" 가 아니라 "유저모드 드라이버들"

UMD는 이미 말했던바와 같이 단지 DLL이다. D3D의 가호를 받고 KMD에 곧바로 연결되어있기는 해도, 여전히 보통의 DLL이다. 그래서 호출한 프로세스와 같은 주소공간에서 돌아간다.

하지만 요즘의 OS들은 멀티태스킹이다. 그리고 사실 그런지 이미 좀 되었지.

계속해서 얘기하고 있는 이 "GPU"라는 물건은 공유 자원이다. 메인 디스플레이를 구동하는 GPU는 단 하나만 존재한다(SLI 나 크로스파이어 구성을 했더라도 말이지). (주 : 이러한 하나 뿐인 GPU를 멀티태스킹 OS에서 공유하여 쓸 수 있게 만드는 것은) 자동으로 되지 않는다. 옛날에는 3D를 사용하는 애플리케이션 하나가 활성화되면 다른 모든 애플리케이션들은 접근을 하지 못하게 하는 식으로 처리했다. 하지만 윈도우 시스템을 렌더링하는 데 GPU를 쓴다면 그렇게 할 수는 없다. 이것이 바로 GPU로의 접근을 중재하여 시분할로 처리해주는 같은 작업들을 행하는 구성요소가 필요한 이유이다.


스케줄러

스케줄러는 시스템 구성요소이다. 하지만 여기서 얘기하는 것은 CPU스케줄러나 IO스케줄러에 대해서가 아니라 그래픽스 스케줄러임을 명심하자. 스케줄러가 행하는 것은 니가 생각하는 바로 그것이다. 3D 파이프라인을 사용하려는 여러 앱들의 접근을 시분할로 중재해주는 것이져. 컨텍스트 스위칭이 발생하면, 최소한 GPU 의 상태 변환은 일어나고(이를 처리하기 위해 커맨드 버퍼에 추가 커맨드가 들어감), 비디오 메모리로 리소스가 올라가거나 내려오는 스와핑이 일어날 수도 있다. 그리고 당연히 어떤 한 시점에서 3D 파이프라인에 커맨드를 주는 것은 단 하나의 프로세스이다.

콘솔 프로그래머들은 PC 3D API 의 상당히 고수준이며 직접 제어할 수 없고 알아서 처리되는(hand-off) 특성에 의해 야기되는 성능 비용에 대해 종종 불만을 표시한다. 하지만 PC 의 3D API 나 드라이버가  해결해야 하는 문제들은 콘솔 게임에 비해 더 복잡하다. 예를 들자면 PC 에서는 모든 스테이트의 변화를 관리하여야 하는데, 이는 다른 앱이 언제 상태를 바꿔버릴지 알 수 없기 때문이다. 또한 PC 에서는 망가진 애플리케이션을 우회하고 이로 인해 발생하는 보이지 않는 성능 문제를 해결해야 한다. 이는 드라이버 개발자 자신을 포함해서 누구에게도 즐겁지 않은 상당히 성가신 관행이지만, 현실 세계에서는 비즈니스 관점이라는 게 있으니까여. 사람들은 잘 돌아가고 있는 것은 계속해서 (아주 부드럽게) 잘 돌아가기를 바라져 넵. 애플리케이션에다 대고 "그건 틀렸어 임마!"라고 외치고는 부루퉁한 표정으로 엄청 느린 길을 택해서 간다면 친구를 아무도 사귈 수 없을겁니다.


커널 모드 드라이버(kernel-mode drver, KMD)

KMD 는 하드웨어를 다루는 부분이다. UMD는 동시에 여러 인스턴스가 실행중일 수 있지만, KMD는 항상 오직 하나만 실행될 수 있다. 그리고 만약 그게 크래쉬된다면 잦되는거져ㅋ. 예전에는 블루스크린이었지만, 요즘 윈도는 크래쉬된 드라이버를 죽였다 다시 살리는 게 가능하다. 물론 이건 최소한 커널 메모리 손상이 아닌 단순한 크래시일때만 가능하다. 그렇지 않다면 끝장인거임ㅋ

커널 모드 드라이버는 모든 '단 하나 뿐인 것들'을 다룬다. 여러 앱이 GPU 메모리를 두고 다투지만, 실제 GPU 메모리는 하나밖에 없다. 누군가는 명령을 내리고 실제로 메모리를 할당하며 이를 매핑해야한다. 이와 유사하게 누군가는 시스템이 시작될 때 GPU를 초기화하거나, 디스플레이 모드를 설정하거나 디스플레이로부터 모드 정보를 얻어온다던가 하드웨어 마우스 커서를 관리하는 일, 하드웨어 타이머를 설정하여 GPU가 일정 시간 응답이 없으면 리셋을 하는 일, 인터럽트에 응답하는 일 등을 해야 한다. 이것이 바로 KMD가 하는 일이다.

또한 컨텐츠 보호 / DRM과 관련하여 비디오 플레이어와 GPU간에 보호/DRM 된 패스를 설정하여, 지저분한 유저 모드 코드들 통해 디코딩된 소중한 픽셀들이 드러나는 일이 없도록 해서, 이를 디스크에 덤프한다던가 하는 등의 끔찍한 일이 일어나지 않도록 해야 한다. KMD 는 이에 일부 관여한다.

가장 중요한 사실은 KMD가 관리하는 커맨드 버퍼가 하드웨어가 실제로 처리하는 바로 그 커맨드 버퍼라는 것이다. UMD 가 생성하는 커맨드 버퍼는 실제 커맨드 버퍼가 아니다. GPU 가 접근 가능한 랜덤한 메모리 조각에 불과하다. 실제로는 다음과 같은 일이 일어난다. UMD 가 커맨드 버퍼 작업을 마치면 이를 스케줄러에 접수시킨다. 스케줄러는 프로세스가 깨어날 때 까지 기다렸다가 UMD 커맨드버퍼를 KMD 커맨드버퍼에 넘겨준다. 그러면 KMD는 (UMD) 커맨드 버퍼를 호출하는 명령을 주 커맨드 버퍼에 써넣는데, 만약 GPU 가 메인 메모리를 읽을 수 없을 경우 (UMD) 커맨드 버퍼의 내용을 비디오 메모리로 먼저 DMA 한 다음에 이 작업을 수행한다. 메인 커맨드 버퍼는 보통 (아주 작은) 링 버퍼로, 거기에 실제 쓰여지는 것은 시스템/초기화 명령과 실제 3D 커맨드 버퍼를 호출하는 명령밖에 없다.

어쨌든 이 커맨드 버퍼는 메모리상에 존재하는 버퍼일 뿐이다. 그래픽 카드에게 이 버퍼의 위치는 보통 읽기 포인터와 쓰기 포인터를 통해 알려진다. 읽기 포인터는 주 커맨드 버퍼의 어디를 GPU 처리하고 있는지를 표시하며, 쓰기 포인터는 KMD 가 명령을 써 놓은 위치가 어디까지인지를(좀 더 정확히 얘기하자면 KMD 가 GPU 에게 어디까지 명령을 작성했는지 알려줬는지를) 나타낸다. 이들은 하드웨어 레지스터로, 메모리에 매핑되어 있으며 KMD 가 이를 주기적으로 업데이트한다(보통 KMD 가 새로운 작업 덩어리를 접수시킬 때 업데이트한다).


버스(bus)

KMD 가 이렇게 써 놨다고 해서 이게 바로 그래픽 카드로 가는 것은 아니다(그래픽 카드가 CPU 다이에 통합되어 있지 않은 다음에야). 먼저 버스를 통과해야 한다. 요즈음은 PCI 익스프레이스이다. DMA 전송 등도 같은 경로를 거친다. 이는 오래 걸리는 것은 아니지만, 어쨌든 이 여행의 또다른 단계이다. 그리고 마침내...


커맨드 프로세서(command processor)!

커맨드 프로세서는 GPU 의 프론트엔드이다. KMD 가 작성한 커맨드들을 실제로 읽어들이는 것이 이곳이다. 이미 글이 충분히 길어졌기에, 나머지는 다음 포스팅에서 계속.


OpenGL 의 몇가지 차이점들

OpenGL 은 이 글에서 설명한 내용과 매우 비슷하다. 하지만 API 와 UMD 가 딱 잘라 나뉘어지지 않는다. 그리고 D3D 와는 달리, (GLSL)셰이더 컴파일은 API가 아니라 드라이버단에서 전부 처리된다. 따라서 3D 하드웨어 제조사들마다 하나씩 GLSL 프론트엔드를 구현하게 되는데, 이는 기본적으로는 같은 스펙을 구현한 것이지만 각자의 버그나 특이성을 갖게 되는 부작용이 발생하게 된다. 노잼이져. 그리고 이는 드라이버가 셰이더를 만날 때 마다 최적화 - 그 '비싼' 최적화들을 포함해서 - 를 처리해 한다는 것을 의미한다. D3D 의 바이트코드는 이런 문제에 대한 매우 깔끔한 해결책이다. 컴파일러가 하나밖에 없으니까. (따라서 제조사간에 미묘하게 달라서 호환되지 않는 방언들이 존재하는 문제도 발생하지 않게 된다.) 그리고 이는 니가 보통 수행하는 비싼 데이터 흐름 분석을 가능하게 한다. <- 원문은 'and it allows for some costlier data-flow analysis than you would normally do' 인데 costlier data-flow analysis 가 뭘 의미하는지 감이 안 잡히네? 뭔가 특정한 걸 의미하는 거 같은데...


생략되고 간소화된 부분들

이 글은 개요이기에 엄청나게 많은 미묘한 사항들을 적당히 얼버무리고 넘어갔다. 예를 들자면 스케줄러의 경우 딱 하나만 있는 것이 아니고 여러 가지 구현이 존재할 수 있다(드라이버가 이를 선택할 수 있음). CPU 와 GPU 간의 동기화에 대해서도 아직까지 전혀 설명하지 않았으며, 기타 등등 여러가지가 있다. 그리고 내가 무언가 중요한 것을 완전히 까먹고 있을 수도 있는데, 이러한 것은 고칠 수 있도록 알려 달라. 하지만 지금은 안녕. 담에보아~
2016/09/13 00:18 2016/09/13 00:18