Applicability of SDL_Delay and OS scheduling

I have been trying to enforce a fixed-rate timing loop for my program, which I initially implemented by busywaiting to stall any leftover time.

The only SDL function that seems to let me enforce a timing delay is SDL_Delay and its precise variants, all of which “may take longer due to OS scheduling.” I had always assumed this makes SDL_Delay completely impractical for my purposes, but a couple of sources I’ve seen don’t seem worried about this.

Handmade Penguin Chapter 18: Enforcing a Video Frame Rate claims “Linux is much better at hitting its time targets” than Windows (though it’s an old source). They use SDL_Delay to loop, but with a spare 1 ms “buffer” occupied by busy waiting.

I also asked my computer science professor, who said the delay would be negligible, at least on a modern Linux system (though I should have asked for further specification).

Now I am questioning my assumption. Is SDL_Delay as impractical as I imagined? What about the environment; how can I know if it would be, say, useless on Windows but okay on Linux?

And if SDL_Delay is unreliable, then what is its use case to begin with? It doesn’t seem useful for a loop, especially since (before SDL_DelayNS or SDL_DelayPrecise) it doesn’t have precision below 1 ms. How would one make a fixed loop if sleep functions are fundamentally unreliable?

That won’t play nice with vsync.

You can implement a basic fixed-rate game logic system with a time accumulator. Basically, measure the difference between the start of the last frame and the current one, then add it to the accumulator. When the accumulator is greater than a specific threshold (like 1/60th of a second) then, in a loop, run one tick of your game logic and subtract 1/60th second (or whatever) from the accumulator until it’s back below the threshold.

You can use this to separate your game logic from your framerate without using delays etc.

SDL_Delay() isn’t really useful for trying to hit exact framerates. For keeping in sync with the monitor, use vsync.

You can use SDL_Delay() to implement a framerate limiter, in case the user turns off vsync and you want to make sure your game doesn’t run at 9000 FPS. So if the time between the start of the last frame and the current one is less than, say, 1/300th of a second, you call SDL_Delay(1) until it’s been at least 1/300th of a second. In that situation, you don’t really care if it doesn’t return for longer than 1 ms.

4 Likes

Do not use the SDL_Delay function to put the thread to sleep between iterations of the main loop, because (at least on Windows) it is very inaccurate. At 60fps, the time window for each frame is 16.7ms, so millisecond precision is insufficient. Besides, the scheduler often puts the thread to sleep for longer than the specified delay, one millisecond or more, so entire milliseconds are lost, which can be valuable in the case of more demanding games.


SDL3 includes functions for suspending a thread for a specified number of nanoseconds — SDL_DelayNS and SDL_DelayPrecise. In combination with the SDL_GetTicksNS function, you can write a much more precise main game loop. If you need to put a thread to sleep, calculate the sleep duration and then put the thread to sleep once (avoid loops). The calculations are very simple because all of these functions operate in nanoseconds, so there’s no need to convert anything to proper time units. It still won’t be 100% accurate, as it will still depend on the OS scheduler, but it will be significantly better than one based on SDL_Delay.

I use this method in my engine, where the main loop always performs 60 logic updates per second, in regular intervals, regardless of the VSync state. The main loop iterations are executed with an accuracy of hundredths of a millisecond (from time to time it can drop to tenths of a millisecond), which is sufficient for me.

1 Like

I’m not very familiar with how VSync works, but from how you describe the accumulator, wouldn’t it still be busy waiting while counting up? Especially so in the case where VSync is disabled, and the only other method of controlling time is sleep/delay functions?

Thanks for the clarification. I only recently upgraded to SDL3, so I should have a chance to replace SDL_Delay with one of the more precise versions.

You mentioned using SDL_GetTicksNS to measure current time, but I have been using SDL_GetPerformanceCounter (since SDL2 did not have SDL_GetTicksNS). Is there a particular reason for your choice? Is it because SDL_GetPerformanceCounter is “typically used for profiling?” The documentation never specifies why this is the case so I am not sure.

A long time ago, when I was writing my first platformer game, I used (on Windows) the QueryPerformanceFrequency and QueryPerformanceCounter functions to determine the time, as well as the Sleep function to put the thread to sleep. The functions related to counters do not operate on typical units of time, but on counts, the number of which per second depends on the Windows version and/or hardware. This meant that these counts had to be manually converted to milliseconds in order to suspend the thread using the Sleep function.

To achieve a sleep time more precise than milliseconds, the sleep was performed for a smaller number of milliseconds, and the remaining sub-millisecond portion was consumed using a spinlock, implemented as a loop that repeatedly reads the counter state and executes an pause assembly instruction in each iteration. It worked very well – precisely and economically.


Later, when I was using the SDL2 library, I did something similar, except that I didn’t directly use functions from the Win32 API, but instead used the SDL_GetPerformanceFrequency, SDL_GetPerformanceCounter, and SDL_Delay functions. These functions provided the same thing, namely counters with unusual units and millisecond precision of sleeps. So I still had to manually convert the counts to milliseconds and implement spinlocks:

I implemented thread sleeping between frames in a similar way in the game engine I’m currently working on.


But when SDL3 came along, it turned out that this library uses nanoseconds as the basic unit of time. Not only do events have nanosecond timestamps, but there are also functions available to retrieve the time in nanoseconds and to put a thread to sleep for a specified number of nanoseconds, regardless of the platform. So, when adapting the engine code to the new SDL3, I was able to get rid of my own calculations and spinlocks, since SDL does it for me (and for all supported platforms).

So yeah, SDL3 significantly simplifies the implementation of a precisely functioning main game loop, therefore there is no longer a need to convert units and implement spinlocks manually, to achieve sub-millisecond sleeps precision.

1 Like

Vsync just synchronizes to the monitor’s vertical refresh. So when using something like SDL_RenderPresent() with vsync enabled the calling thread will block until everything has been sent off for rendering and the monitor’s refresh has happened. For most monitors the refresh rate will be 60hz, but that isn’t guaranteed. High refresh rate monitors are becoming more popular, and some monitors/GPUs/connectors can only do higher resolutions at 30hz.

The idea behind the accumulator is to decouple your game’s logic rate from its frame rate. If the FPS is higher than the logic rate, then you’d be rendering multiple frames for every game logic “tick”. If the rendering can’t keep up and the FPS drops below the logic rate, then you’d have multiple logic ticks per rendered frame.

So your game can keep rendering stuff (animations, etc) while the game logic (which might have computationally expensive stuff like enemy AI, physics, etc) is only done every so often.

This is especially important for physics. Having a variable physics sim rate is how you get physics jank, where objects pass through each other, players fall through the floor, etc

edit: you can still do input handling etc more often than the game logic rate if the frame rate is higher, so your game’s inputs stay snappy.

This also lets you have object movement etc partially separated from game logic rate, where instead of directly setting object position you have a MoveTo function that takes an optional time parameter that specifies how long the move should take, so that every frame your rendering logic updates the position of every moving object and they move smoothly regardless of FPS (your game logic can cancel this by calling MoveTo with different parameters and also maybe a StopMovement function)

1 Like