winit: Consolidate poll_events() and run_forever() into get_event(Timeout)
EDIT: THIS ISSUE HAS BEEN SUPERSEDED by #459.
In #219, I examined some unfortunate event handling complexities on MacOS X. That examination led me to view and re-implement both poll_events() and run_forever() as special cases of a more general API, get_event(Timeout) -> Option<Event>.
I propose:
- Refactoring each platform to expose this
get_event(Timeout)interface, - Refactoring
poll_events()andrun_forever()to be implemented in terms ofget_event(Timeout), and - Deprecating
poll_events()andrun_forever()in favor ofget_event(Timeout).
This is a user-facing API change, so I’m accompanying this proposal with a larger than usual explanation.
Receive-with-time-constraint is the fundamental operation
poll_events() allows the user to communicate “I want all the events that are ready, but I don’t want you to wait”. Let’s call that get_event(Timeout::Now), and make the loop explicit:
// current
events_loop.poll_events(|event| {
match event {
Event::WindowEvent { event: WindowEvent::Resized(w, h), .. } => {
println!("The window was resized to {}x{}", w, h);
},
_ => ()
}
});
// proposed
while let Some(event) = events_loop.get_event(Timeout::Now) {
match event {
Event::WindowEvent { event: WindowEvent::Resized(w, h), .. }) => {
println!("The window was resized to {}x{}", w, h);
},
_ => (),
}
}
run_forever() allows the user to communicate “I want all the events until I tell you to stop”, which… is kind of weird, isn’t it? At any rate, it’s the user’s intention to block indefinitely and eventually break out of the loop. Let’s call that get_event(Timeout::Forever), and again push down the loop:
// current
events_loop.run_forever(|event| {
match event {
winit::Event::WindowEvent { event: winit::WindowEvent::Closed, .. } => {
winit::ControlFlow::Break
},
_ => winit::ControlFlow::Continue,
}
});
// proposed
loop {
match events_loop.get_event(Timeout::Forever) {
winit::Event::WindowEvent { event: winit::WindowEvent::Closed, .. } => {
break;
},
_ => ()
}
}
Some users have additional needs, like “I want all the events between now and when I need to update my state/draw the next frame”. pistoncore-glutin_window does this today by unconditionally spawning a thread that wakes up the event loop after a fixed period of time. We should be able to support it efficiently on every platform without needing extra threads, and with a timeout-centric API, we can:
// current
let events_loop_proxy = self.events_loop.create_proxy();
thread::spawn(move || {
thread::sleep(timeout);
events_loop_proxy.wakeup().ok(); // wakeup can fail only if the event loop went away
});
{
let ref mut events = self.events;
self.events_loop.run_forever(|ev| {
events.push_back(ev);
glutin::ControlFlow::Break
});
}
let event = self.events.pop_front();
// proposed
let event = events_loop.get_event(Timeout::Deadline(timeout));
Timeouts are sufficient to justify a new API
Consider a game that wants to update and render at 60 fps. If the game is running faster than 60 fps, it should sleep and wait until it’s time to start the next frame; if it’s running slower, it should busy-wait.
This use case presently requires a calling run_forever(callback) to allow the thread to sleep, plus a second thread keeping track of time and calling Proxy::wakeup() as needed. That’s wasteful and unnecessarily complex.
winit should support timeouts pattern directly. Additionally, this is a common requirement; every event loop has some way to block until a specified time, or some idiomaitc way to synthesize a timeout. winit should use this where appropriate.
Simplifies the API
winit puts user code at the bottom of the stack, and does not force the user to organize their code in terms of callbacks… except for event handling. Changing the API so that the user calls get_event(Timeout) in a loop adds consistency, and it lets the user define and manage their event loop with normal Rust statements instead of a callback that returns a ControlFlow enum.
This also makes EventsLoop look more like a futures::stream, or a generator, or many other things that users might already know. “I want to get a message from you” requires less explanation than having two different functions that take two different callbacks.
Reduces duplication
poll_events() and run_forever() have considerable overlap, and in the case of the MacOS platform, considerable code duplication. Not only can get_event(Timeout) provide more functionality, in the case of the MacOS platform, refactoring into this caused a net reduction in code.
Low risk
It’s very easy to implement poll_events() and run_forever() in terms of get_event(Timeout), which means there’s very little risk in deciding to move that direction.
winit can continue to provide the same poll_events() + run_forever() API to the user while refactoring platforms into get_event(Timeout) behind the scenes. Once they’re all ready, winit can update the examples, deprecate poll_events() and run_forever(), and ultimately remove at some later point.
Next steps
I think this covers everything:
pub enum Timeout {
Now,
Forever,
Deadline(std::time::Duration),
}
impl EventsLoop {
pub fn get_event(Timeout) -> Option<Event> {
// …
}
}
A change like this requires buy-in, and this isn’t my project, so I don’t know what all would need to happen. I’m happy to start by soliciting feedback, so: what does everyone think?
About this issue
- Original URL
- State: closed
- Created 7 years ago
- Reactions: 12
- Comments: 32 (11 by maintainers)
Commits related to this issue
- Switch from .run_forever() to .poll_events() This commit uses `.poll_events()` to allow processing multiple events per rendered frame. (Before, only one event was processed per frame.) This is necess... — committed to jturner314/gfx by jturner314 6 years ago
- Switch from .run_forever() to .poll_events() This commit uses `.poll_events()` to allow processing multiple events per rendered frame. (Before, only one event was processed per frame.) This is necess... — committed to jturner314/gfx by jturner314 6 years ago
- Merge #1822 1822: Switch from .run_forever() to .poll_events() in gfx_app r=kvark a=jturner314 This commit uses `.poll_events()` to allow processing multiple events per rendered frame. (Before, only... — committed to gfx-rs/gfx by bors[bot] 6 years ago
- Merge next back to dev branch (#305) * refactor(windows): `begin_resize_drag` now similar to gtk's (#200) * refactor(windows): `begin_resize_drag` now similart to gtk's * fix * feat(linux): ... — committed to madsmtm/winit by deleted user 2 years ago
- Publish New Versions (#234) * feat(macos): Add `unhide_application` method, closes #182 (#231) * feat(macos): Add `unhide_application` method * Update src/platform/macos.rs Co-authored-by: A... — committed to madsmtm/winit by github-actions[bot] 2 years ago
They care very much so - I believe most of the points in this issue are being addressed in #638 in @Osspial’s heroic effort to rewrite the event loop, though I’m not familiar enough with the PR to know for sure if everything in this issue has been addressed. That PR is just about ready to go - feel free to have a look, test it and share any constructive input you might have. The maintainers are a group of collaborators working on this in their limited, free time and for many, winit is just one of many open source crates to which they contribute, each with their own slew of “showstopper” issues to address.
There are any number of reasons: porting existing code which is not written in a callback style, to building libraries on top of winit to name a couple. If two libraries make the decision to control the event loop then those libraries are forever incompatible.
Inconvenience is also an important factor - I may not even care about the resizing issue on macos, as for many applications it doesn’t matter if the loop blocks for that period of time, and yet to implement the basic primitive of “wait with timeout” the universal operation support by synchronous APIs, I have to:
Even then, there’s still a race condition between the “wake up” and the next event coming in - I’m not really sure how you avoid that if you’re forced (by winit and not the platform) to use multiple threads!
The stack swapping feature is a nice-to-have workaround for a relatively minor platform-specific bug: replacing winit’s old API with the current one because of this bug was a terrible mistake.
I wonder why these issues aren’t getting solved. Winit is basically useless as it is now. You can choose between using 100% CPU (
poll_events) or not rendering anything at all (loop_forever). If this isn’t going to be solved, please update Winit with a warning telling people not to use it.What you describe is more or less winit’s old API. We transitioned to callbacks because it solves the fact that MacOS uses a resize callback, and also in the longer term because of Android and Emscripten.
It’s the opposite, the
futureslibrary is based on callbacks.So, if I’m understanding things correctly, combining what’s been outlined here with the problems we have with Windows, then doesn’t this mean that our current design is at odds with most platforms?
Unfortunately, using the X connection fd like this does not work reliably, because OpenGL/vulkan implementations will do IO on that fd internally, causing unrelated events to be read early and read-readiness notifications to be missed.
winitalready controls the event loop. It provides no wait handles with which to link to other event loops, which means you can either a) letwinitblock inside the windowing system or b) block yourself somewhere outside the windowing system. If your goal is to service events promptly, you must userun_forever().I opened this issue to propose poll-with-timeout as a more useful common denominator: this would let you block inside the windowing system and regain control again after a fixed period. This doesn’t change the fact that
winitis still blocking in its own event loop; there’s no way to tellwinitto e.g. wait on a windowing event and wait on a socket becoming readable.IMO, the main issue here is control over the main thread. Right now, users call
winitandwinitreturns back to the user. This isn’t how MacOS works; you’re not supposed to constantly bail out of the platform event loop, and there are situations (like resizing) where the platform’s event loop takes over anyway. It’s also not how the web is designed to work, and it’s much more difficult to paper over that.Poll-with-timeout is a more useful common denominator, but it’s not the lowest common denominator. The common interface that all platforms can share is to have the user surrender their thread to
winit, and havewinitcall back into the user’s code.Inverting control flow would absolutely be a giant breaking change, but that doesn’t avoid the question: which is the right model? Should the user call
winit, or shouldwinitcall the user? Does the main thread belong to the user, or does it belong to the platform? @tomaka?If the user should call
winit, then we should merge coroutines support for MacOS, since MacOS’s design is at odds withwinit’s design. We should also give up on havingwinit’s web platform work the same as the other platforms, since… it can’t.If
winitshould call the user, then we should figure out what that API should look like, and we should figure out how to get there from here. It’s worth noting that I used coroutines to merge two incompatible event loop models in #237; we could do the same thing to provide a user-calls-winitcompatibility layer on top of a newwinit-calls-user API.One option is to totally invert the control flow:
The user’s
fn main()would only exist to call intowinit::Main(), which might be a Rust event loop that runs forever (most platforms), a native event loop that runs forever (MacOS), or a function that sets up callbacks and returns (web).winitneed only guarantee that each platform would somehow call back into the user’s code after initialization.@Boscop I’m trying to make a program that draws only after events (user interaction, fs/network input, timers above 1 second). I expect to make a loop that waits until there is an event, then processes all pending events, then draws.
The first thing I try is
poll_events. It handles all pending events and then yields to rendering, but I still need something to wait for the next event before I repeat the loop. The next thing to try isloop_forever. It waits for events and handles them, but it doesn’t return when there are no more events. It’s hard to understand how someone could have decided that there shall be one function that processes events without waiting and then returns, and one function that waits and processes events but doesn’t return, but no function that waits for events, processes them and then returns. I can’t draw from within the infinite loop, because that causes only one event to be processed for each frame, making the program unresponsive. There is no ‘idle’ event I can use to detect when it’s appropriate to draw.