DirectX/[Inflearn_rookiss] Part2: DirectX12

2. Constant Buffer & Root Signature

헛둘이 2023. 1. 15. 17:34
1. Constant Buffer
cbuffer TEST_B0 : register(b0)
{
    float4 offset0;
}

cbuffer TEST_B1 : register(b1)
{
    float offset1;
}

- cbuffer : 상수 버퍼를 선언하는 키워드

- TEST_B0 : 상수 버퍼 이름

- register(b0) : 상수 버퍼를 저장할 레지스터 공간을 지정 (b0은 GPU에 지정된 레지스터 공간)

- float offset0 : 상수 버퍼 내부에 정의된 변수 이름과 타입

 

상수 버퍼(Constant Buffer란?)

- GPU에서 프로그램 실행 도중 사용되는 상수값

- 이런 상수 값들은 쉐이더 프로그램에서 사용되며 프로그램 실행 중 동적으로 수정될 수 있다.

 

*상수 버퍼의 크기는 256의 배수로 만들어야 한다.

- 이유는 GPU 내부의 최적화 때문

- GPU 내부에선 데이터를 버스로 전송하는데 버스의 크기는 일반적으로 256byte이다.

- 256바이트의 배수로 만들면 필요한 버스의 대역폭을 줄일 수 있다.

 

void ConstantBuffer::Init(uint32 size, uint32 count)
{
	_elementSize = (size + 255) & ~255;
	_elementCount = count;

	CreateBuffer();
}

- elementSize = (size + 255) & ~255; 이 코드는 하위 8비트를 0으로 밀어주는 코드이다

- 255는 1111 1111이며, 255를 반전하면 0000 0000이고 이 값과 & (AND) 연산을 하게 되면 하위 8비트가 날라가게 된다.

- size + 255는 버퍼란 여유 공간이 필요하기 때문 (다음 256의 배수를 선택하기 위해)

- 256이 아니라 255를 더해주는 이유는 사이즈가 256이 들어왔을 때의 경우는 256을 택하기 위해 (511이 됨)

 

 


2. Root Signature

Root Table

API bind slot : 외부 코드에서 어떤 칸에 데이터를 넣어주겠다 라고 합의한 데이터

HLSL bind slot : GPU 레지스터의 이름 (상수 버퍼 용도로 사용할 애들은 b로 시작한다)

root constant : 어떤 용도로 사용할 것인지, root descriptor도 넣을 수 있다. 

 

*Root Signature를 서명하는 단계와 데이터를 그 곳에 이동시키는 단계는 아예 별개임

 

 

root CBV는 root descriptor인데 포인터마냥 다른 리소스를 가리키는 뷰의 개념

 

 

desc. table은 "정책"이라고 볼 수 있다.

1번 desc. table이 활성화가 되면 1번이 가리키는 애들이 활성화가 되고, 2번은 무시

2번 desc. table이 활성화가 되면 말 그대로 반대로 됨

 

한 마디로 Root Signature를 통해 어디를 사용하겠다! 라고 계약을 하고 데이터를 밀어 넣어야 함

 

근데 왜 저렇게 포인터처럼 만들어서 가리키게 할까?

그냥 포인터를 제거하고 포인터 자리에 때려박으면 안되나?

- 무한정으로 늘릴 수 없고 제한이 있음

- 4바이트 64개 제한

 

 

CD3DX12_ROOT_PARAMETER param[2];
param[0].InitAsConstantBufferView(0); // b0
param[1].InitAsConstantBufferView(1); // b1

들어가는 순서대로 슬롯에 들어가기 때문에 위 코드에 의하면 아래와 같이 매칭이 된다.

 

D3D12_ROOT_SIGNATURE_DESC sigDesc = CD3DX12_ROOT_SIGNATURE_DESC(2, param);
sigDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;
	

ComPtr<ID3DBlob> blobSignature;
ComPtr<ID3DBlob> blobError;

D3D12SerializeRootSignature(&sigDesc, D3D_ROOT_SIGNATURE_VERSION_1, &blobSignature, &blobError);
device->CreateRootSignature(0, blobSignature->GetBufferPointer(), blobSignature->GetBufferSize(),
IID_PPV_ARGS(&_signature));

- 그리고 그 값은 위 코드에서 적용 된다.

 

 

 


3. 정리, ConstantBuffer를 만드는 방법

 

 

1. 루트 테이블을 만들어서 몇번 인덱스에 어떤 내용, 어떤 레지스터를 사용할 것인지 전달한다.

Root Table

예제에서는 그냥 Init에서 0번과 1번에 ConstantBufferView를 사용하겠다고 정함

void RootSignature::Init(ComPtr<ID3D12Device> device)
{
	CD3DX12_ROOT_PARAMETER param[2];
	param[0].InitAsConstantBufferView(0); // b0
	param[1].InitAsConstantBufferView(1); // b1


	D3D12_ROOT_SIGNATURE_DESC sigDesc = CD3DX12_ROOT_SIGNATURE_DESC(2, param);
	sigDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;
	

	ComPtr<ID3DBlob> blobSignature;
	ComPtr<ID3DBlob> blobError;

	D3D12SerializeRootSignature(&sigDesc, D3D_ROOT_SIGNATURE_VERSION_1, &blobSignature, &blobError);
	device->CreateRootSignature(0, blobSignature->GetBufferPointer(), blobSignature->GetBufferSize(),
	IID_PPV_ARGS(&_signature));
}

 

2. ConstantBuffer를 생성하고 초기화

- 예제에서 ConstantBuffer는 버퍼 여러개를 소유한 덩어리 자체를 의미

- Init에서 버퍼의 사이즈와 개수를 인자로 받아서 소유한다.

- CreateBuffer에서 버퍼 사이즈 * 개수를 통해 할당받아야 할 메모리를 결정한 후 할당

- 접근할 때는 GetGpuVirtualAddress를 통해 접근 (인자로 인덱스를 받아서 몇 번 버퍼에 접근할 것인지를 전달)

void ConstantBuffer::CreateBuffer()
{
	uint32 bufferSize = _elementSize * _elementCount;

	D3D12_HEAP_PROPERTIES heapProperty = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
	D3D12_RESOURCE_DESC desc = CD3DX12_RESOURCE_DESC::Buffer(bufferSize);

	DEVICE->CreateCommittedResource(
		&heapProperty,
		D3D12_HEAP_FLAG_NONE,
		&desc,
		D3D12_RESOURCE_STATE_GENERIC_READ,
		nullptr,
		IID_PPV_ARGS(&_cbvBuffer));
	
	_cbvBuffer->Map(0, nullptr, reinterpret_cast<void**>(&_mappedBuffer));
}

 

3. Mesh의 Render에서 ConstantBuffer에 데이터(float4, 통상 위치정보)를 밀어넣는다.

void Mesh::Render()
{
	// 실제로는 그려줄 때는 이 뷰를 사용해서 그려주게 된다.
	CMD_LIST->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
	CMD_LIST->IASetVertexBuffers(0, 1, &_vertexBufferView);

	// 1. Buffer에 데이터 세팅
	// - GPU 메모리에 뚜껑을 열고 데이터를 집어넣는 부분이 Mesh의 Init에 비슷하게 구현되어 있음
	// 2. Buffer의 주소를 Register에 전송

	GEngine->GetCB()->PushData(0, &_transform, sizeof(_transform));
	GEngine->GetCB()->PushData(1, &_transform, sizeof(_transform));


	//CMD_LIST->SetGraphicsRootConstantBufferView(0, GEngine->GetCB()->GetGpuVirtualAddress(0));

	CMD_LIST->DrawInstanced(_vertexCount, 1, 0, 0);
}

 

4. CommandQueue의 RenderBegin에서 ConstantBuffer의 인덱스를 초기화해준다.

GEngine->GetCB()->Clear();

 

5. 쉐이더 코드에서 해당 값들을 참조하여 실제 삼각형의 정보에 반영한다.

cbuffer TEST_B0 : register(b0)
{
    float4 offset0;
}

cbuffer TEST_B1 : register(b1)
{
    float4 offset1;
}


struct VS_IN
{
    float3 pos : POSITION;
    float4 color : COLOR;
};

struct VS_OUT
{
    float4 pos : SV_Position;
    float4 color : COLOR;
};


VS_OUT VS_Main(VS_IN input)
{
    VS_OUT output = (VS_OUT)0;
    
    output.pos = float4(input.pos, 1.f);
    output.pos += offset0;
    output.color = input.color;
    output.color += offset1;
 
    return output;
}


float4 PS_Main(VS_OUT input) : SV_Target
{
    return input.color;
}

 

 

6. 결과