/* test_meow_img_budget.c — M12 预算解码(meow_img_load_file_dim / _mem_dim)宿主验证。 * * 覆盖矩阵(对齐 M12 任务 3/4): * B1 探针:meow_img_probe_file 只读头取原始尺寸(8.32MP 图),坏图/不存在返回 0 * B2 小图不动:64x64 走旧路径,与 meow_img_load_file 逐字节相同;maxdim<=0 等价旧入口 * B3 长边约束:8.32MP baseline JPEG 在 maxdim=480/300/128/64 下结果两维都 ≤ maxdim,长宽比保持 * B4 峰值对比:同一 8.32MP JPEG「旧入口 vs 新入口」瞬时分配峰值(--wrap malloc 打点), * 断言新入口峰值 < 旧入口 1/2;B4b 极端 maxdim=100;B4c finding 原始场景 1600x2400 页 * B5 内容正确:新入口结果与「旧入口整幅解码 + meow_img_downscale」网格采样比对(平均差阈值) * B6 进升级:progressive JPEG 走同一缩解路径(峰值记录 + 长边约束) * B7 内存入口:meow_img_load_mem_dim 与文件入口等价(峰值 < 旧内存入口 1/2) * B8 坏图:坏 JPEG(魔数+垃圾)/ 空内存 / 不存在路径 → NULL(不崩) * B9 PNG/BMP:尺寸约束成立,峰值如实记录(stb 无解码期缩放 → 与旧入口同级) * * 夹具(生成一次): * python3 test_meow_img_budget_gen.py "$PREFIX/tmp/meow_img_budget" * 编译(本 sdk 目录下;--wrap 用于给 malloc/free 家族打点): * gcc -Wall -Wextra -I. -Iinclude -DMEOW_UI_HOST_TEST -o test_meow_img_budget \ * test_meow_img_budget.c meow_ui_img.c stb_image.c -lm \ * -Wl,--wrap=malloc -Wl,--wrap=calloc -Wl,--wrap=realloc -Wl,--wrap=free * 运行:./test_meow_img_budget "$PREFIX/tmp/meow_img_budget" (exit 0=PASS / 1=FAIL) * 夹具目录可省略:默认 $PREFIX/tmp/meow_img_budget;缺失时自动调 gen 脚本生成。 */ #define _GNU_SOURCE #include "meow_ui.h" #include "meow_ui_internal.h" /* meow_gfx_blit_rgba 原型(本测试只给桩) */ #include #include #include #include #include /* ------------------------------------------------------------------ */ /* 分配打点:--wrap malloc/free 家族,记录「存活字节」与峰值 */ /* ------------------------------------------------------------------ */ #define AHDR 32 /* 头部长度:保留原始大小,保证 16 字节对齐 */ static size_t g_live = 0; static size_t g_peak = 0; extern void *__real_malloc(size_t); extern void *__real_calloc(size_t, size_t); extern void *__real_realloc(void *, size_t); extern void __real_free(void *); static void acc_alloc(size_t n) { g_live += n; if (g_live > g_peak) g_peak = g_live; } void *__wrap_malloc(size_t n) { size_t *h = (size_t *)__real_malloc(n + AHDR); if (!h) return NULL; h[0] = n; acc_alloc(n); return (char *)h + AHDR; } void *__wrap_calloc(size_t a, size_t b) { size_t n = a * b; void *p = __wrap_malloc(n); if (p) memset(p, 0, n); return p; } void __wrap_free(void *p) { size_t *h; if (!p) return; h = (size_t *)((char *)p - AHDR); g_live -= h[0]; __real_free(h); } void *__wrap_realloc(void *p, size_t n) { size_t old = p ? ((size_t *)((char *)p - AHDR))[0] : 0; void *q = __wrap_malloc(n); if (!q) return NULL; if (p) { memcpy(q, p, old < n ? old : n); __wrap_free(p); } return q; } static size_t peak_reset(void) { g_peak = g_live; return g_live; } static size_t peak_since(size_t base) { return g_peak - base; } /* meow_ui_img.c 的绘制函数需要的 gfx 符号:本测试只测解码/内存,桩即可。 */ void meow_gfx_blit_rgba(int x, int y, int w, int h, const uint8_t *rgba, int alpha) { (void)x; (void)y; (void)w; (void)h; (void)rgba; (void)alpha; } /* ------------------------------------------------------------------ */ /* 断言与打印 */ /* ------------------------------------------------------------------ */ static int g_fail = 0; #define CHECK(cond, name) do { \ if (cond) printf(" PASS %s\n", name); \ else { printf(" FAIL %s (%s:%d)\n", name, __FILE__, __LINE__); g_fail++; } } while (0) static void peak_report(const char *tag, size_t bytes) { printf(" peak %-28s %8.2f MiB (%zu B)\n", tag, (double)bytes / (1024.0 * 1024.0), bytes); } /* ------------------------------------------------------------------ */ /* 路径与夹具 */ /* ------------------------------------------------------------------ */ static const char *g_dir = NULL; static char *path_join(const char *name) { size_t n = strlen(g_dir) + strlen(name) + 2; char *p = (char *)malloc(n); if (!p) { printf("OOM\n"); exit(1); } snprintf(p, n, "%s/%s", g_dir, name); return p; } static int file_exists(const char *p) { struct stat st; return stat(p, &st) == 0 && st.st_size > 0; } static void ensure_fixtures(void) { char *big = path_join("big.jpg"); if (file_exists(big)) { free(big); return; } free(big); printf("夹具缺失,尝试 python3 test_meow_img_budget_gen.py %s\n", g_dir); { char cmd[1024]; snprintf(cmd, sizeof cmd, "python3 test_meow_img_budget_gen.py '%s'", g_dir); if (system(cmd) != 0) printf(" 生成失败(缺 Pillow?)——请按头注释手动生成\n"); } } static uint8_t *read_whole(const char *p, size_t *len) { FILE *f = fopen(p, "rb"); uint8_t *buf; long n; if (!f) return NULL; fseek(f, 0, SEEK_END); n = ftell(f); fseek(f, 0, SEEK_SET); if (n <= 0) { fclose(f); return NULL; } buf = (uint8_t *)malloc((size_t)n); if (!buf) { fclose(f); return NULL; } if (fread(buf, 1, (size_t)n, f) != (size_t)n) { free(buf); fclose(f); return NULL; } fclose(f); *len = (size_t)n; return buf; } /* ------------------------------------------------------------------ */ /* 图像辅助 */ /* ------------------------------------------------------------------ */ static int nonblank(const meow_img *im) { int w = meow_img_w(im), h = meow_img_h(im); const unsigned char *p = meow_img_pixels(im); long sum = 0; if (!p || w <= 0 || h <= 0) return 0; for (long i = 0; i < (long)w * h; i += 97) sum += p[i * 4] + p[i * 4 + 1] + p[i * 4 + 2]; return sum > 0; } /* 按比例网格采样比对两图(尺寸可不同):返回各通道平均绝对差。 */ static double mean_abs_diff(const meow_img *a, const meow_img *b, int grid) { int wa = meow_img_w(a), ha = meow_img_h(a); int wb = meow_img_w(b), hb = meow_img_h(b); const unsigned char *pa = meow_img_pixels(a), *pb = meow_img_pixels(b); long acc = 0, cnt = 0; if (!pa || !pb || wa <= 0 || hb <= 0) return 1e9; for (int gy = 0; gy < grid; gy++) { int ya = (int)((long)gy * ha / grid); int yb = (int)((long)gy * hb / grid); for (int gx = 0; gx < grid; gx++) { int xa = (int)((long)gx * wa / grid); int xb = (int)((long)gx * wb / grid); const unsigned char *qa = pa + ((size_t)ya * wa + xa) * 4; const unsigned char *qb = pb + ((size_t)yb * wb + xb) * 4; for (int k = 0; k < 3; k++) { int d = (int)qa[k] - (int)qb[k]; acc += d < 0 ? -d : d; cnt++; } } } return cnt ? (double)acc / (double)cnt : 1e9; } /* ================================================================== */ int main(int argc, char **argv) { char *p_big, *p_prog, *p_small, *p_png, *p_bmp, *p_broken, *p_missing; size_t peak_old, peak_new; if (argc > 1) { g_dir = argv[1]; } else { /* Termux 的 /tmp 不可写:优先 $TMPDIR,其次 $PREFIX/tmp。 */ static char dflt[600]; const char *tmp = getenv("TMPDIR"); const char *pre = getenv("PREFIX"); if (tmp && *tmp) snprintf(dflt, sizeof dflt, "%s/meow_img_budget", tmp); else if (pre && *pre) snprintf(dflt, sizeof dflt, "%s/tmp/meow_img_budget", pre); else snprintf(dflt, sizeof dflt, "/tmp/meow_img_budget"); g_dir = dflt; } ensure_fixtures(); p_big = path_join("big.jpg"); p_prog = path_join("bigprog.jpg"); p_small = path_join("small.jpg"); p_png = path_join("big.png"); p_bmp = path_join("big.bmp"); p_broken = path_join("broken.jpg"); p_missing = path_join("no_such_file.jpg"); if (!file_exists(p_big)) { printf("FATAL 夹具 %s 不存在(先跑 test_meow_img_budget_gen.py)\n", p_big); return 1; } /* ---- B1 探针 ---- */ printf("[B1] 头探测(零像素内存)\n"); { int w = 0, h = 0, c = 0; CHECK(meow_img_probe_file(p_big, &w, &h, &c) == 1, "probe 成功"); CHECK(w == 3200 && h == 2600, "probe 原始尺寸 3200x2600"); CHECK(c == 3, "probe 通道数 3"); printf(" probe → %dx%d ch=%d\n", w, h, c); CHECK(meow_img_probe_file(p_broken, NULL, NULL, NULL) == 0, "probe 截断图 → 0"); CHECK(meow_img_probe_file(p_missing, NULL, NULL, NULL) == 0, "probe 不存在 → 0"); CHECK(meow_img_probe_file(NULL, NULL, NULL, NULL) == 0, "probe NULL → 0"); } /* ---- B2 小图不动 ---- */ printf("[B2] 小图不动 / maxdim<=0 等价旧入口\n"); { meow_img *a = meow_img_load_file(p_small); meow_img *b = meow_img_load_file_dim(p_small, 64); meow_img *z = meow_img_load_file_dim(p_small, 0); CHECK(a && b && z, "小图三条路径都成功"); CHECK(meow_img_w(b) == meow_img_w(a) && meow_img_h(b) == meow_img_h(a), "maxdim=64 → 尺寸不变"); CHECK(b && a && meow_img_w(a) * meow_img_h(a) > 0 && memcmp(meow_img_pixels(a), meow_img_pixels(b), (size_t)meow_img_w(a) * meow_img_h(a) * 4) == 0, "小图逐字节与旧入口一致"); CHECK(z && a && memcmp(meow_img_pixels(a), meow_img_pixels(z), (size_t)meow_img_w(a) * meow_img_h(a) * 4) == 0, "maxdim=0 等价旧入口(逐字节)"); meow_img_free(a); meow_img_free(b); meow_img_free(z); } /* ---- B3 长边约束 ---- */ printf("[B3] 8.32MP baseline JPEG 长边约束\n"); { static const int dims[] = { 480, 300, 128, 64 }; int wi = 0, hi = 0; meow_img *full = meow_img_load_file(p_big); CHECK(full != NULL, "旧入口解码成功"); if (full) { wi = meow_img_w(full); hi = meow_img_h(full); } printf(" 原图 %dx%d (%.2f MP)\n", wi, hi, (double)wi * hi / 1e6); for (size_t i = 0; i < sizeof dims / sizeof dims[0]; i++) { meow_img *s = meow_img_load_file_dim(p_big, dims[i]); char name[96]; snprintf(name, sizeof name, "maxdim=%d → 两维 ≤ maxdim 且非空", dims[i]); CHECK(s && meow_img_w(s) <= dims[i] && meow_img_h(s) <= dims[i] && nonblank(s), name); if (s) { double ar0 = (double)wi / hi, ar1 = (double)meow_img_w(s) / meow_img_h(s); CHECK(ar1 > ar0 * 0.98 && ar1 < ar0 * 1.02, "长宽比保持(±2%)"); printf(" maxdim=%-4d → %dx%d\n", dims[i], meow_img_w(s), meow_img_h(s)); } meow_img_free(s); } meow_img_free(full); } /* ---- B4 峰值对比(JPEG) ---- */ printf("[B4] 瞬时分配峰值:旧入口 vs 新入口(同一 8.32MP JPEG)\n"); { size_t base = peak_reset(); meow_img *old = meow_img_load_file(p_big); peak_old = peak_since(base); base = peak_reset(); meow_img *nw = meow_img_load_file_dim(p_big, 480); peak_new = peak_since(base); CHECK(old && nw, "两条路径都成功"); peak_report("旧入口 meow_img_load_file", peak_old); peak_report("新入口 _dim(480)", peak_new); printf(" 整幅 RGBA 理论值 = %.2f MiB;新/旧 = %.2f\n", (double)meow_img_w(old) * meow_img_h(old) * 4 / (1024.0 * 1024.0), (double)peak_new / (double)peak_old); CHECK(peak_new * 2 < peak_old, "新入口峰值 < 旧入口 1/2(不物化整幅 RGBA)"); CHECK(peak_new < (size_t)20 * 1024 * 1024, "新入口峰值 < 20 MiB(8.32MP 页可加载)"); /* B5 内容正确性:新 vs 旧整幅 + downscale 精修 */ if (old && nw) { meow_img *ref = meow_img_downscale(old, 480); double d = mean_abs_diff(nw, ref ? ref : old, 64); printf(" 与「旧入口+downscale」网格平均差 = %.2f\n", d); CHECK(d < 20.0, "缩解内容结构一致(平均差 < 20)"); meow_img_free(ref); } meow_img_free(old); meow_img_free(nw); } /* ---- B4b 极端 maxdim ---- */ printf("[B4b] maxdim=100(8 倍缩解档位 + 精修)\n"); { size_t base = peak_reset(); meow_img *s = meow_img_load_file_dim(p_big, 100); size_t peak = peak_since(base); CHECK(s && meow_img_w(s) <= 100 && meow_img_h(s) <= 100 && nonblank(s), "结果 ≤100 且非空"); peak_report("_dim(100)", peak); CHECK(peak * 2 < peak_old, "峰值仍 < 旧入口 1/2"); meow_img_free(s); } /* ---- B4c finding 原始场景 ---- */ printf("[B4c] finding 原始场景:1600x2400 漫画页\n"); { char *p_page = path_join("page.jpg"); if (file_exists(p_page)) { size_t base = peak_reset(); meow_img *old = meow_img_load_file(p_page); size_t p_old = peak_since(base); base = peak_reset(); meow_img *nw = meow_img_load_file_dim(p_page, 720); size_t p_new = peak_since(base); CHECK(old && nw, "1600x2400 两条路径都成功"); if (nw) CHECK(meow_img_w(nw) <= 720 && meow_img_h(nw) <= 720 && nonblank(nw), "结果 ≤720 且非空"); if (old) printf(" 整幅 RGBA 理论值 = %.2f MiB\n", (double)meow_img_w(old) * meow_img_h(old) * 4 / (1024.0 * 1024.0)); peak_report("旧入口 page.jpg", p_old); peak_report("新入口 _dim(720)", p_new); CHECK(p_new < (size_t)10 * 1024 * 1024, "新入口峰值 < 10 MiB(设备 ~14MB 口径内)"); meow_img_free(old); meow_img_free(nw); } else { printf(" SKIP page.jpg(夹具缺失)\n"); } free(p_page); } /* ---- B6 进升级 JPEG ---- */ printf("[B6] progressive JPEG(同一缩解路径)\n"); { size_t base = peak_reset(); meow_img *old = meow_img_load_file(p_prog); size_t p_old = peak_since(base); base = peak_reset(); meow_img *nw = meow_img_load_file_dim(p_prog, 480); size_t p_new = peak_since(base); CHECK(old && nw, "进升级两条路径都成功"); CHECK(nw && meow_img_w(nw) <= 480 && meow_img_h(nw) <= 480 && nonblank(nw), "进升级结果 ≤480 且非空"); peak_report("旧入口 progressive", p_old); peak_report("新入口 _dim(480) progressive", p_new); CHECK(p_new < p_old, "进升级峰值下降(系数阵列仍在扫描期驻留,见报告)"); meow_img_free(old); meow_img_free(nw); } /* ---- B7 内存入口 ---- */ printf("[B7] 内存入口 meow_img_load_mem_dim(CBZ/档案页场景)\n"); { size_t len = 0; uint8_t *buf = read_whole(p_big, &len); CHECK(buf != NULL, "读入 8.32MP JPEG 到内存"); if (buf) { size_t base = peak_reset(); meow_img *old = meow_img_load_mem(buf, (int)len); size_t p_old = peak_since(base); base = peak_reset(); meow_img *nw = meow_img_load_mem_dim(buf, (int)len, 480); size_t p_new = peak_since(base); CHECK(old && nw, "内存入口两条路径都成功"); CHECK(nw && meow_img_w(nw) <= 480 && nonblank(nw), "内存入口结果 ≤480 且非空"); peak_report("旧 meow_img_load_mem", p_old); peak_report("新 meow_img_load_mem_dim", p_new); CHECK(p_new * 2 < p_old, "内存入口峰值 < 旧入口 1/2"); meow_img_free(old); meow_img_free(nw); free(buf); } } /* ---- B8 坏图 ---- */ printf("[B8] 坏图容错\n"); { size_t len = 0; uint8_t *buf; CHECK(meow_img_load_file_dim(p_broken, 480) == NULL, "截断 JPEG → NULL"); CHECK(meow_img_load_file_dim(p_missing, 480) == NULL, "不存在路径 → NULL"); CHECK(meow_img_load_file_dim(NULL, 480) == NULL, "NULL 路径 → NULL"); buf = read_whole(p_broken, &len); CHECK(buf && meow_img_load_mem_dim(buf, (int)len, 480) == NULL, "截断 JPEG 内存入口 → NULL"); free(buf); CHECK(meow_img_load_mem_dim("x", 3, 480) == NULL, "垃圾内存 → NULL"); } /* ---- B9 PNG / BMP(stb 无解码期缩放,如实记录峰值) ---- */ printf("[B9] PNG / BMP:尺寸约束成立,峰值如实记录(stb 无解码期缩放)\n"); { struct { const char *tag; const char *path; } fx[] = { { "PNG", p_png }, { "BMP", p_bmp }, }; for (size_t i = 0; i < sizeof fx / sizeof fx[0]; i++) { if (!file_exists(fx[i].path)) { printf(" SKIP %s(夹具缺失)\n", fx[i].tag); continue; } { size_t base = peak_reset(); meow_img *old = meow_img_load_file(fx[i].path); size_t p_old = peak_since(base); meow_img *nw; char name[96]; base = peak_reset(); nw = meow_img_load_file_dim(fx[i].path, 480); { size_t p_new = peak_since(base); snprintf(name, sizeof name, "%s _dim(480) 两维 ≤480 且非空", fx[i].tag); CHECK(nw && meow_img_w(nw) <= 480 && meow_img_h(nw) <= 480 && nonblank(nw), name); peak_report(fx[i].tag, p_old); peak_report(" ↑ 新入口", p_new); snprintf(name, sizeof name, "%s 峰值与旧入口同级(stb 无解码期缩放)", fx[i].tag); CHECK(p_new <= p_old + p_old / 8, name); if (old) printf(" %s 原图 %dx%d\n", fx[i].tag, meow_img_w(old), meow_img_h(old)); meow_img_free(nw); } meow_img_free(old); } } } if (g_fail) printf("\nRESULT: FAIL (%d)\n", g_fail); else printf("\nRESULT: ALL PASS\n"); free(p_big); free(p_prog); free(p_small); free(p_png); free(p_bmp); free(p_broken); free(p_missing); return g_fail ? 1 : 0; }