오늘은 Compute Shader에 대해 복습하고 예제코드를 분석해보자.
Compute Shader에 대해 복습하기 전에 우선 CPU와 GPU의 차이에 대해 먼저 복습해보자
CPU는 연산의 주체가 되는 ALU가 적은대신 메모리 비중이 높다.
GPU는 연산의 주체가 되는 ALU가 많아서 단순한 연산에 강점을 가지고 코어가 많기 때문에 병렬로 처리하기에 적합하다.
Compute Shader는 이러한 GPU의 특징을 살려서 분할 정복과 같은 병렬성 알고리즘을 통해 단순하지마 연산양이 많은 데이터를 처리한다. 이를 통해 렌더링에는 관련없지만 연산이 필요한 경우에 Compute Shader를 사용할 수 있다.
Compute Shader는 Pipeline 상에는 없고 독립적으로 사용가능하다.
먼저 첫번째 예시 코드를 실행해보자. 실행해보면 result.txt 파일이 다음과 같이 생성된다.
이는 데이터 2개를 던져주고 GPU를 통해 덧셈연산을 해준 것이다.
코드상에서 살펴보자면 밑의 두개의 함수가 이 연산에 관여하고 있다.
BuildBuffersAndViews();
DoComputeWork();
그리고 SRV두개를 통해 입력을 받아온 다음에 넘겨주고 UAV를 통해 결과값을 받아온다.
ComPtr<ID3D11Buffer> _outputBuffer;
ComPtr<ID3D11Buffer> _outputDebugBuffer;
ComPtr<ID3D11ShaderResourceView> _inputASRV;
ComPtr<ID3D11ShaderResourceView> _inputBSRV;
ComPtr<ID3D11UnorderedAccessView> _outputUAV;
그리고 해당 값을 받아오기 위해서 Shader쪽에서는 StructuredBuffer 변수를 통해 받아주고 덧셈 연산을 한 결과를 결과
StructuredBuffer에 저장해주고 있다.
struct Data
{
float3 v1;
float2 v2;
};
StructuredBuffer<Data> gInputA;
StructuredBuffer<Data> gInputB;
//READ + Write
RWStructuredBuffer<Data> gOutput;
[numthreads(32, 1, 1)]
void CS(int3 dtid : SV_DispatchThreadID)
{
gOutput[dtid.x].v1 = gInputA[dtid.x].v1 + gInputB[dtid.x].v1;
gOutput[dtid.x].v2 = gInputA[dtid.x].v2 + gInputB[dtid.x].v2;
}
여기서 CPU에서 GPU에 정보를 넘겨주기 위해 BuildBuffersAndViews() 함수를 통해 위에서 정의한 SRV와 UAV 버퍼의 옵션을 정해주고 초기화하여 만들어준다.
void VecAddDemo::BuildBuffersAndViews()
{
std::vector<Data> dataA(_numElements);
std::vector<Data> dataB(_numElements);
for (int i = 0; i < _numElements; ++i)
{
dataA[i].v1 = XMFLOAT3(i, i, i);
dataA[i].v2 = XMFLOAT2(i, 0);
dataB[i].v1 = XMFLOAT3(-i, i, 0.0f);
dataB[i].v2 = XMFLOAT2(0, -i);
}
// Create a buffer to be bound as a shader input (D3D11_BIND_SHADER_RESOURCE).
D3D11_BUFFER_DESC inputDesc;
inputDesc.Usage = D3D11_USAGE_DEFAULT;
inputDesc.ByteWidth = sizeof(Data) * _numElements;
inputDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
inputDesc.CPUAccessFlags = 0;
inputDesc.StructureByteStride = sizeof(Data);
inputDesc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED;
D3D11_SUBRESOURCE_DATA vinitDataA;
vinitDataA.pSysMem = &dataA[0];
ComPtr<ID3D11Buffer> bufferA;
HR(_device->CreateBuffer(&inputDesc, &vinitDataA, bufferA.GetAddressOf()));
D3D11_SUBRESOURCE_DATA vinitDataB;
vinitDataB.pSysMem = &dataB[0];
ComPtr<ID3D11Buffer> bufferB;
HR(_device->CreateBuffer(&inputDesc, &vinitDataB, bufferB.GetAddressOf()));
// Create a read-write buffer the compute shader can write to (D3D11_BIND_UNORDERED_ACCESS).
D3D11_BUFFER_DESC outputDesc;
outputDesc.Usage = D3D11_USAGE_DEFAULT;
outputDesc.ByteWidth = sizeof(Data) * _numElements;
outputDesc.BindFlags = D3D11_BIND_UNORDERED_ACCESS;
outputDesc.CPUAccessFlags = 0;
outputDesc.StructureByteStride = sizeof(Data);
outputDesc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED;
HR(_device->CreateBuffer(&outputDesc, 0, _outputBuffer.GetAddressOf()));
// Create a system memory version of the buffer to read the results back from.
outputDesc.Usage = D3D11_USAGE_STAGING;
outputDesc.BindFlags = 0;
outputDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
HR(_device->CreateBuffer(&outputDesc, 0, _outputDebugBuffer.GetAddressOf()));
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
srvDesc.Format = DXGI_FORMAT_UNKNOWN;
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFEREX;
srvDesc.BufferEx.FirstElement = 0;
srvDesc.BufferEx.Flags = 0;
srvDesc.BufferEx.NumElements = _numElements;
_device->CreateShaderResourceView(bufferA.Get(), &srvDesc, _inputASRV.GetAddressOf());
_device->CreateShaderResourceView(bufferB.Get(), &srvDesc, _inputBSRV.GetAddressOf());
D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc;
uavDesc.Format = DXGI_FORMAT_UNKNOWN;
uavDesc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER;
uavDesc.Buffer.FirstElement = 0;
uavDesc.Buffer.Flags = 0;
uavDesc.Buffer.NumElements = _numElements;
_device->CreateUnorderedAccessView(_outputBuffer.Get(), &uavDesc, _outputUAV.GetAddressOf());
}
그리고 실행할 때 값을 바인딩해주고 계산작업에 넘겨주는 것은 DoComputeWork함수에서 이루어진다.
void VecAddDemo::DoComputeWork()
{
D3DX11_TECHNIQUE_DESC techDesc;
//바인딩
Effects::VecAddFX->SetInputA(_inputASRV);
Effects::VecAddFX->SetInputB(_inputBSRV);
Effects::VecAddFX->SetOutput(_outputUAV);
Effects::VecAddFX->VecAddTech->GetDesc(&techDesc);
for (UINT p = 0; p < techDesc.Passes; ++p)
{
ID3DX11EffectPass* pass = Effects::VecAddFX->VecAddTech->GetPassByIndex(p);
pass->Apply(0, _deviceContext.Get());
//스레드 그룹지정
_deviceContext->Dispatch(1, 1, 1);
}
// Unbind the input textures from the CS for good housekeeping.
ID3D11ShaderResourceView* nullSRV[1] = { 0 };
_deviceContext->CSSetShaderResources(0, 1, nullSRV);
// Unbind output from compute shader (we are going to use this output as an input in the next pass,
// and a resource cannot be both an output and input at the same time.
ID3D11UnorderedAccessView* nullUAV[1] = { 0 };
_deviceContext->CSSetUnorderedAccessViews(0, 1, nullUAV, 0);
// Disable compute shader.
_deviceContext->CSSetShader(0, 0, 0);
std::ofstream fout("results.txt");
// Copy the output buffer to system memory.
_deviceContext->CopyResource(_outputDebugBuffer.Get(), _outputBuffer.Get());
// Map the data for reading.
D3D11_MAPPED_SUBRESOURCE mappedData;
_deviceContext->Map(_outputDebugBuffer.Get(), 0, D3D11_MAP_READ, 0, &mappedData);
Data* dataView = reinterpret_cast<Data*>(mappedData.pData);
for (int i = 0; i < _numElements; ++i)
{
fout << "(" << dataView[i].v1.x << ", " << dataView[i].v1.y << ", " << dataView[i].v1.z <<
", " << dataView[i].v2.x << ", " << dataView[i].v2.y << ")" << std::endl;
}
_deviceContext->Unmap(_outputDebugBuffer.Get(), 0);
fout.close();
}
받아오는 결과버퍼를 묘사하는 UAV를 통해 값을 가져와서 최종결과값은 outputDebugBuffer에 넣어주고 있는 모습이다.
이렇게 CPU가 아닌 GPU를 통해 연산작업을 해주는 것을 어떻게 사용할 수 있는지는 다음 예제코드를 보고 분석해보자.
우선 예제코드를 실행해보면 Blur효과가 들어가있는 것을 볼 수 가 있다.
예제코드에 gaussian blur 가 적용되어 있는데 이때 모든 픽셀의 값에 Blur 연산을 적용해야하는데 이 부분을 GPU에 넘겨줘서 해주게 된다. 그리고 지금 OM단계에서 렌더타켓이 아니라 텍스처에 렌더링을 해주고 있는 것을 볼 수 있다. 이렇게 해주면 텍스처를 수정하는 것으로 실제 보여지는 화면을 수정해줄 수 있다.
ComPtr<ID3D11ShaderResourceView> _offscreenSRV;
ComPtr<ID3D11UnorderedAccessView> _offscreenUAV;
ComPtr<ID3D11RenderTargetView> _offscreenRTV;
ID3D11RenderTargetView* renderTargets[1] = { _offscreenRTV.Get()};
//렌더타켓을 텍스처로
_deviceContext->OMSetRenderTargets(1, renderTargets, _depthStencilView.Get());
//연산해주고 교체
_blur.BlurInPlace(_deviceContext, _offscreenSRV, _offscreenUAV, 4);
그리고 쉐이더 연산을 보면 가져온 텍스처를 수직또는 수평으로 나눠서 연산을 해주고 있다. 이때 GroupMemoryBarrierWithGroupSync(); 함수를 통해 이 코드의 위에까지의 작업이 모든 스레드에서 끝날 때까지 기다리게 한다.
//입력- 텍스처
Texture2D gInput;
RWTexture2D<float4> gOutput;
#define N 256
#define CacheSize (N + 2*gBlurRadius)
//공유하는 버퍼
groupshared float4 gCache[CacheSize];
[numthreads(N, 1, 1)]
void HorzBlurCS(int3 groupThreadID : SV_GroupThreadID,
int3 dispatchThreadID : SV_DispatchThreadID)
{
//
// Fill local thread storage to reduce bandwidth. To blur
// N pixels, we will need to load N + 2*BlurRadius pixels
// due to the blur radius.
//
//텍스처를 공유하는 버퍼에 캐싱하여서 사용
if (groupThreadID.x < gBlurRadius)
{
// Clamp out of bound samples that occur at image borders.
int x = max(dispatchThreadID.x - gBlurRadius, 0);
gCache[groupThreadID.x] = gInput[int2(x, dispatchThreadID.y)];
}
if (groupThreadID.x >= N - gBlurRadius)
{
// Clamp out of bound samples that occur at image borders.
int x = min(dispatchThreadID.x + gBlurRadius, gInput.Length.x - 1);
gCache[groupThreadID.x + 2 * gBlurRadius] = gInput[int2(x, dispatchThreadID.y)];
}
// Clamp out of bound samples that occur at image borders.
gCache[groupThreadID.x + gBlurRadius] = gInput[min(dispatchThreadID.xy, gInput.Length.xy - 1)];
// Wait for all threads to finish.
GroupMemoryBarrierWithGroupSync();
//
// Now blur each pixel.
//
float4 blurColor = float4(0, 0, 0, 0);
[unroll]
for (int i = -gBlurRadius; i <= gBlurRadius; ++i)
{
int k = groupThreadID.x + gBlurRadius + i;
blurColor += gWeights[i + gBlurRadius] * gCache[k];
}
gOutput[dispatchThreadID.xy] = blurColor;
}
[numthreads(1, N, 1)]
void VertBlurCS(int3 groupThreadID : SV_GroupThreadID,
int3 dispatchThreadID : SV_DispatchThreadID)
{
//
// Fill local thread storage to reduce bandwidth. To blur
// N pixels, we will need to load N + 2*BlurRadius pixels
// due to the blur radius.
//
// This thread group runs N threads. To get the extra 2*BlurRadius pixels,
// have 2*BlurRadius threads sample an extra pixel.
if (groupThreadID.y < gBlurRadius)
{
// Clamp out of bound samples that occur at image borders.
int y = max(dispatchThreadID.y - gBlurRadius, 0);
gCache[groupThreadID.y] = gInput[int2(dispatchThreadID.x, y)];
}
if (groupThreadID.y >= N - gBlurRadius)
{
// Clamp out of bound samples that occur at image borders.
int y = min(dispatchThreadID.y + gBlurRadius, gInput.Length.y - 1);
gCache[groupThreadID.y + 2 * gBlurRadius] = gInput[int2(dispatchThreadID.x, y)];
}
// Clamp out of bound samples that occur at image borders.
gCache[groupThreadID.y + gBlurRadius] = gInput[min(dispatchThreadID.xy, gInput.Length.xy - 1)];
// 위의 1차 작업이 끝날때 까지 기다리기
GroupMemoryBarrierWithGroupSync();
//
// Now blur each pixel.
//
float4 blurColor = float4(0, 0, 0, 0);
[unroll]
for (int i = -gBlurRadius; i <= gBlurRadius; ++i)
{
int k = groupThreadID.y + gBlurRadius + i;
blurColor += gWeights[i + gBlurRadius] * gCache[k];
}
gOutput[dispatchThreadID.xy] = blurColor;
}
'게임공부 > Directx11(물방울책)' 카테고리의 다른 글
[Directx11][C++][물방울]10. Instancing and Culling (1) | 2024.10.30 |
---|---|
[Directx11][C++][물방울]9. Tessellation (5) | 2024.10.29 |
[Directx11][C++][물방울]7. Geometry Shader (1) | 2024.10.22 |
[Directx11][C++][물방울]6. Stencil (1) | 2024.10.21 |
[Directx11][C++][물방울]5. 블렌딩 (1) | 2024.10.19 |