diff --git a/.skills/SKILL.md b/.skills/SKILL.md index 1e687b3e..3d34eaca 100644 --- a/.skills/SKILL.md +++ b/.skills/SKILL.md @@ -847,8 +847,8 @@ rm -rf /path/to/sd/.crosspoint/epub_/sections/ **Source**: `lib/Epub/Epub/Section.cpp`, `lib/Epub/Epub/BookMetadataCache.cpp` **Current Versions** (as of docs/file-formats.md): -- `book.bin`: **Version 5** (metadata structure) -- `section.bin`: **Version 20** (layout structure, includes paragraph LUT) +- `book.bin`: **Version 7** (header A includes cached `tocReliable` byte) +- `section.bin`: **Version 23** (layout structure, includes paragraph LUT) **Version Increment Rules**: 1. **ALWAYS increment version** BEFORE changing binary structure diff --git a/docs/epub-toc-navigation.md b/docs/epub-toc-navigation.md index 2e623c65..6f40af82 100644 --- a/docs/epub-toc-navigation.md +++ b/docs/epub-toc-navigation.md @@ -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. - `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 The section cache (`.bin`) stores pre-rendered page data for a spine item. The file layout: diff --git a/docs/file-formats.md b/docs/file-formats.md index 61c4c8bd..669ae078 100644 --- a/docs/file-formats.md +++ b/docs/file-formats.md @@ -2,7 +2,7 @@ ## `book.bin` -### Version 3 +### Version 7 ImHex Pattern: @@ -12,7 +12,7 @@ import std.string; import std.core; // === Configuration === -#define EXPECTED_VERSION 3 +#define EXPECTED_VERSION 7 #define MAX_STRING_LENGTH 65535 // === String Structure === @@ -34,8 +34,12 @@ fn format_string(String s) { struct Metadata { String title [[comment("Book title")]]; String author [[comment("Book author")]]; + String language [[comment("BCP47 language tag")]]; String coverItemHref [[comment("Path to cover image")]]; 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")]]; // === Spine Entry Structure === @@ -70,7 +74,8 @@ struct BookBin { u32 lutOffset [[comment("Offset to lookup tables"), color("6BCB77")]]; u16 spineCount [[comment("Number of spine entries"), color("4D96FF")]]; 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 metadata [[comment("Book metadata")]]; diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index 0f52d8f8..de26c410 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -868,35 +868,10 @@ bool Epub::hasReliableToc() const { return false; } - const int spineCount = bookMetadataCache->getSpineCount(); - const int tocCount = bookMetadataCache->getTocCount(); - - if (spineCount <= 0 || tocCount <= 0) { - 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 spineReferenced(static_cast(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(toc.spineIndex); - if (!spineReferenced[idx]) { - spineReferenced[idx] = true; - distinctSpinesReferenced++; - } - } - } - - // Require at least 25% spine coverage from TOC references. - const bool reliable = (distinctSpinesReferenced * 4 >= spineCount); + // Reliability is computed once at indexing time and persisted in book.bin's header. + // 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. + const bool reliable = bookMetadataCache->isTocReliable(); tocReliabilityState = reliable ? 1 : 0; return reliable; } diff --git a/lib/Epub/Epub/BookMetadataCache.cpp b/lib/Epub/Epub/BookMetadataCache.cpp index 02b25e2d..593ca70d 100644 --- a/lib/Epub/Epub/BookMetadataCache.cpp +++ b/lib/Epub/Epub/BookMetadataCache.cpp @@ -9,7 +9,7 @@ #include "FsHelpers.h" namespace { -constexpr uint8_t BOOK_CACHE_VERSION = 6; +constexpr uint8_t BOOK_CACHE_VERSION = 7; constexpr char bookBinFile[] = "/book.bin"; constexpr char tmpSpineBinFile[] = "/spine.bin.tmp"; constexpr char tmpTocBinFile[] = "/toc.bin.tmp"; @@ -113,8 +113,8 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta return false; } - constexpr uint32_t headerASize = - sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount); + constexpr uint32_t headerASize = sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + + sizeof(tocCount) + sizeof(uint8_t) /* tocReliable */; const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() + metadata.coverItemHref.size() + metadata.textReferenceHref.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 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, lutOffset); serialization::writePod(bookFile, spineCount); serialization::writePod(bookFile, tocCount); + serialization::writePod(bookFile, static_cast(0)); // placeholder for tocReliable // Metadata serialization::writeString(bookFile, metadata.title); serialization::writeString(bookFile, metadata.author); @@ -156,18 +159,31 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta // LUTs complete // 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 spineToTocIndex(spineCount, -1); + int distinctSpinesReferenced = 0; tocFile.seek(0); for (int j = 0; j < tocCount; j++) { auto tocEntry = readTocEntry(tocFile); if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) { if (spineToTocIndex[tocEntry.spineIndex] == -1) { spineToTocIndex[tocEntry.spineIndex] = static_cast(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); // Pre-open zip file to speed up size calculations if (!zip.open()) { @@ -269,6 +285,10 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta writeTocEntry(bookFile, tocEntry); } + // Patch tocReliable placeholder in header A + bookFile.seek(tocReliableHeaderPos); + serialization::writePod(bookFile, static_cast(tocReliable ? 1 : 0)); + bookFile.close(); spineFile.close(); tocFile.close(); @@ -384,6 +404,9 @@ bool BookMetadataCache::load() { serialization::readPod(bookFile, lutOffset); serialization::readPod(bookFile, spineCount); serialization::readPod(bookFile, tocCount); + uint8_t tocReliableByte; + serialization::readPod(bookFile, tocReliableByte); + tocReliable = (tocReliableByte != 0); serialization::readString(bookFile, coreMetadata.title); serialization::readString(bookFile, coreMetadata.author); diff --git a/lib/Epub/Epub/BookMetadataCache.h b/lib/Epub/Epub/BookMetadataCache.h index feb39a23..fa54d9b0 100644 --- a/lib/Epub/Epub/BookMetadataCache.h +++ b/lib/Epub/Epub/BookMetadataCache.h @@ -50,6 +50,7 @@ class BookMetadataCache { size_t lutOffset; uint16_t spineCount; uint16_t tocCount; + bool tocReliable; bool loaded; bool buildMode; @@ -88,7 +89,13 @@ class BookMetadataCache { BookMetadata coreMetadata; 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; // Building phase (stream to disk immediately) @@ -111,5 +118,6 @@ class BookMetadataCache { TocEntry getTocEntry(int index); int getSpineCount() const { return spineCount; } int getTocCount() const { return tocCount; } + bool isTocReliable() const { return tocReliable; } bool isLoaded() const { return loaded; } };