Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an example showing how to switch between ControlFlow::Poll and Co… #1450

Closed
wants to merge 0 commits into from

Conversation

dabreegster
Copy link

…ntrolFlow::Wait

I'm trying to figure out how to write a game loop that updates a simulation based on how much real time has passed, but lets the user pause. From #1055 and #1018, it seems like other people have been confused by this too. I've reduced the use case to a hopefully minimal stopwatch example that the user can pause/resume. If you just let it run, you can see that the total amount of update time measured by the stopwatch slowly diverges from the time since the beginning of the program.

I'm pretty sure I'm not using WaitUntil correctly. Should the "time has passed, go update by some delta-time" instead happen from a NewEvents(StartCause::ResumeTimeReached) case? I tried some variations of that without any luck. (I can update with some other attempts if that'd be helpful.) Any hints? Hopefully an example like this one could help people in the future understand how to use ControlFlow a little better. Thanks!

  • Tested on all platforms changed
  • Compilation warnings were addressed
  • [X ] cargo fmt has been run on this branch
  • cargo doc builds successfully
  • Added an entry to CHANGELOG.md if knowledge of this change could be valuable to users
  • Updated documentation to reflect any user-facing changes, including notes of platform-specific behavior
  • [X ] Created or updated an example program if it would help users understand this functionality
  • [X ] Updated feature matrix, if new features were added or implemented

examples/pauseable.rs Outdated Show resolved Hide resolved
@goddessfreya goddessfreya added the C - waiting on maintainer A maintainer must review this code label Feb 10, 2020
dabreegster added a commit to a-b-street/abstreet that referenced this pull request Feb 10, 2020
examples/pauseable.rs Outdated Show resolved Hide resolved
examples/pauseable.rs Outdated Show resolved Hide resolved
examples/pauseable.rs Outdated Show resolved Hide resolved
Copy link
Contributor

@Osspial Osspial left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for writing this example up! I've written some comments on how to improve this, but once this PR is ready it should be a pretty big help for people doing more advanced event loop work.

examples/pauseable.rs Outdated Show resolved Hide resolved
examples/pauseable.rs Outdated Show resolved Hide resolved
// update takes more than update_frequency, then the next update will occur
// immediately.)
last_update = now;
*control_flow = ControlFlow::WaitUntil(now + update_frequency);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the "time has passed, go update by some delta-time" instead happen from a NewEvents(StartCause::ResumeTimeReached) case?

Yep! I've written a diff that shows how I'd structure the code with the timer updating in NewEvents:

diff --git a/examples/pauseable.rs b/examples/pauseable.rs
index 3892882a..9675d159 100644
--- a/examples/pauseable.rs
+++ b/examples/pauseable.rs
@@ -1,7 +1,7 @@
 use std::thread;
 use std::time::{Duration, Instant};
 use winit::{
-    event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent},
+    event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent, StartCause},
     event_loop::{ControlFlow, EventLoop},
     window::WindowBuilder,
 };
@@ -27,11 +27,33 @@ fn main() {
     let started_at = Instant::now();
 
     let mut running = true;
-    let mut last_update = Instant::now();
     let mut total = Duration::new(0, 0);
+    let mut update_triggered = false;
 
     event_loop.run(move |event, _, control_flow| {
         match event {
+            Event::NewEvents(StartCause::Init) => {
+                *control_flow = ControlFlow::WaitUntil(Instant::now() + update_frequency);
+            },
+            Event::NewEvents(StartCause::ResumeTimeReached{requested_resume, ..}) => {
+                if running {
+                    // Only do an update when the timer has been reached. We defer running the
+                    // update code until `MainEventsCleared` so that the update can take account
+                    // of user input events.
+                    update_triggered = true;
+
+                    // We schedule the next update relative to the previously scheduled update time,
+                    // rather than Instant::now(). This has a couple benefits:
+                    // - The update timer is immune to drift caused by any delay between the start
+                    //   of the event loop iteration and the time `Instant::now()` is called.
+                    // - If unexpected circumstances cause the update code to get called at a lower
+                    //   frequency than `update_frequency` specifies (say, the OS doesn't schedule
+                    //   the update for 400ms), the scheduler will aggressively schedule updates
+                    //   until we're caught up with real time.
+                    *control_flow = ControlFlow::WaitUntil(requested_resume + update_frequency);
+                    total += update_frequency;
+                }
+            },
             Event::WindowEvent {
                 event: WindowEvent::CloseRequested,
                 ..
@@ -51,9 +73,7 @@ fn main() {
             } => {
                 running = !running;
                 if running {
-                    let now = Instant::now();
-                    last_update = now;
-                    *control_flow = ControlFlow::WaitUntil(now + update_frequency);
+                    *control_flow = ControlFlow::WaitUntil(Instant::now() + update_frequency);
                     println!("- Resuming!");
                 } else {
                     *control_flow = ControlFlow::Wait;
@@ -66,25 +86,18 @@ fn main() {
                 thread::sleep(Duration::from_millis(1));
             }
             Event::MainEventsCleared => {
-                if running {
-                    let dt = last_update.elapsed();
-                    let now = Instant::now();
-                    // We want an update every update_frequency, so calculate the next time before
-                    // we process the update, which takes some amount of time. (If processing the
-                    // update takes more than update_frequency, then the next update will occur
-                    // immediately.)
-                    last_update = now;
-                    *control_flow = ControlFlow::WaitUntil(now + update_frequency);
-
-                    total += dt;
+                if update_triggered {
                     println!(
                         "- Update: +{:?} for a total of {:?}. It's been {:?} since the very start",
-                        dt,
+                        update_frequency,
                         total,
                         started_at.elapsed()
                     );
+
                     // Pretend that updating the application takes about 20ms
                     thread::sleep(Duration::from_millis(20));
+
+                    update_triggered = false;
                 }
                 // Redraw since the application state was updated either by time passing or some
                 // input being handled previously.

Let me know if you have any questions about that - it's hard for me to tell what particular things are going to be confusing, since I've spent so much time working with this API!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed fixes! With this patch, the behavior is now a little strange. If you produce input while running, total rises too quickly, and after leaving the mouse still again, there's a delay (Seems to be about 100ms to me) before the periodic updates resume. I'll take a closer look tonight. Do we need to handle WaitCancelled, since there's input events interrupting things?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What OS are you on? I'm running Windows 10, and I'm not seeing that behavior. It could just be that the additional lines printed to the console on input events are making it seem like total is rising too quickly - if you disable all non-update printlns by commenting out lines 86 and 107, do you observe the same behavior?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we shouldn't have to handle WaitCancelled. If the specified timeout has been reached, the event loop should always emit ResumeTimeReached even if there are additional events ready to be processed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Sorry for the massively delayed response). I'm on Ubuntu 18.04. Commenting the printlns has no effect. Printing the absolute time since the start is the useful comparison.

Attached a video of the behaviour I'm seeing. As soon as I start wiggling the mouse, the total update time jumps up too quickly. Then after stopping the chain of input events, nothing happens; the periodic updates seem to be gone (or delayed). I press space to pause and resume, then things seem to go back to normal.

winit

@dabreegster dabreegster closed this Aug 6, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C - waiting on maintainer A maintainer must review this code
Development

Successfully merging this pull request may close these issues.

4 participants