- reader.c 头注释与页缓存注释:真机 480×853 页「单页 1599KB → 800KB(−50%); 3 槽常驻 3.2MB → 2.4MB」,不再是「两页 1.6MB」。 - MR_RESIDENT_CAP 注释:典型稳态 3 槽 = 2.4MB(窗口只落 2 页时 1.6MB),上限 3.2MB。 - 无判定/常量/代码改动(git diff 只有注释行)。
2674 lines
104 KiB
C
2674 lines
104 KiB
C
/* ui/reader/reader.c — 阅读器页(ACT_READER,全屏):左右翻页 + 条漫连续滚动。
|
||
*
|
||
* 用户口径:优先流畅度,其次功能。两条阅读路径都不在翻页/滚动热路径上解码:
|
||
* - 后台 pthread 解码窗口(视口 ±1 页,总驻留 ≤ MR_SLOTS 张 downscale 后页图),
|
||
* 主循环只取已备好的图;
|
||
* - 翻页:跟手拖动只 blit 两张已缓存页图,松手后 meow_anim 一次短滑动吸附(150ms);
|
||
* - 条漫:meow_scroll 跟手 track + 松手 fling 惯性,按滚动位置懒解码视口 ±1 页;
|
||
* - 只 flush 受影响区(本页/整条可见区);解码/驻留耗时都 meow_log 打点。
|
||
*
|
||
* 页源:logic/reader/page_source 的 mr_book 契约(count/meta/open/close)。条漫排版用
|
||
* meta() 不解码像素;解码入口走 SDK M12 预算解码 meow_img_load_file_dim(JPEG 解码期缩解,
|
||
* PNG/BMP 整幅解码后降采样——超大头先 meow_img_probe_file 预判,超过预算跳过并告警)。
|
||
*
|
||
* 性能(M17):解码目标 = **排版真正要的显示框**(翻页=fit/zoom;条漫=strip 排版高度,
|
||
* 宽恒为视口宽),不是屏长边,所以翻页/滚动热路径只有原生 blit:不做 meow_ui_img_scaled
|
||
* 的每帧重采样,也不再每帧 malloc 1MB 级临时缓冲。几何变化(fit/缩放/切模式)时按新几何
|
||
* 重解一次;拖动/吸附帧只 invalidate 内容区(顶栏静态不重绘)。
|
||
* 内存(M17 起;M22 按真机实测重定;M27 接真缩解后端;M32 页槽改 RGB565):
|
||
* 槽里常驻的是**已落框的 565 位图**(2B/px),单页上限 1.5MB(旧 RGBA 3MB 的同几何口径)、
|
||
* 整窗常驻上限 3.2MB(3 槽 + 1 张在途,超限淘汰最远页)。真机 480×853 页单页 1599KB → 800KB
|
||
* (−50%);3 槽常驻 3.2MB → 2.4MB。解码仍产出 RGBA,worker 当场转 565 再释放 RGBA;这笔暂存
|
||
* (2B/px)由 reader_estimate_peak 的 convert_bytes 计入预算。解码路径与峰值预算由
|
||
* page_source 的 reader_estimate_peak()
|
||
* 统一判定(唯一实现,本文件不再另写模型):
|
||
* - 真缩解后端(M23)能接住的页:峰值只与目标尺寸相关(≈1.3–2.5MB,与源分辨率无关),
|
||
* 直接放行,不做内存门控;
|
||
* - 后端拒绝的页(CMYK/算术编码 JPEG、Adam7 PNG、RLE/1–4bpp BMP、N≥8 小图…)回退 stb:
|
||
* 按 stb 真实分配模型算峰值,超 3MB 占位 / MemAvailable <6MB 占位。
|
||
* 占位可重试,不是永久失败。余量确实充足(MemFree 富余)时允许放宽到 12MB。
|
||
* 页数增长不会线性吃内存(常驻只与解码窗大小有关)。
|
||
* 方向:一律读 M3 的 mr_reader_get_direction()(默认 RTL),不另立全局。
|
||
*
|
||
* 模式(M7):翻页/条漫是一等公民。阅读 chrome 的图标按钮一键切换;每册的模式与 last page
|
||
* 经 meow_config 持久化(键 reader.mode/page.<册标识>,见 logic/reader.c),下次打开同一册
|
||
* 自动恢复。书库列表是文件管理器模型(M18),不再显示续读角标。
|
||
*
|
||
* 跳页:chrome 内 meow_slider 选目标页,松手后先把目标页解码进缓存再落位(不白屏不闪烁;
|
||
* 2.5s 超时则降级落位并提示)。
|
||
*
|
||
* 册间衔接:末页前进 → 同父目录下一个含图册(书库自然序)的首页;首页后退 → 上一册末页;
|
||
* 无相邻册时停住并轻提示(不误触退出)。条漫在条顶/条底继续拉 MR_BOOK_PULL 像素松手触发。
|
||
*
|
||
* 手势(与 SDK 语义对齐,见 sdk/meow_ui_input.c touch_release):
|
||
* - 左缘(x<24)向右划 = 系统返回(MEOW_SWIPE_LEFT)→ 不消费,app.c page_pop 退出;
|
||
* - 屏底 28px 上滑 = 系统回桌面(MEOW_EV_HOME),SDK 不分发给 App,本页不起手;
|
||
* - 翻页模式:中部横拖 = 跟手翻页;tap 分区方向感知(左/右 1/3 = 上/下一页,中 1/3 =
|
||
* chrome 显隐);双击 = 放大。
|
||
* - 条漫模式:竖向拖动 = 整条滚动(fling 惯性);tap = chrome 显隐;双击/横向滑动不动作。
|
||
*
|
||
* 节拍:app.c 的 on_tick 只转给 library_on_tick(不在本页 scope)。本页用一条常驻
|
||
* meow_anim “ticker”(SDK 主循环每轮 meow_anim_tick)驱动结果收取、跳页落位、续读节流
|
||
* 保存与单击判定超时;翻页动画与 meow_scroll fling 也走同一节拍。
|
||
*
|
||
* 生命周期:on_enter 建状态 + 建 TopAppBar/滑块 + 打开册并恢复模式/页码;on_leave 先落盘
|
||
* 续读状态,再全量释放(停解码线程、释放页图/册索引/条漫排版/滚动会话/控件)。
|
||
*/
|
||
#include "tuku_internal.h"
|
||
#include "ui/reader/reader.h"
|
||
#include "logic/reader/page_source.h"
|
||
#include "logic/reader/page_cache.h"
|
||
#include "logic/reader/nav.h"
|
||
|
||
#include <errno.h>
|
||
#include <pthread.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
|
||
#define MR_SLOTS 3 /* 页图缓存槽上限(M22:4→3 省 ~1.4MB;翻页用 2,条漫视口 2–3) */
|
||
#define MR_TOP_BAR_H 56
|
||
#define MR_SLIDER_H 40
|
||
#define MR_SLIDE_MS 150 /* 松手后一次短滑动吸附 */
|
||
#define MR_BOUNCE_MS 120 /* 未过阈值回弹 */
|
||
#define MR_DOUBLE_MS 240 /* 单击等待窗口(区分双击) */
|
||
#define MR_ZOOM_PCT 200 /* 双击放大倍率 */
|
||
#define MR_TICK_MS 16
|
||
#define MR_PAINT_MS 16 /* 跟手拖动重绘节流(≈60fps 上限) */
|
||
#define MR_DRAG_SLOP 8 /* 小于此位移算点击 */
|
||
/* 缩放倍率软上限(M32:绘制已改 meow_gfx_blit_rgb565_scaled,原地采样不再 malloc 目标尺寸
|
||
* 临时图;这里保留原保守系数——按 4B/px 估「放大后解码目标」的峰值,比 565 口径更严,
|
||
* 行为不回退。>此值逐档降倍率,保内存。 */
|
||
#define MR_ZOOM_TEMP_CAP (6 * 1024 * 1024)
|
||
#define MR_BOOK_PULL 110 /* 条漫到顶/底后再拉这么远,松手进相邻册 */
|
||
#define MR_JUMP_TIMEOUT_MS 2500 /* 目标页迟迟解不出来 → 降级落位并提示 */
|
||
#define MR_CONT_SAVE_MS 1000 /* 续读状态落盘节流 */
|
||
#define MR_TOAST_MS 1400
|
||
/* ---- 内存预算(M22 校准;M27 起模型与门控只在 page_source.c 的 reader_estimate_peak 里;
|
||
* M32 页槽改 RGB565,2 字节/像素,所有 RGBA 时代的字节常量在这里按 565 重算)----
|
||
* 真机参考:MemTotal 56872kB、空闲 RSS 6044kB、杀点 RSS≈15.6MB / MemFree 仅 1.4MB。
|
||
* 页图口径:真机页 480×853 = 409,440px;RGBA 1599KB/页 → RGB565 800KB/页。 */
|
||
#define MR_PREFETCH_MIN_KB (12L * 1024) /* 预取邻居要求的最低 MemAvailable */
|
||
#define MR_SKIP_RETRY_MS 1500 /* 被预算跳过的页:焦点页冷却这么久后自动重试 */
|
||
/* 单页缓存上限:旧口径是 3MB@RGBA = 786432px(480×1638);565 下同几何 = 1.5MB。
|
||
* 保持像素上限不变 → 解码几何/显示清晰度与以前完全一致,只是每像素省 2 字节。 */
|
||
#define MR_PAGE_PX_CAP (786432L)
|
||
#define MR_PAGE_BYTES_CAP (MR_PAGE_PX_CAP * 2) /* 1.5MB:单页 565 缓存上限 */
|
||
#define MR_SLOT_BYTES (800L * 1024) /* 真机典型页(480×853)的 565 字节 */
|
||
/* 常驻总上限 = 3 槽 + 1 张「已解码未落槽」的在途结果 = 4 × 800KB = 3.2MB。
|
||
* 旧口径 3MB 只算槽内 RGBA(3 槽最坏 4.8MB,且在途结果完全没算);565 后把在途结果
|
||
* 一起算进来,真实上限反而收紧。典型稳态 3 槽 = 2.4MB(窗口只落 2 页时 1.6MB),上限 3.2MB。 */
|
||
#define MR_RESIDENT_CAP (4 * MR_SLOT_BYTES)
|
||
#define MR_SLOT_TOL 2 /* 缓存图与显示框允许的像素偏差(≤2 走原生 blit,不重采样) */
|
||
|
||
#define MR_REQ_MAX 8
|
||
#define MR_RES_MAX 2 /* 结果队列上限(M22:4→2);在途结果的字节由 pending_bytes 计入预算 */
|
||
|
||
/* view on_event 的事件号不在公共头(sdk/meow_ui_internal.h §6.4);本页只用到 SWIPE=3。 */
|
||
#define MR_EV_SWIPE 3
|
||
|
||
static mr_page_ctrl g_ctrl;
|
||
static char g_dir[MR_PATH_MAX];
|
||
static int g_start_at = MR_READER_START_RESUME; /* 下次 on_enter 的显式起始页(-1=续读) */
|
||
/* 下次 on_enter 的「按文件名定位起始页」(书库点图;只对目录册生效,open_book 消费后清空)。
|
||
* 用文件名而不是下标:书库列表可能被 MR_LIB_SCAN_MAX 截断,下标不可信。 */
|
||
static char g_start_name[MR_NAME_MAX];
|
||
/* 浏览根(生产=MR_ROOT_DIR,宿主测试可改):册间衔接不得越过它。 */
|
||
static char g_root[MR_PATH_MAX] = MR_ROOT_DIR;
|
||
|
||
static mr_book_handle g_handle; /* 目录册或档案册(M11 统一开册) */
|
||
static mr_book g_book;
|
||
static int g_count; /* 0=空册/不可读 */
|
||
static int g_open_err;
|
||
static uint64_t g_open_retry_at; /* 资源类打开失败(ENOMEM/EMFILE…)的自动重试时刻 */
|
||
static int g_open_retry_used; /* 1=本次进入已用过自动重试(避免反复重试) */
|
||
static int g_page; /* 翻页模式当前页;条漫模式=最近落位页 */
|
||
static int g_mode = MR_MODE_PAGED;
|
||
|
||
static int g_vw, g_vh; /* 内容视口 = 全屏 */
|
||
static int g_fit = MR_FIT_WIDTH;
|
||
static int g_zoom_pct = 100;
|
||
static int g_pan_x, g_pan_y;
|
||
static int g_chrome = 1;
|
||
static meow_widget *g_tb;
|
||
static int g_act_mode = -1; /* TopAppBar 动作下标:模式切换 */
|
||
static int g_act_jump = -1; /* 动作下标:跳页滑块 */
|
||
static int g_act_fit = -1; /* 动作下标:fit 档循环 */
|
||
|
||
/* ---- 条漫:排版表 + 滚动会话 ---- */
|
||
static mr_strip g_strip;
|
||
static int g_strip_ok;
|
||
static int g_woff; /* 条漫滚动偏移(px) */
|
||
static meow_scroll *g_scroll;
|
||
|
||
/* ---- 跳页(chrome 滑块 + 预解码落位) ---- */
|
||
static meow_widget *g_slider;
|
||
static int g_jump_open;
|
||
static mr_jump g_jump;
|
||
|
||
/* ---- 轻提示(册边界/模式切换/跳页) ---- */
|
||
static char g_toast[64];
|
||
static uint64_t g_toast_until;
|
||
static int g_title_page = -1; /* 标题里显示的页(条漫滚动停止后同步) */
|
||
|
||
/* ---- 续读落盘(节流) ---- */
|
||
static int g_cont_dirty;
|
||
static int g_cont_last_page = -1;
|
||
static int g_cont_last_mode = -1;
|
||
static uint64_t g_cont_save_ms;
|
||
|
||
/* ---- 当前解码窗(≤MR_SLOTS 页,按优先级排列) ---- */
|
||
static int g_win_idx[MR_SLOTS];
|
||
static int g_win_n;
|
||
static int g_fail_paint; /* 窗口内出现解码失败页 → tick 重绘错误占位 */
|
||
|
||
/* ---- 页图缓存(≤3 槽,RGB565 常驻)----
|
||
* 槽里存的是**已落框**的 565 位图(2B/px),不是解码器的 RGBA:真机 480×853 页
|
||
* 单页 1599KB → 800KB(−50%),3 槽常驻 3.2MB → 2.4MB。命中页绘制 = 每行一次 memcpy(M32)。 */
|
||
typedef struct page_bmp {
|
||
int w, h;
|
||
uint16_t *px; /* w*h,行优先,RGB565(不透明) */
|
||
} page_bmp;
|
||
|
||
typedef struct page_slot {
|
||
int idx; /* -1=空 */
|
||
page_bmp *bmp; /* 已 downscale/落框的 565 页图 */
|
||
int src_w, src_h; /* = bmp->w/h(绘制与观测的几何口径) */
|
||
int want_w, want_h; /* 本次解码按哪个显示框落的(几何判据,见 slot_ready) */
|
||
} page_slot;
|
||
static page_slot g_slot[MR_SLOTS];
|
||
|
||
/* 解码失败页标记:坏图/无法解析 → 永久显示占位(避免无限重试)。
|
||
* 与「预算跳过」是两回事,见 g_page_skip:后者可重试。 */
|
||
static unsigned char g_page_fail[(MR_BOOK_MAX_PAGES + 7) / 8];
|
||
/* 预算跳过页标记(M22):内存不足 / 单页峰值超预算 → 先不解码,显示「内存不足」占位。
|
||
* 必须在条件改善后可重试:离开再回来(焦点页变化)、或用户点按显式重试、或冷却期后自动重试。
|
||
* M17 的实现把这类页直接标成永久 failed,用户再也看不到——那等于把内存问题变成功能问题。 */
|
||
static unsigned char g_page_skip[(MR_BOOK_MAX_PAGES + 7) / 8];
|
||
static uint64_t g_skip_ms; /* 最近一次预算跳过的时刻(自动重试节流) */
|
||
|
||
/* 每页原始像素尺寸缓存(打开时按 count 分配;0,0=未知)。绘制/排版几何只查这里,
|
||
* 不在每帧去 probe 文件(dir_ops_meta 是 fopen+读头,每帧一次会直接拖垮翻页)。 */
|
||
typedef struct page_meta { int w, h; } page_meta;
|
||
static page_meta *g_meta;
|
||
|
||
/* 本次解码计划(主线程在 queue_page 里按显示框算好,经 page_req 传给 worker;worker 在
|
||
* mr_book_open 前写入这些静态量,解码回调只读它们 → 后台线程不再读 UI 全局几何)。
|
||
* want_* = 显示框(几何,slot_ready 的判据);box_* = 字节上限钳制后的落地尺寸
|
||
* (解码结果会精确重采样到 box_*,所以命中页仍是 1 次 blit)。 */
|
||
typedef struct decode_plan {
|
||
int want_w, want_h;
|
||
int box_w, box_h;
|
||
} decode_plan;
|
||
static decode_plan g_plan;
|
||
static int g_plan_page; /* 本次解码的页号(只用于日志) */
|
||
static int g_decode_skip; /* reader_load_page 的跳过原因(MR_SKIP_*) */
|
||
static volatile int g_decode_count; /* 累计解码次数(观察重解抖动/活锁) */
|
||
|
||
#define MR_SKIP_NONE 0 /* 解出来了 */
|
||
#define MR_SKIP_LOWMEM 1 /* 内存门控拦下(MemAvailable 不足) */
|
||
#define MR_SKIP_PEAK 2 /* 真实峰值超预算,且没有余量放行 */
|
||
#define MR_SKIP_CONVERT 3 /* 565 落槽缓冲分配失败(可重试,不是坏图) */
|
||
|
||
static int page_failed(int idx)
|
||
{
|
||
if (idx < 0 || idx >= MR_BOOK_MAX_PAGES) return 0;
|
||
return (g_page_fail[idx >> 3] >> (idx & 7)) & 1;
|
||
}
|
||
|
||
static void page_mark_failed(int idx)
|
||
{
|
||
if (idx < 0 || idx >= MR_BOOK_MAX_PAGES) return;
|
||
g_page_fail[idx >> 3] |= (unsigned char)(1u << (idx & 7));
|
||
}
|
||
|
||
static int page_skipped(int idx)
|
||
{
|
||
if (idx < 0 || idx >= MR_BOOK_MAX_PAGES) return 0;
|
||
return (g_page_skip[idx >> 3] >> (idx & 7)) & 1;
|
||
}
|
||
|
||
static void page_mark_skipped(int idx)
|
||
{
|
||
if (idx < 0 || idx >= MR_BOOK_MAX_PAGES) return;
|
||
g_page_skip[idx >> 3] |= (unsigned char)(1u << (idx & 7));
|
||
g_skip_ms = meow_ui_time_ms();
|
||
}
|
||
|
||
static void page_skip_clear(int idx)
|
||
{
|
||
if (idx < 0 || idx >= MR_BOOK_MAX_PAGES) return;
|
||
g_page_skip[idx >> 3] &= (unsigned char)~(1u << (idx & 7));
|
||
}
|
||
|
||
/* 该页现在没图、且不会自己再来(永久失败 / 被预算跳过)→ 画占位文字而不是「加载中…」。
|
||
* 两者都是「没图」,但只有 skip 是可重试的(见 maybe_retry_skipped_focus)。 */
|
||
static int page_blocked(int idx)
|
||
{
|
||
return page_failed(idx) || page_skipped(idx);
|
||
}
|
||
|
||
/* 占位文字:坏图是不可恢复的;预算跳过是「内存不足」,点按可重试(任务 2 的可重试语义)。 */
|
||
static const char *page_placeholder(int idx)
|
||
{
|
||
if (page_skipped(idx)) return "内存不足,点按重试";
|
||
if (page_failed(idx)) return "图片无法显示";
|
||
return "加载中…";
|
||
}
|
||
|
||
/* ---- 后台解码队列(请求携带解码几何,结果带回同一条几何) ---- */
|
||
typedef struct page_req {
|
||
int idx;
|
||
unsigned gen;
|
||
int want_w, want_h; /* 按哪个显示框解码(主线程算,slot_ready 判据) */
|
||
int box_w, box_h; /* 落地上限(单页字节上限钳制后的目标尺寸) */
|
||
} page_req;
|
||
typedef struct page_res {
|
||
int idx;
|
||
unsigned gen;
|
||
page_bmp *bmp; /* 已转 565 的页图(NULL = 失败/跳过) */
|
||
long bytes; /* bmp 的字节数:在途结果的预算口径(M32) */
|
||
int fail;
|
||
int skip; /* MR_SKIP_*:非 0 = 预算跳过(可重试,不是坏图) */
|
||
int want_w, want_h;
|
||
} page_res;
|
||
|
||
static pthread_t g_worker;
|
||
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
|
||
static pthread_cond_t g_cv = PTHREAD_COND_INITIALIZER;
|
||
static int g_worker_on, g_stop;
|
||
static unsigned g_gen = 1;
|
||
|
||
static page_req g_req[MR_REQ_MAX];
|
||
static int g_req_head, g_req_tail, g_req_n;
|
||
static page_res g_res[MR_RES_MAX];
|
||
static int g_res_n;
|
||
|
||
/* ---- 节拍/动画 ---- */
|
||
static meow_anim *g_ticker;
|
||
static meow_anim *g_snap; /* 翻页滑动吸附 */
|
||
static meow_anim *g_bounce; /* 未过阈值回弹 */
|
||
static int g_slide_delta; /* 翻页目标增量(动画中) */
|
||
static int g_slide_from; /* 进场页起点(px) */
|
||
static int g_anim_v; /* 进场页当前位移(px) */
|
||
static uint64_t g_flip_t0;
|
||
/* 翻页命中/未命中(观测口,M32 用来验证「命中率不退化」;非产品逻辑):
|
||
* 目标页已在槽 = 预取生效,翻页不用等解码。与 M31 的 tier 机制无关。 */
|
||
static volatile int g_flip_hits;
|
||
static volatile int g_flip_misses;
|
||
static uint64_t g_last_tick;
|
||
static uint64_t g_last_paint;
|
||
static int g_quitting; /* on_leave 期间抑制回调 */
|
||
|
||
/* ---- 跟手拖动 ---- */
|
||
static int g_dragging; /* 1=本次按下正在拖动 */
|
||
static int g_drag_mode; /* 0=无 1=横滑翻页 2=放大平移 3=滑块 4=条漫滚动 */
|
||
static int g_drag_x0, g_drag_y0;
|
||
static int g_drag_off; /* 当前页水平位移(px) */
|
||
static int g_drag_pan_x0, g_drag_pan_y0;
|
||
static int g_touch_moved; /* 本次按下是否已产生 >slop 的位移 */
|
||
static int g_wo_last_y; /* 条漫:上一次 move 的 y */
|
||
static int g_wo_y0; /* 条漫:按下点 y(slop 判定) */
|
||
static int g_wo_pull; /* 条漫:到边后继续拉的累计量(>0 往末页方向) */
|
||
|
||
/* ---- 绘页路径计数(宿主测试观察:翻页热路径应全部是 blit)---- */
|
||
#ifdef MR_READER_HOST_TEST
|
||
static int g_blit_draws;
|
||
static int g_scaled_draws;
|
||
#define NOTE_BLIT() (g_blit_draws++)
|
||
#define NOTE_SCALED() (g_scaled_draws++)
|
||
#else
|
||
#define NOTE_BLIT() ((void)0)
|
||
#define NOTE_SCALED() ((void)0)
|
||
#endif
|
||
|
||
/* ---- 单击延迟(双击判定窗口)---- */
|
||
static int g_tap_pending;
|
||
static int g_tap_x, g_tap_y;
|
||
static uint64_t g_tap_ms;
|
||
static uint64_t g_last_tap_ms; /* 上一次点击(跨按下保持,双击判定用) */
|
||
static int g_last_tap_x, g_last_tap_y;
|
||
|
||
/* ---- 前向声明(互相调用:册切换/模式/落位) ---- */
|
||
static int open_book(const char *dir, int start_page, int mode);
|
||
static int book_switch(int delta);
|
||
static void mode_set(int mode, int from_page);
|
||
static void toast(const char *msg);
|
||
static void slider_place(void);
|
||
static void update_title(void);
|
||
static void invalidate_content(void);
|
||
static page_slot *slot_find(int idx);
|
||
static void cont_touch(void);
|
||
static void cont_save_now(void);
|
||
static int current_page(void);
|
||
|
||
/* ============================================================ */
|
||
/* 内存读数与观测(M22) */
|
||
/* ------------------------------------------------------------
|
||
* /proc/meminfo 与 /proc/self/status 是唯一可用的实时内存事实来源。读不到(宿主/非 Linux)
|
||
* 一律返回 -1,调用方必须按「未知」处理,不许猜。宿主测试用 mr_reader_ui_test_set_mem()
|
||
* 注入固定读数,让内存门控/放宽档可以确定性验证。
|
||
* ============================================================ */
|
||
static long g_mem_free_override = -1; /* >=0 = 宿主测试注入的 MemFree(kB) */
|
||
static long g_mem_avail_override = -1; /* >=0 = 宿主测试注入的 MemAvailable(kB) */
|
||
|
||
static long mem_kb(const char *path, const char *key)
|
||
{
|
||
FILE *f = fopen(path, "r");
|
||
if (!f) return -1;
|
||
char line[128];
|
||
long v = -1;
|
||
size_t kl = strlen(key);
|
||
while (fgets(line, sizeof line, f)) {
|
||
if (strncmp(line, key, kl) == 0) { v = strtol(line + kl, NULL, 10); break; }
|
||
}
|
||
fclose(f);
|
||
return v;
|
||
}
|
||
|
||
/* MemFree(kB)。真机杀点就是「MemFree 见底」:15,600kB 那次只剩 1,404kB。 */
|
||
static long mem_free_kb(void)
|
||
{
|
||
if (g_mem_free_override >= 0) return g_mem_free_override;
|
||
return mem_kb("/proc/meminfo", "MemFree:");
|
||
}
|
||
|
||
/* MemAvailable(kB):含可回收页缓存。注意本机在 MemFree 1.4MB 时仍有 ~26MB,
|
||
* 所以它只能当「系统极度缺页」的门控,不能当解码配额(内核不会等回收就杀进程)。 */
|
||
static long mem_avail_kb(void)
|
||
{
|
||
if (g_mem_avail_override >= 0) return g_mem_avail_override;
|
||
return mem_kb("/proc/meminfo", "MemAvailable:");
|
||
}
|
||
|
||
/* ---- 565 页图:转换 / 释放 / 字节口径(M32) ----
|
||
* 解码后立刻转成 565 并释放 RGBA:转换期间 RGBA(4B/px)+ 565(2B/px)同时存在,
|
||
* 这笔暂存由 reader_estimate_peak 的 convert_bytes 计入预算(见 page_source.h)。
|
||
* 失败时也会释放 RGBA(调用方不必再 free),返回 NULL 按「可重试的预算跳过」处理。 */
|
||
static page_bmp *page_bmp_from_img(meow_img *im)
|
||
{
|
||
if (!im) return NULL;
|
||
int w = meow_img_w(im), h = meow_img_h(im);
|
||
const unsigned char *rgba = meow_img_pixels(im);
|
||
page_bmp *b = NULL;
|
||
if (rgba && w > 0 && h > 0) {
|
||
b = (page_bmp *)malloc(sizeof *b);
|
||
uint16_t *px = b ? (uint16_t *)malloc((size_t)w * (size_t)h * sizeof *px) : NULL;
|
||
if (b && px) {
|
||
meow_gfx_rgba_to_rgb565(rgba, px, w * h);
|
||
b->w = w;
|
||
b->h = h;
|
||
b->px = px;
|
||
} else {
|
||
free(px);
|
||
free(b);
|
||
b = NULL;
|
||
}
|
||
}
|
||
meow_img_free(im);
|
||
return b;
|
||
}
|
||
|
||
static void page_bmp_free(page_bmp *b)
|
||
{
|
||
if (!b) return;
|
||
free(b->px);
|
||
free(b);
|
||
}
|
||
|
||
static long page_bmp_bytes(const page_bmp *b)
|
||
{
|
||
return b ? (long)b->w * b->h * 2 : 0;
|
||
}
|
||
|
||
static int resident_pages(void)
|
||
{
|
||
int n = 0;
|
||
for (int i = 0; i < MR_SLOTS; i++)
|
||
if (g_slot[i].bmp) n++;
|
||
return n;
|
||
}
|
||
|
||
static long resident_bytes(void)
|
||
{
|
||
long b = 0;
|
||
for (int i = 0; i < MR_SLOTS; i++)
|
||
b += page_bmp_bytes(g_slot[i].bmp);
|
||
return b;
|
||
}
|
||
|
||
/* 结果队列里「已解码、还没落槽」的 565 字节(M32:这些图也是真实内存)。
|
||
* 只读主线程路径用它做预算;worker 在 g_lock 内写队列,故取快照也在锁内。 */
|
||
static long pending_bytes(void)
|
||
{
|
||
long b = 0;
|
||
pthread_mutex_lock(&g_lock);
|
||
for (int i = 0; i < g_res_n; i++) b += g_res[i].bytes;
|
||
pthread_mutex_unlock(&g_lock);
|
||
return b;
|
||
}
|
||
|
||
/* 进出阅读器各打一行(任务 5):RSS 看当前占用,VmHWM 看本次进入的峰值——
|
||
* 验收就是「离开时的 hwm 不超过预算」。img 列 = 槽内 565 字节;pending 未落槽的在途图。 */
|
||
static void log_mem(const char *tag)
|
||
{
|
||
meow_log(MEOW_LOG_INFO,
|
||
"reader mem[%s]: rss=%ldkB hwm=%ldkB memfree=%ldkB memavail=%ldkB pages=%d img=%ldkB pending=%ldkB",
|
||
tag, mem_kb("/proc/self/status", "VmRSS:"),
|
||
mem_kb("/proc/self/status", "VmHWM:"), mem_free_kb(), mem_avail_kb(),
|
||
resident_pages(), resident_bytes() / 1024, pending_bytes() / 1024);
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 页源解码回调(注入 logic 层的 mr_book 契约;可在后台线程调用) */
|
||
/* ============================================================ */
|
||
/* 由显示框(want_w×want_h)反推解码长边:默认就解到显示框大小(解码后可直接 blit),
|
||
* 只有单页缓存字节上限(MR_PAGE_BYTES_CAP,565 口径 = 2B/px)是硬上界——超长条漫在这里按比例收缩。
|
||
* 不再钳到屏长边:条漫页常见长宽比 1.6~1.8,显示高度本来就 > 屏长边,钳了必然每帧重采样。
|
||
* M32 评审 P1:这里必须按 565 的 2B/px 算——乘 4 会把真机 480×853 页压到 359×639
|
||
* (每帧重采样),等于把整单的收益吃掉。 */
|
||
static int plan_decode_long(int want_w, int want_h)
|
||
{
|
||
int l0 = want_w > want_h ? want_w : want_h;
|
||
int s0 = want_w > want_h ? want_h : want_w;
|
||
if (l0 <= 0) return 0;
|
||
int l = l0 < 64 ? 64 : l0;
|
||
while (l > 64) {
|
||
int s = (int)((long long)s0 * l / l0);
|
||
if ((long long)l * s * 2 <= MR_PAGE_BYTES_CAP) break;
|
||
l = l * 3 / 4;
|
||
}
|
||
return l;
|
||
}
|
||
|
||
/* 显示框 → 实际落地尺寸(保持长宽比,受单页字节上限钳制)。
|
||
* 正常情况下 box == want(解码结果就是显示尺寸 → 单次 blit);只有超大屏/超长条漫
|
||
* 才会收缩,此时绘制走缩放路径(M17 已知代价,不再是常态)。 */
|
||
static void plan_box_size(int want_w, int want_h, int *bw, int *bh)
|
||
{
|
||
int w = want_w > 0 ? want_w : g_vw;
|
||
int h = want_h > 0 ? want_h : g_vh;
|
||
if (w <= 0 || h <= 0) { w = h = 1; }
|
||
int long0 = w > h ? w : h;
|
||
int l = plan_decode_long(w, h);
|
||
if (l > 0 && l < long0) {
|
||
if (w >= h) { h = (int)((long long)h * l / w); w = l; }
|
||
else { w = (int)((long long)w * l / h); h = l; }
|
||
if (w < 1) w = 1;
|
||
if (h < 1) h = 1;
|
||
}
|
||
*bw = w;
|
||
*bh = h;
|
||
}
|
||
|
||
/* 固定字段的解码日志:page/src/peak/path 之后是结论,便于 `grep 'reader decode'`
|
||
* 后按列看曲线。peak = 本次「取页」预算口径 = reader_estimate_peak 的解码峰值
|
||
* + 落槽 565 暂存(M32:ok/fail/skip 三种结论同一列,便于和 --wrap malloc 实测对照);
|
||
* path = 实际解码路径(backend = M23 真缩解后端,stb = 回退旧路径)。 */
|
||
static void log_decode(int page, int src_w, int src_h, long long peak_bytes,
|
||
long memfree, long memavail, const char *verdict, int step,
|
||
int backend, unsigned long long ms)
|
||
{
|
||
meow_log(MEOW_LOG_INFO,
|
||
"reader decode page=%d src=%dx%d peak=%lldkB memfree=%ldkB memavail=%ldkB %s path=%s step=%d ms=%llu",
|
||
page, src_w, src_h, peak_bytes / 1024, memfree, memavail, verdict,
|
||
backend ? "backend" : "stb", step, ms);
|
||
}
|
||
|
||
static meow_img *reader_load_page(const char *path, void *ud, int maxdim)
|
||
{
|
||
(void)ud;
|
||
g_decode_skip = MR_SKIP_NONE;
|
||
uint64_t t0 = meow_ui_time_ms();
|
||
if (!path || !path[0]) return NULL;
|
||
|
||
/* 解码计划由 queue_page() 在主线程算好(显示框 + 落地上限);worker/主线程同步兜底
|
||
* 路径在 mr_book_open 前写入。无计划时退回旧预算 maxdim。 */
|
||
int want_w = g_plan.want_w, want_h = g_plan.want_h;
|
||
int box_w = g_plan.box_w, box_h = g_plan.box_h;
|
||
int page = g_plan_page;
|
||
if (box_w <= 0 || box_h <= 0) {
|
||
if (maxdim > 0) {
|
||
box_w = box_h = maxdim;
|
||
} else {
|
||
box_w = want_w > 0 ? want_w : 1;
|
||
box_h = want_h > 0 ? want_h : 1;
|
||
}
|
||
}
|
||
int dim = box_w > box_h ? box_w : box_h;
|
||
|
||
long mfree = mem_free_kb();
|
||
long mavail = mem_avail_kb();
|
||
|
||
/* 1) 头部探测(零像素内存),再交给 page_source 的唯一模型定径:
|
||
* reader_estimate_peak 同时给出「走后端还是 stb」与「是否占位」。后端能接住的页
|
||
* (M23,峰值与源尺寸无关)直接放行;只有回退 stb 的页才参与峰值预算/MemAvailable
|
||
* 门控。模型不再在本文件另写一份。 */
|
||
mr_img_info in;
|
||
int have = mr_probe_info(path, &in);
|
||
mr_decode_decision dec;
|
||
memset(&dec, 0, sizeof dec);
|
||
dec.step = 1;
|
||
if (have) {
|
||
int step = 1;
|
||
if (in.codec == MR_IMG_CODEC_JPEG) {
|
||
int l0 = in.w > in.h ? in.w : in.h;
|
||
step = meow_img_pick_scale(l0, dim);
|
||
}
|
||
for (;;) {
|
||
reader_estimate_peak(&in, step, box_w, box_h, mfree, mavail, &dec);
|
||
/* 后端命中 / 已进预算 / 已到最粗档位:定案。非 JPEG 无档位可调。 */
|
||
if (dec.path == MR_DECODE_PATH_BACKEND || dec.skip == MR_DECODE_OK ||
|
||
step >= 8 || in.codec != MR_IMG_CODEC_JPEG)
|
||
break;
|
||
step *= 2;
|
||
}
|
||
if (dec.skip != MR_DECODE_OK) {
|
||
g_decode_skip = dec.skip == MR_DECODE_SKIP_LOWMEM ? MR_SKIP_LOWMEM : MR_SKIP_PEAK;
|
||
log_decode(page, in.w, in.h, dec.peak_bytes + dec.convert_bytes, mfree, mavail,
|
||
dec.skip == MR_DECODE_SKIP_LOWMEM ? "skip=lowmem" : "skip=peak",
|
||
dec.step, dec.path == MR_DECODE_PATH_BACKEND,
|
||
(unsigned long long)(meow_ui_time_ms() - t0));
|
||
return NULL;
|
||
}
|
||
}
|
||
|
||
/* 2) 解码(路径/档位已定)。失败与「跳过」必须分开:NULL + skip==0 才是坏图。
|
||
* P1-1:门控按后端放行的页必须用 backend-only 入口——后端中途失败(截断/坏流/
|
||
* DHT 超限/malloc 失败)直接返回 NULL 按坏图占位,绝不静默回落 stb(实测会放出
|
||
* 18–36MB)。只有判定为 stb 的页才允许 stb。 */
|
||
int allow_stb = !have || dec.path != MR_DECODE_PATH_BACKEND;
|
||
int used_backend = 0;
|
||
meow_img *im = have ? meow_img_load_file_step_ex(path, dim, dec.step, allow_stb,
|
||
&used_backend)
|
||
: meow_img_load_file_dim(path, dim);
|
||
if (!im) {
|
||
/* peak 列 = 本次「取页」口径:解码峰值 + 落槽 565 暂存(与 ok/skip 同一列口径)。 */
|
||
log_decode(page, have ? in.w : 0, have ? in.h : 0,
|
||
dec.peak_bytes + dec.convert_bytes, mfree, mavail, "fail",
|
||
dec.step, have && dec.path == MR_DECODE_PATH_BACKEND,
|
||
(unsigned long long)(meow_ui_time_ms() - t0));
|
||
return NULL;
|
||
}
|
||
/* 兜底:非 JPEG 或粗档位输出没被缩到上限内,再降一次(保持驻留纪律)。 */
|
||
if (dim > 0 && (meow_img_w(im) > dim || meow_img_h(im) > dim)) {
|
||
meow_img *small = meow_img_downscale(im, dim);
|
||
if (small) { meow_img_free(im); im = small; }
|
||
}
|
||
/* 3) 精确落到显示框(仅当「故意用了粗档位」时):结果 = box → 命中页仍是一次原生 blit,
|
||
* 不会把内存换来的降级变成每帧重采样(M17 的热路径纪律)。
|
||
* 源图本身就小于显示框(小图/缩略图)时绝不放大:那会凭空多占常驻字节,
|
||
* 且 M17 的 slot_ready 语义依赖「SDK 不做上采样」。 */
|
||
if (have && dec.step > 1 && (in.w >= box_w || in.h >= box_h) &&
|
||
(meow_img_w(im) != box_w || meow_img_h(im) != box_h)) {
|
||
meow_img *fit = meow_img_resample(im, box_w, box_h);
|
||
if (fit) { meow_img_free(im); im = fit; }
|
||
}
|
||
log_decode(page, have ? in.w : 0, have ? in.h : 0,
|
||
dec.peak_bytes + dec.convert_bytes, mfree, mavail, "ok",
|
||
dec.step, used_backend,
|
||
(unsigned long long)(meow_ui_time_ms() - t0));
|
||
return im;
|
||
}
|
||
|
||
static void reader_unload_page(meow_img *img)
|
||
{
|
||
meow_img_free(img);
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 页原始尺寸缓存(绘制几何的唯一来源;每页只 probe 一次) */
|
||
/* ============================================================ */
|
||
static int page_meta_dims(int idx, int *w, int *h)
|
||
{
|
||
if (w) *w = 0;
|
||
if (h) *h = 0;
|
||
if (idx < 0 || idx >= g_count) return 0;
|
||
if (g_meta) {
|
||
if (g_meta[idx].w > 0 && g_meta[idx].h > 0) {
|
||
if (w) *w = g_meta[idx].w;
|
||
if (h) *h = g_meta[idx].h;
|
||
return 1;
|
||
}
|
||
if (g_meta[idx].w < 0) return 0; /* 探测过且未知:别再每帧 fopen 一遍 */
|
||
}
|
||
int sw = 0, sh = 0;
|
||
if (!mr_book_meta(&g_book, idx, &sw, &sh) || sw <= 0 || sh <= 0) {
|
||
if (g_meta) { g_meta[idx].w = -1; g_meta[idx].h = 0; }
|
||
return 0;
|
||
}
|
||
if (g_meta) { g_meta[idx].w = sw; g_meta[idx].h = sh; }
|
||
if (w) *w = sw;
|
||
if (h) *h = sh;
|
||
return 1;
|
||
}
|
||
|
||
/* 显示框(fit + zoom):绘图与点击几何都走这里。未知名按 2:3 竖图占位。 */
|
||
static void page_target_size_at(int idx, int *w, int *h)
|
||
{
|
||
int sw = 0, sh = 0;
|
||
if (w) *w = 0;
|
||
if (h) *h = 0;
|
||
if (!w || !h) return;
|
||
if (!page_meta_dims(idx, &sw, &sh)) {
|
||
/* meta 不可用(未知/坏头):用已解码尺寸兜底(保持真实长宽比),
|
||
* 都没解出来才按 2:3 竖图占位——排版稳定且不会把页面拉变形。 */
|
||
page_slot *s = slot_find(idx);
|
||
if (s) { sw = s->src_w; sh = s->src_h; }
|
||
if (sw <= 0 || sh <= 0) {
|
||
sw = g_vw > 0 ? g_vw : 1;
|
||
sh = sw * 3 / 2;
|
||
}
|
||
}
|
||
if (mr_nav_fit_size(g_fit, sw, sh, g_vw, g_vh, w, h) != 0) return;
|
||
if (g_zoom_pct != 100) mr_nav_zoom_size(*w, *h, g_zoom_pct, w, h);
|
||
}
|
||
|
||
/* 解码/绘制的目标显示框:条漫 = strip 排版几何(宽恒为视口宽、高由排版表定),
|
||
* 翻页 = fit/zoom。解码计划与「缓存是否就绪」必须用同一个函数:否则解出来的尺寸
|
||
* 和排版要的尺寸不同源,条漫长页(长宽比 > 屏高/屏宽)就每帧重采样(M17 评审 P1-1)。 */
|
||
static void page_geometry_at(int idx, int *w, int *h)
|
||
{
|
||
if (w) *w = 0;
|
||
if (h) *h = 0;
|
||
if (!w || !h) return;
|
||
if (g_mode == MR_MODE_WEBTOON && g_strip_ok) {
|
||
int ph = mr_strip_page_h(&g_strip, idx);
|
||
if (ph > 0) { *w = g_vw; *h = ph; return; }
|
||
}
|
||
page_target_size_at(idx, w, h);
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 续读状态(逐册 last page + mode;写入节流,避免滚动时频繁写卡) */
|
||
/* ============================================================ */
|
||
static int current_page(void)
|
||
{
|
||
if (g_count <= 0) return 0;
|
||
if (g_mode == MR_MODE_WEBTOON && g_strip_ok)
|
||
return mr_strip_page_at(&g_strip, g_woff);
|
||
return g_page;
|
||
}
|
||
|
||
static void cont_save_now(void)
|
||
{
|
||
if (!g_dir[0] || g_count <= 0) { g_cont_dirty = 0; return; }
|
||
int p = current_page();
|
||
if (p == g_cont_last_page && g_mode == g_cont_last_mode) { g_cont_dirty = 0; return; }
|
||
mr_cont_save(g_dir, p, g_mode);
|
||
g_cont_last_page = p;
|
||
g_cont_last_mode = g_mode;
|
||
g_cont_dirty = 0;
|
||
meow_log(MEOW_LOG_INFO, "reader cont save page=%d/%d mode=%s",
|
||
p + 1, g_count, mr_mode_name(g_mode));
|
||
}
|
||
|
||
static void cont_touch(void)
|
||
{
|
||
g_cont_dirty = 1;
|
||
}
|
||
|
||
/* 只在滚动/翻页停下且距上次写盘超过 MR_CONT_SAVE_MS 时落盘。 */
|
||
static void cont_save_throttled(void)
|
||
{
|
||
if (!g_cont_dirty) return;
|
||
if (g_scroll && meow_scroll_active(g_scroll)) return;
|
||
if (g_dragging) return;
|
||
uint64_t now = meow_ui_time_ms();
|
||
if (now - g_cont_save_ms < MR_CONT_SAVE_MS) return;
|
||
g_cont_save_ms = now;
|
||
cont_save_now();
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 页图缓存 */
|
||
/* ============================================================ */
|
||
static int focus_page(void)
|
||
{
|
||
if (g_count <= 0) return -1;
|
||
if (g_mode == MR_MODE_WEBTOON && g_strip_ok)
|
||
return mr_strip_page_at(&g_strip, g_woff + g_vh / 2); /* 视口中心页 */
|
||
return g_page;
|
||
}
|
||
|
||
static int in_window(int idx)
|
||
{
|
||
if (g_count <= 0 || idx < 0 || idx >= g_count) return 0;
|
||
for (int i = 0; i < g_win_n; i++)
|
||
if (g_win_idx[i] == idx) return 1;
|
||
return 0;
|
||
}
|
||
|
||
/* 该页此刻是否真的画在屏幕上(异步落位后只重绘这种情况)。 */
|
||
static int page_visible(int idx)
|
||
{
|
||
if (g_count <= 0 || idx < 0 || idx >= g_count) return 0;
|
||
if (g_mode == MR_MODE_WEBTOON && g_strip_ok) {
|
||
int f = mr_strip_page_at(&g_strip, g_woff);
|
||
int l = mr_strip_page_at(&g_strip, g_woff + g_vh - 1);
|
||
return idx >= f && idx <= l;
|
||
}
|
||
return idx == g_page || (g_slide_delta != 0 && idx == g_page + g_slide_delta);
|
||
}
|
||
|
||
static page_slot *slot_find(int idx)
|
||
{
|
||
if (idx < 0) return NULL;
|
||
for (int i = 0; i < MR_SLOTS; i++)
|
||
if (g_slot[i].idx == idx && g_slot[i].bmp) return &g_slot[i];
|
||
return NULL;
|
||
}
|
||
|
||
static void slot_clear(int i)
|
||
{
|
||
if (i < 0 || i >= MR_SLOTS) return;
|
||
page_bmp_free(g_slot[i].bmp);
|
||
g_slot[i].bmp = NULL;
|
||
g_slot[i].idx = -1;
|
||
g_slot[i].src_w = g_slot[i].src_h = 0;
|
||
g_slot[i].want_w = g_slot[i].want_h = 0;
|
||
}
|
||
|
||
static void slots_free_all(void)
|
||
{
|
||
for (int i = 0; i < MR_SLOTS; i++) slot_clear(i);
|
||
}
|
||
|
||
/* 缓存图是否按「当前显示框」解出来的:比较 slot 记录的 want 几何与当前几何。
|
||
* 用几何(而不是 src 像素尺寸)判定,才能同时满足三件事:
|
||
* - fit/zoom/模式变化 → 几何变 → 重解一次(放大再缩回、适高→条漫都能回到纯 blit);
|
||
* - 解码被单页字节上限钳住(超长条漫/大屏)→ want 不变 → 不再反复重解(无活锁);
|
||
* - 源图本身小于显示框(SDK 不做上采样)→ want 不变 → 不会每 tick 重排。 */
|
||
static int slot_ready(int idx)
|
||
{
|
||
page_slot *s = slot_find(idx);
|
||
if (!s) return 0;
|
||
int w = 0, h = 0;
|
||
page_geometry_at(idx, &w, &h);
|
||
if (w <= 0 || h <= 0) return 1;
|
||
if (s->want_w <= 0 || s->want_h <= 0) return 0;
|
||
return abs(w - s->want_w) <= MR_SLOT_TOL && abs(h - s->want_h) <= MR_SLOT_TOL;
|
||
}
|
||
|
||
/* 淘汰离焦点页最远的一张(不淘汰 focus、可见页与 except)。返回被清掉的槽下标,无则 -1。
|
||
* M22:可见页不淘汰——条漫视口可能同时露出 2–3 页,按距离淘汰会把屏幕上的页拆掉,
|
||
* 结果是「一页闪占位 → 重解码 → 再被拆」的抖动。宁可临时超一点字节,也不让可见页消失。 */
|
||
static int slot_evict_far(int except)
|
||
{
|
||
int focus = focus_page();
|
||
int far = -1, far_d = -1;
|
||
for (int i = 0; i < MR_SLOTS; i++) {
|
||
if (i == except || !g_slot[i].bmp) continue;
|
||
if (g_slot[i].idx == focus) continue;
|
||
if (page_visible(g_slot[i].idx)) continue;
|
||
int d = abs(g_slot[i].idx - focus);
|
||
if (d > far_d) { far_d = d; far = i; }
|
||
}
|
||
if (far >= 0) slot_clear(far);
|
||
return far;
|
||
}
|
||
|
||
/* 开新页前先腾地方(任务 3):按「即将落地的单页字节」预判,先淘汰非可见/非焦点的旧图,
|
||
* 让解码缓冲不与陈旧常驻叠加。只在逼近上限时才动手,避免把预取邻居立刻扔掉
|
||
*(那样翻页会从「命中」退化成「重新解码」,反而增加峰值次数)。
|
||
* M32:已解码未落槽的在途结果(pending_bytes)也是真实内存,一起算进常驻上限。 */
|
||
static void slots_reserve(long need_bytes)
|
||
{
|
||
if (need_bytes <= 0) return;
|
||
long pending = pending_bytes();
|
||
while (resident_bytes() + pending + need_bytes > MR_RESIDENT_CAP) {
|
||
long before = resident_bytes();
|
||
if (slot_evict_far(-1) < 0) return; /* 只剩可见页:放行,下一次 slot_put 再收敛 */
|
||
if (resident_bytes() >= before) return; /* 没腾出东西(无可淘汰页):别死循环 */
|
||
}
|
||
}
|
||
|
||
/* 放进缓存;同页旧图直接替换(fit/zoom/模式变了要换尺寸)。返回 1=已放入。
|
||
* want_w/want_h = 这次解码按哪个显示框落的,交给 slot_ready 做几何判据。
|
||
* bmp 的所有权移交本槽(失败时在这里释放)。 */
|
||
static int slot_put(int idx, page_bmp *bmp, int want_w, int want_h)
|
||
{
|
||
if (!bmp || idx < 0) { page_bmp_free(bmp); return 0; }
|
||
int i = -1, empty = -1;
|
||
for (int k = 0; k < MR_SLOTS; k++) {
|
||
if (g_slot[k].idx == idx && g_slot[k].bmp) { i = k; break; }
|
||
if (!g_slot[k].bmp && empty < 0) empty = k;
|
||
}
|
||
if (i < 0) {
|
||
if (empty >= 0) {
|
||
i = empty;
|
||
} else { /* 全满:替换离焦点页最远、且不可见的一张 */
|
||
int focus = focus_page(), far_d = -1;
|
||
for (int k = 0; k < MR_SLOTS; k++) {
|
||
if (page_visible(g_slot[k].idx)) continue;
|
||
int d = abs(g_slot[k].idx - focus);
|
||
if (d > far_d) { far_d = d; i = k; }
|
||
}
|
||
if (i < 0) i = g_slot[0].bmp ? 0 : -1; /* 全可见:只能挤掉第一槽(极端条漫页) */
|
||
}
|
||
}
|
||
if (i < 0) { page_bmp_free(bmp); return 0; }
|
||
|
||
/* 总字节上限:新图放进去前先淘汰最远页,保证常驻 + 在途不超过预算。 */
|
||
long need = page_bmp_bytes(bmp);
|
||
long pending = pending_bytes();
|
||
for (;;) {
|
||
long held = resident_bytes() + pending;
|
||
if (g_slot[i].bmp) held -= page_bmp_bytes(g_slot[i].bmp);
|
||
if (held + need <= MR_RESIDENT_CAP) break;
|
||
long before = resident_bytes();
|
||
if (slot_evict_far(i) < 0) break; /* 只剩目标槽:这一页必须放进来 */
|
||
if (resident_bytes() >= before) break; /* 没腾出东西:收手,别死循环 */
|
||
}
|
||
|
||
slot_clear(i);
|
||
g_slot[i].idx = idx;
|
||
g_slot[i].bmp = bmp;
|
||
g_slot[i].src_w = bmp->w;
|
||
g_slot[i].src_h = bmp->h;
|
||
g_slot[i].want_w = want_w;
|
||
g_slot[i].want_h = want_h;
|
||
return 1;
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 解码窗计算(≤MR_SLOTS 页,按优先级:跳页目标 > 焦点 > 视口 ±1) */
|
||
/* ============================================================ */
|
||
static void win_reset(void)
|
||
{
|
||
g_win_n = 0;
|
||
}
|
||
|
||
static void win_add(int idx)
|
||
{
|
||
if (idx < 0 || idx >= g_count || g_win_n >= MR_SLOTS) return;
|
||
for (int i = 0; i < g_win_n; i++)
|
||
if (g_win_idx[i] == idx) return;
|
||
g_win_idx[g_win_n++] = idx;
|
||
}
|
||
|
||
static void window_compute(void)
|
||
{
|
||
win_reset();
|
||
if (g_count <= 0) return;
|
||
|
||
int jt = mr_jump_target(&g_jump);
|
||
if (jt >= 0) win_add(jt); /* 跳页目标优先,否则会被窗口淘汰 */
|
||
|
||
int focus = focus_page();
|
||
win_add(focus);
|
||
|
||
if (g_mode == MR_MODE_WEBTOON && g_strip_ok) {
|
||
int f = 0, l = 0;
|
||
int budget = MR_SLOTS - g_win_n; /* 可见±1;剩余槽位给视口 */
|
||
if (budget < 1) budget = 1;
|
||
mr_strip_window(&g_strip, g_woff, budget, &f, &l);
|
||
for (int i = f; i <= l; i++) win_add(i);
|
||
/* 视口窗口可能因预算被截:再补焦点两侧各一页,保证滚动方向有前瞻 */
|
||
for (int d = 1; d <= MR_SLOTS; d++) {
|
||
win_add(focus - d);
|
||
win_add(focus + d);
|
||
}
|
||
} else if (g_zoom_pct > 100) {
|
||
win_add(g_page); /* 放大:只驻留当前页,给缩放临时缓冲让内存 */
|
||
} else {
|
||
win_add(g_page - 1);
|
||
win_add(g_page + 1);
|
||
}
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 后台解码 */
|
||
/* ============================================================ */
|
||
static int req_queued(int idx)
|
||
{
|
||
for (int i = 0; i < g_req_n; i++) {
|
||
int p = (g_req_head + i) % MR_REQ_MAX;
|
||
if (g_req[p].idx == idx) return 1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* 预取闸门(任务 4):拖动/惯性期间只解落点页,不做邻居预取;且只在内存确实富裕时预取。
|
||
* MemAvailable 读不到按「不预取」处理(保守)。 */
|
||
static int prefetch_allowed(void)
|
||
{
|
||
if (g_dragging) return 0;
|
||
if (g_scroll && meow_scroll_active(g_scroll)) return 0;
|
||
long a = mem_avail_kb();
|
||
if (a < 0) return 0;
|
||
return a >= MR_PREFETCH_MIN_KB;
|
||
}
|
||
|
||
static void queue_page(int idx)
|
||
{
|
||
if (!mr_book_valid(&g_book) || idx < 0 || idx >= g_count) return;
|
||
/* 永久失败与「仍在跳过期」的页都不排;后者由 maybe_retry_skipped_focus 解封。 */
|
||
if (slot_ready(idx) || page_blocked(idx)) return;
|
||
/* 几何在这里(主线程)算好塞进请求:worker 不再读 g_fit/g_zoom/g_vw/g_vh。 */
|
||
int want_w = 0, want_h = 0;
|
||
page_geometry_at(idx, &want_w, &want_h);
|
||
if (want_w <= 0 || want_h <= 0) { want_w = g_vw; want_h = g_vh; }
|
||
int box_w = 0, box_h = 0;
|
||
plan_box_size(want_w, want_h, &box_w, &box_h);
|
||
/* 先释放再解码:按这一页的落地字节(565)先淘汰不可见旧图,别让常驻与解码缓冲叠加。
|
||
* slots_reserve 内部按「槽内 + 在途结果」对新页做预算(M32:在途结果也算内存)。 */
|
||
slots_reserve((long)box_w * box_h * 2);
|
||
pthread_mutex_lock(&g_lock);
|
||
if (!g_stop && !req_queued(idx) && g_req_n < MR_REQ_MAX) {
|
||
page_req *r = &g_req[g_req_tail];
|
||
r->idx = idx;
|
||
r->gen = g_gen;
|
||
r->want_w = want_w;
|
||
r->want_h = want_h;
|
||
r->box_w = box_w;
|
||
r->box_h = box_h;
|
||
g_req_tail = (g_req_tail + 1) % MR_REQ_MAX;
|
||
g_req_n++;
|
||
pthread_cond_signal(&g_cv);
|
||
}
|
||
pthread_mutex_unlock(&g_lock);
|
||
}
|
||
|
||
static void *reader_worker(void *arg)
|
||
{
|
||
(void)arg;
|
||
for (;;) {
|
||
page_req req;
|
||
pthread_mutex_lock(&g_lock);
|
||
while (!g_stop && g_req_n == 0) pthread_cond_wait(&g_cv, &g_lock);
|
||
if (g_stop) { pthread_mutex_unlock(&g_lock); break; }
|
||
req = g_req[g_req_head];
|
||
g_req_head = (g_req_head + 1) % MR_REQ_MAX;
|
||
g_req_n--;
|
||
/* 取消(任务 4 / M20 P0-3):出队后、mr_book_open 前二次判过期——旧册(gen 变)
|
||
* 或已经不在解码窗(拖动/快翻的中间页)直接丢弃。M20 实测拖动翻页原本 1.75× 解码,
|
||
* 全是这种在飞不可取消的请求。锁内判,窗口也在锁内写,避免共享状态竞争。 */
|
||
int stale = (req.gen != g_gen) || !in_window(req.idx);
|
||
pthread_mutex_unlock(&g_lock);
|
||
if (stale) {
|
||
meow_log(MEOW_LOG_INFO,
|
||
"reader decode page=%d src=0x0 peak=0kB memfree=%ldkB memavail=%ldkB skip=cancel step=0",
|
||
req.idx, mem_free_kb(), mem_avail_kb());
|
||
continue;
|
||
}
|
||
|
||
/* 解码计划:本线程私有(单 worker),解码回调只读这几个量。 */
|
||
g_plan.want_w = req.want_w;
|
||
g_plan.want_h = req.want_h;
|
||
g_plan.box_w = req.box_w;
|
||
g_plan.box_h = req.box_h;
|
||
g_plan_page = req.idx;
|
||
g_decode_skip = MR_SKIP_NONE;
|
||
meow_img *img = mr_book_open(&g_book, req.idx);
|
||
g_decode_count++;
|
||
/* 565 落槽(M32):解码结果只用来生成槽里的 565 位图,RGBA 当场释放。
|
||
* 放在 worker 里做——主线程只收一张已经能直接 blit 的图,翻页不等转换。 */
|
||
int had_img = img != NULL;
|
||
page_bmp *bmp = had_img ? page_bmp_from_img(img) : NULL; /* 接管并释放 RGBA */
|
||
img = NULL;
|
||
if (had_img && !bmp) {
|
||
meow_log(MEOW_LOG_WARN, "reader decode page=%d 565-convert failed (oom)", req.idx);
|
||
}
|
||
int skip = bmp ? MR_SKIP_NONE : (had_img ? MR_SKIP_CONVERT : g_decode_skip);
|
||
|
||
pthread_mutex_lock(&g_lock);
|
||
if (!g_stop && g_res_n < MR_RES_MAX) {
|
||
g_res[g_res_n].idx = req.idx;
|
||
g_res[g_res_n].gen = req.gen;
|
||
g_res[g_res_n].bmp = bmp;
|
||
g_res[g_res_n].bytes = page_bmp_bytes(bmp);
|
||
g_res[g_res_n].fail = (bmp || skip) ? 0 : 1;
|
||
g_res[g_res_n].skip = skip;
|
||
g_res[g_res_n].want_w = req.want_w;
|
||
g_res[g_res_n].want_h = req.want_h;
|
||
g_res_n++;
|
||
bmp = NULL; /* 所有权已移交(失败/跳过时本就是 NULL) */
|
||
}
|
||
pthread_mutex_unlock(&g_lock);
|
||
page_bmp_free(bmp);
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
static void worker_start(void)
|
||
{
|
||
if (g_worker_on) return;
|
||
g_stop = 0;
|
||
if (pthread_create(&g_worker, NULL, reader_worker, NULL) == 0) g_worker_on = 1;
|
||
}
|
||
|
||
static void worker_stop(void)
|
||
{
|
||
if (!g_worker_on) return;
|
||
pthread_mutex_lock(&g_lock);
|
||
g_stop = 1;
|
||
pthread_cond_broadcast(&g_cv);
|
||
pthread_mutex_unlock(&g_lock);
|
||
pthread_join(g_worker, NULL);
|
||
g_worker_on = 0;
|
||
|
||
pthread_mutex_lock(&g_lock);
|
||
for (int i = 0; i < g_res_n; i++) page_bmp_free(g_res[i].bmp);
|
||
g_res_n = 0;
|
||
g_req_n = 0;
|
||
g_req_head = g_req_tail = 0;
|
||
pthread_mutex_unlock(&g_lock);
|
||
}
|
||
|
||
/* 丢弃窗口外的排队请求(主线程调用)。 */
|
||
static void queue_prune(void)
|
||
{
|
||
pthread_mutex_lock(&g_lock);
|
||
page_req keep[MR_REQ_MAX];
|
||
int n = 0;
|
||
for (int i = 0; i < g_req_n; i++) {
|
||
page_req r = g_req[(g_req_head + i) % MR_REQ_MAX];
|
||
if (r.gen != g_gen || !in_window(r.idx)) continue;
|
||
/* 几何已变的请求作废(否则会按旧显示框解出来又被判过期)。 */
|
||
int w = 0, h = 0;
|
||
page_geometry_at(r.idx, &w, &h);
|
||
if (abs(w - r.want_w) > MR_SLOT_TOL || abs(h - r.want_h) > MR_SLOT_TOL) continue;
|
||
keep[n++] = r;
|
||
}
|
||
for (int i = 0; i < n; i++) g_req[i] = keep[i];
|
||
g_req_head = 0;
|
||
g_req_tail = n;
|
||
g_req_n = n;
|
||
pthread_mutex_unlock(&g_lock);
|
||
}
|
||
|
||
/* 收取后台结果:只留窗口内新页。返回 1=有图落位。
|
||
* 结果分三种:ok(落槽)/ fail(坏图,永久占位)/ skip(预算跳过,可重试占位)。 */
|
||
static int results_drain(void)
|
||
{
|
||
page_res got[MR_RES_MAX];
|
||
int n;
|
||
pthread_mutex_lock(&g_lock);
|
||
n = g_res_n;
|
||
for (int i = 0; i < n; i++) got[i] = g_res[i];
|
||
g_res_n = 0;
|
||
pthread_mutex_unlock(&g_lock);
|
||
|
||
int changed = 0;
|
||
for (int i = 0; i < n; i++) {
|
||
if (got[i].gen != g_gen) { page_bmp_free(got[i].bmp); continue; }
|
||
if (got[i].skip) {
|
||
/* 预算跳过(内存不足/峰值超限/565 转换失败):可重试,不写永久失败位。 */
|
||
page_mark_skipped(got[i].idx);
|
||
if (page_visible(got[i].idx)) g_fail_paint = 1;
|
||
continue;
|
||
}
|
||
if (got[i].fail) {
|
||
page_mark_failed(got[i].idx);
|
||
if (page_visible(got[i].idx)) g_fail_paint = 1;
|
||
continue;
|
||
}
|
||
if (in_window(got[i].idx)) {
|
||
if (slot_put(got[i].idx, got[i].bmp, got[i].want_w, got[i].want_h) &&
|
||
page_visible(got[i].idx)) changed = 1;
|
||
page_skip_clear(got[i].idx); /* 解出来了:撤销跳过标记 */
|
||
} else {
|
||
page_bmp_free(got[i].bmp);
|
||
}
|
||
}
|
||
return changed;
|
||
}
|
||
|
||
/* 预算跳过的页如何重试(任务 2 的可重试语义):只对**焦点页**重试,且冷却 MR_SKIP_RETRY_MS,
|
||
* 这样「内存暂时不够」不会变成永久占位,也不会变成每 tick 一次的重试风暴。
|
||
* force=1 是用户显式重试(点按占位页)。 */
|
||
static void maybe_retry_skipped_focus(int force)
|
||
{
|
||
int f = focus_page();
|
||
if (f < 0 || !page_skipped(f) || page_failed(f)) return;
|
||
if (!force) {
|
||
uint64_t now = meow_ui_time_ms();
|
||
if (now - g_skip_ms < MR_SKIP_RETRY_MS) return;
|
||
}
|
||
page_skip_clear(f);
|
||
g_skip_ms = meow_ui_time_ms();
|
||
meow_log(MEOW_LOG_INFO, "reader decode retry page=%d%s", f + 1, force ? " (tap)" : "");
|
||
}
|
||
|
||
/* 窗口随视口/当前页移动:淘汰窗口外图 → 清掉窗口外请求 → 按优先级补页。
|
||
* 窗口本身在锁内写(worker 的取消判据要读它)。 */
|
||
static void window_update(void)
|
||
{
|
||
pthread_mutex_lock(&g_lock);
|
||
window_compute();
|
||
pthread_mutex_unlock(&g_lock);
|
||
/* 不主动清窗口外的图:多留一页「回看缓存」,避免 0→1→0 来回翻时反复重解同一页。
|
||
* 容量仍由 MR_SLOTS 与 slot_put 的字节淘汰兜底(最多 MR_SLOTS 张)。 */
|
||
queue_prune();
|
||
/* 回到某页 → 允许重试这一页(「离开再回来」的可重试语义)。 */
|
||
maybe_retry_skipped_focus(0);
|
||
/* 队列第一位 = 跳页目标或焦点页(必解);其余是预取,按内存闸门决定做不做。 */
|
||
int prefetch = prefetch_allowed();
|
||
for (int i = 0; i < g_win_n; i++) {
|
||
if (i > 0 && !prefetch) continue;
|
||
queue_page(g_win_idx[i]);
|
||
}
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 条漫排版 + 滚动会话 */
|
||
/* ============================================================ */
|
||
static int strip_meta_cb(void *ctx, int index, int *w, int *h)
|
||
{
|
||
(void)ctx;
|
||
return page_meta_dims(index, w, h); /* 走缓存:重排版不再逐页 fopen probe */
|
||
}
|
||
|
||
static int strip_ensure(void)
|
||
{
|
||
if (g_strip_ok && g_strip.count == g_count &&
|
||
g_strip.vw == g_vw && g_strip.vh == g_vh)
|
||
return 1;
|
||
mr_strip_free(&g_strip);
|
||
g_strip_ok = 0;
|
||
if (g_count <= 0) return 0;
|
||
uint64_t t0 = meow_ui_time_ms();
|
||
if (mr_strip_build(&g_strip, g_count, g_vw, g_vh, strip_meta_cb, &g_book) != 0) {
|
||
meow_log(MEOW_LOG_WARN, "reader strip layout failed pages=%d", g_count);
|
||
return 0;
|
||
}
|
||
g_strip_ok = 1;
|
||
meow_log(MEOW_LOG_INFO, "reader strip layout pages=%d total=%d ms=%llu",
|
||
g_count, mr_strip_total(&g_strip),
|
||
(unsigned long long)(meow_ui_time_ms() - t0));
|
||
return 1;
|
||
}
|
||
|
||
static int wo_max(void *ctx)
|
||
{
|
||
(void)ctx;
|
||
return mr_strip_max_offset(&g_strip);
|
||
}
|
||
|
||
static int wo_get(void *ctx)
|
||
{
|
||
(void)ctx;
|
||
return g_woff;
|
||
}
|
||
|
||
static void wo_set(void *ctx, int off)
|
||
{
|
||
(void)ctx;
|
||
int m = mr_strip_max_offset(&g_strip);
|
||
if (off < 0) off = 0;
|
||
if (off > m) off = m;
|
||
if (off == g_woff) return;
|
||
g_woff = off;
|
||
window_update();
|
||
cont_touch();
|
||
/* 重绘由调用方节流:拖动走 drag_paint(),fling 走 wo_redraw()。 */
|
||
}
|
||
|
||
static void wo_redraw(void *ctx)
|
||
{
|
||
(void)ctx;
|
||
invalidate_content(); /* 滚动只动内容区,顶栏静态不重绘 */
|
||
}
|
||
|
||
static void scroll_ensure(void)
|
||
{
|
||
if (g_scroll) return;
|
||
g_scroll = meow_scroll_create(wo_set, wo_max, wo_get, wo_redraw, NULL);
|
||
if (!g_scroll) meow_log(MEOW_LOG_WARN, "reader scroll session unavailable (pool full)");
|
||
}
|
||
|
||
static void scroll_free(void)
|
||
{
|
||
if (!g_scroll) return;
|
||
meow_scroll_free(g_scroll);
|
||
g_scroll = NULL;
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 布局/绘制 */
|
||
/* ============================================================ */
|
||
/* 内容视口失效区:chrome 可见时顶栏是静态的,拖动/吸附帧只刷其下内容,
|
||
* 避免每帧重绘顶栏文字(软渲下文字是最贵的绘制物)。 */
|
||
static void invalidate_content(void)
|
||
{
|
||
if (g_chrome)
|
||
meow_ui_invalidate(0, MR_TOP_BAR_H, g_vw, g_vh - MR_TOP_BAR_H);
|
||
else
|
||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||
}
|
||
|
||
static void current_geom(int *w, int *h)
|
||
{
|
||
page_target_size_at(g_page, w, h);
|
||
if (!w || !h || *w <= 0 || *h <= 0) {
|
||
if (w) *w = g_vw;
|
||
if (h) *h = g_vh;
|
||
}
|
||
}
|
||
|
||
static void clamp_pan(void)
|
||
{
|
||
int w, h;
|
||
current_geom(&w, &h);
|
||
mr_nav_pan_clamp(w, h, g_vw, g_vh, &g_pan_x, &g_pan_y);
|
||
}
|
||
|
||
/* 内容是否超出视口(原始档的方图/宽图即使不放大也需要平移)。 */
|
||
/* 横向是否真的溢出(横向平移才有意义)。适宽长图只会在**竖向**溢出:那属于「页面比
|
||
* 屏幕略高」,不能因此把左右 tap / 横拖从翻页改成平移——否则 1.5 以上长宽比的页
|
||
* 在适宽档完全翻不动(水平平移量本就是 0,点击变空操作)。 */
|
||
static int content_h_overflow(void)
|
||
{
|
||
int w = 0, h = 0;
|
||
current_geom(&w, &h);
|
||
return w > g_vw;
|
||
}
|
||
|
||
/* 平移模式:放大、横向溢出(原始档宽图 / 适高宽图)、或原始档任意溢出。
|
||
* 此时单指拖动=平移,左右 tap=平移步进。
|
||
* 适宽/适高的**竖向单独溢出**不算(适宽长图常见:1.6 比例页在 480x720 上高 768);
|
||
* 若把它算成平移,左右 tap / 横拖会变成「水平平移 0px」的空操作 —— 页面直接翻不动。 */
|
||
static int pan_mode(void)
|
||
{
|
||
if (g_zoom_pct > 100) return 1;
|
||
if (content_h_overflow()) return 1;
|
||
if (g_fit == MR_FIT_ORIGINAL) {
|
||
int w = 0, h = 0;
|
||
current_geom(&w, &h);
|
||
return h > g_vh;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* 画一页(带水平位移);页图尺寸==显示尺寸时走原生 565 blit(翻页热路径零重采样、
|
||
* 每行一次 memcpy),只在 fit/zoom 变化或极端长页(超单页缓存上限)时才退回最近邻缩放。 */
|
||
static void draw_page_at(int idx, int off_x, int off_y)
|
||
{
|
||
page_slot *s = slot_find(idx);
|
||
if (!s) return;
|
||
int w, h;
|
||
page_target_size_at(idx, &w, &h);
|
||
if (w <= 0 || h <= 0) return;
|
||
int x = (g_vw - w) / 2 + g_pan_x + off_x;
|
||
int y = (g_vh - h) / 2 + g_pan_y + off_y;
|
||
if (abs(w - s->src_w) <= MR_SLOT_TOL && abs(h - s->src_h) <= MR_SLOT_TOL) {
|
||
meow_gfx_blit_rgb565(x + (w - s->src_w) / 2, y + (h - s->src_h) / 2,
|
||
s->src_w, s->src_h, s->bmp->px);
|
||
NOTE_BLIT();
|
||
} else {
|
||
meow_gfx_blit_rgb565_scaled(x, y, w, h, s->src_w, s->src_h, s->bmp->px);
|
||
NOTE_SCALED();
|
||
}
|
||
}
|
||
|
||
/* 条漫:按滚动偏移画可见页。典型 2:3 页的页图尺寸与显示尺寸相等 → 原生 565 blit;
|
||
* 其余按最近邻缩放(gfx 内直接采样,不再申请目标尺寸临时缓冲)。 */
|
||
static void draw_strip(const meow_theme *th)
|
||
{
|
||
if (!g_strip_ok) {
|
||
meow_ui_text_center(g_vw / 2, g_vh / 2, "正在排版…", th->font_body, th->onSurfaceVariant);
|
||
return;
|
||
}
|
||
int first = mr_strip_page_at(&g_strip, g_woff);
|
||
int last = mr_strip_page_at(&g_strip, g_woff + g_vh - 1);
|
||
for (int i = first; i <= last; i++) {
|
||
int y = mr_strip_top(&g_strip, i) - g_woff;
|
||
int h = mr_strip_page_h(&g_strip, i);
|
||
if (h <= 0) continue;
|
||
page_slot *s = slot_find(i);
|
||
if (s) {
|
||
/* 页图=显示尺寸时原生 blit:gfx 只处理屏幕上可见的行,
|
||
* 长条页只露一截时不再整页重采样(M7 finding 的子矩形 blit 诉求)。 */
|
||
if (abs(s->src_w - g_vw) <= MR_SLOT_TOL && abs(s->src_h - h) <= MR_SLOT_TOL) {
|
||
meow_gfx_blit_rgb565((g_vw - s->src_w) / 2, y, s->src_w, s->src_h, s->bmp->px);
|
||
NOTE_BLIT();
|
||
} else {
|
||
meow_gfx_blit_rgb565_scaled(0, y, g_vw, h, s->src_w, s->src_h, s->bmp->px);
|
||
NOTE_SCALED();
|
||
}
|
||
continue;
|
||
}
|
||
/* 未就绪:只填可见部分(长页不整块清屏),给一帧占位而不是白屏。 */
|
||
int y0 = y < 0 ? 0 : y;
|
||
int y1 = y + h;
|
||
if (y1 > g_vh) y1 = g_vh;
|
||
if (y1 <= y0) continue;
|
||
meow_ui_fill_rect(0, y0, g_vw, y1 - y0, th->surfaceContainer);
|
||
if (y1 - y0 >= 48)
|
||
meow_ui_text_center(g_vw / 2, y0 + (y1 - y0) / 2 - 10,
|
||
page_placeholder(i),
|
||
th->font_body, th->onSurfaceVariant);
|
||
}
|
||
}
|
||
|
||
static void draw_toast(const meow_theme *th)
|
||
{
|
||
if (!g_toast[0] || meow_ui_time_ms() >= g_toast_until) return;
|
||
int tw = meow_ui_text_width(g_toast, th->font_body) + 40;
|
||
if (tw > g_vw - 16) tw = g_vw - 16;
|
||
int x = (g_vw - tw) / 2;
|
||
int y = g_vh - 120;
|
||
meow_ui_fill_rect_alpha(x, y, tw, 44, th->surfaceContainerHighest, 235);
|
||
meow_ui_text_center(g_vw / 2, y + 12, g_toast, th->font_body, th->onSurface);
|
||
}
|
||
|
||
static void reader_draw(meow_view *v, const meow_theme *th, void *ctx)
|
||
{
|
||
(void)v; (void)ctx;
|
||
int w = g_vw, h = g_vh;
|
||
|
||
if (g_open_err) {
|
||
meow_ui_text_center(w / 2, h / 2 - 20, "无法打开此册", th->font_title, th->onSurface);
|
||
meow_ui_text_center(w / 2, h / 2 + 24, g_dir, th->font_body, th->onSurfaceVariant);
|
||
return;
|
||
}
|
||
if (g_count <= 0) {
|
||
meow_ui_text_center(w / 2, h / 2 - 20, "空册", th->font_title, th->onSurface);
|
||
meow_ui_text_center(w / 2, h / 2 + 24, "这个文件夹里没有图片", th->font_body,
|
||
th->onSurfaceVariant);
|
||
return;
|
||
}
|
||
|
||
if (g_mode == MR_MODE_WEBTOON) {
|
||
draw_strip(th);
|
||
draw_toast(th);
|
||
return;
|
||
}
|
||
|
||
if (g_slide_delta != 0) {
|
||
/* 动画帧:进场页从 from 滑到 0,当前页同步让位(只 blit 两张已缓存页图)。 */
|
||
draw_page_at(g_page, g_anim_v - g_slide_from, 0);
|
||
draw_page_at(g_page + g_slide_delta, g_anim_v, 0);
|
||
draw_toast(th);
|
||
return;
|
||
}
|
||
|
||
if (g_drag_mode == 1 && g_drag_off != 0) {
|
||
/* 跟手:当前页按位移走,相邻页在另一侧同时露出(未就绪就只画当前页)。 */
|
||
int rtl = mr_reader_get_direction() == MR_DIR_RTL;
|
||
int delta = mr_nav_swipe_delta(rtl, g_drag_off);
|
||
int target = mr_nav_clamp_page(g_page + delta, g_count);
|
||
if (target != g_page && slot_find(target)) {
|
||
int from = mr_nav_slide_from(rtl, delta, g_vw);
|
||
draw_page_at(target, g_drag_off + from, 0);
|
||
}
|
||
draw_page_at(g_page, g_drag_off, 0);
|
||
draw_toast(th);
|
||
return;
|
||
}
|
||
|
||
draw_page_at(g_page, 0, 0);
|
||
if (!slot_find(g_page)) {
|
||
meow_ui_text_center(w / 2, h / 2,
|
||
page_placeholder(g_page),
|
||
th->font_body, th->onSurfaceVariant);
|
||
}
|
||
draw_toast(th);
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 状态动作 */
|
||
/* ============================================================ */
|
||
static void update_title(void)
|
||
{
|
||
if (!g_tb) return;
|
||
static const char *const fit_name[MR_FIT_COUNT] = { "适宽", "适高", "原始" };
|
||
const char *fit = (g_fit >= 0 && g_fit < MR_FIT_COUNT) ? fit_name[g_fit] : "适宽";
|
||
char buf[64];
|
||
if (g_count > 0) {
|
||
if (g_mode == MR_MODE_WEBTOON)
|
||
snprintf(buf, sizeof buf, "%d / %d 条漫", current_page() + 1, g_count);
|
||
else
|
||
snprintf(buf, sizeof buf, "%d / %d 翻页 %s", g_page + 1, g_count, fit);
|
||
} else {
|
||
snprintf(buf, sizeof buf, "0 / 0");
|
||
}
|
||
meow_topappbar_set_title(g_tb, buf);
|
||
g_title_page = (g_count > 0) ? current_page() : -1;
|
||
}
|
||
|
||
static void toast(const char *msg)
|
||
{
|
||
snprintf(g_toast, sizeof g_toast, "%s", msg ? msg : "");
|
||
g_toast_until = meow_ui_time_ms() + MR_TOAST_MS;
|
||
meow_ui_invalidate(0, g_vh - 140, g_vw, 140);
|
||
}
|
||
|
||
static void slider_place(void)
|
||
{
|
||
if (!g_slider) return;
|
||
int visible = g_chrome && g_jump_open && g_count > 1;
|
||
if (visible)
|
||
meow_widget_set_rect(g_slider, 24, MR_TOP_BAR_H + 4, g_vw - 48, MR_SLIDER_H);
|
||
else
|
||
meow_widget_set_rect(g_slider, 0, -(MR_TOP_BAR_H + MR_SLIDER_H + 8),
|
||
g_vw - 48, MR_SLIDER_H);
|
||
}
|
||
|
||
static void slider_build(void)
|
||
{
|
||
if (g_slider) { meow_widget_free(g_slider); g_slider = NULL; }
|
||
if (g_count <= 1) return;
|
||
int val = current_page() + 1;
|
||
if (val < 1) val = 1;
|
||
if (val > g_count) val = g_count;
|
||
g_slider = meow_slider_create(0, 0, g_vw - 48, 1, g_count, val);
|
||
slider_place();
|
||
}
|
||
|
||
static void slider_sync(void)
|
||
{
|
||
if (!g_slider || g_count <= 0) return;
|
||
int v = current_page() + 1;
|
||
if (v < 1) v = 1;
|
||
if (v > g_count) v = g_count;
|
||
meow_slider_set(g_slider, v);
|
||
}
|
||
|
||
static void set_fit(int fit)
|
||
{
|
||
if (g_mode == MR_MODE_WEBTOON) {
|
||
toast("条漫固定适宽");
|
||
return;
|
||
}
|
||
g_fit = (fit < 0 || fit >= MR_FIT_COUNT) ? MR_FIT_WIDTH : fit;
|
||
g_zoom_pct = 100;
|
||
g_pan_x = g_pan_y = 0;
|
||
window_update();
|
||
update_title();
|
||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||
}
|
||
|
||
static void set_chrome(int on)
|
||
{
|
||
on = on ? 1 : 0;
|
||
if (g_chrome == on) return;
|
||
g_chrome = on;
|
||
if (g_tb) meow_widget_set_rect(g_tb, 0, on ? 0 : -MR_TOP_BAR_H, g_vw, MR_TOP_BAR_H);
|
||
slider_place();
|
||
meow_ui_invalidate(0, 0, g_vw, MR_TOP_BAR_H + MR_SLIDER_H + 8);
|
||
}
|
||
|
||
/* 平移(dx>0 = 内容右移);放大或原始档溢出时可用。 */
|
||
static void pan_by(int dx, int dy)
|
||
{
|
||
if (!pan_mode()) return;
|
||
g_pan_x += dx;
|
||
g_pan_y += dy;
|
||
clamp_pan();
|
||
invalidate_content();
|
||
}
|
||
|
||
static void commit_page(int target)
|
||
{
|
||
target = mr_nav_clamp_page(target, g_count);
|
||
if (target < 0) return;
|
||
g_page = target;
|
||
g_zoom_pct = 100;
|
||
g_pan_x = g_pan_y = 0;
|
||
window_update();
|
||
update_title();
|
||
slider_sync();
|
||
cont_touch();
|
||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||
}
|
||
|
||
/* ---- 翻页滑动吸附 ---- */
|
||
static void slide_update(meow_anim *a, float value, void *ctx)
|
||
{
|
||
(void)a; (void)ctx;
|
||
g_anim_v = (int)value;
|
||
invalidate_content(); /* 吸附帧只刷内容区(整屏 minus 静态顶栏) */
|
||
}
|
||
|
||
static void slide_done(meow_anim *a, int canceled, void *ctx)
|
||
{
|
||
(void)a; (void)ctx;
|
||
g_snap = NULL;
|
||
if (g_quitting) return;
|
||
int delta = g_slide_delta;
|
||
g_slide_delta = 0;
|
||
g_slide_from = 0;
|
||
g_anim_v = 0;
|
||
if (!canceled && delta)
|
||
commit_page(g_page + delta);
|
||
else
|
||
invalidate_content();
|
||
meow_log(MEOW_LOG_INFO, "reader flip done page=%d/%d total_ms=%llu",
|
||
g_page + 1, g_count,
|
||
(unsigned long long)(meow_ui_time_ms() - g_flip_t0));
|
||
}
|
||
|
||
/* 从 start_off(松手时的位移,无跟手时为 0)滑到目标页。1=动画已启动。 */
|
||
static int start_slide(int delta, int start_off)
|
||
{
|
||
if (g_snap || g_bounce) return 0;
|
||
int rtl = mr_reader_get_direction() == MR_DIR_RTL;
|
||
g_slide_delta = delta;
|
||
g_slide_from = mr_nav_slide_from(rtl, delta, g_vw);
|
||
g_anim_v = start_off + g_slide_from;
|
||
meow_anim_desc d = {
|
||
(float)g_anim_v, 0.0f, MR_SLIDE_MS, MEOW_EASE_OUT,
|
||
slide_update, slide_done, NULL,
|
||
};
|
||
uint64_t t0 = meow_ui_time_ms();
|
||
g_snap = meow_anim_start(&d); /* start 即回调 update:首帧在这里绘制 */
|
||
uint64_t dt = meow_ui_time_ms() - t0;
|
||
if (!g_snap) {
|
||
g_slide_delta = 0;
|
||
g_slide_from = 0;
|
||
g_anim_v = 0;
|
||
commit_page(g_page + delta);
|
||
return 0;
|
||
}
|
||
meow_log(MEOW_LOG_INFO, "reader slide first_frame_ms=%llu page=%d->%d",
|
||
(unsigned long long)dt, g_page + 1, g_page + delta + 1);
|
||
return 1;
|
||
}
|
||
|
||
/* ---- 未过阈值回弹 ---- */
|
||
static void bounce_update(meow_anim *a, float value, void *ctx)
|
||
{
|
||
(void)a; (void)ctx;
|
||
g_drag_off = (int)value;
|
||
invalidate_content();
|
||
}
|
||
|
||
static void bounce_done(meow_anim *a, int canceled, void *ctx)
|
||
{
|
||
(void)a; (void)canceled; (void)ctx;
|
||
g_bounce = NULL;
|
||
g_drag_off = 0;
|
||
g_drag_mode = 0;
|
||
if (!g_quitting) invalidate_content();
|
||
}
|
||
|
||
static void start_bounce(int from_off)
|
||
{
|
||
if (g_bounce || g_snap) { g_drag_off = 0; g_drag_mode = 0; return; }
|
||
meow_anim_desc d = {
|
||
(float)from_off, 0.0f, MR_BOUNCE_MS, MEOW_EASE_OUT,
|
||
bounce_update, bounce_done, NULL,
|
||
};
|
||
g_bounce = meow_anim_start(&d);
|
||
if (!g_bounce) { g_drag_off = 0; g_drag_mode = 0; invalidate_content(); }
|
||
}
|
||
|
||
/* 点击触发的翻页(无跟手位移)。首/末页再前进 → 相邻册(无相邻册则轻提示)。 */
|
||
static void page_flip(int delta)
|
||
{
|
||
if (delta == 0 || g_count <= 0) return;
|
||
int target = mr_nav_clamp_page(g_page + delta, g_count);
|
||
if (target == g_page) { /* 首/末页:册间衔接 */
|
||
book_switch(delta > 0 ? 1 : -1);
|
||
return;
|
||
}
|
||
if (g_snap || g_bounce) return; /* 短动画中忽略连点 */
|
||
if (g_zoom_pct != 100) { /* 翻页回到基准缩放 */
|
||
g_zoom_pct = 100;
|
||
g_pan_x = g_pan_y = 0;
|
||
}
|
||
results_drain();
|
||
|
||
g_flip_t0 = meow_ui_time_ms();
|
||
if (slot_find(target)) {
|
||
g_flip_hits++;
|
||
if (start_slide(delta, 0))
|
||
meow_log(MEOW_LOG_INFO, "reader flip %d->%d hit", g_page + 1, target + 1);
|
||
else
|
||
meow_log(MEOW_LOG_INFO, "reader flip %d->%d hit (no anim slot)", g_page + 1, target + 1);
|
||
} else {
|
||
g_flip_misses++;
|
||
meow_log(MEOW_LOG_INFO, "reader flip %d->%d miss (decode pending)", g_page + 1, target + 1);
|
||
commit_page(target);
|
||
}
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 跳页(chrome 滑块):目标页预解码完成后落位 */
|
||
/* ============================================================ */
|
||
static void jump_land(int page, int timed_out)
|
||
{
|
||
if (g_count <= 0) return;
|
||
page = mr_nav_clamp_page(page, g_count);
|
||
if (page < 0) page = 0;
|
||
if (g_mode == MR_MODE_WEBTOON && g_strip_ok) {
|
||
if (g_scroll) meow_scroll_stop(g_scroll);
|
||
g_woff = mr_strip_offset_for_page(&g_strip, page);
|
||
g_page = page;
|
||
} else {
|
||
g_page = page;
|
||
g_zoom_pct = 100;
|
||
g_pan_x = g_pan_y = 0;
|
||
}
|
||
slider_sync();
|
||
window_update();
|
||
update_title();
|
||
cont_touch();
|
||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||
meow_log(MEOW_LOG_INFO, "reader jump land page=%d/%d%s",
|
||
page + 1, g_count, timed_out ? " (timeout)" : "");
|
||
if (timed_out) toast("页面解码较慢,已跳到目标页");
|
||
else if (page_blocked(page))
|
||
toast(page_skipped(page) ? "目标页内存不足,已跳过" : "目标页无法显示");
|
||
}
|
||
|
||
static void jump_commit_from_slider(void)
|
||
{
|
||
if (!g_slider || g_count <= 0) return;
|
||
int page = meow_slider_get(g_slider) - 1;
|
||
g_jump_open = 0;
|
||
slider_place();
|
||
if (page < 0) page = 0;
|
||
if (page >= g_count) page = g_count - 1;
|
||
if (page == current_page()) { /* 原地:只关面板 */
|
||
meow_ui_invalidate(0, 0, g_vw, MR_TOP_BAR_H + MR_SLIDER_H + 8);
|
||
return;
|
||
}
|
||
if (slot_find(page)) { /* 已缓存:直接落位 */
|
||
jump_land(page, 0);
|
||
return;
|
||
}
|
||
mr_jump_begin(&g_jump, page, g_count, meow_ui_time_ms());
|
||
window_update(); /* 目标页进解码窗(最高优先级) */
|
||
char msg[48];
|
||
snprintf(msg, sizeof msg, "跳转到 P%d…", page + 1);
|
||
toast(msg);
|
||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||
}
|
||
|
||
static void jump_ui_toggle(void)
|
||
{
|
||
if (g_count <= 1) { toast("只有一页,无需跳转"); return; }
|
||
g_jump_open = !g_jump_open;
|
||
if (g_jump_open) {
|
||
if (!g_slider) slider_build();
|
||
slider_sync();
|
||
}
|
||
slider_place();
|
||
meow_ui_invalidate(0, 0, g_vw, MR_TOP_BAR_H + MR_SLIDER_H + 8);
|
||
}
|
||
|
||
static void slider_scrub(int x)
|
||
{
|
||
if (!g_slider || g_count <= 1) return;
|
||
int sx = 24, sw = g_vw - 48;
|
||
if (sw < 1) sw = 1;
|
||
int v = 1 + (int)((long long)(x - sx) * (g_count - 1) / sw);
|
||
if (v < 1) v = 1;
|
||
if (v > g_count) v = g_count;
|
||
meow_slider_set(g_slider, v);
|
||
meow_ui_invalidate(0, MR_TOP_BAR_H, g_vw, MR_SLIDER_H + 8);
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 模式切换(翻页 ⇄ 条漫) */
|
||
/* ============================================================ */
|
||
static void mode_set(int mode, int from_page)
|
||
{
|
||
if (!mr_mode_valid(mode) || mode == g_mode) return;
|
||
int page = from_page >= 0 ? from_page : current_page();
|
||
page = mr_nav_clamp_page(page, g_count);
|
||
if (page < 0) page = 0;
|
||
|
||
if (mode == MR_MODE_WEBTOON) {
|
||
if (!strip_ensure()) { toast("条漫排版失败,保持翻页"); return; }
|
||
scroll_ensure();
|
||
g_page = page;
|
||
g_woff = mr_strip_offset_for_page(&g_strip, page);
|
||
if (g_scroll) meow_scroll_stop(g_scroll);
|
||
} else {
|
||
g_page = page;
|
||
if (g_scroll) { scroll_free(); }
|
||
mr_strip_free(&g_strip);
|
||
g_strip_ok = 0;
|
||
g_woff = 0;
|
||
}
|
||
g_mode = mode;
|
||
g_zoom_pct = 100;
|
||
g_pan_x = g_pan_y = 0;
|
||
slider_sync();
|
||
window_update();
|
||
update_title();
|
||
cont_touch();
|
||
cont_save_now(); /* 模式切换立即记忆 */
|
||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||
meow_log(MEOW_LOG_INFO, "reader mode=%s page=%d/%d",
|
||
mr_mode_name(g_mode), current_page() + 1, g_count);
|
||
toast(mode == MR_MODE_WEBTOON ? "条漫模式 · 连续滚动" : "翻页模式");
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 册间衔接 / 打开册 */
|
||
/* ============================================================ */
|
||
static void open_book_reset_state(void)
|
||
{
|
||
worker_stop();
|
||
scroll_free();
|
||
mr_strip_free(&g_strip);
|
||
g_strip_ok = 0;
|
||
slots_free_all();
|
||
mr_book_close_auto(&g_book, &g_handle);
|
||
if (g_slider) { meow_widget_free(g_slider); g_slider = NULL; }
|
||
g_gen++; /* 作废旧册的异步结果 */
|
||
g_count = 0;
|
||
g_open_err = 0;
|
||
g_open_retry_at = 0;
|
||
g_page = 0;
|
||
g_woff = 0;
|
||
g_zoom_pct = 100;
|
||
g_pan_x = g_pan_y = 0;
|
||
g_cont_dirty = 0;
|
||
g_cont_last_page = -1;
|
||
g_cont_last_mode = -1;
|
||
g_win_n = 0;
|
||
g_fail_paint = 0;
|
||
memset(g_page_fail, 0, sizeof g_page_fail);
|
||
memset(g_page_skip, 0, sizeof g_page_skip);
|
||
g_skip_ms = 0;
|
||
g_jump_open = 0;
|
||
mr_jump_cancel(&g_jump);
|
||
g_toast[0] = '\0';
|
||
g_title_page = -1;
|
||
free(g_meta);
|
||
g_meta = NULL;
|
||
}
|
||
|
||
/* 在目录册里按文件名定位页下标:先精确,再大小写不敏感(书库展示名与磁盘名一致,
|
||
* 兜底只为容错)。-1=没找到。 */
|
||
static int dir_book_find_index(const mr_dir_book *db, const char *name)
|
||
{
|
||
if (!db || !name || !name[0] || db->count <= 0) return -1;
|
||
for (int i = 0; i < db->count; i++)
|
||
if (db->names[i] && strcmp(db->names[i], name) == 0) return i;
|
||
for (int i = 0; i < db->count; i++)
|
||
if (db->names[i] && strcasecmp(db->names[i], name) == 0) return i;
|
||
return -1;
|
||
}
|
||
|
||
/* 打开一本册并恢复模式/起始页。start_page<0 = 末页(上一册衔接)。1=成功。
|
||
* 若 g_start_name 非空(书库点图),优先用**刚枚举出来的目录册**按 basename 定位起始页:
|
||
* 枚举源与翻页完全一致,且不受书库 MR_LIB_SCAN_MAX 截断影响。 */
|
||
static int open_book(const char *dir, int start_page, int mode)
|
||
{
|
||
if (!dir || !dir[0]) return 0;
|
||
char want_name[MR_NAME_MAX];
|
||
snprintf(want_name, sizeof want_name, "%s", g_start_name);
|
||
g_start_name[0] = '\0'; /* 只在本次打开生效 */
|
||
|
||
open_book_reset_state();
|
||
/* reader_page_enter 传入的 dir 就是 g_dir:自拷贝是 snprintf 的未定义行为,
|
||
* 其它调用(册间衔接)才需要复制。 */
|
||
if (dir != g_dir) snprintf(g_dir, sizeof g_dir, "%s", dir);
|
||
g_mode = mr_mode_valid(mode) ? mode : MR_MODE_PAGED;
|
||
|
||
int maxdim = g_vw > g_vh ? g_vw : g_vh; /* 仅作无解码计划时的兜底预算 */
|
||
/* 目录 → 目录册;.cbz/.zip → 档案册(M11)。两者同一个 mr_book 契约,
|
||
* 下面的翻页/条漫消费代码完全不区分 provider。 */
|
||
int n = mr_book_open_auto(&g_book, &g_handle, g_dir, maxdim,
|
||
reader_load_page, reader_unload_page, NULL);
|
||
if (n < 0) {
|
||
int err = errno; /* opendir/malloc 失败原因(设备排障用) */
|
||
g_open_err = 1;
|
||
worker_start();
|
||
update_title();
|
||
meow_log(MEOW_LOG_WARN, "reader open failed: %s (errno=%d %s)", g_dir, err,
|
||
strerror(err));
|
||
/* 资源类失败(内存/FD 瞬时紧张)自动重试一次:library 页已在 on_leave 停止
|
||
* 后台解码,稍后内存回收后重开即可成功;权限/路径错误不重试。 */
|
||
if ((err == ENOMEM || err == EMFILE || err == ENFILE ||
|
||
err == EAGAIN || err == EINTR) && !g_open_retry_used) {
|
||
g_open_retry_used = 1;
|
||
g_open_retry_at = meow_ui_time_ms() + 200;
|
||
}
|
||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||
return 0;
|
||
}
|
||
g_count = n;
|
||
/* 页尺寸缓存:每页 8 字节(4096 页上限 ≈32KB),换来的是一帧内零 fopen probe。 */
|
||
g_meta = (page_meta *)calloc((size_t)g_count, sizeof *g_meta);
|
||
if (!g_meta)
|
||
meow_log(MEOW_LOG_WARN, "reader meta cache unavailable (%d pages)", g_count);
|
||
|
||
int page = start_page < 0 ? g_count - 1 : start_page;
|
||
/* 书库点图:按文件名在刚打开的目录册里定位(与翻页同一份枚举/比较器)。
|
||
* 找不到(文件已改名/删除)才退回 start_page/续读。 */
|
||
if (want_name[0] && g_handle.kind == MR_BOOK_KIND_DIR) {
|
||
int idx = dir_book_find_index(&g_handle.u.dir, want_name);
|
||
if (idx >= 0) page = idx;
|
||
}
|
||
page = mr_nav_clamp_page(page, g_count);
|
||
if (page < 0) page = 0;
|
||
|
||
if (g_mode == MR_MODE_WEBTOON) {
|
||
if (!strip_ensure()) { /* 排版失败:降级翻页,不白屏 */
|
||
meow_log(MEOW_LOG_WARN, "reader strip unavailable, fallback paged");
|
||
g_mode = MR_MODE_PAGED;
|
||
}
|
||
}
|
||
if (g_mode == MR_MODE_WEBTOON) {
|
||
scroll_ensure();
|
||
g_page = page;
|
||
g_woff = mr_strip_offset_for_page(&g_strip, page);
|
||
} else {
|
||
g_page = page;
|
||
g_woff = 0;
|
||
}
|
||
|
||
worker_start();
|
||
if (!g_worker_on && g_count > 0) {
|
||
/* 线程创建失败:退化为主线程同步解起始页(保证不是纯白屏)。 */
|
||
int ww = 0, wh = 0;
|
||
page_geometry_at(g_page, &ww, &wh);
|
||
if (ww <= 0 || wh <= 0) { ww = g_vw; wh = g_vh; }
|
||
int bw = ww, bh = wh;
|
||
plan_box_size(ww, wh, &bw, &bh);
|
||
g_plan.want_w = ww;
|
||
g_plan.want_h = wh;
|
||
g_plan.box_w = bw;
|
||
g_plan.box_h = bh;
|
||
g_plan_page = g_page;
|
||
meow_img *first = mr_book_open(&g_book, g_page);
|
||
if (first) slot_put(g_page, page_bmp_from_img(first), ww, wh);
|
||
meow_log(MEOW_LOG_WARN, "reader worker unavailable, sync first page");
|
||
}
|
||
slider_build();
|
||
window_update(); /* 起始页优先进队 */
|
||
update_title();
|
||
cont_touch();
|
||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||
meow_log(MEOW_LOG_INFO, "reader book=%s pages=%d mode=%s page=%d",
|
||
g_dir, g_count, mr_mode_name(g_mode), current_page() + 1);
|
||
return 1;
|
||
}
|
||
|
||
/* 册间衔接的档案册探测(M11):兄弟表 = 含图目录 ∪ 含图片页的 .cbz/.zip。 */
|
||
static int cbz_pages_probe(const char *path, void *ud)
|
||
{
|
||
(void)ud;
|
||
return mr_zip_count_pages(path);
|
||
}
|
||
|
||
static int book_switch(int delta)
|
||
{
|
||
if (delta == 0) return 0;
|
||
if (g_count <= 0) return 0;
|
||
char next[MR_PATH_MAX];
|
||
/* 根锁定:相邻册必须仍在浏览根内,绝不跳到 SD 卡挂载点以上。 */
|
||
int r = mr_book_adjacent_rooted(g_dir, delta > 0 ? 1 : -1, g_root,
|
||
cbz_pages_probe, NULL, next, (int)sizeof next);
|
||
if (r != 1) {
|
||
toast(delta > 0 ? "已是最后一册" : "已是第一册");
|
||
return 0;
|
||
}
|
||
cont_save_now(); /* 先记忆当前册 */
|
||
int mode = mr_cont_get_mode(next, g_mode); /* 相邻册自己的模式;没有则沿用当前 */
|
||
if (!open_book(next, delta > 0 ? 0 : -1, mode)) {
|
||
toast("无法打开相邻册");
|
||
return 0;
|
||
}
|
||
const char *base = strrchr(g_dir, '/');
|
||
char msg[64];
|
||
snprintf(msg, sizeof msg, "%s · %s", delta > 0 ? "下一册" : "上一册",
|
||
(base && base[1]) ? base + 1 : g_dir);
|
||
toast(msg);
|
||
return 1;
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 跟手拖动 */
|
||
/* ============================================================ */
|
||
static void drag_paint(void)
|
||
{
|
||
uint64_t now = meow_ui_time_ms();
|
||
int gap = g_zoom_pct > 100 ? 33 : MR_PAINT_MS; /* 缩放帧要重采样:30fps 上限 */
|
||
if (now - g_last_paint < (uint64_t)gap) return;
|
||
g_last_paint = now;
|
||
invalidate_content();
|
||
}
|
||
|
||
/* 松手:过阈值且目标页已缓存 → 从当前位移吸附过去;否则回弹。 */
|
||
static void drag_finish(void)
|
||
{
|
||
if (!g_dragging) return;
|
||
g_dragging = 0;
|
||
int dx = g_drag_off;
|
||
int mode = g_drag_mode;
|
||
g_drag_off = 0;
|
||
g_drag_mode = 0;
|
||
|
||
if (mode != 1 || abs(dx) < MR_DRAG_SLOP) {
|
||
invalidate_content();
|
||
return;
|
||
}
|
||
int rtl = mr_reader_get_direction() == MR_DIR_RTL;
|
||
int delta = mr_nav_swipe_delta(rtl, dx);
|
||
int target = mr_nav_clamp_page(g_page + delta, g_count);
|
||
if (mr_nav_commit(dx, g_vw)) {
|
||
if (target == g_page) { /* 首/末页越界:进相邻册 */
|
||
book_switch(delta > 0 ? 1 : -1);
|
||
invalidate_content();
|
||
return;
|
||
}
|
||
g_flip_t0 = meow_ui_time_ms();
|
||
if (slot_find(target) && start_slide(delta, dx)) {
|
||
g_flip_hits++;
|
||
meow_log(MEOW_LOG_INFO, "reader drag flip %d->%d hit", g_page + 1, target + 1);
|
||
} else {
|
||
g_flip_misses++;
|
||
meow_log(MEOW_LOG_INFO, "reader drag flip %d->%d miss (decode pending)",
|
||
g_page + 1, target + 1);
|
||
commit_page(target);
|
||
}
|
||
} else {
|
||
g_drag_off = dx; /* start_bounce 从这里起 */
|
||
g_drag_mode = 1;
|
||
start_bounce(dx);
|
||
}
|
||
}
|
||
|
||
/* 条漫松手:到条顶/条底后再拉过阈值 → 进相邻册;否则起 fling 惯性。 */
|
||
static void webtoon_release(void)
|
||
{
|
||
int pull = g_wo_pull;
|
||
g_wo_pull = 0;
|
||
g_dragging = 0;
|
||
g_drag_mode = 0;
|
||
if (pull >= MR_BOOK_PULL && book_switch(1)) return;
|
||
if (pull <= -MR_BOOK_PULL && book_switch(-1)) return;
|
||
if (g_scroll) meow_scroll_fling(g_scroll);
|
||
cont_touch();
|
||
invalidate_content();
|
||
}
|
||
|
||
void reader_on_touch_down(int x, int y)
|
||
{
|
||
if (!g_ctrl.active || g_quitting) return;
|
||
if (g_count <= 0) return;
|
||
g_touch_moved = 0; /* 每次按下都是新手势(chrome 抬手也不残留) */
|
||
if (y >= g_vh - 28) return; /* 屏底热区:系统回桌面,勿劫持 */
|
||
if (x < 24) return; /* 左缘:系统返回手势,勿劫持 */
|
||
if (g_chrome && y < MR_TOP_BAR_H)
|
||
return; /* 顶栏点击交给 tap(返回/模式/跳页/fit) */
|
||
if (g_chrome && g_jump_open && g_slider && y < MR_TOP_BAR_H + 4 + MR_SLIDER_H) {
|
||
g_dragging = 1;
|
||
g_drag_mode = 3;
|
||
g_touch_moved = 1;
|
||
slider_scrub(x);
|
||
return;
|
||
}
|
||
if (g_snap || g_bounce) return; /* 短动画中不接新手势 */
|
||
|
||
if (g_mode == MR_MODE_WEBTOON) {
|
||
g_dragging = 1;
|
||
g_drag_mode = 4;
|
||
g_wo_y0 = y;
|
||
g_wo_last_y = y;
|
||
g_wo_pull = 0;
|
||
g_tap_pending = 0; /* 条漫没有双击缩放:单击立即生效 */
|
||
if (g_scroll) meow_scroll_stop(g_scroll);
|
||
return;
|
||
}
|
||
|
||
g_dragging = 1;
|
||
g_drag_mode = pan_mode() ? 2 : 1;
|
||
g_drag_x0 = x;
|
||
g_drag_y0 = y;
|
||
g_drag_off = 0;
|
||
g_drag_pan_x0 = g_pan_x;
|
||
g_drag_pan_y0 = g_pan_y;
|
||
}
|
||
|
||
void reader_on_touch_move(int x, int y)
|
||
{
|
||
if (!g_ctrl.active || !g_dragging || g_quitting) return;
|
||
|
||
if (g_drag_mode == 3) { /* 跳页滑块拖动 */
|
||
slider_scrub(x);
|
||
return;
|
||
}
|
||
|
||
if (g_drag_mode == 4) { /* 条漫:整条滚动 */
|
||
int dy = g_wo_last_y - y; /* 手指上移 → 内容上滚 */
|
||
g_wo_last_y = y;
|
||
if (!g_touch_moved && abs(y - g_wo_y0) >= MR_DRAG_SLOP) g_touch_moved = 1;
|
||
if (!g_scroll) return;
|
||
int maxo = mr_strip_max_offset(&g_strip);
|
||
int before = g_woff;
|
||
meow_scroll_track(g_scroll, dy, meow_ui_time_ms());
|
||
int after = g_woff;
|
||
/* 到边后继续同向拉:累计「越界意图」,松手据此进相邻册。
|
||
* 到底后手指继续上移(dy>0)才是「前进」;到顶后继续下移(dy<0)是「后退」。
|
||
* 内容不足一屏(maxo==0)时两向都可拉:按手指方向累计。 */
|
||
int at_bottom = (before >= maxo && after >= maxo);
|
||
int at_top = (before <= 0 && after <= 0);
|
||
if (at_bottom && at_top) {
|
||
g_wo_pull += dy;
|
||
} else if (at_bottom) {
|
||
if (dy > 0) g_wo_pull += dy;
|
||
} else if (at_top) {
|
||
if (dy < 0) g_wo_pull += dy;
|
||
} else {
|
||
g_wo_pull = 0;
|
||
}
|
||
drag_paint();
|
||
return;
|
||
}
|
||
|
||
int dx = x - g_drag_x0;
|
||
int dy = y - g_drag_y0;
|
||
int adx = abs(dx), ady = abs(dy);
|
||
if (!g_touch_moved && (adx >= MR_DRAG_SLOP || ady >= MR_DRAG_SLOP)) {
|
||
g_touch_moved = 1;
|
||
g_tap_pending = 0; /* 变成拖动:作废未决单击 */
|
||
g_last_tap_ms = 0;
|
||
}
|
||
|
||
if (g_drag_mode == 2) { /* 放大态:单指拖拽平移 */
|
||
g_pan_x = g_drag_pan_x0 + dx;
|
||
g_pan_y = g_drag_pan_y0 + dy;
|
||
clamp_pan();
|
||
drag_paint();
|
||
return;
|
||
}
|
||
|
||
/* 竖向主导 → 放弃横滑翻页(松手后不算点击,见 reader_on_tap)。 */
|
||
if (ady > adx * 3 / 2 && ady > 16) {
|
||
g_dragging = 0;
|
||
g_drag_mode = 0;
|
||
g_drag_off = 0;
|
||
invalidate_content();
|
||
return;
|
||
}
|
||
|
||
int rtl = mr_reader_get_direction() == MR_DIR_RTL;
|
||
int delta = mr_nav_swipe_delta(rtl, dx);
|
||
int target = mr_nav_clamp_page(g_page + delta, g_count);
|
||
int off = dx;
|
||
if (target == g_page) { /* 首/末页:橡皮筋 */
|
||
int lim = g_vw / 4;
|
||
if (off > lim) off = lim;
|
||
if (off < -lim) off = -lim;
|
||
}
|
||
g_drag_off = off;
|
||
drag_paint();
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 单击 / 双击 */
|
||
/* ============================================================ */
|
||
static void tap_topbar(int x, int y)
|
||
{
|
||
if (!g_tb) return;
|
||
int hit = meow_topappbar_hit(g_tb, x, y);
|
||
if (hit == MEOW_TOPBAR_BACK) {
|
||
cont_save_now();
|
||
meow_view_page_pop();
|
||
return;
|
||
}
|
||
if (hit < 0) return;
|
||
if (hit == g_act_mode) { mode_set(g_mode == MR_MODE_PAGED ? MR_MODE_WEBTOON
|
||
: MR_MODE_PAGED, -1); return; }
|
||
if (hit == g_act_jump) { jump_ui_toggle(); return; }
|
||
if (hit == g_act_fit) { set_fit(mr_nav_fit_next(g_fit)); return; }
|
||
}
|
||
|
||
static void tap_content(int x, int y)
|
||
{
|
||
(void)y;
|
||
if (g_count <= 0) return;
|
||
int zone = mr_nav_tap_zone(x, g_vw);
|
||
/* 当前页被预算跳过(内存不足占位)→ 中间区点按就是显式重试(任务 2:不能永久 failed)。
|
||
* 只占中间区:左右 1/3 仍必须能翻页,否则占位页会把读者困在这一页。 */
|
||
if (zone == MR_ZONE_MIDDLE) {
|
||
int f = focus_page();
|
||
if (f >= 0 && page_skipped(f) && !page_failed(f)) {
|
||
maybe_retry_skipped_focus(1);
|
||
toast("正在重试…");
|
||
window_update();
|
||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||
return;
|
||
}
|
||
}
|
||
if (zone == MR_ZONE_MIDDLE) {
|
||
set_chrome(!g_chrome);
|
||
return;
|
||
}
|
||
if (pan_mode()) {
|
||
/* 平移模式:左右分区改为平移步进(配合拖拽,补齐两个方向)。 */
|
||
pan_by(zone == MR_ZONE_LEFT ? g_vw / 3 : -g_vw / 3, 0);
|
||
return;
|
||
}
|
||
page_flip(mr_nav_tap_delta(mr_reader_get_direction() == MR_DIR_RTL, zone));
|
||
}
|
||
|
||
static void tap_double(int x, int y)
|
||
{
|
||
if (g_count <= 0) return;
|
||
if (g_mode == MR_MODE_WEBTOON) { /* 条漫不做缩放(保持适宽/内存纪律) */
|
||
set_chrome(!g_chrome);
|
||
return;
|
||
}
|
||
if (g_zoom_pct > 100) {
|
||
g_zoom_pct = 100;
|
||
g_pan_x = g_pan_y = 0;
|
||
} else {
|
||
/* 以双击点为锚放大:该点保持在手指下(再夹到可平移范围)。
|
||
* 倍率按内存纪律夹取:缩放临时缓冲 = 目标宽×高×4,超过软上限就降档。 */
|
||
int bw = 0, bh = 0;
|
||
current_geom(&bw, &bh);
|
||
int z = MR_ZOOM_PCT;
|
||
while (z > 100 && (long long)bw * bh * 4 * z * z / 10000 > MR_ZOOM_TEMP_CAP) z -= 25;
|
||
g_zoom_pct = z;
|
||
g_pan_x = (g_vw / 2 - x) * (z - 100) / 100;
|
||
g_pan_y = (g_vh / 2 - y) * (z - 100) / 100;
|
||
clamp_pan();
|
||
}
|
||
window_update();
|
||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||
}
|
||
|
||
static void tap_flush(void)
|
||
{
|
||
if (!g_tap_pending) return;
|
||
g_tap_pending = 0;
|
||
int x = g_tap_x, y = g_tap_y;
|
||
if (g_chrome && y >= 0 && y < MR_TOP_BAR_H) tap_topbar(x, y);
|
||
else tap_content(x, y);
|
||
}
|
||
|
||
void reader_on_tap(int x, int y)
|
||
{
|
||
if (!g_ctrl.active || g_quitting) return;
|
||
|
||
/* 跳页滑块松手:落位到目标页(目标页预解码后由 tick 落位)。 */
|
||
if (g_drag_mode == 3) {
|
||
g_dragging = 0;
|
||
g_drag_mode = 0;
|
||
g_touch_moved = 0;
|
||
jump_commit_from_slider();
|
||
return;
|
||
}
|
||
|
||
if (g_mode == MR_MODE_WEBTOON) {
|
||
if (g_dragging) {
|
||
if (g_touch_moved) {
|
||
g_touch_moved = 0;
|
||
webtoon_release();
|
||
return;
|
||
}
|
||
g_dragging = 0;
|
||
g_drag_mode = 0;
|
||
} else if (g_touch_moved) {
|
||
g_touch_moved = 0; /* 竖向拖动被 SDK 报成 TAP:忽略 */
|
||
return;
|
||
}
|
||
g_touch_moved = 0;
|
||
if (g_chrome && y >= 0 && y < MR_TOP_BAR_H) { tap_topbar(x, y); return; }
|
||
if (g_jump_open && g_slider && y < MR_TOP_BAR_H + 4 + MR_SLIDER_H) return;
|
||
set_chrome(!g_chrome);
|
||
return;
|
||
}
|
||
|
||
/* 跟手手势收尾:移动过就按拖动处理(过阈值翻页 / 否则回弹),不再当点击。 */
|
||
if (g_dragging) {
|
||
if (g_touch_moved) {
|
||
drag_finish();
|
||
g_touch_moved = 0;
|
||
return;
|
||
}
|
||
g_dragging = 0;
|
||
g_drag_mode = 0;
|
||
g_drag_off = 0;
|
||
} else if (g_touch_moved) {
|
||
g_touch_moved = 0; /* 竖向拖动被 SDK 报成 TAP:忽略 */
|
||
return;
|
||
}
|
||
g_touch_moved = 0;
|
||
|
||
/* chrome 上的点击即时生效,不参与双击判定。 */
|
||
if (g_chrome && y >= 0 && y < MR_TOP_BAR_H) {
|
||
g_tap_pending = 0;
|
||
g_last_tap_ms = 0;
|
||
tap_topbar(x, y);
|
||
return;
|
||
}
|
||
|
||
uint64_t now = meow_ui_time_ms();
|
||
if (g_last_tap_ms && now - g_last_tap_ms <= MR_DOUBLE_MS &&
|
||
abs(x - g_last_tap_x) <= 48 && abs(y - g_last_tap_y) <= 48) {
|
||
g_tap_pending = 0; /* 取消第一击的单击动作 */
|
||
g_last_tap_ms = 0;
|
||
tap_double(x, y);
|
||
return;
|
||
}
|
||
tap_flush(); /* 上一击已过期:先执行 */
|
||
g_tap_pending = 1;
|
||
g_tap_x = x;
|
||
g_tap_y = y;
|
||
g_tap_ms = now;
|
||
g_last_tap_ms = now;
|
||
g_last_tap_x = x;
|
||
g_last_tap_y = y;
|
||
if (!g_ticker) tap_flush(); /* 无节拍器兜底:不延迟单击 */
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 节拍:收结果 + 跳页落位 + 提示超时 + 续读落盘 */
|
||
/* ============================================================ */
|
||
static void reader_tick(void)
|
||
{
|
||
if (!g_ctrl.active || g_quitting) return;
|
||
uint64_t now = meow_ui_time_ms();
|
||
if (now - g_last_tick < MR_TICK_MS) return;
|
||
g_last_tick = now;
|
||
|
||
/* 只有当落位的页真的在屏幕上(或换了几何的当前页)才重绘,预取邻居不刷屏。 */
|
||
if (results_drain() && !g_dragging) invalidate_content();
|
||
|
||
/* 打开册的瞬时资源失败:稍后自动重试一次(进入本页时已停 library 后台解码)。 */
|
||
if (g_open_err && g_open_retry_at && now >= g_open_retry_at) {
|
||
g_open_retry_at = 0;
|
||
int mode = mr_cont_get_mode(g_dir, MR_MODE_PAGED);
|
||
int page = mr_cont_get_page(g_dir, 0);
|
||
open_book(g_dir, page, mode);
|
||
return;
|
||
}
|
||
if (g_fail_paint) {
|
||
g_fail_paint = 0;
|
||
if (!g_dragging) invalidate_content();
|
||
}
|
||
|
||
if (g_tap_pending && now - g_tap_ms >= MR_DOUBLE_MS) tap_flush();
|
||
|
||
if (mr_jump_active(&g_jump)) {
|
||
int target = mr_jump_target(&g_jump);
|
||
int have = target >= 0 && (slot_find(target) != NULL || page_blocked(target));
|
||
int timed_out = 0;
|
||
if (mr_jump_poll(&g_jump, have, now, MR_JUMP_TIMEOUT_MS, &timed_out))
|
||
jump_land(target, timed_out);
|
||
}
|
||
|
||
if (g_toast[0] && now >= g_toast_until) {
|
||
g_toast[0] = '\0';
|
||
meow_ui_invalidate(0, g_vh - 140, g_vw, 140);
|
||
}
|
||
|
||
/* 条漫滚动停止后同步标题页码(滚动中不每帧更新顶栏文字)。 */
|
||
if (g_mode == MR_MODE_WEBTOON && !g_dragging &&
|
||
(!g_scroll || !meow_scroll_active(g_scroll))) {
|
||
if (current_page() != g_title_page) update_title();
|
||
}
|
||
|
||
cont_save_throttled();
|
||
}
|
||
|
||
static void ticker_update(meow_anim *a, float value, void *ctx)
|
||
{
|
||
(void)a; (void)value; (void)ctx;
|
||
reader_tick();
|
||
}
|
||
|
||
static void ticker_done(meow_anim *a, int canceled, void *ctx)
|
||
{
|
||
(void)a; (void)ctx;
|
||
g_ticker = NULL;
|
||
if (canceled || g_quitting || !g_ctrl.active) return;
|
||
meow_anim_desc d = { 0.0f, 1.0f, 0xFFFFFFFFu, MEOW_EASE_LINEAR,
|
||
ticker_update, ticker_done, NULL };
|
||
g_ticker = meow_anim_start(&d);
|
||
}
|
||
|
||
static void ticker_start(void)
|
||
{
|
||
if (g_ticker) return;
|
||
meow_anim_desc d = { 0.0f, 1.0f, 0xFFFFFFFFu, MEOW_EASE_LINEAR,
|
||
ticker_update, ticker_done, NULL };
|
||
g_ticker = meow_anim_start(&d);
|
||
if (!g_ticker) meow_log(MEOW_LOG_WARN, "reader ticker unavailable (anim pool full)");
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 输入:滑动手势(view on_event,先于 app.c 的 on_swipe) */
|
||
/* ============================================================ */
|
||
static int reader_event(meow_view *v, int ev, int a, int b, void *ctx)
|
||
{
|
||
(void)v; (void)ctx;
|
||
if (!g_ctrl.active || g_quitting) return 0;
|
||
if (ev != MR_EV_SWIPE) return 0;
|
||
|
||
if (a == MEOW_SWIPE_LEFT) { /* 左缘返回:交给 app.c page_pop */
|
||
g_dragging = 0;
|
||
g_drag_mode = 0;
|
||
g_drag_off = 0;
|
||
g_touch_moved = 0;
|
||
g_wo_pull = 0;
|
||
cont_save_now(); /* 离开前记忆 */
|
||
return 0;
|
||
}
|
||
if (g_drag_mode == 3) { /* 滑块拖动被 SDK 报成 SWIPE:落位 */
|
||
g_dragging = 0;
|
||
g_drag_mode = 0;
|
||
g_touch_moved = 0;
|
||
jump_commit_from_slider();
|
||
return 1;
|
||
}
|
||
if (g_mode == MR_MODE_WEBTOON) {
|
||
/* 条漫:竖向滑动 = 滚动收尾(fling/册边界);横向滑动不翻页,统一消费。 */
|
||
if (g_dragging) webtoon_release();
|
||
return 1;
|
||
}
|
||
if (a == MEOW_SWIPE_RIGHT) {
|
||
if (g_dragging) { drag_finish(); return 1; }
|
||
if (g_count <= 0) return 1;
|
||
if (pan_mode()) { pan_by(b, 0); return 1; }
|
||
page_flip(mr_nav_swipe_delta(mr_reader_get_direction() == MR_DIR_RTL, b));
|
||
return 1;
|
||
}
|
||
if (a == MEOW_SWIPE_UP || a == MEOW_SWIPE_DOWN) {
|
||
if (g_dragging) { drag_finish(); return 1; }
|
||
if (pan_mode()) { /* 放大 / 原始档溢出:纵向滑动平移 */
|
||
pan_by(0, a == MEOW_SWIPE_DOWN ? b : -b);
|
||
return 1;
|
||
}
|
||
return 0; /* 未放大:纵向滑动不归本页 */
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* view 生命周期 */
|
||
/* ============================================================ */
|
||
static void reader_state_reset(void)
|
||
{
|
||
slots_free_all(); /* 静态槽位初值是 idx=0/img=NULL,先清空 */
|
||
g_count = 0;
|
||
g_open_err = 0;
|
||
g_open_retry_at = 0;
|
||
g_open_retry_used = 0;
|
||
g_page = 0;
|
||
g_mode = MR_MODE_PAGED;
|
||
g_fit = MR_FIT_WIDTH;
|
||
g_zoom_pct = 100;
|
||
g_pan_x = g_pan_y = 0;
|
||
g_chrome = 1;
|
||
g_slide_delta = 0;
|
||
g_slide_from = 0;
|
||
g_anim_v = 0;
|
||
g_dragging = 0;
|
||
g_drag_mode = 0;
|
||
g_drag_off = 0;
|
||
g_touch_moved = 0;
|
||
g_wo_pull = 0;
|
||
g_woff = 0;
|
||
g_strip_ok = 0;
|
||
g_jump_open = 0;
|
||
mr_jump_cancel(&g_jump);
|
||
g_toast[0] = '\0';
|
||
g_title_page = -1;
|
||
g_cont_dirty = 0;
|
||
g_cont_last_page = -1;
|
||
g_cont_last_mode = -1;
|
||
g_win_n = 0;
|
||
g_tap_pending = 0;
|
||
g_last_tap_ms = 0;
|
||
g_quitting = 0;
|
||
/* 本册状态一律清空:g_meta 由 open_book 按新册重建(on_enter 早退也不串上一册);
|
||
* g_start_at 已在 reader_page_enter 入口取走,这里复位防残留。 */
|
||
free(g_meta);
|
||
g_meta = NULL;
|
||
g_start_at = MR_READER_START_RESUME;
|
||
memset(&g_plan, 0, sizeof g_plan);
|
||
g_plan_page = 0;
|
||
g_decode_skip = MR_SKIP_NONE;
|
||
}
|
||
|
||
static void reader_page_enter(meow_view *v, void *ctx)
|
||
{
|
||
mr_page_ctrl *c = (mr_page_ctrl *)ctx;
|
||
c->active = 1;
|
||
g_quitting = 0;
|
||
|
||
int start_at = g_start_at; /* 显式起始页只在本次进入生效 */
|
||
g_vw = meow_ui_width();
|
||
g_vh = meow_ui_height();
|
||
v->x = 0;
|
||
v->y = 0;
|
||
v->w = g_vw;
|
||
v->h = g_vh;
|
||
|
||
meow_ui_set_fullscreen(1);
|
||
const meow_theme *th = meow_theme_get();
|
||
meow_ui_set_page_bg(th->surface);
|
||
|
||
reader_state_reset();
|
||
g_gen++; /* 作废旧册的异步结果 */
|
||
g_last_tick = 0;
|
||
g_last_paint = 0;
|
||
|
||
/* 非根页:控件在 on_enter 建 → owner=ACT_READER(§6.1 跨页泄漏防线)。 */
|
||
if (!g_tb) {
|
||
g_tb = meow_topappbar_create(0, 0, g_vw, MR_TOP_BAR_H, "");
|
||
if (g_tb) {
|
||
meow_topappbar_set_back(g_tb, 1);
|
||
g_act_mode = meow_topappbar_add_action(g_tb, "模式");
|
||
if (g_act_mode >= 0) meow_topappbar_set_item_icon(g_tb, g_act_mode, MEOW_ICON_QUEUE_MUSIC);
|
||
g_act_jump = meow_topappbar_add_action(g_tb, "跳页");
|
||
if (g_act_jump >= 0) meow_topappbar_set_item_icon(g_tb, g_act_jump, MEOW_ICON_SEARCH);
|
||
g_act_fit = meow_topappbar_add_action(g_tb, "适宽");
|
||
if (g_act_fit >= 0) meow_topappbar_set_item_icon(g_tb, g_act_fit, MEOW_ICON_REFRESH);
|
||
}
|
||
}
|
||
if (g_tb) meow_widget_set_rect(g_tb, 0, 0, g_vw, MR_TOP_BAR_H);
|
||
|
||
/* 档案册解压缓存(M11):MeowData/<pkg>/cache。library 页通常已初始化,
|
||
* 这里幂等兜底(进程重启后直接进阅读器、或交付环境只剩 reader 入口)。 */
|
||
{
|
||
const char *data = meow_config_dir();
|
||
if (data && data[0]) mr_cache_init(data);
|
||
}
|
||
|
||
/* 打开册:显式起始页(书库文件管理器模式点第 N 张)优先于续读记录;
|
||
* 否则恢复该册上次的模式与页码(无记录 → 翻页模式第 1 页)。
|
||
* 显式起始页在入口处取走(start_at 局部量),state_reset 之后仍有效。 */
|
||
int mode = mr_cont_get_mode(g_dir, MR_MODE_PAGED);
|
||
int page = start_at != MR_READER_START_RESUME ? start_at
|
||
: mr_cont_get_page(g_dir, 0);
|
||
open_book(g_dir, page, mode);
|
||
|
||
ticker_start();
|
||
log_mem("enter");
|
||
mr_nav_report();
|
||
meow_ui_paint_full();
|
||
|
||
/* 起始页解码是异步的:先给一帧文字/页码,结果在 tick 里落位。 */
|
||
reader_tick();
|
||
}
|
||
|
||
static void reader_page_leave(meow_view *v, void *ctx)
|
||
{
|
||
(void)v;
|
||
((mr_page_ctrl *)ctx)->active = 0;
|
||
g_quitting = 1;
|
||
|
||
cont_save_now(); /* 离开前把最后一页落盘 */
|
||
|
||
if (g_snap) { meow_anim_cancel(g_snap); g_snap = NULL; }
|
||
if (g_bounce) { meow_anim_cancel(g_bounce); g_bounce = NULL; }
|
||
if (g_ticker) { meow_anim_cancel(g_ticker); g_ticker = NULL; }
|
||
|
||
worker_stop(); /* 先停线程,再释放册/图 */
|
||
scroll_free();
|
||
mr_strip_free(&g_strip);
|
||
g_strip_ok = 0;
|
||
slots_free_all();
|
||
free(g_meta);
|
||
g_meta = NULL;
|
||
g_start_at = MR_READER_START_RESUME;
|
||
g_start_name[0] = '\0'; /* 未消费的文件名定位不跨页残留 */
|
||
mr_book_close_auto(&g_book, &g_handle);
|
||
|
||
if (g_slider) { meow_widget_free(g_slider); g_slider = NULL; }
|
||
if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; }
|
||
g_act_mode = g_act_jump = g_act_fit = -1;
|
||
|
||
g_dragging = 0;
|
||
g_drag_mode = 0;
|
||
g_drag_off = 0;
|
||
g_wo_pull = 0;
|
||
g_jump_open = 0;
|
||
mr_jump_cancel(&g_jump);
|
||
g_tap_pending = 0;
|
||
g_slide_delta = 0;
|
||
g_slide_from = 0;
|
||
g_anim_v = 0;
|
||
|
||
log_mem("leave");
|
||
meow_ui_set_fullscreen(0);
|
||
}
|
||
|
||
meow_view *reader_view_create(void)
|
||
{
|
||
static const meow_view_hooks h = {
|
||
.on_enter = reader_page_enter,
|
||
.on_leave = reader_page_leave,
|
||
.on_draw = reader_draw,
|
||
.on_event = reader_event,
|
||
};
|
||
return meow_view_create(ACT_READER, MEOW_VIEW_ACTIVITY, &h, &g_ctrl);
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 书库 → 阅读器导航(app.c / library.c 既有接缝) */
|
||
/* ============================================================ */
|
||
const char *mr_reader_dir(void)
|
||
{
|
||
return g_dir;
|
||
}
|
||
|
||
/* 打开入口共用部分:已在本页时先 pop(完整 leave/enter 生命周期),再设目标并 push。
|
||
* start_name 非空 = 按文件名定位(只对目录册),start_index 忽略。 */
|
||
static void reader_open_common(const char *path, int start_index, const char *start_name)
|
||
{
|
||
if (!path) return;
|
||
/* 已在本页(书库再次点另一张图/另一本册时可能仍是 ACT_READER):
|
||
* SDK 对同一 view 的重复 push 是 no-op(view_enter 见 ENTERED 直接返回),
|
||
* 只改 g_dir 会串到旧册状态。先真正 pop(回收当前册 + 续读落盘),
|
||
* 换目标后再 push,走完整的 leave/enter 生命周期。 */
|
||
int reopen = (meow_ui_activity_active() == ACT_READER) &&
|
||
(meow_ui_activity_depth() > 1);
|
||
if (reopen) meow_view_page_pop();
|
||
|
||
snprintf(g_dir, sizeof g_dir, "%s", path);
|
||
g_start_name[0] = '\0';
|
||
if (start_name && start_name[0]) {
|
||
/* 按文件名定位:由 open_book 在刚枚举出的目录册里找下标(不受列表截断影响)。 */
|
||
snprintf(g_start_name, sizeof g_start_name, "%s", start_name);
|
||
g_start_at = MR_READER_START_RESUME;
|
||
meow_log(MEOW_LOG_INFO, "reader open: %s (name=%s)", g_dir, g_start_name);
|
||
} else {
|
||
/* -1=续读(旧入口语义);其它负数钳到 0;上界在 open_book 里按页数钳。 */
|
||
g_start_at = start_index == MR_READER_START_RESUME ? MR_READER_START_RESUME
|
||
: (start_index < 0 ? 0 : start_index);
|
||
meow_log(MEOW_LOG_INFO, "reader open: %s (start=%d)", g_dir, g_start_at);
|
||
}
|
||
meow_view_page_push(ACT_READER);
|
||
}
|
||
|
||
void mr_reader_open_dir_at(const char *path, int start_index)
|
||
{
|
||
reader_open_common(path, start_index, NULL);
|
||
}
|
||
|
||
void mr_reader_open_dir_at_name(const char *dir, const char *name)
|
||
{
|
||
reader_open_common(dir, MR_READER_START_RESUME, name);
|
||
}
|
||
|
||
void mr_reader_open_dir(const char *path)
|
||
{
|
||
mr_reader_open_dir_at(path, MR_READER_START_RESUME);
|
||
}
|
||
|
||
#ifdef MR_READER_HOST_TEST
|
||
/* 宿主集成测试观察口(不改变任何状态)。 */
|
||
int mr_reader_ui_mode(void) { return g_mode; }
|
||
int mr_reader_ui_page(void) { return current_page(); }
|
||
int mr_reader_ui_count(void) { return g_count; }
|
||
int mr_reader_ui_open_err(void) { return g_open_err; }
|
||
int mr_reader_ui_jump_open(void) { return g_jump_open; }
|
||
int mr_reader_ui_jump_pending(void) { return mr_jump_active(&g_jump); }
|
||
long mr_reader_ui_resident_kb(void) { return resident_bytes() / 1024; }
|
||
int mr_reader_ui_failed_pages(void)
|
||
{
|
||
int n = 0;
|
||
for (int i = 0; i < g_count && i < MR_BOOK_MAX_PAGES; i++)
|
||
if (page_failed(i)) n++;
|
||
return n;
|
||
}
|
||
int mr_reader_ui_skipped_pages(void)
|
||
{
|
||
int n = 0;
|
||
for (int i = 0; i < g_count && i < MR_BOOK_MAX_PAGES; i++)
|
||
if (page_skipped(i) && !page_failed(i)) n++;
|
||
return n;
|
||
}
|
||
void mr_reader_ui_test_set_mem(long free_kb, long avail_kb)
|
||
{
|
||
g_mem_free_override = free_kb;
|
||
g_mem_avail_override = avail_kb;
|
||
}
|
||
/* 峰值模型自检(只读):用真代码的模型算某个文件的预估峰值,供宿主测试与实测对照。
|
||
* 走 page_source 的唯一实现(memfree/mavail 传 -1 = 只算模型,不注入门控读数)。 */
|
||
long long mr_reader_ui_decode_peak_kb(const char *path, int step, int box_w, int box_h)
|
||
{
|
||
mr_img_info in;
|
||
mr_decode_decision dec;
|
||
if (!path || !mr_probe_info(path, &in)) return 0;
|
||
reader_estimate_peak(&in, step, box_w, box_h, -1, -1, &dec);
|
||
return dec.peak_bytes / 1024;
|
||
}
|
||
long long mr_reader_ui_decode_convert_kb(const char *path, int step, int box_w, int box_h)
|
||
{
|
||
mr_img_info in;
|
||
mr_decode_decision dec;
|
||
if (!path || !mr_probe_info(path, &in)) return 0;
|
||
reader_estimate_peak(&in, step, box_w, box_h, -1, -1, &dec);
|
||
return dec.convert_bytes / 1024;
|
||
}
|
||
long mr_reader_ui_peak_cap_kb(void) { return (long)(reader_peak_budget_bytes(-1) / 1024); }
|
||
long mr_reader_ui_peak_budget_kb(void) { return (long)(reader_peak_budget_bytes(mem_free_kb()) / 1024); }
|
||
long mr_reader_ui_resident_cap_kb(void) { return (long)(MR_RESIDENT_CAP / 1024); }
|
||
int mr_reader_ui_blit_draws(void) { return g_blit_draws; }
|
||
int mr_reader_ui_scaled_draws(void) { return g_scaled_draws; }
|
||
int mr_reader_ui_decode_count(void) { return g_decode_count; }
|
||
int mr_reader_ui_fit(void) { return g_fit; }
|
||
int mr_reader_ui_chrome(void) { return g_chrome; }
|
||
/* 指定槽内页图的常驻字节(KB,RGB565 2B/px);0=不在槽内(M32 观测口)。 */
|
||
long mr_reader_ui_slot_kb(int idx)
|
||
{
|
||
page_slot *s = slot_find(idx);
|
||
if (!s) return 0;
|
||
return page_bmp_bytes(s->bmp) / 1024;
|
||
}
|
||
/* M32:已解码、还没落槽的在途结果字节(预算口径,供测试断言它也被计入总预算)。 */
|
||
long mr_reader_ui_pending_kb(void) { return pending_bytes() / 1024; }
|
||
long mr_reader_ui_page_px_cap(void) { return (long)MR_PAGE_PX_CAP; }
|
||
/* 翻页命中/未命中(观测口,非产品逻辑):目标页已在槽 = 预取生效,翻页不用等解码。
|
||
* M31 回滚时整段观测口被删;这两项与 tier 无关,M32 用它验证「命中率不退化」。 */
|
||
int mr_reader_ui_flip_hits(void) { return g_flip_hits; }
|
||
int mr_reader_ui_flip_misses(void) { return g_flip_misses; }
|
||
/* 直接切模式(避开顶栏 tap 命中时序):与顶栏动作走同一个 mode_set。 */
|
||
void mr_reader_test_toggle_mode(void)
|
||
{
|
||
mode_set(g_mode == MR_MODE_PAGED ? MR_MODE_WEBTOON : MR_MODE_PAGED, -1);
|
||
}
|
||
void mr_reader_test_set_root(const char *root)
|
||
{
|
||
if (root && root[0]) snprintf(g_root, sizeof g_root, "%s", root);
|
||
}
|
||
const char *mr_reader_test_root(void) { return g_root; }
|
||
#endif
|
||
|