From 5c47ea96861916e1d191295d4ee7afd5ec4ecb3d Mon Sep 17 00:00:00 2001 From: Tom Hoenderdos Date: Thu, 1 Oct 2026 17:06:33 +0200 Subject: [PATCH 1/3] ci: Build against AtomVM release-0.7 release-0.6 is being phased out. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Tom Hoenderdos --- .github/workflows/esp32-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/esp32-build.yaml b/.github/workflows/esp32-build.yaml index 44b9e72..63361f6 100644 --- a/.github/workflows/esp32-build.yaml +++ b/.github/workflows/esp32-build.yaml @@ -30,7 +30,7 @@ jobs: uses: actions/checkout@v4 with: repository: "atomvm/AtomVM" - ref: "release-0.6" + ref: "release-0.7" - name: Checkout repo uses: actions/checkout@v4 From 683bf365d66303692214c50479bf98b2e6e31f63 Mon Sep 17 00:00:00 2001 From: Tom Hoenderdos Date: Thu, 1 Oct 2026 17:25:36 +0200 Subject: [PATCH 2/3] display_items: Validate display list items The parser trusted every display list item: it read tuple elements without checking the arity, converted terms with term_to_int() without checking that they were integers, and used image binaries without checking their size. A malformed item from Erlang code could read outside a tuple or past the end of an image binary, and a scaled_cropped_image with a zero scale divided by zero in the renderers. Items are now checked before they are drawn. An item with a wrong arity, a value of the wrong type, an out of range value or an unknown command becomes an invalid item that draws nothing, and the rest of the display list is drawn as usual. In detail: - image and scaled_cropped_image need an rgba8888 image tuple with a width and height from 1 to 32767 and a binary holding at least Width * Height * 4 bytes. Their coordinates, sizes, source offsets and scales must be within +-32767, the scales at least 1 and the source offset inside the image. The drawn width and height of a scaled_cropped_image are reduced to what is left of the image right of and below the source offset. - rect and text accept any integer position and size, which is clamped to +-32767 without changing which pixels are drawn, so existing lists that draw oversized rects keep working. - A text font must be an atom. - Integers may be boxed, so values above 2^27 are read the same way on 32-bit and 64-bit builds. Ranges are checked on 64 bits before narrowing to int, so out of range values can't wrap into range. Colors use the low 24 bits of any integer. Each invalid item is logged to stderr on one line with its position, command and arity, at most three per update, so that a list rebuilt on every frame doesn't flood a slow serial console. The drivers now get their items from the new display_items_new_list(), which counts, allocates and parses the list in one place. An empty display list no longer allocates zero bytes, which returned NULL on ESP-IDF and skipped the update with an error, and a display list that is not a proper list is rejected instead of being parsed up to its tail. Text in a ufont font that renders to an empty rect becomes an empty item instead of failing a zero-byte allocation. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Tom Hoenderdos --- dcs_lcd_display_driver.c | 15 +- display_items.c | 532 +++++++++++++++++++++++++++------------ display_items.h | 12 +- docs/primitives.md | 53 +++- epaper_display_driver.c | 15 +- memory_display_driver.c | 15 +- oled_display_driver.c | 15 +- sdl_display/display.c | 26 +- 8 files changed, 457 insertions(+), 226 deletions(-) diff --git a/dcs_lcd_display_driver.c b/dcs_lcd_display_driver.c index f1097a7..14873ad 100644 --- a/dcs_lcd_display_driver.c +++ b/dcs_lcd_display_driver.c @@ -139,21 +139,12 @@ static void display_init_using_list(struct DCSLCDDriver *driver, term init_list) static void do_update(Context *ctx, term display_list) { - int proper; - int len = term_list_length(display_list, &proper); - - BaseDisplayItem *items = malloc(sizeof(BaseDisplayItem) * len); - if (UNLIKELY(!items)) { - fprintf(stderr, "do_update: failed to alloc items\n"); + BaseDisplayItem *items; + size_t len; + if (UNLIKELY(display_items_new_list(display_list, &items, &len, ctx) != DisplayItemsOk)) { return; } - 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); - } - struct DCSLCDDriver *driver = DCS_LCD_DRIVER_FROM_CTX(ctx); int screen_width = driver->screen.w; int screen_height = driver->screen.h; diff --git a/display_items.c b/display_items.c index 50fdec3..3c8b2a6 100644 --- a/display_items.c +++ b/display_items.c @@ -25,6 +25,9 @@ #include #include +#include + +#include "font_data.h" #ifdef ENABLE_UFONT #include "ufontlib.h" @@ -64,188 +67,403 @@ void epd_draw_pixel(int xpos, int ypos, uint8_t color, void *buffer) } #endif /* ENABLE_UFONT */ -void display_items_init_item(BaseDisplayItem *item, term req, Context *ctx) +#define MAX_LOGGED_INVALID_ITEMS 3 + +static bool get_int_element(term req, int index, avm_int64_t *out) { - memset(item, 0, sizeof(*item)); + term t = term_get_tuple_element(req, index); + if (UNLIKELY(!term_is_int64(t))) { + return false; + } + *out = term_to_int64(t); - term cmd = term_get_tuple_element(req, 0); + return true; +} - if (cmd == context_make_atom(ctx, "\x5" - "image")) { - item->primitive = PrimitiveImage; - item->x = term_to_int(term_get_tuple_element(req, 1)); - item->y = term_to_int(term_get_tuple_element(req, 2)); +static bool get_bounded_element(term req, int index, int min, int max, int *out) +{ + avm_int64_t v; + if (UNLIKELY(!get_int_element(req, index, &v) || v < min || v > max)) { + return false; + } + *out = (int) v; - term bgcolor = term_get_tuple_element(req, 3); - if (bgcolor == context_make_atom(ctx, "\xB" - "transparent")) { - item->brcolor = 0; - } else { - item->brcolor = ((uint32_t) term_to_int(bgcolor)) << 8 | 0xFF; - } + return true; +} - term img = term_get_tuple_element(req, 4); +static bool get_coord_element(term req, int index, int *out) +{ + return get_bounded_element(req, index, -DISPLAY_ITEMS_COORD_LIMIT, DISPLAY_ITEMS_COORD_LIMIT, out); +} - term format = term_get_tuple_element(img, 0); - if (format != context_make_atom(ctx, "\x8" - "rgba8888")) { - fprintf(stderr, "unsupported image format: "); - term_display(stderr, format, ctx); - fprintf(stderr, "\n"); - return; - } - item->width = term_to_int(term_get_tuple_element(img, 1)); - item->height = term_to_int(term_get_tuple_element(img, 2)); - item->data.image_data.pix = term_binary_data(term_get_tuple_element(img, 3)); +static bool get_color_element(term req, int index, uint32_t *out) +{ + avm_int64_t color; + if (UNLIKELY(!get_int_element(req, index, &color))) { + return false; + } + *out = ((uint32_t) color) << 8 | 0xFF; - } else if (cmd == globalcontext_make_atom(ctx->global, ATOM_STR("\x14", "scaled_cropped_image"))) { - item->primitive = PrimitiveScaledCroppedImage; - item->x = term_to_int(term_get_tuple_element(req, 1)); - item->y = term_to_int(term_get_tuple_element(req, 2)); - item->width = term_to_int(term_get_tuple_element(req, 3)); - item->height = term_to_int(term_get_tuple_element(req, 4)); - - term bgcolor = term_get_tuple_element(req, 5); - if (bgcolor == globalcontext_make_atom(ctx->global, "\xB" - "transparent")) { - item->brcolor = 0; - } else { - item->brcolor = ((uint32_t) term_to_int(bgcolor)) << 8 | 0xFF; - } + return true; +} - item->source_x = term_to_int(term_get_tuple_element(req, 6)); - item->source_y = term_to_int(term_get_tuple_element(req, 7)); - item->x_scale = term_to_int(term_get_tuple_element(req, 8)); - item->y_scale = term_to_int(term_get_tuple_element(req, 9)); +static bool get_bgcolor_element(term req, int index, uint32_t *out, Context *ctx) +{ + if (term_get_tuple_element(req, index) == globalcontext_make_atom(ctx->global, ATOM_STR("\xB", "transparent"))) { + *out = 0; + return true; + } - // 10th element is for opts, but right now no opts are supported + return get_color_element(req, index, out); +} + +static bool get_rgba8888_image(term img, struct ImageDataWithSize *out, Context *ctx) +{ + if (UNLIKELY(!term_is_tuple(img) || term_get_tuple_arity(img) != 4 + || term_get_tuple_element(img, 0) != globalcontext_make_atom(ctx->global, ATOM_STR("\x8", "rgba8888")))) { + return false; + } + int width; + int height; + if (UNLIKELY(!get_bounded_element(img, 1, 1, DISPLAY_ITEMS_COORD_LIMIT, &width) + || !get_bounded_element(img, 2, 1, DISPLAY_ITEMS_COORD_LIMIT, &height))) { + return false; + } + term pixels = term_get_tuple_element(img, 3); + if (UNLIKELY(!term_is_binary(pixels) || term_binary_size(pixels) < (uint64_t) width * height * 4)) { + return false; + } + out->width = width; + out->height = height; + out->pix = term_binary_data(pixels); - term img = term_get_tuple_element(req, 11); + return true; +} - term format = term_get_tuple_element(img, 0); - if (format != globalcontext_make_atom(ctx->global, "\x8" - "rgba8888")) { - fprintf(stderr, "unsupported image format: "); - term_display(stderr, format, ctx); - fprintf(stderr, "\n"); - return; - } - item->data.image_data_with_size.width = term_to_int(term_get_tuple_element(img, 1)); - item->data.image_data_with_size.height = term_to_int(term_get_tuple_element(img, 2)); - item->data.image_data_with_size.pix = term_binary_data(term_get_tuple_element(img, 3)); - - } else if (cmd == context_make_atom(ctx, "\x4" - "rect")) { - item->primitive = PrimitiveRect; - item->x = term_to_int(term_get_tuple_element(req, 1)); - item->y = term_to_int(term_get_tuple_element(req, 2)); - item->width = term_to_int(term_get_tuple_element(req, 3)); - item->height = term_to_int(term_get_tuple_element(req, 4)); - item->brcolor = term_to_int(term_get_tuple_element(req, 5)) << 8 | 0xFF; - - } else if (cmd == context_make_atom(ctx, "\x4" - "text")) { - item->x = term_to_int(term_get_tuple_element(req, 1)); - item->y = term_to_int(term_get_tuple_element(req, 2)); - uint32_t fgcolor = term_to_int(term_get_tuple_element(req, 4)) << 8 | 0xFF; - uint32_t brcolor; - term bgcolor = term_get_tuple_element(req, 5); - if (bgcolor == globalcontext_make_atom(ctx->global, "\xB" - "transparent")) { - brcolor = 0; - } else { - brcolor = ((uint32_t) term_to_int(bgcolor)) << 8 | 0xFF; - } - term text_term = term_get_tuple_element(req, 6); - int ok; - char *text = interop_term_to_string(text_term, &ok); - if (!ok) { - fprintf(stderr, "invalid text.\n"); - return; +static const char *init_image_item(BaseDisplayItem *item, term req, Context *ctx) +{ + struct ImageDataWithSize img; + if (UNLIKELY(term_get_tuple_arity(req) != 5)) { + return "wrong arity"; + } + if (UNLIKELY(!get_coord_element(req, 1, &item->x) || !get_coord_element(req, 2, &item->y) + || !get_bgcolor_element(req, 3, &item->brcolor, ctx))) { + return "bad position or color"; + } + if (UNLIKELY(!get_rgba8888_image(term_get_tuple_element(req, 4), &img, ctx))) { + return "bad image"; + } + item->primitive = PrimitiveImage; + item->width = img.width; + item->height = img.height; + item->data.image_data.pix = img.pix; + + return NULL; +} + +static const char *init_scaled_cropped_image_item(BaseDisplayItem *item, term req, Context *ctx) +{ + struct ImageDataWithSize img; + if (UNLIKELY(term_get_tuple_arity(req) != 12)) { + return "wrong arity"; + } + if (UNLIKELY(!get_coord_element(req, 1, &item->x) || !get_coord_element(req, 2, &item->y) + || !get_bounded_element(req, 3, 0, DISPLAY_ITEMS_COORD_LIMIT, &item->width) + || !get_bounded_element(req, 4, 0, DISPLAY_ITEMS_COORD_LIMIT, &item->height) + || !get_bgcolor_element(req, 5, &item->brcolor, ctx) + || !get_bounded_element(req, 6, 0, DISPLAY_ITEMS_COORD_LIMIT, &item->source_x) + || !get_bounded_element(req, 7, 0, DISPLAY_ITEMS_COORD_LIMIT, &item->source_y) + || !get_bounded_element(req, 8, 1, DISPLAY_ITEMS_COORD_LIMIT, &item->x_scale) + || !get_bounded_element(req, 9, 1, DISPLAY_ITEMS_COORD_LIMIT, &item->y_scale))) { + return "bad position, size, color, source or scale"; + } + if (UNLIKELY(!get_rgba8888_image(term_get_tuple_element(req, 11), &img, ctx))) { + return "bad image"; + } + if (UNLIKELY(item->source_x >= img.width || item->source_y >= img.height)) { + return "source outside the image"; + } + + int max_width = (img.width - item->source_x) * item->x_scale; + int max_height = (img.height - item->source_y) * item->y_scale; + if (item->width > max_width) { + item->width = max_width; + } + if (item->height > max_height) { + item->height = max_height; + } + + item->primitive = PrimitiveScaledCroppedImage; + item->data.image_data_with_size = img; + + return NULL; +} + +static int clamp_coord(avm_int64_t v) +{ + if (v < -DISPLAY_ITEMS_COORD_LIMIT) { + return -DISPLAY_ITEMS_COORD_LIMIT; + } + if (v > DISPLAY_ITEMS_COORD_LIMIT) { + return DISPLAY_ITEMS_COORD_LIMIT; + } + + return (int) v; +} + +static bool get_clamped_span(term req, int start_index, int size_index, int *start, int *size) +{ + avm_int64_t s; + avm_int64_t n; + if (UNLIKELY(!get_int_element(req, start_index, &s) || !get_int_element(req, size_index, &n))) { + return false; + } + *start = clamp_coord(s); + if (n <= 0) { + *size = 0; + return true; + } + avm_int64_t end = (s > INT64_MAX - n) ? INT64_MAX : s + n; + *size = clamp_coord(end) - *start; + + return true; +} + +static const char *init_rect_item(BaseDisplayItem *item, term req, Context *ctx) +{ + if (UNLIKELY(term_get_tuple_arity(req) != 6)) { + return "wrong arity"; + } + if (UNLIKELY(!get_clamped_span(req, 1, 3, &item->x, &item->width) + || !get_clamped_span(req, 2, 4, &item->y, &item->height) + || !get_color_element(req, 5, &item->brcolor))) { + return "bad position, size or color"; + } + item->primitive = PrimitiveRect; + + return NULL; +} + +static void init_default_font_text(BaseDisplayItem *item, avm_int64_t x, int y, char *text, + uint32_t fgcolor, uint32_t brcolor) +{ + avm_int64_t len = (avm_int64_t) strlen(text); + if (x < -DISPLAY_ITEMS_COORD_LIMIT) { + avm_int64_t skip = (-DISPLAY_ITEMS_COORD_LIMIT - x + CHAR_WIDTH - 1) / CHAR_WIDTH; + if (skip > len) { + skip = len; } + memmove(text, text + skip, len - skip + 1); + len -= skip; + x += skip * CHAR_WIDTH; + } + item->x = clamp_coord(x); + avm_int64_t width = len * CHAR_WIDTH; + if (width > DISPLAY_ITEMS_COORD_LIMIT - item->x) { + width = DISPLAY_ITEMS_COORD_LIMIT - item->x; + } + item->primitive = PrimitiveText; + item->y = y; + item->width = (int) width; + item->height = 16; + item->brcolor = brcolor; + item->data.text_data.fgcolor = fgcolor; + item->data.text_data.text = text; +} - term font = term_get_tuple_element(req, 3); +static const char *init_text_item(BaseDisplayItem *item, term req, Context *ctx) +{ + avm_int64_t x; + avm_int64_t y; + uint32_t fgcolor; + uint32_t brcolor; + if (UNLIKELY(term_get_tuple_arity(req) != 7)) { + return "wrong arity"; + } + if (UNLIKELY(!get_int_element(req, 1, &x) || !get_int_element(req, 2, &y) + || !get_color_element(req, 4, &fgcolor) || !get_bgcolor_element(req, 5, &brcolor, ctx))) { + return "bad position or color"; + } + term font = term_get_tuple_element(req, 3); + if (UNLIKELY(!term_is_atom(font))) { + return "bad font"; + } + term text_term = term_get_tuple_element(req, 6); + int ok; + char *text = interop_term_to_string(text_term, &ok); + if (UNLIKELY(!ok || IS_NULL_PTR(text))) { + return "bad text"; + } - if (font == globalcontext_make_atom(ctx->global, "\xB" "default16px")) { - item->primitive = PrimitiveText; - item->height = 16; - item->width = strlen(text) * 8; - item->brcolor = brcolor; - item->data.text_data.fgcolor = fgcolor; - item->data.text_data.text = text; + if (font == globalcontext_make_atom(ctx->global, ATOM_STR("\xB", "default16px"))) { + init_default_font_text(item, x, clamp_coord(y), text, fgcolor, brcolor); - } else { + } else { #ifdef ENABLE_UFONT - char *handle = interop_atom_to_string(ctx, font); - EpdFont *loaded_font = NULL; - if (handle != NULL) { - loaded_font = ufont_manager_find_by_handle(ufont_manager, handle); - free(handle); - } + char *handle = interop_atom_to_string(ctx, font); + EpdFont *loaded_font = NULL; + if (!IS_NULL_PTR(handle)) { + loaded_font = ufont_manager_find_by_handle(ufont_manager, handle); + free(handle); + } - if (!loaded_font) { - fprintf(stderr, "unsupported font: "); - term_display(stderr, font, ctx); - fprintf(stderr, "\n"); - free(text); - return; - } + if (UNLIKELY(IS_NULL_PTR(loaded_font))) { + free(text); + return "unsupported font"; + } - EpdFontProperties props = epd_font_properties_default(); - EpdRect rect = epd_get_string_rect(loaded_font, text, 0, 0, 0, &props); - - struct Surface surface; - surface.width = rect.width; - surface.height = rect.height; - surface.buffer = malloc(rect.width * rect.height * BPP); - if (!surface.buffer) { - fprintf(stderr, "Failed to allocate ufont surface (%ix%i)\n", - rect.width, rect.height); - free(text); - return; - } - memset(surface.buffer, 0, rect.width * rect.height * BPP); - // Convert Erlang fgcolor (0xRRGGBBAA) to RGBA8888 little- - // endian byte order (R in low byte, alpha byte cleared) so - // epd_draw_pixel can OR it with the per-pixel alpha. - surface.fg_color = ((fgcolor >> 24) & 0xFFu) - | (((fgcolor >> 16) & 0xFFu) << 8) - | (((fgcolor >> 8) & 0xFFu) << 16); - int text_x = 0; - int text_y = loaded_font->ascender; - enum EpdDrawError res = epd_write_default(loaded_font, text, &text_x, &text_y, &surface); + EpdFontProperties props = epd_font_properties_default(); + EpdRect rect = epd_get_string_rect(loaded_font, text, 0, 0, 0, &props); + if (rect.width <= 0 || rect.height <= 0) { free(text); - if (res != EPD_DRAW_SUCCESS) { - fprintf(stderr, "Failed to draw text. Error code: %i\n", res); - free(surface.buffer); - return; - } + return NULL; + } - item->primitive = PrimitiveImage; - item->width = surface.width; - item->height = surface.height; - item->brcolor = brcolor; - item->data.image_data.pix = surface.buffer; - item->owns_data = true; -#else - fprintf(stderr, "unsupported font: "); - term_display(stderr, font, ctx); - fprintf(stderr, "\n"); - item->primitive = PrimitiveText; - item->height = 16; - item->width = strlen(text) * 8; - item->brcolor = brcolor; - item->data.text_data.fgcolor = fgcolor; - item->data.text_data.text = text; + struct Surface surface; + surface.width = rect.width; + surface.height = rect.height; + surface.buffer = malloc(rect.width * rect.height * BPP); + if (IS_NULL_PTR(surface.buffer)) { + free(text); + return "out of memory"; + } + memset(surface.buffer, 0, rect.width * rect.height * BPP); + // Convert Erlang fgcolor (0xRRGGBBAA) to RGBA8888 little- + // endian byte order (R in low byte, alpha byte cleared) so + // epd_draw_pixel can OR it with the per-pixel alpha. + surface.fg_color = ((fgcolor >> 24) & 0xFFu) + | (((fgcolor >> 16) & 0xFFu) << 8) + | (((fgcolor >> 8) & 0xFFu) << 16); + int text_x = 0; + int text_y = loaded_font->ascender; + enum EpdDrawError res = epd_write_default(loaded_font, text, &text_x, &text_y, &surface); + free(text); + if (UNLIKELY(res != EPD_DRAW_SUCCESS)) { + free(surface.buffer); + return "text drawing failed"; + } + item->primitive = PrimitiveImage; + item->x = clamp_coord(x); + item->y = clamp_coord(y); + item->width = surface.width; + item->height = surface.height; + item->brcolor = brcolor; + item->data.image_data.pix = surface.buffer; + item->owns_data = true; +#else + fprintf(stderr, "unsupported font: "); + term_display(stderr, font, ctx); + fprintf(stderr, "\n"); + init_default_font_text(item, x, clamp_coord(y), text, fgcolor, brcolor); #endif - } + } + + return NULL; +} + +static const char *init_item(BaseDisplayItem *item, term req, Context *ctx) +{ + memset(item, 0, sizeof(*item)); + + if (UNLIKELY(!term_is_tuple(req) || term_get_tuple_arity(req) < 1)) { + return "not a command tuple"; + } + + term cmd = term_get_tuple_element(req, 0); + const char *reason; + + if (cmd == globalcontext_make_atom(ctx->global, ATOM_STR("\x5", "image"))) { + reason = init_image_item(item, req, ctx); + + } else if (cmd == globalcontext_make_atom(ctx->global, ATOM_STR("\x14", "scaled_cropped_image"))) { + reason = init_scaled_cropped_image_item(item, req, ctx); + + } else if (cmd == globalcontext_make_atom(ctx->global, ATOM_STR("\x4", "rect"))) { + reason = init_rect_item(item, req, ctx); + + } else if (cmd == globalcontext_make_atom(ctx->global, ATOM_STR("\x4", "text"))) { + reason = init_text_item(item, req, ctx); } else { - fprintf(stderr, "unexpected display list command: "); - term_display(stderr, req, ctx); - fprintf(stderr, "\n"); + reason = "unknown command"; + } + + if (UNLIKELY(reason != NULL)) { + memset(item, 0, sizeof(*item)); + } + + return reason; +} + +static void log_invalid_item(term req, size_t index, const char *reason, Context *ctx) +{ + fprintf(stderr, "invalid display list item"); + if (index > 0) { + fprintf(stderr, " %u", (unsigned) index); + } + if (term_is_tuple(req)) { + int arity = term_get_tuple_arity(req); + fprintf(stderr, " ("); + if (arity >= 1 && term_is_atom(term_get_tuple_element(req, 0))) { + term_display(stderr, term_get_tuple_element(req, 0), ctx); + } else { + fprintf(stderr, "tuple"); + } + fprintf(stderr, "/%d)", arity); + } + fprintf(stderr, ": %s\n", reason); +} + +void display_items_init_item(BaseDisplayItem *item, term req, Context *ctx) +{ + const char *reason = init_item(item, req, ctx); + if (UNLIKELY(reason != NULL)) { + log_invalid_item(req, 0, reason, ctx); + } +} + +display_items_result_t display_items_new_list(term display_list, BaseDisplayItem **items, size_t *items_len, Context *ctx) +{ + int proper; + int len = term_list_length(display_list, &proper); + if (UNLIKELY(!proper)) { + fprintf(stderr, "invalid display list: not a proper list\n"); + return DisplayItemsNotAProperList; + } + + if (len == 0) { + *items = NULL; + *items_len = 0; + return DisplayItemsOk; } + + BaseDisplayItem *new_items = malloc(sizeof(BaseDisplayItem) * len); + if (IS_NULL_PTR(new_items)) { + fprintf(stderr, "failed to allocate display list items\n"); + return DisplayItemsOutOfMemory; + } + + size_t invalid = 0; + for (int i = 0; i < len; i++) { + term req = term_get_list_head(display_list); + const char *reason = init_item(&new_items[i], req, ctx); + if (UNLIKELY(reason != NULL)) { + if (invalid < MAX_LOGGED_INVALID_ITEMS) { + log_invalid_item(req, i + 1, reason, ctx); + } + invalid++; + } + display_list = term_get_list_tail(display_list); + } + if (UNLIKELY(invalid > MAX_LOGGED_INVALID_ITEMS)) { + fprintf(stderr, "%u more invalid display list items\n", (unsigned) (invalid - MAX_LOGGED_INVALID_ITEMS)); + } + + *items = new_items; + *items_len = len; + + return DisplayItemsOk; } void display_items_delete(BaseDisplayItem items[], size_t items_len) diff --git a/display_items.h b/display_items.h index b5d505f..50b3168 100644 --- a/display_items.h +++ b/display_items.h @@ -33,6 +33,8 @@ static inline term context_make_atom(Context *ctx, AtomString string) return globalcontext_make_atom(ctx->global, string); } +#define DISPLAY_ITEMS_COORD_LIMIT 32767 + typedef enum { PrimitiveInvalid = 0, @@ -86,8 +88,16 @@ struct BaseDisplayItem typedef struct BaseDisplayItem BaseDisplayItem; +typedef enum +{ + DisplayItemsOk = 0, + DisplayItemsNotAProperList, + DisplayItemsOutOfMemory +} display_items_result_t; + void display_items_init_item(BaseDisplayItem *item, term req, Context *ctx); + +display_items_result_t display_items_new_list(term display_list, BaseDisplayItem **items, size_t *items_len, Context *ctx); void display_items_delete(BaseDisplayItem items[], size_t items_len); #endif - diff --git a/docs/primitives.md b/docs/primitives.md index 3fdadcb..aa5d901 100644 --- a/docs/primitives.md +++ b/docs/primitives.md @@ -14,11 +14,43 @@ represented as an Erlang tuple with specific parameters defining its appearance ### Colors Colors are represented as 24-bit RGB values. For example, `0xFF0000` represents red (equivalent to HTML color `#FF0000`). Display drivers for monochrome devices may apply dithering, while 16-bit -displays may reduce color depth as needed. +displays may reduce color depth as needed. A color can be any integer, but only its low 24 bits are +used: `16#1FF0000` is the same red and `-1` is white. ### Coordinates and Sizes -All numeric values are integers. Coordinates are specified in pixels, as are sizes. Subpixel or -half-pixel values are not allowed. +All numeric values are integers, of any size AtomVM can represent up to 64 bits. Coordinates are +specified in pixels, as are sizes. Subpixel or half-pixel values are not allowed. + +- `rect` and `text` accept any coordinate and size. Values beyond ±32767 are clamped to that range + without changing which pixels are drawn on screen, so `{rect, 0, 0, 100000, 100000, Color}` + still fills the screen. A `rect` with a width or height of 0 or less draws nothing. +- `image` and `scaled_cropped_image` coordinates, sizes, source offsets and scale factors, and + image widths and heights must be within ±32767, or the item is invalid. + +### Invalid Items +An item with a wrong arity, a value of the wrong type or out of range, or an unknown command is +skipped: it draws nothing and the rest of the display list is drawn as usual. Each skipped item is +logged to stderr, one line per item: + +``` +invalid display list item N (Command/Arity): Reason +``` + +`N` is the item's position in the display list, starting at 1. `(Command/Arity)` is left out when +the item is not a tuple, and `Command` is `tuple` when the first element is not an atom. For +example: + +``` +invalid display list item 2 (rect/5): wrong arity +invalid display list item 3: not a command tuple +invalid display list item 4 (tuple/2): unknown command +``` + +At most 3 invalid items are logged per update. If there are more, one more line gives their number, +such as `2 more invalid display list items`. + +An empty display list draws only the background. A display list that is not a proper list is +rejected with `invalid display list: not a proper list` and the update is skipped. ### Transparent The `transparent` atom indicates that no background is drawn for the item's bounding rectangle, @@ -45,6 +77,9 @@ itself. ## scaled_cropped_image Displays a portion of an image with scaling applied. Useful for sprite sheets or zoomed views. +The item is invalid unless `0 <= SourceX < ImageWidth`, `0 <= SourceY < ImageHeight`, the scale +factors are at least 1 and `Width` and `Height` are at least 0. `Width` and `Height` are reduced to +what is left of the image right of and below the source offset, times the scale factor. ```erlang {scaled_cropped_image, @@ -70,7 +105,11 @@ Draws a filled rectangle with the specified color. ## text -Renders text with the specified font and colors. +Renders text with the specified font and colors. `Font` must be an atom, or the item is invalid. +With the built-in font the text is 8 pixels wide per character and 16 pixels high. On builds +without ufont support, a font other than `default16px` falls back to the built-in font and logs +`unsupported font: Font` on every update. With ufont support, a font that was not registered makes +the item invalid. ```erlang {text, @@ -96,8 +135,10 @@ The format tag indicates the pixel format. For example, `rgba8888` means: - Byte order: R, G, B, A - Each component is 8 bits -**Important:** Width and height must exactly match the dimensions of the image data in the binary. -Incorrect values will result in corrupted image display. +Only `rgba8888` is supported. Width and height must be between 1 and 32767 and the binary must hold +at least `Width * Height * 4` bytes, otherwise the item is invalid. Width and height must match the +dimensions of the image data in the binary; wrong values that pass this check draw a corrupted +image. **Tip:** You can convert images to raw RGBA format using ImageMagick: ```bash diff --git a/epaper_display_driver.c b/epaper_display_driver.c index 1c7e940..d719d73 100644 --- a/epaper_display_driver.c +++ b/epaper_display_driver.c @@ -182,21 +182,12 @@ static void do_update(Context *ctx, term display_list) maybe_refresh(ctx); wait_some_time(ctx); - int proper; - int len = term_list_length(display_list, &proper); - - BaseDisplayItem *items = malloc(sizeof(BaseDisplayItem) * len); - if (UNLIKELY(!items)) { - fprintf(stderr, "do_update: failed to alloc items\n"); + BaseDisplayItem *items; + size_t len; + if (UNLIKELY(display_items_new_list(display_list, &items, &len, ctx) != DisplayItemsOk)) { return; } - 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); - } - struct EpaperDriver *driver = EPAPER_DRIVER_FROM_CTX(ctx); int screen_width = driver->screen.w; int screen_height = driver->screen.h; diff --git a/memory_display_driver.c b/memory_display_driver.c index 5f71029..5233fc9 100644 --- a/memory_display_driver.c +++ b/memory_display_driver.c @@ -94,21 +94,12 @@ static inline int next_vcom(struct MemoryLCDDriver *driver) static void do_update(Context *ctx, term display_list) { - int proper; - int len = term_list_length(display_list, &proper); - - BaseDisplayItem *items = malloc(sizeof(BaseDisplayItem) * len); - if (UNLIKELY(!items)) { - fprintf(stderr, "do_update: failed to alloc items\n"); + BaseDisplayItem *items; + size_t len; + if (UNLIKELY(display_items_new_list(display_list, &items, &len, ctx) != DisplayItemsOk)) { return; } - 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); - } - struct MemoryLCDDriver *driver = MEMORY_LCD_DRIVER_FROM_CTX(ctx); int screen_width = driver->screen.w; int screen_height = driver->screen.h; diff --git a/oled_display_driver.c b/oled_display_driver.c index 4935fee..47da592 100644 --- a/oled_display_driver.c +++ b/oled_display_driver.c @@ -96,21 +96,12 @@ static const struct OLEDDesc *oled_desc_for_compatible(const char *compat) static void do_update(Context *ctx, term display_list) { - int proper; - int len = term_list_length(display_list, &proper); - - BaseDisplayItem *items = malloc(sizeof(BaseDisplayItem) * len); - if (UNLIKELY(!items)) { - fprintf(stderr, "do_update: failed to alloc items\n"); + BaseDisplayItem *items; + size_t len; + if (UNLIKELY(display_items_new_list(display_list, &items, &len, ctx) != DisplayItemsOk)) { return; } - 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); - } - int screen_width = DISPLAY_WIDTH; int screen_height = DISPLAY_HEIGHT; struct OLEDDriver *driver = OLED_DRIVER_FROM_CTX(ctx); diff --git a/sdl_display/display.c b/sdl_display/display.c index 8dc0838..c6760d2 100644 --- a/sdl_display/display.c +++ b/sdl_display/display.c @@ -464,23 +464,18 @@ static int draw_x(int xpos, int ypos, BaseDisplayItem items[], size_t items_len) return 1; } -static void do_update(Context *ctx, term display_list) +static bool do_update(Context *ctx, term display_list) { - int proper; - int len = term_list_length(display_list, &proper); - - BaseDisplayItem *items = malloc(sizeof(BaseDisplayItem) * len); - - 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); + BaseDisplayItem *items; + size_t len; + if (UNLIKELY(display_items_new_list(display_list, &items, &len, ctx) != DisplayItemsOk)) { + return false; } struct Rectangle damaged; damaged.valid = false; dumb_diff(prev_items, prev_items_len, items, len, &damaged); - if (prev_items) { + if (prev_message) { display_items_delete(prev_items, prev_items_len); destroy_message(prev_message, ctx->global); } @@ -489,7 +484,7 @@ static void do_update(Context *ctx, term display_list) if (!damaged.valid) { // skip update - return; + return true; } struct Rectangle screen_rect = { @@ -516,6 +511,8 @@ static void do_update(Context *ctx, term display_list) xpos += drawn_pixels; } } + + return true; } static void process_message(Context *ctx) @@ -544,8 +541,9 @@ static void process_message(Context *ctx) if (cmd == globalcontext_make_atom(ctx->global, "\x6" "update")) { term display_list = term_get_tuple_element(req, 1); - do_update(ctx, display_list); - prev_message = message; + if (do_update(ctx, display_list)) { + prev_message = message; + } // Copy and scale up int scale = screen->scale; From 2be76b120cf5c3d35ddba7dab4a05cddafea3963 Mon Sep 17 00:00:00 2001 From: Tom Hoenderdos Date: Tue, 29 Sep 2026 20:02:33 +0200 Subject: [PATCH 3/3] tests: Add host tests for display items Add tests/items, a host test that builds display_items.c and the DCS LCD renderer against AtomVM's headers, with stubs for the few libAtomVM functions they call and for the ESP-IDF SPI header, so the parser can be exercised without a device. The test builds display list items from real AtomVM terms and checks that malformed items become invalid items with every field zeroed, that valid items carry the expected fields, that rect and text positions are clamped as documented, and that invalid items are logged in the documented format. It then renders images and scaled_cropped_images under occluding rects and compares every pixel with a reference computed from the source bytes. It runs under ASan and UBSan by default, with every image binary in an exactly sized allocation, so any read past an image aborts the test. Allocations made by the parser are counted and must all be released by display_items_delete(). Build and run with: cmake -S tests/items -B build/items \ -DLIBATOMVM_INCLUDE_PATH=/src/libAtomVM cmake --build build/items && build/items/test_items Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Tom Hoenderdos --- tests/items/CMakeLists.txt | 52 ++ tests/items/alloc_counter.c | 46 ++ tests/items/alloc_counter.h | 36 ++ tests/items/atomvm_stubs.c | 141 +++++ tests/items/test_items.c | 962 ++++++++++++++++++++++++++++++++ tests/stubs/driver/spi_master.h | 28 + 6 files changed, 1265 insertions(+) create mode 100644 tests/items/CMakeLists.txt create mode 100644 tests/items/alloc_counter.c create mode 100644 tests/items/alloc_counter.h create mode 100644 tests/items/atomvm_stubs.c create mode 100644 tests/items/test_items.c create mode 100644 tests/stubs/driver/spi_master.h diff --git a/tests/items/CMakeLists.txt b/tests/items/CMakeLists.txt new file mode 100644 index 0000000..249c22b --- /dev/null +++ b/tests/items/CMakeLists.txt @@ -0,0 +1,52 @@ +# +# This file is part of AtomGL. +# +# Copyright 2026 Tom Hoenderdos +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +cmake_minimum_required (VERSION 3.13) +project (atomgl_items_tests C) + +set(CMAKE_C_STANDARD 11) +if (NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Debug) +endif() + +if (NOT LIBATOMVM_INCLUDE_PATH) + message(FATAL_ERROR "Set LIBATOMVM_INCLUDE_PATH to AtomVM's src/libAtomVM (headers only, nothing is linked)") +endif() + +option(ITEMS_SANITIZE "Build with ASan and UBSan, aborting on the first report" ON) + +add_executable(test_items test_items.c atomvm_stubs.c alloc_counter.c + ../../display_items.c ../../dcs_lcd_draw.c ../../font_data.c) +set_source_files_properties(../../display_items.c PROPERTIES + COMPILE_OPTIONS "-include;${CMAKE_CURRENT_SOURCE_DIR}/alloc_counter.h") +target_include_directories(test_items PRIVATE ../.. ../stubs ${LIBATOMVM_INCLUDE_PATH}) +target_compile_definitions(test_items PRIVATE HAVE_ATOMIC) +target_compile_options(test_items PRIVATE -Wall -Wextra -Wno-unused-parameter) +if (ITEMS_SANITIZE) + set(SANITIZE_FLAGS -fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer) + target_compile_options(test_items PRIVATE ${SANITIZE_FLAGS}) + target_link_options(test_items PRIVATE ${SANITIZE_FLAGS}) + # READ_32_UNALIGNED left-shifts bytes >= 0x80 into the sign bit + set_source_files_properties(../../dcs_lcd_draw.c PROPERTIES + COMPILE_OPTIONS "-fno-sanitize=shift-base") + # term_from_int left-shifts negative values + set_source_files_properties(test_items.c PROPERTIES + COMPILE_OPTIONS "-fno-sanitize=shift-base") +endif() diff --git a/tests/items/alloc_counter.c b/tests/items/alloc_counter.c new file mode 100644 index 0000000..8411345 --- /dev/null +++ b/tests/items/alloc_counter.c @@ -0,0 +1,46 @@ +/* + * This file is part of AtomGL. + * + * Copyright 2026 Tom Hoenderdos + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +long alloc_counter_outstanding; +long alloc_counter_calls; +long alloc_counter_fail_at = -1; + +void *alloc_counter_malloc(size_t size) +{ + alloc_counter_calls++; + if (alloc_counter_calls == alloc_counter_fail_at) { + return NULL; + } + void *ptr = malloc(size); + if (ptr != NULL) { + alloc_counter_outstanding++; + } + return ptr; +} + +void alloc_counter_free(void *ptr) +{ + if (ptr != NULL) { + alloc_counter_outstanding--; + } + free(ptr); +} diff --git a/tests/items/alloc_counter.h b/tests/items/alloc_counter.h new file mode 100644 index 0000000..13d9827 --- /dev/null +++ b/tests/items/alloc_counter.h @@ -0,0 +1,36 @@ +/* + * This file is part of AtomGL. + * + * Copyright 2026 Tom Hoenderdos + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef _ALLOC_COUNTER_H_ +#define _ALLOC_COUNTER_H_ + +#include + +extern long alloc_counter_outstanding; +extern long alloc_counter_calls; +extern long alloc_counter_fail_at; + +void *alloc_counter_malloc(size_t size); +void alloc_counter_free(void *ptr); + +#define malloc(size) alloc_counter_malloc(size) +#define free(ptr) alloc_counter_free(ptr) + +#endif diff --git a/tests/items/atomvm_stubs.c b/tests/items/atomvm_stubs.c new file mode 100644 index 0000000..02d6fac --- /dev/null +++ b/tests/items/atomvm_stubs.c @@ -0,0 +1,141 @@ +/* + * This file is part of AtomGL. + * + * Copyright 2026 Tom Hoenderdos + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "alloc_counter.h" + +#define FIRST_ATOM_INDEX 256 +#define MAX_ATOMS 64 + +static char *atoms[MAX_ATOMS]; +static int atoms_len; + +enum AtomTableEnsureAtomResult atom_table_ensure_atom(struct AtomTable *table, + const uint8_t *atom_data, size_t atom_len, enum AtomTableCopyOpt opts, atom_index_t *result) +{ + (void) table; + (void) opts; + + for (int i = 0; i < atoms_len; i++) { + if (strlen(atoms[i]) == atom_len && memcmp(atoms[i], atom_data, atom_len) == 0) { + *result = FIRST_ATOM_INDEX + i; + return AtomTableEnsureAtomOk; + } + } + if (atoms_len == MAX_ATOMS) { + abort(); + } + char *copy = calloc(1, atom_len + 1); + if (copy == NULL) { + abort(); + } + memcpy(copy, atom_data, atom_len); + atoms[atoms_len] = copy; + *result = FIRST_ATOM_INDEX + atoms_len++; + + return AtomTableEnsureAtomOk; +} + +int term_display_non_atoms; + +void term_display(FILE *fd, term t, const Context *ctx) +{ + (void) ctx; + if (term_is_atom(t)) { + int index = term_to_atom_index(t) - FIRST_ATOM_INDEX; + if (index >= 0 && index < atoms_len) { + fprintf(fd, "%s", atoms[index]); + return; + } + } else { + term_display_non_atoms++; + } + fprintf(fd, "", (unsigned long long) t); +} + +char *interop_term_to_string(term t, int *ok) +{ + *ok = 0; + if (term_is_binary(t)) { + size_t len = term_binary_size(t); + char *str = malloc(len + 1); + if (str == NULL) { + return NULL; + } + memcpy(str, term_binary_data(t), len); + str[len] = 0; + *ok = 1; + return str; + } + int proper; + int len = term_list_length(t, &proper); + if (!proper) { + return NULL; + } + char *str = malloc(len + 1); + if (str == NULL) { + return NULL; + } + for (int i = 0; i < len; i++) { + term c = term_get_list_head(t); + if (!term_is_integer(c) || term_to_int(c) < 0 || term_to_int(c) > 255) { + free(str); + return NULL; + } + str[i] = (char) term_to_int(c); + t = term_get_list_tail(t); + } + str[len] = 0; + *ok = 1; + return str; +} + +const char *refc_binary_get_data(const struct RefcBinary *ptr) +{ + (void) ptr; + abort(); +} + +term term_alloc_refc_binary(size_t size, bool is_const, Heap *heap, GlobalContext *glb) +{ + (void) glb; + if (!is_const) { + abort(); + } + term *boxed_value = memory_heap_alloc(heap, TERM_BOXED_REFC_BINARY_SIZE); + boxed_value[0] = ((TERM_BOXED_REFC_BINARY_SIZE - 1) << 6) | TERM_BOXED_REFC_BINARY; + boxed_value[1] = (term) size; + boxed_value[2] = (term) RefcBinaryIsConst; + boxed_value[3] = (term) NULL; + boxed_value[4] = term_nil(); + boxed_value[5] = term_nil(); + return ((term) boxed_value) | TERM_PRIMARY_BOXED; +} diff --git a/tests/items/test_items.c b/tests/items/test_items.c new file mode 100644 index 0000000..b5eaad8 --- /dev/null +++ b/tests/items/test_items.c @@ -0,0 +1,962 @@ +/* + * This file is part of AtomGL. + * + * Copyright 2026 Tom Hoenderdos + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#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 "alloc_counter.h" + +extern int term_display_non_atoms; + +#define SCREEN_W 64 +#define SCREEN_H 48 +#define HEAP_TERMS (1 << 16) +#define MAX_BUFFERS 256 + +static term heap_buf[HEAP_TERMS]; +static Heap heap; +static GlobalContext glb; +static Context ctx; + +static void *buffers[MAX_BUFFERS]; +static int buffers_len; + +static int failures; +static int checks; + +#define CHECK(cond, ...) \ + do { \ + checks++; \ + if (!(cond)) { \ + failures++; \ + fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, "\n"); \ + } \ + } while (0) + +static term atom(const char *name) +{ + char buf[256]; + size_t len = strlen(name); + buf[0] = (char) len; + memcpy(buf + 1, name, len); + return globalcontext_make_atom(&glb, (AtomString) buf); +} + +static Heap *heap_with_room(size_t n) +{ + if (heap.heap_ptr + n > heap.heap_end) { + fprintf(stderr, "test heap exhausted\n"); + abort(); + } + return &heap; +} + +static term boxed_int(avm_int64_t v) +{ + return term_make_boxed_int64(v, heap_with_room(1 + BOXED_TERMS_REQUIRED_FOR_INT64)); +} + +static term tuple(int n, ...) +{ + term t = term_alloc_tuple(n, heap_with_room(1 + n)); + va_list ap; + va_start(ap, n); + for (int i = 0; i < n; i++) { + term_put_tuple_element(t, i, va_arg(ap, term)); + } + va_end(ap); + return t; +} + +static term cons(term head, term tail) +{ + return term_list_init_prepend(memory_heap_alloc(heap_with_room(2), 2), head, tail); +} + +static term list(int n, ...) +{ + term elems[64]; + va_list ap; + va_start(ap, n); + for (int i = 0; i < n; i++) { + elems[i] = va_arg(ap, term); + } + va_end(ap); + term l = term_nil(); + for (int i = n - 1; i >= 0; i--) { + l = cons(elems[i], l); + } + return l; +} + +static term pixels(size_t size) +{ + if (buffers_len == MAX_BUFFERS) { + abort(); + } + uint8_t *data = malloc(size ? size : 1); + if (data == NULL) { + abort(); + } + for (size_t i = 0; i < size; i++) { + size_t px = i / 4; + switch (i % 4) { + case 0: + data[i] = (uint8_t) (((px * 13 + 17) % 32) << 3 | 5); + break; + case 1: + data[i] = (uint8_t) ((((px / 32) * 21 + px * 3) % 64) << 2 | 2); + break; + case 2: + data[i] = (uint8_t) (0xFF - 8 * (px % 32)); + break; + default: + data[i] = 0xFF; + } + } + buffers[buffers_len++] = data; + return term_from_const_binary(data, size, &heap, &glb); +} + +static term rgba(avm_int_t w, avm_int_t h, term bin) +{ + return tuple(4, atom("rgba8888"), term_from_int(w), term_from_int(h), bin); +} + +static term sci(avm_int_t w, avm_int_t h, avm_int_t sx, avm_int_t sy, avm_int_t xs, avm_int_t ys, + term opts, term img) +{ + return tuple(12, atom("scaled_cropped_image"), term_from_int(0), term_from_int(0), term_from_int(w), + term_from_int(h), atom("transparent"), term_from_int(sx), term_from_int(sy), term_from_int(xs), term_from_int(ys), + opts, img); +} + +static void delete_item(const BaseDisplayItem *item) +{ + BaseDisplayItem *copy = malloc(sizeof(BaseDisplayItem)); + if (copy == NULL) { + abort(); + } + *copy = *item; + display_items_delete(copy, 1); +} + +static bool is_zeroed(const BaseDisplayItem *item) +{ + BaseDisplayItem zero; + memset(&zero, 0, sizeof(zero)); + return memcmp(item, &zero, sizeof(zero)) == 0; +} + +static term all_items; + +static void expect_invalid(const char *name, term req) +{ + long outstanding = alloc_counter_outstanding; + BaseDisplayItem item; + memset(&item, 0xA5, sizeof(item)); + display_items_init_item(&item, req, &ctx); + CHECK(item.primitive == PrimitiveInvalid && is_zeroed(&item), + "%s: expected an all-zero PrimitiveInvalid item, got primitive %d bbox (%d, %d, %d, %d)", + name, item.primitive, item.x, item.y, item.width, item.height); + CHECK(alloc_counter_outstanding == outstanding, "%s: %ld allocations left after parsing", name, + alloc_counter_outstanding - outstanding); + delete_item(&item); + all_items = cons(req, all_items); +} + +static void expect_invalid_on_alloc_failure(const char *name, term req, int allocations) +{ + for (int n = 1; n <= allocations; n++) { + long outstanding = alloc_counter_outstanding; + BaseDisplayItem item; + memset(&item, 0xA5, sizeof(item)); + alloc_counter_fail_at = alloc_counter_calls + n; + display_items_init_item(&item, req, &ctx); + alloc_counter_fail_at = -1; + CHECK(item.primitive == PrimitiveInvalid && is_zeroed(&item), + "%s, allocation %d failing: primitive %d", name, n, item.primitive); + CHECK(alloc_counter_outstanding == outstanding, "%s, allocation %d failing: %ld allocations left", + name, n, alloc_counter_outstanding - outstanding); + delete_item(&item); + } +} + +static void parse(term req, BaseDisplayItem *item) +{ + display_items_init_item(item, req, &ctx); + all_items = cons(req, all_items); +} + +static void render(term display_list, uint16_t *frame) +{ + BaseDisplayItem *items; + size_t len; + if (display_items_new_list(display_list, &items, &len, &ctx) != DisplayItemsOk) { + fprintf(stderr, "render: display_items_new_list failed\n"); + abort(); + } + + uint16_t line[SCREEN_W]; + struct DCSLCDScreen screen; + memset(&screen, 0, sizeof(screen)); + screen.w = SCREEN_W; + screen.h = SCREEN_H; + screen.pixels = line; + + for (int ypos = 0; ypos < SCREEN_H; ypos++) { + memset(line, 0, sizeof(line)); + int xpos = 0; + while (xpos < SCREEN_W) { + int drawn_pixels = dcs_lcd_draw_x(&screen, xpos, ypos, items, len); + if (drawn_pixels <= 0) { + fprintf(stderr, "dcs_lcd_draw_x returned %d at (%d, %d)\n", drawn_pixels, xpos, ypos); + abort(); + } + xpos += drawn_pixels; + } + if (frame) { + memcpy(frame + ypos * SCREEN_W, line, sizeof(line)); + } + } + + display_items_delete(items, len); +} + +static bool big_ints(void) +{ + return sizeof(avm_int_t) > sizeof(int); +} + +static void test_scaled_cropped_image_invalid(void) +{ + term ok_img = rgba(4, 2, pixels(32)); + + for (int sy = 2; sy <= 4; sy++) { + expect_invalid("source_y >= image height", sci(4, 2, 0, sy, 1, 1, term_nil(), ok_img)); + } + expect_invalid("source_y huge", sci(4, 2, 0, 100000000, 1, 1, term_nil(), ok_img)); + expect_invalid("source_x >= image width", sci(4, 2, 4, 0, 1, 1, term_nil(), ok_img)); + expect_invalid("source_x -1", sci(4, 1, -1, 0, 1, 1, term_nil(), ok_img)); + expect_invalid("source_y -1", sci(4, 2, 0, -1, 1, 1, term_nil(), ok_img)); + expect_invalid("image height 0, empty binary", sci(4, 1, 0, 0, 1, 1, term_nil(), rgba(4, 0, pixels(0)))); + expect_invalid("image width 0", sci(4, 1, 0, 0, 1, 1, term_nil(), rgba(0, 1, pixels(0)))); + expect_invalid("image width -1", sci(4, 1, 0, 0, 1, 1, term_nil(), rgba(-1, 1, pixels(16)))); + expect_invalid("binary shorter than W*H*4", sci(4, 4, 0, 0, 1, 1, term_nil(), rgba(4, 4, pixels(16)))); + expect_invalid("binary one byte short", sci(4, 2, 0, 0, 1, 1, term_nil(), rgba(4, 2, pixels(31)))); + expect_invalid("pixels not a binary", sci(4, 2, 0, 0, 1, 1, term_nil(), rgba(4, 2, term_from_int(0)))); + expect_invalid("unsupported format", + sci(4, 2, 0, 0, 1, 1, term_nil(), tuple(4, atom("rgb565"), term_from_int(4), term_from_int(2), pixels(32)))); + expect_invalid("image tuple arity 3", sci(4, 2, 0, 0, 1, 1, term_nil(), tuple(3, atom("rgba8888"), term_from_int(4), term_from_int(2)))); + expect_invalid("image not a tuple", sci(4, 2, 0, 0, 1, 1, term_nil(), term_from_int(7))); + expect_invalid("x_scale 0", sci(4, 2, 0, 0, 0, 1, term_nil(), ok_img)); + expect_invalid("y_scale 0", sci(4, 2, 0, 0, 1, 0, term_nil(), ok_img)); + expect_invalid("x_scale -2", sci(4, 2, 0, 0, -2, 1, term_nil(), ok_img)); + expect_invalid("width -1", sci(-1, 2, 0, 0, 1, 1, term_nil(), ok_img)); + expect_invalid("x_scale above limit", sci(4, 2, 0, 0, DISPLAY_ITEMS_COORD_LIMIT + 1, 1, term_nil(), ok_img)); + expect_invalid("x not an integer", + tuple(12, atom("scaled_cropped_image"), atom("a"), term_from_int(0), term_from_int(4), term_from_int(2), + atom("transparent"), term_from_int(0), term_from_int(0), term_from_int(1), term_from_int(1), term_nil(), ok_img)); + expect_invalid("bgcolor not an integer", + tuple(12, atom("scaled_cropped_image"), term_from_int(0), term_from_int(0), term_from_int(4), term_from_int(2), + atom("opaque"), term_from_int(0), term_from_int(0), term_from_int(1), term_from_int(1), term_nil(), ok_img)); + expect_invalid("arity 11", + tuple(11, atom("scaled_cropped_image"), term_from_int(0), term_from_int(0), term_from_int(4), term_from_int(2), + atom("transparent"), term_from_int(0), term_from_int(0), term_from_int(1), term_from_int(1), term_nil())); + expect_invalid("arity 13", + tuple(13, atom("scaled_cropped_image"), term_from_int(0), term_from_int(0), term_from_int(4), term_from_int(2), + atom("transparent"), term_from_int(0), term_from_int(0), term_from_int(1), term_from_int(1), term_nil(), ok_img, + term_from_int(0))); + if (big_ints()) { + avm_int_t wrap = ((avm_int_t) 1 << 32) + 1; + expect_invalid("source_y 2^32 + 1", sci(4, 2, 0, wrap, 1, 1, term_nil(), ok_img)); + expect_invalid("y_scale 2^32 + 1", sci(4, 2, 0, 0, 1, wrap, term_nil(), ok_img)); + expect_invalid("image height 2^32 + 1", sci(4, 2, 0, 0, 1, 1, term_nil(), rgba(4, wrap, pixels(32)))); + } +} + +static void test_image_invalid(void) +{ + term ok_img = rgba(4, 2, pixels(32)); + term transparent = atom("transparent"); + + expect_invalid("image height 0, empty binary", + tuple(5, atom("image"), term_from_int(0), term_from_int(0), transparent, rgba(4, 0, pixels(0)))); + expect_invalid("image width 0", + tuple(5, atom("image"), term_from_int(0), term_from_int(0), transparent, rgba(0, 4, pixels(0)))); + expect_invalid("binary shorter than W*H*4", + tuple(5, atom("image"), term_from_int(0), term_from_int(0), transparent, rgba(4, 4, pixels(16)))); + expect_invalid("pixels not a binary", + tuple(5, atom("image"), term_from_int(0), term_from_int(0), transparent, rgba(4, 2, term_from_int(1)))); + expect_invalid("unsupported format", + tuple(5, atom("image"), term_from_int(0), term_from_int(0), transparent, + tuple(4, atom("rgb565"), term_from_int(4), term_from_int(2), pixels(32)))); + expect_invalid("image not a tuple", + tuple(5, atom("image"), term_from_int(0), term_from_int(0), transparent, term_nil())); + expect_invalid("bgcolor not an integer", + tuple(5, atom("image"), term_from_int(0), term_from_int(0), atom("opaque"), ok_img)); + expect_invalid("arity 4", tuple(4, atom("image"), term_from_int(0), term_from_int(0), transparent)); + expect_invalid("arity 6", + tuple(6, atom("image"), term_from_int(0), term_from_int(0), transparent, ok_img, term_from_int(0))); + expect_invalid("x above limit", + tuple(5, atom("image"), term_from_int(DISPLAY_ITEMS_COORD_LIMIT + 1), term_from_int(0), transparent, ok_img)); + if (big_ints()) { + avm_int_t wrap = ((avm_int_t) 1 << 32) + 5; + expect_invalid("x 2^32 + 5", + tuple(5, atom("image"), term_from_int(wrap), term_from_int(0), transparent, ok_img)); + expect_invalid("image width 2^32 + 4", + tuple(5, atom("image"), term_from_int(0), term_from_int(0), transparent, rgba(wrap - 1, 2, pixels(32)))); + } +} + +static void test_command_invalid(void) +{ + expect_invalid("not a tuple", term_from_int(3)); + expect_invalid("empty tuple", tuple(0)); + expect_invalid("unknown command", tuple(2, atom("triangle"), term_from_int(1))); +} + +static void test_image_valid(void) +{ + BaseDisplayItem item; + term bin = pixels(32); + parse(tuple(5, atom("image"), term_from_int(-1), term_from_int(3), term_from_int(0x102030), rgba(4, 2, bin)), &item); + CHECK(item.primitive == PrimitiveImage, "image: primitive %d", item.primitive); + CHECK(item.x == -1 && item.y == 3 && item.width == 4 && item.height == 2, "image: bbox (%d, %d, %d, %d)", + item.x, item.y, item.width, item.height); + CHECK(item.brcolor == 0x102030FF, "image: brcolor %#x", (unsigned) item.brcolor); + CHECK(item.data.image_data.pix == term_binary_data(bin), "image: pixel pointer"); + delete_item(&item); + + parse(tuple(5, atom("image"), term_from_int(10), term_from_int(10), atom("transparent"), rgba(2, 2, pixels(20))), &item); + CHECK(item.primitive == PrimitiveImage && item.brcolor == 0, "image with spare bytes: primitive %d", + item.primitive); + delete_item(&item); +} + +static void test_scaled_cropped_image_valid(void) +{ + BaseDisplayItem item; + term bin = pixels(32); + parse(tuple(12, atom("scaled_cropped_image"), term_from_int(5), term_from_int(6), term_from_int(7), term_from_int(8), + term_from_int(0xABCDEF), term_from_int(3), term_from_int(1), term_from_int(2), term_from_int(3), term_nil(), + rgba(4, 2, bin)), + &item); + CHECK(item.primitive == PrimitiveScaledCroppedImage, "sci: primitive %d", item.primitive); + CHECK(item.x == 5 && item.y == 6 && item.width == 2 && item.height == 3, "sci: bbox (%d, %d, %d, %d)", + item.x, item.y, item.width, item.height); + CHECK(item.brcolor == 0xABCDEFFF, "sci: brcolor %#x", (unsigned) item.brcolor); + CHECK(item.source_x == 3 && item.source_y == 1 && item.x_scale == 2 && item.y_scale == 3, + "sci: source (%d, %d) scale (%d, %d)", item.source_x, item.source_y, item.x_scale, item.y_scale); + CHECK(item.data.image_data_with_size.width == 4 && item.data.image_data_with_size.height == 2 + && item.data.image_data_with_size.pix == term_binary_data(bin), + "sci: image data"); + delete_item(&item); + + parse(sci(9, 9, 3, 1, 3, 3, term_nil(), rgba(4, 2, pixels(32))), &item); + CHECK(item.primitive == PrimitiveScaledCroppedImage, "sci at last pixel: primitive %d", item.primitive); + delete_item(&item); +} + +static void test_integer_forms(void) +{ + BaseDisplayItem item; + term transparent = atom("transparent"); + term ok_img = rgba(4, 2, pixels(32)); + + parse(tuple(5, atom("image"), term_from_int(0), term_from_int(0), term_from_int(0x7F102030), ok_img), &item); + CHECK(item.primitive == PrimitiveImage && item.brcolor == 0x102030FF, "color above 24 bits: brcolor %#x", + (unsigned) item.brcolor); + delete_item(&item); + parse(tuple(5, atom("image"), term_from_int(0), term_from_int(0), boxed_int(((avm_int64_t) 0xABCD << 32) | 0xFF102030), ok_img), + &item); + CHECK(item.primitive == PrimitiveImage && item.brcolor == 0x102030FF, "boxed color: brcolor %#x", + (unsigned) item.brcolor); + delete_item(&item); + parse(tuple(5, atom("image"), term_from_int(0), term_from_int(0), term_from_int(-1), ok_img), &item); + CHECK(item.primitive == PrimitiveImage && item.brcolor == 0xFFFFFFFF, "color -1: brcolor %#x", + (unsigned) item.brcolor); + delete_item(&item); + + parse(tuple(5, atom("image"), boxed_int(-7), boxed_int(9), transparent, ok_img), &item); + CHECK(item.primitive == PrimitiveImage && item.x == -7 && item.y == 9, "boxed position: primitive %d (%d, %d)", + item.primitive, item.x, item.y); + delete_item(&item); + expect_invalid("image x boxed 2^31", + tuple(5, atom("image"), boxed_int((avm_int64_t) 1 << 31), term_from_int(0), transparent, ok_img)); +} + +static term text_binary(const char *text) +{ + if (buffers_len == MAX_BUFFERS) { + abort(); + } + size_t len = strlen(text); + char *data = malloc(len ? len : 1); + if (data == NULL) { + abort(); + } + memcpy(data, text, len); + buffers[buffers_len++] = data; + return term_from_const_binary(data, len, &heap, &glb); +} + +static term string(const char *text) +{ + term l = term_nil(); + for (int i = (int) strlen(text) - 1; i >= 0; i--) { + l = cons(term_from_int((unsigned char) text[i]), l); + } + return l; +} + +static term rect(avm_int_t x, avm_int_t y, avm_int_t w, avm_int_t h, avm_int_t color) +{ + return tuple(6, atom("rect"), term_from_int(x), term_from_int(y), term_from_int(w), term_from_int(h), term_from_int(color)); +} + +static void test_rect(void) +{ + term c = term_from_int(0x00FF00); + BaseDisplayItem item; + + parse(rect(-3, 4, 30, 20, 0xA0B0C0), &item); + CHECK(item.primitive == PrimitiveRect, "rect: primitive %d", item.primitive); + CHECK(item.x == -3 && item.y == 4 && item.width == 30 && item.height == 20, "rect: bbox (%d, %d, %d, %d)", + item.x, item.y, item.width, item.height); + CHECK(item.brcolor == 0xA0B0C0FF, "rect: brcolor %#x", (unsigned) item.brcolor); + delete_item(&item); + + const int L = DISPLAY_ITEMS_COORD_LIMIT; + avm_int64_t huge = (avm_int64_t) 1 << 40; + struct + { + term x, y, w, h; + int cx, cy, cw, ch; + } clamps[] = { + { term_from_int(0), term_from_int(0), term_from_int(100000), term_from_int(100000), 0, 0, L, L }, + { term_from_int(-100000), term_from_int(-50000), term_from_int(200000), term_from_int(50100), -L, -L, 2 * L, L + 100 }, + { term_from_int(L), term_from_int(-L), term_from_int(L), term_from_int(-L), L, -L, 0, 0 }, + { term_from_int(L + 1), term_from_int(-L - 1), term_from_int(4), term_from_int(4), L, -L, 0, 3 }, + { term_from_int(3), term_from_int(4), term_from_int(-5), term_from_int(0), 3, 4, 0, 0 }, + { boxed_int(-huge), boxed_int(huge), boxed_int(2 * huge), term_from_int(1), -L, L, 2 * L, 0 }, + { boxed_int(INT64_MAX), boxed_int(INT64_MIN), boxed_int(INT64_MAX), boxed_int(INT64_MAX), L, -L, 0, L - 1 }, + }; + for (size_t i = 0; i < sizeof(clamps) / sizeof(clamps[0]); i++) { + parse(tuple(6, atom("rect"), clamps[i].x, clamps[i].y, clamps[i].w, clamps[i].h, c), &item); + CHECK(item.primitive == PrimitiveRect && item.x == clamps[i].cx && item.y == clamps[i].cy + && item.width == clamps[i].cw && item.height == clamps[i].ch, + "clamped rect %zu: primitive %d bbox (%d, %d, %d, %d)", i, item.primitive, item.x, item.y, item.width, + item.height); + delete_item(&item); + } + + expect_invalid("{rect}", tuple(1, atom("rect"))); + expect_invalid("rect arity 5", tuple(5, atom("rect"), term_from_int(0), term_from_int(0), term_from_int(4), term_from_int(4))); + expect_invalid("rect arity 7", + tuple(7, atom("rect"), term_from_int(0), term_from_int(0), term_from_int(4), term_from_int(4), c, c)); + expect_invalid("rect x not an integer", + tuple(6, atom("rect"), atom("a"), term_from_int(0), term_from_int(4), term_from_int(4), c)); + expect_invalid("rect height not an integer", + tuple(6, atom("rect"), term_from_int(0), term_from_int(0), term_from_int(4), term_nil(), c)); + expect_invalid("rect color not an integer", + tuple(6, atom("rect"), term_from_int(0), term_from_int(0), term_from_int(4), term_from_int(4), atom("red"))); +} + +static term text(term x, term font, term fg, term bg, term str) +{ + return tuple(7, atom("text"), x, term_from_int(5), font, fg, bg, str); +} + +static void test_text(void) +{ + term font = atom("default16px"); + term fg = term_from_int(0x102030); + term transparent = atom("transparent"); + BaseDisplayItem item; + + parse(text(term_from_int(-4), font, fg, transparent, text_binary("hi")), &item); + CHECK(item.primitive == PrimitiveText, "text: primitive %d", item.primitive); + CHECK(item.x == -4 && item.y == 5 && item.width == 16 && item.height == 16, "text: bbox (%d, %d, %d, %d)", + item.x, item.y, item.width, item.height); + CHECK(item.brcolor == 0 && item.data.text_data.fgcolor == 0x102030FF, "text: colors %#x %#x", + (unsigned) item.brcolor, (unsigned) item.data.text_data.fgcolor); + CHECK(item.data.text_data.text && strcmp(item.data.text_data.text, "hi") == 0, "text: string"); + delete_item(&item); + + parse(text(term_from_int(1), font, fg, term_from_int(0xFFEEDD), string("abc")), &item); + CHECK(item.primitive == PrimitiveText && item.width == 24 && item.brcolor == 0xFFEEDDFF, + "text from a list: primitive %d width %d brcolor %#x", item.primitive, item.width, (unsigned) item.brcolor); + delete_item(&item); + + parse(text(term_from_int(1), atom("nofont"), fg, transparent, string("ab")), &item); + CHECK(item.primitive == PrimitiveText && item.width == 16, "text with unknown font: primitive %d width %d", + item.primitive, item.width); + delete_item(&item); + + const int L = DISPLAY_ITEMS_COORD_LIMIT; + parse(text(term_from_int(L + 1), font, fg, transparent, string("ab")), &item); + CHECK(item.primitive == PrimitiveText && item.x == L && item.width == 0, "text right of the limit: (%d, %d)", + item.x, item.width); + delete_item(&item); + parse(text(term_from_int(L - 10), font, fg, transparent, string("abc")), &item); + CHECK(item.primitive == PrimitiveText && item.x == L - 10 && item.width == 10, "text across the limit: (%d, %d)", + item.x, item.width); + delete_item(&item); + parse(tuple(7, atom("text"), term_from_int(0), boxed_int(INT64_MIN), font, fg, transparent, string("a")), &item); + CHECK(item.primitive == PrimitiveText && item.y == -L && item.x == 0 && item.width == 8, "text far above: (%d, %d)", + item.x, item.y); + delete_item(&item); + parse(text(term_from_int(-L - 20), font, fg, transparent, string("abcdefgh")), &item); + CHECK(item.primitive == PrimitiveText && item.x == -L + 4 && item.width == 40 + && strcmp(item.data.text_data.text, "defgh") == 0, + "text across -L: (%d, %d) \"%s\"", item.x, item.width, item.data.text_data.text); + delete_item(&item); + parse(text(boxed_int(INT64_MIN), font, fg, transparent, string("ab")), &item); + CHECK(item.primitive == PrimitiveText && item.x == -L && item.width == 0, "text far left: (%d, %d)", item.x, + item.width); + delete_item(&item); + + expect_invalid("{text}", tuple(1, atom("text"))); + expect_invalid("text arity 6", + tuple(6, atom("text"), term_from_int(0), term_from_int(0), font, fg, transparent)); + expect_invalid("text arity 8", + tuple(8, atom("text"), term_from_int(0), term_from_int(0), font, fg, transparent, string("a"), term_from_int(0))); + expect_invalid("text x not an integer", text(atom("a"), font, fg, transparent, string("a"))); + expect_invalid("text color not an integer", text(term_from_int(0), font, atom("red"), transparent, string("a"))); + expect_invalid("text bgcolor not an integer", text(term_from_int(0), font, fg, atom("opaque"), string("a"))); + expect_invalid("text font a tuple", text(term_from_int(0), tuple(1, atom("x")), fg, transparent, string("a"))); + expect_invalid("text font an integer", text(term_from_int(0), term_from_int(16), fg, transparent, string("a"))); + expect_invalid("text font a binary", text(term_from_int(0), text_binary("default16px"), fg, transparent, string("a"))); + expect_invalid("text not a string", text(term_from_int(0), font, fg, transparent, term_from_int(3))); + expect_invalid("text improper list", text(term_from_int(0), font, fg, transparent, cons(term_from_int('a'), term_from_int(3)))); + expect_invalid_on_alloc_failure("text", text(term_from_int(0), font, fg, transparent, string("abc")), 1); +} + +static term *heap_mark(void) +{ + return heap.heap_ptr; +} + +static void heap_release(term *mark) +{ + heap.heap_ptr = mark; +} + +static uint16_t expected_color(const uint8_t *rgba) +{ + uint16_t c = (uint16_t) ((rgba[0] >> 3) << 11 | (rgba[1] >> 2) << 5 | (rgba[2] >> 3)); + return (uint16_t) (c >> 8 | c << 8); +} + +#define OCCLUDER_COLOR 0x3060A0 + +static term add_occluders(term display_list, uint16_t *expected, int x, int y, int w, int h, int seed) +{ + uint8_t rgb[3] = { (OCCLUDER_COLOR >> 16) & 0xFF, (OCCLUDER_COLOR >> 8) & 0xFF, OCCLUDER_COLOR & 0xFF }; + uint16_t color = expected_color(rgb); + for (int r = 0; r < h; r++) { + if ((r + seed) % 4 == 3) { + continue; + } + int rx = x + (r * 5 + seed) % w; + int rw = 1 + (r + seed) % 3; + display_list = cons(rect(rx, y + r, rw, 1, OCCLUDER_COLOR), display_list); + for (int px = rx; px < rx + rw; px++) { + if (px >= 0 && px < SCREEN_W && y + r >= 0 && y + r < SCREEN_H) { + expected[(y + r) * SCREEN_W + px] = color; + } + } + } + return display_list; +} + +static void compare_frames(const char *name, const uint16_t *expected, const uint16_t *frame) +{ + int wrong = 0; + int first = -1; + for (int i = 0; i < SCREEN_W * SCREEN_H; i++) { + if (expected[i] != frame[i]) { + if (first < 0) { + first = i; + } + wrong++; + } + } + CHECK(wrong == 0, "%s: %d pixels differ, first at (%d, %d): %#06x, expected %#06x", name, wrong, + first % SCREEN_W, first / SCREEN_W, frame[first < 0 ? 0 : first], expected[first < 0 ? 0 : first]); +} + +static void check_image_pixels(int x, int y, int w, int h, int seed) +{ + static uint16_t expected[SCREEN_W * SCREEN_H]; + static uint16_t frame[SCREEN_W * SCREEN_H]; + term *mark = heap_mark(); + term bin = pixels(w * h * 4); + const uint8_t *bytes = (const uint8_t *) term_binary_data(bin); + + memset(expected, 0, sizeof(expected)); + for (int py = 0; py < h; py++) { + for (int px = 0; px < w; px++) { + if (x + px >= 0 && x + px < SCREEN_W && y + py >= 0 && y + py < SCREEN_H) { + expected[(y + py) * SCREEN_W + x + px] = expected_color(bytes + 4 * (py * w + px)); + } + } + } + term display_list = list(1, + tuple(5, atom("image"), term_from_int(x), term_from_int(y), atom("transparent"), rgba(w, h, bin))); + display_list = add_occluders(display_list, expected, x, y, w, h, seed); + render(display_list, frame); + + char name[96]; + snprintf(name, sizeof(name), "image %dx%d at (%d, %d)", w, h, x, y); + compare_frames(name, expected, frame); + heap_release(mark); +} + +static FILE *capture; +static int saved_stderr; + +static void begin_capture(void) +{ + capture = tmpfile(); + if (capture == NULL) { + abort(); + } + fflush(stderr); + saved_stderr = dup(STDERR_FILENO); + dup2(fileno(capture), STDERR_FILENO); +} + +static void end_capture(char *out, size_t out_size) +{ + fflush(stderr); + dup2(saved_stderr, STDERR_FILENO); + close(saved_stderr); + rewind(capture); + size_t n = fread(out, 1, out_size - 1, capture); + out[n] = 0; + fclose(capture); +} + +static display_items_result_t capture_new_list(term display_list, char *out, size_t out_size) +{ + BaseDisplayItem *items = NULL; + size_t len = 0; + begin_capture(); + display_items_result_t result = display_items_new_list(display_list, &items, &len, &ctx); + end_capture(out, out_size); + if (result == DisplayItemsOk) { + display_items_delete(items, len); + } + + return result; +} + +static void test_log_format(void) +{ + static char log[4096]; + term *mark = heap_mark(); + term ok = rect(0, 0, 4, 4, 0); + term bad_rect = tuple(5, atom("rect"), term_from_int(0), term_from_int(0), term_from_int(4), term_from_int(4)); + + capture_new_list(list(4, ok, bad_rect, term_from_int(3), tuple(2, term_from_int(1), term_from_int(2))), log, sizeof(log)); + const char *expected = "invalid display list item 2 (rect/5): wrong arity\n" + "invalid display list item 3: not a command tuple\n" + "invalid display list item 4 (tuple/2): unknown command\n"; + CHECK(strcmp(log, expected) == 0, "log of 3 invalid items:\n%s", log); + + capture_new_list(list(6, tuple(0), ok, tuple(1, atom("triangle")), bad_rect, bad_rect, bad_rect), log, + sizeof(log)); + expected = "invalid display list item 1 (tuple/0): not a command tuple\n" + "invalid display list item 3 (triangle/1): unknown command\n" + "invalid display list item 4 (rect/5): wrong arity\n" + "2 more invalid display list items\n"; + CHECK(strcmp(log, expected) == 0, "log of 5 invalid items:\n%s", log); + + capture_new_list(list(2, ok, ok), log, sizeof(log)); + CHECK(log[0] == 0, "log of a valid list:\n%s", log); + + term many = term_nil(); + for (int i = 0; i < 40; i++) { + many = cons(term_from_int(i), many); + } + capture_new_list(many, log, sizeof(log)); + expected = "invalid display list item 1: not a command tuple\n" + "invalid display list item 2: not a command tuple\n" + "invalid display list item 3: not a command tuple\n" + "37 more invalid display list items\n"; + CHECK(strcmp(log, expected) == 0, "log of 40 invalid items:\n%s", log); + + BaseDisplayItem item; + begin_capture(); + display_items_init_item(&item, bad_rect, &ctx); + end_capture(log, sizeof(log)); + CHECK(strcmp(log, "invalid display list item (rect/5): wrong arity\n") == 0, "log of one item:\n%s", log); + heap_release(mark); +} + +static void test_new_list(void) +{ + static char log[4096]; + term *mark = heap_mark(); + term ok = rect(0, 0, 4, 4, 0); + BaseDisplayItem *items = (BaseDisplayItem *) 1; + size_t len = 1; + + long outstanding = alloc_counter_outstanding; + display_items_result_t result = display_items_new_list(term_nil(), &items, &len, &ctx); + CHECK(result == DisplayItemsOk && items == NULL && len == 0, "empty list: result %d, %zu items", result, len); + CHECK(alloc_counter_outstanding == outstanding, "empty list: %ld allocations", + alloc_counter_outstanding - outstanding); + display_items_delete(items, len); + + result = display_items_new_list(list(2, ok, ok), &items, &len, &ctx); + CHECK(result == DisplayItemsOk && items != NULL && len == 2 && items[1].primitive == PrimitiveRect, + "list of 2: result %d, %zu items", result, len); + display_items_delete(items, len); + + result = capture_new_list(cons(ok, term_from_int(1)), log, sizeof(log)); + CHECK(result == DisplayItemsNotAProperList && strcmp(log, "invalid display list: not a proper list\n") == 0, + "improper list: result %d, log:\n%s", result, log); + result = capture_new_list(ok, log, sizeof(log)); + CHECK(result == DisplayItemsNotAProperList, "tuple instead of a list: result %d", result); + result = capture_new_list(atom("rect"), log, sizeof(log)); + CHECK(result == DisplayItemsNotAProperList, "atom instead of a list: result %d", result); + + alloc_counter_fail_at = alloc_counter_calls + 1; + result = capture_new_list(list(1, ok), log, sizeof(log)); + alloc_counter_fail_at = -1; + CHECK(result == DisplayItemsOutOfMemory && strcmp(log, "failed to allocate display list items\n") == 0, + "allocation failure: result %d, log:\n%s", result, log); + CHECK(alloc_counter_outstanding == outstanding, "after errors: %ld allocations", + alloc_counter_outstanding - outstanding); + heap_release(mark); +} + +static void test_huge_rect_pixels(void) +{ + static uint16_t expected[SCREEN_W * SCREEN_H]; + static uint16_t frame[SCREEN_W * SCREEN_H]; + const uint8_t rgb[3] = { 0x12, 0x34, 0x56 }; + term *mark = heap_mark(); + for (int i = 0; i < SCREEN_W * SCREEN_H; i++) { + expected[i] = expected_color(rgb); + } + render(list(1, rect(0, 0, 100000, 100000, 0x123456)), frame); + compare_frames("rect 100000x100000", expected, frame); + avm_int64_t huge = (avm_int64_t) 1 << 40; + term req = tuple(6, atom("rect"), boxed_int(-huge), term_from_int(-100000), boxed_int(2 * huge), term_from_int(200000), + term_from_int(0x123456)); + render(list(1, req), frame); + compare_frames("rect from -2^40 to 2^40", expected, frame); + heap_release(mark); +} + +static void test_image_pixels(void) +{ + check_image_pixels(3, 2, 5, 4, 0); + check_image_pixels(-2, -1, 7, 5, 1); + check_image_pixels(SCREEN_W - 4, SCREEN_H - 3, 6, 5, 2); +} + +struct Sprite +{ + int x; + int y; + int width; + int height; + int source_x; + int source_y; + int x_scale; + int y_scale; + int img_width; + int img_height; + const uint8_t *bytes; +}; + +static int min_int(int a, int b) +{ + return (a < b) ? a : b; +} + +static int sprite_drawn_width(const struct Sprite *s) +{ + return min_int(s->width, (s->img_width - s->source_x) * s->x_scale); +} + +static int sprite_drawn_height(const struct Sprite *s) +{ + return min_int(s->height, (s->img_height - s->source_y) * s->y_scale); +} + +static bool sprite_pixel(const struct Sprite *s, int px, int py, uint16_t *color) +{ + int drawn_w = sprite_drawn_width(s); + int drawn_h = sprite_drawn_height(s); + if (px < 0 || py < 0 || px >= drawn_w || py >= drawn_h) { + return false; + } + int c = px / s->x_scale; + int r = py / s->y_scale; + *color = expected_color(s->bytes + 4 * ((s->source_y + r) * s->img_width + s->source_x + c)); + return true; +} + +static void check_sprite_pixels(const struct Sprite *s, int seed) +{ + static uint16_t expected[SCREEN_W * SCREEN_H]; + static uint16_t frame[SCREEN_W * SCREEN_H]; + char name[160]; + snprintf(name, sizeof(name), + "sprite %dx%d from (%d, %d) of %dx%d, scale %dx%d, at (%d, %d)", s->width, s->height, + s->source_x, s->source_y, s->img_width, s->img_height, s->x_scale, s->y_scale, s->x, s->y); + + memset(expected, 0, sizeof(expected)); + for (int y = 0; y < SCREEN_H; y++) { + for (int x = 0; x < SCREEN_W; x++) { + sprite_pixel(s, x - s->x, y - s->y, &expected[y * SCREEN_W + x]); + } + } + + term *mark = heap_mark(); + term bin = term_from_const_binary(s->bytes, s->img_width * s->img_height * 4, &heap, &glb); + term req = tuple(12, atom("scaled_cropped_image"), term_from_int(s->x), term_from_int(s->y), term_from_int(s->width), + term_from_int(s->height), atom("transparent"), term_from_int(s->source_x), term_from_int(s->source_y), + term_from_int(s->x_scale), term_from_int(s->y_scale), term_nil(), + rgba(s->img_width, s->img_height, bin)); + + BaseDisplayItem item; + display_items_init_item(&item, req, &ctx); + CHECK(item.primitive == PrimitiveScaledCroppedImage && item.width == sprite_drawn_width(s) + && item.height == sprite_drawn_height(s), + "%s: primitive %d, size %dx%d, expected %dx%d", name, item.primitive, item.width, item.height, + sprite_drawn_width(s), sprite_drawn_height(s)); + delete_item(&item); + + term display_list = add_occluders(list(1, req), expected, s->x, s->y, sprite_drawn_width(s), + sprite_drawn_height(s), seed); + render(display_list, frame); + compare_frames(name, expected, frame); + heap_release(mark); +} + +static void test_sprite_pixels(void) +{ + enum + { + ImgW = 6, + ImgH = 5 + }; + term bin = pixels(ImgW * ImgH * 4); + const uint8_t *bytes = (const uint8_t *) term_binary_data(bin); + int seed = 0; + + for (int sx = 0; sx <= 2; sx += 2) { + for (int sy = 0; sy <= 1; sy++) { + for (int crop = 0; crop < 3; crop++) { + for (int xs = 1; xs <= 3; xs++) { + for (int ys = 1; ys <= 3; ys++) { + int rest_w = ImgW - sx; + int rest_h = ImgH - sy; + struct Sprite s; + memset(&s, 0, sizeof(s)); + s.img_width = ImgW; + s.img_height = ImgH; + s.bytes = bytes; + s.source_x = sx; + s.source_y = sy; + s.x_scale = xs; + s.y_scale = ys; + if (crop == 0) { + s.width = (rest_w - 2) * xs - (xs - 1); + s.height = (rest_h - 2) * ys - (ys - 1); + } else if (crop == 1) { + s.width = rest_w * xs; + s.height = rest_h * ys; + } else { + s.width = rest_w * xs + 3; + s.height = rest_h * ys + 2 * ys + 1; + } + s.x = (seed % 5 == 4) ? -3 : 2 + seed % 7; + s.y = (seed % 5 == 4) ? -2 : 1 + seed % 5; + check_sprite_pixels(&s, seed); + seed++; + } + } + } + } + } +} + +int main(void) +{ + heap.heap_start = heap_buf; + heap.heap_ptr = heap_buf; + heap.heap_end = heap_buf + HEAP_TERMS; + ctx.global = &glb; + all_items = term_nil(); + + test_scaled_cropped_image_invalid(); + test_image_invalid(); + test_command_invalid(); + test_image_valid(); + test_scaled_cropped_image_valid(); + test_integer_forms(); + test_rect(); + test_text(); + test_log_format(); + test_new_list(); + test_huge_rect_pixels(); + test_image_pixels(); + test_sprite_pixels(); + + render(all_items, NULL); + + CHECK(term_display_non_atoms == 0, "%d terms other than atoms were logged", term_display_non_atoms); + + for (int i = 0; i < buffers_len; i++) { + free(buffers[i]); + } + CHECK(alloc_counter_outstanding == 0, "%ld allocations were not freed", alloc_counter_outstanding); + + if (failures) { + fprintf(stderr, "%d of %d checks failed\n", failures, checks); + return 1; + } + printf("all %d display item checks passed\n", checks); + return 0; +} diff --git a/tests/stubs/driver/spi_master.h b/tests/stubs/driver/spi_master.h new file mode 100644 index 0000000..a0581ca --- /dev/null +++ b/tests/stubs/driver/spi_master.h @@ -0,0 +1,28 @@ +/* + * This file is part of AtomGL. + * + * Copyright 2026 Tom Hoenderdos + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef _TESTS_SPI_MASTER_STUB_H_ +#define _TESTS_SPI_MASTER_STUB_H_ + +#include + +#define SPI_SWAP_DATA_TX(data, len) ((uint16_t) ((((data) >> 8) & 0xFF) | (((data) & 0xFF) << 8))) + +#endif