SurfaceFlinger需要hw vsync来对sw vsync进行校准,来防止误差变大。

VSyncWorker

在hwc中有个VSyncWorker的实现来做这个事情。

具体实现https://github.com/omnirom/android_external_drm_hwcomposer/blob/875653a0bc485b1cfa041f5d348929c52889e864/drm/VSyncWorker.cpp

来看下VSyncWorker::Routine(),代码有删减。

void VSyncWorker::Routine() {

  int display = display_;
  std::shared_ptr<VsyncCallback> callback(callback_);
 
  DrmCrtc *crtc = drm_->GetCrtcForDisplay(display);

  uint32_t high_crtc = (crtc->pipe() << DRM_VBLANK_HIGH_CRTC_SHIFT);

  drmVBlank vblank;
  memset(&vblank, 0, sizeof(vblank));
  vblank.request.type = (drmVBlankSeqType)(
      DRM_VBLANK_RELATIVE | (high_crtc & DRM_VBLANK_HIGH_CRTC_MASK));
  vblank.request.sequence = 1;

  int64_t timestamp;
  ret = drmWaitVBlank(drm_->fd(), &vblank);

  timestamp = (int64_t)vblank.reply.tval_sec * kOneSecondNs +
                (int64_t)vblank.reply.tval_usec * 1000;

  if (callback)
    callback->Callback(display, timestamp);

  if (enabled_ && vsync_callback_hook_ && vsync_callback_data_)
    vsync_callback_hook_(vsync_callback_data_, display, timestamp);

  last_timestamp_ = timestamp;
}

里面最重要的操作drmWaitVBlank;从drm中获取drmVBlank vblank信息。

获取成功后更新时间戳,如果注册了callback,vsync_callback_hook_,就会执行回调函数。

本文具体看driver是如何工作,不对VSyncWorker过多分析。

drmVBlank的实现

typedef struct _drmVBlankReq {
    drmVBlankSeqType type;
    unsigned int sequence;
    unsigned long signal;
} drmVBlankReq, *drmVBlankReqPtr;

typedef struct _drmVBlankReply {
    drmVBlankSeqType type;
    unsigned int sequence;
    long tval_sec;
    long tval_usec;
} drmVBlankReply, *drmVBlankReplyPtr;

typedef union _drmVBlank {
    drmVBlankReq request;
    drmVBlankReply reply;
} drmVBlank, *drmVBlankPtr;

等vsync的时候,填入drmVBlankReq,sequence,type;

sequence=1:会在drm driver中使用

type:这里用的是DRM_VBLANK_RELATIVE,相对时间;high_crtc,用于判断要读取哪个crtc的vsync

调用 ret = ioctl(fd, DRM_IOCTL_WAIT_VBLANK, vbl);

drm vblank的kernel driver

从drm_ioctl.c中可以看到drm中会调用

int drm_wait_vblank_ioctl(struct drm_device *dev, void *data,
              struct drm_file *file_priv)
{
    struct drm_crtc *crtc;
    struct drm_vblank_crtc *vblank;
    union drm_wait_vblank *vblwait = data;
    int ret;
    u64 req_seq, seq;

    flags = vblwait->request.type & _DRM_VBLANK_FLAGS_MASK;
    high_pipe = (vblwait->request.type & _DRM_VBLANK_HIGH_CRTC_MASK);
    pipe_index = high_pipe >> _DRM_VBLANK_HIGH_CRTC_SHIFT;

    if (pipe >= dev->num_crtcs)
        return -EINVAL;

    vblank = &dev->vblank[pipe];
    seq = drm_vblank_count(dev, pipe);

    switch (vblwait->request.type & _DRM_VBLANK_TYPES_MASK) {
    case _DRM_VBLANK_RELATIVE:
        req_seq = seq + vblwait->request.sequence;
        vblwait->request.sequence = req_seq;
        vblwait->request.type &= ~_DRM_VBLANK_RELATIVE;
        break;
    case _DRM_VBLANK_ABSOLUTE:
        req_seq = widen_32_to_64(vblwait->request.sequence, seq);
        break;
    default:
        ret = -EINVAL;
        goto done;
    }

    if (flags & _DRM_VBLANK_EVENT) {

        return drm_queue_vblank_event(dev, pipe, req_seq, vblwait, file_priv);
    }

    if (req_seq != seq) {
        int wait;

        wait = wait_event_interruptible_timeout(vblank->queue,
            drm_vblank_passed(drm_vblank_count(dev, pipe), req_seq) ||
                      !READ_ONCE(vblank->enabled),
            msecs_to_jiffies(3000));
    }

}
  1. 通过type传入的high_crtc,来找到对应crtc的vblank, vblank = &dev->vblank[pipe]。
  2. 判断vblank_disable_immediate,是不是可以直接更新vblank,这里没有置位,不会执行。
  3. 获取当前的vblank的count值
  4. 根据传入的type,对seq执行不同的操作;这里是把拿到的vblank count+vblwait->request.sequence;VsyncWorker传入的vblwait->request.sequence=1.
  5. req_seq和seq如果不相等,调用wait_event_interruptible_timeout,休眠当前进程。一直等到vblank count与req_seq相等,或者超时。
  6. 成功后,调用drm_wait_vblank_reply(dev, pipe, &vblwait->reply),构建reply,返回给user。

drm vblank更新

先来看下vblank count是怎么更新的。

drm用vblank来抽象vsync,vsync是display模块产生的,正常情况下开启后会按照一定时间触发中断。

在各家vendor实现的drm driver中会注册vsync的中断服务程序,便于软件进行处理异常,包括vsync

比如rockchip

kernel\drivers\gpu\drm\rockchip\rockchip_drm_vop.c
static int vop_bind(struct device *dev, struct device *master, void *data)
{
    ... ...
    ret = devm_request_irq(dev, vop->irq, vop_isr,
                   IRQF_SHARED, dev_name(dev), vop);
    ... ...
}

来看下中断处理函数

static irqreturn_t vop_isr(int irq, void *data)
{
    ... ...
    if (active_irqs & FS_INTR) {
        drm_crtc_handle_vblank(crtc);
        vop_handle_vblank(vop);
        active_irqs &= ~FS_INTR;
        ret = IRQ_HANDLED;
    }
    ... ...
}

这个vblank的函数比较耀眼,来具体看下

看到store_vblank里会把vblank->count+1; 然后会唤醒等待队列。

中断里把vblank count+1;中断是display模块产生的,如果刷新率为60帧,每16.6ms就会来一个中断,这个操作与user space无关。

drm_wait_vblank_ioctl等到vblank count之后就会唤醒进程,并返回给user。

总结

用一张简图总结下大致流程:

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐