图库: 阅读器页槽改 RGB565 常驻,页图内存减半(M32)

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

No files matched your search

+184 -61
View File
@@ -15,8 +15,12 @@
* 宽恒为视口宽),不是屏长边,所以翻页/滚动热路径只有原生 blit:不做 meow_ui_img_scaled
* 的每帧重采样,也不再每帧 malloc 1MB 级临时缓冲。几何变化(fit/缩放/切模式)时按新几何
* 重解一次;拖动/吸附帧只 invalidate 内容区(顶栏静态不重绘)。
* 内存(M17 起;M22 按真机实测重定;M27 接真缩解后端):单页显示缓存 ≤3MB、整窗常驻
* ≤3MB(3 槽,超限淘汰最远页)。解码路径与峰值预算由 page_source 的 reader_estimate_peak()
* 内存(M17 起;M22 按真机实测重定;M27 接真缩解后端;M32 页槽改 RGB565):
* 槽里常驻的是**已落框的 565 位图**(2B/px),单页上限 1.5MB(旧 RGBA 3MB 的同几何口径)、
* 整窗常驻上限 3.2MB(3 槽 + 1 张在途,超限淘汰最远页)。真机 480×853 页 1599KB → 800KB,
* 两页常驻 3.2MB → 1.6MB。解码仍产出 RGBA,worker 当场转 565 再释放 RGBA;这笔暂存
* (2B/px)由 reader_estimate_peak 的 convert_bytes 计入预算。解码路径与峰值预算由
* page_source 的 reader_estimate_peak()
* 统一判定(唯一实现,本文件不再另写模型):
* - 真缩解后端(M23)能接住的页:峰值只与目标尺寸相关(≈1.3–2.5MB,与源分辨率无关),
* 直接放行,不做内存门控;
@@ -77,16 +81,25 @@
#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 里)----
* 真机参考:MemTotal 56872kB、空闲 RSS 6044kB、杀点 RSS≈15.6MB / MemFree 仅 1.4MB。 */
/* ---- 内存预算(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 /* 被预算跳过的页:焦点页冷却这么久后自动重试 */
#define MR_PAGE_BYTES_CAP (3L * 1024 * 1024) /* 单页「显示尺寸」缓存字节上限 */
#define MR_RESIDENT_CAP (3L * 1024 * 1024) /* 页图缓存总字节上限(M22 由 9MB 校准为 3MB) */
/* 单页缓存上限:旧口径是 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 后把在途结果
* 一起算进来,真实上限反而收紧。典型稳态(2 页)1.6MB,只有上限的一半。 */
#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;未取走的已解码图不计入常驻预算) */
#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
@@ -146,12 +159,19 @@ static int g_win_idx[MR_SLOTS];
static int g_win_n;
static int g_fail_paint; /* 窗口内出现解码失败页 → tick 重绘错误占位 */
/* ---- 页图缓存(≤4 张 downscale 后的图)---- */
/* ---- 页图缓存(≤3 槽,RGB565 常驻)----
* 槽里存的是**已落框**的 565 位图(2B/px),不是解码器的 RGBA:真机 480×853 页
* 1599KB → 800KB,两页常驻 3.2MB → 1.6MB。命中页绘制 = 每行一次 memcpy(M32)。 */
typedef struct page_bmp {
int w, h;
uint16_t *px; /* w*h,行优先,RGB565(不透明) */
} page_bmp;
typedef struct page_slot {
int idx; /* -1=空 */
meow_img *img; /* 已 downscale 的页图 */
int src_w, src_h;
int want_w, want_h; /* 本次解码按哪个显示框落的(几何判据,见 slot_ready) */
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];
@@ -185,6 +205,7 @@ 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)
{
@@ -242,7 +263,8 @@ typedef struct page_req {
typedef struct page_res {
int idx;
unsigned gen;
meow_img *img;
page_bmp *bmp; /* 已转 565 的页图(NULL = 失败/跳过) */
long bytes; /* bmp 的字节数:在途结果的预算口径(M32) */
int fail;
int skip; /* MR_SKIP_*:非 0 = 预算跳过(可重试,不是坏图) */
int want_w, want_h;
@@ -267,6 +289,10 @@ 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 期间抑制回调 */
@@ -352,11 +378,51 @@ static long mem_avail_kb(void)
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].img) n++;
if (g_slot[i].bmp) n++;
return n;
}
@@ -364,18 +430,30 @@ static long resident_bytes(void)
{
long b = 0;
for (int i = 0; i < MR_SLOTS; i++)
if (g_slot[i].img) b += (long)g_slot[i].src_w * g_slot[i].src_h * 4;
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 不超过预算」。 */
* 验收就是「离开时的 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",
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);
resident_pages(), resident_bytes() / 1024, pending_bytes() / 1024);
}
/* ============================================================ */
@@ -481,7 +559,7 @@ static meow_img *reader_load_page(const char *path, void *ud, int maxdim)
}
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,
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));
@@ -668,15 +746,15 @@ 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].img) return &g_slot[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;
if (g_slot[i].img) meow_img_free(g_slot[i].img);
g_slot[i].img = NULL;
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;
@@ -711,7 +789,7 @@ 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].img) continue;
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);
@@ -723,24 +801,29 @@ static int slot_evict_far(int except)
/* 开新页前先腾地方(任务 3):按「即将落地的单页字节」预判,先淘汰非可见/非焦点的旧图,
* 让解码缓冲不与陈旧常驻叠加。只在逼近上限时才动手,避免把预取邻居立刻扔掉
*(那样翻页会从「命中」退化成「重新解码」,反而增加峰值次数)。 */
*(那样翻页会从「命中」退化成「重新解码」,反而增加峰值次数)。
* M32:已解码未落槽的在途结果(pending_bytes)也是真实内存,一起算进常驻上限。 */
static void slots_reserve(long need_bytes)
{
if (need_bytes <= 0) return;
while (resident_bytes() + need_bytes > MR_RESIDENT_CAP) {
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 做几何判据。 */
static int slot_put(int idx, meow_img *img, int want_w, int want_h)
* want_w/want_h = 这次解码按哪个显示框落的,交给 slot_ready 做几何判据。
* bmp 的所有权移交本槽(失败时在这里释放)。 */
static int slot_put(int idx, page_bmp *bmp, int want_w, int want_h)
{
if (!img || idx < 0) return 0;
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].img) { i = k; break; }
if (!g_slot[k].img && empty < 0) empty = 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) {
@@ -752,25 +835,28 @@ static int slot_put(int idx, meow_img *img, int want_w, int want_h)
int d = abs(g_slot[k].idx - focus);
if (d > far_d) { far_d = d; i = k; }
}
if (i < 0) i = g_slot[0].img ? 0 : -1; /* 全可见:只能挤掉第一槽(极端条漫页) */
if (i < 0) i = g_slot[0].bmp ? 0 : -1; /* 全可见:只能挤掉第一槽(极端条漫页) */
}
}
if (i < 0) { meow_img_free(img); return 0; }
if (i < 0) { page_bmp_free(bmp); return 0; }
/* 总字节上限:新图放进去前先淘汰最远页,保证常驻图不超过预算。 */
long need = (long)meow_img_w(img) * meow_img_h(img) * 4;
/* 总字节上限:新图放进去前先淘汰最远页,保证常驻 + 在途不超过预算。 */
long need = page_bmp_bytes(bmp);
long pending = pending_bytes();
for (;;) {
long held = resident_bytes();
if (g_slot[i].img) held -= (long)g_slot[i].src_w * g_slot[i].src_h * 4;
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].img = img;
g_slot[i].src_w = meow_img_w(img);
g_slot[i].src_h = meow_img_h(img);
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;
@@ -856,8 +942,9 @@ static void queue_page(int idx)
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);
/* 先释放再解码:按这一页的落地字节先淘汰不可见旧图,别让常驻与解码缓冲叠加。 */
slots_reserve((long)box_w * box_h * 4);
/* 先释放再解码:按这一页的落地字节(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];
@@ -906,22 +993,31 @@ static void *reader_worker(void *arg)
g_decode_skip = MR_SKIP_NONE;
meow_img *img = mr_book_open(&g_book, req.idx);
g_decode_count++;
int skip = img ? MR_SKIP_NONE : g_decode_skip;
/* 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].img = img;
g_res[g_res_n].fail = (img || skip) ? 0 : 1;
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++;
img = NULL; /* 所有权已移交(失败/跳过时本就是 NULL) */
bmp = NULL; /* 所有权已移交(失败/跳过时本就是 NULL) */
}
pthread_mutex_unlock(&g_lock);
if (img) meow_img_free(img);
page_bmp_free(bmp);
}
return NULL;
}
@@ -944,7 +1040,7 @@ static void worker_stop(void)
g_worker_on = 0;
pthread_mutex_lock(&g_lock);
for (int i = 0; i < g_res_n; i++) meow_img_free(g_res[i].img);
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;
@@ -987,9 +1083,9 @@ 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; }
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;
@@ -1000,11 +1096,11 @@ static int results_drain(void)
continue;
}
if (in_window(got[i].idx)) {
if (slot_put(got[i].idx, got[i].img, got[i].want_w, got[i].want_h) &&
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 {
meow_img_free(got[i].img);
page_bmp_free(got[i].bmp);
}
}
return changed;
@@ -1176,8 +1272,8 @@ static int pan_mode(void)
return 0;
}
/* 画一页(带水平位移);解码尺寸==显示尺寸时走原生 blit(翻页热路径零重采样),
* 只在 fit/zoom 变化或极端长页(超单页缓存上限)时才退回最近邻缩放。 */
/* 画一页(带水平位移);页图尺寸==显示尺寸时走原生 565 blit(翻页热路径零重采样、
* 每行一次 memcpy),只在 fit/zoom 变化或极端长页(超单页缓存上限)时才退回最近邻缩放。 */
static void draw_page_at(int idx, int off_x, int off_y)
{
page_slot *s = slot_find(idx);
@@ -1188,16 +1284,17 @@ static void draw_page_at(int idx, int off_x, int off_y)
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_ui_img(x + (w - s->src_w) / 2, y + (h - s->src_h) / 2, s->img);
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_ui_img_scaled(x, y, w, h, s->img);
meow_gfx_blit_rgb565_scaled(x, y, w, h, s->src_w, s->src_h, s->bmp->px);
NOTE_SCALED();
}
}
/* 条漫:按滚动偏移画可见页。典型 2:3 页的解码尺寸与显示尺寸相等 → 原生 blit;
* 其余按最近邻缩放(缩放在 SDK 内部申请目标尺寸临时缓冲,单页峰值受控)。 */
/* 条漫:按滚动偏移画可见页。典型 2:3 页的页图尺寸与显示尺寸相等 → 原生 565 blit;
* 其余按最近邻缩放(gfx 内直接采样,不再申请目标尺寸临时缓冲)。 */
static void draw_strip(const meow_theme *th)
{
if (!g_strip_ok) {
@@ -1212,13 +1309,13 @@ static void draw_strip(const meow_theme *th)
if (h <= 0) continue;
page_slot *s = slot_find(i);
if (s) {
/* 解码=显示尺寸时原生 blit:gfx 只处理屏幕上可见的行,
/* 页图=显示尺寸时原生 blit:gfx 只处理屏幕上可见的行,
* 长条页只露一截时不再整页重采样(M7 finding 的子矩形 blit 诉求)。 */
if (abs(s->src_w - g_vw) <= MR_SLOT_TOL && abs(s->src_h - h) <= MR_SLOT_TOL) {
meow_ui_img((g_vw - s->src_w) / 2, y, s->img);
meow_gfx_blit_rgb565((g_vw - s->src_w) / 2, y, s->src_w, s->src_h, s->bmp->px);
NOTE_BLIT();
} else {
meow_ui_img_scaled(0, y, g_vw, h, s->img);
meow_gfx_blit_rgb565_scaled(0, y, g_vw, h, s->src_w, s->src_h, s->bmp->px);
NOTE_SCALED();
}
continue;
@@ -1507,11 +1604,13 @@ static void page_flip(int delta)
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);
}
@@ -1762,7 +1861,7 @@ static int open_book(const char *dir, int start_page, int mode)
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, first, ww, wh);
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();
@@ -1845,8 +1944,10 @@ static void drag_finish(void)
}
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);
@@ -2519,6 +2620,14 @@ long long mr_reader_ui_decode_peak_kb(const char *path, int step, int box_w, int
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); }
@@ -2527,6 +2636,20 @@ 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)
{
+12 -2
View File
@@ -37,7 +37,7 @@ int mr_reader_ui_page(void); /* 当前页(条漫=视口顶端所在页
int mr_reader_ui_count(void); /* 当前册页数 */
int mr_reader_ui_jump_open(void); /* 1=跳页滑块已展开 */
int mr_reader_ui_jump_pending(void);/* 1=跳页目标等待解码 */
long mr_reader_ui_resident_kb(void);/* 驻留页图字节(KB) */
long mr_reader_ui_resident_kb(void);/* 槽内常驻页图字节(KB,RGB565:2 字节/像素) */
int mr_reader_ui_failed_pages(void);/* 解码失败(坏图)页数 */
int mr_reader_ui_skipped_pages(void);/* 被内存预算跳过(可重试)的页数 */
/* 注入 /proc/meminfo 读数(kB);-1 = 恢复真实读取。用于确定性验证内存门控与放宽档。 */
@@ -45,9 +45,19 @@ void mr_reader_ui_test_set_mem(long free_kb, long avail_kb);
/* 峰值模型自检:用产品代码里的同一个模型算预估峰值(kB),供宿主测试与 --wrap malloc
* 实测对照。0=探测失败。 */
long long mr_reader_ui_decode_peak_kb(const char *path, int step, int box_w, int box_h);
/* M32:同一次取页里的 565 落槽暂存(kB,= 2×tw×th);与 decode_peak 分开报,便于
* 「解码峰值」与「落槽暂存」各自对照实测。 */
long long mr_reader_ui_decode_convert_kb(const char *path, int step, int box_w, int box_h);
long mr_reader_ui_peak_cap_kb(void); /* 常态峰值上限(kB) */
long mr_reader_ui_peak_budget_kb(void); /* 当前 MemFree 下的实际预算(kB) */
long mr_reader_ui_resident_cap_kb(void); /* 常驻上限(kB) */
long mr_reader_ui_resident_cap_kb(void); /* 常驻上限(kB;含在途结果) */
/* ---- M32 RGB565 页槽预算口径 ---- */
long mr_reader_ui_pending_kb(void); /* 已解码、还没落槽的在途结果字节(KB) */
long mr_reader_ui_page_px_cap(void); /* 单页缓存像素上限(旧 RGBA 3MB 的同几何口径) */
long mr_reader_ui_slot_kb(int idx); /* 指定槽内页图的常驻字节(KB,565);0=不在槽内 */
/* 翻页命中/未命中(观测口,用于验证「命中率不退化」):目标页已在槽 = 预取生效。 */
int mr_reader_ui_flip_hits(void);
int mr_reader_ui_flip_misses(void);
int mr_reader_ui_blit_draws(void); /* 累计原生 blit 绘页次数(热路径应为纯 blit) */
int mr_reader_ui_scaled_draws(void);/* 累计重采样绘页次数(每帧重采样=卡顿) */
int mr_reader_ui_decode_count(void);/* 累计解码次数(查重解抖动/活锁) */