Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

codingfarm

6. Direct3D의 그리기 연산 - 상수 버퍼(constant buffer) 본문

computer graphics/DX12 book

6. Direct3D의 그리기 연산 - 상수 버퍼(constant buffer)

scarecrow1992 2021. 6. 7. 00:05
  • 버퍼 : shader 프로그램에서 참조하는 자료를 담는 GPU 자원(ID3D12Resource)의 예
  • 버퍼의 종류 : 텍스처, 기타 버퍼 자원등

 

0. 상수버퍼

  • 정의
    • 장면의 물체마다 달라지는 상수 데이터를 담기 위한 저장공간
  • vertex buffer나 index buffer와 달리 constant buffer는 CPU가 프레임당 한번 갱신하는것이 일반적이다.
    • 가령 카메라가 매 프레임 이동한다면, 프레임마다 상수 버퍼를 새 시야 행렬로 갱신해야 한다.
  • 크기가 반드시 최소 하드웨어 할당 크기(256 바이트)의 배수 여야 한다.

가령 이전에 배운 vertex shader에서 아래와 같은 코드를 본적 있을것이다.

1
2
3
cbuffer cbPerObject : register(b0) {
    float4x4 gWorldViewProj; 
};
cs

이 코드는 cbPerObject라는 cbuffer 객체(constant buffer)를 참조하는 상수 버퍼 구조체이다. 이 예에서 constant buffer는 gWorldViewProj라는 4$\times$4 행렬 하나만 저장한다. 이 행렬은 한 점을 국소 공간에서 동차 절단 공간으로 변환하는데 쓰이는 global matrix, 시야 행렬, 투영 행렬을 하나로 결합한것이다.

HLSL에서 4$\times$4 행렬은 내장형식 float4x4로 대표된다. 그 외에도float3x4, float2x2등 여러 형식이 있다.

 

1. 상수 버퍼의 생성

 

앞서 배운 constant buffer의 특징을 살펴보면, 생성시 주의사항이 2가지 존재한다.

  1. default heap이 아니라 upload heap에 만들어야 한다.
    • 그래야 CPU가 버퍼의 내용을 갱신할 수 있다.
    • default heap은 CPU가 갱신 불가
  2. 크기는 256 byte의 배수여야 한다.

 

장면내 물체의 특서을 저장하는 constant buffer의 특성상, 같은 종류의 상수 버퍼를 여러개 사용해야 하는 경우가 많다.

가령 위의 상수버퍼 cbPerObject는 물체마다 달라지는 상수들을 담기위한 버퍼이므로, 장면의 물체가 n개이면 이 종류의 상수 버퍼가 n개 필요하다.

다음 코드는 NumElements 개의 상수 버퍼 객체를 담는 하나의 버퍼를 생성하는 방법을 보여준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct ObjectConstants {
    DirectX::XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4();
};
 
UINT elementByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
 
ComPtr<ID3D12Resource> mUploadCBuffer;
device->CreateCommittedResource(
        &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
        D3D12_HEAP_FLAG_NONE,
        &CD3DX12_RESOURCE_DESC::Buffer(mElementByteSize * NumElements),
        D3D12_RESOURCE_STATE_GENERIC_READ,
        nullptr,
        IID_PPV_ARGS(&mUploadCBuffer));
cs

mUploadCBuffer를 ObjectConstants 형식의 상수 버퍼들의 배열을 담는 buffer라고 볼 수 있다. mUploadCBuffer는 그저 constant buffer를 묶는 역할을 할 뿐 cb가 아니지만, 이러한 버퍼 자체를 constant buffer라 칭하기도 한다.

어떤 물체를 그릴때가 되면 이 buffer에서 해당 물체를 위한 상수들이 있는 부분영역을 서술하는 constant buffer view를 pipeline에 묶는다.

여기서 mElementByteSize는 하나의 상수 버퍼의 크기를 256의 배수가 되게끔 맞춘 크기이다. 이런 값을 편하게 구하기 위한 편의용 함수 d3dUtil::CalcConstantBufferByteSize가 존재한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static UINT CalcConstantBufferByteSize(UINT byteSize) {
    // Constant buffers must be a multiple of the minimum hardware
    // allocation size (usually 256 bytes).  So round up to nearest
    // multiple of 256.  We do this by adding 255 and then masking off
    // the lower 2 bytes which store all bits < 256.
    // Example: Suppose byteSize = 300.
    // (300 + 255) & ~255
    // 555 & ~255
    // 0x022B & ~0x00ff
    // 0x022B & 0xff00
    // 0x0200
    // 512
    return (byteSize + 255& ~255;
}
cs

 

그냥 상수 버퍼 구조체에 dummy data를 채워넣어 256바이트의 배수로 맞추는것도 하나의 방법임

 

셰이더 모형(shader model)

  • Direct3D 12가 5.1부터 도입한 기능
  • 상수 버퍼를 정의하는 또다른 HLSL 문법
1
2
3
4
5
6
7
8
9
10
11
12
cbuffer ObjectConstants : register(b0) {    
    float4x4 gWorldViewProj;    
    uint matIndex;
};
 
// ===============================
 
struct ObjectConstants {
    float4x4 gWorldViewProj;
    uint matIndex;
};
ConstantBuffer<ObjectConstants> gObjConstants : register(b0);
cs

위는 기존의 방식, 아래는 shader model을 이용한 방법이다.

상수 버퍼에 담을 자료의 형식을 개별적인 구조체로 정의하고, 그 구조체를 이용해서 상수 버퍼를 정의한다. 이후 셰이더 프로그램에서는 다음과 같이 자료 멤버 구문을 이용해서 상수 버퍼의 필드들에 접근한다.

uint index = gObjConstants.matIndex;

 

 

2. 상수 버퍼의 갱신(update)

앞에서 상수 버퍼를 upload heap(D3D12_HEAP_TYPE_UPLOAD 형식의 heap)에 생성했으므로, CPU에서 constant buffer자원에 자료를 올릴 수 있다(CPU에서 update 가능). 절차는 아래와 같다.

  1. Map 메서드를 이용하여 상수 버퍼의 subresource 주소를 얻는다.
  2. memcpy를 이용하여 subresource의 자원을 BYTE* type의 변수에 copy한다.

 

자료를 올리기 위해 자원 자료를 가리키는 포인터를 얻어야 하는데, 그러려면 다음과 같이 Map 메서드를 호출해야 한다.

1
2
3
4
5
6
7
8
9
10
ComPtr<ID3D12Resource> mUploadBuffer;
BYTE* mMappedData = nullptr;
mUploadBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedData));
 
memcpy(mMappedData, &data, dataSizeInBytes);
 
if(mUploadBuffer != nullptr)
    mUploadBuffer->Unmap(0, nullptr);
 
mMappedData = nullptr;
cs

 

1
2
3
4
5
HRESULT Map(
  UINT              Subresource,
  const D3D12_RANGE *pReadRange,
  void              **ppData
);
cs

Map 메서드

resource내에 있는 특정한 subresource의 CPU pointer를 얻어오지만, pointer 값을 application에 공개하지 않을 수 있다.

맵은 또한 필요할 때 CPU 캐시를 무효화하므로 이 주소에 대한 CPU 읽기가 GPU에 의해 수정된 내용을 반영합니다.

 

ComPtr의 Get 메서드는 buffer의 주소를 얻는데 쓰이며 Map 메서드로 얻은 주소는 buffer의 저장공간 주소이다.

 

매개변수

1. UINT Subresource;

  • subresource의 index 값을 명시한다.
  • 버퍼의 경우에는 버퍼 자체가 유일한 subresource이므로 그냥 0을 지정한다.

 

2. const D3D12_RANGE *pReadRange;

  • 접근하기위한 메모리의 범위를 서술하는 D3D12_RANGE 구조체의 포인터
  • 자원 전체를 대응시키려면 null pointer릴 지정

 

3. void **ppData;

  • 출력 매개변수
  • 대응된 자료를 가리키는 포인터가 설정됨

 

memcpy를 통해 시스템 메모리에 있는 자료를 상수버퍼에 복사할 수 있다.

memcpy(mMappedData, &data, dataSizeInBytes);

즉, constant buffer에 저장된 subresource에 자료를 저장하기 위해선 Map 메서드를 통해 address를 얻은 후, memcpy 함수를 사용하여 BYTE* 형 변수의 값을 subresource에 저장한다.

 

 

상수 버퍼에 자료를 다 복사했으면, 해당 메모리를 해제하기 전에 Unmap을 호출해 주어야 함

1
2
3
4
if(mUploadBuffer != nullptr)
    mUploadBuffer->Unmap(0, nullptr);
 
mMappedData = nullptr;
cs

 

1
2
3
4
void Unmap(
  UINT              Subresource,
  const D3D12_RANGE *pWrittenRange
);
cs

Unmap

  • 리소스의 지정된 하위 리소스에 대한 CPU 포인터를 무효화합니다.
  • Unmap은 또한 필요할 때 CPU 캐시를 flush하여 이 주소로 향하는 GPU가 CPU에 의해 수정된 내용을 반영하도록 한다.

 

매개변수

1. UINT Subresource;

  • 해제할 subresource의 index를 명시한다.
  • buffer의 경우 0을 전달한다.

 

2. const D3D12_RANGE *pWrittenRange;

  • 대응을 해제할 메모리 범위를 서술하는 D3D12_RANGE 구조체의 포인터
  • nullptr : 자원 전체의 대응을 해제

 

3. 업로드 버퍼 보조 클래스

예제 프레임워크의 UploadBuffer.h에는 업로드 버퍼를 손쉽게 다룰 수 있는 UploadBuffer 클래스가 정의되어 있다.

이 클래스는 업로드 버퍼 자원의 생성 및 파괴와 자원의 메모리 대응 및 해제를 처리 해주며, 버퍼의 특정 항목을 갱신하는 CopyData 메서드를 제공한다.

이 책의 예제들은 CPU에서 업로드 버퍼의 내용을 변경해야 할 때(가령 시야 행렬이 변했을 때) 이 CopyData 메서드를 사용한다

이 클래스는 constant buffer뿐만 아니라 그 어떤 upload buffer에도 사용 가능하다.

다만, constant buffer를 생성할 경우 isConstantBuffer argument에 true를 설정하여 버퍼의 크기가 256 byte의 배수가 되게끔 해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#pragma once
 
#include "d3dUtil.h"
 
template<typename T>
class UploadBuffer {
public:
    UploadBuffer(ID3D12Device* device, UINT elementCount, bool isConstantBuffer) : 
        mIsConstantBuffer(isConstantBuffer)
    {
        mElementByteSize = sizeof(T);
 
        // Constant buffer elements need to be multiples of 256 bytes.
        // This is because the hardware can only view constant data 
        // at m*256 byte offsets and of n*256 byte lengths. 
        // typedef struct D3D12_CONSTANT_BUFFER_VIEW_DESC {
        // UINT64 OffsetInBytes; // multiple of 256
        // UINT   SizeInBytes;   // multiple of 256
        // } D3D12_CONSTANT_BUFFER_VIEW_DESC;
        if(isConstantBuffer)
            mElementByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(T));
 
        ThrowIfFailed(device->CreateCommittedResource(
            &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
            D3D12_HEAP_FLAG_NONE,
            &CD3DX12_RESOURCE_DESC::Buffer(mElementByteSize*elementCount),
            D3D12_RESOURCE_STATE_GENERIC_READ,
            nullptr,
            IID_PPV_ARGS(&mUploadBuffer)));
 
        ThrowIfFailed(mUploadBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedData)));
 
        // We do not need to unmap until we are done with the resource.  However, we must not write to
        // the resource while it is in use by the GPU (so we must use synchronization techniques).
    }
 
    UploadBuffer(const UploadBuffer& rhs) = delete;
    UploadBuffer& operator=(const UploadBuffer& rhs) = delete;
    ~UploadBuffer()
    {
        if(mUploadBuffer != nullptr)
            mUploadBuffer->Unmap(0, nullptr);
 
        mMappedData = nullptr;
    }
 
    ID3D12Resource* Resource()const
    {
        return mUploadBuffer.Get();
    }
 
    void CopyData(int elementIndex, const T& data) {
        memcpy(&mMappedData[elementIndex*mElementByteSize], &data, sizeof(T));
    }
 
private:
    Microsoft::WRL::ComPtr<ID3D12Resource> mUploadBuffer;
    BYTE* mMappedData = nullptr;
 
    UINT mElementByteSize = 0;
    bool mIsConstantBuffer = false;
};
cs

 

상수 버퍼의 갱신 응용

이제 앞서배운 상수 버퍼의 갱신(update)기능을 응용해보자

일반적으로 물체의 세계 행렬은 장면 안에서 물제가 이동, 회전, 크기변화가 발생하면 바뀌며

물체의 시야 행렬은 카메라가 이동하거나 회전하면 바뀐다.

그리고 투영행렬은 창의 크기가 변하면 바뀐다.

이번장의 예제 프로그램에서 사용자는 마우스를 이용해서 카메라를 이동$\cdot$회전 할 수 있다. 이를 위해, 매 프레임 호출되는 Update 함수에서 세계-시야-투영 행렬(세계 행렬, 시야 행렬, 투영행렬을 결합한 행렬)을 새 시야 행렬로 갱신한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
void BoxApp::OnMouseMove(WPARAM btnState, int x, int y) {
    if((btnState & MK_LBUTTON) != 0) {
        // Make each pixel correspond to a quarter of a degree.
        // 마우스의 한 픽셀 이동을 4분의 1도에 대응시킨다.
        float dx = XMConvertToRadians(0.25f*static_cast<float>(x - mLastMousePos.x));
        float dy = XMConvertToRadians(0.25f*static_cast<float>(y - mLastMousePos.y));
 
        // Update angles based on input to orbit camera around box.
        // 입력에 기초해 각도를 갱신해서 카메라가 상자를 중심으로 공전하게 된다.
        mTheta -= dx;
        mPhi -= dy;
 
        // Restrict the angle mPhi.
        // mPhit 각도를 제한한다.
        mPhi = MathHelper::Clamp(mPhi, 0.1f, MathHelper::Pi - 0.1f);
    }
    else if((btnState & MK_RBUTTON) != 0)     {
        // Make each pixel correspond to 0.005 unit in the scene.
        // 마우스 한 픽셀 이동을 장면의 0.005 단위에 대응시킨다.
        float dx = 0.005f*static_cast<float>(x - mLastMousePos.x);
        float dy = 0.005f*static_cast<float>(y - mLastMousePos.y);
 
        // Update the camera radius based on input.
        // 입력에 기초해서 카메라 반지름을 갱신한다.
        mRadius += dx - dy;
 
        // Restrict the radius.
        // 반지름을 제한한다.
        mRadius = MathHelper::Clamp(mRadius, 3.0f, 15.0f);
    }
 
    mLastMousePos.x = x;
    mLastMousePos.y = y;
}
 
 
void BoxApp::Update(const GameTimer& gt) {
    // Convert Spherical to Cartesian coordinates.
    // 구면 좌표를 데카르트 좌표(직교 좌표)로 변환한다.
    float x = mRadius*sinf(mPhi)*cosf(mTheta);
    float z = mRadius*sinf(mPhi)*sinf(mTheta);
    float y = mRadius*cosf(mPhi);
 
    // Build the view matrix.
    // 시야 행렬을 구축한다.
    XMVECTOR pos = XMVectorSet(x, y, z, 1.0f);
    XMVECTOR target = XMVectorZero();
    XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
 
    // 위쪽방향이 up일때, pos에서 target을 바라보는 벡터를 구하기 위한 변환 행렬을 구한다.
    XMMATRIX view = XMMatrixLookAtLH(pos, target, up);
    XMStoreFloat4x4(&mView, view);
 
    XMMATRIX world = XMLoadFloat4x4(&mWorld);
    XMMATRIX proj = XMLoadFloat4x4(&mProj);
    XMMATRIX worldViewProj = world*view*proj;
 
    // Update the constant buffer with the latest worldViewProj matrix.
    // 최신의 worldViewProj 행렬로 상수 버퍼를 갱신한다.
    ObjectConstants objConstants;
    XMStoreFloat4x4(&objConstants.WorldViewProj, XMMatrixTranspose(worldViewProj));
    mObjectCB->CopyData(0, objConstants);
}
cs

주의할 점은 HLSL과 Direct Math의 행렬 방향이 같지 않다는 것이다. 이는 DirectX::XMMatrixTranspose 함수를 통해 행렬을 전치하는 것으로 해결 가능하다.

 

 

4. 상수 버퍼 서술자

  • constant buffer에 대해 서술하고 pipeline에 binding 되기 위한 descriptor
  • D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV 형식의 descriptor heap에 담긴다.
  • 이 heap은 셰이더 자원 뷰(SRV), 순서 없는 접근 뷰(UAV) 서술자들을 섞어서 담을 수 있다.

 

상수 버퍼 뷰 heap 생성

cbuffer descriptor heap를 만드는 예제코드는 아래와 같다.

1
2
3
4
5
6
7
8
9
D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc;
cbvHeapDesc.NumDescriptors = 1;
cbvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
cbvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
cbvHeapDesc.NodeMask = 0;
 
Comtr<ID3D12DescriptorHeap> mCbvHeap = nullptr;
md3dDevice->CreateDescriptorHeap(&cbvHeapDesc,
    IID_PPV_ARGS(&mCbvHeap));
cs

이번장의 예제 프로그램은 셰이더 자원이나 순서없는 접근 뷰를 사용하지 않으며 물체를 하나만 그리므로, 이 힙에는 상수 버퍼 뷰에 대한 서술자 하나만 담으면 된다.

 

상수 버퍼 뷰 생성

cbuffer view의 생성은 D3D12_CONSTANT_BUFFER_VIEW_DESC 인스턴스를 채운 후 ID3D12Device::CreateConstantBufferView를 호출해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 각 물체가 개별적으로 가지게 될 constant data
struct ObjectConstants {
    XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4();
};
 
std::unique_ptr<UploadBuffer<ObjectConstants>> mObjectCB = nullptr;
 
mObjectCB = std::make_unique<UploadBuffer<ObjectConstants>>(md3dDevice.Get(), 1true);
 
UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
 
// Offset to the ith object constant buffer in the buffer.
D3D12_GPU_VIRTUAL_ADDRESS cbAddress = mObjectCB->Resource()->GetGPUVirtualAddress();
int boxCBufIndex = 0;
cbAddress += boxCBufIndex*objCBByteSize;
 
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
cbvDesc.BufferLocation = cbAddress;
cbvDesc.SizeInBytes = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
 
md3dDevice->CreateConstantBufferView(
    &cbvDesc,
    mCbvHeap->GetCPUDescriptorHandleForHeapStart());
cs

D3D12_CONSTANT_BUFFER_VIEW_DESC는 cbuffer 자원 중 HLSL 상수 버퍼 구조체에 binding될 부분을 서술한다. 앞서 말했듯이 cbuffer에는 물체당 상수 자료 n개의 배열을 저장한다. BufferLocation과 SizeInBytes를 적절히 지정함으로써 i번째 물체의 상수 자료에 대한 뷰를 얻을 수 있다.

하드웨어의 제약에 따라 D3D12_CONSTANT_BUFFER_VIEW_DESC::BufferLocation과 D3D12_CONSTANT_BUFFER_VIEW_DESC::SizeInBytes 멤버는 반드시 256 바이트의 배수여야 한다.

 

 

5. 루트 서명과 서술자 테이블

루트 서명

  • ID3D12RootSignature로 대표됨
  • 정의 : 루트 매개변수(root parameter)들의 배열
    • root parameter : 주어진 그리기 호출에서 shader들이 기대하는 resource들을 description 한다. 
      • 예 : root constant, root descriptor, descriptor table 등...
    • 루트 서명(root signature)은 그리기 호출 전에 응용 프로그램이 반드시 rendering pipeline에 binding해야 하는 자원들이 무엇이고 그 자원들이 셰이더 입력 레지스터들에 어떻게 대응되는지를 정의한다.(input layout과 다른점은?)
  • root signature는 반드시 그리기 호출에 쓰이는 shader들과 호환되어야 한다.
    • 즉, root signature는 그리기 호출 전에 rendering pipeline에 binding 되었다고 shader들이 기대하는 모든 resource을 제공해야한다.
  • root signature의 유효성은 파이프 라인 상태 객체를 생성할 때 검증된다.

shader 프로그램들은 특정 종류의 resource들이 렌더링 pipeline에 binding된 상태에서 그리기 호출이 실행되었다고 기대한다. 자원들은 특정 레지스터 슬롯에 묶이며, 셰이더 프로그램들은 그 슬롯들을 통해서 자원들에 접근한다. 가령 이전의 정점 셰이더와 픽셀 셰이더는 cbuffer 하나가 레지스터 b0에 묶여있다고 기대한다. 이 책의 이후 예제들에 나오는 좀 더 복잡한 정점 셰이더들과 픽셀 셰이더들은 여러 개의 상수 버퍼들과 텍스처들, 그리고 표본추출기(sampler)들이 다양한 레지스터 슬롯들에 묶여 있다고 기대한다.

다음은 pipeline에 binding된 다양한 resource들의 예이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 텍스처 레지스터 슬롯 0에 binding된 텍스처 resource
Texture2D gDiffuseMap : register(t0);
 
// 표본추출기 레지스터 슬롯 0~5에 묶인 표본추출기 자원들
SamplerState gsamPointWrap            : register(s0);
SamplerState gsamPointClamp           : register(s1);
SamplerState gsamLinearWrap           : register(s2);
SamplerState gsamLinearClamp         : register(s3);
SamplerState gsamAnisotropicWrap     : register(s4);
SamplerState gsamAnisotropicClamp     : register(s5);
 
// 상수 버퍼 레지스터 슬롯 0~2에 묶인 cbuffer 자원
cbuffer cbPerObject : register(b0) {
    float4x4 gWorld;
    float4x4 gTexTransform;
};
 
// 재질마다 달라지는 상수 자료
cbuffer cbPass : register(b1) {
    float4x4 gView;
    float4x4 gProj;
    [...]    // 추가 코드들
};
 
cbuffer cbMaterial : register(b2) {
    float4        gDiffuseAlbedo;
    float3        gFresnelR0;
    float         gRoughness;
   float4x4     gMatTransform;
};
cs
shader 프로그램은 본질적으로 하나의 함수이며, shader에 입력되는 자원들은 함수의 매개변수들에 해당하므로, root signatrue는 곧 함수의 서명 즉, 함수의 매개변수를 정의하는 수단이라 할 수 있다.

매개변수에 어떤 자원을 인수로 묶느냐에 따라 shader의 출력이 달라진다.
가령 vertex shader의 출력은 shader에 입력되는 실제 vertex들에 의존하며, 따라서 해당 pipeline 단계에 묶인 자원들에 의존한다.

 

서술자 테이블

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
ComPtr<ID3D12RootSignature> mRootSignature = nullptr;
 
// Shader programs typically require resources as input (constant buffers,
// textures, samplers).  The root signature defines the resources the shader
// programs expect.  If we think of the shader programs as a function, and
// the input resources as function parameters, then the root signature can be
// thought of as defining the function signature.  
 
// Root parameter can be a table, root descriptor or root constants.
CD3DX12_ROOT_PARAMETER slotRootParameter[1];
 
// Create a single descriptor table of CBVs.
// 하나의 CBV를 담는 descriptor table을 생성한다
CD3DX12_DESCRIPTOR_RANGE cbvTable;
cbvTable.Init(
    D3D12_DESCRIPTOR_RANGE_TYPE_CBV,
    1,  // 테이블의 서술자 개수
    0); // 이 루트 매개변수에 묶일 셰이더 인수들의 기준 레지스터 번호
 
// root parameter를 descriptor table로 초기화한다.
slotRootParameter[0].InitAsDescriptorTable(
    1,          // 구간(range) 갯수
    &cbvTable); // 구간들의 배열을 가리키는 포인터
 
// A root signature is an array of root parameters.
// 루트 서명은 루트 매개변수들의 배열이다.
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(1, slotRootParameter, 0, nullptr, 
    D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
 
// create a root signature with a single slot which points to a descriptor range consisting of a single constant buffer
// 상수 버퍼 하나로 구성된 서술가 구간을 가리키는
// 슬롯 하나로 이루어진 루트 서명을 생성한다.
ComPtr<ID3DBlob> serializedRootSig = nullptr;
ComPtr<ID3DBlob> errorBlob = nullptr;
HRESULT hr = D3D12SerializeRootSignature(
    &rootSigDesc, 
    D3D_ROOT_SIGNATURE_VERSION_1,
    serializedRootSig.GetAddressOf(), 
    errorBlob.GetAddressOf());
 
if(errorBlob != nullptr)
{
    ::OutputDebugStringA((char*)errorBlob->GetBufferPointer());
}
ThrowIfFailed(hr);
 
ThrowIfFailed(md3dDevice->CreateRootSignature(
    0,
    serializedRootSig->GetBufferPointer(),
    serializedRootSig->GetBufferSize(),
    IID_PPV_ARGS(&mRootSignature)));
cs

CD3DX12_ROOT_PARAMETER와 CD3DX12_DESCRIPTOR_RANGE는 다음장에서 좀 더 자세히 설명하겠다.

일단 지금은 다음 코드가 CBV 하나(상수 버퍼 레지스터 0에 binding되는, 즉 HLSL 코드의 register(b0)에 대응되는)를 담은 descriptor table을 기대하는 root parameter를 생성한다는 점만 이해하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
CD3DX12_ROOT_PARAMETER slotRootParameter[1];
 
// 하나의 CBV를 담는 descriptor table을 생성한다
CD3DX12_DESCRIPTOR_RANGE cbvTable;
cbvTable.Init(
    D3D12_DESCRIPTOR_RANGE_TYPE_CBV,
    1,  // 테이블의 서술자 개수
    0); // 이 루트 매개변수에 묶일 셰이더 인수들의 기준 레지스터 번호
 
// root parameter를 descriptor table로 초기화한다.
slotRootParameter[0].InitAsDescriptorTable(
    1,          // 구간(range) 갯수
    &cbvTable); // 구간들의 배열을 가리키는 포인터
cs

이번장의 root signature 예제는 아주 단순한편이다. 앞으로 점점 더 복잡한 root signature를 만나게 될것이다.

root signature는 응용 프로그램이 rendering pipeline에 binding할 resource를 정의하기만 한다. 즉, root signature가 실제로 resource들을 binding 하지는 않는다. command list를 통해서 root signature를 설정한 후에는 ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable을 호출해서 descriptor table을 pipeline에 binding 한다.

1
2
3
4
void SetGraphicsRootDescriptorTable(
  UINT                        RootParameterIndex,
  D3D12_GPU_DESCRIPTOR_HANDLE BaseDescriptor
);
cs

정의

descriptor table을 graphics root signature로 설정(set) 한다.

 

매개변수

1. UINT RootParameterIndex;

  • 설정하고자 하는 root signature의 index


2. D3D12_GPU_DESCRIPTOR_HANDLE BaseDescriptor;

  • 설정하고자 하는 descriptor table의 첫 descriptor에 해당하는 descriptor(heap에 있는)의 handle
  • 가령, 서술자 다섯개를 담는 descriptor table로 정의된 root signature의 경우, heap에서 base descriptor에 해당하는 descriptor와 그 다음의 네 서술자가 root signature의 descriptor table에 설정된다.

 

다음 코드는 root signature가 CBV  heap 을 command list에 set하고, pipeline에 binding할 resource들을 지정하는 descriptor table을 set한다.

1
2
3
4
5
6
7
8
9
mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
ID3D12DescriptorHeap* descriptorHeaps[] = { mCbvHeap.Get() };
mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps);
 
// 이번 그리기 호출에서 사용할 CBV의 오프셋
CD3DX12_GPU_DESCRIPTOR_HANDLE cbv(mCbvHeap->GetGPUDescriptorHandleForHeapStart());
cbv.Offset(cbvIndex, mCbvSrvUavDescriptorSize);
 
mCommandList->SetGraphicsRootDescriptorTable(0, cbv);
cs

 

 

  • 성능을 위해서는 root signature를 최대한 작게 만들고, 한 프레임을 rendering 하는 과정에서 root signature의 변경을 최소화 해야한다.
  • 응용 프로그램이 binding한 root signature의 구성물(descriptor table, root constant, root descriptor)가 그리기 /분재(dispatch) 호출들 사이에서 변할 때 마다, Direct3D 12 드라이버가 해당 내용물에 자동으로 버전 번호를 부여한다. 따라서 각각의 그리기/분배 호출은 고유한 루트 서명 상태들의 전체 집합을 받게 된다.
  • root signature를 바꾸면 기존의 모든 binding이 사라진다. 따라서 새 root signature이 기대하는 모든 resource를 pipeline에 다시 binding 해야 한다.

 

 

 

 

 

 

 

 

 

 

 

 

Comments