
Автором является Александра Бутрова AleksandraButrova, тема “Разработка графической подсистемы для встроенных операционных систем”. При написании диплома были использованы три открытых проекта: Embox, Nuklear и stb. Последний использовался только для загрузки картинок, а вот Nuklear являлся, по сути, виновником торжества. Можно сказать, что работа свелась к интеграции Nuklear и Embox. Первый предоставлял лёгкую графическую библиотеку, а Embox отвечал за встроенные системы.
До данной работы графические приложения для Embox могли разрабатываться только на фреймворке Qt, который, безусловно, является замечательным, поскольку он:
- Кросс-платформенный
- Содержит в себе много всего полезного
- Открытый и хорошо отлаженный
Но в то же время Qt не всегда подходит для встроенных систем, поскольку:
- Очень большой
- Требовательный по ресурсам
- Написан на С++, а не C
Кроме того, есть нюансы с лицензией. Короче, мы в проекте давно задумывались над портированием чего-нибудь легковесного и пристально смотрели в сторону уже упомянутого Храбровым Дмитрием DeXPeriX проекта Nuklear. Нам понравилось использование чистого С и маленькое количество кода (по сути, один заголовочный файл). Плюс прекрасная лицензия:
This software is dual-licensed to the public domain and under the following license: you are granted a perpetual, irrevocable license to copy, modify, publish and distribute this file as you see fit.
В общем, Nuklear прекрасно подходит для интеграции с другими проектами.
Конечно, поскольку это диплом, задача была не просто использовать библиотеку, которая понравилась научнику. Было рассмотрено 6 библиотек и выявлено два подхода к построению графических примитивов: retained и immediate. Кроме самих библиотек рассматривались и общие модели построения графических подсистем, начиная, конечно, с легендарной X11. Но поскольку основной акцент в работе был сделан на ограниченность ресурсов, то лучшим был признан своеобразный аналог directFB, присутствующий в Embox.
Возвращаясь к Nuklear, который
Приведу для сравнения код функции main
из оригинального примера
int main(int argc, char *argv[])
{
/* Platform */
static GLFWwindow *win;
int width = 0, height = 0;
/* GUI */
struct device device;
struct nk_font_atlas atlas;
struct nk_context ctx;
/* GLFW */
glfwSetErrorCallback(error_callback);
if (!glfwInit()) {
fprintf(stdout, "[GFLW] failed to init!\n");
exit(1);
}
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
win = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Demo", NULL, NULL);
glfwMakeContextCurrent(win);
glfwSetWindowUserPointer(win, &ctx);
glfwSetCharCallback(win, text_input);
glfwSetScrollCallback(win, scroll_input);
glfwGetWindowSize(win, &width, &height);
/* OpenGL */
glViewport(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
glewExperimental = 1;
if (glewInit() != GLEW_OK) {
fprintf(stderr, "Failed to setup GLEW\n");
exit(1);
}
/* GUI */
{device_init(&device);
{const void *image; int w, h;
struct nk_font *font;
nk_font_atlas_init_default(&atlas);
nk_font_atlas_begin(&atlas);
font = nk_font_atlas_add_default(&atlas, 13, 0);
image = nk_font_atlas_bake(&atlas, &w, &h, NK_FONT_ATLAS_RGBA32);
device_upload_atlas(&device, image, w, h);
nk_font_atlas_end(&atlas, nk_handle_id((int)device.font_tex), &device.null);
nk_init_default(&ctx, &font->handle);
glEnable(GL_TEXTURE_2D);
while (!glfwWindowShouldClose(win))
{
/* input */
pump_input(&ctx, win);
/* draw */
{struct nk_canvas canvas;
canvas_begin(&ctx, &canvas, 0, 0, 0, width, height, nk_rgb(250,250,250));
{
nk_fill_rect(canvas.painter, nk_rect(15,15,210,210), 5, nk_rgb(247, 230, 154));
nk_fill_rect(canvas.painter, nk_rect(20,20,200,200), 5, nk_rgb(188, 174, 118));
nk_draw_text(canvas.painter, nk_rect(30, 30, 150, 20), "Text to draw", 12, &font->handle, nk_rgb(188,174,118), nk_rgb(0,0,0));
nk_fill_rect(canvas.painter, nk_rect(250,20,100,100), 0, nk_rgb(0,0,255));
nk_fill_circle(canvas.painter, nk_rect(20,250,100,100), nk_rgb(255,0,0));
nk_fill_triangle(canvas.painter, 250, 250, 350, 250, 300, 350, nk_rgb(0,255,0));
nk_fill_arc(canvas.painter, 300, 180, 50, 0, 3.141592654f * 3.0f / 4.0f, nk_rgb(255,255,0));
{float points[12];
points[0] = 200; points[1] = 250;
points[2] = 250; points[3] = 350;
points[4] = 225; points[5] = 350;
points[6] = 200; points[7] = 300;
points[8] = 175; points[9] = 350;
points[10] = 150; points[11] = 350;
nk_fill_polygon(canvas.painter, points, 6, nk_rgb(0,0,0));}
nk_stroke_line(canvas.painter, 15, 10, 200, 10, 2.0f, nk_rgb(189,45,75));
nk_stroke_rect(canvas.painter, nk_rect(370, 20, 100, 100), 10, 3, nk_rgb(0,0,255));
nk_stroke_curve(canvas.painter, 380, 200, 405, 270, 455, 120, 480, 200, 2, nk_rgb(0,150,220));
nk_stroke_circle(canvas.painter, nk_rect(20, 370, 100, 100), 5, nk_rgb(0,255,120));
nk_stroke_triangle(canvas.painter, 370, 250, 470, 250, 420, 350, 6, nk_rgb(255,0,143));
}
canvas_end(&ctx, &canvas);}
/* Draw */
glfwGetWindowSize(win, &width, &height);
glViewport(0, 0, width, height);
glClear(GL_COLOR_BUFFER_BIT);
glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
device_draw(&device, &ctx, width, height, NK_ANTI_ALIASING_ON);
glfwSwapBuffers(win);
}}}
nk_font_atlas_clear(&atlas);
nk_free(&ctx);
device_shutdown(&device);
glfwTerminate();
return 0;
}
и модифицированного примера
int main(int argc, char *argv[]) {
long int screensize = 0;
uint8_t *fbp = 0;
struct fb_info *fb_info;
struct nk_color rgb_white = { .a = 0xff, .r = 0xff, .g = 0xff, .b = 0xff};
fb_info = fb_lookup(0);
printf("%dx%d, %dbpp\n", fb_info->var.xres, fb_info->var.yres,
fb_info->var.bits_per_pixel);
/* Figure out the size of the screen in bytes */
screensize = fb_info->var.xres * fb_info->var.yres
* fb_info->var.bits_per_pixel / 8;
/* Map the device to memory */
fbp = (uint8_t *) mmap_device_memory((void *) fb_info->screen_base,
screensize, PROT_READ | PROT_WRITE, MAP_SHARED,
(uint64_t) ((uintptr_t) fb_info->screen_base));
if ((int) fbp == -1) {
perror("Error: failed to map framebuffer device to memory");
exit(4);
}
printf("The framebuffer device was mapped to memory successfully.\n");
struct fb_fillrect rect;
rect.dx = 0;
rect.dy = 0;
rect.width = fb_info->var.xres;
rect.height = fb_info->var.yres;
rect.rop = ROP_COPY;
rect.color = rgba_to_device_color(fb_info, &rgb_white);
fb_fillrect(fb_info, &rect);
/* GUI */
static struct nk_context ctx;
static struct nk_canvas canvas;
uint32_t width = 0, height = 0;
static struct nk_user_font font;
font.userdata.ptr = (void *) font_vga_8x8.data;
font.height = font_vga_8x8.height;
font.width = your_text_width_calculation;
nk_init_default(&ctx, &font);
width = fb_info->var.xres;
height = fb_info->var.yres;
/* Draw */
while (1) {
/* what to draw */
canvas_begin(&ctx, &canvas, 0, 0, 0, width, height,
nk_rgb(100, 100, 100));
{
canvas.painter->use_clipping = NK_CLIPPING_OFF;
nk_fill_rect(canvas.painter, nk_rect(15, 15, 140, 140), 5,
nk_rgb(247, 230, 154));
nk_fill_rect(canvas.painter, nk_rect(20, 20, 135, 135), 5,
nk_rgb(188, 174, 118));
nk_draw_text(canvas.painter, nk_rect(30, 30, 100, 20),
"Text to draw", 12, &font, nk_rgb(188, 174, 118),
nk_rgb(0, 0, 0));
nk_fill_rect(canvas.painter, nk_rect(160, 20, 70, 70), 0,
nk_rgb(0, 0, 255));
nk_fill_circle(canvas.painter, nk_rect(20, 160, 60, 60),
nk_rgb(255, 0, 0));
nk_fill_triangle(canvas.painter, 160, 160, 230, 160, 195, 220,
nk_rgb(0, 255, 0));
nk_fill_arc(canvas.painter, 195, 120, 30, 0,
3.141592654f * 3.0f / 4.0f, nk_rgb(255, 255, 0));
nk_stroke_line(canvas.painter, 15, 10, 100, 10, 2.0f,
nk_rgb(189, 45, 75));
nk_stroke_rect(canvas.painter, nk_rect(235, 20, 70, 70), 10, 3,
nk_rgb(0, 0, 255));
nk_stroke_curve(canvas.painter, 235, 130, 252, 170, 288, 80, 305,
130, 1, nk_rgb(0, 150, 220));
nk_stroke_triangle(canvas.painter, 235, 160, 305, 160, 270, 220, 10,
nk_rgb(255, 0, 143));
nk_stroke_circle(canvas.painter, nk_rect(90, 160, 60, 60), 2,
nk_rgb(0, 255, 120));
{
struct nk_image im;
int w, h, format;
struct nk_rect r;
im.handle.ptr = stbi_load("SPBGU_logo.png", &w, &h, &format, 0);
r = nk_rect(320, 10, w, h);
im.w = w;
im.h = h;
im.region[0] = (unsigned short) 0;
im.region[1] = (unsigned short) 0;
im.region[2] = (unsigned short) r.w;
im.region[3] = (unsigned short) r.h;
printf("load %p, %d, %d, %d\n", im.handle.ptr, w, h, format);
nk_draw_image(canvas.painter, r, &im, nk_rgb(100, 0, 0));
stbi_image_free(im.handle.ptr);
}
}
canvas_end(&ctx, &canvas);
/* Draw each element */
draw(fb_info, &ctx, width, height);
}
nk_free(&ctx);
printf("\nEnd of program.\nIf you see it then something goes wrong.\n");
return 0;
}
Код работы с библиотекой почти не претерпел изменений. Изменения касались загрузки своих шрифтов, различного функционала openGL и других специфичных платформенных частей.
Самая важная платформо-зависимая часть — это, конечно, отрисовка: функции device_draw и draw соответственно. Собственно, это вызов того самого рендеринга. Так как Nuklear по типу отрисовки относиться к imediate, то присутствует цикл, в котором постоянно отрисовывается сцена путем вызова этой функции. Сам код отрисовки следующий:
для openGL
static void device_draw(struct device *dev, struct nk_context *ctx, int width, int height,
enum nk_anti_aliasing AA)
{
GLfloat ortho[4][4] = {
{2.0f, 0.0f, 0.0f, 0.0f},
{0.0f,-2.0f, 0.0f, 0.0f},
{0.0f, 0.0f,-1.0f, 0.0f},
{-1.0f,1.0f, 0.0f, 1.0f},
};
ortho[0][0] /= (GLfloat)width;
ortho[1][1] /= (GLfloat)height;
/* setup global state */
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glDisable(GL_CULL_FACE);
glDisable(GL_DEPTH_TEST);
glEnable(GL_SCISSOR_TEST);
glActiveTexture(GL_TEXTURE0);
/* setup program */
glUseProgram(dev->prog);
glUniform1i(dev->uniform_tex, 0);
glUniformMatrix4fv(dev->uniform_proj, 1, GL_FALSE, &ortho[0][0]);
{
/* convert from command queue into draw list and draw to screen */
const struct nk_draw_command *cmd;
void *vertices, *elements;
const nk_draw_index *offset = NULL;
/* allocate vertex and element buffer */
glBindVertexArray(dev->vao);
glBindBuffer(GL_ARRAY_BUFFER, dev->vbo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, dev->ebo);
glBufferData(GL_ARRAY_BUFFER, MAX_VERTEX_MEMORY, NULL, GL_STREAM_DRAW);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, MAX_ELEMENT_MEMORY, NULL, GL_STREAM_DRAW);
/* load draw vertices & elements directly into vertex + element buffer */
vertices = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
elements = glMapBuffer(GL_ELEMENT_ARRAY_BUFFER, GL_WRITE_ONLY);
{
/* fill convert configuration */
struct nk_convert_config config;
static const struct nk_draw_vertex_layout_element vertex_layout[] = {
{NK_VERTEX_POSITION, NK_FORMAT_FLOAT, NK_OFFSETOF(struct nk_glfw_vertex, position)},
{NK_VERTEX_TEXCOORD, NK_FORMAT_FLOAT, NK_OFFSETOF(struct nk_glfw_vertex, uv)},
{NK_VERTEX_COLOR, NK_FORMAT_R8G8B8A8, NK_OFFSETOF(struct nk_glfw_vertex, col)},
{NK_VERTEX_LAYOUT_END}
};
NK_MEMSET(&config, 0, sizeof(config));
config.vertex_layout = vertex_layout;
config.vertex_size = sizeof(struct nk_glfw_vertex);
config.vertex_alignment = NK_ALIGNOF(struct nk_glfw_vertex);
config.null = dev->null;
config.circle_segment_count = 22;
config.curve_segment_count = 22;
config.arc_segment_count = 22;
config.global_alpha = 1.0f;
config.shape_AA = AA;
config.line_AA = AA;
/* setup buffers to load vertices and elements */
{struct nk_buffer vbuf, ebuf;
nk_buffer_init_fixed(&vbuf, vertices, MAX_VERTEX_MEMORY);
nk_buffer_init_fixed(&ebuf, elements, MAX_ELEMENT_MEMORY);
nk_convert(ctx, &dev->cmds, &vbuf, &ebuf, &config);}
}
glUnmapBuffer(GL_ARRAY_BUFFER);
glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER);
/* iterate over and execute each draw command */
nk_draw_foreach(cmd, ctx, &dev->cmds)
{
if (!cmd->elem_count) continue;
glBindTexture(GL_TEXTURE_2D, (GLuint)cmd->texture.id);
glScissor(
(GLint)(cmd->clip_rect.x),
(GLint)((height - (GLint)(cmd->clip_rect.y + cmd->clip_rect.h))),
(GLint)(cmd->clip_rect.w),
(GLint)(cmd->clip_rect.h));
glDrawElements(GL_TRIANGLES, (GLsizei)cmd->elem_count, GL_UNSIGNED_SHORT, offset);
offset += cmd->elem_count;
}
nk_clear(ctx);
}
/* default OpenGL state */
glUseProgram(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glBindVertexArray(0);
glDisable(GL_BLEND);
glDisable(GL_SCISSOR_TEST);
}
Как видно, здесь присутствует еще один цикл
/* iterate over and execute each draw command */
nk_draw_foreach(cmd, ctx, &dev->cmds)
Как нетрудно догадаться, этот цикл проходит по всем командам для отрисовки сцены, и рисует их средствами платформы.
Поэтому аналогичный код функции draw для Embox тоже содержит этот цикл
static inline void draw(struct fb_info *fb, struct nk_context *ctx, int width, int height) {
assert(fb);
assert(ctx);
const struct nk_command *cmd;
/* iterate over and execute each draw command */
nk_foreach(cmd, ctx)
{
switch (cmd->type) {
case NK_COMMAND_NOP:
break;
case NK_COMMAND_LINE: {
const struct nk_command_line *c =
(const struct nk_command_line*) cmd;
embox_stroke_line( fb, c->begin.x, c->begin.y, c->end.x, c->end.y,
&c->color, c->line_thickness);
}
break;
case NK_COMMAND_CURVE: {
const struct nk_command_curve *c =
(const struct nk_command_curve*) cmd;
int x[4];
int y[4];
x[0] = c->begin.x;
x[1] = c->ctrl[0].x;
x[2] = c->ctrl[1].x;
x[3] = c->end.x;
y[0] = c->begin.y;
y[1] = c->ctrl[0].y;
y[2] = c->ctrl[1].y;
y[3] = c->end.y;
embox_stroke_curve( fb, x, y, &c->color, c->line_thickness);
}
break;
case NK_COMMAND_RECT: {
const struct nk_command_rect *c =
(const struct nk_command_rect*) cmd;
embox_stroke_rect( fb, c->x, c->y, c->w, c->h, &c->color,
(float) c->rounding, c->line_thickness);
}
break;
case NK_COMMAND_RECT_FILLED: {
const struct nk_command_rect_filled *c =
(const struct nk_command_rect_filled*) cmd;
embox_fill_rect( fb, c->x, c->y, c->w, c->h, &c->color);
}
break;
case NK_COMMAND_CIRCLE: {
const struct nk_command_circle *c =
(const struct nk_command_circle*) cmd;
embox_stroke_circle( fb, (float) c->x + (float) c->w / 2,
(float) c->y + (float) c->h / 2, (float) c->w / 2,
&c->color, c->line_thickness);
}
break;
case NK_COMMAND_CIRCLE_FILLED: {
const struct nk_command_circle_filled *c =
(const struct nk_command_circle_filled *) cmd;
embox_fill_circle( fb, c->x + c->w / 2, c->y + c->h / 2, c->w / 2,
&c->color);
}
break;
case NK_COMMAND_ARC: {
const struct nk_command_arc *c = (const struct nk_command_arc*) cmd;
embox_stroke_arc( fb, c->cx, c->cy, c->r, c->a[0], c->a[1], &c->color,
c->line_thickness);
}
break;
case NK_COMMAND_ARC_FILLED: {
const struct nk_command_arc_filled *c =
(const struct nk_command_arc_filled*) cmd;
embox_fill_arc( fb, c->cx, c->cy, c->r, c->a[0], c->a[1], &c->color);
}
break;
case NK_COMMAND_TRIANGLE: {
const struct nk_command_triangle *c =
(const struct nk_command_triangle*) cmd;
embox_stroke_triangle( fb, c->a.x, c->a.y, c->b.x, c->b.y, c->c.x,
c->c.y, &c->color, c->line_thickness);
}
break;
case NK_COMMAND_TRIANGLE_FILLED: {
const struct nk_command_triangle_filled *c =
(const struct nk_command_triangle_filled*) cmd;
embox_fill_triangle( fb, c->a.x, c->a.y, c->b.x, c->b.y, c->c.x,
c->c.y, &c->color);
}
break;
case NK_COMMAND_TEXT: {
const struct nk_command_text *c =
(const struct nk_command_text*) cmd;
embox_add_text( fb, ctx, c->x, c->y, &c->foreground, &c->background, c->string,
c->length);
}
break;
case NK_COMMAND_IMAGE: {
const struct nk_command_image *c =
(const struct nk_command_image*) cmd;
int color = rgba_to_device_color( fb, &c->col);
embox_add_image( fb, c->img, c->x, c->y, c->w, c->h, color);
}
break;
/* unrealized primitives */
/*
case NK_COMMAND_SCISSOR: {
const struct nk_command_scissor *s = (const struct nk_command_scissor*)cmd;
nk_draw_list_add_clip(&ctx->draw_list, nk_rect(s->x, s->y, s->w, s->h));
} break;
case NK_COMMAND_POLYGON: {
int i;
const struct nk_command_polygon*p = (const struct nk_command_polygon*)cmd;
for (i = 0; i < p->point_count; ++i) {
struct nk_vec2 pnt = nk_vec2((float)p->points[i].x, (float)p->points[i].y);
nk_draw_list_path_line_to(&ctx->draw_list, pnt);
}
nk_draw_list_path_stroke(&ctx->draw_list, p->color, NK_STROKE_CLOSED, p->line_thickness);
} break;
case NK_COMMAND_POLYGON_FILLED: {
int i;
const struct nk_command_polygon_filled *p = (const struct nk_command_polygon_filled*)cmd;
for (i = 0; i < p->point_count; ++i) {
struct nk_vec2 pnt = nk_vec2((float)p->points[i].x, (float)p->points[i].y);
nk_draw_list_path_line_to(&ctx->draw_list, pnt);
}
nk_draw_list_path_fill(&ctx->draw_list, p->color);
} break;
case NK_COMMAND_POLYLINE: {
int i;
const struct nk_command_polyline *p = (const struct nk_command_polyline*)cmd;
for (i = 0; i < p->point_count; ++i) {
struct nk_vec2 pnt = nk_vec2((float)p->points[i].x, (float)p->points[i].y);
nk_draw_list_path_line_to(&ctx->draw_list, pnt);
}
nk_draw_list_path_stroke(&ctx->draw_list, p->color, NK_STROKE_OPEN, p->line_thickness);
} break;
case NK_COMMAND_RECT_MULTI_COLOR: {
const struct nk_command_rect_multi_color *r = (const struct nk_command_rect_multi_color*)cmd;
nk_draw_list_fill_rect_multi_color(&ctx->draw_list, nk_rect(r->x, r->y, r->w, r->h),
r->left, r->top, r->right, r->bottom);
} break; */
default:
break;
}
}
nk_clear(ctx);
}
Тело цикла выглядит сильно по-другому, поскольку необходимо реализовать собственные примитивы, которые уже реализованы в OpenGL.
После реализации этих примитивов сцена стала отрисовываться корректно

Герб СПбГУ, конечно, добавлен для диплома и не присутствует в оригинальном примере от nuklear :)
Конечно, не всё было так просто. Первой проблемой, которая мешала скомпилировать заголовочный файл в его оригинальном виде, стало то, что nuklear ориентирован на стандарт c89, где нет ключевого слова inline, но нет и предупреждений (warnings) на статические функции которые не используются. У нас по умолчанию используется c99, точнее gnu-расширение, и нам пришлось сделать PR в оригинальный nuklear для поддержки c99.
Ещё одной проблемой, напрямую не связанной с дипломом, было то, что формат пикселей бывает различный, и формат изображения в памяти может не совпадать с аппаратным форматом. До этого мы использовали простое преобразование из обычного RGB с 8 битами на каждый канал в тот или иной аппаратный формат в драйвере конкретного графического устройства, пришлось добавить прослойку для полноценного преобразования для разных форматов.
Всего было опробовано 3 платформы:
- QEMU/x86 (графика boch)
- QEMU/ARM (графика pl110)
- STM32F7Discovery (встроенная графика)
С последней платформой возникло максимальное количество неприятностей: и проблема с выравниванием структур, и нехватка памяти для загрузки картинки, и книжная ориентация экрана (т.е. ширина изображения меньше высоты). Но в результате со всеми этими задачами Саша справилась, и захотелось запустить уже “настоящий” пример. Им стал тоже стандартный пример из nuklear skinning.
Внешний вид при запуске на QEMU/ARM

Ну и фотографии с платой STM32F7Discovery

canvas

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