关于渲染循环策略的思考

时间:2012-07-09 21:22:00

标签: multithreading synchronization rendering game-engine

我知道这些考虑因素的数百种变体已在网上发布。但是,我没有发现任何可以解决我确切问题的事情,所以我希望你能帮助我看清楚。

我目前正在使用OpenGL在Java中进行2D游戏开发。使用的语言和图形库与我的问题无关,因为它具有更一般的特征。

我正在尝试设计一个通用游戏循环,可以或多或少地用于任何具有中等重度图形(主要是位图纹理)的游戏,甚至可能更重的游戏逻辑(AI,碰撞检测等)

基本上我期望维护一个可以更新的对象的列表/数组/缓冲区(位置,速度和其他游戏相关的更新)和渲染(更新位置的纹理/帧)不会像光滑和有效一样停止可能的。

1)一个用于更新+渲染的线程

我已经尝试并且只使用一个线程丢弃了一个顺序解决方案(当计算用户输入时,两个线程就完成了。)

  • 计算更改并更新缓冲区对象
  • 将更新位置的纹理渲染为后备缓冲区
  • 将backbuffer交换到前面

显然,当交换缓冲区在硬件上阻塞时会浪费很多好的计算时间,这需要更高效的解决方案

2)一个用于更新的线程,一个用于渲染的线程

通过将程序拆分为更新线程和渲染线程并同步对共享缓冲区的访问,我应该能够确保相当稳定的帧速率。同步对共享缓冲区的访问可以通过多种方式完成,但它们都有一个共同点。它们都禁止线程并发。虽然这可能是一个公平的权衡,但我想知道是什么使得同步成为必要。

3)与2相同,但没有同步

我确实理解了不小心实现并发线程可能导致的许多问题。生产者/消费者,读者/作者和类似的情况导致潜在的僵局。但是,如果满足以下条件(并且它们应该是),我不明白为什么我需要确保共享数据的同步:

  • 渲染线程只能从共享缓冲区中读取
  • 更新线程既可以读取共享缓冲区,也可以写入共享缓冲区(因此它是唯一的“编写者”)
  • 在游戏运行时,共享缓冲区永远不会为空或
  • 线程永远不会睡觉
  • 渲染不必100%准确。如果某些共享对象尚未更新,导致它们是其他对象后面的一个更新步骤(即大约10-20毫秒),则没有人会注意到。

-

那么......我在这里错过了哪些明显的东西?为什么我需要同步进行此设置?

  • 线程是否会在未正确同步时缓存导致问题的数据?
  • 或者,如果写作线程在不幸的时间中断,数据可能会出现乱码吗?
  • 或者我的拟议政策是否有任何普遍问题使其无用?

欢迎提出任何想法,意见或建议。或者,如果这个特定问题已在其他地方得到解决,我将不胜感激。

3 个答案:

答案 0 :(得分:2)

我决定花一点时间测试这个,因为我从这个网站得到了很多好的答案,我想我会发布这个来完成这个问题。也许别人会发现这些信息很有用。

我做了一个简单的精灵渲染应用程序的3个不同的实现,其中更新和渲染在不同的线程中运行。

1)没有同步

渲染器最高运行速度为60 FPS。 Updater运行速度尽可能快 要更新和呈现的精灵存在于两个线程共享的列表中。没有同步,因此线程只是随意读取和写入数据。

2)共享数据的同步

渲染器最高运行速度为60 FPS。 Updater以与渲染器相同的速度运行 要更新和呈现的数据存在于两个线程共享的列表中。该列表完全同步。 Updater更新列表中的所有精灵。然后渲染器获得对列表的访问权限,并将所有精灵渲染到屏幕上。

3)同步使用了双重渲染队列

渲染器最高运行速度为60 FPS。 Updater以与渲染器相同的速度运行 Updater更新列表并将sprite发送到2渲染队列的被动队列。同时,渲染器在活动渲染队列中渲染精灵。当Updater将最后一个对象复制到被动渲染队列时,它会尝试交换主动和被动队列。如果渲染器未完成呈现前一个队列,则交换将阻止。这是唯一的阻塞同步。一旦渲染器完成当前帧,就进行交换,渲染器可以开始渲染新队列,并且Updater可以开始更新并发送到另一个(现在是被动的)队列。

我在每个方法上运行了3次测试,在这里我计算了每秒执行更新和渲染的次数。

测试1:
精灵的数量足够低,因此渲染器可以全速运行(60 FPS) 每个精灵的更新逻辑太重,不允许更新程序保持同步。

测试2:
精灵的数量太高,以至于渲染器无法全速运行 每个精灵的更新逻辑非常简单,因此它们可以跟上。

测试3:
精灵的数量恰好足以使渲染器保持低于最大速度 每个精灵的更新逻辑都非常重,足以让Updater保持低于渲染器的最大速度。

结果

无同步 - 测试1:
渲染器每秒运行60次(最大速度) Updater每秒运行45次。

无同步 - 测试2:
渲染器每秒运行24次 Updater每秒运行1150次。

无同步 - 测试3:
渲染器每秒运行58次 Updater每秒运行51次。

同步共享数据 - 测试1:
渲染器每秒运行23次(最大速度) Updater每秒运行24次。

同步共享数据 - 测试2:
渲染器每秒运行23次 Updater每秒运行23次。

同步共享数据 - 测试3:
渲染器每秒运行17次 Updater每秒运行17次。

同步双重排队 - 测试1:
渲染器每秒运行43次(最大速度) Updater每秒运行43次。

同步双重排队 - 测试2:
渲染器每秒运行24次 Updater每秒运行24次。

同步双重排队 - 测试3:
渲染器每秒运行54次 Updater每秒运行54次。

结论

正如你所指出的那样,Jirka,即使没有同步的方法似乎无害,只有一个作家可能会产生不必要的副作用,并且它肯定不会保持渲染帧的一致性。

使用双队列渲染比使用一个大型共享精灵列表渲染更快,这并不奇怪。然而,令人惊讶的是,如果你考虑到这样一个事实:在没有更新的情况下渲染多个帧没有任何好处,也没有渲染多次更新,那么双队列方法的最终结果实际上与非同步方法一样快。

可能有其他事情可以说或尝试过,但我已经看够了。我永远不会考虑再次使用Update / Render系统的非同步访问..

答案 1 :(得分:1)

可以在没有(很多)同步的情况下拥有一个独立的渲染和更新线程。看看

http://blog.slapware.eu/game-engine/programming/multithreaded-renderloop-part1/

http://blog.slapware.eu/game-engine/programming/multithreaded-renderloop-part2/

用于解释和实现(源+二进制文件)。这并不容易,但它正是你想要的。

答案 2 :(得分:0)

不同步的方法对你来说很合适;如果这是股票英特尔硬件 1 ,那么加倍。我还是不会用它。

非同步并发几乎永远不可靠的原因是处理器可以自由地在主RAM和缓存之间进行存储和加载。这可以破坏几乎任何不同步的协议。但是,正如你所说,没有人会注意到你的应用中场景是否从未突然改变;所有数据都会转到RAM中,迟早会被其他线程看到。

然而,您无法保证何时会以及在哪个序列中,这使您理论上可能会以奇怪的方式混合两个后续帧(在场景或其光照突然改变之前和之后)。

根据您的编程语言及其内存模型(我认为C ++早于C ++ 11?),您可能会发现轻量级同步原语,其保证的副作用是适当的内存屏障,其对性能的影响可以忽略不计。这是我建议的起点。极端性能优化(超出可证明安全性)应该是优化引擎的最后阶段。


1 )i86从不重新排序商店。我不认为这在任何地方都有记录,我不想依赖它。您仍然可以重新排序读取,因此无论如何在您的方案中都无济于事。