Simulación de fluidos en Rust: CPU vs GPU con compute shaders

Simulación de fluidos en Rust: CPU vs GPU con compute shaders

05 Aug 2026 Violetta H. 9 vistas

¡Hola, entusiasta del código y los píxeles danzantes!

Soy Violetta, y hoy nos vamos a subir a la montaña rusa más emocionante del desarrollo de motores indie: la simulación de fluidos en tiempo real. No, no hablo de ver cómo se derrite un cubo de hielo en un vaso, sino de crear océanos, humo y explosiones de partículas que hagan sudar a tu GPU (de felicidad).

La fecha es 5 de agosto de 2026, y si algo hemos aprendido en estos años es que la CPU ya no es la reina del baile. La fiesta se mudó a la GPU, y el anfitrión es el compute shader. Pero, ¿cómo diablos lo hacemos en Rust, ese lenguaje que amamos por su seguridad y odiamos por su curva de aprendizaje? Hoy vamos a comparar dos enfoques: el "Viejo Testamento" (CPU mandando matrices) y el "Nuevo Evangelio" (GPU calculando posiciones). Prepárate para una comparativa que te hará replantearte tu arquitectura.


El Problema: 40,000 Puntos de Agonía

Imagina que quieres renderizar 40,000 gotas de agua en tu motor indie. En el enfoque clásico, cada gota es un objeto con su propia matriz de transformación. Tu CPU, cual oficinista abrumado, debe calcular y enviar 64 bytes por matriz a la GPU. ¿El resultado? 10-15 FPS. Es como intentar llenar una piscina con un gotero. El cuello de botella no es tu tarjeta gráfica, sino el pobre bus PCI Express y un procesador que suplica piedad.

La transferencia de datos es el asesino silencioso. 40,000 matrices son 2.56 MB de datos por frame. A 60 FPS, eso es más de 150 MB por segundo solo para mover datos que la GPU podría calcular ella misma. Es un desperdicio monumental de ancho de banda y ciclos de CPU que podrían usarse para la lógica del juego o la IA.


La Solución: Compute Shaders al Rescate

Aquí es donde entra el compute shader, el superhéroe que no sabías que necesitabas. En lugar de que la CPU haga todo el trabajo pesado, le decimos a la GPU: "Oye, aquí tienes las reglas del juego. Calcula tú las posiciones". En Rust, esto se traduce en usar APIs como wgpu o Vulkan para crear buffers de almacenamiento y pipelines de cómputo.

La clave está en el dispatch. En lugar de 40,000 llamadas de dibujo, hacemos una sola. La GPU ejecuta un kernel que calcula las posiciones en paralelo. Es como pasar de tener 40,000 empleados enviando informes individuales a tener un superordenador que calcula todo en un nanosegundo. Los principios son universales, ya sea en Unity con ComputeBuffer o en Rust con wgpu::Buffer y StorageBuffer accesible desde tu shader.


Comparativa Técnica: La Batalla de los Buffers

Vamos a desglosar esto en secciones claras, porque soy fanática de la claridad (y de que no te duermas a mitad del artículo).

1. Arquitectura de Datos

  • Enfoque CPU: Cada punto necesita una matriz 4x4 (64 bytes). Los datos viven en la RAM del sistema y se copian a la VRAM cada frame. Es un flujo constante de información redundante.
  • Enfoque GPU (Compute): Solo necesitas un RWStructuredBuffer<float3> (o wgpu::Buffer con uso STORAGE). Los datos viven en la VRAM. El kernel escribe directamente en este buffer. La CPU solo envía parámetros pequeños como _Step, _Resolution y _Time (unos pocos bytes).

2. Flujo de Trabajo (Pipeline)

  • CPU: La CPU calcula la física, actualiza las matrices, y luego las envía. Luego, el renderizador las usa para dibujar. Es un proceso secuencial y rígido.
  • GPU: La CPU configura el compute pipeline con un kernel (definido con #[spirv(compute)] o @compute en wgsl), define numthreads(8,8,1) y ejecuta dispatch. Luego, sin transferir datos, un draw call instanciado usa ese buffer para pintar. La GPU hace todo: simula y renderiza.

3. Rendimiento y Escalabilidad

  • CPU: Con 40,000 puntos, te quedas en 10-15 FPS. Con 1 millón, tu PC se convierte en una tostadora. El cuello de botella es la CPU y el bus de datos.
  • GPU: Con 1 millón de puntos, obtienes 24 FPS con sombras y 65 FPS sin ellas. El cuello de botella ahora es la GPU, que es exactamente donde queremos que esté. La CPU está libre para hacer otras cosas, como gestionar la entrada del jugador o la IA. La escalabilidad es exponencialmente mejor.

Implementación Paso a Paso (El Baile del Código)

Para Rust, con wgpu, el proceso es una coreografía elegante:

  1. Crea el Buffer de Almacenamiento: Un wgpu::Buffer con usage: BufferUsages::STORAGE | BufferUsages::VERTEX. Aquí es donde la GPU escribirá las posiciones.
  2. Define el Kernel: En tu WGSL, escribe algo como @compute @workgroup_size(8, 8, 1). Dentro, usa @builtin(global_invocation_id) (equivalente a SV_DispatchThreadID) para que cada hilo calcule una coordenada UV y la posición correspondiente. Esto es pura matemática procedural, como generar ondas con senos y cosenos.
  3. Dispatch: En Rust, llama compute_pass.dispatch_workgroups(width / 8, height / 8, 1). Esto activa la GPU.
  4. Renderizado Procedural: El shader de fragmentos necesita una función configure_procedural() (o ConfigureProcedural en HLSL) que lea la instancia actual del buffer y construya la matriz de transformación in situ. En wgpu, usas @builtin(instance_index) para acceder al buffer.

Los Detalles que Importan (Y que Te Salvarán el Finde)

  • Gestión de Recursos: En Rust, olvídate de OnEnable/OnDisable. Usa RAII. Crea el buffer en el init() y destrúyelo en el drop(). Esto evita fugas de memoria y problemas con hot reloads.
  • Compatibilidad: Asegúrate de que tu GPU soporte compute shaders. En WebGPU (wgpu), el target 4.5 o superior es tu amigo. Para OpenGL ES 3.1+, también funciona.
  • Optimización de Kernels: No hagas un kernel monolítico. Divide la simulación en fases: un kernel para calcular fuerzas, otro para integrar velocidades, otro para actualizar posiciones. Usa macros para activar/desactivar features sin duplicar código.

Reflexión Final: ¿Vale la Pena?

Absolutamente. La diferencia entre 10 FPS y 65 FPS no es una mejora, es un cambio de paradigma. Implementar compute shaders en Rust es como pasar de un coche de caballos a un coche eléctrico: la potencia está ahí, pero necesitas aprender a manejar la nueva interfaz. Al principio, la curva de aprendizaje de wgpu y WGSL te hará tirar el teclado, pero una vez que ves 1 millón de partículas moviéndose como un fluido real a 60 FPS, no hay vuelta atrás.

Mi recomendación:

  • Empieza pequeño. Simula 10,000 partículas, luego 100,000.
  • Domina el dispatch y el buffer.
  • No tengas miedo de leer los ejemplos de wgpu (son tu mejor amigo).
  • Recuerda, el objetivo no es solo renderizar, es simular en la GPU.

Deja que tu CPU respire y dedica tiempo a la lógica del juego. En 2026, la frontera entre simulación y renderizado se ha desdibujado. Los motores indie que no adopten esta filosofía quedarán atrás.

V
Autor del artículo Violetta H.

Comentarios

Deja un comentario