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 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 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 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 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 surface: WgpuWrapper<wgpu::Surface<'static>>,
207 configuration: SurfaceConfiguration,
208}
209
210#[derive(Resource, Default)]
211pub struct WindowSurfaces {
212 surfaces: EntityHashMap<SurfaceData>,
213 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#[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 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 #[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
435const DEFAULT_DESIRED_MAXIMUM_FRAME_LATENCY: u32 = 2;
440
441pub fn create_surfaces(
443 #[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 let surface = unsafe {
465 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 let mut format = *formats.first().expect("No supported formats for surface");
477 for available_format in formats {
478 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}