Files
MeowOS-SDK/tuku/test/test_reader_many_pages.c
T
AskaEth 1725af631b 图库: 门控模型接真缩解后端 + _step 全格式后端(M27)
M23 交付了真缩解后端却「修好了用不上」:_step 只接 JPEG,而阅读器的峰值模型
仍是 stb 口径(PNG/BMP 按 W·H·4、JPEG 按缩解后整幅 RGBA),12MP/PNG 页全被判
超预算占位。本次把两处缝焊上:

- sdk/meow_ui_img.c:meow_img_load_file_step 的 JPEG-only 限制放开到 PNG/BMP;
  新增 meow_img_load_file_step_ex 回报实际路径(backend/stb),日志可核对预判。
- tuku/logic/reader/page_source.[ch]:新增 reader_estimate_peak(),成为峰值模型 +
  预算 + MemAvailable 门控的唯一实现(reader 不再另写一份)。
  * 后端命中(JPEG SOF0/1/2、≤3 分量、8bit、N=1..7;PNG 非交错/标准压缩/合法色型;
    BMP DIB≥40/BI_RGB/8·16·24·32bpp)→ 峰值 = 目标图 + 分量平面/行缓冲 + 固定开销,
    与源分辨率无关 → 直接放行,不门控;
  * 只有回退 stb(CMYK/算术编码、Adam7、RLE/1-4bpp、N≥8 小图等)才按 M22 stb 模型
    算峰值,超 3MB 或 MemAvailable <6MB 才占位。Adam7 另补 stb 的整幅暂存上界。
- reader:删掉本地 stb 模型/常量,统一调 reader_estimate_peak;日志加 path=backend|stb。
- 测试:6 类典型页(2000×3000 baseline/渐进/restart、1600×2400 PNG/BMP、4MP、12MP)
  断言全部 backend + 不占位 + 估算 ≥ 实测且 ≤1.5×(实测 1.03–1.08×);两条路径输出
  尺寸逐像素一致;CMYK/Adam7/1bpp 断言走 stb 并按旧门控占位;连翻 30 页无占位、
  单页分配峰值 ≤3MB;membudget 链接补 meow_ui_time.c 与 M23 两个解码 TU。
2026-11-06 11:46:16 +08:00

322 lines
13 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, 1); /* bpp=1:M23 后端不支持 → 回退 stb(M27 门控仍生效) */
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());
/* M22/M27:坏图 = 永久占位(failed);超预算的**回退路径**大页 = 预算跳过(skipped,
* 可重试,不是 failed)。M27 起后端能接住的 24bpp 大图会直接解出,所以这里用 1bpp
* (后端拒绝 → stb)保证仍在验证门控,而不是「大图一律占位」的旧语义。 */
CHECK(mr_reader_ui_failed_pages() == 1,
"garbage page is a permanent placeholder (failed=%d)", mr_reader_ui_failed_pages());
CHECK(mr_reader_ui_skipped_pages() == 1,
"oversized page is a retryable budget skip (skipped=%d)", mr_reader_ui_skipped_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;
}