UPDATE: I’ve consolidated the original code into a set of low-level utilities, which can be found on my github here. These can be used to generate basic dashboards using a raspberry pi and something like AWS’s GetMetricWidgetImage API for cloudwatch.
I was working on a Raspberry Pi project recently and encountered a need for displaying some graphics on the raspberry pi screen without having a window system (I run raspbian lite on all my Pi’s). Luckily, I had some old code which did something similar to what I needed, and could draw basic shapes. It was designed on top of the linux framebuffer, but it had numerous performance issues and wasn’t really feasible for use as a result. You could watch it clear the screen line-by-line.
I decided I should try and optimize it, and try to expand its featureset to include display of PNGs and JPEGs as well, because who doesn’t need that.
Optimizing
The first step in optimizing the code was eliminating as much of the per-pixel logic as possible. For example, I had draw routines that looked a little bit like this (some bounds checking and additional logic removed):
void draw_rect(int x, int y, int w, int h, int color, int* context) {
for (int cx = x; cx < x + w; cx++) {
for (int cy = y; cy < y + h; cy++) {
set_pixel(cx, cy, color, context);
}
}
}
I initially wrote this years ago when I was learning C, and didn’t know
much about performance. Operating on individual elements in a buffer like
that is extremely slow. The solution in this case, was simple: draw as
little as possible using per-pixel logic (or memset()
if its a grayscale
color), and then memcpy()
it to fill the rest of the area. These
operations are much faster, since they are more optimized and can use SIMD
instructions if they are available. As a result, drawing now gives the
illusion of an instant color change if you fill the entire screen.
void draw_rect(int x, int y, int w, int h, int color, int* context) {
// Write the first line
for (int cx = x; cx < x + w; cx++) {
set_pixel(cx, y, color, context);
}
// Repeat it over the rest of the shape.
for (int cy = y + 1; cy < y + h; cy++) {
// Copy one line at a time.
memcpy(
&context->data[context->width * cy + x,
&context->data[context->width * y + x,
w * sizeof(int)
);
}
}
Images!
With this in mind, I took a similar approach to images. First, we load the
original image at its full resolution and store it in memory. Then, we do
a resize operation to scale and crop it to the necessary size for display
(also in memory). Then a screen display operation is just a memcpy()
operation for each line of the image. This allowed me to implement a
libpng and libjpeg reader that fills the following image data structure:
typedef struct {
int* data;
int width;
int height;
} image_t;
Text?
Fonts take a similar approach. Currently, I’m using fonts which are
defined in 8x8 C-arrays, and are imported into a font data structure
similar to image_t
. The only difference here is the font images have
offsets, so the image backing a comma or underscore character can be as
small as possible, and drawn at an offset. This provides an advantage
over storing a fixed-width image set which contains an image for each
character and is the same size.
Next, the fonts are mapped into an array based on their ASCII code, so they can be programmatically accessed and displayed onscreen.
The current fonts implementation is a bit limited, and doesn’t support multiple font sizes, and is limited to drawing on a solid color background. due to a lack of the ability to calculate transparency.
Next Steps
Currently, I’m working on support for transparency. This will require memory usage to increase since we will need to add the ability to layer content on top of each other. Doing so will allow us to implement a better font system, possibly using freetype to render actual fonts.
Github
I’ve put a copy of the routines and a demo makefile up on GitHub, which can be cloned and run. This should work on many Linux distributions with relative ease, I’ve run the current version on Raspbian lite and the original version was written on top of Ubuntu server. This can’t be run with the X-window system (or any) window system that uses the screen for graphics since they won’t play nice.