Ggez: Does not gracefully handle it if the main loop update is too slow

Created on 3 Jun 2019  路  5Comments  路  Source: ggez/ggez

Describe the bug
Described here: https://www.reddit.com/r/rust/comments/bvrkpx/a_guide_to_rust_2d_game_frameworks_2019/epv2f4q/?context=3

Additionally, I've encountered situations where I've been able to destabilize the main loop;
i.e. where the update method runs too slow and ggez keeps running update, failing to
draw or handle inputs in a timely manner.

To Reproduce

At a GUESS, you make a loop with event::check_update_time() that takes longer than the given update time to execute. I believe it is possible to make the main loop hang forever by doing that, and it won't ever get around to checking for events or handling quit requests. Need to double check.

Expected behavior

Not sure! But making everything hang forever is obviously wrong. Options:

  • Check, and log a warning when this happens
  • Check, and if the main loop is taking too long then yield anyway to handle events before trying again. Your game will lag, but won't hang.
  • Check, and return Result from check_update_time so we can distinguish between "time is caught up", "time is not caught up", and "it doesn't look like time will ever catch up". Basically, incorporate a watchdog into it and force the user to handle the case where it takes too long.

Screenshots or pasted code

todo

Hardware and Software:

  • ggez version: 0.4.4 or 0.5.0-rc.X; check_update_time() shouldn't really be different between them
*GOOD FIRST ISSUE* Type-CODE bug

Most helpful comment

Easiest (and I think typical) way to solve this is to have max number of times check_update_time returns true for every update. This is essentially how Unity solves the issue as well (source): An update step (== check_update_time iteration) has a fixed dt, but the ~sum~ real world computation time of those steps should never be over a given max timespan per update, otherwise one runs into a feedback loop with more and more update steps to catch up per update.

Alternatively check_update_time could clamp residual_update_dt to zero if "current time spend updating" + "last render timer" took already more than the available time budget... but sounds to me like this gets quite tricky and hard to control 馃

All 5 comments

Easiest (and I think typical) way to solve this is to have max number of times check_update_time returns true for every update. This is essentially how Unity solves the issue as well (source): An update step (== check_update_time iteration) has a fixed dt, but the ~sum~ real world computation time of those steps should never be over a given max timespan per update, otherwise one runs into a feedback loop with more and more update steps to catch up per update.

Alternatively check_update_time could clamp residual_update_dt to zero if "current time spend updating" + "last render timer" took already more than the available time budget... but sounds to me like this gets quite tricky and hard to control 馃

...That's honestly a great idea, and simple to implement. Thank you!

Possible link to add as a reference: https://joetsoi.github.io/fix-your-timestep-rust-ggez/

The way examples in ggez devel currently treat #774 , which is to use ? to unwrap check_update_time(), exiting the application on failure, behaves undesirably in two cases:

  • Apps that initialize context, then do synchronous asset loading/initialization processing, and _then_ call event::run will register a long delay between the context initialization and the first update, which the timer perceives as requiring a "catch-up" of over 20 frames, resulting in an immediate crash
  • When I leave my computer idle for a long time and it goes to sleep or shuts off the screens or something, it prevents the app from updating for a long time, which also results in an accumulation of time on the timer, resulting in crash

I have two possible ideas as to how to fix this, possibly both:

  • Suggested handling should be .unwrap_or(false) instead of ?. This way, the application doesn't crash if it's updating too slow, but it does yield time back to the event handler, allowing other events to process, such as quit events.
  • Prevent the timer from accumulating too much time. Ignore time deltas that are too large to want the app to "catch up", such as 20 divided by the last requested FPS. This prevents the undesirable 'speed up' effect that occurs with the former solution, where the app updates 20x as fast for a while in order to try to "catch up".

I just removed the Result itself and undid the timer accumulator, since ? makes it wayyyyyyyy to easy for this to silently fail. Since failing means that the error gets caught by the event handler loop, a warning logged (iff you are capturing for log invocations), and the event loop stops cleanly, this is not desirable behavior. Moreover, apparently the accumulation of time can also occur when the game is running but not drawing anything... either 'cause it's off screen/minimized, or as seems to have happened to me with several of ggez's examples sometimes, everything's ready to go and running except the graphics context and it just displays the window for an instant and immediately exits.

This is currently what I use to deal with this problem in my own game's code, but as the comments say, it's not perfect:

impl event::EventHandler for MainState {
    fn update(&mut self, ctx: &mut Context) -> GameResult {
        let frame_duration: Duration = Duration::from_secs_f32(TARGET_FRAME_TIME);
        let update_start = Instant::now();

        // Run multiple updates if necessary to catch up
        // ...but not too many
        //
        // TODO: If the window is off screen or such for a long time this will try to catch
        // up on all the frames it missed
        // Make it pause while not selected or something.
        let mut max_updates = 5;
        while update_start > (self.last_update_time + frame_duration) {
            self.run_one_frame();

            self.last_update_time += frame_duration;
            let update_end = Instant::now();
            self.update_duration = update_end - update_start;
            max_updates -= 1;
            if max_updates == 0 {
                warn!("Did more than 5 updates in a row without drawing, reconsider your laggy life");
                break;
            }
        }
    }

I think in the end, solving this problem needs more sophisticated control flow than ggez can or should try to provide for you. Oh well.

Was this page helpful?
0 / 5 - 0 ratings