I am writing first-person shooter game with fast motion. I have a fixed high-frequency physics loop thread, which is separated from variable-frequency rendering thread. I need to choose the physics frequency, but I'm unsure which value to choose. I have 3 options with the following properties:
120 Hz:
- Monitor support: 1:1 or 1:2 mapping to rendering thread refresh rate
- Is divided by mains frequency in US, may be good for older CRTs there
125 Hz:
- 8ms integer frame delta (if time is expressed in integer ms, in a games like Quake3)
- Default USB poll rate for most consumer mices (why the hell is it so?!..)
128 Hz:
- Frame delta of 1/128th minimizes division error
- Windows sheduler exposes least jitter values (WinAPI's
Sleep(1);
actually sleeps for 1ms±0.29ms), least possible sleep is hardcoded in ntoskrnl like this, and capped to 0.5ms
Which rate should i choose?
PS: Feel free to run my test code. If you are lucky Linux user, please share your port and your results, it may be interesting. On Windows: see how worse is 125hz jitter, compared to 128hz:
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <cstdint>
#include <vector>
#include <algorithm>
#include <numeric>
#include <iostream>
uint64_t qpf() {
uint64_t hpF = 0;
::QueryPerformanceFrequency(reinterpret_cast<LARGE_INTEGER*>(&hpF));
return hpF;
}
uint64_t qpc() {
uint64_t hpC = 0;
::QueryPerformanceCounter(reinterpret_cast<LARGE_INTEGER*>(&hpC));
return hpC;
}
// very precise (~1000x times), but would consume 100% cpu in absense of active threads.
void yield_active() {
SwitchToThread();
}
// default precision is 15.625ms, but 1ms +-0.3ms if NtSetTimerResolution() was called
void yield_passive() {
SleepEx(1, false);
}
// import ntdll stuff, this is the heart of Sleep()/SleepEx():
static int(__stdcall *NtDelayExecution)(BOOL Alertable, PLARGE_INTEGER DelayInterval) = (int(__stdcall*)(BOOL, PLARGE_INTEGER)) GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtDelayExecution");
// 0.5ms +-0.3ms, 5000 us value is hardcoded into ntoskrnl, which is hard to change; i tried, attempt failed:
// https://superuser.com/questions/1326252/changing-windows-thread-sheduler-timeslice
// https://reverseengineering.stackexchange.com/questions/18384/ntoskrnl-checksum-mismatch/18387
void yield_passive_hp(const int64_t& delayInterval) {
//delay.QuadPart = (milliseconds > 1) ? -10000LL * (milliseconds - 1) : -1LL;
::NtDelayExecution(false, (PLARGE_INTEGER)(&delayInterval));
}
// ensure that some moment defined by target timespamp had occured
uint64_t wait_stamp(uint64_t targetStamp) {
uint64_t currentStamp;
while ((currentStamp = qpc()) < targetStamp)
//yield_active();
yield_passive_hp(-1000); // yield_passive_hp(-1000); asking to wait for 100us or 0.1ms, implementation would wait for at least 0.5ms
//yield_passive();
return currentStamp;
}
template< typename T >
T series_mean(const std::vector< T >& vSeries) {
auto lagSum = std::accumulate(vSeries.begin(), vSeries.end(), T(0));
return lagSum / vSeries.size();
}
template< typename T >
T std_deviation(const std::vector< T >& vSeries, const T& lagMean = 0) {
std::vector<T> vSqrDiff(vSeries.size());
std::transform(vSeries.begin(), vSeries.end(), vSqrDiff.begin(), [lagMean](const T& x) { return x - lagMean; });
double varianceSum = std::inner_product(vSqrDiff.begin(), vSqrDiff.end(), vSqrDiff.begin(), T(0));
return std::sqrt(varianceSum / vSeries.size());
}
int main() {
double benchmarkTime = 1.0; // run game loop for 1.0 second
double fixedFPS = 128.0; // try to stick as close as possible to 1/128th timeslices
auto hpF = qpf();
auto fixedDelta = 1.0 / fixedFPS;
auto numTicks = static_cast< uint64_t >( benchmarkTime * fixedFPS );
std::vector< double > vLags;
vLags.reserve(numTicks);
auto startStamp = qpc();
uint64_t frameNumber = 0;
do {
auto stampDelta = static_cast<uint64_t>(static_cast<double>(hpF * frameNumber++) * fixedDelta);
auto targetStamp = startStamp + stampDelta;
auto actualStamp = wait_stamp(targetStamp);
auto lastDelta = static_cast<double>(actualStamp - targetStamp) / hpF;
vLags.emplace_back(lastDelta);
} while (frameNumber < numTicks);
auto lagMean = series_mean(vLags);
auto lagDeviation = std_deviation(vLags, lagMean);
std::cout << "lagMean=" << lagMean * 1000 << " ms; lagDeviation=" << lagDeviation * 1000 << " ms;\n";
return 0;
}
x = x + v * dt
you'd normally want to have same dt and fixed dt for all users to put them in equal conditions and normalize error. example: Quake3, where/seta com_maxfps 125
fps gives user an ability to perform higher jumps, than standart 85fps. AFAIK, interframe delays there are made with means ofselect(..., timeval)
function \$\endgroup\$ – xakepp35 Aug 22 '18 at 10:46