Fix Window feedback loop between the OS and Bevy (#7517)

# Objective

Fix #7377
Fix #7513

## Solution

Record the changes made to the Bevy `Window` from `winit` as 'canon' to avoid Bevy sending those changes back to `winit` again, causing a feedback loop.

## Changelog

* Removed `ModifiesWindows` system label.
  Neither `despawn_window` nor `changed_window` actually modify the `Window` component so all the `.after(ModifiesWindows)` shouldn't be necessary.
* Moved `changed_window` and `despawn_window` systems to `CoreStage::Last` to avoid systems making changes to the `Window` between `changed_window` and the end of the frame as they would be ignored.

## Migration Guide
The `ModifiesWindows` system label was removed.


Co-authored-by: devil-ira <justthecooldude@gmail.com>
This commit is contained in:
ira 2023-02-07 14:18:13 +00:00
parent 6314f50e7b
commit f69f1329e0
7 changed files with 49 additions and 83 deletions

View File

@ -11,7 +11,6 @@ mod render;
pub use alpha::*;
use bevy_transform::TransformSystem;
use bevy_window::ModifiesWindows;
pub use bundle::*;
pub use fog::*;
pub use light::*;
@ -199,8 +198,7 @@ impl Plugin for PbrPlugin {
.in_set(SimulationLightSystems::AssignLightsToClusters)
.after(TransformSystem::TransformPropagate)
.after(VisibilitySystems::CheckVisibility)
.after(CameraUpdateSystem)
.after(ModifiesWindows),
.after(CameraUpdateSystem),
)
.add_system(
update_directional_light_cascades

View File

@ -7,7 +7,6 @@ use bevy_reflect::{
std_traits::ReflectDefault, FromReflect, GetTypeRegistration, Reflect, ReflectDeserialize,
ReflectSerialize,
};
use bevy_window::ModifiesWindows;
use serde::{Deserialize, Serialize};
/// Adds [`Camera`](crate::camera::Camera) driver systems for a given projection type.
@ -43,7 +42,6 @@ impl<T: CameraProjection + Component + GetTypeRegistration> Plugin for CameraPro
.add_system(
crate::camera::camera_system::<T>
.in_set(CameraUpdateSystem)
.after(ModifiesWindows)
// We assume that each camera will only have one projection,
// so we can ignore ambiguities with all other monomorphizations.
// FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481.

View File

@ -28,7 +28,6 @@ use bevy_asset::AddAsset;
use bevy_ecs::prelude::*;
use bevy_render::{camera::CameraUpdateSystem, ExtractSchedule, RenderApp};
use bevy_sprite::SpriteSystem;
use bevy_window::ModifiesWindows;
use std::num::NonZeroUsize;
#[derive(Default)]
@ -83,7 +82,6 @@ impl Plugin for TextPlugin {
.add_system(
update_text2d_layout
.in_base_set(CoreSet::PostUpdate)
.after(ModifiesWindows)
// Potential conflict: `Assets<Image>`
// In practice, they run independently since `bevy_render::camera_update_system`
// will only ever observe its own render target, and `update_text2d_layout`

View File

@ -34,7 +34,6 @@ use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_input::InputSystem;
use bevy_transform::TransformSystem;
use bevy_window::ModifiesWindows;
use stack::ui_stack_system;
pub use stack::UiStack;
use update::update_clipping_system;
@ -110,7 +109,6 @@ impl Plugin for UiPlugin {
widget::text_system
.in_base_set(CoreSet::PostUpdate)
.before(UiSystem::Flex)
.after(ModifiesWindows)
// Potential conflict: `Assets<Image>`
// In practice, they run independently since `bevy_render::camera_update_system`
// will only ever observe its own render target, and `widget::text_system`
@ -135,8 +133,7 @@ impl Plugin for UiPlugin {
.add_system(
flex_node_system
.in_set(UiSystem::Flex)
.before(TransformSystem::TransformPropagate)
.after(ModifiesWindows),
.before(TransformSystem::TransformPropagate),
)
.add_system(ui_stack_system.in_set(UiSystem::Stack))
.add_system(

View File

@ -137,9 +137,6 @@ impl Plugin for WindowPlugin {
}
}
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
pub struct ModifiesWindows;
/// Defines the specific conditions the application should exit on
#[derive(Clone)]
pub enum ExitCondition {

View File

@ -6,7 +6,7 @@ mod winit_config;
mod winit_windows;
use bevy_ecs::system::{SystemParam, SystemState};
use system::{changed_window, create_window, despawn_window};
use system::{changed_window, create_window, despawn_window, CachedWindow};
pub use winit_config::*;
pub use winit_windows::*;
@ -26,7 +26,7 @@ use bevy_utils::{
};
use bevy_window::{
exit_on_all_closed, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime,
ModifiesWindows, ReceivedCharacter, RequestRedraw, Window, WindowBackendScaleFactorChanged,
ReceivedCharacter, RequestRedraw, Window, WindowBackendScaleFactorChanged,
WindowCloseRequested, WindowCreated, WindowFocused, WindowMoved, WindowResized,
WindowScaleFactorChanged,
};
@ -39,7 +39,6 @@ use winit::{
event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopWindowTarget},
};
use crate::system::WinitWindowInfo;
#[cfg(target_arch = "wasm32")]
use crate::web_resize::{CanvasParentResizeEventChannel, CanvasParentResizePlugin};
@ -70,7 +69,6 @@ impl Plugin for WinitPlugin {
app.init_non_send_resource::<WinitWindows>()
.init_resource::<WinitSettings>()
.set_runner(winit_runner)
.configure_set(ModifiesWindows.in_base_set(CoreSet::PostUpdate))
// exit_on_all_closed only uses the query to determine if the query is empty,
// and so doesn't care about ordering relative to changed_window
.add_systems(
@ -79,7 +77,7 @@ impl Plugin for WinitPlugin {
// Update the state of the window before attempting to despawn to ensure consistent event ordering
despawn_window.after(changed_window),
)
.in_set(ModifiesWindows),
.in_base_set(CoreSet::Last),
);
#[cfg(target_arch = "wasm32")]
@ -349,7 +347,7 @@ pub fn winit_runner(mut app: App) {
// Fetch and prepare details from the world
let mut system_state: SystemState<(
NonSend<WinitWindows>,
Query<(&mut Window, &mut WinitWindowInfo)>,
Query<(&mut Window, &mut CachedWindow)>,
WindowEvents,
InputEvents,
CursorEvents,
@ -376,7 +374,7 @@ pub fn winit_runner(mut app: App) {
return;
};
let (mut window, mut info) =
let (mut window, mut cache) =
if let Ok((window, info)) = window_query.get_mut(window_entity) {
(window, info)
} else {
@ -394,7 +392,6 @@ pub fn winit_runner(mut app: App) {
window
.resolution
.set_physical_resolution(size.width, size.height);
info.last_winit_size = size;
window_events.window_resized.send(WindowResized {
window: window_entity,
@ -421,11 +418,7 @@ pub fn winit_runner(mut app: App) {
window.resolution.physical_height() as f64 - position.y,
);
// bypassing change detection to not trigger feedback loop with system `changed_window`
// this system change the cursor position in winit
window
.bypass_change_detection()
.set_physical_cursor_position(Some(physical_position));
window.set_physical_cursor_position(Some(physical_position));
cursor_events.cursor_moved.send(CursorMoved {
window: window_entity,
@ -439,14 +432,7 @@ pub fn winit_runner(mut app: App) {
});
}
WindowEvent::CursorLeft { .. } => {
// Component
if let Ok((mut window, _)) = window_query.get_mut(window_entity) {
// bypassing change detection to not trigger feedback loop with system `changed_window`
// this system change the cursor position in winit
window
.bypass_change_detection()
.set_physical_cursor_position(None);
}
window.set_physical_cursor_position(None);
cursor_events.cursor_left.send(CursorLeft {
window: window_entity,
@ -594,6 +580,10 @@ pub fn winit_runner(mut app: App) {
},
_ => {}
}
if window.is_changed() {
cache.window = window.clone();
}
}
event::Event::DeviceEvent {
event: DeviceEvent::MouseMotion { delta: (x, y) },

View File

@ -39,20 +39,19 @@ pub(crate) fn create_window<'a>(
mut winit_windows: NonSendMut<WinitWindows>,
#[cfg(target_arch = "wasm32")] event_channel: ResMut<CanvasParentResizeEventChannel>,
) {
for (entity, mut component) in created_windows {
for (entity, mut window) in created_windows {
if winit_windows.get_window(entity).is_some() {
continue;
}
info!(
"Creating new window {:?} ({:?})",
component.title.as_str(),
window.title.as_str(),
entity
);
let winit_window = winit_windows.create_window(event_loop, entity, &component);
let current_size = winit_window.inner_size();
component
let winit_window = winit_windows.create_window(event_loop, entity, &window);
window
.resolution
.set_scale_factor(winit_window.scale_factor());
commands
@ -61,18 +60,14 @@ pub(crate) fn create_window<'a>(
window_handle: winit_window.raw_window_handle(),
display_handle: winit_window.raw_display_handle(),
})
.insert(WinitWindowInfo {
previous: component.clone(),
last_winit_size: PhysicalSize {
width: current_size.width,
height: current_size.height,
},
.insert(CachedWindow {
window: window.clone(),
});
#[cfg(target_arch = "wasm32")]
{
if component.fit_canvas_to_parent {
let selector = if let Some(selector) = &component.canvas {
if window.fit_canvas_to_parent {
let selector = if let Some(selector) = &window.canvas {
selector
} else {
WINIT_CANVAS_SELECTOR
@ -106,11 +101,10 @@ pub(crate) fn despawn_window(
}
}
/// Previous state of the window so we can check sub-portions of what actually was changed.
/// The cached state of the window so we can check which properties were changed from within the app.
#[derive(Debug, Clone, Component)]
pub struct WinitWindowInfo {
pub previous: Window,
pub last_winit_size: PhysicalSize<u32>,
pub struct CachedWindow {
pub window: Window,
}
// Detect changes to the window and update the winit window accordingly.
@ -121,18 +115,16 @@ pub struct WinitWindowInfo {
// - [`Window::canvas`] currently cannot be updated after startup, not entirely sure if it would work well with the
// event channel stuff.
pub(crate) fn changed_window(
mut changed_windows: Query<(Entity, &mut Window, &mut WinitWindowInfo), Changed<Window>>,
mut changed_windows: Query<(Entity, &mut Window, &mut CachedWindow), Changed<Window>>,
winit_windows: NonSendMut<WinitWindows>,
) {
for (entity, mut window, mut info) in &mut changed_windows {
let previous = &info.previous;
for (entity, mut window, mut cache) in &mut changed_windows {
if let Some(winit_window) = winit_windows.get_window(entity) {
if window.title != previous.title {
if window.title != cache.window.title {
winit_window.set_title(window.title.as_str());
}
if window.mode != previous.mode {
if window.mode != cache.window.mode {
let new_mode = match window.mode {
bevy_window::WindowMode::BorderlessFullscreen => {
Some(winit::window::Fullscreen::Borderless(None))
@ -156,19 +148,15 @@ pub(crate) fn changed_window(
winit_window.set_fullscreen(new_mode);
}
}
if window.resolution != previous.resolution {
if window.resolution != cache.window.resolution {
let physical_size = PhysicalSize::new(
window.resolution.physical_width(),
window.resolution.physical_height(),
);
// Prevents "window.resolution values set from a winit resize event" from
// being set here, creating feedback loops.
if physical_size != info.last_winit_size {
winit_window.set_inner_size(physical_size);
}
}
if window.physical_cursor_position() != previous.physical_cursor_position() {
if window.physical_cursor_position() != cache.window.physical_cursor_position() {
if let Some(physical_position) = window.physical_cursor_position() {
let inner_size = winit_window.inner_size();
@ -184,21 +172,21 @@ pub(crate) fn changed_window(
}
}
if window.cursor.icon != previous.cursor.icon {
if window.cursor.icon != cache.window.cursor.icon {
winit_window.set_cursor_icon(converters::convert_cursor_icon(window.cursor.icon));
}
if window.cursor.grab_mode != previous.cursor.grab_mode {
if window.cursor.grab_mode != cache.window.cursor.grab_mode {
crate::winit_windows::attempt_grab(winit_window, window.cursor.grab_mode);
}
if window.cursor.visible != previous.cursor.visible {
if window.cursor.visible != cache.window.cursor.visible {
winit_window.set_cursor_visible(window.cursor.visible);
}
if window.cursor.hit_test != previous.cursor.hit_test {
if window.cursor.hit_test != cache.window.cursor.hit_test {
if let Err(err) = winit_window.set_cursor_hittest(window.cursor.hit_test) {
window.cursor.hit_test = previous.cursor.hit_test;
window.cursor.hit_test = cache.window.cursor.hit_test;
warn!(
"Could not set cursor hit test for window {:?}: {:?}",
window.title, err
@ -206,19 +194,19 @@ pub(crate) fn changed_window(
}
}
if window.decorations != previous.decorations
if window.decorations != cache.window.decorations
&& window.decorations != winit_window.is_decorated()
{
winit_window.set_decorations(window.decorations);
}
if window.resizable != previous.resizable
if window.resizable != cache.window.resizable
&& window.resizable != winit_window.is_resizable()
{
winit_window.set_resizable(window.resizable);
}
if window.resize_constraints != previous.resize_constraints {
if window.resize_constraints != cache.window.resize_constraints {
let constraints = window.resize_constraints.check_constraints();
let min_inner_size = LogicalSize {
width: constraints.min_width,
@ -235,7 +223,7 @@ pub(crate) fn changed_window(
}
}
if window.position != previous.position {
if window.position != cache.window.position {
if let Some(position) = crate::winit_window_position(
&window.position,
&window.resolution,
@ -262,42 +250,42 @@ pub(crate) fn changed_window(
winit_window.set_minimized(minimized);
}
if window.focused != previous.focused && window.focused {
if window.focused != cache.window.focused && window.focused {
winit_window.focus_window();
}
if window.window_level != previous.window_level {
if window.window_level != cache.window.window_level {
winit_window.set_window_level(convert_window_level(window.window_level));
}
// Currently unsupported changes
if window.transparent != previous.transparent {
window.transparent = previous.transparent;
if window.transparent != cache.window.transparent {
window.transparent = cache.window.transparent;
warn!(
"Winit does not currently support updating transparency after window creation."
);
}
#[cfg(target_arch = "wasm32")]
if window.canvas != previous.canvas {
window.canvas = previous.canvas.clone();
if window.canvas != cache.window.canvas {
window.canvas = cache.window.canvas.clone();
warn!(
"Bevy currently doesn't support modifying the window canvas after initialization."
);
}
if window.ime_enabled != previous.ime_enabled {
if window.ime_enabled != cache.window.ime_enabled {
winit_window.set_ime_allowed(window.ime_enabled);
}
if window.ime_position != previous.ime_position {
if window.ime_position != cache.window.ime_position {
winit_window.set_ime_position(LogicalPosition::new(
window.ime_position.x,
window.ime_position.y,
));
}
info.previous = window.clone();
cache.window = window.clone();
}
}
}