Time

Created
Tagscore

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.

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

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.

The elapsed time is stored as a 32-bit integer value and will wrap around to zero in a reasonably short amount of time.
The functions have a resolution of 10 to 16 milliseconds.
On Windows, timeGetTime suffers from the same limitations. timeBeginPeriod changes the minimum resolution of the timing functions but is not precise enough (~1 millisecond). It also increases the pressure on the kernel for thread scheduling.
⚠️
On Windows and Xbox One, the result of GetTickCount64 is stored as a 64-bit integer value, but also suffers from a poor resolution.

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(&currentTime);

    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, &currentTime);

    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:

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();
  }
};