Jorge
Jorge Autor de Aprende Unreal Engine 4.

DirectX 12: Descriptors

DirectX 12: Descriptors

Para poder usar los recursos almacenados en la memoria de la GPU necesitamos descriptores. En este tutorial veremos que son y como crearlos.

Tabla de contenidos

Parte 1. Window

Parte 2. Adapter

Parte 3. Conceptos

Parte 4. Queue

Parte 5. Resources

» Parte 6. Descriptores

Parte 7. Pipeline State

Parte 8. Swapchain

Parte 9. Triángulo

Parte 10. Root Signature

Parte 11. Texturas

¿Qué es un descriptor?

De los tutoriales anteriores hemos conseguido reservar espacio en memoria de la GPU y copiar datos.

Sin embargo existe un problema. Todos los cálculos se hacen con los registros de la GPU. Obviamente no puedes almacenar una textura o una matrix de 4x4 floats en un registro de 128 bits.

Descriptor imposible cargar

Imposible cargar un recurso en un registro de la GPU.

¿Cómo lo hacemos?

La idea es usar un descriptor. Un descriptor es una pequeña estructura que almacena la dirección dónde está alojado el recurso y su tamaño.

Este descriptor es el que usará el shader para cargar en alguno de sus registros.

Descriptor cargar

El panorama queda así, tenemos una serie de recursos:

ComPtr<ID3D12Resource> VertexBuffer;
ComPtr<ID3D12Resource> IndexBuffer;
ComPtr<ID3D12Resource> ContantBuffer;
ComPtr<ID3D12Resource> RenderTarget;

Que para poder ser usados por la GPU debemos crear descriptores para cada uno:

D3D12_VERTEX_BUFFER_VIEW VertexBufferView; // descriptor para vertex buffer
D3D12_INDEX_BUFFER_VIEW VertexBufferView; // descriptor para index buffer
D3D12_CPU_DESCRIPTOR_HANDLE ConstantBufferView; // descriptor para constant buffer
D3D12_CPU_DESCRIPTOR_HANDLE RenderTargetView; // descriptor para render target

Aquí “view” es sinónimo de descriptor.

Los descriptores, a su vez, están alojados en memoria de la GPU. La GPU puede cargar estos descriptores en sus registros.

Veamos en detalles algunos descriptores.

Descriptor para Vertex Buffer

Vamos a crear un descriptor para un recurso vertex buffer.

Empezamos con el recurso. De anteriores tutoriales sabemos como crear un Vertex Buffer:

FVertex TriangleVertices[] = {
		...
};

ComPtr<ID3D12Resource> VertexBuffer;
// Alojado en memoria de la GPU y hemos copiado 
// el array TriangleVertices en VertexBuffer
// como hemos visto en tutoriales anteriores

// Vamos a crear un descriptor para que pueda usarlo la GPU
D3D12_VERTEX_BUFFER_VIEW VertexBufferView;

// un descriptor no es más que una dirección en memoria y el tamaño
VertexBufferView.BufferLocation = VertexBuffer->GetGPUVirtualAddress();
VertexBufferView.SizeInBytes = sizeof(TriangleVertices);
VertexBufferView.StrideInBytes = sizeof(FVertex);

¿Cómo usamos este descriptor?

Lo podemos usar como entrada de la etapa Input Assembler del pipeline:

CommandList->IASetVertexBuffers(
  0, /* input slot, usually 0 */
  1, /* number of vertex buffers descriptors in the array */
  &VertexBufferView
);

Descriptor para Index Buffer

Para crear un descriptor de un recurso Index Buffer:

DWORD Indices[] = {
	0, 1, 2, // first triangle
	0, 3, 1 // second triangle
};

ComPtr<ID3D12Resource> IndexBuffer;
// alojar memoria en la GPU para el recurso IndexBuffer
// y copiarle los datos de Indices, tal y como hemos visto
// en anteriores tutoriales

D3D12_INDEX_BUFFER_VIEW IndexBufferView;
IndexBufferView.BufferLocation = IndexBuffer->GetGPUVirtualAddress();
IndexBufferView.Format = DXGI_FORMAT_R32_UINT; // unsigned int 32 bits
IndexBufferView.SizeInBytes = sizeof(Indices);

¿Cómo usamos este descriptor?

Lo usamos para indicar la entrada de la etapa Input Assembler del pipeline:

CommandList->IASetVertexBuffers(0, 1, &VertexBufferView);
CommandList->IASetIndexBuffer(&IndexBufferView);	

Descriptor Heap

¿Recuerdas lo que dijimos anteriormente?

Los descriptores, a su vez, están alojados en memoria de la GPU. La GPU puede cargar estos descriptores en sus registros.

Sin embargo, no hemos alojado en memoria de la GPU ni VertexBufferView ni IndexBufferView.

Los descriptores para Vertex Buffer e Index Buffer son especiales tanto en cuanto se los pasamos directamente al pipeline con:

CommandList->IASetVertexBuffers(0, 1, &VertexBufferView);
CommandList->IASetIndexBuffer(&IndexBufferView);	

Estos comandos ya se ocupan de alojar los descriptores en memoria de la GPU y cargarlos en los registros correspondientes.

El resto de descriptores sí hay que alojarlos en memoria de la GPU.

Estos descriptores son del tipo:

D3D12_CPU_DESCRIPTOR_HANDLE ConstantBuffer, RenderTarget, ...;

El espacio de memoria dónde se alojan los descriptores se llama descriptor heap. Puedes ver un descriptor heap como un array de descriptores.

La clase que representa un descriptor heap es ID3D12DescriptorHeap.

Para crear un descriptor heap:

D3D12_DESCRIPTOR_HEAP_DESC DescriptorHeapDesc{};

DescriptorHeapDesc.NumDescriptors = ...;
/* numero de descriptores que va a alojar, podrias pensar en esta propiedad como tamaño del array */;

DescriptorHeapDesc.Type = ...;
/* El tipo de descriptor que va a alojar
  D3D12_DESCRIPTOR_HEAP_TYPE_RTV <- para descriptores render target
  D3D12_DESCRIPTOR_HEAP_TYPE_DSV <- para descriptores depth stencil
  D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER <- para descriptores sampler de texturas
  D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV <- para descriptores constantbuffer, shader view y unordered access
*/

DescriptorHeapDesc.Flags =  ...;
/* 
  D3D12_DESCRIPTOR_HEAP_FLAG_NONE <- los descriptores alojados en este heap no serán visibles a las etapas programables (shaders)
  D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE <- los descriptores alojados en este heap serán visibles a las etapas programables (shaders)
*/

DescriptorHeapDesc.NodeMask = 0;

ComPtr<ID3D12DescriptorHeap> DescriptorHeap;

Device->CreateDescriptorHeap(&DescriptorHeapDesc, IID_PPV_ARGS(&DescriptorHeap));

Veamos algunos ejemplos para clarificarlo:

Descriptor para Render Target

Un render target es la textura que escribe la etapa Output Merge como resultado de ejecutar el pipeline.

Del mismo modo que hemos indicado previamente las entradas para Input Assembler con:

CommandList->IASetVertexBuffers(0, 1, &VertexBufferView);
CommandList->IASetIndexBuffer(&IndexBufferView);	

Debemos indicar la salida de Output Merger:

D3D12_CPU_DESCRIPTOR_HANDLE RenderTargetView = ...;
D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView = ...;

CommandList->OMSetRenderTargets(
  1, // número de render targets
  &RenderTargetView,
  FALSE, // RTsSingleHandleToDescriptorRange
  &DepthStencilView
);

Referencia: https://docs.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12graphicscommandlist-omsetrendertargets

Los descriptores deben estar alojados en un heap:

ComPtr<ID3D12Resource> RenderTargets[4];
// Crear recursos render targets. En siguientes tutoriales veremos
// como hacerlo de manera efectiva.

ComPtr<ID3D12DescriptorHeap> RenderTargetViewHeap;

D3D12_DESCRIPTOR_HEAP_DESC RenderTargetViewHeapDesc{};
RenderTargetViewHeapDesc.NumDescriptors = kFrameCount;
RenderTargetViewHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
RenderTargetViewHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
RenderTargetViewHeapDesc.NodeMask = 0;
	
Device->CreateDescriptorHeap(&RenderTargetViewHeapDesc, IID_PPV_ARGS(&RenderTargetViewHeap));

Ya tenemos el descriptor heap que tiene almacenado kFrameCount descriptores.

Puedes acceder al primer descriptor alojado en el descriptor heap usando el método:

D3D12_CPU_DESCRIPTOR_HANDLE DescriptorView = RenderTargetViewHeap->GetCPUDescriptorHandleForHeapStart();

Si quieres el siguiente descriptor debes incrementar el puntero:

D3D12_CPU_DESCRIPTOR_HANDLE DescriptorView = RenderTargetViewHeap->GetCPUDescriptorHandleForHeapStart();

UINT HeapIncrementSize = Device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

DescriptorView.ptr += HeapIncrementSize; 
// DescriptorView es ahora el segundo descriptor alojado en el heap

Pero estos descriptores aún no están asociados a ningún recurso.

Para ello se usa el método CreateRenderTargetView:

ComPtr<ID3D12Resource> RenderTargets[4];

D3D12_CPU_DESCRIPTOR_HANDLE DescriptorView = RenderTargetViewHeap->GetCPUDescriptorHandleForHeapStart();
// modificar DescriptorView.ptr para que apunte al descriptor deseado

Device->CreateRenderTargetView(
  RenderTargets[FrameIndex].Get(), 
  nullptr, 
  DescriptorView
);

Este sería un código bastante típico para asociar los descriptores a los recursos.

ComPtr<ID3D12Resource> RenderTargets[4];
ComPtr<ID3D12DescriptorHeap> RenderTargetViewHeap;

// ... crear el heap ...

D3D12_CPU_DESCRIPTOR_HANDLE DescriptorView = RenderTargetViewHeap->GetCPUDescriptorHandleForHeapStart();

UINT HeapIncrementSize = Device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

for (UINT FrameIndex = 0; FrameIndex < kFrameCount; ++FrameIndex)
{
  Device->CreateRenderTargetView(
    RenderTargets[FrameIndex].Get(), 
    nullptr, 
    DescriptorView
  );
  DescriptorView.ptr += HeapIncrementSize;
}

Así podrías usar los descriptores render target:

UINT CurrentFrameIndex = ...; // el render target actual

D3D12_CPU_DESCRIPTOR_HANDLE RenderTargetView = RenderTargetViewHeap->GetCPUDescriptorHandleForHeapStart();
RenderTargetView.ptr += ((SIZE_T)CurrentFrameIndex) * Device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView = ...;

CommandList->OMSetRenderTargets(
  1, // número de render targets
  &RenderTargetView,
  FALSE, // RTsSingleHandleToDescriptorRange
  &DepthStencilView
);

Descriptor para Constant Buffer

Un constant buffer es un recurso que no cambia a lo largo de la ejecución del pipeline.

Ejemplo típico son las matrices de transformación, la dirección de la luz, el tiempo transcurrido, etc.,

Un constant buffer es un recurso que puede cambiar en cada frame, pero no durante un mismo frame. Es decir, es constante con respecto a la ejecución del pipeline.

struct FSomeConstants {
  XMMATRIX MVP;
  XMFLOAT3 LightDir;
};

const UINT SizeInBytes = (sizeof(FSomeConstants) + 256) & ~255; 
// es un requisito que, para reservar memoria de un constant buffer, la cantidad de memoria a reservar debe ser múltiplo de 256

FSomeConstants SomeConstants;
SomeConstants.MVP = ...;
SomeConstants.LightPos = ...;

ComPtr<ID3D12Resource> ContantBuffer;
// reservar memoria y copiar SomeConstant a ConstantBuffer.

ComPtr<ID3D12DescriptorHeap> ConstantBufferDescriptorHeap;
// crear un descriptor heap para alojar el descriptor

// Crear el descriptor
D3D12_CONSTANT_BUFFER_VIEW_DESC ConstantBufferViewDesc{};
ConstantBufferViewDesc.BufferLocation = ConstantBuffer->GetGPUVirtualAddress();
ConstantBufferViewDesc.SizeInBytes = SizeInBytes; // debe ser múltiplo de 256
Device->CreateConstantBufferView(&ConstantBufferViewDesc, ConstantBufferDescriptorHeap->GetCPUDescriptorHandleForHeapStart());

¿Dónde usamos este descriptor? Este descriptor debemos pasárselo a los shaders de algún modo para que puedan hacer uso del recurso.

Lo veremos en siguientes tutoriales.

Código para el proyecto

Añade el siguiente código al proyecto:

// DemoApp.h
class DemoApp
{
  static const UINT kFrameCount = 2;
  // ...
private:
  // ...
  ComPtr<ID3D12DescriptorHeap> RenderTargetViewHeap;
  ComPtr<ID3D12Resource> RenderTargets[kFrameCount]; // extracted from Swapchain
  void CreateRenderTargets();
}

void DemoApp::CreateRenderTargets()
{
  /* RenderTargetHeap */
  D3D12_DESCRIPTOR_HEAP_DESC DescriptorHeapDesc{};
  DescriptorHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
  DescriptorHeapDesc.NodeMask = 0;
  DescriptorHeapDesc.NumDescriptors = kFrameCount;
  DescriptorHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;

  Device->CreateDescriptorHeap(&DescriptorHeapDesc, IID_PPV_ARGS(&RenderTargetViewHeap));

  for (UINT FrameIndex = 0; FrameIndex < kFrameCount; ++FrameIndex)
  {
    /*
    @TODO:
    Crearemos el recurso RenderTargets[FrameIndex]
    en siguientes tutoriales. En concreto, el relacionado
    con Swapchain.
    */

    /* Crear el descriptor para RenderTargets[FrameIndex] */
    D3D12_RENDER_TARGET_VIEW_DESC RTDesc{};
    RTDesc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D;
    RTDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    RTDesc.Texture2D.MipSlice = 0;
    RTDesc.Texture2D.PlaneSlice = 0;

    D3D12_CPU_DESCRIPTOR_HANDLE DestDescriptor = RenderTargetViewHeap->GetCPUDescriptorHandleForHeapStart();
    DestDescriptor.ptr += ((SIZE_T)FrameIndex) * Device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

    Device->CreateRenderTargetView(RenderTargets[FrameIndex].Get(), &RTDesc, DestDescriptor);
  }
}

comments powered by Disqus