bevy_render/view/window/
mod.rs

1use crate::{
2    render_resource::{
3        BindGroupEntries, PipelineCache, SpecializedRenderPipelines, SurfaceTexture, TextureView,
4    },
5    renderer::{RenderAdapter, RenderDevice, RenderInstance},
6    texture::TextureFormatPixelInfo,
7    Extract, ExtractSchedule, Render, RenderApp, RenderSet, WgpuWrapper,
8};
9use bevy_app::{App, Plugin};
10use bevy_ecs::{entity::EntityHashMap, prelude::*};
11#[cfg(target_os = "linux")]
12use bevy_utils::warn_once;
13use bevy_utils::{default, tracing::debug, HashSet};
14use bevy_window::{
15    CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosing,
16};
17use std::{
18    num::NonZeroU32,
19    ops::{Deref, DerefMut},
20    sync::PoisonError,
21};
22use wgpu::{
23    BufferUsages, SurfaceConfiguration, SurfaceTargetUnsafe, TextureFormat, TextureUsages,
24    TextureViewDescriptor,
25};
26
27pub mod screenshot;
28
29use screenshot::{
30    ScreenshotManager, ScreenshotPlugin, ScreenshotPreparedState, ScreenshotToScreenPipeline,
31};
32
33use super::Msaa;
34
35pub struct WindowRenderPlugin;
36
37impl Plugin for WindowRenderPlugin {
38    fn build(&self, app: &mut App) {
39        app.add_plugins(ScreenshotPlugin);
40
41        if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
42            render_app
43                .init_resource::<ExtractedWindows>()
44                .init_resource::<WindowSurfaces>()
45                .add_systems(ExtractSchedule, extract_windows)
46                .add_systems(
47                    Render,
48                    create_surfaces
49                        .run_if(need_surface_configuration)
50                        .before(prepare_windows),
51                )
52                .add_systems(Render, prepare_windows.in_set(RenderSet::ManageViews));
53        }
54    }
55
56    fn finish(&self, app: &mut App) {
57        if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
58            render_app.init_resource::<ScreenshotToScreenPipeline>();
59        }
60    }
61}
62
63pub struct ExtractedWindow {
64    /// An entity that contains the components in [`Window`].
65    pub entity: Entity,
66    pub handle: RawHandleWrapper,
67    pub physical_width: u32,
68    pub physical_height: u32,
69    pub present_mode: PresentMode,
70    pub desired_maximum_frame_latency: Option<NonZeroU32>,
71    /// Note: this will not always be the swap chain texture view. When taking a screenshot,
72    /// this will point to an alternative texture instead to allow for copying the render result
73    /// to CPU memory.
74    pub swap_chain_texture_view: Option<TextureView>,
75    pub swap_chain_texture: Option<SurfaceTexture>,
76    pub swap_chain_texture_format: Option<TextureFormat>,
77    pub screenshot_memory: Option<ScreenshotPreparedState>,
78    pub size_changed: bool,
79    pub present_mode_changed: bool,
80    pub alpha_mode: CompositeAlphaMode,
81    pub screenshot_func: Option<screenshot::ScreenshotFn>,
82}
83
84impl ExtractedWindow {
85    fn set_swapchain_texture(&mut self, frame: wgpu::SurfaceTexture) {
86        let texture_view_descriptor = TextureViewDescriptor {
87            format: Some(frame.texture.format().add_srgb_suffix()),
88            ..default()
89        };
90        self.swap_chain_texture_view = Some(TextureView::from(
91            frame.texture.create_view(&texture_view_descriptor),
92        ));
93        self.swap_chain_texture = Some(SurfaceTexture::from(frame));
94    }
95}
96
97#[derive(Default, Resource)]
98pub struct ExtractedWindows {
99    pub primary: Option<Entity>,
100    pub windows: EntityHashMap<ExtractedWindow>,
101}
102
103impl Deref for ExtractedWindows {
104    type Target = EntityHashMap<ExtractedWindow>;
105
106    fn deref(&self) -> &Self::Target {
107        &self.windows
108    }
109}
110
111impl DerefMut for ExtractedWindows {
112    fn deref_mut(&mut self) -> &mut Self::Target {
113        &mut self.windows
114    }
115}
116
117fn extract_windows(
118    mut extracted_windows: ResMut<ExtractedWindows>,
119    screenshot_manager: Extract<Res<ScreenshotManager>>,
120    mut closing: Extract<EventReader<WindowClosing>>,
121    windows: Extract<Query<(Entity, &Window, &RawHandleWrapper, Option<&PrimaryWindow>)>>,
122    mut removed: Extract<RemovedComponents<RawHandleWrapper>>,
123    mut window_surfaces: ResMut<WindowSurfaces>,
124) {
125    for (entity, window, handle, primary) in windows.iter() {
126        if primary.is_some() {
127            extracted_windows.primary = Some(entity);
128        }
129
130        let (new_width, new_height) = (
131            window.resolution.physical_width().max(1),
132            window.resolution.physical_height().max(1),
133        );
134
135        let extracted_window = extracted_windows.entry(entity).or_insert(ExtractedWindow {
136            entity,
137            handle: handle.clone(),
138            physical_width: new_width,
139            physical_height: new_height,
140            present_mode: window.present_mode,
141            desired_maximum_frame_latency: window.desired_maximum_frame_latency,
142            swap_chain_texture: None,
143            swap_chain_texture_view: None,
144            size_changed: false,
145            swap_chain_texture_format: None,
146            present_mode_changed: false,
147            alpha_mode: window.composite_alpha_mode,
148            screenshot_func: None,
149            screenshot_memory: None,
150        });
151
152        // NOTE: Drop the swap chain frame here
153        extracted_window.swap_chain_texture_view = None;
154        extracted_window.size_changed = new_width != extracted_window.physical_width
155            || new_height != extracted_window.physical_height;
156        extracted_window.present_mode_changed =
157            window.present_mode != extracted_window.present_mode;
158
159        if extracted_window.size_changed {
160            debug!(
161                "Window size changed from {}x{} to {}x{}",
162                extracted_window.physical_width,
163                extracted_window.physical_height,
164                new_width,
165                new_height
166            );
167            extracted_window.physical_width = new_width;
168            extracted_window.physical_height = new_height;
169        }
170
171        if extracted_window.present_mode_changed {
172            debug!(
173                "Window Present Mode changed from {:?} to {:?}",
174                extracted_window.present_mode, window.present_mode
175            );
176            extracted_window.present_mode = window.present_mode;
177        }
178    }
179
180    for closing_window in closing.read() {
181        extracted_windows.remove(&closing_window.window);
182        window_surfaces.remove(&closing_window.window);
183    }
184    for removed_window in removed.read() {
185        extracted_windows.remove(&removed_window);
186        window_surfaces.remove(&removed_window);
187    }
188    // This lock will never block because `callbacks` is `pub(crate)` and this is the singular callsite where it's locked.
189    // Even if a user had multiple copies of this system, since the system has a mutable resource access the two systems would never run
190    // at the same time
191    // TODO: since this is guaranteed, should the lock be replaced with an UnsafeCell to remove the overhead, or is it minor enough to be ignored?
192    for (window, screenshot_func) in screenshot_manager
193        .callbacks
194        .lock()
195        .unwrap_or_else(PoisonError::into_inner)
196        .drain()
197    {
198        if let Some(window) = extracted_windows.get_mut(&window) {
199            window.screenshot_func = Some(screenshot_func);
200        }
201    }
202}
203
204struct SurfaceData {
205    // TODO: what lifetime should this be?
206    surface: WgpuWrapper<wgpu::Surface<'static>>,
207    configuration: SurfaceConfiguration,
208}
209
210#[derive(Resource, Default)]
211pub struct WindowSurfaces {
212    surfaces: EntityHashMap<SurfaceData>,
213    /// List of windows that we have already called the initial `configure_surface` for
214    configured_windows: HashSet<Entity>,
215}
216
217impl WindowSurfaces {
218    fn remove(&mut self, window: &Entity) {
219        self.surfaces.remove(window);
220        self.configured_windows.remove(window);
221    }
222}
223
224#[cfg(target_os = "linux")]
225const NVIDIA_VENDOR_ID: u32 = 0x10DE;
226
227/// (re)configures window surfaces, and obtains a swapchain texture for rendering.
228///
229/// NOTE: `get_current_texture` in `prepare_windows` can take a long time if the GPU workload is
230/// the performance bottleneck. This can be seen in profiles as multiple prepare-set systems all
231/// taking an unusually long time to complete, and all finishing at about the same time as the
232/// `prepare_windows` system. Improvements in bevy are planned to avoid this happening when it
233/// should not but it will still happen as it is easy for a user to create a large GPU workload
234/// relative to the GPU performance and/or CPU workload.
235/// This can be caused by many reasons, but several of them are:
236/// - GPU workload is more than your current GPU can manage
237/// - Error / performance bug in your custom shaders
238/// - wgpu was unable to detect a proper GPU hardware-accelerated device given the chosen
239///   [`Backends`](crate::settings::Backends), [`WgpuLimits`](crate::settings::WgpuLimits),
240///   and/or [`WgpuFeatures`](crate::settings::WgpuFeatures). For example, on Windows currently
241///   `DirectX 11` is not supported by wgpu 0.12 and so if your GPU/drivers do not support Vulkan,
242///   it may be that a software renderer called "Microsoft Basic Render Driver" using `DirectX 12`
243///   will be chosen and performance will be very poor. This is visible in a log message that is
244///   output during renderer initialization. Future versions of wgpu will support `DirectX 11`, but
245///   another alternative is to try to use [`ANGLE`](https://github.com/gfx-rs/wgpu#angle) and
246///   [`Backends::GL`](crate::settings::Backends::GL) if your GPU/drivers support `OpenGL 4.3` / `OpenGL ES 3.0` or
247///   later.
248#[allow(clippy::too_many_arguments)]
249pub fn prepare_windows(
250    mut windows: ResMut<ExtractedWindows>,
251    mut window_surfaces: ResMut<WindowSurfaces>,
252    render_device: Res<RenderDevice>,
253    render_adapter: Res<RenderAdapter>,
254    screenshot_pipeline: Res<ScreenshotToScreenPipeline>,
255    pipeline_cache: Res<PipelineCache>,
256    mut pipelines: ResMut<SpecializedRenderPipelines<ScreenshotToScreenPipeline>>,
257    mut msaa: ResMut<Msaa>,
258    #[cfg(target_os = "linux")] render_instance: Res<RenderInstance>,
259) {
260    for window in windows.windows.values_mut() {
261        let window_surfaces = window_surfaces.deref_mut();
262        let Some(surface_data) = window_surfaces.surfaces.get(&window.entity) else {
263            continue;
264        };
265
266        // This is an ugly hack to work around drivers that don't support MSAA.
267        // This should be removed once https://github.com/bevyengine/bevy/issues/7194 lands and we're doing proper
268        // feature detection for MSAA.
269        // When removed, we can also remove the `.after(prepare_windows)` of `prepare_core_3d_depth_textures` and `prepare_prepass_textures`
270        let sample_flags = render_adapter
271            .get_texture_format_features(surface_data.configuration.format)
272            .flags;
273
274        if !sample_flags.sample_count_supported(msaa.samples()) {
275            let fallback = if sample_flags.sample_count_supported(Msaa::default().samples()) {
276                Msaa::default()
277            } else {
278                Msaa::Off
279            };
280
281            let fallback_str = if fallback == Msaa::Off {
282                "disabling MSAA".to_owned()
283            } else {
284                format!("MSAA {}x", fallback.samples())
285            };
286
287            bevy_utils::tracing::warn!(
288                "MSAA {}x is not supported on this device. Falling back to {}.",
289                msaa.samples(),
290                fallback_str,
291            );
292            *msaa = fallback;
293        }
294
295        // A recurring issue is hitting `wgpu::SurfaceError::Timeout` on certain Linux
296        // mesa driver implementations. This seems to be a quirk of some drivers.
297        // We'd rather keep panicking when not on Linux mesa, because in those case,
298        // the `Timeout` is still probably the symptom of a degraded unrecoverable
299        // application state.
300        // see https://github.com/bevyengine/bevy/pull/5957
301        // and https://github.com/gfx-rs/wgpu/issues/1218
302        #[cfg(target_os = "linux")]
303        let may_erroneously_timeout = || {
304            render_instance
305                .enumerate_adapters(wgpu::Backends::VULKAN)
306                .iter()
307                .any(|adapter| {
308                    let name = adapter.get_info().name;
309                    name.starts_with("Radeon")
310                        || name.starts_with("AMD")
311                        || name.starts_with("Intel")
312                })
313        };
314
315        #[cfg(target_os = "linux")]
316        let is_nvidia = || {
317            render_instance
318                .enumerate_adapters(wgpu::Backends::VULKAN)
319                .iter()
320                .any(|adapter| adapter.get_info().vendor & 0xFFFF == NVIDIA_VENDOR_ID)
321        };
322
323        let not_already_configured = window_surfaces.configured_windows.insert(window.entity);
324
325        let surface = &surface_data.surface;
326        if not_already_configured || window.size_changed || window.present_mode_changed {
327            match surface.get_current_texture() {
328                Ok(frame) => window.set_swapchain_texture(frame),
329                #[cfg(target_os = "linux")]
330                Err(wgpu::SurfaceError::Outdated) if is_nvidia() => {
331                    warn_once!(
332                        "Couldn't get swap chain texture. This often happens with \
333                        the NVIDIA drivers on Linux. It can be safely ignored."
334                    );
335                }
336                Err(err) => panic!("Error configuring surface: {err}"),
337            };
338        } else {
339            match surface.get_current_texture() {
340                Ok(frame) => {
341                    window.set_swapchain_texture(frame);
342                }
343                #[cfg(target_os = "linux")]
344                Err(wgpu::SurfaceError::Outdated) if is_nvidia() => {
345                    warn_once!(
346                        "Couldn't get swap chain texture. This often happens with \
347                        the NVIDIA drivers on Linux. It can be safely ignored."
348                    );
349                }
350                Err(wgpu::SurfaceError::Outdated) => {
351                    render_device.configure_surface(surface, &surface_data.configuration);
352                    let frame = surface
353                        .get_current_texture()
354                        .expect("Error reconfiguring surface");
355                    window.set_swapchain_texture(frame);
356                }
357                #[cfg(target_os = "linux")]
358                Err(wgpu::SurfaceError::Timeout) if may_erroneously_timeout() => {
359                    bevy_utils::tracing::trace!(
360                        "Couldn't get swap chain texture. This is probably a quirk \
361                        of your Linux GPU driver, so it can be safely ignored."
362                    );
363                }
364                Err(err) => {
365                    panic!("Couldn't get swap chain texture, operation unrecoverable: {err}");
366                }
367            }
368        };
369        window.swap_chain_texture_format = Some(surface_data.configuration.format);
370
371        if window.screenshot_func.is_some() {
372            let texture = render_device.create_texture(&wgpu::TextureDescriptor {
373                label: Some("screenshot-capture-rendertarget"),
374                size: wgpu::Extent3d {
375                    width: surface_data.configuration.width,
376                    height: surface_data.configuration.height,
377                    depth_or_array_layers: 1,
378                },
379                mip_level_count: 1,
380                sample_count: 1,
381                dimension: wgpu::TextureDimension::D2,
382                format: surface_data.configuration.format.add_srgb_suffix(),
383                usage: TextureUsages::RENDER_ATTACHMENT
384                    | TextureUsages::COPY_SRC
385                    | TextureUsages::TEXTURE_BINDING,
386                view_formats: &[],
387            });
388            let texture_view = texture.create_view(&Default::default());
389            let buffer = render_device.create_buffer(&wgpu::BufferDescriptor {
390                label: Some("screenshot-transfer-buffer"),
391                size: screenshot::get_aligned_size(
392                    window.physical_width,
393                    window.physical_height,
394                    surface_data.configuration.format.pixel_size() as u32,
395                ) as u64,
396                usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
397                mapped_at_creation: false,
398            });
399            let bind_group = render_device.create_bind_group(
400                "screenshot-to-screen-bind-group",
401                &screenshot_pipeline.bind_group_layout,
402                &BindGroupEntries::single(&texture_view),
403            );
404            let pipeline_id = pipelines.specialize(
405                &pipeline_cache,
406                &screenshot_pipeline,
407                surface_data.configuration.format,
408            );
409            window.swap_chain_texture_view = Some(texture_view);
410            window.screenshot_memory = Some(ScreenshotPreparedState {
411                texture,
412                buffer,
413                bind_group,
414                pipeline_id,
415            });
416        }
417    }
418}
419
420pub fn need_surface_configuration(
421    windows: Res<ExtractedWindows>,
422    window_surfaces: Res<WindowSurfaces>,
423) -> bool {
424    for window in windows.windows.values() {
425        if !window_surfaces.configured_windows.contains(&window.entity)
426            || window.size_changed
427            || window.present_mode_changed
428        {
429            return true;
430        }
431    }
432    false
433}
434
435// 2 is wgpu's default/what we've been using so far.
436// 1 is the minimum, but may cause lower framerates due to the cpu waiting for the gpu to finish
437// all work for the previous frame before starting work on the next frame, which then means the gpu
438// has to wait for the cpu to finish to start on the next frame.
439const DEFAULT_DESIRED_MAXIMUM_FRAME_LATENCY: u32 = 2;
440
441/// Creates window surfaces.
442pub fn create_surfaces(
443    // By accessing a NonSend resource, we tell the scheduler to put this system on the main thread,
444    // which is necessary for some OS's
445    #[cfg(any(target_os = "macos", target_os = "ios"))] _marker: Option<
446        NonSend<bevy_core::NonSendMarker>,
447    >,
448    windows: Res<ExtractedWindows>,
449    mut window_surfaces: ResMut<WindowSurfaces>,
450    render_instance: Res<RenderInstance>,
451    render_adapter: Res<RenderAdapter>,
452    render_device: Res<RenderDevice>,
453) {
454    for window in windows.windows.values() {
455        let data = window_surfaces
456            .surfaces
457            .entry(window.entity)
458            .or_insert_with(|| {
459                let surface_target = SurfaceTargetUnsafe::RawHandle {
460                    raw_display_handle: window.handle.display_handle,
461                    raw_window_handle: window.handle.window_handle,
462                };
463                // SAFETY: The window handles in ExtractedWindows will always be valid objects to create surfaces on
464                let surface = unsafe {
465                    // NOTE: On some OSes this MUST be called from the main thread.
466                    // As of wgpu 0.15, only fallible if the given window is a HTML canvas and obtaining a WebGPU or WebGL2 context fails.
467                    render_instance
468                        .create_surface_unsafe(surface_target)
469                        .expect("Failed to create wgpu surface")
470                };
471                let caps = surface.get_capabilities(&render_adapter);
472                let formats = caps.formats;
473                // For future HDR output support, we'll need to request a format that supports HDR,
474                // but as of wgpu 0.15 that is not yet supported.
475                // Prefer sRGB formats for surfaces, but fall back to first available format if no sRGB formats are available.
476                let mut format = *formats.first().expect("No supported formats for surface");
477                for available_format in formats {
478                    // Rgba8UnormSrgb and Bgra8UnormSrgb and the only sRGB formats wgpu exposes that we can use for surfaces.
479                    if available_format == TextureFormat::Rgba8UnormSrgb
480                        || available_format == TextureFormat::Bgra8UnormSrgb
481                    {
482                        format = available_format;
483                        break;
484                    }
485                }
486
487                let configuration = wgpu::SurfaceConfiguration {
488                    format,
489                    width: window.physical_width,
490                    height: window.physical_height,
491                    usage: TextureUsages::RENDER_ATTACHMENT,
492                    present_mode: match window.present_mode {
493                        PresentMode::Fifo => wgpu::PresentMode::Fifo,
494                        PresentMode::FifoRelaxed => wgpu::PresentMode::FifoRelaxed,
495                        PresentMode::Mailbox => wgpu::PresentMode::Mailbox,
496                        PresentMode::Immediate => wgpu::PresentMode::Immediate,
497                        PresentMode::AutoVsync => wgpu::PresentMode::AutoVsync,
498                        PresentMode::AutoNoVsync => wgpu::PresentMode::AutoNoVsync,
499                    },
500                    desired_maximum_frame_latency: window
501                        .desired_maximum_frame_latency
502                        .map(NonZeroU32::get)
503                        .unwrap_or(DEFAULT_DESIRED_MAXIMUM_FRAME_LATENCY),
504                    alpha_mode: match window.alpha_mode {
505                        CompositeAlphaMode::Auto => wgpu::CompositeAlphaMode::Auto,
506                        CompositeAlphaMode::Opaque => wgpu::CompositeAlphaMode::Opaque,
507                        CompositeAlphaMode::PreMultiplied => {
508                            wgpu::CompositeAlphaMode::PreMultiplied
509                        }
510                        CompositeAlphaMode::PostMultiplied => {
511                            wgpu::CompositeAlphaMode::PostMultiplied
512                        }
513                        CompositeAlphaMode::Inherit => wgpu::CompositeAlphaMode::Inherit,
514                    },
515                    view_formats: if !format.is_srgb() {
516                        vec![format.add_srgb_suffix()]
517                    } else {
518                        vec![]
519                    },
520                };
521
522                render_device.configure_surface(&surface, &configuration);
523
524                SurfaceData {
525                    surface: WgpuWrapper::new(surface),
526                    configuration,
527                }
528            });
529
530        if window.size_changed || window.present_mode_changed {
531            data.configuration.width = window.physical_width;
532            data.configuration.height = window.physical_height;
533            data.configuration.present_mode = match window.present_mode {
534                PresentMode::Fifo => wgpu::PresentMode::Fifo,
535                PresentMode::FifoRelaxed => wgpu::PresentMode::FifoRelaxed,
536                PresentMode::Mailbox => wgpu::PresentMode::Mailbox,
537                PresentMode::Immediate => wgpu::PresentMode::Immediate,
538                PresentMode::AutoVsync => wgpu::PresentMode::AutoVsync,
539                PresentMode::AutoNoVsync => wgpu::PresentMode::AutoNoVsync,
540            };
541            render_device.configure_surface(&data.surface, &data.configuration);
542        }
543    }
544}