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:
AskaEth committed 2026-11-05 21:28:50 +08:00
1 parent 773ab9ff92
commit 40751c09bd
10 files changed
+1257 -166

No files matched your search

+121 -27
View File
@@ -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;
}
/* ============================================================ */
+24
View File
@@ -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 当一本书 */
/* ------------------------------------------------------------