그래픽스(DirectX)

[DirectX11] GPU에게 일 시키기(Compute Shader)

gamzachips 2023. 4. 29.

 

GPU

GPU는 단일 위치 또는 순차적 위치에 있는 많은 양의 메모리를 처리하는데 최적화되어있다.

병렬적으로 처리될 수 있는, 비슷한 작업을 해야 하는 많은 양의 데이터 요소가 있을 때 GPU를 사용한다.

 

예를 들어 그래픽스에서 vertex와 pixel은 독립적으로 처리되기 때문에 병렬적으로 연산한다.

다른 예로, particel system의 경우 입자들이 서로 상호작용하지 않는다고 단순화하면

각 입자의 물리는 독립적으로 계산될 수 있다.

 

이러한 GPU의 병렬 구조의 방대한 계산력은 그래픽 외의 몇몇 작업에서도 이점이 있다.

그래픽이 아닌 다른 곳에 GPU를 사용하는 것을 general purpose GPU (GPGPU) programming라고 한다.

이러한 GPU의 활용은 GPGPU뿐만 아니라 그래픽 효과들을 구현하는 데에도 많이 사용된다.

 

GPU에서 전형적으로는 렌더링 파이프라인의 입력으로서 결과를 연산 결과를 사용하므로

GPU에서 CPU로 변환이 필요하지 않다.

하지만 GPGPU를 위해서는 GPU로 계산한 결과를 VRAM(video RAM)에서 CPU의 RAM으로 복사해와야 한다.

이러한 복사 자체는 느리지만, 매 프레임마다 복사하는 것은 아니므로

GPU를 통해 속도를 높이는 것과 비교한다면 문제가 되지 않을 것이다.

 

compute shader는 Direct3D에서 제공하는 프로그래밍 가능한 shader이다.

이는 렌더링 파이프라인의 어느 한 부분이 아니지만, 대신 GPU의 리소스를 읽고 쓴다.

 

 

 

compute shader의 요소

1. constant buffer를 통한 전역변수

2. Input, Output 리소스

3. numthread[(x,y,z)] <- 스레드 그룹의 topology

스레드의 3D 그리드로서의 스레드 그룹에 있는 스레드 수를 나타냄

4. 각 스레드에 실행할 명령어가 있는 shader body

5. 스레드 식별 system value 변수

 

 

데이터 Input과 Output Resources

compute shader에 바운딩할 수 있는 리소스는 buffers , textures로 두 종류이다.

Input 리소스는 SRV를 생성함으로써 shader에 input으로 바운딩한다(SRV는 read-only).

 

Output리소스는 앞에 RW가 붙는다(Read-Write).

compute shader에 있는 이 리소스의 요소를 읽거나 쓸 수 있다.

그리고 타입과 차원을 지정해주어야 한다(예: RWTexture2D<float4> gOutput; ).

 

output 리소스의 바운딩에는 unordered access view(UAV) 타입을 사용해야 한다(ID3D11UnorderedAccessView).

UAV에 바운딩하기 위해 리소스 DESC의 BindFlags로 D3D11_BIND_UNORDERED_ACCESS를 지정해야 한다.

또한 texture 작업을 위해 compute shader를 사용한 후 이를 가지고 텍스쳐링을 하는 것이 흔하므로,

'D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS' 를 결합하여 지정하는 경우가 많다.

 

 

 

 

결과를 시스템 메모리로 복사하기

UAV를 통해 만든 버퍼는 GPU에 저장되어있으므로 이 결과를 사용하기 위해서는 CPU로 가져와야 한다.

Usage가 'D3D11_USAGE_STAGING', CPUAccessFlags가 'D3D11_CPU_ACCESS_READ' 로 지정된

시스템 메모리 버퍼를 만든다.

 

ID3D11DeviceContext::CopyResource를 사용해서 GPU 리소스를 시스템 메모리로 복사해온다.

시스템 메모리는 복사할 리소스와 같은 타입, 크기여야 한다.

 

마지막으로 시스템 메모리 버퍼를 CPU에 매핑한다.

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);
 

 

 

스레드와 스레드 그룹

GPU 프로그래밍에서, 실행에 필요한 스레드의 수는 스레드 그룹(thread groups)의 그리드로 나누어진다.

스레드 그룹은 각각의 멀티프로세서에서 실행된다.

예를 들어 16개의 멀티프로세서가 있다면, 16개의 스레드 그룹으로 분리하여 처리할 수 있다.

또한 성능 향상을 위해서 하나의 멀티프로세서에서 2개 이상의 스레드 그룹을 처리하게 할 수 있다.

각 스레드 그룹 내의 스레드끼리는 메모리를 공유해서 접근한다.

스레드 동기화 작업도 각 스레드 그룹 내에서 일어난다.

 

스레드 그룹은 n개의 스레드로 이루어진다.

하드웨어는 스레드들을 warp로 나누며, warp마다 32개의 스레드 (NVIDIA 기준)가 있다(ATI는 64개; warpfront size).

하나의 warp는 SIMD32에서 처리되는데, 32개의 스레드에 대해 명령어가 동시에 실행되는 것이다.

따라서 성능을 위해 스레드 그룹의 사이즈 항상 warp size (32)의 배수인 것이 좋다.

또한 warpfront size를 사용하면 두 종류의 GPU를 모두 만족시킬 수 있으므로 권장된다.

 

 

 

 

 

스레드 식별 System Value

  1. 각 스레드 그룹은 ID가 할당되어있다. SV_GroupID
  2. 스레드 그룹 안의 각 스레드들은 각 그룹에서의 고유한 ID가 있다. SV_GroupThreadID 스레드 로컬 저장 메모리로 indexing하는 데 유용함
  3. Dispatch call :스레드 그룹의 그리드를 발신. dispath thread ID는 dispatch call에 의해 생성된 모든 스레드들 사이에서 고유한 식별자이다. group ID와 group thread ID를 가지고 정해짐. SV_DispatchThreadID
  4. group thread ID의 선형 인덱스 버전. SV_GroupIndex
 
 

 

스레드를 여러 차원으로 관리하는 것은 다차원 자료를 쉽게 처리하기 위함일 것이다.

예를 들어 2차원 Texture는 2차원 스레드로 관리하기에 편하다.

한 그룹에 스레드 수는 1024개까지 제한된다.

 

 

 

 

RawBuffer 이용한 CS

ByteAdressBuffer로, 주소 크기를 byte형으로 직접적으로 지시해주는 버퍼이다.

struct Input
{
	float value;
};
struct Output
{
	uint32 groupID[3];
	uint32 groupThreadID[3];
	uint32 dispatchThreadID[3];
	uint32 groupIndex;
};
 
uint32 threadCount = 10 * 8 * 3; //하나의 스레드그룹 내에서 운영할 스레드 개수 
uint32 groupCount = 2 * 1 * 1;
uint32 count = groupCount * threadCount; 

vector<Input> inputs(count);
for (int32 i = 0; i < count; i++)
	inputs[i].value = rand() % 10000;

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());

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

vector<Output> outputs(count);
rawBuffer->CopyFromOutput(outputs.data());
 

Shader

ByteAddressBuffer Input;
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)] //스레드의 개수
void CS(ComputeInput input)
{
	uint index = input.groupID.x * (10 * 8 * 3) + input.groupIndex;
	uint outAddress = index * 11 * 4; //offset //sizeof(ComputeInput) = 10 * 4
	
	uint inAddress = index * 4; //4byte
	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()));
	}
};
 

RawBuffer보다는, TextureBuffer나 StructuredBuffer를 주로 사용한다.

 

 

 

StructuredBuffer 이용한 CS

 

input과 output의 ByteWidth설정해준다. stride * count

 

랜덤한 3차원 벡터 64개의 크기를 Compute Shader를 통해 구해본다. (루나책 연습문제 1번 변형)

struct InputDesc
{
	int x;
	int y;
	int z;
};

struct OutputDesc
{
	float result;
};

StructuredBuffer<InputDesc> Input;
RWStructuredBuffer<OutputDesc> Output;

[numthreads(64, 1, 1)]
void CS(uint id : SV_DispatchThreadID)
{
	float result = (float)(pow(Input[id].x, 2) + pow(Input[id].y, 2) + pow(Input[id].z, 2));
	result = sqrt(result);
	Output[id].result = result;
}

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

 

C++ 코드. 원래 연습문제는 크기가 1~10인 벡터를 만드는 것이지만, 편의상.. x,y,z의 길이가 1~10인 랜덤 벡터 64개를 만들었다.

int count = 64;
	struct Input
	{
		int x; int y; int z;
	};
	vector<Input> inputs(count);
	for (int i = 0; i < count; i++)
	{
		Input input;
		input.x = rand() % 10 + 1;
		input.y = rand() % 10 + 1;
		input.z = rand() % 10 + 1;
		inputs[i] = input;
	}

	auto buffer = make_shared<StructuredBuffer>(inputs.data(), sizeof(int) * 3, count, sizeof(float), count);

	_shader->GetSRV("Input")->SetResource(buffer->GetSRV().Get());
	_shader->GetUAV("Output")->SetUnorderedAccessView(buffer->GetUAV().Get());
	_shader->Dispatch(0, 0, 1, 1, 1);

	vector<float> outputs(count);
	buffer->CopyFromOutput(outputs.data());

	FILE* file;
	::fopen_s(&file, "../StructedBuffer.csv", "w");
	::fprintf
	(
		file,
		"X,Y,Z,Magnitude\n"
	);

	for (uint32 i = 0; i < count; i++)
	{
		const float& temp = outputs[i];
		::fprintf
		(
			file, "%d,%d,%d,%f,\n",inputs[i].x, inputs[i].y, inputs[i].z, temp
		);
	}

	::fclose(file);
 

 

TextureBuffer 이용한 CS

Compute Shader를 이용하여 Texture를 흑백으로 만들어본다.

 

Input, SRV, Output,UAV를 만든다.

ID3D11Texture2D를 이용한다. input, output.

Texture의 결과는 그릴 것이므로, output의 BinfFlags에 D3D11_BIND_SHADER_RESOURCE를 추가하고,

output으로 SRV로 만든다.

 

shader 코드

각 스레드마다 고유한 DispatchThreadID를 이용하여,

각 스레드에게 id에 해당하는 색상을 처리하게 한다. 2D Texture이므로 2차원 스레드를 관리한다.

Texture2DArray<float4> Input;
RWTexture2DArray<float4> Output;

[numthreads(32, 32, 1)]
void CS(uint3 id : SV_DispatchThreadID)
{
	float4 color = Input.Load(int4(id, 0));
    Output[id] = (color.r + color.g + color.b) / 3.0f;
}

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

C++코드

auto shader = make_shared<Shader>(L"TextureBufferDemo.fx");
	
auto texture = RESOURCES->Load<Texture>(L"Ebichu", L"..\\Resources\\Textures\\Ebichu.jpg");
shared_ptr<TextureBuffer> textureBuffer = make_shared<TextureBuffer>(texture->GetTexture2D());

shader->GetSRV("Input")->SetResource(textureBuffer->GetSRV().Get());
shader->GetUAV("Output")->SetUnorderedAccessView(textureBuffer->GetUAV().Get());

uint32 width = textureBuffer->GetWidth();
uint32 height = textureBuffer->GetHeight();
uint32 arraySize = textureBuffer->GetArraySize();

uint32 x = max(1, (width + 31) / 32);
uint32 y = max(1, (height + 31) / 32);
shader->Dispatch(0, 0, x, y, arraySize);

auto newSrv = textureBuffer->GetOutputSRV();
 

texture에 newSrv를 적용하여 그린다.

 

 

참고 자료

Introduction to 3D Game Programming with DirectX11 - Frank D. Luna

Rookiss님 인프런 멘토링 강의

 

 

 

댓글