Clutter代码分析
文章目录
本文尝试分析Clutter代码实现,其目的是获取调试Mutter代码bug的能力。Clutter的基本知识可以从网络获取,后面会贴一些基础文档。Clutter是mutter的基本组成部件,如果不能正确理解clutter,那么就无法理解mutter对屏幕进行的绘制。
首先clutter绘制时要考虑多显示器的支持,ClutterStage是一个大的坐标系,或者说绘制区域,而我们需要一块区域来表示显示器输出的区域,这就是ClutterStageView
。ClutterActor
作为一个舞台上的"演员",是一个2D的材质,被放到了ClutterStage
所表示的3D坐标系中。其中camera位于坐标原点,且方向为(0,0,-1),即向下看。
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队列的元素如下:
|
|
函数所作的第一件事就是检查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
参数。如果entry
为NULL
,则该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_redraws
为FALSE
,说明没有ClutterActor
需要重新绘制,则直接返回。反之,则将其置为FALSE
,然后进行进一步的处理。
函数末尾本质就是将pending_queue_redraws
队列中的元素一个一个取下,然后依次调用_clutter_actor_finish_queue_redraw
函数。也就是说,当ClutterActor
需要进行更新的时候,Clutter
并不是直接调用后端对其进行更新,而是先将该请求缓存起来,最后在ClutterStageView
需要更新的时候一同进行更新,这样可以避免一些重复操作。可以看到,调用clutter_stage_maybe_finish_queue_redraw
函数的直接位置就是ClutterStageView
中ClutterFrameClock
的frame
回调函数。
ClutterFrameClock
注意这个对象是为了实现多显示器使用独立刷新率时实现的对象,本质表示一个显示器的FrameClock。ClutterFrameClock
直接继承GObject,与其一同定义的还有一组接口:
|
|
创建ClutterFrameClock
时,需要传入该对象:
|
|
clutter_frame_clock_new
该函数创建一个ClutterFrameClock
对象。ClutterFrameClock
的init函数非常简单,如下:
|
|
clutter_frame_clock_new
函数会调用g_object_new
创建对象,然后将参数保存在对象中,最后调用init_frame_clock_source
进行初始化操作。事件驱动编程的核心就是事件源与对应的处理,和明显ClutterFrameClock
可以作为一个事件源,init_frame_clock_source
函数的核心就是创建这样一个事件源(GSource),然后加入到当前线程的事件循环中。可以看到,这个GSource
的GSourceFuncs
为frame_clock_source_funcs
:
|
|
看到prepare
和check
都为NULL,这说明prepare
和check
都默认为NULL。事实上这个GSource
使用Ready Time
触发dispatch操作。dispatch函数最终会调用clutter_frame_clock_dispatch
函数,后面进行分析。
clutter_frame_clock_schedule_update
该函数用于调度起一次更新操作,需要注意ClutterFrameClock
本身是一个状态机,有如下状态:
|
|
在不同状态下,调用clutter_frame_clock_schedule_update
所在成的效果是不同的。ClutterFrameClock
刚被创建时,其状态为INIT
,则调用该函数时如下:
|
|
其中next_update_time_us
为后面设置GSource
的Ready Time
时的事件戳,把其设置为当前时间,也就是立即进行dispatch
操作的意思。而IDLE
状态时,会计算该时间戳:
|
|
计算方式比较复杂,最后进行分析。如果当前为SCHEDULED
状态,即GSource
已经设置好了Ready Time
,正在等待dispatch,则直接返回。对于DISPATCHING
和PENDING_PRESENTED
状态,则设置pending_reschedule
标志,然后返回。对于IDLE
和INIT
,计算完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
的状态:
|
|
ClutterStageView
只有Mutter
中的Clutter
有该类。从Mutter
的角度来看,ClutterStageView
将一个ClutterStage
的特定区域渲染到一个屏幕区域,其实质上承担的任务为多显示器的输出工作。除此之外,ClutterStageView
还持有前后端的FrameBuffer,并且在最近的改动中管理FrameClock。
ClutterStageView
是一个GObject,作为基类被具体实现的子类继承。
Frame Clock回调函数
前面提到了ClutterStageView
内部包含了一个ClutterFrameClock
,我们可以在该对象的创建函数中看到:
|
|
现在来分析其注册的两个回调函数:
|
|
其中before_frame
函数做的事情比较单一,就是处理当前ClutterStageView
等待处理的事件:
|
|
然后frame
回调函数的处理就相当复杂了,毕竟是整个Mutter渲染的核心驱动,涉及了大量的Clutter实现细节。
TODO
ClutterStageViewCogl
这里只关注redraw_view
回调函数。可以看到ClutterStageViewCogl
是实现了ClutterStageWindow
接口,且前面发现在ClutterStageView
的frame
回调函数中调用了_clutter_stage_window_redraw_view
函数,这里就来分析这个函数。ClutterStageViewCogl
实现的接口如下:
|
|
clutter_stage_cogl_redraw_view
进行了scanout操作:
|
|
该操作直接调用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。函数传入的参数为ClutterStageCogl
和ClutterStageView
,也就是所有与更新相关的信息都记录在了ClutterStageView
中。函数首先获取framebuffer信息:
|
|
ClutterStageView
中保存了一个redraw_clip
:
|
|
如果redraw_clip为空,则会进行full_redraw也就是全屏重新绘制。
ClutterActor
clutter_actor_queue_redraw
分析这个函数的目的是想搞明白一个ClutterActor
是如何绘制到屏幕上的。从函数名称看该函数将对应的ClutterActor
排到redraw队列,等待下一次绘制。这里就需要搞明白:
- 这个绘制队列究竟是什么
- 什么时候进行绘制
- 绘制的原理是什么样子的
函数本质直接调用_clutter_actor_queue_redraw_full
,且提供的ClutterPaintVolume
和ClutterEffect
都是NULL
。我们知道每一个ClutterActor
都是有parent的,最顶层的parrent就是ClutterStage
,函数获取到ClutterStage
之后,调用了_clutter_stage_queue_actor_redraw
:
|
|
而该函数的操作可以在ClutterStage
的分析中看到。由于前面看到CluterEffect
为NULL
,则函数直接将自己的is_dirty
属性设置为TRUE
。至此,函数结束,可以看到函数最大的工作还是委托给了ClutterStage
进行。
_clutter_actor_finish_queue_redraw
从ClutterStage
的分析中可以看到,该函数与clutter_actor_queue_redraw
不同,是直接进行ClutterActor
重绘制的函数。函数首先清空priv->queue_redraw_entry
,毕竟函数被调用时,pending_queue_redraws
队列正在进行清空操作。
文章作者 crab2313
上次更新 2021-04-07 (773f6f3)