📱

📱 RenderDoc で Android のdraw callは計測できない - TB(D)RにおけるGPU時間の計測に関する注意

2023/02/11に公開・約23,700字

(Qiita の記事の移転; 文やダイアグラムなど加筆するかも)

クリックベイトサムネイル:
RenderDoc Event Browser

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 (≈レンダーターゲット)[1].
RenderDoc の ⏰

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

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

前提

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

対象となるGPUアーキテクチャ

主に Android で使用される Adreno, Mali, PowerVR が主に想定されるが, Apple Silicon に積まれるGPUに対しても同じ議論が成立すると思われる.

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

手法 アーキテクチャ
Tile-Based Rendering Adreno, Mali
Tile-Based Deferred Rendering PowerVR, Apple Silicon[4], VideoCore IV[5]
Immediate Rendering デスクトップ NVIDIA, AMD, etc.
NVIDIA Tegra について

Tile-Based Rendering ではない[3:1].
Qualcomm のSoCでタイムスタンプの罠に嵌った人の質問では, Tegra では想定通りになったとのこと.

Vivante(VeriSillicon) について

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

追加情報1

Vulkan Hardware Database に VeriSillicon の Android 端末が存在する. NXP の i.MX 8M Mini EVKB のものらしい.

追加情報2

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

https://twitter.com/jekstrand_/status/1560312633231904768

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

Vulkan

Vulkan では 1.0 のコアに, query pool を用いる query の一種 として timestamp query があり,
コマンドバッファ中のコマンドとしては vkCmdWriteTimestamp が該当するほか, 拡張 VK_KHR_synchronization2にvkCmdWriteTimestamp2KHRがある.
機能は同じだが, vkCmdWriteTimestamp2KHRはパイプラインステージの設定にVK_KHR_synchronization2のそれを受け取る点が異なる.

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

Adreno の Vulkan 実装の状況

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

Mali の Vulkan 実装の状況

サポートしていない端末には Mali が圧倒的に多いが, ごく最近(2022/07 現在)のドライババージョンではサポートし始めている模様.

OpenGL ES

GLES では拡張 GL_EXT_disjoint_timer_query が存在し, 概ねデスクトップ OpenGL の timer query に相当する機能が規定されている.

Android

Android ではそこそこの実装でサポートされている一方, サポートしていない端末には Adreno 540 (2017) を積んだものも含まれている. ドライバのバージョンによる違いだと思われる.

gpuinfo.org のデータベースではサポート率が 約56% に留まるが, これには以下が関係していると思われる:

  • Android の GLES 実装, という括りではかなり昔のものまで含んでしまうこと
  • Vulkan と異なりこの仕様そのものがコア仕様でなく拡張であること
iOS

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

Metal のタイムスタンプ機能については, バージョンやデバイスによって差が激しいようだが, 充実しているといえる.

ドライババージョンによる機能の差

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

本編

TB(D)R の挙動について既知の場合はスキップ

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

Rust っぽい疑似コードで大まかな挙動を示す.
パイプラインステージのうち議論の本質に関係の無いものや, アーキテクチャごとの詳細な最適化など[10]については, 割愛する.

Immediate Rendering なGPU

グラフィクスAPIの draw コマンドは, ある程度直感的な動きをする:

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 では 大きめ[11]かつかなり柔軟な模様 (Snapdragon Profiler で実際の大きさを確認できる).
Adreno について, タイルの大きさなどの情報を取得するための Qualcomm のベンダ拡張の仕様がリリースされた.

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」という括りは, もはや無い.

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の挙動に関する質問に, 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 の実装を覗いてみる.
これは query pool のインデックスuint32_t query位置にその時点でのタイムスタンプを書かせるコマンドだが,
その実装の冒頭には非常に興味深いコメントがある:

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」 とは Adreno 流の「タイル専用メモリ」の呼び方であり, 「blob」は mesa の文脈ではベンダ製のクローズドソースなドライバのことを指す.

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

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

Adreno のオープンソース GLES ドライバ開発者の投稿

Rob Clark 氏は mesa の freedreno (Adreno 用の GLES ドライバ)の開発者の1人だが,
Qualcomm の開発者フォーラムにて, GL_EXT_disjoint_timer_queryで得られる値についての質問に回答を投稿している(強調筆者):

(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[15] では開始/終了は意味を持たない感じなので)
コマンドストリーム[16]からすると, 前のタイルで書き込んだタイムスタンプを次のタイルで上書きしているように見える, ...

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

もちろんこの挙動は 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 によって深く考慮されなかった可能性を示唆する.

Oculus Developers の記事でプロファイリング用に RenderDoc が推奨されているが

この記事では, 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 へのコメントによれば(強調筆者):

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 の最後[17]に生成される.
render pass 中のタイムスタンプは, 全て同じ値になる.
イミディエイトモードのGPUならば, Vulkan が定義するような draw ごとのタイムスタンプが行える.

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

Metal: タイムスタンプの使用可能箇所

MTLDevice には, コマンドバッファ中の箇所を表す列挙体MTLCounterSamplingPointを受け取って, そのデバイスでのタイムスタンプの使用可能箇所を問い合わせるインターフェース, 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 アーキテクチャで実装することが不可能であることが指摘されたが, 投稿者によれば:

Apple Silicon devices only return true for atStageBoundary; all the other enum values return false.

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

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-L1013
つまり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 の仕様リリースに, Qualcomm のベンダ拡張の proposal が2つ含まれている.

画像処理関連の機能を提供するVK_QCOM_image_processingはとりあえず置いておいて, タイルの大きさを取得できるインターフェースを提供するVK_QCOM_tile_propertiesが興味深い.
まだ生成されたドキュメントの形にはなっていないが, proposal の asciidoc は見ることができる.

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

インターフェース

現状では以下のようなインターフェースが提案されており, render pass や VK_KHR_dynamic_renderingのVkRenderingInfoについて, タイルの情報を取得する機能を提供したいことがわかる.

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[14:1] で 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) に実装されている.
2022/06/14 にリリースされた仕様でありサポートしている実装はないが, VK_EXT_subpass_merge_feedback に, 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

脚注
  1. 実際には更に render pass 間のパイプライン化による誤差がある. ↩︎

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

  3. Platform/GFX/MobileGPUs - MozillaWiki ↩︎ ↩︎

  4. Bring your Metal app to Apple silicon Macs - WWDC20 - Videos - Apple Developer ↩︎

  5. VideoCore® IV 3D Architecture Reference Guide ↩︎

  6. Tile-based Rasterization in Nvidia GPUs ↩︎

  7. Real World Technologies - Forums - Thread: Article: Tile-based Rasterization in Nvidia GPUs: Rob Clark 氏の投稿. ↩︎

  8. ただ, GLES 3.1 の仕様書は GLES 3.0 のそれと比べて一気にボリュームが増える事実がある. ↩︎

  9. Metal Retrospective - Roblox Blog:
    > there is no GL driver on iOS 10 on the newest iPhones, apparently. GL is implemented on top of Metal, ↩︎

  10. Early Z rejection, Hidden Surface Removal, Forward Pixel Kill, Transaction Elimination, etc. ↩︎

  11. Adreno tiling · Wiki · freedreno / freedreno · GitLab ↩︎

  12. IDVS(Index-Driven Vertex Shading): The Bifrost Shader Core, IDVS shader variants(Arm Mali Offline Compiler User Guide Version 7.3) ↩︎

  13. HSR(Hidden Surface Removal): Sorting Objects and Geometry on PowerVR Hardware - Imagination ↩︎

  14. 他にはコンピュートシェーダの計測にも使用できるほか, Adreno は render pass 中のジオメトリの複雑度により, tile-based でなくデスクトップのようなイミディエイトモードに切り替える機能を持つ (FlexRender). ↩︎ ↩︎

  15. 英語の文章ではよく単純に tiler と呼ばれる. ↩︎

  16. blob がハードウェアにコマンドストリームを送る動作の解析結果だと思われる. ↩︎

  17. MTLRenderCommandEncoderの最後, つまり render pass の最後. ↩︎

Discussion

ログインするとコメントできます