3 puntos por GN⁺ 2024-06-07 | 1 comentarios | Compartir por WhatsApp
  • Un desarrollador con experiencia en OpenGL aprendió Vulkan por primera vez y en solo 3 meses creó dos pequeños demos de juegos y el motor reutilizable EDBR
  • En lugar de diseñar desde el inicio un motor de propósito general, primero hizo juegos pequeños y luego convirtió en motor solo las partes necesarias, reduciendo el diseño excesivo y el bike-shedding
  • El motor tiene un tamaño de 19k LoC, con 6.7k LoC de código gráfico y 2k LoC de abstracción ligera de Vulkan, e incluye compute skinning, CSM, sombreado PBR, MSAA, post FX y renderizado de UI
  • Usa vk-bootstrap, Vulkan Memory Allocator, volk, VK_KHR_dynamic_rendering, constantes push, direcciones de dispositivo de buffer y descriptores bindless para reducir el boilerplate de Vulkan y el uso de descriptor sets
  • Vulkan ofreció eliminación del estado global, mejores errores de validación, depuración de shaders con RenderDoc y consistencia entre GPU y SO, pero la sincronización explícita todavía debe manejarse manualmente

El motor EDBR basado en Vulkan creado en 3 meses

  • EDBR (Elias Daler’s Bikeshed Engine) comenzó como un proyecto para aprender Vulkan y luego evolucionó a un pequeño motor reutilizable para proyectos posteriores
  • El código del motor y de los juegos está publicado en el repositorio de GitHub
  • El tamaño del código al momento de escribirlo era el siguiente
    • Motor en sí: 19k LoC
      • Código relacionado con gráficos: 6.7k LoC
      • Abstracción ligera de Vulkan: 2k LoC
    • Juego 3D del gato: 4.6k LoC
    • Juego de plataformas 2D: 1.2k LoC
  • Parte del código no gráfico, como el procesamiento de entrada y el sistema de audio, se tomó de un motor anterior, mientras que los gráficos y varios sistemas clave se escribieron desde cero
  • Considera que fue más adecuado reescribirlo que intentar encajar Vulkan en una abstracción previa de OpenGL

Ruta de aprendizaje desde la programación gráfica hasta Vulkan

  • Si estás empezando en programación gráfica, conviene más comenzar con OpenGL que con Vulkan para no quedar abrumado por la complejidad
  • Como objetivo mínimo, recomienda poder mostrar en pantalla un modelo con texturas y aplicar una iluminación simple Blinn-Phong
  • El shadow mapping básico ayuda a aprender a renderizar una escena desde otro punto de vista y hacia otro render target, además de muestrear una depth texture
  • Recomienda los siguientes recursos para aprender OpenGL
  • Para aprender Vulkan, lo que más ayudó fue vkguide; si estás empezando, recomienda seguirlo completo, aunque para un juego simple tal vez no haga falta de inmediato un nivel de complejidad como “GPU driven rendering”
  • La Vulkan Lecture Series by TU Wien cubre los fundamentos de Vulkan, y en particular las clases sobre sincronización resultan útiles
  • Como resultado del primer mes de aprendizaje, implementó las siguientes funciones
    • Carga de modelos glTF
    • Compute skinning
    • Frustum culling
    • Shadow mapping y cascaded shadow maps

Por qué eligió Vulkan y comparación con WebGPU

  • El objetivo era crear un pequeño juego 3D de escritorio centrado en Windows y Linux, y como prefería tecnologías open source y estándares abiertos, eligió entre OpenGL y Vulkan
  • Considera que OpenGL es suficiente para juegos pequeños, pero ve incierto su futuro porque es poco probable que salgan nuevas versiones y en macOS está deprecated
  • También aprendió algo de WebGPU, pero encontró las siguientes limitaciones
    • Aún no es estable y no hay tantos tutoriales ni ejemplos
    • La sintaxis de WGSL le gustó menos que la de GLSL
    • En escritorio se acerca más a ser un wrapper sobre DirectX, Vulkan y Metal, por lo que las capturas de RenderDoc cambian según la plataforma y las llamadas de WebGPU no corresponden 1:1 con las llamadas de la API nativa
    • No tiene bindless textures ni push constants
  • WebGPU también tiene ventajas claras
    • Tiene mejores errores de validación que OpenGL/WebGL y no tiene estado global
    • Comparte similitudes con Vulkan, así que ayuda antes de aprender Vulkan
    • Requiere menos boilerplate que Vulkan para mostrar algo en pantalla
    • No hace falta manejar directamente la sincronización explícita
    • Permite ejecutar juegos en el navegador

Flujo de renderizado por frame

  • Un frame se divide en varias etapas, y cada una se implementa como un pipeline o un pass
  • En la etapa de skinning, los modelos con animación esquelética se procesan con un compute shader
    • La entrada son un mesh sin skinning y joint matrices
    • La salida es un vertex buffer que se usará en las etapas posteriores de renderizado
    • En las etapas siguientes, los mesh estáticos y los mesh con skinning pueden tratarse de forma similar
  • En la etapa de CSM se realiza cascaded shadow mapping usando una depth texture de 4096x4096 y 3 slices
  • En la etapa de geometry + shading se dibujan los modelos y se aplica sombreado usando el shadow map y la información de luces
    • Los modelos PBR son casi iguales a lo descrito en Physically Based Rendering in Filament
    • El fragment shader realiza en una sola draw call todos los cálculos de luces que afectan al mesh correspondiente
  • Todo se dibuja primero en una multi-sampled texture y luego se hace resolve
  • El depth resolve se procesa manualmente con un fragment shader
    • Recorre todos los fragmentos de la depth texture con multisampling y escribe el valor mínimo en una depth texture sin MS
  • La etapa de post FX actualmente solo aplica depth fog, y más adelante planea procesar también tone mapping y bloom en esta fase
  • En la etapa de UI dibuja la interfaz de diálogo y la procesa en una sola draw call

Librerías para reducir el boilerplate de Vulkan

  • vk-bootstrap reduce el boilerplate de inicialización de Vulkan, como la selección del physical device y la creación del swapchain
    • No es un wrapper completo de las funciones de Vulkan, sino que afecta sobre todo la fase de inicialización
  • Vulkan Memory Allocator evita tener que manejar directamente la asignación de memoria en Vulkan
  • volk simplifica la carga de funciones de extensiones
    • Por ejemplo, permite usar funciones de extensión como vkSetDebugUtilsObjectNameEXT sin tener que guardar manualmente sus punteros
  • La clase GfxDevice agrupa funciones y objetos de Vulkan que se usan con frecuencia
    • Inicialización del contexto de Vulkan
    • Creación y gestión del swapchain
    • beginFrame y endFrame
    • Creación de images y carga de texturas
    • Creación de buffers
    • Gestión del descriptor set bindless
  • GfxDevice.cpp tenía 714 líneas al momento de escribirlo, y resulta conveniente pasar un solo objeto en lugar de distribuir VkDevice, VkQueue, VmaAllocator y otros por varias partes

Compilación de shaders y estrategia para evitar descriptor sets

  • Eligió GLSL como lenguaje de shaders por su experiencia con OpenGL
  • Los shaders se compilan a SPIR-V en la etapa de build, no en runtime
    • El código de carga de shaders en runtime se simplifica
    • No hay dependencia de un compilador de shaders en runtime
    • Los errores de shaders se detectan en la etapa de build
  • glslc permite especificar DEPFILE en CMake, así que cuando cambian los include de shaders, los archivos relacionados pueden recompilarse automáticamente
  • En Vulkan, los uniform deben agruparse en descriptor sets, así que pasar datos es más complejo que en OpenGL
  • En esta implementación se redujo mucho el uso de descriptor sets
    • Solo se usa un descriptor set global para texture y sampler bindless
    • El resto se pasa mayormente con push constants
    • Se usa buffer device address para pasar direcciones de buffer mediante push constants

pipeline class y dynamic rendering

  • Las etapas de renderizado se separan en pipeline class como PostFXPipeline
  • Cada pipeline normalmente cumple estos roles
    • init: carga de shaders e inicialización de VkPipeline y VkPipelineLayout
    • cleanup: liberación del pipeline y del layout
    • draw: recibe las entradas necesarias en cada frame y ejecuta el draw call
  • Se asume que draw se llama entre vkCmdBeginRendering y vkCmdEndRendering
  • El pipeline no se preocupa internamente por a qué texture se renderiza en un render pass; quien lo llama decide el render target
  • Se usa VK_KHR_dynamic_rendering en todo el proyecto y no se usan render pass ni subpass de Vulkan
    • Escuchó que en GPU basadas en tiles los render pass y subpass son más eficientes, pero por ahora no se considera soporte móvil
    • Dynamic rendering hace que la implementación sea mucho más sencilla

Uso de PVP, BDA y bindless descriptor

  • Se usa un solo tipo de vértice para todos los mesh
  • Con programmable vertex pulling se pueden evitar definiciones de formato de vértices como el VAO de OpenGL o VkVertexInputBindingDescription y VkVertexInputAttributeDescription de Vulkan
  • Con buffer device address se puede pasar la dirección del buffer por push constants sin bindear el vertex buffer a un descriptor set
  • En push constants y buffers se usa layout scalar
    • Es más fácil de alinear que std430, así que puede tratarse casi igual que una estructura de C++
    • Reduce la necesidad de miembros de padding en las estructuras de C++
  • Los bindless descriptor se usan con un descriptor set grande que contiene arreglos de texture y sampler
    • Cuando se carga una texture nueva, se coloca en el arreglo textures y ese índice se usa como bindless texture id
    • El texture id se pasa al shader mediante push constants
  • Los sampler se separan de las imágenes; se crean common sampler al iniciar y se colocan en el arreglo samplers
  • El material buffer también usa bindless texture id
    • Solo se pasa el material ID por push constants y el fragment shader consulta el material buffer
    • Permite acceder a textures con un entero por material, sin descriptor sets voluminosos
  • Como referencia sobre bindless texture, recomienda Vulkan Bindless Texture

Datos dinámicos que se suben en cada frame

  • Para los datos que deben enviarse del CPU al GPU en cada frame, se preasigna un arreglo grande y se llena desde el índice 0 en cada frame
  • Por ejemplo, todas las joint matrix se guardan en un gran arreglo de mat4, y para cada skinned mesh se pasa por push constants el índice inicial
  • Hay dos formas de hacerlo
    • Tener N buffers en el GPU e ir alternándolos según el frame-in-flight
    • Tener un solo buffer en el GPU y N staging buffer del lado del CPU
  • En la mayoría de los casos se recomienda la primera opción
    • Usa más memoria de GPU, pero no requiere sincronización manual
  • Si hace falta ahorrar memoria de GPU, la segunda opción puede ser útil
    • No encontró una diferencia de rendimiento notable entre ambos métodos, aunque al subir volúmenes muy grandes de datos en cada frame podría aparecer una diferencia

cleanup y sincronización

  • El patrón de deletion queue de vkguide no fue muy útil en el motor del autor
    • Porque no se asignan ni destruyen objetos Vulkan nuevos en cada frame
  • El cleanup basado en destructores de C++ tampoco resultó cómodo
    • Requiere wrapper class, move constructor y move assignment, lo que aumenta la complejidad
    • Existe el riesgo de eliminar por error, al destruir un wrapper, un objeto que sigue en uso a mitad del frame
  • Actualmente llama de forma explícita a una función cleanup para liberar los objetos Vulkan en un solo lugar
    • Es fácil olvidar una llamada, pero al cerrar, los Vulkan validation error y los assert de VMA señalan el cleanup faltante
  • La synchronization en Vulkan es difícil y debe administrarse de forma explícita
    • OpenGL y WebGPU manejan por su cuenta la sincronización necesaria para leer textures o buffers
    • En Vulkan hay que insertar barriers manualmente para evitar data races
  • Actualmente divide los draw en pass y pipeline, e inserta barriers manualmente entre ellos
    • Por ejemplo, después de que el pass de skinning con compute shader escribe vertex data, se inserta un barrier antes de que el pass de shadow mapping lo lea
  • Esto podría automatizarse con un render graph, pero por ahora está conforme con la sincronización manual
  • La synchronization validation layer de vkconfig ayuda a encontrar errores de sincronización

sprite, skinning y separación entre game y renderer

  • Con bindless texture es fácil dibujar muchos sprite en un solo draw call sin vertex buffer
  • El vertex shader de sprite genera las coordenadas del quad y los UV con gl_VertexIndex
  • Todos los draw call de sprite se acumulan como SpriteDrawCommand en SpriteDrawBuffer
    • transform
    • rango de UV
    • color
    • texture ID
    • shader ID
  • El draw call real tiene la forma vkCmdDraw(cmd, 6, spriteDrawCommands.size(), 0, 0)
    • 6 vértices por sprite
    • tantas instancias como sprites
  • El sprite renderer puede dibujar 10 mil sprites en 315 microsegundos
  • El compute skinning toma los vértices de entrada y las joint matrices de un mesh con skeletal animation y genera un skinned vertex buffer
    • Incluso 3 gatos con el mismo mesh pueden tener animaciones distintas
    • Se necesita un output vertex buffer por cada instancia del mesh
  • La game logic y el renderer se separan mediante draw command
    • La game logic usa entt
    • El renderer no conoce entities ni game object; solo maneja luces, parámetros de escena y mesh draw command
  • MeshDrawCommand incluye meshId, matriz de transformación, bounding sphere, puntero a skinned mesh, índice inicial de joint matrix y si proyecta sombras

carga de escenas, UI, Dear ImGui

  • no hizo un editor de niveles propio y en su lugar usó Blender y exportó en glTF
    • crear un editor de niveles por cuenta propia puede tomar meses o incluso años, así que ahorra tiempo
  • usa el nombre del nodo para crear prefabs y definir la forma física
    • ejemplo: en Interact.Sphere.Diary, Interact antes del primer punto es el nombre del prefab
    • Sphere se usa para crear un cuerpo físico esférico en el sistema de físicas
    • también se pueden usar Capsule y Box, y si no hay ninguno, la forma física se genera a partir de los vértices de la malla
  • los modelos complejos no se insertan directamente en el glTF del nivel, sino que se colocan como objetos Empty->Arrows y se les da un nombre como Cat.NearStore
    • crea el prefab Cat y le agrega la etiqueta NearStore
  • los prefabs se escriben en JSON y pueden incluir glTF externo e información de movimiento y físicas
  • el sistema de UI está inspirado en la API de UI de Roblox
    • origen
    • tamaño relativo
    • posición relativa
    • offsetPosition, offsetSize
    • tamaño fijo
    • tamaño basado en el contenido de label/image
  • después de calcular de forma recursiva el tamaño de los elementos de UI, calcula la posición y dibuja desde el parent hacia los children
  • Dear ImGui se usa para herramientas de desarrollo y depuración
  • había un problema donde Dear ImGui se veía mal en un framebuffer sRGB, así que escribió su propio backend de Dear ImGui
    • solo implementó la parte de rendering, mientras que el backend SDL predeterminado de Dear ImGui maneja la lógica/interacción con el SO, como eventos de entrada y portapapeles
  • las ventajas de su backend propio son las siguientes
    • soporta bindless texture id, por lo que se pueden dibujar imágenes con ImGui::Image(bindlessTextureId, ...)
    • puede dibujar correctamente imágenes lineales y no lineales pasando el formato
    • se puede inicializar y manejar de la misma forma que el resto del código Vulkan del motor

bibliotecas usadas y efectos del cambio a Vulkan

  • para físicas usa Jolt Physics
    • se usa principalmente para la resolución de colisiones y el movimiento básico del personaje
    • JPH::CharacterVirtual maneja bien el movimiento básico del personaje
  • para ECS usa entt
    • en lugar de un ECS hecho por él mismo, usa una biblioteca externa para reducir el código que debe mantener
  • para audio usa openal-soft, libogg, libvorbis
  • para profiling usa Tracy
    • ayuda a verificar cuánto tiempo realmente consume cierto código y a evitar bike-shedding innecesario
  • esto es lo que ganó al cambiar a Vulkan
    • al desaparecer el estado global de OpenGL, la abstracción se vuelve más sencilla
    • se necesitan menos cosas como shader.bind() al estilo OpenGL, state trackers o RAII mágico
    • los errores de validación son más completos que en OpenGL
    • en RenderDoc se puede depurar directamente el vertex shader y el fragment shader
    • las diferencias de comportamiento entre la GPU y el SO se notan menos que con OpenGL
    • en el futuro podrá explorar otros shading languages como Slang o Shady
    • se puede controlar mucho más cada aspecto del pipeline gráfico

trabajo futuro

  • estos son los planes a futuro
    • soporte para fuentes sign-distance field
    • carga de muchas imágenes y generación paralela de mipmaps
    • bloom
    • volumetric fog
    • animation blending
    • render graph
    • ambient occlusion
    • terminar el juego
  • aprender Vulkan fue difícil, pero no tanto como esperaba, y le sirvió para entender más a fondo la programación gráfica y las API modernas

1 comentarios

 
GN⁺ 2024-06-07
Opiniones en Hacker News
  • El minimalismo es muy eficaz
    Yo tomé el camino opuesto, y por eso estoy sufriendo bastante. Estoy haciendo un cliente de metaverso en Rust, y ahora mismo en otra pantalla está mostrando a un avatar atravesando en tranvía una enorme ciudad steampunk. Antes de lanzar una nueva prerelase, lo dejo corriendo 12 horas
    Encima de Vulkan puse WGPU y Rend3, y Rend3 ofrece una API limpia donde creas meshes, texturas 2D y objetos; los objetos referencian los meshes y las texturas, y aparecen en pantalla. El conteo de referencias de Rust mantiene las conexiones en orden, así que usarlo es intuitivo
    Pero cuando se acumulan muchas capas, aparecen los problemas. WGPU intenta soportar navegadores web, Vulkan, Metal, DX11 (eliminado recientemente), DX12, Android y hasta OpenGL, así que necesita un equipo grande de desarrollo y es difícil de cambiar. La propia API de WGPU también se parece bastante a Vulkan, por lo que hay que preocuparse directamente por la asignación de memoria de GPU y la sincronización
    WGPU tiene el problema del mínimo común denominador. Algunas plataformas no pueden soportar ciertas funciones, y WGPU no puede manejar sin interferencias la actualización simultánea de memoria de GPU desde varios hilos que sí soporta Vulkan. Los juegos o clientes que manejan mundos grandes necesitan esta función para subir contenido a la GPU sin matar la tasa de fotogramas. Como cada plataforma tiene restricciones de concurrencia distintas, los conflictos de locks también pueden degradar mucho el rendimiento
    Rend3 intentó actuar como un código pegamento razonable que se encargara de la sincronización y la asignación, pero resolver la sincronización de forma general es difícil, sobre todo. El frustum culling da una gran mejora de rendimiento, pero el occlusion culling terminó siendo contraproducente por el costo de cómputo. El manejo de transparencias también es un dolor de cabeza porque requiere ordenamiento por profundidad. Como trabajo con un mundo con muchas ventanas, necesito sí o sí objetos translúcidos que permitan ver hacia afuera y mirar hacia adentro
    Parece que la gente del stack 3D de Rust está molesta porque llevo 3 años presionando para que arreglen el stack. Todos son voluntarios, y Vulkan se mantiene porque tiene dinero y una base de usuarios. El creador de Rend3 se rindió recientemente, así que ahora me toca meterme y arreglarlo. Poca gente construye cosas complejas con WGPU; la mayoría son juegos 2D que también podrían haberse hecho en Flash, o escenas 3D estáticas simples. Los proyectos comerciales siguen usando Unity o UE5
    Ir directamente con Vulkan también implica escribir uno mismo la sincronización, la asignación, el frustum culling y el manejo de transparencias, así que es un cambio grande
    Además, Vulkano, que es un wrapper sobre Vulkan y Metal, también tiene el problema del mínimo común denominador. Tanto Vulkan como Metal soportan la actualización simultánea de assets de GPU, pero Vulkano no lo permite. Claro que Apple lo maneja de otra manera

    • WGPU usa WebGPU y, hasta donde sé, todavía ningún navegador soporta hilos
      https://gpuweb.github.io/gpuweb/explainer/#multithreading
      https://github.com/gpuweb/gpuweb/issues/354
      OpenGL tampoco soportó nunca hilos, así que usar OpenGL no puede funcionar para eso
    • La primera parte es correcta, pero la segunda no. La asignación y la sincronización se manejan automáticamente
    • Metal es más antiguo que Vulkan, así que, estrictamente hablando, el que lo hace de otra manera es Vulkan
    • El hecho de que WGPU no pueda actualizar memoria de GPU desde varios hilos sin interferencias es justamente el punto clave que quería entender bien para crear una buena experiencia. Ojalá WGPU encuentre la forma de agregar esta función, aunque sea como extensión
    • También leí con mucho interés el post que publicaste antes en URLO: https://users.rust-lang.org/t/game-dev-in-rust-some-notes-on...
  • Hace poco más de un año intenté aprender Vulkan, pero no quiero volver a tocarlo. Me resulta muy incómodo que hayan descartado OpenGL y lo hayan reemplazado por algo que vuelve absurdamente difíciles incluso las tareas simples. Por ejemplo, hasta para un solo cubo giratorio se necesitan cientos de líneas de código.
    OpenGL tampoco era fácil, pero una persona normal podía aprender lo básico en relativamente poco tiempo. Podías comprar en una librería grande un libro introductorio sobre programación gráfica y, en una o dos tardes, tener renderizado básico en pantalla. Vulkan quizá sea mejor en algunos aspectos, pero no es realista esperar aprenderlo rápido.
    Imaginen que, con la salida de los chips modernos de Intel/ARM/AMD, dijeran que ya no se puede usar C ni C++ y que “como dejaremos de dar soporte a lenguajes de alto nivel, ahora usen solo ensamblador; tendrán más control y será más rápido”. Obviamente se diría que es una locura.

    • Hasta donde sé, OpenGL fue deprecated solo en macOS, y seguirá existiendo durante mucho tiempo.
      A menudo se dice que Vulkan es más una “API de GPU” que una API gráfica de alto nivel. Visto así, su complejidad no sorprende, y el dominio en sí es difícil.
      Más que la analogía de eliminar los lenguajes de alto nivel en CPUs modernas y obligar a usar ensamblador, se parece más a la diferencia entre C/C++ monohilo y C/C++ multihilo. La complejidad aumenta mucho y, si no sabes lo que haces, explota o rinde peor, pero también es el camino práctico hacia adelante.
      OpenGL, en general, puede implementarse sobre Vulkan. Es una pena que el estándar OpenGL ya no se desarrolle activamente, pero nada es eterno.
    • Que un solo cubo giratorio tenga cientos de líneas no significa mucho. Agregar un segundo cubo u otra figura no duplica la cantidad de líneas.
      El problema es que OpenGL ya no encaja bien con el hardware actual, así que si se usa de forma ingenua el rendimiento es muy ineficiente. Incluso en OpenGL, cuando llegas a técnicas para eliminar la carga del driver, Vulkan no resulta tan mucho más difícil.
      Los chips actuales de Intel/ARM/AMD tampoco soportan C ni C++ directamente. Ya estás usando ensamblador o herramientas de terceros que traducen desde C/C++. El objetivo de Vulkan también es ofrecer una interfaz estándar de bajo nivel para la GPU, y permitir que encima se construyan abstracciones más fáciles de usar.
    • Vulkan es un objetivo para escribir bibliotecas como OpenGL. La ventaja es que gran parte del código de nivel biblioteca pasa de drivers de dispositivo opacos y llenos de bugs a bibliotecas en espacio de usuario.
    • Si quieres OpenGL, puedes usar ANGLE.
      https://github.com/google/angle
      Varios teléfonos ahora incluyen ANGLE como el único soporte de OpenGL sobre drivers Vulkan.
      Si quieres una API moderna relativamente fácil y portable, puedes usar WebGPU mediante wgpu en Rust o dawn en C++.
  • Creo que Vulkan es excelente, pero su propósito es aprovechar al máximo las funciones avanzadas de GPU. Al usar funciones avanzadas de GPU, también puede rendir mejor que OpenGL.
    Si no apuntas a técnicas avanzadas de renderizado, normalmente siento que OpenGL es la ruta recomendada.
    Todavía hay muchos juegos 2D, low-poly o con gráficos estilo PS1, y esos juegos no necesitan usar Vulkan.
    Vulkan es un ejemplo de cómo la industria de juegos AAA se inclinó hacia la calidad de renderizado y el aspecto visual. Los estudios AAA justifican sus presupuestos con motores y contenido muy avanzados, pero el mercado de juegos 2D/low-poly está creciendo porque los jugadores se están cansando y se dan cuenta de que quieren jugabilidad más que gráficos.
    Si eres desarrollador de juegos, probablemente quieras enfocarte en la jugabilidad y las funciones más que en la calidad del renderizado.

    • Vulkan tiene ventajas sobre OpenGL aunque no te interese la fidelidad visual.
      No tiene estado global, puedes elegir en tiempo de ejecución qué GPU usar, y el manejo de errores de OpenGL es terrible. Tiene capas de validación, el arte oficial es genial y la documentación es excelente. También permite subir datos a la GPU de forma asíncrona desde un segundo hilo de manera más razonable, y cuenta con funciones avanzadas de GPU como mesh shaders o RTX.
    • Aquí el motor parece estar más cerca de la realidad financiera del desarrollo de juegos. Los gráficos de alta fidelidad son tremendamente caros, y para un estudio pequeño es difícil producirlos con plazos y presupuestos realistas.
      ¿Los consumidores rechazarían un juego indie con gráficos de nivel AAA? Probablemente no. La razón de que haya pocos es que financieramente no cierran, y porque existe un mercado lo suficientemente grande que acepta gráficos más estilizados y de menor fidelidad.
    • El punto intermedio es WebGPU. Es mucho menos verboso que Vulkan y garantiza ejecutarse en todas partes, incluidos los navegadores.
      Al mismo tiempo, da acceso a funciones modernas como compute shaders que no se pueden usar en WebGL, y tampoco arrastra tanto legado de múltiples formas de hacer lo mismo como OpenGL. Su principal ventaja es que es nuevo, pero por eso hay muchos menos tutoriales, y esa es una desventaja bastante seria.
    • OpenGL está en declive, casi nadie se preocupa por Metal, y quienes usan Vulkan por voluntad propia son solo magos barbudos del código que se tomaron un descanso de escribir kernels el fin de semana.
      Hay un sinfín de frameworks y motores, pero la mayoría están incompletos y todos tienen opiniones fuertes sobre la estructura del código. Algo como: “crea un árbol de escenas con callbacks. No, escribe clases y objetos de entidades y componentes. Espera, ahora todo es modo inmediato, funcional y sin estado”.
      A eso se suma el caos de plataformas. Si agregas las notificaciones push en juegos móviles, compras dentro de la app y la firma obligatoria mediante Xcode, se vuelve un desastre total. Hay una razón por la que Unity tiene cuota de mercado, y no es porque sea un software excelente. Es porque lo multiplataforma que va más allá de una webview vistosa sigue siendo un dolor enorme.
    • No es solo cuestión de rendimiento. Creo que una de las razones para alejarse de OpenGL es que las funciones de alto nivel no son consistentes entre implementaciones de drivers o entre GPUs.
      Si de todos modos tienes que seguir escribiendo código específico para cada GPU/driver, la capa de abstracción no tiene sentido. Es mejor separar un driver de bajo nivel de una biblioteca de alto nivel.
  • Este artículo tiene muchos buenos consejos. En particular, me llamó la atención “no implementes algo si no lo necesitas ahora”.
    Este es un tema por el que siempre peleo con programadores junior que ya tienen algunos años de carrera, pero todavía están creciendo. Suelen obsesionarse con las “mejores prácticas” y las herramientas nuevas y llamativas de moda, pero les cuesta partir del problema que hay que resolver y enfocarse en lo mínimo necesario para resolverlo.

    • Esto está en el contexto de un equipo de una sola persona. Se ve claro con el siguiente consejo: “recuerda que puedes reescribir cualquier parte del juego/motor más adelante”.
      En organizaciones medianas o grandes, normalmente no es así. Por lo general pasas a lo siguiente y casi no hay tiempo para volver a mirarlo. Es lamentable, pero por eso hay que hacerlo bien desde el principio y reducir el margen para crear bugs o efectos secundarios.
      En demasiadas bases de código, las nuevas funcionalidades se apuraron tanto que terminaron convirtiéndose en un campo minado, donde hasta un cambio pequeño obligaba a revisar manualmente todas las funciones y a tener en la cabeza todo el contexto de la aplicación.
    • La sobreingeniería sigue siendo un dolor de cabeza. Al final, aumenta mucho la complejidad sin prácticamente ningún beneficio.
      Según mi experiencia, cuando más adelante aparece un requisito nuevo, la solución genérica que se había creado antes resulta totalmente inadecuada y de todos modos hay que rehacerla. Hay que resolver el problema que tienes enfrente, no problemas futuros desconocidos.
    • También puedes decirles que YAGNI es una mejor práctica :D
    • Si sigues estrictamente este principio, el trabajo puede terminar consistiendo en gran parte en unir varias API y servicios, y adaptar software ya escrito a las necesidades de la empresa.
      Por eso no sorprende que la gente intente escribir algo por su cuenta en cada oportunidad posible. Este tipo de trabajo pesado de plomería digital desgasta, así que para conservar la cordura un poco más de tiempo, dan ganas de meter de vez en cuando algo más divertido.
    • “No implementes algo si no lo necesitas ahora” es un consejo útil al empezar. Aunque también es una regla que los expertos pueden romper.
  • Parece que el sitio murió por exceso de tráfico, así que dejo una caché: https://web.archive.org/web/20240606103630/https://edw.is/le...

  • Excelente artículo. Aprendí Vulkan por mi cuenta para crear un motor de visualización de datos científicos: https://datoviz.org/ todavía es bastante experimental y pronto planeo lanzar una nueva versión.
    Ya tenía conocimientos de OpenGL, pero aprender Vulkan fue realmente difícil. Hace 5 años los materiales de aprendizaje tampoco eran tan buenos. Aun así acepté el desafío y fue muy divertido.
    Me tomó meses entender el papel de decenas de abstracciones, y en el proceso también escribí un pequeño wrapper para que usar Vulkan fuera un poco menos doloroso: https://datoviz.org/api/vklite/
    Este wrapper solo admite algunas de las funcionalidades más necesarias para visualización científica.

  • Obtener mejor rendimiento con Vulkan que con OpenGL no es sencillo. A los drivers de Vulkan les faltan unas 20 mil líneas de código que en los drivers de OpenGL se encargaban por ti de configurar el pipeline de renderizado y los render targets.
    Ese código ya está dentro de los drivers de OpenGL y fue optimizado durante más de 20 años por algunas de las mejores personas de la industria.
    Así que armar ingenuamente sobre Vulkan algo equivalente a lo que OpenGL ofrecía por defecto no produce mágicamente buen rendimiento. Requiere más trabajo, y empiezan a acumularse problemas reales como poner los fences y las primitivas de sincronización correctas.
    Solo cuando realmente sabes lo que estás haciendo y puedes ejecutar el renderizado con buen paralelismo y sincronización correcta puedes empezar a soñar con las ventajas de rendimiento de Vulkan.
    Como desarrollador aficionado, uso OpenGL ES3 por su simplicidad. Para mí ya es suficientemente bueno, y tengo muchas cosas más urgentes que escribir los molestos descriptores descriptores descriptores de vértices de Vulkan.
    Como referencia, mi motor está aquí: https://github.com/ensisoft/detonator

    • Escuché lo mismo sobre DirectX 12 y DirectX 11. En un libro decían que, si no sabes lo que estás haciendo, es muy probable que DirectX 12 tenga peor rendimiento que DirectX 11.
    • Para mí, la parte más importante es el compilador de shaders. OpenGL lo trae incorporado, pero con Vulkan hay que incorporar otra dependencia más.
      Escuché que Vulkan ahora permite texturas bindless, así que el desorden de descriptores debería ser menos horrible que antes.
      Vulkan es atractivo, pero tiene un costo inicial alto que no quiero pagar.
    • ¿No se podría crear como biblioteca una capa OpenGL-to-Vulkan para apuntar a Vulkan pero usarlo en un nivel de abstracción más alto?
      Luego se podrían reemplazar gradualmente partes de esa biblioteca por rutinas optimizadas para el caso de uso.
  • Está bueno que haya más material sobre Vulkan, pero este también sufre el mismo problema que he visto en todos los recursos de Vulkan que muestran algo en pantalla.
    Antes incluso de mostrar un caso simple, todos introducen otra capa de abstracción encima de Vulkan. Siempre dicen que uses vk-bootstrap, volk, vma u otras bibliotecas.
    No sé si existe algún material que muestre un ejemplo de gestión de memoria manual. Parece que las únicas opciones son usar vma o meterse directo con la especificación. ¿Es pedir demasiado un ejemplo lo más básico posible, sin agregar ninguna biblioteca aparte del propio Vulkan SDK?

    • En desarrollo de juegos, lo habitual es asignar de entrada un bloque grande de memoria y usar dentro de él un asignador bump.
      La mayoría de los juegos tienen, a grandes rasgos, tres tipos de vida útil: permanente/al inicio, por nivel y por frame.
      Como estas vidas útiles se anidan, con un solo asignador tipo pila se puede llegar bastante lejos. Cuando termina un frame o un nivel, simplemente lo devuelves a la posición inicial.
      Hay patrones más complejos, pero solo con este enfoque ya se obtiene bastante utilidad, y se puede usar tanto en CPU como en GPU.
    • Vulkan fue diseñado desde el principio como una API extremadamente de bajo nivel, y partía de la idea de que se necesitarían bibliotecas para llevarlo al nivel de OpenGL/DX11.
      En ese sentido, usar ampliamente bibliotecas encima es algo muy normal. Es como no escribir software moderno directamente contra llamadas al sistema.
    • Sí, eso es pedir demasiado. Ni siquiera los ejemplos “oficiales” de la documentación lo hacen así. Leer la especificación de Vulkan es casi un entrenamiento para soportar tonterías técnicas.
      Cuando comparas las instrucciones de inicialización de Vulkan de alguien con el código del repositorio de Khronos Group, lees la especificación de Vulkan 1.3 y te das cuenta de que para hacer algo tienes que leer la especificación de forma salteada y no en orden, queda claro que fracasó.
      Fracasaron. Es un mal trabajo incluso con otros criterios. Solo que, como puedes hacerlo una vez y luego olvidarte de casi todo, los expertos no se quejan demasiado.
      En otro comentario de este hilo dejé una parte del código fuente con anotaciones de capítulos y secciones de la especificación. Es una implementación general que puede usarse junto con SDL y similares.
      Al momento de escribir esto, el enfoque estándar es usar VMA y Volk, que vienen incluidos en el Vulkan SDK oficial. Eso por sí solo ya dice bastante sobre el estado actual.
  • Durante varios años intenté aprender Vulkan de forma intermitente. Antes conocía bastante bien OpenGL ES 2 y 3.
    Una de las cosas difíciles fue entender cómo debería usarse en un motor real, no en un sample. Muchos ejemplos asignan justo lo necesario o asignan cientos de cosas para asegurarse de que nunca falten.
    Cuando aprendí DirectX, me ayudó MiniEngine de Microsoft, porque no era demasiado complejo y tenía cosas como DescriptorAllocator para gestionar la asignación de descriptores. Me pregunto si existe algo similar para Vulkan.
    Otra dificultad es saber cómo crear buenas abstracciones para materiales, mallas y orden de renderizado. ¿Hay algún buen motor o framework que valga la pena estudiar para ir más allá de los tutoriales?

    • Vulkan es bastante parecido a DirectX 12, así que algunos conceptos se trasladan tal cual. Para la asignación de memoria ayuda usar una biblioteca llamada VMA. Maneja varios casos excepcionales molestos que el estándar fue acumulando con los años y es bastante potente.
      Para la asignación de descriptor sets, para mí solo tiene sentido un patrón. Hay que asumir que los pools tienen vida corta y se crean en cantidad. Si la asignación desde el pool actual falla, creas un pool nuevo y no mantienes tú mismo un contador del número de descriptores. El estándar permite todo tipo de comportamientos de pool distintos al simple conteo. Basta con descartar los pools viejos después de que termine el último command buffer que los referencia.
      Los pipeline barriers y los layouts de imagen son un verdadero dolor de cabeza. Conviene abstraerlos con una capa que rastree el último uso y el último formato de cada recurso, y agregue las barreras necesarias. Puede volverse complejo, pero vale la pena cuando aparecen situaciones más complicadas, como pases opcionales o la posibilidad de cambiar el orden de los pases.
      Las mallas, los materiales y el orden de renderizado son difíciles de resumir en un comentario de HN, y dependen mucho del algoritmo de renderizado elegido. No creo que valga la pena el enorme esfuerzo de hacer bien una solución muy generalizada.
    • Si quieres ver un motor real, algo como vkQuake es una buena referencia: https://github.com/Novum/vkQuake
  • Si eres un lector casual y te preguntas qué hace falta para escribir “Hello, Triangle!” en Vulkan 1.3, mira esto: https://github.com/Planimeter/game-engine-3d/blob/main/src/g...

    • Sí. Aun así, usando vk-bootstrap queda un poco mejor, con 600 líneas de código: https://github.com/charles-lunarg/vk-bootstrap/blob/main/exa...
      La inicialización de Vulkan y la gestión básica del swapchain son muy verbosas, pero una vez que pasas por eso y luego creas abstracciones cómodas para la creación y gestión de pipelines, todo mejora mucho.