Time
Created | |
---|---|
Tags | core |
Overivew
The current time is obtained uaing operating system's functions.
The world time depends on the time step policy than can be changed during runtime.
- Fixed or variable time steps
- Slow-down or speed-up time
- Throttling to save battery (maximum FPS)
The accumulated elapsed time should be represented by a double.
https://randomascii.wordpress.com/2012/02/13/dont-store-that-in-a-float/
Time Steps
There are two possible time steps.
- Fixed time step
- Variable time step
Fixed time step
- The game feels better.
- The gameplay is more deterministic.
Some systems always use a fixed time step such as the physics system.
Variable time step
Some games cannot have a fixed time step such as unconstrained sandbox games.
Frames per Seconds
Frames per Seconds (FPS) represents the update and rendering speed. The number of images rendered per second. It is used to express the rate of a particular frame or the average performance over a duration.
The Frequency is expressed in Hertz where 1 Frame per Second is equal to 1 Hertz (in hardware measurements such as the display rate of a monitor).
It's better to express the frame rate in milliseconds to profile code.
Runtime Loop
Typically, game that run at a fixed time step are locked to 60 FPS or 30 FPS.
60 FPS is about 16 milliseconds and 30 FPS is about 33 milliseconds.
The game code requires microseconds precision.
Each frame, we measure the time that has elapsed and use that to update the game.
double deltaTime = GetElapsedTime();
Update(deltaTime);
Object Update
Calculation that occur over a period of time are typically multiplied by the elapsed time.
position = position + speed * deltaTime
Basic Timing
Some functions have been traditionally used to measure time intervals but they suffer from a low resolution and poor performances.
Windows and Xbox
GetTickCount, timeGetTime, GetSystemTime
On Windows and Xbox, the GetTickCount
, and GetSystemTime
functions retrieve the number of milliseconds that have elapsed since the system was started.
Xbox 360
On PowerPC, the mftb
instruction returns the time base registers (TBL and TBU) combined as a 64-bit value.
Because of lag issues, it is recommended to use the 32-bit compiler intrinsic that returns the TBL register only.
unsigned int __mftb32();
High Precision Timing
❌Do not hard code the clock frequency.
High precision timing is platform-specific and relies an reading the processor’s time stamp counter (TSC). It provides time stamps with a resolution of 1 microsecond or better.
There are multiple methods to obtain a reliable and efficient performance counter.
They have different guarantees about accuracy, precision, and consistency.
Some may skip, freeze, or continue to run when the title is paused in the debugger.
RDTSC
The rdtsc instruction is an acronym for Read Time-Stamp Counter.
The instruction is a reliable way to get the processor time stamp.
The processor time stamp records the number of clock cycles since the last reset.
However, the synchronization across multiple cores is not guaranteed.
The instruction returns a 64-bit unsigned integer representing a tick count.
✔Very high resolution.
❌Some processors are unable to synchronize the clocks on each core to the same value.
❌Some processors will execute instructions out of order and it can result in incorrect cycle counts.
Windows
The __rdtsc()
compiler intrinsic generates the rdtsc instruction.
#include <intrin.h>
uint64_t rdtsc()
{
return __rdtsc();
}
Clang
The __builtin_readcyclecounter()
compiler intrinsic generates the rdtsc instruction on x86/x64 and ARM64.
Assembly
Alternatively, the rdtsc instruction can be called directly using assembly code.
The 64-bit result is stored in the EDX:EAX registers.
uint64_t rdtsc()
{
uint32_t low, high;
__asm__ __volatile__ ("rdtsc" : "=a" (low), "=d" (high));
return ((uint64_t)high << 32) | low;
}
High-Resolution Timer (Windows and Xbox)
The High-Resolution Timer functions can acquire a high-resolution time stamps or measure time intervals.
The QueryPerformanceFrequency()
function retrieves the frequency of the performance counter, in counts per second. It should be called once at startup only and the result should be cached. The frequency is stored as a LARGE_INTEGER
structure.
The QueryPerformanceCounter()
function retrieves the current value of the performance counter.
It should be called before and after the activity to be timed.
The elapsed number of ticks is computed by the difference between the two performance counters.
The value is then multiplied by 1000 and divided by the frequency to compute the elapsed time in milliseconds.
To prevent loss-of-precision, the value is converted into milliseconds before dividing by the frequency.
✔Very high resolution.
✔Synchronize across all of the cores.
#include <windows.h>
class Timer
{
LARGE_INTEGER frequency;
LARGE_INTEGER startTime;
public:
Timer()
{
QueryPerformanceFrequency(&frequency);
ResetElapsedTime();
}
void ResetElapsedTime()
{
QueryPerformanceCounter(&startTime);
}
double GetElapsedTime() const
{
LARGE_INTEGER currentTime;
QueryPerformanceCounter(¤tTime);
double timeDiff = (double)(currentTime.QuadPart - startTime.QuadPart);
return (timeDiff * 1000.0) / (double)frequency.QuadPart;
}
};
Apple
On macOS and iOS, the mach APIs provided a high precision counter.
The mach_timebase_info()
function retrieves a mach_timebase_info_data_t
structure that contains the processor's frequency in nanoseconds.
struct mach_timebase_info
{
uint32_t numer;
uint32_t denom;
};
The frequency of the clock is computed as (denom / numer) * 10^9
.
The mach_absolute_time()
function retrieves the current value of the counter.
The elapsed time is computed by computing the delta time between two time points and multiplying by the frequency.
#include <mach/mach_time.h>
class Timer
{
double frequency;
uint64_t startTime;
public:
Timer()
{
mach_timebase_info_data_t info;
mach_timebase_info(&info);
frequency = ((double)info.denom / (double)info.numer) * 1000000000.0;
ResetElapsedTime();
}
void ResetElapsedTime()
{
startTime = mach_absolute_time();
}
double GetElapsedTime() const
{
uint64_t currentTime = mach_absolute_time();
return ((double)(currentTime - startTime) * frequency);
}
};
POSIX
On Linux, high precision timing is provided by POSIX functions.
The clock_gettime()
function retrieves the time of the specified clock in a timespec
structure.
The CLOCK_MONOTONIC
clock represents monotonic time since some unspecified starting point.
The timespec
structure contains a tv_nsec
member that represents the time in nanoseconds.
#include <time.h>
class Timer
{
timespec startTime;
public:
Timer()
{
ResetElapsedTime();
}
void ResetElapsedTime()
{
clock_gettime(CLOCK_MONOTONIC, &startTime);
}
double GetElapsedTime() const
{
timespec currentTime;
clock_gettime(CLOCK_MONOTONIC, ¤tTime);
return ((double)(currentTime.tv_nsec - startTime.tv_nsec) * 1000000000.0);
}
};
std::chrono
The chrono
library provides varying degrees of precision to track time.
ℹAvailable from C++11.
The chrono
library defines three main types:
- clock: starting point and tick rate
- time point: duration of time that has passed in a clock
- duration: span of time defined as a number of ticks
Advantages
✔Type safetey
- Type safe time operations.
- C++14 adds user literals for type safe immediate values.
✔Conversions
- No implicit conversion with integer or float types.
- Loss-less conversions between chrono types are implicit.
- Otherwise, requires a special cast.
✔Performances
- Represented by a long long type by default.
- Can be replaced by a custom representation (ie. 32-bit type).
- Compile time checks.
- No additional assembly code generated.
High resolution clock
std::chrono::high_resolution_clock
represents the clock with the smallest tick period.
⚠Earlier implementations in Visual Studio were not based on QueryPerformanceCounter
, but it was fixed since Visual Studio 2015.
The now()
function returns a time point representing the current point in time as a std::chrono::time_point
type.
#include <chrono>
class Timer
{
std::chrono::time_point<std::chrono::high_resolution_clock> startTime;
public:
Timer()
{
ResetElapsedTime();
}
void ResetElapsedTime()
{
startTime = std::chrono::high_resolution_clock::now();
}
double GetElapsedTime() const
{
auto endTime = std::chrono::high_resolution_clock::now();
auto elapsedTime = endTime - startTime;
auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(elapsedTime);
return milliseconds.count();
}
};