Files
MeowOS-SDK/tuku/logic/reader/page_source.h
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

194 lines
10 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.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 */