65

游戏引擎中的资源生命期管理问题

 4 years ago
source link: https://www.tuicool.com/articles/mQZFry3
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

最近我们开发中的游戏引擎在修理资源管理模块中的 bug 时,我提出了一些想法,希望可以简化资源对象的生命期管理。

其实这个模块已经被重构过几次了。我想理一下它的发展轨迹。

最开始,我们不想太考虑资源的生命期问题,全部都不释放。当然,谁都明白,这种策略只适合做 demo ,不可能用在产品中。

因为我们整个引擎的框架是用 lua 搭建,那么,最直接的想法就是利用 lua 自带的 gc 来回收那些不被引用的资源对象。我不太喜欢这个简单粗暴的方法。因为首先, gc 不会太及时,其次 gc 方法触发的时机很难控制,容易干扰正常的运行流程。图形显示模块是时间敏感的,如果因为资源释放占用了 cpu 的话,很容易变成肉眼可查的卡顿。

另一个促使我们认真考虑资源管理模块的设计的原因是,当我们从 demo 过渡到现实世界的大游戏场景时,过多的资源量触发了 bgfx 的一个内部限制:如果你在一个渲染帧内调用了过多资源 api (例如创建新的 buffer texture 等),会超出 bgfx 的多线程渲染内部的一个消息管道上限,直接让程序崩溃。

所以我们不得不比计划提前实现资源的异步加载模块,它属于资源管理模块的一部分,所以也就顺理成章的考虑整个资源管理模块的设计。

我们一开始实现了一个中规中矩的引用计数方案。资源永远都被 ECS 中的 C 引用,且永远没有对外引用,所以并没有循环引用的问题。引用计数一定可以正确的管理资源的生命周期。一旦引用计数为 0 ,把课回收资源放到一个集合里,交给一个 system 处理即可。

但我直觉上不喜欢在一个基于 lua 这样自带 gc 的语言构建的框架中使用一个蹩脚的引用计数机制。而且,移动设备是一个内存受限的环境,我认为基于业务上不再对资源引用与否来觉得是否可以释放资源不是最好的管理方式。

我认为可以把资源分为两类:一,从 IO 获取的资源,它们有唯一的名字(文件名)。这类资源即使从内存释放,也可以重新加载回内存。二,根据其它数据由代码生成的资源,如果销毁不太容易重建。

第一类资源是大头,我认为它们实际上可以随时从内存销毁,释放内存供其它使用。部分资源类型还有替代方案:例如贴图,我们可以用统一空白贴图临时顶替使用。

针对第一类资源,生命期管理就不必基于它是否在内存中还有引用决定,而应该由是否很久没有使用决定。一个长期未使用的资源对象,无论在 ECS 中是否还有 C 对其引用,资源管理模块都有权销毁它,直到下次使用它时再通过异步加载模块读回。

而第二类资源就麻烦一些。如果我们随意删除,就很难重建(因为失去了当初创建它的上下文)。我们决定在内存富裕的情况下,永远保留这类资源。在迫不得已的时候,再在条件允许时删除它。最早的实现又用回了引用计数方案,但很快又去掉了。我们的 ECS 框架很容易遍历所有的资源,所以在必要的时候确定一个资源对象是否还有 C 在引用并不复杂。所以不必额外做繁杂的加减引用操作。

后来,在经过一些讨论后。我又从 imgui 的设计中得到了新的灵感:

其实,当初我们也考虑过给每种第二类资源提供一个回调函数,在销毁后调用一下就能重建回来。但并不是总能简单的写出这个创建函数。例如,如果一张贴图是用场景上的一个摄像机渲染出来的,那么这个回调函数就涉及相关场景对象了。一个简单的闭包函数很可能破坏掉 ECS 的设计原则。

但是,如果我们反过来想,如果每帧都主动创建这种资源呢?好比 imgui 并不保存控件的状态,每帧都去画一下那个控件一样。这样就不会影响 ECS 的设计原则,可以用自然的方式去创建动态资源。之后,我们就可以再这个基础上 cache 上一帧的结果,而避免每帧都创建。

这样设计后,第二类资源对象就和第一类对象一样,可以在任意时刻销毁。资源管理模块只需要按 LRU 算法淘汰超出内存阙值的资源就够了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK