perf(xtc): optimize XTCH grayscale rendering and memory usage
- 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
This commit is contained in:
parent
4ef35afb4d
commit
30c50ef45b
@ -286,12 +286,13 @@ void GfxRenderer::freeBwBufferChunks() {
|
|||||||
* This should be called before grayscale buffers are populated.
|
* This should be called before grayscale buffers are populated.
|
||||||
* A `restoreBwBuffer` call should always follow the grayscale render if this method was called.
|
* A `restoreBwBuffer` call should always follow the grayscale render if this method was called.
|
||||||
* Uses chunked allocation to avoid needing 48KB of contiguous memory.
|
* 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();
|
const uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
|
||||||
if (!frameBuffer) {
|
if (!frameBuffer) {
|
||||||
Serial.printf("[%lu] [GFX] !! No framebuffer in storeBwBuffer\n", millis());
|
Serial.printf("[%lu] [GFX] !! No framebuffer in storeBwBuffer\n", millis());
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allocate and copy each chunk
|
// Allocate and copy each chunk
|
||||||
@ -312,7 +313,7 @@ void GfxRenderer::storeBwBuffer() {
|
|||||||
BW_BUFFER_CHUNK_SIZE);
|
BW_BUFFER_CHUNK_SIZE);
|
||||||
// Free previously allocated chunks
|
// Free previously allocated chunks
|
||||||
freeBwBufferChunks();
|
freeBwBufferChunks();
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
memcpy(bwBufferChunks[i], frameBuffer + offset, BW_BUFFER_CHUNK_SIZE);
|
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,
|
Serial.printf("[%lu] [GFX] Stored BW buffer in %zu chunks (%zu bytes each)\n", millis(), BW_BUFFER_NUM_CHUNKS,
|
||||||
BW_BUFFER_CHUNK_SIZE);
|
BW_BUFFER_CHUNK_SIZE);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -65,7 +65,7 @@ class GfxRenderer {
|
|||||||
void copyGrayscaleLsbBuffers() const;
|
void copyGrayscaleLsbBuffers() const;
|
||||||
void copyGrayscaleMsbBuffers() const;
|
void copyGrayscaleMsbBuffers() const;
|
||||||
void displayGrayBuffer() const;
|
void displayGrayBuffer() const;
|
||||||
void storeBwBuffer();
|
bool storeBwBuffer(); // Returns true if buffer was stored successfully
|
||||||
void restoreBwBuffer();
|
void restoreBwBuffer();
|
||||||
|
|
||||||
// Low level functions
|
// Low level functions
|
||||||
|
|||||||
@ -149,7 +149,7 @@ XtcError XtcParser::readPageTable() {
|
|||||||
return XtcError::READ_ERROR;
|
return XtcError::READ_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_pageTable[i].offset = entry.dataOffset;
|
m_pageTable[i].offset = static_cast<uint32_t>(entry.dataOffset);
|
||||||
m_pageTable[i].size = entry.dataSize;
|
m_pageTable[i].size = entry.dataSize;
|
||||||
m_pageTable[i].width = entry.width;
|
m_pageTable[i].width = entry.width;
|
||||||
m_pageTable[i].height = entry.height;
|
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
|
// Seek to page data
|
||||||
if (!m_file.seek(page.offset)) {
|
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;
|
m_lastError = XtcError::READ_ERROR;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -82,14 +82,15 @@ struct XtgPageHeader {
|
|||||||
};
|
};
|
||||||
#pragma pack(pop)
|
#pragma pack(pop)
|
||||||
|
|
||||||
// Page information (internal use)
|
// Page information (internal use, optimized for memory)
|
||||||
struct PageInfo {
|
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)
|
uint32_t size; // Data size (bytes)
|
||||||
uint16_t width; // Page width
|
uint16_t width; // Page width
|
||||||
uint16_t height; // Page height
|
uint16_t height; // Page height
|
||||||
uint8_t bitDepth; // 1 = XTG (1-bit), 2 = XTH (2-bit grayscale)
|
uint8_t bitDepth; // 1 = XTG (1-bit), 2 = XTH (2-bit grayscale)
|
||||||
};
|
uint8_t padding; // Alignment padding
|
||||||
|
}; // 16 bytes total
|
||||||
|
|
||||||
// Error codes
|
// Error codes
|
||||||
enum class XtcError {
|
enum class XtcError {
|
||||||
|
|||||||
@ -203,26 +203,88 @@ void XtcReaderActivity::renderPage() {
|
|||||||
const uint8_t* plane2 = pageBuffer + planeSize; // Bit2 plane
|
const uint8_t* plane2 = pageBuffer + planeSize; // Bit2 plane
|
||||||
const size_t colBytes = (pageHeight + 7) / 8; // Bytes per column (100 for 800 height)
|
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 y = 0; y < pageHeight; y++) {
|
||||||
for (uint16_t x = 0; x < pageWidth; x++) {
|
for (uint16_t x = 0; x < pageWidth; x++) {
|
||||||
// Column-major, right to left: column index = (width - 1 - x)
|
pixelCounts[getPixelValue(x, y)]++;
|
||||||
const size_t colIndex = pageWidth - 1 - x;
|
}
|
||||||
const size_t byteInCol = y / 8;
|
}
|
||||||
const size_t bitInByte = 7 - (y % 8); // MSB = topmost pixel
|
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;
|
// Pass 1: BW buffer - draw all non-white pixels as black
|
||||||
const uint8_t bit1 = (plane1[byteOffset] >> bitInByte) & 1;
|
for (uint16_t y = 0; y < pageHeight; y++) {
|
||||||
const uint8_t bit2 = (plane2[byteOffset] >> bitInByte) & 1;
|
for (uint16_t x = 0; x < pageWidth; x++) {
|
||||||
const uint8_t pixelValue = (bit1 << 1) | bit2;
|
if (getPixelValue(x, y) >= 1) {
|
||||||
|
|
||||||
// Threshold: 0=White, 1,2,3=Dark (for best text contrast)
|
|
||||||
const bool isBlack = (pixelValue >= 1);
|
|
||||||
|
|
||||||
if (isBlack) {
|
|
||||||
renderer.drawPixel(x, y, true);
|
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 {
|
} else {
|
||||||
// 1-bit mode: 8 pixels per byte, MSB first
|
// 1-bit mode: 8 pixels per byte, MSB first
|
||||||
const size_t srcRowBytes = (pageWidth + 7) / 8; // 60 bytes for 480 width
|
const size_t srcRowBytes = (pageWidth + 7) / 8; // 60 bytes for 480 width
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user