📜 ⬆️ ⬇️

How to write a diploma using three open projects

It is no secret that in our project they use students to teach. More precisely, on the basis of the project, students master the practical aspects of system programming: they write diplomas, term papers, do research, and so on. Here is one diploma, successfully defended last summer, and will be discussed in this article.

The author is Aleksandra Butrova AleksandraButrova , the topic “Development of the graphics subsystem for embedded operating systems”. When writing a diploma, three open projects were used: Embox , Nuklear and stb . The latter was used only for downloading pictures, but Nuklear was, in fact, the cause of the celebration. We can say that the work has been reduced to the integration of Nuklear and Embox. The first provided an easy graphic library, and Embox was responsible for embedded systems.

Prior to this work, graphic applications for Embox could be developed only on the Qt framework, which is certainly remarkable because it:


But at the same time, Qt is not always suitable for embedded systems, because:
')

In addition, there are nuances with the license. In short, we in the project had been thinking about porting something lightweight for a long time and were looking intently towards the already mentioned Nuklear project by Dmitry Hrabrovy of DeXPeriX. We liked the use of pure C and a small amount of code (in fact, one header file). Plus a great license:
This software is a non-licensed software license.

In general, Nuklear is perfect for integration with other projects.

Of course, since this is a diploma, the task was not just to use the library that the scientist liked. 6 libraries were reviewed and two approaches to the construction of graphic primitives were identified: retained and immediate . In addition to the libraries themselves, general models for the construction of graphic subsystems were also considered, starting, of course, with the legendary X11 . But since the main focus of the work was on limited resources, the original directFB analogue present in Embox was recognized as the best.

Returning to Nuklear, which, by a strange coincidence, was nevertheless chosen as a graphic library, it should be noted that it has several rendering options (sets of functions for drawing primitives) for different platforms, examples of use for X11, sdl and OpenGL are given. In order to run it on another platform, you need to implement your own rendering. For Embox, of course, there was no rendering. The first practical task was to modify the existing example from the Nuklear repository so that it would somehow get together and run on Embox. For this, the simplest example was chosen - canvas, which, in fact, demonstrates the output of graphic primitives.

I will cite the main function code for comparison.
from the original example
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; } 


and modified example
 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) rw; im.region[3] = (unsigned short) rh; 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; } 


The code for working with the library almost did not change. Changes concerned loading of the fonts, various functionality of openGL and other specific platform parts.
The most important platform-dependent part is, of course, drawing: the device_draw and draw functions, respectively. Actually, this is the challenge of the very rendering. Since Nuklear is related to imediate by the type of drawing, there is a loop in which the scene is constantly drawn by calling this function. The drawing code itself is as follows:

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


As you can see, there is another cycle here.

 /* iterate over and execute each draw command */ nk_draw_foreach(cmd, ctx, &dev->cmds) 

As you might guess, this cycle goes through all the commands to draw the scene, and draws them with the platform tools.

Therefore, a similar draw function code for Embox also contains this loop.
 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->ax, c->ay, c->bx, c->by, c->cx, c->cy, &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->ax, c->ay, c->bx, c->by, c->cx, c->cy, &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); } 


The body of the loop looks much different, because you need to implement your own primitives, which are already implemented in OpenGL.

After the implementation of these primitives, the scene began to draw correctly.


The coat of arms of St. Petersburg State University, of course, is added for a diploma and is not present in the original example from nuklear :)

Of course, not everything was so simple. The first problem that prevented compiling the header file in its original form was that nuklear is oriented to the c89 standard, where there is no inline keyword, but there are no warnings (warnings) on static functions that are not used. We use c99 by default, or rather the gnu-extension, and we had to make PR into the original nuklear to support c99.

Another problem, not directly related to the diploma, was that the pixel format is different, and the image format in memory may not coincide with the hardware format. Before that, we used a simple conversion from a regular RGB with 8 bits per channel to one or another hardware format in the driver of a specific graphic device, we had to add a layer for full conversion for different formats.

A total of 3 platforms were tested:


With the last platform, the maximum amount of trouble arose: the problem with alignment of the structures, and the lack of memory to load the image, and the portrait orientation of the screen (i.e., the image width is less than the height). But as a result, Sasha coped with all these tasks and wanted to launch a “real” example. They also became a standard example from nuklear skinning.

Appearance when running on QEMU / ARM



Well, photos with the STM32F7Discovery card


canvas


skinning

I do not want to retell the diploma, the full text can be downloaded here . In conclusion, I would like to note that the author, while writing her diploma, participated in several living projects, gained practical experience in real work with distributed modern projects. And it is not so important that she knows the authors of one of these projects personally, it’s still easier to enter the project. After all, as I said, this project is not the only one that was used in the writing of the diploma. And in my opinion, theses on the basis of open existing projects should be as much as possible. After all, this is the most effective way to become a full-fledged specialist.

Source: https://habr.com/ru/post/347282/


All Articles