SDL/OpenGL 游戏在 144Hz 屏幕上运行速度过快;无法使用垂直同步
SDL/OpenGL game runs too fast on 144Hz screen; can't use vsync
我是这样处理游戏循环的:
while (running) {
diff = duration_cast<milliseconds>(end - start).count();
start = clock::now();
dt = diff / (16.0);
handleInput(); // get input
update(dt); // game logic
render(); // render game
SDL_GL_SwapWindow(window); // swap frame buffer
end = clock::now();
}
它旨在成为锁定到 60FPS 的固定时间步长游戏(它是 SNES 游戏的重制模拟)但是它在我的 144hz 屏幕上以 144 时间步长运行,速度太快了。 Vsync 无法解决这个问题,那有什么办法呢?
以下是如何实现游戏循环的简单示例:
int32_t tickInteval = 1000/FPS; // frequency in Hz to period in ms
uint32_t lastUpdateTime = 0;
int32_t deltaTime = 0;
while (running) { // running condition
uint32_t currentTime = SDL_GetTicks();
deltaTime = currentTime - lastUpdateTime;
int32_t timeToSleep = tickInteval - deltaTime;
if(timeToSleep > 0)
{
SDL_Delay(timeToSleep); // energy saving
}
update(deltaTime); // game logic
lastUpdateTime = currentTime;
}
我建议仔细研究这个主题。
UPD.
有人可能会担心 uint32_t
溢出。是的,它会溢出。经过将近两个月的不间断 运行 游戏(准确地说是 49.7 天)。那么会发生什么? currentTime
将是一个非常小的正整数,lastUpdateTime
将是一个非常大的正整数。但是二相减无论如何都不会溢出。此外,如果差异不适合 int32_t
,它将围绕 UINT_MAX + 1
的模进行环绕,从而产生一个小的正整数,这将是这两个值不同的刻度的确切数量(关于无符号溢出一个).
SDL_Delay()
和 SDL_GetTicks()
解决方案由于不精确而不理想。
我们可以热循环:
while (1)
if stopwatch.time() < 16666 // in usec
break
stopwatch.reset()
input()
update()
render()
但这并不理想,因为它会耗尽 CPU 可以放在其他地方或节省能源的循环。
我们只能演偶数帧
while (1)
if frameCount = 1
frameCount = 0
else
frameCount++
input()
update()
render()
waitForVSync // aka SDL_GL_SwapWindow(window)
但这只能让我们达到 72 fps。
如果我们需要尽可能接近 60 FPS,最好混合使用上述两种方法:对偶数帧进行热循环,直到我们达到 16.666 毫秒。
while (1)
if frameCount = 1
frameCount = 0
else
while stopWatch.time() < 16666 // in usec
break
stopwatch.reset()
frameCount++
input()
update()
render()
waitForVSync // aka SDL_GL_SwapWindow(window)
这样做的好处:您不必只跳过奇数帧,您可以将它们用于各种用途。也许在奇数帧上更新,在偶数帧上渲染?在奇数帧上应用视频过滤器?很多可能性。
注意:您可能应该整体查看帧时间。此解决方案仅在帧速率至少为 120 fps 时有效。当帧率下降到 20 秒以下,并且你想要比 OS 线程给你的更准确时,热循环是你最好的选择。
另一种选择是利用 OS 调度,这往往比 OS 线程休眠更准确。但是,仍然不能保证所有系统的准确性。
我是这样处理游戏循环的:
while (running) {
diff = duration_cast<milliseconds>(end - start).count();
start = clock::now();
dt = diff / (16.0);
handleInput(); // get input
update(dt); // game logic
render(); // render game
SDL_GL_SwapWindow(window); // swap frame buffer
end = clock::now();
}
它旨在成为锁定到 60FPS 的固定时间步长游戏(它是 SNES 游戏的重制模拟)但是它在我的 144hz 屏幕上以 144 时间步长运行,速度太快了。 Vsync 无法解决这个问题,那有什么办法呢?
以下是如何实现游戏循环的简单示例:
int32_t tickInteval = 1000/FPS; // frequency in Hz to period in ms
uint32_t lastUpdateTime = 0;
int32_t deltaTime = 0;
while (running) { // running condition
uint32_t currentTime = SDL_GetTicks();
deltaTime = currentTime - lastUpdateTime;
int32_t timeToSleep = tickInteval - deltaTime;
if(timeToSleep > 0)
{
SDL_Delay(timeToSleep); // energy saving
}
update(deltaTime); // game logic
lastUpdateTime = currentTime;
}
我建议仔细研究这个主题。
UPD.
有人可能会担心 uint32_t
溢出。是的,它会溢出。经过将近两个月的不间断 运行 游戏(准确地说是 49.7 天)。那么会发生什么? currentTime
将是一个非常小的正整数,lastUpdateTime
将是一个非常大的正整数。但是二相减无论如何都不会溢出。此外,如果差异不适合 int32_t
,它将围绕 UINT_MAX + 1
的模进行环绕,从而产生一个小的正整数,这将是这两个值不同的刻度的确切数量(关于无符号溢出一个).
SDL_Delay()
和 SDL_GetTicks()
解决方案由于不精确而不理想。
我们可以热循环:
while (1)
if stopwatch.time() < 16666 // in usec
break
stopwatch.reset()
input()
update()
render()
但这并不理想,因为它会耗尽 CPU 可以放在其他地方或节省能源的循环。
我们只能演偶数帧
while (1)
if frameCount = 1
frameCount = 0
else
frameCount++
input()
update()
render()
waitForVSync // aka SDL_GL_SwapWindow(window)
但这只能让我们达到 72 fps。
如果我们需要尽可能接近 60 FPS,最好混合使用上述两种方法:对偶数帧进行热循环,直到我们达到 16.666 毫秒。
while (1)
if frameCount = 1
frameCount = 0
else
while stopWatch.time() < 16666 // in usec
break
stopwatch.reset()
frameCount++
input()
update()
render()
waitForVSync // aka SDL_GL_SwapWindow(window)
这样做的好处:您不必只跳过奇数帧,您可以将它们用于各种用途。也许在奇数帧上更新,在偶数帧上渲染?在奇数帧上应用视频过滤器?很多可能性。
注意:您可能应该整体查看帧时间。此解决方案仅在帧速率至少为 120 fps 时有效。当帧率下降到 20 秒以下,并且你想要比 OS 线程给你的更准确时,热循环是你最好的选择。
另一种选择是利用 OS 调度,这往往比 OS 线程休眠更准确。但是,仍然不能保证所有系统的准确性。