Merge branch 'feat/tuku-reader-perf-and-crash-fixes'
This commit is contained in:
commit
782a5f7c90
5 files changed
+1154
-89
No files matched your search
@@ -107,6 +107,40 @@ if [ -f "$SDK/meow_ui_view.c" ] && [ -f "$SDK/meow_ui_config.c" ]; then
|
|||||||
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
|
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
|
||||||
-pthread -lm -lz -o "$OUT"/test_open_book_host
|
-pthread -lm -lz -o "$OUT"/test_open_book_host
|
||||||
"$OUT"/test_open_book_host
|
"$OUT"/test_open_book_host
|
||||||
|
|
||||||
|
# 翻页热路径(M17):解码=显示尺寸 → 翻页零重采样;fit 切换按新几何重解。
|
||||||
|
gcc -Wall -Wextra -O1 -DMEOW_UI_HOST_TEST -DMR_READER_HOST_TEST -I. -I"$SDK" -I"$SDK/include" \
|
||||||
|
test/test_reader_perf.c ui/reader/reader.c \
|
||||||
|
logic/library.c logic/reader.c logic/reader/page_source.c \
|
||||||
|
logic/reader/archive_source.c logic/reader/page_cache.c logic/zip/zip.c \
|
||||||
|
logic/reader/nav.c \
|
||||||
|
"$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \
|
||||||
|
"$SDK"/meow_ui_vol.c "$SDK"/meow_ui_log.c "$SDK"/meow_ui_widget.c \
|
||||||
|
"$SDK"/meow_ui_input.c "$SDK"/meow_ui_activity.c "$SDK"/meow_ui_dialog.c \
|
||||||
|
"$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \
|
||||||
|
"$SDK"/meow_ui_draw.c "$SDK"/meow_ui_view.c "$SDK"/meow_ui_box.c \
|
||||||
|
"$SDK"/meow_ui_anim.c "$SDK"/meow_ui_scroll.c "$SDK"/meow_ui_img.c \
|
||||||
|
"$SDK"/meow_ui_media.c "$SDK"/meow_notify_ipc.c "$SDK"/meow_ui_config.c \
|
||||||
|
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
|
||||||
|
-pthread -lm -lz -o "$OUT"/test_reader_perf
|
||||||
|
"$OUT"/test_reader_perf
|
||||||
|
|
||||||
|
# 140 页夹打开/翻页/跳页 + mr_reader_open_dir_at(文件管理器模式入口)。
|
||||||
|
gcc -Wall -Wextra -O1 -DMEOW_UI_HOST_TEST -DMR_READER_HOST_TEST -I. -I"$SDK" -I"$SDK/include" \
|
||||||
|
test/test_reader_many_pages.c ui/reader/reader.c \
|
||||||
|
logic/library.c logic/reader.c logic/reader/page_source.c \
|
||||||
|
logic/reader/archive_source.c logic/reader/page_cache.c logic/zip/zip.c \
|
||||||
|
logic/reader/nav.c \
|
||||||
|
"$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \
|
||||||
|
"$SDK"/meow_ui_vol.c "$SDK"/meow_ui_log.c "$SDK"/meow_ui_widget.c \
|
||||||
|
"$SDK"/meow_ui_input.c "$SDK"/meow_ui_activity.c "$SDK"/meow_ui_dialog.c \
|
||||||
|
"$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \
|
||||||
|
"$SDK"/meow_ui_draw.c "$SDK"/meow_ui_view.c "$SDK"/meow_ui_box.c \
|
||||||
|
"$SDK"/meow_ui_anim.c "$SDK"/meow_ui_scroll.c "$SDK"/meow_ui_img.c \
|
||||||
|
"$SDK"/meow_ui_media.c "$SDK"/meow_notify_ipc.c "$SDK"/meow_ui_config.c \
|
||||||
|
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
|
||||||
|
-pthread -lm -lz -o "$OUT"/test_reader_many_pages
|
||||||
|
"$OUT"/test_reader_many_pages
|
||||||
else
|
else
|
||||||
echo "SKIP test_settings_lock_host/test_browse_ui_host: SDK 源码树不可用(只发布 meow_ui.a 的环境)"
|
echo "SKIP test_settings_lock_host/test_browse_ui_host: SDK 源码树不可用(只发布 meow_ui.a 的环境)"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -0,0 +1,317 @@
|
|||||||
|
/* test_reader_many_pages.c — 多页(140 张)文件夹打开/翻页/跳页回归 +「从第 N 张开始」API。
|
||||||
|
*
|
||||||
|
* 用户反馈:「文件夹里图片过多时(例如 140 个),APP 会直接闪退」;且后续书库要改成文件
|
||||||
|
* 管理器模式,需要「从选中的那张开始看」。本测试走真实 SDK UI 链路(view/activity/anim/
|
||||||
|
* 真实解码),断言:
|
||||||
|
* M1 140 页册可打开,页数/模式正确(翻页模式、第 1 页);
|
||||||
|
* M2 mr_reader_open_dir_at(dir, 57):从第 57 页(0 基)开始;越界 9999 → 末页;负数 → 首页;
|
||||||
|
* M3 显式下标优先于续读记录;mr_reader_open_dir 仍走续读记录(旧语义不变);
|
||||||
|
* M4 连续翻页穿过坏图(解码失败)不崩、不卡死,坏页给占位后继续翻;
|
||||||
|
* M5 条漫模式:140 页排版/跳页/滚动可用;
|
||||||
|
* M6 常驻页图有字节上界(<=9MB)——页数增长不会线性吃内存。
|
||||||
|
*
|
||||||
|
* 编译:test/run_tests.sh(与 test_open_book_host 同口径;只发布 meow_ui.a 时 SKIP)。
|
||||||
|
*/
|
||||||
|
#include "tuku_internal.h"
|
||||||
|
#include "ui/reader/reader.h"
|
||||||
|
#include "meow_ui_internal.h"
|
||||||
|
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#define BOOK_PAGES 140
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
static void put16(unsigned char *p, unsigned v) { p[0] = (unsigned char)v; p[1] = (unsigned char)(v >> 8); }
|
||||||
|
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 + seed) & 0xFF);
|
||||||
|
row[x * 3 + 2] = (unsigned char)(seed & 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 write_garbage(const char *path)
|
||||||
|
{
|
||||||
|
static const unsigned char junk[64] = { 'N', 'O', 'T', 'A', 'N', 'I', 'M', 'G' };
|
||||||
|
FILE *f = fopen(path, "wb");
|
||||||
|
if (!f) return -1;
|
||||||
|
int ok = fwrite(junk, 1, sizeof junk, f) == sizeof junk;
|
||||||
|
if (fclose(f) != 0) ok = 0;
|
||||||
|
return ok ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 合法 BMP24 头(声称 3000x3000,无像素数据):stbi_info 能读到尺寸,
|
||||||
|
* 整幅 RGBA 峰值 36MB 必须被 MR_DECODE_PEAK_CAP(12MB) 在解码前拦下
|
||||||
|
* (旧代码的 raw 上限是 40MB,这种页会被放行 → 真机上瞬时分配即可能被 OOM 杀掉)。 */
|
||||||
|
static int write_huge_bmp_header(const char *path)
|
||||||
|
{
|
||||||
|
unsigned char b[54] = { 0 };
|
||||||
|
b[0] = 'B'; b[1] = 'M';
|
||||||
|
put32(b + 2, 54); /* 文件大小(谎报,只为通过头检查) */
|
||||||
|
put32(b + 10, 54); /* 像素数据偏移 */
|
||||||
|
put32(b + 14, 40); /* BITMAPINFOHEADER */
|
||||||
|
put32(b + 18, 3000); /* width */
|
||||||
|
put32(b + 22, 3000); /* height */
|
||||||
|
put16(b + 26, 1); /* planes */
|
||||||
|
put16(b + 28, 24); /* bpp */
|
||||||
|
FILE *f = fopen(path, "wb");
|
||||||
|
if (!f) return -1;
|
||||||
|
int ok = fwrite(b, 1, sizeof b, f) == sizeof b;
|
||||||
|
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 int tap(int x, int y)
|
||||||
|
{
|
||||||
|
reader_on_touch_down(x, y);
|
||||||
|
reader_on_tap(x, y);
|
||||||
|
pump_ms(300);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
const char *base = getenv("TMPDIR");
|
||||||
|
if (!base || !base[0]) base = ".";
|
||||||
|
char tmpl[512];
|
||||||
|
snprintf(tmpl, sizeof tmpl, "%s/mr_many_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], book[MR_PATH_MAX], bad[MR_PATH_MAX];
|
||||||
|
snprintf(root, sizeof root, "./mr_many_books_%ld", (long)getpid());
|
||||||
|
snprintf(book, sizeof book, "%s/v140", root);
|
||||||
|
snprintf(bad, sizeof bad, "%s/vbad", root);
|
||||||
|
if (mkdir_p(book) != 0 || mkdir_p(bad) != 0) { printf("SKIP: mkdir failed\n"); return 0; }
|
||||||
|
for (int i = 0; i < BOOK_PAGES; i++) {
|
||||||
|
char p[MR_PATH_MAX];
|
||||||
|
snprintf(p, sizeof p, "%s/p%03d.bmp", book, i + 1);
|
||||||
|
if (write_bmp24(p, 200, 267, (unsigned)i) != 0) { printf("SKIP: bmp write failed\n"); return 0; }
|
||||||
|
}
|
||||||
|
/* 坏册:第 2 页是垃圾字节(解码失败 → 占位,不许崩/死循环)。 */
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
char p[MR_PATH_MAX];
|
||||||
|
snprintf(p, sizeof p, "%s/p%d.bmp", bad, i + 1);
|
||||||
|
if (i == 1) { if (write_garbage(p) != 0) { printf("SKIP: write failed\n"); return 0; } }
|
||||||
|
else if (i == 2) { if (write_huge_bmp_header(p) != 0) { printf("SKIP: write failed\n"); return 0; } }
|
||||||
|
else if (write_bmp24(p, 200, 267, (unsigned)i) != 0) { printf("SKIP: bmp write failed\n"); return 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
int w = meow_ui_width();
|
||||||
|
int mode_x = w - 10 - 48 * 3 + 24;
|
||||||
|
int jump_x = w - 10 - 48 * 2 + 24;
|
||||||
|
int bar_y = 28;
|
||||||
|
int slider_y = 56 + 4 + 20;
|
||||||
|
int sx = 24, sw = w - 48;
|
||||||
|
|
||||||
|
/* ---- M1 140 页可打开 ---- */
|
||||||
|
mr_reader_open_dir_at(book, 0);
|
||||||
|
CHECK(meow_ui_activity_active() == ACT_READER, "reader active");
|
||||||
|
CHECK(mr_reader_ui_count() == BOOK_PAGES, "pages=%d (want %d)",
|
||||||
|
mr_reader_ui_count(), BOOK_PAGES);
|
||||||
|
CHECK(mr_reader_ui_page() == 0, "start at page 0 (got %d)", mr_reader_ui_page());
|
||||||
|
pump_ms(600);
|
||||||
|
int draws0 = mr_reader_ui_blit_draws() + mr_reader_ui_scaled_draws();
|
||||||
|
meow_ui_paint_full();
|
||||||
|
CHECK(mr_reader_ui_blit_draws() + mr_reader_ui_scaled_draws() > draws0,
|
||||||
|
"draw path executed (paged, 140p book)");
|
||||||
|
|
||||||
|
/* ---- M2 从第 N 张开始 / 越界钳制 ---- */
|
||||||
|
meow_view_page_pop();
|
||||||
|
mr_reader_open_dir_at(book, 57);
|
||||||
|
CHECK(mr_reader_ui_page() == 57, "open_dir_at(57) -> page %d", mr_reader_ui_page());
|
||||||
|
pump_ms(400);
|
||||||
|
meow_view_page_pop();
|
||||||
|
|
||||||
|
mr_reader_open_dir_at(book, 9999);
|
||||||
|
CHECK(mr_reader_ui_page() == BOOK_PAGES - 1, "open_dir_at(9999) clamps to last (%d)",
|
||||||
|
mr_reader_ui_page());
|
||||||
|
pump_ms(400);
|
||||||
|
meow_view_page_pop();
|
||||||
|
|
||||||
|
mr_reader_open_dir_at(book, -5);
|
||||||
|
CHECK(mr_reader_ui_page() == 0, "open_dir_at(-5) clamps to first (%d)", mr_reader_ui_page());
|
||||||
|
pump_ms(300);
|
||||||
|
meow_view_page_pop();
|
||||||
|
|
||||||
|
/* ---- M3 显式下标优先于续读;open_dir 保持续读语义 ---- */
|
||||||
|
mr_reader_open_dir_at(book, 77); /* 离开时会把 77 落盘 */
|
||||||
|
pump_ms(300);
|
||||||
|
meow_view_page_pop();
|
||||||
|
mr_reader_open_dir_at(book, 12); /* 显式下标赢过续读记录 77 */
|
||||||
|
CHECK(mr_reader_ui_page() == 12, "explicit start beats resume record (%d)",
|
||||||
|
mr_reader_ui_page());
|
||||||
|
pump_ms(300);
|
||||||
|
meow_view_page_pop();
|
||||||
|
mr_reader_open_dir(book); /* 旧入口 = 续读 */
|
||||||
|
CHECK(mr_reader_ui_page() == 12, "open_dir resumes saved page (%d)", mr_reader_ui_page());
|
||||||
|
pump_ms(300);
|
||||||
|
|
||||||
|
/* ---- M4 连续翻页(含异步解码)不崩、页码推进、内存有界 ---- */
|
||||||
|
int ok_move = 1;
|
||||||
|
for (int i = 0; i < 12; i++) {
|
||||||
|
int before = mr_reader_ui_page();
|
||||||
|
tap(40, 300);
|
||||||
|
pump_ms(260);
|
||||||
|
meow_ui_paint_full(); /* 绘制路径覆盖(P2-5) */
|
||||||
|
if (mr_reader_ui_page() == before) ok_move = 0;
|
||||||
|
}
|
||||||
|
CHECK(mr_reader_ui_blit_draws() + mr_reader_ui_scaled_draws() > draws0,
|
||||||
|
"draw path executed across flips");
|
||||||
|
CHECK(ok_move, "12 consecutive flips advanced the page");
|
||||||
|
CHECK(mr_reader_ui_resident_kb() <= 9 * 1024, "resident bounded on 140p book (%ldKB)",
|
||||||
|
mr_reader_ui_resident_kb());
|
||||||
|
|
||||||
|
/* 坏图册:翻过坏页不崩、可继续翻。 */
|
||||||
|
meow_view_page_pop();
|
||||||
|
mr_reader_open_dir_at(bad, 0);
|
||||||
|
pump_ms(600);
|
||||||
|
int bad_ok = 1;
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
int before = mr_reader_ui_page();
|
||||||
|
tap(40, 300);
|
||||||
|
pump_ms(300);
|
||||||
|
if (mr_reader_ui_page() == before && before < 3) bad_ok = 0;
|
||||||
|
}
|
||||||
|
CHECK(bad_ok, "flip across corrupt/huge pages keeps working (page=%d)", mr_reader_ui_page());
|
||||||
|
CHECK(mr_reader_ui_resident_kb() <= 9 * 1024,
|
||||||
|
"huge page skipped without allocating it (%ldKB)", mr_reader_ui_resident_kb());
|
||||||
|
CHECK(mr_reader_ui_failed_pages() == 2,
|
||||||
|
"garbage + oversized page both degraded to placeholder (failed=%d)",
|
||||||
|
mr_reader_ui_failed_pages());
|
||||||
|
CHECK(meow_ui_activity_active() == ACT_READER, "still in reader after corrupt page");
|
||||||
|
|
||||||
|
/* ---- M5 条漫:140 页排版 + 跳页落位 ---- */
|
||||||
|
meow_view_page_pop();
|
||||||
|
mr_reader_open_dir_at(book, 0);
|
||||||
|
pump_ms(400);
|
||||||
|
CHECK(mr_reader_ui_count() == BOOK_PAGES, "back on the 140p book (%d)", mr_reader_ui_count());
|
||||||
|
CHECK(tap(mode_x, bar_y) == 1, "mode icon consumed");
|
||||||
|
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "webtoon mode on 140 pages");
|
||||||
|
pump_ms(500);
|
||||||
|
CHECK(tap(jump_x, bar_y) == 1, "jump icon consumed");
|
||||||
|
CHECK(mr_reader_ui_jump_open() == 1, "slider open");
|
||||||
|
reader_on_touch_down(sx + sw, slider_y);
|
||||||
|
reader_on_tap(sx + sw, slider_y);
|
||||||
|
pump_ms(900);
|
||||||
|
CHECK(mr_reader_ui_jump_pending() == 0, "jump landed in webtoon");
|
||||||
|
{
|
||||||
|
int d0 = mr_reader_ui_blit_draws() + mr_reader_ui_scaled_draws();
|
||||||
|
meow_ui_paint_full();
|
||||||
|
meow_ui_paint_full(); /* 条漫绘制路径覆盖(P2-5) */
|
||||||
|
CHECK(mr_reader_ui_blit_draws() + mr_reader_ui_scaled_draws() > d0,
|
||||||
|
"draw path executed (webtoon)");
|
||||||
|
}
|
||||||
|
CHECK(mr_reader_ui_page() >= BOOK_PAGES - 3, "jumped near last page (got %d)",
|
||||||
|
mr_reader_ui_page());
|
||||||
|
CHECK(mr_reader_ui_resident_kb() <= 9 * 1024, "resident bounded in webtoon (%ldKB)",
|
||||||
|
mr_reader_ui_resident_kb());
|
||||||
|
|
||||||
|
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" : "many-pages reader checks passed");
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
/* test_reader_perf.c — 阅读页绘页热路径回归(需要 SDK 源码树,宿主跑真实 view/anim/img)。
|
||||||
|
*
|
||||||
|
* 用户反馈:「翻页很卡,横翻竖翻都卡」「40 页也卡」。真因是每帧一次 meow_ui_img_scaled:
|
||||||
|
* 解码尺寸 ≠ 排版要的显示尺寸 → 每帧重采样 + 每帧 1MB 级临时缓冲 malloc/free。
|
||||||
|
* 修法:解码期直接按**排版要的几何**落地(翻页=fit/zoom,条漫=strip 排版高度),翻页只 blit。
|
||||||
|
*
|
||||||
|
* 夹具刻意用 **600x960(长宽比 1.6)**:条漫常见比例,显示高度 768 > 屏长边 720——
|
||||||
|
* 早期夹具用 600x800(1.33)恰好绕过「钳到屏长边」的边界,给过过于乐观的信号(评审 P1-1)。
|
||||||
|
*
|
||||||
|
* 断言(每项都看 blit/scaled 计数增量):
|
||||||
|
* P1 适宽进入:blit > 0 / scaled = 0;
|
||||||
|
* P2 连续翻页(点击来回 10 次):scaled 不增长;
|
||||||
|
* P3 翻页模式**跟手拖动**:scaled 不增长且确实画了页;
|
||||||
|
* P4 条漫模式(切换后滚动):scaled 不增长、blit 增长;
|
||||||
|
* P5 「先点适高,再切条漫」:scaled 不增长(评审点名的第二入口);
|
||||||
|
* P6 双击放大再双击缩回:缩回后按新几何重解 → scaled 不增长(P2-1);
|
||||||
|
* P7 静止若干帧不产生新解码(无重解抖动/活锁,P2-2);
|
||||||
|
* P8 驻留页图有字节上界(<= 9MB)。
|
||||||
|
*
|
||||||
|
* 编译:test/run_tests.sh(与 test_open_book_host 同口径;只发布 meow_ui.a 时 SKIP)。
|
||||||
|
*/
|
||||||
|
#include "tuku_internal.h"
|
||||||
|
#include "ui/reader/reader.h"
|
||||||
|
#include "meow_ui_internal.h"
|
||||||
|
#include "logic/reader/nav.h"
|
||||||
|
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#define PAGE_W 600
|
||||||
|
#define PAGE_H 960 /* 长宽比 1.6:适宽显示 480x768(高 > 屏长边 720) */
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
static void put16(unsigned char *p, unsigned v) { p[0] = (unsigned char)v; p[1] = (unsigned char)(v >> 8); }
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 真双击:两次点击间隔 < MR_DOUBLE_MS(240ms)。 */
|
||||||
|
static void double_tap(int x, int y)
|
||||||
|
{
|
||||||
|
reader_on_touch_down(x, y);
|
||||||
|
reader_on_tap(x, y);
|
||||||
|
pump_ms(60);
|
||||||
|
reader_on_touch_down(x, y);
|
||||||
|
reader_on_tap(x, y);
|
||||||
|
pump_ms(120);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw_once(void) { meow_ui_paint_full(); }
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
const char *base = getenv("TMPDIR");
|
||||||
|
if (!base || !base[0]) base = ".";
|
||||||
|
char tmpl[512];
|
||||||
|
snprintf(tmpl, sizeof tmpl, "%s/mr_perf_XXXXXX", base);
|
||||||
|
char *data = mkdtemp(tmpl);
|
||||||
|
if (!data) { printf("SKIP: mkdtemp failed\n"); return 0; }
|
||||||
|
setenv("MEOW_DATA_DIR", data, 1);
|
||||||
|
|
||||||
|
char book[MR_PATH_MAX];
|
||||||
|
snprintf(book, sizeof book, "./mr_perf_books_%ld/v1", (long)getpid());
|
||||||
|
if (mkdir_p(book) != 0) { printf("SKIP: mkdir failed\n"); return 0; }
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
char p[MR_PATH_MAX];
|
||||||
|
snprintf(p, sizeof p, "%s/p%d.bmp", book, i + 1);
|
||||||
|
if (write_bmp24(p, PAGE_W, PAGE_H, (unsigned)i) != 0) {
|
||||||
|
printf("SKIP: bmp write failed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
char book2[MR_PATH_MAX];
|
||||||
|
snprintf(book2, sizeof book2, "./mr_perf_books_%ld/v2", (long)getpid());
|
||||||
|
if (mkdir_p(book2) != 0) { printf("SKIP: mkdir failed\n"); return 0; }
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
char p[MR_PATH_MAX];
|
||||||
|
snprintf(p, sizeof p, "%s/q%d.bmp", book2, i + 1);
|
||||||
|
if (write_bmp24(p, 800, 1280, (unsigned)(i + 7)) != 0) {
|
||||||
|
printf("SKIP: bmp write failed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
int w = meow_ui_width();
|
||||||
|
int mode_x = w - 10 - 48 * 3 + 24; /* 顶栏动作 0 = 模式 */
|
||||||
|
int fit_x = mode_x + 48 * 2; /* 动作 2 = fit */
|
||||||
|
int bar_y = 28;
|
||||||
|
|
||||||
|
/* ---- P1 进入(适宽):解码 480x768 == 显示框 → 纯 blit ---- */
|
||||||
|
mr_reader_open_dir(book);
|
||||||
|
CHECK(mr_reader_ui_count() == 4, "book pages=%d", mr_reader_ui_count());
|
||||||
|
pump_ms(800);
|
||||||
|
draw_once();
|
||||||
|
int b0 = mr_reader_ui_blit_draws(), s0 = mr_reader_ui_scaled_draws();
|
||||||
|
draw_once();
|
||||||
|
CHECK(mr_reader_ui_blit_draws() > b0, "paged: page drawn with native blit");
|
||||||
|
CHECK(mr_reader_ui_scaled_draws() == s0,
|
||||||
|
"paged fit-width: zero resample (scaled %d -> %d)",
|
||||||
|
s0, mr_reader_ui_scaled_draws());
|
||||||
|
|
||||||
|
/* ---- P2 连续翻页 ---- */
|
||||||
|
int bt = mr_reader_ui_blit_draws(), st = mr_reader_ui_scaled_draws();
|
||||||
|
int pages_ok = 1;
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
int before = mr_reader_ui_page();
|
||||||
|
tap(i % 2 == 0 ? 40 : w - 40, 300); /* RTL:左 1/3=下一页、右 1/3=上一页 */
|
||||||
|
pump_ms(350);
|
||||||
|
if (mr_reader_ui_page() == before) pages_ok = 0;
|
||||||
|
draw_once();
|
||||||
|
}
|
||||||
|
CHECK(pages_ok, "10 flips advanced the page each time");
|
||||||
|
CHECK(mr_reader_ui_blit_draws() - bt >= 10, "flips drew pages (%d blits)",
|
||||||
|
mr_reader_ui_blit_draws() - bt);
|
||||||
|
CHECK(mr_reader_ui_scaled_draws() == st,
|
||||||
|
"flip hot path: zero resample (scaled %d -> %d)", st, mr_reader_ui_scaled_draws());
|
||||||
|
|
||||||
|
/* ---- P3 翻页模式跟手拖动(评审实测的 scaled 路径)---- */
|
||||||
|
st = mr_reader_ui_scaled_draws();
|
||||||
|
bt = mr_reader_ui_blit_draws();
|
||||||
|
reader_on_touch_down(400, 300);
|
||||||
|
for (int i = 1; i <= 8; i++) {
|
||||||
|
reader_on_touch_move(400 - i * 25, 300);
|
||||||
|
draw_once();
|
||||||
|
}
|
||||||
|
reader_on_tap(200, 300); /* 松手(未过阈值 → 回弹) */
|
||||||
|
pump_ms(300);
|
||||||
|
draw_once();
|
||||||
|
CHECK(mr_reader_ui_blit_draws() > bt, "paged drag drew pages (%d blits)",
|
||||||
|
mr_reader_ui_blit_draws() - bt);
|
||||||
|
CHECK(mr_reader_ui_scaled_draws() == st,
|
||||||
|
"paged drag: zero resample (scaled %d -> %d)", st, mr_reader_ui_scaled_draws());
|
||||||
|
|
||||||
|
/* ---- P4 条漫模式:切换后滚动,长宽比 1.6 页也必须零重采样 ---- */
|
||||||
|
tap(mode_x, bar_y);
|
||||||
|
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "switched to webtoon");
|
||||||
|
pump_ms(800);
|
||||||
|
draw_once();
|
||||||
|
st = mr_reader_ui_scaled_draws();
|
||||||
|
bt = mr_reader_ui_blit_draws();
|
||||||
|
reader_on_touch_down(240, 600);
|
||||||
|
for (int i = 1; i <= 8; i++) {
|
||||||
|
reader_on_touch_move(240, 600 - i * 30);
|
||||||
|
draw_once();
|
||||||
|
}
|
||||||
|
reader_on_tap(240, 300);
|
||||||
|
pump_ms(400);
|
||||||
|
draw_once();
|
||||||
|
CHECK(mr_reader_ui_blit_draws() > bt, "webtoon scroll drew pages (%d blits)",
|
||||||
|
mr_reader_ui_blit_draws() - bt);
|
||||||
|
CHECK(mr_reader_ui_scaled_draws() == st,
|
||||||
|
"webtoon (1.6 page): zero resample (scaled %d -> %d)", st,
|
||||||
|
mr_reader_ui_scaled_draws());
|
||||||
|
|
||||||
|
/* ---- P5 「先适高、再切条漫」:strip 恒按屏宽排版,解码必须跟随 ---- */
|
||||||
|
tap(mode_x, bar_y); /* 回翻页 */
|
||||||
|
CHECK(mr_reader_ui_mode() == MR_MODE_PAGED, "back to paged");
|
||||||
|
pump_ms(500);
|
||||||
|
tap(fit_x, bar_y); /* 适宽 → 适高 */
|
||||||
|
CHECK(mr_reader_ui_fit() == MR_FIT_HEIGHT, "fit-height selected (got %d)",
|
||||||
|
mr_reader_ui_fit());
|
||||||
|
pump_ms(700);
|
||||||
|
tap(mode_x, bar_y); /* 适高档 → 条漫 */
|
||||||
|
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "webtoon after fit-height");
|
||||||
|
pump_ms(900);
|
||||||
|
draw_once();
|
||||||
|
st = mr_reader_ui_scaled_draws();
|
||||||
|
bt = mr_reader_ui_blit_draws();
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
reader_on_touch_down(240, 600);
|
||||||
|
for (int k = 1; k <= 4; k++) {
|
||||||
|
reader_on_touch_move(240, 600 - k * 30);
|
||||||
|
draw_once();
|
||||||
|
}
|
||||||
|
reader_on_tap(240, 300);
|
||||||
|
pump_ms(250);
|
||||||
|
draw_once();
|
||||||
|
}
|
||||||
|
CHECK(mr_reader_ui_blit_draws() > bt, "webtoon after fit-height drew pages (%d blits)",
|
||||||
|
mr_reader_ui_blit_draws() - bt);
|
||||||
|
CHECK(mr_reader_ui_scaled_draws() == st,
|
||||||
|
"fit-height -> webtoon: zero resample (scaled %d -> %d)", st,
|
||||||
|
mr_reader_ui_scaled_draws());
|
||||||
|
|
||||||
|
/* ---- P6 双击放大再缩回(评审 P2-1:缩回后必须按新几何重解)---- */
|
||||||
|
tap(mode_x, bar_y); /* 回翻页 */
|
||||||
|
CHECK(mr_reader_ui_mode() == MR_MODE_PAGED, "paged for zoom test");
|
||||||
|
pump_ms(600);
|
||||||
|
draw_once();
|
||||||
|
double_tap(240, 360); /* 放大 */
|
||||||
|
pump_ms(700);
|
||||||
|
draw_once();
|
||||||
|
double_tap(240, 360); /* 缩回 */
|
||||||
|
pump_ms(800);
|
||||||
|
draw_once();
|
||||||
|
st = mr_reader_ui_scaled_draws();
|
||||||
|
for (int i = 0; i < 5; i++) draw_once();
|
||||||
|
CHECK(mr_reader_ui_scaled_draws() == st,
|
||||||
|
"zoom out: page re-decoded at fit geometry, zero resample (scaled %d -> %d)",
|
||||||
|
st, mr_reader_ui_scaled_draws());
|
||||||
|
|
||||||
|
/* ---- P7 静止不抖:无新解码(重解活锁回归,P2-2)---- */
|
||||||
|
pump_ms(300);
|
||||||
|
int d0 = mr_reader_ui_decode_count();
|
||||||
|
for (int i = 0; i < 10; i++) { draw_once(); pump_ms(40); }
|
||||||
|
CHECK(mr_reader_ui_decode_count() == d0,
|
||||||
|
"idle: no extra decodes (%d -> %d)", d0, mr_reader_ui_decode_count());
|
||||||
|
|
||||||
|
/* ---- P8 驻留内存上界 ---- */
|
||||||
|
long kb = mr_reader_ui_resident_kb();
|
||||||
|
printf(" info: 600x960 resident=%ldKB blit=%d scaled=%d decodes=%d\n", kb,
|
||||||
|
mr_reader_ui_blit_draws(), mr_reader_ui_scaled_draws(),
|
||||||
|
mr_reader_ui_decode_count());
|
||||||
|
CHECK(kb <= 9 * 1024, "resident page images bounded (got %ldKB)", kb);
|
||||||
|
|
||||||
|
/* ---- P9 800x1280(评审表格另一档):翻页 + 条漫都必须零重采样 ---- */
|
||||||
|
meow_view_page_pop();
|
||||||
|
mr_reader_open_dir(book2);
|
||||||
|
pump_ms(800);
|
||||||
|
draw_once();
|
||||||
|
st = mr_reader_ui_scaled_draws();
|
||||||
|
bt = mr_reader_ui_blit_draws();
|
||||||
|
reader_on_touch_down(400, 300);
|
||||||
|
for (int i = 1; i <= 6; i++) {
|
||||||
|
reader_on_touch_move(400 - i * 25, 300);
|
||||||
|
draw_once();
|
||||||
|
}
|
||||||
|
reader_on_tap(200, 300);
|
||||||
|
pump_ms(300);
|
||||||
|
draw_once();
|
||||||
|
CHECK(mr_reader_ui_blit_draws() > bt && mr_reader_ui_scaled_draws() == st,
|
||||||
|
"800x1280 paged drag: zero resample (blit +%d, scaled +%d)",
|
||||||
|
mr_reader_ui_blit_draws() - bt, mr_reader_ui_scaled_draws() - st);
|
||||||
|
tap(mode_x, bar_y); /* 条漫 */
|
||||||
|
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "800x1280 webtoon");
|
||||||
|
pump_ms(800);
|
||||||
|
draw_once();
|
||||||
|
st = mr_reader_ui_scaled_draws();
|
||||||
|
bt = mr_reader_ui_blit_draws();
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
reader_on_touch_down(240, 600);
|
||||||
|
for (int k = 1; k <= 4; k++) {
|
||||||
|
reader_on_touch_move(240, 600 - k * 30);
|
||||||
|
draw_once();
|
||||||
|
}
|
||||||
|
reader_on_tap(240, 300);
|
||||||
|
pump_ms(250);
|
||||||
|
draw_once();
|
||||||
|
}
|
||||||
|
CHECK(mr_reader_ui_blit_draws() > bt,
|
||||||
|
"800x1280 webtoon drew pages (%d blits)", mr_reader_ui_blit_draws() - bt);
|
||||||
|
CHECK(mr_reader_ui_scaled_draws() == st,
|
||||||
|
"800x1280 webtoon: zero resample (scaled %d -> %d)", st,
|
||||||
|
mr_reader_ui_scaled_draws());
|
||||||
|
|
||||||
|
meow_view_page_pop();
|
||||||
|
meow_view_free(reader);
|
||||||
|
meow_view_free(lib);
|
||||||
|
meow_ui_shutdown();
|
||||||
|
|
||||||
|
/* ---- P10 大屏(1080x1920):fit 目标 >3MB 被字节上限钳住,不得反复重解(P2-2)---- */
|
||||||
|
meow_ui_opts o2 = { 1080, 1920 };
|
||||||
|
if (meow_ui_init(&o2) == 0) {
|
||||||
|
lib = meow_view_create(ACT_LIBRARY, MEOW_VIEW_ACTIVITY, &lh, NULL);
|
||||||
|
reader = reader_view_create();
|
||||||
|
meow_view_page_push(ACT_LIBRARY);
|
||||||
|
mr_reader_open_dir(book);
|
||||||
|
pump_ms(1200);
|
||||||
|
meow_ui_paint_full();
|
||||||
|
int dbig = mr_reader_ui_decode_count();
|
||||||
|
for (int i = 0; i < 10; i++) { meow_ui_paint_full(); pump_ms(40); }
|
||||||
|
CHECK(mr_reader_ui_decode_count() == dbig,
|
||||||
|
"large screen: clamped decode is stable, no live-lock (%d -> %d)",
|
||||||
|
dbig, mr_reader_ui_decode_count());
|
||||||
|
printf(" info: large screen resident=%ldKB decodes=%d\n",
|
||||||
|
mr_reader_ui_resident_kb(), mr_reader_ui_decode_count());
|
||||||
|
meow_view_page_pop();
|
||||||
|
meow_view_free(reader);
|
||||||
|
meow_view_free(lib);
|
||||||
|
meow_ui_shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
rm_rf(book);
|
||||||
|
rm_rf(book2);
|
||||||
|
char rootdir[MR_PATH_MAX];
|
||||||
|
snprintf(rootdir, sizeof rootdir, "./mr_perf_books_%ld", (long)getpid());
|
||||||
|
rm_rf(rootdir);
|
||||||
|
rm_rf(data);
|
||||||
|
|
||||||
|
printf("%s: %s\n", g_fail ? "FAIL" : "PASS",
|
||||||
|
g_fail ? "see failures above" : "reader flip hot-path checks passed");
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
+387
-89
@@ -10,6 +10,13 @@
|
|||||||
* 页源:logic/reader/page_source 的 mr_book 契约(count/meta/open/close)。条漫排版用
|
* 页源:logic/reader/page_source 的 mr_book 契约(count/meta/open/close)。条漫排版用
|
||||||
* meta() 不解码像素;解码入口走 SDK M12 预算解码 meow_img_load_file_dim(JPEG 解码期缩解,
|
* meta() 不解码像素;解码入口走 SDK M12 预算解码 meow_img_load_file_dim(JPEG 解码期缩解,
|
||||||
* PNG/BMP 整幅解码后降采样——超大头先 meow_img_probe_file 预判,超过预算跳过并告警)。
|
* PNG/BMP 整幅解码后降采样——超大头先 meow_img_probe_file 预判,超过预算跳过并告警)。
|
||||||
|
*
|
||||||
|
* 性能(M17):解码目标 = **排版真正要的显示框**(翻页=fit/zoom;条漫=strip 排版高度,
|
||||||
|
* 宽恒为视口宽),不是屏长边,所以翻页/滚动热路径只有原生 blit:不做 meow_ui_img_scaled
|
||||||
|
* 的每帧重采样,也不再每帧 malloc 1MB 级临时缓冲。几何变化(fit/缩放/切模式)时按新几何
|
||||||
|
* 重解一次;拖动/吸附帧只 invalidate 内容区(顶栏静态不重绘)。
|
||||||
|
* 内存:单页显示缓存 ≤3MB、整窗 ≤9MB(超限淘汰最远页),解码中间峰值 >12MB 的页直接跳过
|
||||||
|
* 并显示占位——页数增长不会线性吃内存(常驻只与解码窗大小有关)。
|
||||||
* 方向:一律读 M3 的 mr_reader_get_direction()(默认 RTL),不另立全局。
|
* 方向:一律读 M3 的 mr_reader_get_direction()(默认 RTL),不另立全局。
|
||||||
*
|
*
|
||||||
* 模式(M7):翻页/条漫是一等公民。阅读 chrome 的图标按钮一键切换;每册的模式与 last page
|
* 模式(M7):翻页/条漫是一等公民。阅读 chrome 的图标按钮一键切换;每册的模式与 last page
|
||||||
@@ -64,8 +71,11 @@
|
|||||||
#define MR_JUMP_TIMEOUT_MS 2500 /* 目标页迟迟解不出来 → 降级落位并提示 */
|
#define MR_JUMP_TIMEOUT_MS 2500 /* 目标页迟迟解不出来 → 降级落位并提示 */
|
||||||
#define MR_CONT_SAVE_MS 1000 /* 续读状态落盘节流 */
|
#define MR_CONT_SAVE_MS 1000 /* 续读状态落盘节流 */
|
||||||
#define MR_TOAST_MS 1400
|
#define MR_TOAST_MS 1400
|
||||||
#define MR_LOAD_RAW_WARN (12L * 1024 * 1024) /* 非缩解格式原始 RGBA 超此值提示残余风险 */
|
#define MR_LOAD_RAW_WARN (8L * 1024 * 1024) /* 非缩解格式原始 RGBA 超此值提示残余风险 */
|
||||||
#define MR_LOAD_RAW_CAP (40L * 1024 * 1024) /* 超此值直接跳过(防 OOM;页显示占位) */
|
#define MR_DECODE_PEAK_CAP (12L * 1024 * 1024) /* 单次解码中间缓冲峰值上限(超则跳过,防 OOM 闪退) */
|
||||||
|
#define MR_PAGE_BYTES_CAP (3L * 1024 * 1024) /* 单页「显示尺寸」缓存字节上限 */
|
||||||
|
#define MR_RESIDENT_CAP (9L * 1024 * 1024) /* 页图缓存总字节上限(超则淘汰最远页) */
|
||||||
|
#define MR_SLOT_TOL 2 /* 缓存图与显示框允许的像素偏差(≤2 走原生 blit,不重采样) */
|
||||||
|
|
||||||
#define MR_REQ_MAX 8
|
#define MR_REQ_MAX 8
|
||||||
#define MR_RES_MAX 4
|
#define MR_RES_MAX 4
|
||||||
@@ -75,6 +85,7 @@
|
|||||||
|
|
||||||
static mr_page_ctrl g_ctrl;
|
static mr_page_ctrl g_ctrl;
|
||||||
static char g_dir[MR_PATH_MAX];
|
static char g_dir[MR_PATH_MAX];
|
||||||
|
static int g_start_at = MR_READER_START_RESUME; /* 下次 on_enter 的显式起始页(-1=续读) */
|
||||||
|
|
||||||
static mr_book_handle g_handle; /* 目录册或档案册(M11 统一开册) */
|
static mr_book_handle g_handle; /* 目录册或档案册(M11 统一开册) */
|
||||||
static mr_book g_book;
|
static mr_book g_book;
|
||||||
@@ -127,12 +138,24 @@ typedef struct page_slot {
|
|||||||
int idx; /* -1=空 */
|
int idx; /* -1=空 */
|
||||||
meow_img *img; /* 已 downscale 的页图 */
|
meow_img *img; /* 已 downscale 的页图 */
|
||||||
int src_w, src_h;
|
int src_w, src_h;
|
||||||
|
int want_w, want_h; /* 本次解码按哪个显示框落的(几何判据,见 slot_ready) */
|
||||||
} page_slot;
|
} page_slot;
|
||||||
static page_slot g_slot[MR_SLOTS];
|
static page_slot g_slot[MR_SLOTS];
|
||||||
|
|
||||||
/* 解码失败页标记:避免对坏图/超大图无限重试,UI 显示「图片无法显示」而不是一直加载中。 */
|
/* 解码失败页标记:避免对坏图/超大图无限重试,UI 显示「图片无法显示」而不是一直加载中。 */
|
||||||
static unsigned char g_page_fail[(MR_BOOK_MAX_PAGES + 7) / 8];
|
static unsigned char g_page_fail[(MR_BOOK_MAX_PAGES + 7) / 8];
|
||||||
|
|
||||||
|
/* 每页原始像素尺寸缓存(打开时按 count 分配;0,0=未知)。绘制/排版几何只查这里,
|
||||||
|
* 不在每帧去 probe 文件(dir_ops_meta 是 fopen+读头,每帧一次会直接拖垮翻页)。 */
|
||||||
|
typedef struct page_meta { int w, h; } page_meta;
|
||||||
|
static page_meta *g_meta;
|
||||||
|
|
||||||
|
/* 本次解码计划(主线程在 queue_page 里算好,经 page_req 传给 worker;worker 在
|
||||||
|
* mr_book_open 前写入这几个静态量,解码回调只读它们 → 后台线程不再读 UI 全局几何)。 */
|
||||||
|
static int g_plan_want_w, g_plan_want_h; /* 显示框(几何,未钳制) */
|
||||||
|
static int g_plan_long; /* 实际解码长边(受单页字节上限钳制) */
|
||||||
|
static volatile int g_decode_count; /* 累计解码次数(观察重解抖动/活锁) */
|
||||||
|
|
||||||
static int page_failed(int idx)
|
static int page_failed(int idx)
|
||||||
{
|
{
|
||||||
if (idx < 0 || idx >= MR_BOOK_MAX_PAGES) return 0;
|
if (idx < 0 || idx >= MR_BOOK_MAX_PAGES) return 0;
|
||||||
@@ -145,9 +168,20 @@ static void page_mark_failed(int idx)
|
|||||||
g_page_fail[idx >> 3] |= (unsigned char)(1u << (idx & 7));
|
g_page_fail[idx >> 3] |= (unsigned char)(1u << (idx & 7));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 后台解码队列 ---- */
|
/* ---- 后台解码队列(请求携带解码几何,结果带回同一条几何) ---- */
|
||||||
typedef struct page_req { int idx; unsigned gen; } page_req;
|
typedef struct page_req {
|
||||||
typedef struct page_res { int idx; unsigned gen; meow_img *img; int fail; } page_res;
|
int idx;
|
||||||
|
unsigned gen;
|
||||||
|
int want_w, want_h; /* 按哪个显示框解码(主线程算) */
|
||||||
|
int long_side; /* 实际解码长边(≤ 单页字节上限) */
|
||||||
|
} page_req;
|
||||||
|
typedef struct page_res {
|
||||||
|
int idx;
|
||||||
|
unsigned gen;
|
||||||
|
meow_img *img;
|
||||||
|
int fail;
|
||||||
|
int want_w, want_h;
|
||||||
|
} page_res;
|
||||||
|
|
||||||
static pthread_t g_worker;
|
static pthread_t g_worker;
|
||||||
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
|
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||||
@@ -183,6 +217,17 @@ static int g_wo_last_y; /* 条漫:上一次 move 的 y */
|
|||||||
static int g_wo_y0; /* 条漫:按下点 y(slop 判定) */
|
static int g_wo_y0; /* 条漫:按下点 y(slop 判定) */
|
||||||
static int g_wo_pull; /* 条漫:到边后继续拉的累计量(>0 往末页方向) */
|
static int g_wo_pull; /* 条漫:到边后继续拉的累计量(>0 往末页方向) */
|
||||||
|
|
||||||
|
/* ---- 绘页路径计数(宿主测试观察:翻页热路径应全部是 blit)---- */
|
||||||
|
#ifdef MR_READER_HOST_TEST
|
||||||
|
static int g_blit_draws;
|
||||||
|
static int g_scaled_draws;
|
||||||
|
#define NOTE_BLIT() (g_blit_draws++)
|
||||||
|
#define NOTE_SCALED() (g_scaled_draws++)
|
||||||
|
#else
|
||||||
|
#define NOTE_BLIT() ((void)0)
|
||||||
|
#define NOTE_SCALED() ((void)0)
|
||||||
|
#endif
|
||||||
|
|
||||||
/* ---- 单击延迟(双击判定窗口)---- */
|
/* ---- 单击延迟(双击判定窗口)---- */
|
||||||
static int g_tap_pending;
|
static int g_tap_pending;
|
||||||
static int g_tap_x, g_tap_y;
|
static int g_tap_x, g_tap_y;
|
||||||
@@ -197,6 +242,8 @@ static void mode_set(int mode, int from_page);
|
|||||||
static void toast(const char *msg);
|
static void toast(const char *msg);
|
||||||
static void slider_place(void);
|
static void slider_place(void);
|
||||||
static void update_title(void);
|
static void update_title(void);
|
||||||
|
static void invalidate_content(void);
|
||||||
|
static page_slot *slot_find(int idx);
|
||||||
static void cont_touch(void);
|
static void cont_touch(void);
|
||||||
static void cont_save_now(void);
|
static void cont_save_now(void);
|
||||||
static int current_page(void);
|
static int current_page(void);
|
||||||
@@ -213,34 +260,68 @@ static int path_is_jpeg(const char *path)
|
|||||||
return strcasecmp(dot, "jpg") == 0 || strcasecmp(dot, "jpeg") == 0;
|
return strcasecmp(dot, "jpg") == 0 || strcasecmp(dot, "jpeg") == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 由显示框(want_w×want_h)反推解码长边:默认就解到显示框大小(解码后可直接 blit),
|
||||||
|
* 只有单页缓存字节上限(MR_PAGE_BYTES_CAP)是硬上界——超长条漫在这里按比例收缩。
|
||||||
|
* 不再钳到屏长边:条漫页常见长宽比 1.6~1.8,显示高度本来就 > 屏长边,钳了必然每帧重采样。 */
|
||||||
|
static int plan_decode_long(int want_w, int want_h)
|
||||||
|
{
|
||||||
|
int l0 = want_w > want_h ? want_w : want_h;
|
||||||
|
int s0 = want_w > want_h ? want_h : want_w;
|
||||||
|
if (l0 <= 0) return 0;
|
||||||
|
int l = l0 < 64 ? 64 : l0;
|
||||||
|
while (l > 64) {
|
||||||
|
int s = (int)((long long)s0 * l / l0);
|
||||||
|
if ((long long)l * s * 4 <= MR_PAGE_BYTES_CAP) break;
|
||||||
|
l = l * 3 / 4;
|
||||||
|
}
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
static meow_img *reader_load_page(const char *path, void *ud, int maxdim)
|
static meow_img *reader_load_page(const char *path, void *ud, int maxdim)
|
||||||
{
|
{
|
||||||
(void)ud;
|
(void)ud;
|
||||||
if (!path || !path[0]) return NULL;
|
if (!path || !path[0]) return NULL;
|
||||||
|
|
||||||
|
/* 解码计划由 queue_page() 在主线程算好(want 显示框 + 实际长边),worker/主线程同步兜底
|
||||||
|
* 路径在 mr_book_open 前写入;无计划时退回旧预算 maxdim。 */
|
||||||
|
int want_w = g_plan_want_w, want_h = g_plan_want_h;
|
||||||
|
int dim = g_plan_long > 0 ? g_plan_long : maxdim;
|
||||||
|
|
||||||
/* 预算预判(M12):先读文件头拿原始尺寸,不解码像素。
|
/* 预算预判(M12):先读文件头拿原始尺寸,不解码像素。
|
||||||
* - 非缩解格式(PNG/BMP)原始 RGBA 超过预算 → 跳过,避免真机 OOM(页面显示占位);
|
* - 解码中间缓冲峰值超上限 → 跳过(页面显示占位),避免真机被 OOM 杀掉(闪退);
|
||||||
* - 超过告警线 → 记 WARN(残余风险,按 M12 报告口径如实提示)。 */
|
* - 峰值 = 解码缓冲 + 随后的 downscale 输出(≤单页缓存上限),两者会同时驻留;
|
||||||
|
* - 非缩解格式(PNG/BMP)原始 RGBA 超过告警线 → 记 WARN(残余风险)。 */
|
||||||
int pw = 0, ph = 0, ch = 0;
|
int pw = 0, ph = 0, ch = 0;
|
||||||
if (meow_img_probe_file(path, &pw, &ph, &ch) && pw > 0 && ph > 0) {
|
int have = meow_img_probe_file(path, &pw, &ph, &ch) && pw > 0 && ph > 0;
|
||||||
long long raw = (long long)pw * (long long)ph * 4;
|
if (have) {
|
||||||
if (!path_is_jpeg(path)) {
|
int l0 = pw > ph ? pw : ph;
|
||||||
if (raw > MR_LOAD_RAW_CAP) {
|
long long out_bytes = MR_PAGE_BYTES_CAP; /* downscale/缩解输出上限 */
|
||||||
meow_log(MEOW_LOG_WARN, "reader decode skip %dx%d (raw %lldKB > cap): %s",
|
if (want_w > 0 && want_h > 0 && (long long)want_w * want_h * 4 < out_bytes)
|
||||||
pw, ph, raw / 1024, path);
|
out_bytes = (long long)want_w * want_h * 4;
|
||||||
return NULL;
|
long long decode_bytes;
|
||||||
}
|
if (path_is_jpeg(path)) {
|
||||||
if (raw > MR_LOAD_RAW_WARN)
|
/* 估 stbi 缩解后的输出(与 SDK 的档位选择同式):仍超上限才跳过。 */
|
||||||
|
int s = 1;
|
||||||
|
while (s < 8 && (l0 / (s * 2)) >= dim) s *= 2;
|
||||||
|
decode_bytes = (long long)(pw / s) * (ph / s) * 4;
|
||||||
|
} else {
|
||||||
|
decode_bytes = (long long)pw * ph * 4; /* 整幅解码后降采样 */
|
||||||
|
if (decode_bytes > MR_LOAD_RAW_WARN)
|
||||||
meow_log(MEOW_LOG_WARN, "reader decode large non-scalable %dx%d (%lldKB): %s",
|
meow_log(MEOW_LOG_WARN, "reader decode large non-scalable %dx%d (%lldKB): %s",
|
||||||
pw, ph, raw / 1024, path);
|
pw, ph, decode_bytes / 1024, path);
|
||||||
|
}
|
||||||
|
if (decode_bytes + out_bytes > MR_DECODE_PEAK_CAP) {
|
||||||
|
meow_log(MEOW_LOG_WARN, "reader decode skip %dx%d (peak %lldKB > cap): %s",
|
||||||
|
pw, ph, (decode_bytes + out_bytes) / 1024, path);
|
||||||
|
return NULL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
meow_img *im = meow_img_load_file_dim(path, maxdim);
|
meow_img *im = meow_img_load_file_dim(path, dim);
|
||||||
if (!im) return NULL;
|
if (!im) return NULL;
|
||||||
/* 兜底:M12 之前的格式/未知格式若没被缩到预算内,再降一次(保持驻留纪律)。 */
|
/* 兜底:M12 之前的格式/未知格式若没被缩到预算内,再降一次(保持驻留纪律)。 */
|
||||||
if (maxdim > 0 && (meow_img_w(im) > maxdim || meow_img_h(im) > maxdim)) {
|
if (dim > 0 && (meow_img_w(im) > dim || meow_img_h(im) > dim)) {
|
||||||
meow_img *small = meow_img_downscale(im, maxdim);
|
meow_img *small = meow_img_downscale(im, dim);
|
||||||
if (small) { meow_img_free(im); im = small; }
|
if (small) { meow_img_free(im); im = small; }
|
||||||
}
|
}
|
||||||
return im;
|
return im;
|
||||||
@@ -291,6 +372,69 @@ static void log_mem(const char *tag)
|
|||||||
mem_kb("/proc/self/status", "VmRSS:"), resident_pages(), resident_bytes() / 1024);
|
mem_kb("/proc/self/status", "VmRSS:"), resident_pages(), resident_bytes() / 1024);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* 页原始尺寸缓存(绘制几何的唯一来源;每页只 probe 一次) */
|
||||||
|
/* ============================================================ */
|
||||||
|
static int page_meta_dims(int idx, int *w, int *h)
|
||||||
|
{
|
||||||
|
if (w) *w = 0;
|
||||||
|
if (h) *h = 0;
|
||||||
|
if (idx < 0 || idx >= g_count) return 0;
|
||||||
|
if (g_meta) {
|
||||||
|
if (g_meta[idx].w > 0 && g_meta[idx].h > 0) {
|
||||||
|
if (w) *w = g_meta[idx].w;
|
||||||
|
if (h) *h = g_meta[idx].h;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (g_meta[idx].w < 0) return 0; /* 探测过且未知:别再每帧 fopen 一遍 */
|
||||||
|
}
|
||||||
|
int sw = 0, sh = 0;
|
||||||
|
if (!mr_book_meta(&g_book, idx, &sw, &sh) || sw <= 0 || sh <= 0) {
|
||||||
|
if (g_meta) { g_meta[idx].w = -1; g_meta[idx].h = 0; }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (g_meta) { g_meta[idx].w = sw; g_meta[idx].h = sh; }
|
||||||
|
if (w) *w = sw;
|
||||||
|
if (h) *h = sh;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 显示框(fit + zoom):绘图与点击几何都走这里。未知名按 2:3 竖图占位。 */
|
||||||
|
static void page_target_size_at(int idx, int *w, int *h)
|
||||||
|
{
|
||||||
|
int sw = 0, sh = 0;
|
||||||
|
if (w) *w = 0;
|
||||||
|
if (h) *h = 0;
|
||||||
|
if (!w || !h) return;
|
||||||
|
if (!page_meta_dims(idx, &sw, &sh)) {
|
||||||
|
/* meta 不可用(未知/坏头):用已解码尺寸兜底(保持真实长宽比),
|
||||||
|
* 都没解出来才按 2:3 竖图占位——排版稳定且不会把页面拉变形。 */
|
||||||
|
page_slot *s = slot_find(idx);
|
||||||
|
if (s) { sw = s->src_w; sh = s->src_h; }
|
||||||
|
if (sw <= 0 || sh <= 0) {
|
||||||
|
sw = g_vw > 0 ? g_vw : 1;
|
||||||
|
sh = sw * 3 / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mr_nav_fit_size(g_fit, sw, sh, g_vw, g_vh, w, h) != 0) return;
|
||||||
|
if (g_zoom_pct != 100) mr_nav_zoom_size(*w, *h, g_zoom_pct, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 解码/绘制的目标显示框:条漫 = strip 排版几何(宽恒为视口宽、高由排版表定),
|
||||||
|
* 翻页 = fit/zoom。解码计划与「缓存是否就绪」必须用同一个函数:否则解出来的尺寸
|
||||||
|
* 和排版要的尺寸不同源,条漫长页(长宽比 > 屏高/屏宽)就每帧重采样(M17 评审 P1-1)。 */
|
||||||
|
static void page_geometry_at(int idx, int *w, int *h)
|
||||||
|
{
|
||||||
|
if (w) *w = 0;
|
||||||
|
if (h) *h = 0;
|
||||||
|
if (!w || !h) return;
|
||||||
|
if (g_mode == MR_MODE_WEBTOON && g_strip_ok) {
|
||||||
|
int ph = mr_strip_page_h(&g_strip, idx);
|
||||||
|
if (ph > 0) { *w = g_vw; *h = ph; return; }
|
||||||
|
}
|
||||||
|
page_target_size_at(idx, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================ */
|
/* ============================================================ */
|
||||||
/* 续读状态(逐册 last page + mode;写入节流,避免滚动时频繁写卡) */
|
/* 续读状态(逐册 last page + mode;写入节流,避免滚动时频繁写卡) */
|
||||||
/* ============================================================ */
|
/* ============================================================ */
|
||||||
@@ -351,6 +495,18 @@ static int in_window(int idx)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 该页此刻是否真的画在屏幕上(异步落位后只重绘这种情况)。 */
|
||||||
|
static int page_visible(int idx)
|
||||||
|
{
|
||||||
|
if (g_count <= 0 || idx < 0 || idx >= g_count) return 0;
|
||||||
|
if (g_mode == MR_MODE_WEBTOON && g_strip_ok) {
|
||||||
|
int f = mr_strip_page_at(&g_strip, g_woff);
|
||||||
|
int l = mr_strip_page_at(&g_strip, g_woff + g_vh - 1);
|
||||||
|
return idx >= f && idx <= l;
|
||||||
|
}
|
||||||
|
return idx == g_page || (g_slide_delta != 0 && idx == g_page + g_slide_delta);
|
||||||
|
}
|
||||||
|
|
||||||
static page_slot *slot_find(int idx)
|
static page_slot *slot_find(int idx)
|
||||||
{
|
{
|
||||||
if (idx < 0) return NULL;
|
if (idx < 0) return NULL;
|
||||||
@@ -366,6 +522,7 @@ static void slot_clear(int i)
|
|||||||
g_slot[i].img = NULL;
|
g_slot[i].img = NULL;
|
||||||
g_slot[i].idx = -1;
|
g_slot[i].idx = -1;
|
||||||
g_slot[i].src_w = g_slot[i].src_h = 0;
|
g_slot[i].src_w = g_slot[i].src_h = 0;
|
||||||
|
g_slot[i].want_w = g_slot[i].want_h = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void slots_free_all(void)
|
static void slots_free_all(void)
|
||||||
@@ -373,25 +530,76 @@ static void slots_free_all(void)
|
|||||||
for (int i = 0; i < MR_SLOTS; i++) slot_clear(i);
|
for (int i = 0; i < MR_SLOTS; i++) slot_clear(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 放进缓存;缓存满时淘汰离焦点页最远的一张。返回 1=已放入。 */
|
/* 缓存图是否按「当前显示框」解出来的:比较 slot 记录的 want 几何与当前几何。
|
||||||
static int slot_put(int idx, meow_img *img)
|
* 用几何(而不是 src 像素尺寸)判定,才能同时满足三件事:
|
||||||
|
* - fit/zoom/模式变化 → 几何变 → 重解一次(放大再缩回、适高→条漫都能回到纯 blit);
|
||||||
|
* - 解码被单页字节上限钳住(超长条漫/大屏)→ want 不变 → 不再反复重解(无活锁);
|
||||||
|
* - 源图本身小于显示框(SDK 不做上采样)→ want 不变 → 不会每 tick 重排。 */
|
||||||
|
static int slot_ready(int idx)
|
||||||
|
{
|
||||||
|
page_slot *s = slot_find(idx);
|
||||||
|
if (!s) return 0;
|
||||||
|
int w = 0, h = 0;
|
||||||
|
page_geometry_at(idx, &w, &h);
|
||||||
|
if (w <= 0 || h <= 0) return 1;
|
||||||
|
if (s->want_w <= 0 || s->want_h <= 0) return 0;
|
||||||
|
return abs(w - s->want_w) <= MR_SLOT_TOL && abs(h - s->want_h) <= MR_SLOT_TOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 淘汰离焦点页最远的一张(不淘汰 focus 与 except)。返回被清掉的槽下标,无则 -1。 */
|
||||||
|
static int slot_evict_far(int except)
|
||||||
{
|
{
|
||||||
if (!img || idx < 0) return 0;
|
|
||||||
int focus = focus_page();
|
int focus = focus_page();
|
||||||
int empty = -1, far = -1, far_d = -1;
|
int far = -1, far_d = -1;
|
||||||
for (int i = 0; i < MR_SLOTS; i++) {
|
for (int i = 0; i < MR_SLOTS; i++) {
|
||||||
if (g_slot[i].idx == idx && g_slot[i].img) { meow_img_free(img); return 0; }
|
if (i == except || !g_slot[i].img) continue;
|
||||||
if (!g_slot[i].img && empty < 0) empty = i;
|
if (g_slot[i].idx == focus) continue;
|
||||||
int d = !g_slot[i].img ? -1 : abs(g_slot[i].idx - focus);
|
int d = abs(g_slot[i].idx - focus);
|
||||||
if (d > far_d) { far_d = d; far = i; }
|
if (d > far_d) { far_d = d; far = i; }
|
||||||
}
|
}
|
||||||
int i = empty >= 0 ? empty : far;
|
if (far >= 0) slot_clear(far);
|
||||||
|
return far;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 放进缓存;同页旧图直接替换(fit/zoom/模式变了要换尺寸)。返回 1=已放入。
|
||||||
|
* want_w/want_h = 这次解码按哪个显示框落的,交给 slot_ready 做几何判据。 */
|
||||||
|
static int slot_put(int idx, meow_img *img, int want_w, int want_h)
|
||||||
|
{
|
||||||
|
if (!img || idx < 0) 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 (i < 0) {
|
||||||
|
if (empty >= 0) {
|
||||||
|
i = empty;
|
||||||
|
} else { /* 全满:替换离焦点页最远的一张 */
|
||||||
|
int focus = focus_page(), far_d = -1;
|
||||||
|
for (int k = 0; k < MR_SLOTS; k++) {
|
||||||
|
int d = abs(g_slot[k].idx - focus);
|
||||||
|
if (d > far_d) { far_d = d; i = k; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (i < 0) { meow_img_free(img); return 0; }
|
if (i < 0) { meow_img_free(img); return 0; }
|
||||||
|
|
||||||
|
/* 总字节上限:新图放进去前先淘汰最远页,保证常驻图不超过预算。 */
|
||||||
|
long need = (long)meow_img_w(img) * meow_img_h(img) * 4;
|
||||||
|
for (;;) {
|
||||||
|
long held = resident_bytes();
|
||||||
|
if (g_slot[i].img) held -= (long)g_slot[i].src_w * g_slot[i].src_h * 4;
|
||||||
|
if (held + need <= MR_RESIDENT_CAP) break;
|
||||||
|
if (slot_evict_far(i) < 0) break; /* 只剩目标槽:这一页必须放进来 */
|
||||||
|
}
|
||||||
|
|
||||||
slot_clear(i);
|
slot_clear(i);
|
||||||
g_slot[i].idx = idx;
|
g_slot[i].idx = idx;
|
||||||
g_slot[i].img = img;
|
g_slot[i].img = img;
|
||||||
g_slot[i].src_w = meow_img_w(img);
|
g_slot[i].src_w = meow_img_w(img);
|
||||||
g_slot[i].src_h = meow_img_h(img);
|
g_slot[i].src_h = meow_img_h(img);
|
||||||
|
g_slot[i].want_w = want_w;
|
||||||
|
g_slot[i].want_h = want_h;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,11 +664,20 @@ static int req_queued(int idx)
|
|||||||
static void queue_page(int idx)
|
static void queue_page(int idx)
|
||||||
{
|
{
|
||||||
if (!mr_book_valid(&g_book) || idx < 0 || idx >= g_count) return;
|
if (!mr_book_valid(&g_book) || idx < 0 || idx >= g_count) return;
|
||||||
if (slot_find(idx) || page_failed(idx)) return;
|
if (slot_ready(idx) || page_failed(idx)) return;
|
||||||
|
/* 几何在这里(主线程)算好塞进请求:worker 不再读 g_fit/g_zoom/g_vw/g_vh。 */
|
||||||
|
int want_w = 0, want_h = 0;
|
||||||
|
page_geometry_at(idx, &want_w, &want_h);
|
||||||
|
if (want_w <= 0 || want_h <= 0) { want_w = g_vw; want_h = g_vh; }
|
||||||
|
int long_side = plan_decode_long(want_w, want_h);
|
||||||
pthread_mutex_lock(&g_lock);
|
pthread_mutex_lock(&g_lock);
|
||||||
if (!g_stop && !req_queued(idx) && g_req_n < MR_REQ_MAX) {
|
if (!g_stop && !req_queued(idx) && g_req_n < MR_REQ_MAX) {
|
||||||
g_req[g_req_tail].idx = idx;
|
page_req *r = &g_req[g_req_tail];
|
||||||
g_req[g_req_tail].gen = g_gen;
|
r->idx = idx;
|
||||||
|
r->gen = g_gen;
|
||||||
|
r->want_w = want_w;
|
||||||
|
r->want_h = want_h;
|
||||||
|
r->long_side = long_side;
|
||||||
g_req_tail = (g_req_tail + 1) % MR_REQ_MAX;
|
g_req_tail = (g_req_tail + 1) % MR_REQ_MAX;
|
||||||
g_req_n++;
|
g_req_n++;
|
||||||
pthread_cond_signal(&g_cv);
|
pthread_cond_signal(&g_cv);
|
||||||
@@ -481,13 +698,19 @@ static void *reader_worker(void *arg)
|
|||||||
g_req_n--;
|
g_req_n--;
|
||||||
pthread_mutex_unlock(&g_lock);
|
pthread_mutex_unlock(&g_lock);
|
||||||
|
|
||||||
|
/* 解码计划:本线程私有(单 worker),解码回调只读这三个量。 */
|
||||||
|
g_plan_want_w = req.want_w;
|
||||||
|
g_plan_want_h = req.want_h;
|
||||||
|
g_plan_long = req.long_side;
|
||||||
uint64_t t0 = meow_ui_time_ms();
|
uint64_t t0 = meow_ui_time_ms();
|
||||||
meow_img *img = mr_book_open(&g_book, req.idx);
|
meow_img *img = mr_book_open(&g_book, req.idx);
|
||||||
uint64_t dt = meow_ui_time_ms() - t0;
|
uint64_t dt = meow_ui_time_ms() - t0;
|
||||||
|
g_decode_count++;
|
||||||
int w = img ? meow_img_w(img) : 0;
|
int w = img ? meow_img_w(img) : 0;
|
||||||
int h = img ? meow_img_h(img) : 0;
|
int h = img ? meow_img_h(img) : 0;
|
||||||
meow_log(MEOW_LOG_INFO, "reader decode page=%d ms=%llu %dx%d %s",
|
meow_log(MEOW_LOG_INFO, "reader decode page=%d want=%dx%d long=%d ms=%llu %dx%d %s",
|
||||||
req.idx, (unsigned long long)dt, w, h, img ? "ok" : "fail");
|
req.idx, req.want_w, req.want_h, req.long_side,
|
||||||
|
(unsigned long long)dt, w, h, img ? "ok" : "fail");
|
||||||
|
|
||||||
pthread_mutex_lock(&g_lock);
|
pthread_mutex_lock(&g_lock);
|
||||||
if (!g_stop && g_res_n < MR_RES_MAX) {
|
if (!g_stop && g_res_n < MR_RES_MAX) {
|
||||||
@@ -495,6 +718,8 @@ static void *reader_worker(void *arg)
|
|||||||
g_res[g_res_n].gen = req.gen;
|
g_res[g_res_n].gen = req.gen;
|
||||||
g_res[g_res_n].img = img;
|
g_res[g_res_n].img = img;
|
||||||
g_res[g_res_n].fail = img ? 0 : 1;
|
g_res[g_res_n].fail = img ? 0 : 1;
|
||||||
|
g_res[g_res_n].want_w = req.want_w;
|
||||||
|
g_res[g_res_n].want_h = req.want_h;
|
||||||
g_res_n++;
|
g_res_n++;
|
||||||
img = NULL; /* 所有权已移交(失败时本就是 NULL) */
|
img = NULL; /* 所有权已移交(失败时本就是 NULL) */
|
||||||
}
|
}
|
||||||
@@ -537,7 +762,12 @@ static void queue_prune(void)
|
|||||||
int n = 0;
|
int n = 0;
|
||||||
for (int i = 0; i < g_req_n; i++) {
|
for (int i = 0; i < g_req_n; i++) {
|
||||||
page_req r = g_req[(g_req_head + i) % MR_REQ_MAX];
|
page_req r = g_req[(g_req_head + i) % MR_REQ_MAX];
|
||||||
if (r.gen == g_gen && in_window(r.idx)) keep[n++] = r;
|
if (r.gen != g_gen || !in_window(r.idx)) continue;
|
||||||
|
/* 几何已变的请求作废(否则会按旧显示框解出来又被判过期)。 */
|
||||||
|
int w = 0, h = 0;
|
||||||
|
page_geometry_at(r.idx, &w, &h);
|
||||||
|
if (abs(w - r.want_w) > MR_SLOT_TOL || abs(h - r.want_h) > MR_SLOT_TOL) continue;
|
||||||
|
keep[n++] = r;
|
||||||
}
|
}
|
||||||
for (int i = 0; i < n; i++) g_req[i] = keep[i];
|
for (int i = 0; i < n; i++) g_req[i] = keep[i];
|
||||||
g_req_head = 0;
|
g_req_head = 0;
|
||||||
@@ -562,11 +792,12 @@ static int results_drain(void)
|
|||||||
if (got[i].gen != g_gen) { meow_img_free(got[i].img); continue; }
|
if (got[i].gen != g_gen) { meow_img_free(got[i].img); continue; }
|
||||||
if (got[i].fail) {
|
if (got[i].fail) {
|
||||||
page_mark_failed(got[i].idx);
|
page_mark_failed(got[i].idx);
|
||||||
if (in_window(got[i].idx)) g_fail_paint = 1;
|
if (page_visible(got[i].idx)) g_fail_paint = 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (in_window(got[i].idx) && !slot_find(got[i].idx)) {
|
if (in_window(got[i].idx)) {
|
||||||
if (slot_put(got[i].idx, got[i].img)) changed = 1;
|
if (slot_put(got[i].idx, got[i].img, got[i].want_w, got[i].want_h) &&
|
||||||
|
page_visible(got[i].idx)) changed = 1;
|
||||||
} else {
|
} else {
|
||||||
meow_img_free(got[i].img);
|
meow_img_free(got[i].img);
|
||||||
}
|
}
|
||||||
@@ -574,13 +805,12 @@ static int results_drain(void)
|
|||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 窗口随视口/当前页移动:淘汰窗口外图 → 清掉窗口外请求 → 按优先级补页。 */
|
|
||||||
/* 窗口随视口/当前页移动:淘汰窗口外图 → 清掉窗口外请求 → 按优先级补页。 */
|
/* 窗口随视口/当前页移动:淘汰窗口外图 → 清掉窗口外请求 → 按优先级补页。 */
|
||||||
static void window_update(void)
|
static void window_update(void)
|
||||||
{
|
{
|
||||||
window_compute();
|
window_compute();
|
||||||
for (int i = 0; i < MR_SLOTS; i++)
|
/* 不主动清窗口外的图:多留一页「回看缓存」,避免 0→1→0 来回翻时反复重解同一页。
|
||||||
if (g_slot[i].idx >= 0 && !in_window(g_slot[i].idx)) slot_clear(i);
|
* 容量仍由 MR_SLOTS 与 slot_put 的字节淘汰兜底(最多 MR_SLOTS 张)。 */
|
||||||
queue_prune();
|
queue_prune();
|
||||||
for (int i = 0; i < g_win_n; i++) queue_page(g_win_idx[i]);
|
for (int i = 0; i < g_win_n; i++) queue_page(g_win_idx[i]);
|
||||||
}
|
}
|
||||||
@@ -590,7 +820,8 @@ static void window_update(void)
|
|||||||
/* ============================================================ */
|
/* ============================================================ */
|
||||||
static int strip_meta_cb(void *ctx, int index, int *w, int *h)
|
static int strip_meta_cb(void *ctx, int index, int *w, int *h)
|
||||||
{
|
{
|
||||||
return mr_book_meta((const mr_book *)ctx, index, w, h);
|
(void)ctx;
|
||||||
|
return page_meta_dims(index, w, h); /* 走缓存:重排版不再逐页 fopen probe */
|
||||||
}
|
}
|
||||||
|
|
||||||
static int strip_ensure(void)
|
static int strip_ensure(void)
|
||||||
@@ -641,7 +872,7 @@ static void wo_set(void *ctx, int off)
|
|||||||
static void wo_redraw(void *ctx)
|
static void wo_redraw(void *ctx)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
invalidate_content(); /* 滚动只动内容区,顶栏静态不重绘 */
|
||||||
}
|
}
|
||||||
|
|
||||||
static void scroll_ensure(void)
|
static void scroll_ensure(void)
|
||||||
@@ -661,27 +892,23 @@ static void scroll_free(void)
|
|||||||
/* ============================================================ */
|
/* ============================================================ */
|
||||||
/* 布局/绘制 */
|
/* 布局/绘制 */
|
||||||
/* ============================================================ */
|
/* ============================================================ */
|
||||||
static void page_target_size(const page_slot *s, int *w, int *h)
|
/* 内容视口失效区:chrome 可见时顶栏是静态的,拖动/吸附帧只刷其下内容,
|
||||||
|
* 避免每帧重绘顶栏文字(软渲下文字是最贵的绘制物)。 */
|
||||||
|
static void invalidate_content(void)
|
||||||
{
|
{
|
||||||
*w = *h = 0;
|
if (g_chrome)
|
||||||
if (!s) return;
|
meow_ui_invalidate(0, MR_TOP_BAR_H, g_vw, g_vh - MR_TOP_BAR_H);
|
||||||
if (mr_nav_fit_size(g_fit, s->src_w, s->src_h, g_vw, g_vh, w, h) != 0) return;
|
else
|
||||||
if (g_zoom_pct != 100) mr_nav_zoom_size(*w, *h, g_zoom_pct, w, h);
|
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void current_geom(int *w, int *h)
|
static void current_geom(int *w, int *h)
|
||||||
{
|
{
|
||||||
page_slot *s = slot_find(g_page);
|
page_target_size_at(g_page, w, h);
|
||||||
if (s) { page_target_size(s, w, h); return; }
|
if (!w || !h || *w <= 0 || *h <= 0) {
|
||||||
/* 页未就绪:用 meta 估算,保证平移/缩放的夹取稳定。 */
|
if (w) *w = g_vw;
|
||||||
int sw = 0, sh = 0;
|
if (h) *h = g_vh;
|
||||||
if (mr_book_meta(&g_book, g_page, &sw, &sh) && sw > 0 && sh > 0) {
|
|
||||||
mr_nav_fit_size(g_fit, sw, sh, g_vw, g_vh, w, h);
|
|
||||||
if (g_zoom_pct != 100) mr_nav_zoom_size(*w, *h, g_zoom_pct, w, h);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
*w = g_vw;
|
|
||||||
*h = g_vh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void clamp_pan(void)
|
static void clamp_pan(void)
|
||||||
@@ -692,31 +919,50 @@ static void clamp_pan(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 内容是否超出视口(原始档的方图/宽图即使不放大也需要平移)。 */
|
/* 内容是否超出视口(原始档的方图/宽图即使不放大也需要平移)。 */
|
||||||
static int content_overflow(void)
|
/* 横向是否真的溢出(横向平移才有意义)。适宽长图只会在**竖向**溢出:那属于「页面比
|
||||||
|
* 屏幕略高」,不能因此把左右 tap / 横拖从翻页改成平移——否则 1.5 以上长宽比的页
|
||||||
|
* 在适宽档完全翻不动(水平平移量本就是 0,点击变空操作)。 */
|
||||||
|
static int content_h_overflow(void)
|
||||||
{
|
{
|
||||||
int w, h;
|
int w = 0, h = 0;
|
||||||
current_geom(&w, &h);
|
current_geom(&w, &h);
|
||||||
return w > g_vw || h > g_vh;
|
return w > g_vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 平移模式:放大,或原始档溢出。此时单指拖动=平移,左右 tap=平移步进。 */
|
/* 平移模式:放大、横向溢出(原始档宽图 / 适高宽图)、或原始档任意溢出。
|
||||||
|
* 此时单指拖动=平移,左右 tap=平移步进。
|
||||||
|
* 适宽/适高的**竖向单独溢出**不算(适宽长图常见:1.6 比例页在 480x720 上高 768);
|
||||||
|
* 若把它算成平移,左右 tap / 横拖会变成「水平平移 0px」的空操作 —— 页面直接翻不动。 */
|
||||||
static int pan_mode(void)
|
static int pan_mode(void)
|
||||||
{
|
{
|
||||||
return g_zoom_pct > 100 || content_overflow();
|
if (g_zoom_pct > 100) return 1;
|
||||||
|
if (content_h_overflow()) return 1;
|
||||||
|
if (g_fit == MR_FIT_ORIGINAL) {
|
||||||
|
int w = 0, h = 0;
|
||||||
|
current_geom(&w, &h);
|
||||||
|
return h > g_vh;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 画一页(带水平位移);图与目标尺寸相等时走原生 blit(翻页热路径),否则最近邻缩放。 */
|
/* 画一页(带水平位移);解码尺寸==显示尺寸时走原生 blit(翻页热路径零重采样),
|
||||||
|
* 只在 fit/zoom 变化或极端长页(超单页缓存上限)时才退回最近邻缩放。 */
|
||||||
static void draw_page_at(int idx, int off_x, int off_y)
|
static void draw_page_at(int idx, int off_x, int off_y)
|
||||||
{
|
{
|
||||||
page_slot *s = slot_find(idx);
|
page_slot *s = slot_find(idx);
|
||||||
if (!s) return;
|
if (!s) return;
|
||||||
int w, h;
|
int w, h;
|
||||||
page_target_size(s, &w, &h);
|
page_target_size_at(idx, &w, &h);
|
||||||
if (w <= 0 || h <= 0) return;
|
if (w <= 0 || h <= 0) return;
|
||||||
int x = (g_vw - w) / 2 + g_pan_x + off_x;
|
int x = (g_vw - w) / 2 + g_pan_x + off_x;
|
||||||
int y = (g_vh - h) / 2 + g_pan_y + off_y;
|
int y = (g_vh - h) / 2 + g_pan_y + off_y;
|
||||||
if (w == s->src_w && h == s->src_h) meow_ui_img(x, y, s->img);
|
if (abs(w - s->src_w) <= MR_SLOT_TOL && abs(h - s->src_h) <= MR_SLOT_TOL) {
|
||||||
else meow_ui_img_scaled(x, y, w, h, s->img);
|
meow_ui_img(x + (w - s->src_w) / 2, y + (h - s->src_h) / 2, s->img);
|
||||||
|
NOTE_BLIT();
|
||||||
|
} else {
|
||||||
|
meow_ui_img_scaled(x, y, w, h, s->img);
|
||||||
|
NOTE_SCALED();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 条漫:按滚动偏移画可见页。典型 2:3 页的解码尺寸与显示尺寸相等 → 原生 blit;
|
/* 条漫:按滚动偏移画可见页。典型 2:3 页的解码尺寸与显示尺寸相等 → 原生 blit;
|
||||||
@@ -735,8 +981,15 @@ static void draw_strip(const meow_theme *th)
|
|||||||
if (h <= 0) continue;
|
if (h <= 0) continue;
|
||||||
page_slot *s = slot_find(i);
|
page_slot *s = slot_find(i);
|
||||||
if (s) {
|
if (s) {
|
||||||
if (s->src_w == g_vw && s->src_h == h) meow_ui_img(0, y, s->img);
|
/* 解码=显示尺寸时原生 blit:gfx 只处理屏幕上可见的行,
|
||||||
else meow_ui_img_scaled(0, y, g_vw, h, s->img);
|
* 长条页只露一截时不再整页重采样(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);
|
||||||
|
NOTE_BLIT();
|
||||||
|
} else {
|
||||||
|
meow_ui_img_scaled(0, y, g_vw, h, s->img);
|
||||||
|
NOTE_SCALED();
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
/* 未就绪:只填可见部分(长页不整块清屏),给一帧占位而不是白屏。 */
|
/* 未就绪:只填可见部分(长页不整块清屏),给一帧占位而不是白屏。 */
|
||||||
@@ -907,7 +1160,7 @@ static void pan_by(int dx, int dy)
|
|||||||
g_pan_x += dx;
|
g_pan_x += dx;
|
||||||
g_pan_y += dy;
|
g_pan_y += dy;
|
||||||
clamp_pan();
|
clamp_pan();
|
||||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
invalidate_content();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void commit_page(int target)
|
static void commit_page(int target)
|
||||||
@@ -929,7 +1182,7 @@ static void slide_update(meow_anim *a, float value, void *ctx)
|
|||||||
{
|
{
|
||||||
(void)a; (void)ctx;
|
(void)a; (void)ctx;
|
||||||
g_anim_v = (int)value;
|
g_anim_v = (int)value;
|
||||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
invalidate_content(); /* 吸附帧只刷内容区(整屏 minus 静态顶栏) */
|
||||||
}
|
}
|
||||||
|
|
||||||
static void slide_done(meow_anim *a, int canceled, void *ctx)
|
static void slide_done(meow_anim *a, int canceled, void *ctx)
|
||||||
@@ -944,7 +1197,7 @@ static void slide_done(meow_anim *a, int canceled, void *ctx)
|
|||||||
if (!canceled && delta)
|
if (!canceled && delta)
|
||||||
commit_page(g_page + delta);
|
commit_page(g_page + delta);
|
||||||
else
|
else
|
||||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
invalidate_content();
|
||||||
meow_log(MEOW_LOG_INFO, "reader flip done page=%d/%d total_ms=%llu",
|
meow_log(MEOW_LOG_INFO, "reader flip done page=%d/%d total_ms=%llu",
|
||||||
g_page + 1, g_count,
|
g_page + 1, g_count,
|
||||||
(unsigned long long)(meow_ui_time_ms() - g_flip_t0));
|
(unsigned long long)(meow_ui_time_ms() - g_flip_t0));
|
||||||
@@ -982,7 +1235,7 @@ static void bounce_update(meow_anim *a, float value, void *ctx)
|
|||||||
{
|
{
|
||||||
(void)a; (void)ctx;
|
(void)a; (void)ctx;
|
||||||
g_drag_off = (int)value;
|
g_drag_off = (int)value;
|
||||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
invalidate_content();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void bounce_done(meow_anim *a, int canceled, void *ctx)
|
static void bounce_done(meow_anim *a, int canceled, void *ctx)
|
||||||
@@ -991,7 +1244,7 @@ static void bounce_done(meow_anim *a, int canceled, void *ctx)
|
|||||||
g_bounce = NULL;
|
g_bounce = NULL;
|
||||||
g_drag_off = 0;
|
g_drag_off = 0;
|
||||||
g_drag_mode = 0;
|
g_drag_mode = 0;
|
||||||
if (!g_quitting) meow_ui_invalidate(0, 0, g_vw, g_vh);
|
if (!g_quitting) invalidate_content();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void start_bounce(int from_off)
|
static void start_bounce(int from_off)
|
||||||
@@ -1002,7 +1255,7 @@ static void start_bounce(int from_off)
|
|||||||
bounce_update, bounce_done, NULL,
|
bounce_update, bounce_done, NULL,
|
||||||
};
|
};
|
||||||
g_bounce = meow_anim_start(&d);
|
g_bounce = meow_anim_start(&d);
|
||||||
if (!g_bounce) { g_drag_off = 0; g_drag_mode = 0; meow_ui_invalidate(0, 0, g_vw, g_vh); }
|
if (!g_bounce) { g_drag_off = 0; g_drag_mode = 0; invalidate_content(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 点击触发的翻页(无跟手位移)。首/末页再前进 → 相邻册(无相邻册则轻提示)。 */
|
/* 点击触发的翻页(无跟手位移)。首/末页再前进 → 相邻册(无相邻册则轻提示)。 */
|
||||||
@@ -1176,6 +1429,8 @@ static void open_book_reset_state(void)
|
|||||||
mr_jump_cancel(&g_jump);
|
mr_jump_cancel(&g_jump);
|
||||||
g_toast[0] = '\0';
|
g_toast[0] = '\0';
|
||||||
g_title_page = -1;
|
g_title_page = -1;
|
||||||
|
free(g_meta);
|
||||||
|
g_meta = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 打开一本册并恢复模式/起始页。start_page<0 = 末页(上一册衔接)。1=成功。 */
|
/* 打开一本册并恢复模式/起始页。start_page<0 = 末页(上一册衔接)。1=成功。 */
|
||||||
@@ -1188,7 +1443,7 @@ static int open_book(const char *dir, int start_page, int mode)
|
|||||||
if (dir != g_dir) snprintf(g_dir, sizeof g_dir, "%s", dir);
|
if (dir != g_dir) snprintf(g_dir, sizeof g_dir, "%s", dir);
|
||||||
g_mode = mr_mode_valid(mode) ? mode : MR_MODE_PAGED;
|
g_mode = mr_mode_valid(mode) ? mode : MR_MODE_PAGED;
|
||||||
|
|
||||||
int maxdim = g_vw > g_vh ? g_vw : g_vh;
|
int maxdim = g_vw > g_vh ? g_vw : g_vh; /* 仅作无解码计划时的兜底预算 */
|
||||||
/* 目录 → 目录册;.cbz/.zip → 档案册(M11)。两者同一个 mr_book 契约,
|
/* 目录 → 目录册;.cbz/.zip → 档案册(M11)。两者同一个 mr_book 契约,
|
||||||
* 下面的翻页/条漫消费代码完全不区分 provider。 */
|
* 下面的翻页/条漫消费代码完全不区分 provider。 */
|
||||||
int n = mr_book_open_auto(&g_book, &g_handle, g_dir, maxdim,
|
int n = mr_book_open_auto(&g_book, &g_handle, g_dir, maxdim,
|
||||||
@@ -1211,6 +1466,10 @@ static int open_book(const char *dir, int start_page, int mode)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
g_count = n;
|
g_count = n;
|
||||||
|
/* 页尺寸缓存:每页 8 字节(4096 页上限 ≈32KB),换来的是一帧内零 fopen probe。 */
|
||||||
|
g_meta = (page_meta *)calloc((size_t)g_count, sizeof *g_meta);
|
||||||
|
if (!g_meta)
|
||||||
|
meow_log(MEOW_LOG_WARN, "reader meta cache unavailable (%d pages)", g_count);
|
||||||
|
|
||||||
int page = start_page < 0 ? g_count - 1 : start_page;
|
int page = start_page < 0 ? g_count - 1 : start_page;
|
||||||
page = mr_nav_clamp_page(page, g_count);
|
page = mr_nav_clamp_page(page, g_count);
|
||||||
@@ -1234,8 +1493,14 @@ static int open_book(const char *dir, int start_page, int mode)
|
|||||||
worker_start();
|
worker_start();
|
||||||
if (!g_worker_on && g_count > 0) {
|
if (!g_worker_on && g_count > 0) {
|
||||||
/* 线程创建失败:退化为主线程同步解起始页(保证不是纯白屏)。 */
|
/* 线程创建失败:退化为主线程同步解起始页(保证不是纯白屏)。 */
|
||||||
|
int ww = 0, wh = 0;
|
||||||
|
page_geometry_at(g_page, &ww, &wh);
|
||||||
|
if (ww <= 0 || wh <= 0) { ww = g_vw; wh = g_vh; }
|
||||||
|
g_plan_want_w = ww;
|
||||||
|
g_plan_want_h = wh;
|
||||||
|
g_plan_long = plan_decode_long(ww, wh);
|
||||||
meow_img *first = mr_book_open(&g_book, g_page);
|
meow_img *first = mr_book_open(&g_book, g_page);
|
||||||
if (first) slot_put(g_page, first);
|
if (first) slot_put(g_page, first, ww, wh);
|
||||||
meow_log(MEOW_LOG_WARN, "reader worker unavailable, sync first page");
|
meow_log(MEOW_LOG_WARN, "reader worker unavailable, sync first page");
|
||||||
}
|
}
|
||||||
slider_build();
|
slider_build();
|
||||||
@@ -1289,7 +1554,7 @@ static void drag_paint(void)
|
|||||||
int gap = g_zoom_pct > 100 ? 33 : MR_PAINT_MS; /* 缩放帧要重采样:30fps 上限 */
|
int gap = g_zoom_pct > 100 ? 33 : MR_PAINT_MS; /* 缩放帧要重采样:30fps 上限 */
|
||||||
if (now - g_last_paint < (uint64_t)gap) return;
|
if (now - g_last_paint < (uint64_t)gap) return;
|
||||||
g_last_paint = now;
|
g_last_paint = now;
|
||||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
invalidate_content();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 松手:过阈值且目标页已缓存 → 从当前位移吸附过去;否则回弹。 */
|
/* 松手:过阈值且目标页已缓存 → 从当前位移吸附过去;否则回弹。 */
|
||||||
@@ -1303,7 +1568,7 @@ static void drag_finish(void)
|
|||||||
g_drag_mode = 0;
|
g_drag_mode = 0;
|
||||||
|
|
||||||
if (mode != 1 || abs(dx) < MR_DRAG_SLOP) {
|
if (mode != 1 || abs(dx) < MR_DRAG_SLOP) {
|
||||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
invalidate_content();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int rtl = mr_reader_get_direction() == MR_DIR_RTL;
|
int rtl = mr_reader_get_direction() == MR_DIR_RTL;
|
||||||
@@ -1312,7 +1577,7 @@ static void drag_finish(void)
|
|||||||
if (mr_nav_commit(dx, g_vw)) {
|
if (mr_nav_commit(dx, g_vw)) {
|
||||||
if (target == g_page) { /* 首/末页越界:进相邻册 */
|
if (target == g_page) { /* 首/末页越界:进相邻册 */
|
||||||
book_switch(delta > 0 ? 1 : -1);
|
book_switch(delta > 0 ? 1 : -1);
|
||||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
invalidate_content();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
g_flip_t0 = meow_ui_time_ms();
|
g_flip_t0 = meow_ui_time_ms();
|
||||||
@@ -1341,7 +1606,7 @@ static void webtoon_release(void)
|
|||||||
if (pull <= -MR_BOOK_PULL && book_switch(-1)) return;
|
if (pull <= -MR_BOOK_PULL && book_switch(-1)) return;
|
||||||
if (g_scroll) meow_scroll_fling(g_scroll);
|
if (g_scroll) meow_scroll_fling(g_scroll);
|
||||||
cont_touch();
|
cont_touch();
|
||||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
invalidate_content();
|
||||||
}
|
}
|
||||||
|
|
||||||
void reader_on_touch_down(int x, int y)
|
void reader_on_touch_down(int x, int y)
|
||||||
@@ -1440,7 +1705,7 @@ void reader_on_touch_move(int x, int y)
|
|||||||
g_dragging = 0;
|
g_dragging = 0;
|
||||||
g_drag_mode = 0;
|
g_drag_mode = 0;
|
||||||
g_drag_off = 0;
|
g_drag_off = 0;
|
||||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
invalidate_content();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1614,9 +1879,8 @@ static void reader_tick(void)
|
|||||||
if (now - g_last_tick < MR_TICK_MS) return;
|
if (now - g_last_tick < MR_TICK_MS) return;
|
||||||
g_last_tick = now;
|
g_last_tick = now;
|
||||||
|
|
||||||
int had_current = slot_find(current_page()) != NULL;
|
/* 只有当落位的页真的在屏幕上(或换了几何的当前页)才重绘,预取邻居不刷屏。 */
|
||||||
if (results_drain() && !had_current && slot_find(current_page()) && !g_dragging)
|
if (results_drain() && !g_dragging) invalidate_content();
|
||||||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
|
||||||
|
|
||||||
/* 打开册的瞬时资源失败:稍后自动重试一次(进入本页时已停 library 后台解码)。 */
|
/* 打开册的瞬时资源失败:稍后自动重试一次(进入本页时已停 library 后台解码)。 */
|
||||||
if (g_open_err && g_open_retry_at && now >= g_open_retry_at) {
|
if (g_open_err && g_open_retry_at && now >= g_open_retry_at) {
|
||||||
@@ -1628,7 +1892,7 @@ static void reader_tick(void)
|
|||||||
}
|
}
|
||||||
if (g_fail_paint) {
|
if (g_fail_paint) {
|
||||||
g_fail_paint = 0;
|
g_fail_paint = 0;
|
||||||
if (!g_dragging) meow_ui_invalidate(0, 0, g_vw, g_vh);
|
if (!g_dragging) invalidate_content();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (g_tap_pending && now - g_tap_ms >= MR_DOUBLE_MS) tap_flush();
|
if (g_tap_pending && now - g_tap_ms >= MR_DOUBLE_MS) tap_flush();
|
||||||
@@ -1765,6 +2029,12 @@ static void reader_state_reset(void)
|
|||||||
g_tap_pending = 0;
|
g_tap_pending = 0;
|
||||||
g_last_tap_ms = 0;
|
g_last_tap_ms = 0;
|
||||||
g_quitting = 0;
|
g_quitting = 0;
|
||||||
|
/* 本册状态一律清空:g_meta 由 open_book 按新册重建(on_enter 早退也不串上一册);
|
||||||
|
* g_start_at 已在 reader_page_enter 入口取走,这里复位防残留。 */
|
||||||
|
free(g_meta);
|
||||||
|
g_meta = NULL;
|
||||||
|
g_start_at = MR_READER_START_RESUME;
|
||||||
|
g_plan_want_w = g_plan_want_h = g_plan_long = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void reader_page_enter(meow_view *v, void *ctx)
|
static void reader_page_enter(meow_view *v, void *ctx)
|
||||||
@@ -1773,6 +2043,7 @@ static void reader_page_enter(meow_view *v, void *ctx)
|
|||||||
c->active = 1;
|
c->active = 1;
|
||||||
g_quitting = 0;
|
g_quitting = 0;
|
||||||
|
|
||||||
|
int start_at = g_start_at; /* 显式起始页只在本次进入生效 */
|
||||||
g_vw = meow_ui_width();
|
g_vw = meow_ui_width();
|
||||||
g_vh = meow_ui_height();
|
g_vh = meow_ui_height();
|
||||||
v->x = 0;
|
v->x = 0;
|
||||||
@@ -1811,9 +2082,12 @@ static void reader_page_enter(meow_view *v, void *ctx)
|
|||||||
if (data && data[0]) mr_cache_init(data);
|
if (data && data[0]) mr_cache_init(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 打开册:恢复该册上次的模式与页码(无记录 → 翻页模式第 1 页)。 */
|
/* 打开册:显式起始页(书库文件管理器模式点第 N 张)优先于续读记录;
|
||||||
|
* 否则恢复该册上次的模式与页码(无记录 → 翻页模式第 1 页)。
|
||||||
|
* 显式起始页在入口处取走(start_at 局部量),state_reset 之后仍有效。 */
|
||||||
int mode = mr_cont_get_mode(g_dir, MR_MODE_PAGED);
|
int mode = mr_cont_get_mode(g_dir, MR_MODE_PAGED);
|
||||||
int page = mr_cont_get_page(g_dir, 0);
|
int page = start_at != MR_READER_START_RESUME ? start_at
|
||||||
|
: mr_cont_get_page(g_dir, 0);
|
||||||
open_book(g_dir, page, mode);
|
open_book(g_dir, page, mode);
|
||||||
|
|
||||||
ticker_start();
|
ticker_start();
|
||||||
@@ -1842,6 +2116,9 @@ static void reader_page_leave(meow_view *v, void *ctx)
|
|||||||
mr_strip_free(&g_strip);
|
mr_strip_free(&g_strip);
|
||||||
g_strip_ok = 0;
|
g_strip_ok = 0;
|
||||||
slots_free_all();
|
slots_free_all();
|
||||||
|
free(g_meta);
|
||||||
|
g_meta = NULL;
|
||||||
|
g_start_at = MR_READER_START_RESUME;
|
||||||
mr_book_close_auto(&g_book, &g_handle);
|
mr_book_close_auto(&g_book, &g_handle);
|
||||||
|
|
||||||
if (g_slider) { meow_widget_free(g_slider); g_slider = NULL; }
|
if (g_slider) { meow_widget_free(g_slider); g_slider = NULL; }
|
||||||
@@ -1882,14 +2159,22 @@ const char *mr_reader_dir(void)
|
|||||||
return g_dir;
|
return g_dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
void mr_reader_open_dir(const char *path)
|
void mr_reader_open_dir_at(const char *path, int start_index)
|
||||||
{
|
{
|
||||||
if (!path) return;
|
if (!path) return;
|
||||||
snprintf(g_dir, sizeof g_dir, "%s", path);
|
snprintf(g_dir, sizeof g_dir, "%s", path);
|
||||||
meow_log(MEOW_LOG_INFO, "reader open: %s", g_dir);
|
/* -1=续读(旧入口语义);其它负数钳到 0;上界在 open_book 里按页数钳。 */
|
||||||
|
g_start_at = start_index == MR_READER_START_RESUME ? MR_READER_START_RESUME
|
||||||
|
: (start_index < 0 ? 0 : start_index);
|
||||||
|
meow_log(MEOW_LOG_INFO, "reader open: %s (start=%d)", g_dir, g_start_at);
|
||||||
meow_view_page_push(ACT_READER);
|
meow_view_page_push(ACT_READER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void mr_reader_open_dir(const char *path)
|
||||||
|
{
|
||||||
|
mr_reader_open_dir_at(path, MR_READER_START_RESUME);
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef MR_READER_HOST_TEST
|
#ifdef MR_READER_HOST_TEST
|
||||||
/* 宿主集成测试观察口(不改变任何状态)。 */
|
/* 宿主集成测试观察口(不改变任何状态)。 */
|
||||||
int mr_reader_ui_mode(void) { return g_mode; }
|
int mr_reader_ui_mode(void) { return g_mode; }
|
||||||
@@ -1898,4 +2183,17 @@ int mr_reader_ui_count(void) { return g_count; }
|
|||||||
int mr_reader_ui_open_err(void) { return g_open_err; }
|
int mr_reader_ui_open_err(void) { return g_open_err; }
|
||||||
int mr_reader_ui_jump_open(void) { return g_jump_open; }
|
int mr_reader_ui_jump_open(void) { return g_jump_open; }
|
||||||
int mr_reader_ui_jump_pending(void) { return mr_jump_active(&g_jump); }
|
int mr_reader_ui_jump_pending(void) { return mr_jump_active(&g_jump); }
|
||||||
|
long mr_reader_ui_resident_kb(void) { return resident_bytes() / 1024; }
|
||||||
|
int mr_reader_ui_failed_pages(void)
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
for (int i = 0; i < g_count && i < MR_BOOK_MAX_PAGES; i++)
|
||||||
|
if (page_failed(i)) n++;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
int mr_reader_ui_blit_draws(void) { return g_blit_draws; }
|
||||||
|
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; }
|
||||||
#endif
|
#endif
|
||||||
@@ -13,6 +13,15 @@ extern "C" {
|
|||||||
void reader_on_touch_down(int x, int y);
|
void reader_on_touch_down(int x, int y);
|
||||||
void reader_on_touch_move(int x, int y);
|
void reader_on_touch_move(int x, int y);
|
||||||
|
|
||||||
|
/* 书库 → 阅读器(文件管理器模式用):按目录内图片自然序从 start_index 开始。
|
||||||
|
* - start_index >= 册页数:钳到末页;其它负数按 0 处理;
|
||||||
|
* - start_index == MR_READER_START_RESUME(-1):沿用该册续读记录(= mr_reader_open_dir)。
|
||||||
|
* 显式下标优先于续读记录:用户在列表里点了某张图,就不该被上次的页码带走。 */
|
||||||
|
#define MR_READER_START_RESUME (-1)
|
||||||
|
void mr_reader_open_dir_at(const char *dir, int start_index);
|
||||||
|
/* 保留旧入口:等价于 mr_reader_open_dir_at(dir, MR_READER_START_RESUME)。 */
|
||||||
|
void mr_reader_open_dir(const char *dir);
|
||||||
|
|
||||||
#ifdef MR_READER_HOST_TEST
|
#ifdef MR_READER_HOST_TEST
|
||||||
/* 宿主集成测试专用:只读观察本页状态(不参与产品逻辑)。 */
|
/* 宿主集成测试专用:只读观察本页状态(不参与产品逻辑)。 */
|
||||||
int mr_reader_ui_mode(void); /* MR_MODE_PAGED / MR_MODE_WEBTOON */
|
int mr_reader_ui_mode(void); /* MR_MODE_PAGED / MR_MODE_WEBTOON */
|
||||||
@@ -20,6 +29,13 @@ int mr_reader_ui_page(void); /* 当前页(条漫=视口顶端所在页
|
|||||||
int mr_reader_ui_count(void); /* 当前册页数 */
|
int mr_reader_ui_count(void); /* 当前册页数 */
|
||||||
int mr_reader_ui_jump_open(void); /* 1=跳页滑块已展开 */
|
int mr_reader_ui_jump_open(void); /* 1=跳页滑块已展开 */
|
||||||
int mr_reader_ui_jump_pending(void);/* 1=跳页目标等待解码 */
|
int mr_reader_ui_jump_pending(void);/* 1=跳页目标等待解码 */
|
||||||
|
long mr_reader_ui_resident_kb(void);/* 驻留页图字节(KB) */
|
||||||
|
int mr_reader_ui_failed_pages(void);/* 解码失败/超预算被跳过的页数 */
|
||||||
|
int mr_reader_ui_blit_draws(void); /* 累计原生 blit 绘页次数(热路径应为纯 blit) */
|
||||||
|
int mr_reader_ui_scaled_draws(void);/* 累计重采样绘页次数(每帧重采样=卡顿) */
|
||||||
|
int mr_reader_ui_decode_count(void);/* 累计解码次数(查重解抖动/活锁) */
|
||||||
|
int mr_reader_ui_fit(void); /* 当前 fit 档(MR_FIT_*) */
|
||||||
|
int mr_reader_ui_chrome(void); /* 1=顶栏可见 */
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|||||||
Reference in new issue
Block a user