本文尝试分析Clutter代码实现,其目的是获取调试Mutter代码bug的能力。Clutter的基本知识可以从网络获取,后面会贴一些基础文档。Clutter是mutter的基本组成部件,如果不能正确理解clutter,那么就无法理解mutter对屏幕进行的绘制。

首先clutter绘制时要考虑多显示器的支持,ClutterStage是一个大的坐标系,或者说绘制区域,而我们需要一块区域来表示显示器输出的区域,这就是ClutterStageViewClutterActor作为一个舞台上的"演员",是一个2D的材质,被放到了ClutterStage所表示的3D坐标系中。其中camera位于坐标原点,且方向为(0,0,-1),即向下看。

参考资料 - Mutter Wiki

ClutterStage

从抽象的概念上看,ClutterStage是最高一层的可见对象,这句话是指上是说整个渲染节点是组织成树状的,而ClutterStage就是它的根节点。从这个意义上来说,很明显ClutterStage是一个特殊的ClutterActor,因此在Clutter中被实现为ClutterActor的一个子类。

clutter_stage_paint_view

clutter_stage_init

ClutterStage并没有_new函数,说明Mutter中并不直接使用这个类,而是使用它的子类。从_init函数中可以看到,ClutterStage中保存了一个后端ClutterStageWindow,该对象由ClutterBackend创建。viewport也是从ClutterStageWindow中获取的,但是很明显viewport就是整个渲染区域(单屏幕下应该为屏幕大小)。

_clutter_stage_queue_actor_redraw

分析ClutterActor的redraw操作时见到了该函数,其本质就是将ClutterActor的更新请求排列到redraw队列上。这个redraw队列的元素如下:

1
2
3
4
5
6
struct _ClutterStageQueueRedrawEntry
{
  ClutterActor *actor;
  gboolean has_clip;
  ClutterPaintVolume clip;
};

函数所作的第一件事就是检查pending_finish_queue_redraws标志,如果为false,则会对当前ClutterStage上外挂的所有ClutterStageView执行schedule_update操作,然后将标志置为true。该操作的潜在逻辑就是一旦pending_queue_redraws队列上有元素加入,说明整个ClutterStageView需要更新,所以要调度起一个update操作(具体到Native后端,即一次屏幕的刷新)。这里注意如果有两个显示器,那么两个显示器的刷新是会互相干涉的,后面的VRR实现应该要改变这一行为,然显示器不受其他显示器上才显示的ClutterActor的影响。

后面的操作则简单,首先明确一个ClutterActor在这个queue里只能有一个entry,该entry被保存在ClutterActor中。当ClutterActor调用该函数时,需要将entry传入,即函数的entry参数。如果entryNULL,则该ClutterActor不在redraw队列中,需要重新创建ClutterStageQueueRedrawEntry。反之,则需要对entry进行更新。接下来的分析应该围绕这个队列stage->priv->pending_queue_redraws进行。

clutter_stage_maybe_finish_queue_redraw

进一步搜索之后,发现stage->priv->pending_queue_redraws只有这个函数用到了,这个函数就是对所有ClutterActor进行重绘制的函数。函数开头与_clutter_stage_queue_actor_redraw对应,一旦发现pending_finish_queue_redrawsFALSE,说明没有ClutterActor需要重新绘制,则直接返回。反之,则将其置为FALSE,然后进行进一步的处理。

函数末尾本质就是将pending_queue_redraws队列中的元素一个一个取下,然后依次调用_clutter_actor_finish_queue_redraw函数。也就是说,当ClutterActor需要进行更新的时候,Clutter并不是直接调用后端对其进行更新,而是先将该请求缓存起来,最后在ClutterStageView需要更新的时候一同进行更新,这样可以避免一些重复操作。可以看到,调用clutter_stage_maybe_finish_queue_redraw函数的直接位置就是ClutterStageViewClutterFrameClockframe回调函数。

ClutterFrameClock

注意这个对象是为了实现多显示器使用独立刷新率时实现的对象,本质表示一个显示器的FrameClock。ClutterFrameClock直接继承GObject,与其一同定义的还有一组接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
typedef struct _ClutterFrameListenerIface
{
  void (* before_frame) (ClutterFrameClock *frame_clock,
                         int64_t            frame_count,
                         gpointer           user_data);
  ClutterFrameResult (* frame) (ClutterFrameClock *frame_clock,
                                int64_t            frame_count,
                                int64_t            time_us,
                                gpointer           user_data);
} ClutterFrameListenerIface;

创建ClutterFrameClock时,需要传入该对象:

1
2
3
4
CLUTTER_EXPORT
ClutterFrameClock * clutter_frame_clock_new (float                            refresh_rate,
                                             const ClutterFrameListenerIface *iface,
                                             gpointer                         user_data);

clutter_frame_clock_new

该函数创建一个ClutterFrameClock对象。ClutterFrameClock的init函数非常简单,如下:

1
2
3
4
5
static void
clutter_frame_clock_init (ClutterFrameClock *frame_clock)
{
  frame_clock->state = CLUTTER_FRAME_CLOCK_STATE_INIT;
}

clutter_frame_clock_new函数会调用g_object_new创建对象,然后将参数保存在对象中,最后调用init_frame_clock_source进行初始化操作。事件驱动编程的核心就是事件源与对应的处理,和明显ClutterFrameClock可以作为一个事件源,init_frame_clock_source函数的核心就是创建这样一个事件源(GSource),然后加入到当前线程的事件循环中。可以看到,这个GSourceGSourceFuncsframe_clock_source_funcs

1
2
3
4
5
6
static GSourceFuncs frame_clock_source_funcs = {
  NULL,
  NULL,
  frame_clock_source_dispatch,
  NULL
};

看到preparecheck都为NULL,这说明preparecheck都默认为NULL。事实上这个GSource使用Ready Time触发dispatch操作。dispatch函数最终会调用clutter_frame_clock_dispatch函数,后面进行分析。

clutter_frame_clock_schedule_update

该函数用于调度起一次更新操作,需要注意ClutterFrameClock本身是一个状态机,有如下状态:

1
2
3
4
5
6
7
8
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;

在不同状态下,调用clutter_frame_clock_schedule_update所在成的效果是不同的。ClutterFrameClock刚被创建时,其状态为INIT,则调用该函数时如下:

1
2
3
4
5
  switch (frame_clock->state)
    {
    case CLUTTER_FRAME_CLOCK_STATE_INIT:
      next_update_time_us = g_get_monotonic_time ();
      break;

其中next_update_time_us为后面设置GSourceReady Time时的事件戳,把其设置为当前时间,也就是立即进行dispatch操作的意思。而IDLE状态时,会计算该时间戳:

1
2
3
4
5
    case CLUTTER_FRAME_CLOCK_STATE_IDLE:
      calculate_next_update_time_us (frame_clock,
                                     &next_update_time_us,
                                     &frame_clock->next_presentation_time_us);
      frame_clock->is_next_presentation_time_valid = TRUE;

计算方式比较复杂,最后进行分析。如果当前为SCHEDULED状态,即GSource已经设置好了Ready Time,正在等待dispatch,则直接返回。对于DISPATCHINGPENDING_PRESENTED状态,则设置pending_reschedule标志,然后返回。对于IDLEINIT,计算完next_update_time_us后将会设置对应的Ready Time,然后将状态设置为SCHEDULED

clutter_frame_clock_dispatch

前面提到了这个函数是对应的dispatch函数。函数第一件做的事情就是清空Ready Time,停止dispatch后续的触发,然后将ClutterFrameClock的状态设置为DISPATCHING。随后函数自增记录frame产生个数的计数器,然后调用创建ClutterFrameClock时传入的before_frame回掉函数。接下来更新ClutterFrameClock中注册的ClutterTimeline对象,然后调用frame回调函数。函数末尾会根据frame回调函数返回的值决定ClutterFrameClock的状态:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    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;
          maybe_reschedule_update (frame_clock);
          break;
        }

ClutterStageView

只有Mutter中的Clutter有该类。从Mutter的角度来看,ClutterStageView将一个ClutterStage的特定区域渲染到一个屏幕区域,其实质上承担的任务为多显示器的输出工作。除此之外,ClutterStageView还持有前后端的FrameBuffer,并且在最近的改动中管理FrameClock。

ClutterStageView是一个GObject,作为基类被具体实现的子类继承。

Frame Clock回调函数

前面提到了ClutterStageView内部包含了一个ClutterFrameClock,我们可以在该对象的创建函数中看到:

1
2
3
  priv->frame_clock = clutter_frame_clock_new (priv->refresh_rate,
                                               &frame_clock_listener_iface,
                                               view);

现在来分析其注册的两个回调函数:

1
2
3
4
static const ClutterFrameListenerIface frame_clock_listener_iface = {
  .before_frame = handle_frame_clock_before_frame,
  .frame = handle_frame_clock_frame,
};

其中before_frame函数做的事情比较单一,就是处理当前ClutterStageView等待处理的事件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static void
handle_frame_clock_before_frame (ClutterFrameClock *frame_clock,
                                 int64_t            frame_count,
                                 gpointer           user_data)
{
  ClutterStageView *view = user_data;
  ClutterStageViewPrivate *priv =
    clutter_stage_view_get_instance_private (view);

  _clutter_stage_process_queued_events (priv->stage);
}

然后frame回调函数的处理就相当复杂了,毕竟是整个Mutter渲染的核心驱动,涉及了大量的Clutter实现细节。

TODO

ClutterStageViewCogl

这里只关注redraw_view回调函数。可以看到ClutterStageViewCogl是实现了ClutterStageWindow接口,且前面发现在ClutterStageViewframe回调函数中调用了_clutter_stage_window_redraw_view函数,这里就来分析这个函数。ClutterStageViewCogl实现的接口如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static void
clutter_stage_window_iface_init (ClutterStageWindowInterface *iface)
{
  iface->realize = clutter_stage_cogl_realize;
  iface->unrealize = clutter_stage_cogl_unrealize;
  iface->get_wrapper = clutter_stage_cogl_get_wrapper;
  iface->resize = clutter_stage_cogl_resize;
  iface->show = clutter_stage_cogl_show;
  iface->hide = clutter_stage_cogl_hide;
  iface->get_frame_counter = clutter_stage_cogl_get_frame_counter;
  iface->redraw_view = clutter_stage_cogl_redraw_view;
}

clutter_stage_cogl_redraw_view进行了scanout操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  scanout = clutter_stage_view_take_scanout(view);
  if (scanout)
  {
    g_autoptr(GError) error = NULL;

    if (clutter_stage_cogl_scanout_view(stage_cogl, view, scanout, &error))
      return;

    g_warning("Failed to scan out client buffer: %s", error->message);
  }

该操作直接调用cogl_onscreen_direct_scanout,进而调用原先在Native后端从winsys注册的onscreen_direct_scanout函数指针~~,进而调用Native后端的相关代码,进行pageflip~~。这里是direct_scanout,目测是unredirection模式,即直接将一个buffer的内容scanout到显示器上。之后,调用clutter_stage_cogl_redraw_view_primary函数。

clutter_stage_cogl_redraw_view_primary

该函数应该就是主要的redraw函数。负责重新绘制整个显示器的framebuffer。函数传入的参数为ClutterStageCoglClutterStageView,也就是所有与更新相关的信息都记录在了ClutterStageView中。函数首先获取framebuffer信息:

1
2
3
4
  clutter_stage_view_get_layout (view, &view_rect);
  fb_scale = clutter_stage_view_get_scale (view);
  fb_width = cogl_framebuffer_get_width (fb);
  fb_height = cogl_framebuffer_get_height (fb);

ClutterStageView中保存了一个redraw_clip

1
  redraw_clip = clutter_stage_view_take_redraw_clip (view);

如果redraw_clip为空,则会进行full_redraw也就是全屏重新绘制。

ClutterActor

clutter_actor_queue_redraw

分析这个函数的目的是想搞明白一个ClutterActor是如何绘制到屏幕上的。从函数名称看该函数将对应的ClutterActor排到redraw队列,等待下一次绘制。这里就需要搞明白:

  • 这个绘制队列究竟是什么
  • 什么时候进行绘制
  • 绘制的原理是什么样子的

函数本质直接调用_clutter_actor_queue_redraw_full,且提供的ClutterPaintVolumeClutterEffect都是NULL。我们知道每一个ClutterActor都是有parent的,最顶层的parrent就是ClutterStage,函数获取到ClutterStage之后,调用了_clutter_stage_queue_actor_redraw

1
2
3
4
5
  self->priv->queue_redraw_entry =
    _clutter_stage_queue_actor_redraw (CLUTTER_STAGE (stage),
                                       priv->queue_redraw_entry,
                                       self,
                                       volume);

而该函数的操作可以在ClutterStage的分析中看到。由于前面看到CluterEffectNULL,则函数直接将自己的is_dirty属性设置为TRUE。至此,函数结束,可以看到函数最大的工作还是委托给了ClutterStage进行。

_clutter_actor_finish_queue_redraw

ClutterStage的分析中可以看到,该函数与clutter_actor_queue_redraw不同,是直接进行ClutterActor重绘制的函数。函数首先清空priv->queue_redraw_entry,毕竟函数被调用时,pending_queue_redraws队列正在进行清空操作。