[gtk/gtk-3-24: 9/11] frame clock: adjust reported frame time



commit b80bc06b993f69f9c5686844b8d50242853257de
Author: Yariv Barkan <21448-yarivb users noreply gitlab gnome org>
Date:   Wed Jun 10 10:45:14 2020 +0300

    frame clock: adjust reported frame time
    
    When an animation is started while the application is idle, that often
    happens as a result of some external event. This can be an input event,
    an expired timer, data arriving over the network etc. The result is that
    the first animation clock cycle could be scheduled at some random time,
    as opposed to follow up cycles which are usually scheduled right after a
    vsync.
    
    Since the frame time we report to the application is correlated to the
    time when the frame clock was scheduled to run, this can result in
    uneven times reported in the first few animation frames. In order to fix
    that, we measure the phase of the first clock cycle - i.e. the offset
    between the first cycle and the preceding vsync. Once we start receiving
    "frame drawn" signals, the cadence of the frame clock scheduling becomes
    tied to the vsync. In order to maintain the regularity of the reported
    frame times, we adjust subsequent reported frame times with the
    aforementioned phase.

 gdk/gdkframeclockidle.c | 121 +++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 115 insertions(+), 6 deletions(-)
---
diff --git a/gdk/gdkframeclockidle.c b/gdk/gdkframeclockidle.c
index d93bb7407f..bbedd606f5 100644
--- a/gdk/gdkframeclockidle.c
+++ b/gdk/gdkframeclockidle.c
@@ -36,13 +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;                   /* 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;
@@ -378,6 +390,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)
 {
@@ -418,21 +449,88 @@ gdk_frame_clock_paint_idle (void *data)
 
               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:
+               *
+               *   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                                      ^------^
+               */
+              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 */
-                  priv->smoothed_frame_time_base = priv->frame_time;
-                  priv->smoothed_frame_time_period = frame_interval;
+                  /* 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
                 {
+                  /* compute_smooth_frame_time() ensures monotonicity */
                   priv->smoothed_frame_time_base =
-                      compute_smooth_frame_time (clock, priv->frame_time,
+                      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_period = frame_interval;
               priv->smoothed_frame_time_reported = priv->smoothed_frame_time_base;
 
               _gdk_frame_clock_begin_frame (clock);
@@ -562,7 +660,8 @@ 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->smoothed_frame_time_base);
+                                                               priv->smoothed_frame_time_base -
+                                                               priv->smoothed_frame_time_phase);
       maybe_start_idle (clock_idle, FALSE);
     }
 
@@ -598,6 +697,11 @@ 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, FALSE);
 }
@@ -613,6 +717,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)
     {


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]