真机(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 交叉构建零警告。
194 lines
10 KiB
C
194 lines
10 KiB
C
/* logic/reader/page_source.h — 页源契约(纯 C,不依赖 meow_ui.h)。
|
||
*
|
||
* 一册 = 有序页序列。契约只有四件事:
|
||
* count 总页数
|
||
* meta(i) 页的原始像素尺寸(可选;条漫连续滚动先用它排版,未知时返回 0)
|
||
* open(i) 取第 i 页:解码 + downscale 后返回 meow_img*(调用方拥有,必须 close)
|
||
* close(img) 释放 open 返回的图像
|
||
*
|
||
* 两种消费方式共用同一契约,后续实现零改动:
|
||
* - 左右翻页(本任务):预解码窗口 = 当前 ±1 页,翻页只 blit 窗口内已备好的图;
|
||
* - 条漫连续滚动(M7):先 meta(i) 累加高度排版,再按滚动位置 open(视口±1 页)。
|
||
* zip 册(M9/M11)只需另写一套 mr_book_ops,目录册是第一个实现。
|
||
*
|
||
* meow_img 在 logic 层是前向声明的不透明句柄;解码/释放由 UI 层注入回调
|
||
* (logic 不 include SDK,宿主测试用假回调即可覆盖契约边界)。
|
||
*/
|
||
#ifndef MR_PAGE_SOURCE_H
|
||
#define MR_PAGE_SOURCE_H
|
||
|
||
#include "logic/library.h" /* mr_path_is_image / mr_natural_cmp / MR_PATH_MAX / MR_NAME_MAX */
|
||
#include <stddef.h> /* size_t(mr_probe_size_mem) */
|
||
|
||
#ifdef __cplusplus
|
||
extern "C" {
|
||
#endif
|
||
|
||
struct meow_img;
|
||
typedef struct meow_img meow_img; /* SDK 不透明句柄;logic 层只透传 */
|
||
|
||
#define MR_BOOK_MAX_PAGES 4096 /* 单册页数上限(防坏卡/巨目录撑爆内存) */
|
||
|
||
/* 解码回调(UI 层注入):解码 path,长边 >maxdim 时等比 downscale;失败返回 NULL。
|
||
* 必须可在后台线程调用(宿主测试用假实现)。 */
|
||
typedef meow_img *(*mr_book_load_fn)(const char *path, void *ud, int maxdim);
|
||
/* 释放 open 返回的图像(UI 层注入 meow_img_free;宿主测试用假实现计数)。 */
|
||
typedef void (*mr_book_unload_fn)(meow_img *img);
|
||
|
||
typedef struct mr_book_ops {
|
||
int (*count)(void *ctx);
|
||
/* 页原始尺寸(未 downscale);1=已知,0=未知。可为 NULL。 */
|
||
int (*meta)(void *ctx, int index, int *w, int *h);
|
||
/* 取页:越界/解码失败返回 NULL。返回值的所有权交给调用方。 */
|
||
meow_img *(*open)(void *ctx, int index);
|
||
/* 释放 open 返回的图像;须容忍 NULL。 */
|
||
void (*close)(void *ctx, meow_img *img);
|
||
/* 释放 provider 自身(册内索引等)。可为 NULL。 */
|
||
void (*destroy)(void *ctx);
|
||
} mr_book_ops;
|
||
|
||
typedef struct mr_book {
|
||
const mr_book_ops *ops;
|
||
void *ctx;
|
||
} mr_book;
|
||
|
||
/* ---- 契约包装:所有入口对 NULL/越界安全(越界 open 返回 NULL) ---- */
|
||
int mr_book_valid(const mr_book *b);
|
||
int mr_book_count(const mr_book *b);
|
||
int mr_book_meta(const mr_book *b, int index, int *w, int *h);
|
||
meow_img *mr_book_open(mr_book *b, int index);
|
||
void mr_book_close(mr_book *b, meow_img *img);
|
||
/* 释放 provider 自身(不含已 open 的图像——那些由调用方先 close)。幂等。 */
|
||
void mr_book_destroy(mr_book *b);
|
||
|
||
/* ---- 目录册(文件夹即书):目录直接子文件里的图片按自然序成册 ---- */
|
||
typedef struct mr_dir_book {
|
||
char dir[MR_PATH_MAX];
|
||
char **names; /* 自然序文件名(堆分配) */
|
||
int count;
|
||
int maxdim; /* open 时传给解码回调的长边上限 */
|
||
mr_book_load_fn load;
|
||
mr_book_unload_fn unload;
|
||
void *ud; /* 解码回调的宿主上下文 */
|
||
} mr_dir_book;
|
||
|
||
/* 扫描 dir 的直接子文件,收图片名并自然排序。
|
||
* 返回页数(>=0);参数非法 / 目录不可读 / 内存不足返回 -1(db 已清空、可安全 free)。 */
|
||
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);
|
||
void mr_dir_book_free(mr_dir_book *db);
|
||
/* 第 index 页的完整路径;越界时输出空串。 */
|
||
void mr_dir_book_path(const mr_dir_book *db, int index, char *out, int outsz);
|
||
/* 把目录册绑定到通用 mr_book(db 生命周期由调用方持有)。 */
|
||
void mr_dir_book_bind(mr_book *b, mr_dir_book *db);
|
||
|
||
/* 纯 C 头解析:取图片原始像素尺寸(不解码像素)。
|
||
* PNG(IHDR)/ JPEG(SOF0-15)/ BMP(BITMAPINFOHEADER+)。1=已知,0=未知。 */
|
||
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 当一本书 */
|
||
/* ------------------------------------------------------------
|
||
* 目录册之外的第二种 mr_book 实现,契约完全相同。条目名按 zip 原始字节
|
||
* 透传(通常 UTF-8):只收「图片后缀 + 路径安全 + 非隐藏 + 非目录」的条目,
|
||
* 跨子目录按 mr_natural_cmp 自然排序。open(i) 先把条目解压进
|
||
* logic/reader/page_cache(磁盘 LRU),再用注入的解码回调按路径解码——
|
||
* 与目录册走同一条 UI 解码/预算路径,翻页与条漫两种模式零改动复用。
|
||
*
|
||
* 条目名永远不会被拼成文件系统路径(缓存文件名是哈希),路径安全过滤是
|
||
* 纵深防御:拒绝绝对路径、'..' 段、反斜杠与内嵌 NUL。
|
||
* ============================================================ */
|
||
#define MR_ZIP_MAX_ENTRY_NAME 1024 /* 超长条目名跳过(防病态包) */
|
||
#define MR_ZIP_PAGE_MAX_BYTES (64LL * 1024 * 1024) /* 单页解压上限(防 zip 炸弹) */
|
||
|
||
typedef struct mr_zip_book {
|
||
char path[MR_PATH_MAX];
|
||
void *arch; /* mz_archive*(zip 层句柄;不对外暴露) */
|
||
void *lock; /* pthread_mutex_t*:mz_read* 串行化(open/meta 共用) */
|
||
int *order; /* 页 i → zip 条目下标(堆分配) */
|
||
int *meta_w; /* 页原始宽/高(按需解压头探测;NULL=未探测) */
|
||
int *meta_h;
|
||
int count;
|
||
int maxdim;
|
||
long long mtime; /* 档案 mtime/size:解压缓存键的一部分 */
|
||
long long size;
|
||
mr_book_load_fn load; /* 可为 NULL(只取封面/页路径时不需要解码) */
|
||
mr_book_unload_fn unload;
|
||
void *ud;
|
||
} mr_zip_book;
|
||
|
||
/* 打开档案并建立页表。返回页数(>=0);参数非法/坏档/空档案不可读返回 -1
|
||
* (zb 已清空、可安全 free)。0 页的合法档案返回 0(zb 仍持有打开的 archive)。 */
|
||
int mr_zip_book_init(mr_zip_book *zb, const char *path, int maxdim,
|
||
mr_book_load_fn load, mr_book_unload_fn unload, void *ud);
|
||
void mr_zip_book_free(mr_zip_book *zb);
|
||
/* 把档案册绑定到通用 mr_book(zb 生命周期由调用方持有)。 */
|
||
void mr_zip_book_bind(mr_book *b, mr_zip_book *zb);
|
||
/* 第 index 页在 zip 内的条目名(原始字节);越界输出空串。 */
|
||
void mr_zip_book_page_name(const mr_zip_book *zb, int index, char *out, int outsz);
|
||
/* 确保第 index 页已解压到缓存并返回缓存文件路径;越界/坏档/缓存不可用 -1。 */
|
||
int mr_zip_book_page_path(mr_zip_book *zb, int index, char *out, int outsz);
|
||
|
||
/* 只数页(书库扫描/邻册判断用;不解压、不建缓存)。0=不是档案或没有图片页。 */
|
||
int mr_zip_count_pages(const char *path);
|
||
/* 档案封面:档内自然序第一张图解到缓存,路径写入 out。1=成功。 */
|
||
int mr_zip_cover_path(const char *path, char *out, int outsz);
|
||
/* 条目是否算一页(图片后缀 + 路径安全 + 非隐藏 + 非目录)。1=是。 */
|
||
int mr_zip_entry_is_page(const char *name, unsigned name_len);
|
||
|
||
/* ============================================================ */
|
||
/* 统一开册(目录册 / 档案册) */
|
||
/* ------------------------------------------------------------
|
||
* 书库点入的行既可能是目录也可能是 .cbz/.zip;阅读器只调这里,不关心 provider。
|
||
* 句柄由调用方提供(阅读器用静态变量),生命周期 = 打开到 close_auto。
|
||
* ============================================================ */
|
||
#define MR_BOOK_KIND_NONE 0
|
||
#define MR_BOOK_KIND_DIR 1
|
||
#define MR_BOOK_KIND_ZIP 2
|
||
|
||
typedef struct mr_book_handle {
|
||
int kind;
|
||
union {
|
||
mr_dir_book dir;
|
||
mr_zip_book zip;
|
||
} u;
|
||
} mr_book_handle;
|
||
|
||
/* 按路径自动选 provider 并绑定 b。返回页数(>=0)或 -1(打不开)。
|
||
* 页数为 0 时 b 不绑定(空册),句柄仍可安全 mr_book_close_auto。 */
|
||
int mr_book_open_auto(mr_book *b, mr_book_handle *h, const char *path, int maxdim,
|
||
mr_book_load_fn load, mr_book_unload_fn unload, void *ud);
|
||
/* 释放 provider(先 mr_book_destroy 再释放各自索引);可重复调用。 */
|
||
void mr_book_close_auto(mr_book *b, mr_book_handle *h);
|
||
|
||
#ifdef __cplusplus
|
||
}
|
||
#endif
|
||
#endif /* MR_PAGE_SOURCE_H */
|