2

我知道这些考虑的数百种变体已经发布在整个网络上。但是,我还没有找到任何可以解决我的确切问题的东西,所以我希望你能帮助我看到光明。

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

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

基本上,我希望维护一个可以更新(位置、速度和其他与游戏相关的更新)和渲染(更新位置的纹理/帧)的对象列表/数组/缓冲区,尽可能流畅和高效。

1)一个线程更新+渲染

我已经尝试并放弃了仅使用一个线程的顺序解决方案(好吧,计算用户输入时是两个)。

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

显然,当交换缓冲区阻塞硬件时,浪费了大量的计算时间,这需要更有效的解决方案

2) 一个线程更新,一个渲染线程

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

3) 同2,但不同步

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

  • 渲染线程只能从共享缓冲区中读取
  • 更新线程可以读取和写入共享缓冲区(因此它是唯一的“写入器”)
  • 游戏运行时共享缓冲区永远不会是空的或满的
  • 线程永远不会休眠
  • 渲染不必是 100% 准确的。如果某些共享对象尚未更新,导致它们比其他对象落后一个更新步骤(即大约 10-20 毫秒),则任何人都不应注意到。

-

那么......我在这里错过了什么明显的事情?为什么我需要此设置的同步?

  • 线程是否可以在未正确同步时缓存导致问题的数据?
  • 或者如果写入线程在不幸的时间被中断,数据会以某种方式出现乱码吗?
  • 或者我提出的策略是否存在任何一般性问题,使其无用?

任何想法、评论或建议都非常受欢迎。或者,如果这个特定问题已经在其他地方得到解决,我将不胜感激。

4

3 回答 3

3

我决定花一点时间对此进行测试,并且由于我从这个站点得到了很多好的答案,我想我会发布这个来完成这个问题。也许其他人会发现这些信息很有用。

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

1) 没有同步

渲染器以最高 60 FPS 运行。更新程序尽可能快地运行 更新和渲染的精灵存在于两个线程共享的列表中。不存在同步,因此线程只是随意读取和写入数据。

2)共享数据的同步

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

3)同步使用双渲染队列

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

我对每种方法进行了 3 次测试,其中我计算了每秒执行更新和渲染的次数。

测试 1:
精灵的数量足够少,因此渲染器可以全速运行 (60 FPS)
每个精灵的更新逻辑太重,无法让更新器跟上。

测试 2:
精灵数量过多,渲染器无法全速运行。
每个精灵的更新逻辑非常简单,所以他们可以跟得上。

测试 3:
精灵的数量正好足够让渲染器运行在略低于最大速度的水平。
每个精灵的更新逻辑足够重,以使更新程序运行在略低于渲染器的最大速度。

结果

不同步 - 测试 1:
渲染器每秒运行 60 次(最大速度)。
更新程序每秒运行 45 次。

不同步 - 测试 2:
渲染器每秒运行 24 次。
更新程序每秒运行 1150 次。

不同步 - 测试 3:
渲染器每秒运行 58 次。
更新程序每秒运行 51 次。

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

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

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

同步双队列 - 测试 1:
渲染器每秒运行 43 次(最大速度)。
更新程序每秒运行 43 次。

同步双队列 - 测试 2:
渲染器每秒运行 24 次。
更新程序每秒运行 24 次。

同步双队列 - 测试 3:
渲染器每秒运行 54 次。
更新程序每秒运行 54 次。

结论

正如您所指出的,Jirka,即使只有一个写入器时不同步的方法看起来无害,它也会产生不必要的副作用,而且它肯定不会保持渲染帧的一致性。

使用双队列渲染比使用一个大型共享精灵列表渲染更快也就不足为奇了。然而,令人惊讶的是,如果考虑到渲染多个帧而不更新,也没有更新多次而不渲染的事实,那么双队列方法的最终结果实际上与非同步方法一样快。

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

于 2012-07-17T22:01:49.800 回答
1

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

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

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

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

于 2012-10-11T03:12:52.710 回答
0

不同步的方法对您来说效果很好;如果这是库存的英特尔硬件1,那就更是如此。我仍然不会使用它。

非同步并发几乎从不可靠工作的原因是处理器可以自由支配何时在主 RAM 和缓存之间进行存储和加载。这可以颠覆几乎所有不同步的协议。但是,正如您所说,如果您的应用程序中的场景从未突然改变,那么没有人可能会注意到;所有数据都将进入 RAM 并迟早对另一个线程可见。

但是,您无法保证何时以及以何种顺序进行,这使您理论上有可能以奇怪的方式混合两个后续帧(场景或其照明的突然变化之前和之后)。

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


1 ) i86 从不重新订购商店。我认为这在任何地方都没有记录,我不想依赖它。您仍然可以重新排序读取,因此无论如何这对您的场景没有帮助。

于 2012-07-09T21:59:11.317 回答