Los formatos permitidos: la lista corta que hay que saberse
Los dieciséis formatos que admiten STORAGE_BINDING en el núcleo de WebGPU, por qué faltan los que faltan, y las features tier1 y tier2 que amplían la lista.
La lista de formatos que admiten STORAGE_BINDING es sorprendentemente corta, y sus ausencias son de las que arruinan un diseño a mitad de camino: no está rgba8unorm-srgb, no están los formatos de profundidad, no están los comprimidos, y bgra8unorm —el formato del canvas en la mayoría de las plataformas— necesita una feature. Aprenderse la lista corta ahorra el rodeo de descubrirlas una a una.
- Recitar los dieciséis formatos con capacidad de storage en el núcleo de WebGPU.
- Explicar por qué faltan los sRGB, los comprimidos y los de profundidad.
- Comprobar en tiempo de ejecución qué formatos amplían
texture-formats-tier1y-tier2. - Elegir la ruta correcta cuando el formato que quieres no está disponible.
La lista del núcleo
Estos son los formatos que un dispositivo WebGPU sin ninguna feature adicional acepta con GPUTextureUsage.STORAGE_BINDING:
| Grupo | Formatos |
|---|---|
| 8 bits por canal, 4 canales | rgba8unorm, rgba8snorm, rgba8uint, rgba8sint |
| 16 bits por canal, 4 canales | rgba16uint, rgba16sint, rgba16float |
| 32 bits, 1 canal | r32uint, r32sint, r32float |
| 32 bits, 2 canales | rg32uint, rg32sint, rg32float |
| 32 bits, 4 canales | rgba32uint, rgba32sint, rgba32float |
Dieciséis en total. Y uno más condicional: bgra8unorm, solo si el dispositivo expone la feature bgra8unorm-storage.
El patrón se lee de un vistazo. Todo lo que hay son formatos de 4 bytes por canal, o de 4 canales, o de un solo canal de 32 bits. Faltan sistemáticamente los formatos de uno y dos canales de 8 y 16 bits: no hay r8unorm, ni rg8unorm, ni r16float, ni rg16float en el núcleo. Esa ausencia sorprende porque son formatos muy útiles para máscaras, mapas de altura y buffers intermedios de un canal.
rgba8unorm-srgb no está: la conversión sRGB es hardware de muestreo y de mezcla, no de escritura directa. bgra8unorm no está sin feature, lo que impide escribir directamente al canvas desde un compute en muchas plataformas. Los formatos de profundidad no están: para escribir profundidad hay que usar el depth attachment de un render pass, o un r32float como buffer paralelo. Y los comprimidos no están y nunca estarán: comprimir un bloque requiere ver los dieciséis texels a la vez, cosa que el modelo de escritura por texel no permite.
Comprobarlo desde el código
La forma robusta es una tabla y una función que consulta las features del dispositivo:
const STORAGE_NUCLEO = new Set([
'rgba8unorm', 'rgba8snorm', 'rgba8uint', 'rgba8sint',
'rgba16uint', 'rgba16sint', 'rgba16float',
'r32uint', 'r32sint', 'r32float',
'rg32uint', 'rg32sint', 'rg32float',
'rgba32uint', 'rgba32sint', 'rgba32float',
]);
const STORAGE_TIER1 = new Set([
'r8unorm', 'r8snorm', 'r8uint', 'r8sint',
'rg8unorm', 'rg8snorm', 'rg8uint', 'rg8sint',
'r16uint', 'r16sint', 'r16float',
'rg16uint', 'rg16sint', 'rg16float',
'rgb10a2uint', 'rgb10a2unorm', 'rg11b10ufloat',
'r16unorm', 'r16snorm', 'rg16unorm', 'rg16snorm',
'rgba16unorm', 'rgba16snorm',
]);
function admiteStorage(device, formato) {
if (STORAGE_NUCLEO.has(formato)) return true;
if (formato === 'bgra8unorm') return device.features.has('bgra8unorm-storage');
if (STORAGE_TIER1.has(formato)) return device.features.has('texture-formats-tier1');
return false;
}
Y la elección con degradación se escribe una vez:
function formatoIntermedio(device) {
if (admiteStorage(device, 'r16float')) return 'r16float'; // 2 bytes
return 'r32float'; // 4 bytes, siempre
}
Ese patrón —el formato ideal si está, uno más caro pero universal si no— es la forma correcta de tratar todas las features de WebGPU, y evita ramas de código distintas: solo cambia una cadena.
Lo que amplía cada tier
texture-formats-tier1 añade STORAGE_BINDING con acceso de solo lectura y solo escritura a r8unorm, r8snorm, r8uint, r8sint, rg8unorm, rg8snorm, rg8uint, rg8sint, r16uint, r16sint, r16float, rg16uint, rg16sint, rg16float, rgb10a2uint, rgb10a2unorm y rg11b10ufloat. Además habilita los formatos r16unorm, r16snorm, rg16unorm, rg16snorm, rgba16unorm y rgba16snorm —que no existen sin la feature— con storage y con render attachment, y hace renderizables r8snorm, rg8snorm y rgba8snorm. Pedirla implica pedir rg11b10ufloat-renderable.
texture-formats-tier2 es sobre todo una ampliación del acceso read-write, y se explica en la lección de modos de acceso. Habilitarla implica texture-formats-tier1 y rg11b10ufloat-renderable.
const adapter = await navigator.gpu.requestAdapter();
const opcionales = ['texture-formats-tier2', 'texture-formats-tier1',
'bgra8unorm-storage', 'float32-filterable'];
const device = await adapter.requestDevice({
requiredFeatures: opcionales.filter((f) => adapter.features.has(f)),
});
Pedir tier2 cuando está disponible es gratis y trae tier1 de regalo. Pedir features que el adaptador no tiene lanza un TypeError en requestDevice, de ahí el filtrado.
Cuando el formato que quieres no está
Hay tres salidas y conviene conocer las tres.
Usar un formato mayor. Si necesitabas r8unorm para una máscara y no está, r32float funciona en todas partes a cambio de cuatro veces la memoria. Para máscaras de resolución media es perfectamente aceptable.
Empaquetar a mano en un formato entero. Un r32uint puede contener cuatro valores de 8 bits empaquetados con pack4x8unorm(), y el shader que lee los desempaqueta con unpack4x8unorm(). Es la técnica que permite escribir datos de cualquier disposición en los formatos que sí están:
@group(0) @binding(0) var empaquetado : texture_storage_2d<r32uint, write>;
let bits = pack4x8unorm(vec4<f32>(r, g, b, a));
textureStore(empaquetado, coord, vec4<u32>(bits, 0u, 0u, 0u));
La contrapartida es que la textura empaquetada no se puede filtrar al leerla: el filtrado bilineal de enteros empaquetados no tiene sentido. Para leerla hace falta textureLoad y hacer la interpolación a mano, si es que hace falta.
Usar un storage buffer. Si no necesitas ni el filtrado ni la localidad del hardware de texturas, un storage buffer con la indexación manual no tiene ninguna restricción de formato: guardas lo que quieras con el layout que quieras. Cuándo compensa es el tema de la última lección de este nivel.
Resulta tentador ver la lista como conservadurismo de la especificación, y no lo es: es el conjunto de formatos que las tablas de descriptores de imagen de todo el hardware objetivo garantizan para escritura sin conversión. En Vulkan, la escritura a una imagen de storage con un formato que el hardware no soporta nativamente exige que el driver emule la conversión en el shader, lo que produce código distinto en cada implementación y rendimiento impredecible. La lista del núcleo es la intersección de lo que todos soportan en silicio. Y la existencia de los tiers es el reconocimiento de que esa intersección se ha quedado pequeña: el hardware de 2020 en adelante soporta bastante más, y texture-formats-tier1 es esencialmente lo que soporta el hardware que ya no es antiguo. La estrategia correcta para un proyecto que empieza hoy es pedir los dos tiers, usar el formato ideal cuando estén, y tener una ruta de degradación con formatos del núcleo que no cambie la estructura del código.