Files
MeowOS-SDK/sdk/test_meow_img_budget.c
T
AskaEth 21bffa1aeb sdk: 图片层追加预算解码入口,JPEG 解码期缩解(峰值 43.7→14.0 MiB)
M6 finding(vuln/high):meow_img_load_file 必先整幅 RGBA 解码,1600×2400 漫画页
瞬时峰值 20.2 MiB(15.4 MiB 输出 + 5.5 MiB YCbCr 平面),超设备 ~14MB 口径,
App 侧无法规避。

调研(vendored stb v2.30):无解码期缩放能力——JPEG 走固定 hs/vs 重采样到全尺寸,
PNG 一次性 inflate 整幅 raw_image,公共 API 只有 load/load_16/loadf/info。
完整缩小 IDCT 需改编 1/2、1/4、1/8 IDCT + 进升级系数存储 + MCU 尺寸,改动面大且
易出静默像素错误,故不重写解码器。

改为受控的最小追加(sdk/stb_image.h):
- 新增 stbi_load_scaled / stbi_load_scaled_from_memory:JPEG 输出级按步长 s 抽取
  行/列(resample 状态机逐行推进,仅采样行做颜色转换),整幅原尺寸输出缓冲从不
  物化;scale<=1 与旧 API 逐字节等价,非 JPEG 忽略。
- 进升级 JPEG:各分量系数 IDCT 进像素平面后立即释放(原来拖到输出分配之后)。
- 旧公共 API 行为不变。

SDK 追加式入口(旧 API 一字未改):
- meow_img_load_file_dim / meow_img_load_mem_dim(path/buf, maxdim):stbi_info 头探测
  选 1/2、1/4、1/8 整数档(取「长边/s ≥ maxdim」最大档,不欠采样),再交既有
  meow_img_downscale 精修;小图/maxdim<=0 走旧路径。
- meow_img_probe_file:只读头取尺寸,供 App 在 OOM 前提示。

实测(宿主 test_meow_img_budget,--wrap malloc 打点,全部 PASS):
  8.32MP JPEG   43.70 → 13.96 MiB(新/旧 0.32)
  1600×2400 页  20.16 →  9.18 MiB
  progressive   43.70 → 35.83 MiB(系数阵列扫描期驻留,残余上限)
  PNG 2400×1600 25.65 → 25.65 MiB(stb 无 PNG 缩解,如实记录)
  BMP 2400×1600 14.65 → 15.23 MiB(同上,多一次降采样小缓冲)
缩解结果与「整幅解码+downscale」网格平均差 6.28;对 PIL 逐像素比对与全幅解码
同误差量级(mean ≤0.02, max ≤3)。

验证:build_sdk.sh 重编 meow_ui.a 0 warning;examples/hello 重链 0 warning;
tuku 用 MEOW_SDK_DIR 指向本分支 sdk 重链 0 warning;既有 test_meow_img_color 仍全 PASS。
2026-11-04 21:02:39 +08:00

465 lines
19 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_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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <sys/stat.h>
/* ------------------------------------------------------------------ */
/* 分配打点:--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;
}