I am writing a little snake game in C, using SDL, on Windows.
On each frame I move each square a constant amount of pixels (e.g. 2 pixels).
As long as I have Vsync enabled, the movement is very smooth. However, I was trying to achieve the same smoothness without Vsync, just to learn how things work, and after a lot of struggle I have managed to time the cycles down to almost a perfect 0.016667 seconds (60Hz monitor here) by using the Performance Counter to measure repeated SDL_Delay(1) until 0.014 seconds of a cycle + an empty loop up to 0.016667s. This works great (so it's not a matter of SDL_Delay(1) taking more than it should) I guess, because 1 in 1000 frames is off by 0.1ms, and this should not be noticeable.
But the movement is still jerky sometimes - as if it doesn't move at all for one frame and then catches up double on the next. This I think boils down to the fact that the game is updating and rendering when it should, but the actual display on the monitor takes place a little sooner than it should, and then a little later than it should.
So I did some time measurements and the results are very surprising to me - see attached graphs. With Vsync enabled, the measured cycle times are all over the place - much much worse than the non-vsynced version - and I was expecting a perfect constant time. So the question is: Is the actual time between 2 consecutive drawings to the physical monitor not constant? Because if that is the case there is no way you can achieve what I am trying without some sort of feedback of when exactly the last drawing took place...
P.S. The update and rendering are very very fast - e.g. if the cycle is not limited it can go millions of times per second. So that is not slowing the cycle down.
#include <SDL.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <windows.h>
typedef enum {false, true} bool;
#define NONE 0
#define QUIT 1
#define LEFT 2
#define RIGHT 3
#define UP 4
#define DOWN 5
typedef struct Color
{
char R, G, B, A;
}Color;
typedef struct Square
{
int x, y, side, dir;
Color color;
}Square;
const int WINDOW_W = 600;
const int WINDOW_H = 800;
const int SQ_SIDE = 48;
const Color SQ_COLOR = {0, 0, 0x99, 0xFF};
const Color BG_COLOR = {0xFF, 0xFF, 0xFF, 0xFF};
SDL_Window* g_window = NULL;
SDL_Renderer* g_renderer = NULL;
float speed = 120; /* pixels per second */
int display_refresh_rate;
bool vsync_enabled = false; /* enable/disable vsync from here */
bool clear_screen(void);
void draw_rect(int x, int y, int width, int height, Color color);
void update_square_position(Square *square);
int process_input_events();
int main(int argc, char* args[])
{
SDL_Init(SDL_INIT_VIDEO);
g_window = SDL_CreateWindow("Snake", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WINDOW_W, WINDOW_H, SDL_WINDOW_SHOWN);
int renderer_flags = SDL_RENDERER_ACCELERATED;
if(vsync_enabled) renderer_flags |= SDL_RENDERER_PRESENTVSYNC;
g_renderer = SDL_CreateRenderer(g_window, -1, renderer_flags);
SDL_SetRenderDrawBlendMode(g_renderer, SDL_BLENDMODE_BLEND);
int display_index;
SDL_DisplayMode display_mode;
display_index = SDL_GetWindowDisplayIndex(g_window);
SDL_GetDesktopDisplayMode(display_index, &display_mode);
display_refresh_rate = display_mode.refresh_rate;
float target_cycle_time = 1.0f / display_refresh_rate;
clear_screen();
unsigned long long tick_now = SDL_GetPerformanceCounter();
unsigned long long last_render_tick = tick_now;
float time_from_last_render = 0.0f;
float render_times_array[3601];
float perf_freq = (float)SDL_GetPerformanceFrequency();
timeBeginPeriod(1);
Square *square = (Square*) malloc(sizeof(Square));
square->x = 50;
square->y = 50;
square->color = SQ_COLOR;
square->side = SQ_SIDE;
square->dir = RIGHT;
int i = 0;
while(1)
{
tick_now = SDL_GetPerformanceCounter();
time_from_last_render = (tick_now - last_render_tick) / perf_freq;
if(time_from_last_render < target_cycle_time - 0.002f)
SDL_Delay(1);
if(vsync_enabled || (time_from_last_render >= target_cycle_time))
{
if(process_input_events()== QUIT) break;
update_square_position(square);
clear_screen();
draw_rect(square->x, square->y, SQ_SIDE, SQ_SIDE, square->color);
SDL_RenderPresent(g_renderer);
last_render_tick = tick_now;
/* save 3600 timestamps and print them */
render_times_array[i++] = time_from_last_render;
if(i >= 3600)
{
clear_screen();
int j;
for(j = 0; j < i; j++)
printf("%f
", render_times_array[j]);
i=0;
break;
}
}
}
SDL_DestroyWindow(g_window);
SDL_DestroyRenderer(g_renderer);
SDL_Quit();
return 0;
}
bool clear_screen(void)
{
/* SDL functions return 0 on success! */
if(SDL_SetRenderDrawColor(g_renderer, BG_COLOR.R, BG_COLOR.G, BG_COLOR.B, BG_COLOR.A))
return false;
else if(SDL_RenderClear(g_renderer))
return false;
return true;
}
void draw_rect(int x, int y, int width, int height, Color color)
{
SDL_Rect rect = {x, y, width, height};
SDL_SetRenderDrawColor(g_renderer, color.R, color.G, color.B, color.A);
SDL_RenderFillRect(g_renderer, &rect);
}
void update_square_position(Square *square)
{
int step = speed / display_refresh_rate;
if(square->dir == RIGHT)
{
square->x += step;
if(square->x > WINDOW_W - 100) square->dir = DOWN;
}
if(square->dir == LEFT)
{
square->x -= step;
if(square->x < 50) square->dir = UP;
}
if(square->dir == DOWN)
{
square->y += step;
if(square->y > WINDOW_H - 100) square->dir = LEFT;
}
if(square->dir == UP)
{
square->y -= step;
if(square->y < 50) square->dir = RIGHT;
}
}
int process_input_events()
{
SDL_Event event_handler;
while(SDL_PollEvent(&event_handler) != 0)
{
if(event_handler.type == SDL_QUIT)
return QUIT;
}
return NONE;
}
See Question&Answers more detail:
os