美文网首页回家
Vsync同步机制 一

Vsync同步机制 一

作者: 泡面先生_Jack | 来源:发表于2020-01-10 11:38 被阅读0次

    什么是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周期到来的时候就可以显示到屏幕上了。

    相关文章

      网友评论

        本文标题:Vsync同步机制 一

        本文链接:https://www.haomeiwen.com/subject/kyomoctx.html