Warstwy walidacji interfejsu Vulkan na Androidzie

Większość interfejsów API do wyświetlania grafiki dla dorosłych nie sprawdza błędów, bo może to skutkować spadkiem wydajności. Interfejs Vulkan ma warstwy weryfikacji, które umożliwiają sprawdzanie błędów podczas programowania, co pozwala uniknąć spadku wydajności na etapie kompilacji aplikacji. Warstwy walidacji wykorzystują mechanizm warstw ogólnego przeznaczenia, który przechwytuje punkty wejścia interfejsu API.

Warstwa walidacji pojedynczej Khronos

Wcześniej interfejs Vulkan udostępniało wiele warstw weryfikacji, które trzeba było włączyć w określonej kolejności. Począwszy od wersji pakietu SDK Vulkan 1.1.106.0, aplikacja musi włączyć tylko pojedynczą warstwę weryfikacji (VK_LAYER_KHRONOS_validation), aby uzyskać wszystkie funkcje z poprzednich warstw weryfikacji.

Używanie warstw weryfikacyjnych w pakiecie APK

Warstwy weryfikacji pakietów w pakiecie APK zapewniają optymalną zgodność. Warstwy weryfikacyjne są dostępne w postaci gotowych plików binarnych lub można je kompilować z poziomu kodu źródłowego.

Użyj gotowych plików binarnych

Najnowsze pliki binarne warstwy funkcji Android Vulkan Weryfikacja znajdziesz ze strony wersji na GitHubie.

Najprostszym sposobem dodania warstw do pliku APK jest wyodrębnienie gotowych plików binarnych warstw do katalogu src/main/jniLibs/ modułu z zachowaniem katalogów interfejsu ABI (takich jak arm64-v8a lub x86-64). Na przykład:

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

Tworzenie warstwy walidacji na podstawie kodu źródłowego

Aby debugować kod źródłowy warstwy walidacji, pobierz najnowsze źródło z repozytorium GitHub grupy Khronos i postępuj zgodnie z podanymi tam instrukcjami kompilacji.

Sprawdź, czy warstwa weryfikacji jest prawidłowo zapakowana

Niezależnie od tego, czy korzystasz z gotowych warstw Khronos, czy z warstw utworzonych ze źródła, proces kompilacji tworzy w pakiecie APK ostateczną strukturę pliku podobną do tej:

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

To polecenie pokazuje, jak sprawdzić, czy Twój plik APK zawiera warstwę weryfikacji zgodnie z oczekiwaniami:

$ 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

Włącz warstwę weryfikacji podczas tworzenia instancji

Interfejs Vulkan API pozwala aplikacji na włączanie warstw podczas tworzenia instancji. Punkty wejścia, które przechwytuje warstwa, jako pierwszy parametr muszą mieć jeden z tych obiektów:

  • VkInstance
  • VkPhysicalDevice
  • VkDevice
  • VkCommandBuffer
  • VkQueue

Wywołaj funkcję vkEnumerateInstanceLayerProperties(), aby wyświetlić listę dostępnych warstw i ich właściwości. Interfejs Vulkan włącza warstwy podczas wykonywania działania vkCreateInstance().

Ten fragment kodu pokazuje, jak aplikacja może użyć interfejsu Vulkan API do programowego wysyłania zapytań i włączania warstw:

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

Domyślne dane wyjściowe dzienników Logcat

Warstwa weryfikacji wyświetla ostrzeżenia i komunikaty o błędach w narzędziu Logcat oznaczonym tagiem VALIDATION. Komunikat warstwy weryfikacyjnej wygląda tak (z uwzględnieniem podziałów wierszy w celu ułatwienia przewijania):

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)

Włącz wywołanie zwrotne debugowania

Rozszerzenie Debug Utils VK_EXT_debug_utils umożliwia aplikacji utworzenie komunikatora debugowania, który przekazuje komunikaty warstwy weryfikacji do wywołania zwrotnego dostarczonego przez aplikację. Twoje urządzenie może nie implementować tego rozszerzenia, ale zostało ono zaimplementowane w najnowszych warstwach weryfikacji. Istnieje też wycofane rozszerzenie o nazwie VK_EXT_debug_report, które zapewnia podobne możliwości, gdy VK_EXT_debug_utils jest niedostępne.

Zanim użyjesz rozszerzenia Debug Utils, sprawdź, czy Twoje urządzenie lub wczytana warstwa weryfikacji obsługuje tę funkcję. Z przykładu poniżej dowiesz się, jak sprawdzić, czy rozszerzenie debug utils jest obsługiwane, a także zarejestrować wywołanie zwrotne, jeśli jest ono obsługiwane przez urządzenie lub warstwę walidacyjną.

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

Gdy aplikacja zarejestruje się i włączy wywołanie zwrotne, system przekieruje do niej wiadomości debugowania.

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

Użyj zewnętrznych warstw weryfikacyjnych

Nie musisz tworzyć pakietów warstw weryfikacyjnych w pliku APK. Urządzenia z Androidem 9 (poziom interfejsu API 28) lub nowszym mogą korzystać z warstw weryfikacji spoza pliku binarnego, a potem dynamicznie je wyłączać i włączać. Wykonaj czynności opisane w tej sekcji, aby przekazać warstwy weryfikacji na urządzenie testowe:

Włącz w aplikacji korzystanie z zewnętrznych warstw weryfikacyjnych

Model zabezpieczeń i zasady Androida znacznie różnią się od innych platform. Aby wczytać zewnętrzne warstwy weryfikacyjne, musi być spełniony jeden z tych warunków:

  • Aplikacja docelowa jest możliwe do debugowania. Dzięki temu uzyskasz więcej informacji na temat debugowania, ale może ona niekorzystnie wpłynąć na działanie aplikacji.

  • Aplikacja docelowa jest uruchamiana w kompilacji userdebug systemu operacyjnego, który przyznaje dostęp roota.

  • Aplikacje kierowane tylko na Androida 11 (poziom interfejsu API 30) lub nowszego: docelowy plik manifestu Androida zawiera ten element meta-data:

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

Wczytywanie zewnętrznej warstwy walidacji

Urządzenia z Androidem 9 (poziom interfejsu API 28) lub nowszym umożliwiają Vulkanowi ładowanie warstwy weryfikacji z pamięci lokalnej aplikacji. Począwszy od Androida 10 (poziom interfejsu API 29), interfejs Vulkan może też wczytywać warstwę weryfikacji z osobnego pliku APK. Możesz wybrać dowolną metodę, o ile obsługuje ją wersja Androida.

Wczytaj plik binarny warstwy walidacji z pamięci lokalnej urządzenia

Vulkan szuka pliku binarnego w tymczasowym katalogu przechowywania danych na Twoim urządzeniu, więc musisz go najpierw przenieść do tego katalogu, korzystając z funkcji Android DebugBridge (adb) w ten sposób:

  1. Użyj polecenia adb push, aby wczytać plik binarny warstwy do pamięci danych aplikacji na urządzeniu:

    $ adb push libVkLayer_khronos_validation.so /data/local/tmp
    
  2. Użyj poleceń adb shell i run-as, aby wczytać warstwę w procesie aplikacji. Oznacza to, że plik binarny ma taki sam dostęp do urządzenia, jaki ma aplikacja. Nie wymaga dostępu do roota.

    $ 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. Włącz warstwę.

Wczytaj plik binarny warstwy weryfikacyjnej z innego pliku APK

Za pomocą adb możesz zainstalować plik APK zawierający warstwę, a potem włączyć warstwę.

adb install --abi abi path_to_apk

Włączanie warstw poza aplikacją

Warstwy Vulkan możesz włączać w poszczególnych aplikacjach lub globalnie. Ustawienia poszczególnych aplikacji zachowują się przy ponownym uruchomieniu, natomiast właściwości globalne są czyszczone przy ponownym uruchomieniu.

Włączanie warstw dla poszczególnych aplikacji

Poniżej znajdziesz instrukcje włączania warstw dla poszczególnych aplikacji:

  1. Użyj ustawień powłoki adb, aby włączyć warstwy:

    $ adb shell settings put global enable_gpu_debug_layers 1
    
  2. Określ aplikację docelową, w której chcesz włączyć warstwy:

    $ adb shell settings put global gpu_debug_app <package_name>
    
  3. Określ listę warstw, które chcesz włączyć (od góry do dołu), oddzielając każdą z nich dwukropkiem:

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

    Ponieważ mamy jedną warstwę weryfikacji Khronos, polecenie będzie prawdopodobnie wyglądać tak:

    $ adb shell settings put global gpu_debug_layers VK_LAYER_KHRONOS_validation
    
  4. Określ co najmniej jeden pakiet, aby wyszukać w nim warstwy:

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

Aby sprawdzić, czy ustawienia są włączone, użyj tych poleceń:

$ 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

Wybrane ustawienia są zachowywane po ponownym uruchomieniu urządzenia, dlatego warto wyczyścić te ustawienia po wczytaniu warstw:

$ 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

Globalne włączanie warstw

Do czasu ponownego uruchomienia możesz włączyć jedną lub więcej warstw globalnie. Spowoduje to próbę wczytania warstw wszystkich aplikacji, w tym natywnych wykonywalnych.

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