//! Standalone wgpu SDF raymarcher. //! //! Opens a window, creates a GPU pipeline from a WGSL shader string, //! and renders with orbit camera controls. Used by the CLI `view` command. pub mod pipeline; pub mod camera; use anyhow::Result; use camera::Camera; use pipeline::RenderPipeline; use std::sync::Arc; use winit::application::ApplicationHandler; use winit::dpi::PhysicalSize; use winit::event::WindowEvent; use winit::event_loop::{ActiveEventLoop, EventLoop}; use winit::window::{Window, WindowId}; pub fn run(wgsl_source: String, bounding_radius: f64) -> Result<()> { let event_loop = EventLoop::new()?; let mut app = App { state: None, wgsl_source, bounding_radius, }; event_loop.run_app(&mut app)?; Ok(()) } struct App { state: Option, wgsl_source: String, bounding_radius: f64, } struct RenderState { window: Arc, surface: wgpu::Surface<'static>, device: wgpu::Device, queue: wgpu::Queue, config: wgpu::SurfaceConfiguration, pipeline: RenderPipeline, camera: Camera, mouse_state: MouseState, start_time: std::time::Instant, } #[derive(Default)] struct MouseState { dragging: bool, last_x: f64, last_y: f64, } impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &ActiveEventLoop) { if self.state.is_some() { return; } let attrs = Window::default_attributes() .with_title("Cord") .with_inner_size(PhysicalSize::new(1280u32, 720)); let window = Arc::new(event_loop.create_window(attrs).unwrap()); let state = pollster::block_on(init_render_state( window, &self.wgsl_source, self.bounding_radius, )); match state { Ok(s) => self.state = Some(s), Err(e) => { eprintln!("render init failed: {e}"); event_loop.exit(); } } } fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) { let Some(state) = &mut self.state else { return }; match event { WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::Resized(size) => { if size.width > 0 && size.height > 0 { state.config.width = size.width; state.config.height = size.height; state.surface.configure(&state.device, &state.config); state.window.request_redraw(); } } WindowEvent::MouseInput { state: btn_state, button, .. } => { if button == winit::event::MouseButton::Left { state.mouse_state.dragging = btn_state == winit::event::ElementState::Pressed; } } WindowEvent::CursorMoved { position, .. } => { if state.mouse_state.dragging { let dx = position.x - state.mouse_state.last_x; let dy = position.y - state.mouse_state.last_y; state.camera.orbit(dx as f32 * 0.005, dy as f32 * 0.005); state.window.request_redraw(); } state.mouse_state.last_x = position.x; state.mouse_state.last_y = position.y; } WindowEvent::MouseWheel { delta, .. } => { let scroll = match delta { winit::event::MouseScrollDelta::LineDelta(_, y) => y, winit::event::MouseScrollDelta::PixelDelta(p) => p.y as f32 * 0.01, }; state.camera.zoom(scroll); state.window.request_redraw(); } WindowEvent::RedrawRequested => { let output = match state.surface.get_current_texture() { Ok(t) => t, Err(wgpu::SurfaceError::Lost) => { state.surface.configure(&state.device, &state.config); return; } Err(e) => { eprintln!("surface error: {e}"); return; } }; let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default()); let elapsed = state.start_time.elapsed().as_secs_f32(); state.pipeline.update_uniforms( &state.queue, state.config.width, state.config.height, elapsed, &state.camera, ); let mut encoder = state.device.create_command_encoder( &wgpu::CommandEncoderDescriptor { label: Some("render") }, ); { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("main"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, ..Default::default() }); state.pipeline.draw(&mut pass); } state.queue.submit(std::iter::once(encoder.finish())); output.present(); } _ => {} } } } async fn init_render_state( window: Arc, wgsl_source: &str, bounding_radius: f64, ) -> Result { let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { backends: wgpu::Backends::all(), ..Default::default() }); let surface = instance.create_surface(window.clone())?; let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, compatible_surface: Some(&surface), force_fallback_adapter: false, }) .await .ok_or_else(|| anyhow::anyhow!("no suitable GPU adapter"))?; let (device, queue) = adapter .request_device(&wgpu::DeviceDescriptor { label: Some("cord"), ..Default::default() }, None) .await?; let size = window.inner_size(); let caps = surface.get_capabilities(&adapter); let format = caps.formats.iter() .find(|f| f.is_srgb()) .copied() .unwrap_or(caps.formats[0]); let config = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format, width: size.width.max(1), height: size.height.max(1), present_mode: wgpu::PresentMode::Fifo, alpha_mode: caps.alpha_modes[0], view_formats: vec![], desired_maximum_frame_latency: 2, }; surface.configure(&device, &config); let scene_scale = (bounding_radius as f32).max(0.1); let pipeline = RenderPipeline::new(&device, format, wgsl_source, scene_scale)?; let camera = Camera::new(scene_scale); Ok(RenderState { window, surface, device, queue, config, pipeline, camera, mouse_state: MouseState::default(), start_time: std::time::Instant::now(), }) }