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

+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);