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。
This commit is contained in:
AskaEth committed 2026-11-04 21:02:39 +08:00
1 parent 5f40d6fd87
commit 21bffa1aeb
5 files changed
+684 -12

No files matched your search

+80 -12
View File
@@ -429,6 +429,14 @@ STBIDEF stbi_uc *stbi_load_from_file (FILE *f, int *x, int *y, int *channels_in
// for stbi_load_from_file, file pointer is left pointing immediately after image
#endif
// 追加(MeowOS SDK M12):JPEG 解码期降采样。scale>=2 时按 s 抽取输出行/列,
// 不物化整幅原尺寸输出缓冲(*x,*y 返回 ceil(原尺寸/s))。scale<=1 与
// stbi_load / stbi_load_from_memory 完全等价;非 JPEG 格式忽略 scale。
STBIDEF stbi_uc *stbi_load_scaled_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels, int scale);
#ifndef STBI_NO_STDIO
STBIDEF stbi_uc *stbi_load_scaled (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels, int scale);
#endif
#ifndef STBI_NO_GIF
STBIDEF stbi_uc *stbi_load_gif_from_memory(stbi_uc const *buffer, int len, int **delays, int *x, int *y, int *z, int *comp, int req_comp);
#endif
@@ -907,7 +915,7 @@ typedef struct
#ifndef STBI_NO_JPEG
static int stbi__jpeg_test(stbi__context *s);
static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri);
static void *stbi__jpeg_load_ex(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int scale);
static int stbi__jpeg_info(stbi__context *s, int *x, int *y, int *comp);
#endif
@@ -1134,7 +1142,7 @@ STBIDEF void stbi_set_flip_vertically_on_load_thread(int flag_true_if_should_fli
: stbi__vertically_flip_on_load_global)
#endif // STBI_THREAD_LOCAL
static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc)
static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc, int jpeg_scale)
{
memset(ri, 0, sizeof(*ri)); // make sure it's initialized if we add new fields
ri->bits_per_channel = 8; // default is 8 so most paths don't have to be changed
@@ -1165,7 +1173,9 @@ static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int re
// bytes matching expectations; these are prone to false positives, so
// try them later
#ifndef STBI_NO_JPEG
if (stbi__jpeg_test(s)) return stbi__jpeg_load(s,x,y,comp,req_comp, ri);
if (stbi__jpeg_test(s)) return stbi__jpeg_load_ex(s,x,y,comp,req_comp, ri, jpeg_scale);
#else
STBI_NOTUSED(jpeg_scale);
#endif
#ifndef STBI_NO_PNM
if (stbi__pnm_test(s)) return stbi__pnm_load(s,x,y,comp,req_comp, ri);
@@ -1257,10 +1267,10 @@ static void stbi__vertical_flip_slices(void *image, int w, int h, int z, int byt
}
#endif
static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x, int *y, int *comp, int req_comp)
static unsigned char *stbi__load_and_postprocess_8bit_ex(stbi__context *s, int *x, int *y, int *comp, int req_comp, int jpeg_scale)
{
stbi__result_info ri;
void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 8);
void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 8, jpeg_scale);
if (result == NULL)
return NULL;
@@ -1283,10 +1293,15 @@ static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x,
return (unsigned char *) result;
}
static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x, int *y, int *comp, int req_comp)
{
return stbi__load_and_postprocess_8bit_ex(s, x, y, comp, req_comp, 1);
}
static stbi__uint16 *stbi__load_and_postprocess_16bit(stbi__context *s, int *x, int *y, int *comp, int req_comp)
{
stbi__result_info ri;
void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 16);
void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 16, 1);
if (result == NULL)
return NULL;
@@ -1409,6 +1424,19 @@ STBIDEF stbi_us *stbi_load_16(char const *filename, int *x, int *y, int *comp, i
return result;
}
// 追加(M12):JPEG 解码期降采样(见头文件声明)。非 JPEG 等价于 stbi_load。
STBIDEF stbi_uc *stbi_load_scaled(char const *filename, int *x, int *y, int *comp, int req_comp, int scale)
{
FILE *f = stbi__fopen(filename, "rb");
stbi_uc *result;
stbi__context s;
if (!f) return stbi__errpuc("can't fopen", "Unable to open file");
stbi__start_file(&s,f);
result = stbi__load_and_postprocess_8bit_ex(&s,x,y,comp,req_comp, scale);
if (result) fseek(f, - (int) (s.img_buffer_end - s.img_buffer), SEEK_CUR);
fclose(f);
return result;
}
#endif //!STBI_NO_STDIO
@@ -1433,6 +1461,14 @@ STBIDEF stbi_uc *stbi_load_from_memory(stbi_uc const *buffer, int len, int *x, i
return stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp);
}
// 追加(M12):JPEG 解码期降采样(见头文件声明)。非 JPEG 等价于 stbi_load_from_memory。
STBIDEF stbi_uc *stbi_load_scaled_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp, int scale)
{
stbi__context s;
stbi__start_mem(&s,buffer,len);
return stbi__load_and_postprocess_8bit_ex(&s,x,y,comp,req_comp, scale);
}
STBIDEF stbi_uc *stbi_load_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp, int req_comp)
{
stbi__context s;
@@ -1982,6 +2018,7 @@ typedef struct
int nomore; // flag if we saw a marker so must stop
int progressive;
int scale; // 追加(M12):输出抽取步长,默认 1=原尺寸
int spec_start;
int spec_end;
int succ_high;
@@ -3092,6 +3129,12 @@ static void stbi__jpeg_finish(stbi__jpeg *z)
z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*j*8+i*8, z->img_comp[n].w2, data);
}
}
// 追加(M12):该分量系数已全部 IDCT 进像素平面,立即释放,降低进升级 JPEG 的峰值
if (z->img_comp[n].raw_coeff) {
STBI_FREE(z->img_comp[n].raw_coeff);
z->img_comp[n].raw_coeff = 0;
z->img_comp[n].coeff = 0;
}
}
}
}
@@ -3893,6 +3936,10 @@ static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp
unsigned int i,j;
stbi_uc *output;
stbi_uc *coutput[4] = { NULL, NULL, NULL, NULL };
stbi_uc *rowbuf = NULL; // 追加(M12):整行临时缓冲,仅 dec>1 时使用
int dec = z->scale > 1 ? z->scale : 1;
int out_w = (z->s->img_x + dec - 1) / dec;
int out_h = (z->s->img_y + dec - 1) / dec;
stbi__resample res_comp[4];
@@ -3918,13 +3965,19 @@ static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp
else r->resample = stbi__resample_row_generic;
}
// 追加(M12):dec>1 时整行先转成像素再按步长抽取,不物化整幅原尺寸输出缓冲
if (dec > 1) {
rowbuf = (stbi_uc *) stbi__malloc_mad2(n, z->s->img_x, 0);
if (!rowbuf) { stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); }
}
// can't error after this so, this is safe
output = (stbi_uc *) stbi__malloc_mad3(n, z->s->img_x, z->s->img_y, 1);
if (!output) { stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); }
output = (stbi_uc *) stbi__malloc_mad3(n, out_w, out_h, 1);
if (!output) { STBI_FREE(rowbuf); stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); }
// now go ahead and resample
for (j=0; j < z->s->img_y; ++j) {
stbi_uc *out = output + n * z->s->img_x * j;
stbi_uc *out;
for (k=0; k < decode_n; ++k) {
stbi__resample *r = &res_comp[k];
int y_bot = r->ystep >= (r->vs >> 1);
@@ -3939,6 +3992,9 @@ static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp
r->line1 += z->img_comp[k].w2;
}
}
// 追加(M12):resample 状态机须逐行推进(上面已完成);非采样行直接丢弃
if (dec > 1 && (j % (unsigned int) dec) != 0) continue;
out = (dec > 1) ? rowbuf : output + (size_t) n * out_w * j;
if (n >= 3) {
stbi_uc *y = coutput[0];
if (z->s->img_n == 3) {
@@ -4016,16 +4072,27 @@ static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp
for (i=0; i < z->s->img_x; ++i) { *out++ = y[i]; *out++ = 255; }
}
}
// 追加(M12):rowbuf 已按整行转好像素,按步长抽取写入输出行
if (dec > 1) {
stbi_uc *dst = output + (size_t) n * out_w * (j / (unsigned int) dec);
unsigned int x;
for (x=0; x < (unsigned int) out_w; ++x) {
int c;
for (c=0; c < n; ++c)
dst[(size_t) x * n + c] = rowbuf[(size_t) x * dec * n + c];
}
}
}
STBI_FREE(rowbuf);
stbi__cleanup_jpeg(z);
*out_x = z->s->img_x;
*out_y = z->s->img_y;
*out_x = out_w;
*out_y = out_h;
if (comp) *comp = z->s->img_n >= 3 ? 3 : 1; // report original components, not output
return output;
}
}
static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri)
static void *stbi__jpeg_load_ex(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int scale)
{
unsigned char* result;
stbi__jpeg* j = (stbi__jpeg*) stbi__malloc(sizeof(stbi__jpeg));
@@ -4033,6 +4100,7 @@ static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int re
memset(j, 0, sizeof(stbi__jpeg));
STBI_NOTUSED(ri);
j->s = s;
j->scale = scale; // 追加(M12):输出抽取步长
stbi__setup_jpeg(j);
result = load_jpeg_image(j, x,y,comp,req_comp);
STBI_FREE(j);