From 30c50ef45b5d2698ab82cb2b1fefcdf5d4f9279e Mon Sep 17 00:00:00 2001 From: Eunchurn Park Date: Sun, 28 Dec 2025 18:04:48 +0900 Subject: [PATCH] perf(xtc): optimize XTCH grayscale rendering and memory usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove storeBwBuffer() requirement for grayscale rendering - Peak memory reduced from 144KB to 96KB - Use 4-pass rendering: BW display → LSB → MSB → grayscale → BW restore - Optimize PageInfo struct (24→16 bytes) - Change offset from uint64_t to uint32_t (4GB file limit) - Saves ~15KB for large books (e.g., 1883 pages: 45KB → 30KB) - Add storeBwBuffer() return value for error handling - Add pixel distribution logging for debugging - Fix LSB/MSB grayscale mapping per README spec --- lib/GfxRenderer/GfxRenderer.cpp | 8 +- lib/GfxRenderer/GfxRenderer.h | 2 +- lib/Xtc/Xtc/XtcParser.cpp | 4 +- lib/Xtc/Xtc/XtcTypes.h | 7 +- src/activities/reader/XtcReaderActivity.cpp | 88 ++++++++++++++++++--- 5 files changed, 87 insertions(+), 22 deletions(-) diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 6433748..196fdfd 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -286,12 +286,13 @@ void GfxRenderer::freeBwBufferChunks() { * This should be called before grayscale buffers are populated. * A `restoreBwBuffer` call should always follow the grayscale render if this method was called. * Uses chunked allocation to avoid needing 48KB of contiguous memory. + * Returns true if buffer was stored successfully, false if allocation failed. */ -void GfxRenderer::storeBwBuffer() { +bool GfxRenderer::storeBwBuffer() { const uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); if (!frameBuffer) { Serial.printf("[%lu] [GFX] !! No framebuffer in storeBwBuffer\n", millis()); - return; + return false; } // Allocate and copy each chunk @@ -312,7 +313,7 @@ void GfxRenderer::storeBwBuffer() { BW_BUFFER_CHUNK_SIZE); // Free previously allocated chunks freeBwBufferChunks(); - return; + return false; } memcpy(bwBufferChunks[i], frameBuffer + offset, BW_BUFFER_CHUNK_SIZE); @@ -320,6 +321,7 @@ void GfxRenderer::storeBwBuffer() { Serial.printf("[%lu] [GFX] Stored BW buffer in %zu chunks (%zu bytes each)\n", millis(), BW_BUFFER_NUM_CHUNKS, BW_BUFFER_CHUNK_SIZE); + return true; } /** diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 00a525d..3893bc9 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -65,7 +65,7 @@ class GfxRenderer { void copyGrayscaleLsbBuffers() const; void copyGrayscaleMsbBuffers() const; void displayGrayBuffer() const; - void storeBwBuffer(); + bool storeBwBuffer(); // Returns true if buffer was stored successfully void restoreBwBuffer(); // Low level functions diff --git a/lib/Xtc/Xtc/XtcParser.cpp b/lib/Xtc/Xtc/XtcParser.cpp index 41b5796..cb380f9 100644 --- a/lib/Xtc/Xtc/XtcParser.cpp +++ b/lib/Xtc/Xtc/XtcParser.cpp @@ -149,7 +149,7 @@ XtcError XtcParser::readPageTable() { return XtcError::READ_ERROR; } - m_pageTable[i].offset = entry.dataOffset; + m_pageTable[i].offset = static_cast(entry.dataOffset); m_pageTable[i].size = entry.dataSize; m_pageTable[i].width = entry.width; m_pageTable[i].height = entry.height; @@ -189,7 +189,7 @@ size_t XtcParser::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSiz // Seek to page data if (!m_file.seek(page.offset)) { - Serial.printf("[%lu] [XTC] Failed to seek to page %u at offset %llu\n", millis(), pageIndex, page.offset); + Serial.printf("[%lu] [XTC] Failed to seek to page %u at offset %lu\n", millis(), pageIndex, page.offset); m_lastError = XtcError::READ_ERROR; return 0; } diff --git a/lib/Xtc/Xtc/XtcTypes.h b/lib/Xtc/Xtc/XtcTypes.h index 97b1f69..30761d9 100644 --- a/lib/Xtc/Xtc/XtcTypes.h +++ b/lib/Xtc/Xtc/XtcTypes.h @@ -82,14 +82,15 @@ struct XtgPageHeader { }; #pragma pack(pop) -// Page information (internal use) +// Page information (internal use, optimized for memory) struct PageInfo { - uint64_t offset; // File offset to page data + uint32_t offset; // File offset to page data (max 4GB file size) uint32_t size; // Data size (bytes) uint16_t width; // Page width uint16_t height; // Page height uint8_t bitDepth; // 1 = XTG (1-bit), 2 = XTH (2-bit grayscale) -}; + uint8_t padding; // Alignment padding +}; // 16 bytes total // Error codes enum class XtcError { diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index 37ef54d..c52454a 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -203,26 +203,88 @@ void XtcReaderActivity::renderPage() { const uint8_t* plane2 = pageBuffer + planeSize; // Bit2 plane const size_t colBytes = (pageHeight + 7) / 8; // Bytes per column (100 for 800 height) + // Lambda to get pixel value at (x, y) + auto getPixelValue = [&](uint16_t x, uint16_t y) -> uint8_t { + const size_t colIndex = pageWidth - 1 - x; + const size_t byteInCol = y / 8; + const size_t bitInByte = 7 - (y % 8); + const size_t byteOffset = colIndex * colBytes + byteInCol; + const uint8_t bit1 = (plane1[byteOffset] >> bitInByte) & 1; + const uint8_t bit2 = (plane2[byteOffset] >> bitInByte) & 1; + return (bit1 << 1) | bit2; + }; + + // Optimized grayscale rendering without storeBwBuffer (saves 48KB peak memory) + // Flow: BW display → LSB/MSB passes → grayscale display → re-render BW for next frame + + // Count pixel distribution for debugging + uint32_t pixelCounts[4] = {0, 0, 0, 0}; for (uint16_t y = 0; y < pageHeight; y++) { for (uint16_t x = 0; x < pageWidth; x++) { - // Column-major, right to left: column index = (width - 1 - x) - const size_t colIndex = pageWidth - 1 - x; - const size_t byteInCol = y / 8; - const size_t bitInByte = 7 - (y % 8); // MSB = topmost pixel + pixelCounts[getPixelValue(x, y)]++; + } + } + Serial.printf("[%lu] [XTR] Pixel distribution: White=%lu, DarkGrey=%lu, LightGrey=%lu, Black=%lu\n", millis(), + pixelCounts[0], pixelCounts[1], pixelCounts[2], pixelCounts[3]); - const size_t byteOffset = colIndex * colBytes + byteInCol; - const uint8_t bit1 = (plane1[byteOffset] >> bitInByte) & 1; - const uint8_t bit2 = (plane2[byteOffset] >> bitInByte) & 1; - const uint8_t pixelValue = (bit1 << 1) | bit2; - - // Threshold: 0=White, 1,2,3=Dark (for best text contrast) - const bool isBlack = (pixelValue >= 1); - - if (isBlack) { + // Pass 1: BW buffer - draw all non-white pixels as black + for (uint16_t y = 0; y < pageHeight; y++) { + for (uint16_t x = 0; x < pageWidth; x++) { + if (getPixelValue(x, y) >= 1) { renderer.drawPixel(x, y, true); } } } + + // Display BW first with half refresh (clean base for grayscale overlay) + renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + + // Pass 2: LSB buffer - mark DARK gray only (XTH value 1) + // README: "mark the **dark** grays pixels with `1`" + renderer.clearScreen(0x00); + for (uint16_t y = 0; y < pageHeight; y++) { + for (uint16_t x = 0; x < pageWidth; x++) { + if (getPixelValue(x, y) == 1) { // Dark grey only + renderer.drawPixel(x, y, true); + } + } + } + renderer.copyGrayscaleLsbBuffers(); + + // Pass 3: MSB buffer - mark LIGHT AND DARK gray (XTH value 1 or 2) + // README: "mark the **light and dark** grays pixels with `1`" + renderer.clearScreen(0x00); + for (uint16_t y = 0; y < pageHeight; y++) { + for (uint16_t x = 0; x < pageWidth; x++) { + const uint8_t pv = getPixelValue(x, y); + if (pv == 1 || pv == 2) { // Dark grey or Light grey + renderer.drawPixel(x, y, true); + } + } + } + renderer.copyGrayscaleMsbBuffers(); + + // Display grayscale overlay + renderer.displayGrayBuffer(); + + // Pass 4: Re-render BW to framebuffer (restore for next frame, instead of restoreBwBuffer) + renderer.clearScreen(); + for (uint16_t y = 0; y < pageHeight; y++) { + for (uint16_t x = 0; x < pageWidth; x++) { + if (getPixelValue(x, y) >= 1) { + renderer.drawPixel(x, y, true); + } + } + } + + // Reset refresh counter (grayscale display is a full refresh) + pagesUntilFullRefresh = pagesPerRefresh; + + free(pageBuffer); + + Serial.printf("[%lu] [XTR] Rendered page %lu/%lu (2-bit grayscale)\n", millis(), currentPage + 1, + xtc->getPageCount()); + return; } else { // 1-bit mode: 8 pixels per byte, MSB first const size_t srcRowBytes = (pageWidth + 7) / 8; // 60 bytes for 480 width