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            });
126            rpass.set_pipeline(&self.pipeline);
127            rpass.set_bind_group(0, &self.global_group, &[]);
128            for i in 0..self.bunnies.len() {
129                let offset =
130                    (i as wgpu::DynamicOffset) * (uniform_alignment as wgpu::DynamicOffset);
131                rpass.set_bind_group(1, &self.local_group, &[offset]);
132                rpass.draw(0..4, 0..1);
133            }
134        }
135
136        queue.submit(Some(encoder.finish()));
137    }
138}
139
140impl crate::framework::Example for Example {
141    fn init(
142        config: &wgpu::SurfaceConfiguration,
143        _adapter: &wgpu::Adapter,
144        device: &wgpu::Device,
145        queue: &wgpu::Queue,
146    ) -> Self {
147        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
148            label: None,
149            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
150                "../../../../wgpu-hal/examples/halmark/shader.wgsl"
151            ))),
152        });
153
154        let global_bind_group_layout =
155            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
156                entries: &[
157                    wgpu::BindGroupLayoutEntry {
158                        binding: 0,
159                        visibility: wgpu::ShaderStages::VERTEX,
160                        ty: wgpu::BindingType::Buffer {
161                            ty: wgpu::BufferBindingType::Uniform,
162                            has_dynamic_offset: false,
163                            min_binding_size: wgpu::BufferSize::new(size_of::<Globals>() as _),
164                        },
165                        count: None,
166                    },
167                    wgpu::BindGroupLayoutEntry {
168                        binding: 1,
169                        visibility: wgpu::ShaderStages::FRAGMENT,
170                        ty: wgpu::BindingType::Texture {
171                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
172                            view_dimension: wgpu::TextureViewDimension::D2,
173                            multisampled: false,
174                        },
175                        count: None,
176                    },
177                    wgpu::BindGroupLayoutEntry {
178                        binding: 2,
179                        visibility: wgpu::ShaderStages::FRAGMENT,
180                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
181                        count: None,
182                    },
183                ],
184                label: None,
185            });
186        let local_bind_group_layout =
187            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
188                entries: &[wgpu::BindGroupLayoutEntry {
189                    binding: 0,
190                    visibility: wgpu::ShaderStages::VERTEX,
191                    ty: wgpu::BindingType::Buffer {
192                        ty: wgpu::BufferBindingType::Uniform,
193                        has_dynamic_offset: true,
194                        min_binding_size: wgpu::BufferSize::new(size_of::<Bunny>() as _),
195                    },
196                    count: None,
197                }],
198                label: None,
199            });
200        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
201            label: None,
202            bind_group_layouts: &[&global_bind_group_layout, &local_bind_group_layout],
203            push_constant_ranges: &[],
204        });
205
206        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
207            label: None,
208            layout: Some(&pipeline_layout),
209            vertex: wgpu::VertexState {
210                module: &shader,
211                entry_point: Some("vs_main"),
212                compilation_options: Default::default(),
213                buffers: &[],
214            },
215            fragment: Some(wgpu::FragmentState {
216                module: &shader,
217                entry_point: Some("fs_main"),
218                compilation_options: Default::default(),
219                targets: &[Some(wgpu::ColorTargetState {
220                    format: config.view_formats[0],
221                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
222                    write_mask: wgpu::ColorWrites::default(),
223                })],
224            }),
225            primitive: wgpu::PrimitiveState {
226                topology: wgpu::PrimitiveTopology::TriangleStrip,
227                strip_index_format: Some(wgpu::IndexFormat::Uint16),
228                ..wgpu::PrimitiveState::default()
229            },
230            depth_stencil: None,
231            multisample: wgpu::MultisampleState::default(),
232            multiview: None,
233            cache: None,
234        });
235
236        let texture = {
237            let img_data = include_bytes!("../../../../logo.png");
238            let decoder = png::Decoder::new(std::io::Cursor::new(img_data));
239            let mut reader = decoder.read_info().unwrap();
240            let mut buf = vec![0; reader.output_buffer_size()];
241            let info = reader.next_frame(&mut buf).unwrap();
242
243            let size = wgpu::Extent3d {
244                width: info.width,
245                height: info.height,
246                depth_or_array_layers: 1,
247            };
248            let texture = device.create_texture(&wgpu::TextureDescriptor {
249                label: None,
250                size,
251                mip_level_count: 1,
252                sample_count: 1,
253                dimension: wgpu::TextureDimension::D2,
254                format: wgpu::TextureFormat::Rgba8UnormSrgb,
255                usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
256                view_formats: &[],
257            });
258            queue.write_texture(
259                texture.as_image_copy(),
260                &buf,
261                wgpu::TexelCopyBufferLayout {
262                    offset: 0,
263                    bytes_per_row: Some(info.width * 4),
264                    rows_per_image: None,
265                },
266                size,
267            );
268            texture
269        };
270
271        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
272            label: None,
273            address_mode_u: wgpu::AddressMode::ClampToEdge,
274            address_mode_v: wgpu::AddressMode::ClampToEdge,
275            address_mode_w: wgpu::AddressMode::ClampToEdge,
276            mag_filter: wgpu::FilterMode::Linear,
277            min_filter: wgpu::FilterMode::Nearest,
278            mipmap_filter: wgpu::FilterMode::Nearest,
279            ..Default::default()
280        });
281
282        let globals = Globals {
283            mvp: glam::Mat4::orthographic_rh(
284                0.0,
285                config.width as f32,
286                0.0,
287                config.height as f32,
288                -1.0,
289                1.0,
290            )
291            .to_cols_array_2d(),
292            size: [BUNNY_SIZE; 2],
293            pad: [0.0; 2],
294        };
295
296        let global_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
297            label: Some("global"),
298            contents: bytemuck::bytes_of(&globals),
299            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM,
300        });
301        let uniform_alignment =
302            device.limits().min_uniform_buffer_offset_alignment as wgpu::BufferAddress;
303        let local_buffer = device.create_buffer(&wgpu::BufferDescriptor {
304            label: Some("local"),
305            size: (MAX_BUNNIES as wgpu::BufferAddress) * uniform_alignment,
306            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM,
307            mapped_at_creation: false,
308        });
309
310        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
311        let global_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
312            layout: &global_bind_group_layout,
313            entries: &[
314                wgpu::BindGroupEntry {
315                    binding: 0,
316                    resource: global_buffer.as_entire_binding(),
317                },
318                wgpu::BindGroupEntry {
319                    binding: 1,
320                    resource: wgpu::BindingResource::TextureView(&view),
321                },
322                wgpu::BindGroupEntry {
323                    binding: 2,
324                    resource: wgpu::BindingResource::Sampler(&sampler),
325                },
326            ],
327            label: None,
328        });
329        let local_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
330            layout: &local_bind_group_layout,
331            entries: &[wgpu::BindGroupEntry {
332                binding: 0,
333                resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
334                    buffer: &local_buffer,
335                    offset: 0,
336                    size: wgpu::BufferSize::new(size_of::<Bunny>() as _),
337                }),
338            }],
339            label: None,
340        });
341
342        let rng = WyRand::new_seed(42);
343
344        let mut ex = Example {
345            view,
346            sampler,
347            global_bind_group_layout,
348            pipeline,
349            global_group,
350            local_group,
351            bunnies: Vec::new(),
352            local_buffer,
353            extent: [config.width, config.height],
354            rng,
355        };
356
357        ex.spawn_bunnies();
358
359        ex
360    }
361
362    fn update(&mut self, event: winit::event::WindowEvent) {
363        if let winit::event::WindowEvent::KeyboardInput {
364            event:
365                KeyEvent {
366                    logical_key: Key::Named(NamedKey::Space),
367                    state: ElementState::Pressed,
368                    ..
369                },
370            ..
371        } = event
372        {
373            self.spawn_bunnies();
374        }
375    }
376
377    fn resize(
378        &mut self,
379        sc_desc: &wgpu::SurfaceConfiguration,
380        device: &wgpu::Device,
381        _queue: &wgpu::Queue,
382    ) {
383        self.extent = [sc_desc.width, sc_desc.height];
384
385        let globals = Globals {
386            mvp: glam::Mat4::orthographic_rh(
387                0.0,
388                sc_desc.width as f32,
389                0.0,
390                sc_desc.height as f32,
391                -1.0,
392                1.0,
393            )
394            .to_cols_array_2d(),
395            size: [BUNNY_SIZE; 2],
396            pad: [0.0; 2],
397        };
398
399        let global_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
400            label: Some("global"),
401            contents: bytemuck::bytes_of(&globals),
402            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM,
403        });
404
405        let global_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
406            layout: &self.global_bind_group_layout,
407            entries: &[
408                wgpu::BindGroupEntry {
409                    binding: 0,
410                    resource: global_buffer.as_entire_binding(),
411                },
412                wgpu::BindGroupEntry {
413                    binding: 1,
414                    resource: wgpu::BindingResource::TextureView(&self.view),
415                },
416                wgpu::BindGroupEntry {
417                    binding: 2,
418                    resource: wgpu::BindingResource::Sampler(&self.sampler),
419                },
420            ],
421            label: None,
422        });
423        self.global_group = global_group;
424    }
425
426    fn render(&mut self, view: &wgpu::TextureView, device: &wgpu::Device, queue: &wgpu::Queue) {
427        self.render_inner(view, device, queue);
428    }
429}
430
431pub fn main() {
432    crate::framework::run::<Example>("bunnymark");
433}
434
435#[cfg(test)]
436#[wgpu_test::gpu_test]
437pub static TEST: crate::framework::ExampleTestParams = crate::framework::ExampleTestParams {
438    name: "bunnymark",
439    image_path: "/examples/features/src/bunnymark/screenshot.png",
440    width: 1024,
441    height: 768,
442    optional_features: wgpu::Features::default(),
443    base_test_parameters: wgpu_test::TestParameters::default(),
444    // We're looking for very small differences, so look in the high percentiles.
445    comparisons: &[
446        wgpu_test::ComparisonType::Mean(0.05),
447        wgpu_test::ComparisonType::Percentile {
448            percentile: 0.99,
449            threshold: 0.37,
450        },
451    ],
452    _phantom: std::marker::PhantomData::<Example>,
453};