レイトレのおまけ

レイトレ合宿6に出したやつでレイトレと直接関係の無いSwiftの都合の部分などを少し書いておこうと思います。

レイトレ合宿6のホームページにはpngがありますが、とりあえず今回のレンダリング結果をjpgにしたものはこんな感じです。

f:id:c5h12:20180911022639j:plain

ちゃんとレンダリングが終わっています(笑

とてもノイジーなのは突貫工事で突っ込んだglTF用のマテリアルでグロッシー成分のインポータンスサンプリングをさぼっているせいでしょう。

手元のマシンで終わるくらいに設定しておけば、本番環境ではもっとサンプル数が増えるだろうからちょっとはマシになるんじゃないかと思ったのですがそんなうまい話はありませんでした。

Dispatch

さて、まずはDispatchについて。

前回、Dispatchのqueue.asyncで並列化したらスレッドが大量に立って大変なことになっていたと書きました。

そこで今回よく調べたところ

DispatchQueue.concurrentPerform(iterations: Int, execute work: (Int) -> Void)

というfor文感覚で使うためのものがあることが判明。 これを使うとちゃんとCPUのコア数だけのスレッドで処理してくれます。

つまりこれについては自分の使い方が悪かったという話です。

ただ終わるまでブロックされてしまうので、途中経過を出すにはasyncの中で使うような工夫が必要になります。

少し言い訳をすると、SwiftのDispatchはObjective-C時代から使われているAPIのラッパーです。 そこで対応するものを並べてみると

C Swift
dispatch_async queue_instance.async
dispatch_apply DispatchQueue.concurrentPerform

そこなんで全然違う名前に変えたの。

いやapplyじゃ意味不明じゃない、という気持ちは解らなくはない。 まあこういうフリーダムなところはアップル系の良いところでもありますが。

Array

SwiftのArrayは値型扱いさ。でもCopy on Writeという仕組みで変更されるまでは参照を共有してるだけだぜ。ちなみにこいつはスレッドセーフじゃない。

という仕様は有名です。 これについては慣れればそんなもんかという程度のことなのですが、Dispatchと組み合わせて使って気づいたことがありました。

スライドにもちらっと書いたことなのですが、Arrayの内容について処理するときにはいくつか書き方が考えられます。

// (1)
for (i, a) in array.enumerated() {
    ...
}

// (2)
for a in array {
    ...
}

// (3)
for i in 0..<array.count {
    let a = array[i]
    ...
}

多くは(2)かなという気はしますが、(1)と(2)のiteratorを生成する処理は排他制御されているのか、並列処理の中で使うとパフォーマンスが上がらないことがありました。

ことがありました、というのは大丈夫なこともあるようなんですよね…

最初に気づいたのは三葉レイのコードをSwiftに移植してみたときで、流石にこれは遅すぎないかと思ったのがきっかけ。

できるだけシンプルなものをGistにあげてみました。 手元での実行結果はこんな感じになります。

$ swiftc -O -o arraybench arraybench.swift 
$ ./arraybench
(省略)
initializing...
start serial with iterator loop
done:1.71359694004059[sec]
start concurrent with iterator loop
done:1.43492197990417[sec]
start serial with index loop
done:1.62267899513245[sec]
start concurrent with index loop
done:0.414744019508362[sec]

というわけで今回は並列処理内では基本的にインデックスでループさせるようにしました。

小手先の技

おまけの小ネタ。

今回は全ピクセルに1回はレイが飛ぶようにとピクセル当たりのサンプル数を少なくして、代わりに何回も全体をレンダリングする弱気な設定にしていました。 その上で、万が一全ピクセルに処理が行き渡らなかった場合に備えて導入したのがこれ。

f:id:c5h12:20180911022707g:plain

なんとタイル単位ではなく、ランダムなピクセルの位置を詰め込んだバケット単位でレンダリングしています。 1回もレンダリングされていないピクセルはわかりやすいように青くしていますが、これなら前回のようにタイルが埋まり切っていないという一発でわかる未完状態にはならないという酷い発想です。 そもそも画像テクスチャもないし、レイのコヒーレンシーとか気にするほど最適化されてないしという考えに基づいています(ォ

ついでにデノイザーと組み合わせれば確実と思いましたが結局デノイザーは未実装になったので片手落ちです。

ところがこれ、スレッド毎の負荷が平均化されたためかタイル単位でレンダリングするより少し速くなるという結果に。

やってみないとわからないものですね…