HAR グラフィック パイプライン

このページでは、高可用性レンダラ(HAR)の完全なグラフィック パイプラインについて詳しく説明し、Figma デザイン ドキュメントから画面に表示される最終的なピクセルまでのデータの流れを追跡します。

概要

このパイプラインは、高レベルの UI 定義を低レベルのグラフィック コマンドに変換し、ハードウェア ディスプレイに効率的に表示します。このパイプラインは、自動車の安全に不可欠なアプリ向けに設計されており、決定論的なレンダリング、効率的な状態管理、Direct Rendering Manager(DRM)や Generic Buffer Management(GBM)などのプラットフォーム グラフィック サブシステムとの堅牢な連携を重視しています。

このパイプラインは、次の 4 つの主要なフェーズに分けることができます。

  1. プリレンダリング: シーングラフの処理、カスタマイズの適用、レイアウトの解決。
  2. コマンド生成: 解決されたシーングラフをバックエンドに依存しない表示リストに変換。
  3. レンダリング: Impeller グラフィック エンジンを使用して描画コマンドを実行。
  4. プレゼンテーション: フレームバッファの管理とディスプレイ ハードウェアとの同期。

HAR グラフィック フロー

図 1.HAR グラフィック フロー。

フェーズ 1: プリレンダリング

このフェーズでは、静的な Figma デザインと動的なアプリの状態が、レンダリングの準備ができた完全に解決されたインメモリ UI ツリーに変換されます。このフェーズは、メインの表示ループとは別の専用のリデューサ スレッドで実行されます。

1.1 DesignCompose の基盤

HAR パイプラインは、DesignCompose エコシステム上に構築されています。

  • ソース: UI は Figma で設計され、DesignCompose プラグインを使用してエクスポートされます。
  • 定義: 出力は、デザイン(ノード、スタイル、バリアント)のシリアル化された表現である DesignComposeDefinition のインスタンスです。
  • データ バインディング: アプリの UI モデルは、手続き型マクロ(たとえば、 #[Design(node = "#speed")])を使用して、Rust 構造体のフィールドを Figma ドキュメント内の特定の名前付きノードに明示的にバインドします。これにより、アプリの状態によってビジュアル要素のプロパティが自動的に制御されます。

この基盤の主要コンポーネントは次のとおりです。

  • リデューサ: 中央のイベント ループとして機能し、アクションを処理して現在の状態を更新します。フレームワークには DefaultReducer が用意されていますが、必要に応じてカスタム リデューサ実装を提供できます。
  • プレゼンター: 現在の状態を UI モデルにブリッジします。Presenter トレイトは harry フレームワーク クレートで指定され、参照 実装(UIModelPresenter)は harry-app-core クレートで提供されます。
  • UI モデル: 現在の状態に基づいてカスタマイズを生成します。UI モデルコードは、derive_customizations クレートによって提供される DesignDocument マクロを使用して生成されます。harry-app-core クレートの UIModel 構造体は、この例を示しています。
  • Squoosh: デザインに従って UI をレンダリングするために使用される SquooshView データ構造とバリアント リポジトリを提供します。シリアル化されたデザイン ドキュメントは、dc_bundle クレートによって DesignCompose ライブラリから読み込まれ、ランタイム パフォーマンスを向上させるために SquooshView 構造体のツリーに変換されます。

1.2 リデューサ ループ

パイプラインはアクションによって駆動されます。フレームワークは、フレームワーク自体が使用する内部アクションを定義する Actions 列挙型を指定しますが、ユーザーがアプリ固有の追加アクション(UpdateVehicleSpeedButtonPress など)を定義できるようにする CustomAction バリアントも含まれています。

フレームワークには、アプリの状態に影響するアクションの実装を簡素化し、必要に応じて副作用を生成する StateAction トレイトも用意されています。副作用は、処理のためにリデューサからアプリに返されます。harry-app-core クレートの CustomActions 列挙型は、この詳細な例を示しています。

リデューサ ループの基本的な概要は次のとおりです。

  • アクション処理: Reducer はアクションを受け取り、現在の状態を更新します。これは、現在の速度や、どの警告灯が点灯しているかなどの生データです。副作用(シートベルト ライトが点滅したときにチャイムを鳴らす信号など)を生成することもあります。
  • プレゼンテーション: Presenter は新しい状態を UIModel にマッピングします。 UIModel はビューモデルで、UI 用に特別にフォーマットされたデータ(たとえば、速度「120」を文字列「65 mph」にフォーマットするなど)を保持します。
  • カスタマイズの生成: UI モデルの apply メソッドが呼び出され、RenderCustomization インスタンスのセットが生成されます。これらは、Figma デザインを変更するための明示的な手順です(例: 「ノード #speed のテキストを '65 mph' に設定する」)。
  • 最適化のための UpdatePolicy: プリレンダリング パスのたびに、次のレンダリング更新が必要なタイミングを示す UpdatePolicy 値が返されます。保留中の状態変更がなく、アニメーションが実行されていない場合、UpdatePolicy は、すぐに更新する必要がないことを示します。このような場合、リデューサは新しい表示リストの生成を停止し、不要なレンダリング サイクルを防ぎ、新しいアクションまたはイベントによって変更がトリガーされるまでリソースを節約します。

1.3 ビューの取り込みとリポジトリの初期化

パイプラインは DesignComposeDefinition インスタンスから始まります。これは、DesignCompose によってプロトコル バッファ構造にシリアル化された Figma デザイン ドキュメントです。

  • 初期読み込み: 起動時に、メインのデザイン(ルートノードで指定)が DesignComposeDefinition から初期 SquooshView ツリーに変換されます。この作業を行うのは 1 回限りです。

  • リポジトリ: SquooshVariantRepository は、再利用可能なコンポーネント バリアントと最初に読み込まれたビューを管理します。

  • 遅延読み込み: 起動時間とメモリ使用量を最小限に抑えるため、追加のビュー(初期ルートノード ツリーの一部ではないビュー)は、レンダリング ロジックで明示的に参照され、必要になった場合にのみ(リストのカスタマイズ時など)、ドキュメントから遅延読み込みされます。

1.4 カスタマイズ パス

SquooshView ツリーをトラバースして、動的なアプリの状態を適用します。

  • バリアントの入れ替え: ランタイム ロジックに基づいて、コンポーネント インスタンスが特定のバリアントに置き換えられます(たとえば、現在のドライブモードを表すアイコンをスポーツからエコに変更するなど)。

  • リストの展開: Figma の単一のテンプレート アイテムが、子要素の動的リストに置き換えられます。アニメーションの安定した ID を確認するために、これらの子要素に新しい一意の ID が生成されます。

  • テキストとスタイルのオーバーライド: テキスト コンテンツ(速度の値など)とスタイル(不透明度、色など)が現在の状態から更新されます。

1.5 変数の解決

Figma またはアプリのローカルで定義されたデザイン トークンと変数が解決されます。

  • バインディング: 変数(色や寸法など)を参照する SquooshView プロパティは、現在のフレームの具体的な値に置き換えられます。

1.6 レイアウトの計算

  • 動的レイアウト: DynamicLayout は、SquooshView ツリー内のすべてのノードの最終的な位置とサイズ(境界)を計算します。

  • テキスト レイアウト: TextHelper は、LayoutHelper トレイトの実装を使用して、テキストの指標、折り返し、シェイプを計算します。これにより、レンダリング前にテキストが制約内で正しく流れることを確認できます。

1.7 ダイヤルとゲージ

これは、自動車の UI 専用のステップです。

  • MeterData: ノードにメーターデータ(Figma で定義)がある場合、そのジオメトリは meter_value(車両速度など)に基づいて動的に変更されます。
    • 円弧: スイープ角度が調整されます。
    • 回転: 回転変換は、開始角度と終了角度に基づいて計算されます。
    • プログレス バー: 長方形の幅または高さがスケーリングされます。
    • プログレス ベクトル: ベクトルパスの長さが調整されます。

1.8 アニメーション

  • 差分: 現在の SquooshViewPreRenderCacheprevious_squoosh_view と比較されます。

  • 補間: プロパティが変更された場合、Squoosh は補間器を作成して、値(不透明度や変換など)を時間とともにスムーズに移行します。

フェーズ 2: コマンド生成

SquooshView ツリーが完全に解決され、アニメーション化されると、描画コマンドの線形シーケンスに変換されます。

このフェーズの主要コンポーネントは DisplayList クレートです。

  • generate_dl: この関数は、SquooshView ツリーを再帰的にトラバースします。

  • 翻訳:

    • 図形とパス: 適切な DisplayListAppearance バリアント(RectPath など)を使用して DisplayListEntry に変換
    • テキスト: TextHelper を使用してテキスト描画エントリに変換。
    • 変換とクリップ: 描画状態スタックを管理するために、PushTransform3DPopTransform3D または PushClipRegionPopClipRegion のペアに変換。
    • マスキング: PushMaskLayerPopMaskLayer のペアに変換して、レイヤを正しく作成してブレンド。

最終的な結果は、描画方法に関係なく、描画する内容 を記述する Vec<DisplayListEntry> のインスタンスです。

2.1 ルーパーへの引き渡し

DisplayList が生成されると、リデューサはそれを ViewDescriptor のインスタンスでラップし、Rust MPSC チャネル(LooperMessage)を介してルーパー スレッドに送信します。Looper はレンダリング フェーズと表示フェーズを担当するため、リデューサ スレッドがグラフィック パイプラインをブロックすることはありません。

フェーズ 3: レンダリング

プラットフォームに依存しない DisplayList はレンダリング バックエンドに渡され、抽象コマンドが GPU 命令に変換されます。

HAR は、元々 Flutter 用に構築されたレンダリング エンジンである Impeller を使用します。Impeller は、ビルド時に小規模で効率的なシェーダーセットをプリコンパイルすることで、シェーダー コンパイルによるフレームレートのグリッチの問題を解決するように設計されています。このアプローチは、効果的なバッチ処理と高度に最適化されたバックエンドと組み合わせることで、次のことを実現します。

  • 決定論的なパフォーマンス: ランタイム シェーダー コンパイルのグリッチをほぼ解消します。
  • 高速起動: 初期化のオーバーヘッドを削減します。
  • フットプリントの縮小: コンパクトなバイナリサイズを生成します。

Impeller のアーキテクチャの詳細については、[Introducing Impeller - Flutter's new rendering engine][impeller-video] をご覧ください。 動画では Flutter について説明していますが、これらのコアのメリットは HAR 自動車スタックに直接適用されます。

レンダリング フェーズの主要コンポーネントは次のとおりです。

  • ImpellerRenderer: プリレンダリング フェーズの表示リストを Impeller レンダリング コマンドに変換します。

  • Impeller Rust API: Rust で使用するために Impeller ライブラリをラップします (impeller クレートと impeller-rs-bindgen クレート)。

  • TypographyContext: フォントの登録とテキストのシェイプを管理します。

impeller-video

3.1 初期化とサーフェス管理

  • コンテキストの作成: レンダラは、OpenGL ES バックエンドを使用して impeller::Context のインスタンスを初期化し、プラットフォームの GL コンテキストから OpenGL ES 関数ポインタを解決するコールバックを渡します。

  • ラップされた FBO サーフェス: Impeller は独自のウィンドウを作成する代わりに、フェーズ 4 で提供される既存の OpenGL フレームバッファ オブジェクト(FBO)にレンダリングします。これを行うには、Surface::create_wrapped_fbo を呼び出します。

3.2 リソース管理

  • 画像: 標準形式と KTX2 圧縮テクスチャをサポートしています。これらは GPU テクスチャにアップロードされ、内部の Resources 構造体によって管理されます。

  • フォント: TrueType フォントと OpenType フォントが読み込まれ、テキスト レンダリング用に TypographyContext に登録されます。

  • 外部画像: 外部テクスチャ(カメラフィードや外部 3D レンダラなど)の特別な処理では、ゼロコピー レンダリングのために EGLImage インスタンスまたは外部 OpenGL テクスチャを Impeller Texture オブジェクトにバインドします。

3.3 レンダリング パス

render ループは、DisplayListBuilder を使用して Impeller DisplayList インスタンスを構築します(プリレンダリング フェーズで生成される Vec<DisplayListEntry> と混同しないでください)。

  1. バッファをクリアし、DPI スケーリングとディスプレイの回転のグローバル変換を適用します。

  2. 入力 DisplayListEntry アイテムを反復処理します。

    • 状態: save()restore() を使用して、変換とクリップ領域をプッシュおよびポップします。
    • プリミティブ: RectRoundedRect は標準のペイント オペレーションを使用して描画されます。
    • パス: 複雑なベクトルパス(動的な Arc インスタンスを含む)が構築されて描画されます。
    • テキスト: TextStyledTextTypographyContext を使用してレンダリングされます。
    • 画像: 標準画像と外部画像は draw_texture_rect を使用して描画されます。
  3. 構築された Impeller 表示リストを surface.draw_display_list() を使用してサーフェスに送信し、基盤となる GL コマンドを生成します。

  4. 基盤となるコンテキストで swap_buffers() を呼び出して、フェーズ 4 をトリガーします。

フェーズ 4: プレゼンテーション

この最終フェーズでは、レンダリングされたフレームを表示するためにディスプレイ ハードウェアとのやり取りを処理します。HAR は、Android Automotive OS(AAOS)Software-Defined Vehicle(SDV)で堅牢な直接レンダリング パスを使用します。

このフェーズの主要コンポーネントは HarDirectRenderingContexthar-gl-context クレート内)です。

4.1 アーキテクチャ

プレゼンテーション レイヤは、オフスクリーン描画ターゲットを使用したダブル バッファリング アプローチを使用します。

  1. 描画バッファ: Impeller がシーンをレンダリングするオフスクリーン FBO。

  2. 解決バッファ(省略可): マルチサンプル アンチエイリアシング(MSAA)をサポートする補助バッファ(省略可)

    • これは、基盤となる OpenGL ES 実装または構成で必要な場合に有効にできます。このような場合、レンダリング バッファにブリット(ビットブロック転送)する前に、マルチサンプリングされた描画バッファを解決するための中間ターゲットとして機能します。
  3. レンダリング バッファ: GBM オブジェクトによってバックアップされる汎用バッファ。これは、一般的なグラフィック スワップチェーンのバックバッファに対応します。

  4. フロント バッファ: ディスプレイにスキャンアウトされる GBM バッファ。

4.2 スワップチェーン

swap_buffers が呼び出されると、HAR は次の手順を行います。

  1. 描画バッファの内容をレンダリング バッファにブリットします(実装で必要な場合は、解決バッファに中間ブリットします)。

  2. GL コンテキストで glFlush() を呼び出し、GPU の完了を追跡する EGL_SYNC_NATIVE_FENCE_ANDROID のインスタンスを作成します。

  3. レンダリング バッファを画面にスワップする DRM アトミック リクエストを構築します。このリクエストには、GPU フェンス FD(インフェンスと呼ばれます)が含まれており、GPU の描画が完了する前にディスプレイ コントローラがレンダリング バッファを表示しないようにします。

  4. 同時に、DRM から新しいフェンス(アウトフェンスと呼ばれます)をリクエストして、前のバッファ(前のフレームのフロント バッファ)が画面に表示されなくなったタイミングを通知します。

  5. ノンブロッキング フラグを使用してアトミック リクエストをコミットし、グラフィック サブシステムが同期されたままメインスレッドが続行できるようにします。

  6. 新しいアウトフェンスをコンテキストに保存して、後続のフレームの swap_buffers プロセスの開始時に HAR がシグナルを待機できるようにします。 これにより、GPU がまだ表示されているバッファに描画されるのを防ぎます。

4.3 ダイレクト モードの設定

HAR は、DRM サブシステムと Kernel Mode Setting(KMS)サブシステムを使用してカーネルと直接やり取りし、ディスプレイの解像度 AAOS SDV を構成します。SurfaceFlinger などのウィンドウ マネージャーとのやり取りをバイパスすることで(特定の構成)、ディスプレイ ハードウェアを排他的かつ優先的に制御できます。

4.4 外部レンダリング

HAR は、特定の UI 要素(Figma のタグで識別)のレンダリングを外部プロセスまたはスレッドに委任することをサポートしています。これは、複雑な 3D シーン(Kanzi や Unity などのエンジンからのエゴカーのビジュアリゼーションなど)や、専用の OpenGL コンテキストを必要とするその他のコンテンツを統合する場合に便利です。

4.4.1 主要コンポーネント

  • HarExternalRenderContext: 外部サービス専用のオフスクリーン EGL コンテキスト。
  • SurfacePool: ダブル バッファリングまたはトリプル バッファリング用の LocalSurfaceTextureEGLImage)バッファのセットを管理します。
  • SharedSurfaceExternalImage: 外部サービスとメイン レンダラの間で EGLImage ハンドルを渡すためのスレッドセーフなラッパー。

4.4.2 ワークフロー

ワークフローは次の順序で実行されます。

  1. 外部サービスが起動し、メインルーパーに登録して、レンダリングする Figma タグ(#cluster/3d-car など)を識別します。

  2. サービスは、ルーパーからの RenderStart シグナルを待機して、レンダリングをディスプレイの VSYNC シグナルに合わせます。

  3. オフスクリーンで、サービスは SurfacePool によって提供されるフレームバッファにコンテンツをレンダリングします。

  4. サービスはコンテキストで swap_buffers を呼び出します。これにより、プールがローテーションされ、完了したフレームが SharedSurface のインスタンスとして使用できるようになります。

  5. SharedSurfaceExternalImage でラップされ、Rust MPSC チャネルを介してルーパーに送信されます。

  6. メインの Impeller レンダラ(フェーズ 3)が外部画像を受け取ります。ピクセルデータをコピーする代わりに、基盤となる EGLImage をテクスチャに直接バインドして、メインシーンの一部として描画し、ゼロコピー合成を実現します。

4.5 開発プラットフォームとテストプラットフォーム(har-platform-linux)

開発とテストを目的として、HAR アプリは標準の Linux デスクトップ環境とヘッドレス セットアップをターゲットにできます。これらのプラットフォームは、crates/reference/platforms/har-platform-linux クレートに実装されています。

本番環境の AAOS SDV ターゲットとは異なり、これらのプラットフォームでは、ディスプレイ出力に har-gl-contextdirect-rendering サブシステムを使用しません。代わりに、標準の Rust OpenGL クレートを使用します。

  • ウィンドウ モード: ウィンドウ管理とイベント ループに winit を使用し、OpenGL ES コンテキストの作成とウィンドウ システムとの統合に glutin を使用します。

  • ヘッドレス モード: har-gl-context クレートを使用して、デフォルトの EGL ディスプレイでオフスクリーン pbuffer コンテキストを作成します。これにより、表示ウィンドウや直接ディスプレイ ハードウェア アクセスを必要とせずにオフスクリーン バッファにレンダリングできます。これは主に自動テストやバックエンド処理に使用されます。