Vulkan. Guía del desarrollador. Capas de validación

Soy traductor de CG Tribe en Izhevsk, y aquí estoy compartiendo la traducción del manual de la API de Vulkan. Enlace fuente: vulkan-tutorial.com .



Esta publicación es una continuación de la publicación anterior " Vulkan. Guía del desarrollador. Dibujar un triángulo ", está dedicada a la traducción del capítulo Capas de validación.



Contenido
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







Capas de validación







¿Qué son las capas de validación?



El diseño de la API de Vulkan se basa en la idea de una carga mínima en el controlador, por lo que, por defecto, la capacidad de detectar errores está muy limitada. Incluso errores tan simples como valores incorrectos en las enumeraciones o pasar punteros nulos generalmente no se manejan explícitamente y conducen a fallas o comportamientos indefinidos. Dado que trabajar con Vulkan requiere una descripción detallada de cada acción, estos errores pueden ocurrir con bastante frecuencia.



Para resolver este problema, Vulkan usa capas de validación . Las capas de validación son componentes opcionales que se pueden conectar a llamadas de función para realizar operaciones adicionales. Las siguientes operaciones se pueden realizar en las capas de validación:



  • Comprobación de los valores de los parámetros según la especificación para detectar errores
  • Seguimiento de fugas de recursos
  • Control de seguridad de transmisión
  • Registro de cada llamada y sus parámetros
  • Seguimiento de llamadas de Vulkan para creación de perfiles y reproducción


A continuación se muestra un ejemplo de cómo se podría implementar una función en la capa de validación:



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);
}
      
      





Puede combinar capas de validación entre sí para utilizar todas las funciones de depuración que necesita. Además, las capas de validación se pueden habilitar para las compilaciones de depuración y deshabilitarlas por completo para las versiones de lanzamiento, lo cual es muy conveniente.



Vulkan no tiene capas de validación integradas, pero Vulkan SDK de LunarG proporciona un buen conjunto de capas para rastrear los errores más comunes. Todas las capas son de código abierto y siempre puede ver qué errores están rastreando. Gracias a las capas de validación, puede evitar errores en diferentes controladores asociados con un comportamiento indefinido.



Para utilizar capas de validación, deben estar instaladas en el sistema. Por ejemplo, las capas de validación de LunarG solo están disponibles si está instalado Vulkan SDK.



Anteriormente, Vulkan tenía dos tipos de capas de validación: específicas de la instancia y específicas del dispositivo. La conclusión es que las capas de instancia verifican llamadas relacionadas con objetos Vulkan globales, mientras que las capas de dispositivos solo verifican llamadas relacionadas con una GPU específica. En este punto, las capas de dispositivos están en desuso, por lo que las capas de validación de instancias se aplican a todas las llamadas de Vulkan. La especificación aún recomienda la inclusión de capas de validación a nivel de dispositivo, incluso para proporcionar compatibilidad, que es necesaria para algunas implementaciones. Especificaremos las mismas capas para la instancia y el dispositivo lógico, sobre lo que aprenderemos un poco más adelante.



Usando capas de validación



En esta sección, veremos cómo conectar las capas proporcionadas por Vulkan SDK. Además de las extensiones, debemos especificar los nombres de las capas para conectarlas. Todos los cheques que nos son útiles se recopilan en una capa denominada " VK_LAYER_KHRONOS_validation



".



Agreguemos dos constantes de configuración. El primero (validationLayers) enumerará las capas de validación que queremos incluir. El segundo (enableValidationLayers) permitirá la conexión dependiendo del modo de construcción. Esta macro NDEBUG



es parte del estándar C ++ y significa "no 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
      
      





Agreguemos una nueva función checkValidationLayerSupport



que verificará si todas las capas requeridas están disponibles. Primero, obtengamos una lista de capas disponibles usando vkEnumerateInstanceLayerProperties



. Su uso es similar a la función que vkEnumerateInstanceExtensionProperties



vimos anteriormente.



bool checkValidationLayerSupport() {
    uint32_t layerCount;
    vkEnumerateInstanceLayerProperties(&layerCount, nullptr);

    std::vector<VkLayerProperties> availableLayers(layerCount);
    vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data());

    return false;
      
      





Después de eso, verifique si todas las capas de validationLayers



están presentes en availableLayers



. Es posible que deba conectarse <cstring>



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





La función ahora se puede utilizar en createInstance



:



void createInstance() {
    if (enableValidationLayers && !checkValidationLayerSupport()) {
        throw std::runtime_error("validation layers requested, but not available!");
    }

    ...
}
      
      





Ejecute el programa en modo de depuración y asegúrese de que no haya errores.



En la estructura, VkInstanceCreateInfo



especifique los nombres de las capas de validación conectadas:



if (enableValidationLayers) {
    createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
    createInfo.ppEnabledLayerNames = validationLayers.data();
} else {
    createInfo.enabledLayerCount = 0;
}
      
      





Si nuestra verificación fue aprobada, vkCreateInstance



no debería devolver un error VK_ERROR_LAYER_NOT_PRESENT



, pero es mejor verificar esto ejecutando el programa.



Intercepción de mensajes de depuración



De forma predeterminada, las capas de validación envían mensajes de depuración a la salida estándar, pero puede manejarlos usted mismo proporcionando una función de devolución de llamada. Esto le permitirá filtrar los mensajes que le gustaría recibir, ya que no todos contienen advertencias de error. Si desea omitir este paso, vaya directamente a la última sección del capítulo.



Para conectar una función de devolución de llamada para procesar mensajes, debe configurar un debug messenger usando la extensión VK_EXT_debug_utils



.



Primero, agreguemos una función getRequiredExtensions



que devolverá la lista requerida de extensiones dependiendo de si las capas de validación están conectadas o no.



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





Se requieren extensiones GLFW y la extensión debug messenger se agrega según las condiciones. Tenga en cuenta que estamos utilizando una macro VK_EXT_DEBUG_UTILS_EXTENSION_NAME



para evitar errores tipográficos.



Ahora podemos usar esta función en createInstance



:



auto extensions = getRequiredExtensions();
createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size());
createInfo.ppEnabledExtensionNames = extensions.data();
      
      





Ejecute el programa para verificar si recibimos un error VK_ERROR_EXTENSION_NOT_PRESENT



.



Ahora veamos cuál es la función de devolución de llamada en sí. Agreguemos un nuevo método estático con un prototipo PFN_vkDebugUtilsMessengerCallbackEXT



. VKAPI_ATTR



y VKAPI_CALL



asegúrese de que el método tenga la firma correcta.



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





El primer parámetro determina la gravedad de los mensajes, que son:



  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT



    : mensaje de diagnóstico
  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT



    : mensaje informativo, por ejemplo, sobre la creación de un recurso
  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT



    : un mensaje sobre el comportamiento que no es necesariamente incorrecto, pero que probablemente indica un error
  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT



    : mensaje sobre un comportamiento incorrecto que podría provocar un bloqueo


Los valores para la enumeración se eligen de tal manera que pueda utilizar la operación de comparación para filtrar los mensajes por encima o por debajo de algún umbral, por ejemplo:



if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) {
    // Message is important enough to show
}
      
      





El parámetro messageType



puede tener los siguientes valores:



  • VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT



    : el evento que ocurrió no está relacionado con la especificación o el rendimiento
  • VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT



    : el evento ocurrido viola la especificación o indica un posible error
  • VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT



    : Vulkan puede no usarse de manera óptima


El parámetro se pCallbackData



refiere a una estructura VkDebugUtilsMessengerCallbackDataEXT



que contiene los detalles del mensaje. Los miembros más importantes de la estructura son:



  • pMessage



    : mensaje de depuración como una cadena terminada en nulo
  • pObjects



    : una matriz de descriptores de objetos relacionados con el mensaje
  • objectCount



    : número de objetos en la matriz


El parámetro pUserData



contiene el puntero pasado durante la configuración de la función de devolución de llamada.



La función de devolución de llamada devuelve un VkBool32



tipo. El resultado indica si debe terminar la llamada que generó el mensaje. Si la función de devolución de llamada regresa VK_TRUE



, la llamada se cancela y se devuelve un código de error VK_ERROR_VALIDATION_FAILED_EXT



. Como regla general, esto sucede solo cuando se prueban las capas de validación, en nuestro caso, debe regresar VK_FALSE



.



Queda por decirle a Vulkan sobre la función de devolución de llamada. Sorprendentemente, incluso controlar una función de devolución de llamada de depuración en Vulkan requiere un descriptor que debe crearse y destruirse explícitamente. Esta función de devolución de llamada es parte del debug messengery su número es ilimitado. Agregue un miembro de la clase para el descriptor después de instance



:



VkDebugUtilsMessengerEXT debugMessenger;
      
      





Ahora agregue una función setupDebugMessenger



para ser llamada initVulkan



justo después createInstance



:



void initVulkan() {
    createInstance();
    setupDebugMessenger();
}

void setupDebugMessenger() {
    if (!enableValidationLayers) return;

}
      
      





Necesitamos completar la estructura con detalles sobre el mensajero y su función de devolución de llamada:



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
      
      





El campo le messageSeverity



permite especificar la gravedad por la cual se llamará a la función de devolución de llamada. Configuramos todos los grados, excepto VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT



para ser notificado de posibles problemas y no ensuciar la consola con información detallada de depuración.



Del mismo modo, el campo le messageType



permite filtrar mensajes por tipo. Hemos seleccionado todos los tipos, pero siempre puede desactivar los innecesarios.



Se pfnUserCallback



pasa un puntero a la función de devolución de llamada al campo . Opcionalmente, puede pasar un puntero al campo pUserData



, se pasará a la función de devolución de llamada a través de un parámetro pUserData



.



Tenga en cuenta que hay otras formas de personalizar los mensajes de la capa de validación y depurar las devoluciones de llamada, pero esta es la mejor manera de comenzar con Vulkan. Para obtener más información sobre otros métodos, consulte la especificación de la extensión .



La estructura debe pasarse a la función vkCreateDebugutilsMessengerEXT



para crear el objeto VkDebugUtilsMessengerEXT



. Esta es una función de extensión, por lo que no se carga automáticamente. Necesita encontrar su dirección usted mismo usando vkGetInstanceProcAddr



. Crearemos nuestra propia función de proxy que hace esto internamente. Agréguelo antes de la definición de clase 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 función para crear un mensajero:



if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) {
    throw std::runtime_error("failed to set up debug messenger!");
}
      
      





El penúltimo parámetro es opcional, es la función de devolución de llamada del asignador, que especificaremos como nullptr



. El resto de los parámetros son bastante simples. Dado que el mensajero se usa para una instancia de Vulkan específica (y sus capas de validación), se debe pasar un puntero a esta instancia como primer argumento. Nos encontraremos con este patrón para otros objetos secundarios.



El objeto VkDebugUtilsMessengerEXT



debe destruirse llamando vkDestroyDebugUtilsMessengerEXT



. Además de para vkCreateDebugUtilsMessengerEXT



, debemos cargar esta función explícitamente.



Luego CreateDebugUtilsMessengerEXT



crea otra función 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);
    }
}
      
      





Compruebe que esta función sea una función estática de la clase o una función fuera de la clase. Después de eso, se puede llamar en una función cleanup



:



void cleanup() {
    if (enableValidationLayers) {
        DestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr);
    }

    vkDestroyInstance(instance, nullptr);

    glfwDestroyWindow(window);

    glfwTerminate();
}
      
      







Instancia de depuración de Vulkan



Hemos agregado depuración con capas de validación, pero todavía hay un poco más. Se requiere vkCreateDebugUtilsMessengerEXT



una instancia válida vkDestroyDebugUtilsMessengerEXT



para llamar y se debe llamar antes de que se destruya la instancia. Por lo tanto, no podemos depurar vkCreateInstance



y todavía vkDestroyInstance



.



Sin embargo, si lee la especificación detenidamente , verá que es posible crear un mensajero de depuración independiente para estas dos funciones. Para hacer esto, necesita establecer el puntero de pNext



estructura VkInstanceCreateInfo



en la estructura VkDebugUtilsMessengerCreateInfoEXT



. Primero, muevamos el llenado VkDebugUtilsMessengerCreateInfoEXT



a un 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 reutilizarlo en una función 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!");
    }
}
      
      





La variable debugCreateInfo



está fuera de la instrucción if para que no se destruya antes de ser llamada vkCreateInstance



. Crear un debug messenger adicional de esta manera le permite usarlo automáticamente en vkCreateInstance



y vkDestroyInstance



, después de lo cual será destruido.



Pruebas



Cometemos un error deliberadamente para ver las capas de validación en acción.

Elimine temporalmente la llamada DestroyDebugUtilsMessengerEXT



en la función cleanup



y ejecute el programa. Debería terminar con algo como esto:







para averiguar qué llamada resultó en el envío del mensaje, agregue un punto de interrupción a la función de devolución de llamada del mensaje y observe la pila de llamadas.





Configuraciones



Hay muchas más personalizaciones que gobiernan el comportamiento de los niveles de validación más allá de los especificados en la estructura VkDebugUtilsMessengerCreateInfoEXT



. Vaya a Vulkan SDK y abra el directorio Config



. Allí encontrará un archivo vk_layer_settings.txt



que explica cómo configurar capas.



Para configurar las capas, copiar el archivo en el directorio Debug



y Release



y siga las instrucciones para configurar el comportamiento deseado. Sin embargo, a lo largo del resto de este manual, se asumirá que está utilizando la configuración predeterminada.



En el futuro, cometeremos errores deliberadamente para mostrarle lo conveniente y efectivo que es usar capas de validación para rastrearlos.



Código C ++



All Articles