图库: 阅读器两级清晰度,先低档上屏、停稳再换清晰(M31)
真机 1280x2275 单页解码 392ms,首次看某页有一瞬加载感。改成: - 大页(JPEG ≥1MP)先按显示长边一半的档位(真机页=step 4)解一版低档, 精确落到显示框后立刻上屏(换图仍是单次原生 blit,不闪白、不重置缩放/滚动); - 停稳 200ms 后才解目标档位(step 2),在**同一槽**原地替换低档,低档随即释放, 常驻不翻倍;高档失败/预算跳过时保留低档,不把整页标成「内存不足」; - 拖动/连翻/条漫滚动/跳页在途期间只解低档;高档请求复用 M22 的 gen/窗口 stale 判据作废(只多一条 sharpen 谓词),出队前/后两条路径都计入取消; - 日志尾部新增 tier=low|high(取消行同),真机可直接核对先糊后清。 分档只给 JPEG:step 是 JPEG 缩解概念,PNG/BMP 后端按目标逐行丢行本来就不慢, 且峰值模型对非 JPEG 按「输出=显示框」建模,多一条小图→落框路径会让模型低估 (不新增解码路径,也不改 sdk/ 与 page_source 的唯一模型)。 宿主回归: - test_reader_membudget B10:JPEG 低档先落位(open 不阻塞)→ 高档落同槽 (槽内字节不变、槽位全程不空)→ 拖动中强塞的高档请求被取消且不被解码; - B8 连翻 100 页(6 类 1.3–12MP 循环)峰值增量 2714KB ≤3MB、常驻 ≤3MB, 低档/高档都确实跑过;B3 断言小页保持单档(不付两次解码); - test_reader_perf P11:非 JPEG 大页不分档,仍是一次原生 blit。
This commit is contained in:
1 parent
ce34974727
commit
62826e1d13
4 files changed
+513
-51
No files matched your search
+337
-43
@@ -24,6 +24,10 @@
|
||||
* 按 stb 真实分配模型算峰值,超 3MB 占位 / MemAvailable <6MB 占位。
|
||||
* 占位可重试,不是永久失败。余量确实充足(MemFree 富余)时允许放宽到 12MB。
|
||||
* 页数增长不会线性吃内存(常驻只与解码窗大小有关)。
|
||||
* 手感(M31):JPEG 大页(≥1MP)先按「显示长边一半」的档位解一版低档并立刻上屏,停稳
|
||||
* MR_SHARPEN_MS 后才解目标档位、在同一槽原地替换(两档都落满显示框 → 换图仍是单次 blit,
|
||||
* 不闪白、不重置缩放/滚动)。拖动/连翻/条漫滚动/跳页在途期间只解低档;高档请求复用 M22 的
|
||||
* gen/窗口取消通道作废。每页解码日志尾部带 tier=low|high 便于真机核对「先糊后清」。
|
||||
* 方向:一律读 M3 的 mr_reader_get_direction()(默认 RTL),不另立全局。
|
||||
*
|
||||
* 模式(M7):翻页/条漫是一等公民。阅读 chrome 的图标按钮一键切换;每册的模式与 last page
|
||||
@@ -31,7 +35,7 @@
|
||||
* 自动恢复。书库列表是文件管理器模型(M18),不再显示续读角标。
|
||||
*
|
||||
* 跳页:chrome 内 meow_slider 选目标页,松手后先把目标页解码进缓存再落位(不白屏不闪烁;
|
||||
* 2.5s 超时则降级落位并提示)。
|
||||
* 目标页的低档落位即可跳过去,停稳后原地换清晰;2.5s 超时则降级落位并提示)。
|
||||
*
|
||||
* 册间衔接:末页前进 → 同父目录下一个含图册(书库自然序)的首页;首页后退 → 上一册末页;
|
||||
* 无相邻册时停住并轻提示(不误触退出)。条漫在条顶/条底继续拉 MR_BOOK_PULL 像素松手触发。
|
||||
@@ -85,6 +89,19 @@
|
||||
#define MR_RESIDENT_CAP (3L * 1024 * 1024) /* 页图缓存总字节上限(M22 由 9MB 校准为 3MB) */
|
||||
#define MR_SLOT_TOL 2 /* 缓存图与显示框允许的像素偏差(≤2 走原生 blit,不重采样) */
|
||||
|
||||
/* ---- 两级清晰度(M31):先低档秒出,停稳再换高档 ----
|
||||
* 大页(真机 1280×2275 单页解码 392ms)打开/翻到时先按「显示长边的一半」解一版低档,
|
||||
* 落框后立刻上屏;停稳 MR_SHARPEN_MS 后再解目标档位替换同一槽。低档与高档都**精确落到
|
||||
* 显示框**,所以换图那一下尺寸不变:仍是一次原生 blit,不闪白、不重置缩放/滚动。
|
||||
* 内存:同页只占一个槽(高档落地时替换低档),低档的额外峰值只是「小图 + 一次落框重采样」,
|
||||
* 已由 page_source 的峰值模型计入(step>1 且输出≠显示框时的 +box)。 */
|
||||
#define MR_SHARPEN_MS 200 /* 停稳多久后才发起高档解码(150–300ms 区间取中) */
|
||||
#define MR_TIER_MIN_PIXELS 1000000LL /* 源像素少于此值不分档:解本身不慢,分档只添乱 */
|
||||
#define MR_LOW_MIN_LONG 240 /* 低档输出长边下限(再粗就糊到难看) */
|
||||
#define MR_LOW_MAX_LONG 480 /* 低档输出长边上限(再细省不了多少解码) */
|
||||
|
||||
/* MR_TIER_LOW / MR_TIER_HIGH 定义在 ui/reader/reader.h(宿主测试也要用同一套档位值)。 */
|
||||
|
||||
#define MR_REQ_MAX 8
|
||||
#define MR_RES_MAX 2 /* 结果队列上限(M22:4→2;未取走的已解码图不计入常驻预算) */
|
||||
|
||||
@@ -152,6 +169,9 @@ typedef struct page_slot {
|
||||
meow_img *img; /* 已 downscale 的页图 */
|
||||
int src_w, src_h;
|
||||
int want_w, want_h; /* 本次解码按哪个显示框落的(几何判据,见 slot_ready) */
|
||||
unsigned char tier; /* MR_TIER_*:低档过渡图 / 目标档位(M31) */
|
||||
unsigned char hi_tried; /* 1=这一槽最近一次「换清晰」失败/跳过(别每 tick 重试;
|
||||
* 拖动取消时由 settle_reset 复位,停稳后还能再试) */
|
||||
} page_slot;
|
||||
static page_slot g_slot[MR_SLOTS];
|
||||
|
||||
@@ -176,10 +196,12 @@ static page_meta *g_meta;
|
||||
typedef struct decode_plan {
|
||||
int want_w, want_h;
|
||||
int box_w, box_h;
|
||||
int tier; /* MR_TIER_LOW / MR_TIER_HIGH(M31) */
|
||||
} decode_plan;
|
||||
static decode_plan g_plan;
|
||||
static int g_plan_page; /* 本次解码的页号(只用于日志) */
|
||||
static int g_decode_skip; /* reader_load_page 的跳过原因(MR_SKIP_*) */
|
||||
static int g_decode_tier; /* reader_load_page 实际生效的档位(低档降为单档时=HIGH) */
|
||||
static volatile int g_decode_count; /* 累计解码次数(观察重解抖动/活锁) */
|
||||
|
||||
#define MR_SKIP_NONE 0 /* 解出来了 */
|
||||
@@ -238,6 +260,8 @@ typedef struct page_req {
|
||||
unsigned gen;
|
||||
int want_w, want_h; /* 按哪个显示框解码(主线程算,slot_ready 判据) */
|
||||
int box_w, box_h; /* 落地上限(单页字节上限钳制后的目标尺寸) */
|
||||
unsigned char tier; /* MR_TIER_*(M31:低档过渡 / 目标档位) */
|
||||
unsigned char sharpen; /* 1=「停稳后换清晰」的追加请求:动起来就作废 */
|
||||
} page_req;
|
||||
typedef struct page_res {
|
||||
int idx;
|
||||
@@ -246,6 +270,7 @@ typedef struct page_res {
|
||||
int fail;
|
||||
int skip; /* MR_SKIP_*:非 0 = 预算跳过(可重试,不是坏图) */
|
||||
int want_w, want_h;
|
||||
unsigned char tier; /* MR_TIER_*:结果按哪一档解出来的 */
|
||||
} page_res;
|
||||
|
||||
static pthread_t g_worker;
|
||||
@@ -259,6 +284,19 @@ static int g_req_head, g_req_tail, g_req_n;
|
||||
static page_res g_res[MR_RES_MAX];
|
||||
static int g_res_n;
|
||||
|
||||
/* ---- 两级清晰度状态(M31) ----
|
||||
* g_sharpen_idx:当前唯一允许解高档的页(-1=拖动/未停稳/无需升级)。worker 在锁内据此
|
||||
* 作废「在飞的高档请求」——复用 M22 已有的 gen/窗口取消机制,只是多一条判据。
|
||||
* g_move_ms:最近一次「动」的时刻(按下/拖动/吸附动画/条漫滚动/fling/换页);
|
||||
* 停稳 MR_SHARPEN_MS 后才允许升级。宿主测试可用 mr_reader_ui_test_set_sharpen_ms 改写窗口。 */
|
||||
static int g_sharpen_idx = -1;
|
||||
static int g_hi_inflight = -1; /* 已排队/正在解的目标档位页(去重用;锁内读写) */
|
||||
static uint64_t g_move_ms;
|
||||
static int g_sharpen_ms = MR_SHARPEN_MS;
|
||||
static volatile int g_low_decodes; /* 完成的低档解码次数(宿主测试/日志对照) */
|
||||
static volatile int g_high_decodes; /* 完成的目标档位解码次数 */
|
||||
static volatile int g_high_canceled; /* 被取消的高档请求数(拖动/换页/停稳失效) */
|
||||
|
||||
/* ---- 节拍/动画 ---- */
|
||||
static meow_anim *g_ticker;
|
||||
static meow_anim *g_snap; /* 翻页滑动吸附 */
|
||||
@@ -420,15 +458,64 @@ static void plan_box_size(int want_w, int want_h, int *bw, int *bh)
|
||||
|
||||
/* 固定字段的解码日志:page/src/peak/path 之后是结论,便于 `grep 'reader decode'`
|
||||
* 后按列看曲线。peak = reader_estimate_peak 的预估峰值(字节/kB);path = 实际解码路径
|
||||
* (backend = M23 真缩解后端,stb = 回退旧路径)。 */
|
||||
* (backend = M23 真缩解后端,stb = 回退旧路径);tier = 本次请求的清晰度档位(M31)。 */
|
||||
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)
|
||||
int backend, int tier, 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",
|
||||
"reader decode page=%d src=%dx%d peak=%lldkB memfree=%ldkB memavail=%ldkB %s path=%s step=%d ms=%llu tier=%s",
|
||||
page, src_w, src_h, peak_bytes / 1024, memfree, memavail, verdict,
|
||||
backend ? "backend" : "stb", step, ms);
|
||||
backend ? "backend" : "stb", step, ms,
|
||||
tier == MR_TIER_LOW ? "low" : "high");
|
||||
}
|
||||
|
||||
/* 低档解码目标长边(M31):显示长边的一半,夹到 [MR_LOW_MIN_LONG, MR_LOW_MAX_LONG]。
|
||||
* 只对 JPEG 分档,且必须真的下探一档以上(后端 DCT 阶 n = ceil(8×目标/源长边);
|
||||
* 低档 n 不更小 = 同样的活,只多一次落框重采样):
|
||||
* - 「档位」(step)本来就是 JPEG 缩解的概念;PNG/BMP 后端按目标逐行丢行,本来就不慢,
|
||||
* 而且阅读器的单页峰值模型对非 JPEG 按「输出=显示框」建模——再加一条小图→落框的
|
||||
* 路径会让模型低估。分档只留给真机 168–400ms 的 JPEG 大页。
|
||||
* 返回 0 = 这页不分档,直接按目标档位解。 */
|
||||
static int plan_low_dim(const mr_img_info *in, int box_w, int box_h)
|
||||
{
|
||||
if (!in || in->w <= 0 || in->h <= 0) return 0;
|
||||
if (in->codec != MR_IMG_CODEC_JPEG) return 0;
|
||||
if ((long long)in->w * in->h < MR_TIER_MIN_PIXELS) return 0;
|
||||
int l0 = in->w > in->h ? in->w : in->h;
|
||||
int bl = box_w > box_h ? box_w : box_h;
|
||||
if (l0 <= 0 || bl <= 0) return 0;
|
||||
int lo = bl / 2;
|
||||
if (lo < MR_LOW_MIN_LONG) lo = MR_LOW_MIN_LONG;
|
||||
if (lo > MR_LOW_MAX_LONG) lo = MR_LOW_MAX_LONG;
|
||||
if (lo >= bl) return 0; /* 显示框本来就不大:没有更粗的余地 */
|
||||
int nh = (int)((8LL * bl + l0 - 1) / l0);
|
||||
int nl = (int)((8LL * lo + l0 - 1) / l0);
|
||||
if (nh < 1) nh = 1;
|
||||
if (nl < 1) nl = 1;
|
||||
if (nl >= nh) return 0; /* 同一 DCT 档:低档并不更快 */
|
||||
return lo;
|
||||
}
|
||||
|
||||
/* 按「解码目标长边」跑一次 page_source 的唯一峰值模型(含超预算时继续加粗档位的旧逻辑)。
|
||||
* JPEG 档位由目标长边反推,与 meow_img_load_file_step_ex 的 eff 口径一致;模型在
|
||||
* 「step>1 且输出≠显示框」时会把落框临时图算进峰值——低档走的正是这条已校准的路径。 */
|
||||
static void estimate_for(const mr_img_info *in, int load_dim, int box_w, int box_h,
|
||||
long mfree, long mavail, mr_decode_decision *dec)
|
||||
{
|
||||
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, load_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;
|
||||
}
|
||||
}
|
||||
|
||||
static meow_img *reader_load_page(const char *path, void *ud, int maxdim)
|
||||
@@ -438,8 +525,8 @@ static meow_img *reader_load_page(const char *path, void *ud, int maxdim)
|
||||
uint64_t t0 = meow_ui_time_ms();
|
||||
if (!path || !path[0]) return NULL;
|
||||
|
||||
/* 解码计划由 queue_page() 在主线程算好(显示框 + 落地上限);worker/主线程同步兜底
|
||||
* 路径在 mr_book_open 前写入。无计划时退回旧预算 maxdim。 */
|
||||
/* 解码计划由 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;
|
||||
@@ -460,30 +547,38 @@ static meow_img *reader_load_page(const char *path, void *ud, int maxdim)
|
||||
* reader_estimate_peak 同时给出「走后端还是 stb」与「是否占位」。后端能接住的页
|
||||
* (M23,峰值与源尺寸无关)直接放行;只有回退 stb 的页才参与峰值预算/MemAvailable
|
||||
* 门控。模型不再在本文件另写一份。 */
|
||||
int tier = g_plan.tier;
|
||||
mr_img_info in;
|
||||
int have = mr_probe_info(path, &in);
|
||||
int load_dim = dim; /* 本次解码的输出长边(低档 < dim) */
|
||||
mr_decode_decision dec;
|
||||
memset(&dec, 0, sizeof dec);
|
||||
dec.step = 1;
|
||||
g_decode_tier = MR_TIER_HIGH;
|
||||
if (!have) tier = MR_TIER_HIGH; /* 尺寸未知:没有分档依据,按单档解(日志别谎报) */
|
||||
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);
|
||||
if (tier == MR_TIER_LOW) {
|
||||
int lo = plan_low_dim(&in, box_w, box_h);
|
||||
if (lo > 0 && lo < dim) load_dim = lo;
|
||||
else tier = MR_TIER_HIGH; /* 这页不值得/无法再粗 → 单档 */
|
||||
}
|
||||
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;
|
||||
estimate_for(&in, load_dim, box_w, box_h, mfree, mavail, &dec);
|
||||
/* 低档多一次落框重采样,峰值比高档高;门控拒绝时退回单档(高档),别把本来
|
||||
* 能显示的页变成「内存不足」占位。非 JPEG 的 stb 回退没有缩解能力(整幅解后
|
||||
* 再缩),低档只是白解一遍,同样退回单档。 */
|
||||
if (tier == MR_TIER_LOW &&
|
||||
(dec.skip != MR_DECODE_OK ||
|
||||
(dec.path == MR_DECODE_PATH_STB && in.codec != MR_IMG_CODEC_JPEG))) {
|
||||
tier = MR_TIER_HIGH;
|
||||
load_dim = dim;
|
||||
estimate_for(&in, load_dim, box_w, box_h, mfree, mavail, &dec);
|
||||
}
|
||||
g_decode_tier = tier; /* 实际生效的档位(worker 据此计数/落槽) */
|
||||
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, mfree, mavail,
|
||||
dec.skip == MR_DECODE_SKIP_LOWMEM ? "skip=lowmem" : "skip=peak",
|
||||
dec.step, dec.path == MR_DECODE_PATH_BACKEND,
|
||||
dec.step, dec.path == MR_DECODE_PATH_BACKEND, tier,
|
||||
(unsigned long long)(meow_ui_time_ms() - t0));
|
||||
return NULL;
|
||||
}
|
||||
@@ -495,12 +590,12 @@ static meow_img *reader_load_page(const char *path, void *ud, int maxdim)
|
||||
* 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,
|
||||
meow_img *im = have ? meow_img_load_file_step_ex(path, load_dim, dec.step, allow_stb,
|
||||
&used_backend)
|
||||
: meow_img_load_file_dim(path, dim);
|
||||
if (!im) {
|
||||
log_decode(page, have ? in.w : 0, have ? in.h : 0, dec.peak_bytes, mfree, mavail, "fail",
|
||||
dec.step, have && dec.path == MR_DECODE_PATH_BACKEND,
|
||||
dec.step, have && dec.path == MR_DECODE_PATH_BACKEND, tier,
|
||||
(unsigned long long)(meow_ui_time_ms() - t0));
|
||||
return NULL;
|
||||
}
|
||||
@@ -509,17 +604,18 @@ static meow_img *reader_load_page(const char *path, void *ud, int maxdim)
|
||||
meow_img *small = meow_img_downscale(im, dim);
|
||||
if (small) { meow_img_free(im); im = small; }
|
||||
}
|
||||
/* 3) 精确落到显示框(仅当「故意用了粗档位」时):结果 = box → 命中页仍是一次原生 blit,
|
||||
* 不会把内存换来的降级变成每帧重采样(M17 的热路径纪律)。
|
||||
/* 3) 精确落到显示框(低档过渡图 / 故意用了粗档位时):结果 = box → 命中页仍是一次原生
|
||||
* blit,不会把「先糊后清」变成每帧重采样(M17 的热路径纪律)。低档也落满显示框,
|
||||
* 所以换高档时尺寸不变:不闪白、不重置缩放/滚动,且两档在槽里是同一张的位置。
|
||||
* 源图本身就小于显示框(小图/缩略图)时绝不放大:那会凭空多占常驻字节,
|
||||
* 且 M17 的 slot_ready 语义依赖「SDK 不做上采样」。 */
|
||||
if (have && dec.step > 1 && (in.w >= box_w || in.h >= box_h) &&
|
||||
if (have && (tier == MR_TIER_LOW || 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, mfree, mavail, "ok",
|
||||
dec.step, used_backend,
|
||||
dec.step, used_backend, tier,
|
||||
(unsigned long long)(meow_ui_time_ms() - t0));
|
||||
return im;
|
||||
}
|
||||
@@ -680,6 +776,8 @@ static void slot_clear(int i)
|
||||
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;
|
||||
g_slot[i].tier = MR_TIER_HIGH;
|
||||
g_slot[i].hi_tried = 0;
|
||||
}
|
||||
|
||||
static void slots_free_all(void)
|
||||
@@ -732,9 +830,11 @@ static void slots_reserve(long need_bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/* 放进缓存;同页旧图直接替换(fit/zoom/模式变了要换尺寸)。返回 1=已放入。
|
||||
* want_w/want_h = 这次解码按哪个显示框落的,交给 slot_ready 做几何判据。 */
|
||||
static int slot_put(int idx, meow_img *img, int want_w, int want_h)
|
||||
/* 放进缓存;同页旧图直接替换(fit/zoom/模式变了要换尺寸;低档→高档也走这里)。
|
||||
* 返回 1=已放入。want_w/want_h = 这次解码按哪个显示框落的,交给 slot_ready 做几何判据。
|
||||
* tier 记下这一槽的清晰度:低档图先顶上,高档图落地时**原地替换**(同一槽、同一尺寸),
|
||||
* 页面不闪白、不重置缩放/滚动,常驻也不会一页两张。 */
|
||||
static int slot_put(int idx, meow_img *img, int want_w, int want_h, int tier)
|
||||
{
|
||||
if (!img || idx < 0) return 0;
|
||||
int i = -1, empty = -1;
|
||||
@@ -766,6 +866,7 @@ static int slot_put(int idx, meow_img *img, int want_w, int want_h)
|
||||
if (slot_evict_far(i) < 0) break; /* 只剩目标槽:这一页必须放进来 */
|
||||
}
|
||||
|
||||
/* 低档 → 高档替换:低档图在这里释放,不会与高档同时常驻。 */
|
||||
slot_clear(i);
|
||||
g_slot[i].idx = idx;
|
||||
g_slot[i].img = img;
|
||||
@@ -773,6 +874,7 @@ static int slot_put(int idx, meow_img *img, int want_w, int want_h)
|
||||
g_slot[i].src_h = meow_img_h(img);
|
||||
g_slot[i].want_w = want_w;
|
||||
g_slot[i].want_h = want_h;
|
||||
g_slot[i].tier = (unsigned char)(tier == MR_TIER_LOW ? MR_TIER_LOW : MR_TIER_HIGH);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -845,11 +947,19 @@ static int prefetch_allowed(void)
|
||||
return a >= MR_PREFETCH_MIN_KB;
|
||||
}
|
||||
|
||||
static void queue_page(int idx)
|
||||
/* 大页才分档:真机 1280×2275 / 1280×1032 这类页单次解码 168–400ms;小图本来就快,
|
||||
* 分两档只会多一次解码 + 落框重采样。 */
|
||||
static int page_needs_tier(int idx)
|
||||
{
|
||||
int sw = 0, sh = 0;
|
||||
if (!page_meta_dims(idx, &sw, &sh)) return 0;
|
||||
return (long long)sw * sh >= MR_TIER_MIN_PIXELS;
|
||||
}
|
||||
|
||||
/* 排一个解码请求。tier 决定解低档还是目标档位;sharpen=1 表示「停稳后换清晰」的追加
|
||||
* 请求——动起来(拖动/翻页/滚动)就由 gen/窗口/sharpen 三条判据一起作废。 */
|
||||
static int queue_page_tier(int idx, int tier, int sharpen)
|
||||
{
|
||||
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);
|
||||
@@ -858,8 +968,12 @@ static void queue_page(int idx)
|
||||
plan_box_size(want_w, want_h, &box_w, &box_h);
|
||||
/* 先释放再解码:按这一页的落地字节先淘汰不可见旧图,别让常驻与解码缓冲叠加。 */
|
||||
slots_reserve((long)box_w * box_h * 4);
|
||||
int queued = 0;
|
||||
pthread_mutex_lock(&g_lock);
|
||||
if (!g_stop && !req_queued(idx) && g_req_n < MR_REQ_MAX) {
|
||||
/* 去重:已排队(req_queued)或「换清晰」请求正在 worker 手里(g_hi_inflight)都不再发。
|
||||
* 后者必须单列:请求被 worker 取走后就不在队列里了,只看 req_queued 会每 tick 重发一次。 */
|
||||
if (!g_stop && !req_queued(idx) && g_req_n < MR_REQ_MAX &&
|
||||
!(sharpen && g_hi_inflight == idx)) {
|
||||
page_req *r = &g_req[g_req_tail];
|
||||
r->idx = idx;
|
||||
r->gen = g_gen;
|
||||
@@ -867,11 +981,35 @@ static void queue_page(int idx)
|
||||
r->want_h = want_h;
|
||||
r->box_w = box_w;
|
||||
r->box_h = box_h;
|
||||
r->tier = (unsigned char)(tier == MR_TIER_LOW ? MR_TIER_LOW : MR_TIER_HIGH);
|
||||
r->sharpen = (unsigned char)(sharpen ? 1 : 0);
|
||||
g_req_tail = (g_req_tail + 1) % MR_REQ_MAX;
|
||||
g_req_n++;
|
||||
if (sharpen) g_hi_inflight = idx;
|
||||
queued = 1;
|
||||
pthread_cond_signal(&g_cv);
|
||||
}
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return queued;
|
||||
}
|
||||
|
||||
static void queue_page(int idx)
|
||||
{
|
||||
if (!mr_book_valid(&g_book) || idx < 0 || idx >= g_count) return;
|
||||
/* 永久失败与「仍在跳过期」的页都不排;后者由 maybe_retry_skipped_focus 解封。 */
|
||||
if (page_blocked(idx)) return;
|
||||
page_slot *s = slot_find(idx);
|
||||
if (s && slot_ready(idx)) {
|
||||
/* 已经能显示:只有「低档 → 目标档位」这一种情况还要再解,而且必须是当前焦点页、
|
||||
* 已停稳(g_sharpen_idx 由 tier_pump 在去抖窗口后置位)、并且最近一次升级没失败过。
|
||||
* hi_tried 只在「解坏了 / 预算跳过」时置位(避免每 tick 重试);拖动取消会复位。 */
|
||||
if (s->tier != MR_TIER_LOW || s->hi_tried || g_sharpen_idx != idx ||
|
||||
!page_needs_tier(idx))
|
||||
return;
|
||||
queue_page_tier(idx, MR_TIER_HIGH, 1);
|
||||
return;
|
||||
}
|
||||
queue_page_tier(idx, page_needs_tier(idx) ? MR_TIER_LOW : MR_TIER_HIGH, 0);
|
||||
}
|
||||
|
||||
static void *reader_worker(void *arg)
|
||||
@@ -885,15 +1023,26 @@ static void *reader_worker(void *arg)
|
||||
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× 解码,
|
||||
* 全是这种在飞不可取消的请求。锁内判,窗口也在锁内写,避免共享状态竞争。 */
|
||||
/* 取消(任务 4 / M20 P0-3;M31 扩展):出队后、mr_book_open 前二次判过期——旧册
|
||||
* (gen 变)或已经不在解码窗(拖动/快翻的中间页)直接丢弃。M20 实测拖动翻页原本
|
||||
* 1.75× 解码,全是这种在飞不可取消的请求。锁内判,窗口/sharpen 目标也在锁内写,
|
||||
* 避免共享状态竞争。
|
||||
* M31:拖动/连翻期间只许解低档——「停稳换清晰」的请求一旦动起来就作废(复用同一条
|
||||
* stale 判据,不新造取消通道)。 */
|
||||
int stale = (req.gen != g_gen) || !in_window(req.idx);
|
||||
int stale_motion = 0;
|
||||
if (!stale && req.sharpen && req.idx != g_sharpen_idx) {
|
||||
stale = 1;
|
||||
stale_motion = 1;
|
||||
}
|
||||
if (stale && req.sharpen && g_hi_inflight == req.idx) g_hi_inflight = -1;
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
if (stale) {
|
||||
if (stale_motion) g_high_canceled++;
|
||||
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());
|
||||
"reader decode page=%d src=0x0 peak=0kB memfree=%ldkB memavail=%ldkB skip=cancel step=0 tier=%s",
|
||||
req.idx, mem_free_kb(), mem_avail_kb(),
|
||||
req.tier == MR_TIER_LOW ? "low" : "high");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -902,10 +1051,17 @@ static void *reader_worker(void *arg)
|
||||
g_plan.want_h = req.want_h;
|
||||
g_plan.box_w = req.box_w;
|
||||
g_plan.box_h = req.box_h;
|
||||
g_plan.tier = req.tier;
|
||||
g_plan_page = req.idx;
|
||||
g_decode_skip = MR_SKIP_NONE;
|
||||
g_decode_tier = MR_TIER_HIGH;
|
||||
meow_img *img = mr_book_open(&g_book, req.idx);
|
||||
g_decode_count++;
|
||||
/* 档位以解码回调**实际生效**的为准:低档请求遇到非 JPEG/不值得分档的页会降为
|
||||
* 单档(reader_load_page 把 g_decode_tier 置回 HIGH),落槽也就不会误标成低档。 */
|
||||
int eff_tier = g_decode_tier;
|
||||
if (eff_tier == MR_TIER_LOW) g_low_decodes++;
|
||||
else g_high_decodes++;
|
||||
int skip = img ? MR_SKIP_NONE : g_decode_skip;
|
||||
|
||||
pthread_mutex_lock(&g_lock);
|
||||
@@ -917,9 +1073,12 @@ static void *reader_worker(void *arg)
|
||||
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[g_res_n].tier = (unsigned char)eff_tier;
|
||||
g_res_n++;
|
||||
img = NULL; /* 所有权已移交(失败/跳过时本就是 NULL) */
|
||||
}
|
||||
/* 本页的「换清晰」请求已经走完(无论落不落槽):解除去重标记,允许下一轮重新判定。 */
|
||||
if (req.sharpen && g_hi_inflight == req.idx) g_hi_inflight = -1;
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
if (img) meow_img_free(img);
|
||||
}
|
||||
@@ -948,10 +1107,12 @@ static void worker_stop(void)
|
||||
g_res_n = 0;
|
||||
g_req_n = 0;
|
||||
g_req_head = g_req_tail = 0;
|
||||
g_hi_inflight = -1;
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
}
|
||||
|
||||
/* 丢弃窗口外的排队请求(主线程调用)。 */
|
||||
/* 丢弃窗口外的排队请求(主线程调用)。M31:还没出队的「停稳换清晰」请求,一旦动起来
|
||||
* (g_sharpen_idx 被清)也在这里作废,不必等 worker 出队才发现。 */
|
||||
static void queue_prune(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_lock);
|
||||
@@ -960,6 +1121,11 @@ static void queue_prune(void)
|
||||
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;
|
||||
if (r.sharpen && r.idx != g_sharpen_idx) {
|
||||
if (g_hi_inflight == r.idx) g_hi_inflight = -1;
|
||||
g_high_canceled++;
|
||||
continue;
|
||||
}
|
||||
/* 几何已变的请求作废(否则会按旧显示框解出来又被判过期)。 */
|
||||
int w = 0, h = 0;
|
||||
page_geometry_at(r.idx, &w, &h);
|
||||
@@ -974,7 +1140,9 @@ static void queue_prune(void)
|
||||
}
|
||||
|
||||
/* 收取后台结果:只留窗口内新页。返回 1=有图落位。
|
||||
* 结果分三种:ok(落槽)/ fail(坏图,永久占位)/ skip(预算跳过,可重试占位)。 */
|
||||
* 结果分三种:ok(落槽)/ fail(坏图,永久占位)/ skip(预算跳过,可重试占位)。
|
||||
* M31:高档(停稳换清晰)失败/跳过时,低档已经在屏幕上——保留低档继续看,不把整页标成
|
||||
* 「内存不足」或坏图;低档自己的失败/跳过才走旧语义(那才是真的没图)。 */
|
||||
static int results_drain(void)
|
||||
{
|
||||
page_res got[MR_RES_MAX];
|
||||
@@ -988,20 +1156,25 @@ static int results_drain(void)
|
||||
int changed = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (got[i].gen != g_gen) { meow_img_free(got[i].img); continue; }
|
||||
page_slot *low = got[i].tier == MR_TIER_HIGH ? slot_find(got[i].idx) : NULL;
|
||||
if (low && low->tier != MR_TIER_LOW) low = NULL; /* 已有目标档:不是「低档兜底」 */
|
||||
if (got[i].skip) {
|
||||
/* 预算跳过(内存不足/峰值超限):可重试,不写永久失败位。 */
|
||||
if (low) { low->hi_tried = 1; continue; } /* 低档在屏:降级显示,不占位 */
|
||||
page_mark_skipped(got[i].idx);
|
||||
if (page_visible(got[i].idx)) g_fail_paint = 1;
|
||||
continue;
|
||||
}
|
||||
if (got[i].fail) {
|
||||
if (low) { low->hi_tried = 1; continue; }
|
||||
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].img, got[i].want_w, got[i].want_h) &&
|
||||
page_visible(got[i].idx)) changed = 1;
|
||||
if (slot_put(got[i].idx, got[i].img, got[i].want_w, got[i].want_h,
|
||||
got[i].tier) && page_visible(got[i].idx))
|
||||
changed = 1;
|
||||
page_skip_clear(got[i].idx); /* 解出来了:撤销跳过标记 */
|
||||
} else {
|
||||
meow_img_free(got[i].img);
|
||||
@@ -1046,6 +1219,72 @@ static void window_update(void)
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 两级清晰度的停稳去抖(M31) */
|
||||
/* ------------------------------------------------------------
|
||||
* 规则只有三条:
|
||||
* 1) 拖动 / 吸附动画 / 条漫滚动(含 fling)期间一律不动高档:g_sharpen_idx=-1,
|
||||
* 在飞/排队的「停稳换清晰」请求按 M22 的取消通道作废;跳页在途时同样不解高档
|
||||
* (单 worker 串行,导航优先);
|
||||
* 2) 停稳 MR_SHARPEN_MS 后,焦点页若是低档,才允许它解目标档位(g_sharpen_idx=focus);
|
||||
* 3) 每次「动」都重置停稳计时;换页/换几何也会清掉资格(重新计时)。
|
||||
* 高档只发给焦点页:单 worker 串行,先保证用户正看的那页清晰,邻居留低档待翻到再说。 */
|
||||
static int motion_active(void)
|
||||
{
|
||||
if (g_dragging) return 1;
|
||||
if (g_snap || g_bounce) return 1;
|
||||
if (g_scroll && meow_scroll_active(g_scroll)) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 「动」了:重置停稳计时,并取消当前的高档资格(排队/在飞的请求随之作废)。
|
||||
* 被取消的这一页如果还是低档,把 hi_tried 复位——停稳后还能再试一次。 */
|
||||
static void settle_reset(void)
|
||||
{
|
||||
g_move_ms = meow_ui_time_ms();
|
||||
if (g_sharpen_idx < 0) return;
|
||||
page_slot *s = slot_find(g_sharpen_idx);
|
||||
if (s && s->tier == MR_TIER_LOW) s->hi_tried = 0;
|
||||
pthread_mutex_lock(&g_lock);
|
||||
g_sharpen_idx = -1;
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
queue_prune();
|
||||
}
|
||||
|
||||
static void tier_pump(uint64_t now)
|
||||
{
|
||||
if (g_count <= 0 || g_quitting) return;
|
||||
if (motion_active()) {
|
||||
g_move_ms = now;
|
||||
if (g_sharpen_idx >= 0) settle_reset();
|
||||
return;
|
||||
}
|
||||
/* 跳页在途:单 worker 串行,「换清晰」是锦上添花,不能插在跳页目标的低档前面——
|
||||
* 否则滑块一拖就是几秒(早已过停稳窗口),老页的高档会把跳页落位往后挤几百 ms。 */
|
||||
if (mr_jump_active(&g_jump)) {
|
||||
if (g_sharpen_idx >= 0) settle_reset();
|
||||
return;
|
||||
}
|
||||
int f = focus_page();
|
||||
if (f < 0) return;
|
||||
page_slot *s = slot_find(f);
|
||||
if (!s || s->tier != MR_TIER_LOW || s->hi_tried || !page_needs_tier(f)) {
|
||||
if (g_sharpen_idx == f) {
|
||||
pthread_mutex_lock(&g_lock);
|
||||
g_sharpen_idx = -1;
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (now - g_move_ms < (uint64_t)g_sharpen_ms) return; /* 还没停稳 */
|
||||
if (g_sharpen_idx != f) {
|
||||
pthread_mutex_lock(&g_lock);
|
||||
g_sharpen_idx = f;
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
}
|
||||
queue_page(f); /* 幂等:已排队/在飞/已清晰/已试过都直接返回 */
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 条漫排版 + 滚动会话 */
|
||||
/* ============================================================ */
|
||||
@@ -1369,6 +1608,7 @@ static void set_fit(int fit)
|
||||
g_fit = (fit < 0 || fit >= MR_FIT_COUNT) ? MR_FIT_WIDTH : fit;
|
||||
g_zoom_pct = 100;
|
||||
g_pan_x = g_pan_y = 0;
|
||||
settle_reset(); /* 几何变了:停稳计时重来,低档先上屏再换清晰 */
|
||||
window_update();
|
||||
update_title();
|
||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||||
@@ -1401,6 +1641,7 @@ static void commit_page(int target)
|
||||
g_page = target;
|
||||
g_zoom_pct = 100;
|
||||
g_pan_x = g_pan_y = 0;
|
||||
settle_reset(); /* 换页:停稳计时重来(拖动/连翻期间不解高档) */
|
||||
window_update();
|
||||
update_title();
|
||||
slider_sync();
|
||||
@@ -1535,6 +1776,7 @@ static void jump_land(int page, int timed_out)
|
||||
g_pan_x = g_pan_y = 0;
|
||||
}
|
||||
slider_sync();
|
||||
settle_reset(); /* 落位:停稳计时重来 */
|
||||
window_update();
|
||||
update_title();
|
||||
cont_touch();
|
||||
@@ -1621,6 +1863,7 @@ static void mode_set(int mode, int from_page)
|
||||
g_zoom_pct = 100;
|
||||
g_pan_x = g_pan_y = 0;
|
||||
slider_sync();
|
||||
settle_reset(); /* 模式/排版几何变了:停稳计时重来 */
|
||||
window_update();
|
||||
update_title();
|
||||
cont_touch();
|
||||
@@ -1663,6 +1906,9 @@ static void open_book_reset_state(void)
|
||||
mr_jump_cancel(&g_jump);
|
||||
g_toast[0] = '\0';
|
||||
g_title_page = -1;
|
||||
g_sharpen_idx = -1; /* 新册:高档资格清零,停稳计时重来 */
|
||||
g_hi_inflight = -1;
|
||||
g_move_ms = meow_ui_time_ms();
|
||||
free(g_meta);
|
||||
g_meta = NULL;
|
||||
}
|
||||
@@ -1760,9 +2006,10 @@ static int open_book(const char *dir, int start_page, int mode)
|
||||
g_plan.want_h = wh;
|
||||
g_plan.box_w = bw;
|
||||
g_plan.box_h = bh;
|
||||
g_plan.tier = MR_TIER_HIGH; /* 没有后台线程:直接解目标档,不分档 */
|
||||
g_plan_page = g_page;
|
||||
meow_img *first = mr_book_open(&g_book, g_page);
|
||||
if (first) slot_put(g_page, first, ww, wh);
|
||||
if (first) slot_put(g_page, first, ww, wh, MR_TIER_HIGH);
|
||||
meow_log(MEOW_LOG_WARN, "reader worker unavailable, sync first page");
|
||||
}
|
||||
slider_build();
|
||||
@@ -1876,6 +2123,7 @@ void reader_on_touch_down(int x, int y)
|
||||
{
|
||||
if (!g_ctrl.active || g_quitting) return;
|
||||
if (g_count <= 0) return;
|
||||
settle_reset(); /* 手一按下去就不算停稳:在飞的高档请求作废 */
|
||||
g_touch_moved = 0; /* 每次按下都是新手势(chrome 抬手也不残留) */
|
||||
if (y >= g_vh - 28) return; /* 屏底热区:系统回桌面,勿劫持 */
|
||||
if (x < 24) return; /* 左缘:系统返回手势,勿劫持 */
|
||||
@@ -2055,6 +2303,7 @@ static void tap_double(int x, int y)
|
||||
g_pan_y = (g_vh / 2 - y) * (z - 100) / 100;
|
||||
clamp_pan();
|
||||
}
|
||||
settle_reset(); /* 缩放几何变了:重新计时(低档先上屏) */
|
||||
window_update();
|
||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||||
}
|
||||
@@ -2157,6 +2406,9 @@ static void reader_tick(void)
|
||||
/* 只有当落位的页真的在屏幕上(或换了几何的当前页)才重绘,预取邻居不刷屏。 */
|
||||
if (results_drain() && !g_dragging) invalidate_content();
|
||||
|
||||
/* 两级清晰度:停稳去抖(拖动/滚动期间只留低档,停稳后才补高档)。 */
|
||||
tier_pump(now);
|
||||
|
||||
/* 打开册的瞬时资源失败:稍后自动重试一次(进入本页时已停 library 后台解码)。 */
|
||||
if (g_open_err && g_open_retry_at && now >= g_open_retry_at) {
|
||||
g_open_retry_at = 0;
|
||||
@@ -2312,6 +2564,13 @@ static void reader_state_reset(void)
|
||||
memset(&g_plan, 0, sizeof g_plan);
|
||||
g_plan_page = 0;
|
||||
g_decode_skip = MR_SKIP_NONE;
|
||||
g_decode_tier = MR_TIER_HIGH;
|
||||
g_sharpen_idx = -1;
|
||||
g_hi_inflight = -1;
|
||||
g_move_ms = meow_ui_time_ms();
|
||||
g_low_decodes = 0;
|
||||
g_high_decodes = 0;
|
||||
g_high_canceled = 0;
|
||||
}
|
||||
|
||||
static void reader_page_enter(meow_view *v, void *ctx)
|
||||
@@ -2527,6 +2786,41 @@ 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; }
|
||||
/* M31 两级清晰度观察口:焦点页当前档位(-1=无图/0=低档/1=目标档位)、各档解码次数、
|
||||
* 被取消的高档请求数、当前允许升级的页(-1=拖动/未停稳/无需升级)。 */
|
||||
int mr_reader_ui_focus_tier(void)
|
||||
{
|
||||
page_slot *s = slot_find(focus_page());
|
||||
if (!s) return -1;
|
||||
return s->tier == MR_TIER_LOW ? MR_TIER_LOW : MR_TIER_HIGH;
|
||||
}
|
||||
int mr_reader_ui_slot_tier(int idx)
|
||||
{
|
||||
page_slot *s = slot_find(idx);
|
||||
if (!s) return -1;
|
||||
return s->tier == MR_TIER_LOW ? MR_TIER_LOW : MR_TIER_HIGH;
|
||||
}
|
||||
long mr_reader_ui_slot_kb(int idx)
|
||||
{
|
||||
page_slot *s = slot_find(idx);
|
||||
if (!s) return 0;
|
||||
return (long)s->src_w * s->src_h * 4 / 1024;
|
||||
}
|
||||
int mr_reader_ui_low_decodes(void) { return g_low_decodes; }
|
||||
int mr_reader_ui_high_decodes(void) { return g_high_decodes; }
|
||||
int mr_reader_ui_high_canceled(void) { return g_high_canceled; }
|
||||
int mr_reader_ui_sharpen_idx(void) { return g_sharpen_idx; }
|
||||
/* 宿主测试改写「停稳」窗口:0=立刻允许高档(同步验证落位),很大=冻结高档(验证低档先落位)。 */
|
||||
void mr_reader_ui_test_set_sharpen_ms(int ms)
|
||||
{
|
||||
g_sharpen_ms = ms < 0 ? 0 : ms;
|
||||
}
|
||||
/* 宿主测试:绕过停稳判定强行排一个「换清晰」请求,用于验证拖动中会被取消。 */
|
||||
void mr_reader_ui_test_queue_high(int idx)
|
||||
{
|
||||
if (idx < 0 || idx >= g_count) return;
|
||||
queue_page_tier(idx, MR_TIER_HIGH, 1);
|
||||
}
|
||||
/* 直接切模式(避开顶栏 tap 命中时序):与顶栏动作走同一个 mode_set。 */
|
||||
void mr_reader_test_toggle_mode(void)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user