fix(ncm): 三连修 — 封面切歌不更新/缓冲无反馈失败无声/暂停切歌边界 (REPORT §18)

- cover worker 忙等死循环(成功后不清目标,每圈解码+换入+gen++,CPU 100%+每帧整盘重绘)
  → 成功清目标转怠速; 失败指数退避 0.5-4s 连 8 次放弃+WARN 日志; 去重加在拉目标;
  cover_get 持锁+cover_put 修绘制期 UAF; g_host_mock 防真网守卫
- 封面兜底污染修复: 不再把单曲 al_pic 写进 g_st.cover_url(否则 al_pic 缺失曲回落成
  上一首封面且撞去重 → 封面永停); plr_page_enter 强制重估自愈; pl_page_enter 回填歌单封面
- 缓冲 UI: BUFFER 态「缓冲中… N%」(g_play.dl_done/total, 下载回调累加); 播放错误中文
  错误条+snack(旧版 err 无 UI 消费者=无声死等); STOPPED 态播放键=重试; play/next 清 err
- 写失败 set_err 修连环跳歌(旧版静默 break 被外层当自然播完,流死时跳完歌单+狂发 song_url);
  still_current 护栏修下载途中切歌的状态/快照过期覆盖
- 验证(0 真实请求): cover_chain_sim 7 组+player_flow_sim P1-P5 全 PASS, 旧代码对照
  T2/P2/P4 必败坐实根因; audiod_chain_sim 14/14; pagination/window/core+qemu 回归 PASS;
  宿主+mipsel 自有代码 0 warning; 9 页离屏渲染 rc=0 (含新 mock 页 playerbuf/playererr)
- ncm md5 50d41bbf4a379e925699ba013b997ffd (未部署真机, 留主会话)
This commit is contained in:
MeowOS committed 2026-09-01 05:29:09 +08:00
1 parent 614694ad4a
commit 6200a41b71
13 files changed
+820 -20

No files matched your search

+132
View File
@@ -1454,3 +1454,135 @@ b2b6869990c8ef3906c100f18c7194e8 bench/window_sim.c
- **meta 盘缓存无版本化清理**:结构体再改版靠 ver/size 判 miss 自动重建;无 TTL
(歌曲改名/换封面不主动失效——与 ncm_cache 同级的「缓存可再生」取舍)。
- **host 构建有 SDK `meow_ui_vol.c`「system 返回值忽略」告警**(SDK 源,非本 App;mipsel musl 无)。
---
## 18. 三连修:封面切歌不更新 + 缓冲无反馈/失败无声 + 暂停/切歌边界稳健性(2026-09-01,用户三连报)
> 范围:仅 `patch_v2/ncm_app/`(ui/play/logic/bench/test + 本文件)+ 根 `.gitignore`(新增 3 个 sim
> 二进制忽略条目)。未动 `sdk/`、`meow_launcherd/`、`meow_audiod/`、`vendor/`。
> 风控纪律:本批真实 NCM 请求 **0 次**(全部离线 stub/sim;并新增 mock 防真网守卫,见 §18.5)。
> **本批未部署真机**(部署留主会话统一做)。
### 18.1 问题①:封面只在第一首歌出现、切歌不更新 —— 根因(三因叠加)
1. **cover worker 忙等死循环(主因,CPU 饥饿)**:`cover_worker` 换入成功后**不清目标 URL**
(`g_tgt_url` 残留),主循环每圈 缓存读 200KB + JPEG 解码 + free/换入 + `g_gen++`,成功路径
**无 sleep** → 首张封面到位后 worker 永久 100% CPU;同时 `cover_changed()` 恒真 → 播放页
tick 每帧整盘重绘(黑胶行扫 sqrt + cover blit)。单核 MIPS 上 UI 线程被饿到极低帧率,
慢网络下新封面请求(10s 超时级)看似永不完成。sim 实证:旧代码 settle 后 1.2s 内
`cover_changed` 翻动 **119 次**(新代码 0 次)。
2. **封面兜底值污染(确定性子根因)**:`player_play_song` 把本曲 `al_pic` 经
`appstate_set_playing_song` 写进 `g_st.cover_url`(本意=歌单封面兜底)。此后 al_pic 缺失的
歌曲回落到的是**上一首的专辑图 URL**,且撞 `cover_request` 去重(== 当前展示)→ 封面永远
停在上一首。
3. **绘制期 UAF**:`cover_get()` 无锁返回裸指针,UI 绘制期间 worker 换入即 `meow_img_free`
同一张图(忙等循环下每 ~1/100s 一次)→ use-after-free/撕裂绘制风险。
**修法**(cover.c + player.c + playlist.c):
- worker 换入成功即 `g_tgt_url[0]=0` 转怠速(250ms 轮询);下载/解码失败**指数退避**
0.5s→4s、连续 8 次放弃并清目标(UI 重进页会重新提交),每次失败 WARN 日志(不再无声)。
- `cover_request` 去重加判「在拉目标」(g_tgt_url)+ `g_host_mock` 直接返回(0 网络纪律)。
- `cover_get()` 改**持锁返回** + 新增 `cover_put()`,两个绘制调用点(player 黑胶/playlist
头块)配对使用,UAF 闭合。
- `player_play_song` 不再用 al_pic 污染兜底(`appstate_set_playing_song(idx, NULL)`),
`g_st.cover_url` 回归「歌单封面兜底」单语义。
- `plr_page_enter` 清零 `g_last_meta_id`/`g_last_song` 强制重估(重进播放页自愈:
离开期间切歌/封面失败后回来首个 tick 即重请求当前曲封面)。
- `pl_page_enter` 补请求歌单封面——旧版 `playlist_open` 只 `cover_cache_reset` 清掉、无人
再请求 → 歌单头块封面永远占位 ♪(顺手回填,缓存命中后开销≈一次盘读+解码)。
### 18.2 问题②:点歌后全程无反馈、失败无声死等 —— 根因与修法
- **下载本就不在 UI 线程**(核查结论):song_url 获取 + 流式落盘全在播放线程
(`run_song`→`song_local_file`),UI 无阻塞点;「像卡死」= ①无缓冲 UI ②错误无出口
③问题①的 CPU 忙等拉低整机帧率 三感叠加。
- `g_play.err/errmsg` **无任何 UI 消费者**(`player_manager_error()` 全仓零调用)→
VIP/版权/超时全部无声。修法:
- 播放页歌词区三态:`NCM_PC_BUFFER` →「缓冲中… N%」(新增 `g_play.dl_done/dl_total`,
下载回调锁内累加,song_url 的 size 做分母,未知则转 KB;tick 按 ~2KB 粒度刷动态带);
`err!=0` → 红字中文错误条(无音源 VIP/版权 / 登录过期 / 网络失败)+「点播放键重试」,
错误沿弹 snack;有词时歌词照旧。
- 播放键在 `STOPPED 且有当前曲`(出错后)= **重试**(`player_manager_play(cur)`),
图标在 PAUSED/STOPPED 画三角、PLAYING/BUFFER 画暂停。
- `player_manager_play/next` 清 `err` 并重置下载计数(旧错误不残留)。
### 18.3 问题③:暂停/恢复/切歌边界稳健性
- **写失败连环跳歌(真根因级)**:`audio_out_write` 失败旧版静默 `break` → 外层当
「自然播完」`next(1)` → 流死时每首都写失败 → 连环跳完整个歌单且每跳一首发一次
song_url 请求(风控隐患 + 用户看到的「放不了」)。修:写失败 `set_err(NCM_ERR_IO)`
→ STOPPED 停住、cur 不动、UI 出错误条。sim 实证:旧代码写失败后 err=0、cur 冲到 -1;
新代码 err=NCM_ERR_IO、cur 不变、writes=1 即停。
- **下载途中切歌竞态**:旧版旧曲下载完成后继续开解码/开音频并无条件
`state=PLAYING` + 写过期快照 → 把新曲的 BUFFER 覆盖成 PLAYING(UI 假「在播」)+ 封面/歌名
短暂回跳。修:`still_current()` 护栏——物化后/下载失败后/下载完成后三处校验
`cur_song==idx && state==BUFFER`,否则静默回外层(缓存已落盘不亏)。sim P4 实证新旧差异。
- 流死重开链(A2 已有 resume 自动 reopen)UI 侧闭环:被抢占→PAUSED(播/停图标正确)、
resume reopen 失败 → STOPPED+错误条(可重试);`audiod pause rc=-9` 路径不动(pause 失败
仅记日志,恢复时走 reopen)。
- 已知边界(不改):下载/ song_url 不可中途取消(ncm_http 无 cancel),切歌响应最坏
多等一个在途请求超时(30s 上限,正常秒级);BUFFER 态点暂停无效(语义:缓冲未开播)。
### 18.4 验证记录(0 真实 NCM 请求)
| 项 | 方式 | 结果 |
|---|---|---|
| 封面链 | `bench/cover_chain_sim.c`(新;真实 cover.c+ncm_cache,stub HTTP/假 img) | **新代码 7 组全 PASS**:首请求出图/定居零忙等/切歌换图+cover_current.jpg 同步/去重/失败×2退避成功/连败 8 次放弃保留旧图/空 URL+reset。**旧代码对照**:T2 忙等 119 次/1.2s FAIL(坐实根因①) |
| 播放状态机 | `bench/player_flow_sim.c`(新;真实 player_manager.c 播放线程,stub 解码/音频/网络) | **新代码 P1-P5 全 PASS**:自然顺延(err=0)/写失败置错停住不跳歌/无音源错误条/下载中切歌无过期状态覆盖/错误后重试。**旧代码对照**:P2(err=0+cur=-1 连环跳歌)、P4(旧曲覆盖 PLAYING+过期快照)FAIL |
| audiod 链回归 | `bench/audiod_chain_sim`(真实 audiod_host 假 socket) | 14/14 PASS(含抢占/流死重开,问题③链路回归) |
| 滑窗回归 | pagination_sim + window_sim(重编新 appstate.c) | 全 PASS |
| core 回归 | test_ncm_core_host + `qemu-mipsel out/test_ncm_core_mipsel` | 全 PASS |
| 宿主构建 | build_ncm_host.sh | 自有代码 **0 warning**(仅 SDK meow_ui_vol.c 既有告警) |
| 宿主离屏渲染 | home/playlist/player/playerbuf/playererr/settings/search/login/lands | 全 rc=0;playerbuf 出「缓冲中… 25%」位(宿主 mock 字体无 CJK,中文留白为既有宿主限制,设备 GBK 字库正常)、playererr 错误条+播放三角 |
| mipsel | build_ncm.sh(自有代码 -Wall -Wextra) | **0 warning**;text 868772 / bss 1080900(与 §17 持平) |
### 18.5 md5(本批产物 + 改动文件)
```
50d41bbf4a379e925699ba013b997ffd ncm # mipsel 静态 App(新;未部署,留主会话)
87de141c6a2719bb2b93807f282077de ncm_host # 宿主离屏调试二进制
6715607f561c5df83b251569b0ae2b7c app/logic/cover.c
d6aee631734622526befb977d4e3cddf app/logic/appstate.c
ca22772678de6e293747c9ef4890b80f app/ncm_app_internal.h
2d0eb786ec78f48e7e0625f4bd7a74c9 app/play/player_manager.c
d2bdfbed5569af760119d0ffb785e43e app/ui/player.c
c596763af26fd0c687286a250fccd90a app/ui/playlist.c
08fc30f6ddf77c638ca4a8f3a20aef0e app/test/test_ncm_app.c
30b163bfc2c880b819f8168b10895161 bench/cover_chain_sim.c
fee4204e945866f5a07ca7d191fa83e3 bench/player_flow_sim.c
```
### 18.6 出入清单
```
修改:
app/logic/cover.c # worker 成功清目标转怠速(修忙等死循环)+失败指数退避 8 次放弃+
# WARN 日志+去重加在拉目标+g_host_mock 防真网+cover_get 持锁/cover_put(修 UAF)
app/ui/player.c # 缓冲三态 UI(缓冲中…N%/错误条+重试)+播放键 STOPPED=重试+enter 自愈重估+
# 下载进度 tick 刷新+错误沿 snack+cover_put 配对+不污染封面兜底
app/ui/playlist.c # cover_put 配对+pl_page_enter 请求歌单封面(头块回填)
app/play/player_manager.c# dl_done/dl_total 进度+play/next 清 err+still_current 切歌护栏三处+
# 写失败 set_err(修连环跳歌)
app/logic/appstate.c # set_playing_song 注释对齐(cover_url 仅兜底语义)
app/ncm_app_internal.h # cover_put/player_manager_dl 声明+g_play.dl_*+cover_get 语义注释
app/test/test_ncm_app.c # mock 新页 playerbuf/playererr(缓冲/错误态离屏渲染)
.gitignore # window_sim/cover_chain_sim/player_flow_sim 二进制忽略
ncm / ncm_host # 重编产物
新增:
bench/cover_chain_sim.c # 封面链离线回归(7 组,含旧代码对照实证忙等根因)
bench/player_flow_sim.c # 播放状态机离线回归(P1-P5,含旧代码对照实证跳歌/竞态根因)
REPORT.md # 本批记录(§18)
(未动 sdk/、meow_launcherd/、meow_audiod/、vendor/;未部署真机;未动设备系统/恢复区)
```
### 18.7 诚实出入 / 遗留
- **真机未部署未验**(留主会话)。真机验收点:①切歌封面跟随(含离开播放页再进)②点歌即见
「缓冲中… N%」、失败出中文错误条+可点播放键重试 ③歌单头块封面回填 ④退出播放页再进无残旧封面。
- **切歌响应延迟下限**:在途 song_url/下载不可取消(ncm_http 无 cancel 口),连切时旧请求
跑完才轮新曲(正常秒级;30s 超时兜底)。要更低延迟需 core 层加取消位,本批未动 core。
- **封面失败放弃后**靠「重进播放页/切歌」重新武装(enter 自愈),无定时重试——弱网长期
挂机时封面可能缺席但不影响播放。
- 播放页 BUFFER 态进度条仍画 0:00 标尺(缓冲文案在歌词区),未做进度条一体化——观感项。
- meow_audiod_host 工作区有他人改动(其他 agent 在改 audiod),本批未 add/未提交该文件。