Framebuffer Drawing Primitives

Name

Drawing Primitives -- updating the display

Synopsis

#include <cyg/io/framebuf.h>
      

void cyg_fb_write_pixel(cyg_fb* fbdev, cyg_ucount16 x, cyg_ucount16 y, cyg_fb_colour colour);

cyg_fb_colour cyg_fb_read_pixel(cyg_fb* fbdev, cyg_ucount16 x, cyg_ucount16 y);

void cyg_fb_write_hline(cyg_fb* fbdev, cyg_ucount16 x, cyg_ucount16 y, cyg_ucount16 len, cyg_fb_colour colour);

void cyg_fb_write_vline(cyg_fb* fbdev, cyg_ucount16 x, cyg_ucount16 y, cyg_ucount16 len, cyg_fb_colour colour);

void cyg_fb_fill_block(cyg_fb* fbdev, cyg_ucount16 x, cyg_ucount16 y, cyg_ucount16 width, cyg_ucount16 height, cyg_fb_colour colour);

void cyg_fb_write_block(cyg_fb* fbdev, cyg_ucount16 x, cyg_ucount16 y, cyg_ucount16 width, cyg_ucount16 height, const void* data, cyg_ucount16 offset, cyg_ucount16 stride);

void cyg_fb_read_block(cyg_fb* fbdev, cyg_ucount16 x, cyg_ucount16 y, cyg_ucount16 width, cyg_ucount16 height, void* data, cyg_ucount16 offset, cyg_ucount16 stride);

void cyg_fb_move_block(cyg_fb* fbdev, cyg_ucount16 x, cyg_ucount16 y, cyg_ucount16 width, cyg_ucount16 height, cyg_ucount16 new_x, cyg_ucount16 new_y);

void cyg_fb_synch(cyg_fb* fbdev, cyg_ucount16 when);

void CYG_FB_WRITE_PIXEL(FRAMEBUF, cyg_ucount16 x, cyg_ucount16 y, cyg_fb_colour colour);

cyg_fb_colour CYG_FB_READ_PIXEL(FRAMEBUF, cyg_ucount16 x, cyg_ucount16 y);

void CYG_FB_WRITE_HLINE(FRAMEBUF, cyg_ucount16 x, cyg_ucount16 y, cyg_ucount16 len, cyg_fb_colour colour);

void CYG_FB_WRITE_VLINE(FRAMEBUF, cyg_ucount16 x, cyg_ucount16 y, cyg_ucount16 len, cyg_fb_colour colour);

void CYG_FB_FILL_BLOCK(FRAMEBUF, cyg_ucount16 x, cyg_ucount16 y, cyg_ucount16 width, cyg_ucount16 height, cyg_fb_colour colour);

void CYG_FB_WRITE_BLOCK(FRAMEBUF, cyg_ucount16 x, cyg_ucount16 y, cyg_ucount16 width, cyg_ucount16 height, const void* data, cyg_ucount16 offset, cyg_ucount16 stride);

void CYG_FB_READ_BLOCK(FRAMEBUF, cyg_ucount16 x, cyg_ucount16 y, cyg_ucount16 width, cyg_ucount16 height, void* data, cyg_ucount16 offset, cyg_ucount16 stride);

void CYG_FB_MOVE_BLOCK(FRAMEBUF, cyg_ucount16 x, cyg_ucount16 y, cyg_ucount16 width, cyg_ucount16 height, cyg_ucount16 new_x, cyg_ucount16 new_y);

void CYG_FB_SYNCH(FRAMEBUF, cyg_ucount16 when);

Description

The eCos framebuffer infrastructure defines a small number of drawing primitives. These are not intended to provide full graphical functionality like multiple windows, drawing text in arbitrary fonts, or anything like that. Instead they provide building blocks for higher-level graphical toolkits. The available primitives are:

  1. Manipulating individual pixels.

  2. Drawing horizontal and vertical lines.

  3. Block fills.

  4. Moving blocks between the framebuffer and main memory.

  5. Moving blocks within the framebuffer.

  6. For double-buffered devices, synchronizing the framebuffer contents with the actual display.

There are two versions for each primitive: a macro and a function. The macro can be used if the desired framebuffer device is known at compile-time. Its first argument should be a framebuffer identifier, for example 320x240x16, and must be one of the entries in the configuration option CYGDAT_IO_FRAMEBUF_DEVICES. In the examples below it is assumed that FRAMEBUF has been #define'd to a suitable identifier. The function can be used if the desired framebuffer device is selected at run-time. Its first argument should be a pointer to the appropriate cyg_fb structure.

The pixel, line, and block fill primitives take a cyg_fb_colour argument. For details of colour handling see Framebuffer Colours. This argument should have no more bits set than are appropriate for the display depth. For example on a 4bpp only the bottom four bits of the colour may be set, otherwise the behaviour is undefined.

None of the primitives will perform any run-time error checking, except possibly for some assertions in a debug build. If higher-level code provides invalid arguments, for example trying to write a block which extends past the right hand side of the screen, then the system's behaviour is undefined. It is the responsibility of higher-level code to perform clipping to the screen boundaries.

Manipulating Individual Pixels

The primitives for manipulating individual pixels are very simple: a pixel can be written or read back. The following example shows one way of drawing a diagonal line:

void
draw_diagonal(cyg_fb* fb,
              cyg_ucount16 x, cyg_ucount16 y, cyg_ucount16 len,
              cyg_fb_colour colour)
{
    while ( len-- ) {
        cyg_fb_write_pixel(fb, x++, y++, colour);
    }
}
    

The next example shows how to draw a horizontal XOR line on a 1bpp display.

void
draw_horz_xor(cyg_ucount16 x, cyg_ucount16 y, cyg_ucount16 len)
{
    cyg_fb_colour colour;
    while ( len--) {
        colour = CYG_FB_READ_PIXEL(FRAMEBUF, x, y);
        CYG_FB_WRITE_PIXEL(FRAMEBUF, x++, y, colour ^ 0x01);
    }
}
    

The pixel macros should normally be avoided. Determining the correct location within framebuffer memory corresponding to a set of coordinates for each pixel is a comparatively expensive operation. Instead there is direct support for iterating over parts of the display, avoiding unnecessary overheads.

Drawing Simple Lines

Higher-level graphics code often needs to draw single-pixel horizontal and vertical lines. If the application involves multiple windows then these will usually have thin borders around them. Widgets such as buttons and scrollbars also often have thin borders.

cyg_fb_draw_hline and CYG_FB_DRAW_HLINE draw a horizontal line of the specified colour, starting at the x and y coordinates and extending to the right (increasing x) for a total of len pixels. A 50 pixel line starting at (100,100) will end at (149,100).

cyg_fb_draw_vline and CYG_FB_DRAW_VLINE take the same arguments, but the line extends down (increasing y).

These primitives do not directly support drawing lines more than one pixel thick, but block fills can be used to achieve those. There is no generic support for drawing arbitrary lines, instead that is left to higher-level graphics toolkits.

Block Fills

Filling a rectangular part of the screen with a solid colour is another common requirement for higher-level code. The simplest example is during initialization, to set the display's whole background to a known value. Block fills are also often used when creating new windows or drawing the bulk of a simple button or scrollbar widget. cyg_fb_fill_block and CYG_FB_FILL_BLOCK provide this functionality.

The x and y arguments specify the top-left corner of the block to be filled. The width and height arguments specify the number of pixels affected, a total of width * height. The following example illustrates part of the process for initializing a framebuffer, assumed here to have a writeable palette with default settings.

int
display_init(void)
{
    int result = CYG_FB_ON(FRAMEBUF);
    if ( result ) {
        return result;
    }
    CYG_FB_FILL_BLOCK(FRAMEBUF, 0, 0,
                      CYG_FB_WIDTH(FRAMEBUF), CYG_FB_HEIGHT(FRAMEBUF),
                      CYG_FB_DEFAULT_PALETTE_WHITE);
    …
}
    

Copying Blocks between the Framebuffer and Main Memory

The block transfer primitives serve two main purposes: drawing images. and saving parts of the current display to be restored later. For simple linear framebuffers the primitives just implement copy operations, with no data conversion of any sort. For non-linear ones the primitives act as if the framebuffer memory was linear. For example, consider a 2bpp display where the two bits for a single pixel are split over two separate bytes in framebuffer memory, or two planes. For a block write operation the source data should still be organized with four full pixels per byte, as for a linear framebuffer of the same depth. and the block write primitive will distribute the bits over the framebuffer memory as required. Similarly a block read will combine the appropriate bits from different locations in framebuffer memory and the resulting memory block will have four full pixels per byte.

Because the block transfer primitives perform no data conversion, if they are to be used for rendering images then those images should be pre-formatted appropriately for the framebuffer device. For small images this would normally happen on the host-side as part of the application build process. For larger images it will usually be better to store them in a compressed format and decompress them at run-time, trading off memory for cpu cycles.

The x and y arguments specify the top-left corner of the block to be transferred, and the width and height arguments determine the size. The data, offset and stride arguments determine the location and layout of the block in main memory:

data

The source or destination for the transfer. For 1bpp, 2bpp and 4bpp devices the data will be packed in accordance with the framebuffer device's endianness as per the CYG_FB_FLAGS0_LE flag. Each row starts in a new byte so there may be some padding on the right. For 16bpp and 32bpp the data should be aligned to the appropriate boundary.

offset

Sometimes only part of an image should be written to the screen. A vertical offset can be achieved simply by adjusting data to point at the appropriate row within the image instead of the top row. For 8bpp, 16bpp and 32bpp displays an additional horizontal offset can also be achieved by adjusting data. However for 1bpp, 2bpp and 4bpp displays the starting position within the image may be in the middle of a byte. Hence the horizontal pixel offset can instead be specified with the offset argument.

stride

This indicates the number of bytes between rows. Usually it will be related to the width, but there are exceptions such as when drawing only part of an image.

The following example fills a 4bpp display with an image held in memory and already in the right format. If the image is smaller than the display it will be centered. If the image is larger then the center portion will fill the entire display.

void
draw_image(const void* data, int width, int height)
{
    cyg_ucount16 stride;
    cyg_ucount16 x, y, offset;

#if (4 != CYG_FB_DEPTH(FRAMEBUF))
# error This code assumes a 4bpp display
#endif

    stride = (width + 1) >> 1;  // 4bpp to byte stride

    if (width < CYG_FB_WIDTH(FRAMEBUF)) {
        x      = (CYG_FB_WIDTH(FRAMEBUF) - width) >> 1;
        offset = 0;
    } else {
        x      = 0;
        offset = (width - CYG_FB_WIDTH(FRAMEBUF)) >> 1;
        width  = CYG_FB_WIDTH(FRAMEBUF);
    }
    if (height < CYG_FB_HEIGHT(FRAMEBUF)) {
        y      = (CYG_FB_HEIGHT(FRAMEBUF) - height) >> 1;
    } else {
        y      = 0;
        data   = (const void*)((const cyg_uint8*)data +
                 (stride * ((height - CYG_FB_HEIGHT(FRAMEBUF)) >> 1));
        height = CYG_FB_HEIGHT(FRAMEBUF);
    }
    CYG_FB_WRITE_BLOCK(FRAMEBUF, x, y, width, height, data, offset, stride);
}
    

Moving Blocks with the Framebuffer

Sometimes it is necessary to move a block of data around the screen, especially when using a higher-level graphics toolkit that supports multiple windows. Block moves can be implemented by a read into main memory followed by a write block, but this is expensive and imposes an additional memory requirement. Instead the framebuffer infrastructure provides a generic block move primitive. It will handle all cases where the source and destination positions overlap. The x and y arguments specify the top-left corner of the block to be moved, and width and height determine the block size. new_x and new_y specify the destination. The source data will remain unchanged except in areas where it overlaps the destination.

Synchronizing Double-Buffered Displays

Some framebuffer devices are double-buffered: the framebuffer memory that gets manipulated by the drawing primitives is separate from what is actually displayed, and a synch operation is needed to update the display. In some cases this may be because the actual display memory is not directly accessible by the processor, for example it may instead be attached via an SPI bus. Instead drawing happens in a buffer in main memory, and then this gets transferred over the SPI bus to the actual display hardware during a synch. In other cases it may be a software artefact. Some drawing operations, especially ones involving complex curves, can take a very long time and it may be considered undesirable to have the user see this happening a few pixels at a time. Instead the drawing happens in a separate buffer in main memory and then a double buffer synch just involves a block move to framebuffer memory. Typically that block move is much faster than the drawing operation. Obviously there is a cost: an extra area of memory, and the synch operation itself can consume many cycles and much of the available memory bandwidth.

It is the responsibility of the framebuffer device driver to provide the extra main memory. As far as higher-level code is concerned the only difference between an ordinary and a double-buffered display is that with the latter changes do not become visible until a synch operation has been performed. The framebuffer infrastructure provides support for a bounding box, keeping track of what has been updated since the last synch. This means only the updated part of the screen has to be transferred to the display hardware.

The synch primitives take two arguments. The first identifies the framebuffer device. The second should be one of CYG_FB_UPDATE_NOW for an immediate update, or CYG_FB_UPDATE_VERTICAL_RETRACE. Some display hardware involves a lengthy vertical retrace period every 10-20 milliseconds during which nothing gets drawn to the screen, and performing the synch during this time means that the end user is unaware of the operation (assuming the synch can be completed in the time available). When the hardware supports it, specifying CYG_FB_UPDATE_VERTICAL_RETRACE means that the synch operation will block until the next vertical retrace takes place and then perform the update. This may be an expensive operation, for example it may involve polling a bit in a register. In a multi-threaded environment it may also be unreliable because the thread performing the synch may get interrupted or rescheduled in the middle of the operation. When the hardware does not involve vertical retraces, or when there is no easy way to detect them, the second argument to the synch operation will just be ignored and the update will always happen immediately.

It is up to higher level code to determine when a synch operation is appropriate. One approach for typical event-driven code is to perform the synch at the start of the event loop, just before waiting for an input or timer event. This may not be optimal. For example if there two small updates to opposite corners of the screen then it would be better to make two synch calls with small bounding boxes, rather than a single synch call with a a large bounding box that requires most of the framebuffer memory to be updated.

Leaving out the synch operations leads to portability problems. On hardware which does not involve double-buffering the synch operation is a no-op, usually eliminated at compile-time, so invoking synch does not add any code size or cpu cycle overhead. On double-buffered hardware, leaving out the synch means the user cannot see what has been drawn into the framebuffer.

2017-02-09
Documentation license for this page: Open Publication License