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 ドキュメント内の特定の名前付きノードに明示的にバインドします。これにより、アプリの状態によって視覚要素のプロパティが自動的に制御されます。

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

  • Reducer: 中央のイベントループとして機能し、アクションを処理して現在の状態を更新します。フレームワークは DefaultReducer を提供しますが、必要に応じてカスタム リデューサー実装を提供できます。
  • Presenter: 現在の状態を UI モデルにブリッジします。Presenter トレイトは harry フレームワーク クレートで指定され、参照実装(UIModelPresenter)は harry-app-core クレートで提供されます。
  • UI モデル: 現在の状態に基づいてカスタマイズを生成します。UI モデルコードは、derive_customizations クレートによって提供される DesignDocument マクロを使用して生成されます。harry-app-core クレートの UIModel 構造体は、この例を示しています。
  • Squoosh: デザインに沿って UI をレンダリングするために使用される SquooshView データ構造とバリアント リポジトリを提供します。シリアル化された設計ドキュメントは、DesignCompose ライブラリから dc_bundle クレートによって読み込まれ、効率的なランタイム パフォーマンスを実現するために 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 は、直ちに更新する必要がないことを示します。このような場合、Reducer は新しい表示リストの生成を停止し、新しいアクションまたはイベントによって変更がトリガーされるまで、不要なレンダリング サイクルを回避してリソースを節約します。

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

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

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

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

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

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

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

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

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

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

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. surface.draw_display_list() を使用して、ビルドされた Impeller 表示リストをサーフェスに送信し、基盤となる GL コマンドを生成します。

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

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

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

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

4.1 アーキテクチャ

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

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

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

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

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

4.2 スワップ チェーン

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

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

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

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

  4. 前のバッファ(前のフレームのフロント バッファ)が画面に表示されなくなったときにシグナルを送信するために、DRM から新しいフェンス(アウトフェンス)を同時にリクエストします。

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

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

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

HAR は、DRM とカーネルモード設定(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 コンテキストを作成します。これにより、表示可能なウィンドウやディスプレイ ハードウェアへの直接アクセスを必要とせずに、オフスクリーン バッファへのレンダリングが可能になります。これは主に、自動テストやバックエンド処理に使用されます。