버텍스버퍼의 효율적인 사용

2007.10.01 21:17

단장 조회 수:5100 추천:33

버텍스버퍼의 효율적인 사용
by
Richard Huddy
RichardH@nvidia.com

역: overdrv the blacksmith
overdrv@shinbiro.com

Problem statement:
DX7을 사용하는 App에서 범하는 가장 일반적인 구조상의 문제는 버텍스 버퍼의 잘못된 사용이다.
Aim:
모든 데이터 타입들에 대해 좋은 성능을 얻는 것

Complications
Dynamic 버퍼 대 Static 버퍼
최적의 버텍스 버퍼 사이즈
최적의 FVF
Index를 사용할 것이냐 사용하지 않을 것이냐? 그것이 문제로다.
Strips 대 lists
“Optimize”가 뭐냐?
어떤 캐쉬들이 사용되며 어떤 영향을 미치나?
버텍스 버퍼에 Lock을 건다는 것은 어떤 의미인가?
버텍스 버퍼의 사용이 부적절한 것은 어떤 경우일까?

Problem Resolution
모든 경우를 다루는 방법
Static Data
Dynamic Data
Large data sets

Essential thinking… or “Why it should all make sense”.
일차적으로 우선 왜 버텍스 버퍼가 좋은 걸까?
CPU 의 Register 이름 바꾸기
GPU cache에 대해서 생각해보기
DX8에서 바뀌는 것은 무얼까?

부록1
Dynamic 버텍스 버퍼를 갱신 시키기 위한 의사코드

부록2
숙지해야 할 6가지 규칙들

용어 정의 와 몇가지 전제조건들

일단 니들은 DX7 하구 아주아주 친한 거로 간주할거다.

“HEL”                        
알지? CPU에서 다 에뮬레이션 하는거
“Regular HAL”        The software HAL on DX7.  
모든 Transform, 라이트 계산은 전부 CPU에서 하구 비됴카드는 단지 rasterize만
“TnL HAL”                
Transform, clipping, lighting and rasterization 몽땅 비됴카드에서 하는거
“VB”                        
버텍스 버퍼
“GPU”                        
Graphics Processing Unit.
“FVF”                        
Flexible Vertex Format

우리가 사용하는 메모리들은 각각 PC상의 어디에 생기나?
Local Video Memory:                Frame Buffers, Z buffers,  대부분의 texture들
AGP memory:                        Overspill textures, Vertex Buffers
System Memory:                        Program code, 명시적으루 System 메모리로 로딩한 Vertex Buffer들, 모든 시스템 메모리 Surface와 그 사본들.
Problem statement:
DX7을 사용하는 App에서 범하는 가장 일반적인 구조상의 문제는 버텍스 버퍼의 잘못된 사용이다.

졸라 잘난 넘들의 사교집단(Technical Developer Relations) 스탭들하구 일을 하면서 일상적으루 가장 흔히 접하게 되는 것은 바로바로 버텍스 버퍼의 부적절한 사용이 되겠다. 음… 근데 문제는 일단 DX7에서 버텍스 버퍼를 우끼고 자빠지게 사용함으로 인해서 치뤄야 할 대가는 결코 만만한 것이 아니란 거다. 졸라…
Aim
모든 데이터 타입들에서 졸라 좋은 성능을 끌어내기
졸라… 성능이 졸라 중시되는 모든 3D App들의 공통된 목적은 그 App가 돌아가는 컴터를 졸라 고생시킬지라도 어찌 됐거나 먹어주는 폴리곤 처리능력을 보이는 것이다. 그데 이걸 졸라 어렵게 만드는 가장 골때리는 문제점은 유저들의 H/W성능이 천차만별이라는 것이다. 버뜨! 비유티,
이런 문제점에도 불구하고 몇 가지 반드시 지켜야 하는 불변의 법칙들이 있다는 거다. 이 몇 가지 규칙들은 니 눔이 만들려고 하는 App가 그놈이 돌아가게 될 H/W에서 H/W가 지원하는 최대한의 성능을 발휘할 수 있도록 하는데 졸라 적잖은 도움을 줄거다. 음… 그런 관계로 일단 이 문서에서는 ‘버텍스 버퍼와 그 넘의 효율적인 사용’ 이라는 주제로 함 야부리를 까주마.

음… 이제부터 우리의 목표는 static이나 dynamic 버텍스 버퍼 모두에서 뛰어난 폴리곤 처리능력을 얻는 것이고, 사실 구세대 H/W에서 했던 것과는 달리 실제로 H/W의 이론적인 (역주: 비됴카드 광고 문구에 적혀있는) 최대 처리능력과 근사한 아웃풋을 얻는 것이 가능하다.

Complications
Dynamic 대 Static data
흐음… 일반적으로 사람들이 잘못 알고있는 것들 중 대표적인 한가지는 H/W TnL이 오직 static 버텍스 버퍼만을 위해서 설계된 관계로 dynamic 버텍스 버퍼를 사용할 경우엔 H/W 가속의 지원을 받지 못함으로 인해서 심각한 성능저하를 초래하는 것으로 알고 있다는 것이다.

흠… 근데 졸라… 사실은 맞는 말이다… -_-; 그러나 D3D API들을 적절히 사용함으로써 Dynamic하게 생성된 버텍스 버퍼를 통해서도 CPU만 사용했을 때 보다는 훨씬 뛰어난 처리능력을 얻어낼 수 있다는 것이다.
실제로 GeForce256 상에서 dynamic 데이터를 이용해 초당 약 천백만(11 million 컥… -_-;)  폴리곤의 처리능력을 얻을 수 있었는데 이는 이론적으로 static 데이터를 통해서 얻을 수 있는 최대치의 약 70%정도에 해당하는 것이다.

사실 Static 데이터는 다루기가 보다 쉽고, 수많은 benchmark들을 통해서도 알 수 있듯이 H/W 스펙에서 제시하는 최대 처리능력을 실제로도 얻어낼 수가 있다.
음… 그런데 dynamic과 static 데이터는 각각 다른 접근 방법이 필요하다. 그러나 하나의 App안에서 그다지 큰 어려움 없이 2가지 데이터 타입 모두를 다루는 것이 가능하다.

최적의 Vertex Buffer 크기
일단 GeForce 비됴카드 시리즈의 GPU들에 있어서는 최적의 버텍스 버퍼 사이즈라는 것은 없다. 버뜨, 다음과 같은 규칙들이 적용되니깐 잘들 보시라.

버텍스 버퍼 스위칭은 졸라 비싼 작업이다. (regular HAL을 사용하는 경우 더욱더 그러하닷) 따라서… 복수의 오브젝트들을 하나의 버텍스 버퍼로 묶어버리는 것이 바람직하다. 뭐 단순히 생각해봐도 버텍스 버퍼 transition에 드는 비용을 절감할 수 있지 않겠냐?
200개가 안되는 폴리곤으로 이루어진 프리미티브들의 묶음은 바람직한 프리미티브들로 간주하기엔 어려움이 있다(sub-optimal). 더군다나 10개 이하의 아주 작은 개수의 폴리곤들로 이루어진 프리미티브 묶음들은 이 최적화의 주요 목표가 되어야 한다.
버텍스 포맷에 들어있는 씰데 없는(redundant) 데이터는 버텍스 버퍼의 사이즈만 키울 뿐이고 결정적으로 AGP 버스를 통한 데이터 전송률을 무쟈게 떨어뜨릴 것이다.

흠… (1) 번과 (3) 번은 서로 그 요구사항에 있어서 상충한다는 점에 주목하라. 아까도 말했듯 버텍스 버퍼 스위칭은 매우 큰 비용을 치루는 작업이므로 App에서 사용하는 모든 혹은 대다수의 버텍스들이 가급적 동일한 버텍스 포맷을 사용하게 하고 그에 따라서 아주 적은 개수의 버텍스 버퍼들에 모든 데이터를 올려가며 작업하게 되면 더 좋은 성능을 얻게 된다는 거다. 음… 이건 매우 전형적인 최적화 방법이며 또한 작업을 졸라 복잡하게 만드는 요인이기도 하다.
Note that (1) and (3) can produce conflicting demands.  Since switching VB is so expensive it can be advantageous to use a common vertex format throughout all or much of your app so that all vertex data can live in a small number of VBs.  This is quite typical of optimization issues and makes the task significantly more complex.

Microsoft는 DX7의 사용기간 내내 “대략 1000개의 버텍스” 정도가 적당하다고 부르짖어 왔다. 버뜨, b. u. t. 그러나 최근 증명된 자료들에 의하면 더 큰 버텍스 버퍼에서 더 좋은 성능을 내고 있으며,  size에 있어서 대략 2000 버텍스를 넘기는 넘 들이 더 우수했다는 것이다.

최적의 FVF
일반 규칙은 다음과 같다. 컴팩트한 FVF를 사용하시라!
중복(redundant) 데이터는 데이터 전송에 있어서 버스 사용효율을 떨어뜨리고 일반적으로 좋지 않은 결과를 초래한다.(씨바 누가 모르냐? -_-+). 뭐 때때로는 VB transition (역주: DrawPrimitive 호출할때마다 계속 다른 버텍스 버퍼를 세팅하는 것을 말하는 것이라 사료됨) 줄일 수 있거나 버텍스들의 복사횟수를 줄임으로 인해서 보상 받을 수 있을 수도 있기는 하겠지만 대부분의 경우 시스템 성능을 저하시키게 되므로 이런 경우는 피해야 한다.
만약, 아주 적은 양의 중복 데이터를 포함한다면(말하자면 FVF에서 텍스쳐 좌표 2개 가지는 경우지) 오직 하나의 dynamic 버텍스 버퍼를 사용하라는 거다. 이렇게 하는 경우 거의 대부분의 경우 좋은 결과를 얻을 수 있다는 건데, 그 이유는 DX7 runtime에는 오직 하나의 dynamic VB를 사용할 때 특별한 성능향상을 얻을 수 있는 버그가 있기 때문이다. (구린 해석 -_-;)
If including a small amount of redundant data (say, a second texture coordinate pair) allows you to use just one dynamic VB then it’s almost certain to be a win because there’s a bug in the DX7 runtime which means that there are special performance benefits to using one, and only one, dynamic VB.

음… GeForce 시리즈의 GPU들에서는 매우 효율적으로 처리할 수 있는 특정 데이터 타입이 있는데, 일반적으로 그 사이즈에 있어서는 32Byte나 64바이트가 되겠다..
That said there are certain highly efficient data types which the GeForce family of GPUs handle very quickly.  These are typically 32 bytes or 64 bytes in length.
인덱스 버퍼를 사용할 것이냐 그렇지 않을 것이냐?
일반적으로, Indexed 프리미티브들은 그렇지 않은 넘들에 비해서 보다 바람직하다. 그중에서도 졸라 젤로 좋은 넘은 Indexed Strip이 되겠고,  두번째로는 Indexed List가 되겠다. 인덱싱은 실제로 이미 GPU캐쉬 속에 들어있는 버텍스들을 참조하는 인덱스들로 이루어진  strip과 같은 최적의 경우에서 알 수 있듯이 커다란 폴리곤 버퍼를 다루는데 있어서 전체적으로 사용되는 bandwidth를 줄여준다는 것이다. 이런 경우 최종적으로 3각 폴리곤을 구성하기 위해서 GPU로 Load되어야 하는 전체 데이터량은  단지 2바이트밖에 안 된다는 것이다.(DX7에서 모든 인덱스는 각각  2바이트 크기를 가진다)

향후 개발될 칩들에서의 점차 bandwidth 의 요구는 증가되고 있으므로, 앞으로 인덱싱이 보다 선호되리라는 것은 당연하다. 게다가, DX8에서 등장하게 될 Index Buffer는 졸라 작업을 더욱 편하게 해준다.
Strips 대 lists
일단 strip이란 넘은 단지 하나의 추가 버텍스를 통해서 각각의 triangle을 구성할 수 있는 관계로 strip이 보다 선호되는 형태의 데이터 구조란 것은 놀라울 것도 없다. 뭐 그렇지만 strip의 원초적인 제약사항에 의해서 정형화된 grid 형태가 가장 이상적인 경우가 되겠다. 이 경우엔 indexed strip의 경우 폴리곤 하나당 단지 WORD 하나만 날려주면 되겠다. (Indexed List의 경우는 폴리곤 하나 당 적어도 3개의 WORD가 필요한 거 알지?)
“Optimize”가 뭐야?
(역주: 이거는 이제 안쓰이는 넘이니깐 걍 skip!)

어떤 cache가 동작 중이고 어떤 영향을 미치지?
GeForce 시리즈의 GPU들에는 서로 독립된 2가지의 cache가 존재하며, 각각의 적절한 사용 여하에 따라서 전체 성능에 지대한 영향을 미친다.

The GPU Memory Cache
음… 뭐 이넘은 순수한 memory cache가 되겠다. 그니깐 버텍스 버퍼로부터 버텍스들을 fetch할 때 읽어 들이는 넘들 중 가장 최근에 사용된 AGP 메모리 라인들을 저장하는 것이 되겠다. 일단 Cache line의 사이즈는 32바이트 이다. 따라서 니넘이 사용하는 FVF가 정확히 32의 배수로 떨어지지 않을 경우 가급적 버텍스 데이터를 접근할 때 sequential한 방법을 사용하는 것은 니넘의 책임 되겠다. 왜냐면 이렇게 하지 않을 경우 전혀 사용되지도 않는 데이터들이 fetch되서 cache로 읽어 들여질 것이기 때문이다. 버텍스 버퍼를 죽어라고 random access 하는 것은 캐쉬를 무용지물로 만들어 버릴 수도 있응께 이런 경우는 절대적으루다가 피해야 한다.
모든 캐쉬에서와 마찬가지로 “Locality of Reference”(역주: 지속적으루다가 이전에 접근했던 데이터와 인접한 데이터를 접근하게 되면 캐쉬 히트율이 높아져서 성능이 좋아진다는 거지 뭐) 는 가장 기본적인 원칙이고 이 규칙을 잘 따른다면 충분한 보상을 받게 된다.

The GPU Vertex Cache
GPU의 2번째 캐쉬로서 더욱 잘 알려진 넘이 되겠는데 이넘은 post-transform 과 Light를 위한 캐쉬 되겠다. 칩에서 사용된 가장 최근에 사용된 서로 다른 10개의 버텍스들이 FIFO로 운영되는 캐쉬에 저장된다. 이 캐쉬의 값이 커질수록 GPU의 부하를 가중시킨다. (The value of this cache is greater the higher the load on the) 캐쉬가 FIFO의 형태로 작동한다는 사실이 언뜻 보기에 직관적이지는 못하지만, 실제의 경우 졸라 효율적으로 동작한다는 것을 보여주고 있다. 이 넘도 Memory cache에서와 마찬가지로 근본적인 룰은 “Locality of Reference”가 되겠다. 막 사용된 버텍스 데이터는 가능한 신속하게 다시 사용되어야 한다. 아! 그리고 이넘 역시 메모리 캐쉬에서와 만찬가지로 버텍스 데이터의 random access는 피하는 것이 좋을 것이다.
그런데 명백한 것은 Vertex Cache란 넘은 오직 Indexed Primitive를 사용하는 경우에만 그 성능을 발휘한다는 것인데, 이해가 잘 안된다면 잠시동안 곰곰히 생각해 보시라… -_-;
(If that’s not obvious, think about it for a short while…)
버텍스 버퍼 생성의 최선의 방법
버텍스 버퍼들이 렌더링을 위해 사용될 때 그넘들 각각을 어디에 위치시키는 가는 아주 아주 중요한 요소로 작용한다.

S/W 디바이스(예를 들어 regular HAL과 같은…)의 경우엔 VB들이 생성시 System Memory에 만들어질 것을 필요로 한다. 반면 H/W Device (TnL HAL같은 넘)의 경우는 시스템 메모리가 아닌 곳에 존재해야 한다. 전형적으로 이것은 driver가 버텍스 버퍼를 AGP에 생성시키게 되는데, AGP 메모리는 CPU입장에서는 ‘Write’ 가 빠르고 GPU입장에서는 ‘Read’가 빠르다는 것이다. 그런데 중요한 접은 CPU가 ‘Read’하려 할 때는 엄청 졸라 무쟈게 느리다는 것이다!

GPU의 입장에서 버텍스 버퍼 생성시 가장 이상적인 옵션은 WRITEONLY가 되겠다. 일반적인 경우 GPU 렌더링에 사용되는 버텍스 버퍼에 SYSTEMMEMORY 옵션을 주는 것은 절대 하지 말아야 겠다.

그 배경을 이루는 근본 논리는 몹시 단순하다. 우리는 단지 다음과 같은 규칙만 따르면 되는 것이 되게따.
버텍스 버퍼가 CPU에 의해서 Read 작업이 필요한 경우에만 SYSTEMMEMORY옵션을 주어서 생성시킨다.
이것은 CPU가 데이터에 직접 접근하는 데에 사용되는 것을 포함한다.
여기에 모호한 부분은 없는 것 같다. 만약 CPU가 버텍스 버퍼로부터 데이터를 읽어들여야 한다면 해당 버텍스 버퍼는 시스템 메모리에 생성되어야 한다. 오케?

이런 전차로… 좀 우끼고 자빠진 상황이 발생할 수도 있는데, 하나의 버텍스 버퍼를 두고 CPU는 아주 드물게 접근을 하고 GPU는 매우 자주 접근을 해야 하는 경우가 되겠다. 이런 경우에는 차라리 CPU 접근용 System Memory의 VertexBuffer하나랑 GPU접근용으로 AGP에 생성시킨 VertexBuffer하나를 각각 만들어서 적절히 사용하는 것이 바람직하겠다.

주목할 만한 점은 VB 데이터에 대한 가장 전형적인 사용패턴은 Application이 VB를 만들고 이후 단지 ‘쓰기’ 작업만 하는 것이 되겠다. 따라서, CPU를 사용해 VB로부터 데이터를 읽어들인다거나 하는 것은 상태적으로 일반적인 작업은 아닌 것이다.(relatively unusual)

Vertex Buffer에 Lock을 건다는 것이 어떤 의미일까?
Vertex Buffer에 lock을 건다라고 하는 것은 Lock함수 호출을 통해서 리턴되는 포인터를 통해서 VB의 메모리로의 직접적인 접근을 얻는 것이다. 중요한 점은 이때 리턴되는 포인터는 Lock이 걸려있는 동안에 한해서만 유효하다는 것이다. DirectX의 모든 다른 Surfaces들에서와 마찬가지로 이후의 Lock 호출에서 얻어지는 포인터들이 유효(remain valid)하다라는 것 또한 보장할 수 없다. 이것이 의미하는 바는 절대로 한번 Lock을 통해서 얻어진 포인터를 Unlock후에도 어딘가에 저장하고 있다가 사용하려는 우를 범해서는 아니 되신다는 것이다. 뭐 그래도 굳이 사용하겠다면 10중 8,9는 추적이 곤란한 시스템 불안정을 야기 하겠다.

DirectX의 모든 surface들에서 그러하듯, 마찬가지로 Vertex Buffer에 대한 Lock 역시 아주 느린 process에 해당한다. 따라서 Lock을 사용할 때엔 매우 신중해야 한다.

성능향상을 위한 Flag들
치명적으루다가 현재 GPU에서 사용되고있는 VB에 Lock을 건다는 것은 그래픽 파이프라인의 전체 과정을 중단시키고 심각한 성능 저하를 야기 시킨다. 버텍스 버퍼에 lock을 걸때에는 WRITEONLY, DISCARDCONTENTS 그리구 NOOVERWRITE 와 같은 넘들이 올바르게 사용되었는가 확인해야 한다.

뭐 위에서 언급한 flag들에 관한 자세한 내용은 DX Help를 참조하시도록 하고, 이 문서의 끝에 있는 ‘부록 1’ 로 제공된 의사코드들을 주의깊게 살펴들보고 반드시 이해하도록 하시라!

DISCARDCONTENTS 과 NOOVERWRITE 옵션 둘다를 동시에 사용하면 어떤 우끼고 자빠진 일이 발생하는 거지?

본인을 포함한 많은 사람들은 위의 두 가지 플래그 모두가 동시에 세팅 되었을 경우 DISCARDCONTENTS 요넘이 우세할 것이라고 오해하고 있다. 후후… 우낀넘들…  NOOVERWRITE 옵션이 사용되는 경우엔 App는 현재 사용되고 있는 데이터에 대해 어떤 손상도 입히지 않는다는 묵시적인 약속이 내부적으로 이루어져 있다. 이런 전차로 NOOVERWRITE 옵션이 주어지는 경우 자연스럽게 DISCARDCONTENTS 요넘은 무시되어 버리게 된다.

뭐 어찌됬거나 Lock 호출할 때 위의 두 가지 옵션을 동시에 주는 일은 하지 않기를 권한다.

음… WRITEONLY 플래그를 버텍스 버퍼 생성시, 그리고 Lock을 호출하는 두 경우 모두에 사용하게 되면 driver는 AGP 메모리를 리턴하기 땜에 아주아주 이득이 있겠다…
뭐 앞에서도 언급했듯, CPU가 AGP 메모리에서 데이터를 읽을 때엔 아주 느리지만, GPU가 읽을 때엔 CPU 와 cached된 System Memory사이의 읽기 속도 보다도 몇 배나 빠르거덩

구럼 언제는 버텍스 버퍼를 쓰는게 적합하지 않은건데?
네버! 씨바 그런경우 절대루 엄따!
뭐 알고있겠지만 만약 TnL을 지원받도록 코딩을 하고 있다면 반드시! 언제나! Vertex Buffer를 사용해야 한다. 이건 모든 버텍스 데이터들에 적용되며, 심지어는 3D 연산을 사용하지 않는 HUD 아트(뭐냐? -_-a)나 text같은 것에도 적용이 되겠다. 만약 니눔이 버텍스 버퍼 사용에 실패했을 시에는(fail to use a vertex buffer - 역주: D3D Vertex Buffer를 사용하는 것이 아니라 User Buffer를 사용하는 경우를 말하는 것 같음) D3D runtime이 알아서 데이터들을 내부 Vertex Buffer로 복사해서 사용은 하겠지만, 이때D3D가 알아서 DISCARDCONTENTS 나 NOOVERWRITE를 이용해 위에서 언급한 최적의 규칙들을 적용하지는 않게 되겠다.
흠… 최악의 경우 D3D Vertex Buffer를 사용하지 않는 경우 약 1/2 정도의 성능의 저하가 발생하는 것을 나는 봤지롱. 여러분은 절대루 이런 오류를 범하는 일이 없기를 바란다.
Problem Resolution
각각의 경우를 다루기 위한 방법들:
Static Data
Static data 는 수백 프레임동안 혹은 게임이 진행되는 동안 내내 바뀌지 않는 버텍스 데이터를 말한다. 이러한 스태틱 데이터들은 반드시 AGP메모리에 상주시켜 다루어야 한다. (이렇게 하려면 WRITEONLY 옵션은 켜고 SYSTEMMEMORY옵션은 주지 않아야 한다.) 그런데 일반적으루다가 AGP메모리량은 free Video 메모리보다는 훨씬 많기 때문에 상당히 큰 사이즈의 버텍스 버퍼를 유지하는 것도 가능하다. 현존하는 대부분의 게임들은 32MB 의 버텍스 데이터를 넘지 않고 있다. 그런 관계로 니넘이 뭔가 그렇게 해야 하는 특별한 이유가 없는 한 다른 넘들처럼 하는게 좋겠지? 버뜨! 한가지 알아야 할 점은 AGP에서 메모리를 할당받는 것은 언제나 실제 physical memory를 사용한다는 것이다. (역주: Lockable Memory를 말하는 것이다. 절대 OS의 Virtual Memory Manager에 의해서 swapping되는 경우가 없다)

음… 각각 static 버텍스 버퍼에 대해서는 반드시 다음의 순서를 지키도록 하자.
버텍스 버퍼 생성시에는 WRITEONLY 옵션만 주기로 하자
WRITEONLY옵션을 주고 버텍스 버퍼를 한차례 Lock을 걸고 데이터를 채우자. 만약 이 버텍스 버퍼에 데이터를 채우는 과정이 성능에 민감한 부분이라면 순차적으로(sequential) 데이터를 Write해서 CPU가 더욱 좋은 성능을 발휘하도록 도와주자. 말했다시피 AGP 메모리로의 쓰기용 랜덤 접근은 무쟈게 느리다.
Vertex 버퍼를 Unlock 한다.
Vertex Buffer를 Optimize한다. (역주: DX7 에 존재하던 Optimize()함수를 호출하라는 얘기인 거 같다)
Dynamic Data
Dynamic 데이터는 내용이 바뀌며 게임진행동안 계속해서 ‘Write’연산이 필요한 데이터를 말한다. (비록 드물게 내용이 바뀔지라도) 그런 이유로 때때로 Lock이 필요할 것이므로, Optimize() 함수의 호출 대상이 되어선 안되겠다.
Dynamic 데이터는 크게 2가지 분류로 나눌 수 있겠다.
하나는 주로 Write 오퍼레이션의 대상으로 효율적인 렌더링이 이루어지는 넘이며 일반적으로 Dynamic Data를 사용하는 주 목적이 되겠다.
또 다른 하나는 CPU에 의해서 ‘읽기’, ‘쓰기’ 모두 이루어지는 넘이 되겠다. 알다시피 CPU의 AGP 메모리 Read는 몹시 느리기 때문에 이 넘들은 성능을 위해 AGP 메모리에 상주해선 안되겠다. 위에서 언급한 2가지 dynamic 데이터들은 앞으로 본 문서상에서 각각 ‘W/O’(Write Only) 와 ‘R/W’(Read Write)로 언급하게 될 것이다.

각 Dynamic 데이터에 대해서는 다음의 룰들이 적용되어 진다.

W/O:
WRITEONLY 옵션만을 주어서 생성하고, 데이터가 갱신될 때마다 …
WRITEONLY 와 DISCARDCONTENTS나 NOVERWRITE 중 하나를 세팅해서 Lock을 걸어서 데이터를 채운다.  어떤 플래그를 언제 주어야 할지 선택하기 위해 부록1 의 의사코드를 가이드라인으로 사용하기 바란다. 만약 이 부분이 성능에 민감한 부분에서 수행된다면 스태틱에서와 마찬가지로 CPU가 최적화 할 수 있도록 가급적  Sequential하게 Write를 하도록 하자.
버텍스 버퍼 Unlock
DrawIndexedPrimitive() 호출

R/W:
버텍스 버퍼를 SYSTEMMEMORY 옵션을 주어서 생성하고 데이터 생신이 필요할 때마다
Vertex 버퍼를 Lock하고 하고싶은 수작 맘대로 하렴. 버뜨, DISCARDCONTENTS, NOOVERWRITE 그리구 WRITEONLY 플래그들 중 어떤 것도 Locking 옵션으로 줄 수가 없단다…
버텍스 버퍼 Unlock
DrawIndexedPrimitive() 호출
Large data sets
졸라 큰 데이터 셋은 일반적으로 보다 작은 서브셋들로 나누어서 각 서브셋들의 특징에 맞게 static 혹은 dynamic 버퍼로 취급하는 것이 보다 바람직하겠다. 만약 몹시 큰 버텍스 버퍼나 많은 수의 버텍스 버퍼들을 만들 생각이라면 아래 언급하는 것들에 주의해야 한다.

첫째, 생성시키는 모든 버텍스 버퍼 각각에 대해서 DX runtime내부적으로 리소스 관리를 위해서 대략 2K 정도의 메모리 overhead가 발생한다.

만약 버텍스 버퍼를 여러 개 생성시킨다면 일반적으로 그것이 의미하는 것은 버텍스 버퍼간의 스위칭이 빈번하게 발생된다는 것이 될텐데, 이런 버텍스 버퍼 transition은 DX상에서 몹시 오버헤드가 큰 작업 중 하나이므로 가능한 이런 경우를 피하도록 노력해야 한다.

비록 하나의 버텍스 버퍼가 가질 수 있는 총 버텍스 개수는 65535개가 되겠지만 실제로 이렇게까지 쓰는 것은 당근 바람직한 것이 못되겠다. 기본적으로 DX7의 Vertex Buffer renaming scheme 은 드라이버가 원본 버퍼와 동일 사이즈의 연속된 free AGP 메모리 블록을 찾도록 요구하기 때문에 이러한 요구사항을 만족시키기 위해서는 모다 작은 메모리 블록을 사용하는 것이 아무래도 드라이버 입장에선 일하기가 보다 수월하겠다.
Note that although the maximum number of vertices in a VB is 65535 it’s not usually a great idea to approach this kind of size.  Since the VB renaming scheme in DX7 requires the driver to find a contiguous free block of AGP memory which the same size as the original buffer it can be easier for the driver to satisfy these requirements if the request is for a smaller chunk of memory.  Typically that’s more likely in cases where the VBs are themselves smaller.
Typically that’s more likely in cases where the VBs are themselves smaller.

데이터 셋들의 사이즈가 시스템의 실제 physical memory의 사이즈와 같거나 보다 클 경우라면 일반적으로 다음과 같은 다음과 같은 방식으로 처리하는 것이 좋겠다.

가정:
Physical memory: 128Mb
AGP Heap size: 44Mb
Vertex Data: 256Mb

Suggested arrangement:
지원해야 하는 각각의 FVF에 대해서:
약 4K 정도의 버텍스 데이터를 담을 수 있는 버텍스 버퍼를 WRITELONY 플래그를 주어 AGP 메모리에 생성시킨다.
사용할 FVF와 동일한 포맷을 가지는 메모리 버퍼 사본을 시스템 메모리에 생성한다. 비록 이 시스템 메모리에 생성된 버텍스 버퍼 사본이 모든 시스템의 physical memory를 사용해서 Windows가 Virutual Memory Manager를 가동시키더라도 상관 읎다.

Then at render time:
BeginScene()
1) Lock() 호출에선 해당 버텍스 버퍼를 WRITEONLY 와 DISCARDCONTENTS 플래그를 이용해서 Lock을 건다. 그리고 이후의 Lock 호출에선 WRITEONLY와 NOOVERWRITE 두개를 사용한다.
(역주: 원문은 Lock the appropriate VB using WRITEONLY and NOOVERWRITE for the first lock, and using both WRITEONLY and DISCARDCONTENTS on subsequent locks 로 되어있는데, 아무래도 저자가 잠시 착각한 듯 하여, 수정해서 번역했습니다. 이때, 두번째 이후부터 Lock을 할 때 버텍스 버퍼에 남은 공간이 부족할 경우엔 다시 DISCARD 옵션을 주어서 버텍스 버퍼를 flush시키고 사용을 하면 됩니다. 물론 그 이후부터는 버텍스 버퍼를 다 사용할 때까지 다시 NOOVERWRITE옵션을 사용하면 되지요…)

이제 시스템 메모리에 생성되었던 사본 데이터를 Vertex Buffer가 가득 찰 때까지 복사해 넣는다.
2) Unlock 한다.
3) 렌더링 한다.
4) 현재 작업중인 버텍스 포맷을 사용하는 모든 데이터가 렌더링 될 때까지 위의 작업을 반복한다
5) 전체 FVF의 버텍스 데이터들이 모두 렌더링 될 때까지 위의 작업을 반복한다.
EndScene()
Flip() or Blt()

주목할 점은 위의 렌더링 순서를 매 프레임마다 반대로 함으로써 이전 프레임의 마지막에 physical memory에 남아있던 버텍스 데이터를 현재 프레임의 시작 시에 재사용할 수 있다는 것이다. 이렇게 하면 Windows의 Virtual Memory Manager가 매번 버텍스 데이터를 swap in/out 하는 과정을 최대한 피할 수 있겠다.

계속해서 언급하는 바 이지만, 버텍스 버퍼에 데이터를 기록할 때 연속된 곳을 차례로 접근함으로써 보다 나은 성능을 얻을 수 있다. 그렇지 않은 경우 CPU가 AGP 메모리와 연동하는 mechanism때문에 몹시 열악한 성능을 보여줄 것이다.

Essential thinking… or “Why it should all make sense”.

일단 버텍스 버퍼가 왜 좋은거냐?

버텍스 버퍼가 좋은 이유는 그것들은 소유권의 개념(semantics of ownership)이 있으며, driver상에서 버텍스 버퍼를 최적의 메모리에 위치시키는 능력을 지니고 있기 때문이다. 버텍스 버퍼의 생성과 Locking시에 적절한 플래그들의 사용은 최적의 성능을 얻기 위해 필수 불가결한 것이다.
만약 semantics of ownership이라고 하는 명쾌한 개념이 없다면 driver는 Vertex Buffer의 내용을 driver가 유지하고 있는 메모리로 복사하게 될 것이고 이는 지속적인 성능 저하를 야기하며 필요이상으로 bandwidth를 허비하게 될 것이다.

Register renaming in CPUs
Driver레벨에서 지원되는 기술로 “VB renaming”이라는 이름으로 알려진 녀석인데, 최근 고성능 CPU 디자인으로부터 생겨난 기술되겠다.
음… 이 졸라 좋은 기술을 접해보지 못한 너거뜰을 위해 이제부텀 내가 어떤 상황에 이 넘을 사용해서 고성능 병렬 register 기반 CPU들에서 얻을 수 있는 졸라 사악한 이득을 알려주마

다음과 같은 instruction 수행 순서를 가지는 object code를 가정해 보자.

mov        a,1
mov        b,a
mov        a,2

첫번째 명령은 별 문제가 없다. 값 1가 a로 들어가겠지…
두 번째 명령은 a 의 내용을 b 로 복사한다.
세 번째 명령은 2번째의 명령이 완료되기를 기다렸다가 값 2를 a 에 대입하게 된다.

여기서 내가 말하고자 하는 점이 무엇인고 하니 세 번째 명령 실행 시에 발생하는 묵시적 대기 상태를 어떻게 제거할 수 있겠는가 하는 것이다.
분명 이 상태로는 2번째 명령이 완료되기 전에는 a 로 2 라는 값을 써넣는 것이 불가능 하다.

음… 그 해결책은 code를 통해서는 접근이 불가능하고 오직 프로세서 자신에 의해서 운영되는 제 3의 레지스터를 낑궈 넣는 것이 되겠다.
세 번째 명령은 분명 이전에 a 가 가지고 있던 값을 날려버리게 되므로 우리는 값이 실제로 어디로 씌여지건 신경 쓰지 않아도 되므로, 이후 ‘a’가 언급된 곳을 그것이 참조하는 값으로 간주할 수 있다. 이쯤에서 이후의 명령수행에서 효과적으로 ‘a’ 를 대신해서 사용될 쉐도우 레지스터 ‘S’를 소개한다. 이제 우리는 위의 코드를 다음 과 같이 대치해서 동일한 결과를 얻을 수 있다. (아… 해석이 매끄럽게 되지 않아서 원문 붙임다 -_-)
Since the 3rd instruction is guaranteed to destroy the previous contents of ‘a’ we don’t really care where the value is put provided that we subsequently take all mention of ‘a’ as referring to that value.  In this case we’ll introduce a shadow register ‘S’ which effectively takes the place of ‘a’ in subsequent instructions.  So we can substitute the following code and gain the same effect.

mov        a,1
mov        b,a
mov        S,2

이제 ‘a’ 로의 모든 참조를 ‘S’ 로 대치 시켰고, 2번째, 3번째 명령을 완벽히 병렬처리 시킬 수 있게 되었다. 이로써 우리는 한번에 수행될 수 있는 명령들의 개수를 2배로 만들었다.
Now, provided that we subsequently direct all references to ‘a’ towards ‘S’ then we can completely paralellize the 2nd and 3rd instructions.  If we achieve this then we have doubled the number of instructions which can be handled at one time.

Vertex Buffer renaming 역시 같은 방법으로 동작하지만 driver가 App의 free memory가 허용하는 한 renaming을 수행하게 함으로써 renaming 방법을 보다 일반화 시켰다.
VB renaming works in the same way but generalizes the renaming method by allowing the driver to perform renaming as many times as the app (and free memory) allows

Renaming없이는 성능에 그다지 감동의 물결이 넘실대지는 않을 것이다.
Without renaming performance can be pretty unimpressive.

Lock과 Create에 사용되는 flag들과의 적절한 조합으로 몇 배나 높은 성능을 얻을 수 있다.
With constructive use of the Lock and Create flags performance can be several times higher.

GPU 캐쉬에 대한 고찰
Post-transform cache의 경우는 DrawIndexedPrimitive(Stip 이나 List를 사용할 경우)와 같은 Indexed 렌더링 인터페이스를 사용할 경우만 작동한다.
또한 메모리 캐쉬는 높은 Locality of Reference가 적용될 경우에만 실제 효과를 볼 수 있을 것이다.
이런 이유로, 성능 극대화를 위해 Indexed triangle strip을 사용할 것을 졸라 권장한다. 왜냐구? 알다시피 indexed tri strip을 사용하게 되면 버텍스들이 자연적으로 VB상에서 정렬되어 있어야 하고 인덱스 시퀀스에 의해서 자연스럽게 ‘Locality of Reference’가 효율적으로 이루어지기 때문이다.

만약 strip을 사용하는 것이 부적절 한 것으로 판단이 될 경우엔 indexed list를 사용하는 것이 좋다. 그러나 -_-;
If it proves impractical to use strips then use indexed lists but otherwise observe all of the same rules.

DX8 에서 바뀐 것들

DX8 에서 Vertex Buffer를 다루는 데에 있어서 바뀐 중요한 점 2가지는 Indexed Buffer와  Lightweight VertexBuffer change가 추가 되었다는 것이다.

Index Buffer(이후 ‘IB’) 는 index 데이터를 API로 넘겨주는 매우 효율적인 방법이다. 특정 버텍스 버퍼와 적절히 associate되어 전혀 변하지 않거나 아주 드물게 변하는 인덱스 셋을 사용하는 경우라면 뛰어난 최적화의 기회를 제공하게 된다

또한 DX8에서는 32Bit 인덱스들을 사용할 수도 있으며, 이전보다 훨씬 큰 사이즈의 버텍스 버퍼를 사용할 수도 있게 되었다.

DX8 에서는 Vertex Buffer를 사용하지 않고는 렌더링하는 것을 더욱 곤란하게 하여 잘못된 작업을 하는 것을 더욱 곤란하게 했다.
In DX8 it is now harder to render without using Vertex Buffers – which reduces the number of  opportunities for getting things wrong

그리고, advanced 사용자를 위해 separate DMA stream 이라고 하는 새로운 개념을 도입했는데, 이것을 사용할 경우 서로 다른 버텍스 버퍼들로부터 서로다른 버텍스 데이터의 구성요소들(components)을 사용할 수 있게 된다. 이로서 버텍스 데이터 내에 변하지 않는 부분들은 분리된 static VB에 위치 시키고 실제로 값을 바꾸어야 할 필요가 있는 부분만 접근하고 조작 할 수 있도록 하여 추가적인 이득을 얻을 수 있게 되었다.
And, for advanced users, there is a new idea of separate DMA streams which allow you to take different components of the vertex data from different VBs.  This has the additional benefit of allowing the unchanging parts of the vertex data to be placed in a separate static VB and therefore allowing the app to access and change only that part of the data which actually needs to change.

한마디로 DX8은 잘못된 코딩을 하는 것이 올바른 작업을 하는 것보다 어렵게 되었다. 만약 DX8로의 upgrade를 주저하고 있다면 지금 다시 한번 잘~ 생각해 보시라!

Appendix 1

Pseudo code for updating dynamic Vertex Buffers.

•CreateVB(WRITEONLY, 4K-ish);
•I = 0;
Add:                •Space in VB for N vertices?
  •Yes:  { Flag = NOOVERWRITE; }
•Else
  •No:  { Flag = DISCARDCONTENTS; I = 0; }
•Lock(Flag | WRITEONLY);
•Fill in N vertices at index I
•Unlock();
•DIPVB(I);
•I += N;
•GOTO Add;

Appendix 2

Six simple rules to take home.

Always use VBs, for everything.

If you’re locking a VB then usually you should be using WRITEONLY and one of DISCARDCONENTS and NOOVERWRITE.

VBs shouldn’t be in system memory unless the CPU is going to read from them.

CPUs shouldn’t read from the same VBs that GPUs read from.

Don’t use ProcessVertices to speed up TnL hardware– it’s almost certain to slow it down instead.

Use the resources that NVIDIA makes available to developers on it’s web site.  We supply header files for both C and C++ users which offer a sample implementation of well handled Vertex Buffers.  These can be found on the public part of NVIDIA’s developer web site under the heading “Dynamic Vertex Buffer Header Files” and are at NVIDIA's developer web page.


아… 혼자 보기엔 너무 아까워서, 짧은 영어실력이지만 번역을 함 해보았는데 모쪼록 도움이 되었으면 합니다.
오자, 탈자, 오역이 곳곳에서 발견되어도 널리 이해해 주시기 바랍니다. ^^a

댓글 0

파일 첨부

여기에 파일을 끌어 놓거나 파일 첨부 버튼을 클릭하세요.

파일 크기 제한 : 0MB (허용 확장자 : *.*)

0개 첨부 됨 ( / )
 
목록
번호 제목 글쓴이 날짜 조회 수
64 Guitar World선정 최고의 기타솔로곡 단장 2006.06.14 247598
63 Networking Best Practices in XBOX360 단장 2007.12.19 10894
62 마력 구하는 공식 단장 2013.07.11 7383
61 Windows 8.1 복구 파티션 만들기 단장 2013.11.13 6692
» 버텍스버퍼의 효율적인 사용 단장 2007.10.01 5100
59 효율적인 동기화를 위한 아이디어 하나 단장 2007.09.29 3281
58 vTune 사용법 단장 2009.01.13 3234
57 술의 이력서 단장 2007.02.09 2270
56 Large Address Aware file 단장 2014.03.05 1948
55 Fast Bit Counting 단장 2009.02.21 1883
54 std::tr1 단장 2008.03.13 1848
53 골프의 물리학 단장 2009.05.30 1681
52 자동차 정비용어 정리 단장 2007.01.20 1660
51 Nat기반 P2P 프로그래밍 단장 2007.11.21 1627
50 제트 추력 엔진 단장 2008.04.29 1600
49 이스람(Islam:회교:回敎)에서의 성 단장 2007.02.08 1379
48 한국운전면허를 일본운전면허로 바꾸기 [2] 단장 2007.10.26 1351
47 [C++]function objects 단장 2007.05.01 1341
46 NAT 홀펀칭 단장 2007.10.25 1293
45 Stream of Life 단장 2009.06.29 1285