图库: 阅读器两级清晰度,先低档上屏、停稳再换清晰(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:
AskaEth committed 2026-11-06 13:10:01 +08:00
1 parent ce34974727
commit 62826e1d13
4 files changed
+513 -51

No files matched your search

+114 -7
View File
@@ -17,7 +17,10 @@
* B7b 回退路径:CMYK / Adam7 PNG / 1bpp BMP 走 stb,按旧门控占位
* B7c P1-1 旁路回归:截断 12MP JPEG / 3000×3000 24bpp 头-only BMP(判定 backend,
* 后端失败必须 NULL、峰值 ≤3MB)/ 非法位深 PNG(矩阵拒绝→stb 占位)
* B8 M27:连翻 30 页(6 类循环)不掉页、不占位、任何时刻存活增量 ≤3MB
* B8 M27/M31:连翻 100 页(6 类循环)不掉页、不占位、任何时刻存活增量 ≤3MB;
* 分档页低档/高档都确实跑过(M31:先低后清不把峰值/常驻翻倍)
* B10 M31:两级清晰度生命周期(JPEG 大页):低档先落位(open 不阻塞)→ 停稳后高档落
* 同一槽(槽内字节不变、槽位不空)→ 拖动中强塞的高档请求被取消通道作废
* B9 M27:CMYK 回退页集成 —— 占位可重试且不崩
*
* 夹具:BMP/PNG 小夹具由本文件用 C 写;JPEG/PNG/BMP 大夹具由 python3+Pillow 生成
@@ -591,6 +594,9 @@ int main(void)
mr_reader_ui_failed_pages());
/* 取消/预取纪律:100 次翻页的解码次数必须有界(M20 实测曾 1.75× 浪费)。 */
CHECK(decodes <= 100 * 2, "decode count bounded over 100 flips (%d)", decodes);
/* M31:小页(480×640=0.3MP)不分档——低档只会多一次解码 + 落框,纯开销。 */
CHECK(mr_reader_ui_low_decodes() == 0,
"small pages stay single-tier (%d low decodes)", mr_reader_ui_low_decodes());
meow_view_page_pop();
pump_ms(200);
}
@@ -762,7 +768,10 @@ int main(void)
printf(" SKIP jpeg assertions: Pillow unavailable (python3 -c 'import PIL')\n");
}
/* ---- B8 M27:连翻 30 页(6 类典型页循环)——不掉页、不占位、峰值 ≤3MB ---- */
/* ---- B8 M27/M31:连翻 100 页(6 类典型页循环)——不掉页、不占位、峰值不增长 ----
* M31 起这些 1.3–12MP 的页走两级清晰度(低档先上屏、停稳换高档),所以这条同时是
* 「分档不把峰值/常驻翻倍」的回归:低档与高档在同一槽原地替换,任何时刻的存活增量
* 仍按单页解码峰值封顶。 */
if (have_jpeg && mixed_pages == 30) {
mr_reader_ui_test_set_mem(4096, 16000);
mr_reader_open_dir(mixed);
@@ -772,11 +781,12 @@ int main(void)
int advanced = 0, skipped_max = 0;
size_t live_steady = g_live;
int d0 = mr_reader_ui_decode_count();
int low0 = mr_reader_ui_low_decodes(), high0 = mr_reader_ui_high_decodes();
/* P2-3:只在窗口稳定后 reset 一次,之后不再 reset——这样淘汰旧图不会掩盖
* 「分配新图 + 解码」的真实峰值;指标 = 整个 30 翻窗口内相对稳态基线的最大存活增量。
* 「分配新图 + 解码」的真实峰值;指标 = 整个 100 翻窗口内相对稳态基线的最大存活增量。
* (旧实现每翻页 reset 一次,淘汰把基线拉低,报告里的 536KB 是伪值。) */
size_t base = peak_reset();
for (int i = 0; i < 30; i++) {
for (int i = 0; i < 100; i++) {
int before = mr_reader_ui_page();
tap(i % 2 == 0 ? 40 : w - 40, 300);
pump_ms(220);
@@ -785,20 +795,117 @@ int main(void)
skipped_max = mr_reader_ui_skipped_pages();
}
size_t transient = g_peak > base ? g_peak - base : 0;
INFO("30 mixed flips: advanced=%d skipped=%d decodes=%d steady=%zuKB "
INFO("100 mixed flips: advanced=%d skipped=%d decodes=%d (low=%d high=%d) steady=%zuKB "
"resident=%ldKB max_live_delta=%zuKB",
advanced, skipped_max, mr_reader_ui_decode_count() - d0,
mr_reader_ui_low_decodes() - low0, mr_reader_ui_high_decodes() - high0,
live_steady / 1024, mr_reader_ui_resident_kb(), transient / 1024);
CHECK(advanced == 30, "30 页全部翻到 (%d/30)", advanced);
CHECK(advanced == 100, "100 页全部翻到 (%d/100)", advanced);
CHECK(skipped_max == 0, "典型页连翻无占位 (max=%d)", skipped_max);
CHECK(mr_reader_ui_failed_pages() == 0, "无永久失败页 (%d)",
mr_reader_ui_failed_pages());
CHECK(mr_reader_ui_low_decodes() > low0 && mr_reader_ui_high_decodes() > high0,
"大页确实走了两级(low +%d, high +%d)",
mr_reader_ui_low_decodes() - low0, mr_reader_ui_high_decodes() - high0);
CHECK(mr_reader_ui_resident_kb() <= mr_reader_ui_resident_cap_kb(),
"常驻仍在上限内 (%ldKB ≤ %ldKB)", mr_reader_ui_resident_kb(),
mr_reader_ui_resident_cap_kb());
CHECK(transient <= 3 * 1024 * 1024,
"30 页连翻任何时刻存活增量 ≤3MB (%zuKB)", transient / 1024);
"100 页连翻任何时刻存活增量 ≤3MB (%zuKB)", transient / 1024);
meow_view_page_pop();
pump_ms(200);
}
/* ---- B10 M31:两级清晰度生命周期(2000×3000 JPEG,真机同类大页)----
* 1) 冻结停稳窗口 → 只解低档:焦点页 tier=low、常驻=显示框大小(仍是一次原生 blit),
* 主线程不等解码(open 立即返回);
* 2) 放开停稳窗口 → 高档落同一槽:tier=high、槽内字节不变(不翻倍常驻)、槽位全程不空;
* 3) 拖动中强塞的高档请求 → 被 M22 的 gen/窗口取消通道作废(skip=cancel),不会被解出来。 */
if (have_jpeg) {
char tierbook[MR_PATH_MAX];
snprintf(tierbook, sizeof tierbook, "%s/tier", root);
int tier_ok = mkdir_p(tierbook) == 0;
for (int i = 0; tier_ok && i < 2; i++) {
char src[MR_PATH_MAX], dst[MR_PATH_MAX];
snprintf(src, sizeof src, "%s/base2000.jpg", jpgbook);
snprintf(dst, sizeof dst, "%s/t%d.jpg", tierbook, i + 1);
if (copy_file(src, dst) != 0) tier_ok = 0;
}
if (tier_ok) {
mr_reader_ui_test_set_mem(4096, 16000);
mr_reader_ui_test_set_sharpen_ms(60000); /* 冻结:先验证低档先落位 */
uint64_t t_open = (uint64_t)meow_ui_time_ms();
mr_reader_open_dir(tierbook);
uint64_t open_ms = (uint64_t)meow_ui_time_ms() - t_open;
CHECK(mr_reader_ui_count() == 2, "tier book pages=%d", mr_reader_ui_count());
CHECK(open_ms < 500, "open is non-blocking (%llu ms)",
(unsigned long long)open_ms);
pump_ms(800);
meow_ui_paint_full();
int page0 = mr_reader_ui_page();
CHECK(mr_reader_ui_focus_tier() == MR_TIER_LOW,
"low tier lands first (tier=%d)", mr_reader_ui_focus_tier());
CHECK(mr_reader_ui_low_decodes() >= 1, "low decode issued (%d)",
mr_reader_ui_low_decodes());
CHECK(mr_reader_ui_high_decodes() == 0,
"no high decode before the settle window (%d)", mr_reader_ui_high_decodes());
long low_kb = mr_reader_ui_slot_kb(page0);
CHECK(low_kb > 0, "low tier image resident (%ldKB)", low_kb);
int bl = mr_reader_ui_blit_draws(), sc = mr_reader_ui_scaled_draws();
meow_ui_paint_full();
CHECK(mr_reader_ui_blit_draws() > bl && mr_reader_ui_scaled_draws() == sc,
"low tier draws as native blit (blit +%d, scaled +%d)",
mr_reader_ui_blit_draws() - bl, mr_reader_ui_scaled_draws() - sc);
/* 放开升级:边等边采样,焦点槽任何时刻都不能空(不闪白)。 */
int high0 = mr_reader_ui_high_decodes();
long min_slot = low_kb;
mr_reader_ui_test_set_sharpen_ms(0);
for (int i = 0; i < 100 && mr_reader_ui_focus_tier() != MR_TIER_HIGH; i++) {
pump_ms(10);
long k = mr_reader_ui_slot_kb(page0);
if (k > 0 && k < min_slot) min_slot = k;
}
meow_ui_paint_full();
CHECK(mr_reader_ui_page() == page0, "tier swap keeps the page (%d -> %d)",
page0, mr_reader_ui_page());
CHECK(mr_reader_ui_focus_tier() == MR_TIER_HIGH,
"high tier replaces low in the same slot (tier=%d)",
mr_reader_ui_focus_tier());
CHECK(mr_reader_ui_high_decodes() > high0,
"high decode issued after settle (%d -> %d)", high0,
mr_reader_ui_high_decodes());
long high_kb = mr_reader_ui_slot_kb(page0);
CHECK(high_kb > 0 && high_kb <= low_kb + 8,
"same-slot swap: one page resident, not two (%ldKB -> %ldKB)", low_kb,
high_kb);
CHECK(min_slot > 0, "no blank frame while swapping tiers (min slot %ldKB)",
min_slot);
CHECK(mr_reader_ui_scaled_draws() == sc,
"high tier still native blit (scaled %d -> %d)", sc,
mr_reader_ui_scaled_draws());
/* 拖动中取消高档:拖动期间强塞一个「换清晰」请求,必须被取消通道作废。 */
int hd0 = mr_reader_ui_high_decodes(), hc0 = mr_reader_ui_high_canceled();
reader_on_touch_down(240, 400);
reader_on_touch_move(180, 400); /* 进入拖动 → g_sharpen_idx=-1 */
CHECK(mr_reader_ui_sharpen_idx() == -1, "drag clears sharpen eligibility");
mr_reader_ui_test_queue_high(page0);
pump_ms(200);
CHECK(mr_reader_ui_high_canceled() > hc0,
"high request canceled while dragging (%d -> %d)", hc0,
mr_reader_ui_high_canceled());
CHECK(mr_reader_ui_high_decodes() == hd0,
"canceled high request is never decoded (%d -> %d)", hd0,
mr_reader_ui_high_decodes());
reader_on_tap(240, 400); /* 松手:回弹,不翻页 */
pump_ms(300);
mr_reader_ui_test_set_sharpen_ms(200); /* 恢复产品默认停稳窗口 */
meow_view_page_pop();
pump_ms(200);
}
}
/* ---- B9 M27:回退路径集成 —— CMYK 页占位(可重试)且不崩 ---- */
if (have_fallback) {
mr_reader_ui_test_set_mem(4096, 16000);
+45 -1
View File
@@ -15,7 +15,12 @@
* P5 「先点适高,再切条漫」:scaled 不增长(评审点名的第二入口);
* P6 双击放大再双击缩回:缩回后按新几何重解 → scaled 不增长(P2-1);
* P7 静止若干帧不产生新解码(无重解抖动/活锁,P2-2);
* P8 驻留页图有字节上界(<= 9MB)。
* P8 驻留页图有字节上界(<= 9MB);
* P9 800×1280(评审表格另一档):翻页 + 条漫都必须零重采样;
* P10 大屏(1080×1920):fit 目标被字节上限钳住后不得反复重解;
* P11 分档策略(M31):非 JPEG 大页不分档(一次解到目标档、仍是原生 blit);
* 低档先落位→停稳换清晰→拖动取消的完整生命周期在 test_reader_membudget 的 B10
* 用真实 JPEG 夹具覆盖。
*
* 编译:test/run_tests.sh(与 test_open_book_host 同口径;只发布 meow_ui.a 时 SKIP)。
*/
@@ -368,6 +373,45 @@ int main(void)
"800x1280 webtoon: zero resample (scaled %d -> %d)", st,
mr_reader_ui_scaled_draws());
/* ---- P11 分档策略(M31):非 JPEG 大页不分档——一次解到目标档,仍是原生 blit ----
* 低档/高档的完整生命周期(低档先落位→停稳换清晰→拖动取消)在 test_reader_membudget
* 的 B10 用真实 JPEG 夹具覆盖;这里只钉住「分档只给 JPEG」这条策略,防止把 PNG/BMP
* 也拖进未校准的「小图→落框」峰值路径。 */
{
char book3[MR_PATH_MAX];
snprintf(book3, sizeof book3, "./mr_perf_books_%ld/v3", (long)getpid());
int ok3 = mkdir_p(book3) == 0;
for (int i = 0; ok3 && i < 2; i++) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/t%d.bmp", book3, i + 1);
/* 1600×2400 = 3.84MP:量级与真机大页相同,但格式不是 JPEG。 */
if (write_bmp24(p, 1600, 2400, (unsigned)(i + 41)) != 0) ok3 = 0;
}
if (!ok3) {
printf(" SKIP: tier fixture write failed\n");
} else {
meow_view_page_pop(); /* 关掉 book2 */
uint64_t t_open = meow_ui_time_ms();
mr_reader_open_dir(book3);
uint64_t open_ms = meow_ui_time_ms() - t_open;
CHECK(mr_reader_ui_count() == 2, "tier book pages=%d", mr_reader_ui_count());
CHECK(open_ms < 500, "open returns without decoding on the UI thread (%llu ms)",
(unsigned long long)open_ms);
pump_ms(600);
draw_once();
CHECK(mr_reader_ui_focus_tier() == MR_TIER_HIGH,
"non-JPEG big page decodes straight to the target tier (tier=%d)",
mr_reader_ui_focus_tier());
CHECK(mr_reader_ui_low_decodes() == 0,
"no low tier for PNG/BMP (%d low decodes)", mr_reader_ui_low_decodes());
int bl = mr_reader_ui_blit_draws(), sc = mr_reader_ui_scaled_draws();
draw_once();
CHECK(mr_reader_ui_blit_draws() > bl && mr_reader_ui_scaled_draws() == sc,
"big BMP page draws as native blit (blit +%d, scaled +%d)",
mr_reader_ui_blit_draws() - bl, mr_reader_ui_scaled_draws() - sc);
}
}
meow_view_page_pop();
meow_view_free(reader);
meow_view_free(lib);