Android での Vulkan のスタートガイド

1. はじめに

ゲームで Vulkan を使用する理由

Vulkan は、Android のメインの低レベル グラフィック API です。Vulkan は、独自のゲームエンジンとレンダラを実装するゲームが高いパフォーマンスを発揮できるようにします。

Vulkan は Android 7.0(API レベル 24)以降の Android で利用できます。注: Android 10.0 以降を搭載した新しい 64 ビットの Android デバイスでは、Vulkan 1.1 のサポートが必須となっています。2022 Android ベースライン プロファイルでも、Vulkan API の最小バージョンとして 1.1 が設定されています。

描画呼び出しが多く、OpenGL ES を使用するゲームでは、OpenGL ES 内での描画呼び出しのコストが高いため、ドライバのオーバーヘッドが著しく増加する可能性があります。これらのゲームは、フレーム時間の大半をグラフィックス ドライバに費やすため、CPU バウンドになる可能性があります。また、OpenGL ES から Vulkan に切り替えると CPU と電力の使用を大幅に削減できる可能性もあります。これは、描画呼び出しを削減するためにインスタンス化を効率よく使用できない複雑なシーンがゲームにある場合に特に当てはまります。

作成するアプリの概要

この Codelab では、基本的な C++ Android アプリを使い、コードを追加して Vulkan レンダリング パイプラインをセットアップします。それから、テクスチャが適用された回転する三角形を Vulkan を使って画面にレンダリングするコードを実装します。

必要なもの

2. セットアップする

開発環境をセットアップする

Android Studio でネイティブ プロジェクトを扱ったことがない場合は、Android NDK と CMake のインストールが必要な場合があります。すでにインストールされている場合は、「プロジェクトをセットアップする」に進みます。

SDK、NDK、CMake がインストールされていることを確認する

Android Studio を起動します。[Welcome to Android Studio] ウィンドウが表示されたら、[Configure] プルダウン メニューを開き、[SDK Manager] オプションを選択します。

3b7b47a139bc456.png

既存のプロジェクトをすでに開いている場合は、代わりに [Tools] メニューから [SDK Manager] を開けます。[Tools] メニューをクリックして、[SDK Manager] を選択すると、[SDK Manager] ウィンドウが開きます。

サイドバーで、[Appearance & Behavior] > [System Settings] > [Android SDK] の順に選択します。[Android SDK] ペインで [SDK Platforms] タブを選択すると、インストール済みのツール オプションのリストが表示されます。Android SDK 12.0 以降がインストールされていることを確認します。

931f6ae02822f417.png

次に、[SDK Tools] タブを選択し、[NDK] と [CMake] がインストールされていることを確認します。

注: バージョンが比較的新しいものであれば具体的なバージョンは問いません。現在のバージョンは NDK 26.1.10909125 と CMake 3.22.1 です。デフォルトでインストールされる NDK のバージョンは、今後の NDK リリースに合わせて順次変更されます。特定のバージョンの NDK をインストールする必要がある場合は、Android Studio リファレンスの特定バージョンの NDK をインストールするセクションの下にある NDK のインストールの手順に沿って操作してください。

d28adf9279adec4.png

必要なツールのチェックボックスをすべてオンにしたら、ウィンドウの下にある [Apply] ボタンをクリックしてインストールします。その後 [OK] ボタンをクリックして、[Android SDK] ウィンドウを閉じます。

プロジェクトをセットアップする

git リポジトリには C++ テンプレートから派生したプロジェクトが出発点として設定されています。出発点となるプロジェクトは、アプリの初期化とイベントの処理を実装しますが、グラフィックのセットアップやレンダリングはまだ行っていません。

リポジトリのクローンを作成する

コマンドラインから、ルート プロジェクト ディレクトリを含めるディレクトリに変更し、GitHub からクローンを作成します。

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

[codelab] start: empty app というリポジトリの最初の commit から開始していることを確認します。

プロジェクトを Android Studio で開いてビルドし、接続されたデバイスで実行します。プロジェクトが起動して、何もない黒い画面が表示されます。以降のセクションで、グラフィックのレンダリングを追加していきます。

3. Vulkan のインスタンスとデバイスを作成する

Vulkan API を使用するために初期化する最初の手順は、Vulkan インスタンス オブジェクト(VkInstance)の作成です。

VkInstance オブジェクトは、アプリケーションの Vulkan Runtime のインスタンスを表します。これは Vulkan API のルート オブジェクトです。アクティブにする Vulkan デバイス オブジェクトとレイヤの情報を取得して、インスタンス化するために使用されます。

アプリケーションが VkInstance を作成する際には、アプリケーション自身についての情報を提供する必要があります。名前、バージョン、必要な Vulkan インスタンス拡張機能などがこれに該当します。

Vulkan API の設計にはレイヤシステムが含まれています。このシステムは、API 呼び出しが GPU ドライバに到達する前にインターセプトして処理するメカニズムを提供します。アプリケーションは、VkInstance を作成する際に、アクティブにするレイヤを指定できます。最もよく使われるレイヤは Vulkan 検証レイヤです。このレイヤは API の使用についてランタイム分析を行い、エラーやパフォーマンスの低下につながる手法を見つけます。

VkInstance が作成されると、アプリケーションはそれを使用して、システムで利用可能な物理デバイスへのクエリ、論理デバイスの作成、レンダリング先となるサーフェスの作成を行うことができます。

通常はアプリケーションの開始時に VkInstance が 1 つ作成され、終了時に破棄されます。ただし、同じアプリケーション内で複数の VkInstance を作成することもできます。アプリケーションが複数の GPU を使用する必要がある場合や、複数のウィンドウを作成する必要がある場合がこれに該当します。

// 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 は、システム上の物理 Vulkan デバイスを表す Vulkan オブジェクトです。ほとんどの Android デバイスは、GPU を表す VkPhysicalDevice を 1 つしか返しません。ただし、PC または Android デバイスが複数の物理デバイスを列挙することもあります。たとえば、1 台のパソコンにディスクリート GPU とインテグレーテッド GPU の両方がある場合があります。

VkPhysicalDevice は、名前、ベンダー、ドライバのバージョン、サポートする機能などのプロパティを対象にクエリできます。特定のアプリケーションに最も適した物理デバイスを選択するためにこの情報を利用できます。

VkPhysicalDevice を選択したら、アプリケーションはそこから論理デバイスを作成できます。論理デバイスは、アプリケーション固有の物理デバイスを表します。論理デバイスには独自の状態とリソースがあり、同じ物理デバイスから作成された論理デバイスが他にあったとしても、その論理デバイスから独立しています。

各種のキュー ファミリーに基づくさまざまな種類のキューがあり、キューのファミリーごとに決められた一部のコマンドのみを許可します。たとえば、計算処理のコマンドのみを許可するキュー ファミリーや、メモリ転送関連のコマンドのみを許可するキュー ファミリーが考えられます。

VkPhysicalDevice は、利用できるすべての種類のキュー ファミリーを列挙できます。ここではグラフィックのキューについてのみ扱いますが、COMPUTE または TRANSFER のみをサポートするキューがある可能性もあります。キュー ファミリーには独自のタイプはありません。代わりに、親オブジェクト(VkPhysicalDevice)内では数値インデックス タイプ uint32_t で表されます。

VkPhysicalDevice は、複数の論理デバイスを作成して持つことができます。これはアプリケーションが複数の GPU を使用する必要がある場合や、複数のウィンドウを作成する必要がある場合に役立ちます。

VkDevice は、論理 Vulkan デバイスを表す Vulkan オブジェクトです。物理デバイスを抽象化して、バッファ、イメージ、シェーダーなど、Vulkan リソースの作成、管理に必要な機能すべてを提供します。

VkDevice は VkPhysicalDevice から作成され、自身を作成したアプリケーション固有のものとなります。論理デバイスには独自の状態とリソースがあり、同じ物理デバイスから作成された論理デバイスが他にあったとしても、その論理デバイスから独立しています。

VkSurfaceKHR オブジェクトは、レンダリング処理のターゲットにできるサーフェスを表します。デバイスの画面にグラフィックを表示するために、アプリケーション ウィンドウ オブジェクトへの参照を使用してサーフェスを作成します。VkSurfaceKHR オブジェクトを作成したら、アプリケーションがそれを使用して VkSwapchainKHR オブジェクトを作成できます。

VkSwapchainKHR オブジェクトは、画面上で視覚化できるようになる前にレンダリングを行うバッファを持つ、インフラストラクチャを表します。これは実質的には、画面に表示されるのを待っているイメージのキューです。画面に描画するためにイメージを取得して、それからキューに戻します。キューの厳密な仕組みと、キューにあるイメージを表示する条件は、スワップ チェーンがどのようにセットアップされているかによって異なりますが、スワップ チェーンの一般的な用途は、画面のリフレッシュ レートと同期してイメージを表示することです。

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

アプリケーションをデバッグする必要がある場合は、検証レイヤのサポートをセットアップできます。また、ゲームで必要になる可能性がある特定の拡張機能をチェックすることもできます。

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

適切なセットアップを発見して VkInstance を作成したら、レンダリング先のウィンドウを表す VkSurface を作成します。

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

利用できる物理デバイス(GPU)を列挙して、利用できる最初の適切なデバイスを選択します。

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

デバイスが適切なものであるかどうかチェックするために、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;
}

使用する物理デバイスが決まったら、論理デバイス(VkDevice)を作成します。これは、初期化され、アプリケーションが使用する他のオブジェクトすべてを作成する準備ができている Vulkan デバイスを表します。

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

この手順が終わる時点では、まだセットアップ プロセスの途中であるため、何もレンダリングされていない黒いウィンドウが表示されます。問題が発生している場合は、作業中のものと [codelab] step: create instance and device というリポジトリの commit を比較します。

4. スワップ チェーンを作成してオブジェクトを同期する

VkSwapchain は、画面に表示できるイメージのキューを表す Vulkan オブジェクトです。これは、ダブル バッファリングまたはトリプル バッファリングを実装するために使用されます。ダブル バッファリングやトリプル バッファリングによってテアリングを減らし、パフォーマンスを向上させることができます。

VkSwapchain を作成するために、アプリケーションはまず VkSurfaceKHR オブジェクトを作成する必要があります。インスタンスの作成の手順のなかでウィンドウをセットアップした際に、VkSurfaceKHR オブジェクトをすでに作成しました。

VkSwapchainKHR オブジェクトには、関連するイメージが多数あります。これらのイメージは、レンダリングされたシーンを保存するために使用されます。アプリケーションは VkSwapchainKHR オブジェクトからイメージを取得してレンダリングし、画面に表示できます。

イメージが画面に表示されると、アプリケーションからは利用できなくなります。アプリケーションがイメージを再度レンダリングできるようになるには、VkSwapchainKHR オブジェクトから別のイメージを取得する必要があります。

VkSwapchain は、通常はアプリケーションの開始時に 1 つ作成され、終了時に破棄されます。ただし、同じアプリケーション内で複数の VkSwapchain を作成して破棄することもできます。アプリケーションが複数の GPU を使用する必要がある場合や、複数のウィンドウを作成する必要がある場合がこれに該当します。

同期オブジェクトは、同期のために使用されるオブジェクトです。Vulkan には、VkFence、VkSemaphore、VkEvent があります。これらは複数のキューの間でリソースへのアクセスを管理するために使われます。複数のキューとレンダーパスを使用する場合はこれらのオブジェクトが必要ですが、ここではシンプルな例を扱っているため、使用しません。

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

また、デバイスがコンテキストを失った後には、スワップ チェーンの再作成の準備をする必要があります。たとえば、ユーザーが別のアプリケーションに切り替えた場合がこれに該当します。

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

この手順が終わる時点では、まだセットアップ プロセスの途中であるため、何もレンダリングされていない黒いウィンドウが表示されます。問題が発生している場合は、作業中のものと [codelab] step: create swapchain and sync objects というリポジトリの commit を比較します。

5. レンダーパスとフレームバッファを作成する

VkImageView は、VkImage にどうやってアクセスするかを表す Vulkan オブジェクトです。アクセスするイメージのサブリソース範囲、使用するピクセル形式、チャネルに適用するスウィズルを指定します。

VkRenderPass は、GPU がどのようにシーンをレンダリングするかを表す Vulkan オブジェクトです。使用するアタッチメント、アタッチメントをレンダリングする順序、レンダリング パイプラインの各段階でアタッチメントをどのように使用するかを指定します。

VkFramebuffer は、レンダーパスの実行中にアタッチメントとして使用されるイメージビューのセットを表す Vulkan オブジェクトです。つまり、実際のイメージのアタッチメントをレンダーパスにバインドします。

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

Vulkan におけるアタッチメントは、通常レンダー ターゲットと呼ばれるもので、普通はレンダリングの出力として使われるイメージです。ここではその形式のみを示す必要があります。たとえば、レンダーパスは特定のカラー形式や深度ステンシル形式を出力する場合があります。また、パスの開始時にアタッチメントがコンテンツを保持するか、破棄するか、消去するかも指定する必要があります。

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

フレームバッファは、アタッチメント(レンダー ターゲット)に使用できる実際のイメージへのリンクを表します。レンダーパスとイメージビューのセットを指定して、フレームバッファ オブジェクトを作成します。

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

この手順が終わる時点では、まだセットアップ プロセスの途中であるため、何もレンダリングされていない黒いウィンドウが表示されます。問題が発生している場合は、作業中のものと [codelab] step: create renderpass and framebuffer というリポジトリの commit を比較します。

6. シェーダーとパイプラインを作成する

VkShaderModule は、プログラム可能なシェーダーを表す Vulkan オブジェクトです。シェーダーは、グラフィック データに対するさまざまな処理を実行するために使用されます。たとえば、頂点の変換、ピクセルのシェーディング、グローバル エフェクトの計算などの処理があります。

VkPipeline は、プログラム可能なグラフィック パイプラインを表す Vulkan オブジェクトです。GPU がシーンをどのようにレンダリングするかを示す、一連の状態オブジェクトです。

VkDescriptorSetLayout は、記述子のグループである VkDescriptorSet のテンプレートです。記述子は、シェーダーがリソース(バッファ、イメージ、サンプラーなど)にアクセスできるようにするハンドルです。

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

createShaderModule 関数を定義して、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;
}

シンプルな頂点シェーダーとフラグメント シェーダーを読み込むグラフィック パイプラインを作成します。

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

この手順が終わる時点では、まだセットアップ プロセスの途中であるため、何もレンダリングされていない黒いウィンドウが表示されます。問題が発生している場合は、作業中のものと [codelab] step: create shader and pipeline というリポジトリの commit を比較します。

7. 記述子のセットとユニフォーム バッファ

VkDescriptorSet は、記述子のリソースのコレクションを表す Vulkan オブジェクトです。記述子のリソースは、シェーダーに対する入力のために使用されます。たとえば、ユニフォーム バッファ、イメージ サンプラー、ストレージ バッファなどを入力します。VkDescriptorSet を作成するには、VkDescriptorPool を作成する必要があります。

VkBuffer は、GPU と CPU の間でデータを共有するために使用されるメモリバッファです。ユニフォーム バッファとして使用すると、VkBuffer はシェーダーにデータをユニフォーム変数として渡します。ユニフォーム変数は、パイプライン内のすべてのシェーダーがアクセスできる定数です。

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

指定した VkDescriptorPool から割り当てられる VkDescriptorSet を作成します。

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

ユニフォーム バッファ構造体を指定して、ユニフォーム バッファを作成します。vkAllocateMemory を使用して、VkDeviceMemory からメモリを割り当てます。そして、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);
}

適切なメモリタイプを調べるためのヘルパー関数。

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

この手順が終わる時点では、まだセットアップ プロセスの途中であるため、何もレンダリングされていない黒いウィンドウが表示されます。問題が発生している場合は、作業中のものと [codelab] step: descriptorset and uniform buffer というリポジトリの commit を比較します。

8. コマンド バッファ - 作成、記録、描画

VkCommandPool は、コマンド バッファを割り当てるために使用されるシンプルなオブジェクトです。これは特定のキュー ファミリーに接続されます。

VkCommandBuffer は、GPU が実行する一連のコマンドを表す Vulkan オブジェクトです。これは低レベルのオブジェクトで、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()));
}

この手順が終わる時点では、まだセットアップ プロセスの途中であるため、何もレンダリングされていない黒いウィンドウが表示されます。問題が発生している場合は、作業中のものと [codelab] step: create command pool and command buffer というリポジトリの commit を比較します。

ユニフォーム バッファを更新して、コマンド バッファを記録、描画する

Vulkan におけるコマンドは、描画処理およびメモリ転送と同様に、関数呼び出しによって直接実行されることはありません。代わりに、すべての保留中の処理は、コマンド バッファ オブジェクトに記録されます。このようにして処理を行う利点は、何をするか Vulkan に指示する準備ができたときに、すべてのコマンドがまとめて送信され、すべてのコマンドを利用できる状態で Vulkan がコマンドを効率的に処理できることです。また、必要に応じてコマンドの記録を複数のスレッドで実行できるという利点もあります。

Vulkan では、すべてのレンダリングがレンダーパス内で行われます。この例では、レンダーパスは以前にセットアップしたフレームバッファにレンダリングします。

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

また、レンダリングするすべての頂点に対して同じ変換行列を使用しているため、ユニフォーム バッファを更新する必要があるかもしれません。

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

では、レンダリングします。作成したコマンド バッファを取得して、キューに送信します。

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

スワップ チェーンを再作成して、向きの変化を処理します。

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

アプリケーションのライフサイクルに統合します。

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

この手順を終えると、色のついた三角形がようやく画面に表示されます。

b07da8354cdd1629.png

画面を確認してください。問題が発生した場合は、[codelab] step: update uniform buffer, record command buffer and draw リポジトリへの commit と自分の作業を比較してください。

9. 三角形を回転させる

三角形を回転させるには、MVP マトリックスをシェーダーに渡す前に、回転を適用する必要があります。これは、モデル内の各頂点について、同じ行列の計算が重複して行われないようにするためです。

アプリケーション側で MVP マトリックスを計算するには、変換マトリックスが必要です。GLM ライブラリは、C++ の数学ライブラリです。GLSL の仕様に基づくグラフィック ソフトウェアを作成するために使用されます。このライブラリには、回転を適用した行列を作成するために必要な rotate 関数があります。

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

この手順を終えると、回転している三角形が画面に表示されます。画面を確認してください。問題が発生した場合は、[codelab] step: rotate triangle リポジトリへの commit と自分の作業を比較してください。

10. テクスチャを適用する

三角形にテクスチャを適用するには、まずイメージのファイルを非圧縮形式でメモリに読み込む必要があります。この手順では stb イメージ ライブラリを使用してイメージデータを RAM に読み込み、デコードしてから、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);
}

次に、前の手順でイメージデータをコピーした VkBuffer から、VkImage を作成します。

VkImage は、実際のテクスチャ データを保持するオブジェクトです。ピクセルのデータをテクスチャのメインメモリに保持しますが、それを読み取る方法については多くの情報を持ってはいません。そのため、次のセクションで VkImageView を作成する必要があります。

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

次に、VkImageView と VkSampler を作成します。VkSampler は、レンダリングされた各ピクセルの色の見本をとるためにフラグメント シェーダーが使用できます。

VkImageView は VkImage のラッパーです。テクスチャのデータを解釈する方法についての情報を保持します。たとえば、レイヤの一部にのみアクセスするということや、ピクセルのチャネルを特定の形でシャッフルするということを指定できます。

VkSampler は、特定のシェーダーでのテクスチャへのアクセスについてのデータを保持します。ピクセルをどのようにブレンドするか、どのように mipmap を使用するかについての情報を保持します。サンプラーは記述子内で VkImageView とともに使用されます。

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

また、シェーダーに渡すためにもサンプラーを作成する必要があります。

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

最後にシェーダーを変更して、頂点の色を使用する代わりにイメージから見本をとるようにします。テクスチャの座標は、テクスチャ上の位置をジオメトリ サーフェス上の位置にマッピングする、浮動小数点の位置です。この例では、正規化された(サイズが {1, 1} の)三角形があるので、頂点の texCoords を直接入力する頂点シェーダーの出力として vTexCoords を定義することで、このプロセスが完了します。

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

フラグメント シェーダーがサンプラーとテクスチャを使用します。

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

この手順を終えると、回転している三角形にテクスチャが適用されます。

b3426db4d6e94e89.gif

画面を確認してください。問題が発生した場合は、[codelab] step: apply texture リポジトリへの commit と自分の作業を比較してください。

11. 検証レイヤを追加する

検証レイヤはオプションのコンポーネントです。Vulkan の関数呼び出しをフックして、追加の処理を適用します:

  1. パラメータの値を検証して誤った使い方を検出する
  2. オブジェクトの作成と破棄をトラッキングして、リソースのリークを発見する
  3. スレッドセーフかどうかをチェックする
  4. プロファイリングと再生のために呼び出しのログを記録する

検証レイヤはダウンロードのサイズが大きいので、APK 内には含めていません。検証レイヤを有効にするには、次の手順に沿って対応してください:

最新の Android バイナリをダウンロードします: https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases

ダウンロードしたものを app/src/main/jniLibs にあるそれぞれの ABI フォルダに配置します

以下の手順に沿って検証レイヤを有効にします

// 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. 完了

これで、Vulkan のレンダリング パイプラインのセットアップが完了しました。ゲームの開発を始めることができます。

Vulkan から Android に機能を追加していきますので、最新情報を随時ご確認ください。

Android で Vulkan を使い始めるための詳細については、Android での Vulkan のスタートガイドをご覧ください。