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