Linux kernel のDRM APIを使用して出力映像をキャプチャする方法

DRMのスタック図

はじめに

ソフトウェアエンジニアの福永です。
ラズパイをサイネージや動画プレイヤーとして使用していると、現在再生している内容をキャプチャしたいことがままあると思います。
そういった際、32bitOSではDispmanX APIでキャプチャを行っていましたが、64bitのraspberry pi OSでは使用できません。

この記事は、その代替案の一つである、linux kernelのDRMによるキャプチャを試した時の備忘録です。
あくまで、DRM APIを使用して、出力映像をキャプチャする手順を示すことを目的としています。
そのため、使用する技術の詳しい説明は外部にぶん投げてます。あしからず。

環境

ハードウェア

  • raspberry pi zero 2 w
  • 1280 x 1024 ディスプレイ
    • HDMI接続

OS

  • 2024-07-04-raspios-bookworm-arm64

ライブラリ

  • sudo apt install libdrm-dev libdrm-test

DRMとは

Direct Rendering Managerの略。GPU機能の抽象化をしているkernel subsystemです。
詳しくは以下の資料が分かりやすいです。
https://bootlin.com/pub/conferences/2017/kr/ripard-drm/ripard-drm.pdf

raspberry pi では、/boot/firmware/config.txtにてdtoverlay=vc4-kms-v3dを記述して再起動することで、DRM/KMSが動作するようになります。

libdrmはドライバのwrapperで、ハードウェアに合わせたものがインストールされます。
キャプチャはハードウェア共通のDRM APIで実装できたため、ハードウェアが異なる環境でも同様のコードが動くと思われます。

以下は自分の理解でのスタック図です。

fig.1 DRM絡みのスタック図

サンプルコード

https://github.com/cerevo/capture_from_drm

具体的な処理の流れ

1. DRMデバイスを開く

/dev/dri/card<n>がドライバへのデバイスパスです。

VideoCore IV以前の場合は、card0vc4ドライバへのパスですが、VideoCore VI以降ではv3dドライバのデバイスパスとなっています。
VideoCore VI以降はcard1を指定しましょう。

libdrm-testに含まれるmodetestを使用すると、任意のデバイスパスにアタッチしているドライバがDRMに対応しているかを調べることができます。
まず、引数となるbus_idを以下のコマンドで確認します。
cat /sys/kerne/debug/dri/<card_num>/name
次に、modetestを-Dでbus_idを指定して実行します。
modetest -D <bus_id>
このコマンドによって、使用可能な解像度や現在のplane一覧が取得出来たら、そのドライバはDRMの操作に対応しています。
また、使用するGPUのドライバが判明している場合は、-Mオプションで直接指定することも可能です。
e.g. modetest -M vc4

2. フレームバッファIDを取得

まず、出力映像のフレームバッファ(/dev/fb<n>とは別物の、GPUにあるメモリ空間)のIDを取得する必要があります。
ざっと調べた感じ

  1. CRTCから取得する
  2. plane(各描画データ)一覧から取得する

という二つの方法があるようでした。
今回はplane一覧から取得しましたが、複数の映像出力端子を使用する場合はConnector経由でCRTCからとった方が良さそうです。

具体的な手順は、

  1. drmSetClientCap(drmFd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1)
    • Primary属性(実際に端子から出力される映像そのもののPlanes)やCursor属性 (文字通りポインタデバイスによるカーソルの描画要素) を含めて取得できるようにする操作
  2. drmModeGetPlaneResources
    • plane一覧を取得
  3. ↑で取得したplane毎のidからdrmModeGetPlane
    • plane本体のメタデータを取得する。

となります。

3で取得したplaneのメタデータの中にフレームバッファIDが格納されています。

3. フレームバッファIDからGEMのhandleを取得

drmModeGetFB2でフレームバッファIDからGEMのhandleを取得します。
GEMは Graphics Execution Managerの略。メモリ管理してるっぽいです。
https://docs.kernel.org/gpu/drm-mm.html

0ではないhandlesを探し、そのhandlesと同じ添え字のpitchesoffsetsを使用してユーザ空間へマッピングします。
が、その前にhandlesからマッピング先を示すfdを取得する必要があります。
(マッピング処理は5項で行います)

4. GEM handleからGPUのメモリを指すfdを取得

DRM_IOCTL_PRIME_HANDLE_TO_FDでGEM handleからPRIMEによる共有バッファのfdを取得できます。
PRIMEはdma-bufをベースにしたデバイス間バッファ共有システムの一種らしいです。
https://docs.kernel.org/gpu/drm-mm.html#prime-buffer-sharing

(この辺で用語の多さに辟易し始めました)

ここで取得したfdをmmapすると対象フレームバッファがユーザ空間に展開されます。

5. 取得fdをmmapしてユーザ空間へ展開

これでユーザー空間にてメモリを扱えるようになります。
色空間やバッファ構造、サイズに関してはdrmModeGetFB2で取得した構造体(struct drm_mode_fb_cmd2)に入っています。
mmapのoffsetも同一の構造体にあるoffsetsで、sizeはpitches * height / nで求められます。(nはpixel_format次第)
後はファイルに書き出すなりなんなりお好きにどうぞ。

以下がサンプルコードでキャプチャした画像と、実際の表示内容の比較です。

img.1 DRMでキャプチャした画像
img.2 ディスプレイに表示されている内容

綺麗にキャプチャすることが出来ています。

おまけ

キャプチャデータはRGBA8888で扱いたかったのですが、VLC(というかVideo CoreIV?)はYU12(YUV420)で出力するらしく、かつピクセルフォーマットは変更できなさそうでした。
また、今回フルスケールのデータは必要なく、節約のためダウンスケールしたかったのですが、DispmanXでは可能だったダウンスケールしつつのキャプチャもDRMでは出来なさそうだったため、別途フォーマット変換とダウンスケールをopenGL ESで行いました。
すると、データフローが、

  1. GPUのメモリをユーザ空間へマッピング
  2. それをGPUへ送って変換する
  3. 変換後のデータをGPUから取得

という、なんとも気持ち悪い形になってしまいました。
何か良い方法を知っている方がいらっしゃったらご教授いただけると幸いです。

参考文献

Back To Top
© Cerevo Inc.