A trip through the Graphics Pipeline 2011 요약정리와 번역 중간 어디쯤(…) – Part 2

(이하는 A trip through the Graphics Pipeline 2011, part 2 를 번역한 내용임돠)

Part 2 – GPU 메모리 구조와 커맨드 프로세서

Not so fast

이 전 글에서 3D 렌더링 명령들이 GPU 에 도달하기까지 PC에서 거쳐가는 다양한 단계들에 대해 설명했슴다. 한 줄로 요약하자면 ‘님이 생각한거보다 더 복잡함’ 이져. 그리고는 커맨드 프로세서를 소개하고, 그것이 우리가 꼼꼼하게 준비한 커맨드 버퍼와 실질적으로 어떤 관련이 있는지 설명했구여. 음..어케 말해야되지.. 실은 거짓말을 했슴다. 실제로는 커맨드 프로세서를 이 글에서 처음 만나게 될 거예여. 이 커맨드 버퍼 어쩌구 하는 것은 모두 메모리를 통과한다는 사실을 명심하세여. 그 메모리는 PCI 익스프레스 버스를 통해 접근하는 시스템 메모리든, 로컬 비디오 메모리든 어쨌든 메모리임다. 그래픽스 파이프라인을 차례대로 살펴보기로 한 만큼, 커맨드 프로세서에 대해 알아보기 전에 이 메모리에 대해 먼저 잠깐 살펴보도록 하져.

메모리 서브시스템

GPU 는 범용의 CPU 나 다른 하드웨어에서 볼 수 있는 메모리 서브시스템과는 다른 형태의 메모리 서브시스템을 가지고 있슴다. 왜냐면 사용 패턴이 매우 다르기 때문이져. GPU의 메모리 서브시스템은 일반적인 머신에서 볼 수 있는 것과는 근본적으로 두 가지 면에서 차이가 남돠.

첫째로 GPU 의 메모리 서브시스템은 빠릅니다. 엄청 빠르져. Core i7 2600K 의 메모리 대역폭은 19GB/s 정도를 찍을 거예여. 날씨가 좋고 미풍이 불 때 다운힐을 한다면 말이져(…). 한편 GeForce GTX 480의 경우, 총 메모리 대역폭은 180GB/s 에 근접함다. 거의 한 자릿수가 차이나네여? 헐.

두번째는 GPU 메모리 서브시스템은 느립니다. 엄청 느려여. AnandTech에서 제공한 메모리 지연시간 자료에 클럭속도를 곱하면, 네할렘(1세대 Core i7) CPU에서 캐시 미스가 발생한 경우 약 140사이클 정도가 지연되는 걸 알 수 있슴다. 방금 언급한 GeForce GTX 480 의 경우 400에서 800사이클이 지연되져.(주 : 원문의 링크가 깨져서 같은 문서인 걸로 보이는 것을 찾아서 연결했는데, 원래 링크가 살아있을 때 본 적이 없어서 맞는지는 모르겠네여. 내용을 보니 맞는 거 같긴 한데…) 따라서 사이클 단위로 얘기한다면, GeForce GTX 480 은 Core i7 에 비해 4배가 조금 넘는 메모리 지연 시간을 가지고 있다..라고 할 수 있겠네여. 근데 방금 얘기한 Core i7 은 2.93GHz 로 동작하는 한편, GTX 480 의 셰이더 클럭은 1.4GHz 로 동작하니까.. 여기서 다시 2를 더 곱해줘야겠네여. 헐. 이거 또 한 자릿수 차이네여? 뭔가 희한한 일이 벌어지고 있군여. 이거 보아하니.. 뉴스에서 계속 떠들어대던 그 트레이드오프 라는 거 중에 하나구만!

넹 그렇슴다. GPU 는 대역폭을 엄청나게 증가시키는 대가로 엄청난 지연 시간을 지불한 거져(그리고 결과적으로 상당한 수준의 전력을 소비하게 되었지만, 이건 이 글의 범위를 벗어나니까). GPU 는 지연시간보다 처리량을 우선한다, 이게 일반적인 패턴이라는 것임다. 다시 말해 어떤 작업을 했는데 아직 결과가 안 나왔다면, 그걸 기다리고 있을 게 아니라 뭔가 다른 걸 한다! 라는 거져.

이게 바로 GPU 메모리에 대해 알아야 할 거의 대부분임다. 여기에 한 가지 더. DRAM에 대한 일반적인 내용인데, 이게 또 앞으로 계속 중요할거예여. DRAM 은 2D 격자 구조임다. 물리적으로든 논리적으로든 둘 다 그렇져. (가로) 행과 (세로) 열이 있어여. 이러한 행과 열이 교차하는 지점에는 트랜지스터와 커패시터가 있슴다. 여기서 이런 재료를 가지고 실제 메모리를 구성하는 방법이 궁금하다! 그러면 위키백과가 여러분의 친구가 되어줄거예여. 어쨌거나, 여기서 주목해야 할 부분은 DRAM 내부의 위치를 나타내는 주소는 행 주소와 열 주소로 나뉘어진다는 것이져. 그리고 DRAM 은 내부적으로 한 번에 읽기 쓰기를 할 때 항상 주어진 행의 모든 열에 접근을 한다는 것두여. 이것은 같은 분량의 접근이라 해도 DRAM 의 한 줄 내에서 다 이뤄질때가 여러 줄에 나뉘어 접근하는 경우보다 훨씬 싸게 먹힌다는 것을 의미하져. 지금은 이게 DRAM 에 관한 잡다한 정보들 중 하나인것 처럼 보이지만 앞으로 계속 더 중요해질검돠. 명심하셈. 시험에 꼭 나오는 거예여. 이러한 사실을 이 전 문단의 내용과 엮어 얘기해 보자면, 몇몇 바이트씩 메모리의 여기저기서 읽어대는것으로는 위에서 얘기한 메모리 대역폭 최고치에 도달할 수 없다 라는 것이져. 메모리 대역폭을 꽉 채우려면, 한 번 읽을 때 DRAM 한 줄을 완전히 채워서 읽어야 한다 이말임다.

PCIe 호스트 인터페이스

그래픽스 프로그래머 입장에서 하드웨어의 이 부분은 그닥 흥미롭지가 않슴다. 사실 GPU 하드웨어 구조도 마찬가지져. 하집만 중요한 건 얘가 너무 느려서 정체구간이 되기 때문에 언젠가 신경을 쓸 수 밖엔 없다는  사실이져. 따라서 님이 해야만 하는 건 이걸 잘 처리할 수 있는 능력자들을 구해서 이런 일이 일어나지 않게 하는거져(…). 그렇지 않다면 CPU 가 비디오 메모리나 여러 GPU 레지스터를 를 읽고 쓰는 일이라던가 GPU 가 메인 메모리(의 일부)를 읽고 쓰는 일이 이 PCIe 호스트 인터페이스를 통해서 일어나야 하는데, 이게 엄청 느려서 모두가 골치가 아플 거란 말이져. 얘가 느리기로는 메모리 지연보다도 더 심한데, 그게 신호가 칩에서 일단 나온 담에 슬롯으로 갔다가 메인보드 일부를 거쳐서 CPU어딘가에 도착하는데 한 1주일은 걸린단 말이져(아님 뭐 CPU/GPU 속도에 비해서 그만큼 느리게 느껴진다..라는 것이거나). 대역폭 자체는 그렇게 나쁘지 않슴다. 요새 GPU 대부분의 총 대역폭은 최대 8GB/s 로, 이는 CPU 총 대역폭의 절반에서 삼분의 일 정도로 쓸만한 수준임다.  그리고 옛날 표준이었던 AGP 가 CPU 에서 GPU 로의 채널은 고속인데 비해 반대 방향은 그렇지 못했던 것과 달리, PCIe 는 CPU와 GPU 간에 대칭형 연결 구조로 동작하져.

Some final memory bits and pieces

이제 3D 커맨드를 볼 수 있는 곳까지 정말 아주 가까이 왔슴다. 너무 가까이 와서 거의 맛을 볼 수 있을 정도져(주 : 이양반 드립 보소…). 하지만 여길 벗어나서 3D  커맨드로 가기위해서는 선택을 내려야 하는 것이 한 가지 더 있슴다. 바로 메모리에 대한 것인데, 메모리에는 (로컬) 비디오 메모리와 매핑된 시스템 메모리 두 종류가 있기 때문이져. 하나는 북쪽을 향해 하루 정도 가야 하고, 하나는 PCI Express 고속도로를 타고 1주일 정도 남쪽으로 가야 함돠(주 : 노스브리지와 사우스브리지 칩셋에 대한 비유인듯). 어느 쪽을 택해야할까여?

쉬운 해결책은 어디로 갈 지 알려주는 별도의 어드레스 라인을 추가해주는것임다. 간단하고 잘 동작하면서 많이 사용되었던 방법이져. 아니면 몇몇 콘솔에서처럼(하지만 PC는 해당사항이 없는) 단일 메모리 구조(Unified Memory Architecture) 로 가는 방법도 있슴다. 이 경우에는 ‘어느 쪽’으로 가느냐의 선택지가 없슴다. 그냥 ‘메모리’져. 뭔가 좀 더 근사한 걸 원한다면 MMU(메모리 관리 유닛, Memory Management Unit)을 추가하는 방법이 있슴다. 이렇게 하면 주소 공간을 완전히 가상화해서 빈번하게 접근이 일어나는 텍스처 부분들은 비디오 메모리(얘네는 빠르져)에 넣어두고, 다른 부분들은 시스템 메모리에, 그리고 나머지 대부분은 메모리에 아예 매핑하지도 않고 갑자기 짠 하고 나타나게 한다던가(주 : …) 아니면 좀 더 흔하게는 디스크에서 읽어들이는 마법을 부리게 할 수 있슴다. 근데 이 디스크에서 읽어들이는 게 50년 쯤 걸리는게 문제져. “메모리 접근 = 하루” 라는 비유를 기준으로 하면 HD(주 : 하드디스크인듯)에서 읽어들이는 건 정말로 이 정도 걸림돠. 제법 빠른 하드일 경우에여. 엿같져. 근데 옆길로 좀 샜네여.

그럼 MMU를 보도록 하져. 얘는 비디오메모리가 거의 다 썼을  때 실제로 데이터를 카피한다던가 하는 작업 없이도 비디오 메모리 주소 공간의 단편화를 제거할 수 있게 해 줌다. 짱이네염. 그리고 여러 프로세스가 하나의 GPU를 공유하는것을 훨씬 쉽게 만들어주기도 하구여. 이걸 하나 이용할 수 있긴 한데, 반드시 있어야한다고 규정되어있는지는 확실치가 않네여. 있으면 확실히 좋은 건 분명한데 말이져 (이 부분에 대해서 누구 좀 도와주실 분? 여기에 대해서 확실히 알게 되면 이 글을 업데이트할텐데, 지금은 솔직히 이걸 뒤져 볼 기분이 아니라서염(…)). 어쨌든, MMU/가상메모리는 캐시와 메모리 일관성consistency을 고려하지 않는 구조에서라고 해도 간단히 갖다 붙일 수 있을 만한 것이라고 할 수는 없슴다. 그런데 어떤 특정한 단계stage에 속한다고 하기도 어려운데, 어쨌든 어딘가에서 반드시 언급을 해야하긴 하기에 여기다 집어넣었슴다.

DMA엔진도 있슴다. 비싼 3D 하드웨어/셰이더 코어을 끌어들이지 않고 메모리를 이리저리 복사할 수 있져. 보통은 적어도 시스템 메모리와 비디오 메모리 간의 (양방향) 복사를 처리함다.  종종 비디오 메모리에서 비디오 메모리로 복사가 가능하기도 하져(VRAM 단편화 제거를 해야 할 때 유용하겠져). 보통 시스템 메모리에서 시스템 메모리로 복사는 처리할 수 없슴다. 메모리 복사 유닛이 아니라 GPU이기 때문이져. 시스템 메모리간 복사는 PCIe 버스를 두 번이나 통과할 필요가 없는 CPU에서 처리해야져.

업데이트 : 그림을 하나 그렸슴다. (그림을 집어넣기엔 레이아웃이 너무 좁아서 링크로 처리함다.) (…라고 저자가 썼으나 그냥 바로 그림 집어넣음ㅇㅇ 여긴 공간이 될거 같아서염 : 주)

이 그림에서 몇 가지 세부사항을 더 확인할 수 있슴다. GPU에는 여러 개의 메모리 컨트롤러가 달려 있고, 각각의 컨트롤러에는 여러 개의 메모리 뱅크를 제어하져. 그리고 이들의 앞에는 대역폭이 얼마나 필요하든 처리할 수 있을만큼 충분한 메모리 허브가 있슴다.

여기서 정리를 한 번 해 보져. CPU 단에서는 커맨드 버퍼가 준비되었슴다. 그리고 PCIe 호스트 인터페이스가 있는데, 이걸 통해서 CPU는 커맨드 버퍼에 대해 알려주고 그 주소를 레지스터에 쓸 수 있슴다. 그리고 그 주소로부터 데이터를 가져오는 로직이 있는데, 이는 다음과 같이 동작하져: 비디오 메모리에 커맨드 버퍼가 있어서 시스템 메모리로부터 (커맨드 버퍼 데이터가) PCIe 를 통해 전송되어야 할 경우, KMD가 DMA 전송을 준비해서 CPU든 GPU의 셰이더 코어든 어느 쪽도 계속해서 신경 써야 할 필요가 없게 해 줌다. 그러면 비디오 메모리에 메모리 서브시스템을 통해 접근할 수 있는 사본이 마련되져. 모든 경로가 파악되었으니, 바야흐로 몇몇 커맨드에 대해 알아볼 준비가 완료되었군여!

마침내 바로 그 커맨드 프로세서!

커맨드 프로세서에 대한 얘기는 요즈음 많이 볼 수 있는 다음 단어로 시작함다.

“버퍼링…”

앞서 언급한 바와 같이, 커맨드 프로세서까지의 메모리 경로 양쪽 모두 대역폭이 넓지만 동시에 지연시간 또한 큼다. 최근의 GPU 파이프라인에 있어서 이러한 문제를 우회하는 방법은 다수의 독립적 스레드를 돌리는 것이져. 하지만 이 경우에도, 커맨드 프로세서는 단 하나이고, 이 커맨드 프로세서는 커맨드 버퍼를 순서에 맞게 처리할 필요가 있슴다(왜냐면 이 커맨드 버퍼에는 상태 변경이라던가 올바른 순서대로 처리되어야 하는 렌더링 커맨드들이 들어 있기 때문이져). 그래서 차선택을 수행함다. 충분히 큰 버퍼를 마련해서 hiccup을 피할 수 있을만큼 많은 양의 커맨드들을 미리 가져다 두는 거져.

이 버퍼에서 커맨드들은 실제 커맨드 처리 프론트엔드로 보내어지는데, 이 프론트엔드는 기본적으로 어떻게 (하드웨어 특정적인 형태의) 명령들을 파싱해야하는지 아는 상태기계져. 몇몇 명령은 2D 렌더링과 관련이 있는데, 이는 별도의 2D 커맨드 프로세서가 없을 경우임다. 별도의 2D 커맨드 프로세서가 있으면 3D 프론트엔드는 이런 명령들을 볼 수 조차 없져. 어느 쪽이든 요즘 GPU들에는 여전히 2D 전용 하드웨어가 감춰져 있슴다. 다이 어딘가에 텍스트모드라던가 픽셀당 4비트 비트플레인 모드, 부드러운 스크롤 등을 처리하는 VGA 칩이 존재하는 것과 마찬가지로 말이져. 현미경 없이 다이에서 이런 것들을 찾아내는 데에 행운이 함께하기를. 어쨌든, 이러한 요소들이 존재하지만 앞으로 얘네들에 대해 언급하지는 않을거예여. 그리고 프리미티브들을 3D/셰이더 파이프로 넘겨주는 명령들이 있슴다. 유후~! 얘네는 다음 파트에서 다룰거예여. 3D/셰이더 파이프로 가는 또다른 명령들 중에는 여러 가지 이유로 인해(그리고 여러 파이프라인 설정하에서) 아무 것도 그리지 않는 명령도 있슴다. 얘네는 더 나중에 다룰게여.

그리고 상태를 변경하는 명령들이 있져. 프로그래머라면 얘네는 단순히 어떤 변수 값을 바꾸는 거라고 생각할 거예여. 그리고 기본적으로 그렇기도 하져. 하지만 GPU 는 대규모 병렬 컴퓨터massively parallel computer 이기에, 이러한 병렬 시스템에서 전역변수의 값을 그냥 바꾸고 아무 일 없기를 기대할 수는 없어여. 어떠한 강제된 불변식invariant에 의해 모든 것이 제대로 돌아갈 것이라는 보장이 있지 않는 한, 어딘가 버그가 있을 테고 언젠가는 그게 터질테니까여. 잘 알려진 여러 방법들이 존재하는데, 기본적으로 모든 칩들은 각각의 상태들에 대해 각기 다른 방법들을 사용함다.

  •  상태를 바꿀 때 마다, 그 상태값을 참조하는 모든 작업들은 완료되어야 함다(기본적으로 파이프라인의 일부를 비우는flush 것이져). 역사적으로 이 방법이 그래픽 칩들이 대부분의 상태 변경을 처리하는 방식임다 – 간단하고 비싸지 않져. 작업batch의 갯수가 적고, 삼각형 수도 얼마 안 되고 파이프라인도 짧다면 말이져. 그런데 맙소사, 작업과 삼각형 갯수가 늘어나고 파이프라인도 길어지면서, 이런 식의 접근에 드는 비용이 엄청 치고 올라가버렸네여. 물론 다음과 같은 경우에는 여전히 이런 방식이 사용되고 있슴다 : 자주 바뀌지 않는 상태거나(수십 번 정도의 부분적인 파이프라인 비우기flush 는 한 프레임 전체에 대해서는 그닥 큰 몫을 차지하지는 않져), 특정 방식으로의 구현 결과물이 매우 고비용이거나 구현하기 까다로운 상태일 경우져.
  • 하드웨어를 완전히 상태 없이stateless 구현할 수도 있슴다. 상태 변경 명령을 해당 변경을 처리해야 하는 단계까지 내려보낸 다음, 그 단계의 이후 단계로 보내는 모든 것에 현재 상태값을 덧붙여서 매 싸이클마다 보내면 되져. 상태값은 어디에도 저장되지 않되, 어디에나 함께 하게 되는것임다. 그렇게 하면 파이프라인상의 특정 스테이지에서 어떤 상태가 무슨 값인지 필요할때마다 읽어올 수 있게 되져. 왜냐면 그 상태값은 그 단계까지 넘겨져왔기 때문임다(그리고 그 상태값은 다시 그 다음 단계로 넘겨지게 되져). 만약 상태값을 저장하는 데 몇 비트만 필요하다면, 이 방법은 제법 저렴하고 실용적임다. 그렇지만 만약 그게 샘플 상태를 포함한 사용중인 텍스쳐 전체에 대한 상태값이라고 하면, 별로 효율적이지 못할거예여.
  • 상태값을 한 벌만 저장해두고 사용할 경우 렌더링 파이프라인의 어느 단계에서 상태값을 바꿀 때마다 파이프라인을 비우느라 내보내는 데이터 양이 엄청 많을 수 있슴다. 이럴 때엔 상태값 두 벌(혹은 네 벌)을 이용하게 하면 상태를 설정하는 앞단을 좀 더 앞서나가게 할 수 있져. 각 상태들에 대한 값을 두 벌 저장하기에 충분한 레지스터(슬롯)이 있다고 가정하고, 현재 처리중인 작업이 0번 슬롯을 참조하고 있다고 해 보져. 1번 슬롯의 상태값을 변경하는 것은 (0번 슬롯을 참조하며) 처리중인 그 작업을 멈추거나 영향을 주거나 하는 것 전혀 없이 처리가 가능함다. 이제 파이프라인을 따라 상태값 전체를 보낼 필요 없이, 명령당 0번과 1번 중 어느 슬롯인지를 선택하는데 필요한 1비트만 있으면 됨다. 물론, 슬롯 0번과 1번이 모두 사용중일 때 상태 변경 명령을 만나게 되면 대기해야 할 수 밖에 없겠져. 하지만 어쨌든 조금은 나아졌슴다. 이 기법은 슬롯이 두 개 보다 많을 경우에도 잘 동작함다.
  • 샘플러나 텍스처 Shader Resource View 상태의 경우, 한 번에 엄청나게 많은 수의 상태값을 설정할 수 있지만 실제로 그렇게 사용하는 경우는 잘 없슴다. 그래서 동시에 2벌의 상태값 세트를 운용하는 경우라고 해도, 이를 위해 2*128개의 텍스처 상태값을 저장하기 위한 공간을 예약해두지는 않져. 이 경우엔 일종의 ‘레지스터 이름 변경 기법’을 사용함다. 128개의 텍스처 디스크립터를 가지고 있는 풀pool을 이용하는 것이져. 만약 어떤 셰이더가 한번에 128개의 텍스처를 동시에 설정하려 한다면, 상태값 변경은 매우 오랜 시간이 걸리게 되겠져(주 : 텍스처 값 설정을 위해 결국 모든 물리적 텍스처 디스크립터들이 비워질때까지 이를 참조하는 이전 작업들이 완료되는 것을 기다려야 하기 때문). 그렇지만 보통은 한 번에 20개 미만의 텍스처를 사용할테고, 이 정도 갯수라면 이들을 여러 벌 운용할 만큼 충분한 공간이 되는 것임다.

위 항목들은 상태변경에 있어 모든 것을 포괄하는 것은 아님다. 하지만 요지는 애플리케이션단(혹은 이 문제에 있어서는 UMD나 KMD, 혹은 명령어 버퍼도)에 있어 변수의 값을 변경하는 것과 같이 간단해 보이는 것도, 처리를 하는 데 속도저하가 일어나지 않게 하기 위해서는 무시하지 못할 만큼 많은 분량의 하드웨어 지원이 필요할 수 있다는 것임다.

동기화

마침내 마지막 항목인 CPU/GPU 와 GPU/GPU 간의 동기화 처리 명령어군이네여.

일반적으로 이들은 모두 “이벤트 X가 발생하면 Y를 해라’의 형식임다. “Y를 해라”부분부터 먼저 다뤄볼게여. 여기엔 두 가지 적합한 선택지가 있슴다. 하나는 푸시 모델 통지push-model notification이예여. GPU가 CPU에게 무언가를 당장하라고 소리치는거져(“이봐 CPU! 지금 0번 디스플레이가 수직 귀선 기간vertical blanking interval에 들어가니까 티어링tearing 없이 버퍼를 바꾸려면 지금 해야할걸!”). 다른 하나는 풀 모델pull-model로, GPU가 뭔가 일어난 일들에 대해 기억하고 있고 CPU가 나중에 그걸 물어보는 것이져(“이봐 GPU, 가장 최근에 처리를 시작한 명령 버퍼 조각이 어느거지?” – “어디보자… id 303이네.”). 전자는 보통 인터럽트를 이용해 구현되고 자주 발생하지 않지만 우선순위가 높은 이벤트를 처리하는 쓰임다. 왜냐면 인터럽트가 꽤나 비싸거든여. 후자에 대해 알아야 하는 것은 CPU가 접근 가능한 GPU레지스터와, 어떤 이벤트가 발생했을 때 명령 버퍼로부터 이 레지스터들에 값을 쓰는 것이 어떻게 이뤄지는가 임다.

이러한 레지스터 16개가 있다고 가정하져. 레지스터 0번을 currentCommandBufferSeqId 라고 이름붙이져. GPU에 접수시키는 모든 커맨드 버퍼에 일련 번호를 붙이고(이 작업은 KMD에서 이뤄지져), 각 커맨드 버퍼의 처리를 시작할 때 “커맨드 버퍼 처리가 이 곳까지 도달했을 때, 레지스터 0에 값을 쓴다”는 동작을 추가함다. 그렇게 되면 오호라, 이제 GPU가 어느 커맨드 버퍼를 처리하는지 알 수 있게 되었네여! 그리고 커맨드 프로세서는 명령을 순서대로 엄격하게 처리하니까, 만약 id 303 번 커맨드 버퍼의 첫 번째 명령이 수행되었다면 id 302번을 포함해 그 앞의 커맨드 버퍼들은 모두 처리가 완료되었기에 KMD에 반환되어 할당을 해지하거나, 내용을 수정하거나, 싸구려 놀이공원으로 변경할 수 있다는 것이져.

이제 X가 무엇이 될 수 있는지 예를 들어보겠슴다. “여기까지 왔다면”이 그 중 하나가 될 수 있겠네여. 가장 간단하면서도 매우 유용한 예임다. 또다른 예는 “커맨드 버퍼 이 지점 이전에 오는 모든 셰이더들이 텍스처로부터 읽기를 완료했다면”이 될 수 있겠네여(이는 텍스처/렌더 타겟 메모리를 안전하게 반환할 수 있는 지점을 나타내져). “만약 활성화된 모든 렌더 타겟/UAV로의 렌더링이 완료되었다면” 도 있져(이는 해당 렌더타겟/UAV를 안전하게 텍스처로 이용할 수 있는 지점을 나타냄다). “만약 이 지점까지의 모든 작업이 완료되었다면” 이라던가, 그 외 여러가지가 있을 수 있슴다.

이러한 것들은 종종 담장fence으로 불리기도 하져. 상태 레지스터에 어떤 값들을 써넣을지 선택하는 데는 여러가지 방법이 있지만, 필자가 바람직하다고 보는 것은 순차적으로 증가하는 값을 사용하는 것임다(몇몇 비트는 다른 정보를 저장하는 데 쓸 수 있긴 하지만여). 넵, 지금 제가 어떤 합리적인 근거 없이 어떤 정보 한 조각을 덜렁 던진 것이긴 한데.. 왜 그랬는지 아마 님도 아실듯여. 여기에 대해선 나중에 별도의 포스팅으로 자세히 설명해볼까 함돠(이 시리즈는 아니지만여).

자 여기까지 절반은 왔슴다. 이제 GPU에서 CPU로 상태를 알려줘서 드라이버단에서 제대로 메모리 관리를 할 수 있게 되었네여(특히, 버텍스 버퍼나 명령 버퍼, 텍스처나 기타 리소스에 사용된 메모리를 언제 안전하게 반환할 수 있는지 알수 있게 되었져). 하지만 그게 전부가 아님돠. 퍼즐 한 조각이 빠져있져. 예를 들어, 만약 온전히 GPU단에서만 동기화를 해야 할 경우 어떻게 해야 할까여? 렌더 타겟의 예로 돌아가보져. 렌더타겟은 거기에 렌더링하는 작업이 끝나기 전에는 텍스처로 사용할 수 없슴다(그리고 몇 단계를 더 거쳐야 하는데, 이건 나중에 텍스처링 유닛을 다룰 때 상세히 설명할게여). 해법은 “대기”스타일의 명령어임다: “레지스터 M에 N이라는 값을 담길 때 까지 대기”같은 형태인거져. 값을 비교하는 연산은 “같거나” 가 되거나 “미만이거나”도 될 수 있슴다(이 경우 랩어라운드wraparound 도 고려하여 처리해야함을 유의하세여). 혹은 좀 더 복잡한 식이 될 수도 있는데, 간단히 설명하기 위해 “같거나”의 예를 든 것임다. 이 명령어가 있어 배치batch 를 접수시키기 전에 렌더타겟의 동기화가 가능해지져. 또한 GPU를 완전히 비우는flush 처리도 가능하게 해 줌다: “모든 대기중인 작업이 처리 완료되면 레지스터0번에 ++seqid 값을 기록” 이나 “레지스터0 의 값이 seqId 가 될 때 까지 대기” 같은 형태로 말이져. 좋아여 좋아. GPU/GPU 동기화가 문제가 해결되었네여. 그리고 좀 더 정교한 동기화 형태를 포함하는 Compute Shader 가 탑재된 DX11 이전까지, 이러한 방식은 GPU단 동기화를 처리하는 유일한 방법이었져. 일반적인 렌더링이라면 더 이상은 필요없져.

한편, 이러한 레지스터에 값을 쓰는 게 CPU단에서도 가능하게 되면, 이걸 식으로도 활용 가능함다. 특정 값이 될 때 까지 대기하는 명령을 포함하는 명령 버퍼의 일부를 접수시키고, GPU단이 아닌 CPU단에서 레지스터의 값을 변경하는 것이져. 이러한 기법은 D3D11 에서 지원되는, CPU단에서 락되어있는 버텍스/인덱스 버퍼(아마도 이는 다른 스레드에서 값을 쓰기 위해 락되어있겠져)를 참조하는 배치를 접수하는 식의 멀티스레드 렌더링을 구현하는 데 사용할 수 있슴다. 간단히 실제 렌더 콜 앞에 대기 명령을 채워넣고, 버텍스/인덱스 버퍼가 실제로 언락되었을 때 레지스터에 값을 쓰게 할 수도 있슴다. GPU가 해당 명령을 처리하기 전까지, 그 대기 명령은 단순한 no-op(주:아무 것도 하지 않는 명령)이져. 만약 그 명령을 처리하게 되면(주:그리고 그 때까지 버텍스/인덱스버퍼가 준비 완료되어 언락되지 않았다면), (커맨드 프로세서는) 시간을 보내며 데이터가 마련될 때까지 기다리게 되겠져. 근사하지 않나여? 사실 이러한 방식을 구현하는 건 CPU에서 쓰기 가능한 레지스터 없이도 구현 가능함다. “점프”명령을 통해 명령 버퍼를 접수시킨 후에도 수정 가능하다면 말이져. 자세한 사항은 관심있는 독자들을 위한 몫으로 남겨둘게여.

물론, 레지스터에 값을 쓰고 대기하는 모델이 아닌 다른 방식으로 구현도 가능함다. GPU/GPU 단 동기화를 처리하는데는 렌더타겟이 안전하게 사용할 수 있음을 보장하는 “렌더타겟 배리어rendertarget barrier” 명령과, “전부 비우기” 명령이 있으면 되져. 하지만 필자는 레지스터 형태의 모델을 더 선호함다. “사용중인 리소스의 CPU로의 역보고back-reporting” 와 “GPU단 내부에서의 동기화” 두 가지 몹을 잘 설계된 무기 하나로 일타쌍피할 수 있기 때문이져.

업데이트: 그림을 하나 그렸슴다. (주: 원 포스팅에서는 링크로 처리했지만 이번에도 그냥 그림을 바로 갖다박았슴다.)

좀 복잡해서 나중에 디테일을 좀 줄여 간단하게 만들긴 할텐데, 어쨌든 핵심은 다음과 같슴다: 커맨드 프로세서는 FIFO 가 선두에 있고, 그 담에 명령어 해석 로직이 있구여, 명령어 실행은 2D유닛, 3D 프론트엔드(일반적인 3D렌더링), 셰이더 유닛(컴퓨트 셰이더 처리)등과 같은 여러 블록이 서로 직접 통신하면서 처리됨다. 그리고 동기화/대기 명령을 처리하는 블럭이 있구여(얘네는 언급했던 외부 접근 가능한 레지스터를 가지고 있져), 명령 버퍼 점프/호출을 처리하는 유닛이 있슴다(얘가 FIFO로 가는 명령어를 가져올 주소를 변경하는 처리를 하져). 작업 처리를 요청받은 모든 유닛은 처리가 끝나면 완료 이벤트를 돌려줌다. 그러면 이를 통해 이를테면 어떤 텍스처가 더 이상 사용되지 않으면 거기에 할당된 메모리를 반환한다던가 하는 처리가 가능해지져.

닫는 글

다음으로는 실질적인 렌더링 작업을 처음으로 다루게 됨다. GPU를 다루는 시리즈 3번째 파트에서 마침내 실제 버텍스 데이터에 데이터에 대해 파보게 되는 것이져! (아직 삼각형을 그리는rasterize 건 아녜여. 요건 좀 더 있다가.)

사실 이미 이 단계에서 분기가 벌써 등장함다. 만약 컴퓨터 셰이더를 사용한다면, 다음 단계는… 컴퓨트 셰이더를 실행하는 것이져. 하지만 그러지 않을 거예여. 컴퓨트 셰이더는 좀 더 나중에 다루게 될 거예여. 일반적인 렌더링이 먼저져.

소소한 면책조항 하나: 다시 한 번 말하지만, 이 시리즈는 어떤 큰 그림을 그리면서, 필요한(혹은 재밌을만한) 부분을 상세히 들어다보는 형태로 진행됨다. 하지만 편의상(그리고 쉬운 이해를 위해) 생략된 부분이 많이 있다는 거, 하지만 정말 중요한 것은 빼먹지 않았다는 걸 믿어주십셔. 물론, 틀린 부분이 있을 수 있슴다. 만약 버그를 발견했다면 꼭 알려주세염!

그럼 다음 글에서 만나요~