Skip to content

Walk only items that cover the current row - #22

Open
TomHoenderdos wants to merge 1 commit into
atomvm:mainfrom
TomHoenderdos:row-walk
Open

TomHoenderdos wants to merge 1 commit into
atomvm:mainfrom
TomHoenderdos:row-walk

Conversation

@TomHoenderdos

Copy link
Copy Markdown
Contributor

Summary

Every draw_x call walked the whole display list and tested each item's bounding box against the row, once per run of pixels. The drivers now build, once per row, the list of items that cover it with display_items_row() and pass that to the renderers' draw_x, so items above or below the row cost one bounding box test per line instead of one per run. Output is unchanged.

All drivers (DCS LCD, e-paper, memory LCD, OLED, SDL) and renderers (dcs_lcd, mono, epaper, SDL) are updated. The row pointer array is allocated once per update with the items.

Tested on hardware

ESP32-S3 badge, ST7789 320×240 RGB565 at 80 MHz SPI, ESP-IDF 5.5.2, a racing game with 71–104 items per frame (it uses the shape primitives from #18), with the profiling from #18:

before (median / p90) with this change (median / p90)
draw 71 / 102 ms 40 / 47 ms
frame 119 / 153 ms 111 / 133 ms

Other testing

  • tests/items (ASan+UBSan) on the host, which renders through dcs_lcd_draw_x() with the row lists.
  • Not tested: the SDL plugin as a whole (it doesn't build against release-0.6 on main, ufont_manager_register arity).

This touches the same lines of dcs_lcd_display_driver.c and display_items.h as #18; whichever is merged second will be rebased.

🤖 Generated with Claude Code

Every draw_x call walked the whole display list and tested each item's
bounding box against the row. A line is drawn with one call per run of
pixels, so a line cost runs * items bounding box tests, even though
most items are above or below it.

On an ESP32-S3 badge with a 320x240 ST7789 at 80 MHz, a racing game
sends about 89 items per frame: road, kerb and centre line trapezoids
in 20 horizontal bands, full width grass rects, a sprite strip, text
and cars. Only about 10 items cover any row, but the trapezoids break
each row into many short runs, and drawing took 21 ms per frame, about
89 us per line against 64 us to send one.

The drivers now build, once per row, the list of items that cover it
with display_items_row(), in display list order, and pass it to the
renderers' draw_x functions, which take an array of item pointers and
no longer test the row themselves. The items are not copied, so state
kept in an item stays with it. The pointer array is allocated with the
items, once per update, in every driver. Output is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Tom Hoenderdos <tomhoenderdos@gmail.com>

@bettio bettio left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR after PR we are getting close :)

I have a few comments on possible improvements on this PR:

  1. Rebase on main. #23 replaced do_update() in sdl_display/display.c with a plain full redraw, so that hunk conflicts (GitHub shows it as dirty). Everything else applies as is. The resolution I built and ran is:

        BaseDisplayItem **row = malloc(sizeof(BaseDisplayItem *) * len);
        if (UNLIKELY(len > 0 && !row)) {
            fprintf(stderr, "do_update: failed to alloc row\n");
            display_items_delete(items, len);
            return;
        }
    
        for (int ypos = 0; ypos < screen->h; ypos++) {
            size_t row_len = display_items_row(items, len, ypos, row);
            int xpos = 0;
            while (xpos < screen->w) {
                xpos += draw_x(xpos, ypos, row, row_len);
            }
        }
    
        free(row);
        display_items_delete(items, len);

    Note the display_items_delete() on the failure path: on main the items are no longer kept in prev_items, so returning without it would leak them. (It becomes moot with point 2.)

  2. Consider dropping the row array altogether. The same malloc / check / free block now appears in five drivers plus the test. Instead of a separate array, the per-row list can be threaded through the items themselves: add a scratch link to the item,

    struct BaseDisplayItem
    {
        ...
        // Scratch link used while rendering: the next item covering the current row.
        struct BaseDisplayItem *next;
    };

    let display_items_row(items, len, ypos) return the head of that list (append with a BaseDisplayItem **link, terminate with NULL), and let every *_draw_x() take BaseDisplayItem *head and walk item->next. I tried it on top of your branch: identical output on the same 2000 random lists, same speed within noise (the walk already loads one pointer per item either way), and the same 4 bytes per item the array costs (56 -> 60 bytes on ESP32). What it removes is an allocation and an out-of-memory path per update in every driver, the len == 0 special case, and the items_len parameter of draw_x(). The field must be documented as rendering scratch, like the row memo of your shape items, and must never take part in item comparison. If you would rather not touch the struct, the other way to get a single failure path is to allocate the pointer array in the same malloc as the items inside display_items_new_list().

  3. Subject line. Per the AtomVM style guide the subject starts with one of the standard verbs; "Walk only items that cover the current row" reads well but "Optimize draw_x with per-row item lists" (or similar) would match the convention.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants