Escuela Técnica Superior de Ingeniería

 

Grado en Ingeniería Informática

Animación por Ordenador

Curso 2025/2026

 

Práctica 2e

Sincronización del proceso de dibujo

 

Objetivo

 

Añadir los métodos necesarios para enviar buffers de comandos (vacíos) a las colas de la GPU para generar las imágenes de la swapchain de manera sincronizada.

 

 

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. Como hemos comentado en proyectos anteriores, 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. Para sincronizar este proceso se utilizan los semáforos y las vallas que ya hemos generado.

El proceso de creación de la imagen tiene tres pasos: adquirir la imagen, generar la imagen y presentar la imagen. Para adquirir la imagen de una swapchain se utiliza la función vkAcquireNextImageKHR(). Esta función espera a que la siguiente imagen esté disponible para devolver el control almacenando en pImageIndex el índice de la imagen disponible.

VkResult vkAcquireNextImageKHR(
  VkDevice       device,
  VkSwapchainKHR swapchain,
  uint64_t       timeout,
  VkSemaphore    semaphore,
  VkFence        fence,
  uint32_t*      pImageIndex);

El campo timeout se refiere al tiempo máximo de espera para obtener la imagen.

El campo semaphore se refiere al semáforo que hay que esperar para poder renderizar en la imagen.

El campo fence indica la valla a abrir cuando sea posible renderizar la imagen.

Para generar la imagen hay que lanzar el buffer de comandos sobre una cola gráfica del dispositivo. Para esto se utiliza la función vkQueueSubmit().

VkResult vkQueueSubmit (
  VkQueue             queue,
  uint32_t            submitCount,
  const VkSubmitInfo* pSubmits,
  VkFence             fence);

El argumento fence contiene la valla que se señalará al terminar el proceso de ejecución del buffer de comandos. La estructura VkSubmitInfo describe la configuración del lanzamiento.

typedef struct VkSubmitInfo {
  VkStructureType             sType;
  const void*                 pNext;
  uint32_t                    waitSemaphoreCount;
  const VkSemaphore*          pWaitSemaphores;
  const VkPipelineStageFlags* pWaitDstStageMask;
  uint32_t                    commandBufferCount;
  const VkCommandBuffer*      pCommandBuffers;
  uint32_t                    signalSemaphoreCount;
  const VkSemaphore*          pSignalSemaphores;
} VkSubmitInfo;

El campo pCommandBuffers indica los buffers a lanzar. Se pueden lanzar varios buffers en la misma llamada.

El número de buffers se indica en commandBufferCount.

El campo pWaitSemaphores indica los semáforos a los que hay que esperar para lanzar el renderizado. Pueden indicarse varios semáforos.

El campo pSignalSemaphores indica los semáforos que hay que abrir al terminar el proceso de renderizado. Pueden indicarse varios semáforos.

Para terminar el proceso hay que presentar la imagen renderizada. Para esto se utiliza la función vkQueuePresentKHR(). La configuración de esta llamada se describe en la estructura VkPresentInfoKHR.

VkResult vkQueuePresentKHR(
  VkQueue                 queue,
  const VkPresentInfoKHR* pPresentInfo);

typedef struct VkPresentInfoKHR {
  VkStructureType       sType;
  const void*           pNext;
  uint32_t              waitSemaphoreCount;
  const VkSemaphore*    pWaitSemaphores;
  uint32_t              swapchainCount;
  const VkSwapchainKHR* pSwapchains;
  const uint32_t*       pImageIndices;
  VkResult*             pResults;
} } VkPresentInfoKHR;

El campo pWaitSemaphores contiene una lista de semáforos a los que hay que esperar para realizar la presentación.

El número de semáforos se indica en waitSemaphoreCount.

En una misma llamada se puede lanzar la presentación de varias imágenes. Esto puede ser util cuando una aplicación trabaja con varias ventanas. El campo pSwapchains contiene la lista de swapchains sobre las que presentar imágenes.

El campo pImageIndices contiene el índice de la imagen a presentar de cada swapchain.

El número de swapchains se indica en swapchainCount.

 

 

Modificaciones de la clase GEDrawingContext

 

El fichero de cabecera de la clase GEDrawingContext se ha modificado para añadir los nuevos métodos necesarios para la sincronización y el envío de los buffers de comandos. Se han incluido tres métodos públicos que serán llamados desde la función draw() de la aplicación para conseguir generar y mostrar las imágenes.

class GEDrawingContext
{
  ...

public:

  // Métodos de generación de la imagen
  void waitForNextImage(GEGraphicsContext* gc);
  void submitGraphicsCommands(GEGraphicsContext* gc, 
                              std::vector<VkCommandBuffer> commandBuffers);
  void submitPresentCommands(GEGraphicsContext* gc);

  ...
};

Los métodos añadidos son los siguientes:

  • waitForNextImage(GEGraphicsContext* gc): Espera a que la siguiente imagen a generar esté disponible, es decir, que la valla asociada a su presentación esté desactivada y el semáforo que indica que puede ser generada esté abierto.

  • submitGraphicsCommands(GEGraphicsContext* gc, std::vector<VkCommandBuffer> commandBuffers): Envía a la GPU los comandos para generar una imagen. Para ello tiene que esperar a que la imagen termine de ser presentada. Al enviar los comandos se desactiva el semáforo de imagen disponible para que no pueda sobreescribirse mientras no haya sido generada. Al terminar la ejecución de los comandos se debe activar el semáforo de imagen renderizada.

  • submitPresentCommands(GEGraphicsContext* gc): Envía a la GPU la orden de realizar la presentación de la imagen activa. La función debe esperar a que el semáforo que indica que la imagen ha sido renderizada esté activo.

A continuación se muestra el código de estos métodos.

///////////////////////////////////////////////////////////////////////////////////
/////                                                                         /////
/////                          Métodos públicos                               /////
/////                                                                         /////
///////////////////////////////////////////////////////////////////////////////////
 
//
// FUNCIÓN: GEDrawingContext::waitForNextImage(GEGraphicsContext* gc)
//
// PROPÓSITO: Espera hasta que la próxima imagen esté lista para ser generada
//
void GEDrawingContext::waitForNextImage(GEGraphicsContext* gc)
{
  vkWaitForFences(gc->device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX);

  uint32_t imageIndex;
  VkResult result = vkAcquireNextImageKHR(gc->device, 
                                          swapChain, 
                                          UINT64_MAX, 
                                          imageAvailableSemaphores[currentFrame], 
                                          VK_NULL_HANDLE, 
                                          &imageIndex);
  currentImage = imageIndex;

  if (result != VK_SUCCESS && 
      result != VK_SUBOPTIMAL_KHR && 
      result != VK_ERROR_OUT_OF_DATE_KHR)
  {
    throw std::runtime_error("failed to acquire swap chain image!");
  }
}

//
// FUNCIÓN: GEDrawingContext::submitGraphicsCommands(GEGraphicsContext* gc, 
//                                    std::vector<VkCommandBuffer> commandBuffers)
//
// PROPÓSITO: Envía los comandos gráficos al dispositivo
//
void GEDrawingContext::submitGraphicsCommands(GEGraphicsContext* gc, 
                                      std::vector<VkCommandBuffer> commandBuffers)
{
  if (imagesInFlight[currentImage] != VK_NULL_HANDLE)
  {
    vkWaitForFences(gc->device, 1, &imagesInFlight[currentImage],VK_TRUE,UINT64_MAX);
  }
  imagesInFlight[currentImage] = inFlightFences[currentFrame];

  VkSemaphore waitSemaphores[] = { imageAvailableSemaphores[currentFrame] };
  VkPipelineStageFlags waitStages[]={VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
  VkSemaphore signalSemaphores[] = { renderFinishedSemaphores[currentFrame] };

  VkSubmitInfo submitInfo = {};
  submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
  submitInfo.waitSemaphoreCount = 1;
  submitInfo.pWaitSemaphores = waitSemaphores;
  submitInfo.pWaitDstStageMask = waitStages;
  submitInfo.commandBufferCount = 1;
  submitInfo.pCommandBuffers = &commandBuffers[currentImage];
  submitInfo.signalSemaphoreCount = 1;
  submitInfo.pSignalSemaphores = signalSemaphores;

  vkResetFences(gc->device, 1, &inFlightFences[currentFrame]);
  
  VkResult result = vkQueueSubmit(graphicsQueue, 1, &submitInfo, 
                                                    inFlightFences[currentFrame]);

  if (result != VK_SUCCESS)
  {
    throw std::runtime_error("failed to submit draw command buffer!");
  }
}

//
// FUNCIÓN: GEDrawingContext::submitPresentCommands(GEGraphicsContext* gc)
//
// PROPÓSITO: Envía los comandos de presentación al dispositivo
//
void GEDrawingContext::submitPresentCommands(GEGraphicsContext* gc)
{
  VkSemaphore signalSemaphores[] = { renderFinishedSemaphores[currentFrame] };
  VkSwapchainKHR swapChains[] = { swapChain };

  VkPresentInfoKHR presentInfo = {};
  presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
  presentInfo.waitSemaphoreCount = 1;
  presentInfo.pWaitSemaphores = signalSemaphores;
  presentInfo.swapchainCount = 1;
  presentInfo.pSwapchains = swapChains;
  presentInfo.pImageIndices = &currentImage;

  VkResult result = vkQueuePresentKHR(presentQueue, &presentInfo);

  if (result != VK_ERROR_OUT_OF_DATE_KHR && 
      result != VK_SUBOPTIMAL_KHR && 
      result != VK_SUCCESS)
  {
    throw std::runtime_error("failed to present swap chain image!");
  }

  currentFrame = (currentFrame + 1) % frameCount;
}

 

 

Modificaciones de la clase GEApplication

 

Para lanzar la generación de la imagen es necesario darle contenido al método draw() de la clase GEApplication que se había dejado vacío hasta este momento. Tal y como se ha explicado, el proceso de dibujo tiene tres fases. En primer lugar se espera hasta que la siguiente imagen de la swapchain esté disponible para ser generada. En segundo lugar se envían el buffer de comandos a la cola gráfica del dispositivo para que se genere la imagen. Por último se envían los comandos de presentación a la cola de presentación del dispositivo para que la imagen se muestre. Queda por desarrollar el contenido de los buffers de comandos para poder renderizar las imágenes.

//
// FUNCIÓN: GEApplication::draw()
//
// PROPÓSITO: Lanza la generación del dibujo
//
void GEApplication::draw()
{
  vulkan->waitForNextImage();
  // TODO: Rellenar el buffer de comandos
  vulkan->submitGraphicsCommands();
  vulkan->submitPresentCommands();
}

 

 

Aspecto final

 

En la consola se ofrece el mensaje de que los buffers de comandos (vacíos) ya se están enviando. Se puede observar que el fondo de la ventana aparece ahora en negro. Esto quiere decir que el contenido de la ventana se está tomando ya de las imágenes de la swapchain, pero todavía no hemos generado las imágenes.

Captura 5