A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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.

---------

authored by

Justinian and committed by
GitHub
825ef56a ed0811c8

+16 -13
+6 -6
lib/Epub/Epub/converters/DirectPixelWriter.h
··· 30 30 void init(GfxRenderer& renderer) { 31 31 fb = renderer.getFrameBuffer(); 32 32 mode = renderer.getRenderMode(); 33 - displayWidthBytes = display.getDisplayWidthBytes(); 33 + displayWidthBytes = renderer.getDisplayWidthBytes(); 34 34 35 - const int phyW = display.getDisplayWidth(); 36 - const int phyH = display.getDisplayHeight(); 35 + const int phyW = renderer.getDisplayWidth(); 36 + const int phyH = renderer.getDisplayHeight(); 37 37 38 38 switch (renderer.getOrientation()) { 39 39 case GfxRenderer::Portrait: 40 - // phyX = y, phyY = (DISPLAY_HEIGHT-1) - x 40 + // phyX = y, phyY = (phyH-1) - x 41 41 phyXBase = 0; 42 42 phyYBase = phyH - 1; 43 43 phyXStepX = 0; ··· 46 46 phyYStepY = 0; 47 47 break; 48 48 case GfxRenderer::LandscapeClockwise: 49 - // phyX = (DISPLAY_WIDTH-1) - x, phyY = (DISPLAY_HEIGHT-1) - y 49 + // phyX = (phyW-1) - x, phyY = (phyH-1) - y 50 50 phyXBase = phyW - 1; 51 51 phyYBase = phyH - 1; 52 52 phyXStepX = -1; ··· 55 55 phyYStepY = -1; 56 56 break; 57 57 case GfxRenderer::PortraitInverted: 58 - // phyX = (DISPLAY_WIDTH-1) - y, phyY = x 58 + // phyX = (phyW-1) - y, phyY = x 59 59 phyXBase = phyW - 1; 60 60 phyYBase = 0; 61 61 phyXStepX = 0;
+3 -3
lib/GfxRenderer/GfxRenderer.cpp
··· 131 131 if (renderMode == GfxRenderer::BW && bmpVal < 3) { 132 132 // Black (also paints over the grays in BW mode) 133 133 renderer.drawPixel(screenX, screenY, pixelState); 134 - } else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (bmpVal == 1 || (gpio.deviceIsX4() && bmpVal == 2))) { 134 + } else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) { 135 135 // Light gray (also mark the MSB if it's going to be a dark gray too) 136 - // X3 AA tuning: keep only the darker antialias level to avoid washed text 136 + // Dedicated X3 gray LUTs now provide proper 4-level gray on both devices 137 137 // We have to flag pixels in reverse for the gray buffers, as 0 leave alone, 1 update 138 138 renderer.drawPixel(screenX, screenY, false); 139 139 } else if (renderMode == GfxRenderer::GRAYSCALE_LSB && bmpVal == 1) { ··· 686 686 687 687 if (renderMode == BW && val < 3) { 688 688 drawPixel(screenX, screenY); 689 - } else if (renderMode == GRAYSCALE_MSB && (val == 1 || (gpio.deviceIsX4() && val == 2))) { 689 + } else if (renderMode == GRAYSCALE_MSB && (val == 1 || val == 2)) { 690 690 drawPixel(screenX, screenY, false); 691 691 } else if (renderMode == GRAYSCALE_LSB && val == 1) { 692 692 drawPixel(screenX, screenY, false);
+3
lib/GfxRenderer/GfxRenderer.h
··· 157 157 // Low level functions 158 158 uint8_t* getFrameBuffer() const; 159 159 size_t getBufferSize() const; 160 + uint16_t getDisplayWidth() const { return panelWidth; } 161 + uint16_t getDisplayHeight() const { return panelHeight; } 162 + uint16_t getDisplayWidthBytes() const { return panelWidthBytes; } 160 163 };
+4 -4
src/util/ScreenshotUtil.cpp
··· 14 14 const uint8_t* fb = renderer.getFrameBuffer(); 15 15 if (fb) { 16 16 String filename_str = "/screenshots/screenshot-" + String(millis()) + ".bmp"; 17 - if (ScreenshotUtil::saveFramebufferAsBmp(filename_str.c_str(), fb, display.getDisplayWidth(), 18 - display.getDisplayHeight())) { 17 + if (ScreenshotUtil::saveFramebufferAsBmp(filename_str.c_str(), fb, renderer.getDisplayWidth(), 18 + renderer.getDisplayHeight())) { 19 19 LOG_DBG("SCR", "Screenshot saved to %s", filename_str.c_str()); 20 20 } else { 21 21 LOG_ERR("SCR", "Failed to save screenshot"); ··· 26 26 27 27 // Display a border around the screen to indicate a screenshot was taken 28 28 if (renderer.storeBwBuffer()) { 29 - renderer.drawRect(6, 6, display.getDisplayHeight() - 12, display.getDisplayWidth() - 12, 2, true); 29 + renderer.drawRect(6, 6, renderer.getDisplayHeight() - 12, renderer.getDisplayWidth() - 12, 2, true); 30 30 renderer.displayBuffer(); 31 31 delay(1000); 32 32 renderer.restoreBwBuffer(); ··· 76 76 } 77 77 78 78 const uint32_t rowSizePadded = (phyWidth + 31) / 32 * 4; 79 - // Max row size for 528px width (X3) = 68 bytes; use fixed buffer to avoid VLA 79 + // Max row size for 528px height (X3) after rotation = 68 bytes; use fixed buffer to avoid VLA 80 80 constexpr size_t kMaxRowSize = 68; 81 81 if (rowSizePadded > kMaxRowSize) { 82 82 LOG_ERR("SCR", "Row size %u exceeds buffer capacity", rowSizePadded);