Basic Drawing

This chapter shows you how to use the basic functions in the QNX Graphics Framework library.

It covers:

Setting up GF

Let's look at the steps your application needs to perform to use the GF library for 2D drawing.

  1. Attach to a graphics device by calling gf_dev_attach(). This gives you a handle to the device, which you need to create draw contexts and surfaces, and to attach to displays.
  2. Attach to the device's display (or displays if there's more than one) by calling gf_display_attach(). This gives you a display handle, which you require to use hardware cursors and layers for the display.
  3. Attach to one or more of the display's layers by calling gf_layer_attach(). In the QNX Graphics Framework, displays always have at least one layer (layer 0).

    You need the handle for an attached layer to control any of the layer properties (such as brightness, contrast, and saturation), chroma and alpha blending, and viewports. You also use this handle to attach a surface to the layer.

  4. Create a surface for each layer by calling gf_surface_create_layer(). You require at least one surface per layer in order to display an image on that layer. You can target more than one layer at the same surface.
  5. Set the visible surface for each layer by calling gf_layer_set_surfaces().
  6. If you set any of the layer's parameters, commit the changes with gf_layer_update(). Any call to a gf_layer_set*() function must be followed by gf_layer_update() to make the changes take effect.
  7. Create a draw context by calling gf_context_create().
  8. Associate the surface with the draw context by calling gf_context_set_surface().

Note: Never pass NULL as the handle for a device, display, layer, surface, or context in functions that have a handle parameter. Doing so will cause your application to crash.

Let's look at the details of each of these steps.

Attaching to a device and display

In the example below, you attach to the first device in dev/io-display (which is represented by the manifest GF_DEVICE_INDEX(0) — you can also attach to a device by name). Then, you attach to each of the device's displays, and print some information about that display's resolution, refresh rate, and number of layers. Finally, when the application is ready to exit, it's good practice to call gf_dev_detach() to disconnect from the device.


Note: Calling gf_dev_detach() isn't strictly necessary, as the monitor process io-display will clean up resources when the application exits. However, it's good practice to do so.

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


Attaching to display layers

In this code snippet, you attach to the main layer (the main_layer_index member of the display's information structure) of the display returned by 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");
    }

Create and set surfaces

In this snippet, the application creates a surface for the layer attached in the previous step using gf_surface_create_layer(). This surface has the same dimensions as the display (the xres and yres members of display_info), and the same format (which you can retrieve from an attached layer using gf_layer_query(), and by inspecting the format member of the information structure). The surface is then displayed on the layer using 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");
    }
}

Once you've created a surface, you can fill in a gf_surface_info structure with the surface information (most importantly, the surface ID), using gf_surface_get_info().

Create a draw context

The final step is to create a draw context, and set a previously created surface for the context.



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


At this point your application can start calling draw functions (see below). To draw you need to:

  1. Call gf_draw_begin() to gain exclusive access to the graphics hardware. This function blocks until the graphics hardware is released by any other rendering threads.
  2. Call any gf_draw*() or gf_context*() functions (except for gf_context_set_surface()) once you have exclusive access to the graphics hardware. In fact, you should limit your calls to rendering and context functions inside a gf_draw_begin() ... gf_draw_end() block. This is for two reasons:
  3. A proper Advanced Graphics application must call gf_draw_flush() or gf_draw_finish() to ensure the draw commands are marshalled to the graphics device(s).
  4. Call gf_draw_end() to release the graphics hardware. Every successful call to gf_draw_begin() must be matched by a call to gf_draw_end().

Before your application exits, it's a good idea to clean up allocated resources. Although not strictly required, you should:


Note: If an application terminates abnormally, or if your application doesn't clean up GF resources, io-display will automatically perform the cleanup.

Using draw coordinates

For any function that takes the coordinates of a rectangle (such as gf_draw_rect(), gf_draw_blit*(), gf_context_set_clipping(), and so on), the order of the coordinates is important. By convention, the upper-right corner must be first (parameters x1,y1), followed by the lower-left (parameters x2,y2). This limitation reduces runtime overhead and code size. If you reverse the order of coordinates, the function won't work, and it returns GF_ERR_PARM.

Also note that rectangle coordinates are inclusive. So, for example, drawing a rectangle of (0,0) and (0,0) with gf_draw_rect() fills a rectangle of one pixel.

Drawing rectangles

Use gf_draw_rect() to draw a filled rectangle. This function takes a handle for a draw context, and the coordinates of the upper-left and lower-right corner of rectangle (inclusive). The fill color is the foreground color for the context.

Here's an example that draws a small black rectangle inside a larger white rectangle:


Note: The code examples in this chapter assume you've already performed all the steps required to start drawing using 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);

Drawing lines and polygons

Use gf_draw_polyline() and gf_draw_poly_fill() to draw polygons.

Both of these functions take the draw context, an array of points, and the number of points in the array as arguments. The gf_draw_polyline() function takes a flag as the fourth argument to indicate whether the last point should be joined to the first. If you simply pass 0, this function draws an open polyline. Here's an example that draws two triangles using these functions:

...
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);
...

Bitmaps

A bitmap, in the GF library, is simply a stream of bytes that represents a one-color image. Images in other “bitmap” formats must be processed by the image library.

You can use gf_draw_bitmap() to draw a 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);

Blitting

Use gf_draw_blit1() to blit within a surface, or gf_draw_blit2() to blit between potentially two surfaces. If you want to scale the area (increasing or decreasing its size), use gf_draw_blitscaled().

For example, to blit the bitmap drawn in the previous example, add these lines of code:

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

Note that we pass NULL for the source or destination surface for the scaled blit or gf_draw_blit2(). This selects the surface associated with the context parameter.


Note: If you are blitting between two surfaces with different formats, and your hardware doesn't support color-space conversion, you should put the source surface in system RAM to improve performance. Be sure to set the GF_SURFACE_CREATE_CPU_FAST_ACCESS flag when you create the source surface using gf_surface_create().

Multithreaded applications

The GF library is thread safe. It ensures that your thread or process has exclusive access to the hardware in a gf_draw_begin() and gf_draw_end() block, and leaves the hardware in a known state.

Threads, whether in the same or different processes, can render to the same surface. When the threads are in different processes, only one thread needs to attach to the display and the layer the shared surface is associated with. To share a surface across processes, one thread creates the surface and communicates its surface ID (the sid member of the surface information structure filled in by gf_surface_get_info()). The second thread then calls gf_surface_attach_by_sid() and passes the surface ID. Keep in mind that there is nothing preventing one thread from rendering over top of the other thread's work.

Threads within the same process can share a connection to a graphics device to reduce the memory required to allocate device handles. The first thread attaches to the device using gf_dev_attach(), and other threads call gf_dev_register_thread() to register themselves as users of the device.

Debugging

To make debugging easier, you can link your application against the GF debug library libgf_g instead of the standard library libgf.

While using the debug version of the library adds a performance penalty at runtime, it can be useful for catching problems related to errant usage of the GF API, such as attempting to draw when the device is not locked by the calling thread, or calling functions which could result in deadlock while the device is locked by the calling thread.

The debug versions of libgf are compiled with assertions enabled. If an assertion fails, a message is sent to the standard error, and a NULL pointer dereference is made deliberately. This allows a stack backtrace to be generated (either using dumper, or directly using the debugger) to determine the source of the offending call. In addition to the asserts, other error messages may be sent to standard error when running with the debug version.