wgpu_examples/boids/
mod.rs

1// Flocking boids example with gpu compute update pass
2// adapted from https://github.com/austinEng/webgpu-samples/blob/master/src/examples/computeBoids.ts
3
4use nanorand::{Rng, WyRand};
5use wgpu::util::DeviceExt;
6
7// number of boid particles to simulate
8
9const NUM_PARTICLES: u32 = 1500;
10
11// number of single-particle calculations (invocations) in each gpu work group
12
13const PARTICLES_PER_GROUP: u32 = 64;
14
15/// Example struct holds references to wgpu resources and frame persistent data
16struct Example {
17    particle_bind_groups: Vec<wgpu::BindGroup>,
18    particle_buffers: Vec<wgpu::Buffer>,
19    vertices_buffer: wgpu::Buffer,
20    compute_pipeline: wgpu::ComputePipeline,
21    render_pipeline: wgpu::RenderPipeline,
22    work_group_count: u32,
23    frame_num: usize,
24}
25
26impl crate::framework::Example for Example {
27    fn required_limits() -> wgpu::Limits {
28        wgpu::Limits::downlevel_defaults()
29    }
30
31    fn required_downlevel_capabilities() -> wgpu::DownlevelCapabilities {
32        wgpu::DownlevelCapabilities {
33            flags: wgpu::DownlevelFlags::COMPUTE_SHADERS,
34            ..Default::default()
35        }
36    }
37
38    /// constructs initial instance of Example struct
39    fn init(
40        config: &wgpu::SurfaceConfiguration,
41        _adapter: &wgpu::Adapter,
42        device: &wgpu::Device,
43        _queue: &wgpu::Queue,
44    ) -> Self {
45        let compute_shader = device.create_shader_module(wgpu::include_wgsl!("compute.wgsl"));
46        let draw_shader = device.create_shader_module(wgpu::include_wgsl!("draw.wgsl"));
47
48        // buffer for simulation parameters uniform
49
50        let sim_param_data = [
51            0.04f32, // deltaT
52            0.1,     // rule1Distance
53            0.025,   // rule2Distance
54            0.025,   // rule3Distance
55            0.02,    // rule1Scale
56            0.05,    // rule2Scale
57            0.005,   // rule3Scale
58        ]
59        .to_vec();
60        let sim_param_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
61            label: Some("Simulation Parameter Buffer"),
62            contents: bytemuck::cast_slice(&sim_param_data),
63            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
64        });
65
66        // create compute bind layout group and compute pipeline layout
67
68        let compute_bind_group_layout =
69            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
70                entries: &[
71                    wgpu::BindGroupLayoutEntry {
72                        binding: 0,
73                        visibility: wgpu::ShaderStages::COMPUTE,
74                        ty: wgpu::BindingType::Buffer {
75                            ty: wgpu::BufferBindingType::Uniform,
76                            has_dynamic_offset: false,
77                            min_binding_size: wgpu::BufferSize::new(
78                                (sim_param_data.len() * size_of::<f32>()) as _,
79                            ),
80                        },
81                        count: None,
82                    },
83                    wgpu::BindGroupLayoutEntry {
84                        binding: 1,
85                        visibility: wgpu::ShaderStages::COMPUTE,
86                        ty: wgpu::BindingType::Buffer {
87                            ty: wgpu::BufferBindingType::Storage { read_only: true },
88                            has_dynamic_offset: false,
89                            min_binding_size: wgpu::BufferSize::new((NUM_PARTICLES * 16) as _),
90                        },
91                        count: None,
92                    },
93                    wgpu::BindGroupLayoutEntry {
94                        binding: 2,
95                        visibility: wgpu::ShaderStages::COMPUTE,
96                        ty: wgpu::BindingType::Buffer {
97                            ty: wgpu::BufferBindingType::Storage { read_only: false },
98                            has_dynamic_offset: false,
99                            min_binding_size: wgpu::BufferSize::new((NUM_PARTICLES * 16) as _),
100                        },
101                        count: None,
102                    },
103                ],
104                label: None,
105            });
106        let compute_pipeline_layout =
107            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
108                label: Some("compute"),
109                bind_group_layouts: &[&compute_bind_group_layout],
110                push_constant_ranges: &[],
111            });
112
113        // create render pipeline with empty bind group layout
114
115        let render_pipeline_layout =
116            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
117                label: Some("render"),
118                bind_group_layouts: &[],
119                push_constant_ranges: &[],
120            });
121
122        let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
123            label: None,
124            layout: Some(&render_pipeline_layout),
125            vertex: wgpu::VertexState {
126                module: &draw_shader,
127                entry_point: Some("main_vs"),
128                compilation_options: Default::default(),
129                buffers: &[
130                    wgpu::VertexBufferLayout {
131                        array_stride: 4 * 4,
132                        step_mode: wgpu::VertexStepMode::Instance,
133                        attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2],
134                    },
135                    wgpu::VertexBufferLayout {
136                        array_stride: 2 * 4,
137                        step_mode: wgpu::VertexStepMode::Vertex,
138                        attributes: &wgpu::vertex_attr_array![2 => Float32x2],
139                    },
140                ],
141            },
142            fragment: Some(wgpu::FragmentState {
143                module: &draw_shader,
144                entry_point: Some("main_fs"),
145                compilation_options: Default::default(),
146                targets: &[Some(config.view_formats[0].into())],
147            }),
148            primitive: wgpu::PrimitiveState::default(),
149            depth_stencil: None,
150            multisample: wgpu::MultisampleState::default(),
151            multiview: None,
152            cache: None,
153        });
154
155        // create compute pipeline
156
157        let compute_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
158            label: Some("Compute pipeline"),
159            layout: Some(&compute_pipeline_layout),
160            module: &compute_shader,
161            entry_point: Some("main"),
162            compilation_options: Default::default(),
163            cache: None,
164        });
165
166        // buffer for the three 2d triangle vertices of each instance
167
168        let vertex_buffer_data = [-0.01f32, -0.02, 0.01, -0.02, 0.00, 0.02];
169        let vertices_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
170            label: Some("Vertex Buffer"),
171            contents: bytemuck::bytes_of(&vertex_buffer_data),
172            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
173        });
174
175        // buffer for all particles data of type [(posx,posy,velx,vely),...]
176
177        let mut initial_particle_data = vec![0.0f32; (4 * NUM_PARTICLES) as usize];
178        let mut rng = WyRand::new_seed(42);
179        let mut unif = || rng.generate::<f32>() * 2f32 - 1f32; // Generate a num (-1, 1)
180        for particle_instance_chunk in initial_particle_data.chunks_mut(4) {
181            particle_instance_chunk[0] = unif(); // posx
182            particle_instance_chunk[1] = unif(); // posy
183            particle_instance_chunk[2] = unif() * 0.1; // velx
184            particle_instance_chunk[3] = unif() * 0.1; // vely
185        }
186
187        // creates two buffers of particle data each of size NUM_PARTICLES
188        // the two buffers alternate as dst and src for each frame
189
190        let mut particle_buffers = Vec::<wgpu::Buffer>::new();
191        let mut particle_bind_groups = Vec::<wgpu::BindGroup>::new();
192        for i in 0..2 {
193            particle_buffers.push(
194                device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
195                    label: Some(&format!("Particle Buffer {i}")),
196                    contents: bytemuck::cast_slice(&initial_particle_data),
197                    usage: wgpu::BufferUsages::VERTEX
198                        | wgpu::BufferUsages::STORAGE
199                        | wgpu::BufferUsages::COPY_DST,
200                }),
201            );
202        }
203
204        // create two bind groups, one for each buffer as the src
205        // where the alternate buffer is used as the dst
206
207        for i in 0..2 {
208            particle_bind_groups.push(device.create_bind_group(&wgpu::BindGroupDescriptor {
209                layout: &compute_bind_group_layout,
210                entries: &[
211                    wgpu::BindGroupEntry {
212                        binding: 0,
213                        resource: sim_param_buffer.as_entire_binding(),
214                    },
215                    wgpu::BindGroupEntry {
216                        binding: 1,
217                        resource: particle_buffers[i].as_entire_binding(),
218                    },
219                    wgpu::BindGroupEntry {
220                        binding: 2,
221                        resource: particle_buffers[(i + 1) % 2].as_entire_binding(), // bind to opposite buffer
222                    },
223                ],
224                label: None,
225            }));
226        }
227
228        // calculates number of work groups from PARTICLES_PER_GROUP constant
229        let work_group_count =
230            ((NUM_PARTICLES as f32) / (PARTICLES_PER_GROUP as f32)).ceil() as u32;
231
232        // returns Example struct and No encoder commands
233
234        Example {
235            particle_bind_groups,
236            particle_buffers,
237            vertices_buffer,
238            compute_pipeline,
239            render_pipeline,
240            work_group_count,
241            frame_num: 0,
242        }
243    }
244
245    /// update is called for any WindowEvent not handled by the framework
246    fn update(&mut self, _event: winit::event::WindowEvent) {
247        //empty
248    }
249
250    /// resize is called on WindowEvent::Resized events
251    fn resize(
252        &mut self,
253        _sc_desc: &wgpu::SurfaceConfiguration,
254        _device: &wgpu::Device,
255        _queue: &wgpu::Queue,
256    ) {
257        //empty
258    }
259
260    /// render is called each frame, dispatching compute groups proportional
261    ///   a TriangleList draw call for all NUM_PARTICLES at 3 vertices each
262    fn render(&mut self, view: &wgpu::TextureView, device: &wgpu::Device, queue: &wgpu::Queue) {
263        // create render pass descriptor and its color attachments
264        let color_attachments = [Some(wgpu::RenderPassColorAttachment {
265            view,
266            depth_slice: None,
267            resolve_target: None,
268            ops: wgpu::Operations {
269                load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
270                store: wgpu::StoreOp::Store,
271            },
272        })];
273        let render_pass_descriptor = wgpu::RenderPassDescriptor {
274            label: None,
275            color_attachments: &color_attachments,
276            depth_stencil_attachment: None,
277            timestamp_writes: None,
278            occlusion_query_set: None,
279        };
280
281        // get command encoder
282        let mut command_encoder =
283            device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
284
285        command_encoder.push_debug_group("compute boid movement");
286        {
287            // compute pass
288            let mut cpass = command_encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
289                label: None,
290                timestamp_writes: None,
291            });
292            cpass.set_pipeline(&self.compute_pipeline);
293            cpass.set_bind_group(0, &self.particle_bind_groups[self.frame_num % 2], &[]);
294            cpass.dispatch_workgroups(self.work_group_count, 1, 1);
295        }
296        command_encoder.pop_debug_group();
297
298        command_encoder.push_debug_group("render boids");
299        {
300            // render pass
301            let mut rpass = command_encoder.begin_render_pass(&render_pass_descriptor);
302            rpass.set_pipeline(&self.render_pipeline);
303            // render dst particles
304            rpass.set_vertex_buffer(0, self.particle_buffers[(self.frame_num + 1) % 2].slice(..));
305            // the three instance-local vertices
306            rpass.set_vertex_buffer(1, self.vertices_buffer.slice(..));
307            rpass.draw(0..3, 0..NUM_PARTICLES);
308        }
309        command_encoder.pop_debug_group();
310
311        // update frame count
312        self.frame_num += 1;
313
314        // done
315        queue.submit(Some(command_encoder.finish()));
316    }
317}
318
319/// run example
320pub fn main() {
321    crate::framework::run::<Example>("boids");
322}
323
324#[cfg(test)]
325#[wgpu_test::gpu_test]
326pub static TEST: crate::framework::ExampleTestParams = crate::framework::ExampleTestParams {
327    name: "boids",
328    // Generated on 1080ti on Vk/Windows
329    image_path: "/examples/features/src/boids/screenshot.png",
330    width: 1024,
331    height: 768,
332    optional_features: wgpu::Features::default(),
333    base_test_parameters: wgpu_test::TestParameters::default()
334        .downlevel_flags(wgpu::DownlevelFlags::COMPUTE_SHADERS)
335        .limits(wgpu::Limits::downlevel_defaults())
336        // Lots of validation errors, maybe related to https://github.com/gfx-rs/wgpu/issues/3160
337        .expect_fail(wgpu_test::FailureCase::molten_vk()),
338    comparisons: &[wgpu_test::ComparisonType::Mean(0.005)],
339    _phantom: std::marker::PhantomData::<Example>,
340};