High-Resolution Timers
Learn to measure time intervals with high accuracy in your games
Previously, we introduced the SDL_GetTicks()
function, which returns the number of milliseconds that have passed since SDL was initialized.
However, in some situations, milliseconds are not granular enough. We need to use timers that use smaller units, such as microseconds and nanoseconds.
Timers that use these smaller units are typically called high-resolution timers. How they work and how we access them is something that varies from platform to platform, but SDL provides some utilities that can help us.
Starting Point
If you want to follow along, this lesson uses the same architecture we built earlier in the chapter. We set up a basic World
and GameObject
hierarchy and used SDL_GetTicks()
to pass a time delta to our Tick()
functions.
This starting point contains the Window
, GameObject
, World
, and Goblin
classes from the previous lesson. Compared to the previous lesson, we've removed our frame-rate capping logic:
Files
Getting a High Resolution Timer
SDL provides access to a high-resolution timer using SDL_GetPerformanceCounter()
. The values returned from this function should be accurate enough to detect even tiny changes in time:
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
int main(int, char**) {
SDL_Init(SDL_INIT_VIDEO);
std::cout << "\nPerformance Counter: "
<< SDL_GetPerformanceCounter();
std::cout << "\nPerformance Counter: "
<< SDL_GetPerformanceCounter();
std::cout << "\nPerformance Counter: "
<< SDL_GetPerformanceCounter();
SDL_Quit();
return 0;
}
Performance Counter: 23369230619011
Performance Counter: 23369230624712
Performance Counter: 23369230627816
The values returned from this function are useful when compared to other values returned by that same function. That is, we'd call SDL_GetPerformanceCounter()
twice, and compare the difference. One of the main uses for this is to compare the performance of different approaches to solving a problem in part of our code where performance is important.
Below, our program is creating a large array, and we use SDL_GetPerformanceCounter()
to determine whether reserving the required memory ahead of time reduces the performance cost:
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
#include <vector>
int main(int, char**) {
SDL_Init(SDL_INIT_VIDEO);
// Option A
std::vector<int> A;
Uint64 StartA{SDL_GetPerformanceCounter()};
for (int i{0}; i < 1'000'000'000; ++i) {
A.emplace_back(i);
}
Uint64 EndA{SDL_GetPerformanceCounter()};
std::cout << "\nA Cost: " << EndA - StartA;
// Option B
std::vector<int> B;
Uint64 StartB{SDL_GetPerformanceCounter()};
B.reserve(1'000'000'000);
for (int i{0}; i < 1'000'000'000; ++i) {
B.emplace_back(i);
}
Uint64 EndB{SDL_GetPerformanceCounter()};
std::cout << "\nB Cost: " << EndB - StartB;
SDL_Quit();
return 0;
}
We'd likely find option B to be faster, so we'd go with that approach in our program:
A Cost: 38112382
B Cost: 13096310
High Resolution Ticking
Let's use SDL_GetPerformanceCounter()
to calculate high resolution time deltas for our Tick()
functions:
src/main.cpp
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
#include "Window.h"
#include "World.h"
int main(int, char**) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
World GameWorld;
GameWorld.SpawnGoblin("Goblin Rogue", 100, 200);
SDL_Event Event;
bool IsRunning{true};
Uint64 PreviousFrame{
SDL_GetPerformanceCounter()};
while (IsRunning) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_EVENT_QUIT) {
IsRunning = false;
}
GameWorld.HandleEvent(Event);
}
Uint64 ThisFrame{SDL_GetPerformanceCounter()};
Uint64 TimeDelta{ThisFrame - PreviousFrame};
PreviousFrame = ThisFrame;
std::cout << "\nTimeDelta: " << TimeDelta;
// TimeDelta was previously in seconds but,
// with this new technique, we nore longer know
// what units are being used here
// We'll address this in the next section
GameWorld.Tick(TimeDelta);
GameWindow.Render();
GameWorld.Render(GameWindow.GetSurface());
GameWindow.Update();
}
SDL_Quit();
return 0;
}
TimeDelta: 1032
TimeDelta: 932
TimeDelta: 980
However, when we do this, we also need to consider what unit of time SDL_GetPerformanceCounter()
uses.
It returns values in the smallest unit the platform supports, however, that unit varies from platform to platform. It could be milliseconds; it could be nanoseconds; it could be something else entirely.
SDL_GetPerformanceFrequency()
The SDL_GetPerformanceFrequency()
function can help us understand what unit is being used on the platform our program is running on. It does this by returning an integer representing how many of those units there are in a second:
src/main.cpp
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
int main(int, char**) {
SDL_Init(SDL_INIT_VIDEO);
std::cout << SDL_GetPerformanceFrequency();
SDL_Quit();
return 0;
}
10000000
For example:
- A return value of 1,000 would mean it uses milliseconds
- A return value of 1,000,000 would mean it uses microseconds
- A return value of 1,000,000,000 would mean it uses nanoseconds
We can use this to convert our time delta to a standard, known unit of time such that our Tick()
functions can behave consistently across different platforms. Below, we deliver the time delta in seconds:
src/main.cpp
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
#include "Window.h"
#include "World.h"
int main(int, char**) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
World GameWorld;
GameWorld.SpawnGoblin("Goblin Rogue", 100, 200);
SDL_Event Event;
bool IsRunning{true};
Uint64 PreviousFrame{SDL_GetPerformanceCounter()};
const float PerformanceFrequency{
static_cast<float>(
SDL_GetPerformanceFrequency()
)
};
while (IsRunning) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_EVENT_QUIT) {
IsRunning = false;
}
GameWorld.HandleEvent(Event);
}
Uint64 ThisFrame{SDL_GetPerformanceCounter()};
float TimeDelta{(ThisFrame - PreviousFrame)
/ PerformanceFrequency
};
PreviousFrame = ThisFrame;
std::cout << "\nTimeDelta: " << TimeDelta;
// This is now back to using seconds again
GameWorld.Tick(TimeDelta);
GameWindow.Render();
GameWorld.Render(GameWindow.GetSurface());
GameWindow.Update();
}
SDL_Quit();
return 0;
}
TimeDelta: 0.0001639
TimeDelta: 0.0001633
TimeDelta: 0.0001631
High-Resolution Timers using std::chrono
The standard library also provides access to high resolution-timers using std::chrono
. We may prefer to use it as an alternative to SDL's functions, or on projects where SDL is not available.
High-resolution timers are available through std::chrono::high_resolution_clock
, alongside utilities like std::chrono::duration
and std::chrono::duration_cast
to convert time deltas to specific units like seconds or nanoseconds.
Below, we have an example of profiling the performance of a function using std::chrono
:
#include <chrono>
#include <iostream>
void DoWork() {/* ... */}
int main() {
using namespace std::chrono;
auto start{high_resolution_clock::now()};
DoWork();
auto end{high_resolution_clock::now()};
duration<double> TimeDelta{end - start};
std::cout << "TimeDelta: "
<< TimeDelta.count()
<< " Seconds ("
<< duration_cast<nanoseconds>(end - start)
<< ')';
}
TimeDelta: 0.000065 Seconds (65100ns)
In this example, we update our application loop to provide high-resolution time deltas to Tick()
functions using std::chrono
instead of SDL's functions:
src/main.cpp
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <chrono>
#include <iostream>
#include "Window.h"
#include "World.h"
int main(int, char**) {
using namespace std::chrono;
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
World GameWorld;
GameWorld.SpawnGoblin("Goblin Rogue", 100, 200);
SDL_Event Event;
bool IsRunning{true};
auto PreviousFrame{high_resolution_clock::now()};
while (IsRunning) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_EVENT_QUIT) {
IsRunning = false;
}
GameWorld.HandleEvent(Event);
}
auto ThisFrame{high_resolution_clock::now()};
duration<float> TimeDelta{
ThisFrame - PreviousFrame};
PreviousFrame = ThisFrame;
std::cout << "\nTimeDelta: "
<< TimeDelta.count() << " Seconds";
GameWorld.Tick(TimeDelta.count());
GameWindow.Render();
GameWorld.Render(GameWindow.GetSurface());
GameWindow.Update();
}
SDL_Quit();
return 0;
}
TimeDelta: 0.0001155 Seconds
TimeDelta: 0.0001091 Seconds
TimeDelta: 0.0001031 Seconds
We cover std::chrono
in more detail in our .
Summary
We've covered several key concepts related to high-resolution timing:
- Using
SDL_GetPerformanceCounter()
to measure time intervals. - Understanding timer precision with
SDL_GetPerformanceFrequency()
. - Calculating time deltas for game loops.
- Exploring
std::chrono
as an alternative to SDL timing functions.
These tools are useful for creating smooth, responsive game experiences and optimizing performance.
Callbacks and Function Pointers
An introduction to first-class functions, function pointers, and the flexible design options that they unlock