Skip to content

Refactor#12

Open
bettio wants to merge 52 commits intoatomvm:mainfrom
bettio:refactor
Open

Refactor#12
bettio wants to merge 52 commits intoatomvm:mainfrom
bettio:refactor

Conversation

@bettio
Copy link
Copy Markdown
Collaborator

@bettio bettio commented Apr 15, 2026

Reorganizes AtomGL display drivers into one unified driver per
hardware family, each dispatched via a compatible-string lookup
returning a per-controller descriptor. Bytes-on-the-wire are
unchanged for every previously supported compatible.

Before → After

Family Before After
DCS LCD 3 per-controller drivers (ili934x, ili948x, st7789) covering 6 compatibles 1 unified driver (dcs_lcd_display_driver.c) + DCSLCDDesc descriptor table
E-paper 2 per-panel drivers (5in65_acep_7c, gdep073e01) covering 2 compatibles 1 unified driver (epaper_display_driver.c) + EPaperDesc descriptor table
OLED (I2C) 1 driver with if/else branching on 3 variants 1 driver (oled_display_driver.c) + OLEDDesc descriptor table
Sharp memory LCD 1 driver with file-static screen state 1 driver with embedded state

What every driver gets for free

  • clock_speed_hz Erlang opt: override the descriptor's default
    SPI clock rate.
  • width, height, x_offset, y_offset, color_order Erlang
    opts apply uniformly to every DCS LCD / e-paper / memory LCD
    compatible.
  • init_list Erlang-side init override now applies to every DCS
    LCD and e-paper compatible (extended with
    {wait_busy_level, 0|1} for e-paper).
  • Reset GPIO is optional on every DCS LCD controller.
  • A new compatible is ~30 lines of descriptor data, no new C code
    path required.
  • Multi-instance prep: zero file-statics in any ESP32 driver; each
    driver's state travels with its CONTAINER_OF-recoverable
    struct.

Hardware validation

Hardware-confirmed on real panels with the demo app and a
regression test scene:

  • ilitek,ili9341 (TFT RGB565)
  • sitronix,st7789 (TFT RGB565)
  • waveshare,5in65-acep-7c (7-color e-ink)
  • good-display/gdep073e01 (7-color e-ink)
  • sharp,memory-lcd (monochrome SPI)

Build-only validation (no panels in the test rig):

  • solomon-systech,ssd1306, solomon-systech,ssd1315,
    sino-wealth,sh1106 (OLED variants)
  • ilitek,ili9342c, ilitek,ili9486, ilitek,ili9488,
    sitronix,st7796 (DCS LCD variants)

For the OLED family, byte-for-byte equivalence of the post-refactor
init sequences was verified via _Static_assert size guards on the
init arrays and manual transcription review against the pre-refactor
inline code. Hardware validation on real OLED panels should precede
relying on this driver in production.

Preserved behaviour

  • Every previously-supported compatible string still works.
  • Existing applications pass their opts exactly as before.
  • Bytes-on-the-wire unchanged for every controller.
  • Display-list semantics, scanline rendering, dithering, and alpha
    blending are unchanged.

Follow-ups (deferred, not blocking)

  • Hardware validation of the OLED variants
    (SSD1306/SSD1315/SH1106).
  • Rotations 2/3 validation on ILI9341/ILI9342C/ST7789/ST7796 —
    descriptor madctl[] tables carry 0xFF sentinels where the
    pre-refactor drivers didn't support those rotations.
  • Multi-instance hardware test (two panels on one ESP32).
  • init_list Erlang override for OLED (parity with e-paper).
  • Migrate DCS LCD init sequences to length-based framing (e-paper
    already uses this).
  • Bump ILI SPI clock from 27 MHz to 40 MHz after per-board IOMUX
    validation.
  • Strip BGR bit from ILI MADCTL tables so color_order: rgb works
    cleanly on ili9341 / ili9342c.
  • Graceful failure for unknown commands in all drivers
    (SDL-style).
  • Switch ufont rendering surface from 32bpp RGBA to 4bpp alpha-only
    (drops a 921×63 glyph from ~227KB to ~29KB).

Test plan

  • idf.py build on the full AtomVM tree.
  • Hardware smoke test on each available panel (ILI9341, ST7789,
    Waveshare 5.65", GDEP073E01, Sharp memory LCD) with the demo
    app and the regression test scene.
  • Hardware validation of SSD1306/SSD1315/SH1106 — follow-up,
    flagged above.

bettio added 30 commits April 15, 2026 14:09
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>
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>
Replace display_init_9341(), display_init_9342c(),
display_init_9486(), display_init_9488(), display_init_std(), and
display_init_alt_gamma_2() with calls to dcs_lcd_execute_init_seq()
reading the byte-array tables from dcs_lcd_commands.c. Remove the
six static init functions and their now-unused controller register
defines (keep TFTWIDTH/TFTHEIGHT).

The display_init_using_list() Erlang-driven init path is unchanged.
The delay() helper stays -- still used in the reset path and by
display_init_using_list(). Bytes-on-the-wire are unchanged for
every controller.

Signed-off-by: Davide Bettio <davide@uninstall.it>
bettio added 22 commits April 15, 2026 14:53
Define struct DCSLCDDesc and populate descriptors for the 6
compatible strings served by the DCS LCD drivers: ILI9341,
ILI9342C, ILI9486, ILI9488, ST7789, ST7796.

Each descriptor captures native resolution, SPI clock, pixel
format, per-rotation MADCTL values, default color order, and the
built-in init sequence pointer. Rotations not validated on the
existing hardware are marked 0xFF.

Signed-off-by: Davide Bettio <davide@uninstall.it>
Expose the SPI clock rate as an Erlang opt so applications can
override the descriptor default.

Signed-off-by: Davide Bettio <davide@uninstall.it>
Move the SWRESET, SLPOUT and DISPON commands (with their delays)
into the per-controller init sequence tables. Each driver
previously issued these manually around dcs_lcd_execute_init_seq;
folding them into the tables lets a single shared driver execute
the entire bring-up sequence with no controller-specific C code.

ILI controllers gain a DISPON+120ms tail (matching st7789). ST7789
without a reset GPIO drops the explicit SWRESET+100ms in favour of
SWRESET+5ms+SLPOUT+120ms -- both datasheet-compliant.

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>
@petermm
Copy link
Copy Markdown
Contributor

petermm commented Apr 17, 2026

per AMP:

PR Review — Branch pr/12 (52 commits)

Reviewer: Amp (AI-assisted)
Date: 2026-04-17
Scope: ~4100 lines added, ~4500 removed across 46 files


Summary

This PR is a major architectural refactor that:

  1. Consolidates duplicated per-driver boilerplate (message queues, mailbox consumption, process_messages loops, send_message helpers) into shared modules: display_task, display_message, display_items.
  2. Replaces per-driver if/else init code with data-driven descriptor tables and compact byte-array init sequences for OLED (SSD1306/SSD1315/SH1106), DCS LCD (ILI9341/9342C/9486/9488/ST7789/ST7796), and e-paper (ACeP 5.65", GDEP073E01).
  3. Adds uFontLib custom font support to all ESP32 drivers (previously SDL-only).
  4. Fixes several bugs: off-by-one in draw_scaled_cropped_img_x, memory leaks in e-paper do_update, ufont crash on allocation failure, inverted IFF validation logic in ufont_parse, ufont text ignoring background color.
  5. Removes legacy drivers: 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.

Overall assessment: The refactoring direction is excellent — it significantly reduces code duplication and makes adding new display controllers trivial. The init-sequence table design is sound (with _Static_assert guardrails). Several issues should be addressed before or shortly after merge.


Architecture Highlights (Positive)

  • CONTAINER_OF pattern for recovering the driver struct from ctx->platform_data is clean and avoids unnecessary casts.
  • Init sequence format is well-designed: DCS LCD uses a sentinel 0x00 end marker (safe since NOP is not a real init command), while e-paper and OLED use length-framing (because 0x00 is PSR on Waveshare/GoodDisplay controllers). The _Static_assert on sequence sizes is a strong guardrail against byte-counting errors.
  • Descriptor tables (OLEDDesc, DCSLCDDesc, EPaperDesc) capture all controller variation as pure data, eliminating function-pointer complexity.
  • display_task.c cleanly abstracts the FreeRTOS task+queue pattern with a function-pointer callback for per-driver message handling, plus intercepts register_font generically.

Issues

🔴 P0 — uFont Global State & Lifetime

File: display_task.c:33, display_items.c:31

ufont_manager is a global variable shared across all display drivers. Each call to display_task_process_messages overwrites it (line 106). With multiple display ports, the last task to start wins and earlier registrations are lost.

Additionally, ufont_parse() stores raw pointers into the passed buffer (ufontlib.c), but the font binary comes from an Erlang message that is disposed immediately after try_handle_register_font returns (line 116-118). If the AtomVM runtime reclaims that memory, all registered fonts become use-after-free.

Recommendation: Make the font manager per-task (embed in DisplayTaskArgs), and deep-copy the font binary before parsing:

// display_task.h
struct DisplayTaskArgs
{
    QueueHandle_t messages_queue;
    void (*process_message_fn)(Message *message, Context *ctx);
    Context *ctx;
#ifdef ENABLE_UFONT
    UFontManager *ufont_manager;
#endif
};

// display_task.c — in try_handle_register_font:
size_t sz = term_binary_size(font_bin);
void *owned = malloc(sz);
if (!owned) { /* reply error, return true */ }
memcpy(owned, term_binary_data(font_bin), sz);
EpdFont *loaded_font = ufont_parse(owned, sz);
if (!loaded_font) { free(owned); /* reply error */ }

🔴 P0 — Uninitialized Display Items on Error Paths

File: display_items.c

display_items_init_item() sets only owns_data = false at entry (line 69), then has multiple early returns on error (unsupported format: lines 93-96, 128-132; invalid text: 163-165; failed ufont alloc: 201-205; failed ufont draw: 217-219). The caller (do_update) still includes these items in the render loop, which reads uninitialized primitive, width, height, x, y fields.

Suggested fix — initialize sentinel at function entry:

 void display_items_init_item(BaseDisplayItem *item, term req, Context *ctx)
 {
+    item->primitive = PrimitiveInvalid;
+    item->x = -1;
+    item->y = -1;
+    item->width = 0;
+    item->height = 0;
+    item->brcolor = 0;
     item->owns_data = false;
+    memset(&item->data, 0, sizeof(item->data));
+    item->source_x = item->source_y = 0;
+    item->x_scale = item->y_scale = 1;

Also: lines 186-190 leak text on the "unsupported font" path, and line 217-219 leaks surface.buffer on draw failure.


🟡 P1 — Silent Message Loss on Queue Overflow

File: display_task.c:42-57

When the queue is full, display_task_consume_mailbox drops the oldest message (disposing it) to make room. For gen_call messages, the caller's Erlang process will block forever waiting for a reply that never comes.

// Current: silently drops oldest message
if (xQueueSend(args->messages_queue, &msg, 0) != pdTRUE) {
    Message *old = NULL;
    if (xQueueReceive(args->messages_queue, &old, 0) == pdTRUE && old) {
        // ... dispose old, no reply sent ...
    }

Recommendation: Either use backpressure (block with a timeout) or send an error reply before disposing:

// Option A: backpressure (simplest, matches old behavior)
xQueueSend(args->messages_queue, &msg, portMAX_DELAY);

🟡 P1 — Port Creation Succeeds Even When Init Fails

Files: dcs_lcd_display_driver.c:272-278, epaper_display_driver.c:415-421, oled_display_driver.c (equivalent)

Each *_create_port() function creates a Context, calls display_init(), and returns the context unconditionally. But display_init() has multiple early return paths (invalid compat string, bad GPIOs, unsupported rotation, I2C/SPI failure). On any of these, the caller receives a live port with no worker task, a partial driver struct, and leaked allocations.

Recommendation: Have display_init() return bool and return NULL from *_create_port() on failure, freeing any partial allocations.


🟡 P1 — Pixel Write Boundary Check Off-by-One

Files: mono_draw.c:71, epaper_draw.c:37

Both use > instead of >=:

// mono_draw.c:71
if (xpos > screen->w) {   // allows write at xpos == w (out of bounds)

// epaper_draw.c:37
if (xpos > screen->w) {   // same issue

Fix:

-    if (xpos > screen->w) {
+    if (xpos >= screen->w) {

🟢 P2 — Typo in Error Message

File: display_driver.c:73

ESP_LOGE(TAG, "No matching display driver for given `comptaible`: `%s`.", compat_string);

Fix:

-    ESP_LOGE(TAG, "No matching display driver for given `comptaible`: `%s`.", compat_string);
+    ESP_LOGE(TAG, "No matching display driver for given `compatible`: `%s`.", compat_string);

🟢 P2 — Missing malloc/heap_caps_malloc NULL Checks

Several allocations are unchecked:

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-bit

Carried 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants