컴퓨트 쉐이더(CPU가 아닌 GPU에 일을 넘기는 방식)에 대해 알아볼 것이다.
지금까지 작업한 WinMain의 코드들은 CPU에서 돌아가는 코드들이었다.
Engine 프로젝트 쪽에 있는 DirectX로 처리되는 게 GPU에 의해 처리되는 것이다.
렌더링 파이프라인을 타서 GPU에서 이용되는 거였다.
GPU한테 일을 시키기 위해 Shader를 만들어서 이런저런 걸 기술해서 GPU에게 시킬 수 있었다.
역사적인 부분을 봐야 한다.
CPU는 코어 개수가 많지 않지만 개개인이 똑똑한 직원들이다
GPU는 몸값은 낮지만 숫자가 많다고 볼 수 있다.
GPU, CPU 양쪽을 바쁘게 하는 게 최종 목표다.
CPU는 할게 많으니 언제든지 찾을 수 있는 반면
GPU는 상대적으로 Rendering 부분 외에는 아직까지 사용하지 않으니까 일부 CPU에서 연산량이 많이 들어가지만 연산이 어렵지 않은 경우 그걸 GPU에게 넘겨서 시켜 보는 걸 생각할 수 있을 것이다.
이런 이유로 GPU가 발전할 때는 그래픽스를 위해 발전했지만 언제부턴가 코어를 낭비하는 게 아깝다는 생각이 들기 시작한다.
대표적으로 암호학, 가상화폐 채굴, AI 같은 경우도 CPU가 모두 하기보다는 GPU에 넘기면 이득을 볼 수 있다. 업무끼리 독립적인 경우 장점이 있다.
업무가 복잡하고 데이터가 얽혀 있다면 병렬로 시킬 수 없어서 CPU를 쓴다.
병렬로 처리하는 단순 작업을 GPU에게 외주로 하면 좋다.
GPGPU(General Purpose GPU)라고 하는데 그 기술들이 CUDA부터 시작해서 발전을 해와서 요즘에는 일반적인 연산에서도 GPU에서 하는 기법을 알아야 한다.
DX 쪽에서도 ComputeShader를 이용해서 GPU에 단순 연산 시킬 수 있는데
연산을 던저서 결과 얻어서 동작하고, 결과를 취합해서 뭔가를 할 수 있다.
대표적으로 컨텐츠나 클라 만들 때 텍스쳐 다루는 부분을 조작해서 다른 파일을 만들고 싶다거나, 영상처리나 합성에도 장점이 있을 거고, 하나의 스트림을 압축하는 경우, 애니메이션의 경우 보간작업하는 경우, 순간적인 상태의 애니메이션을 알고 싶을 때 등에 사용할 수 있다.
어떤 컨텐츠를 넘길지는 나중의 문제고 어떤 식으로 외주를 시키고 돌려받는지가 주제다.
CPU에 비해 작업방식은 복잡하다.
CPU는 일반적 코드를 적고, 공용 정보는 락을 잡고 멀티 스레드에서 작업을 할 수 있었지만 GPU에서 하는 건 더 복잡하다.
한번 연습해서 만들면 GPU를 사용할 수 있게 된다.
작업을 시작한다.
GPU에게 어떤 식으로 일을 분배할지가 핵심이다.
GPU에다가 데이터를 넘기고 다시 받기 위해서는 Reosource가 필요하다.
여러 방식 중 RowBuffer라는 걸 이용해서 만들어 보도록 한다.
raw buffer 혹은 byte address buffer라고도 불리는데
첫 번째로 테스트할 것은 Byte Address Buffer고 나중에 다른 방식도 알아볼 것인데 이 방식은 dx11에서 처음 생겼다고 한다.
주소를 이용해서 직접적으로 주소값을 연산해서 접근하는 형태라는 걸 알 수 있는데 C++의 기반으로 표현하면 일종의 byte형 포인터를 다루는 느낌이라고 생각할 수 있다.
raw buffer라는 건 날것의 것으로 어떻게 가공해서 사용할지는 우리가 정하면 된다고 볼 수 있다.
실습을 통해서 해보자.
첫 번째 버퍼 타입을 만들 것이다.
리소스를 만들 건데 지금까지 어떻게 했는지 보면
버퍼를 만들어 준 다음에 그것을 묘사하는 뷰라는 걸 만들어서 GPU와 통신할 때는 뷰라는 묘사한 정보를 건네주어서 작업을 했었다. 선수 작업이 필요하다
1. RawBuffer 클래스 만들기
Engine에 05. ComputeShader라는 필터를 임시로 만들고 어디에 위치시킬지는 나중에 고민을 해본다.
그 안에 Buffer 필터를 추가하고 그 안에 RawBuffer라는 클래스를 만든다.
이 아이를 통해 입력을 받아줄 것이고, 입력을 받아준 다음에 Compute Shader에다 연산을 요청하고, 그 연산이 취합이 되면 그 결과를 다시 받아주는 역할을 하게 된다.
input과 output을 동시에 담당을 한다.
리소스를 따로 만들어도 되지만 이런 식으로 리소스를 필요한 애들을 한 번에 묶어서 하나의 클래스로 파는 것도 편리하기 때문에 묶어서 관리하는 방식으로 만들어 본다.
버퍼를 3개 만들어 볼 것이다.
RawBuffer.h
#pragma once
class RawBuffer
{
public:
RawBuffer(void* inputData, uint32 inputByte, uint32 outputByte);
~RawBuffer();
public:
void CreateBuffer();
void CopyToInput(void* data); // CPU 쪽 메모리에서 데이터를 만들어줘서 그것을 건내주세요하면 _input Buffer에 저장이 되어서 건내주는 형태
void CopyFromOutput(void* data); // _result Buffer에 들어가 있는 것을 다시 긁어 오는 형태로 작업
private:
void CreateInput();
void CreateSRV();
void CreateOutput();
void CreateUAV();
void CreateResult();
private:
ComPtr<ID3D11Buffer> _input;
ComPtr<ID3D11ShaderResourceView> _srv;
ComPtr<ID3D11Buffer> _output;
ComPtr<ID3D11UnorderedAccessView> _uav;
ComPtr<ID3D11Buffer> _result;
private:
void* _inputData; // 입력받을 데이터
uint32 _inputByte = 0; // 크기
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)
{
CreateBuffer();
}
RawBuffer::~RawBuffer()
{
}
void RawBuffer::CreateBuffer()
{
CreateInput();
CreateSRV();
CreateOutput();
CreateUAV();
CreateResult();
}
//CPU 메모리에서 GPU로 복사하는 형태
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);
}
DC->Unmap(_input.Get(), 0);
}
// 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;
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;
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()));
}
Engine 프로젝트를 빌드 한다.
2. BufferDemo 클래스를 생성하고 세팅하기
Client로 가서 간단한 예제를 살펴본다.
Client/Game필터에서 SceneDemo클래스만 두고 나머지는 삭제한다.
그리고 SceneDemo 클래스를 복붙하고 이름을 RawBufferDemo라고 한다.
코드를 수정한다.
#pragma once
class RawBufferDemo : public IExecute
{
public:
void Init() override;
void Update() override;
void Render() override;
private:
shared_ptr<Shader> _shader;
private:
};
#include "pch.h"
#include "RawBufferDemo.h"
void RawBufferDemo::Init()
{
_shader = make_shared<Shader>(L"24. RawBufferDemo.fx");
}
void RawBufferDemo::Update()
{
}
void RawBufferDemo::Render()
{
}
Client/Shaders 필터에 Week4 필터를 만들어 준다.
23. RenderDemo.fx를 복붙 해서 이름을 24. RawBufferDemo.fx로 하고 Week4 필터에 넣는다.
Main으로 가서
#include "pch.h"
#include "Main.h"
#include "Engine/Game.h"
#include "SceneDemo.h"
#include "RawBufferDemo.h"
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
GameDesc desc;
desc.appName = L"GameCoding";
desc.hInstance = hInstance;
desc.vsync = false;
desc.hWnd = NULL;
desc.width = 800;
desc.height = 600;
desc.clearColor = Color(0.f, 0.f, 0.f, 0.f);
desc.app = make_shared<RawBufferDemo>(); // 실행 단위
GAME->Run(desc);
return 0;
}
이렇게 시작을 해본다.
Client를 빌드한다.
3. 24. RawBufferDemo.fx 쉐이더 만들기
먼저 24. RawBufferDemo.fx부터 작업을 한다.
// 연산을 하는 용도로 GPU를 사용하는 방식을 Compute Shader라고 보면 된다.
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)] // thread의 갯수 의미 240개 고용하겠다는 의미
void CS(ComputeInput input)
{
uint index = input.groupIndex;
uint outAddress = index * 10 * 4; // ComputeInput이 4바이트가 10개 있는 거랑 마찬가지다. 주소의 offset 계산
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()));
}
};
40바이트씩을 기입을 해서 자기의 정보를 건네줄 것인데 이 결과물이 최종적으로 완성이 되면 어떤 식으로 출력이 되는지를 로그를 찍어서 그걸 살펴보면서 분석을 해 볼 것이다.
그게 핵심이다. 이걸 왜 강조하는지를 살펴본다.
4. RawBufferDemo에서 Shader에 버퍼를 만들어서 UAV로 Shader로 보낸 뒤 나온 결과를 로그로 출력하기
RawBufferDemo.h에
struct Output
{
uint32 groupID[3];
uint32 groupThreadID[3];
uint32 dispatchThreadID[3];
uint32 groupIndex;
};
를 추가해 준다. 이게 24. RawBufferDemo.fx에서 넣어준
struct ComputeInput
{
uint3 groupID : SV_GroupID;
uint3 groupThreadID : SV_GroupThreadID;
uint3 dispatchThreadID : SV_DispatchThreadID;
uint groupIndex : SV_GroupIndex;
};
이걸 받아주기 위해 맞춰줬다.
output buffer에서는 출력을 얼마짜리인지 크기를 만들어 줘야 한다.
RawBufferDemo.cpp에 가서
void RawBufferDemo::Init()
{
_shader = make_shared<Shader>(L"24. RawBufferDemo.fx");
// 하나의 쓰레드 그룹 내에서 운형할 쓰레드 개수
uint32 count = 10 * 8 * 3; // MSN 공식 문서에 이렇게 되어 있어서
shared_ptr<RawBuffer> rawBuffer = make_shared<RawBuffer>(nullptr, 0, sizeof(Output) *count);
}
이렇게 데이터 크기를 집어주게 되면 이 데이터의 크기에 맞는 버퍼가 만들어질 것이고,
그걸 uav로 만들어 준 다음에
실질적으로 결과물을 받아오는 용도로 활용할 수 있게 된다.
RawBuffer.h로 가서
public:
ComPtr<ID3D11ShaderResourceView> GetSRV() { return _srv; }
ComPtr<ID3D11UnorderedAccessView> GetUAV() { return _uav; }
를 추가한다.
Engine 프로젝트를 빌드하고,
void RawBufferDemo::Init()
{
_shader = make_shared<Shader>(L"24. RawBufferDemo.fx");
// 하나의 쓰레드 그룹 내에서 운영할 쓰레드 개수
uint32 count = 10 * 8 * 3; // MSN 공식 문서에 이렇게 되어 있어서
shared_ptr<RawBuffer> rawBuffer = make_shared<RawBuffer>(nullptr, 0, sizeof(Output) *count);
_shader->GetUAV("Output")->SetUnorderedAccessView(rawBuffer->GetUAV().Get());
// 원래는 draw 계열 함수를 썼는데 얘는 그냥 CompeuteShader를 실행하세요가 Dispatch로 실행된다
// x, y, z 쓰레드 그룹 지정
_shader->Dispatch(0, 0, 1, 1, 1); // 수치들이 ComputInput 여기의 값들과 연관이 있다.
// 각각 어떤 값들을 리턴 했는지 로그를 찍어 본다.
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\\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\\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
);
}
::fclose(file);
}
실행을 하면
빈 화면이 보인다.
5 결과 분석하기
결과 sln 파일이 있는 폴더에 RawBuffer.csv라는 엑셀파일이 만들어져 있다.
여러 수치들이 나와 있는데 규칙성을 찾아본다.
얘기하고 있는 게 어떤 의미인지 분석을 해보면 된다.
엑셀의 수치들이 뭘 의미하는지 이해가 안 갈 수 있다.
코드를 실행할 때 스레드들을 열심히 고용해서 스레드들한테 일을 시키는 형태가 된다. 그러면 CPU에서는 스레드를 관리해서 만든 다음에 공용데이터를 이용하거나 여기서부터 여기까지 처리해 주세요라고 일감을 만들어서 떠넘기는 방식이다. GPU로 할 때는 워낙 스레드들을 많이 고용하다 보니까 그런 부분들을 우리가 일일이 지정하기보다는 어떤 규칙성을 갖고 하는 게 합리적이라고 생각할 수 있는데
struct ComputeInput
{
uint3 groupID : SV_GroupID;
uint3 groupThreadID : SV_GroupThreadID;
uint3 dispatchThreadID : SV_DispatchThreadID;
uint groupIndex : SV_GroupIndex;
};
이런 식으로 고유한 넘버링이 있다고 볼 수 있다.
이 넘버링이 렌덤 하게 선택되는 게 아니라
고용한 개수와 일관적인 규칙으로 넘버링이 붙는다는 걸 볼 수 있다.
그 정보를 이용해서 그룹 인덱스는 0부터 0,1,2,3,4,5,6,7,8 이렇게 순차적으로 증가한다고 볼 수 있는 거니까
하나의 스레드마다 실행되는 일감을 어떻게 구분하느냐, 고유한 정보들을 이용해 가지고 내가 어떤 정보를 가지고 조작을 하고, 내가 완료해서 가공한 정보를 몇 번째 칸에다가 넣어줘야 되는지를 같이 연산할 필요가 있다.
[numthreads(10, 8, 3)] // thread의 갯수 의미 240개 고용하겠다는 의미
void CS(ComputeInput input)
{
uint index = input.groupIndex;
uint outAddress = index * 10 * 4; // ComputeInput이 4바이트가 10개 있는 거랑 마찬가지다. 주소의 offset 계산
Output.Store3(outAddress + 0, input.groupID);
Output.Store3(outAddress + 12, input.groupThreadID);
Output.Store3(outAddress + 24, input.dispatchThreadID);
Output.Store(outAddress + 36, input.groupIndex);
}
서로 안 건드리고 작업할 수 있는 이유가 이런 식으로 작업해야 할 영역을 집어 가지고 그곳에다가 데이터를 넣어주기 때문에 깔끔하게 들어간다.
지금 중요한 건 240개의 스레드를 운영하고 있고,
uint32 count = 10 * 8 * 3;
스레드 그룹은
_shader->Dispatch(0, 0, 1, 1, 1);
1개만 운영하고 있다.
240개를 하나의 팀이라 할 수 있는데 그 팀을 한 개만 고용을 한 것이다.
Dispatch를 통해 실행을 하면 자기 자신이 몇 번째인지 알고 있기 때문에 모든 정보들을 원하는 칸에다가 넣어주었다고 보면 된다.
그 정보를 다시 로그로 찍어서 csv 파일로 만들어서 살펴보는 상황이다.
다음시간에 정보들이 무슨 의미인지, Dispatch에서 넣어주는 x, y, z와 count에서 넣어주는 10, 8, 3은 무엇을 의미하는지를 살펴본다.
'DirectX' 카테고리의 다른 글
75_TextureBuffer (0) | 2024.03.22 |
---|---|
74_System Value 분석 (0) | 2024.03.22 |
72_Quaternion (0) | 2024.03.20 |
71_인스턴싱_Scene 구조 정리 (0) | 2024.03.20 |
70_인스턴싱_인스턴싱 통합 (0) | 2024.03.20 |
댓글