1use crate::renderer::wgpu_wrapper;
2use crate::sync_world::{MainEntity, RenderEntity, SyncToRenderWorld};
3use crate::{camera::extract_cameras, renderer::RenderQueue};
4use crate::{
5 render_resource::{SurfaceTexture, TextureView},
6 renderer::{RenderAdapter, RenderDevice, RenderInstance},
7 Extract, ExtractSchedule, Render, RenderApp, RenderSystems,
8};
9use bevy_app::{App, Plugin};
10use bevy_ecs::prelude::*;
11use bevy_ecs::system::RunSystemOnce;
12use bevy_log::{debug, info, warn};
13use bevy_utils::default;
14use bevy_window::{
15 CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosing,
16};
17use core::num::NonZero;
18use wgpu::{
19 SurfaceConfiguration, SurfaceTargetUnsafe, TextureFormat, TextureUsages, TextureViewDescriptor,
20};
21
22pub mod screenshot;
23
24use screenshot::ScreenshotPlugin;
25
26pub struct WindowRenderPlugin;
27
28impl Plugin for WindowRenderPlugin {
29 fn build(&self, app: &mut App) {
30 app.add_plugins(ScreenshotPlugin);
31
32 {
36 app.add_observer(|trigger: On<Add<Window>>, mut commands: Commands| {
37 commands
38 .entity(trigger.entity)
39 .insert(SyncToRenderWorld::default());
40 });
41
42 let _ = app.world_mut().run_system_once(
44 |mut commands: Commands, windows: Query<Entity, With<Window>>| {
45 for entity in &windows {
46 commands.entity(entity).insert(SyncToRenderWorld::default());
47 }
48 },
49 );
50 }
51
52 if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
53 render_app
54 .add_systems(ExtractSchedule, extract_windows.before(extract_cameras))
55 .add_systems(
56 Render,
57 create_surfaces
58 .run_if(need_surface_configuration)
59 .before(prepare_windows),
60 )
61 .add_systems(Render, prepare_windows.in_set(RenderSystems::PrepareViews));
62 }
63 }
64}
65
66#[derive(Component)]
67pub struct ExtractedWindow {
68 pub physical_width: u32,
69 pub physical_height: u32,
70 pub present_mode: PresentMode,
71 pub desired_maximum_frame_latency: Option<NonZero<u32>>,
72 pub swap_chain_texture_view: Option<TextureView>,
76 pub swap_chain_texture: Option<SurfaceTexture>,
77 pub swap_chain_texture_format: Option<TextureFormat>,
78 pub swap_chain_texture_view_format: Option<TextureFormat>,
81 pub size_changed: bool,
82 pub present_mode_changed: bool,
83 pub alpha_mode: CompositeAlphaMode,
84 pub needs_initial_present: bool,
89}
90
91impl ExtractedWindow {
92 fn set_swapchain_texture(&mut self, frame: wgpu::SurfaceTexture) {
93 self.swap_chain_texture_view_format = Some(frame.texture.format().add_srgb_suffix());
94 let texture_view_descriptor = TextureViewDescriptor {
95 format: self.swap_chain_texture_view_format,
96 ..default()
97 };
98 self.swap_chain_texture_view = Some(TextureView::from(
99 frame.texture.create_view(&texture_view_descriptor),
100 ));
101 self.swap_chain_texture = Some(SurfaceTexture::from(frame));
102 }
103
104 fn has_swapchain_texture(&self) -> bool {
105 self.swap_chain_texture_view.is_some() && self.swap_chain_texture.is_some()
106 }
107
108 pub fn present(&mut self, queue: &RenderQueue) {
109 if let Some(surface_texture) = self.swap_chain_texture.take() {
110 surface_texture.present(queue);
115 }
116 }
117}
118
119fn extract_windows(
120 mut commands: Commands,
121 mut extracted_windows: Query<&mut ExtractedWindow>,
122 mut closing: Extract<MessageReader<WindowClosing>>,
123 windows: Extract<Query<(RenderEntity, &Window, &RawHandleWrapper, Has<PrimaryWindow>)>>,
124 mut removed: Extract<RemovedComponents<RawHandleWrapper>>,
125 mut removed_primary: Extract<RemovedComponents<PrimaryWindow>>,
126 mapper: Extract<Query<&RenderEntity>>,
127) {
128 for (render_entity, window, handle, is_primary) in windows.iter() {
129 if is_primary {
130 commands.entity(render_entity).insert(PrimaryWindow);
131 }
132
133 let (new_width, new_height) = (
134 window.resolution.physical_width().max(1),
135 window.resolution.physical_height().max(1),
136 );
137
138 let Ok(mut extracted_window) = extracted_windows.get_mut(render_entity) else {
139 commands.entity(render_entity).insert((
140 ExtractedWindow {
141 physical_width: new_width,
142 physical_height: new_height,
143 present_mode: window.present_mode,
144 desired_maximum_frame_latency: window.desired_maximum_frame_latency,
145 swap_chain_texture: None,
146 swap_chain_texture_view: None,
147 size_changed: false,
148 swap_chain_texture_format: None,
149 swap_chain_texture_view_format: None,
150 present_mode_changed: false,
151 alpha_mode: window.composite_alpha_mode,
152 needs_initial_present: true,
153 },
154 handle.clone(),
155 ));
156 continue;
157 };
158
159 if extracted_window.swap_chain_texture.is_none() {
160 extracted_window.swap_chain_texture_view = None;
165 }
166
167 extracted_window.size_changed = new_width != extracted_window.physical_width
168 || new_height != extracted_window.physical_height;
169 extracted_window.present_mode_changed =
170 window.present_mode != extracted_window.present_mode;
171
172 if extracted_window.size_changed {
173 debug!(
174 "Window size changed from {}x{} to {}x{}",
175 extracted_window.physical_width,
176 extracted_window.physical_height,
177 new_width,
178 new_height
179 );
180 extracted_window.physical_width = new_width;
181 extracted_window.physical_height = new_height;
182 }
183
184 if extracted_window.present_mode_changed {
185 debug!(
186 "Window Present Mode changed from {:?} to {:?}",
187 extracted_window.present_mode, window.present_mode
188 );
189 extracted_window.present_mode = window.present_mode;
190 }
191 }
192
193 for closing_window in closing.read() {
205 if let Ok(render_entity) = mapper.get(closing_window.window) {
206 commands
207 .entity(render_entity.entity())
208 .remove::<(ExtractedWindow, RawHandleWrapper, SurfaceData)>();
209 }
210 }
211 for removed_window in removed.read() {
212 if let Ok(render_entity) = mapper.get(removed_window) {
213 commands
214 .entity(render_entity.entity())
215 .remove::<(ExtractedWindow, RawHandleWrapper, SurfaceData)>();
216 }
217 }
218 for removed_window in removed_primary.read() {
219 if let Ok(render_entity) = mapper.get(removed_window) {
220 commands
221 .entity(render_entity.entity())
222 .remove::<PrimaryWindow>();
223 }
224 }
225}
226
227wgpu_wrapper!(struct WgpuSurface(wgpu::Surface<'static>));
229
230#[derive(Component)]
231pub struct SurfaceData {
232 surface: WgpuSurface,
233 configuration: SurfaceConfiguration,
234 texture_view_format: Option<TextureFormat>,
235}
236
237pub fn prepare_windows(
259 mut windows: Query<(MainEntity, &mut ExtractedWindow, Option<&SurfaceData>)>,
260 render_device: Res<RenderDevice>,
261 sorted_cameras: Res<crate::camera::SortedCameras>,
262 #[cfg(target_os = "linux")] render_instance: Res<RenderInstance>,
263) {
264 for (main_entity, mut window, maybe_surface_data) in &mut windows {
265 let is_camera_target = sorted_cameras.0.iter().any(|c| {
271 matches!(
272 &c.target,
273 Some(bevy_camera::NormalizedRenderTarget::Window(w)) if w.entity() == main_entity
274 ) && matches!(c.output_mode, bevy_camera::CameraOutputMode::Write { .. })
275 });
276 if !is_camera_target && !window.needs_initial_present {
277 continue;
278 }
279
280 let Some(surface_data) = maybe_surface_data else {
281 continue;
282 };
283
284 if window.has_swapchain_texture() && !window.size_changed && !window.present_mode_changed {
286 continue;
287 }
288
289 #[cfg(target_os = "linux")]
297 let may_erroneously_timeout = || {
298 bevy_tasks::IoTaskPool::get().scope(|scope| {
299 scope.spawn(async {
300 render_instance
301 .enumerate_adapters(wgpu::Backends::VULKAN)
302 .await
303 .iter()
304 .any(|adapter| {
305 let name = adapter.get_info().name;
306 name.starts_with("Radeon")
307 || name.starts_with("AMD")
308 || name.starts_with("Intel")
309 })
310 });
311 })[0]
312 };
313
314 let surface = &surface_data.surface;
315 match surface.get_current_texture() {
316 wgpu::CurrentSurfaceTexture::Success(surface_texture)
317 | wgpu::CurrentSurfaceTexture::Suboptimal(surface_texture) => {
318 window.set_swapchain_texture(surface_texture);
319 }
320 #[cfg(target_os = "linux")]
321 wgpu::CurrentSurfaceTexture::Timeout if may_erroneously_timeout() => {
322 bevy_log::trace!(
323 "Couldn't get swap chain texture. This is probably a quirk \
324 of your Linux GPU driver, so it can be safely ignored."
325 );
326 }
327 wgpu::CurrentSurfaceTexture::Outdated => {
328 render_device.configure_surface(surface, &surface_data.configuration);
329 let frame = match surface.get_current_texture() {
330 wgpu::CurrentSurfaceTexture::Success(surface_texture)
331 | wgpu::CurrentSurfaceTexture::Suboptimal(surface_texture) => surface_texture,
332 variant => {
333 warn!(
336 "Couldn't get swap chain texture after configuring. Cause: '{variant:?}'"
337 );
338 continue;
339 }
340 };
341 window.set_swapchain_texture(frame);
342 }
343 wgpu::CurrentSurfaceTexture::Occluded => {}
344 other => {
345 bevy_log::error!("Couldn't get swap chain texture: {other:?}");
346 }
347 }
348 window.swap_chain_texture_format = Some(surface_data.configuration.format);
349 }
350}
351
352pub fn need_surface_configuration(windows: Query<(&ExtractedWindow, Has<SurfaceData>)>) -> bool {
353 for (window, has_surface_data) in &windows {
354 if !has_surface_data || window.size_changed || window.present_mode_changed {
355 return true;
356 }
357 }
358 false
359}
360
361const DEFAULT_DESIRED_MAXIMUM_FRAME_LATENCY: u32 = 2;
366
367pub fn create_surfaces(
369 mut commands: Commands,
370 #[cfg(any(target_os = "macos", target_os = "ios"))] _marker: bevy_ecs::system::NonSendMarker,
373 mut windows: Query<(
374 Entity,
375 &mut ExtractedWindow,
376 &RawHandleWrapper,
377 Option<&mut SurfaceData>,
378 )>,
379 render_instance: Res<RenderInstance>,
380 render_adapter: Res<RenderAdapter>,
381 render_device: Res<RenderDevice>,
382) {
383 for (entity, mut window, handle, mut maybe_surface_data) in &mut windows {
384 let Some(data) = maybe_surface_data.as_mut() else {
385 let surface_target = SurfaceTargetUnsafe::RawHandle {
386 raw_display_handle: Some(handle.get_display_handle()),
387 raw_window_handle: handle.get_window_handle(),
388 };
389 let surface = unsafe {
391 render_instance
394 .create_surface_unsafe(surface_target)
395 .expect("Failed to create wgpu surface")
396 };
397 let caps = surface.get_capabilities(&render_adapter);
398 let present_mode = present_mode(&window, &caps);
399 let formats = caps.formats;
400 let mut format = *formats.first().expect("No supported formats for surface");
404 for available_format in formats {
405 if available_format == TextureFormat::Rgba8UnormSrgb
407 || available_format == TextureFormat::Bgra8UnormSrgb
408 {
409 format = available_format;
410 break;
411 }
412 }
413
414 let texture_view_format = if !format.is_srgb() {
415 Some(format.add_srgb_suffix())
416 } else {
417 None
418 };
419 let configuration = SurfaceConfiguration {
420 format,
421 width: window.physical_width,
422 height: window.physical_height,
423 usage: TextureUsages::RENDER_ATTACHMENT,
424 present_mode,
425 desired_maximum_frame_latency: window
426 .desired_maximum_frame_latency
427 .map(NonZero::<u32>::get)
428 .unwrap_or(DEFAULT_DESIRED_MAXIMUM_FRAME_LATENCY),
429 alpha_mode: match window.alpha_mode {
430 CompositeAlphaMode::Auto => wgpu::CompositeAlphaMode::Auto,
431 CompositeAlphaMode::Opaque => wgpu::CompositeAlphaMode::Opaque,
432 CompositeAlphaMode::PreMultiplied => wgpu::CompositeAlphaMode::PreMultiplied,
433 CompositeAlphaMode::PostMultiplied => wgpu::CompositeAlphaMode::PostMultiplied,
434 CompositeAlphaMode::Inherit => wgpu::CompositeAlphaMode::Inherit,
435 },
436 view_formats: match texture_view_format {
437 Some(format) => vec![format],
438 None => vec![],
439 },
440 color_space: wgpu::SurfaceColorSpace::Auto,
441 };
442
443 render_device.configure_surface(&surface, &configuration);
444
445 commands.entity(entity).insert(SurfaceData {
446 surface: WgpuSurface::new(surface),
447 configuration,
448 texture_view_format,
449 });
450 continue;
451 };
452
453 if window.size_changed || window.present_mode_changed {
454 drop(window.swap_chain_texture.take());
457 #[cfg_attr(
458 target_arch = "wasm32",
459 expect(clippy::drop_non_drop, reason = "texture views are not drop on wasm")
460 )]
461 drop(window.swap_chain_texture_view.take());
462
463 data.configuration.width = window.physical_width;
464 data.configuration.height = window.physical_height;
465 let caps = data.surface.get_capabilities(&render_adapter);
466 data.configuration.present_mode = present_mode(&window, &caps);
467 render_device.configure_surface(&data.surface, &data.configuration);
468 }
469 }
470}
471
472fn present_mode(window: &ExtractedWindow, caps: &wgpu::SurfaceCapabilities) -> wgpu::PresentMode {
473 let present_mode = match window.present_mode {
474 PresentMode::Fifo => wgpu::PresentMode::Fifo,
475 PresentMode::FifoRelaxed => wgpu::PresentMode::FifoRelaxed,
476 PresentMode::Mailbox => wgpu::PresentMode::Mailbox,
477 PresentMode::Immediate => wgpu::PresentMode::Immediate,
478 PresentMode::AutoVsync => wgpu::PresentMode::AutoVsync,
479 PresentMode::AutoNoVsync => wgpu::PresentMode::AutoNoVsync,
480 };
481 let fallbacks = match present_mode {
482 wgpu::PresentMode::AutoVsync => {
483 &[wgpu::PresentMode::FifoRelaxed, wgpu::PresentMode::Fifo][..]
484 }
485 wgpu::PresentMode::AutoNoVsync => &[
486 wgpu::PresentMode::Immediate,
487 wgpu::PresentMode::Mailbox,
488 wgpu::PresentMode::Fifo,
489 ][..],
490 wgpu::PresentMode::Mailbox => &[
491 wgpu::PresentMode::Mailbox,
492 wgpu::PresentMode::Immediate,
493 wgpu::PresentMode::Fifo,
494 ][..],
495 x => &[x, wgpu::PresentMode::Fifo][..],
497 };
498 let new_present_mode = fallbacks
499 .iter()
500 .copied()
501 .find(|fallback| caps.present_modes.contains(fallback))
502 .unwrap_or_else(|| {
503 unreachable!(
504 "Fallback system failed to choose present mode. \
505 This is a bug. Mode: {:?}, Options: {:?}",
506 window.present_mode, &caps.present_modes
507 );
508 });
509 if new_present_mode != present_mode && fallbacks.contains(&present_mode) {
510 info!("PresentMode {present_mode:?} requested but not available. Falling back to {new_present_mode:?}");
511 }
512 new_present_mode
513}