Làm quen với Vulkan trên Android

1. Giới thiệu

Tại sao tôi nên dùng Vulkan trong trò chơi?

Vulkan là API đồ hoạ chính cấp thấp trên Android. Vulkan giúp bạn đạt được hiệu suất cao hơn cho những trò chơi triển khai công cụ phát triển trò chơi và trình kết xuất riêng.

Vulkan có trên Android từ Android 7.0 (API cấp 24). Lưu ý: Các thiết bị Android 64 bit mới kể từ Android 10.0 cần phải hỗ trợ Vulkan 1.1. Hồ sơ cơ sở trên Android 2022 cũng yêu cầu API Vulkan phiên bản tối thiểu là 1.1.

Các trò chơi có nhiều hàm gọi vẽ và sử dụng OpenGL ES có thể bị hao tổn đáng kể về trình điều khiển do mất nhiều chi phí khi thực hiện hàm gọi vẽ trong OpenGL ES. Những trò chơi này có thể bị CPU ràng buộc khi dành phần lớn thời gian kết xuất khung hình cho trình điều khiển đồ hoạ. Chúng cũng có thể giúp giảm đáng kể mức sử dụng CPU và pin khi chuyển từ OpenGL ES sang Vulkan. Điều này đặc biệt phù hợp nếu trò chơi có các cảnh phức tạp mà không thể sử dụng hiệu ứng nhiều mảnh ghép một cách hiệu quả để giảm thiểu hàm gọi vẽ.

Sản phẩm bạn sẽ tạo ra

Trong lớp học lập trình này, bạn sẽ tạo một ứng dụng Android C++ cơ bản rồi thêm mã để thiết lập quy trình kết xuất của Vulkan. Sau đó, bạn sẽ triển khai mã sử dụng Vulkan để kết xuất một hình tam giác xoay, có hoạ tiết trên màn hình.

Những gì bạn cần

2. Thiết lập

Thiết lập môi trường phát triển

Nếu trước đây chưa từng làm việc với các dự án gốc trong Android Studio, thì có thể bạn cần phải cài đặt Android NDK và CMake. Nếu bạn đã cài đặt các công cụ này, hãy chuyển sang bước Thiết lập dự án.

Kiểm tra để đảm bảo rằng bạn đã cài đặt SDK, NDK và CMake

Mở Android Studio. Khi cửa sổ Chào mừng bạn đến với Android Studio hiển thị, hãy mở trình đơn thả xuống Định cấu hình rồi chọn tuỳ chọn Trình quản lý SDK.

3b7b47a139bc456.png

Nếu đã mở một dự án hiện có, bạn có thể mở Trình quản lý SDK qua trình đơn Công cụ. Nhấp vào trình đơn Tools (Công cụ) rồi chọn SDK Manager (Trình quản lý SDK), cửa sổ Trình quản lý SDK sẽ mở ra.

Trong thanh bên, chọn theo thứ tự: Appearance & Behavior > System Settings > Android SDK (Giao diện và hành vi > Cài đặt hệ thống > Android SDK). Chọn thẻ SDK Platforms (Nền tảng SDK) trong ngăn SDK Android để hiển thị danh sách các tuỳ chọn công cụ đã cài đặt. Đảm bảo bạn đã cài đặt SDK Android 12.0 trở lên.

931f6ae02822f417.png

Tiếp theo, hãy chọn thẻ SDK Tools (Bộ công cụ SDK) và đảm bảo bạn đã cài đặt NDK cũng như CMake.

Lưu ý: Phiên bản chính xác không quan trọng, miễn đó là phiên bản mới. Tuy nhiên, chúng ta đang dùng NDK 26.1.10909125 và CMake 3.22.1. Phiên bản NDK đang được cài đặt theo mặc định sẽ thay đổi theo thời gian tuỳ theo các bản phát hành NDK tiếp theo. Nếu bạn cần cài đặt một phiên bản NDK cụ thể, hãy làm theo hướng dẫn trong tài liệu tham khảo của Android Studio về cách cài đặt NDK trong phần "Install a specific version of the NDK" (Cài đặt phiên bản NDK cụ thể).

d28adf9279adec4.png

Sau khi chọn tất cả các công cụ cần thiết, hãy nhấp vào nút Apply (Áp dụng) ở cuối cửa sổ để cài đặt các công cụ đó. Sau đó, bạn có thể nhấp vào nút OK để đóng cửa sổ SDK Android.

Thiết lập dự án

Một dự án khởi động có nguồn gốc từ mẫu C++ đã được thiết lập cho bạn trong một kho lưu trữ git. Dự án này triển khai việc khởi động ứng dụng và xử lý sự kiện nhưng chưa thiết lập hay kết xuất hình ảnh đồ hoạ nào.

Sao chép kho lưu trữ

Từ dòng lệnh, hãy thay đổi thành thư mục bạn muốn chứa thư mục gốc của dự án và sao chép thư mục đó từ GitHub:

git clone -b codelab/start https://github.com/android/getting-started-with-vulkan-on-android-codelab.git --recurse-submodules

Hãy đảm bảo rằng bạn đang bắt đầu từ cam kết ban đầu của kho lưu trữ có tiêu đề [codelab] start: empty app.

Mở dự án bằng Android Studio, tạo dự án rồi chạy dự án này trên một thiết bị đi kèm. Dự án này sẽ chạy trên màn hình trống màu đen, hình ảnh đồ hoạ được thêm và kết xuất trong các phần sau.

3. Tạo một thực thể và thiết bị Vulkan

Bước đầu tiên trong quá trình khởi động API Vulkan để sử dụng là tạo một đối tượng thực thể Vulkan (VkInstance).

Đối tượng VkInstance này đại diện cho thực thể của thời gian chạy Vulkan trong ứng dụng. Đây là đối tượng gốc của API Vulkan và dùng để tạo thực thể đối tượng thiết bị Vulkan cũng như truy xuất thông tin về đối tượng này và lớp bất kỳ mà nó muốn kích hoạt.

Khi tạo VkInstance, ứng dụng phải cung cấp thông tin về chính nó, chẳng hạn như tên, phiên bản và tiện ích của thực thể Vulkan mà ứng dụng cần.

Thiết kế API Vulkan bao gồm một hệ thống lớp cung cấp cơ chế để chặn và xử lý các lệnh gọi API trước khi chúng tiếp cận trình điều khiển GPU. Ứng dụng này có thể chỉ định các lớp cần kích hoạt khi tạo VkInstance. Lớp thường dùng nhất là lớp xác thực Vulkan. Lớp này cung cấp dữ liệu phân tích thời gian chạy của việc sử dụng API để tìm ra các lỗi hoặc phương pháp cải thiện hiệu suất dưới mức tối ưu.

Sau khi VkInstance được tạo, ứng dụng có thể sử dụng đối tượng này để truy vấn các thiết bị thực có trên hệ thống, tạo thiết bị logic và tạo các nền tảng để kết xuất.

VkInstance thường được tạo ngay khi khởi động ứng dụng và bị huỷ bỏ cuối cùng. Tuy nhiên, bạn có thể tạo nhiều VkInstance trong cùng một ứng dụng, chẳng hạn như nếu ứng dụng đó cần dùng nhiều GPU hoặc tạo nhiều cửa sổ.

// CODELAB: hellovk.h
void HelloVK::createInstance() {
  VkApplicationInfo appInfo{};
  appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
  appInfo.pApplicationName = "Hello Triangle";
  appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
  appInfo.pEngineName = "No Engine";
  appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
  appInfo.apiVersion = VK_API_VERSION_1_0;

  VkInstanceCreateInfo createInfo{};
  createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
  createInfo.pApplicationInfo = &appInfo;
  createInfo.enabledExtensionCount = (uint32_t)requiredExtensions.size();
  createInfo.ppEnabledExtensionNames = requiredExtensions.data();
  createInfo.pApplicationInfo = &appInfo;

  createInfo.enabledLayerCount = 0;
  createInfo.pNext = nullptr;

  VK_CHECK(vkCreateInstance(&createInfo, nullptr, &instance));
  }
}

VkPhysicalDevice là một đối tượng Vulkan đại diện cho thiết bị Vulkan thực trên hệ thống. Hầu hết các thiết bị Android sẽ chỉ trả về một đối tượng VkPhysicalDevice đại diện cho GPU. Tuy nhiên, một máy tính hoặc thiết bị Android có thể liệt kê nhiều thiết bị thực. Ví dụ: một máy tính bao gồm cả GPU riêng biệt mà GPU tích hợp.

VkPhysicalDevices có thể được truy vấn các thuộc tính, chẳng hạn như tên, nhà cung cấp, phiên bản trình điều khiển và các tính năng được hỗ trợ. Thông tin này có thể dùng để chọn thiết bị thực tốt nhất cho một ứng dụng cụ thể.

Sau khi VkPhysicalDevice được chọn, ứng dụng có thể tạo một thiết bị logic từ đối tượng này. Thiết bị logic đại diện cho thiết bị thực dành riêng cho ứng dụng. Đối tượng này có trạng thái và tài nguyên riêng, độc lập với các thiết bị logic khác có thể được tạo từ cùng một thiết bị thực.

Có nhiều loại hàng đợi bắt nguồn từ Nhóm hàng đợi khác và mỗi nhóm hàng đợi chỉ cho phép một nhóm nhỏ các lệnh. Ví dụ: có thể có một nhóm hàng đợi chỉ cho phép xử lý các lệnh tính toán hoặc một nhóm hàng đợi chỉ cho phép bộ nhớ chuyển các lệnh có liên quan.

Một VkPhysicalDevice có thể liệt kê mọi loại Nhóm hàng đợi có sẵn. Ở đây, chúng ta chỉ quan tâm đến hàng đợi hình ảnh đồ hoạ nhưng cũng có các hàng đợi khác chỉ hỗ trợ COMPUTE hoặc TRANSFER. Một Nhóm hàng đợi không có loại của riêng mình. Thay vào đó, nhóm này được biểu thị bằng loại chỉ mục dạng số uint32_t bên trong đối tượng mẹ (VkPhysicalDevice).

Có thể tạo nhiều thiết bị logic từ một VkPhysicalDevice. Điều này hữu ích cho các ứng dụng cần dùng nhiều GPU hoặc tạo nhiều cửa sổ.

VkDevice là một đối tượng Vulkan đại diện cho thiết bị Vulkan logic. Đối tượng này là dạng trừu tượng nhỏ so với thiết bị thực, cung cấp mọi chức năng cần thiết để tạo và quản lý các tài nguyên Vulkan, chẳng hạn như vùng đệm, hình ảnh và chương trình đổ bóng.

VkDevice được tạo từ VkPhysicalDevice và dành riêng cho ứng dụng đã tạo nó. Đối tượng này có trạng thái và tài nguyên riêng, độc lập với các thiết bị logic khác có thể được tạo từ cùng một thiết bị thực.

Đối tượng VkSurfaceKHR đại diện cho một nền tảng có thể là mục tiêu của các hoạt động kết xuất. Để hiển thị hình ảnh đồ hoạ trên màn hình thiết bị, bạn sẽ tạo một nền tảng bằng cách tham chiếu đến đối tượng của cửa sổ ứng dụng. Sau khi một đối tượng VkSurfaceKHR được tạo, ứng dụng có thể dùng đối tượng này để tạo đối tượng VkSwapchainKHR.

Đối tượng VkSwapchainKHR đại diện cho cơ sở hạ tầng sở hữu vùng đệm mà chúng ta sẽ kết xuất trước khi có thể trực quan hoá chúng trên màn hình. Đây là hàng đợi hình ảnh thiết yếu đang chờ xuất hiện trên màn hình. Chúng ta sẽ thu được hình ảnh đó để vẽ rồi trả về hàng đợi. Mức độ hoạt động chính xác của hàng đợi và các điều kiện để hiển thị một hình ảnh từ hàng đợi tuỳ thuộc vào cách thiết lập chuỗi hoán đổi. Tuy nhiên, mục đích chung của chuỗi này là đồng bộ hoá hoạt động trình chiếu hình ảnh với tốc độ làm mới trên màn hình.

// CODELAB: hellovk.h - Data Types
struct QueueFamilyIndices {
  std::optional<uint32_t> graphicsFamily;
  std::optional<uint32_t> presentFamily;
  bool isComplete() {
    return graphicsFamily.has_value() && presentFamily.has_value();
  }
};

struct SwapChainSupportDetails {
  VkSurfaceCapabilitiesKHR capabilities;
  std::vector<VkSurfaceFormatKHR> formats;
  std::vector<VkPresentModeKHR> presentModes;
};

struct ANativeWindowDeleter {
  void operator()(ANativeWindow *window) { ANativeWindow_release(window); }
};

Bạn có thể thiết lập lớp xác thực hỗ trợ nếu cần gỡ lỗi cho ứng dụng. Bạn cũng có thể kiểm tra các tiện ích cụ thể mà trò chơi của bạn sẽ cần.

// CODELAB: hellovk.h
bool HelloVK::checkValidationLayerSupport() {
  uint32_t layerCount;
  vkEnumerateInstanceLayerProperties(&layerCount, nullptr);

  std::vector<VkLayerProperties> availableLayers(layerCount);
  vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data());

  for (const char *layerName : validationLayers) {
    bool layerFound = false;
    for (const auto &layerProperties : availableLayers) {
      if (strcmp(layerName, layerProperties.layerName) == 0) {
        layerFound = true;
        break;
      }
    }

    if (!layerFound) {
      return false;
    }
  }
  return true;
}

std::vector<const char *> HelloVK::getRequiredExtensions(
    bool enableValidationLayers) {
  std::vector<const char *> extensions;
  extensions.push_back("VK_KHR_surface");
  extensions.push_back("VK_KHR_android_surface");
  if (enableValidationLayers) {
    extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
  }
  return extensions;
}

Sau khi bạn tìm được quy trình thiết lập phù hợp và tạo VkInstance, hãy tạo VkSurface đại diện cho cửa sổ cần kết xuất đến đó.

// CODELAB: hellovk.h
void HelloVK::createSurface() {
  assert(window != nullptr);  // window not initialized
  const VkAndroidSurfaceCreateInfoKHR create_info{
      .sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR,
      .pNext = nullptr,
      .flags = 0,
      .window = window.get()};

  VK_CHECK(vkCreateAndroidSurfaceKHR(instance, &create_info,
                                     nullptr /* pAllocator */, &surface));
}

Liệt kê thiết bị thực (GPU) có sẵn rồi chọn thiết bị thích hợp đầu tiên có sẵn.

// CODELAB: hellovk.h
void HelloVK::pickPhysicalDevice() {
  uint32_t deviceCount = 0;
  vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);

  assert(deviceCount > 0);  // failed to find GPUs with Vulkan support!

  std::vector<VkPhysicalDevice> devices(deviceCount);
  vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());

  for (const auto &device : devices) {
    if (isDeviceSuitable(device)) {
      physicalDevice = device;
      break;
    }
  }

  assert(physicalDevice != VK_NULL_HANDLE);  // failed to find a suitable GPU!
}

Để kiểm tra xem thiết bị có thích hợp hay không, chúng ta cần tìm một thiết bị hỗ trợ hàng đợi GRAPHICS.

// CODELAB: hellovk.h
bool HelloVK::isDeviceSuitable(VkPhysicalDevice device) {
  QueueFamilyIndices indices = findQueueFamilies(device);
  bool extensionsSupported = checkDeviceExtensionSupport(device);
  bool swapChainAdequate = false;
  if (extensionsSupported) {
    SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device);
    swapChainAdequate = !swapChainSupport.formats.empty() &&
                        !swapChainSupport.presentModes.empty();
  }
  return indices.isComplete() && extensionsSupported && swapChainAdequate;
}
// CODELAB: hellovk.h
bool HelloVK::checkDeviceExtensionSupport(VkPhysicalDevice device) {
  uint32_t extensionCount;
  vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount,
                                       nullptr);

  std::vector<VkExtensionProperties> availableExtensions(extensionCount);
  vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount,
                                       availableExtensions.data());

  std::set<std::string> requiredExtensions(deviceExtensions.begin(),
                                           deviceExtensions.end());

  for (const auto &extension : availableExtensions) {
    requiredExtensions.erase(extension.extensionName);
  }

  return requiredExtensions.empty();
}
// CODELAB: hellovk.h
QueueFamilyIndices HelloVK::findQueueFamilies(VkPhysicalDevice device) {
  QueueFamilyIndices indices;

  uint32_t queueFamilyCount = 0;
  vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr);

  std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
  vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount,
                                           queueFamilies.data());

  int i = 0;
  for (const auto &queueFamily : queueFamilies) {
    if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) {
      indices.graphicsFamily = i;
    }

    VkBool32 presentSupport = false;
    vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport);
    if (presentSupport) {
      indices.presentFamily = i;
    }

    if (indices.isComplete()) {
      break;
    }

    i++;
  }
  return indices;
}

Sau khi tìm được PhysicalDevice để sử dụng, hãy tạo một thiết bị logic (gọi là VkDevice). Thiết bị này đại diện cho một thiết bị Vulkan đã khởi động, sẵn sàng tạo mọi đối tượng khác để ứng dụng của bạn dùng.

// CODELAB: hellovk.h
void HelloVK::createLogicalDeviceAndQueue() {
  QueueFamilyIndices indices = findQueueFamilies(physicalDevice);
  std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;
  std::set<uint32_t> uniqueQueueFamilies = {indices.graphicsFamily.value(),
                                            indices.presentFamily.value()};
  float queuePriority = 1.0f;
  for (uint32_t queueFamily : uniqueQueueFamilies) {
    VkDeviceQueueCreateInfo queueCreateInfo{};
    queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
    queueCreateInfo.queueFamilyIndex = queueFamily;
    queueCreateInfo.queueCount = 1;
    queueCreateInfo.pQueuePriorities = &queuePriority;
    queueCreateInfos.push_back(queueCreateInfo);
  }

  VkPhysicalDeviceFeatures deviceFeatures{};

  VkDeviceCreateInfo createInfo{};
  createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
  createInfo.queueCreateInfoCount =
      static_cast<uint32_t>(queueCreateInfos.size());
  createInfo.pQueueCreateInfos = queueCreateInfos.data();
  createInfo.pEnabledFeatures = &deviceFeatures;
  createInfo.enabledExtensionCount =
      static_cast<uint32_t>(deviceExtensions.size());
  createInfo.ppEnabledExtensionNames = deviceExtensions.data();
  if (enableValidationLayers) {
    createInfo.enabledLayerCount =
        static_cast<uint32_t>(validationLayers.size());
    createInfo.ppEnabledLayerNames = validationLayers.data();
  } else {
    createInfo.enabledLayerCount = 0;
  }

  VK_CHECK(vkCreateDevice(physicalDevice, &createInfo, nullptr, &device));

  vkGetDeviceQueue(device, indices.graphicsFamily.value(), 0, &graphicsQueue);
  vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue);
}

Ở cuối bước này, bạn sẽ chỉ thấy một cửa sổ màu đen không có nội dung nào được kết xuất vì đây vẫn là phần giữa của quy trình thiết lập. Nếu xảy ra sự cố, bạn có thể so sánh công việc của mình với cam kết của kho lưu trữ có tiêu đề [codelab] step: create instance and device.

4. Tạo Swapchain và đồng bộ hoá các đối tượng

VkSwapchain là một đối tượng Vulkan đại diện cho hàng đợi hình ảnh có thể xuất hiện trên màn hình. Đối tượng này dùng để triển khai vùng đệm kép hoặc vùng đệm bộ ba, có thể giảm thiểu tình trạng xé hình và cải thiện hiệu suất.

Để tạo VkSwapchain, trước tiên, ứng dụng phải tạo một đối tượng VkSurfaceKHR. Chúng tôi đã tạo đối tượng VkSurfaceKHR khi thiết lập cửa sổ ở bước tạo thực thể.

Đối tượng VkSwapchainKHR sẽ có nhiều hình ảnh được liên kết. Những hình ảnh này dùng để lưu trữ cảnh đã kết xuất. Ứng dụng có thể lấy một hình ảnh từ đối tượng VkSwapchainKHR, kết xuất hình ảnh rồi hiển thị trên màn hình.

Sau khi xuất hiện trên màn hình, hình ảnh này sẽ không còn trong ứng dụng nữa. Ứng dụng phải lấy một hình ảnh khác từ đối tượng VkSwapchainKHR trước khi nó có thể kết xuất lại.

VkSwapchain thường được tạo khi khởi động ứng dụng và bị huỷ bỏ cuối cùng. Tuy nhiên, bạn có thể tạo và huỷ bỏ nhiều VkSwapchain trong cùng một ứng dụng, chẳng hạn như nếu ứng dụng đó cần dùng nhiều GPU hoặc tạo nhiều cửa sổ.

Đối tượng đồng bộ hoá là đối tượng dùng cho quá trình đồng bộ hoá. Vulkan có các đối tượng VkFence, VkSemaphore và VkEvent dùng để kiểm soát quyền truy cập vào tài nguyên trên nhiều hàng đợi. Đây là những đối tượng cần thiết nếu bạn dùng nhiều hàng đợi và lượt kết xuất. Tuy nhiên, trong ví dụ đơn giản của mình, chúng tôi sẽ không sử dụng chúng.

// CODELAB: hellovk.h
void HelloVK::createSyncObjects() {
  imageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
  renderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
  inFlightFences.resize(MAX_FRAMES_IN_FLIGHT);

  VkSemaphoreCreateInfo semaphoreInfo{};
  semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;

  VkFenceCreateInfo fenceInfo{};
  fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
  fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
  for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
    VK_CHECK(vkCreateSemaphore(device, &semaphoreInfo, nullptr,
                               &imageAvailableSemaphores[i]));

    VK_CHECK(vkCreateSemaphore(device, &semaphoreInfo, nullptr,
                               &renderFinishedSemaphores[i]));

    VK_CHECK(vkCreateFence(device, &fenceInfo, nullptr, &inFlightFences[i]));
  }
}
// CODELAB: hellovk.h
void HelloVK::createSwapChain() {
  SwapChainSupportDetails swapChainSupport =
      querySwapChainSupport(physicalDevice);

  auto chooseSwapSurfaceFormat =
      [](const std::vector<VkSurfaceFormatKHR> &availableFormats) {
        for (const auto &availableFormat : availableFormats) {
          if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB &&
              availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
            return availableFormat;
          }
        }
        return availableFormats[0];
      };

  VkSurfaceFormatKHR surfaceFormat =
      chooseSwapSurfaceFormat(swapChainSupport.formats);

  // Please check
  // https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkPresentModeKHR.html
  // for a discourse on different present modes.
  //
  // VK_PRESENT_MODE_FIFO_KHR = Hard Vsync
  // This is always supported on Android phones
  VkPresentModeKHR presentMode = VK_PRESENT_MODE_FIFO_KHR;

  uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1;
  if (swapChainSupport.capabilities.maxImageCount > 0 &&
      imageCount > swapChainSupport.capabilities.maxImageCount) {
    imageCount = swapChainSupport.capabilities.maxImageCount;
  }
  pretransformFlag = swapChainSupport.capabilities.currentTransform;

  VkSwapchainCreateInfoKHR createInfo{};
  createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
  createInfo.surface = surface;
  createInfo.minImageCount = imageCount;
  createInfo.imageFormat = surfaceFormat.format;
  createInfo.imageColorSpace = surfaceFormat.colorSpace;
  createInfo.imageExtent = displaySizeIdentity;
  createInfo.imageArrayLayers = 1;
  createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
  createInfo.preTransform = pretransformFlag;

  QueueFamilyIndices indices = findQueueFamilies(physicalDevice);
  uint32_t queueFamilyIndices[] = {indices.graphicsFamily.value(),
                                   indices.presentFamily.value()};

  if (indices.graphicsFamily != indices.presentFamily) {
    createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
    createInfo.queueFamilyIndexCount = 2;
    createInfo.pQueueFamilyIndices = queueFamilyIndices;
  } else {
    createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
    createInfo.queueFamilyIndexCount = 0;
    createInfo.pQueueFamilyIndices = nullptr;
  }
  createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR;
  createInfo.presentMode = presentMode;
  createInfo.clipped = VK_TRUE;
  createInfo.oldSwapchain = VK_NULL_HANDLE;

  VK_CHECK(vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain));

  vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr);
  swapChainImages.resize(imageCount);
  vkGetSwapchainImagesKHR(device, swapChain, &imageCount,
                          swapChainImages.data());

  swapChainImageFormat = surfaceFormat.format;
  swapChainExtent = displaySizeIdentity;
}
// CODELAB: hellovk.h
SwapChainSupportDetails HelloVK::querySwapChainSupport(
    VkPhysicalDevice device) {
  SwapChainSupportDetails details;

  vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface,
                                            &details.capabilities);

  uint32_t formatCount;
  vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr);

  if (formatCount != 0) {
    details.formats.resize(formatCount);
    vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount,
                                         details.formats.data());
  }

  uint32_t presentModeCount;
  vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount,
                                            nullptr);

  if (presentModeCount != 0) {
    details.presentModes.resize(presentModeCount);
    vkGetPhysicalDeviceSurfacePresentModesKHR(
        device, surface, &presentModeCount, details.presentModes.data());
  }
  return details;
}

Bạn cũng có thể cần chuẩn bị để tạo lại chuỗi hoán đổi sau khi thiết bị mất ngữ cảnh. Ví dụ: khi người dùng chuyển sang một ứng dụng khác.

// CODELAB: hellovk.h
void HelloVK::reset(ANativeWindow *newWindow, AAssetManager *newManager) {
  window.reset(newWindow);
  assetManager = newManager;
  if (initialized) {
    createSurface();
    recreateSwapChain();
  }
}

void HelloVK::recreateSwapChain() {
  vkDeviceWaitIdle(device);
  cleanupSwapChain();
  createSwapChain();
}

Ở cuối bước này, bạn sẽ chỉ thấy một cửa sổ màu đen không có nội dung nào được kết xuất vì đây vẫn là phần giữa của quy trình thiết lập. Nếu xảy ra sự cố, bạn có thể so sánh công việc của mình với cam kết của kho lưu trữ có tiêu đề [codelab] step: create swapchain and sync objects.

5. Tạo Renderpass và Framebuffer

VkImageView là một đối tượng Vulkan mô tả cách truy cập vào VkImage. Đối tượng này chỉ định phạm vi tài nguyên phụ của hình ảnh cần truy cập, định dạng pixel cần sử dụng và phương thức swizzle cần áp dụng cho các kênh.

VkRenderPass là một đối tượng Vulkan mô tả cách GPU sẽ kết xuất một cảnh. Đối tượng này chỉ định các tệp đính kèm sẽ được dùng, thứ tự tệp đính kèm được kết xuất và cách chúng được sử dụng ở từng giai đoạn của quy trình kết xuất.

VkFramebuffer là một đối tượng Vulkan đại diện cho tập hợp khung hiển thị hình ảnh sẽ được dùng làm tệp đính kèm trong quá trình thực thi một lượt kết xuất. Nói cách khác, đối tượng này liên kết tệp đính kèm hình ảnh thực tế với lượt kết xuất đó.

// CODELAB: hellovk.h
void HelloVK::createImageViews() {
  swapChainImageViews.resize(swapChainImages.size());
  for (size_t i = 0; i < swapChainImages.size(); i++) {
    VkImageViewCreateInfo createInfo{};
    createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
    createInfo.image = swapChainImages[i];
    createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
    createInfo.format = swapChainImageFormat;
    createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
    createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
    createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
    createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
    createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
    createInfo.subresourceRange.baseMipLevel = 0;
    createInfo.subresourceRange.levelCount = 1;
    createInfo.subresourceRange.baseArrayLayer = 0;
    createInfo.subresourceRange.layerCount = 1;
    VK_CHECK(vkCreateImageView(device, &createInfo, nullptr,
                               &swapChainImageViews[i]));
  }
}

Tệp đính kèm trong Vulkan thường được gọi là mục tiêu kết xuất, thường là một hình ảnh dùng làm dữ liệu đầu ra cho quá trình kết xuất. Chỉ định dạng này cần được mô tả tại đây, chẳng hạn như lượt kết xuất có thể tạo ra một định dạng màu cụ thể hoặc định dạng khung tô chiều sâu. Bạn cũng cần chỉ định xem tệp đính kèm nên giữ nguyên, loại bỏ hay làm rõ nội dung khi bắt đầu lượt kết xuất.

// CODELAB: hellovk.h
void HelloVK::createRenderPass() {
  VkAttachmentDescription colorAttachment{};
  colorAttachment.format = swapChainImageFormat;
  colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;

  colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
  colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;

  colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
  colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;

  colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
  colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;

  VkAttachmentReference colorAttachmentRef{};
  colorAttachmentRef.attachment = 0;
  colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

  VkSubpassDescription subpass{};
  subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
  subpass.colorAttachmentCount = 1;
  subpass.pColorAttachments = &colorAttachmentRef;

  VkSubpassDependency dependency{};
  dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
  dependency.dstSubpass = 0;
  dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
  dependency.srcAccessMask = 0;
  dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
  dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;

  VkRenderPassCreateInfo renderPassInfo{};
  renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
  renderPassInfo.attachmentCount = 1;
  renderPassInfo.pAttachments = &colorAttachment;
  renderPassInfo.subpassCount = 1;
  renderPassInfo.pSubpasses = &subpass;
  renderPassInfo.dependencyCount = 1;
  renderPassInfo.pDependencies = &dependency;

  VK_CHECK(vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass));
}

Framebuffer đại diện cho đường liên kết đến hình ảnh thực tế có thể dùng cho tệp đính kèm (mục tiêu kết xuất). Tạo một đối tượng Framebuffer bằng cách chỉ định RenderPass và tập hợp ImageView.

// CODELAB: hellovk.h
void HelloVK::createFramebuffers() {
  swapChainFramebuffers.resize(swapChainImageViews.size());
  for (size_t i = 0; i < swapChainImageViews.size(); i++) {
    VkImageView attachments[] = {swapChainImageViews[i]};

    VkFramebufferCreateInfo framebufferInfo{};
    framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
    framebufferInfo.renderPass = renderPass;
    framebufferInfo.attachmentCount = 1;
    framebufferInfo.pAttachments = attachments;
    framebufferInfo.width = swapChainExtent.width;
    framebufferInfo.height = swapChainExtent.height;
    framebufferInfo.layers = 1;

    VK_CHECK(vkCreateFramebuffer(device, &framebufferInfo, nullptr,
                                 &swapChainFramebuffers[i]));
  }
}

Ở cuối bước này, bạn sẽ chỉ thấy một cửa sổ màu đen không có nội dung nào được kết xuất vì đây vẫn là phần giữa của quy trình thiết lập. Nếu xảy ra sự cố, bạn có thể so sánh công việc của mình với cam kết của kho lưu trữ có tiêu đề [codelab] step: create renderpass and framebuffer.

6. Tạo Chương trình đổ bóng và Quy trình

VkShaderModule là một đối tượng Vulkan đại diện cho chương trình đổ bóng có thể lập trình. Chương trình đổ bóng được dùng để thực hiện nhiều hoạt động về dữ liệu đồ hoạ, chẳng hạn như biến đổi đỉnh, đổ bóng pixel và tính toán các hiệu ứng chung.

VkPipeline là một đối tượng Vulkan đại diện cho quy trình đồ hoạ có thể lập trình. Đối tượng này là tập hợp đối tượng trạng thái mô tả cách GPU sẽ kết xuất một cảnh.

VkDescriptorSetLayout là mẫu cho một VkDescriptorSet. Đối tượng này có thể là một nhóm dữ liệu đặc tả. Dữ liệu đặc tả là ô điều khiển cho phép chương trình đổ bóng truy cập vào tài nguyên (chẳng hạn như Vùng đệm, Hình ảnh và Trình lấy mẫu).

// CODELAB: hellovk.h
void HelloVK::createDescriptorSetLayout() {
  VkDescriptorSetLayoutBinding uboLayoutBinding{};
  uboLayoutBinding.binding = 0;
  uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
  uboLayoutBinding.descriptorCount = 1;
  uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
  uboLayoutBinding.pImmutableSamplers = nullptr;

  VkDescriptorSetLayoutCreateInfo layoutInfo{};
  layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
  layoutInfo.bindingCount = 1;
  layoutInfo.pBindings = &uboLayoutBinding;

  VK_CHECK(vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr,
                                       &descriptorSetLayout));
}

Xác định hàm createShaderModule cần tải trong chương trình đổ bóng cho đối tượng VkShaderModule

// CODELAB: hellovk.h
VkShaderModule HelloVK::createShaderModule(const std::vector<uint8_t> &code) {
  VkShaderModuleCreateInfo createInfo{};
  createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
  createInfo.codeSize = code.size();

  // Satisfies alignment requirements since the allocator
  // in vector ensures worst case requirements
  createInfo.pCode = reinterpret_cast<const uint32_t *>(code.data());
  VkShaderModule shaderModule;
  VK_CHECK(vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule));

  return shaderModule;
}

Tạo quy trình đồ hoạ tải một đỉnh đơn giản và chương trình đổ bóng mảnh.

// CODELAB: hellovk.h
void HelloVK::createGraphicsPipeline() {
  auto vertShaderCode =
      LoadBinaryFileToVector("shaders/shader.vert.spv", assetManager);
  auto fragShaderCode =
      LoadBinaryFileToVector("shaders/shader.frag.spv", assetManager);

  VkShaderModule vertShaderModule = createShaderModule(vertShaderCode);
  VkShaderModule fragShaderModule = createShaderModule(fragShaderCode);

  VkPipelineShaderStageCreateInfo vertShaderStageInfo{};
  vertShaderStageInfo.sType =
      VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
  vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;
  vertShaderStageInfo.module = vertShaderModule;
  vertShaderStageInfo.pName = "main";

  VkPipelineShaderStageCreateInfo fragShaderStageInfo{};
  fragShaderStageInfo.sType =
      VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
  fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
  fragShaderStageInfo.module = fragShaderModule;
  fragShaderStageInfo.pName = "main";

  VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo,
                                                    fragShaderStageInfo};

  VkPipelineVertexInputStateCreateInfo vertexInputInfo{};
  vertexInputInfo.sType =
      VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
  vertexInputInfo.vertexBindingDescriptionCount = 0;
  vertexInputInfo.pVertexBindingDescriptions = nullptr;
  vertexInputInfo.vertexAttributeDescriptionCount = 0;
  vertexInputInfo.pVertexAttributeDescriptions = nullptr;

  VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
  inputAssembly.sType =
      VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
  inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
  inputAssembly.primitiveRestartEnable = VK_FALSE;

  VkPipelineViewportStateCreateInfo viewportState{};
  viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
  viewportState.viewportCount = 1;
  viewportState.scissorCount = 1;

  VkPipelineRasterizationStateCreateInfo rasterizer{};
  rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
  rasterizer.depthClampEnable = VK_FALSE;
  rasterizer.rasterizerDiscardEnable = VK_FALSE;
  rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
  rasterizer.lineWidth = 1.0f;

  rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
  rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;

  rasterizer.depthBiasEnable = VK_FALSE;
  rasterizer.depthBiasConstantFactor = 0.0f;
  rasterizer.depthBiasClamp = 0.0f;
  rasterizer.depthBiasSlopeFactor = 0.0f;

  VkPipelineMultisampleStateCreateInfo multisampling{};
  multisampling.sType =
      VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
  multisampling.sampleShadingEnable = VK_FALSE;
  multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
  multisampling.minSampleShading = 1.0f;
  multisampling.pSampleMask = nullptr;
  multisampling.alphaToCoverageEnable = VK_FALSE;
  multisampling.alphaToOneEnable = VK_FALSE;

  VkPipelineColorBlendAttachmentState colorBlendAttachment{};
  colorBlendAttachment.colorWriteMask =
      VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
      VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
  colorBlendAttachment.blendEnable = VK_FALSE;

  VkPipelineColorBlendStateCreateInfo colorBlending{};
  colorBlending.sType =
      VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
  colorBlending.logicOpEnable = VK_FALSE;
  colorBlending.logicOp = VK_LOGIC_OP_COPY;
  colorBlending.attachmentCount = 1;
  colorBlending.pAttachments = &colorBlendAttachment;
  colorBlending.blendConstants[0] = 0.0f;
  colorBlending.blendConstants[1] = 0.0f;
  colorBlending.blendConstants[2] = 0.0f;
  colorBlending.blendConstants[3] = 0.0f;

  VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
  pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
  pipelineLayoutInfo.setLayoutCount = 1;
  pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;
  pipelineLayoutInfo.pushConstantRangeCount = 0;
  pipelineLayoutInfo.pPushConstantRanges = nullptr;

  VK_CHECK(vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr,
                                  &pipelineLayout));
  std::vector<VkDynamicState> dynamicStateEnables = {VK_DYNAMIC_STATE_VIEWPORT,
                                                     VK_DYNAMIC_STATE_SCISSOR};
  VkPipelineDynamicStateCreateInfo dynamicStateCI{};
  dynamicStateCI.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
  dynamicStateCI.pDynamicStates = dynamicStateEnables.data();
  dynamicStateCI.dynamicStateCount =
      static_cast<uint32_t>(dynamicStateEnables.size());

  VkGraphicsPipelineCreateInfo pipelineInfo{};
  pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
  pipelineInfo.stageCount = 2;
  pipelineInfo.pStages = shaderStages;
  pipelineInfo.pVertexInputState = &vertexInputInfo;
  pipelineInfo.pInputAssemblyState = &inputAssembly;
  pipelineInfo.pViewportState = &viewportState;
  pipelineInfo.pRasterizationState = &rasterizer;
  pipelineInfo.pMultisampleState = &multisampling;
  pipelineInfo.pDepthStencilState = nullptr;
  pipelineInfo.pColorBlendState = &colorBlending;
  pipelineInfo.pDynamicState = &dynamicStateCI;
  pipelineInfo.layout = pipelineLayout;
  pipelineInfo.renderPass = renderPass;
  pipelineInfo.subpass = 0;
  pipelineInfo.basePipelineHandle = VK_NULL_HANDLE;
  pipelineInfo.basePipelineIndex = -1;

  VK_CHECK(vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo,
                                     nullptr, &graphicsPipeline));
  vkDestroyShaderModule(device, fragShaderModule, nullptr);
  vkDestroyShaderModule(device, vertShaderModule, nullptr);
}

Ở cuối bước này, bạn sẽ chỉ thấy một cửa sổ màu đen không có nội dung nào được kết xuất vì đây vẫn là phần giữa của quy trình thiết lập. Nếu xảy ra sự cố, bạn có thể so sánh công việc của mình với cam kết của kho lưu trữ có tiêu đề [codelab] step: create shader and pipeline.

7. DescriptorSet và Vùng đệm đồng nhất

VkDescriptorSet là một đối tượng Vulkan đại diện cho tập hợp tài nguyên dữ liệu đặc tả. Tài nguyên dữ liệu đặc tả dùng để cung cấp dữ liệu đầu vào cho chương trình đổ bóng, chẳng hạn như vùng đệm đồng nhất, trình lấy mẫu hình ảnh và vùng đệm lưu trữ. Để tạo các VkDescriptorSet, chúng ta sẽ cần tạo một VkDescriptorPool.

VkBuffer là vùng đệm bộ nhớ dùng để chia sẻ dữ liệu giữa GPU và CPU. Khi được dùng làm Vùng đệm đồng nhất, vùng đệm này sẽ truyền dữ liệu đến chương trình đổ bóng dưới dạng các biến đồng nhất. Các biến đồng nhất là hằng số mà mọi chương trình đổ bóng có thể dùng trong một quy trình.

// CODELAB: hellovk.h
void HelloVK::createDescriptorPool() {
  VkDescriptorPoolSize poolSize{};
  poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
  poolSize.descriptorCount = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);

  VkDescriptorPoolCreateInfo poolInfo{};
  poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
  poolInfo.poolSizeCount = 1;
  poolInfo.pPoolSizes = &poolSize;
  poolInfo.maxSets = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);

  VK_CHECK(vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool));
}

Tạo VkDescriptorSet được phân bổ từ VkDescriptorPool đã chỉ định.

// CODELAB: hellovk.h
void HelloVK::createDescriptorSets() {
  std::vector<VkDescriptorSetLayout> layouts(MAX_FRAMES_IN_FLIGHT,
                                             descriptorSetLayout);
  VkDescriptorSetAllocateInfo allocInfo{};
  allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
  allocInfo.descriptorPool = descriptorPool;
  allocInfo.descriptorSetCount = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);
  allocInfo.pSetLayouts = layouts.data();

  descriptorSets.resize(MAX_FRAMES_IN_FLIGHT);
  VK_CHECK(vkAllocateDescriptorSets(device, &allocInfo, descriptorSets.data()));

  for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
    VkDescriptorBufferInfo bufferInfo{};
    bufferInfo.buffer = uniformBuffers[i];
    bufferInfo.offset = 0;
    bufferInfo.range = sizeof(UniformBufferObject);

    VkWriteDescriptorSet descriptorWrite{};
    descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
    descriptorWrite.dstSet = descriptorSets[i];
    descriptorWrite.dstBinding = 0;
    descriptorWrite.dstArrayElement = 0;
    descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
    descriptorWrite.descriptorCount = 1;
    descriptorWrite.pBufferInfo = &bufferInfo;

    vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);
  }
}

Chỉ định cấu trúc Vùng đệm đồng nhất và tạo vùng đệm đồng nhất. Hãy nhớ phân bổ bộ nhớ từ VkDeviceMemory bằng vkAllocateMemory và liên kết vùng đệm này với bộ nhớ bằng vkBindBufferMemory.

// CODELAB: hellovk.h
struct UniformBufferObject {
  std::array<float, 16> mvp;
};

void HelloVK::createUniformBuffers() {
  VkDeviceSize bufferSize = sizeof(UniformBufferObject);

  uniformBuffers.resize(MAX_FRAMES_IN_FLIGHT);
  uniformBuffersMemory.resize(MAX_FRAMES_IN_FLIGHT);

  for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
    createBuffer(bufferSize, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
                     VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
                 uniformBuffers[i], uniformBuffersMemory[i]);
  }
}
// CODELAB: hellovk.h
void HelloVK::createBuffer(VkDeviceSize size, VkBufferUsageFlags usage,
                           VkMemoryPropertyFlags properties, VkBuffer &buffer,
                           VkDeviceMemory &bufferMemory) {
  VkBufferCreateInfo bufferInfo{};
  bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
  bufferInfo.size = size;
  bufferInfo.usage = usage;
  bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

  VK_CHECK(vkCreateBuffer(device, &bufferInfo, nullptr, &buffer));

  VkMemoryRequirements memRequirements;
  vkGetBufferMemoryRequirements(device, buffer, &memRequirements);

  VkMemoryAllocateInfo allocInfo{};
  allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
  allocInfo.allocationSize = memRequirements.size;
  allocInfo.memoryTypeIndex =
      findMemoryType(memRequirements.memoryTypeBits, properties);

  VK_CHECK(vkAllocateMemory(device, &allocInfo, nullptr, &bufferMemory));

  vkBindBufferMemory(device, buffer, bufferMemory, 0);
}

Dùng hàm trợ giúp để tìm đúng loại bộ nhớ.

// CODELAB: hellovk.h
/*
 * Finds the index of the memory heap which matches a particular buffer's memory
 * requirements. Vulkan manages these requirements as a bitset, in this case
 * expressed through a uint32_t.
 */
uint32_t HelloVK::findMemoryType(uint32_t typeFilter,
                                 VkMemoryPropertyFlags properties) {
  VkPhysicalDeviceMemoryProperties memProperties;
  vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);

  for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {
    if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags &
                                    properties) == properties) {
      return i;
    }
  }

  assert(false);  // failed to find a suitable memory type!
  return -1;
}

Ở cuối bước này, bạn sẽ chỉ thấy một cửa sổ màu đen không có nội dung nào được kết xuất vì đây vẫn là phần giữa của quy trình thiết lập. Nếu xảy ra sự cố, bạn có thể so sánh công việc của mình với cam kết của kho lưu trữ có tiêu đề [codelab] step: descriptorset and uniform buffer.

8. Vùng đệm lệnh – tạo, ghi và Vẽ

VkCommandPool là một đối tượng đơn giản dùng để phân bổ CommandBuffer. Đối tượng này được kết nối với một Nhóm hàng đợi cụ thể.

VkCommandBuffer là một đối tượng Vulkan đại diện cho danh sách các lệnh mà GPU sẽ thực thi. Đây là một đối tượng cấp thấp cung cấp chế độ kiểm soát chi tiết đối với GPU.

// CODELAB: hellovk.h
void HelloVK::createCommandPool() {
  QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice);
  VkCommandPoolCreateInfo poolInfo{};
  poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
  poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
  poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value();
  VK_CHECK(vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool));
}
// CODELAB: hellovk.h
void HelloVK::createCommandBuffer() {
  commandBuffers.resize(MAX_FRAMES_IN_FLIGHT);
  VkCommandBufferAllocateInfo allocInfo{};
  allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
  allocInfo.commandPool = commandPool;
  allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
  allocInfo.commandBufferCount = commandBuffers.size();

  VK_CHECK(vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data()));
}

Ở cuối bước này, bạn sẽ chỉ thấy một cửa sổ màu đen không có nội dung nào được kết xuất vì đây vẫn là phần giữa của quy trình thiết lập. Nếu xảy ra sự cố, bạn có thể so sánh công việc của mình với cam kết của kho lưu trữ có tiêu đề [codelab] step: create command pool and command buffer.

Cập nhật vùng đệm đồng nhất, ghi vùng đệm lệnh và vẽ

Các lệnh trong Vulkan (chẳng hạn như thao tác vẽ và chuyển bộ nhớ) không được thực thi trực tiếp bằng các lệnh gọi hàm. Thay vào đó, mọi thao tác đang chờ xử lý sẽ được ghi trong các đối tượng vùng đệm lệnh. Lợi ích của việc này đó là khi chúng ta sẵn sàng cho Vulkan biết việc chúng ta muốn làm, tất cả các lệnh sẽ được gửi cùng nhau và Vulkan có thể xử lý lệnh một cách hiệu quả vì mọi lệnh đều đã sẵn sàng. Ngoài ra, việc này cũng cho phép ghi lệnh trong nhiều luồng, nếu muốn.

Trong Vulkan, mọi quá trình kết xuất đều diễn ra trong RenderPass. Trong ví dụ của chúng tôi, RenderPass sẽ kết xuất vào FrameBuffer mà chúng tôi đã thiết lập trước đây.

// CODELAB: hellovk.h
void HelloVK::recordCommandBuffer(VkCommandBuffer commandBuffer,
                                  uint32_t imageIndex) {
  VkCommandBufferBeginInfo beginInfo{};
  beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
  beginInfo.flags = 0;
  beginInfo.pInheritanceInfo = nullptr;

  VK_CHECK(vkBeginCommandBuffer(commandBuffer, &beginInfo));

  VkRenderPassBeginInfo renderPassInfo{};
  renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
  renderPassInfo.renderPass = renderPass;
  renderPassInfo.framebuffer = swapChainFramebuffers[imageIndex];
  renderPassInfo.renderArea.offset = {0, 0};
  renderPassInfo.renderArea.extent = swapChainExtent;

  VkViewport viewport{};
  viewport.width = (float)swapChainExtent.width;
  viewport.height = (float)swapChainExtent.height;
  viewport.minDepth = 0.0f;
  viewport.maxDepth = 1.0f;
  vkCmdSetViewport(commandBuffer, 0, 1, &viewport);

  VkRect2D scissor{};
  scissor.extent = swapChainExtent;
  vkCmdSetScissor(commandBuffer, 0, 1, &scissor);

  static float grey;
  grey += 0.005f;
  if (grey > 1.0f) {
    grey = 0.0f;
  }
  VkClearValue clearColor = {grey, grey, grey, 1.0f};

  renderPassInfo.clearValueCount = 1;
  renderPassInfo.pClearValues = &clearColor;
  vkCmdBeginRenderPass(commandBuffer, &renderPassInfo,
                       VK_SUBPASS_CONTENTS_INLINE);
  vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
                    graphicsPipeline);
  vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
                          pipelineLayout, 0, 1, &descriptorSets[currentFrame],
                          0, nullptr);

  vkCmdDraw(commandBuffer, 3, 1, 0, 0);
  vkCmdEndRenderPass(commandBuffer);
  VK_CHECK(vkEndCommandBuffer(commandBuffer));
}

Bạn cũng có thể cần cập nhật Vùng đệm đồng nhất vì chúng tôi đang dùng cùng một ma trận biến đổi cho mọi đỉnh mà chúng tôi đang kết xuất.

// CODELAB: hellovk.h
void HelloVK::updateUniformBuffer(uint32_t currentImage) {
  SwapChainSupportDetails swapChainSupport =
      querySwapChainSupport(physicalDevice);
  UniformBufferObject ubo{};
  getPrerotationMatrix(swapChainSupport.capabilities, pretransformFlag,
                       ubo.mvp);
  void *data;
  vkMapMemory(device, uniformBuffersMemory[currentImage], 0, sizeof(ubo), 0,
              &data);
  memcpy(data, &ubo, sizeof(ubo));
  vkUnmapMemory(device, uniformBuffersMemory[currentImage]);
}

Giờ là lúc kết xuất! Tải vùng đệm lệnh mà bạn đã tạo rồi gửi vào hàng đợi.

// CODELAB: hellovk.h
void HelloVK::render() {
  if (orientationChanged) {
    onOrientationChange();
  }

  vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE,
                  UINT64_MAX);
  uint32_t imageIndex;
  VkResult result = vkAcquireNextImageKHR(
      device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame],
      VK_NULL_HANDLE, &imageIndex);
  if (result == VK_ERROR_OUT_OF_DATE_KHR) {
    recreateSwapChain();
    return;
  }
  assert(result == VK_SUCCESS ||
         result == VK_SUBOPTIMAL_KHR);  // failed to acquire swap chain image
  updateUniformBuffer(currentFrame);

  vkResetFences(device, 1, &inFlightFences[currentFrame]);
  vkResetCommandBuffer(commandBuffers[currentFrame], 0);

  recordCommandBuffer(commandBuffers[currentFrame], imageIndex);

  VkSubmitInfo submitInfo{};
  submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;

  VkSemaphore waitSemaphores[] = {imageAvailableSemaphores[currentFrame]};
  VkPipelineStageFlags waitStages[] = {
      VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
  submitInfo.waitSemaphoreCount = 1;
  submitInfo.pWaitSemaphores = waitSemaphores;
  submitInfo.pWaitDstStageMask = waitStages;
  submitInfo.commandBufferCount = 1;
  submitInfo.pCommandBuffers = &commandBuffers[currentFrame];
  VkSemaphore signalSemaphores[] = {renderFinishedSemaphores[currentFrame]};
  submitInfo.signalSemaphoreCount = 1;
  submitInfo.pSignalSemaphores = signalSemaphores;

  VK_CHECK(vkQueueSubmit(graphicsQueue, 1, &submitInfo,
                         inFlightFences[currentFrame]));

  VkPresentInfoKHR presentInfo{};
  presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;

  presentInfo.waitSemaphoreCount = 1;
  presentInfo.pWaitSemaphores = signalSemaphores;

  VkSwapchainKHR swapChains[] = {swapChain};
  presentInfo.swapchainCount = 1;
  presentInfo.pSwapchains = swapChains;
  presentInfo.pImageIndices = &imageIndex;
  presentInfo.pResults = nullptr;

  result = vkQueuePresentKHR(presentQueue, &presentInfo);
  if (result == VK_SUBOPTIMAL_KHR) {
    orientationChanged = true;
  } else if (result == VK_ERROR_OUT_OF_DATE_KHR) {
    recreateSwapChain();
  } else {
    assert(result == VK_SUCCESS);  // failed to present swap chain image!
  }
  currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
}

Xử lý hoạt động Thay đổi hướng bằng cách tạo lại chuỗi hoán đổi.

// CODELAB: hellovk.h
void HelloVK::onOrientationChange() {
  recreateSwapChain();
  orientationChanged = false;
}

Tích hợp vào vòng đời của ứng dụng.

// CODELAB: vk_main.cpp
/**
 * Called by the Android runtime whenever events happen so the
 * app can react to it.
 */
static void HandleCmd(struct android_app *app, int32_t cmd) {
  auto *engine = (VulkanEngine *)app->userData;
  switch (cmd) {
    case APP_CMD_START:
      if (engine->app->window != nullptr) {
        engine->app_backend->reset(app->window, app->activity->assetManager);
        engine->app_backend->initVulkan();
        engine->canRender = true;
      }
    case APP_CMD_INIT_WINDOW:
      // The window is being shown, get it ready.
      LOGI("Called - APP_CMD_INIT_WINDOW");
      if (engine->app->window != nullptr) {
        LOGI("Setting a new surface");
        engine->app_backend->reset(app->window, app->activity->assetManager);
        if (!engine->app_backend->initialized) {
          LOGI("Starting application");
          engine->app_backend->initVulkan();
        }
        engine->canRender = true;
      }
      break;
    case APP_CMD_TERM_WINDOW:
      // The window is being hidden or closed, clean it up.
      engine->canRender = false;
      break;
    case APP_CMD_DESTROY:
      // The window is being hidden or closed, clean it up.
      LOGI("Destroying");
      engine->app_backend->cleanup();
    default:
      break;
  }
}

/*
 * Entry point required by the Android Glue library.
 * This can also be achieved more verbosely by manually declaring JNI functions
 * and calling them from the Android application layer.
 */
void android_main(struct android_app *state) {
  VulkanEngine engine{};
  vkt::HelloVK vulkanBackend{};

  engine.app = state;
  engine.app_backend = &vulkanBackend;
  state->userData = &engine;
  state->onAppCmd = HandleCmd;

  android_app_set_key_event_filter(state, VulkanKeyEventFilter);
  android_app_set_motion_event_filter(state, VulkanMotionEventFilter);

  while (true) {
    int ident;
    int events;
    android_poll_source *source;
    while ((ident = ALooper_pollAll(engine.canRender ? 0 : -1, nullptr, &events,
                                    (void **)&source)) >= 0) {
      if (source != nullptr) {
        source->process(state, source);
      }
    }

    HandleInputEvents(state);

    engine.app_backend->render();
  }
}

Ở cuối bước này, sau cùng, bạn sẽ thấy một hình tam giác có màu trên màn hình!

b07da8354cdd1629.png

Kiểm tra nhằm đảm bảo đây là trường hợp đúng và nếu xảy ra sự cố, bạn có thể so sánh công việc của mình với cam kết của kho lưu trữ có tiêu đề [codelab] step: update uniform buffer, record command buffer and draw.

9. Xoay hình tam giác

Để xoay hình tam giác, chúng ta cần áp dụng tính năng xoay cho ma trận MVP trước khi truyền ma trận này đến chương trình đổ bóng. Thao tác này giúp ngăn trùng lặp công việc tính toán trên cùng một ma trận cần hoàn tất cho từng đỉnh trong mô hình.

Để tính toán ma trận MVP ở phía ứng dụng, cần có một ma trận biến đổi xoay. Thư viện GLM là thư viện toán học C++ để viết phần mềm đồ hoạ dựa trên các thông số kỹ thuật GLSL. Thư viện này có hàm rotate cần thiết để tạo ma trận áp dụng tính năng xoay.

// CODELAB: hellovk.h
// Additional includes to make our lives easier than composing
// transformation matrices manually
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

// change our UniformBufferObject construct to use glm::mat4
struct UniformBufferObject {
  glm::mat4 mvp;
};
// CODELAB: hellovk.h
/*
 * getPrerotationMatrix handles screen rotation with 3 hardcoded rotation
 * matrices (detailed below). We skip the 180 degrees rotation.
 */
void getPrerotationMatrix(const VkSurfaceCapabilitiesKHR &capabilities,
                          const VkSurfaceTransformFlagBitsKHR &pretransformFlag,
                          glm::mat4 &mat, float ratio) {
  // mat is initialized to the identity matrix
  mat = glm::mat4(1.0f);

  // scale by screen ratio
  mat = glm::scale(mat, glm::vec3(1.0f, ratio, 1.0f));

  // rotate 1 degree every function call.
  static float currentAngleDegrees = 0.0f;
  currentAngleDegrees += 1.0f;
  if ( currentAngleDegrees >= 360.0f ) {
    currentAngleDegrees = 0.0f;
  }

  mat = glm::rotate(mat, glm::radians(currentAngleDegrees), glm::vec3(0.0f, 0.0f, 1.0f));
}

Ở cuối bước này, bạn sẽ thấy hình tam giác đang xoay trên màn hình! Kiểm tra nhằm đảm bảo đây là trường hợp đúng và nếu xảy ra sự cố, bạn có thể so sánh công việc của mình với cam kết của kho lưu trữ có tiêu đề [codelab] step: rotate triangle.

10. Áp dụng hoạ tiết

Để áp dụng hoạ tiết cho hình tam giác, trước tiên, bạn cần tải tệp hình ảnh ở định dạng không nén trong bộ nhớ. Bước này sử dụng thư viện hình ảnh stb để tải và giải mã dữ liệu hình ảnh vào RAM, sau đó dữ liệu này được sao chép sang vùng đệm của Vulkan (VkBuffer).

// CODELAB: hellovk.h
void HelloVK::decodeImage() {
  std::vector<uint8_t> imageData = LoadBinaryFileToVector("texture.png",
                                                          assetManager);
  if (imageData.size() == 0) {
      LOGE("Fail to load image.");
      return;
  }

  unsigned char* decodedData = stbi_load_from_memory(imageData.data(),
      imageData.size(), &textureWidth, &textureHeight, &textureChannels, 0);
  if (decodedData == nullptr) {
      LOGE("Fail to load image to memory, %s", stbi_failure_reason());
      return;
  }

  size_t imageSize = textureWidth * textureHeight * textureChannels;

  VkBufferCreateInfo createInfo{};
  createInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
  createInfo.size = imageSize;
  createInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
  createInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
  VK_CHECK(vkCreateBuffer(device, &createInfo, nullptr, &stagingBuffer));

  VkMemoryRequirements memRequirements;
  vkGetBufferMemoryRequirements(device, stagingBuffer, &memRequirements);

  VkMemoryAllocateInfo allocInfo{};
  allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
  allocInfo.allocationSize = memRequirements.size;
  allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits,
      VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);

  VK_CHECK(vkAllocateMemory(device, &allocInfo, nullptr, &stagingMemory));
  VK_CHECK(vkBindBufferMemory(device, stagingBuffer, stagingMemory, 0));

  uint8_t *data;
  VK_CHECK(vkMapMemory(device, stagingMemory, 0, memRequirements.size, 0,
                       (void **)&data));
  memcpy(data, decodedData, imageSize);
  vkUnmapMemory(device, stagingMemory);

  stbi_image_free(decodedData);
}

Tiếp theo, hãy tạo VkImage từ VkBuffer được điền sẵn dữ liệu hình ảnh ở bước trước đó.

VkImage là đối tượng lưu trữ dữ liệu hoạ tiết thực tế. Đối tượng này lưu trữ dữ liệu pixel trong bộ nhớ chính của hoạ tiết nhưng không chứa nhiều thông tin về cách đọc dữ liệu đó. Đó là lý do vì sao chúng ta cần tạo VkImageView trong phần tiếp theo.

// CODELAB: hellovk.h
void HelloVK::createTextureImage() {
  VkImageCreateInfo imageInfo{};
  imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
  imageInfo.imageType = VK_IMAGE_TYPE_2D;
  imageInfo.extent.width = textureWidth;
  imageInfo.extent.height = textureHeight;
  imageInfo.extent.depth = 1;
  imageInfo.mipLevels = 1;
  imageInfo.arrayLayers = 1;
  imageInfo.format = VK_FORMAT_R8G8B8_UNORM;
  imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
  imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
  imageInfo.usage =
      VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
  imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
  imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

  VK_CHECK(vkCreateImage(device, &imageInfo, nullptr, &textureImage));

  VkMemoryRequirements memRequirements;
  vkGetImageMemoryRequirements(device, textureImage, &memRequirements);

  VkMemoryAllocateInfo allocInfo{};
  allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
  allocInfo.allocationSize = memRequirements.size;
  allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits,
                                          VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);

  VK_CHECK(vkAllocateMemory(device, &allocInfo, nullptr, &textureImageMemory));

  vkBindImageMemory(device, textureImage, textureImageMemory, 0);
}
// CODELAB: hellovk.h
void HelloVK::copyBufferToImage() {
  VkImageSubresourceRange subresourceRange{};
  subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
  subresourceRange.baseMipLevel = 0;
  subresourceRange.levelCount = 1;
  subresourceRange.layerCount = 1;

  VkImageMemoryBarrier imageMemoryBarrier{};
  imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
  imageMemoryBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
  imageMemoryBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
  imageMemoryBarrier.image = textureImage;
  imageMemoryBarrier.subresourceRange = subresourceRange;
  imageMemoryBarrier.srcAccessMask = 0;
  imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
  imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
  imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;

  VkCommandBuffer cmd;
  VkCommandBufferAllocateInfo cmdAllocInfo{};
  cmdAllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
  cmdAllocInfo.commandPool = commandPool;
  cmdAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
  cmdAllocInfo.commandBufferCount = 1;

  VK_CHECK(vkAllocateCommandBuffers(device, &cmdAllocInfo, &cmd));

  VkCommandBufferBeginInfo beginInfo{};
  beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
  vkBeginCommandBuffer(cmd, &beginInfo);

  vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_HOST_BIT,
                       VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0,
                       nullptr, 1, &imageMemoryBarrier);

  VkBufferImageCopy bufferImageCopy{};
  bufferImageCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
  bufferImageCopy.imageSubresource.mipLevel = 0;
  bufferImageCopy.imageSubresource.baseArrayLayer = 0;
  bufferImageCopy.imageSubresource.layerCount = 1;
  bufferImageCopy.imageExtent.width = textureWidth;
  bufferImageCopy.imageExtent.height = textureHeight;
  bufferImageCopy.imageExtent.depth = 1;
  bufferImageCopy.bufferOffset = 0;

  vkCmdCopyBufferToImage(cmd, stagingBuffer, textureImage,
                         VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                         1, &bufferImageCopy);

  imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
  imageMemoryBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
  imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
  imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;

  vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT,
                       VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr,
                       0, nullptr, 1, &imageMemoryBarrier);

  vkEndCommandBuffer(cmd);

  VkSubmitInfo submitInfo{};
  submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
  submitInfo.commandBufferCount = 1;
  submitInfo.pCommandBuffers = &cmd;

  VK_CHECK(vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE));
  vkQueueWaitIdle(graphicsQueue);
}

Tiếp theo, hãy tạo VkImageView và VkSampler mà chương trình đổ bóng mảnh có thể dùng để lấy mẫu màu cho từng pixel đã kết xuất.

VkImageView là một trình bao bọc ở trên VkImage. Trình bao bọc này lưu trữ thông tin về cách diễn giải dữ liệu của hoạ tiết, chẳng hạn như nếu bạn chỉ muốn truy cập vào một vùng hoặc lớp và nếu bạn muốn xáo trộn các kênh pixel theo một cách cụ thể.

VkSampler lưu trữ dữ liệu để chương trình đổ bóng cụ thể truy cập vào hoạ tiết. Đối tượng này lưu trữ thông tin về cách kết hợp các pixel hoặc cách thực hiện mipmap. Các trình lấy mẫu được dùng cùng với VkImageView trong dữ liệu đặc tả.

// CODELAB: hellovk.h
void HelloVK::createTextureImageViews() {
  VkImageViewCreateInfo createInfo{};
  createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
  createInfo.image = textureImage;
  createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
  createInfo.format = VK_FORMAT_R8G8B8_UNORM;
  createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
  createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
  createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
  createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
  createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
  createInfo.subresourceRange.baseMipLevel = 0;
  createInfo.subresourceRange.levelCount = 1;
  createInfo.subresourceRange.baseArrayLayer = 0;
  createInfo.subresourceRange.layerCount = 1;

  VK_CHECK(vkCreateImageView(device, &createInfo, nullptr, &textureImageView));
}

Chúng ta cũng cần tạo một Trình lấy mẫu để truyền đến chương trình đổ bóng

// CODELAB: hellovk.h
void HelloVK::createTextureSampler() {
  VkSamplerCreateInfo createInfo{};
  createInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
  createInfo.magFilter = VK_FILTER_LINEAR;
  createInfo.minFilter = VK_FILTER_LINEAR;
  createInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
  createInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
  createInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
  createInfo.anisotropyEnable = VK_FALSE;
  createInfo.maxAnisotropy = 16;
  createInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
  createInfo.unnormalizedCoordinates = VK_FALSE;
  createInfo.compareEnable = VK_FALSE;
  createInfo.compareOp = VK_COMPARE_OP_ALWAYS;
  createInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
  createInfo.mipLodBias = 0.0f;
  createInfo.minLod = 0.0f;
  createInfo.maxLod = VK_LOD_CLAMP_NONE;

  VK_CHECK(vkCreateSampler(device, &createInfo, nullptr, &textureSampler));
}

Cuối cùng, hãy sửa đổi chương trình đổ bóng để lấy mẫu hình ảnh thay vì dùng màu sắc của đỉnh. Toạ độ của hoạ tiết là vị trí dấu phẩy động ánh xạ các vị trí trên một hoạ tiết đến các vị trí trên một nền tảng hình học. Trong ví dụ của chúng tôi, quy trình này được hoàn tất bằng cách xác định vTexCoords làm dữ liệu đầu ra của chương trình đổ bóng đỉnh. Chúng tôi điền trực tiếp chương trình này bằng texCoords của đỉnh vì đã có một hình tam giác được chuẩn hoá (kích thước {1, 1}).

// CODELAB: shader.vert
#version 450

// Uniform buffer containing an MVP matrix.
// Currently the vulkan backend only sets the rotation matrix
// required to handle device rotation.
layout(binding = 0) uniform UniformBufferObject {
    mat4 MVP;
} ubo;

vec2 positions[3] = vec2[](
    vec2(0.0, 0.577),
    vec2(-0.5, -0.289),
    vec2(0.5, -0.289)
);

vec2 texCoords[3] = vec2[](
    vec2(0.5, 1.0),
    vec2(0.0, 0.0),
    vec2(1.0, 0.0)
);

layout(location = 0) out vec2 vTexCoords;

void main() {
    gl_Position = ubo.MVP * vec4(positions[gl_VertexIndex], 0.0, 1.0);
    vTexCoords = texCoords[gl_VertexIndex];
}

Đổ bóng mảnh bằng Trình lấy mẫu và hoạ tiết.

// CODELAB: shader.frag
#version 450

layout(location = 0) in vec2 vTexCoords;

layout(binding = 1) uniform sampler2D samp;

// Output colour for the fragment
layout(location = 0) out vec4 outColor;

void main() {
    outColor = texture(samp, vTexCoords);
}

Ở cuối bước này, bạn sẽ thấy hình tam giác xoay đã có hoạ tiết!

b3426db4d6e94e89.gif

Kiểm tra nhằm đảm bảo đây là trường hợp đúng và nếu xảy ra sự cố, bạn có thể so sánh công việc của mình với cam kết của kho lưu trữ có tiêu đề [codelab] step: apply texture.

11. Thêm lớp xác thực

Lớp xác thực là các thành phần không bắt buộc kết nối với các lệnh gọi hàm của Vulkan để áp dụng những thao tác khác như:

  1. Xác thực giá trị của tham số để phát hiện hành vi sử dụng sai mục đích
  2. Theo dõi việc tạo và huỷ bỏ đối tượng để tìm sự cố rò rỉ tài nguyên
  3. Kiểm tra mức độ an toàn của luồng
  4. Ghi nhật ký lệnh gọi cho việc phân tích và phát lại

Vì lớp xác thực này là tệp tải xuống có thể điều chỉnh kích thước, chúng tôi đã chọn không truyền chúng vào APK. Vì vậy, để bật lớp xác thực, vui lòng làm theo các bước đơn giản bên dưới:

Tải các tệp nhị phân mới nhất của Android xuống từ: https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases

Đặt các tệp này vào thư mục ABI tương ứng trong: app/src/main/jniLibs

Làm theo các bước bên dưới để bật lớp xác thực

// CODELAB: hellovk.h
void HelloVK::createInstance() {
  assert(!enableValidationLayers ||
         checkValidationLayerSupport());  // validation layers requested, but not available!
  auto requiredExtensions = getRequiredExtensions(enableValidationLayers);

  VkApplicationInfo appInfo{};
  appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
  appInfo.pApplicationName = "Hello Triangle";
  appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
  appInfo.pEngineName = "No Engine";
  appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
  appInfo.apiVersion = VK_API_VERSION_1_0;

  VkInstanceCreateInfo createInfo{};
  createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
  createInfo.pApplicationInfo = &appInfo;
  createInfo.enabledExtensionCount = (uint32_t)requiredExtensions.size();
  createInfo.ppEnabledExtensionNames = requiredExtensions.data();
  createInfo.pApplicationInfo = &appInfo;

  if (enableValidationLayers) {
    VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{};
    createInfo.enabledLayerCount =
        static_cast<uint32_t>(validationLayers.size());
    createInfo.ppEnabledLayerNames = validationLayers.data();
    populateDebugMessengerCreateInfo(debugCreateInfo);
    createInfo.pNext = (VkDebugUtilsMessengerCreateInfoEXT *)&debugCreateInfo;
  } else {
    createInfo.enabledLayerCount = 0;
    createInfo.pNext = nullptr;
  }

  VK_CHECK(vkCreateInstance(&createInfo, nullptr, &instance));

  uint32_t extensionCount = 0;
  vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr);
  std::vector<VkExtensionProperties> extensions(extensionCount);
  vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount,
                                         extensions.data());
  LOGI("available extensions");
  for (const auto &extension : extensions) {
    LOGI("\t %s", extension.extensionName);
  }
}

12. Xin chúc mừng

Xin chúc mừng! Bạn đã thiết lập thành công quy trình kết xuất trong Vulkan và sẵn sàng phát triển trò chơi của mình!

Hãy chú ý theo dõi vì chúng tôi sẽ bổ sung thêm nhiều tính năng từ Vulkan cho Android.

Để biết thêm thông tin về cách làm quen với Vulkan trên Android, hãy đọc bài viết Làm quen với Vulkan trên Android.