From 66268507c859e80f366feceb0509fbc9c90aab79 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Thu, 6 Aug 2026 16:53:00 +0200 Subject: [PATCH] fix(epaper): persist the window union across refreshes (fix partial-refresh tears) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit e-paper RAM persists between refreshes, but the decoder reset the accumulated window union on every 0x20 MASTER_ACTIVATION. A PARTIAL refresh (BadgeOS redraws only a strip when you navigate) then set a small window, so composeFrame emitted just that strip, placed top-left with the rest black — reading as a 90°-rotated screen with a black band down the side. Keep the union (and the greyscale flag) across refreshes so it stays at the full-panel extent set by the first full refresh; every frame now composes the whole persistent RAM and partial updates land in place. Cleared only by a real RST (0x12). Paged full refreshes (GxEPD2) re-cover the panel each frame so they are unchanged; the BadgeOS menu render is byte-identical and all 23 decoder/epaper tests pass. --- .../src/simulation/displays/SSD168xDecoder.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/frontend/src/simulation/displays/SSD168xDecoder.ts b/frontend/src/simulation/displays/SSD168xDecoder.ts index 1604640c..5a50ccfe 100644 --- a/frontend/src/simulation/displays/SSD168xDecoder.ts +++ b/frontend/src/simulation/displays/SSD168xDecoder.ts @@ -396,12 +396,16 @@ export class SSD168xDecoder { this.refreshedCount += 1; const frame = this.composeFrame(); this.onFlush?.(frame); - // Start a fresh window union for the next frame's pages, and a fresh - // red-plane-written flag so a subsequent 1-bit-only refresh (0x24 with - // no 0x26) is not mis-detected as greyscale. - this.winXSet = false; - this.winYSet = false; - this.redWritten = false; + // Do NOT reset the window union or the greyscale flag here. e-paper RAM + // persists between refreshes: a PARTIAL refresh (BadgeOS redraws only a + // strip on navigation) sets a small window, and resetting the union made + // composeFrame emit just that strip — placed top-left with the rest + // black, which reads as a 90°-rotated screen with a black band. Keeping + // the union means it stays at the full-panel extent established by the + // first full refresh, so every frame composes the whole persistent RAM + // and partial updates land in place. Paged full refreshes (GxEPD2) still + // re-cover the full panel each frame, so their union is unchanged. Both + // are cleared only by a real RST (0x12 -> reset()). return; } // WRITE_BLACK/RED are recognized no-ops here; handleData() routes their