Các lớp xác thực Vulkan trên Android

Hầu hết API đồ hoạ tường minh đều không thực hiện quy trình kiểm tra lỗi do việc này có thể dẫn đến hậu quả về hiệu suất. Vulkan có các lớp xác thực cung cấp tính năng kiểm tra lỗi trong quá trình phát triển, giúp tránh tác hại về hiệu suất trong bản phát hành của ứng dụng. Lớp xác thực dựa trên cơ chế phân lớp dùng cho nhiều mục đích có khả năng chặn các điểm truy cập API.

Lớp xác thực Khronos đơn

Trước đây, Vulkan cung cấp nhiều lớp xác thực cần được bật theo thứ tự cụ thể. Kể từ bản phát hành SDK Vulkan 1.1.106.0, ứng dụng chỉ phải bật một lớp xác thực, VK_LAYER_KHRONOS_validation, để nhận tất cả tính năng từ các lớp xác thực trước đó.

Sử dụng các lớp xác thực được đóng gói trong APK

Việc đóng gói các lớp xác thực trong APK đảm bảo khả năng tương thích tối ưu. Các lớp xác thực có sẵn dưới dạng tệp nhị phân tạo sẵn hoặc có thể tạo từ mã nguồn.

Sử dụng tệp nhị phân tạo sẵn

Tải các tệp nhị phân của Lớp xác thực Vulkan Android mới nhất xuống từ trang phát hành GitHub.

Cách dễ dàng nhất để thêm lớp vào APK là trích xuất các tệp nhị phân của lớp được tạo sẵn vào thư mục src/main/jniLibs/ của mô-đun, với các thư mục ABI (chẳng hạn như arm64-v8a hoặc x86-64) nguyên vẹn, như sau:

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

Tạo lớp xác thực từ mã nguồn

Để gỡ lỗi thành mã nguồn lớp xác thực, hãy lấy nguồn mới nhất từ kho lưu trữ GitHub của Khronos Group và làm theo hướng dẫn tạo ở đó.

Xác minh rằng lớp xác thực được đóng gói đúng cách

Bất kể bạn tạo bằng các lớp Khronos tạo sẵn hay các lớp được tạo từ nguồn, quy trình xây dựng sẽ tạo ra một cấu trúc tệp cuối cùng trong APK như sau:

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

Lệnh sau đây cho biết cách xác minh rằng APK chứa lớp xác thực như mong đợi:

$ 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

Bật lớp xác thực trong quá trình tạo thực thể

Vulkan API cho phép ứng dụng bật các lớp trong quá trình tạo thực thể. Điểm truy cập mà lớp chặn phải có một trong các đối tượng sau làm tham số đầu tiên:

  • VkInstance
  • VkPhysicalDevice
  • VkDevice
  • VkCommandBuffer
  • VkQueue

Gọi vkEnumerateInstanceLayerProperties() để liệt kê các lớp có sẵn và thuộc tính của các lớp đó. Vulkan sẽ bật các lớp khi vkCreateInstance() thực thi.

Đoạn mã sau đây cho biết cách một ứng dụng có thể sử dụng Vulkan API để truy vấn và bật các lớp theo phương thức lập trình:

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

Đầu ra logcat mặc định

Lớp xác thực đưa ra cảnh báo và thông báo lỗi trong logcat được gắn thẻ VALIDATION. Thông báo của lớp xác thực có dạng như sau (phần ngắt dòng được thêm vào đây để bạn cuộn dễ dàng hơn):

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)

Bật lệnh gọi lại gỡ lỗi

Phần mở rộng Tiện ích gỡ lỗi VK_EXT_debug_utils cho phép ứng dụng của bạn tạo một trình đưa tin gỡ lỗi để truyền thông báo của lớp xác thực đến lệnh gọi lại do ứng dụng cung cấp. Thiết bị có thể không triển khai phần mở rộng này nhưng phần mở rộng được triển khai trong các lớp xác thực gần đây nhất. Ngoài ra, còn một phần mở rộng không dùng nữa tên là VK_EXT_debug_report, cung cấp các tính năng tương tự nếu không có VK_EXT_debug_utils.

Trước khi sử dụng phần mở rộng Tiện ích gỡ lỗi, bạn phải đảm bảo rằng thiết bị hoặc lớp xác thực được tải hỗ trợ phần mở rộng đó. Ví dụ sau đây cho biết cách kiểm tra xem phần mở rộng tiện ích gỡ lỗi có được hỗ trợ hay không và đăng ký lệnh gọi lại nếu phần mở rộng đó được thiết bị hoặc lớp xác thực hỗ trợ.

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

Sau khi ứng dụng đăng ký và bật lệnh gọi lại, hệ thống sẽ định tuyến thông báo gỡ lỗi cho ứng dụng.

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

Sử dụng các lớp xác thực bên ngoài

Bạn không phải đóng gói lớp xác thực trong APK; các thiết bị chạy Android 9 (API cấp 28) trở lên có thể sử dụng lớp xác thực bên ngoài tệp nhị phân cũng như tắt và bật các lớp này một cách linh động. Hãy thực hiện các bước trong phần này để đẩy lớp xác thực đến thiết bị kiểm thử của bạn:

Cho phép ứng dụng sử dụng lớp xác thực bên ngoài

Mô hình và chính sách bảo mật của Android khác biệt đáng kể so với các nền tảng khác. Để tải lớp xác thực bên ngoài, một trong các điều kiện sau phải đúng:

  • Ứng dụng mục tiêu là ứng dụng có thể gỡ lỗi. Tuỳ chọn này giúp cung cấp nhiều thông tin gỡ lỗi hơn, nhưng có thể ảnh hưởng tiêu cực đến hiệu suất của ứng dụng.

  • Ứng dụng mục tiêu chạy trên bản dựng userdebug của hệ điều hành cấp quyền truy cập gốc.

  • Chỉ những ứng dụng nhắm mục tiêu đến Android 11 (API cấp 30) trở lên: Tệp kê khai Android mục tiêu của bạn bao gồm phần tử meta-data sau đây:

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

Tải một lớp xác thực bên ngoài

Thiết bị chạy Android 9 (API cấp 28) trở lên cho phép Vulkan tải lớp xác thực từ bộ nhớ cục bộ của ứng dụng. Kể từ Android 10 (API cấp 29), Vulkan cũng có thể tải lớp xác thực từ một APK riêng. Bạn có thể chọn bất kỳ phương thức nào mà bạn thích miễn là phiên bản Android hỗ trợ phương thức đó.

Tải tệp nhị phân của lớp xác thực từ bộ nhớ cục bộ của thiết bị

Vì Vulkan tìm kiếm tệp nhị phân trong thư mục lưu trữ dữ liệu tạm thời của thiết bị nên trước tiên, bạn phải đẩy tệp nhị phân vào thư mục đó bằng Cầu gỡ lỗi Android (adb). Cách thức như sau:

  1. Sử dụng lệnh adb push để tải tệp nhị phân của lớp vào bộ nhớ dữ liệu của ứng dụng trên thiết bị:

    $ adb push libVkLayer_khronos_validation.so /data/local/tmp
    
  2. Sử dụng các lệnh adb shellrun-as để tải lớp thông qua quy trình ứng dụng. Nghĩa là, tệp nhị phân có cùng quyền truy cập thiết bị như ứng dụng mà không yêu cầu quyền truy cập gốc.

    $ 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. Bật lớp.

Tải tệp nhị phân của lớp xác thực từ một APK khác

Bạn có thể sử dụng adb để cài đặt một APK chứa lớp này rồi bật lớp.

adb install --abi abi path_to_apk

Bật các lớp bên ngoài ứng dụng

Bạn có thể bật các lớp Vulkan trên mỗi ứng dụng hoặc trên toàn cục. Các chế độ cài đặt theo từng ứng dụng vẫn được giữ nguyên khi khởi động lại, trong khi các thuộc tính chung sẽ bị xoá khi bạn khởi động lại.

Bật lớp trên mỗi ứng dụng

Các bước sau đây mô tả cách bật lớp trên mỗi ứng dụng:

  1. Sử dụng chế độ cài đặt adb shell để bật các lớp:

    $ adb shell settings put global enable_gpu_debug_layers 1
    
  2. Chỉ định ứng dụng mục tiêu bạn cần bật các lớp:

    $ adb shell settings put global gpu_debug_app <package_name>
    
  3. Chỉ định danh sách các lớp cần bật (từ trên xuống dưới), phân tách từng lớp bằng dấu hai chấm:

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

    Vì chúng tôi có một lớp xác thực Khronos duy nhất, nên lệnh sẽ có dạng như sau:

    $ adb shell settings put global gpu_debug_layers VK_LAYER_KHRONOS_validation
    
  4. Hãy chỉ định một hoặc nhiều gói để tìm kiếm các lớp bên trong:

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

Bạn có thể kiểm tra xem chế độ cài đặt này có được bật hay không bằng các lệnh sau đây:

$ 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

Vì các chế độ cài đặt bạn áp dụng vẫn tồn tại sau những lần khởi động lại thiết bị, nên bạn có thể xoá các chế độ cài đặt đó sau khi tải các lớp:

$ 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

Bật các lớp trên toàn cục

Bạn có thể bật một hoặc nhiều lớp trên toàn cục cho đến lần khởi động lại tiếp theo. Lệnh này cố gắng tải các lớp cho tất cả ứng dụng, bao gồm cả các tệp thực thi gốc.

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