
-
Metal 4ゲームの知識を深める
Metal 4の最新機能について学びましょう。新しいレイトレーシング機能は、極めて複雑で視覚的にリッチなワークロードをAppleシリコンに組み込む際に役立ちます。MetalFXを使用して、レンダリングのアップスケール、フレームの補間、シーンのノイズ除去を実行し、ワークロードをスケーリングする方法につい説明します。 このセッションの内容を十分理解できるよう、最初に「Discover Metal 4」および「Explore Metal 4 games」をご覧になることをおすすめします。
関連する章
- 0:00 - イントロダクション
- 2:13 - レンダリングのアップスケール
- 7:17 - フレーム補間
- 13:50 - Metal 4によるレイトレーシング
- 19:25 - アップスケールと同時にノイズ除去
- 26:08 - 次のステップ
リソース
関連ビデオ
WWDC25
WWDC23
WWDC22
-
このビデオを検索
こんにちは Matias Koskelaです 今日は Appleプラットフォームでの 高度なゲームやプロ向けアプリの 開発をさらに前進させるうえで 役立つ手法と ベストプラクティスを紹介します
このビデオの前に Metal 4の概要を説明する 「Metal 4の概要」と 最新版のMetalの使い方を紹介する 「Metal 4ゲームの詳細」を ご覧になることをお勧めします この「Metal 4ゲームの知識を深める」は Metal 4ゲームシリーズの第2部です また Metal 4による機械学習と グラフィックスの統合について 説明しているビデオもあります
ご覧の「サイバーパンク2077」などの ゲームでは レンダリング品質の向上により リアルさが高まっています これによりピクセルあたりの 処理負荷が高まり 高解像度 高フレームレートの 難易度が上がっています Metalを使えば iPhoneからMacまで 幅広いAppleプラットフォーム向けに 高品質なフレームのレンダリングができます ラスタ化やレイトレーシングなどの 手法を使用する場合 Metalには使いやすいAPIがあります
MetalFX Upscalingを使用すれば 高解像度 高フレームレートでも ワークロードのスケーリングができます
さらに上を目指すなら 新しいMetalFXフレームインターポレータを 利用できます
サイバーパンク2077などの最新ゲームは リアルタイムのパストレーシングで 現実感を出しています このようなリアルタイムの レンダリングの機能強化は Metal 4の優れた新機能によって 実現可能になっています レイトレーシングの機能強化や新しい MetalFXのノイズ除去アップスケーラがあり ゲーム内で必要なレイの数を減らして スケーリングを容易にします
MetalFXのアップスケーラは高解像度 高フレームレートの実現に役立ちます 新しいMetalFXのフレームインターポレータで よりスムーズなゲームプレイを実現できます 新しいMetal 4のレイトレーシング機能で パフォーマンスがさらに向上し MetalFXのノイズ除去アップスケーラと 組み合わせて使用できます
アップスケーリングは広く使用されており 多くのシナリオで パフォーマンス向上に役立ちます MetalFXには 機械学習ベースの アップスケーラがあり 2022年からAppleプラットフォームの 一部となっていますが 毎年改善が重ねられています
MetalFX Upscalingに 新しいツールと手法が追加されており それらを使用すると ゲームの品質と パフォーマンスの向上に役立ちます 最初のステップは時間アップスケーリングを 適切にゲームに適用することです 露出の入力パラメータの適切な使用も そのプロセスの一部です 動的解像度により パフォーマンスを さらに向上させることができます リアクティビティヒントを使用して 特定の シナリオの品質を向上させることもできます
一般的なレンダリングパイプラインを考えます 最初にフレームのラスタ化または レイトレーシングが行われ
その後 モーションブラーなどの 効果の後処理が実行されます 次に 露出とトーンマッピングが適用され UIがレンダリングされ 最終的に フレームが プレイヤーに表示されます
MetalFX Upscalingを追加するには ジッタ処理済みのレンダリングの後 後処理の前が最適です アップスケーラの組み込みの詳細は 「MetalFX Upscalingで パフォーマンスを向上させる」を ご覧ください 今年は ゲームの パフォーマンス向上に役立つ ツールや機能がさらに追加されます
高品質を実現するには アップスケーラに 適正な露出値を設定することが重要です
大きく誤った値を渡すと ちらつきや ゴーストが生じる可能性があります
レンダリングパイプラインでは アップスケーラの入力色と出力色は リニア色空間にあります アップスケーラは 露出と呼ばれる パラメータを取ります 露出に色入力を乗算することで トーンマッピングにおいて 使用される露出におおよそ適した 輝度が得られます
これは プレイヤーに表示される際の フレームの目に見える特徴を アップスケーラが理解するのに役立ちます この値は アップスケーラにとっての ヒントに過ぎず これによって出力の輝度は変更されません MetalFXには アップスケーラに送信する 露出の入力値の調整に役立つ 新しいツールが含まれています
これは露出デバッガと呼ばれます 有効にするには 環境変数 MTLFX_EXPOSURE_TOOL_ENABLEDを設定します これでアップスケーラによりフレーム上に 灰色のチェッカー盤がレンダリングされ 露出値の逆数が適用されます
ディスプレイ上で パイプラインの 最終段階でこのパターンが どう見えるかを確認できます
アップスケーラに渡す露出値が トーンマッパーと合っていない場合 チェッカー盤の表示が 暗すぎたり明るすぎたりします
ゲームの実行中にチェッカー盤の 輝度が変わる場合も 不整合があることの表れです
露出値が適正であれば 格子のパターンは 均一なミッドグレーになります
ゲームの複雑さがシーンによって 大きく変化する場合があるため 多くのゲームで動的解像度レンダリングが 採用されています
フレームが複雑になるほど アップスケーラの入力解像度が下がります さらに課題が生じた場合は ゲームにおいて動的に さらなる入力解像度の引き下げが行われます MetalFXの時間アップスケーラで 動的サイズの入力がサポートされ すべてのフレームで同じサイズの入力を 渡す必要がなくなりました スケーリングの品質を最大限に高めるには 必要でない限り 最大スケールを 2倍より大きな設定にしないようにします
MetalFXの時間アップスケーラの もう1つの新機能は ピクセルのリアクティビティに関する ヒントをアップスケーラに提供する オプションの機能です
ゲームで透過エフェクトまたは 花火のようなパーティクルを レンダリングする場合 モーションテクスチャや深度テクスチャには レンダリングされません
スケーリング比が高く 入力解像度が低いと パーティクルが背景に溶け込んで見えたり
ゴーストが見えたりすることがあります これは レンダリングにおいて パーティクルがテクスチャのディテールや 鏡面ハイライトのように 表示されることがあるためです
デベロッパがパーティクルの処理を コントロールできるように アップスケーラがリアクティブマスクという 新しいオプションの入力を 受け付けるようになります このマスクでは エフェクトの 対象領域をマーキングできます
使用するには シェーダでリアクティブマスクの値を設定します 例えば G-Bufferのマテリアルタイプに 基づく値を使用します ホストコードで エンコーディングの前に 時間アップスケーラオブジェクトに テクスチャをバインドします
リアクティブマスクを使用するのは 入力解像度を高くする方法を 取れない場合のみにしてください また 別のアップスケーラ用に調整された リアクティブマスク使用しないでください MetalFXのアップスケーラ出力で適切に 表示される領域がマスクされる 可能性があるためです アップスケーラを使用すると 高品質で 優れたパフォーマンスが得られます しかし より高いリフレッシュレートが 必要な場合があります 今年 MetalFXに 全Appleプラットフォーム 対応のフレーム補間が導入されます
MetalFXのフレーム補間をゲームに 組み込むのはとても簡単です まず 補間オブジェクトを設定し 補間されたフレームにUIをレンダリングし フレームを適切に表示し ペースを調整します
フレーム補間は レンダリング済みの ピクセルを利用するのに役立ち スムーズなゲーム体験を実現できます
これは同じレンダリングパイプラインですが 今回はUIレンダリングがありません
トーンマッピングの後で フレームを補間します 解像度とフレームレートを高くする場合は 同じパイプラインでアップスケーリングと 補間処理の両方を行うことができます
MetalFXのフレーム補間を使用する場合 モーションベクトルと深度の 2つのフレームがレンダリングされます アップスケーラを採用している場合 同じモーションベクトルと深度を 使用できます モーションテクスチャのオブジェクトは 色がありますが 右に移動しているためです これらの入力により MetalFXで これら2つのレンダリングされた フレームの間にフレームが生成されます
補間の設定をして 総合的なパフォーマンスを向上させるには アップスケールオブジェクトを 補間記述子に渡します インターポレータを作成する際 モーションスケールと深度の規則を 定義します 次に 必要な5つのテクスチャすべてを インターポレータにバインドします
補間されたフレームの取得を開始したら UIのレンダリングについて考えます
通常のレンダリングパイプラインでは 各フレームの最後にUIがレンダリングされ その位置はフレーム補間が行われるのと ほぼ同じ位置です
UIレンダリングで要素がフレームに アルファブレンドされます 各フレームでテキストの変化があっても モーションテクスチャや 深度テクスチャは変更されません
フレーム補間を有効にして 見栄えの良いUIを実現するには いくつかの方法があります
フレーム補間を使用して UIをレンダリングするために よく使われる手法は3つです 合成UIとオフスクリーンUI フレームごとのUIです
合成UIでは インターポレータは 前のフレームN - 1に加え UIなしの現在のフレームNと UIありの同じフレームNを取得します 合成UIは 最も簡単に導入できます このモードでは フレームインターポレータで UIありと UIなしのテクスチャの差分がわかります このように 補間後のフレームでの UIの削除と適切な位置への配置を 試行することができます ただし すでにブレンドされたピクセルの ブレンド解除を完璧に行うことはできません そこで そのためには 他のいずれかの 方法を使用します
例えば オフスクリーンUIでは UIが完全に独立したUIテクスチャに レンダリングされます
インターポレータはそれを 補間後のフレームに追加します これをインターポレータの入力にすると 読み込みと保存の負担から解放されます インターポレータがUIを出力に 書き込むことができるためです
最後に フレームごとのUIでは UI処理はデベロッパのコードに依存し 必要なコードの変更量が 最も大きくなる可能性があります ただし この場合 補間後のフレームの UIを更新することもでき プレイヤーにとって最も スムーズな体験が得られます
これで 補間後のフレームでも 見栄えの良いUIを表示できます ここで 補間されたフレームと ネイティブにレンダリングされたフレームの 両方を正しい順序 正しい間隔で表示する方法を 考える必要があります
通常 ゲームのレンダリングは RenderスレッドとGPU Presentスレッドで構成されます Renderスレッドで GPUと Presentationに必要な処理の 準備を行います フレームがレンダリングされる際 インターポレータが レンダリングされた フレームと前のフレームの間に タイムスタンプ付きのフレームを 生成できます これにより ゲームで 補間されたフレームを表示できます 表示間隔の経過後 新しくレンダリングされた フレームを表示できます
この間隔の長さを一貫した形で 決定するのは難しい場合があります しかし ゲームのペーシングを適切に 行うためには必要なことです
新しいMetal HUDは ペーシングがずれている タイミングの特定に役立つ優れたツールです 有効化の方法の詳細については「ゲームを レベルアップさせる方法」をご覧ください このツールの優れた新機能についても 説明しています
フレーム間隔のグラフをご覧ください 横軸は時間で 縦軸はフレーム間隔の長さです
グラフが不規則なパターンを示していて フレームの更新間隔が長いことを示す スパイクがランダムに見える場合 ペーシングがずれています
もう1つ ペーシングが ずれていることがわかるのは フレーム間隔のヒストグラムバケットが 2つ以上ある場合です
ペーシングが修正されると ターゲットディスプレイの リフレッシュレートに合っていれば 平坦な線が表示されます 下回っていれば 一定のパターンの繰り返しになり ヒストグラムバケットが最大2つ存在します
その処理を正しく行う方法の例がこちらで 便利なpresentHelperクラスを使用します 描画のループでは すべてが 低解像度のテクスチャにレンダリングされ MetalFXアップスケーラで アップスケールされます UIのレンダリング開始をヘルパーに 指示した後 UIがレンダリングされます 最後に インターポレータの呼び出しが presentHelperクラスで処理されます 実装の詳細については サンプルコードを確認してください
ペーシングに加えて デルタタイムとカメラのパラメータを 適切に設定することも重要です 全パラメータが適正でないとオクルージョン 領域にアーティファクトが生じかねません
適正なパラメータを使用すると オクルージョン領域が正しく処理されます
これは インターポレータで 実際の シミュレーションのモーションの長さに 合わせて モーションベクトルを 調整できるようになったためです
すべての入力とペーシングが適切になると 補間されたフレームが適切に表示されます また 補間の入力は適正な高さの フレームレートにする必要があります 補間前に 少なくとも30フレーム/秒と なるようにします
アップスケーラとフレームインターポレータは ほぼどのようなレンダリングスタイルの スケーリングにも汎用的に使える手法です これに対して レイトレーシングは通常 ハイエンドの レンダリングシナリオで使用されます Metal 4では新しいレイトレーシング機能が 多数追加されており アクセラレーション構造のビルドや 交差関数に関連するものがあります
Metalのレイトレーシングを使った Apple プラットフォーム向けゲームが増えています
このデモでは リアルなライティングで ドローンが床面に反射する様子が見えます レイトレーシングの手法や 複雑さはゲームごとに異なります
そのため 交差関数の管理の柔軟性を高め アクセラレーション構造のビルドの オプションを増やす必要があります
Metal 4では この2つを効率化するための 新機能が導入されています
Metalのレイトレーシングの基本事項 アクセラレーション構造のビルドや 交差関数の詳細については 「Metalレイトレーシングのガイド」 をご覧ください
1本の木の周りに草が生えている単純な シーンのレイトレーシングを考えます
このようにシンプルなシーンでも 複数種類のマテリアルがあります アルファテストされた木の葉や 不透明な木の幹などです
そのため さまざまなレイトレーシングの 交差関数が必要になります 主光線用とシャドウレイ用で別々です
交差関数バッファは引数バッファであり シーンの交差関数へのハンドルが 格納されます
例えば 主光線をトレーシングするために 草と木の葉で同様の機能が 必要な場合があります 交差関数バッファを使用することにより 同じ交差関数を指す複数のエントリを 簡単に持つことができます
交差関数バッファのインデックスを 設定するために必要なことは 1つはインスタンスレベルでの状態の設定で この例のシーンには2つの インスタンスがあります もう1つはジオメトリレベルで この場合 草には1つだけ 木には2つのジオメトリがあります インターセクタでは 木の幹に当たる シャドウレイにはどの交差関数を 使用するかの情報が必要です
インスタンスのアクセラレーション構造を 作成する際 各インスタンス記述子で intersectionFunctionTableOffsetを 指定します
プリミティブアクセラレーション構造を ビルドする際も ジオメトリ記述子に intersectionFunctionTableOffsetを 設定します
シェーダでインターセクタを設定する際には "intersection_function_buffer"を タグに追加します
次に ジオメトリの乗数を インターセクタに設定します 乗数は 交差関数バッファ内の 光線の種類の数です
この例では ジオメトリごとに 2種類の光線があります したがって ここでの正しい値は2です その2種類の光線のうち トレーシングする光線の種類に対応する ベースインデックスの指定が必要です この例で 主光線をトレーシングするための ベースインデックスは0になります
シャドウをトレーシングする場合の ベースIDは1です
木の幹のインスタンスとジオメトリの影響 ジオメトリの乗数およびシャドウレイの ベースIDを組み合わせると 目的の交差関数を指すポインタが得られます
intersectメソッドに 交差関数バッファの引数を渡して コードを完成させます
バッファ サイズおよびストライドを 指定することにより 従来の他のAPIの場合と比較して 柔軟性が高くなります DirectXから移植する場合は シェーダバインディングテーブルを Metalの交差関数バッファに 簡単に移植できます
DirectXでは 光線を送出する記述子を 作成する際に 交差関数バッファのアドレスと ストライドをホストに設定します Metalではこれをシェーダで設定します SIMDグループのすべてのスレッドで 同じ値を設定する必要があり そうしないと 動作が未定義になります
光線タイプのインデックスとジオメトリの乗数は DirectXとMetalで 同じように扱われます アプリではシェーダでこれらを設定できます DirectXとMetalでは インスタンスの アクセラレーション構造の作成時に インスタンスごとのインスタンス オフセットインデックスを設定します ただしジオメトリオフセットインデックスは DirectXでは自動的に生成されますが Metalでは ジオメトリオフセットを デベロッパが柔軟に設定できます
レイトレーシングが用いられた ゲームのMetalへの移植が 交差関数バッファにより 大幅に改善されます デベロッパの準備が整えば Metal 4では アクセラレーション構造のビルド方法を 最適化することもできます
Metalではすでに アクセラレーション構造のビルドに関して さまざまな制御ができました デフォルトの動作に加え リフィットのための最適化ができ 大規模なシーンを実現したり アクセラレーション構造を 迅速にビルドしたりできます 今年はさらに柔軟性が高まり 高速交差を選択して レイトレーシングに かかる時間を短縮できます
また アクセラレーション構造の メモリ使用量を最小限に抑える 選択をすることもできます
使用のフラグはアクセラレーション構造の ビルドごとに設定でき すべてのアクセラレーション構造で 同じにする必要はありません
新しいアクセラレーション構造の フラグにより レイトレーシングを レンダリングパイプラインに含められ よりニーズに合わせやすくなります 確率的影響に対して使用する場合は デノイザが必要です そして MetalFXアップスケーラに ノイズ除去を含められるようになりました
単純なハイブリッドのレイトレーシングから 複雑なパストレーシングまで リアルタイムレイトレーシングが常に 使用されることが増えています この画像の例では レイトレーシングによって すべてが より地について見え
反射が大幅に改善しています レイトレーシングで品質とパフォーマンスの 最適なトレードオフを実現するには ノイズ除去を使用する光線を少なくします
新しいMetalFX APIを使用すれば アップスケーリングとノイズ除去を 組み合わせるには いくつかの入力を 追加するだけで簡単です ただし ノイズ除去アップスケーラを より強化し 入力を追加し 詳細情報を正しく処理することにより さらに品質を向上させることができます
アップスケーラとデノイザを 組み合わせる前に 従来はどのような手順で 行われていたかを確認しましょう
通常のリアルタイムのインタラクティブな レイトレーシングレンダリングパイプラインでは 複数のエフェクトを別々にトレーシングし 別々にノイズ除去を行い 結果を1つのノイズのないジッタ処理済み テクスチャにまとめます それをMetalFXの時間アップスケーラで アップスケールし さらに後処理を行います
従来のデノイザでは シーンごとに 個別のパラメータ調整が必要でした こちらは パラメータの調整を 行わない場合のノイズ除去の一例です これに対して MetalFXのノイズ除去アップスケーラでは パラメータを調整する必要はありません これは メインのレンダリングの後 後処理の直前に適用されます MetalFXの機械学習を活用した手法により 幅広いシナリオで 堅牢性とパフォーマンスに優れた 高品質のノイズ除去と アップスケーリングが得られます 組み込むのも簡単です ノイズ除去アップスケーラを組み込むうえで アップスケーラの組み込みは よい出発点となります ここでは アップスケーラへの 入力を確認します 色 モーション 深度です 新しい結合APIは アップスケーラAPIのスーパーセットです
新しいAPIでは ノイズのない 補助バッファを追加する必要があります これが左に表示されています ほとんどが すでにアプリで 使用されている可能性があります それぞれについて 詳しく見ていきましょう
新しい入力の1つ目は法線です 最適な結果を得るには これらが ワールド空間内にある必要があります
次は ディフューズアルベドです これは マテリアルの拡散放射輝度の ベースカラーです
次の粗さは 表面がどれだけ滑らかか または粗いかを表し リニア値になっています 最後の入力は鏡面アルベドです レンダリングの鏡面反射輝度の ノイズのない近似値になります フレネル成分を含んでいる必要があります コードで これらの新しい入力を 追加するのは簡単です
一般的な時間アップスケーラを 作成するのに必要なコードは10行程度です ノイズ除去バージョンを有効にするには スケーラの種類を変更し 付加的なテクスチャの種類を 追加する必要があります
同様に スケーラをエンコードする場合 これはアップスケーラ呼び出しになります ここで唯一の違いは追加の入力テクスチャを バインドする必要があることです
デノイザの基本的な使い方を設定した後 さらに改善するには いくつかの オプションの入力を使用し 組み込みに関する一般的な 落とし穴を回避します
品質向上に利用できるオプションの 入力テクスチャがいくつかあります
1つ目は鏡面反射到達距離です これは ピクセルの最初の可視点から 次に跳ね返る位置までの 光線の長さです 次に ノイズ除去強度マスクです これを使用することで ノイズ除去が 不要な領域のマーキングができます 最後は 透明オーバーレイで アルファチャネルをもとに色をブレンドする ために使用します アップスケールのみでノイズ除去されません
組み込みで最も一般的な問題は 入力のノイズの多さです これを修正するには 標準的なパストレーシングのサンプリングに 対する改善をすべて行う必要があります NEE(Next-Event-Estimation)や 重点サンプリング法などです また 光源の多い大規模なシーンでは 実際に対象領域に影響する光源の サンプリングを主に行うようにします
レイトレーシングのサンプリング品質に 関連するもう1つの課題が 相関を持つ乱数です 相関が強すぎる乱数ジェネレータを 使用すべきではありません 空間相関と時間相関のいずれも アーティファクトにつながりかねません
補助データに関連する落とし穴の1つが 金属物質のディフューズアルベドに 関するものです この例では チェスの駒が金属製であるため 鏡面アルベドで色が表現されています この場合 チェスの駒にディフューズ アルベドを使用すると暗くなります
最後に 法線関連のよくある 落とし穴があります MetalFXのノイズ除去アップスケーラで ノイズ除去判定を適切に行うには 法線がワールド空間にあることが前提です テクスチャのデータ型は符号ビットを 持つものを使用する必要があります そうでないと カメラの向きによっては 最適な品質が得られない可能性あります
これらの項目すべてに対応することで 適切にノイズ除去され アップスケールされたフレームが得られます
1つのレンダラにこれらの機能を すべて搭載したら どうなるか見てみましょう
同僚がまとめてくれたデモで 先ほどお話したレンダリング パイプラインを使ったものがあります このデモでは新しいMetal 4の レイトレーシング機能を使って レンダリングのレイトレーシング部分を 最適化しています MetalFXのノイズ除去アップスケーラで ノイズ除去とアップスケーリングを 同時に行います 露出とトーンマッピングの後 MetalFXフレームインターポレータで フレームを補間します
このデモでは グローバルイルミネーション 反射 シャドウ アンビエントオクルージョンなどの 高度なレイトレーシングの 照明効果を使用して 2台のロボットがチェスをするシーンを 生き生きと描き出しています
右上のビューは MetalFXの処理を行う前のレンダリングです 他のビューでは 他のMetalFXの入力が 使用されています
MetalFXのノイズ除去アップスケーラと フレームインターポレータの 両方を採用しています デノイザを使用すると 最終的な見た目を すべて手動で調整する手間がなくなり レンダリングが大幅に楽になります
すでにMetalFXアップスケーラを 組み込んだことがある場合は フレーム補間へとアップグレードする よいチャンスです MetalFXが初めての場合は まずアップスケーラから確認してください 次に レイトレーシングのエフェクトに 今日説明した交差関数バッファなどの ベストプラクティスが用いられて いることを確認します そして ノイズ除去アップスケーラで ゲームの光線量を減らします
皆さんのゲームで新機能を実際に ご活用いただき Metal 4を使って 何を作られるのか楽しみにしています ご視聴ありがとうございました
-
-
6:46 - Reactive Mask
// Create reactive mask setup in shader out.reactivity = m_material_id == eRain ? (m_material_id == eSpark ? 1.0f : 0.0f) : 0.8f; // Set reactive mask before encoding upscaler on host temporalUpscaler.reactiveMask = reactiveMaskTexture;
-
8:35 - MetalFX Frame Interpolator
// Create and configure the interpolator descriptor MTLFXFrameInterpolatorDescriptor* desc = [MTLFXFrameInterpolatorDescriptor new]; desc.scaler = temporalScaler; // ... // Create the effect and configure your effect id<MTLFXFrameInterpolator> interpolator = [desc newFrameInterpolatorWithDevice:device]; interpolator.motionVectorScaleX = mvecScaleX; interpolator.motionVectorScaleY = mvecScaleY; interpolator.depthReversed = YES; // Set input textures interpolator.colorTexture = colorTexture; interpolator.prevColorTexture = prevColorTexture; interpolator.depthTexture = depthTexture; interpolator.motionTexture = motionTexture; interpolator.outputTexture = outputTexture;
-
12:45 - Interpolator present helper class
#include <thread> #include <mutex> #include <sys/event.h> #include <mach/mach_time.h> class PresentThread { int m_timerQueue; std::thread m_encodingThread, m_pacingThread; std::mutex m_mutex; std::condition_variable m_scheduleCV, m_threadCV, m_pacingCV; float m_minDuration; uint32_t m_width, m_height; MTLPixelFormat m_pixelFormat; const static uint32_t kNumBuffers = 3; uint32_t m_bufferIndex, m_inputIndex; bool m_renderingUI, m_presentsPending; CAMetalLayer *m_metalLayer; id<MTLCommandQueue> m_presentQueue; id<MTLEvent> m_event; id<MTLSharedEvent> m_paceEvent, m_paceEvent2; uint64_t m_eventValue; uint32_t m_paceCount; int32_t m_numQueued, m_framesInFlight; id<MTLTexture> m_backBuffers[kNumBuffers]; id<MTLTexture> m_interpolationOutputs[kNumBuffers]; id<MTLTexture> m_interpolationInputs[2]; id<MTLRenderPipelineState> m_copyPipeline; std::function<void(id<MTLRenderCommandEncoder>)> m_uiCallback = nullptr; void PresentThreadFunction(); void PacingThreadFunction(); void CopyTexture(id<MTLCommandBuffer> commandBuffer, id<MTLTexture> dest, id<MTLTexture> src, NSString *label); public: PresentThread(float minDuration, CAMetalLayer *metalLayer); ~PresentThread() { std::unique_lock<std::mutex> lock(m_mutex); m_numQueued = -1; m_threadCV.notify_one(); m_encodingThread.join(); } void StartFrame(id<MTLCommandBuffer> commandBuffer) { [commandBuffer encodeWaitForEvent:m_event value:m_eventValue++]; } void StartUI(id<MTLCommandBuffer> commandBuffer) { assert(m_uiCallback == nullptr); if(!m_renderingUI) { CopyTexture(commandBuffer, m_interpolationInputs[m_inputIndex], m_backBuffers[m_bufferIndex], @"Copy HUDLESS"); m_renderingUI = true; } } void Present(id<MTLFXFrameInterpolator> frameInterpolator, id<MTLCommandQueue> queue); id<MTLTexture> GetBackBuffer() { return m_backBuffers[m_bufferIndex]; } void Resize(uint32_t width, uint32_t height, MTLPixelFormat pixelFormat); void DrainPendingPresents() { std::unique_lock<std::mutex> lock(m_mutex); while(m_presentsPending) m_scheduleCV.wait(lock); } bool UICallbackEnabled() const { return m_uiCallback != nullptr; } void SetUICallback(std::function<void(id<MTLRenderCommandEncoder>)> callback) { m_uiCallback = callback; } }; PresentThread::PresentThread(float minDuration, CAMetalLayer *metalLayer) : m_encodingThread(&PresentThread::PresentThreadFunction, this) , m_pacingThread(&PresentThread::PacingThreadFunction, this) , m_minDuration(minDuration) , m_numQueued(0) , m_metalLayer(metalLayer) , m_inputIndex(0u) , m_bufferIndex(0u) , m_renderingUI(false) , m_presentsPending(false) , m_framesInFlight(0) , m_paceCount(0) , m_eventValue(0) { id<MTLDevice> device = metalLayer.device; m_presentQueue = [device newCommandQueue]; m_presentQueue.label = @"presentQ"; m_timerQueue = kqueue(); metalLayer.maximumDrawableCount = 3; Resize(metalLayer.drawableSize.width, metalLayer.drawableSize.height, metalLayer.pixelFormat); m_event = [device newEvent]; m_paceEvent = [device newSharedEvent]; m_paceEvent2 = [device newSharedEvent]; } void PresentThread::Present(id<MTLFXFrameInterpolator> frameInterpolator, id<MTLCommandQueue> queue) { id<MTLCommandBuffer> commandBuffer = [queue commandBuffer]; if(m_renderingUI) { frameInterpolator.colorTexture = m_interpolationInputs[m_inputIndex]; frameInterpolator.prevColorTexture = m_interpolationInputs[m_inputIndex^1]; frameInterpolator.uiTexture = m_backBuffers[m_bufferIndex]; } else { frameInterpolator.colorTexture = m_backBuffers[m_bufferIndex]; frameInterpolator.prevColorTexture = m_backBuffers[(m_bufferIndex + kNumBuffers - 1) % kNumBuffers]; frameInterpolator.uiTexture = nullptr; } frameInterpolator.outputTexture = m_interpolationOutputs[m_bufferIndex]; [frameInterpolator encodeToCommandBuffer:commandBuffer]; [commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> _Nonnull) { std::unique_lock<std::mutex> lock(m_mutex); m_framesInFlight--; m_scheduleCV.notify_one(); m_paceCount++; m_pacingCV.notify_one(); }]; [commandBuffer encodeSignalEvent:m_event value:m_eventValue++]; [commandBuffer commit]; std::unique_lock<std::mutex> lock(m_mutex); m_framesInFlight++; m_numQueued++; m_presentsPending = true; m_threadCV.notify_one(); while((m_framesInFlight >= 2) || (m_numQueued >= 2)) m_scheduleCV.wait(lock); m_bufferIndex = (m_bufferIndex + 1) % kNumBuffers; m_inputIndex = m_inputIndex^1u; m_renderingUI = false; } void PresentThread::CopyTexture(id<MTLCommandBuffer> commandBuffer, id<MTLTexture> dest, id<MTLTexture> src, NSString *label) { MTLRenderPassDescriptor *desc = [MTLRenderPassDescriptor new]; desc.colorAttachments[0].texture = dest; desc.colorAttachments[0].loadAction = MTLLoadActionDontCare; desc.colorAttachments[0].storeAction = MTLStoreActionStore; id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:desc]; [renderEncoder setFragmentTexture:src atIndex:0]; [renderEncoder setRenderPipelineState:m_copyPipeline]; [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3]; if(m_uiCallback) m_uiCallback(renderEncoder); renderEncoder.label = label; [renderEncoder endEncoding]; } void PresentThread::PacingThreadFunction() { NSThread *thread = [NSThread currentThread]; [thread setName:@"PacingThread"]; [thread setQualityOfService:NSQualityOfServiceUserInteractive]; [thread setThreadPriority:1.f]; mach_timebase_info_data_t info; mach_timebase_info(&info); // maximum delta (0.1ms) in machtime units const uint64_t maxDeltaInNanoSecs = 100000000; const uint64_t maxDelta = maxDeltaInNanoSecs * info.denom / info.numer; uint64_t time = mach_absolute_time(); uint64_t paceEventValue = 0; for(;;) { std::unique_lock<std::mutex> lock(m_mutex); while(m_paceCount == 0) m_pacingCV.wait(lock); m_paceCount--; lock.unlock(); // we get signal... const uint64_t prevTime = time; time = mach_absolute_time(); m_paceEvent.signaledValue = ++paceEventValue; const uint64_t delta = std::min(time - prevTime, maxDelta); const uint64_t timeStamp = time + ((delta*31)>>6); struct kevent64_s timerEvent, eventOut; struct timespec timeout; timeout.tv_nsec = maxDeltaInNanoSecs; timeout.tv_sec = 0; EV_SET64(&timerEvent, 0, EVFILT_TIMER, EV_ADD | EV_ONESHOT | EV_ENABLE, NOTE_CRITICAL | NOTE_LEEWAY | NOTE_MACHTIME | NOTE_ABSOLUTE, timeStamp, 0, 0, 0); kevent64(m_timerQueue, &timerEvent, 1, &eventOut, 1, 0, &timeout); // main screen turn on... m_paceEvent2.signaledValue = ++paceEventValue; } } void PresentThread::PresentThreadFunction() { NSThread *thread = [NSThread currentThread]; [thread setName:@"PresentThread"]; [thread setQualityOfService:NSQualityOfServiceUserInteractive]; [thread setThreadPriority:1.f]; uint64_t eventValue = 0; uint32_t bufferIndex = 0; uint64_t paceEventValue = 0; for(;;) { std::unique_lock<std::mutex> lock(m_mutex); if(m_numQueued == 0) { m_presentsPending = false; m_scheduleCV.notify_one(); } while(m_numQueued == 0) m_threadCV.wait(lock); if(m_numQueued < 0) break; lock.unlock(); @autoreleasepool { id<CAMetalDrawable> drawable = [m_metalLayer nextDrawable]; lock.lock(); m_numQueued--; m_scheduleCV.notify_one(); lock.unlock(); id<MTLCommandBuffer> commandBuffer = [m_presentQueue commandBuffer]; [commandBuffer encodeWaitForEvent:m_event value:++eventValue]; CopyTexture(commandBuffer, drawable.texture, m_interpolationOutputs[bufferIndex], @"Copy Interpolated"); [commandBuffer encodeSignalEvent:m_event value:++eventValue]; [commandBuffer encodeWaitForEvent:m_paceEvent value:++paceEventValue]; if(m_minDuration > 0.f) [commandBuffer presentDrawable:drawable afterMinimumDuration:m_minDuration]; else [commandBuffer presentDrawable:drawable]; [commandBuffer commit]; } @autoreleasepool { id<MTLCommandBuffer> commandBuffer = [m_presentQueue commandBuffer]; id<CAMetalDrawable> drawable = [m_metalLayer nextDrawable]; CopyTexture(commandBuffer, drawable.texture, m_backBuffers[bufferIndex], @"Copy Rendered"); [commandBuffer encodeWaitForEvent:m_paceEvent2 value:++paceEventValue]; if(m_minDuration > 0.f) [commandBuffer presentDrawable:drawable afterMinimumDuration:m_minDuration]; else [commandBuffer presentDrawable:drawable]; [commandBuffer commit]; } bufferIndex = (bufferIndex + 1) % kNumBuffers; } } void PresentThread::Resize(uint32_t width, uint32_t height, MTLPixelFormat pixelFormat) { if((m_width != width) || (m_height != height) || (m_pixelFormat != pixelFormat)) { id<MTLDevice> device = m_metalLayer.device; if(m_pixelFormat != pixelFormat) { id<MTLLibrary> lib = [device newDefaultLibrary]; MTLRenderPipelineDescriptor *pipelineDesc = [MTLRenderPipelineDescriptor new]; pipelineDesc.vertexFunction = [lib newFunctionWithName:@"FSQ_VS_V4T2"]; pipelineDesc.fragmentFunction = [lib newFunctionWithName:@"FSQ_simpleCopy"]; pipelineDesc.colorAttachments[0].pixelFormat = pixelFormat; m_copyPipeline = [device newRenderPipelineStateWithDescriptor:pipelineDesc error:nil]; m_pixelFormat = pixelFormat; } DrainPendingPresents(); m_width = width; m_height = height; MTLTextureDescriptor *texDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:pixelFormat width:width height:height mipmapped:NO]; texDesc.storageMode = MTLStorageModePrivate; for(uint32_t i = 0; i < kNumBuffers; i++) { texDesc.usage = MTLTextureUsageShaderRead|MTLTextureUsageShaderWrite|MTLTextureUsageRenderTarget; m_backBuffers[i] = [device newTextureWithDescriptor:texDesc]; texDesc.usage = MTLTextureUsageShaderRead|MTLTextureUsageRenderTarget; m_interpolationOutputs[i] = [device newTextureWithDescriptor:texDesc]; } texDesc.usage = MTLTextureUsageShaderRead|MTLTextureUsageRenderTarget; m_interpolationInputs[0] = [device newTextureWithDescriptor:texDesc]; m_interpolationInputs[1] = [device newTextureWithDescriptor:texDesc]; } }
-
13:00 - Set intersection function table offset
// Set intersection function table offset on host-side geometry descriptors NSMutableArray<MTLAccelerationStructureGeometryDescriptor *> *geomDescs ...; for (auto g = 0; g < geomList.size(); ++g) { MTLAccelerationStructureGeometryDescriptor *descriptor = ...; descriptor.intersectionFunctionTableOffset = g; ... [geomDescs addObject:descriptor]; }
-
13:01 - Set up the intersector
// Set up the intersector metal::raytracing::intersector<intersection_function_buffer, instancing, triangle> trace; trace.set_geometry_multiplier(2); // Number of ray types, defaults to 1 trace.set_base_id(1); // Set ray type index, defaults to 0
-
13:02 - Ray trace intersection function buffers
// Ray trace intersection function buffers // Set up intersection function buffer arguments intersection_function_buffer_arguments ifb_arguments; ifb_arguments.intersection_function_buffer = raytracingResources.ifbBuffer; ifb_arguments.intersection_function_buffer_size = raytracingResources.ifbBufferSize; ifb_arguments.intersection_function_stride = raytracingResources.ifbBufferStride; // Set up the ray and finish intersecting metal::raytracing::ray r = { origin, direction }; auto result = trace.intersect(r, ads, ifb_arguments);
-
13:02 - Change of temporal scaler setup to denoised temporal scaler setup
// Change of temporal scaler setup to denoised temporal scaler setup MTLFXTemporalScalerDescriptor* desc = [MTLFXTemporalScalerDescriptor new]; desc.colorTextureFormat = MTLPixelFormatBGRA8Unorm_sRGB; desc.outputTextureFormat = MTLPixelFormatBGRA8Unorm_sRGB; desc.depthTextureFormat = DepthStencilFormat; desc.motionTextureFormat = MotionVectorFormat; desc.diffuseAlbedoTextureFormat = DiffuseAlbedoFormat; desc.specularAlbedoTextureFormat = SpecularAlbedoFormat; desc.normalTextureFormat = NormalVectorFormat; desc.roughnessTextureFormat = RoughnessFormat; desc.inputWidth = _mainViewWidth; desc.inputHeight = _mainViewHeight; desc.outputWidth = _screenWidth; desc.outputHeight = _screenHeight; temporalScaler = [desc newTemporalDenoisedScalerWithDevice:_device];
-
13:04 - Change temporal scaler encode to denoiser temporal scaler encode
// Change temporal scaler encode to denoiser temporal scaler encode temporalScaler.colorTexture = _mainView; temporalScaler.motionTexture = _motionTexture; temporalScaler.diffuseAlbedoTexture = _diffuseAlbedoTexture; temporalScaler.specularAlbedoTexture = _specularAlbedoTexture; temporalScaler.normalTexture = _normalTexture; temporalScaler.roughnessTexture = _roughnessTexture; temporalScaler.depthTexture = _depthTexture; temporalScaler.jitterOffsetX = _pixelJitter.x; temporalScaler.jitterOffsetY = -_pixelJitter.y; temporalScaler.outputTexture = _upscaledColorTarget; temporalScaler.motionVectorScaleX = (float)_motionTexture.width; temporalScaler.motionVectorScaleY = (float)_motionTexture.height; [temporalScaler encodeToCommandBuffer:commandBuffer];
-
16:04 - Creating instance descriptors for instance acceleration structure
// Creating instance descriptors for instance acceleration structure MTLAccelerationStructureInstanceDescriptor *grassInstanceDesc, *treeInstanceDesc = . . .; grassInstanceDesc.intersectionFunctionTableOffset = 0; treeInstanceDesc.intersectionFunctionTableOffset = 1; // Create buffer for instance descriptors of as many trees/grass instances the scene holds id <MTLBuffer> instanceDescs = . . .; for (auto i = 0; i < scene.instances.size(); ++i) . . .
-