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