图库: 阅读器翻页按显示尺寸解码(零重采样)+ 多页闪退防御 + 从第 N 张开始(M17)
用户反馈「横竖翻页都卡(40 页也卡)」「140 张图的文件夹一打开就闪退」 「要从中间某张开始看」三条,本次分别处理: 翻页卡顿(真因:每帧一次重采样) - 解码目标从「屏长边」改成「当前 fit/zoom 的显示尺寸」:适宽 3:4 页原来解成 540x720、显示 480x640,draw_page_at 于是每帧走 meow_ui_img_scaled —— 每页每帧 多一遍全图最近邻重采样 + 一次 1.2MB 临时 malloc/free(>128KB 走 mmap)。 - 现在解码结果与显示框一致(±2px 容差)→ 翻页/滚动热路径只有原生 blit。 - fit/zoom 变化时按新几何重解(slot_ready 判据 + 同页替换),不再残留缩放; 已到解码预算或已是原始分辨率的小图不会反复重解。 - 拖动/吸附/fling 帧只 invalidate 内容区(顶栏静态,不再每帧重绘标题文字); 异步落位只在「该页真的在屏幕上」时才重绘,预取邻居不刷屏。 - 页原始尺寸按册缓存(8B/页),绘制几何不再每帧 fopen probe 文件。 多页闪退(宿主未能复现,按内存边界防御) - 常驻页图改为「显示尺寸」并加上限:单页 ≤3MB、整窗 ≤9MB(超限淘汰最远页); 常驻内存只与解码窗大小有关,不随页数增长。 - 解码中间峰值 >12MB 的页直接跳过并显示占位 + WARN(旧代码 PNG/BMP 放到 40MB, 3000x3000 就是 36MB 瞬时分配;JPEG 完全没上限)。140 页夹具(含坏图/超大图) 走真实 UI 链路打开、翻页、跳页、条漫全部通过。 新增 mr_reader_open_dir_at(dir, start_index)(文件管理器模式入口) - 显式下标优先于续读记录;-1=续读(旧入口语义),其它负数钳到 0,≥页数钳到末页。 - mr_reader_open_dir 保留为 (dir, MR_READER_START_RESUME) 的包装;reader.h 导出。 测试:新增 test_reader_perf(翻页零重采样/fit 切换/驻留上界)与 test_reader_many_pages(140 页 + 坏图/超大图 + 起始下标/钳制/续读优先级 + 条漫跳页)。
This commit is contained in:
1 parent
73a9603ef0
commit
462cfd5bc0
5 files changed
+858
-71
No files matched your search
+286
-71
@@ -10,6 +10,12 @@
|
||||
* 页源:logic/reader/page_source 的 mr_book 契约(count/meta/open/close)。条漫排版用
|
||||
* meta() 不解码像素;解码入口走 SDK M12 预算解码 meow_img_load_file_dim(JPEG 解码期缩解,
|
||||
* PNG/BMP 整幅解码后降采样——超大头先 meow_img_probe_file 预判,超过预算跳过并告警)。
|
||||
*
|
||||
* 性能(M17):解码目标 = **当前 fit/zoom 的显示尺寸**(不是屏长边),所以翻页/滚动热路径
|
||||
* 只有原生 blit:不做 meow_ui_img_scaled 的每帧重采样,也不再每帧 malloc 1.2MB 临时缓冲。
|
||||
* fit 变化时窗口按新几何重解一次;拖动/吸附帧只 invalidate 内容区(顶栏静态不重绘)。
|
||||
* 内存:单页显示缓存 ≤3MB、整窗 ≤9MB(超限淘汰最远页),解码中间峰值 >12MB 的页直接跳过
|
||||
* 并显示占位——页数增长不会线性吃内存(常驻只与解码窗大小有关)。
|
||||
* 方向:一律读 M3 的 mr_reader_get_direction()(默认 RTL),不另立全局。
|
||||
*
|
||||
* 模式(M7):翻页/条漫是一等公民。阅读 chrome 的图标按钮一键切换;每册的模式与 last page
|
||||
@@ -64,8 +70,11 @@
|
||||
#define MR_JUMP_TIMEOUT_MS 2500 /* 目标页迟迟解不出来 → 降级落位并提示 */
|
||||
#define MR_CONT_SAVE_MS 1000 /* 续读状态落盘节流 */
|
||||
#define MR_TOAST_MS 1400
|
||||
#define MR_LOAD_RAW_WARN (12L * 1024 * 1024) /* 非缩解格式原始 RGBA 超此值提示残余风险 */
|
||||
#define MR_LOAD_RAW_CAP (40L * 1024 * 1024) /* 超此值直接跳过(防 OOM;页显示占位) */
|
||||
#define MR_LOAD_RAW_WARN (8L * 1024 * 1024) /* 非缩解格式原始 RGBA 超此值提示残余风险 */
|
||||
#define MR_DECODE_PEAK_CAP (12L * 1024 * 1024) /* 单次解码中间缓冲峰值上限(超则跳过,防 OOM 闪退) */
|
||||
#define MR_PAGE_BYTES_CAP (3L * 1024 * 1024) /* 单页「显示尺寸」缓存字节上限 */
|
||||
#define MR_RESIDENT_CAP (9L * 1024 * 1024) /* 页图缓存总字节上限(超则淘汰最远页) */
|
||||
#define MR_SLOT_TOL 2 /* 缓存图与显示框允许的像素偏差(≤2 走原生 blit,不重采样) */
|
||||
|
||||
#define MR_REQ_MAX 8
|
||||
#define MR_RES_MAX 4
|
||||
@@ -75,6 +84,7 @@
|
||||
|
||||
static mr_page_ctrl g_ctrl;
|
||||
static char g_dir[MR_PATH_MAX];
|
||||
static int g_start_at = MR_READER_START_RESUME; /* 下次 on_enter 的显式起始页(-1=续读) */
|
||||
|
||||
static mr_book_handle g_handle; /* 目录册或档案册(M11 统一开册) */
|
||||
static mr_book g_book;
|
||||
@@ -133,6 +143,14 @@ static page_slot g_slot[MR_SLOTS];
|
||||
/* 解码失败页标记:避免对坏图/超大图无限重试,UI 显示「图片无法显示」而不是一直加载中。 */
|
||||
static unsigned char g_page_fail[(MR_BOOK_MAX_PAGES + 7) / 8];
|
||||
|
||||
/* 每页原始像素尺寸缓存(打开时按 count 分配;0,0=未知)。绘制/排版几何只查这里,
|
||||
* 不在每帧去 probe 文件(dir_ops_meta 是 fopen+读头,每帧一次会直接拖垮翻页)。 */
|
||||
typedef struct page_meta { int w, h; } page_meta;
|
||||
static page_meta *g_meta;
|
||||
|
||||
/* 解码预算(屏长边):超过它的显示尺寸无法靠「再解一次」变大,用于避免重解死循环。 */
|
||||
static int g_decode_budget = 720;
|
||||
|
||||
static int page_failed(int idx)
|
||||
{
|
||||
if (idx < 0 || idx >= MR_BOOK_MAX_PAGES) return 0;
|
||||
@@ -183,6 +201,17 @@ static int g_wo_last_y; /* 条漫:上一次 move 的 y */
|
||||
static int g_wo_y0; /* 条漫:按下点 y(slop 判定) */
|
||||
static int g_wo_pull; /* 条漫:到边后继续拉的累计量(>0 往末页方向) */
|
||||
|
||||
/* ---- 绘页路径计数(宿主测试观察:翻页热路径应全部是 blit)---- */
|
||||
#ifdef MR_READER_HOST_TEST
|
||||
static int g_blit_draws;
|
||||
static int g_scaled_draws;
|
||||
#define NOTE_BLIT() (g_blit_draws++)
|
||||
#define NOTE_SCALED() (g_scaled_draws++)
|
||||
#else
|
||||
#define NOTE_BLIT() ((void)0)
|
||||
#define NOTE_SCALED() ((void)0)
|
||||
#endif
|
||||
|
||||
/* ---- 单击延迟(双击判定窗口)---- */
|
||||
static int g_tap_pending;
|
||||
static int g_tap_x, g_tap_y;
|
||||
@@ -197,6 +226,8 @@ static void mode_set(int mode, int from_page);
|
||||
static void toast(const char *msg);
|
||||
static void slider_place(void);
|
||||
static void update_title(void);
|
||||
static void invalidate_content(void);
|
||||
static page_slot *slot_find(int idx);
|
||||
static void cont_touch(void);
|
||||
static void cont_save_now(void);
|
||||
static int current_page(void);
|
||||
@@ -213,34 +244,66 @@ static int path_is_jpeg(const char *path)
|
||||
return strcasecmp(dot, "jpg") == 0 || strcasecmp(dot, "jpeg") == 0;
|
||||
}
|
||||
|
||||
/* 目标解码长边:优先=当前 fit/zoom 的显示尺寸(解码后可直接 blit,翻页零重采样),
|
||||
* 再受屏长边预算与单页字节上限约束。pw/ph=原始尺寸。 */
|
||||
static int decode_long_side(int pw, int ph, int budget)
|
||||
{
|
||||
int tw = 0, th = 0;
|
||||
int long0 = pw > ph ? pw : ph;
|
||||
int short0 = pw > ph ? ph : pw;
|
||||
if (mr_nav_fit_size(g_fit, pw, ph, g_vw, g_vh, &tw, &th) != 0)
|
||||
return budget;
|
||||
if (g_zoom_pct != 100) mr_nav_zoom_size(tw, th, g_zoom_pct, &tw, &th);
|
||||
int l = tw > th ? tw : th;
|
||||
if (l < 64) l = 64;
|
||||
if (budget > 0 && l > budget) l = budget;
|
||||
/* 单页缓存上限:长边按比例收到能装下为止(高度极大的长条漫走这里)。 */
|
||||
while (l > 64) {
|
||||
int s = (int)((long long)short0 * l / long0);
|
||||
if ((long long)l * s * 4 <= MR_PAGE_BYTES_CAP) break;
|
||||
l = l * 3 / 4;
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
static meow_img *reader_load_page(const char *path, void *ud, int maxdim)
|
||||
{
|
||||
(void)ud;
|
||||
if (!path || !path[0]) return NULL;
|
||||
|
||||
/* 预算预判(M12):先读文件头拿原始尺寸,不解码像素。
|
||||
* - 非缩解格式(PNG/BMP)原始 RGBA 超过预算 → 跳过,避免真机 OOM(页面显示占位);
|
||||
* - 超过告警线 → 记 WARN(残余风险,按 M12 报告口径如实提示)。 */
|
||||
* - 解码中间缓冲峰值超上限 → 跳过(页面显示占位),避免真机被 OOM 杀掉(闪退);
|
||||
* - 非缩解格式(PNG/BMP)原始 RGBA 超过告警线 → 记 WARN(残余风险)。 */
|
||||
int pw = 0, ph = 0, ch = 0;
|
||||
if (meow_img_probe_file(path, &pw, &ph, &ch) && pw > 0 && ph > 0) {
|
||||
long long raw = (long long)pw * (long long)ph * 4;
|
||||
if (!path_is_jpeg(path)) {
|
||||
if (raw > MR_LOAD_RAW_CAP) {
|
||||
meow_log(MEOW_LOG_WARN, "reader decode skip %dx%d (raw %lldKB > cap): %s",
|
||||
pw, ph, raw / 1024, path);
|
||||
return NULL;
|
||||
}
|
||||
if (raw > MR_LOAD_RAW_WARN)
|
||||
int have = meow_img_probe_file(path, &pw, &ph, &ch) && pw > 0 && ph > 0;
|
||||
int dim = maxdim;
|
||||
if (have) {
|
||||
int l0 = pw > ph ? pw : ph;
|
||||
dim = decode_long_side(pw, ph, maxdim);
|
||||
long long peak;
|
||||
if (path_is_jpeg(path)) {
|
||||
/* 估 stbi 缩解后的输出(与 SDK 的档位选择同式):仍超上限才跳过。 */
|
||||
int s = 1;
|
||||
while (s < 8 && (l0 / (s * 2)) >= dim) s *= 2;
|
||||
peak = (long long)(pw / s) * (ph / s) * 4;
|
||||
} else {
|
||||
peak = (long long)pw * ph * 4; /* 整幅解码后降采样 */
|
||||
if (peak > MR_LOAD_RAW_WARN)
|
||||
meow_log(MEOW_LOG_WARN, "reader decode large non-scalable %dx%d (%lldKB): %s",
|
||||
pw, ph, raw / 1024, path);
|
||||
pw, ph, peak / 1024, path);
|
||||
}
|
||||
if (peak > MR_DECODE_PEAK_CAP) {
|
||||
meow_log(MEOW_LOG_WARN, "reader decode skip %dx%d (peak %lldKB > cap): %s",
|
||||
pw, ph, peak / 1024, path);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
meow_img *im = meow_img_load_file_dim(path, maxdim);
|
||||
meow_img *im = meow_img_load_file_dim(path, dim);
|
||||
if (!im) return NULL;
|
||||
/* 兜底:M12 之前的格式/未知格式若没被缩到预算内,再降一次(保持驻留纪律)。 */
|
||||
if (maxdim > 0 && (meow_img_w(im) > maxdim || meow_img_h(im) > maxdim)) {
|
||||
meow_img *small = meow_img_downscale(im, maxdim);
|
||||
if (dim > 0 && (meow_img_w(im) > dim || meow_img_h(im) > dim)) {
|
||||
meow_img *small = meow_img_downscale(im, dim);
|
||||
if (small) { meow_img_free(im); im = small; }
|
||||
}
|
||||
return im;
|
||||
@@ -291,6 +354,54 @@ static void log_mem(const char *tag)
|
||||
mem_kb("/proc/self/status", "VmRSS:"), resident_pages(), resident_bytes() / 1024);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 页原始尺寸缓存(绘制几何的唯一来源;每页只 probe 一次) */
|
||||
/* ============================================================ */
|
||||
static int page_meta_dims(int idx, int *w, int *h)
|
||||
{
|
||||
if (w) *w = 0;
|
||||
if (h) *h = 0;
|
||||
if (idx < 0 || idx >= g_count) return 0;
|
||||
if (g_meta) {
|
||||
if (g_meta[idx].w > 0 && g_meta[idx].h > 0) {
|
||||
if (w) *w = g_meta[idx].w;
|
||||
if (h) *h = g_meta[idx].h;
|
||||
return 1;
|
||||
}
|
||||
if (g_meta[idx].w < 0) return 0; /* 探测过且未知:别再每帧 fopen 一遍 */
|
||||
}
|
||||
int sw = 0, sh = 0;
|
||||
if (!mr_book_meta(&g_book, idx, &sw, &sh) || sw <= 0 || sh <= 0) {
|
||||
if (g_meta) { g_meta[idx].w = -1; g_meta[idx].h = 0; }
|
||||
return 0;
|
||||
}
|
||||
if (g_meta) { g_meta[idx].w = sw; g_meta[idx].h = sh; }
|
||||
if (w) *w = sw;
|
||||
if (h) *h = sh;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 显示框(fit + zoom):绘图与点击几何都走这里。未知名按 2:3 竖图占位。 */
|
||||
static void page_target_size_at(int idx, int *w, int *h)
|
||||
{
|
||||
int sw = 0, sh = 0;
|
||||
if (w) *w = 0;
|
||||
if (h) *h = 0;
|
||||
if (!w || !h) return;
|
||||
if (!page_meta_dims(idx, &sw, &sh)) {
|
||||
/* meta 不可用(未知/坏头):用已解码尺寸兜底(保持真实长宽比),
|
||||
* 都没解出来才按 2:3 竖图占位——排版稳定且不会把页面拉变形。 */
|
||||
page_slot *s = slot_find(idx);
|
||||
if (s) { sw = s->src_w; sh = s->src_h; }
|
||||
if (sw <= 0 || sh <= 0) {
|
||||
sw = g_vw > 0 ? g_vw : 1;
|
||||
sh = sw * 3 / 2;
|
||||
}
|
||||
}
|
||||
if (mr_nav_fit_size(g_fit, sw, sh, g_vw, g_vh, w, h) != 0) return;
|
||||
if (g_zoom_pct != 100) mr_nav_zoom_size(*w, *h, g_zoom_pct, w, h);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 续读状态(逐册 last page + mode;写入节流,避免滚动时频繁写卡) */
|
||||
/* ============================================================ */
|
||||
@@ -351,6 +462,18 @@ static int in_window(int idx)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 该页此刻是否真的画在屏幕上(异步落位后只重绘这种情况)。 */
|
||||
static int page_visible(int idx)
|
||||
{
|
||||
if (g_count <= 0 || idx < 0 || idx >= g_count) return 0;
|
||||
if (g_mode == MR_MODE_WEBTOON && g_strip_ok) {
|
||||
int f = mr_strip_page_at(&g_strip, g_woff);
|
||||
int l = mr_strip_page_at(&g_strip, g_woff + g_vh - 1);
|
||||
return idx >= f && idx <= l;
|
||||
}
|
||||
return idx == g_page || (g_slide_delta != 0 && idx == g_page + g_slide_delta);
|
||||
}
|
||||
|
||||
static page_slot *slot_find(int idx)
|
||||
{
|
||||
if (idx < 0) return NULL;
|
||||
@@ -373,20 +496,70 @@ static void slots_free_all(void)
|
||||
for (int i = 0; i < MR_SLOTS; i++) slot_clear(i);
|
||||
}
|
||||
|
||||
/* 放进缓存;缓存满时淘汰离焦点页最远的一张。返回 1=已放入。 */
|
||||
/* 缓存图是否就是当前 fit/zoom 的显示尺寸(可直接 blit)。已达解码预算的图也算就绪,
|
||||
* 否则「原始/放大」档会陷入「解不出目标尺寸 → 反复重解」。 */
|
||||
static int slot_ready(int idx)
|
||||
{
|
||||
page_slot *s = slot_find(idx);
|
||||
if (!s) return 0;
|
||||
int tw = 0, th = 0;
|
||||
page_target_size_at(idx, &tw, &th);
|
||||
if (tw <= 0 || th <= 0) return 1;
|
||||
if (abs(tw - s->src_w) <= MR_SLOT_TOL && abs(th - s->src_h) <= MR_SLOT_TOL) return 1;
|
||||
if (g_decode_budget > 0 &&
|
||||
(s->src_w >= g_decode_budget || s->src_h >= g_decode_budget)) return 1;
|
||||
/* 已是原始分辨率(小图不放大,SDK 不做上采样):这就是能拿到的最好结果。 */
|
||||
int mw = 0, mh = 0;
|
||||
if (page_meta_dims(idx, &mw, &mh) && s->src_w >= mw && s->src_h >= mh) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 淘汰离焦点页最远的一张(不淘汰 focus 与 except)。返回被清掉的槽下标,无则 -1。 */
|
||||
static int slot_evict_far(int except)
|
||||
{
|
||||
int focus = focus_page();
|
||||
int far = -1, far_d = -1;
|
||||
for (int i = 0; i < MR_SLOTS; i++) {
|
||||
if (i == except || !g_slot[i].img) continue;
|
||||
if (g_slot[i].idx == focus) continue;
|
||||
int d = abs(g_slot[i].idx - focus);
|
||||
if (d > far_d) { far_d = d; far = i; }
|
||||
}
|
||||
if (far >= 0) slot_clear(far);
|
||||
return far;
|
||||
}
|
||||
|
||||
/* 放进缓存;同页旧图直接替换(fit/zoom 变了要换尺寸)。返回 1=已放入。 */
|
||||
static int slot_put(int idx, meow_img *img)
|
||||
{
|
||||
if (!img || idx < 0) return 0;
|
||||
int focus = focus_page();
|
||||
int empty = -1, far = -1, far_d = -1;
|
||||
for (int i = 0; i < MR_SLOTS; i++) {
|
||||
if (g_slot[i].idx == idx && g_slot[i].img) { meow_img_free(img); return 0; }
|
||||
if (!g_slot[i].img && empty < 0) empty = i;
|
||||
int d = !g_slot[i].img ? -1 : abs(g_slot[i].idx - focus);
|
||||
if (d > far_d) { far_d = d; far = i; }
|
||||
int i = -1, empty = -1;
|
||||
for (int k = 0; k < MR_SLOTS; k++) {
|
||||
if (g_slot[k].idx == idx && g_slot[k].img) { i = k; break; }
|
||||
if (!g_slot[k].img && empty < 0) empty = k;
|
||||
}
|
||||
if (i < 0) {
|
||||
if (empty >= 0) {
|
||||
i = empty;
|
||||
} else { /* 全满:替换离焦点页最远的一张 */
|
||||
int focus = focus_page(), far_d = -1;
|
||||
for (int k = 0; k < MR_SLOTS; k++) {
|
||||
int d = abs(g_slot[k].idx - focus);
|
||||
if (d > far_d) { far_d = d; i = k; }
|
||||
}
|
||||
}
|
||||
}
|
||||
int i = empty >= 0 ? empty : far;
|
||||
if (i < 0) { meow_img_free(img); return 0; }
|
||||
|
||||
/* 总字节上限:新图放进去前先淘汰最远页,保证常驻图不超过预算。 */
|
||||
long need = (long)meow_img_w(img) * meow_img_h(img) * 4;
|
||||
for (;;) {
|
||||
long held = resident_bytes();
|
||||
if (g_slot[i].img) held -= (long)g_slot[i].src_w * g_slot[i].src_h * 4;
|
||||
if (held + need <= MR_RESIDENT_CAP) break;
|
||||
if (slot_evict_far(i) < 0) break; /* 只剩目标槽:这一页必须放进来 */
|
||||
}
|
||||
|
||||
slot_clear(i);
|
||||
g_slot[i].idx = idx;
|
||||
g_slot[i].img = img;
|
||||
@@ -456,7 +629,7 @@ static int req_queued(int idx)
|
||||
static void queue_page(int idx)
|
||||
{
|
||||
if (!mr_book_valid(&g_book) || idx < 0 || idx >= g_count) return;
|
||||
if (slot_find(idx) || page_failed(idx)) return;
|
||||
if (slot_ready(idx) || page_failed(idx)) return;
|
||||
pthread_mutex_lock(&g_lock);
|
||||
if (!g_stop && !req_queued(idx) && g_req_n < MR_REQ_MAX) {
|
||||
g_req[g_req_tail].idx = idx;
|
||||
@@ -562,11 +735,11 @@ static int results_drain(void)
|
||||
if (got[i].gen != g_gen) { meow_img_free(got[i].img); continue; }
|
||||
if (got[i].fail) {
|
||||
page_mark_failed(got[i].idx);
|
||||
if (in_window(got[i].idx)) g_fail_paint = 1;
|
||||
if (page_visible(got[i].idx)) g_fail_paint = 1;
|
||||
continue;
|
||||
}
|
||||
if (in_window(got[i].idx) && !slot_find(got[i].idx)) {
|
||||
if (slot_put(got[i].idx, got[i].img)) changed = 1;
|
||||
if (in_window(got[i].idx)) {
|
||||
if (slot_put(got[i].idx, got[i].img) && page_visible(got[i].idx)) changed = 1;
|
||||
} else {
|
||||
meow_img_free(got[i].img);
|
||||
}
|
||||
@@ -590,7 +763,8 @@ static void window_update(void)
|
||||
/* ============================================================ */
|
||||
static int strip_meta_cb(void *ctx, int index, int *w, int *h)
|
||||
{
|
||||
return mr_book_meta((const mr_book *)ctx, index, w, h);
|
||||
(void)ctx;
|
||||
return page_meta_dims(index, w, h); /* 走缓存:重排版不再逐页 fopen probe */
|
||||
}
|
||||
|
||||
static int strip_ensure(void)
|
||||
@@ -641,7 +815,7 @@ static void wo_set(void *ctx, int off)
|
||||
static void wo_redraw(void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||||
invalidate_content(); /* 滚动只动内容区,顶栏静态不重绘 */
|
||||
}
|
||||
|
||||
static void scroll_ensure(void)
|
||||
@@ -661,27 +835,23 @@ static void scroll_free(void)
|
||||
/* ============================================================ */
|
||||
/* 布局/绘制 */
|
||||
/* ============================================================ */
|
||||
static void page_target_size(const page_slot *s, int *w, int *h)
|
||||
/* 内容视口失效区:chrome 可见时顶栏是静态的,拖动/吸附帧只刷其下内容,
|
||||
* 避免每帧重绘顶栏文字(软渲下文字是最贵的绘制物)。 */
|
||||
static void invalidate_content(void)
|
||||
{
|
||||
*w = *h = 0;
|
||||
if (!s) return;
|
||||
if (mr_nav_fit_size(g_fit, s->src_w, s->src_h, g_vw, g_vh, w, h) != 0) return;
|
||||
if (g_zoom_pct != 100) mr_nav_zoom_size(*w, *h, g_zoom_pct, w, h);
|
||||
if (g_chrome)
|
||||
meow_ui_invalidate(0, MR_TOP_BAR_H, g_vw, g_vh - MR_TOP_BAR_H);
|
||||
else
|
||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||||
}
|
||||
|
||||
static void current_geom(int *w, int *h)
|
||||
{
|
||||
page_slot *s = slot_find(g_page);
|
||||
if (s) { page_target_size(s, w, h); return; }
|
||||
/* 页未就绪:用 meta 估算,保证平移/缩放的夹取稳定。 */
|
||||
int sw = 0, sh = 0;
|
||||
if (mr_book_meta(&g_book, g_page, &sw, &sh) && sw > 0 && sh > 0) {
|
||||
mr_nav_fit_size(g_fit, sw, sh, g_vw, g_vh, w, h);
|
||||
if (g_zoom_pct != 100) mr_nav_zoom_size(*w, *h, g_zoom_pct, w, h);
|
||||
return;
|
||||
page_target_size_at(g_page, w, h);
|
||||
if (!w || !h || *w <= 0 || *h <= 0) {
|
||||
if (w) *w = g_vw;
|
||||
if (h) *h = g_vh;
|
||||
}
|
||||
*w = g_vw;
|
||||
*h = g_vh;
|
||||
}
|
||||
|
||||
static void clamp_pan(void)
|
||||
@@ -705,18 +875,24 @@ static int pan_mode(void)
|
||||
return g_zoom_pct > 100 || content_overflow();
|
||||
}
|
||||
|
||||
/* 画一页(带水平位移);图与目标尺寸相等时走原生 blit(翻页热路径),否则最近邻缩放。 */
|
||||
/* 画一页(带水平位移);解码尺寸==显示尺寸时走原生 blit(翻页热路径零重采样),
|
||||
* 只在 fit/zoom 变化或极端长页(超单页缓存上限)时才退回最近邻缩放。 */
|
||||
static void draw_page_at(int idx, int off_x, int off_y)
|
||||
{
|
||||
page_slot *s = slot_find(idx);
|
||||
if (!s) return;
|
||||
int w, h;
|
||||
page_target_size(s, &w, &h);
|
||||
page_target_size_at(idx, &w, &h);
|
||||
if (w <= 0 || h <= 0) return;
|
||||
int x = (g_vw - w) / 2 + g_pan_x + off_x;
|
||||
int y = (g_vh - h) / 2 + g_pan_y + off_y;
|
||||
if (w == s->src_w && h == s->src_h) meow_ui_img(x, y, s->img);
|
||||
else meow_ui_img_scaled(x, y, w, h, s->img);
|
||||
if (abs(w - s->src_w) <= MR_SLOT_TOL && abs(h - s->src_h) <= MR_SLOT_TOL) {
|
||||
meow_ui_img(x + (w - s->src_w) / 2, y + (h - s->src_h) / 2, s->img);
|
||||
NOTE_BLIT();
|
||||
} else {
|
||||
meow_ui_img_scaled(x, y, w, h, s->img);
|
||||
NOTE_SCALED();
|
||||
}
|
||||
}
|
||||
|
||||
/* 条漫:按滚动偏移画可见页。典型 2:3 页的解码尺寸与显示尺寸相等 → 原生 blit;
|
||||
@@ -735,8 +911,15 @@ static void draw_strip(const meow_theme *th)
|
||||
if (h <= 0) continue;
|
||||
page_slot *s = slot_find(i);
|
||||
if (s) {
|
||||
if (s->src_w == g_vw && s->src_h == h) meow_ui_img(0, y, s->img);
|
||||
else meow_ui_img_scaled(0, y, g_vw, h, s->img);
|
||||
/* 解码=显示尺寸时原生 blit:gfx 只处理屏幕上可见的行,
|
||||
* 长条页只露一截时不再整页重采样(M7 finding 的子矩形 blit 诉求)。 */
|
||||
if (abs(s->src_w - g_vw) <= MR_SLOT_TOL && abs(s->src_h - h) <= MR_SLOT_TOL) {
|
||||
meow_ui_img((g_vw - s->src_w) / 2, y, s->img);
|
||||
NOTE_BLIT();
|
||||
} else {
|
||||
meow_ui_img_scaled(0, y, g_vw, h, s->img);
|
||||
NOTE_SCALED();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
/* 未就绪:只填可见部分(长页不整块清屏),给一帧占位而不是白屏。 */
|
||||
@@ -907,7 +1090,7 @@ static void pan_by(int dx, int dy)
|
||||
g_pan_x += dx;
|
||||
g_pan_y += dy;
|
||||
clamp_pan();
|
||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||||
invalidate_content();
|
||||
}
|
||||
|
||||
static void commit_page(int target)
|
||||
@@ -929,7 +1112,7 @@ static void slide_update(meow_anim *a, float value, void *ctx)
|
||||
{
|
||||
(void)a; (void)ctx;
|
||||
g_anim_v = (int)value;
|
||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||||
invalidate_content(); /* 吸附帧只刷内容区(整屏 minus 静态顶栏) */
|
||||
}
|
||||
|
||||
static void slide_done(meow_anim *a, int canceled, void *ctx)
|
||||
@@ -944,7 +1127,7 @@ static void slide_done(meow_anim *a, int canceled, void *ctx)
|
||||
if (!canceled && delta)
|
||||
commit_page(g_page + delta);
|
||||
else
|
||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||||
invalidate_content();
|
||||
meow_log(MEOW_LOG_INFO, "reader flip done page=%d/%d total_ms=%llu",
|
||||
g_page + 1, g_count,
|
||||
(unsigned long long)(meow_ui_time_ms() - g_flip_t0));
|
||||
@@ -982,7 +1165,7 @@ static void bounce_update(meow_anim *a, float value, void *ctx)
|
||||
{
|
||||
(void)a; (void)ctx;
|
||||
g_drag_off = (int)value;
|
||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||||
invalidate_content();
|
||||
}
|
||||
|
||||
static void bounce_done(meow_anim *a, int canceled, void *ctx)
|
||||
@@ -991,7 +1174,7 @@ static void bounce_done(meow_anim *a, int canceled, void *ctx)
|
||||
g_bounce = NULL;
|
||||
g_drag_off = 0;
|
||||
g_drag_mode = 0;
|
||||
if (!g_quitting) meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||||
if (!g_quitting) invalidate_content();
|
||||
}
|
||||
|
||||
static void start_bounce(int from_off)
|
||||
@@ -1002,7 +1185,7 @@ static void start_bounce(int from_off)
|
||||
bounce_update, bounce_done, NULL,
|
||||
};
|
||||
g_bounce = meow_anim_start(&d);
|
||||
if (!g_bounce) { g_drag_off = 0; g_drag_mode = 0; meow_ui_invalidate(0, 0, g_vw, g_vh); }
|
||||
if (!g_bounce) { g_drag_off = 0; g_drag_mode = 0; invalidate_content(); }
|
||||
}
|
||||
|
||||
/* 点击触发的翻页(无跟手位移)。首/末页再前进 → 相邻册(无相邻册则轻提示)。 */
|
||||
@@ -1176,6 +1359,8 @@ static void open_book_reset_state(void)
|
||||
mr_jump_cancel(&g_jump);
|
||||
g_toast[0] = '\0';
|
||||
g_title_page = -1;
|
||||
free(g_meta);
|
||||
g_meta = NULL;
|
||||
}
|
||||
|
||||
/* 打开一本册并恢复模式/起始页。start_page<0 = 末页(上一册衔接)。1=成功。 */
|
||||
@@ -1189,6 +1374,7 @@ static int open_book(const char *dir, int start_page, int mode)
|
||||
g_mode = mr_mode_valid(mode) ? mode : MR_MODE_PAGED;
|
||||
|
||||
int maxdim = g_vw > g_vh ? g_vw : g_vh;
|
||||
g_decode_budget = maxdim;
|
||||
/* 目录 → 目录册;.cbz/.zip → 档案册(M11)。两者同一个 mr_book 契约,
|
||||
* 下面的翻页/条漫消费代码完全不区分 provider。 */
|
||||
int n = mr_book_open_auto(&g_book, &g_handle, g_dir, maxdim,
|
||||
@@ -1211,6 +1397,10 @@ static int open_book(const char *dir, int start_page, int mode)
|
||||
return 0;
|
||||
}
|
||||
g_count = n;
|
||||
/* 页尺寸缓存:每页 8 字节(4096 页上限 ≈32KB),换来的是一帧内零 fopen probe。 */
|
||||
g_meta = (page_meta *)calloc((size_t)g_count, sizeof *g_meta);
|
||||
if (!g_meta)
|
||||
meow_log(MEOW_LOG_WARN, "reader meta cache unavailable (%d pages)", g_count);
|
||||
|
||||
int page = start_page < 0 ? g_count - 1 : start_page;
|
||||
page = mr_nav_clamp_page(page, g_count);
|
||||
@@ -1289,7 +1479,7 @@ static void drag_paint(void)
|
||||
int gap = g_zoom_pct > 100 ? 33 : MR_PAINT_MS; /* 缩放帧要重采样:30fps 上限 */
|
||||
if (now - g_last_paint < (uint64_t)gap) return;
|
||||
g_last_paint = now;
|
||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||||
invalidate_content();
|
||||
}
|
||||
|
||||
/* 松手:过阈值且目标页已缓存 → 从当前位移吸附过去;否则回弹。 */
|
||||
@@ -1303,7 +1493,7 @@ static void drag_finish(void)
|
||||
g_drag_mode = 0;
|
||||
|
||||
if (mode != 1 || abs(dx) < MR_DRAG_SLOP) {
|
||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||||
invalidate_content();
|
||||
return;
|
||||
}
|
||||
int rtl = mr_reader_get_direction() == MR_DIR_RTL;
|
||||
@@ -1312,7 +1502,7 @@ static void drag_finish(void)
|
||||
if (mr_nav_commit(dx, g_vw)) {
|
||||
if (target == g_page) { /* 首/末页越界:进相邻册 */
|
||||
book_switch(delta > 0 ? 1 : -1);
|
||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||||
invalidate_content();
|
||||
return;
|
||||
}
|
||||
g_flip_t0 = meow_ui_time_ms();
|
||||
@@ -1341,7 +1531,7 @@ static void webtoon_release(void)
|
||||
if (pull <= -MR_BOOK_PULL && book_switch(-1)) return;
|
||||
if (g_scroll) meow_scroll_fling(g_scroll);
|
||||
cont_touch();
|
||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||||
invalidate_content();
|
||||
}
|
||||
|
||||
void reader_on_touch_down(int x, int y)
|
||||
@@ -1440,7 +1630,7 @@ void reader_on_touch_move(int x, int y)
|
||||
g_dragging = 0;
|
||||
g_drag_mode = 0;
|
||||
g_drag_off = 0;
|
||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||||
invalidate_content();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1614,9 +1804,8 @@ static void reader_tick(void)
|
||||
if (now - g_last_tick < MR_TICK_MS) return;
|
||||
g_last_tick = now;
|
||||
|
||||
int had_current = slot_find(current_page()) != NULL;
|
||||
if (results_drain() && !had_current && slot_find(current_page()) && !g_dragging)
|
||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||||
/* 只有当落位的页真的在屏幕上(或换了几何的当前页)才重绘,预取邻居不刷屏。 */
|
||||
if (results_drain() && !g_dragging) invalidate_content();
|
||||
|
||||
/* 打开册的瞬时资源失败:稍后自动重试一次(进入本页时已停 library 后台解码)。 */
|
||||
if (g_open_err && g_open_retry_at && now >= g_open_retry_at) {
|
||||
@@ -1628,7 +1817,7 @@ static void reader_tick(void)
|
||||
}
|
||||
if (g_fail_paint) {
|
||||
g_fail_paint = 0;
|
||||
if (!g_dragging) meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||||
if (!g_dragging) invalidate_content();
|
||||
}
|
||||
|
||||
if (g_tap_pending && now - g_tap_ms >= MR_DOUBLE_MS) tap_flush();
|
||||
@@ -1811,9 +2000,14 @@ static void reader_page_enter(meow_view *v, void *ctx)
|
||||
if (data && data[0]) mr_cache_init(data);
|
||||
}
|
||||
|
||||
/* 打开册:恢复该册上次的模式与页码(无记录 → 翻页模式第 1 页)。 */
|
||||
/* 打开册:显式起始页(书库文件管理器模式点第 N 张)优先于续读记录;
|
||||
* 否则恢复该册上次的模式与页码(无记录 → 翻页模式第 1 页)。 */
|
||||
int mode = mr_cont_get_mode(g_dir, MR_MODE_PAGED);
|
||||
int page = mr_cont_get_page(g_dir, 0);
|
||||
if (g_start_at != MR_READER_START_RESUME) {
|
||||
page = g_start_at;
|
||||
g_start_at = MR_READER_START_RESUME;
|
||||
}
|
||||
open_book(g_dir, page, mode);
|
||||
|
||||
ticker_start();
|
||||
@@ -1842,6 +2036,9 @@ static void reader_page_leave(meow_view *v, void *ctx)
|
||||
mr_strip_free(&g_strip);
|
||||
g_strip_ok = 0;
|
||||
slots_free_all();
|
||||
free(g_meta);
|
||||
g_meta = NULL;
|
||||
g_start_at = MR_READER_START_RESUME;
|
||||
mr_book_close_auto(&g_book, &g_handle);
|
||||
|
||||
if (g_slider) { meow_widget_free(g_slider); g_slider = NULL; }
|
||||
@@ -1882,14 +2079,22 @@ const char *mr_reader_dir(void)
|
||||
return g_dir;
|
||||
}
|
||||
|
||||
void mr_reader_open_dir(const char *path)
|
||||
void mr_reader_open_dir_at(const char *path, int start_index)
|
||||
{
|
||||
if (!path) return;
|
||||
snprintf(g_dir, sizeof g_dir, "%s", path);
|
||||
meow_log(MEOW_LOG_INFO, "reader open: %s", g_dir);
|
||||
/* -1=续读(旧入口语义);其它负数钳到 0;上界在 open_book 里按页数钳。 */
|
||||
g_start_at = start_index == MR_READER_START_RESUME ? MR_READER_START_RESUME
|
||||
: (start_index < 0 ? 0 : start_index);
|
||||
meow_log(MEOW_LOG_INFO, "reader open: %s (start=%d)", g_dir, g_start_at);
|
||||
meow_view_page_push(ACT_READER);
|
||||
}
|
||||
|
||||
void mr_reader_open_dir(const char *path)
|
||||
{
|
||||
mr_reader_open_dir_at(path, MR_READER_START_RESUME);
|
||||
}
|
||||
|
||||
#ifdef MR_READER_HOST_TEST
|
||||
/* 宿主集成测试观察口(不改变任何状态)。 */
|
||||
int mr_reader_ui_mode(void) { return g_mode; }
|
||||
@@ -1898,4 +2103,14 @@ int mr_reader_ui_count(void) { return g_count; }
|
||||
int mr_reader_ui_open_err(void) { return g_open_err; }
|
||||
int mr_reader_ui_jump_open(void) { return g_jump_open; }
|
||||
int mr_reader_ui_jump_pending(void) { return mr_jump_active(&g_jump); }
|
||||
long mr_reader_ui_resident_kb(void) { return resident_bytes() / 1024; }
|
||||
int mr_reader_ui_failed_pages(void)
|
||||
{
|
||||
int n = 0;
|
||||
for (int i = 0; i < g_count && i < MR_BOOK_MAX_PAGES; i++)
|
||||
if (page_failed(i)) n++;
|
||||
return n;
|
||||
}
|
||||
int mr_reader_ui_blit_draws(void) { return g_blit_draws; }
|
||||
int mr_reader_ui_scaled_draws(void) { return g_scaled_draws; }
|
||||
#endif
|
||||
@@ -13,6 +13,15 @@ extern "C" {
|
||||
void reader_on_touch_down(int x, int y);
|
||||
void reader_on_touch_move(int x, int y);
|
||||
|
||||
/* 书库 → 阅读器(文件管理器模式用):按目录内图片自然序从 start_index 开始。
|
||||
* - start_index >= 册页数:钳到末页;其它负数按 0 处理;
|
||||
* - start_index == MR_READER_START_RESUME(-1):沿用该册续读记录(= mr_reader_open_dir)。
|
||||
* 显式下标优先于续读记录:用户在列表里点了某张图,就不该被上次的页码带走。 */
|
||||
#define MR_READER_START_RESUME (-1)
|
||||
void mr_reader_open_dir_at(const char *dir, int start_index);
|
||||
/* 保留旧入口:等价于 mr_reader_open_dir_at(dir, MR_READER_START_RESUME)。 */
|
||||
void mr_reader_open_dir(const char *dir);
|
||||
|
||||
#ifdef MR_READER_HOST_TEST
|
||||
/* 宿主集成测试专用:只读观察本页状态(不参与产品逻辑)。 */
|
||||
int mr_reader_ui_mode(void); /* MR_MODE_PAGED / MR_MODE_WEBTOON */
|
||||
@@ -20,6 +29,10 @@ int mr_reader_ui_page(void); /* 当前页(条漫=视口顶端所在页
|
||||
int mr_reader_ui_count(void); /* 当前册页数 */
|
||||
int mr_reader_ui_jump_open(void); /* 1=跳页滑块已展开 */
|
||||
int mr_reader_ui_jump_pending(void);/* 1=跳页目标等待解码 */
|
||||
long mr_reader_ui_resident_kb(void);/* 驻留页图字节(KB) */
|
||||
int mr_reader_ui_failed_pages(void);/* 解码失败/超预算被跳过的页数 */
|
||||
int mr_reader_ui_blit_draws(void); /* 累计原生 blit 绘页次数(热路径应为纯 blit) */
|
||||
int mr_reader_ui_scaled_draws(void);/* 累计重采样绘页次数(每帧重采样=卡顿) */
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
Reference in new issue
Block a user