Skip to main content

bevy_render/view/window/
mod.rs

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