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
This commit is contained in:
Jonas Ådahl 2020-03-06 23:28:51 +01:00
parent da633dcc52
commit 96a108ed4e
5 changed files with 543 additions and 0 deletions

View File

@ -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;
}

View File

@ -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 */

View File

@ -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',

View File

@ -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)
)

View File

@ -31,6 +31,7 @@ clutter_conform_tests_classes_tests = [
clutter_conform_tests_general_tests = [
'binding-pool',
'color',
'frame-clock',
'interval',
'script-parser',
'timeline',