Files
MeowOS-SDK/tuku/test/test_reader_membudget.c
T
AskaEth e896a28fba 图库: 修 M31 评审 P1(两档共存峰值 + 换清晰挤掉预取)与三个 P2(r2)
P1-1 内存账没算全:高档解码时低档还占着槽,但门控/预留只按新图算。
- 高档请求携带发起时的常驻快照(page_req.resident),解码前按
  「常驻 + reader_estimate_peak()」算这次操作的总峰值,上限 MR_TIER_TOTAL_CAP
  (12MB,与 page_source 的绝对上限同值;常态 3MB 只约束 stb 回退路径,
  后端页本来就超 3MB 且已被 M27 放行),超了跳过高档、保留低档(skip=coexist)。
- 峰值本身靠调度压回基线:分页模式焦点页还是低档时不预取邻居(预取来会被
  换清晰腾掉),换清晰前 slots_reserve 腾掉非可见页,高档只与焦点低档共存。
  B8(--wrap malloc,两实例并行):max_live_delta 3250KB→550KB,
  绝对峰值 5279KB→3932KB(基线 3929KB)。
P1-2 换清晰挤掉预取:换清晰后不重跑窗口,邻居洞留到下次翻页(0/8 命中、4 次/页)。
- 新增每页 in-flight 去重(g_req_inflight):请求被 worker 取走后不再被窗口刷新重排,
  这是重复解码/挤掉邻居的根因之一。
- 高档落位后按最近一次翻页方向补回邻居(sharpen_refill);反方向不补(3MB 常驻
  上限下放不下,必然再被挤掉)。
- 真机几何探针(480×720,1280×2275 JPEG,8 次 700ms/页):0/8→8/8 命中,
  4→1.5 次解码/页。B8 顺序 100 页:100/100 命中、149 次解码(1.49/页)。

P2-1:B8 改成 101 页册顺序前翻 100 次(每页都是不同的下一页),4MP/12MP 夹具
真正成为焦点页;断言最后一页=100、命中率 ≥90%、解码 ≤2/页+余量。
P2-2:B10 去掉恒真的 slot_kb 比较,改断言「总常驻不增加」+「高档操作增量 ≤3MB」+
「两档共存绝对峰值有界」;补双击放大后换清晰不重置缩放/平移、条漫滚动位置不变。
P2-3:B10 整串生命周期解码次数 ≤12(实测 7);B8 解码次数上限。
夹具用硬链接(同文件系统),避免多页册真拷贝几百 MB。

顺带:MR_SHARPEN_MS 200→150(任务允许 150–300ms,给「换清晰→补邻居」留翻页余量);
新增宿主观察口 flip_hits/misses、zoom/pan/woff(只读)。
2026-11-06 16:15:50 +08:00

1044 lines
51 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* test_reader_membudget.c — 阅读器解码内存预算回归(M22;M27 接真缩解后端;需要 SDK 源码树)。
*
* 真机事故(R3 Pro II 480×720 / MemTotal 56872kB):空闲 RSS 6044kB → 翻页 11400kB
* → 峰值 15600kB(当时 MemFree 仅 1404kB)→ SIGKILL。M20 审计把账算清:4 槽 RGBA 常驻
* 5356kB、单页解码尖峰 3.4–35MB(旧模型低估 3.5–8 倍)。本测试用 `--wrap malloc` 量
* **真实分配峰值**(不是 VmHWM 估计),把「模型 / 阈值 / 驻留」钉在实测上。
*
* 断言:
* B1 峰值模型 ≤ 实测(模型必须是不低估的上界;同时 ≤ 实测×2,避免离谱保守)
* B2 单页解码峰值(预算内的页)≤ 3MB 硬阈值
* B3 连翻 100 页:峰值不增长、无永久失败页、解码次数有界(取消生效)
* B4 常驻页图 ≤ 常驻上限(3MB)
* B5 内存门控:MemAvailable 不足 → 不解码直接占位(可重试,不是永久失败)
* B6 预算跳过可重试:MemFree 充足时同一页能解出来(占位不是永久 failed)
* B7 M27:6 类典型页(2000×3000 baseline/渐进/restart、1600×2400 PNG/BMP、4MP、12MP)
* 全部走后端、不占位;估算 ≥ 实测且 ≤1.5×;输出尺寸与显示框一致
* B7b 回退路径:CMYK / Adam7 PNG / 1bpp BMP 走 stb,按旧门控占位
* B7c P1-1 旁路回归:截断 12MP JPEG / 3000×3000 24bpp 头-only BMP(判定 backend,
* 后端失败必须 NULL、峰值 ≤3MB)/ 非法位深 PNG(矩阵拒绝→stb 占位)
* B8 M27/M31:顺序连翻 100 页(101 页册,6 类循环,每页都是不同的下一页)不掉页、
* 不占位、存活增量 ≤3MB、绝对峰值有上界、预取命中 ≥90%、解码次数 ≤2/页+余量
* (M31 r2:修「每翻一页解 4 次」与高峰值,评审 P1-1/P1-2/P2-1/P2-3)
* B10 M31:两级清晰度生命周期(JPEG 大页):低档先落位(open 不阻塞)→ 停稳后高档落
* 同一槽(总常驻不增加、槽位不空、本次操作增量 ≤3MB)→ 双击放大换清晰不重置缩放/平移
* → 拖动中强塞的高档请求被取消通道作废 → 整串解码次数有界
* B9 M27:CMYK 回退页集成 —— 占位可重试且不崩
*
* 夹具:BMP/PNG 小夹具由本文件用 C 写;JPEG/PNG/BMP 大夹具由 python3+Pillow 生成
* (test/make_mem_samples.py),缺 PIL 时跳过相关断言(打印 SKIP)。
*
* 编译:test/run_tests.sh(与 test_reader_perf 同口径 + --wrap malloc 家族)。
*/
#define _GNU_SOURCE
#include "tuku_internal.h"
#include "ui/reader/reader.h"
#include "meow_ui_internal.h"
#include "logic/reader/nav.h"
#include "logic/reader/page_source.h"
#include "stb_image.h" /* stbi_load_scaled:两条路径输出尺寸一致性对照 */
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
/* ------------------------------------------------------------------ */
/* 分配打点:--wrap malloc/free 家族(与 sdk/test_meow_img_budget.c 同法)*/
/* ------------------------------------------------------------------ */
#define AHDR 32
#define AMAGIC 0x6D6B4D32324D454DU /* 只统计本包装器的分配;libc 内部(strdup 等)原样透传 */
static size_t g_live = 0;
static size_t g_peak = 0;
extern void *__real_malloc(size_t);
extern void *__real_calloc(size_t, size_t);
extern void *__real_realloc(void *, size_t);
extern void __real_free(void *);
static void acc_alloc(size_t n)
{
g_live += n;
if (g_live > g_peak) g_peak = g_live;
}
void *__wrap_malloc(size_t n)
{
size_t *h = (size_t *)__real_malloc(n + AHDR);
if (!h) return NULL;
h[0] = n;
h[1] = (size_t)AMAGIC;
acc_alloc(n);
return (char *)h + AHDR;
}
void *__wrap_calloc(size_t a, size_t b)
{
size_t n = a * b;
void *p = __wrap_malloc(n);
if (p) memset(p, 0, n);
return p;
}
void __wrap_free(void *p)
{
size_t *h;
if (!p) return;
h = (size_t *)((char *)p - AHDR);
if (h[1] != (size_t)AMAGIC) { __real_free(p); return; } /* 非本包装器的分配 */
h[1] = 0;
g_live -= h[0];
__real_free(h);
}
void *__wrap_realloc(void *p, size_t n)
{
size_t *h = p ? (size_t *)((char *)p - AHDR) : NULL;
if (p && (!h || h[1] != (size_t)AMAGIC)) return __real_realloc(p, n); /* 外来指针 */
size_t old = h ? h[0] : 0;
void *q = __wrap_malloc(n);
if (!q) return NULL;
if (p) {
memcpy(q, p, old < n ? old : n);
__wrap_free(p);
}
return q;
}
static size_t peak_reset(void) { g_peak = g_live; return g_live; }
static size_t peak_since(size_t base) { return g_peak - base; }
void mr_nav_report(void) {}
static int g_fail;
#define CHECK(cond, ...) do { \
if (cond) { printf(" PASS\n"); } \
else { g_fail++; printf(" FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
#define INFO(...) do { printf(" info: "); printf(__VA_ARGS__); printf("\n"); } while (0)
/* ------------------------------------------------------------------ */
/* 夹具写入 */
/* ------------------------------------------------------------------ */
static void put16(unsigned char *p, unsigned v) { p[0] = (unsigned char)v; p[1] = (unsigned char)(v >> 8); }
static void put32be(unsigned char *p, unsigned v)
{
p[0] = (unsigned char)(v >> 24); p[1] = (unsigned char)(v >> 16);
p[2] = (unsigned char)(v >> 8); p[3] = (unsigned char)v;
}
static void put32(unsigned char *p, unsigned v)
{
p[0] = (unsigned char)v; p[1] = (unsigned char)(v >> 8);
p[2] = (unsigned char)(v >> 16); p[3] = (unsigned char)(v >> 24);
}
static int write_bmp24(const char *path, int w, int h, unsigned seed)
{
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++) {
row[x * 3 + 0] = (unsigned char)((x + seed) & 0xFF);
row[x * 3 + 1] = (unsigned char)((y * 3 + seed) & 0xFF);
row[x * 3 + 2] = (unsigned char)((x ^ y) & 0xFF);
}
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;
}
/* 1bpp BMP(后端不支持 → 回退 stb):用于「回退路径按旧门控占位」的集成断言。 */
static int write_bmp1(const char *path, int w, int h)
{
int stride = ((w + 31) / 32) * 4;
int imgsz = stride * h;
unsigned char fh[14] = { 0 }, ih[40] = { 0 };
unsigned char pal[8] = { 0, 0, 0, 0, 255, 255, 255, 0 };
fh[0] = 'B'; fh[1] = 'M';
put32(fh + 2, (unsigned)(14 + 40 + 8 + imgsz));
put32(fh + 10, (unsigned)(14 + 40 + 8));
put32(ih + 0, 40);
put32(ih + 4, (unsigned)w);
put32(ih + 8, (unsigned)h);
put16(ih + 12, 1);
put16(ih + 14, 1);
put32(ih + 20, (unsigned)imgsz);
put32(ih + 32, 2);
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 &&
fwrite(pal, 1, sizeof pal, f) == sizeof pal;
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 < stride; x++)
row[x] = (unsigned char)((y / 16 + x) & 1 ? 0x00 : 0xFF);
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 copy_file(const char *src, const char *dst)
{
FILE *in = fopen(src, "rb");
if (!in) return -1;
FILE *out = fopen(dst, "wb");
if (!out) { fclose(in); return -1; }
unsigned char buf[16384];
size_t n;
int ok = 1;
while ((n = fread(buf, 1, sizeof buf, in)) > 0)
if (fwrite(buf, 1, n, out) != n) { ok = 0; break; }
fclose(in);
if (fclose(out) != 0) ok = 0;
return ok ? 0 : -1;
}
/* 同一文件系统优先硬链接:多页册只是「同样的夹具当很多页」,101 页 × 12MP/BMP/PNG 真拷贝
* 要写几百 MB,跑一次测试 I/O 比解码还久(评审复跑成本)。读侧只按路径打开,硬链接完全等价。 */
static int link_or_copy(const char *src, const char *dst)
{
if (link(src, dst) == 0) return 0;
return copy_file(src, dst);
}
static int have_file(const char *path)
{
struct stat st;
return stat(path, &st) == 0 && st.st_size > 0;
}
static unsigned crc32_buf(const unsigned char *p, size_t n)
{
unsigned c = 0xFFFFFFFFu;
for (size_t i = 0; i < n; i++) {
c ^= p[i];
for (int k = 0; k < 8; k++) c = (c >> 1) ^ (0xEDB88320u & (unsigned)(-(int)(c & 1)));
}
return c ^ 0xFFFFFFFFu;
}
static int write_png_rgb(const char *path, int w, int h, unsigned seed)
{
/* zlib 流用 stored 块(不依赖 zlib),只为造一个真 PNG 让 stb 走「源缓冲 + RGBA 转换」路径。 */
size_t rowlen = (size_t)w * 3 + 1;
size_t rawlen = rowlen * (size_t)h;
unsigned char *raw = (unsigned char *)malloc(rawlen);
if (!raw) return -1;
for (int y = 0; y < h; y++) {
unsigned char *r = raw + rowlen * (size_t)y;
r[0] = 0; /* filter none */
for (int x = 0; x < w; x++) {
r[1 + x * 3 + 0] = (unsigned char)((x + seed) & 0xFF);
r[1 + x * 3 + 1] = (unsigned char)(y & 0xFF);
r[1 + x * 3 + 2] = (unsigned char)((x ^ y) & 0xFF);
}
}
size_t nblk = (rawlen + 65534) / 65535;
size_t zlen = 2 + rawlen + nblk * 5 + 4;
unsigned char *z = (unsigned char *)malloc(zlen);
if (!z) { free(raw); return -1; }
size_t zi = 0;
z[zi++] = 0x78; z[zi++] = 0x01;
for (size_t off = 0; off < rawlen; off += 65535) {
size_t n = rawlen - off > 65535 ? 65535 : rawlen - off;
z[zi++] = (off + n == rawlen) ? 1 : 0; /* BFINAL */
z[zi++] = (unsigned char)(n & 0xFF);
z[zi++] = (unsigned char)(n >> 8);
z[zi++] = (unsigned char)(~n & 0xFF);
z[zi++] = (unsigned char)((~n >> 8) & 0xFF);
memcpy(z + zi, raw + off, n);
zi += n;
}
/* adler32(大端,zlib 流尾) */
{
unsigned a = 1, b = 0;
for (size_t i = 0; i < rawlen; i++) { a = (a + raw[i]) % 65521; b = (b + a) % 65521; }
put32be(z + zi, (b << 16) | a);
zi += 4;
}
free(raw);
FILE *f = fopen(path, "wb");
if (!f) { free(z); return -1; }
static const unsigned char sig[8] = { 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A };
int ok = fwrite(sig, 1, 8, f) == 8;
unsigned char ihdr[13] = { 0 };
put32be(ihdr + 0, (unsigned)w);
put32be(ihdr + 4, (unsigned)h);
ihdr[8] = 8; /* bit depth */
ihdr[9] = 2; /* color type RGB */
struct { const char *tag; const unsigned char *data; unsigned len; } chunks[2] = {
{ "IHDR", ihdr, 13 }, { "IDAT", z, (unsigned)zi },
};
for (int k = 0; ok && k < 2; k++) {
unsigned char lenb[4], crcb[4];
put32be(lenb, chunks[k].len);
if (fwrite(lenb, 1, 4, f) != 4) ok = 0;
if (fwrite(chunks[k].tag, 1, 4, f) != 4) ok = 0;
if (fwrite(chunks[k].data, 1, chunks[k].len, f) != chunks[k].len) ok = 0;
unsigned crc = crc32_buf((const unsigned char *)chunks[k].tag, 4);
/* 逐段续算:简化起见重算整块 */
{
unsigned char *tmp = (unsigned char *)malloc(4 + chunks[k].len);
if (tmp) {
memcpy(tmp, chunks[k].tag, 4);
memcpy(tmp + 4, chunks[k].data, chunks[k].len);
crc = crc32_buf(tmp, 4 + chunks[k].len);
free(tmp);
}
}
put32be(crcb, crc);
if (fwrite(crcb, 1, 4, f) != 4) ok = 0;
}
if (ok) {
unsigned char iend[12] = { 0, 0, 0, 0, 'I', 'E', 'N', 'D', 0xAE, 0x42, 0x60, 0x82 };
if (fwrite(iend, 1, 12, f) != 12) ok = 0;
}
free(z);
if (fclose(f) != 0) ok = 0;
return ok ? 0 : -1;
}
static int mkdir_p(const char *path)
{
char tmp[MR_PATH_MAX];
snprintf(tmp, sizeof tmp, "%s", path);
for (size_t i = 1; i <= strlen(tmp); i++) {
if (tmp[i] == '/' || tmp[i] == '\0') {
char ch = tmp[i];
tmp[i] = '\0';
if (mkdir(tmp, 0755) != 0) {
struct stat st;
if (stat(tmp, &st) != 0 || !S_ISDIR(st.st_mode)) return -1;
}
tmp[i] = ch;
}
}
return 0;
}
static int rm_rf(const char *path)
{
struct stat st;
if (lstat(path, &st) != 0) return -1;
if (!S_ISDIR(st.st_mode)) return unlink(path);
DIR *d = opendir(path);
if (!d) return -1;
struct dirent *de;
while ((de = readdir(d)) != NULL) {
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue;
char full[MR_PATH_MAX];
snprintf(full, sizeof full, "%s/%s", path, de->d_name);
rm_rf(full);
}
closedir(d);
return rmdir(path);
}
static void pump_ms(int ms)
{
for (int i = 0; i < ms / 10; i++) {
usleep(10 * 1000);
meow_anim_tick();
}
}
static void tap(int x, int y)
{
reader_on_touch_down(x, y);
reader_on_tap(x, y);
pump_ms(300);
}
/* 生成 JPEG 夹具(Pillow 不可用则返回 0,相关断言 SKIP)。脚本本体在
* test/make_mem_samples.py(与本文件同目录;run_tests.sh 已 cd 到 tuku/)。 */
static int gen_jpeg_fixtures(const char *dir)
{
char cmd[512];
snprintf(cmd, sizeof cmd, "python3 test/make_mem_samples.py '%s' >/dev/null 2>&1", dir);
if (system(cmd) != 0) return 0;
static const char *const need[] = {
"page.jpg", "big4.jpg", "prog.jpg",
"base2000.jpg", "prog2000.jpg", "restart2000.jpg",
"page1600.png", "page1600.bmp", "mp4.jpg", "mp12.jpg",
"cmyk.jpg", "adam7.png", "mono1.bmp",
"trunc2000.jpg", "hdr3000.bmp", "baddepth.png",
};
for (size_t i = 0; i < sizeof need / sizeof need[0]; i++) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/%s", dir, need[i]);
if (!have_file(p)) return 0;
}
return 1;
}
/* 与 reader_load_page 完全同路径:_step_ex → 兜底降采样 → 落框重采样。
* allow_stb 与阅读器一致:判定 backend 时传 0(后端失败即 NULL,绝不回退 stb)。
* 返回真实分配峰值(字节),并回报实际路径与最终尺寸。 */
static size_t measure_page_decode(const char *path, const mr_img_info *in, int step,
int allow_stb, int box_w, int box_h,
int *used_backend, 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 (used_backend) *used_backend = used;
if (ow && oh && im) { *ow = meow_img_w(im); *oh = meow_img_h(im); }
size_t peak = peak_since(base);
if (im) meow_img_free(im);
return peak;
}
int main(void)
{
const char *base = getenv("TMPDIR");
if (!base || !base[0]) base = ".";
char tmpl[512];
snprintf(tmpl, sizeof tmpl, "%s/mr_membudget_XXXXXX", base);
char *data = mkdtemp(tmpl);
if (!data) { printf("SKIP: mkdtemp failed\n"); return 0; }
setenv("MEOW_DATA_DIR", data, 1);
char root[MR_PATH_MAX];
snprintf(root, sizeof root, "./mr_membudget_%ld", (long)getpid());
char book[MR_PATH_MAX], big[MR_PATH_MAX], pngbook[MR_PATH_MAX], jpgbook[MR_PATH_MAX];
char mixed[MR_PATH_MAX], fallback[MR_PATH_MAX];
snprintf(book, sizeof book, "%s/small", root);
snprintf(big, sizeof big, "%s/big", root);
snprintf(pngbook, sizeof pngbook, "%s/png", root);
snprintf(jpgbook, sizeof jpgbook, "%s/jpg", root);
snprintf(mixed, sizeof mixed, "%s/mixed", root);
snprintf(fallback, sizeof fallback, "%s/fallback", root);
if (mkdir_p(book) != 0 || mkdir_p(big) != 0 || mkdir_p(pngbook) != 0 ||
mkdir_p(jpgbook) != 0 || mkdir_p(mixed) != 0 || mkdir_p(fallback) != 0) {
printf("SKIP: mkdir failed\n");
return 0;
}
/* 480x640 BMP ×8:单页解码峰值 ≈ 480*640*4*2 ≈ 2.5MB,能进 3MB 预算(可连翻)。
* 长边 640 ≤ 解码长边 720 → 后端判定「小图走旧入口」→ stb,正好覆盖回退门控。 */
for (int i = 0; i < 8; i++) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/p%02d.bmp", book, i);
if (write_bmp24(p, 480, 640, (unsigned)i) != 0) { printf("SKIP: bmp write\n"); return 0; }
}
/* 1200x1600 1bpp BMP(后端拒绝 → stb):stb 峰值 ≈ 7.7MB + 落框 ≈ 9MB。
* 3MB 常态预算下必须占位;MemFree 富余(放宽到 12MB)时必须能解出来。 */
{
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/huge.bmp", big);
if (write_bmp1(p, 1200, 1600) != 0) { printf("SKIP: bmp write\n"); return 0; }
}
/* 600x960 PNG(RGB):长边 960 > 720 → 走后端;覆盖 PNG 逐行路径。 */
for (int i = 0; i < 3; i++) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/q%d.png", pngbook, i);
if (write_png_rgb(p, 600, 960, (unsigned)i) != 0) { printf("SKIP: png write\n"); return 0; }
}
int have_jpeg = gen_jpeg_fixtures(jpgbook);
/* 顺序连翻 100 页:6 类典型页循环(12MP / 4MP / baseline / 渐进 / PNG / BMP)。
* 每类都必须走后端且不占位——这是「修好了却用不上」的回归防线。
* M31 r2(评审 P2-1):101 页顺序遍历(不是 30 页来回跳)——每次翻页都落到**不同的
* 下一页**,4MP/12MP 夹具都会真正成为焦点页并被解码/换清晰。 */
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 = 101; /* 100 次前翻 → 第 101 页(不回卷、不触发册间衔接) */
int mixed_pages = 0;
if (have_jpeg) {
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%03d_%s", mixed, i, g_mix[i % mix_n]);
if (link_or_copy(src, dst) == 0) mixed_pages++;
}
}
/* 回退路径集成:CMYK(4 分量)必须占位且不崩。 */
int have_fallback = 0;
if (have_jpeg) {
char src[MR_PATH_MAX], dst[MR_PATH_MAX];
snprintf(src, sizeof src, "%s/cmyk.jpg", jpgbook);
snprintf(dst, sizeof dst, "%s/cmyk.jpg", fallback);
if (copy_file(src, dst) == 0) have_fallback = 1;
}
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { printf("SKIP: meow_ui_init failed (no host backend)\n"); return 0; }
static const meow_view_hooks lh = { 0 };
meow_view *lib = meow_view_create(ACT_LIBRARY, MEOW_VIEW_ACTIVITY, &lh, NULL);
meow_view *reader = reader_view_create();
meow_view_page_push(ACT_LIBRARY);
/* 宿主内存读数固定:3MB 常态预算生效(不被开发机的巨大 MemFree 放宽)。 */
mr_reader_ui_test_set_mem(4096, 16000);
/* ---- B1/B2 单页解码峰值 vs 模型(PNG:stb 不能解码期缩解 → 整幅落地)---- */
{
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/q0.png", pngbook);
long long model = mr_reader_ui_decode_peak_kb(p, 1, 480, 768);
size_t b0 = g_live;
peak_reset();
/* 走产品解码入口(含落框重采样),与阅读器 worker 完全同路径 */
meow_img *im = meow_img_load_file_step(p, 768, 1);
if (im && (meow_img_w(im) != 480 || meow_img_h(im) != 768)) {
meow_img *fit = meow_img_resample(im, 480, 768);
if (fit) { meow_img_free(im); im = fit; }
}
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);
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);
}
/* ---- B5 内存门控:MemAvailable 不足 → 不解码、占位(可重试)---- */
{
mr_reader_ui_test_set_mem(20000, 1000); /* MemAvailable 1MB < 6MB 下限 */
mr_reader_open_dir(book);
CHECK(mr_reader_ui_count() == 8, "book pages=%d", mr_reader_ui_count());
int d0 = mr_reader_ui_decode_count();
pump_ms(600);
CHECK(mr_reader_ui_decode_count() - d0 <= 1,
"low mem: no decode attempt (%d new decodes)", mr_reader_ui_decode_count() - d0);
CHECK(mr_reader_ui_skipped_pages() > 0, "low mem: page skipped (skipped=%d)",
mr_reader_ui_skipped_pages());
CHECK(mr_reader_ui_failed_pages() == 0, "low mem: not a permanent failure (failed=%d)",
mr_reader_ui_failed_pages());
/* 内存恢复 + 点按显式重试 → 同一页能解出来(可重试语义) */
mr_reader_ui_test_set_mem(20000, 20000);
int y = meow_ui_height() / 2;
tap(meow_ui_width() / 2, y);
pump_ms(500);
CHECK(mr_reader_ui_skipped_pages() == 0, "retry after mem recovers (skipped=%d)",
mr_reader_ui_skipped_pages());
CHECK(mr_reader_ui_resident_kb() > 0, "retry landed a page image (resident=%ldkB)",
mr_reader_ui_resident_kb());
meow_view_page_pop();
pump_ms(200);
}
/* ---- B3/B4 连翻 100 页:真实分配峰值不增长、常驻有界、取消有界 ---- */
{
mr_reader_ui_test_set_mem(4096, 16000);
mr_reader_open_dir(book);
pump_ms(800);
int w = meow_ui_width();
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();
/* 基线 = 首屏稳定后的存活字节;之后任何分配都不该再超过 基线+3MB(= 单页解码峰值)。 */
size_t live_steady = g_live;
size_t peak_steady = g_peak;
int skipped_max = 0, advanced = 0;
for (int i = 0; i < 100; i++) {
int before = mr_reader_ui_page();
tap(i % 2 == 0 ? 40 : w - 40, 300); /* RTL:左 1/3=下一页 */
pump_ms(120); /* 覆盖滑页 + 紧随其后的预取解码 */
if (mr_reader_ui_page() != before) advanced++;
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) {
g_fail++;
printf(" FAIL flip %d: resident %ldkB > cap %lldkB\n", i, kb, resident_cap);
break;
}
}
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);
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);
CHECK(live_end <= live_steady + 512 * 1024,
"live bytes do not drift up over 100 flips (%zuKB -> %zuKB)",
live_steady / 1024, live_end / 1024);
CHECK(skipped_max == 0, "no page skipped while flipping small pages (max=%d)", skipped_max);
CHECK(mr_reader_ui_failed_pages() == 0, "no permanently failed pages (%d)",
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);
}
/* ---- B6 预算跳过可重试:大页在低预算下占位,MemFree 充足后能解出来 ---- */
{
mr_reader_ui_test_set_mem(4096, 16000); /* MemFree 4MB → 3MB 常态预算 */
mr_reader_open_dir(big);
pump_ms(900);
CHECK(mr_reader_ui_skipped_pages() == 1, "over-budget page skipped (skipped=%d)",
mr_reader_ui_skipped_pages());
CHECK(mr_reader_ui_failed_pages() == 0, "over-budget page is not a permanent failure (failed=%d)",
mr_reader_ui_failed_pages());
/* MemFree 富余 → 放宽档(峰值上限 = MemFree-4MB,最高 12MB);大页仍应能解出。 */
mr_reader_ui_test_set_mem(48000, 48000);
tap(meow_ui_width() / 2, meow_ui_height() / 2);
pump_ms(1200);
INFO("relaxed budget=%ldkB skipped=%d resident=%ldkB", mr_reader_ui_peak_budget_kb(),
mr_reader_ui_skipped_pages(), mr_reader_ui_resident_kb());
CHECK(mr_reader_ui_skipped_pages() == 0,
"over-budget page decodable once MemFree is plenty (skipped=%d)",
mr_reader_ui_skipped_pages());
meow_view_page_pop();
pump_ms(200);
}
/* ---- B7 M27:6 类典型页 —— 全部走后端、不占位、模型 ≥ 实测且 ≤1.5× ---- */
if (have_jpeg) {
static const char *const typical[] = {
"base2000.jpg", "prog2000.jpg", "restart2000.jpg",
"page1600.png", "page1600.bmp", "mp4.jpg", "mp12.jpg",
};
printf(" [B7] 典型页(fit-width 480,框按源宽高比):\n");
printf(" %-16s %-8s %10s %10s %6s\n", "page", "path", "est(kB)", "measured", "ratio");
for (size_t i = 0; i < sizeof typical / sizeof typical[0]; i++) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/%s", jpgbook, typical[i]);
mr_img_info in;
if (!mr_probe_info(p, &in)) { CHECK(0, "probe %s", typical[i]); continue; }
int box_w = 480;
int box_h = (int)((long)in.h * box_w / in.w);
int dim = box_w > box_h ? box_w : box_h;
int step = in.codec == MR_IMG_CODEC_JPEG
? meow_img_pick_scale(in.w > in.h ? in.w : in.h, dim) : 1;
mr_decode_decision dec;
reader_estimate_peak(&in, step, box_w, box_h, -1, -1, &dec);
int used = 0, ow = 0, oh = 0;
size_t measured = measure_page_decode(p, &in, dec.step, 1, box_w, box_h,
&used, &ow, &oh) + 1;
printf(" %-16s %-8s %10lld %10zu %6.2f\n", typical[i],
dec.path == MR_DECODE_PATH_BACKEND ? "backend" : "stb",
dec.peak_bytes / 1024, measured / 1024,
(double)dec.peak_bytes / (double)measured);
CHECK(dec.path == MR_DECODE_PATH_BACKEND, "%s 走后端", typical[i]);
CHECK(dec.skip == MR_DECODE_OK, "%s 不占位", typical[i]);
CHECK(used == 1, "%s 实际路径=backend", typical[i]);
CHECK(dec.peak_bytes <= 2560LL * 1024, "%s 估算 ≤2560kB (%lldkB)",
typical[i], dec.peak_bytes / 1024);
CHECK(dec.peak_bytes >= (long long)measured, "%s 模型 ≥ 实测 (%lldkB < %zukB)",
typical[i], dec.peak_bytes / 1024, measured / 1024);
CHECK(dec.peak_bytes <= (long long)measured * 3 / 2, "%s 模型 ≤1.5× 实测",
typical[i]);
CHECK(ow == box_w && oh == box_h, "%s 输出 %dx%d == 显示框 %dx%d",
typical[i], ow, oh, box_w, box_h);
}
/* 输出尺寸口径:后端直出 == 「stbi_load_scaled(step) + 落框精修」旧路径
* (典型页必须逐像素同尺寸;源尺寸不是档位整数倍时 M23 已记录 1px 舍入口径差)。 */
for (size_t i = 0; i < sizeof typical / sizeof typical[0]; i++) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/%s", jpgbook, typical[i]);
mr_img_info in;
if (!mr_probe_info(p, &in)) continue;
int box_w = 480;
int box_h = (int)((long)in.h * box_w / in.w);
int dim = box_w > box_h ? box_w : box_h;
int step = in.codec == MR_IMG_CODEC_JPEG
? meow_img_pick_scale(in.w > in.h ? in.w : in.h, dim) : 1;
int used = 0, ow = 0, oh = 0;
measure_page_decode(p, &in, step, 1, box_w, box_h, &used, &ow, &oh);
/* 旧路径(与 meow_img_load_file_step 的 stb 分支同式):先 stbi 缩解,再精修。 */
int sw = 0, sh = 0, sc = 0, ew = 0, eh = 0;
unsigned char *raw = stbi_load_scaled(p, &sw, &sh, &sc, 4, step);
if (raw) {
if (sw <= dim && sh <= dim) { ew = sw; eh = sh; }
else if (sw >= sh) {
ew = dim;
eh = (int)((long)sh * dim / sw);
if (eh < 1) eh = 1;
} else {
eh = dim;
ew = (int)((long)sw * dim / sh);
if (ew < 1) ew = 1;
}
}
if (raw) stbi_image_free(raw);
CHECK(used == 1 && ow == ew && oh == eh,
"%s 两条路径输出尺寸一致(后端 %dx%d / stb %dx%d)",
typical[i], ow, oh, ew, eh);
}
/* 回退路径:后端拒绝的 3 类必须走 stb,且按旧门控占位(不崩、不误放行)。 */
static const char *const fallback[] = { "cmyk.jpg", "adam7.png", "mono1.bmp" };
printf(" [B7b] 回退路径(必须 stb + 占位):\n");
for (size_t i = 0; i < sizeof fallback / sizeof fallback[0]; i++) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/%s", jpgbook, fallback[i]);
mr_img_info in;
if (!mr_probe_info(p, &in)) { CHECK(0, "probe %s", fallback[i]); continue; }
int box_w = 480;
int box_h = (int)((long)in.h * box_w / in.w);
int dim = box_w > box_h ? box_w : box_h;
int step = in.codec == MR_IMG_CODEC_JPEG
? meow_img_pick_scale(in.w > in.h ? in.w : in.h, dim) : 1;
mr_decode_decision dec;
reader_estimate_peak(&in, step, box_w, box_h, -1, -1, &dec);
int used = 1, ow = 0, oh = 0;
size_t measured = measure_page_decode(p, &in, dec.step, 1, box_w, box_h,
&used, &ow, &oh) + 1;
printf(" %-16s %-8s %10lld %10zu %6.2f\n", fallback[i],
dec.path == MR_DECODE_PATH_BACKEND ? "backend" : "stb",
dec.peak_bytes / 1024, measured / 1024,
(double)dec.peak_bytes / (double)measured);
CHECK(dec.path == MR_DECODE_PATH_STB, "%s 回退 stb", fallback[i]);
CHECK(dec.skip == MR_DECODE_SKIP_PEAK, "%s 按旧门控占位", fallback[i]);
CHECK(used == 0, "%s 实际路径=stb", fallback[i]);
CHECK(dec.peak_bytes >= (long long)measured, "%s stb 模型 ≥ 实测", fallback[i]);
CHECK(dec.peak_bytes <= (long long)measured * 3 / 2, "%s stb 模型 ≤1.5× 实测",
fallback[i]);
if (ow > 0 && oh > 0) {} /* 尺寸仅记录 */
}
/* ---- B7c(P1-1 回归):头部判支持、解码中途失败的坏文件 ----
* 旧实现「后端失败 → 静默回退 stb」实测放出 16–36MB;现在必须:
* - 判定 backend 的(截断 JPEG / 头-only 24bpp BMP)→ backend-only 解码返回 NULL,
* 峰值只含失败前的小分配(≤3MB),绝不 stb 整幅;
* - 判定 stb 的(非法位深 PNG,矩阵已镜像 png_depth_ok)→ skip=peak,不分配。 */
static const char *const badmeta[] = { "trunc2000.jpg", "hdr3000.bmp", "baddepth.png" };
printf(" [B7c] 坏文件(P1-1 旁路回归):\n");
for (size_t i = 0; i < sizeof badmeta / sizeof badmeta[0]; i++) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/%s", jpgbook, badmeta[i]);
mr_img_info in;
if (!mr_probe_info(p, &in)) { CHECK(0, "probe %s", badmeta[i]); continue; }
int box_w = 480;
int box_h = (int)((long)in.h * box_w / in.w);
int dim = box_w > box_h ? box_w : box_h;
int step = in.codec == MR_IMG_CODEC_JPEG
? meow_img_pick_scale(in.w > in.h ? in.w : in.h, dim) : 1;
mr_decode_decision dec;
reader_estimate_peak(&in, step, box_w, box_h, -1, -1, &dec);
if (dec.path == MR_DECODE_PATH_BACKEND) {
int used = 1, ow = 0, oh = 0;
size_t measured = measure_page_decode(p, &in, dec.step, 0, box_w, box_h,
&used, &ow, &oh) + 1;
printf(" %-16s backend-only measured=%zuKB (backend 失败→NULL)\n",
badmeta[i], measured / 1024);
CHECK(used == 0, "%s 不走 stb(used=0)", badmeta[i]);
CHECK(ow == 0 && oh == 0, "%s 返回 NULL(坏图占位)", badmeta[i]);
CHECK(measured <= 3 * 1024 * 1024,
"%s backend-only 峰值 ≤3MB (%zuKB)", badmeta[i], measured / 1024);
} else {
printf(" %-16s stb skip=%d est=%lldkB(门控占位,不分配)\n",
badmeta[i], dec.skip, dec.peak_bytes / 1024);
CHECK(dec.skip != MR_DECODE_OK, "%s 门控必须占位 (skip=%d)",
badmeta[i], dec.skip);
CHECK(dec.peak_bytes > 3LL * 1024 * 1024,
"%s stb 模型判超预算 (%lldkB)", badmeta[i], dec.peak_bytes / 1024);
}
}
} else {
printf(" SKIP jpeg assertions: Pillow unavailable (python3 -c 'import PIL')\n");
}
/* ---- B8 M27/M31:顺序连翻 100 页(101 页册,6 类典型页循环)——不掉页、不占位、
* 峰值不增长、预取命中、解码次数有界 ----
* M31 起这些 1.3–12MP 的页走两级清晰度(低档先上屏、停稳换高档)。评审 P1:
* - 顺序前翻(每页都落到不同的下一页),4MP/12MP 夹具真正成为焦点页;
* - 断言语义翻页命中率(目标页已在槽)与解码次数上限,防止「每翻一页解 4 次」;
* - 存活增量仍按单页解码峰值封顶(两档共存时换清晰前会腾掉非可见页,见 reader.c)。 */
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() == 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();
int low0 = mr_reader_ui_low_decodes(), high0 = mr_reader_ui_high_decodes();
int hits0 = mr_reader_ui_flip_hits(), miss0 = mr_reader_ui_flip_misses();
/* P2-3:只在窗口稳定后 reset 一次,之后不再 reset——这样淘汰旧图不会掩盖
* 「分配新图 + 解码」的真实峰值;指标 = 整个 100 翻窗口内相对稳态基线的最大存活增量。
* (旧实现每翻页 reset 一次,淘汰把基线拉低,报告里的 536KB 是伪值。) */
size_t base = peak_reset();
for (int i = 0; i < 100; i++) {
int before = mr_reader_ui_page();
tap(40, 300); /* RTL:左 1/3 = 下一页(顺序前翻) */
pump_ms(400); /* 700ms/页:给「换清晰→补邻居」留出余量 */
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;
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("100 mixed flips: advanced=%d last_page=%d hits=%d miss=%d skipped=%d decodes=%d "
"(low=%d high=%d) steady=%zuKB resident=%ldKB max_live_delta=%zuKB abs_peak=%zuKB",
advanced, mr_reader_ui_page(), hits, miss, skipped_max, decodes,
mr_reader_ui_low_decodes() - low0, mr_reader_ui_high_decodes() - high0,
live_steady / 1024, mr_reader_ui_resident_kb(), transient / 1024, g_peak / 1024);
CHECK(advanced == 100 && mr_reader_ui_page() == 100,
"100 次顺序前翻逐页推进(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());
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);
/* 顺序阅读的预取命中率:评审要求不低于基线 7/8。 */
CHECK(hits >= 90, "顺序翻页预取命中率 ≥90%%(hit=%d miss=%d)", hits, miss);
/* 解码次数上限:分档页 = 低档 + 高档(2 次/页),非 JPEG 单档(1 次/页)+ 开册/补邻居余量。
* 出现「每页 4 次」的放大(P1-2 的病灶)时这里会立刻失败。 */
CHECK(decodes <= 100 * 2 + 20, "100 次翻页解码次数 ≤2/页+余量 (decodes=%d)", decodes);
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,
"100 页连翻任何时刻存活增量 ≤3MB (%zuKB)", transient / 1024);
/* 两档共存的总账:常驻(≤3MB)+ 单次解码(≤3MB)——不许因为「基线里已含低档」
* 就看着没涨;绝对存活峰值同样要有上界。 */
CHECK(g_peak <= (size_t)(mr_reader_ui_resident_cap_kb() + 3 * 1024) * 1024,
"绝对存活峰值 ≤ 常驻上限+3MB (%zuKB)", g_peak / 1024);
meow_view_page_pop();
pump_ms(200);
}
/* ---- B10 M31:两级清晰度生命周期(2000×3000 JPEG,真机同类大页)----
* 1) 冻结停稳窗口 → 只解低档:焦点页 tier=low、常驻=显示框大小(仍是一次原生 blit),
* 主线程不等解码(open 立即返回);
* 2) 放开停稳窗口 → 高档落同一槽:tier=high、**总常驻不增加**(不是恒真的槽内字节比较)、
* 槽位全程不空、本次操作存活增量 ≤3MB、两档共存绝对峰值有上界;
* 3) 双击放大后按新几何先落低档,再换清晰 → 缩放/平移一字不差(P2-2);
* 4) 拖动中强塞的高档请求 → 被 M22 的 gen/窗口取消通道作废(skip=cancel),不会被解出来;
* 5) 整串生命周期解码次数有界(P2-3)。 */
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 (link_or_copy(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();
int dec0 = mr_reader_ui_decode_count();
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);
long res_low = mr_reader_ui_resident_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;
size_t op_base = g_live;
peak_reset();
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;
}
size_t op_peak = peak_since(op_base); /* 高档这一次操作的存活增量 */
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());
/* P2-2:旧的 `slot_kb` 相等是恒真(两档都落同一个 box)。改成真正会抓问题的
* 「总常驻不增加」:如果换清晰偷偷多留一张低档,这里立刻涨。 */
long res_high = mr_reader_ui_resident_kb();
CHECK(res_high > 0 && res_high <= res_low,
"换清晰不增加常驻(没有第二张低档常驻):%ldKB -> %ldKB",
res_low, res_high);
CHECK(op_peak <= 3 * 1024 * 1024,
"高档单次操作的存活增量 ≤3MB (%zuKB)", op_peak / 1024);
/* 两档共存总账:常驻 + 本次解码 的绝对峰值有上界(不是「基线含低档所以没涨」)。 */
CHECK(g_peak <= (size_t)(mr_reader_ui_resident_cap_kb() + 3 * 1024) * 1024,
"两档共存绝对峰值 ≤ 常驻上限+3MB (%zuKB)", g_peak / 1024);
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());
/* P2-2 之二:换清晰不得重置缩放/平移。先冻结窗口 → 双击放大(低档按新几何重解)
* → 记录缩放/平移 → 放开升级 → 高档落位后两者必须一字不差。 */
mr_reader_ui_test_set_sharpen_ms(60000); /* 先冻结,让放大后的低档先落位 */
reader_on_touch_down(240, 360);
reader_on_tap(240, 360);
pump_ms(60);
reader_on_touch_down(240, 360);
reader_on_tap(240, 360);
pump_ms(150);
pump_ms(600);
int zoom_before = mr_reader_ui_zoom_pct();
int panx_before = mr_reader_ui_pan_x();
int pany_before = mr_reader_ui_pan_y();
CHECK(zoom_before > 100, "double tap zoomed in (zoom=%d)", zoom_before);
CHECK(mr_reader_ui_focus_tier() == MR_TIER_LOW,
"zoomed page re-decodes to low tier first (tier=%d)",
mr_reader_ui_focus_tier());
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);
CHECK(mr_reader_ui_focus_tier() == MR_TIER_HIGH,
"zoomed page reaches the target tier (tier=%d)",
mr_reader_ui_focus_tier());
CHECK(mr_reader_ui_zoom_pct() == zoom_before &&
mr_reader_ui_pan_x() == panx_before &&
mr_reader_ui_pan_y() == pany_before,
"换清晰不重置缩放/平移(zoom %d→%d, pan %d,%d→%d,%d)",
zoom_before, mr_reader_ui_zoom_pct(), panx_before, pany_before,
mr_reader_ui_pan_x(), mr_reader_ui_pan_y());
/* 拖动中取消高档:拖动期间强塞一个「换清晰」请求,必须被取消通道作废。 */
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);
/* P2-2 之三:条漫滚动位置在换清晰时不得被重置(换的是视口中心页的档位)。 */
mr_reader_ui_test_set_sharpen_ms(60000); /* 先冻结:切模式时不要立刻换清晰 */
mr_reader_test_toggle_mode(); /* 翻页 → 条漫 */
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "tier book switched to webtoon");
pump_ms(300);
reader_on_touch_down(240, 600);
for (int k = 1; k <= 5; k++) {
reader_on_touch_move(240, 600 - k * 50);
pump_ms(10);
}
reader_on_tap(240, 300); /* 松手:fling */
pump_ms(2000); /* 等 fling 停稳 */
int woff0 = mr_reader_ui_woff();
int wpage0 = mr_reader_ui_page();
CHECK(woff0 > 0, "webtoon scrolled to a non-top offset (%d px)", woff0);
CHECK(mr_reader_ui_focus_tier() == MR_TIER_LOW,
"webtoon center page is low tier before settle (tier=%d)",
mr_reader_ui_focus_tier());
mr_reader_ui_test_set_sharpen_ms(0);
for (int i = 0; i < 200 && mr_reader_ui_focus_tier() != MR_TIER_HIGH; i++)
pump_ms(10);
CHECK(mr_reader_ui_focus_tier() == MR_TIER_HIGH,
"webtoon center page reaches the target tier (tier=%d)",
mr_reader_ui_focus_tier());
CHECK(mr_reader_ui_woff() == woff0 && mr_reader_ui_page() == wpage0,
"换清晰不重置条漫滚动位置(woff %d→%d, page %d→%d)",
woff0, mr_reader_ui_woff(), wpage0, mr_reader_ui_page());
mr_reader_ui_test_set_sharpen_ms(150); /* 恢复产品默认停稳窗口 */
/* P2-3:这一串(开册低+高、放大低+高、条漫低+高、一次被取消的高档)解码次数有界。 */
int decodes = mr_reader_ui_decode_count() - dec0;
CHECK(decodes <= 12, "两档生命周期解码次数有界 (decodes=%d)", decodes);
INFO("tier lifecycle: decodes=%d resident=%ldKB (低档/高档/放大/条漫 各一次换清晰)",
decodes, mr_reader_ui_resident_kb());
meow_view_page_pop();
pump_ms(200);
}
}
/* ---- B9 M27:回退路径集成 —— CMYK 页占位(可重试)且不崩 ---- */
if (have_fallback) {
mr_reader_ui_test_set_mem(4096, 16000);
mr_reader_open_dir(fallback);
pump_ms(900);
CHECK(mr_reader_ui_count() == 1, "fallback book pages=%d", mr_reader_ui_count());
CHECK(mr_reader_ui_skipped_pages() == 1, "CMYK 页按旧门控占位 (skipped=%d)",
mr_reader_ui_skipped_pages());
CHECK(mr_reader_ui_failed_pages() == 0, "CMYK 页不是永久失败 (%d)",
mr_reader_ui_failed_pages());
CHECK(mr_reader_ui_resident_kb() == 0, "CMYK 页未驻留 (resident=%ldkB)",
mr_reader_ui_resident_kb());
meow_view_page_pop();
pump_ms(200);
}
meow_view_page_pop();
meow_view_free(reader);
meow_view_free(lib);
meow_ui_shutdown();
rm_rf(root);
rm_rf(data);
printf("%s: %s\n", g_fail ? "FAIL" : "PASS",
g_fail ? "see failures above" : "reader memory budget checks passed");
return g_fail ? 1 : 0;
}