diff --git a/CMakeLists.txt b/CMakeLists.txt index 39707ae..5fe14cb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,7 @@ idf_component_register(SRCS "ufontlib.c" "backlight_gpio.c" "image_helpers.c" + "shape.c" "spng.c" "miniz.c" PRIV_REQUIRES "libatomvm" "avm_sys" "avm_builtins" "driver" "sdmmc" "vfs" "fatfs" @@ -57,6 +58,13 @@ idf_component_register(SRCS target_compile_definitions(${COMPONENT_TARGET} PRIVATE "-DSPNG_USE_MINIZ=1" "-DENABLE_UFONT") +# Not in PRIV_REQUIRES: requirements are collected before -D variables are visible +if (ATOMGL_PROFILE) + target_compile_definitions(${COMPONENT_TARGET} PRIVATE "-DATOMGL_PROFILE") + idf_component_get_property(esp_timer_lib esp_timer COMPONENT_LIB) + target_link_libraries(${COMPONENT_LIB} PRIVATE ${esp_timer_lib}) +endif() + if (IDF_VERSION_MAJOR EQUAL 4) idf_build_set_property( LINK_OPTIONS "-Wl,--whole-archive ${CMAKE_CURRENT_BINARY_DIR}/lib${COMPONENT_NAME}.a -Wl,--no-whole-archive" diff --git a/README.Md b/README.Md index fb3c950..f129067 100644 --- a/README.Md +++ b/README.Md @@ -196,6 +196,7 @@ AtomGL supports the following primitive types: * **scaled_cropped_image** - Display scaled and cropped images * **rect** - Draw filled rectangles * **text** - Render text with specified font +* **rounded_rect**, **line**, **circle**, **ellipse**, **arc**, **polygon** - Draw filled shapes Example display list with multiple primitives: diff --git a/dcs_lcd_color.h b/dcs_lcd_color.h index cb23e1f..da9ac0c 100644 --- a/dcs_lcd_color.h +++ b/dcs_lcd_color.h @@ -36,12 +36,7 @@ static inline uint16_t alpha_blend_rgb565(uint32_t fg, uint32_t bg, uint8_t alph return (uint16_t) ((result >> 16) | result); } -static inline uint8_t rgba8888_get_alpha(uint32_t color) -{ - return color & 0xFF; -} - -static inline uint16_t rgba8888_color_to_rgb565(uint32_t color) +static inline __attribute__((always_inline)) uint16_t rgba8888_color_to_rgb565(uint32_t color) { uint8_t r = color >> 24; uint8_t g = (color >> 16) & 0xFF; @@ -50,12 +45,12 @@ static inline uint16_t rgba8888_color_to_rgb565(uint32_t color) return (((uint16_t) (r >> 3)) << 11) | (((uint16_t) (g >> 2)) << 5) | ((uint16_t) b >> 3); } -static inline uint16_t rgb565_color_to_surface(uint16_t color16) +static inline __attribute__((always_inline)) uint16_t rgb565_color_to_surface(uint16_t color16) { return (uint16_t) SPI_SWAP_DATA_TX(color16, 16); } -static inline uint16_t uint32_color_to_surface(uint32_t color) +static inline __attribute__((always_inline)) uint16_t uint32_color_to_surface(uint32_t color) { uint16_t color16 = rgba8888_color_to_rgb565(color); diff --git a/dcs_lcd_display_driver.c b/dcs_lcd_display_driver.c index 3ab2db5..7ba1505 100644 --- a/dcs_lcd_display_driver.c +++ b/dcs_lcd_display_driver.c @@ -61,6 +61,10 @@ #include "spi_dc_driver.h" #include "spi_display.h" +#ifdef ATOMGL_PROFILE +#include +#endif + #define SPI_MODE 0 #include "font_data.h" @@ -139,11 +143,18 @@ static void display_init_using_list(struct DCSLCDDriver *driver, term init_list) static void do_update(Context *ctx, term display_list) { +#ifdef ATOMGL_PROFILE + int64_t frame_start = esp_timer_get_time(); +#endif + BaseDisplayItem *items; size_t len; if (UNLIKELY(display_items_new_list(display_list, &items, &len, ctx) != DisplayItemsOk)) { return; } +#ifdef ATOMGL_PROFILE + int64_t parse_us = esp_timer_get_time() - frame_start; +#endif struct DCSLCDDriver *driver = DCS_LCD_DRIVER_FROM_CTX(ctx); int screen_width = driver->screen.w; @@ -155,19 +166,41 @@ static void do_update(Context *ctx, term display_list) bool transaction_in_progress = false; +#ifdef ATOMGL_PROFILE + int64_t draw_us = 0; + int64_t max_line_us = 0; + int64_t spi_wait_us = 0; +#endif + for (int ypos = 0; ypos < screen_height; ypos++) { +#ifdef ATOMGL_PROFILE + int64_t line_start = esp_timer_get_time(); +#endif BaseDisplayItem *row = display_items_row(items, len, ypos); int xpos = 0; while (xpos < screen_width) { int drawn_pixels = dcs_lcd_draw_x(&driver->screen, xpos, ypos, row); xpos += drawn_pixels; } +#ifdef ATOMGL_PROFILE + int64_t line_us = esp_timer_get_time() - line_start; + draw_us += line_us; + if (line_us > max_line_us) { + max_line_us = line_us; + } +#endif if (transaction_in_progress) { spi_transaction_t *trans; // I did a quick measurement, and most of the time is spent waiting for DMA transaction // eg. 23 us spent in draw_x, 188 us spent in spi_device_get_trans_result +#ifdef ATOMGL_PROFILE + int64_t wait_start = esp_timer_get_time(); +#endif spi_device_get_trans_result(driver->bus.spi_disp.handle, &trans, portMAX_DELAY); +#ifdef ATOMGL_PROFILE + spi_wait_us += esp_timer_get_time() - wait_start; +#endif } // Swap scanline buffers. @@ -191,12 +224,25 @@ static void do_update(Context *ctx, term display_list) if (transaction_in_progress) { spi_transaction_t *trans; +#ifdef ATOMGL_PROFILE + int64_t wait_start = esp_timer_get_time(); +#endif spi_device_get_trans_result(driver->bus.spi_disp.handle, &trans, portMAX_DELAY); +#ifdef ATOMGL_PROFILE + spi_wait_us += esp_timer_get_time() - wait_start; +#endif } spi_device_release_bus(driver->bus.spi_disp.handle); display_items_delete(items, len); + +#ifdef ATOMGL_PROFILE + fprintf(stderr, + "atomgl: %zu items, parse %lld us, draw %lld us, max line %lld us, spi wait %lld us, frame %lld us\n", + len, (long long) parse_us, (long long) draw_us, (long long) max_line_us, + (long long) spi_wait_us, (long long) (esp_timer_get_time() - frame_start)); +#endif } static void process_message(Message *message, Context *ctx) diff --git a/dcs_lcd_draw.c b/dcs_lcd_draw.c index 32d40f1..df911f7 100644 --- a/dcs_lcd_draw.c +++ b/dcs_lcd_draw.c @@ -20,6 +20,7 @@ #include "dcs_lcd_draw.h" +#include #include #include #include @@ -66,8 +67,14 @@ int dcs_lcd_draw_image_x(const struct DCSLCDScreen *screen, uint16_t color = rgba8888_color_to_rgb565(img_pixel); uint16_t blended = alpha_blend_rgb565(color, bgcolor, alpha); pixmem16[drawn_pixels] = rgb565_color_to_surface(blended); - } else { + } else if (drawn_pixels > 0) { return drawn_pixels; + } else { + int run = 1; + while (j + run < width && rgba8888_get_alpha(READ_32_UNALIGNED(pixels + run)) != 0xFF) { + run++; + } + return -run; } drawn_pixels++; pixels++; @@ -99,6 +106,26 @@ int dcs_lcd_draw_rect_x(const struct DCSLCDScreen *screen, return drawn_pixels; } +int dcs_lcd_draw_shape_x(const struct DCSLCDScreen *screen, + int xpos, int ypos, int max_line_len, BaseDisplayItem *item, + int *outside_run) +{ + if (display_items_shape_outside_run(item, xpos, ypos, outside_run)) { + return 0; + } + bool inside; + int run = shape_run(item->data.shape_data.shape, xpos, ypos, &inside); + if (!inside) { + display_items_shape_remember_outside(item, xpos, ypos, run); + *outside_run = run; + return 0; + } + if (run > max_line_len) { + run = max_line_len; + } + return dcs_lcd_draw_rect_x(screen, xpos, ypos, run, item); +} + int dcs_lcd_draw_text_x(const struct DCSLCDScreen *screen, int xpos, int ypos, int max_line_len, BaseDisplayItem *item) { @@ -145,8 +172,14 @@ int dcs_lcd_draw_text_x(const struct DCSLCDScreen *screen, pixmem16[drawn_pixels] = fgcolor; } else if (visible_bg) { pixmem16[drawn_pixels] = bgcolor; - } else { + } else if (drawn_pixels > 0) { return drawn_pixels; + } else { + int run = 1; + while (j + run < width && (fontdata[((unsigned char) text[(j + run) / CHAR_WIDTH]) * 16 + ypos - y] & (1 << (7 - (j + run) % CHAR_WIDTH))) == 0) { + run++; + } + return -run; } drawn_pixels++; } @@ -170,39 +203,44 @@ int dcs_lcd_draw_scaled_cropped_img_x(const struct DCSLCDScreen *screen, } int width = item->width; - const char *data = item->data.image_data_with_size.pix; int drawn_pixels = 0; - int y_scale = item->y_scale; - int x_scale = item->x_scale; - int img_width = item->data.image_data_with_size.width; - - int source_x = item->source_x; - int source_y = item->source_y; - - uint32_t *pixels = ((uint32_t *) data) + (source_y + ((ypos - y) / y_scale)) * img_width + source_x + ((xpos - x) / x_scale); uint16_t *pixmem16 = (uint16_t *) (((uint8_t *) screen->pixels) + xpos * sizeof(uint16_t)); if (width > xpos - x + max_line_len) { width = xpos - x + max_line_len; } - for (int j = xpos - x; j < width; j++) { - uint32_t img_pixel = READ_32_UNALIGNED(pixels); + struct ScaledCroppedRow src; + display_items_scaled_cropped_row_init(&src, item, ypos - y); + + int j = xpos - x; + while (j < width) { + int run; + uint32_t img_pixel = READ_32_UNALIGNED(display_items_scaled_cropped_row_run(&src, j, width, &run)); uint8_t alpha = rgba8888_get_alpha(img_pixel); + uint16_t color; if (alpha == 0xFF) { - uint16_t color = uint32_color_to_surface(img_pixel); - pixmem16[drawn_pixels] = color; + color = uint32_color_to_surface(img_pixel); } else if (visible_bg) { - uint16_t color = rgba8888_color_to_rgb565(img_pixel); - uint16_t blended = alpha_blend_rgb565(color, bgcolor, alpha); - pixmem16[drawn_pixels] = rgb565_color_to_surface(blended); - } else { + color = rgb565_color_to_surface(alpha_blend_rgb565(rgba8888_color_to_rgb565(img_pixel), bgcolor, alpha)); + } else if (drawn_pixels > 0) { return drawn_pixels; + } else { + int transparent = run; + j += run; + while (j < width + && rgba8888_get_alpha(READ_32_UNALIGNED(display_items_scaled_cropped_row_run(&src, j, width, &run))) != 0xFF) { + transparent += run; + j += run; + } + return -transparent; } - drawn_pixels++; - pixels = ((uint32_t *) data) + (source_y + ((ypos - y) / y_scale)) * img_width + source_x + ((j + 1) / x_scale); + for (int k = 0; k < run; k++) { + pixmem16[drawn_pixels++] = color; + } + j += run; } return drawn_pixels; @@ -212,7 +250,7 @@ int dcs_lcd_draw_x(const struct DCSLCDScreen *screen, int xpos, int ypos, BaseDisplayItem *row) { int line_len = screen->w - xpos; - bool below = false; + int transparent_run = INT_MAX; for (BaseDisplayItem *item = row; item != NULL; item = item->next) { if (xpos < item->x) { @@ -226,8 +264,9 @@ int dcs_lcd_draw_x(const struct DCSLCDScreen *screen, continue; } - int max_line_len = below ? 1 : line_len; + int max_line_len = (line_len < transparent_run) ? line_len : transparent_run; + int run = 1; int drawn_pixels = 0; switch (item->primitive) { case PrimitiveImage: @@ -245,16 +284,27 @@ int dcs_lcd_draw_x(const struct DCSLCDScreen *screen, case PrimitiveText: drawn_pixels = dcs_lcd_draw_text_x(screen, xpos, ypos, max_line_len, item); break; + + case PrimitiveShape: + drawn_pixels = dcs_lcd_draw_shape_x(screen, xpos, ypos, max_line_len, item, &run); + break; + default: { fprintf(stderr, "unexpected display list command.\n"); } } - if (drawn_pixels != 0) { + if (drawn_pixels > 0) { return drawn_pixels; } - below = true; + // Transparent for run pixels: images and text return it negated, shapes set it + if (drawn_pixels < 0) { + run = -drawn_pixels; + } + if (run < transparent_run) { + transparent_run = run; + } } return 1; diff --git a/dcs_lcd_draw.h b/dcs_lcd_draw.h index 73c0394..72f630b 100644 --- a/dcs_lcd_draw.h +++ b/dcs_lcd_draw.h @@ -30,6 +30,9 @@ int dcs_lcd_draw_image_x(const struct DCSLCDScreen *screen, int dcs_lcd_draw_rect_x(const struct DCSLCDScreen *screen, int xpos, int ypos, int max_line_len, BaseDisplayItem *item); +int dcs_lcd_draw_shape_x(const struct DCSLCDScreen *screen, + int xpos, int ypos, int max_line_len, BaseDisplayItem *item, int *outside_run); + int dcs_lcd_draw_text_x(const struct DCSLCDScreen *screen, int xpos, int ypos, int max_line_len, BaseDisplayItem *item); diff --git a/display_items.c b/display_items.c index 3c8b2a6..8bd2524 100644 --- a/display_items.c +++ b/display_items.c @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -96,6 +97,11 @@ 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); } +static bool get_shape_value_element(term req, int index, int *out) +{ + return get_bounded_element(req, index, -SHAPE_VALUE_LIMIT, SHAPE_VALUE_LIMIT, out); +} + static bool get_color_element(term req, int index, uint32_t *out) { avm_int64_t color; @@ -117,6 +123,12 @@ static bool get_bgcolor_element(term req, int index, uint32_t *out, Context *ctx return get_color_element(req, index, out); } +static int normalize_deg(avm_int64_t deg) +{ + deg %= 360; + return (int) ((deg < 0) ? deg + 360 : deg); +} + static bool get_rgba8888_image(term img, struct ImageDataWithSize *out, Context *ctx) { if (UNLIKELY(!term_is_tuple(img) || term_get_tuple_arity(img) != 4 @@ -193,12 +205,177 @@ static const char *init_scaled_cropped_image_item(BaseDisplayItem *item, term re item->height = max_height; } + term flip_x = globalcontext_make_atom(ctx->global, ATOM_STR("\x6", "flip_x")); + term flip_y = globalcontext_make_atom(ctx->global, ATOM_STR("\x6", "flip_y")); + term opts = term_get_tuple_element(req, 10); + while (term_is_nonempty_list(opts)) { + term opt = term_get_list_head(opts); + if (term_is_tuple(opt) && term_get_tuple_arity(opt) == 2 + && term_get_tuple_element(opt, 1) == TRUE_ATOM) { + opt = term_get_tuple_element(opt, 0); + } + if (opt == flip_x) { + item->flip_x = true; + } else if (opt == flip_y) { + item->flip_y = true; + } + opts = term_get_list_tail(opts); + } + item->primitive = PrimitiveScaledCroppedImage; item->data.image_data_with_size = img; return NULL; } +static const char *init_shape_item(BaseDisplayItem *item, struct ShapeData *shape, uint32_t color) +{ + if (IS_NULL_PTR(shape)) { + return "out of memory"; + } + item->primitive = PrimitiveShape; + item->brcolor = color; + item->data.shape_data.shape = shape; + shape_bounds(shape, &item->x, &item->y, &item->width, &item->height); + + return NULL; +} + +static const char *init_rounded_rect_item(BaseDisplayItem *item, term req) +{ + int x, y, width, height, radius; + uint32_t color; + if (UNLIKELY(term_get_tuple_arity(req) != 7)) { + return "wrong arity"; + } + if (UNLIKELY(!get_shape_value_element(req, 1, &x) || !get_shape_value_element(req, 2, &y) + || !get_bounded_element(req, 3, 1, SHAPE_VALUE_LIMIT, &width) + || !get_bounded_element(req, 4, 1, SHAPE_VALUE_LIMIT, &height) + || !get_bounded_element(req, 5, 0, SHAPE_VALUE_LIMIT, &radius) + || !get_color_element(req, 6, &color))) { + return "bad position, size, radius or color"; + } + + return init_shape_item(item, shape_new_rounded_rect(x, y, width, height, radius), color); +} + +static const char *init_circle_item(BaseDisplayItem *item, term req) +{ + int cx, cy, radius; + uint32_t color; + if (UNLIKELY(term_get_tuple_arity(req) != 5)) { + return "wrong arity"; + } + if (UNLIKELY(!get_shape_value_element(req, 1, &cx) || !get_shape_value_element(req, 2, &cy) + || !get_bounded_element(req, 3, 1, SHAPE_VALUE_LIMIT, &radius) + || !get_color_element(req, 4, &color))) { + return "bad center, radius or color"; + } + + return init_shape_item(item, shape_new_ellipse(cx, cy, radius, radius), color); +} + +static const char *init_ellipse_item(BaseDisplayItem *item, term req) +{ + int cx, cy, rx, ry; + uint32_t color; + if (UNLIKELY(term_get_tuple_arity(req) != 6)) { + return "wrong arity"; + } + if (UNLIKELY(!get_shape_value_element(req, 1, &cx) || !get_shape_value_element(req, 2, &cy) + || !get_bounded_element(req, 3, 1, SHAPE_VALUE_LIMIT, &rx) + || !get_bounded_element(req, 4, 1, SHAPE_VALUE_LIMIT, &ry) + || !get_color_element(req, 5, &color))) { + return "bad center, radii or color"; + } + + return init_shape_item(item, shape_new_ellipse(cx, cy, rx, ry), color); +} + +static const char *init_line_item(BaseDisplayItem *item, term req) +{ + int x1, y1, x2, y2, thickness; + uint32_t color; + if (UNLIKELY(term_get_tuple_arity(req) != 7)) { + return "wrong arity"; + } + if (UNLIKELY(!get_shape_value_element(req, 1, &x1) || !get_shape_value_element(req, 2, &y1) + || !get_shape_value_element(req, 3, &x2) || !get_shape_value_element(req, 4, &y2) + || !get_bounded_element(req, 5, 1, SHAPE_VALUE_LIMIT, &thickness) + || !get_color_element(req, 6, &color))) { + return "bad points, thickness or color"; + } + + return init_shape_item(item, shape_new_line(x1, y1, x2, y2, thickness), color); +} + +static const char *init_arc_item(BaseDisplayItem *item, term req) +{ + int cx, cy, radius, thickness; + avm_int64_t start, end; + uint32_t color; + if (UNLIKELY(term_get_tuple_arity(req) != 8)) { + return "wrong arity"; + } + if (UNLIKELY(!get_shape_value_element(req, 1, &cx) || !get_shape_value_element(req, 2, &cy) + || !get_bounded_element(req, 3, 1, SHAPE_VALUE_LIMIT, &radius) + || !get_bounded_element(req, 4, 1, SHAPE_VALUE_LIMIT, &thickness) + || !get_int_element(req, 5, &start) || !get_int_element(req, 6, &end) || start == end + || !get_color_element(req, 7, &color))) { + return "bad center, radius, thickness, angles or color"; + } + + int start_deg = normalize_deg(start); + int end_deg = normalize_deg(end); + if (start_deg == end_deg) { + end_deg += 360; + } + + return init_shape_item(item, shape_new_arc(cx, cy, radius, thickness, start_deg, end_deg), color); +} + +static const char *init_polygon_item(BaseDisplayItem *item, term req) +{ + uint32_t color; + if (UNLIKELY(term_get_tuple_arity(req) != 3)) { + return "wrong arity"; + } + term points = term_get_tuple_element(req, 1); + int proper; + int points_len = term_list_length(points, &proper); + if (UNLIKELY(!proper || points_len < 3 || !get_color_element(req, 2, &color))) { + return "bad points or color"; + } + if (UNLIKELY(points_len > SHAPE_POLYGON_MAX_POINTS)) { + return "too many points"; + } + + struct ShapeData *shape = shape_polygon_begin(points_len); + if (IS_NULL_PTR(shape)) { + return "out of memory"; + } + for (int i = 0; i < points_len; i++) { + term point = term_get_list_head(points); + int x, y; + if (UNLIKELY(!term_is_tuple(point) || term_get_tuple_arity(point) != 2 + || !get_shape_value_element(point, 0, &x) || !get_shape_value_element(point, 1, &y))) { + shape_destroy(shape); + return "bad points or color"; + } + if (UNLIKELY(!shape_polygon_add_point(shape, x, y))) { + shape_destroy(shape); + return "bad points or color"; + } + points = term_get_list_tail(points); + } + if (UNLIKELY(!shape_polygon_end(shape))) { + shape_destroy(shape); + return "bad points or color"; + } + + return init_shape_item(item, shape, color); +} + static int clamp_coord(avm_int64_t v) { if (v < -DISPLAY_ITEMS_COORD_LIMIT) { @@ -385,6 +562,24 @@ static const char *init_item(BaseDisplayItem *item, term req, Context *ctx) } else if (cmd == globalcontext_make_atom(ctx->global, ATOM_STR("\x4", "text"))) { reason = init_text_item(item, req, ctx); + } else if (cmd == globalcontext_make_atom(ctx->global, ATOM_STR("\xC", "rounded_rect"))) { + reason = init_rounded_rect_item(item, req); + + } else if (cmd == globalcontext_make_atom(ctx->global, ATOM_STR("\x6", "circle"))) { + reason = init_circle_item(item, req); + + } else if (cmd == globalcontext_make_atom(ctx->global, ATOM_STR("\x7", "ellipse"))) { + reason = init_ellipse_item(item, req); + + } else if (cmd == globalcontext_make_atom(ctx->global, ATOM_STR("\x4", "line"))) { + reason = init_line_item(item, req); + + } else if (cmd == globalcontext_make_atom(ctx->global, ATOM_STR("\x3", "arc"))) { + reason = init_arc_item(item, req); + + } else if (cmd == globalcontext_make_atom(ctx->global, ATOM_STR("\x7", "polygon"))) { + reason = init_polygon_item(item, req); + } else { reason = "unknown command"; } @@ -485,6 +680,10 @@ void display_items_delete(BaseDisplayItem items[], size_t items_len) free((char *) item->data.text_data.text); break; + case PrimitiveShape: + shape_destroy(item->data.shape_data.shape); + break; + default: { break; } diff --git a/display_items.h b/display_items.h index 6fa2260..9194431 100644 --- a/display_items.h +++ b/display_items.h @@ -27,6 +27,8 @@ #include +#include "shape.h" + // TODO: deprecated helper, remove this static inline term context_make_atom(Context *ctx, AtomString string) { @@ -41,7 +43,8 @@ typedef enum PrimitiveImage, PrimitiveScaledCroppedImage, PrimitiveRect, - PrimitiveText + PrimitiveText, + PrimitiveShape } primitive_t; struct TextData @@ -62,6 +65,16 @@ struct ImageDataWithSize const char *pix; }; +struct ShapeItemData +{ + struct ShapeData *shape; + // Rendering scratch, never compared: on outside_row the shape doesn't cover + // outside_from..outside_to - 1, so draw_x can skip it there without calling shape_run() + int16_t outside_row; + int16_t outside_from; + int16_t outside_to; +}; + struct BaseDisplayItem { primitive_t primitive; @@ -75,6 +88,7 @@ struct BaseDisplayItem struct ImageData image_data; struct ImageDataWithSize image_data_with_size; struct TextData text_data; + struct ShapeItemData shape_data; } data; //used just for scaled cropped image @@ -82,6 +96,8 @@ struct BaseDisplayItem int source_y; int x_scale; int y_scale; + bool flip_x; + bool flip_y; bool owns_data; @@ -116,6 +132,121 @@ static inline BaseDisplayItem *display_items_row(BaseDisplayItem items[], size_t return head; } +static inline uint8_t rgba8888_get_alpha(uint32_t color) +{ + return color & 0xFF; +} + +// A scaled_cropped_image shows display pixel px of an axis (counted from the item's x or y) +// from source pixel px / scale of the cropped image (counted from source_x or source_y). With +// flip_x or flip_y the axis is mirrored within the part of the item the image covers, which is +// all of it unless the item is wider or taller than the scaled image. Past the end of the image +// the edge source pixel repeats. +struct ScaledCroppedAxis +{ + int last_px; // with flip, the last display pixel the image covers, mirrored to 0 + int scale; + int last_src; // the last source pixel of the cropped image + bool flip; +}; + +static inline void display_items_scaled_cropped_axis_init(struct ScaledCroppedAxis *axis, int len, + int src_len, int scale, bool flip) +{ + int64_t covered = (int64_t) src_len * scale; + axis->last_px = ((len < covered) ? len : (int) covered) - 1; + axis->scale = scale; + axis->last_src = src_len - 1; + axis->flip = flip; +} + +static inline int display_items_scaled_cropped_axis_src(const struct ScaledCroppedAxis *axis, + int px) +{ + if (axis->flip) { + px = axis->last_px - px; + // Past the mirrored image: the edge pixel, source pixel 0 + if (px < 0) { + px = 0; + } + } + int src = px / axis->scale; + return (src < axis->last_src) ? src : axis->last_src; +} + +// One display row of a scaled_cropped_image: its source row and the mapping of its columns. +struct ScaledCroppedRow +{ + const uint32_t *pixels; // the source row, from source_x + struct ScaledCroppedAxis cols; +}; + +static inline void display_items_scaled_cropped_row_init(struct ScaledCroppedRow *row, + const BaseDisplayItem *item, int row_px) +{ + const struct ImageDataWithSize *img = &item->data.image_data_with_size; + struct ScaledCroppedAxis rows; + display_items_scaled_cropped_axis_init(&rows, item->height, img->height - item->source_y, + item->y_scale, item->flip_y); + int src_row = display_items_scaled_cropped_axis_src(&rows, row_px); + row->pixels = ((const uint32_t *) img->pix) + (item->source_y + src_row) * img->width + + item->source_x; + display_items_scaled_cropped_axis_init(&row->cols, item->width, img->width - item->source_x, + item->x_scale, item->flip_x); +} + +static inline bool display_items_shape_outside_run(const BaseDisplayItem *item, int xpos, int ypos, + int *run) +{ + const struct ShapeItemData *data = &item->data.shape_data; + if (ypos == data->outside_row && xpos >= data->outside_from && xpos < data->outside_to) { + *run = data->outside_to - xpos; + return true; + } + return false; +} + +static inline void display_items_shape_remember_outside(BaseDisplayItem *item, int xpos, int ypos, + int run) +{ + if (ypos < 0 || ypos > INT16_MAX || xpos < 0 || run > INT16_MAX - xpos) { + return; + } + struct ShapeItemData *data = &item->data.shape_data; + data->outside_row = (int16_t) ypos; + data->outside_from = (int16_t) xpos; + data->outside_to = (int16_t) (xpos + run); +} + +// The source pixel shown at display column col_px of the row, and in *run how many display +// columns from col_px, up to end, show that same source pixel. +static inline __attribute__((always_inline)) const uint32_t *display_items_scaled_cropped_row_run( + const struct ScaledCroppedRow *row, int col_px, int end, int *run) +{ + const struct ScaledCroppedAxis *cols = &row->cols; + int src; + int left; + if (!cols->flip) { + src = col_px / cols->scale; + left = (src + 1) * cols->scale - col_px; + if (src >= cols->last_src) { + src = cols->last_src; + left = end - col_px; + } + } else { + int px = cols->last_px - col_px; + if (px >= 0) { + src = px / cols->scale; + left = px - src * cols->scale + 1; + } else { + src = 0; + left = end - col_px; + } + } + *run = (left < end - col_px) ? left : end - col_px; + return row->pixels + src; +} + 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); diff --git a/docs/primitives.md b/docs/primitives.md index aa5d901..570fc11 100644 --- a/docs/primitives.md +++ b/docs/primitives.md @@ -24,8 +24,10 @@ 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. +- `image` and `scaled_cropped_image` coordinates, sizes, source offsets and scale factors, image + widths and heights, and every value of a shape (coordinates, sizes, radii, thicknesses and + polygon points) must be within ±32767, or the item is invalid. +- Arc angles can be any integer. ### Invalid Items An item with a wrong arity, a value of the wrong type or out of range, or an unknown command is @@ -61,6 +63,20 @@ performance implications. Text can be provided as either an Erlang string (a list) or an Elixir string (a binary). UTF-8 encoding is supported. +### Shapes +Shape primitives (`rounded_rect`, `line`, `circle`, `ellipse`, `arc`, `polygon`) paint only the +pixels inside the shape; pixels in the bounding box but outside the shape show whatever item is +below in the display list. + +`line`, `circle`, `ellipse` and `arc` place their points on pixel centers: the pixel at `{X, Y}` is +drawn when it is inside. Round edges use the midpoint rule: a pixel at offset `{DX, DY}` from the +center is inside a radius `R` when `DX * DX + DY * DY < R * R + R`, so a shape of radius `R` is +`2 * R + 1` pixels across and a circle of radius 1 is a 5 pixel plus. An ellipse applies the same +rule to each axis. `polygon` vertices lie on pixel corners instead (see below), so a polygon +outlined with `line` items at the same coordinates is off by half a pixel: the lines along its top +and left edges cover its first row and column, the lines along its right and bottom edges fall just +outside it. + ## image Displays an image at the specified position. The image dimensions are determined by the image tuple @@ -81,13 +97,19 @@ The item is invalid unless `0 <= SourceX < ImageWidth`, `0 <= SourceY < ImageHei 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. +`Opts` is a list. `flip_x` or `{flip_x, true}` mirrors the item horizontally, `flip_y` or +`{flip_y, true}` vertically. Other entries are ignored, and so is an `Opts` that is not a list. +A flip mirrors the drawn pixels in place, not the whole source image: with `flip_x`, the pixel at +offset `C` from the item's left edge shows what the unflipped item shows at offset `Width - 1 - C`, +also when `Width` is not a multiple of the scale factor. + ```erlang {scaled_cropped_image, X, Y, Width, Height, % bounding rect in pixels BackgroundColor, % RGB background color, a "hex color" can be used here, or transparent atom SourceX, SourceY, % offset inside the source image from where the image is taken XScaleFactor, YScaleFactor, % integer scaling factor, 1 is original, 2 is twice, etc. - Opts, % option keyword list, always []: right now no additional options are supported + Opts, % option list: [flip_x], [flip_y], [flip_x, flip_y], or [] for none Image % image tuple } ``` @@ -121,6 +143,108 @@ the item invalid. } ``` +## rounded_rect + +Draws a filled rectangle with rounded corners. The corners are quarter circles centered on the +pixels `Radius` pixels in from each corner, so a `2 * R + 1` square with radius `R` is the same as a +circle of radius `R`. The radius clamp keeps a straight edge of at least one pixel on every side: +a 12 pixel high button gets a radius of at most 5. + +```erlang +{rounded_rect, + X, Y, Width, Height, % bounding rect in pixels + Radius, % corner radius in pixels, >= 0, clamped to (min(Width, Height) - 1) div 2 + Color % RGB fill color +} +``` + +## line + +Draws a straight line of any angle, covering both endpoints. + +```erlang +{line, + X1, Y1, X2, Y2, % endpoints in pixels + Thickness, % line width in pixels, >= 1 + Color % RGB line color +} +``` + +A line is `Thickness` pixels thick measured along its minor axis: a line that is wider than it is +tall covers `Thickness` pixels in every column from `X1` to `X2`, and a taller line `Thickness` +pixels in every row from `Y1` to `Y2`. A 1 pixel line has one pixel per column (or row) and no +gaps. With an even thickness the extra pixel goes above (or left of) the ideal line. From thickness +3 on, both ends get a round cap: a disc `Thickness` pixels across, centered on the line's body, that +reaches `(Thickness - 1) div 2` pixels past the endpoint and never sticks out above or below the +line. With an even thickness the body, and so the cap, is centered half a pixel above (or left of) +the endpoint: a horizontal line of thickness 4 from `{0, 0}` to `{4, 0}` is a 7 by 4 block covering +columns -1 to 5 and rows -2 to 1. A line with equal endpoints is a disc of radius +`(Thickness - 1) div 2` on that point, drawn with the midpoint rule (a single pixel for thickness 1 +or 2). + +## circle + +Draws a filled circle centered on the given point. + +```erlang +{circle, + CX, CY, % center in pixels + R, % radius in pixels, > 0, the circle is 2 * R + 1 pixels wide + Color % RGB fill color +} +``` + +## ellipse + +Draws a filled ellipse centered on the given point. + +```erlang +{ellipse, + CX, CY, % center in pixels + RX, RY, % horizontal and vertical radius in pixels, > 0 + Color % RGB fill color +} +``` + +## arc + +Draws a ring segment (or, with a large thickness, a pie slice). + +```erlang +{arc, + CX, CY, % center in pixels + R, % outer radius in pixels, > 0 + Thickness, % ring thickness in pixels, > 0, Thickness >= R draws a pie slice + StartDeg, EndDeg, % integer degrees, 0 is 3 o'clock, drawn clockwise from StartDeg to EndDeg + Color % RGB color +} +``` + +The arc sweeps `(EndDeg - StartDeg) mod 360` degrees clockwise, so `{0, 90}` is a quarter, +`{0, -90}` three quarters and `{0, 400}` 40 degrees. Different angles that are equal mod 360, such +as `{0, 360}` or `{0, -720}`, draw a full ring. `StartDeg == EndDeg` is invalid. + +A pixel is drawn when its center is inside the ring (by the midpoint rule, for both the outer +radius `R` and the inner radius `R - Thickness`) and either its center lies within the sweep, both +edges included, or one of the two edge rays from the center passes through the pixel. The rays keep +narrow arcs visible: a 1 degree arc still draws the ring pixels its edges cross. Because both edges +are included, two arcs that share an edge angle both draw the pixels on it. + +## polygon + +Draws a filled polygon from a list of vertices, using the even-odd fill rule. Vertices lie on +pixel corners: the pixel at `{X, Y}` spans `X..X+1` and `Y..Y+1` and is drawn when its center is +inside, so `[{0, 0}, {10, 0}, {10, 10}, {0, 10}]` fills exactly 10 by 10 pixels, columns and rows 0 +to 9. Unlike the other shapes, which include their edges, a polygon's fill is half-open: polygons +that share an edge don't overlap and leave no gap between them. + +```erlang +{polygon, + Points, % list of 3 to 64 {X, Y} vertices on pixel corners, even-odd fill + Color % RGB fill color +} +``` + ## Image Tuples An image tuple contains all the information required for displaying an image. Images are tagged diff --git a/epaper_draw.c b/epaper_draw.c index d68013c..ef46e80 100644 --- a/epaper_draw.c +++ b/epaper_draw.c @@ -20,6 +20,7 @@ #include "epaper_draw.h" +#include #include #include #include @@ -82,7 +83,7 @@ int epaper_draw_image_x(const struct EpaperScreen *screen, for (int j = xpos - x; j < width; j++) { uint32_t img_pixel = READ_32_UNALIGNED(pixels); - if ((*pixels >> 24) & 0xFF) { + if (rgba8888_get_alpha(img_pixel) != 0) { uint8_t r = img_pixel >> 24; uint8_t g = (img_pixel >> 16) & 0xFF; uint8_t b = (img_pixel >> 8) & 0xFF; @@ -96,8 +97,14 @@ int epaper_draw_image_x(const struct EpaperScreen *screen, screen->palette, screen->palette_size); epaper_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); - } else { + } else if (drawn_pixels > 0) { return drawn_pixels; + } else { + int run = 1; + while (j + run < width && rgba8888_get_alpha(READ_32_UNALIGNED(pixels + run)) == 0) { + run++; + } + return -run; } drawn_pixels++; pixels++; @@ -133,6 +140,27 @@ int epaper_draw_rect_x(const struct EpaperScreen *screen, return drawn_pixels; } +int epaper_draw_shape_x(const struct EpaperScreen *screen, + uint8_t *line_buf, int xpos, int ypos, int max_line_len, + BaseDisplayItem *item, + int *outside_run) +{ + if (display_items_shape_outside_run(item, xpos, ypos, outside_run)) { + return 0; + } + bool inside; + int run = shape_run(item->data.shape_data.shape, xpos, ypos, &inside); + if (!inside) { + display_items_shape_remember_outside(item, xpos, ypos, run); + *outside_run = run; + return 0; + } + if (run > max_line_len) { + run = max_line_len; + } + return epaper_draw_rect_x(screen, line_buf, xpos, ypos, run, item); +} + int epaper_draw_text_x(const struct EpaperScreen *screen, uint8_t *line_buf, int xpos, int ypos, int max_line_len, BaseDisplayItem *item) @@ -196,8 +224,14 @@ int epaper_draw_text_x(const struct EpaperScreen *screen, screen->palette, screen->palette_size); epaper_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); - } else { + } else if (drawn_pixels > 0) { return drawn_pixels; + } else { + int run = 1; + while (j + run < width && (fontdata[((unsigned char) text[(j + run) / CHAR_WIDTH]) * 16 + ypos - y] & (1 << (7 - (j + run) % CHAR_WIDTH))) == 0) { + run++; + } + return -run; } drawn_pixels++; } @@ -229,44 +263,50 @@ int epaper_draw_scaled_cropped_img_x(const struct EpaperScreen *screen, } int width = item->width; - const char *data = item->data.image_data_with_size.pix; int drawn_pixels = 0; - int y_scale = item->y_scale; - int x_scale = item->x_scale; - int img_width = item->data.image_data_with_size.width; - - int source_x = item->source_x; - int source_y = item->source_y; - - uint32_t *pixels = ((uint32_t *) data) + (source_y + ((ypos - y) / y_scale)) * img_width + source_x + ((xpos - x) / x_scale); - if (width > xpos - x + max_line_len) { width = xpos - x + max_line_len; } - for (int j = xpos - x; j < width; j++) { - uint32_t img_pixel = READ_32_UNALIGNED(pixels); - if ((*pixels >> 24) & 0xFF) { + struct ScaledCroppedRow src; + display_items_scaled_cropped_row_init(&src, item, ypos - y); + + int j = xpos - x; + while (j < width) { + int run; + uint32_t img_pixel = READ_32_UNALIGNED(display_items_scaled_cropped_row_run(&src, j, width, &run)); + if (rgba8888_get_alpha(img_pixel) != 0) { uint8_t r = img_pixel >> 24; uint8_t g = (img_pixel >> 16) & 0xFF; uint8_t b = (img_pixel >> 8) & 0xFF; - - uint8_t c = epaper_dither_acep7(xpos + drawn_pixels, ypos, r, g, b, - screen->palette, screen->palette_size); - epaper_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); - + for (int k = 0; k < run; k++) { + uint8_t c = epaper_dither_acep7(xpos + drawn_pixels, ypos, r, g, b, + screen->palette, screen->palette_size); + epaper_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); + drawn_pixels++; + } } else if (visible_bg) { - uint8_t c = epaper_dither_acep7(xpos + drawn_pixels, ypos, bgcolor_r, bgcolor_g, bgcolor_b, - screen->palette, screen->palette_size); - epaper_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); - - } else { + for (int k = 0; k < run; k++) { + uint8_t c = epaper_dither_acep7(xpos + drawn_pixels, ypos, bgcolor_r, bgcolor_g, bgcolor_b, + screen->palette, screen->palette_size); + epaper_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); + drawn_pixels++; + } + } else if (drawn_pixels > 0) { return drawn_pixels; + } else { + int transparent = run; + j += run; + while (j < width + && rgba8888_get_alpha(READ_32_UNALIGNED(display_items_scaled_cropped_row_run(&src, j, width, &run))) == 0) { + transparent += run; + j += run; + } + return -transparent; } - drawn_pixels++; - pixels = ((uint32_t *) data) + (source_y + ((ypos - y) / y_scale)) * img_width + source_x + ((j + 1) / x_scale); + j += run; } return drawn_pixels; @@ -277,7 +317,7 @@ int epaper_draw_x(const struct EpaperScreen *screen, BaseDisplayItem *row) { int line_len = screen->w - xpos; - bool below = false; + int transparent_run = INT_MAX; for (BaseDisplayItem *item = row; item != NULL; item = item->next) { if (xpos < item->x) { @@ -291,8 +331,9 @@ int epaper_draw_x(const struct EpaperScreen *screen, continue; } - int max_line_len = below ? 1 : line_len; + int max_line_len = (line_len < transparent_run) ? line_len : transparent_run; + int run = 1; int drawn_pixels = 0; switch (item->primitive) { case PrimitiveImage: @@ -311,16 +352,26 @@ int epaper_draw_x(const struct EpaperScreen *screen, drawn_pixels = epaper_draw_text_x(screen, line_buf, xpos, ypos, max_line_len, item); break; + case PrimitiveShape: + drawn_pixels = epaper_draw_shape_x(screen, line_buf, xpos, ypos, max_line_len, item, &run); + break; + default: { fprintf(stderr, "unexpected display list command.\n"); } } - if (drawn_pixels != 0) { + if (drawn_pixels > 0) { return drawn_pixels; } - below = true; + // Transparent for run pixels: images and text return it negated, shapes set it + if (drawn_pixels < 0) { + run = -drawn_pixels; + } + if (run < transparent_run) { + transparent_run = run; + } } return 1; diff --git a/epaper_draw.h b/epaper_draw.h index 3f0fe07..5b9918c 100644 --- a/epaper_draw.h +++ b/epaper_draw.h @@ -35,6 +35,10 @@ int epaper_draw_rect_x(const struct EpaperScreen *screen, uint8_t *line_buf, int xpos, int ypos, int max_line_len, BaseDisplayItem *item); +int epaper_draw_shape_x(const struct EpaperScreen *screen, + uint8_t *line_buf, int xpos, int ypos, int max_line_len, + BaseDisplayItem *item, int *outside_run); + int epaper_draw_text_x(const struct EpaperScreen *screen, uint8_t *line_buf, int xpos, int ypos, int max_line_len, BaseDisplayItem *item); diff --git a/mono_draw.c b/mono_draw.c index 0eaf41d..c7e8a8c 100644 --- a/mono_draw.c +++ b/mono_draw.c @@ -20,6 +20,7 @@ #include "mono_draw.h" +#include #include #include #include @@ -113,7 +114,7 @@ int mono_draw_image_x(const struct MonoScreen *screen, for (int j = xpos - x; j < width; j++) { uint32_t img_pixel = READ_32_UNALIGNED(pixels); - if ((*pixels >> 24) & 0xFF) { + if (rgba8888_get_alpha(img_pixel) != 0) { uint8_t r = img_pixel >> 24; uint8_t g = (img_pixel >> 16) & 0xFF; uint8_t b = (img_pixel >> 8) & 0xFF; @@ -125,8 +126,14 @@ int mono_draw_image_x(const struct MonoScreen *screen, int c = get_color(xpos + drawn_pixels, ypos, bgcolor_r, bgcolor_g, bgcolor_b); mono_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); - } else { + } else if (drawn_pixels > 0) { return drawn_pixels; + } else { + int run = 1; + while (j + run < width && rgba8888_get_alpha(READ_32_UNALIGNED(pixels + run)) == 0) { + run++; + } + return -run; } drawn_pixels++; pixels++; @@ -162,6 +169,27 @@ int mono_draw_rect_x(const struct MonoScreen *screen, return drawn_pixels; } +int mono_draw_shape_x(const struct MonoScreen *screen, + uint8_t *line_buf, int xpos, int ypos, int max_line_len, + BaseDisplayItem *item, + int *outside_run) +{ + if (display_items_shape_outside_run(item, xpos, ypos, outside_run)) { + return 0; + } + bool inside; + int run = shape_run(item->data.shape_data.shape, xpos, ypos, &inside); + if (!inside) { + display_items_shape_remember_outside(item, xpos, ypos, run); + *outside_run = run; + return 0; + } + if (run > max_line_len) { + run = max_line_len; + } + return mono_draw_rect_x(screen, line_buf, xpos, ypos, run, item); +} + int mono_draw_text_x(const struct MonoScreen *screen, uint8_t *line_buf, int xpos, int ypos, int max_line_len, BaseDisplayItem *item) @@ -223,8 +251,14 @@ int mono_draw_text_x(const struct MonoScreen *screen, int c = get_color(xpos + drawn_pixels, ypos, bgcolor_r, bgcolor_g, bgcolor_b); mono_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); - } else { + } else if (drawn_pixels > 0) { return drawn_pixels; + } else { + int run = 1; + while (j + run < width && (fontdata[((unsigned char) text[(j + run) / CHAR_WIDTH]) * 16 + ypos - y] & (1 << (7 - (j + run) % CHAR_WIDTH))) == 0) { + run++; + } + return -run; } drawn_pixels++; } @@ -256,42 +290,48 @@ int mono_draw_scaled_cropped_img_x(const struct MonoScreen *screen, } int width = item->width; - const char *data = item->data.image_data_with_size.pix; int drawn_pixels = 0; - int y_scale = item->y_scale; - int x_scale = item->x_scale; - int img_width = item->data.image_data_with_size.width; - - int source_x = item->source_x; - int source_y = item->source_y; - - uint32_t *pixels = ((uint32_t *) data) + (source_y + ((ypos - y) / y_scale)) * img_width + source_x + ((xpos - x) / x_scale); - if (width > xpos - x + max_line_len) { width = xpos - x + max_line_len; } - for (int j = xpos - x; j < width; j++) { - uint32_t img_pixel = READ_32_UNALIGNED(pixels); - if ((*pixels >> 24) & 0xFF) { + struct ScaledCroppedRow src; + display_items_scaled_cropped_row_init(&src, item, ypos - y); + + int j = xpos - x; + while (j < width) { + int run; + uint32_t img_pixel = READ_32_UNALIGNED(display_items_scaled_cropped_row_run(&src, j, width, &run)); + if (rgba8888_get_alpha(img_pixel) != 0) { uint8_t r = img_pixel >> 24; uint8_t g = (img_pixel >> 16) & 0xFF; uint8_t b = (img_pixel >> 8) & 0xFF; - - int c = get_color(xpos + drawn_pixels, ypos, r, g, b); - mono_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); - + for (int k = 0; k < run; k++) { + int c = get_color(xpos + drawn_pixels, ypos, r, g, b); + mono_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); + drawn_pixels++; + } } else if (visible_bg) { - int c = get_color(xpos + drawn_pixels, ypos, bgcolor_r, bgcolor_g, bgcolor_b); - mono_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); - - } else { + for (int k = 0; k < run; k++) { + int c = get_color(xpos + drawn_pixels, ypos, bgcolor_r, bgcolor_g, bgcolor_b); + mono_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); + drawn_pixels++; + } + } else if (drawn_pixels > 0) { return drawn_pixels; + } else { + int transparent = run; + j += run; + while (j < width + && rgba8888_get_alpha(READ_32_UNALIGNED(display_items_scaled_cropped_row_run(&src, j, width, &run))) == 0) { + transparent += run; + j += run; + } + return -transparent; } - drawn_pixels++; - pixels = ((uint32_t *) data) + (source_y + ((ypos - y) / y_scale)) * img_width + source_x + ((j + 1) / x_scale); + j += run; } return drawn_pixels; @@ -302,7 +342,7 @@ int mono_draw_x(const struct MonoScreen *screen, BaseDisplayItem *row) { int line_len = screen->w - xpos; - bool below = false; + int transparent_run = INT_MAX; for (BaseDisplayItem *item = row; item != NULL; item = item->next) { if (xpos < item->x) { @@ -316,8 +356,9 @@ int mono_draw_x(const struct MonoScreen *screen, continue; } - int max_line_len = below ? 1 : line_len; + int max_line_len = (line_len < transparent_run) ? line_len : transparent_run; + int run = 1; int drawn_pixels = 0; switch (item->primitive) { case PrimitiveImage: @@ -340,16 +381,26 @@ int mono_draw_x(const struct MonoScreen *screen, drawn_pixels = mono_draw_text_x(screen, line_buf, xpos, ypos, max_line_len, item); break; + case PrimitiveShape: + drawn_pixels = mono_draw_shape_x(screen, line_buf, xpos, ypos, max_line_len, item, &run); + break; + default: { fprintf(stderr, "unexpected display list command.\n"); } } - if (drawn_pixels != 0) { + if (drawn_pixels > 0) { return drawn_pixels; } - below = true; + // Transparent for run pixels: images and text return it negated, shapes set it + if (drawn_pixels < 0) { + run = -drawn_pixels; + } + if (run < transparent_run) { + transparent_run = run; + } } return 1; diff --git a/mono_draw.h b/mono_draw.h index e35cd36..9dd518b 100644 --- a/mono_draw.h +++ b/mono_draw.h @@ -40,6 +40,10 @@ int mono_draw_rect_x(const struct MonoScreen *screen, uint8_t *line_buf, int xpos, int ypos, int max_line_len, BaseDisplayItem *item); +int mono_draw_shape_x(const struct MonoScreen *screen, + uint8_t *line_buf, int xpos, int ypos, int max_line_len, + BaseDisplayItem *item, int *outside_run); + int mono_draw_text_x(const struct MonoScreen *screen, uint8_t *line_buf, int xpos, int ypos, int max_line_len, BaseDisplayItem *item); diff --git a/sdl_display/CMakeLists.txt b/sdl_display/CMakeLists.txt index 0d95983..b4deb08 100644 --- a/sdl_display/CMakeLists.txt +++ b/sdl_display/CMakeLists.txt @@ -43,7 +43,7 @@ endif() set(CMAKE_SHARED_LIBRARY_PREFIX "") -add_library(avm_display_port_driver SHARED display.c ../ufontlib.c ../image_helpers.c ../spng.c ../font_data.c ../display_items.c ../display_message.c) +add_library(avm_display_port_driver SHARED display.c ../ufontlib.c ../image_helpers.c ../spng.c ../font_data.c ../display_items.c ../display_message.c ../shape.c) if (AVM_DISABLE_SMP) target_compile_definitions(avm_display_port_driver PUBLIC AVM_NO_SMP) diff --git a/sdl_display/display.c b/sdl_display/display.c index 5e905b4..7622b94 100644 --- a/sdl_display/display.c +++ b/sdl_display/display.c @@ -19,6 +19,7 @@ */ #include +#include #include #include #include @@ -128,13 +129,19 @@ static int draw_image_x(int xpos, int ypos, int max_line_len, BaseDisplayItem *i for (int j = xpos - x; j < width; j++) { uint32_t img_pixel = READ_32_UNALIGNED(pixels); - if ((*pixels >> 24) & 0xFF) { + if (rgba8888_get_alpha(img_pixel) != 0) { Uint32 color = uint32_color_to_surface(screen, img_pixel); pixmem32[drawn_pixels] = color; } else if (visible_bg) { pixmem32[drawn_pixels] = bgcolor; - } else { + } else if (drawn_pixels > 0) { return drawn_pixels; + } else { + int run = 1; + while (j + run < width && rgba8888_get_alpha(READ_32_UNALIGNED(pixels + run)) == 0) { + run++; + } + return -run; } drawn_pixels++; pixels++; @@ -158,36 +165,44 @@ static int draw_scaled_cropped_img_x(int xpos, int ypos, int max_line_len, BaseD } int width = item->width; - const char *data = item->data.image_data_with_size.pix; int drawn_pixels = 0; - int y_scale = item->y_scale; - int x_scale = item->x_scale; - int img_width = item->data.image_data_with_size.width; - - int source_x = item->source_x; - int source_y = item->source_y; - - uint32_t *pixels = ((uint32_t *) data) + (source_y + ((ypos - y) / y_scale)) * img_width + source_x + ((xpos - x) / x_scale); Uint32 *pixmem32 = (Uint32 *) (((uint8_t *) screen->pixels) + screen->w * ypos * BPP + xpos * BPP); if (width > xpos - x + max_line_len) { width = xpos - x + max_line_len; } - for (int j = xpos - x; j < width; j++) { - uint32_t img_pixel = READ_32_UNALIGNED(pixels); - if ((*pixels >> 24) & 0xFF) { + struct ScaledCroppedRow src; + display_items_scaled_cropped_row_init(&src, item, ypos - y); + + int j = xpos - x; + while (j < width) { + int run; + uint32_t img_pixel = READ_32_UNALIGNED(display_items_scaled_cropped_row_run(&src, j, width, &run)); + if (rgba8888_get_alpha(img_pixel) != 0) { Uint32 color = uint32_color_to_surface(screen, img_pixel); - pixmem32[drawn_pixels] = color; + for (int k = 0; k < run; k++) { + pixmem32[drawn_pixels++] = color; + } } else if (visible_bg) { - pixmem32[drawn_pixels] = bgcolor; - } else { + for (int k = 0; k < run; k++) { + pixmem32[drawn_pixels++] = bgcolor; + } + } else if (drawn_pixels > 0) { return drawn_pixels; + } else { + int transparent = run; + j += run; + while (j < width + && rgba8888_get_alpha(READ_32_UNALIGNED(display_items_scaled_cropped_row_run(&src, j, width, &run))) == 0) { + transparent += run; + j += run; + } + return -transparent; } - drawn_pixels++; - pixels = ((uint32_t *) data) + (source_y + ((ypos - y) / y_scale)) * img_width + source_x + ((j + 1) / x_scale); + j += run; } return drawn_pixels; @@ -215,6 +230,25 @@ static int draw_rect_x(int xpos, int ypos, int max_line_len, BaseDisplayItem *it return drawn_pixels; } +static int draw_shape_x(int xpos, int ypos, int max_line_len, BaseDisplayItem *item, + int *outside_run) +{ + if (display_items_shape_outside_run(item, xpos, ypos, outside_run)) { + return 0; + } + bool inside; + int run = shape_run(item->data.shape_data.shape, xpos, ypos, &inside); + if (!inside) { + display_items_shape_remember_outside(item, xpos, ypos, run); + *outside_run = run; + return 0; + } + if (run > max_line_len) { + run = max_line_len; + } + return draw_rect_x(xpos, ypos, run, item); +} + static int draw_text_x(int xpos, int ypos, int max_line_len, BaseDisplayItem *item) { int x = item->x; @@ -260,8 +294,14 @@ static int draw_text_x(int xpos, int ypos, int max_line_len, BaseDisplayItem *it pixmem32[drawn_pixels] = fgcolor; } else if (visible_bg) { pixmem32[drawn_pixels] = bgcolor; - } else { + } else if (drawn_pixels > 0) { return drawn_pixels; + } else { + int run = 1; + while (j + run < width && (fontdata[((unsigned char) text[(j + run) / CHAR_WIDTH]) * 16 + ypos - y] & (1 << (7 - (j + run) % CHAR_WIDTH))) == 0) { + run++; + } + return -run; } drawn_pixels++; } @@ -272,7 +312,7 @@ static int draw_text_x(int xpos, int ypos, int max_line_len, BaseDisplayItem *it static int draw_x(int xpos, int ypos, BaseDisplayItem *row) { int line_len = screen->w - xpos; - bool below = false; + int transparent_run = INT_MAX; for (BaseDisplayItem *item = row; item != NULL; item = item->next) { if (xpos < item->x) { @@ -286,8 +326,9 @@ static int draw_x(int xpos, int ypos, BaseDisplayItem *row) continue; } - int max_line_len = below ? 1 : line_len; + int max_line_len = (line_len < transparent_run) ? line_len : transparent_run; + int run = 1; int drawn_pixels = 0; switch (item->primitive) { case PrimitiveImage: @@ -306,16 +347,26 @@ static int draw_x(int xpos, int ypos, BaseDisplayItem *row) drawn_pixels = draw_text_x(xpos, ypos, max_line_len, item); break; + case PrimitiveShape: + drawn_pixels = draw_shape_x(xpos, ypos, max_line_len, item, &run); + break; + default: { fprintf(stderr, "unexpected display list command.\n"); } } - if (drawn_pixels != 0) { + if (drawn_pixels > 0) { return drawn_pixels; } - below = true; + // Transparent for run pixels: images and text return it negated, shapes set it + if (drawn_pixels < 0) { + run = -drawn_pixels; + } + if (run < transparent_run) { + transparent_run = run; + } } return 1; diff --git a/sdl_display/test_display.erl b/sdl_display/test_display.erl index 27b932a..c19dbc0 100644 --- a/sdl_display/test_display.erl +++ b/sdl_display/test_display.erl @@ -19,7 +19,7 @@ % -module(test_display). --export([start/0, loop/0]). +-export([start/0, shapes/0, loop/0]). start() -> Display = erlang:open_port({spawn, "display"}, []), @@ -28,20 +28,34 @@ start() -> disp(Display, 16#00FF0000), disp(Display, 16#0000FF00), - % TODO: let's switch back to the following as soon as tuple format is changed in port.c - % Display ! {'$call', {self(), make_ref()}, {subscribe_input}}, - Display ! {self(), make_ref(), {subscribe_input, all}}, + Display ! {'$call', {self(), make_ref()}, {subscribe_input, all}}, loop(). +shapes() -> + Display = erlang:open_port({spawn, "display"}, []), + Sprite = {rgba8888, 2, 1, <<255, 0, 0, 255, 0, 0, 255, 255>>}, + Scene = [ + {text, 10, 4, default16px, 16#FFFFFF, transparent, <<"shapes">>}, + {rounded_rect, 10, 24, 100, 30, 8, 16#3060C0}, + {line, 120, 24, 220, 54, 3, 16#FFCC00}, + {circle, 40, 90, 20, 16#20C040}, + {ellipse, 110, 90, 35, 15, 16#C04080}, + {arc, 190, 90, 25, 6, -90, 180, 16#00C0C0}, + {polygon, [{10, 180}, {50, 130}, {90, 180}, {50, 160}], 16#E06020}, + {scaled_cropped_image, 110, 140, 40, 20, transparent, 0, 0, 20, 20, [], Sprite}, + {scaled_cropped_image, 160, 140, 40, 20, transparent, 0, 0, 20, 20, [{flip_x, true}], Sprite}, + {rect, 0, 0, 240, 240, 16#202020} + ], + Display ! {'$call', {self(), make_ref()}, {update, Scene}}, + loop(). + disp(Display, Num) -> Bin = integer_to_binary(Num), Scene = [ {text, 10, 20, default16px, Num, 16#FFFFFF, <<"Test ", Bin/binary>>} ], - % TODO: let's switch back to the following as soon as tuple format is changed in port.c - % Display ! {'$call', {self(), make_ref()}, {update, Scene}}. - Display ! {self(), make_ref(), {update, Scene}}. + Display ! {'$call', {self(), make_ref()}, {update, Scene}}. loop() -> receive diff --git a/shape.c b/shape.c new file mode 100644 index 0000000..1eb1772 --- /dev/null +++ b/shape.c @@ -0,0 +1,1202 @@ +/* + * 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 "shape.h" + +#include +#include +#include + +_Static_assert(SHAPE_VALUE_LIMIT == 32767, "shape.c's overflow bounds assume SHAPE_VALUE_LIMIT is 32767"); + +struct ShapeData +{ + shape_kind_t kind; + int x; + int y; + int w; + int h; +}; + +struct ShapeRowCache +{ + int row; + int left; + int right; +}; + +struct ShapeConvex +{ + struct ShapeData base; + struct ShapeRowCache row_cache; +}; + +struct ShapeRoundedRect +{ + struct ShapeConvex convex; + int radius; +}; + +struct ShapeEllipse +{ + struct ShapeConvex convex; + int cx; + int cy; + int rx; + int ry; +}; + +struct ShapeLine +{ + struct ShapeConvex convex; + int x1; + int y1; + int x2; + int y2; + int thickness; + int cap_radius; + bool x_major; + int a1; + int b1; + int a2; + int b2; + int da; + int db; + int64_t t_da; +}; + +// arc_update_row() inserts at most 13 breakpoints: the bbox ends, the center, two outer +// circle and two hole edges, two half-plane edges and both ends of the run on each of the two +// rays. Every segment starts at a distinct breakpoint, so a row has at most that many segments. +#define ARC_MAX_BREAKPOINTS 13 + +struct ShapeArc +{ + struct ShapeData base; + int cx; + int cy; + int radius; + int thickness; + int sweep; + int32_t start_vx; + int32_t start_vy; + int32_t end_vx; + int32_t end_vy; + int seg_row; + int seg_count; + int seg_start[ARC_MAX_BREAKPOINTS + 1]; + bool seg_inside[ARC_MAX_BREAKPOINTS]; +}; + +struct PolygonEdge +{ + int ytop; + int ybot; + int xtop; + int dx; + int col; + int rem; + int step_col; + int step_rem; +}; + +struct ShapePolygon +{ + struct ShapeData base; + int points_len; + int points_added; + struct ShapePoint first; + struct ShapePoint last; + int edges_len; + int cache_row; + int next_edge; + int active_len; + uint16_t *active; + int toggles_len; + int *toggles; + int last_upto; + struct PolygonEdge edges[]; +}; + +static void *shape_alloc(size_t size, shape_kind_t kind) +{ + struct ShapeData *shape = malloc(size); + if (shape != NULL) { + memset(shape, 0, size); + shape->kind = kind; + } + return shape; +} + +static inline int int_min(int a, int b) +{ + return (a > b) ? b : a; +} + +static inline int int_max(int a, int b) +{ + return (a > b) ? a : b; +} + +// sin(d) for d = 0..90 degrees, Q14 (16384 = 1.0) +static const int16_t sin_q14[91] = { + 0, 286, 572, 857, 1143, 1428, 1713, 1997, 2280, 2563, + 2845, 3126, 3406, 3686, 3964, 4240, 4516, 4790, 5063, 5334, + 5604, 5872, 6138, 6402, 6664, 6924, 7182, 7438, 7692, 7943, + 8192, 8438, 8682, 8923, 9162, 9397, 9630, 9860, 10087, 10311, + 10531, 10749, 10963, 11174, 11381, 11585, 11786, 11982, 12176, 12365, + 12551, 12733, 12911, 13085, 13255, 13421, 13583, 13741, 13894, 14044, + 14189, 14330, 14466, 14598, 14726, 14849, 14968, 15082, 15191, 15296, + 15396, 15491, 15582, 15668, 15749, 15826, 15897, 15964, 16026, 16083, + 16135, 16182, 16225, 16262, 16294, 16322, 16344, 16362, 16374, 16382, + 16384 +}; + +static int normalize_deg(int deg) +{ + deg %= 360; + return (deg < 0) ? deg + 360 : deg; +} + +static int32_t sin_deg(int deg) +{ + deg = normalize_deg(deg); + if (deg <= 90) { + return sin_q14[deg]; + } else if (deg <= 180) { + return sin_q14[180 - deg]; + } else if (deg <= 270) { + return -sin_q14[deg - 180]; + } else { + return -sin_q14[360 - deg]; + } +} + +static int32_t cos_deg(int deg) +{ + return sin_deg(normalize_deg(deg) + 90); +} + +static inline bool in_limit(int v) +{ + return v >= -SHAPE_VALUE_LIMIT && v <= SHAPE_VALUE_LIMIT; +} + +static int32_t floor_div32(int32_t n, int32_t d) +{ + int32_t q = n / d; + if ((n % d) != 0 && n < 0) { + q--; + } + return q; +} + +static int32_t ceil_div32(int32_t n, int32_t d) +{ + return -floor_div32(-n, d); +} + +static int64_t floor_div(int64_t n, int64_t d) +{ + if (n >= INT32_MIN && n <= INT32_MAX && d <= INT32_MAX) { + return floor_div32((int32_t) n, (int32_t) d); + } + int64_t q = n / d; + if ((n % d) != 0 && n < 0) { + q--; + } + return q; +} + +static int64_t ceil_div(int64_t n, int64_t d) +{ + return -floor_div(-n, d); +} + +static void convex_init(struct ShapeConvex *convex) +{ + convex->row_cache.row = INT_MIN; +} + +struct ShapeData *shape_new_rounded_rect(int x, int y, int w, int h, int radius) +{ + if (!in_limit(x) || !in_limit(y) || !in_limit(w) || !in_limit(h) || !in_limit(radius) + || w <= 0 || h <= 0 || radius < 0) { + return NULL; + } + struct ShapeRoundedRect *rr = shape_alloc(sizeof(*rr), ShapeKindRoundedRect); + if (rr == NULL) { + return NULL; + } + convex_init(&rr->convex); + rr->convex.base.x = x; + rr->convex.base.y = y; + rr->convex.base.w = w; + rr->convex.base.h = h; + rr->radius = int_min(radius, (int_min(w, h) - 1) / 2); + return &rr->convex.base; +} + +static void line_bounds(struct ShapeLine *l) +{ + int t = l->thickness; + int r = (t - 1) / 2; + int64_t dx = (int64_t) l->x2 - l->x1; + int64_t dy = (int64_t) l->y2 - l->y1; + int lo_x = int_min(l->x1, l->x2) - r; + int hi_x = int_max(l->x1, l->x2) + r; + int lo_y = int_min(l->y1, l->y2) - r; + int hi_y = int_max(l->y1, l->y2) + r; + if (dx != 0 || dy != 0) { + if ((dx < 0 ? -dx : dx) >= (dy < 0 ? -dy : dy)) { + lo_y = int_min(l->y1, l->y2) - t / 2; + } else { + lo_x = int_min(l->x1, l->x2) - t / 2; + } + } + struct ShapeData *base = &l->convex.base; + base->x = lo_x; + base->y = lo_y; + base->w = hi_x - lo_x + 1; + base->h = hi_y - lo_y + 1; +} + +static void line_frame(struct ShapeLine *l) +{ + int64_t dx = (int64_t) l->x2 - l->x1; + int64_t dy = (int64_t) l->y2 - l->y1; + l->x_major = (dx < 0 ? -dx : dx) >= (dy < 0 ? -dy : dy); + int a1 = l->x_major ? l->x1 : l->y1; + int b1 = l->x_major ? l->y1 : l->x1; + int a2 = l->x_major ? l->x2 : l->y2; + int b2 = l->x_major ? l->y2 : l->x2; + if (a1 > a2) { + l->a1 = a2; + l->b1 = b2; + l->a2 = a1; + l->b2 = b1; + } else { + l->a1 = a1; + l->b1 = b1; + l->a2 = a2; + l->b2 = b2; + } + l->da = l->a2 - l->a1; + l->db = l->b2 - l->b1; + l->t_da = (int64_t) l->thickness * l->da; +} + +struct ShapeData *shape_new_line(int x1, int y1, int x2, int y2, int thickness) +{ + if (!in_limit(x1) || !in_limit(y1) || !in_limit(x2) || !in_limit(y2) || !in_limit(thickness) + || thickness <= 0) { + return NULL; + } + struct ShapeLine *l = shape_alloc(sizeof(*l), ShapeKindLine); + if (l == NULL) { + return NULL; + } + convex_init(&l->convex); + l->x1 = x1; + l->y1 = y1; + l->x2 = x2; + l->y2 = y2; + l->thickness = thickness; + l->cap_radius = (thickness - 1) / 2; + line_bounds(l); + line_frame(l); + return &l->convex.base; +} + +struct ShapeData *shape_new_ellipse(int cx, int cy, int rx, int ry) +{ + if (!in_limit(cx) || !in_limit(cy) || !in_limit(rx) || !in_limit(ry) || rx <= 0 || ry <= 0) { + return NULL; + } + struct ShapeEllipse *e = shape_alloc(sizeof(*e), ShapeKindEllipse); + if (e == NULL) { + return NULL; + } + convex_init(&e->convex); + e->cx = cx; + e->cy = cy; + e->rx = rx; + e->ry = ry; + e->convex.base.x = cx - rx; + e->convex.base.y = cy - ry; + e->convex.base.w = 2 * rx + 1; + e->convex.base.h = 2 * ry + 1; + return &e->convex.base; +} + +struct ShapeData *shape_new_arc(int cx, int cy, int radius, int thickness, int start_deg, + int end_deg) +{ + if (!in_limit(cx) || !in_limit(cy) || !in_limit(radius) || !in_limit(thickness) || radius <= 0 + || thickness <= 0 || start_deg == end_deg) { + return NULL; + } + int sweep = normalize_deg(normalize_deg(end_deg) - normalize_deg(start_deg)); + if (sweep == 0) { + sweep = 360; + } + + struct ShapeArc *arc = shape_alloc(sizeof(*arc), ShapeKindArc); + if (arc == NULL) { + return NULL; + } + arc->base.x = cx - radius; + arc->base.y = cy - radius; + arc->base.w = 2 * radius + 1; + arc->base.h = 2 * radius + 1; + arc->cx = cx; + arc->cy = cy; + arc->radius = radius; + arc->thickness = thickness; + arc->sweep = sweep; + arc->start_vx = cos_deg(start_deg); + arc->start_vy = sin_deg(start_deg); + arc->end_vx = cos_deg(end_deg); + arc->end_vy = sin_deg(end_deg); + arc->seg_row = INT_MIN; + return &arc->base; +} + +static inline bool in_disc(int64_t dx, int64_t dy, int64_t r) +{ + return dx * dx + dy * dy < r * r + r; +} + +static inline bool in_bbox(const struct ShapeData *b, int x, int y) +{ + return x >= b->x && x < b->x + b->w && y >= b->y && y < b->y + b->h; +} + +static int64_t polygon_edge_num(const struct PolygonEdge *e, int y) +{ + int64_t dy = (int64_t) e->ybot - e->ytop; + return (2 * (int64_t) e->xtop - 1) * dy + (int64_t) e->dx * (2 * ((int64_t) y - e->ytop) + 1); +} + +static void polygon_edge_start(struct PolygonEdge *e, int y) +{ + int64_t den = 2 * ((int64_t) e->ybot - e->ytop); + int64_t num = polygon_edge_num(e, y); + int64_t q = floor_div(num, den); + e->col = (int) (q + 1); + e->rem = (int) (num - q * den); +} + +static void polygon_edge_step(struct PolygonEdge *e) +{ + int den = 2 * (e->ybot - e->ytop); + e->col += e->step_col; + e->rem += e->step_rem; + if (e->rem >= den) { + e->rem -= den; + e->col++; + } +} + +static void polygon_update_row(struct ShapePolygon *poly, int y) +{ + struct PolygonEdge *edges = poly->edges; + uint16_t *active = poly->active; + int n = 0; + if (poly->cache_row != INT_MIN && y == poly->cache_row + 1) { + for (int i = 0; i < poly->active_len; i++) { + struct PolygonEdge *e = &edges[active[i]]; + if (e->ybot > y) { + polygon_edge_step(e); + active[n++] = active[i]; + } + } + } else { + poly->next_edge = 0; + } + while (poly->next_edge < poly->edges_len && edges[poly->next_edge].ytop <= y) { + struct PolygonEdge *e = &edges[poly->next_edge]; + if (e->ybot > y) { + polygon_edge_start(e, y); + active[n++] = (uint16_t) poly->next_edge; + } + poly->next_edge++; + } + poly->active_len = n; + + for (int i = 1; i < n; i++) { + uint16_t edge = active[i]; + int col = edges[edge].col; + int j = i; + while (j > 0 && edges[active[j - 1]].col > col) { + active[j] = active[j - 1]; + j--; + } + active[j] = edge; + } + + int out = 0; + int i = 0; + while (i < n) { + int col = edges[active[i]].col; + int j = i + 1; + while (j < n && edges[active[j]].col == col) { + j++; + } + if (((j - i) & 1) != 0) { + poly->toggles[out++] = col; + } + i = j; + } + poly->toggles_len = out; + poly->cache_row = y; +} + +static inline bool polygon_toggles_upto_is(const struct ShapePolygon *poly, int i, int x) +{ + return (i == 0 || poly->toggles[i - 1] <= x) && (i == poly->toggles_len || poly->toggles[i] > x); +} + +static int polygon_toggles_upto(struct ShapePolygon *poly, int x, int y) +{ + if (poly->cache_row != y) { + polygon_update_row(poly, y); + poly->last_upto = 0; + } + int last = poly->last_upto; + if (polygon_toggles_upto_is(poly, last, x)) { + return last; + } + if (last < poly->toggles_len && polygon_toggles_upto_is(poly, last + 1, x)) { + poly->last_upto = last + 1; + return last + 1; + } + int lo = 0; + int hi = poly->toggles_len; + while (lo < hi) { + int mid = lo + (hi - lo) / 2; + if (poly->toggles[mid] <= x) { + lo = mid + 1; + } else { + hi = mid; + } + } + poly->last_upto = lo; + return lo; +} + +static bool polygon_contains(const struct ShapePolygon *poly, int x, int y) +{ + if (!in_bbox(&poly->base, x, y)) { + return false; + } + bool inside = false; + for (int i = 0; i < poly->edges_len; i++) { + const struct PolygonEdge *e = &poly->edges[i]; + if (e->ytop <= y && y < e->ybot + && polygon_edge_num(e, y) < 2 * (int64_t) x * ((int64_t) e->ybot - e->ytop)) { + inside = !inside; + } + } + return inside; +} + +static int polygon_edge_cmp(const void *a, const void *b) +{ + const struct PolygonEdge *ea = a; + const struct PolygonEdge *eb = b; + if (ea->ytop != eb->ytop) { + return (ea->ytop > eb->ytop) - (ea->ytop < eb->ytop); + } + if (ea->ybot != eb->ybot) { + return (ea->ybot > eb->ybot) - (ea->ybot < eb->ybot); + } + if (ea->xtop != eb->xtop) { + return (ea->xtop > eb->xtop) - (ea->xtop < eb->xtop); + } + return (ea->dx > eb->dx) - (ea->dx < eb->dx); +} + +struct ShapeData *shape_polygon_begin(int points_len) +{ + if (points_len < 3 || points_len > SHAPE_POLYGON_MAX_POINTS) { + return NULL; + } + _Static_assert(SHAPE_POLYGON_MAX_POINTS <= UINT16_MAX + 1, "edge indexes must fit uint16_t"); + size_t size = sizeof(struct ShapePolygon) + sizeof(struct PolygonEdge) * points_len + + sizeof(int) * points_len + sizeof(uint16_t) * points_len; + struct ShapePolygon *poly = shape_alloc(size, ShapeKindPolygon); + if (poly == NULL) { + return NULL; + } + poly->points_len = points_len; + poly->toggles = (int *) (poly->edges + points_len); + poly->active = (uint16_t *) (poly->toggles + points_len); + poly->cache_row = INT_MIN; + return &poly->base; +} + +static void polygon_add_edge(struct ShapePolygon *poly, const struct ShapePoint *a, + const struct ShapePoint *b) +{ + if (a->y == b->y) { + return; + } + if (a->y > b->y) { + const struct ShapePoint *t = a; + a = b; + b = t; + } + struct PolygonEdge *e = &poly->edges[poly->edges_len++]; + e->ytop = a->y; + e->ybot = b->y; + e->xtop = a->x; + e->dx = b->x - a->x; + int den = 2 * (e->ybot - e->ytop); + e->step_col = (int) floor_div(2 * (int64_t) e->dx, den); + e->step_rem = 2 * e->dx - e->step_col * den; +} + +bool shape_polygon_add_point(struct ShapeData *shape, int x, int y) +{ + struct ShapePolygon *poly = (struct ShapePolygon *) shape; + if (!in_limit(x) || !in_limit(y) || poly->points_added == poly->points_len) { + return false; + } + struct ShapePoint p = { x, y }; + struct ShapeData *b = &poly->base; + if (poly->points_added == 0) { + poly->first = p; + b->x = x; + b->y = y; + } else { + polygon_add_edge(poly, &poly->last, &p); + int max_x = int_max(b->x + b->w, x); + int max_y = int_max(b->y + b->h, y); + b->x = int_min(b->x, x); + b->y = int_min(b->y, y); + b->w = max_x - b->x; + b->h = max_y - b->y; + } + poly->last = p; + poly->points_added++; + return true; +} + +bool shape_polygon_end(struct ShapeData *shape) +{ + struct ShapePolygon *poly = (struct ShapePolygon *) shape; + if (poly->points_added != poly->points_len) { + return false; + } + polygon_add_edge(poly, &poly->last, &poly->first); + qsort(poly->edges, poly->edges_len, sizeof(struct PolygonEdge), polygon_edge_cmp); + return true; +} + +struct ShapeData *shape_new_polygon(const struct ShapePoint *points, int points_len) +{ + if (points == NULL) { + return NULL; + } + struct ShapeData *shape = shape_polygon_begin(points_len); + if (shape == NULL) { + return NULL; + } + for (int i = 0; i < points_len; i++) { + if (!shape_polygon_add_point(shape, points[i].x, points[i].y)) { + shape_destroy(shape); + return NULL; + } + } + shape_polygon_end(shape); + return shape; +} + +static bool rounded_rect_contains(const struct ShapeRoundedRect *rr, int x, int y) +{ + const struct ShapeData *b = &rr->convex.base; + if (!in_bbox(b, x, y)) { + return false; + } + int r = rr->radius; + if (r == 0) { + return true; + } + + int left = b->x + r; + int right = b->x + b->w - 1 - r; + int top = b->y + r; + int bottom = b->y + b->h - 1 - r; + + int cx; + int cy; + if (x < left) { + cx = left; + } else if (x > right) { + cx = right; + } else { + return true; + } + if (y < top) { + cy = top; + } else if (y > bottom) { + cy = bottom; + } else { + return true; + } + return in_disc(x - cx, y - cy, r); +} + +static bool ellipse_contains(const struct ShapeEllipse *e, int x, int y) +{ + if (!in_bbox(&e->convex.base, x, y)) { + return false; + } + int64_t dx = (int64_t) x - e->cx; + int64_t dy = (int64_t) y - e->cy; + int64_t rx = e->rx; + int64_t ry = e->ry; + // midpoint criterion dx^2 / (rx^2 + rx) + dy^2 / (ry^2 + ry) < 1 + int64_t kx = rx * rx + rx; + int64_t ky = ry * ry + ry; + return dx * dx * ky + dy * dy * kx < kx * ky; +} + +static bool line_in_cap(const struct ShapeLine *l, int64_t da, int64_t db) +{ + int64_t t = l->thickness; + int64_t c = (t % 2 == 0) ? 1 : 0; + int64_t u = 2 * da; + int64_t v = 2 * db + c; + return u * u + v * v < t * t - 1; +} + +static bool line_contains(const struct ShapeLine *l, int x, int y) +{ + if (!in_bbox(&l->convex.base, x, y)) { + return false; + } + if (l->da == 0) { + int64_t dx = (int64_t) x - l->x1; + int64_t dy = (int64_t) y - l->y1; + return (dx == 0 && dy == 0) || in_disc(dx, dy, l->cap_radius); + } + int a = l->x_major ? x : y; + int b = l->x_major ? y : x; + if (line_in_cap(l, (int64_t) a - l->a1, (int64_t) b - l->b1) + || line_in_cap(l, (int64_t) a - l->a2, (int64_t) b - l->b2)) { + return true; + } + if (a < l->a1 || a > l->a2) { + return false; + } + int64_t e = 2 * (((int64_t) b - l->b1) * l->da - ((int64_t) a - l->a1) * l->db); + return -l->t_da <= e && e < l->t_da; +} + +static inline int64_t cross(int64_t ux, int64_t uy, int64_t vx, int64_t vy) +{ + return ux * vy - uy * vx; +} + +static bool arc_in_outer(const struct ShapeArc *arc, int x, int y) +{ + return in_disc((int64_t) x - arc->cx, (int64_t) y - arc->cy, arc->radius); +} + +static bool arc_in_hole(const struct ShapeArc *arc, int x, int y) +{ + int inner_radius = arc->radius - arc->thickness; + if (inner_radius <= 0) { + return false; + } + return in_disc((int64_t) x - arc->cx, (int64_t) y - arc->cy, inner_radius); +} + +static bool arc_cross_a(const struct ShapeArc *arc, int x, int y) +{ + int64_t dx = (int64_t) x - arc->cx; + int64_t dy = (int64_t) y - arc->cy; + if (arc->sweep <= 180) { + return cross(arc->start_vx, arc->start_vy, dx, dy) >= 0; + } + return cross(arc->end_vx, arc->end_vy, dx, dy) > 0; +} + +static bool arc_cross_b(const struct ShapeArc *arc, int x, int y) +{ + int64_t dx = (int64_t) x - arc->cx; + int64_t dy = (int64_t) y - arc->cy; + if (arc->sweep <= 180) { + return cross(dx, dy, arc->end_vx, arc->end_vy) >= 0; + } + return cross(dx, dy, arc->start_vx, arc->start_vy) > 0; +} + +static bool arc_on_ray(int32_t vx, int32_t vy, int64_t dx, int64_t dy) +{ + int64_t c = cross(vx, vy, dx, dy); + int64_t s = (int64_t) (vx < 0 ? -vx : vx) + (vy < 0 ? -vy : vy); + return 2 * (c < 0 ? -c : c) <= s && vx * dx + vy * dy >= 0; +} + +static bool arc_contains(const struct ShapeArc *arc, int x, int y) +{ + if (!in_bbox(&arc->base, x, y)) { + return false; + } + if (!arc_in_outer(arc, x, y) || arc_in_hole(arc, x, y)) { + return false; + } + if (arc->sweep == 360) { + return true; + } + bool in_sweep; + if (arc->sweep <= 180) { + in_sweep = arc_cross_a(arc, x, y) && arc_cross_b(arc, x, y); + } else { + in_sweep = !(arc_cross_a(arc, x, y) && arc_cross_b(arc, x, y)); + } + int64_t dx = (int64_t) x - arc->cx; + int64_t dy = (int64_t) y - arc->cy; + return in_sweep || arc_on_ray(arc->start_vx, arc->start_vy, dx, dy) + || arc_on_ray(arc->end_vx, arc->end_vy, dx, dy); +} + +bool shape_contains(const struct ShapeData *shape, int x, int y) +{ + switch (shape->kind) { + case ShapeKindRoundedRect: + return rounded_rect_contains((const struct ShapeRoundedRect *) shape, x, y); + case ShapeKindLine: + return line_contains((const struct ShapeLine *) shape, x, y); + case ShapeKindEllipse: + return ellipse_contains((const struct ShapeEllipse *) shape, x, y); + case ShapeKindArc: + return arc_contains((const struct ShapeArc *) shape, x, y); + case ShapeKindPolygon: + return polygon_contains((const struct ShapePolygon *) shape, x, y); + default: + return false; + } +} + +static int line_candidate_x(const struct ShapeLine *l, int y, int *last) +{ + int64_t da = l->da; + int64_t db = l->db; + int64_t t = l->t_da; + int r = l->cap_radius; + + if (da > 0) { + if (l->x_major) { + int64_t k = 2 * ((int64_t) y - l->b1) * da; + int64_t lo; + int64_t hi; + if (db > 0) { + lo = floor_div(k - t, 2 * db) + 1; + hi = floor_div(k + t, 2 * db); + } else if (db < 0) { + lo = ceil_div(-(k + t), -2 * db); + hi = ceil_div(t - k, -2 * db) - 1; + } else if (-t <= k && k < t) { + lo = 0; + hi = da; + } else { + lo = 1; + hi = 0; + } + lo = lo < 0 ? 0 : lo; + hi = hi > da ? da : hi; + if (lo <= hi) { + *last = (int) (l->a1 + hi); + return (int) (l->a1 + lo); + } + } else if (y >= l->a1 && y <= l->a2) { + int64_t j = 2 * ((int64_t) y - l->a1) * db; + *last = (int) (l->b1 + ceil_div(j + t, 2 * da) - 1); + return (int) (l->b1 + ceil_div(j - t, 2 * da)); + } + } + int64_t dy1 = (int64_t) y - l->y1; + *last = (dy1 >= -r && dy1 <= r) ? l->x1 : l->x2; + return *last; +} + +static int convex_candidate_x(const struct ShapeData *shape) +{ + switch (shape->kind) { + case ShapeKindRoundedRect: + return shape->x + shape->w / 2; + case ShapeKindEllipse: + return ((const struct ShapeEllipse *) shape)->cx; + default: + return 0; + } +} + +static int line_row_edge(struct ShapeData *shape, int y, int x, int limit, int dir) +{ + int step = 1; + while ((limit - x) * dir >= step && shape_contains(shape, x + step * dir, y)) { + x += step * dir; + step *= 2; + } + int far = ((limit - x) * dir >= step) ? step - 1 : (limit - x) * dir; + int lo = 0; + while (lo < far) { + int mid = lo + (far - lo + 1) / 2; + if (shape_contains(shape, x + mid * dir, y)) { + lo = mid; + } else { + far = mid - 1; + } + } + return x + lo * dir; +} + +static void convex_update_row(struct ShapeConvex *convex, int y) +{ + struct ShapeData *shape = &convex->base; + int bx = shape->x; + int end = shape->x + shape->w; + int line_last = 0; + int candidate = (shape->kind == ShapeKindLine) + ? line_candidate_x((const struct ShapeLine *) shape, y, &line_last) + : convex_candidate_x(shape); + candidate = int_max(bx, int_min(candidate, end - 1)); + + int left; + int right; + if (!shape_contains(shape, candidate, y)) { + left = bx; + right = bx; + } else if (shape->kind == ShapeKindLine) { + int last = int_max(candidate, int_min(line_last, end - 1)); + if (!shape_contains(shape, last, y)) { + last = candidate; + } + left = line_row_edge(shape, y, candidate, bx, -1); + right = line_row_edge(shape, y, last, end - 1, 1) + 1; + } else { + int lo = bx; + int hi = candidate; + while (lo < hi) { + int mid = lo + (hi - lo) / 2; + if (shape_contains(shape, mid, y)) { + hi = mid; + } else { + lo = mid + 1; + } + } + left = lo; + + lo = candidate + 1; + hi = end; + while (lo < hi) { + int mid = lo + (hi - lo) / 2; + if (shape_contains(shape, mid, y)) { + lo = mid + 1; + } else { + hi = mid; + } + } + right = lo; + } + + convex->row_cache.row = y; + convex->row_cache.left = left; + convex->row_cache.right = right; +} + +static int convex_run(struct ShapeConvex *convex, int x, int y, int end, bool *inside) +{ + if (convex->row_cache.row != y) { + convex_update_row(convex, y); + } + int left = convex->row_cache.left; + int right = convex->row_cache.right; + + if (x < left) { + *inside = false; + return left - x; + } + if (x < right) { + *inside = true; + return right - x; + } + *inside = false; + return end - x; +} + +static int find_change(const struct ShapeArc *arc, int y, int lo, int hi, + bool (*pred)(const struct ShapeArc *, int, int)) +{ + if (lo >= hi) { + return hi; + } + bool base = pred(arc, lo, y); + int l = lo; + int h = hi; + while (l < h) { + int mid = l + (h - l) / 2; + if (pred(arc, mid, y) == base) { + l = mid + 1; + } else { + h = mid; + } + } + return l; +} + +static void arc_add_breakpoint(int *bps, int *n, int x, int bx, int end) +{ + if (x < bx) { + x = bx; + } else if (x > end) { + x = end; + } + for (int i = 0; i < *n; i++) { + if (bps[i] == x) { + return; + } + } + int i = *n; + while (i > 0 && bps[i - 1] > x) { + bps[i] = bps[i - 1]; + i--; + } + bps[i] = x; + (*n)++; +} + +static void arc_add_ray_breakpoints(const struct ShapeArc *arc, int32_t vx, int32_t vy, int y, + int *bps, int *n) +{ + int32_t dy = y - arc->cy; + int32_t k = 2 * vx * dy; + int32_t s = (vx < 0 ? -vx : vx) + (vy < 0 ? -vy : vy); + int32_t lo = -arc->radius; + int32_t hi = arc->radius; + if (vy > 0) { + lo = int_max(lo, ceil_div32(k - s, 2 * vy)); + hi = int_min(hi, floor_div32(k + s, 2 * vy)); + } else if (vy < 0) { + lo = int_max(lo, ceil_div32(-(k + s), -2 * vy)); + hi = int_min(hi, floor_div32(s - k, -2 * vy)); + } else if (k < -s || k > s) { + return; + } + int32_t m = vy * dy; + if (vx > 0) { + lo = int_max(lo, ceil_div32(-m, vx)); + } else if (vx < 0) { + hi = int_min(hi, floor_div32(m, -vx)); + } else if (m < 0) { + return; + } + if (lo <= hi) { + int bx = arc->base.x; + int end = arc->base.x + arc->base.w; + arc_add_breakpoint(bps, n, arc->cx + lo, bx, end); + arc_add_breakpoint(bps, n, arc->cx + hi + 1, bx, end); + } +} + +static void arc_update_row(struct ShapeArc *arc, int y) +{ + int bx = arc->base.x; + int end = arc->base.x + arc->base.w; + int bps[ARC_MAX_BREAKPOINTS]; + int n = 0; + arc_add_breakpoint(bps, &n, bx, bx, end); + arc_add_breakpoint(bps, &n, end, bx, end); + + int c = arc->cx; + if (c < bx) { + c = bx; + } else if (c > end) { + c = end; + } + arc_add_breakpoint(bps, &n, c, bx, end); + + bool have_hole = arc->radius - arc->thickness > 0; + if (bx < c) { + arc_add_breakpoint(bps, &n, find_change(arc, y, bx, c, arc_in_outer), bx, end); + if (have_hole) { + arc_add_breakpoint(bps, &n, find_change(arc, y, bx, c, arc_in_hole), bx, end); + } + } + if (c < end) { + arc_add_breakpoint(bps, &n, find_change(arc, y, c, end, arc_in_outer), bx, end); + if (have_hole) { + arc_add_breakpoint(bps, &n, find_change(arc, y, c, end, arc_in_hole), bx, end); + } + } + + if (arc->sweep != 360) { + arc_add_breakpoint(bps, &n, find_change(arc, y, bx, end, arc_cross_a), bx, end); + arc_add_breakpoint(bps, &n, find_change(arc, y, bx, end, arc_cross_b), bx, end); + arc_add_ray_breakpoints(arc, arc->start_vx, arc->start_vy, y, bps, &n); + arc_add_ray_breakpoints(arc, arc->end_vx, arc->end_vy, y, bps, &n); + } + + int seg_count = 0; + arc->seg_start[0] = bps[0]; + bool prev_inside = arc_contains(arc, bps[0], y); + for (int i = 1; i < n; i++) { + bool inside = arc_contains(arc, bps[i], y); + if (inside != prev_inside) { + arc->seg_inside[seg_count] = prev_inside; + seg_count++; + arc->seg_start[seg_count] = bps[i]; + prev_inside = inside; + } + } + arc->seg_inside[seg_count] = prev_inside; + seg_count++; + arc->seg_start[seg_count] = end; + + arc->seg_count = seg_count; + arc->seg_row = y; +} + +static int arc_run(struct ShapeArc *arc, int x, int y, bool *inside) +{ + if (arc->seg_row != y) { + arc_update_row(arc, y); + } + + int i = 0; + while (i + 1 < arc->seg_count && arc->seg_start[i + 1] <= x) { + i++; + } + *inside = arc->seg_inside[i]; + return arc->seg_start[i + 1] - x; +} + +static int polygon_run(struct ShapePolygon *poly, int x, int y, int end, bool *inside) +{ + int i = polygon_toggles_upto(poly, x, y); + *inside = (i & 1) != 0; + if (i < poly->toggles_len && poly->toggles[i] < end) { + return poly->toggles[i] - x; + } + return end - x; +} + +int shape_run(struct ShapeData *shape, int x, int y, bool *inside) +{ + int bx = shape->x; + int end = shape->x + shape->w; + if (y < shape->y || y >= shape->y + shape->h || x >= end) { + *inside = false; + return 1; + } + if (x < bx) { + *inside = false; + int64_t gap = (int64_t) bx - x; + return (gap > INT_MAX) ? INT_MAX : (int) gap; + } + + switch (shape->kind) { + case ShapeKindRoundedRect: + case ShapeKindLine: + case ShapeKindEllipse: + return convex_run((struct ShapeConvex *) shape, x, y, end, inside); + case ShapeKindArc: + return arc_run((struct ShapeArc *) shape, x, y, inside); + case ShapeKindPolygon: + return polygon_run((struct ShapePolygon *) shape, x, y, end, inside); + default: + *inside = false; + return 1; + } +} + +shape_kind_t shape_kind(const struct ShapeData *shape) +{ + return shape->kind; +} + +void shape_bounds(const struct ShapeData *shape, int *x, int *y, int *w, int *h) +{ + *x = shape->x; + *y = shape->y; + *w = shape->w; + *h = shape->h; +} + +bool shape_equal(const struct ShapeData *a, const struct ShapeData *b) +{ + if (a->kind != b->kind) { + return false; + } + switch (a->kind) { + case ShapeKindRoundedRect: { + const struct ShapeRoundedRect *rr_a = (const struct ShapeRoundedRect *) a; + const struct ShapeRoundedRect *rr_b = (const struct ShapeRoundedRect *) b; + return a->x == b->x && a->y == b->y && a->w == b->w && a->h == b->h + && rr_a->radius == rr_b->radius; + } + case ShapeKindLine: { + const struct ShapeLine *l_a = (const struct ShapeLine *) a; + const struct ShapeLine *l_b = (const struct ShapeLine *) b; + return l_a->x1 == l_b->x1 && l_a->y1 == l_b->y1 && l_a->x2 == l_b->x2 + && l_a->y2 == l_b->y2 && l_a->thickness == l_b->thickness; + } + case ShapeKindEllipse: { + const struct ShapeEllipse *e_a = (const struct ShapeEllipse *) a; + const struct ShapeEllipse *e_b = (const struct ShapeEllipse *) b; + return e_a->cx == e_b->cx && e_a->cy == e_b->cy && e_a->rx == e_b->rx + && e_a->ry == e_b->ry; + } + case ShapeKindArc: { + const struct ShapeArc *arc_a = (const struct ShapeArc *) a; + const struct ShapeArc *arc_b = (const struct ShapeArc *) b; + return arc_a->cx == arc_b->cx && arc_a->cy == arc_b->cy + && arc_a->radius == arc_b->radius && arc_a->thickness == arc_b->thickness + && arc_a->sweep == arc_b->sweep && arc_a->start_vx == arc_b->start_vx + && arc_a->start_vy == arc_b->start_vy && arc_a->end_vx == arc_b->end_vx + && arc_a->end_vy == arc_b->end_vy; + } + case ShapeKindPolygon: { + const struct ShapePolygon *poly_a = (const struct ShapePolygon *) a; + const struct ShapePolygon *poly_b = (const struct ShapePolygon *) b; + if (poly_a->edges_len != poly_b->edges_len) { + return false; + } + for (int i = 0; i < poly_a->edges_len; i++) { + if (polygon_edge_cmp(&poly_a->edges[i], &poly_b->edges[i]) != 0) { + return false; + } + } + return true; + } + default: + return false; + } +} + +void shape_destroy(struct ShapeData *shape) +{ + free(shape); +} diff --git a/shape.h b/shape.h new file mode 100644 index 0000000..c2cdc49 --- /dev/null +++ b/shape.h @@ -0,0 +1,74 @@ +/* + * 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 _SHAPE_H_ +#define _SHAPE_H_ + +#include +#include + +#define SHAPE_VALUE_LIMIT 32767 + +#define SHAPE_POLYGON_MAX_POINTS 64 + +typedef enum +{ + ShapeKindRoundedRect, + ShapeKindLine, + ShapeKindEllipse, + ShapeKindArc, + ShapeKindPolygon +} shape_kind_t; + +struct ShapePoint +{ + int x; + int y; +}; + +struct ShapeData; + +struct ShapeData *shape_new_rounded_rect(int x, int y, int w, int h, int radius); +struct ShapeData *shape_new_line(int x1, int y1, int x2, int y2, int thickness); +struct ShapeData *shape_new_ellipse(int cx, int cy, int rx, int ry); +struct ShapeData *shape_new_arc(int cx, int cy, int radius, int thickness, int start_deg, + int end_deg); +struct ShapeData *shape_new_polygon(const struct ShapePoint *points, int points_len); + +// Builds a polygon in place, without an array of points: shape_polygon_begin() allocates it for +// points_len vertices, shape_polygon_add_point() appends each of them in order and +// shape_polygon_end() closes the outline once all of them are in. Destroy the shape if either +// returns false. +struct ShapeData *shape_polygon_begin(int points_len); +bool shape_polygon_add_point(struct ShapeData *shape, int x, int y); +bool shape_polygon_end(struct ShapeData *shape); +void shape_destroy(struct ShapeData *shape); + +shape_kind_t shape_kind(const struct ShapeData *shape); +bool shape_contains(const struct ShapeData *shape, int x, int y); +// Run of pixels from (x, y) with the same inside/outside state: at least 1, never past the +// bounding box; *inside tells which. Updates the shape's row cache, so rows in order are the +// fast path (polygons rebuild on a jump) and one shape must not be walked by two renderers at +// once. +int shape_run(struct ShapeData *shape, int x, int y, bool *inside); +void shape_bounds(const struct ShapeData *shape, int *x, int *y, int *w, int *h); +bool shape_equal(const struct ShapeData *a, const struct ShapeData *b); + +#endif diff --git a/tests/bench/CMakeLists.txt b/tests/bench/CMakeLists.txt new file mode 100644 index 0000000..56f2206 --- /dev/null +++ b/tests/bench/CMakeLists.txt @@ -0,0 +1,41 @@ +# +# 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_bench C) + +set(CMAKE_C_STANDARD 99) +if (NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +if (NOT LIBATOMVM_INCLUDE_PATH) + message(FATAL_ERROR "Set LIBATOMVM_INCLUDE_PATH to AtomVM's src/libAtomVM (headers only, nothing is linked)") +endif() + +add_executable(bench bench.c ../../dcs_lcd_draw.c ../../font_data.c ../../shape.c) +target_include_directories(bench PRIVATE ../.. ../stubs ${LIBATOMVM_INCLUDE_PATH}) +target_compile_definitions(bench PRIVATE HAVE_ATOMIC) +target_compile_options(bench PRIVATE -Wall -Wextra -Wno-unused-parameter) + +add_executable(test_flip test_flip.c) +target_include_directories(test_flip PRIVATE ../.. ../stubs ${LIBATOMVM_INCLUDE_PATH}) +target_compile_definitions(test_flip PRIVATE HAVE_ATOMIC) +target_compile_options(test_flip PRIVATE -Wall -Wextra -Wno-unused-parameter) diff --git a/tests/bench/README.md b/tests/bench/README.md new file mode 100644 index 0000000..5e615fd --- /dev/null +++ b/tests/bench/README.md @@ -0,0 +1,247 @@ + + +# Renderer benchmark and sprite tests + +This directory builds two host programs against the real renderer sources. +They need AtomVM's headers (`display_items.h` includes `context.h`), which +is why they live here and not in `tests/shapes`: + +- `bench`: times the DCS LCD scanline renderer over a set of scenes. +- `test_flip`: checks that `scaled_cropped_image` source pixel lookups, + flipped or not, stay inside the source image and return the same + pixels as a reference mapping kept in the test. + +Build and run (`LIBATOMVM_INCLUDE_PATH` points at AtomVM's +`src/libAtomVM`; only headers are used, nothing from AtomVM is linked): + +```sh +cmake -S tests/bench -B build/bench -DLIBATOMVM_INCLUDE_PATH=/path/to/AtomVM/src/libAtomVM +cmake --build build/bench +./build/bench/bench +./build/bench/test_flip +``` + +## Host benchmark + +`bench.c` calls `dcs_lcd_draw_x()`, the function the ESP32 LCD driver +calls for every pixel run, over a 240x240 screen with a single line +buffer, and builds each row's item list with `display_items_row()` as +the driver does. No ESP-IDF code is linked; `tests/stubs/driver/spi_master.h` +stands in for the one ESP-IDF header the renderer includes. + +Each scene is drawn over a full-screen background rect. The first table +gives the mean time per frame (240 lines, after a warm-up) and per line. +The second table repeats every scene that contains shapes with each shape +replaced by a `rect` of its bounding box: the ratio is what the shapes +cost over flat rects of the same size. + +The scenes cover single shapes, grids of small shapes, 50 overlapping +circles, 12 lines and 5 arcs that all cross the same rows, a 64-point +comb polygon (the most points a polygon may have, with about 32 edge +crossings on every row) next to a 4-point polygon of the same bounding +box, a text UI with 25 text items, and full-screen sprites at scale 1 +and 3, plain and flipped. + +Scaled images are drawn one source pixel at a time, which suits the +ESP32 build (optimized for size with -Os) but does not vectorize on +the host: the sprite scenes at scale 1 are much slower here than on a +build that draws pixel by pixel, while on the device they are not. + +Host numbers are only meaningful relative to each other. The host has no +SPI bus and its CPU is much faster than the ESP32-S3's, so they do not +predict frame rates on the device. Example output (Apple Silicon, -O3): + +``` +scene us/frame us/line +background only 4.3 0.02 +rounded_rect 8.0 0.03 +circle 9.7 0.04 +ellipse 9.4 0.04 +thin line 4.7 0.02 +diagonal thick line 12.6 0.05 +arc gauge 18.8 0.08 +ring 16.0 0.07 +full-screen circle 18.8 0.08 +star polygon 14.0 0.06 +20 bullets 27.3 0.11 +50 overlapping bullets 126.6 0.53 +6 buttons 20.0 0.08 +12 crossing lines 77.0 0.32 +5 concentric arcs 122.1 0.51 +64-point comb polygon 76.5 0.32 +4-point polygon, comb bbox 6.7 0.03 +text UI (25 text items) 74.6 0.31 +sprite x1 224.5 0.94 +sprite x3 78.9 0.33 +sprite x1 flip x 232.7 0.97 +sprite x1 flip xy 230.9 0.96 +sprite x3 flip x 79.1 0.33 +sprite x3 flip xy 79.4 0.33 + +shapes vs. rects of their bounding boxes +scene shape us rect us ratio +rounded_rect 8.0 4.5 1.78 +circle 9.7 4.5 2.17 +ellipse 9.4 4.5 2.08 +thin line 4.7 4.3 1.09 +diagonal thick line 12.6 5.5 2.27 +arc gauge 18.8 5.1 3.70 +ring 16.0 5.1 3.11 +full-screen circle 18.8 4.3 4.34 +star polygon 14.0 5.3 2.66 +20 bullets 27.3 13.7 2.00 +50 overlapping bullets 126.6 36.1 3.50 +6 buttons 20.0 8.7 2.29 +12 crossing lines 77.0 4.4 17.56 +5 concentric arcs 122.1 13.4 9.09 +64-point comb polygon 76.5 5.5 14.01 +4-point polygon, comb bbox 6.7 5.3 1.26 + +64-point comb over a 4-point polygon of its bbox: 11.43x +``` + +The rect version of a scene of long lines is a stack of full-screen +rects, of which only the top one is drawn, hence the large ratios for +the crossing lines and the comb. + +## Line budget on the ESP32-S3 + +The driver draws each line while the previous one is sent over SPI DMA +(see below), so what matters on the device is how long a line takes to +draw compared to how long it takes to send. The time to send a line +depends on the panel and the SPI clock: + +``` +line_time = width * bits_per_pixel / spi_clock +``` + +For example: + +- 240 px RGB565 at 40 MHz: 240 * 16 / 40 MHz = 96 us. +- 320 px RGB565 at 80 MHz: 320 * 16 / 80 MHz = 64 us. + +A line that takes longer than `line_time` to draw delays the next +transfer. + +The table estimates the slowest line of each scene on an ESP32-S3 at +160 MHz. The estimates come from counting, per line, the calls and loop +steps of the renderer and the shape code on the host, and weighting each +with a low and a high cycle count for the Xtensa LX7 running from flash +cache. They are estimates, not measurements; `ATOMGL_PROFILE` gives the +real numbers. + +``` +scene slowest line, us +background only 2 - 3 +rounded_rect 7 - 13 +circle 7 - 13 +ellipse 8 - 14 +thin line 6 - 10 +diagonal thick line 6 - 10 +arc gauge 16 - 30 +ring 12 - 22 +full-screen circle 7 - 13 +star polygon 7 - 13 +20 bullets 41 - 74 +6 buttons 14 - 26 +12 crossing lines 50 - 92 +sprite x1, x3, flipped or not 18 - 30 +5 concentric arcs 77 - 148 +50 overlapping bullets 143 - 254 +``` + +A line costs roughly a fixed amount per item that crosses it plus a +fixed amount per run of pixels the line breaks into, so scenes stay +within the line time as long as each line stays modest (the figures +below are for the 96 us of a 240 px panel at 40 MHz): + +- Long lines: about a dozen crossing the same line, at 4 - 8 us each. +- Arcs: 15 - 30 us each where they cross a line; about 3 to 6 on the + same line. Each arc solves its row from scratch. +- Polygons: about 1 us per edge crossing on a line, so up to roughly 60 + crossings per line. A convex polygon has 2, whatever its number of + points. A line where many edges start costs more, since each of them + is set up there. +- Many overlapping items: every draw call walks the items that cover + the line, so a line where 50 items overlap and break it into 40 runs + costs 2000 item visits. Items above or below the line cost only one + bounding box test per line. Transparent text and image pixels are + skipped as whole runs, so text without a background costs about one + run per glyph stroke. + +Past the line time the frame only gets longer: each slow line delays +the next transfer by its excess. + +## On-device profiling + +`dcs_lcd_display_driver.c` can time each display update. The timer is +compiled in only when the firmware is built with `ATOMGL_PROFILE`, and +costs nothing otherwise: + +```sh +idf.py -DATOMGL_PROFILE=1 build flash monitor +``` + +Each update then logs one line to stderr (visible in `idf.py monitor`): + +``` +atomgl: items, parse us, draw us, max line us, spi wait us, frame us +``` + +- `n` is the number of items in the display list. +- `parse` is the time spent turning the display list into items + (`display_items_init_list()`). +- `draw` is the sum over all lines of the time spent drawing them with + `dcs_lcd_draw_x()`. It does not include SPI waits. +- `max line` is the drawing time of the slowest line, without SPI + waits. +- `spi wait` is the sum of the time spent blocked in + `spi_device_get_trans_result()`, waiting for the previous line's + transfer to finish. +- `frame` is the whole update: parsing, drawing, SPI waits, queueing + the transfers, and freeing the items. + +All times come from `esp_timer_get_time()`, so they are wall-clock +times: if the display task is preempted by another task or an +interrupt while drawing a line, that time is counted too. A single +large `max line` can be preemption; look at many frames, and at `draw` +divided by the number of lines, before blaming one line. + +The driver draws each line while the previous one is sent over SPI +DMA, so each line costs about the larger of its drawing time and +`line_time` (see above). When `draw` divided by the number of lines is +well below `line_time`, most of the frame is `spi wait` and drawing is +hidden behind the transfers. When `spi wait` is close to zero, drawing +is the bottleneck and the frame gets longer with every slow line. + +To try a shape on the device, send it a scene with `port:call/2`, as in +the top-level README: + +```erlang +Scene = [ + {circle, 120, 120, 50, 16#20C040}, + {rect, 0, 0, 240, 240, 16#202020} +], +ok = port:call(Display, {update, Scene}) +``` + +Replace the circle with any other item from `docs/primitives.md` and +compare `max line` in the monitor output. diff --git a/tests/bench/bench.c b/tests/bench/bench.c new file mode 100644 index 0000000..91656ec --- /dev/null +++ b/tests/bench/bench.c @@ -0,0 +1,386 @@ +/* + * 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 "dcs_lcd_draw.h" +#include "dcs_lcd_screen.h" +#include "display_items.h" +#include "shape.h" + +#define SCREEN_SIZE 240 +#define WARMUP_FRAMES 20 +#define MIN_ELAPSED_US 500000.0 +#define MAX_ITEMS 64 +#define MAX_RATIOS 32 + +// 240 px * 16 bit at 40 MHz +#define SPI_US_PER_LINE 96.0 + +static uint16_t line_buf[SCREEN_SIZE]; +static struct DCSLCDScreen screen; +static uint64_t g_checksum = 0; + +struct Ratio +{ + const char *name; + double shape_us; + double rect_us; +}; + +static struct Ratio ratios[MAX_RATIOS]; +static int ratios_len = 0; + +struct Scene +{ + BaseDisplayItem items[MAX_ITEMS]; + struct ShapeData *shapes[MAX_ITEMS]; + int len; + int shapes_len; +}; + +static void render_frame(BaseDisplayItem items[], size_t len) +{ + for (int ypos = 0; ypos < screen.h; ypos++) { + BaseDisplayItem *row = display_items_row(items, len, ypos); + int xpos = 0; + while (xpos < screen.w) { + xpos += dcs_lcd_draw_x(&screen, xpos, ypos, row); + } + for (int i = 0; i < screen.w; i++) { + g_checksum += line_buf[i]; + } + } +} + +static double now_us(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double) ts.tv_sec * 1e6 + (double) ts.tv_nsec / 1e3; +} + +static double run_bench(BaseDisplayItem items[], size_t len) +{ + for (int i = 0; i < WARMUP_FRAMES; i++) { + render_frame(items, len); + } + + double start = now_us(); + long frames = 0; + double elapsed; + do { + render_frame(items, len); + frames++; + elapsed = now_us() - start; + } while (elapsed < MIN_ELAPSED_US); + + return elapsed / (double) frames; +} + +static uint32_t rgba(uint32_t rgb) +{ + return (rgb << 8) | 0xFF; +} + +static BaseDisplayItem *scene_item(struct Scene *scene) +{ + if (scene->len == MAX_ITEMS) { + fprintf(stderr, "too many items in scene\n"); + exit(1); + } + BaseDisplayItem *item = &scene->items[scene->len++]; + memset(item, 0, sizeof(*item)); + return item; +} + +static void add_rect(struct Scene *scene, int x, int y, int w, int h, uint32_t rgb) +{ + BaseDisplayItem *item = scene_item(scene); + item->primitive = PrimitiveRect; + item->x = x; + item->y = y; + item->width = w; + item->height = h; + item->brcolor = rgba(rgb); +} + +static void add_background(struct Scene *scene) +{ + add_rect(scene, 0, 0, SCREEN_SIZE, SCREEN_SIZE, 0x202020); +} + +static void add_shape(struct Scene *scene, struct ShapeData *shape, uint32_t rgb) +{ + if (shape == NULL) { + fprintf(stderr, "invalid shape in scene\n"); + exit(1); + } + scene->shapes[scene->shapes_len++] = shape; + BaseDisplayItem *item = scene_item(scene); + item->primitive = PrimitiveShape; + item->brcolor = rgba(rgb); + item->data.shape_data.shape = shape; + shape_bounds(shape, &item->x, &item->y, &item->width, &item->height); +} + +static void add_text(struct Scene *scene, int x, int y, const char *text, uint32_t fg, bool bg) +{ + BaseDisplayItem *item = scene_item(scene); + item->primitive = PrimitiveText; + item->x = x; + item->y = y; + item->width = (int) strlen(text) * 8; + item->height = 16; + item->brcolor = bg ? rgba(0x303030) : 0; + item->data.text_data.fgcolor = rgba(fg); + item->data.text_data.text = text; +} + +static void free_scene(struct Scene *scene) +{ + for (int i = 0; i < scene->shapes_len; i++) { + shape_destroy(scene->shapes[i]); + } +} + +static void print_scene(const char *name, double us) +{ + double us_line = us / (double) SCREEN_SIZE; + printf("%-26s %10.1f %9.2f\n", name, us, us_line); +} + +static double bench_scene(const char *name, struct Scene *scene) +{ + double us = run_bench(scene->items, scene->len); + print_scene(name, us); + + if (scene->shapes_len > 0) { + BaseDisplayItem rects[MAX_ITEMS]; + memcpy(rects, scene->items, sizeof(rects[0]) * scene->len); + for (int i = 0; i < scene->len; i++) { + if (rects[i].primitive == PrimitiveShape) { + rects[i].primitive = PrimitiveRect; + rects[i].data.shape_data.shape = NULL; + } + } + if (ratios_len < MAX_RATIOS) { + ratios[ratios_len++] = (struct Ratio){ name, us, run_bench(rects, scene->len) }; + } + } + + free_scene(scene); + return us; +} + +static void bench_sprite(const char *name, int scale, bool flip_x, bool flip_y) +{ + int side = SCREEN_SIZE / scale; + uint8_t *pix = malloc((size_t) side * side * 4); + for (int i = 0; i < side * side; i++) { + pix[4 * i + 0] = (uint8_t) (i * 7); + pix[4 * i + 1] = (uint8_t) (i * 13); + pix[4 * i + 2] = (uint8_t) (i * 29); + pix[4 * i + 3] = 0xFF; + } + + struct Scene scene = { .len = 0, .shapes_len = 0 }; + BaseDisplayItem *item = scene_item(&scene); + item->primitive = PrimitiveScaledCroppedImage; + item->width = SCREEN_SIZE; + item->height = SCREEN_SIZE; + item->x_scale = scale; + item->y_scale = scale; + item->data.image_data_with_size.width = side; + item->data.image_data_with_size.height = side; + item->data.image_data_with_size.pix = (const char *) pix; + item->flip_x = flip_x; + item->flip_y = flip_y; + add_background(&scene); + + bench_scene(name, &scene); + free(pix); +} + +static double bench_shape(const char *name, uint32_t rgb, struct ShapeData *shape) +{ + struct Scene scene = { 0 }; + add_shape(&scene, shape, rgb); + add_background(&scene); + return bench_scene(name, &scene); +} + +static const char *const labels[] = { + "Temperature", "Humidity", "Pressure", "Wind", "Rain", "UV index", + "Sunrise", "Sunset", "Battery", "WiFi", "Uptime", "Free heap" +}; +static const char *const values[] = { + "21.5 C", "48 %", "1013 hPa", "12 km/h", "0.2 mm", "3", + "07:31", "19:02", "87 %", "-61 dBm", "3d 04h", "112 KiB" +}; + +int main(void) +{ + memset(&screen, 0, sizeof(screen)); + screen.w = SCREEN_SIZE; + screen.h = SCREEN_SIZE; + screen.pixels = line_buf; + + printf("%-26s %10s %9s\n", "scene", "us/frame", "us/line"); + + { + struct Scene scene = { 0 }; + add_background(&scene); + bench_scene("background only", &scene); + } + + bench_shape("rounded_rect", 0x3060C0, shape_new_rounded_rect(70, 70, 100, 100, 12)); + bench_shape("circle", 0x20C040, shape_new_ellipse(120, 120, 50, 50)); + bench_shape("ellipse", 0xC04080, shape_new_ellipse(120, 120, 80, 40)); + bench_shape("thin line", 0xFFCC00, shape_new_line(10, 115, 230, 125, 1)); + bench_shape("diagonal thick line", 0xFFCC00, shape_new_line(20, 20, 220, 220, 3)); + bench_shape("arc gauge", 0x00C0C0, shape_new_arc(120, 120, 60, 10, 0, 270)); + bench_shape("ring", 0x00C0C0, shape_new_arc(120, 120, 60, 10, 0, 360)); + bench_shape("full-screen circle", 0x20C040, shape_new_ellipse(120, 120, 119, 119)); + + { + static const struct ShapePoint star[] = { + { 120, 20 }, { 150, 95 }, { 230, 95 }, { 165, 140 }, { 190, 220 }, + { 120, 170 }, { 50, 220 }, { 75, 140 }, { 10, 95 }, { 90, 95 } + }; + bench_shape("star polygon", 0xE06020, shape_new_polygon(star, sizeof(star) / sizeof(star[0]))); + } + + { + struct Scene scene = { 0 }; + for (int j = 0; j < 4; j++) { + for (int i = 0; i < 5; i++) { + add_shape(&scene, shape_new_ellipse(30 + 45 * i, 40 + 50 * j, 8, 8), 0x4080FF); + } + } + add_background(&scene); + bench_scene("20 bullets", &scene); + } + + { + struct Scene scene = { 0 }; + for (int j = 0; j < 5; j++) { + for (int i = 0; i < 10; i++) { + add_shape(&scene, shape_new_ellipse(20 + 22 * i, 30 + 45 * j, 16, 16), 0x4080FF + 0x10 * i); + } + } + add_background(&scene); + bench_scene("50 overlapping bullets", &scene); + } + + { + struct Scene scene = { 0 }; + for (int r = 0; r < 3; r++) { + for (int c = 0; c < 2; c++) { + add_shape(&scene, shape_new_rounded_rect(20 + 110 * c, 20 + 70 * r, 90, 50, 10), 0x3060C0); + } + } + add_background(&scene); + bench_scene("6 buttons", &scene); + } + + { + struct Scene scene = { 0 }; + for (int i = 0; i < 12; i++) { + add_shape(&scene, shape_new_line(0, 10 + 20 * i, 239, 230 - 20 * i, 1 + i % 4), 0xFFCC00); + } + add_background(&scene); + bench_scene("12 crossing lines", &scene); + } + + { + struct Scene scene = { 0 }; + for (int i = 0; i < 5; i++) { + add_shape(&scene, shape_new_arc(120, 120, 30 + 20 * i, 8, 30 * i, 30 * i + 270), 0x00C0C0); + } + add_background(&scene); + bench_scene("5 concentric arcs", &scene); + } + + double comb_us; + double box_us; + { + struct ShapePoint comb[SHAPE_POLYGON_MAX_POINTS]; + int teeth = SHAPE_POLYGON_MAX_POINTS / 4; + for (int t = 0; t < teeth; t++) { + int x0 = 10 + (2 * t) * 220 / (2 * teeth); + int x1 = 10 + (2 * t + 1) * 220 / (2 * teeth); + comb[4 * t + 0] = (struct ShapePoint){ x0, 230 }; + comb[4 * t + 1] = (struct ShapePoint){ x0, 10 }; + comb[4 * t + 2] = (struct ShapePoint){ x1 + 1, 10 }; + comb[4 * t + 3] = (struct ShapePoint){ x1 + 1, 230 }; + } + static char comb_name[32]; + snprintf(comb_name, sizeof(comb_name), "%d-point comb polygon", SHAPE_POLYGON_MAX_POINTS); + struct ShapeData *s = shape_new_polygon(comb, SHAPE_POLYGON_MAX_POINTS); + int bx, by, bw, bh; + shape_bounds(s, &bx, &by, &bw, &bh); + comb_us = bench_shape(comb_name, 0xE06020, s); + struct ShapePoint box[] = { { bx, by }, { bx + bw, by }, { bx + bw, by + bh }, { bx, by + bh } }; + box_us = bench_shape("4-point polygon, comb bbox", 0xE06020, shape_new_polygon(box, 4)); + } + + { + struct Scene scene = { 0 }; + add_text(&scene, 8, 0, "Weather station", 0xFFFFFF, false); + for (int i = 0; i < 12; i++) { + add_text(&scene, 8, 20 + 18 * i, labels[i], 0xC0C0C0, false); + add_text(&scene, 136, 20 + 18 * i, values[i], 0xFFCC00, i % 2 == 0); + } + add_rect(&scene, 0, 0, SCREEN_SIZE, 17, 0x3060C0); + add_background(&scene); + bench_scene("text UI (25 text items)", &scene); + } + + bench_sprite("sprite x1", 1, false, false); + bench_sprite("sprite x3", 3, false, false); + bench_sprite("sprite x1 flip x", 1, true, false); + bench_sprite("sprite x1 flip xy", 1, true, true); + bench_sprite("sprite x3 flip x", 3, true, false); + bench_sprite("sprite x3 flip xy", 3, true, true); + + printf("\nshapes vs. rects of their bounding boxes\n"); + printf("%-26s %10s %10s %7s\n", "scene", "shape us", "rect us", "ratio"); + for (int i = 0; i < ratios_len; i++) { + printf("%-26s %10.1f %10.1f %7.2f\n", ratios[i].name, ratios[i].shape_us, ratios[i].rect_us, + ratios[i].shape_us / ratios[i].rect_us); + } + + printf("\n%d-point comb over a 4-point polygon of its bbox: %.2fx\n", SHAPE_POLYGON_MAX_POINTS, + comb_us / box_us); + + printf("\nchecksum: %llu\n", (unsigned long long) g_checksum); + printf("\nHost numbers are only meaningful relative to each other: the host CPU\n" + "is not an ESP32-S3. On the device a 240 px RGB565 line takes %.0f us\n" + "to send at 40 MHz SPI, and drawing the next line is hidden behind that\n" + "transfer as long as it is faster. Measure on the device with\n" + "ATOMGL_PROFILE, see tests/bench/README.md.\n", + SPI_US_PER_LINE); + + return 0; +} diff --git a/tests/bench/test_flip.c b/tests/bench/test_flip.c new file mode 100644 index 0000000..45bbb64 --- /dev/null +++ b/tests/bench/test_flip.c @@ -0,0 +1,256 @@ +/* + * 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 "display_items.h" + +static int failures = 0; + +// Reference mapping, one division per pixel: the stepping helpers must agree with it +static int expected_src(int px, int len, int src_len, int scale, bool flip) +{ + if (flip) { + long covered = (long) src_len * scale; + px = (int) ((len < covered) ? len : covered) - 1 - px; + } + int src = (px < 0) ? 0 : px / scale; + return (src > src_len - 1) ? src_len - 1 : src; +} + +static const uint32_t *expected_pixel(const BaseDisplayItem *item, int col_px, int row_px) +{ + const struct ImageDataWithSize *img = &item->data.image_data_with_size; + int row = expected_src(row_px, item->height, img->height - item->source_y, item->y_scale, + item->flip_y); + int col = expected_src(col_px, item->width, img->width - item->source_x, item->x_scale, + item->flip_x); + return ((const uint32_t *) img->pix) + (item->source_y + row) * img->width + item->source_x + + col; +} + +static void check_in_bounds(const char *name, BaseDisplayItem *item, const uint32_t *pix, + int img_width, int img_height) +{ + const uint32_t *lo = pix; + const uint32_t *hi = pix + (size_t) img_width * (size_t) img_height; + + for (int row_px = 0; row_px < item->height; row_px++) { + struct ScaledCroppedRow src; + display_items_scaled_cropped_row_init(&src, item, row_px); + int col_px = 0; + while (col_px < item->width) { + int run; + const uint32_t *p = display_items_scaled_cropped_row_run(&src, col_px, item->width, &run); + if (p < lo || p >= hi) { + fprintf(stderr, "%s: (%d,%d) -> pointer %ld outside [0, %d)\n", name, col_px, + row_px, (long) (p - pix), img_width * img_height); + failures++; + } + col_px += run; + } + } +} + +static BaseDisplayItem make_item(int width, int height, int source_x, int source_y, int x_scale, + int y_scale, bool flip_x, bool flip_y, int img_width, int img_height, const uint32_t *pix) +{ + BaseDisplayItem item; + memset(&item, 0, sizeof(item)); + item.primitive = PrimitiveScaledCroppedImage; + item.width = width; + item.height = height; + item.source_x = source_x; + item.source_y = source_y; + item.x_scale = x_scale; + item.y_scale = y_scale; + item.flip_x = flip_x; + item.flip_y = flip_y; + item.data.image_data_with_size.width = img_width; + item.data.image_data_with_size.height = img_height; + item.data.image_data_with_size.pix = (const char *) pix; + return item; +} + +// Every run, walked from any start column, covers 1 to (end - start) columns that all map to +// the pixel it returns +static void check_row_walk(const char *name, BaseDisplayItem *item) +{ + for (int row_px = 0; row_px < item->height; row_px++) { + struct ScaledCroppedRow src; + display_items_scaled_cropped_row_init(&src, item, row_px); + for (int start = 0; start < item->width; start++) { + int col_px = start; + while (col_px < item->width) { + int run; + const uint32_t *p = display_items_scaled_cropped_row_run(&src, col_px, item->width, &run); + if (run < 1 || col_px + run > item->width) { + fprintf(stderr, "%s: run %d at (%d,%d)\n", name, run, col_px, row_px); + failures++; + return; + } + for (int k = 0; k < run; k++) { + const uint32_t *expected = expected_pixel(item, col_px + k, row_px); + if (p != expected) { + fprintf(stderr, "%s: (%d,%d) walked from %d is %ld pixels off\n", name, + col_px + k, row_px, start, (long) (p - expected)); + failures++; + return; + } + } + col_px += run; + } + } + } +} + +static void check_mirror(const char *name, const BaseDisplayItem *item) +{ + BaseDisplayItem plain = *item; + plain.flip_x = false; + plain.flip_y = false; + for (int row_px = 0; row_px < item->height; row_px++) { + for (int col_px = 0; col_px < item->width; col_px++) { + int mirror_col = item->flip_x ? item->width - 1 - col_px : col_px; + int mirror_row = item->flip_y ? item->height - 1 - row_px : row_px; + const uint32_t *p = expected_pixel(item, col_px, row_px); + const uint32_t *expected = expected_pixel(&plain, mirror_col, mirror_row); + if (p != expected) { + fprintf(stderr, "%s: (%d,%d) is not the mirror of (%d,%d)\n", name, col_px, row_px, + mirror_col, mirror_row); + failures++; + return; + } + } + } +} + +static uint32_t rng_state = 12345; + +static int rng_range(int lo, int hi) +{ + rng_state = rng_state * 1103515245u + 12345u; + return lo + (int) ((rng_state >> 8) % (uint32_t) (hi - lo + 1)); +} + +static void test_row_walk_random(void) +{ + static uint32_t pix[24 * 24]; + for (int i = 0; i < 3000; i++) { + int img_width = rng_range(1, 24); + int img_height = rng_range(1, 24); + int source_x = rng_range(0, img_width - 1); + int source_y = rng_range(0, img_height - 1); + int x_scale = rng_range(1, 4); + int y_scale = rng_range(1, 4); + int width; + int height; + switch (rng_range(0, 2)) { + case 0: + width = (img_width - source_x) * x_scale; + height = (img_height - source_y) * y_scale; + break; + case 1: + width = (img_width - source_x) * x_scale + rng_range(0, x_scale - 1); + height = (img_height - source_y) * y_scale + rng_range(0, y_scale - 1); + break; + default: + width = rng_range(1, 110); + height = rng_range(1, 30); + break; + } + BaseDisplayItem item = make_item(width, height, source_x, source_y, x_scale, y_scale, + rng_range(0, 1), rng_range(0, 1), img_width, img_height, pix); + int before = failures; + check_in_bounds("random item", &item, pix, img_width, img_height); + check_row_walk("random item", &item); + if (width <= (img_width - source_x) * x_scale && height <= (img_height - source_y) * y_scale) { + check_mirror("random item", &item); + } + if (failures != before) { + fprintf(stderr, " item %dx%d src (%d,%d) scale %dx%d flip %d/%d image %dx%d\n", width, + height, source_x, source_y, x_scale, y_scale, item.flip_x, item.flip_y, img_width, + img_height); + return; + } + } +} + +int main(void) +{ + { + static const uint32_t pix[4] = { 0 }; + BaseDisplayItem item = make_item(7, 1, 1, 0, 2, 1, false, false, 4, 1, pix); + check_in_bounds("horizontal unflipped over-read", &item, pix, 4, 1); + check_row_walk("horizontal unflipped over-read", &item); + } + + { + static const uint32_t pix[4] = { 0 }; + BaseDisplayItem item = make_item(7, 1, 0, 0, 2, 1, true, false, 4, 1, pix); + check_in_bounds("horizontal flip_x under-read", &item, pix, 4, 1); + check_row_walk("horizontal flip_x under-read", &item); + } + + { + static const uint32_t pix[4] = { 0 }; + BaseDisplayItem item = make_item(1, 7, 0, 1, 1, 2, false, false, 1, 4, pix); + check_in_bounds("vertical unflipped over-read", &item, pix, 1, 4); + check_row_walk("vertical unflipped over-read", &item); + } + + { + static const uint32_t pix[4] = { 0 }; + BaseDisplayItem item = make_item(1, 7, 0, 0, 1, 1, false, true, 1, 4, pix); + check_in_bounds("vertical flip_y under-read", &item, pix, 1, 4); + check_row_walk("vertical flip_y under-read", &item); + } + + { + static const uint32_t pix[16 * 8] = { 0 }; + BaseDisplayItem item = make_item(20, 10, 3, 1, 2, 2, true, true, 16, 8, pix); + check_in_bounds("both axes flipped, cropped and scaled", &item, pix, 16, 8); + check_row_walk("both axes flipped, cropped and scaled", &item); + } + + { + static const uint32_t pix[3] = { 0 }; + BaseDisplayItem item = make_item(5, 1, 0, 0, 2, 1, true, false, 3, 1, pix); + static const int expected[5] = { 2, 1, 1, 0, 0 }; + for (int c = 0; c < 5; c++) { + if (expected_pixel(&item, c, 0) != pix + expected[c]) { + fprintf(stderr, "odd width flip: column %d shows %ld, expected %d\n", c, + (long) (expected_pixel(&item, c, 0) - pix), expected[c]); + failures++; + } + } + check_row_walk("odd width flip", &item); + } + + test_row_walk_random(); + + if (failures) { + fprintf(stderr, "%d failure(s)\n", failures); + return 1; + } + printf("all flip clamp tests passed\n"); + return 0; +} diff --git a/tests/items/CMakeLists.txt b/tests/items/CMakeLists.txt index 249c22b..bf30d06 100644 --- a/tests/items/CMakeLists.txt +++ b/tests/items/CMakeLists.txt @@ -33,18 +33,20 @@ 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 + ../../display_items.c ../../shape.c ../../dcs_lcd_draw.c ../../mono_draw.c + ../../epaper_draw.c ../../epaper_color.c ../../font_data.c) +set_source_files_properties(../../display_items.c ../../shape.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_link_libraries(test_items PRIVATE m) 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 + set_source_files_properties(../../dcs_lcd_draw.c ../../mono_draw.c ../../epaper_draw.c PROPERTIES COMPILE_OPTIONS "-fno-sanitize=shift-base") # term_from_int left-shifts negative values set_source_files_properties(test_items.c PROPERTIES diff --git a/tests/items/test_items.c b/tests/items/test_items.c index aa369f3..46d08d2 100644 --- a/tests/items/test_items.c +++ b/tests/items/test_items.c @@ -34,6 +34,9 @@ #include "dcs_lcd_draw.h" #include "dcs_lcd_screen.h" #include "display_items.h" +#include "epaper_color.h" +#include "epaper_draw.h" +#include "mono_draw.h" #include "alloc_counter.h" @@ -156,6 +159,18 @@ 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 flips(bool flip_x, bool flip_y) +{ + term opts = term_nil(); + if (flip_y) { + opts = cons(tuple(2, atom("flip_y"), TRUE_ATOM), opts); + } + if (flip_x) { + opts = cons(tuple(2, atom("flip_x"), TRUE_ATOM), opts); + } + return opts; +} + 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) { @@ -267,14 +282,15 @@ 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, flip_y", sci(4, 2, 0, sy, 1, 1, flips(false, true), ok_img)); 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_y huge, flip_y", sci(4, 2, 0, 100000000, 1, 1, flips(false, true), ok_img)); + expect_invalid("source_x >= image width, flip_x", sci(4, 2, 4, 0, 1, 1, flips(true, false), 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 0, flip_x", sci(4, 1, 0, 0, 1, 1, flips(true, false), 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)))); @@ -343,11 +359,82 @@ static void test_image_invalid(void) } } -static void test_command_invalid(void) +static void test_shape_invalid(void) { + term c = term_from_int(0xFF0000); + 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))); + expect_invalid("circle with ellipse arity", + tuple(6, atom("circle"), term_from_int(10), term_from_int(10), term_from_int(5), term_from_int(5), c)); + expect_invalid("ellipse with circle arity", tuple(5, atom("ellipse"), term_from_int(10), term_from_int(10), term_from_int(5), c)); + expect_invalid("circle radius above limit", + tuple(5, atom("circle"), term_from_int(10), term_from_int(10), term_from_int(SHAPE_VALUE_LIMIT + 1), c)); + expect_invalid("line thickness 0", + tuple(7, atom("line"), term_from_int(0), term_from_int(0), term_from_int(9), term_from_int(9), term_from_int(0), c)); + expect_invalid("rounded_rect color not an integer", + tuple(7, atom("rounded_rect"), term_from_int(0), term_from_int(0), term_from_int(9), term_from_int(9), term_from_int(2), atom("red"))); + expect_invalid("rounded_rect radius above limit", + tuple(7, atom("rounded_rect"), term_from_int(0), term_from_int(0), term_from_int(9), term_from_int(9), term_from_int(SHAPE_VALUE_LIMIT + 1), c)); + expect_invalid("arc equal angles", + tuple(8, atom("arc"), term_from_int(20), term_from_int(20), term_from_int(10), term_from_int(3), term_from_int(45), term_from_int(45), c)); + expect_invalid("arc angle not an integer", + tuple(8, atom("arc"), term_from_int(20), term_from_int(20), term_from_int(10), term_from_int(3), atom("a"), term_from_int(45), c)); + expect_invalid("arc thickness above limit", + tuple(8, atom("arc"), term_from_int(20), term_from_int(20), term_from_int(10), term_from_int(SHAPE_VALUE_LIMIT + 1), + term_from_int(0), term_from_int(90), c)); + + term pts = list(3, tuple(2, term_from_int(0), term_from_int(0)), tuple(2, term_from_int(9), term_from_int(0)), + tuple(2, term_from_int(0), term_from_int(9))); + expect_invalid("polygon with 2 points", + tuple(3, atom("polygon"), list(2, tuple(2, term_from_int(0), term_from_int(0)), tuple(2, term_from_int(9), term_from_int(0))), c)); + expect_invalid("polygon improper list", + tuple(3, atom("polygon"), + cons(tuple(2, term_from_int(0), term_from_int(0)), + cons(tuple(2, term_from_int(9), term_from_int(0)), cons(tuple(2, term_from_int(0), term_from_int(9)), term_from_int(1)))), + c)); + expect_invalid("polygon point not a tuple", + tuple(3, atom("polygon"), list(3, tuple(2, term_from_int(0), term_from_int(0)), term_from_int(9), tuple(2, term_from_int(0), term_from_int(9))), c)); + expect_invalid("polygon point arity 3", + tuple(3, atom("polygon"), + list(3, tuple(2, term_from_int(0), term_from_int(0)), tuple(3, term_from_int(9), term_from_int(0), term_from_int(0)), + tuple(2, term_from_int(0), term_from_int(9))), + c)); + expect_invalid("polygon point above limit", + tuple(3, atom("polygon"), + list(3, tuple(2, term_from_int(0), term_from_int(0)), tuple(2, term_from_int(SHAPE_VALUE_LIMIT + 1), term_from_int(0)), + tuple(2, term_from_int(0), term_from_int(9))), + c)); + expect_invalid("polygon not a list", tuple(3, atom("polygon"), term_from_int(4), c)); + expect_invalid("polygon arity 4", tuple(4, atom("polygon"), pts, c, term_from_int(0))); + + term many = term_nil(); + for (int i = 0; i < SHAPE_POLYGON_MAX_POINTS + 1; i++) { + many = cons(tuple(2, term_from_int(i % 2 ? 40 : 0), term_from_int(i % 40)), many); + } + expect_invalid("polygon above the point cap", tuple(3, atom("polygon"), many, c)); + + if (big_ints()) { + avm_int_t wrap = ((avm_int_t) 1 << 32) + 5; + expect_invalid("rounded_rect width 2^32 + 5", + tuple(7, atom("rounded_rect"), term_from_int(0), term_from_int(0), term_from_int(wrap), term_from_int(9), term_from_int(2), c)); + expect_invalid("circle radius 2^32 + 5", + tuple(5, atom("circle"), term_from_int(10), term_from_int(10), term_from_int(wrap), c)); + expect_invalid("circle cx 2^32 + 5", + tuple(5, atom("circle"), term_from_int(wrap), term_from_int(10), term_from_int(3), c)); + expect_invalid("line thickness 2^32 + 1", + tuple(7, atom("line"), term_from_int(0), term_from_int(0), term_from_int(9), term_from_int(9), term_from_int(wrap - 4), c)); + expect_invalid("polygon point 2^32 + 5", + tuple(3, atom("polygon"), + list(3, tuple(2, term_from_int(0), term_from_int(0)), tuple(2, term_from_int(wrap), term_from_int(0)), + tuple(2, term_from_int(0), term_from_int(9))), + c)); + avm_int_t huge = (avm_int_t) 1 << 40; + expect_invalid("arc equal huge angles", + tuple(8, atom("arc"), term_from_int(20), term_from_int(20), term_from_int(10), term_from_int(3), term_from_int(huge), + term_from_int(huge), c)); + } } static void test_image_valid(void) @@ -373,7 +460,7 @@ 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(), + term_from_int(0xABCDEF), term_from_int(3), term_from_int(1), term_from_int(2), term_from_int(3), flips(true, true), rgba(4, 2, bin)), &item); CHECK(item.primitive == PrimitiveScaledCroppedImage, "sci: primitive %d", item.primitive); @@ -382,16 +469,148 @@ static void test_scaled_cropped_image_valid(void) 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.flip_x && item.flip_y, "sci: flips %d %d", item.flip_x, item.flip_y); 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); + parse(sci(4, 2, 0, 0, 1, 1, list(2, tuple(2, atom("flip_x"), FALSE_ATOM), term_from_int(3)), rgba(4, 2, pixels(32))), + &item); + CHECK(item.primitive == PrimitiveScaledCroppedImage && !item.flip_x && !item.flip_y, + "sci with false/garbage opts: primitive %d flips %d %d", item.primitive, item.flip_x, item.flip_y); + delete_item(&item); + + struct + { + const char *name; + term opts; + bool flip_x; + bool flip_y; + } opts[] = { + { "bare atoms", list(2, atom("flip_y"), atom("flip_x")), true, true }, + { "bare flip_x", list(1, atom("flip_x")), true, false }, + { "mixed forms", list(3, atom("flip_y"), tuple(2, atom("flip_x"), TRUE_ATOM), atom("other")), true, true }, + { "{flip_y, 1}", list(1, tuple(2, atom("flip_y"), term_from_int(1))), false, false }, + { "{flip_x}", list(1, tuple(1, atom("flip_x"))), false, false }, + { "not a list", atom("flip_x"), false, false }, + { "a tuple", tuple(2, atom("flip_x"), TRUE_ATOM), false, false }, + { "improper tail", cons(atom("flip_y"), atom("flip_x")), false, true }, + }; + for (size_t i = 0; i < sizeof(opts) / sizeof(opts[0]); i++) { + parse(sci(4, 2, 0, 0, 1, 1, opts[i].opts, rgba(4, 2, pixels(32))), &item); + CHECK(item.primitive == PrimitiveScaledCroppedImage && item.flip_x == opts[i].flip_x + && item.flip_y == opts[i].flip_y, + "sci opts %s: primitive %d flips %d %d", opts[i].name, item.primitive, item.flip_x, item.flip_y); + delete_item(&item); + } + + parse(sci(9, 9, 3, 1, 3, 3, flips(true, true), rgba(4, 2, pixels(32))), &item); CHECK(item.primitive == PrimitiveScaledCroppedImage, "sci at last pixel: primitive %d", item.primitive); delete_item(&item); } +static void check_shape(const char *name, term req, shape_kind_t kind, int x, int y, int w, int h, + BaseDisplayItem *out) +{ + BaseDisplayItem item; + parse(req, &item); + CHECK(item.primitive == PrimitiveShape && item.data.shape_data.shape != NULL, "%s: primitive %d", name, item.primitive); + if (out) { + memset(out, 0, sizeof(*out)); + } + if (item.primitive != PrimitiveShape) { + return; + } + CHECK(shape_kind(item.data.shape_data.shape) == kind, "%s: kind %d", name, shape_kind(item.data.shape_data.shape)); + CHECK(item.brcolor == 0x123456FF, "%s: brcolor %#x", name, (unsigned) item.brcolor); + CHECK(item.x == x && item.y == y && item.width == w && item.height == h, + "%s: bbox (%d, %d, %d, %d), expected (%d, %d, %d, %d)", name, item.x, item.y, item.width, + item.height, x, y, w, h); + if (out) { + *out = item; + } else { + delete_item(&item); + } +} + +static void check_arc_sweep(avm_int_t start, avm_int_t end, int sweep) +{ + BaseDisplayItem item; + char name[96]; + snprintf(name, sizeof(name), "arc %lld..%lld", (long long) start, (long long) end); + check_shape(name, + tuple(8, atom("arc"), term_from_int(20), term_from_int(20), term_from_int(10), term_from_int(3), term_from_int(start), term_from_int(end), + term_from_int(0x123456)), + ShapeKindArc, 10, 10, 21, 21, &item); + if (item.primitive == PrimitiveShape) { + int ref_start = (int) (((start % 360) + 360) % 360); + struct ShapeData *ref = shape_new_arc(20, 20, 10, 3, ref_start, ref_start + sweep); + CHECK(ref != NULL && shape_equal(item.data.shape_data.shape, ref), "%s: sweep is not %d", name, sweep); + shape_destroy(ref); + delete_item(&item); + } +} + +static void test_shape_valid(void) +{ + term c = term_from_int(0x123456); + + check_shape("circle", tuple(5, atom("circle"), term_from_int(10), term_from_int(12), term_from_int(4), c), ShapeKindEllipse, 6, 8, + 9, 9, NULL); + check_shape("ellipse", tuple(6, atom("ellipse"), term_from_int(10), term_from_int(12), term_from_int(4), term_from_int(2), c), + ShapeKindEllipse, 6, 10, 9, 5, NULL); + check_shape("rounded_rect", + tuple(7, atom("rounded_rect"), term_from_int(1), term_from_int(2), term_from_int(30), term_from_int(20), term_from_int(5), c), + ShapeKindRoundedRect, 1, 2, 30, 20, NULL); + check_shape("line", tuple(7, atom("line"), term_from_int(0), term_from_int(5), term_from_int(40), term_from_int(5), term_from_int(1), c), + ShapeKindLine, 0, 5, 41, 1, NULL); + check_shape("circle at the value limit", + tuple(5, atom("circle"), term_from_int(-SHAPE_VALUE_LIMIT), term_from_int(SHAPE_VALUE_LIMIT), term_from_int(SHAPE_VALUE_LIMIT), c), + ShapeKindEllipse, -2 * SHAPE_VALUE_LIMIT, 0, 2 * SHAPE_VALUE_LIMIT + 1, 2 * SHAPE_VALUE_LIMIT + 1, NULL); + check_shape("polygon", + tuple(3, atom("polygon"), + list(3, tuple(2, term_from_int(2), term_from_int(3)), tuple(2, term_from_int(30), term_from_int(3)), + tuple(2, term_from_int(2), term_from_int(25))), + c), + ShapeKindPolygon, 2, 3, 28, 22, NULL); + + term many = term_nil(); + struct ShapePoint many_points[SHAPE_POLYGON_MAX_POINTS]; + for (int i = 0; i < SHAPE_POLYGON_MAX_POINTS; i++) { + many = cons(tuple(2, term_from_int(i % 2 ? 40 : 0), term_from_int(i / 2)), many); + many_points[SHAPE_POLYGON_MAX_POINTS - 1 - i] = (struct ShapePoint){ i % 2 ? 40 : 0, i / 2 }; + } + BaseDisplayItem comb; + check_shape("polygon at the point cap", tuple(3, atom("polygon"), many, c), ShapeKindPolygon, 0, 0, 40, SHAPE_POLYGON_MAX_POINTS / 2 - 1, &comb); + if (comb.primitive == PrimitiveShape) { + struct ShapeData *ref = shape_new_polygon(many_points, SHAPE_POLYGON_MAX_POINTS); + CHECK(ref != NULL && shape_equal(comb.data.shape_data.shape, ref), "polygon cap: points differ"); + shape_destroy(ref); + delete_item(&comb); + } + + check_arc_sweep(0, 90, 90); + check_arc_sweep(0, -90, 270); + check_arc_sweep(0, -359, 1); + check_arc_sweep(0, 400, 40); + check_arc_sweep(0, 360, 360); + check_arc_sweep(0, -360, 360); + check_arc_sweep(0, 720, 360); + check_arc_sweep(-720, 0, 360); + check_arc_sweep(350, 10, 20); + check_arc_sweep(10, 350, 340); + if (big_ints()) { + avm_int_t huge = (avm_int_t) 1 << 40; + check_arc_sweep(huge, huge + 90, 90); + check_arc_sweep(huge, huge + 360, 360); + check_arc_sweep(-huge, -huge - 90, 270); + avm_int_t turns = (avm_int_t) 360 << 32; + check_arc_sweep(turns + 5, 5, 360); + check_arc_sweep(turns + 5, 95, 90); + } +} + static void test_integer_forms(void) { BaseDisplayItem item; @@ -418,6 +637,38 @@ static void test_integer_forms(void) 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)); + expect_invalid("circle radius boxed 2^40", + tuple(5, atom("circle"), term_from_int(0), term_from_int(0), boxed_int((avm_int64_t) 1 << 40), term_from_int(0))); + + struct + { + avm_int64_t start; + avm_int64_t end; + int ref_start; + int sweep; + } arcs[] = { + { (avm_int64_t) 1 << 27, ((avm_int64_t) 1 << 27) + 90, (int) (((avm_int64_t) 1 << 27) % 360), 90 }, + { -((avm_int64_t) 1 << 27), 0, (int) (360 - ((avm_int64_t) 1 << 27) % 360), (int) (((avm_int64_t) 1 << 27) % 360) }, + { INT64_MAX, INT64_MAX - 10, (int) (INT64_MAX % 360), 350 }, + { INT64_MIN, 0, (int) (360 + INT64_MIN % 360), (int) (-(INT64_MIN % 360)) }, + { (avm_int64_t) 360 << 40, 0, 0, 360 }, + }; + for (size_t i = 0; i < sizeof(arcs) / sizeof(arcs[0]); i++) { + parse(tuple(8, atom("arc"), term_from_int(20), term_from_int(20), term_from_int(10), term_from_int(3), boxed_int(arcs[i].start), + boxed_int(arcs[i].end), term_from_int(0)), + &item); + CHECK(item.primitive == PrimitiveShape, "boxed arc %zu: primitive %d", i, item.primitive); + if (item.primitive == PrimitiveShape) { + struct ShapeData *ref = shape_new_arc(20, 20, 10, 3, arcs[i].ref_start, arcs[i].ref_start + arcs[i].sweep); + CHECK(ref != NULL && shape_equal(item.data.shape_data.shape, ref), "boxed arc %zu: expected %d + %d", i, + arcs[i].ref_start, arcs[i].sweep); + shape_destroy(ref); + } + delete_item(&item); + } + expect_invalid("arc equal boxed angles", + tuple(8, atom("arc"), term_from_int(20), term_from_int(20), term_from_int(10), term_from_int(3), boxed_int(INT64_MIN), + boxed_int(INT64_MIN), term_from_int(0))); } static term text_binary(const char *text) @@ -567,6 +818,20 @@ static void test_text(void) expect_invalid_on_alloc_failure("text", text(term_from_int(0), font, fg, transparent, string("abc")), 1); } +static void test_alloc_failures(void) +{ + term c = term_from_int(0x123456); + expect_invalid_on_alloc_failure("rounded_rect", + tuple(7, atom("rounded_rect"), term_from_int(0), term_from_int(0), term_from_int(9), term_from_int(9), term_from_int(2), c), 1); + expect_invalid_on_alloc_failure("circle", tuple(5, atom("circle"), term_from_int(10), term_from_int(10), term_from_int(4), c), 1); + expect_invalid_on_alloc_failure("polygon", + tuple(3, atom("polygon"), + list(3, tuple(2, term_from_int(0), term_from_int(0)), tuple(2, term_from_int(9), term_from_int(0)), + tuple(2, term_from_int(0), term_from_int(9))), + c), + 1); +} + static term *heap_mark(void) { return heap.heap_ptr; @@ -730,6 +995,73 @@ static void test_log_format(void) heap_release(mark); } +static void expect_reason(const char *name, term req, const char *expected) +{ + static char log[256]; + BaseDisplayItem item; + begin_capture(); + display_items_init_item(&item, req, &ctx); + end_capture(log, sizeof(log)); + delete_item(&item); + CHECK(strcmp(log, expected) == 0, "%s: logged\n%s", name, log); +} + +static void expect_reason_on_alloc_failure(const char *name, term req, const char *expected) +{ + alloc_counter_fail_at = alloc_counter_calls + 1; + expect_reason(name, req, expected); + alloc_counter_fail_at = -1; +} + +static void test_shape_reasons(void) +{ + term *mark = heap_mark(); + term c = term_from_int(0xFF0000); + + expect_reason("rounded_rect arity", + tuple(6, atom("rounded_rect"), term_from_int(0), term_from_int(0), term_from_int(9), term_from_int(9), term_from_int(2)), + "invalid display list item (rounded_rect/6): wrong arity\n"); + expect_reason("rounded_rect zero width", + tuple(7, atom("rounded_rect"), term_from_int(0), term_from_int(0), term_from_int(0), term_from_int(9), term_from_int(2), c), + "invalid display list item (rounded_rect/7): bad position, size, radius or color\n"); + expect_reason_on_alloc_failure("rounded_rect", + tuple(7, atom("rounded_rect"), term_from_int(0), term_from_int(0), term_from_int(9), term_from_int(9), term_from_int(2), c), + "invalid display list item (rounded_rect/7): out of memory\n"); + expect_reason("circle arity", tuple(4, atom("circle"), term_from_int(0), term_from_int(0), term_from_int(3)), + "invalid display list item (circle/4): wrong arity\n"); + expect_reason("circle zero radius", tuple(5, atom("circle"), term_from_int(0), term_from_int(0), term_from_int(0), c), + "invalid display list item (circle/5): bad center, radius or color\n"); + expect_reason_on_alloc_failure("circle", tuple(5, atom("circle"), term_from_int(0), term_from_int(0), term_from_int(3), c), + "invalid display list item (circle/5): out of memory\n"); + + expect_reason("ellipse zero radius", + tuple(6, atom("ellipse"), term_from_int(0), term_from_int(0), term_from_int(3), term_from_int(0), c), + "invalid display list item (ellipse/6): bad center, radii or color\n"); + expect_reason("line zero thickness", + tuple(7, atom("line"), term_from_int(0), term_from_int(0), term_from_int(9), term_from_int(9), term_from_int(0), c), + "invalid display list item (line/7): bad points, thickness or color\n"); + expect_reason("arc equal angles", + tuple(8, atom("arc"), term_from_int(20), term_from_int(20), term_from_int(10), term_from_int(3), term_from_int(45), term_from_int(45), c), + "invalid display list item (arc/8): bad center, radius, thickness, angles or color\n"); + expect_reason("polygon two points", tuple(3, atom("polygon"), list(2, tuple(2, term_from_int(0), term_from_int(0)), tuple(2, term_from_int(4), term_from_int(0))), c), + "invalid display list item (polygon/3): bad points or color\n"); + expect_reason("polygon point not a pair", tuple(3, atom("polygon"), list(3, tuple(2, term_from_int(0), term_from_int(0)), tuple(2, term_from_int(4), term_from_int(0)), term_from_int(4)), c), + "invalid display list item (polygon/3): bad points or color\n"); + term too_many = term_nil(); + for (int i = 0; i < SHAPE_POLYGON_MAX_POINTS + 1; i++) { + too_many = cons(tuple(2, term_from_int(i % 2), term_from_int(i)), too_many); + } + expect_reason("polygon above the point cap", tuple(3, atom("polygon"), too_many, c), + "invalid display list item (polygon/3): too many points\n"); + expect_reason_on_alloc_failure("polygon", + tuple(3, atom("polygon"), + list(3, tuple(2, term_from_int(0), term_from_int(0)), tuple(2, term_from_int(9), term_from_int(0)), + tuple(2, term_from_int(0), term_from_int(9))), + c), + "invalid display list item (polygon/3): out of memory\n"); + heap_release(mark); +} + static void test_new_list(void) { static char log[4096]; @@ -804,6 +1136,8 @@ struct Sprite int source_y; int x_scale; int y_scale; + bool flip_x; + bool flip_y; int img_width; int img_height; const uint8_t *bytes; @@ -831,6 +1165,12 @@ static bool sprite_pixel(const struct Sprite *s, int px, int py, uint16_t *color if (px < 0 || py < 0 || px >= drawn_w || py >= drawn_h) { return false; } + if (s->flip_x) { + px = drawn_w - 1 - px; + } + if (s->flip_y) { + py = drawn_h - 1 - py; + } 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)); @@ -843,8 +1183,9 @@ static void check_sprite_pixels(const struct Sprite *s, int seed) 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); + "sprite %dx%d from (%d, %d) of %dx%d, scale %dx%d, flip %d %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->flip_x, s->flip_y, + s->x, s->y); memset(expected, 0, sizeof(expected)); for (int y = 0; y < SCREEN_H; y++) { @@ -857,7 +1198,7 @@ static void check_sprite_pixels(const struct Sprite *s, int seed) 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(), + term_from_int(s->x_scale), term_from_int(s->y_scale), flips(s->flip_x, s->flip_y), rgba(s->img_width, s->img_height, bin)); BaseDisplayItem item; @@ -875,6 +1216,291 @@ static void check_sprite_pixels(const struct Sprite *s, int seed) heap_release(mark); } +static uint32_t scene_rng = 1; + +static int scene_rand(int n) +{ + scene_rng = scene_rng * 1103515245u + 12345u; + return (int) ((scene_rng >> 8) % (uint32_t) n); +} + +static uint32_t scene_color(void) +{ + return ((uint32_t) scene_rand(0x1000000) << 8) | 0xFF; +} + +typedef enum +{ + RendererDcsLcd, + RendererMono, + RendererEpaper +} renderer_t; + +static const char *const renderer_names[] = { "dcs_lcd", "mono", "epaper" }; + +static int renderer_line_bytes(renderer_t renderer) +{ + switch (renderer) { + case RendererMono: + return SCREEN_W / 8; + case RendererEpaper: + return SCREEN_W / 2; + default: + return SCREEN_W * 2; + } +} + +static int renderer_draw_x(renderer_t renderer, int w, uint16_t *line, int xpos, int ypos, BaseDisplayItem *row) +{ + switch (renderer) { + case RendererMono: { + struct MonoScreen screen = { .w = w, .h = SCREEN_H }; + return mono_draw_x(&screen, (uint8_t *) line, xpos, ypos, row); + } + case RendererEpaper: { + struct EpaperScreen screen = { .w = w, .h = SCREEN_H, .palette = epaper_acep_palette, .palette_size = 7 }; + return epaper_draw_x(&screen, (uint8_t *) line, xpos, ypos, row); + } + default: { + struct DCSLCDScreen screen; + memset(&screen, 0, sizeof(screen)); + screen.w = w; + screen.h = SCREEN_H; + screen.pixels = line; + return dcs_lcd_draw_x(&screen, xpos, ypos, row); + } + } +} + +// One pixel per call: no run is ever longer than 1, so this is the reference for the runs +static void render_items(renderer_t renderer, BaseDisplayItem *items, size_t len, uint8_t *frame, bool one_pixel) +{ + uint16_t line[SCREEN_W]; + int line_bytes = renderer_line_bytes(renderer); + + for (int ypos = 0; ypos < SCREEN_H; ypos++) { + memset(line, 0, sizeof(line)); + BaseDisplayItem *row = display_items_row(items, len, ypos); + int xpos = 0; + while (xpos < SCREEN_W) { + int drawn_pixels = renderer_draw_x(renderer, one_pixel ? xpos + 1 : SCREEN_W, line, xpos, ypos, row); + if (drawn_pixels <= 0 || (one_pixel && drawn_pixels != 1)) { + fprintf(stderr, "%s draw_x returned %d at (%d, %d)\n", renderer_names[renderer], drawn_pixels, xpos, + ypos); + abort(); + } + xpos += drawn_pixels; + } + memcpy(frame + ypos * line_bytes, line, line_bytes); + } +} + +static uint32_t *scene_image(int width, int height) +{ + static const uint8_t alphas[] = { 0, 0, 0x80, 0xFF, 0xFF, 0xFF }; + uint32_t *pix = malloc(sizeof(uint32_t) * width * height); + if (pix == NULL) { + abort(); + } + for (int i = 0; i < width * height; i++) { + uint32_t rgb = (uint32_t) scene_rand(0x1000000); + uint8_t bytes[4] = { rgb >> 16, rgb >> 8, rgb, alphas[scene_rand(sizeof(alphas))] }; + memcpy(&pix[i], bytes, 4); + } + return pix; +} + +static struct ShapeData *scene_shape(void) +{ + int x = scene_rand(SCREEN_W + 20) - 10; + int y = scene_rand(SCREEN_H + 20) - 10; + switch (scene_rand(5)) { + case 0: + return shape_new_rounded_rect(x, y, 1 + scene_rand(40), 1 + scene_rand(30), scene_rand(12)); + case 1: + return shape_new_ellipse(x, y, 1 + scene_rand(20), 1 + scene_rand(20)); + case 2: + return shape_new_line(x, y, scene_rand(SCREEN_W), scene_rand(SCREEN_H), 1 + scene_rand(6)); + case 3: + return shape_new_arc(x, y, 1 + scene_rand(24), 1 + scene_rand(8), scene_rand(360), + scene_rand(360) + 1); + default: { + struct ShapePoint points[8]; + int n = 3 + scene_rand(6); + for (int i = 0; i < n; i++) { + points[i] = (struct ShapePoint){ x + scene_rand(40), y + scene_rand(30) }; + } + return shape_new_polygon(points, n); + } + } +} + +static void test_runs_match_pixels(void) +{ + static const char *const texts[] = { "Hi", "a b", " .:", "#O", "i I", " " }; + static uint8_t expected[SCREEN_W * 2 * SCREEN_H]; + static uint8_t frame[SCREEN_W * 2 * SCREEN_H]; + + for (int scene = 0; scene < 2000; scene++) { + BaseDisplayItem items[12]; + uint32_t *images[12]; + size_t len = 1 + scene_rand(12); + memset(items, 0, sizeof(items)); + for (size_t i = 0; i < len; i++) { + BaseDisplayItem *item = &items[i]; + images[i] = NULL; + item->x = scene_rand(SCREEN_W + 10) - 5; + item->y = scene_rand(SCREEN_H + 10) - 5; + item->brcolor = scene_rand(2) ? scene_color() : 0; + switch (scene_rand(5)) { + case 0: + item->primitive = PrimitiveRect; + item->width = 1 + scene_rand(40); + item->height = 1 + scene_rand(30); + item->brcolor = scene_color(); + break; + case 1: + item->primitive = PrimitiveText; + item->data.text_data.text = texts[scene_rand(sizeof(texts) / sizeof(texts[0]))]; + item->data.text_data.fgcolor = scene_color(); + item->width = (int) strlen(item->data.text_data.text) * 8; + item->height = 16; + break; + case 2: + item->primitive = PrimitiveImage; + item->width = 1 + scene_rand(24); + item->height = 1 + scene_rand(24); + images[i] = scene_image(item->width, item->height); + item->data.image_data.pix = (const char *) images[i]; + break; + case 3: { + int img_width = 1 + scene_rand(12); + int img_height = 1 + scene_rand(12); + item->primitive = PrimitiveScaledCroppedImage; + item->source_x = scene_rand(img_width); + item->source_y = scene_rand(img_height); + item->x_scale = 1 + scene_rand(3); + item->y_scale = 1 + scene_rand(3); + item->flip_x = scene_rand(2); + item->flip_y = scene_rand(2); + item->width = 1 + scene_rand((img_width - item->source_x) * item->x_scale); + item->height = 1 + scene_rand((img_height - item->source_y) * item->y_scale); + images[i] = scene_image(img_width, img_height); + item->data.image_data_with_size.width = img_width; + item->data.image_data_with_size.height = img_height; + item->data.image_data_with_size.pix = (const char *) images[i]; + break; + } + default: { + struct ShapeData *shape = scene_shape(); + if (shape == NULL) { + item->primitive = PrimitiveRect; + item->width = 1; + item->height = 1; + item->brcolor = scene_color(); + break; + } + item->primitive = PrimitiveShape; + item->brcolor = scene_color(); + item->data.shape_data.shape = shape; + shape_bounds(shape, &item->x, &item->y, &item->width, &item->height); + break; + } + } + } + + bool same = true; + for (renderer_t renderer = RendererDcsLcd; renderer <= RendererEpaper && same; renderer++) { + render_items(renderer, items, len, expected, true); + render_items(renderer, items, len, frame, false); + same = memcmp(expected, frame, renderer_line_bytes(renderer) * SCREEN_H) == 0; + CHECK(same, "%s scene %d: rendering by runs differs from rendering pixel by pixel", + renderer_names[renderer], scene); + } + + for (size_t i = 0; i < len; i++) { + if (items[i].primitive == PrimitiveShape) { + shape_destroy(items[i].data.shape_data.shape); + } + free(images[i]); + } + if (!same) { + return; + } + } +} + +static int draw_image_x(renderer_t renderer, BaseDisplayItem *item) +{ + uint16_t line[SCREEN_W]; + memset(line, 0, sizeof(line)); + bool scaled = item->primitive == PrimitiveScaledCroppedImage; + switch (renderer) { + case RendererMono: { + struct MonoScreen screen = { .w = SCREEN_W, .h = SCREEN_H }; + return scaled ? mono_draw_scaled_cropped_img_x(&screen, (uint8_t *) line, 0, 0, 2, item) + : mono_draw_image_x(&screen, (uint8_t *) line, 0, 0, 2, item); + } + case RendererEpaper: { + struct EpaperScreen screen = { .w = SCREEN_W, .h = SCREEN_H, .palette = epaper_acep_palette, .palette_size = 7 }; + return scaled ? epaper_draw_scaled_cropped_img_x(&screen, (uint8_t *) line, 0, 0, 2, item) + : epaper_draw_image_x(&screen, (uint8_t *) line, 0, 0, 2, item); + } + default: { + struct DCSLCDScreen screen; + memset(&screen, 0, sizeof(screen)); + screen.w = SCREEN_W; + screen.h = SCREEN_H; + screen.pixels = line; + return scaled ? dcs_lcd_draw_scaled_cropped_img_x(&screen, 0, 0, 2, item) + : dcs_lcd_draw_image_x(&screen, 0, 0, 2, item); + } + } +} + +static void test_image_alpha(void) +{ + static const uint8_t transparent_red[4] = { 0xFF, 0, 0, 0 }; + static const uint8_t opaque_black[4] = { 0, 0, 0, 0xFF }; + struct + { + const uint8_t *pixels[2]; + int expected; + } cases[] = { + { { transparent_red, opaque_black }, -1 }, + { { opaque_black, transparent_red }, 1 }, + { { transparent_red, transparent_red }, -2 }, + }; + + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + uint8_t pix[8]; + memcpy(pix, cases[i].pixels[0], 4); + memcpy(pix + 4, cases[i].pixels[1], 4); + for (renderer_t renderer = RendererDcsLcd; renderer <= RendererEpaper; renderer++) { + for (int scaled = 0; scaled < 2; scaled++) { + BaseDisplayItem item; + memset(&item, 0, sizeof(item)); + item.width = 2; + item.height = 1; + if (scaled) { + item.primitive = PrimitiveScaledCroppedImage; + item.x_scale = 1; + item.y_scale = 1; + item.data.image_data_with_size.width = 2; + item.data.image_data_with_size.height = 1; + item.data.image_data_with_size.pix = (const char *) pix; + } else { + item.primitive = PrimitiveImage; + item.data.image_data.pix = (const char *) pix; + } + int drawn = draw_image_x(renderer, &item); + CHECK(drawn == cases[i].expected, "%s %s alpha case %zu: returned %d, expected %d", + renderer_names[renderer], scaled ? "scaled image" : "image", i, drawn, cases[i].expected); + } + } + } +} + static void test_sprite_pixels(void) { enum @@ -891,31 +1517,35 @@ static void test_sprite_pixels(void) 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; + for (int f = 0; f < 4; f++) { + 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; + s.flip_x = f & 1; + s.flip_y = f & 2; + 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++; } - s.x = (seed % 5 == 4) ? -3 : 2 + seed % 7; - s.y = (seed % 5 == 4) ? -2 : 1 + seed % 5; - check_sprite_pixels(&s, seed); - seed++; } } } @@ -933,17 +1563,22 @@ int main(void) test_scaled_cropped_image_invalid(); test_image_invalid(); - test_command_invalid(); + test_shape_invalid(); test_image_valid(); test_scaled_cropped_image_valid(); + test_shape_valid(); test_integer_forms(); test_rect(); test_text(); + test_alloc_failures(); test_log_format(); + test_shape_reasons(); test_new_list(); test_huge_rect_pixels(); test_image_pixels(); test_sprite_pixels(); + test_runs_match_pixels(); + test_image_alpha(); render(all_items, NULL); diff --git a/tests/shapes/CMakeLists.txt b/tests/shapes/CMakeLists.txt new file mode 100644 index 0000000..a3ed490 --- /dev/null +++ b/tests/shapes/CMakeLists.txt @@ -0,0 +1,34 @@ +# +# 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_shapes_tests C) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Werror") + +option(SHAPES_UBSAN "Build with -fsanitize=undefined, aborting on the first report" OFF) +if(SHAPES_UBSAN) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=undefined -fno-sanitize-recover=all -fno-omit-frame-pointer") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=undefined") +endif() + +add_executable(test_shapes test_shapes.c ../../shape.c) +target_include_directories(test_shapes PRIVATE ../..) diff --git a/tests/shapes/test_shapes.c b/tests/shapes/test_shapes.c new file mode 100644 index 0000000..05a4e12 --- /dev/null +++ b/tests/shapes/test_shapes.c @@ -0,0 +1,1614 @@ +/* + * 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 "shape.h" + +static int failures = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "%s:%d: CHECK failed: %s\n", __FILE__, __LINE__, \ + #cond); \ + failures++; \ + } \ + } while (0) + +static void check_render(struct ShapeData *shape, const char *expected, int line) +{ + int bx, by, bw, bh; + shape_bounds(shape, &bx, &by, &bw, &bh); + char *buf = malloc(bh * (bw + 1) + 1); + char *p = buf; + for (int y = by; y < by + bh; y++) { + for (int x = bx; x < bx + bw; x++) { + *p++ = shape_contains(shape, x, y) ? '#' : '.'; + } + *p++ = '\n'; + } + *p = '\0'; + if (strcmp(buf, expected) != 0) { + fprintf(stderr, "%s:%d: render mismatch\nexpected:\n%sgot:\n%s", __FILE__, line, + expected, buf); + failures++; + } + free(buf); +} + +#define CHECK_RENDER(shape, expected) check_render(shape, expected, __LINE__) + +static void check_bounds(struct ShapeData *shape, int x, int y, int w, int h, int line) +{ + int bx, by, bw, bh; + shape_bounds(shape, &bx, &by, &bw, &bh); + if (bx != x || by != y || bw != w || bh != h) { + fprintf(stderr, "%s:%d: bounds (%d,%d,%d,%d) != expected (%d,%d,%d,%d)\n", __FILE__, + line, bx, by, bw, bh, x, y, w, h); + failures++; + } +} + +#define CHECK_BOUNDS(shape, x, y, w, h) check_bounds(shape, x, y, w, h, __LINE__) + +static bool same_pixels(struct ShapeData *a, struct ShapeData *b) +{ + int ax, ay, aw, ah; + int bx, by, bw, bh; + shape_bounds(a, &ax, &ay, &aw, &ah); + shape_bounds(b, &bx, &by, &bw, &bh); + int x0 = ax < bx ? ax : bx; + int y0 = ay < by ? ay : by; + int x1 = ax + aw > bx + bw ? ax + aw : bx + bw; + int y1 = ay + ah > by + bh ? ay + ah : by + bh; + for (int y = y0 - 1; y <= y1; y++) { + for (int x = x0 - 1; x <= x1; x++) { + if (shape_contains(a, x, y) != shape_contains(b, x, y)) { + return false; + } + } + } + return true; +} + +static void test_circle_r2(void) +{ + struct ShapeData *s; + CHECK((s = shape_new_ellipse(10, 20, 2, 2)) != NULL); + CHECK_BOUNDS(s, 8, 18, 5, 5); + CHECK_RENDER(s, + ".###.\n" + "#####\n" + "#####\n" + "#####\n" + ".###.\n"); + shape_destroy(s); +} + +static void test_circle_small(void) +{ + struct ShapeData *s; + CHECK((s = shape_new_ellipse(0, 0, 1, 1)) != NULL); + CHECK_BOUNDS(s, -1, -1, 3, 3); + CHECK_RENDER(s, + ".#.\n" + "###\n" + ".#.\n"); + shape_destroy(s); + + CHECK((s = shape_new_ellipse(0, 0, 3, 3)) != NULL); + CHECK_RENDER(s, + "..###..\n" + ".#####.\n" + "#######\n" + "#######\n" + "#######\n" + ".#####.\n" + "..###..\n"); + shape_destroy(s); + + CHECK((s = shape_new_ellipse(0, 0, 3, 1)) != NULL); + CHECK_RENDER(s, + ".#####.\n" + "#######\n" + ".#####.\n"); + shape_destroy(s); +} + +static void test_ellipse_symmetry(void) +{ + struct ShapeData *s; + CHECK((s = shape_new_ellipse(0, 0, 9, 4)) != NULL); + CHECK_BOUNDS(s, -9, -4, 19, 9); + for (int y = -4; y <= 4; y++) { + for (int x = -9; x <= 9; x++) { + CHECK(shape_contains(s, x, y) == shape_contains(s, -x, y)); + CHECK(shape_contains(s, x, y) == shape_contains(s, x, -y)); + } + } + CHECK(shape_contains(s, 9, 0)); + CHECK(shape_contains(s, 0, 4)); + CHECK(!shape_contains(s, 10, 0)); + CHECK(!shape_contains(s, 9, 4)); + shape_destroy(s); +} + +static void test_ellipse_invalid(void) +{ + CHECK(shape_new_ellipse(0, 0, 0, 5) == NULL); + CHECK(shape_new_ellipse(0, 0, 5, -1) == NULL); +} + +static void test_rounded_rect(void) +{ + struct ShapeData *s; + CHECK((s = shape_new_rounded_rect(3, 4, 6, 4, 2)) != NULL); + CHECK_BOUNDS(s, 3, 4, 6, 4); + CHECK_RENDER(s, + ".####.\n" + "######\n" + "######\n" + ".####.\n"); + shape_destroy(s); +} + +static void test_rounded_rect_small_radii(void) +{ + struct ShapeData *s; + CHECK((s = shape_new_rounded_rect(0, 0, 5, 3, 1)) != NULL); + CHECK_RENDER(s, + ".###.\n" + "#####\n" + ".###.\n"); + shape_destroy(s); + + CHECK((s = shape_new_rounded_rect(0, 0, 7, 5, 1)) != NULL); + CHECK_RENDER(s, + ".#####.\n" + "#######\n" + "#######\n" + "#######\n" + ".#####.\n"); + shape_destroy(s); + + CHECK((s = shape_new_rounded_rect(0, 0, 7, 5, 2)) != NULL); + CHECK_RENDER(s, + ".#####.\n" + "#######\n" + "#######\n" + "#######\n" + ".#####.\n"); + shape_destroy(s); + + CHECK((s = shape_new_rounded_rect(0, 0, 9, 7, 3)) != NULL); + CHECK_RENDER(s, + "..#####..\n" + ".#######.\n" + "#########\n" + "#########\n" + "#########\n" + ".#######.\n" + "..#####..\n"); + shape_destroy(s); + + CHECK((s = shape_new_rounded_rect(0, 0, 2, 2, 1)) != NULL); + CHECK_RENDER(s, + "##\n" + "##\n"); + shape_destroy(s); +} + +static void test_rounded_rect_matches_circle(void) +{ + for (int r = 1; r <= 20; r++) { + struct ShapeData *rr; + struct ShapeData *circle; + CHECK((rr = shape_new_rounded_rect(7 - r, -3 - r, 2 * r + 1, 2 * r + 1, r)) != NULL); + CHECK((circle = shape_new_ellipse(7, -3, r, r)) != NULL); + if (!same_pixels(rr, circle)) { + fprintf(stderr, "%s:%d: rounded_rect radius %d differs from circle\n", __FILE__, + __LINE__, r); + failures++; + } + shape_destroy(rr); + shape_destroy(circle); + } +} + +static void test_rounded_rect_radius_zero_is_rect(void) +{ + struct ShapeData *s; + CHECK((s = shape_new_rounded_rect(0, 0, 3, 2, 0)) != NULL); + CHECK_RENDER(s, + "###\n" + "###\n"); + shape_destroy(s); +} + +static void test_rounded_rect_radius_clamped(void) +{ + struct ShapeData *a; + struct ShapeData *b; + CHECK((a = shape_new_rounded_rect(0, 0, 10, 6, 100)) != NULL); + CHECK((b = shape_new_rounded_rect(0, 0, 10, 6, 2)) != NULL); + CHECK(shape_equal(a, b)); + shape_destroy(b); + CHECK((b = shape_new_rounded_rect(0, 0, 10, 6, 1)) != NULL); + CHECK(!shape_equal(a, b)); + shape_destroy(a); + shape_destroy(b); +} + +static void test_rounded_rect_invalid(void) +{ + CHECK(shape_new_rounded_rect(0, 0, 0, 5, 1) == NULL); + CHECK(shape_new_rounded_rect(0, 0, 5, -2, 1) == NULL); + CHECK(shape_new_rounded_rect(0, 0, 5, 5, -1) == NULL); +} + +static void test_line_horizontal_thickness(void) +{ + struct ShapeData *s; + CHECK((s = shape_new_line(0, 0, 4, 0, 1)) != NULL); + CHECK_BOUNDS(s, 0, 0, 5, 1); + CHECK_RENDER(s, "#####\n"); + shape_destroy(s); + + CHECK((s = shape_new_line(0, 0, 4, 0, 2)) != NULL); + CHECK_BOUNDS(s, 0, -1, 5, 2); + CHECK_RENDER(s, + "#####\n" + "#####\n"); + shape_destroy(s); + + CHECK((s = shape_new_line(0, 0, 4, 0, 3)) != NULL); + CHECK_BOUNDS(s, -1, -1, 7, 3); + CHECK_RENDER(s, + ".#####.\n" + "#######\n" + ".#####.\n"); + shape_destroy(s); + + CHECK((s = shape_new_line(0, 0, 0, 2, 2)) != NULL); + CHECK_BOUNDS(s, -1, 0, 2, 3); + CHECK_RENDER(s, + "##\n" + "##\n" + "##\n"); + shape_destroy(s); +} + +static void test_line_even_caps(void) +{ + struct ShapeData *s; + CHECK((s = shape_new_line(0, 0, 4, 0, 4)) != NULL); + CHECK_BOUNDS(s, -1, -2, 7, 4); + CHECK_RENDER(s, + "#######\n" + "#######\n" + "#######\n" + "#######\n"); + shape_destroy(s); + + CHECK((s = shape_new_line(0, 0, 4, 0, 6)) != NULL); + CHECK_BOUNDS(s, -2, -3, 9, 6); + CHECK_RENDER(s, + ".#######.\n" + "#########\n" + "#########\n" + "#########\n" + "#########\n" + ".#######.\n"); + shape_destroy(s); + + CHECK((s = shape_new_line(0, 0, 0, 4, 4)) != NULL); + CHECK_BOUNDS(s, -2, -1, 4, 7); + CHECK_RENDER(s, + "####\n" + "####\n" + "####\n" + "####\n" + "####\n" + "####\n" + "####\n"); + shape_destroy(s); + + CHECK((s = shape_new_line(0, 4, 0, 0, 6)) != NULL); + CHECK_BOUNDS(s, -3, -2, 6, 9); + CHECK_RENDER(s, + ".####.\n" + "######\n" + "######\n" + "######\n" + "######\n" + "######\n" + "######\n" + "######\n" + ".####.\n"); + shape_destroy(s); + + CHECK((s = shape_new_line(0, 0, 5, 5, 2)) != NULL); + CHECK_BOUNDS(s, 0, -1, 6, 7); + CHECK_RENDER(s, + "#.....\n" + "##....\n" + ".##...\n" + "..##..\n" + "...##.\n" + "....##\n" + ".....#\n"); + shape_destroy(s); + + CHECK((s = shape_new_line(0, 0, 5, 5, 4)) != NULL); + CHECK_BOUNDS(s, -1, -2, 8, 9); + CHECK_RENDER(s, + "###.....\n" + "###.....\n" + "####....\n" + "#####...\n" + "..####..\n" + "...#####\n" + "....####\n" + ".....###\n" + ".....###\n"); + shape_destroy(s); + + CHECK((s = shape_new_line(5, 5, 0, 0, 6)) != NULL); + CHECK_BOUNDS(s, -2, -3, 10, 11); + CHECK_RENDER(s, + ".###......\n" + "#####.....\n" + "#####.....\n" + "######....\n" + "#######...\n" + ".########.\n" + "...#######\n" + "....######\n" + ".....#####\n" + ".....#####\n" + "......###.\n"); + shape_destroy(s); +} + +static void test_line_diagonal_connected(void) +{ + struct ShapeData *s; + CHECK((s = shape_new_line(0, 0, 10, 10, 1)) != NULL); + CHECK_BOUNDS(s, 0, 0, 11, 11); + for (int y = 0; y <= 10; y++) { + for (int x = 0; x <= 10; x++) { + CHECK(shape_contains(s, x, y) == (x == y)); + } + } + shape_destroy(s); +} + +static void test_line_shallow_one_pixel_per_column(void) +{ + struct ShapeData *s; + CHECK((s = shape_new_line(0, 0, 20, 10, 1)) != NULL); + CHECK_BOUNDS(s, 0, 0, 21, 11); + int count = 0; + for (int y = 0; y <= 10; y++) { + for (int x = 0; x <= 20; x++) { + bool in = shape_contains(s, x, y); + CHECK(in == (y == x / 2)); + count += in ? 1 : 0; + } + } + CHECK(count == 21); + shape_destroy(s); +} + +static void test_line_thick_sloped(void) +{ + struct ShapeData *s; + CHECK((s = shape_new_line(0, 0, 12, 5, 3)) != NULL); + CHECK_BOUNDS(s, -1, -1, 15, 8); + CHECK_RENDER(s, + ".##............\n" + "#####..........\n" + ".#######.......\n" + "...#######.....\n" + ".....#######...\n" + "........######.\n" + "..........#####\n" + "............##.\n"); + shape_destroy(s); +} + +static void test_line_reversed_is_same(void) +{ + struct ShapeData *a; + struct ShapeData *b; + CHECK((a = shape_new_line(-3, 7, 12, -4, 4)) != NULL); + CHECK((b = shape_new_line(12, -4, -3, 7, 4)) != NULL); + CHECK(same_pixels(a, b)); + shape_destroy(a); + shape_destroy(b); +} + +static void test_line_zero_length_is_dot(void) +{ + struct ShapeData *s; + CHECK((s = shape_new_line(5, 5, 5, 5, 1)) != NULL); + CHECK_BOUNDS(s, 5, 5, 1, 1); + CHECK(shape_contains(s, 5, 5)); + CHECK(!shape_contains(s, 6, 5)); + shape_destroy(s); + + CHECK((s = shape_new_line(5, 5, 5, 5, 2)) != NULL); + CHECK_BOUNDS(s, 5, 5, 1, 1); + CHECK_RENDER(s, "#\n"); + shape_destroy(s); + + struct ShapeData *disc; + CHECK((s = shape_new_line(5, 5, 5, 5, 5)) != NULL); + CHECK((disc = shape_new_ellipse(5, 5, 2, 2)) != NULL); + CHECK_BOUNDS(s, 3, 3, 5, 5); + CHECK(same_pixels(s, disc)); + shape_destroy(s); + shape_destroy(disc); +} + +static void test_line_invalid(void) +{ + CHECK(shape_new_line(0, 0, 4, 4, 0) == NULL); +} + +static void test_negative_coordinates(void) +{ + struct ShapeData *s; + CHECK((s = shape_new_ellipse(-10, -10, 2, 2)) != NULL); + CHECK_RENDER(s, + ".###.\n" + "#####\n" + "#####\n" + "#####\n" + ".###.\n"); + shape_destroy(s); + + CHECK((s = shape_new_rounded_rect(-3, -7, 6, 4, 2)) != NULL); + CHECK_RENDER(s, + ".####.\n" + "######\n" + "######\n" + ".####.\n"); + shape_destroy(s); +} + +static void test_equal(void) +{ + struct ShapeData *a; + struct ShapeData *b; + CHECK((a = shape_new_ellipse(1, 2, 3, 4)) != NULL); + CHECK((b = shape_new_ellipse(1, 2, 3, 4)) != NULL); + CHECK(shape_equal(a, b)); + shape_destroy(b); + CHECK((b = shape_new_ellipse(1, 2, 3, 5)) != NULL); + CHECK(!shape_equal(a, b)); + shape_destroy(b); + CHECK((b = shape_new_line(1, 2, 3, 4, 1)) != NULL); + CHECK(!shape_equal(a, b)); + shape_destroy(a); + shape_destroy(b); +} + +static void test_arc_quarter(void) +{ + struct ShapeData *s; + CHECK((s = shape_new_arc(0, 0, 10, 3, 0, 90)) != NULL); + CHECK_BOUNDS(s, -10, -10, 21, 21); + CHECK(shape_contains(s, 7, 7)); + CHECK(!shape_contains(s, 7, -7)); + CHECK(!shape_contains(s, -7, 7)); + CHECK(!shape_contains(s, 0, 0)); + CHECK(!shape_contains(s, 3, 3)); + CHECK(shape_contains(s, 10, 0)); + CHECK(shape_contains(s, 0, 10)); + shape_destroy(s); +} + +static void test_arc_full_ring(void) +{ + struct ShapeData *s; + CHECK((s = shape_new_arc(0, 0, 10, 2, 0, 360)) != NULL); + CHECK(shape_contains(s, 9, 0)); + CHECK(shape_contains(s, -9, 0)); + CHECK(shape_contains(s, 0, 9)); + CHECK(shape_contains(s, 0, -9)); + CHECK(!shape_contains(s, 0, 0)); + shape_destroy(s); +} + +static void test_arc_over_180(void) +{ + struct ShapeData *s; + CHECK((s = shape_new_arc(0, 0, 10, 3, 0, 270)) != NULL); + CHECK(shape_contains(s, 7, 7)); + CHECK(shape_contains(s, -7, 7)); + CHECK(shape_contains(s, -7, -7)); + CHECK(!shape_contains(s, 7, -7)); + shape_destroy(s); +} + +static void test_arc_wraparound(void) +{ + struct ShapeData *s; + CHECK((s = shape_new_arc(0, 0, 10, 3, 300, 60)) != NULL); + CHECK(shape_contains(s, 9, 0)); + CHECK(shape_contains(s, 5, -8)); + CHECK(shape_contains(s, 5, 8)); + CHECK(!shape_contains(s, 0, 9)); + CHECK(!shape_contains(s, -9, 0)); + shape_destroy(s); +} + +static void test_arc_negative_angles(void) +{ + struct ShapeData *a; + struct ShapeData *b; + CHECK((a = shape_new_arc(0, 0, 10, 3, -90, 0)) != NULL); + CHECK((b = shape_new_arc(0, 0, 10, 3, 270, 360)) != NULL); + for (int y = -10; y <= 10; y++) { + for (int x = -10; x <= 10; x++) { + CHECK(shape_contains(a, x, y) == shape_contains(b, x, y)); + } + } + CHECK(shape_contains(a, 7, -7)); + CHECK(!shape_contains(a, 7, 7)); + shape_destroy(a); + shape_destroy(b); +} + +static void check_arc_same(int start_deg, int end_deg, int ref_start, int ref_end, int line) +{ + struct ShapeData *a = shape_new_arc(0, 0, 12, 4, start_deg, end_deg); + struct ShapeData *b = shape_new_arc(0, 0, 12, 4, ref_start, ref_end); + if (a == NULL || b == NULL) { + fprintf(stderr, "%s:%d: arc init failed\n", __FILE__, line); + failures++; + shape_destroy(a); + shape_destroy(b); + return; + } + if (!same_pixels(a, b)) { + fprintf(stderr, "%s:%d: arc (%d, %d) differs from (%d, %d)\n", __FILE__, line, + start_deg, end_deg, ref_start, ref_end); + failures++; + } + shape_destroy(a); + shape_destroy(b); +} + +#define CHECK_ARC_SAME(s, e, rs, re) check_arc_same(s, e, rs, re, __LINE__) + +static int arc_sweep(int start_deg, int end_deg) +{ + struct ShapeData *s = shape_new_arc(0, 0, 10, 2, start_deg, end_deg); + if (s == NULL) { + return -1; + } + int start = ((start_deg % 360) + 360) % 360; + int sweep = 0; + for (int k = 1; k <= 360 && sweep == 0; k++) { + struct ShapeData *ref = shape_new_arc(0, 0, 10, 2, start, start + k); + if (shape_equal(s, ref)) { + sweep = k; + } + shape_destroy(ref); + } + shape_destroy(s); + return sweep; +} + +static void test_arc_sweep_rule(void) +{ + CHECK(arc_sweep(0, 90) == 90); + CHECK(arc_sweep(0, -90) == 270); + CHECK(arc_sweep(0, -359) == 1); + CHECK(arc_sweep(0, 360) == 360); + CHECK(arc_sweep(0, -360) == 360); + CHECK(arc_sweep(0, 720) == 360); + CHECK(arc_sweep(0, 400) == 40); + CHECK(arc_sweep(90, 0) == 270); + CHECK(arc_sweep(-90, 0) == 90); + CHECK(arc_sweep(0, 0) == -1); + CHECK(arc_sweep(45, 45) == -1); + CHECK(arc_sweep(-360, -360) == -1); + CHECK(arc_sweep(INT_MIN, INT_MAX) == 255); + CHECK(arc_sweep(INT_MAX, INT_MIN) == 105); + + CHECK_ARC_SAME(0, -90, 0, 270); + CHECK_ARC_SAME(0, -359, 0, 1); + CHECK_ARC_SAME(0, 400, 0, 40); + CHECK_ARC_SAME(0, -360, 0, 360); + CHECK_ARC_SAME(0, 720, 0, 360); + CHECK_ARC_SAME(90, 0, 90, 360); + CHECK_ARC_SAME(-90, 0, 270, 360); + + struct ShapeData *s; + CHECK((s = shape_new_arc(0, 0, 12, 4, 0, -360)) != NULL); + CHECK(shape_contains(s, 10, 0)); + CHECK(shape_contains(s, 0, 10)); + CHECK(shape_contains(s, -10, 0)); + CHECK(shape_contains(s, 0, -10)); + CHECK(shape_contains(s, 7, -7)); + shape_destroy(s); + + CHECK((s = shape_new_arc(0, 0, 40, 4, 0, -359)) != NULL); + CHECK(shape_contains(s, 38, 0)); + CHECK(!shape_contains(s, 0, 38)); + CHECK(!shape_contains(s, -38, 0)); + CHECK(!shape_contains(s, 27, -27)); + shape_destroy(s); +} + +static void test_arc_pie(void) +{ + struct ShapeData *s; + CHECK((s = shape_new_arc(0, 0, 10, 10, 0, 90)) != NULL); + CHECK(shape_contains(s, 0, 0)); + CHECK(shape_contains(s, 2, 2)); + CHECK(!shape_contains(s, -2, -2)); + shape_destroy(s); +} + +static void test_arc_matches_circle_when_full_pie(void) +{ + struct ShapeData *arc; + struct ShapeData *circle; + CHECK((arc = shape_new_arc(3, 4, 6, 100, 0, 360)) != NULL); + CHECK((circle = shape_new_ellipse(3, 4, 6, 6)) != NULL); + for (int y = -3; y <= 11; y++) { + for (int x = -4; x <= 10; x++) { + CHECK(shape_contains(arc, x, y) == shape_contains(circle, x, y)); + } + } + shape_destroy(arc); + shape_destroy(circle); +} + +static void test_arc_ring_matches_circles(void) +{ + for (int r = 1; r <= 15; r++) { + for (int t = 1; t <= r + 1; t++) { + struct ShapeData *arc; + struct ShapeData *outer; + struct ShapeData *hole; + CHECK((arc = shape_new_arc(2, 3, r, t, 0, 360)) != NULL); + CHECK((outer = shape_new_ellipse(2, 3, r, r)) != NULL); + hole = r - t > 0 ? shape_new_ellipse(2, 3, r - t, r - t) : NULL; + bool have_hole = hole != NULL; + for (int y = 3 - r - 1; y <= 3 + r + 1; y++) { + for (int x = 2 - r - 1; x <= 2 + r + 1; x++) { + bool expected = shape_contains(outer, x, y) + && !(have_hole && shape_contains(hole, x, y)); + if (shape_contains(arc, x, y) != expected) { + fprintf(stderr, "%s:%d: ring r=%d t=%d differs at (%d,%d)\n", __FILE__, + __LINE__, r, t, x, y); + failures++; + } + } + } + shape_destroy(arc); + shape_destroy(outer); + shape_destroy(hole); + } + } +} + +static void check_runs(struct ShapeData *shape, int line); + +static bool is_empty(struct ShapeData *s) +{ + int bx, by, bw, bh; + shape_bounds(s, &bx, &by, &bw, &bh); + for (int y = by; y < by + bh; y++) { + for (int x = bx; x < bx + bw; x++) { + if (shape_contains(s, x, y)) { + return false; + } + } + } + return true; +} + +static void test_arc_tiny_sweep_visible(void) +{ + static const int radii[] = { 5, 10, 20, 60 }; + static const int thicknesses[] = { 1, 3 }; + for (size_t i = 0; i < sizeof(radii) / sizeof(radii[0]); i++) { + for (size_t j = 0; j < sizeof(thicknesses) / sizeof(thicknesses[0]); j++) { + int empty = 0; + for (int start = 0; start < 360; start++) { + struct ShapeData *s = shape_new_arc(3, -2, radii[i], thicknesses[j], start, start + 1); + CHECK(s != NULL); + empty += is_empty(s) ? 1 : 0; + if (radii[i] < 60 || start % 7 == 0) { + check_runs(s, __LINE__); + } + shape_destroy(s); + } + if (empty) { + fprintf(stderr, "%s:%d: r=%d t=%d: %d of 360 one-degree arcs are empty\n", __FILE__, + __LINE__, radii[i], thicknesses[j], empty); + failures++; + } + } + } +} + +static void test_arc_invalid(void) +{ + CHECK(shape_new_arc(0, 0, 0, 1, 0, 90) == NULL); + CHECK(shape_new_arc(0, 0, 10, 0, 0, 90) == NULL); + CHECK(shape_new_arc(0, 0, 10, 2, 45, 45) == NULL); +} + +static void test_large_values(void) +{ + struct ShapeData *s; + CHECK((s = shape_new_ellipse(0, 0, 10000, 10000)) != NULL); + CHECK(shape_contains(s, 10000, 0)); + CHECK(!shape_contains(s, 10000, 10000)); + shape_destroy(s); + CHECK((s = shape_new_arc(0, 0, 10000, 5, 0, 90)) != NULL); + CHECK(shape_contains(s, 7071, 7071)); + CHECK(!shape_contains(s, 7071, -7071)); + shape_destroy(s); + CHECK((s = shape_new_line(-2000, -2000, 2000, 2000, 3)) != NULL); + CHECK(shape_contains(s, 1234, 1234)); + CHECK(!shape_contains(s, 1234, -1234)); + shape_destroy(s); +} + +static void test_polygon_square_matches_rect(void) +{ + struct ShapePoint pts[] = { { 0, 0 }, { 4, 0 }, { 4, 4 }, { 0, 4 } }; + struct ShapeData *s; + CHECK((s = shape_new_polygon(pts, 4)) != NULL); + CHECK_BOUNDS(s, 0, 0, 4, 4); + CHECK_RENDER(s, + "####\n" + "####\n" + "####\n" + "####\n"); + shape_destroy(s); +} + +static void test_polygon_concave(void) +{ + struct ShapePoint pts[] = { { 0, 0 }, { 4, 0 }, { 4, 2 }, { 2, 2 }, { 2, 4 }, { 0, 4 } }; + struct ShapeData *s; + CHECK((s = shape_new_polygon(pts, 6)) != NULL); + CHECK_RENDER(s, + "####\n" + "####\n" + "##..\n" + "##..\n"); + shape_destroy(s); +} + +static void test_polygon_even_odd_hole(void) +{ + struct ShapePoint pts[] = { + { 0, 0 }, { 6, 0 }, { 6, 6 }, { 0, 6 }, { 0, 0 }, + { 2, 2 }, { 2, 4 }, { 4, 4 }, { 4, 2 }, { 2, 2 } + }; + struct ShapeData *s; + CHECK((s = shape_new_polygon(pts, 10)) != NULL); + CHECK(shape_contains(s, 1, 4)); + CHECK(!shape_contains(s, 3, 3)); + CHECK(shape_contains(s, 5, 3)); + CHECK(shape_contains(s, 5, 5)); + shape_destroy(s); +} + +static void test_polygon_triangle_rows_out_of_order(void) +{ + struct ShapePoint pts[] = { { 0, 0 }, { 8, 0 }, { 0, 8 } }; + struct ShapeData *s; + CHECK((s = shape_new_polygon(pts, 3)) != NULL); + CHECK(shape_contains(s, 0, 7)); + CHECK(shape_contains(s, 6, 0)); + CHECK(!shape_contains(s, 7, 7)); + CHECK(shape_contains(s, 0, 7)); + CHECK(!shape_contains(s, 1, 7)); + shape_destroy(s); +} + +static void test_polygon_copies_points(void) +{ + struct ShapePoint pts[] = { { 0, 0 }, { 4, 0 }, { 4, 4 }, { 0, 4 } }; + struct ShapeData *s; + CHECK((s = shape_new_polygon(pts, 4)) != NULL); + pts[1].x = 100; + CHECK(!shape_contains(s, 50, 1)); + CHECK(shape_contains(s, 3, 1)); + shape_destroy(s); +} + +static void test_polygon_invalid(void) +{ + struct ShapePoint pts[] = { { 0, 0 }, { 4, 0 } }; + CHECK(shape_new_polygon(pts, 2) == NULL); + CHECK(shape_new_polygon(NULL, 0) == NULL); + CHECK(shape_polygon_begin(2) == NULL); +} + +static void test_polygon_max_points(void) +{ + int n = SHAPE_POLYGON_MAX_POINTS + 1; + struct ShapePoint *pts = malloc(sizeof(struct ShapePoint) * n); + for (int i = 0; i < n; i++) { + pts[i].x = i % 2; + pts[i].y = i; + } + struct ShapeData *s; + CHECK(shape_new_polygon(pts, n) == NULL); + CHECK((s = shape_new_polygon(pts, n - 1)) != NULL); + shape_destroy(s); + + CHECK(shape_polygon_begin(n) == NULL); + CHECK((s = shape_polygon_begin(4)) != NULL); + CHECK(shape_polygon_add_point(s, 0, 0)); + CHECK(shape_polygon_add_point(s, 4, 0)); + CHECK(shape_polygon_add_point(s, 4, 4)); + CHECK(!shape_polygon_end(s)); + CHECK(!shape_polygon_add_point(s, SHAPE_VALUE_LIMIT + 1, 4)); + CHECK(shape_polygon_add_point(s, 0, 4)); + CHECK(!shape_polygon_add_point(s, 0, 0)); + CHECK(shape_polygon_end(s)); + CHECK(shape_contains(s, 3, 3)); + shape_destroy(s); + free(pts); +} + +static void test_polygon_equal(void) +{ + struct ShapePoint p1[] = { { 0, 0 }, { 4, 0 }, { 0, 4 } }; + struct ShapePoint p2[] = { { 0, 0 }, { 4, 0 }, { 0, 5 } }; + struct ShapeData *a; + struct ShapeData *b; + struct ShapeData *c; + CHECK((a = shape_new_polygon(p1, 3)) != NULL); + CHECK((b = shape_new_polygon(p1, 3)) != NULL); + CHECK((c = shape_new_polygon(p2, 3)) != NULL); + shape_contains(a, 1, 1); + CHECK(shape_equal(a, b)); + CHECK(!shape_equal(a, c)); + shape_destroy(a); + shape_destroy(b); + shape_destroy(c); +} + +static void check_runs(struct ShapeData *shape, int line) +{ + int bx, by, bw, bh; + shape_bounds(shape, &bx, &by, &bw, &bh); + int end = bx + bw; + bool *row = malloc(sizeof(bool) * bw); + int *expected = malloc(sizeof(int) * bw); + + for (int k = 0; k < 2 * bh; k++) { + int y = (k < bh) ? by + k : by + 2 * bh - 1 - k; + for (int i = 0; i < bw; i++) { + row[i] = shape_contains(shape, bx + i, y); + } + for (int i = bw - 1; i >= 0; i--) { + expected[i] = (i + 1 < bw && row[i + 1] == row[i]) ? expected[i + 1] + 1 : 1; + } + for (int x = bx; x < end; x++) { + bool inside; + int run = shape_run(shape, x, y, &inside); + if (run < 1 || x + run > end) { + fprintf(stderr, "%s:%d: bad run %d at (%d,%d)\n", __FILE__, line, run, x, y); + failures++; + goto out; + } + if (inside != row[x - bx] || run != expected[x - bx]) { + fprintf(stderr, "%s:%d: run at (%d,%d) is %d/%s, expected %d/%s\n", __FILE__, + line, x, y, run, inside ? "in" : "out", expected[x - bx], + row[x - bx] ? "in" : "out"); + failures++; + goto out; + } + } + } +out: + free(row); + free(expected); +} + +#define CHECK_RUNS(shape) check_runs(shape, __LINE__) + +static long render_by_runs(struct ShapeData *s) +{ + int bx, by, bw, bh; + shape_bounds(s, &bx, &by, &bw, &bh); + long count = 0; + for (int y = by; y < by + bh; y++) { + for (int x = bx; x < bx + bw; x++) { + bool inside; + shape_run(s, x, y, &inside); + count += inside ? 1 : 0; + } + } + return count; +} + +static void test_polygon_comb(void) +{ + int n = SHAPE_POLYGON_MAX_POINTS; + struct ShapePoint *pts = malloc(sizeof(struct ShapePoint) * n); + struct ShapeData *s; + + for (int i = 0; i < n; i++) { + int tooth = i / 3; + pts[i].x = tooth; + pts[i].y = (i % 3 == 1) ? 240 : 0; + } + CHECK((s = shape_new_polygon(pts, n)) != NULL); + CHECK(render_by_runs(s) == 0); + CHECK_RUNS(s); + shape_destroy(s); + + for (int t = 0; t < n / 4; t++) { + pts[4 * t + 0] = (struct ShapePoint){ 2 * t, 240 }; + pts[4 * t + 1] = (struct ShapePoint){ 2 * t, 10 }; + pts[4 * t + 2] = (struct ShapePoint){ 2 * t + 1, 10 }; + pts[4 * t + 3] = (struct ShapePoint){ 2 * t + 1, 240 }; + } + CHECK((s = shape_new_polygon(pts, n)) != NULL); + CHECK(render_by_runs(s) == (long) (n / 4) * 230); + CHECK_RUNS(s); + shape_destroy(s); + free(pts); +} + +static void check_run_outside_bbox(struct ShapeData *s, int line) +{ + int bx, by, bw, bh; + shape_bounds(s, &bx, &by, &bw, &bh); + int xs[] = { INT_MIN, INT_MIN + 1, bx - 100000, bx - 1, bx, bx + bw - 1, bx + bw, INT_MAX }; + int ys[] = { INT_MIN, by - 1, by, by + bh - 1, by + bh, INT_MAX }; + for (size_t i = 0; i < sizeof(xs) / sizeof(xs[0]); i++) { + for (size_t j = 0; j < sizeof(ys) / sizeof(ys[0]); j++) { + int x = xs[i]; + int y = ys[j]; + bool in_rows = y >= by && y < by + bh; + if (in_rows && x >= bx && x < bx + bw) { + continue; + } + long long gap = (long long) bx - x; + int expected = (in_rows && x < bx) ? (gap > INT_MAX ? INT_MAX : (int) gap) : 1; + bool inside = true; + int run = shape_run(s, x, y, &inside); + if (inside || run != expected) { + fprintf(stderr, "%s:%d: run at (%d,%d) is %d/%s, expected %d/out\n", __FILE__, line, + x, y, run, inside ? "in" : "out", expected); + failures++; + } + } + } +} + +static void test_run_outside_bbox(void) +{ + struct ShapeData *s; + CHECK((s = shape_new_rounded_rect(10, 20, 30, 40, 5)) != NULL); + check_run_outside_bbox(s, __LINE__); + shape_destroy(s); + CHECK((s = shape_new_ellipse(-5, 7, 9, 4)) != NULL); + check_run_outside_bbox(s, __LINE__); + shape_destroy(s); + CHECK((s = shape_new_line(0, 0, 50, 13, 5)) != NULL); + check_run_outside_bbox(s, __LINE__); + shape_destroy(s); + CHECK((s = shape_new_arc(100, 100, 30, 8, 10, 250)) != NULL); + check_run_outside_bbox(s, __LINE__); + shape_destroy(s); + CHECK((s = shape_new_arc(-SHAPE_VALUE_LIMIT, SHAPE_VALUE_LIMIT, SHAPE_VALUE_LIMIT, 1, 0, 360)) != NULL); + check_run_outside_bbox(s, __LINE__); + shape_destroy(s); + struct ShapePoint pts[] = { { 0, 0 }, { 40, 10 }, { 10, 40 }, { 20, 15 } }; + CHECK((s = shape_new_polygon(pts, 4)) != NULL); + check_run_outside_bbox(s, __LINE__); + shape_destroy(s); +} + +static void test_runs_match_contains(void) +{ + struct ShapeData *s; + + int ellipses[][4] = { { 0, 0, 9, 4 }, { 3, -2, 1, 7 }, { 10, 20, 2, 2 }, { -7, 5, 1, 1 }, + { 0, 0, 30, 30 } }; + for (size_t i = 0; i < sizeof(ellipses) / sizeof(ellipses[0]); i++) { + CHECK((s = shape_new_ellipse(ellipses[i][0], ellipses[i][1], ellipses[i][2], ellipses[i][3])) != NULL); + CHECK_RUNS(s); + shape_destroy(s); + } + + int rects[][5] = { { 3, 4, 6, 4, 2 }, { 0, 0, 17, 9, 4 }, { -5, -5, 3, 20, 10 }, + { 0, 0, 1, 1, 0 }, { 2, 2, 40, 12, 6 } }; + for (size_t i = 0; i < sizeof(rects) / sizeof(rects[0]); i++) { + CHECK((s = shape_new_rounded_rect(rects[i][0], rects[i][1], rects[i][2], rects[i][3], rects[i][4])) != NULL); + CHECK_RUNS(s); + shape_destroy(s); + } + + int lines[][5] = { { 0, 0, 4, 0, 1 }, { 0, 0, 4, 0, 3 }, { 0, 0, 9, 3, 1 }, { 0, 0, 3, 9, 2 }, + { 9, 0, 0, 5, 4 }, { 5, 5, 5, 5, 1 }, { 0, 0, 0, 8, 3 }, { -3, 7, 12, -4, 5 }, + { 0, 0, 40, 1, 1 }, { 0, 0, 1, 40, 1 }, { 20, 3, -20, 9, 2 } }; + for (size_t i = 0; i < sizeof(lines) / sizeof(lines[0]); i++) { + CHECK((s = shape_new_line(lines[i][0], lines[i][1], lines[i][2], lines[i][3], lines[i][4])) != NULL); + CHECK_RUNS(s); + shape_destroy(s); + } + + int arcs[][6] = { { 0, 0, 10, 3, 0, 90 }, { 0, 0, 10, 3, 300, 60 }, { 0, 0, 10, 10, 0, 270 }, + { 0, 0, 6, 2, 0, 360 }, { 5, 5, 12, 4, -90, 180 }, { 0, 0, 1, 1, 0, 90 }, + { 0, 0, 20, 1, 45, 46 }, { 0, 0, 20, 5, 89, 91 }, { 0, 0, 20, 5, 179, 181 }, + { 0, 0, 20, 25, 10, 350 }, { 0, 0, 20, 5, 0, 180 }, { 0, 0, 20, 5, 90, 270 } }; + for (size_t i = 0; i < sizeof(arcs) / sizeof(arcs[0]); i++) { + CHECK((s = shape_new_arc(arcs[i][0], arcs[i][1], arcs[i][2], arcs[i][3], arcs[i][4], arcs[i][5])) != NULL); + CHECK_RUNS(s); + shape_destroy(s); + } + + struct ShapePoint square[] = { { 0, 0 }, { 4, 0 }, { 4, 4 }, { 0, 4 } }; + struct ShapePoint ell[] = { { 0, 0 }, { 4, 0 }, { 4, 2 }, { 2, 2 }, { 2, 4 }, { 0, 4 } }; + struct ShapePoint hole[] = { { 0, 0 }, { 6, 0 }, { 6, 6 }, { 0, 6 }, { 0, 0 }, { 2, 2 }, + { 2, 4 }, { 4, 4 }, { 4, 2 }, { 2, 2 } }; + struct ShapePoint tri[] = { { 0, 0 }, { 8, 0 }, { 0, 8 } }; + struct ShapePoint star[] = { { 10, 0 }, { 16, 19 }, { 0, 7 }, { 20, 7 }, { 4, 19 } }; + struct ShapePoint neg[] = { { -9, -3 }, { 5, -8 }, { 2, 6 } }; + CHECK((s = shape_new_polygon(square, 4)) != NULL); + CHECK_RUNS(s); + shape_destroy(s); + CHECK((s = shape_new_polygon(ell, 6)) != NULL); + CHECK_RUNS(s); + shape_destroy(s); + CHECK((s = shape_new_polygon(hole, 10)) != NULL); + CHECK_RUNS(s); + shape_destroy(s); + CHECK((s = shape_new_polygon(tri, 3)) != NULL); + CHECK_RUNS(s); + shape_destroy(s); + CHECK((s = shape_new_polygon(star, 5)) != NULL); + CHECK_RUNS(s); + shape_destroy(s); + CHECK((s = shape_new_polygon(neg, 3)) != NULL); + CHECK_RUNS(s); + shape_destroy(s); +} + +static uint32_t rng_state = 12345; + +static int rng_range(int lo, int hi) +{ + rng_state = rng_state * 1103515245u + 12345u; + return lo + (int) ((rng_state >> 8) % (uint32_t) (hi - lo + 1)); +} + +static void test_arc_runs_random(void) +{ + for (int i = 0; i < 400; i++) { + int r = rng_range(1, 40); + int t = rng_range(1, r + 3); + int start = rng_range(-400, 400); + int end = start + rng_range(-420, 420); + int cx = rng_range(-20, 20); + int cy = rng_range(-20, 20); + struct ShapeData *s = shape_new_arc(cx, cy, r, t, start, end); + if (s == NULL) { + continue; + } + CHECK_RUNS(s); + shape_destroy(s); + } +} + +static void test_polygon_runs_random(void) +{ + struct ShapePoint pts[40]; + for (int i = 0; i < 400; i++) { + int n = rng_range(3, 40); + int span = rng_range(1, 30); + for (int j = 0; j < n; j++) { + if (j > 0 && rng_range(0, 5) == 0) { + pts[j] = pts[j - 1]; + pts[j].x += rng_range(-1, 1) * rng_range(0, span); + } else { + pts[j].x = rng_range(-span, span); + pts[j].y = rng_range(-span, span); + } + } + struct ShapeData *s = shape_new_polygon(pts, n); + CHECK(s != NULL); + CHECK_RUNS(s); + shape_destroy(s); + } +} + +static bool is_symmetric(struct ShapeData *s) +{ + int bx, by, bw, bh; + shape_bounds(s, &bx, &by, &bw, &bh); + for (int y = by; y < by + bh; y++) { + for (int x = bx; x < bx + bw; x++) { + bool in = shape_contains(s, x, y); + if (in != shape_contains(s, 2 * bx + bw - 1 - x, y) + || in != shape_contains(s, x, 2 * by + bh - 1 - y)) { + return false; + } + } + } + return true; +} + +static void test_ellipse_runs_random(void) +{ + for (int i = 0; i < 300; i++) { + struct ShapeData *s; + CHECK((s = shape_new_ellipse(rng_range(-30, 30), rng_range(-30, 30), rng_range(1, 40), rng_range(1, 40))) != NULL); + CHECK_RUNS(s); + CHECK(is_symmetric(s)); + shape_destroy(s); + } +} + +static void test_rounded_rect_runs_random(void) +{ + for (int i = 0; i < 300; i++) { + struct ShapeData *s; + CHECK((s = shape_new_rounded_rect(rng_range(-30, 30), rng_range(-30, 30), rng_range(1, 60), rng_range(1, 60), rng_range(0, 35))) != NULL); + CHECK_RUNS(s); + CHECK(is_symmetric(s)); + shape_destroy(s); + } +} + +static bool ref_line_contains(int x1, int y1, int x2, int y2, int t, int x, int y) +{ + if (x1 == x2 && y1 == y2) { + long long r = (t - 1) / 2; + long long dx = x - x1; + long long dy = y - y1; + return (dx == 0 && dy == 0) || dx * dx + dy * dy < r * r + r; + } + bool x_major = llabs((long long) x2 - x1) >= llabs((long long) y2 - y1); + long long ea[2] = { x_major ? x1 : y1, x_major ? x2 : y2 }; + long long eb[2] = { x_major ? y1 : x1, x_major ? y2 : x2 }; + for (int i = 0; i < 2; i++) { + long long u = 2 * ((x_major ? x : y) - ea[i]); + long long v = 2 * ((x_major ? y : x) - eb[i]) + (t % 2 == 0 ? 1 : 0); + if (u * u + v * v < (long long) t * t - 1) { + return true; + } + } + long long a1 = x_major ? x1 : y1; + long long b1 = x_major ? y1 : x1; + long long a2 = x_major ? x2 : y2; + long long b2 = x_major ? y2 : x2; + long long a = x_major ? x : y; + long long b = x_major ? y : x; + if (a1 == a2 || a < (a1 < a2 ? a1 : a2) || a > (a1 < a2 ? a2 : a1)) { + return false; + } + long long da = a2 - a1; + long long o2 = 2 * ((b - b1) * da - (a - a1) * (b2 - b1)); + if (da < 0) { + da = -da; + o2 = -o2; + } + return -t * da <= o2 && o2 < t * da; +} + +static bool line_matches_reference(struct ShapeData *s, int x1, int y1, int x2, int y2, int t) +{ + int margin = t + 2; + int lo_x = (x1 < x2 ? x1 : x2) - margin; + int hi_x = (x1 < x2 ? x2 : x1) + margin; + int lo_y = (y1 < y2 ? y1 : y2) - margin; + int hi_y = (y1 < y2 ? y2 : y1) + margin; + for (int y = lo_y; y <= hi_y; y++) { + for (int x = lo_x; x <= hi_x; x++) { + if (shape_contains(s, x, y) != ref_line_contains(x1, y1, x2, y2, t, x, y)) { + fprintf(stderr, "line (%d,%d)-(%d,%d) T=%d differs from the reference at (%d,%d)\n", + x1, y1, x2, y2, t, x, y); + return false; + } + } + } + return true; +} + +static void check_line_properties(int x1, int y1, int x2, int y2, int t) +{ + struct ShapeData *s; + CHECK((s = shape_new_line(x1, y1, x2, y2, t)) != NULL); + int bx, by, bw, bh; + shape_bounds(s, &bx, &by, &bw, &bh); + int dx = abs(x2 - x1); + int dy = abs(y2 - y1); + bool x_major = dx >= dy; + int a_lo = x_major ? bx : by; + int a_hi = x_major ? bx + bw : by + bh; + int b_lo = x_major ? by : bx; + int b_hi = x_major ? by + bh : bx + bw; + int a1 = x_major ? (x1 < x2 ? x1 : x2) : (y1 < y2 ? y1 : y2); + int a2 = x_major ? (x1 < x2 ? x2 : x1) : (y1 < y2 ? y2 : y1); + bool ok = true; + int prev_first = 0; + for (int a = a_lo - 1; a <= a_hi && ok; a++) { + int count = 0; + int first = 0; + int last = 0; + for (int b = b_lo - 1; b <= b_hi; b++) { + bool in = x_major ? shape_contains(s, a, b) : shape_contains(s, b, a); + if (in) { + if (count == 0) { + first = b; + } + last = b; + count++; + } + } + bool in_range = a >= a1 && a <= a2; + if (t <= 2 && a1 != a2) { + ok = count == (in_range ? t : 0); + } else if (in_range && a1 != a2) { + ok = count >= t; + } + ok = ok && (count == 0 || last - first + 1 == count); + if (ok && t == 1 && in_range && a > a1) { + ok = abs(first - prev_first) <= 1; + } + prev_first = first; + } + ok = ok && shape_contains(s, x1, y1) && shape_contains(s, x2, y2); + ok = ok && line_matches_reference(s, x1, y1, x2, y2, t); + if (a1 == a2) { + struct ShapeData *disc; + int dr = (t - 1) / 2; + if (dr > 0) { + CHECK((disc = shape_new_ellipse(x1, y1, dr, dr)) != NULL); + ok = ok && same_pixels(s, disc); + shape_destroy(disc); + } else { + ok = ok && bw == 1 && bh == 1; + } + } + if (!ok) { + fprintf(stderr, "%s:%d: line (%d,%d)-(%d,%d) T=%d violates line properties\n", __FILE__, + __LINE__, x1, y1, x2, y2, t); + failures++; + } + CHECK_RUNS(s); + + struct ShapeData *r; + CHECK((r = shape_new_line(x2, y2, x1, y1, t)) != NULL); + CHECK(same_pixels(s, r)); + shape_destroy(r); + shape_destroy(s); +} + +static void test_line_runs_random(void) +{ + for (int i = 0; i < 2000; i++) { + int major = rng_range(0, 80); + int minor = rng_range(-major, major); + int dx = rng_range(0, 1) ? major : -major; + int dy = minor; + if (rng_range(0, 1)) { + int tmp = dx; + dx = dy; + dy = tmp; + } + int x1 = rng_range(-30, 30); + int y1 = rng_range(-30, 30); + check_line_properties(x1, y1, x1 + dx, y1 + dy, rng_range(1, 9)); + } + for (int t = 1; t <= 4; t++) { + check_line_properties(0, 0, 17, 17, t); + check_line_properties(0, 0, -17, 17, t); + check_line_properties(0, 0, 17, 16, t); + check_line_properties(0, 0, 16, 17, t); + check_line_properties(0, 0, 0, -9, t); + check_line_properties(0, 0, 1, 0, t); + check_line_properties(0, 0, 0, 0, t); + } +} + +static bool check_row_extreme(struct ShapeData *s, int y, int wx0, int wx1) +{ + int bx, by, bw, bh; + shape_bounds(s, &bx, &by, &bw, &bh); + int end = bx + bw; + for (int x = bx; x < end;) { + bool inside; + int run = shape_run(s, x, y, &inside); + if (run < 1 || run > end - x || shape_contains(s, x, y) != inside + || shape_contains(s, x + run - 1, y) != inside + || (run < end - x && shape_contains(s, x + run, y) == inside)) { + return false; + } + x += run; + } + wx0 = wx0 < bx ? bx : wx0; + wx1 = wx1 > end ? end : wx1; + bool row[300]; + int expected[300]; + int n = wx1 - wx0; + if (n <= 0) { + return true; + } + if (n > 300) { + return false; + } + for (int i = 0; i < n; i++) { + row[i] = shape_contains(s, wx0 + i, y); + } + for (int i = n - 1; i >= 0; i--) { + expected[i] = (i + 1 < n && row[i + 1] == row[i]) ? expected[i + 1] + 1 : 1; + } + for (int i = 0; i < n; i++) { + int x = wx0 + i; + bool inside; + int run = shape_run(s, x, y, &inside); + if (run < 1 || run > end - x || inside != row[i]) { + return false; + } + if (i + expected[i] < n) { + if (run != expected[i]) { + return false; + } + } else if (run < expected[i] || shape_contains(s, x + run - 1, y) != inside + || (run < end - x && shape_contains(s, x + run, y) == inside)) { + return false; + } + } + return true; +} + +static void check_extreme(struct ShapeData *s, const char *what, int line) +{ + int bx, by, bw, bh; + shape_bounds(s, &bx, &by, &bw, &bh); + bool ok = !shape_contains(s, bx - 1, by) && !shape_contains(s, bx + bw, by) + && !shape_contains(s, bx, by - 1) && !shape_contains(s, bx, by + bh) + && !shape_contains(s, INT_MIN, INT_MIN) && !shape_contains(s, INT_MAX, INT_MAX); + int edge_rows[] = { by, by + 1, by + bh / 2, by + bh - 2, by + bh - 1 }; + for (size_t i = 0; ok && i < sizeof(edge_rows) / sizeof(edge_rows[0]); i++) { + if (edge_rows[i] >= by && edge_rows[i] < by + bh) { + ok = check_row_extreme(s, edge_rows[i], bx, bx + 4) + && check_row_extreme(s, edge_rows[i], bx + bw - 4, bx + bw); + } + } + int y0 = by > -2 ? by : -2; + int y1 = by + bh < 242 ? by + bh : 242; + for (int y = y0; ok && y < y1; y++) { + ok = check_row_extreme(s, y, -2, 242); + } + if (!ok) { + fprintf(stderr, "%s:%d: extreme %s at bbox (%d,%d,%d,%d) failed\n", __FILE__, line, what, + bx, by, bw, bh); + failures++; + } +} + +static void test_extreme_values(void) +{ + const int lim = SHAPE_VALUE_LIMIT; + const int coords[] = { -lim, -lim / 2, 0, 120, lim }; + const int ncoords = sizeof(coords) / sizeof(coords[0]); + const int sizes[] = { 1, 2, 3, lim / 2, lim - 1, lim }; + const int nsizes = sizeof(sizes) / sizeof(sizes[0]); + struct ShapeData *s; + + for (int i = 0; i < ncoords; i++) { + for (int j = 0; j < ncoords; j++) { + int cx = coords[i]; + int cy = coords[j]; + for (int a = 0; a < nsizes; a++) { + for (int b = 0; b < nsizes; b++) { + CHECK((s = shape_new_ellipse(cx, cy, sizes[a], sizes[b])) != NULL); + check_extreme(s, "ellipse", __LINE__); + shape_destroy(s); + + CHECK((s = shape_new_rounded_rect(cx, cy, sizes[a], sizes[b], lim)) != NULL); + check_extreme(s, "rounded_rect", __LINE__); + shape_destroy(s); + + static const int angles[][2] = { { 0, 90 }, { 45, -45 }, { 0, 360 }, + { 179, 181 }, { INT_MIN, INT_MAX } }; + int k = (a + b) % 5; + CHECK((s = shape_new_arc(cx, cy, sizes[a], sizes[b], angles[k][0], angles[k][1])) != NULL); + check_extreme(s, "arc", __LINE__); + shape_destroy(s); + } + } + } + } + + const int ends[] = { -lim, 0, lim }; + const int thick[] = { 1, 2, 3, 4, lim - 1, lim }; + for (int p = 0; p < 9; p++) { + for (int q = 0; q < 9; q++) { + for (size_t t = 0; t < sizeof(thick) / sizeof(thick[0]); t++) { + CHECK((s = shape_new_line(ends[p % 3], ends[p / 3], ends[q % 3], ends[q / 3], thick[t])) != NULL); + check_extreme(s, "line", __LINE__); + shape_destroy(s); + } + } + } + CHECK((s = shape_new_line(-lim, -lim + 1, lim, lim, 1)) != NULL); + check_extreme(s, "line", __LINE__); + shape_destroy(s); + CHECK((s = shape_new_line(-lim, lim, lim - 3, -lim, 1)) != NULL); + check_extreme(s, "line", __LINE__); + shape_destroy(s); + + struct ShapePoint square[] = { { -lim, -lim }, { lim, -lim }, { lim, lim }, { -lim, lim } }; + struct ShapePoint bowtie[] = { { -lim, -lim }, { lim, lim }, { lim, -lim }, { -lim, lim } }; + struct ShapePoint sliver[] = { { -lim, -lim }, { lim, lim - 1 }, { lim, lim } }; + CHECK((s = shape_new_polygon(square, 4)) != NULL); + check_extreme(s, "polygon", __LINE__); + shape_destroy(s); + CHECK((s = shape_new_polygon(bowtie, 4)) != NULL); + check_extreme(s, "polygon", __LINE__); + shape_destroy(s); + CHECK((s = shape_new_polygon(sliver, 3)) != NULL); + check_extreme(s, "polygon", __LINE__); + shape_destroy(s); + + int n = SHAPE_POLYGON_MAX_POINTS; + struct ShapePoint *comb = malloc(sizeof(struct ShapePoint) * n); + for (int i = 0; i < n; i++) { + comb[i].x = -lim + (int) ((int64_t) (i / 2) * 2 * lim / (n / 2)); + comb[i].y = ((i + 1) % 4 < 2) ? -lim : lim; + } + CHECK((s = shape_new_polygon(comb, n)) != NULL); + check_extreme(s, "polygon", __LINE__); + shape_destroy(s); + free(comb); +} + +static void test_out_of_range_rejected(void) +{ + const int over = SHAPE_VALUE_LIMIT + 1; + const int under = -SHAPE_VALUE_LIMIT - 1; + struct ShapeData *s; + + CHECK(shape_new_ellipse(over, 0, 1, 1) == NULL); + CHECK(shape_new_ellipse(0, under, 1, 1) == NULL); + CHECK(shape_new_ellipse(0, 0, over, 1) == NULL); + CHECK(shape_new_ellipse(0, 0, 1, INT_MAX) == NULL); + + CHECK(shape_new_rounded_rect(under, 0, 1, 1, 0) == NULL); + CHECK(shape_new_rounded_rect(0, over, 1, 1, 0) == NULL); + CHECK(shape_new_rounded_rect(0, 0, over, 1, 0) == NULL); + CHECK(shape_new_rounded_rect(0, 0, 1, over, 0) == NULL); + CHECK(shape_new_rounded_rect(0, 0, 1, 1, over) == NULL); + + CHECK(shape_new_line(over, 0, 0, 0, 1) == NULL); + CHECK(shape_new_line(0, under, 0, 0, 1) == NULL); + CHECK(shape_new_line(0, 0, INT_MIN, 0, 1) == NULL); + CHECK(shape_new_line(0, 0, 0, over, 1) == NULL); + CHECK(shape_new_line(0, 0, 0, 0, over) == NULL); + + CHECK(shape_new_arc(over, 0, 1, 1, 0, 90) == NULL); + CHECK(shape_new_arc(0, under, 1, 1, 0, 90) == NULL); + CHECK(shape_new_arc(0, 0, over, 1, 0, 90) == NULL); + CHECK(shape_new_arc(0, 0, 1, over, 0, 90) == NULL); + + struct ShapePoint pts[] = { { 0, 0 }, { over, 0 }, { 0, 4 } }; + CHECK(shape_new_polygon(pts, 3) == NULL); + pts[1].x = 4; + pts[2].y = under; + CHECK(shape_new_polygon(pts, 3) == NULL); + pts[2].y = SHAPE_VALUE_LIMIT; + CHECK((s = shape_new_polygon(pts, 3)) != NULL); + shape_destroy(s); + + CHECK((s = shape_new_ellipse(SHAPE_VALUE_LIMIT, -SHAPE_VALUE_LIMIT, SHAPE_VALUE_LIMIT, 1)) != NULL); + shape_destroy(s); +} + +static void test_run_ellipse_values(void) +{ + struct ShapeData *s; + bool inside; + CHECK((s = shape_new_ellipse(10, 20, 2, 2)) != NULL); + CHECK(shape_run(s, 8, 18, &inside) == 1 && !inside); + CHECK(shape_run(s, 9, 18, &inside) == 3 && inside); + CHECK(shape_run(s, 10, 18, &inside) == 2 && inside); + CHECK(shape_run(s, 12, 18, &inside) == 1 && !inside); + CHECK(shape_run(s, 8, 20, &inside) == 5 && inside); + shape_destroy(s); +} + +int main(void) +{ + test_circle_r2(); + test_circle_small(); + test_ellipse_symmetry(); + test_ellipse_invalid(); + test_rounded_rect(); + test_rounded_rect_small_radii(); + test_rounded_rect_matches_circle(); + test_rounded_rect_radius_zero_is_rect(); + test_rounded_rect_radius_clamped(); + test_rounded_rect_invalid(); + test_line_horizontal_thickness(); + test_line_even_caps(); + test_line_diagonal_connected(); + test_line_shallow_one_pixel_per_column(); + test_line_thick_sloped(); + test_line_reversed_is_same(); + test_line_zero_length_is_dot(); + test_line_invalid(); + test_negative_coordinates(); + test_equal(); + test_arc_quarter(); + test_arc_full_ring(); + test_arc_over_180(); + test_arc_wraparound(); + test_arc_negative_angles(); + test_arc_sweep_rule(); + test_arc_pie(); + test_arc_matches_circle_when_full_pie(); + test_arc_ring_matches_circles(); + test_arc_tiny_sweep_visible(); + test_arc_invalid(); + test_large_values(); + test_polygon_square_matches_rect(); + test_polygon_concave(); + test_polygon_even_odd_hole(); + test_polygon_triangle_rows_out_of_order(); + test_polygon_copies_points(); + test_polygon_invalid(); + test_polygon_max_points(); + test_polygon_comb(); + test_polygon_equal(); + test_runs_match_contains(); + test_run_outside_bbox(); + test_arc_runs_random(); + test_polygon_runs_random(); + test_ellipse_runs_random(); + test_rounded_rect_runs_random(); + test_line_runs_random(); + test_run_ellipse_values(); + test_out_of_range_rejected(); + test_extreme_values(); + + if (failures) { + fprintf(stderr, "%d failure(s)\n", failures); + return 1; + } + printf("all shape tests passed\n"); + return 0; +}