KlayGE的资源载入系统
一年半之前,在开发KlayGE 4.0的时候,郭鹏就已经设计了一个资源载入系统,可以解决异步载入和重复载入的问题。但我只是实现了一个很粗略的异步载入。随着场景复杂度的需求越来越高,一个完善的资源载入系统也就成为必需。顺着之前的设计思路,我最近终于完成了实现。
目标
KlayGE的资源载入系统有几个设计要求:
- 至少支持Texture、Model、Effect、Font和Post Processor这五种资源,并能快速扩展支持新的资源。
- 可以选择同步或异步载入。
- 不重复载入资源,以减少浪费。
- 自动化管理,不需要手动指定资源ID,尽量不需要显式删除。
- 资源不需要单根继承。
- 异步无阻塞载入。
之前的ResLoader能支持2,4和5,部分支持1,并以很烂的方式支持3,完全不支持6。新的ResLoader终于圆满解决了这几个问题。
和传统资源管理器相比
首先做个横向比较,和其他游戏引擎里常见的传统的资源管理器相比。这里T表示资源指针的类型,ResMgr表示资源管理器。
传统资源管理器 | KlayGE的资源载入系统 | |
---|---|---|
资源继承 | 不同类型的资源需要继承自Resource类 | 每种资源完全独立,不需要任何继承 |
资源管理器继承 | 根据不同资源写多个不同的管理器,比如ShaderManager、TextureManager等。 这些管理器也继承自一个ResourceManager类 |
统一的资源载入器,也不需要任何继承 |
异步载入 | int id = ResMgr.AsyncQuery(res_name, …); | function<T()> rl = ResLoader.AsyncQuery(res_name, …); |
获取载入的异步资源 | T p = ResMgr.GetRes(id); | T p = rl();如果资源还没载入完成,会返回空。 |
卸载资源 | ResMgr.Unload(id); | p.reset(); |
从这里可以看出,除了继承,还有一个很主要的区别在于,传统资源管理器的资源获取等操作都需要和全局的资源管理器本身打交道。而在KlayGE的资源载入系统里,这些操作只是局部的,不需要与ResLoader交互。
和老版本相比
横向比完了纵向比。老版本的ResLoader并不负责检测资源重复载入的情况,Texture、Model和PostProcessor如果载入两次,就会在内存中保留两份资源。只有属于同一个Model的Texture可以做到不重复载入。而Effect和Font则在RenderFactory里面做重复检测。新版本里,这些都被集中到了ResLoader,不区分资源类型,都用同样的方式进行管理。
老版本的异步载入是阻塞的,如果试图获取一个还没完成的资源指针,系统会阻塞直到载入完成才返回。另外,老版本会对每个载入的资源都分配一个线程(通过全局的线程池),而新版本只会分配一个线程来完成整个资源载入队列。
方法
实现这几个要求的关键之一在于资源描述。资源需要单根继承的原因在于需要让资源管理器调用这个基类,才能知道如何读写文件,如何把数据放入资源。但其实资源载入过程中真正需要的是一个资源描述,用来描述资源载入的流程。资源描述和资源之间的耦合很弱,而且仅仅存在于载入阶段,至于载入之后,就能抛弃这个资源描述。如果是传统的继承结构,这个耦合就无法被消除。
有了资源描述之后,就需要根据功能详细设计资源描述的接口。资源的载入可以分为两个阶段。第一个是可以分到子线程的、与设备无关的阶段,主要是文件读写和格式解析。第二个是必须在主线程、和设备相关的阶段,主要是调用具体API建立资源。同时,因为同一个资源的资源描述是独立建立的,所以资源描述还需要负责比较两个资源描述是否代表了同一个资源,否则就无法避免重复载入。这一般是通过资源类型、资源名和建立的参数来比较。
如果检测到了要载入的资源曾经被载入过,那就需要重用这个资源。这里有个需要注意的地方是,资源实际上应该分为无状态和有状态两种类型。无状态的资源,比如Texture和Font,每一次载入都会是完全相同的,可以直接复用资源指针,称为CopyData。有状态的资源,比如Effect和PostProcess,在使用的过程中会修改一些状态,所以如果复用资源指针,就无法让两个资源独立工作。所以这里只能复用第一阶段的数据,称为CloneResource,还需要单独执行一次第二阶段来建立资源。需要注意的是,Model也是有状态的,因为Model内包含了一个Effect,但Model的VB/IB这些可以复用,所以在CloneResource的时候需要区分。
对于不同类型的资源,需要从资源描述的基类派生一个独立的资源描述,实现前面描述的几个功能,就大功告成了。资源本身不限于要修改。在资源描述的帮助下,ResLoader就能统一地用同样的方法管理不同类型的资源,并且资源本身不需要涉及继承体系。扩展到新的资源只需要写一个新的资源描述就可以了,耦合度很低。ResLoader本身包含两个队列,Loaded队列保存已经载入完成的资源,Loading队列保存正在载入中的资源。在载入资源后,ResLoader会把资源描述从Loading队列移到Loaded队列,并保留一个资源的weak_ptr.通过定期检查这个弱指针,ResLoader可以知道这个资源是否已经被删除了,如果是的话,就可以把这个资源也从ResLoader的Loaded队列里删除。同时ResLoader也有个和传统相同的Unload函数,用来强制卸载特定资源。
资源载入本身的状态机是这样的。同步载入没啥好说,这里就看异步载入的。
注意这里需要重复检查是否在Loaded队列的原因是,如果一个异步资源在等待子线程执行的时候,同一个资源再次执行了同步载入,那么它会出现在Loaded队列。
总结
通过这个新的资源载入系统,资源浪费得到了根除,资源载入速度也有所提高。比如Deferred Rendering的例子,内存耗用减少了10%,载入速度提高了20%。并且,由于整个系统无阻塞,载入过程中渲染线程仍会继续执行,所以可以看到场景一点一点加载的样子。