Files
EzVibe/docs/investigation/segfault-solution.md
e2hang 798e5c2f7d checkpoint: segfault root cause analysis + Live2D stability fixes
- _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
2026-05-23 13:33:58 +08:00

7.5 KiB
Raw Permalink Blame History

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 渲染线程的子 QObjectGPU 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 驱动异常。


三、修复方案

修复 1Chromium 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。

修复 3WebGL 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;
    ...
}

// 修复后:用独立 writeIdxNaN 替换为 (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-762showEvent 为空函数体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 诊断过程记录