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