diff --git a/sdk/include/meow_ui_image.h b/sdk/include/meow_ui_image.h index 805400a..028a8f8 100644 --- a/sdk/include/meow_ui_image.h +++ b/sdk/include/meow_ui_image.h @@ -62,10 +62,15 @@ int meow_img_pick_scale(int longside, int maxdim); * 断言与实测见 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 完全相同,额外回报**实际**解码路径: - * used_backend = 1(M23 后端直出目标尺寸)/ 0(回退 stb)。阅读器用它把「门控预判」 - * 与「真实路径」对上日志;宿主测试据此断言后端命中。NULL 时等价 _step。 */ -meow_img *meow_img_load_file_step_ex(const char *path, int maxdim, int step, int *used_backend); +/* 追加(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 diff --git a/sdk/meow_ui_img.c b/sdk/meow_ui_img.c index e4d34ad..4de3d31 100644 --- a/sdk/meow_ui_img.c +++ b/sdk/meow_ui_img.c @@ -101,18 +101,26 @@ meow_img *meow_img_load_file_dim(const char *path, int maxdim) /* 追加(M22):档位由调用方指定。与 meow_img_load_file_dim 同语义(结果长边 ≤ maxdim, * 超了再精修一次),只是档位不再由 maxdim 反推 —— 内存预算紧时可选更粗的档位。 * M27:真缩解后端优先(step 折算成目标长边 ceil(长边/step),后端按该长边选 DCT 阶, - * 峰值只与输出尺寸相关);后端不支持时回退 stbi_load_scaled(step),语义与 M22 一致。 */ -meow_img *meow_img_load_file_step_ex(const char *path, int maxdim, int step, int *used_backend) + * 峰值只与输出尺寸相关);后端不支持时回退 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); + 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 时不精修, @@ -130,6 +138,7 @@ meow_img *meow_img_load_file_step_ex(const char *path, int maxdim, int step, int 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); @@ -137,7 +146,8 @@ meow_img *meow_img_load_file_step_ex(const char *path, int maxdim, int step, int meow_img *meow_img_load_file_step(const char *path, int maxdim, int step) { - return meow_img_load_file_step_ex(path, maxdim, step, NULL); + /* 通用入口保持 M22 语义:后端失败允许回退 stb。 */ + return meow_img_load_file_step_ex(path, maxdim, step, 1, NULL); } /* 追加(M22):精确重采样到目标尺寸(最近邻,双向)。阅读器把缩解档位的结果一次性 diff --git a/tuku/logic/reader/page_source.c b/tuku/logic/reader/page_source.c index 8a0c9e1..bed0546 100644 --- a/tuku/logic/reader/page_source.c +++ b/tuku/logic/reader/page_source.c @@ -486,6 +486,16 @@ static int mr_png_bits(int color, int depth) } } +/* 位深合法性(与 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) @@ -511,7 +521,7 @@ static int mr_backend_supported(const mr_img_info *in, int eff) 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_bits(in->color, in->depth) == 0) return 0; + if (!mr_png_depth_ok(in->color, in->depth)) return 0; return 1; } case MR_IMG_CODEC_BMP: @@ -648,12 +658,19 @@ void reader_estimate_peak(const mr_img_info *in, int step, int box_w, int box_h, int longside = in->w > in->h ? in->w : in->h; int eff = mr_effective_dim(longside, step, dim); - /* 1) 先看后端能否接住:命中 → 直接放行(后端峰值与源尺寸无关,不需要门控)。 */ + /* 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); - out->skip = MR_DECODE_OK; + 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; } diff --git a/tuku/logic/reader/page_source.h b/tuku/logic/reader/page_source.h index fab2893..024a959 100644 --- a/tuku/logic/reader/page_source.h +++ b/tuku/logic/reader/page_source.h @@ -140,7 +140,8 @@ int mr_probe_info_mem(const void *buf, size_t len, mr_img_info *info); * * 参数:step 与 meow_img_load_file_step 同义(JPEG 档位提示);box_w/box_h 是阅读器 * 最终显示框;memfree_kb/memavail_kb 读不到时传 -1。out 必须非 NULL。 - * 估算口径保证 ≥ 实测(--wrap malloc)且不超过实测 1.5×(见 tuku/test 断言)。 */ + * 估算精度口径:**后端命中页 / 典型页**保证估算 ≥ 实测(--wrap malloc)且 ≤1.5×; + * 回退 stb 的小图只保证门控安全(可能保守,不强求 1.5× 上界)。断言见 tuku/test。 */ #define MR_DECODE_PATH_STB 0 #define MR_DECODE_PATH_BACKEND 1 diff --git a/tuku/test/make_mem_samples.py b/tuku/test/make_mem_samples.py index 928c3b0..8c3f0ee 100644 --- a/tuku/test/make_mem_samples.py +++ b/tuku/test/make_mem_samples.py @@ -93,6 +93,40 @@ def main(): 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("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 diff --git a/tuku/test/test_reader_many_pages.c b/tuku/test/test_reader_many_pages.c index 463d80c..4f2b3da 100644 --- a/tuku/test/test_reader_many_pages.c +++ b/tuku/test/test_reader_many_pages.c @@ -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, 1); /* bpp=1:M23 后端不支持 → 回退 stb(M27 门控仍生效) */ + 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,18 +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/M27:坏图 = 永久占位(failed);超预算的**回退路径**大页 = 预算跳过(skipped, - * 可重试,不是 failed)。M27 起后端能接住的 24bpp 大图会直接解出,所以这里用 1bpp - * (后端拒绝 → stb)保证仍在验证门控,而不是「大图一律占位」的旧语义。 */ - 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 页排版 + 跳页落位 ---- */ diff --git a/tuku/test/test_reader_membudget.c b/tuku/test/test_reader_membudget.c index e4897bb..e83cae6 100644 --- a/tuku/test/test_reader_membudget.c +++ b/tuku/test/test_reader_membudget.c @@ -15,7 +15,9 @@ * B7 M27:6 类典型页(2000×3000 baseline/渐进/restart、1600×2400 PNG/BMP、4MP、12MP) * 全部走后端、不占位;估算 ≥ 实测且 ≤1.5×;输出尺寸与显示框一致 * B7b 回退路径:CMYK / Adam7 PNG / 1bpp BMP 走 stb,按旧门控占位 - * B8 M27:连翻 30 页(6 类循环)不掉页、不占位、分配峰值 ≤3MB + * 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 写;JPEG/PNG/BMP 大夹具由 python3+Pillow 生成 @@ -374,6 +376,7 @@ static int gen_jpeg_fixtures(const char *dir) "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]; @@ -384,15 +387,17 @@ static int gen_jpeg_fixtures(const char *dir) } /* 与 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 box_w, int box_h, int *used_backend, int *ow, int *oh) + 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, &used); + 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; } @@ -633,7 +638,7 @@ int main(void) 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, box_w, box_h, + 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", @@ -664,7 +669,7 @@ int main(void) 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, box_w, box_h, &used, &ow, &oh); + 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); @@ -701,7 +706,7 @@ int main(void) 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, box_w, box_h, + 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", @@ -715,6 +720,44 @@ int main(void) 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); + } + } } else { printf(" SKIP jpeg assertions: Pillow unavailable (python3 -c 'import PIL')\n"); } @@ -728,32 +771,30 @@ int main(void) int w = meow_ui_width(); int advanced = 0, skipped_max = 0; size_t live_steady = g_live; - size_t peak_steady = g_peak; int d0 = mr_reader_ui_decode_count(); - size_t peak_max = 0; + /* 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(); - size_t base = g_live; - peak_reset(); /* 每页单独量「相对当前存活」的瞬时增量 */ tap(i % 2 == 0 ? 40 : w - 40, 300); pump_ms(220); - size_t step_peak = g_peak > base ? g_peak - base : 0; - if (step_peak > peak_max) peak_max = step_peak; 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 > peak_steady ? g_peak - peak_steady : 0; + size_t transient = g_peak > base ? g_peak - base : 0; INFO("30 mixed flips: advanced=%d skipped=%d decodes=%d steady=%zuKB " - "max_step_peak=%zuKB cumulative=%zuKB", + "resident=%ldKB max_live_delta=%zuKB", advanced, skipped_max, mr_reader_ui_decode_count() - d0, - live_steady / 1024, peak_max / 1024, transient / 1024); + 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(peak_max <= 3 * 1024 * 1024, - "30 页连翻单页分配峰值 ≤3MB (%zuKB)", peak_max / 1024); + CHECK(transient <= 3 * 1024 * 1024, + "30 页连翻任何时刻存活增量 ≤3MB (%zuKB)", transient / 1024); meow_view_page_pop(); pump_ms(200); } diff --git a/tuku/ui/reader/reader.c b/tuku/ui/reader/reader.c index 24b528a..ded7039 100644 --- a/tuku/ui/reader/reader.c +++ b/tuku/ui/reader/reader.c @@ -490,13 +490,17 @@ static meow_img *reader_load_page(const char *path, void *ud, int maxdim) } /* 2) 解码(路径/档位已定)。失败与「跳过」必须分开:NULL + skip==0 才是坏图。 - * step_ex 回报实际路径,日志据此区分 backend / stb(决策与实际必须一致)。 */ + * 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, &used_backend) + 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, dec.peak_bytes, mfree, mavail, "fail", - dec.step, used_backend, + dec.step, have && dec.path == MR_DECODE_PATH_BACKEND, (unsigned long long)(meow_ui_time_ms() - t0)); return NULL; }