Skip to content

GDEQ031T10: shadow framebuffer is committed before the panel confirms the refresh #606

Description

@Crazypedia

Summary

Drivers/gdeq031t10-module/source/gdeq031t10.cpp keeps shadow_framebuffer as its record of what the panel is currently displaying. Both refresh paths update that record before the panel confirms the refresh completed, so any failure leaves it describing content that was never shown.

Present on current main (c729e83).

Where

refresh_full() copies the render buffer into the shadow at line 235, then uses the shadow as the source for the new-data write. The confirming wait_while_busy() is at line 246, after the CMD_DISPLAY_REFRESH trigger:

std::memcpy(internal->shadow_framebuffer, render_bitmap, FRAMEBUFFER_SIZE);   // line 235
if (!write_command(internal, CMD_DATA_START_NEW) || !write_data(...)) { ... }
if (!write_command(internal, CMD_DISPLAY_REFRESH)) { ... }
delay_millis(1);
if (!wait_while_busy(internal)) {                                              // line 246
    LOG_E(TAG, "Full refresh did not complete");                               // shadow already updated
}

A failed new-data write, a failed refresh trigger, or a BUSY timeout all leave the shadow updated while the panel still holds the previous frame.

refresh_window() has the same shape, writing the shadow inside the gather loop at line 310, before the new-data write and before its confirm at line 321.

Why it matters

The shadow is not only a change-detection cache. It is sent to the controller as CMD_DATA_START_OLD (line 229) so the panel can compute per-pixel transitions. Once it is wrong:

  • The controller computes transitions from an image the panel never displayed, so the next update is derived from a false starting point.
  • gdeq031t10_draw_bitmap() diffs the incoming frame against the shadow (line 392) to find the changed bounding box. A region that matches a wrong shadow is treated as clean, so the panel keeps stale content while the UI believes it was painted.

Nothing self-corrects, because no path reads back what the panel actually holds.

Current exposure

The second consequence is presently masked: gdeq031t10_draw_bitmap() sets force_full_refresh = true unconditionally at line 376 (// Work-around until partial updates are working), which makes the nothing_changed early-out at line 402 unreachable and refresh_window() dead code.

So today the live consequence is the corrupted "old data" starting point after a failed refresh. Both consequences become fully live as soon as partial updates are re-enabled.

Suggested fix

Commit the shadow only after wait_while_busy() confirms, in both paths. On failure the shadow keeps describing the previous content, which is still what the panel shows, so the next change scan correctly sees the region as dirty and repaints it.

refresh_full() can send new data straight from render_bitmap rather than staging it through the shadow, which also removes a full-framebuffer copy from the path.

A note on scope

I have been troubleshooting this on a T-Deck Max, which is the only e-paper hardware I can test on, and I am genuinely unsure how much of this generalises to other e-paper panels in the tree. The one comparison I could make is the M5Stack PaperS3, which hands its previous-frame tracking and waveform computation to EPDiy's highlevel API rather than maintaining its own buffer, so I do not think it is affected by this particular problem. GDEQ031T10 tracks the panel contents itself, which is where the ordering matters.

If anyone with other e-paper hardware has a view on this, I would welcome it. I have learned considerably more about e-ink rendering in the last few weeks than I ever expected to, and I am still fairly sure I am missing things.

Happy to open a PR for this.

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