Vulkan. Guia do desenvolvedor. Camadas de validação

Eu sou um tradutor da CG Tribe em Izhevsk, e aqui estou compartilhando uma tradução do manual da API Vulkan. Link da fonte - vulkan-tutorial.com .



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
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







Camadas de validação







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óstico
  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT



    : mensagem informativa, por exemplo, sobre a criação de um recurso
  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT



    : mensagem sobre o comportamento que não é necessariamente incorreto, mas provavelmente indica um erro
  • VK_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 desempenho
  • VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT



    : o evento ocorrido viola a especificação ou indica um possível erro
  • VK_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 nulo
  • pObjects



    : uma matriz de descritores de objetos relacionados à mensagem
  • objectCount



    : 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 ++



All Articles