はじめに
ソフトウェアエンジニアの福永です。
ラズパイをサイネージや動画プレイヤーとして使用していると、現在再生している内容をキャプチャしたいことがままあると思います。
そういった際、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で実装できたため、ハードウェアが異なる環境でも同様のコードが動くと思われます。
以下は自分の理解でのスタック図です。

サンプルコード
https://github.com/cerevo/capture_from_drm
具体的な処理の流れ
1. DRMデバイスを開く
/dev/dri/card<n>
がドライバへのデバイスパスです。
VideoCore IV以前の場合は、card0
がvc4
ドライバへのパスですが、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を取得する必要があります。
ざっと調べた感じ
- CRTCから取得する
- plane(各描画データ)一覧から取得する
という二つの方法があるようでした。
今回はplane一覧から取得しましたが、複数の映像出力端子を使用する場合はConnector経由でCRTCからとった方が良さそうです。
具体的な手順は、
drmSetClientCap(drmFd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1)
- Primary属性(実際に端子から出力される映像そのもののPlanes)やCursor属性 (文字通りポインタデバイスによるカーソルの描画要素) を含めて取得できるようにする操作
drmModeGetPlaneResources
- plane一覧を取得
- ↑で取得した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
と同じ添え字のpitches
とoffsets
を使用してユーザ空間へマッピングします。
が、その前に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
次第)
後はファイルに書き出すなりなんなりお好きにどうぞ。
以下がサンプルコードでキャプチャした画像と、実際の表示内容の比較です。


綺麗にキャプチャすることが出来ています。
おまけ
キャプチャデータはRGBA8888で扱いたかったのですが、VLC(というかVideo CoreIV?)はYU12(YUV420)で出力するらしく、かつピクセルフォーマットは変更できなさそうでした。
また、今回フルスケールのデータは必要なく、節約のためダウンスケールしたかったのですが、DispmanXでは可能だったダウンスケールしつつのキャプチャもDRMでは出来なさそうだったため、別途フォーマット変換とダウンスケールをopenGL ESで行いました。
すると、データフローが、
- GPUのメモリをユーザ空間へマッピング
- それをGPUへ送って変換する
- 変換後のデータをGPUから取得
という、なんとも気持ち悪い形になってしまいました。
何か良い方法を知っている方がいらっしゃったらご教授いただけると幸いです。