大家好,本文整理了現代圖形API的技術要點,重點研究了並行和GPU Driven Render Pipeline相關的知識點,調查了WebGPU的相關支持情況。
另外,本文對實時光線追蹤也進行了簡要的分析。這是我非常感興趣的技術方向,也是圖形學的發展方向之一。本系列后續文章會圍繞這個方向進行更多的研究和實現相關的Demo。
上一篇博文:
WebGPU學習(四):Alpha To Coverage
下一篇博文:
WebGPU學習(六):學習“rotatingCube”示例
本文內容
前置知識
-
現代圖形API包括哪些API?
包括DX12、Vulkan、Metal -
MVP是什么?
是WebGPU的最小可用版本。
在1.0版本發布前,先發布MVP版本。
技術要點
現代圖形API包含下面的技術要點:
下面依次進行分析:
並行
為了提高多核CPU和GPU的利用率,現代圖形API充分支持了並行。
並行包含下面的技術要點:
Multiple Queues
介紹
為了提高GPU利用率,可以將不同種類的任務對應的command buffer提交到3種隊列中:
graphics queue
copy queue
compute queue
不同隊列的任務能夠在GPU中並行執行,從而實現Async Compute,提高利用率。
參考資料
Multi-engine synchronization
WebGPU支持情況
根據Multiple Queues skeleton proposal,MVP只支持單隊列:
what single queue is exposed in the MVP
同步
介紹
有3個技術可以實現CPU與GPU之間以及GPU內部的同步:
- semaphores
我不了解它,它應該是用來同步隊列的
- memory barrier
它用來避免GPU因為資源依賴關系造成等待,以及避免CPU和GPU之間發生Race Condition。
現代圖形API更加底層,以前GPU做的同步工作也交給了圖形程序員,更加靈活的同時也加重了程序員的負擔。
- fence
它用來在CPU和GPU之間同步。
這3個技術的關系可以參考Vulkan Multi-Threading:
WebGPU支持情況
- semaphores
因為目前只支持單隊列,所以不需要它
- memory barrier
大家都表示memory barrier不容易實現,所以barriers由WebGPU幫我們做了(參考Memory barriers investigations、Memory Barriers portability、The case for passes -> Synchronization and validation),我們只需要給WebGPU一些提示(如指定buffer的usage)
- fence
支持以計數的方式實現fence。
參考資料
TimelineFences
多線程
介紹
可以在線程中執行現代圖形API相關的渲染任務:
-
在線程中更新資源
如更新buffer -
並行地編譯shader
-
並行地創建pipeline state
-
在線程中創建command buffer
WebGPU支持情況
有兩種方法實現多線程:
- 通過OffscreenCanvas API,實現主線程與渲染線程分離
根據Rendering to OffscreenCanvas on non-yielding workers:
WebGPU支持OffScreenCanvas API,但是目前Chrome不能使用它。
- 創建worker,在worker中執行WebGPU相關的渲染任務
Create a proposal for multi-worker中提出了WebGPU如何在worker中執行渲染任務:
1.Asynchronous texture & buffer uploads
2.Asynchronous shader compilation
3.Asynchronous pipeline state creation
4.Using MTLParallelRenderEncoder
5.Each thread in a thread pool records into its own command buffer
根據Minutes for GPU Web meeting 2019-08-05 -> Multi threading:
其中的1,2,3正在實現中;
4, 5會最終實現(沒有說好久實現);
根據我目前的調查:
1.shader編譯和創建pipeline state目前是同步的,還不是異步的。
2.在WebGPU 規范中,GPUTexture,GPUBuffer,GPUDevice,GPUComputePipeline,GPURenderPipeline,GPUShaderModule是Serializable的,意味着可以傳給worker。
那是不是現在已經可以在worker中使用它們,從而實現1,2,3呢?需要進一步驗證!
擴展閱讀
引擎對於多線程的封裝:
Parallelizing the Naughty Dog Engine using Fibers
Destiny’s Multi-threaded Renderer Architecture
內存管理
介紹
與memory barriers類似,現代圖形API需要程序員自己管理GPU的資源。
如Memory Management in Vulkan™ and DX12所示:
參考資料
Memory Management in Vulkan™ and DX12
WebGPU支持情況
根據WebGPU as low level graphics API :
WebGPU compares closest to Metal (probably since Apple is the one that originally proposed it)--both don't require manual memory management while DX12 and Vulkan do
不需要手動管理memory,WebGPU會幫我們管理
延遲渲染
defer shading
包括兩個步驟:
第一個pass遍歷gameObjects,創建gbuffer;
第二個pass遍歷lights,使用gbuffer計算光照。
相對於前向渲染,它的優點是只在屏幕上出現的像素中計算SHADING,從而使復雜度由O(M * N)將為O(M) + O(N)
WebGPU支持情況
因為支持MRT(多渲染目標),所以支持延遲渲染。
值得一提的是兩個優化的方向:
- 優化內存訪問
在Investigation: Managing on-chip memory中提到:
第一個pass創建gbuffer后,gbuffer的數據會從on-chip內存移到主內存中;
第二個pass讀取gbuffer時,將gbuffer的數據從主內存移到on-chip內存。
gbuffer的數據來回移動,造成了性能損失。
因此在Add render sub-passes中,建議增加render的子pass,在子pass中讀取gbuffer,從而實現在創建和讀取gbuffer期間,gbuffer的數據一直在on-chip內存中。
Minutes for GPU Web meeting 2019-10-28也討論了這一點。
WebGPU可能會在extension中支持這個優化。
- 針對tile-based defer shading,使用compute shader,在第二個pass中剔除光源,剩余的光源參與光照計算
正如DirectX 11 Rendering in Battlefield 3所說:
Hybrid Graphics/Compute shading pipeline:
› Graphics pipeline rasterizes gbuffers for opaque surfaces
› Compute pipeline uses gbuffers, culls lights, computes lighting &
combines with shading
› Graphics pipeline renders transparent surfaces on top
參考資料
延遲着色法
Optimizing tile-based light culling
DirectX 11 Rendering in Battlefield 3
textureless defer render
介紹
在defer shading的第一個pass中,我們將gameObject的幾何數據(如Position, Normal等)和材質貼圖數據(如從diffuse map中獲得的diffuse)存到gbuffer中。
有了bindless texture的支持,我們可以對此進行優化:
- gbuffer不再存儲材質貼圖的數據,而是存儲uv和material id。在第二個pass中,shader根據它們去獲取對應材質貼圖texture的數據
這樣做的優點是:
1.減少了gbuffer的大小
2.只在可見的像素中,采樣texture的數據,減少了采樣次數
這樣做也存在一些問題,不過都是可以解決的:
具體可以參考什么是deferred material shading?是否會在未來流行開來?:
1.多材質如何做deferred shading?總不能每個像素做動態分支,一個一個判斷吧。有人提出了做tile把像素區塊合並,然后一次性dispatch,性能會高很多。至於vgpr,sgpr,lds占用率之類需要通盤考慮,偏向一邊都會影響性能。
2.結果SSAO,SSR之類的post effect還是需要用到normal,roughness之類的g-bufffer信息。應用上還是需要權衡利弊。
以及參考Deferred Texturing:
What about mip levels, or derivatives?
- gbuffer不存儲幾何數據,而是存儲primitive ID。在第二個pass中,接收vertex data,在每個可見像素上執行vertex shader
具體可以參考Deferred Texturing -> Defer All The Things:
It stores only primitive IDs in its G-buffer; then in a later pass, it fetches vertex data, re-runs the vertex shader per pixel (!), finds the barycentric coordinates of each fragment within its triangle, interpolates the vertex attributes, then finally samples all the textures and does the shading work.
WebGPU支持情況
根據本文后面bindless texture的分析,目前WebGPU不支持bindless texture
或許可用texture 2d array代替bindless texture,從而使用WebGPU實現textureless defer render
參考資料
Deferred Texturing
什么是deferred material shading?是否會在未來流行開來?
BINDLESS TEXTURING FOR DEFERRED RENDERING AND DECALS
Modern textureless deferred rendering techniques
GPU Driven Render Pipeline
介紹
這個技術應該是在[Siggraph15] GPU-Driven Rendering Pipelines中提出來的。它的思想是把渲染任務從CPU端移到GPU端,減少CPU與GPU的同步和數據傳輸,實現1個draw call就渲染整個場景,從而提高GPU的利用率。
優點
-
GPU更細粒度的Visibility
-
不需要在CPU和GPU之間來回傳遞數據
應用場景
-
繪制大量的靜態物體
-
繪制人群
-
繪制模塊化半自動生成內容
主要步驟
離線處理
1.分解gameObject的mesh為多個cluster
參考GPU Driven Pipeline — 工具鏈與進階渲染
CPU
1.對gameObject進行粗粒度的frustum cull
2.使用persistent map buffer,准備GPU的數據
可以按照數據的類型,創建多個mapped buffer(如一個buffer存儲人群的數據,另一個buffer存儲所有靜態物體的數據)
3.使用virtual texture處理texture
所有的texture數據一次性全部准備好,只綁定一次texture
4.用indirect draw發起multi draw call,提交mapped buffer
WebGPU目前不支持multi draw,因此需要發起多個draw call,每個draw call使用indirect draw提交對應的mapped buffer
GPU
1.對gameObject進行frustum cull和occlusion cull
2.對gameObject的cluster進行frustum cull和occlusion cull
3.修改index buffer,生成新的indices數據
根據Proposal: Run all index buffers through a compute shader validator:
I'm inclined to propose that WebGPU MVP doesn't support index buffers changed on the GPU, since this is quite a bit of headache, but eventually we can do that.
...
In an actual 1.0 release we'll absolutely need to support GPU-generated indices, there is no question here.
WebGPU MVP不會支持在GPU端修改index buffer,1.0版本會支持。
4.multi draw call
根據ExecuteIndirect investigation:
In order to issue draw calls on the CPU, there must be a synchronization point where the CPU waits for the GPU update to complete. This is particularly devastating for WebGPU, where if the CPU has to wait for the GPU, you miss your implicit present and now you're a frame late. Being able to issue these commands on the GPU directly means the rendering and update steps can be in sync.
在GPU端發起draw call可以去掉“CPU和GPU同步”的開銷。
However, making it an extension seems valuable.
可能會在WebGPU extension中支持該特性。
總結
GPU Driven Render Pipeline可以一次性取得所有mesh data,通過virtual texture可以取得所有texture,意味着整個場景只需要一次drawcall
參考資料
[Siggraph15] GPU-Driven Rendering Pipelines
[GDC16] Optimizing the Graphics Pipeline with Compute
知乎大神MaxwellGeng關於GPU Driven Rendering Pipelines的相關文章1
知乎大神MaxwellGeng關於GPU Driven Rendering Pipelines的相關文章2
現在我們介紹下GPU Driven Render Pipeline相關的概念和技術要點:
Approaching zero driver overhead
這個概念(簡稱為AZDO)出自approaching-zero-driver-overhead,它分析了OpenGL如何使用GPU實現CPU端0負載,具體包括下面幾個方面:
- persistent map buffer
介紹
該技術是為了在“CPU把數據傳輸到GPU“時減小數據傳輸的開銷。
它包括下面的步驟:
1.映射GPU的buffer到CPU
2.在CPU端修改這個mapped buffer的數據(因為mapped buffer在shared memory中,CPU和GPU都可以訪問它,所以要使用fence同步來確保GPU沒操作這個buffer)
3.提交修改buffer數據的command
4.GPU執行該command,更新buffer數據
通過上面的步驟,不再需要“從CPU傳輸新buffer的數據到GPU”了,減小了開銷
參考資料:
Persistent mapped buffers
Persistent Mapped Buffers in OpenGL
WebGPU支持情況
有兩種方式實現“CPU把數據傳輸到GPU“:
1.調用GPUBuffer->setSubData方法
該方法性能差,需要從CPU傳輸數據到GPU(WebGPU規范並沒有定義該方法,但是Chrome的WebGPU實現目前有該方法)
2.使用persistent map buffer技術
對於該方法,有以下的要點要說明:
1)不需要fence
WebGPU提供了GPUBuffer->unmap方法,該方法將buffer設置為unmapped state,使該buffer能夠被GPU使用。
WebGPU應該在該方法中幫我們做了fence同步的工作。
2)如何創建mapped buffer?
有兩種方式創建:
a)調用GPUDevice->createBufferMapped方法,創建mapped buffer
Make it easier to upload data into buffers correctly指出:
createBufferMapped創建的buffer會使內存增加,因此需要destory它。
b)調用GPUBuffer->mapReadAsync,mapWriteAsync,將buffer設置為mapped buffer
Make it easier to upload data into buffers correctly指出,使用mapWriteAsync會造成一些問題:
in WebGPU, have an implicit present after rAF() returns
...
Using mapWriteAsync() requires you to wait on a promise, so if you do the naive thing and just wait on the promise inside rAF(), you’ll miss your present
...
Could we replace mapWriteAsync returning a Promise with it taking a callback that is guaranteed to execute before any submitted queue bundles are executed?
其中“rAF”指“requestAnimationFrame”
我們根據示例代碼來說明下這個問題:
function frame(time){
...
const vertexBuffer = device.createBuffer({
...
usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
});
vertexBuffer.mapWriteAsync().then((vertexBufferData) => {
設置vertexBufferData
vertexBuffer.unmap();
提交修改buffer數據的command到隊列中
...
});
window.requestAnimationFrame(frame);
}
因為mapWriteAsync是異步操作,而frame函數是同步操作,所以當執行到unmap時,可能已經執行了好幾次frame(過了好幾幀)。
在這幾幀中,可能提交了其它的command到隊列,WebGPU可能會在這幾幀之間提交了隊列中的command到GPU,GPU可能已經執行了其中的一些command。
執行unmap時,我們預期GPU還沒有執行其它的command,但實際上可能已經執行了。這樣會造成不同步的錯誤。
為了解決該問題,或許可以使用await關鍵字,將mapWriteAsync變成同步操作。
示例代碼如下:
async function frame(time){
...
var vertexBufferData = await vertexBuffer.mapWriteAsync();
設置vertexBufferData
vertexBuffer.unmap();
提交修改buffer數據的command到隊列
...
}
這里給出使用persistent map buffer技術的參考代碼(來自Buffer operations)
(參考代碼通過“調用GPUDevice->createBufferMapped方法”來創建mapped buffer):
//Updating data to an existing buffer(destBuffer)
function bufferSubData(device, destBuffer, destOffset, srcArrayBuffer) {
const byteCount = srcArrayBuffer.byteLength;
const [srcBuffer, arrayBuffer] = device.createBufferMapped({
size: byteCount,
usage: GPUBufferUsage.COPY_SRC
});
new Uint8Array(arrayBuffer).set(new Uint8Array(srcArrayBuffer)); // memcpy
srcBuffer.unmap();
const encoder = device.createCommandEncoder();
encoder.copyBufferToBuffer(srcBuffer, 0, destBuffer, destOffset, byteCount);
const commandBuffer = encoder.finish();
const queue = device.getQueue();
queue.submit([commandBuffer]);
srcBuffer.destroy();
}
參考資料
Make it easier to upload data into buffers correctly
What is the purpose of WebGPUSwapChain.present()?
Buffer operations
Minutes for GPU Web meeting 2019-10-21
- indirect draw
介紹
以WebGPU為例,draw方法需要指定頂點個數、實例個數等數據,每次只能繪制一個gameObject(可以批量繪制多個實例instance):
void draw(unsigned long vertexCount, unsigned long instanceCount, unsigned long firstVertex, unsigned long firstInstance);
而indirect draw可以使用buffer進行批量繪制多個gameObject(也可以批量繪制多個實例),這個buffer包含了每個gameObject的頂點個數等數據:
void drawIndirect(GPUBuffer indirectBuffer, GPUBufferSize indirectOffset);
優點
1.可以在compute shader修改buffer的數據,從而實現gpu cull
2.減少了繪制gameObject的次數
3.減少了CPU和GPU之間的同步開銷
WebGPU支持情況
支持Indirect draw/dispatch,相關討論參考 Indirect draw/dispatch commands investigation
參考資料
What are the advantage of using indirect rendering in OpenGL?
vulkan Indirect drawing
INDIRECT RENDERING : “A WAY TO A MILLION DRAW CALLS”
Surviving without gl_DrawID
- bindless texture and virtual texture
bindless texture和virtual texture可以結合使用,實現“只綁定一次texture”。
具體參見本文后面的說明:
其它->Bindless Texture
其它->Virtual Texture
GPU Cull
在GPU端實現剔除。
實現思路
1.創建persistent map buffer,indirect draw該buffer
2.在compute shader進行cull操作,將剩余的gameObject對應的draw call數據(如頂點個數)寫到該buffer中
相關技術要點
- 剔除的目標可以是gameObject的整個mesh,也可以是部分mesh(以cluster為單位)
具體可參考GPU Driven Pipeline — 工具鏈與進階渲染
- frustum cull
通過判斷目標是否在主相機的視錐體中,來實現剔除
- occlusion cull
通過判斷目標是否被遮擋,來實現剔除
具體可參考Hi-Z GPU Occlusion Culling
GPU Lod
在GPU端實現lod。
這個我沒有仔細研究,讀者可以參考相關資料:
谷歌搜索結果
GPU based dynamic geometry LOD
Hybrid Render For Real-time Ray Tracing
介紹
以前Ray Tracing只在離線渲染中使用(如制作CG電影,一般會使用path tracing來加快收斂速度),現在隨着DXR(DirectX Raytracing)的發布,新增了Ray Tracing管線,提出了專為Ray Tracing設計的shader,再配合上新的降噪方法(如使用SVGF降噪算法或者NVDIA提供的基於AI的降噪SDK),能夠實現實時的Ray Tracing!
混合渲染
完全用Ray Tracing來渲染太耗性能,所以目前業界使用混合方案來實現實時Ray Tracing:
如果支持DXR,可以使用“光柵化管線 + Ray Tracing管線”來實現;
如果不支持DXR,可以使用“光柵化管線 + Compute管線(即使用compute shader)”來實現。
我們可以把渲染分解為:
(圖來自於《Ray Tracing Gems》)
WebGPU支持情況
根據Is there some plan for Ray Tracing?:
There are not plan for ray-tracing for the forseeable future because WebGPU is meant to be extremely portable and ray-tracing isn't mature yet and is implemented only by a single hardware vendor for now.
WebGPU目前不支持Ray Tracing管線,因此只能使用“光柵化管線 + Compute管線(即使用compute shader)”來實現混合渲染。
如何使用WebGPU學習和實現Ray Tracing
可以按照下面的步驟:
1.廣泛收集相關資料,對整個技術體系有初步的了解(讀者可以看下面的“學習資料”)
2.參考Ray Tracing in One Weekend、Ray Tracing: The Next Week、對應的詳解,使用fragment shader,從0實現Ray Tracing。
目前只需要渲染球體或者立方體就好了,不用渲染模型。
3.使用compute shader實現Ray Tracing
4.使用混合渲染(如使用光柵化實現GBuffer和直接光照,使用Ray Tracing實現陰影和反射)
5.實現降噪算法
直接實現SVGF很有難度,可以先實現其中的子環節(如temporal anti aliasing、tone map、Edge-Avoiding À-Trous等),然后再把它們組裝起來,實現SVGF
6.渲染模型
需要實現BVH
7.進一步研究和實現,探索path tracing、優化采樣、優化光線排序和連貫性、支持更多的材質等方向
學習資料
一篇光線追蹤的入門
光線追蹤與實時渲染的未來
實時光線追蹤技術:業界發展近況與未來挑戰
Introduction to NVIDIA RTX and DirectX Ray Tracing
如何評價微軟的 DXR(DirectX Raytracing)?
Daily Pathtracer!安利下不錯的Pathtracer學習資料
Ray Tracing in One Weekend
Ray Tracing: The Next Week
Ray Tracing in One Weekend和Ray Tracing: The Next Week的詳解
基於OpenGL的GPU光線追蹤
Webgl中采用PBR的實時光線追蹤
Spatiotemporal Variance-Guided Filter, 向實時光線追蹤邁進
系統學習Ray Tracing的資料:Ray Tracing Gems
其它
Bindless Texture
在WebGPU中,什么是bind texture?
Investigation: Bindless resources提到:
Currently, in WebGPU, if a draw/dispatch call wants to use a resource, that resource must be part of a pre-baked "bind group" and then associated with the draw call inside the current render/compute pass. This means that all the resources that the draw/dispatch call could possibly access are explicitly listed by the programmer at the draw/dispatch site.
也就是說,我們需要定義每個texture在shader的binding,然后在每次提交command時,綁定該texture。
我們來看具體的textureCube sample:
綁定的texture需要在shader中指定binding:
//在fragment shader中指定binding為2
const fragmentShaderGLSL = `#version 450
...
layout(set = 0, binding = 2) uniform texture2D myTexture;
在BindGroup中,設置binding為2的相關數據:
const bindGroupLayout = device.createBindGroupLayout({
bindings: [
...
{
// Texture view
binding: 2,
visibility: GPUShaderStage.FRAGMENT,
type: "sampled-texture"
}]
});
...
const uniformBindGroup = device.createBindGroup({
layout: bindGroupLayout,
bindings: [
...
{
binding: 2,
resource: cubeTexture.createView(),
}],
});
把BindGroup設置到Pipeline中:
const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] });
const pipeline = device.createRenderPipeline({
layout: pipelineLayout,
...
});
提交command時,設置該bind group和pipeline:
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
passEncoder.setBindGroup(0, uniformBindGroup);
...
passEncoder.endPass();
在WebGPU中,什么是bindless texture?
Investigation: Bindless resources提到:
"Bindless" is a model where the programmer doesn't explicitly list all of the available resources at the draw/dispatch site. Instead, a large swath of resources are made available to the GPU ahead of time (e.g. during application launch) and then shaders can access any/all of them at runtime.
可以將所有的texture設置到一個buffer中,將其傳給GPU,然后shader可以在運行時操作任意的texture。
這樣的好處是我們不需要在每次提交command時綁定特定的texture,只需要綁定一次。
如果不支持bindless texture,可以使用texture 2d array替代
參考approaching-zero-driver-overhead->36頁,我們可以使用texture 2d array代替bindless texture,只需要綁定一次texture 2d array,不需要在每次提交command時綁定特定的texture。
texture 2d array的優點參考:
為什么要強調Texture2DArray在地形上的應用?
缺點是texture array中的每個texture的大小、格式要相同,而bindless texture沒有該要求。
為了解決該缺點,我們可以按照大小和格式,把texture划分為多組,對應多個texture 2d array。
WebGPU支持情況
從Minutes for GPU Web meeting 2019-08-12中得知,目前還未決定何時實現bindless texture,可能實現為extension,可能在1.0版本后實現。
所以目前可考慮用texture 2d array作為替代品
參考資料
OPENGL AZDO : BINDLESS TEXTURES : BATCHING PROBLEM SOLVED
Virtual Texture
思想
把所有要用到的texture拼到一起,組成physic texture;
通過索引,只把當前要用到的texture加載到內存中。
優點
1.只綁定一次texture
2.組成physic texture的子紋理的格式和mipmap等可以不一樣;
3.減小了內存占用(內存中只有當前使用的texture)
缺點
因為要不斷地在內存中加載/卸載texture,所以增加了IO開銷
應用場景
- 地形紋理
WebGPU支持情況
有人提出了Investigation: Sparse Resources, 希望WebGPU增加操作堆heap的API。不過目前沒有回應。
我目前不清楚WebGPU是否能實現virtual texture
參考資料
approaching-zero-driver-overhead -> Sparse Texture
知乎->Virtual Texture Tools & Practices
關於對virtual texture的淺顯認識
Tessellation
根據Investigation: Tessellation:
Let's wait until after the release of a MVP
WebGPU應該會在MVP后考慮加入Tessellation shader
Mesh Shader
介紹
NVDIA在Turing架構中推出了新的管線,用來替代光柵化管線。新管線只保留了Pixel Shader(即fragment shader),新增了Task Shader和Mesh Shader,如下圖所示:
新管線更適合於GPU Driven Render Pipeline的理念,包括以下的特性:
類似於Compute管線(compute shader),具有強大的計算能力;
把Mesh分解為Meshlet(類似於GPU Driven Render Pipeline中提到的Cluster),更好地支持cluster cull。
WebGPU支持情況
根據Investigation: Tessellation中的討論,因為Vulkan和Metal還沒支持Mesh Shader,所以WebGPU至少要等它們支持后才會考慮支持。
參考資料
DX12支持了Mesh Shader
Introduction to Turing Mesh Shaders
怎么評價nvidia 推出mesh shader管線?