月度归档:2017年04月

OpenShader月报 #1

TL;DR: 弃坑啦!!!

好吧这是开玩笑的,虽然沉寂了很长时间,怎么看都像是"好难啊不做了"的样子,但项目确实还在继续开发中.本来说好1月或2月发一个预览版本,然而实际上...实际上从1月初到3月初这段时间我基本没怎么碰OpenShader的代码,一方面是家里有事,另一方面当时我在摸鱼两个即兴项目(还特么是两个??),等想起来"哟我还有一个大坑没填"时已经是3月中旬了,所以说现在写一篇月报其实挺符合现实情况.

那么,首先是关于之前开的那些坑填的状况:

  • 池化VBO&MultiDraw (90%)
    池化VBO和MultiDraw已经基本完成了,目前现存的问题是内存占用稍微有点大,似乎还存在内存颠簸的问题,不过这些都是可以优化的. 从表现来看,池化VBO是相当成功的,砖块渲染的开销一下子从之前的第一骤降到了第三第四,甚至低于天空渲染和云渲染的开销...(谁想得到天空渲染怎么会有那么大的开销??),其实这也不意外,一个池化的VBO能承载16个Chunk(是的,16x256x16的那玩意,不是16x16x16的RenderChunk)的顶点数据,并用1次绑定加1到4次DrawCall(取决于这些Chunk的位置,大多数情况下是1~2次,极端情况会是4次)将它们全部渲染出来,在过去这需要16次绑定和16次DrawCall,而且实际情况会比这还要多 --- RenderChunk只有16x16x16,在山地之类的立体地形中渲染1个Chunk需要渲染多个RenderChunk --- 不过也有可能比这少,毕竟有些RenderChunk可能不可见.但无论如何,总体开销都不可能比池化VBO+MultiDraw要低. 在实现VBO内存池时我遇到了个吔屎的问题,我忘了OpenGL中大部分DrawCall类指令的偏移量单位是顶点而不是字节...这意味着顶点大小不同的顶点数据不能存入同一个内存池,因为数据的偏移量必须是顶点大小的整倍数,否则在DrawCall中没法指定偏移量,而顶点大小不同的数据存在一起时对齐会变得非常麻烦,因此最后我设计成使用多个内存池来存储16个Chunk的数据,每个内存池只存储特定一种大小的数据,问题解决了,虽然浪费的内存变得更多了... 不过还有一个小问题是现在KHR_Debug_Callback返回的Debug信息中经常会狂刷"Pixel-path performance warning: Pixel transfer is synchronized with 3D rendering." 主要发生在内存池扩容删除旧缓存时,这是什么鬼...
  • 指令队列 (100%)
    指令队列顺利完工了,但是表现的却不是很好,性能感觉提升了不到10%...而且Debug变得异常艰难,因为报错后只知道是在哪种指令中出错了,甚至都不知道是哪个Stage提交的哪个指令...不过也不意外,Nvidia在AZDO(Approaching Zero Driver Overhead)那篇presentation中提到他们用软件实现的指令队列的性能提升也只有14%. 现在只能期待后继的优化中将尽可能多的工作塞到指令准备时并行执行了,毕竟目前渲染准备阶段占用的时间也挺多的,如果能砍掉这部分时间(无论是优化还是并行执行)那对性能也是有很大的提升.
  • Early-Clear (0% 蛤蛤蛤)
    这一个完全没弄,主要是暂时没有特别大的需求,但今后肯定会有的.

正在进行的工作/已完成的其他工作:

  • 清理临时代码 (90%)
    花了几天清理了很多临时代码,主要是ASM那方面,之前在开发时遇到需要修改MC代码的情况时,就把MC代码复制到项目中的同名类里,利用Java类加载顺序的特性来实现覆盖原始代码.现在用ASM彻底重写了这部分了,为即将到来的开源做好了准备. 至于其他方面的临时代码(大量的TODO) 慢慢解决吧...
  • 采样器 (100%)
    采样器决定了着色器如何从纹理中获取纹素,在过去(去年12月的版本)其实已经有采样器的原型了,但是只能用来控制着色器读取哪些纹理,而不能控制着色器怎么读取,现在开发者可以设置采样器的参数了,包括纹理过滤、各向异性过滤、Wrap、Mipmap以及是否开启比较模式.采样器有什么用呢? Shadersmod中启动shadowHardwareFiltering其实就是开启Shadow map的比较模式. 采样器还有两个版本的实现,一个是针对低版本的传统模式,通过glTexParameter修改纹理的采样参数来实现;另一个是针对拥有ARB_sampler_object扩展的高版本,通过采样器对象来实现.
  • 自定义Uniform (50%)
    虽然OpenShader内置了不少Uniform参数,但毕竟总有我想不到的时候,因此总得有一种方法能让光影包开发者自行设定Uniform,目前的解决方案是让开发者可以添加一个自定义Uniform,然后监听更新事件并手动更新Uniform内容,这样的设计有两个缺陷,一个是只有Mod式的光影包能写代码来处理事件监听,另一个是MinecraftForge的居然要求事件总线的注册要在Mod初始化中进行,否则就会抛一个警告信息...我在考虑要不要设计一种DSL让开发者通过简单的表达式来描述需要获取的内容;或者利用Java的ScriptEngine内置JavaScript的特性,让开发者写JS脚本去获取数据.不过这些都无法回避一个问题,就是运行时Minecraft的字段都是混淆的...如何访问这些被混淆的字段还是个问题.
  • 图像统计 (25%)
    简单地说,图像统计是用来获取渲染结果中的某些信息,比如平均亮度(用于HDR)或中央深度(用于DOF)什么的,这个操作主要面临的问题是如何将获取到的结果再递交回去,比如说将统计到的结果再注入到一个Uniform里,这是最简单的方式,不过会涉及到CPU-GPU同步的停顿,对于这个问题,国产的开源引擎KlayGE的解决方案是使用计算着色器来统计亮度,然后将结果写入一个纹理的固定位置(DX的左上角(0,0)),后续处理时就直接从此纹理的固定位置读数据就行了,整个操作可以一气呵成,无需CPU从GPU那里读回数据,顶多有必要的话加一个屏障保证HDR发生在亮度统计之后即可,写入纹理在OpenGL中对应的操作是Image Load Store,不过假如让我来实现这个的话,我可能会选择SSBO,因为Image Load Store到4.2才进入核心扩展,而4.3就有了SSBO了... 嗯,然后你问没有计算着色器的辣鸡该怎么办? 把纹理慢慢读回内存然后让CPU慢慢跑吧,同步的停顿想着就感人...
  • 兼容Shadersmod光影包 (60%)
    听上去像是一个很不可思议的事情,但考虑到OpenShader在设计时就是具有高度可配置性,那么Shadersmod的光影包不就是一种特殊格式的OpenShader光影包吗(笑),这个月我主要就是在弄这个,期间也找出并解决了OpenShader不少问题.目前的情况是已经能跑我在光影包教程中写的那个MyFirstShader了(不过还存在一些小问题),至于别的...我试了一下SEUS,看上去能跑,但是总感觉有些差别,特别是炫光效果经常会闪烁,估计是纹理采样参数有差别;体积云也会有断层,估计是噪音图的Wrap没有设置成REPEAT而导致;流体砖块的Attribute注入也跪了,滑稽 23333
  • 无尽的Bug... (1%)
    还有很多问题需要修复,有些是缺乏边界条件时的处理,正常使用没问题,但如果使用姿势稍有不对就 --- DUANG! 还有一些就是彻头彻尾的Bug,比如原版MC的RenderChunk载入是使用一个优先队列,距离玩家最近的未加载RenderChunk永远是会被最优先处理的,这保证了玩家在移动时自己身旁的RenderChunk总是被加载出来的,而现在这个设定貌似坏掉了,变成了一个先入先出队列,结果就是当玩家都已经跑进未加载区域了,那边还在吭哧吭哧地加载旧区块...
  • 开源! \o/ \o/ \o/
    说好了的开源项目,怎么迟迟没有开源呢? 最大的阻力:临时代码已经被消灭了,我打算等Shadersmod兼容差不多实现了后就传到Github上,啊当然是创一个新仓库了,我怎么会把现在的私有仓库中羞耻的旧代码亮出来! (笑)

差不多就是这些,我去肝毕设去了(又是弃坑一段时间的节奏?),学校突然把毕设报告从6月提到了5月,药丸药丸啊 ?

4.26更新

基本能跑SEUS 10.1了,除了没有DOF (因为还没实现图像统计) 但是有个奇怪的问题就是亮的吃屎...想不出这是什么原因啊...

4.28更新

能跑SEUS10.2了,SEUS11.0也差不多不过水面有些问题.10.2屏幕过亮的问题是eyeBrightness弄错了,此外还修掉了一些奇怪的问题,不过依然有一些光影会跪的比较惨,群众喜闻乐见的Chocapic13 V6天空渲染会莫名其妙地坏掉.

另外还有一个问题可能要在很久之后修复,甚至是不修复...就是矩阵精度问题,Minecraft一直是直接用glTranslate、glRotate之类的GL函数直接操作OpenGL矩阵,而OpenShader则提供了一个封装(通过ASM魔改1.8新增的GlStateManager来实现,具体原理篇幅有限先跳过了 233),对矩阵的操作会先缓存在应用层,到下一次DrawCall之前再通过glLoadMatrix更新到驱动,这个可以降低GL函数调用的开销,然而带来的问题是OpenShader的矩阵精度好像差了一点点...

这"一点点"对常见的矩阵(模型视图矩阵和投影矩阵)没有影响,但对法线矩阵(gl_NormalMatrix,还记得我在Shadersmod教程的附录里提到的计算方法吗?模型视图矩阵的左上3x3的逆的转置)有着微妙的影响,由GL函数得到的模型视图矩阵算出的法线矩阵在乘完法线后,似乎能近乎神奇地保证法线的单位长度依然保持在1左右,至少是小于等于1;而OpenShader的矩阵呢,乘完后长度稍微大了那么一丁点...非常非常小的一丁点,但在某些鲁棒性较差的算法里就会跪了,比如说我的那个Shadersmod教程里的MyFirstShader...

MyFirstShader里用到了一个将vec3格式的法线编码成vec2格式的算法:

vec2 normalEncode(vec3 n) {
vec2 enc = normalize(n.xy) * (sqrt(-n.z*0.5+0.5));
enc = enc*0.5+0.5;
return enc;
}

...

vec2 normal = normalEncode(gl_NormalMatrix * gl_Normal);

最要命的地方在于那个sqrt(-n.z*0.5+0.5),它假定法线的长度一定不超过1,因此z一定是介于[-1,1]的,由此它才可以放心大胆地计算sqrt(-n.z*0.5+0.5)而不用担心-n.z*0.5+0.5小于0时sqrt蹦个NaN,但是由于矩阵精度问题,这里的法线长度超过1了,于是n.z=1.00001, -n.z*0.5+0.5=-0.000005, sqrt(-0.000005)=NAN,完.IEEE754规定NaN参与的运算都会变成NaN,就像JS中的undefined那样毒性十足,于是整个算法就跪了,编码出的法线在特定的角度(一般是平面垂直玩家视线时)会毫无征兆地突然崩掉.解决的方法其实非常简单,给gl_NormalMatrix * gl_Normal套个normalize就行了,幸运的是市面上几乎所有光影包在获取法线时都是用normalize(gl_NormalMatrix * gl_Normal) (你问我的那个教程光影? 它可不算"流入市面"哦?),因此基本不用担心这个问题,所以我也不太急着修复它...(更何况我也不知道如何修复 hehe)

4.29更新

试了一下发现Chocapic13 V3也会跪掉,原因是Chocapic13 V3使用了一个Shadersmod非常罕见的特性:DRAWBUFFERS中允许空白占位符,比如DRAWBUFFERS:NNN1N2就是gl_FragData[3]向colortex1输出颜色,gl_FragData[5]向colortex2输出颜色,而gl_FragData[0]、[1]、[2]和[4]不向任何RenderTarget输出.我当时没料到还有这种操作 233 V6跪掉的原因是Optifine内置的新版Shadersmod似乎支持在一个pass中向一个缓冲同时即读即写了,奇怪的是它的文档中却明确提到"Writing to color attachments that the composite shader also reads from will generate artifacts" 难道他自己搞了大新闻结果忘了更新文档了吗 ? 其实针对即读即写的情况启用Ping-pong我之前已经打算把它作为OpenShader相对Shadersmod而言的新特性了,不过因为需求不大一直没实现,现在看来是被Optifine将了一军了 233

阅读全文 [...]