https://asawicki.info/news_1754_direct3d_12_long_way_to_access_data
데이터에 액세스하는 먼 길
쉐이더 스터디 발표자료
- 새로운 그래픽 API인 Direct3D 12와 벌칸이 매우 복잡한 이유 중 하나는 데이터에 액세스하는 데 매우 많은 수준의 간접성이 있기 때문입니다.
- 일반 C++ CPU 변수에 일부 데이터가 준비되어 있으면 셰이더에서 데이터에 액세스하기까지 먼 길을 가야 합니다.
- 얼마 전에 이런 글을 쓴 적이 있습니다: "벌칸: 데이터 액세스를 위한 먼 길"이라는 글을 쓴 적이 있습니다. 일상 업무에서 두 API를 모두 다룰 수 있는 특권이 있기 때문에 D3D12에 해당하는 글도 작성할 수 있을 것 같아서 여기에 올립니다.
- 그래픽 또는 게임 프로그래머로서 이미 D3D12의 기본 사항을 알고 있지만, 여기에 정의된 모든 개념에 대해 혼란을 느끼는 분들을 위해 이 글을 준비했습니다.
다음은 다이어그램과 모든 요소에 대한 설명입니다:
Let's start from the final destination of our data – the shader. We are in the realm of the shader code written in HLSL language, denoted by yellow background. Consider an example of a vertex shader, where we want to multiply vertex position by a combined world-view-projection matrix, to transform it from local model space (this is how vertices of a model are stored in memory) to DirectX clip space (this is the space required for vertex positions on the vertex shader output). Because our matrix is 4x4, we extend the 3-component vertex position to homogenous coordinates by adding 4th component equal to 1 – a thing that is out of scope of this article.
- 데이터의 최종 목적지인 셰이더부터 시작해 봅시다.
- 우리는 HLSL(High-Level Shading Language) 언어로 작성된 셰이더 코드의 영역에 있습니다.
- 이 영역은 노란색 배경으로 표시됩니다.
- 여기서는 정점 셰이더의 예를 고려해 보겠습니다.
- 이 예제에서는 정점 위치에 결합된 월드-뷰-프로젝션 행렬을 곱하여 로컬 모델 공간(모델의 정점이 메모리에 저장되는 방식)에서 DirectX 클립 공간(정점 셰이더 출력에 필요한 공간)으로 변환하고자 합니다.
- 행렬이 4x4이기 때문에 3-성분 정점 위치를 동차 좌표로 확장하여 4번째 성분을 1로 추가합니다.
- 이 부분은 이 글의 범위를 벗어나므로 자세한 설명은 생략합니다.
output.pos = mul(perObjectConstants.worldViewProj, float4(pos, 1));
Here we have a first entity, a first name for our data: perObjectConstants.worldViewProj. This is a symbol valid only inside the code of our vertex shader, defined in its global scope as a constant buffer:
- 여기에는 데이터의 첫 번째 엔티티인 perObjectConstants.worldViewProj라는 이름이 있습니다.
- 이것은 버텍스 셰이더의 코드 내에서만 유효한 심볼로, 전역 범위에서 상수 버퍼(constant buffer)로 정의됩니다:
struct PerObjectConstants
{
float4x4 worldViewProj;
};
ConstantBuffer<PerObjectConstants> perObjectConstants : register(b3, space0);
Here is a place where we cross the realms from yellow to green background. On the shader side, this specific constant buffer is referred as perObjectConstants. For the outside world, we give it another identifier: “register b3 in space 0”. Registers (or slots) where we bind various resources to be accessible to shaders is a concept known in many graphics APIs for a long time.
- 여기서는 노란색 배경에서 녹색 배경으로 영역을 넘어가는 부분을 다루고 있습니다.
- 셰이더 측에서는 이 특정 상수 버퍼를 **perObjectConstants**로 참조합니다.
- 외부 세계에서는 이를 "register b3 in space 0"이라는 다른 식별자로 제공합니다.
- 셰이더에 접근 가능한 다양한 리소스를 바인딩하는 레지스터(또는 슬롯)는 많은 그래픽스 API에서 오랜 시간 동안 사용되어 온 개념입니다.
In D3D12, the register pool is actually 2-dimensional, with the second dimension called “space”. This is because we could define a resource as an unbounded array that has no defined limit on the number of elements, so all higher slots are reserved and so we have to jump to a higher space to have more slots available – but this is out of scope of this article. We normally just use space 0. In this case, the space0 part in HLSL code could be omitted.
- D3D12에서는 레지스터 풀이 실제로 2차원이며, 두 번째 차원을 "space"라고 부릅니다.
- 이는 우리가 정의한 리소스가 정의된 요소 수에 대한 제한이 없는 무한 배열로 간주될 수 있기 때문입니다.
- 그래서 모든 높은 슬롯이 예약되어 있으며, 더 많은 슬롯을 사용하려면 더 높은 공간으로 넘어가야 합니다.
- 그러나 이는 이 글의 범위를 벗어납니다.
- 일반적으로 우리는 단순히 space 0을 사용합니다.
- 이 경우 HLSL 코드에서 space0 부분은 생략할 수 있습니다.
Moving to the realm of C++ code that uses D3D12, we refer to our constant buffer in a root signature. This is an object that describes all the resources that will be bound to our vertex, pixel, and other shaders. Root signature can be defined programmatically by filling structure D3D12_ROOT_SIGNATURE_DESC, calling function D3D12SerializeRootSignature to get a raw data buffer with its serialized content, then pass this to device->CreateRootSignature to create a ID3D12RootSignature object, which later becomes part of a pipeline state object (ID3D12PipelineState). It also has to be set in a recorded command list by calling ID3D12GraphicsCommandList::SetGraphicsRootSignature. Alternatively, a root signature can be defined inside HLSL code, using a not-so-pretty syntax in form of a string.
- D3D12를 사용하는 C++ 코드의 영역으로 넘어가면, 상수 버퍼를 루트 서명에서 참조합니다.
- 루트 서명은 정점 셰이더, 픽셀 셰이더 등과 같은 모든 셰이더에 바인딩될 리소스를 설명하는 객체입니다.
- 루트 서명은 구조체 **D3D12_ROOT_SIGNATURE_DESC**를 채우고, D3D12SerializeRootSignature 함수를 호출하여 직렬화된 내용이 담긴 원시 데이터 버퍼를 얻은 다음, 이를 **device->CreateRootSignature**에 전달하여 ID3D12RootSignature 객체를 생성합니다.
- 이 객체는 나중에 파이프라인 상태 객체(ID3D12PipelineState)의 일부가 됩니다.
- 또한, 명령 리스트에 기록된 후 **ID3D12GraphicsCommandList::SetGraphicsRootSignature**를 호출하여 설정해야 합니다.
- 또는 루트 서명을 HLSL 코드 내에서 문자열 형태의 그리 예쁘지 않은 구문을 사용하여 정의할 수도 있습니다.
But this is all a side note here. What is important to us is that after we define a root parameter describing our constant buffer in register b3, we move to another representation of it. From now on, we will refer to the constant buffer by the index at which we define our root parameter in the root signature, for example: 2:
- 하지만 이것은 모두 부가적인 내용입니다.
- 우리가 중요하게 생각해야 할 것은, 상수 버퍼를 레지스터 b3에 정의한 후, 이를 다른 방식으로 표현하게 된다는 점입니다.
- 이제부터는 루트 서명에서 루트 파라미터를 정의한 인덱스, 예를 들어 2를 사용하여 상수 버퍼를 참조할 것입니다.
D3D12_ROOT_PARAMETER params[10];
// ...
params[2].ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS;
params[2].ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX;
params[2].Constants.ShaderRegister = 3;
params[2].Constants.RegisterSpace = 0;
params[2].Constants.Num32BitValues = 16;
// ...
Here, our diagram branches in to 3 options, marked as A), B), C). These are 3 different ways of passing a constant buffer, which differ in the number of levels of indirection. We choose it by setting appropriate values in the D3D12_ROOT_PARAMETER::ParameterType member. Let’s start with the easiest one A), which is called “32BIT_CONSTANTS”, also known as “root constants”. As you can see in the code above, we only specify register, space, and the number of 32-bit values, which is 16 in case of a 4x4 matrix of float numbers. ShaderVisibility is common to all types of root parameters and can be a specific shader stage (like vertex shader in this example) or D3D12_SHADER_VISIBILITY_ALL. Strangely, this is not a bit flag where we could specify any combination of shader stages. But this is unimportant here.
- 여기에서 우리의 다이어그램은 A), B), C)로 표시된 3가지 옵션으로 나뉩니다.
- 이들은 상수 버퍼를 전달하는 3가지 다른 방법으로, 간접 참조의 레벨 수가 다릅니다.
- 우리는 D3D12_ROOT_PARAMETER::ParameterType 멤버에 적절한 값을 설정하여 선택합니다.
- 가장 쉬운 방법인 A)부터 시작해 보겠습니다.
- 이 방법은 "32BIT_CONSTANTS"라고도 불리며, "루트 상수"로 알려져 있습니다.
- 위의 코드에서 보듯이, 우리는 레지스터, 스페이스, 그리고 32비트 값의 수를 지정합니다.
- 여기서 4x4 행렬의 경우 16개의 32비트 값이 필요합니다.
- **ShaderVisibility**는 모든 유형의 루트 파라미터에 공통이며, 특정 셰이더 단계(이 예제에서는 정점 셰이더) 또는 **D3D12_SHADER_VISIBILITY_ALL**일 수 있습니다.
- 이상하게도, 이는 셰이더 단계의 조합을 지정할 수 있는 비트 플래그가 아닙니다. 하지만 이것은 여기서 중요하지 않습니다.
The reason this approach A) is the simplest is because we can pass the data straight from a pointer to a C++ variable while recording a command list, like this:
- 이 방법 A)가 가장 간단한 이유는, command list 명령 리스트를 기록할 때 C++ 변수의 포인터에서 직접 데이터를 전달할 수 있기 때문입니다.
glm::mat4 worldViewProj = ...
commandList->SetGraphicsRoot32BitConstants(
2, // RootParameterIndex
16, // Num32BitValuesToSet
&worldViewProj, // pSrcData
0); // DestOffsetIn32BitValues
Note the “CPU_data” in the diagram belong to the realm of “Preparation (CPU)” (blue background), which means the pointer doesn’t need to remain valid for the time when the command list is executed on the GPU, only for the call to SetGraphicsRoot32BitConstants. This function makes a copy of the pointed data and stores them inside the command list, in between draw calls and other commands that we are recording.
- 다이어그램에서 "CPU_data"는 "Preparation (CPU)" (파란색 배경) 영역에 속합니다.
- 이는 포인터가 GPU에서 명령 리스트가 실행되는 동안 유효할 필요가 없고,
- SetGraphicsRoot32BitConstants 호출 시에만 유효해야 함을 의미합니다.
- 이 함수는 포인터가 가리키는 데이터의 복사본을 만들어 명령 리스트에 저장하며,
- 이는 드로우 호출 및 기록 중인 기타 명령 사이에 위치하게 됩니다.
Because of this, it is recommended not to over-use this feature. It is better to avoid it or use it only for passing some small amount of data that really changes every draw call. Some index of an instance that a shader will use to refer to some larger record fetched from a buffer may be a good application of the “32BIT_CONSTANTS” root parameter type. Passing a whole 4x4 matrix, as we do in this example, is not so much.
- 따라서 이 기능을 과도하게 사용하지 않는 것이 좋습니다.
- 이 기능은 드로우 호출마다 실제로 변경되는 소량의 데이터를 전달하는 데만 사용하는 것이 좋습니다.
- 예를 들어, 셰이더가 버퍼에서 가져온 더 큰 기록(larger record fetched)을 참조할 인스턴스의 인덱스를 전달하는 경우가
- "32BIT_CONSTANTS" 루트 파라미터 유형의 좋은 활용 사례일 수 있습니다.
- 이 예제에서처럼 전체 4x4 행렬을 전달하는 것은 그리 권장되지 않습니다.
So let’s see the option B), called “CBV”, also known as “root descriptor”. All we need to do when defining the root signature is to specify that register 3 space 0 will be of type CBV:
- 이제 옵션 B)인 "CBV"를 살펴보겠습니다.
- "root descriptor"라고도 알려진 이 방법에서는
- 루트 서명을 정의할 때 레지스터 3, 스페이스 0이 CBV(상수 버퍼 뷰) 유형임을 지정하면 됩니다.
params[2].ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV;
params[2].ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX;
params[2].Descriptor.ShaderRegister = 3;
params[2].Descriptor.RegisterSpace = 0;
Here we have an additional level of indirection involved – a real constant buffer. Note that in approach A) we didn’t have any such thing, despite we defined ConstantBuffer in the shader code. This is because it was kind of shortcut, an “imaginary constant buffer” created by D3D12 from directly passed root constants. Now we will have a real constant buffer object. While recording our command buffer, we have to set our root argument 2 to the address of that constant buffer, using function ID3D12GraphicsCommandList::SetGraphicsRootConstantBufferView.
- 여기에서는 추가적인 간접 참조 레벨이 포함된 실제 상수 버퍼가 사용됩니다.
- A) 접근 방식에서는 셰이더 코드에 **ConstantBuffer**를 정의했음에도 불구하고, 실제 상수 버퍼가 없었습니다.
- 이는 D3D12가 직접 전달된 루트 상수로부터 생성한 일종의 단축키, 즉 "'imaginary constant buffer가상의 상수 버퍼"였기 때문입니다.
- 이제 우리는 실제 상수 버퍼 객체를 사용할 것입니다.
- 명령 버퍼를 기록할 때, ID3D12GraphicsCommandList::SetGraphicsRootConstantBufferView 함수를 사용하여 루트 인수 2를 해당 상수 버퍼의 주소로 설정해야 합니다.
Note that words “descriptor” and “view” can be used somewhat interchangeably when discussing D3D12. Unlike in Vulkan, “views” are not separate objects here. Functions called Create*View just setup descriptors in D3D12.
- D3D12에서 "descriptor"와 "view"라는 용어는 다소 서로 교환 가능하게 사용될 수 있습니다.
- Vulkan과 달리, D3D12에서는 "뷰"가 별도의 객체가 아닙니다.
- **Create*View**라는 함수는 D3D12에서 단순히 디스크립터(descriptors)를 설정하는 역할을 합니다.
ID3D12Resource* constBuf = ...
D3D12_GPU_VIRTUAL_ADDRESS constBufGPUAddr = constBuf->GetGPUVirtualAddress();
commandList->SetGraphicsRootConstantBufferView(
2, // RootParameterIndex
constGPUBufAddr); // BufferLocation
D3D12_GPU_VIRTUAL_ADDRESS is just a typedef to UINT64. D3D12, more often than Vulkan, allows to manipulate raw memory addresses. Thanks to this, there is no need for a separate parameter for an offset from the beginning of the buffer to the beginning of the data we want to bind. We can just increment the address by the right amount of bytes. This is a big upgrade compared to Direct3D 11 with functions like ID3D11DeviceContext::VSSetConstantBuffers, where there was no offset or address, so every data record had to have its own buffer to start at the beginning. Only a later update to the API in DirectX 11.1 added function ID3D11DeviceContext1::VSSetConstantBuffers1 that accepted an offset, allowing to bundle many records of data in a single buffer object.
- **D3D12_GPU_VIRTUAL_ADDRESS**는 단순히 **UINT64**에 대한 타입 정의입니다.
- D3D12는 Vulkan보다 더 자주 원시 메모리 주소를 직접 조작할 수 있게 해줍니다.
- 덕분에 버퍼의 시작부터 우리가 바인딩하려는 데이터의 시작까지의 오프셋을 위한 별도의 매개변수가 필요하지 않습니다.
- 주소를 적절한 바이트 수만큼 증가시키면 됩니다.
- 이는 Direct3D 11에서 **ID3D11DeviceContext::VSSetConstantBuffers**와 같은 함수들을 사용할 때와 비교해 큰 업그레이드입니다.
- Direct3D 11에서는 오프셋이나 주소가 없어서, 모든 데이터 기록이 시작부터 시작하는 별도의 버퍼를 가져야 했습니다.
- 나중에 DirectX 11.1에서 ID3D11DeviceContext1::VSSetConstantBuffers1 함수가 오프셋을 지원하게 되었고,
- 이를 통해 많은 데이터 기록을 하나의 버퍼 객체에 묶을 수 있게 되었습니다.
Back to the DirectX 12, we now need to discuss how a constant buffer is created. Actually, there is no such concept as a “constant buffer” per se. In D3D12 there are just “buffers” and “textures”, together called “resources”. To create a resource, we have to allocate some memory for it. There are two ways to do it, again differing in the number of levels of indirection.
- DirectX 12로 돌아와서, 이제 상수 버퍼가 어떻게 생성되는지에 대해 논의해야 합니다.
- 사실, D3D12에서는 "상수 버퍼(constant buffer)"라는 개념이 존재하지 않습니다.
- D3D12에서는 단순히 버퍼(buffers)"와 "텍스처(textures)"가 있으며, 이들을 함께 "리소스(resources)"라고 부릅니다.
- 리소스를 생성하려면 해당 리소스를 위한 메모리를 할당해야 합니다.
- 메모리 할당 방법에는 두 가지가 있으며, 다시 간접 참조의 레벨 수에 따라 다릅니다.
First and the easiest option to create a resource is to call ID3D12Device::CreateCommittedResource. This function creates a buffer or texture and allocates memory for it in one call. The resource will then have its own dedicated memory block. We do it like this:
- 첫 번째이자 가장 쉬운 방법은 **ID3D12Device::CreateCommittedResource**를 호출하는 것입니다.
- 이 함수는 버퍼 또는 텍스처를 생성하고 메모리를 할당하는 작업을 한 번의 호출로 처리합니다.
- 이렇게 생성된 리소스는 전용 메모리 블록을 가지게 됩니다. 이를 이렇게 수행합니다:
D3D12_HEAP_PROPERTIES heapProps = {};
heapProps.Type = D3D12_HEAP_TYPE_UPLOAD;
D3D12_RESOURCE_DESC resDesc = {};
resDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
resDesc.Alignment = 0;
resDesc.Width = sizeof(glm::mat4);
resDesc.Height = 1;
resDesc.DepthOrArraySize = 1;
resDesc.MipLevels = 1;
resDesc.Format = DXGI_FORMAT_UNKNOWN;
resDesc.SampleDesc.Count = 1;
resDesc.SampleDesc.Quality = 0;
resDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
resDesc.Flags = D3D12_RESOURCE_FLAG_NONE;
ID3D12Resource* constBuf;
HRESULT res = device->CreateCommittedResource(
&heapProps,
D3D12_HEAP_FLAG_NONE,
&resDesc,
D3D12_RESOURCE_STATE_COPY_DEST, // InitialResourceState
nullptr, // pOptimizedClearValue
IID_PPV_ARGS(&constBuf));
Structure D3D12_HEAP_PROPERTIES describes the memory where we want to allocate our buffer, e.g. D3D12_HEAP_TYPE_DEFAULT in case of GPU memory and D3D12_HEAP_TYPE_UPLOAD in case of system memory. Structure D3D12_RESOURCE_DESC describes all the parameters of our resource. In case of a buffer, most of them take some default values, like in this example. Only Width is the proper length of the buffer, in bytes. All other members take some meaningful values when we create textures, as they have a more complex structure with more parameters needed to describe them. But in this article we focus on buffers.
- D3D12_HEAP_PROPERTIES 구조체는 버퍼를 할당할 메모리의 유형을 설명합니다.
- 예를 들어, GPU 메모리의 경우 D3D12_HEAP_TYPE_DEFAULT, 시스템 메모리의 경우 **D3D12_HEAP_TYPE_UPLOAD**를 사용합니다.
- D3D12_RESOURCE_DESC 구조체는 리소스의 모든 매개변수를 설명합니다.
- 버퍼의 경우, 대부분 기본값을 사용하며, 예제와 같이 **Width**만이 버퍼의 정확한 길이(바이트 단위)를 나타냅니다.
- 텍스처를 생성할 때는 모든 다른 멤버가 더 의미 있는 값을 가지며, 텍스처는 더 복잡한 구조와 더 많은 매개변수를 필요로 합니다.
- 그러나 이 기사에서는 버퍼에 집중합니다.
Creating all the resources this way is fine in D3D12, as we don’t have a small upper limit on the number of allocations we can make – unlike in Vulkan, where the limit is only 4096 on most GPUs. Nonetheless, memory allocation can be a slow operation, so it might be more efficient to pre-allocate larger memory blocks and dedicate parts of them to our small textures and buffers. <CommercialBreak> There is a free library that can help with this: D3D12 Memory Allocator from AMD :) </CommercialBreak>
- 이 방식으로 모든 리소스를 생성하는 것은 D3D12에서 괜찮습니다.
- Vulkan과 달리 D3D12에서는 할당할 수 있는 수에 대한 작은 상한선이 없습니다
- https://www.vulkan.gpuinfo.org/displaydevicelimit.php?name=maxMemoryAllocationCount&platform=windows
- (대부분의 GPU에서는 4096이 한계입니다).
- 그럼에도 불구하고 메모리 할당은 느린 작업일 수 있으므로,
- 더 큰 메모리 블록을 미리 할당하고 그 일부를 작은 텍스처와 버퍼에 할당하는 것이 더 효율적일 수 있습니다.
- 이를 도와줄 수 있는 무료 라이브러리가 있습니다: AMD의 D3D12 메모리 얼로케이터 :)
If we want to do it by ourselves, we take the second path, where another object appears – a ID3D12Heap. It represents a block of memory allocated by D3D12. Having such heap and an offset at which we want to place our new buffer inside of it, we can create the buffer like this, using function ID3D12Device::CreatePlacedResource, which could be much faster:
- 직접 처리하고 싶다면 두 번째 경로를 선택하게 되며,
- 여기서는 **ID3D12Heap**이라는 다른 객체가 등장합니다.
- 이 객체는 D3D12에 의해 할당된 메모리 블록을 나타냅니다.
- 이러한 힙과 새 버퍼를 배치할 오프셋을 가진 상태에서,
- ID3D12Device::CreatePlacedResource 함수를 사용하여 버퍼를 생성할 수 있습니다. 이 방법은 훨씬 빠를 수 있습니다.
ID3D12Heap* heap = ...
UINT64 offsetInHeap = ...
ID3D12Resource* constBuf;
HRESULT res = device->CreatePlacedResource(
heap,
offsetInHeap,
&resDesc,
D3D12_RESOURCE_STATE_COPY_DEST, // InitialResourceState
nullptr, // pOptimizedClearValue
IID_PPV_ARGS(&constBuf));
Now we have to learn how to allocate the heap. This is quite easy. The only caveat is that we should check D3D12_FEATURE_DATA_D3D12_OPTIONS::ResourceHeapTier during initialization of our app. When it is D3D12_RESOURCE_HEAP_TIER_2, we can freely mix all kinds of resources in one heap. However, if we find its value to be D3D12_RESOURCE_HEAP_TIER_1 (e.g. on NVIDIA GeForce GTX 900 series and earlier), we have to keep 3 types of resources (buffers, render target or depth stencil textures, other textures) in separate heaps and create them with flags like D3D12_HEAP_FLAG_ALLOW_ONLY_BUFFERS in this example. The D3D12MA library would do it automatically.
- 이제 힙을 할당하는 방법을 배워야 합니다. 이것은 아주 쉽습니다.
- 유일한 주의 사항은 애플리케이션 초기화 시 **D3D12_FEATURE_DATA_D3D12_OPTIONS::ResourceHeapTier**를 확인해야 한다는 점입니다.
- 이 값이 **D3D12_RESOURCE_HEAP_TIER_2**일 경우, 모든 종류의 리소스를 하나의 힙에서 자유롭게 혼합할 수 있습니다.
- 그러나 값이 **D3D12_RESOURCE_HEAP_TIER_1**인 경우(예: NVIDIA GeForce GTX 900 시리즈 및 그 이전 모델에서는 이 값일 수 있습니다),
- 버퍼, 렌더 타겟 또는 깊이 스텐실 텍스처, 기타 텍스처의 3가지 유형을 별도의 힙에 보관해야 하며,
- 이 예제에서는 **D3D12_HEAP_FLAG_ALLOW_ONLY_BUFFERS**와 같은 플래그를 사용하여 생성해야 합니다.
- D3D12MA 라이브러리는 이를 자동으로 처리해줍니다.
D3D12_HEAP_DESC heapDesc = {};
heapDesc.SizeInBytes = 64ull * 1024 * 1024; // 64 MB
heapDesc.Properties.Type = D3D12_HEAP_TYPE_DEFAULT;
heapDesc.Alignment = D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT;
heapDesc.Flags = D3D12_HEAP_FLAG_ALLOW_ONLY_BUFFERS;
ID3D12Heap* heap;
HRESULT res = device->CreateHeap(&heapDesc, IID_PPV_ARGS(&heap));
To have the whole approach B) fully described, the only remaining part is the way to transfer our data from raw CPU void* pointer to the constant buffer. Again, there are two ways to do it. If the buffer was created in the CPU memory, like D3D12_HEAP_TYPE_UPLOAD, we can call ID3D12Resource::Map to “map” its memory, get a raw CPU pointer to its data and just do memcpy. GPU executing our shader can then read straight from this buffer, but the access will cause transfers over PCI Express bus, which may be slow.
- 전체 접근 방식 B)를 완전히 설명하기 위해, 데이터 전송 방법에 대해 알아보겠습니다.
- 데이터는 원시 CPU void* 포인터에서 상수 버퍼로 전송됩니다.
- 이 작업에는 두 가지 방법이 있습니다.
- 첫째, 버퍼가 CPU 메모리에서 생성된 경우(예: D3D12_HEAP_TYPE_UPLOAD), **ID3D12Resource::Map**을 호출하여 메모리를 "매핑"하고 데이터에 대한 원시 CPU 포인터를 얻은 다음 **memcpy**를 수행할 수 있습니다.
- GPU에서 셰이더를 실행할 수 있으며, 이 버퍼에서 직접 읽을 수 있지만,
- PCI Express 버스를 통한 전송이 필요하므로 속도가 느릴 수 있습니다.
The other option is to have another “staging” buffer created in CPU memory (D3D12_HEAP_TYPE_UPLOAD), our main buffer created in GPU memory (D3D12_HEAP_TYPE_DEFAULT) and explicitly issue a copy operation between them, to be executed at the optimal moment. It can be a call to ID3D12GraphicsCommandList::CopyResource or some other copy function, e.g. CopyBufferRegion.
- 다른 옵션은 CPU 메모리에서 또 다른 "스테이징" 버퍼(D3D12_HEAP_TYPE_UPLOAD)를 생성하고,
- GPU 메모리에서 주요 버퍼(D3D12_HEAP_TYPE_DEFAULT)를 생성한 다음,
- 이들 간의 복사 작업을 명시적으로 수행하는 것입니다.
- 이 작업은 최적의 시점에 실행될 수 있습니다.
- 이때 ID3D12GraphicsCommandList::CopyResource 또는 다른 복사 함수(예: CopyBufferRegion)를 호출할 수 있습니다.
Note that in the diagram, “Map()” lies on the side of “Preparation (CPU)”, which means it happens at the exact moment we call this code in C++. On the other hand, “CopyResource()” is a command recorded to the command list and executed later by the GPU, so it lies on the green side. We can record such copy operation to a command list executed on the graphics queue, to happen at the exact moment between our draw calls, or do it on copy queue or async compute queue, to happen asynchronously. You can watch my talk “Efficient Use of GPU Memory in Modern Games” to learn which way is more efficient in what cases.
- 다이어그램에서 "Map()"은 "Preparation (CPU)" 측에 위치합니다.
- 이는 C++에서 이 코드를 호출하는 정확한 순간에 발생함을 의미합니다.
- 반면에 "CopyResource()"는 명령 리스트에 기록된 명령으로 GPU가 나중에 실행하므로, 녹색 측에 위치합니다.
- 이러한 복사 작업을 그래픽스 큐에서 드로우 호출 사이의 정확한 시점에 실행되도록 명령 리스트에 기록하거나, 복사 큐 또는 비동기 컴퓨트 큐에서 비동기적으로 수행할 수 있습니다.
- 어떤 방식이 더 효율적인지는 제 강연 "Efficient Use of GPU Memory in Modern Games"에서 확인할 수 있습니다.
Another important thing to note is that in the diagram, “constBuf” lies on the edge between the realm of command list execution and the pure CPU code. It means that the buffer itself (the one that we pass as the root argument) and the data inside of it (at least the part that we read in the shader) must remain alive and unchanged from the moment we submit the command buffer for execution until we are sure it finished executing on the GPU. Otherwise, bad things may happen, resulting in non-deterministic, hard to find bugs. We can ensure this e.g. by allocating and freeing data in a ring-buffer fashion.
- 또한, 다이어그램에서 "constBuf"는 명령 리스트 실행 영역과 순수 CPU 코드 영역 사이의 경계에 위치합니다.
- 이는 버퍼 자체(루트 인수로 전달된 버퍼)와 그 안의 데이터(셰이더에서 읽는 부분)가 명령 버퍼를 제출하여 실행될 때부터 GPU에서 실행이 완료될 때까지 살아 있고 변경되지 않아야 함을 의미합니다.
- 그렇지 않으면 비결정적이고 찾기 어려운 버그가 발생할 수 있습니다.
- 이를 보장하기 위해 링 버퍼 방식으로 데이터를 할당하고 해제하는 방법을 사용할 수 있습니다.
It is time to move on to the last approach C), called “DESCRIPTOR_TABLE”. It will use everything we learned so far plus additional level of indirection in form of a descriptor heap. Although the most complicated, this is also the recommended way for typical use cases, as it puts the least pressure on the precious space in the root signature. One root parameter can describe the an entire set of shader registers and they can be even indexed dynamically in the shader (watch out for NonUniformResourceIndex when doing this!). Still, we will limit ourselves to just a single register b3 in this example, for simplicity.
- 이제 마지막 접근 방식 C)인 "DESCRIPTOR_TABLE"으로 넘어가겠습니다.
- 이 방법은 지금까지 배운 모든 것을 사용하며, 추가적인 간접 참조 레벨인 디스크립터 힙을 포함합니다.
- 가장 복잡하지만 일반적인 사용 사례에는 권장되는 방법이며, 루트 서명에서 소중한 공간에 가장 적은 압박을 가합니다.
- 하나의 루트 파라미터로 전체 셰이더 레지스터 세트를 설명할 수 있으며,
- 셰이더에서 동적으로 인덱싱할 수도 있습니다(이때 **NonUniformResourceIndex**에 주의하세요!).
- 그래도 예제를 단순하게 유지하기 위해 하나의 레지스터 b3만 사용할 것입니다.
D3D12_DESCRIPTOR_RANGE descRange = {};
descRange.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
descRange.NumDescriptors = 1;
descRange.BaseShaderRegister = 3;
descRange.RegisterSpace = 0;
descRange.OffsetInDescriptorsFromTableStart = 0;
params[2].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
params[2].ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX;
params[2].DescriptorTable.NumDescriptorRanges = 1;
params[2].DescriptorTable.pDescriptorRanges = &descRange;
This time the ParameterType is D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE. We have another (surprise!) level of indirection here which allows us to specify an entire array of “descriptor ranges”, but we specify just one. Each descriptor range specifies shader register, space, number of descriptors, and an additional offset, which I will mention later.
- 이번에는 **ParameterType**이 **D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE**입니다.
- 여기에는 또 다른(놀랍게도!) 간접 참조 레벨이 있어 전체 "디스크립터 범위" 배열을 지정할 수 있습니다.
- 하지만 우리는 하나만 지정합니다.
- 각 디스크립터 범위는 셰이더 레지스터, 스페이스, 디스크립터의 수, 그리고 추가적인 오프셋을 지정합니다.
- 추가적인 오프셋에 대해서는 나중에 언급하겠습니다.
Now we must create descriptorHeap. An object of type ID3D12DescriptorHeap is a special piece of D3D12 memory allocated to store descriptors. In other words, what we’ve set directly as a “CBV” root argument in the approach B), we will now store in a special place that the root signature will access. While creating the descriptor heap, we need to specify the type of descriptors we need to store (either CBV + SRV + UAV, SAMPLER, RTV, or DSV) and its maximum capacity. Having such an object, we can just refer to its content by indexing the descriptors.
- 이제 descriptorHeap을 생성해야 합니다.
- ID3D12DescriptorHeap 타입의 객체는 디스크립터를 저장하기 위해 할당된 D3D12 메모리의 특수한 조각입니다.
- 즉, 접근 방식 B)에서 "CBV" 루트 인수로 직접 설정한 내용을 이제는 루트 서명이 접근할 수 있는 특별한 장소에 저장합니다.
- 디스크립터 힙을 생성할 때, 저장할 디스크립터의 유형(CBV + SRV + UAV, SAMPLER, RTV, 또는 DSV)과 최대 용량을 지정해야 합니다.
- 이러한 객체를 가진 후에는 디스크립터를 인덱싱하여 그 내용을 참조할 수 있습니다.
D3D12_DESCRIPTOR_HEAP_DESC descriptorHeapDesc = {};
descriptorHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
descriptorHeapDesc.NumDescriptors = 1000;
descriptorHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
descriptorHeapDesc.NodeMask = 0;
ID3D12DescriptorHeap* descriptorHeap;
HRESULT res = device->CreateDescriptorHeap(
&descriptorHeapDesc, IID_PPV_ARGS(&descriptorHeap)));
To set our root argument 2 to use a specific descriptor from our newly created descriptor heap, we have to call two functions. First, ID3D12GraphicsCommandList::SetDescriptorHeaps, just sets the whole descriptor heap as the current one. There can be only one descriptor heap of type CBV_SRV_UAV and one of type SAMPLER set at a time, so you must keep all the descriptors you may need for a draw call in one big heap.
- 루트 인수 2를 새로 생성한 디스크립터 힙에서 특정 디스크립터를 사용하도록 설정하려면 두 가지 함수를 호출해야 합니다.
- 먼저, **ID3D12GraphicsCommandList::SetDescriptorHeaps**를 호출하여 전체 디스크립터 힙을 현재 힙으로 설정합니다.
- 한 번에 설정할 수 있는 디스크립터 힙은 CBV_SRV_UAV 타입의 하나와 SAMPLER 타입의 하나뿐입니다.
- 따라서 드로우 호출에 필요한 모든 디스크립터를 하나의 큰 힙에 유지해야 합니다.
commandList->SetDescriptorHeaps(1, &descriptorHeap);
The second call is to function ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable. It sets our specific root argument 2 to point to a specific descriptor in the heap. One caveat is that the function expects a D3D12_GPU_DESCRIPTOR_HANDLE, which is a structure containing (again!) a raw memory address. We can fetch an address of the beginning of the heap by calling ID3D12DescriptorHeap::GetGPUDescriptorHandleForHeapStart. Then we need to increment it by the index of our descriptor in the heap, multiplied by a size of a descriptor, as the address is expressed in bytes.
- 두 번째 호출은 ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable 함수입니다.
- 이 함수는 특정 루트 인수 2를 힙 내의 특정 디스크립터를 가리키도록 설정합니다.
- 주의할 점은 이 함수가 **D3D12_GPU_DESCRIPTOR_HANDLE**을 기대한다는 것입니다.
- 이 구조체는 (다시!) 원시 메모리 주소를 포함하고 있습니다.
- 힙의 시작 주소를 가져오려면 **ID3D12DescriptorHeap::GetGPUDescriptorHandleForHeapStart**를 호출하면 됩니다.
- 그런 다음, 주소를 힙에서 디스크립터의 인덱스만큼 증가시켜야 합니다.
- 이때 인덱스에 디스크립터의 크기를 곱해야 합니다.
- 주소는 바이트 단위로 표현됩니다.
Descriptors of a specific kind have a constant size that can be fetched using function ID3D12Device::GetDescriptorHandleIncrementSize. We can do it just once, during program startup. The value is implementation-defined, but we can expect it to be fairly small. For example, on the PC that I now have handy, with GeForce RTX 2080 Ti card installed, the CBV_SRV_UAV descriptor size is 32 bytes, so there should be no problem to use a heap capable of storing many thousands of such descriptors.
- 특정 종류의 디스크립터는 일정한 크기를 가지며,
- 이 크기는 ID3D12Device::GetDescriptorHandleIncrementSize 함수를 사용하여 가져올 수 있습니다.
- 이 작업은 프로그램 시작 시 한 번만 수행하면 됩니다.
- 값은 구현에 따라 정의되지만, 대체로 꽤 작습니다.
- 예를 들어, 제가 현재 사용하고 있는 GeForce RTX 2080 Ti 카드가 설치된 PC에서는 CBV_SRV_UAV 디스크립터의 크기가 32바이트이므로, 수천 개의 디스크립터를 저장할 수 있는 힙을 사용하는 데는 문제가 없을 것입니다.
D3D12_GPU_DESCRIPTOR_HANDLE GPUDescriptorHandle =
descriptorHeap->GetGPUDescriptorHandleForHeapStart();
GPUDescriptorHandle.ptr += descriptorIndex *
device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
commandList->SetGraphicsRootDescriptorTable(
2, // RootParameterIndex
&GPUDescriptorHandle); // BaseDescriptor
Note that although the function is called SetGraphicsRootDescriptorTable, there is no such thing as “descriptor table” as a separate type of object. A “descriptor table” is just a sequence of 1 or more descriptors in a descriptor heap, denoted by an address of the first one.
- 함수 이름이 **SetGraphicsRootDescriptorTable**이라고 해서 "디스크립터 테이블"이라는 별도의 타입의 객체가 있는 것은 아닙니다.
- "디스크립터 테이블"은 단지 디스크립터 힙 내의 1개 이상의 디스크립터 시퀀스를 의미하며,
- 첫 번째 디스크립터의 주소로 식별됩니다.
Note also that while D3D12_GPU_VIRTUAL_ADDRESS was just a typedef to UINT64, D3D12_GPU_DESCRIPTOR_HANDLE (and D3D12_CPU_DESCRIPTOR_HANDLE described later) are structures that have the address as member ptr. It is a bit of inconsistency, but it makes sense for them to be different types so the developer doesn’t confuse them by accident.
- 또한, **D3D12_GPU_VIRTUAL_ADDRESS**는 단순히 **UINT64**의 typedef이지만,
- D3D12_GPU_DESCRIPTOR_HANDLE (그리고 나중에 설명할 D3D12_CPU_DESCRIPTOR_HANDLE)는 주소를 멤버 ptr로 갖는 구조체입니다.
- 이는 약간의 불일치가 있지만, 개발자가 실수로 혼동하지 않도록 서로 다른 타입으로 설계된 것입니다.
Note also that despite similar names, ID3D12DescriptorHeap is not a kind of ID3D12Heap, but a completely separate type of object. ID3D12Heap is a piece of D3D12 memory intended to create “placed” resources (buffers and textures) in it, while ID3D12DescriptorHeap is used for storing descriptors. Flags for selecting memory type are also different. ID3D12Heap uses D3D12_HEAP_TYPE_DEFAULT, UPLOAD (mentioned above), or READBACK, while ID3D12DescriptorHeap just accepts D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE set or not set (which is described below)
- 또한, 비슷한 이름에도 불구하고 **ID3D12DescriptorHeap**은 **ID3D12Heap**의 일종이 아니라 완전히 별개의 타입의 객체입니다.
- **ID3D12Heap**은 "배치된" 리소스(버퍼 및 텍스처)를 생성하기 위한 D3D12 메모리 조각인 반면,
- **ID3D12DescriptorHeap**은 디스크립터를 저장하는 데 사용됩니다.
- 메모리 타입 선택을 위한 플래그도 다릅니다.
- **ID3D12Heap**은 D3D12_HEAP_TYPE_DEFAULT, UPLOAD (앞서 언급한) 또는 **READBACK**을 사용하며,
- **ID3D12DescriptorHeap**은 단지 D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE 플래그를 설정하거나 설정하지 않는 것을 수용합니다
- (아래에 설명됩니다).
We discussed the creation of the descriptor heap, as well as setting it as the root parameter. One thing that remains is actually filling the contents of the descriptor. Similarly to what we did with a root descriptor in approach B) by calling SetGraphicsRootConstantBufferView, a descriptor in a descriptor heap can be initialized using function ID3D12Device::CreateConstantBufferView.
- 디스크립터 힙의 생성과 이를 루트 파라미터로 설정하는 방법을 논의했습니다.
- 남은 작업은 실제로 디스크립터의 내용을 채우는 것입니다.
- 접근 방식 B)에서 **SetGraphicsRootConstantBufferView**를 호출하여 루트 디스크립터를 설정한 것과 유사하게,
- 디스크립터 힙의 디스크립터는 ID3D12Device::CreateConstantBufferView 함수를 사용하여 초기화할 수 있습니다.
In the following code, structure D3D12_CONSTANT_BUFFER_VIEW_DESC describes the parameters of the constant buffer our descriptor should point to, including its GPU virtual address and size. Second parameter of the CreateConstantBufferView function is the address of the descriptor that should be written. Once again, it has to be a memory address in bytes, so we need to fetch the address of the heap start and then increment it by the descriptor index multiplied by the size of a descriptor. This time, however, it is a CPU address.
- 다음 코드에서는 D3D12_CONSTANT_BUFFER_VIEW_DESC 구조체가 디스크립터가 가리킬 상수 버퍼의 매개변수를 설명합니다.
- 여기에는 GPU 가상 주소와 크기가 포함됩니다.
- CreateConstantBufferView 함수의 두 번째 매개변수는 작성할 디스크립터의 주소입니다.
- 다시 말해, 이 주소는 바이트 단위로 표현된 메모리 주소여야 하므로, 힙의 시작 주소를 가져오고 디스크립터 인덱스와 디스크립터 크기를 곱하여 주소를 증가시켜야 합니다.
- 이번에는 CPU 주소입니다.
D3D12_GPU_VIRTUAL_ADDRESS constBufGPUAddr = constBuf->GetGPUVirtualAddress();
D3D12_CONSTANT_BUFFER_VIEW_DESC CBVDesc = {};
CBVDesc.BufferLocation = constBufGPUAddr;
CBVDesc.SizeInBytes = sizeof(glm::mat4);
D3D12_CPU_DESCRIPTOR_HANDLE CPUDescriptorHandle =
descriptorHeap->GetCPUDescriptorHandleForHeapStart();
CPUDescriptorHandle.ptr += descriptorIndex *
device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
device->CreateConstantBufferView(&CBVDesc, CPUDescriptorHandle);
Note that in the considerations above, we met 3 different ways of indexing descriptors inside the heap, which should not be confused:
- When setting some descriptor as a root parameter using SetGraphicsRootDescriptorTable, we use an address of type D3D12_GPU_VIRTUAL_ADDRESS (note the “GPU” part of the address type). It is expressed in bytes, so any descriptor index has to be multiplied by the “DescriptorHandleIncrementSize”. This operation is a command recorded to a command buffer, hence it lies on the green background.
- When filling a descriptor using CreateConstantBufferView, we use the address of type D3D12_CPU_VIRTUAL_ADDRESS. It is also expressed in bytes, so a descriptor index has to be multiplied by the same “DescriptorHandleIncrementSize” – this value is the same on CPU and GPU side. This operation happens immediately on the CPU, hence it lies on the blue background.
- An additional offset can be applied from the descriptor we set as the root parameter to the one that will be accessed by the shader, by filling D3D12_DESCRIPTOR_RANGE::OffsetInDescriptorsFromTableStart. As its name says, it is expressed in descriptors, not bytes, so it doesn’t need to be multiplied by anything. In our example, we just set it to 0.
위의 설명에서는 힙 내의 디스크립터를 인덱싱하는 세 가지 다른 방법에 대해 설명하고 있습니다.
이 방법들을 혼동하지 않도록 주의해야 합니다.
- SetGraphicsRootDescriptorTable을 사용하여 어떤 디스크립터를 루트 매개변수로 설정할 때는
- D3D12_GPU_VIRTUAL_ADDRESS 타입의 주소를 사용합니다(여기서 "GPU" 부분에 주목하세요).
- 이 주소는 바이트 단위로 표현되므로 디스크립터 인덱스에 "DescriptorHandleIncrementSize"를 곱해야 합니다.
- 이 작업은 명령이 커맨드 버퍼에 기록되는 과정에서 이루어지므로, 이 부분은 초록색 배경에 해당합니다.
- CreateConstantBufferView를 사용하여 디스크립터를 채울 때는
- D3D12_CPU_VIRTUAL_ADDRESS 타입의 주소를 사용합니다.
- 이 또한 바이트 단위로 표현되므로, 디스크립터 인덱스에 같은 "DescriptorHandleIncrementSize"를 곱해야 합니다.
- 이 값은 CPU와 GPU 측 모두에서 동일합니다.
- 이 작업은 CPU에서 즉시 이루어지며, 따라서 이 부분은 파란색 배경에 해당합니다.
- 셰이더에 의해 접근될 디스크립터와 루트 매개변수로 설정된 디스크립터 사이에 추가적인 오프셋을 적용할 수 있습니다.
- 이 오프셋은 **D3D12_DESCRIPTOR_RANGE::OffsetInDescriptorsFromTableStart**를 사용해 설정하며,
- 이름에서 알 수 있듯이 바이트가 아닌 디스크립터 단위로 표현되므로 아무것도 곱할 필요가 없습니다.
- 예제에서는 이 값을 0으로 설정했습니다.
Note that “descriptorHeap” is depicted at the edge between command buffer and CPU realm, just like “constBuf”. It means that the object as a whole, as well as its descriptors that are referred by our shader, also needs to remain alive and unchanged from the moment we submit the command buffer for execution until we are sure it finished executing on the GPU. Otherwise, bad things will certainly happen. This is very important, as accessing bad descriptors is a notorious source of GPU crashes, while D3D Debug Layer cannot detect them, only GPU-based validation can!
- 또한, "descriptorHeap"은 명령 버퍼와 CPU 영역 사이의 경계에 위치한 것으로 나타나 있으며, "constBuf"도 마찬가지입니다.
- 이는 전체 객체뿐만 아니라 셰이더에 의해 참조되는 디스크립터도 커맨드 버퍼가 실행되기 위해 제출된 순간부터 GPU에서 실행이 완료될 때까지 계속해서 살아 있고 변경되지 않은 상태로 유지되어야 함을 의미합니다.
- 그렇지 않으면 GPU 충돌과 같은 심각한 문제가 발생할 수 있습니다.
- 잘못된 디스크립터에 접근하는 것은 GPU 충돌의 주요 원인이 되며,
- D3D Debug Layer는 이를 감지할 수 없고 GPU 기반 검증만이 이를 감지할 수 있다는 점에서 매우 중요합니다.
Similarly to the constant buffer, a descriptor heap can also be set up in two different ways. A more direct way is to create just one heap with D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE and write to it using functions like CreateConstantBufferView, as in the example above. How descriptor heaps work internally is an implementation detail, but we can imagine a possible implementation of storing them in the special region of GPU memory visible to the CPU called Base Address Register (BAR). Then, the CreateConstantBufferView call would write data through PCI Express bus.
- 상수 버퍼와 유사하게, 디스크립터 힙도 두 가지 다른 방법으로 설정할 수 있습니다.
- 보다 직접적인 방법은 D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE 플래그를 사용하여 하나의 힙만 생성하고,
- 위의 예제처럼 CreateConstantBufferView 같은 함수를 사용해 데이터를 쓰는 것입니다.
- 디스크립터 힙이 내부적으로 어떻게 작동하는지는 구현 세부사항에 해당하지만,
- 이를 CPU가 볼 수 있는 GPU 메모리의 특수 영역(Base Address Register, BAR)에 저장하는 가능한 구현을 상상할 수 있습니다.
- 그러면 CreateConstantBufferView 호출은 PCI Express 버스를 통해 데이터를 쓰는 방식이 됩니다.
The other option is to create one “staging” descriptor heap without D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE flag that will probably end up in the system memory, another one with this flag, and do a copy between them using function ID3D12Device::CopyDescriptorsSimple or, less efficient and thus not recommended, CopyDescriptors. Note that unlike CopyResource function mentioned earlier, copying descriptors is a CPU operation happening immediately, not a command recorded to a command buffer, thus the loop arrow lies on the blue background.
- 다른 방법은 D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE 플래그가 없는 "staging(스테이징)" 디스크립터 힙을 시스템 메모리에 만들고,
- 이 플래그가 있는 또 다른 힙을 생성한 후,
- ID3D12Device::CopyDescriptorsSimple 함수를 사용해 이들 사이에 복사하는 것입니다.
- 덜 효율적이므로 권장되지 않지만 **CopyDescriptors**를 사용할 수도 있습니다.
- CopyResource 함수와는 달리 디스크립터를 복사하는 작업은 CPU에서 즉시 이루어지며,
- 이는 커맨드 버퍼에 기록되는 명령이 아니므로 파란색 배경에 해당하는 루프 화살표가 표시됩니다.
This is all. We went through the whole diagram. Congratulations on reading that far! Let me now mention briefly how binding other types of resources differs from binding a constant buffer we described above. Obviously, the A) “32BIT_CONSTANT” variant is available only for constants. Other types of resources can only use B) or C).
- 여기까지가 전체 다이어그램을 설명한 내용입니다. 이렇게 멀리까지 읽어주셔서 감사합니다!
- 이제 상수 버퍼와 다른 종류의 리소스를 바인딩하는 방법이 어떻게 다른지 간략하게 언급하겠습니다.
- 당연히 A) "32BIT_CONSTANT" 방식은 상수에만 사용할 수 있으며,
- 다른 종류의 리소스는 B) 또는 C)만 사용할 수 있습니다.
With buffers and textures bound as Shader Resource View (SRV), we define them in HLSL code as global variables of type: Texture1D, Texture2D, Texture3D, Texture2DMS (for multisampled), TextureCube, Texture2DArray etc. or StructuredBuffer, ByteAddressBuffer and assign them a register like t3. On the CPU side, we keep them in the same descriptor heap of type D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV. When root parameter is of type D3D12_ROOT_PARAMETER_TYPE_SRV, a root descriptor can be set up using function ID3D12GraphicsCommandList::SetGraphicsRootShaderResourceView (textures are not supported here, only buffers). In case of using a descriptor table, a descriptor in a descriptor heap is set up using ID3D12Device::CreateShaderResourceView. The D3D12_SHADER_RESOURCE_VIEW_DESC we have to fill is more extensive than in case of constant buffers and gives an opportunity to specify a “view” into a texture limited to certain mip levels or array slices.
- 버퍼와 텍스처를 **Shader Resource View (SRV)**로 바인딩할 때,
- 이를 HLSL 코드에서 Texture1D, Texture2D, Texture3D, Texture2DMS(멀티샘플링의 경우), TextureCube, Texture2DArray 등 또는 StructuredBuffer, ByteAddressBuffer와 같은 전역 변수로 정의하고, t3과 같은 레지스터에 할당합니다.
- CPU 측에서는 D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV 유형의 동일한 디스크립터 힙에 보관합니다.
- 루트 파라미터가 D3D12_ROOT_PARAMETER_TYPE_SRV 유형일 때,
- ID3D12GraphicsCommandList::SetGraphicsRootShaderResourceView 함수를 사용하여 루트 디스크립터를 설정할 수 있습니다
- (이 경우 텍스처는 지원되지 않으며, 오직 버퍼만 지원됩니다).
- 디스크립터 테이블을 사용하는 경우, ID3D12Device::CreateShaderResourceView 함수를 사용하여 디스크립터 힙에 디스크립터를 설정합니다.
- 이때 작성해야 하는 D3D12_SHADER_RESOURCE_VIEW_DESC는 상수 버퍼의 경우보다 더 포괄적이며,
- 특정 mip 레벨이나 배열 슬라이스로 제한된 텍스처에 대한 "뷰"를 지정할 수 있는 기회를 제공합니다.
Buffers and textures can also be bound as Unordered Access View (UAV), which allows to read as well as write them in a shader. For this, a global variable must be defined in HLSL of type RWTexture2D, RWStructuredBuffer, RWByteAddressBuffer etc. with a register assigned like u3. On the CPU side, we keep them in the same descriptor heap of type D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV. When root parameter is of type D3D12_ROOT_PARAMETER_TYPE_UAV, a root descriptor can be set up using function ID3D12GraphicsCommandList::SetGraphicsRootUnorderedAccessView (again, only buffers are supported). In case of using a descriptor table, a descriptor in a descriptor heap is set up using ID3D12Device::CreateUnorderedAccessView.
- 버퍼와 텍스처는 **Unordered Access View (UAV)**로도 바인딩할 수 있으며,
- 이를 통해 셰이더에서 읽기와 쓰기가 모두 가능합니다.
- 이를 위해 HLSL에서 RWTexture2D, RWStructuredBuffer, RWByteAddressBuffer 등과 같은 유형의 전역 변수를 정의하고 u3과 같은 레지스터를 할당해야 합니다.
- CPU 측에서는 D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV 유형의 동일한 디스크립터 힙에 보관합니다.
- 루트 파라미터가 D3D12_ROOT_PARAMETER_TYPE_UAV 유형일 때, ID3D12GraphicsCommandList::SetGraphicsRootUnorderedAccessView 함수를 사용하여 루트 디스크립터를 설정할 수 있습니다
- (이 경우에도 텍스처는 지원되지 않으며, 오직 버퍼만 지원됩니다).
- 디스크립터 테이블을 사용하는 경우, ID3D12Device::CreateUnorderedAccessView 함수를 사용하여 디스크립터 힙에 디스크립터를 설정합니다.
Samplers are different, because a sampler is just a set of parameters used when sampling a texture, so there are no data that it would refer to. Everything is stored in the sampler itself, which can be compared to the parameters passed while setting up a descriptor. In a shader, you need to define a global variable of type SamplerState and assign it a register like s3. On the CPU side, there must be a separate descriptor heap created for them of type D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER. The size of a single sampler can also be different, so you need to fetch it using device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER). Setting up a sampler is done using function ID3D12Device::CreateSampler. You then need to set this descriptor set with samplers, along with the one for CBV_SRV_UAV, to ID3D12GraphicsCommandList::SetDescriptorHeaps and point a root argument to a specific sampler using SetGraphicsRootDescriptorTable.
- 샘플러는 다릅니다. 샘플러는 텍스처를 샘플링할 때 사용되는 매개변수 집합일 뿐이므로 참조할 데이터가 없습니다.
- 모든 것이 샘플러 자체에 저장되며, 이는 디스크립터를 설정할 때 전달되는 매개변수와 비교할 수 있습니다.
- 셰이더에서 SamplerState 유형의 전역 변수를 정의하고 s3과 같은 레지스터를 할당해야 합니다.
- CPU 측에서는 D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER 유형의 별도 디스크립터 힙을 생성해야 합니다.
- 단일 샘플러의 크기도 다를 수 있으므로,
- device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER) 함수를 사용하여 이를 가져와야 합니다.
- 샘플러 설정은 ID3D12Device::CreateSampler 함수를 사용하여 수행합니다.
- 그런 다음, 샘플러용 디스크립터 세트와 함께 CBV_SRV_UAV용 디스크립터 세트를 ID3D12GraphicsCommandList::SetDescriptorHeaps 함수에 설정하고,
- SetGraphicsRootDescriptorTable 함수를 통해 루트 인수에 특정 샘플러를 지정해야 합니다.
There is also a shortcut called “static samplers”. You can define a sampler differently, without using a descriptor heap, by filling member D3D12_ROOT_SIGNATURE_DESC::pStaticSamplers and NumStaticSamplers. Then, parameters of a sampler are “baked” into the root signature.
- 또한 "정적 샘플러"라는 단축 방법이 있습니다.
- 디스크립터 힙을 사용하지 않고 D3D12_ROOT_SIGNATURE_DESC::pStaticSamplers와 NumStaticSamplers 멤버를 채워 샘플러를 다르게 정의할 수 있습니다.
- 그런 다음 샘플러의 매개변수는 루트 서명에 "베이크"됩니다.
Finally, Render Target View (RTV) and Depth Stencil View (DSV) are special kinds of descriptors. We also need to create a descriptor heap for them, of type D3D12_DESCRIPTOR_HEAP_TYPE_RTV or D3D12_DESCRIPTOR_HEAP_TYPE_DSV, respectively, but unlike CBV_SRV_UAV and SAMPLER we don’t need to set such descriptor heap as “current” using ID3D12GraphicsCommandList::SetDescriptorHeaps. We can directly pass CPU addresses of these descriptors (of type D3D12_CPU_DESCRIPTOR_HANDLE) to functions that operate on render-target or depth-stencil textures, like ID3D12GraphicsCommandList::ClearRenderTargetView, ClearDepthStencilView, OMSetRenderTargets.
- 마지막으로, Render Target View (RTV)와 Depth Stencil View (DSV)는 특별한 종류의 디스크립터입니다.
- 이들에 대해서도 각각 D3D12_DESCRIPTOR_HEAP_TYPE_RTV 또는 D3D12_DESCRIPTOR_HEAP_TYPE_DSV 유형의 디스크립터 힙을 생성해야 하지만,
- CBV_SRV_UAV 및 SAMPLER과 달리 ID3D12GraphicsCommandList::SetDescriptorHeaps를 사용해 해당 디스크립터 힙을 "현재"로 설정할 필요는 없습니다.
- ID3D12GraphicsCommandList::ClearRenderTargetView, ClearDepthStencilView, OMSetRenderTargets와 같은 렌더 타겟 또는 깊이 스텐실 텍스처에 대한 작업을 수행하는 함수에 이러한 디스크립터의 CPU 주소(D3D12_CPU_DESCRIPTOR_HANDLE 유형)를 직접 전달할 수 있습니다.
You can also read article “Resource binding in HLSL” in Microsoft documentation about a similar topic, and my new article: “Shapes and forms of DX12 root signatures”.
비슷한 주제에 대한 Microsoft 문서에서 "HLSL의 리소스 바인딩" 문서와 저의 새 문서도 읽어볼 수 있습니다:
"DX12 루트 서명의 모양과 형태".