(DirectX : Basic) 4. Constant Buffer

Posted by : at

Category : DirectX



기존에 그린 삼각형을 이동, 색상변경해보자.

쉐이더 단계에서 사용할 상수를 넘기는 법을 배운다.

예를들어 Vertext 쉐이딩 단계에서 상수를 넣고싶다.
이럴경우 어떻게 상수를 넣어야할까?? 매개변수로 상수를 넣을 수 있을까?(물론없음)

그런데 왜 이렇게 힘들게 넣을까? 단순 변수 하나 넣는게 이렇게 힘들어서야??
GPU의 레지스터단에 변수를 추가하는 명령이기에 신중하게 해야한다.

// ...

VS_OUT VS_Main(VS_IN input)
{
    VS_OUT output = (VS_OUT)0;

    output.pos = float4(input.pos, 1.f);
    output.pos += ???;		// 여기를 변경할 수 있다면??
    output.color = input.color;
    output.color += ???;

    return output;
}

// ...

ConstantBuffer

우선 지난 장에서 쉐이딩 파이프라인을 RootSignature에서 관리하고
RootSignature를 통해서 쉐이딩 파이프라인 중 상수를 넣을 수 있다.

// cbuffer(constant buffer)를 사용할꺼고 register b0를 사용하겠습니다
// 이름은 offset0로 해주세요
cbuffer TEST_B0 : register(b0)
{
    float4 offset0;
};

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

// ...

// offset0, 1은 이렇게 쓰겠다.
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;
}
void RootSignature::Init(ComPtr<ID3D12Device> device) 
{ 
	// 우선은 두 개의 값을 넘긴다고 알려줘야한다. 
	CD3DX12_ROOT_PARAMETER param[2]; 
	param[0].InitAsConstantBufferView(0);  
	// 0번 -> b0 -> CBV(Constant Buffer View), CBV는 레지스터이름이 b로 시작함 

	param[1].InitAsConstantBufferView(1); // 1번 -> b1 -> CBV 

	D3D12_ROOT_SIGNATURE_DESC sigDesc = CD3DX12_ROOT_SIGNATURE_DESC(2, param);

	// ...
void CommandQueue::RenderBegin(const D3D12_VIEWPORT* vp, const D3D12_RECT* rect) 
{ 
	// ... 
	// RootSignature가 사용되게 해달라고 전달 
	_cmdList->SetGraphicsRootSignature(ROOT_SIGNATURE.Get());

여기까지하면 RootSignature를 통해서 상수를 넘길준비까지 되었다.

이제 직접 상수를 넣는 부분이 필요한데

  1. 데이터를 담을 GPU 메모리를 할당한다.
  2. CPU의 데이터를 GPU 메모리에 데이터를 담는다
  3. GPU 레지스터가 GPU 메모리에서 읽어온다
  4. 읽어온 데이터를 쉐이딩 단계에서 사용한다.

데이터를 담을 GPU 메모리 할당

// ConstantBuffer라는 클래스에서 담당
void ConstantBuffer::Init(uint32 size, uint32 count) 
{ 
	// 상수 버퍼는 256 바이트 배수로 만들어야 한다 
	// 0 256 512 768 
	_elementSize = (size + 255) & ~255; 
	// & ~255 하위 8bits 정보를 모두 0으로 만들기에 256의 배수가 된다.
	_elementCount = count; 
	CreateBuffer(); 
}
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)); 
	// 여기까지하면 _mappedBuffer를 통해 GPU 메모리에 데이터를 넣을 수 있다
	// 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). 

	// 여기서 질문? CPU가 데이터를 GPU에 쓸때
	// GPU가 데이터를 읽어가 버리면???
	// 절대 그럴일이없음 -> fence로 현재 GPU작업이 다 끝나면 다음작업을 CPU에서 하도록 강제하게 만들어둠.
}

GPU 메모리에 데이터를 담는다

GPU 레지스터가 GPU 메모리에서 읽어와 쉐이딩 단계에서 사용한다.

void Mesh::Render() 
{ 
	// ... 

	// 1) Buffer에다가 데이터 세팅 
	// 2) Buffer의 주소를 register에다가 전송 
	GEngine->GetCB()->PushData(0, &_transform, sizeof(_transform)); 
	GEngine->GetCB()->PushData(1, &_transform, sizeof(_transform)); 

        // ...
}
void ConstantBuffer::PushData(int32 rootParamIndex, void* buffer, uint32 size) 
{ 
	assert(_currentIndex < _elementSize);

	// 2. GPU 메모리에 데이터를 담는다
	::memcpy(&_mappedBuffer[_currentIndex * _elementSize], buffer, size); 
	D3D12_GPU_VIRTUAL_ADDRESS address = GetGpuVirtualAddress(_currentIndex);

	// 3. GPU 레지스터가 GPU 메모리에서 읽어와 쉐이딩 단계에서 사용한다. 
	// 커멘드 리스트에 등록해 GPU Register가 읽어가도록 명령
	CMD_LIST->SetGraphicsRootConstantBufferView(rootParamIndex, address); 
	_currentIndex++; 
}

주의할 점

구현중 이해가 되지 않는 부분이 있을 것이다.
메모리 공간할당의 부분인데 사용될 메모리보다 GPU공간을 더 많이 할당한다.
이런 할당을 하는 이유는 CPU->GPU 메모리 복사는 즉시 일어나고
레지스터가 GPU메모리를 읽어가는 부분은 큐에 의해 동작하기에 즉시 동작하지 않는다

꼭 레지스터에 데이터를 쓸때의 문제는 아니고, 커멘드리스트(cmdlist)를 쓸때 공통적으로 발생하는 주의사항이다.

void ConstantBuffer::PushData(int32 rootParamIndex, void* buffer, uint32 size) 
{ 
	assert(_currentIndex < _elementSize);
	// 메모리의 복사는 즉시 일어나는 부분이고 
	::memcpy(&_mappedBuffer[_currentIndex * _elementSize], buffer, size); 
	D3D12_GPU_VIRTUAL_ADDRESS address = GetGpuVirtualAddress(_currentIndex);


	// 레지스터에 주소값을 전달하는건 queue에 의해 나중에 일어나는 부분
	CMD_LIST->SetGraphicsRootConstantBufferView(rootParamIndex, address); 
	_currentIndex++; 
}

그럼 복사는 일어났는데 queue에 명령이 쌓여 내가 원하는 시점의 데이터를 못 가져오는 경우가 시차때문에 발생할수 있지 않을까?
버퍼를 여러개만들어서 이 문제를 해결해야한다.

void ConstantBuffer::CreateBuffer()  
{  
	uint32 bufferSize = _elementSize * _elementCount; 
	// 버퍼사이즈를 사용하는 레지스터 count만큼 잡음 
	D3D12_HEAP_PROPERTIES heapProperty = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);  
	D3D12_RESOURCE_DESC desc = CD3DX12_RESOURCE_DESC::Buffer(bufferSize);

About Taehyung Kim

안녕하세요? 8년차 현업 C++ 개발자 김태형이라고 합니다. 😁 C/C++을 사랑하며 다양한 사람과의 협업을 즐깁니다. ☕ 꾸준한 자기개발을 미덕이라 생각하며 노력중이며, 제가 얻은 지식을 홈페이지에 정리 중입니다. 좀 더 상세한 제 이력서 혹은 Private 프로젝트 접근 권한을 원하신다면 메일주세요. 😎

Star
Useful Links