multiDrawIndirect: qué es, qué resolvería y en qué estado está
Qué hace exactamente la llamada que emite varios dibujados de una vez, por qué no está en el núcleo de WebGPU a día de hoy, y cómo estructurar el código para vivir sin ella sin cerrarse la puerta.
El patrón de la lección anterior tiene un límite claro: la CPU sigue grabando una llamada de dibujo por lote, así que el coste de grabación crece con la variedad del contenido aunque no crezca con su cantidad. La funcionalidad que elimina ese último término existe en las APIs nativas desde hace años, se llama multi-draw indirect, y en WebGPU no está en el núcleo. Conviene saber exactamente qué es, cuánto costaría no tenerla en tu caso concreto, y cómo dejar el código preparado sin construir sobre algo que hoy no puedes usar.
- Describir qué hace
multiDrawIndirecty en qué se diferencia de un bucle dedrawIndirect. - Calcular el coste de CPU de un bucle de dibujados indirectos en tu escena.
- Enumerar las tres estrategias que reducen el número de lotes sin nuevas funcionalidades.
- Situar el estado real de la propuesta y decidir si construir sobre ella.
Qué hace y qué resolvería
Un drawIndirect lee un bloque de argumentos y emite un dibujado. Un multiDrawIndirect lee un array de bloques y un número de dibujados, y emite todos de una sola llamada. La variante con contador va más allá: el número de dibujados también se lee de un búfer, con lo que ni siquiera cuántos dibujados hay tiene que saberlo la CPU.
Eso cierra el círculo que la lección anterior dejaba abierto. Con multi-draw indirect, un compute shader podría no solo vaciar lotes sino crearlos: agrupar dinámicamente, generar niveles de detalle, emitir un dibujado por grupo de triángulos superviviente. El bucle de JavaScript desaparecería por completo y el coste de grabación del fotograma sería constante, independiente tanto de la cantidad como de la variedad del contenido.
En Chrome existe una implementación experimental y no estándar, expuesta como la feature chromium-experimental-multi-draw-indirect y accesible solo activando la bandera de WebGPU no seguro en la configuración del navegador. Añade los métodos multiDrawIndirect y multiDrawIndexedIndirect al codificador de render pass. Hay una propuesta de estandarización abierta en el repositorio de la especificación, y a día de hoy sigue sin estar en el núcleo ni en los tres motores.
El nombre lo dice todo: una funcionalidad con prefijo de proveedor, detrás de una bandera que el usuario tiene que activar a mano, no es algo sobre lo que se pueda entregar un producto. Se puede probar, se puede medir para saber cuánto ganarías, y no se puede depender de ella.
Cuánto cuesta realmente no tenerla
Pon el número, que es lo que convierte la queja en una decisión. El coste de grabar una llamada de dibujo en WebGPU —incluida la validación, el marshalling a través del enlace del navegador y la escritura en el búfer de comandos— está en el orden de uno a dos microsegundos en hardware de escritorio típico, y algo más en móvil.
| Lotes distintos | Coste de CPU por fotograma | Porcentaje de un presupuesto de 16,6 ms |
|---|---|---|
| 50 | 0,08 ms | 0,5% |
| 200 | 0,3 ms | 1,8% |
| 1000 | 1,5 ms | 9% |
| 5000 | 7,5 ms | 45% |
Ahora sitúa tu contenido en esa tabla. Un configurador de producto tiene entre 20 y 200 combinaciones distintas de malla y material. Un visor arquitectónico, entre 100 y 500. Un juego de navegador ambicioso, quizá 1000. Solo a partir de varios miles de lotes el término se vuelve dominante, y llegar a varios miles de combinaciones distintas —no de objetos— en contenido web es raro y casi siempre indica un problema de organización del arte más que una necesidad real.
La conclusión honesta es que la ausencia de multi-draw indirect es un techo real y está bastante más arriba de donde la mayoría de las aplicaciones web van a llegar. El término que de verdad dolía, el proporcional al número de objetos, ya lo has eliminado con el dibujado indirecto y el instanciado.
Las tres estrategias que sí puedes aplicar hoy
Reducir la variedad. Es la de mayor efecto y la única que ataca la causa. Parámetros por instancia en lugar de materiales distintos, atlas o arrays de textura en lugar de una textura por objeto, y fusión de mallas que comparten material en tramos del mismo megabúfer. Un contenido bien organizado tiene un orden de magnitud menos lotes que el mismo contenido organizado por acumulación.
Pregrabar la lista. Si el bucle de comandos es idéntico en cada fotograma —y con dibujado indirecto lo es, porque los números están en memoria— se puede grabar una vez y ejecutar muchas. Eso es exactamente lo que hacen los render bundles, y la combinación de un bundle con argumentos indirectos es la aproximación más cercana a multi-draw indirect que permite el núcleo del API. Es el tema del nivel siguiente.
Medir antes de actuar. El coste de grabación se mide con un cronómetro alrededor del bucle de grabación, no con el tiempo de fotograma. Si esa medición dice 0,2 ms, el problema no está aquí y cualquier esfuerzo en esta dirección es tiempo perdido.
La reacción típica ante una funcionalidad que falta es una de dos: ignorarla y escribir el código pegado a lo que hay, o intentar abstraerla con una capa que la emule y que acaba costando más que lo que ahorra. Hay una tercera vía, mucho más barata, y consiste en fijarse en la forma de los datos. multiDrawIndirect consume un búfer de bloques contiguos y un número de dibujados. El bucle de drawIndexedIndirect que escribes hoy consume exactamente lo mismo: el mismo búfer, los mismos bloques contiguos, el mismo número, y lo único que cambia es que recorre los bloques en JavaScript en lugar de dejar que los recorra el procesador de comandos. Si mantienes esa forma —bloques contiguos, paso fijo, todos los lotes del mismo pipeline seguidos, el contador en un sitio conocido— la migración el día que la funcionalidad esté disponible es sustituir un bucle de tres líneas por una llamada. Y mientras tanto no has pagado ninguna abstracción. Lo que rompe esa propiedad, y hay que evitarlo conscientemente, es intercalar estado entre dibujados: un setBindGroup distinto por lote, un setVertexBuffer por malla, un setPipeline cada pocos elementos. Cada una de esas intercalaciones parte la secuencia en dos y es exactamente lo que multi-draw indirect no puede hacer, porque emite N dibujados con el mismo estado. Es decir: la disciplina que te prepara para la funcionalidad futura es la misma que te hace ir más rápido hoy, porque los cambios de estado son el otro coste de CPU relevante. Y ahí está el remate que casi nadie ve: si organizas el código para que un solo estado cubra muchos lotes —megabúfer de vértices, materiales indexados desde un storage buffer, texturas en arrays— acabas necesitando mucho menos multi-draw indirect de lo que creías, porque el número de secuencias contiguas que te quedan es pequeño. La funcionalidad que falta importa en proporción inversa a lo bien organizado que esté lo que ya tienes.
Lo que no arregla, para cerrar con honestidad
Aunque llegara mañana al núcleo del API, multi-draw indirect no convertiría un renderizador web en uno de última generación, y conviene no atribuirle poderes que no tiene.
No permite cambiar de pipeline ni de bind group entre dibujados: todos los que emite comparten estado. No genera geometría, que es lo que hacen los mesh shaders y lo que hace falta para el nivel de detalle por grupos de triángulos. No resuelve la selección de materiales sin descriptores sin límite, que es otra pieza ausente. Y no toca en absoluto el coste de GPU, que es donde está el cuello en la mayoría de las escenas web.
Lo que aporta es que el término proporcional a la variedad del contenido desaparezca del coste de CPU. Es una mejora real, es acotada, y saber su tamaño exacto —esa tabla de más arriba— es más útil que esperarla.