wgpu_examples/hello_triangle/
mod.rs

1use std::{borrow::Cow, future::Future, sync::Arc};
2use wgpu::CurrentSurfaceTexture;
3use winit::{
4    application::ApplicationHandler,
5    event::WindowEvent,
6    event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy},
7    window::Window,
8};
9
10/// Runs a future to completion. On native this blocks synchronously via pollster.
11/// On wasm this spawns a local task so control returns to the browser immediately.
12#[cfg(not(target_arch = "wasm32"))]
13fn spawn(f: impl Future<Output = ()> + 'static) {
14    pollster::block_on(f);
15}
16
17/// Runs a future to completion. On native this blocks synchronously via pollster.
18/// On wasm this spawns a local task so control returns to the browser immediately.
19#[cfg(target_arch = "wasm32")]
20fn spawn(f: impl Future<Output = ()> + 'static) {
21    wasm_bindgen_futures::spawn_local(f);
22}
23
24struct WgpuState {
25    instance: wgpu::Instance,
26    window: Arc<Window>,
27    device: wgpu::Device,
28    queue: wgpu::Queue,
29    surface: wgpu::Surface<'static>,
30    config: wgpu::SurfaceConfiguration,
31    render_pipeline: wgpu::RenderPipeline,
32}
33
34enum TriangleAction {
35    Initialized(WgpuState),
36}
37
38#[expect(clippy::large_enum_variant)]
39enum AppState {
40    Uninitialized,
41    Loading,
42    Running(WgpuState),
43}
44
45struct App {
46    proxy: EventLoopProxy<TriangleAction>,
47    window: Option<Arc<Window>>,
48    state: AppState,
49}
50
51impl App {
52    fn new(event_loop: &EventLoop<TriangleAction>) -> Self {
53        Self {
54            proxy: event_loop.create_proxy(),
55            window: None,
56            state: AppState::Uninitialized,
57        }
58    }
59
60    fn resize_surface(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
61        let AppState::Running(wgpu_state) = &mut self.state else {
62            return;
63        };
64
65        // Reconfigure the surface with the new size
66        wgpu_state.config.width = new_size.width.max(1);
67        wgpu_state.config.height = new_size.height.max(1);
68        wgpu_state
69            .surface
70            .configure(&wgpu_state.device, &wgpu_state.config);
71
72        // On macos the window needs to be redrawn manually after resizing
73        if let Some(window) = &self.window {
74            window.request_redraw();
75        }
76    }
77}
78
79impl ApplicationHandler<TriangleAction> for App {
80    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
81        if !matches!(self.state, AppState::Uninitialized) {
82            return;
83        }
84        self.state = AppState::Loading;
85
86        #[cfg_attr(
87            not(target_arch = "wasm32"),
88            expect(unused_mut, reason = "wasm32 re-assigns to specify canvas")
89        )]
90        let mut attributes = Window::default_attributes();
91
92        #[cfg(target_arch = "wasm32")]
93        {
94            use wasm_bindgen::JsCast;
95            use winit::platform::web::WindowAttributesExtWebSys;
96            let canvas = web_sys::window()
97                .unwrap()
98                .document()
99                .unwrap()
100                .get_element_by_id("canvas")
101                .unwrap()
102                .dyn_into::<web_sys::HtmlCanvasElement>()
103                .unwrap();
104            attributes = attributes.with_canvas(Some(canvas));
105        }
106
107        let window = Arc::new(
108            event_loop
109                .create_window(attributes)
110                .expect("Failed to create window"),
111        );
112        self.window = Some(window.clone());
113
114        let display_handle = event_loop.owned_display_handle();
115        let proxy = self.proxy.clone();
116
117        spawn(async move {
118            let mut size = window.inner_size();
119            size.width = size.width.max(1);
120            size.height = size.height.max(1);
121
122            let instance =
123                wgpu::Instance::new(wgpu::InstanceDescriptor::new_with_display_handle_from_env(
124                    Box::new(display_handle),
125                ));
126
127            let surface = instance.create_surface(window.clone()).unwrap();
128            let adapter = instance
129                .request_adapter(&wgpu::RequestAdapterOptions {
130                    power_preference: wgpu::PowerPreference::default(),
131                    // Request an adapter which can render to our surface
132                    compatible_surface: Some(&surface),
133                    ..Default::default()
134                })
135                .await
136                .expect("Failed to find an appropriate adapter");
137
138            // Create the logical device and command queue
139            let (device, queue) = adapter
140                .request_device(&wgpu::DeviceDescriptor {
141                    label: None,
142                    required_features: wgpu::Features::empty(),
143                    // Make sure we use the texture resolution limits from the adapter,
144                    // so we can support images the size of the swapchain.
145                    required_limits: wgpu::Limits::downlevel_webgl2_defaults()
146                        .using_resolution(adapter.limits()),
147                    experimental_features: wgpu::ExperimentalFeatures::disabled(),
148                    memory_hints: wgpu::MemoryHints::MemoryUsage,
149                    trace: wgpu::Trace::Off,
150                })
151                .await
152                .expect("Failed to create device");
153
154            // Load the shaders from disk
155            let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
156                label: None,
157                source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shader.wgsl"))),
158            });
159
160            let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
161                label: None,
162                bind_group_layouts: &[],
163                immediate_size: 0,
164            });
165
166            let swapchain_capabilities = surface.get_capabilities(&adapter);
167            let swapchain_format = swapchain_capabilities.formats[0];
168
169            let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
170                label: None,
171                layout: Some(&pipeline_layout),
172                vertex: wgpu::VertexState {
173                    module: &shader,
174                    entry_point: Some("vs_main"),
175                    buffers: &[],
176                    compilation_options: Default::default(),
177                },
178                fragment: Some(wgpu::FragmentState {
179                    module: &shader,
180                    entry_point: Some("fs_main"),
181                    compilation_options: Default::default(),
182                    targets: &[Some(swapchain_format.into())],
183                }),
184                primitive: wgpu::PrimitiveState::default(),
185                depth_stencil: None,
186                multisample: wgpu::MultisampleState::default(),
187                multiview_mask: None,
188                cache: None,
189            });
190
191            let config = surface
192                .get_default_config(&adapter, size.width, size.height)
193                .unwrap();
194
195            surface.configure(&device, &config);
196
197            let _ = proxy.send_event(TriangleAction::Initialized(WgpuState {
198                instance,
199                window,
200                device,
201                queue,
202                surface,
203                config,
204                render_pipeline,
205            }));
206        });
207    }
208
209    fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: TriangleAction) {
210        match event {
211            TriangleAction::Initialized(wgpu_state) => {
212                self.state = AppState::Running(wgpu_state);
213
214                // winit might have updated the window size while we were
215                // creating the surface asynchronously, so resize the surface.
216                if let Some(window) = &self.window {
217                    self.resize_surface(window.inner_size());
218                }
219            }
220        }
221    }
222
223    fn window_event(
224        &mut self,
225        event_loop: &ActiveEventLoop,
226        _window_id: winit::window::WindowId,
227        event: WindowEvent,
228    ) {
229        let AppState::Running(wgpu_state) = &mut self.state else {
230            return;
231        };
232
233        match event {
234            WindowEvent::Resized(new_size) => {
235                self.resize_surface(new_size);
236            }
237            WindowEvent::RedrawRequested => {
238                let frame = match wgpu_state.surface.get_current_texture() {
239                    CurrentSurfaceTexture::Success(frame) => frame,
240                    CurrentSurfaceTexture::Timeout | CurrentSurfaceTexture::Occluded => {
241                        // Try again later
242                        if let Some(window) = &self.window {
243                            window.request_redraw();
244                        }
245                        return;
246                    }
247                    CurrentSurfaceTexture::Suboptimal(texture) => {
248                        drop(texture);
249
250                        wgpu_state
251                            .surface
252                            .configure(&wgpu_state.device, &wgpu_state.config);
253                        if let Some(window) = &self.window {
254                            window.request_redraw();
255                        }
256                        return;
257                    }
258                    CurrentSurfaceTexture::Outdated => {
259                        wgpu_state
260                            .surface
261                            .configure(&wgpu_state.device, &wgpu_state.config);
262                        if let Some(window) = &self.window {
263                            window.request_redraw();
264                        }
265                        return;
266                    }
267                    CurrentSurfaceTexture::Validation => {
268                        unreachable!("No error scope registered, so validation errors will panic")
269                    }
270                    CurrentSurfaceTexture::Lost => {
271                        wgpu_state.surface = wgpu_state
272                            .instance
273                            .create_surface(wgpu_state.window.clone())
274                            .unwrap();
275                        wgpu_state
276                            .surface
277                            .configure(&wgpu_state.device, &wgpu_state.config);
278                        if let Some(window) = &self.window {
279                            window.request_redraw();
280                        }
281                        return;
282                    }
283                };
284
285                let view = frame
286                    .texture
287                    .create_view(&wgpu::TextureViewDescriptor::default());
288                let mut encoder = wgpu_state
289                    .device
290                    .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
291                {
292                    let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
293                        label: None,
294                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
295                            view: &view,
296                            depth_slice: None,
297                            resolve_target: None,
298                            ops: wgpu::Operations {
299                                load: wgpu::LoadOp::Clear(wgpu::Color::GREEN),
300                                store: wgpu::StoreOp::Store,
301                            },
302                        })],
303                        depth_stencil_attachment: None,
304                        timestamp_writes: None,
305                        occlusion_query_set: None,
306                        multiview_mask: None,
307                    });
308                    rpass.set_pipeline(&wgpu_state.render_pipeline);
309                    rpass.draw(0..3, 0..1);
310                }
311
312                wgpu_state.queue.submit(Some(encoder.finish()));
313                if let Some(window) = &self.window {
314                    window.pre_present_notify();
315                }
316                wgpu_state.queue.present(frame);
317            }
318            WindowEvent::Occluded(is_occluded) => {
319                if !is_occluded {
320                    if let Some(window) = &self.window {
321                        window.request_redraw();
322                    }
323                }
324            }
325            WindowEvent::CloseRequested => event_loop.exit(),
326            _ => {}
327        }
328    }
329}
330
331pub fn main() {
332    cfg_if::cfg_if! {
333        if #[cfg(target_arch = "wasm32")] {
334            std::panic::set_hook(Box::new(console_error_panic_hook::hook));
335            console_log::init().expect("could not initialize logger");
336        } else {
337            env_logger::init();
338        }
339    }
340
341    let event_loop = EventLoop::with_user_event().build().unwrap();
342
343    #[cfg_attr(target_arch = "wasm32", expect(unused_mut))]
344    let mut app = App::new(&event_loop);
345
346    cfg_if::cfg_if! {
347        if #[cfg(target_arch = "wasm32")] {
348            use winit::platform::web::EventLoopExtWebSys;
349            event_loop.spawn_app(app);
350        } else {
351            event_loop.run_app(&mut app).unwrap();
352        }
353    }
354}