- _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
172 lines
6.6 KiB
Markdown
172 lines
6.6 KiB
Markdown
# 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。
|