Escuela Técnica Superior de Ingeniería

 

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.

Figura1