getContext webgpu: qué es el contexto y qué no es
Qué representa un GPUCanvasContext, por qué no depende del dispositivo hasta que lo configuras, cómo se relaciona con el compositor del navegador y qué métodos tiene.
El contexto de canvas de WebGPU es el objeto más pequeño de la API y el peor entendido. No es un renderizador, no guarda estado gráfico y no dibuja nada: es un intermediario entre tu dispositivo y el compositor del navegador, cuyo único trabajo real es entregarte una textura por fotograma y llevársela cuando terminas. Casi todos los problemas de «no se ve nada» vienen de tratarlo como si fuera algo más.
- Explicar qué representa un
GPUCanvasContexty qué relación tiene con el compositor. - Enumerar los cuatro métodos del contexto y para qué sirve cada uno.
- Justificar por qué el contexto se obtiene antes de tener un dispositivo.
- Reconocer los errores derivados de tratar el contexto como un renderizador.
Un intermediario, no un renderizador
const canvas = document.querySelector('#lienzo');
const context = canvas.getContext('webgpu');
Esa llamada no toca la GPU. Devuelve un objeto que todavía no sirve para nada, porque no sabe con qué dispositivo va a trabajar ni en qué formato. Es deliberado y refleja cómo funciona por dentro: el contexto es el dueño de una cadena de intercambio de imágenes que el compositor del navegador va a leer, y hasta que no sabes qué dispositivo las va a producir no se puede reservar nada.
La secuencia completa de un fotograma, vista desde el sistema, es esta. El compositor del navegador tiene una lista de superficies que combina para producir lo que ves: el texto de la página, las imágenes, los vídeos, y también los canvas acelerados. El contexto de WebGPU gestiona una de esas superficies. Cuando llamas a getCurrentTexture(), el contexto te presta una imagen de su cadena para que escribas en ella. Cuando el fotograma termina, la entrega al compositor, que la combina con el resto de la página según las reglas de CSS y produce la imagen final.
De ahí salen tres hechos que explican mucho comportamiento.
El contexto no dibuja: el dispositivo dibuja. Todo lo que se pinta se pinta con un GPURenderPassEncoder cuyo adjunto de color es una vista de la textura que el contexto prestó. El contexto solo entrega y recoge.
El contexto no tiene bucle. No hay nada equivalente a present() o swapBuffers(). La entrega ocurre sola cuando el navegador va a componer un fotograma. Tu bucle es requestAnimationFrame y nada más.
Un canvas tiene un solo contexto en toda su vida. Si el elemento ya tiene un contexto 2D o de WebGL, getContext('webgpu') devuelve null. Para superponer capas se usan varios elementos canvas apilados con CSS.
OffscreenCanvas expone el mismo getContext('webgpu') y el mismo objeto de contexto, con el mismo comportamiento. Es lo que permite mover el render a un worker: se transfiere el canvas con transferControlToOffscreen(), se pasa al worker, y ahí dentro se obtiene el contexto y se configura con un dispositivo obtenido desde WorkerNavigator.gpu.
Los cuatro métodos
La superficie de la API es minúscula.
configure(configuracion) conecta el contexto con un dispositivo y fija el formato y las opciones de la cadena de imágenes. Sin esto, getCurrentTexture() no funciona. Tiene su propia lección porque el descriptor tiene siete campos y varios importan.
unconfigure() deshace la configuración y libera la cadena de imágenes. Es lo que hay que llamar cuando una vista se desmonta y el canvas va a seguir existiendo, o antes de reconfigurar con un dispositivo distinto tras una pérdida.
getCurrentTexture() devuelve la GPUTexture sobre la que dibujar este fotograma. Es el método que más reglas tiene y merece la lección del ciclo del fotograma.
getConfiguration() devuelve la configuración actual, o null si el contexto no está configurado. Sirve para dos cosas: comprobar si hay que configurar, y detectar qué campos soporta la implementación, porque la especificación indica que un agente de usuario que no implemente una opción no debería exponer ese miembro. Es el mecanismo de detección de características para cosas como el mapeo de tonos.
const conf = context.getConfiguration();
if (conf === null) {
console.log('el contexto no esta configurado todavia');
} else {
console.log('formato actual:', conf.format);
console.log('soporta toneMapping:', 'toneMapping' in conf);
}
Y una propiedad: context.canvas, que devuelve el elemento del que salió. Es útil para no tener que arrastrar la referencia por todas partes.
Por qué el contexto se obtiene antes que el dispositivo
Parece un orden extraño: obtener un objeto inútil y rellenarlo después. Hay tres razones concretas.
El canvas existe en el DOM, no en la GPU. getContext es una operación del elemento HTML, síncrona por definición desde que existe canvas. Hacerla asíncrona para esperar a que hubiera un dispositivo habría roto la coherencia con el resto de la plataforma.
El dispositivo puede cambiar. Tras una pérdida, el canvas sigue siendo el mismo y el dispositivo es otro. Que la asociación se establezca en configure() y no en getContext() permite reconfigurar sin tocar el DOM.
Un dispositivo puede alimentar varios canvas. No hay una relación uno a uno. Una aplicación con cuatro vistas puede tener cuatro canvas, cuatro contextos y un solo dispositivo, compartiendo pipelines y recursos entre las cuatro. Eso solo funciona si la asociación es por configuración y no por creación.
Ese último punto es más útil de lo que parece. Un editor con vista superior, frontal, lateral y en perspectiva no necesita cuatro dispositivos ni cuatro copias de la geometría: necesita cuatro contextos configurados con el mismo dispositivo, y cuatro pases de render en el mismo command buffer.
const device = await adapter.requestDevice();
const formato = navigator.gpu.getPreferredCanvasFormat();
const vistas = ['#sup', '#frente', '#lado', '#persp'].map((sel) => {
const canvas = document.querySelector(sel);
const ctx = canvas.getContext('webgpu');
ctx.configure({ device, format: formato, alphaMode: 'opaque' });
return { canvas, ctx };
});
// Un solo encoder, cuatro pases, un solo submit.
const encoder = device.createCommandEncoder();
for (const v of vistas) {
const pase = encoder.beginRenderPass({
colorAttachments: [{
view: v.ctx.getCurrentTexture().createView(),
clearValue: { r: 0.04, g: 0.04, b: 0.06, a: 1 },
loadOp: 'clear',
storeOp: 'store',
}],
});
// ... dibujar la escena con la camara de esta vista
pase.end();
}
device.queue.submit([encoder.finish()]);
Los errores que produce entenderlo mal
Cuatro síntomas frecuentes, todos con la misma raíz.
«getCurrentTexture lanza un error». El contexto no está configurado. Es el fallo más común y su causa habitual es reconfigurar tras una pérdida y olvidarse de que unconfigure deja el contexto inutilizable hasta la siguiente configuración.
«El canvas está negro pero no hay errores». El pase dibujó a una textura que no era la del contexto, o el storeOp era 'discard', o el contexto se configuró con un formato distinto del que declara el pipeline. Ninguna de las tres da error de consola.
«Se ve el fotograma anterior». Se guardó la textura o su vista en una variable y se está reutilizando entre fotogramas. La textura del contexto no es estable entre fotogramas.
«Funciona en un canvas y no en el segundo». El segundo elemento ya tenía un contexto de otro tipo, o no se configuró.
Hay un detalle que no es de WebGPU sino de la plataforma web y que arruina el aspecto de más proyectos que cualquier bug de la API: un elemento canvas tiene un tamaño de píxeles y un tamaño de presentación, y son independientes.
canvas.width y canvas.height son el tamaño del búfer: cuántos píxeles hay de verdad. style.width y style.height, o el tamaño que le dé el layout, son el tamaño en la página en píxeles de CSS. Si no los sincronizas, el navegador escala, y escalar de 300 por 150 —el tamaño por defecto de un canvas, que casi nadie recuerda— a mil píxeles de ancho produce exactamente el aspecto pastoso que la gente atribuye a que «WebGPU se ve mal».
La sincronización correcta tiene que tener en cuenta devicePixelRatio, porque en una pantalla de alta densidad un píxel de CSS son dos o tres físicos:
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const rect = canvas.getBoundingClientRect();
canvas.width = Math.max(1, Math.round(rect.width * dpr));
canvas.height = Math.max(1, Math.round(rect.height * dpr));El Math.min con 2 no es cosmética: en un móvil con ratio 3, renderizar a resolución nativa cuadruplica el número de píxeles frente a ratio 1,5, y el coste de fragmentos escala con el área. Limitarlo es la optimización con mejor relación entre esfuerzo y resultado de toda la aplicación, y el usuario no distingue la diferencia. Y el Math.max(1, ...) tampoco sobra: un canvas de dimensión cero es un error de validación al configurar, y ocurre en cuanto el elemento está oculto o dentro de un contenedor que aún no tiene tamaño.
Con el intermediario en su sitio, toca el descriptor que lo pone a funcionar: configure() completo.