Escuela Técnica Superior de Ingeniería

 

Grado en Ingeniería Informática

Animación por Ordenador

Curso 2025/2026

 

Práctica 1c

Creación de una instancia de Vulkan

 

Objetivos

 

Añadir al proyecto una instancia de Vulkan con las extensiones requeridas por GLFW y sin capa de validación.

 

 

Instancias, capas y extensiones

 

Una instancia es una estructura que inicializa la biblioteca de Vulkan para poder ejecutar sus funciones en una aplicación. Básicamente es una estructura que almacena los punteros a las funciones de la biblioteca. El uso de instancias permite personalizar el entorno de ejecución de Vulkan por medio de dos características: capas y extensiones. Las capas permiten sustituir las versiones de las funciones, por ejemplo sustituyendo funciones muy rápidas que no realizan comprobaciones por versiones más lentas que si las introducen. Las extensiones permiten añadir nuevas funciones al conjunto básico de Vulkan.

Para crear una instancia se utiliza el siguiente método:

VkResult vkCreateInstance (
  const VkInstanceCreateInfo*   pCreateInfo,
  const VkAllocationCallbacks*  pAllocator,
  VkInstance*                   pInstance);

Vulkan permite utilizar gestores de memoria dinámica propios. De esta forma, la creación y liberación de estructuras se puede personalizar por medio de una estructura VkAllocationCallbacks. En las funciones de la biblioteca que admiten este campo se puede introducir un valor nulo (nullptr) para indicar que se utilice el gestor de memoria por defecto. 

typedef struct VkAllocationCallbacks {
  void*                                pUserData;
  PFN_vkAllocationFunction             pfnAllocation;
  PFN_vkReallocationFunction           pfnReallocation;
  PFN_vkFreeFunction                   pfnFree;
  PFN_vkInternalAllocationNotification pfnInternalAllocation;
  PFN_vkInternalFreeNotification       pfnInternalFree;
} VkAllocationCallbacks;

La estructura  VkInstanceCreateInfo permite definir las características de la instancia que se desea crear. Casi todas las estructuras de Vulkan comienzan con un campo llamado sType que identifica el tipo de estructura. En este caso el valor que debe tener este campo es VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO. Los campos pNext y flags son también comunes a muchas estructuras de Vulkan y están pensados para posibles ampliaciones futuras por lo que se suelen dejar nulos. El campo pApplicationInfo contiene información sobre la aplicación que estamos programando. Los campos enabledLayerCount y ppEnabledLayerNames describen el número y los nombres de las capas que se quieren activar en la instancia. Los campos enabledExtensionCount y ppEnabledExtensionNames describen el número y los nombres de las extensiones a incluir en la instancia.

typedef struct VkInstanceCreateInfo {
  VkStructureType          sType;
  const void*              pNext;
  VkInstanceCreateFlags    flags;
  const VkApplicationInfo* pApplicationInfo;
  uint32_t                 enabledLayerCount;
  const char* const*       ppEnabledLayerNames;
  uint32_t                 enabledExtensionCount;
  const char* const*       ppEnabledExtensionNames;
} VkInstanceCreateInfo;

La estructura VkApplicationInfo permite describir la aplicación. El campo sType de esta estructura debe contener el valor VK_STRUCTURE_TYPE_APPLICATION_INFO. El resto de campos son de texto libre para indicar el nombre de la aplicación y la versión.

typedef struct VkApplicationInfo {
  VkStructureType sType;
  const void*     pNext;
  const char*     pApplicationName;
  uint32_t        applicationVersion;
  const char*     pEngineName;
  uint32_t        engineVersion;
  uint32_t        apiVersion;
} VkApplicationInfo;

Para conocer cuales son las capas disponibles a la hora de crear una instancia se utiliza el método vkEnumerateInstanceLayerProperties(). Generalmente se ejecuta en dos pasos. En el primer paso el argumento pProperties se deja a nulo y la función devuelve en pPropertyCount el número de capas disponibles. Esto permite reservar la memoria suficiente para almacenar la información. En la segunda ejecución se introduce el número de capas a leer y el array de estructuras para almacenar sus propiedades.

VkResult vkEnumerateInstanceLayerProperties (
  uint32_t*          pPropertyCount,
  VkLayerProperties* pProperties);

Las propiedades de las capas se describen en estructuras VkLayerProperties. El primer campo de la estructura contiene el nombre de la capa que se podrá utilizar en el proceso de creación de la instancia.

typedef struct VkLayerProperties {
  char     layerName[VK_MAX_EXTENSION_NAME_SIZE];
  uint32_t specVersion;
  uint32_t implementationVersion;
  char     description[VK_MAX_DESCRIPTION_SIZE];
} VkLayerProperties;

Para obtener las extensiones disponibles se utiliza el método vkEnumerateInstanceExtensionProperties() que lee las extensiones asociadas a cada capa. Si el parámetro pLayerName se deja a nulo se leen las extensiones independientes de las capas. De nuevo se trata de una función que se ejecuta en dos pasos, el primero para conocer el número de extensiones y el segundo para leer realmente la lista de propiedades.

VkResult vkEnumerateInstanceExtensionProperties (
  const char*            pLayerName,
  uint32_t*              pPropertyCount,
  VkExtensionProperties* pProperties);

La estructura VkExtensionProperties contiene simplemente el nombre de la extensión, que se utiliza en la creación de las instancias, y la versión.

typedef struct VkExtensionProperties {
  char     extensionName[VK_MAX_EXTENSION_NAME_SIZE];
  uint32_t specVersion;
} VkExtensionProperties;

Como las extensiones amplían las funciones disponibles en las instancias, no podemos saber a priori cuales son estas funciones disponibles. Para poder ejecutarlas se obtiene el puntero a la función a partir de su nombre mediante los siguientes métodos. El primero permite obtener funciones genéricas de la instancia y el segundo obtiene referencias a funciones vinculadas a un dispositivo.

PFN_vkVoidFunction vkGetInstanceProcAddr (
  VkInstance  instance,
  const char* pName); 

PFN_vkVoidFunction vkGetDeviceProcAddr (
  VkDevice    device,
  const char* pName);

La biblioteca GLFW dispone de una función para obtener las extensiones que necesita para usar Vulkan como contexto gráfico de las ventanas. Con esta función se puede obtener en una única llamada la lista de extensiones a incluir en la instancia. 

const char** glfwGetRequiredInstanceExtensions(
  uint32_t* glfwExtensionCount);

 

 

La clase GEGraphicsContext

 

Para incluir la instancia de Vulkan (una estructura Vkknstance) vamos a crear una nueva clase donde almacenaremos la información del contexto gráfico que estamos creando. La clase se denomina GEGraphicsContext y su cabecera es la siguiente.

#pragma once

#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#include <vulkan/vulkan.h>

class GEGraphicsContext
{
public:
  VkInstance instance;

public:
  GEGraphicsContext(GLFWwindow* window);
  ~GEGraphicsContext();

private:
  // Métodos de inicialización de Vulkan
  void createInstance();

  // Métodos auxiliares
  void showInstanceProperties();
};

La clase tiene los siguientes métodos:

  • GEGraphicsContext(GLFWwindow* window): Constructor de la clase. Construye la instancia y muestra sus propiedades.

  • ~GEGraphicsContext(): Destructor de la clase. Destruye la instancia.

  •  createInstance(): Genera la instancia con las extensiones requeridas por la ventana gráfica de GLFW y sin capas.

  •  showInstanceProperties(): Muestra en la consola los nombres de las extensiones disponibles en la plataforma de ejecución y las extensiones requeridas por GLFW.

#include "GEGraphicsContext.h"
#include <iostream>
#include <vector>
#include <fstream>

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

//
// FUNCIÓN: GEGraphicsContext::GEGraphicsContext(GLFWwindow* window)
//
// PROPÓSITO: Crea un contexto gráfico de Vulkan (instancia)
//
GEGraphicsContext::GEGraphicsContext(GLFWwindow* window)
{
  createInstance();
  showInstanceProperties();
}

//
// FUNCIÓN: GEGraphicsContext::~GEGraphicsContext()
//
// PROPÓSITO: Destruye el contexto gráfico
//
GEGraphicsContext::~GEGraphicsContext()
{
  vkDestroyInstance(instance, nullptr);
}

//////////////////////////////////////////////////////////////////////////////////////
/////                                                                            /////
/////                     Métodos de inicialización de Vulkan                    /////
/////                                                                            /////
//////////////////////////////////////////////////////////////////////////////////////

//
// FUNCIÓN: GEGraphicsContext::createInstance()
//
// PROPÓSITO: Crea la instancia de Vulkan
//
void GEGraphicsContext::createInstance()
{
  VkApplicationInfo appInfo = {};
  appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
  appInfo.pApplicationName = "Game Engine";
  appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
  appInfo.pEngineName = "GE";
  appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
  appInfo.apiVersion = VK_API_VERSION_1_0;

  uint32_t glfwExtensionCount = 0;
  const char** glfwExtensions;
  glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);

  VkInstanceCreateInfo createInfo = {};
  createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
  createInfo.pApplicationInfo = &appInfo;
  createInfo.enabledExtensionCount = glfwExtensionCount;
  createInfo.ppEnabledExtensionNames = glfwExtensions;
  createInfo.enabledLayerCount = 0;

  if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS)
  {
    throw std::runtime_error("failed to create instance!");
  }
}

//////////////////////////////////////////////////////////////////////////////////////
/////                                                                            /////
/////                             Métodos auxiliares                             /////
/////                                                                            /////
//////////////////////////////////////////////////////////////////////////////////////

//
// FUNCIÓN: GEGraphicsContext::showInstanceProperties()
//
// PROPÓSITO: Muestra las propiedades de la instancia
//
void GEGraphicsContext::showInstanceProperties() 
{
  // Mostrar propiedades de la instancia
  uint32_t extensionCount = 0;
  vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr);
  std::vector<VkExtensionProperties> extensions(extensionCount);
  vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data());
  std::cout << "Available extensions:" << std::endl;
  for (const VkExtensionProperties& extension : extensions)
  {
    std::cout << "\t" << extension.extensionName << std::endl;
  }

  // Mostrar requisitos de GLFW
  uint32_t glfwExtensionCount = 0;
  const char** glfwExtensions;
  glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
  std::cout << "GLFW extensions:" << std::endl;
  for (uint32_t i = 0; i < glfwExtensionCount; i++)
  {
    std::cout << "\t" << glfwExtensions[i] << std::endl;
  }
}

 

 

Modificaciones de la clase GEApplication

 

La clase GEApplication se ha modificado para añadir el campo gc, que contiene un objeto GEGraphicsContext con el contexto gráfico de Vulkan.

//
// 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;

  ...
};

El método run() inclujye ahora la creación del contexto gráfico.

//
// FUNCIÓN: GEApplication::run()
//
// PROPÓSITO: Ejecuta la aplicación
//
void GEApplication::run()
{
  this->window = initWindow();
  this->windowPos = initWindowPos();
  this->gc = new GEGraphicsContext(window);

  mainLoop();

  cleanup();
}

 

 

 

Aspecto final

 

A continuación se muestra el aspecto de la aplicación. La consola muestra la información respecto a las extensiones disponibles y las requeridas por GLFW. Esta información depende de la plataforma en la que se esté ejecutando la aplicación.

Figura18