SDL Frame limiter

Hi,

I currently have a rectangle wich I can move with a and d.
I also made this frame limiter:

const int FPS = 60;
const int frameDelay = 1000 / FPS;

//SDL initializing

while (!quit) {

	frameStart = SDL_GetTicks();

	//Updating and drawing

	frameTime = SDL_GetTicks() - frameStart;

	if (frameDelay > frameTime) {

		SDL_Delay(frameDelay - frameTime);

	}

}

But when I set the FPS to 60 its lagging very much, although if I set it to 600 it’s very smooth.
It’s definitely not me because my monitor probably has a 60 hz refresh rate.

How can I fix this?

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.