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/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. 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..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; @@ -55,7 +183,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; @@ -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; } @@ -98,7 +232,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; @@ -117,15 +251,16 @@ 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; - 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; @@ -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; @@ -180,7 +320,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; @@ -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); 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/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; diff --git a/docs/display-drivers.md b/docs/display-drivers.md index f48c3cb..79956a6 100644 --- a/docs/display-drivers.md +++ b/docs/display-drivers.md @@ -322,6 +322,90 @@ 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. + +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 | +|--------|------|-------------|---------| +| `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. diff --git a/rgb_lcd_display_driver.c b/rgb_lcd_display_driver.c new file mode 100644 index 0000000..e2ac8bc --- /dev/null +++ b/rgb_lcd_display_driver.c @@ -0,0 +1,1026 @@ +/* + * 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) { + // 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)); + } 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; + } + 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) { + 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 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); + 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; +}