[译] Designing a Parallel Game Engine (1)
原文:https://software.intel.com/en-us/articles/designing-the-framework-of-a-parallel-game-engine/
此文章是为介绍多线程并行游戏引擎的经典文章,原文较长,这里分成2部分。
1.引言
随着多核处理器的发展和普及,游戏引擎也需要从古老的单线程模型进化到并行模型。如何挖掘多核的处理能力,成为开发者必须要考虑的问题。一旦硬件多核能力被很好的运用之后,引擎的性能也会成倍增长,游戏就能够支持更多的object,rigidbody,effect对象,或者更为智能的ai系统。
1.1.概述
简单来说,并行游戏引擎框架就是一个多线程引擎框架,能够充分理由多核来做任务分配,同一时间能够处理更多的任务。说起来简单,事实上由于各种限制以及游戏对象的交互和依赖,这件事变得十分复杂。数据同步就是一个非常头疼的问题,我们考虑了各种复杂的场景,如何在不使用同步锁的情况下做正确的数据同步。同步提出一种方法来优化并行数据同步,使其耗时达到最小。
1.2.假设
开始阅读之前,你需要具备基本的游戏引擎开发经验,多线程编程经验。
2.并行执行状态
并行执行状态对于引擎能否做高效的多线程并发极为重要。(什么是并行执行状态parallel execution state?我的理解这是一个全局概念,表示了当前所有并行任务的内部状态,是否执行完毕?是否只执行了一半?是否有数据要同步到其他线程?最终在一个时间点把所有状态汇集,render frame到显示器,这个状态就展示在了用户面前)为了高的并行度以及小的同步开销,每个并行的系统在自身执行周期内和其他系统都需要保证低耦合。举个例子,某些数据是必须共享的,像object的transform数据,为了解除依赖,每个系统都维护了一份transform的拷贝。对共享数据的任何修改都会被发送至state manager,以消息的形式push到queue里保存。一旦不同系统执行完毕,就被收到数据变化的通知,更新共享数据的拷贝。如此一来,同步开销就会降低,系统的独立程度就会更高,当然代价是额外的内存空间以及消息通知。
2.1.执行模式
当操作遵从同一个时钟同步时,将有利于做简单的执行状态管理。时钟周期并不需要和一帧的时长保持一致,甚至都可以不保持一个固定的时钟周期,例如你可以把它和frame count绑定,这样一来一个时钟周期就等于一帧的时间。有2种同步执行模式,free step mode(在同一个始终周期内允许部分任务不执行完成),lock step mode(一个时钟周期内全部任务都执行完成)。
2.1.1.Free Step Mode
这种执行模式允许任务在任意的时间点执行完毕。该模式下,原本简单的state通知将会变得复杂,在state change notification时数据也同时要传递过去,这是由于当需要数据的系统准备好更新时,修改数据的系统很可能尚未结束。
2.1.2.Lock Step Mode
Lock Step模式下所有的任务都在一个时钟周期内结束。相对于Free Step Mode这很容易实现,消息通知也不需要捎带上data,因为在所有task结束后的data sychronization阶段,任意的system都可以直接查询其他system的数据。
Lock Step模式也可以用来实现一个伪Free Step模式,例如把运算拆分交叉排布在多个step里(相当于原本一个step的工作被拆分到了多个step里)。
2.2.数据同步
不同的系统修改共享数据是相当常见的,前面说到,数据更新我们使用messaging的形式来通知state manager。那么一个system应该访问哪一个数据才是最新最正确的呢?有2种方案:
1)Time:最后一次修改data的system具有最新有效值。
2)Priority:高优先级的system具有有效值。相同优先级的取Time最新的
在这2种机制的保证下,失效的临时数据被覆盖或者直接丢弃。
(保留)
3.引擎
引擎应当是灵活可伸缩的,对于功能模块可以很方便的扩展和替换。
具体分为Framework和Manager两大部分,Framework包含所有的游戏实体,以及Game Loop逻辑。Manager都是单例,维护了全局的数据或状态。
注意Engine的下面还连接了若干system,system是不同的游戏功能模块,它被抽象在了Engine之外。这有利于模块化,Engine更像是胶水代码把System组装起来(高度模块化使得我们可以从Engine动态地,随意地load需要的模块,unload不需要的模块,保持runtime的最小化)。
Interfaces层定义了engine和system的通信接口,engine实现system需要的接口,system实现engine需要的接口。
下面给出了实际的引擎图示:
如同第2章所述,system之间是高度独立,可并行执行的。但当system之间有交互时,数据便无法保证处于稳定状态。system的交互有下面2种情况:
1)通知其他system自己修改了共享数据(position或者orientation等)
2)需要请求调用其他system的功能函数
上一章介绍的State manager解决了问题1,在3.2.2章节会详细讲解。
为了解决问题2,system提供了一种service的机制来为其他system提供服务。在3.2.3章节会详细讲解。
3.1.Framework
Framework负责将引擎的各个部分整合起来。除了manager是静态初始化,引擎的初始化部分全部在Framework中。场景信息也保存在framework中。为了保持灵活性,场景由一个包含universal objects的universal scene来实现,universal scene仅仅是把场景不同功能部分绑在一起的容器。3.1.2章节会详细讲解。
Game Loop流程如下图(每一步的执行内容如字面意思,不再单独赘述):
3.1.1.Scheduler
在每一个clock tick开始时,调度器通过task manager来提交执行system逻辑。对于Free Step Mode而言,调度器和system沟通决定其需要多少个clock tick才能执行完成,这个clock tick的数量还可能动态调整。Lock Step Mode比较简单,所有system都在同一个clock tick中执行,调度器等待所有system的执行结束。
3.1.2.Universal Scene & Objects
Universal Scene和Objects是包含system功能的容器,除此之外完全不提供其他功能。它们可以被任意扩充包含任意system的功能(extensions插件),而不用知道其具体实现。这种松耦合使system之间相互独立,能够更好的并行处理。
举一个简单的例子,一个universal scene被扩充成包含graphics,physics和其他属性。graphics scene负责图像处理和显示,physics scene负责构建物理世界,进行物理模拟。
universal scene额universal objects负责注册其包含的system插件到state manager,这样physics插件对坐标的修改就能通知到graphics插件。在5.2章节System components会详细阐述。
3.2.Managers
Managers作为单例提供引擎的全局功能。Manager有5个,分别是Task,State,Service,Environment,Platform
3.2.1.Task Manager
Task manager维护了一个线程池,调度执行system的任务。线程池的线程数通常和处理器核心个数保持一致。
每一个system有一个主任务(称之为functional decomposition),每一个主任务又可以针对数据分为多个子任务(称之为data decomposition)。想象一下对于一个游戏场景中的大量怪物,每一个怪物在AI系统中可以认为是单独的一份data,不同的怪物可以在不同的线程并行跑AI,这就叫data decomposition。
3.2.2.State Manager
前面说过,state manager就是不同system之间的通信中转器,一个典型的消息系统。为了减轻通信开销,system仅仅只注册感兴趣的事件。可以通过观察者模式来实现。
Framework实现了2种state manager,一种用于处理scene level的data change事件,一种是处理object level的data change事件。区分这两者有利于降低处理不必要消息的开销。
为了消除同步锁的开销,state manager为每一个线程创建了一个消息队列,这样,消息通知是不需要锁的,多个消息队列可以在一个执行周期结束后通过2.2章节的方法合并。
某些时刻,数据变化的消息通知也未尝不可以并行处理,通常来说,在数据变化通知阶段universal objects之间是没有依赖的,有依赖的只是universal object内部的不同system插件。这就意味着universal objects可以并行处理data change通知事件。
3.2.3.Service Manager
Service manager定义了部分system的公共接口来方便其他system调用,system之间是隔离的,所有的公共接口只能使用ServiceManager提供的。
Service manager还有另外一个作用。一些system的属性并不会走statemanager,比如屏幕分辨率,或者是重力常数,这些属性通过service manager直接访问。事实上,不同system之间相互访问各自的属性这种情况是不常见的,这些属性通常都是供外部修改的(例如通过debug模式的console控制台),并非频繁改变的。
3.2.4.Environment Manager
Environment manager提供引擎运行时环境的管理。比如:环境变量,用户设置,task执行信息等等。通过Environment能够查询引擎的执行状态。
3.2.5.Platform Manager
Platform manager抽象了引擎需要的所有的系统调用,并提供若干系统相关的复杂功能,隐藏了平台相关性。另外,platform manager还需要提供处理器相关的信息,比如其对SIMD指令的支持程度,有哪些特殊的优化行为等,这样引擎可以根据不同的平台做相关的优化。