Files
MeowOS-SDK/sdk/test_meow_ui_scroll.c
T
AskaEth c85877b447 SDK: 修 meow_scroll_wrap 池槽泄漏(free 归还 wrap 上下文槽)+ 64 次回收/交错/池满回归(M19)
meow_scroll_wrap 从 MEOW_SCROLL_POOL(=8) 个 wrap 上下文槽占一个,meow_scroll_free 只归还
会话槽、从不归还 wrap 槽也不清 ctx.w → 进程内最多成功 wrap 8 次,之后恒 NULL。图库
list_rebuild/load_dir 每次重建,进 8 个目录后列表丢惯性(g_scroll==NULL 走 scroll_by 兜底),
os.meow.files 同样每次 reload 重建。

- meow_scroll 结构内记 wrap_slot(创建置 -1),wrap 时写入槽下标;free 时按该下标清
  g_wrap_ctx[].w/lv 并归还 g_wrap_used,随后置 -1(二次 free 不触碰任何槽)。
  直接 create 的实例 wrap_slot=-1,free 不误清别人的槽。
- 池满语义不变:8 个同时活跃时第 9 个 wrap 仍返回 NULL(不越界);free(NULL) 安全。
- test_meow_ui_scroll 补回归:64 次 wrap/free 全可用(修复前 8/64,修复后 64/64)、free 后
  再 wrap、8 满 + 第 9 个 NULL + 释放其一即复用、两 wrap 交错 free 其一不串槽、直接 create
  不误清槽、list→listview→list 槽复用不残留 kind、free(NULL)/二次 free。
2026-11-05 19:38:55 +08:00

397 lines
15 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_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_ui_nav.c \
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./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 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;
}