Optimizing the graphics_pipeline_

2,195 views
2,094 views

Published on

1 Comment
5 Likes
Statistics
Notes
No Downloads
Views
Total views
2,195
On SlideShare
0
From Embeds
0
Number of Embeds
316
Actions
Shares
0
Downloads
70
Comments
1
Likes
5
Embeds 0
No embeds

No notes for slide

Optimizing the graphics_pipeline_

  1. 1. Optimizing the Graphics Pipeline By Cem Cebenoyan and Matthias Wloka 역: overdrv * 본문에 앞서… GDC 2003 에서 발표된 nvidia 문서와 최근 출간된 GPU Gems의 Chapter28. Graphics Pipeline Performance’를 적당히 섞어서 편집 및 번역 하였습니다. 사실 GPU Gems의 내용은 GDC 와 뼈대는 거의 같고 내용상 다소 가감이 있는 정도로 보입니다. 시장에 나온 지 얼마 안된 뜨끈뜨끈한 책의 내용을 번역한다는 점에서 찜찜한 점도 있지만, 어차피 GDC2003 그리고 그보다도 훨씬 전에 nvidia site 에 올라왔던 문서들에서 밝혀졌던 공공연한 내용이고 하니 다소 찔리긴 해도 홍익인간의 이념으로 번역하 게 되었습니다. 부족한 번역 실력이지만 모쪼록 도움이 되시기 바랍니다. 아! GDC2003에서 발표된 문서의 원본은 http://developer.nvidia.com/docs/io/4000/GDC2003_PipelinePerformance.pdf 이곳에서 얻을 수 있습니다. 1. Overview 지난 몇 년간 H/W 가속에 의한 렌더링 파이프라인은 급격하게 복잡해 졌으며, 그로 인해 성능 특성에 관 한 혼란과 난해함 역시도 가중 되었다. 과거, ‘성능향상’ 이라 함은 단순히 우리가 만드는 Renderer 내부 에서 얼마나 CPU cycle을 적게 사용하는가에 관한 문제였지만, 현재는 병목 구간을 찾아내고, 시스템적 으로 어떻게 그 것들을 제거하는 가를 의미하게 되었다. 결국, 정확한 최적화 방법을 알지 못한다면, 투자 해야 하는 시간, 수고에 비해서 얻을 수 있는 것은 상대적으로 적다는 것이다. 모쪼록 이 문서를 통해서 우 리가 원하는 결과를 얻어보자! 1) The Pipeline 최 상위레벨에서 봤을 때, pipeline이라고 하는 것은 CPU 와 GPU 의 두 부분으로 나눌 수 있을 것이다. 물론 CPU 최적화는 우리가 만들려는 application의 전체적인 최적화에 있어서 매우 critical한 부분이 되 지만, CPU 최적화의 대부분은 graphics pipeline과 큰 관계가 없으므로 이 문서에서 다루고자 하는 내용 은 아니다.
  2. 2. 위 그림에서 볼 수 있듯, GPU는 각자 특수한 목적을 가진 별도의 processor처럼 병렬처리 되는 수많은 기능 유닛들을 가지고 있으며, 여러 지점에서 병목현상이 발생 할 수 있다. 그러한 병목 지점으로는
  3. 3. vertex 및 index fetching, vertex shading (T&L), fragment shading, raster operation(ROP) 등 이 있 다. 2) Methodology 병목지점에 대한 적절한 판단이 없다면 결국 많은 시간 낭비만 하게 될 것이므로, 우리는 ‘문제 파악’ 과 ‘ 최적화’ 라는 기본적인 단계로 최적화 과정의 정형화된 절차를 마련했다. 1. 병목지점 파악 - pipeline의 각 stage에 대해서 작업량(workload)을 변화시키거나 H/W의 작업능력(예를 들 어 clock speed 같은…) 을 변화 시켰을 때, application의 성능도 함께 변한다면 그 stage 는 병목 지점이다! 2. 최적화 - 병목현상이 있는 것으로 밝혀진 stage에 대해 더 이상 성능의 향상이 일어나지 않거나, 충 분히 만족할 만큼의 성능을 얻을 때까지 작업량을 줄인다. 3. 위 작업의 반복 - 최종적으로 만족할만한 성능을 얻을 때까지 위에 언급한 1번 과 2번을 각 stage에 대해 반 복한다. 2. Locating the Bottleneck 병목지점 찾기는 최적화 과정의 절반이라 해도 과언이 아닌데, 그 이유는 우리로 하여금 실제로 최적화 노 력이 집중되어야 하는 부분을 정확히 파악할 수 있도록 해주어 의미 없는 수고(역주: 실제로는 제작중인 application에서 그다지 큰 병목구간이 아닌 부분임에도 불구하고 애써 최적화 하려는 시간, 노력의 낭비) 를 덜어주기 때문이다.
  4. 4. 위 그림의 flow chart는 application에서 정확한 병목지점을 찾기 위한 일련의 과정을 보여주고 있다. 주 목할 점은 pipeline의 마지막 단계인 frame buffer operation 에서 시작하여, 제일 첫 단계인 CPU작업 에 서 끝이 난다는 것이다. 한가지 더 알아두어야 할 점은, 만약 어떤 한 개의 primitive (일반적인 경우 triangle)를 처리하는 데 있어서 하나의 병목 지점을 가지고 있다고 했을 때, 상황에 따라 그 병목 지점이 변할 가능성이 농후 하다는 것이다. 따라서, 종종 복수 stage의 작업량을 변화시켰을 때, 성능에 변화가 오 는 경우가 흔히 있다. 예를 들어, low-polygon 스카이 박스는 종종 fragment shading 혹은 frame-buffer 접근에 의해서 영향을 받고, 화면상에 단지 몇 개의 픽셀로 보이는 (멀리 있거나, 가까이 있지만 모델링 자 체가 몹시 작아서) 스키닝 메쉬의 경우는 때때로 CPU 혹은 vertex processing이 문제의 원인이 된다. 이 런 전차로, object-by-object 혹은 material-by-material 로 작업량을 변화 시켰을 때 도움이 되는 경우 가 자주 있다. (역주: 어떤 병목현상이 발생하는 원인이 절대적으로 한가지 것에 영향을 받는 것이 아니라 상황에 따라 달라질 수 있다는 것이다. 위에서 언급한 skybox 예와 마찬가지로, 파티클 같은 경우는 화면 에 가까이서 찍힐 경우 크게 확대된 수많은 primitive들이 반복해서 출력되므로 fill rate에 영향을 받지만, 상대적으로 멀리에서 자그마하게 찍히는 경우라면 vertex processing이나 그 밖의 처리에서 더 많은 영 향을 받을 수 있다는 것이다) 우리는 종종 각 pipeline stage에 대해 그것들이 동작하는 GPU clock에 대해 언급하곤 하는데, clock speed를 외부에서 조작하고 application의 성능 변화를 모니터링 할 수 있게 해주는 PowerStrip (EnTech Taiwan 2003)과 같은 툴과 함께 사용할 때에 이것은 매우 중요한 정보가 된다.
  5. 5. 1) Raster Operation pipeline에 있어서 가장 끝자락에 위치하고 있는 raster operation(혹은 ROP)은 depth 와 stencil buffer 로의 읽기/쓰기, depth 와 stencil 비교, color 값 읽기/쓰기, 그리고 alpha blending, testing 과 같은 작업 을 담당한다. 딱 봐도 알겠지만, ROP 작업은 frame-buffer bandwidth에 대한 비용을 지불해야 한다. 어떤 application이 frame-buffer-bandwidth에 대해서 영향을 받고 있는지를 테스트하기 위한 최적의 방법은 color 나 depth buffer, 혹은 두 가지 모두의 bit depth를 바꿔보는 것이다. 만일, bit depth를 32bit 에서 16bit로 줄였을 때 괄목할만한 성능의 향상이 있었다면, 그 application은 명백히 frame- buffer-bandwidth에 영향을 받고 있는 것이다. Frame-buffer bandwidth는 GPU memory clock에 의해 결정되므로, memory clock을 조작하는 것 또 한 이 병목현상을 검사하기 위한 좋은 방법의 하나 이다. 2) Texture Bandwidth Texture Bandwidth는 memory로 texture의 fetch 요청이 갈 때마다 사용 되어진다(consumed). 근래의 GPU들은 필요 이상의 memory접근을 최소화 하기 위해서 texture cache를 가지고 있지만, 여전히 texture fetch로 인한 성능 저하는 발생하기 마련이며, 상당량의 memory bandwidth또한 차지하는 것이 사실이다. 앞서 우리가 ROP가 병목지점인지를 알아내려 할 때에 했던 것처럼 frame-buffer 포맷을 변경 하는 방법 을 이용하는 것 보다는, 차라리 texture 포맷을 변경하는 것이 이 녀석이 병목지점인지를 알아보려 할 때 보다 나은 방법이 될 수도 있다. 그러나, 그 보다는 차라리 큰 숫자의 mipmap LOD bias를 사용함으로써 적절하게 텍스쳐 사이즈를 바꾸는 방법을 사용하길 권한다. 이렇게 함으로써 mipmap 단계 중 매우 낮은 level의 것을 접근하게 되므로 효과적으로 fetch되는 텍스쳐의 사이즈를 줄일 수 있다. 만약, 이러한 조작 으로 상당한 성능 향상이 있었다면, texture bandwidth에 영향을 받고 있는 것이다. (역주: 위에도 언급 되지만 D3DSAMPLERSTATETYPE의 D3DSAMP_MIPMAPLODBIAS 값으로 큰 양의 숫자를 지정할수 록 낮은 mip level의 텍스쳐가 선택 되며 vice versa 이다. Default 값은 0) Texture bandwidth는 GPU memory clock에 의해서 결정된다. 3) Fragment Shading (역주: 다 아시겠지만 D3D에서의 Pixel Shading을 말합니다 ) Fragment shading은 주어진 color 와 depth 값을 가지고 fragment를 만들어 내는 실제 비용에 의존한다. 즉, “pixel shader” 혹은 “fragment shader”의 수행 비용이 된다. 일반적으로 fragment shading과 frame-buffer bandwidth는 둘 다 화면 해상도에 의해 직접적인 영향을 받기 때문에 종종 ‘fill rate’ 이란 말로 뭉뚱그려져 사용이 되는데, 알다시피 사실 그들은 pipeline상에서 각기 서로 다른 stage이며, 둘 간 의 차이점을 명확히 구분할 수 있는 능력을 갖는 것은 향후 효과적인 최적화를 하는데 있어서 필수적인 것 이다. (역주: 이 문서를 읽다 보면 저절로 구분하게 되니 안심하세요… ^^)
  6. 6. Fragment-Processing을 직접 application 개발자가 프로그래밍 할 수 있는 GPU들이 탄생하기 전에는 fragment shading에 의해서 application의 성능이 영향을 받는 경우는 극히 드물었다. Frame-buffer bandwidth가 화면 해상도와 성능 사이에서 피할 수 없는 상호 관계를 가지는 경우는 종종 있었다. 하여간 이처럼 개발에 유연함을 더해주는 새로운 기술의 탄생으로 인해 오늘도 개발자들은 멋들어진 픽셀들을 찍 어내기 위해서 수많은 시간을 투자하고 있다. Fragment Shading이 병목지점인지를 판단하기 위한 첫 번째 단계는 단순히 해상도를 바꾸어 보는 것이 다. 이미 우리는 앞서 frame-buffer의 bit depth를 변화시켜 봄으로써 frame-buffer bandwidth에서 발 생하는 병목현상은 이미 제거했기 때문에, 해상도 변경이 성능에 변화를 가져온다면, 범인은 남은 fragment shading이 될 가능성이 농후한 것이다. 부가적인 방법으로는 fragment program의 길이를 바꾸 어보고 성능에 영향을 미치는 가 보는 것이다. 그러나, 주의할 점은 안타깝게도 몹시 똘똘한 device driver에 의해서 쉽게 최적화 되어 사라져 버릴 수 있는 단순한(의미 없는) instruction의 추가는 정확한 결과를 위해 피해야 한다는 것이다. 4) Vertex Processing Rendering pipeline 에서 vertex transformation stage는 model-space position, vertex normal, texture 좌표, 등등 과 같은 vertex 속성들의 집합을 입력으로 받아 clipping 및 rasterization에 적합한 homogeneous clip-space position, vertex별 lighting연산 결과값, 텍스쳐 좌표 등의 값들을 생산해 내 는 역할을 한다. 따라서, 이 stage의 성능은 처리해야 할 버텍스들의 숫자에 따른 각 버텍스 별 작업량에 의해 결정된다. 만일 programmable transformation이 사용되었다면 (역주: vertex shader 나 HLSL등을 통해서 vertex transform이 일어난다면) 이 stage가 병목지점이 되는지는 단지 vertex program의 길이를 바꾸어 봄으 로써 쉽게 판단할 수 있다. 만약, 그렇게 해서 성능에 변화가 있다면 vertex-processing에 의해서 영향을 받고 있다는 것이다. 만일, instruction을 추가하려 한다면, 의미 있는 작업을 하는 instruction들을 추가하 도록 유의해야 한다. 그렇지 않을 경우, compiler 나 driver에 의해서 최적화되어 사라지기 때문이다. 반면, fixed-function transformation을 사용하고 있다면, 다소 꽁수가 필요한데, specular lighting 이나 texture-coordinate generation state와 같은 vertex 작업을 조작해서 작업량에 변화를 주도록 해보자. Vertex 처리 속도는 GPU core clock에 의해서 결정된다. 5) Vertex and Index Transfer Vertices 와 indices 데이터는 전체 pipeline중 GPU 파트의 첫 번째 단계로서 GPU에 의해서 fetch된다. Vertex 와 index fetch 성능은 실제로 vertex 및 index 데이터가 존재하는 메모리 위치에 따라 달라진다. 일반적으로는 둘 다 system memory (AGP나 PCI 버스를 통해서 GPU로 전송된다) 혹은 (video card 내 의) local frame-buffer memory에 존재하게 된다. 때때로, 근래 graphics API들은 driver가 적절한
  7. 7. memory 타입을 결정할 수 있도록 application에서 용도에 따른 힌트 정보를 넘겨줄 수 있도록 허락하지 만, 특정 PC platform에선 이들이 어디에 놓일 지의 결정이 여전히 application이 아니라 전적으로 device driver의 몫으로 남겨지기도 한다. (역주: 모두 아시는 내용이지만 D3D의 경우는 D3DPOOL 과 D3DUSAGE의 조합을 통해서 가능하죠?) Vertex나 Index Fetching이 어떤 application의 병목지점인지를 알아내기 위해서는 Vertex Format의 크기를 바꿔보도록 한다. Vertex 와 Index fetching 성능은 data가 system memory에 존재할 경우 AGP/PCI 버스의 속도에 의해 서 결정되며, local frame-buffer memory에 존재한다면, memory clock speed에 의해서 결정된다. 만일, 이것들에 대한 어떤 테스트도 성능에 영향을 끼치지 않는다면, 여러분의 application은 아마도 CPU 에 의해 주로 영향을 받고 있다는 것이 된다. 이 사실을 확인하기 위해서 CPU를 underclocking 해보고, 만약 그와 비례해서 성능이 변화된다면 CPU에 의해 성능이 좌우되고 있다는 것이다. 3. Optimization 이제 모든 병목 구간을 찾아냈으므로, application 성능을 높이기 위해 stage별로 최적화를 해야 한다. 다 음에 나오는 tip들은 각 stage 별로 categorize 되어있다. 1) Optimizing on the CPU 수많은 application들이 CPU에 의해 성능이 결정되는 형태로 제작되고 있다. 물론, 복잡한 물리 연산이나 AI 처리에 의한 어쩔 수 없는 경우도 있지만, 잘못된 batching 이나 resource 관리 때문인 경우도 많이 있 다. 만약 여러분의 application이 CPU에 의해 성능이 결정되고 있다면, rendering pipeline상에서 CPU 작업을 덜어주기 위한 다음의 방법들을 시도해보자.  Reduce Resource Locking Application에서 GPU 자원에 대한 동기화된 접근을 하게 되면 언제나 GPU pipeline 전반에 걸 쳐 작업 정지가 일어날 수 있으며 이는 CPU와 GPU cycle 모두에 대해서 비싼 대가를 필요로 한 다. 이때, CPU는 GPU가 하던 작업을 끝내고 idle상태로 진입해서 요청한 resource를 건네줄 때 까지 반드시 멈추어 기다려야 하며 (역주: GPU와 공유되는 자원에 대한 접근을 획득하기 까지 CPU는 Spin Lock상태로 대기하고 있다) 그 동안 아까운 CPU cycle들을 낭비하고 있어야 한다. 마찬가지로 GPU 역시도 CPU가 가져간 resource를 풀어줄 때까지 pipeline을 놀리고 있어야 하 며, resource를 획득했을 때, 데이터를 다시 채우는(refill) 작업이 필요하다.
  8. 8. 이러한 Locking은 다음과 같은 경우 항상 발생한다. - 바로 직전에 렌더링한 surface에 대해 Lock을 걸거나 데이터를 읽으려 할 때 (그림 참조) - Texture 나 Vertex Buffer 와 같이 GPU가 읽어 들여야 하는 Surface들에 Write 하려 는 경우 일반적으로, GPU가 rendering동안에 사용하는 resource로의 접근은 피해야 한다. (역주: 사용이 빈번한 vertex buffer 및 index buffer의 경우 생성시, 그리고 Lock() 을 호출할 때, 적절한 option flag를 주어, 이러한 CPU와 GPU간의 공유 resource lock에 의해 발생하는 application의 성능 저하를 최대한 피해 갈 수 있습니다.)  Maximize Batch Size 다른 말로 “Batch 개수 최소화 하기” 라고도 한다. ‘Batch’라 함은 DirectX 에서 DrawIndexedPrimitive()와 같은 단일 Rendering API 호출을 통해 render되는 primitive 들의 그룹을 말한다. Batch의 ‘size’라 함은 Batch가 지니고 있는 primitive들의 개수를 말 한다. 언젠가 어떤 현자(賢者)가 말했듯이 “Batch, Batch, Batch!”(Wloka 2003) 인 것이다 … (역주: GPU Gem에 나오는 문장 그대로 번역한 것인데, GPU Gem에는 필자가 ‘Cem Cebenoyan’ 이 넘 한넘만 되어있다. 추측 컨데, GDC 2003 발표때 함께 참여한 Matthias Wloka 이 눔 이름이 책에 빠진게 미안해서 함께 나오게 해줄라고 넣은 문장인 거 같다. 유치 한 거뜰….) 모든 Geometry를 그리기 위한 API 함수 호출은 매번 상응하는 CPU cost를 가진다. 따라서, 매 번 draw 관련 함수 호출 시 넘겨지는 triangle들의 숫자를 극대화 시킴으로 해서, 주어진 개수만 큼의 triangle들을 렌더링 하기 위해 필요한 CPU작업량을 최소화 할 수 있는 것이다. Batch의 size를 maximize하기 위한 몇 가지 팁들은 다음과 같다.
  9. 9. - 만일 triangle strip을 사용한다면 서로 분리된 strip group 사이에 보이지 않는 triangle을 하나 삽입해서 하나의 strip으로 만들어 버려라. Material을 공유하기만 한다면 이런 방식을 통해 한번의 draw 호출에 복수개의 strip들을 넘기는 효과를 얻을 수 있다. (역주: 몇 권 인지는 기억이 안 나지만 예전에 GPG 에도 소개되었던 테크닉인데, 서로 material은 공유하지만 하나의 메쉬를 strip 형태로 구성하는 과정에서 부득이하게 단절된 strip group이 복수 개 생겨났을 때, 그 사이에 rendering되지 않는 polygon을 추가해서 하 나의 stip list로 만들어 버리는 것을 말하는 거 같음 – 렌더링 되지 않는 폴리곤은 2개 이상 의 버텍스가 동일 위치를 가지게 해서 면적이 0 이 되도록 만들거나 혹은, 현재 cull 모드의 back face 폴리곤을 추가 하는 식의 방법을 사용한다) - Texture 페이지를 사용하라 Batch가 깨져버리는 가장 빈번한 경우는 서로 다른 object들이 서로 다른 texture를 사용 하는 경우이다. 여러 개의 texture를 하나의 2D texture에 적절히 배치시키고, texture 좌 표들 또한 그에 맞게 적절히 setting 함으로써, 복수의 texture를 사용하는 geometry 를 한 번의 draw 호출로 넘겨버릴 수 있다. 단, 이 technique을 사용할 경우 mipmapping과 anti- aliasing 관련한 문제의 소지가 있다는 것인데, 이를 위한 해결책으로는 각각의 2D texture 를 cube map의 각 면에 배정해서 사용하는 것이다. - Batch Size를 증가시키기 위해 GPU의 shader 분기 기능을 사용하라. 최근의 GPU들은 유연한 vertex 및 fragment-processing pipeline을 가지고 있으며, shader 내부에서 branching하는 것을 지원한다. 예를 들어, 2개의 Batch가 있는데, 하나는 4-bone skinning vertex shader를 필요로 하는 녀석 이고, 다른 하나는 2-bone skinning vertex shader를 필요로 하는 경우라면, Batch를 2개로 분리하고 각각을 위한 shader를 만드는 대신, 필요한 bone의 개수 만큼 loop를 돌며 blending weight를 누적하고, weight 합이 1.0 이 될 때 loop를 빠져 나오도록 하는 vertex shader를 제작할 수도 있다. 이런 방 법을 통해 2개의 batch는 하나로 묶일 수 있다. 한편, Shader 분기를 지원하지 않는 architecture 에서는, 다소의 추가 비용을 투자해서 동일한 결과를 얻을 수 있는데, 오직 4- bone vertex shader 하나만 만들고 모든 vertex에 대해서 사용하는 대신, 4개보다 적은 bone의 영향을 받는 vertex의 경우는 해당 influence의 weight를 0.0으로 채워버리면 된다. - Matrix의 참조테이블로 vertex shader의 constant memory를 사용하라 비교적 적은 폴리곤을 가지는 다수개의 오브젝트들이 서로 모든 material 속성은 공유하지 만 단지 matrix상태가 다른 경우에도 자주 batch들이 분리된다. (예를 들어, 유사한 나무들 로 이루어진 숲이나 Particle 시스템이 이에 속한다). 이런 경우, n 개의 서로 다른 matrix들
  10. 10. 을 vertex shader constant memory로 load할 수 있으며, index정보를 각 오브젝트에서 사용하는 vertex format의 constant memory에 저장한다. 그런 다음, 이 인덱스를 사용해 서 vertex shader의 constant memory를 참조해서 적절한 matrix를 사용하도록 함으로써, n개 오브젝트를 동시에 rendering 할 수 있다. - Defer decisions as far down in the pipeline as possible Gloss factor로는 glossiness를 위해 pixel shader constant를 쓰려고 batch를 나누는 것 보다는 texture의 alpha channel을 사용하는 것이 훨씬 빠르다. 마찬가지로, shading data 를 texture나 vertex에 넣음으로써, 보다 큰 batch를 사용하는 것이 가능할 수 있다. 2) Reducing the Cost of Vertex Transfer 사실 Vertex transfer가 application에 있어서 병목지점으로 작용하는 경우는 드물다. 그러나, 그런 경우가 아주 일어나지 않는다는 말은 아니다. 만약, vertex transfer나 (더욱 희박하긴 하지만) index transfer가 application에서 병목현상을 일으키고 있다면, 다음에 설명하는 것들을 시도해 보 자.  가능한 한 vertex format 사용되는 데이터 타입에 최소한의 바이트를 사용하자 Don’t use floats for everything if bytes would suffice (for colors, for example)  Vertex Program 내에서 계산이 가능한 값이라면 굳이 Vertex Format에 포함 시키 지 말자 예를 들어, tangent, binormal 그리고 normal과 같은 값들은 굳이 전부 저장할 필요가 없는 경우 가 자주 있다. 위의 경우 3개 중에 2개만 있다면 남은 하나는 vertex program 내에서 간단히 cross product를 사용해서 계산될 수 있다. 물론, 이런 방식은 vertex-processing 속도와 vertex transfer 속도 사이에서 trade-off 해야 한다.  32-bit index 보다는 16-bit index를 사용하도록 하자 16-bit index는 fetch하는 비용도 적고, 인덱스 사이의 이동도 빠르며 메모리 또한 아낄 수 있다.  상대적으로 순차적인 방법으로 Vertex Data에 접근하도록 하자 근래 GPU들은 vertex 데이터를 fetch할 때 cache memory에 접근한다. 참조하는 메모리 공간 상의 위치가 크게 변하지 않거나 동일한 곳을 반복해서 접근 한다면, cache hit율이 높아져 bandwidth 요구량이 줄어든다. (spatial locality of reference)
  11. 11. 3) Optimizing Vertex Processing 근래의 GPU에서 Vertex Processing이 병목지점이 되는 경우는 드물지만, 사용 방법이나 H/W에 따라서 발생할 수도 있다. 만일 Vertex Processing이 병목 지점이라 판단이 된다면 다음의 방법을 시도해 보자.  post-T&L vertex cache에 초점을 맞춰 최적화 하자 근래의 GPU들은 가장 최근에 transform된 vertex들의 결과를 저장하기 위한 FIFO 형태의 자그 마한 cache를 가지고 있다. 만일, cache hit가 일어나서 cache에 저장된 데이터를 사용하게 되 면, 이전 pipeline에서 이미 처리된 것을 사용하게 되므로 transform 이나 lighting같은 작업을 건너뛸 수 있게 된다. 이 cache의 장점을 사용하기 위해서는 반드시 indexed primitive를 사용 해야 하며, 앞서 설명한 locality of reference를 극대화 하기 위해서 mesh의 vertex 데이터들 을 가능한 정렬시켜야 할 필요가 있다. 이러한 작업을 돕기 위해 D3DX나 NVTriStrip(nvidia 2003) 과 같은 녀석들을 사용할 수도 있다.  처리 되어야 할 Vertex 숫자를 줄이자 근본적인 해결 방법이라고 보기는 힘들지만, static LOD(역주: runtime에 동적으로 LOD를 적용 시키는 progressive mesh 방식이 아니라, 미리 준비해 둔 서로 다른 detail을 가지는 mesh set 을 용도에 따라 선택하는 방식을 말하는 것이라 사료됨) 와 같은 간단한 Level-of-Detail 정책 을 사용할 수 있는데, 이를 통해 확실하게 vertex-processing 작업량은 줄일 수 있다.  Vertex-processing LOD를 사용하라 처리해야 할 Vertex 개수에 대한 LOD와 함께, vertex 별로 해주어야 할 처리 자체에 대해서도 LOD를 시도해 보자. 예를 들어, 멀리 떨어져 있는 캐릭터들의 경우, 사실 4-bone skinning이 필 요하지 않은 경우가 많으며, lighting을 위해서도 (퀄리티는 다소 떨어 질 수 있지만) 보다 값싼 연산을 적용할 수도 있다. 또한, multipass로 동작해야 하는 material이 사용된 경우엔 낮은 LOD가 적용되는 객체들에는 처리할 pass의 숫자를 줄여서 vertex-processing 비용을 감소 시 킬 수 있다.  Object당 (Vertex 당이 아니라) 처리되는 계산은 CPU의 작업으로 돌리자 어떤 계산이 각 object별로, 혹은 프레임 별로 한번만 이루어져도 되지만, 편의를 위해 vertex shader에서 처리되는 경우가 종종 있다. 예를 들어, eye space로의 directional light의 transform은 사실 단지 프레임당 한번만 계산이 이루어져도 되지만, 때때로 vertex shader에서 이루어 지는 경우가 있다. (역주: 다들 아시는 내용이지만, vertex shader에 어떤 처리가 들어있 다는 것은 그 vertex shader를 사용하는 모든 vertex 들에 대해서 적용이 되므로, 오브젝트 별 혹은 해당 프레임에 처리되는 모든 object 들에 동일하게 적용되는 사항에 대해서는 굳이 shader에서 처리하지 말자는 얘기)
  12. 12.  올바른 좌표공간를 사용하자 종종 좌표공간(coordinate space)의 선택은 vertex program 내에서 어떤 값을 계산하는 데 필 요한 instruction들의 개수에 영향을 미친다. 예를 들어, vertex lighting을 할 때, vertex normal은 object space 좌표의 형태이고, light vector는 eye space의 값이라면, 둘 중에 하나 는 vertex shader 내에서 다른 좌표공간으로 변환을 시켜야 할 것이다. 만일, 그렇게 하는 대신 light vector를 CPU를 통해 각 object별로 object space로 미리 변환을 시켜서 사용한다면, vertex당 들어가는 transform을 줄일 수 있고, 따라서 사용되는 GPU vertex instruction의 개 수를 절약할 수 있다.  vertex branching을 사용해서 계산을 “early-out” 시키자 (역주: 필요 없는 연산을 끝 까지 진행하지 말고 중도에 끝낼 수만 있다면 끝내 버리자) 만일 vertex shader 내에서 복수개의 광원에 대한 처리를 반복해야 한다면, saturation이 1.0이 되는가 미리 체크해서(광원을 등지고 있는 경우), 그런 경우라면 해당 광원에 대해 더 이상 진행 하지 말고 처리를 끝내 버리자. Skinning에 대해서도 유사한 최적화를 적용할 수 있는데, 해당 버 텍스에 대해 현재까지 처리된 weight의 합이 1.0이 된다면 남은 weight들은 모두 0.0 이라는 얘 기가 되므로 더 이상 진행하지 않아도 된다. 알아두어야 할 것은, GPU가 어떻게 vertex branching을 동작시키는가에 의존적이기 때문에, 모든 GPU architecture에서의 성능향상을 보 장하지는 않는다는 점이다. 4) Speeding Up Fragment Shading 만약 여러분이 길고 복잡한 fragment shader를 사용한다면, 종종 fragment-shading에 의해 application 성능이 영향을 받기 쉽다. 만약 그런 경우라면 다음의 것들을 시도해보자.  Depth부터 렌더링 하자. (Render Depth First) Primary shading pass rendering을 시작하기 전에 오직 depth만 미리 rendering하게 되면, 극 적인 성능향상을 가져오는 경우가 있는데, 특히 high depth complexity를 가지는 장면의 rendering의 경우는 수행해야 하는 fragment shading과 frame-buffer memory접근의 횟수를 줄여줌으로써 더욱 이득을 볼 수 있다. (역주: 다들 아시는 얘기겠지만 사족을 덧붙인다면, 예를 들어 나무가 빼곡히 들어선 숲의 모든 나무들에 대해서 per-pixel lighting과 bump를 적용시켜 렌더링 한다고 가정해 보자. 위와 같은 technique을 사용하지 않고, 원단으로 렌더링을 하게 되면, 최종적으로 화면에 출력되는 모양과 는 상관없이(모든 객체가 렌더링 되기 전 까지는 이 픽셀이 화면에 찍힐지 찍히지 않을 지는 결 정할 수 없으므로…당연한 얘기지만 pixel단위 비교가 필요하기 때문에 단순 object 정렬로는 부 족하다) 모든 나무들의 기둥, 줄기, 잎을 이루는 모든 픽셀들에 대해서 lighting과 bump 연산이 들어가게 된다. 결과 화면 상에서는 시야에 가까운 쪽의 나무들에 의해 가려져서 실제로는 화면
  13. 13. 에 출력이 되지 않는 녀석들까지도 모두 값비싼 processing이 동일하게 들어 갸야 한다는 얘기 이다. 얼마나 낭비인가! 이런 낭비를 방지하기 위해 미리 z-write 만 enable시킨 상태에서 화면 을 렌더링 해서 z-buffer를 준비해 두는 것이다. 그 다음 실제 rendering pass에서는 early-z 최 적화에 의해 정확히 현재 화면에 보여질 pixel 들에 대해서만 lighting과 bump 처리가 이루어지 도록 할 수 있다는 것이다(혹은 early-z 최적화 지원이 안 되는 h/w인 경우도 z-compare에 의 해 적어도 frame-buffer로의 픽셀 비용은 아낄 수 있다) 앞서 본문에서도 밝혔듯, low depth complexity, 그러니깐 뭐 예를 들어 화면에 달랑 삼각 폴리곤 하나만 -_-; 그리는 경우라면 별다 른 이득이 없겠지만, 나무가 빼곡한 숲과 같은 high depth complexity를 가지는 scene의 경우라 면 그 성능 차이는 실로 엄청날 것이다) Depth-only pass의 이점을 온전히 얻기 위해서는 단지 frame buffer로의 color write만 disable시키는 것으로는 충분치 못하고, fragment들에 이루어지는 모든 shading 또한 disable 시켜야 한다. (역주: 당연한 얘기 아닌가? -_-;)  Early-Z Optimization이 실력 발휘를 해서 필요없는 fragment-processing은 건너 뛸 수 있도록 하자! (Help early-z optimizations throw away fragment processing) 근래의 GPU들은 H/W 설계 단계에서 이미 가려진 fragment들에 대한 shading은 하지 않도록 되어있다. 그러나 아무리 그렇더라도 H/W 입장에서 이러한 최적화가 동작할 수 있는 근거가 되 는 정보는 오직 현재의 화면 상태 밖에 없다는 것이다. 그러므로, rough하게나마 front-to-back order로 렌더링을 할 수 있다면 성능을 극적으로 향상시킬 수 있다. (역주: 시야에 가까운 객체로 부터 먼 객체 순서로 미리 sort를 시켜 그 순서대로 rendering을 시켜주면 앞서 rendering된 녀 석에 의해 가려지게 되는 부분의 fragment shading은 GPU상에서 skip 되므로 성능 향상을 꾀 할 수 있다는 것이다) 또한, 앞선 tip에서 밝혔듯, rendering pass를 분리해 depth first 렌더링을 하면 효과적으로 shaded-depth complexity를 1로 감소시킬 수 있으며, 이를 통해 뒤따르는 pass들(실제 복잡한 shading 연산이 이루어지는)에서 실질적인 성능향상이 이루어지는 것이다.  복잡한 function들은 texture에 저장하자 텍스쳐는 static하게 접근 가능한 아주 유용한 데이터 공간으로 활용될 수 있다. Vertex or Pixel shader 동작 시에 불필요한 수학계산을 최대한으로 줄여줄 수가 있기 때문이 다. (역주: 카툰렌더링이나, 기타의 특별한 효과 등을 나타내기 위해 선계산된 Lookup Table을 Texture에 저장시켜놓음으로써 최종 R,G,B까지 계산하지 않고, UV만을 계산함으로써fragment shader의 instruction수를 대폭 줄여 줄 수가 있다)  Fragment당 작업을 vertex shader의 일로 돌려 버리자 앞선 tip 중에서 object 별 작업은 vertex shader로부터 CPU로 위임하도록 했던 것과 같이 screen space상에서 정확히 선형 보간이 가능한 계산처럼 vertex 별로 이루어질 수 있는 계산 들은 굳이 pixel shader에서 하지 말고, vertex shader로 옮겨 버리자는 것이다.
  14. 14. (역주: vertex정보에 담기는 color나 텍스쳐 좌표등은 화면에 Rasterizing되는 과정에서 자동으 로 보간이 이루어 지는데. 그 정해진 보간만 필요한 경우에는 pixel shader과정을 과감히 생략할 수도 있다. 바로 위의 tip 에서 언급된 내용과 함께, 카툰렌더링 같은 것이 하나의 예라고 할 수 있다)  Use the lowest precision necessary DirectX 9 와 같은 API는 프로그래머로 하여금, fragment shader code 상에서 quantity 혹은 계산이 낮은 precision으로 동작할 수 있도록 precision hint를 지정할 수 있도록 하고 있다. 많 은 GPU들이 내부적으로 precision 을 줄이고 성능을 향상시킬 수 있도록 이러한 hint 정보를 활 용 할 수 있다.  불필요한 normalization은 피하자 일반적으로 흔히 범하는 실수가 어떤 계산을 함에 있어서 모든 vector들을 매번 normalizing 하 는 것을 칭하는 “normalization-happy”라고 하는 것이다. (vector의) length가 변하지 않는 변 환은 어떤 것들이 있으며, cube-map lookup처럼 vector의 길이와는 상관 없는(굳이 normalize 하지 않아도 상관 없는) 계산이 어떤 것들인지를 구분하여 normalize는 필요로 하는 곳에 적절 히 사용하도록 하자.  Fragment shader의 LOD사용을 고려해 보자 멀리 떨어져 있는 object들의 경우엔 perspective에 의해서 자연히 처리되어야 하는 pixel의 개 수가 줄어드는 LOD효과가 있게 되므로 비록 vertex LOD에서처럼 큰 성능 향상을 기대하기는 힘들지만, 먼 거리에 있는 녀석들에게는 shader의 복잡도를 줄여준다거나 사용되는 pass를 감소 시켜서 fragment-processing 처리량을 줄일 수 있다.  불필요한 곳에서의 trilinear filtering은 disable 시키자 Trilinear filtering이 추가적인 texture bandwidth를 잡아먹지 않는다고 가정할 지라도 근래 대 부분의 GPU architecture상에서 fragment shader의 계산에 필요한 추가적인 cycle을 필요로 한다. Mip-Level 의 변화가 쉽게 눈에 띄지 않는 texture라면 fill rate을 위해서라도 trilinear filtering을 끄도록 하자.  가능하다면 최대한 simplest type의 shader를 사용하자 Direct3D 와 OpenGL 모두에서 fragment를 shading하기 위한 수많은 다른 방법들이 존재한다. 예를 들어, Direct3D 9 에서 you can specify fragment shading using, in order of increasing complexity and power, texture stage states, pixel shaders version 1.x(ps.1.1 ~ ps.1.4), pixel shaders version 2.x., or pixel shaders version 3.0. 일반적인 경우, 원하는 효과를 만들 수 있도록 지원하기만 한다면 최대한 단순한 타입의 shader
  15. 15. 를 사용하도록 하자. Simpler shader type은 묵시적인 약속이지만(implicit assumption), GPU driver에 의해서 보다 빠른 native pixel processing 코드로 컴파일 된다는 것이고, 한가지 바람 직한 side effect 로는 simpler shader를 사양할수록 보다 다양한 종류의 h/w 에서 동작 할 수 있다는 것이다. 5) Texture Bandwidth 줄이기 만일 여러분의 application이 memory-bandwidth에 영향을 받고 있다는 것을 알아냈으며, 대부분 texture fetching에 의한 것이라면, 다음의 최적화 방법들을 고려해보자.  사용되는 텍스쳐의 사이즈를 줄이자 목표 해상도와 texture coordinate들에 대해서 다시 한번 고려해 보자. 정말로 여러분의 게임 플 레이어들은 그렇게까지 높은 mip level의 텍스쳐를 원하고 있는가? 만약 그렇지 않다면, 텍스쳐 사이즈를 줄이는 것을 고려해 보자. 특히, video card의 local memory를 넘치게 사용해서 어쩔 수 없이 system memory에 텍스쳐가 저장되어 매번 AGP나 PCI등을 통해서 접근되어야 하는 좋지 않은 상황이라면 더 없이 좋은 방법 이다. NVPerfHUD (nvidia 2003)과 같은 도구를 이용 해서 이런 상황인지의 여부를 진단 할 수 있으며, driver가 다양한 장소에 할당한 각 memory들 의 양도 볼 수 있다.  모든 color 텍스쳐들은 압축하자 사용되는 모든 텍스쳐들은 각 텍스쳐들의 alpha 채널의 용도에 따라서 DXT1, DXT3 또는 DXT5 등의 형태로 압축되어야 한다. 이렇게 함으로써, memory사용량, texture bandwidth 요 구량을 줄일 수 있으며, texture cache의 효율성 또한 높일 수 있다.  꼭 필요한 경우가 아니라면 값비싼 texture format의 사용은 피하자 64-bit나 128-bit floating-point format들과 같이 큰 텍스쳐 포맷들은 분명 텍스쳐 fetch를 위 해서 더 많은 bandwidth를 사용하게 된다. 꼭 필요한 경우에만 사용하자. (역주: 이런 포맷도 지 원 하는지 몰랐습니다. 찾아보니 D3DFORMAT중에 D3DFMT_G32R32F, D3DFMT_A32B32G32R32F 와 같은 녀석들도 있군요… -_-a)  Minify될 수 있는 모든 텍스쳐들에 대해서는 항상 mipmapping을 사용하자 Mipmapping을 사용함으로 인해 texture aliasing이 줄어들어 퀄리티가 향상되는 것은 물론이 고, minify된 texture를 위한 texture-memory접근 패턴을 localizing함으로 인해서 texture cache 사용률을 향상시킬 수 있다. 만일, mipmapping을 사용함으로 인해서 화면의 특정 부분이 뭉개져 보이는(blurry) 현상이 생겼다면, mipmapping을 disable시키거나 큰 음의 숫자로 LOD bias를 조절하려 하지 말고, 대신 batch별로 적절하게 레벨을 조절해서 anisotropic filtering을 사용하도록 하자.
  16. 16. 6) Optimizing Frame-Buffer Bandwidth Pipeline에 있어서 마지막 단계인 ROP는 frame-buffer memory에 직접적으로 접근하며, frame- buffer bandwidth를 사용하는 유일한 가장 큰 고객이다. 다음에 frame-buffer bandwidth를 최적 화하는 방법을 적었다.  Depth 먼저 렌더링하자 이렇게 함으로써, 앞선 섹션에서 밝혔듯이 fragment-shading 비용을 줄일 뿐만 아니라 frame- buffer bandwidth 사용량도 줄이게 된다.  Alpha Blending 사용을 자제하자 Destination-blending factor를 0 이 아닌 값으로 세팅해서 알파블렌딩을 사용하게 되면, frame buffer로의 read 와 write 작업이 모두 일어나게 되므로, 잠재적으로 bandwidth 사용량이 2배가 된다는 점에 주목하자. Alpha blending은 반드시 필요로 하는 곳에만 사용하도록 하고, 특히 alpha blending을 사용해서 높은 depth complexity 의 scene을 연출에야 할 경우에는 더욱 세 심한 주의를 기울여야 한다. (역주: 1비트 알파를 사용해도 충분한데 8비트의 알파 채널을 사용함으로써 불필요한 pixel 계산 이 일어날 수도 있으며, 완전히 투명한 부분이 매우 많은 경우 적당히 폴리곤을 늘려서 pixel단위 의 계산 량을 줄이는 것도 좋은 방법이다.)  괜찮다면 depth write 를 꺼버리자 Bandwidth를 잡아먹는 또 다른 녀석으로 depth 쓰기(역주: z-write ^^;)가 있으며, depth write가 disable되어야 하는 상황은 1) 최종 depth데이터가 이미 depth buffer에 있는 상태에서의 multipass rendering, 2) particle과 같은 alpha-blended 효과들, 3) shadow map으로 object들을 렌더링 하는 경우 (사실, color-based shadow map으로 렌더 링하기 위한 것이라면 depth read 역시도 disable 시킬 수 있다) 등이 있다.  불필요한 color-buffer clear는 피하자 만일, 여러분의 application에서 모든 pixel이 frame buffer 전체에 write 된다는 것이 보장된다 면, 소중한 bandwidth를 소모해버리는 color clear 를 하지 말자 (역주: 어차피 매번 새로운 렌 더링으로 화면을 덮어쓰므로, 굳이 화면을 clear 해주지 않아도 이전 화면이 남아있지 않으므 로). 그러나, 알아두어야 할 점은 많은 early-z 최적화들이 clear된 depth buffer의 deterministic contents에 의존하고 있으므로, depth buffer 와 stencil buffer는 할 수 있을 때 마다 clear해주어야 한다는 것이다. (Note, however, that you should clear the depth and
  17. 17. stencil buffers whenever you can, because many early-z optimizations rely on the deterministic contents of a cleared depth buffer. ) (역주: 대부분의 하드웨어는 depth-buffer, stencil-buffer가 동일 메모리 상에 존재 하게 되므 로 clear을 할 경우에는 가급적이면 동시에 한번의 함수콜로 clearing 해 주는 것이 좀더 유리하 다. 즉, Depth-Buffer Clearing과 Stencil-Buffer Clearing을 굳이 분리하지 말자)  대략 앞에 있는 놈에서부터 뒤쪽에 있는 놈 순으로 렌더링 해줍시다. 앞선 섹션에서 언급한대로 fragment-shading에서의 이점은 물론이고, frame-buffer bandwidth에서도 유사한 이득을 볼 수 있다. Early-Z H/W 최적화는 불필요한 frame-buffer read/write를 건너뛸 수 있도록 해준다. 사실, 이러한 최적화를 지원하지 않는 구형 H/W 에서 역 시도 이 방법으로 이점을 얻을 수 있는데, 많은 fragment들이 depth test 에서 실패하게 될 것이 므로, 결과적으로 이 방법을 택하지 않았을 때 보다는 frame-buffer로의 color 와 depth write 가 보다 적게 이루어질 것이기 때문이다.  Skybox 렌더링 최적화 Skybox는 주로 frame-buffer-bandwidth 에 의존적이 되지만, 어떤 방법으로 최적화 할 것인지 를 상황에 따라 결정해야 할 필요가 있다. - 첫 번째 방법: Depth writing만 끄고, 맨 마지막에 Skybox를 렌더링 한다. (이렇게 함으로 써, regular depth buffering과 함께 early-z 최적화가 bandwidth를 아껴줄 것이다) - 두 번째 방법: Depth read/write 모두 끄고, 제일 처음에 Skybox를 렌더링 한다. 어떤 방법이 보다 많이 bandwidth를 save 해줄 지는 target h/w 와 최종 완성된 화면에서 skybox가 보이는 부분이 얼마만큼의 비중을 차지할 지에 따라서 결정된다. 만약, skybox의 대 부분이 가려져 있다면, 첫 번째 방법이 더 좋을 가능성이 있으며, 그렇지 않은 경우엔 두 번째 방 법이 보다 bandwidth를 아껴줄 것이다.  Floating-point frame buffer는 꼭 필요한 경우에만 사용하자 이런 포맷은 명백히 보다 작은 포맷인 정수형을 사용할 때 보다 훨씬 많은 bandwidth를 소모한 다. The same applies for multiple render targets.  가능 하다면 16-bit depth buffer를 사용하자 Depth 관련 작업은 bandwidth를 몹시 잡아먹는 녀석이다. 따라서, 32-bit 대신에 16-bit를 사 용하는 것은 giant win (-_-?)이 될 수 있다. (역자의문: 무슨 뜻 일까요? 부산 사람인가? Giant win은… 롯데 자이언츠 혹은, 샌프란시스코 자이언츠가 이긴걸까요? -_-a ) (Depth transactions are a huge consumer of bandwidth, so using 16-bit instead of 32-bit
  18. 18. can be a giant win, and 16-bit is often enough for small-scale, indoor scene that don’t require stencil) 그리고, 16-bit는 주로 stencil을 사용하지 않는 작은 크기의 실내 장면의 표현에 충분하다. 또 한, 동적인 cube map과 같이 depth 를 필요로 하는 render-to-texture 효과용으로 적합하다.  가능하다면 16-bit color를 사용하자 이는 특히 dynamic cube map이나 projected-color map과 같은 render-to-texture 효과용으 로 적합한데, 그 이유는 16-bit color만으로도 충분히 멋진 결과물을 만들어내기 때문이다. 4. Conclusion 알려줄 것 다 알려줬으니 대략 열심히 해보자!!! 음… 책이 오자마자, 읽어보고 게임 관련 일을 하시는 분이라면 누구나 한번쯤 읽어 볼 필요가 있고, 혹은 누군가의 간지러운 부분을 긁어줄 수도 있겠다 싶어서 부족한 영어실력 및 3D 지식이지만 짬짬이 시간을 내어 용감하게 번역을 해 보았으며, 본래 의미를 해칠 듯 한 부분들은 원문도 함께 두었습니다. 일단은 spirit3d 에 올릴 계획인데, 누구라도 신경 쓰지 마시고, 퍼다가 도움이 되시길 바랍니다. 그게 spirit3d 의 건국이념 인 듯 하구요… ^^; 그리고, 오역 및 잘못된 주석들은 여러분들이 부족한 저를 대신해 수정을 해 주시기 바랍니다. 그래야 저 로인해 잘못된 지식이 전파되는 것을 막죠… ^^; 긴 글 읽어주셔서 감사합니다. - 2004/5/11 overdrv p.s 크흑… 예전에 번역해서 올렸던 Vertex Buffer 의 효율적인 사용에 관한 문서를 여기에 간단하게 다 시 한번 요약해서 별첨하려 했는데… 차일 피일 미루다가 결국엔 정리하던 것을 홀랑 지워버리고 이대로 그냥 올립니다. ^^; 오늘이 7월 2일이니… 으으~ 2달 가까이 미루다가 결국… -_-v

×