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:
- Manipulating individual pixels.
- Drawing horizontal and vertical lines.
- Block fills.
- Moving blocks between the framebuffer and main memory.
- Moving blocks within the framebuffer.
- 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 adjustingdata
. 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 theoffset
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.
2024-03-18 | Open Publication License |