Contenido
1.
2.
3.
4.
5.
6. Uniform-
7.
8.
9.
10. -
11. Multisampling
FAQ
2.
3.
4.
-
-
- Window surface
- Swap chain
- Image views
- (pipeline)
5.
- Staging
6. Uniform-
- layout
- sets
7.
- Image view image sampler
- image sampler
8.
9.
10. -
11. Multisampling
FAQ
Cadena de intercambio
- Comprobación del soporte de la cadena de intercambio
- Conexión de extensiones
- Solicitar información sobre el soporte de la cadena de intercambio
- Elegir la configuración para la cadena de intercambio
- Creación de cadena de intercambio
- Obtener una imagen de una 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á:
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 ++