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 durchführen, um Leistungseinbußen beim Release-Build deiner App zu vermeiden. Validierungsebenen basieren auf einem allgemeinen Layer-Mechanismus, der API-Einstiegspunkte abfängt.

Einzelne Khronos-Validierungsebene

Bisher gab es bei Vulkan mehrere Validierungsebenen, die in einer bestimmten Reihenfolge aktiviert werden mussten. Ab dem Vulkan SDK-Release 1.1.106.0 muss deine App nur noch eine einzelne Validierungsebene (VK_LAYER_KHRONOS_validation) aktivieren, um alle Funktionen aus den vorherigen Validierungsebenen zu nutzen.

Im APK gepackte Validierungsebenen verwenden

Durch das Packen der Validierungsebenen in deinem APK wird eine optimale Kompatibilität sichergestellt. Die Validierungsebenen sind als vorgefertigte Binärdateien verfügbar oder können aus Quellcode erstellt werden.

Vordefinierte Binärprogramme verwenden

Laden Sie die neuesten Binärdateien der Android Vulkan Validation-Ebene von der GitHub-Releaseseite herunter.

Die einfachste Methode zum Hinzufügen der Layers zu deinem APK besteht darin, die vordefinierten Layer-Binärdateien in das Verzeichnis src/main/jniLibs/ deines Moduls zu extrahieren. Die ABI-Verzeichnisse (z. B. arm64-v8a oder x86-64) bleiben dabei intakt:

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 Fehler im Quellcode der Validierungsschicht beheben möchten, rufen Sie die neueste Quelle aus dem GitHub-Repository der Khronos Group ab und folgen Sie den dort angezeigten Build-Anweisungen.

Prüfen, ob die Validierungsschicht richtig gepackt ist

Unabhängig davon, ob du deine Builds mit den vordefinierten Khronos-Ebenen oder mit den aus dem Quellcode erstellten Ebenen erstellst, erzeugt der Build-Prozess eine endgültige Dateistruktur in deinem 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

Der folgende Befehl zeigt, wie du prüfen kannst, ob dein APK die Validierungsebene wie erwartet 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 bei der Instanzerstellung Ebenen aktivieren. Einstiegspunkte, die eine Ebene abfängt, müssen eines der folgenden Objekte als ersten Parameter haben:

  • VkInstance
  • VkPhysicalDevice
  • VkDevice
  • VkCommandBuffer
  • VkQueue

Rufen Sie vkEnumerateInstanceLayerProperties() auf, um die verfügbaren Ebenen und ihre Eigenschaften aufzulisten. Vulkan aktiviert Ebenen, wenn vkCreateInstance() ausgeführt wird.

Das folgende Code-Snippet zeigt, wie eine App mithilfe 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 im Logcat Warnungen und Fehlermeldungen aus, die mit einem VALIDATION-Tag gekennzeichnet sind. Eine Nachricht der Validierungsebene sieht so aus (mit Zeilenumbrüchen für ein 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 Meldungen der Validierungsebene an einen von der Anwendung bereitgestellten Callback übergibt. Ihr Gerät implementiert diese Erweiterung möglicherweise nicht, sie ist jedoch in den neuesten Validierungsebenen implementiert. Es gibt auch eine verworfene Erweiterung namens VK_EXT_debug_report, die ähnliche Funktionen bietet, wenn VK_EXT_debug_utils nicht verfügbar ist.

Bevor Sie die Erweiterung „Debug Utils“ verwenden, sollten Sie prüfen, ob sie von Ihrem Gerät oder einer geladenen Validierungsebene unterstützt wird. Das folgende Beispiel zeigt, wie Sie prüfen, ob die Erweiterung „Debug utils“ unterstützt wird, und einen Callback registrieren, wenn die Erweiterung 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 Anwendung den Callback registriert und aktiviert hat, leitet das System Debugging-Nachrichten an diese 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

Du musst Validierungsebenen in deinem APK nicht verpacken. Geräte mit Android 9 (API-Level 28) und höher können Validierungsebenen außerhalb deines Binärprogramms verwenden und dynamisch deaktivieren und aktivieren. Führen Sie die Schritte in diesem Abschnitt aus, um Validierungsebenen an Ihr Testgerät zu übertragen:

Anwendung für die Verwendung externer Validierungsebenen aktivieren

Das Sicherheitsmodell und die Richtlinien von Android unterscheiden sich erheblich von anderen Plattformen. Zum Laden externer Validierungsebenen muss eine der folgenden Bedingungen erfüllt sein:

  • Die Ziel-App kann fehlerbereinigt werden. Diese Option führt zu mehr Fehlerbehebungsinformationen, kann sich aber negativ auf die Leistung Ihrer Anwendung auswirken.

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

  • Apps, die nur auf Android 11 (API-Level 30) oder höher ausgerichtet sind: Ihre Android-Zielmanifestdatei 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 Validierungsschicht aus dem lokalen Speicher der App laden. Ab Android 10 (API-Level 29) kann Vulkan die Validierungsebene auch aus einem separaten APK laden. Sie können die gewünschte Methode auswählen, solange sie von Ihrer Android-Version unterstützt wird.

Eine Validierungsschicht-Binärdatei aus dem lokalen Speicher des Geräts laden

Da Vulkan im temporären Datenspeicherverzeichnis Ihres Geräts nach der Binärdatei sucht, müssen Sie sie zuerst mithilfe der Android Debug Bridge (ADB) in dieses Verzeichnis übertragen:

  1. Verwenden Sie den Befehl adb push, um das Layer-Binärprogramm 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 Anwendungsprozess zu laden. Das heißt, die Binärdatei hat denselben Gerätezugriff wie die App, ohne dass ein 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. Ebene aktivieren

Validierungsschicht-Binärprogramm aus einem anderen APK laden

Du kannst mit adb ein APK installieren, das die Ebene enthält, und dann die Ebene aktivieren.

adb install --abi abi path_to_apk

Ebenen außerhalb der Anwendung aktivieren

Du kannst Vulkan-Ebenen entweder für einzelne Apps oder global aktivieren. App-spezifische Einstellungen bleiben bei einem Neustart bestehen, während globale Attribute beim Neustart gelöscht werden.

Ebenen für einzelne Apps aktivieren

In den folgenden Schritten wird beschrieben, wie Ebenen für einzelne Apps aktiviert werden:

  1. Verwenden Sie die ADB-Shell-Einstellungen, um die Ebenen zu aktivieren:

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

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

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

    Da wir nur eine einzige Khronos-Validierungsschicht 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 angewendeten Einstellungen bei jedem Geräteneustart beibehalten werden, sollten Sie die Einstellungen 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 bis zum nächsten Neustart global aktivieren. Dadurch wird versucht, die Layer für alle Anwendungen zu laden, einschließlich nativer ausführbarer Dateien.

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