Aquí publicaré una traducción del manual de la API de Vulkan. Enlace fuente: vulkan-tutorial.com . Dado que otro usuario de Habr, kiwhy (https://habr.com/ru/users/kiwhy/), está involucrado en la traducción del mismo manual, acordamos
compartir las lecciones entre nosotros. En mis publicaciones, proporcionaré enlaces a capítulos traducidos por kiwhy.
Contenido
1. Introducción
Vea el artículo del autor kiwhy - habr.com/ru/post/462137
2. Resumen
Antecedentes de Vulkan
¿Cómo dibujar un triángulo?
- Paso 1: instancia y dispositivos físicos
- Paso 2: unidades lógicas y familias de colas
- Paso 3: superficie de la ventana y cadenas de intercambio
- Paso 4: vistas de imágenes y búferes de marcos
- Paso 5 - Renderizar pases
- Paso 6: la canalización de gráficos
- Paso 7 - Grupo de comando y búfer de comando
- Paso 8 - bucle principal
- conclusiones
Conceptos de API
En este capítulo, comenzaremos con Vulkan y veremos qué problemas puede resolver. Describiremos los pasos necesarios para crear su primer triángulo. Esto le dará una descripción general del estándar y le permitirá comprender la lógica detrás del diseño de los capítulos siguientes. Concluimos con un vistazo a la estructura de la API de Vulkan y los casos de uso típicos.
Requisitos previos para Vulkan
Al igual que las API de gráficos anteriores, Vulkan se concibe como una abstracción multiplataforma sobre la GPU . El principal problema con la mayoría de estas API es que durante su desarrollo utilizaron hardware de gráficos que se limitaba a la funcionalidad fija. Los desarrolladores tenían que proporcionar datos de vértices en un formato estándar y dependían completamente de los fabricantes de GPU para la iluminación y las sombras.
A medida que se desarrolló la arquitectura de las tarjetas de video, comenzaron a aparecer más y más funciones programables. Todas las funciones nuevas debían combinarse con las API existentes de alguna manera. Esto condujo a abstracciones imperfectas y muchas hipótesis por parte del controlador de gráficos sobre cómo traducir la intención del programador en arquitecturas gráficas modernas. Por lo tanto, se publica una gran cantidad de actualizaciones de controladores para mejorar el rendimiento en los juegos. Debido a la complejidad de dichos controladores, a menudo existen discrepancias entre los proveedores, por ejemplo, en la sintaxis adoptada para los sombreadores.... Aparte de esto, la última década también ha visto una afluencia de dispositivos móviles con un potente hardware gráfico. Las arquitecturas de estas GPU móviles pueden variar mucho según el tamaño y los requisitos de energía. Un ejemplo es el renderizado basado en mosaicos , que puede proporcionar un mejor rendimiento mediante un mejor control de la funcionalidad. Otra limitación debido a la antigüedad de la API es el soporte limitado para subprocesos múltiples, lo que puede provocar un cuello de botella en el lado de la CPU.
Vulkan ayuda a resolver estos problemas porque fue construido desde cero para arquitecturas gráficas modernas. Esto reduce la sobrecarga del lado del conductor al permitir que los desarrolladores describan claramente sus objetivos utilizando una API detallada. Vulkan le permite crear y enviar comandos en paralelo en múltiples hilos. También reduce las discrepancias de compilación entre sombreadores al pasar a un formato de código de bytes estandarizado y usar un solo compilador. Finalmente, Vulkan reúne las capacidades centrales de las tarjetas gráficas actuales al integrar capacidades gráficas y de computación en una sola API.
¿Cómo dibujo un triángulo?
Echaremos un vistazo rápido a los pasos necesarios para dibujar un triángulo. Esto le dará una descripción general del proceso. En los siguientes capítulos se ofrecerá una descripción detallada de cada concepto.
Paso 1 - Instancia y dispositivos físicos
Trabajar con Vulkan comienza configurando la API de Vulkan a través de VkInstance . Se crea una instancia utilizando la descripción de su programa y las extensiones que desee utilizar. Después de la instanciación, puede consultar qué hardware admite Vulkan y seleccionar uno o más VkPhysicalDevices para realizar operaciones. Puede realizar consultas sobre parámetros como el tamaño de VRAM y las capacidades del dispositivo para seleccionar los dispositivos que desee si prefiere utilizar tarjetas gráficas especializadas.
Paso 2 - Dispositivo lógico y familias de colas
Después de seleccionar el dispositivo de hardware apropiado para usar, necesita crear un VkDevice (dispositivo lógico), donde describirá con más detalle qué características ( VkPhysicalDeviceFeatures ) usará, por ejemplo, renderizado en múltiples ventanas gráficas. s (renderizado de múltiples vistas) y flotantes de 64 bits. También debe establecer qué familias de colas le gustaría utilizar. Muchas de las operaciones realizadas con Vulkan, como los comandos de dibujo y las operaciones en memoria, se realizan de forma asincrónica después de enviarse a VkQueue.... Las colas se asignan a partir de una familia de colas, donde cada familia admite un conjunto específico de operaciones. Por ejemplo, pueden existir familias separadas de colas para operaciones de gráficos, operaciones de cálculo y transferencias de datos de memoria. Además, su disponibilidad se puede utilizar como parámetro clave a la hora de elegir un dispositivo físico. Algunos dispositivos habilitados para Vulkan no ofrecen ninguna capacidad gráfica, sin embargo, todas las tarjetas gráficas modernas habilitadas para Vulkan generalmente admiten todas las operaciones de cola que necesitamos.
Paso 3 - Superficie de la ventana y cadenas de intercambio
Si está interesado en algo más que el renderizado fuera de la pantalla, necesita crear una ventana para mostrar las imágenes renderizadas. Windows se puede crear utilizando bibliotecas o API de plataforma nativa como GLFW y SDL . Usaremos GLFW para este tutorial, que cubriremos con más detalle en el próximo capítulo.
Necesitamos dos componentes más para renderizar en la ventana de la aplicación: la superficie de la ventana (
VkSurfaceKHR
) y la cadena de presentación ( VkSwapchainKHR
). Presta atención al sufijoKHR
lo que denota que estos objetos son parte de la extensión Vulkan. La API de Vulkan es completamente independiente de la plataforma, por lo que necesitamos usar la extensión estandarizada WSI (Window System Interface) para interactuar con el administrador de ventanas. Surface es una abstracción de ventana multiplataforma para renderizado que normalmente se crea haciendo referencia a un identificador de ventana nativo, HWND
como en Windows. Afortunadamente, la biblioteca GLFW tiene una función incorporada para trabajar con detalles específicos de la plataforma.
Una cadena de espectáculos es un conjunto de objetivos de renderizado. Su tarea es asegurarse de que la imagen que se está renderizando actualmente difiera de la que se muestra en la pantalla. Esto le permite realizar un seguimiento de que solo se muestran las imágenes renderizadas. Cada vez que necesitamos crear un marco, tenemos que hacer una solicitud para que la cadena de espectáculos nos proporcione una imagen para renderizar. Una vez creado el marco, la imagen vuelve a la cadena de visualización para mostrarse en la pantalla en algún momento. El número de objetivos de renderizado y las condiciones para mostrar imágenes terminadas en la pantalla depende del modo actual. Estos modos incluyen almacenamiento en búfer doble (vsync) y almacenamiento en búfer triple. Los cubriremos en el capítulo sobre la creación de una cadena de espectáculos.
Algunas plataformas permiten renderizar directamente a la pantalla a través de extensiones
VK_KHR_display
y VK_KHR_display_swapchain
sin interactuar con ningún administrador de ventanas. Esto le permite crear una superficie que representa toda la pantalla y se puede utilizar, por ejemplo, para implementar su propio administrador de ventanas.
Paso 4 - Vistas de imagen y framebuffers
Para dibujar en la imagen obtenida de la cadena de visualización, tenemos que envolverla en un VkImageView y VkFramebuffer . La vista de imagen se refiere a una parte específica de la imagen que se utiliza, y el framebuffer se refiere a las vistas de imagen, que se utilizan como búfer de color, profundidad y plantilla. Dado que puede haber muchas imágenes diferentes en la cadena de visualización, crearemos una vista de imagen y un búfer de fotogramas para cada una de ellas por adelantado y seleccionaremos la imagen requerida durante el dibujo.
Paso 5:
Pases de procesamiento Los pases de procesamiento de Vulkan describen el tipo de imágenes utilizadas durante las operaciones de procesamiento, cómo se utilizan y cómo se debe manejar su contenido. Antes de dibujar el triángulo, le diremos a Vulkan que queremos usar una sola imagen como búfer de color y que necesitamos borrarla antes de dibujar. Si la pasada de renderización solo describe el tipo de imágenes utilizadas como búfer, entonces VkFramebuffer asocia imágenes específicas con esas ranuras.
Paso 6 - Canalización de
gráficos La canalización de gráficos en Vulkan se configura creando un objeto VkPipeline . Describe el estado configurable de la tarjeta de video, como el tamaño de la ventana gráfica o la operación del búfer de profundidad, así como el estado programable usando objetos VkShaderModule . Los objetos VkShaderModule se crean a partir del código de bytes del sombreador. El controlador también debe especificar qué destinos de procesamiento se utilizarán en la canalización. Los configuramos haciendo referencia al pase de renderizado.
Una de las características más distintivas de Vulkan en comparación con las API existentes es que casi todas las configuraciones del sistema para la canalización de gráficos deben estar predefinidas. Esto significa que si desea cambiar a un sombreador diferente o cambiar ligeramente el diseño del vértice, debe volver a crear completamente la canalización de gráficos. Por lo tanto, tendrá que crear muchos objetos VkPipeline de antemano para todas las combinaciones necesarias para las operaciones de renderizado. Solo algunos ajustes básicos, como el tamaño de la ventana gráfica y el color claro, se pueden cambiar de forma dinámica. Todos los estados deben describirse explícitamente. Entonces, por ejemplo, no hay un estado de combinación de colores predeterminado.
Afortunadamente, debido a que el proceso se parece más a una compilación por delante, en lugar de compilar sobre la marcha, el controlador tiene más oportunidades de optimización y el rendimiento es más predecible porque los cambios de estado significativos, como cambiar a una canalización de gráficos diferente, se especifican explícitamente.
Paso 7: grupo de comandos y búferes de comando
Como se mencionó, muchas operaciones en Vulkan, como operaciones de dibujo, deben estar en cola. Antes de enviar operaciones, deben escribirse en VkCommandBuffer . Los búferes de comandos provienen de VkCommandPool , que está asociado con una familia de colas específica. Para dibujar un triángulo simple, necesitamos escribir un búfer de comando con las siguientes operaciones:
- Iniciar pase de renderizado
- Enlazar canalización de gráficos
- Dibuja 3 vértices
- Finalizar pase de renderizado
Dado que la instancia de la imagen en el framebuffer depende de qué imagen nos dará la cadena de visualización, necesitamos escribir un búfer de comando para cada imagen posible y seleccionar la que necesitamos durante el dibujo. Podemos escribir el búfer de comando cada vez para cada marco, pero esto es menos eficiente.
Paso 8 - Bucle principal
Después de enviar los comandos de dibujo al búfer de comandos, el bucle principal parece bastante simple. Primero, obtenemos la imagen de la cadena de espectáculos con
vkAcquireNextImageKHR
. Luego podemos seleccionar el búfer de comando apropiado para esa imagen y ejecutarlo con vkQueueSubmit . Finalmente, devolvemos la imagen a la cadena de visualización para mostrarla usando vkQueuePresentKHR
.
Las operaciones enviadas a la cola se realizan de forma asincrónica. Por lo tanto, debemos usar objetos de sincronización, semáforos, para asegurar el orden de inicio correcto. Es necesario configurar la ejecución del búfer de comando de dibujo de tal manera que se lleve a cabo solo después de que la imagen haya sido recuperada de la cadena de visualización, de lo contrario puede surgir una situación cuando comencemos a renderizar una imagen que aún se está leyendo para mostrarla en la pantalla. La llamada
vkQueuePresentKHR
, a su vez, debe esperar a que se complete el renderizado, para lo cual usaremos el segundo semáforo. Notificará el final del renderizado.
Conclusiones
Esta descripción general rápida le brinda una descripción general del trabajo antes de dibujar su primer triángulo. En realidad, hay muchos más pasos. Estos incluyen la asignación de búferes de vértices, la creación de búferes uniformes y la carga de imágenes de textura, todo lo cual cubriremos en los próximos capítulos, pero por ahora, comencemos de manera simple. Cuanto más avancemos, más difícil será el material. Tenga en cuenta que decidimos tomar el camino complicado al incrustar inicialmente las coordenadas del vértice en el sombreador de vértices en lugar de usar el búfer de vértices. Esta decisión se debe al hecho de que para administrar los búferes de vértice, primero debe estar familiarizado con los búferes de comando.
Resumamos brevemente. Para dibujar el primer triángulo, necesitamos:
- Crear VkInstance
- Seleccione una tarjeta de video compatible ( VkPhysicalDevice )
- Cree VkDevice y VkQueue para dibujar y mostrar
- Crear ventana, superficie de ventana y mostrar cadena
- Envuelva las imágenes de la cadena de visualización en VkImageView
- Cree un pase de renderizado que defina los objetivos de renderizado y su uso
- Crear framebuffer para renderizar
- Configurar la canalización de gráficos
- Distribuir y escribir comandos de dibujo en el búfer para cada imagen en la cadena de visualización.
- Renderice cuadros a imágenes recibidas enviando el búfer de comando correcto y devolviendo las imágenes a la cadena de visualización
A pesar de que existen muchos pasos, el significado de cada uno de ellos quedará claro en los siguientes capítulos. Si no puede encontrar un paso, vuelva a este capítulo.
Conceptos de API
Este capítulo concluye con una descripción general rápida de cómo se estructuran las API de Vulkan en un nivel inferior.
Estándar de codificación
Todas las funciones, enumeraciones y estructuras de Vulkan están etiquetadas bajo un encabezado
vulkan.h
que se incluye en el SDK de Vulkan desarrollado por LunarG. La instalación del SDK se tratará en el siguiente capítulo.
Las funciones tienen el prefijo
vk
en minúsculas, los tipos enumerados (enum) y las estructuras tienen el prefijo Vk
y los valores enumerados tienen el prefijo VK_
. La API hace un uso extensivo de estructuras para proporcionar parámetros a funciones. Por ejemplo, los objetos generalmente se crean de acuerdo con el siguiente patrón:
Muchas estructuras en Vulkan requieren que especifique explícitamente el tipo de estructura en el miembro
sType
. Un miembro pNext
puede apuntar a una estructura de extensión y siempre será de tiponullptr
... Las funciones que crean o destruyen un objeto tendrán un parámetro VkAllocationCallbacks , que te permite usar tu propio asignador de memoria y que en el manual también tendrá un tipo nullptr
.
Casi todas las funciones devuelven VkResult , que es
VK_SUCCESS
un código de error o un código de error. La especificación establece qué códigos de error puede devolver cada función y qué significan.
Capas de validación
Como se mencionó, Vulkan fue diseñado para proporcionar un alto rendimiento con bajas cargas de controladores. Por lo tanto, incluye capacidades de detección y corrección de errores automáticas muy limitadas. Si comete un error, el controlador se bloqueará o, peor aún, continuará funcionando en su tarjeta gráfica, pero fallará en otras tarjetas gráficas.
Por lo tanto, Vulkan le permite ejecutar validaciones avanzadas utilizando una función conocida como capas de validación.... Las capas de validación son fragmentos de código que se pueden insertar entre la API y el controlador de gráficos para realizar una validación adicional en los parámetros de función y realizar un seguimiento de los problemas de gestión de la memoria. Esto es conveniente porque puede iniciarlos durante el desarrollo y luego deshabilitarlos por completo al iniciar el programa sin costo adicional. Cualquiera puede escribir sus propias capas de validación, pero Vulkan SDK de LunarG proporciona un conjunto estándar que usaremos a lo largo del tutorial. También debe registrar una función de devolución de llamada para recibir mensajes de depuración de capas.
Dado que las operaciones en Vulkan son muy detalladas y las capas de validación son bastante extensas, será mucho más fácil para usted determinar la causa de la pantalla negra en comparación con OpenGL y Direct3D.
Solo queda un paso antes de comenzar a codificar, y es configurar el entorno de desarrollo.