From 825ef56ad8e8e4ae85ce5e6cfe0984a6bc316aff Mon Sep 17 00:00:00 2001 From: Justinian Date: Thu, 9 Apr 2026 02:58:59 +0100 Subject: [PATCH] feat: X3 grayscale antialiasing improvements (#1607) ## Summary Improves text antialiasing quality on the Xteink X3 (SSD1677) display to bring it closer to X4 rendering quality. Addresses white lines through letter strokes and ghosting artifacts during page turns and screen transitions. ## Changes ### Display Driver (open-x4-sdk) - Dedicated X3 grayscale LUTs with tuned VDL drive strengths for dark gray (2 time units) and light gray (3 time units), with active GND hold on non-gray transitions to prevent floating source crosstalk - Tight scan timing: TP2/TP3 reduced to 1 (total gate-on 7 units vs 17), minimizing parasitic charge leakage that caused white lines through letter strokes - Fast diff BB reinforcement: Added mild VDH reinforcing pulse to lut_x3_bb_full so black pixels are actively driven during differential refreshes, clearing gray residue/ghosting - displayGrayBuffer() updated to use the dedicated gray LUT bank instead of full refresh LUTs for X3 ### Rendering Pipeline - Re-enabled light gray rendering for X3 text and images, now safe with dedicated gray LUTs providing proper 4-level gray - Removed isLightGrayRestricted() gating that was limiting X3 to 3-level gray - Runtime display dimensions in DirectPixelWriter and ScreenshotUtil, replaced hardcoded constants with runtime getters to support X3 792x528 resolution ### Note The open-x4-sdk submodule references a commit on juicecultus/community-sdk. A corresponding PR to open-x4-epaper/community-sdk should be merged first so the submodule ref resolves on upstream. ## Testing Tested on physical X3 hardware. White lines through letters significantly reduced, in-book ghosting improved via BB reinforcement, antialiasing visually closer to X4 quality. ## AI Disclosure Yes, AI was used to assist with the development of these changes. --------- --- lib/Epub/Epub/converters/DirectPixelWriter.h | 12 ++++++------ lib/GfxRenderer/GfxRenderer.cpp | 6 +++--- lib/GfxRenderer/GfxRenderer.h | 3 +++ open-x4-sdk | 2 +- src/util/ScreenshotUtil.cpp | 8 ++++---- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/lib/Epub/Epub/converters/DirectPixelWriter.h b/lib/Epub/Epub/converters/DirectPixelWriter.h index 1154b49fb..bc66c2f78 100644 --- a/lib/Epub/Epub/converters/DirectPixelWriter.h +++ b/lib/Epub/Epub/converters/DirectPixelWriter.h @@ -30,14 +30,14 @@ struct DirectPixelWriter { void init(GfxRenderer& renderer) { fb = renderer.getFrameBuffer(); mode = renderer.getRenderMode(); - displayWidthBytes = display.getDisplayWidthBytes(); + displayWidthBytes = renderer.getDisplayWidthBytes(); - const int phyW = display.getDisplayWidth(); - const int phyH = display.getDisplayHeight(); + const int phyW = renderer.getDisplayWidth(); + const int phyH = renderer.getDisplayHeight(); switch (renderer.getOrientation()) { case GfxRenderer::Portrait: - // phyX = y, phyY = (DISPLAY_HEIGHT-1) - x + // phyX = y, phyY = (phyH-1) - x phyXBase = 0; phyYBase = phyH - 1; phyXStepX = 0; @@ -46,7 +46,7 @@ struct DirectPixelWriter { phyYStepY = 0; break; case GfxRenderer::LandscapeClockwise: - // phyX = (DISPLAY_WIDTH-1) - x, phyY = (DISPLAY_HEIGHT-1) - y + // phyX = (phyW-1) - x, phyY = (phyH-1) - y phyXBase = phyW - 1; phyYBase = phyH - 1; phyXStepX = -1; @@ -55,7 +55,7 @@ struct DirectPixelWriter { phyYStepY = -1; break; case GfxRenderer::PortraitInverted: - // phyX = (DISPLAY_WIDTH-1) - y, phyY = x + // phyX = (phyW-1) - y, phyY = x phyXBase = phyW - 1; phyYBase = 0; phyXStepX = 0; diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 1e7bc7124..c1a4b869f 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -131,9 +131,9 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode if (renderMode == GfxRenderer::BW && bmpVal < 3) { // Black (also paints over the grays in BW mode) renderer.drawPixel(screenX, screenY, pixelState); - } else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (bmpVal == 1 || (gpio.deviceIsX4() && bmpVal == 2))) { + } else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) { // Light gray (also mark the MSB if it's going to be a dark gray too) - // X3 AA tuning: keep only the darker antialias level to avoid washed text + // Dedicated X3 gray LUTs now provide proper 4-level gray on both devices // We have to flag pixels in reverse for the gray buffers, as 0 leave alone, 1 update renderer.drawPixel(screenX, screenY, false); } else if (renderMode == GfxRenderer::GRAYSCALE_LSB && bmpVal == 1) { @@ -686,7 +686,7 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con if (renderMode == BW && val < 3) { drawPixel(screenX, screenY); - } else if (renderMode == GRAYSCALE_MSB && (val == 1 || (gpio.deviceIsX4() && val == 2))) { + } else if (renderMode == GRAYSCALE_MSB && (val == 1 || val == 2)) { drawPixel(screenX, screenY, false); } else if (renderMode == GRAYSCALE_LSB && val == 1) { drawPixel(screenX, screenY, false); diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 01556522d..e683e3122 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -157,4 +157,7 @@ class GfxRenderer { // Low level functions uint8_t* getFrameBuffer() const; size_t getBufferSize() const; + uint16_t getDisplayWidth() const { return panelWidth; } + uint16_t getDisplayHeight() const { return panelHeight; } + uint16_t getDisplayWidthBytes() const { return panelWidthBytes; } }; diff --git a/open-x4-sdk b/open-x4-sdk index 157d724d7..a64a3c29b 160000 --- a/open-x4-sdk +++ b/open-x4-sdk @@ -1 +1 @@ -Subproject commit 157d724d7a7389d49fe108e6dd5da2455a5340ba +Subproject commit a64a3c29bebc59b2ccdfe15492cfc4b5e4c26360 diff --git a/src/util/ScreenshotUtil.cpp b/src/util/ScreenshotUtil.cpp index 41f075447..0f14f5060 100644 --- a/src/util/ScreenshotUtil.cpp +++ b/src/util/ScreenshotUtil.cpp @@ -14,8 +14,8 @@ void ScreenshotUtil::takeScreenshot(GfxRenderer& renderer) { const uint8_t* fb = renderer.getFrameBuffer(); if (fb) { String filename_str = "/screenshots/screenshot-" + String(millis()) + ".bmp"; - if (ScreenshotUtil::saveFramebufferAsBmp(filename_str.c_str(), fb, display.getDisplayWidth(), - display.getDisplayHeight())) { + if (ScreenshotUtil::saveFramebufferAsBmp(filename_str.c_str(), fb, renderer.getDisplayWidth(), + renderer.getDisplayHeight())) { LOG_DBG("SCR", "Screenshot saved to %s", filename_str.c_str()); } else { LOG_ERR("SCR", "Failed to save screenshot"); @@ -26,7 +26,7 @@ void ScreenshotUtil::takeScreenshot(GfxRenderer& renderer) { // Display a border around the screen to indicate a screenshot was taken if (renderer.storeBwBuffer()) { - renderer.drawRect(6, 6, display.getDisplayHeight() - 12, display.getDisplayWidth() - 12, 2, true); + renderer.drawRect(6, 6, renderer.getDisplayHeight() - 12, renderer.getDisplayWidth() - 12, 2, true); renderer.displayBuffer(); delay(1000); renderer.restoreBwBuffer(); @@ -76,7 +76,7 @@ bool ScreenshotUtil::saveFramebufferAsBmp(const char* filename, const uint8_t* f } const uint32_t rowSizePadded = (phyWidth + 31) / 32 * 4; - // Max row size for 528px width (X3) = 68 bytes; use fixed buffer to avoid VLA + // Max row size for 528px height (X3) after rotation = 68 bytes; use fixed buffer to avoid VLA constexpr size_t kMaxRowSize = 68; if (rowSizePadded > kMaxRowSize) { LOG_ERR("SCR", "Row size %u exceeds buffer capacity", rowSizePadded);