Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
b2809fd
feat: factory LUT grayscale rendering engine and perf optimizations
Apr 19, 2026
5e0976b
feat: PXC/XTC viewers, sleep screen, and EPUB factory rendering
Apr 19, 2026
53e300c
feat: grayscale screenshot capture for factory LUT renders
Apr 19, 2026
29e8d75
fix: remove contrast boost from grayscale image quantization
Apr 22, 2026
a405dd0
fix: scope contrast boost to 1-bit thumbnails only, not 2-bit image q…
Apr 22, 2026
a65c4bd
chore: clang-format
Apr 22, 2026
1d135f4
chore: point open-x4-sdk to community-sdk PR#33 (factory LUT grayscale)
Apr 23, 2026
edea5a4
Use HALF_REFRESH instead of FULL_REFRESH for Blank and Logo sleep scr…
pablohc Apr 23, 2026
548c84b
Use direct XTCH plane rendering for 2-bit sleep screen covers
pablohc Apr 23, 2026
228ac94
Use direct XTC page rendering for 1-bit sleep screen covers
pablohc Apr 23, 2026
c791f09
fix: add HALF_REFRESH pre-conditioning for XTCH 2-bit sleep cover fro…
pablohc Apr 23, 2026
bb69393
fix: restore 'Entering Sleep' overlay in sleep screen
pablohc Apr 23, 2026
ee67faa
fix: Atkinson 1-bit dithering for XTCH 2-bit home cover thumbnails
pablohc Apr 23, 2026
2bab76c
fix: Atkinson 1-bit dithering for XTC 1-bit home cover thumbnails
pablohc Apr 23, 2026
565c77f
refactor: use Atkinson1BitDitherer and computeSrcRange helper
pablohc Apr 23, 2026
50bb37e
style: apply clang-format to Xtc.cpp
pablohc Apr 23, 2026
9f3bb61
fix: resolve leftover conflict markers in SleepActivity.cpp
pablohc Apr 23, 2026
b8fa515
fix: unify XTCH 2-bit thumb generation to sequential dithered 1-bit BMP
pablohc Apr 24, 2026
f9be6d9
fix: use area-averaged strip processing for XTCH 2-bit thumb generation
pablohc Apr 24, 2026
56f5e90
fix: validate I/O return values in 2-bit cover BMP generation
pablohc Apr 24, 2026
7fa2b3d
fix: reject oversized PXC files to prevent out-of-bounds write
pablohc Apr 24, 2026
3c06bc0
fix: correct BMP header array size from 70 to 74 bytes
pablohc Apr 24, 2026
c47074b
fix: remove truncated thumbnail on failure in 2-bit thumb generation
pablohc Apr 24, 2026
1233a12
refactor: replace duplicated clamp-and-scale with computeSrcRange in …
pablohc Apr 24, 2026
16fec5c
Remove X3 device-specific grayscale mode handling
itsthisjustin Apr 27, 2026
f0a032f
Disable factory gray mode for X3 devices
itsthisjustin Apr 27, 2026
763d06b
Remove outdated display mode comments
itsthisjustin Apr 27, 2026
8ac04d0
fix: gate 1-bit XTC sleep pre-flash on lastSleepFromReader
Apr 27, 2026
adb7ab2
fix: tighten PXC validation and screenshot BW restore
Apr 27, 2026
2b831ec
docs: explain why sleep screenshot leaves controller desynced
Apr 27, 2026
265fb12
fix: correct Bayer dither thresholds for 4-level palette
Apr 28, 2026
1d72e23
fix: raise EPUB image dither thresholds for factory LUT
Apr 28, 2026
5c14403
Revert "fix: raise EPUB image dither thresholds for factory LUT"
Apr 28, 2026
423c086
fix: adjust dither thresholds and add soft-shoulder for factory LUT
Apr 28, 2026
0e425fd
Merge branch 'master' into feat/lut-pxc
osteotek Apr 30, 2026
97cf9ed
Merge remote-tracking branch 'upstream/master' into feat/lut-pxc
May 2, 2026
65599c6
Merge remote-tracking branch 'upstream/master' into feat/lut-pxc
May 4, 2026
b3fc76e
Merge remote-tracking branch 'upstream/master' into feat/lut-pxc
May 4, 2026
a871656
Merge remote-tracking branch 'upstream/master' into feat/lut-pxc
zgredex May 5, 2026
e2ffc4b
fix: reduce Bayer dither soft-shoulder and rebalance thresholds for f…
pablohc May 5, 2026
fcebe9f
Merge remote-tracking branch 'upstream/master' into feat/lut-pxc
zgredex May 6, 2026
bfb07c6
Merge branch 'feat/lut-pxc' of github.com:zgredex/crosspoint-reader i…
zgredex May 6, 2026
f6a6ddb
fix: PXC sleep screens always use factory LUT grayscale
zgredex May 8, 2026
c0bea86
chore: update SDK to fix X3 factory fast mode fallback
zgredex May 8, 2026
52ddb1f
chore: update SDK to fix grayscaleRevert logic bug
zgredex May 8, 2026
f4960a3
fix: resolve PR1614 xtc status bar merge
zgredex May 8, 2026
1df0060
Merge remote-tracking branch 'upstream/master' into feat/lut-pxc
zgredex May 8, 2026
7ee4f6f
chore: apply clang-format fix for xtc reader
zgredex May 8, 2026
07309bf
merge: resolve conflicts with upstream/master
zgredex May 9, 2026
9d03cae
fix: declare gray cleanup intent + drop redundant RED RAM rebase
zgredex May 9, 2026
63e1dad
Merge remote-tracking branch 'upstream/master' into feat/lut-pxc
zgredex May 9, 2026
368b83b
feat: PXC viewer sibling navigation and set-sleep-cover
zgredex May 9, 2026
783b3c0
add 2-bit XTC render quality setting (Speed/Quality)
zgredex May 12, 2026
4bc9dd2
add prev/next labels to BMP/PXC viewers + Left/Right bindings
zgredex May 12, 2026
843b213
Merge remote-tracking branch 'upstream/master' into feat/lut-pxc
zgredex May 12, 2026
f22edb4
fix: clean sleep and XTC transitions
zgredex May 13, 2026
53a86f5
chore: point SDK to combined grayscale fixes
zgredex May 13, 2026
f687b03
fix: clean factory grayscale transitions
zgredex May 13, 2026
30baf8f
fix: full preflash grayscale sleep screens
zgredex May 13, 2026
c0a6a2a
fix: use SDK factory gray sleep handling
zgredex May 14, 2026
38bcba4
fix: stabilize factory gray sleep screens
zgredex May 15, 2026
9266211
refactor: GrayscaleDriveMode + stock-V5.5.9 factory experiments
zgredex May 16, 2026
400b0bc
chore: bump open-x4-sdk to 4e949dc (VCOM restore in factory Activate)
zgredex May 17, 2026
40bd133
chore: bump open-x4-sdk to 7c8afd0 (revert VCOM restore for stock-match)
zgredex May 17, 2026
ba72549
feat: re-enable precondition with stock-match (CTRL2=0xF7)
zgredex May 17, 2026
f024256
chore: bump open-x4-sdk to b4b9d39 (revert byte-exact LUT experiment)
zgredex May 17, 2026
57b56c6
feat: extend stock-match LUT-before-RAM order to all factory paths
zgredex May 17, 2026
0bd54cc
fix: set displayState=FactoryLut after factory activate in all render…
pablohc May 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/Epub/Epub/blocks/ImageBlock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,13 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
}

const int destY = y + row;
pw.beginRow(destY);
pw.beginRow(destY, x);
for (int col = 0; col < cachedWidth; col++) {
const int byteIdx = col >> 2; // col / 4
const int bitShift = 6 - (col & 3) * 2; // MSB first within byte
uint8_t pixelValue = (rowBuffer[byteIdx] >> bitShift) & 0x03;

pw.writePixel(x + col, pixelValue);
pw.writePixel(pixelValue);
}
}

Expand Down
131 changes: 83 additions & 48 deletions lib/Epub/Epub/converters/DirectPixelWriter.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,26 @@

// Direct framebuffer writer that eliminates per-pixel overhead from the image
// rendering hot path. Pre-computes orientation transform as linear coefficients
// and caches render-mode state so the inner loop is: one multiply, one add,
// one shift, and one AND per pixel — no branches, no method calls.
// and caches render-mode state so the inner loop is: two increments, one shift,
// one AND, and one or two bit-writes per pixel — no multiplies, no branches on
// mode or orientation, no method calls.
//
// Caller is responsible for ensuring (outX, outY) are within screen bounds.
// ImageBlock::render() already validates this before entering the pixel loop,
// and the JPEG/PNG callbacks pre-clamp destination ranges to screen bounds.
// Usage:
// pw.init(renderer);
// for each row: pw.beginRow(logicalY); // resets running X/Y state to col 0
// for each col: pw.writePixel(value); // advances running state, writes bit
//
// Caller must call writePixel() for every column in order (0, 1, 2, …) because
// the running state is advanced unconditionally on each call. Caller is also
// responsible for ensuring columns are within screen bounds before entering the
// loop; no bounds checking is performed here.
struct DirectPixelWriter {
uint8_t* fb;
GfxRenderer::RenderMode mode;
uint16_t displayWidthBytes; // Runtime framebuffer stride (X4: 100, X3: 99)
uint8_t* fb2; // Secondary framebuffer for MSB plane (null = two-pass / not active)
uint8_t writeFbMask; // Bit i set → write to fb when pixelValue==i (pre-computed from render mode)
uint8_t writeFb2Mask; // Bit i set → write to fb2 when pixelValue==i
bool fbClearBit; // true = clear bit (BW black); false = set bit (all gray modes)
uint16_t displayWidthBytes; // Runtime framebuffer stride

// Orientation is collapsed into a linear transform:
// phyX = phyXBase + x * phyXStepX + y * phyXStepY
Expand All @@ -24,14 +34,44 @@ struct DirectPixelWriter {
int phyXStepX, phyYStepX; // per logical-X step
int phyXStepY, phyYStepY; // per logical-Y step

// Row-precomputed: the Y-dependent portion of the physical coords
int rowPhyXBase, rowPhyYBase;
// Pre-computed once in init(): physical-Y advance per logical-X step (in byte-index units).
int32_t byteIdxYStep;

// Running state — reset by beginRow(), advanced by writePixel().
int curPhyX;
int32_t curByteIdx;

void init(GfxRenderer& renderer) {
void init(const GfxRenderer& renderer) {
fb = renderer.getFrameBuffer();
mode = renderer.getRenderMode();
fb2 = renderer.getSecondaryFrameBuffer();
displayWidthBytes = renderer.getDisplayWidthBytes();

// Pre-compute write masks once so the inner loop has zero mode branches.
writeFbMask = 0;
writeFb2Mask = 0;
fbClearBit = false;
switch (renderer.getRenderMode()) {
case GfxRenderer::BW:
writeFbMask = 0x3;
fbClearBit = true;
break;
case GfxRenderer::GRAYSCALE_MSB:
writeFbMask = 0x6;
break;
case GfxRenderer::GRAYSCALE_LSB:
writeFbMask = 0x2;
break;
case GfxRenderer::GRAY2_LSB:
writeFbMask = 0x5;
if (fb2) writeFb2Mask = 0x3;
break;
case GfxRenderer::GRAY2_MSB:
writeFbMask = 0x3;
break;
default:
break;
}

const int phyW = renderer.getDisplayWidth();
const int phyH = renderer.getDisplayHeight();

Expand Down Expand Up @@ -82,52 +122,47 @@ struct DirectPixelWriter {
phyYStepY = 1;
break;
}

// Per-column advance in physical-Y expressed as a byte-index delta.
byteIdxYStep = static_cast<int32_t>(phyYStepX) * static_cast<int32_t>(displayWidthBytes);
}

// Call once per row before the column loop.
// Pre-computes the Y-dependent portion so writePixel() only needs the X part.
inline void beginRow(int logicalY) {
rowPhyXBase = phyXBase + logicalY * phyXStepY;
rowPhyYBase = phyYBase + logicalY * phyYStepY;
// startLogicalX is the X coordinate of the first writePixel() call for this row (default 0).
// Running state is initialised at startLogicalX so every subsequent writePixel() call
// advances to startLogicalX+1, startLogicalX+2, … with zero per-pixel multiplies.
inline void beginRow(int logicalY, int startLogicalX = 0) {
const int rowPhyXBase = phyXBase + logicalY * phyXStepY;
const int rowPhyYBase = phyYBase + logicalY * phyYStepY;
curPhyX = rowPhyXBase + startLogicalX * phyXStepX;
curByteIdx =
static_cast<int32_t>(rowPhyYBase + startLogicalX * phyYStepX) * static_cast<int32_t>(displayWidthBytes);
}

// Write a single 2-bit dithered pixel value to the framebuffer.
// Must be called after beginRow() for the current row.
// Write a single 2-bit pixel value to the framebuffer and advance to the next column.
// Must be called after beginRow() for the current row, for every column in order.
// No bounds checking — caller guarantees coordinates are valid.
inline void writePixel(int logicalX, uint8_t pixelValue) const {
// Determine whether to draw based on render mode
bool draw;
bool state;
switch (mode) {
case GfxRenderer::BW:
draw = (pixelValue < 3);
state = true;
break;
case GfxRenderer::GRAYSCALE_MSB:
draw = (pixelValue == 1 || pixelValue == 2);
state = false;
break;
case GfxRenderer::GRAYSCALE_LSB:
draw = (pixelValue == 1);
state = false;
break;
default:
return;
}

if (!draw) return;

const int phyX = rowPhyXBase + logicalX * phyXStepX;
const int phyY = rowPhyYBase + logicalX * phyYStepX;

const uint16_t byteIndex = phyY * displayWidthBytes + (phyX >> 3);
// No mode switch — write masks are pre-computed in init() and stored as members.
inline void writePixel(uint8_t pixelValue) {
const int phyX = curPhyX;
const int32_t byteIdx = curByteIdx;
curPhyX += phyXStepX;
curByteIdx += byteIdxYStep;

const bool doFb = (writeFbMask >> pixelValue) & 1;
const bool doFb2 = (writeFb2Mask >> pixelValue) & 1;
if (!doFb && !doFb2) return;

const uint32_t bi = static_cast<uint32_t>(byteIdx) + static_cast<uint32_t>(phyX >> 3);
const uint8_t bitMask = 1 << (7 - (phyX & 7));

if (state) {
fb[byteIndex] &= ~bitMask; // Clear bit (draw black)
} else {
fb[byteIndex] |= bitMask; // Set bit (draw white)
if (doFb) {
if (fbClearBit)
fb[bi] &= ~bitMask;
else
fb[bi] |= bitMask;
}
if (doFb2) fb2[bi] |= bitMask;
}
};

Expand Down
23 changes: 19 additions & 4 deletions lib/Epub/Epub/converters/DitherUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,30 @@ inline const uint8_t bayer4x4[4][4] = {
// Apply Bayer dithering and quantize to 4 levels (0-3)
// Stateless - works correctly with any pixel processing order
inline uint8_t applyBayerDither4Level(uint8_t gray, int x, int y) {
// Soft-shoulder darkening for factory LUT: EPUB image pages render on the
// factory LUT, where palette levels are physically lighter than on the
// differential LUT. Apply a -8 offset to mid-bright pixels onward to bring
// highlights/midtones back down without crushing deep shadow detail.
// Ramp the offset from 0 to 8 across gray [0, 64], flat -8 above 64.
int g = gray;
int offset = (g < 64) ? g * 8 / 64 : 8;
g -= offset;

int bayer = bayer4x4[y & 3][x & 3];
int dither = (bayer - 8) * 5; // Scale to +/-40 (half of quantization step 85)

int adjusted = gray + dither;
int adjusted = g + dither;
if (adjusted < 0) adjusted = 0;
if (adjusted > 255) adjusted = 255;

if (adjusted < 64) return 0;
if (adjusted < 128) return 1;
if (adjusted < 192) return 2;
// T01=48: slightly above original 43, keeps shadow detail while allowing
// near-black pixels to lift to dark gray.
// T12=133: raised from 128 to push more mid-bright source pixels into the
// palette 1 / palette 2 dither zone for perceptual mid-gray.
// T23=218: raised from 192 to expand light-gray range, preserving
// highlight detail on the brighter factory LUT.
if (adjusted < 48) return 0;
if (adjusted < 133) return 1;
if (adjusted < 218) return 2;
return 3;
}
16 changes: 8 additions & 8 deletions lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
if (fineScaleFPX == FP_ONE && fineScaleFPY == FP_ONE) {
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
const int outY = cfgY + dstY;
pw.beginRow(outY);
pw.beginRow(outY, cfgX + dstXStart);
if (caching) cw.beginRow(outY, ctx->config->y);
const uint8_t* row = &pixels[(dstY - blockY) * stride];
for (int dstX = dstXStart; dstX < dstXEnd; dstX++) {
Expand All @@ -189,7 +189,7 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
pw.writePixel(outX, dithered);
pw.writePixel(dithered);
if (caching) cw.writePixel(outX, dithered);
}
}
Expand All @@ -210,7 +210,7 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {

for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
const int outY = cfgY + dstY;
pw.beginRow(outY);
pw.beginRow(outY, cfgX + dstXStart);
if (caching) cw.beginRow(outY, ctx->config->y);
const int32_t srcFyFP = dstY * invScaleFPY;
const int32_t fy = srcFyFP & FP_MASK;
Expand Down Expand Up @@ -248,7 +248,7 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
pw.writePixel(outX, dithered);
pw.writePixel(dithered);
if (caching) cw.writePixel(outX, dithered);
}

Expand All @@ -271,7 +271,7 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
pw.writePixel(outX, dithered);
pw.writePixel(dithered);
if (caching) cw.writePixel(outX, dithered);
}

Expand All @@ -297,7 +297,7 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
pw.writePixel(outX, dithered);
pw.writePixel(dithered);
if (caching) cw.writePixel(outX, dithered);
}
}
Expand All @@ -307,7 +307,7 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
// === Nearest-neighbor (downscale: fineScale < 1.0) ===
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
const int outY = cfgY + dstY;
pw.beginRow(outY);
pw.beginRow(outY, cfgX + dstXStart);
if (caching) cw.beginRow(outY, ctx->config->y);
const int32_t srcFyFP = dstY * invScaleFPY;
int ly = (srcFyFP >> FP_SHIFT) - blockY;
Expand All @@ -330,7 +330,7 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
pw.writePixel(outX, dithered);
pw.writePixel(dithered);
if (caching) cw.writePixel(outX, dithered);
}
}
Expand Down
4 changes: 2 additions & 2 deletions lib/Epub/Epub/converters/PngToFramebufferConverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ int pngDrawCallback(PNGDRAW* pDraw) {
// Pre-compute orientation and render-mode state once per row
DirectPixelWriter pw;
pw.init(*ctx->renderer);
pw.beginRow(outY);
pw.beginRow(outY, outXBase);

DirectCacheWriter cw;
if (caching) {
Expand All @@ -220,7 +220,7 @@ int pngDrawCallback(PNGDRAW* pDraw) {
ditheredGray = gray / 85;
if (ditheredGray > 3) ditheredGray = 3;
}
pw.writePixel(outX, ditheredGray);
pw.writePixel(ditheredGray);
if (caching) cw.writePixel(outX, ditheredGray);
}

Expand Down
2 changes: 2 additions & 0 deletions lib/FsHelpers/FsHelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ bool hasPngExtension(std::string_view fileName) { return checkFileExtension(file

bool hasBmpExtension(std::string_view fileName) { return checkFileExtension(fileName, ".bmp"); }

bool hasPxcExtension(std::string_view fileName) { return checkFileExtension(fileName, ".pxc"); }

bool hasGifExtension(std::string_view fileName) { return checkFileExtension(fileName, ".gif"); }

bool hasEpubExtension(std::string_view fileName) { return checkFileExtension(fileName, ".epub"); }
Expand Down
3 changes: 3 additions & 0 deletions lib/FsHelpers/FsHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ inline bool hasPngExtension(const String& fileName) {
// Check for .bmp extension (case-insensitive)
bool hasBmpExtension(std::string_view fileName);

// Check for .pxc extension (case-insensitive)
bool hasPxcExtension(std::string_view fileName);

// Check for .gif extension (case-insensitive)
bool hasGifExtension(std::string_view fileName);
inline bool hasGifExtension(const String& fileName) {
Expand Down
Loading
Loading