Vulkan. Guia do desenvolvedor. Desenhe um triângulo

Sou tradutor da CG Tribe em Izhevsk e continuo a carregar traduções do manual da API Vulkan. Link da fonte - vulkan-tutorial.com .



Esta publicação é dedicada à tradução da seção Desenhando um triângulo, ou seja, a subseção Configuração, o Código de base e os capítulos de Instância.



Conteúdo
1.



2.



3.



4.





    • (instance)
  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









Código Base



  • Estrutura geral
  • Gestão de recursos
  • Integração GLFW




Estrutura geral



No capítulo anterior, cobrimos como criar um projeto para Vulkan, configurá-lo corretamente e testá-lo usando um trecho de código. Neste capítulo, começaremos com o básico.



Considere o seguinte código:



#include <vulkan/vulkan.h>

#include <iostream>
#include <stdexcept>
#include <cstdlib>

class HelloTriangleApplication {
public:
    void run() {
        initVulkan();
        mainLoop();
        cleanup();
    }

private:
    void initVulkan() {

    }

    void mainLoop() {

    }

    void cleanup() {

    }
};

int main() {
    HelloTriangleApplication app;

    try {
        app.run();
    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}
      
      





Primeiro, incluímos o arquivo de cabeçalho Vulkan do SDK do LunarG. Arquivos de cabeçalho stdexcepts



e iostream



são usados ​​para tratamento e distribuição de erros. O arquivo de cabeçalho cstdlib



fornece macros EXIT_SUCCESS



e EXIT_FAILURE



.



O programa em si é encapsulado na classe HelloTriangleApplication, na qual armazenaremos objetos Vulkan como membros privados da classe. Lá, também adicionaremos funções para inicializar cada objeto, chamadas a partir da função initVulkan



. Depois disso, vamos criar um loop principal para renderizar frames. Para fazer isso, preencha uma função mainLoop



onde o loop será executado até que a janela seja fechada. Após fechar a janela e sair, os mainLoop



recursos devem ser liberados. Para fazer isso, preencha cleanup



.



Se ocorrer um erro crítico durante a operação, lançaremos uma exceção std::runtime_error



que será detectada na função main



e a descrição será exibida em std::cerr



. Um desses erros pode ser, por exemplo, uma mensagem de que a extensão necessária não é compatível. Para lidar com muitos dos tipos de exceção padrão, pegamos um mais geral std::exception



.



Quase todos os capítulos subsequentes irão adicionar novas funções chamadas de initVulkan



e novos objetos Vulkan que precisam ser liberados cleanup



quando o programa termina.



Gestão de recursos



Se os objetos Vulkan não forem mais necessários, eles devem ser destruídos. C ++ permite que você desaloque recursos automaticamente usando RAII ou ponteiros inteligentes fornecidos pelo arquivo de cabeçalho <memory>



. No entanto, neste tutorial, decidimos escrever explicitamente quando alocar e desalocar objetos Vulkan. Afinal, essa é a peculiaridade do trabalho de Vulkan - descrever detalhadamente cada operação para evitar possíveis erros.



Depois de ler o tutorial, você pode implementar o gerenciamento automático de recursos escrevendo classes C ++ que recebem objetos Vulkan no construtor e os liberam no destruidor. Você também pode implementar seu próprio deleter para std::unique_ptr



ou std::shared_ptr



, dependendo de seus requisitos. O conceito RAII é recomendado para programas maiores, mas é útil aprender mais sobre ele.



Objetos Vulkan são criados diretamente usando uma função como vkCreateXXX , ou alocados por meio de outro objeto usando uma função como vkAllocateXXX . Depois de se certificar de que o objeto não está em uso em nenhum outro lugar, você deve destruí-lo com vkDestroyXXX ou vkFreeXXX . Os parâmetros para esses recursos normalmente variar dependendo do tipo de objeto, mas não há um parâmetro comum: pAllocator



. Este é um parâmetro opcional que permite usar retornos de chamada para alocação de memória personalizada. Não vamos precisar disso no manual, vamos passar como um argumento nullptr



.



Integração GLFW



Vulkan funciona bem sem criar uma janela ao usar a renderização fora da tela, mas muito melhor quando o resultado é visível na tela.

Primeiro, substitua a linha pelo #include <vulkan/vulkan.h>



seguinte:



#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
      
      





Adicione uma função initWindow



e adicione sua chamada do método run



antes de outras chamadas. Usaremos o initWindow



GLFW para inicializar e criar uma janela.



void run() {
    initWindow();
    initVulkan();
    mainLoop();
    cleanup();
}

private:
    void initWindow() {

    }
      
      





A primeira chamada a initWindow



deve ser uma função glfwInit()



que inicializa a biblioteca GLFW. GLFW foi originalmente projetado para funcionar com OpenGL. Não precisamos de um contexto OpenGL, então indique que não precisamos criá-lo usando a seguinte chamada:



glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
      
      





Desative temporariamente a capacidade de redimensionar a janela, uma vez que lidar com essa situação requer consideração separada:



glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
      
      





Resta criar uma janela. Para fazer isso, adicione um membro privado GLFWwindow* window;



e inicialize a janela com:



window = glfwCreateWindow(800, 600, "Vulkan", nullptr, nullptr);
      
      





Os primeiros três parâmetros definem a largura, altura e título da janela. O quarto parâmetro é opcional, ele permite que você especifique o monitor no qual a janela será exibida. O último parâmetro é específico para OpenGL.



Seria bom usar constantes para a largura e altura da janela, pois precisaremos desses valores em outro lugar. Adicione as seguintes linhas antes da definição da classe HelloTriangleApplication



:



const uint32_t WIDTH = 800;
const uint32_t HEIGHT = 600;
      
      





e substitua a chamada para criar uma janela com



window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
      
      





Você deve ter a seguinte função initWindow



:



void initWindow() {
    glfwInit();

    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
    glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);

    window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
}


      
      





Vamos descrever o loop principal no método mainLoop



para manter o aplicativo em execução até que a janela seja fechada:



void mainLoop() {
    while (!glfwWindowShouldClose(window)) {
        glfwPollEvents();
    }
}
      
      





Este código não deve levantar dúvidas. Ele lida com eventos como pressionar o botão X antes que o usuário feche a janela. Também a partir desse loop chamaremos uma função para renderizar quadros individuais.



Após fechar a janela, precisamos liberar recursos e sair do GLFW. Primeiro, vamos adicionar o cleanup



seguinte código:



void cleanup() {
    glfwDestroyWindow(window);

    glfwTerminate();
}
      
      





Como resultado, após iniciar o programa, você verá uma janela com um nome Vulkan



que será exibida até que o programa seja fechado. Agora que temos um esqueleto para trabalhar com o Vulkan, vamos prosseguir para a criação de nosso primeiro objeto Vulkan!



Código C ++











Instância



  • Instanciação
  • Verificando extensões com suporte
  • Limpeza




Instanciação



A primeira coisa que você precisa fazer é criar uma instância para inicializar a biblioteca. Uma instância é o link entre seu programa e a biblioteca Vulkan e, para criá-la, você precisará fornecer ao driver algumas informações sobre seu programa.



Adicione um método createInstance



e chame-o de uma função initVulkan



.



void initVulkan() {
    createInstance();
}
      
      





Adicione um membro de instância à nossa classe para manter um identificador de instância:



private:
VkInstance instance;
      
      





Agora precisamos preencher uma estrutura especial com informações sobre o programa. Tecnicamente, os dados são opcionais, no entanto, isso permitirá que o motorista obtenha informações úteis para otimizar o trabalho com seu programa. Essa estrutura é chamada de VkApplicationInfo



:



void createInstance() {
    VkApplicationInfo appInfo{};
    appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
    appInfo.pApplicationName = "Hello Triangle";
    appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
    appInfo.pEngineName = "No Engine";
    appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
    appInfo.apiVersion = VK_API_VERSION_1_0;
}
      
      





Conforme mencionado, muitas estruturas em Vulkan requerem uma definição de tipo explícita no membro sType . Além disso, esta estrutura, como muitas outras, contém um elemento pNext



que permite fornecer informações para extensões. Usamos a inicialização de valor para preencher a estrutura com zeros.



A maioria das informações no Vulkan é passada por estruturas, então você precisa preencher mais uma estrutura para fornecer informações suficientes para criar uma instância. A estrutura a seguir é necessária, ela informa ao driver quais extensões globais e camadas de validação queremos usar. "Global" significa que as extensões se aplicam a todo o programa e não a um dispositivo específico.



VkInstanceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
      
      





Os primeiros dois parâmetros não levantam dúvidas. Os próximos dois membros definem as extensões globais necessárias. Como você já sabe, a API Vulkan é totalmente independente de plataforma. Isso significa que você precisa de uma extensão para interagir com o sistema de janelas. O GLFW tem uma função interna útil que retorna uma lista de extensões necessárias.



uint32_t glfwExtensionCount = 0;
const char** glfwExtensions;

glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);

createInfo.enabledExtensionCount = glfwExtensionCount;
createInfo.ppEnabledExtensionNames = glfwExtensions;
      
      





Os dois últimos membros da estrutura definem quais camadas de validação global incluir. Falaremos sobre eles com mais detalhes no próximo capítulo, portanto, deixe esses valores em branco por enquanto.



createInfo.enabledLayerCount = 0;
      
      





Agora você fez tudo o que era necessário para criar uma instância. Faça uma chamada vkCreateInstance



:



VkResult result = vkCreateInstance(&createInfo, nullptr, &instance);
      
      





Via de regra, os parâmetros das funções para a criação de objetos estão nesta ordem:



  • Ponteiro para uma estrutura com as informações necessárias
  • Ponteiro para alocador personalizado
  • Ponteiro para uma variável onde o descritor do novo objeto será escrito


Se tudo for feito corretamente, o descritor da instância será armazenado na instância . Quase todas as funções Vulkan retornam um valor VkResult , que pode ser um VK_SUCCESS



código de erro ou um código de erro. Não precisamos armazenar o resultado para garantir que a instância foi criada. Vamos usar uma verificação simples:



if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
    throw std::runtime_error("failed to create instance!");
}
      
      





Agora execute o programa para verificar se a instância foi criada com sucesso.



Verificando extensões com suporte



Se olharmos a documentação do Vulkan , podemos descobrir que um dos possíveis códigos de erro é VK_ERROR_EXTENSION_NOT_PRESENT



. Podemos simplesmente especificar as extensões necessárias e parar de trabalhar se elas não forem suportadas. Isso faz sentido para extensões principais, como a interface do sistema de janelas, mas e se quisermos testar os recursos opcionais?



Para obter uma lista de extensões suportadas antes de instanciar, use a função vkEnumerateInstanceExtensionProperties... O primeiro parâmetro da função é opcional, permite filtrar extensões por uma camada de validação específica, por isso vamos deixá-lo vazio por enquanto. A função também requer um ponteiro para uma variável, onde o número de extensões será escrito e um ponteiro para uma área da memória onde as informações sobre eles devem ser gravadas.



Para alocar memória para armazenar informações de ramal, primeiro você precisa saber o número de ramais. Deixe o último parâmetro em branco para solicitar o número de extensões:



uint32_t extensionCount = 0;
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr);
      
      





Aloque uma matriz para armazenar informações de extensão (não se esqueça include <vector>



):



std::vector<VkExtensionProperties> extensions(extensionCount);
      
      





Agora você pode solicitar informações sobre extensões.



vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data());
      
      





Cada estrutura VkExtensionProperties contém o nome e a versão da extensão. Eles podem ser listados com um loop for simples ( \t



aqui está a guia de recuo):



std::cout << "available extensions:\n";

for (const auto& extension : extensions) {
    std::cout << '\t' << extension.extensionName << '\n';
}
      
      





Você pode adicionar este código a uma função createInstance



para obter mais informações sobre o suporte Vulkan. Você também pode tentar criar uma função que verificará se todas as extensões retornadas pela função glfwGetRequiredInstanceExtensions



estão incluídas na lista de extensões suportadas.





Limpeza



VkInstance deve ser destruído antes de fechar o programa. Isso pode ser feitocleanup



usando a função VkDestroyInstance :



void cleanup() {
    vkDestroyInstance(instance, nullptr);

    glfwDestroyWindow(window);

    glfwTerminate();
}
      
      





Os parâmetros da função vkDestroyInstance são autoexplicativos. Conforme mencionado no capítulo anterior, as funções de alocação e desalocação no Vulkan aceitam ponteiros opcionais para alocadores personalizados que não usamos e passamos nullptr



. Todos os outros recursos do Vulkan devem ser limpos antes que a instância seja destruída.



Antes de passar para etapas mais complexas, precisamos configurar as camadas de validação para facilitar a depuração.



Código C ++



All Articles