您的位置:首页 > 其它

Doom3 引擎渲染管线分析

2012-06-24 10:51 162 查看
转发请注明出处

Doom3的渲染管线分为两个阶段,一个是前端渲染,一个是后端渲染。其中前端

渲染负责绘制简单的2D UI和提供解析场景树并将需要绘制的物体打包且用“渲染命令”

进行序列化,并排序然后送往后端进行渲染。后端渲染是真正进行渲染的地方,它接受

前端传来的“渲染命令”链表,然后逐一解析并调用OPENGL API进行渲染。

Doom3每帧都会调用session的UpdateScreen函数,该函数如下:

void idSessionLocal::UpdateScreen( bool outOfSequence )

{

...

renderSystem->BeginFrame( renderSystem->GetScreenWidth(), renderSystem->GetScreenHeight() );

// draw everything

Draw();

if ( com_speeds.GetBool() ) {

renderSystem->EndFrame( &time_frontend, &time_backend );

} else {

renderSystem->EndFrame( NULL, NULL );

}

insideUpdateScreen = false;

}

其中 renderSystem->BeginFrame 是往渲染的命令队列中加入“选择缓冲区命令”,然后

是调用 idSessionLocal自身的 Draw()函数,该函数是解析整个场景树,并将对应的命令放入

后端渲染命令队列中。然后调用 renderSystem->EndFrame 该函数,是运行所有在后端命令队列

中的命令,进行真正的渲染。下面开始讨论支持这种渲染机制的一些基础“构件”。

高效率的帧间临时内存分配器:

由于在渲染过程中,需要生成大量的临时性结构体与数据缓冲区,若这些临时性结构体都由

系统提供的new,delete,malloc,free 来在“帧”中分配,然后在帧结尾处释放的话会很没效率并且

会造成大量的内存碎片的产生(因为这些临时性结构体都比较小)。为了解决这个问题,Doom3提供

了一种帧分配内存器。该分配器是一个全局frameData_t的变量。该结构体声明如下:

typedef struct {

frameMemoryBlock_t*memory;

frameMemoryBlock_t*alloc;

srfTriangles_t *firstDeferredFreeTriSurf;

srfTriangles_t *lastDeferredFreeTriSurf;

int memoryHighwater;// max used on any frame

emptyCommand_t*cmdHead, *cmdTail;// may be of other command type based on commandId

} frameData_t;

其中与帧临时性内存分配有关的结构体还有:

typedef struct frameMemoryBlock_s {

struct frameMemoryBlock_s *next;

int size;

int used;

int poop;// so that base is 16 byte aligned

byte base[4];// dynamically allocated as [size]

} frameMemoryBlock_t;

frameMemoryBlock_t这个结构真正持有,分配的内存块,其中size是为该节点分配的内存总大小一般为

1MB , used是表明该内存块的使用量。frameData_t的职责是维护了一个内存分配链表与后端渲染消息队列

frameData_t中的memory是内存分配的链表头,alloc指向了正在被分配的内存块节点。该分配器的策略是

分配一个内存块链表供每帧重复性使用,在每帧的结尾处调用R_ToggleSmpFrame该函数并不真正释放该链

表内存,而是将分配的内存块链表中的各各节点的used变量重置为0,然后更新全局frameData_t结构的

memoryHighwater 的变量,该变量标明内存最大的使用“高度”,也就是每帧中该内存块链表分配节点的实际

使用大小的总和。

该分配器的初始化是通过调用 R_InitFrameData()完成的。该函数会在堆中分配出全局frameData_t

变量,然后为该分配器初始化一个frameMemoryBlock内存块,并让alloc指针与memory指针指向它。

在每一帧中渲染函数都会调用R_FrameAlloc来分配一些临时空间用来存放用于渲染的临时结构体。该函数会

先检查alloc所指向的当前正在分配的内存块是否满足分配空间的要求,若满足则直接改写内存节点中的used

变量,然后返回分配的内存指针,若当前块大小不满足要求则先看该块下一个节点是否存在(注意这一点!

可能有人会问若当前alloc是当前正在分配的内存块,那哪来的下一个节点?block->next的指针难道不应该

为空吗?! 但别忘了为了实现快速分配,该内存块仅仅在帧结尾处调用R_ToggleSmpFrame进行“释放”但该

释放并不是真正意义上的在堆中释放,而是便利之前分配过的内存节点列表然后把所有内存块的used重置为0

这样也就实现了一次堆分配多次使用的功能,不用每帧都真正的在堆中分配或释放。)若存在则直接在下一个节点中

分配,若还不满足要求,那说明当前分配的内存大于整个块的最大值直接报错。若当前alloc没有后续节点

则分配之。然后更新全局frameData_t结构的alloc指向当前分配块,更新当前分配块的used成员变量。

该分配器的真实释放是通过调用R_ShutdownFrameData(),该函数是真正的释放函数。调用该函数会逐一

释放掉分配的内存块节点,然后释放掉全局的frameData_t结构。该R_ShutdownFrameData()函数会在系统结束

时才调用。这样这些分配出去的内存块可以在游戏中一直存在以备重复使用。

高效率的状态转换

OpenGL是个状态机,在渲染的过程中需要频繁切换各种渲染状态,这就造成了在函数调用时的效率低下。

关键一点不在于状态的变换,而是在于有可能当前状态就是想要变换到的状态而重复的调用函数。Doom3引擎

维护了一个名为glstate_t的结构体。该结构体里有个记录当前状态的数据成员 int glStateBits 该整型值

每一位都代表了一种状态。这样当某位置为时代表当前状态为开。例如 GLS_DEPTHFUNC_ALWAYS 为 0x00020000

也就是第18位置位代表 glDepthFunc(GL_ALWAYS)设置了。OpenGL的状态统一由GL_State(int stateBits),该

函数首先利用传入的stateBits与当前的状态glStateBits进行异或操作(XOR)得到一个diff 32位的值,这样

只要diff中某位为 1 则说明该位有变更,直接根据该位的指引变更相应的状态即可,这样就实现了真正变更

需要变更的状态,节省了效率。源码(Tr_backend.cpp 239行)。

渲染命令队列

Doom3维护着一个渲染命令队列,该队列每帧都会被重新分配。命令队列是单链表,他被安插在了frameData_t

结构体中:

typedef struct {

...

...

emptyCommand_t*cmdHead, *cmdTail;// may be of other command type based on commandId

} frameData_t;

由于该队列被每帧不断的重新分配,所以分配效率是个关键。同样该队列也不是用new ,malloc , delete , free分配

的,该队列的节点是通过上面讲到的帧分配器分配的。要往后端渲染器发布命令需要调用R_GetCommandBuffer(int bytes)函数

获得分配好的 渲染命令对象,该函数会先在帧分配器中分配一个渲染命令对象节点,然后自动将其挂接在命令队列队尾。

由于渲染命令对象是在帧分配器上分配的,故该对象同样无需释放。因为每帧帧分配器会自动“清理”分配出的所有内存。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: