Vulkan. Guía del desarrollador. Cadena de intercambio

Sigo publicando traducciones del manual de la API de Vulkan (el enlace al original es vulkan-tutorial.com ), y hoy quiero compartir la traducción de un nuevo capítulo: Cadena de intercambio de la sección Dibujar un triángulo, subsección Presentación.



Contenido
1.



2.



3.



4.







  1. (pipeline)


5.



  1. Staging


6. Uniform-



  1. layout
  2. sets


7.



  1. Image view image sampler
  2. image sampler


8.



9.



10. -



11. Multisampling



FAQ









Cadena de intercambio





Vulkan no tiene un framebuffer predeterminado, por lo que necesita una infraestructura con búferes para renderizar las imágenes antes de mostrarlas. Esta infraestructura se denomina cadena de intercambio y debe crearse explícitamente en Vulkan. La cadena de intercambio es una cola de imágenes que esperan ser mostradas en la pantalla. El programa primero solicita un objeto image(VkImage)



para dibujar y, después de renderizarlo, lo envía de vuelta a la cola. Exactamente cómo funciona la cola depende de la configuración, pero la tarea principal de la cadena de intercambio es sincronizar la visualización de imágenes con la frecuencia de actualización de la pantalla.



Comprobación del soporte de la cadena de intercambio



Algunas tarjetas de video especializadas no tienen salidas de pantalla y, por lo tanto, no pueden mostrar imágenes en la pantalla. Además, el mapeo de la pantalla está vinculado al sistema de ventanas y no es parte del núcleo de Vulkan. Por lo tanto, necesitamos conectar la extensión VK_KHR_swapchain



.



Primero isDeviceSuitable



, cambiemos la función para verificar si la extensión es compatible. Ya hemos trabajado con la lista de extensiones compatibles antes, por lo que no debería haber ninguna dificultad. Tenga en cuenta que el archivo de encabezado Vulkan proporciona una práctica macro VK_KHR_SWAPCHAIN_EXTENSION_NAME



que se define como " VK_KHR_swapchain



". La ventaja de esta macro es que si comete un error de ortografía, el compilador se lo advertirá.



Comencemos declarando una lista de extensiones requeridas.



const std::vector<const char*> deviceExtensions = {
    VK_KHR_SWAPCHAIN_EXTENSION_NAME
};
      
      





Para una verificación adicional, creemos una nueva función checkDeviceExtensionSupport



llamada desde isDeviceSuitable



:



bool isDeviceSuitable(VkPhysicalDevice device) {
    QueueFamilyIndices indices = findQueueFamilies(device);

    bool extensionsSupported = checkDeviceExtensionSupport(device);

    return indices.isComplete() && extensionsSupported;
}

bool checkDeviceExtensionSupport(VkPhysicalDevice device) {
    return true;
}
      
      





Cambiemos el cuerpo de la función para comprobar si todas las extensiones que necesitamos están en la lista de compatibles.



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





Aquí solía std::set<std::string>



almacenar los nombres de las extensiones requeridas pero aún no confirmadas. También puede utilizar un bucle anidado como en una función checkValidationLayerSupport



. La diferencia de rendimiento no es significativa.



Ahora ejecutemos el programa y asegurémonos de que nuestra tarjeta de video sea adecuada para crear una cadena de intercambio. Tenga en cuenta que la presencia de una cola de visualización ya implica la compatibilidad con la extensión de la cadena de intercambio. Sin embargo, es mejor asegurarse de esto explícitamente.



Conexión de extensiones



Para usar la cadena de intercambio, primero debe habilitar la extensión VK_KHR_swapchain



. Para hacer esto, cambiemos ligeramente el relleno VkDeviceCreateInfo



al crear el dispositivo lógico:



createInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size());
createInfo.ppEnabledExtensionNames = deviceExtensions.data();
      
      





Solicitar información sobre el soporte de la cadena de intercambio



Verificar solo para ver si la cadena de intercambio está disponible no es suficiente. La creación de la cadena de intercambio implica mucha más configuración, por lo que necesitamos solicitar más información.



En total, debe verificar 3 tipos de propiedades:



  • Capacidades básicas de la superficie, como número mínimo / máximo de imágenes en la cadena de intercambio, ancho y alto mínimo / máximo de imágenes
  • Formato de superficie (formato de píxeles, espacio de color)
  • Modos operativos disponibles


Para trabajar con estos datos usaremos la estructura:



struct SwapChainSupportDetails {
    VkSurfaceCapabilitiesKHR capabilities;
    std::vector<VkSurfaceFormatKHR> formats;
    std::vector<VkPresentModeKHR> presentModes;
};
      
      





Ahora creemos una función querySwapChainSupport



que llene esta estructura.



SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device) {
    SwapChainSupportDetails details;

    return details;
}
      
      





Comencemos con las capacidades de superficie. Son fáciles de consultar y volver a la estructura VkSurfaceCapabilitiesKHR



.



vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities);
      
      





Esta función acepta los archivos VkPhysicalDevice



y VkSurfaceKHR



. Cada vez que solicitemos una funcionalidad compatible, estos dos parámetros serán los primeros, ya que son componentes clave de la cadena de intercambio.



El siguiente paso es consultar los formatos de superficie admitidos. Para hacer esto, realicemos el ritual ya familiar con una llamada de doble función:



uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr);

if (formatCount != 0) {
    details.formats.resize(formatCount);
    vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, details.formats.data());
}
      
      





Asegúrese de asignar suficiente espacio en el vector para obtener todos los formatos disponibles.



De la misma manera, solicitamos los modos de operación admitidos usando la función vkGetPhysicalDeviceSurfacePresentModesKHR



:



uint32_t presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr);

if (presentModeCount != 0) {
    details.presentModes.resize(presentModeCount);
    vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data());
}
      
      





Cuando toda la información necesaria esté en la estructura, agregue la función isDeviceSuitable



para verificar si la cadena de intercambio es compatible. Para los propósitos de este tutorial, asumiremos que si hay al menos un formato de imagen compatible y un modo compatible para la superficie de la ventana, entonces la cadena de intercambio es compatible.



bool swapChainAdequate = false;
if (extensionsSupported) {
    SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device);
    swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty();
}
      
      





Solo necesita solicitar soporte de cadena de intercambio después de haber verificado que la extensión está disponible.



La última línea de la función cambia a:



return indices.isComplete() && extensionsSupported && swapChainAdequate;
      
      





Elegir la configuración para la cadena de intercambio



Si es swapChainAdequate



verdadero, se admite la cadena de intercambio. Pero la cadena de intercambio puede tener varios modos. Escribamos algunas funciones para encontrar la configuración adecuada para crear la cadena de intercambio más eficiente.



En total, resaltemos 3 tipos de configuraciones:

  • formato de superficie (profundidad de color)
  • modo de funcionamiento (condiciones para cambiar fotogramas en la pantalla)
  • extensión de intercambio (resolución de imágenes en la cadena de intercambio)


Para cada configuración, buscaremos algún valor "ideal", y si no está disponible, usaremos alguna lógica para elegir cuál es.



Formato de superficie



Agreguemos una función para seleccionar un formato:



VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& availableFormats) {

}
      
      





Posteriormente, pasaremos un miembro formats



de la estructura SwapChainSupportDetails



como argumento.



Cada elemento availableFormats



contiene miembros format



y colorSpace



. El campo format



define el número y los tipos de canales. Por ejemplo, VK_FORMAT_B8G8R8A8_SRGB



significa que tenemos canales B, G, R y alfa de 8 bits cada uno, para un total de 32 bits por píxel. Una bandera VK_COLOR_SPACE_SRGB_NONLINEAR_KHR



en el campo colorSpace



indica si se admite el espacio de color SRGB. Tenga en cuenta que en una versión anterior de la especificación se llamaba a esta bandera VK_COLORSPACE_SRGB_NONLINEAR_KHR



.



Usaremos SRGB como espacio de color. SRGB es un estándar para la representación de colores en imágenes, reproduce mejor los colores percibidos. Es por eso que también usaremos uno de los formatos SRGB como formato de color - VK_FORMAT_B8G8R8A8_SRGB



.



Repasemos la lista y comprobemos si la combinación que necesitamos está disponible:



for (const auto& availableFormat : availableFormats) {
    if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
        return availableFormat;
    }
}
      
      





De lo contrario, podemos ordenar los formatos disponibles de los más adecuados a los menos adecuados, pero en la mayoría de los casos simplemente podemos tomar el primero de la lista.



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

      
      





Horas Laborales



El modo de operación es quizás la configuración más importante para la cadena de intercambio, ya que determina las condiciones para cambiar los marcos en la pantalla.



Hay cuatro modos disponibles en Vulkan:



  • VK_PRESENT_MODE_IMMEDIATE_KHR



    : , , , .
  • VK_PRESENT_MODE_FIFO_KHR



    : . , . , . , .
  • VK_PRESENT_MODE_FIFO_RELAXED_KHR



    : , . . .
  • VK_PRESENT_MODE_MAILBOX_KHR



    : esta es otra variación del segundo modo. En lugar de bloquear el programa cuando la cola está llena, las imágenes de la cola se reemplazan por otras nuevas. Este modo es adecuado para implementar el almacenamiento en búfer triple. Con él, puede evitar la aparición de artefactos con baja latencia.


Solo se garantiza que el modo estará disponible VK_PRESENT_MODE_FIFO_KHR



, por lo que nuevamente tendremos que escribir una función para encontrar el mejor modo disponible:



VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR>& availablePresentModes) {
    return VK_PRESENT_MODE_FIFO_KHR;
}
      
      





Personalmente, creo que es mejor utilizar el almacenamiento en búfer triple. Evita artefactos con baja latencia.



Así que revisemos la lista para verificar los modos disponibles:



VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR>& availablePresentModes) {
    for (const auto& availablePresentMode : availablePresentModes) {
        if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) {
            return availablePresentMode;
        }
    }

    return VK_PRESENT_MODE_FIFO_KHR;
}
      
      





Extensión de intercambio



Queda por configurar la última propiedad. Para hacer esto, agregue una función:



VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) {

}
      
      





La extensión de intercambio es la resolución de las imágenes en la cadena de intercambio, que casi siempre coincide con la resolución de la ventana (en píxeles) donde se representan las imágenes. Conseguimos el rango permitido en la estructura VkSurfaceCapabilitiesKHR



. Vulkan nos dice qué resolución debemos establecer usando un campo currentExtent



(coincide con el tamaño de la ventana). Sin embargo, algunos administradores de ventanas permiten diferentes resoluciones. Para esto, currentExtent



se especifica un valor especial para el ancho y la altura : el valor máximo del tipo uint32_t



. En este caso, del intervalo entre minImageExtent



y, maxImageExtent



elegiremos la resolución que mejor se adapte a la resolución de la ventana. Lo principal es especificar correctamente las unidades de medida.



GLFW utiliza dos unidades de medida: píxeles y coordenadas de pantalla . Entonces, la resolución {WIDTH, HEIGHT}



que especificamos al crear la ventana se mide en coordenadas de pantalla. Pero como Vulkan trabaja con píxeles, la resolución de la cadena de intercambio también debe especificarse en píxeles. Si está utilizando una pantalla de alta resolución (como la pantalla Retina de Apple), las coordenadas de la pantalla no coinciden con los píxeles: debido a la mayor densidad de píxeles, la resolución de la ventana es mayor en píxeles que en las coordenadas de la pantalla. Dado que Vulkan no arreglará el permiso de la cadena de intercambio por nosotros, no podemos usar el permiso original {WIDTH, HEIGHT}



. En cambio, deberíamos usar glfwGetFramebufferSize



para consultar la resolución de la ventana en píxeles antes de asignarla a las resoluciones de imagen mínima y máxima.



#include <cstdint> // Necessary for UINT32_MAX

...

VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) {
    if (capabilities.currentExtent.width != UINT32_MAX) {
        return capabilities.currentExtent;
    } else {
        int width, height;
        glfwGetFramebufferSize(window, &width, &height);

        VkExtent2D actualExtent = {
            static_cast<uint32_t>(width),
            static_cast<uint32_t>(height)
        };

        actualExtent.width = std::max(capabilities.minImageExtent.width, std::min(capabilities.maxImageExtent.width, actualExtent.width));
        actualExtent.height = std::max(capabilities.minImageExtent.height, std::min(capabilities.maxImageExtent.height, actualExtent.height));

        return actualExtent;
    }
}
      
      





Función max



y min



se utiliza para limitar los valores width



y height



dentro de las resoluciones disponibles. No olvide incluir el archivo de encabezado <algorithm>



para usar las funciones.



Creación de cadena de intercambio



Ahora tenemos toda la información que necesitamos para crear una cadena de intercambio adecuada.



Creemos una función createSwapChain



y la llamemos initVulkan



después de crear el dispositivo lógico.



void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
}

void createSwapChain() {
    SwapChainSupportDetails swapChainSupport = querySwapChainSupport(physicalDevice);

    VkSurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats);
    VkPresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.presentModes);
    VkExtent2D extent = chooseSwapExtent(swapChainSupport.capabilities);
}
      
      





Ahora debe decidir cuántos objetos de imagen deben estar en la cadena de intercambio. La implementación especifica la cantidad mínima requerida para el trabajo:



uint32_t imageCount = swapChainSupport.capabilities.minImageCount;
      
      





Sin embargo, si solo usa este mínimo, a veces tendrá que esperar a que el controlador finalice las operaciones internas para obtener la siguiente imagen. Por tanto, es mejor solicitar al menos uno más del mínimo especificado:



uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1;
      
      





Es importante no exceder la cantidad máxima. Un valor 0



indica que no se especifica un máximo.



if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) {
    imageCount = swapChainSupport.capabilities.maxImageCount;
}
      
      





La cadena de intercambio es un objeto Vulkan, por lo que debe completar la estructura para crearla. El comienzo de la estructura ya nos es familiar:



VkSwapchainCreateInfoKHR createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
createInfo.surface = surface;
      
      





Primero, se especifica la superficie, a la que se adjunta la cadena de intercambio, luego - información para crear objetos de imagen:



createInfo.minImageCount = imageCount;
createInfo.imageFormat = surfaceFormat.format;
createInfo.imageColorSpace = surfaceFormat.colorSpace;
createInfo.imageExtent = extent;
createInfo.imageArrayLayers = 1;
createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
      
      





En imageArrayLayers



especifica el número de capas de las que consta cada imagen. Siempre habrá valor aquí 1



, a menos que, por supuesto, se trate de imágenes estéreo. El campo de bit imageUsage



indica para qué operaciones se utilizarán las imágenes obtenidas de la cadena de intercambio. En el tutorial, los renderizaremos directamente, pero primero puede renderizar en una imagen separada, por ejemplo, para el procesamiento posterior. En este caso, utilice el valor VK_IMAGE_USAGE_TRANSFER_DST_BIT



y, para la transferencia, utilice la operación de memoria.



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; // Optional
    createInfo.pQueueFamilyIndices = nullptr; // Optional
}
      
      





Luego, debe especificar cómo manejar los objetos de imágenes que se utilizan en varias familias de colas. Esto es cierto para los casos en los que la familia de gráficos y la familia de pantallas son familias diferentes. Procesaremos imágenes en la cola de gráficos y luego las enviaremos a la cola de visualización.



Hay dos formas de procesar imágenes con acceso desde varias colas:



  • VK_SHARING_MODE_EXCLUSIVE



    : un objeto pertenece a una familia de colas y la propiedad debe transferirse explícitamente antes de usarlo en otra familia de colas. Este método proporciona el mayor rendimiento.

  • VK_SHARING_MODE_CONCURRENT



    : Los objetos se pueden utilizar en varias familias de colas sin transferir explícitamente la propiedad.



Si tenemos varias colas, usaremos VK_SHARING_MODE_CONCURRENT



. Este método requiere que especifique de antemano entre qué familias de cola se compartirá la propiedad. Esto se puede hacer usando los parámetros queueFamilyIndexCount



y pQueueFamilyIndices



. Si la familia de colas de gráficos y la familia de colas de visualización son iguales, lo que es más común, utilice VK_SHARING_MODE_EXCLUSIVE



.



createInfo.preTransform = swapChainSupport.capabilities.currentTransform;
      
      





Puede especificar que las imágenes de la cadena de intercambio se apliquen con cualquiera de las transformaciones admitidas ( supportedTransforms



pulgadas capabilities



), por ejemplo, girar 90 grados en sentido horario o voltear horizontalmente. Para no aplicar ninguna transformación, simplemente déjelo currentTransform



.



createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
      
      





El campo compositeAlpha



indica si se debe usar el canal alfa para combinar con otras ventanas en el sistema de ventanas. Probablemente no necesite un canal alfa, así que déjelo VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR



.



createInfo.presentMode = presentMode;
createInfo.clipped = VK_TRUE;
      
      





El campo presentMode



habla por sí mismo. Si lo ponemos VK_TRUE



en el campo clipped



, entonces no nos interesan los píxeles ocultos (por ejemplo, si parte de nuestra ventana está cubierta por otra ventana). Siempre puede desactivar el recorte si necesita leer los píxeles, pero por ahora dejemos el recorte activado.



createInfo.oldSwapchain = VK_NULL_HANDLE;
      
      





El último campo permanece - oldSwapChain



. Si la cadena de intercambio deja de ser válida, por ejemplo, debido al cambio de tamaño de la ventana, será necesario volver a crearla desde cero y oldSwapChain



especificar en el campo un enlace a la cadena de intercambio anterior. Este es un tema complejo que cubriremos en un capítulo posterior. Por ahora, digamos que solo tenemos una cadena de intercambio.



Agreguemos un miembro de la clase para almacenar el objeto VkSwapchainKHR



:



VkSwapchainKHR swapChain;
      
      





Ahora solo necesita llamar vkCreateSwapchainKHR



para crear la cadena de intercambio:



if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) {
    throw std::runtime_error("failed to create swap chain!");
}
      
      





Los siguientes parámetros se pasan a la función: dispositivo lógico, información de la cadena de intercambio, un asignador personalizado opcional y un puntero para escribir el resultado. No hay sorpresas. La cadena de intercambio debe destruirse utilizando vkDestroySwapchainKHR



antes de que se destruya el dispositivo:



void cleanup() {
    vkDestroySwapchainKHR(device, swapChain, nullptr);
    ...
}
      
      





Ahora ejecutemos el programa para asegurarnos de que la cadena de intercambio se haya creado correctamente. Si recibe un mensaje de error o un mensaje como « vkGetInstanceProcAddress SteamOverlayVulkanLayer.dll»



, vaya a la sección de preguntas frecuentes .



Intentemos eliminar la línea createInfo.imageExtent = extent;



con las capas de validación habilitadas. Uno de los niveles de validación detectará inmediatamente el error y nos notificará:



imagen



Obtener una imagen de una cadena de intercambio



Ahora que se ha creado la cadena de intercambio, queda obtener los descriptores de VkImages . Agreguemos un miembro de la clase para almacenar descriptores:



std::vector<VkImage> swapChainImages;
      
      





Los objetos de imagen de la cadena de intercambio se destruirán automáticamente después de que se destruya la cadena de intercambio, por lo que no es necesario agregar ningún código de limpieza.



Inmediatamente después de la llamada, vkCreateSwapchainKHR



agregue el código para obtener los descriptores. Recuerde que hemos especificado solo el número mínimo de imágenes en la cadena de intercambio, lo que significa que puede haber más. Por lo tanto, primero solicitamos el número real de imágenes usando la función vkGetSwapchainImagesKHR



, luego asignamos el espacio necesario en el contenedor y lo volvemos vkGetSwapchainImagesKHR



a llamar para obtener los descriptores.



vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr);
swapChainImages.resize(imageCount);
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data());
      
      





Y lo último: guarde el formato y la resolución de las imágenes de la cadena de intercambio en variables de clase. Los necesitaremos en el futuro.



VkSwapchainKHR swapChain;
std::vector<VkImage> swapChainImages;
VkFormat swapChainImageFormat;
VkExtent2D swapChainExtent;

...

swapChainImageFormat = surfaceFormat.format;
swapChainExtent = extent;
      
      





Ahora tenemos una imagen para dibujar y mostrar. En el próximo capítulo, le mostraremos cómo configurar una imagen para usarla como destino de renderizado y comenzar con la canalización de gráficos y los comandos de dibujo.



C ++



All Articles