月度归档:2016年12月

OpenShader月报 #0

虽然没有做paperwork的习惯,但我觉得在这时候还是写一篇总结这个月工作进展的文章比较好,毕竟卫星放出来如果没时不时地搞点大新闻的话总会让人觉得弃坑了是吧 233
  • RenderChunk更新优化
    众所周知MC的地图是以16x256x16的Chunk - 即所谓的区块 - 为存储单位,但在渲染时是以16x16x16的RenderChunk为单位进行更新和渲染,问题在于RenderChunk的更新并不是很快,在i5-4590的机器上大概需要6ms每单位,虽然MC已经引入了多线程更新RenderChunk的设计,但这只局限于由地图加载导致的更新,由砖块变化产生的更新依然要求单线程更新,这就导致了当发生大规模的砖块更新时游戏帧率会骤然下降,比如大规模流水或接入高频红石电路的红石灯等.
    RenderChunk的更新操作实际上是遍历区块中16x16x16的部分逐砖块地进行可见性判断,然后将砖块的模型面填充入顶点缓冲中,其中获取光照度和获取砖块的IBlockState这两步操作开销莫名的大,而在原版的更新流程中它们的调用又十分频繁,因此在此做一个简单的缓存就能戏剧性地加快原版更新速度.
    然而Forge还增加了它自己的更新流程,被称为ForgeLightPileline,顾名思义,它是解决在复杂模型下原版光照错误的问题,在默认情况下它是取代原版流程的,它的速度跟未经优化的原版差不多,然而它的可优化空间实在不大,一堆堆的三维数组像节日彩球一样在程序里传来传去还要不停地拆包打包可真没有什么优化手段,但是没有它又还真不行,因此这里采用的措施是增加一个判断器根据被渲染的砖块的模型类型来判断该采用哪个更新流程,毫无疑问这一步并不快,因此加入了一个LUT缓存,采用打表查表的方式来加速判断.
    此外还有一个被临时取消掉了的优化,是让上文提到的发生变化的砖块也进行多线程更新,这一项优化效果斐然,但是却存在两个巨大的缺陷,一个是在首次进入游戏时有大概5%的概率因为某个竞态条件而陷入卡死 - 提交入队列的更新任务莫名其妙地没有被执行,这个问题我调查了几天也没有结果;如果说它可以通过一个条件变量强制在首次更新时采用单线程来"解决"的话,下一个问题根本无法绕过,在更新区块时,某些BlockRenderLayer的变化要到下一帧才会反馈,首先要解释一下BlockRenderLayer(以下简称layer),从1.8开始MC中的砖块被区分为4种layer: SOLID(无Alpha测试,有Mipmap)、CUTOUT_MIPPED(有Alpha测试,有Mipmap)、CUTOUT(有Alpha测试,无Mipmap)和TRANSLUCENT(开启混合). 在渲染时MC会将按照这4个layer逐层渲染砖块,原因很显然,不同的layer需要不同的渲染状态.而在引入多线程更新后遇到的问题就是,当区块发生更新时,某些layer中的顶点变化总会慢一帧才能表现出来.
    比如,草地的layer是CUTOUT_MIPPED,石头和泥土地是SOLID,当你敲碎一个放置在草地旁的石头后,石头破碎的地方不会被填上草地,而且会露出一个空洞,这个空洞直到下一帧时才会补上,然而奇怪的是设断点会发现更新只发生了一次并且是发生在敲碎石头的那一帧,这难以解释为何一次更新的效果直到下一帧才会生效,在更新完毕后将数据从客户端(CPU/内存)上传到服务器(GPU/显存)的操作无论是单线程版本还是多线程版本都会被留在主线程进行,或许是由于内存可见性什么的问题,但不管怎么说,具体原因始终未能确定,因此只能暂时取消掉多线程更新这个优化.
    图:在击碎砖块的瞬间留下的空洞,这个空洞在下一帧即被填上了
  • 自定义砖块贴图
    Shadersmod增加了法线贴图"_N"和高光贴图"_S"两种额外的砖块贴图,现在OpenShader也完成了新增砖块贴图的功能了,开发者可以自定义要载入的贴图的后缀名以及当无贴图时的默认值,比如法线的默认值可以采用0xFF7F7FFF(BGRA格式的(1, 0.5, 0.5, 1)). 不过显然在切换光影包时重新载入一遍贴图会很慢,因此Mod提供了一个选项是提前缓存_N和_S两种后缀的贴图并且在切换光影包时不卸载它们.
  • 界面&选项
    现在已经有了一个类似Shadersmod那样的光影包菜单,并且实现了切换或重载,理论上讲Mod现在已经可以正常使用了.
  • 多线程渲染准备
    在进行正式渲染之前,MC需要先准备渲染数据,比如计算镜头数据、对场景做视锥裁剪等,这一部分在之前是单线程完成的,现在这一步是以Pass为单位进行多线程处理,当Pass数量小于等于CPU核数时,渲染准备所需的时间就从"所有Pass准备时间之和"变成了"最慢的Pass的准备时间",也算是省出了一些时间吧...
正在进行的工作:
  • 池化VBO&MultiDraw (60%)
    当我在测试用的光影包中尝试实现一个朴素三重CSM(不根据视锥做偏移,阴影镜头始终对准玩家)时发现性能急剧下降,从Profiler的结果来看一个问题是最大视距的那个阴影镜头所需要的准备时间太长了,另一个问题是砖块绘制过慢,在10视距时,玩家视角与3个ShadowMapping过程总共需要绘制1000+次RenderChunk,绘制一个RenderChunk时还要为每个layer绑定一次VAO再做一次Drawcall,这样算下来每帧会需要数千次Drawcall和等量的VAO绑定,性能瓶颈妥妥地是在驱动上,事实上确实当显卡占用没增加多少而CPU却有一个核心已经跑满了. 因此问题在于如何降低调用数量,N卡有个BindlessMultiDrawIndirect,可以在不绑定VAO/VBO的情况下进行单指令多渲染,但显然我们不能指望一个供应商独占扩展...因此我把注意力集中到了最简单的MultiDraw上,MultiDraw是个很早的特性,我记得好像是GL1.5时代就有了...它可以一次指令绘制一个几何体中多个不连续的部分,而缺点是中途不能切换纹理等,显然这对MC来说不是问题,关键在于如何将多个RenderChunk中的几个layer的顶点数据都整个在一个VBO中.
    我的解决方案是以4x16x4个RenderChunk为一组(也就是覆盖4x4个Chunk)使用一个手动维护的内存池来共同存储所有layer的顶点数据,这个内存池的实体是在显卡显存中的一个VBO,在客户端程序会负责空间分配以及数据传输等工作,当RenderChunk向内存池首次提交一次数据更新时,内存池会根据数据体积分配一段略大于数据体积的空间,然后返回给提交者一个指针(有意思的是,这个指针是可变值(Mutable),我知道一个可变值指针听上去即不可思议又恶心,但这确实能省下不少事),同时会在一个SortedSet中记录已分配的指针,有个例外是空数据提交产生的指针,这种指针不会被插入到那个SortedSet中.
    每一个内存池都要面临的问题是碎片整理和扩容,理论上这个内存池可以通过glCopySubBuffer来整理碎片,只要将末尾的内存复制到前面的空隙即可(glCopySubBuffer有个特性是复制源范围和目标范围不能重合,这导致了复制空隙紧后面的数据到前面来,以"冒泡"的形式整理碎片的方法不能用了),但我暂时还没实现,现在的做法是在扩容的同时顺便进行碎片整理,glBufferData在扩容的时候不会保留旧的数据,因此得先分配一个新的VBO,然后把数据复制过去,再销毁旧的.在复制时如果遇到空隙的话就会跳过去,同时纠正之后的指针的偏移量(这也是为什么它是可变值)
  • 指令队列 (30%)
    鉴于OpenGL要求必须在主线程渲染的特点,压榨性能的方式之一就是将渲染数据的整理工作通过多线程来处理,主线程只负责指令提交工作,指令队列响应了这一思想,所有渲染操作都被分类为一些指令,一个阻塞队列负责缓存这些指令,协线程负责收集数据和生成渲染指令,并将指令送入队列,主线程从队列取出指令并执行,这应该可以一定程度上提高性能,毕竟拥有更高的并行度,并且主线程执行的工作更单一,不会受CPU缓存污染的困扰. 现在,唯一的问题在于,从队列中读取指令并执行的开销会抵消这些提升吗...
  • Early-Clear (0% :P)
    上文提到了渲染和数据准备是分开的,但事实上有一种渲染操作"基本"无需数据 - 清除缓冲,说它"基本"是因为主视角的清除颜色缓冲还依赖于雾颜色,而雾颜色的计算是在相机计算中进行的,因此可以在完成相机计算后主线程提前开始为各个Pass进行缓冲清除,而协线程继续准备其余的数据.
  • 很...很多... (紫asdfghjkqwertyuiozxcvbnm%)
    显然今年没Demo了! 看看明年1月卫星能不能落地吧! 新年快乐 🙂
阅读全文 [...]

[新坑预告] Yet another shadersmod implementation

最近闭(zhuang)关(si)了几个月,我并没有被Hello Games干掉(顺便在这里祝贺他们没有跑路而坚持在Steam黑五打折期间出了个主要更新,以及敢于向玩家们弹小窗的勇气 - 他们是我见过的第二个向玩家弹窗发送更新通知的游戏厂商,第一个是制作MachineCraft的G2CREW) 这4个月我基本都在过着6点睡2点起(嗯,早晨6点,下午2点)每天见不到几个小时的太阳的生活,在肝腻了文明V(没有I)、舰R、崩3、WT后我终于决定该写点什么,正好手头这个秘密开发了5个月的坑终于有些眉目了,因此决定放个预(wei)告(xing),有人说把坑公开出来就能督促自己不弃坑,然而对我这样的脸皮略厚的人来说好像并没有什么用...2014年我公布了AsmEventBus(基于ASM的Java事件总线系统,比Guava那个基于反射的系统要快很多)和一个没公开名字的Java模块系统(如果称它是"类OSGI"显然有些装逼,但它做的事确实和OSGI差不多) 然后理所当然地坑了,其实对于前者我挺耿耿于怀的,明明只要再完善一点就是个很好的库...今年年初在日狗的软工作业要求下不得不放了个某文字H游戏引擎的Java开源复刻卫星,然后,嗯,然后没下文了.写到这时我自己都忍不住掩面笑了一下,看来羞耻柱这种东西对我这样倒错的人来说并没有什么监督作用.其实我挺怀念11~12年的那段时光,那段不知失败为何物敢于写任何自己想写的代码的日子,在11年的最后一个月我在这个房间的同一个角落半生不熟地用C#写一个文字猎奇游戏,用慢的吔翔的GDI+在WinForm上画文字(现在一想其实挺像ERA啊,当时我要知道ERA的存在的话是不是就给ERA写脚本去了?) 最吃精(?)的是当时我居然在试图写一个自己设计的脚本语言的解析器,幸好当时没做出来,不然这足以让现在的我感到自愧不如,有人说好的程序员应当在看到自己6个月以前写的代码时能发觉自己现在的进步,这么说我应该尽快删IDE退圈了. (笑) 不过我倒真的挺怀念那个项目,毕竟能亲手(即使是只有文字)肢解幻想乡的女孩子怎么想也是一件刺激又有趣的事情,有机会的话我一定要把它复刻出来. (啊呸)

怀古伤今的时间到此为止了,现在说说手头上这个坑,当时提出做MC光影Mod的复刻这个概念是在什么时候已经无从考证了,印象中是14年7月我和ici2cc给CustomSteve做光影Mod兼容时第一次提出了自己做光影的想法,不过当时由于很"容易"地完成了CS与光影的兼容工作,因此这个念头就被打消了.一年半后的16年2月我写完了光影包教程后和ici侃大山时聊到了光影Mod的种种不足,当时我开玩笑地提出自己也打算写一个光影Mod,ici沉默半响后问道你是认真的吗,这时我才开始真正考虑这件事,具体的讨论过程记不清了,结论大概是坑太大填不起,而且当时我刚完稿光影Mod教程很累,也并不想就这么立刻废掉自己的工作成果,更重要的是我想去填一个自己之前挖出来的大坑(这个坑不想提了...也不用猜,"基本上"从未公布过) 因此这件事就被放下了,时间到了6月,那时不知在哪我看到了一个消息,Continuum光影包的作者在用C++给MC写一个渲染器,不用说也知道它会支持第三方光影包,当时就把正在补伊里野的天空的我吓得把播放器关了,为什么呢,因为当初我被Continuum的作者肛过一次(大雾) 2月初我在撰写光影包教程的最后两章时ici弹小窗告诉我"被抢先了",当时吓得我差点提前去见幽幽子,ici赶在我失神之前发来条链接,我赶紧缓过来点开一看是个油土鳖视频列表,上面三四片Continuum的作者录制的光影包教程视频,简单地看了一下后我半自我安慰地得出了个结论:(局座脸)这教程,飞不起来! 本着公平竞争的原则,这里贴出视频列表地址,为什么那么说呢,我感觉他过度死扣光照、ToneMapping和PBR,而忽略了对光影Mod特性的介绍,最简单的例子,除了我的那篇附录以外还能从哪找到对光影Mod的技术规范文档呢,恐怕官方Wiki都没有这么详细,然而话是这么说,但毕竟人家已经抢先发出来了,"第一个光影Mod教程"这个头衔是抢不到了,只好奋笔疾书去抢"第一个成文的光影Mod教程" (笑) 这也是为什么我的教程中最后两章明显的很潦草的原因 (当然,我写烦了也是一个原因...) 后来我的教程发出来了,而他的教程弃坑了,我还顺手打了两发对他的黑枪 (诶嘿,我这人咋就这么爱打黑枪呢) 一个是他那个"号称世界最强却实际只是又一个C13衍生品的光影包",一个是他开的"MC的Vulkan渲染器"坑.故事就看似告一段落了,然而我万万没想到的是他那个Vulkan渲染器在知难而退后又蜕变成了"C++写的OpenGL4.5渲染器",然后又把我肛了一次!被一个人肛两次这事能忍吗!

然而当时是考试周,我只能忍下去了 (笑) 考试结束后我开始探究技术可行性(后来证明这几乎是无用的,所有遇到的问题这时都没发现) 并于7月的实习第一天在那间位于商住一体楼里的破房子中建了项目的Git仓库,版本记录显示前三天我写的代码除了Mod主类和Coremod的LoadingPlugin以外没有一行保留到了现在 (手动滑稽) 一方面说明了那个"百分之多少(记不清了)的代码是要在一年内被重构掉"的理论是正确的,另一方面说明了当时我是有多么低估了问题,虽然那次糟糕的实习让我失去了去CJ和Jeb见面(对此ici可以吹一辈子(笑))以及勾搭上养猪场的机会并且错过了魔都THO,但如果说那一个月实习有什么用的话,那就是让我塌下心能用当初徒手肝解析器的劲头从零制作"Yet another shadersmod implementation",不,它不叫"Yasi"或什么的(虽然现在看起来还挺酷!) 也不叫ShaderCraft云云,我将其命名为OpenShader,因为Open这个词对于开源程序员来说就如同猫薄荷对猫一样充满魅力,显然这是开源的,不过我将它暂时托管到了私有仓库中,因为当初我也实在不敢确定它可以完成,毕竟我之前失败过的太多了,事实上,直到11月初时它甚至还没法正常运行,而我最初的计划是10月初给出一个Demo...当初我写光影包教程时也曾"计划"在10月初完稿,不过现在看来这次我似乎不必拖到次年2月了,然而Mod维护是一件长期的工作,不是吗?主要的技术突破都是在11月中完成的,现在它已经实现了:

  • 高度自定义的渲染管线,可自定义每一帧(Frame)渲染时采用的Pass,以及每一Pass包含哪些绘制阶段(Stage)
  • 着色器加载系统,包含一个简单的Includer实现(无需那个ARB扩展).
  • 可自定义的帧缓冲和帧缓冲的挂件(颜色、深度、模板),包括挂件的尺寸和格式.
  • 顶点着色器的顶点属性(Attribute)注入.
  • 一致变量(Uniform)注入.
  • 优化的GlStateManager,尽可能地减少OpenGL调用.
  • 特性(Feature)系统,用于开启、关闭或改变一些MC的功能与属性,比如设置太阳偏斜(光影Mod的sunAngle)
  • 使用VAO渲染区块 (WOW!) 理论上讲90%+的显卡都支持ARB_vertex_array_object和APPLE_vertex_array_object中的一个,如果有哪个辣鸡卡两个都不支持,那它也不见得跑得动着色器.
  • 支持Mod式和外部文件式的光影包(其实只实现了前者)

当然,它还有一个不短的TODO List,由于没有写TODO List的习惯(??)这里先随手写上一些能想到的:

  • 纹理系统,比如光影Mod的载入法线和高光纹理,以及载入外部纹理文件,可以搞基于LUT的颜色校正啦.
  • 用户界面.
  • 外部文件式的光影包的解析.
  • 资源管理...
  • 条件允许时使用UBO更新一致变量.
  • 世界上有两群人需要人间会社程序员的人间关怀:乌干达的可怜儿童,和苹果机用户.
  • 还有一些疯狂的念头,不过都是要在前面这些完成的前提下才有...
  • 大量的,关于紫sama蓝sama幽幽sama觉sama恋sama秦心酱和玛艾露贝莉x莲子的福利

想说的暂时是这么多,如果成了的话喜大普奔,再一次弃坑了的话那就又是一次喜闻乐见的自挂城墙,我先睡觉(jue)去了...这里贴一个Mod式光影包的光影包初始化代码和渲染管线构建代码的一部分 (群众:有毛用啊!) 可以大概了解一下它的API风格,毕竟将来有了外部配置文件式的光影包后,基本上也会跟它差不多:

53057998_p0

不好意思,贴错了...



20161203071732 20161203065401

这个才对 √

阅读全文 [...]