图库: 阅读器翻页按显示尺寸解码(零重采样)+ 多页闪退防御 + 从第 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:
AskaEth committed 2026-11-05 18:06:26 +08:00
1 parent 73a9603ef0
commit 462cfd5bc0
5 files changed
+858 -71

No files matched your search

+222
View File
@@ -0,0 +1,222 @@
/* test_reader_perf.c — 阅读页翻页热路径回归(需要 SDK 源码树,宿主跑真实 view/anim/img)。
*
* 用户反馈:「翻页很卡,横翻竖翻都卡」「40 页也卡」。真因是每帧一次 meow_ui_img_scaled:
* 解码尺寸(maxdim=屏长边) ≠ 显示尺寸(适宽/适高) → 每帧重采样 + 每帧 1.2MB 临时缓冲
* malloc/free。修法:解码期直接按显示尺寸落地,翻页只做原生 blit。
*
* 本测试用 600x800(3:4)合成 BMP 当页图,断言:
* P1 适宽:解码 480x640 == 显示框 → 绘页零重采样(scaled_draws 不增长);
* P2 连续翻页 10 次(预取窗口命中):每帧仍零重采样,且确实画了页(blit 增长);
* P3 切「适高」:重解到 540x720 后同样零重采样(fit 变化会按新几何重解,不残留缩放);
* P4 驻留内存有上界(<= 9MB:3~4 张显示尺寸页)。
*
* 编译:test/run_tests.sh(与 test_open_book_host 同口径;只发布 meow_ui.a 时 SKIP)。
*/
#include "tuku_internal.h"
#include "ui/reader/reader.h"
#include "meow_ui_internal.h"
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
void mr_nav_report(void) {}
static int g_fail;
#define CHECK(cond, ...) do { \
if (cond) { printf(" PASS\n"); } \
else { g_fail++; printf(" FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
static void put16(unsigned char *p, unsigned v) { p[0] = (unsigned char)v; p[1] = (unsigned char)(v >> 8); }
static void put32(unsigned char *p, unsigned v)
{
p[0] = (unsigned char)v; p[1] = (unsigned char)(v >> 8);
p[2] = (unsigned char)(v >> 16); p[3] = (unsigned char)(v >> 24);
}
/* 合成 BMP24(stb 可解);填一点非零像素,避免全 0 触发任何「透明」特判。 */
static int write_bmp24(const char *path, int w, int h, unsigned seed)
{
int stride = ((w * 3 + 3) / 4) * 4;
int imgsz = stride * h;
unsigned char fh[14] = { 0 }, ih[40] = { 0 };
fh[0] = 'B'; fh[1] = 'M';
put32(fh + 2, (unsigned)(54 + imgsz));
put32(fh + 10, 54);
put32(ih + 0, 40);
put32(ih + 4, (unsigned)w);
put32(ih + 8, (unsigned)h);
put16(ih + 12, 1);
put16(ih + 14, 24);
put32(ih + 20, (unsigned)imgsz);
FILE *f = fopen(path, "wb");
if (!f) return -1;
int ok = fwrite(fh, 1, sizeof fh, f) == sizeof fh &&
fwrite(ih, 1, sizeof ih, f) == sizeof ih;
unsigned char *row = (unsigned char *)calloc(1, (size_t)stride);
if (!row) ok = 0;
for (int y = 0; ok && y < h; y++) {
for (int x = 0; x < w; x++) {
row[x * 3 + 0] = (unsigned char)((x + seed) & 0xFF);
row[x * 3 + 1] = (unsigned char)((y * 3 + seed) & 0xFF);
row[x * 3 + 2] = (unsigned char)((x ^ y) & 0xFF);
}
if (fwrite(row, 1, (size_t)stride, f) != (size_t)stride) ok = 0;
}
free(row);
if (fclose(f) != 0) ok = 0;
return ok ? 0 : -1;
}
static int mkdir_p(const char *path)
{
char tmp[MR_PATH_MAX];
snprintf(tmp, sizeof tmp, "%s", path);
for (size_t i = 1; i <= strlen(tmp); i++) {
if (tmp[i] == '/' || tmp[i] == '\0') {
char ch = tmp[i];
tmp[i] = '\0';
if (mkdir(tmp, 0755) != 0) {
struct stat st;
if (stat(tmp, &st) != 0 || !S_ISDIR(st.st_mode)) return -1;
}
tmp[i] = ch;
}
}
return 0;
}
static int rm_rf(const char *path)
{
struct stat st;
if (lstat(path, &st) != 0) return -1;
if (!S_ISDIR(st.st_mode)) return unlink(path);
DIR *d = opendir(path);
if (!d) return -1;
struct dirent *de;
while ((de = readdir(d)) != NULL) {
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue;
char full[MR_PATH_MAX];
snprintf(full, sizeof full, "%s/%s", path, de->d_name);
rm_rf(full);
}
closedir(d);
return rmdir(path);
}
static void pump_ms(int ms)
{
for (int i = 0; i < ms / 10; i++) {
usleep(10 * 1000);
meow_anim_tick();
}
}
static void tap(int x, int y)
{
reader_on_touch_down(x, y);
reader_on_tap(x, y);
pump_ms(300); /* 越过双击判定窗口,让单击生效 */
}
static void draw_once(void) { meow_ui_paint_full(); }
int main(void)
{
const char *base = getenv("TMPDIR");
if (!base || !base[0]) base = ".";
char tmpl[512];
snprintf(tmpl, sizeof tmpl, "%s/mr_perf_XXXXXX", base);
char *data = mkdtemp(tmpl);
if (!data) { printf("SKIP: mkdtemp failed\n"); return 0; }
setenv("MEOW_DATA_DIR", data, 1);
char book[MR_PATH_MAX];
snprintf(book, sizeof book, "./mr_perf_books_%ld/v1", (long)getpid());
if (mkdir_p(book) != 0) { printf("SKIP: mkdir failed\n"); return 0; }
for (int i = 0; i < 4; i++) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/p%d.bmp", book, i + 1);
if (write_bmp24(p, 600, 800, (unsigned)i) != 0) { printf("SKIP: bmp write failed\n"); return 0; }
}
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { printf("SKIP: meow_ui_init failed (no host backend)\n"); return 0; }
static const meow_view_hooks lh = { 0 };
meow_view *lib = meow_view_create(ACT_LIBRARY, MEOW_VIEW_ACTIVITY, &lh, NULL);
meow_view *reader = reader_view_create();
meow_view_page_push(ACT_LIBRARY);
int w = meow_ui_width();
int fit_x = w - 10 - 48 * 3 + 24 + 48 * 2; /* 顶栏第 3 个动作 = fit */
int bar_y = 28;
/* ---- P1 进入:适宽 480x640 == 解码尺寸 → 纯 blit ---- */
mr_reader_open_dir(book);
CHECK(mr_reader_ui_count() == 4, "book pages=%d", mr_reader_ui_count());
pump_ms(700);
draw_once();
int b0 = mr_reader_ui_blit_draws(), s0 = mr_reader_ui_scaled_draws();
draw_once();
CHECK(mr_reader_ui_blit_draws() > b0, "page drawn with native blit");
CHECK(mr_reader_ui_scaled_draws() == s0,
"fit-width: zero resample per frame (scaled %d -> %d)",
s0, mr_reader_ui_scaled_draws());
/* ---- P2 连续翻页:预取命中,仍零重采样 ---- */
int bt = mr_reader_ui_blit_draws(), st = mr_reader_ui_scaled_draws();
int pages_ok = 1;
for (int i = 0; i < 10; i++) {
int before = mr_reader_ui_page();
/* RTL 默认:左 1/3=下一页、右 1/3=上一页;来回翻避免撞到首末页的册间衔接。 */
tap(i % 2 ? w - 40 : 40, 300);
pump_ms(350);
if (mr_reader_ui_page() == before) pages_ok = 0;
draw_once();
}
CHECK(pages_ok, "10 flips advanced the page each time");
CHECK(mr_reader_ui_blit_draws() - bt >= 10, "flips drew pages (%d blits)",
mr_reader_ui_blit_draws() - bt);
CHECK(mr_reader_ui_scaled_draws() == st,
"flip hot path: zero resample (scaled %d -> %d)", st, mr_reader_ui_scaled_draws());
/* ---- P3 切「适高」:按新几何重解后再测 ---- */
tap(fit_x, bar_y); /* 适宽 → 适高 */
pump_ms(600);
draw_once();
int s2 = mr_reader_ui_scaled_draws();
for (int i = 0; i < 5; i++) {
tap(i % 2 ? w - 40 : 40, 300);
pump_ms(350);
draw_once();
}
CHECK(mr_reader_ui_scaled_draws() == s2,
"fit-height: re-decoded page still zero resample (scaled %d -> %d)",
s2, mr_reader_ui_scaled_draws());
/* ---- P4 驻留内存上界 ---- */
long kb = mr_reader_ui_resident_kb();
printf(" info: resident=%ldKB blit=%d scaled=%d\n", kb,
mr_reader_ui_blit_draws(), mr_reader_ui_scaled_draws());
CHECK(kb <= 9 * 1024, "resident page images bounded (got %ldKB)", kb);
meow_view_page_pop();
meow_view_free(reader);
meow_view_free(lib);
meow_ui_shutdown();
rm_rf(book);
char rootdir[MR_PATH_MAX];
snprintf(rootdir, sizeof rootdir, "./mr_perf_books_%ld", (long)getpid());
rm_rf(rootdir);
rm_rf(data);
printf("%s: %s\n", g_fail ? "FAIL" : "PASS",
g_fail ? "see failures above" : "reader flip hot-path checks passed");
return g_fail ? 1 : 0;
}