Merge branch 'master' of https://github.com/jpirnay/crosspoint-reader into refactor-progress-theme
This commit is contained in:
+2
-2
@@ -847,8 +847,8 @@ rm -rf /path/to/sd/.crosspoint/epub_<hash>/sections/
|
|||||||
**Source**: `lib/Epub/Epub/Section.cpp`, `lib/Epub/Epub/BookMetadataCache.cpp`
|
**Source**: `lib/Epub/Epub/Section.cpp`, `lib/Epub/Epub/BookMetadataCache.cpp`
|
||||||
|
|
||||||
**Current Versions** (as of docs/file-formats.md):
|
**Current Versions** (as of docs/file-formats.md):
|
||||||
- `book.bin`: **Version 5** (metadata structure)
|
- `book.bin`: **Version 7** (header A includes cached `tocReliable` byte)
|
||||||
- `section.bin`: **Version 20** (layout structure, includes paragraph LUT)
|
- `section.bin`: **Version 23** (layout structure, includes paragraph LUT)
|
||||||
|
|
||||||
**Version Increment Rules**:
|
**Version Increment Rules**:
|
||||||
1. **ALWAYS increment version** BEFORE changing binary structure
|
1. **ALWAYS increment version** BEFORE changing binary structure
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ Spine items before the first TOC entry (cover pages) and after the last (appendi
|
|||||||
- `getTocItem(i)` returns the TOC entry (title, spineIndex, anchor) for TOC index `i` -- also a file seek per call, not cached in memory. Code that queries TOC metadata in a loop should cache the results locally first.
|
- `getTocItem(i)` returns the TOC entry (title, spineIndex, anchor) for TOC index `i` -- also a file seek per call, not cached in memory. Code that queries TOC metadata in a loop should cache the results locally first.
|
||||||
- `getSpineIndexForTocIndex(i)` does the reverse lookup (TOC index to spine index).
|
- `getSpineIndexForTocIndex(i)` does the reverse lookup (TOC index to spine index).
|
||||||
|
|
||||||
|
### Cached TOC reliability flag
|
||||||
|
|
||||||
|
`hasReliableToc()` answers whether the TOC has enough spine coverage (>=25% of spines referenced) to drive chapter UX, with short-circuits for `tocCount <= 0` and the "large book with one TOC entry" pathology.
|
||||||
|
|
||||||
|
The result is computed once during `buildBookBin` (folded into the existing `spineIndex->tocIndex` scan, so no extra disk pass) and persisted as a single byte in book.bin's header A. `Epub::hasReliableToc()` reads `BookMetadataCache::isTocReliable()` and caches the bool in `tocReliabilityState`.
|
||||||
|
|
||||||
|
This matters because the check used to recompute the answer on demand by calling `getTocEntry(i)` for every TOC entry, which does two SD-card seeks per call. On a 2858-entry web-novel TOC that was ~5700 seeks (~7 seconds) added to first-page latency. `BOOK_CACHE_VERSION` was bumped to 7 for this layout change; older caches are rebuilt on next open.
|
||||||
|
|
||||||
## Section cache file format
|
## Section cache file format
|
||||||
|
|
||||||
The section cache (`.bin`) stores pre-rendered page data for a spine item. The file layout:
|
The section cache (`.bin`) stores pre-rendered page data for a spine item. The file layout:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## `book.bin`
|
## `book.bin`
|
||||||
|
|
||||||
### Version 3
|
### Version 7
|
||||||
|
|
||||||
ImHex Pattern:
|
ImHex Pattern:
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ import std.string;
|
|||||||
import std.core;
|
import std.core;
|
||||||
|
|
||||||
// === Configuration ===
|
// === Configuration ===
|
||||||
#define EXPECTED_VERSION 3
|
#define EXPECTED_VERSION 7
|
||||||
#define MAX_STRING_LENGTH 65535
|
#define MAX_STRING_LENGTH 65535
|
||||||
|
|
||||||
// === String Structure ===
|
// === String Structure ===
|
||||||
@@ -34,8 +34,12 @@ fn format_string(String s) {
|
|||||||
struct Metadata {
|
struct Metadata {
|
||||||
String title [[comment("Book title")]];
|
String title [[comment("Book title")]];
|
||||||
String author [[comment("Book author")]];
|
String author [[comment("Book author")]];
|
||||||
|
String language [[comment("BCP47 language tag")]];
|
||||||
String coverItemHref [[comment("Path to cover image")]];
|
String coverItemHref [[comment("Path to cover image")]];
|
||||||
String textReferenceHref [[comment("Path to guided first text reference")]];
|
String textReferenceHref [[comment("Path to guided first text reference")]];
|
||||||
|
String series [[comment("Series name")]];
|
||||||
|
String seriesIndex [[comment("Series index/position")]];
|
||||||
|
String description [[comment("Book description / blurb")]];
|
||||||
} [[comment("Book metadata information")]];
|
} [[comment("Book metadata information")]];
|
||||||
|
|
||||||
// === Spine Entry Structure ===
|
// === Spine Entry Structure ===
|
||||||
@@ -70,7 +74,8 @@ struct BookBin {
|
|||||||
u32 lutOffset [[comment("Offset to lookup tables"), color("6BCB77")]];
|
u32 lutOffset [[comment("Offset to lookup tables"), color("6BCB77")]];
|
||||||
u16 spineCount [[comment("Number of spine entries"), color("4D96FF")]];
|
u16 spineCount [[comment("Number of spine entries"), color("4D96FF")]];
|
||||||
u16 tocCount [[comment("Number of TOC entries"), color("FF6B9D")]];
|
u16 tocCount [[comment("Number of TOC entries"), color("FF6B9D")]];
|
||||||
|
u8 tocReliable [[comment("1 if TOC has >=25% spine coverage, 0 otherwise"), color("F4A261")]];
|
||||||
|
|
||||||
// Metadata section
|
// Metadata section
|
||||||
Metadata metadata [[comment("Book metadata")]];
|
Metadata metadata [[comment("Book metadata")]];
|
||||||
|
|
||||||
|
|||||||
+4
-29
@@ -868,35 +868,10 @@ bool Epub::hasReliableToc() const {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int spineCount = bookMetadataCache->getSpineCount();
|
// Reliability is computed once at indexing time and persisted in book.bin's header.
|
||||||
const int tocCount = bookMetadataCache->getTocCount();
|
// This avoids the O(tocCount) seek-heavy scan that previously fired on first page load
|
||||||
|
// for every book — a large web-novel TOC (~3000 entries) added several seconds of latency.
|
||||||
if (spineCount <= 0 || tocCount <= 0) {
|
const bool reliable = bookMetadataCache->isTocReliable();
|
||||||
tocReliabilityState = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a larger book only exposes one TOC entry, treat TOC as unusable for chapter UX.
|
|
||||||
if (spineCount >= 8 && tocCount <= 1) {
|
|
||||||
tocReliabilityState = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<bool> spineReferenced(static_cast<size_t>(spineCount), false);
|
|
||||||
int distinctSpinesReferenced = 0;
|
|
||||||
for (int i = 0; i < tocCount; i++) {
|
|
||||||
const auto toc = bookMetadataCache->getTocEntry(i);
|
|
||||||
if (toc.spineIndex >= 0 && toc.spineIndex < spineCount) {
|
|
||||||
const size_t idx = static_cast<size_t>(toc.spineIndex);
|
|
||||||
if (!spineReferenced[idx]) {
|
|
||||||
spineReferenced[idx] = true;
|
|
||||||
distinctSpinesReferenced++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Require at least 25% spine coverage from TOC references.
|
|
||||||
const bool reliable = (distinctSpinesReferenced * 4 >= spineCount);
|
|
||||||
tocReliabilityState = reliable ? 1 : 0;
|
tocReliabilityState = reliable ? 1 : 0;
|
||||||
return reliable;
|
return reliable;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
#include "FsHelpers.h"
|
#include "FsHelpers.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t BOOK_CACHE_VERSION = 6;
|
constexpr uint8_t BOOK_CACHE_VERSION = 7;
|
||||||
constexpr char bookBinFile[] = "/book.bin";
|
constexpr char bookBinFile[] = "/book.bin";
|
||||||
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
||||||
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
|
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
|
||||||
@@ -113,8 +113,8 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr uint32_t headerASize =
|
constexpr uint32_t headerASize = sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) +
|
||||||
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount);
|
sizeof(tocCount) + sizeof(uint8_t) /* tocReliable */;
|
||||||
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() +
|
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() +
|
||||||
metadata.coverItemHref.size() + metadata.textReferenceHref.size() +
|
metadata.coverItemHref.size() + metadata.textReferenceHref.size() +
|
||||||
metadata.series.size() + metadata.seriesIndex.size() + metadata.description.size() +
|
metadata.series.size() + metadata.seriesIndex.size() + metadata.description.size() +
|
||||||
@@ -122,11 +122,14 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
const uint32_t lutSize = sizeof(uint32_t) * spineCount + sizeof(uint32_t) * tocCount;
|
const uint32_t lutSize = sizeof(uint32_t) * spineCount + sizeof(uint32_t) * tocCount;
|
||||||
const uint32_t lutOffset = headerASize + metadataSize;
|
const uint32_t lutOffset = headerASize + metadataSize;
|
||||||
|
|
||||||
// Header A
|
// Header A. tocReliable is patched at the end once the TOC scan below has computed it.
|
||||||
|
const uint32_t tocReliableHeaderPos =
|
||||||
|
sizeof(BOOK_CACHE_VERSION) + sizeof(uint32_t) /* lutOffset */ + sizeof(spineCount) + sizeof(tocCount);
|
||||||
serialization::writePod(bookFile, BOOK_CACHE_VERSION);
|
serialization::writePod(bookFile, BOOK_CACHE_VERSION);
|
||||||
serialization::writePod(bookFile, lutOffset);
|
serialization::writePod(bookFile, lutOffset);
|
||||||
serialization::writePod(bookFile, spineCount);
|
serialization::writePod(bookFile, spineCount);
|
||||||
serialization::writePod(bookFile, tocCount);
|
serialization::writePod(bookFile, tocCount);
|
||||||
|
serialization::writePod(bookFile, static_cast<uint8_t>(0)); // placeholder for tocReliable
|
||||||
// Metadata
|
// Metadata
|
||||||
serialization::writeString(bookFile, metadata.title);
|
serialization::writeString(bookFile, metadata.title);
|
||||||
serialization::writeString(bookFile, metadata.author);
|
serialization::writeString(bookFile, metadata.author);
|
||||||
@@ -156,18 +159,31 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
// LUTs complete
|
// LUTs complete
|
||||||
// Loop through spines from spine file matching up TOC indexes, calculating cumulative size and writing to book.bin
|
// Loop through spines from spine file matching up TOC indexes, calculating cumulative size and writing to book.bin
|
||||||
|
|
||||||
// Build spineIndex->tocIndex mapping in one pass (O(n) instead of O(n*m))
|
// Build spineIndex->tocIndex mapping in one pass (O(n) instead of O(n*m)).
|
||||||
|
// Also count distinct spines referenced by the TOC so tocReliable can be persisted in the
|
||||||
|
// header below — without this, every first-page load on a large book pays an O(tocCount)
|
||||||
|
// seek-heavy scan in Epub::hasReliableToc().
|
||||||
std::deque<int16_t> spineToTocIndex(spineCount, -1);
|
std::deque<int16_t> spineToTocIndex(spineCount, -1);
|
||||||
|
int distinctSpinesReferenced = 0;
|
||||||
tocFile.seek(0);
|
tocFile.seek(0);
|
||||||
for (int j = 0; j < tocCount; j++) {
|
for (int j = 0; j < tocCount; j++) {
|
||||||
auto tocEntry = readTocEntry(tocFile);
|
auto tocEntry = readTocEntry(tocFile);
|
||||||
if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) {
|
if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) {
|
||||||
if (spineToTocIndex[tocEntry.spineIndex] == -1) {
|
if (spineToTocIndex[tocEntry.spineIndex] == -1) {
|
||||||
spineToTocIndex[tocEntry.spineIndex] = static_cast<int16_t>(j);
|
spineToTocIndex[tocEntry.spineIndex] = static_cast<int16_t>(j);
|
||||||
|
distinctSpinesReferenced++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mirrors the heuristic in Epub::hasReliableToc(): require >=25% distinct spine coverage,
|
||||||
|
// with short-circuits for edge cases (no entries, or large book with a single TOC entry).
|
||||||
|
if (spineCount > 0 && tocCount > 0 && !(spineCount >= 8 && tocCount <= 1)) {
|
||||||
|
tocReliable = (distinctSpinesReferenced * 4 >= spineCount);
|
||||||
|
} else {
|
||||||
|
tocReliable = false;
|
||||||
|
}
|
||||||
|
|
||||||
ZipFile zip(epubPath);
|
ZipFile zip(epubPath);
|
||||||
// Pre-open zip file to speed up size calculations
|
// Pre-open zip file to speed up size calculations
|
||||||
if (!zip.open()) {
|
if (!zip.open()) {
|
||||||
@@ -269,6 +285,10 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
writeTocEntry(bookFile, tocEntry);
|
writeTocEntry(bookFile, tocEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Patch tocReliable placeholder in header A
|
||||||
|
bookFile.seek(tocReliableHeaderPos);
|
||||||
|
serialization::writePod(bookFile, static_cast<uint8_t>(tocReliable ? 1 : 0));
|
||||||
|
|
||||||
bookFile.close();
|
bookFile.close();
|
||||||
spineFile.close();
|
spineFile.close();
|
||||||
tocFile.close();
|
tocFile.close();
|
||||||
@@ -384,6 +404,9 @@ bool BookMetadataCache::load() {
|
|||||||
serialization::readPod(bookFile, lutOffset);
|
serialization::readPod(bookFile, lutOffset);
|
||||||
serialization::readPod(bookFile, spineCount);
|
serialization::readPod(bookFile, spineCount);
|
||||||
serialization::readPod(bookFile, tocCount);
|
serialization::readPod(bookFile, tocCount);
|
||||||
|
uint8_t tocReliableByte;
|
||||||
|
serialization::readPod(bookFile, tocReliableByte);
|
||||||
|
tocReliable = (tocReliableByte != 0);
|
||||||
|
|
||||||
serialization::readString(bookFile, coreMetadata.title);
|
serialization::readString(bookFile, coreMetadata.title);
|
||||||
serialization::readString(bookFile, coreMetadata.author);
|
serialization::readString(bookFile, coreMetadata.author);
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class BookMetadataCache {
|
|||||||
size_t lutOffset;
|
size_t lutOffset;
|
||||||
uint16_t spineCount;
|
uint16_t spineCount;
|
||||||
uint16_t tocCount;
|
uint16_t tocCount;
|
||||||
|
bool tocReliable;
|
||||||
bool loaded;
|
bool loaded;
|
||||||
bool buildMode;
|
bool buildMode;
|
||||||
|
|
||||||
@@ -88,7 +89,13 @@ class BookMetadataCache {
|
|||||||
BookMetadata coreMetadata;
|
BookMetadata coreMetadata;
|
||||||
|
|
||||||
explicit BookMetadataCache(std::string cachePath)
|
explicit BookMetadataCache(std::string cachePath)
|
||||||
: cachePath(std::move(cachePath)), lutOffset(0), spineCount(0), tocCount(0), loaded(false), buildMode(false) {}
|
: cachePath(std::move(cachePath)),
|
||||||
|
lutOffset(0),
|
||||||
|
spineCount(0),
|
||||||
|
tocCount(0),
|
||||||
|
tocReliable(false),
|
||||||
|
loaded(false),
|
||||||
|
buildMode(false) {}
|
||||||
~BookMetadataCache() = default;
|
~BookMetadataCache() = default;
|
||||||
|
|
||||||
// Building phase (stream to disk immediately)
|
// Building phase (stream to disk immediately)
|
||||||
@@ -111,5 +118,6 @@ class BookMetadataCache {
|
|||||||
TocEntry getTocEntry(int index);
|
TocEntry getTocEntry(int index);
|
||||||
int getSpineCount() const { return spineCount; }
|
int getSpineCount() const { return spineCount; }
|
||||||
int getTocCount() const { return tocCount; }
|
int getTocCount() const { return tocCount; }
|
||||||
|
bool isTocReliable() const { return tocReliable; }
|
||||||
bool isLoaded() const { return loaded; }
|
bool isLoaded() const { return loaded; }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,13 +14,14 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "ReaderUtils.h"
|
#include "ReaderUtils.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "XtcReaderChapterSelectionActivity.h"
|
#include "XtcReaderChapterSelectionActivity.h"
|
||||||
#include "components/UITheme.h"
|
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -477,12 +478,12 @@ void XtcReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION a
|
|||||||
case BA::BTN_PREV_SECTION:
|
case BA::BTN_PREV_SECTION:
|
||||||
if (xtc->hasChapters()) {
|
if (xtc->hasChapters()) {
|
||||||
const auto& chapters = xtc->getChapters();
|
const auto& chapters = xtc->getChapters();
|
||||||
for (int i = static_cast<int>(chapters.size()) - 1; i >= 0; i--) {
|
const auto prevChapter = std::find_if(chapters.rbegin(), chapters.rend(),
|
||||||
if (chapters[i].startPage < currentPage) {
|
[this](const auto& ch) { return ch.startPage < currentPage; });
|
||||||
currentPage = chapters[i].startPage;
|
|
||||||
requestUpdate();
|
if (prevChapter != chapters.rend()) {
|
||||||
break;
|
currentPage = prevChapter->startPage;
|
||||||
}
|
requestUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in New Issue
Block a user