1use std::{future::Future, sync::Arc};
20use 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#[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 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 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 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 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 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 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 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 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 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}