Este post é uma continuação do post anterior " Vulkan. Guia do desenvolvedor. Desenhando um triângulo ", é dedicado à tradução das camadas de validação do capítulo.
Conteúdo
Camadas de validação
- O que são camadas de validação?
- Usando camadas de validação
- Interceptação de mensagens de depuração
- Instância de depuração de Vulkan
- Testando
- Configurações
O que são camadas de validação?
O design da API Vulkan é baseado na ideia de carga mínima no driver, portanto, por padrão, a capacidade de detectar erros é severamente limitada. Mesmo erros simples, como valores incorretos em enumerações ou passagem de ponteiros nulos, geralmente não são tratados explicitamente e levam a travamentos ou comportamento indefinido. Como trabalhar com o Vulkan requer uma descrição detalhada de cada ação, esses erros podem ocorrer com bastante frequência.
Para resolver esse problema, Vulkan usa camadas de validação . Camadas de validação são componentes opcionais que podem ser conectados em chamadas de função para executar operações adicionais. As seguintes operações podem ser realizadas nas camadas de validação:
- Verificar os valores dos parâmetros de acordo com a especificação para detectar erros
- Rastreamento de vazamento de recursos
- Verificação de segurança de streaming
- Registro de cada chamada e seus parâmetros
- Vulkan Call Tracking for Profiling and Replay
Abaixo está um exemplo de como uma função pode ser implementada na camada de validação:
VkResult vkCreateInstance(
const VkInstanceCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkInstance* instance) {
if (pCreateInfo == nullptr || instance == nullptr) {
log("Null pointer passed to required parameter!");
return VK_ERROR_INITIALIZATION_FAILED;
}
return real_vkCreateInstance(pCreateInfo, pAllocator, instance);
}
Você pode combinar camadas de validação entre si para usar todos os recursos de depuração de que precisa. Além disso, as camadas de validação podem ser habilitadas para compilações de depuração e completamente desabilitadas para compilações de lançamento, o que é muito conveniente.
Vulkan não tem camadas de validação integradas, mas o Vulkan SDK da LunarG fornece um bom conjunto de camadas para rastrear os bugs mais comuns. Todas as camadas são de código aberto e você sempre pode ver quais bugs estão rastreando. Graças às camadas de validação, você pode evitar erros em diferentes drivers associados a comportamento indefinido.
Para usar camadas de validação, elas devem ser instaladas no sistema. Por exemplo, as camadas de validação do LunarG só estão disponíveis se o Vulkan SDK estiver instalado.
Anteriormente, o Vulkan tinha dois tipos de camadas de validação: específicas da instância e específicas do dispositivo. O resultado final é que as camadas de instância verificam chamadas relacionadas a objetos Vulkan globais, enquanto as camadas de dispositivo verificam apenas chamadas relacionadas a uma GPU específica. Neste ponto, as camadas de dispositivo estão obsoletas, portanto, as camadas de validação de instância são aplicadas a todas as chamadas Vulkan. A especificação continua recomendando a inclusão de camadas de validação no nível do dispositivo, incluindo para interoperabilidade, que é necessária para algumas implementações. Especificaremos as mesmas camadas para a instância e o dispositivo lógico, sobre as quais aprenderemos um pouco mais tarde.
Usando camadas de validação
Nesta seção, veremos como conectar as camadas fornecidas pelo Vulkan SDK. Bem como para extensões, devemos especificar os nomes das camadas para conectá-los. Todos os cheques úteis para nós são coletados em uma camada chamada "
VK_LAYER_KHRONOS_validation
".
Vamos adicionar duas constantes de configuração. O primeiro (validationLayers) listará quais camadas de validação queremos incluir. O segundo (enableValidationLayers) permitirá a conexão dependendo do modo de construção. Esta macro
NDEBUG
faz parte do padrão C ++ e significa “não depurar”.
const uint32_t WIDTH = 800;
const uint32_t HEIGHT = 600;
const std::vector<const char*> validationLayers = {
"VK_LAYER_KHRONOS_validation"
};
#ifdef NDEBUG
const bool enableValidationLayers = false;
#else
const bool enableValidationLayers = true;
#endif
Vamos adicionar uma nova função
checkValidationLayerSupport
que irá verificar se todas as camadas necessárias estão disponíveis. Primeiro, vamos obter uma lista de camadas disponíveis usando
vkEnumerateInstanceLayerProperties
. Seu uso é semelhante à função
vkEnumerateInstanceExtensionProperties
que examinamos anteriormente.
bool checkValidationLayerSupport() {
uint32_t layerCount;
vkEnumerateInstanceLayerProperties(&layerCount, nullptr);
std::vector<VkLayerProperties> availableLayers(layerCount);
vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data());
return false;
Depois disso, verifique se todas as camadas de
validationLayers
estão presentes em
availableLayers
. Pode ser necessário conectar-se
<cstring>
a
strcmp
.
for (const char* layerName : validationLayers) {
bool layerFound = false;
for (const auto& layerProperties : availableLayers) {
if (strcmp(layerName, layerProperties.layerName) == 0) {
layerFound = true;
break;
}
}
if (!layerFound) {
return false;
}
}
return true;
A função agora pode ser usada em
createInstance
:
void createInstance() {
if (enableValidationLayers && !checkValidationLayerSupport()) {
throw std::runtime_error("validation layers requested, but not available!");
}
...
}
Execute o programa em modo de depuração e certifique-se de que não haja erros.
Na estrutura,
VkInstanceCreateInfo
especifique os nomes das camadas de validação conectadas:
if (enableValidationLayers) {
createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
createInfo.ppEnabledLayerNames = validationLayers.data();
} else {
createInfo.enabledLayerCount = 0;
}
Se nossa verificação foi aprovada,
vkCreateInstance
não deve retornar um erro
VK_ERROR_LAYER_NOT_PRESENT
, mas é melhor verificar isso executando o programa.
Interceptação de mensagens de depuração
Por padrão, as camadas de validação enviam mensagens de depuração para a saída padrão, mas você mesmo pode manipulá-las fornecendo uma função de retorno de chamada. Isso permitirá que você filtre as mensagens que deseja receber, pois nem todas elas contêm avisos de erro. Se você quiser pular esta etapa, pule direto para a última seção do capítulo.
Para conectar uma função de retorno de chamada para processar mensagens, você precisa configurar um mensageiro de depuração usando o
VK_EXT_debug_utils
.
Primeiro, vamos adicionar uma função
getRequiredExtensions
que retornará a lista necessária de extensões dependendo se as camadas de validação estão conectadas ou não.
std::vector<const char*> getRequiredExtensions() {
uint32_t glfwExtensionCount = 0;
const char** glfwExtensions;
glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
std::vector<const char*> extensions(glfwExtensions, glfwExtensions + glfwExtensionCount);
if (enableValidationLayers) {
extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
}
return extensions;
}
As extensões do GLFW são necessárias e a extensão do mensageiro de depuração é adicionada com base nas condições. Observe que estamos usando uma macro
VK_EXT_DEBUG_UTILS_EXTENSION_NAME
para evitar erros de digitação.
Agora podemos usar esta função em
createInstance
:
auto extensions = getRequiredExtensions();
createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size());
createInfo.ppEnabledExtensionNames = extensions.data();
Execute o programa para verificar se recebemos um erro
VK_ERROR_EXTENSION_NOT_PRESENT
.
Agora vamos ver o que é a própria função de retorno de chamada. Vamos adicionar um novo método estático com um protótipo
PFN_vkDebugUtilsMessengerCallbackEXT
.
VKAPI_ATTR
e
VKAPI_CALL
certifique-se de que o método tenha a assinatura correta.
static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(
VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
VkDebugUtilsMessageTypeFlagsEXT messageType,
const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
void* pUserData) {
std::cerr << "validation layer: " << pCallbackData->pMessage << std::endl;
return VK_FALSE;
}
O primeiro parâmetro determina a gravidade das mensagens, que são:
VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT
: mensagem de diagnósticoVK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT
: mensagem informativa, por exemplo, sobre a criação de um recursoVK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT
: mensagem sobre o comportamento que não é necessariamente incorreto, mas provavelmente indica um erroVK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT
: mensagem sobre comportamento incorreto que pode levar a uma falha
Os valores para a enumeração são escolhidos de forma que você possa usar a operação de comparação para filtrar mensagens acima ou abaixo de algum limite, por exemplo:
if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) {
// Message is important enough to show
}
O parâmetro
messageType
pode ter os seguintes valores:
VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT
: o evento que ocorreu não está relacionado à especificação ou desempenhoVK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT
: o evento ocorrido viola a especificação ou indica um possível erroVK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT
: Vulkan pode não ser usado de forma otimizada
O parâmetro
pCallbackData
se refere a uma estrutura
VkDebugUtilsMessengerCallbackDataEXT
que contém os detalhes da mensagem. Os membros mais importantes da estrutura são:
pMessage
: mensagem de depuração como uma string terminada em nulopObjects
: uma matriz de descritores de objetos relacionados à mensagemobjectCount
: número de objetos na matriz
O parâmetro
pUserData
contém o ponteiro passado durante a configuração da função de retorno de chamada.
A função de retorno de chamada retorna um
VkBool32
tipo. O resultado indica se é necessário encerrar a chamada que gerou a mensagem. Se a função de retorno de chamada retornar
VK_TRUE
, a chamada será cancelada e um código de erro será retornado
VK_ERROR_VALIDATION_FAILED_EXT
. Como regra, isso acontece apenas ao testar as próprias camadas de validação, em nosso caso, você precisa retornar
VK_FALSE
.
Resta dizer a Vulkan sobre a função de retorno de chamada. Surpreendentemente, mesmo o controle de uma função de retorno de chamada de depuração no Vulkan requer um descritor que deve ser explicitamente criado e destruído. Esta função de retorno de chamada é parte do mensageiro de depuração, e seu número é ilimitado. Adicione um membro da classe para o descritor após
instance
:
VkDebugUtilsMessengerEXT debugMessenger;
Agora adicione uma função
setupDebugMessenger
que será chamada
initVulkan
logo a seguir
createInstance
:
void initVulkan() {
createInstance();
setupDebugMessenger();
}
void setupDebugMessenger() {
if (!enableValidationLayers) return;
}
Precisamos preencher a estrutura com detalhes sobre o messenger e suas funções de retorno de chamada:
VkDebugUtilsMessengerCreateInfoEXT createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
createInfo.pfnUserCallback = debugCallback;
createInfo.pUserData = nullptr; // Optional
O campo
messageSeverity
permite que você especifique a gravidade para a qual a função de retorno de chamada será chamada. Ajustamos todos os níveis, exceto
VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT
para sermos notificados de possíveis problemas e não para confundir o console com informações detalhadas de depuração.
Da mesma forma, o campo
messageType
permite filtrar mensagens por tipo. Selecionamos todos os tipos, mas você sempre pode desativar os desnecessários.
Um
pfnUserCallback
ponteiro para a função de retorno de chamada é passado para o campo . Opcionalmente, você pode passar um ponteiro para o campo
pUserData
, ele será passado para a função de retorno de chamada por meio de um parâmetro
pUserData
.
Observe que há outras maneiras de personalizar as mensagens da camada de validação e depurar retornos de chamada, mas esta é a melhor maneira de começar a usar o Vulkan. Para obter mais informações sobre outros métodos, consulte a especificação da extensão .
A estrutura deve ser passada para a função
vkCreateDebugutilsMessengerEXT
de criação do objeto
VkDebugUtilsMessengerEXT
. Este é um recurso de extensão, portanto, não é carregado automaticamente. Você precisa encontrar seu endereço sozinho usando
vkGetInstanceProcAddr
. Criaremos nossa própria função de proxy que faz isso internamente. Adicione-o antes da definição da classe
HelloTriangleApplication
.
VkResult CreateDebugUtilsMessengerEXT(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pDebugMessenger) {
auto func = (PFN_vkCreateDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT");
if (func != nullptr) {
return func(instance, pCreateInfo, pAllocator, pDebugMessenger);
} else {
return VK_ERROR_EXTENSION_NOT_PRESENT;
}
}
Usamos esta função para criar um mensageiro:
if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) {
throw std::runtime_error("failed to set up debug messenger!");
}
O penúltimo parâmetro é opcional, é a função de retorno de chamada do alocador, que iremos especificar como
nullptr
. O resto dos parâmetros são bastante simples. Como o mensageiro é usado para uma instância específica do Vulkan (e suas camadas de validação), um ponteiro para essa instância deve ser passado como o primeiro argumento. Encontraremos esse padrão para outros objetos filhos.
O objeto
VkDebugUtilsMessengerEXT
deve ser destruído chamando
vkDestroyDebugUtilsMessengerEXT
. Além disso
vkCreateDebugUtilsMessengerEXT
, devemos carregar essa função explicitamente.
Em seguida,
CreateDebugUtilsMessengerEXT
crie outra função de proxy:
void DestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT debugMessenger, const VkAllocationCallbacks* pAllocator) {
auto func = (PFN_vkDestroyDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT");
if (func != nullptr) {
func(instance, debugMessenger, pAllocator);
}
}
Verifique se esta função é uma função estática da classe ou uma função fora da classe. Depois disso, ele pode ser chamado em uma função
cleanup
:
void cleanup() {
if (enableValidationLayers) {
DestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr);
}
vkDestroyInstance(instance, nullptr);
glfwDestroyWindow(window);
glfwTerminate();
}
Instância de depuração de Vulkan
Adicionamos depuração com camadas de validação, mas ainda há um pouco mais. Uma
vkCreateDebugUtilsMessengerEXT
instância válida é
vkDestroyDebugUtilsMessengerEXT
necessária para chamar e deve ser chamada antes que a instância seja destruída. Portanto, não podemos depurar no
vkCreateInstance
e ainda
vkDestroyInstance
.
No entanto, se você ler a especificação com atenção , verá que é possível criar um mensageiro de depuração separado para essas duas funções. Para fazer isso, você precisa definir a
pNext
estrutura ponteiro
VkInstanceCreateInfo
para a estrutura
VkDebugUtilsMessengerCreateInfoEXT
. Primeiro, vamos mover o preenchimento para
VkDebugUtilsMessengerCreateInfoEXT
um método separado:
void populateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo) {
createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
createInfo.pfnUserCallback = debugCallback;
}
...
void setupDebugMessenger() {
if (!enableValidationLayers) return;
VkDebugUtilsMessengerCreateInfoEXT createInfo;
populateDebugMessengerCreateInfo(createInfo);
if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) {
throw std::runtime_error("failed to set up debug messenger!");
}
}
Podemos reutilizá-lo em uma função
createInstance
:
void createInstance() {
...
VkInstanceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
...
VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo;
if (enableValidationLayers) {
createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
createInfo.ppEnabledLayerNames = validationLayers.data();
populateDebugMessengerCreateInfo(debugCreateInfo);
createInfo.pNext = (VkDebugUtilsMessengerCreateInfoEXT*) &debugCreateInfo;
} else {
createInfo.enabledLayerCount = 0;
createInfo.pNext = nullptr;
}
if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
throw std::runtime_error("failed to create instance!");
}
}
A variável
debugCreateInfo
está fora da instrução if para que não seja destruída antes de ser chamada
vkCreateInstance
. Criar um mensageiro de depuração adicional desta forma permite que você o use automaticamente no
vkCreateInstance
e
vkDestroyInstance
, após o qual ele será destruído.
Testando
Vamos cometer um erro deliberadamente ao ver as camadas de validação em ação.
Remova temporariamente a chamada
DestroyDebugUtilsMessengerEXT
na função
cleanup
e execute o programa. Você deve terminar com algo assim:
Para descobrir qual chamada resultou no envio da mensagem, adicione um ponto de interrupção à função de retorno de chamada da mensagem e observe a pilha de chamadas.
Configurações
Existem muitas outras personalizações que definem o comportamento dos níveis de validação além daqueles especificados na estrutura
VkDebugUtilsMessengerCreateInfoEXT
. Acesse Vulkan SDK e abra o diretório
Config
. Lá você encontrará um arquivo
vk_layer_settings.txt
que explica como configurar camadas.
Para configurar as camadas, copiar o arquivo para o diretório
Debug
e
Release
e siga as instruções para configurar o comportamento desejado. No entanto, ao longo do restante deste guia, será assumido que você está usando as configurações padrão.
No futuro, cometeremos erros deliberadamente para mostrar como é conveniente e eficaz usar camadas de validação para rastreá-los.
Código C ++