Files
MeowOS-SDK/tuku/test/test_reader_many_pages.c
T
AskaEth ceb65dfd01 图库: 修条漫长页/竖翻每帧重采样(解码几何对齐排版)+ 判据与活锁(M17 评审 P1-1/P2)
评审用真实 UI 链路实测:600x960、800x1280(长宽比 1.6)在翻页拖动与条漫模式
都是 blit+0 / scaled+5;「先适高再切条漫」scaled+10。根因是解码几何来源与排版几何
不同源:decode_long_side() 把长边钳到屏长边 720(600x960 解成 450x720),而
strip_page_height() 按屏宽算出 480x768;mode_set() 又不动 g_fit,适高/原始进条漫
100% 重采样。

修复:
- 解码目标 = 排版真正要的显示框:新增 page_geometry_at()(条漫=strip 高度、宽=视口宽;
  翻页=fit/zoom),解码计划与绘页/就绪判据同源。
- plan_decode_long() 去掉屏长边钳制,只保留 MR_PAGE_BYTES_CAP(3MB) 硬上界,
  超长条漫按比例收缩(不再因为「高度 > 屏长边」就永远对不上)。
- slot 记录本次解码的 want 几何;slot_ready() 改为几何比较:
  fit/缩放/模式变化 → 重解一次(放大再缩回、适高→条漫回到纯 blit);
  被 3MB 钳住的解码 want 不变 → 不再反复重解(大屏活锁)。
- 解码几何随 page_req 下发、随 page_res 返回:worker 不再读 g_fit/g_zoom/g_vw/g_vh。
- 峰值估算把 downscale/缩解输出一并计入(P2-3)。
- 适宽长图(竖向单独溢出)不再算平移模式:1.6 比例页原先左右 tap/横拖会变成
  「水平平移 0px」的空操作,页面根本翻不动;现在仍按翻页处理(原始档/横向溢出不变)。
- 窗口不再主动清窗口外的页图:0→1→0 来回翻不再反复重解同一页(140 页测试
  decode 次数 21→14);容量仍由 MR_SLOTS + 字节淘汰兜底。
- state_reset 复位 g_meta/g_start_at(起始页在 on_enter 入口先取走),删重复注释。

测试:test_reader_perf 夹具改成 600x960(1.6)并补 800x1280、翻页拖动、条漫、
「适高→条漫」、双击放大缩回、静止无重解、大屏 1080x1920 无活锁;test_reader_many_pages
补 meow_ui_paint_full(绘制路径覆盖,P2-5)。
2026-11-05 18:28:52 +08:00

318 lines
12 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* test_reader_many_pages.c — 多页(140 张)文件夹打开/翻页/跳页回归 +「从第 N 张开始」API。
*
* 用户反馈:「文件夹里图片过多时(例如 140 个),APP 会直接闪退」;且后续书库要改成文件
* 管理器模式,需要「从选中的那张开始看」。本测试走真实 SDK UI 链路(view/activity/anim/
* 真实解码),断言:
* M1 140 页册可打开,页数/模式正确(翻页模式、第 1 页);
* M2 mr_reader_open_dir_at(dir, 57):从第 57 页(0 基)开始;越界 9999 → 末页;负数 → 首页;
* M3 显式下标优先于续读记录;mr_reader_open_dir 仍走续读记录(旧语义不变);
* M4 连续翻页穿过坏图(解码失败)不崩、不卡死,坏页给占位后继续翻;
* M5 条漫模式:140 页排版/跳页/滚动可用;
* M6 常驻页图有字节上界(<=9MB)——页数增长不会线性吃内存。
*
* 编译: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>
#define BOOK_PAGES 140
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);
}
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 + seed) & 0xFF);
row[x * 3 + 2] = (unsigned char)(seed & 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 write_garbage(const char *path)
{
static const unsigned char junk[64] = { 'N', 'O', 'T', 'A', 'N', 'I', 'M', 'G' };
FILE *f = fopen(path, "wb");
if (!f) return -1;
int ok = fwrite(junk, 1, sizeof junk, f) == sizeof junk;
if (fclose(f) != 0) ok = 0;
return ok ? 0 : -1;
}
/* 合法 BMP24 头(声称 3000x3000,无像素数据):stbi_info 能读到尺寸,
* 整幅 RGBA 峰值 36MB 必须被 MR_DECODE_PEAK_CAP(12MB) 在解码前拦下
* (旧代码的 raw 上限是 40MB,这种页会被放行 → 真机上瞬时分配即可能被 OOM 杀掉)。 */
static int write_huge_bmp_header(const char *path)
{
unsigned char b[54] = { 0 };
b[0] = 'B'; b[1] = 'M';
put32(b + 2, 54); /* 文件大小(谎报,只为通过头检查) */
put32(b + 10, 54); /* 像素数据偏移 */
put32(b + 14, 40); /* BITMAPINFOHEADER */
put32(b + 18, 3000); /* width */
put32(b + 22, 3000); /* height */
put16(b + 26, 1); /* planes */
put16(b + 28, 24); /* bpp */
FILE *f = fopen(path, "wb");
if (!f) return -1;
int ok = fwrite(b, 1, sizeof b, f) == sizeof b;
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 int tap(int x, int y)
{
reader_on_touch_down(x, y);
reader_on_tap(x, y);
pump_ms(300);
return 1;
}
int main(void)
{
const char *base = getenv("TMPDIR");
if (!base || !base[0]) base = ".";
char tmpl[512];
snprintf(tmpl, sizeof tmpl, "%s/mr_many_XXXXXX", base);
char *data = mkdtemp(tmpl);
if (!data) { printf("SKIP: mkdtemp failed\n"); return 0; }
setenv("MEOW_DATA_DIR", data, 1);
char root[MR_PATH_MAX], book[MR_PATH_MAX], bad[MR_PATH_MAX];
snprintf(root, sizeof root, "./mr_many_books_%ld", (long)getpid());
snprintf(book, sizeof book, "%s/v140", root);
snprintf(bad, sizeof bad, "%s/vbad", root);
if (mkdir_p(book) != 0 || mkdir_p(bad) != 0) { printf("SKIP: mkdir failed\n"); return 0; }
for (int i = 0; i < BOOK_PAGES; i++) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/p%03d.bmp", book, i + 1);
if (write_bmp24(p, 200, 267, (unsigned)i) != 0) { printf("SKIP: bmp write failed\n"); return 0; }
}
/* 坏册:第 2 页是垃圾字节(解码失败 → 占位,不许崩/死循环)。 */
for (int i = 0; i < 4; i++) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/p%d.bmp", bad, i + 1);
if (i == 1) { if (write_garbage(p) != 0) { printf("SKIP: write failed\n"); return 0; } }
else if (i == 2) { if (write_huge_bmp_header(p) != 0) { printf("SKIP: write failed\n"); return 0; } }
else if (write_bmp24(p, 200, 267, (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 mode_x = w - 10 - 48 * 3 + 24;
int jump_x = w - 10 - 48 * 2 + 24;
int bar_y = 28;
int slider_y = 56 + 4 + 20;
int sx = 24, sw = w - 48;
/* ---- M1 140 页可打开 ---- */
mr_reader_open_dir_at(book, 0);
CHECK(meow_ui_activity_active() == ACT_READER, "reader active");
CHECK(mr_reader_ui_count() == BOOK_PAGES, "pages=%d (want %d)",
mr_reader_ui_count(), BOOK_PAGES);
CHECK(mr_reader_ui_page() == 0, "start at page 0 (got %d)", mr_reader_ui_page());
pump_ms(600);
int draws0 = mr_reader_ui_blit_draws() + mr_reader_ui_scaled_draws();
meow_ui_paint_full();
CHECK(mr_reader_ui_blit_draws() + mr_reader_ui_scaled_draws() > draws0,
"draw path executed (paged, 140p book)");
/* ---- M2 从第 N 张开始 / 越界钳制 ---- */
meow_view_page_pop();
mr_reader_open_dir_at(book, 57);
CHECK(mr_reader_ui_page() == 57, "open_dir_at(57) -> page %d", mr_reader_ui_page());
pump_ms(400);
meow_view_page_pop();
mr_reader_open_dir_at(book, 9999);
CHECK(mr_reader_ui_page() == BOOK_PAGES - 1, "open_dir_at(9999) clamps to last (%d)",
mr_reader_ui_page());
pump_ms(400);
meow_view_page_pop();
mr_reader_open_dir_at(book, -5);
CHECK(mr_reader_ui_page() == 0, "open_dir_at(-5) clamps to first (%d)", mr_reader_ui_page());
pump_ms(300);
meow_view_page_pop();
/* ---- M3 显式下标优先于续读;open_dir 保持续读语义 ---- */
mr_reader_open_dir_at(book, 77); /* 离开时会把 77 落盘 */
pump_ms(300);
meow_view_page_pop();
mr_reader_open_dir_at(book, 12); /* 显式下标赢过续读记录 77 */
CHECK(mr_reader_ui_page() == 12, "explicit start beats resume record (%d)",
mr_reader_ui_page());
pump_ms(300);
meow_view_page_pop();
mr_reader_open_dir(book); /* 旧入口 = 续读 */
CHECK(mr_reader_ui_page() == 12, "open_dir resumes saved page (%d)", mr_reader_ui_page());
pump_ms(300);
/* ---- M4 连续翻页(含异步解码)不崩、页码推进、内存有界 ---- */
int ok_move = 1;
for (int i = 0; i < 12; i++) {
int before = mr_reader_ui_page();
tap(40, 300);
pump_ms(260);
meow_ui_paint_full(); /* 绘制路径覆盖(P2-5) */
if (mr_reader_ui_page() == before) ok_move = 0;
}
CHECK(mr_reader_ui_blit_draws() + mr_reader_ui_scaled_draws() > draws0,
"draw path executed across flips");
CHECK(ok_move, "12 consecutive flips advanced the page");
CHECK(mr_reader_ui_resident_kb() <= 9 * 1024, "resident bounded on 140p book (%ldKB)",
mr_reader_ui_resident_kb());
/* 坏图册:翻过坏页不崩、可继续翻。 */
meow_view_page_pop();
mr_reader_open_dir_at(bad, 0);
pump_ms(600);
int bad_ok = 1;
for (int i = 0; i < 5; i++) {
int before = mr_reader_ui_page();
tap(40, 300);
pump_ms(300);
if (mr_reader_ui_page() == before && before < 3) bad_ok = 0;
}
CHECK(bad_ok, "flip across corrupt/huge pages keeps working (page=%d)", mr_reader_ui_page());
CHECK(mr_reader_ui_resident_kb() <= 9 * 1024,
"huge page skipped without allocating it (%ldKB)", mr_reader_ui_resident_kb());
CHECK(mr_reader_ui_failed_pages() == 2,
"garbage + oversized page both degraded to placeholder (failed=%d)",
mr_reader_ui_failed_pages());
CHECK(meow_ui_activity_active() == ACT_READER, "still in reader after corrupt page");
/* ---- M5 条漫:140 页排版 + 跳页落位 ---- */
meow_view_page_pop();
mr_reader_open_dir_at(book, 0);
pump_ms(400);
CHECK(mr_reader_ui_count() == BOOK_PAGES, "back on the 140p book (%d)", mr_reader_ui_count());
CHECK(tap(mode_x, bar_y) == 1, "mode icon consumed");
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "webtoon mode on 140 pages");
pump_ms(500);
CHECK(tap(jump_x, bar_y) == 1, "jump icon consumed");
CHECK(mr_reader_ui_jump_open() == 1, "slider open");
reader_on_touch_down(sx + sw, slider_y);
reader_on_tap(sx + sw, slider_y);
pump_ms(900);
CHECK(mr_reader_ui_jump_pending() == 0, "jump landed in webtoon");
{
int d0 = mr_reader_ui_blit_draws() + mr_reader_ui_scaled_draws();
meow_ui_paint_full();
meow_ui_paint_full(); /* 条漫绘制路径覆盖(P2-5) */
CHECK(mr_reader_ui_blit_draws() + mr_reader_ui_scaled_draws() > d0,
"draw path executed (webtoon)");
}
CHECK(mr_reader_ui_page() >= BOOK_PAGES - 3, "jumped near last page (got %d)",
mr_reader_ui_page());
CHECK(mr_reader_ui_resident_kb() <= 9 * 1024, "resident bounded in webtoon (%ldKB)",
mr_reader_ui_resident_kb());
meow_view_page_pop();
meow_view_free(reader);
meow_view_free(lib);
meow_ui_shutdown();
rm_rf(root);
rm_rf(data);
printf("%s: %s\n", g_fail ? "FAIL" : "PASS",
g_fail ? "see failures above" : "many-pages reader checks passed");
return g_fail ? 1 : 0;
}