« July 2014 | Main | September 2014 »

August 29, 2014

近日工作记录

sproto 基本算完成了, 等 lua 5.3 正式发布后, 还需要把 64bit 整数支持一下。我给 sproto 加了 lua 封装,以方便更好的支持 rpc 。

子熏同学完成了 sproto 的 jit 版本。但似乎性能提升不是很明显。

我希望可以在 skynet 的下个大版本,把 sproto 作为推荐 C/S 通讯协议加进去。


目前正在开发的 skynet 新特性是可以把单个服务的外来消息全部 log 在一个文件中。目前支持了 skynet 的普通消息以及 socket 消息。如果有必要,还可以把组播消息加上。

目前这个特性主要用来调试。其实可以为之开发配套的工具,比如另外做一个调试工具,能够把所以记录的消息重放给一个特定的服务脚本,便可重现一个服务的工作历史。目前 log 文件中记录的消息时间和消息内容足以重现。只要消息中不包含内存地址,这种录像重播的测试方法应该是有效的。

不过暂时还没碰到需要这种调试(比如一个服务出现异常,可以利用录像回溯之前发生的事情,以及当时的现场),等需要时再根据需要制作这样的工具。


等 lua 5.3 正式发布后,打算把 pbc 跟进一下。skynet 里的 int64 支持也可以用 lua 5.3 官方特性取代。我相信到那个时候就可以发布 skynet 的 1.0 版了。


ejoy2d 这个项目,公司有许多同事有兴趣做进一步贡献。所以我把主仓库迁移到 ejoy 名下。

由于正在用 ejoy2d 开发的两个新项目比较紧,最近 ejoy2d 里增加了不少临时项目用的接口。暂时还没有精力去规整。许多新特性(比如粒子绑定,对资源异步加载的支持)都没能及时加上文档。

目前实测在 iphone4 上 ejoy2d 可能会有性能问题。为此,增加了一个 renderbuffer 的特性,可以把一批渲染的定点输出到固定的顶点 buffer 中,这对用复杂图素拼装起来的静态背景会有一些效果。不过关键还在于 iphone4 的 GPU 性能太差,稍微复杂一点的 fragment shader 就会很勉强,为次可能需要给 ejoy2d 加入更灵活的 shader 定制特性。

经过几天的努力,终于把我们新项目在 iphone4 上的 fps 从 12 提升到了 18 ,勉强可以接受了吧。离目标 30fps 还有一些距离,如果进一步的细调还是可以达到的,但会增加很多制作上的难度。不知道到明年,还是否需要考虑 iphone4 这个档次的硬件。


btw, 乘 steam 打折,周末玩了一天文明 V 的第二扩展,还是很不错的。非常期待年底的 beyond earth 。

August 12, 2014

STM 的简单实现

STM 全称 Software transactional memory

在前年的项目里,我制作了一个类似的东西。随着 skynet 的日趋完善,我希望找到一个更为简单易用的方法实现类似的需求。

对于不常更新的数据,我在 skynet 里增加了 sharedata 模块,用于配置数据的共享。每次更新数据,就将全部数据打包成一个只读的树结构,允许多个 lua vm 共享读。改写的时候,重新生成一份,并将老数据设置脏标记,指示读取者去获取新版本。

这个方案有两个缺点,不适合实时的数据更新。其一,更新成本过大;其二,新版本的通告有较长时间的延迟。

我希望再设计一套方案解决这个实时性问题,可以用于频繁的数据交换。(注:在 mmorpg 中,很可能被用于同一地图上的多个对象间的数据交换)

一开始的想法是做一个支持事务的树结构。对于写方,每次对树的修改都同时修改本地的 lua table 以及被修改 patch 累计到一个尽量紧凑的序列化串中。一个事务结束时,调用 commit 将快速 merge patch 。并将整个序列化串共享出去。相当于快速做一个快照。

读取者,每次读取时则对最新版的快照增加一次引用,并要需反序列化它的一部分,变成本地的 lua table 。

我花了一整天实现这个想法,在写了几百行代码后,意识到设计过于复杂了。因为,对于最终在 lua 中操作的数据,实现一个复杂的数据结构,并提供复杂的 C 接口去操作它性能上不会太划算。更好的方法是把数据分成小片断(树的一个分支),按需通过序列化和反序列化做数据交换。

既然序列化过程是必须的,我们就不需要关注数据结构的问题。STM 需要管理的只是序列化后的消息的版本而已。这一部分(尤其是每个版本的生命期管理)虽然也不太容易做对,但结构简单的多。

我在 skynet 的 dev 分支上提交了叫做 stm 的 lua C 模块。

obj = stm.new(msg,sz) 可以用来生成一个用于数据交换的消息对象。通常,可以使用 skynet.pack(...) 来得到这个 msg 指针,和 sz 长度。

如果想把这条消息传递给别的服务,可以先用 copy = stm.copy(obj) 获得一个 copy 。这个 copy 是一个lightuserdata ,把它发送出去即可。

获得这个 copy 的一方,使用 reader = stm.newcopy(copy) 就能拿到这个对象了。

使用 reader(function(msg, sz) ... end) 可以把内含的 msg/sz 解出来。通常用 reader(skynet.unpack) 即可。

reader 返回的第一个参数为 true 时,成功获取了数据。之后是解码函数的返回值。

若第一个参数为 false 。stm 对象中可能没有数据,也可能版本没有更新。

这里提到的版本指:生成数据的一方,可以用 obj(msg,sz) 更新对象里面的数据。而读取方能正确感知数据的更新。


test/teststm.lua 是一个简单的范例。

August 02, 2014

Unity3D asset bundle 格式简析

Unity3D 的 asset bundle 的格式并没有公开。但为了做更好的差异更新,我们还是希望了解其打包格式。这样可以制作专门的差异比较合并工具,会比直接做二进制差异比较效果好的多。因为可以把 asset bundle 内的数据拆分为独立单元,只对变更的单元做差异比较即可。

网上能查到的资料并不是官方给出的,最为流行的是一个叫做 disunity 的开源工具。它是用 java 编写的,只有源代码,而没有给出格式说明(而后者比代码重要的多)。通过阅读 disunity 的代码,我整理出如下记录:


asset bundle 分为压缩模式和非压缩模式。压缩模式仅仅是用开源的 lzma 库 对整个非压缩包做了一次整体压缩。压缩数据的头有 13 个字节,前 5 个字节是 lzma 解压缩的 API 需要穿入的 props ,接下来的 4 字节是解压缩后的数据库长度。最后 4 字节不用理会它。

把压缩数据解开后,就和非压缩模式没有差别,下面只讨论非压缩格式:

assert bundle 的文件头是从这样一个数据结构序列化出来的。

struct AssetBundleFileHead {
     struct LevelInfo {
          unsigned int PackSize;
          unsigned int UncompressedSize;
     };

     string          FileID;
     unsigned int     Version;
     string          MainVersion;
     string          BuildVersion;
     size_t          MinimumStreamedBytes;
     size_t          HeaderSize;
     size_t          NumberOfLevelsToDownloadBeforeStreaming;
     size_t          LevelCount;
     LevelInfo     LevelList[];
     size_t          CompleteFileSize;
     size_t          FileInfoHeaderSize;
     bool          Compressed;
};

string 是直接以 \0 结尾的字符串,顺序序列化;size_t 是大端的 4 字节数字;bool 是单个字节;vector 就是顺着排列的结构。

根据 Unity 版本的不同,assert bundle 的格式也不完全相同。Version 指明了 bundle 的格式版本,从 Unity 3.5 开始到 4.x 版都使用 Version = 3 ,下面只讨论这个版本。HeaderSize 应该恰好等于以上这个文件头的数据长度。

一个 assert bundle 是由多个 asset 文件打包而成,接下来顺序打包了这些 asset 。序列化成这样的结构:

struct AssetFileHeader {
     struct AssetFileInfo {
          string name;
          size_t offset;
          size_t length;
     };
     size_t FileCount;
     AssetFileInfo     File[];
};

这样我们就可以分解出被打包在一起的多个 Asset 了(大多数情况下只有一个)。offset 表示的是除去 HeaderSize 后的偏移量。我们可以用 HeaderSize 加上那个部分的 offset 得到这个部分相对于整个 bundle 的文件偏移。

对于每个 asset ,又有它自己的数据头。数据头除了基本的数据头结构 AssetHeader 外,还有额外的三个部分。disunity 把它们称为 TypeTree ObjectPath 和 AssetRef 。注意:这里 Format 随不同 Unity3D 的版本有所不同,我们只关心目前的版本的格式,这里 Format 为 9 (其它版本的格式,在大小端等问题上有所不同)。

struct AssetHeader {
     size_t TypeTreeSize;
     size_t FileSize;
     unsigned int Format;
     size_t dataOffset;
     size_t Unknown;

Unity 对 Asset 数据做了简单粗暴的序列化操作。整个序列化过程是针对每种对象的数据结构进行的。TypeTree 是对数据结构本身的描述,通过这个描述,就可以反序列化出每个对象。

AssetHeader 后面紧跟着的就是 TypeTree 。但是,这个 TypeTree 对于 asset bundle 来说是可选的,因为数据结构的信息可以事先放置在引擎中(引擎多半只支持固有的数据类型)。在发布到移动设备上时,TypeTree 是不打包到 asset bundle 中的。

每个 asset 对象,都有一个 class id ,可以在 TypeTree 中查到如何反序列化。class id 的和具体类型的对应关系,在 Unity3d 的官方文档 可以查到。但若我们只是想将差异比较在对象一级进行(而不是具体比较对象中具体的属性),那么就不需要解开具体对象的细节信息,这部分也不用关心。所以这里也不展开(有兴趣可以读一下 disunity 的代码,格式并不复杂)。

在 AssetHeader 中的 TypeTreeSize 指的就是 TypeTree 部分的大小。接下来是每个 AssetObject 的描述数据。

struct ObjectHeader {
     struct ObjectInfo {
          int pathID;
          int offset;
          int length;
          byte classID[8];
     };
     int ObjectCount;
     ObjectInfo Object[];
};

这里,所有的 int 都是以小端编码的 4 字节整数(不同于外部文件格式采用的大端编码)。在 Unity3D 中,每个对象都有唯一的字符串 path ,但是在 asset bundle 里并没有直接保存字符串,而是一个 hash 过的整数,也可以看成是对这个对象的索引号。真正的对象放在数据头的后面,偏移量为 offset 的地方。

这里的 offset 是相对当前 asset 块的。如果想取得正确的相对整个文件的位置,应该是文件的 HeaderSize + asset 的 offset + asset 的 dataOffset + 这里的 object offset 。


接在 ObjectHeader 后的是 AssetRef 表,记录了 Asset 的引用关系。用于指明这个 bundle 内 asset 对外部 asset 的引用情况。AssetRefTable 结构如下:

struct AssetTable {
     struct AssetRef {
          byte GUID[8];
          int type;
          string filePath;
          string assetPath;
     };
     int Count;
     byte Unknown;
    vector Refs;