Vulkan, Metal y D3D12: de qué es WebGPU el denominador común
Cómo se traduce cada concepto de WebGPU a las tres APIs nativas sobre las que se implementa, qué se dejó fuera a propósito y por qué eso explica la forma de la API.
WebGPU no es un envoltorio de Vulkan. Es una abstracción diseñada para traducirse con eficiencia a tres APIs nativas que no se parecen entre sí tanto como se cuenta, y ese requisito de triple traducción explica prácticamente todas sus decisiones: por qué agrupa recursos, por qué congela el estado en pipelines, por qué no te deja tocar la memoria y por qué le faltan cosas que en Vulkan das por sentadas.
- Situar Vulkan, Metal y D3D12 y explicar qué comparten como generación de APIs.
- Traducir los conceptos centrales de WebGPU a su equivalente en cada API nativa.
- Justificar qué capacidades nativas quedaron fuera de WebGPU y con qué criterio.
- Reconocer qué coste asume la implementación por ti y dónde eso se nota.
Una generación, tres dialectos
Entre 2014 y 2016 las tres plataformas grandes reemplazaron sus APIs gráficas casi a la vez. Apple publicó Metal en 2014, Khronos Vulkan en 2016 y Microsoft Direct3D 12 en 2015. No fue casualidad: las tres respondían al mismo diagnóstico. Las APIs anteriores —OpenGL, Direct3D 11— eran máquinas de estados con drivers enormes que adivinaban lo que la aplicación iba a hacer, recompilaban shaders en el peor momento posible y no se podían alimentar desde varios hilos. El cuello de botella de un juego de 2014 no era la GPU: era el hilo que le hablaba.
La generación nueva comparte cuatro ideas, y son exactamente las cuatro ideas de WebGPU:
El estado se congela en objetos. En lugar de mil llamadas que ajustan variables globales, construyes un objeto de pipeline que contiene todo el estado gráfico y lo validas una vez. VkPipeline, MTLRenderPipelineState, ID3D12PipelineState, GPURenderPipeline: el mismo concepto con cuatro nombres.
Los recursos se agrupan. No se enlaza una textura a una unidad: se construye un bloque de descriptores que apunta a varios recursos y se enlaza el bloque entero. VkDescriptorSet, el argument buffer de Metal, la descriptor table de D3D12, el GPUBindGroup de WebGPU.
Los comandos se graban, no se ejecutan. Escribes comandos en un búfer y lo entregas a una cola cuando has terminado. VkCommandBuffer, MTLCommandBuffer, ID3D12GraphicsCommandList, GPUCommandBuffer.
El trabajo entra por colas explícitas. No hay un «contexto actual» global; hay objetos de cola a los que envías lotes.
WebGPU se sitúa encima de las tres. La tabla siguiente es el diccionario mínimo que conviene tener en la cabeza:
| WebGPU | Vulkan | Metal | Direct3D 12 |
|---|---|---|---|
GPUAdapter |
VkPhysicalDevice |
MTLDevice |
IDXGIAdapter |
GPUDevice |
VkDevice |
MTLDevice |
ID3D12Device |
GPUQueue |
VkQueue |
MTLCommandQueue |
ID3D12CommandQueue |
GPUBuffer |
VkBuffer más VkDeviceMemory |
MTLBuffer |
ID3D12Resource |
GPUTexture |
VkImage más memoria |
MTLTexture |
ID3D12Resource |
GPUTextureView |
VkImageView |
vista de MTLTexture |
vista de descriptor |
GPUBindGroupLayout |
VkDescriptorSetLayout |
firma del argument buffer | root signature parcial |
GPUBindGroup |
VkDescriptorSet |
argument buffer | descriptor table |
GPURenderPipeline |
VkPipeline gráfico |
MTLRenderPipelineState |
PSO gráfico |
GPUCommandEncoder |
VkCommandBuffer grabando |
MTLCommandBuffer |
command list |
| Render pass | VkRenderPass y dynamic rendering |
MTLRenderCommandEncoder |
render target binding |
| WGSL | SPIR-V | MSL | HLSL y DXIL |
Fíjate en la fila de GPUBuffer. En Vulkan un búfer y su memoria son dos objetos: reservas un bloque de VkDeviceMemory y luego enlazas búferes dentro. Metal y D3D12 lo hacen de formas intermedias. WebGPU los fusiona en un solo objeto y no te deja ver la memoria. Esa es la primera concesión grande: la portabilidad exige el mínimo común denominador del modelo de memoria, y el mínimo común denominador es «no hay modelo de memoria expuesto».
Qué se quedó fuera y por qué
La lista de lo que WebGPU decidió no exponer es corta y muy informativa.
Gestión de memoria explícita. No reservas heaps, no colocas recursos en un heap, no haces aliasing de dos texturas sobre la misma memoria. La implementación decide dónde vive cada cosa. El motivo es doble: los tres modelos de memoria nativos son distintos, y exponer la memoria abriría la puerta a leer lo que hay dentro.
Barreras y semáforos. No hay vkCmdPipelineBarrier. WebGPU deduce las dependencias entre pases a partir de qué recursos usa cada uno y coloca las transiciones de estado por su cuenta. El coste es real: la implementación es conservadora donde tú sabrías que no hace falta serlo. La ganancia es que la clase de error más difícil de depurar en Vulkan —una barrera que falta y produce un fallo que solo aparece en una GPU concreta— desaparece.
Varias colas. Vulkan expone colas de gráficos, de cómputo y de transferencia, a veces varias de cada. WebGPU expone exactamente una, device.queue, y la especificación deja la puerta abierta a más sin haberlas definido. En hardware donde una copia asíncrona podría solaparse con el render, WebGPU no te da forma de expresarlo.
Descriptores sin límite. El bindless —indexar un array de miles de texturas desde el shader— es el fundamento de los motores modernos y no está en WebGPU. Es la ausencia que más duele en 2026 y sobre la que hay trabajo en curso.
Subpases. Un VkRenderPass con subpases permite encadenar pases que se comunican por memoria en chip, lo que en móvil es una optimización enorme. WebGPU no tiene subpases.
Control de la compilación. No eliges cuándo se compila un shader a código máquina ni puedes guardar el resultado en caché entre sesiones. Existe GPUShaderModuleDescriptor.compilationHints como pista, pero no hay una caché de pipelines expuesta comparable a VkPipelineCache.
Que no escribas barreras no quiere decir que no las haya. La implementación las inserta, y su coste aparece en el perfil como tiempo de GPU inexplicable entre pases. La palanca que sí tienes es la organización: cuantos menos pases y menos cambios de uso de un mismo recurso, menos transiciones. Escribir a una textura en un pase y leerla en el siguiente cuesta una transición; hacerlo cuatro veces cuesta cuatro.
Lo que esto significa para tu código
Tres consecuencias prácticas se derivan directamente de todo lo anterior.
La primera es que el rendimiento de WebGPU tiene un techo por debajo del de la API nativa equivalente, y ese techo está mucho más arriba de lo que la mayoría de aplicaciones web tocan. La diferencia real entre WebGPU y Vulkan escrito por un experto está en el bindless, en el control de la memoria y en las colas asíncronas. Si tu problema es dibujar diez mil objetos o simular un millón de partículas, no vas a tocar ese techo.
La segunda es que los errores de portabilidad cambian de naturaleza. En Vulkan te preocupa que una GPU concreta tenga un fallo de driver. En WebGPU te preocupa que un dispositivo no tenga un límite lo bastante alto o no exponga una feature. Es un problema mucho más tratable, porque es consultable en tiempo de ejecución antes de intentar nada.
La tercera es que el conocimiento se transfiere en las dos direcciones. Quien aprende WebGPU a fondo puede leer código de Vulkan y entenderlo, porque los conceptos son los mismos con menos ceremonia. Y al revés: la literatura de optimización de GPU escrita para las APIs nativas —la de NVIDIA, la de AMD, la de Arm sobre GPUs móviles— se aplica casi entera, porque describe el hardware, no la API.
Hay un modelo mental que separa a quien depura rápido de quien se atasca: lo que escribes en WebGPU no es lo que ejecuta la GPU, y ni siquiera es lo que recibe el driver. Entre tu draw() y el silicio hay una traducción a Vulkan, Metal o D3D12; una traducción de WGSL a SPIR-V, MSL o HLSL; el compilador de shaders del driver; y las heurísticas de ese compilador. Cuatro capas, cada una con su propia idea de qué es una optimización.
De ahí salen dos hábitos que ahorran días. El primero: cuando un shader va inexplicablemente lento o produce un resultado distinto en dos máquinas, el sospechoso no es tu código, es la capa de traducción, y la forma de confirmarlo es reducir el shader a la mínima expresión que reproduce el problema y probarlo en las tres plataformas. El segundo: no confíes en la intuición de coste que traes de la CPU. Que una operación parezca cara en el código fuente no dice nada sobre lo que sobrevive tras cuatro pasadas de optimización; en una GPU, mover datos suele costar más que calcularlos, y a veces mucho más que recalcularlos dos veces.
Con el diccionario en la mano, el siguiente paso es ver cómo se relacionan entre sí los objetos de WebGPU, que es donde la abstracción deja de ser una tabla de equivalencias y se convierte en un grafo con reglas de vida propias.