From ff125ac74d108a409c01133b7e62ef644dc8fe39 Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Wed, 17 Jun 2026 13:57:15 +0200 Subject: [PATCH 1/8] dcs_lcd_color: Add display_color helpers and fix alpha mask Add display_color_to_rgb565() and display_color_to_surface() helpers for converting internal display color values (0xRRGGBBAA format) to RGB565 and surface-ready format. Fix missing & 0xFF mask on the red channel extraction in rgba8888_color_to_rgb565(). Use the new helpers consistently for brcolor and fgcolor fields. Signed-off-by: Ibrahim YILMAZ --- dcs_lcd_color.h | 18 +++++++++++++++++- dcs_lcd_draw.c | 10 +++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/dcs_lcd_color.h b/dcs_lcd_color.h index cb23e1f..36dfa56 100644 --- a/dcs_lcd_color.h +++ b/dcs_lcd_color.h @@ -43,7 +43,16 @@ static inline uint8_t rgba8888_get_alpha(uint32_t color) static inline uint16_t rgba8888_color_to_rgb565(uint32_t color) { - uint8_t r = color >> 24; + uint8_t r = (color >> 24) & 0xFF; + uint8_t g = (color >> 16) & 0xFF; + uint8_t b = (color >> 8) & 0xFF; + + return (((uint16_t) (r >> 3)) << 11) | (((uint16_t) (g >> 2)) << 5) | ((uint16_t) b >> 3); +} + +static inline uint16_t display_color_to_rgb565(uint32_t color) +{ + uint8_t r = (color >> 24) & 0xFF; uint8_t g = (color >> 16) & 0xFF; uint8_t b = (color >> 8) & 0xFF; @@ -55,6 +64,13 @@ static inline uint16_t rgb565_color_to_surface(uint16_t color16) return (uint16_t) SPI_SWAP_DATA_TX(color16, 16); } +static inline uint16_t display_color_to_surface(uint32_t color) +{ + uint16_t color16 = display_color_to_rgb565(color); + + return rgb565_color_to_surface(color16); +} + static inline uint16_t uint32_color_to_surface(uint32_t color) { uint16_t color16 = rgba8888_color_to_rgb565(color); diff --git a/dcs_lcd_draw.c b/dcs_lcd_draw.c index 80438c1..dc4f7e4 100644 --- a/dcs_lcd_draw.c +++ b/dcs_lcd_draw.c @@ -55,7 +55,7 @@ int dcs_lcd_draw_image_x(const struct DCSLCDScreen *screen, uint16_t bgcolor = 0; bool visible_bg; if (item->brcolor != 0) { - bgcolor = rgba8888_color_to_rgb565(item->brcolor); + bgcolor = display_color_to_rgb565(item->brcolor); visible_bg = true; } else { visible_bg = false; @@ -98,7 +98,7 @@ int dcs_lcd_draw_rect_x(const struct DCSLCDScreen *screen, { int x = item->x; int width = item->width; - uint16_t color = uint32_color_to_surface(item->brcolor); + uint16_t color = display_color_to_surface(item->brcolor); int drawn_pixels = 0; @@ -121,11 +121,11 @@ int dcs_lcd_draw_text_x(const struct DCSLCDScreen *screen, { int x = item->x; int y = item->y; - uint16_t fgcolor = uint32_color_to_surface(item->data.text_data.fgcolor); + uint16_t fgcolor = display_color_to_surface(item->data.text_data.fgcolor); uint16_t bgcolor; bool visible_bg; if (item->brcolor != 0) { - bgcolor = uint32_color_to_surface(item->brcolor); + bgcolor = display_color_to_surface(item->brcolor); visible_bg = true; } else { visible_bg = false; @@ -180,7 +180,7 @@ int dcs_lcd_draw_scaled_cropped_img_x(const struct DCSLCDScreen *screen, uint16_t bgcolor = 0; bool visible_bg; if (item->brcolor != 0) { - bgcolor = rgba8888_color_to_rgb565(item->brcolor); + bgcolor = display_color_to_rgb565(item->brcolor); visible_bg = true; } else { visible_bg = false; From 23cccf3ee9d257eacb1b6191c63341319cdc461a Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Wed, 17 Jun 2026 13:58:27 +0200 Subject: [PATCH 2/8] dcs_lcd_draw: Add display-list compositing for transparent backgrounds When a primitive has transparent background (brcolor == 0), partial-alpha pixels were previously blended against the current framebuffer content, which is unreliable with double-buffered region rendering: the work framebuffer contains uninitialized PSRAM data rather than the intended lower display-list layer. Add dcs_lcd_resolve_pixel_rgb565() and per-primitive pixel resolution functions that walk the display list to find the solid colour from the next lower opaque item. Partial-alpha transparent pixels now blend over that resolved lower-layer pixel instead of framebuffer memory. This enables correct anti-aliased uFont text rendering over transparent backgrounds on RGB LCD displays. Signed-off-by: Ibrahim YILMAZ --- dcs_lcd_draw.c | 160 ++++++++++++++++++++++++++++++++++++++++++++++--- dcs_lcd_draw.h | 9 ++- 2 files changed, 159 insertions(+), 10 deletions(-) diff --git a/dcs_lcd_draw.c b/dcs_lcd_draw.c index dc4f7e4..9c391ae 100644 --- a/dcs_lcd_draw.c +++ b/dcs_lcd_draw.c @@ -46,8 +46,136 @@ int dcs_lcd_find_max_line_len(const struct DCSLCDScreen *screen, return line_len; } +static bool dcs_lcd_resolve_pixel_rgb565(const struct DCSLCDScreen *screen, + int xpos, int ypos, BaseDisplayItem items[], size_t items_len, size_t start_index, uint16_t *out_color); + +static bool dcs_lcd_image_pixel_rgb565(const struct DCSLCDScreen *screen, + BaseDisplayItem *item, int xpos, int ypos, BaseDisplayItem items[], size_t items_len, size_t item_index, uint16_t *out_color) +{ + int x = item->x; + int y = item->y; + int rel_x = xpos - x; + int rel_y = ypos - y; + uint32_t *pixels = ((uint32_t *) item->data.image_data.pix) + (rel_y * item->width) + rel_x; + uint32_t img_pixel = READ_32_UNALIGNED(pixels); + uint8_t alpha = rgba8888_get_alpha(img_pixel); + + if (alpha == 0xFF) { + *out_color = rgba8888_color_to_rgb565(img_pixel); + return true; + } + if (item->brcolor != 0) { + uint16_t color = rgba8888_color_to_rgb565(img_pixel); + uint16_t bgcolor = display_color_to_rgb565(item->brcolor); + *out_color = alpha_blend_rgb565(color, bgcolor, alpha); + return true; + } + if (alpha > 0) { + uint16_t lower = 0; + uint16_t color = rgba8888_color_to_rgb565(img_pixel); + (void) dcs_lcd_resolve_pixel_rgb565(screen, xpos, ypos, items, items_len, item_index + 1, &lower); + *out_color = alpha_blend_rgb565(color, lower, alpha); + return true; + } + + return dcs_lcd_resolve_pixel_rgb565(screen, xpos, ypos, items, items_len, item_index + 1, out_color); +} + +static bool dcs_lcd_scaled_image_pixel_rgb565(const struct DCSLCDScreen *screen, + BaseDisplayItem *item, int xpos, int ypos, BaseDisplayItem items[], size_t items_len, size_t item_index, uint16_t *out_color) +{ + int x = item->x; + int y = item->y; + int source_x = item->source_x + ((xpos - x) / item->x_scale); + int source_y = item->source_y + ((ypos - y) / item->y_scale); + int img_width = item->data.image_data_with_size.width; + uint32_t *pixels = ((uint32_t *) item->data.image_data_with_size.pix) + (source_y * img_width) + source_x; + uint32_t img_pixel = READ_32_UNALIGNED(pixels); + uint8_t alpha = rgba8888_get_alpha(img_pixel); + + if (alpha == 0xFF) { + *out_color = rgba8888_color_to_rgb565(img_pixel); + return true; + } + if (item->brcolor != 0) { + uint16_t color = rgba8888_color_to_rgb565(img_pixel); + uint16_t bgcolor = display_color_to_rgb565(item->brcolor); + *out_color = alpha_blend_rgb565(color, bgcolor, alpha); + return true; + } + if (alpha > 0) { + uint16_t lower = 0; + uint16_t color = rgba8888_color_to_rgb565(img_pixel); + (void) dcs_lcd_resolve_pixel_rgb565(screen, xpos, ypos, items, items_len, item_index + 1, &lower); + *out_color = alpha_blend_rgb565(color, lower, alpha); + return true; + } + + return dcs_lcd_resolve_pixel_rgb565(screen, xpos, ypos, items, items_len, item_index + 1, out_color); +} + +static bool dcs_lcd_text_pixel_rgb565(const struct DCSLCDScreen *screen, + BaseDisplayItem *item, int xpos, int ypos, BaseDisplayItem items[], size_t items_len, size_t item_index, uint16_t *out_color) +{ + int x = item->x; + int y = item->y; + char *text = (char *) item->data.text_data.text; + int char_index = (xpos - x) / CHAR_WIDTH; + char c = text[char_index]; + unsigned const char *glyph = fontdata + ((unsigned char) c) * 16; + unsigned char row = glyph[ypos - y]; + int k = (xpos - x) % CHAR_WIDTH; + + if (row & (1 << (7 - k))) { + *out_color = display_color_to_rgb565(item->data.text_data.fgcolor); + return true; + } + if (item->brcolor != 0) { + *out_color = display_color_to_rgb565(item->brcolor); + return true; + } + + return dcs_lcd_resolve_pixel_rgb565(screen, xpos, ypos, items, items_len, item_index + 1, out_color); +} + +static bool dcs_lcd_resolve_pixel_rgb565(const struct DCSLCDScreen *screen, + int xpos, int ypos, BaseDisplayItem items[], size_t items_len, size_t start_index, uint16_t *out_color) +{ + for (size_t i = start_index; i < items_len; i++) { + BaseDisplayItem *item = &items[i]; + if ((xpos < item->x) || (xpos >= item->x + item->width) || (ypos < item->y) || (ypos >= item->y + item->height)) { + continue; + } + + switch (item->primitive) { + case PrimitiveImage: + if (dcs_lcd_image_pixel_rgb565(screen, item, xpos, ypos, items, items_len, i, out_color)) { + return true; + } + break; + case PrimitiveRect: + *out_color = display_color_to_rgb565(item->brcolor); + return true; + case PrimitiveScaledCroppedImage: + if (dcs_lcd_scaled_image_pixel_rgb565(screen, item, xpos, ypos, items, items_len, i, out_color)) { + return true; + } + break; + case PrimitiveText: + if (dcs_lcd_text_pixel_rgb565(screen, item, xpos, ypos, items, items_len, i, out_color)) { + return true; + } + break; + default: + break; + } + } + return false; +} + int dcs_lcd_draw_image_x(const struct DCSLCDScreen *screen, - int xpos, int ypos, int max_line_len, BaseDisplayItem *item) + int xpos, int ypos, int max_line_len, BaseDisplayItem *item, + BaseDisplayItem items[], size_t items_len, size_t item_index) { int x = item->x; int y = item->y; @@ -83,6 +211,12 @@ int dcs_lcd_draw_image_x(const struct DCSLCDScreen *screen, uint16_t color = rgba8888_color_to_rgb565(img_pixel); uint16_t blended = alpha_blend_rgb565(color, bgcolor, alpha); pixmem16[drawn_pixels] = rgb565_color_to_surface(blended); + } else if (alpha > 0) { + uint16_t color = rgba8888_color_to_rgb565(img_pixel); + uint16_t lower = 0; + (void) dcs_lcd_resolve_pixel_rgb565(screen, xpos + drawn_pixels, ypos, items, items_len, item_index + 1, &lower); + uint16_t blended = alpha_blend_rgb565(color, lower, alpha); + pixmem16[drawn_pixels] = rgb565_color_to_surface(blended); } else { return drawn_pixels; } @@ -117,7 +251,8 @@ int dcs_lcd_draw_rect_x(const struct DCSLCDScreen *screen, } int dcs_lcd_draw_text_x(const struct DCSLCDScreen *screen, - int xpos, int ypos, int max_line_len, BaseDisplayItem *item) + int xpos, int ypos, int max_line_len, BaseDisplayItem *item, + BaseDisplayItem items[], size_t items_len, size_t item_index) { int x = item->x; int y = item->y; @@ -163,7 +298,11 @@ int dcs_lcd_draw_text_x(const struct DCSLCDScreen *screen, } else if (visible_bg) { pixmem16[drawn_pixels] = bgcolor; } else { - return drawn_pixels; + uint16_t lower = 0; + if (!dcs_lcd_resolve_pixel_rgb565(screen, xpos + drawn_pixels, ypos, items, items_len, item_index + 1, &lower)) { + return drawn_pixels; + } + pixmem16[drawn_pixels] = rgb565_color_to_surface(lower); } drawn_pixels++; } @@ -172,7 +311,8 @@ int dcs_lcd_draw_text_x(const struct DCSLCDScreen *screen, } int dcs_lcd_draw_scaled_cropped_img_x(const struct DCSLCDScreen *screen, - int xpos, int ypos, int max_line_len, BaseDisplayItem *item) + int xpos, int ypos, int max_line_len, BaseDisplayItem *item, + BaseDisplayItem items[], size_t items_len, size_t item_index) { int x = item->x; int y = item->y; @@ -219,6 +359,12 @@ int dcs_lcd_draw_scaled_cropped_img_x(const struct DCSLCDScreen *screen, uint16_t color = rgba8888_color_to_rgb565(img_pixel); uint16_t blended = alpha_blend_rgb565(color, bgcolor, alpha); pixmem16[drawn_pixels] = rgb565_color_to_surface(blended); + } else if (alpha > 0) { + uint16_t color = rgba8888_color_to_rgb565(img_pixel); + uint16_t lower = 0; + (void) dcs_lcd_resolve_pixel_rgb565(screen, xpos + drawn_pixels, ypos, items, items_len, item_index + 1, &lower); + uint16_t blended = alpha_blend_rgb565(color, lower, alpha); + pixmem16[drawn_pixels] = rgb565_color_to_surface(blended); } else { return drawn_pixels; } @@ -245,7 +391,7 @@ int dcs_lcd_draw_x(const struct DCSLCDScreen *screen, int drawn_pixels = 0; switch (items[i].primitive) { case PrimitiveImage: - drawn_pixels = dcs_lcd_draw_image_x(screen, xpos, ypos, max_line_len, item); + drawn_pixels = dcs_lcd_draw_image_x(screen, xpos, ypos, max_line_len, item, items, items_len, i); break; case PrimitiveRect: @@ -253,11 +399,11 @@ int dcs_lcd_draw_x(const struct DCSLCDScreen *screen, break; case PrimitiveScaledCroppedImage: - drawn_pixels = dcs_lcd_draw_scaled_cropped_img_x(screen, xpos, ypos, max_line_len, item); + drawn_pixels = dcs_lcd_draw_scaled_cropped_img_x(screen, xpos, ypos, max_line_len, item, items, items_len, i); break; case PrimitiveText: - drawn_pixels = dcs_lcd_draw_text_x(screen, xpos, ypos, max_line_len, item); + drawn_pixels = dcs_lcd_draw_text_x(screen, xpos, ypos, max_line_len, item, items, items_len, i); break; default: { fprintf(stderr, "unexpected display list command.\n"); diff --git a/dcs_lcd_draw.h b/dcs_lcd_draw.h index a99b407..3894564 100644 --- a/dcs_lcd_draw.h +++ b/dcs_lcd_draw.h @@ -25,16 +25,19 @@ #include "display_items.h" int dcs_lcd_draw_image_x(const struct DCSLCDScreen *screen, - int xpos, int ypos, int max_line_len, BaseDisplayItem *item); + int xpos, int ypos, int max_line_len, BaseDisplayItem *item, + BaseDisplayItem items[], size_t items_len, size_t item_index); int dcs_lcd_draw_rect_x(const struct DCSLCDScreen *screen, int xpos, int ypos, int max_line_len, BaseDisplayItem *item); int dcs_lcd_draw_text_x(const struct DCSLCDScreen *screen, - int xpos, int ypos, int max_line_len, BaseDisplayItem *item); + int xpos, int ypos, int max_line_len, BaseDisplayItem *item, + BaseDisplayItem items[], size_t items_len, size_t item_index); int dcs_lcd_draw_scaled_cropped_img_x(const struct DCSLCDScreen *screen, - int xpos, int ypos, int max_line_len, BaseDisplayItem *item); + int xpos, int ypos, int max_line_len, BaseDisplayItem *item, + BaseDisplayItem items[], size_t items_len, size_t item_index); int dcs_lcd_find_max_line_len(const struct DCSLCDScreen *screen, BaseDisplayItem items[], size_t items_len, int xpos, int ypos); From a055634b74898e6b3044c71cacb635d8d784e299 Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Wed, 17 Jun 2026 13:58:53 +0200 Subject: [PATCH 3/8] display_task: Pre-ack update_region and draw_rgb565_raw, fix EPD pixel byte order Add update_region (partial screen update) and draw_rgb565_raw (direct RGB565 binary draw) to the pre-ack list so callers receive an immediate acknowledgement without risk of mailbox queue timeout. Fix epd_draw_pixel to write RGBA bytes individually instead of relying on uint32_t endianness. The previous code wrote alpha in the MSB of a uint32_t, which on little-endian ESP32 placed the R and B channels in reversed positions relative to the declared RGBA byte order. Signed-off-by: Ibrahim YILMAZ --- display_items.c | 9 ++++++--- display_task.c | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/display_items.c b/display_items.c index 50fdec3..1853eea 100644 --- a/display_items.c +++ b/display_items.c @@ -51,8 +51,8 @@ void epd_draw_pixel(int xpos, int ypos, uint8_t color, void *buffer) return; } - uint32_t *pixel = (uint32_t *) (((uint8_t *) surface->buffer) - + (surface->width * ypos + xpos) * sizeof(uint32_t)); + uint8_t *pixel = ((uint8_t *) surface->buffer) + + (surface->width * ypos + xpos) * sizeof(uint32_t); // The `color` parameter is the LUT-mapped glyph value from // draw_char: 0 = full foreground (fg_color=0 in default props), @@ -60,7 +60,10 @@ void epd_draw_pixel(int xpos, int ypos, uint8_t color, void *buffer) // the foreground RGB on transparent with anti-aliased alpha // derived from the inverted grayscale. uint8_t alpha = (15 - (color >> 4)) * 17; - *pixel = ((uint32_t) alpha << 24) | (surface->fg_color & 0x00FFFFFFu); + pixel[0] = (surface->fg_color >> 24) & 0xFFu; + pixel[1] = (surface->fg_color >> 16) & 0xFFu; + pixel[2] = (surface->fg_color >> 8) & 0xFFu; + pixel[3] = alpha; } #endif /* ENABLE_UFONT */ diff --git a/display_task.c b/display_task.c index 457ba4b..05f4755 100644 --- a/display_task.c +++ b/display_task.c @@ -47,6 +47,8 @@ static bool try_pre_ack_render_cmd(Message *message, Context *ctx) term cmd = term_get_tuple_element(req, 0); if (cmd != globalcontext_make_atom(ctx->global, "\x6" "update") + && cmd != globalcontext_make_atom(ctx->global, "\xD" "update_region") + && cmd != globalcontext_make_atom(ctx->global, "\xF" "draw_rgb565_raw") && cmd != globalcontext_make_atom(ctx->global, "\xB" "draw_buffer")) { return false; From 7fef6bf61b43a8f9306455253e3cd413c52fd5ef Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Wed, 17 Jun 2026 13:59:39 +0200 Subject: [PATCH 4/8] Add RGB LCD display driver for ESP-IDF 5+ Add a new display driver for RGB LCD panels using the esp_lcd RGB interface (ESP-IDF 5+). The driver supports: - Double framebuffering in PSRAM for tear-free rendering - Full-screen and region-based partial updates (update_region) - Direct raw RGB565 pixel drawing (draw_rgb565_raw) - base64-encoded RLE RGB565 images with nearest-neighbour scaling - Fullscreen background image storage in PSRAM with per-line restore before each region update - Frame buffer mirroring across inactive framebuffers The RGB LCD driver is only compiled when ESP-IDF >= 5. Compatible strings: "waveshare,esp32-s3-touch-lcd-7" and "esp_lcd,rgb". Signed-off-by: Ibrahim YILMAZ --- CMakeLists.txt | 7 +- display_driver.c | 9 + rgb_lcd_display_driver.c | 1006 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 1021 insertions(+), 1 deletion(-) create mode 100644 rgb_lcd_display_driver.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 39707ae..311244d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,8 +22,12 @@ # A link option will be used with esp-idf 4.x if (IDF_VERSION_MAJOR GREATER_EQUAL 5) set(OPTIONAL_WHOLE_ARCHIVE WHOLE_ARCHIVE) + set(OPTIONAL_ESP_LCD_REQUIRES "esp_lcd") + set(OPTIONAL_RGB_LCD_SRCS "rgb_lcd_display_driver.c") else() set(OPTIONAL_WHOLE_ARCHIVE "") + set(OPTIONAL_ESP_LCD_REQUIRES "") + set(OPTIONAL_RGB_LCD_SRCS "") endif() idf_component_register(SRCS @@ -44,6 +48,7 @@ idf_component_register(SRCS "memory_display_driver.c" "oled_commands.c" "oled_display_driver.c" + ${OPTIONAL_RGB_LCD_SRCS} "spi_dc_driver.c" "spi_display.c" "ufontlib.c" @@ -51,7 +56,7 @@ idf_component_register(SRCS "image_helpers.c" "spng.c" "miniz.c" - PRIV_REQUIRES "libatomvm" "avm_sys" "avm_builtins" "driver" "sdmmc" "vfs" "fatfs" + PRIV_REQUIRES "libatomvm" "avm_sys" "avm_builtins" "driver" ${OPTIONAL_ESP_LCD_REQUIRES} "sdmmc" "vfs" "fatfs" ${OPTIONAL_WHOLE_ARCHIVE} ) diff --git a/display_driver.c b/display_driver.c index dce82db..af2a9f7 100644 --- a/display_driver.c +++ b/display_driver.c @@ -20,6 +20,7 @@ #include +#include #include #include @@ -33,6 +34,9 @@ Context *epaper_display_create_port(GlobalContext *global, term opts); Context *dcs_lcd_display_create_port(GlobalContext *global, term opts); Context *memory_lcd_display_create_port(GlobalContext *global, term opts); Context *oled_display_create_port(GlobalContext *global, term opts); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) +Context *rgb_lcd_display_create_port(GlobalContext *global, term opts); +#endif Context *display_create_port(GlobalContext *global, term opts) { @@ -54,6 +58,11 @@ Context *display_create_port(GlobalContext *global, term opts) if (!strcmp(compat_string, "waveshare,5in65-acep-7c") || !strcmp(compat_string, "good-display/gdep073e01")) { ctx = epaper_display_create_port(global, opts); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + } else if (!strcmp(compat_string, "waveshare,esp32-s3-touch-lcd-7") + || !strcmp(compat_string, "esp_lcd,rgb")) { + ctx = rgb_lcd_display_create_port(global, opts); +#endif } else if (!strcmp(compat_string, "sharp,memory-lcd")) { ctx = memory_lcd_display_create_port(global, opts); } else if (!strcmp(compat_string, "ilitek,ili9341") diff --git a/rgb_lcd_display_driver.c b/rgb_lcd_display_driver.c new file mode 100644 index 0000000..6571a54 --- /dev/null +++ b/rgb_lcd_display_driver.c @@ -0,0 +1,1006 @@ +/* + * This file is part of AtomGL. + * + * Copyright 2026 AtomGL contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "display_driver.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "dcs_lcd_draw.h" +#include "dcs_lcd_screen.h" +#include "display_items.h" +#include "display_message.h" +#include "display_task.h" + +static const char *TAG = "rgb_lcd_display_driver"; + +struct RGBLCDDriver +{ + esp_lcd_panel_handle_t panel; + struct DCSLCDScreen screen; + uint16_t *framebuffers[3]; + size_t framebuffer_count; + int active_fb_index; + int previous_fb_index; + uint16_t *cover_buffer; + size_t cover_buffer_pixels; + uint16_t *scaled_buffer; + size_t scaled_buffer_pixels; + uint16_t *background_buffer; + size_t background_buffer_pixels; + Context *ctx; + struct DisplayTaskArgs display_args; +}; + +#define RGB_LCD_DRIVER_FROM_CTX(ctx) \ + CONTAINER_OF((struct DisplayTaskArgs *) (ctx)->platform_data, struct RGBLCDDriver, display_args) + +static void surface_line_to_rgb565(uint16_t *line, int width) +{ + for (int i = 0; i < width; i++) { + line[i] = __builtin_bswap16(line[i]); + } +} + +static uint16_t *active_framebuffer(struct RGBLCDDriver *driver) +{ + if (driver->active_fb_index < 0 || driver->active_fb_index >= (int) driver->framebuffer_count) { + return NULL; + } + return driver->framebuffers[driver->active_fb_index]; +} + +static int select_work_framebuffer(struct RGBLCDDriver *driver) +{ + if (driver->framebuffer_count == 0) { + return -1; + } + + for (size_t i = 0; i < driver->framebuffer_count; i++) { + if ((int) i != driver->active_fb_index && (int) i != driver->previous_fb_index) { + return (int) i; + } + } + for (size_t i = 0; i < driver->framebuffer_count; i++) { + if ((int) i != driver->active_fb_index) { + return (int) i; + } + } + return driver->active_fb_index; +} + +static esp_err_t switch_to_framebuffer(struct RGBLCDDriver *driver, int fb_index) +{ + if (fb_index < 0 || fb_index >= (int) driver->framebuffer_count) { + return ESP_ERR_INVALID_ARG; + } + + esp_err_t err = esp_lcd_panel_draw_bitmap( + driver->panel, 0, 0, driver->screen.w, driver->screen.h, driver->framebuffers[fb_index]); + if (err == ESP_OK) { + driver->previous_fb_index = driver->active_fb_index; + driver->active_fb_index = fb_index; + } + return err; +} + +static void copy_rgb565_region_to_framebuffer(struct RGBLCDDriver *driver, int fb_index, int x, int y, int width, int height, const uint16_t *pixels) +{ + uint16_t *dst_fb = driver->framebuffers[fb_index]; + for (int row = 0; row < height; row++) { + uint16_t *dst = dst_fb + ((y + row) * driver->screen.w) + x; + memcpy(dst, pixels + ((size_t) row * width), (size_t) width * sizeof(uint16_t)); + } +} + +static void mirror_line_to_inactive_framebuffers(struct RGBLCDDriver *driver, int y, int x0, int width, const uint16_t *line) +{ + if (driver->framebuffer_count <= 1) { + return; + } + + for (size_t i = 0; i < driver->framebuffer_count; i++) { + if ((int) i == driver->active_fb_index) { + continue; + } + uint16_t *fb = driver->framebuffers[i]; + if (!fb) { + continue; + } + memcpy(fb + ((size_t) y * driver->screen.w) + x0, line, (size_t) width * sizeof(uint16_t)); + } +} + +static void mirror_active_to_inactive_framebuffers(struct RGBLCDDriver *driver) +{ + if (driver->framebuffer_count <= 1) { + return; + } + + uint16_t *active = active_framebuffer(driver); + if (!active) { + return; + } + + size_t fb_bytes = (size_t) driver->screen.w * (size_t) driver->screen.h * sizeof(uint16_t); + for (size_t i = 0; i < driver->framebuffer_count; i++) { + if ((int) i == driver->active_fb_index) { + continue; + } + uint16_t *fb = driver->framebuffers[i]; + if (!fb) { + continue; + } + memcpy(fb, active, fb_bytes); + } +} + +static void mirror_region_to_inactive_framebuffers(struct RGBLCDDriver *driver, int x, int y, int width, int height, const uint16_t *pixels) +{ + if (driver->framebuffer_count <= 1) { + return; + } + + for (size_t i = 0; i < driver->framebuffer_count; i++) { + if ((int) i == driver->active_fb_index) { + continue; + } + if (!driver->framebuffers[i]) { + continue; + } + copy_rgb565_region_to_framebuffer(driver, (int) i, x, y, width, height, pixels); + } +} +static void mirror_region_from_active_to_inactive_framebuffers(struct RGBLCDDriver *driver, int x, int y, int width, int height) +{ + if (driver->framebuffer_count <= 1) { + return; + } + + uint16_t *active = active_framebuffer(driver); + if (!active) { + return; + } + + for (size_t i = 0; i < driver->framebuffer_count; i++) { + if ((int) i == driver->active_fb_index) { + continue; + } + uint16_t *fb = driver->framebuffers[i]; + if (!fb) { + continue; + } + for (int row = 0; row < height; row++) { + const uint16_t *src = active + ((size_t) (y + row) * driver->screen.w) + x; + uint16_t *dst = fb + ((size_t) (y + row) * driver->screen.w) + x; + memcpy(dst, src, (size_t) width * sizeof(uint16_t)); + } + } +} + +static bool ensure_cover_buffer(struct RGBLCDDriver *driver, int width, int height) +{ + size_t pixel_count = (size_t) width * (size_t) height; + if (driver->cover_buffer && driver->cover_buffer_pixels >= pixel_count) { + return true; + } + + if (driver->cover_buffer) { + heap_caps_free(driver->cover_buffer); + driver->cover_buffer = NULL; + driver->cover_buffer_pixels = 0; + } + + driver->cover_buffer = heap_caps_malloc(pixel_count * sizeof(uint16_t), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (!driver->cover_buffer) { + driver->cover_buffer = heap_caps_malloc(pixel_count * sizeof(uint16_t), MALLOC_CAP_8BIT); + } + if (!driver->cover_buffer) { + ESP_LOGE(TAG, "Failed to allocate cover buffer (%zu pixels).", pixel_count); + return false; + } + + driver->cover_buffer_pixels = pixel_count; + return true; +} + +static bool ensure_scaled_buffer(struct RGBLCDDriver *driver, int width, int height) +{ + size_t pixel_count = (size_t) width * (size_t) height; + if (driver->scaled_buffer && driver->scaled_buffer_pixels >= pixel_count) { + return true; + } + + if (driver->scaled_buffer) { + heap_caps_free(driver->scaled_buffer); + driver->scaled_buffer = NULL; + driver->scaled_buffer_pixels = 0; + } + + driver->scaled_buffer = heap_caps_malloc(pixel_count * sizeof(uint16_t), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (!driver->scaled_buffer) { + driver->scaled_buffer = heap_caps_malloc(pixel_count * sizeof(uint16_t), MALLOC_CAP_8BIT); + } + if (!driver->scaled_buffer) { + ESP_LOGE(TAG, "Failed to allocate scaled buffer (%zu pixels).", pixel_count); + return false; + } + + driver->scaled_buffer_pixels = pixel_count; + return true; +} + +static bool ensure_background_buffer(struct RGBLCDDriver *driver) +{ + size_t pixel_count = (size_t) driver->screen.w * (size_t) driver->screen.h; + if (driver->background_buffer && driver->background_buffer_pixels >= pixel_count) { + return true; + } + + if (driver->background_buffer) { + heap_caps_free(driver->background_buffer); + driver->background_buffer = NULL; + driver->background_buffer_pixels = 0; + } + + driver->background_buffer = heap_caps_malloc(pixel_count * sizeof(uint16_t), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (!driver->background_buffer) { + driver->background_buffer = heap_caps_malloc(pixel_count * sizeof(uint16_t), MALLOC_CAP_8BIT); + } + if (!driver->background_buffer) { + ESP_LOGE(TAG, "Failed to allocate background buffer (%zu pixels).", pixel_count); + return false; + } + + driver->background_buffer_pixels = pixel_count; + return true; +} + +static void maybe_store_fullscreen_background( + struct RGBLCDDriver *driver, int x, int y, int width, int height, const uint16_t *pixels) +{ + if (x != 0 || y != 0 || width != driver->screen.w || height != driver->screen.h) { + return; + } + if (!ensure_background_buffer(driver)) { + return; + } + + size_t bytes = (size_t) width * (size_t) height * sizeof(uint16_t); + memcpy(driver->background_buffer, pixels, bytes); + ESP_LOGI(TAG, "stored fullscreen background: %dx%d", width, height); +} + +static bool restore_background_line_to_surface(struct RGBLCDDriver *driver, int y, int x0, int width) +{ + if (!driver->background_buffer) { + return false; + } + + memcpy( + driver->screen.pixels + x0, + driver->background_buffer + ((size_t) y * driver->screen.w) + x0, + (size_t) width * sizeof(uint16_t)); + surface_line_to_rgb565(driver->screen.pixels + x0, width); + return true; +} + +static void scale_rgb565_nearest( + const uint16_t *src, int src_w, int src_h, uint16_t *dst, int dst_w, int dst_h) +{ + for (int y = 0; y < dst_h; y++) { + int src_y = ((y * src_h) + (dst_h / 2)) / dst_h; + if (src_y >= src_h) { + src_y = src_h - 1; + } + const uint16_t *src_row = src + ((size_t) src_y * src_w); + uint16_t *dst_row = dst + ((size_t) y * dst_w); + for (int x = 0; x < dst_w; x++) { + int src_x = ((x * src_w) + (dst_w / 2)) / dst_w; + if (src_x >= src_w) { + src_x = src_w - 1; + } + dst_row[x] = src_row[src_x]; + } + } +} + +static bool display_list_to_items(Context *ctx, term display_list, BaseDisplayItem **out_items, int *out_len) +{ + int proper; + int len = term_list_length(display_list, &proper); + if (!proper || len < 0) { + ESP_LOGE(TAG, "Invalid display list."); + return false; + } + + BaseDisplayItem *items = malloc(sizeof(BaseDisplayItem) * len); + if (UNLIKELY(!items)) { + fprintf(stderr, "rgb display_list_to_items: failed to alloc items\n"); + return false; + } + + term t = display_list; + for (int i = 0; i < len; i++) { + display_items_init_item(&items[i], term_get_list_head(t), ctx); + t = term_get_list_tail(t); + } + + *out_items = items; + *out_len = len; + return true; +} + +static bool render_items_to_framebuffer( + struct RGBLCDDriver *driver, int fb_index, int x0, int y0, int width, int height, BaseDisplayItem *items, int len) +{ + uint16_t *fb = driver->framebuffers[fb_index]; + for (int y = y0; y < y0 + height; y++) { + (void) restore_background_line_to_surface(driver, y, x0, width); + int x = x0; + while (x < x0 + width) { + int drawn_pixels = dcs_lcd_draw_x(&driver->screen, x, y, items, len); + if (drawn_pixels <= 0) { + ESP_LOGE(TAG, "Renderer stalled at x=%d y=%d.", x, y); + return false; + } + if (x + drawn_pixels > x0 + width) { + drawn_pixels = (x0 + width) - x; + } + x += drawn_pixels; + } + + surface_line_to_rgb565(driver->screen.pixels + x0, width); + memcpy(fb + ((size_t) y * driver->screen.w) + x0, driver->screen.pixels + x0, (size_t) width * sizeof(uint16_t)); + } + return true; +} + +static bool int_from_opts(term opts, const char *atom_str, int default_value, int *out, GlobalContext *global) +{ + term value = interop_kv_get_value_default(opts, atom_str, term_from_int(default_value), global); + if (!term_is_integer(value)) { + return false; + } + *out = term_to_int(value); + return true; +} + +static bool bool_from_opts(term opts, const char *atom_str, bool default_value, bool *out, GlobalContext *global) +{ + term value = interop_kv_get_value_default( + opts, atom_str, default_value ? TRUE_ATOM : FALSE_ATOM, global); + if (value == TRUE_ATOM) { + *out = true; + return true; + } + if (value == FALSE_ATOM) { + *out = false; + return true; + } + return false; +} + +static bool parse_data_gpios(term opts, int data_gpios[16], GlobalContext *global) +{ + term key = globalcontext_make_atom(global, ATOM_STR("\xA", "data_gpios")); + term value = interop_proplist_get_value(opts, key); + if (value == term_nil()) { + return false; + } + + term list = value; + for (int i = 0; i < 16; i++) { + if (!term_is_nonempty_list(list)) { + return false; + } + term head = term_get_list_head(list); + if (!term_is_integer(head)) { + return false; + } + data_gpios[i] = term_to_int(head); + list = term_get_list_tail(list); + } + + return list == term_nil(); +} + +static void do_update(Context *ctx, term display_list) +{ + struct RGBLCDDriver *driver = RGB_LCD_DRIVER_FROM_CTX(ctx); + BaseDisplayItem *items = NULL; + int len = 0; + if (!display_list_to_items(ctx, display_list, &items, &len)) { + return; + } + + if (driver->framebuffer_count > 1) { + int work_fb = select_work_framebuffer(driver); + if (work_fb >= 0 + && render_items_to_framebuffer(driver, work_fb, 0, 0, driver->screen.w, driver->screen.h, items, len)) { + esp_err_t err = switch_to_framebuffer(driver, work_fb); + if (err != ESP_OK) { + ESP_LOGE(TAG, "framebuffer switch failed: %s", esp_err_to_name(err)); + } else { + // Keep non-active framebuffers aligned so cover-only swaps don't require full-frame copy. + mirror_active_to_inactive_framebuffers(driver); + } + } + display_items_delete(items, len); + return; + } + + for (int y = 0; y < driver->screen.h; y++) { + (void) restore_background_line_to_surface(driver, y, 0, driver->screen.w); + int x = 0; + while (x < driver->screen.w) { + int drawn_pixels = dcs_lcd_draw_x(&driver->screen, x, y, items, len); + if (drawn_pixels <= 0) { + ESP_LOGE(TAG, "Renderer stalled at x=%d y=%d.", x, y); + display_items_delete(items, len); + return; + } + x += drawn_pixels; + } + + surface_line_to_rgb565(driver->screen.pixels, driver->screen.w); + esp_err_t err = esp_lcd_panel_draw_bitmap( + driver->panel, 0, y, driver->screen.w, y + 1, driver->screen.pixels); + if (err != ESP_OK) { + ESP_LOGE(TAG, "draw_bitmap failed: %s", esp_err_to_name(err)); + break; + } + } + + display_items_delete(items, len); +} + +static void do_update_region(Context *ctx, int x0, int y0, int width, int height, term display_list) +{ + struct RGBLCDDriver *driver = RGB_LCD_DRIVER_FROM_CTX(ctx); + if (width <= 0 || height <= 0 || x0 >= driver->screen.w || y0 >= driver->screen.h) { + return; + } + if (x0 < 0) { + width += x0; + x0 = 0; + } + if (y0 < 0) { + height += y0; + y0 = 0; + } + if (x0 + width > driver->screen.w) { + width = driver->screen.w - x0; + } + if (y0 + height > driver->screen.h) { + height = driver->screen.h - y0; + } + + BaseDisplayItem *items = NULL; + int len = 0; + if (!display_list_to_items(ctx, display_list, &items, &len)) { + return; + } + + if (driver->framebuffer_count > 1) { + int work_fb = select_work_framebuffer(driver); + if (work_fb >= 0 + && render_items_to_framebuffer(driver, work_fb, x0, y0, width, height, items, len)) { + esp_err_t err = switch_to_framebuffer(driver, work_fb); + if (err != ESP_OK) { + ESP_LOGE(TAG, "region framebuffer switch failed: %s", esp_err_to_name(err)); + } else { + mirror_region_from_active_to_inactive_framebuffers(driver, x0, y0, width, height); + } + } + display_items_delete(items, len); + return; + } + + for (int y = y0; y < y0 + height; y++) { + (void) restore_background_line_to_surface(driver, y, x0, width); + int x = x0; + while (x < x0 + width) { + int drawn_pixels = dcs_lcd_draw_x(&driver->screen, x, y, items, len); + if (drawn_pixels <= 0) { + ESP_LOGE(TAG, "Region renderer stalled at x=%d y=%d.", x, y); + display_items_delete(items, len); + return; + } + if (x + drawn_pixels > x0 + width) { + drawn_pixels = (x0 + width) - x; + } + x += drawn_pixels; + } + + surface_line_to_rgb565(driver->screen.pixels + x0, width); + esp_err_t err = esp_lcd_panel_draw_bitmap( + driver->panel, x0, y, x0 + width, y + 1, driver->screen.pixels + x0); + if (err != ESP_OK) { + ESP_LOGE(TAG, "draw_bitmap region failed: %s", esp_err_to_name(err)); + break; + } + + mirror_line_to_inactive_framebuffers(driver, y, x0, width, driver->screen.pixels + x0); + } + + display_items_delete(items, len); +} + +static int base64_value(uint8_t c) +{ + if (c >= 'A' && c <= 'Z') { + return c - 'A'; + } + if (c >= 'a' && c <= 'z') { + return c - 'a' + 26; + } + if (c >= '0' && c <= '9') { + return c - '0' + 52; + } + if (c == '+') { + return 62; + } + if (c == '/') { + return 63; + } + if (c == '=') { + return -2; + } + return -1; +} + +static bool draw_rle_pixel(struct RGBLCDDriver *driver, int width, int height, uint32_t *out_index, uint16_t color) +{ + uint32_t pixel_count = (uint32_t) width * (uint32_t) height; + if (*out_index >= pixel_count) { + return true; + } + + driver->cover_buffer[*out_index] = color; + (*out_index)++; + + return true; +} + +static bool feed_rle_byte(struct RGBLCDDriver *driver, int width, int height, uint32_t *out_index, uint8_t *rle_len, uint8_t rle_buf[3], uint8_t byte) +{ + rle_buf[(*rle_len)++] = byte; + if (*rle_len < 3) { + return true; + } + + uint8_t count = rle_buf[0]; + uint16_t color = (uint16_t) rle_buf[1] | ((uint16_t) rle_buf[2] << 8); + for (uint8_t i = 0; i < count; i++) { + if (!draw_rle_pixel(driver, width, height, out_index, color)) { + *rle_len = 0; + return false; + } + } + *rle_len = 0; + return true; +} + +static void do_draw_rgb565_rle_base64_scaled( + Context *ctx, int x, int y, int src_width, int src_height, int dst_width, int dst_height, term b64_term) +{ + struct RGBLCDDriver *driver = RGB_LCD_DRIVER_FROM_CTX(ctx); + if (!term_is_binary(b64_term) + || src_width <= 0 || src_height <= 0 + || dst_width <= 0 || dst_height <= 0 + || x < 0 || y < 0 + || x + dst_width > driver->screen.w || y + dst_height > driver->screen.h) { + ESP_LOGE(TAG, "Invalid draw_rgb565_rle_base64 arguments."); + return; + } + if (!ensure_cover_buffer(driver, src_width, src_height)) { + return; + } + + const uint8_t *b64 = (const uint8_t *) term_binary_data(b64_term); + size_t b64_len = term_binary_size(b64_term); + uint8_t quad[4]; + uint8_t quad_len = 0; + uint8_t rle_buf[3]; + uint8_t rle_len = 0; + uint32_t out_index = 0; + + for (size_t i = 0; i < b64_len; i++) { + int v = base64_value(b64[i]); + if (v == -1) { + continue; + } + quad[quad_len++] = b64[i]; + if (quad_len < 4) { + continue; + } + + int v0 = base64_value(quad[0]); + int v1 = base64_value(quad[1]); + int v2 = base64_value(quad[2]); + int v3 = base64_value(quad[3]); + if (v0 < 0 || v1 < 0 || v2 == -1 || v3 == -1) { + ESP_LOGE(TAG, "Invalid base64 cover data."); + return; + } + + uint8_t out0 = (uint8_t) ((v0 << 2) | (v1 >> 4)); + if (!feed_rle_byte(driver, src_width, src_height, &out_index, &rle_len, rle_buf, out0)) { + return; + } + if (v2 >= 0) { + uint8_t out1 = (uint8_t) (((v1 & 0x0F) << 4) | (v2 >> 2)); + if (!feed_rle_byte(driver, src_width, src_height, &out_index, &rle_len, rle_buf, out1)) { + return; + } + } + if (v3 >= 0) { + uint8_t out2 = (uint8_t) (((v2 & 0x03) << 6) | v3); + if (!feed_rle_byte(driver, src_width, src_height, &out_index, &rle_len, rle_buf, out2)) { + return; + } + } + quad_len = 0; + + if (out_index >= (uint32_t) src_width * (uint32_t) src_height) { + break; + } + if ((i & 0x1FFF) == 0x1FFF) { + vTaskDelay(1); + } + } + + uint32_t pixel_count = (uint32_t) src_width * (uint32_t) src_height; + if (out_index == pixel_count) { + const uint16_t *draw_pixels = driver->cover_buffer; + int draw_width = src_width; + int draw_height = src_height; + + if (dst_width != src_width || dst_height != src_height) { + if (!ensure_scaled_buffer(driver, dst_width, dst_height)) { + return; + } + scale_rgb565_nearest( + driver->cover_buffer, src_width, src_height, driver->scaled_buffer, dst_width, dst_height); + draw_pixels = driver->scaled_buffer; + draw_width = dst_width; + draw_height = dst_height; + } + + maybe_store_fullscreen_background(driver, x, y, draw_width, draw_height, draw_pixels); + + if (driver->framebuffer_count > 1) { + int work_fb = select_work_framebuffer(driver); + if (work_fb < 0) { + ESP_LOGE(TAG, "cover framebuffer select failed."); + return; + } + copy_rgb565_region_to_framebuffer(driver, work_fb, x, y, draw_width, draw_height, draw_pixels); + esp_err_t err = switch_to_framebuffer(driver, work_fb); + if (err != ESP_OK) { + ESP_LOGE(TAG, "cover framebuffer switch failed: %s", esp_err_to_name(err)); + return; + } + mirror_region_to_inactive_framebuffers(driver, x, y, draw_width, draw_height, draw_pixels); + } else { + esp_err_t err = esp_lcd_panel_draw_bitmap( + driver->panel, x, y, x + draw_width, y + draw_height, draw_pixels); + if (err != ESP_OK) { + ESP_LOGE(TAG, "draw_bitmap cover failed: %s", esp_err_to_name(err)); + return; + } + mirror_region_to_inactive_framebuffers(driver, x, y, draw_width, draw_height, draw_pixels); + } + } + + ESP_LOGI( + TAG, "cover RLE draw complete: %" PRIu32 "/%" PRIu32 " pixels (%dx%d -> %dx%d)", + out_index, pixel_count, src_width, src_height, dst_width, dst_height); +} + +static void do_draw_rgb565_rle_base64(Context *ctx, int x, int y, int width, int height, term b64_term) +{ + do_draw_rgb565_rle_base64_scaled(ctx, x, y, width, height, width, height, b64_term); +} + +static void process_message(Message *message, Context *ctx) +{ + GenMessage gen_message; + if (UNLIKELY(port_parse_gen_message(message->message, &gen_message) != GenCallMessage)) { + fprintf(stderr, "Received invalid message."); + AVM_ABORT(); + } + + term req = gen_message.req; + if (UNLIKELY(!term_is_tuple(req) || term_get_tuple_arity(req) < 1)) { + AVM_ABORT(); + } + term cmd = term_get_tuple_element(req, 0); + + struct RGBLCDDriver *driver = RGB_LCD_DRIVER_FROM_CTX(ctx); + + if (cmd == globalcontext_make_atom(ctx->global, ATOM_STR("\x6", "update"))) { + do_update(ctx, term_get_tuple_element(req, 1)); + return; + + } else if (cmd == globalcontext_make_atom(ctx->global, ATOM_STR("\xD", "update_region"))) { + int x = term_to_int(term_get_tuple_element(req, 1)); + int y = term_to_int(term_get_tuple_element(req, 2)); + int width = term_to_int(term_get_tuple_element(req, 3)); + int height = term_to_int(term_get_tuple_element(req, 4)); + do_update_region(ctx, x, y, width, height, term_get_tuple_element(req, 5)); + return; + + } else if (cmd == globalcontext_make_atom(ctx->global, ATOM_STR("\x16", "draw_rgb565_rle_base64"))) { + int x = term_to_int(term_get_tuple_element(req, 1)); + int y = term_to_int(term_get_tuple_element(req, 2)); + int width = term_to_int(term_get_tuple_element(req, 3)); + int height = term_to_int(term_get_tuple_element(req, 4)); + do_draw_rgb565_rle_base64(ctx, x, y, width, height, term_get_tuple_element(req, 5)); + return; + + } else if (cmd == globalcontext_make_atom(ctx->global, ATOM_STR("\x1D", "draw_rgb565_rle_base64_scaled"))) { + int x = term_to_int(term_get_tuple_element(req, 1)); + int y = term_to_int(term_get_tuple_element(req, 2)); + int src_width = term_to_int(term_get_tuple_element(req, 3)); + int src_height = term_to_int(term_get_tuple_element(req, 4)); + int dst_width = term_to_int(term_get_tuple_element(req, 5)); + int dst_height = term_to_int(term_get_tuple_element(req, 6)); + do_draw_rgb565_rle_base64_scaled( + ctx, x, y, src_width, src_height, dst_width, dst_height, term_get_tuple_element(req, 7)); + return; + + } else if (cmd == globalcontext_make_atom(ctx->global, ATOM_STR("\xB", "draw_buffer"))) { + int x = term_to_int(term_get_tuple_element(req, 1)); + int y = term_to_int(term_get_tuple_element(req, 2)); + int width = term_to_int(term_get_tuple_element(req, 3)); + int height = term_to_int(term_get_tuple_element(req, 4)); + unsigned long addr_low = term_to_int(term_get_tuple_element(req, 5)); + unsigned long addr_high = term_to_int(term_get_tuple_element(req, 6)); + const void *data = (const void *) (addr_low | (addr_high << 16)); + + esp_lcd_panel_draw_bitmap(driver->panel, x, y, x + width, y + height, data); + return; + + } else if (cmd == globalcontext_make_atom(ctx->global, ATOM_STR("\xF", "draw_rgb565_raw"))) { + int x = term_to_int(term_get_tuple_element(req, 1)); + int y = term_to_int(term_get_tuple_element(req, 2)); + int width = term_to_int(term_get_tuple_element(req, 3)); + int height = term_to_int(term_get_tuple_element(req, 4)); + term pixels_term = term_get_tuple_element(req, 5); + + if (!term_is_binary(pixels_term) || width <= 0 || height <= 0 + || x < 0 || y < 0 + || x + width > driver->screen.w || y + height > driver->screen.h) { + ESP_LOGE(TAG, "Invalid draw_rgb565_raw arguments."); + return; + } + + size_t expected = (size_t) width * (size_t) height * 2; + const uint8_t *raw = (const uint8_t *) term_binary_data(pixels_term); + size_t raw_len = term_binary_size(pixels_term); + + if (raw_len < expected) { + ESP_LOGE(TAG, "draw_rgb565_raw: data too small (%zu < %zu)", raw_len, expected); + return; + } + + if (driver->framebuffer_count > 1) { + maybe_store_fullscreen_background(driver, x, y, width, height, (const uint16_t *) raw); + int work_fb = select_work_framebuffer(driver); + if (work_fb < 0) { + ESP_LOGE(TAG, "draw_rgb565_raw: framebuffer select failed."); + return; + } + copy_rgb565_region_to_framebuffer(driver, work_fb, x, y, width, height, (const uint16_t *) raw); + esp_err_t err = switch_to_framebuffer(driver, work_fb); + if (err != ESP_OK) { + ESP_LOGE(TAG, "draw_rgb565_raw: framebuffer switch failed: %s", esp_err_to_name(err)); + return; + } + mirror_region_to_inactive_framebuffers(driver, x, y, width, height, (const uint16_t *) raw); + } else { + maybe_store_fullscreen_background(driver, x, y, width, height, (const uint16_t *) raw); + esp_err_t err = esp_lcd_panel_draw_bitmap( + driver->panel, x, y, x + width, y + height, raw); + if (err != ESP_OK) { + ESP_LOGE(TAG, "draw_rgb565_raw: draw_bitmap failed: %s", esp_err_to_name(err)); + return; + } + mirror_region_to_inactive_framebuffers(driver, x, y, width, height, (const uint16_t *) raw); + } + + ESP_LOGI(TAG, "draw_rgb565_raw: %dx%d pixels at (%d,%d)", width, height, x, y); + return; + } + + fprintf(stderr, "rgb_display: "); + term_display(stderr, req, ctx); + fprintf(stderr, "\n"); + + BEGIN_WITH_STACK_HEAP(TUPLE_SIZE(2) + REF_SIZE, heap); + term return_tuple = term_alloc_tuple(2, &heap); + term_put_tuple_element(return_tuple, 0, gen_message.ref); + term_put_tuple_element(return_tuple, 1, OK_ATOM); + display_message_send(gen_message.pid, return_tuple, ctx->global); + END_WITH_STACK_HEAP(heap, ctx->global); +} + +static void display_init(Context *ctx, term opts) +{ + struct RGBLCDDriver *driver = calloc(1, sizeof(struct RGBLCDDriver)); + if (!driver) { + ESP_LOGE(TAG, "Failed to allocate driver."); + return; + } + driver->active_fb_index = 0; + driver->previous_fb_index = -1; + + int width; + int height; + int pclk_hz; + int hsync_pulse_width; + int hsync_back_porch; + int hsync_front_porch; + int vsync_pulse_width; + int vsync_back_porch; + int vsync_front_porch; + int hsync_gpio; + int vsync_gpio; + int de_gpio; + int pclk_gpio; + int bounce_buffer_size_px; + bool pclk_active_neg; + bool fb_in_psram; + int data_gpios[16]; + + bool ok = true; + ok = ok && int_from_opts(opts, ATOM_STR("\x5", "width"), 800, &width, ctx->global); + ok = ok && int_from_opts(opts, ATOM_STR("\x6", "height"), 480, &height, ctx->global); + ok = ok && int_from_opts(opts, ATOM_STR("\x7", "pclk_hz"), 16000000, &pclk_hz, ctx->global); + ok = ok && int_from_opts(opts, ATOM_STR("\x11", "hsync_pulse_width"), 4, &hsync_pulse_width, ctx->global); + ok = ok && int_from_opts(opts, ATOM_STR("\x10", "hsync_back_porch"), 8, &hsync_back_porch, ctx->global); + ok = ok && int_from_opts(opts, ATOM_STR("\x11", "hsync_front_porch"), 8, &hsync_front_porch, ctx->global); + ok = ok && int_from_opts(opts, ATOM_STR("\x11", "vsync_pulse_width"), 4, &vsync_pulse_width, ctx->global); + ok = ok && int_from_opts(opts, ATOM_STR("\x10", "vsync_back_porch"), 8, &vsync_back_porch, ctx->global); + ok = ok && int_from_opts(opts, ATOM_STR("\x11", "vsync_front_porch"), 8, &vsync_front_porch, ctx->global); + ok = ok && int_from_opts(opts, ATOM_STR("\xA", "hsync_gpio"), 46, &hsync_gpio, ctx->global); + ok = ok && int_from_opts(opts, ATOM_STR("\xA", "vsync_gpio"), 3, &vsync_gpio, ctx->global); + ok = ok && int_from_opts(opts, ATOM_STR("\x7", "de_gpio"), 5, &de_gpio, ctx->global); + ok = ok && int_from_opts(opts, ATOM_STR("\x9", "pclk_gpio"), 7, &pclk_gpio, ctx->global); + ok = ok && int_from_opts(opts, ATOM_STR("\x15", "bounce_buffer_size_px"), 8000, &bounce_buffer_size_px, ctx->global); + ok = ok && bool_from_opts(opts, ATOM_STR("\xF", "pclk_active_neg"), true, &pclk_active_neg, ctx->global); + ok = ok && bool_from_opts(opts, ATOM_STR("\xB", "fb_in_psram"), true, &fb_in_psram, ctx->global); + ok = ok && parse_data_gpios(opts, data_gpios, ctx->global); + + if (!ok) { + ESP_LOGE(TAG, "Failed init: invalid RGB LCD parameters."); + free(driver); + return; + } + + driver->screen.w = width; + driver->screen.h = height; + driver->screen.pixels = heap_caps_malloc(width * sizeof(uint16_t), MALLOC_CAP_DMA); + if (!driver->screen.pixels) { + ESP_LOGE(TAG, "Failed to allocate scanline."); + free(driver); + return; + } + + esp_lcd_rgb_panel_config_t config = { + .clk_src = LCD_CLK_SRC_PLL160M, + .timings = { + .pclk_hz = pclk_hz, + .h_res = width, + .v_res = height, + .hsync_pulse_width = hsync_pulse_width, + .hsync_back_porch = hsync_back_porch, + .hsync_front_porch = hsync_front_porch, + .vsync_pulse_width = vsync_pulse_width, + .vsync_back_porch = vsync_back_porch, + .vsync_front_porch = vsync_front_porch, + .flags = { + .pclk_active_neg = pclk_active_neg, + }, + }, + .data_width = 16, + .num_fbs = 2, + .psram_trans_align = 64, + .bounce_buffer_size_px = bounce_buffer_size_px, + .hsync_gpio_num = hsync_gpio, + .vsync_gpio_num = vsync_gpio, + .de_gpio_num = de_gpio, + .pclk_gpio_num = pclk_gpio, + .disp_gpio_num = -1, + .flags = { + .fb_in_psram = fb_in_psram, + }, + }; + + for (int i = 0; i < 16; i++) { + config.data_gpio_nums[i] = data_gpios[i]; + } + + esp_err_t err = esp_lcd_new_rgb_panel(&config, &driver->panel); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_lcd_new_rgb_panel failed: %s", esp_err_to_name(err)); + heap_caps_free(driver->screen.pixels); + free(driver); + return; + } + + err = esp_lcd_panel_reset(driver->panel); + if (err != ESP_OK) { + ESP_LOGE(TAG, "panel reset failed: %s", esp_err_to_name(err)); + return; + } + err = esp_lcd_panel_init(driver->panel); + if (err != ESP_OK) { + ESP_LOGE(TAG, "panel init failed: %s", esp_err_to_name(err)); + return; + } + + void *fb0 = NULL; + void *fb1 = NULL; + err = esp_lcd_rgb_panel_get_frame_buffer(driver->panel, 2, &fb0, &fb1); + if (err == ESP_OK && fb0 && fb1) { + driver->framebuffers[0] = fb0; + driver->framebuffers[1] = fb1; + driver->framebuffer_count = 2; + ESP_LOGI(TAG, "Using RGB LCD double framebuffer: %p %p", fb0, fb1); + } else { + ESP_LOGW(TAG, "RGB LCD multi-framebuffer unavailable, using draw_bitmap path: %s", esp_err_to_name(err)); + } + + driver->display_args.messages_queue = xQueueCreate(32, sizeof(Message *)); + driver->display_args.process_message_fn = process_message; + driver->display_args.ctx = ctx; + driver->ctx = ctx; + ctx->platform_data = &driver->display_args; + + xTaskCreate(display_task_process_messages, "display", 10000, &driver->display_args, 1, NULL); +} + +Context *rgb_lcd_display_create_port(GlobalContext *global, term opts) +{ + Context *ctx = context_new(global); + ctx->native_handler = display_task_consume_mailbox; + display_init(ctx, opts); + return ctx; +} From b98879d4b9af020e3200ef1d67ff6b717fdbee13 Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Wed, 17 Jun 2026 14:00:50 +0200 Subject: [PATCH 5/8] docs: Document RGB LCD driver, update_region, draw_rgb565_raw, and transparent compositing Add documentation for the new RGB LCD display driver to display-drivers.md, including all configuration options, compat strings, and an example configuration. Document the update_region and draw_rgb565_raw port commands. Update the primitives documentation to describe the display-list transparent background compositing behaviour for anti-aliased uFont text. Signed-off-by: Ibrahim YILMAZ --- docs/display-drivers.md | 82 +++++++++++++++++++++++++++++++++++++++++ docs/primitives.md | 5 +++ 2 files changed, 87 insertions(+) diff --git a/docs/display-drivers.md b/docs/display-drivers.md index f48c3cb..019b202 100644 --- a/docs/display-drivers.md +++ b/docs/display-drivers.md @@ -322,6 +322,88 @@ Each entry can be: These sequences are highly specific to each display model and typically come from the manufacturer's datasheet or reference implementation. +### RGB LCD Panels (esp_lcd,rgb) + +RGB LCD panels driven through the ESP32-S3 RGB LCD peripheral. Requires ESP-IDF 5 or later. + +**Compatible strings:** `"esp_lcd,rgb"` or `"waveshare,esp32-s3-touch-lcd-7"` + +| Option | Type | Description | Default | +|--------|------|-------------|---------| +| `width` | integer | Display width in pixels | 800 | +| `height` | integer | Display height in pixels | 480 | +| `pclk_hz` | integer | Pixel clock frequency | 16_000_000 | +| `hsync_pulse_width` | integer | HSYNC pulse width | 4 | +| `hsync_back_porch` | integer | HSYNC back porch | 8 | +| `hsync_front_porch` | integer | HSYNC front porch | 8 | +| `vsync_pulse_width` | integer | VSYNC pulse width | 4 | +| `vsync_back_porch` | integer | VSYNC back porch | 8 | +| `vsync_front_porch` | integer | VSYNC front porch | 8 | +| `hsync_gpio` | integer | HSYNC GPIO pin | 46 | +| `vsync_gpio` | integer | VSYNC GPIO pin | 3 | +| `de_gpio` | integer | Data enable GPIO pin | 5 | +| `pclk_gpio` | integer | Pixel clock GPIO pin | 7 | +| `data_gpios` | list of 16 integers | Data GPIO pins (R0–R4, G0–G5, B0–B4) | Required | +| `bounce_buffer_size_px` | integer | DMA bounce buffer size | 8000 | +| `pclk_active_neg` | boolean | PCLK active on negative edge | true | +| `fb_in_psram` | boolean | Place framebuffers in PSRAM | true | + +The RGB LCD driver supports double framebuffering in PSRAM when available for +tear-free rendering. It provides the standard `update` and `update_region` port +commands, plus `draw_rgb565_raw` for direct RGB565 binary frame delivery. + +**Example:** +```elixir +rgb_lcd_opts = [ + compatible: "esp_lcd,rgb", + width: 800, + height: 480, + pclk_hz: 16_000_000, + hsync_pulse_width: 4, + hsync_back_porch: 8, + hsync_front_porch: 8, + vsync_pulse_width: 4, + vsync_back_porch: 8, + vsync_front_porch: 8, + hsync_gpio: 46, + vsync_gpio: 3, + de_gpio: 5, + pclk_gpio: 7, + data_gpios: [10, 9, 46, 3, 18, 8, 17, 16, 15, 47, 48, 45, 42, 6, 1, 2], + bounce_buffer_size_px: 8000, + pclk_active_neg: true, + fb_in_psram: true +] +``` + +## Region Updates and Raw Drawing + +In addition to full-screen `update`, the following port commands are available on +supported drivers: + +### update_region + +Partially updates a rectangular region of the display without redrawing the entire +screen. Useful for incremental UI updates like progress bars or dynamic text fields +where a full-screen redraw is unnecessary. + +```elixir +# Update only a 200×100 region at (50, 40) +:port.call(display, {:update_region, x, y, width, height, display_list}, 500) +``` + +### draw_rgb565_raw + +Draws raw RGB565 binary pixel data directly to the display. Each pixel is 2 bytes +in little-endian RGB565 format. The binary must contain exactly `width × height × 2` +bytes. + +```elixir +# Draw a 100×100 pre-formatted RGB565 image at (10, 10) +rgb565_binary = <<...>> +:port.call(display, {:draw_rgb565_raw, 10, 10, 100, 100, rgb565_binary}, 5000) +``` + ## Updating the Display Once configured, update the display using the display port: diff --git a/docs/primitives.md b/docs/primitives.md index 3fdadcb..114903e 100644 --- a/docs/primitives.md +++ b/docs/primitives.md @@ -25,6 +25,11 @@ The `transparent` atom indicates that no background is drawn for the item's boun allowing the item to be properly rendered over lower items in the display list. This may have performance implications. +Anti-aliased uFont text with a transparent background is composited against the display list: +partial-alpha glyph edge pixels blend with the resolved colour from the next lower opaque item +rather than against framebuffer memory. This produces smooth anti-aliased text on any +background, provided a solid rectangle exists below it in the display list. + ### Text Text can be provided as either an Erlang string (a list) or an Elixir string (a binary). UTF-8 encoding is supported. From 3db31a15ee5941aab6033e08e5059d47bf43371a Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Wed, 17 Jun 2026 14:01:14 +0200 Subject: [PATCH 6/8] README: List RGB LCD panels in supported hardware Signed-off-by: Ibrahim YILMAZ --- README.Md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.Md b/README.Md index fb3c950..b000647 100644 --- a/README.Md +++ b/README.Md @@ -88,6 +88,8 @@ software dithering * `sharp,memory-lcd`: Sharp Memory LCDs - 400x240, 1-bit monochrome * `solomon-systech,ssd1306`: Solomon Systech SSD1306 - 128x64, 1-bit monochrome * `sino-wealth,sh1106`: Sino Wealth SH1106 - 128x64, 1-bit monochrome +* `esp_lcd,rgb` / `waveshare,esp32-s3-touch-lcd-7`: RGB LCD panels using the ESP32-S3 LCD peripheral, +requires ESP-IDF 5 or later, 16-bit colors (RGB565) [SDL Linux display](sdl_display/) is also supported and can be built as an AtomVM plugin. From dbff78209f013429a7c44bd0e47a48261cfa7307 Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Wed, 17 Jun 2026 14:06:56 +0200 Subject: [PATCH 7/8] docs: Name tested Waveshare ESP32-S3 7-inch RGB Touch LCD Signed-off-by: Ibrahim YILMAZ --- docs/display-drivers.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/display-drivers.md b/docs/display-drivers.md index 019b202..79956a6 100644 --- a/docs/display-drivers.md +++ b/docs/display-drivers.md @@ -326,6 +326,8 @@ These sequences are highly specific to each display model and typically come fro RGB LCD panels driven through the ESP32-S3 RGB LCD peripheral. Requires ESP-IDF 5 or later. +Developed and tested on the **Waveshare ESP32-S3 7-inch RGB Touch LCD** (800×480, 16-bit RGB565 parallel interface). + **Compatible strings:** `"esp_lcd,rgb"` or `"waveshare,esp32-s3-touch-lcd-7"` | Option | Type | Description | Default | From 0f7fc293d79a01592be1474d3719111a94adb2c7 Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Wed, 17 Jun 2026 14:48:27 +0200 Subject: [PATCH 8/8] rgb_lcd: Preserve full framebuffer across region updates and raw draws When update_region, draw_rgb565_raw, or the RLE cover path write only their target region to the work framebuffer and then switch to it, the rest of the screen content is lost because the work FB contains stale data. Fix: copy the full active framebuffer to the work FB before writing the region pixels, so the entire screen state is preserved across all framebuffer switches. Signed-off-by: Ibrahim YILMAZ --- rgb_lcd_display_driver.c | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/rgb_lcd_display_driver.c b/rgb_lcd_display_driver.c index 6571a54..e2ac8bc 100644 --- a/rgb_lcd_display_driver.c +++ b/rgb_lcd_display_driver.c @@ -518,8 +518,16 @@ static void do_update_region(Context *ctx, int x0, int y0, int width, int height if (driver->framebuffer_count > 1) { int work_fb = select_work_framebuffer(driver); - if (work_fb >= 0 - && render_items_to_framebuffer(driver, work_fb, x0, y0, width, height, items, len)) { + if (work_fb >= 0) { + // Copy entire active framebuffer to the work buffer so content + // outside the updated region (e.g. cover image drawn via + // draw_rgb565_raw) is preserved across framebuffer switches. + uint16_t *active_fb = active_framebuffer(driver); + if (active_fb) { + size_t fb_bytes = (size_t) driver->screen.w * (size_t) driver->screen.h * sizeof(uint16_t); + memcpy(driver->framebuffers[work_fb], active_fb, fb_bytes); + } + if (render_items_to_framebuffer(driver, work_fb, x0, y0, width, height, items, len)) { esp_err_t err = switch_to_framebuffer(driver, work_fb); if (err != ESP_OK) { ESP_LOGE(TAG, "region framebuffer switch failed: %s", esp_err_to_name(err)); @@ -710,6 +718,11 @@ static void do_draw_rgb565_rle_base64_scaled( ESP_LOGE(TAG, "cover framebuffer select failed."); return; } + uint16_t *active_fb = active_framebuffer(driver); + if (active_fb) { + size_t fb_bytes = (size_t) driver->screen.w * (size_t) driver->screen.h * sizeof(uint16_t); + memcpy(driver->framebuffers[work_fb], active_fb, fb_bytes); + } copy_rgb565_region_to_framebuffer(driver, work_fb, x, y, draw_width, draw_height, draw_pixels); esp_err_t err = switch_to_framebuffer(driver, work_fb); if (err != ESP_OK) { @@ -827,6 +840,13 @@ static void process_message(Message *message, Context *ctx) ESP_LOGE(TAG, "draw_rgb565_raw: framebuffer select failed."); return; } + // Copy full active framebuffer to work buffer so the rest of + // the screen (info, progress, controls) is preserved. + uint16_t *active_fb = active_framebuffer(driver); + if (active_fb) { + size_t fb_bytes = (size_t) driver->screen.w * (size_t) driver->screen.h * sizeof(uint16_t); + memcpy(driver->framebuffers[work_fb], active_fb, fb_bytes); + } copy_rgb565_region_to_framebuffer(driver, work_fb, x, y, width, height, (const uint16_t *) raw); esp_err_t err = switch_to_framebuffer(driver, work_fb); if (err != ESP_OK) {