De WebGL a WebGPU

François Beaufort
François Beaufort

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

Es reconfortante saber que WebGL y WebGPU comparten muchos conceptos básicos. Ambas APIs te permiten ejecutar pequeños programas llamados sombreadores en la GPU. WebGL admite sombreadores de vértices y fragmentos, mientras que WebGPU también admite sombreadores de cómputos. WebGL usa OpenGL Shading Language (GLSL), mientras que WebGPU usa WebGPU Shading Language (WGSL). Si bien los dos lenguajes son diferentes, los conceptos subyacentes son prácticamente 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 muchos estados globales. Algunos parámetros de configuración se aplican a todas las operaciones de renderización, como las texturas y los búferes que están vinculados. Puedes establecer este estado global llamando a varias funciones de la API, y permanecerá vigente hasta que lo cambies. El estado global en WebGL es una fuente importante de errores, ya que es fácil olvidar cambiar un parámetro de configuración global. Además, el estado global dificulta el uso compartido del 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 que era global en WebGL. Una canalización contiene información como qué combinación, topología y atributos usar. Una canalización es inmutable. Si quieres cambiar algunos parámetros de configuración, debes crear otra canalización. WebGPU también usa codificadores de comandos para agrupar comandos y ejecutarlos en el orden en que se registraron. Esto es útil, por ejemplo, en el mapeo de sombras, en el que, en una sola pasada sobre los objetos, la aplicación puede registrar varios flujos de comandos, uno para cada mapa de sombras de la luz.

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

Ya no se sincronizará

En las GPUs, suele ser ineficiente enviar comandos y esperar a que se completen de forma síncrona, ya que esto puede vaciar la canalización y provocar 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, llamar a gl.getError() requiere un IPC síncrono desde el proceso de JavaScript al proceso de la GPU y viceversa. Esto puede provocar una burbuja en el lado de la CPU a medida que se comunican los dos procesos.

Para evitar estas burbujas, WebGPU se diseñó para ser completamente asíncrono. El modelo de errores y todas las demás operaciones se realizan 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 brinda a las aplicaciones un rendimiento confiable.

Sombreadores de procesamiento

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. Los sombreadores de cómputos 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 más información sobre el cómputo con GPU y más detalles en este extenso artículo sobre WebGPU.

Procesamiento de fotogramas de video

El procesamiento de fotogramas de video con JavaScript y WebAssembly tiene algunos inconvenientes: 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 los trabajadores y los subprocesos de la CPU. WebGPU no tiene esas limitaciones, lo que lo hace ideal 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 aplicaciones de forma predeterminada

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

Manejo de lienzos

WebGL administra automáticamente el lienzo después de que creas un contexto de WebGL y proporcionas atributos de contexto, como alfa, antialiasing, colorSpace, profundidad, preserveDrawingBuffer o stencil.

Por otro lado, WebGPU requiere que administres el lienzo por tu cuenta. Por ejemplo, para lograr el suavizado en WebGPU, crearías una textura de múltiples muestras y renderizarías en ella. Luego, resolverías la textura de múltiples muestras en una textura normal y dibujarías esa textura en el lienzo. Esta administración manual te permite generar resultados en tantos lienzos como desees desde un solo objeto GPUDevice. En cambio, WebGL solo puede crear un contexto por lienzo.

Consulta la demostración de múltiples lienzos de WebGPU.

Como nota adicional, los navegadores actualmente tienen un límite en la cantidad de lienzos de WebGL por página. En el momento de escribir este artículo, Chrome y Safari solo pueden usar hasta 16 lienzos de WebGL de forma simultánea, mientras que 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 elementos canvas 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 devuelve desde la API. Esto significa que puedes ver rápidamente dónde se produjo el error en tu código, 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 procesables. 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 GPUError, las advertencias de la consola y las herramientas para desarrolladores del navegador.

De nombres a índices

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

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

Generación de mipmaps

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

En WebGPU, debes generar los mipmaps por tu cuenta. No hay ninguna función integrada para hacerlo. Consulta el debate sobre la especificación 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 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 se parecen mucho a los búferes uniformes, solo son compatibles con WebGPU y son más potentes y flexibles que los búferes uniformes.

  • Los datos de los búferes de almacenamiento que se pasan a los sombreadores pueden ser mucho más grandes que los búferes uniformes. Si bien la especificación 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 son de escritura y admiten algunas operaciones atómicas, mientras que los búferes uniformes son solo de lectura. Esto permite implementar nuevas clases de algoritmos.

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

Las texturas de almacenamiento solo se admiten en WebGPU y son a las texturas lo que los búferes de almacenamiento son a los búferes uniformes. Son más flexibles que las texturas normales, ya que 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 o formato después de crearlos. Solo puedes cambiar su contenido.

Diferencias en las convenciones de espacio

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

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

WebGL usa la convención de OpenGL, en la que el eje Y apunta hacia arriba y el eje Z apunta hacia el visualizador. WebGPU usa la convención de Metal, en la que el eje Y apunta hacia abajo y el eje Z apunta hacia afuera de la pantalla. Ten en cuenta que la dirección del eje Y es hacia abajo en las coordenadas del búfer de fotogramas, las coordenadas de la ventana gráfica y las coordenadas de fragmentos o píxeles. En el espacio de recorte, la dirección del eje Y sigue siendo hacia 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 recomiendo WebGPUFundamentals.org para obtener un análisis detallado de las diferencias entre WebGPU y WebGL.