본문 바로가기

DirectX11 3D/기본 문법

<DirectX11 3D> 59 - ComputeShader(CS)(1) - RawBuffer(ByteAdress)

 

필요한 개념


RawBuffer 개념


더보기

RawBuffer : CS에 데이터를 넣을 수 있는
Raw : 날것, 순수한 상태(가공하지 않은) 원시 데이터
이미지에서 Raw라고 한다면 보정하지 않은 순수한 상태의 이미지
프로그래밍에서는 Raw가 동일한 바이트로 순서대로 쌓인 파일
(우리가 했던 Byte파일에 4바이트 쓰고 8바이트 쓰고.. 이게 아니라, 계속 4바이트를 쓴다. 그거를 RawFormat이라함)
RawBuffer : Raw 형태로 순차적으로 쌓인 데이터를 CS에서 다루는 버퍼

 

 

 

ThreadGroup 개념


더보기

GroupID : 그룹 자체의 ID
GroupThreadID : 그룹 내에서의 스레드의 ID(그룹이 넘어가면 0으로 초기화됨)
DispatchThreadID : 전체에서의 스레드의 ID (그룹이 넘어가면 누적됨)
GroupIndex : 그룹 내에서의 스레드 번호(그룹이 넘어가면 0으로 초기화됨)

* Thread 제한이 있다.
한 그룹 내에서 쓸수 있는 Thread의 최대 개수는 1024개(보통 1024개가 넘어가면 그룹을 쪼개줌)
픽셀은 1024를 넘어씀(32 x 32) 256 x 256을 처리할 때는 x,y를 32단위로 나눠서(1024로)만들어서 나눠서 처리하게 된다.


참조하기->
https://docs.microsoft.com/ko-kr/windows/win32/direct3dhlsl/sv-groupid

 

SV_GroupID - Win32 apps

컴퓨팅 셰이더가 실행 중인 스레드 그룹에 대한 인덱스입니다.

docs.microsoft.com

 

 

 

만들어진 파일(실행 결과)

ThreadGroup이 1개일때

 

ThreadGroup이 2개일때 - 1

 

* 우리는 x 방향으로 1 증가해서 DispatchThreadID가 (1, 0, 0) * (10, 8, 3) + 위치 값이 된다.

그래서 10부터 누적되는 것이다.

 

 

ThreadGroup이 2개일때 - 2

 

 

 

 

 

Buffers.h 추가된 내용


더보기
...

///////////////////////////////////////////////////////////////////////////////

// ComputeShader를 사용하기 위해
// 버퍼를 3개 만들 건데 그것들을 공통적으로 관리할 클래스를 만든다.

class CsResource
{
public:
	CsResource();
	virtual ~CsResource();

protected:
	// 객체 생성해주는 애들
	virtual void CreateInput() {}
	virtual void CreateSRV() {}

	virtual void CreateOutput() {}
	virtual void CreateUAV() {}

	virtual void CreateResult() {}

	// 가상 함수들을 하나로 묶어서 호출해주는 함수
	void CreateBuffer();

public:
	ID3D11ShaderResourceView* SRV() { return srv; }
	ID3D11UnorderedAccessView* UAV() { return uav; }


protected:
	//ID3DResource란? ID3D11Buffer, ID3D11Texture 등의 부모 인터페이스
	// RawBuffer, StructuredBuffer는 모두 Buffer로 사용할 것이고,
	// Texture2DBuffer는 Texture2D를 사용할 것이다.
	ID3D11Resource* input = NULL;
	// Shader로 넘기기 위해(ID3D11Resource를) 리소스를 세이더에서 어떻게 다룰 것인지 알려주는 애
	ID3D11ShaderResourceView* srv = NULL;

	ID3D11Resource* output = NULL;
	ID3D11UnorderedAccessView* uav = NULL;

	// 여기서 srv는 input용으로 쓸 것이고, uav는 output용으로 사용할 것이다.
	// shader에서 Cpu로 데이터가 스레드로 처리되어서 돌아온다.
	// 그 때 돌아올때, 어떤 형식으로 정렬되어있는지 알 수 없다.
	// 프로그래머가 리턴될 걸 어떻게 쪼개서 사용할 것인지 프로그래머가 결정하는 것이지
	// 얘가 알 수 가 없다. 그래서 정렬되지 않은 걸 access한다해서 unorderedAccessView이다.(추론)
	
	// uav로 만드는 애는 디폴트 여야 한다. 디폴트는 cpu에서 접근해서 데이터를 가져올 수없다.
	// 그래서 output을 복사할것임(copyResource()로)
	// 그거는 디폴트든 뭐든 상관안하고 리소스를 복사해주는 함수임
	// result로 복사해서 데이터를 가져올 것이다.

	ID3D11Resource* result = NULL;
};

///////////////////////////////////////////////////////////////////////////////

class RawBuffer : public CsResource
{
public:
	RawBuffer(void* inputData, UINT inputByte, UINT outputByte);
	~RawBuffer();

private:
	void CreateInput() override;
	void CreateSRV() override;

	void CreateOutput() override;
	void CreateUAV() override;

	void CreateResult() override;

public:
	// 데이터가 중간에 복사가 필요하면, input으로 복사해줘야 한다면
	void CopyToInput(void* data);
	// output으로 부터 결과를 복사해올때
	void CopyFromOuput(void* data);

private:
	// Raw 버퍼를 통해 shader로 넘길 데이터
	void* inputData;
	// 입력으로 얼마만큼 바이트수를 쓸지
	UINT inputByte;	
	// 리턴 받는 바이트 수는 어떻게 될지
	UINT outputByte;
};

 

 

 

 

 

 

 

Buffers.Cpp 추가된 내용


더보기
...

///////////////////////////////////////////////////////////////////////////////

CsResource::CsResource()
{
}

CsResource::~CsResource()
{
	SafeRelease(input);
	SafeRelease(srv);

	SafeRelease(output);
	SafeRelease(uav);

	SafeRelease(result);
}


void CsResource::CreateBuffer()
{
	CreateInput();
	CreateSRV();

	CreateOutput();
	CreateUAV();

	CreateResult();
}

///////////////////////////////////////////////////////////////////////////////

RawBuffer::RawBuffer(void* inputData, UINT inputByte, UINT outputByte)
	: CsResource()
	, inputData(inputData), inputByte(inputByte), outputByte(outputByte)
{
	// 버퍼들을 생성
	CreateBuffer();
}

RawBuffer::~RawBuffer()
{
}

void RawBuffer::CreateInput()
{
	// 최초의 데이터를 가지고 하는 경우도 있지만 최초의 데이터는 안 넣고
	// 버퍼만 만들고 필요할 때 넣는 경우가 있다.
	// 미리 잡아놈
	// inputByte가 1보다 작다면 buffer를 생성할 수 없다
	if (inputByte < 1) return;

	// 버퍼 생성
	// inputData가 null이라면 크기에 맞도록 초기화 데이터는 안넣고 
	// 버퍼는 만들어짐
	ID3D11Buffer* buffer = NULL;

	// DESC구조체 중에 접두사 C가 붙은 것은 구조체를 상속 받아 초기값을 세팅해 주기 위한 구조체
	// DESC 값을 미리 세팅해주기 위한 것이다.

	D3D11_BUFFER_DESC desc;
	ZeroMemory(&desc, sizeof(D3D11_BUFFER_DESC));

	// 버퍼 크기
	desc.ByteWidth = inputByte;
	// D3D11_BIND_SHADER_RESOURCE 형태로 Buffer를 연결
	desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
	// D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS : ByteAdress 버퍼로 사용하겠다고 명시
	desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS;
	// cpu에서는 써야하고 gpu에서는 읽어야 하니까
	desc.Usage = D3D11_USAGE_DYNAMIC;
	desc.CPUAccessFlags = D3D10_CPU_ACCESS_WRITE;
	
	D3D11_SUBRESOURCE_DATA subResource = { 0 };
	subResource.pSysMem = inputData;
	
	// 초기화 데이터 있으면 subResource 넣어주고 아니면 null
	Check(D3D::GetDevice()->CreateBuffer(&desc, inputData != NULL ? &subResource : NULL, &buffer));

	// 부모로 캐스팅해서 지금 만든 버퍼 저장
	input = (ID3D11Resource*)buffer;
}

void RawBuffer::CreateSRV()
{
	if (inputByte < 1) return;


	ID3D11Buffer* buffer = (ID3D11Buffer*)input;

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


	D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
	ZeroMemory(&srvDesc, sizeof(D3D11_SHADER_RESOURCE_VIEW_DESC));
	// Format은 4Byte, R32 하나만 사용하겠다고 명시
	// GPU의 자료형은 float 하나이므로 GPU에서는 기본 Byte는 4바이트(cpu에서는 기본 바이트는 1바이트인데)
	// Raw버퍼 다룰때는 기본 4바이트하는게 편함
	// 그런데 우리는 실제 데이터를 float으로 사용할지 안할지는 모름(그래서 TYPELESS(타입은 모르겠다.))
	// TYPELESS를 사용시에 CS에서 원하는 데이터를 끌어서 사용가능(알아서 자료형 끊어서 사용해라)
	srvDesc.Format = DXGI_FORMAT_R32_TYPELESS;
	// D3D11_SRV_DIMENSION_BUFFEREX(BUFFER로 해도 되지만 왜 BUFFEREX로 하냐면 BUFFEREX.Flags에 Raw로 한다고 명시 하기위해 + BUFFEREX.Element사용 위해)
	srvDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFEREX;
	// SRV를 Raw버퍼 용으로 다뤄라(Shader에서 ByteAdress용으로 다뤄라)
	srvDesc.BufferEx.Flags = D3D11_BUFFEREX_SRV_FLAG_RAW;
	// 전체 데이터의 개수(전체 크기의 / 4), float은 기본 바이트 형이 4byte이니까 4로 나눔
	srvDesc.BufferEx.NumElements = desc.ByteWidth / 4;

	Check(D3D::GetDevice()->CreateShaderResourceView(buffer, &srvDesc, &srv));
}

void RawBuffer::CreateOutput()
{
	ID3D11Buffer* buffer = NULL;

	D3D11_BUFFER_DESC desc;
	ZeroMemory(&desc, sizeof(D3D11_BUFFER_DESC));

	desc.ByteWidth = outputByte;
	// UAV에 연결하겠다.
	desc.BindFlags = D3D11_BIND_UNORDERED_ACCESS;
	// Raw 버퍼 용으로 사용하겠다.
	desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS;

	Check(D3D::GetDevice()->CreateBuffer(&desc, NULL, &buffer));

	output = (ID3D11Resource*)buffer;
}

void RawBuffer::CreateUAV()
{
	ID3D11Buffer* buffer = (ID3D11Buffer*)output;

	D3D11_BUFFER_DESC desc;
	// Buffer나 Texture나 Desc를 가지는 애들, DESC얻어오는 것임
	// desc로 부터 만들어지는 모든 인터페이스는 GetDesc()를 가지고 있다.
	// 자기가 어떻게 생성된지 알아야하니까
	// 가져온 이유는 ByteWidth가져다 쓰기 위해, 버퍼를 만들때, 몇바이트로 만들었느냐
	buffer->GetDesc(&desc);


	D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc;
	ZeroMemory(&uavDesc, sizeof(D3D11_UNORDERED_ACCESS_VIEW_DESC));
	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(D3D::GetDevice()->CreateUnorderedAccessView(buffer, &uavDesc, &uav));
}

void RawBuffer::CreateResult()
{
	ID3D11Buffer* buffer;

	D3D11_BUFFER_DESC desc;
	// output Data를 가져올 건데 output은 resource이다.
	// 그래서 우리가 원하는 형태의 Buffer로 캐스팅
	((ID3D11Buffer*)output)->GetDesc(&desc);

	// 접근을 해서 무조건 읽어 들어야 하니까
	desc.Usage = D3D11_USAGE_STAGING;
	// 결과를 읽어 들어야 하니까
	desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
	// 0인이유는 아무데도 연결안될 거니까, 0이 아니라면 위에 D3D11_BIND_UNORDERED_ACCESS로
	// 연결이 되버림(-> 생성이 실패함)
	// unordered bind flag가 D3D11_BIND_UNORDERED_ACCESS면 무조건 디폴트 사용해야 한다.
	// unordered access view에 연결될려면 BindFlags는 반드시 D3D11_USAGE_DEFAULT(0)이어야한다.
	// unordered access view에 연결되어서 값을 가져올 것이다.
	// 디폴트는 CPU 읽는게 불가하니까, 리소스를 복사(CopyResource 사용해야함)  
	desc.BindFlags = D3D11_USAGE_DEFAULT; // 0
	desc.MiscFlags = 0;

	Check(D3D::GetDevice()->CreateBuffer(&desc, NULL, &buffer));

	result = (ID3D11Resource*)buffer;
}

void RawBuffer::CopyToInput(void* data)
{
	// 데이터를 입력용으로 사용
	// CPU에서 이쪽 버퍼로 써주기 위해 사용

	D3D11_MAPPED_SUBRESOURCE subResource;
	D3D::GetDC()->Map(input, 0, D3D11_MAP_WRITE_DISCARD, 0, &subResource);
	{
		// 복사해주면 됨
		memcpy(subResource.pData, data, inputByte);
	}
	D3D::GetDC()->Unmap(input, 0);
}

void RawBuffer::CopyFromOuput(void* data)
{
	// CopyResource() : 어떤 USAGE건 간에 src리소스를 접근해서 dest 리소스를 복사해준다.
	// output리소스를 result리소스에 복사됨
	D3D::GetDC()->CopyResource(result, output);

	// result는 STAGING이어서 무조건 읽고 쓰기가 가능하니까
	// result를 락을 걸고 result로 부터 읽어오게 된다.
	D3D11_MAPPED_SUBRESOURCE subResource;
	D3D::GetDC()->Map(result, 0, D3D11_MAP_READ, 0, &subResource);
	{
		// gpu 메모리 주소로 부터 data에 받는다.
		memcpy(data, subResource.pData, outputByte);
	}
	// result 락 해제
	D3D::GetDC()->Unmap(result, 0);
}

 

RawBufferDemo.h


더보기
#pragma once
#include "Systems/IExecute.h"

class RawBufferDemo : public IExecute
{
public:
	virtual void Initialize() override;
	virtual void Ready() override {};
	virtual void Destroy() override {};
	virtual void Update() override;
	virtual void PreRender() override {};
	virtual void Render() override;
	virtual void PostRender() override {};
	virtual void ResizeScreen() override {};

private:

};

 

RawBufferDemo.cpp


더보기
#include "stdafx.h"
#include "RawBufferDemo.h"

void RawBufferDemo::Initialize()
{
	Shader* shader = new Shader(L"60_RawBuffer.fx");

	// 스레드 그룹 1개 내에서 스레드 그룹을 운용될 개수
	// 10 : 스레드의 x개수, 8 : 스레드의 y개수, 3 : 스레드의 z개수
	// UINT count = 10 * 8 * 3;

	
	// 스레드 그룹이 2개가 되었으므로 2를 곱함
	// 그룹 x방향 2니까 GroupID가 0, 1로 가게됨
	// GroupThreadID는 그안에서 다시 계산, DispatchThreadID은 누적됨
	// GroupIndex 다시 시작됨
	UINT count = 2 * 10 * 8 * 3;

	struct Output
	{
		UINT GroupID[3];
		UINT GroupThreadID[3];
		UINT DispatchThreadID[3];
		UINT GroupIndex;

		// return value
		// input 했던 value 변수를 output에다가 그대로 써줌(출력을 위해)
		float RetValue;
	};

	// 2번인자 input이 0보다 커야만 RawBuffer를 만든다.(0을 주면 지금 안만들게 되는것)
	//RawBuffer* rawBuffer = new RawBuffer(NULL, 0, sizeof(Output) * count);

	// 입력을 위한 구조체
	struct Input
	{
		float value = 0.0f;
	};

	Input* input = new Input[count];

	// 스레드의 개수만큼 돌림
	// 아무 숫자나 넣어놈
	for (UINT i = 0; i < count; i++)
		input[i].value = Math::Random(0.0f, 10000.0f);

	RawBuffer* rawBuffer = new RawBuffer(input, sizeof(Input) * count, sizeof(Output) * count);

	shader->AsSRV("Input")->SetResource(rawBuffer->SRV());
	// Return 받을때는 UAV로 받는다고 했다.
	// UAV를 연결해주기 위해
	shader->AsUAV("Output")->SetUnorderedAccessView(rawBuffer->UAV());

	// shader를 구동시키는건 렌더링파이프라인에서는 draw를 사용했지만
	// CS에서는 Dispatch()사용
	// 3번인자 : x 스레드 그룹의 개수, 4번인자 : y 스레드 그룹의 개수, 5번인자 : z 스레드 그룹의 개수
	// (ex) 1, 1, 1일경우) 3개를 곱해봐야 1 -> 결국 스레드 그룹은 1개이다.
	//shader->Dispatch(0, 0, 1, 1, 1);

	// 스레드 그룹이 2개가됨(2 * 1 * 1)
	// x 방향으로 그룹을 넓힌 것이 됨
	shader->Dispatch(0, 0, 2, 1, 1);

	// output에다가 읽어옴
	Output* output = new Output[count];
	rawBuffer->CopyFromOuput(output);

	// 데이터를 확인해보기 위해 파일로 써봄
	FILE* file;
	fopen_s(&file, "../Raw.csv", "w");

	assert(file != 0);

	// 스레드의 개수만큼 돌림
	for (UINT i = 0; i < count; i++)
	{
		Output temp = output[i];

		fprintf
		(
			file,
			"%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%f\n",
			i,
			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.RetValue
		);
	}

	fclose(file);

}

void RawBufferDemo::Update()
{
}

void RawBufferDemo::Render()
{
}

 

 

RawBuffer.fx


더보기
// 입력이 있다면 이런식으로
ByteAddressBuffer Input; // SRV

// 입력은 없다니까
// UAV에 연결될 ouput은 RW 구문이 앞에 붙는다.
RWByteAddressBuffer Output; // UAV

struct Group
{
    // 배열로 안쓰고(3개니까) 3를 붙여서 사용하면 된다.
    uint3 GroupID;
    uint3 GroupThreadID;
    uint3 DispatchThreadID;
    uint GroupIndex;
    float RetValue;
};

// 입력될 구조체
struct ComputeInput
{
    // SV(SystemValue) : 시스템 value이니까, 우리가 shader에 넘겨주거나, 혹은 Shader로부터 값을 받아오는, 명시해주는 semantic
    uint3 GroupID : SV_GroupID; // 시스템으로 부터 ID가 입력될 것임
    uint3 GroupThreadID : SV_GroupThreadID;
    uint3 DispatchThreadID : SV_DispatchThreadID;
    uint GroupIndex : SV_GroupIndex;
};

// 스레드 하나하나가 이 함수를 물고 실행한다.
// 우리는 shader->Dispatch() -> 총 1개로 스레드 그룹으로 설정했다.
// 그룹 하나에서 사용할 스레드의 개수 count가 여기서 결정남
// x, y, z 그룹 내에서 다시 x, y, z으로 분할되는 것이 됨
[numthreads(10, 8, 3)]
void CS(ComputeInput input)
{
    // 요것들을 받아서 넣을 것임
    Group group;
    
    // 아이디 체계가 어떻게 되었는지 보고가려고 이렇게 함
    
    // asuint() : uint형으로 casting해주는 함수
    // (uint)input.GroupID -> 이렇게 캐스팅해도 되지만, cs에서는 안정성이 확보되지 않음
    // 그래서 ms에서도 asuint()를 사용해서 캐스팅하라고 권고하고 있다.
    group.GroupID = asuint(input.GroupID);
    group.GroupThreadID = asuint(input.GroupThreadID);
    group.DispatchThreadID = asuint(input.DispatchThreadID);
    group.GroupIndex = asuint(input.GroupIndex);
    
    // byteAddressBuffer는 순차적으로 byte로 들어온다.(4바이트씩)
    // cpu에서 바이트는 1바이트지만, gpu에서는 바이트는 4바이트(float)
    // 쓸 주소를 결정
    // 스레드 번호가 들어옴(순차적(0, 1, 2....))
    // input.GroupID.x가 0, 1이 됨(x방향으로) -> 페이지 수
    // 한 그룹에는 스레드의 개수가 10 * 8 * 3이다.
    uint index = input.GroupID.x * 10 * 8 * 3 + input.GroupIndex;
    // 10은 uint가 총 10개니까 * uint형 하나가 4바이트
    // GroupIndex를 곱한 이유가 GroupIndex는 그룹 내에서의 인덱스
    // [numthreads(10, 8, 3)] 0 ~ 9까지 가고 y가 1증가해서 다시 가고...
    // 스레드 하나가 40byte를 쓰게된다. index가 0이라면 0부터, 1이라면 40, 2라면 80....
    // 몇 번 부터 쓸건지
    // RetValue가 더 생겨서 1개 더 추가해서(데이터가 총 11개)
    // 스레드 하나가 11개 변수를 쓰는 걸로 바뀜
    uint outAddress = index * 11 * 4;
    
    
    // 읽어들일 주소 계산(float이고 4바이트 짜리 하나니까) Input 구조체에 하나만 넣어서
    uint inAddress = index * 4;
    // float4면 Load4하면 됨
    // 데이터를 읽어들이기 위해 Load()
    group.RetValue = asfloat(Input.Load(inAddress));
    
    // Store() : 데이터를 내보내기 위해 사용하는 함수
    // 우리는 uint 3개짜리를 내보내니까 Store3
    // 1번 인자 : 쓸 주소,
    // 계산된 0번 부터 쓰겠다는 의미
    Output.Store3(outAddress + 0, asuint(group.GroupID)); // 12바이트를 씀
    // 12바이트를 써서 +12 부터 씀
    Output.Store3(outAddress + 12, asuint(group.GroupThreadID)); // 24
    Output.Store3(outAddress + 24, asuint(group.DispatchThreadID)); // 36
    Output.Store(outAddress + 36, asuint(group.GroupIndex)); // 40
   // float형이라도 asuint로 캐스팅해야 한다.(안그러면 0나옴)
    Output.Store(outAddress + 40, asuint(group.RetValue));
    
    
    // 결국 순서대로 OutputBuffer(UAV)에 쓰게됨
}

// CS를 운영하기 위해 technique가 있어야함
technique11 T0
{
    pass P0
    {
        // VS와 PS는 기본 셰이더라서 무조건 NULL 만들어 줘야 함
        SetVertexShader(NULL);
        SetPixelShader(NULL);

        // 1번 : 셰이더 버전(5_0은 11의 셰이더 버젼), 2번인자 : 함수 이름()
        SetComputeShader(CompileShader(cs_5_0, CS()));
    }
}