23.精确控制帧率

  • 学习人数 15K+
  • 适合有C语言基础人群学习
avatar
林耿亮

你好编程主讲老师

在之前的章节当中,我们计算画面的帧率时,仅仅只考虑了Sleep函数所占用的时间,并未考虑执行其他语句所占用的时间。例如,每绘制一帧画面后,使用Sleep(40)休眠40毫秒。若绘制一帧画面所需的时间很短,我们可以忽略绘制画面的时间。但是,随着画面越来越复杂,绘制画面所需时间在几毫秒、几十毫秒时,很显然,已经不能再忽略画面绘制的时间了。此外,Sleep函数所休眠的时间也并不是非常准确的,它也会对画面帧率的准确性产生影响。

这一节中,我们将展开讨论以上提到的问题,并找到一个可以精确地控制画面帧率的方案。

1. 高精度计时器

试想在计算机中有个计数器,开机后,它从0开始累加,每隔固定的时间就让计数器加1,每秒钟累加F次。F可以看作计数器累加的频率,一般情况下,F都会很大,在以上。如此一来,我们可以利用它来做高精度计时。

例如,我们需要计算处理某一个事务所耗时长,可以遵循以下步骤:

  1. 获取一次计数器当前数值,记为startCount
  2. 处理事务。
  3. 再次获取一次计算器当前数值,记为endCount
  4. (endCount - startCount) / F即为所耗费的时间,单位为秒s

若需要将秒s转换为微秒us,可以在其后乘以1000000。即为,(endCount - startCount) / F * 1000000

在windows系统上,上述步骤对应的代码如下:

//  开始时间、结束时间、频率F
LARGE_INTEGER startCount, endCount, F;

//  获取频率F
QueryPerformanceFrequency(&F);

//  获取起始计数
QueryPerformanceCounter(&startCount);

//  处理事务:休眠1000ms
Sleep(1000);

//  获取结束计数
QueryPerformanceCounter(&endCount);

//  (结束 - 开始) / 频率 * 1000000 = 时差
long long elapse = (endCount.QuadPart - startCount.QuadPart)
    / F.QuadPart * 1000000;

//  打印各数值
printf("start %lld\n", startCount.QuadPart);
printf("end %lld\n", endCount.QuadPart);
printf("end - start %lld\n", endCount.QuadPart - startCount.QuadPart);
printf("F %lld\n", F.QuadPart);
printf("elapse %lld\n", elapse);

调用QueryPerformanceFrequency(&F)获取计数器累加频率,并存储到变量F当中。调用QueryPerformanceCounter(&startCount)获取起始计数,并存储到变量startCount当中。用休眠1000ms作为处理事务的代码。事务处理完成后,再调用QueryPerformanceCounter(&endCount)获取终止计数,并存储到变量endCount当中。

类型LARGE_INTEGER是一个如下定义的结构体:

typedef union _LARGE_INTEGER {
    struct {
        DWORD LowPart;
        LONG HighPart;
    } DUMMYSTRUCTNAME;
    struct {
        DWORD LowPart;
        LONG HighPart;
    } u;
    LONGLONG QuadPart;
} LARGE_INTEGER;

其中,成员QuadPart用于存储计时器数值或频率,类型为long long。另外,成员HighPart可以获得QuadPart的高4字节,成员LowPart可以获得QuadPart的低4字节。

接下来,我们用(endCount - startCount) / F * 1000000计算处理事务所耗时长。

运行后,各变量数值结果如下:

精确误差

变量 数值 意义
startCount 274399139118 起始计数
endCount 274409271614 终止计数
endCount - startCount 10132496 计数差值
F 10000000 计数器频率
elapse 1000000 时差