DRMとグラフィックス

このページの目次

フレームがレンダリングされても、画面に表示されるとは限りません。dma-bufのインポートが成功しても、それが直接スキャンアウトできることを意味しません。DRMはレンダリング、バッファ共有、表示コミットを別々に扱う必要があります。モニターがないデバイスでも、レンダリング機能を提供する可能性があります。

参照目標読書位置
ユーザー/カーネルの責務とKMS第1~2節
コミット完了とオブジェクトハンドル第3~4節
共有同期と黒画面の特定第5~6節

1. ユーザー空間とカーネル空間の役割分担

ユーザー空間ドライバ(UMD、例えばMesa内のOpenGL/Vulkan実装)は、グラフィックスAPI、シェーダーコンパイル、コマンドの構成を処理します。カーネル空間ドライバ(KMD、例えばamdgpu、i915、xe、nouveau)は、デバイスアクセス、バッファ管理、アドレス空間、スケジューリング、同期などのメカニズムを提供します。表示をサポートするドライバはKMSも実装します。

これは責務の分離であり、「カーネルがコマンドを全く検証しない」という意味ではありません。検証、分離、コミットインターフェースはハードウェアとドライバによって異なります。UMDも、カーネルがサポートするUAPIとハードウェア能力に適合する必要があります。ユーザー空間をアップグレードしても、欠けているカーネル機能が自動的に追加されるわけではありません。

デバイスノード一般的な用途権限の意味
/dev/dri/cardNプライマリノード、表示制御インターフェースを含むmodesetなどの操作は、DRM master、leaseなどのルールに従う
/dev/dri/renderDNレンダリングおよび関連リソース操作modesettingを提供しない;ファイル権限とセキュリティポリシーに従う

card0は列挙名であり、デスクトップで現在使用されている「メインGPU」であることは保証されません。renderノードはDRM masterを取得する必要はありませんが、すべてのユーザーが天然にアクセスできるわけではありません。具体的なインターフェースについては、DRMユーザー空間APIを参照してください。

2. KMS:表示リソースとその接続の記述

レンダリング完了と表示完了は、2つの異なる引き継ぎである アプリケーション / UMD GPUレンダリングをコミット 共有バッファ dma-bufと同期 コンポザ / KMS 合成または直接スキャンアウトを選択 Plane CRTC Encoder Connector 表示パス:バッファはPlaneの画像ソースとして機能する 古いバッファは、すべてのユーザーがアクセスを終了するまで再利用できない 共有メモリは、フォーマット互換性、レンダリング待機、表示解放、アクセス権限の問題を自動的に解決しない。
  • Plane:画像レイヤーを提供し、framebuffer、ソースクロップ、ターゲット位置を含む。タイプにはprimary、overlay、cursorがあり、カーソルを一律にoverlayに分類すべきではない。
  • CRTC:スキャンタイミングと関連プレーンを管理し、画像をモードに従って出力する。この歴史的な名称は、現代のモニターが陰極線管(CRT)を使用していることを意味しない。
  • Encoder:出力エンコーディングとルーティングリソースを抽象化する。
  • Connector:表示端末への接続を表現し、接続状態、モード、EDIDなどの情報を報告できる。

接続はハードウェアリソースの制約を受けます。Planeが任意のCRTCに接続できるとは限らず、表示帯域幅とスケーリング能力にも制限があります。ハードウェアがサポートする場合、1つのCRTCを複数の出力にクロックすることができます。CRTCとモニターを単純に1対1で対応させることはできません。

Framebufferは画像レイアウトオブジェクトであり、メモリ割り当てそのものではありません。 サイズ、ピクセルフォーマット、ピッチ、オフセット、モディファイア、および基盤となるバッファの関連を記述します。2つのバッファのバイト数が同じでも、フォーマットやモディファイアが異なる場合、交換可能ではありません。オブジェクトと属性の詳細については、KMSドキュメントを参照してください。

リニア画像でのピッチとオフセットの計算

3×2のリニアXRGB8888画像を取り上げます。各ピクセルは4バイトを占め、行間隔(ピッチ)が16バイトであると仮定します。画像は基盤オブジェクトのoffset=8から始まります。各行の実際のピクセルは12バイトを占めますが、次の行に進むには16バイト進みます。ピクセル(x, y)の開始位置はoffset + y × pitch + x × 4であり、最後のピクセル(2, 1)はバイト32~35に位置し、6つのピクセルが8~31に密接に並ぶわけではありません。

区間(左閉右開)内容
[0, 8)この画像以前のオブジェクト空間
[8, 20)第0行の3つのピクセル
[20, 24)行間パディング
[24, 36)第1行の3つのピクセル
def pixel_span(width, height, pitch, offset, object_size, x, y):
    if min(width, height) <= 0 or offset < 0 or pitch < width * 4:
        raise ValueError("無効なリニアレイアウト")
    end = offset + (height - 1) * pitch + width * 4
    if end > object_size or not (0 <= x < width and 0 <= y < height):
        raise ValueError("ピクセルまたは画像が範囲外")
    start = offset + y * pitch + x * 4
    return start, start + 4

assert pixel_span(3, 2, 16, 8, 36, 2, 1) == (32, 36)
assert pixel_span(3, 2, 16, 8, 36, 0, 1) == (24, 28)
for args in [(3, 2, 16, 8, 35, 2, 1), (3, 2, 8, 8, 36, 0, 0),
             (3, 2, 16, 8, 36, 3, 0), (3, 2, 16, 8, 36, 0, 2)]:
    try:
        pixel_span(*args)
    except ValueError:
        pass
    else:
        raise AssertionError("無効なレイアウトまたは座標は拒否されるべき")

ここでの 36 バイトは、最後の有効な画素までを含むための数学的な下限にすぎない。実際の割り当てには、ドライバのアラインメント、ページサイズ、インターフェースの要件も影響するため、返された pitch と size を使用する。Python の整数はオーバーフローしないが、式を C に移す場合は乗算と加算のオーバーフローも検査する必要がある。

この36バイトは、最後の有効ピクセルを覆うための数学的な下限に過ぎず、実際の割り当てはドライバのアライメント、ページサイズ、インターフェース要件の影響を受けるため、インターフェースが返すピッチとサイズを使用する必要があります。Pythonの整数はオーバーフローしません。Cに数式を持ち込む際には、乗算と加算のオーバーフローもチェックする必要があります。

このアドレス指定は、仮定されたリニアで単一フォーマットプレーンのレイアウトにのみ適用されます。タイル化や圧縮モディファイアを持つストレージは、この方法でアドレス指定できません。NV12などのマルチプレーンピクセルフォーマットでは、各プレーンのピッチとオフセットを個別に記述する必要があります。⁠ピクセルフォーマット内のplaneとKMS表示オーバーレイのPlaneは異なる概念です。 1つのKMS Planeは、複数のフォーマットプレーンで構成される画像をスキャンアウトできます。

3. Atomic modesetting:状態のセットを事前に検証する

ユーザー空間はatomic ioctlを通じて、framebufferをplaneに配置し、CRTCのモードとconnectorのルーティングを設定するなど、オブジェクト属性のセットをコミットします。カーネルは関連オブジェクトの状態を構築し、リソース、フォーマット、帯域幅、およびドライバの制約をチェックします。drm_atomic_stateはカーネル内部構造体であり、ユーザー空間が直接割り当てて渡す汎用C構造体ではありません。

TEST_ONLYは表示状態を変更せずに検証のみを実行できますが、テストが成功しても、将来のコミットに対してリソースが予約されるわけではありません。実際のコミット時には、その時点の条件を再チェックする必要があります。

atomicの価値は、ユーザー空間がオブジェクトを1つずつ変更し、利用できない中間状態を自分で処理する必要がないことです。すべてのハードウェアレジスタが1つのCPU命令で変更されることを約束するものではなく、モード変更操作が完全に黒屏なしで実行されることも保証しません。複数のCRTCが関与する場合、すべてのモニターが物理的に同時にリフレッシュされるとは限りません。

なぜ個々のオブジェクトだけでなく、組み合わせを検証しなければならないのか

2つの表示パイプラインが教育用の帯域幅予算10を共有していると仮定します。それぞれが現在4を消費しています。Aを6に上げる個別テストでは、合計は6+4=10です。Bを6に上げる個別テストも通過します。しかし、両方を同時に6に上げる組み合わせには12が必要であり、拒否されなければなりません。個別に通過したローカル設定から、有効なグローバル設定を組み立てることはできません。

同様に、Aのアップグレードテストが通過した後、Bが先にアップグレードをコミットした場合、後にAを実際にコミットする際には、新しい状態4+6に対応しなければなりません。TEST_ONLYの意義は候補状態をチェックすることであり、残りのリソースを予約することではありません。実際のドライバは、クロック、ルーティング、スケーラ、リンクなどの条件もチェックします。ここでの単一の数字は共有制約を理解するのに役立ちますが、特定のGPUの利用可能な表示帯域幅を計算するために使用することはできません。

成功の返却と画面の表示

ブロッキングコミットとノンブロッキングコミットの待機動作は異なります。ノンブロッキングコミットが受け入れられた後、ハードウェアの作業は継続できるため、イベントや出力フェンスなどのメカニズムを通じて完了を判断する必要があります。入力フェンスは、新しいバッファのレンダリング依存関係を示すことができます。

通常のページフラップは、ティアリングを避けるためにvblankと組み合わせて使用されることが多いですが、非同期更新、完全なmodeset、および異なるドライバパスにはそれぞれ制約があり、「検証後、vblankでティアリングなしの原子適用」と一律に記述することはできません。古いframebufferの再利用は、対応する完了と解放プロトコルに従う必要があり、ioctlがすでに返されたからといって解放されるわけではありません。

新しいフレームの表示開始が、新しいフレームの即時オーバーレイを意味しない

CRTC Aが現在バッファXをスキャンしており、Yにページフラップする準備ができているとします。Yの入力フェンスが完了したことは、レンダラーがYの書き込みを終了したことを意味します。Aのページフラップ完了は、この表示切替が完了し、A上でのXの古いスキャン使用を終了できることを意味します。この時点でYは現在のスキャンオブジェクトであり、解放されたばかりのオブジェクトではありません。CRTC BがまだXをスキャンしている場合、Aがページフラップを完了したからといって、Xを生産者に返してオーバーレイすることはできません。

AはYに切り替わったが、Xは他のパイプラインで使用されている可能性がある 初期状態 A → X;B → X X:スキャン中 Aのページフラップ完了 A → Y;B → X X:依然としてオーバーレイ不可 Bのページフラップ完了 A → Y;B → Z X:スキャン使用なし YはA上で現在のフレームとなり、後続の置換を待って回収される必要がある スキャン使用のみを描画;他の消費者も完了する必要がある。バッファは安全に再利用できる。

以下では、「使用」を(CRTC, buffer)の関係として記録し、各CRTCにスキャンレイヤーが1つだけあり、同時に待機中のページフラップが最大1つのみという通常のページフラップをシミュレートします。完了通知は所属するCRTCごとに処理され、2つの画面が同時に完了すると仮定しません。

class Scanout:
    def __init__(self):
        self.current = {"A": "X", "B": "X"}
        self.pending = {}
        self.ready = {"X"}

    def render_done(self, buffer):
        self.ready.add(buffer)

    def submit(self, crtc, new):
        assert crtc in self.current and crtc not in self.pending
        self.pending[crtc] = new           # リクエストの受け入れは切替完了を意味しない

    def complete_flip(self, crtc):
        new = self.pending[crtc]
        if new not in self.ready:
            raise RuntimeError("入力レンダリング依存関係がまだ完了していない")
        self.current[crtc] = new
        del self.pending[crtc]

    def reusable(self, buffer):
        return buffer not in self.current.values() and buffer not in self.pending.values()

s = Scanout()
s.submit("A", "Y")
assert not s.reusable("X") and not s.reusable("Y")
try:
    s.complete_flip("A")
except RuntimeError:
    pass
else:
    raise AssertionError("入力依存関係を越えてはならない")
s.render_done("Y")
s.complete_flip("A")
assert not s.reusable("X")                 # BはまだXを使用中
assert not s.reusable("Y")                 # AはYをスキャン中
s.render_done("Z")
s.submit("B", "Z")
assert not s.reusable("X")                 # ioctlの受け取りだけでは解放不十分
s.complete_flip("B")
assert s.reusable("X")
assert not s.reusable("Y") and not s.reusable("Z")

モデルにおける再利用可能とは、スキャンまたは待機中の使用が残っていないことを示すだけで、実際のシステムでは、GPU、ビデオエンコーダなどの他の消費者の解放条件も統合する必要があります。OUT_FENCE_PTRはCRTCに属しており、任意のdma-bufのグローバルな「誰も使用していない」証明と解釈することはできません。writebackの出力フェンスは、書き戻しターゲットへの書き込み完了を示し、対応する属性プロトコルに従って解釈する必要があります。

A/Bバッファを選択し、直接レンダリング、コミット、またはvblankを進めます。画面は、キューに入れられたフレームがフェンスと提示条件を満たすまで、現在のスキャン内容を保持します。TEST_ONLYは画面を勝手に変更しません。

図を準備しています
レンダリング完了、コミット受け入れ、画面表示は3つの異なる時刻

バッファが存在しても、レンダリングが完了したことを意味しません。atomic ioctlの成功も、表示されたことを意味しません。古いscanout参照が解放された後、古いバッファのオーバーレイが許可されます。

4. GEM:オブジェクト、ハンドル、および実際のストレージ

GEMは、グラフィックスバッファオブジェクトとその関連するライフサイクルフレームワークを提供します。割り当て方法はドライバと具体的なインターフェースによって決定され、すべてのGPU割り当てを表すために普遍的に呼び出せるdrm_gem_object_create(dev, size)は存在しません。

オブジェクトのストレージはシステムメモリまたはVRAMから来ることがあります。TTMなどのメカニズムは配置とマイグレーションを管理できますが、すべてのGEMドライバがTTMを使用しなければならないわけではありません。CPUマッピング、GPUマッピング、および表示スキャン能力はそれぞれ個別に確立される能力であり、オブジェクトを作成した後に自然にすべてが利用可能になるわけではありません。

プロセス間共有:整数ハンドルをコピーするのではなく、参照を渡す

GEMハンドルは通常、特定のDRMファイルコンテキストに属します。整数ハンドルを別の独立してDRMデバイスをオープンしたプロセスに送信しても、自動的に同じオブジェクトを指すわけではありません。プロセス間共有は、通常、dma-buf fdのエクスポート、およびUnixソケットのSCM_RIGHTSを介した渡しが一般的です。受信側は自分の参照を取得し、インターフェースに従ってインポートします。DRMメモリ管理を参照してください。

5. dma-buf:基盤ストレージの共有とアクセスの調整

例:アプリケーションがフレームをレンダリングし、コンポザがそのdma-bufを受け取り、それをテクスチャとして合成して出力バッファに合成するか、条件を満たす場合は直接表示planeにスキャンアウトするかを決定します。

2つのパスともにアプリケーションの基盤ストレージを共有できますが、後者のパスでは、画像フォーマット、モディファイア、スケーリング、位置、出力カラー処理などの制約がすべて満たされている必要があります。インポートが成功しても、直接スキャンアウトできることが保証されるわけではありません。デバイス間ではマイグレーションやコピーが必要な場合もあるため、dma-bufは無条件のゼロコピーを保証するものではありません。

引き継ぎ方向待つべきこと
生産者が新しいフレームを消費者に渡すGPUレンダリングが完了し、消費者が読み取り可能になるまで
消費者が古いフレームを生産者に返す表示または他の消費が完了し、生産者がオーバーレイ可能になるまで

参照はオブジェクトが存在し続けることを保証しますが、読み書きの間に競合がないことを証明するものではありません。

暗黙的同期は、reservationオブジェクトとフェンスを介して依存関係を追跡し、明示的同期は、API/プロトコルを介してフェンス、syncobjなどの同期オブジェクトを渡します。どのメカニズムを選択するかは、全体のパスによって異なります。CPUがdma-bufにアクセスするときのキャッシュ同期は、GPU作業の完了を待つことを代替できません。キャッシュの可視性とタスクの完了は異なる条件です。詳細はdma-bufドキュメントを参照してください。

6. fbdevとトラブルシューティングのエントリポイント

fbdevは従来のframebufferインターフェースを提供しますが、機能と一貫したリソースモデリング能力は限られています。歴史的には、アクセラレーションや垂直同期待機などのインターフェースが存在したため、「GPUアクセラレーションがない、vsyncもない」と絶対的に言うことはできません。最新のDRMドライバは、設定に応じてfbdev互換レイヤーを提供できますが、すべてのシステムが/dev/fb0を必ず公開するわけではありません。

問題を調査する際は、境界を特定して位置合わせしてください。アプリケーションがレンダリングを正常にコミットしたか、フェンスが完了したか、コンポザがバッファを受け取ったか、KMS設定が通過したか、connectorが接続しているかを確認します。黒屏をすべて「VRAMが割り当てられていない」せいにするのはやめましょう。

ノードと接続状態をリードオンリーで確認します:

ls -l /dev/dri/
from pathlib import Path

for status in sorted(Path('/sys/class/drm').glob('card*-*/status')):
    try:
        print(status.parent.name, status.read_text().strip())
    except OSError as exc:
        print(status.parent.name, type(exc).__name__)

connectedは接続検出状態を示すだけで、その出力が有効な画像をスキャンしていることを証明するものではありません。これらのディレクトリがないからといって、システムにGPUが全くないことを意味するわけではありません。デバイスはrender機能のみを提供している可能性があります。