feat: X3 grayscale antialiasing improvements#1607
feat: X3 grayscale antialiasing improvements#1607itsthisjustin merged 6 commits intocrosspoint-reader:masterfrom
Conversation
## Summary Adds dedicated X3 grayscale LUTs and improves fast differential refresh for better antialiased text rendering on the Xteink X3 display. ## Changes - Dedicated grayscale LUTs (lut_x3_*_gray): Single-phase waveforms with tuned VDL drive for dark gray (2 units) and light gray (3 units), with active GND hold on non-gray transitions to prevent crosstalk - Tight scan timing: TP2/TP3 reduced from 6 to 1 to minimize idle gate-on time (17 to 7 units per row), reducing parasitic charge leakage that causes white lines through letter strokes - BB reinforcement: Added mild VDH pulse (VS=0x10) to lut_x3_bb_full so black pixels get actively reinforced during fast differential refreshes, clearing gray residue/ghosting - displayGrayBuffer() updated to use the dedicated gray LUT bank instead of full refresh LUTs Companion to crosspoint-reader/crosspoint-reader#1607 ## AI Disclosure Yes, AI was used to assist with the development of these changes.
|
There's some overlap with #1572 that needs to be resolved quick |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📜 Recent review details⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
📝 WalkthroughWalkthroughDirectPixelWriter and ScreenshotUtil now use renderer-provided runtime display dimensions and stride via new GfxRenderer getters. GfxRenderer exposes three new accessors. Grayscale handling was unified (value 2 treated as light gray on all devices). Submodule pointer updated. Changes
Sequence Diagram(s)(omitted) Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/util/ScreenshotUtil.cpp (1)
28-29: Consider usinggetScreenWidth()/getScreenHeight()for clarity.The current code swaps
getDisplayHeight()/getDisplayWidth()because Portrait orientation maps physical height to logical width. While this works correctly, using the logical accessors would be clearer:- renderer.drawRect(6, 6, renderer.getDisplayHeight() - 12, renderer.getDisplayWidth() - 12, 2, true); + renderer.drawRect(6, 6, renderer.getScreenWidth() - 12, renderer.getScreenHeight() - 12, 2, true);This makes the intent explicit and would remain correct if orientation handling ever changes.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/util/ScreenshotUtil.cpp` around lines 28 - 29, Replace the swapped display dimension calls with the logical screen accessors to make intent explicit: in the renderer.storeBwBuffer() block, change the renderer.drawRect call that currently uses renderer.getDisplayHeight() and renderer.getDisplayWidth() to use renderer.getScreenWidth() and renderer.getScreenHeight() (or the appropriate logical-width/height accessors) so the rectangle parameters reflect logical screen dimensions; update the call referencing renderer.drawRect and leave renderer.storeBwBuffer() unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@open-x4-sdk`:
- Line 1: The PR currently pins the submodule to commit
121824b5089d3f584fc8b345a94d5e07821003aa which is not reachable on the
configured remote (https://github.com/open-x4-epaper/community-sdk.git); block
this merge and either update the submodule reference to a commit that exists on
the remote or wait until the upstream community-sdk PR is merged so that commit
121824b5089d3f584fc8b345a94d5e07821003aa becomes reachable, then update the
submodule pointer accordingly (ensure the submodule URL and commit hash in the
repo match the upstream remote and are fetchable in CI).
---
Nitpick comments:
In `@src/util/ScreenshotUtil.cpp`:
- Around line 28-29: Replace the swapped display dimension calls with the
logical screen accessors to make intent explicit: in the
renderer.storeBwBuffer() block, change the renderer.drawRect call that currently
uses renderer.getDisplayHeight() and renderer.getDisplayWidth() to use
renderer.getScreenWidth() and renderer.getScreenHeight() (or the appropriate
logical-width/height accessors) so the rectangle parameters reflect logical
screen dimensions; update the call referencing renderer.drawRect and leave
renderer.storeBwBuffer() unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 150fcd39-7cdf-4b11-be86-d8c8caf8a687
📒 Files selected for processing (5)
lib/Epub/Epub/converters/DirectPixelWriter.hlib/GfxRenderer/GfxRenderer.cpplib/GfxRenderer/GfxRenderer.hopen-x4-sdksrc/util/ScreenshotUtil.cpp
📜 Review details
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: pablohc
Repo: crosspoint-reader/crosspoint-reader PR: 1554
File: src/activities/boot_sleep/SleepActivity.cpp:40-63
Timestamp: 2026-04-02T15:35:02.228Z
Learning: In crosspoint-reader/crosspoint-reader (src/activities/boot_sleep/SleepActivity.cpp, SleepActivity::renderBitmapSleepScreen), CrossPointSettings::SLEEP_SCREEN_FILTER::FILTER_CONTRAST is intentionally handled implicitly: `hasGreyscale` is computed as `bitmap.hasGreyscale() && SETTINGS.sleepScreenFilter == FILTER_NONE`. When FILTER_CONTRAST is selected, `hasGreyscale` evaluates to false, which skips the 4-level greyscale rendering path and falls back to 1-bit B&W dithered rendering, producing a visibly higher-contrast result on bitmaps with greyscale data. LOGO and BLANK screens are unaffected by FILTER_CONTRAST by design — they contain no bitmap content where a contrast effect would be meaningful. Do not flag FILTER_CONTRAST as unimplemented or a no-op in future reviews.
Learnt from: Tritlo
Repo: crosspoint-reader/crosspoint-reader PR: 1003
File: src/activities/reader/EpubReaderActivity.cpp:657-674
Timestamp: 2026-02-19T17:46:36.345Z
Learning: In src/activities/reader/EpubReaderActivity.cpp's renderContents() method, when uncached images exist, Phase 1 intentionally calls displayBuffer(forceFullRefresh) to perform a HALF_REFRESH (if needed), while Phase 2 intentionally calls renderer.displayBuffer() directly without forceFullRefresh. This is by design: Phase 1's refresh clears the screen properly to prevent ghosting, so Phase 2 can use a faster refresh mode for better performance. The grayscale anti-aliasing is handled separately after renderContents() via displayGrayBuffer().
📚 Learning: 2026-04-02T15:35:02.228Z
Learnt from: pablohc
Repo: crosspoint-reader/crosspoint-reader PR: 1554
File: src/activities/boot_sleep/SleepActivity.cpp:40-63
Timestamp: 2026-04-02T15:35:02.228Z
Learning: In crosspoint-reader/crosspoint-reader (src/activities/boot_sleep/SleepActivity.cpp, SleepActivity::renderBitmapSleepScreen), CrossPointSettings::SLEEP_SCREEN_FILTER::FILTER_CONTRAST is intentionally handled implicitly: `hasGreyscale` is computed as `bitmap.hasGreyscale() && SETTINGS.sleepScreenFilter == FILTER_NONE`. When FILTER_CONTRAST is selected, `hasGreyscale` evaluates to false, which skips the 4-level greyscale rendering path and falls back to 1-bit B&W dithered rendering, producing a visibly higher-contrast result on bitmaps with greyscale data. LOGO and BLANK screens are unaffected by FILTER_CONTRAST by design — they contain no bitmap content where a contrast effect would be meaningful. Do not flag FILTER_CONTRAST as unimplemented or a no-op in future reviews.
Applied to files:
src/util/ScreenshotUtil.cpplib/GfxRenderer/GfxRenderer.cpp
📚 Learning: 2026-03-09T11:53:24.166Z
Learnt from: jpirnay
Repo: crosspoint-reader/crosspoint-reader PR: 1360
File: src/activities/boot_sleep/SleepActivity.cpp:83-86
Timestamp: 2026-03-09T11:53:24.166Z
Learning: In crosspoint-reader/crosspoint-reader (ESP32-C3 e-ink reader), `CrossPointState::lastSleepImage` is a `uint8_t` with `UINT8_MAX` used as an "unset" sentinel. A sentinel collision at index 255 would require 256+ full-screen BMP sleep images (~240 KB each minimum); at that count the heap is exhausted building the `std::vector<std::string> files` long before the sentinel can collide, so the collision is unreachable in practice. Widening the type would also require a binary state file version bump (`serialization::readPod` reads exactly `sizeof(uint8_t)`), adding unjustified churn. Do not flag this as a bug in future reviews.
Applied to files:
src/util/ScreenshotUtil.cpp
📚 Learning: 2026-02-27T22:49:59.600Z
Learnt from: ngxson
Repo: crosspoint-reader/crosspoint-reader PR: 1218
File: src/activities/ActivityManager.cpp:254-265
Timestamp: 2026-02-27T22:49:59.600Z
Learning: In this codebase, assertions are always enabled (no NDEBUG). Use assert() to crash on programmer errors and surface logic bugs during development and in production builds. Do not rely on asserts for runtime error handling; they should enforce invariants that must always hold. Keep asserts side-effect free and inexpensive, and avoid relying on them for user-visible failures. Include <cassert> where appropriate and document the invariant being tested.
Applied to files:
src/util/ScreenshotUtil.cpplib/GfxRenderer/GfxRenderer.cpp
📚 Learning: 2026-03-02T10:14:16.036Z
Learnt from: Uri-Tauber
Repo: crosspoint-reader/crosspoint-reader PR: 1245
File: lib/Epub/Epub/Section.cpp:277-308
Timestamp: 2026-03-02T10:14:16.036Z
Learning: Guideline: Strengthen serialization::readString to defend against unbounded growth when reading from disk data. Implement and enforce a maximum allowed length (e.g., a configured or reasonable constant) and validate the incoming length before resizing or allocating. Audit all call sites (e.g., BookMetadataCache, TextBlock, KOReaderCredentialStore, Section cache readers) to ensure they do not rely on unbounded len-based resizing. If the readString API must remain, add internal safeguards (bounds checks, length validation, and error handling) so per-call-site validations are not required. Ensure Section cache files remain versioned (SECTION_FILE_VERSION) and parameter mismatches invalidate caches, but do not rely on unsafe allocations; prefer safe, bounded reads with explicit errors on invalid data.
Applied to files:
src/util/ScreenshotUtil.cpplib/GfxRenderer/GfxRenderer.cpp
📚 Learning: 2026-03-28T11:06:29.611Z
Learnt from: pablohc
Repo: crosspoint-reader/crosspoint-reader PR: 1488
File: src/activities/home/HomeActivity.cpp:92-95
Timestamp: 2026-03-28T11:06:29.611Z
Learning: When reviewing crosspoint-reader code, avoid flagging a missing `renderer.displayBuffer()` call immediately after `GUI.drawPopup()` / `BaseTheme::drawPopup()`: `BaseTheme::drawPopup()` already calls `renderer.displayBuffer()` before returning, so the popup is guaranteed to be flushed to the e-ink panel before subsequent blocking work begins. Conversely, do not require a `renderer.displayBuffer()` call after `fillPopupProgress()`; it intentionally does not flush, so intermediate progress-bar updates may not appear unless the update granularity warrants an explicit flush elsewhere.
Applied to files:
src/util/ScreenshotUtil.cpplib/GfxRenderer/GfxRenderer.cpp
📚 Learning: 2026-02-19T17:46:36.345Z
Learnt from: Tritlo
Repo: crosspoint-reader/crosspoint-reader PR: 1003
File: src/activities/reader/EpubReaderActivity.cpp:657-674
Timestamp: 2026-02-19T17:46:36.345Z
Learning: In src/activities/reader/EpubReaderActivity.cpp's renderContents() method, when uncached images exist, Phase 1 intentionally calls displayBuffer(forceFullRefresh) to perform a HALF_REFRESH (if needed), while Phase 2 intentionally calls renderer.displayBuffer() directly without forceFullRefresh. This is by design: Phase 1's refresh clears the screen properly to prevent ghosting, so Phase 2 can use a faster refresh mode for better performance. The grayscale anti-aliasing is handled separately after renderContents() via displayGrayBuffer().
Applied to files:
lib/Epub/Epub/converters/DirectPixelWriter.h
📚 Learning: 2026-03-17T15:27:17.468Z
Learnt from: znelson
Repo: crosspoint-reader/crosspoint-reader PR: 1413
File: lib/EpdFont/EpdFont.cpp:34-36
Timestamp: 2026-03-17T15:27:17.468Z
Learning: In crosspoint-reader/crosspoint-reader, `EpdFont::getGlyph()` falls back to `getGlyph(REPLACEMENT_GLYPH)` (U+FFFD) before returning `nullptr`. All fonts in this project are guaranteed to include a U+FFFD replacement glyph, so the `!glyph` null branch in `EpdFont::getTextBounds()` (and similar rendering paths) is unreachable in practice. Do not flag stale-state issues in that branch (e.g., leftover `lastBaseAdvanceFP`/`lastBaseTop` after a null glyph) as bugs in future reviews.
Applied to files:
lib/GfxRenderer/GfxRenderer.cpp
🔇 Additional comments (8)
lib/GfxRenderer/GfxRenderer.cpp (2)
134-142: LGTM - Light gray rendering now enabled for both X3 and X4 devices.The removal of the device-type gating and updated comment correctly reflect the PR's objective: with dedicated X3 gray LUTs in the display driver, both
bmpVal == 1(dark gray) andbmpVal == 2(light gray) are now rendered inGRAYSCALE_MSBmode on all devices. The logic remains consistent with theGRAYSCALE_LSBcheck below (dark gray only atbmpVal == 1).
686-690: LGTM - Consistent grayscale handling for bitmap rendering.The
GRAYSCALE_MSBcondition now matches the text rendering path inrenderCharImpl, ensuring images and text receive identical 4-level gray treatment on both X3 and X4 devices.lib/GfxRenderer/GfxRenderer.h (1)
160-162: LGTM - Clean runtime dimension accessors.These inline getters correctly expose the runtime-initialized panel dimensions, enabling
DirectPixelWriterandScreenshotUtilto support both X4 (800×480) and X3 (792×528) displays without compile-time constants.lib/Epub/Epub/converters/DirectPixelWriter.h (3)
18-36: LGTM - Runtime dimension initialization is correct.The new
displayWidthBytesmember and initialization from the renderer's accessors properly support both X4 (800×480) and X3 (792×528) displays. ThephyW/phyHlocal variables correctly feed into the orientation transform calculations below.
38-74: LGTM - Orientation transforms correctly use runtime dimensions.Each orientation case properly substitutes the runtime
phyW/phyHfor the compile-time constants in the base coordinate calculations. The step coefficients remain unchanged as they only depend on direction, not panel size.
123-124: LGTM - Framebuffer indexing uses runtime dimension.The
byteIndexcalculation correctly uses the cacheddisplayWidthBytesmember, enabling proper pixel addressing for both X4 and X3 display widths.src/util/ScreenshotUtil.cpp (2)
17-18: LGTM - Screenshot dimensions now runtime-based.Using
renderer.getDisplayWidth()/getDisplayHeight()correctly passes the actual panel dimensions tosaveFramebufferAsBmp, supporting both X4 (800×480) and X3 (792×528) displays.
79-80: LGTM - Buffer size correctly accounts for X3 resolution.The increased
kMaxRowSizefrom 64 to 68 bytes correctly accommodates X3's 528px dimension after 90° rotation:(528 + 31) / 32 * 4 = 68bytes per padded BMP row. The comment clearly documents the rationale.
open-x4-sdk
Outdated
| @@ -1 +1 @@ | |||
| Subproject commit 157d724d7a7389d49fe108e6dd5da2455a5340ba | |||
| Subproject commit 121824b5089d3f584fc8b345a94d5e07821003aa | |||
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read submodule URL from .gitmodules
url="$(git config -f .gitmodules --get submodule.open-x4-sdk.url)"
sha="121824b5089d3f584fc8b345a94d5e07821003aa"
echo "Submodule URL: $url"
echo "Target SHA: $sha"
# Verify commit object is reachable on remote
if git ls-remote "$url" | grep -qi "$sha"; then
echo "OK: SHA is reachable on remote."
else
echo "ERROR: SHA not found on remote refs. Submodule checkout may fail."
exit 1
fiRepository: crosspoint-reader/crosspoint-reader
Length of output: 267
Block merge until upstream community-sdk PR is merged and commit becomes reachable on remote.
Line 1 pins the submodule to commit 121824b5089d3f584fc8b345a94d5e07821003aa. This commit is currently not reachable on the configured remote (https://github.com/open-x4-epaper/community-sdk.git), which will cause submodule checkout failures during CI and fresh clones. Merge only after the upstream PR is merged and the commit is available on the remote.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@open-x4-sdk` at line 1, The PR currently pins the submodule to commit
121824b5089d3f584fc8b345a94d5e07821003aa which is not reachable on the
configured remote (https://github.com/open-x4-epaper/community-sdk.git); block
this merge and either update the submodule reference to a commit that exists on
the remote or wait until the upstream community-sdk PR is merged so that commit
121824b5089d3f584fc8b345a94d5e07821003aa becomes reachable, then update the
submodule pointer accordingly (ensure the submodule URL and commit hash in the
repo match the upstream remote and are fetchable in CI).
|
Ah, and the SDK submodule needs to be updated to point to the now merged commit open-x4-epaper/community-sdk@a64a3c2. |
…ive, home screen HALF_REFRESH - Add dedicated X3 grayscale LUTs (lut_x3_*_gray) with tuned drive strengths - Reduce light gray (BW) drive from 6 to 3 time units (TP0=3) to fix white lines - Active GND hold on BB/WB/VCOM to prevent floating source crosstalk - Re-enable light gray rendering for X3 text and images - Remove isLightGrayRestricted gating (no longer needed with dedicated LUTs) - Replace hardcoded display constants with runtime values in DirectPixelWriter - Home screen uses HALF_REFRESH on first render to clear ghosting - Runtime display dimension fixes for ScreenshotUtil
- EpubReaderMenuActivity uses HALF_REFRESH on first render to clear ghosting from the book page, matching HomeActivity behavior
- Home and in-book menu now use normal fast refresh instead of HALF_REFRESH - Relies on BB reinforcement (VS=0x10) in fast diff LUTs for ghosting clearing - No black flash on any screen transition
581e601 to
fe6ef40
Compare
|
I will test this shortly and review |
## 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. ---------
## 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. ---------
取り込み内容: - グリフ間隔のdifferential rounding修正 (crosspoint-reader#1413) - X3グレースケールAA品質改善 + open-x4-sdk LUT更新 (crosspoint-reader#1607) - ISO 639-2ハイフネーション対応 (crosspoint-reader#1461) - WiFi初回接続修正 (crosspoint-reader#1521) - 本の末尾ナビゲーション調整 (crosspoint-reader#1425) - ファイルブラウザに拡張子表示 (crosspoint-reader#1019) - ZipFile RAII + メモリリーク修正 (crosspoint-reader#1433, crosspoint-reader#1628) - JPEGDECパッチ削除→upstream修正に切替 (crosspoint-reader#1465) - logPrintfリファクタ (crosspoint-reader#1546) - BMP重複排除 (crosspoint-reader#1439) - C++20 requires / デフォルトメンバ初期化子 (crosspoint-reader#1420, crosspoint-reader#1435) - X3画像レンダリング動的解像度対応 (crosspoint-reader#1572) スキップ: - X3初期サポート (crosspoint-reader#875) — 既に統合済み - ロシア語/ウクライナ語翻訳 — 日本語フォークでは不要 コンフリクト解決: - GfxRenderer.cpp: CJK ExternalFontコード維持 + upstream differential rounding採用 - open-x4-sdk: upstream a64a3c2 (専用X3 LUT + BB強化) に更新 - platformio.ini: JPEGDEC patch削除、CJK UIフォントチェック維持 - Section.cpp: フォーク版バージョン27 + ヒープ安全チェック維持 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

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)
Rendering Pipeline
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.