Doom3 引擎渲染管线分析

来源:互联网 发布:nginx server配置 编辑:程序博客网 时间:2024/05/19 00:09

       转发请注明出处

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)函数
获得分配好的 渲染命令对象,该函数会先在帧分配器中分配一个渲染命令对象节点,然后自动将其挂接在命令队列队尾。
由于渲染命令对象是在帧分配器上分配的,故该对象同样无需释放。因为每帧帧分配器会自动“清理”分配出的所有内存。
原创粉丝点击