Merge branch 'feat/wire-reader-gate-model-to-real-decoder-b'
This commit is contained in:
commit
2af7095d35
9 files changed
+933
-209
No files matched your search
@@ -56,11 +56,22 @@ int meow_img_pick_scale(int longside, int maxdim);
|
||||
/* 追加(M22)显式档位解码:语义与 meow_img_load_file_dim 相同(结果长边 ≤ maxdim,
|
||||
* 超了就再精修一次),只是缩解档位由调用方决定。内存吃紧时可以选更粗的档位——输出更小、
|
||||
* 峰值更低,代价是细节变软。step <= 1 等价长边约束入口;step > 8 按 8 处理。
|
||||
* M23 起:JPEG 先走真缩解后端(step 折算成目标长边 ceil(长边/step),后端按该长边选 DCT 阶),
|
||||
* 峰值只与输出尺寸相关;不支持的特性回退 stb(step 原样传入)。PNG/BMP 暂仍走 stb —— 阅读器的
|
||||
* 单次解码预算模型(M22)还是「整幅物化」口径,待 M27 同步后放开(见 M23 r1 报告)。 */
|
||||
* M23 起:先走真缩解后端(step 折算成目标长边 ceil(长边/step),后端按该长边选 DCT 阶),
|
||||
* 峰值只与输出尺寸相关;不支持的特性回退 stb(step 原样传入)。
|
||||
* M27 起 PNG/BMP 也走同一后端入口(阅读器峰值模型已同步为「目标图 + 工作面」口径);
|
||||
* 断言与实测见 tuku/test/test_reader_membudget.c 的 B7。 */
|
||||
meow_img *meow_img_load_file_step(const char *path, int maxdim, int step);
|
||||
|
||||
/* 追加(M27):与 meow_img_load_file_step 相同,额外控制回退并回报**实际**路径:
|
||||
* allow_stb_fallback = 1:后端不支持/失败时回退 stb(与 _step 完全一致);
|
||||
* allow_stb_fallback = 0:只尝试后端,失败返回 NULL——**绝不**回落 stb。阅读器的门控
|
||||
* 按「后端峰值」放行时必须用 0,否则头部判定通过、解码中途失败的文件会静默走 stb,
|
||||
* 实测可放出 18–36MB(截断 12MP JPEG / 头-only BMP / 非法位深 PNG)。失败页由调用方
|
||||
* 按坏图占位。
|
||||
* used_backend = 1(M23 后端直出目标尺寸)/ 0(未走成后端)。NULL 时等价 _step(允许回退)。 */
|
||||
meow_img *meow_img_load_file_step_ex(const char *path, int maxdim, int step,
|
||||
int allow_stb_fallback, int *used_backend);
|
||||
|
||||
/* 追加(M22)精确重采样到 w×h(最近邻,缩小/放大都支持)。阅读器用它把「缩解档位」的
|
||||
* 结果一次性落到显示框,使命中页仍是单次原生 blit(不做每帧重采样)。同尺寸返回 NULL
|
||||
* (调用方保留原图);失败返回 NULL。 */
|
||||
|
||||
+36
-29
@@ -99,50 +99,57 @@ meow_img *meow_img_load_file_dim(const char *path, int maxdim)
|
||||
}
|
||||
|
||||
/* 追加(M22):档位由调用方指定。与 meow_img_load_file_dim 同语义(结果长边 ≤ maxdim,
|
||||
* 超了再精修一次),只是档位不再由 maxdim 反推 —— 内存预算紧时可选更粗的档位。 */
|
||||
meow_img *meow_img_load_file_step(const char *path, int maxdim, int step)
|
||||
* 超了再精修一次),只是档位不再由 maxdim 反推 —— 内存预算紧时可选更粗的档位。
|
||||
* M27:真缩解后端优先(step 折算成目标长边 ceil(长边/step),后端按该长边选 DCT 阶,
|
||||
* 峰值只与输出尺寸相关);后端不支持时回退 stbi_load_scaled(step),语义与 M22 一致。
|
||||
*
|
||||
* M27 r1(P1-1):allow_stb_fallback=0 时只尝试后端,**绝不回退 stb**——阅读器的门控按
|
||||
* 「后端峰值」放行时必须走这里,否则「头部说支持、解码中途失败」会静默落到 stb,
|
||||
* 实测可放出 18–36MB(截断 12MP JPEG / 头-only BMP / 非法位深 PNG),即 M22 的 SIGKILL 场景。
|
||||
* 后端失败返回 NULL(调用方按坏图占位)。 */
|
||||
meow_img *meow_img_load_file_step_ex(const char *path, int maxdim, int step,
|
||||
int allow_stb_fallback, int *used_backend)
|
||||
{
|
||||
if (used_backend) *used_backend = 0;
|
||||
if (!path) return NULL;
|
||||
if (step <= 1 && maxdim <= 0) return meow_img_load_file(path);
|
||||
if (step <= 1 && maxdim <= 0)
|
||||
return allow_stb_fallback ? meow_img_load_file(path) : NULL;
|
||||
if (step <= 0) step = 1;
|
||||
if (step > 8) step = 8;
|
||||
int w = 0, h = 0, c = 0;
|
||||
if (!stbi_info(path, &w, &h, &c)) return NULL;
|
||||
int longside = w > h ? w : h;
|
||||
if (step == 1 && maxdim <= 0) return meow_img_load_file(path);
|
||||
/* 追加(M23):真缩解后端优先(本入口只接 JPEG)。step 是「输出别比这个档位更细」的提示:
|
||||
* 折算成目标长边 ceil(长边/step) 传给后端(后端按该长边自动选 DCT 阶),与下面
|
||||
* stbi_load_scaled(step) 的语义一致,峰值只与输出尺寸相关;maxdim<=0 时不精修,
|
||||
* 结果仍是档位尺寸。后端不支持(CMYK/算术编码/交错…)时回退 stb,行为与 M22 一致。
|
||||
*
|
||||
* PNG/BMP 在本入口暂不接后端:阅读器的单次解码峰值**预算模型**还是 M22 的「整幅物化」
|
||||
* 口径,实测峰值骤降会让它的 model≤2×measured 自检失真(详见 M23 r1 报告的 M27 待办)。
|
||||
* 待 M27 同步模型后,把这里的 JPEG 判定去掉即可。 */
|
||||
if (step == 1 && maxdim <= 0)
|
||||
return allow_stb_fallback ? meow_img_load_file(path) : NULL;
|
||||
/* 追加(M23/M27):真缩解后端优先(JPEG/PNG/BMP 全部格式)。step 是「输出别比这个档位
|
||||
* 更细」的提示:折算成目标长边 ceil(长边/step) 传给后端(后端按该长边自动选 DCT 阶),
|
||||
* 与下面 stbi_load_scaled(step) 的语义一致,峰值只与输出尺寸相关;maxdim<=0 时不精修,
|
||||
* 结果仍是档位尺寸。后端不支持(CMYK/算术编码/交错/RLE…)时回退 stb,行为与 M22 一致。
|
||||
* M27 起阅读器的峰值模型已与后端同口径(page_source 的 reader_estimate_peak),
|
||||
* 所以 PNG/BMP 也一并接后端——这正是「修好了却用不上」的最后一处缝。 */
|
||||
{
|
||||
unsigned char sig[3];
|
||||
int is_jpeg = 0;
|
||||
FILE *fp = fopen(path, "rb");
|
||||
if (fp) {
|
||||
if (fread(sig, 1, 3, fp) == 3 &&
|
||||
sig[0] == 0xFF && sig[1] == 0xD8 && sig[2] == 0xFF)
|
||||
is_jpeg = 1;
|
||||
fclose(fp);
|
||||
}
|
||||
if (is_jpeg) {
|
||||
int eff = maxdim > 0 ? maxdim : longside;
|
||||
int step_cap = (longside + step - 1) / step;
|
||||
unsigned char *bp = NULL;
|
||||
int bw = 0, bh = 0;
|
||||
if (step_cap > 0 && step_cap < eff) eff = step_cap;
|
||||
if (eff > 0 && meow_img_backend_file(path, eff, &bw, &bh, &bp) && bp)
|
||||
return img_finish_dim(bp, bw, bh, maxdim > 0 ? maxdim : longside);
|
||||
int eff = maxdim > 0 ? maxdim : longside;
|
||||
int step_cap = (longside + step - 1) / step;
|
||||
unsigned char *bp = NULL;
|
||||
int bw = 0, bh = 0;
|
||||
if (step_cap > 0 && step_cap < eff) eff = step_cap;
|
||||
if (eff > 0 && meow_img_backend_file(path, eff, &bw, &bh, &bp) && bp) {
|
||||
if (used_backend) *used_backend = 1;
|
||||
return img_finish_dim(bp, bw, bh, maxdim > 0 ? maxdim : longside);
|
||||
}
|
||||
}
|
||||
if (!allow_stb_fallback) return NULL; /* P1-1:门控按后端放行的页,失败即坏图 */
|
||||
unsigned char *p = stbi_load_scaled(path, &w, &h, &c, 4, step);
|
||||
/* maxdim<=0:不精修(保留档位输出的原始尺寸)。 */
|
||||
return img_finish_dim(p, w, h, maxdim > 0 ? maxdim : longside);
|
||||
}
|
||||
|
||||
meow_img *meow_img_load_file_step(const char *path, int maxdim, int step)
|
||||
{
|
||||
/* 通用入口保持 M22 语义:后端失败允许回退 stb。 */
|
||||
return meow_img_load_file_step_ex(path, maxdim, step, 1, NULL);
|
||||
}
|
||||
|
||||
/* 追加(M22):精确重采样到目标尺寸(最近邻,双向)。阅读器把缩解档位的结果一次性
|
||||
* 落到显示框,之后命中页就是单次原生 blit(M17 的热路径纪律不被粗档位破坏)。 */
|
||||
static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover);
|
||||
|
||||
@@ -200,10 +200,19 @@ static int probe_png(probe_src *s, mr_img_info *in)
|
||||
if (memcmp(hdr, sig, 8) != 0 || memcmp(hdr + 12, "IHDR", 4) != 0) return 0;
|
||||
unsigned ww = probe_be32(hdr + 16), hh = probe_be32(hdr + 20);
|
||||
if (ww == 0 || hh == 0) return 0;
|
||||
/* IHDR 的位深/颜色类型(紧跟在宽高后)。截断头(stb 也读不下去)按 RGB 8bit 记。 */
|
||||
/* IHDR 的位深/颜色类型/压缩/滤波/交错(紧跟宽高之后)。截断头(stb 也读不下去)
|
||||
* 按 RGB 8bit 记;但后端支持字段保持「未知(0)」→ 保守回退 stb 门控。 */
|
||||
int depth = 8, ctype = 2;
|
||||
unsigned char ex[2];
|
||||
if (ps_read(s, ex, sizeof ex)) { depth = ex[0]; ctype = ex[1]; }
|
||||
unsigned char ex[5];
|
||||
if (ps_read(s, ex, sizeof ex)) {
|
||||
depth = ex[0];
|
||||
ctype = ex[1];
|
||||
in->depth = ex[0];
|
||||
in->color = ex[1];
|
||||
in->compression = ex[2];
|
||||
in->filter = ex[3];
|
||||
in->interlace = ex[4];
|
||||
}
|
||||
int ch;
|
||||
switch (ctype) {
|
||||
case 0: ch = 1; break; /* 灰度 */
|
||||
@@ -259,16 +268,20 @@ static int probe_jpeg(probe_src *s, mr_img_info *in)
|
||||
in->codec = MR_IMG_CODEC_JPEG;
|
||||
in->progressive = prog;
|
||||
in->channels = 3;
|
||||
in->sof = m; /* M27:0xC0/0xC1/0xC2 才可能走后端 */
|
||||
in->depth = so[0]; /* 精度(8 才可能走后端) */
|
||||
/* 未知采样因子时按 4:4:4 估(3·W·H,偏保守):宁可多算峰值,不要低估。 */
|
||||
in->planes = (long long)ww * hh * 3;
|
||||
unsigned char nc = 0;
|
||||
if (ps_read(s, &nc, 1) && nc >= 1 && nc <= 4 && len >= 8 + 3 * nc) {
|
||||
int hv[4] = { 0, 0, 0, 0 };
|
||||
int tq[4] = { 0, 0, 0, 0 };
|
||||
int hmax = 1, vmax = 1, ok = 1;
|
||||
for (int i = 0; i < nc && ok; i++) {
|
||||
unsigned char comp[3]; /* id / 采样因子 hv / 量化表 */
|
||||
if (!ps_read(s, comp, sizeof comp)) { ok = 0; break; }
|
||||
hv[i] = comp[1];
|
||||
tq[i] = comp[2];
|
||||
int ch = hv[i] >> 4, cv = hv[i] & 15;
|
||||
if (ch < 1 || ch > 4 || cv < 1 || cv > 4) { ok = 0; break; }
|
||||
if (ch > hmax) hmax = ch;
|
||||
@@ -278,6 +291,13 @@ static int probe_jpeg(probe_src *s, mr_img_info *in)
|
||||
if (hmax % (hv[i] >> 4) != 0 || vmax % (hv[i] & 15) != 0) ok = 0;
|
||||
}
|
||||
if (ok) {
|
||||
/* M27:给后端支持判定留原始采样因子/量化表号(4 分量已在上面排除)。 */
|
||||
in->ncomp = nc;
|
||||
for (int i = 0; i < nc; i++) {
|
||||
in->comp_h[i] = (unsigned char)(hv[i] >> 4);
|
||||
in->comp_v[i] = (unsigned char)(hv[i] & 15);
|
||||
in->comp_tq[i] = (unsigned char)tq[i];
|
||||
}
|
||||
/* 分量面字节 = 与 stb 完全同式的 w2×h2(MCU 对齐 + 采样因子);
|
||||
* 渐进式每个分量还多一张 w2×h2×2 的 short 系数面(M20 实测 +23.5MB 的来源)。 */
|
||||
long long mcu_x = (ww + 8 * hmax - 1) / (8 * hmax);
|
||||
@@ -310,15 +330,24 @@ static int probe_bmp(probe_src *s, mr_img_info *in)
|
||||
((unsigned)hdr[24] << 16) | ((unsigned)hdr[25] << 24));
|
||||
if (hh < 0) hh = -hh; /* 自底向上 BMP */
|
||||
if (ww <= 0 || hh <= 0) return 0;
|
||||
unsigned char ex[4]; /* biPlanes(2) / biBitCount(2) */
|
||||
unsigned char ex[8]; /* biPlanes(2) / biBitCount(2) / biCompression(4) */
|
||||
int bpp = 24;
|
||||
if (ps_read(s, ex, sizeof ex)) bpp = (int)((unsigned)ex[2] | ((unsigned)ex[3] << 8));
|
||||
int have_ex = ps_read(s, ex, sizeof ex);
|
||||
if (have_ex) bpp = (int)((unsigned)ex[2] | ((unsigned)ex[3] << 8));
|
||||
in->w = ww;
|
||||
in->h = hh;
|
||||
in->codec = MR_IMG_CODEC_BMP;
|
||||
in->channels = bpp >= 8 ? bpp / 8 : 4; /* stb 一律解到 RGBA 目标 */
|
||||
in->progressive = 0;
|
||||
in->planes = 0; /* stb 直接写 RGBA 目标,不另开源缓冲 */
|
||||
/* M27:后端支持判定(DIB≥40 / BI_RGB / 8·16·24·32bpp)。字段读不全就保持未知 → stb。 */
|
||||
in->dib = (int)((unsigned)hdr[14] | ((unsigned)hdr[15] << 8) |
|
||||
((unsigned)hdr[16] << 16) | ((unsigned)hdr[17] << 24));
|
||||
if (have_ex) {
|
||||
in->depth = bpp;
|
||||
in->compression = (int)((unsigned)ex[4] | ((unsigned)ex[5] << 8) |
|
||||
((unsigned)ex[6] << 16) | ((unsigned)ex[7] << 24));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -387,6 +416,276 @@ int mr_probe_size(const char *path, int *w, int *h)
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 解码峰值模型 + 内存门控(M27:唯一实现) */
|
||||
/* ------------------------------------------------------------
|
||||
* 阅读器与页源都只调 reader_estimate_peak()——模型口径、预算策略、门控顺序都只在这里
|
||||
* 写一份。两个模型:
|
||||
* 后端(M23):峰值 = 目标图字节 + 后端工作面(JPEG 分量平面 / PNG·BMP 行缓冲)+ 固定开销。
|
||||
* 与源图分辨率/压缩体积无关;命中即放行(不做内存门控)。
|
||||
* stb(回退):M22 的真实分配模型(planes + 抽取输出 + 精修/落框临时图)。
|
||||
* 只有这类页参与 3MB 峰值预算与 MemAvailable < 6MB 门控。
|
||||
* ============================================================ */
|
||||
#define MR_PEAK_CAP_BYTES (3LL * 1024 * 1024) /* 常态单次解码峰值上限 */
|
||||
#define MR_PEAK_HARD_BYTES (12LL * 1024 * 1024) /* MemFree 富余时的绝对上限 */
|
||||
#define MR_MEM_FREE_MARGIN_KB (4L * 1024) /* 放宽档:MemFree 需比峰值多这么多 */
|
||||
#define MR_MEM_FLOOR_KB (6L * 1024) /* 回退路径:MemAvailable 下限 */
|
||||
#define MR_DECODE_SLACK (64LL * 1024) /* 结构体/行缓冲/分配器开销余量 */
|
||||
#define MR_BACKEND_SOFT_CAP (2400LL * 1024) /* 与 meow_jpeg.c 的 2.4MB 预算同值 */
|
||||
#define MR_BACKEND_HARD_CAP (8LL * 1024 * 1024) /* meow_jpeg.c 的 MJ_PEAK_CAP */
|
||||
|
||||
long long reader_peak_budget_bytes(long memfree_kb)
|
||||
{
|
||||
long long budget = MR_PEAK_CAP_BYTES;
|
||||
if (memfree_kb >= 0) {
|
||||
long long room = (memfree_kb - MR_MEM_FREE_MARGIN_KB) * 1024;
|
||||
if (room > budget) budget = room;
|
||||
if (budget > MR_PEAK_HARD_BYTES) budget = MR_PEAK_HARD_BYTES;
|
||||
}
|
||||
return budget;
|
||||
}
|
||||
|
||||
/* 与 meow_img_target_dims / meow_img_downscale 同一公式(纯 C 层不得依赖 SDK)。 */
|
||||
static void mr_target_dims(int w, int h, int maxdim, int *tw, int *th)
|
||||
{
|
||||
if (w >= h) {
|
||||
*tw = maxdim;
|
||||
*th = (int)((long)h * maxdim / w);
|
||||
} else {
|
||||
*th = maxdim;
|
||||
*tw = (int)((long)w * maxdim / h);
|
||||
}
|
||||
if (*tw < 1) *tw = 1;
|
||||
if (*th < 1) *th = 1;
|
||||
}
|
||||
|
||||
static long long mr_ceil_div(long long a, long long b) { return (a + b - 1) / b; }
|
||||
|
||||
/* 档位折算:与 meow_img_load_file_step 的 eff 计算逐字一致
|
||||
* (eff = min(maxdim, ceil(长边/step)),后端按 eff 选 DCT 阶)。 */
|
||||
static int mr_effective_dim(int longside, int step, int dim)
|
||||
{
|
||||
if (step < 1) step = 1;
|
||||
if (step > 8) step = 8;
|
||||
int eff = dim;
|
||||
int cap = (int)mr_ceil_div(longside, step);
|
||||
if (cap > 0 && cap < eff) eff = cap;
|
||||
return eff;
|
||||
}
|
||||
|
||||
/* PNG 位深 × 颜色类型 → 每像素位数(与 meow_png_stream.c 的 png_bits_of 同式)。 */
|
||||
static int mr_png_bits(int color, int depth)
|
||||
{
|
||||
switch (color) {
|
||||
case 0: return depth;
|
||||
case 2: return depth * 3;
|
||||
case 3: return depth;
|
||||
case 4: return depth * 2;
|
||||
case 6: return depth * 4;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 位深合法性(与 meow_png_stream.c 的 png_depth_ok **逐字同式**,P2-1):
|
||||
* color 0/3 允许 1/2/4/8(灰度再加 16),color 2/4/6 只允许 8/16。 */
|
||||
static int mr_png_depth_ok(int color, int depth)
|
||||
{
|
||||
if (color == 0) return depth == 1 || depth == 2 || depth == 4 || depth == 8 || depth == 16;
|
||||
if (color == 3) return depth == 1 || depth == 2 || depth == 4 || depth == 8;
|
||||
if (color == 2 || color == 4 || color == 6) return depth == 8 || depth == 16;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 后端支持矩阵(镜像 meow_jpeg.c / meow_png_stream.c 的拒绝条件;字段未知一律不支持)。
|
||||
* eff 是后端实际会收到的 maxdim(见 mr_effective_dim)。 */
|
||||
static int mr_backend_supported(const mr_img_info *in, int eff)
|
||||
{
|
||||
if (!in || in->w <= 0 || in->h <= 0 || eff <= 0) return 0;
|
||||
int longside = in->w > in->h ? in->w : in->h;
|
||||
if (longside <= eff) return 0; /* 小图走旧入口 */
|
||||
|
||||
switch (in->codec) {
|
||||
case MR_IMG_CODEC_JPEG: {
|
||||
if (in->sof != 0xC0 && in->sof != 0xC1 && in->sof != 0xC2) return 0;
|
||||
if (in->depth != 8) return 0;
|
||||
if (in->ncomp < 1 || in->ncomp > 3) return 0;
|
||||
for (int i = 0; i < in->ncomp; i++) {
|
||||
if (in->comp_h[i] < 1 || in->comp_h[i] > 4) return 0;
|
||||
if (in->comp_v[i] < 1 || in->comp_v[i] > 4) return 0;
|
||||
if (in->comp_tq[i] > 3) return 0;
|
||||
}
|
||||
int n = (int)mr_ceil_div(8LL * eff, longside);
|
||||
if (n < 1) n = 1;
|
||||
return n <= 7; /* N=8 起几乎没有缩解收益 → stb */
|
||||
}
|
||||
case MR_IMG_CODEC_PNG: {
|
||||
if (in->interlace || in->compression || in->filter) return 0;
|
||||
if (in->w > 65535 || in->h > 65535) return 0;
|
||||
if (!mr_png_depth_ok(in->color, in->depth)) return 0;
|
||||
return 1;
|
||||
}
|
||||
case MR_IMG_CODEC_BMP:
|
||||
if (in->dib < 40 || in->dib > 1024) return 0;
|
||||
if (in->compression != 0) return 0; /* RLE/BITFIELDS → stb */
|
||||
return in->depth == 8 || in->depth == 16 || in->depth == 24 || in->depth == 32;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* JPEG 分量平面字节(与 meow_jpeg.c 的 mj_layout 同式,n[] 是各分量的 DCT 阶)。 */
|
||||
static long long mr_jpeg_plane_bytes(const mr_img_info *in, const int n[3])
|
||||
{
|
||||
int hmax = 1, vmax = 1;
|
||||
for (int i = 0; i < in->ncomp; i++) {
|
||||
if (in->comp_h[i] > hmax) hmax = in->comp_h[i];
|
||||
if (in->comp_v[i] > vmax) vmax = in->comp_v[i];
|
||||
}
|
||||
long long mcux = mr_ceil_div(in->w, 8LL * hmax);
|
||||
long long mcuy = mr_ceil_div(in->h, 8LL * vmax);
|
||||
long long total = 0;
|
||||
for (int i = 0; i < in->ncomp; i++) {
|
||||
long long sw = mr_ceil_div(in->w * (long long)in->comp_h[i], hmax);
|
||||
long long sh = mr_ceil_div(in->h * (long long)in->comp_v[i], vmax);
|
||||
long long ow = mr_ceil_div(sw, 8);
|
||||
long long oh = mr_ceil_div(sh, 8);
|
||||
long long bx = mcux * in->comp_h[i];
|
||||
long long by = mcuy * in->comp_v[i];
|
||||
if (bx < ow) bx = ow;
|
||||
if (by < oh) by = oh;
|
||||
total += bx * (long long)n[i] * by * (long long)n[i];
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/* 后端峰值:目标图 + 工作面 + 固定开销。JPEG 的两步降档(色度减半、2.4MB 预算、
|
||||
* 8MB 硬上限)与 meow_jpeg.c 一致——估算因此能贴着实测,而不是靠放大余量。 */
|
||||
static long long mr_backend_peak_bytes(const mr_img_info *in, int eff, int step,
|
||||
int box_w, int box_h, int *tw, int *th)
|
||||
{
|
||||
int dim = box_w > box_h ? box_w : box_h;
|
||||
mr_target_dims(in->w, in->h, eff, tw, th);
|
||||
long long target = (long long)*tw * (*th) * 4;
|
||||
long long work = 0;
|
||||
|
||||
if (in->codec == MR_IMG_CODEC_JPEG) {
|
||||
int longside = in->w > in->h ? in->w : in->h;
|
||||
int base = (int)mr_ceil_div(8LL * eff, longside);
|
||||
if (base < 1) base = 1;
|
||||
int n[3] = { base, base, base };
|
||||
work = mr_jpeg_plane_bytes(in, n);
|
||||
/* 4:4:4 色度与亮度等大:亮度平面超过 400KB 时色度降一档(mj_sof 同式)。 */
|
||||
if (in->ncomp == 3 && in->comp_h[0] == in->comp_h[1] && in->comp_v[0] == in->comp_v[1] &&
|
||||
work / 3 > 400LL * 1024) {
|
||||
for (int i = 1; i < 3; i++) if (n[i] > 1) n[i] >>= 1;
|
||||
work = mr_jpeg_plane_bytes(in, n);
|
||||
}
|
||||
/* 平面 + 目标图超 2.4MB:先降色度(亮度保持清晰)。 */
|
||||
if (work + target > MR_BACKEND_SOFT_CAP) {
|
||||
int changed = 0;
|
||||
for (int i = 1; i < 3; i++) if (n[i] > 1) { n[i] >>= 1; changed = 1; }
|
||||
if (changed) work = mr_jpeg_plane_bytes(in, n);
|
||||
}
|
||||
/* 最后一道防线:异常巨图整体降档(最多 N=1)。 */
|
||||
while (work + target > MR_BACKEND_HARD_CAP && base > 1) {
|
||||
base >>= 1;
|
||||
for (int i = 0; i < 3; i++) if (n[i] > base) n[i] = base;
|
||||
work = mr_jpeg_plane_bytes(in, n);
|
||||
}
|
||||
} else if (in->codec == MR_IMG_CODEC_PNG) {
|
||||
long long rowbytes = (in->w * (long long)mr_png_bits(in->color, in->depth) + 7) / 8;
|
||||
work = 3 * rowbytes + 1; /* line + prev + cur(png_stream 同式) */
|
||||
work += 48LL * 1024; /* inflate 状态余量(zlib 窗口等) */
|
||||
} else { /* BMP */
|
||||
int bpp = in->depth > 0 ? in->depth : 24;
|
||||
long long rowbytes = (in->w * (long long)bpp + 31) / 32 * 4;
|
||||
work = rowbytes; /* 单行 seek 缓冲 */
|
||||
work += 48LL * 1024; /* 结构体/调色板/stdio 余量 */
|
||||
}
|
||||
|
||||
long long peak = target + work + MR_DECODE_SLACK;
|
||||
|
||||
/* 阅读器在「粗档位 + 输出尺寸与显示框不一致」时还会拷一张显示框大小的图;
|
||||
* 条件与 reader_load_page 逐字对齐,避免把常态误加一倍。 */
|
||||
if (step > 1 && dim > 0 && (in->w >= box_w || in->h >= box_h) &&
|
||||
(*tw != box_w || *th != box_h)) {
|
||||
peak += (long long)box_w * box_h * 4;
|
||||
}
|
||||
return peak;
|
||||
}
|
||||
|
||||
/* stb 回退路径峰值(M22 模型 + M27 Adam7 修正):只有这类页才参与预算门控。 */
|
||||
static long long mr_stb_peak_bytes(const mr_img_info *in, int step, int box_w, int box_h)
|
||||
{
|
||||
long long W = in->w, H = in->h;
|
||||
long long box = (long long)box_w * box_h * 4;
|
||||
long long dst;
|
||||
if (in->codec == MR_IMG_CODEC_JPEG) {
|
||||
if (step < 1) step = 1;
|
||||
dst = mr_ceil_div(W, step) * mr_ceil_div(H, step) * 4;
|
||||
} else {
|
||||
dst = W * H * 4; /* PNG/BMP:stb 无解码期缩放,整幅落地 */
|
||||
}
|
||||
long long peak = in->planes + dst; /* stb 阶段 */
|
||||
/* Adam7 交错 PNG:stb 额外整幅暂存各 pass 的 raw 与一张中间 RGBA(实测 1600×2400
|
||||
* 45MB,M22 模型只算 26MB)。多算 planes+dst 保证是上界;这类页无论预算如何都超过
|
||||
* 12MB 放宽档,结果仍是占位(安全侧)。 */
|
||||
if (in->codec == MR_IMG_CODEC_PNG && in->interlace)
|
||||
peak += in->planes + dst;
|
||||
long long finished = dst > box ? box : dst;
|
||||
if (dst + finished > peak) peak = dst + finished;
|
||||
if (finished + box > peak) peak = finished + box;
|
||||
return peak + MR_DECODE_SLACK;
|
||||
}
|
||||
|
||||
void reader_estimate_peak(const mr_img_info *in, int step, int box_w, int box_h,
|
||||
long memfree_kb, long memavail_kb, mr_decode_decision *out)
|
||||
{
|
||||
if (!out) return;
|
||||
out->path = MR_DECODE_PATH_STB;
|
||||
out->skip = MR_DECODE_OK;
|
||||
out->step = 1;
|
||||
out->tw = out->th = 0;
|
||||
out->peak_bytes = 0;
|
||||
out->budget_bytes = reader_peak_budget_bytes(memfree_kb);
|
||||
if (!in || in->w <= 0 || in->h <= 0 || box_w <= 0 || box_h <= 0) return;
|
||||
|
||||
if (step < 1) step = 1;
|
||||
if (step > 8) step = 8;
|
||||
out->step = step;
|
||||
|
||||
int dim = box_w > box_h ? box_w : box_h;
|
||||
int longside = in->w > in->h ? in->w : in->h;
|
||||
int eff = mr_effective_dim(longside, step, dim);
|
||||
|
||||
/* 1) 先看后端能否接住:命中时峰值与源尺寸无关(典型页 <2.6MB),不做 3MB 常态门控;
|
||||
* 但仍保留两道安全阀(P2-2):MemAvailable <6MB 直接占位、估算超过 12MB 硬上限占位。
|
||||
* 后端路由由 reader 用 backend-only 入口执行(P1-1),失败即坏图,绝不回落 stb。 */
|
||||
if (mr_backend_supported(in, eff)) {
|
||||
out->path = MR_DECODE_PATH_BACKEND;
|
||||
out->peak_bytes = mr_backend_peak_bytes(in, eff, step, box_w, box_h,
|
||||
&out->tw, &out->th);
|
||||
if (memavail_kb >= 0 && memavail_kb < MR_MEM_FLOOR_KB)
|
||||
out->skip = MR_DECODE_SKIP_LOWMEM;
|
||||
else if (out->peak_bytes > MR_PEAK_HARD_BYTES)
|
||||
out->skip = MR_DECODE_SKIP_PEAK;
|
||||
else
|
||||
out->skip = MR_DECODE_OK;
|
||||
return;
|
||||
}
|
||||
|
||||
/* 2) 回退 stb:只有这里才做 MemAvailable 与峰值预算门控。 */
|
||||
if (longside <= dim) { out->tw = in->w; out->th = in->h; }
|
||||
else mr_target_dims(in->w, in->h, dim, &out->tw, &out->th);
|
||||
out->peak_bytes = mr_stb_peak_bytes(in, step, box_w, box_h);
|
||||
if (memavail_kb >= 0 && memavail_kb < MR_MEM_FLOOR_KB)
|
||||
out->skip = MR_DECODE_SKIP_LOWMEM;
|
||||
else if (out->peak_bytes > out->budget_bytes)
|
||||
out->skip = MR_DECODE_SKIP_PEAK;
|
||||
else
|
||||
out->skip = MR_DECODE_OK;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* mr_book_ops 适配器(目录册) */
|
||||
/* ============================================================ */
|
||||
|
||||
@@ -107,11 +107,64 @@ typedef struct mr_img_info {
|
||||
int progressive; /* 1=渐进式 JPEG(SOF2/SOF6/SOF10/SOF14) */
|
||||
int channels; /* 源通道数(JPEG 分量数 / PNG 颜色类型 / BMP 位深/8) */
|
||||
long long planes; /* 见上:stb 中间面字节(不含最终 RGBA 输出) */
|
||||
|
||||
/* ---- 追加(M27):真缩解后端(M23)支持判定所需的头字段。
|
||||
* 缺省 0 = 未知;判定按「不支持」处理(保守回退 stb 门控),绝不因缺字段误走后端。 ---- */
|
||||
int depth; /* JPEG 精度 / PNG 位深 / BMP 位深(bpp) */
|
||||
int color; /* PNG 颜色类型(0/2/3/4/6);非 PNG 为 0 */
|
||||
int sof; /* JPEG SOF marker(0xC0/0xC1/0xC2 才可能走后端) */
|
||||
int interlace; /* PNG 1=Adam7 交错(后端不支持) */
|
||||
int compression; /* PNG 压缩法 / BMP BI_*(0 才可能走后端) */
|
||||
int filter; /* PNG 滤波法(0 才可能走后端) */
|
||||
int dib; /* BMP DIB 头长度(<40 后端不支持) */
|
||||
int ncomp; /* JPEG 分量数(1..3;4=CMYK → 后端不支持) */
|
||||
unsigned char comp_h[3], comp_v[3], comp_tq[3]; /* JPEG 采样因子/量化表号 */
|
||||
} mr_img_info;
|
||||
|
||||
int mr_probe_info(const char *path, mr_img_info *info);
|
||||
int mr_probe_info_mem(const void *buf, size_t len, mr_img_info *info);
|
||||
|
||||
/* ---- 追加(M27):解码峰值模型 + 门控的唯一实现 ----
|
||||
* 阅读器(ui/reader)与页源(本层)都只调 reader_estimate_peak,不得各写一份模型。
|
||||
* 一次调用同时回答三件事:
|
||||
* path 走后端(MR_DECODE_PATH_BACKEND)还是回退 stb(MR_DECODE_PATH_STB);
|
||||
* skip 是否占位(MR_DECODE_OK / SKIP_LOWMEM / SKIP_PEAK);
|
||||
* peak_bytes + tw/th 预估峰值与产出尺寸(日志/测试直接对照实测)。
|
||||
*
|
||||
* 判定顺序(M27 核心设计):
|
||||
* 1. 头部特征满足 M23 后端支持矩阵(JPEG SOF0/1/2、≤3 分量、8bit、N=1..7;
|
||||
* PNG 非交错/标准压缩;BMP DIB≥40/BI_RGB/8·16·24·32bpp)→ **走后端**:
|
||||
* 峰值 = 目标图字节 + 后端工作面 + 固定开销,与源分辨率无关 → 不占位(不做内存门控)。
|
||||
* 2. 否则回退 stb:按 M22 的 stb 真实分配模型算峰值,超预算 → SKIP_PEAK;
|
||||
* MemAvailable < 6MB → SKIP_LOWMEM。这两道门控只作用于回退路径。
|
||||
*
|
||||
* 参数:step 与 meow_img_load_file_step 同义(JPEG 档位提示);box_w/box_h 是阅读器
|
||||
* 最终显示框;memfree_kb/memavail_kb 读不到时传 -1。out 必须非 NULL。
|
||||
* 估算精度口径:**后端命中页 / 典型页**保证估算 ≥ 实测(--wrap malloc)且 ≤1.5×;
|
||||
* 回退 stb 的小图只保证门控安全(可能保守,不强求 1.5× 上界)。断言见 tuku/test。 */
|
||||
#define MR_DECODE_PATH_STB 0
|
||||
#define MR_DECODE_PATH_BACKEND 1
|
||||
|
||||
#define MR_DECODE_OK 0
|
||||
#define MR_DECODE_SKIP_LOWMEM 1
|
||||
#define MR_DECODE_SKIP_PEAK 2
|
||||
|
||||
typedef struct mr_decode_decision {
|
||||
int path; /* MR_DECODE_PATH_* */
|
||||
int skip; /* MR_DECODE_OK / SKIP_* */
|
||||
int step; /* 实际档位(<=1 按 1) */
|
||||
int tw, th; /* 预估最终产出尺寸(含阅读器落框精修) */
|
||||
long long peak_bytes; /* 预估峰值(字节,含固定开销) */
|
||||
long long budget_bytes; /* 本次预算(字节;SKIP_PEAK 的比较基准) */
|
||||
} mr_decode_decision;
|
||||
|
||||
void reader_estimate_peak(const mr_img_info *in, int step, int box_w, int box_h,
|
||||
long memfree_kb, long memavail_kb, mr_decode_decision *out);
|
||||
|
||||
/* 预算策略(reader_estimate_peak 内部用的就是它):常态 3MB;MemFree 富余时放宽到
|
||||
* min(12MB, MemFree-4MB);读不到 MemFree(<0)只用常态。暴露给日志/测试,避免复制。 */
|
||||
long long reader_peak_budget_bytes(long memfree_kb);
|
||||
|
||||
/* ============================================================ */
|
||||
/* 档案册(M11):.cbz / .zip 当一本书 */
|
||||
/* ------------------------------------------------------------
|
||||
|
||||
@@ -1,38 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
"""make_mem_samples.py — 为 test_reader_membudget.c 生成 JPEG 夹具(M22)。
|
||||
"""make_mem_samples.py — 为 test_reader_membudget.c 生成解码夹具(M22 / M27)。
|
||||
|
||||
用法:python3 make_mem_samples.py <输出目录>
|
||||
依赖:Pillow。没有 Pillow 时测试自动 SKIP JPEG 断言(BMP/PNG 夹具由 C 直接写)。
|
||||
依赖:Pillow。没有 Pillow 时测试自动 SKIP JPEG 相关断言(BMP/PNG 小夹具由 C 直接写)。
|
||||
|
||||
刻意用**结构化图案**而不是噪声:文件极小、解码路径与真实漫画页一致(白底 + 分镜框)。
|
||||
page.jpg 1000x1500 baseline(1.5MP,真机崩溃页所在档位)
|
||||
big4.jpg 2000x2000 baseline(4MP,手机照片档)
|
||||
prog.jpg 1000x1500 progressive(SOF2:验证渐进识别 + 峰值模型)
|
||||
M27 追加「6 类典型页」与「回退路径」夹具,用来断言门控模型与真缩解后端同口径:
|
||||
典型(应全部走后端、不占位):
|
||||
base2000.jpg 2000x3000 baseline
|
||||
prog2000.jpg 2000x3000 progressive
|
||||
restart2000.jpg 2000x3000 progressive + restart markers
|
||||
page1600.png 1600x2400 PNG
|
||||
page1600.bmp 1600x2400 BMP
|
||||
mp4.jpg 2320x1740(4MP)
|
||||
mp12.jpg 4000x3000(12MP)
|
||||
回退(后端拒绝、按旧 stb 门控占位):
|
||||
cmyk.jpg 2000x3000 CMYK(4 分量)
|
||||
adam7.png 1600x2400 Adam7 交错
|
||||
mono1.bmp 1600x2400 1bpp
|
||||
M22 旧断言仍用:
|
||||
page.jpg / big4.jpg / prog.jpg(1000x1500 / 2000x2000 / 1000x1500 progressive)
|
||||
"""
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
def pattern(w, h):
|
||||
im = Image.new("RGB", (w, h), (248, 247, 244))
|
||||
def pattern(w, h, mode="RGB"):
|
||||
im = Image.new(mode, (w, h), (248, 247, 244) if mode == "RGB" else 200)
|
||||
d = ImageDraw.Draw(im)
|
||||
for k in range(0, min(w, h) - 200, 70):
|
||||
d.rectangle([20 + k, 20 + k, 160 + k, 120 + k], outline=(10, 10, 12), width=3)
|
||||
return im
|
||||
|
||||
|
||||
def chunk(tag, data):
|
||||
return (struct.pack(">I", len(data)) + tag + data +
|
||||
struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF))
|
||||
|
||||
|
||||
def write_adam7(path, img):
|
||||
"""手写 Adam7 交错 PNG(Pillow 不支持写交错)。RGB8。"""
|
||||
w, h = img.size
|
||||
px = img.load()
|
||||
raw = bytearray()
|
||||
passes = [(0, 0, 8, 8), (4, 0, 8, 8), (0, 4, 4, 8), (2, 0, 4, 4),
|
||||
(0, 2, 2, 4), (1, 0, 2, 2), (0, 1, 1, 2)]
|
||||
for (x0, y0, dx, dy) in passes:
|
||||
xs = list(range(x0, w, dx))
|
||||
ys = list(range(y0, h, dy))
|
||||
if not xs or not ys:
|
||||
continue
|
||||
for y in ys:
|
||||
raw.append(0)
|
||||
for x in xs:
|
||||
r, g, b = px[x, y][:3]
|
||||
raw += bytes((r, g, b))
|
||||
ihdr = struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 1)
|
||||
data = (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) +
|
||||
chunk(b"IDAT", zlib.compress(bytes(raw))) + chunk(b"IEND", b""))
|
||||
with open(path, "wb") as f:
|
||||
f.write(data)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print(__doc__)
|
||||
return 2
|
||||
out = sys.argv[1]
|
||||
os.makedirs(out, exist_ok=True)
|
||||
|
||||
# ---- M22 旧断言夹具 ----
|
||||
pattern(1000, 1500).save(os.path.join(out, "page.jpg"), "JPEG", quality=60, subsampling=2)
|
||||
pattern(2000, 2000).save(os.path.join(out, "big4.jpg"), "JPEG", quality=60, subsampling=2)
|
||||
pattern(1000, 1500).save(os.path.join(out, "prog.jpg"), "JPEG", quality=60, subsampling=2,
|
||||
progressive=True)
|
||||
|
||||
# ---- M27 6 类典型页 ----
|
||||
big = pattern(2000, 3000)
|
||||
big.save(os.path.join(out, "base2000.jpg"), "JPEG", quality=60, subsampling=2)
|
||||
big.save(os.path.join(out, "prog2000.jpg"), "JPEG", quality=60, subsampling=2, progressive=True)
|
||||
big.save(os.path.join(out, "restart2000.jpg"), "JPEG", quality=60, subsampling=2,
|
||||
progressive=True, restart_marker_blocks=8)
|
||||
pattern(1600, 2400).save(os.path.join(out, "page1600.png"), "PNG")
|
||||
pattern(1600, 2400).save(os.path.join(out, "page1600.bmp"), "BMP")
|
||||
pattern(2320, 1740).save(os.path.join(out, "mp4.jpg"), "JPEG", quality=60, subsampling=2)
|
||||
pattern(4000, 3000).save(os.path.join(out, "mp12.jpg"), "JPEG", quality=60, subsampling=2)
|
||||
|
||||
# ---- M27 回退路径 ----
|
||||
big.convert("CMYK").save(os.path.join(out, "cmyk.jpg"), "JPEG", quality=60)
|
||||
write_adam7(os.path.join(out, "adam7.png"), pattern(1600, 2400))
|
||||
pattern(1600, 2400).convert("1").save(os.path.join(out, "mono1.bmp"), "BMP")
|
||||
|
||||
# ---- M27 r1(P1-1):头部判支持但解码中途失败/后端拒绝的坏文件 ----
|
||||
# 1) 截断的 12MP JPEG:SOF 仍在(probe/矩阵判 backend),熵段被腰斩 → 后端失败;
|
||||
# 旧实现会静默回退 stb 并分配 ~18MB。
|
||||
with open(os.path.join(out, "mp12.jpg"), "rb") as f:
|
||||
blob = f.read(200000)
|
||||
with open(os.path.join(out, "trunc2000.jpg"), "wb") as f:
|
||||
f.write(blob)
|
||||
# 2) 3000x3000 24bpp BMP,只有 54 字节头(无像素):矩阵判 backend,后端失败;
|
||||
# 旧实现回退 stb 会 malloc 36MB 并"解出"一张全黑图。
|
||||
hdr = bytearray(54)
|
||||
hdr[0:2] = b"BM"
|
||||
struct.pack_into("<I", hdr, 2, 54) # 谎报文件大小
|
||||
struct.pack_into("<I", hdr, 10, 54) # 像素数据偏移
|
||||
struct.pack_into("<I", hdr, 14, 40) # BITMAPINFOHEADER
|
||||
struct.pack_into("<i", hdr, 18, 3000) # width
|
||||
struct.pack_into("<i", hdr, 22, 3000) # height
|
||||
struct.pack_into("<H", hdr, 26, 1) # planes
|
||||
struct.pack_into("<H", hdr, 28, 24) # bpp
|
||||
with open(os.path.join(out, "hdr3000.bmp"), "wb") as f:
|
||||
f.write(hdr)
|
||||
# 3) 非法位深 PNG(color=2 RGB + depth=1):后端 png_depth_ok 拒绝 → 必须判 stb;
|
||||
# 旧矩阵只按 color 算位宽 → 误判 backend,失败后回退 stb 分配 ~16MB。
|
||||
w, h = 1600, 2400
|
||||
rowbytes = (w * 3 + 7) // 8 # 600 字节/行(depth=1 RGB)
|
||||
raw = bytearray()
|
||||
for y in range(h):
|
||||
raw.append(0)
|
||||
raw += bytes((x * 7 + y) & 0xFF for x in range(rowbytes))
|
||||
ihdr = struct.pack(">IIBBBBB", w, h, 1, 2, 0, 0, 0)
|
||||
data = (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) +
|
||||
chunk(b"IDAT", zlib.compress(bytes(raw))) + chunk(b"IEND", b""))
|
||||
with open(os.path.join(out, "baddepth.png"), "wb") as f:
|
||||
f.write(data)
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -159,13 +159,13 @@ if [ -f "$SDK/meow_ui_view.c" ] && [ -f "$SDK/meow_ui_config.c" ]; then
|
||||
logic/library.c logic/reader.c logic/reader/page_source.c \
|
||||
logic/reader/archive_source.c logic/reader/page_cache.c logic/zip/zip.c \
|
||||
logic/reader/nav.c \
|
||||
"$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \
|
||||
"$SDK"/meow_ui_time.c "$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \
|
||||
"$SDK"/meow_ui_vol.c "$SDK"/meow_ui_log.c "$SDK"/meow_ui_widget.c \
|
||||
"$SDK"/meow_ui_input.c "$SDK"/meow_ui_activity.c "$SDK"/meow_ui_dialog.c \
|
||||
"$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \
|
||||
"$SDK"/meow_ui_draw.c "$SDK"/meow_ui_view.c "$SDK"/meow_ui_box.c \
|
||||
"$SDK"/meow_ui_anim.c "$SDK"/meow_ui_scroll.c "$SDK"/meow_ui_img.c \
|
||||
"$SDK"/meow_jpeg.c "$SDK"/meow_png_stream.c "$SDK"/meow_ui_time.c \
|
||||
"$SDK"/meow_jpeg.c "$SDK"/meow_png_stream.c \
|
||||
"$SDK"/meow_ui_media.c "$SDK"/meow_notify_ipc.c "$SDK"/meow_ui_config.c \
|
||||
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
|
||||
-pthread -lm -lz -o "$OUT"/test_reader_membudget \
|
||||
|
||||
@@ -85,9 +85,13 @@ static int write_garbage(const char *path)
|
||||
}
|
||||
|
||||
/* 合法 BMP24 头(声称 3000x3000,无像素数据):stbi_info 能读到尺寸,
|
||||
* 整幅 RGBA 峰值 36MB 必须被 MR_DECODE_PEAK_CAP(12MB) 在解码前拦下
|
||||
* (旧代码的 raw 上限是 40MB,这种页会被放行 → 真机上瞬时分配即可能被 OOM 杀掉)。 */
|
||||
static int write_huge_bmp_header(const char *path)
|
||||
* 整幅 RGBA 峰值 36MB 必须在解码前拦下(旧代码的 raw 上限是 40MB,这种页会被放行 →
|
||||
* 真机上瞬时分配即可能被 OOM 杀掉)。
|
||||
* M27 r1:24bpp(后端支持)与 1bpp(后端拒绝→stb)两种都要覆盖:
|
||||
* - 24bpp 头-only:判定 backend → backend-only 失败 → NULL/坏图占位,峰值必须 ≤3MB;
|
||||
* - 1bpp 头-only:判定 stb → 旧门控 skip=peak,零分配。
|
||||
* 旧实现把 24bpp 换成 1bpp 恰好绕开了「矩阵接受、解码失败→静默回退 stb」的 P1 旁路。 */
|
||||
static int write_huge_bmp_header(const char *path, int bpp)
|
||||
{
|
||||
unsigned char b[54] = { 0 };
|
||||
b[0] = 'B'; b[1] = 'M';
|
||||
@@ -97,7 +101,7 @@ static int write_huge_bmp_header(const char *path)
|
||||
put32(b + 18, 3000); /* width */
|
||||
put32(b + 22, 3000); /* height */
|
||||
put16(b + 26, 1); /* planes */
|
||||
put16(b + 28, 24); /* bpp */
|
||||
put16(b + 28, (unsigned)bpp); /* 24=后端支持但无像素数据;1=后端拒绝→stb */
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
int ok = fwrite(b, 1, sizeof b, f) == sizeof b;
|
||||
@@ -177,12 +181,15 @@ int main(void)
|
||||
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++) {
|
||||
/* 坏册(5 页):1=有效;2=垃圾字节(永久 failed);3=3000×3000 24bpp 头-only
|
||||
* (判定 backend→后端失败→NULL,永久 failed,峰值必须小);4=3000×3000 1bpp 头-only
|
||||
* (回退 stb→skip=peak,可重试);5=有效。 */
|
||||
for (int i = 0; i < 5; 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 (i == 2) { if (write_huge_bmp_header(p, 24) != 0) { printf("SKIP: write failed\n"); return 0; } }
|
||||
else if (i == 3) { if (write_huge_bmp_header(p, 1) != 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; }
|
||||
}
|
||||
|
||||
@@ -259,7 +266,7 @@ int main(void)
|
||||
CHECK(mr_reader_ui_resident_kb() <= 9 * 1024, "resident bounded on 140p book (%ldKB)",
|
||||
mr_reader_ui_resident_kb());
|
||||
|
||||
/* 坏图册:翻过坏页不崩、可继续翻。 */
|
||||
/* 坏图册(5 页):翻过坏页不崩、可继续翻。最后一页是有效页,允许不前进。 */
|
||||
meow_view_page_pop();
|
||||
mr_reader_open_dir_at(bad, 0);
|
||||
pump_ms(600);
|
||||
@@ -268,16 +275,21 @@ int main(void)
|
||||
int before = mr_reader_ui_page();
|
||||
tap(40, 300);
|
||||
pump_ms(300);
|
||||
if (mr_reader_ui_page() == before && before < 3) bad_ok = 0;
|
||||
if (mr_reader_ui_page() == before && before < 4) 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:坏图 = 永久占位(failed);超预算大页 = 预算跳过(skipped,可重试,不是 failed)。 */
|
||||
CHECK(mr_reader_ui_failed_pages() == 1,
|
||||
"garbage page is a permanent placeholder (failed=%d)", mr_reader_ui_failed_pages());
|
||||
"huge pages skipped/rejected without allocating them (%ldKB)", mr_reader_ui_resident_kb());
|
||||
/* M22/M27 r1:坏图 = 永久占位(failed);超预算的**回退路径**大页 = 预算跳过(skipped)。
|
||||
* - 垃圾页 + 3000×3000 24bpp 头-only(判定 backend,backend-only 失败→NULL)→ failed=2;
|
||||
* - 3000×3000 1bpp 头-only(判定 stb,超预算)→ skipped=1。
|
||||
* 这条 24bpp 夹具正是 P1 旁路的复现:旧实现会静默回退 stb 并分配 36MB。 */
|
||||
CHECK(mr_reader_ui_failed_pages() == 2,
|
||||
"garbage + 24bpp head-only are permanent placeholders (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());
|
||||
"1bpp 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 页排版 + 跳页落位 ---- */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* test_reader_membudget.c — 阅读器解码内存预算回归(M22;需要 SDK 源码树)。
|
||||
/* test_reader_membudget.c — 阅读器解码内存预算回归(M22;M27 接真缩解后端;需要 SDK 源码树)。
|
||||
*
|
||||
* 真机事故(R3 Pro II 480×720 / MemTotal 56872kB):空闲 RSS 6044kB → 翻页 11400kB
|
||||
* → 峰值 15600kB(当时 MemFree 仅 1404kB)→ SIGKILL。M20 审计把账算清:4 槽 RGBA 常驻
|
||||
@@ -12,10 +12,16 @@
|
||||
* B4 常驻页图 ≤ 常驻上限(3MB)
|
||||
* B5 内存门控:MemAvailable 不足 → 不解码直接占位(可重试,不是永久失败)
|
||||
* B6 预算跳过可重试:MemFree 充足时同一页能解出来(占位不是永久 failed)
|
||||
* B7 快翻取消:拖动期间只解落点页,不产生中间页解码
|
||||
* B7 M27:6 类典型页(2000×3000 baseline/渐进/restart、1600×2400 PNG/BMP、4MP、12MP)
|
||||
* 全部走后端、不占位;估算 ≥ 实测且 ≤1.5×;输出尺寸与显示框一致
|
||||
* B7b 回退路径:CMYK / Adam7 PNG / 1bpp BMP 走 stb,按旧门控占位
|
||||
* B7c P1-1 旁路回归:截断 12MP JPEG / 3000×3000 24bpp 头-only BMP(判定 backend,
|
||||
* 后端失败必须 NULL、峰值 ≤3MB)/ 非法位深 PNG(矩阵拒绝→stb 占位)
|
||||
* B8 M27:连翻 30 页(6 类循环)不掉页、不占位、任何时刻存活增量 ≤3MB
|
||||
* B9 M27:CMYK 回退页集成 —— 占位可重试且不崩
|
||||
*
|
||||
* 夹具:BMP/PNG 由本文件用 C 写(不依赖 Pillow);JPEG 由 python3+Pillow 生成,
|
||||
* 缺 PIL 时跳过 JPEG 相关断言(打印 SKIP)。全部夹具体积极小(结构化图案)。
|
||||
* 夹具:BMP/PNG 小夹具由本文件用 C 写;JPEG/PNG/BMP 大夹具由 python3+Pillow 生成
|
||||
* (test/make_mem_samples.py),缺 PIL 时跳过相关断言(打印 SKIP)。
|
||||
*
|
||||
* 编译:test/run_tests.sh(与 test_reader_perf 同口径 + --wrap malloc 家族)。
|
||||
*/
|
||||
@@ -24,6 +30,8 @@
|
||||
#include "ui/reader/reader.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include "logic/reader/nav.h"
|
||||
#include "logic/reader/page_source.h"
|
||||
#include "stb_image.h" /* stbi_load_scaled:两条路径输出尺寸一致性对照 */
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
@@ -156,6 +164,63 @@ static int write_bmp24(const char *path, int w, int h, unsigned seed)
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
/* 1bpp BMP(后端不支持 → 回退 stb):用于「回退路径按旧门控占位」的集成断言。 */
|
||||
static int write_bmp1(const char *path, int w, int h)
|
||||
{
|
||||
int stride = ((w + 31) / 32) * 4;
|
||||
int imgsz = stride * h;
|
||||
unsigned char fh[14] = { 0 }, ih[40] = { 0 };
|
||||
unsigned char pal[8] = { 0, 0, 0, 0, 255, 255, 255, 0 };
|
||||
fh[0] = 'B'; fh[1] = 'M';
|
||||
put32(fh + 2, (unsigned)(14 + 40 + 8 + imgsz));
|
||||
put32(fh + 10, (unsigned)(14 + 40 + 8));
|
||||
put32(ih + 0, 40);
|
||||
put32(ih + 4, (unsigned)w);
|
||||
put32(ih + 8, (unsigned)h);
|
||||
put16(ih + 12, 1);
|
||||
put16(ih + 14, 1);
|
||||
put32(ih + 20, (unsigned)imgsz);
|
||||
put32(ih + 32, 2);
|
||||
|
||||
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 &&
|
||||
fwrite(pal, 1, sizeof pal, f) == sizeof pal;
|
||||
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 < stride; x++)
|
||||
row[x] = (unsigned char)((y / 16 + x) & 1 ? 0x00 : 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 copy_file(const char *src, const char *dst)
|
||||
{
|
||||
FILE *in = fopen(src, "rb");
|
||||
if (!in) return -1;
|
||||
FILE *out = fopen(dst, "wb");
|
||||
if (!out) { fclose(in); return -1; }
|
||||
unsigned char buf[16384];
|
||||
size_t n;
|
||||
int ok = 1;
|
||||
while ((n = fread(buf, 1, sizeof buf, in)) > 0)
|
||||
if (fwrite(buf, 1, n, out) != n) { ok = 0; break; }
|
||||
fclose(in);
|
||||
if (fclose(out) != 0) ok = 0;
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
static int have_file(const char *path)
|
||||
{
|
||||
struct stat st;
|
||||
return stat(path, &st) == 0 && st.st_size > 0;
|
||||
}
|
||||
|
||||
static unsigned crc32_buf(const unsigned char *p, size_t n)
|
||||
{
|
||||
unsigned c = 0xFFFFFFFFu;
|
||||
@@ -306,10 +371,47 @@ static int gen_jpeg_fixtures(const char *dir)
|
||||
char cmd[512];
|
||||
snprintf(cmd, sizeof cmd, "python3 test/make_mem_samples.py '%s' >/dev/null 2>&1", dir);
|
||||
if (system(cmd) != 0) return 0;
|
||||
struct stat st;
|
||||
char p[MR_PATH_MAX];
|
||||
snprintf(p, sizeof p, "%s/page.jpg", dir);
|
||||
return stat(p, &st) == 0 && st.st_size > 0;
|
||||
static const char *const need[] = {
|
||||
"page.jpg", "big4.jpg", "prog.jpg",
|
||||
"base2000.jpg", "prog2000.jpg", "restart2000.jpg",
|
||||
"page1600.png", "page1600.bmp", "mp4.jpg", "mp12.jpg",
|
||||
"cmyk.jpg", "adam7.png", "mono1.bmp",
|
||||
"trunc2000.jpg", "hdr3000.bmp", "baddepth.png",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof need / sizeof need[0]; i++) {
|
||||
char p[MR_PATH_MAX];
|
||||
snprintf(p, sizeof p, "%s/%s", dir, need[i]);
|
||||
if (!have_file(p)) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 与 reader_load_page 完全同路径:_step_ex → 兜底降采样 → 落框重采样。
|
||||
* allow_stb 与阅读器一致:判定 backend 时传 0(后端失败即 NULL,绝不回退 stb)。
|
||||
* 返回真实分配峰值(字节),并回报实际路径与最终尺寸。 */
|
||||
static size_t measure_page_decode(const char *path, const mr_img_info *in, int step,
|
||||
int allow_stb, int box_w, int box_h,
|
||||
int *used_backend, int *ow, int *oh)
|
||||
{
|
||||
int dim = box_w > box_h ? box_w : box_h;
|
||||
size_t base = g_live;
|
||||
peak_reset();
|
||||
int used = 0;
|
||||
meow_img *im = meow_img_load_file_step_ex(path, dim, step, allow_stb, &used);
|
||||
if (im && (meow_img_w(im) > dim || meow_img_h(im) > dim)) {
|
||||
meow_img *small = meow_img_downscale(im, dim);
|
||||
if (small) { meow_img_free(im); im = small; }
|
||||
}
|
||||
if (im && in && step > 1 && (in->w >= box_w || in->h >= box_h) &&
|
||||
(meow_img_w(im) != box_w || meow_img_h(im) != box_h)) {
|
||||
meow_img *fit = meow_img_resample(im, box_w, box_h);
|
||||
if (fit) { meow_img_free(im); im = fit; }
|
||||
}
|
||||
if (used_backend) *used_backend = used;
|
||||
if (ow && oh && im) { *ow = meow_img_w(im); *oh = meow_img_h(im); }
|
||||
size_t peak = peak_since(base);
|
||||
if (im) meow_img_free(im);
|
||||
return peak;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
@@ -325,29 +427,33 @@ int main(void)
|
||||
char root[MR_PATH_MAX];
|
||||
snprintf(root, sizeof root, "./mr_membudget_%ld", (long)getpid());
|
||||
char book[MR_PATH_MAX], big[MR_PATH_MAX], pngbook[MR_PATH_MAX], jpgbook[MR_PATH_MAX];
|
||||
char mixed[MR_PATH_MAX], fallback[MR_PATH_MAX];
|
||||
snprintf(book, sizeof book, "%s/small", root);
|
||||
snprintf(big, sizeof big, "%s/big", root);
|
||||
snprintf(pngbook, sizeof pngbook, "%s/png", root);
|
||||
snprintf(jpgbook, sizeof jpgbook, "%s/jpg", root);
|
||||
snprintf(mixed, sizeof mixed, "%s/mixed", root);
|
||||
snprintf(fallback, sizeof fallback, "%s/fallback", root);
|
||||
if (mkdir_p(book) != 0 || mkdir_p(big) != 0 || mkdir_p(pngbook) != 0 ||
|
||||
mkdir_p(jpgbook) != 0) {
|
||||
mkdir_p(jpgbook) != 0 || mkdir_p(mixed) != 0 || mkdir_p(fallback) != 0) {
|
||||
printf("SKIP: mkdir failed\n");
|
||||
return 0;
|
||||
}
|
||||
/* 480x640 BMP ×8:单页解码峰值 ≈ 480*640*4*2 ≈ 2.5MB,能进 3MB 预算(可连翻)。 */
|
||||
/* 480x640 BMP ×8:单页解码峰值 ≈ 480*640*4*2 ≈ 2.5MB,能进 3MB 预算(可连翻)。
|
||||
* 长边 640 ≤ 解码长边 720 → 后端判定「小图走旧入口」→ stb,正好覆盖回退门控。 */
|
||||
for (int i = 0; i < 8; i++) {
|
||||
char p[MR_PATH_MAX];
|
||||
snprintf(p, sizeof p, "%s/p%02d.bmp", book, i);
|
||||
if (write_bmp24(p, 480, 640, (unsigned)i) != 0) { printf("SKIP: bmp write\n"); return 0; }
|
||||
}
|
||||
/* 1200x1600 BMP:模型峰值 ≈ 7.7MB + 目标 ≈ 9MB —— 3MB 常态预算下必须占位,
|
||||
* MemFree 富余(放宽到 12MB)时必须能解出来。 */
|
||||
/* 1200x1600 1bpp BMP(后端拒绝 → stb):stb 峰值 ≈ 7.7MB + 落框 ≈ 9MB。
|
||||
* 3MB 常态预算下必须占位;MemFree 富余(放宽到 12MB)时必须能解出来。 */
|
||||
{
|
||||
char p[MR_PATH_MAX];
|
||||
snprintf(p, sizeof p, "%s/huge.bmp", big);
|
||||
if (write_bmp24(p, 1200, 1600, 3) != 0) { printf("SKIP: bmp write\n"); return 0; }
|
||||
if (write_bmp1(p, 1200, 1600) != 0) { printf("SKIP: bmp write\n"); return 0; }
|
||||
}
|
||||
/* 600x960 PNG(RGB):走「源缓冲 + RGBA 转换」路径(stb 不能解码期缩解)。 */
|
||||
/* 600x960 PNG(RGB):长边 960 > 720 → 走后端;覆盖 PNG 逐行路径。 */
|
||||
for (int i = 0; i < 3; i++) {
|
||||
char p[MR_PATH_MAX];
|
||||
snprintf(p, sizeof p, "%s/q%d.png", pngbook, i);
|
||||
@@ -355,6 +461,31 @@ int main(void)
|
||||
}
|
||||
int have_jpeg = gen_jpeg_fixtures(jpgbook);
|
||||
|
||||
/* 连翻 30 页:6 类典型页循环(12MP / 4MP / baseline / 渐进 / PNG / BMP)。
|
||||
* 每类都必须走后端且不占位——这是「修好了却用不上」的回归防线。 */
|
||||
static const char *const g_mix[] = {
|
||||
"base2000.jpg", "prog2000.jpg", "page1600.png",
|
||||
"page1600.bmp", "mp4.jpg", "mp12.jpg",
|
||||
};
|
||||
const int mix_n = (int)(sizeof g_mix / sizeof g_mix[0]);
|
||||
int mixed_pages = 0;
|
||||
if (have_jpeg) {
|
||||
for (int i = 0; i < 30; i++) {
|
||||
char src[MR_PATH_MAX], dst[MR_PATH_MAX];
|
||||
snprintf(src, sizeof src, "%s/%s", jpgbook, g_mix[i % mix_n]);
|
||||
snprintf(dst, sizeof dst, "%s/p%02d_%s", mixed, i, g_mix[i % mix_n]);
|
||||
if (copy_file(src, dst) == 0) mixed_pages++;
|
||||
}
|
||||
}
|
||||
/* 回退路径集成:CMYK(4 分量)必须占位且不崩。 */
|
||||
int have_fallback = 0;
|
||||
if (have_jpeg) {
|
||||
char src[MR_PATH_MAX], dst[MR_PATH_MAX];
|
||||
snprintf(src, sizeof src, "%s/cmyk.jpg", jpgbook);
|
||||
snprintf(dst, sizeof dst, "%s/cmyk.jpg", fallback);
|
||||
if (copy_file(src, dst) == 0) have_fallback = 1;
|
||||
}
|
||||
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
if (meow_ui_init(&o) != 0) { printf("SKIP: meow_ui_init failed (no host backend)\n"); return 0; }
|
||||
|
||||
@@ -486,47 +617,204 @@ int main(void)
|
||||
pump_ms(200);
|
||||
}
|
||||
|
||||
/* ---- B7 JPEG:模型不低估 + 3MB 阈值下「哪些 JPEG 会变占位」---- */
|
||||
/* ---- B7 M27:6 类典型页 —— 全部走后端、不占位、模型 ≥ 实测且 ≤1.5× ---- */
|
||||
if (have_jpeg) {
|
||||
char p[MR_PATH_MAX];
|
||||
snprintf(p, sizeof p, "%s/page.jpg", jpgbook);
|
||||
long long model = mr_reader_ui_decode_peak_kb(p, 2, 480, 720);
|
||||
size_t b0 = g_live;
|
||||
peak_reset();
|
||||
meow_img *im = meow_img_load_file_step(p, 720, 2);
|
||||
if (im && (meow_img_w(im) != 480 || meow_img_h(im) != 720)) {
|
||||
meow_img *fit = meow_img_resample(im, 480, 720);
|
||||
if (fit) { meow_img_free(im); im = fit; }
|
||||
static const char *const typical[] = {
|
||||
"base2000.jpg", "prog2000.jpg", "restart2000.jpg",
|
||||
"page1600.png", "page1600.bmp", "mp4.jpg", "mp12.jpg",
|
||||
};
|
||||
printf(" [B7] 典型页(fit-width 480,框按源宽高比):\n");
|
||||
printf(" %-16s %-8s %10s %10s %6s\n", "page", "path", "est(kB)", "measured", "ratio");
|
||||
for (size_t i = 0; i < sizeof typical / sizeof typical[0]; i++) {
|
||||
char p[MR_PATH_MAX];
|
||||
snprintf(p, sizeof p, "%s/%s", jpgbook, typical[i]);
|
||||
mr_img_info in;
|
||||
if (!mr_probe_info(p, &in)) { CHECK(0, "probe %s", typical[i]); continue; }
|
||||
int box_w = 480;
|
||||
int box_h = (int)((long)in.h * box_w / in.w);
|
||||
int dim = box_w > box_h ? box_w : box_h;
|
||||
int step = in.codec == MR_IMG_CODEC_JPEG
|
||||
? meow_img_pick_scale(in.w > in.h ? in.w : in.h, dim) : 1;
|
||||
mr_decode_decision dec;
|
||||
reader_estimate_peak(&in, step, box_w, box_h, -1, -1, &dec);
|
||||
int used = 0, ow = 0, oh = 0;
|
||||
size_t measured = measure_page_decode(p, &in, dec.step, 1, box_w, box_h,
|
||||
&used, &ow, &oh) + 1;
|
||||
printf(" %-16s %-8s %10lld %10zu %6.2f\n", typical[i],
|
||||
dec.path == MR_DECODE_PATH_BACKEND ? "backend" : "stb",
|
||||
dec.peak_bytes / 1024, measured / 1024,
|
||||
(double)dec.peak_bytes / (double)measured);
|
||||
CHECK(dec.path == MR_DECODE_PATH_BACKEND, "%s 走后端", typical[i]);
|
||||
CHECK(dec.skip == MR_DECODE_OK, "%s 不占位", typical[i]);
|
||||
CHECK(used == 1, "%s 实际路径=backend", typical[i]);
|
||||
CHECK(dec.peak_bytes <= 2560LL * 1024, "%s 估算 ≤2560kB (%lldkB)",
|
||||
typical[i], dec.peak_bytes / 1024);
|
||||
CHECK(dec.peak_bytes >= (long long)measured, "%s 模型 ≥ 实测 (%lldkB < %zukB)",
|
||||
typical[i], dec.peak_bytes / 1024, measured / 1024);
|
||||
CHECK(dec.peak_bytes <= (long long)measured * 3 / 2, "%s 模型 ≤1.5× 实测",
|
||||
typical[i]);
|
||||
CHECK(ow == box_w && oh == box_h, "%s 输出 %dx%d == 显示框 %dx%d",
|
||||
typical[i], ow, oh, box_w, box_h);
|
||||
}
|
||||
/* 输出尺寸口径:后端直出 == 「stbi_load_scaled(step) + 落框精修」旧路径
|
||||
* (典型页必须逐像素同尺寸;源尺寸不是档位整数倍时 M23 已记录 1px 舍入口径差)。 */
|
||||
for (size_t i = 0; i < sizeof typical / sizeof typical[0]; i++) {
|
||||
char p[MR_PATH_MAX];
|
||||
snprintf(p, sizeof p, "%s/%s", jpgbook, typical[i]);
|
||||
mr_img_info in;
|
||||
if (!mr_probe_info(p, &in)) continue;
|
||||
int box_w = 480;
|
||||
int box_h = (int)((long)in.h * box_w / in.w);
|
||||
int dim = box_w > box_h ? box_w : box_h;
|
||||
int step = in.codec == MR_IMG_CODEC_JPEG
|
||||
? meow_img_pick_scale(in.w > in.h ? in.w : in.h, dim) : 1;
|
||||
int used = 0, ow = 0, oh = 0;
|
||||
measure_page_decode(p, &in, step, 1, box_w, box_h, &used, &ow, &oh);
|
||||
/* 旧路径(与 meow_img_load_file_step 的 stb 分支同式):先 stbi 缩解,再精修。 */
|
||||
int sw = 0, sh = 0, sc = 0, ew = 0, eh = 0;
|
||||
unsigned char *raw = stbi_load_scaled(p, &sw, &sh, &sc, 4, step);
|
||||
if (raw) {
|
||||
if (sw <= dim && sh <= dim) { ew = sw; eh = sh; }
|
||||
else if (sw >= sh) {
|
||||
ew = dim;
|
||||
eh = (int)((long)sh * dim / sw);
|
||||
if (eh < 1) eh = 1;
|
||||
} else {
|
||||
eh = dim;
|
||||
ew = (int)((long)sw * dim / sh);
|
||||
if (ew < 1) ew = 1;
|
||||
}
|
||||
}
|
||||
if (raw) stbi_image_free(raw);
|
||||
CHECK(used == 1 && ow == ew && oh == eh,
|
||||
"%s 两条路径输出尺寸一致(后端 %dx%d / stb %dx%d)",
|
||||
typical[i], ow, oh, ew, eh);
|
||||
}
|
||||
/* 回退路径:后端拒绝的 3 类必须走 stb,且按旧门控占位(不崩、不误放行)。 */
|
||||
static const char *const fallback[] = { "cmyk.jpg", "adam7.png", "mono1.bmp" };
|
||||
printf(" [B7b] 回退路径(必须 stb + 占位):\n");
|
||||
for (size_t i = 0; i < sizeof fallback / sizeof fallback[0]; i++) {
|
||||
char p[MR_PATH_MAX];
|
||||
snprintf(p, sizeof p, "%s/%s", jpgbook, fallback[i]);
|
||||
mr_img_info in;
|
||||
if (!mr_probe_info(p, &in)) { CHECK(0, "probe %s", fallback[i]); continue; }
|
||||
int box_w = 480;
|
||||
int box_h = (int)((long)in.h * box_w / in.w);
|
||||
int dim = box_w > box_h ? box_w : box_h;
|
||||
int step = in.codec == MR_IMG_CODEC_JPEG
|
||||
? meow_img_pick_scale(in.w > in.h ? in.w : in.h, dim) : 1;
|
||||
mr_decode_decision dec;
|
||||
reader_estimate_peak(&in, step, box_w, box_h, -1, -1, &dec);
|
||||
int used = 1, ow = 0, oh = 0;
|
||||
size_t measured = measure_page_decode(p, &in, dec.step, 1, box_w, box_h,
|
||||
&used, &ow, &oh) + 1;
|
||||
printf(" %-16s %-8s %10lld %10zu %6.2f\n", fallback[i],
|
||||
dec.path == MR_DECODE_PATH_BACKEND ? "backend" : "stb",
|
||||
dec.peak_bytes / 1024, measured / 1024,
|
||||
(double)dec.peak_bytes / (double)measured);
|
||||
CHECK(dec.path == MR_DECODE_PATH_STB, "%s 回退 stb", fallback[i]);
|
||||
CHECK(dec.skip == MR_DECODE_SKIP_PEAK, "%s 按旧门控占位", fallback[i]);
|
||||
CHECK(used == 0, "%s 实际路径=stb", fallback[i]);
|
||||
CHECK(dec.peak_bytes >= (long long)measured, "%s stb 模型 ≥ 实测", fallback[i]);
|
||||
CHECK(dec.peak_bytes <= (long long)measured * 3 / 2, "%s stb 模型 ≤1.5× 实测",
|
||||
fallback[i]);
|
||||
if (ow > 0 && oh > 0) {} /* 尺寸仅记录 */
|
||||
}
|
||||
/* ---- B7c(P1-1 回归):头部判支持、解码中途失败的坏文件 ----
|
||||
* 旧实现「后端失败 → 静默回退 stb」实测放出 16–36MB;现在必须:
|
||||
* - 判定 backend 的(截断 JPEG / 头-only 24bpp BMP)→ backend-only 解码返回 NULL,
|
||||
* 峰值只含失败前的小分配(≤3MB),绝不 stb 整幅;
|
||||
* - 判定 stb 的(非法位深 PNG,矩阵已镜像 png_depth_ok)→ skip=peak,不分配。 */
|
||||
static const char *const badmeta[] = { "trunc2000.jpg", "hdr3000.bmp", "baddepth.png" };
|
||||
printf(" [B7c] 坏文件(P1-1 旁路回归):\n");
|
||||
for (size_t i = 0; i < sizeof badmeta / sizeof badmeta[0]; i++) {
|
||||
char p[MR_PATH_MAX];
|
||||
snprintf(p, sizeof p, "%s/%s", jpgbook, badmeta[i]);
|
||||
mr_img_info in;
|
||||
if (!mr_probe_info(p, &in)) { CHECK(0, "probe %s", badmeta[i]); continue; }
|
||||
int box_w = 480;
|
||||
int box_h = (int)((long)in.h * box_w / in.w);
|
||||
int dim = box_w > box_h ? box_w : box_h;
|
||||
int step = in.codec == MR_IMG_CODEC_JPEG
|
||||
? meow_img_pick_scale(in.w > in.h ? in.w : in.h, dim) : 1;
|
||||
mr_decode_decision dec;
|
||||
reader_estimate_peak(&in, step, box_w, box_h, -1, -1, &dec);
|
||||
if (dec.path == MR_DECODE_PATH_BACKEND) {
|
||||
int used = 1, ow = 0, oh = 0;
|
||||
size_t measured = measure_page_decode(p, &in, dec.step, 0, box_w, box_h,
|
||||
&used, &ow, &oh) + 1;
|
||||
printf(" %-16s backend-only measured=%zuKB (backend 失败→NULL)\n",
|
||||
badmeta[i], measured / 1024);
|
||||
CHECK(used == 0, "%s 不走 stb(used=0)", badmeta[i]);
|
||||
CHECK(ow == 0 && oh == 0, "%s 返回 NULL(坏图占位)", badmeta[i]);
|
||||
CHECK(measured <= 3 * 1024 * 1024,
|
||||
"%s backend-only 峰值 ≤3MB (%zuKB)", badmeta[i], measured / 1024);
|
||||
} else {
|
||||
printf(" %-16s stb skip=%d est=%lldkB(门控占位,不分配)\n",
|
||||
badmeta[i], dec.skip, dec.peak_bytes / 1024);
|
||||
CHECK(dec.skip != MR_DECODE_OK, "%s 门控必须占位 (skip=%d)",
|
||||
badmeta[i], dec.skip);
|
||||
CHECK(dec.peak_bytes > 3LL * 1024 * 1024,
|
||||
"%s stb 模型判超预算 (%lldkB)", badmeta[i], dec.peak_bytes / 1024);
|
||||
}
|
||||
}
|
||||
size_t measured = peak_since(b0) / 1024 + 1;
|
||||
int got = im ? 1 : 0;
|
||||
if (im) meow_img_free(im);
|
||||
INFO("jpeg 1000x1500 step=2: model=%lldkB measured=%zukB decoded=%d", model, measured, got);
|
||||
CHECK(got, "jpeg fixture decodes");
|
||||
CHECK(model >= (long long)measured, "jpeg model upper bound (%lldkB < %zukB)", model, measured);
|
||||
CHECK(model <= (long long)measured * 2, "jpeg model not pessimistic (%lldkB vs %zukB)",
|
||||
model, measured);
|
||||
/* 1.5MP(真机崩溃页档位)在 3MB 预算内必须可解:某档 step ≤ cap */
|
||||
long long cap = mr_reader_ui_peak_cap_kb();
|
||||
long long best = mr_reader_ui_decode_peak_kb(p, 8, 480, 720);
|
||||
INFO("jpeg 1000x1500 step=8 model=%lldkB cap=%lldkB", best, cap);
|
||||
CHECK(best <= cap, "1.5MP jpeg fits the 3MB cap at some step (%lldkB > %lldkB)", best, cap);
|
||||
/* 4MP(手机照片档)在 3MB 预算下不可解 → 必须占位(诚实代价,见报告) */
|
||||
snprintf(p, sizeof p, "%s/big4.jpg", jpgbook);
|
||||
long long big = mr_reader_ui_decode_peak_kb(p, 8, 480, 480);
|
||||
INFO("jpeg 2000x2000 step=8 model=%lldkB (cap=%lldkB -> %s)",
|
||||
big, cap, big <= cap ? "decodable" : "placeholder");
|
||||
CHECK(big > cap, "4MP jpeg exceeds the 3MB cap even at step=8 (as documented)");
|
||||
/* 渐进式:识别 + 峰值模型包含 short 系数面(同样尺寸必须比 baseline 高) */
|
||||
snprintf(p, sizeof p, "%s/prog.jpg", jpgbook);
|
||||
long long prog = mr_reader_ui_decode_peak_kb(p, 2, 480, 720);
|
||||
INFO("jpeg 1000x1500 progressive step=2 model=%lldkB", prog);
|
||||
CHECK(prog > model, "progressive jpeg modeled higher than baseline (%lldkB vs %lldkB)",
|
||||
prog, model);
|
||||
} else {
|
||||
printf(" SKIP jpeg assertions: Pillow unavailable (python3 -c 'import PIL')\n");
|
||||
}
|
||||
|
||||
/* ---- B8 M27:连翻 30 页(6 类典型页循环)——不掉页、不占位、峰值 ≤3MB ---- */
|
||||
if (have_jpeg && mixed_pages == 30) {
|
||||
mr_reader_ui_test_set_mem(4096, 16000);
|
||||
mr_reader_open_dir(mixed);
|
||||
pump_ms(1200);
|
||||
CHECK(mr_reader_ui_count() == 30, "mixed book pages=%d", mr_reader_ui_count());
|
||||
int w = meow_ui_width();
|
||||
int advanced = 0, skipped_max = 0;
|
||||
size_t live_steady = g_live;
|
||||
int d0 = mr_reader_ui_decode_count();
|
||||
/* P2-3:只在窗口稳定后 reset 一次,之后不再 reset——这样淘汰旧图不会掩盖
|
||||
* 「分配新图 + 解码」的真实峰值;指标 = 整个 30 翻窗口内相对稳态基线的最大存活增量。
|
||||
* (旧实现每翻页 reset 一次,淘汰把基线拉低,报告里的 536KB 是伪值。) */
|
||||
size_t base = peak_reset();
|
||||
for (int i = 0; i < 30; i++) {
|
||||
int before = mr_reader_ui_page();
|
||||
tap(i % 2 == 0 ? 40 : w - 40, 300);
|
||||
pump_ms(220);
|
||||
if (mr_reader_ui_page() != before) advanced++;
|
||||
if (mr_reader_ui_skipped_pages() > skipped_max)
|
||||
skipped_max = mr_reader_ui_skipped_pages();
|
||||
}
|
||||
size_t transient = g_peak > base ? g_peak - base : 0;
|
||||
INFO("30 mixed flips: advanced=%d skipped=%d decodes=%d steady=%zuKB "
|
||||
"resident=%ldKB max_live_delta=%zuKB",
|
||||
advanced, skipped_max, mr_reader_ui_decode_count() - d0,
|
||||
live_steady / 1024, mr_reader_ui_resident_kb(), transient / 1024);
|
||||
CHECK(advanced == 30, "30 页全部翻到 (%d/30)", advanced);
|
||||
CHECK(skipped_max == 0, "典型页连翻无占位 (max=%d)", skipped_max);
|
||||
CHECK(mr_reader_ui_failed_pages() == 0, "无永久失败页 (%d)",
|
||||
mr_reader_ui_failed_pages());
|
||||
CHECK(transient <= 3 * 1024 * 1024,
|
||||
"30 页连翻任何时刻存活增量 ≤3MB (%zuKB)", transient / 1024);
|
||||
meow_view_page_pop();
|
||||
pump_ms(200);
|
||||
}
|
||||
|
||||
/* ---- B9 M27:回退路径集成 —— CMYK 页占位(可重试)且不崩 ---- */
|
||||
if (have_fallback) {
|
||||
mr_reader_ui_test_set_mem(4096, 16000);
|
||||
mr_reader_open_dir(fallback);
|
||||
pump_ms(900);
|
||||
CHECK(mr_reader_ui_count() == 1, "fallback book pages=%d", mr_reader_ui_count());
|
||||
CHECK(mr_reader_ui_skipped_pages() == 1, "CMYK 页按旧门控占位 (skipped=%d)",
|
||||
mr_reader_ui_skipped_pages());
|
||||
CHECK(mr_reader_ui_failed_pages() == 0, "CMYK 页不是永久失败 (%d)",
|
||||
mr_reader_ui_failed_pages());
|
||||
CHECK(mr_reader_ui_resident_kb() == 0, "CMYK 页未驻留 (resident=%ldkB)",
|
||||
mr_reader_ui_resident_kb());
|
||||
meow_view_page_pop();
|
||||
pump_ms(200);
|
||||
}
|
||||
|
||||
meow_view_page_pop();
|
||||
meow_view_free(reader);
|
||||
meow_view_free(lib);
|
||||
|
||||
+59
-99
@@ -15,11 +15,14 @@
|
||||
* 宽恒为视口宽),不是屏长边,所以翻页/滚动热路径只有原生 blit:不做 meow_ui_img_scaled
|
||||
* 的每帧重采样,也不再每帧 malloc 1MB 级临时缓冲。几何变化(fit/缩放/切模式)时按新几何
|
||||
* 重解一次;拖动/吸附帧只 invalidate 内容区(顶栏静态不重绘)。
|
||||
* 内存(M17 起;M22 按真机实测重定):单页显示缓存 ≤3MB、整窗常驻 ≤3MB(3 槽,超限淘汰
|
||||
* 最远页),单次解码真实峰值常态 ≤3MB —— 超出时 JPEG 先降缩解档位、PNG/BMP 直接占位
|
||||
* (占位可重试,不是永久失败)。峰值模型见 decode_peak_bytes():它按 stb 的真实分配算
|
||||
* (JPEG 分量面、渐进的 short 系数面、PNG 的源缓冲 + RGBA 转换副本),不再是「把 stb 当
|
||||
* 缩解库」的低估式。余量确实充足(MemFree 富余)时允许放宽到 MR_DECODE_PEAK_HARD。
|
||||
* 内存(M17 起;M22 按真机实测重定;M27 接真缩解后端):单页显示缓存 ≤3MB、整窗常驻
|
||||
* ≤3MB(3 槽,超限淘汰最远页)。解码路径与峰值预算由 page_source 的 reader_estimate_peak()
|
||||
* 统一判定(唯一实现,本文件不再另写模型):
|
||||
* - 真缩解后端(M23)能接住的页:峰值只与目标尺寸相关(≈1.3–2.5MB,与源分辨率无关),
|
||||
* 直接放行,不做内存门控;
|
||||
* - 后端拒绝的页(CMYK/算术编码 JPEG、Adam7 PNG、RLE/1–4bpp BMP、N≥8 小图…)回退 stb:
|
||||
* 按 stb 真实分配模型算峰值,超 3MB 占位 / MemAvailable <6MB 占位。
|
||||
* 占位可重试,不是永久失败。余量确实充足(MemFree 富余)时允许放宽到 12MB。
|
||||
* 页数增长不会线性吃内存(常驻只与解码窗大小有关)。
|
||||
* 方向:一律读 M3 的 mr_reader_get_direction()(默认 RTL),不另立全局。
|
||||
*
|
||||
@@ -74,15 +77,10 @@
|
||||
#define MR_JUMP_TIMEOUT_MS 2500 /* 目标页迟迟解不出来 → 降级落位并提示 */
|
||||
#define MR_CONT_SAVE_MS 1000 /* 续读状态落盘节流 */
|
||||
#define MR_TOAST_MS 1400
|
||||
/* ---- 内存预算(M22,按 R3 Pro II 实测校准:MemTotal 56872kB、空闲 RSS 6044kB、
|
||||
* 杀点 RSS≈15.6MB / 当时 MemFree 仅 1.4MB;M20 宿主实测单页解码尖峰 3.4–35MB)---- */
|
||||
#define MR_DECODE_PEAK_CAP (3L * 1024 * 1024) /* 常态单次解码峰值上限(真实模型;3MB 硬阈值) */
|
||||
#define MR_DECODE_PEAK_HARD (12L * 1024 * 1024) /* 余量充足时的绝对峰值上限(不再超老 12MB 档) */
|
||||
#define MR_MEM_FREE_MARGIN_KB (4L * 1024) /* 放宽档:MemFree 必须比峰值多出这么多 */
|
||||
#define MR_MEM_FLOOR_KB (6L * 1024) /* 解码前门控:MemAvailable 低于此值不解码 */
|
||||
/* ---- 内存预算(M22 校准;M27 起模型与门控只在 page_source.c 的 reader_estimate_peak 里)----
|
||||
* 真机参考:MemTotal 56872kB、空闲 RSS 6044kB、杀点 RSS≈15.6MB / MemFree 仅 1.4MB。 */
|
||||
#define MR_PREFETCH_MIN_KB (12L * 1024) /* 预取邻居要求的最低 MemAvailable */
|
||||
#define MR_SKIP_RETRY_MS 1500 /* 被预算跳过的页:焦点页冷却这么久后自动重试 */
|
||||
#define MR_DECODE_SLACK (64L * 1024) /* 峰值模型余量:结构体/行缓冲/分配器开销(实测 5–6KB) */
|
||||
#define MR_PAGE_BYTES_CAP (3L * 1024 * 1024) /* 单页「显示尺寸」缓存字节上限 */
|
||||
#define MR_RESIDENT_CAP (3L * 1024 * 1024) /* 页图缓存总字节上限(M22 由 9MB 校准为 3MB) */
|
||||
#define MR_SLOT_TOL 2 /* 缓存图与显示框允许的像素偏差(≤2 走原生 blit,不重采样) */
|
||||
@@ -420,60 +418,17 @@ static void plan_box_size(int want_w, int want_h, int *bw, int *bh)
|
||||
*bh = h;
|
||||
}
|
||||
|
||||
/* 真实解码峰值模型(M22)。上一版把 stb 当「缩解库」估 (W/s)(H/s)·4,对 JPEG 低估 3.5–8 倍
|
||||
* (M20 宿主实测:2000×3000 baseline +10.1MB、渐进 +23.5MB、1200×1600 PNG +12.9MB)。
|
||||
* 这里按 stb 的真实分配逐项相加(与 stb_image.h 的分配点对齐):
|
||||
* 1) stb 阶段:中间面 planes + 解码输出缓冲 dst(两者并存到解压结束)
|
||||
* JPEG:planes = 分量面(+渐进系数面),dst = (W/s)(H/s)·4(s>1 时只物化抽取输出)
|
||||
* PNG :planes = 源像素缓冲,dst = W·H·4(ch==4 时同一缓冲,planes 记 0)
|
||||
* BMP :planes = 0,dst = W·H·4(stb 直接写 RGBA 目标)
|
||||
* 2) 精修阶段:dst 超长边上限时再落一张 box 大小的图(dst + finished 并存)
|
||||
* 3) 落框阶段:档位偏粗时把 finished 重采样到显示框(finished + box 并存)
|
||||
* 峰值 = 三段的最大值。返回字节数(不是 kB),参数非法返回 0。 */
|
||||
static long long decode_peak_bytes(const mr_img_info *in, int step, int box_w, int box_h)
|
||||
{
|
||||
if (!in || in->w <= 0 || in->h <= 0 || box_w <= 0 || box_h <= 0) return 0;
|
||||
long long W = in->w, H = in->h;
|
||||
long long box = (long long)box_w * box_h * 4;
|
||||
long long dst;
|
||||
if (in->codec == MR_IMG_CODEC_JPEG) {
|
||||
if (step < 1) step = 1;
|
||||
dst = ((W + step - 1) / step) * ((H + step - 1) / step) * 4;
|
||||
} else {
|
||||
dst = W * H * 4; /* PNG/BMP:无解码期缩放,整幅落地 */
|
||||
}
|
||||
long long peak = in->planes + dst; /* stb 阶段 */
|
||||
long long finished = dst > box ? box : dst; /* 精修/落框后的最终图 */
|
||||
if (dst + finished > peak) peak = dst + finished;
|
||||
if (finished + box > peak) peak = finished + box;
|
||||
return peak + MR_DECODE_SLACK; /* 结构体/行缓冲/分配器开销的实测余量 */
|
||||
}
|
||||
|
||||
/* 本次解码的峰值预算:
|
||||
* - 常态 = MR_DECODE_PEAK_CAP(3MB 硬阈值,覆盖基线 6044 + 常驻 3MB + 峰值 3MB ≤ 13.5MB);
|
||||
* - 只有 MemFree 真的富余时才放宽(真机杀点由 MemFree 见底触发,不是固定 RSS),
|
||||
* 放宽上限 = min(MR_DECODE_PEAK_HARD, MemFree - MR_MEM_FREE_MARGIN_KB);
|
||||
* - 读不到 MemFree → 只用常态预算(不猜)。 */
|
||||
static long long decode_peak_budget(long memfree_kb)
|
||||
{
|
||||
long long budget = MR_DECODE_PEAK_CAP;
|
||||
if (memfree_kb >= 0) {
|
||||
long long room = (memfree_kb - MR_MEM_FREE_MARGIN_KB) * 1024;
|
||||
if (room > budget) budget = room;
|
||||
if (budget > MR_DECODE_PEAK_HARD) budget = MR_DECODE_PEAK_HARD;
|
||||
}
|
||||
return budget;
|
||||
}
|
||||
|
||||
/* 固定字段的解码日志(任务 5):page/src/peak/memfree/memavail 之后是结论,
|
||||
* 便于 `grep 'reader decode'` 后按列看曲线。 */
|
||||
/* 固定字段的解码日志:page/src/peak/path 之后是结论,便于 `grep 'reader decode'`
|
||||
* 后按列看曲线。peak = reader_estimate_peak 的预估峰值(字节/kB);path = 实际解码路径
|
||||
* (backend = M23 真缩解后端,stb = 回退旧路径)。 */
|
||||
static void log_decode(int page, int src_w, int src_h, long long peak_bytes,
|
||||
long memfree, long memavail, const char *verdict, int step,
|
||||
unsigned long long ms)
|
||||
int backend, unsigned long long ms)
|
||||
{
|
||||
meow_log(MEOW_LOG_INFO,
|
||||
"reader decode page=%d src=%dx%d peak=%lldkB memfree=%ldkB memavail=%ldkB %s step=%d ms=%llu",
|
||||
page, src_w, src_h, peak_bytes / 1024, memfree, memavail, verdict, step, ms);
|
||||
"reader decode page=%d src=%dx%d peak=%lldkB memfree=%ldkB memavail=%ldkB %s path=%s step=%d ms=%llu",
|
||||
page, src_w, src_h, peak_bytes / 1024, memfree, memavail, verdict,
|
||||
backend ? "backend" : "stb", step, ms);
|
||||
}
|
||||
|
||||
static meow_img *reader_load_page(const char *path, void *ud, int maxdim)
|
||||
@@ -501,50 +456,51 @@ static meow_img *reader_load_page(const char *path, void *ud, int maxdim)
|
||||
long mfree = mem_free_kb();
|
||||
long mavail = mem_avail_kb();
|
||||
|
||||
/* 1) 门控(任务 1):MemAvailable 不足 → 不解码、不分配,直接占位(可重试)。
|
||||
* 阈值 = MR_MEM_FLOOR_KB。读不到(-1)按「可解码」放行,不因探测失败卡死阅读。 */
|
||||
if (mavail >= 0 && mavail < MR_MEM_FLOOR_KB) {
|
||||
g_decode_skip = MR_SKIP_LOWMEM;
|
||||
log_decode(page, 0, 0, 0, mfree, mavail, "skip=lowmem", 1,
|
||||
(unsigned long long)(meow_ui_time_ms() - t0));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 2) 头部探测(零像素内存)→ 真实峰值模型 → 选缩解档位。 */
|
||||
/* 1) 头部探测(零像素内存),再交给 page_source 的唯一模型定径:
|
||||
* reader_estimate_peak 同时给出「走后端还是 stb」与「是否占位」。后端能接住的页
|
||||
* (M23,峰值与源尺寸无关)直接放行;只有回退 stb 的页才参与峰值预算/MemAvailable
|
||||
* 门控。模型不再在本文件另写一份。 */
|
||||
mr_img_info in;
|
||||
int have = mr_probe_info(path, &in);
|
||||
int step = 1;
|
||||
long long peak = 0;
|
||||
mr_decode_decision dec;
|
||||
memset(&dec, 0, sizeof dec);
|
||||
dec.step = 1;
|
||||
if (have) {
|
||||
long long budget = decode_peak_budget(mfree);
|
||||
int step = 1;
|
||||
if (in.codec == MR_IMG_CODEC_JPEG) {
|
||||
/* 从「不欠采样」的档位起步,逐步加粗直到峰值进预算。
|
||||
* 分量面与档位无关,所以大页最终仍会无档可用 → 占位(诚实代价)。 */
|
||||
int l0 = in.w > in.h ? in.w : in.h;
|
||||
step = meow_img_pick_scale(l0, dim);
|
||||
for (;;) {
|
||||
peak = decode_peak_bytes(&in, step, box_w, box_h);
|
||||
if (peak <= budget) break;
|
||||
if (step >= 8) { step = 8; break; }
|
||||
step *= 2;
|
||||
}
|
||||
} else {
|
||||
step = 1;
|
||||
peak = decode_peak_bytes(&in, step, box_w, box_h);
|
||||
}
|
||||
if (peak > budget) {
|
||||
g_decode_skip = MR_SKIP_PEAK;
|
||||
log_decode(page, in.w, in.h, peak, mfree, mavail, "skip=peak", step,
|
||||
for (;;) {
|
||||
reader_estimate_peak(&in, step, box_w, box_h, mfree, mavail, &dec);
|
||||
/* 后端命中 / 已进预算 / 已到最粗档位:定案。非 JPEG 无档位可调。 */
|
||||
if (dec.path == MR_DECODE_PATH_BACKEND || dec.skip == MR_DECODE_OK ||
|
||||
step >= 8 || in.codec != MR_IMG_CODEC_JPEG)
|
||||
break;
|
||||
step *= 2;
|
||||
}
|
||||
if (dec.skip != MR_DECODE_OK) {
|
||||
g_decode_skip = dec.skip == MR_DECODE_SKIP_LOWMEM ? MR_SKIP_LOWMEM : MR_SKIP_PEAK;
|
||||
log_decode(page, in.w, in.h, dec.peak_bytes, mfree, mavail,
|
||||
dec.skip == MR_DECODE_SKIP_LOWMEM ? "skip=lowmem" : "skip=peak",
|
||||
dec.step, dec.path == MR_DECODE_PATH_BACKEND,
|
||||
(unsigned long long)(meow_ui_time_ms() - t0));
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* 3) 解码(档位已定)。失败与「跳过」必须分开:NULL + skip==0 才是坏图。 */
|
||||
meow_img *im = have ? meow_img_load_file_step(path, dim, step)
|
||||
/* 2) 解码(路径/档位已定)。失败与「跳过」必须分开:NULL + skip==0 才是坏图。
|
||||
* P1-1:门控按后端放行的页必须用 backend-only 入口——后端中途失败(截断/坏流/
|
||||
* DHT 超限/malloc 失败)直接返回 NULL 按坏图占位,绝不静默回落 stb(实测会放出
|
||||
* 18–36MB)。只有判定为 stb 的页才允许 stb。 */
|
||||
int allow_stb = !have || dec.path != MR_DECODE_PATH_BACKEND;
|
||||
int used_backend = 0;
|
||||
meow_img *im = have ? meow_img_load_file_step_ex(path, dim, dec.step, allow_stb,
|
||||
&used_backend)
|
||||
: meow_img_load_file_dim(path, dim);
|
||||
if (!im) {
|
||||
log_decode(page, have ? in.w : 0, have ? in.h : 0, peak, mfree, mavail, "fail", step,
|
||||
log_decode(page, have ? in.w : 0, have ? in.h : 0, dec.peak_bytes, mfree, mavail, "fail",
|
||||
dec.step, have && dec.path == MR_DECODE_PATH_BACKEND,
|
||||
(unsigned long long)(meow_ui_time_ms() - t0));
|
||||
return NULL;
|
||||
}
|
||||
@@ -553,16 +509,17 @@ static meow_img *reader_load_page(const char *path, void *ud, int maxdim)
|
||||
meow_img *small = meow_img_downscale(im, dim);
|
||||
if (small) { meow_img_free(im); im = small; }
|
||||
}
|
||||
/* 4) 精确落到显示框(仅当「故意用了粗档位」时):结果 = box → 命中页仍是一次原生 blit,
|
||||
/* 3) 精确落到显示框(仅当「故意用了粗档位」时):结果 = box → 命中页仍是一次原生 blit,
|
||||
* 不会把内存换来的降级变成每帧重采样(M17 的热路径纪律)。
|
||||
* 源图本身就小于显示框(小图/缩略图)时绝不放大:那会凭空多占常驻字节,
|
||||
* 且 M17 的 slot_ready 语义依赖「SDK 不做上采样」。 */
|
||||
if (have && step > 1 && (in.w >= box_w || in.h >= box_h) &&
|
||||
if (have && dec.step > 1 && (in.w >= box_w || in.h >= box_h) &&
|
||||
(meow_img_w(im) != box_w || meow_img_h(im) != box_h)) {
|
||||
meow_img *fit = meow_img_resample(im, box_w, box_h);
|
||||
if (fit) { meow_img_free(im); im = fit; }
|
||||
}
|
||||
log_decode(page, have ? in.w : 0, have ? in.h : 0, peak, mfree, mavail, "ok", step,
|
||||
log_decode(page, have ? in.w : 0, have ? in.h : 0, dec.peak_bytes, mfree, mavail, "ok",
|
||||
dec.step, used_backend,
|
||||
(unsigned long long)(meow_ui_time_ms() - t0));
|
||||
return im;
|
||||
}
|
||||
@@ -2552,15 +2509,18 @@ void mr_reader_ui_test_set_mem(long free_kb, long avail_kb)
|
||||
g_mem_free_override = free_kb;
|
||||
g_mem_avail_override = avail_kb;
|
||||
}
|
||||
/* 峰值模型自检(只读):用真代码的模型算某个文件的预估峰值,供宿主测试与实测对照。 */
|
||||
/* 峰值模型自检(只读):用真代码的模型算某个文件的预估峰值,供宿主测试与实测对照。
|
||||
* 走 page_source 的唯一实现(memfree/mavail 传 -1 = 只算模型,不注入门控读数)。 */
|
||||
long long mr_reader_ui_decode_peak_kb(const char *path, int step, int box_w, int box_h)
|
||||
{
|
||||
mr_img_info in;
|
||||
mr_decode_decision dec;
|
||||
if (!path || !mr_probe_info(path, &in)) return 0;
|
||||
return decode_peak_bytes(&in, step, box_w, box_h) / 1024;
|
||||
reader_estimate_peak(&in, step, box_w, box_h, -1, -1, &dec);
|
||||
return dec.peak_bytes / 1024;
|
||||
}
|
||||
long mr_reader_ui_peak_cap_kb(void) { return (long)(MR_DECODE_PEAK_CAP / 1024); }
|
||||
long mr_reader_ui_peak_budget_kb(void) { return (long)(decode_peak_budget(mem_free_kb()) / 1024); }
|
||||
long mr_reader_ui_peak_cap_kb(void) { return (long)(reader_peak_budget_bytes(-1) / 1024); }
|
||||
long mr_reader_ui_peak_budget_kb(void) { return (long)(reader_peak_budget_bytes(mem_free_kb()) / 1024); }
|
||||
long mr_reader_ui_resident_cap_kb(void) { return (long)(MR_RESIDENT_CAP / 1024); }
|
||||
int mr_reader_ui_blit_draws(void) { return g_blit_draws; }
|
||||
int mr_reader_ui_scaled_draws(void) { return g_scaled_draws; }
|
||||
|
||||
Reference in new issue
Block a user