# 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 中沒有資料。 ![image](https://hackmd.io/_uploads/rkTiaqZ7el.png) 改成寫入 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 ``` 得到正確的圖: ![image](https://hackmd.io/_uploads/BJEKgbfXeg.png) --- `./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 位元。 ![PixelSamples32bppRGBA](https://hackmd.io/_uploads/Hk_5BpINxe.png) * RGBA32: alpha 在最低 8 位元。 ![HexRGBAbits](https://hackmd.io/_uploads/HJXor684lg.png) ##### 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 之間的關係為: ![image](https://hackmd.io/_uploads/rkEbL6IVle.png) #### 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+)