首先注意一个名字上的区分,一开始看代码的人可能会对命名有疑惑。在Jonas的设计中,整个KMS相关代码分为两个上下文:main和impl。main上下文就是mutter运行的上下文,即GMainLoop所在线程,而impl上下文虽然目前也运行在GMainLoop所在线程,但是从设计上就是KMS操作运行的上下文,后期可能迁移到独立线程上。因此,所有命名为Impl的类都是注册在Impl上下文的。

MetaKms

该对象为简单容器,是Jonas为了实现transactional modesetting对KMS进行的抽象。transactional KMS最终的目的有两个:

  • 使mutter可以利用Atomic Modesetting API,更加充分高效的利用硬件特性,消除modesetting的中间状态
  • 使KMS API的调用主体可以为独立线程,本质上是允许KMS API的异步调用,即调用时

首先明确其抽象的目标,即transactional modesettingtransactional这个词与数据库中的意义一致。即对于KMS设备进行的modesetting是原子性的,没有中间状态,要么成功要么失败,失败时不会进行任何状态更新。因此MetaKms使用MetaKmsUpdate抽象一个transaction。这套抽象目前使用普通的KMS API实现,但是只要接口移植完毕,那么即可直接调用Atomic KMS实现对应的操作。因此MetaKms作为容器也有选择后端实现的功能,但是当前的实现都为普通KMS。

创建MetaKms对象时,需要传入一个MetaBackendmeta_kms_new函数会自行创建一个MetaKmsImpl对象:

1
2
3
  kms = g_object_new (META_TYPE_KMS, NULL);
  kms->backend = backend;
  kms->impl = META_KMS_IMPL (meta_kms_impl_simple_new (kms, error))

随后会从后端中取出MetaUdev对象,然后注册两个信号的处理函数,可以看出这是为了处理GPU热插拔事件:

1
2
3
4
5
  kms->hotplug_handler_id =
    g_signal_connect (udev, "hotplug", G_CALLBACK (on_udev_hotplug), kms);
  kms->removed_handler_id =
    g_signal_connect (udev, "device-removed",
                      G_CALLBACK (on_udev_device_removed), kms);

MetaKmsUpdate

这个结构体是transactional KMS API设计的核心。其核心思想是:

  • 对显示控制器的操作(即modesetting)transaction化
  • 每个MetaKmsUpdate即代表一个transaction,操作时将所有操作缓存到MetaKmsUpdate
  • 只有commit操作时,原先cache的操作才会真正应用到显示控制器中

因此,MetaKms会保存一个当前的MetaKmsUpdate,所有需要进行的KMS操作都需要缓存到其中,然后在合适的时机进行commit。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
struct _MetaKmsUpdate
{
  MetaKmsDevice *device;

  gboolean is_sealed;
  uint64_t sequence_number;

  MetaPowerSave power_save;
  GList *mode_sets;
  GList *plane_assignments;
  GList *connector_updates;
  GList *crtc_gammas;

  MetaKmsCustomPageFlipFunc custom_page_flip_func;
  gpointer custom_page_flip_user_data;

  GList *page_flip_listeners;
  GList *result_listeners;
};

在明白上述原理后,这个对象的表示也就比较容易理解了。首先is_sealed表示这个对象是否已经被封装好,即是否能够继续向其中添加操作。随后紧跟的是多个List,分别可以保存特定的一类操作。

MetaKms提供了多个接口,用于创建、获取,并commit一个MetaKmsUpdate

1
2
3
MetaKmsUpdate * meta_kms_ensure_pending_update (MetaKms *kms);
MetaKmsUpdate * meta_kms_get_pending_update (MetaKms *kms);
MetaKmsFeedback * meta_kms_post_pending_update_sync (MetaKms *kms)

因此,MetaKmsImpl的实现的功能就显而易见了,即负责真正执行一个MetaKmsUpdate

MetaKmsImplDevice

为了实现Atomic Modesetting,首先需要抽象一个KMS支持的操作。MetaKmsImplDevice抽象出了这个接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct _MetaKmsImplDeviceClass
{
  GObjectClass parent_class;

  void (* setup_drm_event_context) (MetaKmsImplDevice *impl,
                                    drmEventContext   *drm_event_context);
  MetaKmsFeedback * (* process_update) (MetaKmsImplDevice *impl,
                                        MetaKmsUpdate     *update);
  void (* handle_page_flip_callback) (MetaKmsImplDevice   *impl,
                                      MetaKmsPageFlipData *page_flip_data);
  void (* discard_pending_page_flips) (MetaKmsImplDevice *impl);
};

并由MetaKmsImplDeviceSimpleMetaKmsImplDeviceAtomic实现。显然handle_page_flip_callbackdiscard_pending_page_flipsprocess被直接当作虚函数导出。另一个非常重要的操作是dispatch,实现如下:

1
2
3
4
5
6
7
  drm_event_context = (drmEventContext) { 0 };
  klass->setup_drm_event_context (impl_device, &drm_event_context);

  while (TRUE)
    {
      if (drmHandleEvent (priv->fd, &drm_event_context) != 0)
        {

可以看出,首先使用setup_drm_event_context回调函数设置好drmEventContext,本质上就是page flip的处理函数,在随后的drmHandleEvent中,如果出现PageFlip事件,则会调用MetaKmsImplDeviceXXX中设置好的Page flip处理函数进行处理。

除此之外比较有意思的是这个类保存了所有的DRM设备属性,也就是connectorCRTC等KMS接口导出的对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
typedef struct _MetaKmsImplDevicePrivate
{
  MetaKmsDevice *device;
  MetaKmsImpl *impl;

  int fd;
  GSource *fd_source;
  char *path;

  GList *crtcs;
  GList *connectors;
  GList *planes;

  MetaKmsDeviceCaps caps;

  GList *fallback_modes;
} MetaKmsImplDevicePrivate;

可以在该类型的初始化函数中得到对应的初始化方式,且该类型的初始化函数中还隐藏了比较重要的信息:一个GSource,如下:

1
2
3
4
  priv->fd_source =
    meta_kms_register_fd_in_impl (meta_kms_impl_get_kms (priv->impl), priv->fd,
                                  kms_event_dispatch_in_impl,
                                  impl_device);

本质上是在GMainContext中注册了一个GSource,以DRM的文件描述符的可读状态为事件源,以传入的kms_event_dispatch_in_impl函数作为dispatch回调函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static gpointer
kms_event_dispatch_in_impl (MetaKmsImpl  *impl,
                            gpointer      user_data,
                            GError      **error)
{
  MetaKmsImplDevice *impl_device = user_data;
  gboolean ret;

  ret = meta_kms_impl_device_dispatch (impl_device, error);
  return GINT_TO_POINTER (ret);
}

而在该函数中调用了meta_kms_impl_device_dispatch函数,除此之外别无它地。上述即为Page Flip的处理机制,从这里看,MetaKmsImplDevice一旦创建,即会注册Page Flip的处理函数,并进行处理。

MetaKmsImplDeviceSimple

~~这个是目前mutter唯一实现的MetaKmsImpl,~~实质上是用普通KMS(即非atomic)操作实现transacational KMS的接口。可以预见,后续mutter项目会使用Atomic KMS接口实现一个MetaKmsImplAtomic,实现事实意义上的atomic KMS支持。

这里分析MetaKmsImplSimple几个比较复杂的操作。首先明确入口为meta_kms_impl_simple_process_update函数,其内部连续处理缓存在MetaKmsUpdate中的KMS操作。简单的操作只是简单的wrap一个drmMode*函数,可以略过,直接分析最复杂的PageFlip处理。先看其缓存在MetaKmsUpdate中的形式:

1
2
3
4
5
6
7
8
typedef struct _MetaKmsPageFlip
{
  MetaKmsCrtc *crtc;
  const MetaKmsPageFlipFeedback *feedback;
  gpointer user_data;
  MetaKmsCustomPageFlipFunc custom_page_flip_func;
  gpointer custom_page_flip_user_data;
} MetaKmsPageFlip;

先理解drmModePageFlip函数,该函数与drmModeSetCrtc函数非常相似,只不过其只有在VBlank事件来了时才生效,也就是告诉DRM当VBlank事件到来时,更新framebuffer。我们可以将函数的flags参数传入DRM_MODE_PAGE_FLIP_EVENT,此时每当PageFlip发生时,都会产生一个PageFlip事件。事件通过drm文件描述符变为可读告知用户态,且我们可以使用drmHandleEvent处理事件。注意drmModePageFlip的最后一个参数为user_data,一个用户态指针,每当PageFlip事件生成时,对应的指针就会跟着传入drmHandleEvent函数,所以我们可以通过该指针辨别PageFlip。

接下来看process_page_flip函数,该函数是PageFlip处理的核心。首先可以知道MetaKmsPageFlip中提供了一个让调用方自己写PageFlip处理函数的机制,即对应于custom_page_flip_func函数指针和其对应的数据。当他们存在时,就通过它们进行PageFlip的处理,反之则使用标准的处理流程,即调用drmModePageFlip函数:

1
2
3
4
5
      ret = drmModePageFlip (fd,
                             meta_kms_crtc_get_id (crtc),
                             plane_assignment->fb_id,
                             DRM_MODE_PAGE_FLIP_EVENT,
                             meta_kms_page_flip_data_ref (page_flip_data));

page_flip_data究竟记录了什么,目前不需要知道,到时候在分析mutter frame调度器的时候进行分析,目前只看到:

1
2
3
4
  page_flip_data = meta_kms_page_flip_data_new (impl,
                                                crtc,
                                                page_flip->feedback,
                                                page_flip->user_data);

注意drmModePageFlip函数在一个VBlank期间只能调用一次,在已经调用过一次的情况下再继续调用的话会返回-EBUSY。可以看到process_page_flip函数在该情况下实现了一个缓存机制,将返回-EBUSY的调用重新调度到下一次VBlank。这里只需要看到它是将多余的PageFlip计算出一个时间间隔,并缓存到了一张表内,其余细节在分析frame调度时再分析。

MetaKmsImplDeviceAtomic

半年之后jonas终于把Atomic Modesetting的支持做完了,不过熟悉GNOME的人都应该懂这个特性起码review半年以上,也就是mutter 40都不一定可以合入。前面提到Atomic Modesetting的支持就差最后一个Buffer,以前我对于DRM的理解没有那么深刻,结果误解了这个Buffer的含义。这个Buffer实质上是指Buffer更新,即设置CRTC的scanout。因此,需要定义一个PlaneAssignment用于抽象这个操作。

Atomic Modesetting的实质是使用Atomic API替换掉原有的legacy接口,需要重新实现一个MetaKmsImplDevice,也就是MetaKmsImplDeviceAtomic。该函数的核心操作就是process_update,在进行这个操作时,首先自己是否已经初始化,如果没有初始化,则进行以下操作:

  • MetaKmsDevice里管理的所有connector的CRTC_ID属性设置为0
  • MetaKmsDevice里管理的所有plane的CRTC_IDFB_ID设置为0

随后则进行update操作,如下:

  • 依次处理MetaKmsUpdate中保存的信息,本质上就是更改对应Object里的属性
  • 然后commit

MetaKmsDevice

没意思。

有意思了,原先没有看到PageFlip事件的处理在MetaKmsDevice里,单纯以为其是drm设备文件描述符的容器。漏掉了最重要的函数meta_kms_device_dispatch_sync。先分析通用实现,然后再看MetaKmsImplDevice

可以看到函数首先调用MetaKmsImplmeta_kms_impl_dispatch_idle函数,后续调用了MetaKmsImplDevice实现的dispatch函数,中间穿插了meta_kms_flush_callback的调用。

MetaCursorRenderer

来看个比较独立也看起来比较简单的东西吧:鼠标光标绘制。首先看通用的抽象即MetaCursorRender。首先明确光标绘制的两种方式,硬件光标和软件光标。首先说软件光标,这个比较好理解,即直接在屏幕上进行光标的绘制,与绘制窗口无异。硬件光标则不同,目前的显示控制器一般都实现了cursor图层(plane),即屏幕的主图层和cursor图层是相互独立的,只有在scanout的时候才由硬件进行叠加操作。cursor图层一般支持绘制一块比较小的bitmap到屏幕上,而该bimtap可以在屏幕上快速移动。但是硬件实现的cursor图层是有局限性的,特定情况下并不能启用,所以mutter需要管理硬件cursor的启用。

事实上通过简单思考即抽象出Cursor管理需要向外提供的接口:

  • 光标(相对屏幕)位置的设置和获取
  • 硬件光标的启用与停用管理
  • 光标位图设置
  • 光标更新(绘制)

mutter使用MetaCursorRender抽象cursor的绘制器,而使用MetaCursorSprite描述光标图片(注意,这个可以是动画)。为了管理硬件cursor的启用条件,定义了MetaHwCursorInhibitor接口,实现了该接口的对象可以提供一个函数用于确定是否可以启用硬件cursor。

可以看到MetaCursorRender向外提供的接口如下:

  • set_curosr/get_cursor,更换和获取`MetaCursorSprite(并绘制)
  • set_position/get_position,更该和获取光标位置(并绘制)
  • force_update,强制重新绘制光标
  • {is,add,remove}_hw_cursor_inhibitor,向其注册删除MetaHwCursorInhibitor

先来看一下绘制光标的抽象接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  if (cursor_sprite)
    meta_cursor_sprite_prepare_at (cursor_sprite,
                                   (int) priv->current_x,
                                   (int) priv->current_y);

  handled_by_backend =
    META_CURSOR_RENDERER_GET_CLASS (renderer)->update_cursor (renderer,
                                                              cursor_sprite);
  if (handled_by_backend != priv->handled_by_backend)
    {
      priv->handled_by_backend = handled_by_backend;
      should_redraw = TRUE;
    }

这是meta_cursor_renderer_update_cursor中的片段。其大致逻辑就是,如果子类实现的update_cursor函数没有处理光标绘制,则交由MetaCursorRenderer实现的软件绘制器进行光标绘制。

MetaDrmBuffer

Buffer管理应该是transacational KMS最后一个还没有做的部分了。很明显一个MetaDrmBuffer对应一个DRM Buffer,且其只提供一个虚函数:get_fb_id。现在看到这个类有三个实现:

  • DUMB,这个就是那个几乎所有DRM驱动都支持的DUMB Buffer,不支持GPU加速,不看
  • GBM,这个应该是本地GPU通过libgbm分配出来的Buffer
  • Import,这个是其他GPU通过prime接口导出的Buffer,这个也暂时不看

MetaDrmBuffer是一个抽象类,由于上面的DUMBImport类型的Buffer都没有进一步分析的必要,所以这里主要分析MetaDrmBufferGbm。首先明确这个Buffer对应的是DRM中的Buffer概念,且是使用libgbm分配而出的Buffer,所以这一套API与EGL和DRM高度相关。MetaDrmBufferGbm提供了两个创建方法:

  • new_lock_front。该方法对应EGL中的lock front buffer操作,本质是锁定一个gbm_surface中的一个后端Buffer然后将其返回供用户操作。一旦完成操作则需要将该Buffer返还gbm_surface并解除锁定。
  • new_take。直接将一个gbm_bo包装成一个MetaDrmBufferGbm

无论使用哪个方法进行创建,都需要使用meta_gpu_kms_add_fb将整个buffer注册到DRM中,并得到一个framebuffer id并保存起来。

MetaRendererNative

原来一直没看懂这个东西存在的必要,现在明白了。MetaRenderer的功能是维护一个MetaRendererView列表,同时提供创建CoglRenderer的功能。为了理解它,首先明确整个MutterClutterStage是只有一个的,也就是哪怕有多个显示器,整个Session中也是公用一个ClutterStage的。在Clutter将整个桌面渲染出来之后,需要将其进行拆分,并对应到所有的显示器中。对于Native后端来说,上述操作就是多个CRTC的scanout设置到同一个Buffer的不同区域中。真正负责将输出渲染(Render)到逻辑显示器上的就是MetaRandererView,而MetaRenderer则负责管理MetaRenderer上的显示器。

MetaRendererViewNative

MetaRendererViewClutterStageViewCogl的子类,提供了有限的接口,大部分需要其子类实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
enum
{
  PROP_0,

  PROP_TRANSFORM,
  PROP_CRTC,

  PROP_LAST
};

static GParamSpec *obj_props[PROP_LAST];

struct _MetaRendererView
{
  ClutterStageViewCogl parent;

  MetaMonitorTransform transform;

  MetaCrtc *crtc;
};

简单包含了两个属性:CRTC对应的ID以及显示器变换。

TODO: 涉及到COGL,以及ClutterStageView,后面再看