# Linux 核心專題: 虛擬攝影裝置驅動程式
> 執行人: liangchingyun
> [解說影片](https://www.youtube.com/watch?v=Ye0y3MaOC48)
### Reviewed by `dingsen-Greenhorn`
「Print this informations.」→ 「Print this information.」
(information 是不可數名詞,不用加 s)
> 了解,之後會提交 PR 修改這部分。
### Reviewed by `charliechiou`
目前的實驗測試在 framebuffer 寫入圖片,想詢問是有辦法換成影片輸入的嗎 ? 單純依照帧數輸入圖片至 buffer 中或是否需要做對應的調整 ?
> 可以,ffmpeg 會把影片轉成一張張圖片,讀進 framebuffer。
### Reviewed by `wurrrrrrrrrr`
kmodleak 報告顯示有 13 個未釋放的記憶體分配,那這些記憶體可能是由哪個函式所分配的呢?
> 可能是 `register_framebuffer()` 分配的記憶體在卸載時未正確釋放。
### Reviewed by `horseface1110`
kmodleak 報告顯示有 13 個未釋放的記憶體分配,但是為什麼需要強調有一個記憶體空間大小是32768 byte呢?這個數字有甚麼涵義嗎?
> 代表最大的未釋放記憶體大小是 32768 bytes。
>
### Reviewed by `MuChengChen`
在 DMABUF 的測試結果中有提到「驅動為了確保正常運作,自動分配了比請求更多的 buffer」,那請求分配的 buffer 和驅動多分配的 buffer 數量間呈現怎麼樣的關係 ? 是請求分配的 buffer 越多驅動多分配的 buffer 就越多嗎 ?
> 待測試
### Reviewed by `leonnig`
> 將 RGB565 格式的 ui.bin 轉換成 RGB888 格式的 ui.rgb
想請問在文中有提到 RGB565 效率較高,然後有附上由 RGB565 轉換為 RGB888 的程式,那在這個專案中有提到是使用哪一種方式嗎,有的考量為何 ?
> 此專案是使用 RGB24,原因可能為 V4L2 的預設 pixel format 是 RGB24 , 且因使用較多的位元數較多,圖片品質會比 RGB565 好。
## TODO: 重現去年實驗並確保在 Linux v6.11+ 運作
> [2024 年報告-1](https://hackmd.io/@sysprog/HJBxRsRr0), [2024 年報告-2](https://hackmd.io/@sysprog/S1l3ZlcLA)
### 在 Linux v6.11+ 執行原專案
#### 編譯
`./fb.c` 中使用 `remap_vmalloc_range()` 等函式需要加上:
```c
#include <linux/vmalloc.h>
```
加上後即可成功編譯。[#38](https://github.com/sysprog21/vcam/pull/40)
#### 執行專案
```
./vcam-util [選項]
```
```
-h --help Print this informations.
-c --create Create a new device.
-m --modify idx Modify a device.
-r --remove idx Remove a device.
-l --list List devices.
-s --size WIDTHxHEIGHTxCROPRATIO Specify virtual resolution or crop ratio.
Crop ratio will only be applied in cropping mode.
For instance:
WxH: 640x480 Specify the virtual resolution.
CR: 5/6 Apply crop ratio to the current resolution.
WxHxCR: 640x480x5/6 Specify the virtual resolution and apply with crop ratio.
-p --pixfmt pix_fmt Specify pixel format (rgb24,yuyv).
-d --device /dev/* Control device node.
```
##### 建立虛擬攝影機裝置
:::danger
注意用語: device 是「裝置」
:::
1. 預設
```
$ sudo ./vcam-util -c
```
2. 自訂解析度和像素格式(預設:解析度`640x480`,像素格式`rgb24`)
```
$ sudo ./vcam-util -c -s 1280x720 -p yuyv
```
3. 加入裁切比例(預設:`1/1`)
```
$ sudo ./vcam-util -c -s 1280x720x5/6 -p rgb24
```
4. 指定控制裝置節點(預設:`/dev/vcamctl`)
```
$ sudo ./vcam-util -c -d /dev/custom_vcamctl
```
##### 修改索引為 idx 的裝置
```
$ sudo ./vcam-util -m idx -s 1920x1080 -p yuyv
```
> idx 從 1 開始
##### 刪除索引為 idx 的裝置
```
$ sudo ./vcam-util -r idx
```
> idx 從 1 開始
##### 列出所有虛擬攝影機裝置
```
$ sudo ./vcam-util -l
```
##### 測試
查看圖片格式
```
$ ffprobe image.jpg
...
Stream #0:0: Video: mjpeg (Progressive), yuvj444p(pc, bt470bg/unknown/unknown), 612x544 [SAR 300:300 DAR 9:8], 25 fps, 25 tbr, 25 tbn
```
調整 vcam 輸出格式
```
$ sudo ./vcam-util -m idx -s 612x544 -p yuyv
```
將圖片寫入虛擬攝影機的 framebuffer 裝置。
```
$ sudo ffmpeg -i image.jpg -vf scale=612:544 -f rawvideo -pix_fmt yuyv422 -y /dev/fbX
```
> Pixel Format : `'YUYV'` 對應到 `yuyv422`
使用 VLC 查看輸出
```
$ vlc v4l2:///dev/video0
VLC media player 3.0.20 Vetinari (revision 3.0.20-0-g6f0d0ab126b)
```
無法開啟 VLC,可能因為我是用遠端連線,待測試。改存成圖片來確認虛擬攝影機內容:
```
# 從虛擬攝影機擷取一幀
$ sudo v4l2-ctl -d /dev/video0 --stream-mmap --stream-count=1 --stream-to=capture.raw
# 檢查檔案大小
$ ls -lh capture.raw
# 轉換成 JPG
$ ffmpeg -f rawvideo -pix_fmt yuyv422 -s 612x544 -i capture.raw output.jpg
```
得到以下畫面,代表 framebuffer 中沒有資料。

改成寫入 framebuffer 時馬上擷取:
```
$ sudo ffmpeg -i image.jpg -vf scale=612:544 -f rawvideo -pix_fmt rgb24 -y /dev/fbX -f rawvideo -pix_fmt yuyv422 -frames:v 1 capture.raw
```
得到正確的圖:

---
`./Makefile` 理解:
```makefile
target = vcam # 定義模組的名稱
vcam-objs = module.o control.o device.o videobuf.o fb.o # 指定 vcam.o 是由這幾個 .o 檔組成
obj-m = $(target).o
CFLAGS_utils = -O2 -Wall -Wextra -pedantic -std=c99
# 同時編譯 kernel module 與 user tool
.PHONY: all
all: kmod vcam-util
# 用 GCC 編譯一個 user-space 的 C 程式 vcam-util.c
vcam-util: vcam-util.c vcam.h
$(CC) $(CFLAGS_utils) -o $@ $<
# 使用 kernel build system 進行模組建構
kmod:
$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
.PHONY: clean
clean:
$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
$(RM) vcam-util
```
### 通過 V4L2 測試
使用 `vcam-util` 來確認 video 裝置的名稱:
```
$ sudo ./vcam-util -l
1. fb1(640,480,1/1,rgb24) -> /dev/video0
```
對 vcam 做測試,顯示所有程式皆通過:
```
$ sudo v4l2-compliance -d /dev/video0 -f
Total for vcam device /dev/video0: 48, Succeeded: 48, Failed: 0, Warnings: 0
```
### 檢查記憶體洩漏
使用 [kmodleak](https://github.com/tzussman/kmodleak) 來追蹤記憶體洩漏。
編譯後,執行:
```
$ sudo ./kmodleak vcam
```
在另一個終端機掛載及卸載 vcam 模組:
```
$ sudo insmod vcam.ko
$ sudo rmmod vcam
```
卸載後 kmodleak 會自動停止,並產生報告:
```
using page size: 4096
Tracing module memory allocs... Unload module (or hit Ctrl-C) to end
module 'vcam' loaded
module 'vcam' unloaded
13 stacks with outstanding allocations:
32768 bytes in 1 allocations from stack
addr = 0x230248 size = 32768
0 [<ffffffff84a80e92>] __alloc_pages_noprof+0x232
1 [<ffffffff84a80e92>] __alloc_pages_noprof+0x232
2 [<ffffffff84a90ce7>] allocate_slab+0xa7
3 [<ffffffff84a90ff8>] new_slab+0x38
4 [<ffffffff84a9162c>] ___slab_alloc+0x5fc
5 [<ffffffff84a93761>] __kmalloc_cache_noprof+0x2c1
6 [<ffffffff85007cc7>] do_register_framebuffer+0x297
7 [<ffffffff85007d71>] register_framebuffer+0x21
8 [<ffffffffc18e9470>] vcamfb_init+0x2a0
9 [<ffffffffc18e8161>] create_vcam_device+0x2c1
10 [<ffffffffc18e6307>] request_vcam_device+0xa7
11 [<ffffffffc18f3045>] __param_devices_max+0x2215
12 [<ffffffff84602c5b>] do_one_initcall+0x5b
13 [<ffffffff848060b7>] do_init_module+0x97
14 [<ffffffff84807715>] load_module+0x6b5
15 [<ffffffff84807b46>] init_module_from_file+0x96
16 [<ffffffff84807cdc>] idempotent_init_module+0x11c
17 [<ffffffff84808024>] __x64_sys_finit_module+0x64
18 [<ffffffff8460c360>] x64_sys_call+0x2580
19 [<ffffffff8589124e>] do_syscall_64+0x7e
20 [<ffffffff85a0012b>] entry_SYSCALL_64_after_hwframe+0x76
...
```
結果顯示有 13 個沒有被釋放掉的記憶體配置,其中一個為 32768 bytes。
## TODO: vcam 所需的背景知識回顧
> 適度整理之前的報告內容,針對 Linux v6.8+,探討 V4L2 和 Linux framebuffer
### 影像處理
#### RGBA color model
> [維基百科](https://en.wikipedia.org/wiki/RGBA_color_model)
RGBA: red, green, blue, alpha。其中 alpha 代表透明度。
##### RGBA8888
用 32 位元的無號整數來表達
* ARGB32: alpha 在最高 8 位元。

* RGBA32: alpha 在最低 8 位元。

##### RGB888
用 24 位元的無號整數來表達(Red: 8 bits / Green: 8 bits / Blue: 8 bits)
##### RGB565
用 16 位元的無號整數來表達(Red: 5 bits / Green: 6 bits / Blue: 5 bits)
效率很高,每個像素只需要 2 bytes,因此很常被使用。
##### 轉換程式
將 RGB565 格式的 `ui.bin` 轉換成 RGB888 格式的 `ui.rgb`
```java
//convert RGB565 to RGB888
byte src[] = loadBytes("ui.bin");
byte dst[] = new byte[src.length/2*3]; // 從 2 bytes 變成 3 bytes
for (int i=0, j=0; i<src.length; i+=2) {
int c = src[i] + (src[i+1]<<8); // 將 2 bytes 合併成一個 16-bit 整數
byte r = byte(((c & 0xF800) >> 11) << 3);
byte g = byte(((c & 0x7E0) >> 5) << 2);
byte b = byte(((c & 0x1F)) << 3);
dst[j++]=r;
dst[j++]=g;
dst[j++]=b;
}
saveBytes("ui.rgb", dst);
size(754,754);
println(dst.length);
loadPixels();
for (int i=0; i<dst.length-3; ) {
char r = char(dst[i++] & 255);
char g = char(dst[i++] & 255);
char b = char(dst[i++] & 255);
pixels[i/3] = color(r, g, b);
}
updatePixels();
```
> 假設某像素為 `1111100000011111`,`r` 的變換:
> 1. `& 0xF800` --> `1111100000000000`
> 2. `>> 11` --> `11111`
> 3. `<< 3` --> `11111000`,補足 8 位元
#### YUV color model
> [2.10 YUV Formats](https://www.kernel.org/doc/html/v4.12/media/uapi/v4l/yuv-formats.html): 以使用的位元個數區分
電視使用的標準。Y 代表亮度,U、V 代表彩度。原因是人眼對亮度較敏感,需給予較大的頻寬。其與 RGB 之間的關係為:

#### Motion JPEG
> [維基百科](https://zh.wikipedia.org/zh-tw/Motion_JPEG)
一種影像壓縮格式,其中每一訊框圖像都分別使用JPEG編碼。
只單獨的對某一訊框進行壓縮,而不考慮影像畫面中不同訊框之間的變化。因此壓縮率不高,但處理成本較低。
### 虛擬攝影機裝置
> 參考 [2024 年報告-1](https://hackmd.io/@sysprog/HJBxRsRr0)
:::danger
使用 Graphviz 重新製圖並嵌入到 HackMD 筆記頁面
:::
```graphviz
digraph _graph_name_ {
rankdir=BT;
graph [fontname="DFKai-SB", label="何謂虛擬攝影機裝置驅動程式", fontsize=30];
node [fontname="DFKai-SB"];
edge [fontname="DFKai-SB"];
subgraph cluster_real {
label = "";
style = dashed;
A[label="視訊輸入\n(VideoX)", shape=box, width=2.5, fontsize=20];
B[label="裝置驅動程式", shape=box, width=2.5, fontsize=20];
C[label="攝影機裝置", shape=box, width=2.5, fontsize=20];
a[label="Userspace", shape=plaintext, fontsize=20];
b[label="Kernel", shape=plaintext, fontsize=20];
c[label="Hardware", shape=plaintext, fontsize=20];
{ rank=same; a; A; }
{ rank=same; b; B; }
{ rank=same; c; C; }
C -> B -> A;
}
subgraph cluster_virtual {
label = "";
style = dashed;
D[label="視訊輸入\n(VideoX)", shape=box, width=2.5, fontsize=20];
E[label="裝置驅動程式", shape=box, width=2.5, fontsize=20];
F[label="虛擬攝影機裝置", shape=box, width=2.5, fontsize=20];
d[label="Userspace", shape=plaintext, fontsize=20];
e[label="Kernel", shape=plaintext, fontsize=20];
f[label="Hardware", shape=plaintext, fontsize=20];
{ rank=max; d, D; F}
{ rank=same; e; E; }
F -> E -> D;
f -> e -> d [style=invis];
}
B -> e [label=" 使用虛擬攝影機 \n 取代真實攝影機 ", style=dashed, constraint=false, fontsize=20];
}
```
使用 V4l2 框架以及 Linux Frambuffer API
運作流程:將資料寫入虛擬 Frambuffer 中,影片播放程式可以透過 /dev/videoX 來播放影片
```graphviz
digraph _graph_name_ {
rankdir=BT;
graph [fontname="DFKai-SB"];
node [fontname="DFKai-SB"];
edge [fontname="DFKai-SB"];
subgraph cluster_a {
label = "";
style = invis;
A[label="視訊輸入\n(/dev/VideoX)", shape=box, width=1.5];
B[label="vcam", shape=box, width=1.5];
a[label="Userspace", shape=plaintext];
x[label="", shape=plaintext];
b[label="Kernel", shape=plaintext];
{rank=max; a; A}
{rank=same; b; B}
#{rank=same; x; C}
B -> A;
b -> x -> a [style=invis];
}
subgraph cluster_b {
label = "";
style = invis;
C[label="Framebuffer API", shape=box, width=2];
D[label="虛擬 Framebuffer\n(/dev/fbX)", shape=box, width=2];
c[label="", shape=plaintext];
y[label="", shape=plaintext];
d[label="", shape=plaintext];
{rank=max; c; D}
{rank=same; y; C}
D -> C;
d -> y -> c [style=invis];
}
C -> B [constraint=false]
}
```
#### V4L2 (Video for Linux 2)
> [Linux V4L2 學習](https://work-blog.readthedocs.io/en/latest/index.html)
Linux 下關於視訊裝置的驅動框架,支援的裝置有:
1. Video capture device (ex. 攝影機)
2. Video output device (ex. 螢幕)
3. Radio device (沒有影像,只有聲音)
兩種角度來看 V4L2 框架
1. userspace 角度
常見的 ioctl 參數
* VIDIOC_QUERYCAP: 詢問 V4L2 裝置的 capability
* VIDIOC_S_INPUT / VIDIOC_G_INPUT: 設置、取得目前的輸入來源
* VIDIOC_S_FMT / VIDIOC_G_FMT: 設置、取得 V4L2 裝置的 format (ex. resolution)
2. 驅動程式開發者角度
* 關係綁定
* 將 V4L2 的結構體嵌入到自己所定義的結構體中,例如:
```c
struct vcam_device *create_vcam_device(size_t idx,
struct vcam_device_spec *dev_spec)
{
struct video_device *vdev;
...
}
```
* 透過 V4L2 的函式將自定義的結構體跟 V4L2 框架作綁定,例如:
```c
ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
```
* 函數綁定
* 實作特定函式並且將函式跟 V4L2 框架綁定
* 當某件事情發生時,驅動框架就會呼叫綁定的程式
#### Linux framebuffer
類似畫布
Frambuffer: RAM 中的一段連續記憶體,CPU 或 GPU 會把要顯示的影像放到 Framebuffer 中,讓 Display 裝置顯示。
```graphviz
digraph _graph_name_ {
rankdir=BT;
graph [fontname="DFKai-SB"];
node [fontname="DFKai-SB"];
edge [fontname="DFKai-SB"];
subgraph cluster_a {
label = "RAM";
Framebuffer[label="Framebuffer", shape=box, width=1.5];
}
Display[label="Display", shape=box, width=1.5, height=0.7];
Framebuffer -> Display [constraint=false, minlen=3]
}
```
:::danger
使用 Graphviz 重新製圖並嵌入到 HackMD 筆記頁面
:::
Linux 有提供 Framebuffer 的 API 框架,使用者可以透過 API 對 Framebuffer 進行操作,也可以使用該框架做一個須你的 Framebuffer。
### vcam 專案
framebuffer 為輸入!
MP4 → framebuffer → vcam 的 /dev/video1 (可被擷取) → VLC / MPlayer
#### 針對 Linux v6.8+ 的調整
Problem:
在其版本上無法成功編譯
Solution:
1. 修正呼叫函式時所用的參數數量: `class_create(owner, name)` --> `class_create(name)`
2. 修正 `struct vb2_queue` 的 member 名稱: `min_buffers_needed` --> `min_queued_buffers`
3. 修改初始化 `struct fb_info` 的方式: `info->flags = FBINFO_FLAG_DEFAULT;` --> `fb_data->info = framebuffer_alloc(0, &dev->vdev.dev);`
注意:不可以直接改成定值,以防 Linux 核心哪天將初始值改掉。
:::danger
確認後,提交 pull request,注意要讓舊的 Linux 核心也能編譯 vcam 核心模組。
:::
## TODO: 支援 DMABUF
> 提交 pull request
### 原理理解
DMA-BUF (Direct Memory Access Buffer) : 允許在不同裝置間共享的 buffer。
```graphviz
digraph {
A [label="行程 A"]
B [label="行程 B\n來自 socket"]
DA [label="裝置 A" shape="rectangle"]
DB [label="裝置 B" shape="rectangle"]
BUF [label="buffer" shape="rectangle"]
subgraph cluster_0 {
{rank=same A B}
A->B
label="userspace"
}
subgraph cluster_1 {
DA->BUF
DB->BUF
label="kernel space"
}
DA->A [label=" 導出 fd"]
B->DB [label=" 導入 fd"]
}
```
簡單來說,buffer 為共享的目標,應用程式將 buffer 導出為 fd,在 userspace 傳遞,再將 fd 導入另一個裝置。
### 程式實作
查詢關鍵字:
```
grep -rn 'vb2_queue'
```
在 `vb2_queue.io_modes` 中啟用 `VB2_DMABUF`
```diff
- q->io_modes = VB2_MMAP | VB2_USERPTR | VB2_READ;
+ q->io_modes = VB2_MMAP | VB2_USERPTR | VB2_READ | VB2_DMABUF;
```
新增操作函式
```diff
static const struct vb2_ops vcam_vb2_ops = {
.queue_setup = vcam_out_queue_setup,
.buf_prepare = vcam_out_buffer_prepare,
.buf_queue = vcam_out_buffer_queue,
.start_streaming = vcam_start_streaming,
.stop_streaming = vcam_stop_streaming,
.wait_prepare = vcam_outbuf_unlock,
.wait_finish = vcam_outbuf_lock,
+ .buf_init = vcam_buf_init,
+ .buf_cleanup = vcam_buf_cleanup,
};
```
```c
static int vcam_buf_init(struct vb2_buffer *vb)
{
struct vcam_out_buffer *buf = container_of(vb, struct vcam_out_buffer, vb.vb2_buf);
buf->filled = 0;
INIT_LIST_HEAD(&buf->list);
pr_debug("vcam_buf_init: buffer initialized\n");
return 0;
}
static void vcam_buf_cleanup(struct vb2_buffer *vb)
{
pr_debug("vcam_buf_cleanup called\n");
}
```
修改 buffer 的 memory type:
`vb2_queue.mem_ops` 原指定為 `vb2_vmalloc_memops`,但 vmalloc 的記憶體不能拿去 DMA, 無法支援 DMABUF。
> 原因: `vmalloc()` 分配的是虛擬連續、實體不連續的記憶體。大多數硬體 DMA 控制器只接受 實體連續記憶體。[reference](https://www.cnblogs.com/arnoldlu/p/18063912)
改為 `vb2_dma_contig_memops`
```diff
- q->mem_ops = &vb2_vmalloc_memops;
+ q->mem_ops = &vb2_dma_contig_memops;
```
載入模組
```
$ sudo modprobe videobuf2-dma-contig
```
### 測試程式
#### 確認支援 DMABUF
```c
int main() {
const char *dev_name = "/dev/video0";
int fd = open(dev_name, O_RDWR);
if (fd < 0) {
perror("Failed to open video device");
return 1;
}
struct v4l2_requestbuffers req;
memset(&req, 0, sizeof(req));
req.count = 2;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_DMABUF;
if (ioctl(fd, VIDIOC_REQBUFS, &req) == -1) {
perror("VIDIOC_REQBUFS (DMABUF) not supported");
close(fd);
return 1;
}
printf("DMABUF is supported! Requested %u buffers, got %u.\n", 2, req.count);
close(fd);
return 0;
}
```
```
$ gcc -o test_dmabuf test_dmabuf.c
$ ./test_dmabuf
DMABUF is supported! Requested 2 buffers, got 3.
```
驅動為了確保正常運作,自動分配了比請求更多的 buffer,確認 vcam 支援 DMABUF。
#### 確認 zero-copy 的效益
```
$ gcc -o test_dmabuf_performance test_dmabuf_performance.c
$ ./test_dmabuf_performance
```
## 專案貢獻
[Pull Requests](https://github.com/sysprog21/vcam/pulls?q=is%3Amerged+is%3Apr+author%3Aliangchingyun+)