- _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
7.5 KiB
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 同步机制存在已知缺陷:
- GPU sandbox (seccomp-bpf) 限制了某些 GPU 驱动需要的系统调用 → GPU 进程被 silently kill
- Command buffer IPC 在 GPU 进程死亡/重启后进入无效状态 →
WaitForGetOffsetInRange检测到并报错 - 浏览器进程尝试访问已失效的 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 顶点数据处理缺陷
// 修复前
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)
# 在 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)
# 修复前:先创建(父=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)
// 修复前
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 诊断过程记录