Files
EzVibe/docs/investigation/segfault-root-cause-and-fix.md
e2hang 798e5c2f7d checkpoint: segfault root cause analysis + Live2D stability fixes
- _refresh_messages: widget pool reuse to avoid layout cascade → QWebEngineView crash
- viewer.html: QWebChannel bridge, texture resize, burst render, gl.finish
- live2d_view.py: debug checkpoints, playMotion/setExpression render-on-demand
- main.py: Chromium flags --no-sandbox --in-process-gpu --disable-gpu-rasterization --ignore-gpu-blocklist
- scheduler.py: test_reminder re-enabled
- docs: complete root cause analysis
2026-05-23 13:33:58 +08:00

172 lines
6.6 KiB
Markdown
Raw Permalink 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.
# EzVibe Segfault — 完整根因分析与修复方案
**日期:** 2026-05-23
**状态:** ✅ 已定位根因,已修复
---
## 一、崩溃现象
```
QObject::setParent: Cannot set parent, new parent is in a different thread (x12)
[JS→PY] init_start → webgl_ok → shader_ok → test_rect_ok → model_ok:d=307
[JS→PY] loading_textures:n=2 → texture_ok:0 → texture_ok:1 → textures_done:n=2
[JS→PY] building_buffers → buffers_ok:n=307 → scheduling_render
[JS→PY] ready → first_frame_ok
... 运行 20-30 秒正常 ...
Segmentation fault (core dumped)
```
- Live2D 初始化全部成功WebGL 上下文、Shader、模型 307 drawables、纹理、缓冲区、首帧渲染
- 崩溃发生在 `first_frame_ok` 之后 **20-30 秒**
- 稳定复现100%
---
## 二、诊断过程
### 阶段 1排除 GPU 命令缓冲区损坏
早期崩溃在纹理加载后立即发生,伴随 `GPU state invalid after WaitForGetOffsetInRange` 错误。
**修复:** 添加 `--no-sandbox --in-process-gpu` Chromium 标志,消除了 GPU 进程 IPC 同步问题。
**结果:** GPU 命令缓冲区错误消失,但 segfault 仍然存在(只是延迟出现)。
### 阶段 2排除连续 WebGL 渲染循环
`viewer.html` 使用 `setInterval(100ms)` 持续渲染 WebGL 帧。每次渲染触发 `gl.bufferSubData` + `gl.drawElements`,导致 Chromium GPU 对象频繁跨线程 reparent产生额外 `setParent` 警告)。
**修复:** 将连续渲染改为单次 burst 模式(初始 10 帧后停止),按需渲染。
**结果:** `setParent` 警告数量从 23 个减少到 12 个(仅初始化阶段),且第一帧渲染成功。崩溃延迟到 ~30 秒。
### 阶段 3追踪崩溃时间点
通过添加 QWebChannel 调试探针(`[JS→PY]` 消息)精确定位:
```
[JS→PY] first_frame_ok
(20-30 秒正常运行)
Segmentation fault
```
崩溃时间(启动后 ~30-40 秒,即 first_frame_ok 后 20-30 秒)精准对应 **brain timer** (`_init_brain_timer.start(30_000)`)。
### 阶段 4隔离确认
禁用 brain timer + test_reminder 后,应用稳定运行 **2 分钟+**,无崩溃。
**确认:** brain timer每 30s或 scheduler 触发的 `show_reminder``_refresh_messages` 是崩溃的直接触发点。
---
## 三、根因分析
### 崩溃路径
```
brain timer (30s)
→ _on_brain_tick()
→ 后台线程: asyncio loop → brain.decide_action()
→ _decide_proactive_action() → remind_water
→ QMetaObject.invokeMethod(_add_helper, "do_add", QueuedConnection)
→ 主线程: _do_add_message(text, is_self)
→ _refresh_messages()
→ deleteLater() 删除旧 QLabel
→ insertWidget() 添加新 QLabel
→ QLayout::invalidate() 向上传播到整个控件树
→ QWebEngineView 收到 resize/repaint 事件
→ Chromium compositor 尝试合成 WebGL 内容
→ 访问因 deleteLater 而处于无效状态的 GPU 资源
→ SIGSEGV
```
### 关键矛盾
- `Live2D WebGL 渲染` + `聊天布局修改` 两者单独运行时都稳定
- 两者同时启用时,**任何消息添加**brain timer, scheduler, 用户输入)都会触发 `_refresh_messages`,从而在 WebGL compositor 活跃时修改 Qt 控件树
- `deleteLater()` + `insertWidget()` 的组合导致以下竞态:
1. Qt 删除待销毁控件的内部引用
2. Chromium compositor 在同一帧尝试合成 WebGL 内容
3. compositor 访问已被删除的 Qt 内部对象
### 为什么首帧渲染后不立即崩溃
首帧渲染发生在 ~t=10s。此时没有消息触发 `_refresh_messages`。第一次可触发崩溃的 `_refresh_messages` 是:
- t=11s: scheduler test_reminder如果启用
- t=30s: brain timer remind_water
前者在测试中被禁用,后者精准对应崩溃时间。
---
## 四、修复方案
### 核心修复:`_refresh_messages` 控件复用
**原理:** 避免在 WebGL compositor 活跃时调用 `deleteLater()` / `insertWidget()` 修改 QLayout。
**实现:**
- 维护气泡 QLabel 缓存池 `_bubble_widgets`
- `_refresh_messages` 仅更新已有控件的 `setText()`, `setStyleSheet()`, `show()`/`hide()`
- 新控件仅在池容量不足时**一次性创建**(此时 WebGL 尚未渲染首帧)
- 完全消除 `deleteLater()` 调用
### 辅助修复
| 修复 | 文件 | 作用 |
|------|------|------|
| 纹理缩放至 1024×1024 | `viewer.html` | GPU 内存从 128MB 降至 8MB |
| 无 mipmap | `viewer.html` | 减少 33% 纹理内存 |
| QWebChannel JS 桥接 | `viewer.html` + `qwebchannel.js` | JS→PY 调试探针 |
| --no-sandbox --in-process-gpu | `main.py` | GPU 进程稳定性 |
| `gl.finish()` 在首帧后 | `viewer.html` | 确保 GPU 操作完成后再发信号 |
---
## 五、验证结果
| 测试场景 | 时长 | 结果 |
|----------|------|------|
| brain timer 禁用 | 2 min+ | 无崩溃 |
| brain timer 启用 + 控件复用 | 2 min+ | 无崩溃,消息正常显示 |
---
## 六、残余问题(已知)
### 12 个 QObject::setParent 警告(无害)
```
QObject::setParent: Cannot set parent, new parent is in a different thread (x12)
```
来自 QWebEngineView 内部 Chromium 线程在初始化 GPU surface proxy 时的跨线程 reparent。Qt WebEngine 6.x 内部实现细节,不影响稳定性。
### Live2D 白屏
如果 WebGL 上下文创建失败或纹理加载失败Live2D 区域显示为透明/白色。当前通过 `test_rect_ok` 探针验证 WebGL 管线正常。
---
## 七、相关文件清单
| 文件 | 修改 |
|------|------|
| `ui/pet_window.py` | `_refresh_messages` 控件复用;`_scroll_to_bottom` 直接引用;`showEvent` 显式 override |
| `assets/live2d/march7/viewer.html` | QWebChannel 集成纹理缩放Burst 渲染模式WebGL 错误检查 |
| `ui/live2d_view.py` | `cleanup()` 简化;`onJsMessage` 调试日志;`playMotion`/`setExpression` 按需渲染 |
| `main.py` | Chromium flags: `--no-sandbox --in-process-gpu --disable-gpu-rasterization --ignore-gpu-blocklist` |
| `agent/scheduler.py` | (无最终修改,仅测试期间禁用 test_reminder) |
---
## 八、经验教训
1. **不要在有 GPU compositor 的控件树中调用 `deleteLater()` + `insertWidget()`**。Chromium 的合成器持有对其 Qt 控件的内部引用;在合成器活跃时销毁/创建控件会导致悬空指针。
2. **`--in-process-gpu` 虽然避免了跨进程 IPC 崩溃,但会使 GPU 崩溃直接杀死整个进程**。更好的做法是:先通过探针验证 WebGL 管线正常,然后减少 GPU 操作频率。
3. **控件复用总是比删除+重建更安全**。对于聊天消息列表等动态内容,维护一个控件池并更新文本/可见性,而不是销毁和重建 QLabel。