wgpu_examples/hello_triangle/
mod.rs1use 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#[cfg(not(target_arch = "wasm32"))]
13fn spawn(f: impl Future<Output = ()> + 'static) {
14 pollster::block_on(f);
15}
16
17#[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 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 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 compatible_surface: Some(&surface),
133 ..Default::default()
134 })
135 .await
136 .expect("Failed to find an appropriate adapter");
137
138 let (device, queue) = adapter
140 .request_device(&wgpu::DeviceDescriptor {
141 label: None,
142 required_features: wgpu::Features::empty(),
143 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 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 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 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}