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ột 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