diff --git a/src/app.rs b/src/app/mod.rs similarity index 72% rename from src/app.rs rename to src/app/mod.rs index 8c8aafc..39b4498 100644 --- a/src/app.rs +++ b/src/app/mod.rs @@ -1,22 +1,30 @@ +mod state; + #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; use tracing::info; +use std::sync::Arc; + use winit::{ application::ApplicationHandler, + dpi::PhysicalSize, event::*, event_loop::ActiveEventLoop, keyboard::{KeyCode, PhysicalKey}, window::{Window, WindowId}, }; -#[derive(Debug, Default)] +use state::State; + +#[derive(Debug)] pub struct App { - window: Option, width: u32, height: u32, title: String, + window: Option>, + state: Option, } impl App { @@ -25,27 +33,24 @@ impl App { title: title.into(), width, height, - ..Default::default() + window: None, + state: None, } } } -/// @link https://docs.rs/winit/latest/winit/index.html +/// @link https://docs.rs/winit/0.30.0/winit/index.html impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &ActiveEventLoop) { let window_attributes = Window::default_attributes() .with_title(&self.title) .with_inner_size(winit::dpi::LogicalSize::new(self.width, self.height)); - let window = event_loop.create_window(window_attributes).unwrap(); + let window = Arc::new(event_loop.create_window(window_attributes).unwrap()); #[cfg(target_arch = "wasm32")] { - // Winit prevents sizing with CSS, so we have to set - // the size manually when on web. - use winit::dpi::PhysicalSize; - let _ = window.request_inner_size(PhysicalSize::new(self.width, self.height)); - + // web import trait method WindowExtWebSys::canvas(&self) use winit::platform::web::WindowExtWebSys; web_sys::window() @@ -68,10 +73,20 @@ impl ApplicationHandler for App { .expect("Couldn't append canvas to element!"); } + // FIXME requesting adapter and device return futures, but ApplicationHandler::resumed is synchronous + let state = pollster::block_on(State::new(Arc::clone(&window))); + self.window = Some(window); + self.state = Some(state); } fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) { + let state = self.state.as_mut().unwrap(); + + if state.input(&event) { + return; + } + match event { WindowEvent::CloseRequested | WindowEvent::KeyboardInput { @@ -86,6 +101,14 @@ impl ApplicationHandler for App { println!("Close requested, stopping …"); event_loop.exit(); } + WindowEvent::Resized(physical_size) => { + state.resize(physical_size); + } + WindowEvent::ScaleFactorChanged { + scale_factor: _, .. + } => { + state.resize(PhysicalSize::new(self.width, self.height)); + } WindowEvent::RedrawRequested => { // Redraw the application. // @@ -93,7 +116,20 @@ impl ApplicationHandler for App { // this event rather than in AboutToWait, since rendering in here allows // the program to gracefully handle redraws requested by the OS. - // Draw. + // Draw + let size = state.size(); + + state.update(); + + match state.render() { + Ok(_) => {} + // Reconfigure the surface if lost + Err(wgpu::SurfaceError::Lost) => state.resize(size), + // The system is out of memory, we should probably quit + Err(wgpu::SurfaceError::OutOfMemory) => event_loop.exit(), + // All other errors (Outdated, Timeout) should be resolved by the next frame + Err(e) => eprintln!("{:?}", e), + } // Queue a RedrawRequested event. // diff --git a/src/app/state.rs b/src/app/state.rs new file mode 100644 index 0000000..09b7344 --- /dev/null +++ b/src/app/state.rs @@ -0,0 +1,190 @@ +use std::sync::Arc; + +use wgpu::PresentMode; +use winit::{dpi::PhysicalSize, event::WindowEvent, window::Window}; + +#[derive(Debug)] +pub struct State { + surface: wgpu::Surface<'static>, + device: wgpu::Device, + queue: wgpu::Queue, + config: wgpu::SurfaceConfiguration, + size: PhysicalSize, + #[cfg(target_arch = "wasm32")] + window: Arc, +} + +impl State { + // Creating some of the wgpu types requires async code + pub async fn new(window: Arc) -> Self { + let size = window.inner_size(); + + // The instance is a handle to our GPU + // Backends::all => Vulkan + Metal + DX12 + Browser WebGPU + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + #[cfg(not(target_arch = "wasm32"))] + backends: wgpu::Backends::PRIMARY, + #[cfg(target_arch = "wasm32")] + backends: wgpu::Backends::GL, + ..Default::default() + }); + + // The surface needs to live as long as the window that created it. + // https://github.com/rust-windowing/winit/issues/3626#issuecomment-2081794856 + // https://users.rust-lang.org/t/in-wgpu-how-do-i-reset-a-buffer-after-making-it-device-create-buffer-init/106391/13 + let surface = instance.create_surface(Arc::clone(&window)).unwrap(); + + // alternatively use instance.enumerate_adapters to manually check for a proper adapter + // https://sotrh.github.io/learn-wgpu/beginner/tutorial2-surface/#state-new + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: Some(&surface), + force_fallback_adapter: false, + }) + .await + .unwrap(); + + // use adapter.features() or device.features() to get a list of supported features + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + required_features: wgpu::Features::empty(), + // WebGL doesn't support all of wgpu's features, so if + // we're building for the web, we'll have to disable some. + #[cfg(target_arch = "wasm32")] + required_limits: wgpu::Limits::downlevel_webgl2_defaults(), + #[cfg(not(target_arch = "wasm32"))] + required_limits: wgpu::Limits::default(), + label: None, + }, + None, // Trace path + ) + .await + .unwrap(); + + let surface_caps = surface.get_capabilities(&adapter); + // Shader code in this tutorial assumes an sRGB surface texture. Using a different + // one will result in all the colors coming out darker. If you want to support non + // sRGB surfaces, you'll need to account for that when drawing to the frame. + let surface_format = surface_caps + .formats + .iter() + .copied() + .find(|f| f.is_srgb()) + .unwrap_or(surface_caps.formats[0]); + + let config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: surface_format, + width: size.width, + height: size.height, + present_mode: PresentMode::AutoNoVsync, //surface_caps.present_modes[0], + alpha_mode: surface_caps.alpha_modes[0], + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + + Self { + #[cfg(target_arch = "wasm32")] + window, + surface, + device, + queue, + config, + size, + } + } + + #[cfg(target_arch = "wasm32")] + pub fn window(&self) -> Arc { + Arc::clone(&self.window) + } + + pub fn size(&self) -> PhysicalSize { + self.size + } + + pub fn resize(&mut self, new_size: PhysicalSize) { + let surface_width; + let surface_height; + + // A Desktop Window in Xwayland also wants the physical size as texture size. + // Otherwise there will be a black border after scaling it down with a scale factor. + // TODO check scaling in a X11 session and in a Wayland Desktop Window + #[cfg(not(target_arch = "wasm32"))] + { + surface_width = new_size.width; + surface_height = new_size.height; + } + + // Web platform with a scale factor 2.0 would double the canvas size and + // trigger further resize events until surface width and height are greater than + // the maximum supported texture size (e.g. 2048x2048). + // Keep the desired physical size, but scale the texture down to it's desired size. + #[cfg(target_arch = "wasm32")] + { + let scale_factor = self.window().as_ref().scale_factor(); + surface_width = ((new_size.width as f64) / scale_factor) as u32; + surface_height = ((new_size.height as f64) / scale_factor) as u32; + } + + if new_size.width > 0 && new_size.height > 0 { + self.size = new_size; + self.config.width = surface_width; + self.config.height = surface_height; + self.surface.configure(&self.device, &self.config); + } + } + + pub fn input(&mut self, event: &WindowEvent) -> bool { + let _ = event; + false + } + + pub fn update(&mut self) { + // todo!() + } + + pub fn render(&mut self) -> Result<(), wgpu::SurfaceError> { + let output = self.surface.get_current_texture()?; + + let view = output + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Render Encoder"), + }); + + { + let _render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Render Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 0.1, + g: 0.2, + b: 0.3, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + }); + } + + // submit will accept anything that implements IntoIter + self.queue.submit(std::iter::once(encoder.finish())); + output.present(); + + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index a759189..759537e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,18 +10,29 @@ use winit::event_loop::{ControlFlow, EventLoop}; #[cfg_attr(target_arch = "wasm32", wasm_bindgen(start))] pub async fn run_app() { + let log_level = tracing::Level::WARN; + cfg_if! { if #[cfg(target_arch = "wasm32")] { console_error_panic_hook::set_once(); - tracing_wasm::set_as_global_default(); + // tracing for all log levels + // tracing_wasm::set_as_global_default(); + + let wasm_layer_config = tracing_wasm::WASMLayerConfigBuilder::new().set_max_level(log_level).build(); + tracing_wasm::set_as_global_default_with_config(wasm_layer_config); } else { - tracing_subscriber::fmt::init(); + // tracing for all log levels + // tracing_subscriber::fmt::init(); + + use tracing_subscriber::FmtSubscriber; + let subscriber = FmtSubscriber::builder().with_max_level(log_level).finish(); + tracing::subscriber::set_global_default(subscriber).expect("setting default tracing subscriber failed!"); } } - info!("Info!"); - warn!("Warning!!"); - error!("Error!!!"); + info!("Example Info!"); + warn!("Example Warning!!"); + error!("Example Error!!!"); let event_loop = EventLoop::new().unwrap(); @@ -37,12 +48,13 @@ pub async fn run_app() { #[allow(unused_mut)] let mut app = app::App::new("WebGPU Rendering", 500, 400); - cfg_if! { - if #[cfg(target_arch = "wasm32")] { - use winit::platform::web::EventLoopExtWebSys; - event_loop.spawn_app(app); - } else { - event_loop.run_app(&mut app).unwrap(); - } + #[cfg(target_arch = "wasm32")] + { + // web import trait method EventLoopExtWebSys::spawn_app(app: App) + use winit::platform::web::EventLoopExtWebSys; + event_loop.spawn_app(app); } + + #[cfg(not(target_arch = "wasm32"))] + event_loop.run_app(&mut app).unwrap(); }