[086]VSYNC研究-最后的窗户纸

2023-11-09 08:53:42 浏览数 (3)

背景

最近在精读努比亚团队的SurfaceFlinger模块-VSYNC研究,其中有一段话一直困扰到我,成为了彻底理解vsync的最后一层窗户纸。

3.2.2 nextAnticipatedVsyncTimeFromLocked 有了这个回归系数和截距,就可以传入上一次app或者sf发射的时间,计算出下一次发射的时间

一、我的疑问

按照他的说法,每次nextVsync都是依赖上一次app或者sf发射的时间earliestVsync,那问题来了,如果屏幕界面停止刷新一段时间,然后app再次刷新,那这时候上一次app发射的时间earliestVsync远远早于希望拿到的nextVsync一个Vsync周期以上,按照计算公式,根本算不出正确nextVsync。

举个例子
代码语言:javascript复制
假设周期16ms,上次vsync的时间为16ms,然后过了160ms,我这个时候请求nextVsync。
因为earliestVsync是16ms,算出来的应该是32ms,但是实际的nextVsync应该是16   160   16 = 192 ms

二、传入的实际数据

其实传入的实际数据是timing.earliestVsyncnow timing.workDuration timing.readyDuration两个数字的中最大值,而且绝大多数还是用后者这个数字

代码语言:javascript复制
    auto nextVsyncTime = tracker.nextAnticipatedVSyncTimeFrom(
            std::max(timing.earliestVsync, now   timing.workDuration   timing.readyDuration));

workDurationreadyDuration与我之前文章写的有关[070]一文带你看懂Vsync Phase,建议先看这个文章。

举个例子

我通过dumpsys SurfaceFlinger | grep duration,获取我的设备参数

代码语言:javascript复制
XXXXX:/ $ dumpsys SurfaceFlinger | grep duration
           app duration:  16666666 ns            SF duration:  15666666 ns

最后作用到app和sf的vsync如下

vsync类型

workDuration

readyDuration

app vsync

app duration(16666666 ns)

SF duration (15666666 ns)

sf vsync

SF duration(15666666 ns)

0

很明显now timing.workDuration timing.readyDuration,用这个值作为下面函数timePoint就可以计算出nextVsync。

不知道怎么计算的可以参考我前面的文章[085]SW VSYNC模型更新与校准

代码语言:javascript复制
nsecs_t VSyncPredictor::nextAnticipatedVSyncTimeFromLocked(nsecs_t timePoint) const {
    auto const [slope, intercept] = getVSyncPredictionModelLocked();

    if (mTimestamps.empty()) {
        traceInt64If("VSP-mode", 1);
        auto const knownTimestamp = mKnownTimestamp ? *mKnownTimestamp : timePoint;
        auto const numPeriodsOut = ((timePoint - knownTimestamp) / mIdealPeriod)   1;
        return knownTimestamp   numPeriodsOut * mIdealPeriod;
    }

    auto const oldest = *std::min_element(mTimestamps.begin(), mTimestamps.end());

    // See b/145667109, the ordinal calculation must take into account the intercept.
    auto const zeroPoint = oldest   intercept;
    auto const ordinalRequest = (timePoint - zeroPoint   slope) / slope;
    auto const prediction = (ordinalRequest * slope)   intercept   oldest;

    traceInt64If("VSP-mode", 0);
    traceInt64If("VSP-timePoint", timePoint);
    traceInt64If("VSP-prediction", prediction);

    auto const printer = [&, slope = slope, intercept = intercept] {
        std::stringstream str;
        str << "prediction made from: " << timePoint << "prediction: " << prediction << " ( "
            << prediction - timePoint << ") slope: " << slope << " intercept: " << intercept
            << "oldestTS: " << oldest << " ordinal: " << ordinalRequest;
        return str.str();
    };

    ALOGV("%s", printer().c_str());
    LOG_ALWAYS_FATAL_IF(prediction < timePoint, "VSyncPredictor: model miscalculation: %s",
                        printer().c_str());

    return prediction;
}

三、继续思考

到这里其实还有几点疑惑,需要进一步解答

3.1 等于每次app要申请的时候,会走到resyncAndRefresh中,这个函数就会强制进行一次硬件的VSYNC校准。

这句话明显不可能,因为trace中可以看到hw vsync正常刷新的时候就会关闭。

合理的说法应该是如果两次app vsync的request nextvsync时间差大于750ms,就会触发一下hardware vsync同步。

代码语言:javascript复制
/frameworks/native/services/surfaceflinger/Scheduler/Scheduler.cpp

void Scheduler::resync() {
    static constexpr nsecs_t kIgnoreDelay = ms2ns(750);//如果两次app vsync的request 

    const nsecs_t now = systemTime();
    const nsecs_t last = mLastResyncTime.exchange(now);

    if (now - last > kIgnoreDelay) {
        resyncToHardwareVsync(false, mRefreshRateConfigs.getCurrentRefreshRate().getVsyncPeriod()); //硬件校准
    }
}

我通过住trace,验证了这个

3.2 假如触发了硬件的VSYNC校准,就会清空模型中的数据,如果算出nextVsync

关键看这个代码,一旦mTimestamps为空,就会用mKnownTimestamp来计算,也就是"VSP-mode"为1.

代码语言:javascript复制
nsecs_t VSyncPredictor::nextAnticipatedVSyncTimeFromLocked(nsecs_t timePoint) const {
    auto const [slope, intercept] = getVSyncPredictionModelLocked();

    if (mTimestamps.empty()) {
        traceInt64If("VSP-mode", 1);
        auto const knownTimestamp = mKnownTimestamp ? *mKnownTimestamp : timePoint;
        auto const numPeriodsOut = ((timePoint - knownTimestamp) / mIdealPeriod)   1;
        return knownTimestamp   numPeriodsOut * mIdealPeriod;
    }

mKnownTimestamp 又是在clear的时候被赋值了采样的vsync中的最大值

代码语言:javascript复制
void VSyncPredictor::clearTimestamps() {
    if (!mTimestamps.empty()) {
        auto const maxRb = *std::max_element(mTimestamps.begin(), mTimestamps.end());
        if (mKnownTimestamp) {
            mKnownTimestamp = std::max(*mKnownTimestamp, maxRb);
        } else {
            mKnownTimestamp = maxRb;
        }

        mTimestamps.clear();
        mLastTimestampIndex = 0;
    }
}
简单总结一下

"VSP-mode"为1,是用采样数据中清空前,最新的时间戳nextvsync "VSP-mode"为0,是用采样数据中最老的时间戳配合上拟合的模型算nextvsync 这个采样的数据会在hw vsync或者present fence signal后被加入进来,采样的数据保留最新的6个,重新计算模型。

我通过trace,也验证了这个事情,开始硬件vsync的第一个nextvsync是"VSP-mode"为1的方式算出来的。

3.3 app vsync和next vsync的关系

我之前以为next vsync就是app vsync,其实大错特错,看这段代码

代码语言:javascript复制
ScheduleResult VSyncDispatchTimerQueueEntry::schedule(VSyncDispatch::ScheduleTiming timing, VSyncTracker& tracker, nsecs_t now) {
    auto nextVsyncTime = tracker.nextAnticipatedVSyncTimeFrom(
            std::max(timing.earliestVsync, now   timing.workDuration   timing.readyDuration));
    auto nextWakeupTime = nextVsyncTime - timing.workDuration - timing.readyDuration;
 ···
     auto const nextReadyTime = nextVsyncTime - timing.readyDuration;
     mArmedInfo = {nextWakeupTime, nextVsyncTime, nextReadyTime};
     return getExpectedCallbackTime(nextVsyncTime, timing);
 }

这里有一个重要的结构体ArmedInfo,解读一下。

代码语言:javascript复制
    struct ArmingInfo {
        nsecs_t mActualWakeupTime;
        nsecs_t mActualVsyncTime;
        nsecs_t mActualReadyTime;
    };
app vsync的ArmingInfo

nextWakeupTime:下一个app vsync触发时机,触发app绘制

nextVsyncTime:nextWakeupTime触发app绘制,最后送显屏幕的时间。

nextReadyTime:nextWakeupTime触发app绘制完成,触发sf合成的时间。

sf vsync的ArmingInfo

nextWakeupTime:下一个sf vsync触发时机,触发sf合成

nextVsyncTime:nextWakeupTime触发sf合成,最后送显屏幕的时间。

nextReadyTime:等同于nextVsyncTime。

虽然app request和sf request next vsync不在同一时间,但是得到的nextVsyncTime其实是同一个,因为两者最后送显时间是一样的。

3.4 意外的问题分析

我抓trace的发现一个很奇怪的事情,就是明明按照[070]一文带你看懂Vsync Phase相位差计算,我的设备sf和app的vsync不应该有相位差,但是我发现我的设备一直保持有100us左右的间隔。

仔细分析trace,原来是因为TimeDispatcher中对SF vsync的触发的callback,过于耗时,导致delay了app vsync的触发,然后产生了轻微的offset。

四、总结

总算是把SurfaceFlinger模块-VSYNC研究所讲的全部消化了,当然也发现文章很多说的不是很到位的地方,因为正确理解了ArmingInfo ,对之前自己写的文章[070]一文带你看懂Vsync Phase有了更加深刻的理解,最后还是要感谢努比亚团队的文章,终于可以在大脑中形成vsync完整工作流程,哈哈。

白话版Vsync的理解

代码语言:javascript复制
1.首先采样的数据,只有两个来源,hw vsync和present fence,采样的数据只会保留最近的6个hw vsync 时间戳。
2.根据采样的数据训练出模型参数。
3.然后把std::max(timing.earliestVsync, now   timing.workDuration   timing.readyDuration))带入模型就可以获得下一个nextvsync
4.然后nextvsync - workDuration - readyDuration就是wakeuptime
5.我的设备app workDuration = 16.6 readyDuration = 15.6 
6.wakeuptime就是真的app vsync触发的时间,设置到timedispatcher里。


对于sf,步骤一样,只是workDuration和readyDuration改了
5.我的设备sf workDuration = 15.6 readyDuration = 0
6.wakeuptime就是真的sf vsync触发的时间,设置到timedispatcher里。

如果上时间界面不更新的话,因为超过750ms,首先会触发硬件采样的vsync,同时清空采样的数据,
然后保存采样数据中的最大值mKnownTimestamp ,这时候就会走vsp-mode 1,会使用mKnownTimestamp来算nextvsync

0 人点赞