图库: 门控模型接真缩解后端 + _step 全格式后端(M27)
M23 交付了真缩解后端却「修好了用不上」:_step 只接 JPEG,而阅读器的峰值模型
仍是 stb 口径(PNG/BMP 按 W·H·4、JPEG 按缩解后整幅 RGBA),12MP/PNG 页全被判
超预算占位。本次把两处缝焊上:
- sdk/meow_ui_img.c:meow_img_load_file_step 的 JPEG-only 限制放开到 PNG/BMP;
新增 meow_img_load_file_step_ex 回报实际路径(backend/stb),日志可核对预判。
- tuku/logic/reader/page_source.[ch]:新增 reader_estimate_peak(),成为峰值模型 +
预算 + MemAvailable 门控的唯一实现(reader 不再另写一份)。
* 后端命中(JPEG SOF0/1/2、≤3 分量、8bit、N=1..7;PNG 非交错/标准压缩/合法色型;
BMP DIB≥40/BI_RGB/8·16·24·32bpp)→ 峰值 = 目标图 + 分量平面/行缓冲 + 固定开销,
与源分辨率无关 → 直接放行,不门控;
* 只有回退 stb(CMYK/算术编码、Adam7、RLE/1-4bpp、N≥8 小图等)才按 M22 stb 模型
算峰值,超 3MB 或 MemAvailable <6MB 才占位。Adam7 另补 stb 的整幅暂存上界。
- reader:删掉本地 stb 模型/常量,统一调 reader_estimate_peak;日志加 path=backend|stb。
- 测试:6 类典型页(2000×3000 baseline/渐进/restart、1600×2400 PNG/BMP、4MP、12MP)
断言全部 backend + 不占位 + 估算 ≥ 实测且 ≤1.5×(实测 1.03–1.08×);两条路径输出
尺寸逐像素一致;CMYK/Adam7/1bpp 断言走 stb 并按旧门控占位;连翻 30 页无占位、
单页分配峰值 ≤3MB;membudget 链接补 meow_ui_time.c 与 M23 两个解码 TU。
This commit is contained in:
1 parent
668629bda3
commit
1725af631b
9 files changed
+797
-195
No files matched your search
@@ -1,38 +1,98 @@
|
||||
#!/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")
|
||||
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 \
|
||||
|
||||
@@ -97,7 +97,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, 1); /* bpp=1:M23 后端不支持 → 回退 stb(M27 门控仍生效) */
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
int ok = fwrite(b, 1, sizeof b, f) == sizeof b;
|
||||
@@ -273,7 +273,9 @@ int main(void)
|
||||
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)。 */
|
||||
/* 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());
|
||||
CHECK(mr_reader_ui_skipped_pages() == 1,
|
||||
|
||||
@@ -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,14 @@
|
||||
* 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,按旧门控占位
|
||||
* 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 +28,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 +162,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 +369,44 @@ 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",
|
||||
};
|
||||
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 → 兜底降采样 → 落框重采样。
|
||||
* 返回真实分配峰值(字节),并回报实际路径与最终尺寸。 */
|
||||
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 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);
|
||||
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 +422,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 +456,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 +612,168 @@ 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, 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, 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, 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) {} /* 尺寸仅记录 */
|
||||
}
|
||||
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;
|
||||
size_t peak_steady = g_peak;
|
||||
int d0 = mr_reader_ui_decode_count();
|
||||
size_t peak_max = 0;
|
||||
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;
|
||||
INFO("30 mixed flips: advanced=%d skipped=%d decodes=%d steady=%zuKB "
|
||||
"max_step_peak=%zuKB cumulative=%zuKB",
|
||||
advanced, skipped_max, mr_reader_ui_decode_count() - d0,
|
||||
live_steady / 1024, peak_max / 1024, 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);
|
||||
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);
|
||||
|
||||
Reference in new issue
Block a user