Link

Transições de layout

Para copiarmos os dados do buffer para o objeto de imagem criado, precisamos executar o comando vkCmdCopyBufferToImage para concluir a tarefa. Mas esse comando exige que a imagem esteja no layout correto primeiro. Para isso, vamos criar uma nova função transitionImageLayout para lidar com transições de layout:

void Renderer::transitionImageLayout(VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout) {

}

A operação de transferir um layout de uma imagem para outro requer gravar um buffer de comando e enviá-lo para uma fila. Para isso, chamamos nossa função auxiliar beginSingleTimeCommands que inicia a operação de gravação e no final da função chamamos endSingleTimeCommands para submeter o buffer de comando a fila de gráficos e liberá-lo assim que for executado:

void Renderer::transitionImageLayout(VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout) {
    VkCommandBuffer commandBuffer = beginSingleTimeCommands();


    endSingleTimeCommands(commandBuffer);
}

Como mencionado anteriormente, uma das maneiras de realizar transições de layout é usando uma barreira, mais especificamente uma barreira de memória de imagem. Esse tipo de barreira é representado pela instância VkImageMemoryBarrier e é aplicável aos diferentes tipos de acesso à memória por meio de um intervalo específico de sub-recurso de imagem do objeto de imagem.

VkImageMemoryBarrier barrier = {};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.oldLayout = oldLayout;
barrier.newLayout = newLayout;

Além do campo sType, os dois primeiros campos especificam a transição do layout. É possível usar VK_IMAGE_LAYOUT_UNDEFINED como oldLayout se não nos importamos com o conteúdo existente da imagem.

barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;

Se estivermos usando a barreira para transferir a propriedade da família de filas, então os campos srcQueueFamilyIndex e dstQueueFamilyIndex devem ser os índices das famílias de filas de origem e destino, respectivamente. Em nosso caso, não há transferência de propriedade entre as filas, portanto, ambos os campos são definidos como VK_QUEUE_FAMILY_IGNORED.

barrier.image = image;
barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
barrier.subresourceRange.baseMipLevel = 0;
barrier.subresourceRange.levelCount = 1;
barrier.subresourceRange.baseArrayLayer = 0;
barrier.subresourceRange.layerCount = 1;

Os campos image e subresourceRange especificam, respectivamente, a imagem que é afetada e a parte específica da imagem. O aspecto ou aspectos da imagem que desejamos fazer a transferência de layout é especificado em subresourceRange.aspectMask. Para imagens coloridas, como no nosso caso, isso deve ser VK_IMAGE_ASPECT_COLOR_BIT. Nossa imagem não é um array e não possui níveis de mapeamento MIP, portanto apenas um nível (subresourceRange.levelCount) e uma camada (subresourceRange.layerCount) são especificados.

Barreiras são usadas principalmente para fins de sincronização, portanto, devemos especificar quais tipos de operações que envolvem o recurso devem acontecer antes da barreira e quais operações que envolvem o recurso devem aguardar na barreira. Precisamos fazer isso apesar de já estarmos usando vkQueueWaitIdle em endSingleTimeCommands para sincronizar manualmente. Os valores corretos dependem do antigo e do novo layout. No nosso caso, estamos interessados em duas transições:

  • VK_IMAGE_LAYOUT_UNDEFINEDVK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: a transferência para escrita que não precisa esperar por nada.
  • VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMALVK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: a leitura pelo shader deve aguardar o final da transferência.

Sem essas transições, a operação de transferência de dados pode não ser apenas inválida, mas os dados podem não se tornar visíveis para outras operações executadas na imagem.

A primeira transição é especificada usando as seguintes máscaras de acesso e estágios do pipeline:

VkPipelineStageFlags sourceStage;
VkPipelineStageFlags destinationStage;

if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED
	&& newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
	barrier.srcAccessMask = 0;
	barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;

	sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
	destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
}

Nessa transição, a transferência para escrita (VK_ACCESS_TRANSFER_WRITE_BIT) deve ocorrer no estágio de transferência do pipeline (VK_PIPELINE_STAGE_TRANSFER_BIT). Como as gravações não precisam esperar por nada, podemos especificar uma máscara de acesso vazia e o primeiro estágio de pipeline possível (VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT) para as operações de pré-barreira. Deve-se observar que VK_PIPELINE_STAGE_TRANSFER_BIT não é um estágio real nos pipelines gráficos e de computação. É mais um pseudo-estágio em que as transferências acontecem (OVERVOORDE, 2018). Para mais informações e outros exemplos de pseudo-estágios, verifique a documentação do Vulkan.

A segunda transição é especificada da seguinte forma:

...
else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
	&& newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
	barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
	barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;

	sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
	destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
} else {
	qFatal("Unsupported layout transition!");
}

Nela, a imagem será gravada no mesmo estágio de pipeline e subsequentemente lida pelo fragment shader, e é por isso que especificamos o acesso à leitura de shader (VK_ACCESS_SHADER_READ_BIT) no estágio do pipeline do fragment shader (VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT).

Agora que tratamos de todas as transições necessárias para o nosso caso, podemos configurar as barreiras no pipeline gráfico. Isso é feito chamando a função vkCmdPipelineBarrier:

m_deviceFunctions->vkCmdPipelineBarrier(
	commandBuffer,
	sourceStage,
	destinationStage,
	0,
	0,
	nullptr,
	0,
	nullptr,
	1,
	&barrier
);

Uma única chamada para vkCmdPipelineBarrier pode ser usada para acionar várias operações de barreira. Existem três tipos de operações de barreira: barreiras de memória global, barreiras de buffer e barreiras de imagem como a que estamos usando aqui. Os dois parâmetros após o buffer de comando especificam, respectivamente, quais estágios do pipeline escreveram no recurso por último e quais etapas irão ler do recurso a seguir. Ou seja, eles especificam a origem e o destino do fluxo de dados representado pela barreira. Cada um é construído a partir de um número de membros da enumeração VkPipelineStageFlagBits. Os estágios do pipeline que podemos especificar antes e depois da barreira dependem de como usamos o recurso antes e depois da barreira4.

4 Os valores permitidos são listados aqui.

O quarto parâmetro especifica um conjunto de sinalizadores que descreve como a dependência representada pela barreira afeta os recursos referenciados pela barreira. O único sinalizador definido é VK_DEPENDENCY_BY_REGION_BIT, que indica que a barreira afeta apenas a região modificada pelos estágios de origem (se puder ser determinada), que é consumida pelos estágios de destino. Isso significa que a implementação já pode começar a ler as partes de um recurso que foram escritas até o momento. Não utilizaremos esse recurso aqui, portanto definimos esse campo como 0.

Os últimos três pares de parâmetros fazem referência a conjuntos de barreiras de pipeline dos três tipos disponíveis mencionados anteriormente – o primeiro, para barreiras de memória global; o segundo, para barreiras de buffer; o terceiro, para barreiras de imagem.


Anterior Próximo