Files
MeowOS-SDK/sdk/test_meow_img_budget.c
T
AskaEth d84dd949c7 SDK: 图片解码真缩解后端 —— JPEG DCT 域 / PNG 逐行 / BMP 跳行(M23)
stb 的 stbi_load_scaled 是「先整幅解、再重采样」:2000x3000 baseline 峰值 10.07MB、
渐进式 25.84MB、1600x2400 PNG 25.65MB,真机解码余量只有 ~4.2MB。新增解码后端在
解码期只算目标分辨率,峰值只与输出尺寸相关:

- meow_jpeg.c:自写纯 C JPEG 解码(baseline + progressive,Huffman)。按
  ceil(8*maxdim/长边) 选 N 点 DCT 缩解(N=1..7),基函数为「8 点 IDCT 按像素区间
  盒式平均」的 Q12 定点(DC 恒为 F00/8,N 之间亮度一致);系数边解边累加进分量平面,
  不留整幅系数阵列;渐进式跳过精细化扫描(Ah>0)、只解各频带首次扫描 —— 渐进式与
  baseline 内存足迹相同(这是 libjpeg 系列做不到的:它对渐进式必须整幅驻留 JBLOCK)。
  不支持项(4 分量 CMYK/YCCK、算术/无损/分层、12bit 精度)返回 0 回退 stb。
- meow_png_stream.c:libz 逐行 inflate + 反滤波,只把需要的列写进目标行;
  gray/gray+A/RGB/RGBA/调色板,1/2/4/8/16bit(16→8 取高字节);Adam7 等回退 stb。
  BMP 按目标行 seek + 行内跳列(8bpp 调色板/16bpp RGB555/24/32bpp),不读全图。
- meow_ui_img.c:load_file_dim / load_mem_dim 先进后端,失败/不支持回落 stb 旧路径
  (stbi_load_scaled + 精修);输出尺寸与 meow_img_downscale 同公式,对外语义不变。

宿主实测(--wrap malloc,maxdim=720,M23 前后同一夹具):
  2000x3000 baseline     10.07 → 1.86 MiB
  2000x3000 progressive  25.84 → 1.86 MiB
  1600x2400 PNG          25.65 → 1.33 MiB
  1600x2400 BMP          15.97 → 1.32 MiB
  4MP/8MP/12MP JPEG       9.67/13.37/20.12 → 2.30/2.20/2.29 MiB(不随分辨率线性增长)

sdk/test_meow_img_stream.c:峰值断言(≤2.5MiB)、与 stb 整幅+降采样的像素比对
(平均差 4.4-13.7/255,块均值结构相关 ≥0.88)、内存入口逐字节一致、回退矩阵
(CMYK/1bpp BMP/Adam7 PNG)、restart marker、缩解阶扫描 N=4/5/7、坏图容错。
build_sdk.sh 增加 meow_jpeg / meow_png_stream 两个 TU(MIPS32 -Os 零警告)。
体积:目标码 +24.9KB(meow_ui.a +25.5KB),picbrowser 静态二进制 +18.2KB。
2026-11-05 21:33:54 +08:00

467 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:尺寸约束成立;M23 起 PNG/BMP 也走解码期缩解(逐行 inflate / 按行 seek),
* 峰值应显著低于旧入口(本用例只断言 ≤ maxdim + 非空,并打印峰值供报告引用)
*
* 夹具(生成一次):
* python3 test_meow_img_budget_gen.py "$PREFIX/tmp/meow_img_budget"
* 编译(本 sdk 目录下;--wrap 用于给 malloc/free 家族打点;-lz 供 PNG 后端 inflate):
* gcc -Wall -Wextra -I. -Iinclude -DMEOW_UI_HOST_TEST -o test_meow_img_budget \
* test_meow_img_budget.c meow_ui_img.c meow_jpeg.c meow_png_stream.c stb_image.c \
* -lm -lz \
* -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(M23 起也走解码期缩解;只断言尺寸/非空,峰值打印) ---- */
printf("[B9] PNG / BMP:尺寸约束成立,峰值记录(M23 逐行/跳行缩解,应远低于旧入口)\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 新入口峰值 ≤ 旧入口(不再整幅物化)", fx[i].tag);
CHECK(p_new <= p_old, 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;
}