В этой статье я хочу продемонстрировать использование DispmanX API одноплатных компьютеров Raspberry. DispmanX API предоставляет возможность создавать на десктопе Raspberry новые отображаемые слои. Слои висят над слоями. Их можно динамически создавать, удалять, перемещать, их можно масштабировать. При этом, сам видеоконтроллер будет их объединять и отображать на экране монитора. Интересно, что слои могут иметь альфа канал, и тогда, изображения всех слоев будут сами собой смешиваться. Так же, кроме 32-ти двухбитных ARGB слоев можно создавать, например, YUV420 слои, или слои других типов. В Raspberry по умолчанию уже есть два слоя. Самый нижний содержит изображение десктопа. Весь вывод через иксы идет в этот слой. И есть второй, самый верхний слой, в котором живет изображение курсора мыши.

Я покажу, как создавать новый слой, писать в него изображение и как его перемещать по экрану. Собственно говоря, на демо видео выше и показана работа такой программы. Здесь созданы четыре новых 32-х битных слоя ARGB. В каждый из слоев я записываю пикселы из заранее подготовленных битмапов. Мои битмапы это изображения облаков, солнца и воздушных шаров. Слои перемещаются по экрану с разной скоростью, нависая над самым нижним иксовым слоем.
Итак, DispmanX API. Это довольно низкоуровневая штука специфичная именно для Raspberry и его видеоконтроллеров. И это «нестандартная» штука. Вообще в ядре Linux и в графической подсистеме Linux есть место для программирования видео слоев через DRM, Direct Rendering Manager, но создатели Raspberry почему-то решили создать свой велосипед. Хотя, с другой стороны это не сложный велосипед, на котором вполне можно ездить. DispmanX работает на Pi-Zero, на Raspberry Pi / Pi2 / Pi3 и Pi4. В общем на всех малинках. Хотя, по правде говоря, на Pi4 уже есть нормальный OpenGLESv3. Тут такие бубны возможно уже и не нужны. Но с другой стороны, DispmanX гораздо проще (хотя и меньше возможностей), чем OpenGLES.

Чтобы писать программы с использованием этого DispmanX API нужно подключать заголовочный файл /opt/vc/include/bcm_host.h. Кроме этого, потребуется еще и линковать программу с библиотекой libbcm_host.so, которая расположена в папке /opt/vc/lib.

Все функции нужного нам API начинаются с vc_dispmanx_*…

Первое, что нужно сделать для создания новых слоев это получить доступ к дисплею с помощью следующей пары функций:

bcm_host_init();
DISPMANX_DISPLAY_HANDLE_T display = vc_dispmanx_display_open( 0 );

Теперь можно создать «ресурс», который будет содержать изображение слоя:

VC_IMAGE_TYPE_T type = VC_IMAGE_ARGB8888;
uint32_t UnusedImagePtr;
int SrcImageWidth = 512; //image must be 32 bytes aligned size
int SrcImageWidth = 196;
DISPMANX_RESOURCE_HANDLE_T resource = vc_dispmanx_resource_create( type, SrcImageWidth, SrcImageHeight,  &UnusedImagePtr );

Здесь тип слоя выбран 32-х битный с альфа каналом. Но могут быть другие типы, как я уже написал, даже YUV. Использование YUV слоя имеет смысл для динамических слоев, например, при воспроизведении видео. Тогда объем записываемых данных в экран существенно снижается, да и перекодировать YUV в RGB не нужно, что экономит ценные такты процессора.

После создания нового ресурса, полученный ранее хандлер display может быть использован для добавления нового слоя-элемента на дисплей:

VC_DISPMANX_ALPHA_T alpha;
	alpha.flags =
		(DISPMANX_FLAGS_ALPHA_T)(DISPMANX_FLAGS_ALPHA_FROM_SOURCE | DISPMANX_FLAGS_ALPHA_MIX);
	alpha.opacity = 255;
	alpha.mask = 0;
int OutLayer = 200;
DISPMANX_ELEMENT_HANDLE_T vc_element = vc_dispmanx_element_add(
		update,
		display,
		OutLayer,
		&dst_rect, resource, &src_rect, DISPMANX_PROTECTION_NONE, &alpha, NULL, DISPMANX_NO_ROTATE );

Тут есть еще один весьма важный параметер update. Каждый раз, когда нужно модифицировать содержимое дисплея, добавить или удалить слой, или переместить его, или записать новое изображение в слой, нужно отметить начало изменений и окончание изменений.

Для начала изменения элементов на дисплее нужно сделать:

DISPMANX_UPDATE_HANDLE_T update =  vc_dispmanx_update_start( Priority );

Затем меняете, все что нужно, создаете слой или перемещаете, пишите в слой новые пиксели и потом закрываете изменения с помощью функции:

vc_dispmanx_update_submit_sync( update );

Как можно понять из названия функции изменения вступят в силу после очередного кадрового импульса видеоразвертки.

Запись пикселов в слой производится функцией:

vc_dispmanx_resource_write_data(
		resource,
		type,
		pitch,
		Pixels, //pointer to ARGB pixels
		&rect );

К сожалению, несмотря на наличие параметра rect, невозможно обновить произвольный фрагмент внутри изображения. Можно обновить только «полосу», то есть top и bottom прямоугольника можно задавать, но left всегда будет 0 и right всегда будет по ширине изображения.

Как ни странно, это пожалуй и весь минимум знаний, необходимый для манипулирования слоями.

В своей программе я написал небольшую обертку над DispmanX API. Это позволяет мне каждый слой описывать отдельной структурой:

struct DISPMANX_ELEMENT {
    DISPMANX_RESOURCE_HANDLE_T  res_;
    DISPMANX_ELEMENT_HANDLE_T   vc_element_;
    VC_IMAGE_TYPE_T type_;
    uint32_t src_width_;
    uint32_t src_height_;
    uint32_t dst_layer_;
    uint32_t dst_width_;
    uint32_t dst_height_;
    int32_t  dst_x_;
    int32_t  dst_y_;
};

Ну и многие мои оберточные функции принимают параметром указатель на такую структуру:

void dispmanx_init();
void dispmanx_element_init(struct DISPMANX_ELEMENT* Element);
struct DISPMANX_ELEMENT dispmanx_element_create( VC_IMAGE_TYPE_T type, int SrcW, int SrcH, int OutX, int OutY, int OutW, int OutH, int OutLayer );
void dispmanx_element_delete(struct DISPMANX_ELEMENT* Element);
void dispmanx_element_write(struct DISPMANX_ELEMENT* Element, char* Pixels);
void dispmanx_element_move( DISPMANX_UPDATE_HANDLE_T update, struct DISPMANX_ELEMENT* Element, int32_t NewX, int32_t NewY );
DISPMANX_UPDATE_HANDLE_T dispmanx_start_update( int Priority );
void dispmanx_sync( DISPMANX_UPDATE_HANDLE_T Update );

Таким образом я могу создавать несколько слоев и с легкостью манипулировать ими.

Так же я написал функцию для чтения битмапа. Причем, функция хитрая — если в битмапе встречается чисто зеленый пиксел 0x00FF00, то я рассматриваю его как прозрачный пиксел в моем изображении и соответствующим образом выставляю байт Alpha этого пикселя в ноль.

В паинте нарисовал три изображения.

Облака:



Солнце:



Воздушные шары:



В программе создается четыре слоя. Первые два слоя создаются из одного и того же битмапа облака, но размер слоя делаю разным, использую масштабирование, вторые облака больше первых. Третий слой — это воздушные шары и в четвертый слой загружаю солнце:

typedef struct OBJ{
	int width_;
	int height_;
	int x_;
	int y_;
	int layer_;
	int speed_;
	char* pixels_;
} OBJ_;

int main(int argc , char *argv[])
{
	cout << "Hello Raspberry DispmanX API!\n";
	dispmanx_init();

	OBJ_ cloud1;
	cloud1.pixels_ = LoadBitmap( (char*)"clouds.bmp", &cloud1.width_, &cloud1.height_ );
	cloud1.layer_ = 100;
	cloud1.x_ = 100;
	cloud1.y_ = 120;
	cloud1.speed_ = 3;

	struct DISPMANX_ELEMENT cloud1_element = dispmanx_element_create(
		VC_IMAGE_ARGB8888, cloud1.width_, cloud1.height_, cloud1.x_, cloud1.y_, cloud1.width_, cloud1.height_, cloud1.layer_ );
	dispmanx_element_write( &cloud1_element, cloud1.pixels_ );

	OBJ_ cloud2;
	cloud2.pixels_ = LoadBitmap( (char*)"clouds.bmp", &cloud2.width_, &cloud2.height_ );
	cloud2.layer_ = 101;
	cloud2.x_ = 10;
	cloud2.y_ = 230;
	cloud2.speed_ = 2;

	struct DISPMANX_ELEMENT cloud2_element = dispmanx_element_create(
		VC_IMAGE_ARGB8888, cloud2.width_, cloud2.height_, cloud2.x_, cloud2.y_, cloud2.width_*1.3, cloud2.height_*1.4, cloud2.layer_ );
	dispmanx_element_write( &cloud2_element, cloud2.pixels_ );

	OBJ_ balls;
	balls.pixels_ = LoadBitmap( (char*)"balls.bmp", &balls.width_, &balls.height_ );
	balls.layer_ = 102;
	balls.x_ = -100;
	balls.y_ = 351;
	balls.speed_ = 5;

	struct DISPMANX_ELEMENT balls_element = dispmanx_element_create(
		VC_IMAGE_ARGB8888, balls.width_, balls.height_, balls.x_, balls.y_, balls.width_, balls.height_, balls.layer_ );
	dispmanx_element_write( &balls_element, balls.pixels_ );

	OBJ_ sun;
	sun.pixels_ = LoadBitmap( (char*)"sun.bmp", &sun.width_, &sun.height_ );
	sun.layer_ = 99;
	sun.x_ = -250;
	sun.y_ = 10;
	sun.speed_ = 1;

	struct DISPMANX_ELEMENT sun_element = dispmanx_element_create(
		VC_IMAGE_ARGB8888, sun.width_, sun.height_, sun.x_, sun.y_, sun.width_, sun.height_, sun.layer_ );
	dispmanx_element_write( &sun_element, sun.pixels_ );
.......................

В вечном цикле перемещаю слои по экрану с разной скоростью:


while(1)
	{
		this_thread::sleep_for( chrono::milliseconds(20) );
		cloud1.x_ += cloud1.speed_;
		if( cloud1.x_>= 1920 )
			cloud1.x_ = 10 - cloud1.width_;

		cloud2.x_ += cloud2.speed_;
		if( cloud2.x_>= 1920 )
			cloud2.x_ = 133 - cloud2.width_;

		balls.x_ += balls.speed_;
		if( balls.x_>= 1920 )
			balls.x_ = 200 - balls.width_;

		sun.x_ += sun.speed_;
		if( sun.x_>= 1920 )
			sun.x_ = 250 - sun.width_;

		DISPMANX_UPDATE_HANDLE_T update = dispmanx_start_update(10);
		dispmanx_element_move( update, &cloud1_element, cloud1.x_, cloud1.y_ );
		dispmanx_element_move( update, &cloud2_element, cloud2.x_, cloud2.y_ );
		dispmanx_element_move( update, &balls_element, balls.x_, balls.y_ );
		dispmanx_element_move( update, &sun_element,   sun.x_,   sun.y_ );
		dispmanx_sync( update );
	}

Вот и все.

Весь код моей программы можно взять на github.

Компилировать программу на raspberry — командой make. Потом запускать из командной строки: ./demo и получится то, что вы видите выше на видео демонстрации.

Кстати говоря, на Raspberry можно посмотреть список всех видеослоев командой vcgencmd с параметром dispmanx_list. Вот так выглядит вывод этой команды на Pi4 до запуска моего демо:



Как и я написал уже есть два слоя: слой для Xorg и слой для мыши.

А вот так выглядит список слоев после запуска моего demo:



Видно, что было добавлено четыре новых слоя. Я делал скриншоты на распбери командой scrot. Интересно, что она захватывает только нижний иксовый слой, поэтому на скриншоте не видно ни облаков ни воздушных шаров, которые находятся в других слоях.

Я знаю, что микрокомпьютеры Raspberry иногда используются для создания различных киосков. И для киосков иногда нужно делать OSD, On Screen Display — то есть накладывать одно изображение поверх другого. Мне кажется, что DispmanX API идеально подходит для этих применений. Возможно кому-то понравится такое решение.