diff options
author | Alexander Larsson <alexl@redhat.com> | 2020-06-15 10:49:41 +0000 |
---|---|---|
committer | Alexander Larsson <alexl@redhat.com> | 2020-06-15 10:49:41 +0000 |
commit | 7638bcc8e9ad461cc455f50db3a956848e1f656f (patch) | |
tree | 1f51ca2f312510b99d3c478c6818098ca423a05d | |
parent | 40fc27fd55077150c78f6bf877cea049d67b2167 (diff) | |
parent | cf91cf4825754b2412a44365ad1e033d661d92e9 (diff) | |
download | gtk+-7638bcc8e9ad461cc455f50db3a956848e1f656f.tar.gz |
Merge branch 'frame-clock-alternative-approach' into 'gtk-3-24'
Frame clock alternative approach
See merge request GNOME/gtk!1931
-rw-r--r-- | gdk/gdkframeclock.c | 36 | ||||
-rw-r--r-- | gdk/gdkframeclockidle.c | 308 | ||||
-rw-r--r-- | gdk/gdkframeclockprivate.h | 1 |
3 files changed, 264 insertions, 81 deletions
diff --git a/gdk/gdkframeclock.c b/gdk/gdkframeclock.c index a482354d7e..6f5e9cc869 100644 --- a/gdk/gdkframeclock.c +++ b/gdk/gdkframeclock.c @@ -505,11 +505,15 @@ _gdk_frame_clock_debug_print_timings (GdkFrameClock *clock, GString *str; gint64 previous_frame_time = 0; + gint64 previous_smoothed_frame_time = 0; GdkFrameTimings *previous_timings = gdk_frame_clock_get_timings (clock, timings->frame_counter - 1); if (previous_timings != NULL) - previous_frame_time = previous_timings->frame_time; + { + previous_frame_time = previous_timings->frame_time; + previous_smoothed_frame_time = previous_timings->smoothed_frame_time; + } str = g_string_new (""); @@ -518,6 +522,9 @@ _gdk_frame_clock_debug_print_timings (GdkFrameClock *clock, { g_string_append_printf (str, " interval=%-4.1f", (timings->frame_time - previous_frame_time) / 1000.); g_string_append_printf (str, timings->slept_before ? " (sleep)" : " "); + g_string_append_printf (str, " smoothed=%4.1f / %-4.1f", + (timings->smoothed_frame_time - timings->frame_time) / 1000., + (timings->smoothed_frame_time - previous_smoothed_frame_time) / 1000.); } if (timings->layout_start_time != 0) g_string_append_printf (str, " layout_start=%-4.1f", (timings->layout_start_time - timings->frame_time) / 1000.); @@ -525,6 +532,8 @@ _gdk_frame_clock_debug_print_timings (GdkFrameClock *clock, g_string_append_printf (str, " paint_start=%-4.1f", (timings->paint_start_time - timings->frame_time) / 1000.); if (timings->frame_end_time != 0) g_string_append_printf (str, " frame_end=%-4.1f", (timings->frame_end_time - timings->frame_time) / 1000.); + if (timings->drawn_time != 0) + g_string_append_printf (str, " drawn=%-4.1f", (timings->drawn_time - timings->frame_time) / 1000.); if (timings->presentation_time != 0) g_string_append_printf (str, " present=%-4.1f", (timings->presentation_time - timings->frame_time) / 1000.); if (timings->predicted_presentation_time != 0) @@ -566,16 +575,12 @@ gdk_frame_clock_get_refresh_info (GdkFrameClock *frame_clock, gint64 *presentation_time_return) { gint64 frame_counter; + gint64 default_refresh_interval = DEFAULT_REFRESH_INTERVAL; g_return_if_fail (GDK_IS_FRAME_CLOCK (frame_clock)); frame_counter = gdk_frame_clock_get_frame_counter (frame_clock); - if (presentation_time_return) - *presentation_time_return = 0; - if (refresh_interval_return) - *refresh_interval_return = DEFAULT_REFRESH_INTERVAL; - while (TRUE) { GdkFrameTimings *timings = gdk_frame_clock_get_timings (frame_clock, frame_counter); @@ -583,19 +588,21 @@ gdk_frame_clock_get_refresh_info (GdkFrameClock *frame_clock, gint64 refresh_interval; if (timings == NULL) - return; + break; refresh_interval = timings->refresh_interval; presentation_time = timings->presentation_time; + if (refresh_interval == 0) + refresh_interval = default_refresh_interval; + else + default_refresh_interval = refresh_interval; + if (presentation_time != 0) { if (presentation_time > base_time - MAX_HISTORY_AGE && presentation_time_return) { - if (refresh_interval == 0) - refresh_interval = DEFAULT_REFRESH_INTERVAL; - if (refresh_interval_return) *refresh_interval_return = refresh_interval; @@ -604,13 +611,20 @@ gdk_frame_clock_get_refresh_info (GdkFrameClock *frame_clock, if (presentation_time_return) *presentation_time_return = presentation_time; + + return; } - return; + break; } frame_counter--; } + + if (presentation_time_return) + *presentation_time_return = 0; + if (refresh_interval_return) + *refresh_interval_return = default_refresh_interval; } void diff --git a/gdk/gdkframeclockidle.c b/gdk/gdkframeclockidle.c index 8f17d7596f..89f5823a72 100644 --- a/gdk/gdkframeclockidle.c +++ b/gdk/gdkframeclockidle.c @@ -36,10 +36,25 @@ #define FRAME_INTERVAL 16667 /* microseconds */ +typedef enum { + SMOOTH_PHASE_STATE_VALID = 0, /* explicit, since we count on zero-init */ + SMOOTH_PHASE_STATE_AWAIT_FIRST, + SMOOTH_PHASE_STATE_AWAIT_DRAWN, +} SmoothDeltaState; + struct _GdkFrameClockIdlePrivate { - gint64 frame_time; - gint64 min_next_frame_time; + gint64 frame_time; /* The exact time we last ran the clock cycle, or 0 if never */ + gint64 smoothed_frame_time_base; /* A grid-aligned version of frame_time (grid size == refresh period), never more than half a grid from frame_time */ + gint64 smoothed_frame_time_period; /* The grid size that smoothed_frame_time_base is aligned to */ + gint64 smoothed_frame_time_reported; /* Ensures we are always monotonic */ + gint64 smoothed_frame_time_phase; /* The offset of the first reported frame time, in the current animation sequence, from the preceding vsync */ + gint64 min_next_frame_time; /* We're not synced to vblank, so wait at least until this before next cycle to avoid busy looping */ + SmoothDeltaState smooth_phase_state; /* The state of smoothed_frame_time_phase - is it valid, awaiting vsync etc. Thanks to zero-init, the initial value + of smoothed_frame_time_phase is `0`. This is valid, since we didn't get a "frame drawn" event yet. Accordingly, + the initial value of smooth_phase_state is SMOOTH_PHASE_STATE_VALID. See the comment in gdk_frame_clock_paint_idle() + for details. */ + gint64 sleep_serial; #ifdef G_ENABLE_DEBUG gint64 freeze_time; @@ -54,6 +69,7 @@ struct _GdkFrameClockIdlePrivate GdkFrameClockPhase phase; guint in_paint_idle : 1; + guint paint_is_thaw : 1; #ifdef G_OS_WIN32 guint begin_period : 1; #endif @@ -124,8 +140,8 @@ gdk_frame_clock_idle_init (GdkFrameClockIdle *frame_clock_idle) frame_clock_idle->priv = priv = gdk_frame_clock_idle_get_instance_private (frame_clock_idle); - priv->frame_time = g_get_monotonic_time (); /* more sane than zero */ priv->freeze_count = 0; + priv->smoothed_frame_time_period = FRAME_INTERVAL; } static void @@ -156,44 +172,107 @@ gdk_frame_clock_idle_dispose (GObject *object) G_OBJECT_CLASS (gdk_frame_clock_idle_parent_class)->dispose (object); } +/* Note: This is never called on first frame, so + * smoothed_frame_time_base != 0 and we have a valid frame_interval. */ static gint64 -compute_frame_time (GdkFrameClockIdle *idle) +compute_smooth_frame_time (GdkFrameClock *clock, + gint64 new_frame_time, + gboolean new_frame_time_is_vsync_related, + gint64 smoothed_frame_time_base, + gint64 frame_interval) { - GdkFrameClockIdlePrivate *priv = idle->priv; - gint64 computed_frame_time; - - computed_frame_time = g_get_monotonic_time (); + GdkFrameClockIdlePrivate *priv = GDK_FRAME_CLOCK_IDLE (clock)->priv; + int frames_passed; + gint64 new_smoothed_time; + gint64 current_error; + gint64 correction_magnitude; + + /* Consecutive frame, assume it is an integer number of frames later, so round to nearest such */ + /* NOTE: This is >= 0, because smoothed_frame_time_base is < frame_interval/2 from old_frame_time + * and new_frame_time >= old_frame_time. */ + frames_passed = (new_frame_time - smoothed_frame_time_base + frame_interval / 2) / frame_interval; + + /* We use an approximately whole number of frames in the future from + * last smoothed frame time. This way we avoid minor jitter in the + * frame times making the animation speed uneven, but still animate + * evenly in case of whole frame skips. */ + new_smoothed_time = smoothed_frame_time_base + frames_passed * frame_interval; + + /* However, sometimes the smoothed time is too much off from the + * real time. For example, if the first frame clock cycle happened + * not due to a frame rendering but an input event, then + * new_frame_time could happen to be near the middle between two + * frames. If that happens and we then start regularly animating at + * the refresh_rate, then the jitter in the real time may cause us + * to randomly sometimes round up, and sometimes down. + * + * To combat this we converge the smooth time towards the real time + * in a way that is slow when they are near and fast when they are + * far from each other. + * + * This is done by using the square of the error as the correction + * magnitude. I.e. if the error is 0.5 frame, we correct by + * 0.5*0.5=0.25 frame, if the error is 0.25 we correct by 0.125, if + * the error is 0.1, frame we correct by 0.01 frame, etc. + * + * The actual computation is: + * (current_error/frame_interval)*(current_error/frame_interval)*frame_interval + * But this can be simplified as below. + * + * Note: We only do this correction if the new frame is caused by a + * thaw of the frame clock, so that we know the time is actually + * related to the physical vblank. For frameclock cycles triggered + * by other events we always step up in whole frames from the last + * reported time. + */ + if (new_frame_time_is_vsync_related) + { + current_error = new_smoothed_time - new_frame_time; + correction_magnitude = current_error * current_error / frame_interval; /* Note, this is always > 0 due to the square */ + if (current_error > 0) + new_smoothed_time -= correction_magnitude; + else + new_smoothed_time += correction_magnitude; + } - /* ensure monotonicity of frame time */ - if (computed_frame_time <= priv->frame_time) - computed_frame_time = priv->frame_time + 1; + /* Ensure we're always monotonic */ + if (new_smoothed_time <= priv->smoothed_frame_time_reported) + new_smoothed_time = priv->smoothed_frame_time_reported; - return computed_frame_time; + return new_smoothed_time; } static gint64 gdk_frame_clock_idle_get_frame_time (GdkFrameClock *clock) { GdkFrameClockIdlePrivate *priv = GDK_FRAME_CLOCK_IDLE (clock)->priv; - gint64 computed_frame_time; + gint64 now; + gint64 new_smoothed_time; /* can't change frame time during a paint */ if (priv->phase != GDK_FRAME_CLOCK_PHASE_NONE && - priv->phase != GDK_FRAME_CLOCK_PHASE_FLUSH_EVENTS) - return priv->frame_time; + priv->phase != GDK_FRAME_CLOCK_PHASE_FLUSH_EVENTS && + (priv->phase != GDK_FRAME_CLOCK_PHASE_BEFORE_PAINT || priv->in_paint_idle)) + return priv->smoothed_frame_time_base; - /* Outside a paint, pick something close to "now" */ - computed_frame_time = compute_frame_time (GDK_FRAME_CLOCK_IDLE (clock)); + /* Outside a paint, pick something smoothed close to now */ + now = g_get_monotonic_time (); - /* 16ms is 60fps. We only update frame time that often because we'd - * like to try to keep animations on the same start times. - * get_frame_time() would normally be used outside of a paint to - * record an animation start time for example. - */ - if ((computed_frame_time - priv->frame_time) > FRAME_INTERVAL) - priv->frame_time = computed_frame_time; + /* First time frame, just return something */ + if (priv->smoothed_frame_time_base == 0) + { + priv->smoothed_frame_time_reported = now; + return now; + } + + /* Since time is monotonic this is <= what we will pick for the next cycle, but + more likely than not it will be equal if we're doing a constant animation. */ + new_smoothed_time = compute_smooth_frame_time (clock, now, FALSE, + priv->smoothed_frame_time_base, + priv->smoothed_frame_time_period); - return priv->frame_time; + priv->smoothed_frame_time_reported = new_smoothed_time; + return new_smoothed_time; } #define RUN_FLUSH_IDLE(priv) \ @@ -211,7 +290,8 @@ gdk_frame_clock_idle_get_frame_time (GdkFrameClock *clock) (priv)->updating_count > 0)) static void -maybe_start_idle (GdkFrameClockIdle *clock_idle) +maybe_start_idle (GdkFrameClockIdle *clock_idle, + gboolean caused_by_thaw) { GdkFrameClockIdlePrivate *priv = clock_idle->priv; @@ -221,7 +301,7 @@ maybe_start_idle (GdkFrameClockIdle *clock_idle) if (priv->min_next_frame_time != 0) { - gint64 now = compute_frame_time (clock_idle); + gint64 now = g_get_monotonic_time (); gint64 min_interval_us = MAX (priv->min_next_frame_time, now) - now; min_interval = (min_interval_us + 500) / 1000; } @@ -239,6 +319,7 @@ maybe_start_idle (GdkFrameClockIdle *clock_idle) if (!priv->in_paint_idle && priv->paint_idle_id == 0 && RUN_PAINT_IDLE (priv)) { + priv->paint_is_thaw = caused_by_thaw; priv->paint_idle_id = gdk_threads_add_timeout_full (GDK_PRIORITY_REDRAW, min_interval, gdk_frame_clock_paint_idle, @@ -267,23 +348,6 @@ maybe_stop_idle (GdkFrameClockIdle *clock_idle) } } -static gint64 -compute_min_next_frame_time (GdkFrameClockIdle *clock_idle, - gint64 last_frame_time) -{ - gint64 presentation_time; - gint64 refresh_interval; - - gdk_frame_clock_get_refresh_info (GDK_FRAME_CLOCK (clock_idle), - last_frame_time, - &refresh_interval, &presentation_time); - - if (presentation_time == 0) - return last_frame_time + refresh_interval; - else - return presentation_time + refresh_interval / 2; -} - static gboolean gdk_frame_clock_flush_idle (void *data) { @@ -310,6 +374,25 @@ gdk_frame_clock_flush_idle (void *data) return FALSE; } +/* + * Returns the positive remainder. + * + * As an example, lets consider (-5) % 16: + * + * (-5) % 16 = (0 * 16) + (-5) = -5 + * + * If we only want positive remainders, we can instead calculate + * + * (-5) % 16 = (1 * 16) + (-5) = 11 + * + * The built-in `%` operator returns the former, positive_modulo() returns the latter. + */ +static int +positive_modulo (int i, int n) +{ + return (i % n + n) % n; +} + static gboolean gdk_frame_clock_paint_idle (void *data) { @@ -343,39 +426,103 @@ gdk_frame_clock_paint_idle (void *data) if (priv->freeze_count == 0) { gint64 frame_interval = FRAME_INTERVAL; - gint64 reset_frame_time; - gint64 smoothest_frame_time; - gint64 frame_time_error; - GdkFrameTimings *prev_timings = - gdk_frame_clock_get_current_timings (clock); + GdkFrameTimings *prev_timings = gdk_frame_clock_get_current_timings (clock); if (prev_timings && prev_timings->refresh_interval) frame_interval = prev_timings->refresh_interval; - /* We are likely not getting precisely even callbacks in real - * time, particularly if the event loop is busy. - * This is a documented limitation in the precision of - * gdk_threads_add_timeout_full and g_timeout_add_full. + priv->frame_time = g_get_monotonic_time (); + + /* + * The first clock cycle of an animation might have been triggered by some external event. An external + * event can be an input event, an expired timer, data arriving over the network etc. This can happen at + * any time, so the cycle could have been scheduled at some random time rather then immediately after a + * frame completion. The offset between the start of the first animation cycle and the preceding vsync is + * called the "phase" of the clock cycle start time (not to be confused with the phase of the frame + * clock). + * + * In this first clock cycle, the "smooth" frame time is simply the time when the cycle was started. This + * could be followed by several cycles which are not vsync-related. As long as we don't get a "frame + * drawn" signal from the compositor, the clock cycles will occur every about frame_interval. Once we do + * get a "frame drawn" signal, from this point on the frame clock cycles will start shortly after the + * corresponding vsync signals, again every about frame_interval. The first vsync-related clock cycle + * might occur less than a refresh interval away from the last non-vsync-related cycle. See the diagram + * below for details. So while the cadence stays the same - a frame clock cycle every about frame_interval + * - the phase of the cycles start time has changed. + * + * Since we might have already reported the frame time to the application in the previous clock cycles, we + * have to adjust future reported frame times. We want the first vsync-related smooth time to be separated + * by exactly 1 frame_interval from the previous one, in order to maintain the regularity of the reported + * frame times. To achieve that, from this point on we add the phase of the first clock cycle start time to + * the smooth time. In order to compute that phase, accounting for possible skipped frames (e.g. due to + * compositor stalls), we want the following to be true: + * + * first_vsync_smooth_time = last_non_vsync_smooth_time + frame_interval * (1 + frames_skipped) + * + * We can assign the following known/desired values to the above equation: + * + * last_non_vsync_smooth_time = smoothed_frame_time_base + * first_vsync_smooth_time = frame_time + smoothed_frame_time_phase + * + * That leads us to the following, from which we can extract smoothed_frame_time_phase: * - * In order to avoid this imprecision from compounding between - * frames and affecting visual smoothness, we correct frame_time - * to more precisely match the even refresh interval of the - * physical display. This also means we proactively avoid (most) - * missed frames before they occur. + * frame_time + smoothed_frame_time_phase = smoothed_frame_time_base + + * frame_interval * (1 + frames_skipped) + * + * In the following diagram, '|' mark a vsync, '*' mark the start of a clock cycle, '+' is the adjusted + * frame time, '!' marks the reception of "frame drawn" events from the compositor. Note that the clock + * cycle cadence changed after the first vsync-related cycle. This cadence is kept even if we don't + * receive a 'frame drawn' signal in a subsequent frame, since then we schedule the clock at intervals of + * refresh_interval. + * + * vsync | | | | | |... + * frame drawn | | |! |! | |... + * cycle start | * | * |* |* |* |... + * adjusted times | * | * | + | + | + |... + * phase ^------^ */ - smoothest_frame_time = priv->frame_time + frame_interval; - reset_frame_time = compute_frame_time (clock_idle); - frame_time_error = ABS (reset_frame_time - smoothest_frame_time); - if (frame_time_error >= frame_interval) - priv->frame_time = reset_frame_time; + if (priv->smooth_phase_state == SMOOTH_PHASE_STATE_AWAIT_FIRST) + { + /* First animation cycle - usually unrelated to vsync */ + priv->smoothed_frame_time_base = 0; + priv->smoothed_frame_time_phase = 0; + priv->smooth_phase_state = SMOOTH_PHASE_STATE_AWAIT_DRAWN; + } + else if (priv->smooth_phase_state == SMOOTH_PHASE_STATE_AWAIT_DRAWN && + priv->paint_is_thaw) + { + /* First vsync-related animation cycle, we can now compute the phase. We want the phase to satisfy + 0 <= phase < frame_interval */ + priv->smoothed_frame_time_phase = + positive_modulo (priv->smoothed_frame_time_base - priv->frame_time, + frame_interval); + priv->smooth_phase_state = SMOOTH_PHASE_STATE_VALID; + } + + if (priv->smoothed_frame_time_base == 0) + { + /* First frame ever, or first cycle in a new animation sequence. Ensure monotonicity */ + priv->smoothed_frame_time_base = MAX (priv->frame_time, priv->smoothed_frame_time_reported); + } else - priv->frame_time = smoothest_frame_time; + { + /* compute_smooth_frame_time() ensures monotonicity */ + priv->smoothed_frame_time_base = + compute_smooth_frame_time (clock, priv->frame_time + priv->smoothed_frame_time_phase, + priv->paint_is_thaw, + priv->smoothed_frame_time_base, + priv->smoothed_frame_time_period); + } + + priv->smoothed_frame_time_period = frame_interval; + priv->smoothed_frame_time_reported = priv->smoothed_frame_time_base; _gdk_frame_clock_begin_frame (clock); /* Note "current" is different now so timings != prev_timings */ timings = gdk_frame_clock_get_current_timings (clock); timings->frame_time = priv->frame_time; + timings->smoothed_frame_time = priv->smoothed_frame_time_base; timings->slept_before = priv->sleep_serial != get_sleep_serial (); priv->phase = GDK_FRAME_CLOCK_PHASE_BEFORE_PAINT; @@ -496,9 +643,20 @@ gdk_frame_clock_paint_idle (void *data) */ if (priv->freeze_count == 0) { - priv->min_next_frame_time = compute_min_next_frame_time (clock_idle, - priv->frame_time); - maybe_start_idle (clock_idle); + /* + * If we don't receive "frame drawn" events, smooth_cycle_start will simply be advanced in constant increments of + * the refresh interval. That way we get absolute target times for the next cycles, which should prevent skewing + * in the scheduling of the frame clock. + * + * Once we do receive "frame drawn" events, smooth_cycle_start will track the vsync, and do so in a more stable + * way compared to frame_time. If we then no longer receive "frame drawn" events, smooth_cycle_start will again be + * simply advanced in increments of the refresh interval, but this time we are in sync with the vsync. If we start + * receiving "frame drawn" events shortly after loosing them, then we should still be in sync. + */ + gint64 smooth_cycle_start = priv->smoothed_frame_time_base - priv->smoothed_frame_time_phase; + priv->min_next_frame_time = smooth_cycle_start + priv->smoothed_frame_time_period; + + maybe_start_idle (clock_idle, FALSE); } if (priv->freeze_count == 0) @@ -515,7 +673,7 @@ gdk_frame_clock_idle_request_phase (GdkFrameClock *clock, GdkFrameClockIdlePrivate *priv = clock_idle->priv; priv->requested |= phase; - maybe_start_idle (clock_idle); + maybe_start_idle (clock_idle, FALSE); } static void @@ -533,8 +691,13 @@ gdk_frame_clock_idle_begin_updating (GdkFrameClock *clock) } #endif + if (priv->updating_count == 0) + { + priv->smooth_phase_state = SMOOTH_PHASE_STATE_AWAIT_FIRST; + } + priv->updating_count++; - maybe_start_idle (clock_idle); + maybe_start_idle (clock_idle, FALSE); } static void @@ -548,6 +711,11 @@ gdk_frame_clock_idle_end_updating (GdkFrameClock *clock) priv->updating_count--; maybe_stop_idle (clock_idle); + if (priv->updating_count == 0) + { + priv->smooth_phase_state = SMOOTH_PHASE_STATE_VALID; + } + #ifdef G_OS_WIN32 if (priv->updating_count == 0 && priv->begin_period) { @@ -586,7 +754,7 @@ gdk_frame_clock_idle_thaw (GdkFrameClock *clock) priv->freeze_count--; if (priv->freeze_count == 0) { - maybe_start_idle (clock_idle); + maybe_start_idle (clock_idle, TRUE); /* If nothing is requested so we didn't start an idle, we need * to skip to the end of the state chain, since the idle won't * run and do it for us. diff --git a/gdk/gdkframeclockprivate.h b/gdk/gdkframeclockprivate.h index d568887ba3..1ace7cf102 100644 --- a/gdk/gdkframeclockprivate.h +++ b/gdk/gdkframeclockprivate.h @@ -89,6 +89,7 @@ struct _GdkFrameTimings gint64 frame_counter; guint64 cookie; gint64 frame_time; + gint64 smoothed_frame_time; gint64 drawn_time; gint64 presentation_time; gint64 refresh_interval; |