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.
5.
6. Uniform-
7.
8.
9.
10. -
11. Multisampling
FAQ
2.
3.
4.
-
- (instance)
- (pipeline)
5.
- Staging
6. Uniform-
- layout
- sets
7.
- Image view image sampler
- 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 feito
cleanup
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 ++