Since 9.x it becomes possible to patch LVGL to natively support new pixel formats. It can be a simple color swap mode not originally planned or a super specific display pixel format.
In this posts serie, we will cover the necessary steps for you to add such a custom pixel format and end up with a patch to apply to LVGL sources.
In the following examples, we'll use a custom pixel format on ABRG 32bpp (8A, 8B, 8R, 8G). This is a made up format and is only used as an example purpose. We'll name this pixel format ABGR8888.
Adding the color format to lv_color.h
The first step is to let LVGL know about our new pixel format. To do this, we'll add our pixel format in src/misc/lv_color.h in the lv_color_format_t enum. Here we need to add the color fomat and specify to use it natively if LVGL is configured in 32bpp:
...
LV_COLOR_FORMAT_ABGR8888 = 0x36,
...
#elif LV_COLOR_DEPTH == 32
LV_COLOR_FORMAT_NATIVE = LV_COLOR_FORMAT_ABGR8888,
#else
Then, complete both lv_color_format_get_bpp and lv_color_format_has_alpha:
uint8_t lv_color_format_get_bpp(lv_color_format_t format) {
...
case LV_COLOR_FORMAT_ARGB8888:
case LV_COLOR_FORMAT_XRGB8888:
case LV_COLOR_FORMAT_ABGR8888: /* Added this line on the return 32 case */
return 32;
...
}
bool lv_color_format_has_alpha(lv_color_format_t format) {
...
case LV_COLOR_FORMAT_ARGB4444:
case LV_COLOR_FORMAT_ABGR8888: /* Added here as our pixel format has an alpha chanel */
return true;
...
}
We now are ready to implement the blending functions!
Adding the blending functions
LVGL needs a way to know how to blend a pixel format into a draw buffer, which can be the same or another color format. In our case, we are only interrested to blend our custom pixel format with itself, but the procedure is the same to blend any other format.
The blending in LVGL can be of two different type:
Color blending: Simple blending of a single color in a given area. Consist of filling an area with the color, with or without masking.
Image blending: Blending of an image descriptor comming with its source buffer to blend. Can be with or without mask.
In the examples, we only cover the color/image blend with no mask or with a mask, but not the AA, to keep it simple.
So first, create your custom blending files (which you can copy paste from existant) in src/draw/sw/blend/:
lv_draw_sw_blend_to_abgr8888.c: Functions definitions
lv_draw_sw_blend_to_abgr8888.h: Functions declarations
Color blending function
The color blending function has this prototype (can also be customized):
blend_color_to_format(lv_draw_sw_blend_fill_dsc_t * dsc)
Where the dsc parameter contains all the information about the color, the destination buffer, the coordinates, ... You should en up with something like this:
static inline void *drawbuf_next_row(const void *buf, uint32_t stride)
{
return (void *)((uint8_t *)buf + stride);
}
void lv_draw_sw_blend_color_to_abgr8888(lv_draw_sw_blend_fill_dsc_t * dsc)
{
int32_t w = dsc->dest_w;
int32_t h = dsc->dest_h;
lv_color_t color_abgr8888;
lv_opa_t opa = dsc->opa;
const lv_opa_t * mask = dsc->mask_buf;
int32_t mask_stride = dsc->mask_stride;
uint32_t *dest_buf_abgr8888 = dsc->dest_buf;
int32_t dest_stride = dsc->dest_stride;
int32_t x;
int32_t y;
/* Invert here, as we did not patched lv_color_make() */
color_abgr8888.blue = = dsc->color.red;
color_abgr8888.green = = dsc->color.green;
color_abgr8888.red = = dsc->color.blue;
LV_UNUSED(w);
LV_UNUSED(h);
LV_UNUSED(x);
LV_UNUSED(y);
LV_UNUSED(opa);
LV_UNUSED(mask);
LV_UNUSED(mask_stride);
LV_UNUSED(dest_stride);
/* Simple fill, no mask, full opacity */
if(mask == NULL && opa >= LV_OPA_MAX) {
for(y = 0; y < h; y++) {
for (x = 0; x < w; x++) {
dest_buf_u8[x] = color_abgr8888;
}
/* Get next row */
dest_buf_abgr8888 = drawbuf_next_row(dest_buf_abgr8888, dest_stride);
}
/* Fill with mask */
} else if(mask && opa >= LV_OPA_MAX) {
for(y = 0; y < h; y++) {
for(x = 0; x < w; x++) {
/* Mask is on 8bits */
uint8_t mask8 = *((uint8_t *)&mask[x]);
/* Apply color to all the mask pixel which are not 0 */
if(mask8 != 0x00) {
dest_buf_u8[x] = LV_OPA_MIX2(color_abgr8888.alpha, mask);
}
}
/* Get next row */
dest_buf_abgr8888 = drawbuf_next_row(dest_buf_abgr8888, dest_stride);
mask += mask_stride;
}
}
}
We can see the two different branches, once without masking, once with a mask. The mask is a uint8_t array containing a mask per bit (0-255), defining how to blend each pixel. The masking branch is taken for example when drawing letters, which are just a rectangle area with a mask to only draw on the letters outlines, with AA.
Now add the function prototype in the previously created header.
Image blending function
Then is the image blending function, which acts exactly like the color blending, but with a source buffer instead of a single color:
void lv_draw_sw_blend_image_to_abgr8888(lv_draw_sw_blend_image_dsc_t * dsc)
{
switch(dsc->src_color_format) {
case LV_COLOR_FORMAT_ABGR8888:
abgr8888_image_blend(dsc);
break;
default:
LV_LOG_WARN("Not supported source color format");
break;
}
}
static uint8_t blend_channel(uint8_t c1, uint8_t c2, uint8_t mask) {
return (uint8_t)(((c1 * (255 - mask)) + (c2 * mask)) / 255);
}
static uint32_t blend_abgr(uint32_t color1, uint32_t color2, uint8_t mask) {
uint8_t a1 = (color1 >> 24) & 0xFF;
uint8_t b1 = (color1 >> 16) & 0xFF;
uint8_t g1 = (color1 >> 8) & 0xFF;
uint8_t r1 = color1 & 0xFF;
uint8_t a2 = (color2 >> 24) & 0xFF;
uint8_t b2 = (color2 >> 16) & 0xFF;
uint8_t g2 = (color2 >> 8) & 0xFF;
uint8_t r2 = color2 & 0xFF;
uint8_t a = blend_channel(a1, a2, mask);
uint8_t b = blend_channel(b1, b2, mask);
uint8_t g = blend_channel(g1, g2, mask);
uint8_t r = blend_channel(r1, r2, mask);
return (a << 24) | (b << 16) | (g << 8) | r;
}
static void abgr8888_image_blend(lv_draw_sw_blend_image_dsc_t * dsc)
{
int32_t w = dsc->dest_w;
int32_t h = dsc->dest_h;
lv_opa_t opa = dsc->opa;
uint32_t * dest_buf = dsc->dest_buf;
int32_t dest_stride = dsc->dest_stride;
const uint32_t * src_buf = dsc->src_buf;
int32_t src_stride = dsc->src_stride;
const lv_opa_t * mask_buf = dsc->mask_buf;
int32_t mask_stride = dsc->mask_stride;
int32_t dest_x;
int32_t src_x;
int32_t y;
if(dsc->blend_mode == LV_BLEND_MODE_NORMAL) {
if(mask_buf == NULL && opa >= LV_OPA_MAX) {
for(y = 0; y < h; y++) {
for(dest_x = 0, src_x = 0; dest_x < w; dest_x++, src_x++) {
dest_buf[dest_x] = src_buf[src_x];
}
dest_buf = drawbuf_next_row(dest_buf, dest_stride);
src_buf = drawbuf_next_row(src_buf, src_stride);
}
} else (mask_buf != NULL && opa >= LV_OPA_MAX) {
for(y = 0; y < h; y++) {
for(x = 0; x < w; x++) {
uint8_t mask = mask_buf[x]
if (mask != 0x00) {
dest_buf[x] = blend_abgr(dest_buf[x], src_buf[x], mask);
}
}
dest_buf = drawbuf_next_row(dest_buf, dest_stride);
src_buf = drawbuf_next_row(src_buf, src_stride);
mask += mask_stride;
}
}
}
}
We see that like before we treat both with or without masking. In the masking case, the destintaion buffer is also used in the mix. Now add the function prototype in the previously created header.
Adding our functions to LVGL SW draw
We also see that we need another function called lv_draw_sw_blend_image_to_abgr8888, as it will now need to be added to the common lv_sw_blend function found in lv_draw_sw_blend.c file. Here we need to add both calls to our new functions. So first include the header:
#include "lv_draw_sw_blend_to_abgr8888.h
In lv_sw_blend, we can now add our functions calls. This function have two main branches:
/*Color fill, as now image source is specified*/
if(blend_dsc->src_buf == NULL) {
...
switch(layer->color_format) {
...
case LV_COLOR_FORMAT_ABGR8888:
lv_draw_sw_blend_color_to_abgr8888(&fill_dsc);
break;
}
} else {
switch(layer->color_format) {
...
case LV_COLOR_FORMAT_ABGR8888:
lv_draw_sw_blend_image_to_abgr8888(&image_dsc);
break;
}
}
We now are ready to let LVGL use our custom pixel format for basic color and image blending, which allow use to draw simple LVGL objects, letters and already decoded images (more on that in a future post!).
Conclusion
This first part covers the very basics of adding a custom pixel format to LVGL 9.x. It is kept pretty simple for example purpose, but I strongly encourage you to check the already existing work in LVGL to base your addition on it.
In the next part we'll see how to add transformation to our pixel format, to be able to rotate and scale our objects!