Tuku reader: 真实解码峰值模型 + 3MB 硬阈值 + 解码取消 + 3 槽(M22 止损版)
真机(R3 Pro II 480×720 / MemTotal 56872kB):空闲 RSS 6044kB → 翻页 11400kB → 峰值 15600kB(MemFree 仅 1404kB)→ SIGKILL。M20 审计坐实根因是「单页解码瞬时峰值」, 且旧模型把 stb 当缩解库,对 JPEG 低估 3.5–8 倍(实测 baseline 2000×3000 +10.1MB、 渐进 +23.5MB、1200×1600 PNG +12.9MB)。本次按实测重定预算: - 真实峰值模型 decode_peak_bytes():JPEG 分量面(+渐进 short 系数面)、PNG 源缓冲 + RGBA 转换副本、BMP 直写目标,再叠精修/落框两段一次性重采样;上限常态 3MB (MR_DECODE_PEAK_CAP),只有 MemFree 真富余时才放宽到 min(12MB, MemFree-4MB)。 - 超预算时的降级:JPEG 逐步加粗 stbi 缩解档位(meow_img_load_file_step), PNG/BMP 无解码期缩放 → 占位;结果精确重采样回显示框(meow_img_resample), 命中页仍是单次 blit(M17 热路径纪律不破)。 - 占位可重试(g_page_skip 与永久失败 g_page_fail 分离):焦点页冷却 1.5s 自动重试、 中间区点按立即重试、离开再回来即重试;坏图仍是永久占位。 - 解码前内存门控:MemAvailable < 6MB 不解码(读不到就放行,不猜)。 - 解码请求取消:worker 出队后、mr_book_open 前二次判 gen/窗口;拖动/fling 期间只解落点页, 预取邻居仅当 MemAvailable ≥ 12MB。 - 先释放再解码:queue_page 按落地字节 slots_reserve() 先淘汰不可见旧图;可见页不淘汰。 - 槽位 4→3、常驻上限 9MB→3MB、结果队列 4→2。 - 可观测性:`reader decode page=N src=WxH peak=XkB memfree=YkB memavail=ZkB ok|skip=… step=S ms=M` 与进出阅读器 rss/hwm/memfree/memavail。 page_source: 头部探测新增 codec/progressive/channels/planes(mr_probe_info), 只带最小 SOF 的合成头仍能取尺寸(旧契约不退化)。 宿主证据:新增 test_reader_membudget(--wrap malloc 计真实峰值): PNG 600×960 model 4001kB vs 实测 3943kB;JPEG 1000×1500 step2 model 3749/实测 3711; 连翻 100 页 peak_transient 0kB(先释放再解码让缓冲复用)、驻留 ≤3MB、解码 100 次(取消生效)、 3MB 阈值下 1.5MP JPEG 可解、4MP JPEG 需 MemFree 放行(否则占位)、渐进式模型更高于 baseline。 run_tests.sh 全绿(含 zip 106 / cbz 325)、run_webtoon_tests.sh 全绿、SDK 与 tuku 交叉构建零警告。
This commit is contained in:
1 parent
773ab9ff92
commit
40751c09bd
10 files changed
+1257
-166
No files matched your search
@@ -34,6 +34,22 @@ int meow_img_probe_file(const char *path, int *w, int *h, int *channels);
|
||||
meow_img *meow_img_load_file_dim(const char *path, int maxdim);
|
||||
meow_img *meow_img_load_mem_dim(const void *buf, int len, int maxdim);
|
||||
|
||||
/* 追加(M22)缩解档位选择:取满足「长边/档位 ≥ maxdim」的最大整数档 s ∈ {1,2,4,8},
|
||||
* 保证缩解结果不欠采样。meow_img_load_file_dim 内部用的就是这个函数;调用方(阅读器)
|
||||
* 用它枚举候选档位做内存预算,避免两处各写一份而漂移。 */
|
||||
int meow_img_pick_scale(int longside, int maxdim);
|
||||
|
||||
/* 追加(M22)显式档位解码:语义与 meow_img_load_file_dim 相同(结果长边 ≤ maxdim,
|
||||
* 超了就再精修一次),只是缩解档位由调用方决定。内存吃紧时可以选更粗的档位——输出更小、
|
||||
* 峰值更低,代价是细节变软。step <= 1 等价长边约束入口;step > 8 按 8 处理。
|
||||
* 非 JPEG 格式 stb 没有解码期缩放,step 被忽略(整幅解码)。 */
|
||||
meow_img *meow_img_load_file_step(const char *path, int maxdim, int step);
|
||||
|
||||
/* 追加(M22)精确重采样到 w×h(最近邻,缩小/放大都支持)。阅读器用它把「缩解档位」的
|
||||
* 结果一次性落到显示框,使命中页仍是单次原生 blit(不做每帧重采样)。同尺寸返回 NULL
|
||||
* (调用方保留原图);失败返回 NULL。 */
|
||||
meow_img *meow_img_resample(meow_img *im, int w, int h);
|
||||
|
||||
int meow_img_w(const meow_img *img);
|
||||
int meow_img_h(const meow_img *img);
|
||||
/* 访问器:取 RGBA 像素指针(4 通道 straight alpha;供取色/直读像素)。meow_img 对外 opaque,
|
||||
|
||||
+33
-3
@@ -45,7 +45,7 @@ meow_img *meow_img_load_file(const char *path)
|
||||
/* 追加(M12):预算解码。选整数缩解档位 s ∈ {1,2,4,8}——取满足「长边/s ≥ maxdim」的最大档,
|
||||
* 保证缩解结果不欠采样(不丢细节),再由 meow_img_downscale 精修到 maxdim。长边 < 2*maxdim
|
||||
* 时无档可用(s=1),退回整幅解码后精修——此时本就接近目标尺寸,无需牺牲。 */
|
||||
static int img_pick_scale(int longside, int maxdim)
|
||||
int meow_img_pick_scale(int longside, int maxdim)
|
||||
{
|
||||
int s = 1;
|
||||
while (s < 8 && (longside / (s * 2)) >= maxdim) s *= 2;
|
||||
@@ -83,10 +83,40 @@ meow_img *meow_img_load_file_dim(const char *path, int maxdim)
|
||||
if (!stbi_info(path, &w, &h, &c)) return NULL;
|
||||
int longside = w > h ? w : h;
|
||||
if (longside <= maxdim) return meow_img_load_file(path); /* 小图不动:旧路径 */
|
||||
unsigned char *p = stbi_load_scaled(path, &w, &h, &c, 4, img_pick_scale(longside, maxdim));
|
||||
unsigned char *p = stbi_load_scaled(path, &w, &h, &c, 4, meow_img_pick_scale(longside, maxdim));
|
||||
return img_finish_dim(p, w, h, maxdim);
|
||||
}
|
||||
|
||||
/* 追加(M22):档位由调用方指定。与 meow_img_load_file_dim 同语义(结果长边 ≤ maxdim,
|
||||
* 超了再精修一次),只是档位不再由 maxdim 反推 —— 内存预算紧时可选更粗的档位。 */
|
||||
meow_img *meow_img_load_file_step(const char *path, int maxdim, int step)
|
||||
{
|
||||
if (!path) return NULL;
|
||||
if (step <= 1 && maxdim <= 0) return meow_img_load_file(path);
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
/* 追加(M22):精确重采样到目标尺寸(最近邻,双向)。阅读器把缩解档位的结果一次性
|
||||
* 落到显示框,之后命中页就是单次原生 blit(M17 的热路径纪律不被粗档位破坏)。 */
|
||||
static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover);
|
||||
|
||||
meow_img *meow_img_resample(meow_img *im, int w, int h)
|
||||
{
|
||||
if (!im || w <= 0 || h <= 0) return NULL;
|
||||
if (im->w == w && im->h == h) return NULL;
|
||||
unsigned char *tmp = scale_buf(im, w, h, 0);
|
||||
if (!tmp) return NULL;
|
||||
return img_alloc(w, h, tmp);
|
||||
}
|
||||
|
||||
meow_img *meow_img_load_mem_dim(const void *buf, int len, int maxdim)
|
||||
{
|
||||
if (!buf || len <= 0) return NULL;
|
||||
@@ -97,7 +127,7 @@ meow_img *meow_img_load_mem_dim(const void *buf, int len, int maxdim)
|
||||
if (longside <= maxdim) return meow_img_load_mem(buf, len);
|
||||
unsigned char *p = stbi_load_scaled_from_memory((const unsigned char *)buf, len,
|
||||
&w, &h, &c, 4,
|
||||
img_pick_scale(longside, maxdim));
|
||||
meow_img_pick_scale(longside, maxdim));
|
||||
return img_finish_dim(p, w, h, maxdim);
|
||||
}
|
||||
|
||||
|
||||
+121
-27
@@ -192,7 +192,7 @@ static unsigned probe_be32(const unsigned char *p)
|
||||
return ((unsigned)p[0] << 24) | ((unsigned)p[1] << 16) | ((unsigned)p[2] << 8) | p[3];
|
||||
}
|
||||
|
||||
static int probe_png(probe_src *s, int *w, int *h)
|
||||
static int probe_png(probe_src *s, mr_img_info *in)
|
||||
{
|
||||
unsigned char hdr[24];
|
||||
if (!ps_read(s, hdr, sizeof hdr)) return 0;
|
||||
@@ -200,12 +200,33 @@ static int probe_png(probe_src *s, int *w, int *h)
|
||||
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;
|
||||
*w = (int)ww;
|
||||
*h = (int)hh;
|
||||
/* IHDR 的位深/颜色类型(紧跟在宽高后)。截断头(stb 也读不下去)按 RGB 8bit 记。 */
|
||||
int depth = 8, ctype = 2;
|
||||
unsigned char ex[2];
|
||||
if (ps_read(s, ex, sizeof ex)) { depth = ex[0]; ctype = ex[1]; }
|
||||
int ch;
|
||||
switch (ctype) {
|
||||
case 0: ch = 1; break; /* 灰度 */
|
||||
case 2: ch = 3; break; /* RGB */
|
||||
case 3: ch = 3; break; /* 调色板:stb 展开成 RGB(A) */
|
||||
case 4: ch = 2; break; /* 灰度+alpha */
|
||||
case 6: ch = 4; break; /* RGBA */
|
||||
default: ch = 3; break;
|
||||
}
|
||||
if (ww > 0x7FFFFFu || hh > 0x7FFFFFu) return 0; /* 防 int 溢出 */
|
||||
int bpc = (depth == 16) ? 2 : 1;
|
||||
in->w = (int)ww;
|
||||
in->h = (int)hh;
|
||||
in->codec = MR_IMG_CODEC_PNG;
|
||||
in->channels = ch;
|
||||
in->progressive = 0;
|
||||
/* ch==4:stb 直接解到目标 RGBA 缓冲(就是最终输出),中间面记 0。
|
||||
* 其余:stb 先解出 ch 通道源缓冲,再 convert_format 复制一张 RGBA。 */
|
||||
in->planes = (ch == 4) ? 0 : (long long)ww * hh * ch * bpc;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int probe_jpeg(probe_src *s, int *w, int *h)
|
||||
static int probe_jpeg(probe_src *s, mr_img_info *in)
|
||||
{
|
||||
unsigned char b[2];
|
||||
if (!ps_read(s, b, 2) || b[0] != 0xFF || b[1] != 0xD8) return 0;
|
||||
@@ -224,20 +245,61 @@ static int probe_jpeg(probe_src *s, int *w, int *h)
|
||||
if (len < 2) return 0;
|
||||
int is_sof = (m >= 0xC0 && m <= 0xCF) && m != 0xC4 && m != 0xC8 && m != 0xCC;
|
||||
if (is_sof) {
|
||||
/* 5 字节就够拿宽高(精度 + 高 + 宽);分量数与采样因子能读就读。
|
||||
* 只带最小 SOF 的合成头(宿主测试夹具)也必须能取到尺寸——尺寸探测是
|
||||
* M4 起的硬契约,不能因为预算模型要更多信息就退化。 */
|
||||
unsigned char so[5];
|
||||
if (len < 7 || !ps_read(s, so, 5)) return 0;
|
||||
if (len < 7 || !ps_read(s, so, sizeof so)) return 0;
|
||||
int hh = ((int)so[1] << 8) | so[2];
|
||||
int ww = ((int)so[3] << 8) | so[4];
|
||||
if (ww <= 0 || hh <= 0) return 0;
|
||||
*w = ww;
|
||||
*h = hh;
|
||||
int prog = (m == 0xC2 || m == 0xC6 || m == 0xCA || m == 0xCE);
|
||||
in->w = ww;
|
||||
in->h = hh;
|
||||
in->codec = MR_IMG_CODEC_JPEG;
|
||||
in->progressive = prog;
|
||||
in->channels = 3;
|
||||
/* 未知采样因子时按 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 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];
|
||||
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;
|
||||
if (cv > vmax) vmax = cv;
|
||||
}
|
||||
for (int i = 0; ok && i < nc; i++) {
|
||||
if (hmax % (hv[i] >> 4) != 0 || vmax % (hv[i] & 15) != 0) ok = 0;
|
||||
}
|
||||
if (ok) {
|
||||
/* 分量面字节 = 与 stb 完全同式的 w2×h2(MCU 对齐 + 采样因子);
|
||||
* 渐进式每个分量还多一张 w2×h2×2 的 short 系数面(M20 实测 +23.5MB 的来源)。 */
|
||||
long long mcu_x = (ww + 8 * hmax - 1) / (8 * hmax);
|
||||
long long mcu_y = (hh + 8 * vmax - 1) / (8 * vmax);
|
||||
long long planes = 0;
|
||||
for (int i = 0; i < nc; i++) {
|
||||
long long w2 = mcu_x * (hv[i] >> 4) * 8;
|
||||
long long h2 = mcu_y * (hv[i] & 15) * 8;
|
||||
planes += w2 * h2;
|
||||
if (prog) planes += w2 * h2 * 2;
|
||||
}
|
||||
in->channels = nc;
|
||||
in->planes = planes;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if (!ps_skip(s, len - 2)) return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int probe_bmp(probe_src *s, int *w, int *h)
|
||||
static int probe_bmp(probe_src *s, mr_img_info *in)
|
||||
{
|
||||
unsigned char hdr[26];
|
||||
if (!ps_read(s, hdr, sizeof hdr)) return 0;
|
||||
@@ -248,49 +310,81 @@ static int probe_bmp(probe_src *s, int *w, int *h)
|
||||
((unsigned)hdr[24] << 16) | ((unsigned)hdr[25] << 24));
|
||||
if (hh < 0) hh = -hh; /* 自底向上 BMP */
|
||||
if (ww <= 0 || hh <= 0) return 0;
|
||||
*w = ww;
|
||||
*h = hh;
|
||||
unsigned char ex[4]; /* biPlanes(2) / biBitCount(2) */
|
||||
int bpp = 24;
|
||||
if (ps_read(s, ex, sizeof 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 目标,不另开源缓冲 */
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int probe_any(probe_src *s, int *w, int *h)
|
||||
static int probe_any(probe_src *s, mr_img_info *in)
|
||||
{
|
||||
memset(in, 0, sizeof *in);
|
||||
for (int pass = 0; pass < 3; pass++) {
|
||||
s->pos = 0;
|
||||
if (s->f) rewind(s->f);
|
||||
int ok = (pass == 0) ? probe_png(s, w, h)
|
||||
: (pass == 1) ? probe_jpeg(s, w, h)
|
||||
: probe_bmp(s, w, h);
|
||||
int ok = (pass == 0) ? probe_png(s, in)
|
||||
: (pass == 1) ? probe_jpeg(s, in)
|
||||
: probe_bmp(s, in);
|
||||
if (ok) return 1;
|
||||
memset(in, 0, sizeof *in);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mr_probe_size_mem(const void *buf, size_t len, int *w, int *h)
|
||||
int mr_probe_info_mem(const void *buf, size_t len, mr_img_info *info)
|
||||
{
|
||||
if (w) *w = 0;
|
||||
if (h) *h = 0;
|
||||
if (!buf || len < 8 || !w || !h) return 0;
|
||||
if (info) memset(info, 0, sizeof *info);
|
||||
if (!buf || len < 8 || !info) return 0;
|
||||
probe_src s = { (const unsigned char *)buf, len, 0, NULL };
|
||||
if (!probe_any(&s, w, h) || *w <= 0 || *h <= 0) {
|
||||
*w = *h = 0;
|
||||
if (!probe_any(&s, info) || info->w <= 0 || info->h <= 0) {
|
||||
memset(info, 0, sizeof *info);
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int mr_probe_size(const char *path, int *w, int *h)
|
||||
int mr_probe_info(const char *path, mr_img_info *info)
|
||||
{
|
||||
if (w) *w = 0;
|
||||
if (h) *h = 0;
|
||||
if (!path || !path[0] || !w || !h) return 0;
|
||||
if (info) memset(info, 0, sizeof *info);
|
||||
if (!path || !path[0] || !info) return 0;
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return 0;
|
||||
probe_src s = { NULL, 0, 0, f };
|
||||
int ok = probe_any(&s, w, h);
|
||||
int ok = probe_any(&s, info);
|
||||
fclose(f);
|
||||
if (ok && (*w <= 0 || *h <= 0)) { *w = *h = 0; return 0; }
|
||||
return ok;
|
||||
if (!ok || info->w <= 0 || info->h <= 0) {
|
||||
memset(info, 0, sizeof *info);
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int mr_probe_size_mem(const void *buf, size_t len, int *w, int *h)
|
||||
{
|
||||
mr_img_info in;
|
||||
if (w) *w = 0;
|
||||
if (h) *h = 0;
|
||||
if (!mr_probe_info_mem(buf, len, &in)) return 0;
|
||||
if (w) *w = in.w;
|
||||
if (h) *h = in.h;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int mr_probe_size(const char *path, int *w, int *h)
|
||||
{
|
||||
mr_img_info in;
|
||||
if (w) *w = 0;
|
||||
if (h) *h = 0;
|
||||
if (!mr_probe_info(path, &in)) return 0;
|
||||
if (w) *w = in.w;
|
||||
if (h) *h = in.h;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
|
||||
@@ -88,6 +88,30 @@ int mr_probe_size(const char *path, int *w, int *h);
|
||||
/* 同上,但从内存缓冲探测(档案页解压后不需要落盘再读一遍)。 */
|
||||
int mr_probe_size_mem(const void *buf, size_t len, int *w, int *h);
|
||||
|
||||
/* ---- 追加(M22):解码内存预算需要的头部信息(仍然零像素内存) ----
|
||||
* 解析器按**内容**嗅探(PNG → JPEG → BMP),不信任扩展名:一个 .jpg 后缀的 PNG 也能
|
||||
* 得到正确结论。planes 是 stb 解码时**除最终 RGBA 输出缓冲以外**的中间面字节数:
|
||||
* - JPEG:各分量面 w2*h2(按 MCU 对齐 + 采样因子,与 stb 同式),渐进式再加 short 系数面;
|
||||
* - PNG :源像素缓冲 ch*W*H(16bit 再 ×2,ch==4 时就是输出缓冲本身,故记 0);
|
||||
* - BMP :0(stb 对 24/32bpp 直接写 RGBA 目标,不另开源缓冲)。
|
||||
* 调用方(阅读器)再叠加「抽取输出 + 精修/落框临时图」得到真实峰值。
|
||||
* 1=已知(info 已填满),0=未知/损坏(info 清零且 codec=MR_IMG_CODEC_UNKNOWN)。 */
|
||||
#define MR_IMG_CODEC_UNKNOWN 0
|
||||
#define MR_IMG_CODEC_JPEG 1
|
||||
#define MR_IMG_CODEC_PNG 2
|
||||
#define MR_IMG_CODEC_BMP 3
|
||||
|
||||
typedef struct mr_img_info {
|
||||
int w, h;
|
||||
int codec; /* MR_IMG_CODEC_* */
|
||||
int progressive; /* 1=渐进式 JPEG(SOF2/SOF6/SOF10/SOF14) */
|
||||
int channels; /* 源通道数(JPEG 分量数 / PNG 颜色类型 / BMP 位深/8) */
|
||||
long long planes; /* 见上:stb 中间面字节(不含最终 RGBA 输出) */
|
||||
} 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);
|
||||
|
||||
/* ============================================================ */
|
||||
/* 档案册(M11):.cbz / .zip 当一本书 */
|
||||
/* ------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""make_mem_samples.py — 为 test_reader_membudget.c 生成 JPEG 夹具(M22)。
|
||||
|
||||
用法:python3 make_mem_samples.py <输出目录>
|
||||
依赖:Pillow。没有 Pillow 时测试自动 SKIP JPEG 断言(BMP/PNG 夹具由 C 直接写)。
|
||||
|
||||
刻意用**结构化图案**而不是噪声:文件极小、解码路径与真实漫画页一致(白底 + 分镜框)。
|
||||
page.jpg 1000x1500 baseline(1.5MP,真机崩溃页所在档位)
|
||||
big4.jpg 2000x2000 baseline(4MP,手机照片档)
|
||||
prog.jpg 1000x1500 progressive(SOF2:验证渐进识别 + 峰值模型)
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
def pattern(w, h):
|
||||
im = Image.new("RGB", (w, h), (248, 247, 244))
|
||||
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 main():
|
||||
if len(sys.argv) != 2:
|
||||
print(__doc__)
|
||||
return 2
|
||||
out = sys.argv[1]
|
||||
os.makedirs(out, exist_ok=True)
|
||||
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)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -13,6 +13,9 @@
|
||||
# 交付环境只有库+头时自动 SKIP。
|
||||
# - test_open_book_host(可选):书库点击「封面图 + 含图子目录」的父目录 → 阅读器打开的
|
||||
# 完整 UI 链路回归(合成夹具,不读真实相册);同样需要 SDK 源码树。
|
||||
# - test_reader_membudget(可选,M22):--wrap malloc 计真实解码峰值 → 单页 ≤3MB 硬阈值、
|
||||
# 连翻 100 页峰值不增长、内存门控/预算跳过可重试、常驻 ≤3MB、取消机制有界。JPEG 夹具由
|
||||
# test/make_mem_samples.py 生成(无 Pillow 时该项 SKIP)。
|
||||
# reader.c 的方向持久化只 include 配置/日志两个模块头;宿主测试用 test/settings_reader_stub.c
|
||||
# 顶替 meow_config_* 与 meow_log(所以下面各条都带 -I"$SDK" 与该存根)。
|
||||
# logic/library.c 的探测缓存用 pthread 互斥锁 → 相关编译带 -pthread。
|
||||
@@ -143,6 +146,25 @@ if [ -f "$SDK/meow_ui_view.c" ] && [ -f "$SDK/meow_ui_config.c" ]; then
|
||||
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
|
||||
-pthread -lm -lz -o "$OUT"/test_reader_many_pages
|
||||
"$OUT"/test_reader_many_pages
|
||||
|
||||
# 解码内存预算(M22):--wrap malloc 计真实峰值 → 单页 ≤3MB、连翻 100 页峰值不增长、
|
||||
# 内存门控/预算跳过可重试、常驻 ≤3MB。真机 SIGKILL 事故的回归防线。
|
||||
gcc -Wall -Wextra -O1 -DMEOW_UI_HOST_TEST -DMR_READER_HOST_TEST -I. -I"$SDK" -I"$SDK/include" \
|
||||
test/test_reader_membudget.c ui/reader/reader.c \
|
||||
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_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_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 \
|
||||
-Wl,--wrap=malloc -Wl,--wrap=calloc -Wl,--wrap=realloc -Wl,--wrap=free
|
||||
"$OUT"/test_reader_membudget
|
||||
else
|
||||
echo "SKIP test_settings_lock_host/test_browse_ui_host: SDK 源码树不可用(只发布 meow_ui.a 的环境)"
|
||||
fi
|
||||
|
||||
@@ -273,9 +273,11 @@ 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());
|
||||
CHECK(mr_reader_ui_failed_pages() == 2,
|
||||
"garbage + oversized page both degraded to placeholder (failed=%d)",
|
||||
mr_reader_ui_failed_pages());
|
||||
/* M22:坏图 = 永久占位(failed);超预算大页 = 预算跳过(skipped,可重试,不是 failed)。 */
|
||||
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,
|
||||
"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 页排版 + 跳页落位 ---- */
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
/* test_reader_membudget.c — 阅读器解码内存预算回归(M22;需要 SDK 源码树)。
|
||||
*
|
||||
* 真机事故(R3 Pro II 480×720 / MemTotal 56872kB):空闲 RSS 6044kB → 翻页 11400kB
|
||||
* → 峰值 15600kB(当时 MemFree 仅 1404kB)→ SIGKILL。M20 审计把账算清:4 槽 RGBA 常驻
|
||||
* 5356kB、单页解码尖峰 3.4–35MB(旧模型低估 3.5–8 倍)。本测试用 `--wrap malloc` 量
|
||||
* **真实分配峰值**(不是 VmHWM 估计),把「模型 / 阈值 / 驻留」钉在实测上。
|
||||
*
|
||||
* 断言:
|
||||
* B1 峰值模型 ≤ 实测(模型必须是不低估的上界;同时 ≤ 实测×2,避免离谱保守)
|
||||
* B2 单页解码峰值(预算内的页)≤ 3MB 硬阈值
|
||||
* B3 连翻 100 页:峰值不增长、无永久失败页、解码次数有界(取消生效)
|
||||
* B4 常驻页图 ≤ 常驻上限(3MB)
|
||||
* B5 内存门控:MemAvailable 不足 → 不解码直接占位(可重试,不是永久失败)
|
||||
* B6 预算跳过可重试:MemFree 充足时同一页能解出来(占位不是永久 failed)
|
||||
* B7 快翻取消:拖动期间只解落点页,不产生中间页解码
|
||||
*
|
||||
* 夹具:BMP/PNG 由本文件用 C 写(不依赖 Pillow);JPEG 由 python3+Pillow 生成,
|
||||
* 缺 PIL 时跳过 JPEG 相关断言(打印 SKIP)。全部夹具体积极小(结构化图案)。
|
||||
*
|
||||
* 编译:test/run_tests.sh(与 test_reader_perf 同口径 + --wrap malloc 家族)。
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include "tuku_internal.h"
|
||||
#include "ui/reader/reader.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include "logic/reader/nav.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 分配打点:--wrap malloc/free 家族(与 sdk/test_meow_img_budget.c 同法)*/
|
||||
/* ------------------------------------------------------------------ */
|
||||
#define AHDR 32
|
||||
#define AMAGIC 0x6D6B4D32324D454DU /* 只统计本包装器的分配;libc 内部(strdup 等)原样透传 */
|
||||
|
||||
static size_t g_live = 0;
|
||||
static size_t g_peak = 0;
|
||||
|
||||
extern void *__real_malloc(size_t);
|
||||
extern void *__real_calloc(size_t, size_t);
|
||||
extern void *__real_realloc(void *, size_t);
|
||||
extern void __real_free(void *);
|
||||
|
||||
static void acc_alloc(size_t n)
|
||||
{
|
||||
g_live += n;
|
||||
if (g_live > g_peak) g_peak = g_live;
|
||||
}
|
||||
|
||||
void *__wrap_malloc(size_t n)
|
||||
{
|
||||
size_t *h = (size_t *)__real_malloc(n + AHDR);
|
||||
if (!h) return NULL;
|
||||
h[0] = n;
|
||||
h[1] = (size_t)AMAGIC;
|
||||
acc_alloc(n);
|
||||
return (char *)h + AHDR;
|
||||
}
|
||||
|
||||
void *__wrap_calloc(size_t a, size_t b)
|
||||
{
|
||||
size_t n = a * b;
|
||||
void *p = __wrap_malloc(n);
|
||||
if (p) memset(p, 0, n);
|
||||
return p;
|
||||
}
|
||||
|
||||
void __wrap_free(void *p)
|
||||
{
|
||||
size_t *h;
|
||||
if (!p) return;
|
||||
h = (size_t *)((char *)p - AHDR);
|
||||
if (h[1] != (size_t)AMAGIC) { __real_free(p); return; } /* 非本包装器的分配 */
|
||||
h[1] = 0;
|
||||
g_live -= h[0];
|
||||
__real_free(h);
|
||||
}
|
||||
|
||||
void *__wrap_realloc(void *p, size_t n)
|
||||
{
|
||||
size_t *h = p ? (size_t *)((char *)p - AHDR) : NULL;
|
||||
if (p && (!h || h[1] != (size_t)AMAGIC)) return __real_realloc(p, n); /* 外来指针 */
|
||||
size_t old = h ? h[0] : 0;
|
||||
void *q = __wrap_malloc(n);
|
||||
if (!q) return NULL;
|
||||
if (p) {
|
||||
memcpy(q, p, old < n ? old : n);
|
||||
__wrap_free(p);
|
||||
}
|
||||
return q;
|
||||
}
|
||||
|
||||
static size_t peak_reset(void) { g_peak = g_live; return g_live; }
|
||||
static size_t peak_since(size_t base) { return g_peak - base; }
|
||||
|
||||
void mr_nav_report(void) {}
|
||||
|
||||
static int g_fail;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { printf(" PASS\n"); } \
|
||||
else { g_fail++; printf(" FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
#define INFO(...) do { printf(" info: "); printf(__VA_ARGS__); printf("\n"); } while (0)
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 夹具写入 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void put16(unsigned char *p, unsigned v) { p[0] = (unsigned char)v; p[1] = (unsigned char)(v >> 8); }
|
||||
static void put32be(unsigned char *p, unsigned v)
|
||||
{
|
||||
p[0] = (unsigned char)(v >> 24); p[1] = (unsigned char)(v >> 16);
|
||||
p[2] = (unsigned char)(v >> 8); p[3] = (unsigned char)v;
|
||||
}
|
||||
static void put32(unsigned char *p, unsigned v)
|
||||
{
|
||||
p[0] = (unsigned char)v; p[1] = (unsigned char)(v >> 8);
|
||||
p[2] = (unsigned char)(v >> 16); p[3] = (unsigned char)(v >> 24);
|
||||
}
|
||||
|
||||
static int write_bmp24(const char *path, int w, int h, unsigned seed)
|
||||
{
|
||||
int stride = ((w * 3 + 3) / 4) * 4;
|
||||
int imgsz = stride * h;
|
||||
unsigned char fh[14] = { 0 }, ih[40] = { 0 };
|
||||
fh[0] = 'B'; fh[1] = 'M';
|
||||
put32(fh + 2, (unsigned)(54 + imgsz));
|
||||
put32(fh + 10, 54);
|
||||
put32(ih + 0, 40);
|
||||
put32(ih + 4, (unsigned)w);
|
||||
put32(ih + 8, (unsigned)h);
|
||||
put16(ih + 12, 1);
|
||||
put16(ih + 14, 24);
|
||||
put32(ih + 20, (unsigned)imgsz);
|
||||
|
||||
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;
|
||||
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 < w; x++) {
|
||||
row[x * 3 + 0] = (unsigned char)((x + seed) & 0xFF);
|
||||
row[x * 3 + 1] = (unsigned char)((y * 3 + seed) & 0xFF);
|
||||
row[x * 3 + 2] = (unsigned char)((x ^ y) & 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 unsigned crc32_buf(const unsigned char *p, size_t n)
|
||||
{
|
||||
unsigned c = 0xFFFFFFFFu;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
c ^= p[i];
|
||||
for (int k = 0; k < 8; k++) c = (c >> 1) ^ (0xEDB88320u & (unsigned)(-(int)(c & 1)));
|
||||
}
|
||||
return c ^ 0xFFFFFFFFu;
|
||||
}
|
||||
|
||||
static int write_png_rgb(const char *path, int w, int h, unsigned seed)
|
||||
{
|
||||
/* zlib 流用 stored 块(不依赖 zlib),只为造一个真 PNG 让 stb 走「源缓冲 + RGBA 转换」路径。 */
|
||||
size_t rowlen = (size_t)w * 3 + 1;
|
||||
size_t rawlen = rowlen * (size_t)h;
|
||||
unsigned char *raw = (unsigned char *)malloc(rawlen);
|
||||
if (!raw) return -1;
|
||||
for (int y = 0; y < h; y++) {
|
||||
unsigned char *r = raw + rowlen * (size_t)y;
|
||||
r[0] = 0; /* filter none */
|
||||
for (int x = 0; x < w; x++) {
|
||||
r[1 + x * 3 + 0] = (unsigned char)((x + seed) & 0xFF);
|
||||
r[1 + x * 3 + 1] = (unsigned char)(y & 0xFF);
|
||||
r[1 + x * 3 + 2] = (unsigned char)((x ^ y) & 0xFF);
|
||||
}
|
||||
}
|
||||
size_t nblk = (rawlen + 65534) / 65535;
|
||||
size_t zlen = 2 + rawlen + nblk * 5 + 4;
|
||||
unsigned char *z = (unsigned char *)malloc(zlen);
|
||||
if (!z) { free(raw); return -1; }
|
||||
size_t zi = 0;
|
||||
z[zi++] = 0x78; z[zi++] = 0x01;
|
||||
for (size_t off = 0; off < rawlen; off += 65535) {
|
||||
size_t n = rawlen - off > 65535 ? 65535 : rawlen - off;
|
||||
z[zi++] = (off + n == rawlen) ? 1 : 0; /* BFINAL */
|
||||
z[zi++] = (unsigned char)(n & 0xFF);
|
||||
z[zi++] = (unsigned char)(n >> 8);
|
||||
z[zi++] = (unsigned char)(~n & 0xFF);
|
||||
z[zi++] = (unsigned char)((~n >> 8) & 0xFF);
|
||||
memcpy(z + zi, raw + off, n);
|
||||
zi += n;
|
||||
}
|
||||
/* adler32(大端,zlib 流尾) */
|
||||
{
|
||||
unsigned a = 1, b = 0;
|
||||
for (size_t i = 0; i < rawlen; i++) { a = (a + raw[i]) % 65521; b = (b + a) % 65521; }
|
||||
put32be(z + zi, (b << 16) | a);
|
||||
zi += 4;
|
||||
}
|
||||
free(raw);
|
||||
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) { free(z); return -1; }
|
||||
static const unsigned char sig[8] = { 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A };
|
||||
int ok = fwrite(sig, 1, 8, f) == 8;
|
||||
unsigned char ihdr[13] = { 0 };
|
||||
put32be(ihdr + 0, (unsigned)w);
|
||||
put32be(ihdr + 4, (unsigned)h);
|
||||
ihdr[8] = 8; /* bit depth */
|
||||
ihdr[9] = 2; /* color type RGB */
|
||||
struct { const char *tag; const unsigned char *data; unsigned len; } chunks[2] = {
|
||||
{ "IHDR", ihdr, 13 }, { "IDAT", z, (unsigned)zi },
|
||||
};
|
||||
for (int k = 0; ok && k < 2; k++) {
|
||||
unsigned char lenb[4], crcb[4];
|
||||
put32be(lenb, chunks[k].len);
|
||||
if (fwrite(lenb, 1, 4, f) != 4) ok = 0;
|
||||
if (fwrite(chunks[k].tag, 1, 4, f) != 4) ok = 0;
|
||||
if (fwrite(chunks[k].data, 1, chunks[k].len, f) != chunks[k].len) ok = 0;
|
||||
unsigned crc = crc32_buf((const unsigned char *)chunks[k].tag, 4);
|
||||
/* 逐段续算:简化起见重算整块 */
|
||||
{
|
||||
unsigned char *tmp = (unsigned char *)malloc(4 + chunks[k].len);
|
||||
if (tmp) {
|
||||
memcpy(tmp, chunks[k].tag, 4);
|
||||
memcpy(tmp + 4, chunks[k].data, chunks[k].len);
|
||||
crc = crc32_buf(tmp, 4 + chunks[k].len);
|
||||
free(tmp);
|
||||
}
|
||||
}
|
||||
put32be(crcb, crc);
|
||||
if (fwrite(crcb, 1, 4, f) != 4) ok = 0;
|
||||
}
|
||||
if (ok) {
|
||||
unsigned char iend[12] = { 0, 0, 0, 0, 'I', 'E', 'N', 'D', 0xAE, 0x42, 0x60, 0x82 };
|
||||
if (fwrite(iend, 1, 12, f) != 12) ok = 0;
|
||||
}
|
||||
free(z);
|
||||
if (fclose(f) != 0) ok = 0;
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
static int mkdir_p(const char *path)
|
||||
{
|
||||
char tmp[MR_PATH_MAX];
|
||||
snprintf(tmp, sizeof tmp, "%s", path);
|
||||
for (size_t i = 1; i <= strlen(tmp); i++) {
|
||||
if (tmp[i] == '/' || tmp[i] == '\0') {
|
||||
char ch = tmp[i];
|
||||
tmp[i] = '\0';
|
||||
if (mkdir(tmp, 0755) != 0) {
|
||||
struct stat st;
|
||||
if (stat(tmp, &st) != 0 || !S_ISDIR(st.st_mode)) return -1;
|
||||
}
|
||||
tmp[i] = ch;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int rm_rf(const char *path)
|
||||
{
|
||||
struct stat st;
|
||||
if (lstat(path, &st) != 0) return -1;
|
||||
if (!S_ISDIR(st.st_mode)) return unlink(path);
|
||||
DIR *d = opendir(path);
|
||||
if (!d) return -1;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue;
|
||||
char full[MR_PATH_MAX];
|
||||
snprintf(full, sizeof full, "%s/%s", path, de->d_name);
|
||||
rm_rf(full);
|
||||
}
|
||||
closedir(d);
|
||||
return rmdir(path);
|
||||
}
|
||||
|
||||
static void pump_ms(int ms)
|
||||
{
|
||||
for (int i = 0; i < ms / 10; i++) {
|
||||
usleep(10 * 1000);
|
||||
meow_anim_tick();
|
||||
}
|
||||
}
|
||||
|
||||
static void tap(int x, int y)
|
||||
{
|
||||
reader_on_touch_down(x, y);
|
||||
reader_on_tap(x, y);
|
||||
pump_ms(300);
|
||||
}
|
||||
|
||||
/* 生成 JPEG 夹具(Pillow 不可用则返回 0,相关断言 SKIP)。脚本本体在
|
||||
* test/make_mem_samples.py(与本文件同目录;run_tests.sh 已 cd 到 tuku/)。 */
|
||||
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;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const char *base = getenv("TMPDIR");
|
||||
if (!base || !base[0]) base = ".";
|
||||
char tmpl[512];
|
||||
snprintf(tmpl, sizeof tmpl, "%s/mr_membudget_XXXXXX", base);
|
||||
char *data = mkdtemp(tmpl);
|
||||
if (!data) { printf("SKIP: mkdtemp failed\n"); return 0; }
|
||||
setenv("MEOW_DATA_DIR", data, 1);
|
||||
|
||||
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];
|
||||
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);
|
||||
if (mkdir_p(book) != 0 || mkdir_p(big) != 0 || mkdir_p(pngbook) != 0 ||
|
||||
mkdir_p(jpgbook) != 0) {
|
||||
printf("SKIP: mkdir failed\n");
|
||||
return 0;
|
||||
}
|
||||
/* 480x640 BMP ×8:单页解码峰值 ≈ 480*640*4*2 ≈ 2.5MB,能进 3MB 预算(可连翻)。 */
|
||||
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)时必须能解出来。 */
|
||||
{
|
||||
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; }
|
||||
}
|
||||
/* 600x960 PNG(RGB):走「源缓冲 + RGBA 转换」路径(stb 不能解码期缩解)。 */
|
||||
for (int i = 0; i < 3; i++) {
|
||||
char p[MR_PATH_MAX];
|
||||
snprintf(p, sizeof p, "%s/q%d.png", pngbook, i);
|
||||
if (write_png_rgb(p, 600, 960, (unsigned)i) != 0) { printf("SKIP: png write\n"); return 0; }
|
||||
}
|
||||
int have_jpeg = gen_jpeg_fixtures(jpgbook);
|
||||
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
if (meow_ui_init(&o) != 0) { printf("SKIP: meow_ui_init failed (no host backend)\n"); return 0; }
|
||||
|
||||
static const meow_view_hooks lh = { 0 };
|
||||
meow_view *lib = meow_view_create(ACT_LIBRARY, MEOW_VIEW_ACTIVITY, &lh, NULL);
|
||||
meow_view *reader = reader_view_create();
|
||||
meow_view_page_push(ACT_LIBRARY);
|
||||
|
||||
/* 宿主内存读数固定:3MB 常态预算生效(不被开发机的巨大 MemFree 放宽)。 */
|
||||
mr_reader_ui_test_set_mem(4096, 16000);
|
||||
|
||||
/* ---- B1/B2 单页解码峰值 vs 模型(PNG:stb 不能解码期缩解 → 整幅落地)---- */
|
||||
{
|
||||
char p[MR_PATH_MAX];
|
||||
snprintf(p, sizeof p, "%s/q0.png", pngbook);
|
||||
long long model = mr_reader_ui_decode_peak_kb(p, 1, 480, 768);
|
||||
size_t b0 = g_live;
|
||||
peak_reset();
|
||||
/* 走产品解码入口(含落框重采样),与阅读器 worker 完全同路径 */
|
||||
meow_img *im = meow_img_load_file_step(p, 768, 1);
|
||||
if (im && (meow_img_w(im) != 480 || meow_img_h(im) != 768)) {
|
||||
meow_img *fit = meow_img_resample(im, 480, 768);
|
||||
if (fit) { meow_img_free(im); im = fit; }
|
||||
}
|
||||
size_t measured_kb = peak_since(b0) / 1024 + 1;
|
||||
int got = im ? 1 : 0;
|
||||
if (im) meow_img_free(im);
|
||||
INFO("png 600x960: model=%lldkB measured=%zukB decoded=%d", model, measured_kb, got);
|
||||
CHECK(got, "png fixture decodes (writer valid)");
|
||||
CHECK(model >= (long long)measured_kb, "model is an upper bound (%lldkB < %zukB)",
|
||||
model, measured_kb);
|
||||
CHECK(model <= (long long)measured_kb * 2, "model not wildly pessimistic (%lldkB vs %zukB)",
|
||||
model, measured_kb);
|
||||
}
|
||||
|
||||
/* ---- B5 内存门控:MemAvailable 不足 → 不解码、占位(可重试)---- */
|
||||
{
|
||||
mr_reader_ui_test_set_mem(20000, 1000); /* MemAvailable 1MB < 6MB 下限 */
|
||||
mr_reader_open_dir(book);
|
||||
CHECK(mr_reader_ui_count() == 8, "book pages=%d", mr_reader_ui_count());
|
||||
int d0 = mr_reader_ui_decode_count();
|
||||
pump_ms(600);
|
||||
CHECK(mr_reader_ui_decode_count() - d0 <= 1,
|
||||
"low mem: no decode attempt (%d new decodes)", mr_reader_ui_decode_count() - d0);
|
||||
CHECK(mr_reader_ui_skipped_pages() > 0, "low mem: page skipped (skipped=%d)",
|
||||
mr_reader_ui_skipped_pages());
|
||||
CHECK(mr_reader_ui_failed_pages() == 0, "low mem: not a permanent failure (failed=%d)",
|
||||
mr_reader_ui_failed_pages());
|
||||
|
||||
/* 内存恢复 + 点按显式重试 → 同一页能解出来(可重试语义) */
|
||||
mr_reader_ui_test_set_mem(20000, 20000);
|
||||
int y = meow_ui_height() / 2;
|
||||
tap(meow_ui_width() / 2, y);
|
||||
pump_ms(500);
|
||||
CHECK(mr_reader_ui_skipped_pages() == 0, "retry after mem recovers (skipped=%d)",
|
||||
mr_reader_ui_skipped_pages());
|
||||
CHECK(mr_reader_ui_resident_kb() > 0, "retry landed a page image (resident=%ldkB)",
|
||||
mr_reader_ui_resident_kb());
|
||||
meow_view_page_pop();
|
||||
pump_ms(200);
|
||||
}
|
||||
|
||||
/* ---- B3/B4 连翻 100 页:真实分配峰值不增长、常驻有界、取消有界 ---- */
|
||||
{
|
||||
mr_reader_ui_test_set_mem(4096, 16000);
|
||||
mr_reader_open_dir(book);
|
||||
pump_ms(800);
|
||||
int w = meow_ui_width();
|
||||
int d0 = mr_reader_ui_decode_count();
|
||||
long long resident_cap = mr_reader_ui_resident_cap_kb();
|
||||
long long cap = mr_reader_ui_peak_cap_kb();
|
||||
/* 基线 = 首屏稳定后的存活字节;之后任何分配都不该再超过 基线+3MB(= 单页解码峰值)。 */
|
||||
size_t live_steady = g_live;
|
||||
size_t peak_steady = g_peak;
|
||||
int skipped_max = 0, advanced = 0;
|
||||
for (int i = 0; i < 100; i++) {
|
||||
int before = mr_reader_ui_page();
|
||||
tap(i % 2 == 0 ? 40 : w - 40, 300); /* RTL:左 1/3=下一页 */
|
||||
pump_ms(120); /* 覆盖滑页 + 紧随其后的预取解码 */
|
||||
if (mr_reader_ui_page() != before) advanced++;
|
||||
if (mr_reader_ui_skipped_pages() > skipped_max) skipped_max = mr_reader_ui_skipped_pages();
|
||||
meow_ui_paint_full();
|
||||
long kb = mr_reader_ui_resident_kb();
|
||||
if (kb > resident_cap) {
|
||||
g_fail++;
|
||||
printf(" FAIL flip %d: resident %ldkB > cap %lldkB\n", i, kb, resident_cap);
|
||||
break;
|
||||
}
|
||||
}
|
||||
size_t transient = g_peak > peak_steady ? g_peak - peak_steady : 0;
|
||||
size_t live_end = g_live;
|
||||
int decodes = mr_reader_ui_decode_count() - d0;
|
||||
INFO("100 flips: steady_live=%zuKB peak_transient=%zuKB end_live=%zuKB cap=%lldKB decodes=%d advanced=%d",
|
||||
live_steady / 1024, transient / 1024, live_end / 1024, cap, decodes, advanced);
|
||||
CHECK(advanced == 100, "every flip advanced the page (%d/100)", advanced);
|
||||
CHECK(transient <= (size_t)cap * 1024 + 512 * 1024,
|
||||
"no allocation above steady+3MB over 100 flips (%zuKB)", transient / 1024);
|
||||
CHECK(live_end <= live_steady + 512 * 1024,
|
||||
"live bytes do not drift up over 100 flips (%zuKB -> %zuKB)",
|
||||
live_steady / 1024, live_end / 1024);
|
||||
CHECK(skipped_max == 0, "no page skipped while flipping small pages (max=%d)", skipped_max);
|
||||
CHECK(mr_reader_ui_failed_pages() == 0, "no permanently failed pages (%d)",
|
||||
mr_reader_ui_failed_pages());
|
||||
/* 取消/预取纪律:100 次翻页的解码次数必须有界(M20 实测曾 1.75× 浪费)。 */
|
||||
CHECK(decodes <= 100 * 2, "decode count bounded over 100 flips (%d)", decodes);
|
||||
meow_view_page_pop();
|
||||
pump_ms(200);
|
||||
}
|
||||
|
||||
/* ---- B6 预算跳过可重试:大页在低预算下占位,MemFree 充足后能解出来 ---- */
|
||||
{
|
||||
mr_reader_ui_test_set_mem(4096, 16000); /* MemFree 4MB → 3MB 常态预算 */
|
||||
mr_reader_open_dir(big);
|
||||
pump_ms(900);
|
||||
CHECK(mr_reader_ui_skipped_pages() == 1, "over-budget page skipped (skipped=%d)",
|
||||
mr_reader_ui_skipped_pages());
|
||||
CHECK(mr_reader_ui_failed_pages() == 0, "over-budget page is not a permanent failure (failed=%d)",
|
||||
mr_reader_ui_failed_pages());
|
||||
/* MemFree 富余 → 放宽档(峰值上限 = MemFree-4MB,最高 12MB);大页仍应能解出。 */
|
||||
mr_reader_ui_test_set_mem(48000, 48000);
|
||||
tap(meow_ui_width() / 2, meow_ui_height() / 2);
|
||||
pump_ms(1200);
|
||||
INFO("relaxed budget=%ldkB skipped=%d resident=%ldkB", mr_reader_ui_peak_budget_kb(),
|
||||
mr_reader_ui_skipped_pages(), mr_reader_ui_resident_kb());
|
||||
CHECK(mr_reader_ui_skipped_pages() == 0,
|
||||
"over-budget page decodable once MemFree is plenty (skipped=%d)",
|
||||
mr_reader_ui_skipped_pages());
|
||||
meow_view_page_pop();
|
||||
pump_ms(200);
|
||||
}
|
||||
|
||||
/* ---- B7 JPEG:模型不低估 + 3MB 阈值下「哪些 JPEG 会变占位」---- */
|
||||
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; }
|
||||
}
|
||||
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");
|
||||
}
|
||||
|
||||
meow_view_page_pop();
|
||||
meow_view_free(reader);
|
||||
meow_view_free(lib);
|
||||
meow_ui_shutdown();
|
||||
|
||||
rm_rf(root);
|
||||
rm_rf(data);
|
||||
|
||||
printf("%s: %s\n", g_fail ? "FAIL" : "PASS",
|
||||
g_fail ? "see failures above" : "reader memory budget checks passed");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
+445
-132
@@ -15,8 +15,12 @@
|
||||
* 宽恒为视口宽),不是屏长边,所以翻页/滚动热路径只有原生 blit:不做 meow_ui_img_scaled
|
||||
* 的每帧重采样,也不再每帧 malloc 1MB 级临时缓冲。几何变化(fit/缩放/切模式)时按新几何
|
||||
* 重解一次;拖动/吸附帧只 invalidate 内容区(顶栏静态不重绘)。
|
||||
* 内存:单页显示缓存 ≤3MB、整窗 ≤9MB(超限淘汰最远页),解码中间峰值 >12MB 的页直接跳过
|
||||
* 并显示占位——页数增长不会线性吃内存(常驻只与解码窗大小有关)。
|
||||
* 内存(M17 起;M22 按真机实测重定):单页显示缓存 ≤3MB、整窗常驻 ≤3MB(3 槽,超限淘汰
|
||||
* 最远页),单次解码真实峰值常态 ≤3MB —— 超出时 JPEG 先降缩解档位、PNG/BMP 直接占位
|
||||
* (占位可重试,不是永久失败)。峰值模型见 decode_peak_bytes():它按 stb 的真实分配算
|
||||
* (JPEG 分量面、渐进的 short 系数面、PNG 的源缓冲 + RGBA 转换副本),不再是「把 stb 当
|
||||
* 缩解库」的低估式。余量确实充足(MemFree 富余)时允许放宽到 MR_DECODE_PEAK_HARD。
|
||||
* 页数增长不会线性吃内存(常驻只与解码窗大小有关)。
|
||||
* 方向:一律读 M3 的 mr_reader_get_direction()(默认 RTL),不另立全局。
|
||||
*
|
||||
* 模式(M7):翻页/条漫是一等公民。阅读 chrome 的图标按钮一键切换;每册的模式与 last page
|
||||
@@ -54,9 +58,8 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#define MR_SLOTS 4 /* 页图缓存槽上限(条漫视口±1 ≤4;翻页只用 3) */
|
||||
#define MR_SLOTS 3 /* 页图缓存槽上限(M22:4→3 省 ~1.4MB;翻页用 2,条漫视口 2–3) */
|
||||
#define MR_TOP_BAR_H 56
|
||||
#define MR_SLIDER_H 40
|
||||
#define MR_SLIDE_MS 150 /* 松手后一次短滑动吸附 */
|
||||
@@ -71,14 +74,21 @@
|
||||
#define MR_JUMP_TIMEOUT_MS 2500 /* 目标页迟迟解不出来 → 降级落位并提示 */
|
||||
#define MR_CONT_SAVE_MS 1000 /* 续读状态落盘节流 */
|
||||
#define MR_TOAST_MS 1400
|
||||
#define MR_LOAD_RAW_WARN (8L * 1024 * 1024) /* 非缩解格式原始 RGBA 超此值提示残余风险 */
|
||||
#define MR_DECODE_PEAK_CAP (12L * 1024 * 1024) /* 单次解码中间缓冲峰值上限(超则跳过,防 OOM 闪退) */
|
||||
/* ---- 内存预算(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 低于此值不解码 */
|
||||
#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 (9L * 1024 * 1024) /* 页图缓存总字节上限(超则淘汰最远页) */
|
||||
#define MR_RESIDENT_CAP (3L * 1024 * 1024) /* 页图缓存总字节上限(M22 由 9MB 校准为 3MB) */
|
||||
#define MR_SLOT_TOL 2 /* 缓存图与显示框允许的像素偏差(≤2 走原生 blit,不重采样) */
|
||||
|
||||
#define MR_REQ_MAX 8
|
||||
#define MR_RES_MAX 4
|
||||
#define MR_RES_MAX 2 /* 结果队列上限(M22:4→2;未取走的已解码图不计入常驻预算) */
|
||||
|
||||
/* view on_event 的事件号不在公共头(sdk/meow_ui_internal.h §6.4);本页只用到 SWIPE=3。 */
|
||||
#define MR_EV_SWIPE 3
|
||||
@@ -147,20 +157,37 @@ typedef struct page_slot {
|
||||
} page_slot;
|
||||
static page_slot g_slot[MR_SLOTS];
|
||||
|
||||
/* 解码失败页标记:避免对坏图/超大图无限重试,UI 显示「图片无法显示」而不是一直加载中。 */
|
||||
/* 解码失败页标记:坏图/无法解析 → 永久显示占位(避免无限重试)。
|
||||
* 与「预算跳过」是两回事,见 g_page_skip:后者可重试。 */
|
||||
static unsigned char g_page_fail[(MR_BOOK_MAX_PAGES + 7) / 8];
|
||||
/* 预算跳过页标记(M22):内存不足 / 单页峰值超预算 → 先不解码,显示「内存不足」占位。
|
||||
* 必须在条件改善后可重试:离开再回来(焦点页变化)、或用户点按显式重试、或冷却期后自动重试。
|
||||
* M17 的实现把这类页直接标成永久 failed,用户再也看不到——那等于把内存问题变成功能问题。 */
|
||||
static unsigned char g_page_skip[(MR_BOOK_MAX_PAGES + 7) / 8];
|
||||
static uint64_t g_skip_ms; /* 最近一次预算跳过的时刻(自动重试节流) */
|
||||
|
||||
/* 每页原始像素尺寸缓存(打开时按 count 分配;0,0=未知)。绘制/排版几何只查这里,
|
||||
* 不在每帧去 probe 文件(dir_ops_meta 是 fopen+读头,每帧一次会直接拖垮翻页)。 */
|
||||
typedef struct page_meta { int w, h; } page_meta;
|
||||
static page_meta *g_meta;
|
||||
|
||||
/* 本次解码计划(主线程在 queue_page 里算好,经 page_req 传给 worker;worker 在
|
||||
* mr_book_open 前写入这几个静态量,解码回调只读它们 → 后台线程不再读 UI 全局几何)。 */
|
||||
static int g_plan_want_w, g_plan_want_h; /* 显示框(几何,未钳制) */
|
||||
static int g_plan_long; /* 实际解码长边(受单页字节上限钳制) */
|
||||
/* 本次解码计划(主线程在 queue_page 里按显示框算好,经 page_req 传给 worker;worker 在
|
||||
* mr_book_open 前写入这些静态量,解码回调只读它们 → 后台线程不再读 UI 全局几何)。
|
||||
* want_* = 显示框(几何,slot_ready 的判据);box_* = 字节上限钳制后的落地尺寸
|
||||
* (解码结果会精确重采样到 box_*,所以命中页仍是 1 次 blit)。 */
|
||||
typedef struct decode_plan {
|
||||
int want_w, want_h;
|
||||
int box_w, box_h;
|
||||
} decode_plan;
|
||||
static decode_plan g_plan;
|
||||
static int g_plan_page; /* 本次解码的页号(只用于日志) */
|
||||
static int g_decode_skip; /* reader_load_page 的跳过原因(MR_SKIP_*) */
|
||||
static volatile int g_decode_count; /* 累计解码次数(观察重解抖动/活锁) */
|
||||
|
||||
#define MR_SKIP_NONE 0 /* 解出来了 */
|
||||
#define MR_SKIP_LOWMEM 1 /* 内存门控拦下(MemAvailable 不足) */
|
||||
#define MR_SKIP_PEAK 2 /* 真实峰值超预算,且没有余量放行 */
|
||||
|
||||
static int page_failed(int idx)
|
||||
{
|
||||
if (idx < 0 || idx >= MR_BOOK_MAX_PAGES) return 0;
|
||||
@@ -173,18 +200,53 @@ static void page_mark_failed(int idx)
|
||||
g_page_fail[idx >> 3] |= (unsigned char)(1u << (idx & 7));
|
||||
}
|
||||
|
||||
static int page_skipped(int idx)
|
||||
{
|
||||
if (idx < 0 || idx >= MR_BOOK_MAX_PAGES) return 0;
|
||||
return (g_page_skip[idx >> 3] >> (idx & 7)) & 1;
|
||||
}
|
||||
|
||||
static void page_mark_skipped(int idx)
|
||||
{
|
||||
if (idx < 0 || idx >= MR_BOOK_MAX_PAGES) return;
|
||||
g_page_skip[idx >> 3] |= (unsigned char)(1u << (idx & 7));
|
||||
g_skip_ms = meow_ui_time_ms();
|
||||
}
|
||||
|
||||
static void page_skip_clear(int idx)
|
||||
{
|
||||
if (idx < 0 || idx >= MR_BOOK_MAX_PAGES) return;
|
||||
g_page_skip[idx >> 3] &= (unsigned char)~(1u << (idx & 7));
|
||||
}
|
||||
|
||||
/* 该页现在没图、且不会自己再来(永久失败 / 被预算跳过)→ 画占位文字而不是「加载中…」。
|
||||
* 两者都是「没图」,但只有 skip 是可重试的(见 maybe_retry_skipped_focus)。 */
|
||||
static int page_blocked(int idx)
|
||||
{
|
||||
return page_failed(idx) || page_skipped(idx);
|
||||
}
|
||||
|
||||
/* 占位文字:坏图是不可恢复的;预算跳过是「内存不足」,点按可重试(任务 2 的可重试语义)。 */
|
||||
static const char *page_placeholder(int idx)
|
||||
{
|
||||
if (page_skipped(idx)) return "内存不足,点按重试";
|
||||
if (page_failed(idx)) return "图片无法显示";
|
||||
return "加载中…";
|
||||
}
|
||||
|
||||
/* ---- 后台解码队列(请求携带解码几何,结果带回同一条几何) ---- */
|
||||
typedef struct page_req {
|
||||
int idx;
|
||||
unsigned gen;
|
||||
int want_w, want_h; /* 按哪个显示框解码(主线程算) */
|
||||
int long_side; /* 实际解码长边(≤ 单页字节上限) */
|
||||
int want_w, want_h; /* 按哪个显示框解码(主线程算,slot_ready 判据) */
|
||||
int box_w, box_h; /* 落地上限(单页字节上限钳制后的目标尺寸) */
|
||||
} page_req;
|
||||
typedef struct page_res {
|
||||
int idx;
|
||||
unsigned gen;
|
||||
meow_img *img;
|
||||
int fail;
|
||||
int skip; /* MR_SKIP_*:非 0 = 预算跳过(可重试,不是坏图) */
|
||||
int want_w, want_h;
|
||||
} page_res;
|
||||
|
||||
@@ -254,92 +316,15 @@ static void cont_save_now(void);
|
||||
static int current_page(void);
|
||||
|
||||
/* ============================================================ */
|
||||
/* 页源解码回调(注入 logic 层的 mr_book 契约;可在后台线程调用) */
|
||||
/* ============================================================ */
|
||||
/* 扩展名判断:JPEG 支持解码期缩解(M12),PNG/BMP 只能整幅解码后降采样。 */
|
||||
static int path_is_jpeg(const char *path)
|
||||
{
|
||||
const char *dot = path ? strrchr(path, '.') : NULL;
|
||||
if (!dot) return 0;
|
||||
dot++;
|
||||
return strcasecmp(dot, "jpg") == 0 || strcasecmp(dot, "jpeg") == 0;
|
||||
}
|
||||
/* 内存读数与观测(M22) */
|
||||
/* ------------------------------------------------------------
|
||||
* /proc/meminfo 与 /proc/self/status 是唯一可用的实时内存事实来源。读不到(宿主/非 Linux)
|
||||
* 一律返回 -1,调用方必须按「未知」处理,不许猜。宿主测试用 mr_reader_ui_test_set_mem()
|
||||
* 注入固定读数,让内存门控/放宽档可以确定性验证。
|
||||
* ============================================================ */
|
||||
static long g_mem_free_override = -1; /* >=0 = 宿主测试注入的 MemFree(kB) */
|
||||
static long g_mem_avail_override = -1; /* >=0 = 宿主测试注入的 MemAvailable(kB) */
|
||||
|
||||
/* 由显示框(want_w×want_h)反推解码长边:默认就解到显示框大小(解码后可直接 blit),
|
||||
* 只有单页缓存字节上限(MR_PAGE_BYTES_CAP)是硬上界——超长条漫在这里按比例收缩。
|
||||
* 不再钳到屏长边:条漫页常见长宽比 1.6~1.8,显示高度本来就 > 屏长边,钳了必然每帧重采样。 */
|
||||
static int plan_decode_long(int want_w, int want_h)
|
||||
{
|
||||
int l0 = want_w > want_h ? want_w : want_h;
|
||||
int s0 = want_w > want_h ? want_h : want_w;
|
||||
if (l0 <= 0) return 0;
|
||||
int l = l0 < 64 ? 64 : l0;
|
||||
while (l > 64) {
|
||||
int s = (int)((long long)s0 * l / l0);
|
||||
if ((long long)l * s * 4 <= MR_PAGE_BYTES_CAP) break;
|
||||
l = l * 3 / 4;
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
static meow_img *reader_load_page(const char *path, void *ud, int maxdim)
|
||||
{
|
||||
(void)ud;
|
||||
if (!path || !path[0]) return NULL;
|
||||
|
||||
/* 解码计划由 queue_page() 在主线程算好(want 显示框 + 实际长边),worker/主线程同步兜底
|
||||
* 路径在 mr_book_open 前写入;无计划时退回旧预算 maxdim。 */
|
||||
int want_w = g_plan_want_w, want_h = g_plan_want_h;
|
||||
int dim = g_plan_long > 0 ? g_plan_long : maxdim;
|
||||
|
||||
/* 预算预判(M12):先读文件头拿原始尺寸,不解码像素。
|
||||
* - 解码中间缓冲峰值超上限 → 跳过(页面显示占位),避免真机被 OOM 杀掉(闪退);
|
||||
* - 峰值 = 解码缓冲 + 随后的 downscale 输出(≤单页缓存上限),两者会同时驻留;
|
||||
* - 非缩解格式(PNG/BMP)原始 RGBA 超过告警线 → 记 WARN(残余风险)。 */
|
||||
int pw = 0, ph = 0, ch = 0;
|
||||
int have = meow_img_probe_file(path, &pw, &ph, &ch) && pw > 0 && ph > 0;
|
||||
if (have) {
|
||||
int l0 = pw > ph ? pw : ph;
|
||||
long long out_bytes = MR_PAGE_BYTES_CAP; /* downscale/缩解输出上限 */
|
||||
if (want_w > 0 && want_h > 0 && (long long)want_w * want_h * 4 < out_bytes)
|
||||
out_bytes = (long long)want_w * want_h * 4;
|
||||
long long decode_bytes;
|
||||
if (path_is_jpeg(path)) {
|
||||
/* 估 stbi 缩解后的输出(与 SDK 的档位选择同式):仍超上限才跳过。 */
|
||||
int s = 1;
|
||||
while (s < 8 && (l0 / (s * 2)) >= dim) s *= 2;
|
||||
decode_bytes = (long long)(pw / s) * (ph / s) * 4;
|
||||
} else {
|
||||
decode_bytes = (long long)pw * ph * 4; /* 整幅解码后降采样 */
|
||||
if (decode_bytes > MR_LOAD_RAW_WARN)
|
||||
meow_log(MEOW_LOG_WARN, "reader decode large non-scalable %dx%d (%lldKB): %s",
|
||||
pw, ph, decode_bytes / 1024, path);
|
||||
}
|
||||
if (decode_bytes + out_bytes > MR_DECODE_PEAK_CAP) {
|
||||
meow_log(MEOW_LOG_WARN, "reader decode skip %dx%d (peak %lldKB > cap): %s",
|
||||
pw, ph, (decode_bytes + out_bytes) / 1024, path);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
meow_img *im = meow_img_load_file_dim(path, dim);
|
||||
if (!im) return NULL;
|
||||
/* 兜底:M12 之前的格式/未知格式若没被缩到预算内,再降一次(保持驻留纪律)。 */
|
||||
if (dim > 0 && (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; }
|
||||
}
|
||||
return im;
|
||||
}
|
||||
|
||||
static void reader_unload_page(meow_img *img)
|
||||
{
|
||||
meow_img_free(img);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 内存观测(验收用:进出 reader 对比) */
|
||||
/* ============================================================ */
|
||||
static long mem_kb(const char *path, const char *key)
|
||||
{
|
||||
FILE *f = fopen(path, "r");
|
||||
@@ -354,6 +339,21 @@ static long mem_kb(const char *path, const char *key)
|
||||
return v;
|
||||
}
|
||||
|
||||
/* MemFree(kB)。真机杀点就是「MemFree 见底」:15,600kB 那次只剩 1,404kB。 */
|
||||
static long mem_free_kb(void)
|
||||
{
|
||||
if (g_mem_free_override >= 0) return g_mem_free_override;
|
||||
return mem_kb("/proc/meminfo", "MemFree:");
|
||||
}
|
||||
|
||||
/* MemAvailable(kB):含可回收页缓存。注意本机在 MemFree 1.4MB 时仍有 ~26MB,
|
||||
* 所以它只能当「系统极度缺页」的门控,不能当解码配额(内核不会等回收就杀进程)。 */
|
||||
static long mem_avail_kb(void)
|
||||
{
|
||||
if (g_mem_avail_override >= 0) return g_mem_avail_override;
|
||||
return mem_kb("/proc/meminfo", "MemAvailable:");
|
||||
}
|
||||
|
||||
static int resident_pages(void)
|
||||
{
|
||||
int n = 0;
|
||||
@@ -370,11 +370,206 @@ static long resident_bytes(void)
|
||||
return b;
|
||||
}
|
||||
|
||||
/* 进出阅读器各打一行(任务 5):RSS 看当前占用,VmHWM 看本次进入的峰值——
|
||||
* 验收就是「离开时的 hwm 不超过预算」。 */
|
||||
static void log_mem(const char *tag)
|
||||
{
|
||||
meow_log(MEOW_LOG_INFO, "reader mem[%s]: avail=%ldKB rss=%ldKB pages=%d img=%ldKB",
|
||||
tag, mem_kb("/proc/meminfo", "MemAvailable:"),
|
||||
mem_kb("/proc/self/status", "VmRSS:"), resident_pages(), resident_bytes() / 1024);
|
||||
meow_log(MEOW_LOG_INFO, "reader mem[%s]: rss=%ldkB hwm=%ldkB memfree=%ldkB memavail=%ldkB pages=%d img=%ldkB",
|
||||
tag, mem_kb("/proc/self/status", "VmRSS:"),
|
||||
mem_kb("/proc/self/status", "VmHWM:"), mem_free_kb(), mem_avail_kb(),
|
||||
resident_pages(), resident_bytes() / 1024);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 页源解码回调(注入 logic 层的 mr_book 契约;可在后台线程调用) */
|
||||
/* ============================================================ */
|
||||
/* 由显示框(want_w×want_h)反推解码长边:默认就解到显示框大小(解码后可直接 blit),
|
||||
* 只有单页缓存字节上限(MR_PAGE_BYTES_CAP)是硬上界——超长条漫在这里按比例收缩。
|
||||
* 不再钳到屏长边:条漫页常见长宽比 1.6~1.8,显示高度本来就 > 屏长边,钳了必然每帧重采样。 */
|
||||
static int plan_decode_long(int want_w, int want_h)
|
||||
{
|
||||
int l0 = want_w > want_h ? want_w : want_h;
|
||||
int s0 = want_w > want_h ? want_h : want_w;
|
||||
if (l0 <= 0) return 0;
|
||||
int l = l0 < 64 ? 64 : l0;
|
||||
while (l > 64) {
|
||||
int s = (int)((long long)s0 * l / l0);
|
||||
if ((long long)l * s * 4 <= MR_PAGE_BYTES_CAP) break;
|
||||
l = l * 3 / 4;
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
/* 显示框 → 实际落地尺寸(保持长宽比,受单页字节上限钳制)。
|
||||
* 正常情况下 box == want(解码结果就是显示尺寸 → 单次 blit);只有超大屏/超长条漫
|
||||
* 才会收缩,此时绘制走缩放路径(M17 已知代价,不再是常态)。 */
|
||||
static void plan_box_size(int want_w, int want_h, int *bw, int *bh)
|
||||
{
|
||||
int w = want_w > 0 ? want_w : g_vw;
|
||||
int h = want_h > 0 ? want_h : g_vh;
|
||||
if (w <= 0 || h <= 0) { w = h = 1; }
|
||||
int long0 = w > h ? w : h;
|
||||
int l = plan_decode_long(w, h);
|
||||
if (l > 0 && l < long0) {
|
||||
if (w >= h) { h = (int)((long long)h * l / w); w = l; }
|
||||
else { w = (int)((long long)w * l / h); h = l; }
|
||||
if (w < 1) w = 1;
|
||||
if (h < 1) h = 1;
|
||||
}
|
||||
*bw = w;
|
||||
*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'` 后按列看曲线。 */
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
static meow_img *reader_load_page(const char *path, void *ud, int maxdim)
|
||||
{
|
||||
(void)ud;
|
||||
g_decode_skip = MR_SKIP_NONE;
|
||||
uint64_t t0 = meow_ui_time_ms();
|
||||
if (!path || !path[0]) return NULL;
|
||||
|
||||
/* 解码计划由 queue_page() 在主线程算好(显示框 + 落地上限);worker/主线程同步兜底
|
||||
* 路径在 mr_book_open 前写入。无计划时退回旧预算 maxdim。 */
|
||||
int want_w = g_plan.want_w, want_h = g_plan.want_h;
|
||||
int box_w = g_plan.box_w, box_h = g_plan.box_h;
|
||||
int page = g_plan_page;
|
||||
if (box_w <= 0 || box_h <= 0) {
|
||||
if (maxdim > 0) {
|
||||
box_w = box_h = maxdim;
|
||||
} else {
|
||||
box_w = want_w > 0 ? want_w : 1;
|
||||
box_h = want_h > 0 ? want_h : 1;
|
||||
}
|
||||
}
|
||||
int dim = box_w > box_h ? box_w : box_h;
|
||||
|
||||
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) 头部探测(零像素内存)→ 真实峰值模型 → 选缩解档位。 */
|
||||
mr_img_info in;
|
||||
int have = mr_probe_info(path, &in);
|
||||
int step = 1;
|
||||
long long peak = 0;
|
||||
if (have) {
|
||||
long long budget = decode_peak_budget(mfree);
|
||||
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,
|
||||
(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)
|
||||
: 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,
|
||||
(unsigned long long)(meow_ui_time_ms() - t0));
|
||||
return NULL;
|
||||
}
|
||||
/* 兜底:非 JPEG 或粗档位输出没被缩到上限内,再降一次(保持驻留纪律)。 */
|
||||
if (dim > 0 && (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; }
|
||||
}
|
||||
/* 4) 精确落到显示框(仅当「故意用了粗档位」时):结果 = box → 命中页仍是一次原生 blit,
|
||||
* 不会把内存换来的降级变成每帧重采样(M17 的热路径纪律)。
|
||||
* 源图本身就小于显示框(小图/缩略图)时绝不放大:那会凭空多占常驻字节,
|
||||
* 且 M17 的 slot_ready 语义依赖「SDK 不做上采样」。 */
|
||||
if (have && 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,
|
||||
(unsigned long long)(meow_ui_time_ms() - t0));
|
||||
return im;
|
||||
}
|
||||
|
||||
static void reader_unload_page(meow_img *img)
|
||||
{
|
||||
meow_img_free(img);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
@@ -551,7 +746,9 @@ static int slot_ready(int idx)
|
||||
return abs(w - s->want_w) <= MR_SLOT_TOL && abs(h - s->want_h) <= MR_SLOT_TOL;
|
||||
}
|
||||
|
||||
/* 淘汰离焦点页最远的一张(不淘汰 focus 与 except)。返回被清掉的槽下标,无则 -1。 */
|
||||
/* 淘汰离焦点页最远的一张(不淘汰 focus、可见页与 except)。返回被清掉的槽下标,无则 -1。
|
||||
* M22:可见页不淘汰——条漫视口可能同时露出 2–3 页,按距离淘汰会把屏幕上的页拆掉,
|
||||
* 结果是「一页闪占位 → 重解码 → 再被拆」的抖动。宁可临时超一点字节,也不让可见页消失。 */
|
||||
static int slot_evict_far(int except)
|
||||
{
|
||||
int focus = focus_page();
|
||||
@@ -559,6 +756,7 @@ static int slot_evict_far(int except)
|
||||
for (int i = 0; i < MR_SLOTS; i++) {
|
||||
if (i == except || !g_slot[i].img) continue;
|
||||
if (g_slot[i].idx == focus) continue;
|
||||
if (page_visible(g_slot[i].idx)) continue;
|
||||
int d = abs(g_slot[i].idx - focus);
|
||||
if (d > far_d) { far_d = d; far = i; }
|
||||
}
|
||||
@@ -566,6 +764,17 @@ static int slot_evict_far(int except)
|
||||
return far;
|
||||
}
|
||||
|
||||
/* 开新页前先腾地方(任务 3):按「即将落地的单页字节」预判,先淘汰非可见/非焦点的旧图,
|
||||
* 让解码缓冲不与陈旧常驻叠加。只在逼近上限时才动手,避免把预取邻居立刻扔掉
|
||||
*(那样翻页会从「命中」退化成「重新解码」,反而增加峰值次数)。 */
|
||||
static void slots_reserve(long need_bytes)
|
||||
{
|
||||
if (need_bytes <= 0) return;
|
||||
while (resident_bytes() + need_bytes > MR_RESIDENT_CAP) {
|
||||
if (slot_evict_far(-1) < 0) return; /* 只剩可见页:放行,下一次 slot_put 再收敛 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 放进缓存;同页旧图直接替换(fit/zoom/模式变了要换尺寸)。返回 1=已放入。
|
||||
* want_w/want_h = 这次解码按哪个显示框落的,交给 slot_ready 做几何判据。 */
|
||||
static int slot_put(int idx, meow_img *img, int want_w, int want_h)
|
||||
@@ -579,12 +788,14 @@ static int slot_put(int idx, meow_img *img, int want_w, int want_h)
|
||||
if (i < 0) {
|
||||
if (empty >= 0) {
|
||||
i = empty;
|
||||
} else { /* 全满:替换离焦点页最远的一张 */
|
||||
} else { /* 全满:替换离焦点页最远、且不可见的一张 */
|
||||
int focus = focus_page(), far_d = -1;
|
||||
for (int k = 0; k < MR_SLOTS; k++) {
|
||||
if (page_visible(g_slot[k].idx)) continue;
|
||||
int d = abs(g_slot[k].idx - focus);
|
||||
if (d > far_d) { far_d = d; i = k; }
|
||||
}
|
||||
if (i < 0) i = g_slot[0].img ? 0 : -1; /* 全可见:只能挤掉第一槽(极端条漫页) */
|
||||
}
|
||||
}
|
||||
if (i < 0) { meow_img_free(img); return 0; }
|
||||
@@ -666,15 +877,30 @@ static int req_queued(int idx)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 预取闸门(任务 4):拖动/惯性期间只解落点页,不做邻居预取;且只在内存确实富裕时预取。
|
||||
* MemAvailable 读不到按「不预取」处理(保守)。 */
|
||||
static int prefetch_allowed(void)
|
||||
{
|
||||
if (g_dragging) return 0;
|
||||
if (g_scroll && meow_scroll_active(g_scroll)) return 0;
|
||||
long a = mem_avail_kb();
|
||||
if (a < 0) return 0;
|
||||
return a >= MR_PREFETCH_MIN_KB;
|
||||
}
|
||||
|
||||
static void queue_page(int idx)
|
||||
{
|
||||
if (!mr_book_valid(&g_book) || idx < 0 || idx >= g_count) return;
|
||||
if (slot_ready(idx) || page_failed(idx)) return;
|
||||
/* 永久失败与「仍在跳过期」的页都不排;后者由 maybe_retry_skipped_focus 解封。 */
|
||||
if (slot_ready(idx) || page_blocked(idx)) return;
|
||||
/* 几何在这里(主线程)算好塞进请求:worker 不再读 g_fit/g_zoom/g_vw/g_vh。 */
|
||||
int want_w = 0, want_h = 0;
|
||||
page_geometry_at(idx, &want_w, &want_h);
|
||||
if (want_w <= 0 || want_h <= 0) { want_w = g_vw; want_h = g_vh; }
|
||||
int long_side = plan_decode_long(want_w, want_h);
|
||||
int box_w = 0, box_h = 0;
|
||||
plan_box_size(want_w, want_h, &box_w, &box_h);
|
||||
/* 先释放再解码:按这一页的落地字节先淘汰不可见旧图,别让常驻与解码缓冲叠加。 */
|
||||
slots_reserve((long)box_w * box_h * 4);
|
||||
pthread_mutex_lock(&g_lock);
|
||||
if (!g_stop && !req_queued(idx) && g_req_n < MR_REQ_MAX) {
|
||||
page_req *r = &g_req[g_req_tail];
|
||||
@@ -682,7 +908,8 @@ static void queue_page(int idx)
|
||||
r->gen = g_gen;
|
||||
r->want_w = want_w;
|
||||
r->want_h = want_h;
|
||||
r->long_side = long_side;
|
||||
r->box_w = box_w;
|
||||
r->box_h = box_h;
|
||||
g_req_tail = (g_req_tail + 1) % MR_REQ_MAX;
|
||||
g_req_n++;
|
||||
pthread_cond_signal(&g_cv);
|
||||
@@ -701,32 +928,40 @@ static void *reader_worker(void *arg)
|
||||
req = g_req[g_req_head];
|
||||
g_req_head = (g_req_head + 1) % MR_REQ_MAX;
|
||||
g_req_n--;
|
||||
/* 取消(任务 4 / M20 P0-3):出队后、mr_book_open 前二次判过期——旧册(gen 变)
|
||||
* 或已经不在解码窗(拖动/快翻的中间页)直接丢弃。M20 实测拖动翻页原本 1.75× 解码,
|
||||
* 全是这种在飞不可取消的请求。锁内判,窗口也在锁内写,避免共享状态竞争。 */
|
||||
int stale = (req.gen != g_gen) || !in_window(req.idx);
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
if (stale) {
|
||||
meow_log(MEOW_LOG_INFO,
|
||||
"reader decode page=%d src=0x0 peak=0kB memfree=%ldkB memavail=%ldkB skip=cancel step=0",
|
||||
req.idx, mem_free_kb(), mem_avail_kb());
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 解码计划:本线程私有(单 worker),解码回调只读这三个量。 */
|
||||
g_plan_want_w = req.want_w;
|
||||
g_plan_want_h = req.want_h;
|
||||
g_plan_long = req.long_side;
|
||||
uint64_t t0 = meow_ui_time_ms();
|
||||
/* 解码计划:本线程私有(单 worker),解码回调只读这几个量。 */
|
||||
g_plan.want_w = req.want_w;
|
||||
g_plan.want_h = req.want_h;
|
||||
g_plan.box_w = req.box_w;
|
||||
g_plan.box_h = req.box_h;
|
||||
g_plan_page = req.idx;
|
||||
g_decode_skip = MR_SKIP_NONE;
|
||||
meow_img *img = mr_book_open(&g_book, req.idx);
|
||||
uint64_t dt = meow_ui_time_ms() - t0;
|
||||
g_decode_count++;
|
||||
int w = img ? meow_img_w(img) : 0;
|
||||
int h = img ? meow_img_h(img) : 0;
|
||||
meow_log(MEOW_LOG_INFO, "reader decode page=%d want=%dx%d long=%d ms=%llu %dx%d %s",
|
||||
req.idx, req.want_w, req.want_h, req.long_side,
|
||||
(unsigned long long)dt, w, h, img ? "ok" : "fail");
|
||||
int skip = img ? MR_SKIP_NONE : g_decode_skip;
|
||||
|
||||
pthread_mutex_lock(&g_lock);
|
||||
if (!g_stop && g_res_n < MR_RES_MAX) {
|
||||
g_res[g_res_n].idx = req.idx;
|
||||
g_res[g_res_n].gen = req.gen;
|
||||
g_res[g_res_n].img = img;
|
||||
g_res[g_res_n].fail = img ? 0 : 1;
|
||||
g_res[g_res_n].fail = (img || skip) ? 0 : 1;
|
||||
g_res[g_res_n].skip = skip;
|
||||
g_res[g_res_n].want_w = req.want_w;
|
||||
g_res[g_res_n].want_h = req.want_h;
|
||||
g_res_n++;
|
||||
img = NULL; /* 所有权已移交(失败时本就是 NULL) */
|
||||
img = NULL; /* 所有权已移交(失败/跳过时本就是 NULL) */
|
||||
}
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
if (img) meow_img_free(img);
|
||||
@@ -781,7 +1016,8 @@ static void queue_prune(void)
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
}
|
||||
|
||||
/* 收取后台结果:只留窗口内新页。返回 1=有图落位。 */
|
||||
/* 收取后台结果:只留窗口内新页。返回 1=有图落位。
|
||||
* 结果分三种:ok(落槽)/ fail(坏图,永久占位)/ skip(预算跳过,可重试占位)。 */
|
||||
static int results_drain(void)
|
||||
{
|
||||
page_res got[MR_RES_MAX];
|
||||
@@ -795,6 +1031,12 @@ static int results_drain(void)
|
||||
int changed = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (got[i].gen != g_gen) { meow_img_free(got[i].img); continue; }
|
||||
if (got[i].skip) {
|
||||
/* 预算跳过(内存不足/峰值超限):可重试,不写永久失败位。 */
|
||||
page_mark_skipped(got[i].idx);
|
||||
if (page_visible(got[i].idx)) g_fail_paint = 1;
|
||||
continue;
|
||||
}
|
||||
if (got[i].fail) {
|
||||
page_mark_failed(got[i].idx);
|
||||
if (page_visible(got[i].idx)) g_fail_paint = 1;
|
||||
@@ -803,6 +1045,7 @@ static int results_drain(void)
|
||||
if (in_window(got[i].idx)) {
|
||||
if (slot_put(got[i].idx, got[i].img, got[i].want_w, got[i].want_h) &&
|
||||
page_visible(got[i].idx)) changed = 1;
|
||||
page_skip_clear(got[i].idx); /* 解出来了:撤销跳过标记 */
|
||||
} else {
|
||||
meow_img_free(got[i].img);
|
||||
}
|
||||
@@ -810,14 +1053,40 @@ static int results_drain(void)
|
||||
return changed;
|
||||
}
|
||||
|
||||
/* 窗口随视口/当前页移动:淘汰窗口外图 → 清掉窗口外请求 → 按优先级补页。 */
|
||||
/* 预算跳过的页如何重试(任务 2 的可重试语义):只对**焦点页**重试,且冷却 MR_SKIP_RETRY_MS,
|
||||
* 这样「内存暂时不够」不会变成永久占位,也不会变成每 tick 一次的重试风暴。
|
||||
* force=1 是用户显式重试(点按占位页)。 */
|
||||
static void maybe_retry_skipped_focus(int force)
|
||||
{
|
||||
int f = focus_page();
|
||||
if (f < 0 || !page_skipped(f) || page_failed(f)) return;
|
||||
if (!force) {
|
||||
uint64_t now = meow_ui_time_ms();
|
||||
if (now - g_skip_ms < MR_SKIP_RETRY_MS) return;
|
||||
}
|
||||
page_skip_clear(f);
|
||||
g_skip_ms = meow_ui_time_ms();
|
||||
meow_log(MEOW_LOG_INFO, "reader decode retry page=%d%s", f + 1, force ? " (tap)" : "");
|
||||
}
|
||||
|
||||
/* 窗口随视口/当前页移动:淘汰窗口外图 → 清掉窗口外请求 → 按优先级补页。
|
||||
* 窗口本身在锁内写(worker 的取消判据要读它)。 */
|
||||
static void window_update(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_lock);
|
||||
window_compute();
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
/* 不主动清窗口外的图:多留一页「回看缓存」,避免 0→1→0 来回翻时反复重解同一页。
|
||||
* 容量仍由 MR_SLOTS 与 slot_put 的字节淘汰兜底(最多 MR_SLOTS 张)。 */
|
||||
queue_prune();
|
||||
for (int i = 0; i < g_win_n; i++) queue_page(g_win_idx[i]);
|
||||
/* 回到某页 → 允许重试这一页(「离开再回来」的可重试语义)。 */
|
||||
maybe_retry_skipped_focus(0);
|
||||
/* 队列第一位 = 跳页目标或焦点页(必解);其余是预取,按内存闸门决定做不做。 */
|
||||
int prefetch = prefetch_allowed();
|
||||
for (int i = 0; i < g_win_n; i++) {
|
||||
if (i > 0 && !prefetch) continue;
|
||||
queue_page(g_win_idx[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
@@ -1005,7 +1274,7 @@ static void draw_strip(const meow_theme *th)
|
||||
meow_ui_fill_rect(0, y0, g_vw, y1 - y0, th->surfaceContainer);
|
||||
if (y1 - y0 >= 48)
|
||||
meow_ui_text_center(g_vw / 2, y0 + (y1 - y0) / 2 - 10,
|
||||
page_failed(i) ? "图片无法显示" : "加载中…",
|
||||
page_placeholder(i),
|
||||
th->font_body, th->onSurfaceVariant);
|
||||
}
|
||||
}
|
||||
@@ -1069,7 +1338,7 @@ static void reader_draw(meow_view *v, const meow_theme *th, void *ctx)
|
||||
draw_page_at(g_page, 0, 0);
|
||||
if (!slot_find(g_page)) {
|
||||
meow_ui_text_center(w / 2, h / 2,
|
||||
page_failed(g_page) ? "图片无法显示" : "加载中…",
|
||||
page_placeholder(g_page),
|
||||
th->font_body, th->onSurfaceVariant);
|
||||
}
|
||||
draw_toast(th);
|
||||
@@ -1316,7 +1585,8 @@ static void jump_land(int page, int timed_out)
|
||||
meow_log(MEOW_LOG_INFO, "reader jump land page=%d/%d%s",
|
||||
page + 1, g_count, timed_out ? " (timeout)" : "");
|
||||
if (timed_out) toast("页面解码较慢,已跳到目标页");
|
||||
else if (page_failed(page)) toast("目标页无法显示");
|
||||
else if (page_blocked(page))
|
||||
toast(page_skipped(page) ? "目标页内存不足,已跳过" : "目标页无法显示");
|
||||
}
|
||||
|
||||
static void jump_commit_from_slider(void)
|
||||
@@ -1430,6 +1700,8 @@ static void open_book_reset_state(void)
|
||||
g_win_n = 0;
|
||||
g_fail_paint = 0;
|
||||
memset(g_page_fail, 0, sizeof g_page_fail);
|
||||
memset(g_page_skip, 0, sizeof g_page_skip);
|
||||
g_skip_ms = 0;
|
||||
g_jump_open = 0;
|
||||
mr_jump_cancel(&g_jump);
|
||||
g_toast[0] = '\0';
|
||||
@@ -1525,9 +1797,13 @@ static int open_book(const char *dir, int start_page, int mode)
|
||||
int ww = 0, wh = 0;
|
||||
page_geometry_at(g_page, &ww, &wh);
|
||||
if (ww <= 0 || wh <= 0) { ww = g_vw; wh = g_vh; }
|
||||
g_plan_want_w = ww;
|
||||
g_plan_want_h = wh;
|
||||
g_plan_long = plan_decode_long(ww, wh);
|
||||
int bw = ww, bh = wh;
|
||||
plan_box_size(ww, wh, &bw, &bh);
|
||||
g_plan.want_w = ww;
|
||||
g_plan.want_h = wh;
|
||||
g_plan.box_w = bw;
|
||||
g_plan.box_h = bh;
|
||||
g_plan_page = g_page;
|
||||
meow_img *first = mr_book_open(&g_book, g_page);
|
||||
if (first) slot_put(g_page, first, ww, wh);
|
||||
meow_log(MEOW_LOG_WARN, "reader worker unavailable, sync first page");
|
||||
@@ -1776,6 +2052,18 @@ static void tap_content(int x, int y)
|
||||
(void)y;
|
||||
if (g_count <= 0) return;
|
||||
int zone = mr_nav_tap_zone(x, g_vw);
|
||||
/* 当前页被预算跳过(内存不足占位)→ 中间区点按就是显式重试(任务 2:不能永久 failed)。
|
||||
* 只占中间区:左右 1/3 仍必须能翻页,否则占位页会把读者困在这一页。 */
|
||||
if (zone == MR_ZONE_MIDDLE) {
|
||||
int f = focus_page();
|
||||
if (f >= 0 && page_skipped(f) && !page_failed(f)) {
|
||||
maybe_retry_skipped_focus(1);
|
||||
toast("正在重试…");
|
||||
window_update();
|
||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (zone == MR_ZONE_MIDDLE) {
|
||||
set_chrome(!g_chrome);
|
||||
return;
|
||||
@@ -1929,7 +2217,7 @@ static void reader_tick(void)
|
||||
|
||||
if (mr_jump_active(&g_jump)) {
|
||||
int target = mr_jump_target(&g_jump);
|
||||
int have = target >= 0 && (slot_find(target) != NULL || page_failed(target));
|
||||
int have = target >= 0 && (slot_find(target) != NULL || page_blocked(target));
|
||||
int timed_out = 0;
|
||||
if (mr_jump_poll(&g_jump, have, now, MR_JUMP_TIMEOUT_MS, &timed_out))
|
||||
jump_land(target, timed_out);
|
||||
@@ -2064,7 +2352,9 @@ static void reader_state_reset(void)
|
||||
free(g_meta);
|
||||
g_meta = NULL;
|
||||
g_start_at = MR_READER_START_RESUME;
|
||||
g_plan_want_w = g_plan_want_h = g_plan_long = 0;
|
||||
memset(&g_plan, 0, sizeof g_plan);
|
||||
g_plan_page = 0;
|
||||
g_decode_skip = MR_SKIP_NONE;
|
||||
}
|
||||
|
||||
static void reader_page_enter(meow_view *v, void *ctx)
|
||||
@@ -2250,6 +2540,28 @@ int mr_reader_ui_failed_pages(void)
|
||||
if (page_failed(i)) n++;
|
||||
return n;
|
||||
}
|
||||
int mr_reader_ui_skipped_pages(void)
|
||||
{
|
||||
int n = 0;
|
||||
for (int i = 0; i < g_count && i < MR_BOOK_MAX_PAGES; i++)
|
||||
if (page_skipped(i) && !page_failed(i)) n++;
|
||||
return n;
|
||||
}
|
||||
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;
|
||||
}
|
||||
/* 峰值模型自检(只读):用真代码的模型算某个文件的预估峰值,供宿主测试与实测对照。 */
|
||||
long long mr_reader_ui_decode_peak_kb(const char *path, int step, int box_w, int box_h)
|
||||
{
|
||||
mr_img_info in;
|
||||
if (!path || !mr_probe_info(path, &in)) return 0;
|
||||
return decode_peak_bytes(&in, step, box_w, box_h) / 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_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; }
|
||||
int mr_reader_ui_decode_count(void) { return g_decode_count; }
|
||||
@@ -2266,3 +2578,4 @@ void mr_reader_test_set_root(const char *root)
|
||||
}
|
||||
const char *mr_reader_test_root(void) { return g_root; }
|
||||
#endif
|
||||
|
||||
+10
-1
@@ -38,7 +38,16 @@ int mr_reader_ui_count(void); /* 当前册页数 */
|
||||
int mr_reader_ui_jump_open(void); /* 1=跳页滑块已展开 */
|
||||
int mr_reader_ui_jump_pending(void);/* 1=跳页目标等待解码 */
|
||||
long mr_reader_ui_resident_kb(void);/* 驻留页图字节(KB) */
|
||||
int mr_reader_ui_failed_pages(void);/* 解码失败/超预算被跳过的页数 */
|
||||
int mr_reader_ui_failed_pages(void);/* 解码失败(坏图)页数 */
|
||||
int mr_reader_ui_skipped_pages(void);/* 被内存预算跳过(可重试)的页数 */
|
||||
/* 注入 /proc/meminfo 读数(kB);-1 = 恢复真实读取。用于确定性验证内存门控与放宽档。 */
|
||||
void mr_reader_ui_test_set_mem(long free_kb, long avail_kb);
|
||||
/* 峰值模型自检:用产品代码里的同一个模型算预估峰值(kB),供宿主测试与 --wrap malloc
|
||||
* 实测对照。0=探测失败。 */
|
||||
long long mr_reader_ui_decode_peak_kb(const char *path, int step, int box_w, int box_h);
|
||||
long mr_reader_ui_peak_cap_kb(void); /* 常态峰值上限(kB) */
|
||||
long mr_reader_ui_peak_budget_kb(void); /* 当前 MemFree 下的实际预算(kB) */
|
||||
long mr_reader_ui_resident_cap_kb(void); /* 常驻上限(kB) */
|
||||
int mr_reader_ui_blit_draws(void); /* 累计原生 blit 绘页次数(热路径应为纯 blit) */
|
||||
int mr_reader_ui_scaled_draws(void);/* 累计重采样绘页次数(每帧重采样=卡顿) */
|
||||
int mr_reader_ui_decode_count(void);/* 累计解码次数(查重解抖动/活锁) */
|
||||
|
||||
Reference in new issue
Block a user