diff --git a/clutter/clutter/clutter-frame-clock.c b/clutter/clutter/clutter-frame-clock.c new file mode 100644 index 000000000..a05436ae4 --- /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 . + */ + +#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 000000000..a7fee3dca --- /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 . + */ + +#ifndef CLUTTER_FRAME_CLOCK_H +#define CLUTTER_FRAME_CLOCK_H + +#include +#include +#include + +#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 23636c11c..0a2670937 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 000000000..cf548e2c4 --- /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 81b6922f0..07b82ddfd 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',