# 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。