« 程序员应该怎样提高自己 | 返回首页 | 游戏引擎中的资源生命期管理问题 »

资源文件的转换问题

我们上周在游戏引擎上面的工作中遇到一些 bug ,涉及到过去的一些设计问题。维持讨论了几天解决该问题的方案。今天终于把最终方案确定了下来,值得做一个记录。

bug 出在游戏资源文件的转换上面。

游戏里用到的资源通常需要一个导入资源库的过程,例如你的原始贴图是一个 png 文件,但是引擎需要的是对应运行平台的压缩格式,windows 上是 dxt ,手机上是 ktx 等等。这个过程,在 Unity 等商业引擎中,是放在资源导入流程中。

我们的引擎把这个转换过程放在虚拟文件系统这个层次。这个设计决策是因为,我感觉统一导入资源是个痛点,用的人通常需要等待导入过程。Unity 用了 cache server 来解决这个痛点,但我认为 cache server 也存在一些设计问题 ,这个会在后面再展开一次。

我更希望转换过程是惰性的,直到最终运行需要的资源才需要转换。

在我们的设计中,所有需要转换的资源,都有一个后缀为 .lk 的同名文件放在文件系统中。它描述了怎样加工原始素材,它的作用和 Unity 的 .meta 文件基本一致。

我们的虚拟文件系统在发现一个文件有 .lk 时,会在请求该文件的时候调用构建模块转换源文件。

我们一开始假设的前提是,一个源文件加对应的 lk 文件,在加平台参数,三者的内容就决定了最终生成的文件是什么。所以前三者的 hash 就能用于转换过程的 cache 。最近发现,这个前提是不成立的,导致了 bug 的产生。

原因是:对于 shader 文件,它其实是一种代码,类似 c/c++ 代码。编译一个 shader 其实是依赖很多文件的。所以光有一个 shader 源文件无法准确的 cache 结果:例如,我们修改了 shader 中 include 的另一个文件,但是 fileserver 并不知道,虚拟文件系统返回了 cache 结果,而没有重新编译。


一开始想解决这个问题时,我想放弃惰性构建这个机制。即,把资源转换和 fileserver 分离。这样,修改了源文件,就由资源转换模块去构建这个资源文件。客户端永远认为在运行时资源已经是构建好的(和 Unity 一致)。

这个方案最为简单,但在组内讨论的时候很快被否决了。因为这样又倒退回去了,并没有解决原先想解决的痛点。经过几个方案的讨论,我们最终找到了比较合理的方法。

以 shader 为例,假设有一个 shader 文件叫 a.sc ,有一个 a.sc.lk 指明了 sc 该如何构建。我们的 fieserver 在收到 build 请求的时候,会无条件的重新编译 a.sc 在指定平台上的结果,并把结果文件的 hash 返回。这一步是不做任何 cache 的。

但是,新的方案中,如果你请求了 a.sc 在 ios 上的版本,那么,构建模块会在虚拟文件系统中添加一个叫做 a.sc.lk.ios 的构建脚本文件,详细记录了 a.sc 在 ios 上的构建方法,和构建过程中的依赖关系,包括依赖文件的路径,和当前这些依赖文件的 hash 。

那么,这个 a.sc.lk.ios 文件,其实就唯一确定了一个编译好的目标文件。因为任何一个依赖文件的修改,都会导致文件内容的变更(hash 值变了)。

这个文件在当此运行会话中,对 fileserver 的客户端是不可见的。这是因为我们假定运行过程中,所有文件在一颗 merkle tree 上,是不可变更的。但是新的会话就能看见这个新增的 .ios 文件了。

一旦客户端看得到 .ios 文件,它就可以用这个 .ios 文件的 hash 去请求编译的结果。由于每个 .ios 文件的内容都能唯一确定一个编译结果,这样,fileserver 就能对结果做 cache 。

.ios 文件不是资源编译的结果,而是编译的参数。它在数据仓库里就可以有多份,而编译的结果只需要保存最终的一份。大多数情况下,用户只会请求最新的一份编译结果,但万一用户请求过期的版本,fileserver 也可以重新生成出来。

这有点像 github 的大文件储存方案,不在 git 主仓库里保存大的二进制文件,而保存了一个唯一的 url ,把文件放在了另一个服务中。在这里,这另一个服务就是编译资源的模块。


这个机制做到了惰性编译资源,又可以合理的 cache 资源的编译结果。和 Unity 的 cache server 不同,它用来索引 cache 的 key 其实是编译资源的完整过程,包括了编译的依赖关系。

所以,编译模块,完全可以跨项目的 cache 这个过程。即,如果你一张贴图用在一个项目中,被编译过一次;当你把这张贴图复制到新项目使用时,是不需要重新编译的。而 Unity 中同一张贴图即使在同一个项目中换个位置,都会导致 guid 变化,从而让 meta 文件变化,致使资源重新编译。

文件服务还知道所有的需求,所以它完全有能力在你请求 ios 版本的同时,预期你还会在以后请求 andriod 版本。所以 fileserver 还有能力利用闲置时间去提前生成那些尚未请求的版本。而 Unity 的 cache server 则是一个纯粹的 key / value 服务,完全不可能做到这些。

Comments

风哥,七夕快乐。

这个算法跟ccache的想法有点像,ccache也是把源文件、依赖的头文件和编译参数等等全部放一起计算hash。

厉害,伟大的思想

这个算法不错,完全可以直接用 HTTP 协议实现一个动态编译的 HTTP 服务,用 lk.ios 文件哈希值作为 ETag ,然后服务器检查 If-None-Match 头来确定是否需要跳过编译。

缓存服务器不用自己实现,挂一个 Nginx 代理到自己实现的编译服务器就行了。

这样就能把编译和缓存两件事情分开。后者压根不用自己做。

Post a comment

非这个主题相关的留言请到:留言本