Android 上的 Vulkan 驗證層

大多數煽情露骨圖形 API 都不會執行錯誤檢查,因為這可能導致效能降低。Vulkan 具有「驗證層」,可在開發期間執行錯誤檢查,避免應用程式的發布子版本效能下降。驗證層採用一般用途分層機制,可攔截 API 進入點。

單一 Khronos 驗證層

Vulkan 先前提供多個驗證層,且需依照特定順序啟用。從 1.1.106.0 Vulkan SDK 版開始,應用程式只需啟用單一驗證層 VK_LAYER_KHRONOS_validation,就能使用舊版驗證層的所有功能。

使用封裝在 APK 中的驗證層

在 APK 中封裝驗證層可確保發揮最佳相容效果。 驗證層可做為預先建構的二進位檔,也可以使用原始碼建構。

使用預先建構的二進位檔

GitHub 發布頁面下載最新的 Android Vulkan 驗證層二進位檔。

如要在 APK 中新增層,最簡單的方法就是將預先建構的層二進位檔擷取至模組的 src/main/jniLibs/ 目錄,並讓 ABI 目錄 (例如 arm64-v8ax86-64) 保持不變,具體如下所示:

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

使用原始碼建構驗證層

如要對驗證層原始碼進行偵錯,請從 Khronos Group GitHub 存放區中提取最新的原始碼,然後按照其中的建構說明進行操作。

確認驗證層已正確封裝

無論您在建構時是使用 Khronos 預先建構的層,或是使用以原始碼建構的層,建構程序都會在 APK 中產生最終的檔案結構,如下所示:

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

下列指令顯示如何驗證 APK 是否含有預期的驗證層:

$ 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

在建立執行個體時啟用驗證層

Vulkan API 可讓應用程式在建立執行個體時啟用層。層攔截的進入點必須將下列其中一個物件做為第一個參數:

  • VkInstance
  • VkPhysicalDevice
  • VkDevice
  • VkCommandBuffer
  • VkQueue

呼叫 vkEnumerateInstanceLayerProperties() 列出可用層及其屬性。Vulkan 會在 vkCreateInstance() 執行時啟用層。

下列程式碼片段顯示應用程式如何使用 Vulkan API 以程式輔助方式查詢和啟用層:

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

預設 logcat 輸出

驗證層會在標有 VALIDATION 標記的 logcat 中發出警告和錯誤訊息。驗證層訊息如下所示 (當中加入分行符號以方便捲動):

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)

啟用偵錯回呼

偵錯公用程式擴充功能 VK_EXT_debug_utils 可讓應用程式建立偵錯訊息傳遞工具,將驗證層訊息傳遞至應用程式提供的回呼。您的裝置可能並未實作這個擴充功能,不過最新的驗證層中已可實作。另一個已淘汰的擴充功能 VK_EXT_debug_report 會在 VK_EXT_debug_utils 無法使用時提供類似功能。

使用偵錯公用程式擴充功能之前,請先確認您的裝置或載入的驗證層支援這項功能。下列範例說明如何檢查是否支援偵錯公用程式擴充功能,以及在裝置或驗證層支援這項功能時應如何註冊回呼。

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

應用程式註冊並啟用回呼後,系統便會將偵錯訊息轉送至對應的回呼。

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

使用外部驗證層

您不必在 APK 中封裝驗證層;搭載 Android 9 (API 級別 28) 以上版本的裝置可以使用二進位檔之外的驗證層,並動態關閉及開啟這些驗證層。請按照本節中的步驟將驗證層推送至測試裝置:

讓應用程式可以使用外部驗證層

Android 的安全性模型和政策與其他平台有很大差異。如要載入外部驗證層,必須符合下列其中一項條件:

  • 目標應用程式可進行偵錯。這個選項會產生更多偵錯資訊,但可能會降低應用程式的效能。

  • 目標應用程式在授予 Root 存取權的作業系統使用者偵錯版本上執行。

  • 指定目標版本僅限 Android 11 (API 級別 30) 以上版本的應用程式:您的目標 Android 資訊清單檔案包含下列 meta-data 元素:

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

載入外部驗證層

搭載 Android 9 (API 級別 28) 以上版本的裝置允許 Vulkan 從應用程式的本機儲存空間載入驗證層。從 Android 10 (API 級別 29) 開始,Vulkan 也可以從單獨的 APK 載入驗證層。只要 Android 版本支援,您就能自由選擇任何一種方法。

從裝置本機儲存空間載入驗證層二進位檔

由於 Vulkan 會在裝置的暫存資料目錄中尋找二進位檔,因此您必須先使用 Android Debug Bridge (ADB) 將二進位檔推送至該目錄,如下所示:

  1. 使用 adb push 指令將層二進位檔載入應用程式在裝置上的資料儲存空間:

    $ adb push libVkLayer_khronos_validation.so /data/local/tmp
    
  2. 使用 adb shellrun-as 指令,透過應用程式程序載入層。也就是說,二進位檔具有應用程式所擁有的裝置存取權,無須要求 Root 存取權。

    $ 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. 啟用層

從其他 APK 載入驗證層二進位檔

您可以使用 adb 安裝含有層的 APK,然後啟用層

adb install --abi abi path_to_apk

在應用程式外啟用層

您可以為個別應用程式啟用 Vulkan 層,也可以全域啟用。在每次重新啟動時,個別應用程式設定會維持不變,而全域屬性則會在重新啟動時被清除

針對個別應用程式啟用層

下列步驟說明如何針對個別應用程式啟用層:

  1. 使用 ADB 殼層設定啟用層:

    $ adb shell settings put global enable_gpu_debug_layers 1
    
  2. 指定要啟用層的目標應用程式:

    $ adb shell settings put global gpu_debug_app <package_name>
    
  3. 指定要啟用的層的清單 (由上至下),並以半形冒號分隔每個層:

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

    由於我們採用單一 Khronos 驗證層,因此這個指令可能會如下所示:

    $ adb shell settings put global gpu_debug_layers VK_LAYER_KHRONOS_validation
    
  4. 指定一或多個要在其中搜尋層的套件:

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

您可以使用下列指令來檢查設定是否已啟用:

$ 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

由於套用的設定不會因為裝置重新啟動而重設,因此您可能需要在載入層後清除設定:

$ 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

全域啟用層

您可以在裝置下次重新啟動之前,全域啟用一或多個層。 這樣一來,系統會嘗試針對所有應用程式 (包括原生執行檔) 載入層。

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