feat: render placeholders while waiting for images to render (#1003)

This commit is contained in:
Matthías Páll Gissurarson
2026-07-12 21:40:07 +03:00
committed by GitHub
parent 444d87de82
commit 39ea4b045f
5 changed files with 125 additions and 10 deletions
+15
View File
@@ -57,6 +57,10 @@ void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffse
imageBlock->render(renderer, xPos + xOffset, yPos + yOffset); imageBlock->render(renderer, xPos + xOffset, yPos + yOffset);
} }
void PageImage::renderPlaceholder(GfxRenderer& renderer, const int xOffset, const int yOffset) const {
imageBlock->renderPlaceholder(renderer, xPos + xOffset, yPos + yOffset);
}
bool PageImage::serialize(HalFile& file) { bool PageImage::serialize(HalFile& file) {
serialization::writePod(file, xPos); serialization::writePod(file, xPos);
serialization::writePod(file, yPos); serialization::writePod(file, yPos);
@@ -125,6 +129,17 @@ void Page::renderImages(GfxRenderer& renderer, const int fontId, const int xOffs
[](const PageElement& element) { return element.getTag() == TAG_PageImage; }); [](const PageElement& element) { return element.getTag() == TAG_PageImage; });
} }
void Page::renderWithImagePlaceholders(GfxRenderer& renderer, const int fontId, const int xOffset,
const int yOffset) const {
for (const auto& element : elements) {
if (element->getTag() == TAG_PageImage) {
static_cast<const PageImage&>(*element).renderPlaceholder(renderer, xOffset, yOffset);
} else {
element->render(renderer, fontId, xOffset, yOffset);
}
}
}
bool Page::serialize(HalFile& file) const { bool Page::serialize(HalFile& file) const {
const uint16_t count = elements.size(); const uint16_t count = elements.size();
serialization::writePod(file, count); serialization::writePod(file, count);
+9
View File
@@ -50,6 +50,7 @@ class PageImage final : public PageElement {
PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos) PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos)
: PageElement(xPos, yPos), imageBlock(std::move(block)) {} : PageElement(xPos, yPos), imageBlock(std::move(block)) {}
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override; void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
void renderPlaceholder(GfxRenderer& renderer, int xOffset, int yOffset) const;
bool serialize(HalFile& file) override; bool serialize(HalFile& file) override;
PageElementTag getTag() const override { return TAG_PageImage; } PageElementTag getTag() const override { return TAG_PageImage; }
static std::unique_ptr<PageImage> deserialize(HalFile& file); static std::unique_ptr<PageImage> deserialize(HalFile& file);
@@ -89,6 +90,7 @@ class Page {
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const; void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
void renderImages(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const; void renderImages(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
void renderWithImagePlaceholders(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
bool serialize(HalFile& file) const; bool serialize(HalFile& file) const;
static std::unique_ptr<Page> deserialize(HalFile& file); static std::unique_ptr<Page> deserialize(HalFile& file);
@@ -98,6 +100,13 @@ class Page {
[](const std::shared_ptr<PageElement>& el) { return el->getTag() == TAG_PageImage; }); [](const std::shared_ptr<PageElement>& el) { return el->getTag() == TAG_PageImage; });
} }
bool hasImagesNeedingDecode() const {
return std::any_of(elements.begin(), elements.end(), [](const std::shared_ptr<PageElement>& element) {
return element->getTag() == TAG_PageImage &&
static_cast<const PageImage&>(*element).getImageBlock().needsDecode();
});
}
// Get bounding box of all images on the page (union of image rects) // Get bounding box of all images on the page (union of image rects)
// Returns false if no images. Coordinates are relative to page origin. // Returns false if no images. Coordinates are relative to page origin.
bool getImageBoundingBox(int16_t& outX, int16_t& outY, int16_t& outW, int16_t& outH) const { bool getImageBoundingBox(int16_t& outX, int16_t& outY, int16_t& outW, int16_t& outH) const {
+87 -10
View File
@@ -5,6 +5,8 @@
#include <Logging.h> #include <Logging.h>
#include <Serialization.h> #include <Serialization.h>
#include <cstdlib>
#include "Epub/converters/DirectPixelWriter.h" #include "Epub/converters/DirectPixelWriter.h"
#include "Epub/converters/ImageDecoderFactory.h" #include "Epub/converters/ImageDecoderFactory.h"
@@ -29,6 +31,54 @@ std::string getCachePath(const std::string& imagePath) {
return imagePath + ".pxc"; return imagePath + ".pxc";
} }
bool readValidCacheHeader(HalFile& cacheFile, const int expectedWidth, const int expectedHeight, uint16_t& cachedWidth,
uint16_t& cachedHeight) {
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) {
return false;
}
const int widthDiff = abs(cachedWidth - expectedWidth);
const int heightDiff = abs(cachedHeight - expectedHeight);
if (widthDiff > 1 || heightDiff > 1) {
return false;
}
const size_t bytesPerRow = (cachedWidth + 3) / 4;
const size_t expectedSize = 4 + bytesPerRow * cachedHeight;
return cacheFile.size() >= expectedSize;
}
// Pages are deserialized afresh on each visit. Keep a bounded, allocation-free
// record so an image that failed renders its placeholder directly for the rest
// of the reader session instead of paying another placeholder refresh and
// decode. The reader clears this on entry so transient memory/storage failures
// are retried.
constexpr size_t MAX_SESSION_IMAGE_FAILURES = 16;
uint64_t failedImageHashes[MAX_SESSION_IMAGE_FAILURES];
size_t failedImageCount = 0;
uint64_t imagePathHash(const std::string& path) {
uint64_t hash = 14695981039346656037ull;
for (const char c : path) {
hash ^= static_cast<uint8_t>(c);
hash *= 1099511628211ull;
}
return hash;
}
bool imageFailedThisSession(const std::string& path) {
const uint64_t hash = imagePathHash(path);
for (size_t i = 0; i < failedImageCount; i++) {
if (failedImageHashes[i] == hash) return true;
}
return false;
}
void rememberImageFailure(const std::string& path) {
if (failedImageCount == MAX_SESSION_IMAGE_FAILURES || imageFailedThisSession(path)) return;
failedImageHashes[failedImageCount++] = imagePathHash(path);
}
bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth, bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth,
int expectedHeight) { int expectedHeight) {
HalFile cacheFile; HalFile cacheFile;
@@ -37,16 +87,8 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
} }
uint16_t cachedWidth, cachedHeight; uint16_t cachedWidth, cachedHeight;
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) { if (!readValidCacheHeader(cacheFile, expectedWidth, expectedHeight, cachedWidth, cachedHeight)) {
return false; LOG_ERR("IMG", "Invalid image cache: %s", cachePath.c_str());
}
// Verify dimensions are close (allow 1 pixel tolerance for rounding differences)
int widthDiff = abs(cachedWidth - expectedWidth);
int heightDiff = abs(cachedHeight - expectedHeight);
if (widthDiff > 1 || heightDiff > 1) {
LOG_ERR("IMG", "Cache dimension mismatch: %dx%d vs %dx%d", cachedWidth, cachedHeight, expectedWidth,
expectedHeight);
return false; return false;
} }
@@ -119,6 +161,28 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
} // namespace } // namespace
bool ImageBlock::hasValidCache() const {
const auto cachePath = getCachePath(imagePath);
HalFile cacheFile;
if (!Storage.openFileForRead("IMG", cachePath, cacheFile)) {
return false;
}
uint16_t cachedWidth, cachedHeight;
return readValidCacheHeader(cacheFile, width, height, cachedWidth, cachedHeight);
}
bool ImageBlock::needsDecode() const { return !imageFailedThisSession(imagePath) && !hasValidCache(); }
void ImageBlock::clearSessionRenderFailures() { failedImageCount = 0; }
void ImageBlock::renderPlaceholder(GfxRenderer& renderer, const int x, const int y) const {
renderer.fillRect(x, y, width, height, true);
if (width > 2 && height > 2) {
renderer.fillRect(x + 1, y + 1, width - 2, height - 2, false);
}
}
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) { void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
// The font-prewarm scan pass only accumulates glyphs; an image contributes // The font-prewarm scan pass only accumulates glyphs; an image contributes
// none, and its DirectPixelWriter output bypasses the renderer's scan-mode // none, and its DirectPixelWriter output bypasses the renderer's scan-mode
@@ -150,6 +214,11 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
return; return;
} }
if (imageFailedThisSession(imagePath)) {
renderPlaceholder(renderer, x, y);
return;
}
// Try to render from cache first // Try to render from cache first
std::string cachePath = getCachePath(imagePath); std::string cachePath = getCachePath(imagePath);
if (renderFromCache(renderer, cachePath, x, y, width, height)) { if (renderFromCache(renderer, cachePath, x, y, width, height)) {
@@ -161,6 +230,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
HalFile file; HalFile file;
if (!Storage.openFileForRead("IMG", imagePath, file)) { if (!Storage.openFileForRead("IMG", imagePath, file)) {
LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str()); LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str());
rememberImageFailure(imagePath);
renderPlaceholder(renderer, x, y);
return; return;
} }
size_t fileSize = file.size(); size_t fileSize = file.size();
@@ -168,6 +239,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
if (fileSize == 0) { if (fileSize == 0) {
LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str()); LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str());
rememberImageFailure(imagePath);
renderPlaceholder(renderer, x, y);
return; return;
} }
@@ -187,6 +260,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath); ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
if (!decoder) { if (!decoder) {
LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str()); LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str());
rememberImageFailure(imagePath);
renderPlaceholder(renderer, x, y);
return; return;
} }
@@ -195,6 +270,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config); bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
if (!success) { if (!success) {
LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str()); LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str());
rememberImageFailure(imagePath);
renderPlaceholder(renderer, x, y);
return; return;
} }
+4
View File
@@ -16,6 +16,10 @@ class ImageBlock final : public Block {
int16_t getHeight() const { return height; } int16_t getHeight() const { return height; }
bool imageExists() const; bool imageExists() const;
bool hasValidCache() const;
bool needsDecode() const;
void renderPlaceholder(GfxRenderer& renderer, int x, int y) const;
static void clearSessionRenderFailures();
BlockType getType() override { return IMAGE_BLOCK; } BlockType getType() override { return IMAGE_BLOCK; }
bool isEmpty() override { return false; } bool isEmpty() override { return false; }
@@ -155,6 +155,8 @@ void EpubReaderActivity::onEnter() {
return; return;
} }
ImageBlock::clearSessionRenderFailures();
// Configure screen orientation based on settings // Configure screen orientation based on settings
// NOTE: This affects layout math and must be applied before any render calls. // NOTE: This affects layout math and must be applied before any render calls.
ReaderUtils::applyOrientation(renderer, SETTINGS.orientation); ReaderUtils::applyOrientation(renderer, SETTINGS.orientation);
@@ -1346,6 +1348,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
const auto tPrewarm = millis(); const auto tPrewarm = millis();
const bool pageHasImages = page->hasImages(); const bool pageHasImages = page->hasImages();
const bool pageHasImagesNeedingDecode = pageHasImages && page->hasImagesNeedingDecode();
const bool needsTextGrayscale = SETTINGS.textAntiAliasing; const bool needsTextGrayscale = SETTINGS.textAntiAliasing;
const bool needsAnyGrayscale = needsTextGrayscale || pageHasImages; const bool needsAnyGrayscale = needsTextGrayscale || pageHasImages;
auto renderGrayscalePass = [&]() { auto renderGrayscalePass = [&]() {
@@ -1356,6 +1359,13 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
} }
}; };
if (pageHasImagesNeedingDecode) {
page->renderWithImagePlaceholders(renderer, fontId, orientedMarginLeft, orientedMarginTop);
renderStatusBar();
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
renderer.clearScreen();
}
page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop); page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop);
renderStatusBar(); renderStatusBar();
const auto tBwRender = millis(); const auto tBwRender = millis();