Android에서 Vulkan 시작하기

1. 소개

게임에서 Vulkan을 사용하는 이유는 무엇인가요?

Vulkan은 Android의 기본 저수준 그래픽 API입니다. Vulkan을 사용하면 자체 게임 엔진과 렌더기를 구현하는 게임의 성능을 높일 수 있습니다.

Vulkan은 Android에서 Android 7.0(API 수준 24)부터 사용할 수 있습니다. Vulkan 1.1 지원은 Android 10.0부터 새로운 64비트 Android 기기의 요구사항입니다. 2022 Android 기준 프로필도 최소 Vulkan API 버전 1.1을 설정합니다.

그리기 호출이 많고 OpenGL ES를 사용하는 게임은 OpenGL ES 내에서 그리기 호출을 하면 비용이 많이 들기 때문에 상당한 드라이버 오버헤드가 발생할 수 있습니다. 이러한 게임은 그래픽 드라이버에서 프레임 시간의 상당 부분을 소비하여 CPU의 제약을 받을 수 있습니다. 또한 OpenGL ES에서 Vulkan으로 전환하면 이러한 게임의 CPU 및 전력 사용량이 크게 감소할 수 있습니다. 인스턴싱을 효과적으로 사용하여 그리기 호출을 줄일 수 없는 복잡한 장면이 게임에 있는 경우 특히 유용합니다.

빌드할 항목

이 Codelab에서는 기본 C++ Android 앱을 사용하고 Vulkan 렌더링 파이프라인을 설정하는 코드를 추가합니다. 그런 다음 Vulkan을 사용하여 화면에 회전하는 텍스처 삼각형을 렌더링하는 코드를 구현합니다.

필요한 항목

2. 설정

개발 환경 설정

이전에 Android 스튜디오에서 네이티브 프로젝트로 작업한 적이 없다면 Android NDK 및 CMake를 설치해야 할 수 있습니다. 이미 설치한 경우 프로젝트 설정을 진행합니다.

SDK, NDK, CMake가 설치되어 있는지 확인

Android 스튜디오를 시작합니다. 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 도구 탭을 선택하여 NDKCMake가 설치되어 있는지 확인합니다.

참고: 적당히 최근 버전이기만 하면 정확한 버전은 중요하지 않지만 현재 NDK는 26.1.10909125, CMake는 3.22.1 버전입니다. 기본적으로 설치되는 NDK 버전은 시간이 지나며 후속 NDK 출시에 따라 변경됩니다. 특정 버전의 NDK를 설치해야 하는 경우 '특정 버전의 NDK 설치' 섹션에서 NDK 설치에 관한 Android 스튜디오 참조의 안내를 따르세요.

d28adf9279adec4.png

필요한 모든 도구를 선택한 후 창 하단에 있는 Apply 버튼을 클릭하여 설치합니다. 그런 다음 OK 버튼을 클릭하여 Android SDK 창을 닫습니다.

프로젝트 설정

C++ 템플릿에서 파생된 시작 프로젝트는 git 저장소에 설정되어 있습니다. 시작 프로젝트는 앱 초기화와 이벤트 처리를 구현하지만 그래픽 설정이나 렌더링은 아직 실행하지 않습니다.

저장소 클론

명령줄에서 루트 프로젝트 디렉터리를 포함할 디렉터리로 변경하고 GitHub에서 클론합니다.

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

제목이 [codelab] start: empty app인 저장소의 초기 커밋에서 시작하는지 확인합니다.

Android 스튜디오에서 프로젝트를 열고 프로젝트를 빌드한 후 연결된 기기에서 실행합니다. 프로젝트는 빈 검은색 화면으로 시작되며 다음 섹션에서 그래픽 렌더링을 추가하게 됩니다.

3. Vulkan 인스턴스 및 기기 만들기

사용할 Vulkan API를 초기화하려면 먼저 Vulkan 인스턴스 객체(VkInstance)를 만들어야 합니다.

VkInstance 객체는 Vulkan 런타임의 애플리케이션 인스턴스를 나타냅니다. Vulkan API의 루트 객체이며 Vulkan 기기 객체와 활성화하려는 계층에 관한 정보를 가져오고 인스턴스화하는 데 사용됩니다.

애플리케이션에서 VkInstance를 만들면 이름과 버전, 필요한 Vulkan 인스턴스 확장 프로그램 등 자체 정보를 제공해야 합니다.

Vulkan API 디자인에는 GPU 드라이버에 도달하기 전에 API 호출을 가로채 처리하는 메커니즘을 제공하는 계층 시스템이 포함되어 있습니다. 애플리케이션은 VkInstance를 만들 때 활성화할 계층을 지정할 수 있습니다. 가장 흔히 사용되는 계층은 Vulkan 유효성 검사 계층으로, 오류나 최적이 아닌 성능 사례에 관한 API 사용 런타임 분석을 제공합니다.

VkInstance가 만들어지면 애플리케이션은 시스템에서 사용 가능한 실제 기기를 쿼리하고 논리 기기를 만들고 렌더링할 노출 영역을 만드는 데 이를 사용할 수 있습니다.

VkInstance는 일반적으로 애플리케이션 시작 시 한 번 만들어지고 종료 시 소멸됩니다. 하지만 동일한 애플리케이션 내에서 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를 하나만 반환합니다. 하지만 PC나 Android 기기는 실제 기기를 여러 개 열거할 수 있습니다. 예를 들어 컴퓨터에는 개별 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;
}

사용할 PhysicalDevice를 파악하고 나면 논리 기기(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라는 저장소의 커밋과 작업을 비교하세요.

4. Swapchain 및 동기화 객체 만들기

VkSwapchain은 디스플레이에 표시될 수 있는 이미지의 대기열을 나타내는 Vulkan 객체입니다. 이중 버퍼링이나 삼중 버퍼링을 구현하는 데 사용되며 이는 테어링을 줄이고 성능을 향상할 수 있습니다.

VkSwapchain을 만들려면 애플리케이션에서 먼저 VkSurfaceKHR 객체를 만들어야 합니다. 인스턴스 만들기 단계에서 창을 설정할 때 이미 VkSurfaceKHR 객체를 만들었습니다.

VkSwapchainKHR 객체에는 연결된 이미지가 여러 개 있습니다. 이러한 이미지는 렌더링된 장면을 저장하는 데 사용됩니다. 애플리케이션은 VkSwapchainKHR 객체에서 이미지를 획득하여 렌더링한 후 디스플레이에 표시할 수 있습니다.

이미지가 디스플레이에 표시되면 애플리케이션에서는 더 이상 이미지를 사용할 수 없습니다. 애플리케이션은 VkSwapchainKHR 객체에서 다른 이미지를 획득해야 다시 렌더링할 수 있습니다.

VkSwapchain은 일반적으로 애플리케이션 시작 시 한 번 만들어지고 종료 시 소멸됩니다. 하지만 동일한 애플리케이션 내에서 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라는 저장소의 커밋과 작업을 비교하세요.

5. Renderpass 및 Framebuffer 만들기

VkImageView는 VkImage 액세스 방법을 설명하는 Vulkan 객체입니다. 액세스할 이미지의 하위 리소스 범위와 사용할 픽셀 형식, 채널에 적용할 swizzle을 지정합니다.

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

Framebuffer는 첨부파일(렌더링 타겟)로 사용할 수 있는 실제 이미지 링크를 나타냅니다. renderpass와 imageview 집합을 지정하여 Framebuffer 객체를 만듭니다.

// 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라는 저장소의 커밋과 작업을 비교하세요.

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

VkShaderModule 객체에 셰이더를 로드하도록 createShaderModule 함수를 정의합니다.

// 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라는 저장소의 커밋과 작업을 비교하세요.

7. DescriptorSet 및 균일 버퍼

VkDescriptorSet은 설명자 리소스 모음을 나타내는 Vulkan 객체입니다. 설명자 리소스는 균일 버퍼와 이미지 샘플러, 저장소 버퍼 등 셰이더 입력을 제공하는 데 사용됩니다. VkDescriptorSet을 만들려면 VkDescriptorPool을 만들어야 합니다.

VkBuffer는 GPU와 CPU 간 데이터 공유에 사용되는 메모리 버퍼입니다. 균일 버퍼로 활용하면 데이터를 셰이더에 균일 변수로 전달합니다. 균일 변수는 파이프라인의 모든 셰이더에서 액세스할 수 있는 상수입니다.

// 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라는 저장소의 커밋과 작업을 비교하세요.

8. 명령어 버퍼: 생성, 기록, 그리기

VkCommandPool은 CommandBuffer를 할당하는 데 사용되는 간단한 객체입니다. 특정 대기열 제품군과 연결되어 있습니다.

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라는 저장소의 커밋과 작업을 비교하세요.

균일 버퍼 업데이트, 명령어 버퍼 기록, 그리기

그리기 작업과 메모리 전송 등 Vulkan의 명령어는 함수 호출을 사용하여 직접 실행되지 않습니다. 대신 대기 중인 모든 작업이 명령어 버퍼 객체에 기록됩니다. 이렇게 하면 Vulkan에 할 작업을 알려줄 준비가 될 때 모든 명령어가 함께 제출되어 Vulkan에서 더 효율적으로 명령어를 처리할 수 있다는 장점이 있습니다. 모든 명령어를 함께 사용할 수 있기 때문입니다. 또한 원하는 경우 여러 스레드에서 명령어 기록이 발생할 수 있습니다.

Vulkan에서는 모든 렌더링이 RenderPass 내에서 발생합니다. 여기 나온 예에서는 RenderPass가 이전에 설정된 FrameBuffer로 렌더링됩니다.

// 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라는 저장소의 커밋과 작업을 비교합니다.

9. 삼각형 회전

삼각형을 회전하려면 MVP 매트릭스에 회전을 적용한 후 매트릭스를 셰이더에 전달해야 합니다. 이렇게 하면 모델의 각 꼭짓점에 관해 동일한 매트릭스를 계산하는 중복 작업을 방지할 수 있습니다.

애플리케이션 측에서 MVP 매트릭스를 계산하려면 회전 변환 매트릭스가 필요합니다. GLM 라이브러리는 GLSL 사양에 따라 그래픽 소프트웨어를 작성하는 C++ 수학 라이브러리이며 회전이 적용된 매트릭스를 빌드하는 데 필요한 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이라는 저장소의 커밋과 작업을 비교합니다.

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를 만듭니다.

VkImageView는 VkImage 위에 있는 래퍼입니다. 영역이나 계층에만 액세스하려는 경우나 특정 방식으로 픽셀 채널을 셔플하려는 경우 등 텍스처의 데이터를 해석하는 방법에 관한 정보가 들어 있습니다.

VkSampler는 텍스처에 대한 특정 셰이더 액세스 데이터를 보유합니다. 픽셀을 혼합하는 방법이나 밉매핑을 실행하는 방법에 관한 정보가 포함되어 있습니다. 샘플러는 설명자의 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));
}

끝으로 꼭짓점 색상을 사용하는 대신 이미지를 샘플링하도록 셰이더를 수정합니다. 텍스처 좌표는 텍스처의 위치를 기하학적 노출 영역의 위치에 매핑하는 부동 소수점 위치입니다. 이 예에서 이 프로세스는 vTexCoords를 꼭짓점 셰이더의 출력으로 정의하여 완료됩니다. 꼭짓점 셰이더는 정규화된({1, 1} 크기) 삼각형이 있으므로 꼭짓점의 texCoords로 직접 채워집니다.

// 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라는 저장소의 커밋과 작업을 비교합니다.

11. 유효성 검사 계층 추가

유효성 검사 계층은 Vulkan 함수 호출에 연결되어 다음과 같은 추가 작업을 적용하는 선택적 구성요소입니다.

  1. 오용을 감지하기 위한 매개변수 값의 유효성 검사
  2. 리소스 누수를 찾기 위한 객체의 생성과 소멸 추적
  3. 스레드 안전 확인
  4. 프로파일링과 재생을 위한 통화 기록

유효성 검사 계층은 상당히 큰 다운로드이므로 APK 내에 제공하지 않기로 했습니다. 따라서 유효성 검사 계층을 사용 설정하려면 아래 간단한 단계를 따르세요.

https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases에서 최신 Android 바이너리를 다운로드합니다.

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 시작하기를 참고하세요.