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: &[&global_bind_group_layout, &local_bind_group_layout],
204            push_constant_ranges: &[],
205        });
206
207        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
208            label: None,
209            layout: Some(&pipeline_layout),
210            vertex: wgpu::VertexState {
211                module: &shader,
212                entry_point: Some("vs_main"),
213                compilation_options: Default::default(),
214                buffers: &[],
215            },
216            fragment: Some(wgpu::FragmentState {
217                module: &shader,
218                entry_point: Some("fs_main"),
219                compilation_options: Default::default(),
220                targets: &[Some(wgpu::ColorTargetState {
221                    format: config.view_formats[0],
222                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
223                    write_mask: wgpu::ColorWrites::default(),
224                })],
225            }),
226            primitive: wgpu::PrimitiveState {
227                topology: wgpu::PrimitiveTopology::TriangleStrip,
228                strip_index_format: Some(wgpu::IndexFormat::Uint16),
229                ..wgpu::PrimitiveState::default()
230            },
231            depth_stencil: None,
232            multisample: wgpu::MultisampleState::default(),
233            multiview_mask: None,
234            cache: None,
235        });
236
237        let texture = {
238            let img_data = include_bytes!("../../../../logo.png");
239            let decoder = png::Decoder::new(std::io::Cursor::new(img_data));
240            let mut reader = decoder.read_info().unwrap();
241            let buf_len = reader
242                .output_buffer_size()
243                .expect("output buffer would not fit in memory");
244            let mut buf = vec![0; buf_len];
245            let info = reader.next_frame(&mut buf).unwrap();
246
247            let size = wgpu::Extent3d {
248                width: info.width,
249                height: info.height,
250                depth_or_array_layers: 1,
251            };
252            let texture = device.create_texture(&wgpu::TextureDescriptor {
253                label: None,
254                size,
255                mip_level_count: 1,
256                sample_count: 1,
257                dimension: wgpu::TextureDimension::D2,
258                format: wgpu::TextureFormat::Rgba8UnormSrgb,
259                usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
260                view_formats: &[],
261            });
262            queue.write_texture(
263                texture.as_image_copy(),
264                &buf,
265                wgpu::TexelCopyBufferLayout {
266                    offset: 0,
267                    bytes_per_row: Some(info.width * 4),
268                    rows_per_image: None,
269                },
270                size,
271            );
272            texture
273        };
274
275        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
276            label: None,
277            address_mode_u: wgpu::AddressMode::ClampToEdge,
278            address_mode_v: wgpu::AddressMode::ClampToEdge,
279            address_mode_w: wgpu::AddressMode::ClampToEdge,
280            mag_filter: wgpu::FilterMode::Linear,
281            min_filter: wgpu::FilterMode::Nearest,
282            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
283            ..Default::default()
284        });
285
286        let globals = Globals {
287            mvp: glam::Mat4::orthographic_rh(
288                0.0,
289                config.width as f32,
290                0.0,
291                config.height as f32,
292                -1.0,
293                1.0,
294            )
295            .to_cols_array_2d(),
296            size: [BUNNY_SIZE; 2],
297            pad: [0.0; 2],
298        };
299
300        let global_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
301            label: Some("global"),
302            contents: bytemuck::bytes_of(&globals),
303            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM,
304        });
305        let uniform_alignment =
306            device.limits().min_uniform_buffer_offset_alignment as wgpu::BufferAddress;
307        let local_buffer = device.create_buffer(&wgpu::BufferDescriptor {
308            label: Some("local"),
309            size: (MAX_BUNNIES as wgpu::BufferAddress) * uniform_alignment,
310            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM,
311            mapped_at_creation: false,
312        });
313
314        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
315        let global_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
316            layout: &global_bind_group_layout,
317            entries: &[
318                wgpu::BindGroupEntry {
319                    binding: 0,
320                    resource: global_buffer.as_entire_binding(),
321                },
322                wgpu::BindGroupEntry {
323                    binding: 1,
324                    resource: wgpu::BindingResource::TextureView(&view),
325                },
326                wgpu::BindGroupEntry {
327                    binding: 2,
328                    resource: wgpu::BindingResource::Sampler(&sampler),
329                },
330            ],
331            label: None,
332        });
333        let local_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
334            layout: &local_bind_group_layout,
335            entries: &[wgpu::BindGroupEntry {
336                binding: 0,
337                resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
338                    buffer: &local_buffer,
339                    offset: 0,
340                    size: wgpu::BufferSize::new(size_of::<Bunny>() as _),
341                }),
342            }],
343            label: None,
344        });
345
346        let rng = WyRand::new_seed(42);
347
348        let mut ex = Example {
349            view,
350            sampler,
351            global_bind_group_layout,
352            pipeline,
353            global_group,
354            local_group,
355            bunnies: Vec::new(),
356            local_buffer,
357            extent: [config.width, config.height],
358            rng,
359        };
360
361        ex.spawn_bunnies();
362
363        ex
364    }
365
366    fn update(&mut self, event: winit::event::WindowEvent) {
367        if let winit::event::WindowEvent::KeyboardInput {
368            event:
369                KeyEvent {
370                    logical_key: Key::Named(NamedKey::Space),
371                    state: ElementState::Pressed,
372                    ..
373                },
374            ..
375        } = event
376        {
377            self.spawn_bunnies();
378        }
379    }
380
381    fn resize(
382        &mut self,
383        sc_desc: &wgpu::SurfaceConfiguration,
384        device: &wgpu::Device,
385        _queue: &wgpu::Queue,
386    ) {
387        self.extent = [sc_desc.width, sc_desc.height];
388
389        let globals = Globals {
390            mvp: glam::Mat4::orthographic_rh(
391                0.0,
392                sc_desc.width as f32,
393                0.0,
394                sc_desc.height as f32,
395                -1.0,
396                1.0,
397            )
398            .to_cols_array_2d(),
399            size: [BUNNY_SIZE; 2],
400            pad: [0.0; 2],
401        };
402
403        let global_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
404            label: Some("global"),
405            contents: bytemuck::bytes_of(&globals),
406            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM,
407        });
408
409        let global_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
410            layout: &self.global_bind_group_layout,
411            entries: &[
412                wgpu::BindGroupEntry {
413                    binding: 0,
414                    resource: global_buffer.as_entire_binding(),
415                },
416                wgpu::BindGroupEntry {
417                    binding: 1,
418                    resource: wgpu::BindingResource::TextureView(&self.view),
419                },
420                wgpu::BindGroupEntry {
421                    binding: 2,
422                    resource: wgpu::BindingResource::Sampler(&self.sampler),
423                },
424            ],
425            label: None,
426        });
427        self.global_group = global_group;
428    }
429
430    fn render(&mut self, view: &wgpu::TextureView, device: &wgpu::Device, queue: &wgpu::Queue) {
431        self.render_inner(view, device, queue);
432    }
433}
434
435pub fn main() {
436    crate::framework::run::<Example>("bunnymark");
437}
438
439#[cfg(test)]
440#[wgpu_test::gpu_test]
441pub static TEST: crate::framework::ExampleTestParams = crate::framework::ExampleTestParams {
442    name: "bunnymark",
443    image_path: "/examples/features/src/bunnymark/screenshot.png",
444    width: 1024,
445    height: 768,
446    optional_features: wgpu::Features::default(),
447    base_test_parameters: wgpu_test::TestParameters::default(),
448    // We're looking for very small differences, so look in the high percentiles.
449    comparisons: &[
450        wgpu_test::ComparisonType::Mean(0.05),
451        wgpu_test::ComparisonType::Percentile {
452            percentile: 0.99,
453            threshold: 0.37,
454        },
455    ],
456    _phantom: std::marker::PhantomData::<Example>,
457};