Vulkan. Guia do desenvolvedor. Corrente de troca

Eu continuo a publicar traduções do manual da API Vulkan (o link para o original é vulkan-tutorial.com ) e hoje quero compartilhar a tradução de um novo capítulo - Cadeia de troca da seção Desenhando um triângulo, subseção Apresentação.



Contente
1.



2.



3.



4.







  1. (pipeline)


5.



  1. Staging


6. Uniform-



  1. layout
  2. sets


7.



  1. Image view image sampler
  2. image sampler


8.



9.



10. -



11. Multisampling



FAQ









Corrente de troca





Vulkan não tem framebuffer padrão, então ele precisa de uma infraestrutura com buffers onde as imagens serão renderizadas antes de serem exibidas. Essa infraestrutura é chamada de cadeia de troca e deve ser criada explicitamente no Vulkan. A cadeia de troca é uma fila de imagens esperando para serem exibidas na tela. O programa primeiro pede um objeto image(VkImage)



para desenhar e, após a renderização, o envia de volta para a fila. Como a fila funciona depende das configurações, mas a principal tarefa da cadeia de troca é sincronizar a saída das imagens com a taxa de atualização da tela.



Verificando o suporte da cadeia de troca



Algumas placas de vídeo especializadas não têm saídas de vídeo e, portanto, não podem exibir imagens na tela. Além disso, o mapeamento de tela está vinculado ao sistema de janelas e não faz parte do núcleo do Vulkan. Portanto, precisamos conectar a extensão VK_KHR_swapchain



.



Primeiro isDeviceSuitable



, vamos alterar a função para verificar se a extensão é compatível. Já trabalhamos com a lista de extensões suportadas antes, então não deve haver nenhuma dificuldade. Observe que o arquivo de cabeçalho Vulkan fornece uma macro útil VK_KHR_SWAPCHAIN_EXTENSION_NAME



que é definida como " VK_KHR_swapchain



". A vantagem dessa macro é que, se você cometer um erro de ortografia, o compilador o avisará sobre isso.



Vamos começar declarando uma lista de extensões necessárias.



const std::vector<const char*> deviceExtensions = {
    VK_KHR_SWAPCHAIN_EXTENSION_NAME
};
      
      





Para verificação adicional, vamos criar uma nova função checkDeviceExtensionSupport



chamada de isDeviceSuitable



:



bool isDeviceSuitable(VkPhysicalDevice device) {
    QueueFamilyIndices indices = findQueueFamilies(device);

    bool extensionsSupported = checkDeviceExtensionSupport(device);

    return indices.isComplete() && extensionsSupported;
}

bool checkDeviceExtensionSupport(VkPhysicalDevice device) {
    return true;
}
      
      





Vamos alterar o corpo da função para verificar se todas as extensões que precisamos estão na lista de suportadas.



bool checkDeviceExtensionSupport(VkPhysicalDevice device) {
    uint32_t extensionCount;
    vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, nullptr);

    std::vector<VkExtensionProperties> availableExtensions(extensionCount);
    vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, availableExtensions.data());

    std::set<std::string> requiredExtensions(deviceExtensions.begin(), deviceExtensions.end());

    for (const auto& extension : availableExtensions) {
        requiredExtensions.erase(extension.extensionName);
    }

    return requiredExtensions.empty();
}
      
      





Aqui eu costumava std::set<std::string>



armazenar os nomes das extensões necessárias, mas ainda não confirmadas. Você também pode usar um loop aninhado como em uma função checkValidationLayerSupport



. A diferença de desempenho não é significativa.



Agora vamos executar o programa e verificar se nossa placa de vídeo é adequada para criar uma cadeia de troca. Observe que a presença de uma fila de exibição já implica suporte para a extensão da cadeia de troca. No entanto, é melhor certificar-se disso explicitamente.



Conectando extensões



Para usar a cadeia de troca, primeiro você precisa habilitar a extensão VK_KHR_swapchain



. Para fazer isso, vamos alterar ligeiramente o preenchimento VkDeviceCreateInfo



ao criar o dispositivo lógico:



createInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size());
createInfo.ppEnabledExtensionNames = deviceExtensions.data();
      
      





Solicitação de informações de suporte de cadeia de troca



Verificar sozinho se a cadeia de troca está disponível não é suficiente. A criação da cadeia de troca envolve muito mais configuração, por isso precisamos pedir mais informações.



No total, você precisa verificar 3 tipos de propriedades:



  • Capacidades básicas da superfície, como número mínimo / máximo de imagens na cadeia de troca, largura mínima / máxima e altura das imagens
  • Formato de superfície (formato de pixel, espaço de cor)
  • Modos operacionais disponíveis


Para trabalhar com esses dados, usaremos a estrutura:



struct SwapChainSupportDetails {
    VkSurfaceCapabilitiesKHR capabilities;
    std::vector<VkSurfaceFormatKHR> formats;
    std::vector<VkPresentModeKHR> presentModes;
};
      
      





Agora vamos criar uma função querySwapChainSupport



que preencha essa estrutura.



SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device) {
    SwapChainSupportDetails details;

    return details;
}
      
      





Vamos começar com os recursos de superfície. Eles são fáceis de consultar e retornar à estrutura VkSurfaceCapabilitiesKHR



.



vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities);
      
      





Esta função aceita o criado anteriormente VkPhysicalDevice



e VkSurfaceKHR



. Cada vez que solicitamos uma funcionalidade suportada, esses dois parâmetros serão os primeiros, uma vez que são componentes-chave da cadeia de troca.



A próxima etapa é consultar os formatos de superfície suportados. Para fazer isso, vamos realizar o ritual já familiar com uma chamada de função dupla:



uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr);

if (formatCount != 0) {
    details.formats.resize(formatCount);
    vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, details.formats.data());
}
      
      





Certifique-se de alocar espaço suficiente no vetor para obter todos os formatos disponíveis.



Da mesma forma, solicitamos os modos de operação suportados usando a função vkGetPhysicalDeviceSurfacePresentModesKHR



:



uint32_t presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr);

if (presentModeCount != 0) {
    details.presentModes.resize(presentModeCount);
    vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data());
}
      
      





Quando todas as informações necessárias estiverem na estrutura, adicione a função isDeviceSuitable



para verificar se a cadeia de troca é suportada. Para os fins deste tutorial, assumiremos que, se houver pelo menos um formato de imagem compatível e um modo compatível para a superfície da janela, a cadeia de troca será compatível.



bool swapChainAdequate = false;
if (extensionsSupported) {
    SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device);
    swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty();
}
      
      





Você só precisa solicitar suporte à cadeia de troca depois de verificar se a extensão está disponível.



A última linha da função muda para:



return indices.isComplete() && extensionsSupported && swapChainAdequate;
      
      





Escolha das configurações para a cadeia de troca



Se swapChainAdequate



verdadeiro, a cadeia de troca é suportada. Mas a cadeia de troca pode ter vários modos. Vamos escrever algumas funções para encontrar as configurações apropriadas para criar a cadeia de troca mais eficiente.



No total, destacamos 3 tipos de configurações:

  • formato da superfície (profundidade de cor)
  • modo de operação (condições para alterar quadros na tela)
  • extensão de troca (resolução de imagens na cadeia de troca)


Para cada configuração, procuraremos algum valor "ideal" e, se não estiver disponível, usaremos alguma lógica para escolher o que é.



Formato de superfície



Vamos adicionar uma função para selecionar um formato:



VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& availableFormats) {

}
      
      





Posteriormente, passaremos um membro formats



da estrutura SwapChainSupportDetails



como um argumento.



Cada elemento availableFormats



contém membros format



e colorSpace



. O campo format



define o número e os tipos de canais. Por exemplo, VK_FORMAT_B8G8R8A8_SRGB



significa que temos canais B, G, R e alfa de 8 bits cada, para um total de 32 bits por pixel. Um sinalizador VK_COLOR_SPACE_SRGB_NONLINEAR_KHR



no campo colorSpace



indica se o espaço de cores SRGB é compatível. Observe que em uma versão anterior da especificação, esse sinalizador foi chamado VK_COLORSPACE_SRGB_NONLINEAR_KHR



.



Usaremos SRGB como espaço de cores. SRGB é um padrão para representação de cores em imagens, reproduz melhor as cores percebidas. É por isso que também usaremos um dos formatos SRGB como formato de cor - VK_FORMAT_B8G8R8A8_SRGB



.



Vamos examinar a lista e verificar se a combinação de que precisamos está disponível:



for (const auto& availableFormat : availableFormats) {
    if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
        return availableFormat;
    }
}
      
      





Caso contrário, podemos classificar os formatos disponíveis dos mais adequados para os menos adequados, mas na maioria dos casos podemos simplesmente escolher o primeiro da lista.



VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& availableFormats) {
    for (const auto& availableFormat : availableFormats) {
        if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
            return availableFormat;
        }
    }

    return availableFormats[0];
}

      
      





Jornada de trabalho



O modo de operação é talvez a configuração mais importante para a cadeia de troca, uma vez que determina as condições para a mudança de quadros na tela.



Existem quatro modos disponíveis no Vulkan:



  • VK_PRESENT_MODE_IMMEDIATE_KHR



    : , , , .
  • VK_PRESENT_MODE_FIFO_KHR



    : . , . , . , .
  • VK_PRESENT_MODE_FIFO_RELAXED_KHR



    : , . . .
  • VK_PRESENT_MODE_MAILBOX_KHR



    : esta é outra variação do segundo modo. Em vez de bloquear o programa quando a fila está cheia, as imagens na fila são substituídas por novas. Este modo é adequado para implementar buffer triplo. Com ele, você pode evitar o aparecimento de artefatos com baixa latência.


Apenas o modo estará disponível VK_PRESENT_MODE_FIFO_KHR



, então, novamente, teremos que escrever uma função para encontrar o melhor modo disponível:



VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR>& availablePresentModes) {
    return VK_PRESENT_MODE_FIFO_KHR;
}
      
      





Pessoalmente, acho melhor usar buffer triplo. Isso evita artefatos com baixa latência.



Então, vamos percorrer a lista para verificar os modos disponíveis:



VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR>& availablePresentModes) {
    for (const auto& availablePresentMode : availablePresentModes) {
        if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) {
            return availablePresentMode;
        }
    }

    return VK_PRESENT_MODE_FIFO_KHR;
}
      
      





Extensão de troca



Resta configurar a última propriedade. Para fazer isso, adicione uma função:



VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) {

}
      
      





A extensão da troca é a resolução das imagens na cadeia de troca, que quase sempre corresponde à resolução da janela (em pixels) onde as imagens são renderizadas. Conseguimos o alcance permitido na estrutura VkSurfaceCapabilitiesKHR



. Vulkan nos diz qual resolução devemos definir usando um campo currentExtent



(corresponde ao tamanho da janela). No entanto, alguns gerenciadores de janela permitem diferentes resoluções. Para isso, um valor especial para a largura e altura currentExtent



é especificado - o valor máximo do tipo uint32_t



. Neste caso, a partir do intervalo entre minImageExtent



e, maxImageExtent



escolheremos a resolução que melhor corresponda à resolução da janela. O principal é especificar as unidades de medida corretamente.



O GLFW usa duas unidades de medida: pixels e coordenadas da tela . Portanto, a resolução {WIDTH, HEIGHT}



que especificamos ao criar a janela é medida em coordenadas de tela. Mas, como o Vulkan funciona com pixels, a resolução da cadeia de troca também deve ser especificada em pixels. Se você estiver usando um monitor de alta resolução (como o monitor Retina da Apple), as coordenadas da tela não correspondem aos pixels: Devido à densidade de pixels mais alta, a resolução da janela é maior em pixels do que nas coordenadas da tela. Como o Vulkan não corrige a permissão da cadeia de troca para nós, não podemos usar a permissão original {WIDTH, HEIGHT}



. Em vez disso, devemos usar glfwGetFramebufferSize



para consultar a resolução da janela em pixels antes de mapeá-la para as resoluções de imagem mínima e máxima.



#include <cstdint> // Necessary for UINT32_MAX

...

VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) {
    if (capabilities.currentExtent.width != UINT32_MAX) {
        return capabilities.currentExtent;
    } else {
        int width, height;
        glfwGetFramebufferSize(window, &width, &height);

        VkExtent2D actualExtent = {
            static_cast<uint32_t>(width),
            static_cast<uint32_t>(height)
        };

        actualExtent.width = std::max(capabilities.minImageExtent.width, std::min(capabilities.maxImageExtent.width, actualExtent.width));
        actualExtent.height = std::max(capabilities.minImageExtent.height, std::min(capabilities.maxImageExtent.height, actualExtent.height));

        return actualExtent;
    }
}
      
      





Função max



e min



serve para limitar os valores width



e height



dentro das resoluções disponíveis. Não se esqueça de incluir o arquivo de cabeçalho <algorithm>



para usar as funções.



Criação de cadeia de troca



Agora temos todas as informações de que precisamos para criar uma cadeia de troca adequada.



Vamos criar uma função createSwapChain



e chamá-la initVulkan



depois de criar o dispositivo lógico.



void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
}

void createSwapChain() {
    SwapChainSupportDetails swapChainSupport = querySwapChainSupport(physicalDevice);

    VkSurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats);
    VkPresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.presentModes);
    VkExtent2D extent = chooseSwapExtent(swapChainSupport.capabilities);
}
      
      





Agora você precisa decidir quantos objetos de imagem devem estar na cadeia de troca. A implementação especifica a quantidade mínima necessária para o trabalho:



uint32_t imageCount = swapChainSupport.capabilities.minImageCount;
      
      





No entanto, se você usar apenas esse mínimo, às vezes terá que esperar que o driver conclua as operações internas para obter a próxima imagem. Portanto, é melhor solicitar pelo menos um a mais do que o mínimo especificado:



uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1;
      
      





É importante não exceder o valor máximo. Um valor 0



indica que nenhum máximo é especificado.



if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) {
    imageCount = swapChainSupport.capabilities.maxImageCount;
}
      
      





A cadeia de troca é um objeto Vulkan, portanto, você precisa preencher a estrutura para criá-la. O início da estrutura já nos é familiar:



VkSwapchainCreateInfoKHR createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
createInfo.surface = surface;
      
      





Primeiro, a superfície é especificada, à qual a cadeia de troca é anexada, então - informações para criar objetos de imagem:



createInfo.minImageCount = imageCount;
createInfo.imageFormat = surfaceFormat.format;
createInfo.imageColorSpace = surfaceFormat.colorSpace;
createInfo.imageExtent = extent;
createInfo.imageArrayLayers = 1;
createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
      
      





Em imageArrayLayers



especifica o número de camadas em que cada imagem consiste. Sempre haverá valor aqui 1



, a menos, é claro, que sejam imagens estéreo. O campo de bits imageUsage



indica para quais operações as imagens obtidas da cadeia de troca serão usadas. No tutorial, iremos renderizar diretamente para eles, mas você pode renderizar para uma imagem separada primeiro, por exemplo, para pós-processamento. Nesse caso, use o valor VK_IMAGE_USAGE_TRANSFER_DST_BIT



e a operação de memória para transferência.



QueueFamilyIndices indices = findQueueFamilies(physicalDevice);
uint32_t queueFamilyIndices[] = {indices.graphicsFamily.value(), indices.presentFamily.value()};

if (indices.graphicsFamily != indices.presentFamily) {
    createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
    createInfo.queueFamilyIndexCount = 2;
    createInfo.pQueueFamilyIndices = queueFamilyIndices;
} else {
    createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
    createInfo.queueFamilyIndexCount = 0; // Optional
    createInfo.pQueueFamilyIndices = nullptr; // Optional
}
      
      





Em seguida, você precisa especificar como lidar com objetos de imagens que são usados ​​em várias famílias de filas. Isso é verdadeiro para casos em que uma família de gráficos e uma família de exibição são famílias diferentes. Vamos renderizar as imagens na fila de gráficos e, em seguida, enviá-las para a fila de exibição.



Existem duas maneiras de processar imagens com acesso de várias filas:



  • VK_SHARING_MODE_EXCLUSIVE



    : um objeto pertence a uma família de filas e a propriedade deve ser transferida explicitamente antes de usá-lo em outra família de filas. Este método oferece o melhor desempenho.

  • VK_SHARING_MODE_CONCURRENT



    : Os objetos podem ser usados ​​em várias famílias de filas sem transferir explicitamente a propriedade.



Se tivermos várias filas, usaremos VK_SHARING_MODE_CONCURRENT



. Este método requer que você especifique com antecedência entre quais famílias de filas a propriedade será compartilhada. Isso pode ser feito usando os parâmetros queueFamilyIndexCount



e pQueueFamilyIndices



. Se a família da fila de gráficos e a família da fila de exibição forem iguais, o que é mais comum, use VK_SHARING_MODE_EXCLUSIVE



.



createInfo.preTransform = swapChainSupport.capabilities.currentTransform;
      
      





Você pode especificar que as imagens na cadeia de troca sejam aplicadas com qualquer uma das transformações com suporte ( supportedTransforms



em capabilities



), por exemplo, girar 90 graus no sentido horário ou girar horizontalmente. Para não aplicar nenhuma transformação, basta sair currentTransform



.



createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
      
      





O campo compositeAlpha



indica se deve ser usado o canal alfa para mesclar com outras janelas no sistema de janelas. Você provavelmente não precisará de um canal alfa, então deixe-o VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR



.



createInfo.presentMode = presentMode;
createInfo.clipped = VK_TRUE;
      
      





O campo presentMode



fala por si. Se o colocarmos VK_TRUE



no campo clipped



, não estaremos interessados ​​em pixels ocultos (por exemplo, se parte de nossa janela estiver coberta por outra janela). Você sempre pode desativar o recorte se precisar ler os pixels, mas por enquanto vamos deixar o recorte ativado.



createInfo.oldSwapchain = VK_NULL_HANDLE;
      
      





O último campo permanece - oldSwapChain



. Se a cadeia de troca se tornar inválida, por exemplo, devido ao redimensionamento da janela, ela precisará ser recriada do zero e no campo oldSwapChain



especificar um link para a cadeia de troca antiga. Este é um tópico complexo que abordaremos em um capítulo posterior. Por enquanto, digamos que temos apenas uma cadeia de troca.



Vamos adicionar um membro da classe para armazenar o objeto VkSwapchainKHR



:



VkSwapchainKHR swapChain;
      
      





Agora você só precisa chamar vkCreateSwapchainKHR



para criar a cadeia de troca:



if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) {
    throw std::runtime_error("failed to create swap chain!");
}
      
      





Os seguintes parâmetros são passados ​​para a função: dispositivo lógico, informações da cadeia de troca, um alocador personalizado opcional e um ponteiro para escrever o resultado. Sem surpresas. A cadeia de troca deve ser destruída usando vkDestroySwapchainKHR



antes que o dispositivo seja destruído:



void cleanup() {
    vkDestroySwapchainKHR(device, swapChain, nullptr);
    ...
}
      
      





Agora vamos executar o programa para garantir que a cadeia de troca foi criada com sucesso. Se você receber uma mensagem de erro ou uma mensagem como « vkGetInstanceProcAddress SteamOverlayVulkanLayer.dll»



, vá para a seção de perguntas frequentes .



Vamos tentar remover a linha createInfo.imageExtent = extent;



com as camadas de validação habilitadas. Um dos níveis de validação detectará imediatamente o erro e nos notificará:



imagem



Obtendo uma imagem de uma cadeia de troca



Agora que a cadeia de troca foi criada, resta obter os descritores VkImages . Vamos adicionar um membro da classe para armazenar descritores:



std::vector<VkImage> swapChainImages;
      
      





Os objetos de imagem da cadeia de troca serão destruídos automaticamente após a própria cadeia de troca ser destruída, portanto, não há necessidade de adicionar qualquer código de limpeza.



Imediatamente após a chamada, vkCreateSwapchainKHR



adicione o código para obter os descritores. Lembre-se de que especificamos apenas o número mínimo de imagens na cadeia de troca, o que significa que pode haver mais imagens. Portanto, primeiro solicitamos o número real de imagens usando a função vkGetSwapchainImagesKHR



, depois alocamos o espaço necessário no contêiner e o chamamos novamente vkGetSwapchainImagesKHR



para obter os descritores.



vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr);
swapChainImages.resize(imageCount);
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data());
      
      





E a última coisa - salve o formato e a resolução das imagens da cadeia de troca em variáveis ​​de classe. Precisaremos deles no futuro.



VkSwapchainKHR swapChain;
std::vector<VkImage> swapChainImages;
VkFormat swapChainImageFormat;
VkExtent2D swapChainExtent;

...

swapChainImageFormat = surfaceFormat.format;
swapChainExtent = extent;
      
      





Agora temos uma imagem para desenhar e exibir. No próximo capítulo, mostraremos como configurar uma imagem para ser usada como alvos de renderização e começar a usar o pipeline gráfico e os comandos de desenho!



C ++



All Articles