Files
MeowOS-SDK/tuku/logic/reader/page_source.c
T
AskaEth 40751c09bd 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 交叉构建零警告。
2026-11-05 21:28:50 +08:00

442 lines
15 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* logic/reader/page_source.c — 页源契约实现(纯 C):目录册 + 图片头探测。
*
* 契约与两种消费方式的说明见 page_source.h。本文件不含 SDK 依赖:解码/释放由
* UI 层注回回调,因此可在宿主测试里用假回调覆盖「空册/越界/重复 open-close/首末页」。
*/
#include "logic/reader/page_source.h"
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
/* ============================================================ */
/* 契约包装 */
/* ============================================================ */
int mr_book_valid(const mr_book *b)
{
return b && b->ops && b->ops->count && b->ops->open;
}
int mr_book_count(const mr_book *b)
{
if (!mr_book_valid(b)) return 0;
int n = b->ops->count(b->ctx);
return n > 0 ? n : 0;
}
int mr_book_meta(const mr_book *b, int index, int *w, int *h)
{
if (w) *w = 0;
if (h) *h = 0;
if (!mr_book_valid(b) || !b->ops->meta) return 0;
if (index < 0 || index >= mr_book_count(b)) return 0;
return b->ops->meta(b->ctx, index, w, h) ? 1 : 0;
}
meow_img *mr_book_open(mr_book *b, int index)
{
if (!mr_book_valid(b)) return NULL;
if (index < 0 || index >= mr_book_count(b)) return NULL; /* 越界:契约边界 */
return b->ops->open(b->ctx, index);
}
void mr_book_close(mr_book *b, meow_img *img)
{
if (!img) return;
if (!mr_book_valid(b) || !b->ops->close) return;
b->ops->close(b->ctx, img);
}
void mr_book_destroy(mr_book *b)
{
if (!b) return;
if (b->ops && b->ops->destroy && b->ctx) b->ops->destroy(b->ctx);
b->ops = NULL;
b->ctx = NULL;
}
/* ============================================================ */
/* 目录册 */
/* ============================================================ */
static int name_cmp_qsort(const void *pa, const void *pb)
{
const char *a = *(const char *const *)pa;
const char *b = *(const char *const *)pb;
return mr_natural_cmp(a, b);
}
void mr_dir_book_path(const mr_dir_book *db, int index, char *out, int outsz)
{
if (!out || outsz <= 0) return;
out[0] = '\0';
if (!db || index < 0 || index >= db->count || !db->names[index]) return;
size_t dl = strlen(db->dir);
if (dl > 0 && db->dir[dl - 1] == '/')
snprintf(out, (size_t)outsz, "%s%s", db->dir, db->names[index]);
else
snprintf(out, (size_t)outsz, "%s/%s", db->dir, db->names[index]);
}
void mr_dir_book_free(mr_dir_book *db)
{
if (!db) return;
for (int i = 0; i < db->count; i++) free(db->names[i]);
free(db->names);
db->names = NULL;
db->count = 0;
}
int mr_dir_book_init(mr_dir_book *db, const char *dir, int maxdim,
mr_book_load_fn load, mr_book_unload_fn unload, void *ud)
{
if (!db) return -1;
memset(db, 0, sizeof *db);
if (!dir || !dir[0] || !load || !unload) return -1;
if (maxdim <= 0) maxdim = 720;
if (strlen(dir) >= sizeof db->dir) return -1;
DIR *d = opendir(dir);
if (!d) return -1;
snprintf(db->dir, sizeof db->dir, "%s", dir);
db->maxdim = maxdim;
db->load = load;
db->unload = unload;
db->ud = ud;
int cap = 0;
struct dirent *de;
while ((de = readdir(d)) != NULL) {
const char *name = de->d_name;
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
if (name[0] == '.') continue; /* 隐藏文件不参与成册 */
if (!mr_path_is_image(name)) continue;
char full[MR_PATH_MAX];
size_t dl = strlen(db->dir);
int n = (dl > 0 && db->dir[dl - 1] == '/')
? snprintf(full, sizeof full, "%s%s", db->dir, name)
: snprintf(full, sizeof full, "%s/%s", db->dir, name);
if (n <= 0 || n >= (int)sizeof full) continue;
struct stat st;
if (lstat(full, &st) != 0 || !S_ISREG(st.st_mode)) continue; /* 跳过符号链接/目录 */
if (db->count >= MR_BOOK_MAX_PAGES) break;
if (db->count >= cap) {
int nc = cap ? cap * 2 : 32;
char **nn = (char **)realloc(db->names, (size_t)nc * sizeof *nn);
if (!nn) { mr_dir_book_free(db); closedir(d); return -1; }
db->names = nn;
cap = nc;
}
size_t len = strlen(name);
db->names[db->count] = (char *)malloc(len + 1);
if (!db->names[db->count]) { mr_dir_book_free(db); closedir(d); return -1; }
memcpy(db->names[db->count], name, len + 1);
db->count++;
}
closedir(d);
if (db->count > 1)
qsort(db->names, (size_t)db->count, sizeof *db->names, name_cmp_qsort);
return db->count;
}
/* ============================================================ */
/* 图片头探测(不解码像素) */
/* ------------------------------------------------------------
* 同一套解析同时服务「文件」(目录册,流式读不占内存)和「内存缓冲」
* (档案册:page_cache 解压后直接探测,不必再落盘读文件)。
* ============================================================ */
typedef struct probe_src {
const unsigned char *mem; /* 非 NULL = 内存源 */
size_t len, pos;
FILE *f; /* mem == NULL 时用 */
} probe_src;
static int ps_read(probe_src *s, void *buf, size_t n)
{
if (s->mem) {
if (n > s->len - s->pos) return 0;
memcpy(buf, s->mem + s->pos, n);
s->pos += n;
return 1;
}
return fread(buf, 1, n, s->f) == n;
}
static int ps_getc(probe_src *s)
{
if (s->mem) {
if (s->pos >= s->len) return EOF;
return s->mem[s->pos++];
}
return fgetc(s->f);
}
static int ps_skip(probe_src *s, long n)
{
if (n < 0) return 0;
if (s->mem) {
if ((size_t)n > s->len - s->pos) return 0;
s->pos += (size_t)n;
return 1;
}
return fseek(s->f, n, SEEK_CUR) == 0;
}
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, mr_img_info *in)
{
unsigned char hdr[24];
if (!ps_read(s, hdr, sizeof hdr)) return 0;
static const unsigned char sig[8] = { 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A };
if (memcmp(hdr, sig, 8) != 0 || memcmp(hdr + 12, "IHDR", 4) != 0) return 0;
unsigned ww = probe_be32(hdr + 16), hh = probe_be32(hdr + 20);
if (ww == 0 || hh == 0) return 0;
/* IHDR 的位深/颜色类型(紧跟在宽高后)。截断头(stb 也读不下去)按 RGB 8bit 记。 */
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, mr_img_info *in)
{
unsigned char b[2];
if (!ps_read(s, b, 2) || b[0] != 0xFF || b[1] != 0xD8) return 0;
for (;;) {
int c = ps_getc(s);
if (c == EOF) return 0;
if (c != 0xFF) continue;
int m = ps_getc(s);
while (m == 0xFF) m = ps_getc(s); /* 填充字节 */
if (m == EOF) return 0;
if (m == 0xD8 || m == 0x01 || (m >= 0xD0 && m <= 0xD7)) continue; /* 无长度段 */
if (m == 0xD9 || m == 0xDA) return 0; /* EOI/SOS 前没等到 SOF */
unsigned char lb[2];
if (!ps_read(s, lb, 2)) return 0;
int len = ((int)lb[0] << 8) | lb[1];
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, 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;
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, mr_img_info *in)
{
unsigned char hdr[26];
if (!ps_read(s, hdr, sizeof hdr)) return 0;
if (hdr[0] != 'B' || hdr[1] != 'M') return 0;
int ww = (int)((unsigned)hdr[18] | ((unsigned)hdr[19] << 8) |
((unsigned)hdr[20] << 16) | ((unsigned)hdr[21] << 24));
int hh = (int)((unsigned)hdr[22] | ((unsigned)hdr[23] << 8) |
((unsigned)hdr[24] << 16) | ((unsigned)hdr[25] << 24));
if (hh < 0) hh = -hh; /* 自底向上 BMP */
if (ww <= 0 || hh <= 0) return 0;
unsigned char ex[4]; /* biPlanes(2) / biBitCount(2) */
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, 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, 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_info_mem(const void *buf, size_t len, mr_img_info *info)
{
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, info) || info->w <= 0 || info->h <= 0) {
memset(info, 0, sizeof *info);
return 0;
}
return 1;
}
int mr_probe_info(const char *path, mr_img_info *info)
{
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, info);
fclose(f);
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;
}
/* ============================================================ */
/* mr_book_ops 适配器(目录册) */
/* ============================================================ */
static int dir_ops_count(void *ctx)
{
mr_dir_book *db = (mr_dir_book *)ctx;
return db ? db->count : 0;
}
static int dir_ops_meta(void *ctx, int index, int *w, int *h)
{
mr_dir_book *db = (mr_dir_book *)ctx;
if (w) *w = 0;
if (h) *h = 0;
if (!db || index < 0 || index >= db->count) return 0;
char path[MR_PATH_MAX];
mr_dir_book_path(db, index, path, (int)sizeof path);
return mr_probe_size(path, w, h);
}
static meow_img *dir_ops_open(void *ctx, int index)
{
mr_dir_book *db = (mr_dir_book *)ctx;
if (!db || index < 0 || index >= db->count) return NULL;
char path[MR_PATH_MAX];
mr_dir_book_path(db, index, path, (int)sizeof path);
if (!path[0]) return NULL;
return db->load(path, db->ud, db->maxdim);
}
static void dir_ops_close(void *ctx, meow_img *img)
{
mr_dir_book *db = (mr_dir_book *)ctx;
if (db && db->unload && img) db->unload(img);
}
static void dir_ops_destroy(void *ctx)
{
mr_dir_book *db = (mr_dir_book *)ctx;
if (db) mr_dir_book_free(db);
}
static const mr_book_ops g_dir_ops = {
dir_ops_count, dir_ops_meta, dir_ops_open, dir_ops_close, dir_ops_destroy,
};
void mr_dir_book_bind(mr_book *b, mr_dir_book *db)
{
if (!b) return;
b->ops = db ? &g_dir_ops : NULL;
b->ctx = db;
}