Try   HackMD

Linux 核心專題: 虛擬攝影裝置驅動程式

執行人: liangchingyun

TODO: 重現去年實驗並確保在 Linux v6.11+ 運作

2024 年報告-1, 2024 年報告-2

在 Linux v6.11+ 執行原專案

編譯

./fb.c 中使用 remap_vmalloc_range() 等函式需要加上:

#include <linux/vmalloc.h>

加上後即可成功編譯。#38

執行專案

./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.
建立虛擬攝影機裝置

注意用語: 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 Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

改成寫入 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 Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →


./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 來追蹤記憶體洩漏。

編譯後,執行:

$ 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

維基百科

RGBA: red, green, blue, alpha。其中 alpha 代表透明度。

RGBA8888

用 32 位元的無號整數來表達

  • ARGB32: alpha 在最高 8 位元。
    image
  • RGBA32: alpha 在最低 8 位元。
    image
RGB565

用 16 位元的無號整數來表達(Red: 5 bits / Green: 6 bits / Blue: 5 bits)
效率很高,每個像素只需要 2 bytes,因此很常被使用。

轉換程式

將 RGB565 格式的 ui.bin 轉換成 RGB888 格式的 ui.rgb

//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();

假設某像素為 1111100000011111r 的變換:

  1. & 0xF800 > 1111100000000000
  2. >> 11 > 11111
  3. << 3 > 11111000,補足 8 位元

YUV color model

2.10 YUV Formats: 以使用的位元個數區分

電視使用的標準。Y 代表亮度,U、V 代表彩度。原因是人眼對亮度較敏感,需給予較大的頻寬。其與 RGB 之間的關係為:

image

Motion JPEG

維基百科

一種影像壓縮格式,其中每一訊框圖像都分別使用JPEG編碼。

只單獨的對某一訊框進行壓縮,而不考慮影像畫面中不同訊框之間的變化。因此壓縮率不高,但處理成本較低。

虛擬攝影機裝置

使用 Graphviz 重新製圖並嵌入到 HackMD 筆記頁面







_graph_name_

何謂虛擬攝影機裝置驅動程式

cluster_real



cluster_virtual




A

視訊輸入
(VideoX)



B

裝置驅動程式



B->A





e
Kernel



B->e


   使用虛擬攝影機   
   取代真實攝影機   



C

攝影機裝置



C->B





a
Userspace



b
Kernel



c
Hardware



D

視訊輸入
(VideoX)



E

裝置驅動程式



E->D





F

虛擬攝影機裝置



F->E





d
Userspace




f
Hardware




使用 V4l2 框架以及 Linux Frambuffer API
運作流程:將資料寫入虛擬 Frambuffer 中,影片播放程式可以透過 /dev/videoX 來播放影片







_graph_name_


cluster_a


cluster_b



A

視訊輸入
(/dev/VideoX)



B

vcam



B->A





a
Userspace



x




b
Kernel




C

Framebuffer API



C->B





D

虛擬 Framebuffer
(/dev/fbX)



D->C





c



y




d




V4L2 (Video for Linux 2)

Linux V4L2 學習

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 的結構體嵌入到自己所定義的結構體中,例如:
        ​​​​​​​​​​​​struct vcam_device *create_vcam_device(size_t idx,
        ​​​​​​​​​​​​                           struct vcam_device_spec *dev_spec)
        ​​​​​​​​​​​​{
        ​​​​​​​​​​​​    struct video_device *vdev;
        ​​​​​​​​​​​​    ...       
        ​​​​​​​​​​​​}
        
      • 透過 V4L2 的函式將自定義的結構體跟 V4L2 框架作綁定,例如:
        ​​​​​​​​​​​​ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
        
    • 函數綁定
      • 實作特定函式並且將函式跟 V4L2 框架綁定
      • 當某件事情發生時,驅動框架就會呼叫綁定的程式

Linux framebuffer

類似畫布

Frambuffer: RAM 中的一段連續記憶體,CPU 或 GPU 會把要顯示的影像放到 Framebuffer 中,讓 Display 裝置顯示。







_graph_name_


cluster_a

RAM



Framebuffer

Framebuffer



Display

Display



Framebuffer->Display





使用 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 核心哪天將初始值改掉。

確認後,提交 pull request,注意要讓舊的 Linux 核心也能編譯 vcam 核心模組。

TODO: 支援 DMABUF

提交 pull request

DMA-BUF (Direct Memory Access Buffer) : 允許在不同裝置間共享的 buffer。

查詢關鍵字:

grep -rn 'vb2_queue'

vb2_queue.io_modes 中啟用 VB2_DMABUF

- q->io_modes = VB2_MMAP | VB2_USERPTR | VB2_READ;
+ q->io_modes = VB2_MMAP | VB2_USERPTR | VB2_READ | VB2_DMABUF;

新增操作函式

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,
};
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。

reference?

改為 vb2_dma_contig_memops

- q->mem_ops = &vb2_vmalloc_memops;
+ q->mem_ops = &vb2_dma_contig_memops;

載入模組

$ sudo modprobe videobuf2-dma-contig

測試程式

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;
}