Search code examples
c++sdlsleep

this_thread::sleep_for / SDL Rendering Skips instructions


I'm trying to make a sorting visualizer with SDL2, everything works except one thing, the wait time.

The sorting visualizer has a delay, I can change it to whatever i want, but when I set it to around 1ms it skips some instructions.

Here is 10ms vs 1ms:

10ms delay

1ms delay

The video shows how the 1ms delay doesn't actually finish sorting: Picture of 1ms delay algorithm completion.

I suspect the problem being the wait function I use, I'm trying to make this program multi-platform so there are little to no options.

Here's a snippet of the code:

Selection Sort Code (Shown in videos):

void selectionSort(void)  
{  
    int minimum;  
  
    // One by one move boundary of unsorted subarray  
    for (int i = 0; i < totalValue-1; i++)  
    {  
        // Find the minimum element in unsorted array  
        minimum = i;  
        for (int j = i+1; j < totalValue; j++){
            if (randArray[j] < randArray[minimum]){
                minimum = j;
                lineColoration[j] = 2;
                render();
            }
        }
        lineColoration[i] = 1;
  
        // Swap the found minimum element with the first element  
        swap(randArray[minimum], randArray[i]);
        this_thread::sleep_for(waitTime);
        render();
        
    }
}  

Some variables need explanation:

  • totalValue is the amount of values to be sorted (user input)
  • randArray is a vector that stores all the values
  • waitTime is the amount of milliseconds the computer will wait each time (user input)

I've cut the code down, and removed other algorithms to make a reproducible example, not rendering and using cout seems to work, but I still cant pin down if the issue is the render or the wait function:

#include <algorithm>
#include <chrono>
#include <iostream>
#include <random>
#include <thread>
#include <vector>
#include <math.h>

SDL_Window* window;
SDL_Renderer* renderer;

using namespace std;

vector<int> randArray;
int totalValue= 100;
auto waitTime= 1ms;
vector<int> lineColoration;
int lineSize;
int lineHeight;
Uint32 ticks= 0;

void OrganizeVariables()
{
    randArray.clear();
    for(int i= 0; i < totalValue; i++)
        randArray.push_back(i + 1);
    auto rng= default_random_engine{};
    shuffle(begin(randArray), end(randArray), rng);

    lineColoration.assign(totalValue,0);
}

int create_window(void)
{
    window= SDL_CreateWindow("Sorting Visualizer", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 1800, 900, SDL_WINDOW_SHOWN);
    return window != NULL;
}

int create_renderer(void)
{
    renderer= SDL_CreateRenderer(
                  window, -1, SDL_RENDERER_PRESENTVSYNC); // Change SDL_RENDERER_PRESENTVSYNC to SDL_RENDERER_ACCELERATED
    return renderer != NULL;
}


int init(void)
{

    if(SDL_Init(SDL_INIT_VIDEO) != 0)
        goto bad_exit;
    if(create_window() == 0)
        goto quit_sdl;
    if(create_renderer() == 0)
        goto destroy_window;

    cout << "All safety checks passed succesfully" << endl;
    return 1;


destroy_window:
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
quit_sdl:
    SDL_Quit();
bad_exit:
    return 0;
}

void cleanup(void)
{
    SDL_DestroyWindow(window);
    SDL_Quit();
}

void render(void)
{

    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
    SDL_RenderClear(renderer);

    //This is used to only render when 16ms hits (60fps), if true, will set the ticks variable to GetTicks() + 16
    if(SDL_GetTicks() > ticks) {
        for(int i= 0; i < totalValue - 1; i++) {
            // SDL_Rect image_pos = {i*4, 100, 3, randArray[i]*2};
            SDL_Rect fill_pos= {i * (1 + lineSize), 100, lineSize,randArray[i] * lineHeight};
            switch(lineColoration[i]) {
            case 0:
                SDL_SetRenderDrawColor(renderer,255,255,255,255);
                break;
            case 1:
                SDL_SetRenderDrawColor(renderer,255,0,0,255);
                break;
            case 2:
                SDL_SetRenderDrawColor(renderer,0,255,255,255);
                break;
            default:
                cout << "Error, drawing color not defined, exting...";
                cout << "Unkown Color ID: " << lineColoration[i];
                cleanup();
                abort();
                break;
            }
            SDL_RenderFillRect(renderer, &fill_pos);
        }
        SDL_RenderPresent(renderer);
        lineColoration.assign(totalValue,0);
        ticks= SDL_GetTicks() + 16;
    }
}
void selectionSort(void)
{
    int minimum;

    // One by one move boundary of unsorted subarray
    for (int i = 0; i < totalValue-1; i++) {
        // Find the minimum element in unsorted array
        minimum = i;
        for (int j = i+1; j < totalValue; j++) {
            if (randArray[j] < randArray[minimum]) {
                minimum = j;
                lineColoration[j] = 2;
                render();
            }
        }
        lineColoration[i] = 1;

        // Swap the found minimum element with the first element
        swap(randArray[minimum], randArray[i]);
        this_thread::sleep_for(waitTime);
        render();

    }
}
int main(int argc, char** argv)
{
    //Rough estimate of screen size
    lineSize= 1100 / totalValue;
    lineHeight= 700 / totalValue;


    create_window();
    create_renderer();
    OrganizeVariables();
    selectionSort();
    this_thread::sleep_for(5000ms);
    cleanup();
}

Solution

  • The main problem is that you are dropping updates to the screen by making all rendering dependant on an if condition:

    if(SDL_GetTicks() > ticks)

    My tests have shown that only about every 70th call to the function render actually gets rendered. All other calls are filtered by this if condition.

    This extremely high number is because you are calling the function render not only in your outer loop, but also in the inner loop. I see no reason why it should also be called in the inner loop. In my opinion, it should only be called in the outer loop.

    If you only call it in the outer loop, then about every 16th call to the function is actually rendered.

    However, this still means that the last call to the render function only has a 1 in 16 chance of being rendered. Therefore, it is not surprising that the last render of your program does not represent the last sorting step.

    If you want to ensure that the last sorting step gets rendered, you could simply execute the rendering code once unconditionally, after the sorting has finished. However, this may not be the ideal solution, because I believe you should first make a more fundamental decision on how your program should behave:

    In your question, you are using delays of 1ms between calls to render. This means that your program is designed to render 1000 frames per second. However, your monitor can probably only display about 60 frames per second (some gaming monitors can display more). In that case, every displayed frame lasts for at least 16.7 milliseconds.

    Therefore, you must decide how you want your program to behave with regard to the monitor. You could make your program

    1. sort faster than your monitor can display individual sorting steps, so that not all of the sorting steps are rendered, or
    2. sort slower than your monitor can display individual sorting steps, so that all sorting steps are displayed by the monitor for at least one frame, possibly several frames, or
    3. sort at exactly the same speed as your monitor can display, so that one sorting step is displaying for exactly one frame by the monitor.

    Implementing #3 is the easiest of all. Because you have enabled VSYNC in the function call to SDL_CreateRenderer, SDL will automatically limit the number of renders to the display rate of your monitor. Therefore, you don't have to perform any additional waiting in your code and can remove the line

    this_thread::sleep_for(waitTime);

    from the function selectionSort. Also, since SDL knows better than you whether your monitor is ready for the next frame to be drawn, it does not seem appropriate that you try to limit the number of frames yourself. So you can remove the line

    if(SDL_GetTicks() > ticks) {

    and the corresponding closing brace from the function render.

    On the other hand, it may be better to keep the if statement to prevent the massively high frame rates in case SDL doesn't limit them properly. In that case, the frame rate limiter should probably be set well above 60 fps, though (maybe 100-200 fps), to ensure that the frames are passed fast enough to SDL.


    Implementing #1 is harder, as it actually requires you to select which sorting steps to render and which ones not to render. Therefore, in order to implement #1, you will probably need to keep the if statement mentioned above, so that rendering only occurs conditionally.

    However, it does not seem meaningful to make the if statement dependant on elapsed time since the last render, because while wating, the sorting will continue at full speed and it is therefore possible that all of the sorting will be completed with only one frame of rendering. You are currently preventing this from happending by slowing down the sort by using the line

    this_thread::sleep_for(waitTime);

    in the function selectionSort. But this does not seem like an ideal solution, but rather a stopgap measure.

    Instead of making the if condition dependant on time, it would be easier to make it dependant on the number of sorting steps since the last render. That way, you could, for example, program it in such a way that every 5th sorting step gets rendered. In that case, there would be no need anymore to additionally slow down the actual sorting and your code would be simpler.

    As already described above, when implementing #1, you will also have to ensure that you do not drop the last rendering step, or that you at least render the last frame after the sorting is finished. Otherwise, the last frame will likely not display the completed sort, but rather an intermediate sorting step.


    Implementing #2 is similar to implementing #1, except that you will have to use SDL_Delay (which is equivalent this_thread::sleep_for) or SDL_AddTimer to determine when it is time to render the next sorting step.

    Using SDL_AddTimer would require you to handle SDL Events. However, I would recommend that you do this anyway, because that way, you will also be able to handle SDL_QUIT events, so that you can close your program by closing the window. This would also make the line

    this_thread::sleep_for( 5000ms );

    at the end of your program unnecessary, because you could instead wait for the user to close the window, like this:

    for (;;)
    {
        SDL_Event event;
        SDL_WaitEvent( &event );
    
        if ( event.type == SDL_QUIT ) break;
    }
    

    However, it would probably be better if you restructured your entire program, so that you only have one message loop, which responds to both SDL Timer and SDL_QUIT events.