Wavefront Path Tracing
首先,老規矩:
未經允許禁止轉載(防止某些人亂轉,轉着轉着就到蠻牛之類的地方去了)
B站:Heskey0
注:本文需要CUDA和PBRT的知識,推薦書籍《CUDA C Programming》
pbrt第四版的書還沒出,很多哥哥姐姐萌 (*^▽^*) 看代碼的時候會很懵逼。因為中文網上還沒有相關的博客,為了幫助大家理解,我寫下這篇博客,幫助大家了解pbrt第四版的一個核心 —— wavefront
1. Introduction
我們可以通過CUDA,OpenCL這些接口實現在GPU上編程。在GPU上面編程和CPU有很大的不同,熟悉CUDA的都知道,在一個SM上同時可以並行32個線程
即使這樣,GPU編程也會由很多難題。在這個線程束中:
- 有一部分線程中途掛掉了(if-else)
- 高帶寬的CPU沒有辦法利用起來,GPU和CPU之間的內存拷貝會變得昂貴
- GPU的緩存賊小,kernel太大的話,很可能會沖爆緩存
為了盡可能避免這些難題,學術界的大佬萌就想出了一個辦法 -> wavefront path tracing,這個方法被集成到了pbrt-v4中。這個方法的核心就是——把以往寫得很大的kernel拆分成很多的小kernel。
2. 先分析一下老方法
老方法的kernel體量很大:
- 生成路徑
- 對光源采樣
- 不同材質的解決方案不同
在GPU的編程中,分支會導致線程資源不能夠被充分應用。老方法中分支主要會出現在兩個地方:
-
在采樣path的時候,path隨時會中斷。在一個thread終止之后,線程束不會終止,也就是說,一個path中斷之后其它的path還會跑,中斷的path依然會占用線程資源。
-
一個線程束中的path命中不同材質的時候,會導致線程束中每個thread的邏輯不同。
3. Wavefront
3.1 使用路徑池
wavefront path tracing方法中,維護了一個path池,這個池的大小為 \(1M(=2^{20})\) 個path,當path中斷的時候,需要重新生成path,這個時候就可以去池子里面取。path的狀態(path state)被存儲到global memory(DRAM)中,每個path包括shadow ray和extension ray,占用212 bytes。1M個path,每個path占用212 bytes,所以總共就占用了212 MB的內存。(雖然把path池的內存提上去能夠提升性能,但path池的內存太大的時候,性能的提升就不明顯了,所以1M個path已經夠用了)
3.2 拆分kernel
把kernel拆分到3個stages:
- logic stage
- material stage
- ray cast stage
3.2.1 Logic stage
logic stage只有一個kernel,這個kernel的任務就是推動path的前進:
- 計算light sample path和extension path的MIS權重
- 更新throughput
- 確定path是否終止了(Russian roulette“殺死”了ray,ray射出場景)
- 確定ray擊中了什么材質
- 發送一條到material stage的請求
3.2.2 Material stage
每一堆消耗差不多的材質對應一個material kernel。比如,消耗大的就放在一個kernel,消耗小的放到另一個kernel。老方法中,所有的材質代碼都在一個kernel里面,擊中了不同材質,就用一個switch-case做分支。
3.3.3 Ray cast stage
ray cast kernel的任務就是投射出extension ray和shadow ray。與此同時,為了加載logic stage的輸出結果,path state需要記錄ray buffer中的索引
3.3 內存
wavefront formulation最大的缺點就是 —— path state 需要存儲到內存中。那么就需要對memory layout做一些調整,使這個缺點變“弱”。對於GPU來說,使用SOA形式的memory layout會更加高效(使用SOA比AOS快了80%)。
3.4 Queue
通過為材質和光線投射階段生成緊湊的請求Queue,確保每個啟動的kernel總是能執行線程束中的所有線程。 這里的Queue是簡單的預先分配的全局內存緩沖區,因此它們可以包含池中每個路徑的索引。 每個隊列在全局內存中都有一個item計數器,該計數器在寫入隊列時自動增加。 通過將項目計數器設置為零來清除隊列。