月度歸檔: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

這個才對 √

閱讀全文 [...]