굉장히 중요하다
이 문서를 살펴 보면 된다.
https://learn.microsoft.com/ko-kr/windows/win32/direct3dhlsl/sv-groupindex#remarks
SV_GroupIndex - Win32 apps
스레드 그룹 내에서 컴퓨팅 셰이더 스레드의 \ 0034;flattened \ 0034; 인덱스로, 다차원 SV\_GroupThreadID를 1D 값으로 바꿉니다. SV\_GroupIndex 범위는 0~(numthreadsX \ numthreadsY \ numThreadsZ) \ 8211; 1까지입니다.
learn.microsoft.com
셀 단위로 있는게 병사라고 가정을 하고 병사들이 오와 열을 이뤄서 3차원의 배열을 만든 상태라고 보면 된다.
쓰레드를 10, 8, 3 이라고 했었다.
그럼 최종적으로 240개의 스레드르 고용한다는 얘기가 되고 ,
각각의 칸에 해당하는 모든 애들이 하나의 스레드를 나타낸다고 보면 된다.
병사가 10x8x3 명 만큼 하나의 분대 단위를 이룬다고 볼 수 있다.
240개가 모여서 하나의 부대가 만들어지면 그 부대가 하나의 그룹이 되는 것이고 그 그룹이 몇개가 있는지를 Dispatchf르 할 때 5, 3, 2로 3차원으로 팀을 만들어 주는 것이다.
일단은x,y,z로 관리가 되고 있는데 결과적으로 3차원 배열로 나타나 있는 것이지만 개수를 생각한다면 x,y,z값을 다 곱하면 그룹의 개수 혹은 스레드의 개수가 된다는 것을 알 수 있다.
이 그림에서는 Dispatch를 5,3,2로 했고, 실질적으로 number thread는 10, 8, 3 으로 설정했다고 하면 여기서 Group은 5*3*2 = 30개 만큼의 그룹이 있는데
그 그룹마다 240개의 스레드가 있는 상태라고 볼 수 있다.
실습 했을 때는
_shader->Dispatch(0, 0, 1, 1, 1);
1,1,1로 했으니까 사실상 1개의 그룹만 있었다는 얘기가 되는 거고,
1개의 그룹 내에서 몇개의 쓰레드가 있었느냐는
uint32 count = 10 * 8 * 3;
동일하게 240개가 있었다고 보면 된다.
그 다음으로 중요한 건 엑셀 파일로 출력해 살펴 봤던 각기 SV_ 시리즈들이 무엇을 의미 하는지 살펴보면 된다.
엑셀 파일을 보고 유추해 보는 것도 도움이 된다.
위의 그림의 수식을 보고 이해하면 된다.
이걸 이해하는게 왜 중요하냐면 스레드를 고용해서 많은 정보들을 던져 놓을 것인데 데이터를 실질적으로 던질 때의 관건은 어떻게 데이터를 분할해서 시킬 것인지이다.
하나의 이미지 파일을 가공해달라고 Compute Shader에 던지는 경우가 있을 것이고, 하나의 행렬을 이용해서 그 행렬을 여러개의 스레드들이 담당하게 만들어 주는 경우가 있을 것이고 다양한 형태로 일을 분배해 줄 것인데 어떤 애가 어떤애를 담당할지는 넘버링을 참고해서 그 넘버링에 따라서 분할을 해서 일감을 떠넘기게 되면 스레드한테 일을 시키는 코드를 만들 수 있다.
문서를 읽어 보길 권장한다.
두 번째로 실습할 것은 RawBuffer를 사용할 때 즉
RWByteAddressBuffer Output; // UAV
RWByteAddressBuffer를 사용할 때Output만 이용해서
[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);
}
이렇게 추출하는 거 까지 해봤는데
보통은 이렇게 Output만 추출하기 보다는 Input에다가 무엇인가를 넣어주고
그걸 이용해서 다시 가공을 하고 뱉어주는 경우가 많을 것이다 .
그 실습을 이어서 해보도록 할 것이다.
1. GroupDemo 클래스 만들고 세팅하기
RawBufferDemo 클래스를 복붙해서 이름을 GroupDemo라고 해서 진행을 해본다.
Client/Game필터에 넣는다.
개념적으로 다른 건 아니고 input만 추가해서 input을 넣어주는 작업을 해본다.
코드를 GroupDemo에 맞게 수정한다.
_shader = make_shared<Shader>(L"25. GroupDemo.fx");
Main으로 가서 세팅한다.
2. 25. GroupDemo.fx 만들기
24. RawBufferDemo.fx를 복붙하고 이름을 25. GroupDemo.fx로 한다.
이제 목적으로 하는 것은 렌덤값 하나를 정해서 input값을 쉐이더에 건내 줬다가 그 input을 받아서 가공을 하건 그냥 그 값을 읽어줘서 다시 기입을 해서 건내주건 뭔가 read와 write를 해서 던져주는 것을 연습해 볼 것이다.
3. GroupDemo 클래스 작성하기_Input, value 추가하기
GroupDemo.h의 Ouput으로 넣어준 부분을 찾아서 렌덤한 value를 받아 줄 것이다.
Output에 float value를 추가한다.
struct Output
{
uint32 groupID[3];
uint32 groupThreadID[3];
uint32 dispatchThreadID[3];
uint32 groupIndex;
float value;
};
input도 받아줄 것인데
struct Input
{
float value;
};
렌덤한 값을 받아준 다음에 이 값을 shader 쪽에서 긁어가지고
그 값을 가공하건 토스하건 결과물을 다시 output에 넣어줘가지고 변환하는 식으로 작업을 해볼 것이다.
테스트 하고 싶은 건 많은 값을 input으로 건네 줬을 때 내가 원하는, 특정 스레드가 담당하는 그 공간의 input을 긁어서 그 데이터를 다시 Ouput에 복사해서 반환하는 실습을 하는 것이다.
나중에는 의미없는 float value가 아니라 matrix나 애니메이션 행렬이 됐건 의미 있는 정보를 받아 가지고 리턴 해주긴 해야 한다.
GroupDemo::Init에서
uint32 count = 10 * 8 * 3;
이거를 분할해서 묘사를 해본다.
이게 스레드 개수를 말하는 거였다.
uint32 threadCount = 10 * 8 * 3;
uint32 groupCount = 2 * 1 * 1;
uint32 count = groupCount * threadCount; //최종적 스레드 개수
이렇게 실습을 해볼 것이다.
먼저 inputData를 만들어 본다.
vector<Input> inputs(count);
for (int 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);
input과 output이 각각 데이터를 들고 있는 상태가 되는 것이고
shader에 Input의 SRV를 만들어 줄 것이다. 거기다가 rawBuffer에서 SRV를 꺼내준 다음에 그걸 get해서 연결해준다.
_shader->GetSRV("Input")->SetResource(rawBuffer->GetSRV().Get());
Group Count는 2로 하기로 했으니
_shader->Dispatch(0, 0, 2, 1, 1);
그룹은 2개 스레드는 240개인 상태로 동작을 하게 된다.
"GroupID(X),GroupID(Y),GroupID(Z),GroupThreadID(X),GroupThreadID(Y),GroupThreadID(Z),DispatchThreadID(X),DispatchThreadID(Y),DispatchThreadID(Z),GroupIndex, Value\\n"
Value를 추가하고
"%d,%d,%d, %d,%d,%d, %d,%d,%d, %d,%f\\n",
,%f를 추가하고
, temp.value
를 추가한다.
input을 통해 데이터를 전달하고 가공해서 꺼내오는 건데
단일 스레드에서 하는게 아니라 열심히 스레드끼리 배치를 한 다음에 배치된 규칙에 따라 역사가 일어나는 현장이라고 볼 수 있다.
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; //최종적 스레드 개수
vector<Input> inputs(count);
for (int 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());
// 원래는 draw 계열 함수를 썼는데 얘는 그냥 CompeuteShader를 실행하세요가 Dispatch로 실행된다
// x, y, z 쓰레드 그룹 지정
_shader->Dispatch(0, 0, 2, 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, 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);
}
4. 25. GroupDemo.fx 작성하기_Input, value 추가 된 거 반영하기
그리고 25. GroupDemo.fx에다가
ByteAddressBuffer Input; // SRV
를 넣는다.
그룹이 2개가 되었고 value가 추가된 것을 반영해 수정한다.
[numthreads(10, 8, 3)] // thread의 갯수 의미 240개 고용하겠다는 의미
void CS(ComputeInput input)
{
uint index = input.groupID.x * (10 * 8 * 3) + input.groupIndex;
uint outAddress = index * 11 * 4; // ComputeInput이 4바이트가 10개 있는 거랑 마찬가지다. 주소의 offset 계산
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);
}
넣어준 input 값을 꺼내오는 작업을 해본 것이다.
이거는 스레드를 240 * 2 즉 480 개 만큼 고용해서 일을 빠르게 처리하는 부분이 들어갔다고 보면 된다.
5. 분석하기
실행을 해서 로그를 살펴보자.
Value에 렌덤한 값이 추출되는 걸 볼 수 있다.
GroupID(X)가 0, 1 두가지가 있다.
분석을 하면서 이렇게 작업이 된 것이구나 라는 것을 고민을 해본다.
왜 굳이
[numthreads(10, 8, 3)]
이렇게 3차원으로 표현했을까?
1차원으로 240으로 해도 되지 않을까 싶지만
데이터가 쉐이더를 통해 작업을 할 때는 이미지 파일은 2D 파일이니 2차원으로 관리를 해서 32, 32, 1로 한다거나 유동적으로 컨트롤 해서 굳이 CS에서 인덱스 계산 연산을 두 번 하지 않더라도 원하는 타입에 해당하는 정보를 빠르게 추출할 수 있는 수단을 또 이용할 수 있겠다는 생각이 드니까 그래서 이렇 만들지 않았을까 라고 추측을 하고 있다.
어떠한 데이터를 가공냐에 따라 그에 해당하는 넘버링을 구해주면 되는데
조심해야 할 것은 이 numthreads를 무한으로 늘릴 수 없다.
1024개가 최종 사이즈이기 때문에 그 이상으로 가려면 그룹을 나눠서 작업을 해야한다.
대부분의 경우는 적절한 분배를 해서 하나의 스레드가 어떤 구역을 관리할지를 분담해가지고 시킬 수 있기 때문에 큰 장점이 있다고 볼 수 있다.
이 코드들을 이용해서 여러 장난을 쳐보면 된다.
어떤 데이터를 넣을지 같은 거
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);
이런 식으로 byte address를 사용하는 방식 보다는
나중에 배울 structured buffer demo라는 애를 이용하면 structure buffers는
ByteAddressBuffer Input; // SRV
RWByteAddressBuffer Output; // UAV
이런식의 임의의 데이터를 넣어주는게 아니라 정해진 struct가 있고, 정해진 struct의 배열 같은 걸 만들어서 관리할 수 있게 되는데 그러면 쉽게 작업을 할 수 있게 된다.
무조건 ComputeShader를 사용한다고 지금처럼 복잡하게 만드는 건 아니다.
첫번째 예제에서는 가장 간단한 형식으로 만들었기 때문에 이렇게 구성이 된 거지만, 나중에 얼마든지 개선이 될 여지가 있다.
SV(System value)를 분석하기 위해 RawBuffer를 사용했지만 앞으로 얘는 자주 활용할 일이 없다.
다른 방식으로 Compute Shader를 활용한다 하더라도 넘버링과 관련된
struct ComputeInput
{
uint3 groupID : SV_GroupID;
uint3 groupThreadID : SV_GroupThreadID;
uint3 dispatchThreadID : SV_DispatchThreadID;
uint groupIndex : SV_GroupIndex;
};
이 부분들이 동일하게 들어가니까 이부분들을 주의깊게 분석을 해보길 바란다.
'DirectX' 카테고리의 다른 글
76_StructureBuffer (0) | 2024.03.23 |
---|---|
75_TextureBuffer (0) | 2024.03.22 |
73_RawBuffer (0) | 2024.03.21 |
72_Quaternion (0) | 2024.03.20 |
71_인스턴싱_Scene 구조 정리 (0) | 2024.03.20 |
댓글