解耦渲染管道(用于UI响应):multithreading和多个上下文?

警告! 文本墙 (见短版<TL; DR>段落)

我已经注意到很多游戏中的东西(最近在尖端的RTS游戏中,比如Uber Entertainment的Planetary Annihilation,这是令人惊讶的),我认为还有改进的空间。

也就是说,渲染系统是同步的,input事件循环明显地被“绑定”到渲染循环。 所以结果是,例如,鼠标移动和键盘事件通常是排队等待处理,只有一个帧完成渲染。

这可能不是一个完全准确的账户。 当然清楚的是,例如鼠标光标的移动更新不会比游戏渲染中的graphics更快,并且显然当键盘事件被触发时,其结果在下一帧渲染之前在游戏中不可见。

如果帧率非常低,那么这个帧的延迟就会非常令人感觉到,我想强调的是,对于input来说,相同的低频率响应比如果我们只谈论它对animation的影响是一个更大的问题平滑度。

当然行星毁灭是一个仍处于testing阶段的游戏,而且(正如我们所知道的那样)还是没有得到优化。 但是由于这个游戏没有单位上限,所以基本上是一个生命的事实,一旦战斗变得足够大,一旦有足够的单位存在,这个庞大的计算负荷将使任何一台计算机最终达到10 ,5甚至2帧/秒。

有趣的是,游戏仍然是可玩的,因为它不是一个FPS,但是2 fps渲染速度的第一个问题并不是运动的多变(如果没有更多的dynamic渲染LOD或剔除这个问题的范围之外),而是由于鼠标和键盘input处理行为的同步特性(即,鼠标以相同的有限帧速率呈现)引起的明显缺乏响应性。 事实上,我期望发生的事情是,框架需要例如500毫秒的渲染,并在这段时间,鼠标事件队列堆积了大约500鼠标移动事件(由于我的1000Hz鼠标),也许几个键盘事件,然后这个队列只有得到一个刷新的机会,并在该帧完成绘制时处理,当swapbuffers / glFinish / glFlush /等。 函数返回。

这是最好的情况。 我想很多游戏可能只是完全忽略整个队列而放弃一堆事件,导致在低回路频率下更难以玩。 这是因为,如果以60fps的速度进行轻度testing,那么大多数(如果不是所有的)事件仍然可以通过这样的错误系统进行处理。

其中一个主要问题是,这可能使某些快速行动无法执行。 假设我想拖动来select一组单位以给他们一个命令,但是我在500ms内完成了鼠标的点击,拖动,释放动作。 如果事件循环处理程序代码没有以适当的方式更新它的状态,那么我的整个select命令就会失败,最好的情况是select将被正确地扣除,但是仍然不会有帧绘制出来的图片显示了实际的select框本身(因为它在呈现单个框架的过程中进入和退出)。

事实上,PA似乎只是在整个循环中更新“我现在select”状态一次,所以我必须通过至less3个单独的帧单击,拖动和释放,才能实现实际的select命令。 考虑一下:它有效地限制了玩家20 APM(每分钟动作)。

有很多基于手势的控件可以存在,特别是在平板电脑的世界。 在我看来,绑定到渲染循环将会导致严重的响应障碍。

我觉得我现在已经过度地解释了这个介绍。 所以解决scheme显然是将UI与渲染循环分开。

我想渲染光标位置和按键input,并使用一些合理的UI元素, 我的主3D场景分开 。 问题是,完成这个的最好方法是什么?

我可以想出两种方法来处理,可能还有更多。

  1. 没有multithreading。 主GL循环刷新input队列并绘制UI,不一定更新3D场景。 devise3D渲染管道,一次将场景的片段绘制成交替渲染缓冲区或纹理。 这听起来有点不切实际,因为不清楚应该如何分解渲染。 瓷砖? 扫描线? 他们会有多大? 由于超大的工作量通常以2fps的速度渲染,所以我想将场景分割成500ms / 16.667ms = 30个块,但是绝对不能保证每个块都要分配16.67ms。 这听起来像调整块数将导致在GPU上的资源洗牌,基本上导致一堆额外的开销。

  2. <TL; DR#1>两个GL上下文,两个线程。 线程#1刷新input队列并绘制UI,定期更新绘制3D场景的纹理,以全屏方式绘制3D场景,并处理缓冲区交换以确保vsync的平滑。 线程#2将3D场景渲染为与线程#1共享的纹理。 需要使用乒乓scheme来促进资源共享。 线程#2翻转一下线程#1将在其下一个周期读取以确定是否需要翻转纹理。

选项2听起来就像这里走的路…因为选项1仍然需要从先前渲染的帧中读取以便在响应地更新UI时显示某些东西,所以仍然需要使用具有该纹理的乒乓scheme。 所以我认为选项2几乎赢得了所有战线,速度和记忆,选项1似乎充满了复杂性。

但是我也看到了一些非常重要的选项2,因为不清楚在3D渲染跟上屏幕刷新率时如何使它正常运行 – 线程#2必须等待线程#1在可以开始渲染到新的纹理之前完成翻转纹理。 看起来像我不得不dynamic切换回一个更简单的“基本”管道,如果渲染不需要太长时间。

正在形成的架构在我看来就像是一个前后缓冲区的附加抽象。 真正的前后缓冲区现在与繁重的3D渲染任务分离,所以只要UI渲染可以及时完成,刷新率就会不断更新,而新缓冲区对的翻转率就是“真实”的帧率。

<TL; DR#2>我的问题是……这可以用更清洁的方式来完成吗? 有没有一个引擎devise,可以实现这个可能不需要两个上下文? 我知道,例如在iOS上,您需要为每个线程设置一个OpenGL ES上下文,并且据我所知,处理这个问题的唯一的方法是使用线程。

我意识到,“解决”问题的一种可能的方式是仅仅通过使用任何外部接口系统存在而作弊。 例如,在窗口模式下的SDL应用程序(或者甚至全屏模式下)中的Windows上,默认情况下使用默认的OS游标,无论GL窗口中缓冲区交换的速度如何,默认情况下它都应该保持响应。

或者在iOS上,您可以在EAGLView顶部放置常规UI小部件叠加视图,因为它们不是GL的一部分,所以我希望能够绕过GL渲染来响应。

但是我正在谈论一个集成了UI的统一的graphics管道,在这个UI中绘制了与用于绘制3D场景相同的上下文。 这是为了便携性的原因。 尽管我用OpenGL标记了这个问题,但显然这个主题适用于学术用途的DirectX。

我同意,选项2(两个渲染线程以不同的频率运行)将是处理这种情况的最“合乎逻辑”的方法,但是它做出了一个很大的假设:GPU和驱动程序必须支持 – 或者说是模拟 -先发制人的多任务处理。 你想要的是,从线程1的UI绘图命令应该得到高优先级,并立即完成,即使线程2在绘图的中间。 换句话说,在服务线程1之前,您不希望GPU /驱动程序必须等待线程2的渲染完成。

然而,你是司机的摆布,这是否有效,以及如何。 目前,GPU是顶层的单线程设备:它们一次只能处理一个命令缓冲区,而不支持抢占; 您不能在命令缓冲区中间暂停GPU并切换到另一个。 所以司机几乎不得不在内部做你的select1。 当你发出GL调用时,它会caching你的命令,然后在某个时刻将它们分派给GPU,并且它只能在这些缓冲区的边界之间在上下文之间切换。

这里的要点是,根据驱动程序决定如何分割命令缓冲区,来自线程1的UI命令可能会滞留在线程2的长命令缓冲区之后。

我的猜测是,你会发现你的选项2在GPU和驱动程序的某些组合上可以正常工作,而不是在其他的组合上。 您可以通过将glFlush()调用转换为您的线程2渲染来帮助解决问题,这会提示驱动程序继续并完成命令缓冲区,从而为切换上下文提供机会。

这就好比在非抢先式的CPU多任务处理中,你不得不通过调用Sleep(0)或者类似的方式来明确地让步到OS,否则一个长时间运行的操作会locking整个系统。 这就是我们今天用GPU的地方。 希望未来的GPU(和graphicsAPI)能够更好地支持多任务处理,但这是一个非常难以解决的工程问题。

在我看来,如果帧率经常下降到2fps,那么在游戏本身可能会有devise问题,这是正常的。 一些基于回合的游戏可能需要一段时间才能渲染和呈现下一个屏幕,基本上是“静态的”,再加上一些简单的交互式渲染,但这就是游戏风格本身。 通常情况下,如果您的游戏是实时交互式的,那么它具有最小可玩率。 可玩性不仅仅是animation的速度,还有UI的反馈速度。 当然,如果在加载的情况下,你总是可以减less渲染细节,但是如果从潜在的2fps开始,听起来好像要下降很多。

对不起,如果我误解了这个问题, 这是相当长的:)