Skip to content

App hangs permanently when UpdateMode::Reactive with no wake sources is set during a window drag on Windows #25903

Description

@matthewdowns

Bevy version and features

Bevy 0.19.1, default-features = false, features = ["3d"]. No third-party plugins.

Relevant system information

  • rustc 1.97.1 (8bab26f4f 2026-07-14), cargo 1.97.1
  • Windows 11 Pro 10.0.26200
  • AdapterInfo { name: "NVIDIA GeForce RTX 5070 Ti", vendor: 4318, device: 11269, device_type: DiscreteGpu, device_pci_bus_id: "0000:01:00.0", driver: "NVIDIA", driver_info: "616.56", backend: Vulkan, subgroup_min_size: 32, subgroup_max_size: 32, transient_saves_memory: false }

What you did

I wanted to cut the update rate right down while the window is being dragged by
its title bar, as a workaround for rust-windowing/winit#4708. On the first
WindowMoved I switch both update modes to a long Reactive wait with no wake
sources, and switch back to Continuous once the moves stop.

use bevy::prelude::*;
use bevy::window::WindowMoved;
use bevy::winit::{UpdateMode, WinitSettings};
use std::time::{Duration, Instant};

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Update, (count, throttle_while_moved))
        .run();
}

fn count(mut n: Local<u32>, mut started: Local<Option<Instant>>) {
    let began = *started.get_or_insert_with(Instant::now);
    *n += 1;
    println!("update {} at {:.2}s", *n, began.elapsed().as_secs_f64());
}

fn throttle_while_moved(
    mut moves: MessageReader<WindowMoved>,
    mut settings: ResMut<WinitSettings>,
    mut last: Local<Option<Instant>>,
    mut was: Local<bool>,
) {
    if !moves.is_empty() {
        *last = Some(Instant::now());
    }
    moves.clear();

    let moving = last.is_some_and(|w| w.elapsed() < Duration::from_millis(300));
    if moving == *was {
        return;
    }
    *was = moving;
    println!(">>> {}", if moving { "moving" } else { "settled" });

    let mode = if moving {
        UpdateMode::Reactive {
            wait: Duration::from_secs(2),
            react_to_device_events: false,
            react_to_user_events: false,
            react_to_window_events: false,
        }
    } else {
        UpdateMode::Continuous
    };
    settings.focused_mode = mode;
    settings.unfocused_mode = mode;
}

Run it and drag the window by its title bar.

What went wrong

Expected: updates drop to one every 2 seconds while the window is moving, and
the system sees the moves stop and puts the mode back to Continuous.

Actually happened: the app updates for about another 0.6 seconds, then stops
updating permanently. Not a low rate, zero. The window never repaints again, does
not respond to clicks or keys, and never prints ">>> settled" because no system
ever runs again. The process has to be killed.

update 7425 at 6.41s
>>> moving
update 7426 at 6.41s
...
update 7984 at 7.03s     <- last update, ~25s before the process was killed

Additional information

The same config is fine when no drag is involved. Replacing the trigger with
a plain timer, so that nothing else changes, gives exactly the documented
behaviour forever:

fn throttle_after_three_seconds(
    mut settings: ResMut<WinitSettings>,
    mut started: Local<Option<Instant>>,
    mut done: Local<bool>,
) {
    let began = *started.get_or_insert_with(Instant::now);
    if *done || began.elapsed() < Duration::from_secs(3) { return; }
    *done = true;
    // ...same mode assignment as above...
}
update 5640 at 5.00s
update 5641 at 7.00s
update 5642 at 9.00s
update 5643 at 11.00s
update 5644 at 13.00s
update 5645 at 15.00s
update 5646 at 17.00s
update 5647 at 19.00s

So Reactive with no wake sources is not broken by itself. Something about
entering that mode during the Windows modal move loop leaves the event loop with
no standing wake-up, and there is then no path back.

I instrumented a local bevy_winit/src/state.rs to print the StartCause and the
gate values on each pass. During the hang: 950 consecutive passes of
cause=WaitCancelled wait_elapsed=false should_update=false, and
StartCause::ResumeTimeReached never appears again.

One candidate: about_to_wait only arms the timeout when wait_elapsed is
already true.

UpdateMode::Reactive { wait, .. } => {
    if self.wait_elapsed {
        ...
        event_loop.set_control_flow(ControlFlow::WaitUntil(next));
    }
}

I do not know whether this belongs to Bevy or to winit's handling of the modal
move loop.

Related: rust-windowing/winit#4708

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-WindowingPlatform-agnostic interface layer to run your app inC-BugAn unexpected or incorrect behaviorD-ModestA "normal" level of difficulty; suitable for simple features or challenging fixesS-Needs-DesignThis issue requires design work to think about how it would best be accomplished

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions