Skip to content

Two refresh levers: draw into the panel's own framebuffer, and a shared dirty-region contract #26

Description

@bdbarnett

From a design question Brad asked: would moving pygraphics (or its framebuffer) into displayif speed up refreshes?

It would not. pygraphics is already a native C module, with its framebuffer, shapes, drawing and font code in C and its own benchmarks putting that path ten to eighteen times ahead of its pure-Python fallback. Recompiling the same C inside displayif produces the same instructions, and a boundary between two usermods costs nothing after the link. pygraphics also installs as pure Python on boards without custom C modules and as a wheel on desktop, which folding it into a hardware driver repo would spend.

The measured causes of slow refresh, from docs/soft-reset-and-bring-up.md, are bandwidth and cache, not primitive speed: multi-second redraws from a Python per-pixel path, tearing because 16 bits per pixel of continuous scanout does not fit PSRAM bandwidth, and flicker from painting a live scanout buffer or synchronising a whole frame instead of the rows that changed.

That leaves two levers. Both are about ownership rather than location, and both can be done with the repos where they are.

1. Draw into the panel's own framebuffer

The pieces exist. src/ports/esp32/mod_dotclockframebuffer.c:291 takes the panel's own buffers from the driver rather than allocating, and the double-buffer arrangement paints the back buffer and promotes it on refresh. What is missing is a stated contract that a drawing layer can rely on: which buffer is safe to paint right now, when it flips, and whether the caller may hold a memoryview of it across a refresh.

Without that, a caller that wants to be safe allocates its own buffer and copies a whole frame per present. On a 720 by 720 panel that copy is the single largest avoidable cost in the path.

Wanted: an explicit, documented way to ask a display for a writable surface plus its stride and pixel order, and to be told when that surface stops being valid. The attr-exposed method pattern already used for refresh and blit is the obvious shape.

2. A shared dirty-region contract

dotclockframebuffer_msync_rows() at src/ports/esp32/mod_dotclockframebuffer.c:196 already synchronises only the rows a blit touched, and mod_mipidsi.c:406 does the same for its DPI path. But that knowledge is rediscovered inside each blit, from that blit's own arguments. A drawing layer that has just filled twenty scattered rectangles has better information and no way to pass it.

The result is either over-synchronising, a whole frame when a fraction changed, or a call per rectangle, each with its own cache-line rounding and, with bounce buffers on, its own contention that the file's own comment at line 86 warns about.

Wanted: a way to accumulate a dirty region across many drawing operations and present once. A union rectangle is probably enough and is cheap. A row bitmap is the next step up if scattered updates turn out to matter.

Worth measuring first

Neither should be built on the strength of this reasoning alone. The cheap measurement is a full-frame present on the 720 by 720 panel, timed three ways: today's path, painting directly into the panel buffer, and a dirty-region present that touches a tenth of the rows. If the copy and the synchronisation are not most of the time, the levers are the wrong ones and this issue should be closed with the numbers in it.

Related: pygraphics is where the drawing side of any such contract would live, so this cannot land in one repo alone.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions