如何降低OpenGL的CPU使用率和/或如何正确使用OpenGL

4
我正在使用OpenGL构建一个迷宫小老鼠模拟应用,并且我有一种预感我没有正确地处理事情。特别是,我怀疑我以接近恒定的帧速率(60 FPS)刷新我的(大部分静态)图形的方式。我的方法如下:
1)启动计时器 2)绘制我的形状和文本(约一千个):
glBegin(GL_POLYGON);
for (Cartesian vertex : polygon.getVertices()) {
    std::pair<float, float> coordinates = getOpenGlCoordinates(vertex);
    glVertex2f(coordinates.first, coordinates.second);
}   
glEnd();

并且

glPushMatrix();
glScalef(scaleX, scaleY, 0);
glTranslatef(coordinates.first * 1.0/scaleX, coordinates.second * 1.0/scaleY, 0);
for (int i = 0; i < text.size(); i += 1) {
    glutStrokeCharacter(GLUT_STROKE_MONO_ROMAN, text.at(i));
}
glPopMatrix();

3) 调用

glFlush();

4) 停止计时器
5) 休眠 (1/FPS - 持续时间) 秒
6) 调用

glutPostRedisplay();

"问题"在于上述方法会严重占用我的CPU,进程使用率达到了96-100%。我知道使用大量CPU本身并没有什么问题,但我觉得我不应该一直使用那么多。而最让人头疼的是,大部分图形从一帧到另一帧都没有变化。实际上只是一个多边形移动并遮盖一些静态形状。有没有办法告诉OpenGL只重绘自上一帧以来发生了变化的部分(这样可以减少glxxx调用的次数,我认为这是"问题"的根源)?或者,更好的方法是我正确地刷新了我的图形吗?"

如果您不想使用VBO而是继续使用glBegin/glEnd,那么getOpenGlCoordinatescoordinates.first、coordinates.second看起来像函数和类属性的访问,需要编写代码。这些内容不应该在glBegin/gllEnd内部,因为传递原始数据的数组速度更快。如果传递原始数据,那么glVertex2fvglVertex2f更快。 - Spektre
2个回答

6
首先,使用OpenGL时最大的CPU消耗在于即时模式...而你正在使用它(glBegin、glEnd)。即时模式存在的问题是,每个顶点需要进行一系列的OpenGL调用;由于OpenGL使用线程本地状态,这意味着每个OpenGL调用都必须经过某种间接方式。因此,第一步就是摆脱这个问题。
下一个问题与您如何计时显示有关。如果用户输入和显示之间的低延迟不是您的终极目标,那么标准方法是设置双缓冲窗口,启用V-Sync,将交换间隔设置为1,并在渲染帧后进行缓冲区交换(glutSwapBuffers)。确切的时间和阻塞位置取决于实现(不幸的是),但只要您的渲染器能够跟上(即渲染一帧所需的时间小于屏幕刷新间隔),您就可以保证完全命中屏幕刷新频率。 glutPostRedisplay仅仅是为主循环设置了一个标志,以便在没有更多事件挂起时调用显示函数,因此通过它来计时帧重绘并不是非常准确。
最后但并非不重要的是,你可能会被 Windows 的 CPU 时间计算方式嘲笑(时间花费在驱动上下文中,其中包括阻塞、等待 V-Sync),这将被视为消耗的 CPU 时间,而实际上它是可中断的睡眠。然而,你写道,你的代码中已经有了一个 sleep,这将排除这种情况,因为获得更合理的计算的常见方法是在缓冲区交换之前或之后添加一个 Sleep(1)。

仅作澄清,我正在Ubuntu上进行开发,而不是Windows。我会尝试避免使用“即时模式”,看看是否有效。谢谢! - mackorone
@mackorone:你似乎已经在一个数组中组织好了顶点。最简单的方法是告诉OpenGL关于该数组的信息(使用glVertexPointerglEnableClientState(GL_VERTEX_ARRAY)),然后用glDrawArray绘制其全部内容。 - datenwolf
是的,我的顶点被存储在一个向量中。这意味着OpenGL可以一次理解所有这些顶点吗?我对这个双缓冲还很陌生,所以我仍然在试图弄清楚我需要做什么... - mackorone
@mackorone:双缓冲是完全不同的东西。本质上,使用双缓冲时,您首先绘制到“隐藏”的后备缓冲区,然后仅在完成后告诉系统将该后备缓冲区成为可见的前置缓冲区(交换前后缓冲区)。是的,OpenGL可以一次“理解”所有这些顶点。事实上,自OpenGL-1.1推出了近20年来,整个glVertex调用业务已经过时,并引入了顶点数组支持。 - datenwolf

2
我发现将渲染线程休眠可以将 CPU 使用率从(我的情况)26%降低到约8%。
#include <chrono>
#include <thread>

void render_loop(){
  ...
  auto const start_time = std::chrono::steady_clock::now();
  auto const wait_time = std::chrono::milliseconds{ 17 };
  auto next_time = start_time + wait_time;
  while(true){
    ...
    // execute once after thread wakes up every 17ms which is theoretically 60 frames per 
    // second
    auto then = std::chrono::high_resolution_clock::now();
    std::this_thread::sleep_until(next_time);

    ...rendering jobs

    auto elasped_time = 
    std::chrono::duration_cast<std::chrono::milliseconds> (std::chrono::high_resolution_clock::now() - then);
    std::cout << "ms: " << elasped_time.count() << '\n';
    next_time += wait_time;
  }
}

我考虑在线程休眠时尝试测量帧率,但我的使用情况没有这样的必要。结果平均约为16毫秒,所以我认为它已经足够好了。
这篇文章的启发。

虽然std::chrono::milliseconds很好,但我发现使用::nanoseconds会有更好的精度(请确保您使用64位类型,如doubles)。这是可以察觉到的。但是,并不是所有系统都能找到如此精确的时钟。如果有一个可用的,就用它,否则它将默认为标准静默。话虽如此,等待可以通过nanosleep完成。 - user2262111

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接