Libuv实现帧率控制
概念
服务端帧率控制,保证在一段固定的时间内执行完所有事情(包括网络I/O等),如果有空余时间,那么我们Sleep等待一段时间。如果超时我们需要追帧。
注意点
-
只要在程序中只有一个进程的情况下控制服务器的帧率,那么我们在Libuv中注册的定时器永远会存在一定的误差且误差时间无法确定。
-
在Libuv中的idle、prepare、check句柄队列中控制帧率,会无法有效的计算一次Loop循环所耗费的具体时间。因为我们只能依靠loop提供的时间,通过uv_now方法拿到本次循环(uv_run)即将执行的时间,但是由于loop提供的时间每次都未考虑定时器队列和pending队列的执行耗时,所以我们在idle、prepare、check句柄中计算每帧的耗时,是根本不精确的。
- 在定时器队列中控制帧率,首先不能保证Libuv每次循环都触发回调方法。其次该方式也无法有效的计算整个Libuv循环所耗费的具体时间。
解决方案:在Libuv主循环之外控制帧率,这样既可以有效计算整个Libuv循环循环一次所耗费的具体时间,也能保证Libuv每循环一次后都能进行帧率控制。
TIPS:关于UpdateFrame逻辑更新在哪里处理,不管是在idle、prepare、check句柄中处理,或者跟帧率控制一样在Libuv主循环之外处理,均可。
在博主的上篇介绍帧率控制的文件中,明显是有误区的,本篇文章进行纠正。
链接:https://ufgnix0802.blog.csdn.net/article/details/126754967
实现
TIPS:uv_hrtime是Libuv提供的获取时间API,内部实现比较简单。
//数据结构
uv_idle_t m_mainLoop;
bool m_quit;
//帧率控制
uint64_t m_timerStart;
uint64_t m_timerEnd;
int m_frameRate;
int m_repeat;
DWORD m_durationFrameTime;
DWORD m_realTime;
if (0 != uv_idle_start(&m_mainLoop, MainLoop)) { //在Idle句柄中执行UpdateFrame
LOG_ERROR << "uv_idle_start err";
goto Exit;
}
m_mainLoop.data = &pFunc;
m_timerStart = uv_hrtime() / 1000000; //纳秒转毫秒,即单位为毫秒。记录第一次逻辑帧循环时间
m_timerEnd = 0;
m_repeat = 1;
while (true) {
//Update
uv_run(&m_loop, UV_RUN_ONCE);
m_timerEnd = uv_hrtime() / 1000000; //纳秒转毫秒,即单位为毫秒,每次uv_run运行完获取一下截止时间
m_durationFrameTime = (DWORD)(m_frameRate * m_repeat);//m_frameRate是我们自己设计的控制逻辑帧,比如1000,即1s。m_repeat会记录当前是第几次循环。那么控制逻辑帧 x 循环次数 = 累积循环帧时间
m_repeat++;
m_realTime = (DWORD)(m_timerEnd - m_timerStart);
//规定的帧率内执行完所有事件,则Sleep;否则追帧。
if (m_durationFrameTime < m_realTime) {
LOG_INFO << "超出控制帧,追帧";
}
else {
LOG_INFO << "m_durationFrameTime - m_realTime = sleep time:"
<< m_durationFrameTime << " - " << m_realTime << " = " <<
m_durationFrameTime - m_realTime;
Sleep(m_durationFrameTime - m_realTime);
}
if (m_quit) { //退出标记位,bool类型
LOG_INFO << "Service quiting...";
break;
}
}
追帧算法
当我们在规定的控制逻辑帧内处理完所有的事务,那么我们空余的时间应该让进程休息。那么关键来了,如果在规定的控制逻辑帧内没有执行完我们的事务,怎么办呢?那就是追帧,我们立刻执行下一帧,而下一帧的空余时间用来弥补当前帧规定逻辑时间内之外超出的时间。下面为上述追帧算法的图示;