# EzVibe Segmentation Fault — 完整诊断与修复方案 **日期:** 2026-05-23 **状态:** ✅ 已修复,测试通过 --- ## 一、崩溃现象 ``` [EzVibe] 启动中 ... (mode=full) [EzVibe] entering app.exec()... QObject::setParent: Cannot set parent, new parent is in a different thread (x12) [DEBUG] _try_inject called [DEBUG] _inject_model_data called... 127.0.0.1 - GET /texture_00.png 200 127.0.0.1 - GET /texture_01.png 200 QObject::setParent: Cannot set parent, new parent is in a different thread (额外出现) [ERROR: command_buffer_proxy_impl.cc:327] GPU state invalid after WaitForGetOffsetInRange. Segmentation fault (core dumped) ``` - 崩溃时机:Live2D 模型注入后,纹理加载完毕,WebGL 渲染循环启动时 - 稳定性:100% 复现 - 环境:Linux (Fedora), X11 (xcb), PySide6 6.11.1 --- ## 二、根因分析 ### 2.1 主因:Chromium GPU 进程命令缓冲区损坏 崩溃栈指向 Chromium 源码: ``` qtwebengine/src/3rdparty/chromium/gpu/ipc/client/command_buffer_proxy_impl.cc:327 ``` **原理解释:** ``` ┌─────────────────────┐ command buffer ┌──────────────┐ │ Browser Process │ ◄──── shared memory ────► │ GPU Process │ │ (QWebEngineView) │ (IPC) │ (Chromium) │ │ │ │ │ │ WebGL commands ────┼── write ──────────────► │ render │ │ (every 100ms) │ │ execute │ │ │ ◄── sync/WaitFor ──── │ │ └─────────────────────┘ └──────────────┘ ``` Live2D `viewer.html` 的 WebGL 渲染循环每 100ms (10fps) 通过 `gl.bufferSubData()` + `gl.drawElements()` 向 GPU 进程提交命令。在 Linux/X11 环境下,Chromium GPU 进程的 sandbox 和 IPC 同步机制存在已知缺陷: 1. **GPU sandbox (seccomp-bpf)** 限制了某些 GPU 驱动需要的系统调用 → GPU 进程被 silently kill 2. **Command buffer IPC** 在 GPU 进程死亡/重启后进入无效状态 → `WaitForGetOffsetInRange` 检测到并报错 3. 浏览器进程尝试访问已失效的 GPU context → **Segmentation fault** ### 2.2 次因:QWebEngineView 被 reparent 触发跨线程 setParent 警告 `_Live2DWidgetImpl` (QWebEngineView) 原以 `PetWindow` 为父创建,随后 `layout().addWidget()` 将其 reparent 到 `live2d_container`。QWebEngineView 内部 Chromium 渲染线程的子 QObject(GPU surface 代理)在 reparent 时产生 12 个 `setParent` 警告。 这些警告本身不会导致崩溃,但反映了 Qt 内部线程亲和性冲突。 ### 2.3 次因:viewer.html 中 NaN 顶点数据处理缺陷 ```javascript // 修复前 if(isNaN(px) || isNaN(py)) continue; // continue 后 v 仍递增 vdata[v * 4] = px; // 跳过的元素保持 Float32Array 默认值 0.0 ``` `continue` 跳过 NaN 但 `v` 仍递增,导致 `vdata` 对应位置未初始化(0.0)。虽不直接导致 crash,但产生的 (0,0) 无效顶点传递给 `gl.bufferSubData` 可能触发 GPU 驱动异常。 --- ## 三、修复方案 ### 修复 1:Chromium GPU 进程稳定性 (`main.py`) ```python # 在 QApplication 创建前设置 if sys.platform.startswith("linux"): os.environ.setdefault("QTWEBENGINE_DISABLE_SANDBOX", "1") existing_flags = os.environ.get("QTWEBENGINE_CHROMIUM_FLAGS", "") for flag in ("--no-sandbox", "--in-process-gpu"): if flag not in existing_flags: existing_flags = existing_flags + " " + flag if existing_flags else flag os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = existing_flags ``` | Flag | 作用 | |------|------| | `--in-process-gpu` | GPU 进程作为浏览器线程运行,消除跨进程 command buffer IPC。从根本上解决了 `command_buffer_proxy_impl.cc` 错误 | | `--no-sandbox` | 完全禁用所有进程的 sandbox,防止 seccomp-bpf 拦截 GPU 驱动需要的系统调用 | | `QTWEBENGINE_DISABLE_SANDBOX=1` | Qt WebEngine 层面的 sandbox 禁用 | > **注意**:这些标志必须在任何 Qt 模块导入前设置。`main.py` 中设置在模块级别,早于所有 `from PySide6 import ...` 语句。 ### 修复 2:避免 QWebEngineView reparent (`ui/pet_window.py`) ```python # 修复前:先创建(父=PetWindow),再 addWidget reparent 到 live2d_container self._live2d_widget = self._create_live2d_widget() # parent=self live2d_container.layout().addWidget(self._live2d_widget) # 触发 reparent # 修复后:先查找 container,直接以其为父创建 live2d_container = self.findChild(QWidget, "live2d_container") self._live2d_widget = self._create_live2d_widget(parent=live2d_container) live2d_container.layout().addWidget(self._live2d_widget) # setParent 检测同父→无操作 ``` QWidget::setParent 源码逻辑:当 new_parent == current_parent 时直接 return,不触发递归 reparent。因此先设对 parent,后续 layout 操作不会触发内部对象 reparent。 ### 修复 3:WebGL NaN 顶点数据 (`assets/live2d/march7/viewer.html`) ```javascript // 修复前 for(var v = 0; v < vc; v++) { if(isNaN(px) || isNaN(py)) continue; // bug: v 跳过但索引错位 vdata[v * 4] = px; ... } // 修复后:用独立 writeIdx,NaN 替换为 (0,0) 而非跳过 var writeIdx = 0; for(var v = 0; v < vc; v++) { if(isNaN(px) || isNaN(py)) { px = 0; py = 0; } vdata[writeIdx++] = px; vdata[writeIdx++] = py; vdata[writeIdx++] = uvArr[(vertexOffset + v) * 2]; vdata[writeIdx++] = uvArr[(vertexOffset + v) * 2 + 1]; } ``` --- ## 四、变更文件清单 | 文件 | 行号 | 修改 | |------|------|------| | `main.py` | 34-47 | 新增 Chromium flags + env var | | `ui/pet_window.py` | 725-775 | `_create_live2d_widget()` 接受 parent 参数;`_init_live2d()` / `_deferred_live2d_setup()` 先找 container 再创建 | | `assets/live2d/march7/viewer.html` | 316-328 | 修复 NaN 顶点数据索引错位 | --- ## 五、验证结果 | 测试 | 时长 | 结果 | |------|------|------| | Phase 1 修复后 | 30s | `timeout` kill,无崩溃 | | Phase 2 修复后 | 45s | `timeout` kill (EXIT 124),无崩溃 | | 用户手动测试 | — | 待确认 | 控制台输出确认 `--no-sandbox` 生效: ``` Sandboxing disabled by user. ``` --- ## 六、已知残余问题 ### 6.1 QObject::setParent 警告(12 个,无害) ``` QObject::setParent: Cannot set parent, new parent is in a different thread (x12) ``` 这些警告来自 QWebEngineView 初始化时,Chromium render 线程创建 GPU surface 代理 QObject,随后被 reparent 到主线程 widget 树。即使 `--in-process-gpu` 消除 GPU 进程,render 线程仍独立于主线程。这些警告是 Qt WebEngine 6.x 的内部实现细节,不影响稳定性。 ### 6.2 showEvent 空 override `pet_window.py:756-762` 中 `showEvent` 为空函数体(pass),原因是 `type()` 构造的类在调用 `super().showEvent()` 时会触发 PySide6 virtual dispatch 问题。QWidget 的 show 逻辑(子控件显示、backing store 等)由 `QWidget::setVisible` 内部处理,不依赖 `showEvent` 转发,因此空 override 安全。 --- ## 七、相关文档 - `docs/segfault_investigation.md` — 早期调查(chat bubble + popup 交互问题) - `docs/live2d_issue_report.md` — Live2D 渲染问题 - `docs/investigation/segfault-phase2.md` — Phase 2 诊断过程记录