wgpu_examples/bunnymark/
mod.rs

1use bytemuck::{Pod, Zeroable};
2use nanorand::{Rng, WyRand};
3use std::borrow::Cow;
4use wgpu::util::DeviceExt;
5use winit::{
6    event::{ElementState, KeyEvent},
7    keyboard::{Key, NamedKey},
8};
9
10const MAX_BUNNIES: usize = 1 << 20;
11const BUNNY_SIZE: f32 = 0.15 * 256.0;
12const GRAVITY: f32 = -9.8 * 100.0;
13const MAX_VELOCITY: f32 = 750.0;
14
15#[repr(C)]
16#[derive(Clone, Copy, Pod, Zeroable)]
17struct Globals {
18    mvp: [[f32; 4]; 4],
19    size: [f32; 2],
20    pad: [f32; 2],
21}
22
23#[repr(C, align(256))]
24#[derive(Clone, Copy, Pod, Zeroable)]
25struct Bunny {
26    position: [f32; 2],
27    velocity: [f32; 2],
28    color: u32,
29    _pad: [u32; (256 - 20) / 4],
30}
31
32impl Bunny {
33    fn update_data(&mut self, delta: f32, extent: &[u32; 2]) {
34        self.position[0] += self.velocity[0] * delta;
35        self.position[1] += self.velocity[1] * delta;
36        self.velocity[1] += GRAVITY * delta;
37
38        if (self.velocity[0] > 0.0 && self.position[0] + 0.5 * BUNNY_SIZE > extent[0] as f32)
39            || (self.velocity[0] < 0.0 && self.position[0] - 0.5 * BUNNY_SIZE < 0.0)
40        {
41            self.velocity[0] *= -1.0;
42        }
43
44        if self.velocity[1] < 0.0 && self.position[1] < 0.5 * BUNNY_SIZE {
45            self.velocity[1] *= -1.0;
46        }
47
48        // Top boundary check
49        if self.velocity[1] > 0.0 && self.position[1] + 0.5 * BUNNY_SIZE > extent[1] as f32 {
50            self.velocity[1] *= -1.0;
51        }
52    }
53}
54
55/// Example struct holds references to wgpu resources and frame persistent data
56struct Example {
57    view: wgpu::TextureView,
58    sampler: wgpu::Sampler,
59    global_bind_group_layout: wgpu::BindGroupLayout,
60    global_group: wgpu::BindGroup,
61    local_group: wgpu::BindGroup,
62    pipeline: wgpu::RenderPipeline,
63    bunnies: Vec<Bunny>,
64    local_buffer: wgpu::Buffer,
65    extent: [u32; 2],
66    rng: WyRand,
67}
68
69impl Example {
70    fn spawn_bunnies(&mut self) {
71        let spawn_count = 64;
72        let color = self.rng.generate::<u32>();
73        println!(
74            "Spawning {} bunnies, total at {}",
75            spawn_count,
76            self.bunnies.len() + spawn_count
77        );
78        for _ in 0..spawn_count {
79            let speed = self.rng.generate::<f32>() * MAX_VELOCITY - (MAX_VELOCITY * 0.5);
80            self.bunnies.push(Bunny {
81                position: [0.0, 0.5 * (self.extent[1] as f32)],
82                velocity: [speed, 0.0],
83                color,
84                _pad: Zeroable::zeroed(),
85            });
86        }
87    }
88
89    fn render_inner(
90        &mut self,
91        view: &wgpu::TextureView,
92        device: &wgpu::Device,
93        queue: &wgpu::Queue,
94    ) {
95        let delta = 0.01;
96        for bunny in self.bunnies.iter_mut() {
97            bunny.update_data(delta, &self.extent);
98        }
99
100        let uniform_alignment = device.limits().min_uniform_buffer_offset_alignment;
101        queue.write_buffer(&self.local_buffer, 0, bytemuck::cast_slice(&self.bunnies));
102
103        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
104        {
105            let clear_color = wgpu::Color {
106                r: 0.1,
107                g: 0.2,
108                b: 0.3,
109                a: 1.0,
110            };
111            let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
112                label: None,
113                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
114                    view,
115                    depth_slice: None,
116                    resolve_target: None,
117                    ops: wgpu::Operations {
118                        load: wgpu::LoadOp::Clear(clear_color),
119                        store: wgpu::StoreOp::Store,
120                    },
121                })],
122                depth_stencil_attachment: None,
123                timestamp_writes: None,
124                occlusion_query_set: None,
125                multiview_mask: None,
126            });
127            rpass.set_pipeline(&self.pipeline);
128            rpass.set_bind_group(0, &self.global_group, &[]);
129            for i in 0..self.bunnies.len() {
130                let offset =
131                    (i as wgpu::DynamicOffset) * (uniform_alignment as wgpu::DynamicOffset);
132                rpass.set_bind_group(1, &self.local_group, &[offset]);
133                rpass.draw(0..4, 0..1);
134            }
135        }
136
137        queue.submit(Some(encoder.finish()));
138    }
139}
140
141impl crate::framework::Example for Example {
142    fn init(
143        config: &wgpu::SurfaceConfiguration,
144        _adapter: &wgpu::Adapter,
145        device: &wgpu::Device,
146        queue: &wgpu::Queue,
147    ) -> Self {
148        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
149            label: None,
150            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
151                "../../../../wgpu-hal/examples/halmark/shader.wgsl"
152            ))),
153        });
154
155        let global_bind_group_layout =
156            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
157                entries: &[
158                    wgpu::BindGroupLayoutEntry {
159                        binding: 0,
160                        visibility: wgpu::ShaderStages::VERTEX,
161                        ty: wgpu::BindingType::Buffer {
162                            ty: wgpu::BufferBindingType::Uniform,
163                            has_dynamic_offset: false,
164                            min_binding_size: wgpu::BufferSize::new(size_of::<Globals>() as _),
165                        },
166                        count: None,
167                    },
168                    wgpu::BindGroupLayoutEntry {
169                        binding: 1,
170                        visibility: wgpu::ShaderStages::FRAGMENT,
171                        ty: wgpu::BindingType::Texture {
172                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
173                            view_dimension: wgpu::TextureViewDimension::D2,
174                            multisampled: false,
175                        },
176                        count: None,
177                    },
178                    wgpu::BindGroupLayoutEntry {
179                        binding: 2,
180                        visibility: wgpu::ShaderStages::FRAGMENT,
181                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
182                        count: None,
183                    },
184                ],
185                label: None,
186            });
187        let local_bind_group_layout =
188            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
189                entries: &[wgpu::BindGroupLayoutEntry {
190                    binding: 0,
191                    visibility: wgpu::ShaderStages::VERTEX,
192                    ty: wgpu::BindingType::Buffer {
193                        ty: wgpu::BufferBindingType::Uniform,
194                        has_dynamic_offset: true,
195                        min_binding_size: wgpu::BufferSize::new(size_of::<Bunny>() as _),
196                    },
197                    count: None,
198                }],
199                label: None,
200            });
201        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
202            label: None,
203            bind_group_layouts: &[
204                Some(&global_bind_group_layout),
205                Some(&local_bind_group_layout),
206            ],
207            immediate_size: 0,
208        });
209
210        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
211            label: None,
212            layout: Some(&pipeline_layout),
213            vertex: wgpu::VertexState {
214                module: &shader,
215                entry_point: Some("vs_main"),
216                compilation_options: Default::default(),
217                buffers: &[],
218            },
219            fragment: Some(wgpu::FragmentState {
220                module: &shader,
221                entry_point: Some("fs_main"),
222                compilation_options: Default::default(),
223                targets: &[Some(wgpu::ColorTargetState {
224                    format: config.view_formats[0],
225                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
226                    write_mask: wgpu::ColorWrites::default(),
227                })],
228            }),
229            primitive: wgpu::PrimitiveState {
230                topology: wgpu::PrimitiveTopology::TriangleStrip,
231                strip_index_format: Some(wgpu::IndexFormat::Uint16),
232                ..wgpu::PrimitiveState::default()
233            },
234            depth_stencil: None,
235            multisample: wgpu::MultisampleState::default(),
236            multiview_mask: None,
237            cache: None,
238        });
239
240        let texture = {
241            let img_data = include_bytes!("../../../../logo.png");
242            let decoder = png::Decoder::new(std::io::Cursor::new(img_data));
243            let mut reader = decoder.read_info().unwrap();
244            let buf_len = reader
245                .output_buffer_size()
246                .expect("output buffer would not fit in memory");
247            let mut buf = vec![0; buf_len];
248            let info = reader.next_frame(&mut buf).unwrap();
249
250            let size = wgpu::Extent3d {
251                width: info.width,
252                height: info.height,
253                depth_or_array_layers: 1,
254            };
255            let texture = device.create_texture(&wgpu::TextureDescriptor {
256                label: None,
257                size,
258                mip_level_count: 1,
259                sample_count: 1,
260                dimension: wgpu::TextureDimension::D2,
261                format: wgpu::TextureFormat::Rgba8UnormSrgb,
262                usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
263                view_formats: &[],
264            });
265            queue.write_texture(
266                texture.as_image_copy(),
267                &buf,
268                wgpu::TexelCopyBufferLayout {
269                    offset: 0,
270                    bytes_per_row: Some(info.width * 4),
271                    rows_per_image: None,
272                },
273                size,
274            );
275            texture
276        };
277
278        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
279            label: None,
280            address_mode_u: wgpu::AddressMode::ClampToEdge,
281            address_mode_v: wgpu::AddressMode::ClampToEdge,
282            address_mode_w: wgpu::AddressMode::ClampToEdge,
283            mag_filter: wgpu::FilterMode::Linear,
284            min_filter: wgpu::FilterMode::Nearest,
285            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
286            ..Default::default()
287        });
288
289        let globals = Globals {
290            mvp: glam::Mat4::orthographic_rh(
291                0.0,
292                config.width as f32,
293                0.0,
294                config.height as f32,
295                -1.0,
296                1.0,
297            )
298            .to_cols_array_2d(),
299            size: [BUNNY_SIZE; 2],
300            pad: [0.0; 2],
301        };
302
303        let global_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
304            label: Some("global"),
305            contents: bytemuck::bytes_of(&globals),
306            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM,
307        });
308        let uniform_alignment =
309            device.limits().min_uniform_buffer_offset_alignment as wgpu::BufferAddress;
310        let local_buffer = device.create_buffer(&wgpu::BufferDescriptor {
311            label: Some("local"),
312            size: (MAX_BUNNIES as wgpu::BufferAddress) * uniform_alignment,
313            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM,
314            mapped_at_creation: false,
315        });
316
317        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
318        let global_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
319            layout: &global_bind_group_layout,
320            entries: &[
321                wgpu::BindGroupEntry {
322                    binding: 0,
323                    resource: global_buffer.as_entire_binding(),
324                },
325                wgpu::BindGroupEntry {
326                    binding: 1,
327                    resource: wgpu::BindingResource::TextureView(&view),
328                },
329                wgpu::BindGroupEntry {
330                    binding: 2,
331                    resource: wgpu::BindingResource::Sampler(&sampler),
332                },
333            ],
334            label: None,
335        });
336        let local_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
337            layout: &local_bind_group_layout,
338            entries: &[wgpu::BindGroupEntry {
339                binding: 0,
340                resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
341                    buffer: &local_buffer,
342                    offset: 0,
343                    size: wgpu::BufferSize::new(size_of::<Bunny>() as _),
344                }),
345            }],
346            label: None,
347        });
348
349        let rng = WyRand::new_seed(42);
350
351        let mut ex = Example {
352            view,
353            sampler,
354            global_bind_group_layout,
355            pipeline,
356            global_group,
357            local_group,
358            bunnies: Vec::new(),
359            local_buffer,
360            extent: [config.width, config.height],
361            rng,
362        };
363
364        ex.spawn_bunnies();
365
366        ex
367    }
368
369    fn update(&mut self, event: winit::event::WindowEvent) {
370        if let winit::event::WindowEvent::KeyboardInput {
371            event:
372                KeyEvent {
373                    logical_key: Key::Named(NamedKey::Space),
374                    state: ElementState::Pressed,
375                    ..
376                },
377            ..
378        } = event
379        {
380            self.spawn_bunnies();
381        }
382    }
383
384    fn resize(
385        &mut self,
386        sc_desc: &wgpu::SurfaceConfiguration,
387        device: &wgpu::Device,
388        _queue: &wgpu::Queue,
389    ) {
390        self.extent = [sc_desc.width, sc_desc.height];
391
392        let globals = Globals {
393            mvp: glam::Mat4::orthographic_rh(
394                0.0,
395                sc_desc.width as f32,
396                0.0,
397                sc_desc.height as f32,
398                -1.0,
399                1.0,
400            )
401            .to_cols_array_2d(),
402            size: [BUNNY_SIZE; 2],
403            pad: [0.0; 2],
404        };
405
406        let global_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
407            label: Some("global"),
408            contents: bytemuck::bytes_of(&globals),
409            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM,
410        });
411
412        let global_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
413            layout: &self.global_bind_group_layout,
414            entries: &[
415                wgpu::BindGroupEntry {
416                    binding: 0,
417                    resource: global_buffer.as_entire_binding(),
418                },
419                wgpu::BindGroupEntry {
420                    binding: 1,
421                    resource: wgpu::BindingResource::TextureView(&self.view),
422                },
423                wgpu::BindGroupEntry {
424                    binding: 2,
425                    resource: wgpu::BindingResource::Sampler(&self.sampler),
426                },
427            ],
428            label: None,
429        });
430        self.global_group = global_group;
431    }
432
433    fn render(&mut self, view: &wgpu::TextureView, device: &wgpu::Device, queue: &wgpu::Queue) {
434        self.render_inner(view, device, queue);
435    }
436}
437
438pub fn main() {
439    crate::framework::run::<Example>("bunnymark");
440}
441
442#[cfg(test)]
443#[wgpu_test::gpu_test]
444pub static TEST: crate::framework::ExampleTestParams = crate::framework::ExampleTestParams {
445    name: "bunnymark",
446    image_path: "/examples/features/src/bunnymark/screenshot.png",
447    width: 1024,
448    height: 768,
449    optional_features: wgpu::Features::default(),
450    base_test_parameters: wgpu_test::TestParameters::default(),
451    // We're looking for very small differences, so look in the high percentiles.
452    comparisons: &[
453        wgpu_test::ComparisonType::Mean(0.05),
454        wgpu_test::ComparisonType::Percentile {
455            percentile: 0.99,
456            threshold: 0.37,
457        },
458    ],
459    _phantom: std::marker::PhantomData::<Example>,
460};