De WebGL a WebGPU

Francisco 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 reconfortante saber que WebGL y WebGPU comparten muchos conceptos principales. Ambas APIs te permiten ejecutar programas pequeños llamados sombreadores en la GPU. WebGL admite sombreadores de Vertex y de fragmentos, mientras que WebGPU también admite sombreadores de cómputos. WebGL usa el lenguaje de sombreado OpenGL (GLSL), mientras que WebGPU usa el lenguaje de sombreado WebGPU (WGSL). Aunque los dos lenguajes son diferentes, los conceptos subyacentes son en su mayoría los mismos.

Con eso 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. Algunos parámetros de configuración se aplican a todas las operaciones de representación, como la vinculación de texturas y búferes. Para configurar este estado global, llama a varias funciones de la API, y permanecerá 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 a 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 que era global en WebGL. Una canalización contiene información como la combinación, la topología y los atributos que se usarán. Una canalización es inmutable. Si quieres cambiar la configuración, deberás crear otra canalización. WebGPU también usa codificadores de comandos para agrupar comandos por lotes y ejecutarlos en el orden en que se grabaron. Esto es útil en el mapeo de sombras, por ejemplo, donde, en un solo pase sobre los objetos, la aplicación puede grabar varios flujos de comandos, uno para el mapa de sombras de cada luz.

En resumen, a medida que el modelo de estado global de WebGL hacía que la creación de bibliotecas y aplicaciones sólidas y componibles fuera difícil y frágil, WebGPU redujo significativamente la cantidad de estado que los desarrolladores necesitaban realizar un seguimiento cuando enviaban comandos a la GPU.

No sincronizar más

En las GPU, suele ser ineficiente 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 varios procesos con el controlador de GPU que se ejecuta en un proceso independiente de JavaScript.

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

Para 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 tener éxito de inmediato, incluso si la textura es en realidad un error. Solo puedes descubrir el error de forma asíncrona. Este diseño mantiene la comunicación entre procesos sin burbujas y proporciona un rendimiento confiable a las aplicaciones.

Sombreadores de procesamiento

Los sombreadores de procesamiento 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 pueden usarse para una gran variedad de tareas, como el aprendizaje automático, la simulación física y la computación científica. Los sombreadores de procesamiento se ejecutan en paralelo con cientos o incluso miles de subprocesos, lo que los hace muy eficientes para procesar grandes conjuntos de datos. Obtén información sobre el procesamiento de GPU y más detalles en este artículo extenso sobre WebGPU.

Procesamiento de fotogramas de video

El procesamiento de fotogramas de video con JavaScript y WebAssembly tiene algunas desventajas: el costo de copiar los datos de la memoria 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, lo que la convierte en 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 una 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 las aplicaciones 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 un denominador común razonable y más bajo de todas las GPU. Al requerir que los desarrolladores soliciten límites de dispositivos, WebGPU garantiza que las aplicaciones se ejecuten en tantos dispositivos como sea posible.

Control del lienzo

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, preservaDrawingBuffer o stencil.

Por otro lado, WebGPU requiere que administres el lienzo tú mismo. Por ejemplo, para lograr el suavizado de contorno en WebGPU, crearías una textura de varias muestras y renderizarla. Luego, puedes convertir la textura de varias muestras en una normal y dibujar esa textura en el lienzo. Esta administración manual te permite enviar salidas a todos los lienzos que desees 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.

Ten en cuenta que los navegadores actualmente tienen un límite para la cantidad de lienzos de WebGL por página. Al momento de escribir, 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 con 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 se muestra desde la API. Esto significa que puedes ver rápidamente en qué parte de tu código ocurrió el error, lo que 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 prácticos. Por lo general, los mensajes de error incluyen 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 error de GPU, las advertencias de la consola y las herramientas para desarrolladores del navegador.

De los nombres a los índices

En WebGL, muchos elementos están conectados 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 si aparece un error si escribes mal el nombre de la variable uniforme.

Por otro lado, en WebGPU, todo está conectado por completo mediante desplazamiento de bytes o índice (lo que se suele llamar ubicación). Es su responsabilidad mantener sincronizadas las ubicaciones del código en WGSL y JavaScript.

Generación de mipmap

En WebGL, puedes crear el nivel 0 MIP 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 tú mismo. No hay ninguna función integrada para hacer esto. Consulta la discusión sobre las especificaciones para obtener más información sobre la decisión. Puedes usar bibliotecas prácticas, como webgpu-utils, para generar mipmaps o aprender a hacerlo por tu cuenta.

Búferes de almacenamiento y texturas de almacenamiento

WebGL y WebGPU admiten búferes uniformes, y te permiten pasar parámetros constantes de tamaño limitado a sombreadores. Los búferes de almacenamiento, que se parecen mucho a los búferes 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 la especificación indica que las vinculaciones uniformes de búferes 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 solo son de solo lectura. Esto permite la implementación de nuevas clases de algoritmos.

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

Las texturas de almacenamiento solo son compatibles con WebGPU y, a diferencia de los búferes de almacenamiento, en relación con las texturas, se usan búferes uniformes. Son más flexibles que las texturas normales 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.

En WebGPU, los búferes y las texturas son inmutables. Esto significa que no puedes cambiar su tamaño, uso ni formato después de crearlos. Solo puedes cambiar su contenido.

Diferencias en las convenciones de espacio

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

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

WebGL usa la convención de OpenGL, en la que el eje Y está hacia arriba y el Z hacia el visor. WebGPU usa la convención de metal, en la que el eje Y está hacia 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 sigue arriba, como en WebGL.

Agradecimientos

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

También recomendamos WebGPUFundamentals.org para obtener información detallada sobre las diferencias entre WebGPU y WebGL.