Skip to content

dcs_lcd: draw several scanlines per SPI transaction - #24

Open
frans-k wants to merge 1 commit into
atomvm:mainfrom
frans-k:batch-scanlines
Open

frans-k wants to merge 1 commit into
atomvm:mainfrom
frans-k:batch-scanlines

Conversation

@frans-k

@frans-k frans-k commented Oct 5, 2026 •

Copy link
Copy Markdown

I'm a total noob so this might be AI slop (generated and tested with Claude),
but as far as I can tell it's sensible, and it seems to work for the game I tested.

Summary

do_update sent every scanline as its own SPI transaction and waited for each one to finish
before swapping buffers: 240 waits for a 320 x 240 frame. A wait ends when the SPI interrupt
wakes the display task, and that task runs at priority 1, no higher than any task involved (the
second scheduler runs at 5), so while both schedulers are busy each wakeup waits for a slot.

The two scanline buffers now hold a batch of lines each, as many as fit in one transaction
of at most 4092 bytes (what ESP-IDF's SPI master accepts for a bus opened without
max_transfer_sz) and at most 8. On a 320 pixel wide RGB565 panel that is 6 lines, so a frame
is 40 transactions and 40 waits instead of 240. The drawing functions still draw one scanline
at a time into screen.pixels, which now moves down the batch. Output is unchanged. The
RGB888 path (ILI9488) converts the whole batch at once.

No new configuration, no priority change, and no change for a panel too wide for two lines
(the batch is then 1, which is the old behaviour).

Measured on hardware

ESP32-S3 badge, ST7789 320 x 240 RGB565 at 80 MHz SPI, ESP-IDF 5.5.5, AtomVM badge-v1 fork,
AtomGL main at df47163 with and without this change. The application is a first person game
that redraws the whole panel as fast as it can, ray casting on both schedulers, with the LED
chain, wifi retries and the keyboard scan running as on any badge. Frames per second counted
as frames handed to AtomGL, turning on the spot, 5 s windows, six windows each:

AtomGL frames per second
main (df47163) 5.3 to 6.0
main + this change 7.2 to 9.8

Time the display task spent waiting for transactions, per frame, from a profiling build of an
older AtomGL (before #21) with the same application: about 100 to 155 ms with a transaction per
line, 50 to 56 ms with batches of 6.

Stability: two sessions of 4 to 5 minutes of play with held keys on this VM, no task watchdog
and no crash.

Not tested: ILI9488 (3 bytes per pixel) and any panel other than this one.

Why not a higher task priority

Raising the display task's priority above the schedulers made the frame about 30 ms, but it is
not safe: the task calls display_message_send, which takes the scheduler's spin lock, and
the lock can be held by a scheduler thread the display task has just pre-empted on the same
core (ESP-IDF's main task, which runs scheduler 0, is priority 1). sched_yield does not give
way to a lower priority, so the core livelocks until the task watchdog fires (seen at
priority 5 and 6 after 45 to 90 seconds of play). Fewer wakeups avoids the problem.

Update: rebased onto main after #18, #21 and #22

The branch is now one commit on main at 4a4065a. The conflict was in do_update: main now walks only the items on each row (display_items_row) and has the ATOMGL_PROFILE timers. Both are kept: the row lookup and the per-line timing run for each line inside a batch, and the SPI wait is timed per batch.

Measured again on the same badge with protolux's badge-v4 AtomVM build (which pins main at 4a4065a), with and without this commit. Same game, turning on the spot, sleep off, two alternating 90 s runs each, average frames per second:

AtomGL Wall columns as rectangles Walls as polygons
main (4a4065a) about 5.2 about 4.6
main + this change about 7.0 about 6.5

Stability since the rebase: many 90 s measurement runs and several minutes of play by hand on this build, with no watchdog or crash seen.

🤖 Generated with Claude Code

Every scanline was sent as its own transaction, and the task waited for each to finish before
swapping buffers. Finishing a transaction wakes this task, which is slow while the VM's
schedulers are busy (the task runs below them), so on a badge with both schedulers casting
frames the 240 waits of a full frame took 100 to 155 ms of a frame that needs about 30 ms of
wire time.

The two scanline buffers now hold a batch of lines each, as many as fit in one transaction of
at most 4092 bytes (the ESP-IDF default) and at most 8: 6 lines on a 320 pixel wide RGB565
panel. A frame is 40 waits instead of 240. Output is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Frans Krojegård <frans.krojegard@gmail.com>
Signed-off-by: Frans Krojegård <frans@krojegard.com>
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.

1 participant