Используя функции, доступные в WinAPI, можно ли гарантировать, что определенная функция вызывается в соответствии с точной отметкой времени в миллисекунды?И если да, то какова будет правильная реализация?
Я пытаюсь написать программное обеспечение с поддержкой инструментов speedrun.Этот тип программного обеспечения отправляет пользовательские команды ввода в очень точные моменты после запуска сценария, чтобы выполнить невозможные для человека вводы, которые позволяют быстрее завершить видеоигры.Типичная последовательность выглядит примерно так:
- При 0 миллисекундах отправляется событие нажатия правой клавиши
- В 5450 миллисекунд отправляется правая кнопка вверх и событие нажатия клавиши вверх
- На 5460 миллисекундах отправьте событие нажатия левой клавиши
- и т. Д.
То, что я пробовал до сих пор, перечислено ниже.Поскольку у меня нет опыта в нюансах низкого уровня таймеров высокой точности, у меня есть некоторые результаты, но я не понимаю, почему они таковы:
- Использование Sleep в сочетании с timeBeginPeriod , установленный на 1 между входами, дал худшие результаты.Из 20 казней 0 выполнили требование сроков.Я считаю, что это хорошо объяснено в документации по сну
Note that a ready thread is not guaranteed to run immediately. Consequently, the thread may not run until some time after the sleep interval elapses.
Насколько я понимаю, Сон не подходит для этой задачи. - Использование проверки цикла ожидания занятости GetTickCount64 с timeBeginPeriod , установленным в 1, дало немного лучшие результаты.Из 20 казней 2 выполнили требование сроков, но, видимо, это было просто счастливое обстоятельство.Я просмотрел некоторую информацию об этой функции синхронизации, и я подозреваю, что она не обновляется достаточно часто, чтобы обеспечить точность в 1 миллисекунду.
- Замена GetTickCount64 на QueryPerformanceCounter немного улучшил ситуацию.Из 20 казней 8 успешно завершены.Я написал регистратор, который будет хранить временные метки QPC непосредственно перед отправкой каждого ввода и выводить значения в файл после завершения последовательности.Я даже дошел до того, что заранее выделил место для всех переменных в моем коде, чтобы убедиться, что время не тратится впустую на ненужные явные выделения памяти.Значения журнала отличаются от меток времени, которые я предоставляю программе, от 1 до 40 миллисекунд.Программирование общего назначения может с этим смириться, но в моем случае один кадр в игре составляет 16,7 мс, поэтому в худшем случае с такими задержками я могу опоздать на 3 кадра, что противоречит цели всего эксперимента.
- Установка высокого приоритета процесса не имеет никакого значения.
На данный момент я не уверен, где искать дальше.Мои два предположения состоят в том, что, возможно, время, необходимое для итерации цикла занятости и проверки времени с помощью (QPCNow - QPCStart) / QPF, само по себе достаточно велико, чтобы ввести упомянутую задержку, или что процесс прерывается планировщиком ОС в каком-то местевдоль выполнения цикла и управление возвращается слишком поздно.
Игра на 100% детерминирована и заблокирована на 60 кадрах в секунду.Я убежден, что, если мне удастся обеспечить точную синхронизацию ввода, результат всегда будет 20 из 20, но сейчас я начинаю подозревать, что это может быть невозможно.
РЕДАКТИРОВАТЬ: Какза запрос здесь урезанная тестовая версия.Точка останова после второго вызова ExecuteAtTime и просмотра переменных TimeBeforeInput.Для меня это читает 1029 и 6017 (я пропустил десятичные дроби), означая, что код выполнялся через 29 и 17 миллисекунд после того, как должен был.Отказ от ответственности: код не написан, чтобы продемонстрировать хорошие методы программирования.
#include "stdafx.h"
#include <windows.h>
__int64 g_TimeStart = 0;
double g_Frequency = 0.0;
double g_TimeBeforeFirstInput = 0.0;
double g_TimeBeforeSecondInput = 0.0;
double GetMSSinceStart(double& debugOutput)
{
LARGE_INTEGER now;
QueryPerformanceCounter(&now);
debugOutput = double(now.QuadPart - g_TimeStart) / g_Frequency;
return debugOutput;
}
void ExecuteAtTime(double ms, INPUT* keys, double& debugOutput)
{
while(GetMSSinceStart(debugOutput) < ms)
{
}
SendInput(2, keys, sizeof(INPUT));
}
INPUT* InitKeys()
{
INPUT* result = new INPUT[2];
ZeroMemory(result, 2*sizeof(INPUT));
INPUT winKey;
winKey.type = INPUT_KEYBOARD;
winKey.ki.wScan = 0;
winKey.ki.time = 0;
winKey.ki.dwExtraInfo = 0;
winKey.ki.wVk = VK_LWIN;
winKey.ki.dwFlags = 0;
result[0] = winKey;
winKey.ki.dwFlags = KEYEVENTF_KEYUP;
result[1] = winKey;
return result;
}
int _tmain(int argc, _TCHAR* argv[])
{
INPUT* keys = InitKeys();
LARGE_INTEGER qpf;
QueryPerformanceFrequency(&qpf);
g_Frequency = double(qpf.QuadPart) / 1000.0;
LARGE_INTEGER qpcStart;
QueryPerformanceCounter(&qpcStart);
g_TimeStart = qpcStart.QuadPart;
//Opens windows start panel one second after launch
ExecuteAtTime(1000.0, keys, g_TimeBeforeFirstInput);
//Closes windows start panel 5 seconds later
ExecuteAtTime(6000.0, keys, g_TimeBeforeSecondInput);
delete[] keys;
Sleep(1000);
return 0;
}