Vulkan. Entwicklerhandbuch. Validierungsschichten

Ich bin ein Übersetzer von CG Tribe in Ischewsk und teile hier eine Übersetzung des Vulkan API-Handbuchs. Quelllink - vulkan-tutorial.com .



Dieser Beitrag ist eine Fortsetzung des vorherigen Beitrags " Vulkan. Entwicklerhandbuch. Zeichnen eines Dreiecks ". Er ist der Übersetzung des Kapitels Validierungsebenen gewidmet.



Inhalt
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







Validierungsschichten







Was sind Validierungsschichten?



Das Design der Vulkan-API basiert auf der Idee einer minimalen Belastung des Treibers. Daher sind die Fehlererkennungsfunktionen standardmäßig stark eingeschränkt. Selbst einfache Fehler wie falsche Werte in Aufzählungen oder das Übergeben von Nullzeigern werden normalerweise nicht explizit behandelt und führen zu Abstürzen oder undefiniertem Verhalten. Da Vulkan eine detaillierte Beschreibung jeder Aktion benötigt, können solche Fehler häufig auftreten.



Um dieses Problem zu lösen, verwendet Vulkan Validierungsebenen . Validierungsebenen sind optionale Komponenten, die in Funktionsaufrufe eingefügt werden können, um zusätzliche Operationen auszuführen. Die folgenden Vorgänge können in den Validierungsschichten ausgeführt werden:



  • Überprüfen der Parameterwerte gemäß Spezifikation, um Fehler zu erkennen
  • Nachverfolgung von Ressourcenlecks
  • Stream Sicherheitsüberprüfung
  • Protokollierung jedes Anrufs und seiner Parameter
  • Vulkan Call Tracking für Profiling und Replay


Im Folgenden finden Sie ein Beispiel dafür, wie eine Funktion in der Validierungsschicht implementiert werden kann:



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





Sie können Validierungsebenen miteinander kombinieren, um alle erforderlichen Debugging-Funktionen zu nutzen. Außerdem können Validierungsebenen für Debug-Builds aktiviert und für Release-Builds vollständig deaktiviert werden, was sehr praktisch ist.



Vulkan verfügt nicht über integrierte Validierungsebenen, aber das Vulkan SDK von LunarG bietet eine Reihe guter Ebenen, um die häufigsten Fehler zu verfolgen. Alle Ebenen sind Open Source und Sie können immer sehen, welche Fehler sie verfolgen. Dank Validierungsebenen können Sie Fehler bei verschiedenen Treibern vermeiden, die mit undefiniertem Verhalten verbunden sind.



Um Validierungsebenen verwenden zu können, müssen sie auf dem System installiert sein. Beispielsweise sind die Validierungsschichten von LunarG nur verfügbar, wenn das Vulkan SDK installiert ist.



Zuvor hatte Vulkan zwei Arten von Validierungsschichten: instanzspezifisch und gerätespezifisch. Unter dem Strich suchen Instanzebenen nach Aufrufen, die sich auf globale Vulkan-Objekte beziehen, während Geräteebenen nur nach Aufrufen suchen, die sich auf eine bestimmte GPU beziehen. Zu diesem Zeitpunkt sind die Geräteebenen veraltet, sodass die Instanzvalidierungsschichten auf alle Vulkan-Aufrufe angewendet werden. In der Spezifikation wird weiterhin die Einbeziehung von Validierungsschichten auf Geräteebene empfohlen, auch für die Interoperabilität, die für einige Implementierungen erforderlich ist. Wir werden die gleichen Ebenen für die Instanz und das logische Gerät angeben, die wir etwas später kennenlernen werden.



Validierungsschichten verwenden



In diesem Abschnitt erfahren Sie, wie Sie die vom Vulkan SDK bereitgestellten Ebenen verbinden. Genau wie bei Erweiterungen müssen wir die Namen der Ebenen angeben, um sie zu verbinden. Alle für uns nützlichen Schecks werden in einer Ebene mit dem Namen " VK_LAYER_KHRONOS_validation



" gesammelt .



Fügen wir zwei Konfigurationskonstanten hinzu. Die erste (validationLayers) listet auf, welche Validierungsebenen wir einschließen möchten. Die zweite (enableValidationLayers) ermöglicht je nach Erstellungsmodus eine Verbindung. Dieses Makro NDEBUG



ist Teil des C ++ - Standards und steht für "nicht debuggen".



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
      
      





Fügen wir eine neue Funktion hinzu checkValidationLayerSupport



, die prüft, ob alle erforderlichen Ebenen verfügbar sind. Lassen Sie uns zunächst eine Liste der verfügbaren Ebenen mit erstellen vkEnumerateInstanceLayerProperties



. Seine Verwendung ähnelt der Funktion, die vkEnumerateInstanceExtensionProperties



wir zuvor betrachtet haben.



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

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

    return false;
      
      





Überprüfen Sie danach, ob alle Ebenen von validationLayers



vorhanden sind availableLayers



. Möglicherweise müssen Sie eine Verbindung herstellen <cstring>



für 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;
      
      





Die Funktion kann jetzt verwendet werden in createInstance



:



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

    ...
}
      
      





Führen Sie das Programm im Debug-Modus aus und stellen Sie sicher, dass keine Fehler vorliegen.



Geben Sie in der Struktur VkInstanceCreateInfo



die Namen der verbundenen Validierungsschichten an:



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





Wenn unsere Prüfung bestanden wurde, vkCreateInstance



sollte kein Fehler zurückgegeben VK_ERROR_LAYER_NOT_PRESENT



werden. Es ist jedoch besser, dies durch Ausführen des Programms zu überprüfen.



Abfangen von Debug-Meldungen



Standardmäßig senden die Validierungsebenen Debug-Meldungen an die Standardausgabe. Sie können diese jedoch selbst bearbeiten, indem Sie eine Rückruffunktion bereitstellen. Auf diese Weise können Sie die Nachrichten filtern, die Sie erhalten möchten, da nicht alle Fehlerwarnungen enthalten. Wenn Sie diesen Schritt überspringen möchten, fahren Sie direkt mit dem letzten Abschnitt des Kapitels fort.



Um eine Rückruffunktion mit der Verarbeitung von Nachrichten zu verbinden, müssen Sie einen Debug-Messenger mit der Erweiterung konfigurieren VK_EXT_debug_utils



.



Fügen Sie zunächst eine Funktion hinzu getRequiredExtensions



, die die erforderliche Liste der Erweiterungen zurückgibt, je nachdem, ob die Validierungsebenen verbunden sind oder nicht.



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





GLFW-Erweiterungen sind erforderlich, und die Debug-Messenger-Erweiterung wird basierend auf den Bedingungen hinzugefügt. Bitte beachten Sie, dass wir ein Makro verwenden VK_EXT_DEBUG_UTILS_EXTENSION_NAME



, um Tippfehler zu vermeiden.



Wir können diese Funktion jetzt verwenden in createInstance



:



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





Führen Sie das Programm aus, um zu überprüfen, ob ein Fehler aufgetreten ist VK_ERROR_EXTENSION_NOT_PRESENT



.



Nun wollen wir sehen, was die Rückruffunktion selbst ist. Fügen wir eine neue statische Methode mit einem Prototyp hinzu PFN_vkDebugUtilsMessengerCallbackEXT



. VKAPI_ATTR



und VKAPI_CALL



stellen Sie sicher, dass die Methode die richtige Signatur hat.



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





Der erste Parameter bestimmt den Schweregrad der Nachrichten:



  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT



    : Diagnosemeldung
  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT



    : Informationsnachricht zum Beispiel zum Erstellen einer Ressource
  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT



    : Eine Meldung über ein Verhalten, das nicht unbedingt falsch ist, aber höchstwahrscheinlich auf einen Fehler hinweist
  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT



    : Meldung über falsches Verhalten, das zu einem Absturz führen kann


Die Werte für die Aufzählung werden so ausgewählt, dass Sie mit der Vergleichsoperation Nachrichten über oder unter einem bestimmten Schwellenwert herausfiltern können, z. B.:



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





Der Parameter messageType



kann folgende Werte haben:



  • VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT



    : Das aufgetretene Ereignis hängt nicht mit der Spezifikation oder Leistung zusammen
  • VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT



    : Das aufgetretene Ereignis verstößt gegen die Spezifikation oder weist auf einen möglichen Fehler hin
  • VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT



    : Vulkan darf nicht optimal eingesetzt werden


Der Parameter pCallbackData



bezieht sich auf eine Struktur VkDebugUtilsMessengerCallbackDataEXT



, die die Details der Nachricht enthält. Die wichtigsten Mitglieder der Struktur sind:



  • pMessage



    : Debug-Nachricht als nullterminierte Zeichenfolge
  • pObjects



    : Ein Array von Deskriptoren von Objekten, die sich auf die Nachricht beziehen
  • objectCount



    : Anzahl der Objekte im Array


Der Parameter pUserData



enthält den Zeiger, der beim Einrichten der Rückruffunktion übergeben wurde.



Die Rückruffunktion gibt einen VkBool32



Typ zurück. Das Ergebnis gibt an, ob der Anruf, der die Nachricht generiert hat, beendet werden soll. Wenn die Rückruffunktion zurückkehrt VK_TRUE



, wird der Anruf abgebrochen und ein Fehlercode zurückgegeben VK_ERROR_VALIDATION_FAILED_EXT



. Dies geschieht in der Regel nur, wenn Sie die Validierungsschichten selbst testen. In unserem Fall müssen Sie zurückkehren VK_FALSE



.



Es bleibt Vulkan über die Rückruffunktion zu informieren. Überraschenderweise erfordert selbst die Steuerung einer Debug-Rückruffunktion in Vulkan einen Deskriptor, der explizit erstellt und zerstört werden muss. Diese Rückruffunktion ist Teil des Debug-Messengerund ihre Anzahl ist unbegrenzt. Fügen Sie ein Klassenmitglied für den Deskriptor hinzu, nachdem instance



:



VkDebugUtilsMessengerEXT debugMessenger;
      
      





Jetzt eine Funktion hinzufügen , setupDebugMessenger



die aus aufgerufen werden initVulkan



direkt nach createInstance



:



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

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

}
      
      





Wir müssen die Struktur mit Details über den Messenger und seine Rückruffunktionen füllen:



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
      
      





In diesem Feld messageSeverity



können Sie den Schweregrad angeben, für den die Rückruffunktion aufgerufen wird. Wir stellen alle Grade ein, außer VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT



um über mögliche Probleme informiert zu werden und die Konsole nicht mit detaillierten Debugging-Informationen zu überladen.



In ähnlicher Weise messageType



können Sie in diesem Feld Nachrichten nach Typ filtern. Wir haben alle Typen ausgewählt, aber Sie können immer unnötige deaktivieren.



Ein pfnUserCallback



Zeiger auf die Rückruffunktion wird an das Feld übergeben . Optional können Sie einen Zeiger auf das Feld pUserData



übergeben, der über einen Parameter an die Rückruffunktion übergeben wird pUserData



.



Beachten Sie, dass es andere Möglichkeiten gibt, Validierungsschichtnachrichten anzupassen und Rückrufe zu debuggen. Dies ist jedoch der beste Weg, um mit Vulkan zu beginnen. Weitere Informationen zu anderen Methoden finden Sie in der Erweiterungsspezifikation .



Die Struktur muss an die Funktion übergeben werden vkCreateDebugutilsMessengerEXT



, um das Objekt zu erstellen VkDebugUtilsMessengerEXT



. Dies ist eine Erweiterungsfunktion, daher wird sie nicht automatisch geladen. Sie müssen die Adresse selbst finden, indem Sie vkGetInstanceProcAddr



. Wir werden unsere eigene Proxy-Funktion erstellen, die dies intern erledigt. Fügen Sie es vor der Klassendefinition hinzu 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;
    }
}
      
      





Wir verwenden diese Funktion, um einen Messenger zu erstellen:



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





Der vorletzte Parameter ist optional. Dies ist die Rückruffunktion des Allokators, die wir als angeben werden nullptr



. Der Rest der Parameter ist ziemlich einfach. Da der Messenger für eine bestimmte Vulkan-Instanz (und ihre Validierungsebenen) verwendet wird, muss als erstes Argument ein Zeiger auf diese Instanz übergeben werden. Wir werden dieses Muster für andere untergeordnete Objekte finden.



Das Objekt VkDebugUtilsMessengerEXT



muss durch Aufruf zerstört werden vkDestroyDebugUtilsMessengerEXT



. Sowie für vkCreateDebugUtilsMessengerEXT



, müssen wir explizit diese Funktion laden. Erstellen Sie



dann CreateDebugUtilsMessengerEXT



eine weitere Proxy-Funktion:



void DestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT debugMessenger, const VkAllocationCallbacks* pAllocator) {
    auto func = (PFN_vkDestroyDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT");
    if (func != nullptr) {
        func(instance, debugMessenger, pAllocator);
    }
}
      
      





Überprüfen Sie, ob diese Funktion entweder eine statische Funktion der Klasse oder eine Funktion außerhalb der Klasse ist. Danach kann es in einer Funktion aufgerufen werden cleanup



:



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

    vkDestroyInstance(instance, nullptr);

    glfwDestroyWindow(window);

    glfwTerminate();
}
      
      







Debugging-Instanz von Vulkan



Wir haben das Debuggen mit Validierungsebenen hinzugefügt, aber es gibt noch ein bisschen mehr. Zum Aufrufen vkCreateDebugUtilsMessengerEXT



ist eine gültige Instanz vkDestroyDebugUtilsMessengerEXT



erforderlich, die aufgerufen werden muss, bevor die Instanz zerstört wird. Deshalb können wir nicht debuggen vkCreateInstance



und noch vkDestroyInstance



.



Wenn Sie die Spezifikation jedoch sorgfältig lesen, werden Sie feststellen , dass es möglich ist, einen separaten Debug-Messenger für diese beiden Funktionen zu erstellen. Dazu müssen Sie den pNext



Strukturzeiger VkInstanceCreateInfo



auf die Struktur setzen VkDebugUtilsMessengerCreateInfoEXT



. Lassen Sie uns zunächst die Füllung VkDebugUtilsMessengerCreateInfoEXT



in eine separate Methode verschieben:



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





Wir können es in einer Funktion wiederverwenden 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!");
    }
}
      
      





Die Variable debugCreateInfo



befindet sich außerhalb der if-Anweisung, damit sie vor dem Aufruf nicht zerstört wird vkCreateInstance



. Wenn Sie auf diese Weise einen zusätzlichen Debug-Messenger erstellen , können Sie ihn automatisch in vkCreateInstance



und verwenden vkDestroyInstance



. Danach wird er zerstört.



Testen



Machen wir absichtlich einen Fehler, um die Validierungsebenen in Aktion zu sehen.

Entfernen Sie vorübergehend den Aufruf DestroyDebugUtilsMessengerEXT



in der Funktion cleanup



und führen Sie das Programm aus. Sie sollten am Ende Folgendes haben:







Um herauszufinden, welcher Anruf zum Senden der Nachricht geführt hat, fügen Sie der Rückruffunktion der Nachricht einen Haltepunkt hinzu und sehen Sie sich den Aufrufstapel an.





die Einstellungen



Es gibt viel mehr Einstellungen, die das Verhalten der Validierungsstufen definieren, als in der Struktur angegeben VkDebugUtilsMessengerCreateInfoEXT



. Gehen Sie zum Vulkan SDK und öffnen Sie das Verzeichnis Config



. Dort finden Sie eine Datei vk_layer_settings.txt



, in der das Einrichten von Ebenen erläutert wird.



So richten Sie die Schichten, kopieren Sie die Datei in das Verzeichnis Debug



und Release



und folgen Sie den Anweisungen , um das gewünschte Verhalten zu konfigurieren. Im weiteren Verlauf dieses Handbuchs wird jedoch davon ausgegangen, dass Sie die Standardeinstellungen verwenden.



In Zukunft werden wir absichtlich Fehler machen, um Ihnen zu zeigen, wie bequem und effektiv es ist, Validierungsebenen zu verwenden, um sie zu verfolgen.



C ++ - Code



All Articles