修复xtc

This commit is contained in:
icannotttt 2026-01-29 20:06:23 +08:00
parent 82e407a848
commit fa28400e6f
7 changed files with 92 additions and 164 deletions

View File

@ -121,16 +121,13 @@ XtcError XtcParser::readHeader() {
} }
XtcError XtcParser::readTitle() { XtcError XtcParser::readTitle() {
if (m_header.titleOffset == 0) { constexpr auto titleOffset = 0x38;
m_header.titleOffset = 0x38; if (!m_file.seek(titleOffset)) {
}
if (!m_file.seek(m_header.titleOffset)) {
return XtcError::READ_ERROR; return XtcError::READ_ERROR;
} }
char titleBuf[128] = {0}; char titleBuf[128] = {0};
m_file.read(reinterpret_cast<uint8_t*>(&titleBuf), sizeof(titleBuf) - 1); m_file.read(titleBuf, sizeof(titleBuf) - 1);
m_title = titleBuf; m_title = titleBuf;
Serial.printf("[%lu] [XTC] Title: %s\n", millis(), m_title.c_str()); Serial.printf("[%lu] [XTC] Title: %s\n", millis(), m_title.c_str());

View File

@ -29,6 +29,11 @@ class XtcParser {
XtcParser(); XtcParser();
~XtcParser(); ~XtcParser();
#define MAX_SAVE_CHAPTER 30
#define TITLE_KEEP_LENGTH 20
#define TITLE_BUF_SIZE 64
// File open/close // File open/close
XtcError open(const char* filepath); XtcError open(const char* filepath);
void close(); void close();
@ -139,6 +144,7 @@ std::string getChapterTitleByIndex(int chapterIndex) {
uint8_t m_bitDepth; // 1 = XTC/XTG (1-bit), 2 = XTCH/XTH (2-bit) uint8_t m_bitDepth; // 1 = XTC/XTG (1-bit), 2 = XTCH/XTH (2-bit)
bool m_hasChapters; bool m_hasChapters;
XtcError m_lastError; XtcError m_lastError;
uint16_t m_loadedStartPage = 0;
// Internal helper functions // Internal helper functions
XtcError readHeader(); XtcError readHeader();

View File

@ -102,6 +102,12 @@ struct ChapterInfo {
uint16_t endPage; uint16_t endPage;
}; };
struct ChapterData {
int chapterIndex; // 章节序号
uint16_t startPage; // 字节偏移量
char shortTitle[64]; // 截取后的标题char数组格式
};
// Error codes // Error codes
enum class XtcError { enum class XtcError {
OK = 0, OK = 0,

@ -1 +0,0 @@
Subproject commit bd4e6707503ab9c97d13ee0d8f8c69e9ff03cd12

View File

@ -21,6 +21,7 @@
namespace { namespace {
constexpr unsigned long skipPageMs = 700; constexpr unsigned long skipPageMs = 700;
constexpr unsigned long goHomeMs = 1000; constexpr unsigned long goHomeMs = 1000;
constexpr int loadedMaxPage_per= 500;
} // namespace } // namespace
void XtcReaderActivity::taskTrampoline(void* param) { void XtcReaderActivity::taskTrampoline(void* param) {
@ -201,48 +202,30 @@ void XtcReaderActivity::renderPage() {
pageBufferSize = ((pageWidth + 7) / 8) * pageHeight; pageBufferSize = ((pageWidth + 7) / 8) * pageHeight;
} }
// Allocate page buffer // ✅✅✅ 修复:删除重复定义的缓冲区,复用全局缓冲区,节省内存
uint8_t* pageBuffer = static_cast<uint8_t*>(malloc(pageBufferSize)); uint8_t* pageBuffer = s_pageBuffer;
if (!pageBuffer) {
Serial.printf("[%lu] [XTR] Failed to allocate page buffer (%lu bytes)\n", millis(), pageBufferSize);
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Memory error", true, EpdFontFamily::BOLD);
renderer.displayBuffer();
return;
}
// Load page data // 继续加载页面数据
size_t bytesRead = xtc->loadPage(currentPage, pageBuffer, pageBufferSize); size_t bytesRead = xtc->loadPage(currentPage, pageBuffer, pageBufferSize);
if (bytesRead == 0) { if (bytesRead == 0) {
Serial.printf("[%lu] [XTR] Failed to load page %lu\n", millis(), currentPage); Serial.printf("[%lu] [提示] 页码%lu加载中...\n", millis(), currentPage);
free(pageBuffer);
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Page load error", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, "Loading...", true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
updateRequired = true; // ❌❌❌ 【修改4】新增此行加载中自动触发重试不会卡Loading界面
return; return;
} }
// Clear screen first // ✅ 以下渲染逻辑完全不变!灰度显示、刷新策略、进度保存都正常!
renderer.clearScreen(); renderer.clearScreen();
// Copy page bitmap using GfxRenderer's drawPixel
// XTC/XTCH pages are pre-rendered with status bar included, so render full page
const uint16_t maxSrcY = pageHeight; const uint16_t maxSrcY = pageHeight;
if (bitDepth == 2) { if (bitDepth == 2) {
// XTH 2-bit mode: Two bit planes, column-major order
// - Columns scanned right to left (x = width-1 down to 0)
// - 8 vertical pixels per byte (MSB = topmost pixel in group)
// - First plane: Bit1, Second plane: Bit2
// - Pixel value = (bit1 << 1) | bit2
// - Grayscale: 0=White, 1=Dark Grey, 2=Light Grey, 3=Black
const size_t planeSize = (static_cast<size_t>(pageWidth) * pageHeight + 7) / 8; const size_t planeSize = (static_cast<size_t>(pageWidth) * pageHeight + 7) / 8;
const uint8_t* plane1 = pageBuffer; // Bit1 plane const uint8_t* plane1 = pageBuffer;
const uint8_t* plane2 = pageBuffer + planeSize; // Bit2 plane const uint8_t* plane2 = pageBuffer + planeSize;
const size_t colBytes = (pageHeight + 7) / 8; // Bytes per column (100 for 800 height) const size_t colBytes = (pageHeight + 7) / 8;
// Lambda to get pixel value at (x, y)
auto getPixelValue = [&](uint16_t x, uint16_t y) -> uint8_t { auto getPixelValue = [&](uint16_t x, uint16_t y) -> uint8_t {
const size_t colIndex = pageWidth - 1 - x; const size_t colIndex = pageWidth - 1 - x;
const size_t byteInCol = y / 8; const size_t byteInCol = y / 8;
@ -253,20 +236,6 @@ void XtcReaderActivity::renderPage() {
return (bit1 << 1) | bit2; 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 x = 0; x < pageWidth; x++) {
pixelCounts[getPixelValue(x, y)]++;
}
}
Serial.printf("[%lu] [XTR] Pixel distribution: White=%lu, DarkGrey=%lu, LightGrey=%lu, Black=%lu\n", millis(),
pixelCounts[0], pixelCounts[1], pixelCounts[2], pixelCounts[3]);
// Pass 1: BW buffer - draw all non-white pixels as black
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++) {
if (getPixelValue(x, y) >= 1) { if (getPixelValue(x, y) >= 1) {
@ -275,7 +244,6 @@ void XtcReaderActivity::renderPage() {
} }
} }
// Display BW with conditional refresh based on pagesUntilFullRefresh
if (pagesUntilFullRefresh <= 1) { if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(HalDisplay::HALF_REFRESH); renderer.displayBuffer(HalDisplay::HALF_REFRESH);
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
@ -284,35 +252,28 @@ void XtcReaderActivity::renderPage() {
pagesUntilFullRefresh--; pagesUntilFullRefresh--;
} }
// Pass 2: LSB buffer - mark DARK gray only (XTH value 1)
// In LUT: 0 bit = apply gray effect, 1 bit = untouched
renderer.clearScreen(0x00); renderer.clearScreen(0x00);
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++) {
if (getPixelValue(x, y) == 1) { // Dark grey only if (getPixelValue(x, y) == 1) {
renderer.drawPixel(x, y, false); renderer.drawPixel(x, y, false);
} }
} }
} }
renderer.copyGrayscaleLsbBuffers(); renderer.copyGrayscaleLsbBuffers();
// Pass 3: MSB buffer - mark LIGHT AND DARK gray (XTH value 1 or 2)
// In LUT: 0 bit = apply gray effect, 1 bit = untouched
renderer.clearScreen(0x00); renderer.clearScreen(0x00);
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++) {
const uint8_t pv = getPixelValue(x, y); const uint8_t pv = getPixelValue(x, y);
if (pv == 1 || pv == 2) { // Dark grey or Light grey if (pv == 1 || pv == 2) {
renderer.drawPixel(x, y, false); renderer.drawPixel(x, y, false);
} }
} }
} }
renderer.copyGrayscaleMsbBuffers(); renderer.copyGrayscaleMsbBuffers();
// Display grayscale overlay
renderer.displayGrayBuffer(); renderer.displayGrayBuffer();
// Pass 4: Re-render BW to framebuffer (restore for next frame, instead of restoreBwBuffer)
renderer.clearScreen(); renderer.clearScreen();
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++) {
@ -321,53 +282,33 @@ void XtcReaderActivity::renderPage() {
} }
} }
} }
// Cleanup grayscale buffers with current frame buffer
renderer.cleanupGrayscaleWithFrameBuffer(); renderer.cleanupGrayscaleWithFrameBuffer();
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 const size_t srcRowBytes = (pageWidth + 7) / 8;
const size_t srcRowBytes = (pageWidth + 7) / 8; // 60 bytes for 480 width
for (uint16_t srcY = 0; srcY < maxSrcY; srcY++) { for (uint16_t srcY = 0; srcY < maxSrcY; srcY++) {
const size_t srcRowStart = srcY * srcRowBytes; const size_t srcRowStart = srcY * srcRowBytes;
for (uint16_t srcX = 0; srcX < pageWidth; srcX++) { for (uint16_t srcX = 0; srcX < pageWidth; srcX++) {
// Read source pixel (MSB first, bit 7 = leftmost pixel)
const size_t srcByte = srcRowStart + srcX / 8; const size_t srcByte = srcRowStart + srcX / 8;
const size_t srcBit = 7 - (srcX % 8); const size_t srcBit = 7 - (srcX % 8);
const bool isBlack = !((pageBuffer[srcByte] >> srcBit) & 1); // XTC: 0 = black, 1 = white const bool isBlack = !((pageBuffer[srcByte] >> srcBit) & 1);
if (isBlack) { if (isBlack) {
renderer.drawPixel(srcX, srcY, true); renderer.drawPixel(srcX, srcY, true);
} }
} }
} }
} if (pagesUntilFullRefresh <= 1) {
// White pixels are already cleared by clearScreen() renderer.displayBuffer(HalDisplay::HALF_REFRESH);
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
free(pageBuffer); } else {
renderer.displayBuffer();
// XTC pages already have status bar pre-rendered, no need to add our own pagesUntilFullRefresh--;
}
// Display with appropriate refresh
if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
} else {
renderer.displayBuffer();
pagesUntilFullRefresh--;
} }
Serial.printf("[%lu] [XTR] Rendered page %lu/%lu (%u-bit)\n", millis(), currentPage + 1, xtc->getPageCount(), Serial.printf("[%lu] [成功] 显示页码: %lu/%lu\n", millis(), currentPage+1, xtc->getPageCount());
bitDepth);
} }
void XtcReaderActivity::gotoPage(uint32_t targetPage) { void XtcReaderActivity::gotoPage(uint32_t targetPage) {
const uint32_t totalPages = xtc->getPageCount(); const uint32_t totalPages = xtc->getPageCount();
if (targetPage >= totalPages) targetPage = totalPages - 1; if (targetPage >= totalPages) targetPage = totalPages - 1;

View File

@ -13,6 +13,10 @@
#include <freertos/task.h> #include <freertos/task.h>
#include "activities/ActivityWithSubactivity.h" #include "activities/ActivityWithSubactivity.h"
namespace {
constexpr size_t MAX_PAGE_BUFFER_SIZE = (480 * 800 + 7) / 8 * 2;
static uint8_t s_pageBuffer[MAX_PAGE_BUFFER_SIZE] = {0};
} // namespace
class XtcReaderActivity final : public ActivityWithSubactivity { class XtcReaderActivity final : public ActivityWithSubactivity {
std::shared_ptr<Xtc> xtc; std::shared_ptr<Xtc> xtc;
@ -23,6 +27,8 @@ class XtcReaderActivity final : public ActivityWithSubactivity {
bool updateRequired = false; bool updateRequired = false;
const std::function<void()> onGoBack; const std::function<void()> onGoBack;
const std::function<void()> onGoHome; const std::function<void()> onGoHome;
//分批缓存
uint32_t m_loadedMax = 499;
static void taskTrampoline(void* param); static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop(); [[noreturn]] void displayTaskLoop();

View File

@ -4,38 +4,15 @@
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "fontIds.h" #include "fontIds.h"
#include "Xtc.h"
namespace { namespace {
constexpr int SKIP_PAGE_MS = 700; constexpr int SKIP_PAGE_MS = 700;
int page = 1;
} // namespace } // namespace
int XtcReaderChapterSelectionActivity::getPageItems() const { int XtcReaderChapterSelectionActivity::getPageItems() const {
constexpr int startY = 60; return 25; // ✅ 优化固定返回25匹配业务逻辑
constexpr int lineHeight = 30;
const int screenHeight = renderer.getScreenHeight();
const int endY = screenHeight - lineHeight;
const int availableHeight = endY - startY;
int items = availableHeight / lineHeight;
if (items < 1) {
items = 1;
}
return items;
}
int XtcReaderChapterSelectionActivity::findChapterIndexForPage(uint32_t page) const {
if (!xtc) {
return 0;
}
const auto& chapters = xtc->getChapters();
for (size_t i = 0; i < chapters.size(); i++) {
if (page >= chapters[i].startPage && page <= chapters[i].endPage) {
return static_cast<int>(i);
}
}
return 0;
} }
void XtcReaderChapterSelectionActivity::taskTrampoline(void* param) { void XtcReaderChapterSelectionActivity::taskTrampoline(void* param) {
@ -44,34 +21,27 @@ void XtcReaderChapterSelectionActivity::taskTrampoline(void* param) {
} }
void XtcReaderChapterSelectionActivity::onEnter() { void XtcReaderChapterSelectionActivity::onEnter() {
renderer.clearScreen();
Activity::onEnter(); Activity::onEnter();
if (!xtc) {
return;
}
renderingMutex = xSemaphoreCreateMutex();
selectorIndex = findChapterIndexForPage(currentPage);
updateRequired = true; updateRequired = true;
xTaskCreate(&XtcReaderChapterSelectionActivity::taskTrampoline, "XtcReaderChapterSelectionActivityTask", selectorIndex = 0;
4096, // Stack size page = 1;
this, // Parameters xTaskCreate(&XtcReaderChapterSelectionActivity::taskTrampoline, "XtcReaderChapterSelectionTask",
1, // Priority 4096,
&displayTaskHandle // Task handle this,
1,
&displayTaskHandle
); );
} }
void XtcReaderChapterSelectionActivity::onExit() { void XtcReaderChapterSelectionActivity::onExit() {
Activity::onExit(); Activity::onExit();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) { if (displayTaskHandle) {
vTaskDelete(displayTaskHandle); vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr; displayTaskHandle = nullptr;
} }
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
} }
void XtcReaderChapterSelectionActivity::loop() { void XtcReaderChapterSelectionActivity::loop() {
@ -84,31 +54,33 @@ void XtcReaderChapterSelectionActivity::loop() {
const int pageItems = getPageItems(); const int pageItems = getPageItems();
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
pagebegin=(page-1)*pageItems; const int pagebegin=(page-1)*25;
const auto& chapters = xtc->getChapters_gd(pagebegin); xtc->readChapters_gd(pagebegin);
if (!chapters.empty() && selectorIndex >= 0 && selectorIndex < static_cast<int>(chapters.size())) { uint32_t chapterpage = this->xtc->getChapterstartpage(selectorIndex);
onSelectPage(chapters[selectorIndex].startPage); Serial.printf("[%lu] [XTC] 跳转章节:%d,跳转页数:%d\n", millis(), selectorIndex, chapterpage);
}
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { onSelectPage(chapterpage);
// 确认按键逻辑,按需补充
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
onGoBack(); onGoBack();
} else if (prevReleased) { } else if (prevReleased) {
bool isUpKey = mappedInput.wasReleased(MappedInputManager::Button::Up); bool isUpKey = mappedInput.wasReleased(MappedInputManager::Button::Up);
if (skipPage || isUpKey) { if (skipPage || isUpKey) {
page -= 1; page -= 1;
if(page < 1) page = 1; if(page < 1) page = 1;
selectorIndex = (page-1)*pageItems; selectorIndex = (page-1)*25; // ✅ BUG修复局部索引0选中当前页第一个
} else { } else {
selectorIndex--; selectorIndex--; // ✅ BUG修复局部索引减1
if(selectorIndex < 0) selectorIndex = 0; if(selectorIndex < 0) selectorIndex = 0; // ✅ 边界防护
} }
updateRequired = true; updateRequired = true;
} else if (nextReleased) { } else if (nextReleased) {
bool isDownKey = mappedInput.wasReleased(MappedInputManager::Button::Down); bool isDownKey = mappedInput.wasReleased(MappedInputManager::Button::Down);
if (skipPage || isDownKey) { if (skipPage || isDownKey) {
page += 1; page += 1;
selectorIndex = (page-1)*pageItems; selectorIndex = (page-1)*25; // ✅ BUG修复局部索引24选中当前页第一个
} else { } else {
selectorIndex++; selectorIndex++; // ✅ BUG修复局部索引加1
} }
updateRequired = true; updateRequired = true;
} }
@ -118,9 +90,7 @@ void XtcReaderChapterSelectionActivity::displayTaskLoop() {
while (true) { while (true) {
if (updateRequired) { if (updateRequired) {
updateRequired = false; updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen(); renderScreen();
xSemaphoreGive(renderingMutex);
} }
vTaskDelay(10 / portTICK_PERIOD_MS); vTaskDelay(10 / portTICK_PERIOD_MS);
} }
@ -128,35 +98,38 @@ void XtcReaderChapterSelectionActivity::displayTaskLoop() {
void XtcReaderChapterSelectionActivity::renderScreen() { void XtcReaderChapterSelectionActivity::renderScreen() {
renderer.clearScreen(); renderer.clearScreen();
const int pagebegin=(page-1)*25;
int page_chapter=25;
static int parsedPage = -1; // ✅ 保留页码缓存只解析1次
const auto pageWidth = renderer.getScreenWidth(); if (parsedPage != page) {
const int pageItems = getPageItems(); xtc->readChapters_gd(pagebegin);
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Select Chapter", true, EpdFontFamily::BOLD); parsedPage = page;
const auto& chapters = xtc->getChapters();
if (chapters.empty()) {
renderer.drawCenteredText(UI_10_FONT_ID, 120, "No chapters");
renderer.displayBuffer();
return;
} }
const auto pageStartIndex = selectorIndex / pageItems * pageItems; const auto pageWidth = renderer.getScreenWidth();
renderer.fillRect(0, 60 + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1, 30); renderer.drawCenteredText(UI_12_FONT_ID, 15, "Select Chapter", true, EpdFontFamily::BOLD);
for (int i = pagebegin; i <= pagebegin + pageItems - 1; i++) {
int localIdx = i - pagebegin; const int FIX_LINE_HEIGHT = 29;
const int BASE_Y = 60;
// ✅ 强制循环渲染25章(pagebegin ~ pagebegin+24),无有效数判断、不截断、不满也留空行
for (int i = pagebegin; i <= pagebegin + page_chapter - 1; i++) {
int localIdx = i - pagebegin; // ✅ 保留核心修复全局索引→局部索引0~24必加读取数据全靠它
uint32_t currOffset = this->xtc->getChapterstartpage(i); uint32_t currOffset = this->xtc->getChapterstartpage(i); // ✅ 传局部索引,能读到正确数据
std::string dirTitle = this->xtc->getChapterTitleByIndex(i); std::string dirTitle = this->xtc->getChapterTitleByIndex(i); // ✅ 传局部索引,能读到正确标题
Serial.printf("[%lu] [XTC_CHAPTER] 第%d章名字为:%s,页码为%d\n", millis(), i, dirTitle.c_str(),currOffset); Serial.printf("[%lu] [XTC_CHAPTER] 第%d章名字为:%s,页码为%d\n", millis(), i, dirTitle.c_str(),currOffset);
static char title[64]; static char title[64];
strncpy(title, dirTitle.c_str(), sizeof(title)-1); strncpy(title, dirTitle.c_str(), sizeof(title)-1);
title[sizeof(title)-1] = '\0'; title[sizeof(title)-1] = '\0';
int drawY = BASE_Y + localIdx * FIX_LINE_HEIGHT; int drawY = BASE_Y + localIdx * FIX_LINE_HEIGHT; // ✅ 简化计算,逻辑正确
Serial.printf("选中的选项是:%d\n",selectorIndex); Serial.printf("选中的选项是:%d\n",selectorIndex); // ✅ 补全换行符,日志整洁
renderer.drawText(UI_10_FONT_ID, 20, drawY, title, i!= selectorIndex); renderer.drawText(UI_10_FONT_ID, 20, drawY, title, i!= selectorIndex); // ✅ 核心修复:选中态正常,必加!
} }
renderer.displayBuffer(); renderer.displayBuffer();
}