bevy_render/view/window/
mod.rs

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