
# Objective - Fixes #16122 When the wayland feature is not enabled, xwayland is used on wayland. Nvidia drivers are somewhat bugged on linux and return outdated surfaces on xwayland for seemingly no reason. Oftentimes at startup we get into an infine loop where the surface is permanently outdated and nothing (or sometimes only the first frame) is drawn on the screen. ## Solution After experimenting I found that we can safely call configure again and the issue seems to resolve itsef. After this change I couldn't reproduce the original issue after many tries. More testing is probably needed though. The main issue is that `get_current_texture` fails sometimes because the surface remains outdated even after configuring. It would be better to just properly handle and never panic when `get_current_texture` fails. This way we always call configure when outdated and bail when getting the swapchain fails instead of crashing. The number of special cases is also reduced. ## Testing I tested the example "rotation" manually by trying to move around. It works with X11 and Xwayland and the non panicing code paths didn't change so other platforms aren't affected.
412 lines
17 KiB
Rust
412 lines
17 KiB
Rust
use crate::{
|
|
render_resource::{SurfaceTexture, TextureView},
|
|
renderer::{RenderAdapter, RenderDevice, RenderInstance},
|
|
Extract, ExtractSchedule, Render, RenderApp, RenderSet, WgpuWrapper,
|
|
};
|
|
use bevy_app::{App, Plugin};
|
|
use bevy_ecs::{entity::EntityHashMap, prelude::*};
|
|
use bevy_utils::{
|
|
default,
|
|
tracing::{debug, warn},
|
|
HashSet,
|
|
};
|
|
use bevy_window::{
|
|
CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosing,
|
|
};
|
|
use core::{
|
|
num::NonZero,
|
|
ops::{Deref, DerefMut},
|
|
};
|
|
use wgpu::{
|
|
SurfaceConfiguration, SurfaceTargetUnsafe, TextureFormat, TextureUsages, TextureViewDescriptor,
|
|
};
|
|
|
|
pub mod screenshot;
|
|
|
|
use screenshot::{ScreenshotPlugin, ScreenshotToScreenPipeline};
|
|
|
|
pub struct WindowRenderPlugin;
|
|
|
|
impl Plugin for WindowRenderPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.add_plugins(ScreenshotPlugin);
|
|
|
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
|
render_app
|
|
.init_resource::<ExtractedWindows>()
|
|
.init_resource::<WindowSurfaces>()
|
|
.add_systems(ExtractSchedule, extract_windows)
|
|
.add_systems(
|
|
Render,
|
|
create_surfaces
|
|
.run_if(need_surface_configuration)
|
|
.before(prepare_windows),
|
|
)
|
|
.add_systems(Render, prepare_windows.in_set(RenderSet::ManageViews));
|
|
}
|
|
}
|
|
|
|
fn finish(&self, app: &mut App) {
|
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
|
render_app.init_resource::<ScreenshotToScreenPipeline>();
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct ExtractedWindow {
|
|
/// An entity that contains the components in [`Window`].
|
|
pub entity: Entity,
|
|
pub handle: RawHandleWrapper,
|
|
pub physical_width: u32,
|
|
pub physical_height: u32,
|
|
pub present_mode: PresentMode,
|
|
pub desired_maximum_frame_latency: Option<NonZero<u32>>,
|
|
/// Note: this will not always be the swap chain texture view. When taking a screenshot,
|
|
/// this will point to an alternative texture instead to allow for copying the render result
|
|
/// to CPU memory.
|
|
pub swap_chain_texture_view: Option<TextureView>,
|
|
pub swap_chain_texture: Option<SurfaceTexture>,
|
|
pub swap_chain_texture_format: Option<TextureFormat>,
|
|
pub size_changed: bool,
|
|
pub present_mode_changed: bool,
|
|
pub alpha_mode: CompositeAlphaMode,
|
|
}
|
|
|
|
impl ExtractedWindow {
|
|
fn set_swapchain_texture(&mut self, frame: wgpu::SurfaceTexture) {
|
|
let texture_view_descriptor = TextureViewDescriptor {
|
|
format: Some(frame.texture.format().add_srgb_suffix()),
|
|
..default()
|
|
};
|
|
self.swap_chain_texture_view = Some(TextureView::from(
|
|
frame.texture.create_view(&texture_view_descriptor),
|
|
));
|
|
self.swap_chain_texture = Some(SurfaceTexture::from(frame));
|
|
}
|
|
}
|
|
|
|
#[derive(Default, Resource)]
|
|
pub struct ExtractedWindows {
|
|
pub primary: Option<Entity>,
|
|
pub windows: EntityHashMap<ExtractedWindow>,
|
|
}
|
|
|
|
impl Deref for ExtractedWindows {
|
|
type Target = EntityHashMap<ExtractedWindow>;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.windows
|
|
}
|
|
}
|
|
|
|
impl DerefMut for ExtractedWindows {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
&mut self.windows
|
|
}
|
|
}
|
|
|
|
fn extract_windows(
|
|
mut extracted_windows: ResMut<ExtractedWindows>,
|
|
mut closing: Extract<EventReader<WindowClosing>>,
|
|
windows: Extract<Query<(Entity, &Window, &RawHandleWrapper, Option<&PrimaryWindow>)>>,
|
|
mut removed: Extract<RemovedComponents<RawHandleWrapper>>,
|
|
mut window_surfaces: ResMut<WindowSurfaces>,
|
|
) {
|
|
for (entity, window, handle, primary) in windows.iter() {
|
|
if primary.is_some() {
|
|
extracted_windows.primary = Some(entity);
|
|
}
|
|
|
|
let (new_width, new_height) = (
|
|
window.resolution.physical_width().max(1),
|
|
window.resolution.physical_height().max(1),
|
|
);
|
|
|
|
let extracted_window = extracted_windows.entry(entity).or_insert(ExtractedWindow {
|
|
entity,
|
|
handle: handle.clone(),
|
|
physical_width: new_width,
|
|
physical_height: new_height,
|
|
present_mode: window.present_mode,
|
|
desired_maximum_frame_latency: window.desired_maximum_frame_latency,
|
|
swap_chain_texture: None,
|
|
swap_chain_texture_view: None,
|
|
size_changed: false,
|
|
swap_chain_texture_format: None,
|
|
present_mode_changed: false,
|
|
alpha_mode: window.composite_alpha_mode,
|
|
});
|
|
|
|
// NOTE: Drop the swap chain frame here
|
|
extracted_window.swap_chain_texture_view = None;
|
|
extracted_window.size_changed = new_width != extracted_window.physical_width
|
|
|| new_height != extracted_window.physical_height;
|
|
extracted_window.present_mode_changed =
|
|
window.present_mode != extracted_window.present_mode;
|
|
|
|
if extracted_window.size_changed {
|
|
debug!(
|
|
"Window size changed from {}x{} to {}x{}",
|
|
extracted_window.physical_width,
|
|
extracted_window.physical_height,
|
|
new_width,
|
|
new_height
|
|
);
|
|
extracted_window.physical_width = new_width;
|
|
extracted_window.physical_height = new_height;
|
|
}
|
|
|
|
if extracted_window.present_mode_changed {
|
|
debug!(
|
|
"Window Present Mode changed from {:?} to {:?}",
|
|
extracted_window.present_mode, window.present_mode
|
|
);
|
|
extracted_window.present_mode = window.present_mode;
|
|
}
|
|
}
|
|
|
|
for closing_window in closing.read() {
|
|
extracted_windows.remove(&closing_window.window);
|
|
window_surfaces.remove(&closing_window.window);
|
|
}
|
|
for removed_window in removed.read() {
|
|
extracted_windows.remove(&removed_window);
|
|
window_surfaces.remove(&removed_window);
|
|
}
|
|
}
|
|
|
|
struct SurfaceData {
|
|
// TODO: what lifetime should this be?
|
|
surface: WgpuWrapper<wgpu::Surface<'static>>,
|
|
configuration: SurfaceConfiguration,
|
|
}
|
|
|
|
#[derive(Resource, Default)]
|
|
pub struct WindowSurfaces {
|
|
surfaces: EntityHashMap<SurfaceData>,
|
|
/// List of windows that we have already called the initial `configure_surface` for
|
|
configured_windows: HashSet<Entity>,
|
|
}
|
|
|
|
impl WindowSurfaces {
|
|
fn remove(&mut self, window: &Entity) {
|
|
self.surfaces.remove(window);
|
|
self.configured_windows.remove(window);
|
|
}
|
|
}
|
|
|
|
/// (re)configures window surfaces, and obtains a swapchain texture for rendering.
|
|
///
|
|
/// NOTE: `get_current_texture` in `prepare_windows` can take a long time if the GPU workload is
|
|
/// the performance bottleneck. This can be seen in profiles as multiple prepare-set systems all
|
|
/// taking an unusually long time to complete, and all finishing at about the same time as the
|
|
/// `prepare_windows` system. Improvements in bevy are planned to avoid this happening when it
|
|
/// should not but it will still happen as it is easy for a user to create a large GPU workload
|
|
/// relative to the GPU performance and/or CPU workload.
|
|
/// This can be caused by many reasons, but several of them are:
|
|
/// - GPU workload is more than your current GPU can manage
|
|
/// - Error / performance bug in your custom shaders
|
|
/// - wgpu was unable to detect a proper GPU hardware-accelerated device given the chosen
|
|
/// [`Backends`](crate::settings::Backends), [`WgpuLimits`](crate::settings::WgpuLimits),
|
|
/// and/or [`WgpuFeatures`](crate::settings::WgpuFeatures). For example, on Windows currently
|
|
/// `DirectX 11` is not supported by wgpu 0.12 and so if your GPU/drivers do not support Vulkan,
|
|
/// it may be that a software renderer called "Microsoft Basic Render Driver" using `DirectX 12`
|
|
/// will be chosen and performance will be very poor. This is visible in a log message that is
|
|
/// output during renderer initialization. Future versions of wgpu will support `DirectX 11`, but
|
|
/// another alternative is to try to use [`ANGLE`](https://github.com/gfx-rs/wgpu#angle) and
|
|
/// [`Backends::GL`](crate::settings::Backends::GL) if your GPU/drivers support `OpenGL 4.3` / `OpenGL ES 3.0` or
|
|
/// later.
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn prepare_windows(
|
|
mut windows: ResMut<ExtractedWindows>,
|
|
mut window_surfaces: ResMut<WindowSurfaces>,
|
|
render_device: Res<RenderDevice>,
|
|
#[cfg(target_os = "linux")] render_instance: Res<RenderInstance>,
|
|
) {
|
|
for window in windows.windows.values_mut() {
|
|
let window_surfaces = window_surfaces.deref_mut();
|
|
let Some(surface_data) = window_surfaces.surfaces.get(&window.entity) else {
|
|
continue;
|
|
};
|
|
|
|
// A recurring issue is hitting `wgpu::SurfaceError::Timeout` on certain Linux
|
|
// mesa driver implementations. This seems to be a quirk of some drivers.
|
|
// We'd rather keep panicking when not on Linux mesa, because in those case,
|
|
// the `Timeout` is still probably the symptom of a degraded unrecoverable
|
|
// application state.
|
|
// see https://github.com/bevyengine/bevy/pull/5957
|
|
// and https://github.com/gfx-rs/wgpu/issues/1218
|
|
#[cfg(target_os = "linux")]
|
|
let may_erroneously_timeout = || {
|
|
render_instance
|
|
.enumerate_adapters(wgpu::Backends::VULKAN)
|
|
.iter()
|
|
.any(|adapter| {
|
|
let name = adapter.get_info().name;
|
|
name.starts_with("Radeon")
|
|
|| name.starts_with("AMD")
|
|
|| name.starts_with("Intel")
|
|
})
|
|
};
|
|
|
|
let surface = &surface_data.surface;
|
|
match surface.get_current_texture() {
|
|
Ok(frame) => {
|
|
window.set_swapchain_texture(frame);
|
|
}
|
|
Err(wgpu::SurfaceError::Outdated) => {
|
|
render_device.configure_surface(surface, &surface_data.configuration);
|
|
let frame = match surface.get_current_texture() {
|
|
Ok(frame) => frame,
|
|
Err(err) => {
|
|
// This is a common occurrence on X11 and Xwayland with NVIDIA drivers
|
|
// when opening and resizing the window.
|
|
warn!("Couldn't get swap chain texture after configuring. Cause: '{err}'");
|
|
continue;
|
|
}
|
|
};
|
|
window.set_swapchain_texture(frame);
|
|
}
|
|
#[cfg(target_os = "linux")]
|
|
Err(wgpu::SurfaceError::Timeout) if may_erroneously_timeout() => {
|
|
bevy_utils::tracing::trace!(
|
|
"Couldn't get swap chain texture. This is probably a quirk \
|
|
of your Linux GPU driver, so it can be safely ignored."
|
|
);
|
|
}
|
|
Err(err) => {
|
|
panic!("Couldn't get swap chain texture, operation unrecoverable: {err}");
|
|
}
|
|
}
|
|
window.swap_chain_texture_format = Some(surface_data.configuration.format);
|
|
}
|
|
}
|
|
|
|
pub fn need_surface_configuration(
|
|
windows: Res<ExtractedWindows>,
|
|
window_surfaces: Res<WindowSurfaces>,
|
|
) -> bool {
|
|
for window in windows.windows.values() {
|
|
if !window_surfaces.configured_windows.contains(&window.entity)
|
|
|| window.size_changed
|
|
|| window.present_mode_changed
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
// 2 is wgpu's default/what we've been using so far.
|
|
// 1 is the minimum, but may cause lower framerates due to the cpu waiting for the gpu to finish
|
|
// all work for the previous frame before starting work on the next frame, which then means the gpu
|
|
// has to wait for the cpu to finish to start on the next frame.
|
|
const DEFAULT_DESIRED_MAXIMUM_FRAME_LATENCY: u32 = 2;
|
|
|
|
/// Creates window surfaces.
|
|
pub fn create_surfaces(
|
|
// By accessing a NonSend resource, we tell the scheduler to put this system on the main thread,
|
|
// which is necessary for some OS's
|
|
#[cfg(any(target_os = "macos", target_os = "ios"))] _marker: Option<
|
|
NonSend<bevy_core::NonSendMarker>,
|
|
>,
|
|
windows: Res<ExtractedWindows>,
|
|
mut window_surfaces: ResMut<WindowSurfaces>,
|
|
render_instance: Res<RenderInstance>,
|
|
render_adapter: Res<RenderAdapter>,
|
|
render_device: Res<RenderDevice>,
|
|
) {
|
|
for window in windows.windows.values() {
|
|
let data = window_surfaces
|
|
.surfaces
|
|
.entry(window.entity)
|
|
.or_insert_with(|| {
|
|
let surface_target = SurfaceTargetUnsafe::RawHandle {
|
|
raw_display_handle: window.handle.display_handle,
|
|
raw_window_handle: window.handle.window_handle,
|
|
};
|
|
// SAFETY: The window handles in ExtractedWindows will always be valid objects to create surfaces on
|
|
let surface = unsafe {
|
|
// NOTE: On some OSes this MUST be called from the main thread.
|
|
// As of wgpu 0.15, only fallible if the given window is a HTML canvas and obtaining a WebGPU or WebGL2 context fails.
|
|
render_instance
|
|
.create_surface_unsafe(surface_target)
|
|
.expect("Failed to create wgpu surface")
|
|
};
|
|
let caps = surface.get_capabilities(&render_adapter);
|
|
let formats = caps.formats;
|
|
// For future HDR output support, we'll need to request a format that supports HDR,
|
|
// but as of wgpu 0.15 that is not yet supported.
|
|
// Prefer sRGB formats for surfaces, but fall back to first available format if no sRGB formats are available.
|
|
let mut format = *formats.first().expect("No supported formats for surface");
|
|
for available_format in formats {
|
|
// Rgba8UnormSrgb and Bgra8UnormSrgb and the only sRGB formats wgpu exposes that we can use for surfaces.
|
|
if available_format == TextureFormat::Rgba8UnormSrgb
|
|
|| available_format == TextureFormat::Bgra8UnormSrgb
|
|
{
|
|
format = available_format;
|
|
break;
|
|
}
|
|
}
|
|
|
|
let configuration = SurfaceConfiguration {
|
|
format,
|
|
width: window.physical_width,
|
|
height: window.physical_height,
|
|
usage: TextureUsages::RENDER_ATTACHMENT,
|
|
present_mode: match window.present_mode {
|
|
PresentMode::Fifo => wgpu::PresentMode::Fifo,
|
|
PresentMode::FifoRelaxed => wgpu::PresentMode::FifoRelaxed,
|
|
PresentMode::Mailbox => wgpu::PresentMode::Mailbox,
|
|
PresentMode::Immediate => wgpu::PresentMode::Immediate,
|
|
PresentMode::AutoVsync => wgpu::PresentMode::AutoVsync,
|
|
PresentMode::AutoNoVsync => wgpu::PresentMode::AutoNoVsync,
|
|
},
|
|
desired_maximum_frame_latency: window
|
|
.desired_maximum_frame_latency
|
|
.map(NonZero::<u32>::get)
|
|
.unwrap_or(DEFAULT_DESIRED_MAXIMUM_FRAME_LATENCY),
|
|
alpha_mode: match window.alpha_mode {
|
|
CompositeAlphaMode::Auto => wgpu::CompositeAlphaMode::Auto,
|
|
CompositeAlphaMode::Opaque => wgpu::CompositeAlphaMode::Opaque,
|
|
CompositeAlphaMode::PreMultiplied => {
|
|
wgpu::CompositeAlphaMode::PreMultiplied
|
|
}
|
|
CompositeAlphaMode::PostMultiplied => {
|
|
wgpu::CompositeAlphaMode::PostMultiplied
|
|
}
|
|
CompositeAlphaMode::Inherit => wgpu::CompositeAlphaMode::Inherit,
|
|
},
|
|
view_formats: if !format.is_srgb() {
|
|
vec![format.add_srgb_suffix()]
|
|
} else {
|
|
vec![]
|
|
},
|
|
};
|
|
|
|
render_device.configure_surface(&surface, &configuration);
|
|
|
|
SurfaceData {
|
|
surface: WgpuWrapper::new(surface),
|
|
configuration,
|
|
}
|
|
});
|
|
|
|
if window.size_changed || window.present_mode_changed {
|
|
data.configuration.width = window.physical_width;
|
|
data.configuration.height = window.physical_height;
|
|
data.configuration.present_mode = match window.present_mode {
|
|
PresentMode::Fifo => wgpu::PresentMode::Fifo,
|
|
PresentMode::FifoRelaxed => wgpu::PresentMode::FifoRelaxed,
|
|
PresentMode::Mailbox => wgpu::PresentMode::Mailbox,
|
|
PresentMode::Immediate => wgpu::PresentMode::Immediate,
|
|
PresentMode::AutoVsync => wgpu::PresentMode::AutoVsync,
|
|
PresentMode::AutoNoVsync => wgpu::PresentMode::AutoNoVsync,
|
|
};
|
|
render_device.configure_surface(&data.surface, &data.configuration);
|
|
}
|
|
|
|
window_surfaces.configured_windows.insert(window.entity);
|
|
}
|
|
}
|