文章仅记录部分内容作为笔记,对视频感兴趣的可见下面链接的原视频:
https://gameinstitute.qq.com/course/detail/10119
课程目录:
【第一章】内存
- 游戏机内存演化历史
- 游戏引擎内常用的内存分配器介绍
- 面向数据的设计方法
- Entity Component System
【第二章】多线程
- 主/渲染线程结构
- 任务系统
正文:
【第二章】多线程:
一、主/渲染线程结构
关于游戏引擎的发展史暂时不记录。
视频中提到的顽皮狗公司关于神秘海域中多线程设计的结构,关于渲染计算中,比如LIGHTING部分,将其放到了SPU上;
其实我之前也在阅读一本书,是《游戏引擎架构》,因为个人原因,那本书没来得及记录笔记,但是已经阅读了一部分。
https://book.douban.com/review/6707556/
上面链接中的评论说的很好。作者也是顽皮狗的自身开发者,参与了《神秘海域》系列和《最后生还者》此类高品质游戏,如果对这方面感兴趣的,强推这本书,不过书本身较厚,需要一定时间来进行阅读。
视频中是以《天刀》为例子。
渲染线程分离(背景)
- GR1 Demo里 90%的CPU时间花在渲染模块。
使用的渲染pipeline是一个传统的渲染模型
- 做功能扩展和修改是困难重重
希望使用更加先进的基于物理光照模型,会面临一些列问题(复杂度会随着代码量的增加而愈发困难)
- 需求结构更好,效率更高的渲染模块
常见的游戏多线程架构设计
一般是分为一个主线程,这个主线程是用来处理输入,物理,后处理(例如依赖于动画和物理全部处理完之后的操作);还有其他的一些worker线程,用来做IO或者动画,然后渲染线程。
但是这种架构属于一种理想架构,会出现线程等待,类似操作系统中的造成阻塞?这种WAIT和IDLE都会造成整体性能的阻塞。
渲染线程分离(实现)
- 关键点
状态区分(逻辑状态和渲染状态),如何有效的把这两个数据进行区分?
状态切换(渲染状态送入渲染线程),如何将相关内容,更有效的送入到对应线程来执行
- 核心结构(以天刀举例)
GraphicAsyncData(状态区分)
SyncBarrier/SyncZone(状态切换)
SyncBarrier--线程同步点,只有所有线程都到达才能够继续执行。
SyncZone--在该时间范围内,只有一个线程能够执行。
- Hybrid
即包含了StateBuffer(GraphicAsyncData)
- 技术选择(天刀举例)
UE系 基于RenderCommand的RingBuffer,最简单的方式就是:找到对象,设置状态,然后交给GPU处理,以此类推。当你想把这个东西用多线程来实现,一个简单的想法就是把状态拷贝,把他放在一个数组里,这个数组就是RingBuffer。当你一直向RingBuffer里输入状态,另外一个线程会一直处理这个数据,这个就是渲染线程。
这个设计就会造成一个Producer Consumer(生产者消费者问题,这个应该都懂)的问题,有时候主线程会快,渲染线程会慢,或者反过来。一旦出现二者速度不同,必然有一个线程会等待。
Halo系 基于GameState的DoubleBuffer,最简单的方式就是可以把所有的状态都放在一起,例如一个结构体,里面既有逻辑状态也有渲染状态。
最早Halo体是把渲染状态,逻辑状态都存起来,但是存两份,然后在每一帧结束的时候,把这两份的指针交换一下,然后下一帧重新去写。
好处就是在渲染线程,它拿到了游戏逻辑上一帧的完整状态。缺点是相当于把你所有游戏运行时的内存同步的复制了一份,就是说有很大的内存开销。
当时要这么做的原因是很多游戏要做实时存储。
二、JobSystem(任务系统)
任务系统(积累)
Frostbite2
- Parallel Future of a game Engine
Naughty Dog
- Lighting and shading in Uncharted2
举例:
碰撞检测:
碰撞检测的特点是,它的场景,特别是静态场景都是非常固定的,数据结构检查算法也是统一的,也就是说,假设一帧想做N个碰撞检测。完全可以放到N个盒上去执行。
寻路:
和上面相同,场景固定,算法固定,各算法之间不产生依赖关系,很容易做多线程。
光栅化和遮挡检测:
避免送过多不必要的东西给GPU,如果能被完全遮挡,那证明这个东西暂时就不需要送个GPU。
光栅化也是完全可以作为多线程来处理的。
粒子系统的更新
动画蒙皮的更新
任务系统(举例):
- Windows--Parallel Programming Library/ConcurrencyRuntime
- Apple--Grand Central Dispatch(libdispatch)
- Intel TBB
- HPX
任务系统的简单实现形式:
经典的单TaskQueue--当产生一个任务时,这个任务会送入DQ,如图,有很多个THREAD跑在CP的核上,它们就会竞争从QUeue里获取任务。
局限性:
对Queue的访问是一个有锁的操作,每个线程不得不等待才能获得object的访问(操作系统中学习过的内容),每个Task之间可能还存在依赖关系。
后续内容不做记录。