Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

控帧改为等待信号实现 #7

Closed
Krimiston opened this issue Feb 25, 2023 · 3 comments
Closed

控帧改为等待信号实现 #7

Krimiston opened this issue Feb 25, 2023 · 3 comments

Comments

@Krimiston
Copy link
Collaborator

【版本】
0.9.0 (commit 40ef548)

【问题描述】
rgm::base::timer.tick 函数,使用死循环控帧。此实现会使 CPU 持续空跑,浪费 CPU 资源。

【期望】
实现改为操作系统的信号机制,例如 Windows 上的 WaitableTimer 系列。优先考虑跨平台实现,如果没有方便的跨平台库,则分别为 Windows 与 UNIX 写实现。

@Krimiston
Copy link
Collaborator Author

考察后发现问题的关键在于精准控帧,只要能实现,用信号还是 sleep 都可以。
因此改为用 STL 的 sleep_for + sleep 误差修正机制来做。

@Krimiston
Copy link
Collaborator Author

写了个 demo,验证 windows 下的 WaitableTimer 和
STL sleep_for,结果令人很意外很失望。
https://rpg.blue/forum.php?mod=redirect&goto=findpost&ptid=488007&pid=2969420&fromuid=2667091

运行结果:
WaitableTimer1
QQ图片20230228232904
QQ图片20230228232910
QQ图片20230228232912

结论:
WaitableTimer 的精度实际上和 STL sleep_for 一样,都是15ms左右(具体数字取决于操作系统的调度时间片)的粒度。

既然 WaitableTimer 的时间粒度达不到要求,不如优先考虑跨平台的 STL sleep_for + 自旋锁。不过自旋的时间应尽可能短。
目前预想的方案是,二分法 sleep ,剩余时间小于时间粒度时再自旋。

@Krimiston
Copy link
Collaborator Author

关于 timeBeginPeriod 的令人惊喜的最新考察结果:

以下结果为 timeBeginPeriod + WaitableTimer,模拟控帧的结果。
以下结果打印的格式为:请求挂起的纳秒数、实际挂起的纳秒数、误差和误差比。

win7:
模拟60FPS:ptp3
模拟120FPS:ptp4
最小粒度试验:tpt

win10:
模拟60FPS:ptp1
模拟120FPS:ptp2
最小粒度试验:ptp

结论:
可以看出这套函数在 win 上确实能够将误差控制在 1ms 以内,令人满意。
但是,当请求挂起的时间很短(<<1ms)时,实际挂起时间也在 1ms 左右。
所以二分法就没用了,请求挂起的次数越多,误差还越大。不如老老实实一次请求+误差补偿了。

代码:

#include <Windows.h>
#include <stdio.h>
#include <thread>
#include "SDL.h"

int main(int argc, char* argv[])
{
    uint64_t frequency = SDL_GetPerformanceFrequency();
    long double ns_per_count = 1E9 / frequency;
    HANDLE waitable_timer = CreateWaitableTimerW(
        nullptr,
        TRUE,
        nullptr);
    TIMECAPS tc = { 0 };
    timeGetDevCaps(&tc, sizeof(tc));
    printf("%u,%u\n", tc.wPeriodMin, tc.wPeriodMax);
    timeBeginPeriod(tc.wPeriodMin);
    while (waitable_timer)
    {
        long long delay = 1E9 / 60; // 60FPS
        uint64_t before = SDL_GetPerformanceCounter();
#if 1
        LARGE_INTEGER due_time;
        due_time.QuadPart = delay / -100;
        SetWaitableTimerEx(
            waitable_timer,
            &due_time,
            0,
            nullptr, nullptr,
            nullptr,
            0
        );
        WaitForSingleObject(waitable_timer, INFINITE);
#else
        std::this_thread::sleep_for(std::chrono::nanoseconds(delay));
#endif
        uint64_t after = SDL_GetPerformanceCounter();
        long double real = (after - before) * ns_per_count;
        printf("call: %lldns, real: %lfns, error: %lfms %.1f%%\n", delay, real, (real - delay) / 1E6, (real - delay) / delay * 100);
    }
    timeEndPeriod(tc.wPeriodMin);
    CloseHandle(waitable_timer);
    return 0;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant