bevy_render/view/window/
mod.rs

1use crate::renderer::WgpuWrapper;
2use crate::{
3    render_resource::{SurfaceTexture, TextureView},
4    renderer::{RenderAdapter, RenderDevice, RenderInstance},
5    Extract, ExtractSchedule, Render, RenderApp, RenderSystems,
6};
7use bevy_app::{App, Plugin};
8use bevy_ecs::{entity::EntityHashMap, prelude::*};
9use bevy_platform::collections::HashSet;
10use bevy_utils::default;
11use bevy_window::{
12    CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosing,
13};
14use core::{
15    num::NonZero,
16    ops::{Deref, DerefMut},
17};
18use tracing::{debug, info, warn};
19use wgpu::{
20    SurfaceConfiguration, SurfaceTargetUnsafe, TextureFormat, TextureUsages, TextureViewDescriptor,
21};
22
23pub mod screenshot;
24
25use screenshot::ScreenshotPlugin;
26
27pub struct WindowRenderPlugin;
28
29impl Plugin for WindowRenderPlugin {
30    fn build(&self, app: &mut App) {
31        app.add_plugins(ScreenshotPlugin);
32
33        if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
34            render_app
35                .init_resource::<ExtractedWindows>()
36                .init_resource::<WindowSurfaces>()
37                .add_systems(ExtractSchedule, extract_windows)
38                .add_systems(
39                    Render,
40                    create_surfaces
41                        .run_if(need_surface_configuration)
42                        .before(prepare_windows),
43                )
44                .add_systems(Render, prepare_windows.in_set(RenderSystems::ManageViews));
45        }
46    }
47}
48
49pub struct ExtractedWindow {
50    /// An entity that contains the components in [`Window`].
51    pub entity: Entity,
52    pub handle: RawHandleWrapper,
53    pub physical_width: u32,
54    pub physical_height: u32,
55    pub present_mode: PresentMode,
56    pub desired_maximum_frame_latency: Option<NonZero<u32>>,
57    /// Note: this will not always be the swap chain texture view. When taking a screenshot,
58    /// this will point to an alternative texture instead to allow for copying the render result
59    /// to CPU memory.
60    pub swap_chain_texture_view: Option<TextureView>,
61    pub swap_chain_texture: Option<SurfaceTexture>,
62    pub swap_chain_texture_format: Option<TextureFormat>,
63    pub swap_chain_texture_view_format: Option<TextureFormat>,
64    pub size_changed: bool,
65    pub present_mode_changed: bool,
66    pub alpha_mode: CompositeAlphaMode,
67    /// Whether this window needs an initial buffer commit.
68    ///
69    /// On Wayland, windows must present at least once before they are shown.
70    /// See <https://wayland.app/protocols/xdg-shell#xdg_surface>
71    pub needs_initial_present: bool,
72}
73
74impl ExtractedWindow {
75    fn set_swapchain_texture(&mut self, frame: wgpu::SurfaceTexture) {
76        self.swap_chain_texture_view_format = Some(frame.texture.format().add_srgb_suffix());
77        let texture_view_descriptor = TextureViewDescriptor {
78            format: self.swap_chain_texture_view_format,
79            ..default()
80        };
81        self.swap_chain_texture_view = Some(TextureView::from(
82            frame.texture.create_view(&texture_view_descriptor),
83        ));
84        self.swap_chain_texture = Some(SurfaceTexture::from(frame));
85    }
86
87    fn has_swapchain_texture(&self) -> bool {
88        self.swap_chain_texture_view.is_some() && self.swap_chain_texture.is_some()
89    }
90
91    pub fn present(&mut self) {
92        if let Some(surface_texture) = self.swap_chain_texture.take() {
93            // TODO(clean): winit docs recommends calling pre_present_notify before this.
94            // though `present()` doesn't present the frame, it schedules it to be presented
95            // by wgpu.
96            // https://docs.rs/winit/0.29.9/wasm32-unknown-unknown/winit/window/struct.Window.html#method.pre_present_notify
97            surface_texture.present();
98        }
99    }
100}
101
102#[derive(Default, Resource)]
103pub struct ExtractedWindows {
104    pub primary: Option<Entity>,
105    pub windows: EntityHashMap<ExtractedWindow>,
106}
107
108impl Deref for ExtractedWindows {
109    type Target = EntityHashMap<ExtractedWindow>;
110
111    fn deref(&self) -> &Self::Target {
112        &self.windows
113    }
114}
115
116impl DerefMut for ExtractedWindows {
117    fn deref_mut(&mut self) -> &mut Self::Target {
118        &mut self.windows
119    }
120}
121
122fn extract_windows(
123    mut extracted_windows: ResMut<ExtractedWindows>,
124    mut closing: Extract<MessageReader<WindowClosing>>,
125    windows: Extract<Query<(Entity, &Window, &RawHandleWrapper, Option<&PrimaryWindow>)>>,
126    mut removed: Extract<RemovedComponents<RawHandleWrapper>>,
127    mut window_surfaces: ResMut<WindowSurfaces>,
128) {
129    for (entity, window, handle, primary) in windows.iter() {
130        if primary.is_some() {
131            extracted_windows.primary = Some(entity);
132        }
133
134        let (new_width, new_height) = (
135            window.resolution.physical_width().max(1),
136            window.resolution.physical_height().max(1),
137        );
138
139        let extracted_window = extracted_windows.entry(entity).or_insert(ExtractedWindow {
140            entity,
141            handle: handle.clone(),
142            physical_width: new_width,
143            physical_height: new_height,
144            present_mode: window.present_mode,
145            desired_maximum_frame_latency: window.desired_maximum_frame_latency,
146            swap_chain_texture: None,
147            swap_chain_texture_view: None,
148            size_changed: false,
149            swap_chain_texture_format: None,
150            swap_chain_texture_view_format: None,
151            present_mode_changed: false,
152            alpha_mode: window.composite_alpha_mode,
153            needs_initial_present: true,
154        });
155
156        if extracted_window.swap_chain_texture.is_none() {
157            // If we called present on the previous swap-chain texture last update,
158            // then drop the swap chain frame here, otherwise we can keep it for the
159            // next update as an optimization. `prepare_windows` will only acquire a new
160            // swap chain texture if needed.
161            extracted_window.swap_chain_texture_view = None;
162        }
163        extracted_window.size_changed = new_width != extracted_window.physical_width
164            || new_height != extracted_window.physical_height;
165        extracted_window.present_mode_changed =
166            window.present_mode != extracted_window.present_mode;
167
168        if extracted_window.size_changed {
169            debug!(
170                "Window size changed from {}x{} to {}x{}",
171                extracted_window.physical_width,
172                extracted_window.physical_height,
173                new_width,
174                new_height
175            );
176            extracted_window.physical_width = new_width;
177            extracted_window.physical_height = new_height;
178        }
179
180        if extracted_window.present_mode_changed {
181            debug!(
182                "Window Present Mode changed from {:?} to {:?}",
183                extracted_window.present_mode, window.present_mode
184            );
185            extracted_window.present_mode = window.present_mode;
186        }
187    }
188
189    for closing_window in closing.read() {
190        extracted_windows.remove(&closing_window.window);
191        window_surfaces.remove(&closing_window.window);
192    }
193    for removed_window in removed.read() {
194        extracted_windows.remove(&removed_window);
195        window_surfaces.remove(&removed_window);
196    }
197}
198
199struct SurfaceData {
200    // TODO: what lifetime should this be?
201    surface: WgpuWrapper<wgpu::Surface<'static>>,
202    configuration: SurfaceConfiguration,
203    texture_view_format: Option<TextureFormat>,
204}
205
206#[derive(Resource, Default)]
207pub struct WindowSurfaces {
208    surfaces: EntityHashMap<SurfaceData>,
209    /// List of windows that we have already called the initial `configure_surface` for
210    configured_windows: HashSet<Entity>,
211}
212
213impl WindowSurfaces {
214    fn remove(&mut self, window: &Entity) {
215        self.surfaces.remove(window);
216        self.configured_windows.remove(window);
217    }
218}
219
220/// (re)configures window surfaces, and obtains a swapchain texture for rendering.
221///
222/// NOTE: `get_current_texture` in `prepare_windows` can take a long time if the GPU workload is
223/// the performance bottleneck. This can be seen in profiles as multiple prepare-set systems all
224/// taking an unusually long time to complete, and all finishing at about the same time as the
225/// `prepare_windows` system. Improvements in bevy are planned to avoid this happening when it
226/// should not but it will still happen as it is easy for a user to create a large GPU workload
227/// relative to the GPU performance and/or CPU workload.
228/// This can be caused by many reasons, but several of them are:
229/// - GPU workload is more than your current GPU can manage
230/// - Error / performance bug in your custom shaders
231/// - wgpu was unable to detect a proper GPU hardware-accelerated device given the chosen
232///   [`Backends`](crate::settings::Backends), [`WgpuLimits`](crate::settings::WgpuLimits),
233///   and/or [`WgpuFeatures`](crate::settings::WgpuFeatures). For example, on Windows currently
234///   `DirectX 11` is not supported by wgpu 0.12 and so if your GPU/drivers do not support Vulkan,
235///   it may be that a software renderer called "Microsoft Basic Render Driver" using `DirectX 12`
236///   will be chosen and performance will be very poor. This is visible in a log message that is
237///   output during renderer initialization.
238///   Another alternative is to try to use [`ANGLE`](https://github.com/gfx-rs/wgpu#angle) and
239///   [`Backends::GL`](crate::settings::Backends::GL) with the `gles` feature enabled if your
240///   GPU/drivers support `OpenGL 4.3` / `OpenGL ES 3.0` or later.
241pub fn prepare_windows(
242    mut windows: ResMut<ExtractedWindows>,
243    mut window_surfaces: ResMut<WindowSurfaces>,
244    render_device: Res<RenderDevice>,
245    #[cfg(target_os = "linux")] render_instance: Res<RenderInstance>,
246) {
247    for window in windows.windows.values_mut() {
248        let window_surfaces = window_surfaces.deref_mut();
249        let Some(surface_data) = window_surfaces.surfaces.get(&window.entity) else {
250            continue;
251        };
252
253        // We didn't present the previous frame, so we can keep using our existing swapchain texture.
254        if window.has_swapchain_texture() && !window.size_changed && !window.present_mode_changed {
255            continue;
256        }
257
258        // A recurring issue is hitting `wgpu::SurfaceError::Timeout` on certain Linux
259        // mesa driver implementations. This seems to be a quirk of some drivers.
260        // We'd rather keep panicking when not on Linux mesa, because in those case,
261        // the `Timeout` is still probably the symptom of a degraded unrecoverable
262        // application state.
263        // see https://github.com/bevyengine/bevy/pull/5957
264        // and https://github.com/gfx-rs/wgpu/issues/1218
265        #[cfg(target_os = "linux")]
266        let may_erroneously_timeout = || {
267            render_instance
268                .enumerate_adapters(wgpu::Backends::VULKAN)
269                .iter()
270                .any(|adapter| {
271                    let name = adapter.get_info().name;
272                    name.starts_with("Radeon")
273                        || name.starts_with("AMD")
274                        || name.starts_with("Intel")
275                })
276        };
277
278        let surface = &surface_data.surface;
279        match surface.get_current_texture() {
280            Ok(frame) => {
281                window.set_swapchain_texture(frame);
282            }
283            Err(wgpu::SurfaceError::Outdated) => {
284                render_device.configure_surface(surface, &surface_data.configuration);
285                let frame = match surface.get_current_texture() {
286                    Ok(frame) => frame,
287                    Err(err) => {
288                        // This is a common occurrence on X11 and Xwayland with NVIDIA drivers
289                        // when opening and resizing the window.
290                        warn!("Couldn't get swap chain texture after configuring. Cause: '{err}'");
291                        continue;
292                    }
293                };
294                window.set_swapchain_texture(frame);
295            }
296            #[cfg(target_os = "linux")]
297            Err(wgpu::SurfaceError::Timeout) if may_erroneously_timeout() => {
298                tracing::trace!(
299                    "Couldn't get swap chain texture. This is probably a quirk \
300                        of your Linux GPU driver, so it can be safely ignored."
301                );
302            }
303            Err(err) => {
304                panic!("Couldn't get swap chain texture, operation unrecoverable: {err}");
305            }
306        }
307        window.swap_chain_texture_format = Some(surface_data.configuration.format);
308    }
309}
310
311pub fn need_surface_configuration(
312    windows: Res<ExtractedWindows>,
313    window_surfaces: Res<WindowSurfaces>,
314) -> bool {
315    for window in windows.windows.values() {
316        if !window_surfaces.configured_windows.contains(&window.entity)
317            || window.size_changed
318            || window.present_mode_changed
319        {
320            return true;
321        }
322    }
323    false
324}
325
326// 2 is wgpu's default/what we've been using so far.
327// 1 is the minimum, but may cause lower framerates due to the cpu waiting for the gpu to finish
328// all work for the previous frame before starting work on the next frame, which then means the gpu
329// has to wait for the cpu to finish to start on the next frame.
330const DEFAULT_DESIRED_MAXIMUM_FRAME_LATENCY: u32 = 2;
331
332/// Creates window surfaces.
333pub fn create_surfaces(
334    // By accessing a NonSend resource, we tell the scheduler to put this system on the main thread,
335    // which is necessary for some OS's
336    #[cfg(any(target_os = "macos", target_os = "ios"))] _marker: bevy_ecs::system::NonSendMarker,
337    mut windows: ResMut<ExtractedWindows>,
338    mut window_surfaces: ResMut<WindowSurfaces>,
339    render_instance: Res<RenderInstance>,
340    render_adapter: Res<RenderAdapter>,
341    render_device: Res<RenderDevice>,
342) {
343    for window in windows.windows.values_mut() {
344        let data = window_surfaces
345            .surfaces
346            .entry(window.entity)
347            .or_insert_with(|| {
348                let surface_target = SurfaceTargetUnsafe::RawHandle {
349                    raw_display_handle: window.handle.get_display_handle(),
350                    raw_window_handle: window.handle.get_window_handle(),
351                };
352                // SAFETY: The window handles in ExtractedWindows will always be valid objects to create surfaces on
353                let surface = unsafe {
354                    // NOTE: On some OSes this MUST be called from the main thread.
355                    // As of wgpu 0.15, only fallible if the given window is a HTML canvas and obtaining a WebGPU or WebGL2 context fails.
356                    render_instance
357                        .create_surface_unsafe(surface_target)
358                        .expect("Failed to create wgpu surface")
359                };
360                let caps = surface.get_capabilities(&render_adapter);
361                let present_mode = present_mode(window, &caps);
362                let formats = caps.formats;
363                // For future HDR output support, we'll need to request a format that supports HDR,
364                // but as of wgpu 0.15 that is not yet supported.
365                // Prefer sRGB formats for surfaces, but fall back to first available format if no sRGB formats are available.
366                let mut format = *formats.first().expect("No supported formats for surface");
367                for available_format in formats {
368                    // Rgba8UnormSrgb and Bgra8UnormSrgb and the only sRGB formats wgpu exposes that we can use for surfaces.
369                    if available_format == TextureFormat::Rgba8UnormSrgb
370                        || available_format == TextureFormat::Bgra8UnormSrgb
371                    {
372                        format = available_format;
373                        break;
374                    }
375                }
376
377                let texture_view_format = if !format.is_srgb() {
378                    Some(format.add_srgb_suffix())
379                } else {
380                    None
381                };
382                let configuration = SurfaceConfiguration {
383                    format,
384                    width: window.physical_width,
385                    height: window.physical_height,
386                    usage: TextureUsages::RENDER_ATTACHMENT,
387                    present_mode,
388                    desired_maximum_frame_latency: window
389                        .desired_maximum_frame_latency
390                        .map(NonZero::<u32>::get)
391                        .unwrap_or(DEFAULT_DESIRED_MAXIMUM_FRAME_LATENCY),
392                    alpha_mode: match window.alpha_mode {
393                        CompositeAlphaMode::Auto => wgpu::CompositeAlphaMode::Auto,
394                        CompositeAlphaMode::Opaque => wgpu::CompositeAlphaMode::Opaque,
395                        CompositeAlphaMode::PreMultiplied => {
396                            wgpu::CompositeAlphaMode::PreMultiplied
397                        }
398                        CompositeAlphaMode::PostMultiplied => {
399                            wgpu::CompositeAlphaMode::PostMultiplied
400                        }
401                        CompositeAlphaMode::Inherit => wgpu::CompositeAlphaMode::Inherit,
402                    },
403                    view_formats: match texture_view_format {
404                        Some(format) => vec![format],
405                        None => vec![],
406                    },
407                };
408
409                render_device.configure_surface(&surface, &configuration);
410
411                SurfaceData {
412                    surface: WgpuWrapper::new(surface),
413                    configuration,
414                    texture_view_format,
415                }
416            });
417
418        if window.size_changed || window.present_mode_changed {
419            // normally this is dropped on present but we double check here to be safe as failure to
420            // drop it will cause validation errors in wgpu
421            drop(window.swap_chain_texture.take());
422            #[cfg_attr(
423                target_arch = "wasm32",
424                expect(clippy::drop_non_drop, reason = "texture views are not drop on wasm")
425            )]
426            drop(window.swap_chain_texture_view.take());
427
428            data.configuration.width = window.physical_width;
429            data.configuration.height = window.physical_height;
430            let caps = data.surface.get_capabilities(&render_adapter);
431            data.configuration.present_mode = present_mode(window, &caps);
432            render_device.configure_surface(&data.surface, &data.configuration);
433        }
434
435        window_surfaces.configured_windows.insert(window.entity);
436    }
437}
438
439fn present_mode(
440    window: &mut ExtractedWindow,
441    caps: &wgpu::SurfaceCapabilities,
442) -> wgpu::PresentMode {
443    let present_mode = match window.present_mode {
444        PresentMode::Fifo => wgpu::PresentMode::Fifo,
445        PresentMode::FifoRelaxed => wgpu::PresentMode::FifoRelaxed,
446        PresentMode::Mailbox => wgpu::PresentMode::Mailbox,
447        PresentMode::Immediate => wgpu::PresentMode::Immediate,
448        PresentMode::AutoVsync => wgpu::PresentMode::AutoVsync,
449        PresentMode::AutoNoVsync => wgpu::PresentMode::AutoNoVsync,
450    };
451    let fallbacks = match present_mode {
452        wgpu::PresentMode::AutoVsync => {
453            &[wgpu::PresentMode::FifoRelaxed, wgpu::PresentMode::Fifo][..]
454        }
455        wgpu::PresentMode::AutoNoVsync => &[
456            wgpu::PresentMode::Immediate,
457            wgpu::PresentMode::Mailbox,
458            wgpu::PresentMode::Fifo,
459        ][..],
460        wgpu::PresentMode::Mailbox => &[
461            wgpu::PresentMode::Mailbox,
462            wgpu::PresentMode::Immediate,
463            wgpu::PresentMode::Fifo,
464        ][..],
465        // Always end in FIFO to make sure it's always supported
466        x => &[x, wgpu::PresentMode::Fifo][..],
467    };
468    let new_present_mode = fallbacks
469        .iter()
470        .copied()
471        .find(|fallback| caps.present_modes.contains(fallback))
472        .unwrap_or_else(|| {
473            unreachable!(
474                "Fallback system failed to choose present mode. \
475                            This is a bug. Mode: {:?}, Options: {:?}",
476                window.present_mode, &caps.present_modes
477            );
478        });
479    if new_present_mode != present_mode && fallbacks.contains(&present_mode) {
480        info!("PresentMode {present_mode:?} requested but not available. Falling back to {new_present_mode:?}");
481    }
482    new_present_mode
483}