feat(ncm): 三期第一批 — 歌单真分页(取消256截断)+专辑封面ncm_cache+歌词后台加载补锁

[agent-239] 仅改 patch_v2/ncm_app/。

1. 歌单分页(REPORT §14.1): track/all 全 404(§13.1) 弃用, 回 detail(n=1)全量 trackIds
   + song_detail 分批(≤NCM_SONG_BATCH=100) 触底逐页加载。appstate_load_playlist_songs_page
   offset=0 重建 song_ids[]/track_ids_count 载第0页, offset>0 追加(失败保留已载);
   playlist.c g_pl_more=(song_count<track_ids_count && <NCM_SONG_MAX), 触底 g_ploffset=song_count
   起下一页(保留滚动), 首屏失败整页空态+点按重试, 追加失败 snack+保留已载。NCM_SONG_MAX
   256→1000(单页渲染上限, bss +~1MB), NCM_SONG_BATCH 64→100, 删 NCM_PLAYLIST_PAGE(track/all 遗留)。

2. 专辑封面(§14.2): cover.c 封面改走 ncm_cache(LRU, 键=djb2(url)→song_cov<h>.jpg 同池),
   缓存命中免下载/未命中下载→open/write/commit 落盘, 仍写 cover_current.jpg 供 launcher;
   顺带修真 bug: save_cover_raw 在已持 g_cm 临界区内再 lock(非递归死锁)→封面永不刷新, 改调用方持锁。
   歌单列表/详情头块封面保持现状。

3. 歌词后台加载(§14.3): lyric_worker 已异步(§11)未改线程模型; 补 lyric.c g_lyric_mu 数据竞争锁,
   写侧 lyric_parse 整段持锁(清空用 lyric_clear_nolock 免二次取锁), 读侧 lyric_count/line_at/
   line_text/line_tl 逐次取锁, 消除 UI/IPC 与 worker 间数据竞争(原 §10.9 FM4 良性竞态)。

验证(0 真实 NCM 请求): bench/pagination_sim.c 离线 stub 驱动真实 appstate 分页函数全 PASS
(130首读全程/5191首封顶1000/空/越界无操作); mipsel build_ncm.sh 0 warning(text 842820/bss 2052068);
宿主 build_ncm_host.sh+MEOW_UI_DUMP home/playlist/player/lands 渲染正常无崩溃; host SDK_SRC 补
meow_pinyin(拼音IME py_predict 漏编致链接断)。ncm md5 b34bc1ed, ncm_host md5 1a2327e0。
未动 sdk/(主会话改 meow_ui_ime.c)/meow_launcherd/meow_amsd/vendor/core; 未 commit 它们。
This commit is contained in:
MeowOS committed 2026-08-31 19:33:10 +08:00
1 parent 5fac5e380d
commit ee104f1b18
10 files changed
+471 -87

No files matched your search

+31 -13
View File
@@ -34,7 +34,7 @@ static int g_placked;
static volatile int g_plloading;
static volatile int g_plloaded_idx;
static volatile int g_ploffset; /* 本页起始下标(0=首屏重头;>0=触底追加) */
static volatile int g_pl_more; /* 1=尚有下一页可加载(页满且未到 NCM_SONG_MAX) */
static volatile int g_pl_more; /* 1=尚有下一页可加载(song_count < track_ids_count 且未到 NCM_SONG_MAX) */
static void pl_drag_reset(void)
{
@@ -157,7 +157,7 @@ static void pl_draw_empty(void *ctx)
if (g_load_err == 0 && g_st.song_count != 0) return;
if (g_plloading) { ncm_draw_empty(r_list, "LOADING", "正在加载曲目..."); return; }
if (g_load_err == NCM_ERR_AUTH) ncm_draw_empty(r_list, "SESSION EXPIRED", "请重新登录");
else if (g_load_err) ncm_draw_empty(r_list, "LOAD FAILED", "返回后重试");
else if (g_load_err) ncm_draw_empty(r_list, "LOAD FAILED", "点按重试");
else ncm_draw_empty(r_list, "NO TRACKS", "这个歌单是空的");
}
@@ -197,12 +197,11 @@ static void *playlist_worker(void *arg)
int rc;
if (g_host_mock) rc = 0;
else if (my_idx < 0) rc = NCM_ERR_ARG;
else rc = appstate_load_playlist_songs_page(my_idx, off, NCM_PLAYLIST_PAGE);
else rc = appstate_load_playlist_songs_page(my_idx, off, NCM_SONG_BATCH);
pthread_mutex_lock(&g_plmu);
g_load_err = rc;
/* 页满(==NCM_PLAYLIST_PAGE)且未到记忆上限 → 还有下一页;否则到屏尾 */
int loaded = (rc == NCM_OK) ? (g_st.song_count - off) : 0;
g_pl_more = (rc == NCM_OK) && (loaded >= NCM_PLAYLIST_PAGE) &&
/* 本页后仍有多余 trackIds(tail 未到)且未到内存上限 → 还有下一页;否则到屏尾。 */
g_pl_more = (rc == NCM_OK) && (g_st.song_count < g_st.track_ids_count) &&
(g_st.song_count < NCM_SONG_MAX);
g_plloading = 0;
g_plloaded_idx = my_idx;
@@ -211,10 +210,11 @@ static void *playlist_worker(void *arg)
return NULL;
}
/* 触底加载下一页:仅当仍有下一页、未在载、且未到 NCM_SONG_MAX 时发。 */
/* 触底加载下一页:仅当仍有下一页、未在载、未失败、未到 NCM_SONG_MAX 时发。 */
static void pl_maybe_load_more(void)
{
if (g_plthr_on || g_load_err != 0 || g_host_mock) return;
if (g_plthr_on || g_host_mock) return;
if (g_load_err != 0) return; /* 失败态:走点按重试,不自动循环 */
if (!g_pl_more) return;
if (g_st.song_count >= NCM_SONG_MAX) return;
/* 视口底接近列表底(最后一行即将可见即预取) */
@@ -222,11 +222,11 @@ static void pl_maybe_load_more(void)
int max_scroll = list_px - r_list.h;
if (max_scroll < 0) max_scroll = 0;
if (meow_listview_scroll(g_list) < max_scroll - APP_ROW_H * 2) return;
g_ploffset = g_st.song_count;
g_ploffset = g_st.song_count; /* 下一页起始 = 已物化数 */
g_plloading = 1;
g_plthr_on = 1;
if (pthread_create(&g_plthr, NULL, playlist_worker, NULL) != 0) {
g_plthr_on = 0; g_plloading = 0; g_load_err = NCM_ERR_IO;
g_plthr_on = 0; g_plloading = 0; ncm_snack("加载失败");
}
}
@@ -236,6 +236,7 @@ void playlist_refresh(void)
if (g_plthr_on) return;
g_load_err = 0;
g_st.song_count = 0;
g_st.track_ids_count = 0;
g_ploffset = 0;
g_pl_more = 0;
g_plloading = 1;
@@ -256,8 +257,18 @@ void playlist_tick(uint64_t now)
g_plloading = 0;
if (g_plloaded_idx != g_st.active_playlist) { playlist_refresh(); return; }
if (g_load_err != 0) {
if (g_ploffset == 0) g_st.song_count = 0; /* 首屏失败 → 整页空态 */
else { ncm_snack("加载失败"); g_load_err = 0; } /* 追加失败 → 保留已载歌曲 */
if (g_ploffset == 0) {
g_st.song_count = 0; /* 首屏失败 → 整页空态(点按重试) */
pl_apply(1);
} else {
/* 追加失败 → 保留已载歌曲 + 重置 g_pl_more(触底/点击可重试) */
ncm_snack("加载失败");
g_load_err = 0;
g_pl_more = (g_st.song_count < g_st.track_ids_count) &&
(g_st.song_count < NCM_SONG_MAX);
pl_apply(0);
}
return;
}
pl_apply(g_ploffset == 0); /* 首屏回顶;触底追加保持滚动 */
}
@@ -289,7 +300,14 @@ int playlist_on_tap(int x, int y)
if (x >= r_cmt.x && x < r_cmt.x + r_cmt.w && y >= r_cmt.y && y < r_cmt.y + r_cmt.h) {
pl_drag_reset(); ncm_snack("评论功能开发中"); return 1;
}
if (g_load_err != 0 || g_st.song_count == 0) return 0;
if (g_load_err != 0) {
/* 失败态(LOAD FAILED / SESSION EXPIRED / NETWORK ERROR):点按内容区重试。 */
pl_drag_reset();
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
playlist_refresh();
return 1;
}
if (g_st.song_count == 0) return 0; /* 空歌单(无失败):不重试 */
if (meow_widget_hit(g_list, x, y)) {
/* 误触修复:小拖动(<SWIPE_THRESH)会先滚动列表、release 又被 SDK 判成 TAP,旧的
* meow_listview_hit(x,y) 用“释放点 + 更新后的 scroll”命中 → 打到“已移动到手指下”的行,