Vulkan-Validierungsebenen unter Android

Die meisten expliziten Grafik-APIs führen keine Fehlerprüfung durch, da dies zu Leistungseinbußen führen kann. Vulkan bietet Validierungsebenen, die während der Entwicklung eine Fehlerprüfung ermöglichen und Leistungseinbußen im Release-Build Ihrer App vermeiden. Validierungsebenen basieren auf einem allgemeinen Schichtmechanismus, der API-Einstiegspunkte abfängt.

Einzelne Khronos-Validierungsebene

Bisher bot Vulkan mehrere Validierungsebenen, die in einer bestimmten Reihenfolge aktiviert werden mussten. Ab der Version 1.1.106.0 des Vulkan SDK muss in Ihrer App nur noch eine einzelne Validierungsebene, VK_LAYER_KHRONOS_validation, aktiviert sein, um alle Funktionen der vorherigen Validierungsebenen zu erhalten.

In Ihrem APK verpackte Validierungsebenen verwenden

Verpackungsvalidierungsebenen in Ihrem APK sorgen für optimale Kompatibilität. Die Validierungsebenen sind als vorkonfigurierte Binärdateien verfügbar oder können aus Quellcode erstellt werden.

Vordefinierte Binärdateien verwenden

Laden Sie die neuesten Binärdateien der Android Vulkan-Bestätigungsschicht von der GitHub-Release-Seite herunter.

Die einfachste Möglichkeit, die Ebenen Ihrem APK hinzuzufügen, besteht darin, die vorgefertigten Ebenen-Binärdateien in das src/main/jniLibs/-Verzeichnis Ihres Moduls zu extrahieren, wobei ABI-Verzeichnisse wie arm64-v8a oder x86-64 intakt bleiben.

src/main/jniLibs/
  arm64-v8a/
    libVkLayer_khronos_validation.so
  armeabi-v7a/
    libVkLayer_khronos_validation.so
  x86/
    libVkLayer_khronos_validation.so
  x86-64/
    libVkLayer_khronos_validation.so

Validierungsebene aus Quellcode erstellen

Wenn Sie den Quellcode der Validierungsebene debuggen möchten, ziehen Sie die neueste Quelle aus dem GitHub-Repository der Khronos Group und folgen Sie der Anleitung zum Erstellen.

Prüfen, ob die Validierungsebene korrekt verpackt ist

Unabhängig davon, ob Sie mit den vorgefertigten Layers von Khronos oder mit Layers aus der Quelle erstellen, führt der Build-Vorgang zu einer endgültigen Dateistruktur in Ihrem APK, die in etwa so aussieht:

lib/
  arm64-v8a/
    libVkLayer_khronos_validation.so
  armeabi-v7a/
    libVkLayer_khronos_validation.so
  x86/
    libVkLayer_khronos_validation.so
  x86-64/
    libVkLayer_khronos_validation.so

Mit dem folgenden Befehl können Sie prüfen, ob Ihr APK wie erwartet die Validierungsebene enthält:

$ jar -tf project.apk | grep libVkLayer
lib/x86_64/libVkLayer_khronos_validation.so
lib/armeabi-v7a/libVkLayer_khronos_validation.so
lib/arm64-v8a/libVkLayer_khronos_validation.so
lib/x86/libVkLayer_khronos_validation.so

Validierungsebene während der Instanzerstellung aktivieren

Mit der Vulkan API kann eine App Ebenen während der Instanzerstellung aktivieren. Für Eingangspunkte, die von einer Schicht abgefangen werden, muss eines der folgenden Objekte als erster Parameter angegeben werden:

  • VkInstance
  • VkPhysicalDevice
  • VkDevice
  • VkCommandBuffer
  • VkQueue

Rufen Sie vkEnumerateInstanceLayerProperties() auf, um eine Liste der verfügbaren Ebenen und ihrer Eigenschaften aufzurufen. Vulkan aktiviert Ebenen, wenn vkCreateInstance() ausgeführt wird.

Im folgenden Code-Snippet wird gezeigt, wie eine App mit der Vulkan API Ebenen programmatisch abfragen und aktivieren kann:

// Enable just the Khronos validation layer.
static const char *layers[] = {"VK_LAYER_KHRONOS_validation"};

// Get the layer count using a null pointer as the last parameter.
uint32_t instance_layer_present_count = 0;
vkEnumerateInstanceLayerProperties(&instance_layer_present_count, nullptr);

// Enumerate layers with a valid pointer in the last parameter.
VkLayerProperties layer_props[instance_layer_present_count];
vkEnumerateInstanceLayerProperties(&instance_layer_present_count, layer_props);

// Make sure selected validation layers are available.
VkLayerProperties *layer_props_end = layer_props + instance_layer_present_count;
for (const char* layer:layers) {
  assert(layer_props_end !=
  std::find_if(layer_props, layer_props_end, [layer](VkLayerProperties layerProperties) {
    return strcmp(layerProperties.layerName, layer) == 0;
  }));
}

// Create a Vulkan instance, requesting all enabled layers or extensions
// available on the system
VkInstanceCreateInfo instanceCreateInfo{
  .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
  .pNext = nullptr,
  .pApplicationInfo = &appInfo,
  .enabledLayerCount = sizeof(layers) / sizeof(layers[0]),
  .ppEnabledLayerNames = layers,

Standard-logcat-Ausgabe

Die Validierungsebene gibt Warn- und Fehlermeldungen in Logcat aus, die mit dem Tag VALIDATION gekennzeichnet sind. Eine Meldung der Validierungsebene sieht in etwa so aus (mit Zeilenumbrüchen für leichteres Scrollen):

Validation -- Validation Error:
  [ VUID-VkDeviceQueueCreateInfo-pQueuePriorities-parameter ]
Object 0: VK_NULL_HANDLE, type = VK_OBJECT_TYPE_DEVICE; | MessageID = 0xd6d720c6 |
vkCreateDevice: required parameter
  pCreateInfo->pQueueCreateInfos[0].pQueuePriorities specified as NULL.
The Vulkan spec states: pQueuePriorities must be a valid pointer to an array of
  queueCount float values
  (https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html
  #VUID-VkDeviceQueueCreateInfo-pQueuePriorities-parameter)

Debug-Callback aktivieren

Mit der Debug Utils-Erweiterung VK_EXT_debug_utils kann Ihre Anwendung einen Debug-Messenger erstellen, der Nachrichten der Validierungsebene an einen von der Anwendung bereitgestellten Callback weitergibt. Diese Erweiterung ist möglicherweise nicht auf Ihrem Gerät implementiert, aber in den neuesten Validierungsschichten. Es gibt auch die eingestellte Erweiterung VK_EXT_debug_report, die ähnliche Funktionen bietet, wenn VK_EXT_debug_utils nicht verfügbar ist.

Bevor Sie die Debug Utils-Erweiterung verwenden, sollten Sie prüfen, ob Ihr Gerät oder eine geladene Validierungsebene sie unterstützt. Im folgenden Beispiel wird gezeigt, wie geprüft wird, ob die Debug-Utils-Erweiterung unterstützt wird, und ein Rückruf registriert wird, wenn die Erweiterung entweder von der Geräte- oder der Validierungsebene unterstützt wird.

// Get the instance extension count.
uint32_t inst_ext_count = 0;
vkEnumerateInstanceExtensionProperties(nullptr, &inst_ext_count, nullptr);

// Enumerate the instance extensions.
VkExtensionProperties inst_exts[inst_ext_count];
vkEnumerateInstanceExtensionProperties(nullptr, &inst_ext_count, inst_exts);

// Check for debug utils extension within the system driver or loader.
// Check if the debug utils extension is available (in the driver).
VkExtensionProperties *inst_exts_end = inst_exts + inst_ext_count;
bool debugUtilsExtAvailable = inst_exts_end !=
  std::find_if(inst_exts, inst_exts_end, [](VkExtensionProperties
    extensionProperties) {
    return strcmp(extensionProperties.extensionName,
      VK_EXT_DEBUG_UTILS_EXTENSION_NAME) == 0;
  });

if ( !debugUtilsExtAvailable ) {
  // Also check the layers for the debug utils extension.
  for (auto layer: layer_props) {
    uint32_t layer_ext_count;
    vkEnumerateInstanceExtensionProperties(layer.layerName, &layer_ext_count,
      nullptr);
    if (layer_ext_count == 0) continue;
    VkExtensionProperties layer_exts[layer_ext_count];
    vkEnumerateInstanceExtensionProperties(layer.layerName, &layer_ext_count,
    layer_exts);

    VkExtensionProperties * layer_exts_end = layer_exts + layer_ext_count;
    debugUtilsExtAvailable = layer_exts != std::find_if(
      layer_exts, layer_exts_end,[](VkExtensionProperties extensionProperties) {
        return strcmp(extensionProperties.extensionName,
        VK_EXT_DEBUG_UTILS_EXTENSION_NAME) == 0;
      });
    if (debugUtilsExtAvailable) {
        // Add the including layer into the layer request list if necessary.
        break;
    }
  }
}

if (!debugUtilsExtAvailable) return; // since this snippet depends on debugUtils

const char * enabled_inst_exts[] = { ..., VK_EXT_DEBUG_UTILS_EXTENSION_NAME };
uint32_t enabled_extension_count =
  sizeof(enabled_inst_exts)/sizeof(enabled_inst_exts[0]);

// Pass the instance extensions into vkCreateInstance.
VkInstanceCreateInfo instance_info = {};
instance_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
instance_info.enabledExtensionCount = enabled_extension_count;
instance_info.ppEnabledExtensionNames = enabled_inst_exts;

// NOTE: Can still return VK_ERROR_EXTENSION_NOT_PRESENT if validation layer
// isn't loaded.
vkCreateInstance(&instance_info, nullptr, &instance);

auto pfnCreateDebugUtilsMessengerEXT =
  (PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(
    tutorialInstance, "vkCreateDebugUtilsMessengerEXT");
auto pfnDestroyDebugUtilsMessengerEXT =
  (PFN_vkDestroyDebugUtilsMessengerEXT)vkGetInstanceProcAddr(
    tutorialInstance, "vkDestroyDebugUtilsMessengerEXT");

// Create the debug messenger callback with your the settings you want.
VkDebugUtilsMessengerEXT debugUtilsMessenger;
if (pfnCreateDebugUtilsMessengerEXT) {
  VkDebugUtilsMessengerCreateInfoEXT messengerInfo;
  constexpr VkDebugUtilsMessageSeverityFlagsEXT kSeveritiesToLog =
    VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT |
    VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT;

constexpr VkDebugUtilsMessageTypeFlagsEXT kMessagesToLog =
  VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |
  VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
  VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;

  messengerInfo.sType           = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
  messengerInfo.pNext           = nullptr;
  messengerInfo.flags           = 0;
  messengerInfo.messageSeverity = kSeveritiesToLog;
  messengerInfo.messageType     = kMessagesToLog;

  // The DebugUtilsMessenger callback is explained in the following section.
  messengerInfo.pfnUserCallback = &DebugUtilsMessenger;
  messengerInfo.pUserData       = nullptr; // Custom user data passed to callback

  pfnCreateDebugUtilsMessengerEXT(instance, &messengerInfo, nullptr,
    &debugUtilsMessenger);
}

// Later, when shutting down Vulkan, call the following:
if (pfnDestroyDebugUtilsMessengerEXT) {
    pfnDestroyDebugUtilsMessengerEXT(instance, debugUtilsMessenger, nullptr);
}

Nachdem Ihre App den Rückruf registriert und aktiviert hat, leitet das System Debug-Nachrichten an ihn weiter.

#include <android/log.h>

VKAPI_ATTR VkBool32 VKAPI_CALL DebugUtilsMessenger(
                        VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
                        VkDebugUtilsMessageTypeFlagsEXT messageTypes,
                        const VkDebugUtilsMessengerCallbackDataEXT *callbackData,
                        void *userData)
{
  const char validation[]  = "Validation";
  const char performance[] = "Performance";
  const char error[]       = "ERROR";
  const char warning[]     = "WARNING";
  const char unknownType[] = "UNKNOWN_TYPE";
  const char unknownSeverity[] = "UNKNOWN_SEVERITY";
  const char* typeString      = unknownType;
  const char* severityString  = unknownSeverity;
  const char* messageIdName   = callbackData->pMessageIdName;
  int32_t messageIdNumber     = callbackData->messageIdNumber;
  const char* message         = callbackData->pMessage;
  android_LogPriority priority = ANDROID_LOG_UNKNOWN;

  if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) {
    severityString = error;
    priority = ANDROID_LOG_ERROR;
  }
  else if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) {
    severityString = warning;
    priority = ANDROID_LOG_WARN;
  }
  if (messageTypes & VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT) {
     typeString = validation;
  }
  else if (messageTypes & VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT) {
     typeString = performance;
  }

  __android_log_print(priority,
                     "AppName",
                     "%s %s: [%s] Code %i : %s",
                     typeString,
                     severityString,
                     messageIdName,
                     messageIdNumber,
                     message);

  // Returning false tells the layer not to stop when the event occurs, so
  // they see the same behavior with and without validation layers enabled.
  return VK_FALSE;
}

Externe Validierungsebenen verwenden

Sie müssen keine Validierungsebenen in Ihrem APK verpacken. Geräte mit Android 9 (API-Level 28) und höher können Validierungsebenen extern zu Ihrer Binärdatei verwenden und sie dynamisch deaktivieren und aktivieren. Führen Sie die Schritte in diesem Abschnitt aus, um Validierungsebenen auf Ihr Testgerät zu übertragen:

Externe Validierungsebenen für Ihre App aktivieren

Das Sicherheitsmodell und die Richtlinien von Android unterscheiden sich erheblich von anderen Plattformen. Damit externe Validierungsebenen geladen werden können, muss eine der folgenden Bedingungen erfüllt sein:

  • Die Ziel-App ist debugfähig. Diese Option führt zu mehr Informationen zur Fehlerbehebung, kann sich aber negativ auf die Leistung Ihrer App auswirken.

  • Die Ziel-App wird auf einem userdebug-Build des Betriebssystems ausgeführt, der Root-Zugriff gewährt.

  • Apps, die nur auf Android 11 (API-Level 30) oder höher ausgerichtet sind: Ihre Android-Manifestdatei enthält das folgende meta-data-Element:

    <meta-data android:name="com.android.graphics.injectLayers.enable"
      android:value="true"/>
    

Externe Validierungsebene laden

Auf Geräten mit Android 9 (API-Level 28) und höher kann Vulkan die Bestätigungsebene aus dem lokalen Speicher Ihrer App laden. Ab Android 10 (API-Level 29) kann Vulkan die Validierungsebene auch aus einem separaten APK laden. Sie können eine beliebige Methode auswählen, solange sie von Ihrer Android-Version unterstützt wird.

Binärdatei der Validierungsebene aus dem lokalen Speicher des Geräts laden

Da Vulkan nach der Binärdatei im temporären Datenspeicherverzeichnis Ihres Geräts sucht, müssen Sie sie zuerst mit Android Debug Bridge (adb) in dieses Verzeichnis hochladen. Gehen Sie dazu so vor:

  1. Verwenden Sie den Befehl adb push, um die Binärdatei der Ebene in den Datenspeicher Ihrer App auf dem Gerät zu laden:

    $ adb push libVkLayer_khronos_validation.so /data/local/tmp
    
  2. Verwenden Sie die Befehle adb shell und run-as, um die Ebene über Ihren App-Prozess zu laden. Das Binärprogramm hat also denselben Gerätezugriff wie die App, ohne dass Root-Zugriff erforderlich ist.

    $ adb shell run-as com.example.myapp cp
      /data/local/tmp/libVkLayer_khronos_validation.so .
    $ adb shell run-as com.example.myapp ls libVkLayer_khronos_validation.so
    
  3. Aktivieren Sie die Ebene.

Binärdatei der Validierungsebene aus einem anderen APK laden

Sie können adb verwenden, um ein APK zu installieren, das die Ebene enthält, und dann die Ebene zu aktivieren.

adb install --abi abi path_to_apk

Ebenen außerhalb der Anwendung aktivieren

Sie können Vulkan-Ebenen entweder pro App oder global aktivieren. App-spezifische Einstellungen bleiben nach einem Neustart erhalten, während globale Eigenschaften gelöscht werden.

Ebenen für einzelne Apps aktivieren

In den folgenden Schritten wird beschrieben, wie Sie Ebenen für einzelne Apps aktivieren:

  1. Aktivieren Sie die Ebenen mithilfe der adb-Shell-Einstellungen:

    $ adb shell settings put global enable_gpu_debug_layers 1
    
  2. Geben Sie die Zielanwendung an, für die die Ebenen aktiviert werden sollen:

    $ adb shell settings put global gpu_debug_app <package_name>
    
  3. Geben Sie die Liste der zu aktivierenden Ebenen (von oben nach unten) an und trennen Sie die einzelnen Ebenen durch einen Doppelpunkt:

    $ adb shell settings put global gpu_debug_layers <layer1:layer2:layerN>
    

    Da wir nur eine Khronos-Validierungsebene haben, sieht der Befehl wahrscheinlich so aus:

    $ adb shell settings put global gpu_debug_layers VK_LAYER_KHRONOS_validation
    
  4. Geben Sie ein oder mehrere Pakete an, in denen nach Ebenen gesucht werden soll:

    $ adb shell settings put global
      gpu_debug_layer_app <package1:package2:packageN>
    

Mit den folgenden Befehlen können Sie prüfen, ob die Einstellungen aktiviert sind:

$ adb shell settings list global | grep gpu
enable_gpu_debug_layers=1
gpu_debug_app=com.example.myapp
gpu_debug_layers=VK_LAYER_KHRONOS_validation

Da die von Ihnen angewendeten Einstellungen nach einem Neustart des Geräts beibehalten werden, sollten Sie sie nach dem Laden der Ebenen löschen:

$ adb shell settings delete global enable_gpu_debug_layers
$ adb shell settings delete global gpu_debug_app
$ adb shell settings delete global gpu_debug_layers
$ adb shell settings delete global gpu_debug_layer_app

Ebenen global aktivieren

Sie können eine oder mehrere Ebenen global bis zum nächsten Neustart aktivieren. Dabei werden die Ebenen für alle Anwendungen geladen, einschließlich nativer ausführbarer Dateien.

$ adb shell setprop debug.vulkan.layers <layer1:layer2:layerN>