Files
MeowOS-SDK/tuku/logic/reader/page_source.c
T
AskaEth 7036020005 图库: 阅读器页槽改 RGB565 常驻,页图内存减半(M32)
- 页槽从 RGBA 改成「已落框的 RGB565 位图」(2B/px):真机 480×853 页 1599KB → 800KB,
  两页常驻 3.2MB → 1.6MB。解码仍在 worker 里产出 RGBA,当场转 565 并释放 RGBA。
- SDK 新增 565 原语(meow_ui_draw.h / meow_ui_gfx.c):meow_gfx_blit_rgb565 命中页每行
  一次 memcpy(单次拷贝、无每帧重采样),缩放兜底走 565 最近邻。量化与 back_buf 用同一个
  rgb565(),565 屏上落下的像素与旧 RGBA 路径逐像素一致(不引入色偏/新增色带)。
- 预算按 565 重算:单页像素上限沿用旧 3MB@RGBA 的同几何口径(=1.5MB@565),
  MR_RESIDENT_CAP = 3 槽 + 1 张在途 = 3.2MB;reader_estimate_peak 新增 convert_bytes
  (落槽暂存 2B/px)与 peak_bytes 一起参与门控,避免实测超预算。
- 结果队列(已解码、未落槽)字节计入总预算(pending_bytes),不再「偷吃」最多 2 页。
- CBZ meta() 改读条目 4KB 前缀(mz_read_prefix;deflate 解到 4KB 即停,输入缓冲走栈),
  500 页排版不再触发 500 次全量解压。
- 测试:B3/B8 的 565 常驻与在途预算断言、CBZ 500 页 stored/deflate 前后对比、
  P12 565 与 RGBA 路径的逐像素颜色对比(量化误差/RMSE/灰阶梯)。
2026-11-06 18:43:11 +08:00

748 lines
30 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 记;但后端支持字段保持「未知(0)」→ 保守回退 stb 门控。 */
int depth = 8, ctype = 2;
unsigned char ex[5];
if (ps_read(s, ex, sizeof ex)) {
depth = ex[0];
ctype = ex[1];
in->depth = ex[0];
in->color = ex[1];
in->compression = ex[2];
in->filter = ex[3];
in->interlace = ex[4];
}
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;
in->sof = m; /* M27:0xC0/0xC1/0xC2 才可能走后端 */
in->depth = so[0]; /* 精度(8 才可能走后端) */
/* 未知采样因子时按 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 tq[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];
tq[i] = comp[2];
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) {
/* M27:给后端支持判定留原始采样因子/量化表号(4 分量已在上面排除)。 */
in->ncomp = nc;
for (int i = 0; i < nc; i++) {
in->comp_h[i] = (unsigned char)(hv[i] >> 4);
in->comp_v[i] = (unsigned char)(hv[i] & 15);
in->comp_tq[i] = (unsigned char)tq[i];
}
/* 分量面字节 = 与 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[8]; /* biPlanes(2) / biBitCount(2) / biCompression(4) */
int bpp = 24;
int have_ex = ps_read(s, ex, sizeof ex);
if (have_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 目标,不另开源缓冲 */
/* M27:后端支持判定(DIB≥40 / BI_RGB / 8·16·24·32bpp)。字段读不全就保持未知 → stb。 */
in->dib = (int)((unsigned)hdr[14] | ((unsigned)hdr[15] << 8) |
((unsigned)hdr[16] << 16) | ((unsigned)hdr[17] << 24));
if (have_ex) {
in->depth = bpp;
in->compression = (int)((unsigned)ex[4] | ((unsigned)ex[5] << 8) |
((unsigned)ex[6] << 16) | ((unsigned)ex[7] << 24));
}
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;
}
/* ============================================================ */
/* 解码峰值模型 + 内存门控(M27:唯一实现) */
/* ------------------------------------------------------------
* 阅读器与页源都只调 reader_estimate_peak()——模型口径、预算策略、门控顺序都只在这里
* 写一份。两个模型:
* 后端(M23):峰值 = 目标图字节 + 后端工作面(JPEG 分量平面 / PNG·BMP 行缓冲)+ 固定开销。
* 与源图分辨率/压缩体积无关;命中即放行(不做内存门控)。
* stb(回退):M22 的真实分配模型(planes + 抽取输出 + 精修/落框临时图)。
* 只有这类页参与 3MB 峰值预算与 MemAvailable < 6MB 门控。
* M32(页槽改 RGB565):解码仍产出 RGBA 的 meow_img,阅读器把它转成 565 落槽再释放 RGBA。
* 这笔「落槽暂存」= 2B/px(转换时 RGBA 与 565 同时存活)单独记在 convert_bytes,与
* peak_bytes 相加才是「一次取页」的真实内存;stb 门控与阅读器日志都按两者之和算。
* 模型本身(RGBA 解码分配)不变——变的只是每页常驻从 4B/px 降到 2B/px。
* ============================================================ */
#define MR_PEAK_CAP_BYTES (3LL * 1024 * 1024) /* 常态单次解码峰值上限 */
#define MR_PEAK_HARD_BYTES (12LL * 1024 * 1024) /* MemFree 富余时的绝对上限 */
#define MR_MEM_FREE_MARGIN_KB (4L * 1024) /* 放宽档:MemFree 需比峰值多这么多 */
#define MR_MEM_FLOOR_KB (6L * 1024) /* 回退路径:MemAvailable 下限 */
#define MR_DECODE_SLACK (64LL * 1024) /* 结构体/行缓冲/分配器开销余量 */
#define MR_BACKEND_SOFT_CAP (2400LL * 1024) /* 与 meow_jpeg.c 的 2.4MB 预算同值 */
#define MR_BACKEND_HARD_CAP (8LL * 1024 * 1024) /* meow_jpeg.c 的 MJ_PEAK_CAP */
long long reader_peak_budget_bytes(long memfree_kb)
{
long long budget = MR_PEAK_CAP_BYTES;
if (memfree_kb >= 0) {
long long room = (memfree_kb - MR_MEM_FREE_MARGIN_KB) * 1024;
if (room > budget) budget = room;
if (budget > MR_PEAK_HARD_BYTES) budget = MR_PEAK_HARD_BYTES;
}
return budget;
}
/* 与 meow_img_target_dims / meow_img_downscale 同一公式(纯 C 层不得依赖 SDK)。 */
static void mr_target_dims(int w, int h, int maxdim, int *tw, int *th)
{
if (w >= h) {
*tw = maxdim;
*th = (int)((long)h * maxdim / w);
} else {
*th = maxdim;
*tw = (int)((long)w * maxdim / h);
}
if (*tw < 1) *tw = 1;
if (*th < 1) *th = 1;
}
static long long mr_ceil_div(long long a, long long b) { return (a + b - 1) / b; }
/* 档位折算:与 meow_img_load_file_step 的 eff 计算逐字一致
* (eff = min(maxdim, ceil(长边/step)),后端按 eff 选 DCT 阶)。 */
static int mr_effective_dim(int longside, int step, int dim)
{
if (step < 1) step = 1;
if (step > 8) step = 8;
int eff = dim;
int cap = (int)mr_ceil_div(longside, step);
if (cap > 0 && cap < eff) eff = cap;
return eff;
}
/* PNG 位深 × 颜色类型 → 每像素位数(与 meow_png_stream.c 的 png_bits_of 同式)。 */
static int mr_png_bits(int color, int depth)
{
switch (color) {
case 0: return depth;
case 2: return depth * 3;
case 3: return depth;
case 4: return depth * 2;
case 6: return depth * 4;
default: return 0;
}
}
/* 位深合法性(与 meow_png_stream.c 的 png_depth_ok **逐字同式**,P2-1):
* color 0/3 允许 1/2/4/8(灰度再加 16),color 2/4/6 只允许 8/16。 */
static int mr_png_depth_ok(int color, int depth)
{
if (color == 0) return depth == 1 || depth == 2 || depth == 4 || depth == 8 || depth == 16;
if (color == 3) return depth == 1 || depth == 2 || depth == 4 || depth == 8;
if (color == 2 || color == 4 || color == 6) return depth == 8 || depth == 16;
return 0;
}
/* 后端支持矩阵(镜像 meow_jpeg.c / meow_png_stream.c 的拒绝条件;字段未知一律不支持)。
* eff 是后端实际会收到的 maxdim(见 mr_effective_dim)。 */
static int mr_backend_supported(const mr_img_info *in, int eff)
{
if (!in || in->w <= 0 || in->h <= 0 || eff <= 0) return 0;
int longside = in->w > in->h ? in->w : in->h;
if (longside <= eff) return 0; /* 小图走旧入口 */
switch (in->codec) {
case MR_IMG_CODEC_JPEG: {
if (in->sof != 0xC0 && in->sof != 0xC1 && in->sof != 0xC2) return 0;
if (in->depth != 8) return 0;
if (in->ncomp < 1 || in->ncomp > 3) return 0;
for (int i = 0; i < in->ncomp; i++) {
if (in->comp_h[i] < 1 || in->comp_h[i] > 4) return 0;
if (in->comp_v[i] < 1 || in->comp_v[i] > 4) return 0;
if (in->comp_tq[i] > 3) return 0;
}
int n = (int)mr_ceil_div(8LL * eff, longside);
if (n < 1) n = 1;
return n <= 7; /* N=8 起几乎没有缩解收益 → stb */
}
case MR_IMG_CODEC_PNG: {
if (in->interlace || in->compression || in->filter) return 0;
if (in->w > 65535 || in->h > 65535) return 0;
if (!mr_png_depth_ok(in->color, in->depth)) return 0;
return 1;
}
case MR_IMG_CODEC_BMP:
if (in->dib < 40 || in->dib > 1024) return 0;
if (in->compression != 0) return 0; /* RLE/BITFIELDS → stb */
return in->depth == 8 || in->depth == 16 || in->depth == 24 || in->depth == 32;
default:
return 0;
}
}
/* JPEG 分量平面字节(与 meow_jpeg.c 的 mj_layout 同式,n[] 是各分量的 DCT 阶)。 */
static long long mr_jpeg_plane_bytes(const mr_img_info *in, const int n[3])
{
int hmax = 1, vmax = 1;
for (int i = 0; i < in->ncomp; i++) {
if (in->comp_h[i] > hmax) hmax = in->comp_h[i];
if (in->comp_v[i] > vmax) vmax = in->comp_v[i];
}
long long mcux = mr_ceil_div(in->w, 8LL * hmax);
long long mcuy = mr_ceil_div(in->h, 8LL * vmax);
long long total = 0;
for (int i = 0; i < in->ncomp; i++) {
long long sw = mr_ceil_div(in->w * (long long)in->comp_h[i], hmax);
long long sh = mr_ceil_div(in->h * (long long)in->comp_v[i], vmax);
long long ow = mr_ceil_div(sw, 8);
long long oh = mr_ceil_div(sh, 8);
long long bx = mcux * in->comp_h[i];
long long by = mcuy * in->comp_v[i];
if (bx < ow) bx = ow;
if (by < oh) by = oh;
total += bx * (long long)n[i] * by * (long long)n[i];
}
return total;
}
/* 后端峰值:目标图 + 工作面 + 固定开销。JPEG 的两步降档(色度减半、2.4MB 预算、
* 8MB 硬上限)与 meow_jpeg.c 一致——估算因此能贴着实测,而不是靠放大余量。 */
static long long mr_backend_peak_bytes(const mr_img_info *in, int eff, int step,
int box_w, int box_h, int *tw, int *th)
{
int dim = box_w > box_h ? box_w : box_h;
mr_target_dims(in->w, in->h, eff, tw, th);
long long target = (long long)*tw * (*th) * 4;
long long work = 0;
if (in->codec == MR_IMG_CODEC_JPEG) {
int longside = in->w > in->h ? in->w : in->h;
int base = (int)mr_ceil_div(8LL * eff, longside);
if (base < 1) base = 1;
int n[3] = { base, base, base };
work = mr_jpeg_plane_bytes(in, n);
/* 4:4:4 色度与亮度等大:亮度平面超过 400KB 时色度降一档(mj_sof 同式)。 */
if (in->ncomp == 3 && in->comp_h[0] == in->comp_h[1] && in->comp_v[0] == in->comp_v[1] &&
work / 3 > 400LL * 1024) {
for (int i = 1; i < 3; i++) if (n[i] > 1) n[i] >>= 1;
work = mr_jpeg_plane_bytes(in, n);
}
/* 平面 + 目标图超 2.4MB:先降色度(亮度保持清晰)。 */
if (work + target > MR_BACKEND_SOFT_CAP) {
int changed = 0;
for (int i = 1; i < 3; i++) if (n[i] > 1) { n[i] >>= 1; changed = 1; }
if (changed) work = mr_jpeg_plane_bytes(in, n);
}
/* 最后一道防线:异常巨图整体降档(最多 N=1)。 */
while (work + target > MR_BACKEND_HARD_CAP && base > 1) {
base >>= 1;
for (int i = 0; i < 3; i++) if (n[i] > base) n[i] = base;
work = mr_jpeg_plane_bytes(in, n);
}
} else if (in->codec == MR_IMG_CODEC_PNG) {
long long rowbytes = (in->w * (long long)mr_png_bits(in->color, in->depth) + 7) / 8;
work = 3 * rowbytes + 1; /* line + prev + cur(png_stream 同式) */
work += 48LL * 1024; /* inflate 状态余量(zlib 窗口等) */
} else { /* BMP */
int bpp = in->depth > 0 ? in->depth : 24;
long long rowbytes = (in->w * (long long)bpp + 31) / 32 * 4;
work = rowbytes; /* 单行 seek 缓冲 */
work += 48LL * 1024; /* 结构体/调色板/stdio 余量 */
}
long long peak = target + work + MR_DECODE_SLACK;
/* 阅读器在「粗档位 + 输出尺寸与显示框不一致」时还会拷一张显示框大小的图;
* 条件与 reader_load_page 逐字对齐,避免把常态误加一倍。 */
if (step > 1 && dim > 0 && (in->w >= box_w || in->h >= box_h) &&
(*tw != box_w || *th != box_h)) {
peak += (long long)box_w * box_h * 4;
}
return peak;
}
/* stb 回退路径峰值(M22 模型 + M27 Adam7 修正):只有这类页才参与预算门控。 */
static long long mr_stb_peak_bytes(const mr_img_info *in, int step, int box_w, int box_h)
{
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 = mr_ceil_div(W, step) * mr_ceil_div(H, step) * 4;
} else {
dst = W * H * 4; /* PNG/BMP:stb 无解码期缩放,整幅落地 */
}
long long peak = in->planes + dst; /* stb 阶段 */
/* Adam7 交错 PNG:stb 额外整幅暂存各 pass 的 raw 与一张中间 RGBA(实测 1600×2400
* 45MB,M22 模型只算 26MB)。多算 planes+dst 保证是上界;这类页无论预算如何都超过
* 12MB 放宽档,结果仍是占位(安全侧)。 */
if (in->codec == MR_IMG_CODEC_PNG && in->interlace)
peak += in->planes + dst;
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;
}
void reader_estimate_peak(const mr_img_info *in, int step, int box_w, int box_h,
long memfree_kb, long memavail_kb, mr_decode_decision *out)
{
if (!out) return;
out->path = MR_DECODE_PATH_STB;
out->skip = MR_DECODE_OK;
out->step = 1;
out->tw = out->th = 0;
out->peak_bytes = 0;
out->convert_bytes = 0;
out->budget_bytes = reader_peak_budget_bytes(memfree_kb);
if (!in || in->w <= 0 || in->h <= 0 || box_w <= 0 || box_h <= 0) return;
if (step < 1) step = 1;
if (step > 8) step = 8;
out->step = step;
int dim = box_w > box_h ? box_w : box_h;
int longside = in->w > in->h ? in->w : in->h;
int eff = mr_effective_dim(longside, step, dim);
/* 1) 先看后端能否接住:命中时峰值与源尺寸无关(典型页 <2.6MB),不做 3MB 常态门控;
* 但仍保留两道安全阀(P2-2):MemAvailable <6MB 直接占位、估算超过 12MB 硬上限占位。
* 后端路由由 reader 用 backend-only 入口执行(P1-1),失败即坏图,绝不回落 stb。 */
if (mr_backend_supported(in, eff)) {
out->path = MR_DECODE_PATH_BACKEND;
out->peak_bytes = mr_backend_peak_bytes(in, eff, step, box_w, box_h,
&out->tw, &out->th);
out->convert_bytes = (long long)out->tw * out->th * 2; /* 565 落槽暂存(M32) */
if (memavail_kb >= 0 && memavail_kb < MR_MEM_FLOOR_KB)
out->skip = MR_DECODE_SKIP_LOWMEM;
else if (out->peak_bytes + out->convert_bytes > MR_PEAK_HARD_BYTES)
out->skip = MR_DECODE_SKIP_PEAK;
else
out->skip = MR_DECODE_OK;
return;
}
/* 2) 回退 stb:只有这里才做 MemAvailable 与峰值预算门控。 */
if (longside <= dim) { out->tw = in->w; out->th = in->h; }
else mr_target_dims(in->w, in->h, dim, &out->tw, &out->th);
out->peak_bytes = mr_stb_peak_bytes(in, step, box_w, box_h);
out->convert_bytes = (long long)out->tw * out->th * 2; /* 565 落槽暂存(M32) */
if (memavail_kb >= 0 && memavail_kb < MR_MEM_FLOOR_KB)
out->skip = MR_DECODE_SKIP_LOWMEM;
else if (out->peak_bytes + out->convert_bytes > out->budget_bytes)
out->skip = MR_DECODE_SKIP_PEAK; /* 565 暂存也占内存:门控按「解码+落槽」总量算 */
else
out->skip = MR_DECODE_OK;
}
/* ============================================================ */
/* 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;
}