分类目录归档:Java

OpenShader月(nian)报 #2

咕咕,咕咕咕
(ge le, qi keng le)


时隔了很久之后...终于有了第三篇月报,在上一篇中我说到5月要肝毕设,那么,之后的时间? 之后的时间确实没有认真肝OpenShader,有很长一段时间在摸鱼,所以这段时间的进度怎么看都不像半年时间该做出来的...

  • Shadow Caster的裁剪
    在之前,阴影渲染也占据了不少性能开销,就像以前我提到在早期版本测试时使用3个阴影图的CSM时性能就急剧下降了,虽然池化VBO有效降低了渲染时的Drawcall开销,但在MC准备渲染数据时的开销依然不少,因此最好的办法就是在Shadow Caster中也进行一次裁剪,去除掉不能对玩家的视角形成阴影的物体和地形.

    关于裁剪体的计算,先将玩家镜头的视锥体朝向光源镜头的近裁面做一次投影,在投影得到的二维图形中找出最外层的边,每一个边沿着光源镜头的视线方向延伸得到面后,这些面就成了构成裁剪体的裁面.在这个裁剪体范围之内的物体大部分都能对玩家视角内行成阴影.如果说还有什么不完善的话,主要是刚才得到的裁剪体不包括近裁面和远裁面,对太阳光来说近裁面是没有必要的,而不计算远裁面就导致了一些本来不会贡献阴影的物体也被渲染了,会增加一些不必要的开销,一个未来的改进是选择玩家镜头的视锥体中,正面朝向光源的那些裁剪面,用这些裁面再做一次裁剪,就能去除掉那些false positive了.


    举例,比如如果这张图是一个渲染场景,中间高亮的橙色椎体是玩家镜头的视锥体,左上方为环境光源及其方向,圆锥、圆柱、棱角球和方块分别是4个可渲染的物体,那么在无Shadow Caster的裁剪时,这4个物体全部需要渲染.

    使用现在的视锥构建方法时,圆柱被剔除掉了,因为它即不在玩家视线内,也不会给玩家视线内的物体贡献阴影,然而方块仍然会被渲染,尽管它不会贡献阴影.

    最终目标是制作一个这样的裁剪体,保留下的物体只有圆锥 - 它会给玩家视线内贡献阴影,以及棱角球 - 它在玩家视线内.


  • 重新设计的多线程渲染流程
    显然,在OpenGL中不会有真正意义上的高性能多线程渲染(疯狂切context确实可以多线程,但它不够高性能),因此我们设计的多线程渲染流程也和其他的同类产品一样,努力压榨出更多的可在CPU端并行处理的东西,比如裁剪、收集渲染信息等,然后将它们分派到子线程上执行,主线程只保留最必要的工作:执行渲染指令.
    那么,我们先来看看上次月报(17年5月,应该叫半年报了?)时的渲染流程:


    在旧的渲染流程中,整个渲染被分为准备阶段和渲染阶段,在准备阶段程序会收集并配置渲染信息,主要工作分为镜头(计算每一个摄像机的位置和MVP矩阵)、地形(统计哪些RenderChunk需要被渲染)和实体(统计哪些实体和TileEntity需要被渲染)这三部分,此三部分有顺序依赖,不可调换顺序(地形需要镜头的信息来进行视锥裁剪,实体需要知道哪些区块要被渲染). 在旧设计中这三部分都会在准备阶段进行,尽管主线程在处理到每个部分时都会将工作分发给工作线程来进行,从而实现了并行化,但这里还有一个问题 - 主线程被完全浪费了,事实上渲染阶段其实无需等待准备工作全部完成时才进行,有些渲染操作并不涉及到地形和实体,比如天空背景和云朵;此外渲染阶段多线程的利用率非常低 - 只用到了两个线程. 因此我们重构的目标时尽可能缩短准备阶段,并尽量提高渲染阶段的多线程化程度,最终重新设计的渲染流程是这样的:

    在新的渲染流程中,只有镜头配置必须在准备阶段进行,其余的工作都是在渲染阶段进行,在启动渲染阶段时会有三个线程开始工作,主线程等待并处理渲染指令,一个工作线程生成渲染指令,另一个工作线程会串行处理地形的可见性判断,为什么这一步要单线程处理? 这涉及到它的工作方式,地形的可见性判断是用来找出哪些RenderChunk需要被渲染,主要的手段一是视锥裁剪,二是以玩家所在的RenderChunk为起点做一次广度优先搜索,为了防止搜索时重复搜索已处理过的节点,MC会在RenderChunk中标记一个时间戳,每次搜索到节点时都会尝试更新时间戳,如果更新成功(节点时戳比系统时戳旧)那么这个RenderChunk就是未被处理过的,如果更新失败(节点时戳与系统时戳一致)那么这个节点就是已经被处理过的节点,在多遍渲染时程序需要多次进行可见性判断,比如在渲染阴影时,需要有一个pass处理ShadowMap绘制的地形,一个pass处理玩家视角绘制的地形,如果想在此步骤并行处理的话,就需要把时戳换成一个原子整数,然后为每个pass分配一个bitflag,当此pass处理完一个节点时,就给那个原子整数标记上bitflag,显然,当所有的pass处理完毕之后还得再将那个原子整数清零,在旧的渲染流程中我就是这样做的,其结果呢? 嗯...确实是并行化了,但是在性能测试中发现更新原子变量的操作非常的慢,特别是最后重新清零的那个步骤,相比之下,之前串行处理时就不需要这个步骤,因为时戳始终是递增的,在处理完一个pass后,只要将系统时戳再喜+1就能继续处理下一个pass. 因此再三斟酌后,我将这一步换回了由一个线程串行处理所有pass的可见性判断. 但是之后的工作依然是并行处理,每当一个pass完成可见性判断后,程序就会再给线程池分配两个新任务 - 生成这个pass的地形渲染指令,以及准备实体的渲染信息.

  • 帧缓冲的Pingpong
    以前我在Shadersmod教程中提到过不要在一个着色器中对一个RenderTarget即读即写,因为在Texture Barrier出现之前对一个纹理在读出的同时进行写入属于未定义的行为,其结果是不可预料的,因此一般采用的策略是为一个需要即读即写的RenderTarget准备两个纹理,一个纹理作为读纹理,一个作为写纹理,每次运行完渲染后交换两个纹理的职责,原本读的下次变成写的,原本写的下次变成读的,这个方法被称为Pingpong. Shadersmod没有提供Pingpong功能,因此我们只能自行回避即读即写操作(其实回避的是异处读写,在片元着色器中原地读写的话在大部分显卡上实际上是允许的). 而OpenShader提供了Pingpong功能,在默认配置下程序会自动检测每个stage的帧缓冲和绑定的纹理,如果检测到一个纹理即被绑定到输入又被绑定到输出的话,就会自动启用Pingpong.

  • 图像统计 (WIP)
    图像统计之前我们已经介绍过了,主要是给像HDR之类的后处理特效用,不过...现在的OpenShader的图像统计功能非常菜鸡,仅仅只是达到了Shadersmod的水平,换句话说,就是只提供了获取屏幕中央深度的功能... 这个功能其实很鸡肋,想知道深度缓冲中央的值,直接给我texture(gdepth, vec(0.5, 0.5)).r不就得了嘛! 将这个值做成一个uniform除了卡流水线以外没任何用处,尽管中央深度可以通过将其替换为纹理采样来规避(怎么替换将是下一期月报的内容(豹笑)),但像统计全屏幕平均亮度这样的操作就只能靠实打实的图像统计来实现了,因此图像统计这方面还有不少工作要做...

  • RenderChunk渲染参数缓存 (WIP)
    这个...应该是最让我崩溃的部分,大部分功能在去年8月末就完成了,但由于两个折磨人的Bug导致它一直到现在都没彻底完工.

    简单地说,在之前准备渲染参数时需要程序逐一获取每一个RenderChunk的参数,这涉及到了大量的内存访问,以及随之而来的cache miss,即使是原本很简单的操作也会花费大量的时间,渲染参数缓存就是提前将这些参数缓存在一个整数数组中,将所有操作都简化成数组的复制和整数操作 -- 利用了OpenGL所有对象都以整数句柄的形式存在的特点. 比如渲染一个池化的RenderChunk时需要知道它在哪个VBO中、它的首地址偏移量和顶点数量,这些参数都可以缓存在整数数组中.

    这个优化被证明为是成功的,确实能提高不少性能,然而却带来了两个新bug.

    第一个bug是当玩家向任意一个方向移动一定距离时,就会发现反方向消失的区块会在面前出现...事先说明,这个bug已经被解决了,某天在我肝了几宿HOI4后决定再看看能不能解决这个让我崩溃了两个多月的bug,结果在灵光一现后(我一直笃信debug的时候灵感是最重要的 233)几个骚断点找出原因了,原因是我对MC自带的RenderChunk表(我对它的称呼,MCP把那个类称为ViewFrustum...咳)的理解不充分,我原本以为它会完整地利用所有空间,也就是表中每一项都会是一个可见的RenderChunk,然而实际上MC并没有充分利用每一个空间,在某些情况下,比如游戏载入之后玩家向某一个方向持续移动时,会出现某些项中的RenderChunk存在但却不该被渲染的情况. 原版MC中通过一个RenderChunk中的标记来判断是否该被渲染,而我在编写渲染参数缓存时没能正确缓存这个属性,结果导致了这个问题.

    远处那一排非常突兀的地形就是bug所导致的情况,实际上它们应该是在我背后过来的地方的某排区块.

    而第二个bug...则把我困惑到怀疑人生. 简单地说,在设计上,当内存池发生变化时,变化的内容(VBO的id、指针偏移量、数据长度)会被立刻刷新入缓存,然而现在存在的问题是在罕见的情况下缓存和实际内容会有一帧的不同步,结果导致因使用了错误的参数而渲染出错.

    出错的一帧的截屏

    这个bug一直到现在我都没能解决...最近倒是想出了一个workaround,暂时还没有实现,看看下一次更新前能不能做完吧 (鸽?)

  • 一些...杂七杂八的东西
    因为没有changelog,所以更新了什么东西都是靠翻git的变更记录来一点点查的(笑),主要是修bug和一些优化,其实现在OpenShader的状态是三十六拜后就差一哆嗦,然而这一哆嗦就是几个月都没哆嗦出来 ? 这段时间一直都在忙别的,看看这俩月能不能把最后一点肝出来吧,下一篇日志我们会介绍一些native方面的工作--
阅读全文 [...]

FGOW与FMM停止维护

准确说这并不意外,最近几个月如果你使用过FGOW和FMM镜像的话,肯定已经察觉到各种问题诸如无法下载之类的,事实上,FMM最后一次维护是在今年3月左右;而FGOW? 我都忘了上一次更新FGOW是什么时候了...

FGOW(ForgeGradle on Wall)与FMM(Forge Maven Mirror)开发于2014年5月,前者是用于解决一些ForgeGradle的缺陷,比如无法设置Maven源和其他必要文件的下载链接,无法像MCP那样将MC源码部署到开发目录;后者是一个非官方的Forge Maven源,搭建在SAE上. 这两个的组合曾经是很有效的,那么为什么突然就药丸了呢?
  • FGOW最初的设计并没有什么可扩展性,只是一个编码粗暴的针对FG1.1的小工具,而现在FG已经更新到了2.3...在许许多多的底层变动后,要更新FGOW可能跟重写一遍差不多了...
  • FMM搭建在SAE上,用过SAE的都知道它有一些坑爹限制,比如FetchUrl的8MB大小限制,最初这个问题不严重,毕竟鲜有哪个库能超过8MB,但后来FG打包附带了原本要独立下载的第三方库,体积直接猛增到了13MB,SAE的文档也诚不欺我,说8MB上限就是8MB上限,一字节也不带多下的,所以FMM的大文件(准确的说,只有FG的各个快照)一直都是在后台手动更新的...
  • 缺乏存在的意义,FGOW除了重设下载链接外,还有个重要功能是部署MC源码,然而后来有小伙伴指出其实可以直接在项目中建一个同包同名同内容的类,这样根据类加载顺序,实际载入游戏中的是项目中可供我们随意修改的那个类,由此一来,FGOW的那个功能就没什么用了.
  • 同上,既然FMM没了,那么FGOW也卵用了,思前想后,我觉得还是挂代理能一劳永逸地解决所有问题...而zzzz正好又提供了一个公用的SS账号用来构建Forge工作目录,所以我就可以弃坑了 (逃)
  • 缺乏足够的精力去继续维护--- 呸,直说吧,就是我太懒了 ?
  • SAE是要花钱的,当初FMM刚刚上线时SAE的价格还很实惠,我2012年注册时送的2000云豆能用十几年,然而自从SAE改了Mysql收费政策后,大概1000云豆只够用10天...当然这并不是主要原因,一个月30块钱还是氪的起的.
  • 并没有大量的关于紫sama蓝sama幽幽sama觉sama恋sama和玛艾露贝莉x莲子的福利!
那么现在FGOW和FMM没了,又该如何配置开发环境呢,之前提到了zzzz搭建了一个公共SS账号,账号在他的MCMod教程中(https://fmltutor.ustc-zzzz.net/1.1-配置你的工作环境.html). SS我觉得应该是现在码农们常备的"工具"了,假如你没有的话...就去下一个吧! 但是不要用他的链接里的SSR,前几天SSR的作者搞了个大新闻,被婊到删库退圈了 (滑稽) 阅读全文 [...]

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

阅读全文 [...]

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月卫星能不能落地吧! 新年快乐 🙂
阅读全文 [...]

使用SIMD+CriticalNative在Java中加速矩阵运算

对于游戏开发来说,一个健壮高效的数学库是必不可少的,特别是对于3D游戏而言,动作系统在计算骨骼动画时会进行数量可观的矩阵乘法或求逆运算;渲染系统也需要频繁计算变换矩阵.虽然一次矩阵运算消耗的时间可能不多,但对于分秒必争的游戏渲染来说想要力争60fps、死守30fps底线就势必不能放过任何一个免费提升性能的机会. 阅读全文 [...]

从1到100 - 模块化的跨平台程序

前几天完成了被当做作业的小程序,名字相当掩人耳目:Finite Digit Summator,一定程度上是向Digital Differential Analyzer致敬,项目被我扔到了Github上,本身并没有太大应用价值,除了那两幅从東方AA摘下来的字符画,以及一黑黑了两个游戏的梗.

从设计上,它的项目结构很大程度上参考了我以前的项目,以一个核心模块囊括主要功能,然后以多个针对不同平台的子模块负责将功能封装并展现给用户,事实上,除了网页版有一个功能是通过JS重新实现了一遍以外,几乎所有的使用了两遍以上的功能都被集成在了核心模块中,因此可以说下一阶段的目标"实现模块化"我已经完成一半了(笑),剩下的看上去无非是将之前没来得及上线的安卓端做完,修修Bug,刷刷单元测试之类的.

听上去通过模块化来实现跨平台就像当年老一辈眼中实现共产主义一样简单,然而一个实际的跨平台项目想要通过模块化来实现在设计上却是困难重重,最主要的问题在于硬件的局限性和需求的不同.还记得刚才说的"以前的项目"不? 2个月前的寒假时我开了一个新坑,用Java复刻(或者叫抄袭?取决于你怎么看待"yet another alternative implementation"这种东西...)一个Era的开源跨平台版,什么是Era?我放个截图你大概就能知道是什么东西了...

era 阅读全文 [...]

在Gradle中集成Javacc

有时我们会希望在项目中使用一些脚本语言、DSL或特殊格式的配置文件什么的,虽然已经有一些现成的方案,比如使用Java内置的JS引擎,或者使用LuaJ、Groovy之类的外部库,但这些不是局限性略大,就是需要附带庞大的库,比如FML就附带了一个Scala运行时库(以及一个编译器!),而实际上MC现在又有多少个用Scala写的Mod呢?看Kotlin最近势头这么火,估计过几天他们就得附带一个Kotlin库了吧...言归正题,这个时候我们就需要一个自行设计的脚本语言或者配置文件格式了,然而手写一个Parser确实有一定难度,不过好在市面上有一类神奇的东西:"编译器编译器" 阅读全文 [...]

MCMod教程开始恢复更新

MC1.9马上就要发布了,按照"总是差一个版本"的惯例(这是哪的惯例啊),教程准备从1.7更新到1.8了 ? 刚才看了眼第一篇教程,文中介绍的Eclipse居然还是4.3...现在第一篇教程已经更新了,最近那么多人抱怨没法配置开发环境,现在看来一点也不奇怪(捂脸,那篇实在太陈旧了).

想看看当初给MC1.2写ModLoader教程时的原始手稿,结果发现找不到了...这种东西还真能丢啊. 阅读全文 [...]

FGOW1.2.1和FMMv4

Forge在更新到1.8.8之后FGOW1.2.0就不能用了,于是自然而然地就有了FGOW1.2.1,新版本在功能上没有变化,只是支持了使用ForgeGradle2.1的Forge1.8.8和1.8.9.

下载地址:
SkyDrive:http://1drv.ms/21gcxy5
Dropbox:https://www.dropbox.com/s/ekig3gjx32uz3qp/fgow-1.2.1.jar?dl=0
百度网盘:http://pan.baidu.com/s/1geoIkin


此外,ForgeMavenMirror,也就是我们喜闻乐见的ForgeMaven仓库镜像,也更新到v4版本了.
更新内容包括:

  • 缓存了2.0、2.5和2.7的gradle文件,下载地址为"http://forgemavenmirror.sinaapp.com/gradle/gradle-[版本号]-bin.zip",启用它们的方式是修改Forge(其实现在应该叫MDK了)目录下的gradle/wrapper/gradle-wrapper.properties文件,将"distributionUrl="后面的下载地址改为镜像的地址.我之前没有弄这个是因为我不赞同这样做,Gradle的文件策略相当有问题,它是根据下载地址的Hash来识别版本的,这意味着不同下载地址的同一版本Gradle(甚至是同一个地址的https和http下载链接)会被识别为不同文件,你知道我的机器上已经有4个版本的Gradle-2.7-bin了吗?也许他们认为多版本并存很有意义,但我觉得仅凭下载地址来区分的多版本除了虐待硬盘以外毫无意义.不过现在考虑到Gradle已经成了GFW的重点关照对象之一,https链接几乎已经连不通了,这里还是提供了Gradle的缓存.
  • 增加了大量缓存,现在FMM已经可以代替所有的仓库了!对,你可以删掉除FMM以外的所有仓库,经过实测1.8.9可以在只有FMM仓库和本地Forge缓存目录的情况下配置.
  • 智能重定向,过去FMM在失败时只会重定向到Forge的仓库(files.minecraft.net),现在FMM会重定向到"最有可能"的仓库,此外,由于Oschina的Maven镜像复活了,对于Maven中央仓库的资源会重定向到Oschina的镜像.
  • 可选的快速失败,如果你不想要重定向功能的话,可以使用"http://forgemavenmirror.sinaapp.com/mavenff"这个仓库,它会在没找到缓存的情况下直接返回404,而不是重定向,这对于想要继续混合使用其他仓库的人来说很有用.
  • maven-metadata.xml缓存会在每天(北京时间凌晨1点)更新一次.因此,现在快照版本(Snapshot)又会被缓存了(之前由于maven-metadata.xml不会自动更新的问题,一度取消了快照版本的缓存).
  • 一些细微的优化.
阅读全文 [...]