Mulai menggunakan Vulkan di Android

1. Pengantar

Mengapa perlu menggunakan Vulkan dalam game saya?

Vulkan adalah API grafis tingkat rendah utama di Android. Vulkan memungkinkan performa yang lebih tinggi untuk game yang mengimplementasikan game engine dan perendernya sendiri.

Vulkan tersedia di Android mulai Android 7.0 (level API 24). Mulai Android 10.0, dukungan Vulkan 1.1 merupakan persyaratan untuk perangkat Android 64-bit baru. Profil Dasar Pengukuran Android 2022 juga menetapkan versi Vulkan API minimum 1.1.

Game yang memiliki banyak panggilan gambar dan yang menggunakan OpenGL ES dapat mengalami overhead driver yang signifikan karena tingginya biaya pembuatan panggilan gambar dalam OpenGL ES. Game ini dapat terikat dengan CPU karena menghabiskan sebagian besar waktu render frame-nya dalam driver grafis. Game tersebut juga dapat mengalami pengurangan penggunaan CPU dan daya yang signifikan dengan beralih dari OpenGL ES ke Vulkan. Hal ini terutama berlaku jika game memiliki scene kompleks yang tidak dapat menggunakan instance secara efektif untuk mengurangi panggilan gambar.

Yang akan Anda bangun

Dalam codelab ini, Anda akan menggunakan Aplikasi Android C++ dasar dan menambahkan kode untuk menyiapkan pipeline rendering Vulkan. Kemudian, Anda akan mengimplementasikan kode yang menggunakan Vulkan untuk merender segitiga berputar yang bertekstur di layar.

Yang akan Anda butuhkan

2. Mempersiapkan

Menyiapkan lingkungan pengembangan Anda

Jika belum pernah menggunakan project native di Android Studio, Anda mungkin perlu menginstal Android NDK dan CMake. Jika Anda sudah menginstalnya, lanjutkan ke Menyiapkan project.

Memastikan SDK, NDK, dan CMake telah diinstal

Luncurkan Android Studio. Saat jendela Welcome to Android Studio ditampilkan, buka menu dropdown Konfigurasi lalu pilih opsi SDK Manager.

3b7b47a139bc456.png

Jika sudah membuka project, Anda dapat membuka SDK Manager melalui menu Alat. Klik menu Tools lalu pilih SDK Manager. Jendela SDK Manager akan terbuka.

Pada sidebar, pilih secara berurutan: Appearance & Behavior > System Settings > Android SDK. Pilih tab SDK Platforms di panel Android SDK untuk menampilkan daftar opsi alat yang terinstal. Pastikan Android SDK 12.0 atau yang lebih baru telah diinstal.

931f6ae02822f417.png

Selanjutnya, pilih tab SDK Tools, lalu pastikan NDK dan CMake telah diinstal.

Catatan: Versi tepatnya tidak terlalu penting selama versi tersebut cukup baru, tetapi saat ini kita menggunakan NDK 26.1.10909125 dan CMake 3.22.1. Seiring waktu, versi NDK yang terinstal akan berubah secara default dengan rilis NDK berikutnya. Jika Anda perlu menginstal versi NDK tertentu, ikuti petunjuk dalam referensi Android Studio untuk menginstal NDK di bagian "Menginstal versi NDK tertentu".

d28adf9279adec4.png

Setelah semua alat yang diperlukan dicentang, klik tombol Apply di bagian bawah jendela untuk menginstalnya. Anda dapat menutup jendela Android SDK dengan mengklik tombol OK.

Menyiapkan project

Project awal yang berasal dari template C++ telah disiapkan untuk Anda di repositori git. Project awal mengimplementasikan inisialisasi aplikasi dan penanganan peristiwa, tetapi belum melakukan penyiapan atau rendering grafis apa pun.

Meng-clone repo

Dari command line, ubah ke direktori yang ingin Anda sertakan direktori proyek root-nya, lalu clone dari GitHub:

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

Pastikan Anda memulai dari commit awal repo yang bernama [codelab] start: empty app.

Buka project dengan Android Studio, buat project, lalu jalankan di perangkat yang terpasang. Project akan diluncurkan dengan layar hitam kosong, Anda akan menambahkan rendering grafis di bagian berikutnya.

3. Membuat instance dan perangkat Vulkan

Langkah pertama untuk menginisialisasi penggunaan Vulkan API adalah membuat objek instance Vulkan (VkInstance).

Objek VkInstance mewakili instance aplikasi runtime Vulkan. Objek ini adalah objek root Vulkan API dan digunakan untuk mengambil informasi dan membuat instance objek perangkat Vulkan serta lapisan apa pun yang ingin diaktifkan.

Saat membuat VkInstance, aplikasi harus memberikan informasi tentang aplikasi itu sendiri, seperti nama, versi, dan ekstensi instance Vulkan yang diperlukan.

Desain Vulkan API mencakup sistem lapisan yang menyediakan mekanisme untuk menangkap dan memproses panggilan API sebelum mencapai driver GPU. Aplikasi dapat menetapkan lapisan yang akan diaktifkan saat membuat VkInstance. Lapisan yang paling umum digunakan adalah lapisan validasi Vulkan, yang menyediakan analisis runtime penggunaan API untuk error atau praktik performa yang kurang optimal.

Setelah VkInstance dibuat, aplikasi dapat menggunakannya untuk membuat kueri ke perangkat fisik yang tersedia di sistem, membuat perangkat logis, dan membuat tujuan render platform.

VkInstance biasanya dibuat satu kali di awal aplikasi dan dihancurkan di akhir. Namun, beberapa VkInstance dapat dibuat dalam aplikasi yang sama, misalnya jika aplikasi perlu menggunakan beberapa GPU untuk membuat beberapa jendela.

// 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 adalah objek Vulkan yang merepresentasikan perangkat Vulkan fisik di sistem. Sebagian besar perangkat Android hanya akan menampilkan satu VkPhysicalDevice yang merepresentasikan GPU. Namun, PC atau perangkat Android dapat mengenumerasi beberapa perangkat fisik. Misalnya, komputer yang memiliki GPU diskret dan GPU terintegrasi.

Properti VkPhysicalDevice dapat dikueri, seperti nama, vendor, versi driver, dan fitur yang didukung. Informasi ini dapat digunakan untuk memilih perangkat fisik terbaik bagi aplikasi tertentu.

Setelah VkPhysicalDevice dipilih, aplikasi dapat membuat perangkat logis dari objek tersebut. Perangkat logis adalah representasi perangkat fisik yang spesifik untuk aplikasi tersebut. Perangkat ini memiliki status dan resource sendiri dan terpisah dari perangkat logis lain yang mungkin dibuat dari perangkat fisik yang sama.

Ada beragam jenis antrean yang berasal dari berbagai Kelompok Antrean dan setiap kelompok antrean hanya mengizinkan satu subset perintah. Misalnya, mungkin ada kelompok antrean yang hanya mengizinkan pemrosesan perintah komputasi atau yang hanya mengizinkan perintah terkait transfer memori.

VkPhysicalDevice dapat mengenumerasi semua jenis Kelompok Antrean yang tersedia. Kita hanya melihat antrean grafis di sini, tetapi mungkin juga ada antrean tambahan yang hanya mendukung COMPUTE atau TRANSFER. Kelompok Antrean tidak memiliki jenisnya sendiri. Sebagai gantinya, Kelompok Antrean diwakili oleh jenis indeks numerik uint32_t di dalam objek induknya (VkPhysicalDevice).

VkPhysicalDevice dapat membuat beberapa perangkat logis. Hal ini berguna untuk aplikasi yang perlu menggunakan beberapa GPU atau membuat beberapa jendela.

VkDevice adalah objek Vulkan yang mewakili perangkat Vulkan logis. Objek ini adalah abstraksi tipis pada perangkat fisik, dan menyediakan semua fungsi yang diperlukan untuk membuat dan mengelola resource Vulkan, seperti buffer, gambar, dan shader.

VkDevice dibuat dari VkPhysicalDevice dan spesifik untuk aplikasi yang membuatnya. Perangkat ini memiliki status dan resource sendiri dan terpisah dari perangkat logis lain yang mungkin dibuat dari perangkat fisik yang sama.

Objek VkSurfaceKHR merepresentasikan platform yang dapat menjadi target operasi rendering. Untuk menampilkan grafis pada layar perangkat, Anda akan membuat platform menggunakan referensi ke objek jendela aplikasi. Setelah objek VkSurfaceKHR dibuat, aplikasi dapat menggunakannya untuk membuat objek VkSwapchainKHR.

Objek VkSwapchainKHR merepresentasikan infrastruktur yang memiliki buffer yang menjadi tujuan render sebelum memvisualisasikannya di layar. Pada dasarnya, objek ini adalah antrean gambar yang menunggu untuk ditampilkan ke layar. Kita akan memperoleh gambar tersebut untuk digambar, kemudian mengembalikannya ke antrean. Cara kerja antrean dan kondisi untuk menampilkan gambar dari antrean bergantung pada konfigurasi swap chain, tetapi tujuan umum swap chain adalah menyinkronkan tampilan gambar dengan kecepatan refresh layar.

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

Anda dapat menyiapkan dukungan lapisan validasi jika perlu men-debug aplikasi. Anda juga dapat memeriksa ekstensi tertentu yang mungkin diperlukan game.

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

Setelah menemukan penyiapan yang sesuai dan membuat VkInstance, buat VkSurface yang merepresentasikan jendela yang akan dirender.

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

Enumerasi perangkat fisik (GPU) yang tersedia dan pilih perangkat pertama yang sesuai yang tersedia.

// 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!
}

Untuk memeriksa apakah perangkat sesuai, kita perlu menemukan perangkat yang mendukung antrean 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;
}

Setelah mengetahui PhysicalDevice yang akan digunakan, buat perangkat logis (dikenal sebagai VkDevice). Perangkat ini mewakili perangkat Vulkan yang diinisialisasi dan siap membuat semua objek lain untuk digunakan oleh aplikasi Anda.

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

Di akhir langkah ini, Anda hanya dapat melihat jendela hitam tanpa rendering apa pun karena proses penyiapan masih berlangsung. Jika terjadi error, Anda dapat membandingkan pekerjaan Anda dengan commit repo yang bernama [codelab] step: create instance and device.

4. Membuat Swapchain dan objek sinkronisasi

VkSwapchain adalah objek Vulkan yang merepresentasikan antrean gambar yang dapat ditampilkan ke layar. Objek ini digunakan untuk menerapkan buffering ganda atau buffering tiga kali, yang dapat mengurangi tearing dan meningkatkan performa.

Untuk membuat VkSwapchain, aplikasi harus membuat objek VkSurfaceKHR terlebih dahulu. Kita telah membuat objek VkSurfaceKHR ketika menyiapkan jendela pada langkah pembuatan instance.

Akan ada banyak gambar terkait di objek VkSwapchainKHR. Gambar tersebut digunakan untuk menyimpan scene yang dirender. Aplikasi dapat memperoleh gambar dari objek VkSwapchainKHR, merendernya, dan kemudian menampilkannya ke layar.

Setelah ditampilkan ke layar, gambar tidak lagi tersedia untuk aplikasi. Aplikasi harus memperoleh gambar lain dari objek VkSwapchainKHR sebelum dapat merender lagi.

VkSwapchain biasanya dibuat satu kali di awal aplikasi dan dihancurkan di akhir. Namun, beberapa VkSwapchain dapat dibuat dan dihancurkan dalam aplikasi yang sama, misalnya jika aplikasi perlu menggunakan beberapa GPU untuk membuat beberapa jendela.

Objek sinkronisasi adalah objek yang digunakan untuk sinkronisasi. Vulkan memiliki VkFence, VkSemaphore, dan VkEvent yang digunakan untuk mengontrol akses resource di beberapa antrean. Objek tersebut diperlukan jika Anda menggunakan beberapa antrean dan render pass, tetapi contoh sederhana seperti ini tidak memerlukannya.

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

Anda juga perlu menyiapkan pembuatan ulang swap chain setelah perangkat kehilangan konteks. Misalnya, saat pengguna beralih ke aplikasi lainnya.

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

Di akhir langkah ini, Anda hanya dapat melihat jendela hitam tanpa rendering apa pun karena proses penyiapan masih berlangsung. Jika terjadi error, Anda dapat membandingkan pekerjaan Anda dengan commit repo yang bernama [codelab] step: create swapchain and sync objects.

5. Membuat Renderpass and Framebuffer

VkImageView adalah objek Vulkan yang menjelaskan cara mengakses VkImage. Objek ini menentukan rentang subresource gambar yang akan diakses, format piksel yang akan digunakan, dan swizzle yang akan diterapkan pada saluran.

VkRenderPass adalah objek Vulkan yang menjelaskan cara GPU merender scene. Objek ini menentukan lampiran yang akan digunakan, urutan rendering lampiran, dan cara penggunaan lampiran di setiap tahap pipeline rendering.

VkFramebuffer adalah objek Vulkan yang merepresentasikan serangkaian tampilan gambar yang akan digunakan sebagai lampiran selama eksekusi render pass. Dengan kata lain, objek ini mengikat lampiran gambar sebenarnya ke render pass.

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

Lampiran di Vulkan biasanya dikenal sebagai target render, yang merupakan gambar yang digunakan sebagai output rendering. Hanya format yang perlu dideskripsikan di sini, misalnya, render pass dapat menghasilkan output format warna atau format stensil kedalaman tertentu. Anda juga perlu menentukan apakah konten lampiran harus disimpan, dibuang, atau dihapus di awal pass.

// 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 merepresentasikan link ke gambar sebenarnya yang dapat digunakan untuk lampiran (target render). Buat objek Framebuffer dengan menentukan renderpass dan set 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]));
  }
}

Di akhir langkah ini, Anda hanya dapat melihat jendela hitam tanpa rendering apa pun karena proses penyiapan masih berlangsung. Jika terjadi error, Anda dapat membandingkan pekerjaan Anda dengan commit repo yang bernama [codelab] step: create renderpass and framebuffer.

6. Membuat Shader dan Pipeline

VkShaderModule adalah objek Vulkan yang merepresentasikan shader yang dapat diprogram. Shader digunakan untuk melakukan berbagai operasi pada data grafis, seperti mengubah verteks, membuat shade piksel, dan menghitung efek global.

VkPipeline adalah objek Vulkan yang merepresentasikan pipeline grafis yang dapat diprogram. Objek ini adalah sekumpulan objek status yang menjelaskan cara GPU harus merender scene.

VkDescriptorSetLayout adalah template untuk VkDescriptorSet, yang merupakan sekelompok deskriptor. Deskriptor adalah handle yang memungkinkan shader mengakses resource (seperti Buffer, Gambar, atau Sampler).

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

Tentukan fungsi createShaderModule untuk memuat shader ke objek 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;
}

Membuat pipeline grafis yang memuat shader verteks dan fragmen sederhana.

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

Di akhir langkah ini, Anda hanya dapat melihat jendela hitam tanpa rendering apa pun karena proses penyiapan masih berlangsung. Jika terjadi error, Anda dapat membandingkan pekerjaan Anda dengan commit repo yang bernama [codelab] step: create shader and pipeline.

7. DescriptorSet dan Buffer Seragam

VkDescriptorSet adalah objek Vulkan yang merepresentasikan kumpulan resource deskriptor. Resource deskriptor digunakan untuk menyediakan input shader, seperti buffer seragam, sampler gambar, dan buffer penyimpanan. Untuk membuat VkDescriptorSet, kita perlu membuat VkDescriptorPool.

VkBuffer adalah buffer memori yang digunakan untuk berbagi data antara GPU dan CPU. Saat digunakan sebagai Buffer seragam, buffer meneruskan data ke shader sebagai variabel seragam. Variabel seragam adalah konstanta yang dapat diakses oleh semua shader di pipeline.

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

Buat VkDescriptorSet yang dialokasikan dari VkDescriptorPool yang ditentukan.

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

Tentukan struct Buffer Seragam dan buat buffer seragam. Ingat untuk mengalokasikan memori dari VkDeviceMemory menggunakan vkAllocationMemory dan mengikat buffer ke memori menggunakan 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);
}

Fungsi bantuan untuk menemukan jenis memori yang tepat.

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

Di akhir langkah ini, Anda hanya dapat melihat jendela hitam tanpa rendering apa pun karena proses penyiapan masih berlangsung. Jika terjadi error, Anda dapat membandingkan pekerjaan Anda dengan commit repo yang bernama [codelab] step: descriptorset and uniform buffer.

8. Buffer Perintah - buat, catat, dan Gambar

VkCommandPool adalah objek sederhana yang digunakan untuk mengalokasikan CommandBuffer. Objek ini terhubung ke Kelompok Antrean tertentu.

VkCommandBuffer adalah objek Vulkan yang merepresentasikan daftar perintah yang akan dieksekusi GPU. Objek ini adalah objek tingkat rendah yang memberikan kontrol terperinci atas 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()));
}

Di akhir langkah ini, Anda hanya dapat melihat jendela hitam tanpa rendering apa pun karena proses penyiapan masih berlangsung. Jika terjadi error, Anda dapat membandingkan pekerjaan Anda dengan commit repo yang bernama [codelab] step: create command pool and command buffer.

Memperbarui buffer seragam, merekam buffer perintah & menggambar

Perintah di Vulkan, seperti operasi menggambar dan transfer memori, tidak dijalankan secara langsung menggunakan panggilan fungsi. Sebaliknya, semua operasi yang tertunda dicatat dalam objek buffer perintah. Keuntungannya adalah ketika kita siap memberi tahu Vulkan tindakan yang ingin kita lakukan, semua perintah dikirimkan bersama-sama dan Vulkan dapat memproses perintah secara lebih efisien karena semuanya tersedia bersama-sama. Selain itu, hal ini memungkinkan perekaman perintah di beberapa thread jika diinginkan.

Di Vulkan, semua rendering terjadi di dalam RenderPass. Dalam contoh kita, RenderPass akan merender ke FrameBuffer yang sudah disiapkan sebelumnya.

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

Anda mungkin juga perlu memperbarui Buffer Seragam karena kita menggunakan matriks transformasi yang sama untuk semua verteks yang dirender.

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

Sekarang saatnya merender! Dapatkan buffer perintah yang Anda susun dan kirim ke antrean.

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

Tangani Perubahan Orientasi dengan membuat ulang swap chain.

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

Integrasikan ke siklus proses aplikasi.

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

Di akhir langkah ini, Anda akan melihat segitiga berwarna di layar!

b07da8354cdd1629.png

Periksa apakah gambar sudah benar, dan jika ada yang salah, Anda bisa membandingkan pekerjaan Anda dengan commit repo bernama [codelab] step: update uniform buffer, record command buffer and draw.

9. Memutar segitiga

Untuk memutar segitiga, kita perlu menerapkan rotasi ke matriks MVP sebelum meneruskan matriks ke shader. Hal ini dilakukan untuk mencegah duplikasi penghitungan matriks yang sama untuk setiap verteks dalam model.

Untuk menghitung matriks MVP di sisi aplikasi, matriks transformasi rotasi diperlukan. Library GLM adalah library matematika C++ untuk menulis software grafis berdasarkan spesifikasi GLSL dan memiliki fungsi putar yang diperlukan untuk membuat matriks dengan menerapkan rotasi.

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

Di akhir langkah ini, Anda akan melihat segitiga berputar di layar! Periksa apakah sudah benar, dan jika ada yang salah, Anda bisa membandingkan pekerjaan Anda dengan commit repo bernama [codelab] step: rotate triangle.

10. Menerapkan Tekstur

Untuk menerapkan tekstur pada segitiga, file gambar perlu dimuat terlebih dahulu dalam format tidak terkompresi di memori. Langkah ini menggunakan library gambar stb untuk memuat dan mendekode data gambar ke RAM yang kemudian disalin ke buffer 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);
}

Berikutnya, buat VkImage dari VkBuffer yang diisi dengan data gambar dari langkah sebelumnya.

VkImage adalah objek yang menyimpan data tekstur sebenarnya. Objek ini menyimpan data piksel ke dalam memori utama tekstur, tetapi tidak berisi banyak informasi tentang cara membacanya. Itulah sebabnya kita perlu membuat VkImageView di bagian berikutnya.

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

Berikutnya, buat VkImageView dan VkSampler yang dapat digunakan oleh shader fragmen untuk mengambil sampel warna setiap piksel yang dirender.

VkImageView adalah wrapper di atas VkImage. Wrapper ini menyimpan informasi cara menafsirkan data tekstur, misalnya, jika Anda hanya ingin mengakses suatu wilayah atau lapisan, dan jika Anda ingin mengacak saluran piksel dengan cara tertentu.

VkSampler menyimpan data untuk akses shader tertentu ke tekstur. Sampler ini menyimpan informasi cara menggabungkan piksel atau cara melakukan mipmap. Sampler digunakan dengan VkImageView di deskriptor.

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

Kita juga perlu membuat Sampler untuk diteruskan ke shader.

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

Terakhir, kita perlu memodifikasi shader untuk mengambil sampel gambar, bukan menggunakan warna verteks. Koordinat tekstur adalah posisi floating point yang memetakan lokasi pada tekstur ke lokasi pada permukaan geometris. Dalam contoh kita, proses ini diselesaikan dengan mendefinisikan vTexCoords sebagai output shader verteks yang akan diisi dengan texCoords verteks secara langsung karena kita memiliki segitiga yang dinormalisasi (berukuran {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];
}

Shader fragmen menggunakan Sampler dan tekstur.

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

Di akhir langkah ini, Anda akan melihat segitiga berputar memiliki tekstur!

b3426db4d6e94e89.gif

Periksa apakah sudah benar, dan jika ada yang salah, Anda bisa membandingkan pekerjaan Anda dengan commit repo bernama [codelab] step: apply texture.

11. Menambahkan Lapisan Validasi

Lapisan validasi adalah komponen opsional yang terkait ke panggilan fungsi Vulkan untuk menerapkan operasi tambahan seperti:

  1. Memvalidasi nilai parameter untuk mendeteksi penyalahgunaan
  2. Melacak pembuatan dan penghancuran objek untuk menemukan kebocoran resource
  3. Memeriksa keamanan thread
  4. Logging panggilan untuk profiling dan pemutaran ulang

Karena lapisan validasi merupakan download yang cukup besar, kami memilih untuk tidak mengirimkannya dalam APK. Oleh karena itu, untuk mengaktifkan lapisan validasi, ikuti langkah-langkah mudah di bawah ini:

Download biner terbaru Android dari: https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases

Tempatkan biner di folder ABI masing-masing yang berada di: app/src/main/jniLibs

Ikuti langkah-langkah di bawah untuk mengaktifkan lapisan validasi

// 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. Selamat

Selamat, Anda berhasil menyiapkan pipeline rendering Vulkan dan siap mengembangkan game Anda!

Nantikan kabar terbaru karena kami akan menambahkan lebih banyak fitur dari Vulkan untuk Android.

Untuk informasi selengkapnya tentang mulai menggunakan Vulkan di Android, baca Mulai menggunakan Vulkan di Android.