2024年10月11日追記
この記事は2024年9月2日に初投稿しましたが、その時には Cache の同期について間違った情報を記載していました。そこであらためて CPU Cache との同期に関して内容を追加修正しました。
はじめに
V4L2 のストリーミングI/O(V4L2_MEMORY_MMAP) はV4L2 ストリーミング I/O の方式の一つで、V4L2 ドライバ内(カーネル内)で確保した V4L2 バッファを mmap 機構を使ってユーザー空間にマッピングすることで、ユーザープログラムが V4L2 バッファにアクセスできるようにします。V4L2バッファをユーザー空間から直接アクセス出来るため、この方式は比較的よく使われます。
しかし、ある種の V4L2 ドライバでは、mmap でユーザー空間にマッピングする際にキャッシュがオフになってしまってメモリアクセスが非常に遅くなり性能が出ない問題がありました。そのメカニズムに関しては次の記事で説明しています。
この記事では、この問題を解決する手段として dma-heap を使う方法を紹介します。簡単にまとめると次のようになります。
- V4L2 ストリーミングI/O にはユーザープログラムとデータをやりとりする方法に、DMA Buffer を mmap する方法以外に PRIME DMA-BUFs を使う方法があります。
- PRIME DMA-BUFs は 複数のデバイスが同じ DMA Buffer にアクセスし、操作できるようにするための Linux Kernel 内のインフラストラクチャです。
- dma-heap はユーザープログラムが PRIME DMA-BUFs に対して DMA Buffer を割り当てたり、管理したり、操作するためのインターフェースを提供するデバイスドライバです。
- dma-heap が提供する DMA Buffer の mmap は DMA Mapping API を使わないので、Cache Cohrence Hardware を持っていなくても CPU Cacheは無効になりません。
dma-heap とは
dma-heap はユーザープログラムが PRIME DMA-BUFs に対して DMA Buffer を割り当てたり、管理したり、操作するためのインターフェースを提供するデバイスドライバです。
PRIME DMA-BUFs とは
PRIME DMA-BUFs とは Linux Kernel 内部の DMA Buffer の共有 API の略称のことです。これは、異なるデバイスドライバ間で DMA Buffer を共有するためのインフラストラクチャを提供します。これにより、複数のデバイスが同じ DMA Buffer にアクセスし、操作できるようになります。
DMA Buffer の受け渡しには PRIME DMA-BUFs が提供するファイルディスクリプタが使われます。
dma-heap の機能
DMA Buffer の確保
ユーザープログラムは dma-heap に対して DMA Buffer の割り当てを要求することが出来ます。dma-heap は DMA Buffer の割り当てに成功すると、PRIME DMA-BUFs で受け渡し可能なファイルディスクリプタを返します。このファイルディスクリプタをデバイスドライバに渡すことで、ユーザープログラムは DMA Buffer をデバイスドライバと共有することが出来ます。
DMA Buffer の mmap
ユーザープログラムは、dma-heap が確保した DMA Buffer に対して、仮想アドレスを割り当てることが出来ます。仮想アドレスを割り当てるには、dma-heap が生成したファイルディスクリプタに対して mmap することで行います。
ここで重要なのが、 dma-heap で確保した DMA Buffer をユーザー空間に mmap する際は、Linux Kernel の DMA Mapping API を使用しないことです。次の記事で、Linux では Cache Coherence Hardware を持っていないとDMA Buffer をmmap する際に CPU Cache が無効になる問題を説明しました。
- 『Linux では Cache Coherence Hardware を持っていないとDMA Buffer をmmap する際に CPU Cache が無効になる』 @Qiita
- 『Raspberry Pi では DMA Buffer をmmap する際に CPU Cache が無効になる』@Qiita
実は、何故か dma-heap が提供する DMA Buffer の mmap は DMA Mapping API を使わないので、Cache Cohrence Hardware を持っていなくても CPU Cacheは無効になりません。この仕組みを利用して、V4L2 ストリーミングI/Oで性能が出ない問題を dma-heap で解決することが出来ます。
dma-heap の使用例
デバイスファイルを開く
ユーザープログラムからは、 /dev/dma-heap/ にあるデバイスファイルを通じて dma-heap にアクセスすることが出来ます。
Linux Kernel 6.1 には /dev/dma-heap/system と /dev/dma-heap/reserved があります。/dev/dma-heap/reserved は CMA(Contiguous Memory Allocator) を使って DMA Buffer を確保します。
int dma_heap_fd = open("/dev/dma-heap/reserved", O_RDWR);
if (dma_heap_fd == -1) {
perror("/dev/dma-heap/reserved");
eturn EXIT_FAILURE;
}
DMA Buffer を要求する
DMA Buffer の要求は ioctl() を使います。ioctl() のコマンドは DMA_HEAP_IOCTL_ALLOC です。引数として struct dma_heap_allocation_data を使います。struct dma_heap_allocation_data には DMA Buffer のサイズや各種フラグを設定しておきます。 dma-heap は DMA Buffer の確保に成功すると、struct dma_heap_allocation_data の fd フィールドに PRIME DMA-BUFs のファイルディスクリプタを設定します。
struct dma_heap_allocation_data alloc_data = {
.len = dma_heap_size,
.fd = 0,
.fd_flags = (O_RDWR | O_CLOEXEC),
.heap_flags = 0,
};
if (ioctl(dma_heap_fd, DMA_HEAP_IOCTL_ALLOC, &alloc_data)) {
perror("DMA_HEAP_IOCTL_ALLOC");
return EXIT_FAILURE;
}
ユーザー空間に mmap する
前節の ioctl で得たファイルディスクリプタを使って mmap() します。
dma_heap_start = mmap(NULL,
dma_heap_size,
PROT_READ | PROT_WRITE,
MAP_SHARED,
alloc_data.fd,
0);
if (dma_heap_start == MAP_FAILED) {
perror("mmap");
return EXIT_FAILURE;
}
これで DMA Buffer がユーザー空間からアクセス出来るようになったので、必要な処理を行います。
CPU Cache との同期 (2024年10月11日追記)
CPU が DMA Buffer にアクセスする際、 CPU Cache との同期をとる必要がある場合があります。その場合は、CPU が DMA Buffer をアクセスする前後に dma-heap から得た DMA Buffer を示すファイルディスクリプタに対して DMA_BUF_IOCTL_SYNC を発行します。
CPU が DMA Buffer にアクセスする前には、引数のフラグに DMA_BUF_SYNC_START を指定して DMA_BUF_IOCTL_SYNC を発行します。このフラグは、これから CPU がアクセスすることを DMA Buffer に通知してCPU Cache との同期を指示するものです。
また、フラグには同時に DMA Buffer へのアクセス方向を指定します。CPUから読み出し専用の場合は DMA_BUF_SYNC_READ、CPUから書き込み専用の場合は DMA_BUF_SYNC_WRITE、CPUから読み出しと書き込みどちらも行う場合は DMA_BUF_SYNC_RW を指定します。
struct dma_buf_sync dma_buf_sync = {
.flags = DMA_BUF_SYNC_RW | DMA_BUF_SYNC_START
};
if (ioctl(alloc_data.fd, DMA_BUF_IOCTL_SYNC, &dma_buf_sync)) {
perror("DMA_BUF_IOCTL_SYNC START");
return EXIT_FAILURE;
}
CPU が DMA Buffer にアクセスした後には、引数のフラグに DMA_BUF_SYNC_END を指定して DMA_BUF_IOCTL_SYNC を発行します。このフラグは、 CPU がアクセスが終了したことを DMA Buffer に通知してCPU Cache との同期を指示するものです。
また、フラグには同時に DMA Buffer へのアクセス方向を指定します。
struct dma_buf_sync dma_buf_sync = {
.flags = DMA_BUF_SYNC_RW | DMA_BUF_SYNC_END
};
if (ioctl(alloc_data.fd, DMA_BUF_IOCTL_SYNC, &dma_buf_sync)) {
perror("DMA_BUF_IOCTL_SYNC START");
return EXIT_FAILURE;
}
後始末
mmap() で割り当てた仮想アドレスmunmap() します。
if (nunmap(dma_heap_start, dma_heap_size) < 0) {
perror("munmap");
return EXIT_FAILURE;
}
なお、DMA Buffer を確保した際に struct dma_heap_allocation_data の fd_flags フィールドにO_CLOEXEC を指定しておくと、ユーザープログラムが終了した際に自動的に dma-heap が確保した DMA Buffer が解放されます。
V4L2 ストリーミング I/O にdma-heap を使用したソースコードの例
この章では、V4L2 ストリーミング I/O に 前章で紹介した dma-heap を使った例を簡単なソースコードで説明します。
ここでは VIDEO_CAPTURE_MPLANE の例を示します。
0. 下準備
0.1 インクルードファイル
インクルードに必要なファイルは以下の通りです。dma-heap を使う場合は、linux/dme-heap.h と linux/dma-buf.h が必要です。
include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <poll.h>
#include <linux/dma-heap.h>
#include <linux/dma-buf.h>
#include <linux/videodev2.h>
0.2 定数宣言
このソースコード例では、ビデオデバイス名、 dma-heap のデバイス名、イメージの大きさ、バッファの数(CAPUTRE_NUM_BUFFERS)、キャプチャーする回数(CAPTURE_IMAGE_COUNT) を固定値にしています。
#define VIDEO_DEVICE_FILE "/dev/video0"
#define DMA_HEAP_DEVICE_FILE "/dev/dma_heap/reserved"
#define CAPTURE_IMAGE_WIDTH 1920
#define CAPTURE_IMAGE_HEIGHT 1080
#define CAPTURE_IMAGE_PIXELFORMAT V4L2_PIX_FMT_RGB24
#define CAPTURE_IMAGE_COUNT 10
#define CAPTURE_NUM_BUFFERS 4
0.3 変数宣言
このソースコード例で使用する変数は次の通りです。またマルチプレーンなので、バッファの数×プレーン数ぶんのバッファのアドレスとサイズを格納するための配列(buffers)と、dma-heap で得たファイルディスクリプタを格納するための配列(v4l2_planes)も用意しておきます。
int video_fd = 0;
int dma_heap_fd = 0;
int num_planes = 0;
int num_buffers = 0;
struct v4l2_capability v4l2_cap;
struct v4l2_format v4l2_fmt;
struct v4l2_requestbuffers v4l2_reqbuf;
struct v4l2_buffer v4l2_buf;
enum v4l2_buf_type v4l2_buf_type;
struct v4l2_plane v4l2_planes[CAPTURE_NUM_BUFFERS][VIDEO_MAX_PLANES];
struct buffer {
void* start;
size_t length;
} buffers[CAPTURE_NUM_BUFFERS][VIDEO_MAX_PLANES];
1. デバイスを開く
ビデオデバイスの他に dma-heap のデバイスも開いておきます。
video_fd = open(VIDEO_DEVICE_FILE, O_RDWR);
if (video_fd == -1) {
perror(VIDEO_DEVICE_FILE);
return EXIT_FAILURE;
}
dma_heap_fd = open(DMA_HEAP_DEVICE_FILE, O_RDWR);
if (dma_heap_fd == -1) {
perror(DMA_HEAP_DEVICE_FILE);
eturn EXIT_FAILURE;
}
2. ビデオデバイスの対応状況を確認する
対象のビデオデバイスが、V4l2 ストリーム I/O の VIDEO_CAPUTRE_MPLANE に対応しているか確認します。
if (xioctl(video_fd, VIDIOC_QUERYCAP, &v4l2_cap)) {
perror("VIDIOC_QUERYCAP");
return EXIT_FAILURE;
}
if (!(v4l2_cap.capabilities & V4L2_CAP_STREAMING)) {
fprintf(stderr, "Not support streaming\n");
return EXIT_FAILURE;
}
if (!(v4l2_cap.capabilities & V4L2_CAP_VIDEO_CAPTURE_MPLANE)) {
fprintf(stderr, "Not support capture multiplane\n");
return EXIT_FAILURE;
}
3. ビデオデバイスにフォーマットを設定する
ビデオデバイスにキャプチャーするイメージの大きさやフォーマットを設定します。
memset(&v4l2_fmt, 0, sizeof(v4l2_fmt));
v4l2_fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
v4l2_fmt.fmt.pix_mp.width = CAPTURE_IMAGE_WIDTH;
v4l2_fmt.fmt.pix_mp.height = CAPTURE_IMAGE_HEIGHT;
v4l2_fmt.fmt.pix_mp.pixelformat = CAPTURE_IMAGE_PIXELFORMAT;
v4l2_fmt.fmt.pix_mp.num_planes = 1;
if (xioctl(video_fd, VIDIOC_S_FMT, &v4l2_fmt)) {
perror("VIDIOC_S_FMT");
return EXIT_FAILURE;
}
4.ビデオデバイスのフォーマットを確認
前節で設定したイメージフォーマットがちゃんとビデオデバイスに設定されたかを確認します。ついでにプレーン数の確認も行います。
memset(&v4l2_fmt, 0, sizeof(v4l2_fmt));
v4l2_fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
v4l2_fmt.fmt.pix_mp.width = CAPTURE_IMAGE_WIDTH;
v4l2_fmt.fmt.pix_mp.height = CAPTURE_IMAGE_HEIGHT;
v4l2_fmt.fmt.pix_mp.pixelformat = CAPTURE_IMAGE_PIXELFORMAT;
v4l2_fmt.fmt.pix_mp.num_planes = 1;
if (xioctl(video_fd, VIDIOC_G_FMT, &v4l2_fmt)) {
perror("VIDIOC_G_FMT");
return EXIT_FAILURE;
}
if ((v4l2_fmt.fmt.pix_mp.width != CAPTURE_IMAGE_WIDTH ) ||
(v4l2_fmt.fmt.pix_mp.height != CAPTURE_IMAGE_HEIGHT ) ||
(v4l2_fmt.fmt.pix_mp.pixelformat != CAPTURE_IMAGE_PIXELFORMAT)) {
fprintf(stderr, "Not support format\n");
return EXIT_FAILURE;
}
for (num_planes = 0; num_planes < VIDEO_MAX_PLANES; num_planes++)
if (v4l2_fmt.fmt.pix_mp.plane_fmt[num_planes].sizeimage == 0)
break;
if (num_planes == 0) {
fprintf(stderr, "num_planes is 0\n");
return EXIT_FAILURE;
}
5. ビデオデバイスにバッファを要求する
ビデオデバイスに対して、キャプチャーで使用するバッファの数と種類を通知して、バッファの準備を要求します。
ここで、バッファの種類として、V4L2_MEMORY_DMABUF を指定していることに注意してください。これは、ユーザープログラムとのデータの受け渡しに使うバッファとして、PRIME DMA-BUFs を使うことを示します。
memset(&v4l2_reqbuf, 0, sizeof(v4l2_reqbuf));
v4l2_reqbuf.count = CAPTURE_NUM_BUFFERS;
v4l2_reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
v4l2_reqbuf.memory = V4L2_MEMORY_DMABUF;
if (xioctl(video_fd, VIDIOC_REQBUFS, &v4l2_reqbuf)) {
perror("VIDIOC_REQBUFS");
return EXIT_FAILURE;
}
if (v4l2_reqbuf.count < CAPTURE_NUM_BUFFERS) {
fprintf(stderr, "Can not request buffer size\n");
return EXIT_FAILURE;
}
6. バッファを準備する
ビデオデバイスにバッファの各種情報を問い合わせて(VIDIOC_QUERYBUF)、プレーン毎に dma-heap に対して DMA Bufferを確保(DMA_HEAP_IOCTL_ALLOC) します。
dma-heap は DMA Buffer の確保に成功するとユニークなファイルディスクリプタを返します。そのファイルディスクリプタに対して mmap を実行すると、確保された DMA Buffer を仮想アドレスに割り当てられます。
割り当てられた DMA Buffer の仮想アドレスとサイズを buffers (バッファ数×プレーン数の配列)に保存しておきます。
また、dma-heap が生成した DMA Buffer を示すファイルディスクリプタを v4l2_planes (バッファ数×プレーン数の配列)に保存しておきます。
num_buffers = v4l2_reqbuf.count;
memset(&v4l2_planes[0][0], 0, sizeof(v4l2_planes));
memset(&(buffers[0][0]) , 0, sizeof(buffers));
for (int i = 0; i < num_buffers; i++) {
memset(&v4l2_buf, 0, sizeof(v4l2_buf));
v4l2_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
v4l2_buf.memory = V4L2_MEMORY_DMABUF;
v4l2_buf.index = i;
v4l2_buf.length = num_planes;
v4l2_buf.m.planes = &v4l2_planes[i][0];
if (xioctl(video_fd, VIDIOC_QUERYBUF, &v4l2_buf)) {
perror("VIDIOC_QUERYBUF");
return EXIT_FAILURE;
}
for (int plane = 0; plane < num_planes; plane++) {
void* dma_heap_start;
size_t dma_heap_size = v4l2_planes[i][plane].length;
struct dma_heap_allocation_data alloc_data = {
.len = dma_heap_size,
.fd = 0,
.fd_flags = (O_RDWR | O_CLOEXEC),
.heap_flags = 0,
};
if (ioctl(dma_heap_fd, DMA_HEAP_IOCTL_ALLOC, &alloc_data)) {
perror("DMA_HEAP_IOCTL_ALLOC");
return EXIT_FAILURE;
}
dma_heap_start = mmap(NULL,
dma_heap_size,
PROT_READ | PROT_WRITE,
MAP_SHARED,
alloc_data.fd,
0);
if (dma_heap_start == MAP_FAILED) {
perror("mmap");
return EXIT_FAILURE;
}
buffers[i][plane].length = dma_heap_size;
buffers[i][plane].start = dma_heap_start;
v4l2_planes[i][plane].m.fd = alloc_data.fd;
}
}
7. バッファをビデオデバイスのキューに入れる
キャプチャーの場合は、まず、ビデオデバイスのキューに用意したバッファをすべて入れてしまいます(VIDIOC_QBUF)。
バッファの種類として v4l2_buf.memory に V4L2_MEMORY_DMABUF を指定し、バッファの番号を v4l2_buf.index に指定します。また、dma-heap で確保したバッファのファイルディスクリプタの配列(v4l2_planes[バッファ番号])のポインタを v4l2_buf.m.planes に指定します。
V4L2_MEMORY_DMAPBUF の場合は、バッファをビデオデバイスのキューに入れる際に、ビデオデバイスは与えられたファイルディスクリプタに対応する DMA Buffer に対して end_cpu_access を要求します。DMA Buffer は end_cpu_access 要求に対して、sync_for_device を実行します。これは Cache Coherence をソフトウェアで解決する場合は、キャッシュのフラッシュを行います。この処理は Linux Kernel 内部で行われるのでユーザーが特に意識する必要はありません。(2024年10月11日削除)
for (int buf_index = 0; buf_index < num_buffers; buf_index++) {
memset(&v4l2_buf, 0, sizeof(v4l2_buf));
v4l2_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
v4l2_buf.memory = V4L2_MEMORY_DMABUF;
v4l2_buf.index = buf_index;
v4l2_buf.length = num_planes;
v4l2_buf.m.planes = &v4l2_planes[buf_index][0];
if (xioctl(video_fd, VIDIOC_QBUF, &v4l2_buf)) {
perror("VIDIOC_QBUF");
return EXIT_FAILURE;
}
}
8. ストリームを開始する
ビデオデバイスに対して、ストリームの開始(VIDIOC_STREAMON)を指示します。ビデオデバイスはキャプチャーができしだい、キューに入れたバッファにキャプチャーデータを格納します。
v4l2_buf_type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
if (xioctl(video_fd, VIDIOC_STREAMON, &v4l2_buf_type)) {
perror("VIDIOC_STREAMON");
return EXIT_FAILURE;
}
9. キャプチャーする
このソースコード例では、CAPTURE_IMAGE_COUNT で指定された回数だけビデオデータをキャプチャーします。
for (int count = 0; count < CAPTURE_IMAGE_COUNT; count++) {
9.1 ビデオデバイスがバッファにデータを準備できるのを待つ
struct pollfd poll_fds[1];
int poll_result;
poll_fds[0].fd = video_fd;
poll_fds[0].events = POLLIN;
poll_result = poll(poll_fds, 1, 5000);
if (poll_result == -1) {
perror("Waiting for frame");
return EXIT_FAILURE;
}
9.2 キューからデータが準備できたバッファを取り出す
ビデオデバイスのキューからデータが準備できたバッファを取り出します(VIDIOC_DQBUF)。要求する際には、プレーン情報を格納するために空の buf_plane 配列を用意して v4l2_buf.m.planes に指定します。また、バッファの種類として、v4l2_buf.memory に V4L2_MEMORY_DMABUF を指定します。
ビデオデバイスは、データが準備出来たバッファをキューから取り出して、そのバッファの番号を v4l2_buf.index に入れて返します。
V4L2_MEMORY_DMAPBUF の場合は、ビデオデバイスがバッファをキューから取り出す際に、ビデオデバイスは与えられたファイルディスクリプタに対応する DMA Buffer に対して begin_cpu_access を要求します。DMA Buffer は begin_cpu_access 要求に対して、sync_for_cpu を実行します。sync_for_cpu は Cache Coherence をソフトウェアで解決する場合は、キャッシュの Invalidiate を行います。この処理は Linux Kernel 内部で行われるのでユーザーが特に意識する必要はありません。(2024年10月11日削除)
int buf_index;
struct v4l2_plane buf_plane[VIDEO_MAX_PLANES] = {{0}};
memset(&v4l2_buf, 0, sizeof(v4l2_buf));
v4l2_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
v4l2_buf.memory = V4L2_MEMORY_DMABUF;
v4l2_buf.length = num_planes;
v4l2_buf.m.planes = &buf_plane[0];
if (xioctl(video_fd, VIDIOC_DQBUF, &v4l2_buf)) {
perror("VIDIOC_DQBUF");
return EXIT_FAILURE;
}
buf_index = v4l2_buf.index;
9.3 データを処理する
このソースコード例では特に何もしていません。ご希望に合わせて、別のバッファにコピーするなり、ファイルに書き出すなりしてください。(2024年10月11日削除)
データを処理する際、 CPU Cache との同期をとる必要がある場合があります。その場合は、CPU がバッファを読む前に dma-heap から得た DMA Buffer を示すファイルディスクリプタに対して DMA_BUF_IOCTL_SYNC を発行します。その際、フラグには DMA_BUF_SYNC_START を指定します。このフラグは、これから CPU がアクセスすることを DMA Buffer に通知してCPU Cache との同期を指示するものです。(2024年10月11日追記)
for (int plane = 0; plane < num_planes; plane++) {
int dma_buf_fd = v4l2_planes[buf_index][plane].m.fd;
const struct dma_buf_sync dma_buf_sync = {
.flags = DMA_BUF_SYNC_READ | DMA_BUF_SYNC_START
};
if (ioctl(dma_buf_fd, DMA_BUF_IOCTL_SYNC, &dma_buf_sync)) {
perror("DMA_BUF_IOCTL_SYNC START");
return EXIT_FAILURE;
}
}
それから、ご希望に合わせて、別のバッファにコピーするなり、ファイルに書き出すなりしてください。
for (int plane = 0; plane < num_planes; plane++) {
void* buf_start = buffers[buf_index][plane].start;
size_t buf_size = buffers[buf_index][plane].length;
/* データを処理 */
}
その後、データ処理を終えたバッファをビデオデバイスのキューに入れなおす前に、場合によっては CPU Cache との同期をとる必要があります。その場合は、dma-heap から得た DMA Buffer を示すファイルディスクリプタに対して DMA_BUF_IOCTL_SYNC を発行します。その際、フラグには DMA_BUF_SYNC_END を指定します。このフラグは、 CPU がアクセスが終了したことを DMA Buffer に通知してCPU Cache との同期を指示するものです。(2024年10月11日追記)
for (int plane = 0; plane < num_planes; plane++) {
int dma_buf_fd = v4l2_planes[buf_index][plane].m.fd;
const struct dma_buf_sync dma_buf_sync = {
.flags = DMA_BUF_SYNC_READ | DMA_BUF_SYNC_END
};
if (ioctl(dma_buf_fd, DMA_BUF_IOCTL_SYNC, &dma_buf_sync)) {
perror("DMA_BUF_IOCTL_SYNC START");
return EXIT_FAILURE;
}
}
9.4 データ処理を終えたバッファをキューに返す
データ処理を終えたバッファをビデオデバイスのキューに入れなおします。その際、バッファの種類として、v4l2_buf.memory に V4L2_MEMORY_DMABUF を指定し、9.2 でキューから取り出したバッファの番号を v4l2_buf.index に指定します。
V4L2_MEMORY_DMAPBUF の場合は、バッファをビデオデバイスのキューに入れる際に、ビデオデバイスは与えられたファイルディスクリプタに対応する DMA Buffer に対して end_cpu_access を要求します。DMA Buffer は end_cpu_access 要求に対して、sync_for_device を実行します。これは Cache Coherence をソフトウェアで解決する場合は、キャッシュのフラッシュを行います。この処理は Linux Kernel 内部で行われるのでユーザーが特に意識する必要はありません。(2024年10月11日削除)
memset(&v4l2_buf, 0, sizeof(v4l2_buf));
v4l2_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
v4l2_buf.memory = V4L2_MEMORY_DMABUF;
v4l2_buf.index = buf_index;
v4l2_buf.length = num_planes;
v4l2_buf.m.planes = &v4l2_planes[buf_index][0];
if (xioctl(video_fd, VIDIOC_QBUF, &v4l2_buf)) {
perror("VIDIOC_QBUF");
return EXIT_FAILURE;
}
}
10. ストリームを停止する
ビデオデバイスに対して、ストリームの停止(VIDIOC_STREAMOFF)を指示します。ビデオデバイスがストリームを停止した後に戻ってきます。
v4l2_buf_type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
if (xioctl(video_fd, VIDIOC_STREAMOFF, &v4l2_buf_type)) {
perror("VIDIOC_STREAMOFF");
return EXIT_FAILURE;
}
11. バッファの後始末
for (int i = 0; i < CAPTURE_NUM_BUFFERS; i++) {
for (int plane = 0; plane < num_planes; plane++) {
void* start = buffers[i][plane].start;
size_t size = buffers[i][plane].length;
if ((start != NULL) && (start != MAP_FAILED)) {
if (munmap(start, size)) {
perror("munmap");
return EXIT_FAILURE;
}
}
}
}
12. デバイスを閉じる
if (close(dma_heap_fd) < 0) {
perror("Failed to close file");
return EXIT_FAILURE;
}
if (close(video_fd) < 0) {
perror("Failed to close file");
return EXIT_FAILURE;
}
V4L2 ストリーミング I/O (V4L2_MEMORY_MMAP) との性能比較
測定環境
今回測定に使用する環境は次の通りです。
Table.1 測定環境
対象ボード | AMD KV260 |
デバイス | Zynq UltraScale+ MPSoC |
プロセッサ | Arm Cortex-A53 |
OS | Ubuntu 22.04 |
Camera | Raspberry Pi Camera Module 2 |
画像フォーマット | 1920 × 1080 RGB888 |
なお、OS で使用した Ubuntu 22.04 は AMD 社公式のものではなく、筆者が独自に作成したものです。詳細は以下の記事を参照してください。
カメラ(Raspberry Pi Camera Module 2) からの入力は、ZynqMP の PL 部に専用のカメラ入力と、各種画像処理を実装しています。これらは、いずれも AMD(Xilinx) が提供している IP を使っています。また、これら IP を制御するデバイスドライバも AMD(Xilinx) が提供しているものを使用しています。
これらは以下の github リポジトリにあります。
測定結果
mmap を使った場合
まずは比較のために、V4L2_MEMORY_MMAP の場合の測定結果を示します。測定に使用したプログラムは次の通りです。
このプログラムをコンパイルして実行した結果は次の通りです。
shell$ sudo example/v4l2_capture_simple/v4l2_capture_mplane_mmap
Frame :
Width : 1920
Height : 1080
Frames : 10
Proc Time : # Average Per Frame
Total : 0.120384447 #[Second]
Wait : 0.008633502 #[Second]
Dequeue : 0.000008058 #[Second]
Run : 0.111699128 #[Second]
Enqueue : 0.000042049 #[Second]
FPS : 8.306720891 #[Frames Per Second]
dma-heap を使った場合
次に、dma-heap を使った場合の測定結果を示します。測定に使用したプログラムは次の通りです。
このプログラムをコンパイルして実行した結果は次の通りです。
shell$ sudo ./v4l2_capture_mplane_dma_heap
Frame :
Width : 1920
Height : 1080
Frames : 10
Proc Time : # Average Per Frame
Total : 0.040657403 #[Second]
Wait : 0.018922309 #[Second]
Dequeue : 0.000008199 #[Second]
Run : 0.021684052 #[Second]
Enqueue : 0.000041280 #[Second]
FPS : 24.595766911 #[Frames Per Second]
考察
各々の測定結果のうち、Run にかかった時間に着目します。Run は DMA Buffer にキャプチャーされたイメージデータを別のバッファにコピーするだけの簡単な処理を行っています。
mmap を使った場合の Run にかかった時間は1フレームあたり約0.1117秒、dma-heap を使った場合の Run にかかった時間は1フレームあたり約 0.0217秒と、約5倍程度高速になっています。mmap を使った場合では CPU Cache が無効になっているのに対して、dma-heap を使った場合は CPU Cache が有効になっていることが分かります。また、 dma-heap を使った場合の Run 時間には CPU Cache の同期にかかった時間も含まれています。
ただ、FPS(Frame Per Second) は Run の性能向上ほどではありません。せいぜい3倍程度です。dma-heap を使った場合の Wait にかかった時間に着目すると、0.0196秒となっています。これは、1フレーム全体を処理する時間 0.0406 秒のうち48%と半分近くを占めています。Wait 時間は、カメラ入力を待っている時間なので、おそらくこれは使用したカメラの制限か、あるいは画像処理部の制限だと思われます。
参考
- 『V4L2 ストリーミングI/O(V4L2_MEMORY_MMAP) で性能が出ない問題 (2024年改訂版)』 @Qiita
https://qiita.com/ikwzm/items/474ababbc99cb4812bc7 - 『Linux で DMA Bufferを mmap した時に CPU Cacheが無効になる場合がある (はじめに)』@Qiita
https://qiita.com/ikwzm/items/aa7e2373705872ccf764 - 『Linux では Cache Coherence Hardware を持っていないとDMA Buffer をmmap する際に CPU Cache が無効になる』 @Qiita
https://qiita.com/ikwzm/items/0f77072158ce842018fc - 『Raspberry Pi では DMA Buffer をmmap する際に CPU Cache が無効になる』@Qiita
https://qiita.com/ikwzm/items/f8bcb82cfdb818bdb07d - 『Ultra96/Ultra96-V2/KV260/KR260 向け Ubuntu 22.04(Desktop版) ブートイメージの提供』@Qiita
https://qiita.com/ikwzm/items/9d096104a129255823a8 - Raspberry Pi Camera V2 for Kv260
github
https://github.com/ikwzm/RasPi-Camera-V2-KV260