feat: render placeholders while waiting for images to render (#1003)
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user