Could be a few things. Hard to say with a small code example with just the frame limiter.
I’m assuming this is with vsync off? The only guarantee you get with SDL_Delay
is that it sleeps at least that many milliseconds. It may take a bit longer to wake up again and then it would miss the sync pulse and drop a frame, if vsync is on.
Setting it to 600 kind of disables your frame limiter if the update and draw code takes at least 1 millisecond. Then it’s either drawing as fast as possible (vsync off) or the buffer swap is synchronized to the refresh rate (vsync on).
If SDL_GetTicks
and SDL_Delay
would do submillisecond values and be that accurate, you would be getting a frame rate around 62.5 Hz because your limiter tries to sleep up to 16 milliseconds and not 16.666. There are SDL_GetPerformanceCounter
and SDL_GetPerformanceFrequency
that can be used instead of SDL_GetTicks
, but modern operating systems won’t let you sleep for less than a millisecond. Probably need to busy-loop those microseconds.
You might want to measure the whole loop instead of just the updating and drawing part. That way you know if SDL_Delay
slept longer than expected and you have to cut some time off the next frame.
Here’s a crude frame limiter that uses a monotonic counter, a sleep threshold to prevent oversleeping, and a busy-loop for the last few milliseconds. Obviously not optimal, but it should give a reasonably smooth moving square. Controls: w, a, s, d -> Move Square. c, x -> Change FPS in 0.1 Hz steps.
crudeframelimiter.c
#include <SDL.h>
#define INITIAL_FPS 60
static double FPS = INITIAL_FPS;
static double FrameTime = 1.0 / INITIAL_FPS;
static double PerformancePeriod;
static double GetTime()
{
return PerformancePeriod * SDL_GetPerformanceCounter();
}
static void SetTitle(SDL_Window * window)
{
char title[48] = {0};
SDL_snprintf(title, sizeof(title), "Crude frame limiter @ %6.3f Hz", FPS);
SDL_SetWindowTitle(window, title);
}
int main(int argc, char * argv[])
{
SDL_Window * window;
int width, height;
SDL_Renderer * renderer;
SDL_Event e;
int done = 0;
int delay_threshold = 2;
double sleep_time;
SDL_Rect dst_rect = {0, 0, 25, 25};
int rect_move_left = 0;
int rect_move_right = 0;
int rect_move_up = 0;
int rect_move_down = 0;
double rect_x = 50;
double rect_y = 50;
double rect_speed = 250;
double time_counter = 0;
SDL_SetHint(SDL_HINT_RENDER_VSYNC, 0);
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_TIMER) < 0) {
SDL_Log("Failed to initialize SDL: %s", SDL_GetError());
return 1;
}
PerformancePeriod = 1.0 / SDL_GetPerformanceFrequency();
time_counter = GetTime();
window = SDL_CreateWindow("Crude frame limiter", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 800, 500, SDL_WINDOW_RESIZABLE);
if (window == NULL){
SDL_Log("Failed to create window: %s", SDL_GetError());
SDL_Quit();
return 1;
}
renderer = SDL_CreateRenderer(window, -1, 0);
if (renderer == NULL){
SDL_Log("Failed to create renderer: %s", SDL_GetError());
SDL_DestroyWindow(window);
SDL_Quit();
return 1;
}
SDL_GetWindowSize(window, &width, &height);
SetTitle(window);
while (!done) {
while (SDL_PollEvent(&e)) {
if (e.type == SDL_QUIT) {
done = 1;
} else if (e.type == SDL_KEYDOWN) {
Uint32 sym = e.key.keysym.sym;
if (!e.key.repeat) {
if (sym == SDLK_a) {
rect_move_left = 1;
} else if (sym == SDLK_d) {
rect_move_right = 1;
} else if (sym == SDLK_w) {
rect_move_up = 1;
} else if (sym == SDLK_s) {
rect_move_down = 1;
}
}
if (sym == SDLK_x) {
FPS -= 0.1;
if (FPS < 1) {
FPS = 1;
}
FrameTime = 1.0 / FPS;
SetTitle(window);
} else if (sym == SDLK_c) {
FPS += 0.1;
FrameTime = 1.0 / FPS;
SetTitle(window);
}
} else if (e.type == SDL_KEYUP) {
Uint32 sym = e.key.keysym.sym;
if (sym == SDLK_ESCAPE) {
done = 1;
} else if (sym == SDLK_f) {
if (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) {
SDL_SetWindowFullscreen(window, SDL_FALSE);
} else {
SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP);
}
} else if (sym == SDLK_a) {
rect_move_left = 0;
} else if (sym == SDLK_d) {
rect_move_right = 0;
} else if (sym == SDLK_w) {
rect_move_up = 0;
} else if (sym == SDLK_s) {
rect_move_down = 0;
}
} else if (e.type == SDL_WINDOWEVENT) {
if (e.window.event == SDL_WINDOWEVENT_RESIZED || e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
SDL_GetWindowSize(window, &width, &height);
}
}
}
/* Assuming fixed time steps. */
rect_x += (rect_move_right - rect_move_left) * rect_speed * FrameTime;
rect_y += (rect_move_down - rect_move_up) * rect_speed * FrameTime;
dst_rect.x = (int)(rect_x + 0.5);
dst_rect.y = (int)(rect_y + 0.5);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
SDL_RenderFillRect(renderer, &dst_rect);
sleep_time = time_counter - GetTime();
if (sleep_time * 1000 > delay_threshold) {
Uint32 ms = (Uint32)(sleep_time * 1000) - delay_threshold;
SDL_Delay(ms);
if (time_counter < GetTime()) {
delay_threshold++;
SDL_Log("Slept too long. Increased threshold to %u ms.", delay_threshold);
}
}
while (time_counter > GetTime()) {
/* Waiting for the right moment. */
}
SDL_RenderPresent(renderer);
time_counter += FrameTime;
}
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
I’m sure there are even more things to consider, but this is what comes to my mind right now.