충돌과 피킹과 같은 중요한 내용을 다루기 전에 기본기를 다뤄보는 시간
컴퓨팅쉐이더를 통해 CPU가 아닌 GPU한테 일을 넘기는 방식에 대해 알아보자.
그동안 DirectX11 작업을 했던 코드 중에서 CPU와 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클래스 생성 - 입력을 받아주고, 컴퓨팅 쉐이더에 연산을 요청한 다음 연산의 결과값을 받아주는 클래스 (인풋과 아웃풋을 동시에 담당)void* _inputData : CPU에서 받아오는 정보값의 데이터형을 모르기때문에 void*포인터로 생성#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; // 출력해야 되는 데이터의 크기
};
#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()));
}
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()));
}
};
#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;
};
#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()));
}


Dispath(5,3,2) : 그룹들을 묶어놓은 배열, 5x3의 배열이 2개가 있는 5x3x2개의 그룹(10, 8, 3) : 한 그룹 내에 몇개의 쓰레드가 있는지, 10x8x3의 쓰레드SV_GroupID : 몇 번째 그룹인지SV_GroupThreadID : 하나의 그룹 내에서 몇 번째 쓰레드인지, 그룹 내에서는 고유한 값SV_DispatchThreadID : 내가 몇번째 그룹인지, 그 그룹내에서 몇번째인지 전체를 나타내는 값SV_GroupIndex : 1차원에서의 본인의 번호Output만 가져오는것이 아닌 Input에 무엇을 넣어주고 가공해서 다시 결과값을 추출하는 방식으로 진행해보자
#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;
};
#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() { }
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/ko-kr/windows/win32/direct3dhlsl/sv-groupindex