GPUPipelineLayout y la correspondencia con @group
Cómo el pipeline layout ordena los bind group layouts, qué significa el índice de @group en WGSL, el límite de cuatro grupos y el uso de huecos nulos en la lista.
El pipeline layout es el objeto más pequeño de los tres y el que más consecuencias tiene. Es, literalmente, un array ordenado de bind group layouts: la posición en ese array es el número que el WGSL escribe en @group(n). Entender esta correspondencia y el límite que la acompaña es lo que permite diseñar la arquitectura de recursos de un renderer en lugar de improvisarla.
- Construir un
GPUPipelineLayouty relacionar cada posición del array con su@group(n). - Explicar por qué
maxBindGroupsvale 4 y qué implica para el diseño del renderer. - Usar huecos nulos en
bindGroupLayoutscuando un pipeline no usa todos los grupos. - Trazar la jerarquía completa desde el pipeline hasta el recurso concreto.
El array es el índice
device.createPipelineLayout() recibe un descriptor con un label opcional y un array bindGroupLayouts. No hay más campos. La posición en el array es el índice de grupo:
const pipelineLayout = device.createPipelineLayout({
label: 'opaco',
bindGroupLayouts: [
layoutFrame, // -> @group(0)
layoutMaterial, // -> @group(1)
layoutObjeto, // -> @group(2)
],
});
const pipeline = device.createRenderPipeline({
layout: pipelineLayout,
vertex: { module, buffers: [...] },
fragment: { module, targets: [{ format }] },
});
En el WGSL, la correspondencia es directa:
@group(0) @binding(0) var<uniform> frame : DatosFrame;
@group(1) @binding(0) var<uniform> material : DatosMaterial;
@group(1) @binding(1) var albedo : texture_2d<f32>;
@group(2) @binding(0) var<uniform> objeto : DatosObjeto;
Y en el pass, el índice de setBindGroup es el mismo número:
pass.setPipeline(pipeline);
pass.setBindGroup(0, grupoFrame);
pass.setBindGroup(1, material.grupo);
pass.setBindGroup(2, objeto.grupo);
pass.draw(cuenta);
Tres números que tienen que coincidir en tres sitios distintos. Cuando no coinciden, el error es de validación al crear el pipeline si el desajuste es de forma, o de dibujado si el grupo atado no corresponde al layout del hueco.
flowchart TB P[GPURenderPipeline] --> PL[GPUPipelineLayout] PL --> L0[BindGroupLayout indice 0] PL --> L1[BindGroupLayout indice 1] PL --> L2[BindGroupLayout indice 2] L0 -.valida.-> G0[BindGroup de frame] L1 -.valida.-> G1a[BindGroup material marmol] L1 -.valida.-> G1b[BindGroup material metal] L2 -.valida.-> G2[BindGroup por objeto] G0 --> R0[Buffer de camara] G1a --> R1[Textura albedo y sampler] G2 --> R2[Subrango del buffer de modelos] style P fill:#cba6f7,color:#11111b style PL fill:#89b4fa,color:#11111b style L0 fill:#89b4fa,color:#11111b style L1 fill:#89b4fa,color:#11111b style L2 fill:#89b4fa,color:#11111b style G0 fill:#a6e3a1,color:#11111b style G1a fill:#a6e3a1,color:#11111b style G1b fill:#a6e3a1,color:#11111b style G2 fill:#a6e3a1,color:#11111b style R0 fill:#94e2d5,color:#11111b style R1 fill:#94e2d5,color:#11111b style R2 fill:#94e2d5,color:#11111b
Lee el diagrama de arriba abajo y verás la asimetría que hace útil el modelo: un layout, muchos grupos. El layout del índice 1 valida tanto el bind group de mármol como el de metal, y ambos son intercambiables en el mismo hueco sin tocar el pipeline.
Cuatro grupos, ni uno más
maxBindGroups es 4 en el mínimo garantizado. Muchos dispositivos reales ofrecen más —consulta device.limits.maxBindGroups si te interesa saberlo—, pero escribir un renderer que dependa de más de cuatro es renunciar a la portabilidad por una comodidad que no la vale.
El número no es arbitrario. D3D12 y Metal imponen presupuestos parecidos, y cuatro resultó ser el mínimo común denominador de todo el hardware que WebGPU tiene que cubrir, incluida la GPU integrada de un portátil de hace ocho años. Como el límite es de grupos y no de recursos, la restricción real que impone es de organización: obliga a decidir, para cada recurso, en qué cajón vive. Y como los grupos se atan y se desatan por separado, esa decisión determina cuántas llamadas hace tu bucle de dibujado.
Ese es el motivo por el que la organización por frecuencia de cambio, que es el tema del nivel 18, no es un consejo de estilo sino la decisión arquitectónica central del renderer.
Hay un límite combinado, maxBindGroupsPlusVertexBuffers, que acota la suma de grupos y buffers de vértices. Su mínimo garantizado es 24, así que en la práctica no lo rozarás con cuatro grupos y ocho vertex buffers, pero existe y aparece en la especificación para reflejar que en algunos backends ambos consumen el mismo espacio de registros.
Huecos nulos
Un pipeline puede no usar todos los grupos del layout. Por ejemplo, un pass de sombras que solo escribe profundidad no necesita el grupo de material. La especificación permite poner null en las posiciones no usadas:
const layoutSombra = device.createPipelineLayout({
label: 'sombras',
bindGroupLayouts: [
layoutFrame, // @group(0), sí lo usa
null, // @group(1) vacío: el pass de sombras no lee materiales
layoutObjeto, // @group(2), sí lo usa
],
});
La utilidad de esto no es ahorrar memoria: es mantener los índices estables entre pipelines. Si el pass de sombras y el pass opaco usan el mismo índice 2 para los datos por objeto, el bucle de dibujado no tiene que recordar qué índice corresponde a qué en cada pipeline, y los bind groups por objeto se reutilizan tal cual entre ambos. Colapsar los índices para eliminar el hueco —poner layoutObjeto en la posición 1— obligaría a atar el mismo grupo en índices distintos según el pass, que es una fuente de errores tonta y difícil de ver.
Un null en la lista no reserva ranura para nada: en ese índice no se puede atar ningún bind group. Si un pipeline con un hueco nulo en 1 recibe un setBindGroup(1, algo), ese grupo simplemente no es visible para el shader, aunque el estado quede grabado en el encoder.
La gente trata el pipeline layout como un trámite entre el bind group layout y el pipeline, y es justo al revés: es la pieza de la que dependen las otras dos. Dos pipelines que comparten GPUPipelineLayout comparten también la compatibilidad de sus bind groups, y eso significa que al cambiar de pipeline no hace falta volver a atar los grupos que no cambian. La especificación lo llama compatibilidad de layouts, y la regla exacta es que los grupos atados en los índices que ambos pipelines comparten siguen siendo válidos si los pipeline layouts son iguales hasta ese índice. Diseñar los layouts para maximizar ese prefijo común es lo que convierte un bucle de dibujado de cuatro setBindGroup por objeto en uno de uno. Lo desarrollo en la lección de pipeline layout compartido.
Un pipeline layout no se recrea
Como el bind group layout y el bind group, el pipeline layout es inmutable y de larga vida. Se crea al arrancar, se guarda, y se pasa a todos los pipelines que compartan esa organización de recursos. Un renderer típico tiene entre dos y cinco pipeline layouts en total —opaco, transparente, sombras, post-proceso— y decenas o cientos de pipelines construidos sobre ellos.
Guardarlos en un mapa por nombre es suficiente para no perderlos de vista:
const layouts = {
opaco: device.createPipelineLayout({ label: 'opaco', bindGroupLayouts: [f, m, o] }),
sombra: device.createPipelineLayout({ label: 'sombra', bindGroupLayouts: [f, null, o] }),
post: device.createPipelineLayout({ label: 'post', bindGroupLayouts: [layoutPost] }),
};
Ese objeto de cuatro líneas es, en un renderer serio, la documentación más útil de su arquitectura de recursos: quien lo lee sabe en dos segundos qué se ata dónde. La alternativa, el modo auto, no produce ningún documento equivalente.