Escuela Técnica Superior de Ingeniería

 

Grado en Ingeniería Informática

Animación por Ordenador

Curso 2025/2026

 

Práctica 2c

Sincronización del proceso de renderizado

 

Objetivo

 

Añadir semáforos y vallas (fences) al swapchain para sincronizar el proceso de generacioón de imágenes y crear las colas (queues) para enviar los comandos de generación y presentación de las imágenes.

 

 

La sincronización del proceso de renderizado

 

Para generar cada una de las imágenes del swapchain hay que lanzar la ejecución de un buffer de comandos asociado a cada una de ellas.

La ejecución de un buffer de comandos es un proceso asíncrono. Esto quiere decir que la función de lanzamiento devuelve el control sin que realmente el proceso de renderizado se haya completado. El problema es que no podemos lanzar un proceso de renderizado sobre una imagen sin asegurarnos de que el renderizado anterior haya finalizado, es decir, hay que incluir mecanismos de sincronización. En Vulkan esta sincronización es responsabilidad del programador.

Hay dos formas de sincronizar eventos de la swapchain: semáforos (semaphores) y vallas (fences). Un semáforo permite definir puntos de sincronización internos en el dispositivo lógico. Esto permite sincronizar los procesos que se ejecutan de manera asíncrona en la GPU. Las vallas permiten definir puntos de sincronización entre la GPU y la CPU.

Para representar una imagen sobre una superficie la GPU debe realizar dos pasos: generar la imagen y presentarla en la superficie. Para sincronizar esto se definen dos semáforos, uno para indicar que la imagen está disponible para renderizar sobre ella y otro para indicar que la imagen está renderizada y está disponible para presentarla.

Para generar un semáforo se utiliza la función vkCreateSemaphore(). La estructura VkSemaphoreCreateInfo solo tiene los campos sType, pNext y flags que por el momento no tienen contenido

VkResult vkCreateSemaphore (
  VkDevice                     device,
  const VkSemaphoreCreateInfo* pCreateInfo,
  const VkAllocationCallbacks* pAllocator,
  VkSemaphore* 			pSemaphore);
  
typedef struct VkSemaphoreCreateInfo {
  VkStructureType        sType;
  const void*            pNext;
  VkSemaphoreCreateFlags flags;
} VkSemaphoreCreateInfo;

Para sincronizar la presentación de imágenes sobre la swapchain se utilizan vallas (fences). Solo se va a lanzar un proceso de presentación de una imagen si su valla está abierta. Solo abrimos la valla cuando hemos terminado de presentar la imagen en una pasada anterior.

Para crear una valla se utiliza la función vkCreateFence(). La estructura VkFenceCreateInfo por el momento solo tiene los campos sType, pNext y flags sin contenido.

VkResult vkCreateFence (
  VkDevice                     device,
  const VkFenceCreateInfo*     pCreateInfo,
  const VkAllocationCallbacks* pAllocator,
  VkFence*                     pFence);
  
typedef struct VkFenceCreateInfo {
  VkStructureType    sType;
  const void*        pNext;
  VkFenceCreateFlags flags;
} VkFenceCreateInfo;

El proceso gráfico necesita generar las imágenes (un proceso interno de la GPU) y presentarlas en la superficie (un proceso de comunicación entre la GPU y la CPU). Estos procesos se realizan enviando buffers de comandos a una cola de la GPU. Se necesitan, por tanto, dos colas diferentes: una para enviar la orden de generar la imagen y otra para enviar la orden de presentar la imagen. Las colas se obtienen del dispositivo mediante la función vkGetDeviceQueue().

VkResult vkGetDeviceQueue(
  VkDevice       device,
  uint32_t       queueFamilyIndex,
  uint32_t       queueIndex,
  VkQueue*       pQueue);

 

 

Modificaciones de la clase GEDrawingContext

 

Los semáforos, las vallas y las colas se van a definir como campos de la clase GEDrawingContext. Para completar el proceso de sincronización se han añadido, además, algunos campos auxiliares.  El número de imágenes que se van a tratar de forma sincronizada es frameCount. Este parámetro se utilizará para crear el número adecuado de semáforos y vallas. Los nuevos campos de la clase son los sighuientes:

  • imageAvailableSemaphores:  Vector de semáforos utilizados para indicar que las imágenes de la swapchain están  disponibles para ser generadas. El número de semáforos es  frameCount.

  • renderFinishedSemaphores: Vector de semáforos utilizados para indicar que las imágenes de la swapchain están generadas y pendientes de presentación. El número de semáforos es  frameCount.

  • inFlightFences: Vector de vallas utilizadas para indicar que las imágenes estámn preparadas para ser presentadas. El número de vallas es  frameCount.

  • imagesInFlight: Vector de vallas utilizado para relacionar cada imagen con la valla asociada en cada momento. El número de elenentos es imageCount. Para asociar una valla a una imagen se almacena la valla (elemento de inFlightFences) en la posición de imagesInFlight correspondiente a la imagen de la swapchain.

  • frameCount: Número de imágenes a tratar en paralelo. Se va a tomar como (imageCount -1).

  • currentFrame: Índice de los semáforos y vallas a utilizar con la imagen a generar.

  •  currentImage: Índice de la imagen de la swapchain a generar.

  • graphicsQueue: Cola utilizada para enviar comandos de generación de imágenes.

  • presentQueue: Cola utilizada para enviar comandos de presentación de imágenes.

El fichero de cabecera queda así.

#pragma once

#include <vulkan/vulkan.h>
#include <vector>
#include "GEGraphicsContext.h"
#include "GEWindowPosition.h"

class GEDrawingContext
{
public:
  std::vector<VkImageView> imageViews;

private:
  // Campos auxiliares
  uint32_t imageCount;
  uint32_t frameCount;
  size_t currentFrame = 0;
  uint32_t currentImage = 0;

  // Componentes gráficos
  VkSwapchainKHR swapChain;
  VkFormat imageFormat;
  VkExtent2D imageExtent;
  std::vector<VkImage> images;
  VkQueue graphicsQueue;
  VkQueue presentQueue;

  // Sincronización entre imágenes
  std::vector<VkSemaphore> imageAvailableSemaphores;
  std::vector<VkSemaphore> renderFinishedSemaphores;
  std::vector<VkFence> inFlightFences;
  std::vector<VkFence> imagesInFlight;

public:
  GEDrawingContext(GEGraphicsContext* gc, GEWindowPosition wpos);
  void destroy(GEGraphicsContext* gc);
  void recreate(GEGraphicsContext* gc, GEWindowPosition wpos);
  VkFormat getFormat();
  VkExtent2D getExtent();
  uint32_t getImageCount();
  uint32_t getCurrentImage();

private:
  // Métodos de creación de componentes
  void createSwapChain(GEGraphicsContext* gc, GEWindowPosition wpos);
  void createImageViews(VkDevice device);
  void createSyncObjects(VkDevice device);
  void createQueues(GEGraphicsContext* gc);

  // Métodos auxiliares
  VkSurfaceFormatKHR chooseSwapSurfaceFormat(
                       const std::vector<VkSurfaceFormatKHR>& availableFormats);
  VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities, 
                              GEWindowPosition wpos);
};

El código de GEDrawingContext incluye modificaciones en el constructor para construir los semáforos, vallas y colas necesarios para la sincronización y en el destructor para añadir la destrucción de los semáforos, vallas y colas creadas, así como los métodos específicos para crear estos componentes.

///////////////////////////////////////////////////////////////////////////////////
/////                                                                         /////
/////                          Métodos públicos                               /////
/////                                                                         /////
///////////////////////////////////////////////////////////////////////////////////

//
// FUNCIÓN: GEDrawingContext::GEDrawingContext(GEGraphicsContext* gc)
//
// PROPÓSITO: Crea el contexto de dibujo (imágenes y cadena de intercambio)
//
GEDrawingContext::GEDrawingContext(GEGraphicsContext* gc, GEWindowPosition wpos)
{
  createSwapChain(gc, wpos);
  createImageViews(gc->device);
  createSyncObjects(gc->device);
  createQueues(gc);

  std::cout << "Semaphores, fences and queues created!" << std::endl;
}

//
// FUNCIÓN: GEDrawingContext::destroy(GEGraphicsContext* gc)
//
// PROPÓSITO: Destruye los componentes del contexto de dibujo
//
void GEDrawingContext::destroy(GEGraphicsContext* gc)
{
  for (size_t i = 0; i < frameCount; i++)
  {
    vkDestroySemaphore(gc->device, renderFinishedSemaphores[i], nullptr);
    vkDestroySemaphore(gc->device, imageAvailableSemaphores[i], nullptr);
    vkDestroyFence(gc->device, inFlightFences[i], nullptr);
  }

  for (uint32_t i = 0; i < imageCount; i++) 
  {
    vkDestroyImageView(gc->device, imageViews[i], nullptr);
  }
  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)
{
  for (uint32_t i = 0; i < imageCount; i++)
  {
    vkDestroyImageView(gc->device, imageViews[i], nullptr);
  }
  vkDestroySwapchainKHR(gc->device, swapChain, nullptr);

  createSwapChain(gc, wpos);
  createImageViews(gc->device);
}

//
// FUNCIÓN: GEDrawingContext::getCurrentImage()
//
// PROPÓSITO: Obtiene el índice de la imagen a generar
//
uint32_t GEDrawingContext::getCurrentImage()
{
  return currentImage;
}

///////////////////////////////////////////////////////////////////////////////////
/////                                                                         /////
/////               Métodos de creación de los componentes                    /////
/////                                                                         /////
///////////////////////////////////////////////////////////////////////////////////

//
// FUNCIÓN: GEDrawingContext::createSyncObjects(VkDevice device)
//
// PROPÓSITO: Crea los semáforos y los fences para no sobreescribir las imágenes
//
void GEDrawingContext::createSyncObjects(VkDevice device)
{
  frameCount = imageCount - 1;
  imageAvailableSemaphores.resize(frameCount);
  renderFinishedSemaphores.resize(frameCount);
  inFlightFences.resize(frameCount);
  imagesInFlight.resize(images.size(), VK_NULL_HANDLE);

  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 < frameCount; i++)
  {
    if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, 
                                  &imageAvailableSemaphores[i]) != VK_SUCCESS ||
        vkCreateSemaphore(device, &semaphoreInfo, nullptr, 
                                  &renderFinishedSemaphores[i]) != VK_SUCCESS ||
        vkCreateFence(device, &fenceInfo, nullptr, &inFlightFences[i]) != VK_SUCCESS)
    {
      throw std::runtime_error("failed to create synchronization objects!");
    }
  }
}

//
// FUNCIÓN: GEDrawingContext::createQueues(GEGraphicsContext* gc)
//
// PROPÓSITO: Obtiene las colas del dispositivo para enviar los comandos de 
//            generación y presentación de los gráficos
//
void GEDrawingContext::createQueues(GEGraphicsContext* gc)
{
  vkGetDeviceQueue(gc->device, gc->graphicsQueueFamilyIndex, 0, &graphicsQueue);
  vkGetDeviceQueue(gc->device, gc->presentQueueFamilyIndex, 0, &presentQueue);
}

 

 

Aspecto final

 

El aspecto de la aplicación sigue siendo una ventana vacía. En la consola se ofrece el mensaje de creación de los semáforos, vallas y colas.

Figura3