De WebGL a WebGPU

François Beaufort
François Beaufort

Como desarrollador de WebGL, puede que te sientas intimidado y entusiasmado por comenzar a usar WebGPU, el sucesor de WebGL que lleva los avances de las APIs de gráficos modernas a la Web.

Es tranquilizador saber que WebGL y WebGPU comparten muchos conceptos básicos. Ambas APIs te permiten ejecutar programas pequeños llamados sombreadores en la GPU. WebGL admite sombreadores de vértices y fragmentos, mientras que WebGPU también es compatible con sombreadores de cómputos. WebGL usa OpenGL Shading Language (GLSL), mientras que WebGPU usa WebGPU Shading Language (WGSL). Aunque ambos lenguajes son diferentes, los conceptos subyacentes son mayormente los mismos.

Con esto en mente, en este artículo, se destacan algunas diferencias entre WebGL y WebGPU para ayudarte a comenzar.

Estado global

WebGL tiene mucho estado global. Algunas configuraciones se aplican a todas las operaciones de renderización, como las texturas y los búferes que están vinculados. Este estado global se configura llamando a varias funciones de la API, y seguirá vigente hasta que lo cambies. El estado global en WebGL es una gran fuente de errores, ya que es fácil olvidarse de cambiar un parámetro de configuración global. Además, el estado global dificulta el uso compartido de código, ya que los desarrolladores deben tener cuidado de no cambiar accidentalmente el estado global de una manera que afecte otras partes del código.

WebGPU es una API sin estado y no mantiene un estado global. En cambio, usa el concepto de una canalización para encapsular todo el estado de renderización global en WebGL. Una canalización contiene información como qué combinación, topología y atributos usar. Una canalización es inmutable. Si deseas cambiar algunos parámetros de configuración, debes crear otra canalización. WebGPU también usa codificadores de comandos para agrupar los comandos y ejecutarlos en el orden en que se registraron. Esto es útil en el mapeo de sombras, por ejemplo, en el que, en un solo pase sobre los objetos, la aplicación puede grabar varias transmisiones de comandos, una para el mapa de sombras de cada luz.

En resumen, a medida que el modelo de estado global de WebGL dificultaba y frágil la creación de bibliotecas y aplicaciones sólidas y componibles, WebGPU redujo significativamente la cantidad de estado que los desarrolladores debían controlar mientras enviaban comandos a la GPU.

No más sincronización

En las GPU, por lo general, no es eficiente enviar comandos y esperarlos de forma síncrona, ya que esto puede vaciar la canalización y generar burbujas. Esto es especialmente cierto en WebGPU y WebGL, que usan una arquitectura de procesos múltiples con el controlador de GPU que se ejecuta en un proceso independiente de JavaScript.

En WebGL, por ejemplo, llamar a gl.getError() requiere una IPC síncrona desde el proceso de JavaScript hasta el proceso de la GPU y viceversa. Esto puede provocar una burbuja en el lado de la CPU, ya que se comunican los dos procesos.

A fin de evitar estas burbujas, WebGPU está diseñada para ser completamente asíncrona. El modelo de error y todas las demás operaciones ocurren de forma asíncrona. Por ejemplo, cuando creas una textura, la operación parece completarse de inmediato, aunque la textura sea en realidad un error. Solo puedes descubrir el error de forma asíncrona. Este diseño mantiene la comunicación entre procesos libre de burbujas y brinda a las aplicaciones un rendimiento confiable.

Sombreadores de cómputos

Los sombreadores de cómputos son programas que se ejecutan en la GPU para realizar cálculos de uso general. Solo están disponibles en WebGPU, no en WebGL.

A diferencia de los sombreadores de vértices y fragmentos, no se limitan al procesamiento de gráficos y se pueden usar para una amplia variedad de tareas, como el aprendizaje automático, la simulación física y la computación científica. Cientos o incluso miles de subprocesos ejecutan los sombreadores de cómputos en paralelo, lo que los hace muy eficientes para procesar conjuntos de datos grandes. Obtén información sobre el procesamiento de GPU y más detalles en este artículo extenso sobre WebGPU.

Procesamiento de fotogramas

Procesar fotogramas de video con JavaScript y WebAssembly tiene algunas desventajas: el costo de copiar los datos de la memoria de la GPU a la memoria de la CPU, y el paralelismo limitado que se puede lograr con trabajadores y subprocesos de CPU. WebGPU no tiene esas limitaciones, por lo que es una excelente opción para procesar fotogramas de video gracias a su estrecha integración con la API de WebCodecs.

En el siguiente fragmento de código, se muestra cómo importar un VideoFrame como textura externa en WebGPU y procesarlo. Puedes probar esta demostración.

// Init WebGPU device and pipeline...
// Configure canvas context...
// Feed camera stream to video...

(function render() {
  const videoFrame = new VideoFrame(video);
  applyFilter(videoFrame);
  requestAnimationFrame(render);
})();

function applyFilter(videoFrame) {
  const texture = device.importExternalTexture({ source: videoFrame });
  const bindgroup = device.createBindGroup({
    layout: pipeline.getBindGroupLayout(0),
    entries: [{ binding: 0, resource: texture }],
  });
  // Finally, submit commands to GPU
}

Portabilidad de la aplicación de forma predeterminada

WebGPU te obliga a solicitar limits. De forma predeterminada, requestDevice() muestra un GPUDevice que puede no coincidir con las capacidades del hardware del dispositivo físico, sino más bien un denominador común razonable y más bajo de todas las GPU. Dado que los desarrolladores deben solicitar límites de dispositivos, WebGPU garantiza que las aplicaciones se ejecuten en la mayor cantidad posible de dispositivos.

Control de lienzos

WebGL administra automáticamente el lienzo después de que creas un contexto de WebGL y proporcionas atributos de contexto, como alfa, antialias, colorSpace, depth, preserveDrawingBuffer o esténcil.

Por otro lado, WebGPU requiere que administres el lienzo por tu cuenta. Por ejemplo, para lograr un suavizado de contorno en WebGPU, deberías crear una textura de varias muestras y renderizarla. Luego, resolverías la textura de varias muestras a una textura regular y la dibujarías en el lienzo. Esta administración manual te permite enviar resultados a todos los lienzos que quieras desde un solo objeto GPUDevice. Por el contrario, WebGL solo puede crear un contexto por lienzo.

Consulta la demostración de varios lienzos de WebGPU.

Como nota al margen, los navegadores actualmente tienen un límite en la cantidad de lienzos de WebGL por página. Al momento de escribir este documento, Chrome y Safari solo pueden usar hasta 16 lienzos de WebGL a la vez; Firefox puede crear hasta 200. Por otro lado, no hay límite en la cantidad de lienzos de WebGPU por página.

Captura de pantalla que muestra la cantidad máxima de lienzos de WebGL en los navegadores Safari, Chrome y Firefox
La cantidad máxima de lienzos de WebGL en Safari, Chrome y Firefox (de izquierda a derecha): demostración.

Mensajes de error útiles

WebGPU proporciona una pila de llamadas para cada mensaje que muestra la API. Esto significa que puedes ver rápidamente dónde ocurrió el error en tu código, lo cual es útil para depurar y corregir errores.

Además de proporcionar una pila de llamadas, los mensajes de error de WebGPU también son fáciles de entender y procesables. Los mensajes de error suelen incluir una descripción del error y sugerencias para corregirlo.

WebGPU también te permite proporcionar un label personalizado para cada objeto WebGPU. Luego, el navegador usa esta etiqueta en los mensajes de GPUError, las advertencias de la consola y las herramientas para desarrolladores del navegador.

De nombres a índices

En WebGL, muchas cosas están conectadas por nombres. Por ejemplo, puedes declarar una variable uniforme llamada myUniform en GLSL y obtener su ubicación mediante gl.getUniformLocation(program, 'myUniform'). Esto resulta útil, ya que obtienes un error si escribes mal el nombre de la variable uniforme.

Por otro lado, en la WebGPU, todo está completamente conectado por desplazamiento de bytes o índice (lo que a menudo se denomina ubicación). Es tu responsabilidad mantener sincronizadas las ubicaciones del código en WGSL y JavaScript.

Generación de mipmaps

En WebGL, puedes crear la mip de nivel 0 de una textura y, luego, llamar a gl.generateMipmap(). WebGL generará todos los demás niveles de mip por ti.

En WebGPU, debes generar mipmaps por tu cuenta. No hay una función integrada para hacerlo. Consulta el análisis de las especificaciones para obtener más información sobre la decisión. Puedes usar bibliotecas útiles, como webgpu-utils, para generar mipmaps o aprender a hacerlo por tu cuenta.

Búferes de almacenamiento y texturas de almacenamiento

Los búferes uniformes son compatibles con WebGL y WebGPU y te permiten pasar parámetros constantes de tamaño limitado a los sombreadores. Los búferes de almacenamiento, que son muy parecidos a los uniformes, solo son compatibles con WebGPU y son más potentes y flexibles que los uniformes.

  • Los datos de los búferes de almacenamiento que se pasan a los sombreadores pueden ser mucho más grandes que los uniformes. Si bien en la especificación se indica que las vinculaciones de búferes uniformes pueden tener un tamaño de hasta 64 KB (consulta maxUniformBufferBindingSize) , el tamaño máximo de una vinculación de búfer de almacenamiento es de al menos 128 MB en WebGPU (consulta maxStorageBufferBindingSize).

  • Los búferes de almacenamiento admiten escritura y admiten algunas operaciones atómicas, mientras que los búferes uniformes son solo de solo lectura. Esto permite la implementación de nuevas clases de algoritmos.

  • Las vinculaciones de búferes de almacenamiento admiten arrays de tamaño de entorno de ejecución para algoritmos más flexibles, mientras que el sombreador debe proporcionar tamaños de array de búferes uniformes.

Las texturas de almacenamiento solo son compatibles con WebGPU y son para las texturas lo que los búferes de almacenamiento son los búferes uniformes. Son más flexibles que las texturas regulares y admiten escrituras de acceso aleatorio (y también lecturas en el futuro).

Cambios en el búfer y la textura

En WebGL, puedes crear un búfer o una textura y, luego, cambiar su tamaño en cualquier momento con gl.bufferData() y gl.texImage2D() respectivamente, por ejemplo.

En WebGPU, los búferes y las texturas son inmutables. Esto significa que no puedes cambiar su tamaño, uso ni formato una vez que se hayan creado. Solo puedes cambiar su contenido.

Diferencias en las convenciones de espacio

En WebGL, el rango de clips de Z es de -1 a 1. En WebGPU, el rango de espacio de recorte de Z es de 0 a 1. Esto significa que los objetos con valor z igual a 0 son los más cercanos a la cámara, mientras que los objetos con valor z igual a 1 son los que están más alejados.

Ilustración de rangos de espacio de recorte Z en WebGL y WebGPU
Rangos de espacio de recorte Z en WebGL y WebGPU.

WebGL usa la convención de OpenGL, en la que el eje Y está hacia arriba y el eje Z está hacia el usuario. WebGPU usa la convención de Metal, en la que el eje Y está abajo y el eje Z está fuera de la pantalla. Ten en cuenta que la dirección del eje Y es hacia abajo en la coordenada del búfer de fotogramas, la coordenada del viewport y la coordenada del fragmento/píxel. En el espacio de recorte, la dirección del eje Y permanece hacia arriba, como en WebGL.

Agradecimientos

Agradecemos a Corentin Wallez, Gregg Tavares, Stephen White, Ken Russell y Rachel Andrew por revisar este artículo.

También te recomiendo WebGPUFundamentals.org para obtener información detallada de las diferencias entre WebGPU y WebGL.