[D3D] RawBuffer

vector·2025년 12월 4일

충돌과 피킹과 같은 중요한 내용을 다루기 전에 기본기를 다뤄보는 시간

컴퓨팅쉐이더(Compute Shader)

기초 설명

컴퓨팅쉐이더를 통해 CPU가 아닌 GPU한테 일을 넘기는 방식에 대해 알아보자.

그동안 DirectX11 작업을 했던 코드 중에서 CPU와 GPU가 각각 처리됬던 부분들을 보면

  • CPU에 의해 처리되고 있던 코드 : 메인함수쪽 코드 (Client)
  • GPU에 의해 처리되고 있던 코드 : DirectX에 의해서 처리되었던 코드들 (Engine)
    그런 결과물이 렌더링 파이프라인을 타고 GPU를 통해 렌더링 되고 있었다.

최종적으로 병렬로 처리할 수 있는 단순 연산작업을 CPU가 아닌 GPU에서 처리해주면 좋은데 이렇게 범용적인 용도로 GPU를 사용하는 것을 GPGPU(General-Purpose GPU)라고 한다

DirectX에서도 Compote Shader를 사용해서 일반적인 쉐이더가 아닌 GPU에 단순 연산을 시키는 방식으로 이용할 수 있다.
이 방식으로 텍스처를 조작하거나, 영상처리, 합성, 하나의 스트림을 압축하거나, 순간적인 상태의 애니메이션 위치를 알고 싶을때 등 이러한 경우에 사용된다.

작업하기에 앞서 GPU한테 우리가 어떤 식으로 일을 분배할 것인지가 핵심적인 부분이다.
먼저 GPU에 데이터를 넘겼다 다시 받기 위해서는 리소스가 필요하다.
그 중 Raw Buffer(Byte Address Buffer)를 사용하자.
리소스를 만들어주고, 이를 묘사해주는 View를 만들어주는 선작업이 필요하다.

Raw Buffer (Byte Address Buffer)

  • 타입 정보가 없는 "바이트 주소로 직접 접근하는" Raw 메모리 버퍼
  • HLSL에서는 보통 ByteAddressBuffer, RWByteAddressBuffer로 사용
  • StructuredBuffer<float4>처럼 "T 단위 배열"이 아니라, "그냥 메모리 덩어리"를 GPU쪽에서 직접 주소 계산해서 쓰는 느낌이다.
  • 특징
    - 타입 정보가 없다(Untyped / Raw)
    • 접근 단위 : 4바이트(32비트) 단위
    • Load / Store / Interlocked

실습

  • RawBuffer클래스 생성 - 입력을 받아주고, 컴퓨팅 쉐이더에 연산을 요청한 다음 연산의 결과값을 받아주는 클래스 (인풋과 아웃풋을 동시에 담당)

RawBuffer

  • 우선 CPU에서 받아와서 저장해둘 변수 생성
    void* _inputData : CPU에서 받아오는 정보값의 데이터형을 모르기때문에 void*포인터로 생성
  • Input, Output의 버퍼와 View 생성
  • cpu데이터를 가져오고, 결과값을 가져오는 함수 생성

RawBuffer.h

#pragma once
class RawBuffer
{
public:
	RawBuffer(void* inputData, uint32 inputByte, uint32 outputByte);
	~RawBuffer();

public:
	// 모든 Create함수를 호출해주는 함수
	void CreateBuffer();

	// CPU쪽에서 데이터 가져오는 함수
	void CopyToInput(void* data);
	// 결과값을 가져오는 함수
	void CoptFromOutput(void* data);
    
public:
	ComPtr<ID3D11ShaderResourceView> GetSRV() { return _srv; }
	ComPtr<ID3D11UnorderedAccessView> GetUAV() { return _uav; }

	// 버퍼와 view를 만들어주는 함수
private:
	void CreateInput();
	void CreateSRV();
	void CreateOutput();
	void CreateUAV();
	void CreateResult();


private:
	// GPU에 넘겨주는 버퍼
	ComPtr<ID3D11Buffer> _input;
	// input을 묘사하는 View
	ComPtr<ID3D11ShaderResourceView> _srv;
	// 결과값을 받아주는 버퍼
	ComPtr<ID3D11Buffer> _output;
	// output을 묘사하는 View
	ComPtr<ID3D11UnorderedAccessView> _uav;

	// 최종 결과를 취합하기 위한 버퍼
	ComPtr<ID3D11Buffer> _result;


private:
	// * 어떤 데이터가 들어올지 예측하지 못하기 때문에 void형 포인트로 받아주기
	// * _inputData = CPU 메모리에 있는 정보들
	void* _inputData; 

	uint32 _inputByte = 0; // inputByte의 총 개수
	uint32 _outputByte = 0; // 출력해야 되는 데이터의 크기
};

RawBuffer.cpp

#include "pch.h"
#include "RawBuffer.h"

RawBuffer::RawBuffer(void* inputData, uint32 inputByte, uint32 outputByte)
	: _inputData(inputData), _inputByte(inputByte), _outputByte(outputByte)
{
	// 주소값으로 받아온 데이터와, 데이터 크기를 받아와서
	// 버퍼로 만들어서 GPU에다가 우리가 넘길 수 있는 리소스를 만들어 주는 방식
	CreateBuffer();
}

RawBuffer::~RawBuffer()
{
}

void RawBuffer::CreateBuffer()
{
	CreateInput();
	CreateSRV();
	CreateOutput();
	CreateUAV();
	CreateResult();
}

void RawBuffer::CopyToInput(void* data)
{
	D3D11_MAPPED_SUBRESOURCE subResource;
	DC->Map(_input.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &subResource);
	{
		memcpy(subResource.pData, data, _inputByte);//data를 subResource.pData에 넣고
	}
	DC->Unmap(_input.Get(), 0); // cpu메모리를 gpu에 복사
}

void RawBuffer::CoptFromOutput(void* data)
{
	// 출력 데이터 -> result에 복사
	DC->CopyResource(_result.Get(), _output.Get());

	D3D11_MAPPED_SUBRESOURCE subResource;
	DC->Map(_result.Get(), 0, D3D11_MAP_READ, 0, &subResource);
	{
		memcpy(subResource.pData, data, _outputByte);
	}
	DC->Unmap(_result.Get(), 0);
}

void RawBuffer::CreateInput()
{
	if (_inputByte == 0)
		return;

	D3D11_BUFFER_DESC desc;
	ZeroMemory(&desc, sizeof(desc));
	desc.ByteWidth = _inputByte;
	desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
	desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS;	// RAW_BUFFER
	desc.Usage = D3D11_USAGE_DYNAMIC; // CPU-WRITE, GPU-READ
	desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;

	// CreateBuffer() 함수를 이용해서 버퍼 만들기
	D3D11_SUBRESOURCE_DATA subResource = { 0 };
	subResource.pSysMem = _inputData;

	if (_inputData != nullptr)
		CHECK(DEVICE->CreateBuffer(&desc, &subResource, _input.GetAddressOf()));
	else
		CHECK(DEVICE->CreateBuffer(&desc, nullptr, _input.GetAddressOf())); // 혹시 모를 예외처리
}

void RawBuffer::CreateSRV()
{
	if (_inputByte == 0)
		return;

	D3D11_BUFFER_DESC desc;
	_input->GetDesc(&desc);

	D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
	ZeroMemory(&srvDesc, sizeof(srvDesc));
	srvDesc.Format = DXGI_FORMAT_R32_TYPELESS; // 쉐이더에서 알아서
	srvDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFEREX; // SRV_FLAG_RAW
	srvDesc.BufferEx.Flags = D3D11_BUFFEREX_SRV_FLAG_RAW;
	srvDesc.BufferEx.NumElements = desc.ByteWidth / 4;	// 전체 데이터 개수


	CHECK(DEVICE->CreateShaderResourceView(_input.Get(), &srvDesc, _srv.GetAddressOf()));
}

void RawBuffer::CreateOutput()
{
	D3D11_BUFFER_DESC desc;
	ZeroMemory(&desc, sizeof(desc));
	desc.ByteWidth = _outputByte;
	desc.BindFlags = D3D11_BIND_UNORDERED_ACCESS;
	desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS;	// RAW_BUFFER

	CHECK(DEVICE->CreateBuffer(&desc, NULL, _output.GetAddressOf()));
}

void RawBuffer::CreateUAV()
{
	D3D11_BUFFER_DESC desc;
	_output->GetDesc(&desc);

	D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc;
	ZeroMemory(&uavDesc, sizeof(uavDesc));
	uavDesc.Format = DXGI_FORMAT_R32_TYPELESS;
	uavDesc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER;
	uavDesc.Buffer.Flags = D3D11_BUFFER_UAV_FLAG_RAW;
	uavDesc.Buffer.NumElements = desc.ByteWidth / 4;


	CHECK(DEVICE->CreateUnorderedAccessView(_output.Get(), &uavDesc, _uav.GetAddressOf()));
}

void RawBuffer::CreateResult()
{
	D3D11_BUFFER_DESC desc;
	_output->GetDesc(&desc);

	desc.Usage = D3D11_USAGE_STAGING;
	desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
	desc.BindFlags = D3D11_USAGE_DEFAULT; // UAV가 연결되려면, USAGE는 DEFAULT여야 한다.
	desc.MiscFlags = 0;

	CHECK(DEVICE->CreateBuffer(&desc, nullptr, _result.GetAddressOf()));
}

24.RawBufferDemo.fx

RWByteAddressBuffer Output; // UAV

struct ComputeInput
{
    uint3 groupID : SV_GroupID;
    uint3 groupThreadID : SV_GroupThreadID;
    uint3 dispatchThreadID : SV_DispatchThreadID;
    uint groupIndex : SV_GroupIndex;
};

[numthreads(10, 8, 3)] // 쓰레드의 개수를 의미 ( 10 x 8 x 3 = 240개)
void CS(ComputeInput input)
{
    uint index = input.groupIndex;
    uint outAddress = index * 10 * 4; // uint3 3개, uint 1개 해서 10개 * 4
    
    Output.Store3(outAddress + 0, input.groupID);
    Output.Store3(outAddress + 12, input.groupThreadID);
    Output.Store3(outAddress + 24, input.dispatchThreadID);
    Output.Store(outAddress + 36, input.groupIndex);

}

technique11 T0
{
    pass P0
    {
        SetVertexShader(NULL);
        SetPixelShader(NULL);
        SetComputeShader(CompileShader(cs_5_0, CS()));
    }
};

RawBufferDemo

RawBufferDemo.h

  • 잘 작동하는지 확인하기 위해 Output으로 받을 구조체과, 쓰래드 개수와 결과값을 받기위해 UAV 설정해주고, csv파일로 결과 확인하기
#pragma once
#include "IExecute.h"

class RawBufferDemo : public IExecute
{
	struct Output
	{
		uint32 groupID[3];
		uint32 groupThreadID[3];
		uint32 dispatchThreadID[3];
		uint32 groupIndex;
	};

public:
	void Init() override;
	void Update() override;
	void Render() override;


private:
	shared_ptr<Shader> _shader;
};

RawBufferDemo.cpp

#include "pch.h"
#include "RawBuffer.h"

RawBuffer::RawBuffer(void* inputData, uint32 inputByte, uint32 outputByte)
	: _inputData(inputData), _inputByte(inputByte), _outputByte(outputByte)
{
	// 주소값으로 받아온 데이터와, 데이터 크기를 받아와서
	// 버퍼로 만들어서 GPU에다가 우리가 넘길 수 있는 리소스를 만들어 주는 방식
	CreateBuffer();
}

RawBuffer::~RawBuffer()
{
}

void RawBuffer::CreateBuffer()
{
	CreateInput();
	CreateSRV();
	CreateOutput();
	CreateUAV();
	CreateResult();
}

void RawBuffer::CopyToInput(void* data)
{
	D3D11_MAPPED_SUBRESOURCE subResource;
	DC->Map(_input.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &subResource);
	{
		memcpy(subResource.pData, data, _inputByte);//data를 subResource.pData에 넣고
	}
	DC->Unmap(_input.Get(), 0); // cpu메모리를 gpu에 복사
}

void RawBuffer::CopyFromOutput(void* data)
{
	// 출력 데이터 -> result에 복사
	DC->CopyResource(_result.Get(), _output.Get());

	D3D11_MAPPED_SUBRESOURCE subResource;
	DC->Map(_result.Get(), 0, D3D11_MAP_READ, 0, &subResource);
	{
		memcpy(data, subResource.pData, _outputByte);
	}
	DC->Unmap(_result.Get(), 0);
}

void RawBuffer::CreateInput()
{
	if (_inputByte == 0)
		return;

	D3D11_BUFFER_DESC desc;
	ZeroMemory(&desc, sizeof(desc));
	desc.ByteWidth = _inputByte;
	desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
	desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS;	// RAW_BUFFER
	desc.Usage = D3D11_USAGE_DYNAMIC; // CPU-WRITE, GPU-READ
	desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;

	// CreateBuffer() 함수를 이용해서 버퍼 만들기
	D3D11_SUBRESOURCE_DATA subResource = { 0 };
	subResource.pSysMem = _inputData;

	if (_inputData != nullptr)
		CHECK(DEVICE->CreateBuffer(&desc, &subResource, _input.GetAddressOf()));
	else
		CHECK(DEVICE->CreateBuffer(&desc, nullptr, _input.GetAddressOf())); // 혹시 모를 예외처리
}

void RawBuffer::CreateSRV()
{
	if (_inputByte == 0)
		return;

	D3D11_BUFFER_DESC desc;
	_input->GetDesc(&desc);

	D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
	ZeroMemory(&srvDesc, sizeof(srvDesc));
	srvDesc.Format = DXGI_FORMAT_R32_TYPELESS; // 쉐이더에서 알아서
	srvDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFEREX; // SRV_FLAG_RAW
	srvDesc.BufferEx.Flags = D3D11_BUFFEREX_SRV_FLAG_RAW;
	srvDesc.BufferEx.NumElements = desc.ByteWidth / 4;	// 전체 데이터 개수


	CHECK(DEVICE->CreateShaderResourceView(_input.Get(), &srvDesc, _srv.GetAddressOf()));
}

void RawBuffer::CreateOutput()
{
	D3D11_BUFFER_DESC desc;
	ZeroMemory(&desc, sizeof(desc));
	desc.ByteWidth = _outputByte;
	desc.BindFlags = D3D11_BIND_UNORDERED_ACCESS;
	desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS;	// RAW_BUFFER

	CHECK(DEVICE->CreateBuffer(&desc, NULL, _output.GetAddressOf()));
}

void RawBuffer::CreateUAV()
{
	D3D11_BUFFER_DESC desc;
	_output->GetDesc(&desc);

	D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc;
	ZeroMemory(&uavDesc, sizeof(uavDesc));
	uavDesc.Format = DXGI_FORMAT_R32_TYPELESS;
	uavDesc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER;
	uavDesc.Buffer.Flags = D3D11_BUFFER_UAV_FLAG_RAW;
	uavDesc.Buffer.NumElements = desc.ByteWidth / 4;


	CHECK(DEVICE->CreateUnorderedAccessView(_output.Get(), &uavDesc, _uav.GetAddressOf()));
}

void RawBuffer::CreateResult()
{
	D3D11_BUFFER_DESC desc;
	_output->GetDesc(&desc);

	desc.Usage = D3D11_USAGE_STAGING;
	desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
	desc.BindFlags = D3D11_USAGE_DEFAULT; // UAV가 연결되려면, USAGE는 DEFAULT여야 한다.
	desc.MiscFlags = 0;

	CHECK(DEVICE->CreateBuffer(&desc, nullptr, _result.GetAddressOf()));
}

결과

System Value

  • Dispath(5,3,2) : 그룹들을 묶어놓은 배열, 5x3의 배열이 2개가 있는 5x3x2개의 그룹
  • (10, 8, 3) : 한 그룹 내에 몇개의 쓰레드가 있는지, 10x8x3의 쓰레드
  • SV_GroupID : 몇 번째 그룹인지
  • SV_GroupThreadID : 하나의 그룹 내에서 몇 번째 쓰레드인지, 그룹 내에서는 고유한 값
  • SV_DispatchThreadID : 내가 몇번째 그룹인지, 그 그룹내에서 몇번째인지 전체를 나타내는 값
  • SV_GroupIndex : 1차원에서의 본인의 번호

실습2

Output만 가져오는것이 아닌 Input에 무엇을 넣어주고 가공해서 다시 결과값을 추출하는 방식으로 진행해보자

GroupDemo

  • 단순하게 float값을 넣어주고 다시 결과값을 받아오는 간단한 실습

GroupDemo.h

#pragma once
#include "IExecute.h"

class GroupDemo : public IExecute
{
	struct Input
	{
		float value;
	};

	struct Output
	{
		uint32 groupID[3];
		uint32 groupThreadID[3];
		uint32 dispatchThreadID[3];
		uint32 groupIndex;
		float value;
	};

public:
	void Init() override;
	void Update() override;
	void Render() override;


private:
	shared_ptr<Shader> _shader;
};

GroupDemo.cpp

#include "pch.h"
#include "GroupDemo.h"
#include "RawBuffer.h"

void GroupDemo::Init()
{
	_shader = make_shared<Shader>(L"25.GroupDemo.fx");

	// 하나의 쓰레드 그룹 내에서 운영할 쓰레드 개수
	uint32 threadCount = 10 * 8 * 3;
	uint32 groupCount = 2 * 1 * 1;
	uint32 count = groupCount * threadCount;

	// input에 랜덤한 숫자 추가
	vector<Input> inputs(count);
	for (int i = 0; i < count; i++)
	{
		inputs[i].value = rand() % 10000;
	}

	// rawBuffer에 input 데이터와 크기, output크기를 각각 넘겨주기
	shared_ptr<RawBuffer> rawBuffer = make_shared<RawBuffer>(inputs.data(), sizeof(Input) * count, sizeof(Output) * count);
	
	_shader->GetSRV("Input")->SetResource(rawBuffer->GetSRV().Get());
	_shader->GetUAV("Output")->SetUnorderedAccessView(rawBuffer->GetUAV().Get());

	// x, y, z : 쓰레드 그룹
	_shader->Dispatch(0, 0, 2, 1, 1);

	// 확인하기 위한 csv파일만들기
	vector<Output> outputs(count);
	rawBuffer->CopyFromOutput(outputs.data());

	FILE* file;
	::fopen_s(&file, "../RawBuffer.csv", "w");

	::fprintf
	(
		file,
		"GroupID(X),GroupID(Y),GroupID(Z),GroupThreadID(X),GroupThreadID(Y),GroupThreadID(Z),DispatchThreadID(X),DispatchThreadID(Y),DispatchThreadID(Z),GroupIndex,Value\n"
	);

	for (uint32 i = 0; i < count; i++)
	{
		const Output& temp = outputs[i];

		::fprintf
		(
			file,
			"%d,%d,%d,	%d,%d,%d,	%d,%d,%d,	%d, %f\n",
			temp.groupID[0], temp.groupID[1], temp.groupID[2],
			temp.groupThreadID[0], temp.groupThreadID[1], temp.groupThreadID[2],
			temp.dispatchThreadID[0], temp.dispatchThreadID[1], temp.dispatchThreadID[2],
			temp.groupIndex, temp.value
		);
	}

	::fclose(file);
}

void GroupDemo::Update() { }
void GroupDemo::Render() { }

25.GroupDemo.fx


ByteAddressBuffer Input;    // SRV
RWByteAddressBuffer Output; // UAV

struct ComputeInput
{
    uint3 groupID : SV_GroupID;
    uint3 groupThreadID : SV_GroupThreadID;
    uint3 dispatchThreadID : SV_DispatchThreadID;
    uint groupIndex : SV_GroupIndex;
};

[numthreads(10, 8, 3)] // 쓰레드의 개수를 의미 ( 10 x 8 x 3 = 240개)
void CS(ComputeInput input)
{
    // 그룹이 2 1 1 이니 임시로 x 값만 곱해주는 방식
    uint index = input.groupID.x * (10 * 8 * 3) + input.groupIndex;
    uint outAddress = index * 11 * 4; // uint3 3개, uint 1개, value값 1개 해서 11개 * 4
    
    uint inAddress = index * 4;
    float value = (float)Input.Load(inAddress);
    
    Output.Store3(outAddress + 0, input.groupID);
    Output.Store3(outAddress + 12, input.groupThreadID);
    Output.Store3(outAddress + 24, input.dispatchThreadID);
    Output.Store(outAddress + 36, input.groupIndex);
    Output.Store(outAddress + 40, (uint)value);

}

technique11 T0
{
    pass P0
    {
        SetVertexShader(NULL);
        SetPixelShader(NULL);
        SetComputeShader(CompileShader(cs_5_0, CS()));
    }
};

결과

현재는 ByteAddressBuffer를 통해서 offset 주소를 계산해서 정해지지 않은 임의의 형태의 데이터를 넣어줄 수 있지만, structured buffer를 사용하면 정해진 구조체를 통해 배여을 만들어 관리해줌으로 좀 더 쉽게 작업이 가능하다.

참고 사이트

https://learn.microsoft.com/en-us/windows/win32/direct3d11/direct3d-11-advanced-stages-cs-resources#byte-address-buffer

https://learn.microsoft.com/ko-kr/windows/win32/direct3dhlsl/sv-groupindex

참고 강의

https://www.inflearn.com/courses/lecture?courseId=329791&type=LECTURE&unitId=161162&tab=script&subtitleLanguage=ko

profile
게임 클라이언트 프로그래머 준비중 (공부 및 기록용)

0개의 댓글