RK3568 без иксов: VPU → RGA2 → DRM overlay

Задача звучала скучно: показать четыре RTSP-потока на экране, подключённом к плате. Закончилось выпиливанием браузера и разговором с ядром напрямую.

Первая версия была честной и глупой: Chromium в киоске, четыре <video> на страницу, поток отдаёт go2rtc. Работает. Только плата при этом греется до 70°C, вентилятора нет, а top показывает 200% CPU из четырёх доступных ядер. И это на 1080p15.

Проблема не в Chromium как таковом. Проблема в том, что на этом пути кадр успевает несколько раз проехаться через память: декодер отдаёт NV12, браузер конвертирует в RGB, компоновщик кладёт в свой буфер, дальше GPU рисует текстуру. На десктопе этого не замечаешь. На RK3568 с его пропускной способностью памяти — замечаешь сразу.

Что вообще есть в железе

У RK3568 три отдельных блока, которые все хотят одного и того же кадра:

Ключевая мысль: если все трое умеют работать с dma-buf, то кадр можно не копировать ни разу. Декодер выделяет буфер, RGA2 читает его и пишет в другой, VOP2 забирает результат прямо на экран. CPU только раздаёт указатели.

Декодер

MPP — библиотека своеобразная, но рабочая. Смысл в том, чтобы попросить у неё буферы из drm-аллокатора, а не из своего кучного:

mpp_buffer_group_get_external(&grp, MPP_BUFFER_TYPE_DRM);
mpi->control(ctx, MPP_DEC_SET_EXT_BUF_GROUP, grp);

После этого каждый декодированный кадр — это MppFrame, у которого можно спросить fd:

MppBuffer buf = mpp_frame_get_buffer(frame);
int fd = mpp_buffer_get_fd(buf);   // вот он, dma-buf

Этот fd — валюта, которой дальше расплачиваемся со всеми остальными.

Грабля номер один: по умолчанию MPP отдаёт кадры в формате NV12 с выравниванием по 16 пикселей. Если поток 1080p, то реальная высота буфера будет 1088, а не 1080. Не учтёшь — получишь восемь строк мусора внизу и полдня недоумения.

RGA2 вместо GPU

Дальше кадр надо уменьшить (четыре потока на один экран — значит, каждый в четверть) и, возможно, поменять формат. Инстинкт говорит «GLES, текстура, шейдер». Инстинкт неправ: GPU на этой плате нужен для другого, а RGA2 стоит без дела и делает ровно это.

rga_info_t src = {0}, dst = {0};
src.fd = decoded_fd;
dst.fd = scaled_fd;
src.mmuFlag = dst.mmuFlag = 1;
rga_set_rect(&src.rect, 0, 0, 1920, 1080, 1920, 1088, RK_FORMAT_YCbCr_420_SP);
rga_set_rect(&dst.rect, 0, 0,  960,  540,  960,  540, RK_FORMAT_YCbCr_420_SP);
c_RkRgaBlit(&src, &dst, NULL);

Скейл 1080p → 540p занимает около 2 мс и ноль процентов CPU. Формат оставляю NV12: VOP2 умеет его напрямую, конвертить в RGB незачем.

DRM: плоскости, а не окна

Вот здесь и происходит главное. Обычно кадры складывает компоновщик — рисует всё в один буфер и отдаёт его дисплею. Но VOP2 умеет принять несколько буферов и сложить их сам, аппаратно, при выводе. Это и есть overlay planes.

Схема такая: открываем /dev/dri/card0, просим atomic-режим, находим свободные плоскости:

drmSetClientCap(fd, DRM_CLIENT_CAP_ATOMIC, 1);
drmSetClientCap(fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1);

Каждый кадр импортируем как DRM framebuffer поверх того же dma-buf:

drmPrimeFDToHandle(fd, scaled_fd, &handle);
drmModeAddFB2(fd, 960, 540, DRM_FORMAT_NV12, handles, pitches, offsets, &fb_id, 0);

И раскладываем четыре плоскости по экрану одним атомарным коммитом:

drmModeAtomicAddProperty(req, plane_id, prop_fb_id,  fb_id);
drmModeAtomicAddProperty(req, plane_id, prop_crtc_x, x);
drmModeAtomicAddProperty(req, plane_id, prop_crtc_y, y);
/* ... для каждой из четырёх ... */
drmModeAtomicCommit(fd, req, DRM_MODE_ATOMIC_NONBLOCK | DRM_MODE_PAGE_FLIP_EVENT, NULL);

Атомарность здесь не для красоты: все четыре плоскости меняются в один vblank. Никакого тиринга и никакой рассинхронизации между потоками.

Грабля номер два: количество overlay-плоскостей конечно, и не каждая умеет всё. У VOP2 часть плоскостей поддерживает YUV и скейл, часть — только RGB. Смотреть надо не в документацию, а в drmModeGetPlaneResources и реальные IN_FORMATS. У меня на четвёртый поток плоскости уже не хватило — пришлось два потока класть на одну через RGA2-композитинг заранее.

Что получилось

вариантCPUтемпература
Chromium + go2rtc~200%70°C
MPP → RGA2 → DRM~17%47°C

17% — это в основном разбор RTSP и сеть, собственно видео почти ничего не стоит. Плата работает без вентилятора и не троттлит.

Побочный приз: всё это не требует ни X11, ни Wayland. Systemd-юнит стартует бинарь прямо на tty, экран оживает через полторы секунды после питания. Из зависимостей — libdrm, librga, librockchip_mpp.

Стоило ли

Если честно — недели три вечеров. Для одного экрана в кладовке это, конечно, чистое упрямство. Но выяснилось, что весь путь «dma-buf по кругу, CPU только диспетчер» переносится на любой SoC, где есть V4L2 M2M и KMS: Allwinner, i.MX, тот же Raspberry Pi. Rockchip-специфичен здесь только RGA2, и то заменяется на V4L2 M2M-скейлер.

Дальше хочу выкинуть librga и перейти на честный V4L2 — тогда код станет переносимым целиком. Но это уже другой вечер.

← все заметки