什么是Vsync同步机制?
Vsync(垂直同步信号量),用来同步渲染,让AppUI和SurfaceFlinger可以按硬件产生的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周期到来的时候就可以显示到屏幕上了。
网友评论