Refactor#12
Conversation
libAtomVM/utils.h already provides UNUSED. Signed-off-by: Davide Bettio <davide@uninstall.it>
globalcontext_atomstring_from_term() was removed in libAtomVM 0.7. Signed-off-by: Davide Bettio <davide@uninstall.it>
font.c was previously included verbatim by every driver via array in the ESP32 build (and one more in the SDL build). Refactor into a proper compiled module so the data is linked once. font.c is renamed to font_data.c with the static qualifier removed on the array. A new font_data.h declares the array as extern and exposes FONTDATAMAX. All previous consumers (6 ESP32 drivers + the SDL display.c) now include font_data.h instead of font.c, and both CMakeLists.txt entries are updated to compile font_data.c. Signed-off-by: Davide Bettio <davide@uninstall.it>
display_items.h was previously included by every driver via init_item() and destroy_items() function bodies in the ESP32 build (and one more in the SDL build). Refactor into a proper compiled module so the bodies are linked once. Signed-off-by: Davide Bettio <davide@uninstall.it>
Extract the send_message helper (identical across 6 ESP32 drivers and message_helpers.h) into a shared display_message module. Also remove PendingReply, a vestigial struct defined in 6 files but never instantiated or referenced. Signed-off-by: Davide Bettio <davide@uninstall.it>
Extract the FreeRTOS queue, consume_mailbox callback, and process_messages task into a shared display_task module. All seven ESP32 drivers now embed struct DisplayTaskArgs and use the shared dispatch infrastructure via CONTAINER_OF. The consume_mailbox implementation adopts the ili948x variant (non-blocking enqueue with drop-oldest-on-overflow and proper disposal of dropped messages). Also removes unused display_enqueue_message from ili934x and stale mailbox.h include from display_driver.h. Signed-off-by: Davide Bettio <davide@uninstall.it>
Signed-off-by: Davide Bettio <davide@uninstall.it>
Extract writedata, writecommand, and writecmddata from the DCS LCD drivers into a shared module using struct SPIDCBus. Migrate st7789, ili934x, and ili948x to embed the bus sub-struct. Signed-off-by: Davide Bettio <davide@uninstall.it>
Extract alpha_blend_rgb565, rgba8888_color_to_rgb565, and related color helpers into a shared inline-header module. Drop the unused struct Screen parameter. Migrate st7789, ili934x, and ili948x. Signed-off-by: Davide Bettio <davide@uninstall.it>
Unify the per-driver struct Screen into a shared DCSLCDScreen that is the superset of all three: st7789's x_offset/y_offset, ili948x's bytes/bytes_out for RGB888, and the common w/h and pixel buffers. Migrate st7789, ili934x, and ili948x. Signed-off-by: Davide Bettio <davide@uninstall.it>
Extract set_paint_area (with offset support from st7789) and draw_buffer (with RGB888 path from ili948x) into a shared module. Consolidate generic MIPI DCS command macros under a DCS_LCD_ prefix. Zero-init DCSLCDScreen allocation for safe access to uninitialized offset fields. Migrate st7789, ili934x, and ili948x. Signed-off-by: Davide Bettio <davide@uninstall.it>
|
per AMP: PR Review — Branch
|
| File | Line(s) | Allocation |
|---|---|---|
dcs_lcd_display_driver.c |
312, 318-319, 321-322 | calloc(driver), heap_caps_malloc(pixels/pixels_out), heap_caps_malloc(bytes/bytes_out) |
dcs_lcd_commands.c |
64, 88 | heap_caps_malloc(tmpbuf) for DMA transfer |
epaper_display_driver.c |
205, 295, 345 | heap_caps_malloc(buf), malloc(driver) |
oled_display_driver.c |
~118 | malloc(buf) in do_update — leaks items on early return |
On ESP32 with limited heap, heap_caps_malloc(MALLOC_CAP_DMA) can realistically fail.
🟢 P2 — oled_display_driver.c Leaks buf and items on I2C Failure
File: oled_display_driver.c (do_update), around line 124
if (i2c_driver_acquire(driver->i2c_host, &i2c_num, ctx->global) != I2CAcquireOk) {
fprintf(stderr, "Invalid I2C peripheral\n");
return; // leaks: buf, items (and items' owned data)
}Fix:
if (i2c_driver_acquire(driver->i2c_host, &i2c_num, ctx->global) != I2CAcquireOk) {
fprintf(stderr, "Invalid I2C peripheral\n");
+ free(buf);
+ display_items_delete(items, len);
return;
}🟢 P2 — dcs_lcd_draw.c:140 — Misleading Variable Name
uint16_t *pixmem32 = (uint16_t *) ...; // named "32" but is actually 16-bitCarried over from the old st7789_display_driver.c. Trivial rename:
- uint16_t *pixmem32 = (uint16_t *) (((uint8_t *) screen->pixels) + xpos * sizeof(uint16_t));
+ uint16_t *pixmem16 = (uint16_t *) (((uint8_t *) screen->pixels) + xpos * sizeof(uint16_t));(And update the two references at lines 162, 164.)
🟢 P2 — uFont Parser Minimum Size Guard
File: ufontlib.c:602
ufont_parse() reads at offsets 0 and 8 without checking buf_size >= 12:
EpdFont *ufont_parse(const void *iff_binary, int buf_size)
{
+ if (!iff_binary || buf_size < 12) {
+ return NULL;
+ }
if (!ufont_iff_is_valid_ufl(iff_binary)) {
return NULL;
}Design Notes (Non-blocking)
find_max_line_len receives i not items_len — intentional
In all draw modules (dcs_lcd_draw.c:243, epaper_draw.c:308, mono_draw.c:333), the call passes i (current item index) as the length argument:
int max_line_len = below ? 1 : dcs_lcd_find_max_line_len(screen, items, i, xpos, ypos);This is intentional: it scans only items with lower index (higher z-order / painted first) to determine the maximum contiguous pixel run before hitting an overlapping item. The parameter name items_len in the function signature is somewhat misleading but the logic is correct and matches the SDL reference driver.
Duplicate compatible-string tables
display_driver.c duplicates the family-specific compatible tables (dcs_lcd_compat_table, oled_compat_table, epaper_compat_table) as an if/else chain. This creates a maintenance burden — adding a new controller requires editing two places. A future improvement could be a single registration table, but this is fine for now.
context_make_atom helper
display_items.h:31-34 keeps a // TODO: deprecated helper wrapper. Consider removing it and inlining globalcontext_make_atom at call sites as a follow-up.
Summary of Recommendations
| Priority | Issue | Effort |
|---|---|---|
| 🔴 P0 | uFont global state / use-after-free | Medium |
| 🔴 P0 | Uninitialized display items on error paths | Small |
| 🟡 P1 | Silent message loss (queue overflow) | Small |
| 🟡 P1 | Port creation succeeds despite init failure | Medium |
| 🟡 P1 | Pixel write off-by-one (> → >=) |
Trivial |
| 🟢 P2 | Typo comptaible |
Trivial |
| 🟢 P2 | Missing malloc NULL checks | Small |
| 🟢 P2 | OLED do_update leaks on I2C failure | Trivial |
| 🟢 P2 | Misleading pixmem32 name |
Trivial |
| 🟢 P2 | uFont parser min-size guard | Trivial |
Extract the per-pixel scanline renderer (draw_x dispatcher and five draw_*_x helpers) into a shared module. All functions take const struct DCSLCDScreen *screen as their first parameter. Migrate st7789, ili934x, and ili948x. Signed-off-by: Davide Bettio <davide@uninstall.it>
Add driver for the Good Display GDEP073E01 7.3-inch ACeP panel (800x480, 7-color e-ink). Signed-off-by: Davide Bettio <davide@uninstall.it>
Add epaper_screen (shared type), epaper_color (palette- parameterized ACeP 7-color dithering), and epaper_draw (per-pixel draw pipeline). Extend spi_dc_driver with spi_dc_writedatan for variable-width writes. Migrate 5in65_acep_7c and gdep073e01 drivers. Signed-off-by: Davide Bettio <davide@uninstall.it>
Extract the monochrome draw_x dispatcher, five draw_*_x helpers, and Bayer 4x4 dithering into a shared module with struct MonoScreen. Migrate memory_display and ssd1306 drivers. Signed-off-by: Davide Bettio <davide@uninstall.it>
The pixels pointer update at the bottom of the inner loop used j / x_scale, but j is the current iteration's index. After the for-loop increments j, pixels still pointed at the old source column. Use (j + 1) / x_scale so the pointer is correct for the next iteration. Fixes a leftmost-column artifact visible on 4x-scaled images. Signed-off-by: Davide Bettio <davide@uninstall.it>
Both e-paper drivers allocated a DMA scanline buffer with heap_caps_malloc but never freed it. Add the missing free(buf) after spi_device_release_bus in each function. Signed-off-by: Davide Bettio <davide@uninstall.it>
Replace the placeholder pure-primary RGB values with real-world measurements taken from the physical panel. The calibrated values produce more accurate nearest-color matching during dithering. Signed-off-by: Davide Bettio <davide@uninstall.it>
Both headers have zero includers after the monochrome draw functions were extracted into mono_draw. The shared per-pixel draw pipeline and Bayer dither now live in mono_draw.c/h. Signed-off-by: Davide Bettio <davide@uninstall.it>
Move ufontlib from sdl_display/ to the component root. Fix the inverted magic check in ufont_parse and accept standard IFF container layout (FORM + size + uFL0 form type). Wire up register_font as a pre-dispatch handler in the shared display task, ufont_manager initialization, and the epd_draw_pixel rendering callback. Honor FgColor for anti-aliased text. Signed-off-by: Davide Bettio <davide@uninstall.it>
Apply lower_snake_case to SPI and SPI+DC helpers, add module prefixes to display_items, display_task, and display_message public functions, fix word boundaries in controller-specific init function names, convert enum values to PascalCase, and make file-local helpers static. Signed-off-by: Davide Bettio <davide@uninstall.it>
Add a Style Exception comment explaining that FONTDATAMAX and fontdata keep their original Linux kernel names (from lib/fonts/font_8x16.c) to simplify comparison with upstream. Per C_CODING_STYLE.md "Style Exception Documentation" section. No code changes. Signed-off-by: Davide Bettio <davide@uninstall.it>
Convert data_len in spi_display_dma_write/spi_display_write and items_len in display_items_delete/find_max_line_len/draw_x from int to size_t. Rename spi_data parameter to spi_disp. Signed-off-by: Davide Bettio <davide@uninstall.it>
The SDL build compiled ufontlib.c but never defined ENABLE_UFONT, so the rendering path in display_items.c was compiled out. The SDL driver also carried an outdated duplicate of struct Surface and epd_draw_pixel that was missing the fg_color field and hardcoding black. Remove the ESP_PLATFORM guard from the shared epd_draw_pixel in display_items.c, remove the stale SDL-local copy, and add -DENABLE_UFONT to the SDL CMake build. Signed-off-by: Davide Bettio <davide@uninstall.it>
The ufont rendering path hardcoded brcolor to 0 (transparent) when storing the pre-rendered glyph surface as a PrimitiveImage. This caused the scanline draw_image_x to early-return on the first transparent pixel, making the text invisible unless every pixel in the glyph bounding box had non-zero alpha. Use the background color from the display list tuple instead. Signed-off-by: Davide Bettio <davide@uninstall.it>
struct SPI was the shared state struct across all drivers. After the family split, the generic name no longer described the shape of any given driver. The SSD1306 family also drove it further out of truth: the driver uses I2C, not SPI. Rename to family-specific types: - DCS LCD drivers (ili934x, ili948x, st7789) -> DCSLCDDriver - E-paper drivers (5in65_acep_7c, gdep073e01) -> EpaperDriver - SSD1306 family -> OLEDDriver - memory_lcd -> MemoryLCDDriver Rename the matching FROM_CTX macros and the local `spi` variable to `driver`. Signed-off-by: Davide Bettio <davide@uninstall.it>
The built-in font width constant was duplicated in four draw modules. Move it next to FONTDATAMAX in font_data.h where it belongs with the font geometry. Signed-off-by: Davide Bettio <davide@uninstall.it>
The ufont text rendering path allocates a surface buffer proportional to the bounding box. On ESP32 this can exceed available heap for large text, and the missing NULL check crashes in memset. Signed-off-by: Davide Bettio <davide@uninstall.it>
Define a compact byte-array format for controller init sequences and an interpreter (dcs_lcd_execute_init_seq) that walks the table sending commands via spi_dc_write_cmd_data. Transcribe all 6 existing display_init_*() functions into const byte arrays with _Static_assert on each array size to catch any miscount at compile time. Arrays: ILI9341, ILI9342C, ILI9486, ILI9488, ST7789 std, ST7789 alt gamma 2. No caller changes yet — the old static init functions are still in use. Signed-off-by: Davide Bettio <davide@uninstall.it>
Move DCSLCDScreen from a file-static pointer into the DCSLCDDriver struct across all three DCS LCD drivers (ili934x, ili948x, st7789). Removes one global, one heap allocation, and one pointer indirection per driver. Calloc replaces malloc so the embedded screen's unused fields (offsets, bytes for non-9488) are zero-initialised. Also drop the explicit SWRESET, SLPOUT and DISPON commands each driver issued manually; they are now part of the controller's init sequence table. For st7789 this also drops the no-reset-GPIO SWRESET branch. Signed-off-by: Davide Bettio <davide@uninstall.it>
git mv st7789_display_driver.c -> dcs_lcd_display_driver.c; rename the create_port entry point and the log TAG accordingly. Pure rename -- sitronix,st7789 and sitronix,st7796 dispatch through the new function; ILI compatibles still route through their legacy drivers. Signed-off-by: Davide Bettio <davide@uninstall.it>
Generalise the driver into a single descriptor-driven implementation that can run any of the six DCS LCD controllers covered by the DCSLCDDesc table. A compatible-string lookup picks the descriptor; init sequence, MADCTL bytes, default geometry, default colour order, SPI clock and pixel size come from it. Hardware-touching code becomes uniform across controllers: MADCTL slots feed set_rotation (with 0xFF rejecting an unsupported rotation), pixel_bytes selects between RGB565 and RGB888 scanline paths, and the boot flow always emits an explicit INVON/INVOFF. init_list, init_seq_type, width/height, x_offset/y_offset, and color_order now apply to every controller (silently fixing the hardcoded-geometry FIXME the ILI934x driver carried). Signed-off-by: Davide Bettio <davide@uninstall.it>
Route the four ILI compatibles through the unified dcs_lcd_display_create_port and drop the now-orphaned ili934x and ili948x driver files. All six DCS LCD compatibles (ilitek,ili9341/ili9342c/ili9486/ili9488 and sitronix,st7789/st7796) are served by a single descriptor-driven driver, with x_offset/y_offset, color_order, init_list, width/height and an optional reset GPIO available uniformly. Signed-off-by: Davide Bettio <davide@uninstall.it>
The two e-paper drivers currently open-code their init sequences as long chains of spi_dc_write_command/data calls, with BUSY-polling (gdep073e01) or an embedded delay (acep 5.65") interleaved between commands. Each additional panel would add another ~30-60 lines in the same shape. Extract the sequences to declarative byte arrays in the format [CMD][FLAGS_LEN][DATA...][optional DELAY_MS], paired with a size_t length constant. The interpreter takes an extra wait_busy_between_cmds bool to cover the gdep073e01 convention without bloating the per-row format. _Static_assert on array sizes catches miscounted data bytes at build time. Signed-off-by: Davide Bettio <davide@uninstall.it>
Replace the per-panel chains of spi_dc_write_command/write_data calls (acep: 35 lines with an inline vTaskDelay; gdep073e01: 14 steps each followed by wait_busy_level) with a single call to epaper_execute_init_seq for each panel. The byte-array tables come from epaper_commands.c. For gdep073e01 the executor polls BUSY between commands via wait_busy_between_cmds=true. For acep the 100 ms delay between VDCS (0x82) and the second CDI (0x50) is encoded in the table via the delay flag. Bytes-on-the-wire are unchanged. Drop the now-unused gdep073e01 command-byte defines (PSR, PWRR, POF, POFS, PON, BTST1/2/3, DTM, DRF, PLL, CDI, TCON, TRES, REV, VDCS, T_VDCS, PWS, DSLP); the frame-protocol sites use raw hex to match the acep driver. Signed-off-by: Davide Bettio <davide@uninstall.it>
Introduce struct EPaperDesc with everything a unified driver needs to dispatch on compatible string: panel name, native dimensions, SPI clock, palette pointer + size, init sequence, optional per-frame preamble, and the post-frame refresh flags (DRF data byte, POF busy-wait level, periodic white-out interval). Instantiate two descriptors -- epaper_desc_acep7c and epaper_desc_gdep073e01 -- populated from the constants currently baked into the two driver files. The ACeP descriptor also ships a frame_preamble_seq encoding the 0x61 resolution command that today is open-coded in the driver's do_update and clear_screen paths. Signed-off-by: Davide Bettio <davide@uninstall.it>
Fold EpaperScreen into struct EpaperDriver as an embedded member and drop the file-static pointer across both e-paper drivers (acep, gdep073e01). The driver allocation via malloc() already reserves space for it, removing the separate calloc() and its attendant leak-on-driver-free. A prerequisite for the unified e-paper driver: multi-instance support needs every per-screen value to live behind the driver pointer recovered by EPAPER_DRIVER_FROM_CTX, not behind a file-static reachable from a single translation unit. Signed-off-by: Davide Bettio <davide@uninstall.it>
git mv gdep073e01_display_driver.c -> epaper_display_driver.c; rename the create_port entry point and the log TAG accordingly. Pure rename -- the gdep compatible dispatches through the new function, and the waveshare,5in65-acep-7c compatible still calls the separate acep driver file. Signed-off-by: Davide Bettio <davide@uninstall.it>
Generalise epaper_display_driver.c to drive both compatibles from
the per-panel EPaperDesc descriptors. A compatible-string lookup
picks the descriptor; dimensions, SPI clock, palette, init
sequence, per-frame preamble, periodic-refresh interval, and
post-frame refresh flags all come from it.
The driver also accepts an init_list Erlang-side override in the
tuple format used by the DCS LCD driver, extended with
{wait_busy_level, 0|1} for the BUSY-pin polling that e-paper
controllers typically require mid-init.
Route good-display/gdep073e01 and waveshare,5in65-acep-7c through
the single epaper_display_create_port dispatch entry.
Signed-off-by: Davide Bettio <davide@uninstall.it>
The waveshare,5in65-acep-7c compatible now dispatches through the unified epaper_display_driver.c, leaving the legacy 5in65_acep_7c_display_driver.c orphaned. Drop it and its CMakeLists.txt entry. Signed-off-by: Davide Bettio <davide@uninstall.it>
Introduce oled_commands.h/.c with a length-framed init-sequence executor and two init arrays (SSD1306/SH1106 minimal, SSD1315 full). The format ([CMD][FLAGS_LEN][DATA...][DELAY_MS]) matches epaper_commands.h verbatim so the codebase converges on one init-sequence convention. Unlike an inline init transaction, the executor owns the I2C transaction boundary -- one i2c_cmd_link per step -- so that any future vTaskDelay between steps actually waits between commands rather than being a no-op inside a queued cmd_link. Current SSD13xx init steps don't use the DELAY flag. Each array carries a _Static_assert on sizeof to catch miscounted data bytes at compile time. Signed-off-by: Davide Bettio <davide@uninstall.it>
Replace the inline i2c_master_write_byte loop and its long SSD1315/SSD1306 if/else fork with a single oled_execute_init_seq() call dispatched on driver->type. Bytes-on-the-wire are unchanged; the executor opens one i2c_cmd_handle_t per init step instead of batching the entire sequence into one transaction, which on the panel side just means extra stop/start conditions between commands. The optional invert command and the unconditional display ON now ride in a small post-executor transaction inside display_init. They depend on a runtime opt and are not panel-specific data, so they belong with the driver, not the per-variant init array. Drop CMD_SET_CHARGE_PUMP, CMD_SET_SEGMENT_REMAP, and CMD_SET_COM_SCAN_MODE: they were only referenced by the inline init that is being removed here. Signed-off-by: Davide Bettio <davide@uninstall.it>
Define struct OLEDDesc in oled_commands.h with the per-controller fields needed to drive SSD1306, SSD1315, and SH1106 from a single unified driver: dimensions, I2C address, init sequence pointer + length, and the two runtime-behavior flags that today require if/else branching on a display_type_t enum (column reset before each page write; data-stream prefix padding for SH1106's 132-pixel controller exposing a 128-pixel viewport). Instantiate the three descriptors. SSD1306 and SH1106 share the same minimal init array (charge-pump enable + segment / COM-scan remap); SSD1315 uses its own full init. Signed-off-by: Davide Bettio <davide@uninstall.it>
Fold MonoScreen into struct MemoryLCDDriver as an embedded member and drop the file-static pointer + its separate calloc. The per-screen state now travels with the driver pointer recovered by MEMORY_LCD_DRIVER_FROM_CTX, preparing multi-instance support for this driver. While here, accept width/height Erlang opts (defaults 400x240 for the LS027B4DH01) and resolve the `// FIXME: hardcoded width and height`. The new opt surface mirrors what DCS LCD already accepts. struct Screen and its DMA buffers stay at file scope here; they are specific to the Sharp frame format. vcom likewise stays a file-global. Signed-off-by: Davide Bettio <davide@uninstall.it>
Move the Sharp frame-format DMA buffers (pixels, dma_out) and the VCOM toggle state from file scope into struct MemoryLCDDriver as embedded members. Drop the struct Screen wrapper entirely; w/h are carried by the embedded MonoScreen and pixels/dma_out live directly on the driver. Rewrite get_vcom() as next_vcom(driver), advancing the toggle bit on the driver pointer recovered by MEMORY_LCD_DRIVER_FROM_CTX. All file-level globals are gone; nothing in this translation unit now outlives the driver pointer. Signed-off-by: Davide Bettio <davide@uninstall.it>
Fold MonoScreen into struct OLEDDriver as an embedded member and drop the file-static pointer + its separate calloc. The per-screen state now travels with the driver pointer recovered by OLED_DRIVER_FROM_CTX, a prerequisite for multi-instance use. The controller panels are fixed at 128x64 for all three variants, so the MonoScreen dimensions are initialised from the existing DISPLAY_WIDTH / DISPLAY_HEIGHT constants rather than from opts. Signed-off-by: Davide Bettio <davide@uninstall.it>
Generalise the dispatch to drive all three supported controllers from the per-controller OLEDDesc descriptors. A compatible-string lookup replaces the display_type_t enum branching: I2C address, init sequence, per-page column-reset flag, and scanline prefix padding all come from the descriptor. Unknown or missing compatibles now log an ESP_LOGE and early- return instead of silently defaulting to SSD1306, fixing a path that quietly masked typos in the Erlang `compatible` opt. Remove display_type_t, the driver->type field, and the hardcoded I2C_ADDRESS macro -- all three subsumed by the descriptor. Signed-off-by: Davide Bettio <davide@uninstall.it>
git mv ssd1306_display_driver.c -> oled_display_driver.c; rename the create_port entry point and the log TAG accordingly. Pure rename -- all three OLED compatibles (solomon-systech,ssd1306, solomon-systech,ssd1315, sino-wealth,sh1106) now dispatch through oled_display_create_port. The previous filename attributed a Sino Wealth controller (SH1106) to Solomon Systech and was obsolete since the file started driving multiple variants. Signed-off-by: Davide Bettio <davide@uninstall.it>
display_items_init_item has several early-return paths that left primitive/x/y/w/h uninitialized. Callers malloc() the items array, so the draw pipeline read garbage — either tripping the "unexpected display list command" branch or, on unlucky bit patterns, dereferencing junk pointers. memset to zero on entry makes PrimitiveInvalid (= 0) the default and gives the draw dispatchers a safe skip on the zero-bounds case. Also plug two leaks on the same paths: `text` on the unsupported-font branch, `surface.buffer` on the failed ufont draw.
ufont_parse stores raw pointers into its input buffer, which then live in the persistent EpdFont. The buffer is term_binary_data of a gen_server message that the dispatch loop disposes as soon as the handler returns: an immediate UAF for heap binaries, and tied to sender lifetime for refc binaries. Copy into a heap-owned buffer before parsing. The copy lives with the font for the lifetime of the display task, matching the existing "fonts are not unregistered" semantic.
The drop-oldest path in display_task_consume_mailbox can silently
drop an update message to make room for a newer one (rendering a
stale frame is wasted when a fresher one is queued). The dropped
caller was left without a reply and timed out at gen_server:call
— an ugly failure mode for a frame-loss-tolerant design.
Send {Ref, ok} on arrival, before the enqueue-or-drop decision.
Drivers skip the reply on the update path so the caller never
sees a duplicate. draw_buffer gets the same treatment: it is
gen_call-shaped but fire-and-forget at the driver, today timing
out the caller.
register_font and load_image still reply synchronously: they
need to convey the operation outcome (registered handle /
decoded binary) and do not share the drop-for-newer rationale.
Cover the per-frame allocations in do_update, clear_screen, and dcs_lcd_draw_buffer across all four ESP32 drivers. On failure, skip the frame with a log line and free any already-acquired resources. The caller's reply has already been sent at enqueue time (see the prior pre-ack commit), so there is no reply obligation. Also fix an oled do_update early-return on i2c_driver_acquire failure that leaked both `items` and `buf`. Init-time allocations in *_create_port / display_init / display_spi_init remain unchecked; context-teardown-on-init- failure is tracked as a separate follow-up.
I fixed all review comments, except:
|
petermm
left a comment
There was a problem hiding this comment.
Final PR Review — Branch pr/12
Reviewer: Amp (AI-assisted)
Date: 2026-05-02
Scope: 52 original commits + 7 review-fixup commits, ~4100 lines added / ~4500 removed across 46 files
Verdict: ✅ Approve / merge, with two small follow-up tickets noted at the end.
What this PR does
A major architectural refactor of the AtomGL display driver library:
- Boilerplate consolidation — per-driver mailbox handling, message queues, and
process_messagesloops moved into shareddisplay_task/display_message/display_itemsmodules. - Data-driven init sequences — replaces per-controller
if/elseladders with descriptor tables and compact[CMD][FLAGS_LEN][DATA…][DELAY]byte arrays for OLED (SSD1306/SSD1315/SH1106), DCS LCD (ILI9341/9342C/9486/9488/ST7789/ST7796), and e-paper (ACeP 5.65" / GDEP073E01)._Static_assert(sizeof(...))guards every sequence. - uFontLib custom font support added to all ESP32 drivers (previously SDL-only).
- Bug fixes carried in the original 52 commits: off-by-one in
draw_scaled_cropped_img_x, e-paper memory leaks indo_update, ufont allocation-failure crash, inverted IFF magic check (!ufont_iff_is_valid_ufl→ufont_iff_is_valid_ufl), ufont text ignoring background color. - Legacy drivers removed:
ili934x_display_driver.c,ili948x_display_driver.c,st7789_display_driver.c,5in65_acep_7c_display_driver.c,draw_common.h,monochrome.h,message_helpers.h.
Review fixup commits — what landed after first review
| Commit | Issue priority | Summary | Status |
|---|---|---|---|
a3a61e3 |
🟡 P1 | Off-by-one in draw_pixel_x: > → >= in mono_draw.c and epaper_draw.c |
✅ Verified |
176acab |
🟢 P2 | ufont_parse: guard against !iff_binary || buf_size < 12 |
✅ Verified |
0c78709 |
🟢 P2 | Typo: comptaible → compatible in display_driver.c log |
✅ Verified |
e753bc8 |
🔴 P0 | display_items_init_item: memset(item, 0, sizeof(*item)) on entry → PrimitiveInvalid (= 0) is the safe default; also plugs text and surface.buffer leaks |
✅ Verified |
6c0e995 |
🔴 P0 | register_font: deep-copy font binary into a heap-owned buffer before ufont_parse. Resolves UAF after the gen_call message is disposed. |
✅ Verified |
9bd2e1e |
🟡 P1 | Pre-ack update / draw_buffer at enqueue time so callers never time out when the drop-oldest queue evicts a stale frame. Drivers add return to skip duplicate replies. |
✅ Verified |
b01a698 |
🟢 P2 | NULL-check per-frame allocations in do_update / clear_screen / dcs_lcd_draw_buffer across all four ESP32 drivers; fixes OLED leak-on-i2c-failure |
✅ Verified |
Notes on the pre-ack design (9bd2e1e)
The "reply at enqueue" approach is cleaner than the original "use backpressure" suggestion: it preserves the frame-loss-tolerant intent of the drop-oldest queue while ensuring gen_server:call callers never block. Two correctness checks pass:
- Each driver's
process_messageaddsreturnafter theupdatebranch and afterdraw_bufferso the post-switchOK_ATOMreply tuple is not sent twice. try_pre_ack_render_cmdmatches onlyupdate/draw_buffer.register_fontandload_imagekeep their synchronous reply because they must convey the operation outcome.
Architecture highlights (positive)
- CONTAINER_OF pattern for recovering each driver struct from
ctx->platform_datais clean and avoids unsafe casts. - Init sequence framing is well-chosen: DCS LCD uses a
0x00end sentinel (safe — NOP isn't used as init), e-paper and OLED use length-framing (necessary because0x00is PSR / a valid init command on Waveshare/GoodDisplay parts)._Static_assert(sizeof(...))catches byte-counting mistakes at compile time. - Descriptor tables capture all controller variation as pure data; no function pointers needed across the current set.
display_task.ccleanly abstracts the FreeRTOS task+queue pattern with a per-driver function-pointer forprocess_message, plus generic interception ofregister_font.
Outstanding follow-ups
The contributor explicitly deferred two items. Both are acceptable for merge; both deserve a tracking ticket.
Follow-up #1 — Per-task ufont_manager (was 🔴 P0, partial)
File: display_task.c
ufont_manager is still a process-global that every display task overwrites in display_task_process_messages line 148:
UFontManager *ufont_manager; // line 33
void display_task_process_messages(void *arg)
{
struct DisplayTaskArgs *args = arg;
ufont_manager = ufont_manager_new(); // line 148 — last task wins
...
}display_items.c reads the same global:
#ifdef ENABLE_UFONT
#include "ufontlib.h"
extern UFontManager *ufont_manager; // display_items.c:31Why this is fine for now: in the field, only one display port is created per VM, so the race is latent. The deep-copy fix already eliminates the use-after-free, which was the user-visible bug.
Why it should still be fixed eventually: any future use case with two displays (e.g. e-paper + small status OLED) will silently lose font registrations from the first-started task.
Suggested fix — embed the manager in DisplayTaskArgs:
--- a/display_task.h
+++ b/display_task.h
@@ -27,11 +27,16 @@
#include <context.h>
#include <mailbox.h>
+#ifdef ENABLE_UFONT
+#include "ufontlib.h"
+#endif
+
struct DisplayTaskArgs
{
QueueHandle_t messages_queue;
void (*process_message_fn)(Message *message, Context *ctx);
Context *ctx;
+#ifdef ENABLE_UFONT
+ UFontManager *ufont_manager;
+#endif
};
NativeHandlerResult display_task_consume_mailbox(Context *ctx);--- a/display_task.c
+++ b/display_task.c
@@ -30,8 +30,6 @@
#include "display_message.h"
#include "ufontlib.h"
-UFontManager *ufont_manager;
-
static bool try_pre_ack_render_cmd(Message *message, Context *ctx)
{
...
@@ -127,7 +125,8 @@ static bool try_handle_register_font(Message *message, Context *ctx)
char *handle = interop_atom_to_string(ctx,
term_get_tuple_element(req, 1));
if (loaded_font != NULL && handle != NULL) {
- ufont_manager_register(ufont_manager, handle, loaded_font);
+ struct DisplayTaskArgs *args = ctx->platform_data;
+ ufont_manager_register(args->ufont_manager, handle, loaded_font);
}
free(handle);
@@ -145,7 +144,7 @@ void display_task_process_messages(void *arg)
{
struct DisplayTaskArgs *args = arg;
- ufont_manager = ufont_manager_new();
+ args->ufont_manager = ufont_manager_new();
while (true) {
Message *message;--- a/display_items.c
+++ b/display_items.c
@@ -28,8 +28,7 @@
#include <interop.h>
#ifdef ENABLE_UFONT
-#include "ufontlib.h"
-extern UFontManager *ufont_manager;
+#include "display_task.h"
struct Surface
...
@@ -178,8 +177,9 @@ void display_items_init_item(BaseDisplayItem *item, term req, Context *ctx)
} else {
#ifdef ENABLE_UFONT
char *handle = interop_atom_to_string(ctx, font);
+ struct DisplayTaskArgs *args = ctx->platform_data;
EpdFont *loaded_font = NULL;
if (handle != NULL) {
- loaded_font = ufont_manager_find_by_handle(ufont_manager, handle);
+ loaded_font = ufont_manager_find_by_handle(args->ufont_manager, handle);
free(handle);
}Effort: S (≈1h, one focused commit). No new tests needed — existing single-display path keeps working byte-identically.
Follow-up #2 — Context teardown on init failure (was 🟡 P1, deferred)
The contributor flagged this themselves in the b01a698 commit message:
"Init-time allocations in
*_create_port/display_init/display_spi_initremain unchecked; context-teardown-on-init-failure is tracked as a separate follow-up."
Symptom: each *_create_port() always returns a live Context *, even when display_init() hit an early return (invalid compatible string, missing GPIO, I2C/SPI failure, unsupported rotation). Callers receive a port with:
- no worker
xTaskCreate, - partial / NULL
platform_data, - leaked screen buffers, queues, and SPI device handles.
Subsequent mailbox traffic to that port silently hangs (the new pre-ack does not run because the worker never started).
Affected files:
dcs_lcd_display_driver.c—display_inithas 4 early returnsepaper_display_driver.c—display_spi_inithas 2 early returnsoled_display_driver.c—display_inithas 2 early returnsmemory_display_driver.c— implicit, only one failure path
Suggested approach — make init return bool, return NULL from *_create_port on failure:
--- a/dcs_lcd_display_driver.c
+++ b/dcs_lcd_display_driver.c
-static void display_init(Context *ctx, term opts);
+static bool display_init(Context *ctx, term opts);
Context *dcs_lcd_display_create_port(GlobalContext *global, term opts)
{
Context *ctx = context_new(global);
ctx->native_handler = display_task_consume_mailbox;
- display_init(ctx, opts);
+ if (!display_init(ctx, opts)) {
+ context_destroy(ctx);
+ return NULL;
+ }
return ctx;
}Then convert each return; in display_init() to goto cleanup; (or equivalent), and add a single cleanup tail:
static bool display_init(Context *ctx, term opts)
{
struct DCSLCDDriver *driver = NULL;
bool ok = false;
/* … existing init flow, on any failure: goto cleanup … */
ok = true;
cleanup:
if (!ok && driver != NULL) {
if (driver->display_args.messages_queue) {
vQueueDelete(driver->display_args.messages_queue);
}
free(driver->screen.pixels);
free(driver->screen.pixels_out);
free(driver->screen.bytes);
free(driver->screen.bytes_out);
/* spi_bus_remove_device(driver->bus.spi_disp.handle) if init'd */
free(driver);
}
return ok;
}Apply the same shape to the other three drivers. The dispatcher in display_driver.c already treats NULL from a *_create_port as failure (the parent display_create_port returns NULL directly when no compatible matches), so no caller change is needed.
Effort: M (≈half a day, four similar commits). Worth doing because today this is the single class of "silent hang on misconfiguration" most likely to confuse new users wiring up a panel.
Items still in scope but explicitly non-blocking
These are nice-to-haves noted in the original review and not addressed in fixups:
display_driver.cduplicates the family-specificcompatiblestrings as anif/elsechain. A single registration table would be cleaner but is fine for the current 11-controller set.display_items.h:31-34keeps a// TODO: deprecated helpercontext_make_atomwrapper. Can be inlined as a follow-up.dcs_lcd_draw.c:140has apixmem32variable that holds 16-bit pixels — a misleading carry-over name; trivial rename:
--- a/dcs_lcd_draw.c
+++ b/dcs_lcd_draw.c
@@ -137,7 +137,7 @@ int dcs_lcd_draw_text_x(const struct DCSLCDScreen *screen,
int drawn_pixels = 0;
- uint16_t *pixmem32 = (uint16_t *) (((uint8_t *) screen->pixels) + xpos * sizeof(uint16_t));
+ 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;
@@ -159,9 +159,9 @@ int dcs_lcd_draw_text_x(const struct DCSLCDScreen *screen,
}
if (opaque) {
- pixmem32[drawn_pixels] = fgcolor;
+ pixmem16[drawn_pixels] = fgcolor;
} else if (visible_bg) {
- pixmem32[drawn_pixels] = bgcolor;
+ pixmem16[drawn_pixels] = bgcolor;
} else {
return drawn_pixels;
}Recommendation
✅ Merge pr/12. All five P0/P1 issues from the first review are resolved or have a clear, low-risk path forward. The two outstanding items (per-task ufont_manager, init-failure teardown) should be filed as follow-up tickets but do not block.
Suggested ticket titles:
display_task: per-task ufont_manager (multi-display safety)display drivers: clean teardown on init failure (return NULL from *_create_port)
Address the excessive code duplication across AtomGL display
drivers by collapsing each hardware family into one driver,
dispatched via a compatible-string lookup that returns a
per-controller descriptor.
Bytes-on-the-wire and existing
compatiblestrings areunchanged. A new compatible is now ~30 lines of descriptor data,
no new C code. Zero file-statics in any ESP32 driver, so
multi-instance is unblocked.
Also adds support for the Good Display 7.3" 7-color e-paper
panel; use compatible string
good-display/gdep073e01.