this_thread::sleep_for / SDL 渲染跳过指令

this_thread::sleep_for / SDL Rendering Skips instructions

我正在尝试使用 SDL2 制作一个排序可视化工具,一切正常,除了一件事,等待时间。

排序可视化工具有延迟,我可以将其更改为任何我想要的,但是当我将其设置为 1 毫秒左右时,它会跳过一些指令。

这是 10 毫秒与 1 毫秒:

10ms delay

1ms delay

该视频展示了 1 毫秒的延迟实际上并没有完成排序: Picture of 1ms delay algorithm completion.

我怀疑问题出在我使用的等待函数上,我正试图让这个程序成为多平台的,所以几乎没有选择。

这是代码片段:

选择排序代码(在视频中显示):

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();
        
    }
}  

一些变量需要解释:

我已经削减了代码,并删除了其他算法以制作一个可重现的示例,而不是渲染和使用 cout 似乎有效,但我仍然无法确定问题是否出在 render等待函数:

#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();
}

问题是 ticks= SDL_GetTicks() + 16;,因为对于一毫秒的等待来说,这些滴答声太多了,而且 if(SDL_GetTicks() > ticks) 条件在大多数情况下都是假的。

如果你 1ms 等待 ticks= SDL_GetTicks() + 5 它会起作用。

selectionSort循环中,如果在最后,比如说,八次迭代中,if(SDL_GetTicks() > ticks)跳过绘图,循环很可能完成并让一些未决绘图。

不是算法没有完成,而是在ticks达到足够高的数字以允许绘图之前完成。

主要问题是您通过使所有渲染都依赖于 if 条件来放弃对屏幕的更新:

if(SDL_GetTicks() > ticks)

我的测试表明,只有大约每 70 调用函数 render 才会实际渲染。所有其他调用均按此 if 条件过滤。

这个极高的数字是因为您不仅在外循环中而且在内循环中都调用了函数 render。我看不出为什么它也应该在内循环中调用。在我看来,它应该只在外循环中调用。

如果你只在外层循环中调用它,那么大约每 16th 次函数调用都会被渲染。

但是,这仍然意味着最后一次调用render函数只有16分之一的机会被渲染。因此,您程序的最后渲染并不代表最后的排序步骤也就不足为奇了。

如果你想确保最后一个排序步骤得到渲染,你可以在排序完成后无条件地执行一次渲染代码。然而,这可能不是理想的解决方案,因为我认为您应该首先对程序的行为方式做出更基本的决定:

在您的问题中,您在 render 的调用之间使用了 1 毫秒的延迟。这意味着您的程序设计为每秒渲染 1000 帧。但是,您的显示器可能每秒只能显示大约 60 帧(某些游戏显示器可以显示更多)。在这种情况下,每个显示帧持续至少 16.7 毫秒。

因此,您必须决定您希望您的程序如何针对监视器运行。你可以让你的程序

  1. 排序速度比您的显示器可以显示单个排序步骤的速度快,因此不会呈现所有排序步骤,或者
  2. 排序 比您的显示器显示单个排序步骤慢,因此所有排序步骤都由显示器显示至少一帧,可能是几帧,或
  3. 以与您的显示器可以显示的速度完全相同的速度排序,因此一个排序步骤恰好显示显示器的一帧。

实施#3 是最简单的。因为您在 SDL_CreateRenderer 的函数调用中启用了 VSYNC,SDL 会自动将渲染次数限制为显示器的显示速率。因此,您不必在代码中执行任何额外的等待,并且可以删除行

this_thread::sleep_for(waitTime);

来自函数 selectionSort。此外,由于 SDL 比您更了解您的监视器是否已准备好绘制下一帧,因此您尝试自己限制帧数似乎并不合适。所以你可以删除行

if(SDL_GetTicks() > ticks) {

和函数 render.

中相应的右大括号

另一方面,最好保留if语句。在这种情况下,帧速率限制器可能应该设置为远高于 60 fps(可能是 100-200 fps),以确保帧足够快地传递到 SDL。


实施 #1 更难,因为它实际上需要您 select 哪些排序步骤要呈现,哪些不呈现。因此,为了实现#1,您可能需要保留上面提到的 if 语句,这样渲染只会有条件地发生。

然而,使 if 语句依赖于自上次渲染以来经过的时间似乎没有意义,因为在等待时,排序将继续全速进行,因此可能所有排序将只用一帧渲染完成。您目前正在通过使用行

减慢排序来防止这种情况发生

this_thread::sleep_for(waitTime);

在函数selectionSort中。但这似乎不是一个理想的解决方案,而是权宜之计。

与其让 if 条件依赖于时间,不如让它依赖于自上次渲染以来的排序步骤数会更容易。这样,例如,您可以按照每 5th 排序步骤进行渲染的方式对其进行编程。在那种情况下,将不再需要额外减慢实际排序,您的代码也会更简单。

如上所述,在实施#1 时,您还必须确保不会放弃最后的渲染步骤,或者至少在排序完成后渲染最后一帧。否则,最后一帧可能不会显示已完成的排序,而是显示中间排序步骤。


实施#2 与实施#1 类似,只是您必须使用 SDL_Delay (which is equivalent this_thread::sleep_for) or SDL_AddTimer 来确定何时渲染下一个排序步骤。

使用 SDL_AddTimer 需要您 handle SDL Events. However, I would recommend that you do this anyway, because that way, you will also be able to handle SDL_QUIT 事件,这样您就可以通过关闭 window 来关闭您的程序。这也会使行

this_thread::sleep_for( 5000ms );

在你的程序结束时不必要,因为你可以改为等待用户关闭 window,像这样:

for (;;)
{
    SDL_Event event;
    SDL_WaitEvent( &event );

    if ( event.type == SDL_QUIT ) break;
}

但是,如果您重组整个程序可能会更好,这样您就只有一个消息循环,它响应 SDL Timer 和 SDL_QUIT 事件。