//! Connect a window with a renderer. use crate::core::{Color, Size}; use crate::graphics::color; use crate::graphics::compositor; use crate::graphics::error; use crate::graphics::{self, Viewport}; use crate::settings::{self, Settings}; use crate::{Engine, Renderer}; /// A window graphics backend for iced powered by `wgpu`. #[allow(missing_debug_implementations)] pub struct Compositor { instance: wgpu::Instance, adapter: wgpu::Adapter, device: wgpu::Device, queue: wgpu::Queue, format: wgpu::TextureFormat, alpha_mode: wgpu::CompositeAlphaMode, engine: Engine, settings: Settings, } /// A compositor error. #[derive(Debug, Clone, thiserror::Error)] pub enum Error { /// The surface creation failed. #[error("the surface creation failed: {0}")] SurfaceCreationFailed(#[from] wgpu::CreateSurfaceError), /// The surface is not compatible. #[error("the surface is not compatible")] IncompatibleSurface, /// No adapter was found for the options requested. #[error("no adapter was found for the options requested: {0:?}")] NoAdapterFound(String), /// No device request succeeded. #[error("no device request succeeded: {0:?}")] RequestDeviceFailed(Vec<(wgpu::Limits, wgpu::RequestDeviceError)>), } impl From for graphics::Error { fn from(error: Error) -> Self { Self::GraphicsAdapterNotFound { backend: "wgpu", reason: error::Reason::RequestFailed(error.to_string()), } } } impl Compositor { /// Requests a new [`Compositor`] with the given [`Settings`]. /// /// Returns `None` if no compatible graphics adapter could be found. pub async fn request( settings: Settings, compatible_window: Option, ) -> Result { let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { backends: settings.backends, flags: if cfg!(feature = "strict-assertions") { wgpu::InstanceFlags::debugging() } else { wgpu::InstanceFlags::empty() }, ..Default::default() }); log::info!("{settings:#?}"); #[cfg(not(target_arch = "wasm32"))] if log::max_level() >= log::LevelFilter::Info { let available_adapters: Vec<_> = instance .enumerate_adapters(settings.backends) .iter() .map(wgpu::Adapter::get_info) .collect(); log::info!("Available adapters: {available_adapters:#?}"); } #[allow(unsafe_code)] let compatible_surface = compatible_window .and_then(|window| instance.create_surface(window).ok()); let adapter_options = wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::from_env().unwrap_or( if settings.antialiasing.is_none() { wgpu::PowerPreference::LowPower } else { wgpu::PowerPreference::HighPerformance }, ), compatible_surface: compatible_surface.as_ref(), force_fallback_adapter: false, }; let adapter = instance .request_adapter(&adapter_options) .await .ok_or(Error::NoAdapterFound(format!("{:?}", adapter_options)))?; log::info!("Selected: {:#?}", adapter.get_info()); let (format, alpha_mode) = compatible_surface .as_ref() .and_then(|surface| { let capabilities = surface.get_capabilities(&adapter); let mut formats = capabilities.formats.iter().copied(); log::info!("Available formats: {formats:#?}"); let format = if color::GAMMA_CORRECTION { formats.find(wgpu::TextureFormat::is_srgb) } else { formats.find(|format| !wgpu::TextureFormat::is_srgb(format)) }; let format = format.or_else(|| { log::warn!("No format found!"); capabilities.formats.first().copied() }); let alpha_modes = capabilities.alpha_modes; log::info!("Available alpha modes: {alpha_modes:#?}"); let preferred_alpha = if alpha_modes .contains(&wgpu::CompositeAlphaMode::PostMultiplied) { wgpu::CompositeAlphaMode::PostMultiplied } else if alpha_modes .contains(&wgpu::CompositeAlphaMode::PreMultiplied) { wgpu::CompositeAlphaMode::PreMultiplied } else { wgpu::CompositeAlphaMode::Auto }; format.zip(Some(preferred_alpha)) }) .ok_or(Error::IncompatibleSurface)?; log::info!( "Selected format: {format:?} with alpha mode: {alpha_mode:?}" ); #[cfg(target_arch = "wasm32")] let limits = [wgpu::Limits::downlevel_webgl2_defaults() .using_resolution(adapter.limits())]; #[cfg(not(target_arch = "wasm32"))] let limits = [wgpu::Limits::default(), wgpu::Limits::downlevel_defaults()]; let limits = limits.into_iter().map(|limits| wgpu::Limits { max_bind_groups: 2, ..limits }); let mut errors = Vec::new(); for required_limits in limits { let result = adapter .request_device( &wgpu::DeviceDescriptor { label: Some( "iced_wgpu::window::compositor device descriptor", ), required_features: wgpu::Features::empty(), required_limits: required_limits.clone(), memory_hints: wgpu::MemoryHints::MemoryUsage, }, None, ) .await; match result { Ok((device, queue)) => { let engine = Engine::new( &adapter, &device, &queue, format, settings.antialiasing, ); return Ok(Compositor { instance, adapter, device, queue, format, alpha_mode, engine, settings, }); } Err(error) => { errors.push((required_limits, error)); } } } Err(Error::RequestDeviceFailed(errors)) } } /// Creates a [`Compositor`] with the given [`Settings`] and window. pub async fn new( settings: Settings, compatible_window: W, ) -> Result { Compositor::request(settings, Some(compatible_window)).await } /// Presents the given primitives with the given [`Compositor`]. pub fn present>( compositor: &mut Compositor, renderer: &mut Renderer, surface: &mut wgpu::Surface<'static>, viewport: &Viewport, background_color: Color, overlay: &[T], ) -> Result<(), compositor::SurfaceError> { match surface.get_current_texture() { Ok(frame) => { let mut encoder = compositor.device.create_command_encoder( &wgpu::CommandEncoderDescriptor { label: Some("iced_wgpu encoder"), }, ); let view = &frame .texture .create_view(&wgpu::TextureViewDescriptor::default()); renderer.present( &mut compositor.engine, &compositor.device, &compositor.queue, &mut encoder, Some(background_color), frame.texture.format(), view, viewport, overlay, ); let _ = compositor.engine.submit(&compositor.queue, encoder); // Present the frame frame.present(); Ok(()) } Err(error) => match error { wgpu::SurfaceError::Timeout => { Err(compositor::SurfaceError::Timeout) } wgpu::SurfaceError::Outdated => { Err(compositor::SurfaceError::Outdated) } wgpu::SurfaceError::Lost => Err(compositor::SurfaceError::Lost), wgpu::SurfaceError::OutOfMemory => { Err(compositor::SurfaceError::OutOfMemory) } wgpu::SurfaceError::Other => Err(compositor::SurfaceError::Other), }, } } impl graphics::Compositor for Compositor { type Renderer = Renderer; type Surface = wgpu::Surface<'static>; async fn with_backend( settings: graphics::Settings, compatible_window: W, backend: Option<&str>, ) -> Result { match backend { None | Some("wgpu") => { let mut settings = Settings::from(settings); if let Some(backends) = wgpu::Backends::from_env() { settings.backends = backends; } if let Some(present_mode) = settings::present_mode_from_env() { settings.present_mode = present_mode; } Ok(new(settings, compatible_window).await?) } Some(backend) => Err(graphics::Error::GraphicsAdapterNotFound { backend: "wgpu", reason: error::Reason::DidNotMatch { preferred_backend: backend.to_owned(), }, }), } } fn create_renderer(&self) -> Self::Renderer { Renderer::new( &self.device, &self.engine, self.settings.default_font, self.settings.default_text_size, ) } fn create_surface( &mut self, window: W, width: u32, height: u32, ) -> Self::Surface { let mut surface = self .instance .create_surface(window) .expect("Create surface"); if width > 0 && height > 0 { self.configure_surface(&mut surface, width, height); } surface } fn configure_surface( &mut self, surface: &mut Self::Surface, width: u32, height: u32, ) { surface.configure( &self.device, &wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: self.format, present_mode: self.settings.present_mode, width, height, alpha_mode: self.alpha_mode, view_formats: vec![], desired_maximum_frame_latency: 1, }, ); } fn fetch_information(&self) -> compositor::Information { let information = self.adapter.get_info(); compositor::Information { adapter: information.name, backend: format!("{:?}", information.backend), } } fn present>( &mut self, renderer: &mut Self::Renderer, surface: &mut Self::Surface, viewport: &Viewport, background_color: Color, overlay: &[T], ) -> Result<(), compositor::SurfaceError> { present(self, renderer, surface, viewport, background_color, overlay) } fn screenshot>( &mut self, renderer: &mut Self::Renderer, viewport: &Viewport, background_color: Color, overlay: &[T], ) -> Vec { screenshot(self, renderer, viewport, background_color, overlay) } } /// Renders the current surface to an offscreen buffer. /// /// Returns RGBA bytes of the texture data. pub fn screenshot>( compositor: &mut Compositor, renderer: &mut Renderer, viewport: &Viewport, background_color: Color, overlay: &[T], ) -> Vec { let dimensions = BufferDimensions::new(viewport.physical_size()); let texture_extent = wgpu::Extent3d { width: dimensions.width, height: dimensions.height, depth_or_array_layers: 1, }; let texture = compositor.device.create_texture(&wgpu::TextureDescriptor { label: Some("iced_wgpu.offscreen.source_texture"), size: texture_extent, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: compositor.format, usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::TEXTURE_BINDING, view_formats: &[], }); let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); let mut encoder = compositor.device.create_command_encoder( &wgpu::CommandEncoderDescriptor { label: Some("iced_wgpu.offscreen.encoder"), }, ); renderer.present( &mut compositor.engine, &compositor.device, &compositor.queue, &mut encoder, Some(background_color), texture.format(), &view, viewport, overlay, ); let texture = crate::color::convert( &compositor.device, &mut encoder, texture, if color::GAMMA_CORRECTION { wgpu::TextureFormat::Rgba8UnormSrgb } else { wgpu::TextureFormat::Rgba8Unorm }, ); let output_buffer = compositor.device.create_buffer(&wgpu::BufferDescriptor { label: Some("iced_wgpu.offscreen.output_texture_buffer"), size: (dimensions.padded_bytes_per_row * dimensions.height as usize) as u64, usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); encoder.copy_texture_to_buffer( texture.as_image_copy(), wgpu::TexelCopyBufferInfo { buffer: &output_buffer, layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(dimensions.padded_bytes_per_row as u32), rows_per_image: None, }, }, texture_extent, ); let index = compositor.engine.submit(&compositor.queue, encoder); let slice = output_buffer.slice(..); slice.map_async(wgpu::MapMode::Read, |_| {}); let _ = compositor .device .poll(wgpu::Maintain::WaitForSubmissionIndex(index)); let mapped_buffer = slice.get_mapped_range(); mapped_buffer.chunks(dimensions.padded_bytes_per_row).fold( vec![], |mut acc, row| { acc.extend(&row[..dimensions.unpadded_bytes_per_row]); acc }, ) } #[derive(Clone, Copy, Debug)] struct BufferDimensions { width: u32, height: u32, unpadded_bytes_per_row: usize, padded_bytes_per_row: usize, } impl BufferDimensions { fn new(size: Size) -> Self { let unpadded_bytes_per_row = size.width as usize * 4; //slice of buffer per row; always RGBA let alignment = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; //256 let padded_bytes_per_row_padding = (alignment - unpadded_bytes_per_row % alignment) % alignment; let padded_bytes_per_row = unpadded_bytes_per_row + padded_bytes_per_row_padding; Self { width: size.width, height: size.height, unpadded_bytes_per_row, padded_bytes_per_row, } } }