回家

Vsync同步机制 一

2020-01-10  本文已影响0人  泡面先生_Jack

什么是Vsync同步机制?

Vsync(垂直同步信号量),用来同步渲染,让AppUI和SurfaceFlinger可以按硬件产生的VSync节奏进行工作。
Vsync要解决的问题:

Vsync要解决的问题

为什么会产生这样的问题?

CPU负责对UI进行更新,GPU负责对UI进行渲染,两者的频率不一致,会导致CPU还未更新完成,就被GPU渲染到了屏幕上。所以会出现图片上的问题。

如何解决这个问题?

解决这个问题的方法就是让CPU和GPU以相同的节奏进行工作。如下图所示

Vsync

让CPU和GPU以相同的频率进行工作,这就是Vsync要做的工作。Vsync以固定的频率发出信号,每当收到CPU先对UI进行更新,然后GPU再进行绘制,这样就可以解决上面的问题了。

那Android的Vsync机制是如何进行的呢?

Android的Vsync整体框架图

disp_sync_arch.png

这张图可以很明显的看出Vsync事件的传递过程。
Vsync信号并不是有硬件直接产生,而是由DispSync线程产生的。DispSync会根据HWC产生的VSync进行采样,创建模型,然后输出了SW_VSYNC信号,SW_VSYNC再根据SF和APP的phase offset做调整,分别输出给Vsync-sf和Vsync-app。

再看下几个类之间的关系图

HWC产生硬件Vsync信号

在分析HWComposer的时候,HWComposer中会注册硬件Vsync事件回调,在硬件Vsync事件到来的时候,回调HWComposer的vsync函数。

void HWComposer::vsync(int disp, int64_t timestamp) {
    if (uint32_t(disp) < HWC_NUM_PHYSICAL_DISPLAY_TYPES) {
        {
            Mutex::Autolock _l(mLock);
            //记录对应硬件设备Vsync信号产生的时间
            mLastHwVSync[disp] = timestamp;
        }
        //然后直接通知SurfaceFlinger的onVsyncReceived函数
        mEventHandler.onVSyncReceived(disp, timestamp);
    }
}

void SurfaceFlinger::onVSyncReceived(int type, nsecs_t timestamp) {
    bool needsHwVsync = false;

    { // Scope for the lock
        Mutex::Autolock _l(mHWVsyncLock);
        //如果是主显示设备的Vsync信号,且当前硬件Vsync事件是打开的,则调用DispSync的addResyncSample函数
        //将硬件VSync事件添加到DispSYnc的样本中,用于创建软件Vsync时间模型
        if (type == 0 && mPrimaryHWVsyncEnabled) {
            needsHwVsync = mPrimaryDispSync.addResyncSample(timestamp);
        }
    }
     
    //更加样本采样结果,决定是否要关掉硬件Vsync事件。
    if (needsHwVsync) {
        enableHardwareVsync();
    } else {
        disableHardwareVsync(false);
    }
}

HWComposer接收到硬件的Vsync事件并没有直接传递给系统使用,而是通过SurfaceFlinger将Vsync事件,添加到了DispSync的Vsync事件样本,当DispSync采样完成后,则会停止硬件Vsync事件,由软件Vsync根据样本的计算结果产生Vsync事件。
mPrimaryHWVsyncEnabled变量控制DispSync是否需要采集样本,当模型Vsync周期有误差时需要重新打开硬件Vsync,再次采集硬件Vsync样本。

bool DispSync::addResyncSample(nsecs_t timestamp) {
    Mutex::Autolock lock(mMutex);

    //此处相当于做了一个大小为32环形Buffer,每当有新的样本过来之后就添加到数组Buffer中,如果Buffer已满,则替换掉最老的样本
    size_t idx = (mFirstResyncSample + mNumResyncSamples) % MAX_RESYNC_SAMPLES;
    mResyncSamples[idx] = timestamp;

    if (mNumResyncSamples < MAX_RESYNC_SAMPLES) {
        mNumResyncSamples++;
    } else {
        mFirstResyncSample = (mFirstResyncSample + 1) % MAX_RESYNC_SAMPLES;
    }

    //当样本更新后,根据32个硬件Vsync样本计算软件Vsync模型.
    updateModelLocked();

    if (mNumResyncSamplesSincePresent++ > MAX_RESYNC_SAMPLES_WITHOUT_PRESENT) {
        resetErrorLocked();
    }

    //当Vsync周期mPeriod = 0或者误差超过一定的阀值,需要重新采样,否则,则停止采样,关掉硬件Vsync,知道有误差的时候在打开,再次采样
    return mPeriod == 0 || mError > kErrorThreshold;
}

addResyncSample方法主要作用是添加采样样本到Buffer中,DispSync中维护了一个环形的Buffer,大小为32个,每当有新样本过来时候,则将样本添加到Buffer中,如果Buffer已经满了,则替换掉最老的样本。
样本更新后调用updateModelLocked来计算更新DispSync模型。

void DispSync::updateModelLocked() {
    //只有样本数量大于3个的时候才会计算,太少则没有意义
    if (mNumResyncSamples >= MIN_RESYNC_SAMPLES_FOR_UPDATE) {
        nsecs_t durationSum = 0;
        //第一步 :从最老的时间样本开始计算Vsync间隔,然后再计算平均的时间间隔
        for (size_t i = 1; i < mNumResyncSamples; i++) {
            size_t idx = (mFirstResyncSample + i) % MAX_RESYNC_SAMPLES;
            size_t prev = (idx + MAX_RESYNC_SAMPLES - 1) % MAX_RESYNC_SAMPLES;
            durationSum += mResyncSamples[idx] - mResyncSamples[prev];
        }
        //mPeriod就是计算产生的平均Vsync间隔的时间
        mPeriod = durationSum / (mNumResyncSamples - 1);

        //第二部:开始计算平均的偏差时间,因为不可能每个间隔都非常准时,要求平均偏差
        double sampleAvgX = 0;
        double sampleAvgY = 0;
        double scale = 2.0 * M_PI / double(mPeriod);
        for (size_t i = 0; i < mNumResyncSamples; i++) {
            //遍历并计算每个样本相对于平均周期取余的偏差值,(如果完全准时的样本,应该可以整除,余数为0),然后将偏差值转换成弧度。
            size_t idx = (mFirstResyncSample + i) % MAX_RESYNC_SAMPLES;
            nsecs_t sample = mResyncSamples[idx];
            double samplePhase = double(sample % mPeriod) * scale;
            //计算弧度的X和Y
            sampleAvgX += cos(samplePhase);
            sampleAvgY += sin(samplePhase);
        }
        //求平均弧度X,Y
        sampleAvgX /= double(mNumResyncSamples);
        sampleAvgY /= double(mNumResyncSamples);
        
        //将平均弧度转换成平均的偏差值
        mPhase = nsecs_t(atan2(sampleAvgY, sampleAvgX) / scale);

        if (mPhase < 0) {
            mPhase += mPeriod;
        }

        if (kTraceDetailedInfo) {
            ATRACE_INT64("DispSync:Period", mPeriod);
            ATRACE_INT64("DispSync:Phase", mPhase);
        }

        // 人为减少Vsync刷新频率
        mPeriod += mPeriod * mRefreshSkipCount;
        //更新周期和偏差模型
        mThread->updateModel(mPeriod, mPhase);
    }
}

计算平均周期模型和平均偏差模型。
如何计算平均周期?
将所有样本的间隔时间相加,然后除以间隔数,求出平均间隔时间.
如何计算平均偏差?
如果完全准时的样本,应该可以整除平均,余数为0,有偏差的则余数不为0,所以对所有的样本除以mPeriod平均周期时间,计算余数,将余数又转换成弧度. 计算X和Y, 求X和Y的平均值,然后再由平均X,Y求出弧度,再转换成偏差值。这样就计算出平均偏差了.

  void updateModel(nsecs_t period, nsecs_t phase) {
        Mutex::Autolock lock(mMutex);
        mPeriod = period;
        mPhase = phase;
        mCond.signal();
    }

调用VsyncThread的mCond.signal()通知VsyncThread模型更新完成。

DispSync 软件Vsync模型

SurfaceFlinger的Vsync并不是直接使用硬件产生Vsync,而是由软件根据硬件Vsync创建了一个数据模型来模拟产生Vsync信号。负责产生软件Vsync信号的就是DispSync。
DispSync是SurfaceFlinger中的一个变量。

DispSync mPrimaryDispSync;

mPrimaryDispSync就是Vsync的信号源,我们先看下Vsync信号是如何产生的?


DispSync::DispSync() :
        mRefreshSkipCount(0),
        mThread(new DispSyncThread()) {
    //启动VsyncThread
    mThread->run("DispSync", PRIORITY_URGENT_DISPLAY + PRIORITY_MORE_FAVORABLE);
}

DispSync VsyncThread 第一部分

DispSync对象在创建的时候会启动一个VsyncThread线程,该线程用于模拟Vsync信号。然后我们看下VsyncThread线程的threadloop方法


    virtual bool threadLoop() {
        status_t err;
        nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
        nsecs_t nextEventTime = 0;
        //执行循环
        while (true) {
            Vector<CallbackInvocation> callbackInvocations;

            nsecs_t targetTime = 0;

            { // Scope for lock
                Mutex::Autolock lock(mMutex);
                //如果需要停止Vsync线程,则直接返回
                if (mStop) {
                    return false;
                }
                
                //如果mPeriod为0, 则等待
                //在VsyncThread创建的时候,mPeriod默认为0,所以会阻塞在这边。
                //在VsyncThread updateModel更新模型的时候,会设置mPeriod,然后继续执行,线程刚开始的时候Vsync模型还没有创建好,所以无法产生SW Vsync信号
                if (mPeriod == 0) {
                    err = mCond.wait(mMutex);
                    if (err != NO_ERROR) {
                        ALOGE("error waiting for new events: %s (%d)",
                                strerror(-err), err);
                        return false;
                    }
                    continue;
                }

                ......
    }

VsyncThread刚开始由于mPeriod=0,也就是说当前线程还不知道按照什么样的频率产生Vsync信号,所以会等待mPeriod周期模型的更新。
什么时候设置更新mPeriod的时间呢?
就是上面提到的,应用模型样本计算完成后会设置mPeriod和mPhase,这样VsyncThread就会继续执行

DispSync VsyncThread 第二部分

    virtual bool threadLoop() {

                ......

                //计算下次Vsync要产生的时间
                nextEventTime = computeNextEventTimeLocked(now);
                targetTime = nextEventTime;

                bool isWakeup = false;

                //如果还没有到时间,就等待相应的时间后在处理
                if (now < targetTime) {
                    err = mCond.waitRelative(mMutex, targetTime - now);

                    if (err == TIMED_OUT) {
                        isWakeup = true;
                    } else if (err != NO_ERROR) {
                        ALOGE("error waiting for next event: %s (%d)",
                                strerror(-err), err);
                        return false;
                    }
                }

                now = systemTime(SYSTEM_TIME_MONOTONIC);

                //到达执行Vsync时间后,回调监听对象.
                if (isWakeup) {
                    mWakeupLatency = ((mWakeupLatency * 63) +
                            (now - targetTime)) / 64;
                    if (mWakeupLatency > 500000) {
                        // Don't correct by more than 500 us
                        mWakeupLatency = 500000;
                    }
                    if (kTraceDetailedInfo) {
                        ATRACE_INT64("DispSync:WakeupLat", now - nextEventTime);
                        ATRACE_INT64("DispSync:AvgWakeupLat", mWakeupLatency);
                    }
                }

                callbackInvocations = gatherCallbackInvocationsLocked(now);
            }

            if (callbackInvocations.size() > 0) {
                fireCallbackInvocations(callbackInvocations);
            }
        }

        return false;
    }

当有Vsync的周期时间和偏差时间后,VsyncThread就可以模拟产生软件Vsync信号了。产生Vsync信号就会通过回调接口通知监听者.
DispVsync有两个监听者,就是我们上面提到的SurfaceFlinger和APP,SurfaceFlinger和App所需要的Vsync时间不是完全一致。App一般先接到Vsync信号,还是绘制UI,然后SurfaceFlinger再接收到Vsync信号完成UI合成。
VsyncThread做了什么事情呢?
首先计算下次Vsync产生的时间,计算下次产生时间会根据两个监听者上次接收到Vsync时间,然后加上周期时间,偏差时间,和APP或者SF自己的偏差时间。得到下次Vsync时间。

下次Vsync时间 = 上次Vsync时间 + mPeriod(平均周期) + mPhase(平均偏差) + offset(自定义偏差)

得到最近的下次执行时间,Vsync只要Wait相应的时间差就可了,等到执行时间后回调SF或者APP的Vsync信号。这样Vsync信号就产生了.

DisplaySyncSource

上面讲DispSync提到,DispSync有两个监听者,这两个监听这就是两个DisplaySyncSource对象。两个对象是在SurfaceFlinger的init进程创建的.

    sp<VSyncSource> vsyncSrc = new DispSyncSource(&mPrimaryDispSync,
            vsyncPhaseOffsetNs, true, "app");
    sp<VSyncSource> sfVsyncSrc = new DispSyncSource(&mPrimaryDispSync,
            sfVsyncPhaseOffsetNs, true, "sf");

init中创建了两个DispSyncSource对象, 一个是SF的sfVsyncSrc,一个是SurfaceFlinger的sfVsyncSrc对象. DiplaySyncSource构造方法中会保存一个syncOffet值,表示自己收到Vsync偏差时间.
DispSync计算Vsync产生时间的时候,会根据这个偏差进行计算.也就是上面公式中的自定义偏差offset。
DispSync有一些重要的函数,具体如下:

    //开始/关闭监听Vsync事件
    //开始监听Vsync事件就是将自己添加到DispSync的回调监听中
    //关闭监听Vsync事件就是将自己从DispSync监听回调中移除
    virtual void setVSyncEnabled(bool enable) {
        Mutex::Autolock lock(mVsyncMutex);
        if (enable) {
            status_t err = mDispSync->addEventListener(mPhaseOffset,
                    static_cast<DispSync::Callback*>(this));
            if (err != NO_ERROR) {
                ALOGE("error registering vsync callback: %s (%d)",
                        strerror(-err), err);
            }
            //ATRACE_INT(mVsyncOnLabel.string(), 1);
        } else {
            status_t err = mDispSync->removeEventListener(
                    static_cast<DispSync::Callback*>(this));
            if (err != NO_ERROR) {
                ALOGE("error unregistering vsync callback: %s (%d)",
                        strerror(-err), err);
            }
            //ATRACE_INT(mVsyncOnLabel.string(), 0);
        }
        mEnabled = enable;
    }

    //设置自己的回调,即收到Vsync事件后,回调给谁
    virtual void setCallback(const sp<VSyncSource::Callback>& callback) {
        Mutex::Autolock lock(mCallbackMutex);
        mCallback = callback;
    }

    //DispSync的回调接口, 当收到DispSync事件后会回到该接口,然后该接口将事件又回调给自己的callBack
    //也就是后边讲的EventThread,EventThread会监听DisplaySyncSource。
    virtual void onDispSyncEvent(nsecs_t when) {
        sp<VSyncSource::Callback> callback;
        {
            Mutex::Autolock lock(mCallbackMutex);
            callback = mCallback;

        if (callback != NULL) {
            callback->onVSyncEvent(when);
        }
    }

这样,我们就大概理解了Vsync信号的产生过程。
1:HWC硬件产生Vsync信号,给DispSync添加样本
2:DispSync根据样本计算Vsync周期,然后产生软件Vsync信号。计算Vsync事件会根据不同的Listenr计算不同的时间.
3:DisplaySyncSource是DispSync的监听者,有两个DisplaySyncSource对象,分别代表SF和APP的DisplaySyncSource,两个回调的Vsync时间不同.
4:DisplaySyncSource收到Vsync事件后,会发给他自己的监听者EventThread

void EventThread::onVSyncEvent(nsecs_t timestamp) {
    Mutex::Autolock _l(mLock);
    mVSyncEvent[0].header.type = DisplayEventReceiver::DISPLAY_EVENT_VSYNC;
    mVSyncEvent[0].header.id = 0;
    mVSyncEvent[0].header.timestamp = timestamp;
    mVSyncEvent[0].vsync.count++;
    mCondition.broadcast();
}

EventThread收到Vsync时间后会存放到mVSyncEvent[0]中,通知EventThread线程进行处理。

为什么要有两个DisplaySyncSource且时间不一样呢?

Vsync

具体如上图所示
App绘制UI, 绘制完成后交给SurfaceFlinger进行合成, 所以App绘制时间优先于SF合成时间,将两者时间错开,避免资源竞争,增加效率。如何APP和SF的偏差时间设置好的话,APP + SF在一个Vsync周期内就可以绘制合成,在下一个Vsync周期到来的时候就可以显示到屏幕上了。

上一篇下一篇

猜你喜欢

热点阅读