Files
MeowOS-For-R3P2/patch_v2/sdk/test_meow_ui_scroll.c
T
AskaEth ee5796747d feat(sdk): M26 公开仓真缩解图像栈并入主仓 — JPEG DCT/PNG 逐行/BMP 跳行后端 + RGB565 页图
主仓 SDK 的图片层原本是 220 行 stb 直通(整幅解码后重采样)。公开仓 MeowOS-SDK
的同一子系统已被真实 App(tuku 阅读器)驱动并做过峰值实测与像素比对,故以公开仓
为权威方向并入主仓 meow_ui.a,两个方向的新功能都保留(逐文件 diff 确认公开仓是
严格超集:无一处「双方互有增减」)。

- 新增 meow_jpeg.c / meow_jpeg.h:DCT 域缩解 1/8、1/4、1/2(N=1/2/4 点 IDCT),
  渐进式只解到首个精细化扫描;不支持项(CMYK/YCCK、算术编码、过订阅 DHT、16bit
  量化表)返回 0 由调用方回退 stb
- 新增 meow_png_stream.c:libz 逐行 inflate + 反滤波,按步长丢行丢列直写目标尺寸;
  BMP 按目标行 seek + 行内跳列(8/16/24/32bpp)
- meow_ui_img.c + include/meow_ui_image.h:新增 meow_img_pick_scale /
  load_file_step / load_file_step_ex / resample 四个公开入口;_dim 与 _mem_dim
  先进后端,失败/不支持才回退 stb(对外语义不变)
- include/meow_ui_draw.h + meow_ui_gfx.c:M32 RGB565 页图 5 函数
  (meow_gfx_rgb565 / rgba_to_rgb565 / blit_rgb565 / _scaled)。gfx 保留主仓的
  `#include "../meow_launcherd/meow_font.h"` 与 <time.h>(公开仓自 vendored
  meow_font.h 且无 <time.h>,机械覆盖会丢这两行)
- meow_jpeg.h 保持内部:不进 meow_ui.h 伞头,include/ 下无任何头暴露 meow_jpeg_*
- build_sdk.sh:OBJS 插入 meow_jpeg / meow_png_stream(32→34);成员数
  76→78(+meow_font.o +43 freetype),--verify 全为 ELF32 MIPS32 rel2/o32
- 测试:新增 test_meow_img_stream.c(S1-S16),test_meow_img_budget.c 同步公开仓
  版(B9 断言收紧为 p_new <= p_old);22 个既有宿主测试的编译注释行补
  meow_jpeg.c / meow_png_stream.c / -lz(meow_ui_img.c 现引用后端符号,不加即
  undefined reference)

宿主实测:S1-S16 全绿(S7 与 stb 网格像素一致:JPEG mad 4.35-13.69、PNG/BMP
mad 0.00;S9 回退矩阵 CMYK/1bpp BMP/Adam7 均拒绝且 stb 仍成功),B1-B9 全绿
(B9 PNG 峰值 25.65→0.61 MiB、BMP 14.65→0.59 MiB)。回归 test_meow_ui_widgets /
scroll / ime_fix / test_umbrella 全过。zlib 在应用链接期解析:meow_ui.a 只留
inflate* 未定义符号,四个 App 构建脚本原本就带 $SYSROOT/lib/libz.a,静态
linkcheck(meow_ui.a + libz.a -lpthread)通过。

注:主仓 patch_v2/reader_app 仍自带 vendored stb、不用 meow_img_*,本单不改变
阅读器的内存占用;RGB565 页图接入 reader_app 是另一个任务。
2026-11-06 20:44:22 +08:00

408 lines
16 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_meow_ui_scroll.c — §12.3 meow_scroll 滚动物理宿主验证:
* 跟手拖动(track 跟随与 clamp)、速度跟踪→松手 fling(位移序列/停止位置/单调/不越界)、
* 边界过冲 clamp + 简单回弹、snap。时间推进用真实 usleep(meow_anim_tick 驱动 fling)。
*
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/draw/activity/ime + 打桩 font/global):
* gcc -o test_meow_ui_scroll test_meow_ui_scroll.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c meow_ui_nav.c \
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm -lz
* 运行:./test_meow_ui_scroll
*/
#include "meow_ui.h"
#include "meow_ui_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int g_pass = 0, g_fail = 0;
#define CHECK(cond, ...) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
#define LIST_H 400
#define ITEM_H 44
#define COUNT 50
/* max_scroll = COUNT*ITEM_H - LIST_H = 2200 - 400 = 1800 */
#define MAXS (COUNT * ITEM_H - LIST_H)
static int src(int idx, char *label, int len, void *ctx)
{
(void)ctx;
if (idx < 0 || idx >= COUNT) return 0;
snprintf(label, (size_t)len, "row %d", idx);
return 1;
}
static meow_widget *make_list(void)
{
meow_widget *lv = meow_list_create(0, 0, 480, LIST_H, COUNT, src, NULL);
meow_list_set_item_h(lv, ITEM_H);
return lv;
}
/* 喂一串向下(正)速度采样,模拟快速下划 */
static void feed_drag(meow_scroll *s, int step, int n)
{
uint64_t now = meow_ui_time_ms();
for (int i = 0; i < n; i++) {
meow_scroll_track(s, step, now + (uint64_t)i * 16);
}
/* 最后用一个真实稍晚时刻的 track 让采样时间戳真实(保持一致钟) */
meow_scroll_track(s, 0, meow_ui_time_ms());
}
static void pump_until_idle(meow_scroll *s, int *max_off, int *mono_ok)
{
int prev = meow_scroll_offset(s);
int guard = 0;
while (meow_scroll_active(s) && guard++ < 300) {
meow_anim_tick();
int cur = meow_scroll_offset(s);
if (*max_off < cur) *max_off = cur;
if (cur < prev) *mono_ok = 0; /* 下划 fling 应单调不减(回弹段除外:此处只测无回弹) */
prev = cur;
usleep(10000);
}
meow_anim_tick();
}
static void test_track_follow(void)
{
meow_widget *lv = make_list();
meow_scroll *s = meow_scroll_wrap(lv, 0);
CHECK(s != NULL, "wrap");
CHECK(meow_scroll_offset(s) == 0, "init offset 0");
meow_scroll_track(s, 10, meow_ui_time_ms());
CHECK(meow_scroll_offset(s) == 10, "follow +10 got %d", meow_scroll_offset(s));
/* 超量拖到底:clamp 到 max */
meow_scroll_track(s, 100000, meow_ui_time_ms());
CHECK(meow_scroll_offset(s) == MAXS, "clamp to max got %d", meow_scroll_offset(s));
meow_scroll_free(s);
meow_widget_free(lv);
}
static void test_fling_no_boundary(void)
{
meow_widget *lv = make_list();
meow_scroll *s = meow_scroll_wrap(lv, 0);
/* 从 0 起拖 ~300px,建正初速 */
uint64_t t = meow_ui_time_ms();
for (int i = 0; i < 12; i++) meow_scroll_track(s, 25, t + (uint64_t)i * 16);
int start = meow_scroll_offset(s);
CHECK(start > 0, "dragged start>0 got %d", start);
meow_scroll_fling(s);
CHECK(meow_scroll_active(s) == 1, "fling active");
int max_off = start, mono = 1, prev = start;
int guard = 0;
while (meow_scroll_active(s) && guard++ < 300) {
meow_anim_tick();
int cur = meow_scroll_offset(s);
if (max_off < cur) max_off = cur;
if (cur < prev) mono = 0;
prev = cur;
usleep(10000);
}
meow_anim_tick();
int end = meow_scroll_offset(s);
CHECK(mono == 1, "monotonic non-decreasing during fling");
CHECK(end >= start, "fling advances end=%d start=%d", end, start);
CHECK(end <= MAXS, "no overflow end=%d max=%d", end, MAXS);
CHECK(meow_scroll_active(s) == 0, "settles idle");
meow_scroll_free(s);
meow_widget_free(lv);
}
static void test_fling_boundary_bounce(void)
{
meow_widget *lv = make_list();
meow_scroll *s = meow_scroll_wrap(lv, 0);
/* 先拖到近底(~1700) */
meow_list_set_scroll(lv, 1700);
/* 快速下划,冲过底界 */
uint64_t t = meow_ui_time_ms();
for (int i = 0; i < 8; i++) meow_scroll_track(s, 40, t + (uint64_t)i * 16);
meow_scroll_fling(s);
int max_off = 0, mono = 1;
pump_until_idle(s, &max_off, &mono);
int end = meow_scroll_offset(s);
CHECK(max_off <= MAXS, "never exceed max, max_off=%d max=%d", max_off, MAXS);
CHECK(end == MAXS, "bounce settles at bottom end=%d", end); /* 回弹后回到底 */
CHECK(meow_scroll_active(s) == 0, "idle after bounce");
meow_scroll_free(s);
meow_widget_free(lv);
}
static void test_snap(void)
{
meow_widget *lv = make_list();
meow_scroll *s = meow_scroll_wrap(lv, 0);
meow_scroll_set_snap(s, ITEM_H); /* 吸附到行边界 */
/* 慢拖到某非边界位置并给 small velocity → fling 结束 snap 到行倍数 */
uint64_t t = meow_ui_time_ms();
for (int i = 0; i < 6; i++) meow_scroll_track(s, 20, t + (uint64_t)i * 16);
meow_scroll_fling(s);
int max_off = 0, mono = 1;
pump_until_idle(s, &max_off, &mono);
int end = meow_scroll_offset(s);
CHECK((end % ITEM_H) == 0, "snap to item multiple got %d", end);
CHECK(end <= MAXS, "snap in range %d", end);
meow_scroll_free(s);
meow_widget_free(lv);
}
static void test_stop(void)
{
meow_widget *lv = make_list();
meow_scroll *s = meow_scroll_wrap(lv, 0);
uint64_t t = meow_ui_time_ms();
for (int i = 0; i < 6; i++) meow_scroll_track(s, 20, t + (uint64_t)i * 16);
meow_scroll_fling(s);
CHECK(meow_scroll_active(s) == 1, "fling active (stop test)");
meow_scroll_stop(s);
CHECK(meow_scroll_active(s) == 0, "stopped immediately");
meow_scroll_free(s);
meow_widget_free(lv);
}
/* ============ 池槽归还回归(M19)============
* meow_scroll_wrap 从 MEOW_SCROLL_POOL(=8) 个 wrap 上下文槽里占一个;旧实现 free 只归还
* 会话槽、不归还 wrap 槽 → 进程内最多成功 wrap 8 次,之后恒 NULL(图库每次 load_dir 重建,
* 进 8 个目录后列表丢惯性)。以下用例覆盖:长循环回收、free 后重建、交错不串槽、
* 池满仍返回 NULL、直接 create 不误清槽、free(NULL)/二次 free 安全。 */
#define WRAP_POOL 8 /* = meow_ui_scroll.c 的 MEOW_SCROLL_POOL(内部常量,测试按契约固定) */
/* 连续 64 次 wrap/free:每次都要拿到可用会话,且绑定当前列表 */
static void test_wrap_free_recycle(void)
{
meow_widget *lv = make_list();
int ok = 0, first_fail = -1;
for (int i = 0; i < 64; i++) {
meow_scroll *s = meow_scroll_wrap(lv, 0);
if (!s) { if (first_fail < 0) first_fail = i; continue; }
meow_list_set_scroll(lv, 0);
meow_scroll_track(s, 7, meow_ui_time_ms());
if (meow_scroll_offset(s) == 7) ok++;
else if (first_fail < 0) first_fail = i; /* 串槽:set/get 不指向本列表 */
meow_scroll_free(s);
}
CHECK(ok == 64, "64 wrap/free cycles all usable, ok=%d first_fail=%d", ok, first_fail);
meow_widget_free(lv);
}
/* wrap → free → 同一列表再 wrap:必须重新可用 */
static void test_wrap_after_free(void)
{
meow_widget *lv = make_list();
meow_scroll *s = meow_scroll_wrap(lv, 0);
CHECK(s != NULL, "wrap#1 same list");
meow_scroll_free(s);
s = meow_scroll_wrap(lv, 0);
CHECK(s != NULL, "wrap#2 same list after free");
if (s) {
meow_list_set_scroll(lv, 0);
meow_scroll_track(s, 12, meow_ui_time_ms());
CHECK(meow_scroll_offset(s) == 12, "re-wrapped scroll works got %d", meow_scroll_offset(s));
meow_scroll_free(s);
}
meow_widget_free(lv);
}
/* 8 个同时活跃 → 第 9 个仍返回 NULL(池满不越界);释放其一的槽立刻可再用 */
static void test_wrap_pool_full(void)
{
meow_widget *lv[WRAP_POOL];
meow_scroll *s[WRAP_POOL];
int ok = 0;
for (int i = 0; i < WRAP_POOL; i++) {
lv[i] = make_list();
s[i] = meow_scroll_wrap(lv[i], 0);
if (s[i]) ok++;
}
CHECK(ok == WRAP_POOL, "all %d wraps active, ok=%d", WRAP_POOL, ok);
meow_widget *extra_lv = make_list();
meow_scroll *extra = meow_scroll_wrap(extra_lv, 0);
CHECK(extra == NULL, "9th simultaneous wrap returns NULL (pool full), got %p", (void *)extra);
/* 释放中间一个 → 槽立即回收;活着的实例仍各自绑定自己的列表 */
meow_scroll_free(s[3]);
s[3] = NULL;
meow_scroll *again = meow_scroll_wrap(extra_lv, 0);
CHECK(again != NULL, "wrap succeeds right after freeing one slot");
if (again) {
meow_list_set_scroll(extra_lv, 0);
meow_scroll_track(again, 9, meow_ui_time_ms());
CHECK(meow_scroll_offset(again) == 9, "new wrap bound to its own list got %d",
meow_scroll_offset(again));
meow_list_set_scroll(lv[5], 33);
CHECK(meow_scroll_offset(s[5]) == 33, "other live wrap keeps its own list got %d",
meow_scroll_offset(s[5]));
meow_scroll_free(again);
}
for (int i = 0; i < WRAP_POOL; i++) {
if (s[i]) meow_scroll_free(s[i]);
meow_widget_free(lv[i]);
}
meow_widget_free(extra_lv);
}
/* 交错:两个 wrap 活跃 → free 其一 → 再 wrap,三者不串槽 */
static void test_wrap_interleaved(void)
{
meow_widget *a = make_list(), *b = make_list(), *c = make_list();
meow_scroll *sa = meow_scroll_wrap(a, 0);
meow_scroll *sb = meow_scroll_wrap(b, 0);
CHECK(sa != NULL && sb != NULL, "two wraps (interleaved) a=%p b=%p", (void *)sa, (void *)sb);
if (!sa || !sb) {
meow_scroll_free(sa); meow_scroll_free(sb);
meow_widget_free(a); meow_widget_free(b); meow_widget_free(c);
return;
}
meow_list_set_scroll(a, 11);
meow_list_set_scroll(b, 22);
CHECK(meow_scroll_offset(sa) == 11 && meow_scroll_offset(sb) == 22,
"distinct ctx a=%d b=%d", meow_scroll_offset(sa), meow_scroll_offset(sb));
meow_scroll_free(sa);
CHECK(meow_scroll_offset(sb) == 22, "remaining wrap intact after freeing first got %d",
meow_scroll_offset(sb));
meow_scroll *sc = meow_scroll_wrap(c, 0);
CHECK(sc != NULL, "third wrap after freeing first");
if (sc) {
meow_list_set_scroll(c, 0);
meow_scroll_track(sc, 5, meow_ui_time_ms());
CHECK(meow_scroll_offset(sc) == 5, "third wrap bound to its own list got %d",
meow_scroll_offset(sc));
CHECK(meow_scroll_offset(sb) == 22, "second wrap not clobbered by third got %d",
meow_scroll_offset(sb));
meow_scroll_free(sc);
}
meow_scroll_free(sb);
meow_widget_free(a); meow_widget_free(b); meow_widget_free(c);
}
/* ---- 直接 create(非 wrap)的回调:用于验证 free 不误清别人的 wrap 槽 ---- */
static int g_direct_off;
static void direct_set(void *c, int off) { (void)c; g_direct_off = off; }
static int direct_max(void *c) { (void)c; return 1000; }
static int direct_get(void *c) { (void)c; return g_direct_off; }
static void test_direct_create_free_keeps_wrap_slot(void)
{
meow_widget *lv = make_list();
meow_scroll *w = meow_scroll_wrap(lv, 0);
CHECK(w != NULL, "wrap before direct create");
meow_scroll *d = meow_scroll_create(direct_set, direct_max, direct_get, NULL, NULL);
CHECK(d != NULL, "direct create");
if (d) {
meow_scroll_track(d, 40, meow_ui_time_ms());
CHECK(g_direct_off == 40, "direct scroll tracks got %d", g_direct_off);
meow_scroll_free(d); /* 非 wrap 实例:不得归还/清除任何 wrap 槽 */
}
if (w) {
meow_list_set_scroll(lv, 0);
meow_scroll_track(w, 10, meow_ui_time_ms());
CHECK(meow_scroll_offset(w) == 10, "wrap intact after freeing direct instance got %d",
meow_scroll_offset(w));
meow_scroll_free(w);
}
meow_widget_free(lv);
}
/* free(NULL) 安全;同一实例二次 free 不崩、不清活着的 wrap 槽 */
static void test_wrap_free_null_and_double(void)
{
meow_scroll_free(NULL); /* 必须安全 */
meow_widget *a = make_list(), *b = make_list();
meow_scroll *sa = meow_scroll_wrap(a, 0);
meow_scroll *sb = meow_scroll_wrap(b, 0);
CHECK(sa != NULL && sb != NULL, "two wraps (double free) a=%p b=%p", (void *)sa, (void *)sb);
if (sb) meow_list_set_scroll(b, 44);
if (sa) meow_scroll_free(sa);
if (sa) meow_scroll_free(sa); /* 二次 free:重复清零,不得清掉 sb 的 ctx */
if (sb) {
CHECK(meow_scroll_offset(sb) == 44, "live wrap intact after double free got %d",
meow_scroll_offset(sb));
meow_scroll_free(sb);
}
meow_scroll *sc = meow_scroll_wrap(a, 0);
CHECK(sc != NULL, "wrap still available after free(NULL)/double free");
if (sc) meow_scroll_free(sc);
meow_widget_free(a); meow_widget_free(b);
}
/* 虚拟化列表行源(仅需 count 有效,不绘制) */
static int lv_src2(int idx, meow_list_item *out, void *ctx)
{
(void)ctx;
if (idx < 0 || idx >= COUNT) return 0;
memset(out, 0, sizeof *out);
out->title = "row";
return 1;
}
/* 槽复用不继承上一个 wrap 的 kind:list → listview → list 交替必须各自生效
* (free 时清 lv 标记,wrap 时按参数重设;否则复用槽上的旧 kind 会让 set/get 打空) */
static void test_wrap_slot_kind_reset(void)
{
meow_widget *plain = make_list();
meow_widget *vl = meow_listview_create(0, 0, 480, LIST_H, COUNT, lv_src2, NULL,
MEOW_LIST_SEL_SINGLE);
meow_listview_set_item_h(vl, ITEM_H);
meow_scroll *s = meow_scroll_wrap(plain, 0);
CHECK(s != NULL, "wrap plain list");
if (s) meow_scroll_free(s);
s = meow_scroll_wrap(vl, 1);
CHECK(s != NULL, "wrap listview on recycled slot");
if (s) {
meow_listview_set_scroll(vl, 0);
meow_scroll_track(s, 20, meow_ui_time_ms());
CHECK(meow_scroll_offset(s) == 20, "listview wrapped scroll works got %d",
meow_scroll_offset(s));
meow_scroll_free(s);
}
s = meow_scroll_wrap(plain, 0);
CHECK(s != NULL, "wrap plain list again on recycled slot");
if (s) {
meow_list_set_scroll(plain, 0);
meow_scroll_track(s, 15, meow_ui_time_ms());
CHECK(meow_scroll_offset(s) == 15, "plain list after listview slot reuse got %d",
meow_scroll_offset(s));
meow_scroll_free(s);
}
meow_widget_free(vl);
meow_widget_free(plain);
}
int main(void)
{
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
/* 池槽回归先跑:新建进程里第一个动 pool 的用例,便于对照「修复前 8/64、修复后 64/64」 */
test_wrap_free_recycle();
test_track_follow();
test_fling_no_boundary();
test_fling_boundary_bounce();
test_snap();
test_stop();
test_wrap_after_free();
test_wrap_pool_full();
test_wrap_interleaved();
test_direct_create_free_keeps_wrap_slot();
test_wrap_slot_kind_reset();
test_wrap_free_null_and_double();
meow_ui_shutdown();
printf("scroll: %d pass / %d fail\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}