图库: 阅读器页槽改 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

+306 -23
View File
@@ -31,6 +31,7 @@
#include "meow_ui_internal.h"
#include "logic/reader/nav.h"
#include "logic/reader/page_source.h"
#include "logic/zip/zip.h" /* M32:CBZ 排版读数的对照(mz_read_alloc = 旧路径) */
#include "stb_image.h" /* stbi_load_scaled:两条路径输出尺寸一致性对照 */
#include <dirent.h>
@@ -39,6 +40,7 @@
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <zlib.h> /* M32:CBZ 夹具的 deflate 条目 */
/* ------------------------------------------------------------------ */
/* 分配打点:--wrap malloc/free 家族(与 sdk/test_meow_img_budget.c 同法)*/
@@ -313,6 +315,108 @@ static int write_png_rgb(const char *path, int w, int h, unsigned seed)
return ok ? 0 : -1;
}
/* 最小 CBZ 写入器:把同一个源文件写成 count 个 pNNN.bmp 条目。
* deflate=0:stored(未压缩,用来量「整页解压」的分配峰值);deflate=1:method 8(真实
* CBZ 的形态,用来量前缀读省下的 inflate CPU/字节)。 */
static int write_zip_cbz(const char *zip_path, const char *src, int count, int deflate_it)
{
FILE *in = fopen(src, "rb");
if (!in) return -1;
if (fseek(in, 0, SEEK_END) != 0) { fclose(in); return -1; }
long sz = ftell(in);
if (sz <= 0 || fseek(in, 0, SEEK_SET) != 0) { fclose(in); return -1; }
unsigned char *data = (unsigned char *)malloc((size_t)sz);
if (!data || fread(data, 1, (size_t)sz, in) != (size_t)sz) {
free(data);
fclose(in);
return -1;
}
fclose(in);
unsigned crc = crc32_buf(data, (size_t)sz);
/* 可选 deflate(raw,-15 窗口,与 zip 一致)。 */
unsigned char *packed = NULL;
size_t packed_len = (size_t)sz;
if (deflate_it) {
z_stream zs;
memset(&zs, 0, sizeof zs);
if (deflateInit2(&zs, 1, Z_DEFLATED, -MAX_WBITS, 8, Z_DEFAULT_STRATEGY) != Z_OK) {
free(data);
return -1;
}
size_t cap = (size_t)deflateBound(&zs, (uLong)sz);
packed = (unsigned char *)malloc(cap);
if (!packed) { deflateEnd(&zs); free(data); return -1; }
zs.next_in = data;
zs.avail_in = (uInt)sz;
zs.next_out = packed;
zs.avail_out = (uInt)cap;
int zr = deflate(&zs, Z_FINISH);
packed_len = cap - zs.avail_out;
deflateEnd(&zs);
if (zr != Z_STREAM_END) { free(packed); free(data); return -1; }
}
const unsigned char *body = deflate_it ? packed : data;
unsigned body_len = (unsigned)packed_len;
unsigned method = deflate_it ? 8u : 0u;
FILE *f = fopen(zip_path, "wb");
if (!f) { free(packed); free(data); return -1; }
unsigned *offs = (unsigned *)calloc((size_t)count, sizeof *offs);
if (!offs) { free(packed); free(data); fclose(f); return -1; }
int ok = 1;
unsigned off = 0;
for (int i = 0; i < count && ok; i++) {
char name[32];
int nl = snprintf(name, sizeof name, "p%03d.bmp", i);
unsigned char lh[30] = { 0 };
put32(lh, 0x04034b50u);
put16(lh + 4, 20); /* version needed */
put16(lh + 8, (unsigned short)method); /* stored / deflate */
put32(lh + 14, crc);
put32(lh + 18, body_len); /* comp size */
put32(lh + 22, (unsigned)sz); /* uncomp size */
put16(lh + 26, (unsigned short)nl);
offs[i] = off;
if (fwrite(lh, 1, sizeof lh, f) != sizeof lh) ok = 0;
if (ok && fwrite(name, 1, (size_t)nl, f) != (size_t)nl) ok = 0;
if (ok && fwrite(body, 1, body_len, f) != body_len) ok = 0;
off += (unsigned)sizeof lh + (unsigned)nl + body_len;
}
unsigned cd_off = off;
for (int i = 0; i < count && ok; i++) {
char name[32];
int nl = snprintf(name, sizeof name, "p%03d.bmp", i);
unsigned char cd[46] = { 0 };
put32(cd, 0x02014b50u);
put16(cd + 4, 20); /* version made by */
put16(cd + 6, 20); /* version needed */
put16(cd + 10, (unsigned short)method);
put32(cd + 16, crc);
put32(cd + 20, body_len);
put32(cd + 24, (unsigned)sz);
put16(cd + 28, (unsigned short)nl);
put32(cd + 42, offs[i]);
if (fwrite(cd, 1, sizeof cd, f) != sizeof cd) ok = 0;
if (ok && fwrite(name, 1, (size_t)nl, f) != (size_t)nl) ok = 0;
off += (unsigned)sizeof cd + (unsigned)nl;
}
unsigned char eocd[22] = { 0 };
put32(eocd, 0x06054b50u);
put16(eocd + 8, (unsigned short)count);
put16(eocd + 10, (unsigned short)count);
put32(eocd + 12, off - cd_off);
put32(eocd + 16, cd_off);
if (ok && fwrite(eocd, 1, sizeof eocd, f) != sizeof eocd) ok = 0;
free(offs);
free(packed);
free(data);
if (fclose(f) != 0) ok = 0;
return ok ? 0 : -1;
}
static int mkdir_p(const char *path)
{
char tmp[MR_PATH_MAX];
@@ -414,6 +518,39 @@ static size_t measure_page_decode(const char *path, const mr_img_info *in, int s
return peak;
}
/* M32:与阅读器 worker 完全同路径的「一次取页」——解码 → 落框 → 转 RGB565 → 释放 RGBA。
* 返回这段操作的真实分配峰值,用来校验「解码峰值 peak_bytes + 落槽暂存 convert_bytes」
* 这一对预算口径(转换期间 RGBA 与 565 同时存活,正是 convert_bytes 建模的那笔内存)。 */
static size_t measure_page_565(const char *path, const mr_img_info *in, int step,
int allow_stb, int box_w, int box_h, int *ow, int *oh)
{
int dim = box_w > box_h ? box_w : box_h;
size_t base = g_live;
peak_reset();
int used = 0;
meow_img *im = meow_img_load_file_step_ex(path, dim, step, allow_stb, &used);
if (im && (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; }
}
if (im && in && 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; }
}
if (im) {
int w = meow_img_w(im), h = meow_img_h(im);
uint16_t *px = (uint16_t *)malloc((size_t)w * h * sizeof *px);
if (px) {
meow_gfx_rgba_to_rgb565(meow_img_pixels(im), px, w * h);
if (ow && oh) { *ow = w; *oh = h; }
free(px);
}
meow_img_free(im);
}
return peak_since(base);
}
int main(void)
{
const char *base = getenv("TMPDIR");
@@ -461,16 +598,18 @@ int main(void)
}
int have_jpeg = gen_jpeg_fixtures(jpgbook);
/* 连翻 30 页:6 类典型页循环(12MP / 4MP / baseline / 渐进 / PNG / BMP)。
* 每类都必须走后端且不占位——这是「修好了却用不上」的回归防线。 */
/* 顺序前翻 30 页:6 类典型页循环(12MP / 4MP / baseline / 渐进 / PNG / BMP)。
* 每类都必须走后端且不占位——这是「修好了却用不上」的回归防线。
* M32:夹具 31 页,保证 30 次顺序前翻不触发册间衔接(末页再前翻会开相邻册)。 */
static const char *const g_mix[] = {
"base2000.jpg", "prog2000.jpg", "page1600.png",
"page1600.bmp", "mp4.jpg", "mp12.jpg",
};
const int mix_n = (int)(sizeof g_mix / sizeof g_mix[0]);
const int mixed_goal = 31;
int mixed_pages = 0;
if (have_jpeg) {
for (int i = 0; i < 30; i++) {
for (int i = 0; i < mixed_goal; i++) {
char src[MR_PATH_MAX], dst[MR_PATH_MAX];
snprintf(src, sizeof src, "%s/%s", jpgbook, g_mix[i % mix_n]);
snprintf(dst, sizeof dst, "%s/p%02d_%s", mixed, i, g_mix[i % mix_n]);
@@ -513,12 +652,21 @@ int main(void)
size_t measured_kb = peak_since(b0) / 1024 + 1;
int got = im ? 1 : 0;
if (im) meow_img_free(im);
INFO("png 600x960: model=%lldkB measured=%zukB decoded=%d", model, measured_kb, got);
long long conv = mr_reader_ui_decode_convert_kb(p, 1, 480, 768);
size_t measured_565 = measure_page_565(p, NULL, 1, 1, 480, 768, NULL, NULL) / 1024 + 1;
INFO("png 600x960: model=%lldkB measured=%zukB conv=%lldkB measured565=%zukB decoded=%d",
model, measured_kb, conv, measured_565, got);
CHECK(got, "png fixture decodes (writer valid)");
CHECK(model >= (long long)measured_kb, "model is an upper bound (%lldkB < %zukB)",
model, measured_kb);
CHECK(model <= (long long)measured_kb * 2, "model not wildly pessimistic (%lldkB vs %zukB)",
model, measured_kb);
/* M32:565 落槽暂存 = 2×tw×th(480×768 → 720KB),且「解码+落槽」总量仍是上界。 */
CHECK(conv == 480 * 768 * 2 / 1024, "565 落槽暂存 = 2×w×h (%lldkB)", conv);
CHECK(model + conv >= (long long)measured_565,
"解码+落槽 模型 ≥ 实测 (%lld+%lld < %zukB)", model, conv, measured_565);
CHECK(model + conv <= (long long)measured_565 * 2,
"解码+落槽 模型不离谱保守 (%lld+%lld vs %zukB)", model, conv, measured_565);
}
/* ---- B5 内存门控:MemAvailable 不足 → 不解码、占位(可重试)---- */
@@ -554,9 +702,27 @@ int main(void)
mr_reader_open_dir(book);
pump_ms(800);
int w = meow_ui_width();
/* 预热两翻(M32):首次进页窗口只有「焦点 + 下一页」(2 页),稳定态是焦点 ±1(3 页);
* 565 下第 3 页只要 600KB,会正常驻留下来。不预热就会把「多缓存一张预取页」
* 误判成内存漂移。 */
tap(40, 300);
pump_ms(200);
tap(w - 40, 300);
pump_ms(200);
int d0 = mr_reader_ui_decode_count();
long long resident_cap = mr_reader_ui_resident_cap_kb();
long long cap = mr_reader_ui_peak_cap_kb();
long total_resident_max = 0;
/* M32 预算自洽(按 565 重算,M31 两档已回滚 → 单档):常驻上限 = 3 槽 + 1 张在途
* = 4 × 800KB(真机 480×853 页 @565);单页像素上限沿用 RGBA 3MB 的同几何口径
* (显示清晰度不变,只把每像素 4B 变 2B);单次取页仍受 page_source 的 3MB 常态
* 预算 / 12MB 硬上限约束(reader_estimate_peak 的 peak_bytes + convert_bytes)。 */
CHECK(resident_cap == 4 * 800, "常驻上限按 565 重算(%lldKB = 4×800KB)", resident_cap);
CHECK(mr_reader_ui_page_px_cap() == 786432,
"单页像素上限沿用 RGBA 3MB 的同几何口径(%ldpx)", mr_reader_ui_page_px_cap());
CHECK(cap == 3 * 1024, "单次解码常态预算 3MB(%lldKB)", cap);
CHECK(resident_cap + cap <= 12 * 1024,
"常驻 + 一次取页 ≤ 12MB 包络(%lld + %lld)", resident_cap, cap);
/* 基线 = 首屏稳定后的存活字节;之后任何分配都不该再超过 基线+3MB(= 单页解码峰值)。 */
size_t live_steady = g_live;
size_t peak_steady = g_peak;
@@ -569,17 +735,22 @@ int main(void)
if (mr_reader_ui_skipped_pages() > skipped_max) skipped_max = mr_reader_ui_skipped_pages();
meow_ui_paint_full();
long kb = mr_reader_ui_resident_kb();
if (kb > resident_cap) {
long pend = mr_reader_ui_pending_kb();
if (kb + pend > resident_cap) {
g_fail++;
printf(" FAIL flip %d: resident %ldkB > cap %lldkB\n", i, kb, resident_cap);
printf(" FAIL flip %d: resident %ldkB + pending %ldkB > cap %lldkB\n",
i, kb, pend, resident_cap);
break;
}
if (kb > total_resident_max) total_resident_max = kb;
}
size_t transient = g_peak > peak_steady ? g_peak - peak_steady : 0;
size_t live_end = g_live;
int decodes = mr_reader_ui_decode_count() - d0;
INFO("100 flips: steady_live=%zuKB peak_transient=%zuKB end_live=%zuKB cap=%lldKB decodes=%d advanced=%d",
live_steady / 1024, transient / 1024, live_end / 1024, cap, decodes, advanced);
INFO("100 flips: steady_live=%zuKB peak_transient=%zuKB end_live=%zuKB cap=%lldKB "
"resident_max=%ldKB decodes=%d advanced=%d",
live_steady / 1024, transient / 1024, live_end / 1024, resident_cap,
total_resident_max, decodes, advanced);
CHECK(advanced == 100, "every flip advanced the page (%d/100)", advanced);
CHECK(transient <= (size_t)cap * 1024 + 512 * 1024,
"no allocation above steady+3MB over 100 flips (%zuKB)", transient / 1024);
@@ -591,6 +762,10 @@ int main(void)
mr_reader_ui_failed_pages());
/* 取消/预取纪律:100 次翻页的解码次数必须有界(M20 实测曾 1.75× 浪费)。 */
CHECK(decodes <= 100 * 2, "decode count bounded over 100 flips (%d)", decodes);
/* M32:槽内页图是 565(2B/px)。480×640 页 = 600KB(RGBA 时代 1200KB)。 */
CHECK(mr_reader_ui_slot_kb(mr_reader_ui_page()) == 600,
"单页常驻 = w×h×2 = 600KB(实测 %ldKB)",
mr_reader_ui_slot_kb(mr_reader_ui_page()));
meow_view_page_pop();
pump_ms(200);
}
@@ -617,6 +792,87 @@ int main(void)
pump_ms(200);
}
/* ---- B12 M32:500 页 CBZ 排版只读条目**前缀**(4KB),不再整页解压 ----
* 改前:zip_ops_meta 走 mz_read_alloc(整条目解压)→ 500 页 = 500 次全量解压;
* 改后:mz_read_prefix(stored 只读前 4KB / deflate 解到 4KB 即停)。
* 两个夹具:stored 量「每页分配峰值」(旧 27KB/页 → 新 0);deflate 是真实 CBZ 形态,
* 量前缀读省下的 inflate 字节与耗时。 */
{
char zsrc[MR_PATH_MAX];
snprintf(zsrc, sizeof zsrc, "%s/zip_src.bmp", root);
int zok = write_bmp24(zsrc, 96, 96, 7) == 0; /* 96×96 24bpp = 27648B 像素数据 */
CHECK(zok, "CBZ 夹具源图写入");
for (int mode = 0; zok && mode < 2; mode++) {
int deflate = mode; /* 0=stored, 1=deflate */
char zcbz[MR_PATH_MAX];
snprintf(zcbz, sizeof zcbz, "%s/layout500_%s.cbz", root,
deflate ? "deflate" : "stored");
if (write_zip_cbz(zcbz, zsrc, 500, deflate) != 0) {
CHECK(0, "500 页 CBZ 夹具写入(%s)", deflate ? "deflate" : "stored");
continue;
}
mr_book_handle zh;
mr_book zb;
int zn = mr_book_open_auto(&zb, &zh, zcbz, 720, NULL, NULL, NULL);
CHECK(zn == 500, "CBZ 页数=%d(%s)", zn, deflate ? "deflate" : "stored");
if (zn != 500) { mr_book_close_auto(&zb, &zh); continue; }
/* 旧路径读数:整条目读(= 改前 zip_ops_meta 的做法),量每页分配峰值与总耗时。 */
size_t old_page_peak = 0;
uint64_t t_old = meow_ui_time_ms();
const mz_entry *e0 = mz_entry_at((mz_archive *)zh.u.zip.arch, 0);
for (int i = 0; i < 500 && e0; i++) {
void *buf = NULL;
size_t len = 0;
size_t b0 = g_live;
peak_reset();
if (mz_read_alloc((mz_archive *)zh.u.zip.arch, e0, &buf, &len) == MZ_OK) {
size_t pk = peak_since(b0);
if (pk > old_page_peak) old_page_peak = pk;
}
free(buf);
}
uint64_t ms_old = (uint64_t)meow_ui_time_ms() - t_old;
/* 新路径:与条漫排版同一条调用(mr_book_meta 逐页)。
* 第一页会一次性分配 meta 缓存(2×500×4B = 4000B),基线放在它之后,
* 这样量到的就是「每页前缀读」自己的分配。 */
int pw0 = 0, ph0 = 0;
int first_ok = mr_book_meta(&zb, 0, &pw0, &ph0);
uint64_t t_new = meow_ui_time_ms();
size_t base = peak_reset();
int meta_ok = first_ok, dim_ok = (pw0 == 96 && ph0 == 96);
for (int i = 1; i < 500; i++) {
int pw = 0, ph = 0;
if (!mr_book_meta(&zb, i, &pw, &ph)) meta_ok = 0;
else if (pw != 96 || ph != 96) dim_ok = 0;
}
size_t new_peak = peak_since(base);
uint64_t ms_new = (uint64_t)meow_ui_time_ms() - t_new;
long long bytes_old = 500LL * (long long)(e0 ? e0->uncomp_size : 0);
long long bytes_new = 500LL * 4096; /* 上限:每页最多读 4KB */
INFO("CBZ 500 页[%s]:旧=整页解压 %lldkB/页·总%lldkB 峰值%zuKB 耗时%llums;"
"新=前缀 %lldkB/页·总%lldkB 峰值%zuB 耗时%llums",
deflate ? "deflate" : "stored",
e0 ? (long long)e0->uncomp_size / 1024 : 0LL, bytes_old / 1024,
old_page_peak / 1024, (unsigned long long)ms_old,
4LL, bytes_new / 1024, new_peak, (unsigned long long)ms_new);
CHECK(meta_ok, "[%s] 500 页 meta 全部探测成功", deflate ? "deflate" : "stored");
CHECK(dim_ok, "[%s] 500 页尺寸都取到 96x96", deflate ? "deflate" : "stored");
CHECK(new_peak <= 4096,
"[%s] 排版期(首帧之后)每页堆峰值 ≤4KB(实测 %zuB)",
deflate ? "deflate" : "stored", new_peak);
CHECK(old_page_peak >= 16 * 1024,
"[%s] 对照:旧路径每页分配 ≥16KB(实测 %zuB)",
deflate ? "deflate" : "stored", old_page_peak);
CHECK(bytes_new < bytes_old / 4,
"[%s] 前缀读总字节 < 旧路径 1/4(%lldkB vs %lldkB)",
deflate ? "deflate" : "stored", bytes_new / 1024, bytes_old / 1024);
mr_book_close_auto(&zb, &zh);
}
}
/* ---- B7 M27:6 类典型页 —— 全部走后端、不占位、模型 ≥ 实测且 ≤1.5× ---- */
if (have_jpeg) {
static const char *const typical[] = {
@@ -655,6 +911,19 @@ int main(void)
typical[i]);
CHECK(ow == box_w && oh == box_h, "%s 输出 %dx%d == 显示框 %dx%d",
typical[i], ow, oh, box_w, box_h);
/* M32:565 落槽暂存(2B/px)与解码峰值分开建模,但同属一次取页;
* 两者之和必须是「解码 → 转 565」真实峰值的上界(且不保守得离谱)。 */
size_t measured565 = measure_page_565(p, &in, dec.step, 1, box_w, box_h,
NULL, NULL) + 1;
CHECK(dec.convert_bytes == (long long)dec.tw * dec.th * 2,
"%s 落槽暂存 = 2×tw×th (%lldB, %dx%d)", typical[i], dec.convert_bytes,
dec.tw, dec.th);
CHECK(dec.peak_bytes + dec.convert_bytes >= (long long)measured565,
"%s 解码+落槽 模型 ≥ 实测 (%lld+%lldkB < %zukB)", typical[i],
dec.peak_bytes / 1024, dec.convert_bytes / 1024, measured565 / 1024);
CHECK(dec.peak_bytes + dec.convert_bytes <= (long long)measured565 * 3 / 2,
"%s 解码+落槽 模型 ≤1.5× 实测 (%lldkB vs %zukB)", typical[i],
(dec.peak_bytes + dec.convert_bytes) / 1024, measured565 / 1024);
}
/* 输出尺寸口径:后端直出 == 「stbi_load_scaled(step) + 落框精修」旧路径
* (典型页必须逐像素同尺寸;源尺寸不是档位整数倍时 M23 已记录 1px 舍入口径差)。 */
@@ -762,37 +1031,51 @@ int main(void)
printf(" SKIP jpeg assertions: Pillow unavailable (python3 -c 'import PIL')\n");
}
/* ---- B8 M27:连翻 30 页(6 类典型页循环)——不掉页、不占位、峰值 ≤3MB ---- */
if (have_jpeg && mixed_pages == 30) {
/* ---- B8 M27/M32:顺序前翻 30 页(31 页册,6 类典型页循环)——不掉页、不占位、
* 峰值不增长、预取命中率、解码次数有界,且常驻按 565 口径有界 ---- */
if (have_jpeg && mixed_pages == mixed_goal) {
mr_reader_ui_test_set_mem(4096, 16000);
mr_reader_open_dir(mixed);
pump_ms(1200);
CHECK(mr_reader_ui_count() == 30, "mixed book pages=%d", mr_reader_ui_count());
int w = meow_ui_width();
CHECK(mr_reader_ui_count() == mixed_goal, "mixed book pages=%d", mr_reader_ui_count());
int advanced = 0, skipped_max = 0;
size_t live_steady = g_live;
int d0 = mr_reader_ui_decode_count();
/* P2-3:只在窗口稳定后 reset 一次,之后不再 reset——这样淘汰旧图不会掩盖
* 「分配新图 + 解码」的真实峰值;指标 = 整个 30 翻窗口内相对稳态基线的最大存活增量。
* (旧实现每翻页 reset 一次,淘汰把基线拉低,报告里的 536KB 是伪值。) */
int hits0 = mr_reader_ui_flip_hits(), miss0 = mr_reader_ui_flip_misses();
/* 只在窗口稳定后 reset 一次,之后不再 reset——这样淘汰旧图不会掩盖
* 「分配新图 + 解码」的真实峰值;指标 = 整个窗口内相对稳态基线的最大存活增量。 */
size_t base = peak_reset();
for (int i = 0; i < 30; i++) {
int before = mr_reader_ui_page();
tap(i % 2 == 0 ? 40 : w - 40, 300);
pump_ms(220);
if (mr_reader_ui_page() != before) advanced++;
tap(40, 300); /* RTL:左 1/3 = 下一页(顺序前翻) */
pump_ms(260);
if (mr_reader_ui_page() == before + 1) advanced++;
if (mr_reader_ui_skipped_pages() > skipped_max)
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 "
"resident=%ldKB max_live_delta=%zuKB",
advanced, skipped_max, mr_reader_ui_decode_count() - d0,
live_steady / 1024, mr_reader_ui_resident_kb(), transient / 1024);
CHECK(advanced == 30, "30 页全部翻到 (%d/30)", advanced);
int hits = mr_reader_ui_flip_hits() - hits0;
int miss = mr_reader_ui_flip_misses() - miss0;
int decodes = mr_reader_ui_decode_count() - d0;
INFO("30 sequential flips: advanced=%d last=%d hits=%d miss=%d skipped=%d decodes=%d "
"steady=%zuKB resident=%ldKB pending=%ldKB max_live_delta=%zuKB",
advanced, mr_reader_ui_page(), hits, miss, skipped_max, decodes,
live_steady / 1024, mr_reader_ui_resident_kb(), mr_reader_ui_pending_kb(),
transient / 1024);
CHECK(advanced == 30 && mr_reader_ui_page() == 30,
"30 次顺序前翻逐页推进(advanced=%d last=%d)", advanced, mr_reader_ui_page());
CHECK(skipped_max == 0, "典型页连翻无占位 (max=%d)", skipped_max);
CHECK(mr_reader_ui_failed_pages() == 0, "无永久失败页 (%d)",
mr_reader_ui_failed_pages());
/* 顺序阅读的预取命中率:目标页已在槽(预取生效)≥90%。 */
CHECK(hits >= 27, "顺序翻页预取命中率 ≥90%%(hit=%d miss=%d)", hits, miss);
/* 单档(M31 已回滚):每页最多 1 次解码 + 开册/补邻居余量。 */
CHECK(decodes <= 30 + 15, "30 次翻页解码次数 ≤1/页+余量 (decodes=%d)", decodes);
CHECK(mr_reader_ui_resident_kb() + mr_reader_ui_pending_kb() <=
mr_reader_ui_resident_cap_kb(),
"常驻 + 在途 ≤ 上限 (%ldKB + %ldKB ≤ %ldKB)",
mr_reader_ui_resident_kb(), mr_reader_ui_pending_kb(),
mr_reader_ui_resident_cap_kb());
CHECK(transient <= 3 * 1024 * 1024,
"30 页连翻任何时刻存活增量 ≤3MB (%zuKB)", transient / 1024);
meow_view_page_pop();
+172 -1
View File
@@ -15,7 +15,11 @@
* P5 「先点适高,再切条漫」:scaled 不增长(评审点名的第二入口);
* P6 双击放大再双击缩回:缩回后按新几何重解 → scaled 不增长(P2-1);
* P7 静止若干帧不产生新解码(无重解抖动/活锁,P2-2);
* P8 驻留页图有字节上界(<= 9MB)。
* P8 驻留页图有字节上界(<= 9MB);
* P12(M32)颜色正确性:页槽改 RGB565 后,
* a) 565 blit 与 RGBA blit 在 565 屏上**逐像素一致**(同一套量化 → 不引入色偏);
* b) 565 量化误差在理论上界内(R/B ≤7、G ≤3),并给出 RMSE 与代表性采样;
* c) 8bit 灰阶梯/肤色渐变单调、无新增色带(与 RGBA 路径的阶梯数完全相同)。
*
* 编译:test/run_tests.sh(与 test_open_book_host 同口径;只发布 meow_ui.a 时 SKIP)。
*/
@@ -25,6 +29,7 @@
#include "logic/reader/nav.h"
#include <dirent.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -83,6 +88,50 @@ static int write_bmp24(const char *path, int w, int h, unsigned seed)
return ok ? 0 : -1;
}
/* 颜色测试夹具:mode 0 = 8bit 灰阶梯(x 方向 0→255);mode 1 = 肤色渐变(R/G/B 同向缓降)。 */
static int write_bmp24_grad(const char *path, int w, int h, int mode)
{
int stride = ((w * 3 + 3) / 4) * 4;
int imgsz = stride * h;
unsigned char fh[14] = { 0 }, ih[40] = { 0 };
fh[0] = 'B'; fh[1] = 'M';
put32(fh + 2, (unsigned)(54 + imgsz));
put32(fh + 10, 54);
put32(ih + 0, 40);
put32(ih + 4, (unsigned)w);
put32(ih + 8, (unsigned)h);
put16(ih + 12, 1);
put16(ih + 14, 24);
put32(ih + 20, (unsigned)imgsz);
FILE *f = fopen(path, "wb");
if (!f) return -1;
int ok = fwrite(fh, 1, sizeof fh, f) == sizeof fh &&
fwrite(ih, 1, sizeof ih, f) == sizeof ih;
unsigned char *row = (unsigned char *)calloc(1, (size_t)stride);
if (!row) ok = 0;
for (int y = 0; ok && y < h; y++) {
for (int x = 0; x < w; x++) {
int r, g, b;
if (mode == 0) {
int v = w > 1 ? x * 255 / (w - 1) : 0;
r = g = b = v;
} else {
r = 235 - (x * 65) / (w > 1 ? w - 1 : 1);
g = 175 - (x * 60) / (w > 1 ? w - 1 : 1);
b = 130 - (x * 45) / (w > 1 ? w - 1 : 1);
}
row[x * 3 + 0] = (unsigned char)b; /* BMP 是 BGR */
row[x * 3 + 1] = (unsigned char)g;
row[x * 3 + 2] = (unsigned char)r;
}
if (fwrite(row, 1, (size_t)stride, f) != (size_t)stride) ok = 0;
}
free(row);
if (fclose(f) != 0) ok = 0;
return ok ? 0 : -1;
}
static int mkdir_p(const char *path)
{
char tmp[MR_PATH_MAX];
@@ -156,6 +205,125 @@ static void mode_toggle(void)
static void draw_once(void) { meow_ui_paint_full(); }
/* ---- P12(M32):RGB565 页槽颜色正确性 ----
* 同一张图用两条路径各画一次,逐像素比较 back_buf(565 屏):
* a) RGBA 路径 meow_ui_img → put_px → rgb565()(今天的上屏方式)
* b) 565 路径 meow_gfx_blit_rgb565(页槽新路径)
* 两条路径本该逐像素相同(同一个量化函数);再量 565 相对 8bit 源的量化误差,
* 确认不出界、灰阶梯单调(不新增色带)。 */
static void capture565(int x, int y, int w, int h, uint16_t *out)
{
int pitch = meow_ui_back_buf_pitch();
unsigned char *bb = meow_ui_back_buf();
for (int yy = 0; yy < h; yy++)
for (int xx = 0; xx < w; xx++)
out[(size_t)yy * w + xx] =
*(const uint16_t *)(bb + (size_t)(y + yy) * pitch + (size_t)(x + xx) * 2);
}
static int ex5i(int v) { return (v << 3) | (v >> 2); }
static int ex6i(int v) { return (v << 2) | (v >> 4); }
static void color_check(void)
{
const char *dir = getenv("MEOW_DATA_DIR");
if (!dir || !dir[0]) dir = ".";
if (meow_ui_back_buf_bpp() != 16) {
printf(" SKIP color check: host back_buf is %d bpp (only 565 screens are comparable)\n",
meow_ui_back_buf_bpp());
return;
}
struct { const char *name; int w, h, mode; const char *label; } fx[2] = {
{ "color_gray.bmp", 256, 64, 0, "8bit 灰阶梯 256→0..255" },
{ "color_skin.bmp", 480, 360, 1, "肤色渐变 235,175,130 → 170,115,85" },
};
for (size_t k = 0; k < sizeof fx / sizeof fx[0]; k++) {
char path[MR_PATH_MAX];
snprintf(path, sizeof path, "%s/%s", dir, fx[k].name);
if (write_bmp24_grad(path, fx[k].w, fx[k].h, fx[k].mode) != 0) {
CHECK(0, "颜色夹具写入 %s", fx[k].name);
continue;
}
meow_img *im = meow_img_load_file(path);
if (!im) { CHECK(0, "颜色夹具解码 %s", fx[k].name); continue; }
int w = meow_img_w(im), h = meow_img_h(im);
size_t n = (size_t)w * h;
uint16_t *px = (uint16_t *)malloc(n * 2);
uint16_t *ref = (uint16_t *)malloc(n * 2);
uint16_t *got = (uint16_t *)malloc(n * 2);
if (!px || !ref || !got) {
CHECK(0, "颜色夹具缓冲分配");
free(px); free(ref); free(got); meow_img_free(im); continue;
}
meow_gfx_rgba_to_rgb565(meow_img_pixels(im), px, (int)n);
meow_ui_clear(0x000000);
meow_ui_img(0, 0, im); /* 旧路径:RGBA blit */
capture565(0, 0, w, h, ref);
meow_ui_clear(0x000000);
meow_gfx_blit_rgb565(0, 0, w, h, px); /* 新路径:565 blit(页槽) */
capture565(0, 0, w, h, got);
size_t diff = 0;
for (size_t i = 0; i < n; i++) if (ref[i] != got[i]) diff++;
/* 量化误差:565 展开回 8bit 与源像素比。 */
const unsigned char *rgba = meow_img_pixels(im);
long long se = 0;
int maxrb = 0, maxg = 0;
for (size_t i = 0; i < n; i++) {
int r5 = (got[i] >> 11) & 0x1F, g6 = (got[i] >> 5) & 0x3F, b5 = got[i] & 0x1F;
int er = ex5i(r5) - rgba[i * 4 + 0];
int eg = ex6i(g6) - rgba[i * 4 + 1];
int eb = ex5i(b5) - rgba[i * 4 + 2];
if (abs(er) > maxrb) maxrb = abs(er);
if (abs(eb) > maxrb) maxrb = abs(eb);
if (abs(eg) > maxg) maxg = abs(eg);
se += (long long)er * er + (long long)eg * eg + (long long)eb * eb;
}
double rmse = sqrt((double)se / (double)(n * 3));
/* 灰阶梯:输出必须单调不减,且阶梯数与 RGBA 路径完全一致(没有新增色带)。 */
int mono = 1, lv = 1, lv_ref = 1;
for (int x = 1; x < w; x++) {
uint16_t a = got[(size_t)(h / 2) * w + x], b = got[(size_t)(h / 2) * w + x - 1];
uint16_t ar = ref[(size_t)(h / 2) * w + x], br = ref[(size_t)(h / 2) * w + x - 1];
if (a < b) mono = 0;
if (a != b) lv++;
if (ar != br) lv_ref++;
}
printf(" [P12] %-42s 差异像素=%zu/%zu 误差 max R/B=%d G=%d RMSE=%.2f 阶梯=%d\n",
fx[k].label, diff, n, maxrb, maxg, rmse, lv);
if (fx[k].mode == 0) {
for (int x = 0; x < w; x += w / 8) {
int i = (h / 2) * w + x;
printf(" x=%3d 源灰度=%3d → RGBA路径=0x%04X 565路径=0x%04X\n",
x, rgba[(size_t)i * 4], ref[i], got[i]);
}
} else {
for (int x = 0; x < w; x += w / 4) {
int i = (h / 2) * w + x;
printf(" x=%3d 源 RGB=(%3d,%3d,%3d) → RGBA路径=0x%04X 565路径=0x%04X\n",
x, rgba[(size_t)i * 4], rgba[(size_t)i * 4 + 1], rgba[(size_t)i * 4 + 2],
ref[i], got[i]);
}
}
CHECK(diff == 0, "%s:565 blit 与 RGBA blit 逐像素一致(差异 %zu)", fx[k].label, diff);
CHECK(maxrb <= 7 && maxg <= 3,
"%s:量化误差在 565 理论界内(R/B %d ≤7,G %d ≤3)", fx[k].label, maxrb, maxg);
CHECK(rmse <= 3.5, "%s:RMSE %.2f ≤3.5", fx[k].label, rmse);
if (fx[k].mode == 0) {
CHECK(mono, "%s:灰阶梯输出单调(无色带跳变/反转)", fx[k].label);
CHECK(lv >= 30 && lv == lv_ref,
"%s:阶梯数 %d ≥30 且与 RGBA 路径一致(%d)", fx[k].label, lv, lv_ref);
}
free(px); free(ref); free(got);
meow_img_free(im);
}
}
int main(void)
{
const char *base = getenv("TMPDIR");
@@ -328,6 +496,9 @@ int main(void)
mr_reader_ui_decode_count());
CHECK(kb <= 9 * 1024, "resident page images bounded (got %ldKB)", kb);
/* ---- P12(M32):565 页槽的颜色正确性(需要 565 屏的 back_buf)---- */
color_check();
/* ---- P9 800x1280(评审表格另一档):翻页 + 条漫都必须零重采样 ---- */
meow_view_page_pop();
mr_reader_open_dir(book2);