Path: blob/main/crates/bevy_render/src/view/window/mod.rs
9383 views
use crate::renderer::WgpuWrapper;1use crate::{2render_resource::{SurfaceTexture, TextureView},3renderer::{RenderAdapter, RenderDevice, RenderInstance},4Extract, ExtractSchedule, Render, RenderApp, RenderSystems,5};6use bevy_app::{App, Plugin};7use bevy_ecs::{entity::EntityHashMap, prelude::*};8use bevy_log::{debug, info, warn};9use bevy_platform::collections::HashSet;10use bevy_utils::default;11use bevy_window::{12CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosing,13};14use core::{15num::NonZero,16ops::{Deref, DerefMut},17};18use wgpu::{19SurfaceConfiguration, SurfaceTargetUnsafe, TextureFormat, TextureUsages, TextureViewDescriptor,20};2122pub mod screenshot;2324use screenshot::ScreenshotPlugin;2526pub struct WindowRenderPlugin;2728impl Plugin for WindowRenderPlugin {29fn build(&self, app: &mut App) {30app.add_plugins(ScreenshotPlugin);3132if let Some(render_app) = app.get_sub_app_mut(RenderApp) {33render_app34.init_resource::<ExtractedWindows>()35.init_resource::<WindowSurfaces>()36.add_systems(ExtractSchedule, extract_windows)37.add_systems(38Render,39create_surfaces40.run_if(need_surface_configuration)41.before(prepare_windows),42)43.add_systems(Render, prepare_windows.in_set(RenderSystems::ManageViews));44}45}46}4748pub struct ExtractedWindow {49/// An entity that contains the components in [`Window`].50pub entity: Entity,51pub handle: RawHandleWrapper,52pub physical_width: u32,53pub physical_height: u32,54pub present_mode: PresentMode,55pub desired_maximum_frame_latency: Option<NonZero<u32>>,56/// Note: this will not always be the swap chain texture view. When taking a screenshot,57/// this will point to an alternative texture instead to allow for copying the render result58/// to CPU memory.59pub swap_chain_texture_view: Option<TextureView>,60pub swap_chain_texture: Option<SurfaceTexture>,61pub swap_chain_texture_format: Option<TextureFormat>,62pub swap_chain_texture_view_format: Option<TextureFormat>,63pub size_changed: bool,64pub present_mode_changed: bool,65pub alpha_mode: CompositeAlphaMode,66/// Whether this window needs an initial buffer commit.67///68/// On Wayland, windows must present at least once before they are shown.69/// See <https://wayland.app/protocols/xdg-shell#xdg_surface>70pub needs_initial_present: bool,71}7273impl ExtractedWindow {74fn set_swapchain_texture(&mut self, frame: wgpu::SurfaceTexture) {75self.swap_chain_texture_view_format = Some(frame.texture.format().add_srgb_suffix());76let texture_view_descriptor = TextureViewDescriptor {77format: self.swap_chain_texture_view_format,78..default()79};80self.swap_chain_texture_view = Some(TextureView::from(81frame.texture.create_view(&texture_view_descriptor),82));83self.swap_chain_texture = Some(SurfaceTexture::from(frame));84}8586fn has_swapchain_texture(&self) -> bool {87self.swap_chain_texture_view.is_some() && self.swap_chain_texture.is_some()88}8990pub fn present(&mut self) {91if let Some(surface_texture) = self.swap_chain_texture.take() {92// TODO(clean): winit docs recommends calling pre_present_notify before this.93// though `present()` doesn't present the frame, it schedules it to be presented94// by wgpu.95// https://docs.rs/winit/0.29.9/wasm32-unknown-unknown/winit/window/struct.Window.html#method.pre_present_notify96surface_texture.present();97}98}99}100101#[derive(Default, Resource)]102pub struct ExtractedWindows {103pub primary: Option<Entity>,104pub windows: EntityHashMap<ExtractedWindow>,105}106107impl Deref for ExtractedWindows {108type Target = EntityHashMap<ExtractedWindow>;109110fn deref(&self) -> &Self::Target {111&self.windows112}113}114115impl DerefMut for ExtractedWindows {116fn deref_mut(&mut self) -> &mut Self::Target {117&mut self.windows118}119}120121fn extract_windows(122mut extracted_windows: ResMut<ExtractedWindows>,123mut closing: Extract<MessageReader<WindowClosing>>,124windows: Extract<Query<(Entity, &Window, &RawHandleWrapper, Option<&PrimaryWindow>)>>,125mut removed: Extract<RemovedComponents<RawHandleWrapper>>,126mut window_surfaces: ResMut<WindowSurfaces>,127) {128for (entity, window, handle, primary) in windows.iter() {129if primary.is_some() {130extracted_windows.primary = Some(entity);131}132133let (new_width, new_height) = (134window.resolution.physical_width().max(1),135window.resolution.physical_height().max(1),136);137138let extracted_window = extracted_windows.entry(entity).or_insert(ExtractedWindow {139entity,140handle: handle.clone(),141physical_width: new_width,142physical_height: new_height,143present_mode: window.present_mode,144desired_maximum_frame_latency: window.desired_maximum_frame_latency,145swap_chain_texture: None,146swap_chain_texture_view: None,147size_changed: false,148swap_chain_texture_format: None,149swap_chain_texture_view_format: None,150present_mode_changed: false,151alpha_mode: window.composite_alpha_mode,152needs_initial_present: true,153});154155if extracted_window.swap_chain_texture.is_none() {156// If we called present on the previous swap-chain texture last update,157// then drop the swap chain frame here, otherwise we can keep it for the158// next update as an optimization. `prepare_windows` will only acquire a new159// swap chain texture if needed.160extracted_window.swap_chain_texture_view = None;161}162extracted_window.size_changed = new_width != extracted_window.physical_width163|| new_height != extracted_window.physical_height;164extracted_window.present_mode_changed =165window.present_mode != extracted_window.present_mode;166167if extracted_window.size_changed {168debug!(169"Window size changed from {}x{} to {}x{}",170extracted_window.physical_width,171extracted_window.physical_height,172new_width,173new_height174);175extracted_window.physical_width = new_width;176extracted_window.physical_height = new_height;177}178179if extracted_window.present_mode_changed {180debug!(181"Window Present Mode changed from {:?} to {:?}",182extracted_window.present_mode, window.present_mode183);184extracted_window.present_mode = window.present_mode;185}186}187188for closing_window in closing.read() {189extracted_windows.remove(&closing_window.window);190window_surfaces.remove(&closing_window.window);191}192for removed_window in removed.read() {193extracted_windows.remove(&removed_window);194window_surfaces.remove(&removed_window);195}196}197198struct SurfaceData {199// TODO: what lifetime should this be?200surface: WgpuWrapper<wgpu::Surface<'static>>,201configuration: SurfaceConfiguration,202texture_view_format: Option<TextureFormat>,203}204205#[derive(Resource, Default)]206pub struct WindowSurfaces {207surfaces: EntityHashMap<SurfaceData>,208/// List of windows that we have already called the initial `configure_surface` for209configured_windows: HashSet<Entity>,210}211212impl WindowSurfaces {213fn remove(&mut self, window: &Entity) {214self.surfaces.remove(window);215self.configured_windows.remove(window);216}217}218219/// (re)configures window surfaces, and obtains a swapchain texture for rendering.220///221/// NOTE: `get_current_texture` in `prepare_windows` can take a long time if the GPU workload is222/// the performance bottleneck. This can be seen in profiles as multiple prepare-set systems all223/// taking an unusually long time to complete, and all finishing at about the same time as the224/// `prepare_windows` system. Improvements in bevy are planned to avoid this happening when it225/// should not but it will still happen as it is easy for a user to create a large GPU workload226/// relative to the GPU performance and/or CPU workload.227/// This can be caused by many reasons, but several of them are:228/// - GPU workload is more than your current GPU can manage229/// - Error / performance bug in your custom shaders230/// - wgpu was unable to detect a proper GPU hardware-accelerated device given the chosen231/// [`Backends`](crate::settings::Backends), [`WgpuLimits`](crate::settings::WgpuLimits),232/// and/or [`WgpuFeatures`](crate::settings::WgpuFeatures). For example, on Windows currently233/// `DirectX 11` is not supported by wgpu 0.12 and so if your GPU/drivers do not support Vulkan,234/// it may be that a software renderer called "Microsoft Basic Render Driver" using `DirectX 12`235/// will be chosen and performance will be very poor. This is visible in a log message that is236/// output during renderer initialization.237/// Another alternative is to try to use [`ANGLE`](https://github.com/gfx-rs/wgpu#angle) and238/// [`Backends::GL`](crate::settings::Backends::GL) with the `gles` feature enabled if your239/// GPU/drivers support `OpenGL 4.3` / `OpenGL ES 3.0` or later.240pub fn prepare_windows(241mut windows: ResMut<ExtractedWindows>,242mut window_surfaces: ResMut<WindowSurfaces>,243render_device: Res<RenderDevice>,244#[cfg(target_os = "linux")] render_instance: Res<RenderInstance>,245) {246for window in windows.windows.values_mut() {247let window_surfaces = window_surfaces.deref_mut();248let Some(surface_data) = window_surfaces.surfaces.get(&window.entity) else {249continue;250};251252// We didn't present the previous frame, so we can keep using our existing swapchain texture.253if window.has_swapchain_texture() && !window.size_changed && !window.present_mode_changed {254continue;255}256257// A recurring issue is hitting `wgpu::SurfaceError::Timeout` on certain Linux258// mesa driver implementations. This seems to be a quirk of some drivers.259// We'd rather keep panicking when not on Linux mesa, because in those case,260// the `Timeout` is still probably the symptom of a degraded unrecoverable261// application state.262// see https://github.com/bevyengine/bevy/pull/5957263// and https://github.com/gfx-rs/wgpu/issues/1218264#[cfg(target_os = "linux")]265let may_erroneously_timeout = || {266bevy_tasks::IoTaskPool::get().scope(|scope| {267scope.spawn(async {268render_instance269.enumerate_adapters(wgpu::Backends::VULKAN)270.await271.iter()272.any(|adapter| {273let name = adapter.get_info().name;274name.starts_with("Radeon")275|| name.starts_with("AMD")276|| name.starts_with("Intel")277})278});279})[0]280};281282let surface = &surface_data.surface;283match surface.get_current_texture() {284Ok(frame) => {285window.set_swapchain_texture(frame);286}287Err(wgpu::SurfaceError::Outdated) => {288render_device.configure_surface(surface, &surface_data.configuration);289let frame = match surface.get_current_texture() {290Ok(frame) => frame,291Err(err) => {292// This is a common occurrence on X11 and Xwayland with NVIDIA drivers293// when opening and resizing the window.294warn!("Couldn't get swap chain texture after configuring. Cause: '{err}'");295continue;296}297};298window.set_swapchain_texture(frame);299}300#[cfg(target_os = "linux")]301Err(wgpu::SurfaceError::Timeout) if may_erroneously_timeout() => {302bevy_log::trace!(303"Couldn't get swap chain texture. This is probably a quirk \304of your Linux GPU driver, so it can be safely ignored."305);306}307Err(err) => {308panic!("Couldn't get swap chain texture, operation unrecoverable: {err}");309}310}311window.swap_chain_texture_format = Some(surface_data.configuration.format);312}313}314315pub fn need_surface_configuration(316windows: Res<ExtractedWindows>,317window_surfaces: Res<WindowSurfaces>,318) -> bool {319for window in windows.windows.values() {320if !window_surfaces.configured_windows.contains(&window.entity)321|| window.size_changed322|| window.present_mode_changed323{324return true;325}326}327false328}329330// 2 is wgpu's default/what we've been using so far.331// 1 is the minimum, but may cause lower framerates due to the cpu waiting for the gpu to finish332// all work for the previous frame before starting work on the next frame, which then means the gpu333// has to wait for the cpu to finish to start on the next frame.334const DEFAULT_DESIRED_MAXIMUM_FRAME_LATENCY: u32 = 2;335336/// Creates window surfaces.337pub fn create_surfaces(338// By accessing a NonSend resource, we tell the scheduler to put this system on the main thread,339// which is necessary for some OS's340#[cfg(any(target_os = "macos", target_os = "ios"))] _marker: bevy_ecs::system::NonSendMarker,341mut windows: ResMut<ExtractedWindows>,342mut window_surfaces: ResMut<WindowSurfaces>,343render_instance: Res<RenderInstance>,344render_adapter: Res<RenderAdapter>,345render_device: Res<RenderDevice>,346) {347for window in windows.windows.values_mut() {348let data = window_surfaces349.surfaces350.entry(window.entity)351.or_insert_with(|| {352let surface_target = SurfaceTargetUnsafe::RawHandle {353raw_display_handle: window.handle.get_display_handle(),354raw_window_handle: window.handle.get_window_handle(),355};356// SAFETY: The window handles in ExtractedWindows will always be valid objects to create surfaces on357let surface = unsafe {358// NOTE: On some OSes this MUST be called from the main thread.359// As of wgpu 0.15, only fallible if the given window is a HTML canvas and obtaining a WebGPU or WebGL2 context fails.360render_instance361.create_surface_unsafe(surface_target)362.expect("Failed to create wgpu surface")363};364let caps = surface.get_capabilities(&render_adapter);365let present_mode = present_mode(window, &caps);366let formats = caps.formats;367// For future HDR output support, we'll need to request a format that supports HDR,368// but as of wgpu 0.15 that is not yet supported.369// Prefer sRGB formats for surfaces, but fall back to first available format if no sRGB formats are available.370let mut format = *formats.first().expect("No supported formats for surface");371for available_format in formats {372// Rgba8UnormSrgb and Bgra8UnormSrgb and the only sRGB formats wgpu exposes that we can use for surfaces.373if available_format == TextureFormat::Rgba8UnormSrgb374|| available_format == TextureFormat::Bgra8UnormSrgb375{376format = available_format;377break;378}379}380381let texture_view_format = if !format.is_srgb() {382Some(format.add_srgb_suffix())383} else {384None385};386let configuration = SurfaceConfiguration {387format,388width: window.physical_width,389height: window.physical_height,390usage: TextureUsages::RENDER_ATTACHMENT,391present_mode,392desired_maximum_frame_latency: window393.desired_maximum_frame_latency394.map(NonZero::<u32>::get)395.unwrap_or(DEFAULT_DESIRED_MAXIMUM_FRAME_LATENCY),396alpha_mode: match window.alpha_mode {397CompositeAlphaMode::Auto => wgpu::CompositeAlphaMode::Auto,398CompositeAlphaMode::Opaque => wgpu::CompositeAlphaMode::Opaque,399CompositeAlphaMode::PreMultiplied => {400wgpu::CompositeAlphaMode::PreMultiplied401}402CompositeAlphaMode::PostMultiplied => {403wgpu::CompositeAlphaMode::PostMultiplied404}405CompositeAlphaMode::Inherit => wgpu::CompositeAlphaMode::Inherit,406},407view_formats: match texture_view_format {408Some(format) => vec![format],409None => vec![],410},411};412413render_device.configure_surface(&surface, &configuration);414415SurfaceData {416surface: WgpuWrapper::new(surface),417configuration,418texture_view_format,419}420});421422if window.size_changed || window.present_mode_changed {423// normally this is dropped on present but we double check here to be safe as failure to424// drop it will cause validation errors in wgpu425drop(window.swap_chain_texture.take());426#[cfg_attr(427target_arch = "wasm32",428expect(clippy::drop_non_drop, reason = "texture views are not drop on wasm")429)]430drop(window.swap_chain_texture_view.take());431432data.configuration.width = window.physical_width;433data.configuration.height = window.physical_height;434let caps = data.surface.get_capabilities(&render_adapter);435data.configuration.present_mode = present_mode(window, &caps);436render_device.configure_surface(&data.surface, &data.configuration);437}438439window_surfaces.configured_windows.insert(window.entity);440}441}442443fn present_mode(444window: &mut ExtractedWindow,445caps: &wgpu::SurfaceCapabilities,446) -> wgpu::PresentMode {447let present_mode = match window.present_mode {448PresentMode::Fifo => wgpu::PresentMode::Fifo,449PresentMode::FifoRelaxed => wgpu::PresentMode::FifoRelaxed,450PresentMode::Mailbox => wgpu::PresentMode::Mailbox,451PresentMode::Immediate => wgpu::PresentMode::Immediate,452PresentMode::AutoVsync => wgpu::PresentMode::AutoVsync,453PresentMode::AutoNoVsync => wgpu::PresentMode::AutoNoVsync,454};455let fallbacks = match present_mode {456wgpu::PresentMode::AutoVsync => {457&[wgpu::PresentMode::FifoRelaxed, wgpu::PresentMode::Fifo][..]458}459wgpu::PresentMode::AutoNoVsync => &[460wgpu::PresentMode::Immediate,461wgpu::PresentMode::Mailbox,462wgpu::PresentMode::Fifo,463][..],464wgpu::PresentMode::Mailbox => &[465wgpu::PresentMode::Mailbox,466wgpu::PresentMode::Immediate,467wgpu::PresentMode::Fifo,468][..],469// Always end in FIFO to make sure it's always supported470x => &[x, wgpu::PresentMode::Fifo][..],471};472let new_present_mode = fallbacks473.iter()474.copied()475.find(|fallback| caps.present_modes.contains(fallback))476.unwrap_or_else(|| {477unreachable!(478"Fallback system failed to choose present mode. \479This is a bug. Mode: {:?}, Options: {:?}",480window.present_mode, &caps.present_modes481);482});483if new_present_mode != present_mode && fallbacks.contains(&present_mode) {484info!("PresentMode {present_mode:?} requested but not available. Falling back to {new_present_mode:?}");485}486new_present_mode487}488489490