From 803ecbf695dad4a76da53245fb43f1a958a08d43 Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Wed, 17 Jun 2026 13:57:15 +0200 Subject: [PATCH 01/18] 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 f9683c9b76efc8dad63ef7fa7734a052b0d9f2a0 Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Wed, 17 Jun 2026 13:58:27 +0200 Subject: [PATCH 02/18] 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 db55a5c8f415951ade8e8b6152b326b9d5218f14 Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Wed, 17 Jun 2026 13:58:53 +0200 Subject: [PATCH 03/18] 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 1bbee11a69ab5c68c87ec9ee341e1efdd94f87d5 Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Wed, 17 Jun 2026 13:59:39 +0200 Subject: [PATCH 04/18] 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 eb279a078dce71d5787cd732514dfd0e87bd1804 Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Wed, 17 Jun 2026 14:00:50 +0200 Subject: [PATCH 05/18] 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 35db83924e44a7ac6e3eb48a3d39785b8d302453 Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Wed, 17 Jun 2026 14:01:14 +0200 Subject: [PATCH 06/18] 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 83ba3b27a2b121269b5031f0bc9fa2aa4ef6ff5e Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Wed, 17 Jun 2026 14:06:56 +0200 Subject: [PATCH 07/18] 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 d08d96be55c6796ff2762d8932a3c54a1e2f1d67 Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Wed, 17 Jun 2026 14:48:27 +0200 Subject: [PATCH 08/18] 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) { From 65f6a51ae72dc0b29d729e823857124781266543 Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Fri, 19 Jun 2026 14:52:27 +0200 Subject: [PATCH 09/18] Fix AtomGL PR review feedback Signed-off-by: Ibrahim YILMAZ --- dcs_lcd_color.h | 12 ++++-------- display_items.c | 5 ++--- display_task.c | 2 ++ docs/display-drivers.md | 8 +++++--- rgb_lcd_display_driver.c | 12 +++++++----- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/dcs_lcd_color.h b/dcs_lcd_color.h index 36dfa56..2f3554f 100644 --- a/dcs_lcd_color.h +++ b/dcs_lcd_color.h @@ -43,20 +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) & 0xFF; - uint8_t g = (color >> 16) & 0xFF; - uint8_t b = (color >> 8) & 0xFF; + uint8_t r = color >> 24; + uint8_t g = color >> 16; + uint8_t b = color >> 8; 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; - - return (((uint16_t) (r >> 3)) << 11) | (((uint16_t) (g >> 2)) << 5) | ((uint16_t) b >> 3); + return rgba8888_color_to_rgb565(color); } static inline uint16_t rgb565_color_to_surface(uint16_t color16) diff --git a/display_items.c b/display_items.c index 1853eea..a8c714a 100644 --- a/display_items.c +++ b/display_items.c @@ -35,9 +35,8 @@ struct Surface int width; int height; void *buffer; - uint32_t fg_color; // RGBA8888 little-endian byte order with the - // alpha byte cleared; ORed with the per-pixel - // alpha in epd_draw_pixel. + uint32_t fg_color; // RGB bytes in 0x00BBGGRR order; alpha is cleared + // so epd_draw_pixel can append per-pixel alpha. }; #define BPP 4 diff --git a/display_task.c b/display_task.c index 05f4755..08b9b21 100644 --- a/display_task.c +++ b/display_task.c @@ -49,6 +49,8 @@ static bool try_pre_ack_render_cmd(Message *message, Context *ctx) 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, "\x16" "draw_rgb565_rle_base64") + && cmd != globalcontext_make_atom(ctx->global, "\x1D" "draw_rgb565_rle_base64_scaled") && cmd != globalcontext_make_atom(ctx->global, "\xB" "draw_buffer")) { return false; diff --git a/docs/display-drivers.md b/docs/display-drivers.md index 79956a6..54cdd91 100644 --- a/docs/display-drivers.md +++ b/docs/display-drivers.md @@ -386,8 +386,9 @@ 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. +screen. This is a compatibility workaround for partial refreshes until a richer +display-list damage tracker is available. It is 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) @@ -398,7 +399,8 @@ where a full-screen redraw is unnecessary. 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. +bytes. This is a low-level direct-buffer path for preformatted RGB565 data; the +regular image tuple API remains the preferred route for encoded image assets. ```elixir # Draw a 100×100 pre-formatted RGB565 image at (10, 10) diff --git a/rgb_lcd_display_driver.c b/rgb_lcd_display_driver.c index e2ac8bc..c075668 100644 --- a/rgb_lcd_display_driver.c +++ b/rgb_lcd_display_driver.c @@ -2,6 +2,7 @@ * This file is part of AtomGL. * * Copyright 2026 AtomGL contributors + * Copyright 2026 Ibrahim YILMAZ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -528,11 +529,12 @@ static void do_update_region(Context *ctx, int x0, int y0, int width, int height 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)); - } else { - mirror_region_from_active_to_inactive_framebuffers(driver, x0, y0, width, height); + 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); From 98c9971e9006777a13f9b2b44312c712f576e7a9 Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Sat, 20 Jun 2026 13:20:28 +0200 Subject: [PATCH 10/18] fix(display_items): correct uFont pixel order and surface alloc epd_draw_pixel wrote 0x00BBGGRR with wrong byte shifts, breaking text color. Guard uFont surface allocation with size_t math to avoid overflow. Signed-off-by: Ibrahim YILMAZ --- display_items.c | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/display_items.c b/display_items.c index a8c714a..0c69ce5 100644 --- a/display_items.c +++ b/display_items.c @@ -23,6 +23,7 @@ #include #include #include +#include #include @@ -59,9 +60,9 @@ 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[0] = (surface->fg_color >> 24) & 0xFFu; - pixel[1] = (surface->fg_color >> 16) & 0xFFu; - pixel[2] = (surface->fg_color >> 8) & 0xFFu; + pixel[0] = surface->fg_color & 0xFFu; + pixel[1] = (surface->fg_color >> 8) & 0xFFu; + pixel[2] = (surface->fg_color >> 16) & 0xFFu; pixel[3] = alpha; } #endif /* ENABLE_UFONT */ @@ -199,14 +200,28 @@ void display_items_init_item(BaseDisplayItem *item, term req, Context *ctx) struct Surface surface; surface.width = rect.width; surface.height = rect.height; - surface.buffer = malloc(rect.width * rect.height * BPP); + if (rect.width <= 0 || rect.height <= 0) { + fprintf(stderr, "invalid ufont surface size (%ix%i)\n", + rect.width, rect.height); + free(text); + return; + } + size_t pixel_count = (size_t) rect.width * (size_t) rect.height; + if (pixel_count > SIZE_MAX / BPP) { + fprintf(stderr, "ufont surface size overflow (%ix%i)\n", + rect.width, rect.height); + free(text); + return; + } + size_t surface_bytes = pixel_count * BPP; + surface.buffer = malloc(surface_bytes); if (!surface.buffer) { fprintf(stderr, "Failed to allocate ufont surface (%ix%i)\n", rect.width, rect.height); free(text); return; } - memset(surface.buffer, 0, rect.width * rect.height * BPP); + memset(surface.buffer, 0, surface_bytes); // Convert Erlang fgcolor (0xRRGGBBAA) to RGBA8888 little- // endian byte order (R in low byte, alpha byte cleared) so // epd_draw_pixel can OR it with the per-pixel alpha. From 58d4d5a06cd9f975a144257941a07ef79bb259d7 Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Sat, 20 Jun 2026 13:20:56 +0200 Subject: [PATCH 11/18] fix(dcs_lcd_draw): validate scaled_cropped_image parameters Reject non-positive scale factors and out-of-range source offsets at parse time and before pointer arithmetic to avoid divide-by-zero and OOB reads. Signed-off-by: Ibrahim YILMAZ --- dcs_lcd_draw.c | 53 +++++++++++++++++++++++++++++++++++++++---------- display_items.c | 15 ++++++++++++++ 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/dcs_lcd_draw.c b/dcs_lcd_draw.c index 9c391ae..b21361d 100644 --- a/dcs_lcd_draw.c +++ b/dcs_lcd_draw.c @@ -86,9 +86,20 @@ static bool dcs_lcd_scaled_image_pixel_rgb565(const struct DCSLCDScreen *screen, { int x = item->x; int y = item->y; + int img_width = item->data.image_data_with_size.width; + int img_height = item->data.image_data_with_size.height; + + if (item->x_scale <= 0 || item->y_scale <= 0 || item->source_x < 0 || item->source_y < 0 + || item->source_x >= img_width || item->source_y >= img_height) { + return false; + } + 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; + if (source_x < 0 || source_y < 0 || source_x >= img_width || source_y >= img_height) { + return false; + } + 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); @@ -334,22 +345,37 @@ int dcs_lcd_draw_scaled_cropped_img_x(const struct DCSLCDScreen *screen, int y_scale = item->y_scale; int x_scale = item->x_scale; int img_width = item->data.image_data_with_size.width; + int img_height = item->data.image_data_with_size.height; - int source_x = item->source_x; - int source_y = item->source_y; - - uint32_t *pixels = ((uint32_t *) data) + (source_y + ((ypos - y) / y_scale)) * img_width + source_x + ((xpos - x) / x_scale); - uint16_t *pixmem16 = (uint16_t *) (((uint8_t *) screen->pixels) + xpos * sizeof(uint16_t)); + if (x_scale <= 0 || y_scale <= 0 || item->source_x < 0 || item->source_y < 0 + || item->source_x >= img_width || item->source_y >= img_height) { + return 0; + } - if (source_x + (width / x_scale) > img_width) { - width = (img_width - source_x) * x_scale; + if (item->source_x + (width / x_scale) > img_width) { + width = (img_width - item->source_x) * x_scale; } if (width > xpos - x + max_line_len) { width = xpos - x + max_line_len; } - for (int j = xpos - x; j < width; j++) { + if (width <= 0) { + return 0; + } + + int rel_y = ypos - y; + int rel_x = xpos - x; + int source_y = item->source_y + (rel_y / y_scale); + int source_x = item->source_x + (rel_x / x_scale); + if (source_y < 0 || source_y >= img_height || source_x < 0 || source_x >= img_width) { + return 0; + } + + uint32_t *pixels = ((uint32_t *) data) + (source_y * img_width) + source_x; + uint16_t *pixmem16 = (uint16_t *) (((uint8_t *) screen->pixels) + xpos * sizeof(uint16_t)); + + for (int j = rel_x; j < width; j++) { uint32_t img_pixel = READ_32_UNALIGNED(pixels); uint8_t alpha = rgba8888_get_alpha(img_pixel); if (alpha == 0xFF) { @@ -369,7 +395,14 @@ int dcs_lcd_draw_scaled_cropped_img_x(const struct DCSLCDScreen *screen, return drawn_pixels; } drawn_pixels++; - pixels = ((uint32_t *) data) + (source_y + ((ypos - y) / y_scale)) * img_width + source_x + ((j + 1) / x_scale); + int next_rel_x = j + 1; + int next_source_x = item->source_x + (next_rel_x / x_scale); + int next_source_y = item->source_y + (rel_y / y_scale); + if (next_source_x < 0 || next_source_x >= img_width + || next_source_y < 0 || next_source_y >= img_height) { + break; + } + pixels = ((uint32_t *) data) + (next_source_y * img_width) + next_source_x; } return drawn_pixels; diff --git a/display_items.c b/display_items.c index 0c69ce5..66e11f4 100644 --- a/display_items.c +++ b/display_items.c @@ -121,6 +121,15 @@ void display_items_init_item(BaseDisplayItem *item, term req, Context *ctx) item->x_scale = term_to_int(term_get_tuple_element(req, 8)); item->y_scale = term_to_int(term_get_tuple_element(req, 9)); + if (item->x_scale <= 0 || item->y_scale <= 0) { + fprintf(stderr, "scaled_cropped_image: scale factors must be > 0\n"); + return; + } + if (item->source_x < 0 || item->source_y < 0) { + fprintf(stderr, "scaled_cropped_image: source offsets must be >= 0\n"); + return; + } + // 10th element is for opts, but right now no opts are supported term img = term_get_tuple_element(req, 11); @@ -137,6 +146,12 @@ void display_items_init_item(BaseDisplayItem *item, term req, Context *ctx) item->data.image_data_with_size.height = term_to_int(term_get_tuple_element(img, 2)); item->data.image_data_with_size.pix = term_binary_data(term_get_tuple_element(img, 3)); + if (item->source_x >= item->data.image_data_with_size.width + || item->source_y >= item->data.image_data_with_size.height) { + fprintf(stderr, "scaled_cropped_image: source offset outside image\n"); + return; + } + } else if (cmd == context_make_atom(ctx, "\x4" "rect")) { item->primitive = PrimitiveRect; From d088dbecae7b4c86e02428444c98bac8ac8f2db2 Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Sat, 20 Jun 2026 13:21:12 +0200 Subject: [PATCH 12/18] fix(rgb_lcd): free driver resources on panel init failure panel_reset and panel_init early returns leaked the scanline buffer and driver struct. Add rgb_lcd_free_driver and call it on those error paths. Signed-off-by: Ibrahim YILMAZ --- rgb_lcd_display_driver.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/rgb_lcd_display_driver.c b/rgb_lcd_display_driver.c index c075668..542c429 100644 --- a/rgb_lcd_display_driver.c +++ b/rgb_lcd_display_driver.c @@ -883,6 +883,20 @@ static void process_message(Message *message, Context *ctx) END_WITH_STACK_HEAP(heap, ctx->global); } +static void rgb_lcd_free_driver(struct RGBLCDDriver *driver, bool delete_panel) +{ + if (!driver) { + return; + } + if (delete_panel && driver->panel) { + esp_lcd_panel_del(driver->panel); + } + if (driver->screen.pixels) { + heap_caps_free(driver->screen.pixels); + } + free(driver); +} + static void display_init(Context *ctx, term opts) { struct RGBLCDDriver *driver = calloc(1, sizeof(struct RGBLCDDriver)); @@ -990,11 +1004,13 @@ static void display_init(Context *ctx, term opts) err = esp_lcd_panel_reset(driver->panel); if (err != ESP_OK) { ESP_LOGE(TAG, "panel reset failed: %s", esp_err_to_name(err)); + rgb_lcd_free_driver(driver, true); return; } err = esp_lcd_panel_init(driver->panel); if (err != ESP_OK) { ESP_LOGE(TAG, "panel init failed: %s", esp_err_to_name(err)); + rgb_lcd_free_driver(driver, true); return; } From edbb2adba9648a926cbf9ee7c07eff75915dcdbc Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Sat, 20 Jun 2026 13:21:31 +0200 Subject: [PATCH 13/18] refactor(rgb_lcd): route buffer draws through draw_buffer with FB sync Remove draw_rgb565_raw port command per maintainer feedback. Reuse its framebuffer logic in draw_rgb565_region and call it from draw_buffer so double-buffered panels stay consistent across region updates. Signed-off-by: Ibrahim YILMAZ --- display_task.c | 1 - rgb_lcd_display_driver.c | 97 +++++++++++++++++----------------------- 2 files changed, 40 insertions(+), 58 deletions(-) diff --git a/display_task.c b/display_task.c index 08b9b21..009d09a 100644 --- a/display_task.c +++ b/display_task.c @@ -48,7 +48,6 @@ static bool try_pre_ack_render_cmd(Message *message, Context *ctx) 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, "\x16" "draw_rgb565_rle_base64") && cmd != globalcontext_make_atom(ctx->global, "\x1D" "draw_rgb565_rle_base64_scaled") && cmd != globalcontext_make_atom(ctx->global, diff --git a/rgb_lcd_display_driver.c b/rgb_lcd_display_driver.c index 542c429..cd2d22a 100644 --- a/rgb_lcd_display_driver.c +++ b/rgb_lcd_display_driver.c @@ -522,7 +522,7 @@ static void do_update_region(Context *ctx, int x0, int y0, int width, int height 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. + // draw_buffer) 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); @@ -753,6 +753,41 @@ static void do_draw_rgb565_rle_base64(Context *ctx, int x, int y, int width, int do_draw_rgb565_rle_base64_scaled(ctx, x, y, width, height, width, height, b64_term); } +static void draw_rgb565_region(struct RGBLCDDriver *driver, int x, int y, int width, int height, const uint16_t *pixels) +{ + if (driver->framebuffer_count > 1) { + maybe_store_fullscreen_background(driver, x, y, width, height, pixels); + int work_fb = select_work_framebuffer(driver); + if (work_fb < 0) { + ESP_LOGE(TAG, "draw_rgb565_region: 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, width, height, pixels); + esp_err_t err = switch_to_framebuffer(driver, work_fb); + if (err != ESP_OK) { + ESP_LOGE(TAG, "draw_rgb565_region: framebuffer switch failed: %s", esp_err_to_name(err)); + return; + } + mirror_region_to_inactive_framebuffers(driver, x, y, width, height, pixels); + } else { + maybe_store_fullscreen_background(driver, x, y, width, height, pixels); + esp_err_t err = esp_lcd_panel_draw_bitmap( + driver->panel, x, y, x + width, y + height, pixels); + if (err != ESP_OK) { + ESP_LOGE(TAG, "draw_rgb565_region: draw_bitmap failed: %s", esp_err_to_name(err)); + return; + } + mirror_region_to_inactive_framebuffers(driver, x, y, width, height, pixels); + } + + ESP_LOGI(TAG, "draw_rgb565_region: %dx%d pixels at (%d,%d)", width, height, x, y); +} + static void process_message(Message *message, Context *ctx) { GenMessage gen_message; @@ -807,67 +842,15 @@ static void process_message(Message *message, Context *ctx) 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); + const uint16_t *data = (const uint16_t *) (addr_low | (addr_high << 16)); - if (!term_is_binary(pixels_term) || width <= 0 || height <= 0 - || x < 0 || y < 0 + if (!data || 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."); + ESP_LOGE(TAG, "Invalid draw_buffer 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 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) { - 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); + draw_rgb565_region(driver, x, y, width, height, data); return; } From 60365e1d87c5b69a5806b3a1adcac369839eec9d Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Sat, 20 Jun 2026 13:23:15 +0200 Subject: [PATCH 14/18] feat(display_items): add rgb565 image tuple format Accept {:rgb565, width, height, binary} alongside rgba8888 in image and scaled_cropped_image tuples. Validate binary size at parse time and render rgb565 pixels in the DCS LCD draw path. Signed-off-by: Ibrahim YILMAZ --- dcs_lcd_draw.c | 60 ++++++++++++++++++++++++++++++++++------- display_items.c | 71 ++++++++++++++++++++++++++++++++++++------------- display_items.h | 1 + 3 files changed, 104 insertions(+), 28 deletions(-) diff --git a/dcs_lcd_draw.c b/dcs_lcd_draw.c index b21361d..b57c41e 100644 --- a/dcs_lcd_draw.c +++ b/dcs_lcd_draw.c @@ -56,6 +56,14 @@ static bool dcs_lcd_image_pixel_rgb565(const struct DCSLCDScreen *screen, int y = item->y; int rel_x = xpos - x; int rel_y = ypos - y; + + if (item->rgb565_pixels) { + const uint16_t *pixels = ((const uint16_t *) item->data.image_data.pix) + + (rel_y * item->width) + rel_x; + *out_color = pixels[0]; + return true; + } + 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); @@ -100,6 +108,13 @@ static bool dcs_lcd_scaled_image_pixel_rgb565(const struct DCSLCDScreen *screen, return false; } + if (item->rgb565_pixels) { + const uint16_t *pixels16 = ((const uint16_t *) item->data.image_data_with_size.pix) + + (source_y * img_width) + source_x; + *out_color = pixels16[0]; + return true; + } + 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); @@ -191,27 +206,37 @@ int dcs_lcd_draw_image_x(const struct DCSLCDScreen *screen, int x = item->x; int y = item->y; - uint16_t bgcolor = 0; - bool visible_bg; - if (item->brcolor != 0) { - bgcolor = display_color_to_rgb565(item->brcolor); - visible_bg = true; - } else { - visible_bg = false; - } - int width = item->width; const char *data = item->data.image_data.pix; int drawn_pixels = 0; - uint32_t *pixels = ((uint32_t *) data) + (ypos - y) * width + (xpos - x); uint16_t *pixmem16 = (uint16_t *) (((uint8_t *) screen->pixels) + xpos * sizeof(uint16_t)); if (width > xpos - x + max_line_len) { width = xpos - x + max_line_len; } + if (item->rgb565_pixels) { + const uint16_t *pixels16 = ((const uint16_t *) data) + (ypos - y) * item->width + (xpos - x); + for (int j = xpos - x; j < width; j++) { + pixmem16[drawn_pixels] = rgb565_color_to_surface(pixels16[j]); + drawn_pixels++; + } + return drawn_pixels; + } + + uint16_t bgcolor = 0; + bool visible_bg; + if (item->brcolor != 0) { + bgcolor = display_color_to_rgb565(item->brcolor); + visible_bg = true; + } else { + visible_bg = false; + } + + uint32_t *pixels = ((uint32_t *) data) + (ypos - y) * width + (xpos - x); + for (int j = xpos - x; j < width; j++) { uint32_t img_pixel = READ_32_UNALIGNED(pixels); uint8_t alpha = rgba8888_get_alpha(img_pixel); @@ -375,6 +400,21 @@ int dcs_lcd_draw_scaled_cropped_img_x(const struct DCSLCDScreen *screen, uint32_t *pixels = ((uint32_t *) data) + (source_y * img_width) + source_x; uint16_t *pixmem16 = (uint16_t *) (((uint8_t *) screen->pixels) + xpos * sizeof(uint16_t)); + if (item->rgb565_pixels) { + const uint16_t *pixels16 = (const uint16_t *) data; + for (int j = rel_x; j < width; j++) { + int sample_x = item->source_x + (j / x_scale); + int sample_y = item->source_y + (rel_y / y_scale); + if (sample_x < 0 || sample_x >= img_width || sample_y < 0 || sample_y >= img_height) { + break; + } + const uint16_t *src = pixels16 + (sample_y * img_width) + sample_x; + pixmem16[drawn_pixels] = rgb565_color_to_surface(src[0]); + drawn_pixels++; + } + return drawn_pixels; + } + for (int j = rel_x; j < width; j++) { uint32_t img_pixel = READ_32_UNALIGNED(pixels); uint8_t alpha = rgba8888_get_alpha(img_pixel); diff --git a/display_items.c b/display_items.c index 66e11f4..7976cb5 100644 --- a/display_items.c +++ b/display_items.c @@ -67,6 +67,45 @@ void epd_draw_pixel(int xpos, int ypos, uint8_t color, void *buffer) } #endif /* ENABLE_UFONT */ +static bool parse_image_tuple(term img, Context *ctx, int *width, int *height, const char **pix, bool *rgb565_pixels) +{ + term format = term_get_tuple_element(img, 0); + *width = term_to_int(term_get_tuple_element(img, 1)); + *height = term_to_int(term_get_tuple_element(img, 2)); + term data_term = term_get_tuple_element(img, 3); + + if (*width <= 0 || *height <= 0) { + fprintf(stderr, "invalid image dimensions: %ix%i\n", *width, *height); + return false; + } + + size_t bytes_per_pixel; + if (format == context_make_atom(ctx, "\x8" + "rgba8888")) { + *rgb565_pixels = false; + bytes_per_pixel = 4; + } else if (format == context_make_atom(ctx, "\x6" + "rgb565")) { + *rgb565_pixels = true; + bytes_per_pixel = 2; + } else { + fprintf(stderr, "unsupported image format: "); + term_display(stderr, format, ctx); + fprintf(stderr, "\n"); + return false; + } + + size_t expected = (size_t) *width * (size_t) *height * bytes_per_pixel; + if (term_binary_size(data_term) < expected) { + fprintf(stderr, "image binary too small (%zu < %zu)\n", + term_binary_size(data_term), expected); + return false; + } + + *pix = term_binary_data(data_term); + return true; +} + void display_items_init_item(BaseDisplayItem *item, term req, Context *ctx) { memset(item, 0, sizeof(*item)); @@ -89,17 +128,15 @@ void display_items_init_item(BaseDisplayItem *item, term req, Context *ctx) term img = term_get_tuple_element(req, 4); - term format = term_get_tuple_element(img, 0); - if (format != context_make_atom(ctx, "\x8" - "rgba8888")) { - fprintf(stderr, "unsupported image format: "); - term_display(stderr, format, ctx); - fprintf(stderr, "\n"); + int width; + int height; + const char *pix; + if (!parse_image_tuple(img, ctx, &width, &height, &pix, &item->rgb565_pixels)) { return; } - item->width = term_to_int(term_get_tuple_element(img, 1)); - item->height = term_to_int(term_get_tuple_element(img, 2)); - item->data.image_data.pix = term_binary_data(term_get_tuple_element(img, 3)); + item->width = width; + item->height = height; + item->data.image_data.pix = pix; } else if (cmd == globalcontext_make_atom(ctx->global, ATOM_STR("\x14", "scaled_cropped_image"))) { item->primitive = PrimitiveScaledCroppedImage; @@ -134,17 +171,15 @@ void display_items_init_item(BaseDisplayItem *item, term req, Context *ctx) term img = term_get_tuple_element(req, 11); - term format = term_get_tuple_element(img, 0); - if (format != globalcontext_make_atom(ctx->global, "\x8" - "rgba8888")) { - fprintf(stderr, "unsupported image format: "); - term_display(stderr, format, ctx); - fprintf(stderr, "\n"); + int img_width; + int img_height; + const char *pix; + if (!parse_image_tuple(img, ctx, &img_width, &img_height, &pix, &item->rgb565_pixels)) { return; } - item->data.image_data_with_size.width = term_to_int(term_get_tuple_element(img, 1)); - item->data.image_data_with_size.height = term_to_int(term_get_tuple_element(img, 2)); - item->data.image_data_with_size.pix = term_binary_data(term_get_tuple_element(img, 3)); + item->data.image_data_with_size.width = img_width; + item->data.image_data_with_size.height = img_height; + item->data.image_data_with_size.pix = pix; if (item->source_x >= item->data.image_data_with_size.width || item->source_y >= item->data.image_data_with_size.height) { diff --git a/display_items.h b/display_items.h index b5d505f..9f2d1fa 100644 --- a/display_items.h +++ b/display_items.h @@ -82,6 +82,7 @@ struct BaseDisplayItem int y_scale; bool owns_data; + bool rgb565_pixels; }; typedef struct BaseDisplayItem BaseDisplayItem; From 171539a29fc488e8756ed36460e0e4866b864d04 Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Sat, 20 Jun 2026 13:23:15 +0200 Subject: [PATCH 15/18] docs: document draw_buffer and rgb565 image tuples Replace draw_rgb565_raw documentation with draw_buffer and describe the rgb565 image tuple format for direct buffer delivery. Signed-off-by: Ibrahim YILMAZ --- docs/display-drivers.md | 15 +++++++-------- docs/primitives.md | 6 ++++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/docs/display-drivers.md b/docs/display-drivers.md index 54cdd91..faa3f29 100644 --- a/docs/display-drivers.md +++ b/docs/display-drivers.md @@ -352,7 +352,7 @@ Developed and tested on the **Waveshare ESP32-S3 7-inch RGB Touch LCD** (800×48 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. +commands, plus `draw_buffer` for direct RGB565 buffer delivery from image tuples. **Example:** ```elixir @@ -395,17 +395,16 @@ like progress bars or dynamic text fields where a full-screen redraw is unnecess :port.call(display, {:update_region, x, y, width, height, display_list}, 500) ``` -### draw_rgb565_raw +### draw_buffer -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. This is a low-level direct-buffer path for preformatted RGB565 data; the -regular image tuple API remains the preferred route for encoded image assets. +Draws a preformatted RGB565 buffer already resident in memory. Each pixel is 2 bytes +in little-endian RGB565 format. The buffer address is passed as two 32-bit integers +(low and high halves). Use with image tuples such as `{:rgb565, width, height, binary}` +after loading the buffer into device memory. ```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) +:port.call(display, {:draw_buffer, 10, 10, 100, 100, addr_low, addr_high}, 5000) ``` ## Updating the Display diff --git a/docs/primitives.md b/docs/primitives.md index 114903e..3519f66 100644 --- a/docs/primitives.md +++ b/docs/primitives.md @@ -101,6 +101,12 @@ The format tag indicates the pixel format. For example, `rgba8888` means: - Byte order: R, G, B, A - Each component is 8 bits +`rgb565` means: +- 16-bit RGB565 pixels +- Byte order: little-endian +- 2 bytes per pixel (`width × height × 2` bytes total) +- Use with `draw_buffer` for direct framebuffer delivery on RGB LCD drivers + **Important:** Width and height must exactly match the dimensions of the image data in the binary. Incorrect values will result in corrupted image display. From 21eea4a3888269490e9adba7871e3deb153951c1 Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Sat, 20 Jun 2026 13:55:44 +0200 Subject: [PATCH 16/18] fix(rgb_lcd): device-proven compositor, uFont color, and draw_buffer Blend transparent RGBA pixels over restored background with optional lower-layer resolve instead of stalling on alpha==0. Restore RRGGBBAA uFont fgcolor layout. Accept draw_buffer binary tuples and persist partial cover draws into background_buffer. Signed-off-by: Ibrahim YILMAZ --- dcs_lcd_draw.c | 18 +++++++------- display_items.c | 20 ++++++---------- rgb_lcd_display_driver.c | 52 +++++++++++++++++++++++++++++++++------- 3 files changed, 61 insertions(+), 29 deletions(-) diff --git a/dcs_lcd_draw.c b/dcs_lcd_draw.c index b57c41e..f62b5cc 100644 --- a/dcs_lcd_draw.c +++ b/dcs_lcd_draw.c @@ -249,12 +249,13 @@ int dcs_lcd_draw_image_x(const struct DCSLCDScreen *screen, 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 lower = rgb565_color_to_surface(pixmem16[drawn_pixels]); + uint16_t resolved = 0; + if (dcs_lcd_resolve_pixel_rgb565(screen, xpos + drawn_pixels, ypos, items, items_len, item_index + 1, &resolved)) { + lower = resolved; + } uint16_t blended = alpha_blend_rgb565(color, lower, alpha); pixmem16[drawn_pixels] = rgb565_color_to_surface(blended); - } else { - return drawn_pixels; } drawn_pixels++; pixels++; @@ -427,12 +428,13 @@ int dcs_lcd_draw_scaled_cropped_img_x(const struct DCSLCDScreen *screen, 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 lower = rgb565_color_to_surface(pixmem16[drawn_pixels]); + uint16_t resolved = 0; + if (dcs_lcd_resolve_pixel_rgb565(screen, xpos + drawn_pixels, ypos, items, items_len, item_index + 1, &resolved)) { + lower = resolved; + } uint16_t blended = alpha_blend_rgb565(color, lower, alpha); pixmem16[drawn_pixels] = rgb565_color_to_surface(blended); - } else { - return drawn_pixels; } drawn_pixels++; int next_rel_x = j + 1; diff --git a/display_items.c b/display_items.c index 7976cb5..ae77eee 100644 --- a/display_items.c +++ b/display_items.c @@ -36,8 +36,7 @@ struct Surface int width; int height; void *buffer; - uint32_t fg_color; // RGB bytes in 0x00BBGGRR order; alpha is cleared - // so epd_draw_pixel can append per-pixel alpha. + uint32_t fg_color; // 0xRRGGBBAA from Erlang color << 8 | 0xFF }; #define BPP 4 @@ -60,9 +59,9 @@ 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[0] = surface->fg_color & 0xFFu; - pixel[1] = (surface->fg_color >> 8) & 0xFFu; - pixel[2] = (surface->fg_color >> 16) & 0xFFu; + 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 */ @@ -97,8 +96,8 @@ static bool parse_image_tuple(term img, Context *ctx, int *width, int *height, c size_t expected = (size_t) *width * (size_t) *height * bytes_per_pixel; if (term_binary_size(data_term) < expected) { - fprintf(stderr, "image binary too small (%zu < %zu)\n", - term_binary_size(data_term), expected); + fprintf(stderr, "image binary too small (%lu < %zu)\n", + (unsigned long) term_binary_size(data_term), expected); return false; } @@ -272,12 +271,7 @@ void display_items_init_item(BaseDisplayItem *item, term req, Context *ctx) return; } memset(surface.buffer, 0, surface_bytes); - // Convert Erlang fgcolor (0xRRGGBBAA) to RGBA8888 little- - // endian byte order (R in low byte, alpha byte cleared) so - // epd_draw_pixel can OR it with the per-pixel alpha. - surface.fg_color = ((fgcolor >> 24) & 0xFFu) - | (((fgcolor >> 16) & 0xFFu) << 8) - | (((fgcolor >> 8) & 0xFFu) << 16); + surface.fg_color = fgcolor; int text_x = 0; int text_y = loaded_font->ascender; enum EpdDrawError res = epd_write_default(loaded_font, text, &text_x, &text_y, &surface); diff --git a/rgb_lcd_display_driver.c b/rgb_lcd_display_driver.c index cd2d22a..ace2e90 100644 --- a/rgb_lcd_display_driver.c +++ b/rgb_lcd_display_driver.c @@ -753,6 +753,24 @@ static void do_draw_rgb565_rle_base64(Context *ctx, int x, int y, int width, int do_draw_rgb565_rle_base64_scaled(ctx, x, y, width, height, width, height, b64_term); } +static void maybe_store_cover_background( + struct RGBLCDDriver *driver, int x, int y, int width, int height, const uint16_t *pixels) +{ + if (!driver->background_buffer || x < 0 || y < 0 || width <= 0 || height <= 0) { + return; + } + if (x + width > driver->screen.w || y + height > driver->screen.h) { + return; + } + + for (int row = 0; row < height; row++) { + memcpy( + driver->background_buffer + ((size_t) (y + row) * driver->screen.w) + x, + pixels + ((size_t) row * width), + (size_t) width * sizeof(uint16_t)); + } +} + static void draw_rgb565_region(struct RGBLCDDriver *driver, int x, int y, int width, int height, const uint16_t *pixels) { if (driver->framebuffer_count > 1) { @@ -785,6 +803,7 @@ static void draw_rgb565_region(struct RGBLCDDriver *driver, int x, int y, int wi mirror_region_to_inactive_framebuffers(driver, x, y, width, height, pixels); } + maybe_store_cover_background(driver, x, y, width, height, pixels); ESP_LOGI(TAG, "draw_rgb565_region: %dx%d pixels at (%d,%d)", width, height, x, y); } @@ -840,14 +859,31 @@ static void process_message(Message *message, Context *ctx) 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 uint16_t *data = (const uint16_t *) (addr_low | (addr_high << 16)); - - if (!data || width <= 0 || height <= 0 || x < 0 || y < 0 - || x + width > driver->screen.w || y + height > driver->screen.h) { - ESP_LOGE(TAG, "Invalid draw_buffer arguments."); - return; + term payload = term_get_tuple_element(req, 5); + const uint16_t *data = NULL; + + if (term_is_binary(payload)) { + size_t expected = (size_t) width * (size_t) height * 2; + if (width <= 0 || height <= 0 || x < 0 || y < 0 + || x + width > driver->screen.w || y + height > driver->screen.h + || term_binary_size(payload) < expected) { + ESP_LOGE(TAG, "Invalid draw_buffer binary arguments."); + return; + } + data = (const uint16_t *) term_binary_data(payload); + } else { + if (term_get_tuple_arity(req) < 7) { + ESP_LOGE(TAG, "Invalid draw_buffer pointer arguments."); + return; + } + unsigned long addr_low = term_to_int(payload); + unsigned long addr_high = term_to_int(term_get_tuple_element(req, 6)); + data = (const uint16_t *) (addr_low | (addr_high << 16)); + if (!data || width <= 0 || height <= 0 || x < 0 || y < 0 + || x + width > driver->screen.w || y + height > driver->screen.h) { + ESP_LOGE(TAG, "Invalid draw_buffer pointer arguments."); + return; + } } draw_rgb565_region(driver, x, y, width, height, data); From 9818a83f925d34b7d63e73ca666934128a547752 Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Sat, 20 Jun 2026 15:11:08 +0200 Subject: [PATCH 17/18] fix(rgb_lcd): use full image width as stride in transparent image draw dcs_lcd_draw_image_x computed the RGBA source row pointer using the clamped line width instead of item->width. When an RGBA image is drawn partially off-screen (e.g. a scrolling marquee title), the stride is too small and every row after the first reads from the wrong offset, showing garbage pixels. Use item->width for the stride, matching the rgb565 path. Signed-off-by: Ibrahim YILMAZ --- dcs_lcd_draw.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dcs_lcd_draw.c b/dcs_lcd_draw.c index f62b5cc..cf13be6 100644 --- a/dcs_lcd_draw.c +++ b/dcs_lcd_draw.c @@ -235,7 +235,7 @@ int dcs_lcd_draw_image_x(const struct DCSLCDScreen *screen, visible_bg = false; } - uint32_t *pixels = ((uint32_t *) data) + (ypos - y) * width + (xpos - x); + uint32_t *pixels = ((uint32_t *) data) + (ypos - y) * item->width + (xpos - x); for (int j = xpos - x; j < width; j++) { uint32_t img_pixel = READ_32_UNALIGNED(pixels); From 403ab6fd332dbd4a003b8c85e9f414fd91cb78bc Mon Sep 17 00:00:00 2001 From: Ibrahim YILMAZ Date: Sun, 21 Jun 2026 17:40:06 +0200 Subject: [PATCH 18/18] fix(esp32): gate RGB LCD driver to ESP32-S3 and add port helpers Skip rgb_lcd_display_driver on non-S3 targets so AtomVM esp32 CI builds pass without esp_lcd_panel_rgb.h. Add boot framebuffer fill, measure_text port command, and document measure_text in display-drivers.md. Signed-off-by: Ibrahim YILMAZ --- CMakeLists.txt | 2 +- display_driver.c | 6 ++-- display_task.c | 69 +++++++++++++++++++++++++++++++++++++++- docs/display-drivers.md | 10 ++++++ rgb_lcd_display_driver.c | 8 ++++- 5 files changed, 90 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 311244d..d28b439 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,7 @@ # WHOLE_ARCHIVE option is supported only with esp-idf 5.x # A link option will be used with esp-idf 4.x -if (IDF_VERSION_MAJOR GREATER_EQUAL 5) +if (IDF_VERSION_MAJOR GREATER_EQUAL 5 AND IDF_TARGET STREQUAL "esp32s3") set(OPTIONAL_WHOLE_ARCHIVE WHOLE_ARCHIVE) set(OPTIONAL_ESP_LCD_REQUIRES "esp_lcd") set(OPTIONAL_RGB_LCD_SRCS "rgb_lcd_display_driver.c") diff --git a/display_driver.c b/display_driver.c index af2a9f7..08dca1c 100644 --- a/display_driver.c +++ b/display_driver.c @@ -20,6 +20,8 @@ #include +#include + #include #include @@ -34,7 +36,7 @@ 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) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) && CONFIG_IDF_TARGET_ESP32S3 Context *rgb_lcd_display_create_port(GlobalContext *global, term opts); #endif @@ -58,7 +60,7 @@ 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) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) && CONFIG_IDF_TARGET_ESP32S3 } 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); diff --git a/display_task.c b/display_task.c index 009d09a..8e7af82 100644 --- a/display_task.c +++ b/display_task.c @@ -144,6 +144,72 @@ static bool try_handle_register_font(Message *message, Context *ctx) return true; } +static bool try_handle_measure_text(Message *message, Context *ctx) +{ + GenMessage gen_message; + if (UNLIKELY(port_parse_gen_message(message->message, + &gen_message) != GenCallMessage)) { + return false; + } + + term req = gen_message.req; + if (UNLIKELY(!term_is_tuple(req) || term_get_tuple_arity(req) < 3)) { + return false; + } + term cmd = term_get_tuple_element(req, 0); + + if (cmd != globalcontext_make_atom(ctx->global, + "\xC" "measure_text")) { + return false; + } + + char *handle = interop_atom_to_string(ctx, + term_get_tuple_element(req, 1)); + EpdFont *loaded_font = NULL; + if (handle != NULL) { + loaded_font = ufont_manager_find_by_handle(ufont_manager, handle); + free(handle); + } + + term text_bin = term_get_tuple_element(req, 2); + size_t text_len = term_binary_size(text_bin); + char *text = malloc(text_len + 1); + if (text == NULL) { + 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, ERROR_ATOM); + display_message_send(gen_message.pid, return_tuple, ctx->global); + END_WITH_STACK_HEAP(heap, ctx->global); + return true; + } + memcpy(text, term_binary_data(text_bin), text_len); + text[text_len] = '\0'; + + int width = 0; + int height = 0; + if (loaded_font != NULL) { + EpdFontProperties props = epd_font_properties_default(); + EpdRect rect = epd_get_string_rect(loaded_font, text, 0, 0, 0, &props); + width = rect.width; + height = rect.height; + } + free(text); + + BEGIN_WITH_STACK_HEAP(TUPLE_SIZE(3) + TUPLE_SIZE(2) + REF_SIZE, heap); + term result = term_alloc_tuple(3, &heap); + term_put_tuple_element(result, 0, OK_ATOM); + term_put_tuple_element(result, 1, term_from_int(width)); + term_put_tuple_element(result, 2, term_from_int(height)); + 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, result); + display_message_send(gen_message.pid, return_tuple, ctx->global); + END_WITH_STACK_HEAP(heap, ctx->global); + + return true; +} + void display_task_process_messages(void *arg) { struct DisplayTaskArgs *args = arg; @@ -154,7 +220,8 @@ void display_task_process_messages(void *arg) Message *message; xQueueReceive(args->messages_queue, &message, portMAX_DELAY); - if (!try_handle_register_font(message, args->ctx)) { + if (!try_handle_register_font(message, args->ctx) + && !try_handle_measure_text(message, args->ctx)) { args->process_message_fn(message, args->ctx); } diff --git a/docs/display-drivers.md b/docs/display-drivers.md index faa3f29..ad3c371 100644 --- a/docs/display-drivers.md +++ b/docs/display-drivers.md @@ -395,6 +395,16 @@ like progress bars or dynamic text fields where a full-screen redraw is unnecess :port.call(display, {:update_region, x, y, width, height, display_list}, 500) ``` +### measure_text + +Returns the pixel width and height of a text string for a registered uFont handle. +Use this to size marquee regions or layout before building a display list. + +```elixir +# {:ok, width, height} or {:error, reason} +:port.call(display, {:measure_text, :default16px, "Hello"}, 500) +``` + ### draw_buffer Draws a preformatted RGB565 buffer already resident in memory. Each pixel is 2 bytes diff --git a/rgb_lcd_display_driver.c b/rgb_lcd_display_driver.c index ace2e90..95f2a82 100644 --- a/rgb_lcd_display_driver.c +++ b/rgb_lcd_display_driver.c @@ -49,6 +49,9 @@ static const char *TAG = "rgb_lcd_display_driver"; +/* Matches display_server COLOR_BG (16#1A1A); solid fill before first scanout. */ +#define RGB565_BOOT_FILL_BYTE 0x1A + struct RGBLCDDriver { esp_lcd_panel_handle_t panel; @@ -1037,10 +1040,13 @@ static void display_init(Context *ctx, term opts) void *fb1 = NULL; err = esp_lcd_rgb_panel_get_frame_buffer(driver->panel, 2, &fb0, &fb1); if (err == ESP_OK && fb0 && fb1) { + size_t fb_bytes = (size_t) width * (size_t) height * sizeof(uint16_t); + memset(fb0, RGB565_BOOT_FILL_BYTE, fb_bytes); + memset(fb1, RGB565_BOOT_FILL_BYTE, fb_bytes); driver->framebuffers[0] = fb0; driver->framebuffers[1] = fb1; driver->framebuffer_count = 2; - ESP_LOGI(TAG, "Using RGB LCD double framebuffer: %p %p", fb0, fb1); + ESP_LOGI(TAG, "Using RGB LCD double framebuffer: %p %p (boot fill)", fb0, fb1); } else { ESP_LOGW(TAG, "RGB LCD multi-framebuffer unavailable, using draw_bitmap path: %s", esp_err_to_name(err)); }