본문 바로가기
DirectX Practice

Direct3DX 복습하기 3 - Day - Direct3D 11 자원(Memory Resource)(2)

by Srff5123 2024. 9. 24.
728x90

1. 버퍼 자원

버퍼 자원(Buffer Resource)에 속하는 자원들은 Direct3D 11이 사용할 1차원 선형 메모리 블록을 제공한다.

다양한 옵션들을 설정함으로써 행동 방식이 서로 다른 다양한 버퍼들을 만들어 낼 수 있으나, 그림의 나온 것 같은 기본적인 직선형 배치 구조만큼은 모두 동일하다.

 

버퍼의 크기는 바이트 단위이고, 버퍼를 구성하는 원소의 크기는 버퍼의 종류에 따라 다를 수 있으며, 같은 종류의 버퍼라도 특정 설정에 따라 원소의 크기가 다를 수 있다. 원소 개수에 원소 하나의 크기를 곱한 것이 버퍼의 전체 크기이다. 이란 단순한 배열 비슷한 구조 때문에 사용 가능한 버퍼 종류는 매우 다양하다.

버퍼들 중에는 응용 프로그램의 C++ 에서 주로 쓰이는 것도 있고, 파이프 라인에 연결된 후 HLSL 셰이더 프로그램 안에서 주로 쓰이는 것도 있다. 

 

1. 정점 버퍼(Vertex Buffer)

정점 버퍼는 결국에는 정점들로 조립되어서 렌더링 파이프라인에 투입될 자료를 담는 역할을 한다. 가장 단순한 형태의 정점 버퍼는 정점 구조체들의 배열이다. 그러한 배열의 각 원소는 정점의 위치, 법선 벡터, 텍스처 좌표 같은 자료를 담는다.

이러한 정점 원소들은 반드시 사용 가능한 서식(Format)과 형식 명세를 따라야 한다.

그러나 특정 렌더링 알고리즘을 위해 입력 자료를 커스텀화하는 경우 그에 필요한 일반적인 정보를 정점 구조체에 포함시킬 수 있다.

 

위에서 말한 단순한 배열 형태의 정점 버퍼 외에, 좀 더 복잡한 형태의 버퍼를 구성하는 것도 가능하다. 예를 들어 동시에 여러 개의 정점 버퍼를 사용할 수도  있다. 즉, 정점 자료를 여러 개의 버퍼들에 나누어 담을 수 있는 것이다.

이를테면 정점 위치들을 한 버퍼에, 정점 법선 벡터들은 또 다른 버퍼에 담는 등 

이렇게 하면 응용 프로그램은 모든 렌더링 시나리오에서 하나의 커다란 전체 버퍼를 사용하는 대신, 필요에 따라 선택적으로 정점 자료를 추가할 수 있게 된다.

그러면 렌더링 연산에 필요한 자료 전송 대역폭을 줄일 수 있다.

 

또한 다중 버퍼 접근방식은 인스턴싱 방식의 렌더링(Instanced Rendering)을 수행하는 데에도 쓰인다.

이 경우에는 모형의 정점별(Per-Vertex) 자료를 제공하는 하나 이상의 정점 버퍼들과, 정점별 자료가 아니라 

인스턴스별(Per-Instance) 자료를 제공하는 추가적인 정점 버퍼를 둔다. 정점별 자료를 담은 버퍼를 이용해서 동일한 모형의 여러 인스턴스들을 렌더링하되, 인스턴스별 자료를 담은 버퍼를 각 인스턴스에 적용한다.

 

 

해당 그림에 있는 것들은 여러 정점 제출 구성들을 도식화한 것이다.

월드 변환 행렬, 색상 변형 등 모형의 인스턴스들을 각자 차별화하는 데 사용할 수 있는 것이면 어떤 것도 인스턴스별 자료가 될 수 있다. 

이러한 구성에서는 한번의 그리기 메서드 호출로 여러 개의 물체를 렌더링할 수 있으므로 렌더링 연산의 전반적인 CPU 부담이 줄어든다.

 

여러 정점 버퍼 종류들은 앞에서 말한 일반적인 선형 버퍼 배치를 따른다. 각 버퍼는 같은 크기의 원소들로 이루어진 1차원 배열이다. 

자료 원소의 크기는 자신이 속한 버퍼의 다른 원소들과 같다. 그러나 여러 개의 버퍼를 사용하는 경우 각 버퍼의 원소 크기는 서로 다를 수 있다. 

 

1. 정점 버퍼의 용도 및 용법

 

정점 버퍼의 주된 용도는 파이프라인에 정점별 정보를 제공하는 것이다. 정점별 정보를 다중 버퍼 구성에서 직접 제공할 수도 있고, 아니면 인스턴싱을 통해 간접적으로 제공할 수도 있다.

이런 용도에서 정점 버퍼를 파이프라인에 연결하는 주된 지점은 파이프라인의 진입점 역할을 하는 입력 조립기 단계이다. 

정점 버퍼를 스트림 출력 단계에서 연결할 수도있다.

이 경우 스트림 출력 단게를 통해 나온 자료가 정점 버퍼에 기록된다. ( 그 자료를 다음 번 렌더링 패스에서 입력으로 사용할 수 있다.

 

2. 정점 버퍼의 생성

 

파이프라인에 자원을 연결하는 지점마다 그에 해당하는 연결 플래그가 존재한다. 자원을 특정 위치에 연결하기 위해서는 자원을 생성할 때 반드시 그 위치에 해당하는 연결 플래그를 설정해 주어야 한다. 정점 버퍼의 경우 정점 버퍼를 파이프라인에 정점 자료를 입력하는 용도로 사용하라면 D3D11_BIND_VERTEX_BUFFER 연결 플래그를 설정해야 하며, 파이프라인의 정점 자료 스트림 출력용으로 사용하려면 D3D11_BIND_STREAM_OUTPUT 연결 플래그를 설정해야 한다.

연결 플래그 설정 외에 정점 버퍼 생성 시 중요하게 고려할 사항은 버퍼의 사용 패턴에 대한 옵션들이다.

정점 버퍼의 내용이 자주 변할 것인가. 그리고 내용을 누가(CPU, GPU)변결할 것인가에 따라 서로 다른 사용 패턴 플래그들을 설정해야 한다.

예를 들면 버퍼에 정적인 자료를 넣을 셈이라면 D3D11_USAGE_IMMUTABLE 용도 플래그를 지정해서 버퍼 자원을 생성해야 한다.

이 경우, 생성 메서드의 한 인수에 D3D11_SUBRESOURCE_DATA를 지정해서 정점 자료로 버퍼를 일단 초기화한 후에는 버퍼의 내용을 결코 수정할 수 없다.

정적인 지형 메시를 위한 내용을 담는 버퍼가 이런 용도의 정점 버퍼의 예이다.

 

CPU가 자주 갱신할 정점 버퍼라면 버퍼 자원 생성 시 D3D11_USAGE_DYANMIC 용도 플래그를 설정해야 하며, 또한 CPU 접근 매개변수에 CPU 쓰기 플래그도 설정해 주어야 한다.

매 프레임마다 GPU가 아니라 CPU에서 정점 변환을 수행해서 갱신된 버퍼를 복사하는 경우 이런 종류의 정점 버퍼가 적합하다.

이런 CPU 쪽 정점 버퍼 갱신 기법은 다수의 그리기 호출들을 하나의 호출로 축약하기 위해 모든 모형 자료를 하나의 기준계에 맞게 취합하고자 할 때 흔히 쓰인다.

스트림 출력 기능을 통해 GPU가 수정할 정점 버퍼라면 D3D11_USAGE_DEFAULT용도 플래그를 설정하면 된다.

 

 

이 함수는 여러 사용 시나리오에 따라 버퍼 서술 구조체를 각각 다르게 설정한다.

버퍼요소(배열 원소)들의 개수와 버퍼 전체 크기는 버퍼 사용 방식에 무관하게 동일하나. 연결 플래그와 용도 플래그, CPU 접근 플래그는 사용 방식에 따라 달라진다.

다른 종류의 버퍼들에서도 버퍼 서술 구조체를 설정할 때 이와 비슷한 패턴이 쓰인다.

 

3. 자원 뷰 요구 사항

정점 버퍼는 입력기 조립 단계나 스트림 출력 단계에 직접 연결되므로 따로 자원 뷰를 생성해서 적용할 필요가 없다.

 

4. 색인 버퍼(Index Buffer)

저장된 정점 자료를 참조해서 기본도형(Primitive)을 정의하는 아주 유용한 능력을 제공한다. 간단히 말하자면, 색인 버퍼는 정점들의 목록 안을 가리키는 색인들의 목록을 담는다. 원하는 기본도형의 종류에 따라 적절한 개수(점은 1, 선분은 2, 삼각형은 3 등)의  색인들로 기본도형을 구성하는 정점들을 지정함으로써 기본도형을 정의한다.

 

색인 버퍼를 사용하면 정의해야 할 전체 정점 개수를 크게 줄일 수 있다. 서로 인접한, 따라서 정점들을 공유하는 기본도형들을 정의하는 경우, 그런 정점들을 정점 버퍼에 중복해서 저장할 필요 없이 색인 버퍼를 이용해서 여러 번 참조하면 되기 때문이다.

또한 이러한 정점 공유는 여러 개의 기본도형들이 한 정점 셰이더의 동일한 출력 정점을 사용할 수 있게 한다.

정점 셰이더를 거친 정점을 캐시에 담아 두고 여러 개의 기본도형들에 재사용하는 것이 가능하다

그런 재사용은 주어진 하나의 모형에 대한 정점 셰이더의 처리량을 줄일 수 있다.

 

5. 색인 버퍼의 용도 및 용법

 

색인 버퍼는 기본도형 설정 연산에서 쓰일 정점들을 저장하므로, 색인 버퍼를 사용하려면 파이프라인에서 사용할 기본도형들의 위상 구조를 미리 알아야 한다. 

그렇지 않으면 색인 버퍼에 색인들을 어떤 순서로, 저장하면 되는지를 알수가 없다. 일반적으로 기본도형의 특성들은 구체적인 코딩 이전에 렌더링 알고리즘과 기하구조 적재 루틴을 선택, 설계하는 과정에서 함께 결정한다.

색인 버퍼를 생성하고 채운 후에는, 그리기 연산의 수행을 위해 파이프라인을 구성하는 과정에서 색인 버퍼를 입력 조립기 단계에 연결한다.

그 단계는 색인 버퍼를 이용해서 기본도형을 생성해 파이프라인의 다음 단계로 넘겨준다.

색인 버퍼는 아주 구체적인 용도로 쓰이므로, 파이프라인의 다른 어떤 지점에 연결되는 경우는 없다.

 

6.색인 버퍼의 생성

색인 버퍼의 생성 역시 표준적인 버퍼 생성 과정을 따른다. 핵심은 버퍼 서술 구조체 D3D11_BUFFER_DESC를 적절히 채우는 것인데, 색인 버퍼의 경우에는 이 구조체의 설정이 개별 색인 버퍼마다 그리 다르지 않다.

왜냐 색인 버퍼에 채울 자료는 대체로 콘텐츠 제작 프로그램에서 정의해 뽑아낸 것이기 때문이다.

일반적으로 색인 버퍼는 응용 프로그램 초기 단계에서 생성된 후에 별로 변하지 않는다.

그러나 색인 버퍼를 동적으로 갱신해야 하는 새로운 알고리즘을 채택할 수도 있다.

그런 경우를 위해 동적 갱신 설정들로 색인 버퍼를 생성하는 것도 가능하다.

그리기 호출 횟수를 줄이기 위해 여러 개의 기하구조 집합들을 실행 시점에서 하나의 정점 버퍼와 하나의 색인 버퍼로 합치는 경우 동적 갱신이 가능한 색인 버퍼가 필요하다.

이 함수는 우선 버퍼의 크기를 비롯해서 서술 구조체의 여러 필드들을 설정한다.

코드에서 보듯이, 색인 버퍼를 생성할 때에는 연결 플래그를 항상 D3D11_BIND_INDEX_BUFFER로 설정한다.

정적 색인 버퍼의 경우 함수는 용도 플래그를 D3D11_USAGE_IMMUTABLE로 하고 CPU 접근 플래그는 0으로 설정한다.

이 경우 호출자는 반드시 D3D11_SUBRESOURCE_DATA 구조체를 통해서 버퍼의 내용을 제공해야 한다.

동적 색인 버퍼의 경우에는 용도 플래그를 D3D11_USAGE_DYNAMIC으로, CPU 접근 플래그를 D3D11_CPU_ACCESS_WRITE로 설정한다.

 

7. 자원 뷰 요구 사항

색인 버퍼는 파이프라인의 입력 조립기 단게에 직접 연결되므로 자원 뷰를 생성해서 적용할 필요가 없다,

 

2. 상수 버퍼(Constant Buffer)

상수 버퍼는 앞에서 본 두 버퍼와는 달리프로그램 가능 셰이더 단계에 접근해서 HLSL 코드 안에서 직접 접근할 수 있는 종류의 자원이다.

상수 버퍼는 파이프라인 안에서 실행되는 프로그램 가능 세이더 프로그램에 상수 정보를 제공하는데 쓰인다. 상수라는 이름이 붙은 것은, 이 상수 버퍼 안의 자료가 한 번의 그리기 호출이나 배분 호출이 실행되는 동안에는 결코 변하지 않기 떄문이다.

일반적으로 파이프라인 실행들 사이에서만 변하고 한 파이프라인 패스 안에서는 변하지 않는 정보, 이를테면 월드 변환 행렬이나 물체의 색상 같은 것을 이 상수 버퍼에 담아서 셰이더 프로그램에 제공한다.

이러한 매커니즘은 호스트 응용 프로그램에서 각 프로그램 가능 셰이더 단계들 각각에 자료를 공급하는 주된 수단이다.

상수 버퍼 안에 담는 정보의 형식과 양은 상수 버퍼마다 다를 수 있다.

상수 버퍼 안에 담기 정보의 형식과 양은 전적으로 각 셰이더 프로그램에 필요한 자료의 종류와 성격에 의존한다.

HLSL의 기본 형식들을 임의로 조합해서 상수 버퍼를 구성할 수 있으며, 또는 그런 기본 형식들로 이루어진 구조체들로

구성할 수도 있다.

HLSL 고수준 셰이딩 언어는 스칼라나 벡터, 행렬 형식과 그런 형식들의 배열, 클래스 인스턴스, 그리고 이 모든 형식들의 조합을 상수 버퍼에 담을 수 있다.

상수 버퍼는 지금까지 살펴본 다른 버퍼들과는 다소 다르다.

정점 버퍼와 색인 버퍼는 하나의 기존 자료 원소를 정의하고 그 원소를 여러 번 되풀이한다.

즉 배열과 같은 구조이다. 그러나 상수 버퍼는 여러 종류의 자료 원소들을 정의하되, 각 원소를 되풀이 하지는 않는다.

버퍼는 정의된 원소들을 담을 만큼의 크기로만 생성된다.

다른 말로 하면, 상수 버퍼는 배열이 아니라 구조체라고 할 수 있다.

 

1. 상수 버퍼의 용도 및 용법

 

프로그램 가능 파이프라인 단계들 각각마다 하나 이상의 상수 버퍼들을 받을 수 있다.

그 단계들은 연결된 상수 버퍼에 담긴 정보를 자신의 셰이더 프로그램에게 넘겨준다.

셰이더 프로그램 안에서는 상수 버퍼의 자료를 마치 셰이더 프로그램 안에서 전역으로 선언된 구조체처럼 사용한다.

이는 각 구초제의 요소들이 이 가상 전역 범위 안에서 반드시 고유한 이름을 가져야 한다는 뜻이기도 하다.

버퍼 구조체처럼 사용할 수 있게 하는 능력 덕분에, 특정 렌더링 알고리즘을 위해 변형된 셰이더 프로그램을 추가하는 과정이 좀 더 유연해 진다.

상수 버퍼로써 묶기 위해 생성한 버퍼를 파이프라인의 다른 종류의 연결 지점들에는 묶지 못할 수 도있다.

위의 그림에 상수 버퍼를 묶을 수 있는 지점들이 나와 있다.

비교적 큰 상수 버퍼를 만드는 것이 가능하긴 하지만, 셰이더 프로그램에 필요한 모든 매개변수를 담은 커다란 하나의 버퍼를 만드는 것이 바람직한 것은 아니다.

상수 버퍼의 내용을 CPU에서 갱신할 때마다 버퍼를 GPU로 다시 전송해야 한다.

이것이 응용 프로그램 하나에 열 개의 매개변수 들이 필요하나. 물체를 헨더링할 때마다 변하는 것은 그 중 둘 뿐이라고 해도 그리기 메서드를 호출할 때마다 매개변수 열 개를 모두 버퍼에 기록해서 전송해야 한다.

상수 버퍼의 일부만 갱신해서 일부만 전송할 수는 없기 때문이다.

렌더링할 물체의 개수에 따라서는, 이런 추가적인 갱신이 누적되어서 불필요한 대역폭 낭비가 커질 수 있다.

좀 더 나은 방법은 상수 버퍼를 두개 만들어서 하나에는 자주 변하지 않는 매개변수 8개를 담고 또 하나에는 자주 변하는 두 매개변수를 담는 것이다. 이렇게 하면 매 프레임마다 두 뻔째 버퍼만 갱신함으로써 프레임당 자료 갱신량을 크게 줄일 수 있다.

물론 두 번째 버퍼 역시 정말로 필요한 때에만 갱신될수 있도록 신경을 써야한다.

 

2. 상수 버퍼의 생성

상수 버퍼는 최적의 성능을 위한 다양한 자원 구성 옵션들을 제공한다.

상수 버퍼의 구성은 CPU가 얼마나 자주 갱신할 것인가에 따라 크게 두 가지로 나뉜다.

응용 프로그램의 수명주기 동안 상수 버퍼를 자주 갱신할 것이라면 상수 버퍼를 동적 버퍼 형태로 구성하는 것이 바람직하다.

반대로 응용 프로그램 전반에서 변하지 않을 자료를 담는다면 불면 용도 플래그를 지정해서 정적 버퍼로 만드는것이 당연하다.

위의 코드는 전형적인 상수 버퍼 생성 방법을 보여주는 예시이다.

 

정점 버퍼나 색인 버퍼처럼, 동적인 상수 버퍼를 생성할 때에도 용도 플래그 D3D11_USAGE_DYNAMIC을 지정한다.

또한, 실행 시점에서 CPU가 자원을 갱신할 수 있도록 CPU 접근 플래그 D3D11_CPU_ACCESS_WRITE도 지정한다.

정적 상수 버퍼의 경우에는 D3D11_USAGEIMMUTABLE 용도 플래그를 지정하고 CPU 접근은 전혀 없는 것으로 설정한다.

이런 기본 구성 외에, 실행 시점에서 GPU만이 상수 버퍼를 갱싱하게 할 수도있다.

이를 테면 추가/소비 버퍼의 원소 개수를 ID3D11DeviceContext::CopyStructureCount() 메서드를 이용해서 상수 버퍼에 기록하는 등이 그러한 GPU 갱신의 예이다.

이런 경우에는 기본 용도 플래그를 지정하고 CPU 접근은 전혀 없는 것으로 두면 된다. 

그러면 Direct3D 11 실행 시험 모듈이 그 자원을 GPU만 사용하도록 최적화할 수 있다.

 

위의 코드 예제에는 없는 주목할 사항이 몇가지 있다.

1. 일반적인 경우 응용 프로그램은 HLSL 안에서 보이는 상수 버퍼의 내용에 대응되는 C++ 구조체를 정의해서 사용한다.

그러면 응용 프로그램은  주 메모리 안에서 그 구조체 인스턴스를 갱신하고, 그것을 그대로 버퍼에 복사할 수 있다.

 

2. 상수 버퍼의 크기는 반드시 16바이트의 배수이어야 한다. 이러한 요구사항은 GPU의 4쌍(4-tuple) 레지스터 형식들로 버퍼를 효율적으로 처리하기 위한 것인데, 오직 상수 버퍼에만적용된다. C/C++  안의 구조체를 정의할 때 이러한 제약에 유의 해야한다.

 

3. 상수 버퍼 서술 구조체의 연결 플래그에는 오직 D3D11_BIND_CONSTANT_BUFFER만 지정할 수 있다.

이것은 별로 중요한 제약이 아닌데, 어차피 상수 버퍼를 파이프라인의 다른 위치에 연결해야 할 일은 거의 없기 떄문이다.

 

3.자원 뷰 요구사항 

상수 버퍼는 파이프라인의 프로그램 가능 셰이더 단계들에 연결하고 HLSL에서 접근하는 버퍼이지만, 그 내용을 자원 뷰가 다르게 해석해야 할 여지는 없다.

상수 버퍼의 내용은 버퍼에 정의된 그대로 HLSL에 노출되므로 자원 뷰를 사용할 필요가 없다.

 

4. HLSL의 상수 버퍼 자원 객체

이전의 두 버퍼와는 달리 상수 버퍼는 프로그램 가능 셰이더 단계의  HLSL 코드에서 직접 접근할 수 있다.

HLSL 안에서 상수 버퍼는cbuffer 라는 키워드를 이용해서 선언하는 구조체에 대응된다.

  HLSL 셰이더 프로그램은 cbuffer로 선언된 상수 버퍼 구조체 안의 필드들을 마치 전역 범위에서 선언된 변수처럼 사용할 수 있다.

목록에서 보듯이 상수 버퍼 구조체 자체에도 이름이 붙어 있는데, 이 이름은 응용 프로그램이 버퍼를 이름으로 식별해서 적절한 내용을 버퍼에 적재하는 데 쓰인다. 

HLSL 프로그램 자체는 버퍼 이름을 사용하지 않음을 주의해야한다.

상수 버퍼의 이름이나 상수 버퍼의 개별 원소의 이름과 현식은 셰이더 반양 API를 이용해서 알아낼 수 있다.

이 API는 각 필드의 이름과 형식을 알려주는 메서드들을 제공한다.

이들을 이용하면 응용 프로그램은 실행 시점에서 버퍼에 어떤 정보를 집어넣어야 하는지를 파악할 수 있다.

 

 

 

728x90