簡體   English   中英

Windows(以及OS X,Linux)上的Qt中的高分辨率定期計時器

[英]High resolution periodic timer in Qt on Windows (also OS X, Linux)

到目前為止,我發現的有關計時器的所有內容最多只能以1ms的分辨率提供。 QTimer的文檔聲稱這是它可以提供的最好的。

我知道Windows之類的操作系統不是實時操作系統,但我仍然想問這個問題,希望有人知道可以提供幫助的內容。

因此,我正在編寫一個應用程序,要求以相當精確但任意的間隔(例如60次/秒(完整范圍:59-61Hz))調用函數。 這意味着我需要平均每16.67毫秒調用一次。 設計的這一部分不能更改。

我目前擁有的最佳計時源是vsync。 當我擺脫這種情況時,那還不錯。 這不是理想的,因為監視器的頻率與我調用此函數所需的頻率不完全相同,但是可以對其進行某種程度的補償。

更重要的是,給定我要跟蹤的范圍時,精度水平或多或少可用於計時器,但不是我想要的精度水平。 我可以得到一個16ms的計時器來准確地命中16ms〜97%的時間。 我可以得到一個17ms的計時器來准確地命中17ms〜97%的時間。 但是沒有API可以讓我得到16.67嗎?

我要找的東西根本不可能嗎?

背景:該項目稱為Phoenix 本質上,這是一個libretro前端。 Libretro“核心”是封裝在各個共享庫中的游戲機模擬器。 以特定速率調用的API函數是retro_run() 每個調用都模擬一個游戲框架,並調用音頻,視頻等的回調。 為了以控制台的本機幀速率進行仿真,我們必須以完全(或接近於此)速率(即計時器)調用retro_run()。

您可以編寫一個循環,以檢查std::chrono::high_resolution_clock()std::this_thread::yield()直到正確的時間過去。 如果程序在執行過程中需要響應,則應在檢查主循環的線程之外的另一線程中執行。

一些示例代碼: http : //en.cppreference.com/w/cpp/thread/yield

一種替代方法是使用QElapsedTimerPerformanceCounter的值。 您仍然需要從循環中檢查它,並且可能仍然希望在該循環中屈服。 示例代碼: http : //doc.qt.io/qt-4.8/qelapsedtimer.html

只要平均幀速率正確,並且音頻輸出緩沖區retro_run ,就完全沒有必要在任何高度受控的時間調用retro_run

首先,您可能必須使用基於音頻輸出的計時器來測量實時時間。 最終,每個retro_run產生大量音頻。 添加了塊的音頻緩沖區狀態是您的時間參考:如果您提早運行,則緩沖區將太滿,如果您提早運行,則緩沖區將太空。

可以將此錯誤度量輸入到PI控制器中,該控制器的輸出為您提供所需的延遲,直到下次調用retro_run 這將自動確保您的平均速率相位正確。 激活retro_run任何系統延遲都將被整合,等等。

其次,您需要一種在正確的時間喚醒自己的方法。 給定一個調用retro_run的目標時間(例如,根據性能計數器),您將需要喚醒代碼的事件源,以便您可以在必要時比較時間和retro_run

最簡單的方法是重新實現QCoreApplication::notify 您將有機會在每個事件,每個事件循環中,每個線程中傳遞每個事件之前進行retro_run 由於系統事件可能不會經常出現,因此您還需要運行一個計時器以提供更可靠的事件源。 事件是什么無關緊要:任何類型的事件都對您有用。

我不熟悉retro_run線程限制-也許您可以一次在任何一個線程中運行它。 在這種情況下,您可能希望在池中的下一個可用線程上運行它,也許是主線程除外。 因此,有效地,事件(包括計時器事件)被用作為您提供執行上下文的廉價能源。

如果選擇使用專用於retro_run的線程,則該線程應該是僅在互斥retro_run的高優先級線程。 每當有retro_run時間的事件發生時,只要您准備好運行retro_run ,就可以解鎖互斥鎖,並且應該立即安排線程,因為它將搶占大多數其他線程,甚至可以搶占進程中的所有線程。

OTOH,在低內核數系統上,高優先級線程可能會搶占主(gui)線程,因此您也可以直接從獲得retro_run時間事件的任何線程直接調用retro_run

當然,可能會發現,使用來自任意線程的事件來喚醒專用線程會導致過多的最壞情況延遲或過多的延遲擴展-這是特定於系統的,您可能希望收集運行時統計信息,切換線程和事件即時尋找策略,並堅持最好的策略。 選擇是:

  1. 在等待互斥的專用線程中運行retro_run ,解鎖源是通過notify捕獲到具有retro_run時間事件的任何線程,

  2. 在專用線程中的retro_run等待計時器(或任何其他)事件; 仍然通過notify捕獲的事件,

  3. 在gui線程中的retro_run ,解鎖源是傳遞到gui線程的事件,仍通過notify捕獲,

  4. 以上任何一種,但僅使用計時器事件-請注意,您不必擔心它們是哪個計時器事件,它們不需要來自您的計時器,

  5. 像#4一樣,但僅對您的計時器有選擇。

我的實現基於Lorehead的回答。 所有變量的時間以毫秒為單位。 當然,它需要一種停止運行的方式,我也正在考慮減去timeElapsedinterval之間的(運行平均值)差的timeElapsed ,以使平均值+ -n而不是+ 2n,其中2n是平均過沖。

// Typical interval value: 1/60s ~= 16.67ms
void Looper::beginLoop( double interval ) {

    QElapsedTimer timer;
    int counter = 1;
    int printEvery = 240;
    int yieldCounter = 0;

    double timeElapsed = 0.0;

    forever {

        if( timeElapsed > interval ) {
            timer.start();

            counter++;
            if( counter % printEvery == 0 )  {
                qDebug() << "Yield() ran" << yieldCounter << "times";
                qDebug() << "timeElapsed =" << timeElapsed << "ms | interval =" << interval << "ms";
                qDebug() << "Difference:" << timeElapsed - interval << " -- " << ( ( timeElapsed - interval ) / interval ) * 100.0 << "%";
            }

            yieldCounter = 0;

            importantBlockingFunction();

            // Reset the frame timer
            timeElapsed = ( double )timer.nsecsElapsed() / 1000.0 / 1000.0;
        }

        timer.start();

        // Running this just once means massive overhead from calling timer.start() so many times so quickly
        for( int i = 0; i < 100; i++ ) {
            yieldCounter++;
            QThread::yieldCurrentThread();
        }

        timeElapsed += ( double )timer.nsecsElapsed() / 1000.0 / 1000.0;

    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM