|
Grado en Ingeniería Informática
Animación por Ordenador
Curso 2025/2026
|
Práctica 2a
|
|
Creación de la cadena de intercambio
|
|
Objetivos
|
|
Añadir al proyecto la creación de la cadena de intercambio de
imágenes.
|
|
Swapchain
|
|
Respecto a la imagen a mostrar en una superficie, Vulkan ha sustituido el concepto
de doble buffer utilizado por OpenGL por el de una cadena de intercambio de imágenes
o swapchain. En un doble buffer existe un buffer de trabajo sobre el que se genera
la nueva imagen a mostrar y un buffer activo con la imagen que se está mostrando.
Una swapchain contiene múltiples imágenes, de manera que mientras una de ellas se
encuentra en modo presentación Vulkan permite trabajar en paralelo con el resto.
Mientras que OpenGL generaba el doble buffer de forma automática, en Vulkan es
necesario programar de forma detallada la construcción de la swapchain.
Para utilizar swapchains hay que generar el dispositivo lógico con la extensión
VK_KHR_swapchain. Al crear la estructura VkSwapchainKHR se construyen también los
buffers de las imágenes que la forman.
VkResult vkCreateSwapchainKHR(
VkDevice device,
const VkSwapchainCreateInfoKHR* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkSwapchainKHR* pSwapchain);
|
Para crear la cadena de intercambio hay que especificar su
configuración con una estrutura VkSwapchainCreateInfoKHR.
typedef struct VkSwapchainCreateInfoKHR {
VkStructureType sType;
const void* pNext;
VkSwapchainCreateFlagsKHR flags;
VkSurfaceKHR surface;
uint32_t minImageCount;
VkFormat imageFormat;
VkColorSpaceKHR imageColorSpace;
VkExtent2D imageExtent;
uint32_t imageArrayLayers;
VkImageUsageFlags imageUsage;
VkSharingMode imageSharingMode;
uint32_t queueFamilyIndexCount;
const uint32_t* pQueueFamilyIndices;
VkSurfaceTransformFlagBitsKHR preTransform;
VkCompositeAlphaFlagBitsKHR compositeAlpha;
VkPresentModeKHR presentMode;
VkBool32 clipped;
VkSwapchainKHR oldSwapchain;
} VkSwapchainCreateInfoKHR;
|
En la estructura anterior los campos pueden tomar los siguientes
valores:
-
El valor del campo
sType debe ser
VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR.
-
Los campos
pNext y flags
están reservados para posibles modificaciones futuras. De momento deben
asignarse a nullptr y cero, respectivamente.
-
El campo
surface se refiere
a la superficie sobre la que presentaremos las imágenes.
-
El campo
minImageCount contiene el número de imágenes en la swapchain. Para saber
si el dispositivo físico permite utilizar un cierto número de imágenes
se pueden consultar sus propiedades con la función
vkGetPhysicalDeviceSurfaceCapabilitiesKHR().
-
El campo imageFormat describe el formato en que se almacenan los píxeles de la imagen.
Existen numerosos formatos compatibles con Vulkan, por ejemplo VK_FORMAT_B8G8R8A8_UNORM.
-
El campo imageColorSpace solo admite de momento el valor VK_COLOR_SPACE_SRGB_NONLINEAR_KHR.
-
El campo imageExtent contiene el tamaño de las imágenes.
-
El campo
imageArrayLayers el
número de capas. En Vulkan las imágenes son siempre tridimensionales, aunque generalmente
se utilice una única capa (o dos, para imágenes estereoscópicas).
-
El campo
imageUsage define
el uso que se le va a dar a las imágenes de la swapchain. Lo más frecuente es indicar que
se va a utilizar para describir color, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT.
-
El campo imageSharingMode sirve para definir si las imágenes se van a gestionar desde una
única cola (VK_SHARING_MODE_EXCLUSIVE) o si se va a utilizar una cola para generar la imagen
y otra para presentarla (VK_SHARING_MODE_CONCURRENT).
-
Los campos queueFamilyIndexCount y
pQueueFamilyIndices sirven para introducir las familias de colas que van acceder a las imágenes.
Lo normal es que el solo haya una familia de colas y que el modo sea exclusivo.
-
El campo
preTransform sirve para configurar una transformación de las imágenes (rotación de 90º, de 180º,
de 270º, voltear horizontalmente, voltear y rotar, …). Las opciones se definen en la enumeración
VkSurfaceTransformFlagBitsKHR.
-
El campo compositeAlpha indica como tratar las transparencias en la imagen. De esta forma se
pueden presentar imágenes en ventanas que mantengan transparencias. Si queremos un tratamiento opaco
el valor debe ser VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR.
-
El campo presentMode fija el modo de presentación,
que puede ser: VK_PRESENT_MODE_IMMEDIATE_KHR (las imágenes se muestran de manera inmediata, lo que puede
generar tearing, es decir, imágenes troceadas), VK_PRESENT_MODE_FIFO_KHR (las imágenes completadas se
introducen en una cola. Si la cola se llena y no se han mostrado las imágenes la aplicación debe pararse.
Si la cola se vacía la última imagen se repite), VK_PRESENT_MODE_FIFO_RELAXED_KHR (si la cola se vacía,
la nueva imagen se muestra en el momento en que esté lista, lo que puede provocar tearing) o
VK_PRESENT_MODE_MAILBOX_KHR (si la cola se llena la aplicación machaca la última imagen de la cola).
-
El campo clipped indica como debe comportarse la
aplicación cuando está oculta, por ejemplo porque exista otra
ventana que la esté tapando. Si este campo es VK_TRUE significa
que no importa el color de los pixeles ocultos.
-
El último campo, oldSwapchain, permite almacenar la
swapchain anterior cuando se sustituye por una nueva (por
ejemplo, cuando se modifica el tamaño de la ventana).
Al liberar los recursos de la aplicación es importante destruir la
swapchain por medio de la función
vkDestroySwapchainKHR(). Esto destruye también las imágenes
contenidas en la estructura.
void vkDestroySwapchainKHR(
VkDevice device,
VkSwapchainKHR swapchain,
const VkAllocationCallbacks* pAllocator);
|
|
|
La clase
GEDrawingContext
|
|
Para almacenar todos los componentes relacionados con el proceso de
dibujo en la superficie se va a añadir una nueva clase a la
aplicación gráfica. Esta clase se denomina GEDrawingContext.
Esta clase contiene el campo swapChain, que alkmacena la
cadena de iintercambio, así como campos dedicados a almacenar la
lista de imágenes de la cadena de intercambio (images), el
número de imágenes de lla cadena (imageCount), el
formato de las imágenes (imageFormat) y el tamaño de las
imágenes (imageExtent).
#pragma once
#include <vulkan/vulkan.h>
#include <vector>
#include "GEGraphicsContext.h"
#include "GEWindowPosition.h"
class GEDrawingContext
{
private:
// Campos auxiliares
uint32_t imageCount;
// Componentes gráficos
VkSwapchainKHR swapChain;
VkFormat imageFormat;
VkExtent2D imageExtent;
std::vector<VkImage> images;
public:
GEDrawingContext(GEGraphicsContext* gc, GEWindowPosition wpos);
void destroy(GEGraphicsContext* gc);
void recreate(GEGraphicsContext* gc, GEWindowPosition wpos);
VkFormat getFormat();
VkExtent2D getExtent();
uint32_t getImageCount();
private:
// Métodos de creación de componentes
void createSwapChain(GEGraphicsContext* gc, GEWindowPosition wpos);
// Métodos auxiliares
VkSurfaceFormatKHR chooseSwapSurfaceFormat(
const std::vector<VkSurfaceFormatKHR>& availableFormats);
VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities,
GEWindowPosition wpos);
};
|
Los métodos de la clase son los siguientes:
-
GEDrawingContext(GEGraphicsContext* gc, GEWindowPosition
wpos): Constructor de la clase. Crea la cadena de
intercambio y asigna los valores del resto de campos. El código
intenta crear una cadena con 4 imágenes, pero se adapta a otra
cantidad si las capacidades del dispositivo no lo permite.
-
destroy(GEGraphicsContext* gc): Destruye los
compoenentes de la clase.
-
recreate(GEGraphicsContext* gc, GEWindowPosition wpos):
Reconstruye los componentes de la clase. Este método se
utilizará cuando se modifique el tamaño de la superficie.
-
getFormat(): Obtiene el formato utilizado por las
imágenes de la cadena de intercambio.
-
getExtent(): Obtiene el tamaño de las imágenes de la
cadena de intercambio.
-
getImageCount(): Obtiene el número de imágenes de la
cadena de intercambio creada.
-
createSwapChain(GEGraphicsContext* gc, GEWindowPosition
wpos): Crea la cadena de intercambio. Esto crea también el
conjunto de imágenes.
-
chooseSwapSurfaceFormat(...): Elige el formato a
utilizar en las imágenes de entre los formatos soportados por la
superficie.
-
chooseSwapExtent(...):
Elige el tamaño de las imágenes. Se intenta utilizar el tamaño
de la superficie, pero si no es posible se adapta a las
capacidades del dispositivo.
A continuación se muestra el código de estos métodos.
#include "GEDrawingContext.h"
#include "GEGraphicsContext.h"
#include <glm/common.hpp>
#include <iostream>
#include <vector>
///////////////////////////////////////////////////////////////////////////////////
///// /////
///// Métodos públicos /////
///// /////
///////////////////////////////////////////////////////////////////////////////////
//
// FUNCIÓN: GEDrawingContext::GEDrawingContext(...)
//
// PROPÓSITO: Crea el contexto de dibujo (imágenes y cadena de intercambio)
//
GEDrawingContext::GEDrawingContext(GEGraphicsContext* gc, GEWindowPosition wpos)
{
createSwapChain(gc, wpos);
std::cout << "Swapchain created! Image count: " << imageCount << std::endl;
}
//
// FUNCIÓN: GEDrawingContext::destroy(GEGraphicsContext* gc)
//
// PROPÓSITO: Destruye los componentes del contexto de dibujo
//
void GEDrawingContext::destroy(GEGraphicsContext* gc)
{
vkDestroySwapchainKHR(gc->device, swapChain, nullptr);
}
//
// FUNCIÓN: GEDrawingContext::recreate(GEGraphicsContext* gc, GEWindowPosition wpos)
//
// PROPÓSITO: Reconstruye los componentes del contexto de dibujo
//
void GEDrawingContext::recreate(GEGraphicsContext* gc, GEWindowPosition wpos)
{
vkDestroySwapchainKHR(gc->device, swapChain, nullptr);
createSwapChain(gc, wpos);
}
//
// FUNCIÓN: GEDrawingContext::getFormat()
//
// PROPÓSITO: Obtiene el formato de las imágenes
//
VkFormat GEDrawingContext::getFormat()
{
return imageFormat;
}
//
// FUNCIÓN: GEDrawingContext::getExtent()
//
// PROPÓSITO: Obtiene el tamaño de las imágenes
//
VkExtent2D GEDrawingContext::getExtent()
{
return imageExtent;
}
//
// FUNCIÓN: GEDrawingContext::getImageCount()
//
// PROPÓSITO: Obtiene el número de imágenes
//
uint32_t GEDrawingContext::getImageCount()
{
return imageCount;
}
///////////////////////////////////////////////////////////////////////////////////
///// /////
///// Métodos de creación de los componentes /////
///// /////
///////////////////////////////////////////////////////////////////////////////////
//
// FUNCIÓN: GEDrawingContext::createSwapChain(...)
//
// PROPÓSITO: Crea los buffers de intercambio de imágenes, el vector de imágenes
// y sus formatos y tamaños
//
void GEDrawingContext::createSwapChain(GEGraphicsContext* gc, GEWindowPosition wpos)
{
VkSurfaceCapabilitiesKHR capabilities;
std::vector<VkSurfaceFormatKHR> formats;
std::vector<VkPresentModeKHR> presentModes;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(gc->physicalDevice, gc->surface,
&capabilities);
uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(gc->physicalDevice, gc->surface,
&formatCount, nullptr);
if (formatCount != 0)
{
formats.resize(formatCount);
vkGetPhysicalDeviceSurfaceFormatsKHR(gc->physicalDevice, gc->surface,
&formatCount, formats.data());
}
VkSurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(formats);
VkExtent2D extent = chooseSwapExtent(capabilities, wpos);
imageCount = 4;
if (capabilities.maxImageCount > 0 && imageCount > capabilities.maxImageCount)
{
imageCount = capabilities.maxImageCount;
}
VkSwapchainCreateInfoKHR createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
createInfo.surface = gc->surface;
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;
if (gc->graphicsQueueFamilyIndex != gc->presentQueueFamilyIndex)
{
std::vector<uint32_t> queueFamilyIndices;
queueFamilyIndices.resize(2);
queueFamilyIndices.push_back(gc->graphicsQueueFamilyIndex);
queueFamilyIndices.push_back(gc->presentQueueFamilyIndex);
createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
createInfo.queueFamilyIndexCount = 2;
createInfo.pQueueFamilyIndices = queueFamilyIndices.data();
}
else
{
std::vector<uint32_t> queueFamilyIndices;
queueFamilyIndices.resize(1);
queueFamilyIndices.push_back(gc->graphicsQueueFamilyIndex);
createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
createInfo.queueFamilyIndexCount = 1;
createInfo.pQueueFamilyIndices = queueFamilyIndices.data();
}
createInfo.preTransform = capabilities.currentTransform;
createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
createInfo.presentMode = VK_PRESENT_MODE_FIFO_KHR;
createInfo.clipped = VK_TRUE;
if(vkCreateSwapchainKHR(gc->device, &createInfo, nullptr, &swapChain)!=VK_SUCCESS)
{
throw std::runtime_error("failed to create swap chain!");
}
vkGetSwapchainImagesKHR(gc->device, swapChain, &imageCount, nullptr);
images.resize(imageCount);
vkGetSwapchainImagesKHR(gc->device, swapChain, &imageCount, images.data());
imageFormat = surfaceFormat.format;
imageExtent = extent;
}
///////////////////////////////////////////////////////////////////////////////////
///// /////
///// Métodos auxiliares /////
///// /////
///////////////////////////////////////////////////////////////////////////////////
//
// FUNCIÓN: GEDrawingContext::chooseSwapSurfaceFormat(...)
//
// PROPÓSITO: Escoge el formato de imagen entre los soportados por la superficie
//
VkSurfaceFormatKHR GEDrawingContext::chooseSwapSurfaceFormat(
const std::vector<VkSurfaceFormatKHR>& availableFormats)
{
for (const VkSurfaceFormatKHR& availableFormat : availableFormats)
{
if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB &&
availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)
{
return availableFormat;
}
}
return availableFormats[0];
}
//
// FUNCIÓN: GEDrawingContext::chooseSwapExtent(...)
//
// PROPÓSITO: Escoge el tamaño de las imágenes asegurando que puede ser soportado
// por la superficie
//
VkExtent2D GEDrawingContext::chooseSwapExtent(
const VkSurfaceCapabilitiesKHR& capabilities,
GEWindowPosition wpos)
{
if (capabilities.currentExtent.width != UINT32_MAX)
{
return capabilities.currentExtent;
}
else
{
VkExtent2D actualExtent = { };
if (wpos.fullScreen)
{
actualExtent.width = wpos.screenWidth;
actualExtent.height = wpos.screenHeight;
}
else
{
actualExtent.width = wpos.width;
actualExtent.height = wpos.height;
}
actualExtent.width = glm::clamp(actualExtent.width,
capabilities.minImageExtent.width,
capabilities.maxImageExtent.width);
actualExtent.height = glm::clamp(actualExtent.height,
capabilities.minImageExtent.height,
capabilities.maxImageExtent.height);
return actualExtent;
}
}
|
|
|
Modificaciones
de la clase GEApplication
|
|
La clase GEApplication se ha modificado para añadir el campo
dc, que
contiene un objeto GEDrawingContext con la cadena de
intercambio y componentes asociados.
//
// CLASE: GEApplication
//
// DESCRIPCIÓN: Clase que crea y lanza la aplicación gráfica.
//
class GEApplication
{
public:
void run();
private:
GLFWwindow* window;
GEWindowPosition windowPos;
GEGraphicsContext* gc;
GEDrawingContext* dc;
...
};
|
El método run() incluye ahora la creación del contexto de
dibujo y el método cleanup() su destrucción.
//
// FUNCIÓN: GEApplication::run()
//
// PROPÓSITO: Ejecuta la aplicación
//
void GEApplication::run()
{
this->window = initWindow();
this->windowPos = initWindowPos();
this->gc = new GEGraphicsContext(window);
this->dc = new GEDrawingContext(this->gc, this->windowPos);
mainLoop();
cleanup();
}
//
// FUNCIÓN: GEApplication::cleanup()
//
// PROPÓSITO: Libera los recursos y finaliza la aplicación
//
void GEApplication::cleanup()
{
dc->destroy(gc);
delete dc;
delete gc;
glfwDestroyWindow(window);
glfwTerminate();
}
|
|
|
Aspecto final
|
|
El
aspecto de la aplicación sigue siendo una ventana vacía con una
consola. En este caso en la consola se muestra el mensaje del número
de imágenes que forman la cadena de intercambio.
|
|