Cómo empezar a usar Vulkan en Android

1. Introducción

¿Por qué deberías usar Vulkan en tu juego?

Vulkan es la principal API de gráficos de bajo nivel de Android. Vulkan permite alcanzar un rendimiento más alto para los juegos que implementan su propio motor de juego y renderizador.

Vulkan está disponible en Android a partir de Android 7.0 (nivel de API 24). La compatibilidad con Vulkan 1.1 es un requisito para dispositivos Android nuevos de 64 bits a partir de Android 10.0. El perfil de Baseline de Android de 2022 también establece la versión 1.1 como la versión mínima de la API de Vulkan.

Los juegos que tienen muchas llamadas de dibujo y que usan OpenGL ES pueden observar una sobrecarga significativa de los controladores debido al alto costo de realizar llamadas de dibujo en OpenGL ES. Estos juegos pueden vincularse a la CPU, ya que gastan grandes porciones de su latencia de fotogramas en el controlador gráfico. Los juegos también pueden observar reducciones significativas en el uso de CPU y energía si cambian de OpenGL ES a Vulkan. Eso se aplica, en particular, si tu juego tiene escenas complejas que no pueden usar la creación de instancias de manera efectiva para reducir las llamadas de dibujo.

Qué crearás

En este codelab, tomarás una app básica para Android de C++ y agregarás código para configurar la canalización de renderización de Vulkan. Luego, implementarás código que use Vulkan para renderizar un triángulo texturizado y rotativo en la pantalla.

Requisitos

2. Prepara tu entorno

Configura el entorno de desarrollo

Si no trabajaste con proyectos nativos en Android Studio, es posible que debas instalar el NDK de Android y CMake. Si ya los tienes instalados, ve a la sección para configurar el proyecto.

Comprueba que el SDK, el NDK y CMake estén instalados

Inicia Android Studio. Cuando se muestre la ventana Welcome to Android Studio, abre el menú desplegable Configure y selecciona la opción SDK Manager.

3b7b47a139bc456.png

Si ya tienes abierto un proyecto existente, puedes abrir el SDK Manager a través del menú Tools. Haz clic en el menú Tools y selecciona SDK Manager para que se abra la ventana correspondiente.

En la barra lateral, selecciona Appearance & Behavior > System Settings > Android SDK. Selecciona la pestaña SDK Platforms en el panel SDK de Android para mostrar una lista de opciones de herramientas instaladas. Asegúrate de tener instalado el SDK de Android 12.0 o una versión posterior.

931f6ae02822f417.png

Luego, selecciona la pestaña SDK Tools y asegúrate de que el NDK y CMake estén instalados.

Nota: La versión exacta no debería importar, siempre y cuando sean bastante nuevas, pero estamos usando NDK 26.1.10909125 y CMake 3.22.1. La versión del NDK que se instale de forma predeterminada cambiará con el tiempo en los lanzamientos posteriores del NDK. Si necesitas instalar una versión específica del NDK, sigue las instrucciones de la referencia de Android Studio para instalar el NDK en la sección "Cómo instalar una versión específica del NDK".

d28adf9279adec4.png

Una vez que hayas marcado todas las herramientas necesarias, haz clic en el botón Apply ubicado en la parte inferior de la ventana para instalarlas. Luego, haz clic en el botón OK para cerrar la ventana Android SDK.

Configura el proyecto

Se configuró un proyecto inicial derivado de la plantilla C++ en un repositorio Git. El proyecto inicial implementa la inicialización de la app y el control de eventos, pero aún no realiza renderizaciones ni configuración de gráficos.

Clona el repositorio

Desde la línea de comandos, cambia al directorio donde deseas que esté el directorio raíz del proyecto y clónalo desde GitHub:

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

Asegúrate de comenzar desde la confirmación inicial del repo titulada [codelab] start: empty app.

Abre el proyecto con Android Studio, compílalo y, luego, ejecútalo en un dispositivo conectado. El proyecto se iniciará en una pantalla negra vacía, y podrás agregar renderización de gráficos en las secciones siguientes.

3. Crea un dispositivo y una instancia de Vulkan

El primer paso para inicializar la API de Vulkan es crear un objeto de instancia de Vulkan (VkInstance).

El objeto VkInstance representa la instancia del tiempo de ejecución de Vulkan de una aplicación. Es el objeto raíz de la API de Vulkan y se usa para recuperar información y crear instancias de objetos en dispositivos Vulkan y las capas que se deseen activar.

Cuando una aplicación crea un objeto VkInstance, debe proporcionar información sobre sí misma, como el nombre, la versión y las extensiones de instancias de Vulkan que necesita.

El diseño de la API de Vulkan incluye un sistema de capas que proporciona un mecanismo para interceptar y procesar llamadas a la API antes de que lleguen al controlador de GPU. La aplicación puede indicar que se activen capas cuando se crea un objeto VkInstance. La capa de uso más frecuente es la de validación de Vulkan, que proporciona el análisis del tiempo de ejecución de la API para detectar errores o prácticas de rendimiento que están por debajo del nivel óptimo.

Una vez creado un objeto VkInstance, la app puede usarlo para consultar sobre los dispositivos físicos disponibles en el sistema y para crear dispositivos lógicos y superficies donde renderizar.

Por lo general, un objeto VkInstance se crea una sola vez en el inicio de la aplicación y se destruye al final. Sin embargo, es posible crear varios VkInstances dentro de la misma aplicación, por ejemplo, si esta requiere usar varias GPUs o crear varias ventanas.

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

Un objeto VkPhysicalDevice es un objeto de Vulkan que representa un dispositivo Vulkan físico en el sistema. La mayoría de los dispositivos Android solo devolverá un único objeto VkPhysicalDevice que representa a la GPU. Sin embargo, en una PC o en un dispositivo Android se podrían detallar varios dispositivos físicos. Por ejemplo, una computadora que contiene tanto una GPU discreta como una integrada.

Es posible consultar los objetos VkPhysicalDevice para conocer sus propiedades, como el nombre, el proveedor, la versión del controlador y las funciones compatibles. Esta información se puede usar para elegir el mejor dispositivo físico para una aplicación específica.

Una vez elegido un objeto VkPhysicalDevice, la aplicación podrá crear un dispositivo lógico a partir de él. Un dispositivo lógico es una representación del dispositivo físico específico de la aplicación. Tiene su propio estado y sus propios recursos, así como también es independiente de otros dispositivos lógicos que se puedan crear a partir del mismo dispositivo físico.

Hay diferentes tipos de filas que provienen de diferentes familias de filas, y cada una de ellas permite solo un subconjunto de comandos. Por ejemplo, podría haber una familia de filas que solo permite el procesamiento de comandos de procesamiento, o bien otra que solo permite los comandos relacionados con la transferencia de memoria.

Un objeto VkPhysicalDevice puede detallar todos los tipos disponibles de familias de filas. Aquí únicamente nos interesa la fila de gráficos, aunque también puede que haya colas adicionales que admitan solo COMPUTE o TRANSFER. Una familia de filas no tiene su propio tipo. En cambio, se representa con el tipo de índice numérico uint32_t dentro del objeto principal (VkPhysicalDevice).

Es posible crear varios dispositivos lógicos a partir de un VkPhysicalDevice. Esto resulta útil para las aplicaciones que requieren usar varias GPUs o crear varias ventanas.

Un objeto VkDevice es un objeto de Vulkan que representa un dispositivo Vulkan lógico. Se trata de una abstracción delgada sobre el dispositivo físico que proporciona toda la funcionalidad necesaria para crear y administrar recursos de Vulkan, como búferes, imágenes y sombreadores.

Un objeto VkDevice se crea a partir de un objeto VkPhysicalDevice y es específico de la aplicación que lo creó. Tiene su propio estado y sus propios recursos, así como también es independiente de otros dispositivos lógicos que se puedan crear a partir del mismo dispositivo físico.

Un objeto VkSurfaceKHR representa una superficie que puede ser el destino de operaciones de renderización. Para mostrar gráficos en la pantalla del dispositivo, crearás una superficie a partir de una referencia al objeto de la ventana de la aplicación. Una vez creado un objeto VkSurfaceKHR, la aplicación puede usarlo para crear un objeto VkSwapchainKHR.

El objeto VkSwapchainKHR representa una infraestructura que posee los búferes donde realizaremos la renderización antes de que podamos visualizarlos en la pantalla. Básicamente, se trata de una fila de imágenes a la espera de presentarse en la pantalla. Adquiriremos una de esas imágenes para llevarla allí y, luego, la regresaremos a la fila. El funcionamiento exacto de la fila y las condiciones para presentar una imagen de esta dependen de la configuración de la cadena de intercambio, pero el fin general de la cadena de intercambio es sincronizar la presentación de imágenes con la frecuencia de actualización de la pantalla.

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

Si tienes de depurar la aplicación, puedes configurar la compatibilidad de la capa de validación. También puedes buscar las extensiones específicas que tu juego pueda requerir.

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

Una vez que encontraste la configuración apropiada y creaste el objeto VkInstance, crea el objeto VkSurface, que representa la ventana donde realizar la renderización.

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

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

Detalla los dispositivos físicos (GPUs) disponibles y selecciona el primer dispositivo adecuado disponible.

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

Para verificar si el dispositivo es adecuado, tenemos que buscar uno que admita la fila 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;
}

Una vez que hayamos determinado el PhysicalDevice que se usará, crea un dispositivo lógico (conocido como VkDevice). Este representa un dispositivo Vulkan inicializado, listo para crear todos los demás objetos que usará la aplicación.

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

Al final de este paso, solo verás una ventana negra sin ninguna renderización, ya que esta aún se encuentra en la mitad del proceso de configuración. Si algo sale mal, puedes comparar tu trabajo con la confirmación del repo titulada [codelab] step: create instance and device.

4. Crea un objeto Swapchain y sincronizar objetos

Un objeto VkSwapchain es un objeto de Vulkan que representa una fila de imágenes que se pueden presentar en la pantalla. Se utiliza para implementar un almacenamiento en búfer doble o triple, lo que puede reducir rupturas y mejorar el rendimiento.

Para crear un objeto VkSwapchain, la aplicación primero debe crear un objeto VkSurfaceKHR. Ya creamos nuestro objeto VkSurfaceKHR cuando configuramos la ventana durante el paso de creación de instancia.

El objeto VkSwapchainKHR tendrá muchas imágenes asociadas. Dichas imágenes se usan para almacenar el ambiente renderizado. La aplicación puede adquirir una imagen del objeto VkSwapchainKHR, renderizarla y, luego, presentarla en la pantalla.

Una vez que una imagen se presenta en la pantalla, deja de estar disponible para la aplicación. La aplicación debe adquirir otra imagen del objeto VkSwapchainKHR para poder volver a renderizarla.

Por lo general, los objetos VkSwapchain se crean una sola vez en el inicio de la aplicación y se destruyen al final. Sin embargo, es posible crear y destruir varios VkSwapchains dentro de la misma aplicación, por ejemplo, si esta requiere usar varias GPUs o crear varias ventanas.

Los objetos de sincronización son objetos que se usan para sincronizar. Vulkan tiene VkFence, VkSemaphore y VkEvent, que se usan para controlar el acceso de recursos en varias colas. Estos objetos son necesarios cuando usas varias colas y pases de renderización, pero para nuestro ejemplo sencillo no los utilizaremos.

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

También tendrás que prepararte para una recreación de la cadena de intercambio luego de la pérdida de contexto del dispositivo. Por ejemplo, cuando el usuario cambia de aplicación.

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

Al final de este paso, solo verás una ventana negra sin ninguna renderización, ya que esta aún se encuentra en la mitad del proceso de configuración. Si algo sale mal, puedes comparar tu trabajo con la confirmación del repo titulada [codelab] step: create swapchain and sync objects.

5. Crea objetos Renderpass y Framebuffer

Un objeto VkImageView es un objeto de Vulkan que describe cómo acceder a un VkImage. Especifica el rango de subrecursos de la imagen a la que se accederá, el formato de píxeles que se usará y el swizzling que se aplicará a los canales.

Un objeto VkRenderPass es un objeto de Vulkan que describe cómo debería la GPU renderizar un ambiente. Especifica los archivos adjuntos que se usarán, el orden en el que se renderizarán y cómo se usarán en cada etapa de la canalización de la renderización.

Un objeto VkFramebuffer es un objeto de Vulkan que representa un conjunto de vistas de imágenes que se usarán como archivos adjuntos durante la ejecución de un pase de renderización. En otras palabras, vincula los archivos adjuntos reales de la imagen con el pase de renderización.

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

Un archivo adjunto en Vulkan es lo que habitualmente se conoce como el objetivo de renderización, que suele ser una imagen que se usa como resultado para la renderización. Aquí solo se tiene que describir el formato, por ejemplo, el pase de renderización puede arrojar como resultado un formato de color específico o un formato de esténcil de profundidad. También tienes que especificar si el contenido del archivo adjunto se debería conservar, descartar o borrar al principio del pase.

// 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 representa el vínculo con las imágenes reales que se pueden usar para los archivos adjuntos (objetivo de renderización). Para crear un objeto Framebuffer, especifica el pase de renderización y el conjunto de vistas de imágenes.

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

Al final de este paso, solo verás una ventana negra sin ninguna renderización, ya que esta aún se encuentra en la mitad del proceso de configuración. Si algo sale mal, puedes comparar tu trabajo con la confirmación del repo titulada [codelab] step: create renderpass and framebuffer.

6. Crea sombreadores y canalizaciones

Un objeto VkShaderModule es un objeto de Vulkan que representa un sombreador programable. Los sombreadores se usan para realizar diversas operaciones en datos de gráficos, como transformar vértices, sombrear píxeles y procesar efectos globales.

Un objeto VkPipeline es un objeto de Vulkan que representa una canalización de gráficos programable. Es un conjunto de objetos de estado que describen de qué manera la GPU debería renderizar un ambiente.

Un objeto VkDescriptorSetLayout es una plantilla para un objeto VkDescriptorSet que, a su vez, es un grupo de descriptores. Los descriptores son el control que les permite a los sombreadores acceder a los recursos, como los búferes, las imágenes o las muestras.

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

Define la función createShaderModule que se cargará en los sombreadores en objetos 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;
}

Para crear la canalización de gráficos, carga un vértice simple y un sombreador de fragmentos.

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

Al final de este paso, solo verás una ventana negra sin ninguna renderización, ya que esta aún se encuentra en la mitad del proceso de configuración. Si algo sale mal, puedes comparar tu trabajo con la confirmación del repo titulada [codelab] step: create shader and pipeline.

7. DescriptorSet y búfer uniforme

Un objeto VkDescriptorSet es un objeto de Vulkan que representa una colección de recursos del descriptor. Estos se usan para proporcionar entradas del sombreador, como búferes uniformes, muestras de imágenes y búferes de almacenamiento. Para crear objetos VkDescriptorSet, tendremos que crear un objeto VkDescriptorPool.

Un objeto VkBuffer es un búfer de memoria que se usa para compartir datos entre la GPU y la CPU. Cuando se utiliza como búfer uniforme, transfiere datos a sombreadores como variables uniformes. Estas variables son constantes a las que pueden acceder todos los sombreadores en una canalización.

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

Crea VkDescriptorSets asignados a partir del objeto VkDescriptorPool especificado.

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

Especifica nuestro struct de búfer uniforme y crea los búferes uniformes. Para asignar la memoria del objeto VkDeviceMemory, recuerda usar vkAllocateMemory, y vincula el búfer a la memoria por medio de 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);
}

Función auxiliar para buscar el tipo de memoria correcto.

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

Al final de este paso, solo verás una ventana negra sin ninguna renderización, ya que esta aún se encuentra en la mitad del proceso de configuración. Si algo sale mal, puedes comparar tu trabajo con la confirmación del repo titulada [codelab] step: descriptorset and uniform buffer.

8. Búfer de procesamiento: crear, registrar y diseñar

El objeto VkCommandPool es un objeto simple que se usa para asignar CommandBuffers. Está conectado a una familia de filas específica.

Un objeto VkCommandBuffer es un objeto de Vulkan que representa una lista de comandos que ejecutará la GPU. Se trata de un objeto de nivel bajo que proporciona control detallado sobre la 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()));
}

Al final de este paso, solo verás una ventana negra sin ninguna renderización, ya que esta aún se encuentra en la mitad del proceso de configuración. Si algo sale mal, puedes comparar tu trabajo con la confirmación del repo titulada [codelab] step: create command pool and command buffer.

Actualiza el búfer uniforme, registra el búfer de procesamiento y dibuja

Los comandos en Vulkan, como diseñar operaciones y transferencias de memoria, no se ejecutan directamente con llamadas de función. En cambio, todas las operaciones pendientes se graban en objetos de búfer de procesamiento. La ventaja de esto es que cuando estamos listos para indicarle a Vulkan lo que queremos hacer, todos los comandos se envían juntos y Vulkan puede procesarlos de manera más eficiente porque están todos disponibles juntos. Además, esto permite que se registren los procesamientos en varios subprocesos, si así se desea.

En Vulkan, toda la renderización se lleva a cabo dentro de RenderPasses. En nuestro ejemplo, el procesamiento RenderPass realizará la renderización en el procesamiento FrameBuffer que configuramos anteriormente.

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

Es posible que también tengas que actualizar el búfer uniforme, ya que usamos la misma matriz de transformación para todos los vértices que renderizamos.

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

Llegó el momento de la renderización. Toma el búfer de procesamiento que redactaste y envíalo a la fila.

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

Recrea la cadena de intercambio para controlar el cambio de orientación.

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

Integra el procesamiento en el ciclo de vida de la aplicación.

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

Al final de este paso, finalmente verás un triángulo coloreado en la pantalla.

b07da8354cdd1629.png

Verifica que sea así y, si algo sale mal, puedes comparar tu trabajo con la confirmación del repo titulada [codelab] step: update uniform buffer, record command buffer and draw.

9. Rota el triángulo

Para rotar el triángulo, primero tenemos que aplicar la rotación a nuestra matriz de MVP para poder transferir la matriz al sombreador. De esta manera, se evita duplicar el trabajo de calcular la misma matriz para cada uno de los vértices del modelo.

Para calcular la matriz de MVP del lado de la aplicación, se necesita una matriz de transformación de rotación. Una biblioteca GLM es una biblioteca de matemática C++ para programar software de gráficos según especificaciones GLSL y tiene la función de rotación requerida para diseñar la matriz con tal función incorporada.

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

Al final de este paso, verás que el triángulo rota en la pantalla. Verifica que sea así y, si algo sale mal, puedes comparar tu trabajo con la confirmación del repo titulada [codelab] step: rotate triangle.

10. Aplica textura

Para aplicar textura al triángulo, primero se tiene que cargar el archivo de imagen en un formato sin comprimir en la memoria. En este paso, se usa la biblioteca stb image para cargar y decodificar los datos de la imagen en la memoria RAM que luego se copia al búfer de 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);
}

A continuación, crea un objeto VkImage del VkBuffer completado con los datos de la imagen del paso anterior.

VkImage es el objeto que contiene los datos de textura real. Incluye los datos de pixelación en la memoria principal de la textura, pero no contiene mucha información sobre su lectura. Es por eso que tenemos que crear un objeto VkImageView en la sección siguiente.

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

A continuación, crea objetos VkImageView y VkSampler que el sombreador de fragmentos podrá usar para probar el color de cada uno de los píxeles renderizados.

El objeto VkImageView es un wrapper sobre el objeto VkImage. Contiene información acerca de la interpretación de datos de la textura, por ejemplo, si quieres acceder solo a una región o capa, o si quieres reproducir aleatoriamente los canales de pixelación de una manera específica.

VkSampler contiene los datos para el acceso específico del sombreador a la textura. Incluye información acerca de cómo combinar los píxeles o cómo generar mipmaps. Las muestras se usan con VkImageViews en los descriptores.

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

También necesitaremos crear una muestra para pasar a nuestro sombreador.

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

Por último, modifica los sombreadores para probar la imagen, en lugar de usar un color del vértice. Las coordinadas de la textura son posiciones de punto flotante que asignan ubicaciones de una textura a ubicaciones de una superficie geométrica. En nuestro ejemplo, este proceso se completa al definir el valor vTexCoords como salida del sombreador del vértice que rellenamos con el valor texCoords del vértice directamente, ya que tenemos un triángulo normalizado (de tamaño {1, 1}).

// CODELAB: shader.vert
#version 450

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

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

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

layout(location = 0) out vec2 vTexCoords;

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

Sombreador de fragmentos que usa muestras y texturas.

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

Al final de este paso, verás que el triángulo rotativo tiene textura.

b3426db4d6e94e89.gif

Verifica que sea así y, si algo sale mal, puedes comparar tu trabajo con la confirmación del repo titulada [codelab] step: apply texture.

11. Agrega una capa de validación

Las capas de validación son componentes optativos que se adhieren a las llamadas de funciones de Vulkan para aplicar operaciones adicionales, como las siguientes:

  1. Validar los valores de parámetros para detectar el uso inadecuado
  2. Hacer un seguimiento de la creación y la destrucción de objetos para dar con fugas de recursos
  3. Verificar la seguridad de subprocesos
  4. Registrar llamadas para la creación de perfiles y la reproducción

Elegimos no incluir la capa de validación en el APK, ya que esta es una descarga de tamaño considerable. Entonces, para habilitar la capa de validación, sigue estos sencillos pasos:

Descarga los objetos binarios de Android más recientes en: https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases

Colócalos en sus respectivas carpetas ABI ubicadas aquí: app/src/main/jniLibs

Sigue los pasos a continuación para habilitar las capas de validación

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

¡Felicitaciones! Configuraste correctamente la canalización de renderización de Vulkan y tienes todo listo para desarrollar tu juego.

Mantente al tanto, ya que agregaremos más funciones de Vulkan para Android.

Para obtener más información introductoria sobre el uso de Vulkan en Android, lee Cómo empezar a usar Vulkan en Android.