Link

Conjuntos de descritores

Criamos uma imagem, vinculamos uma memória a ela e até enviamos dados para a imagem. Também criamos um sampler para configurar parâmetros de amostragem para nossa textura. Agora queremos usar a textura a partir do shader. No Vulkan, nós fazemos isso através do uso de descritores.

Como mencionado anteriormente, descritores são estruturas de dados opacas que representam recursos do shader. Eles são organizados em conjuntos e seu conteúdo é especificado pelos layouts dos conjuntos de descritores. Para fornecer recursos aos shaders, vinculamos conjuntos de descritores aos pipelines. Podemos ligar vários conjuntos ao mesmo tempo. Para acessar recursos de dentro dos shaders, precisamos especificar de qual conjunto e de qual local dentro de um conjunto (chamado de ligação) o recurso fornecido é adquirido. Para isso, definimos no shader um qualificador de layout com um endereço (set = X, binding = Y), que pode ser traduzido para: obter o recurso do local da ligação Y do conjunto X.

No Vulkan, temos 11 tipos de descritores. São eles:

  • VK_DESCRIPTOR_TYPE_SAMPLER: Define como os dados da imagem são lidos. Dentro dos shaders, um mesmo sampler pode ser usados com várias imagens.
  • VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE: Define imagens das quais podemos ler dados dentro de shaders. Podemos ler dados de uma única imagem usando samplers diferentes.
  • VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER: Esse tipo de descritor combina o sampler e a imagem amostrada como um único objeto. Da perspectiva da API (nosso aplicativo), ainda precisamos criar um sampler e uma imagem separados, mas dentro do shader eles aparecem como um único objeto. Usá-lo pode ser mais ideal (pode ter melhor desempenho) do que usar samplers e imagens separados.
  • VK_DESCRIPTOR_TYPE_STORAGE_IMAGE: Este descritor nos permite ler e armazenar dados dentro de uma imagem.
  • VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER: Esse descritor permite que o conteúdo do buffer seja tratado como se contivesse dados de textura; esses dados são interpretados como texels com um número selecionado de componentes e formato. Dessa maneira, podemos acessar arrays muito grandes de dados (muito maiores que os uniform buffers).
  • VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER: Buffers de armazenamento de texels são semelhantes aos uniform texel buffers. Não apenas eles podem ser usados para leitura, mas também podem ser usados para armazenar dados.
  • VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER e VK_DESCRIPTOR_TYPE_STORAGE_BUFFER: Eles são semelhantes a VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER e VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, exceto que os dados são não formatados e descritos pelas estruturas declaradas no shader.
  • VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC e VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC: Eles são semelhantes a VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER e VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, mas incluem um deslocamento e tamanho que são passados quando o conjunto de descritores é vinculado ao pipeline e não quando o descritor é vinculado ao conjunto. Isso permite que um único buffer em um único conjunto seja atualizado em alta frequência.
  • VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT: Esse descritor é de uso específico dos anexos do render pass. Quando queremos ler dados de uma imagem que é usada como um anexo dentro do mesmo render pass, só podemos fazer isso através de um anexo de entrada. Dessa forma, não precisamos finalizar um render pass e iniciar outro, mas estamos restritos apenas aos fragment shaders e a apenas um único local por instância do fragment shader (uma determinada instância de um fragment shader pode ler dados de coordenadas associadas a coordenadas do fragment shader).

Todos os descritores acima são criados a partir de samplers, imagens ou buffers. A diferença está no modo como os usamos e acessamos dentro dos shaders. Todos os parâmetros adicionais desse acesso podem ter implicações no desempenho. Por exemplo, com buffers de armazenamento (VK_DESCRIPTOR_TYPE_STORAGE_BUFFER), a leitura de dados é provavelmente muito mais rápida do que armazenar dados dentro deles. Da mesma forma, os bufffers de texels (VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER) nos permitem acessar mais elementos do que com uniform buffers (VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER), mas isso também pode vir com o custo de um desempenho pior. Por isso, sempre devemos lembrar de selecionar um descritor que melhor atenda às nossas necessidades.

Neste projeto, queremos usar uma textura. Para este propósito, criamos uma imagem e um sampler. Usaremos ambos para preparar um descritor do tipo VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER.

No Vulkan, o uso de descritores consiste em três partes: Primeiro, temos que especificar um layout de conjunto de descritores durante a criação do pipeline. Depois, devemos alocar um conjunto de descritores de um pool de descritores. Por último, devemos vincular o conjunto de descritores aos comandos de desenho, assim como os buffers de vértice.

O layout do conjunto de descritores especifica os tipos de recursos que serão acessados pelo pipeline. Um conjunto de descritores especifica o buffer ou os recursos de imagem que serão vinculados aos descritores.


Criando um layout do conjunto de descritores

Semelhante às entradas de vértice do shader, precisamos fornecer informações sobre cada um dos descritores usados pelos shaders ao criar o pipeline. Criaremos uma função para lidar com todas essas informações e, assim, criar o layout do conjunto de descritores. Ela será nomeada createDescriptorSetLayout e será chamada logo antes da inicialização do pipeline. Porque precisaremos do layout do conjunto de descritores na criação do pipeline.

void Renderer::initResources() {
	...
	createDescriptorSetLayout();
	initPipeline();
	...
}

void Renderer::createDescriptorSetLayout() {

}

A criação do layout do conjunto de descritores começa definindo os parâmetros de todos os descritores disponíveis em um determinado conjunto. Isso é feito preenchendo uma estrutura do tipo VkDescriptorSetLayoutBinding:

VkDescriptorSetLayoutBinding samplerLayoutBinding = {};
samplerLayoutBinding.binding = 0;
samplerLayoutBinding.descriptorType =
	VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
samplerLayoutBinding.descriptorCount = 1;

Os campos binding e descriptorType dessa estrutura especificam, respectivamente, a ligação usada no shader e o tipo de descritor. A ligação aqui será o primeiro descritor (binding = 0) em um determinado layout. Para evitar o desperdício de memória, devemos manter as ligações o mais compactamente possível (o mais próximo possível de zero), porque os drivers podem alocar memória para os slots do descritor, mesmo que não sejam usados. Como mencionado anteriormente, queremos combinar os objetos de sampler e de imagem no mesmo descritor. Para isso, utilizamos o sinalizador VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER. É possível que a variável do shader represente um array de objetos de sampler e imagem combinados e descriptorCount especifica o número de valores no array. Nossa variável do shader não será um array, portando definimos descriptorCount como 1.

samplerLayoutBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;

Também devemos informar ao Vulkan sobre os estágios de shaders que acessarão este recurso. O campo de bit stageFlags permite combinar todos os estágios de shaders envolvidos. Se o valor for VK_SHADER_STAGE_ALL, todos os estágios de shaders poderão acessar o recurso por meio da ligação especificada. No nosso caso, estamos apenas referenciando o descritor no fragment shader, pois é nele que a cor do fragmento será determinada.

samplerLayoutBinding.pImmutableSamplers = nullptr;

O campo pImmutableSamplers afeta somente os samplers que devem ser permanentemente vinculados ao layout (e não podem ser alterados mais tarde). Mas não precisamos nos preocupar com esse parâmetro, e podemos vincular os samplers como quaisquer outros descritores definindo esse parâmetro como nullptr.

Adicionamos um novo membro de classe em Renderer para armazenar o identificador do layout do conjunto de descritores:

VkDescriptorSetLayout m_descriptorSetLayout = nullptr;

Para a criação de um layout do conjunto de descritores, Vulkan exige que preenchemos uma estrutura VkDescriptorSetLayoutCreateInfo.

VkDescriptorSetLayoutCreateInfo layoutInfo = {};
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = 1;
layoutInfo.pBindings = &samplerLayoutBinding;

Os membros bindingCount e pBindings dessa estrutura contêm, respectivamente, o número de pontos de ligação que o conjunto conterá e um ponteiro para um array contendo suas descrições.

Agora que fornecemos valores para todos os parâmetros, podemos criar um um layout do conjunto de descritores. Isso é feito através da função vkCreateDescriptorSetLayout para a qual precisamos fornecer um identificador de um dispositivo lógico, um ponteiro para a estrutura descrita acima e um ponteiro para uma variável do tipo VkDescriptorSetLayout na qual o identificador do layout do conjunto de descritores criado será armazenado:

VkDevice device = m_window->device();
VkResult result = m_deviceFunctions->vkCreateDescriptorSetLayout(
	device,
	&layoutInfo,
	nullptr,
	&m_descriptorSetLayout
);
if (result != VK_SUCCESS) {
	qFatal("Failed to create descriptor set layout: %d", result);
}

Agora precisamos especificar o layout do conjunto de descritores durante a criação do pipeline para informar ao Vulkan quais descritores os shaders estarão usando. Os layouts do conjunto de descritores são especificados no objeto de layout do pipeline. Modificamos a variável do tipo VkPipelineLayoutCreateInfo para fazer referência ao objeto de layout:

VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &m_descriptorSetLayout;

O layout do conjunto de descritores deve permanecer enquanto podemos criar novos pipelines gráficos, ou seja, até que o programa termine:

void Renderer::releaseResources() {
	...

	m_deviceFunctions->vkDestroyDescriptorSetLayout(
		device,
		m_descriptorSetLayout,
		nullptr
	);
}

Como mencionado acima, o layout do descritor somente descreve o tipo de descritores que podem ser vinculados. Agora, precisamos criar um conjunto de descritores para os objetos de sampler e imagem para vinculá-los ao descritor de sampler e imagem combinados.


Criando um pool de descritores

Como já discutido neste capítulo, conjuntos de descritores não são criados diretamente. Em vez disso, eles são alocados a partir de pools. Antes de podermos alocar um conjunto de descritores, precisamos criar um pool de descritores.

Escreveremos uma nova função createDescriptorPool para configurá-lo. Vamos chamar essa função no final de addTextureImage:

void Renderer::addTextureImage(QString texturePath) {
	...
	createDescriptorPool();
}

void Renderer::createDescriptorPool() {

}

Criar um pool de descritores envolve especificar quantos conjuntos de descritores podem ser alocados a partir dele. Ao mesmo tempo, também precisamos especificar quais tipos de descritores e quantos deles podem ser alocados do pool em todos os conjuntos. Por exemplo, imagine que queremos alocar uma única imagem amostrada e um único buffer de armazenamento de um determinado pool, e que podemos alocar dois conjuntos de descritores do pool. Ao fazer isso, se alocarmos um conjunto de descritores com uma imagem amostrada, o segundo descritor poderá conter apenas um buffer de armazenamento. Se um único conjunto de descritores alocado desse pool contiver ambos os recursos, não poderemos alocar outro conjunto, pois ele teria que estar vazio. Durante a criação do pool de descritores, definimos o número total de descritores individuais e o número total de conjuntos que podem ser alocados a partir dele. Para isso, primeiro preparamos variáveis do tipo VkDescriptorPoolSize que especificam o tipo de um descritor e o número total de descritores de um tipo selecionado que podem ser alocados do pool.

VkDescriptorPoolSize poolSize = {};
poolSize.type =  VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
poolSize.descriptorCount = 1;

Em nosso exemplo, queremos alocar um único conjunto de descritores com apenas um descritor do tipo VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER.

Em seguida, fornecemos um array dessas variáveis para uma variável do tipo VkDescriptorPoolCreateInfo:

VkDescriptorPoolCreateInfo poolInfo = {};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.poolSizeCount = 1;
poolInfo.pPoolSizes = &poolSize;

Como mencionado acima, além do número máximo de descritores individuais disponíveis, também precisamos especificar o número máximo de conjuntos de descritores que podem ser alocados:

poolInfo.maxSets =1;

Em seguida, adicionamos um novo membro de estrutura em Object3D para armazenar o identificador do pool de descritores e chamamos vkCreateDescriptorPool para criá-lo:

...
VkImageView textureImageView = VK_NULL_HANDLE;

VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
...
poolInfo.maxSets = 1;

VkDevice device = m_window->device();

if (m_object->descriptorPool) {
	m_deviceFunctions->vkDestroyDescriptorPool(
		device,
		m_object->descriptorPool,
		nullptr
	);
	m_object->descriptorPool = VK_NULL_HANDLE;
}

VkResult result = m_deviceFunctions->vkCreateDescriptorPool(
	device,
	&poolInfo,
	nullptr,
	&m_object->descriptorPool
);
if (result != VK_SUCCESS) {
	qFatal("Failed to create descriptor pool: %d", result);
}

O pool de descritores deve ser destruído apenas quando não estivermos mais renderizando o objeto 3D. Por isso, adicionamos o seguinte código em releaseObjectResources para destruir o pool de descritores associado ao objeto 3D:

void Renderer::releaseObjectResources() {
	...
	if (m_object->textureImageMemory) {
		m_deviceFunctions->vkFreeMemory(
			device, m_object->textureImageMemory,
			nullptr
		);
		m_object->textureImageMemory = VK_NULL_HANDLE;
	}

	if (m_object->descriptorPool) {
		m_deviceFunctions->vkDestroyDescriptorPool(
			device,
			m_object->descriptorPool,
			nullptr
		);
		m_object->descriptorPool = VK_NULL_HANDLE;
	}
}

Criando um conjunto de descritores

Agora podemos alocar o conjunto de descritores em si. Adicionamos uma função createDescriptorSets para esse fim. Ela deve ser chamada logo após createDescriptorPool no final de addTextureImage:

void Renderer::addTextureImage(QString texturePath) {
	...
	createDescriptorPool();
	createDescriptorSets();
}

void Renderer::createDescriptorSets() {

}

Uma alocação do conjunto de descritores é descrita com uma estrutura VkDescriptorSetAllocateInfo:

VkDescriptorSetAllocateInfo allocInfo = {};
allocInfo.sType =
	VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.descriptorPool = m_object->descriptorPool;
allocInfo.descriptorSetCount = 1;
allocInfo.pSetLayouts = &m_descriptorSetLayout;

Um identificador para o conjunto de descritores do qual alocar os conjuntos é especificado em descriptorPool. O número de conjuntos a serem criados é especificado em descriptorSetCount. O layout de cada conjunto é passado através de um array de identificadores de objeto VkDescriptorSetLayout em pSetLayouts.

Adicionamos um membro de estrutura em Object3D para manter o manipulador do conjunto de descritores e alocamos-o com vkAllocateDescriptorSets:

VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
..

VkDevice device = m_window->device();
VkResult result = m_deviceFunctions->vkAllocateDescriptorSets(
	device,
	&allocInfo,
	&m_object->descriptorSet
);
if (result != VK_SUCCESS) {
	qFatal("Failed to allocate descriptor sets: %d", result);
}

A chamada para vkAllocateDescriptorSets alocará o conjunto de descritores. Não precisamos limpar explicitamente o conjunto de descritores, porque ele será automaticamente liberado quando o pool de descritores for destruído.

O conjunto de descritores foi alocado agora, mas o descritor dentro ainda precisa ser configurado.

Os recursos para uma estrutura de sampler e imagem combinados devem ser especificados em uma estrutura VkDescriptorImageInfo. É aqui que os objetos que criamos anteriormente se juntam.

VkDescriptorImageInfo descriptorImageInfo = {};
descriptorImageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
descriptorImageInfo.imageView = m_object->textureImageView;
descriptorImageInfo.sampler = m_object->textureSampler;

A configuração dos descritores é atualizada usando a função vkUpdateDescriptorSets, que usa um array de estruturas VkWriteDescriptorSet como parâmetro.

VkWriteDescriptorSet descriptorWrite = {};
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrite.dstSet = m_object->descriptorSet;
descriptorWrite.dstBinding = 0;
descriptorWrite.dstArrayElement = 0;

O terceiro e quarto campo especificam, respectivamente, o conjunto de descritores para atualizar e a ligação. Fornecemos nosso índice de ligação de sampler o valor 0. Os descritores podem ser arrays, portanto, também precisamos especificar o primeiro índice no array que queremos atualizar. Não estamos usando um array, então dstArrayElement é simplesmente 0.

descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
descriptorWrite.descriptorCount = 1;

Precisamos especificar o tipo de descritor novamente. É possível atualizar vários descritores de uma vez em um array, iniciando no índice dstArrayElement. O campo descritorCount especifica quantos elementos do array desejamos atualizar.

descriptorWrite.pBufferInfo = nullptr;
descriptorWrite.pImageInfo = &descriptorImageInfo;
descriptorWrite.pTexelBufferView = nullptr;

Os últimos três campos referenciam um array com descriptorCount estruturas que realmente configuram os descritores. Depende do tipo de descritor que um dos três realmente precisamos usar. O campo pBufferInfo é usado para descritores que se referem a dados de buffer, pImageInfo é usado para descritores que se referem a dados de imagem e pTexelBufferView é usado para descritores que se referem a buffers de visualização. Nosso descritor é baseado em imagens, então estamos usando o campo pImageInfo.

m_deviceFunctions->vkUpdateDescriptorSets(
	device,
	1,
	&descriptorWrite,
	0,
	nullptr
);

As atualizações são aplicadas usando a função vkUpdateDescriptorSets. Essa função aceita dois tipos de arrays como parâmetros: um array de VkWriteDescriptorSet e um array de VkCopyDescriptorSet. Este último pode ser usado para copiar os descritores uns aos outros, como o próprio nome indica.


Usando conjuntos de descritores

Agora precisamos atualizar a função startNextFrame para vincular o conjunto de descritores ao buffer de comando ativo para a imagem atual do swap chain. Para isso, chamamos a função vkCmdBindDescriptorSets. Isso precisa ser feito antes da chamada vkCmdDraw:

m_deviceFunctions->vkCmdBindDescriptorSets(
	commandBuffer,
	VK_PIPELINE_BIND_POINT_GRAPHICS,
	m_pipelineLayout,
	0,
	1,
	&m_object->descriptorSet,
	0,
	nullptr
);

Assim como com os pipelines, para acessar os recursos anexados a um conjunto de descritores, o conjunto de descritores deve estar vinculado ao buffer de comando que executará os comandos que acessam esses descritores. Há também dois pontos de ligação para os conjuntos de descritores – um para computação (VK_PIPELINE_BIND_POINT_COMPUTE) e outro para gráficos (VK_PIPELINE_BIND_POINT_GRAPHICS) – que são os conjuntos que serão acessados pelos pipelines do tipo apropriado. O próximo parâmetro é o layout em que os descritores são baseados. Os próximos três parâmetros especificam o índice do primeiro conjunto de descritores, o número de conjuntos a serem vinculados e o array de conjuntos a serem vinculados. Os dois últimos parâmetros especificam um array de deslocamentos que são usados para descritores dinâmicos.


Anterior Próximo