📱 Android + RenderDoc でdraw callのパフォーマンスは計測できない - TB(D)RにおけるGPU時間の計測に関する注意



クリックベイトサムネイル:
![RenderDoc Event Browser](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100016/b975b455-1140-3bd8-4341-b9ece3e5c56c.png "RenderDoc Event Browser; スマホで Duration の値に意味はない")
RenderDoc の _Event Browser_ でコマンドのGPU時間として表示される _Duration_ は, **モバイル端末では意味をなさない**.

いくつかのかなり有名なブログ記事やスライドにおいても, この「計測」が有用であるかのように書かれているが, **それらは誤っている**.

> RenderDoc is not a profiler.

この事実は **UE4/UE5 だろうが Unity だろうが**変わることはない.

ここではこれらGPUアーキテクチャの動作をはじめ, ドライバの開発者の発言やソースコードの引用を含めた様々な根拠や状況証拠などを挙げて論証する.

# TL;DR

##### 結論
モバイル端末のGPUにおいて, render pass 内のGPUタイムスタンプは, 何らかの意味をなす値を持たない:
- draw call(s) ごとのGPU時間の計測を考えることはできない; **そのように呼べる実行の単位そのものが, 存在しない**. 
- 意味をもって計測できるグラフィクスパイプラインの**GPU時間の最小の単位は render pass (≈レンダーターゲット)**[^render_pass_pipelining].

[^render_pass_pipelining]: 実際には更に render pass 間のパイプライン化による誤差がある.

:::note info
厳密には[タイル(ビン)](https://developer.qualcomm.com/sites/default/files/docs/adreno-gpu/snapdragon-game-toolkit/gdg/tutorials/android/gmem_loads.html#detecting-graphics-memory-loads-in-snapdragon-profiler)だが, ベンダ製のプロファイラでないと原理的に行えない[^tile_hardware_counter]上, draw calls をフィルタリングしてパイプラインステージごとに分解して加算したようなものであり, チューニングの参考とするのは難しい.
:::

[^tile_hardware_counter]: ベンダ独自のハードウェアカウンタを読む必要がある; Vulkan や OpenGL ES(のコア仕様)には「タイル」の概念はないことに注意. Vulkan の _render pass_ API は「タイルの概念を露出せずに TB(D)R 向けの最適化を許す」ことが意図された概念だった (これは失敗だったとする向きも多い).

##### RenderDoc の ⏰
RenderDoc の Event Browser で GPU時間として表示される値(**_Duration_**)は, **モバイル端末のGPUでは意味をなさない**.
[なぜ RenderDoc の _Duration_ の値に意味がないと言えるか ?](#なぜ-renderdoc-の-duration-の値に意味がないと言えるか-)

:::note warn
- 更にこれは, 1フレームの計測が何らかの分布の代表値として使えるのか, というような統計的有意性の議論を無視している.
- 作者が Discord や GitHub で繰り返し言っているように:
  > RenderDoc is not a profiler and isn't advertised as one
:::

##### 疑似コード
~~~c
// B - A の値に, 意味がある:
vkCmdWriteTimestamp(..., A);
  vkCmdBeginRenderPass(...);
    ...
    vkCmdDraw*(...)
    ...
  vkCmdEndRenderPass(...);
vkCmdWriteTimestamp(..., B);
~~~
~~~c
// B - A の値に, 意味はない:
vkCmdBeginRenderPass(...);
  ...
  vkCmdWriteTimestamp(..., A);
    vkCmdDraw*(...)
  vkCmdWriteTimestamp(..., B);
  ...
vkCmdEndRenderPass(...);
~~~

# 前提
[本編へ](#本編): Tile-Based (Deferred) Rendering なGPUやグラフィクスAPIのタイムスタンプ機能について既に知っている場合はスキップされたい.

## 対象となるGPUアーキテクチャ
主に Android で使用される Adreno, Mali, PowerVR が主に想定されるが, [Apple Silicon に積まれるGPUに対しても同じ議論が成立すると思われる](#metal-ios-macapple-silicon).

ここでの分類にて, TBDR は TBR に対して, 各タイルにてラスタライゼーションより前に隠面消去を行うもの(PowerVR 流の呼び方[^mobile_gpus]).

| 手法                             | アーキテクチャ |
| ---------------------------------| ------------ |
| Tile-Based Rendering          | Adreno, Mali |
| Tile-Based Deferred Rendering | PowerVR, Apple Silicon[^bring_your_app_to_metal], VideoCore IV[^video_core_iv_spec] |
| Immediate Rendering | デスクトップ NVIDIA, AMD, etc. |

[^mobile_gpus]: [Platform/GFX/MobileGPUs - MozillaWiki](https://wiki.mozilla.org/Platform/GFX/MobileGPUs)
[^bring_your_app_to_metal]: [Bring your Metal app to Apple silicon Macs - WWDC20 - Videos - Apple Developer](https://developer.apple.com/videos/play/wwdc2020/10631)
[^video_core_iv_spec]: [VideoCore® IV 3D Architecture Reference Guide](https://docs.broadcom.com/doc/12358545)

##### NVIDIA Tegra について
Tile-Based Rendering ではない[^mobile_gpus].
[Qualcomm のSoCでタイムスタンプの罠に嵌った人の質問](https://developer.qualcomm.com/forum/qdn-forums/software/adreno-gpu-sdk/29701)では, Tegra では想定通りになったとのこと.

:::note info
Maxwell 以降の NVIDIA は _tile-based rasterization_ を行っていること[^maxwell_tile_based_rasterization]が言及されることがあるが,
これは Tile-Based (Deferred) Rendering とは別物らしい[^maxwell_not_tile_based_rendering].
:::

[^maxwell_tile_based_rasterization]: [Tile-based Rasterization in Nvidia GPUs](https://www.realworldtech.com/tile-based-rasterization-nvidia-gpus/)
[^maxwell_not_tile_based_rendering]: [Real World Technologies - Forums - Thread: Article: Tile-based Rasterization in Nvidia GPUs](https://www.realworldtech.com/forum/?threadid=159876&curpostid=159910): Rob Clark 氏の投稿.

##### Vivante(VeriSillicon) について
現在の Vulkan や GLES 3.x を実装可能なレベルのIPコアについてそもそも不明. [公式サイト](https://verisilicon.com/en/IPPortfolio/VivanteGPUIP)には `Vulkan® 1.0/1.1/1.2` の表記があるが, 主に組込み向けのためか情報は少なく, 2015年以降の動向は判然としない.

:::note info
Mali の Midgard (Mali-T系, 2013-2016) ですら ["_barely_ Vulkan 1.0 capable"](https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/16915) と言われている.
:::

##### 追加情報1
Vulkan Hardware Database に [VeriSillicon の Android 端末](https://vulkan.gpuinfo.org/displayreport.php?id=15504)が存在する. NXP の [i.MX 8M Mini EVKB](https://www.nxp.com/design/development-boards/i-mx-evaluation-and-development-boards/evaluation-kit-for-the-i-mx-8m-mini-applications-processor:8MMINILPD4-EVK) のものらしい.

##### 追加情報2
Vivante のハードウェア設計の品質について, Faith Ekstrand 氏(Intel のドライバ開発者)の見聞.
このスレッドは全体的に各ベンダのハードウェアの設計の品質について非常に興味深い知見を提供しており, 一読を勧める.

<blockquote class="twitter-tweet" data-dnt="true"><p lang="en" dir="ltr">At the bottom of the pile are Imagination, Broadcom, and Vivante. I don&#39;t have much experience with any of them besides talking to people. From what I&#39;ve heard, Vivante is probably the worst of the three but I&#39;m not going to try and well-order the other two.</p>&mdash; Jason Ekstrand 💔🇺🇦 (@jekstrand_) <a href="https://twitter.com/jekstrand_/status/1560312633231904768?ref_src=twsrc%5Etfw">August 18, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

## グラフィクスAPIのGPUタイムスタンプに関する仕様とその実装

### Vulkan

Vulkan では 1.0 のコアに, query pool を用いる query の一種 として timestamp query があり,
コマンドバッファ中のコマンドとしては `vkCmdWriteTimestamp` が該当するほか, 拡張 [`VK_KHR_synchronization2`](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VK_KHR_synchronization2.html)に`vkCmdWriteTimestamp2KHR`がある.
機能は同じだが, `vkCmdWriteTimestamp2KHR`はパイプラインステージの設定に`VK_KHR_synchronization2`のそれを受け取る点が異なる.

実装によるサポートは義務的ではない(`VkQueueFamilyProperties::timestampValidBits`, `VkPhysicalDeviceLimits::timestampComputeAndGraphics`).

##### Adreno の Vulkan 実装の状況
かなり古めのドライババージョンでもサポートを期待でき, Build Date が2018年の(恐ろしく成熟度の低い)バージョンでも, タイムスタンプはサポートしている.

##### Mali の Vulkan 実装の状況
[サポートしていない端末](https://vulkan.gpuinfo.org/listreports.php?limit=timestampComputeAndGraphics&value=0&platform=android)には Mali が圧倒的に多いが, ごく最近(2022/07 現在)のドライババージョンではサポートし始めている模様.

:::note info
Google Pixel 6 (Mali-G78) に至っては, [VK_EXT_calibrated_timestamps](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VK_EXT_calibrated_timestamps.html)に対応している(!); これは今までデスクトップPCにしかなかった.
:::

### OpenGL ES
GLES では拡張 [`GL_EXT_disjoint_timer_query`](https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_disjoint_timer_query.txt) が存在し, 概ねデスクトップ OpenGL の timer query に相当する機能が規定されている.

:::note info
GLES の API に, Vulkan/Metal と異なり, 明確な render pass の境界がないことが, 今回の問題をより複雑にしている.
:::

##### Android
Android では[そこそこの実装でサポート](https://opengles.gpuinfo.org/listreports.php?extension=GL_EXT_disjoint_timer_query)されている一方, [サポートしていない端末](https://opengles.gpuinfo.org/listreports.php?extension=GL_EXT_disjoint_timer_query&option=not)には Adreno 540 (2017) を積んだものも含まれている. ドライバのバージョンによる違いだと思われる.

gpuinfo.org のデータベースではサポート率が 約56% に留まるが, これには以下が関係していると思われる:
- Android の GLES 実装, という括りではかなり昔のものまで含んでしまうこと
- Vulkan と異なりこの仕様そのものがコア仕様でなく拡張であること

##### iOS
iOS の GLES 実装では `GL_EXT_disjoint_timer_query`はサポートされていない.
GLES 3.1 を実装していない[^gles_31_spec]のと合わせて, Metal 推しに伴う GLES の冷遇の一環ではある.

Metal のタイムスタンプ機能については, バージョンやデバイスによって差が激しいようだが, 充実しているといえる.
[^gles_31_spec]: ただ, GLES 3.1 の仕様書は GLES 3.0 のそれと比べて一気にボリュームが増える事実がある.

:::note info
少なくとも iOS 10 以降の GLES ドライバは, Metal で実装されている[^ios_gles_metal].
:::

[^ios_gles_metal]: [Metal Retrospective - Roblox Blog](https://blog.roblox.com/2016/12/metal-retrospective/):
                   > there is no GL driver on iOS 10 on the newest iPhones, apparently. GL is implemented on top of Metal, 

### ドライババージョンによる機能の差
興味深いことに, Mali を積んだ多くの端末のドライバでは, その GLES 実装が`GL_EXT_disjoint_timer_query`に対応しているにも関わらず, Vulkan 実装ではタイムスタンプの使用に対応していない (`VkQueueFamilyProperties::timestampValidBits` == 0) ようだった.
上述のとおり, これは最近のドライバのリリースでは改善されているようで, 単純に Vulkan 実装では対応が行われずにリリースされていただけだと思われる.

:::note info
更に[Android の宿命](https://blog.esper.io/android-dessert-bites-14-gpu-driver-updates-3819534/)として, 永遠にドライバのアップデートを受け取らない端末が多数存在する, という問題がある.

時期どころか, 行うかどうかさえ端末メーカの気まぐれで決まるOTAアップデートに, GPUベンダによるドライバのアップデートが含まれる必要があるが,
Qualcomm が直接端末メーカに渡す SoC に積まれている Adreno の場合と異なり,
Mali の場合 Arm は設計(とドライバ)を SoCベンダに売っている形のため, 更に中間者が挟まっている形になる:
> Adreno: Qualcomm ➡️ 端末メーカ ➡️ 端末
> Mali: Arm ➡️ SoCベンダ ➡️ 端末メーカ ➡️ 端末

(SAMSUNG のように, 「SoCベンダ」が「端末メーカ」である場合もある)
:::

# 本編
[TB(D)R の挙動について既知の場合はスキップ](#根拠)

## 非 TB(D)R と TB(D)R での draw コマンドの挙動の復習

Rust っぽい疑似コードで大まかな挙動を示す.
パイプラインステージのうち議論の本質に関係の無いものや, アーキテクチャごとの詳細な最適化など[^tbdr_vendor_specific_optimizations]については, 割愛する.
[^tbdr_vendor_specific_optimizations]: Early Z rejection, Hidden Surface Removal, Forward Pixel Kill, Transaction Elimination, etc.

### Immediate Rendering なGPU
グラフィクスAPIの draw コマンドは, ある程度直感的な動きをする:
```rust
for draw_command in draw_commands {
  for primitive in draw_command.primitives {
    for vertex in primitive.vertices {
      vertex.shade();
    }

    let fragments = primitive.rasterize();

    for fragment in fragments {
      fragment.shade();
    }
  }
}
```

### TBR(Tile-Based Rendering) なGPU
TBR では, render pass のハードウェアでの実行は大きく2つのパスに分かれる.
1パス目では各プリミティブについて頂点シェーダを実行して所属するタイルを決定し,
2パス目では各タイルについて, 1パス目で収集されたプリミティブのラスタライゼーションのあと, フラグメントシェーダが実行される.
**この時点で, 「draw call ごと」の概念は破綻する**ことがわかる.

#### 1. Binning Pass (Adreno) / Geometry Processing (Mali)
レンダーターゲットをタイルに分割し, draw コマンドのプリミティブについて頂点シェーダを実行し, 各タイルで可視なものを決定していく.

タイルの大きさは Mali では基本的には 16x16 になるのに対して, Adreno では 大きめ[^adreno_tiling]かつかなり柔軟な模様 (Snapdragon Profiler で実際の大きさを確認できる).
Adreno について, [タイルの大きさなどの情報を取得するための Qualcomm のベンダ拡張の仕様がリリースされた](#qualcomm-のタイル関連の拡張-vk_qcom_tile_properties).
[^adreno_tiling]: [Adreno tiling · Wiki · freedreno / freedreno · GitLab](https://gitlab.freedesktop.org/freedreno/freedreno/-/wikis/Adreno-Tiling)

:::note info
Bifrost 以降の Mali では, (タイルの決定に必要な)頂点位置を出力する処理のみをドライバが切り出した, 特別な頂点シェーダが実行される[^mali_bifrost_idvs].
これは「draw call ごと」を考えることを更に難しくする.
[^mali_bifrost_idvs]: IDVS(Index-Driven Vertex Shading): [The Bifrost Shader Core](https://developer.arm.com/documentation/102546/0100/Index-Driven-Geometry-Pipeline), [IDVS shader variants(Arm Mali Offline Compiler User Guide Version 7.3)](https://developer.arm.com/documentation/101863/0703/Using-Mali-Offline-Compiler/Performance-analysis/IDVS-shader-variants)
:::

```rust
for draw_command in draw_commands {
  for primitive in draw_command.primitives {
    // 頂点位置を決定するため頂点シェーダを実行する.
    for vertex in primitive.vertices {
      vertex.shade();
    }

    // 所属するタイルを決定し, 可視プリミティブとして登録する.
    let tiles = find_tiles_for_primitive(&primitive);
    for tile in tiles {
      tile.add_visible_primitive(&primitive);
    }
  }
}

```

#### 2. Rendering Pass (Adreno) / Fragment Processing (Mali)
各タイルについて, 可視なプリミティブのラスタライゼーションとフラグメントシェーダの実行を行う.
「draw call」という括りは, もはや無い.

:::note info
PowerVR や Apple Silicon のような TBDR の場合, ラスタライゼーションやフラグメントシェーディングの前に隠面消去が行われる[^hsr].
[^hsr]: HSR(Hidden Surface Removal): [Sorting Objects and Geometry on PowerVR Hardware - Imagination](https://blog.imaginationtech.com/sorting-objects-and-geometry-on-powervr-hardware/)
:::

```rust
for tile in render_pass.tiles {
  // クリアしないなら, システムRAMのフレームバッファから, 内容を on-chip なタイル専用メモリにロード.
  tile.load_framebuffer_if_not_clearing();

  // このタイルで可視なプリミティブのフラグメントシェーダを実行し,
  // レイテンシが小さく帯域幅の広い on-chip なタイル専用メモリに描画する.
  for primitive in tile.visible_primitives {
    let fragments = primitive.rasterize();

    for fragment in fragments {
      fragment.shade();
    }
  }

  // on-chip なタイル専用メモリの内容を, システムRAMのフレームバッファにストア.
  tile.store_framebuffer();
}
```

## 根拠

### Mali のドライバ開発者の回答
Arm の開発者フォーラムの, [Mali における`GL_EXT_disjoint_timer_query`の挙動に関する質問](https://community.arm.com/support-forums/f/graphics-gaming-and-vr-forum/6869/gl_ext_disjoint_timer_query-for-performance)に, Mali のドライバチームのエンジニアである Peter Harris 氏が興味深い回答をしている(強調筆者):
> Tile-based GPUs like Mali don't even implement the pipeline as a single pipeline. 
> (中略)
> you can't use timer queries for timing single **drawcalls; they don't exist in isolation in any usable form**.
> From a query point of view all drawcalls in the pass will complete **when the last tile in the fragment shading completes**.
> Timer queries can be **used with some success for timing single renderpasses**,

拙訳(強調筆者):
> Mali のような tile-based なGPUはそもそもパイプラインを(OpenGL の仕様で示されるような)1つのパイプラインとして実装していない.
> timer query で個々の draw call の時間を計測することはできない; **draw call は独立した形で存在しない**.
> query の視点では, render pass 中の draw call の終了タイミングは, **最後のタイルのフラグメントシェーディングが完了したとき**になる.
> timer query は**個々の render pass の時間の計測にはある程度有効**だが, ...

要するに *draw call ごと, と呼べるような単位そのものが存在しない*ことを指摘しているが,
**「最後のタイルのフラグメントシェーディングが完了したとき」**, というフレーズが特に注目に値する.

### Adreno のオープンソース Vulkan ドライバのコメント
オープンソースのグラフィクスドライバスタックである mesa には Adreno 用の Vulkan ドライバも含まれ, turnip と呼ばれる.

[turnip の`vkCmdWriteTimestamp2KHR` の実装](https://gitlab.freedesktop.org/mesa/mesa/-/blob/2ea0d735d4a24f995434e0b1ef27cc14b5a5e80d/src/freedreno/vulkan/tu_query.c#L1581-L1594)を覗いてみる.
これは query pool のインデックス`uint32_t query`位置にその時点でのタイムスタンプを書かせるコマンドだが,
その実装の冒頭には非常に興味深いコメントがある:
```c:tu_query.c(1581-1594)
VKAPI_ATTR void VKAPI_CALL
tu_CmdWriteTimestamp2(VkCommandBuffer commandBuffer, ..., uint32_t query)
{
   ...
   /* Inside a render pass, just write the timestamp multiple times so that
    * the user gets the last one if we use GMEM. There isn't really much
    * better we can do, and this seems to be what the blob does too.
    */
   struct tu_cs *cs = cmd->state.pass ? &cmd->draw_cs : &cmd->cs;
```
拙訳(強調筆者):
> render pass 中の場合, とりあえずタイムスタンプは複数回書き込んでしまい,
> GMEM を使用している場合にはユーザーには**最後に書き込んだ値**が得られるようにする.
> 他に特にやりようは無いし, blob もこのようにしているようだ.

「[GMEM](https://developer.qualcomm.com/sites/default/files/docs/adreno-gpu/developer-guide/tutorials/android/gmem_loads.html)」 とは Adreno 流の「タイル専用メモリ」の呼び方であり, 「blob」は mesa の文脈ではベンダ製のクローズドソースなドライバのことを指す.

「GMEM を使用している場合」はまさに Tile-Based Rendering を行う[^adreno_flexrender]場合のことを言っており,
render pass 中の draw コマンドがそれぞれのタイル毎に繰り返されるため, query pool 中のインデックス`query`位置へのタイムスタンプの書込みもまた, タイルごとに繰り返されることを意味している.

タイルごとに**同じ位置への上書きが繰り返される**ので,
アプリケーション側に見える値は**最後のタイルの実行で書き込まれた値**になる.

[^adreno_flexrender]: 他にはコンピュートシェーダの計測にも使用できるほか, Adreno は render pass 中のジオメトリの複雑度により, tile-based でなくデスクトップのようなイミディエイトモードに切り替える機能を持つ ([FlexRender](https://developer.qualcomm.com/sites/default/files/docs/adreno-gpu/developer-guide/gpu/overview.html#flexrender-technology-hybrid-deferred-and-direct-rendering-mode)).

:::note info
`GL_EXT_disjoint_timer_query`について,
freedreno (mesa に含まれる Adreno 用の GLES ドライバ)にも, [同じようなコメント](https://gitlab.freedesktop.org/mesa/mesa/-/blob/6b3af02a6fe87152920d8b32434ea7bed4fb1fa5/src/gallium/drivers/freedreno/a6xx/fd6_query.c#L247-L252)がある:
```c:fd6_query.c(247-252)
/* NOTE: timestamp query isn't going to give terribly sensible results
 * on a tiler.  But it is needed by qapitrace profile heatmap.  If you
 * add in a binning pass, the results get even more non-sensical.  So
 * we just return the timestamp on the last tile and hope that is
 * kind of good enough.
 */
```
:::

### Adreno のオープンソース GLES ドライバ開発者の投稿
[Rob Clark 氏](https://gitlab.freedesktop.org/robclark)は mesa の freedreno (Adreno 用の GLES ドライバ)の開発者の1人だが,
Qualcomm の開発者フォーラムにて, `GL_EXT_disjoint_timer_query`で得られる値についての質問に[回答を投稿](https://developer.qualcomm.com/comment/9788#comment-9788)している(強調筆者):
> (since start and stop time kind of have no sensible meaning with a tiler)
> (中略)
> From the cmdstream, it looks like it is **overwriting the saved timestamps from the previous tile on the next tile**
> (後略)

拙訳(強調筆者):
> (TB(D)R[^tiler] では開始/終了は意味を持たない感じなので)
> コマンドストリーム[^blob_cmdstream]からすると, **前のタイルで書き込んだタイムスタンプを次のタイルで上書き**しているように見える, ...

「前のタイルで書き込んだタイムスタンプを次のタイルで上書きしている」.
やはり, アプリケーション側に見える値は**最後のタイルの実行で書き込まれた値**になる.

[^tiler]: 英語の文章ではよく単純に tiler と呼ばれる.
[^blob_cmdstream]: blob がハードウェアにコマンドストリームを送る動作の解析結果だと思われる.

もちろんこの挙動は implementation detail であり, グラフィクスAPIの仕様で定義されたものではない.

## 状況証拠など

### Adreno の Vulkan 実装におけるバグ
詳しいドライババージョンの範囲は不明だが, `vkCmdWriteTimestamp` について, 以下のバグに遭遇した経験がある:
- secondary command buffer 中でのタイムスタンプが永久に available にならない;
  `VK_QUERY_RESULT_WAIT_BIT`付きの`vkGetQueryPoolResults`が永久にブロックする.
- multiview を使用する render pass でタイムスタンプが view の数に関わらず1つしか書き込まれない.
  仕様の規定通りに view の数だけ書き込まれる前提で`vkGetQueryPoolResults`すると, 永久に available にならない.

これらは, そのどちらも render pass 中の使用で発生するものだということにその重要性がある.
つまり少なくともある時点までは, Qualcomm 内部でこれらのユースケースはテストされなかったと想像される.

これはそもそも render pass 中でタイムスタンプを使用することが Qualcomm によって深く考慮されなかった可能性を示唆する.

:::note info
これは RenderDoc の ⏰ _Time durations for the actions_ の動作にも影響する; 該当するドライババージョンで secondary command buffer を使用していると, ⏰ をクリックした後, 永久に処理が返らない.
:::
:::note info
少なくとも, 以下で発生する:
Adreno 650, `512.444.0`, `Driver Build: 7c50932, Ic71133a7bc, 1594419786 Date: 07/10/20 Compiler Version: EV031.29.00.09 Driver Branch:`

勿論, 該当するドライババージョンでも, primary command buffer 中や, multiview を使用しない場合ならばタイムスタンプは有限時間で available になる; 依然, その結果に有用性はない.
:::

### Oculus Developers の記事でプロファイリング用に RenderDoc が推奨されているが
[この記事](https://developer.oculus.com/documentation/native/android/ts-renderdoc-for-oculus/?locale=ja_JP)では, Meta が Oculus Quest 用に特別に内部でフォークして提供している _RenderDoc for Oculus_ の話をしている:
> Oculusは、RenderDocの独自のフォークを管理するようになりました
> このフォークは、QuestのSnapdragon 835チップとQuest 2のSnapdragon XR2チップからの**低レベルGPUプロファイリングデータ(特にそのタイルレンダラーからの情報)** へのアクセスを提供します。

Binning Pass で作られた ビン(タイル) の実行の詳細を表示する __タイルタイムライン__ やALU命令数など, Snapdragon Profiler でしか見れなかったような情報の表示が追加されている.
ソースコードが提供されていないのが惜しいが, 少なくとも完全にベンダ依存であり, 更には特定のチップ依存である可能性もある.

**つまり, 明らかに Tile-Based Rendering なGPU向けにカスタマイズされた RenderDoc になっている.**
更に, draw call ごとの _Duration_ に関する変更についてはよくわからない.

### Metal: iOS, Mac(Apple Silicon)
iOS のものや M1 などの SoC に積まれているGPUも TBDR であり, Adreno や Mali などと同様の議論が成立するはずだが,
Metal を直接触った経験が少ないため, 直接観測したことはない.
MoltenVK にも, 少し前のバージョンまで timestamp query は適切に実装されていなかった.

#### MoltenVK の開発者のコメント
Apple の SoC の細かい挙動については公開されている情報が少ないが,
MoltenVK の主な開発者でありメンテナである Bill Hollings 氏の, [タイムスタンプの値に関する issue へのコメント](https://github.com/KhronosGroup/MoltenVK/issues/1585#issuecomment-1117720933)によれば(強調筆者):
> Apple SoC timestamps are generated **at the end of the current encoding pass**.
> **All timestamps in the renderpass will have the same timestamp**.
> IM GPU's are different and support the kind of per-draw timestamping that Vulkan defines.

拙訳(強調筆者):
> Apple の SoC のタイムスタンプは現在の **encoding pass の最後[^end_of_metal_encoding_pass]に生成される**.
> **render pass 中のタイムスタンプは, 全て同じ値**になる.
> イミディエイトモードのGPUならば, Vulkan が定義するような draw ごとのタイムスタンプが行える.

これを信じるなら, render pass 中のタイムスタンプは全て, 同じく全てのタイルの処理が完了したときの値になるため, どのペアの差分もゼロになり, 何の意味もないことになる.

[^end_of_metal_encoding_pass]: [`MTLRenderCommandEncoder`](https://developer.apple.com/documentation/metal/mtlrendercommandencoder?language=objc)の最後, つまり render pass の最後.

#### Metal: タイムスタンプの使用可能箇所
MTLDevice には, コマンドバッファ中の箇所を表す列挙体[`MTLCounterSamplingPoint`](https://developer.apple.com/documentation/metal/mtlcountersamplingpoint)を受け取って, そのデバイスでのタイムスタンプの使用可能箇所を問い合わせるインターフェース, [`supportsCounterSampling`](https://developer.apple.com/documentation/metal/mtldevice/3564459-supportscountersampling)が存在する.
定義されている列挙値は:
- `atBlitBoundary`: blit コマンド間
- `atDispatchBoundary`: kernel (コンピュートシェーダ)の dispatch コマンド間
- `atDrawBoundary`: draw コマンド間
- `atStageBoundary`: render pass の頂点/フラグメントステージ間, compute/blit パス間
- `atTileDispatchBoundary`: render pass 内の tile dispatch 間

つまり, draw call ごとの計測を行うためには, この関数が`atDrawBoundary`について`true`を返す必要がある.
だが既に予想がつくように, **Apple Silicon のデバイスでは, このうち `atStageBoundary`についてしか`true`を返さない**らしい.

WebGPUの仕様策定のリポジトリにて, 当時の仕様で定められたタイムスタンプ機能を, TBDR アーキテクチャで実装することが[不可能であることが指摘された](https://github.com/gpuweb/gpuweb/issues/2046)が, 投稿者によれば:
> Apple Silicon devices only return true for `atStageBoundary`; all the other enum values return false. 

つまり Apple Silicon では Metal の実装自体が, このようなインターフェースを通して, **draw call ごとの計測が不可能**であることを報告する.

:::note info
MoltenVK では `MVKCounterSamplingFlags` として露出される; [`vk_mvk_moltenvk.h`](https://github.com/KhronosGroup/MoltenVK/blob/b88c94a651d09eca514fc9d065ca94be7fbb6129/MoltenVK/MoltenVK/API/vk_mvk_moltenvk.h#L842-L850) を参照.
:::

#### Xcode の GPUキャプチャで draw コマンドの所に表示される時間は?
なんだろうね.
ベンダ独自のハードウェアカウンタで, 各パイプラインステージに分解された draw call のワークロードの所要時間を計測して元の draw call ごとに加算したりしているのでなければ,
RenderDoc の _Duration_ のように意味のない値になっているのか?

PowerVR の頃はどうだった🤔?

## なぜ RenderDoc の _Duration_ の値に意味がないと言えるか ?
ここまでの議論から, TB(D)R において Event Browser 中の draw コマンドの _Duration_ の値が無意味なのは自明と言えるが,
`vkCmdBeginRenderPass` の `Colour Pass` のそれにすら, 同様に有用性がない.

### ⏰ _Time durations for the actions_ の動作
Event Browser の ⏰ _Time durations for the actions_ は, キャプチャされたコマンドたちについて,
各 draw コマンドの前後に`vkCmdWriteTimestamp`を挿入してターゲットデバイスに再実行させ, 書き込まれたタイムスタンプの差分を収集する:
- `EventBrowser::on_timeActions_clicked()`: https://github.com/baldurk/renderdoc/blob/6d7d4829ee70e1e84b9ff91bd679bba92a090f3c/qrenderdoc/Windows/EventBrowser.cpp#L3713-L3724
- `VulkanReplay::FetchCounters(const rdcarray<GPUCounter> &counters)`: https://github.com/baldurk/renderdoc/blob/a51b20369f685b4e57ccd97fe203483c6d5c5d1f/renderdoc/driver/vulkan/vk_counters.cpp#L931-L989

Vulkan 以外のグラフィクスAPIについても, 概ね同じような動作をする.

親ノードの値は, 子ノードの値の合計になる.
`EventItemModel::CalculateTotalDuration`: https://github.com/baldurk/renderdoc/blob/6d7d4829ee70e1e84b9ff91bd679bba92a090f3c/qrenderdoc/Windows/EventBrowser.cpp#L991
つまり`Colour Pass`のノード の _Duration_ は, その子ノードである draw コマンドたちの _Duration_ の総和に過ぎず,
何かの参考になる情報にはならない.

### 代替案
子供の draw コマンドの _Duration_ の総和という形でなく,
`vkCmdBeginRenderPass`～`vkCmdEndRenderPass` の前後に`vkCmdWriteTimestamp`を挿入するようにした上で,
その値の差分を`Colour Pass`の _Duration_ として Event Browser に表示するようにできれば,
TB(D)R においてもある程度有用なGPU時間の計測機能として使えたはずだと想像される.

# おまけ

## Qualcomm のタイル関連の拡張 `VK_QCOM_tile_properties`
2022/06/21 の [Vulkan 1.3.222 の仕様リリース](https://github.com/KhronosGroup/Vulkan-Docs/commit/8dcc7469b01529600b712596a5a48ec8c710e228)に, Qualcomm のベンダ拡張の proposal が2つ含まれている.

画像処理関連の機能を提供する`VK_QCOM_image_processing`はとりあえず置いておいて, タイルの大きさを取得できるインターフェースを提供する`VK_QCOM_tile_properties`が興味深い.
まだ生成されたドキュメントの形にはなっていないが, [proposal の asciidoc](https://github.com/KhronosGroup/Vulkan-Docs/commit/8dcc7469b01529600b712596a5a48ec8c710e228#diff-ce15ed42bd08dd0530d815d93a2ac144aa8d89cf0e9f725016b18a8c3d3845dd) は見ることができる.

proposal には, Adreno のタイリング(binning)の動作についての貴重な情報も含まれている.
より実用的には, fragment density map の生成に役立てることが想定されているらしい.

### インターフェース
現状では以下のようなインターフェースが提案されており, render pass や `VK_KHR_dynamic_rendering`の`VkRenderingInfo`について, タイルの情報を取得する機能を提供したいことがわかる.
```c
typedef struct VkTilePropertiesQCOM {
    VkStructureType       sType;
    void*                 pNext;
    VkExtent3D            tileSize;
    VkExtent2D            apronSize;
    VkOffset2D            origin;
} VkTilePropertiesQCOM;

VkResult vkGetFramebufferTilePropertiesQCOM(VkDevice, VkFramebuffer, uint32_t* pPropertiesCount, VkTilePropertiesQCOM* pProperties);
VkResult vkGetDynamicRenderingTilePropertiesQCOM(VkDevice, const VkRenderingInfo*, VkTilePropertiesQCOM* pProperties);
```

### FlexRender について
FlexRender[^adreno_flexrender] で Immediate Rendering になったケースが気になるところだが, こちらも proprosal の issues で触れられている:

```
=== RESOLVED: Adreno implementation may decide to execute certain workloads in direct rendering mode a.k.a Flex render. What is the interaction of this extension with Flex render?

In those cases, the information returned by this extension may not indicate the true execution mode of the GPU.
```
つまり, その場合, 正しい情報を返さないことを実装に許可する内容となっている.

### Subpass の on-chip 動作を確認する拡張
追記(2023/02/13): Pixel 6 の Mali の新しめのドライバ(38.1.0) に[実装されている](https://vulkan.gpuinfo.org/displayreport.php?id=17843).
~~2022/06/14 にリリースされた仕様でありサポートしている実装はないが~~, [`VK_EXT_subpass_merge_feedback`](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_EXT_subpass_merge_feedback.html) に, render pass 中の subpass が実際にマージされたかどうかを問い合わせるためのインターフェースが規定されている.
マージできなかった場合その理由も返せるようになっているらしい.

## Adreno のバグ
タイムスタンプに関連しないが, 他にもいくつか知られている Adreno の Vulkan 実装のバグがある:
- https://github.com/skyline-emu/skyline/blob/cbc896c8f8230a92cffbfdf1c57017b245a0cdb6/app/src/main/cpp/skyline/gpu/trait_manager.h#L46-L68
- https://github.com/dolphin-emu/dolphin/blob/cce6133ef6a95ddca18264290be005ababc6dc6e/Source/Core/VideoCommon/DriverDetails.h#L278-L283

# その他
- Qiita, GitHub や GitLab のコードを embed できないのか ?