- 页槽从 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/灰阶梯)。
748 lines
30 KiB
C
748 lines
30 KiB
C
/* 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;
|
||
}
|