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 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 pub swap_chain_texture_view: Option<TextureView>,
62 pub swap_chain_texture: Option<SurfaceTexture>,
63 pub swap_chain_texture_format: Option<TextureFormat>,
64 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 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 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 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 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 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
223pub 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 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 if window.has_swapchain_texture() && !window.size_changed && !window.present_mode_changed {
274 continue;
275 }
276
277 #[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 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
355const DEFAULT_DESIRED_MAXIMUM_FRAME_LATENCY: u32 = 2;
360
361pub fn create_surfaces(
363 #[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 let surface = unsafe {
383 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 let mut format = *formats.first().expect("No supported formats for surface");
396 for available_format in formats {
397 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 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 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}