getPreferredCanvasFormat: por qué existe esta función
Qué diferencia hay entre bgra8unorm y rgba8unorm, por qué la respuesta correcta depende de la plataforma, y qué pasa si eliges el otro.
navigator.gpu.getPreferredCanvasFormat() devuelve una de dos cadenas y no acepta argumentos. Es la función más simple de toda la API y esconde una de las decisiones de diseño más instructivas: por qué la especificación prefirió obligarte a preguntar antes que elegir un formato único y esconder la conversión.
- Explicar la diferencia entre
bgra8unormyrgba8unorma nivel de bytes. - Justificar por qué la plataforma condiciona cuál es el formato eficiente.
- Cuantificar el coste de usar el formato no preferido.
- Aplicar la disciplina de una única fuente de verdad para el formato.
Dos formatos, los mismos colores
rgba8unorm guarda cada píxel como cuatro bytes en orden rojo, verde, azul, alfa. bgra8unorm guarda los mismos cuatro bytes en orden azul, verde, rojo, alfa. Nada más. La misma precisión, el mismo tamaño, el mismo rango, el mismo espacio de color.
Y desde el shader ni siquiera se nota. Un fragment shader que devuelve vec4f(1.0, 0.0, 0.0, 1.0) produce rojo con los dos formatos: WGSL trabaja siempre en el orden lógico RGBA y el hardware coloca los bytes según el formato del destino. El orden de los canales es un detalle de almacenamiento, no de programación.
Entonces por qué importa. Porque el compositor del sistema operativo, que es quien va a leer esa memoria para ponerla en pantalla, tiene un orden que prefiere, y si no coincide alguien tiene que reordenar.
De dónde viene la preferencia
El orden BGRA tiene una historia larga. Los mapas de bits de Windows lo usan desde los años ochenta, por cómo se leía un entero de 32 bits en little-endian: escribir 0x00FF0000 y que el byte de más peso quedara donde el hardware esperaba el azul. Direct3D lo adoptó, el compositor de Windows trabaja con él, y las GPUs que se diseñaron para ese mercado lo tratan como el camino nativo. Los compositores de macOS y de iOS también favorecen BGRA en muchas configuraciones.
El orden RGBA viene del mundo de OpenGL y es el que usan la mayoría de formatos de imagen y las APIs de la web. Es lo que devuelve getImageData de canvas 2D, es lo que espera putImageData, y es el orden nativo de muchas GPUs móviles.
La consecuencia es que no hay una respuesta correcta universal: depende del sistema operativo, del compositor y a veces de la GPU. Y como la elección tiene un coste medible, la especificación decidió no ocultarla: en lugar de fijar un formato y hacer que la implementación convierta en las plataformas donde no coincide, expone la pregunta.
const formato = navigator.gpu.getPreferredCanvasFormat();
console.log(formato); // 'bgra8unorm' o 'rgba8unorm'
La especificación acota la respuesta: solo puede devolver esos dos valores. No hay que preparar el código para una tercera posibilidad.
Qué cuesta elegir el otro
Nada te impide configurar el canvas con el formato no preferido. Los dos son válidos como formato de canvas en cualquier implementación conforme. Lo que pasa es lo siguiente.
Si el formato coincide con el que quiere el compositor, la textura que produces se entrega tal cual. Cero trabajo adicional.
Si no coincide, alguien tiene que reordenar los canales de cada píxel antes de componer. Ese trabajo lo hace la implementación, con un pase de copia extra o con una capacidad del compositor si la hay. En cualquier caso es una pasada completa sobre todos los píxeles del canvas, cada fotograma.
Para dar magnitud: un canvas de 2560 por 1440 son 3,7 millones de píxeles, 14,7 MB por búfer. Una copia con conversión lee y escribe esos 14,7 MB: casi 30 MB de tráfico de memoria por fotograma, o 1,8 GB por segundo a 60 fotogramas. En una GPU de escritorio con cientos de gigabytes por segundo de ancho de banda es molesto; en un móvil con memoria unificada y una fracción de ese ancho de banda, es una parte apreciable del presupuesto, gastada en reordenar bytes.
Y el efecto es exactamente el mismo tanto si el contenido del canvas es un triángulo como si es una escena completa, porque el coste depende del número de píxeles y no de la complejidad. Es decir: cuanto más simple es tu escena, mayor proporción del tiempo de fotograma se va en la conversión.
La conversión la hace la implementación fuera de tu flujo de comandos. No aparece en un timestamp-query de tus pases, no aparece en el perfilador de JavaScript, y el único síntoma es que el tiempo de fotograma es más alto de lo que la suma de tus pases justifica. Es un buen ejemplo de por qué conviene medir el tiempo de fotograma completo y no solo la suma de las partes que puedes instrumentar.
La disciplina
Una constante, calculada una vez, usada en todos los sitios donde ese formato aparece. Son tres:
export const FORMATO_CANVAS = navigator.gpu.getPreferredCanvasFormat();
// 1. Al configurar el contexto.
context.configure({ device, format: FORMATO_CANVAS, alphaMode: 'opaque' });
// 2. En los targets de cualquier pipeline que dibuje al canvas.
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: { module, entryPoint: 'vs' },
fragment: { module, entryPoint: 'fs', targets: [{ format: FORMATO_CANVAS }] },
});
// 3. Al crear texturas intermedias que se van a copiar al canvas o
// que forman parte de la misma cadena de pases.
const intermedia = device.createTexture({
size: [ancho, alto],
format: FORMATO_CANVAS,
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
});
Si necesitas la variante sRGB para las vistas, se deriva de la misma constante y se declara en viewFormats:
export const FORMATO_CANVAS = navigator.gpu.getPreferredCanvasFormat();
export const FORMATO_VISTA = FORMATO_CANVAS + '-srgb';
context.configure({
device,
format: FORMATO_CANVAS,
viewFormats: [FORMATO_VISTA],
});
const vista = context.getCurrentTexture().createView({ format: FORMATO_VISTA });
Eso funciona porque los dos formatos posibles tienen su variante -srgb con exactamente ese nombre: bgra8unorm-srgb y rgba8unorm-srgb. Es una de las pocas veces en que concatenar cadenas para construir un identificador de la API es defendible.
getPreferredCanvasFormat parece un detalle menor y en realidad es la declaración de principios de toda la API. Otra especificación habría fijado rgba8unorm como formato del canvas y habría dejado que la implementación convirtiera donde hiciera falta. Habría sido más cómodo, y habría escondido un coste por fotograma proporcional al área de la pantalla donde nadie lo habría encontrado nunca.
WebGPU toma la decisión contraria de forma sistemática y conviene reconocer el patrón, porque aparece en toda la API. No hay conversión implícita de formatos de textura al copiar: los formatos tienen que ser compatibles o la validación rechaza. No hay redimensionado automático al copiar entre texturas de tamaños distintos. No hay ajuste automático de la disposición de un struct de uniformes: las reglas de alineación son tuyas y si no las cumples los datos salen mal. No hay generación automática de mipmaps: si los quieres, los generas.
Cada una de esas ausencias es incómoda y cada una responde al mismo criterio: una operación cara nunca ocurre sin que aparezca en tu código. La consecuencia práctica de haber interiorizado esto es que cambia lo que buscas cuando algo va lento. En WebGL, un fotograma lento podía deberse a que el controlador estaba haciendo trabajo que no pediste. En WebGPU, si algo cuesta, está escrito en alguna línea tuya. Es una API que se puede razonar leyendo, y ese es su rasgo más valioso.
Queda el campo del descriptor que decide cómo se mezcla el canvas con la página: alphaMode y la composición.