资源管理系统的初步想法及其它问题

    最近一直在思考动态资源加载的解决方案,上周末看了一下 d3dsdk november 2007 中的 content Streaming 的例子,对于动态多线程后台资源加载有了一些初步的认识和想法,结合最近已经开发了 80% 的虚拟文件系统,引擎的资源管理系统大体上有了一个设计。
    大概的内容是这样的,对于 3D 游戏来讲,按照存储位置可以将资源分为三种类型:
  • 外存资源,指的是存储在硬盘、光盘或者 usb 设备中的物理资源文件。
  • 内存资源,指的是存储在内存中的资源数据,这个数据可以是从外存中加载,也可是游戏运行中创建。
  • 显存资源,指的是存储在GPU控制的显示内存中的资源,这个资源只能通过内存来加载或者写入。

    三种类型的资源,采取不同的加载策略,同时设计为三个不同的层次,其中,外存资源的管理是最底层的,其加载和存储的管理由虚拟文件系统完成,内存资源和显存资源的管理则在引擎的资源管理系统层次上进行。先从外存资源也就是虚拟文件系统说起。

    虚拟文件系统的核心思想是存在一个抽象文件系统的管理接口,有两个接口的实现,一个是针对 windows 文件的,一个针对打包文件的。因为引擎的资源文件在生成时往往是分散的小文件(比如从 max 导出的模型或者动画资源),这些小文件又会引用其他的资源文件,比如模型引用某张纹理,骨骼引用了某个动画等等。如果没有抽象文件接口,而直接采用资源包的形式读取资源,则势必要求资源在生成时也要打入某个资源包中,就需要在资源生成时进行资源的包处理,这样对于美术来讲使用并不方便,并且国内的美术也不习惯这种方式,不利于快速开发。如果能做到文件系统在读取某个资源时不需要关心这个资源的存储方式(也就是说不管它是一个零散的文件还是在某个资源包中)的话,我们就可以在游戏开发期使用零散的资源,而在发布游戏版本时打成资源包,说白了讲就是把多个资源文件打包成一个资源包文件,然后所有的资源读取和存储都在这个资源包中进行操作。有了抽象文件系统的接口,用它读取资源时,就不用关心资源的存储方式了。
    对于 windows 的实现来说,就是普通的文件读写操作,用 api 或者 io stream 实现就可以。对于打包文件系统的实现有些麻烦,因为要做到接口抽象,所以在接口层次上不能暴露有关包的任何信息。在实现的过程中有两个需要注意的地方,一个是资源的快速查找,一个是资源数据的快速读取。查找资源的性能我还是比较在意的,因为性能是资源管理好坏的重要衡量指标之一,所以开始我没打算使用 stl::map,由于 stlext::hash_map 我早已用过,也在考虑之列,不过因为是使用资源的全路径来索引资源,所以总是避免不了要进行字符串的比较的,而且为了保存 string 的 key 占用的内存也不少,时空比上没有达到我比较满意的程度。以前曾经看过 blizzard 的 hash 算法,通过 3 次 hash 操作可以得到一个几乎不会重复的索引(当然还是有一定概率的,只是几百万亿分之一的可能),进行查找时几乎是 O(1) 的时间,更主要的是它无需保存 string 的 key,大大地减少了内存占用,我做了 hash_map 和 blizzard 的 hash 比较测试,30k 的不重复数据,随即查找 10k 数据,结果是在查找上 blizzard 稍有优势,但内存占用上却是 hash_map 的 1/4,这个时空比还是不错的,遂采用。
    对于资源包中资源的快速读取,原以为不会有问题,结果这两天在测试读取性能的时候,结果却有不同:在频繁读取超过某个大小的数据时(比如1M),读取就会比普通的读取零散文件要慢,而且慢的比率比较大,有时甚至能达到 2 倍,这个过程排除了包管理内部的开销造成的影响,甚至我单独写了个测试程序,从 1G 和 256M 的文件中分别读取 256M 大小的数据,1G 的读取就是慢,这让我有些不解,从理论上来说从一个大文件频繁随即读取数据,因为只有一次 fopen 的开销,应该比频繁的 fopen 再 fread 会更快,但实际的结果却不是这样,难道是大文件随机读取数据时硬盘磁头读取扇区数据的方式和读取零散文件不同?这个无从得知,如果知道的朋友请赐教。后来我改用 File Mapping 来加速资源读取操作,效果还是不错,不过由于 File Mapping 本身的一些特性,在随机读取数据时,还需要额外的一些处理,不过这些处理的开销都是可以忽略不计的,整体的性能依然可以达到相当不错的程度。但需要对 98 操作系统进行测试,目前这个工作正在进行。
    内存资源和显存资源的管理,目前引擎中也有基本的资源管理,这点在前一篇 blog 中也提到了,如果对内存和显存资源进行动态的管理,旧有的方式就完全不适合了。对于纯粹的内存资源来说,流程比较简单,当请求一个资源时,送入加载资源队列,在 IO 加载线程中由 virtual file system 进行资源读取,当读取完成后送入资源加载结束队列,游戏线程(可能和渲染线程不是同一个)使用这个内存资源。当物理内存或者指定的内存耗尽时,可以通过回收一些资源来释放内存空间,释放规则可以是最近最少使用等原则。对于显存资源,由于不能直接从外存资源加载,所以流程比较复杂,不过主要思想还是 content streaming 的设计思路,首先一定是渲染线程发起资源的请求,如果在资源管理器中没有找到资源,则视为未加载资源,并将请求加入到资源加载队列,在 IO 加载线程中由 virtual file system 进行资源读取,当读取完成后送入资源加载结束队列,渲染线程创建设备资源(比如 texture、vertex buffer、index buffer、shader 等),创建成功则进行映射数据操作,并送入资源处理队列,在资源处理线程中对设备资源的映射数据指针进行处理和拷贝,完成后送入资源处理完成队列,在渲染线程中就可以正式使用这个设备资源了。当显存或者指定的显存耗尽时,如果还有渲染资源需要上传,就必须要释放设备资源,释放规则可以是最近最少使用等原则。但释放时可以保留内存数据,以便下次请求时直接进行设备资源的处理和上传。关于这部分还有很多细节内容,这需要在实际开发中逐步解决,这里只是提出初步的设想和方案,下次完成之后再详细探讨。
此条目发表在DreamEngine 3D 图形引擎开发分类目录。将固定链接加入收藏夹。

留下评论