# EzVibe Segfault — 完整根因分析与最终修复 **日期:** 2026-05-23 **状态:** ✅ 已修复 --- ## 一、崩溃现象 多种崩溃表现,随时间演变: | 阶段 | 表现 | 时机 | |------|------|------| | 初始 | `GPU state invalid after WaitForGetOffsetInRange` + segfault | Live2D 纹理加载后立即崩溃 | | Phase 2 | `QObject::setParent` 警告 (x23) + segfault | first_frame_ok 后 ~20-30 秒 | | Phase 3 | `QObject::setParent` 警告 (x12) + `startTimer` 警告 + segfault | first_frame_ok 后 ~20-30 秒 | | Phase 4 | `Cannot create children for parent in different thread` + segfault | 消息到达时立即崩溃 | 每次崩溃表现不同,但都发生在 **scheduler/brain timer 触发 `show_reminder` → `_refresh_messages`** 时。 --- ## 二、诊断过程 ### 阶段 1:GPU 命令缓冲区损坏 - 添加 `--no-sandbox --in-process-gpu` Chromium flags - 结果:GPU 错误消失,但 segfault 仍在(延迟出现) ### 阶段 2:连续 WebGL 渲染循环 - 将 `setInterval(100ms)` 连续渲染改为 burst 模式 - 结果:`setParent` 警告从 23 减少到 12,首帧渲染成功 ### 阶段 3:QWebChannel 调试探针 - 引入 `qwebchannel.js` + `[JS→PY]` checkpoint 消息 - 定位:Live2D 初始化完全成功(WebGL, Shader, Model, Textures, Buffers, First Frame 全部通过) - 崩溃发生在 `first_frame_ok` 后 20-30 秒 ### 阶段 4:隔离测试 - 禁用 brain timer + test_reminder → **稳定运行 2 分钟+** - 确认:崩溃触发点为 `show_reminder` → `_refresh_messages` ### 阶段 5:QTextBrowser 揭示真相 - 将 QScrollArea+QLabel 替换为 QTextBrowser - 崩溃变为:`Cannot create children for a parent that is in a different thread` - **关键线索**:QTextDocument 在主线程,但 `setHtml()` 从其他线程调用 --- ## 三、根因 ### 一行代码的 Bug `ui/pet_window.py` 第 284 行(修复前): ```python # ❌ bug: QApplication.instance().thread() 和 container.thread() 都是主线程 if self._QtWidgets.QApplication.instance().thread() != self._container.thread(): ``` **`QApplication.instance().thread()` 返回主线程。`self._container.thread()` 也返回主线程。两者是同一个 QThread 对象,比较结果永远为 False。** 导致:从 scheduler 后台线程调用 `_do_add_message` → `_refresh_messages` → **在非主线程执行 Qt Widget 操作**。 ### 崩溃机制 ``` scheduler 线程 (ezvibe-scheduler) → show_reminder(message) → add_message(text) → 线程检查: main_thread != main_thread → False ✗ → _do_add_message(text) ← 在 scheduler 线程执行! → _refresh_messages() → deleteLater() / insertWidget() / setHtml() → 跨线程修改 Qt Widget 树 → 内部状态损坏 → SIGSEGV ``` ### 为什么症状多样化 - `deleteLater()` 跨线程 → `QObject::setParent` 警告 + 延迟 segfault - `insertWidget()` 跨线程 → 布局级联失效 + `QBoxLayout::setGeometry` crash - `setHtml()` 跨线程 → QTextDocument `Cannot create children` + 立即 crash - 同一时刻的 Qt 内部状态不同 → 每次崩溃栈不同 ### 为什么 Live2D/WebEngine 似乎是个因素 - 禁用 Live2D 时,QWebEngineView 不存在 → 控件树更简单 → 跨线程损坏不致命 - 启用 Live2D 时,QWebEngineView 的 Chromium compositor 持有 GPU 资源 → 控件树损坏触发 compositor 访问无效指针 → segfault - **Live2D 放大了症状,但不是根因** --- ## 四、修复 ### 一行修复 ```python # ✅ 正确:比较当前执行线程与 container 所在线程 if self._QtCore.QThread.currentThread() != self._container.thread(): ``` `QThread.currentThread()` 返回**当前正在执行代码的线程**,而非 QApplication 所在的线程。 ### 辅助优化 | 修改 | 文件 | 作用 | |------|------|------| | QScrollArea+QLabel → QTextBrowser | `pet_window.py` | 用 `setHtml()` 替代控件管理,消除 deleteLater/insertWidget | | 纹理缩放 1024×1024, 无 mipmap | `viewer.html` | GPU 内存 128MB→8MB,消除 GPU 内存压力 | | QWebChannel 集成 + 调试探针 | `viewer.html`, `live2d_view.py` | JS→PY 消息追踪 | | Chromium flags | `main.py` | `--no-sandbox --in-process-gpu --disable-gpu-rasterization --ignore-gpu-blocklist` | | 托盘图标 | `pet_window.py` | 32×32 蓝色 pixmap,修复 "No Icon set" | --- ## 五、变更文件 | 文件 | 关键修改 | |------|---------| | `ui/pet_window.py` | `QThread.currentThread()` 修复;QTextBrowser 替代 QScrollArea | | `assets/live2d/march7/viewer.html` | QWebChannel;纹理缩放;burst 渲染;gl.finish | | `ui/live2d_view.py` | onJsMessage 调试;playMotion/setExpression 按需渲染 | | `main.py` | Chromium flags | | `agent/scheduler.py` | test_reminder 恢复启用 | --- ## 六、经验教训 1. **`QApplication.instance().thread()` ≠ `QThread.currentThread()`**。前者返回 QApplication 所在的线程(始终是主线程),后者返回当前执行代码的线程。对于线程安全判断,必须使用后者。 2. **QTextBrowser 比 QScrollArea + QLabel 更适合消息列表**。QTextBrowser 内部管理内容渲染,不需要创建/删除 QWidget 子控件,避免了 layout cascade 问题。 3. **GPU/WebGL 崩溃可能是症状而非根因**。当 Qt 控件树因跨线程操作而损坏时,任何涉及控件树遍历的操作(包括 WebEngine compositor)都可能触发崩溃。 4. **调试探针比猜测更有效**。QWebChannel checkpoints 精准定位了崩溃发生在 first_frame_ok 之后,而 QTextBrowser 的显式报错最终揭示了线程问题。