wandres.dev
PORTABILIDAD · Límites, features y fallback

Qué puedes asumir y qué no

La lista cerrada de suposiciones seguras sobre WebGPU, las que rompen proyectos, y el criterio con el que se decide si tu producto puede permitirse exigirlo.

⏱ 18 min

La portabilidad no es una fase que se hace al final: es una lista de suposiciones que tomas sin darte cuenta el primer día y que descubres tres meses después, una por una, cada vez que alguien abre tu proyecto en un dispositivo que no es el tuyo. Esta lección cierra el nivel poniendo esa lista por escrito, separando lo que la especificación garantiza de lo que solo garantiza tu portátil, y dando el criterio con el que se decide si tu proyecto puede exigir WebGPU o no.

🎯 Al terminar esta lección sabrás
  • Distinguir las suposiciones que la especificación respalda de las que no respalda nadie.
  • Aplicar un criterio de decisión explícito sobre si el proyecto puede exigir WebGPU.
  • Auditar un motor existente contra una lista de comprobación de portabilidad.
  • Escribir el arranque que falla con un mensaje útil en vez de con una pantalla negra.

Lo que sí puedes asumir

Hay un conjunto pequeño y sólido de cosas que la especificación te garantiza en cualquier implementación conforme. Apoyarte en ellas no tiene riesgo.

Los límites por defecto están. Los treinta y pico valores del contrato mínimo son un suelo, no una estimación: si tu motor cabe en cuatro bind groups, en 64 KiB de uniform, en 128 MiB por binding de storage, en 16 KiB de memoria compartida por workgroup y en 256 invocaciones por workgroup, cabe en todas partes donde WebGPU funcione. Es el material de los límites garantizados frente al hardware real, y es la base de todo lo demás.

El núcleo del lenguaje y de la API está entero. Compute shaders, storage buffers de lectura y escritura, atómicos sobre atomic<u32> y atomic<i32>, memoria compartida de workgroup, barreras, dibujado y despacho indirectos, storage textures, render targets múltiples, instancing, occlusion queries, error scopes y etiquetas no son opcionales. No hace falta comprobarlos ni pedirlos.

El comportamiento numérico básico está definido. f32 es IEEE 754 de precisión simple en todas partes, y los formatos de textura del núcleo, los modos de mezcla, las comparaciones de profundidad y las reglas de alineación de WGSL son idénticos en todas las implementaciones. El mismo shader sobre los mismos datos produce la misma imagen salvo en el último bit.

La validación es determinista. Si tu código pasa la validación en un navegador con los límites por defecto, la pasa en todos. Los errores de validación no son dependientes del hardware: son consecuencia de la especificación, y esa propiedad es la que hace que se puedan cazar todos en desarrollo.

El volumen de recorte va de cero a uno en profundidad. WebGPU nació con la convención moderna, no con la heredada de OpenGL, así que no hay dos convenciones que reconciliar.

Lo que no puedes asumir

Y ahora la lista que de verdad importa, porque cada línea es un fallo real que le ha pasado a alguien.

No asumas que existe WebGPU. navigator.gpu puede no estar. Ese es el caso trivial.

No asumas que si existe la API hay adaptador. requestAdapter() devuelve null con toda normalidad: listas de bloqueo de drivers, hardware sin backend, virtualización, política corporativa. Es un resultado esperado y hay que tratarlo como tal, no como una excepción.

No asumas que el adaptador es una GPU real. Puede ser un adaptador de reserva, con isFallbackAdapter a true, que ejecuta tu código a una velocidad inútil para tiempo real.

No asumas que el dispositivo hereda las capacidades del adaptador. No las hereda. Sin requiredLimits, tu dispositivo tiene los límites por defecto aunque la GPU aguante diez veces más.

No asumas ninguna feature. Ninguna. Ni siquiera timestamp-query, que parece básica y no lo es. La comprobación va donde está la decisión, como se explica en comprobar las features antes de pedirlas.

No asumas que el dispositivo sobrevive. Se pierde: por reinicio del driver, por cambio de GPU, por suspensión del equipo, por un shader que agota el tiempo del vigilante del sistema. Y cuando se pierde, no se recupera: hay que recrearlo todo.

No asumas un rendimiento. Entre la GPU integrada de un portátil de oficina y una dedicada de escritorio hay uno o dos órdenes de magnitud, y las dos pasan exactamente las mismas comprobaciones de la API. La calidad se decide midiendo el tiempo de fotograma, nunca leyendo capacidades.

No asumas el modelo de GPU. Los valores de adapter.info pueden venir vacíos y los límites vienen escalonados a propósito para dificultar la identificación del usuario. Cualquier lógica basada en reconocer el hardware está construida sobre datos que el navegador tiene derecho a ocultar y que cambian entre versiones.

No asumas el mismo resultado bit a bit entre GPU. La especificación permite la contracción de multiplicación y suma y no exige precisión al último bit en las funciones trascendentes. Dos implementaciones conformes pueden diferir, y las dos son correctas.

No asumas que Firefox y Safari son Chrome. Los mensajes de error, el escalonado de los límites, las features disponibles y las plataformas soportadas difieren, tal y como está detallado en el soporte real en 2026.

No asumas que hay una ruta de reserva porque la escribiste. Si no se ejecuta a diario, no existe. Ese es el argumento entero de degradar a WebGL2 sin engañarte.

El criterio de decisión

La pregunta “¿puedo exigir WebGPU en mi proyecto?” tiene respuesta y no es de opinión. Contesta a estas cinco con datos y la respuesta sale sola.

Uno: qué fracción de tus usuarios reales no obtiene adaptador. Medida con una sonda tuya durante al menos una o dos semanas, no leída de una estadística global. Este número es el que manda sobre los otros cuatro.

Dos: qué le pasa a ese usuario si no ve nada. Si tu escena es un adorno, no pasa nada grave y basta una imagen. Si tu escena es el producto, cada usuario sin adaptador es un usuario perdido con un valor que alguien de tu equipo sabe calcular.

Tres: cuánto de tu motor depende de capacidades que WebGL2 no tiene. Haz el inventario real: qué pasadas usan compute, storage buffers de escritura, atómicos o indirectos. Si son cero, una ruta de reserva es viable. Si son el corazón del render, no existe la reserva, existe un segundo producto.

Cuatro: cuánto cuesta mantener dos rutas en tu equipo. No lo que cuesta escribirlas: lo que cuesta que las dos sigan funcionando dentro de un año, con otra gente y con la escena cambiada. Si nadie va a ejecutar la segunda a diario, el coste real es infinito porque la ruta estará rota cuando la necesites.

Cinco: cuánto puedes esperar. El soporte solo mejora con el tiempo. Un proyecto interno que se publica dentro de un año está en una situación distinta de uno que se publica el mes que viene.

De ahí salen tres decisiones limpias:

Situación Decisión
Público controlado o técnico, WebGPU estructural, poca gente fuera Exige WebGPU. Detecta, informa con un mensaje claro y no construyas reserva
Público amplio, WebGPU aporta una capa (post-proceso, partículas, efecto) Mejora progresiva: el producto funciona sin él y mejora con él
Público amplio, WebGPU estructural Dos productos con alcances distintos, o retrasa la decisión hasta que el primer número mejore

Lo que no es una decisión es “ya veremos”. Un proyecto que no ha decidido esto acaba con una ruta principal en WebGPU, una reserva a medias que nadie prueba y un mensaje de error que dice “algo ha fallado”.

La lista de comprobación

Audita tu motor contra esto antes de publicar. Cada punto es un fallo que has visto o que vas a ver.

  • El arranque trata navigator.gpu ausente, requestAdapter() con null, isFallbackAdapter a true y requestDevice() rechazando como cuatro casos distintos, con mensajes distintos y telemetría distinta.
  • Todos los tamaños de buffer, de despacho y de textura se calculan desde device.limits, no desde constantes copiadas.
  • El array requiredFeatures se construye filtrando por adapter.features, nunca a mano.
  • Existe un await device.lost que reinicia la aplicación salvo cuando la razón es "destroyed".
  • Hay un parámetro de URL o una variable de entorno que fuerza la ruta degradada, y esa ruta se ejecuta en integración continua.
  • Hay un modo que crea el dispositivo con los límites por defecto explícitos para detectar en tu máquina lo que fallaría en la más modesta.
  • La calidad visual se decide midiendo el tiempo de fotograma real, no leyendo capacidades.
  • Hay una pantalla de fallo con un texto que el usuario pueda entender y accionar, no una pantalla negra.
export async function arrancar(canvas: HTMLCanvasElement) {
  if (!('gpu' in navigator)) {
    return mostrarAviso('Tu navegador no soporta WebGPU.', 'sin-api');
  }

  const adapter = await navigator.gpu.requestAdapter();
  if (!adapter) {
    return mostrarAviso(
      'Tu equipo no ha podido inicializar la GPU. Prueba a actualizar los ' +
      'controladores de vídeo o a usar otro navegador.',
      'sin-adaptador',
    );
  }

  if (adapter.info.isFallbackAdapter) {
    return mostrarAviso(
      'Tu equipo solo ofrece un adaptador de reserva; la experiencia sería ' +
      'demasiado lenta. Se muestra la versión sencilla.',
      'adaptador-reserva',
    );
  }

  let device: GPUDevice;
  try {
    device = await adapter.requestDevice({
      label: 'principal',
      requiredFeatures: negociarFeatures(adapter),
      requiredLimits: negociarLimites(adapter),
    });
  } catch (e) {
    // Aquí solo se llega por un fallo tuyo: pediste algo sin filtrar.
    console.error('requestDevice ha fallado con la negociación:', e);
    return mostrarAviso('No se ha podido iniciar el motor gráfico.', 'device-ko');
  }

  device.lost.then((info) => {
    if (info.reason !== 'destroyed') {
      console.warn('Dispositivo perdido:', info.reason, info.message);
      arrancar(canvas);
    }
  });

  return iniciarMotor(canvas, device);
}

Fíjate en el detalle del catch: si has filtrado bien las features y los límites, ese bloque no debería ejecutarse nunca, y por eso el mensaje de consola dice que es un fallo tuyo. Un error que solo puede venir de tu propio código merece un mensaje distinto de uno que viene del entorno del usuario, porque la acción que dispara es distinta.

La portabilidad no se añade: se conserva, y el momento en que se pierde siempre es el mismo

Hay un patrón que se repite en todos los proyectos que acaban sin poder publicarse fuera de la máquina de quien los escribió, y el momento exacto en que ocurre se puede señalar con el dedo: es la primera vez que alguien mira adapter.limits en lugar de device.limits. No parece nada. Funciona. Y a partir de ahí el motor empieza a crecer sobre una capacidad que el dispositivo del usuario no tiene, porque el adaptador reporta lo que la GPU puede y el dispositivo lo que se negoció. Lo mismo pasa con la variante hermana, mirar adapter.features después de haber creado el dispositivo. Son dos líneas de código indistinguibles de las correctas a simple vista, no producen ningún error en tu máquina, y son la causa de la mayoría de los bugs de portabilidad de WebGPU que se reportan. Hay una defensa desproporcionadamente eficaz: deja de guardar el adaptador. Úsalo para negociar, saca de él las features y los límites que vas a pedir, crea el dispositivo, y no lo pases a ninguna otra parte de tu código; toda la información que necesitarás después está en device.limits, device.features y device.adapterInfo. Si el adaptador no está accesible, nadie puede consultarlo por error, y una clase entera de fallos deja de ser posible por construcción en vez de por disciplina. Es la misma idea que hay detrás de que WebGPU valide por adelantado en lugar de en cada llamada: es más barato hacer que un error sea imposible que acordarse de no cometerlo.

⚔️ Audita lo que ya tienes
  1. Busca en tu proyecto todas las apariciones de adapter. posteriores a la creación del dispositivo. Debería haber cero.
  2. Pasa la lista de comprobación de esta lección punto por punto y apunta cuántos fallas.
  3. Implementa las cuatro pantallas de fallo distintas con sus cuatro mensajes y su telemetría, y comprueba que puedes provocar las cuatro a voluntad.
  4. Contesta por escrito a las cinco preguntas del criterio de decisión con los números de tu proyecto, y anota la decisión que sale. Vuelve a leerla dentro de tres meses.