Основы рисования

В этой главе будет продемонстрировано использование основных функций библиотеки Graphics Framework

Подразделы:

Настройка GF
Присоединение к графическому устройству и дисплею
Присоединение аппаратных слоев дисплея
Создание поверхности и назначение ее слою
Создание контекста рисования (рендеринга)
Система координат Graphics Framework
Рисование прямоугольников
Рисование линий и полигонов
Битовые карты
Блиттинг
Многопоточные приложения
Отладка

Настройка GF

Рассмотрим шаги, которые должно выполнить приложение для использования библиотеки GF для рисования 2D графики.

  1. Присоединение к графическому устройству посредством вызова gf_dev_attach(). Функция возвращает дескриптор устройства, с помощью которого можно создавать контексты рисования, присоединяться к дисплеям и выделять видео память.
  2. Присоединение к дисплею графического устройства (или к дисплеям) посредством вызова gf_display_attach(). Функция возвращает дескриптор дисплея, который используется для управления курсором и аппаратными слоями дисплея.
  3. Присоединение одного или нескольких аппаратных слоев дисплея посредством вызова gf_layer_attach(). В Graphics Framework все дисплеи имеют хотя бы один аппаратный слой, имеющий индекс 0.

    Для управления параметрами слоев требуется его дескриптор. К параметрам слоев относятся яркость, контрастность, насыщенность, режим хрома и альфа смешивания, а также окна обзора (от англ. "viewports"). Дескриптор также используется для присоединения к слою отображаемой поверхности.

  4. Создание поверхностей для каждого присоединенного слоя посредством вызова gf_surface_create_layer(). Требуется по крайней мере одна поверхность для каждого слоя для отображения в нем изображения. Допустимо использование одной поверхности для вывода изображения в несколько слоев.
  5. Установка видимой поверхности для каждого используемого слоя посредством вызова gf_layer_set_surfaces().
  6. После установки параметров слоев требуется фиксация изменений с помощью gf_layer_update(). Любые вызовы функций вида gf_layer_set*() должны иметь завершающий вызов функции gf_layer_update() для применения внесенных изменений.
  7. Создание контекста рисования (рендеринга) посредством вызова gf_context_create().
  8. Ассоциирование поверхности с контекстом рисования посредством вызова gf_context_set_surface().


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

Рассмотрим эти шаги детально.

Присоединение к графическому устройству и дисплею

В следующем примере производится присоединение к первому устройству в директории /dev/io-display (определяется декларацией GF_DEVICE_INDEX(0) — также можно присоединять устройство по имени). После этого производится подключение к каждому имеющемуся дисплею графического устройства и вывод информации об их разрешениях, частотах обновления и количестве имеющихся слоев. При завершении приложения является хорошей практикой использовать функцию gf_dev_detach() для освобождения ресурсов, затраченных на присоединение устройства.


Note: Вызов gf_dev_detach() не является обязательным, поскольку менеджер графической подсистемы io-display освободит ресурсы автоматически при терминировании приложения. Однако, это является хорошей практикой.

gf_dev_t gdev;
gf_dev_info_t gdev_info;
gf_display_t display;
gf_display_info_t display_info;
if ( gf_dev_attach( &gdev, GF_DEVICE_INDEX( 0 ), &gdev_info ) != GF_ERR_OK )
{
printf( "gf_dev_attach() failed\n" );
return (-1);
}
printf( "Number of displays: %d\n", gdev_info.ndisplays );
for ( i = 0; i < gdev_info.ndisplays; i++; )
{
printf( "Display %d: ", i );
if ( gf_display_attach( &display, gdev, i, &display_info ) == GF_ERR_OK )
{
printf( "%dX%d, refresh = %dHz\n", display_info.xres, display_info.yres, display_info.refresh );
printf( "Number of layers: %d\n", display_info.nlayers );
} else {
printf( "gf_display_attach() failed\n" );
}
}
...
gf_dev_detach( gdev );

Присоединение аппаратных слоев дисплея

Этот фрагмент кода производится подключение к основному слою (определяется полем main_layer_index структуры, содержащей информацию о дисплее) дисплея, который был подключен функцией gf_display_attach():

if ( display != NULL )
{
gf_layer_t layer;
if ( gf_layer_attach( &layer, display, main_layer_index, 0 ) == GF_ERR_OK )
{
...
} else {
printf( "gf_layer_attach() failed\n" );
}
}

Создание поверхности и назначение ее слою

В этом фрагменте кода приложение создает поверхность для присоединенного слоя, используя функцию gf_surface_create_layer(). Эта поверхность имеет тоже самое разрешение, что и основной дисплей (используются поля xres и yres структуры display_info), а также пиксельный формат (можно получить список поддерживаемых пиксельных форматов присоединенного слоя, используя функцию gf_layer_query(), и анализируя поле format информационной структуры). Поверхность будет являться отображаемой после успешного вызова функции gf_layer_set_surfaces().

gf_surface_t surface;
width = display_info.xres;
height = display_info.yres;
...
if ( gf_surface_create_layer( &surface, &layer, 1, 0, width, height, layer_format, NULL, 0 ) == GF_ERR_OK )
{
gf_layer_set_surfaces( layer, &surface, 1 );
/* draw stuff here */
} else {
printf( "gf_surface_create_layer() failed\n" );
}

После успешного создания поверхности возможно запросить у GF структуру gf_surface_info, содержащую информацию о поверхности (включая ID поверхности), используя gf_surface_get_info().

Создание контекста рисования (рендеринга)

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

if ( gf_context_create( &context ) != GF_ERR_OK )
{
fprintf( stderr, "gf_context_create failed\n" );
return (-1);
}
if ( gf_context_set_surface( context, surface[cursurf] ) != GF_ERR_OK )
{
fprintf( stderr, "gf_context_set_surface failed\n" );
return (-1);
}

Начиная с этого момента можно вызывать функции рисования (рассматриваются далее). Для того, чтобы что-то нарисовать необходимо:

  1. Вызов gf_draw_begin() обеспечивает получение эксклюзивного доступа к графическому устройству. Функция блокирует все остальные потоки, которые используют функции рендеринга в пределах данного графического устройства. Рекомендуется минимизировать количество операций, выполняющихся с эксклюзивным доступом к графическому устройству.
  2. Допустим вызов произвольных функций gf_draw*() или gf_context*() (за исключением gf_context_set_surface()) при наличии эксклюзивного доступа к графическому устройству. Ограничение числа операций рендеринга внутри блока gf_draw_begin() ... gf_draw_end() рекомендуется по следующим причинам:
  3. Корректное приложение должно использовать вызов gf_draw_flush() или gf_draw_finish() для гарантирования своевременного исполнения команд графическим устройством.
  4. Вызов gf_draw_end() используется для передачи эксклюзивного доступа другим потребителям. Каждый успешный вызов gf_draw_begin() должен завершаться соответствующим вызовом gf_draw_end().

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


Note: Если приложение терминируется экстренно или не освобождает ресурсы, io-display автоматически выполнит необходимые операции по освобождению ресурсов.

Система координат Graphics Framework

Для любой функции, использующей координаты прямоугольных областей (таких как, например gf_draw_rect(), gf_draw_blit*() и gf_context_set_clipping()), важен порядок следования координат. Координаты, левого-верхнего угла области должны следовать первыми (параметры x1, y1), за ними должны располагаться координаты правого-нижнего угла (параметры x2, y2). Это ограничение сокращает время выполнения и размер кода. Если порядок следования координат изменен, функции рисования не будут работать и вернут код ошибки GF_ERR_PARM.

Необходимо также заметить, что координаты прямоугольных областей включают их границы. Например, рисование прямоугольника из точки (0,0) в точку (0,0) с помощью функции gf_draw_rect() отобразит на экране один пиксель.

Рисование прямоугольников

Функция gf_draw_rect() позволяет нарисовать заполненный (залитый) прямоугольник. Функция принимает дескриптор контекста рисования, координаты левого-верхнего и правого-нижнего углов прямоугольника (включительно). Цвет заливки соответствует фронтальному цвету (англ.: "foreground"), заданному в контексте рисования.

Пример использования данной функции включает рисование малого прямоугольника внутри более крупного:


Note: Все нижеследующие примеры исходного кода подразумевают, что выполнены все описанные шаги по инициализации библиотеки GF и ее ресурсов.

gf_context_set_fgcolor( context, 0xFFFFFF );
gf_draw_rect( context, 10, 10, 50, 50 );
gf_context_set_fgcolor( context, 0x000000 );
gf_draw_rect( context, 20, 20, 40, 40 );

Рисование линий и полигонов

Используйте функции gf_draw_polyline() и gf_draw_poly_fill() для рисования ломаных линий и полигонов.

Обе эти функции принимают на вход следующие аргументы: контекст рисования, массив точек, его размер. Функция gf_draw_polyline() также принимает флаг, позволяющий нарисовать замкнутую ломаную линию. Если это флаг не устанавливать (например, передав функции 0) будет отображена не замкнутая поли-линия. В примере отображаются два треугольника, используя эти функции:

gf_context_set_fgcolor( context, 0xFFFFFF );
pts[0].x = 20;
pts[0].y = 20;
pts[1].x = 10;
pts[1].y = 30;
pts[2].x = 40;
pts[2].y = 30;
/* draws two lines, the first two sides of the triangle set the
flags to GF_DRAW_POLYLINE_CLOSED to complete the triangle: */
gf_draw_polyline( context, pts, 3, 0 );
/* draws a complete, filled triangle: */
gf_draw_poly_fill( context, pts, 3 );

Битовые карты

Битовая карта в терминологии библиотеки GF - это поток байт, определяющих изображение, имеющее единственный отображаемый цвет. Изображения в других форматах “битовых карт” должны обрабатываться специализированным библиотеками.

Функция gf_draw_bitmap() позволяет нарисовать обсуждаемую битовую карту.

uint8_t image[] = { 0x08, 0x1C, 0x3E, 0x7F,
0x3E, 0x1C, 0x08, 0x00 };
gf_bitmap_t btmap;
btmap.image = image;
btmap.stride = 1;
btmap.bit0_offset = 0;
btmap.transparent = 0;
btmap.dim.w = 8;
btmap.dim.h = 8;
gf_context_set_bgcolor( context, 0xFF0000 );
gf_context_set_fgcolor( context, 0xFFFFFF );
gf_draw_bitmap( context, &btmap, 10, 10 );

Блиттинг

Для блиттинга в пределах одной поверхности используется функция gf_draw_blit1(). Для блиттинга между двумя поверхностями предназначена функция gf_draw_blit2(). Если при блитинге необходимо использовать две поверхности с различными размерами, возможно осуществление блиттинга с масштабированием при помощи функции gf_draw_blitscaled().

Пример вызова этих функций для битовой карты из предыдущего фрагмента кода:

gf_draw_blit1( context, 10, 10, 18, 18, 25, 25 );
gf_draw_blitscaled( context, NULL, NULL, 10, 10, 18, 18, 30, 30, 60, 60 );

Необходимо отметить, что в качестве поверхности-источника и поверхности-приемника в вызове gf_draw_blitscaled() передан NULL. Это позволяет адресоваться к поверхности, ассоциированной с контекстом рисования.


Note: Если необходимо выполнить блиттинг между двумя поверхностями с различными пиксельными форматами, и графический контроллер не поддерживает CSC (от англ.: "color-space conversion"), вам следует разместить поверхность-источник в системную память (RAM) для повышения производительности. Для этого необходимо установить флаг GF_SURFACE_CREATE_CPU_FAST_ACCESS при создании поверхности-источника в фукнуции gf_surface_create().

Многопоточные приложения

Библиотека GF является потоко-базопасной. Она подразумевает использование эксклюзивного доступа к оборудованию посредством функций gf_draw_begin() и gf_draw_end(), которые позволяют сохранять предсказуемость поведения графического контроллера.

Потоки, независимо от того к какому процессу они принадлежат, могут адресоваться к одной и той же поверхности. Когда такие потоки находятся в различных процессах, только один поток должен присоединять соответствующие дисплей и слой, а также ассоциировать разделяемую поверхность с ними. Для разделения поверхности между процессами один из потоков должен создать поверхность, определить ее идентификатор (англ.: "SID"; поле sid структуры, заполняемой функцией gf_surface_get_info()) и передать ее в другой процесс. Второй поток должен вызвать функцию gf_surface_attach_by_sid() с полученным SID. Необходимо отметить, что требуется дополнительная синхронизация при параллельной работе потоков с этой поверхностью, если библиотека GF не привлекается для рендеринга содержимого.

Потоки в пределах одного процесса могут разделять соединение с графическим устройством для минимизации потребляемых ресурсов на аллоцирование дескрипторов. Первый поток идентифицирует себя в качестве клиента графической подсистемы с помощью функции gf_dev_attach(), остальные потоки могут вызвать gf_dev_register_thread() для регистрации себя в качестве таковых.

Отладка

Для облегчения отладки приложений можно линковать приложение с отладочной версией библиотеки GF - libgf_g - вместо стандартной версии libgf.

Пока используется отладочная версия библиотеки допустимо появление дополнительных накладных расходов на этапе выполнения приложения, что, однако, облегчает диагностирование проблем использования GF API. Например, попытки использования функций рисования без получения эксклюзивного доступа к устройству текущим потоком или вызов функций, которые могут привести к неразрешимым блокировкам (англ.: "deadlock") при наличии действующей эксклюзивной блокировки в текущем потоке.

Отладочная версия библиотеки libgf собраны с включением дополнительного отладочного вывода и проверками. Если дополнительные проверки завершаются с ошибками, посылается отладочное сообщение в стандартный файл ошибок и преднамеренно разыменовывается указатель NULL для аварийного терминирования приложения. Это позволяет сгенерировать отладочную трассу и стэк вызовов (например, при использовании утилиты dumper или отладчика) для определения источника проблемы и ошибочного вызова. Кроме подобных сообщений в этой версии библиотеки могут быть также добавлены дополнительные диагностические сообщения, не сопровождающиеся принудительной генерацией исключений.


Warning: Необходимо отметить, что использование утилиты dumper или отладчика при получения отладочной информации приложения, работающего с некоторыми драйверами, может приводить к неразрешимым блокировкам (англ.: "deadlock") на уровне графического контроллера и/или чипсета. Объясняется это доступностью регистров контроллера в адресном пространстве приложения и наличием у контроллера неразрешенных для чтения регистров. Подобные ограничения игнорируются отладчиком и утилитой dumper, что приводит к деструктивным последствиям. К счастью, это достаточно легко диагностируется и решается запретом на исполнение указанных утилит для решения задач отладки графически х приложений.




Предыдущий раздел: Библиотека Graphics Framework