wgpu_examples/uniform_values/
mod.rs

1//! Points of interest for seeing uniforms in action:
2//!
3//! 1. the struct for the data stored in the uniform buffer is defined.
4//! 2. the uniform buffer itself is created.
5//! 3. the bind group that will bind the uniform buffer and it's layout are created.
6//! 4. the bind group layout is attached to the pipeline layout.
7//! 5. the uniform buffer and the bind group are stored alongside the pipeline.
8//! 6. an instance of `AppState` is created. This variable will be modified
9//!    to change parameters in the shader and modified by app events to preform and save
10//!    those changes.
11//! 7. (7a and 7b) the `state` variable created at (6) is modified by commands such
12//!    as pressing the arrow keys or zooming in or out.
13//! 8. the contents of the `AppState` are loaded into the uniform buffer in preparation.
14//! 9. the bind group with the uniform buffer is attached to the render pass.
15//!
16//! The usage of the uniform buffer within the shader itself is pretty self-explanatory given
17//! some understanding of WGSL.
18
19use std::{future::Future, sync::Arc};
20// We won't bring StorageBuffer into scope as that might be too easy to confuse
21// with actual GPU-allocated wgpu storage buffers.
22use encase::ShaderType;
23use wgpu::CurrentSurfaceTexture;
24use winit::{
25    application::ApplicationHandler,
26    event::{KeyEvent, WindowEvent},
27    event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy},
28    keyboard::{Key, NamedKey},
29    window::Window,
30};
31
32const ZOOM_INCREMENT_FACTOR: f32 = 1.1;
33const CAMERA_POS_INCREMENT_FACTOR: f32 = 0.1;
34
35#[cfg(not(target_arch = "wasm32"))]
36fn spawn(f: impl Future<Output = ()> + 'static) {
37    pollster::block_on(f);
38}
39
40#[cfg(target_arch = "wasm32")]
41fn spawn(f: impl Future<Output = ()> + 'static) {
42    wasm_bindgen_futures::spawn_local(f);
43}
44
45// (1)
46#[derive(Debug, ShaderType)]
47struct ShaderState {
48    pub cursor_pos: glam::Vec2,
49    pub zoom: f32,
50    pub max_iterations: u32,
51}
52
53impl ShaderState {
54    // Translating Rust structures to WGSL is always tricky and can prove
55    // incredibly difficult to remember all the rules by which WGSL
56    // lays out and formats structs in memory. It is also often extremely
57    // frustrating to debug when things don't go right.
58    //
59    // You may sometimes see structs translated to bytes through
60    // using `#[repr(C)]` on the struct so that the struct has a defined,
61    // guaranteed internal layout and then implementing bytemuck's POD
62    // trait so that one can preform a bitwise cast. There are issues with
63    // this approach though as C's struct layouts aren't always compatible
64    // with WGSL, such as when special WGSL types like vec's and mat's
65    // get involved that have special alignment rules and especially
66    // when the target buffer is going to be used in the uniform memory
67    // space.
68    //
69    // Here though, we use the encase crate which makes translating potentially
70    // complex Rust structs easy through combined use of the [`ShaderType`] trait
71    // / derive macro and the buffer structs which hold data formatted for WGSL
72    // in either the storage or uniform spaces.
73    fn as_wgsl_bytes(&self) -> encase::internal::Result<Vec<u8>> {
74        let mut buffer = encase::UniformBuffer::new(Vec::new());
75        buffer.write(self)?;
76        Ok(buffer.into_inner())
77    }
78
79    fn translate_view(&mut self, increments: i32, axis: usize) {
80        self.cursor_pos[axis] += CAMERA_POS_INCREMENT_FACTOR * increments as f32 / self.zoom;
81    }
82
83    fn zoom(&mut self, amount: f32) {
84        self.zoom += ZOOM_INCREMENT_FACTOR * amount * self.zoom.powf(1.02);
85        self.zoom = self.zoom.max(1.1);
86    }
87}
88
89impl Default for ShaderState {
90    fn default() -> Self {
91        ShaderState {
92            cursor_pos: glam::Vec2::ZERO,
93            zoom: 1.0,
94            max_iterations: 50,
95        }
96    }
97}
98
99struct WgpuContext {
100    pub instance: wgpu::Instance,
101    pub window: Arc<Window>,
102    pub surface: wgpu::Surface<'static>,
103    pub surface_config: wgpu::SurfaceConfiguration,
104    pub device: wgpu::Device,
105    pub queue: wgpu::Queue,
106    pub pipeline: wgpu::RenderPipeline,
107    pub bind_group: wgpu::BindGroup,
108    pub uniform_buffer: wgpu::Buffer,
109}
110
111impl WgpuContext {
112    async fn new(
113        window: Arc<Window>,
114        display_handle: winit::event_loop::OwnedDisplayHandle,
115    ) -> WgpuContext {
116        let size = window.inner_size();
117
118        let instance = wgpu::Instance::new(
119            wgpu::InstanceDescriptor::new_with_display_handle_from_env(Box::new(display_handle)),
120        );
121        let surface = instance.create_surface(window.clone()).unwrap();
122        let adapter = instance
123            .request_adapter(&wgpu::RequestAdapterOptions {
124                power_preference: wgpu::PowerPreference::HighPerformance,
125                compatible_surface: Some(&surface),
126                ..Default::default()
127            })
128            .await
129            .unwrap();
130        let (device, queue) = adapter
131            .request_device(&wgpu::DeviceDescriptor {
132                label: None,
133                required_features: wgpu::Features::empty(),
134                required_limits: wgpu::Limits::downlevel_defaults(),
135                experimental_features: wgpu::ExperimentalFeatures::disabled(),
136                memory_hints: wgpu::MemoryHints::MemoryUsage,
137                trace: wgpu::Trace::Off,
138            })
139            .await
140            .unwrap();
141
142        let shader = device.create_shader_module(wgpu::include_wgsl!("shader.wgsl"));
143
144        // (2)
145        let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
146            label: None,
147            size: size_of::<ShaderState>() as u64,
148            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
149            mapped_at_creation: false,
150        });
151
152        // (3)
153        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
154            label: None,
155            entries: &[wgpu::BindGroupLayoutEntry {
156                binding: 0,
157                visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
158                ty: wgpu::BindingType::Buffer {
159                    ty: wgpu::BufferBindingType::Uniform,
160                    has_dynamic_offset: false,
161                    min_binding_size: None,
162                },
163                count: None,
164            }],
165        });
166        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
167            label: None,
168            layout: &bind_group_layout,
169            entries: &[wgpu::BindGroupEntry {
170                binding: 0,
171                resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
172                    buffer: &uniform_buffer,
173                    offset: 0,
174                    size: None,
175                }),
176            }],
177        });
178
179        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
180            label: None,
181            // (4)
182            bind_group_layouts: &[Some(&bind_group_layout)],
183            immediate_size: 0,
184        });
185
186        let swapchain_capabilities = surface.get_capabilities(&adapter);
187        let swapchain_format = swapchain_capabilities.formats[0];
188
189        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
190            label: None,
191            layout: Some(&pipeline_layout),
192            vertex: wgpu::VertexState {
193                module: &shader,
194                entry_point: Some("vs_main"),
195                compilation_options: Default::default(),
196                buffers: &[],
197            },
198            fragment: Some(wgpu::FragmentState {
199                module: &shader,
200                entry_point: Some("fs_main"),
201                compilation_options: Default::default(),
202                targets: &[Some(swapchain_format.into())],
203            }),
204            primitive: wgpu::PrimitiveState::default(),
205            depth_stencil: None,
206            multisample: wgpu::MultisampleState::default(),
207            multiview_mask: None,
208            cache: None,
209        });
210        let surface_config = surface
211            .get_default_config(&adapter, size.width, size.height)
212            .unwrap();
213        surface.configure(&device, &surface_config);
214
215        // (5)
216        WgpuContext {
217            instance,
218            window,
219            surface,
220            surface_config,
221            device,
222            queue,
223            pipeline,
224            bind_group,
225            uniform_buffer,
226        }
227    }
228
229    fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
230        self.surface_config.width = new_size.width;
231        self.surface_config.height = new_size.height;
232        self.surface.configure(&self.device, &self.surface_config);
233    }
234}
235
236enum UniformAction {
237    Initialized(WgpuContext),
238}
239
240#[expect(clippy::large_enum_variant)]
241enum RunState {
242    Uninitialized,
243    Loading,
244    Running {
245        wgpu_ctx: WgpuContext,
246        // (6)
247        shader_state: ShaderState,
248    },
249}
250
251struct App {
252    proxy: EventLoopProxy<UniformAction>,
253    window: Option<Arc<Window>>,
254    state: RunState,
255}
256
257impl App {
258    fn new(event_loop: &EventLoop<UniformAction>) -> Self {
259        Self {
260            proxy: event_loop.create_proxy(),
261            window: None,
262            state: RunState::Uninitialized,
263        }
264    }
265}
266
267impl ApplicationHandler<UniformAction> for App {
268    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
269        if !matches!(self.state, RunState::Uninitialized) {
270            return;
271        }
272        self.state = RunState::Loading;
273
274        #[cfg_attr(
275            not(target_arch = "wasm32"),
276            expect(unused_mut, reason = "wasm32 re-assigns to specify canvas")
277        )]
278        let mut attributes = Window::default_attributes()
279            .with_title("Remember: Use U/D to change sample count!")
280            .with_inner_size(winit::dpi::LogicalSize::new(900, 900));
281
282        #[cfg(target_arch = "wasm32")]
283        {
284            use wasm_bindgen::JsCast;
285            use winit::platform::web::WindowAttributesExtWebSys;
286            let canvas = web_sys::window()
287                .unwrap()
288                .document()
289                .unwrap()
290                .get_element_by_id("canvas")
291                .unwrap()
292                .dyn_into::<web_sys::HtmlCanvasElement>()
293                .unwrap();
294            attributes = attributes.with_canvas(Some(canvas));
295        }
296
297        let window = Arc::new(
298            event_loop
299                .create_window(attributes)
300                .expect("Failed to create window"),
301        );
302        self.window = Some(window.clone());
303
304        let display_handle = event_loop.owned_display_handle();
305        let proxy = self.proxy.clone();
306
307        spawn(async move {
308            let wgpu_ctx = WgpuContext::new(window, display_handle).await;
309            let _ = proxy.send_event(UniformAction::Initialized(wgpu_ctx));
310        });
311    }
312
313    fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UniformAction) {
314        match event {
315            UniformAction::Initialized(wgpu_ctx) => {
316                self.state = RunState::Running {
317                    wgpu_ctx,
318                    shader_state: ShaderState::default(),
319                };
320                if let Some(window) = &self.window {
321                    window.request_redraw();
322                }
323            }
324        }
325    }
326
327    fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
328        self.state = RunState::Uninitialized;
329    }
330
331    fn window_event(
332        &mut self,
333        event_loop: &ActiveEventLoop,
334        _window_id: winit::window::WindowId,
335        event: WindowEvent,
336    ) {
337        let RunState::Running {
338            wgpu_ctx,
339            shader_state,
340        } = &mut self.state
341        else {
342            return;
343        };
344
345        match event {
346            WindowEvent::CloseRequested => {
347                event_loop.exit();
348            }
349            WindowEvent::KeyboardInput {
350                event: KeyEvent {
351                    logical_key, text, ..
352                },
353                ..
354            } => {
355                if let Key::Named(key) = logical_key {
356                    match key {
357                        NamedKey::Escape => event_loop.exit(),
358                        NamedKey::ArrowUp => shader_state.translate_view(1, 1),
359                        NamedKey::ArrowDown => shader_state.translate_view(-1, 1),
360                        NamedKey::ArrowLeft => shader_state.translate_view(-1, 0),
361                        NamedKey::ArrowRight => shader_state.translate_view(1, 0),
362                        _ => {}
363                    }
364                }
365
366                if let Some(text) = text {
367                    if text == "u" {
368                        shader_state.max_iterations += 3;
369                    } else if text == "d" {
370                        shader_state.max_iterations = shader_state.max_iterations.saturating_sub(3);
371                    }
372                };
373
374                if let Some(window) = &self.window {
375                    window.request_redraw();
376                }
377            }
378            WindowEvent::MouseWheel { delta, .. } => {
379                let change = match delta {
380                    winit::event::MouseScrollDelta::LineDelta(_, vertical) => vertical,
381                    winit::event::MouseScrollDelta::PixelDelta(pos) => pos.y as f32 / 20.0,
382                };
383                // (7b)
384                shader_state.zoom(change);
385                if let Some(window) = &self.window {
386                    window.request_redraw();
387                }
388            }
389            WindowEvent::Resized(new_size) => {
390                wgpu_ctx.resize(new_size);
391                if let Some(window) = &self.window {
392                    window.request_redraw();
393                }
394            }
395            WindowEvent::RedrawRequested => {
396                let frame = match wgpu_ctx.surface.get_current_texture() {
397                    CurrentSurfaceTexture::Success(frame) => frame,
398                    CurrentSurfaceTexture::Timeout | CurrentSurfaceTexture::Occluded => {
399                        if let Some(window) = &self.window {
400                            window.request_redraw();
401                        }
402                        return;
403                    }
404                    CurrentSurfaceTexture::Suboptimal(_) | CurrentSurfaceTexture::Outdated => {
405                        wgpu_ctx
406                            .surface
407                            .configure(&wgpu_ctx.device, &wgpu_ctx.surface_config);
408                        if let Some(window) = &self.window {
409                            window.request_redraw();
410                        }
411                        return;
412                    }
413                    CurrentSurfaceTexture::Validation => {
414                        unreachable!("No error scope registered, so validation errors will panic")
415                    }
416                    CurrentSurfaceTexture::Lost => {
417                        wgpu_ctx.surface = wgpu_ctx
418                            .instance
419                            .create_surface(wgpu_ctx.window.clone())
420                            .unwrap();
421                        wgpu_ctx
422                            .surface
423                            .configure(&wgpu_ctx.device, &wgpu_ctx.surface_config);
424                        if let Some(window) = &self.window {
425                            window.request_redraw();
426                        }
427                        return;
428                    }
429                };
430
431                let view = frame
432                    .texture
433                    .create_view(&wgpu::TextureViewDescriptor::default());
434
435                // (8)
436                wgpu_ctx.queue.write_buffer(
437                    &wgpu_ctx.uniform_buffer,
438                    0,
439                    &shader_state.as_wgsl_bytes().expect(
440                        "Error in encase translating ShaderState \
441                    struct to WGSL bytes.",
442                    ),
443                );
444                let mut encoder = wgpu_ctx
445                    .device
446                    .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
447                {
448                    let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
449                        label: None,
450                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
451                            view: &view,
452                            depth_slice: None,
453                            resolve_target: None,
454                            ops: wgpu::Operations {
455                                load: wgpu::LoadOp::Clear(wgpu::Color::GREEN),
456                                store: wgpu::StoreOp::Store,
457                            },
458                        })],
459                        depth_stencil_attachment: None,
460                        occlusion_query_set: None,
461                        timestamp_writes: None,
462                        multiview_mask: None,
463                    });
464                    render_pass.set_pipeline(&wgpu_ctx.pipeline);
465                    // (9)
466                    render_pass.set_bind_group(0, Some(&wgpu_ctx.bind_group), &[]);
467                    render_pass.draw(0..3, 0..1);
468                }
469                wgpu_ctx.queue.submit(Some(encoder.finish()));
470                if let Some(window) = &self.window {
471                    window.pre_present_notify();
472                }
473                frame.present();
474            }
475            WindowEvent::Occluded(is_occluded) => {
476                if !is_occluded {
477                    if let Some(window) = &self.window {
478                        window.request_redraw();
479                    }
480                }
481            }
482            _ => {}
483        }
484    }
485}
486
487pub fn main() {
488    cfg_if::cfg_if! {
489        if #[cfg(target_arch = "wasm32")] {
490            std::panic::set_hook(Box::new(console_error_panic_hook::hook));
491            console_log::init().expect("could not initialize logger");
492        } else {
493            env_logger::builder().format_timestamp_nanos().init();
494        }
495    }
496
497    let event_loop = EventLoop::with_user_event().build().unwrap();
498
499    #[cfg_attr(target_arch = "wasm32", expect(unused_mut))]
500    let mut app = App::new(&event_loop);
501
502    cfg_if::cfg_if! {
503        if #[cfg(target_arch = "wasm32")] {
504            use winit::platform::web::EventLoopExtWebSys;
505
506            let document = web_sys::window()
507                .and_then(|win| win.document())
508                .expect("Failed to get document.");
509            let body = document.body().unwrap();
510            let controls_text = document
511                .create_element("p")
512                .expect("Failed to create controls text as element.");
513            controls_text.set_inner_html(
514                "Controls: <br/>
515Up, Down, Left, Right: Move view, <br/>
516Scroll: Zoom, <br/>
517U, D: Increase / decrease sample count.",
518            );
519            body.append_child(&controls_text)
520                .expect("Failed to append controls text to body.");
521
522            event_loop.spawn_app(app);
523        } else {
524            event_loop.run_app(&mut app).unwrap();
525        }
526    }
527}