Skip to main content

iced_wgpu/window/
compositor.rs

1//! Connect a window with a renderer.
2use crate::core::Color;
3use crate::core::renderer;
4use crate::graphics::color;
5use crate::graphics::compositor;
6use crate::graphics::error;
7use crate::graphics::{self, Antialiasing, Shell, Viewport};
8use crate::{Engine, Renderer};
9
10/// A window graphics backend for iced powered by `wgpu`.
11pub struct Compositor {
12    instance: wgpu::Instance,
13    adapter: wgpu::Adapter,
14    format: wgpu::TextureFormat,
15    alpha_mode: wgpu::CompositeAlphaMode,
16    engine: Engine,
17    settings: Settings,
18}
19
20/// A compositor error.
21#[derive(Debug, Clone, thiserror::Error)]
22pub enum Error {
23    /// The surface creation failed.
24    #[error("the surface creation failed: {0}")]
25    SurfaceCreationFailed(#[from] wgpu::CreateSurfaceError),
26    /// The surface is not compatible.
27    #[error("the surface is not compatible")]
28    IncompatibleSurface,
29    /// No adapter was found for the options requested.
30    #[error("no adapter was found for the options requested: {0:?}")]
31    NoAdapterFound(String),
32    /// No device request succeeded.
33    #[error("no device request succeeded: {0:?}")]
34    RequestDeviceFailed(Vec<(wgpu::Limits, wgpu::RequestDeviceError)>),
35}
36
37impl From<Error> for graphics::Error {
38    fn from(error: Error) -> Self {
39        Self::GraphicsAdapterNotFound {
40            backend: "wgpu",
41            reason: error::Reason::RequestFailed(error.to_string()),
42        }
43    }
44}
45
46impl Compositor {
47    /// Requests a new [`Compositor`] with the given [`Settings`].
48    ///
49    /// Returns `None` if no compatible graphics adapter could be found.
50    pub async fn request(
51        settings: Settings,
52        display: impl compositor::Display,
53        compatible_window: impl compositor::Window,
54        shell: Shell,
55    ) -> Result<Self, Error> {
56        let instance = wgpu::util::new_instance_with_webgpu_detection(wgpu::InstanceDescriptor {
57            backends: settings.backends,
58            flags: if cfg!(feature = "strict-assertions") {
59                wgpu::InstanceFlags::debugging()
60            } else {
61                wgpu::InstanceFlags::empty()
62            },
63            ..wgpu::InstanceDescriptor::new_with_display_handle(Box::new(display))
64        })
65        .await;
66
67        log::info!("{settings:#?}");
68
69        #[cfg(not(target_arch = "wasm32"))]
70        if log::max_level() >= log::LevelFilter::Info {
71            let available_adapters: Vec<_> = instance
72                .enumerate_adapters(settings.backends)
73                .await
74                .iter()
75                .map(wgpu::Adapter::get_info)
76                .collect();
77            log::info!("Available adapters: {available_adapters:#?}");
78        }
79
80        #[allow(unsafe_code)]
81        let compatible_surface = instance
82            .create_surface(wgpu::SurfaceTarget::Window(Box::new(compatible_window)))
83            .ok();
84
85        let adapter_options = wgpu::RequestAdapterOptions {
86            power_preference: wgpu::PowerPreference::from_env()
87                .unwrap_or(wgpu::PowerPreference::HighPerformance),
88            compatible_surface: compatible_surface.as_ref(),
89            force_fallback_adapter: false,
90        };
91
92        let adapter = instance
93            .request_adapter(&adapter_options)
94            .await
95            .map_err(|_error| Error::NoAdapterFound(format!("{adapter_options:?}")))?;
96
97        log::info!("Selected: {:#?}", adapter.get_info());
98
99        let (format, alpha_mode) = compatible_surface
100            .as_ref()
101            .and_then(|surface| {
102                let capabilities = surface.get_capabilities(&adapter);
103
104                let formats = capabilities.formats.iter().copied();
105
106                log::info!("Available formats: {formats:#?}");
107
108                const BLACKLIST: &[wgpu::TextureFormat] = &[
109                    wgpu::TextureFormat::Rgb10a2Unorm,
110                    wgpu::TextureFormat::Rgb10a2Uint,
111                ];
112
113                let mut formats = formats.filter(|format| {
114                    format.required_features() == wgpu::Features::empty()
115                        && !BLACKLIST.contains(format)
116                });
117
118                let format = if color::GAMMA_CORRECTION {
119                    formats.find(wgpu::TextureFormat::is_srgb)
120                } else {
121                    formats.find(|format| !wgpu::TextureFormat::is_srgb(format))
122                };
123
124                let format = format.or_else(|| {
125                    log::warn!("No format found!");
126
127                    capabilities.formats.first().copied()
128                });
129
130                let alpha_modes = capabilities.alpha_modes;
131
132                log::info!("Available alpha modes: {alpha_modes:#?}");
133
134                let preferred_alpha =
135                    if alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) {
136                        wgpu::CompositeAlphaMode::PreMultiplied
137                    } else {
138                        wgpu::CompositeAlphaMode::Auto
139                    };
140
141                format.zip(Some(preferred_alpha))
142            })
143            .ok_or(Error::IncompatibleSurface)?;
144
145        log::info!("Selected format: {format:?} with alpha mode: {alpha_mode:?}");
146
147        #[cfg(target_arch = "wasm32")]
148        let limits = [wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits())];
149
150        #[cfg(not(target_arch = "wasm32"))]
151        let limits = [wgpu::Limits::default(), wgpu::Limits::downlevel_defaults()];
152
153        let limits = limits.into_iter().map(|limits| wgpu::Limits {
154            max_bind_groups: 2,
155            max_non_sampler_bindings: 2048,
156            ..limits
157        });
158
159        // Request SHADER_F16 only if the adapter supports it (e.g., not available in WebGL2)
160        let required_features = if adapter.features().contains(wgpu::Features::SHADER_F16) {
161            wgpu::Features::SHADER_F16
162        } else {
163            wgpu::Features::empty()
164        };
165
166        let mut errors = Vec::new();
167
168        for required_limits in limits {
169            let result = adapter
170                .request_device(&wgpu::DeviceDescriptor {
171                    label: Some("iced_wgpu::window::compositor device descriptor"),
172                    required_features,
173                    required_limits: required_limits.clone(),
174                    memory_hints: wgpu::MemoryHints::MemoryUsage,
175                    trace: wgpu::Trace::Off,
176                    experimental_features: wgpu::ExperimentalFeatures::disabled(),
177                })
178                .await;
179
180            match result {
181                Ok((device, queue)) => {
182                    let engine = Engine::new(
183                        &adapter,
184                        device,
185                        queue,
186                        format,
187                        settings.antialiasing,
188                        shell,
189                    );
190
191                    return Ok(Compositor {
192                        instance,
193                        adapter,
194                        format,
195                        alpha_mode,
196                        engine,
197                        settings,
198                    });
199                }
200                Err(error) => {
201                    errors.push((required_limits, error));
202                }
203            }
204        }
205
206        Err(Error::RequestDeviceFailed(errors))
207    }
208}
209
210/// Creates a [`Compositor`] with the given [`Settings`] and window.
211pub async fn new(
212    settings: Settings,
213    display: impl compositor::Display,
214    compatible_window: impl compositor::Window,
215    shell: Shell,
216) -> Result<Compositor, Error> {
217    Compositor::request(settings, display, compatible_window, shell).await
218}
219
220/// Presents the given primitives with the given [`Compositor`].
221pub fn present(
222    renderer: &mut Renderer,
223    surface: &mut wgpu::Surface<'static>,
224    viewport: &Viewport,
225    background_color: Color,
226    on_pre_present: impl FnOnce(),
227) -> Result<(), compositor::SurfaceError> {
228    match surface.get_current_texture() {
229        wgpu::CurrentSurfaceTexture::Success(frame) => {
230            let view = &frame
231                .texture
232                .create_view(&wgpu::TextureViewDescriptor::default());
233
234            let _submission = renderer.present(
235                Some(background_color),
236                frame.texture.format(),
237                view,
238                viewport,
239            );
240
241            // Present the frame
242            on_pre_present();
243            frame.present();
244
245            Ok(())
246        }
247        wgpu::CurrentSurfaceTexture::Suboptimal(_) | wgpu::CurrentSurfaceTexture::Outdated => {
248            Err(compositor::SurfaceError::Outdated)
249        }
250        wgpu::CurrentSurfaceTexture::Timeout => Err(compositor::SurfaceError::Timeout),
251        wgpu::CurrentSurfaceTexture::Occluded => Err(compositor::SurfaceError::Occluded),
252        wgpu::CurrentSurfaceTexture::Lost => Err(compositor::SurfaceError::Lost),
253        wgpu::CurrentSurfaceTexture::Validation => Err(compositor::SurfaceError::Other),
254    }
255}
256
257impl graphics::Compositor for Compositor {
258    type Renderer = Renderer;
259    type Surface = wgpu::Surface<'static>;
260
261    async fn with_backend(
262        settings: compositor::Settings,
263        display: impl compositor::Display,
264        compatible_window: impl compositor::Window,
265        shell: Shell,
266        backend: Option<&str>,
267    ) -> Result<Self, graphics::Error> {
268        match backend {
269            None | Some("wgpu") => {
270                let mut settings = Settings::from(settings);
271
272                if let Some(backends) = wgpu::Backends::from_env() {
273                    settings.backends = backends;
274                }
275
276                if let Some(present_mode) = present_mode_from_env() {
277                    settings.present_mode = present_mode;
278                }
279
280                Ok(new(settings, display, compatible_window, shell).await?)
281            }
282            Some(backend) => Err(graphics::Error::GraphicsAdapterNotFound {
283                backend: "wgpu",
284                reason: error::Reason::DidNotMatch {
285                    preferred_backend: backend.to_owned(),
286                },
287            }),
288        }
289    }
290
291    fn create_renderer(&self, settings: renderer::Settings) -> Self::Renderer {
292        Renderer::new(self.engine.clone(), settings)
293    }
294
295    fn create_surface(
296        &mut self,
297        window: impl compositor::Window,
298        width: u32,
299        height: u32,
300    ) -> Self::Surface {
301        let mut surface = self
302            .instance
303            .create_surface(wgpu::SurfaceTarget::Window(Box::new(window)))
304            .expect("Create surface");
305
306        if width > 0 && height > 0 {
307            self.configure_surface(&mut surface, width, height);
308        }
309
310        surface
311    }
312
313    fn configure_surface(&mut self, surface: &mut Self::Surface, width: u32, height: u32) {
314        surface.configure(
315            &self.engine.device,
316            &wgpu::SurfaceConfiguration {
317                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
318                format: self.format,
319                present_mode: self.settings.present_mode,
320                width,
321                height,
322                alpha_mode: self.alpha_mode,
323                view_formats: vec![],
324                desired_maximum_frame_latency: 1,
325            },
326        );
327    }
328
329    fn information(&self) -> compositor::Information {
330        let information = self.adapter.get_info();
331
332        compositor::Information {
333            adapter: information.name,
334            backend: format!("{:?}", information.backend),
335        }
336    }
337
338    fn present(
339        &mut self,
340        renderer: &mut Self::Renderer,
341        surface: &mut Self::Surface,
342        viewport: &Viewport,
343        background_color: Color,
344        on_pre_present: impl FnOnce(),
345    ) -> Result<(), compositor::SurfaceError> {
346        present(
347            renderer,
348            surface,
349            viewport,
350            background_color,
351            on_pre_present,
352        )
353    }
354
355    fn screenshot(
356        &mut self,
357        renderer: &mut Self::Renderer,
358        viewport: &Viewport,
359        background_color: Color,
360    ) -> Vec<u8> {
361        renderer.screenshot(viewport, background_color)
362    }
363}
364
365/// The settings of a [`Compositor`].
366#[derive(Debug, Clone, Copy, PartialEq)]
367pub struct Settings {
368    /// The present mode of the [`Renderer`].
369    ///
370    /// [`Renderer`]: crate::Renderer
371    pub present_mode: wgpu::PresentMode,
372
373    /// The graphics backends to use.
374    pub backends: wgpu::Backends,
375
376    /// The antialiasing strategy that will be used for triangle primitives.
377    ///
378    /// By default, it is `None`.
379    pub antialiasing: Option<Antialiasing>,
380}
381
382impl Default for Settings {
383    fn default() -> Settings {
384        Settings {
385            present_mode: wgpu::PresentMode::AutoVsync,
386            backends: wgpu::Backends::all(),
387            antialiasing: None,
388        }
389    }
390}
391
392impl From<compositor::Settings> for Settings {
393    fn from(settings: compositor::Settings) -> Self {
394        Self {
395            present_mode: if settings.vsync {
396                wgpu::PresentMode::AutoVsync
397            } else {
398                wgpu::PresentMode::AutoNoVsync
399            },
400            antialiasing: settings.antialiasing,
401            ..Settings::default()
402        }
403    }
404}
405
406/// Obtains a [`wgpu::PresentMode`] from the current environment
407/// configuration, if set.
408///
409/// The value returned by this function can be changed by setting
410/// the `ICED_PRESENT_MODE` env variable. The possible values are:
411///
412/// - `vsync` → [`wgpu::PresentMode::AutoVsync`]
413/// - `no_vsync` → [`wgpu::PresentMode::AutoNoVsync`]
414/// - `immediate` → [`wgpu::PresentMode::Immediate`]
415/// - `fifo` → [`wgpu::PresentMode::Fifo`]
416/// - `fifo_relaxed` → [`wgpu::PresentMode::FifoRelaxed`]
417/// - `mailbox` → [`wgpu::PresentMode::Mailbox`]
418pub fn present_mode_from_env() -> Option<wgpu::PresentMode> {
419    let present_mode = std::env::var("ICED_PRESENT_MODE").ok()?;
420
421    match present_mode.to_lowercase().as_str() {
422        "vsync" => Some(wgpu::PresentMode::AutoVsync),
423        "no_vsync" => Some(wgpu::PresentMode::AutoNoVsync),
424        "immediate" => Some(wgpu::PresentMode::Immediate),
425        "fifo" => Some(wgpu::PresentMode::Fifo),
426        "fifo_relaxed" => Some(wgpu::PresentMode::FifoRelaxed),
427        "mailbox" => Some(wgpu::PresentMode::Mailbox),
428        _ => None,
429    }
430}