[mutter] clutter: Add a simple frame clock base



commit 96a108ed4e4027d4e08891e172fb0634ab42535a
Author: Jonas Ã…dahl <jadahl gmail com>
Date:   Fri Mar 6 23:28:51 2020 +0100

    clutter: Add a simple frame clock base
    
    This adds a current unused, apart from tests, frame clock. It just
    reschedules given a refresh rate, based on presentation time feedback.
    
    The aiming for it is to be used with a single frame listener (stage
    views) that will notify when a frame is presented. It does not aim to
    handle multiple frame listeners, instead, it's assumed that different
    frame listeners will use their own frame clocks.
    
    Also add a test that verifies that the basic functionality works.
    
    https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/1285

 clutter/clutter/clutter-frame-clock.c   | 319 ++++++++++++++++++++++++++++++++
 clutter/clutter/clutter-frame-clock.h   |  58 ++++++
 clutter/clutter/meson.build             |   2 +
 src/tests/clutter/conform/frame-clock.c | 163 ++++++++++++++++
 src/tests/clutter/conform/meson.build   |   1 +
 5 files changed, 543 insertions(+)
---
diff --git a/clutter/clutter/clutter-frame-clock.c b/clutter/clutter/clutter-frame-clock.c
new file mode 100644
index 0000000000..a05436ae4d
--- /dev/null
+++ b/clutter/clutter/clutter-frame-clock.c
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2019 Red Hat Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "clutter-build-config.h"
+
+#include "clutter/clutter-frame-clock.h"
+
+#include "clutter/clutter-main.h"
+
+static inline uint64_t
+us (uint64_t us)
+{
+  return us;
+}
+
+static inline uint64_t
+ms2us (uint64_t ms)
+{
+  return us (ms * 1000);
+}
+
+/* Wait 2ms after vblank before starting to draw next frame */
+#define SYNC_DELAY_US ms2us (2)
+
+typedef struct _ClutterFrameListener
+{
+  const ClutterFrameListenerIface *iface;
+  gpointer user_data;
+} ClutterFrameListener;
+
+typedef struct _ClutterClockSource
+{
+  GSource source;
+
+  ClutterFrameClock *frame_clock;
+} ClutterClockSource;
+
+typedef enum _ClutterFrameClockState
+{
+  CLUTTER_FRAME_CLOCK_STATE_INIT,
+  CLUTTER_FRAME_CLOCK_STATE_IDLE,
+  CLUTTER_FRAME_CLOCK_STATE_SCHEDULED,
+  CLUTTER_FRAME_CLOCK_STATE_DISPATCHING,
+  CLUTTER_FRAME_CLOCK_STATE_PENDING_PRESENTED,
+} ClutterFrameClockState;
+
+struct _ClutterFrameClock
+{
+  GObject parent;
+
+  float refresh_rate;
+  ClutterFrameListener listener;
+
+  GSource *source;
+
+  int64_t frame_count;
+
+  ClutterFrameClockState state;
+  int64_t last_presentation_time_us;
+
+  gboolean pending_reschedule;
+};
+
+G_DEFINE_TYPE (ClutterFrameClock, clutter_frame_clock,
+               G_TYPE_OBJECT)
+
+void
+clutter_frame_clock_notify_presented (ClutterFrameClock *frame_clock,
+                                      int64_t            presentation_time_us)
+{
+  if (presentation_time_us > frame_clock->last_presentation_time_us ||
+      ((presentation_time_us - frame_clock->last_presentation_time_us) >
+       INT64_MAX / 2))
+    {
+      frame_clock->last_presentation_time_us = presentation_time_us;
+    }
+  else
+    {
+      g_warning_once ("Bogus presentation time %" G_GINT64_FORMAT
+                      " travelled back in time, using current time.",
+                      presentation_time_us);
+      frame_clock->last_presentation_time_us = g_get_monotonic_time ();
+    }
+
+  switch (frame_clock->state)
+    {
+    case CLUTTER_FRAME_CLOCK_STATE_INIT:
+    case CLUTTER_FRAME_CLOCK_STATE_IDLE:
+    case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED:
+      g_warn_if_reached ();
+      break;
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHING:
+    case CLUTTER_FRAME_CLOCK_STATE_PENDING_PRESENTED:
+      if (frame_clock->pending_reschedule)
+        {
+          frame_clock->pending_reschedule = FALSE;
+          clutter_frame_clock_schedule_update (frame_clock);
+        }
+      else
+        {
+          frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_IDLE;
+        }
+      break;
+    }
+}
+
+static void
+calculate_next_update_time_us (ClutterFrameClock *frame_clock,
+                               int64_t           *out_next_update_time_us)
+{
+  int64_t last_presentation_time_us;
+  int64_t now_us;
+  float refresh_rate;
+  int64_t refresh_interval_us;
+  int64_t min_render_time_allowed_us;
+  int64_t max_render_time_allowed_us;
+  int64_t next_presentation_time_us;
+  int64_t next_update_time_us;
+
+  now_us = g_get_monotonic_time ();
+
+  refresh_rate = frame_clock->refresh_rate;
+  refresh_interval_us = (int64_t) (0.5 + G_USEC_PER_SEC / refresh_rate);
+
+  min_render_time_allowed_us = refresh_interval_us / 2;
+  max_render_time_allowed_us = refresh_interval_us - SYNC_DELAY_US;
+
+  if (min_render_time_allowed_us > max_render_time_allowed_us)
+    min_render_time_allowed_us = max_render_time_allowed_us;
+
+  last_presentation_time_us = frame_clock->last_presentation_time_us;
+  next_presentation_time_us = last_presentation_time_us + refresh_interval_us;
+
+  /* Skip ahead to get close to the actual next presentation time. */
+  if (next_presentation_time_us < now_us)
+    {
+      int64_t logical_clock_offset_us;
+      int64_t logical_clock_phase_us;
+      int64_t hw_clock_offset_us;
+
+      logical_clock_offset_us = now_us % refresh_interval_us;
+      logical_clock_phase_us = now_us - logical_clock_offset_us;
+      hw_clock_offset_us = last_presentation_time_us % refresh_interval_us;
+
+      next_presentation_time_us = logical_clock_phase_us + hw_clock_offset_us;
+    }
+
+  while (next_presentation_time_us < now_us + min_render_time_allowed_us)
+    next_presentation_time_us += refresh_interval_us;
+
+  next_update_time_us = next_presentation_time_us - max_render_time_allowed_us;
+
+  *out_next_update_time_us = next_update_time_us;
+}
+
+void
+clutter_frame_clock_schedule_update (ClutterFrameClock *frame_clock)
+{
+  int64_t next_update_time_us = -1;
+
+  switch (frame_clock->state)
+    {
+    case CLUTTER_FRAME_CLOCK_STATE_INIT:
+      next_update_time_us = g_get_monotonic_time ();
+      break;
+    case CLUTTER_FRAME_CLOCK_STATE_IDLE:
+      calculate_next_update_time_us (frame_clock, &next_update_time_us);
+      break;
+    case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED:
+      return;
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHING:
+    case CLUTTER_FRAME_CLOCK_STATE_PENDING_PRESENTED:
+      frame_clock->pending_reschedule = TRUE;
+      return;
+    }
+
+  g_warn_if_fail (next_update_time_us != -1);
+
+  g_source_set_ready_time (frame_clock->source, next_update_time_us);
+  frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_SCHEDULED;
+}
+
+static gboolean
+clutter_frame_clock_dispatch (gpointer user_data)
+{
+  ClutterFrameClock *frame_clock = user_data;
+  ClutterFrameResult result;
+
+  g_source_set_ready_time (frame_clock->source, -1);
+
+  frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_DISPATCHING;
+
+  result = frame_clock->listener.iface->frame (frame_clock,
+                                               frame_clock->frame_count++,
+                                               frame_clock->listener.user_data);
+
+  switch (frame_clock->state)
+    {
+    case CLUTTER_FRAME_CLOCK_STATE_INIT:
+    case CLUTTER_FRAME_CLOCK_STATE_PENDING_PRESENTED:
+      g_warn_if_reached ();
+      break;
+    case CLUTTER_FRAME_CLOCK_STATE_IDLE:
+    case CLUTTER_FRAME_CLOCK_STATE_SCHEDULED:
+      break;
+    case CLUTTER_FRAME_CLOCK_STATE_DISPATCHING:
+      switch (result)
+        {
+        case CLUTTER_FRAME_RESULT_PENDING_PRESENTED:
+          frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_PENDING_PRESENTED;
+          break;
+        case CLUTTER_FRAME_RESULT_IDLE:
+          frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_IDLE;
+          break;
+        }
+      break;
+    }
+
+  return G_SOURCE_CONTINUE;
+}
+
+static gboolean
+frame_clock_source_dispatch (GSource     *source,
+                             GSourceFunc  callback,
+                             gpointer     user_data)
+{
+  return callback (user_data);
+}
+
+static GSourceFuncs frame_clock_source_funcs = {
+  NULL,
+  NULL,
+  frame_clock_source_dispatch,
+  NULL
+};
+
+static void
+init_frame_clock_source (ClutterFrameClock *frame_clock)
+{
+  GSource *source;
+  ClutterClockSource *clock_source;
+  g_autofree char *name = NULL;
+
+  source = g_source_new (&frame_clock_source_funcs, sizeof (ClutterClockSource));
+  clock_source = (ClutterClockSource *) source;
+
+  name = g_strdup_printf ("Clutter frame clock (%p)", frame_clock);
+  g_source_set_name (source, name);
+  g_source_set_priority (source, CLUTTER_PRIORITY_REDRAW);
+  g_source_set_can_recurse (source, FALSE);
+  g_source_set_callback (source, clutter_frame_clock_dispatch, frame_clock, NULL);
+  clock_source->frame_clock = frame_clock;
+
+  frame_clock->source = source;
+  g_source_attach (source, NULL);
+}
+
+ClutterFrameClock *
+clutter_frame_clock_new (float                            refresh_rate,
+                         const ClutterFrameListenerIface *iface,
+                         gpointer                         user_data)
+{
+  ClutterFrameClock *frame_clock;
+
+  g_assert_cmpfloat (refresh_rate, >, 0.0);
+
+  frame_clock = g_object_new (CLUTTER_TYPE_FRAME_CLOCK, NULL);
+
+  frame_clock->listener.iface = iface;
+  frame_clock->listener.user_data = user_data;
+
+  init_frame_clock_source (frame_clock);
+
+  frame_clock->refresh_rate = refresh_rate;
+
+  return frame_clock;
+}
+
+static void
+clutter_frame_clock_finalize (GObject *object)
+{
+  ClutterFrameClock *frame_clock = CLUTTER_FRAME_CLOCK (object);
+
+  if (frame_clock->source)
+    {
+      g_source_destroy (frame_clock->source);
+      g_source_unref (frame_clock->source);
+    }
+
+  G_OBJECT_CLASS (clutter_frame_clock_parent_class)->finalize (object);
+}
+
+static void
+clutter_frame_clock_init (ClutterFrameClock *frame_clock)
+{
+  frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_INIT;
+}
+
+static void
+clutter_frame_clock_class_init (ClutterFrameClockClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = clutter_frame_clock_finalize;
+}
diff --git a/clutter/clutter/clutter-frame-clock.h b/clutter/clutter/clutter-frame-clock.h
new file mode 100644
index 0000000000..a7fee3dcaf
--- /dev/null
+++ b/clutter/clutter/clutter-frame-clock.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2019 Red Hat Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CLUTTER_FRAME_CLOCK_H
+#define CLUTTER_FRAME_CLOCK_H
+
+#include <glib.h>
+#include <glib-object.h>
+#include <stdint.h>
+
+#include "clutter/clutter.h"
+
+typedef enum _ClutterFrameResult
+{
+  CLUTTER_FRAME_RESULT_PENDING_PRESENTED,
+  CLUTTER_FRAME_RESULT_IDLE,
+} ClutterFrameResult;
+
+#define CLUTTER_TYPE_FRAME_CLOCK (clutter_frame_clock_get_type ())
+CLUTTER_EXPORT
+G_DECLARE_FINAL_TYPE (ClutterFrameClock, clutter_frame_clock,
+                      CLUTTER, FRAME_CLOCK,
+                      GObject)
+
+typedef struct _ClutterFrameListenerIface
+{
+  ClutterFrameResult (* frame) (ClutterFrameClock *frame_clock,
+                                int64_t            frame_count,
+                                gpointer           user_data);
+} ClutterFrameListenerIface;
+
+CLUTTER_EXPORT
+ClutterFrameClock * clutter_frame_clock_new (float                            refresh_rate,
+                                             const ClutterFrameListenerIface *iface,
+                                             gpointer                         user_data);
+
+CLUTTER_EXPORT
+void clutter_frame_clock_notify_presented (ClutterFrameClock *frame_clock,
+                                           int64_t            presentation_time_us);
+
+CLUTTER_EXPORT
+void clutter_frame_clock_schedule_update (ClutterFrameClock *frame_clock);
+
+#endif /* CLUTTER_FRAME_CLOCK_H */
diff --git a/clutter/clutter/meson.build b/clutter/clutter/meson.build
index 23636c11c8..0a26709377 100644
--- a/clutter/clutter/meson.build
+++ b/clutter/clutter/meson.build
@@ -122,6 +122,7 @@ clutter_sources = [
   'clutter-fixed-layout.c',
   'clutter-flatten-effect.c',
   'clutter-flow-layout.c',
+  'clutter-frame-clock.c',
   'clutter-gesture-action.c',
   'clutter-graphene.c',
   'clutter-grid-layout.c',
@@ -191,6 +192,7 @@ clutter_private_headers = [
   'clutter-effect-private.h',
   'clutter-event-private.h',
   'clutter-flatten-effect.h',
+  'clutter-frame-clock.h',
   'clutter-graphene.h',
   'clutter-gesture-action-private.h',
   'clutter-id-pool.h',
diff --git a/src/tests/clutter/conform/frame-clock.c b/src/tests/clutter/conform/frame-clock.c
new file mode 100644
index 0000000000..cf548e2c4e
--- /dev/null
+++ b/src/tests/clutter/conform/frame-clock.c
@@ -0,0 +1,163 @@
+#include "clutter/clutter-frame-clock.h"
+#include "tests/clutter-test-utils.h"
+
+static const float refresh_rate = 60.0;
+static const int64_t refresh_interval_us = (int64_t) (0.5 + G_USEC_PER_SEC /
+                                                      refresh_rate);
+
+static int64_t test_frame_count;
+static int64_t expected_frame_count;
+
+typedef struct _FakeHwClock
+{
+  GSource source;
+
+  ClutterFrameClock *frame_clock;
+
+  int64_t next_presentation_time_us;
+  gboolean has_pending_present;
+} FakeHwClock;
+
+typedef struct _FrameClockTest
+{
+  FakeHwClock *fake_hw_clock;
+
+  GMainLoop *main_loop;
+} FrameClockTest;
+
+static gboolean
+fake_hw_clock_source_dispatch (GSource     *source,
+                               GSourceFunc  callback,
+                               gpointer     user_data)
+{
+  FakeHwClock *fake_hw_clock = (FakeHwClock *) source;
+  ClutterFrameClock *frame_clock = fake_hw_clock->frame_clock;
+
+  if (fake_hw_clock->has_pending_present)
+    {
+      fake_hw_clock->has_pending_present = FALSE;
+      clutter_frame_clock_notify_presented (frame_clock,
+                                            g_source_get_time (source));
+      if (callback)
+        callback (user_data);
+    }
+
+  fake_hw_clock->next_presentation_time_us += refresh_interval_us;
+  g_source_set_ready_time (source, fake_hw_clock->next_presentation_time_us);
+
+  return G_SOURCE_CONTINUE;
+}
+
+static GSourceFuncs fake_hw_clock_source_funcs = {
+  NULL,
+  NULL,
+  fake_hw_clock_source_dispatch,
+  NULL
+};
+
+static FakeHwClock *
+fake_hw_clock_new (ClutterFrameClock *frame_clock,
+                   GSourceFunc        callback,
+                   gpointer           user_data)
+{
+  GSource *source;
+  FakeHwClock *fake_hw_clock;
+
+  source = g_source_new (&fake_hw_clock_source_funcs, sizeof (FakeHwClock));
+  fake_hw_clock = (FakeHwClock *) source;
+  fake_hw_clock->frame_clock = frame_clock;
+
+  fake_hw_clock->next_presentation_time_us =
+    g_get_monotonic_time () + refresh_interval_us;
+  g_source_set_ready_time (source, fake_hw_clock->next_presentation_time_us);
+  g_source_set_callback (source, callback, user_data, NULL);
+
+  return fake_hw_clock;
+}
+
+static ClutterFrameResult
+frame_clock_frame (ClutterFrameClock *frame_clock,
+                   int64_t            frame_count,
+                   gpointer           user_data)
+{
+  FrameClockTest *test = user_data;
+  GMainLoop *main_loop = test->main_loop;
+
+  g_assert_cmpint (frame_count, ==, expected_frame_count);
+
+  expected_frame_count++;
+
+  if (test_frame_count == 0)
+    {
+      g_main_loop_quit (main_loop);
+      return CLUTTER_FRAME_RESULT_IDLE;
+    }
+  else
+    {
+      test->fake_hw_clock->has_pending_present = TRUE;
+    }
+
+  test_frame_count--;
+
+  return CLUTTER_FRAME_RESULT_PENDING_PRESENTED;
+}
+
+static const ClutterFrameListenerIface frame_listener_iface = {
+  .frame = frame_clock_frame,
+};
+
+static gboolean
+schedule_update_hw_callback (gpointer user_data)
+{
+  ClutterFrameClock *frame_clock = user_data;
+
+  clutter_frame_clock_schedule_update (frame_clock);
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+frame_clock_schedule_update (void)
+{
+  FrameClockTest test;
+  ClutterFrameClock *frame_clock;
+  int64_t before_us;
+  int64_t after_us;
+  GSource *source;
+  FakeHwClock *fake_hw_clock;
+
+  test_frame_count = 10;
+  expected_frame_count = 0;
+
+  test.main_loop = g_main_loop_new (NULL, FALSE);
+  frame_clock = clutter_frame_clock_new (refresh_rate,
+                                         &frame_listener_iface,
+                                         &test);
+
+  fake_hw_clock = fake_hw_clock_new (frame_clock,
+                                     schedule_update_hw_callback,
+                                     frame_clock);
+  source = &fake_hw_clock->source;
+  g_source_attach (source, NULL);
+
+  test.fake_hw_clock = fake_hw_clock;
+
+  before_us = g_get_monotonic_time ();
+
+  clutter_frame_clock_schedule_update (frame_clock);
+  g_main_loop_run (test.main_loop);
+
+  after_us = g_get_monotonic_time ();
+
+  g_assert_cmpint (after_us - before_us, >, 10 * refresh_interval_us);
+
+  g_main_loop_unref (test.main_loop);
+
+  g_object_unref (frame_clock);
+  g_source_destroy (source);
+  g_source_unref (source);
+}
+
+CLUTTER_TEST_SUITE (
+  CLUTTER_TEST_UNIT ("/frame-clock/schedule-update", frame_clock_schedule_update)
+)
diff --git a/src/tests/clutter/conform/meson.build b/src/tests/clutter/conform/meson.build
index 81b6922f0f..07b82ddfd7 100644
--- a/src/tests/clutter/conform/meson.build
+++ b/src/tests/clutter/conform/meson.build
@@ -31,6 +31,7 @@ clutter_conform_tests_classes_tests = [
 clutter_conform_tests_general_tests = [
   'binding-pool',
   'color',
+  'frame-clock',
   'interval',
   'script-parser',
   'timeline',


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