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_UNDEFINED
→VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
: a transferência para escrita que não precisa esperar por nada.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
→VK_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.